python/volume-3/chapter-39ch 39 / 64 · 8 min

39Greedy — when the local choice wins globally

In Chapter 38 we let two pointers sweep an array and turned a nested-loop search into a single honest pass. Greedy takes that same instinct to the edge: at every step, take the choice that looks best right now and never take it back. It sounds reckless — one path down the tree, no second-guessing — and for most problems it is. But for a surprising few it lands the globally best answer for almost no work. Here's the plan. We'll watch greedy nail the optimum, then watch it fail on a coin set you'll break with your own hands, then learn the one proof — the exchange argument — that tells the two apart. The whole way through we keep asking the single question that decides everything: can a chain of locally-best moves add up to the globally-best answer? By the end you'll look at a brand-new problem and say, before writing a line, whether the fast never-look-back move is a proof or a trap.

The greedy move: commit now, never look back When greedy lies: the coin that costs you The exchange argument: proving greedy right Huffman: the greedy algorithm in every file you own
scroll to begin

01The greedy move: commit now, never look back

Let's start with what every optimization problem really is underneath: a tree of choices. To make change for 6 units you could hand over a 4, then a 1, then a 1; or a 3 then a 3; or six 1s. Each sequence of picks is a path down a branching tree, and the leaves are the finished answers. Brute force (chapter 35) walks the whole tree and compares every leaf. It's correct, but explosive: a tree with a two-way choice at each of n steps has 2ⁿ leaves. Now watch what greedy does instead.

Greedy refuses to branch. At every node it applies one rule — "take the biggest coin that still fits", "take the meeting that finishes soonest" — commits to it, and descends. One path, root to leaf. No backtracking, no comparing alternatives. If the tree is n deep, greedy does O(n) work where brute force does O(2ⁿ). (Usually there's also an O(n log n) sort up front, to decide the order it considers things in.) The entire gamble is this: can a sequence of locally-best moves add up to the globally-best answer? When it can, you get an exponential speedup for free.

??????? start greedy: one path — O(n) brute force: every leaf — O(2ⁿ)
Fig — Brute force weighs all 2ⁿ leaves; greedy commits to one rule at each node and rides a single root-to-leaf path. The speed is not in doubt — only the correctness.
The greedy template — every greedy algorithm is this
1) Sort or arrange the options by a chosen greedy criterion. 2) Walk them once, and take each one if it doesn't break the rules. 3) Return what you took. The only creativity is choosing the right criterion — and proving it's safe.

So what decides whether "best now" equals "best overall"? Let's find a problem where it's obviously true, and one where it's disastrously false. Start with the disaster. →

02When greedy lies: the coin that costs you

Making change is the folk-tale example of greedy. To pay 87 cents, a cashier grabs a quarter, another quarter, another quarter, a dime, two pennies — the biggest coin that fits, every time. For the coins in your pocket that habit isn't just fast, it's optimal: it always uses the fewest coins. So people conclude greedy "usually works". Watch it break with a tiny change to the coin set.

coins.pypython
def greedy(coins, target):
    n = 0
    for c in sorted(coins, reverse=True):   # biggest coin first
        while target >= c:                  # grab it while it fits
            target -= c
            n += 1
    return n

def optimal(coins, target):                 # dynamic programming — chapter 40
    best = [0] + [float("inf")] * target
    for amt in range(1, target + 1):        # smallest sub-amount up
        for c in coins:
            if c <= amt:
                best[amt] = min(best[amt], best[amt - c] + 1)
    return best[target]

print(greedy([1, 3, 4], 6))   # 3   ->  4 + 1 + 1
print(optimal([1, 3, 4], 6))  # 2   ->  3 + 3

Line by line: greedy sorts the coins large-to-small, then for each denomination subtracts it as many times as it fits, counting coins. optimal is the honest answer. It builds up best[amt] — the fewest coins for every amount from 1 to the target — with each one reusing the smaller answers already computed. That's the dynamic programming of the next chapter; here it's just the referee. The last two lines are the verdict, and I ran them: greedy prints 3, optimal prints 2.

