title: IPO
slug: ipo
difficulty: hard
leetcode_id: 502
leetcode_url: https://leetcode.com/problems/ipo/
categories:
  - arrays
  - heap
  - sorting
patterns:
  - heap
  - greedy

function_signature: "def find_maximized_capital(k: int, w: int, profits: list[int], capital: list[int]) -> int:"

test_cases:
  visible:
    - input: { k: 2, w: 0, profits: [1, 2, 3], capital: [0, 1, 1] }
      expected: 4
    - input: { k: 3, w: 0, profits: [1, 2, 3], capital: [0, 1, 2] }
      expected: 6
  hidden:
    - input: { k: 1, w: 0, profits: [1, 2, 3], capital: [1, 1, 2] }
      expected: 0
    - input: { k: 1, w: 2, profits: [1, 2, 3], capital: [1, 1, 2] }
      expected: 5
    - input: { k: 10, w: 0, profits: [1, 2, 3], capital: [0, 1, 1] }
      expected: 6
    - input: { k: 1, w: 0, profits: [10], capital: [0] }
      expected: 10
    - input: { k: 2, w: 5, profits: [1, 1, 1, 1], capital: [0, 0, 0, 0] }
      expected: 7
    - input: { k: 3, w: 0, profits: [4, 5, 6], capital: [0, 0, 0] }
      expected: 15

description: |
  Suppose LeetCode will start its **IPO** soon. In order to sell a good price of its shares to Venture Capital, LeetCode would like to work on some projects to increase its capital before the **IPO**. Since it has limited resources, it can only finish at most `k` distinct projects before the **IPO**. Help LeetCode design the best way to maximize its total capital after finishing at most `k` distinct projects.

  You are given `n` projects where the i<sup>th</sup> project has a pure profit `profits[i]` and a minimum capital of `capital[i]` is needed to start it.

  Initially, you have `w` capital. When you finish a project, you will obtain its pure profit and the profit will be added to your total capital.

  Pick a list of **at most** `k` distinct projects from given projects to **maximize your final capital**, and return *the final maximized capital*.

  The answer is guaranteed to fit in a 32-bit signed integer.

constraints: |
  - `1 <= k <= 10^5`
  - `0 <= w <= 10^9`
  - `n == profits.length`
  - `n == capital.length`
  - `1 <= n <= 10^5`
  - `0 <= profits[i] <= 10^4`
  - `0 <= capital[i] <= 10^9`

examples:
  - input: "k = 2, w = 0, profits = [1,2,3], capital = [0,1,1]"
    output: "4"
    explanation: "Since your initial capital is 0, you can only start the project indexed 0. After finishing it you will obtain profit 1 and your capital becomes 1. With capital 1, you can either start the project indexed 1 or the project indexed 2. Since you can choose at most 2 projects, you need to finish the project indexed 2 to get the maximum capital. Therefore, output the final maximized capital, which is 0 + 1 + 3 = 4."
  - input: "k = 3, w = 0, profits = [1,2,3], capital = [0,1,2]"
    output: "6"
    explanation: "With initial capital 0, start project 0 (profit 1, capital becomes 1). With capital 1, start project 1 (profit 2, capital becomes 3). With capital 3, start project 2 (profit 3, capital becomes 6)."

