TL;DR
Greedy: sort by required capital + max-heap of unlocked profits β O((n + k) log n) time, O(n) space.
Approach 1 β Brute force: rescan all projects each round
The direct simulation: up to k times, scan every unused project, find the affordable one with the highest profit, take it. The greedy choice itself is already correct here (argued below) β the waste is purely in the scanning.
class Solution:
def findMaximizedCapital(
self, k: int, w: int, profits: list[int], capital: list[int]
) -> int:
n = len(profits)
used = [False] * n
for _ in range(k):
best = -1
for i in range(n):
if not used[i] and capital[i] <= w:
if best == -1 or profits[i] > profits[best]:
best = i
if best == -1: # nothing affordable -> nothing ever will be
break
used[best] = True
w += profits[best]
return w
Complexity: O(k Β· n) time, O(n) space.
With k and n both up to 10^5, that is ~10^10 comparisons β the constraints kill it outright.
Why greedy is safe (exchange argument): completing a project never reduces capital (profits are non-negative and the capital requirement is a threshold, not a payment). So taking the highest-profit affordable project now leaves you with at least as much capital as any other choice would β every project affordable under the alternative is affordable under the greedy too. Any optimal schedule can be exchanged, pick by pick, into the greedy one without losing profit.
Approach 2 β Sort by capital + max-heap of profits
The insight: the affordable set only ever grows (capital is monotonically non-decreasing). So instead of re-deriving it each round, maintain it incrementally: sort projects by required capital once, keep a pointer i to the first still-locked project, and after each capital increase slide the pointer forward, dumping each newly unlocked profit into a max-heap. A binary heap β the classic priority queue with O(log n) insert and O(log n) extract-max β hands us βhighest profit among unlockedβ instantly. Each project is pushed exactly once across all rounds.
import heapq
class Solution:
def findMaximizedCapital(
self, k: int, w: int, profits: list[int], capital: list[int]
) -> int:
projects = sorted(zip(capital, profits)) # by required capital
unlocked: list[int] = [] # max-heap of profits, negated
i = 0
n = len(projects)
for _ in range(k):
while i < n and projects[i][0] <= w:
heapq.heappush(unlocked, -projects[i][1])
i += 1
if not unlocked:
break # can't afford anything, and never will
w -= heapq.heappop(unlocked) # minus a negative: adds profit
return w
Walkthrough of k = 2, w = 0, profits = [1,2,3], capital = [0,1,1]:
projects = [(0,1), (1,2), (1,3)] after sorting by capital.
- Round 1: unlock while requirement
<= 0 β push profit 1, i = 1. Heap top is 1 β w = 1.
- Round 2: unlock while requirement
<= 1 β push profits 2 and 3, i = 3. Heap is now {2, 3} (profit 1 was consumed in round 1); top is 3 β w = 4.
k exhausted β return 4. β
And the degenerate third example (w = 1, both requirements 3): the while-loop unlocks nothing, the heap is empty on round 1, we break immediately and return 1. β
Complexity: sorting O(n log n); each project pushed/popped at most once and k pops β O((n + k) log n) time; O(n) space for the sort and heap.
Approach 3 β No-heap special case worth knowing
The insight: if w already meets the largest capital requirement (or after some prefix of picks it inevitably will, e.g. all requirements are 0), every project is unlocked and the problem collapses to βsum the k largest profitsβ β solvable by sorting profits descending, no heap at all. Interviewers sometimes probe with this simplification first.
import heapq
class Solution:
def findMaximizedCapital(
self, k: int, w: int, profits: list[int], capital: list[int]
) -> int:
if w >= max(capital): # everything affordable from the start
return w + sum(sorted(profits, reverse=True)[:k])
# general case: sort + max-heap (Approach 2)
projects = sorted(zip(capital, profits))
unlocked: list[int] = []
i = 0
n = len(projects)
for _ in range(k):
while i < n and projects[i][0] <= w:
heapq.heappush(unlocked, -projects[i][1])
i += 1
if not unlocked:
break
w -= heapq.heappop(unlocked)
return w
Walkthrough of k = 3, w = 0, profits = [1,2,3], capital = [0,1,2]: w = 0 < max(capital) = 2, so this shortcut does not apply and the heap method runs β rounds unlock and take 1, then 2, then 3, returning 6. The shortcut only fires when the affordability question is trivial.
Complexity: O(n log n) time, O(n) space when it applies.
Common pitfalls
- Treating
capital[i] as money that gets spent: it is only an entry threshold; w never decreases. Subtracting it is the classic mis-read.
- Forgetting the early
break when the heap is empty β without it, popping an empty heap raises, and looping k times pointlessly hides the βcapital can be permanently stuckβ case.
- Sorting by profit instead of by capital: the pointer-unlock trick only works when projects are ordered by their requirement.
- Re-scanning affordability from scratch each round (the O(kΒ·n) trap) β monotonically growing capital means unlocking is one-way and the pointer never moves backward.
Pattern takeaway
When a greedy repeatedly needs βthe best option among those currently eligibleβ and eligibility is monotone (once eligible, always eligible), the recipe is: sort by the unlock key, advance a pointer to feed newly eligible items into a heap keyed by desirability, and pop the heap each round. This sort-plus-heap tandem β two orders maintained at once, one by sorting and one by heap β is the signature move of scheduling-style heap problems.