13.4 Tabulation
Memoization evaluates states on demand through recursion.
13.4 Tabulation
Memoization evaluates states on demand through recursion. Tabulation takes the opposite approach. Instead of starting from the final answer and recursively exploring smaller subproblems, tabulation starts from the smallest subproblems and systematically builds larger solutions until the final answer is reached.
Tabulation is often called bottom-up dynamic programming. Most production-quality dynamic programming implementations eventually use tabulation because it eliminates recursion overhead, provides predictable memory access patterns, and makes space optimization easier.
Problem
Given a dynamic programming state and recurrence relation, how can we compute all states iteratively without recursion?
Understanding Bottom-Up Computation
Consider Fibonacci numbers.
Recurrence:
$$ F(n)=F(n-1)+F(n-2) $$
Memoization starts with:
F(n)
and recursively requests:
F(n-1)
F(n-2)
Tabulation begins with the smallest known states:
F(0)
F(1)
and computes:
F(2)
F(3)
F(4)
...
F(n)
The computation proceeds in dependency order.
The Core Idea
Every recurrence creates a dependency graph.
For Fibonacci:
F(5)
├── F(4)
└── F(3)
F(4)
├── F(3)
└── F(2)
Observe:
F(2)
must be computed before:
F(3)
and:
F(3)
must be computed before:
F(4)
The dependency graph naturally determines the order of evaluation.
Recipe 1: Identify Base States
Every tabulation algorithm begins with known values.
For Fibonacci:
$$ dp[0]=0 $$
$$ dp[1]=1 $$
These entries require no computation.
They serve as the foundation for all later states.
Recipe 2: Determine Dependency Order
Consider:
$$ dp[i] = dp[i-1] + dp[i-2] $$
To compute:
dp[i]
both dependencies must already exist.
Natural iteration:
for i := 2; i <= n; i++ {
dp[i] = dp[i-1] + dp[i-2]
}
The loop follows the recurrence dependencies.
Example: Fibonacci
Tabulation implementation:
func fib(n int) int {
if n <= 1 {
return n
}
dp := make([]int, n+1)
dp[0] = 0
dp[1] = 1
for i := 2; i <= n; i++ {
dp[i] = dp[i-1] + dp[i-2]
}
return dp[n]
}
Complexity:
$$ O(n) $$
Memory:
$$ O(n) $$
No recursion is required.
Example: Staircase Problem
State:
$$ dp[i] $$
Meaning:
ways to reach step i
Recurrence:
$$ dp[i] = dp[i-1] + dp[i-2] $$
Base cases:
$$ dp[0]=1 $$
$$ dp[1]=1 $$
Implementation:
dp[0] = 1
dp[1] = 1
for i := 2; i <= n; i++ {
dp[i] = dp[i-1] + dp[i-2]
}
The structure is identical to Fibonacci.
Example: Grid Paths
Problem:
Move only right or down.
Count paths.
State:
$$ dp[i][j] $$
Meaning:
paths to cell (i,j)
Recurrence:
$$ dp[i][j] = dp[i-1][j] + dp[i][j-1] $$
Dependency graph:
(i-1,j)
|
v
(i,j)
(i,j-1) -> (i,j)
Valid iteration:
for i := 0; i < rows; i++ {
for j := 0; j < cols; j++ {
...
}
}
because:
top cell already computed
left cell already computed
before reaching the current cell.
Example: Coin Change
Problem:
minimum coins
State:
$$ dp[x] $$
Meaning:
minimum coins for amount x
Recurrence:
$$ dp[x] = 1+ \min(dp[x-c]) $$
Initialization:
$$ dp[0]=0 $$
All other states:
$$ \infty $$
Implementation:
for amount := 1; amount <= target; amount++ {
for _, coin := range coins {
if amount >= coin {
dp[amount] = min(
dp[amount],
dp[amount-coin]+1,
)
}
}
}
Amounts grow from small to large.
Dependencies are always available.
Example: Longest Common Subsequence
State:
$$ dp[i][j] $$
Meaning:
LCS length
between prefixes
Recurrence:
If characters match:
$$ dp[i][j] = 1+dp[i-1][j-1] $$
Otherwise:
$$ dp[i][j] = \max ( dp[i-1][j], dp[i][j-1] ) $$
Dependency directions:
up
left
upper-left
Therefore row-major iteration works naturally:
for i := 1; i <= n; i++ {
for j := 1; j <= m; j++ {
...
}
}
All required states already exist.
Building a DP Table
A useful mental model:
-
Draw the state space.
-
Mark base states.
-
Draw dependency arrows.
-
Find an ordering where every dependency appears earlier.
-
Iterate according to that ordering.
This method works for most tabulation problems.
Topological Thinking
Every dynamic programming recurrence forms a directed acyclic graph.
Example:
state A
depends on B and C
Graph:
B -> A
C -> A
Tabulation processes states in topological order.
This guarantees every dependency is available when needed.
Memoization vs Tabulation
| Property | Memoization | Tabulation |
|---|---|---|
| Direction | Top-down | Bottom-up |
| Uses recursion | Yes | No |
| Computes needed states only | Yes | No |
| Stack usage | O(depth) | O(1) |
| Cache locality | Often poor | Usually good |
| Space optimization | Harder | Easier |
| Natural recurrence expression | Excellent | Moderate |
Both approaches ultimately compute the same state graph.
The difference lies in traversal order.
Common Mistakes
Wrong Iteration Order
Suppose:
$$ dp[i] = dp[i+1] +1 $$
Then increasing iteration fails.
Dependencies are unavailable.
Always follow dependency direction.
Missing Initialization
If base states are incorrect:
all subsequent states become incorrect
Initialization errors often propagate through the entire table.
Invalid Boundaries
Grid problems commonly access:
i-1
j-1
Boundary rows and columns require special handling.
Incorrect Default Values
For minimization problems:
initialize with infinity
For maximization problems:
initialize with negative infinity
For counting problems:
initialize with zero
The wrong default value silently corrupts results.
Space Optimization Preview
Observe Fibonacci:
$$ dp[i] = dp[i-1] + dp[i-2] $$
Only two previous states are needed.
The entire table is unnecessary.
Instead:
prev2 := 0
prev1 := 1
for i := 2; i <= n; i++ {
current := prev1 + prev2
prev2 = prev1
prev1 = current
}
Memory drops from:
$$ O(n) $$
to:
$$ O(1) $$
Many dynamic programming algorithms admit similar optimizations.
When Tabulation Is Preferable
Tabulation is usually preferred when:
- all states are reachable
- recursion depth may be large
- cache locality matters
- memory optimization is important
- predictable performance is required
Competitive programming and high-performance systems often favor tabulation for these reasons.
Complexity Analysis
Let:
$$ S $$
be the number of states.
Let:
$$ K $$
be the transition cost.
Each state is processed once.
Total running time:
$$ O(S \times K) $$
Memory usage:
$$ O(S) $$
before any space optimization.
The asymptotic complexity usually matches memoization.
Practical performance is often better because recursion overhead disappears.
Takeaway
Tabulation computes dynamic programming states iteratively in dependency order. Starting from known base cases, it fills the state space until the final answer becomes available. Compared to memoization, tabulation eliminates recursion, improves memory locality, simplifies space optimization, and provides highly predictable execution behavior. For many large-scale dynamic programming problems, tabulation becomes the preferred implementation strategy once the recurrence relation is fully understood.