Pattern 10 · Optimization

Greedy

Make the locally optimal choice at each step and never look back. When it works, it beats DP on every axis — the hard part is knowing when it's actually safe.

time O(n) – O(n log n)space O(1)proof exchange argumentpractice 15 problems
01 core concept

Commit and move on

A greedy algorithm makes the best choice available right now, hoping the sum of local optima is the global optimum. It never reconsiders a past decision — no backtracking, no memo table. That's why it's usually O(n) or O(n log n) with O(1) extra space.

Greedy in action (coin change with denominations 25, 10, 5, 1): Amount: 41 cents Step 1: Take 25 (largest that fits) --> remaining: 16 Step 2: Take 10 --> remaining: 6 Step 3: Take 5 --> remaining: 1 Step 4: Take 1 --> remaining: 0 Total: 4 coins (and this IS optimal for US coins)
Greedy is not always right
Same algorithm, denominations [1, 3, 4], amount 6: greedy takes 4 + 1 + 1 = 3 coins, but the optimum is 3 + 3 = 2. General coin change needs DP. Greedy correctness depends on the structure of the input, not just the problem statement.

The cleanest interview example is Jump Game (LC 55): can you reach the last index if each value is your max jump length? The entire greedy state is one number — the farthest index reachable so far.

1 / 7walkthrough — jump game: track the farthest reach
2
3
1
1
0
4
farthest —
Each value is the max jump length from that index. Can we reach the last index? Greedy state: one number — the farthest index reachable. The amber region will show everything we can reach.
02 when it works

The two properties, and how to prove them

Greedy is only correct when both of these hold:

  • Greedy choice property — a locally optimal choice leads to a globally optimal solution. You never need to reconsider.
  • Optimal substructure — after making the greedy choice, what remains is a smaller instance of the same problem.

vsGreedy or DP?

aspectgreedydynamic programming
ApproachMake the best local choiceTry all options, remember results
Past decisionsNever reconsideredAll considered via table
TimeO(n) or O(n log n)O(n²) or O(n·m)
When to useLocal optimal = global optimalChoices interact
Proof needed?Yes (exchange argument)Just a correct recurrence
ExamplesInterval scheduling, Huffman coding, DijkstraCoin change (general), knapsack, edit distance
Rule of thumb
If greedy works, prefer it — simpler and faster. If you can't prove greedy, use DP.

P1Exchange argument

Show that swapping any non-greedy choice for the greedy choice does not worsen the solution. The classic: activity selection (pick the maximum number of non-overlapping intervals) — always pick the interval that ends earliest.

Proof sketch (activity selection — greedy picks earliest end): Suppose optimal solution O picks interval X instead of greedy's G at some step. Since G ends no later than X: - G doesn't conflict with anything X doesn't conflict with - Swapping X for G gives a solution that's at least as good Therefore greedy is optimal.

P2Greedy stays ahead

By induction, show the greedy solution is at least as good as any alternative after every step. If it's never behind, it can't lose at the end.

03 recognition

When to reach for it

  • "Find the minimum number of X to cover/complete something."
  • "Schedule tasks or intervals optimally."
  • "Maximize/minimize with a simple local rule."
  • The problem involves sorting, then making sequential choices.
  • The locally optimal move feels obviously correct — and you can argue why.
Pre-commit checklist
Before committing to greedy, ask: 1) Does sorting help — and what do I sort by? 2) What is the local rule at each step? 3) Can I construct a counterexample where greedy fails? 4) If greedy fails, is this actually a DP problem?
When NOT to use it
When choices interact — taking the best option now can block a better combination later (general coin change, knapsack, edit distance). If you can't prove the greedy choice with an exchange argument, or you can build a counterexample, switch to DP.
04 the playbook

Sort first, or track a running quantity

Nearly every greedy problem falls into one of two families: sort to make the greedy choice obvious, or track a running quantity in a single pass.

Can I SORT to make the greedy choice obvious? | +-- YES --> What do I sort by? | +-- End time (interval problems) | +-- Start time (meeting rooms) | +-- Ratio / weight (fractional knapsack) | +-- Size (fit small items first) | +-- NO --> Is there a natural greedy rule without sorting? +-- Track farthest reachable (jump game) +-- Track surplus (gas station) +-- Track last occurrence (partition labels)

G1Jump Game — track the farthest reach

