The Archive
Paradigms & Dynamic Programming

Dynamic Programming Foundations

Remember the answers you've already computed, and a mountain of repeated work collapses into a single pass.

Prerequisites

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.

Rendering diagram…

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):

Rendering diagram…

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 recursion vs memoized recursion
// 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;};
switch:

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.
Rendering diagram…

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.

Rendering diagram…
  • 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:

k0123456
dp[k]0112358

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

Memoization (top-down) vs tabulation (bottom-up)
// 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];}
switch:

Complexity

ApproachTimeSpaceNotes
Naive recursionO(2ⁿ)O(n) stackrecomputes everything
Top-down memoizationO(n)O(n) cache + O(n) stackcomputes only reachable states
Bottom-up tabulationO(n)O(n) tableno recursion; ideal for tight loops
Bottom-up + rolling varsO(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 diff and 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.
Memoization in the wild: a generic cache
// 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  };}
switch:

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.

On this page