Greedy grabbed the 4 because it was biggest, then had to patch the leftover 2 with two 1s: 4 + 1 + 1, three coins. The right answer ignores the shiny 4 entirely: 3 + 3, two coins. The greedy criterion — "biggest first" — walked confidently into a worse leaf and, with no backtracking, never noticed. Across the first 100 targets with the set [1, 3, 4], greedy is beaten on 24 of them (6, 10, 14, 18, 22, …). It isn't occasionally unlucky; it is systematically wrong for this coin system.

Make 6 from coins 1 · 3 · 4 greedy — biggest first 4 1 1 = 3 coins ✗ optimal — fewest coins 3 3 = 2 coins ✓ The 4 looked best locally and poisoned the rest. Greedy can't un-grab it.
Fig — One greedy step (take the 4) forces two bad steps after it. Local best, global loss.
InteractiveGreedy vs optimal — find where greedy breaks
coins 1·3·4 — make 6 greedy optimal greedy is optimal here.
6
The US set never loses — it's a "canonical" system. Slide the target on 1·3·4 or 1·7·10 and watch greedy fall behind.
↺ The thing people get backwards
People treat greedy as a reliable default that "usually" works and occasionally slips. It's the reverse: greedy is a claim about a specific problem — "local optimum implies global optimum here" — that happens to be true for a lucky few problems and false for most. Fast never means correct. The 4-coin above is proof: instant, confident, and wrong.

If greedy is a claim, we need to test the claim. When it's true, there's a beautiful reason — and it comes with its own famous winning problem. →

03The exchange argument: proving greedy right

Here is the problem greedy was born for. You have one lecture hall and a pile of talks, each with a start and finish time, and overlapping talks can't share the room. Which talks do you accept to fit the most into the day? The winning rule is almost insultingly simple: always take the talk that finishes earliest among those that still fit. Not the shortest, not the earliest-starting — the earliest-finishing.

exchange.pypython
def schedule(acts):                       # acts: (name, start, finish)
    chosen, last_finish = [], -1
    for name, s, f in sorted(acts, key=lambda a: a[2]):   # by finish time
        if s >= last_finish:              # doesn't overlap what we took
            chosen.append(name); last_finish = f
    return chosen

acts = [("A",1,4),("B",3,5),("C",0,6),("D",5,7),
        ("E",3,9),("F",5,9),("G",6,10),("H",8,11)]
print(schedule(acts))     # ['A', 'D', 'H'] — 3 talks

The loop sorts by finish time, then walks once, keeping a talk only if it starts at or after the last kept talk ended. I ran it against a brute-force check of every one of the 2⁸ possible talk subsets: the biggest compatible set really is 3, and greedy finds it. Why does finishing-earliest win? Because it leaves the room free as early as possible, and that can never hurt — the more time left, the more room for what's next.

That intuition becomes a proof through the exchange argument, the master key for every greedy correctness claim. Take any optimal schedule. Look at its first talk versus greedy's first talk. Greedy's finishes no later — that's how greedy chose it. So swap the optimal's first talk for greedy's. The new first talk frees the room at least as early, nothing else overlaps, and the schedule is still valid and still the same size — still optimal. Repeat down the line, and you've morphed some optimal solution into greedy's, one step at a time, without ever losing a talk. So greedy's answer is optimal too. No choice greedy makes can be "traded up" to a better one — because the best solution can always be traded down to greedy's.

024681011 A ✓ B C D ✓ E F G H ✓ time → keep the earliest-finishing talk that still fits: A → D → H
Fig — Sorted by finish time, greedy grabs A (finishes at 4), skips everything overlapping it, grabs D (finishes at 7), then H. Three talks — provably the most possible.

