Chaya·Edu EDU.CHAYA.DEV
AI 230 · Notes 09
Sessions 16–17 · Oct 27 & 29, 2026
Notes 09 · Week 9 · Sessions 16–17

Dynamic Programming: Greedy That Keeps Receipts

AI 230 · Introduction to Algorithms Prof. Yair Chaya Long Island University, Brooklyn Tuesday & Thursday, October 27 & 29, 2026 · 4:00–5:50 PM

Last week ended with greedy face-down on the floor: coins {1, 3, 4}, amount 6, and the sprinter confidently handing over three coins when two would do. This week we fix it — with the least glamorous idea in this course. Don't guess the first move. Answer every smaller amount first, write each answer down, and build the big answer out of the written ones. That habit has a fancy name, dynamic programming, and it cracks the coins, then a burglar's packing problem, then the "how alike are these two words?" question your spellchecker asks on every keystroke.

Two live demonstrations · marked ● in the contents

A heist, where you pack a 10-pound bag by hand and then press Show the notebook to watch the machine's table fill in and (probably) beat you. And a likeness grid, where you type any two words and watch the same take-it-or-don't logic find the longest thing they share. Every number is computed live. Play with them while you read.

Session planHow we will spend the two sessions

Tuesday, October 27 — Session 16

0:00
The rematch: {1, 3, 4} makes 6, solved by writing answers down (§1)
0:30
The two ingredients, and the Fibonacci horror story (§2)
1:00
Two flavors: a notebook on the recursion, or a table filled small-to-large (§3)
1:25
The heist begins: what's worth stealing, and why greedy packs badly (§4)

Thursday, October 29 — Session 17

0:00
Take it or don't: the knapsack notebook — heist lab (§4)
0:45
How alike are two words? — the likeness grid (§5)
1:25
The one-sentence recipe; greedy vs. DP, settled honestly (§6)
1:40
Project 3 — the spec is out this week (§7)

Section 1Unfinished business

The scene of the crime, one more time. The coins are 1, 3 and 4, the amount is 6, and the rule is "fewest coins." Greedy grabs the 4 — the best-looking first move — and is sunk: 4 + 1 + 1, three coins, when 3 + 3 does it in two. The problem wasn't the arithmetic. The problem was committing to the first grab without knowing what it does to the rest of the job.

So here is the fix, and it is embarrassingly honest: refuse to answer 6 until you have answered everything smaller. Work out the fewest coins for amount 0, then 1, then 2, and so on up — and write every answer in a notebook as you go. By the time you reach 6, the question has become easy, because any first grab leaves a smaller amount, and every smaller amount is already written down.

Watch it happen. Each line below uses only lines already written:

AmountThinkingFewest coinsFor example
0nothing to pay — zero coins, for free0—
1only a 1 fits: grab it, then amount 0 costs 011
2grab a 1, then amount 1 costs 121+1
3grab a 3, then amount 0 costs 0 — beats grabbing a 1 (1+2=3)13
4grab a 4, then amount 0 costs 014
5grab a 4 → amount 1 costs 1, total 2. Grab a 3 → amount 2 costs 2, total 3. Grab a 1 → amount 4 costs 1, total 2. Best: 224+1
6three possible grabs — see the picture below23+3
Amount 6 — try every first grab, then just look up the leftover: grab a 1 → 5 left, notebook says 2 → 3 coins grab a 3 → 3 left, notebook says 1 → 2 coins ✓ grab a 4 → 2 left, notebook says 2 → 3 coins the notebook so far: 0 1 2 1 1 2 2 make 0 make 1 make 2 make 3 make 4 make 5 make 6 — 1 1+1 3 4 4+1 3+3 Greedy answered 6 in one grab and got it wrong. The notebook answered 0–5 first — and couldn't miss.
The whole method in one picture. Amount 6 doesn't guess: it tries all three first grabs, looks up what the leftover costs in the notebook, and takes the cheapest total. The grab of a 3 wins — leftover 3 costs one coin, two coins total. Greedy's beloved 4 loses fair and square, with receipts.

