This deck gives the two ingredients that make dynamic programming apply, then shows the naive exponential recursion for Fibonacci and why it recomputes the same values. It presents the two fixes - top-down memoization and bottom-up tabulation - through a repeatable method for defining the state, writing the recurrence, and choosing the base cases, with Fibonacci and climbing stairs fully hand-traced. It targets four real misconceptions: reaching for dynamic programming when the subproblems never actually overlap, a wrong or missing base case that corrupts an entire table, a memo that is written but never checked before recursing, and a state defined too ambiguously to support a clean recurrence.
Subject: CS3000 Algorithms · 134 slides · symbolic lesson
Open the interactive version of this deck · Homework for this lesson
Objectives
Dynamic programming turns painfully slow recursion into fast, provably correct code by remembering answers you have already worked out. This lesson builds the setup habit from scratch, using two small, fully hand-traced problems. By the end you can:
Warm-up
Discussion prompt
Before we open Dynamic Programming I: Memoization: without looking back, what was the main idea of Maximum Sum Subarray, and what could you do by the end of it that you could not do before?
Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.
Answer:
The maximum sum subarray problem worked three ways: brute force, divide-and-conquer (with the crossing subarray case), and Kadane's linear scan. Targets confusing a subarray with a subsequence, forgetting the crossing case in divide-and-conquer, resetting Kadane's running sum to zero on an all-negative array, and off-by-one errors when reporting the start/end indices.
Concept
Before any new material: cover the screen.
You have named 12 reusable moves so far. Say as many as you can out loud, by number, from memory.
Do not advance until you have actually tried. Getting four of eight is information; skipping the exercise is not.
Here they are. Score yourself.
Today adds one move to this list. Everything else you will need is already above.
The question that starts every proof from here on is not how do I begin. It is which of these applies here?
Counterexample
Discussion prompt
You have named 12 reusable moves so far. Say as many as you can out loud, by number, from memory.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Answer:
Do not advance until you have actually tried. Getting four of eight is information; skipping the exercise is not.
Picture it
Animation
Shows: Counting a DP's cost — a rendered Manim animation.
Rendered with Manim.
Takeaway: This one formula prices every DP you will meet.
Concept
Dynamic programming is a technique for solving a problem by solving smaller versions of the same problem, and saving each answer so it is never worked out twice.
That saving step is the whole trick. Everything else in this lesson is about deciding what to save, how to name it, and where to start.
Analogy
Discussion prompt
Explain What dynamic programming actually is by analogy to something with no CS3000 Algorithms in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
Dynamic programming is a technique for solving a problem by solving smaller versions of the same problem, and saving each answer so it is never worked out twice.
Picture it
Animation
Shows: What dynamic programming actually is — a rendered Manim animation.
Rendered with Manim.
Takeaway: The name is historical. The idea is a cache.
Concept
A problem has optimal substructure when its best answer can be built directly out of the best answers to smaller versions of itself.
optimal substructure — The best solution to the whole problem is assembled from the best solutions to its subproblems, never from a suboptimal one. This is what makes writing a recurrence formula possible at all.
Explain it
Discussion prompt
Explain Ingredient one: optimal substructure to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
A problem has optimal substructure when its best answer can be built directly out of the best answers to smaller versions of itself.
Concept
A problem has overlapping subproblems when solving it the naive recursive way asks the exact same smaller question more than once.
overlapping subproblems — The same smaller subproblem is requested again and again during a naive recursive solution. Saving its answer the first time, instead of recomputing it every time, is exactly what dynamic programming exploits.
Definition probe
Sort into buckets
Every line below is part of the definition of optimal substructure or of overlapping subproblems — one or the other, never both. Put each where it belongs.
Picture it
Animation
Shows: Overlap is what makes memoization pay — a rendered Manim animation.
Rendered with Manim.
Takeaway: fib(3) appears twice. Without a cache, so does everything beneath it.
Intuition
Neither ingredient alone is enough. A problem can have optimal substructure without ever repeating a subproblem - in that case a memo would sit there empty, never once reused, and buying nothing.
Before reaching for a memo, ask two questions: does the best answer come from smaller best answers, and does the naive recursion ask the same question twice? Only when both answers are yes does caching pay off.
Concept
A recursive function solves a problem by calling itself on a smaller version of that same problem, until it reaches a case small enough to answer directly without calling itself again.
That smallest, directly answerable case is the base case. Every recursive function needs at least one, or it never stops calling itself.
Picture it
Animation
Shows: Bottom-up fills the same cells — a rendered Manim animation.
Rendered with Manim.
Takeaway: Left to right, each cell using only cells already written.
Concept
The Fibonacci sequence is a list of numbers where each one is the sum of the two numbers before it, starting from 0 and 1.
\[ F(0)=0,\ F(1)=1,\ F(2)=1,\ F(3)=2,\ F(4)=3,\ F(5)=5,\ F(6)=8 \]
Concept
The definition of the sequence translates directly into a recursive function: to find one term, ask for the two terms before it and add them together.
\[ \text{fib}(n) = \text{fib}(n-1) + \text{fib}(n-2) \quad \text{for } n \ge 2 \]
\[ \text{fib}(0) = 0, \qquad \text{fib}(1) = 1 \]
Intuition
Every call to fib(n) branches into two more calls, fib(n-1) and fib(n-2), and each of those branches again, until the branches finally bottom out at fib(1) or fib(0).
Draw that branching as a tree and a striking pattern jumps out: the same small calls, like fib(2) or fib(1), show up in more than one branch. That repetition is the overlapping subproblem this whole lesson is about.
Ranking
Put in order
Put the moves of Tracing the naive recursion for fib(5) into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. Applying the recurrence: fib(5) needs the two terms before it.
Worked example
Expand fib(5) one call at a time and list every call the naive recursion makes.
Expand fib(5) into fib(4) and fib(3)
Why: Applying the recurrence: fib(5) needs the two terms before it.
\[ \text{fib}(5) \rightarrow \text{fib}(4) + \text{fib}(3) \]
Expand fib(4) into fib(3) and fib(2)
Why: The same rule applies one level down - and notice fib(3) has now appeared as a subproblem of both fib(5) and fib(4).
\[ \text{fib}(4) \rightarrow \text{fib}(3) + \text{fib}(2) \]
Fully expand every branch down to base cases
Why: Continue expanding fib(3) and fib(2) wherever they appear, until only fib(1) and fib(0) remain, and count how many times each call appears anywhere in the tree.
| call | number of times it appears |
|---|---|
| fib(5) | 1 |
| fib(4) | 1 |
| fib(3) | 2 |
| fib(2) | 3 |
| fib(1) | 5 |
| fib(0) | 3 |
Verify the total call count by summing the table
Why: Adding every row of the table gives 1 plus 1 plus 2 plus 3 plus 5 plus 3, which is 15 total calls - matching a direct count of the nodes in the full call tree.
\[ 1+1+2+3+5+3 = 15 \text{ total calls} \]
Picture it
Animation
Shows: Naive recursion recomputes the same work — a rendered Manim animation.
Rendered with Manim.
Takeaway: Two subtrees compute fib(3) independently. That duplication is exponential.
Concept
fib(3) gets requested from two different places: once directly as part of fib(5)'s expansion, and once as part of fib(4)'s expansion. Both requests ask the exact same question and must get the exact same answer.
A naive recursive call has no memory of what it already solved elsewhere in the tree, so it happily redoes the entire fib(3) computation from scratch the second time, and every deeper call inside it as well.
Concept
From the trace, fib(2) appears three separate times in the call tree for fib(5) - once under the first fib(3), once directly under fib(4), and once under the second fib(3).
Every one of those three calls redoes the same two additions from scratch: it calls fib(1) and fib(0) all over again, even though the answer was already found the first time.
Intuition
What feels wrong about this?
Computing the fifth Fibonacci number by the plain recursive definition:
\[ \texttt{fib}(5) \to \texttt{fib}(4), \texttt{fib}(3) \to \texttt{fib}(3), \texttt{fib}(2), \texttt{fib}(2), \texttt{fib}(1) \to \cdots \]
There are only five distinct values that could ever be asked for.
_Plain English only. No notation, no algebra. Just say what bothers you._
The feeling: the tree is enormous but it keeps asking the same handful of questions over and over, and answering each one from scratch every time.
That feeling is the proof. It is not a substitute for the proof — it is the thing the proof writes down.
So the fix does not need a cleverer formula. It needs a place to write answers down. That is the entire content of memoization — and it only works because the number of distinct questions is small.
Concept
Let T(n) count the total number of calls the naive recursion makes to compute fib(n). Each call to fib(n) makes one call to fib(n-1), one call to fib(n-2), and counts itself.
\[ T(n) = T(n-1) + T(n-2) + 1, \qquad T(0)=T(1)=1 \]
This is the Fibonacci recurrence again, in disguise, so the number of calls itself grows like a Fibonacci number - exponentially fast.
\[ T(n) = \Theta(\varphi^{\,n}), \quad \varphi = \frac{1+\sqrt5}{2} \approx 1.618 \]
Concept
Compare this to computing a factorial recursively: factorial(n) calls factorial(n-1) exactly once, which calls factorial(n-2) exactly once, and so on down to the base case.
That call tree is a single straight line, not a branching tree - every subproblem is asked for exactly once. There is nothing to save, because nothing ever gets asked for twice.
Picture it
Animation
Shows: When DP applies at all — a rendered Manim animation.
Rendered with Manim.
Takeaway: Divide and conquer has the first without the second — hence no memo table.
Anomaly
Predict first
A student writes this, and it looks reasonable:
Merge sort splits an array into a first half and a second half, sorts each half recursively, then merges them. A student adds a memo, hoping it will speed up the recursion the way it did for Fibonacci.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: The first half and second half of the array are always different pieces of data - the recursion never calls itself on the same array slice twice, at any level of the split.
Check for overlap before adding any cache: does the naive recursion ever ask the exact same question twice? For merge sort, no two recursive calls ever receive the same slice of the array.
Why: The first half and second half of the array are always different pieces of data - the recursion never calls itself on the same array slice twice, at any level of the split.
Trap
Merge sort splits an array into a first half and a second half, sorts each half recursively, then merges them. A student adds a memo, hoping it will speed up the recursion the way it did for Fibonacci.
Cache each recursive call by the exact array it was given
Why: The first half and second half of the array are always different pieces of data - the recursion never calls itself on the same array slice twice, at any level of the split.
\[ \text{cache hits} = 0 \quad \text{(every subproblem is brand new)} \]
Check for overlap before adding any cache: does the naive recursion ever ask the exact same question twice? For merge sort, no two recursive calls ever receive the same slice of the array.
Skip the memo and use plain divide-and-conquer instead
Why: With zero possible cache hits, a memo adds bookkeeping overhead and saves nothing - the running time stays exactly what plain divide-and-conquer already gives, order n log n.
\[ \text{running time unchanged}: \ \Theta(n \log n) \]
Notation
Annotate
From Trap: reaching for a memo when nothing overlaps — read this one piece at a time. What is each part doing?
On: \( \text{cache hits} = 0 \quad \text{(every subproblem is brand new)} \)
Concept
Fibonacci does have overlapping subproblems, so the fix is straightforward: the first time a call like fib(2) is answered, write its answer down somewhere. The next time fib(2) is requested, look up the answer instead of recomputing it.
Concept
memoization — Storing the result of each subproblem the first time it is computed, in a lookup table often called a cache or memo, so that every later request for the same subproblem is answered instantly instead of recomputed.
Memoization keeps the exact same recursive structure as the naive version - it just adds one check at the start and one write at the end of every call.
Picture it
Animation
Shows: When memoization buys nothing — a rendered Manim animation.
Rendered with Manim.
Takeaway: Divide and conquer has optimal substructure without the overlap.
Explain it to yourself
Discussion prompt
In The shape of a memoized recursive call this move is made:
First, check the cache
Why is that legal? Name the rule or definition it rests on before you read on.
Hint: If you can only say "because that is what you do", the rule is the thing to go and find.
Answer:
If this exact subproblem has already been solved, its answer is sitting in the cache - return it immediately, with no further recursion at all.
Concept
First, check the cache
Why: If this exact subproblem has already been solved, its answer is sitting in the cache - return it immediately, with no further recursion at all.
Otherwise, compute it the normal recursive way
Why: Call the recurrence exactly as the naive version would, recursing into the smaller subproblems it depends on.
Before returning, store the answer in the cache
Why: So that the next time this exact subproblem is requested, the first step's check finds it and skips the recursion entirely.
Concept
The memoized version is the naive recursion with a lookup bolted on the front and a store bolted on the back. Nothing about the recurrence changes.
MEMO-FIB(n, memo)
if n <= 1
return n
if memo[n] is not empty
return memo[n]
memo[n] = MEMO-FIB(n - 1, memo) + MEMO-FIB(n - 2, memo)
return memo[n]Delete lines 4, 5 and the store on line 6 and you have the exponential version. The recurrence on line 6 is identical in both. All memoization does is guarantee that line 6 runs at most once per value of n.
Notation
Every line of MEMO-FIB says one thing. Read the line, then read what it does — not the other way round.
Annotate
Invariant
Watch the memo table. Each entry gets written exactly once. Every later call that wants it returns immediately at line 5, without ever reaching line 6.
Step through it
Each time a call returns, say whether it computed or merely looked up.
Picture it
Animation
Shows: MEMO-FIB executing: the current line of pseudocode is highlighted while the data it touches changes.
Rendered with Manim.
Takeaway: The recurrence is unchanged; the lookup on line 4 is what makes each subproblem run once instead of exponentially often.
Concept
Before writing any cache, name exactly what is being remembered. Let the state be a single number, n, and let the cache entry for n store one thing: the Fibonacci value at that index.
\[ \text{memo}[n] = F(n) \]
This is the same recurrence as before - memoization changes nothing about what is being computed, only how many times each piece of it actually gets computed.
Picture it
Animation
Shows: Choosing the state is the whole design — a rendered Manim animation.
Rendered with Manim.
Takeaway: Too much state wastes space; too little and the recurrence is wrong.
Explain it to yourself
Discussion prompt
In Step one of every DP, named this move is made:
Why the sentence has to come first
Why is that legal? Name the rule or definition it rests on before you read on.
Hint: If you can only say "because that is what you do", the rule is the thing to go and find.
Answer:
The recurrence is a relationship between entries. If you have not said what an entry means, there is nothing for the recurrence to relate.
Concept
The move: #13 (Name the subproblem).
Say what one entry means, in a sentence with no code in it
Why: memo[i] is the i-th Fibonacci number. That sentence is the state. Write it before the recurrence, always.
Why the sentence has to come first
Why: The recurrence is a relationship between entries. If you have not said what an entry means, there is nothing for the recurrence to relate.
Test for a good sentence: hand it to someone who has not seen the problem, point at one table cell, and ask what number goes there. If they cannot answer, the sentence is not done.
This is the move that will fail you if you skip it, on knapsack and on edit distance especially. It pairs with #12 (Case-split on the last decision): name the entry, then name the last decision, and the recurrence is forced.
Concept
Every proof of this kind has the same five or six moves in the same order. The order is not something you rediscover each time.
It is on the right. It will stay on the right through the worked examples that follow.
Why this matters: the structure is now handled. You are not spending working memory on what comes next — you are spending all of it on the one hard step.
Step 1 is where every failed DP fails. A subproblem you cannot say in one sentence is a subproblem you cannot write a recurrence for, and steps 2 through 6 will not rescue it.
Concept
The recurrence for the memoized version is identical to the naive one - memoization is not a different formula, it is the same formula plus a lookup step wrapped around it.
\[ \text{memo}[n] = \text{memo}[n-1] + \text{memo}[n-2] \quad \text{for } n \ge 2 \]
Concept
The base cases also carry over unchanged: they are simply pre-filled into the cache before any recursive call happens, so the very first lookup of n equal to 0 or 1 already succeeds.
\[ \text{memo}[0] = 0, \qquad \text{memo}[1] = 1 \]
Step zero
Discussion prompt
Tracing the top-down memoized fib(5) — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.
Hint: It starts with: Recurse all the way down the first branch
Answer:
Worked example
Trace every call made computing fib(5) with a cache, noting whether each call is a fresh computation (a cache miss) or an instant lookup (a cache hit).
Recurse all the way down the first branch
Why: fib(5) needs fib(4), which needs fib(3), which needs fib(2), which needs fib(1) and fib(0) - none of these have been seen before, so every one is a cache miss.
| call | cache status | result |
|---|---|---|
| fib(5) | miss - not yet computed | pending |
| fib(4) | miss - not yet computed | pending |
| fib(3) | miss - not yet computed | pending |
| fib(2) | miss - not yet computed | pending |
| fib(1) | miss - base case | 1 |
| fib(0) | miss - base case | 0 |
Unwind back up, filling in cached values as each call returns
Why: fib(2) combines the now-known fib(1) and fib(0) into 1, and caches it. fib(3) then asks for fib(2) again - but this time it is already cached, so it returns 1 instantly with no recursion.
| call | cache status | result |
|---|---|---|
| fib(2) computed | stored: memo[2] = 1 | 1 |
| fib(1) (2nd request) | hit - returned instantly | 1 |
| fib(3) computed | stored: memo[3] = 2 | 2 |
Continue unwinding: fib(4) and fib(5) each reuse a cached value
Why: fib(4) needs fib(2) again - a cache hit, returning 1 instantly - then computes and caches memo[4] = 3. fib(5) then needs fib(3) again - a cache hit, returning 2 instantly - then computes memo[5] = 5.
| call | cache status | result |
|---|---|---|
| fib(2) (2nd request) | hit - returned instantly | 1 |
| fib(4) computed | stored: memo[4] = 3 | 3 |
| fib(3) (2nd request) | hit - returned instantly | 2 |
| fib(5) computed | stored: memo[5] = 5 | 5 |
Verify the call count against the naive trace
Why: This trace made 9 total calls - 6 cache misses (one per distinct index 0 through 5) and 3 cache hits - compared to the naive version's 15 calls for the same fib(5). Every cache hit is a branch of recursion that never had to happen.
\[ 9 \text{ calls (memoized)} \ \text{vs} \ 15 \text{ calls (naive)}, \quad \text{fib}(5) = 5 \text{ either way} \]
Picture it
Animation
Shows: The cost of forgetting — a rendered Manim animation.
Rendered with Manim.
Takeaway: Same recursion, same answer. One remembers.
Concept
There are only n plus 1 distinct subproblems for fib(n): the indices 0 through n. Memoization guarantees each one is actually computed - not just called - exactly once.
\[ \text{time} = \Theta(n) \]
Every call beyond the first for a given index is a cache hit, which does a fixed, small amount of work. That is the entire reason the running time collapses from exponential to linear.
Concept
Memoization is not free - the cache itself needs room to store one entry per distinct state, so it costs extra memory that the naive version did not need.
\[ \text{space} = \Theta(n) \quad \text{for the cache} \]
In exchange for that linear amount of space, the running time drops from exponential all the way to linear. That trade - a modest, predictable amount of space for a dramatic amount of time - is the whole economic case for dynamic programming.
Socratic
Discussion prompt
Memoization is not free - the cache itself needs room to store one entry per distinct state, so it costs extra memory that the naive version did not need.
Suppose that were not true. What is the first thing in Dynamic Programming I: Memoization that would stop working?
Hint: Follow it one step downstream. The answer is whatever was quietly relying on it.
Anomaly
Predict first
A student writes this, and it looks reasonable:
A student adds a cache to their Fibonacci function, storing each answer after computing it - but forgets to look the answer up before recursing. Every call still recurses fully, no matter what is already sitting in the cache.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: Because nothing is ever read back out of the cache before recursing, the call tree branches exactly the same way it did with no cache at all - fib(3) and fib(2) each get fully recomputed at every place they appear.
Check the cache first, at the very top of the function, before doing any recursive work at all - not just after. If this state is already cached, return it immediately and skip the recursion entirely.
Why: Because nothing is ever read back out of the cache before recursing, the call tree branches exactly the same way it did with no cache at all - fib(3) and fib(2) each get fully recomputed at every place they appear.
Trap
A student adds a cache to their Fibonacci function, storing each answer after computing it - but forgets to look the answer up before recursing. Every call still recurses fully, no matter what is already sitting in the cache.
Compute fib(5) with a write-only cache
Why: Because nothing is ever read back out of the cache before recursing, the call tree branches exactly the same way it did with no cache at all - fib(3) and fib(2) each get fully recomputed at every place they appear.
\[ \text{total calls} = 15 \quad \text{(identical to the naive version)} \]
Check the cache first, at the very top of the function, before doing any recursive work at all - not just after. If this state is already cached, return it immediately and skip the recursion entirely.
Compute fib(5) with a checked cache
Why: Now the second and later requests for fib(3) and fib(2) are caught before any recursion happens, matching the traced result from before: 9 total calls, 6 of them fresh computations.
\[ \text{total calls} = 9 \quad \text{(6 misses, 3 hits)} \]
Translation
\( \text{total calls} = 9 \quad \text{(6 misses, 3 hits)} \)
Draw it
Translate both ways. First write the expression above as a sentence with no symbols in it at all. Then cover it, and write your sentence back as notation. If the two versions disagree, the disagreement is the thing to fix.
Concept
Top-down memoization starts at the big question, fib(5), and recurses downward toward the base cases, filling in the cache on the way back up. Bottom-up tabulation flips the direction entirely.
Start at the base cases and iterate straight upward, computing fib(2), then fib(3), then fib(4), and so on, using a simple loop with no recursion at all.
Explain it to yourself
Discussion prompt
In The shape of a tabulated solution this move is made:
Loop forward, filling each remaining slot from the recurrence
Why is that legal? Name the rule or definition it rests on before you read on.
Hint: If you can only say "because that is what you do", the rule is the thing to go and find.
Answer:
Every slot is filled using only slots that come before it in the loop, which are already known by the time they're needed.
Concept
Make a table with one slot per state
Why: Here, one slot for every index from 0 up to n.
Fill in the base cases directly
Why: These are known facts, not computed from the recurrence - they seed the whole table.
Loop forward, filling each remaining slot from the recurrence
Why: Every slot is filled using only slots that come before it in the loop, which are already known by the time they're needed.
Step zero
Discussion prompt
Filling the Fibonacci table bottom-up — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.
Hint: It starts with: Seed the base cases
Answer:
Worked example
Build the table for F(n) from n equal to 0 up through n equal to 8, one slot at a time.
Seed the base cases
Why: F(0) and F(1) are known facts, not computed - they start the table.
| n | 0 | 1 |
|---|---|---|
| F(n) | 0 | 1 |
Fill n = 2 through n = 5
Why: Each slot adds the two slots directly before it: F(2) = F(1)+F(0) = 1, F(3) = F(2)+F(1) = 2, F(4) = F(3)+F(2) = 3, F(5) = F(4)+F(3) = 5.
| n | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| F(n) | 0 | 1 | 1 | 2 | 3 | 5 |
Fill n = 6 through n = 8
Why: F(6) = F(5)+F(4) = 5+3 = 8, F(7) = F(6)+F(5) = 8+5 = 13, F(8) = F(7)+F(6) = 13+8 = 21.
| n | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| F(n) | 0 | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 |
Verify F(8) against the recurrence one more time
Why: F(8) must equal F(7) plus F(6), and indeed 13 plus 8 is 21 - the table is internally consistent at every slot, not just the last one.
\[ F(8) = 13 + 8 = 21 \]
Picture it
Animation
Shows: Each line of the worked example "Filling the Fibonacci table bottom-up", appearing one at a time.
The same working the example does, in the order a tutor would write it.
Takeaway: F(8) must equal F(7) plus F(6), and indeed 13 plus 8 is 21 - the table is internally consistent at every slot, not just the last one.
Estimation
Predict first
All three methods answer the exact same question, so they must agree on every value, even though they arrive there completely differently.
Commit before you compute: what does Checking bottom-up against top-down and the naive tree come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.
Correct: Verify the value is identical across all three rows
Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. Every method reports fib(5) equal to 5, confirming that memoization and tabulation change only the amount of repeated work, never the final answer - both fewer calls than the naive method's 15.
Worked example
All three methods answer the exact same question, so they must agree on every value, even though they arrive there completely differently.
Compare fib(5) across all three methods
Why: The naive recursion, the memoized top-down version, and the bottom-up table all computed fib(5) earlier in this lesson.
| method | calls made | fib(5) value |
|---|---|---|
| naive recursion | 15 | 5 |
| top-down memoized | 9 | 5 |
| bottom-up table | 6 (one fill per slot, n=0..5) | 5 |
Verify the value is identical across all three rows
Why: Every method reports fib(5) equal to 5, confirming that memoization and tabulation change only the amount of repeated work, never the final answer - both fewer calls than the naive method's 15.
\[ 5 = 5 = 5 \]
Concept
Both methods rely on the exact same state definition, recurrence, and base cases. The only difference is direction: top-down starts at n and recurses down, caching answers as it returns; bottom-up starts at the base cases and iterates straight up to n.
If you can write one, you can write the other - they are two directions through the same table.
Picture it
Animation
Shows: Top-down and bottom-up are the same table — a rendered Manim animation.
Rendered with Manim.
Takeaway: Choose by which order is easier to write, not by which is faster.
Concept
Top-down memoization still recurses, so for a large n the call stack grows n calls deep before the first base case returns - on some systems, a large enough n can overflow that stack.
Bottom-up tabulation is a plain loop with no recursion at all, so it never risks a stack overflow, no matter how large n is - one real reason to prefer it once the recurrence is understood.
Concept
Look closely at the recurrence: computing F(n) only ever needs F(n-1) and F(n-2) - never anything further back. The full table of every value is more than the recurrence actually requires.
\[ \text{prev}, \text{curr} \leftarrow \text{curr}, \ \text{prev}+\text{curr} \]
Rolling just two variables forward, instead of storing the whole table, drops the space used from linear in n down to a constant amount - while the time to compute the answer stays exactly the same.
Intuition
You are standing at the bottom of a staircase with a certain number of stairs. On each move you can climb either 1 stair or 2 stairs. The question: in how many distinct ways can you reach the very top?
This sounds nothing like Fibonacci at first glance - there is no obvious sum of two numbers in the problem statement. Watch what happens once the state is defined properly.
Concept
Let the state be a single number: which stair is being reached. Store, for that stair, the number of distinct sequences of 1-and-2 moves that land exactly there.
\[ \text{ways}(n) = \text{number of distinct ways to reach stair } n \]
Intuition
What move should we make next?
The subproblem sentence for climbing stairs:
\[ \texttt{ways}(n) = \text{the number of distinct ways to climb } n \text{ stairs, taking 1 or 2 at a time} \]
No recurrence yet. Just the sentence.
There is exactly one question that turns that sentence into a recurrence, and you named it last lesson. What is it?
_Look at your toolkit. Say a move number out loud before this slide advances._ A wrong guess is useful. A silent guess is not.
Picture it
Animation
Shows: Storing the decision, not just the value — a rendered Manim animation.
Rendered with Manim.
Takeaway: The value alone rarely satisfies the question being asked.
Concept
Think about the very last move made to arrive at stair n. It was either a 1-stair step, taken from stair n minus 1, or a 2-stair step, taken from stair n minus 2 - there is no third option.
\[ \text{ways}(n) = \text{ways}(n-1) + \text{ways}(n-2) \quad \text{for } n \ge 2 \]
Every distinct way to reach stair n-1 gives one distinct way to reach stair n by adding a final 1-step, and every distinct way to reach stair n-2 gives one distinct way to reach stair n by adding a final 2-step - together these account for every possible path, with no double counting.
Fill the middle
Fill in the blanks
From Naming the last decision for the stairs — finish the line. Write what belongs on the right of the equals sign before you look.
\texttt\texttt{ways}(n-1) + \texttt{ways}(n-2)(n) = ___
Why: Producing the right-hand side unprompted is the difference between recognising this line and being able to use it. You arrived at stair n. Your last move was either a 1-step from stair n-1, or a 2-step from stair n-2.
Concept
The move: #12 (Case-split on the last decision), then #13 (Name the subproblem).
What was the final step you took?
Why: You arrived at stair n. Your last move was either a 1-step from stair n-1, or a 2-step from stair n-2. There is no third option, which is what makes the case split complete.
Each case is a smaller answer of the same kind
Why: Every way of reaching n-1 extends to exactly one way of reaching n by a 1-step, and likewise for n-2. So you add the two counts.
\[ \texttt{ways}(n) = \texttt{ways}(n-1) + \texttt{ways}(n-2) \]
Check the cases do not overlap
Why: A path whose last move is a 1-step is never a path whose last move is a 2-step, so nothing is double-counted. Always check this — it is where counting DPs go wrong.
The recurrence is Fibonacci, which is a surprise. The way you got there was not a surprise: name the entry, name the last decision, split, write.
Concept
Reaching stair 1 has exactly one way: a single 1-stair step. Reaching stair 0 - already at the bottom, no moves needed - also has exactly one way: the empty sequence of moves.
\[ \text{ways}(0) = 1, \qquad \text{ways}(1) = 1 \]
These are facts known before the recurrence ever runs, exactly the same role Fibonacci's base cases played.
Hypothesis
Predict first
Tracing the top-down memoized ways(5) is about to be worked. State your hypothesis first: which rule or definition decides this one, and what is the first move it forces? Then watch whether the example agrees with you.
Correct: Recurse down to the base cases
Why: ways(5) needs ways(4), which needs ways(3), which needs ways(2), which needs ways(1) and ways(0) - all first requests, so all six are cache misses.
A hypothesis you wrote down is falsifiable; a vague sense of how it will go is not. If the example opens somewhere else, that gap is the thing worth chasing.
Worked example
Trace ways(5) exactly the way fib(5) was traced earlier - the recurrence has the identical shape, so watch for the identical pattern of cache hits.
Recurse down to the base cases
Why: ways(5) needs ways(4), which needs ways(3), which needs ways(2), which needs ways(1) and ways(0) - all first requests, so all six are cache misses.
| call | cache status | result |
|---|---|---|
| ways(5) | miss | pending |
| ways(4) | miss | pending |
| ways(3) | miss | pending |
| ways(2) | miss | pending |
| ways(1) | miss - base case | 1 |
| ways(0) | miss - base case | 1 |
Unwind, filling in cached values as each call returns
Why: ways(2) combines ways(1) and ways(0) into 1 plus 1, or 2, and caches it. ways(3) then requests ways(1) again - a cache hit, returned instantly - and computes ways(2) plus ways(1), or 2 plus 1, which is 3.
| call | cache status | result |
|---|---|---|
| ways(2) computed | stored: memo[2] = 2 | 2 |
| ways(1) (2nd request) | hit | 1 |
| ways(3) computed | stored: memo[3] = 3 | 3 |
Continue unwinding to ways(5)
Why: ways(4) requests ways(2) again - a cache hit, returning 2 - and computes 3 plus 2, which is 5. ways(5) then requests ways(3) again - a cache hit, returning 3 - and computes 5 plus 3, which is 8.
| call | cache status | result |
|---|---|---|
| ways(2) (2nd request) | hit | 2 |
| ways(4) computed | stored: memo[4] = 5 | 5 |
| ways(3) (2nd request) | hit | 3 |
| ways(5) computed | stored: memo[5] = 8 | 8 |
Verify the call count matches the Fibonacci pattern
Why: This trace made 9 total calls - 6 misses, 3 hits - the identical shape found for fib(5), because the recurrence has the identical shape. ways(5) equals 8.
\[ \text{ways}(5) = 8, \quad 9 \text{ total calls} \]
Picture it
Animation
Shows: The memo pattern, in three lines — a rendered Manim animation.
Rendered with Manim.
Takeaway: Forgetting to store turns the cache into decoration.
Ranking
Put in order
Put the moves of Filling the climbing-stairs table bottom-up into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. ways(0) and ways(1) are known facts, not computed.
Worked example
Build the table for ways(n) from n equal to 0 up through n equal to 6.
Seed the base cases
Why: ways(0) and ways(1) are known facts, not computed.
| n | 0 | 1 |
|---|---|---|
| ways(n) | 1 | 1 |
Fill n = 2 through n = 4
Why: ways(2) = ways(1)+ways(0) = 1+1 = 2. ways(3) = ways(2)+ways(1) = 2+1 = 3. ways(4) = ways(3)+ways(2) = 3+2 = 5.
| n | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| ways(n) | 1 | 1 | 2 | 3 | 5 |
Fill n = 5 and n = 6
Why: ways(5) = ways(4)+ways(3) = 5+3 = 8. ways(6) = ways(5)+ways(4) = 8+5 = 13.
| n | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| ways(n) | 1 | 1 | 2 | 3 | 5 | 8 | 13 |
Verify ways(6) against the recurrence
Why: ways(6) must equal ways(5) plus ways(4), and 8 plus 5 is indeed 13 - the table is consistent at every slot.
\[ \text{ways}(6) = 8 + 5 = 13 \]
Picture it
Animation
Shows: Each line of the worked example "Filling the climbing-stairs table bottom-up", appearing one at a time.
The same working the example does, in the order a tutor would write it.
Takeaway: ways(6) must equal ways(5) plus ways(4), and 8 plus 5 is indeed 13 - the table is consistent at every slot.
Estimation
Predict first
Check the table's claim that ways(4) equals 5 by listing every sequence of 1-and-2 moves that sums to exactly 4, without using the recurrence at all.
Commit before you compute: what does Verifying ways(4) by listing every path directly come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.
Correct: Verify the count matches the table
Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. Exactly 5 distinct sequences were listed, and no sequence was listed twice - matching ways(4) equal to 5 from both the top-down and bottom-up traces.
Worked example
Check the table's claim that ways(4) equals 5 by listing every sequence of 1-and-2 moves that sums to exactly 4, without using the recurrence at all.
List every sequence that sums to 4
Why: Four 1-steps; a 1-step, a 1-step, then a 2-step, in every possible order; or two 2-steps.
| sequence | sum |
|---|---|
| 1+1+1+1 | 4 |
| 1+1+2 | 4 |
| 1+2+1 | 4 |
| 2+1+1 | 4 |
| 2+2 | 4 |
Verify the count matches the table
Why: Exactly 5 distinct sequences were listed, and no sequence was listed twice - matching ways(4) equal to 5 from both the top-down and bottom-up traces.
\[ 5 \text{ sequences} = \text{ways}(4) \]
Picture it
Animation
Shows: Each line of the worked example "Verifying ways(4) by listing every path directly", appearing one at a time.
The same working the example does, in the order a tutor would write it.
Takeaway: Exactly 5 distinct sequences were listed, and no sequence was listed twice - matching ways(4) equal to 5 from both the top-down and bottom-up traces.
Anomaly
Predict first
A student writes this, and it looks reasonable:
A student reasons that 'zero stairs means zero ways to do anything' and sets ways(0) to 0 instead of 1, while correctly leaving ways(1) at 1.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: ways(2) = ways(1)+ways(0) = 1+0 = 1.
ways(0) counts the empty sequence of moves - already standing on stair 0 with no steps taken is one valid, distinct way to be there, not zero ways.
Why: ways(2) = ways(1)+ways(0) = 1+0 = 1. ways(3) = ways(2)+ways(1) = 1+1 = 2. ways(4) = ways(3)+ways(2) = 2+1 = 3 - every single downstream value is now wrong, not just ways(0) itself.
Trap
A student reasons that 'zero stairs means zero ways to do anything' and sets ways(0) to 0 instead of 1, while correctly leaving ways(1) at 1.
\[ \text{wrong base case: ways}(0) = 0 \]
Fill the table forward from the wrong base case
Why: ways(2) = ways(1)+ways(0) = 1+0 = 1. ways(3) = ways(2)+ways(1) = 1+1 = 2. ways(4) = ways(3)+ways(2) = 2+1 = 3 - every single downstream value is now wrong, not just ways(0) itself.
| n | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| wrong ways(n) | 0 | 1 | 1 | 2 | 3 |
ways(0) counts the empty sequence of moves - already standing on stair 0 with no steps taken is one valid, distinct way to be there, not zero ways.
\[ \text{correct base case: ways}(0) = 1 \]
Fill the table forward from the correct base case
Why: With ways(0) = 1, the same recurrence gives ways(2) = 2, ways(3) = 3, ways(4) = 5 - matching the direct enumeration of all 5 paths to stair 4 confirmed in the previous slide.
| n | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| correct ways(n) | 1 | 1 | 2 | 3 | 5 |
Notation
Annotate
From Trap: a wrong base case corrupts the whole table — read this one piece at a time. What is each part doing?
On: \( \text{correct base case: ways}(0) = 1 \)
Concept
A cache only helps if it can recognize a repeated question. Two calls are the same subproblem exactly when they share the same state - for Fibonacci and climbing stairs, that state is just the single number n.
The cache key has to capture every piece of information the recurrence actually depends on - nothing more, and nothing less. Leave something out, and different subproblems get confused as if they were the same one; include something irrelevant, and true repeats stop being recognized as repeats.
Intuition
Watch me not know the answer. This is what the first two minutes actually look like.
Trying to set up the climbing-stairs DP from a different starting point.
Try: let the entry be the list of all valid climbing sequences of length n
Why: It is certainly well defined, and the answer is its size. Seems like a reasonable place to start.
The table becomes exponential and the recurrence becomes useless
Why: There are exponentially many sequences, so the table cannot be built, and the relationship between the list for n and the list for n-1 is not something you can write in one line.
Dead end. Not a mistake — a move that was worth trying and did not pay off. This happens in most proofs.
Back up. Store the count, not the objects
Why: You were asked how many, not which. Storing the number keeps the entry to one integer and makes the recurrence a single addition.
The lesson generalizes: if the entry is all the things, look for a version where the entry is a number — a count, a maximum, a minimum, a yes or no.
The expert does not see the whole path in advance. The expert tries something, reads the result, and adjusts. That is the skill.
Concept
Before writing a single formula, say in plain English exactly what varies from subproblem to subproblem, and exactly what a single table entry stores.
For Fibonacci: 'the index n; store the Fibonacci value at that index.' For climbing stairs: 'the stair n; store the number of distinct ways to reach it.' Both are one sentence - if it takes more than one sentence, the state probably needs to be split or clarified.
Concept
With the state pinned down, ask: what are the actual choices that build this state's answer out of smaller states' answers? List every choice, then combine them - by adding, by taking the best, or whatever the problem calls for.
For both Fibonacci and climbing stairs, the recurrence adds together exactly two smaller states - but the reason differs: Fibonacci's is a definition; climbing stairs' comes from reasoning about the very last move.
Concept
Find the smallest state or states whose answer is obvious without applying the recurrence at all - these are facts, not computations, and every other entry ultimately depends on them.
Get a base case wrong, and every value built on top of it is wrong too, even though the recurrence itself was applied correctly at every single step - the trap seen earlier with climbing stairs.
Concept
Once the state, recurrence, and base cases are settled, decide how to fill in the answers: top-down, letting recursion visit only the states actually needed, with a cache to avoid repeats; or bottom-up, looping forward from the base cases through every state up to the target.
Both directions use the identical state, recurrence, and base cases - this last step changes only the mechanics of filling, never the mathematics being computed.
Anomaly
Predict first
A student writes this, and it looks reasonable:
A student tries to define the climbing-stairs state as k, the number of moves used, instead of n, the stair reached: 'let dp[k] be the number of ways using exactly k moves.'
It is wrong. Say what breaks — and say it before you turn the page.
Correct: With steps of size 1 and 2, using exactly k = 2 moves can land on stair 2 (a 1 then a 1), stair 3 (a 1 then a 2, or a 2 then a 1), or stair 4 (a 2 then a 2) - three different destinations, all sharing the same k.
Because dp[k] mixes several different destinations under one label, there is no clean way to write 'ways to reach the top' in terms of it - the state has to pin down exactly one quantity, not a bundle of possibilities.
Why: With steps of size 1 and 2, using exactly k = 2 moves can land on stair 2 (a 1 then a 1), stair 3 (a 1 then a 2, or a 2 then a 1), or stair 4 (a 2 then a 2) - three different destinations, all sharing the same k.
Trap
A student tries to define the climbing-stairs state as k, the number of moves used, instead of n, the stair reached: 'let dp[k] be the number of ways using exactly k moves.'
Try to see what dp[k] actually counts
Why: With steps of size 1 and 2, using exactly k = 2 moves can land on stair 2 (a 1 then a 1), stair 3 (a 1 then a 2, or a 2 then a 1), or stair 4 (a 2 then a 2) - three different destinations, all sharing the same k.
\[ k=2 \Rightarrow \text{ reaches stair } 2, \ 3, \text{ or } 4 \text{ - not one fixed stair} \]
Because dp[k] mixes several different destinations under one label, there is no clean way to write 'ways to reach the top' in terms of it - the state has to pin down exactly one quantity, not a bundle of possibilities.
Define the state as the stair reached instead
Why: dp[n] = number of ways to reach stair n fixes exactly one destination per state, which is what makes dp[n] = dp[n-1] + dp[n-2] well defined: both terms refer to one specific stair each, not a mix of several.
\[ \text{dp}[n] = \text{ways to reach stair } n \quad \text{(one destination, one number)} \]
Two truths and a lie
Sort into buckets
Some of these hold up and some are the exact mistakes this lesson is built to prevent. Sort them.
Intuition
Write out the recursive definition of the problem in plain words first. Then ask: as this recursion unfolds, could two different paths through it ever ask for the exact same smaller state?
If the answer is yes, the state is worth naming precisely and caching. If the answer is genuinely no - like merge sort splitting into always-different halves - a memo is wasted effort, and plain recursion is already the right tool.
Picture it
Animation
Shows: Count the states before you code — a rendered Manim animation.
Rendered with Manim.
Takeaway: The state count is the table size, and the table size is the cost.
Concept
A new variant: the same staircase, but now each move can climb 1, 2, or 3 stairs at a time. Work through all four setup steps on a problem never seen before.
Step 1, the state: let ways3(n) be the number of distinct ways to reach stair n, using moves of size 1, 2, or 3.
Intuition
What move should we make next?
The rule changes. The subproblem sentence does not:
\[ \texttt{ways3}(n) = \text{ways to climb } n \text{ stairs taking 1, 2 or 3 at a time} \]
How many cases now, and how many base cases will you need? Answer both before anyone writes the recurrence.
_Look at your toolkit. Say a move number out loud before this slide advances._ A wrong guess is useful. A silent guess is not.
Concept
Step 2, the recurrence: the last move to reach stair n was a 1-step, a 2-step, or a 3-step, taken from three stairs back, two stairs back, or one stair back respectively.
\[ \text{ways3}(n) = \text{ways3}(n-1) + \text{ways3}(n-2) + \text{ways3}(n-3) \quad \text{for } n \ge 1 \]
Step 3, the base case: ways3(0) is 1, the empty sequence of moves, exactly as before. Any state below stair 0 does not exist, so treat it as contributing 0 ways - there is no way to have already overshot the bottom.
\[ \text{ways3}(0) = 1, \qquad \text{ways3}(n) = 0 \ \text{for } n < 0 \]
Explain it
Discussion prompt
Explain The recurrence and base case for the 3-step version to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
Step 2, the recurrence: the last move to reach stair n was a 1-step, a 2-step, or a 3-step, taken from three stairs back, two stairs back, or one stair back respectively.
Step zero
Discussion prompt
Filling the 3-step table bottom-up — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.
Hint: It starts with: Fill n = 0 through n = 2
Answer:
Worked example
Build the table for ways3(n) from n equal to 0 up through n equal to 5, using the out-of-range convention for any negative index.
Fill n = 0 through n = 2
Why: ways3(0) = 1 is the base case. ways3(1) = ways3(0)+ways3(-1)+ways3(-2) = 1+0+0 = 1. ways3(2) = ways3(1)+ways3(0)+ways3(-1) = 1+1+0 = 2.
| n | 0 | 1 | 2 |
|---|---|---|---|
| ways3(n) | 1 | 1 | 2 |
Fill n = 3 through n = 5
Why: ways3(3) = ways3(2)+ways3(1)+ways3(0) = 2+1+1 = 4. ways3(4) = ways3(3)+ways3(2)+ways3(1) = 4+2+1 = 7. ways3(5) = ways3(4)+ways3(3)+ways3(2) = 7+4+2 = 13.
| n | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| ways3(n) | 1 | 1 | 2 | 4 | 7 | 13 |
Verify ways3(5) against the recurrence
Why: ways3(5) must equal ways3(4) plus ways3(3) plus ways3(2), and indeed 7 plus 4 plus 2 is 13 - the table checks out at its final entry.
\[ \text{ways3}(5) = 7+4+2 = 13 \]
Picture it
Animation
Shows: Bottom-up needs a legal order — a rendered Manim animation.
Rendered with Manim.
Takeaway: Which is the real argument for writing it recursively first.
Step zero
Discussion prompt
Cross-checking ways3(5) by conditioning on the first move instead — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.
Hint: It starts with: Split every path to stair 5 by its first move
Answer:
Worked example
The recurrence was built by reasoning about the last move. As an independent check, reason about the first move instead, and confirm the two ways of thinking agree.
Split every path to stair 5 by its first move
Why: A first move of 1 leaves 4 stairs to go, a first move of 2 leaves 3 stairs to go, and a first move of 3 leaves 2 stairs to go - and the count of ways to finish from there is exactly ways3 of whatever remains.
\[ \text{ways3}(5) = \text{ways3}(4) + \text{ways3}(3) + \text{ways3}(2) \]
Substitute the already-filled table values
Why: ways3(4) is 7, ways3(3) is 4, and ways3(2) is 2, from the table filled in the previous slide.
\[ 7 + 4 + 2 = 13 \]
Verify this matches the last-move recurrence exactly
Why: Conditioning on the first move and conditioning on the last move produced the identical sum, 13, confirming ways3(5) is correct regardless of which end of the path is reasoned about first.
\[ \text{first-move total} = \text{last-move total} = 13 \]
Picture it
Animation
Shows: Each line of the worked example "Cross-checking ways3(5) by conditioning on the first move instead", appearing one at a time.
The same working the example does, in the order a tutor would write it.
Takeaway: Conditioning on the first move and conditioning on the last move produced the identical sum, 13, confirming ways3(5) is correct regardless of which end of the path is reasoned about first.
Ranking
Put in order
Put the moves of Tracing top-down memoized calls for the 3-step version into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. ways3(4) needs ways3(3), which needs ways3(2), which needs ways3(1), which needs ways3(0) - each of these is a first-time request, so each is a cache miss.
Worked example
Trace ways3(4) with a checked cache, tallying how many times each distinct index from 0 to 4 is actually requested, and how many of those requests are hits.
Recurse down the first branch to the base case
Why: ways3(4) needs ways3(3), which needs ways3(2), which needs ways3(1), which needs ways3(0) - each of these is a first-time request, so each is a cache miss. Requests for negative indices return 0 immediately and never touch the cache at all.
| index | first request? | result |
|---|---|---|
| 4 | miss | pending |
| 3 | miss | pending |
| 2 | miss | pending |
| 1 | miss | pending |
| 0 | miss - base case | 1 |
Unwind, and tally every later request to each index
Why: As the recursion unwinds, index 0 is requested 2 more times (both hits), index 1 is requested 2 more times (both hits), and index 2 is requested 1 more time (a hit) - each returning instantly instead of recursing again.
| index | total requests | misses | hits |
|---|---|---|---|
| 0 | 3 | 1 | 2 |
| 1 | 3 | 1 | 2 |
| 2 | 2 | 1 | 1 |
| 3 | 1 | 1 | 0 |
| 4 | 1 | 1 | 0 |
Verify the misses equal the number of distinct states
Why: Summing the misses column gives 1+1+1+1+1, which is 5 - exactly the 5 distinct indices, 0 through 4, that actually exist for this computation. Every one of the 5 hits is recursion that a checked cache made unnecessary.
\[ \text{misses} = 5 = \text{distinct states}, \quad \text{hits} = 5 \]
Picture it
Animation
Shows: Each line of the worked example "Tracing top-down memoized calls for the 3-step version", appearing one at a time.
The same working the example does, in the order a tutor would write it.
Takeaway: Summing the misses column gives 1+1+1+1+1, which is 5 - exactly the 5 distinct indices, 0 through 4, that actually exist for this computation. Every one of the 5 hits is recursion that a checked cache made unnecessary.
Concept
The naive call count T(n) and the memoized call count both follow simple formulas: memoized calls follow 2n minus 1 for n at least 1, while naive calls follow the much faster-growing T(n) = T(n-1) + T(n-2) + 1.
| n | naive calls | memoized calls |
|---|---|---|
| 5 | 15 | 9 |
| 6 | 25 | 11 |
| 10 | 177 | 19 |
By n equal to 10, the naive version has already made over nine times as many calls as the memoized version - and that gap keeps widening exponentially as n grows further.
Pattern
Step through it
Step through How fast the naive call count really grows one row at a time. What is driving the change, and what would the row after the last one be?
Concept
Both Fibonacci and climbing stairs, whether solved top-down with a cache or bottom-up with a table, share the identical complexity, because both have exactly n plus 1 distinct states.
\[ \text{time} = \Theta(n), \qquad \text{space} = \Theta(n) \]
That space can be trimmed further to a constant amount whenever the recurrence only ever looks back a fixed number of steps, as seen with the rolling-variable trick - but the time stays linear either way.
Analogy
Discussion prompt
Explain Time and space complexity, summarized by analogy to something with no CS3000 Algorithms in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
Both Fibonacci and climbing stairs, whether solved top-down with a cache or bottom-up with a table, share the identical complexity, because both have exactly n plus 1 distinct states.
Intuition
Every entry in a correctly filled cache or table is a small, already-proven fact - the true answer for a smaller version of the same problem. Reading a cached value is not a shortcut or a guess; it is reusing a proof already completed.
That is exactly why the base cases and the fill order matter so much: every fact has to be built only from facts that are already known, never from a guess still waiting to be confirmed.
Picture it
Animation
Shows: Memoization: each value computed once — a rendered Manim animation.
Rendered with Manim.
Takeaway: Six cells, six additions. The exponential tree collapses to a line.
Explain it to yourself
Discussion prompt
In The four-step DP setup recipe this move is made:
3. Pin down the base cases
Why is that legal? Name the rule or definition it rests on before you read on.
Hint: If you can only say "because that is what you do", the rule is the thing to go and find.
Answer:
Identify the smallest state or states whose answer is a known fact, not a computation, and get it exactly right - everything else depends on it.
Pattern
1. Define the state in plain words
Why: Name exactly what varies between subproblems and exactly what a single entry stores, before writing any formula.
2. Write the recurrence: what are the actual choices?
Why: List every way a state's answer can be built from smaller states' answers, then combine them the way the problem asks - here, always by adding.
3. Pin down the base cases
Why: Identify the smallest state or states whose answer is a known fact, not a computation, and get it exactly right - everything else depends on it.
4. Choose a direction: top-down with a checked cache, or bottom-up with a table
Why: Both use the identical state, recurrence, and base cases - this step only decides the mechanics of filling them in.
Real world
Discussion prompt
Outside this lesson: where does Dynamic Programming I: Memoization actually turn up? Name one concrete situation — a job, a piece of software someone ships, a decision somebody has to make — and say which part of The four-step DP setup recipe is doing the work in it.
Hint: Vague is the failure mode here. "Engineering" is not a situation; "deciding whether this build is fast enough to ship" is.
Answer:
That deck gives the two ingredients that make dynamic programming apply, then shows the naive exponential recursion for Fibonacci and why it recomputes the same values. It presents the two fixes - top-down memoization and bottom-up tabulation - through a repeatable method for defining the state, writing the recurrence, and choosing the base cases, with Fibonacci and climbing stairs fully hand-traced. It targets four real misconceptions: reaching for dynamic programming when the subproblems never actually overlap, a wrong or missing base case that corrupts an entire table, a memo that is written but never checked before recursing, and a state defined too ambiguously to support a clean recurrence.
Picture it
Animation
Shows: The DP recipe, every time — a rendered Manim animation.
Rendered with Manim.
Takeaway: Step two is where the thinking is; the rest is bookkeeping.
Elimination
Eliminate the wrong options
In the naive recursive call tree for fib(5), how many total times does the call fib(2) get made?
3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.
Survives elimination: A
Why: fib(2) appears three times in the tree: once under the first fib(3), once directly under fib(4), and once under the second fib(3) - matching the hand-traced call-count table from earlier in the lesson.
Check
Recall the naive recursive call tree for fib(5), traced earlier in this lesson.
Check your understanding
In the naive recursive call tree for fib(5), how many total times does the call fib(2) get made?
Answer: A
Why: fib(2) appears three times in the tree: once under the first fib(3), once directly under fib(4), and once under the second fib(3) - matching the hand-traced call-count table from earlier in the lesson.
Check
Recall the top-down memoized trace of fib(5): 9 total calls, made up of cache misses and cache hits.
Check your understanding
How many of the 9 total calls in the memoized fib(5) trace are cache hits - calls that return instantly with no further recursion?
Answer: A
Why: The trace found 6 cache misses, one fresh computation for each distinct index from 0 to 5, and 3 cache hits - the second requests for fib(3), fib(2), and fib(1) - which together make up all 9 calls.
Check
A student sets ways(0) equal to 0 instead of 1, while correctly keeping ways(1) equal to 1, and then fills the table using ways(n) = ways(n-1) + ways(n-2).
Check your understanding
Using this flawed base case, what value does the recurrence produce for ways(3)?
Answer: A
Why: With ways(0) wrongly set to 0, ways(2) becomes ways(1) plus ways(0), or 1 plus 0, which is 1; then ways(3) becomes ways(2) plus ways(1), or 1 plus 1, which is 2 - not the true value of 3.
Prediction
Predict first
Which state definition supports writing a clean recurrence for this problem?
Answer it in your own words, now, with nothing to choose from. The options are on the next slide — and picking the right one off a list is an easier skill than producing it.
Correct: dp[n] = the number of ways to reach stair n
Why: dp[n] pins down exactly one destination stair per state, so dp[n-1] and dp[n-2] each refer to one specific, unambiguous quantity - which is exactly what a recurrence needs to combine cleanly.
Check
Consider the climbing-stairs problem with 1-and-2 stair steps, and the goal of counting the number of distinct ways to reach the top.
Check your understanding
Which state definition supports writing a clean recurrence for this problem?
Answer: A
Why: dp[n] pins down exactly one destination stair per state, so dp[n-1] and dp[n-2] each refer to one specific, unambiguous quantity - which is exactly what a recurrence needs to combine cleanly.
Elimination
Eliminate the wrong options
What is the time complexity of the top-down memoized Fibonacci function, in terms of n?
3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.
Survives elimination: A
Why: There are only n plus 1 distinct states, indices 0 through n, and memoization ensures each is computed exactly once with a constant amount of work, making the total time linear in n.
Check
Recall that fib(n) has exactly n plus 1 distinct states, and memoization guarantees each one is computed exactly once.
Check your understanding
What is the time complexity of the top-down memoized Fibonacci function, in terms of n?
Answer: A
Why: There are only n plus 1 distinct states, indices 0 through n, and memoization ensures each is computed exactly once with a constant amount of work, making the total time linear in n.
Concept
Moves added today:
Moves you reused today:
Moves #13 and #12 are steps 1 and 2 of the skeleton. Everything after them is bookkeeping — which is exactly why the two of them are worth naming.
Full toolkit so far: #1 through #13.
Next session opens with you naming every one of these from memory, before any new material.
Counterexample
Discussion prompt
Moves #13 and #12 are steps 1 and 2 of the skeleton. Everything after them is bookkeeping — which is exactly why the two of them are worth naming.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Answer:
Next session opens with you naming every one of these from memory, before any new material.
Connect it up
Draw it
One page, no notation unless you need it: draw how these connect — The four-step DP setup recipe · Toolkit check-in: name them before you look · What dynamic programming actually is · Ingredient one: optimal substructure · Ingredient two: overlapping subproblems. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.
Recap
Every dynamic programming problem in this lesson followed the same shape: check for optimal substructure and overlapping subproblems, define a state in words, write a recurrence from that state, pick base cases, then fill the answer top-down with a cache or bottom-up with a table.
Memoization and tabulation are the same idea told in two directions - remembering an answer instead of recomputing it - and that one habit turns exponential recursion into linear time.
| mistake | the fix |
|---|---|
| Memoizing a problem with no repeated subproblems | Check for overlap first - a memo only pays off when the same state is asked for twice |
| A wrong or missing base case | Pin down every base case by hand before trusting a single filled-in row |
| A memo that is written but never checked | Always look up the cache before recursing, not just after |
| A state defined too vaguely to write a recurrence | Name exactly what a table entry stores before writing any formula |
Want this taught 1-on-1? Alexander tutors CS3000 Algorithms — $55/session, free consultation.