Understand overlapping subproblems and optimal substructure, and apply top-down memoization and bottom-up tabulation.
Published March 15, 2025
Dynamic Programming (DP) solves problems by breaking them into overlapping subproblems, solving each once, and storing the results. It applies when a problem has:
// Naive recursion: O(2^n) — computes fib(3) many times
int fib(int n) {
if (n <= 1) return n;
return fib(n-1) + fib(n-2);
}
// Top-down DP (memoization): O(n)
int[] memo = new int[n+1];
Arrays.fill(memo, -1);
int fib(int n) {
if (n <= 1) return n;
if (memo[n] != -1) return memo[n];
return memo[n] = fib(n-1) + fib(n-2);
}
// Bottom-up DP (tabulation): O(n) time, O(1) space
int fib(int n) {
if (n <= 1) return n;
int prev2 = 0, prev1 = 1;
for (int i = 2; i <= n; i++) {
int curr = prev1 + prev2;
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
| Top-Down (Memo) | Bottom-Up (Table) | |
|---|---|---|
| Style | Recursive + cache | Iterative |
| When useful | Not all subproblems needed | All subproblems needed |
| Space | O(n) stack + cache | O(n) or less |
| Code | More intuitive | More efficient |
For any DP problem, answer these 4 questions:
dp[i], dp[i][j], dp[i][j][k]dp[i] from smaller subproblems?// State: dp[i] = max money robbing houses 0..i
// Transition: dp[i] = max(dp[i-1], dp[i-2] + nums[i])
// Base: dp[0] = nums[0], dp[1] = max(nums[0], nums[1])
public int rob(int[] nums) {
int n = nums.length;
if (n == 1) return nums[0];
int prev2 = nums[0];
int prev1 = Math.max(nums[0], nums[1]);
for (int i = 2; i < n; i++) {
int curr = Math.max(prev1, prev2 + nums[i]);
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
DP is the right choice when:
Common DP categories:
dp[i] depends on previous cellsdp[i][j] for matrix or two-string problemsdp[i][j] means answer for subarray [i..j]dp[i] only depends on dp[i-1] and dp[i-2], you can use two variables instead of an array.