Greedy: The Algorithm That Never Looks Back
Every algorithm we have met so far does its thinking before it commits — merge sort plans the whole split, a balanced tree fusses over every insert. This week's character does the opposite. A greedy algorithm grabs whatever looks best right now and moves on. No undo, no second thoughts. Sometimes that is provably perfect. Sometimes it is confidently, badly wrong. By Thursday you will know how to tell the two apart — and you will have watched last week's heap earn its keep inside greedy's greatest hit, Huffman coding.
A scheduling lab where three greedy strategies fight over one conference room — pick a strategy, watch it grab meetings one at a time, and find the days that make two of the three embarrass themselves. And a Huffman builder — merge the two rarest letters again and again, watch a code tree grow, and watch the bill in bits shrink against plain fixed-width codes. Every number is computed live. Play with them while you read.
Session planHow we will spend the two sessions
Tuesday, October 20 — Session 14
Thursday, October 22 — Session 15
Section 1The algorithm that never looks back
You already run greedy algorithms all day. A cashier handing you 41 cents of change doesn't sit down and plan — they grab the biggest coin that fits, then do it again: quarter, dime, nickel, penny. At the supermarket you pick the checkout line that looks shortest right now. Packing a trunk, biggest suitcase first. Nobody computes anything. Everybody grabs.
A greedy algorithm builds a solution one piece at a time, and at every step it takes the choice that looks best right now. It never goes back. No undo, no swaps later, no "what if I had…". Each choice is final the moment it is made.
As pseudocode, the whole family fits in four lines — and it will look like this all week, whatever the problem:
Notes 04 and 05 kept insisting that algorithms have personalities, and greedy's is easy to name: the sprinter. It is blazing fast — usually one sort and one pass, so about n log n — and it is almost embarrassingly easy to write. The question is never whether greedy is fast. It always is. The question is whether the answer it sprints to is right. Sometimes yes, provably, every time. Sometimes it is catastrophically wrong — and it will never notice, because noticing would require looking back, and looking back is the one thing it refuses to do.
One problem where greedy face-plants in public (coins, §2). One where it is perfect (scheduling meetings, §3). The simple test that separates the two cases (the swap test, §4). And then greedy's masterpiece — Huffman coding, the compression trick inside zip files — built live with the heap from Notes 07 (§5). The skill you leave with: given a new problem, decide whether grabbing is safe before you trust the grabber.
Section 2Coins: a win and a face-plant
Back to the cashier. The rule is "fewest coins possible," and the greedy instinct is "always grab the biggest coin that still fits." With US coins — 25, 10, 5, 1 — making 41 cents goes: grab 25 (16 left), grab 10 (6 left), grab 5 (1 left), grab 1. Four coins, and you cannot do better. In fact, with US coins, grab-the-biggest gives the fewest coins for every amount. The instinct is not just fast here; it is perfect.
Now move to a strange little country whose coins are 1, 3 and 4, and make 6. Greedy grabs the biggest: a 4 (2 left). Nothing bigger than 1 fits anymore, so: 1, then 1 again. Three coins. But 6 = 3 + 3. Two coins — and the best answer never touches the 4 at all. Greedy's very first grab, the one that looked best, was exactly the move that ruined everything after it. And it will never know, because it never looks back.
It was the same algorithm both times. Greedy being right is a property of the problem, not of the idea. "Grab the best" is neither a good algorithm nor a bad one — it is a gamble whose odds are set by the shape of the problem you point it at. So the real work this week is never writing the greedy code (four lines, one sort). The real work is deciding whether this problem is the kind that keeps greedy honest. That is what §4 is for.
Greedy grabs the 7 (3 left), then 1, 1, 1 — four coins. The best answer is 5 + 5 — two coins. The very first grab did the damage: taking the 7 leaves 3, and 3 can only be built from pennies. The 7 looked best and was poison. Notice how small this counterexample is — when greedy fails, it usually fails on examples tiny enough to find by hand, which is excellent news for §6's checklist.
Section 3One room, many meetings
Here is the week's headline problem, and it is as ordinary as problems get. Your company has one conference room and a pile of booking requests, each with a start time and an end time. Overlapping meetings can't share the room. Your job: approve as many meetings as possible. Not the most hours of use — the most meetings. (Two quick ground rules: back-to-back is fine — a meeting ending at 5 and one starting at 5 don't clash. And times in this section are just hours numbered 0 to 12.)
This smells greedy: go down the list, grab meetings one at a time, skip anything that clashes with what you've already approved. But "grab the best one" needs a definition of best, and that is where the fun starts. Three candidates sound reasonable. All three are wrong — and each one dies by a single small counterexample, which after §2 is exactly how you should expect a greedy idea to die.
- Shortest first? Short meetings leave more room for everyone else… right?
- Earliest start first? First come, first served — start filling the day from the morning.
- Fewest conflicts first? Genuinely clever: take the meeting that clashes with the fewest others, since it does the least damage.
The winner: earliest finish time
The rule that actually works sounds almost too dumb to be it: always take the meeting that ends soonest (among those that don't clash with what you've taken). Not the shortest, not the first to start — the first to get out of the way.
Here is why that is the right instinct. Once you approve a meeting, the only thing it leaves behind for the rest of the day is one number: when the room frees up. Everything still to come cares about that number and nothing else. So the best possible gift to your future self is the smallest one — a room that frees up as early as possible. The meeting that ends soonest leaves the most day behind.
Watch it run on a packed Tuesday — eight requests, including an all-day offsite and a web of overlaps. Sort by finishing time and sweep:
| Next by finish time | Runs | Room free at | Decision |
|---|---|---|---|
| Kickoff | 1–3 | 0 | fits — take it, room free at 3 |
| Design review | 2–4 | 3 | starts at 2, room busy — skip |
| One-on-one | 3–5 | 3 | fits — take it, room free at 5 |
| Demo | 5–7 | 5 | fits — take it, room free at 7 |
| Hiring sync | 4–8 | 7 | starts at 4 — skip |
| Offsite | 0–9 | 7 | starts at 0 — skip |
| Retro | 7–10 | 7 | fits — take it, room free at 10 |
| Wrap-up | 10–12 | 10 | fits — take it. Five approved. |
Five meetings, and no schedule can fit six into this day (that is §4's job to convince you of). Now drive it yourself — and more importantly, drive the losers. The three preset days below include the counterexamples from the figure, scaled up to real schedules.
Some things to try:
- On A packed Tuesday, run all three strategies (or press Compare all three). Earliest start approves the Offsite first and gets 2 meetings; earliest finish gets 5.
- On The short straddler, run shortest first. Its very first grab — the shortest meeting of the day — pokes into both workshops and caps it at 2, when 3 fit.
- On The day-eater, earliest start gets exactly one meeting approved. All day, one meeting.
- Now notice what never happens: earliest finish never loses. Not on these three days, not on any day you could invent. The reason is the next section.
Always. The sweep only ever skips a meeting that clashes with something already approved — and this meeting clashes with nothing, so when its turn comes in the sorted list, it is taken. A conflict-free meeting is free money, and earliest-finish never walks past free money. (Check the losers too: shortest-first and fewest-conflicts also always take it, and even earliest-start does. Being unable to pass up free money is table stakes; the hard part is the meetings that do clash.)
Section 4The swap test
"It never lost in the demo" is not a reason — §2's coin greedy never loses on US amounts you happen to try either, until the day the coins change. Here is the actual reason earliest-finish is safe, in plain sentences. No formulas, and this is not a formal proof — it is "here's why you should believe it," which is the style this course uses on purpose.
Suppose a perfect scheduler — someone with infinite patience who tried every combination — hands you their plan, and it approves more meetings than greedy's. Look at the first meeting in their plan. Greedy's first pick is the earliest-ending meeting in the whole pile, so it ends no later than theirs. Swap greedy's first pick in place of theirs. Did anything break? No: the new first meeting ends at the same time or earlier, so the room frees up at the same time or sooner, so every later meeting in their plan still fits exactly as before. Same number of meetings, nothing lost.
Now do it again for the second meeting, and the third, and so on down the line. Each swap replaces one of their choices with greedy's choice and never loses a meeting. When you run out of swaps, their plan has become greedy's plan — same length as it ever was. So greedy's plan is exactly as big as the perfect plan. The perfect scheduler worked all night and tied the sprinter.
Take any plan at all, and try to swap its choices, one at a time, for greedy's choices. If every single swap can be made without the plan getting worse, then step by step any plan turns into greedy's plan at no cost — so no plan beats greedy, and greedy is safe. If you can't make the swaps work, stop trusting greedy and go hunt for a counterexample instead; it's usually tiny (coins: {1, 3, 4} making 6). That's the whole test. (Trivia: textbooks call this an exchange argument. Same thing, stuffier suit.)
Run the test on the dead ideas from §3 and feel it fail. Shortest-first: swap the short straddler in place of a long meeting — now the plan is worse, because the straddler blocks two slots where one was blocked before. The swap "gets worse," the test jams, and sure enough a counterexample exists. The test failing doesn't just warn you — it usually points straight at the counterexample.
(a) Yes. The argument never needed "earlier" — only "no later." Ends at the same time: the room frees at the same moment, everything still fits, nothing got worse. Ties are fine. (b) No — and this matters. The friend's plan in (a) was also perfect; greedy just found a best plan, not the best plan. The swap test promises greedy is never beaten. It never promises greedy is unique.
Section 5Huffman coding: greedy's masterpiece
In the 1830s, Samuel Morse faced a compression problem before anyone called it that: tapping out letters costs time, so which letters deserve the short taps? His answer: count how often each letter shows up in English and give the common ones the short codes — E, the most common letter, is a single dot. That instinct, done perfectly, is Huffman coding, and a version of it is working inside every zip file, JPEG and MP3 on your laptop.
Here is the setup. You want to send (or store) a message built from a small alphabet — say the letters A, B, C, D, E — using as few bits as possible. The no-thought answer is fixed width: 5 letters need 3 bits each (2 bits only give you 4 patterns), so A=000, B=001, C=010, and so on. A 20-letter message costs exactly 60 bits, no matter what it says. Fixed width ignores the one thing Morse noticed: if E appears 8 times and D appears once, E should be cheap and D can afford to be expensive.
The catch: codes you can actually decode
So give E a short code — say just 0 — and give rare letters longer ones. But now there's a trap. Suppose E = 0 and A = 00. You receive 0000. Is that EEEE? AA? EAE? The message arrives as one unbroken stream of bits — there are no spaces — so the codes themselves must make it obvious where one letter ends and the next begins.
A set of codes is prefix-free if no letter's code is the beginning of another letter's code. E = 0 and A = 00 breaks the rule (E's code is how A's code starts). E = 0 and A = 10 obeys it. Prefix-free codes decode themselves: read bits until what you've read matches a code — that match is certain, because nothing longer could have started that way. No spaces needed, no guessing, ever.
Prefix-free codes and binary trees are the same thing wearing different clothes. Put the letters at the leaves of a binary tree; a letter's code is the path from the top down to it, writing 0 for each left turn and 1 for each right. No letter can be a prefix of another, because no leaf sits on the path to another leaf. Decoding is a walk: start at the top, follow the bits, and every time you land on a letter, write it down and jump back to the top. And the cost is staring at you: a letter's code length is just its depth in the tree. Common letters want to live near the top. Rare letters can be banished to the basement.
The greedy build: merge the two rarest
So the problem became: build a tree where the frequent letters sit high and the rare ones sit low, minimizing total bits — each letter's count times its depth, summed. David Huffman (as a grad student, dodging a final exam by solving the professor's open problem) found the greedy that does it perfectly:
Read what it is doing: the two rarest things get pushed down together, underneath a new parent — one level deeper, where rare things belong. The bundle then competes in the pool by its combined weight, so a pile of rare letters can eventually stand up to one common letter. And "repeatedly pull out the two smallest, push one back" should set off an alarm from Notes 07: that is a priority queue, and this is the moment the heap finally pays rent. Push and pop are the only moves Huffman ever makes.
Watch it run on a 20-letter message with counts A:3, B:2, C:6, D:1, E:8.
| Merge | Two rarest in the pool | New bundle | Pool afterwards |
|---|---|---|---|
| 1 | D (1) and B (2) | [DB] — weight 3 | A:3 · [DB]:3 · C:6 · E:8 |
| 2 | A (3) and [DB] (3) | [A·DB] — weight 6 | C:6 · [A·DB]:6 · E:8 |
| 3 | C (6) and [A·DB] (6) | [C·A·DB] — weight 12 | E:8 · [C·A·DB]:12 |
| 4 | E (8) and [C·A·DB] (12) | the whole tree — weight 20 | done |
Read the codes off the final tree: E = 0, C = 10, A = 110, D = 1110, B = 1111. Prefix-free by construction. Now the bill for our 20-letter message: E pays 8×1, C pays 6×2, A pays 3×3, D pays 1×4, B pays 2×4 — that is 8 + 12 + 9 + 4 + 8 = 41 bits, against 60 bits fixed-width. A third of the message, gone, and the decoder needs no spaces, no table of lengths, nothing but the tree. This is (a piece of) how zip, PNG, JPEG and MP3 think: find what's common, make it cheap.
Add up the weights of the four merges: 3 + 6 + 12 + 20 = 41. The merge weights are the bill. That's no coincidence: every merge pushes everything inside it one level deeper, and one level deeper costs each letter inside one extra bit per appearance — so a merge of weight w adds exactly w bits. Greedy keeps each merge as light as possible, which is exactly keeping the bill small. That's also the one-line swap test for Huffman: if any best tree had a rare letter sitting above a common one, swapping the two letters would shrink the bill — so in a best tree, the rarest live deepest, which is precisely who greedy merges first.
Some things to try:
- Step through A lopsided message and watch the bill: each press adds exactly the new bundle's weight. Final bill: 41 against 60.
- Switch to All letters equal. With nothing to exploit, the savings shrink — but don't vanish. (Fixed width wastes patterns: 3 bits give 8 codes and we only need 5. Huffman never wastes.)
- One letter hogs it: predict E's code length before pressing anything. Then watch the bill — less than half of fixed width.
- After any finished build, check the code table: no code is the start of another. That's the tree doing it, not luck.
0111010. No spaces, no hints — just the walk.Walk from the top: 0 lands on E immediately — write E, back to the top. 1, 1, 1, 0 walks down to D — write D. 1, 0 lands on C. The stream is EDC, and at no point did you guess: every landing was forced. That is the prefix rule working. Try to produce a second valid reading — you can't, because no code is the start of another, so the first match is always the only match.
Never. Suppose some "best" tree has D higher (shorter code) than E. Swap the two letters, leaving the tree's shape alone. E's 8 appearances now each pay the cheaper price and D's single appearance pays the expensive one — the bill strictly shrinks, so the tree wasn't best after all. That's the swap test again, just aimed at trees instead of schedules: in any best tree, rarer letters sit at least as deep as commoner ones. Which is exactly the order greedy buries them in.
Section 6When to reach for greedy
Greedy is the first thing you should try on any optimization problem, and the last thing you should trust. Here is the honest checklist, in the order you should run it:
- Can you even say "best right now" in one sentence? "Ends soonest." "Biggest coin." "Rarest pair." If every candidate rule you write down feels arbitrary, the problem may not have a greedy shape at all.
- Hunt for a counterexample first. Five minutes, tiny inputs — three coins, four meetings. When greedy fails, it almost always fails small (§2's killer was "make 6"). Finding one ends the conversation: greedy is dead, move on.
- No counterexample? Run the swap test. Take an imaginary rival plan and try the swaps: can every choice be swapped for greedy's choice without the plan getting worse? If yes, greedy is safe — ship it and enjoy the n log n. If the swap jams, the jam usually shows you the counterexample you missed in step 2.
And when greedy fails? That's not a dead end — it's next week. The deep reason greedy loses at {1, 3, 4} coins is that the value of grabbing the 4 depends on what you do afterwards, and greedy refuses to think about afterwards. The fix is an algorithm that grabs nothing until it has quietly worked out the answer to every smaller version of the problem first: dynamic programming — greedy that keeps receipts. It solves the coin problem, every coin set, perfectly. That's Notes 09, and Project 3 will eventually make you race the two against each other on the same problem.
To file greedy into the personality gallery from Notes 04 and 05: insertion sort is the hometown kid, brilliant on nearly-sorted days; merge sort the dependable professional, same n log n rain or shine; quicksort the gambler. Greedy is the sprinter — fastest in the field, no memory, no regrets, and either exactly right or wrong with total confidence. Your job, always, is to check which — before the race, with the swap test, because the sprinter will never check for you.
Section 7The code
Both of this week's winners are shorter than their explanations — that is the point of greedy. First, the room scheduler: one sort, one sweep, ten lines.
def best_schedule(meetings): """Approve as many non-overlapping meetings as possible. meetings: a list of (start, end) pairs. Returns the approved pairs.""" by_finish = sorted(meetings, key=lambda m: m[1]) # earliest finish first approved = [] room_free_at = None for start, end in by_finish: if room_free_at is None or start >= room_free_at: approved.append((start, end)) # the room is empty: take it room_free_at = end # and it's busy until this ends # otherwise it clashes with something approved: skip, never reconsider return approved tuesday = [(0, 9), (1, 3), (2, 4), (3, 5), (4, 8), (5, 7), (7, 10), (10, 12)] print(best_schedule(tuesday)) # [(1, 3), (3, 5), (5, 7), (7, 10), (10, 12)] — five meetings, §3's answer
And Huffman — about twenty lines, and the heap from Notes 07 does the heavy lifting. Note the arrival-counter trick in the tuples: it's the exact same tie-breaker trick from Notes 07's priority-queue section, keeping the heap from ever having to compare two trees.
import heapq def huffman(freq): """freq: dict of letter -> how often it appears. Returns dict of letter -> its code, as a string of 0s and 1s.""" # the pool: one entry per letter. The middle number is an arrival counter # so the heap never has to compare two trees (the Notes 07 tie trick). pool = [(f, i, {"letter": c}) for i, (c, f) in enumerate(sorted(freq.items()))] heapq.heapify(pool) # the heap finally pays rent next_id = len(pool) while len(pool) > 1: f1, _, a = heapq.heappop(pool) # the rarest thing in the pool f2, _, b = heapq.heappop(pool) # the second rarest bundle = {"left": a, "right": b} # merge them under one parent heapq.heappush(pool, (f1 + f2, next_id, bundle)) next_id += 1 codes = {} def read_off(node, path): # walk down: left adds 0, right adds 1 if "letter" in node: codes[node["letter"]] = path or "0" # a one-letter alphabet gets "0" else: read_off(node["left"], path + "0") read_off(node["right"], path + "1") read_off(pool[0][2], "") return codes freq = {"A": 3, "B": 2, "C": 6, "D": 1, "E": 8} # 20 letters total codes = huffman(freq) print(codes) # {'A': '110', 'B': '1111', 'C': '10', 'D': '1110', 'E': '0'} bits = sum(freq[c] * len(codes[c]) for c in freq) print(bits, "bits, vs", 3 * sum(freq.values()), "fixed-width") # 41 vs 60
Why ties don't matter (and why your output might differ from a friend's)
Twice in our example the pool held a tie — A and [DB] both weighed 3, then C and [A·DB] both weighed 6. Break those ties the other way and you get a different tree with different codes. Is one wrong? No: the total bill lands on 41 bits either way. Huffman guarantees the best possible total, not one unique tree. So if your code prints different strings than a classmate's, compare the bit totals, not the codes.
This is also why real file formats (zip's DEFLATE, for one) ship a few extra rules that force everyone's tie-breaks to agree — the compressor and the decompressor must build the identical tree from the same counts.
Section 8Before next session
- Review Notes 08. Then close the page and explain to a friend, in plain words: why "ends soonest" wins the room and "shortest" doesn't; what the swap test says and what it buys you; and how Huffman decides who gets the short codes. If you can do that, you have the week.
- Project 1 — Race Your Sort — was due last night (Monday, October 19, 11:59 PM). If yours is in: well done, results and a leaderboard of race screenshots come back this week. If it isn't: the late policy is 10% per calendar day, so today beats tomorrow — get it in, and email me if something went sideways.
- Project 2 is next. The spec lands in the same small-and-fun format as Project 1 — watch the course page for it.
- In the demos: run all three strategies on all three days in Demonstration 1 and make sure you can narrate why each loser loses. In Demonstration 2, before pressing anything on "One letter hogs it," predict E's code length — then check.
- Optional challenge: write ten lines of Python that race greedy coin-change against brute force on every amount from 1 to 50, for any coin set you give it. You now own a greedy-safety detector for coin systems. Try {1, 3, 4}, then try US coins.
Dynamic programming — greedy that keeps receipts. When grabbing fails, the fix is to answer every small version of the problem first, write the answers down, and never solve the same one twice. It cracks the {1, 3, 4} coin problem in a dozen lines, and then the knapsack, and then the "how similar are these two strings?" problem your spellchecker runs on every keystroke.