Module P-11 — Dynamic Programming I: Memoization and Tabulation
What this module covers: Module P-1 flagged naive recursive Fibonacci's O(2ⁿ) blowup and promised a fix — this module delivers it. Dynamic programming is the systematic version of Module F-4's "remember what you've seen" idea, applied to overlapping recursive subproblems instead of array elements. This module covers both directions of the same idea — memoization (top-down) and tabulation (bottom-up) — using the exact same recursive structure each time, just changing how results get stored and reused.
Where the Waste Comes From
Recall Module P-1's naive Fibonacci: every call branches into two further calls, and those calls overlap heavily.
typescript
fibNaive(5) calls fibNaive(4) and fibNaive(3) — but fibNaive(4)also independently calls fibNaive(3), recomputing an entire subtree of work that was already computed moments earlier, just in a different branch of the call tree. This is the defining feature of a dynamic programming problem: overlapping subproblems — the same smaller input recurs many times across different branches of the recursion.
Memoization: Remember, Then Check Before Recomputing
The fix is the identical idea from Module F-4's Two Sum: before doing the expensive work, check a hash map to see if you've already computed this exact result.
typescript
The recursive structure is completely unchanged from fibNaive — same base case, same recursive case. The only addition is cache: check it first, and populate it before returning. This single change collapses the complexity from O(2ⁿ) to O(n) — each distinct value of n is now computed exactly once, ever, no matter how many different branches of the original recursion would have asked for it. This is called top-down dynamic programming: you still start from the original question (fib(n)) and recurse downward toward the base case, same as before — you've just added a memory so no subproblem gets solved twice.
Tabulation: Build the Answer Bottom-Up, No Recursion at All
Tabulation solves the same overlapping-subproblems issue from the opposite direction: instead of starting at n and recursing down to the base cases, start at the base cases and iteratively build up to n, filling a table (usually just an array) as you go.
typescript
Same O(n) time as the memoized version, but with two practical differences worth naming: no recursion at all, so no call stack depth to worry about (Module P-1's O(n) stack-space concern simply doesn't apply here — this is a for loop), and the order of computation is explicit and iterative rather than following the shape of a call tree.
Climbing Stairs: The Same Three-Step Progression, Applied Fresh
"How many distinct ways can you climb n stairs, taking either 1 or 2 steps at a time?" — worth walking through the identical naive → memoized → tabulated progression on a different problem, to see that this isn't a Fibonacci-specific trick.
typescript
Notice this is structurally identical to the Fibonacci progression — same recursive relationship (table[i] depends on the two values before it), same three versions, same complexity improvement at each step. Recognizing "this problem's answer at position i depends on a small number of smaller positions" is the actual skill; once you see that relationship, memoization and tabulation are close to mechanical translations of it.
Compressing Space: You Often Don't Need the Whole Table
For both Fibonacci and Climbing Stairs, table[i] only ever depends on the two values immediately before it — not the entire history. That means the full array is more space than actually necessary:
typescript
Same O(n) time, but now O(1) space instead of O(n) — because only the last two values are ever needed to compute the next one, keeping the full table array around was pure overhead. This optimization doesn't apply to every DP problem (Module P-12 onward includes problems that genuinely need the full table, or a full 2D table), but it's always worth checking: does this problem's recurrence only look back a fixed, small number of steps? If so, a rolling set of variables replaces the array outright.
Where This Shows Up in Your Stack
In-process memoization for expensive pure functions — an API handler that computes something costly from the same small set of inputs repeatedly (a pricing calculation, a report aggregation) can wrap the function in a Map-based cache exactly like fibMemo's cache parameter, turning repeated calls with the same arguments into O(1) lookups after the first.
This is a different tool from Redis/HTTP caching (Module F-4's preview), though the underlying idea — remember, don't recompute — is identical: memoization here is in-process, tied to a single function's call graph and typically cleared when the process restarts; a cache layer like Redis is external, shared across processes, and persists independently of any one function's lifetime.
Database query result caching: recomputing an expensive aggregate query on every request when the underlying data hasn't changed is the exact same wasted-recomputation problem as fibNaive — caching the result (keyed by query parameters, the same idea as keying cache by n) until the underlying data invalidates it avoids redoing identical work repeatedly.
Build systems and incremental compilation (Webpack, Turbopack, Next.js's build cache) skip recompiling files whose inputs haven't changed since the last build — tabulation's "build up from what's already known" logic, applied to a dependency graph instead of a numeric sequence.
Summary
Overlapping subproblems — the same smaller input recurring across many branches of a recursive call tree — is what makes a problem "dynamic programming," and it's exactly what made naive Fibonacci O(2ⁿ).
Memoization (top-down) keeps the original recursive structure and adds a cache (Module F-4's hash map) checked before recursing — collapsing exponential blowup down to linear, since each distinct subproblem is now solved exactly once.
Tabulation (bottom-up) solves the same recurrence iteratively, building answers from the base cases upward — same complexity improvement, but with no recursion or call stack depth involved at all.
The two are structurally interchangeable: the same underlying recurrence relationship, expressed either as "recurse down and remember" or "iterate up and record."
Space can often be compressed further when a recurrence only depends on a small, fixed number of previous results — replacing a full O(n) table with a handful of rolling variables, O(1) space.
The next module, Dynamic Programming II: Classic 1D Problems, applies this exact memoization/tabulation progression to House Robber, Coin Change, and Decode Ways — problems where figuring out what table[i] should represent is the real challenge, not the DP mechanics themselves.
Knowledge Check
What specifically qualifies naive recursive Fibonacci as a dynamic programming problem, according to the module's definition?
In fibMemo, what does the line if (cache.has(n)) return cache.get(n)!; actually prevent, and why is this the mechanism that changes the complexity from O(2ⁿ) to O(n)?
Why is fibConstantSpace able to achieve O(1) space, while fibTabulation (the full-table version) uses O(n) space, even though both compute the exact same result in O(n) time?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.