Dynamic Programming And Greedy
Memoization vs. Tabulation
Back in Module 1, Chapter 2, you saw naive recursive Fibonacci flagged as O(2ⁿ):
Jr Codex DSA Notes
Level: Advanced Prerequisites: Module 9, Chapter 4 Time to complete: ~30 minutes
Table of Contents
- Why Naive Recursion Can Be Slow
- Overlapping Subproblems — the Root Cause
- Top-Down: Memoization
- Using
functools.lru_cache - Bottom-Up: Tabulation
- Memoization vs. Tabulation — Tradeoffs
- The General DP Recipe
- Summary & Next Steps
1. Why Naive Recursion Can Be Slow
Back in Module 1, Chapter 2, you saw naive recursive Fibonacci flagged as O(2ⁿ):
def fib_naive(n):
if n <= 1:
return n
return fib_naive(n - 1) + fib_naive(n - 2)For n = 5, this doesn't feel slow. For n = 40, it takes noticeably long. For n = 100, it will not finish in your lifetime. Dynamic programming exists to fix exactly this class of problem — and the fix doesn't require a different algorithm, just a smarter way of running the same recursive idea.
2. Overlapping Subproblems — the Root Cause
Draw the recursion tree for fib_naive(5) (as introduced conceptually in Module 4, Chapter 2) and a pattern jumps out: fib_naive(3) gets computed twice, fib_naive(2) gets computed three times, and so on. The function keeps re-deriving answers it has already computed.
fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \ / \ / \
fib(2) fib(1) fib(1)fib(0) fib(1) fib(0)
This property — overlapping subproblems — is the signal that a problem is a DP candidate. (The other required property, optimal substructure, means the optimal answer to the whole problem can be built from optimal answers to its subproblems — true here since fib(n) is defined directly in terms of fib(n-1) and fib(n-2).) If neither property holds, memoizing won't help — there's nothing to reuse.
3. Top-Down: Memoization
Memoization keeps the exact same recursive structure, but caches each subproblem's answer the first time it's computed, so repeat calls return instantly instead of re-deriving the answer:
def fib_memo(n, cache=None):
if cache is None:
cache = {}
if n in cache:
return cache[n] # already solved — O(1) lookup
if n <= 1:
return n
cache[n] = fib_memo(n - 1, cache) + fib_memo(n - 2, cache)
return cache[n]Now each value from 0 to n is computed exactly once. Time complexity drops from O(2ⁿ) to O(n) — every subproblem does O(1) work beyond its (cached) recursive calls. Space complexity is O(n) for the cache, plus O(n) for the recursion call stack (Module 1, Chapter 4) — still O(n) overall, but a real cost worth naming.
This is called top-down because you start at the original problem (fib(n)) and recurse down toward the base cases, caching along the way.
4. Using functools.lru_cache
Python's standard library provides a decorator that does exactly this caching for you, with no manual dictionary:
from functools import lru_cache
@lru_cache(maxsize=None)
def fib_lru(n):
if n <= 1:
return n
return fib_lru(n - 1) + fib_lru(n - 2)
# Same O(n) time as the manual cache — lru_cache handles memoization
# automatically based on the function's arguments.lru_cache is the pragmatic default for interview settings and quick scripts. The manual-dictionary version in Section 3 is worth knowing because it generalizes to cases where the cache key isn't just the function's arguments (e.g., caching on a mutable derived state).
5. Bottom-Up: Tabulation
Tabulation flips the direction: instead of starting at fib(n) and recursing down, you start at the base cases and iteratively build up to fib(n), storing each result in a table (often just a list or a couple of variables) as you go.
def fib_tabulation(n):
if n <= 1:
return n
table = [0] * (n + 1)
table[1] = 1
for i in range(2, n + 1):
table[i] = table[i - 1] + table[i - 2]
return table[n]
# O(n) time, O(n) space for the tableSince each fib(i) only ever needs the previous two values, the table can be compressed to two variables:
def fib_optimized(n):
if n <= 1:
return n
prev2, prev1 = 0, 1
for _ in range(2, n + 1):
prev2, prev1 = prev1, prev2 + prev1
return prev1
# O(n) time, O(1) space — no recursion stack, no growing tableThis last version is strictly better than every memoized version above on space, precisely because tabulation has no call stack and, here, doesn't even need the full table — only recursion inherently requires stack space.
6. Memoization vs. Tabulation — Tradeoffs
| Memoization (top-down) | Tabulation (bottom-up) | |
|---|---|---|
| Direction | Starts at the goal, recurses to base cases | Starts at base cases, builds up to the goal |
| Code shape | Looks like the natural recursive solution + a cache | Requires figuring out the right iteration order upfront |
| Space | Cache + recursion call stack (Module 1, Ch. 4) | Just the table (often compressible, sometimes to O(1)) |
| Handles sparse subproblems well | Yes — only computes subproblems actually needed | No — typically computes every subproblem up to n, even unused ones |
| Risk | Deep recursion can hit Python's recursion limit for large n | None — pure iteration |
Neither is universally "better." Memoization is usually the faster path to a correct solution because it mirrors the recursive definition directly. Tabulation is usually the better final solution for production code, since it avoids recursion-depth limits and often reduces space further, as shown above.
7. The General DP Recipe
Every DP problem in this module follows the same steps:
- Write the naive recursive solution first, even if it's exponential — it's the clearest way to express what the problem is asking.
- Identify the recursive relationship (how does the answer to
ndepend on smaller subproblems?). - Confirm overlapping subproblems exist (would the same subproblem get solved more than once?).
- Add memoization (a cache dict or
@lru_cache) — this alone usually fixes the exponential blowup. - Optionally convert to tabulation if you want to avoid recursion depth limits or shrink space further.
Chapters 2 and 3 apply this recipe to progressively harder problems.
8. Summary & Next Steps
Key Takeaways
- DP applies when a problem has overlapping subproblems and optimal substructure — without both, memoizing does nothing.
- Memoization (top-down) caches a recursive solution's results; tabulation (bottom-up) iteratively builds the answer from the base cases up.
- Both typically bring naive exponential recursion down to
O(n)orO(n²)— the specific improvement depends on the problem. - Tabulation avoids recursion-stack space entirely and can sometimes compress further (as with Fibonacci's
O(1)-space version); memoization is often faster to write correctly on the first try.
Concept Check
- What two properties must a problem have for DP to help at all?
- Why does memoized Fibonacci still use
O(n)space, even though the exponential blowup is gone? - Why can the tabulated Fibonacci solution be compressed to
O(1)space, but the memoized version generally can't?
Next Chapter
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index