Why earliest-finishing and not, say, shortest? Because a different rule fails the exchange test, and you can catch it with a three-talk counterexample. Take X(1–5), Y(4–7), Z(6–10). "Shortest first" grabs Y and blocks both X and Z, ending with 1 talk. Earliest-finish grabs X then Z — 2 talks. I ran both, and the numbers are exactly that. The criterion isn't decoration; it's the whole proof.

InteractiveThe explosion greedy refuses to look at
One lecture hall, 30 talks — fit the most in without overlaps a fast machine testing 1,000,000,000 (one billion) subsets every second 10¹⁰10²⁰10³⁰ BRUTE FORCE · weigh every possible subset 2³⁰ = 1.07 billion subsets to weigh GREEDY · one earliest-finish sweep 30 checks — one earliest-finish pass log scale · every tick = ×10 the work Brute force would grind through them for 1.1 seconds Greedy hands you the identical schedule in 30 nanoseconds Same schedule, same answer — greedy just never looks at the other 1.07 billion.
30
The bars are on a log scale — each tick to the right means ten times more work, so even side-by-side they hide the true gap. Read the times instead: at 30 meetings brute force needs about a second; slide to 100 and it needs thousands of times the age of the universe — while greedy never crosses a hundred nanoseconds, and lands on the exact same optimal schedule. That is what "an exponential speedup" buys you.
The deeper cut — the two properties every greedy proof needs
A greedy algorithm is provably optimal when the problem has both: (1) the greedy-choice property — some globally optimal solution contains the greedy first choice (this is what the exchange argument establishes: you can always swap it in), and (2) optimal substructure — after making that choice, the best solution to the leftover subproblem completes the best overall solution. Activity selection has both. So does Huffman coding, and Dijkstra, and the minimum-spanning-tree algorithms. Coin change with [1,3,4] fails the greedy-choice property — taking the 4 is not in any optimal solution for target 6 — which is precisely why the exchange step can't be done, and why greedy is wrong there.

One earliest-finish rule, one exchange proof, an exponential speedup. Now meet the greedy algorithm running silently inside almost every file on your disk. →

04Huffman: the greedy algorithm in every file you own

Text is stored as bits. The lazy scheme gives every character the same width — 8 bits in ASCII — whether it's a hyper-common e or a rare q. That's wasteful. Frequent symbols should get short codes, and rare ones can afford long codes, the way Morse code gives e a single dot. In 1952 a student named David Huffman found the provably shortest such code, and the algorithm is pure greed: repeatedly merge the two rarest symbols into one combined node, until a single tree remains.

huffman.pypython
import heapq

def huffman(freq):
    forest = [[w, i, {s}] for i, (s, w) in enumerate(freq.items())]
    heapq.heapify(forest)                    # a min-heap by weight
    code, nxt = {s: "" for s in freq}, len(freq)
    while len(forest) > 1:
        w1, _, s1 = heapq.heappop(forest)    # the rarest bundle
        w2, _, s2 = heapq.heappop(forest)    # the next rarest
        for s in s1: code[s] = "0" + code[s] # prepend a bit to each side
        for s in s2: code[s] = "1" + code[s]
        heapq.heappush(forest, [w1+w2, nxt, s1|s2]); nxt += 1
    return code

freq = {"a":45, "b":13, "c":12, "d":16, "e":9, "f":5}
code = huffman(freq)
bits = sum(freq[s] * len(code[s]) for s in freq)
print(bits, "vs", 3 * sum(freq.values()))   # 224 vs 300

The forest starts as one tiny tree per symbol, kept in a min-heap (a structure from earlier) so "the two rarest" is always two heappops. Each round fuses them: every symbol on the left branch gets a 0 prepended to its code, every symbol on the right a 1, and the merged node — its weight the sum of the two — goes back in the heap. After n−1 merges one tree is left, and each symbol's code is its path of 0s and 1s from the root. I ran it: the classic frequencies compress to 224 bits versus 300 for a fixed 3-bit code. a, the most common, got the 1-bit code 0, while rare f got 1100. That's a 25% saving, and it is provably the smallest any code can achieve for these frequencies.