Notice what happened to greedy's fatal flaw. Greedy committed to the 4 because it looked best and never checked what it did to the rest. The notebook also considers grabbing the 4 — but then it looks up what the leftover 2 actually costs, instead of hoping. No grab gets trusted; every grab gets billed. That is the whole trick, and it is why we're calling this greedy that keeps receipts.

And look at the amount-5 line: the notebook says 2 coins (4+1) — exactly what greedy would have said. Greedy isn't always wrong; it just can't tell you when it's right. The notebook can, because it checked everything. Here it is as code — a dozen lines, always right, for any coin set on Earth:

def fewest_coins(coins, amount):
    """Fewest coins that make the amount — any coin set, always right."""
    best = [0] + [None] * amount          # best[a] = fewest coins for amount a
    for a in range(1, amount + 1):        # answer every amount, small to large
        for c in coins:                   # try every possible first grab
            if c <= a and best[a - c] is not None:
                option = best[a - c] + 1  # the grab, plus the looked-up rest
                if best[a] is None or option < best[a]:
                    best[a] = option      # write the winner in the notebook
    return best[amount]

print(fewest_coins([1, 3, 4], 6))   # 2 — the 3+3 greedy could never see
Check yourself — The coins are {1, 4, 5} and the amount is 8. Greedy grabs the 5 and ends up with four coins (5+1+1+1). Build the notebook for amounts 0 through 8 by hand. What does it say, and which first grab wins at 8?

The notebook reads 0, 1, 2, 3, 1, 1, 2, 3, 2. At amount 8, the three grabs say: grab a 5 → 3 left, which costs 3, total 4; grab a 1 → 7 left, costs 3, total 4; grab a 4 → 4 left, costs 1, total 2. So 4 + 4, two coins. Same story as {1, 3, 4}: the biggest coin looked best and was poison, and only the notebook — which billed every option against written-down answers — could see it.

Section 2Write it down: the two ingredients

Strip away the coins and two things made that work. Every problem dynamic programming cracks has both, so learn them as a pair:

The two ingredients
  • The big answer is built from smaller versions of the same question. "Fewest coins for 6" is built from "fewest coins for 5, 3 and 2" — same question, smaller number. Not a different problem: the same problem, shrunk.
  • Write every answer down, so nothing is ever solved twice. Amount 2 gets asked about by amount 3, by 5, by 6… Solve it once, write it down, and every later question is a free lookup. The notebook habit is called memoization — yes, spelled without the r: "make a memo of it."

Why does the second ingredient matter so much? Because without the notebook, "built from smaller versions of itself" is a bomb. Here is the canonical horror story. The Fibonacci numbers are the simplest self-built sequence there is — each number is the sum of the two before it: 0, 1, 1, 2, 3, 5, 8, 13… The definition translates to code so directly it feels like cheating:

def fib(n):                        # the innocent-looking disaster
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

notebook = {}
def fib_memo(n):                   # the same thing, keeping receipts
    if n < 2:
        return n
    if n not in notebook:            # only compute what isn't written down
        notebook[n] = fib_memo(n - 1) + fib_memo(n - 2)
    return notebook[n]

Run the innocent version on fib(35) and your laptop goes quiet for several seconds. Why? Draw what it actually does:

5 4 3 3 2 2 1 2 1 1 0 1 0 1 0 gold = asked twice red = three times 15 calls to learn 5 numbers
Naive fib(5): the number in each circle is which Fibonacci is being asked for. fib(3) is worked out twice from scratch, fib(2) three times — 15 calls to learn 5 numbers, and the waste doubles with every level.
The notebook version fib(0) = 0 fib(1) = 1 fib(2) = 1 fib(3) = 2 fib(4) = 3 fib(5) = 5 each line = the two above, added once 6 lines. Done. fib(35)? 36 lines.
Same question with receipts: each Fibonacci is computed exactly once and then looked up forever after. The tree collapses into a straight column.
The horror story, in one picture. Left: the naive recursion re-derives the same answers over and over, and the re-deriving snowballs. Right: the notebook writes each answer once. The code differs by three lines; the running time differs by a factor of nearly a million.

