Pattern 14: Dynamic Programming (Memoization vs. Tabulation)
Pattern guideUpdated
On this page
- 0. The Template (Copy-Paste Skeleton)
- The 5-step method — follow it in this order, every time
- Pattern → recurrence lookup
- Debugging your DP thinking
- 1. Pattern Identification & Logic
- How to Identify (Interview Triggers)
- Two Properties Required for DP
- The Workflow (3-Step Mindset)
- The ‘Trick’ to Know
- 2. The 5 Core DP Patterns
- Pattern A: 0/1 Knapsack
- Pattern B: Unbounded Knapsack
- Pattern C: Longest Common Subsequence (LCS)
- Pattern D: Fibonacci / Linear DP
- Pattern E: Grid / Matrix Path
- 3. Java Implementation
- 0/1 Knapsack (Bottom-Up)
- House Robber (Space-Optimized Linear DP)
- House Robber II (Circular)
- Java Architecture Insights
- 4. Mental Model & Visualization
- Knapsack DP Table (weights=[3,2,4], values=[150,100,200], capacity=4)
- House Robber Table (houses = [2, 7, 9, 3, 1])
- Senior Mental Trigger
- 5. Curated Problem Lists
- Commonly Asked (Bread & Butter)
- FAANG ‘Aha!’ Level (Hard/Unintuitive)
- 6. Time & Space Complexity Table
- 7. How to Debug Your DP Thinking
0. The Template (Copy-Paste Skeleton)
Two skeletons — top-down memo and bottom-up table. Write the recurrence first, in English, before you touch either.
// TEMPLATE A — TOP-DOWN (recursion + memo). Write this FIRST: it's just brute force + a cache.
Integer[] memo; // Integer, so null means "not computed"
int dp(int i, int[] nums) {
if (i < 0) return 0; // 1. BASE CASE
if (memo[i] != null) return memo[i]; // 2. CACHE HIT
int take = nums[i] + dp(i - 2, nums); // 3. TRY EVERY CHOICE
int skip = dp(i - 1, nums);
return memo[i] = Math.max(take, skip); // 4. STORE and return
}
// TEMPLATE B — BOTTOM-UP (iterative table). Convert from A once the recurrence is proven.
int solve(int[] nums) {
int n = nums.length;
int[] dp = new int[n + 1];
dp[0] = 0; dp[1] = nums[0]; // BASE CASES — the usual bug source
for (int i = 2; i <= n; i++) { // ORDER: every dependency already computed
dp[i] = Math.max(dp[i - 1], nums[i - 1] + dp[i - 2]);
}
return dp[n];
}
// TEMPLATE C — SPACE OPTIMISATION: if dp[i] only reads dp[i-1] and dp[i-2], keep 2 variables
int prev2 = 0, prev1 = nums[0];
for (int i = 1; i < nums.length; i++) {
int cur = Math.max(prev1, nums[i] + prev2);
prev2 = prev1; prev1 = cur;
}
return prev1; // O(n) time, O(1) space
// TEMPLATE D — 2-D DP (two sequences / knapsack)
int[][] dp = new int[m + 1][n + 1]; // +1 row/col for the empty-prefix base case
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
dp[i][j] = (a[i-1] == b[j-1])
? dp[i-1][j-1] + 1 // match → extend the diagonal
: Math.max(dp[i-1][j], dp[i][j-1]); // no match → best of dropping one char
return dp[m][n];
The 5-step method — follow it in this order, every time
- State: what does
dp[i]mean? Say it as an English sentence. (“the max money robbable from the firstihouses”). If you can’t say it, you can’t code it. - Recurrence: how does
dp[i]follow from smaller states? This is the whole problem. - Base case: the smallest input, usually
dp[0]. Half of all DP bugs live here. - Order: iterate so every dependency is already filled in.
- Answer: is it
dp[n],dp[n][m], ormax(dp[...])? Not always the last cell.
Pattern → recurrence lookup
| Family | State | Recurrence |
|---|---|---|
| 0/1 Knapsack | dp[i][w] = best using first i items, capacity w |
max(dp[i-1][w], val[i] + dp[i-1][w-wt[i]]) |
| Unbounded knapsack / coin change | dp[amount] |
min(dp[a], dp[a - coin] + 1) — inner loop over coins, not i-1 |
| Fibonacci-style (stairs, house robber) | dp[i] |
f(dp[i-1], dp[i-2]) |
| LCS / edit distance | dp[i][j] over two strings |
match → dp[i-1][j-1] (+1); else best of the neighbours |
| LIS | dp[i] = longest ending at i |
max(dp[j]) + 1 for all j < i with a[j] < a[i] — O(n²), or patience sort for O(n log n) |
| Palindromic substrings | dp[i][j] = is s[i..j] a palindrome |
s[i]==s[j] && dp[i+1][j-1] — iterate i descending |
| Grid paths | dp[r][c] |
dp[r-1][c] + dp[r][c-1] |
Debugging your DP thinking
- Stuck on the recurrence? Write the brute-force recursion first (Template A without the memo), confirm it’s correct on a tiny input, then add the cache. Memoisation never changes the answer, only the speed.
- Wrong answer? Print the table and hand-check the first two rows. It’s almost always a base case or a loop bound.
- Getting
dp[i]fromdp[i+1]? Your iteration direction is backwards — flip the loop.
1. Pattern Identification & Logic
How to Identify (Interview Triggers)
- “Maximize / Minimize” a value under constraints
- “How many ways” to reach a goal
- “Can you reach / achieve” something (yes/no feasibility)
- Problem has optimal substructure (big problem = combining smaller subproblems)
- Problem has overlapping subproblems (same subproblem solved multiple times)
Two Properties Required for DP
- Optimal Substructure: The optimal solution contains optimal solutions to subproblems
- Overlapping Subproblems: Recursive tree recalculates the same states (e.g., Fibonacci calculates f(3) multiple times)
The Workflow (3-Step Mindset)
Step 1: Write the RECURSIVE solution (forget efficiency, just get it correct)
Step 2: MEMOIZE (Top-Down) - add a cache to store results of recursive calls
Step 3: TABULATE (Bottom-Up) - convert recursion into a loop filling a table
The ‘Trick’ to Know
- State definition is everything. Ask yourself: “What variables uniquely define where I am in the problem?” That’s your DP state.
- Transition = the recurrence relation. It’s the
max()ormin()formula connecting current state to previous states. - Base case = the smallest subproblem you can solve without recursion (e.g., 0 items, 0 capacity, empty string).
2. The 5 Core DP Patterns
Pattern A: 0/1 Knapsack
- Sign: “Pick or Don’t Pick” - binary choice for each element
- State:
dp[i][w]= best value using firstiitems with capacityw - Transition:
dp[i][w] = max(dp[i-1][w], val[i] + dp[i-1][w-wt[i]]) - Examples: Partition Equal Subset Sum, Target Sum
Pattern B: Unbounded Knapsack
- Sign: “How many ways to make X” or “Minimum items to reach X” (can reuse items)
- State:
dp[w]= min coins / max ways for capacityw - Transition:
dp[w] = min(dp[w], 1 + dp[w - coin])for each coin - Examples: Coin Change, Rod Cutting
Pattern C: Longest Common Subsequence (LCS)
- Sign: Two strings, looking for max length or edit distance
- State:
dp[i][j]= answer for firstichars of string1 and firstjchars of string2 - Transition: If
s1[i] == s2[j]:dp[i][j] = 1 + dp[i-1][j-1], elsemax(dp[i-1][j], dp[i][j-1]) - Examples: Longest Palindromic Subsequence, Edit Distance
Pattern D: Fibonacci / Linear DP
- Sign: “How many ways to reach step N?” - answer depends on 1-2 previous steps
- State:
dp[i]= answer at positioni - Transition:
dp[i] = dp[i-1] + dp[i-2](or similar) - Examples: Climbing Stairs, House Robber, Decode Ways
Pattern E: Grid / Matrix Path
- Sign: “Minimum path sum” or “Number of paths” in a 2D grid
- State:
dp[i][j]= answer at cell (i, j) - Transition:
dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1]) - Examples: Unique Paths, Minimum Path Sum, Cherry Pickup
3. Java Implementation
0/1 Knapsack (Bottom-Up)
public class Knapsack {
public static int solve(int[] weights, int[] values, int capacity) {
int n = weights.length;
int[][] dp = new int[n + 1][capacity + 1];
for (int i = 1; i <= n; i++) {
for (int w = 1; w <= capacity; w++) {
dp[i][w] = dp[i - 1][w]; // exclude item i
if (weights[i - 1] <= w) {
int include = values[i - 1] + dp[i - 1][w - weights[i - 1]];
dp[i][w] = Math.max(dp[i][w], include);
}
}
}
return dp[n][capacity];
}
}
House Robber (Space-Optimized Linear DP)
class Solution {
public int rob(int[] nums) {
if (nums.length == 1) return nums[0];
int prev2 = 0; // dp[i-2]
int prev1 = 0; // dp[i-1]
for (int num : nums) {
int current = Math.max(prev1, num + prev2);
prev2 = prev1;
prev1 = current;
}
return prev1;
}
}
House Robber II (Circular)
class Solution {
public int rob(int[] nums) {
if (nums.length == 1) return nums[0];
// Can't rob both first and last → solve two sub-problems
return Math.max(
robRange(nums, 0, nums.length - 2),
robRange(nums, 1, nums.length - 1)
);
}
private int robRange(int[] nums, int start, int end) {
int prev2 = 0, prev1 = 0;
for (int i = start; i <= end; i++) {
int current = Math.max(prev1, nums[i] + prev2);
prev2 = prev1;
prev1 = current;
}
return prev1;
}
}
Java Architecture Insights
- Space optimization: For 1D DP where
dp[i]only depends ondp[i-1]anddp[i-2], replace the array with two variables → O(1) space. - For 2D DP: If each row only depends on the previous row, use a 1D array and overwrite in-place (iterate backwards for 0/1 Knapsack to avoid using updated values).
int[][]vsInteger[][]: Primitive arrays are much faster. Only useInteger[][]for memoization where you neednullto distinguish “not computed” from “computed as 0”.
4. Mental Model & Visualization
Knapsack DP Table (weights=[3,2,4], values=[150,100,200], capacity=4)
Capacity → 0 1 2 3 4
Items ↓
0 (none) 0 0 0 0 0
A (3kg, $150) 0 0 0 150 150
B (2kg, $100) 0 0 100 150 250
C (4kg, $200) 0 0 100 150 250
dp[2][4]: max(dp[1][4], 100 + dp[1][2]) = max(150, 100+0) = 150
Wait — with all 3 items: dp[3][4] = max(dp[2][4], 200+dp[2][0]) = max(250, 200) = 250
House Robber Table (houses = [2, 7, 9, 3, 1])
House: 0 1 2 3 4
Money: 2 7 9 3 1
DP: 2 7 11 11 12
dp[4] = max(dp[3], 1 + dp[2]) = max(11, 1+11) = 12
Senior Mental Trigger
“Maximize/minimize with constraints + overlapping subproblems = DP. Define the state, find the transition, start from base case.”
5. Curated Problem Lists
Commonly Asked (Bread & Butter)
| # | Problem | Difficulty | Pattern |
|---|---|---|---|
| LC 70 | Climbing Stairs | Easy | Fibonacci |
| LC 198 | House Robber | Medium | Linear |
| LC 322 | Coin Change | Medium | Unbounded |
| LC 62 | Unique Paths | Medium | Grid |
| LC 300 | Longest Increasing Subsequence | Medium | LIS |
FAANG ‘Aha!’ Level (Hard/Unintuitive)
| # | Problem | Difficulty | Pattern |
|---|---|---|---|
| LC 416 | Partition Equal Subset Sum | Medium | 0/1 Knap |
| LC 1143 | Longest Common Subsequence | Medium | LCS |
| LC 72 | Edit Distance | Medium | LCS |
| LC 312 | Burst Balloons | Hard | Interval |
| LC 139 | Word Break | Medium | Linear |
| LC 10 | Regular Expression Matching | Hard | 2D DP |
6. Time & Space Complexity Table
| Pattern | Time | Space (naive) | Space (optimized) |
|---|---|---|---|
| Fibonacci/Linear | O(n) | O(n) | O(1) |
| 0/1 Knapsack | O(n * W) | O(n * W) | O(W) |
| Unbounded Knap | O(n * W) | O(W) | O(W) |
| LCS / 2D | O(m * n) | O(m * n) | O(min(m, n)) |
| Grid Path | O(m * n) | O(m * n) | O(n) |
7. How to Debug Your DP Thinking
When stuck on a new DP problem, ask:
- What is the “State”? (In Knapsack: item index + remaining weight)
- What is the “Base Case”? (Capacity 0 or no items → value 0)
- What is the “Transition”? (The
max()/min()formula connecting states)