01 01 01 01 01 100 55 25 30 14 a:45 c:12 b:13 d:16 f:5 e:9 codes = path from root: a=0 b=101 f=1100
Fig — Built bottom-up by merging the two rarest each step (f+e→14, c+b→25, 14+d→30, 25+30→55, a+55→100). Common a sits one hop from the root (1 bit); rare f sits four hops down (4 bits).
Wait —
a is the single bit 0, yet b is 101 and e is 1101. Reading a bare stream of bits with no commas, how does the decoder know where one letter stops and the next begins?

Because every symbol is a leaf, no code can be the start of another one. The instant the bits trace a path down to a leaf, that's the letter, and decoding jumps back to the root for the next. This prefix-free property is the quiet genius of building codes from a tree: the stream is unambiguous with no separators at all. That's not a lucky side effect of greed. It's guaranteed, because greedy only ever puts symbols at leaves.

Where you meet this — you used it to load this page
Huffman coding is inside ZIP, gzip and PNG (via DEFLATE), JPEG and MP3 (the entropy-coding stage), and the gzip-compressed HTTP responses your browser fetches all day. Every photo you send and every song you stream is riding a greedy algorithm a graduate student invented for a term paper — reportedly to get out of taking the final exam.

The knob below is why compression works at all. Skew the frequencies and watch the savings appear; flatten them and watch the savings vanish. It runs the real Huffman merge, live.

