502. IPO

Pick at most k projects to maximize your capital, where each project needs a minimum capital to start and pays a profit on completion. Sort projects by their capital requirement, sweep a pointer to unlock everything you can currently afford into a max-heap of profits, and greedily bank the richest affordable project each round.

HardGreedyMax-HeapSortingTypeScript

PROBLEM What we're solving

You start with capital w and may complete at most k of the given projects. To start project i you must already hold at least capital[i]; finishing it pays profits[i], which is added to your capital (you never spend the requirement — it's only a gate). Maximize the capital you end with. Example: k=2, w=0, profits=[1,2,3], capital=[0,1,1]. With w=0 only project 0 (needs 0) is affordable → take it, profit 1, now w=1. Now projects needing 1 are affordable → take the richer one (profit 3) → w=4. Answer: 4.

KEY IDEA Capital only grows, so be greedy on profit

Insight → because finishing a project adds to your capital and never subtracts, the set of affordable projects only ever grows. So a project you can afford now stays affordable forever — there is never a reason to defer it. Each round, among everything currently affordable, simply take the highest profit. A max-heap keyed on profit hands you that pick in O(log n), and a pointer over projects sorted by capital feeds the heap as your capital rises.

RECIPE Sort by capital, unlock, take the richest

  • 1 · Sort by capital. Pair each project as [capital, profit] and sort ascending by capital — this is the order in which projects become affordable as w climbs.
  • 2 · Unlock. Walk a pointer i forward, pushing every project with capital ≤ w into a max-heap of profits. The pointer never resets — once unlocked, always unlocked.
  • 3 · Take the best. If the heap is empty, no project is affordable and none ever will be → stop. Otherwise pop the maximum profit and add it to w. Repeat for up to k rounds.
Classic confusion → the heap is keyed on profit, but projects unlock by capital— two different arrays. Don't sort by profit and don't re-scan from the start each round: the capital pointer only moves forward, because rising capital can only unlock more, never fewer, projects.

COST Complexity & alternatives

Scan all projects each round
O(k · n)
Re-find the best affordable project every round.
Sort + max-heap
O((n + k) log n)
Each project enters the heap once; k pops.

Where the cost goes

Sorting is O(n log n). Across the whole run each project is pushed at most once and popped at most once, so heap work is O((n + k) log n). Space is O(n) for the paired/sorted projects and the heap. The brute-force O(k·n) scan is fine for small inputs but degrades when both k and n are large.

Pattern transfer →"sweep a sorted threshold pointer to feed a heap, then pop the best" is the same engine behind Minimum Cost to Hire K Workers, Maximum Performance of a Team, and Course Schedule III — anywhere items become eligible in sorted order and you greedily keep the best eligible ones.

RUN IT Unlock affordable projects, take the richest

step 0 / 6
STARTSorted the 3 projects by capital required (shown as $profit over c{capital}). Starting capital w = $0, budget of k = 2 projects.
1function findMaximizedCapital(
2 k: number,
3 w: number,
4 profits: number[],
5 capital: number[],
6): number {
7 const n = profits.length;
8
9 // Pair each project as [capital, profit], sorted by capital ascending.
10 const projects = Array.from({ length: n }, (_, i) => [capital[i], profits[i]]);
11 projects.sort((a, b) => a[0] - b[0]);
12
13 // Max-heap of the profits of every project we can currently afford.
14 const affordable = new MaxHeap();
15 let i = 0;
16
17 for (let round = 0; round < k; round++) {
18 // Unlock every project whose capital requirement is within reach.
19 while (i < n && projects[i][0] <= w) {
20 affordable.push(projects[i][1]);
21 i++;
22 }
23 // Nothing affordable now → nothing ever will be. Stop early.
24 if (affordable.size === 0) break;
25 // Greedily bank the most profitable affordable project.
26 w += affordable.pop();
27 }
28
29 return w;
30}
projects (sorted by capital)$1c0$2c1$3c1
affordable (max-heap of profits)
State
1 / 2
round
$0
capital w
0
i (unlocked)
0
|heap|
0
projects done
last profit
project being checkedaffordable (in heap)taken / heap topcurrent capital
slowfast

TYPESCRIPT The solution, annotated

ipo.ts
function findMaximizedCapital(
  k: number,
  w: number,
  profits: number[],
  capital: number[],
): number {
  const n = profits.length;

  // Pair each project as [capital, profit], sorted by capital ascending.
  const projects = Array.from({ length: n }, (_, i) => [capital[i], profits[i]]);
  projects.sort((a, b) => a[0] - b[0]);

  // Max-heap of the profits of every project we can currently afford.
  const affordable = new MaxHeap();
  let i = 0;

  for (let round = 0; round < k; round++) {
    // Unlock every project whose capital requirement is within reach.
    while (i < n && projects[i][0] <= w) {
      affordable.push(projects[i][1]);
      i++;
    }
    // Nothing affordable now → nothing ever will be. Stop early.
    if (affordable.size === 0) break;
    // Greedily bank the most profitable affordable project.
    w += affordable.pop();
  }

  return w;
}

Reading it block by block

Pair & sort by capital. Each project is zipped into [capital, profit] and sorted ascending on capital. Because capital only ever rises, this is exactly the order projects unlock in, so a single forward pointer suffices.
The affordable max-heap. affordable holds the profits of every project we can currently start, with the largest on top. The pointer imarks how far into the sorted list we've unlocked.
Unlock loop. Each round, advance i while projects[i].capital ≤ w, pushing those profits into the heap. The pointer never moves backward — a project affordable at capital w is still affordable at any higher capital.
Empty-heap early exit. If the heap is empty after unlocking, the cheapest remaining project costs more than w; since capital can't grow without doing a project, nothing will ever become affordable — break.
Take the richest. Pop the max profit and add it to w. That single pick is provably optimal for this round (any deferred project stays available), and the larger w may unlock more projects next round. After up to k rounds, w is the answer.
Complexity → Sort is O(n log n); each project is pushed/popped at most once and there are at most k pops → total O((n + k) log n) time, O(n) space.

INTERVIEWFollow-ups they'll ask

  • "Why is greedy correct here?" Capital is monotonically non-decreasing, so the affordable set only grows. Taking the max-profit affordable project now never blocks a future choice (an exchange argument: swapping in the larger profit never hurts).
  • "What if a project could be done more than once?"Then you'd repeatedly take the single most profitable affordable project — re-push it after popping, or just multiply once you find the global max affordable.
  • "What if completing a project could decrease capital (a cost)?"The monotonicity breaks; the affordable set can shrink, so simple greedy fails and you'd need DP or search.
  • "k is huge — say k ≥ n. Optimization?" You can do at most n projects, so the loop naturally stops when the heap empties; effectively you bank every project, largest-profit first.
  • "Can you avoid sorting?" Yes — use a second heap: a min-heap keyed on capital for locked projects, draining its top into the profit max-heap whenever capital ≤ w. Same O((n + k) log n), no explicit sort.

OPTIMAL Greedy

ipo.ts
function findMaximizedCapital(
  k: number,
  w: number,
  profits: number[],
  capital: number[],
): number {
  const n = profits.length;

  // Pair each project as [capital, profit], sorted by capital ascending.
  const projects = Array.from({ length: n }, (_, i) => [capital[i], profits[i]]);
  projects.sort((a, b) => a[0] - b[0]);

  // Max-heap of the profits of every project we can currently afford.
  const affordable = new MaxHeap();
  let i = 0;

  for (let round = 0; round < k; round++) {
    // Unlock every project whose capital requirement is within reach.
    while (i < n && projects[i][0] <= w) {
      affordable.push(projects[i][1]);
      i++;
    }
    // Nothing affordable now → nothing ever will be. Stop early.
    if (affordable.size === 0) break;
    // Greedily bank the most profitable affordable project.
    w += affordable.pop();
  }

  return w;
}
Complexity → Sort is O(n log n); each project is pushed/popped at most once and there are at most k pops → total O((n + k) log n) time, O(n) space.

ALT 1 Brute force — scan for the best affordable each round

O(k · n) time · O(n) space

No heap: each round, linearly scan every not-yet-done project for the most profitable one you can currently afford.

approach-2.ts
function findMaximizedCapital(
  k: number,
  w: number,
  profits: number[],
  capital: number[],
): number {
  const n = profits.length;
  const done = new Array<boolean>(n).fill(false);

  for (let round = 0; round < k; round++) {
    // Find the affordable, unfinished project with the highest profit.
    let best = -1;
    for (let i = 0; i < n; i++) {
      if (!done[i] && capital[i] <= w && (best === -1 || profits[i] > profits[best])) {
        best = i;
      }
    }
    // Nothing affordable → capital can't grow, so stop.
    if (best === -1) break;
    done[best] = true;
    w += profits[best];
  }

  return w;
}
Note → Dead simple and easy to get right under pressure, but every round rescans all n projects → O(k·n). Fine for small inputs; the heap version wins once k and n are both large.

ALT 2 Two heaps — no upfront sort

O((n + k) log n) time · O(n) space

Avoid sorting: keep locked projects in a min-heap keyed on capital, draining their tops into a profit max-heap as capital rises.

approach-3.ts
function findMaximizedCapital(
  k: number,
  w: number,
  profits: number[],
  capital: number[],
): number {
  const n = profits.length;

  // Locked: min-heap on capital required. Affordable: max-heap on profit.
  const locked = new MinHeap<[number, number]>((a, b) => a[0] - b[0]); // [capital, profit]
  const affordable = new MaxHeap<number>();
  for (let i = 0; i < n; i++) locked.push([capital[i], profits[i]]);

  for (let round = 0; round < k; round++) {
    // Move every now-affordable project from locked into affordable.
    while (locked.size > 0 && locked.peek()[0] <= w) {
      affordable.push(locked.pop()[1]);
    }
    if (affordable.size === 0) break;
    w += affordable.pop();
  }

  return w;
}
Note → Same asymptotics as sort + heap (building the locked min-heap is O(n)), and it streams projects in instead of sorting up front — handy if projects arrive online. Slightly more bookkeeping with two heaps.

MNEMONIC The one-liner

"Sort by cost, unlock what you can afford, pocket the fattest profit — repeat k times."

TRIGGERS When you see ___ → reach for ___

pick k items, each gated by a thresholdsort by threshold + max-heap on value
resource only grows after each pickgreedy: take the best currently eligible
eligibility expands as a value risesforward pointer feeds a heap (never reset)
nothing affordableempty heap → break early

SKELETON The reusable shape

skeleton.ts
// projects sorted by capital; affordable = max-heap on profit
projects.sort((a, b) => a.cap - b.cap);
let i = 0;
for (let t = 0; t < k; t++) {
  while (i < n && projects[i].cap <= w) {   // unlock
    affordable.push(projects[i].profit); i++;
  }
  if (affordable.size === 0) break;          // can't afford anything
  w += affordable.pop();                     // take richest
}
return w;

FLASHCARDS Tap to flip

Why does plain greedy work for IPO?
Finishing a project only adds capital, so the affordable set never shrinks. A project you can afford now stays affordable, so taking the max-profit affordable one each round is safe.
tap to flip
1 / 6
Answer to reveal explanations. No penalty for retries.Score: 0 / 9
QUESTION 01
Why is the greedy "take the most profitable affordable project" optimal?
QUESTION 02
The max-heap is keyed on which value?
QUESTION 03
Overall time complexity of the sort + heap solution?
QUESTION 04
During the unlock loop, why does the pointer i never reset to 0?
QUESTION 05
For k=3, w=0, profits=[1,2,3], capital=[0,1,1], what is the final capital?
QUESTION 06
After unlocking, the heap is empty. What is the correct action?
QUESTION 07
Compared to a max-heap, what does the brute-force "scan every project each round" approach cost?
QUESTION 08
#502 · IPOYou start with w capital and may finish at most k distinct projects. Project i needs capital[i] on hand to start and adds profits[i] to your capital when finished. Return the maximum capital after at most k projects.Which algorithmic approach does this primarily use?
QUESTION 09
#502 · IPOYou start with w capital and may finish at most k distinct projects. Project i needs capital[i] on hand to start and adds profits[i] to your capital when finished. Return the maximum capital after at most k projects.Which implementation correctly solves it?