jump_game.py — LC 55python
def can_jump(nums):
    farthest = 0

    for i in range(len(nums)):
        if i > farthest:
            return False          # Can't reach this index
        farthest = max(farthest, i + nums[i])

    return True

G2Jump Game II — BFS-like levels

Minimum number of jumps: treat each jump as a BFS level. Extend the farthest reach while walking the current level; when you hit the level's end, you must jump.

nums = [2, 3, 1, 1, 4] Jump 0: positions reachable = {0} range: [0, 0] Jump 1: from 0, reach up to 2 range: [1, 2] Jump 2: from 1, reach up to 4 --> done! range: [3, 4] Answer: 2 jumps
jump_game_ii.py — LC 45python
def jump(nums):
    jumps = 0
    curr_end = 0       # End of current jump range
    farthest = 0       # Farthest we can reach

    for i in range(len(nums) - 1):
        farthest = max(farthest, i + nums[i])

        if i == curr_end:          # Must jump now
            jumps += 1
            curr_end = farthest

    return jumps

G3Gas Station — track the surplus

If sum(gas) >= sum(cost), a solution exists. Greedy: whenever the running surplus goes negative, none of the stations so far can be the start — restart from the next one.

gas = [1, 2, 3, 4, 5] cost = [3, 4, 5, 1, 2] net = [-2,-2,-2, 3, 3] total = 0, so solution exists Tracking surplus: start=0: surplus = -2 (negative, reset) start=1: surplus = -2 (negative, reset) start=2: surplus = -2 (negative, reset) start=3: surplus = 3 start=3: surplus = 3 + 3 = 6 Answer: start = 3
gas_station.py — LC 134python
def can_complete_circuit(gas, cost):
    if sum(gas) < sum(cost):
        return -1

    start = 0
    surplus = 0

    for i in range(len(gas)):
        surplus += gas[i] - cost[i]
        if surplus < 0:
            start = i + 1       # Restart from next station
            surplus = 0

    return start

G4Partition Labels — track the last occurrence

Extend each partition to include the last occurrence of every character seen in it. When the scan index reaches the current partition's end, cut.

s = "ababcbacadefegdehijhklij" 0 1 2 0123456789012345678901234 Last occurrence: a:8 b:5 c:7 d:14 e:15 f:11 g:13 h:19 i:22 j:23 k:20 l:21 Partition 1: start at 0, must include a's last (8) b(5), c(7) fall within 8 --> end at 8. Size = 9. Partition 2: start at 9, must include d's last (14) e extends to 15; f(11), g(13) within --> end at 15. Size = 7. Partition 3: start at 16, must include h's last (19) i extends to 22, j to 23; k, l within --> end at 23. Size = 8. Answer: [9, 7, 8]
partition_labels.py — LC 763python
def partition_labels(s):
    last = {ch: i for i, ch in enumerate(s)}
    result = []
    start = end = 0

    for i, ch in enumerate(s):
        end = max(end, last[ch])    # Extend partition
        if i == end:                # Reached the end of partition
            result.append(end - start + 1)
            start = end + 1

    return result

G5Task Scheduler — the frequency formula

The most frequent task dictates the minimum time — lay it out first with cooldown gaps, then fill.

tasks = [A,A,A,B,B,B], cooldown n=2 Most frequent: A and B, both freq=3 Arrange most frequent first with gaps: A _ _ A _ _ A A B _ A B _ A B Formula: intervals = (max_freq - 1) * (n + 1) + count_of_max_freq_tasks answer = max(intervals, len(tasks)) -- can't be less than total tasks
task_scheduler.py — LC 621python
def least_interval(tasks, n):
    from collections import Counter

    freq = list(Counter(tasks).values())
    max_freq = max(freq)
    max_count = freq.count(max_freq)   # How many tasks have max frequency

    # Minimum from formula
    intervals = (max_freq - 1) * (n + 1) + max_count

    # Can't be fewer than total tasks
    return max(intervals, len(tasks))

G6Min Arrows — sort by end point

The interval-scheduling greedy choice in its purest form: sort balloons by end point, shoot at the end of the first unburst balloon, and let one arrow take out everything that overlaps.