InteractiveSkew the letters — watch compression appear
Huffman: 3.000 bits/symbol (2424 bits total) saving vs fixed 3-bit code: 0.0%
0
Equal letters can't be compressed — a fixed code is already optimal. The more lopsided the frequencies, the more Huffman wins; that lopsidedness is called low entropy.
The deeper cut — why merging the two rarest is provably optimal
The exchange argument again. In the shortest possible code the two least-frequent symbols must sit as the two deepest leaves, and as siblings — because you can always swap any deepest leaf with a rarest symbol without increasing the total bit count (that's the exchange). So making them siblings first is safe, and merging them into one symbol of combined weight leaves a strictly smaller problem with the same structure (optimal substructure). Induction finishes it. The result reaches the entropy floor of the source to within less than one bit per symbol — Shannon's theoretical minimum for lossless coding.

Greedy commits to one choice and proves it never needed the others. Its rival keeps all the choices alive. Time to put them face to face. →

05Greedy vs dynamic programming: commit vs consider

The coin change split the world in two. Greedy took the biggest coin and moved on. The optimal function did something different: it computed the best answer for every smaller amount and combined them. It never committed early; it kept every sub-answer and reused them. That's dynamic programming (the next chapter), and the contrast is the cleanest way to understand both.

Greedy — commit

Makes one choice at each step by a fixed rule and never revisits it. One path down the tree. Fast (O(n) or O(n log n)), tiny memory. Only correct if the greedy-choice property holds — otherwise silently wrong, like [1,3,4].

DP — consider & remember

Explores every sub-choice but stores each subproblem's answer so it's solved once, not re-solved. Slower and hungrier (often O(n·target) time and memory). Always finds the optimum when the problem has optimal substructure.
greedy: one committed path O(n) · no memory DP: every subproblem, stored once 0 1 1 1 2 2 amt012345 best[amt] — reused, never recomputed O(n·target) · full table
Fig — Greedy spends nothing on memory because it never looks back. DP spends memory precisely so it never has to guess — it has already solved every piece.
The reflex to build
Try greedy first — it's the cheapest thing that could possibly work, and if you can prove it (exchange argument) you're done. If you can find even one counterexample, greedy is dead; reach for DP, which trades memory for a guarantee. The next chapter makes DP a repeatable procedure.

Two techniques, one decision. So how do you know, on a brand-new problem, which one you're holding — before you've bet the system on it? →

06How to know before you trust it

You'll rarely be told "this is a greedy problem". You'll be handed a goal — schedule the most jobs, wire the network for the least cable, route the packet the cheapest way — and you have to decide whether the greedy shortcut is a proof or a trap. Here is the drill. It's the same every time.

name a greedyrule exchange argument?and hunt a counterexample proof holds it breaks ship greedyO(n log n) use DP (ch 9)or backtracking
Fig — The whole discipline: state the rule, then attack it from both sides at once — try to prove it, try to break it. Whichever succeeds first tells you which tool to ship.

Name the greedy rule ("take the earliest-finishing", "merge the two rarest"). Then run two attacks in parallel. Prove it: can you show the greedy choice is always swappable into some optimal solution — the exchange argument? If yes, you're done — greedy is optimal and fast. Break it: at the same time, throw small inputs at it and compare against a brute-force referee, exactly as we did with the coins. A single counterexample kills greedy forever. You don't get to say "but it works most of the time". If it breaks, you already know the escape hatch: dynamic programming.

"It passed my tests" is not a proof
Greedy on [1,3,4] is correct for targets 1–5, then fails at 6. If your tests stopped at 5 you'd have shipped a bug that only fires on certain inputs in production. For greedy, either prove it or find the counterexample — passing a handful of cases proves nothing.

This chapter's real gift is a way of thinking, and it's bigger than algorithms. You use greedy reasoning every day: paying with the biggest bills, running errands nearest-first, packing the fullest boxes. It's a superb heuristic — a fast, good-enough rule of thumb. The 1% move is to hold two thoughts at once: reach for the simple local rule first, and in the same breath ask what it's ignoring. Greedy fails exactly when a locally-worse choice now buys a better position later — the 3 you skipped, the talk you passed up, the move that sets up the next three. Naming that failure mode is a life skill. It's why you sometimes take the smaller reward now to keep the bigger one open, and why "obviously best right now" always deserves one hard look before you commit.

Where else greedy quietly runs the world
GPS routing and internet packet routing lean on Dijkstra (the graphs chapter), a greedy frontier that always expands the closest unvisited place. Kruskal and Prim greedily wire minimum-spanning-tree networks — power grids, chip layouts, clustering. Your OS and CPU evict cache lines greedily (approximate LRU). Each is a local rule with a proof behind it — greed that was checked before it was trusted.

But the coin change that greedy botched still needs its right answer — and so do knapsacks, edit distance, and the longest path through a lattice of choices. The fix is to stop re-solving what you have already solved. The next chapter turns "remember your subproblems" into the most powerful technique in this whole volume: dynamic programming. →

PROGRAMS run them line by line — the same ideas, written the many ways Python offers
Chapter 39, in working code

Greedy is the boldest bet in algorithms: grab the best-looking option right now, never look back. These twelve tiny programs show you exactly when that nerve pays off, and when it walks you — fast and confident — straight into the wrong answer.

Grab the biggest — and where that nerve fails
The folk-tale greedy: pay a total with the fewest coins by always taking the largest that fits. On real coins it's provably optimal; change the coin set by one denomination and the same rule quietly loses.
Greedy is a claim — so test the claim
Greedy isn't a reliable default that 'usually works'. It's an assertion — local best implies global best — that's true for a lucky few problems and false for most. Measure exactly how often it holds.
Activity selection — the problem greedy was born for
One room, many overlapping talks, fit the most in. The winning rule is almost insulting: always take the talk that FINISHES earliest among those that still fit — and an exchange argument proves it optimal.
Huffman — the greedy algorithm in every file you own
Repeatedly merge the two rarest symbols into one node until a single tree remains. Frequent symbols land near the root with short codes, rare ones sink deep — the provably shortest code, and it's running inside ZIP, PNG, JPEG and every gzip response your browser fetches.
end of chapter 39 · six sections
iolinked.com
Written by Ajai Raj