explanation:
  intuition: |
    Imagine you're an investor with limited starting capital, and you want to grow your wealth as quickly as possible by completing projects. Each project requires a minimum investment (capital) to start, but once completed, you pocket the profit and can reinvest.

    The key insight is a **greedy observation**: at any point, among all the projects you *can* afford (those with `capital[i] <= current_capital`), you should always pick the one with the **highest profit**. Why? Because maximising your capital at each step opens up more project options for future rounds.

    Think of it like this: you're standing at the edge of a pool of projects. Some are within reach (you can afford them), others are too expensive. Among those within reach, grab the most valuable one. After completing it, your reach extends further, potentially unlocking more lucrative projects.

    This greedy strategy works because:
    1. Completing a project never decreases your capital (profits are non-negative)
    2. More capital means more options — you can only unlock projects, never lose access to previously affordable ones
    3. We want to maximise final capital, so greedily maximising at each step is optimal

    The challenge is efficiently finding the highest-profit affordable project at each step, which is where the **two-heap** (or sorted array + max-heap) approach shines.

  approach: |
    We solve this using a **Greedy approach with a Max-Heap**:

    **Step 1: Pair and sort projects by capital requirement**

    - Create pairs of `(capital[i], profits[i])` for each project
    - Sort these pairs by capital requirement in ascending order
    - This allows us to efficiently unlock projects as our capital grows

    &nbsp;

    **Step 2: Initialise tracking variables**

    - `current_capital`: Set to `w` (our starting capital)
    - `max_heap`: Empty heap to store profits of affordable projects (use negative values for max-heap in Python)
    - `project_index`: Set to `0` to track which projects we've processed

    &nbsp;

    **Step 3: Repeat up to k times (greedy selection)**

    - **Unlock projects**: While there are unprocessed projects and the next project's capital requirement is within our budget, push its profit onto the max-heap and move to the next project
    - **Select best project**: If the heap is non-empty, pop the maximum profit and add it to `current_capital`
    - **Early exit**: If no projects are affordable (heap is empty), we cannot proceed further — break early

    &nbsp;

    **Step 4: Return the result**

    - Return `current_capital` after completing up to `k` projects

    &nbsp;

    The sorting ensures we process projects in order of affordability, while the max-heap lets us instantly retrieve the highest-profit option among all currently affordable projects.

  common_pitfalls:
    - title: Brute Force Selection
      description: |
        A naive approach might scan all projects at each step to find the best affordable one:
        - For each of `k` rounds, scan all `n` projects
        - Check if affordable and track the maximum profit

        This results in **O(k × n)** time complexity. With `k` and `n` both up to `10^5`, this means up to 10 billion operations — causing **Time Limit Exceeded (TLE)**.

        The heap-based approach reduces this to O(n log n) for sorting + O(k log n) for heap operations.
      wrong_approach: "Linear scan for best affordable project each round"
      correct_approach: "Max-heap to track affordable projects"

    - title: Using a Min-Heap Instead of Max-Heap
      description: |
        Python's `heapq` module implements a min-heap by default. If you push profits directly, you'll get the *smallest* profit, not the largest.

        Always negate profits when pushing (`-profit`) and negate again when popping to get the actual maximum. Alternatively, use a max-heap wrapper.
      wrong_approach: "heappush(heap, profit)"
      correct_approach: "heappush(heap, -profit) and negate when popping"

    - title: Forgetting Early Termination
      description: |
        If at any point no projects are affordable (heap is empty after unlocking), continuing the loop is wasteful. More importantly, trying to pop from an empty heap causes an error.

        Always check if the heap is non-empty before popping. If empty, break out of the loop early — no further progress is possible.
      wrong_approach: "Always iterate k times"
      correct_approach: "Break early if no affordable projects remain"

    - title: Not Sorting by Capital
      description: |
        Without sorting by capital requirement, you'd need to scan all projects each round to find affordable ones. Sorting by capital allows linear unlocking as your capital grows — once a project is unaffordable, all subsequent ones (in sorted order) are too.
      wrong_approach: "Check all projects for affordability each round"
      correct_approach: "Sort by capital, unlock in order as budget grows"

  key_takeaways:
    - "**Greedy + Heap pattern**: When repeatedly selecting the 'best' option from a growing set, use a heap to efficiently track candidates"
    - "**Two-phase processing**: Sort by one criterion (capital) to control unlocking, heap by another (profit) to optimise selection"
    - "**Greedy validity**: This greedy approach works because completing projects only increases capital, never restricting future options"
    - "**Real-world analogy**: This mirrors investment strategies where you reinvest profits to access larger opportunities — a common pattern in scheduling and resource allocation problems"

  time_complexity: "O(n log n). Sorting takes O(n log n), and we perform at most n heap pushes and k heap pops, each O(log n)."
  space_complexity: "O(n). We store all projects as pairs and the heap can hold up to n profit values."

solutions:
  - approach_name: Greedy with Max-Heap
    is_optimal: true
    code: |
      import heapq

      def find_maximized_capital(k: int, w: int, profits: list[int], capital: list[int]) -> int:
          n = len(profits)

          # Pair projects as (capital_required, profit) and sort by capital
          projects = sorted(zip(capital, profits))

          current_capital = w
          max_heap = []  # Max-heap (using negative values)
          project_index = 0

          for _ in range(k):
              # Unlock all projects we can now afford
              while project_index < n and projects[project_index][0] <= current_capital:
                  # Push negative profit for max-heap behavior
                  heapq.heappush(max_heap, -projects[project_index][1])
                  project_index += 1

              # If no affordable projects, we're done
              if not max_heap:
                  break

              # Take the most profitable affordable project
              current_capital += -heapq.heappop(max_heap)

          return current_capital
    explanation: |
      **Time Complexity:** O(n log n) — Sorting dominates; heap operations are O(log n) each.

      **Space Complexity:** O(n) — Storage for sorted pairs and heap.

      We sort projects by capital requirement, then greedily select the highest-profit affordable project at each step using a max-heap. The sorted order ensures we efficiently unlock projects as our capital grows.

  - approach_name: Brute Force
    is_optimal: false
    code: |
      def find_maximized_capital(k: int, w: int, profits: list[int], capital: list[int]) -> int:
          n = len(profits)
          current_capital = w
          completed = [False] * n  # Track which projects are done

          for _ in range(k):
              best_profit = -1
              best_index = -1

              # Find the best affordable project
              for i in range(n):
                  if not completed[i] and capital[i] <= current_capital:
                      if profits[i] > best_profit:
                          best_profit = profits[i]
                          best_index = i

              # No affordable project found
              if best_index == -1:
                  break

              # Complete the best project
              completed[best_index] = True
              current_capital += best_profit

          return current_capital
    explanation: |
      **Time Complexity:** O(k × n) — For each of k rounds, scan all n projects.

      **Space Complexity:** O(n) — Boolean array to track completed projects.

      This approach scans all projects each round to find the best affordable one. While correct, it's too slow for large inputs where k and n approach 10^5. Included to illustrate why the heap optimisation is necessary.
