Dynamic Programming Foundations
Remember the answers you've already computed, and a mountain of repeated work collapses into a single pass.
Prerequisites
- Big-O & Asymptotic Analysis — DP is where
O(2ⁿ)becomesO(n). - Recursion & the Call Stack — we start from a plain recursion, then tame it.
- Divide & Conquer — DP's close cousin, and the thing it is not.
- Backtracking — exhaustive search; DP is what saves you when the search repeats itself.
The 10-second version
Dynamic programming is what you do when a recursive solution solves the same subproblem over and over. If those repeats exist, you can remember each answer the first time you compute it and reuse it forever after — turning an exponential recursion into a polynomial one. It only works when two things are true: the problem has overlapping subproblems (the same smaller question recurs) and optimal substructure (an optimal answer is built from optimal answers to the subproblems). When both hold, you get the two classic recipes: memoization (recurse top-down, cache the results) and tabulation (fill a table bottom-up, in dependency order). Same recurrence, two ways to evaluate it.
Why this existed
The name is the best story in algorithms, and it's completely true.
- 1950 — Richard Bellman arrives at the RAND Corporation, a think tank funded by the U.S. Air Force, to work on multistage decision processes — problems where you make a sequence of decisions over time and want the best overall plan.
- 1953 — Bellman formalizes his Principle of Optimality: whatever the first decision is, the remaining decisions must be optimal for the state that results from it. That principle is exactly what we now call optimal substructure.
- 1957 — he publishes the book Dynamic Programming, and the name enters the language forever.
- 1968 — Donald Michie coins "memoization" (from the Latin memorandum — "to be remembered") for the idea of caching a function's results as you go.
Here's the part people love, in Bellman's own retelling. In the 1950s, "mathematical research" had become politically toxic in Washington — his boss's boss, Defense Secretary Charles Wilson, "had a pathological fear and hatred of the word 'research'." Bellman was doing genuinely deep mathematics inside a defense-funded think tank and needed a project name no bureaucrat could object to. So he deliberately picked words that were scientific-sounding and impossible to attack:
- "programming" — not writing code! In 1950 the word meant planning / scheduling, the way it does in linear programming. Bellman meant "planning over time."
- "dynamic" — chosen precisely because it sounds impressive and has, in his words, "the property as an adjective that it's impossible to use the word 'dynamic' in a pejorative sense." Congressman-proof.
So the field's most impressive-sounding technique is, by its inventor's own account, a name cooked up to dodge a bureaucrat's temper. The math underneath is real; the branding is pure survival instinct.
Why it mattered then: multistage decision problems — resource allocation, scheduling, sequencing — were exactly what the Cold War machine needed solved, and naive exploration blew up exponentially. Bellman's insight let huge instances collapse to a table you could fill in polynomial time. That's not a trick; it's a paradigm.
Inside the inventor's head
Forget the historical problems for a second and look at the smallest possible symptom — the
Fibonacci sequence, fib(n) = fib(n − 1) + fib(n − 2) with fib(0) = 0 and fib(1) = 1.
The recursive code is three lines. Watch what it actually does for fib(5):
Stare at that tree. fib(3) is computed twice. fib(2) is computed three times. Every
one of those repeats is the same question with the same answer, recomputed from scratch.
For fib(40) this blossoms into roughly a billion calls where a few dozen would do. That
duplication is the disease, and it's the entire reason DP exists.
The pivot is almost embarrassingly simple once you see it: if the same question keeps coming back, write down the answer the first time and just look it up later. That's memoization. In code:
// NAIVE: recomputes the same values exponentially many times -> O(2^n)const fibNaive = (n) => (n < 2 ? n : fibNaive(n - 1) + fibNaive(n - 2));// MEMOIZED: each n is computed at most once -> O(n)const fibMemo = (n, cache = new Map()) => { if (n < 2) return n; if (cache.has(n)) return cache.get(n); // already solved -- reuse it const r = fibMemo(n - 1, cache) + fibMemo(n - 2, cache); cache.set(n, r); return r;};The difference is not a constant factor. It's the difference between two billion additions and forty.
The final approach
The two conditions
DP applies exactly when both of these hold:
- Optimal substructure — an optimal solution to the whole is built from optimal solutions to its subproblems. (This is Bellman's Principle of Optimality, verbatim.) If you can't define the answer to a bigger problem in terms of answers to smaller ones, there's nothing to table.
- Overlapping subproblems — the same subproblems recur across different branches of the recursion. Without overlap, you never recompute anything for the cache to help — and that's the divide-and-conquer world instead.
This is the crisp line between DP and divide & conquer: merge sort splits into disjoint
halves ([0, n/2) and [n/2, n)) that never overlap — no cache helps. Fibonacci splits into
n−1 and n−2, which overlap massively — the cache is everything.
Two ways to evaluate one recurrence
The recurrence is the same; only the evaluation order differs.
- Top-down (memoization): write the natural recursion, add a cache, and check it on entry. You compute only the subproblems you actually reach.
- Bottom-up (tabulation): figure out the order in which subproblems depend on each other, allocate a table, and fill it small-to-large. No recursion, no call-stack depth limits.
The invariant
The correctness argument is the same for both styles: every table cell (or cache entry)
dp[k] holds the exact answer to the subproblem of size k. Bottom-up you prove it by
induction on k — the base cases are correct, and each cell is computed from smaller cells
that are already correct. Top-down you prove it by strong induction on the argument: the
recursion returns the right value for every input it's called with, and the cache never
changes that value.
A worked trace
Fill the Fibonacci table bottom-up for n = 6:
k | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
dp[k] | 0 | 1 | 1 | 2 | 3 | 5 | 8 |
Each cell is dp[k−1] + dp[k−2] — a single addition reading two cells already filled to its
left. That's the whole algorithm, and it's the same recurrence the recursion expressed, just
evaluated in an order that never repeats work.
The full implementation
// TOP-DOWN: recursion + cachefunction fibTop(n, cache = new Map()) { if (n < 2) return n; if (cache.has(n)) return cache.get(n); const r = fibTop(n - 1, cache) + fibTop(n - 2, cache); cache.set(n, r); return r;}// BOTTOM-UP: fill the table, then read off the answerfunction fibBottom(n) { if (n < 2) return n; const dp = [0, 1]; for (let k = 2; k <= n; k++) dp[k] = dp[k - 1] + dp[k - 2]; return dp[n];}Complexity
| Approach | Time | Space | Notes |
|---|---|---|---|
| Naive recursion | O(2ⁿ) | O(n) stack | recomputes everything |
| Top-down memoization | O(n) | O(n) cache + O(n) stack | computes only reachable states |
| Bottom-up tabulation | O(n) | O(n) table | no recursion; ideal for tight loops |
| Bottom-up + rolling vars | O(n) | O(1) | only the last two cells are ever read |
To see the contrast with divide and conquer concretely: merge sort's recursion is T(n) = 2·T(n/2) + Θ(n) = Θ(n log n) — the master theorem handles it because the subproblems are
disjoint. Fibonacci's recursion is T(n) = T(n−1) + T(n−2) + Θ(1) = Θ(φⁿ) — exponential —
purely because of overlap. Memoization is the surgical fix for exactly that overlap.
Where you meet it today
- Bioinformatics. Sequence alignment (Needleman-Wunsch, Smith-Waterman) is DP over two strings, and it's how genomes get compared at all.
- Version control & editors.
git diffand your editor's "you changed this word" come from a DP edit-distance computation between two lines. - Route planning & networking. Bellman-Ford — literally Bellman's algorithm — is DP on shortest paths, and it still runs in routing protocols.
- Reinforcement learning. The Bellman equation is the Principle of Optimality wearing an AI hat; Q-learning and value iteration are dynamic programming over an unknown model.
// A generic memoizer -- the essence of top-down DP.function memoize(fn) { const cache = new Map(); return (x) => { if (!cache.has(x)) cache.set(x, fn(x)); // compute once... return cache.get(x); // ...and reuse forever };}Recap
- DP is the cure for overlapping subproblems — the same smaller question asked again and again.
- It needs optimal substructure (solve big from optimal small) and overlap (subproblems recur). No overlap means it's divide and conquer instead.
- Memoization is top-down: recursion plus a cache. Tabulation is bottom-up: fill a
table in dependency order. Same recurrence, same
O(n)answer. - The name is a marketing lie — "dynamic programming" means "impressive-sounding planning," chosen to keep a Defense Department boss happy.
- The invariant: every cached cell holds the exact answer to its subproblem.
Next up: 1D Dynamic Programming — taking this idea to the classic one-dimension problems, and squeezing the table down to a couple of variables.
Backtracking
Build a solution one choice at a time, and the instant a choice can't work, undo it and try the next — a disciplined depth-first walk of the search tree.
1D Dynamic Programming
State, recurrence, base case — the three-line recipe, applied to sequences where each answer depends on only the last few.