Put numbers on it, because the numbers are the lesson. Naive fib(35) makes about 30 million calls — several seconds of Python — to learn 35 numbers. The notebook version computes each of those 35 numbers once: instant. Ask for fib(50) and naive needs about 41 billion calls — hours — while the notebook writes 51 lines. By fib(100), the naive call count has 22 digits: tens of thousands of years even at a billion calls per second, versus 101 notebook lines. (The waste snowballs because the number of calls grows exactly like the Fibonacci numbers themselves — the recursion re-creates its own explosion.)

The moral

The crime was never recursion. The crime was solving the same small problem millions of times and never noticing. The whole of dynamic programming is the refusal to do that: if the same small question keeps coming back, answer it once, write it down, look it up. Everything else this week is this one idea pointed at different problems.

Check yourself — In naive fib(6), how many separate times does the computer work out fib(2) from scratch? Use the fib(5) tree above rather than drawing the whole fib(6) tree.

Five times. fib(6) calls fib(5) and fib(4). From the tree: fib(5) works out fib(2) three times, and fib(4) — the left half of that tree — works it out twice. 3 + 2 = 5. And that pattern should look familiar: 2, 3, 5… the counts of wasted work are themselves Fibonacci numbers, which is exactly why the waste explodes at the same runaway rate as the thing being computed.

Section 3Two flavors, same dish

There are two ways to run the notebook, and you have now seen both without being told. They deserve names, because you will meet both in every DP you ever read:

Top-down and bottom-up
  • Top-down (recursion + a notebook): start from the big question and recurse downward, but check the notebook before computing anything and write into it afterward. That is fib_memo. You think about the problem the natural way and the notebook quietly kills the waste.
  • Bottom-up (fill a table, small to large): skip the recursion entirely. Start from the trivially small cases and fill the notebook in order, so that by the time any entry is being written, everything it needs is already there. That is fewest_coins from §1.

Here is coin change again, top-down this time, so you can see both flavors on the same problem:

def fewest_coins_topdown(coins, amount, notebook=None):
    """Same answers as §1's version — recursion plus a notebook."""
    if notebook is None:
        notebook = {0: 0}                # amount 0 costs 0 coins
    if amount in notebook:                # already answered? look it up
        return notebook[amount]
    best = None
    for c in coins:                       # try every first grab
        if c <= amount:
            rest = fewest_coins_topdown(coins, amount - c, notebook)
            if rest is not None and (best is None or rest + 1 < best):
                best = rest + 1
    notebook[amount] = best               # write it down before returning
    return best

Same answers, always. Same amount of real work, too: each flavor answers every amount from 0 to the target exactly once, so both do "number of amounts × number of coins" small steps — for coins {1, 3, 4} and amount 6, that's 7 amounts × 3 grabs, about twenty tiny lookups in all. The differences are cosmetic: top-down reads like the problem statement and skips amounts that never come up; bottom-up has no recursion to trace and is usually a touch faster in practice. Pick whichever one you can read back without squinting. In this course, bottom-up tables will usually win that contest, and they make better pictures — as you're about to see.

Check yourself — Bottom-up fewest_coins fills in amounts 0, 1, 2, … even if some of them could never be asked about. Isn't that wasted work? When would top-down's "only compute what's asked" actually matter?

For coins it barely matters — nearly every amount genuinely gets consulted on the way up, so the two do essentially the same work. Top-down's laziness pays when the subproblems are spread out and most are never touched: imagine coins {50, 100} and amount 1000 — top-down only ever visits amounts you can actually reach by subtracting 50s and 100s (twenty-ish of them), while bottom-up dutifully fills all 1001 lines. Same answers either way; it's a question of how many notebook lines ever get written. The speed class — each distinct subproblem solved once — is identical, which is the only thing that separates both of them from the naive bomb.

Section 4The knapsack heist

New problem, same medicine. You are a burglar — a thoughtful burglar — standing in a mansion with a bag that holds 10 pounds. Around you is loot: each piece has a weight and a price tag. You can't take everything. The job: choose what goes in the bag so the total price is as high as possible without busting the 10-pound limit. Computer scientists call this the knapsack problem, and it is everywhere a budget is: shipping crates, ad slots, server memory, your weekend.

