Memoization vs Tabulation
There are two ways to write any DP solution. They produce the same answers and have the same big-O time complexity. They differ in how natural the code feels, how much memory they use, and how easy they are to optimize further.
| Aspect | Memoization (top-down) | Tabulation (bottom-up) |
|---|---|---|
| Style | Recursion + cache | Iterative loop filling a table |
| Direction | Start at the answer, recurse down | Start at the base case, build up |
| Visits | Only the subproblems you need | Every subproblem in the table |
| Stack | Uses recursion stack (O(state depth)) | No recursion stack |
| Easier to write? | Yes — mirrors the recurrence | Needs you to pick the fill order |
| Faster constant? | Slightly slower (function calls) | Slightly faster (tight loops) |
| Space-optimizable? | Hard | Easy — drop the table to a row or two |
If you’re stuck, start with memoization. Once it works, consider converting to tabulation for speed or to unlock space optimization. Don’t convert before it works — it’s a recipe for double bugs.
The same problem, three ways
Let’s use the same problem — climbing stairs — and write it as plain recursion, top-down DP, and bottom-up DP, side by side.
Style 1 — pure recursion (the brute force)
Correct. Time: O(2^n). Space: O(n) recursion depth. Times out on the LeetCode judge around n = 35.
Style 2 — memoization (top-down DP)
Add a cache. Check the cache before recursing; fill the cache before returning.
Time: O(n). Space: O(n) for the cache + O(n) recursion stack.
Notice how the body of the function is identical to the brute force — only the cache check and write are new. This is the memoization template: take any recursion, sandwich the body between a cache hit-check and a cache write-back. Done.
Style 3 — tabulation (bottom-up DP)
Replace recursion with a loop that fills the table in order.
Time: O(n). Space: O(n). No recursion, no stack.
And because each cell only depends on the previous two, we can collapse the entire table to two variables:
Time: O(n). Space: O(1). This is the space-optimized bottom-up form — and it’s why tabulation is worth converting to for problems with very tight rolling dependencies.
How to convert memoization to tabulation
The mechanical recipe:
Identify the state variables and their ranges
If your memo is keyed on (i, j) with 0 <= i <= n and 0 <= j <= m, your table is (n + 1) x (m + 1).
Identify the dependency direction
Look at what f(i, j) recursively calls. If it calls f(i - 1, j) and f(i, j - 1), then computing row i needs row i - 1 already done. So you fill the table in increasing i, increasing j order. Always: the cell you’re computing must depend only on cells already filled.
Translate base cases into table initialization
if (i == 0) return ... in the recursion becomes dp[0][...] = ... in the table, set before the loops.
Translate the recursive body into a loop body
Replace every f(a, b) recursive call with dp[a][b]. Replace the return with an assignment to dp[i][j].
Return dp[final state]
Whatever state you would have started the recursion with — dp[n], dp[n - 1][m - 1], etc.
Once you’ve done this conversion three or four times, you can do it in your head while reading the problem.
When to prefer each style
Prefer memoization when
- The state space is large but sparse. If most subproblems are never visited, tabulation wastes time filling the irrelevant cells. Memoization only computes what it needs.
- The transitions are complex. Multi-way recursion (try every split, every subset, every partition) is much easier to express as code that recurses than as a multi-loop table fill.
- You’re still figuring the problem out. Get the recursion right, slap a cache on, ship it. Optimize later if needed.
Prefer tabulation when
- You want to space-optimize. Tabulation lets you keep only the rows you still need (one or two at a time), often dropping
O(n × m)memory down toO(m)orO(1). Memoization can’t do this — the cache has to hold every visited cell. - You’re running into stack overflow. Deep recursion (state depth
>~10,000 in most languages) blows the stack. Tabulation has no stack. - You want the absolute fastest constant factor. Tight inner loops with no function-call overhead matter on heavily-judged problems and competitive-programming benchmarks.
A side-by-side cheat sheet
For the same DP problem, here’s how the two styles compare on every axis:
| Step | Memoization | Tabulation |
|---|---|---|
| Write base case | if (...) return ... at top of function | dp[base index] = ... before loop |
| Write transition | return f(smaller) + f(other smaller) | dp[i] = dp[i - 1] + dp[i - 2] |
| Cache | if memo[i] is set, return memo[i] | (implicit — array is the cache) |
| Call site | return f(n) | return dp[n] |
| Direction | Top-down: needed cells first | Bottom-up: every cell, in order |
A subtle gotcha: the base case in memo arrays
When you use -1 (or any sentinel value) to mean “not yet computed,” the sentinel must be a value the function can never legitimately return. If your DP computes counts that can be -1 (some Manhattan-distance puzzle, or a problem with “no solution → -1” outputs), pick a different sentinel — or use a parallel boolean computed[i] array, or use language-native nullable types (Integer in Java, Optional<int> in C++17, None in Python).
This is the kind of bug that doesn’t crash — it just silently returns wrong answers. Watch for it.
Quick check
Next: the classic patterns — seven shapes that show up in nearly every DP interview problem.