1 / 6walkthrough — min arrows to burst balloons (sorted by end)
[1,6]
[2,8]
[7,12]
[10,16]
arrows 0
Input [[10,16],[2,8],[1,6],[7,12]], sorted by end point. Greedy rule: shoot every arrow at the end of the first unburst balloon — the latest x that still bursts it.
min_arrows.py — LC 452python
def find_min_arrow_shots(points):
    if not points:
        return 0

    points.sort(key=lambda x: x[1])   # Sort by end
    arrows = 1
    curr_end = points[0][1]

    for start, end in points[1:]:
        if start > curr_end:           # No overlap, need new arrow
            arrows += 1
            curr_end = end

    return arrows

G7Boats to Save People — sort + two pointers

people = [1, 2, 2, 3], limit = 3 Sorted: [1, 2, 2, 3] L R Pair (1,3): 1+3=4 > 3, take 3 alone. R-- Pair (1,2): 1+2=3 <= 3, take both. L++, R-- Pair (2): alone. Boats: 3
boats.py — LC 881python
def num_rescue_boats(people, limit):
    people.sort()
    left, right = 0, len(people) - 1
    boats = 0

    while left <= right:
        if people[left] + people[right] <= limit:
            left += 1             # Pair them
        right -= 1                # Heaviest always boards
        boats += 1

    return boats

G8Merge Intervals — sort by start

Sort by start, then merge overlaps greedily. This one is a whole pattern of its own — see Pattern 13.

merge_intervals.py — LC 56python
def merge(intervals):
    intervals.sort()
    merged = [intervals[0]]

    for start, end in intervals[1:]:
        if start <= merged[-1][1]:
            merged[-1][1] = max(merged[-1][1], end)
        else:
            merged.append([start, end])

    return merged

refComplexity reference

problemtimespace
Jump GameO(n)O(1)
Jump Game IIO(n)O(1)
Gas StationO(n)O(1)
Partition LabelsO(n)O(1)*
Task SchedulerO(n)O(1)*
Min Arrows (Balloons)O(n log n)O(1)
Boats to Save PeopleO(n log n)O(1)
Merge IntervalsO(n log n)O(n)
Non-overlapping IntervalsO(n log n)O(1)

* constant extra space (26 letters or similar bounded alphabet).

05 common mistakes

Where greedy goes wrong

Mistake 1 — assuming greedy works without proof
Always verify with counterexamples or an exchange argument before writing code. "It feels right" is how you lose 20 minutes to coin change [1, 3, 4].
Mistake 2 — wrong sort key
End time for interval scheduling (max non-overlapping, min arrows). Start time for merging. Swapping these silently gives wrong answers on some inputs.
Mistake 3 — forgetting edge cases
Empty input, a single element, all-same values — greedy loops are terse and skip these easily.
Mistake 4 — off-by-one in Jump Game II
Iterate up to len(nums) - 1, not len(nums) — reaching the last index must not trigger an extra jump.
Mistake 5 — skipping the Gas Station total check
If sum(gas) < sum(cost), no solution exists — check this first, or the surplus scan returns a bogus start index.
Mistake 6 — confusing the complement problems
"Minimum intervals to remove" and "maximum non-overlapping to keep" are complements — same greedy, answer is n - kept for the removal version.
10 practice

Practice ladder

In this order — easiest first. Tick each one as you solve it, and re-solve anything that took more than 25 minutes two days later.

15 problems
easyLC 455
Sort both sides, then two pointers - feed the greediest child the smallest cookie that fits.
easyLC 121
One pass tracking the running minimum price so far.
easyLC 1005
Flip the most negative value first; spend any leftover flips on the smallest absolute value.
mediumLC 122
Bank every positive day-to-day delta - local gains sum to the global optimum.
mediumLC 55
No sort — track the farthest reachable index.
mediumLC 45
BFS-like levels — jump when you hit the current range's end.
mediumLC 134
Surplus tracking — reset the start when it goes negative.
mediumLC 763
Extend to the last occurrence of every char seen.
mediumLC 621
Frequency formula — most frequent task dictates the layout.
mediumLC 452
Sort by end point, shoot at the end.
mediumLC 881
Sort by weight + two pointers.
mediumLC 56
Sort by start, merge overlapping (see Pattern 13).
mediumLC 435
Sort by end point — max non-overlapping, count removals.
mediumLC 316
Stack + greedy — pop bigger chars that reappear later.
hardLC 135
Two-pass greedy: left-to-right, then right-to-left.