Greedy has an opinion, of course. "Take the best value per pound first!" It even sounds professional. Here is a three-item mansion that bankrupts it:

LootWeightPriceValue per pound
The painting6 lb$48$8.00 — best in the room
Jewelry box5 lb$35$7.00
Carriage clock5 lb$35$7.00
Greedy: best $/lb first painting · 6 lb · $48 4 lb of room left — both 5 lb pieces are now too big to fit. Haul: $48 ✗ The "best" item hogged the bag. The best haul: skip the star jewelry · 5 lb · $35 clock · 5 lb · $35 Haul: $70 ✓ · bag exactly full Two "worse" items beat one "best" one.
Value-per-pound greedy on the three-item mansion. The painting is genuinely the best deal per pound — and taking it is the single worst decision available, because its 6 pounds block both 5-pound pieces. $48 versus $70. Checked by brute force over all eight possible bags; no combination beats $70.

Sound familiar? It's the coin story wearing a ski mask. The grab that looks best right now poisons what's possible afterward, and greedy never checks. So apply the medicine. What's the "smaller version of the same problem" here? When you stand in front of one piece of loot, you have exactly two options, and each leaves you with a smaller heist:

Take it or don't — the knapsack notebook

Go through the loot one piece at a time. For each piece there are only two futures:

  • Skip it. Your best haul is whatever the previous pieces could do with the same room.
  • Take it. You pocket its price, but it eats its weight — so add its price to what the previous pieces could do with the leftover room.

Write the better of the two in the notebook. The notebook line you're filling answers: "what's the best haul using only the first few pieces, with this much room in the bag?" — one row per piece considered, one column per pound of room, every cell built from the row above. The final cell, all pieces considered with the full 10 pounds, is the answer. No guessing anywhere: both futures get looked up, billed, and compared.

