Data structures & algorithms · Dynamic programming
Dynamic programming: the fixed method
One recursion, four ways to write it. This page takes climbing stairs from 15 calls down to 6 table cells, and shows you the exact repeated work that a cache deletes.
Watch the repeats pile up, then vanish →01 The idea
The same answer, worked out over and over
A staircase has n steps. You can take 1 step or 2 steps at a time. How many different ways are there to reach the top? The recursion writes itself: the last move onto step n was either a 1-step from step n − 1 or a 2-step from step n − 2, so the number of ways to reach n is the number of ways to reach n − 1 plus the number of ways to reach n − 2. A rule like that, which builds one answer out of smaller answers of the same kind, is called a recurrence.
Write it as plain recursion and it works — and it is unusably slow. Not because the rule is wrong, but because the call tree asks for the same value again and again. For n = 5 it makes 15 calls to produce 6 distinct answers. Dynamic programming is the fix, and it is always the same fix: remember each answer the first time you work it out.
02 Hand trace
climb(5): 15 calls for 6 answers
Run the plain recursion on n = 5 and count how often each value is asked for. Every box below is one call that runs the whole computation from scratch. The first box in a row is the call that genuinely had to happen; every rose box after it recomputes a number this run already worked out.
Count the boxes: 1 + 1 + 2 + 3 + 5 + 3 = 15 calls, and only 6 of them are the first time that value has been seen. The other 9 are pure waste, and the waste grows fast — climb(10) makes 177 calls and climb(30) makes 2,692,537. Now fill the same six answers into a table instead, smallest first, and read what each new cell needs.
Six cells, four additions, and every cell is written exactly once. The answers are identical — 1, 1, 2, 3, 5, 8 — because the rule never changed. What changed is the order: the recursion works top-down and keeps asking for values it has not stored, while the table works bottom-up and every value it needs is already sitting to its left. That is the whole insight, and the code in section 03 is four ways of writing it down.
03 The four forms
One recursion, four ways to write it
This is the method, and it does not change from problem to problem. Write the recursion. Add a cache. Turn the cache into a table you fill in order. Then throw away the parts of the table you no longer read. Each step below is a small edit to the one before it, and every version returns the same 8 for n = 5.
Form 1 · plain recursion
function climb(n): if n <= 1: return 1 return climb(n - 1) + climb(n - 2)# climb(5) = 8, after 15 calls
Form 2 · memoisation, top-down
memo = empty mapfunction climb(n): if n <= 1: return 1 if n in memo: return memo[n] memo[n] = climb(n - 1) + climb(n - 2) return memo[n]
Form 3 · tabulation, bottom-up
function climb(n): if n <= 1: return 1 dp = new array of size n + 1 dp[0] = 1 dp[1] = 1 for i from 2 to n: dp[i] = dp[i - 1] + dp[i - 2] return dp[n]
Form 4 · two rolling variables
function climb(n): if n <= 1: return 1 prev, curr = 1, 1 # dp[0], dp[1] for i from 2 to n: prev, curr = curr, curr + prev return curr
Form 1 → 2: the map is the whole optimisation. Two lines change — check memo before recursing, write to it after. The second climb(3) in the trace now returns from the map instead of rebuilding the subtree beneath it, so each of the n + 1 states is built once. The call count drops from 2 × climb(n) − 1 to 2n − 1: for n = 5 that is 15 calls down to 9, and for n = 30 it is 2,692,537 down to 59.
Form 2 → 3: once every state is built once, you can choose the order. dp[i] only ever reads i - 1 and i - 2, so filling left to right guarantees both are ready. There are no calls left, so the O(n) call stack disappears and a large n can no longer overflow it. This is the version you write when n is big.
Form 3 → 4: keep only what the recurrence still reads. After writing dp[i], every cell from 0 up to i − 2 is dead — the next cell reads only i and i − 1 — so holding the whole array is waste. Two variables carry that live window of two, and space drops to O(1). What you give up is the table itself — if the question asks which sequence of steps produced the answer, you need Form 3 to walk back through.
Get dp[0] = 1 right or everything after it is wrong. There is exactly one way to climb zero steps: take no steps. Set dp[0] = 0 instead and the table reads 0, 1, 1, 2, 3, 5 — you get 5 for n = 5 rather than 8, and the bug looks like an off-by-one rather than a bad base case.
05 Cheat sheet
The four forms, and what each costs
| Form | Time | Space | What climb(5) costs |
|---|---|---|---|
| 1 · Plain recursion | O(2ⁿ) | O(n) stack | 15 calls, 9 of them repeats |
| 2 · Memoisation, top-down | O(n) | O(n) map + O(n) stack | 9 calls, 4 additions |
| 3 · Tabulation, bottom-up | O(n) | O(n) table, no stack | 6 cells, 4 additions |
| 4 · Two rolling variables | O(n) | O(1) | 2 variables, 4 additions |
06 Where & why
When a table is the right answer
Dynamic programming is not a data structure and not a category of problem. It is what you do when a correct recursion turns out to recompute. So the honest test is never “is this a DP problem” — it is “does my recursion ask for the same input twice”.
Reach for it when…
Use something else when…
| Situation | Better choice | Why |
|---|---|---|
| Every subproblem in the tree is distinct — merge sort, quicksort, binary search | Plain divide and conquer | Nothing overlaps, so a cache only adds memory and lookup cost for zero hits. |
| One locally best choice is provably safe — activity selection, Huffman coding | A greedy algorithm | A single pass, no table at all, and usually O(n log n) from the sort rather than O(n) plus a table. |
| You need the actual sequence of choices, not just the value | Form 3, the full table | Form 4 throws away every earlier cell, so there is nothing left to walk backwards through. |
| The state needs a subset — every combination of 40 items | Meet in the middle, branch and bound, or an approximation | The table would have 2⁴⁰ rows. Being O(n) per cell does not help when there are too many cells to store. |
07 Interview questions
Say these out loud
The question that separates people is not the definition. It is being asked to name the repeated subproblem in a recursion you just wrote.
What is dynamic programming?
What two properties does a problem need before DP helps?
How is that different from divide and conquer?
Walk me through climbing stairs for n = 5.
Why is the plain recursion exponential?
Does memoisation change the complexity or just the constant?
Memoisation or tabulation — which would you write?
What exactly is “the state”, and how do you pick it?
Can you always cut the space down to O(1)?
Why is this the same as Fibonacci?
When would you actually use DP at work?
08 Practice problems
Same method, six times
For each one, write the recursion first and say in one sentence what dp[i] means. Only then decide whether to memoise it or fill a table.