For our heist that notebook is small: 5 pieces of loot (we'll add a vase and a jar of coins to the mansion), 0-to-10 pounds of room — a 6-row, 11-column grid, 66 little numbers, each one a single max-of-two-lookups. Sixty-six tiny sums to be certain about all 32 possible bags. Try the heist yourself first; then open the notebook and watch it not guess:

Demonstration 1The heist — pack the bag, then let the notebook show you up
The loot — tap to put in the bag (10 lb limit)
Bag: 0 / 10 lb$0
This haul
$0
Your best so far
$0
The notebook's best
???

Some things to try:

  1. Pack greedily by value per pound — painting first, then whatever fits. You'll land on $58. Now hunt by hand for something better before pressing the button. (There is something better.)
  2. Press Show the notebook and watch the fill: every cell is just "skip (copy the cell above) or take (price + the cell above-and-to-the-left), whichever is bigger." No cell ever guesses.
  3. When the fill ends, follow the blue trail backwards from the bottom-right cell. Wherever a cell differs from the one above it, that piece was taken. The notebook doesn't just know the best price — it remembers which items, for free.
  4. Reset and try to beat $70. Report back when you give up.

And the code is shorter than the heist briefing:

def best_haul(items, room):
    """items: list of (weight, price). room: what the bag holds.
    rows[i][w] = best haul using only the first i items with w pounds of room."""
    rows = [[0] * (room + 1)]                 # row 0: no items yet — worth $0
    for weight, price in items:
        prev, row = rows[-1], []
        for w in range(room + 1):
            skip = prev[w]                     # don't take it: row above, same room
            take = prev[w - weight] + price if weight <= w else 0
            row.append(max(skip, take))        # the better future wins the cell
        rows.append(row)
    return rows[-1][room]

loot = [(6, 48), (5, 35), (5, 35), (3, 14), (2, 10)]
print(best_haul(loot, 10))   # 70 — jewelry + clock; the $8/lb painting stays
Check yourself — A classmate says: "Forget value per pound — just grab the highest price first." Build a tiny mansion (three items, 10-pound bag) where that rule loses too.

One of many: a safe (10 lb, $50) plus two candlesticks (5 lb, $35 each). Highest-price greedy grabs the safe, the bag is full, haul $50. The notebook takes both candlesticks: $70. Every one-line grabbing rule has a mansion that beats it, because what a grab is worth depends on what it leaves room for — and one-line rules don't look. That dependence is exactly ingredient one from §2, and it's the tell that this is notebook territory.

Section 5How alike are two words?

Last stop, and it's the one in your pocket. You type SUNDAY; your phone wonders if you meant SATURDAY. Before it can decide, it needs a number: how much do these two words have in common? The standard answer: find the longest run of letters that appears in both words in the same order — other letters may be crossed out, but the survivors can't be shuffled. That shared skeleton is called the longest common subsequence, and the same measurement, scaled up, is how DNA analysis decides how related two gene sequences are.

cross out letters — never reorder them: S U N D A Y ← SUNDAY, one letter crossed out S A T U R D A Y ← SATURDAY, three crossed out Both leave S·U·D·A·Y — five letters, same order. No longer skeleton exists.
What "subsequence" means: keep letters, in order, cross out the rest. SUNDAY and SATURDAY both contain SUDAY — cross out one letter on the left, three on the right, and the dotted lines never cross, because order is preserved. Length 5 is the best possible: anything longer would need all six letters of SUNDAY inside SATURDAY, which has no N.

Could you find that by brute force? SUNDAY has 64 ways to cross out letters; check each against SATURDAY. Fine for words — fatal for DNA, where "words" run millions of letters and the cross-out count has more digits than the universe has atoms. Time for the medicine, and here is the lovely part: it's the same take-it-or-don't move from the heist. Look at the last letter of each word:

The last-letter game
  • The last letters match (…DAY / …DAY): that letter is part of the skeleton — keep it, score 1, and the rest of the answer is the best skeleton of the two words with that letter removed from both. A smaller version of the same question.
  • They don't match (…SUNDA / …SATURDAY): the two end-letters can't both survive. So cross out one or the other — try the question with the first word shortened, and with the second word shortened — and keep the better score. Two smaller versions of the same question.

Smaller versions of the same question, asked over and over (the pair "SUND / SATU" gets asked by many different paths) — both ingredients present. So: notebook. This one is a grid, like the heist's: one row per letter of the first word, one column per letter of the second, and the cell at row i, column j answers "how long is the best skeleton of the first i letters versus the first j letters?" Each cell is one look at its three neighbors — diagonal if the letters match, the better of above/left if not. For SUNDAY vs SATURDAY that's a 7 × 9 grid: 63 tiny cells to settle all 64 × 256 possible cross-outs. Type any two words and watch:

Demonstration 2The likeness grid — the longest thing two words share
Word 1 Word 2
Cells in the grid
—
Longest shared
—
The skeleton
—

Some things to try:

  1. Fill SUNDAY vs SATURDAY. Gold cells are where the two letters match — watch the score tick up by exactly 1 from the diagonal at each one. The blue trail afterwards is the walk-back: every diagonal step on it donates one letter, and reading those letters off spells SUDAY.
  2. Try SPRING vs STRING, then AAAA vs AKAKA. Predict the length before filling.
  3. Try two words with nothing in common. The grid fills with zeros — the notebook saying "no skeleton," 63 receipts to prove it.
def longest_common(a, b):
    """grid[i][j] = longest shared skeleton of a's first i letters
    and b's first j letters."""
    grid = [[0] * (len(b) + 1) for _ in range(len(a) + 1)]
    for i in range(1, len(a) + 1):
        for j in range(1, len(b) + 1):
            if a[i - 1] == b[j - 1]:                 # last letters match:
                grid[i][j] = grid[i - 1][j - 1] + 1  # keep the letter, +1
            else:                                    # they can't both survive:
                grid[i][j] = max(grid[i - 1][j],     # shorten word a, or
                                 grid[i][j - 1])     # shorten word b
    return grid[len(a)][len(b)]

print(longest_common("SUNDAY", "SATURDAY"))   # 5 — S·U·D·A·Y
Check yourself — What's the longest common subsequence of ABC and CBA? And why can't it be longer?

Length 1 — any single letter (A, B or C) qualifies, and nothing longer does. Why: take any two letters of ABC in order — AB, AC or BC. In CBA those same pairs appear only in reversed order (B before A, C before A, C before B), and a subsequence is never allowed to reorder. So no two letters survive together. Worth noticing: the words share all three letters and still score 1 — the skeleton measures shared order, not shared ingredients, which is exactly why it's a good likeness measure for spellcheckers and DNA alike.

Section 6When to reach for DP

Three problems, one move. Time to bottle it. Reach for the notebook when all three of these are true:

The DP recipe — three signs you're in notebook territory
  1. The problem asks for a best (or a count) over a pile of choices. Fewest coins, richest bag, longest skeleton.
  2. Any single choice leaves you facing a smaller version of the same problem. Grab a coin → smaller amount. Take or skip the loot → fewer items, less room. Match or cross out a letter → shorter words. If you can finish the sentence "after one choice, I'm left with the same question, only smaller," you have ingredient one.
  3. The number of distinct smaller questions is small. This is the one people forget. Coins: one question per amount — 7 of them. The heist: items × pounds — 66. Words: letters × letters — 63. Millions of paths through the choices, but they all keep landing on the same few questions — so a notebook with one line per question settles everything. If every path led to a brand-new question, the notebook would be as big as the explosion, and DP has nothing to sell you.

And the honest one-line verdict on last week versus this week: greedy is just DP where you can prove the first grab is always safe — the swap test from Notes 08 is that proof. When the swaps go through, you get to skip the whole notebook and keep only the grab: that's why greedy is faster when it's right. When the swaps jam, the grab's value depends on what it leaves behind, and you pay for the notebook. Never the other way around: DP is the safe default, greedy is the earned shortcut.

To file this week into the personality gallery: greedy was the sprinter. Dynamic programming is the accountant. Unhurried, a little boring, writes everything down, audits every option — and is never, ever wrong about the total. (The name, for what it's worth, means nothing: Richard Bellman, who invented the method in the 1950s, later admitted he picked "dynamic programming" partly because it sounded too impressive for his funders to object to. "The notebook method" was taken, presumably.)

Check yourself — For US coins {25, 10, 5, 1}, greedy is provably right and the notebook is right. Which do you ship, and what do you owe before shipping greedy?

Ship greedy — one pass, no table, and for this coin set it's provably perfect. But "provably" is the bill: you owe the swap-test argument (or a brute-force check across amounts, like 08's optional challenge) for this specific coin set, because the proof is about the problem, not the idea — change the coins to {1, 3, 4} and the same code is wrong. No proof in hand? Ship the notebook. Twelve lines, a few more microseconds, zero apologies.

Section 7Before next session

To do — before Tuesday, November 3
  1. Review Notes 09. Then close the page and narrate, from memory, the {1, 3, 4} notebook for amounts 0 through 6 — and say out loud why amount 6 can't get it wrong when greedy did. That one narration is the whole week.
  2. Project 3 — the spec is released this week. Same small-and-fun format as the first two, and it builds directly on these notes. Watch the course page.
  3. In the demos: beat $70 in the heist (or convince yourself nobody can), and run the likeness grid on your own first and last name.
  4. Optional challenge: upgrade last week's greedy-safety detector. Replace its brute-force referee with today's fewest_coins — the notebook is a perfect referee, and now the race to amount 500 finishes before your finger leaves the Enter key. Try {1, 3, 4}, {1, 4, 5}, then US coins.
  5. Optional rabbit hole: add three lines to naive fib — a dict, a lookup, a write — and time fib(35) before and after. Python even ships the notebook as a one-liner: @functools.lru_cache above the def. Same idea, zero effort.
Coming up · Notes 10 · Tuesday & Thursday, November 3 & 5

Dynamic programming, part two. The likeness grid has a sibling called edit distance — instead of "what do these words share?" it asks "how many keystrokes turn one into the other?" — and that's the number actually running your autocorrect, and, scaled up, how biologists align DNA. Same grid, different receipts. Then the notebook leaves flatland: DP on trees, where the "smaller version" isn't a smaller number but a smaller branch. Sessions 18–19.