Dynamic Programming
1D DP | 2D DP | Knapsack | LCS | Intervals | State Machines
1 ā What Is Dynamic Programming?
Dynamic Programming (DP) solves problems by breaking them into overlapping subproblems and storing the results of each subproblem to avoid redundant computation. It is applicable when a problem has optimal substructure (an optimal solution is built from optimal sub-solutions) and overlapping subproblems (the same subproblem is solved multiple times in naive recursion).
Two Implementation Styles
- TOP-DOWN (Memoisation): Write a recursive solution, add a memo table to cache results. Natural translation from the recurrence relation. Call the function with the original problem size.
- BOTTOM-UP (Tabulation): Fill a DP table iteratively, starting from the smallest subproblems. Usually more space-efficient and avoids recursion stack overhead. Preferred in production code.
- Both styles have the same asymptotic complexity. Top-down is often easier to derive; bottom-up is often easier to space-optimise.
- Space optimisation: many DP problems only need the previous row/value, allowing O(n²) space to reduce to O(n) or O(1).
1.1 ā The Four-Step DP Framework
- STEP 1 ā DEFINE THE STATE: What does
dp[i](ordp[i][j]) represent? Be precise. Write it in plain English first. - STEP 2 ā WRITE THE RECURRENCE: How does
dp[i]relate to smaller subproblemsdp[i-1], dp[i-2], ...? This is the heart of the solution. - STEP 3 ā IDENTIFY BASE CASES: What are the smallest subproblems with known answers? Fill these first to avoid out-of-bounds access.
- STEP 4 ā DETERMINE FILL ORDER: Which order guarantees that when computing
dp[i], all requireddp[j < i]are already filled? (Usually left-to-right for 1D, top-left to bottom-right for 2D.)
1.2 ā DP vs Greedy vs Backtracking
| Dimension | Backtracking | Greedy | Dynamic Programming |
|---|---|---|---|
| Core idea | Explore all paths, prune invalid | One locally optimal choice per step | Store & reuse overlapping subproblem results |
| Correctness requirement | No requirement ā explores all | Greedy choice property + optimal substructure | Optimal substructure alone |
| Output | All solutions (or one) | One optimal value | One optimal value (or count) |
| Complexity | Exponential (pruned) | O(n) or O(n log n) | Polynomial: O(n²), O(n*W), etc. |
| When to use | Constraint satisfaction, enumeration | Interval scheduling, greedy exchange | Sequence, knapsack, string, path problems |
| Space | O(depth) | O(1) or O(n) | O(n) to O(n²) |
2 ā Visual Diagrams: DP in Action
Diagram 1 ā Fibonacci: Naive vs Memoised
Fibonacci: Why Memoisation Eliminates Redundancy
fib(5) ā NAIVE RECURSION (no memoisation):
fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \
fib(2) fib(1)
fib(2) computed 3 times, fib(3) computed 2 times -> exponential O(2^n).
fib(5) ā WITH MEMOISATION:
memo = {}
fib(5) -> fib(4) -> fib(3) -> fib(2) -> fib(1) = 1, fib(0) = 0
fib(2) = 1 -> stored in memo[2]
fib(3) = fib(2)+fib(1) = 2 -> stored in memo[3]
fib(4) = fib(3)+fib(2) = 3 -> stored in memo[4] <- fib(2) read from memo
fib(3) already in memo -> return memo[3] = 2
fib(5) = fib(4)+fib(3) = 5
Each subproblem solved ONCE. Total: O(n) time, O(n) space.
Diagram 2 ā Coin Change DP Table
Coin Change: Full DP Table Trace
coins = [1, 5, 6, 9] amount = 11
dp[i] = minimum coins to make amount i. dp[0] = 0, rest = INF initially.
i=0: dp[0] = 0
i=1: try coin 1: dp[1-1]+1 = dp[0]+1 = 1. dp[1] = 1
i=2: try coin 1: dp[1]+1 = 2. dp[2] = 2
i=3: try coin 1: dp[2]+1 = 3. dp[3] = 3
i=4: try coin 1: dp[3]+1 = 4. dp[4] = 4
i=5: try coin 1: dp[4]+1 = 5.
try coin 5: dp[0]+1 = 1. dp[5] = 1 <- coin 5 wins
i=6: try coin 1: dp[5]+1 = 2.
try coin 5: dp[1]+1 = 2.
try coin 6: dp[0]+1 = 1. dp[6] = 1 <- coin 6 wins
i=7: try coin 1: dp[6]+1 = 2.
try coin 5: dp[2]+1 = 3.
try coin 6: dp[1]+1 = 2. dp[7] = 2
i=8: coin 1: dp[7]+1=3. coin 5: dp[3]+1=4. coin 6: dp[2]+1=3. dp[8]=3
i=9: coin 1: dp[8]+1=4. coin 5: dp[4]+1=5. coin 6: dp[3]+1=4.
coin 9: dp[0]+1=1. dp[9] = 1 <- coin 9 wins
i=10: coin 1: dp[9]+1=2. coin 5: dp[5]+1=2. coin 6: dp[4]+1=5.
coin 9: dp[1]+1=2. dp[10] = 2
i=11: coin 1: dp[10]+1=3. coin 5: dp[6]+1=2. coin 6: dp[5]+1=2.
coin 9: dp[2]+1=3. dp[11] = 2
Answer: dp[11] = 2 (coins 5+6 or coins 2+9). Correct!
Diagram 3 ā LCS DP Table
LCS: DP Table Fill and Traceback
s1 = 'ABCDE' s2 = 'ACE'
dp[i][j] = LCS length of s1[0..i-1] and s2[0..j-1]
'' A C E
'' 0 0 0 0
A 0 1 1 1
B 0 1 1 1
C 0 1 2 2
D 0 1 2 2
E 0 1 2 3 <- answer: dp[5][3] = 3
Recurrence:
if s1[i-1] == s2[j-1]: dp[i][j] = dp[i-1][j-1] + 1 (extend LCS)
else: dp[i][j] = max(dp[i-1][j], dp[i][j-1])
Trace the LCS: start at dp[5][3]=3.
E==E (match): go diagonal to dp[4][2]=2. Track 'E'.
D!=C: max(dp[3][2], dp[4][1]) = max(2,1) -> came from dp[3][2].
C==C (match): go diagonal to dp[2][1]=1. Track 'C'.
B!=A: max(dp[1][1], dp[2][0]) = max(1,0) -> came from dp[1][1].
A==A (match): go diagonal to dp[0][0]=0. Track 'A'.
LCS = 'ACE' (reversed tracking). Length 3. Correct!
Diagram 4 ā 0/1 Knapsack DP Table
0/1 Knapsack: DP Table Fill
items = [(w=2,v=6), (w=3,v=10), (w=4,v=12)] capacity W = 5
dp[i][w] = max value using first i items with capacity w.
w: 0 1 2 3 4 5
i=0 : 0 0 0 0 0 0
i=1(2,6) : 0 0 6 6 6 6
i=2(3,10): 0 0 6 10 10 16 <- 16 = v1+v2 = 6+10
i=3(4,12): 0 0 6 10 12 16
Answer: dp[3][5] = 16. Items selected: (w=2,v=6) + (w=3,v=10).
Recurrence:
if items[i-1].w > w: dp[i][w] = dp[i-1][w] // can't include
else: dp[i][w] = max(dp[i-1][w], // exclude
dp[i-1][w - items[i-1].w] + items[i-1].v) // include
3 ā Real-World Use Cases
| DP Problem Pattern | Real-World Application | System |
|---|---|---|
| Shortest / Longest Path | Route optimisation, cheapest flight | Google Maps, airline booking systems |
| Edit Distance (LCS/LIS) | Diff algorithms, DNA alignment | git diff, bioinformatics BLAST |
| Knapsack / Packing | Budget allocation, resource scheduling | Cloud cost optimisation, ad budget planning |
| Sequence alignment | Spell correction, plagiarism detection | Autocorrect, Turnitin, Grammarly |
| Interval DP | Matrix chain multiplication, expression parsing | Compilers, query optimisers |
| Bitmask DP | Travelling Salesman, assignment problems | Logistics routing, hardware scheduling |
| Stock trading DP | Portfolio rebalancing with constraints | Algorithmic trading systems |
| Text segmentation | Word wrap, code formatter line breaking | LaTeX typesetting, VS Code formatter |
| Probability DP | Dice game probabilities, HMM decoding | Speech recognition, Monte Carlo pricing |
4 ā Core Concepts: DP Patterns
4.1 ā 1D DP: Climbing Stairs / Fibonacci
// Climbing Stairs (LC 70) ā number of ways to reach step n
// dp[i] = ways to reach step i = dp[i-1] + dp[i-2]
// Time: O(n) Space: O(1) after optimisation
int climbStairs(int n) {
if (n <= 2) return n;
int prev2 = 1, prev1 = 2;
for (int i = 3; i <= n; i++) {
int curr = prev1 + prev2;
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
// Space optimised: only keep the last two values (rolling variables).
// General pattern: whenever dp[i] depends only on dp[i-1] and dp[i-2],
// you can reduce O(n) space to O(1).4.2 ā 1D DP: Coin Change
// LeetCode 322 ā Coin Change
// Minimum number of coins to make amount. Unlimited supply per denomination.
// Time: O(amount * n) Space: O(amount)
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount+1, amount+1); // INF = amount+1 (impossible sentinel)
dp[0] = 0; // base case: 0 coins for amount 0
for (int i = 1; i <= amount; i++) {
for (int c : coins) {
if (c <= i)
dp[i] = min(dp[i], dp[i-c] + 1);
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
// Why amount+1 as INF? Any valid answer is <= amount (using all 1-coins).
// If dp[amount] > amount after filling, no solution exists.4.3 ā 1D DP: Longest Increasing Subsequence (LIS)
// LeetCode 300 ā Longest Increasing Subsequence
// Time: O(n^2) Space: O(n) [O(n log n) with patience sorting]
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n, 1); // dp[i] = LIS ending at index i
int best = 1;
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) // nums[i] extends LIS at j
dp[i] = max(dp[i], dp[j]+1);
}
best = max(best, dp[i]);
}
return best;
}
// O(n log n) version using binary search + patience sorting
int lengthOfLIS_nlogn(vector<int>& nums) {
vector<int> tails; // tails[i] = smallest tail of all IS of length i+1
for (int x : nums) {
auto it = lower_bound(tails.begin(), tails.end(), x);
if (it == tails.end()) tails.push_back(x); // extend
else *it = x; // replace
}
return (int)tails.size();
}4.4 ā 2D DP: Longest Common Subsequence
// LeetCode 1143 ā Longest Common Subsequence
// Time: O(m*n) Space: O(m*n) -> O(n) with rolling array
int longestCommonSubsequence(string s1, string s2) {
int m = s1.size(), n = s2.size();
vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (s1[i-1] == s2[j-1])
dp[i][j] = dp[i-1][j-1] + 1; // chars match: extend
else
dp[i][j] = max(dp[i-1][j], dp[i][j-1]); // skip one char
}
}
return dp[m][n];
}
// Space-optimised: O(n) using two rolling rows
int lcs_space(string s1, string s2) {
int m = s1.size(), n = s2.size();
vector<int> prev(n+1,0), curr(n+1,0);
for (int i=1;i<=m;i++) {
for (int j=1;j<=n;j++)
curr[j] = s1[i-1]==s2[j-1] ? prev[j-1]+1 : max(prev[j],curr[j-1]);
swap(prev, curr);
fill(curr.begin(), curr.end(), 0);
}
return prev[n];
}4.5 ā 0/1 Knapsack
// 0/1 Knapsack: each item used at most once
// Time: O(n*W) Space: O(W) with 1D optimisation
int knapsack(vector<int>& w, vector<int>& v, int W) {
int n = w.size();
vector<int> dp(W+1, 0);
for (int i = 0; i < n; i++) {
// CRITICAL: iterate W down to w[i] to prevent using item i twice
for (int cap = W; cap >= w[i]; cap--) {
dp[cap] = max(dp[cap], dp[cap - w[i]] + v[i]);
}
}
return dp[W];
}
// Why iterate capacity in REVERSE for 0/1 knapsack?
// When filling dp[cap], we need dp[cap-w[i]] from the PREVIOUS item iteration.
// Iterating forward would update dp[cap-w[i]] first, allowing item i to be
// used multiple times. Reverse iteration preserves the 'at most once' constraint.
// Unbounded Knapsack (items reusable): iterate capacity FORWARD
int unboundedKnapsack(vector<int>& w, vector<int>& v, int W) {
vector<int> dp(W+1, 0);
for (int cap = 1; cap <= W; cap++)
for (int i = 0; i < (int)w.size(); i++)
if (w[i] <= cap)
dp[cap] = max(dp[cap], dp[cap - w[i]] + v[i]);
return dp[W];
}4.6 ā State Machine DP: Stock Problems
// Best Time to Buy and Sell Stock with Cooldown (LC 309)
// States: held (holding stock), sold (just sold), rest (cooldown done)
// Time: O(n) Space: O(1)
int maxProfit(vector<int>& prices) {
int held = INT_MIN, sold = 0, rest = 0;
for (int p : prices) {
int prevHeld = held, prevSold = sold, prevRest = rest;
held = max(prevHeld, prevRest - p); // hold: keep or buy from rest
sold = prevHeld + p; // sell: was holding, now sell
rest = max(prevRest, prevSold); // rest: keep resting or cooldown
}
return max(sold, rest); // can't end holding stock
}
// State transition diagram:
// rest -> held (buy) held -> sold (sell) sold -> rest (cooldown)
// rest -> rest (wait) held -> held (hold)4.7 ā Interval DP: Burst Balloons
// LeetCode 312 ā Burst Balloons
// dp[l][r] = max coins from bursting all balloons between l and r (exclusive)
// Key: think of k as the LAST balloon to burst in [l,r], not the first.
// Time: O(n^3) Space: O(n^2)
int maxCoins(vector<int>& nums) {
int n = nums.size();
// Pad with sentinel 1s at both ends
vector<int> a(n+2);
a[0] = a[n+1] = 1;
for (int i=0;i<n;i++) a[i+1] = nums[i];
n += 2;
vector<vector<int>> dp(n, vector<int>(n, 0));
// len = length of open interval (l,r)
for (int len = 2; len < n; len++) {
for (int l = 0; l < n-len; l++) {
int r = l + len;
for (int k = l+1; k < r; k++) { // k is last burst in (l,r)
dp[l][r] = max(dp[l][r],
dp[l][k] + a[l]*a[k]*a[r] + dp[k][r]);
}
}
}
return dp[0][n-1];
}5 ā Pattern Recognition Guide
| DP Pattern | Problem Signals | State Definition | Recurrence Shape |
|---|---|---|---|
| Linear 1D | Single sequence, previous results matter | dp[i] = answer for prefix i |
dp[i] = f(dp[i-1], dp[i-2], ...) |
| Two-sequence 2D | Two strings/arrays, alignment/matching | dp[i][j] = answer for s1[0..i], s2[0..j] |
dp[i][j] = f(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) |
| Knapsack | Items with weight+value, capacity constraint | dp[i][w] = max value with i items, cap w |
include/exclude item i |
| Interval | Optimal over all sub-intervals [i,j] | dp[i][j] = answer for subarray [i,j] |
min/max over all splits k |
| State machine | Finite states with transitions (stocks) | dp[state] = best value in this state |
transitions between states per element |
| Counting paths | Count ways (not optimise) | dp[i] = number of ways to reach i |
sum of dp[j] for valid j |
| Digit DP | Count numbers in range with digit constraints | dp[pos][tight][...] = count |
try each digit, maintain tight constraint |
| Bitmask DP | Small n (~20), subsets of items | dp[mask] = answer for subset mask |
dp[mask | (1<<i)] = f(dp[mask]) |
How to Identify a DP Problem
- SIGNAL 1: 'Count the number of ways' or 'Find the minimum/maximum'. These are classic DP outputs.
- SIGNAL 2: 'Optimal decisions over a sequence' ā each decision affects future options.
- SIGNAL 3: Future choices depend on past choices (state carries forward).
- SIGNAL 4: Greedy gives wrong answer (try a small counter-example). Backtracking TLEs. DP is the middle ground.
- SIGNAL 5: The problem has well-defined subproblems that can be expressed as 'solve for i' or 'solve for (i,j)'.
- NOT DP: problems with no ordering, problems that require enumerating all solutions, or problems solvable in O(n) with greedy.
Space Optimisation Decision Table
dp[i]depends ondp[i-1]only → use two variables (O(n) → O(1)).dp[i]depends ondp[i-1]anddp[i-2]→ use three rolling variables (O(n) → O(1)).dp[i][j]depends ondp[i-1][...]only → use two 1D arrays (O(n²) → O(n)).dp[i][j]depends ondp[i-1][j-1]→ use one 1D array with careful update order.- 0/1 knapsack 2D → 1D by iterating capacity in REVERSE (prevents item reuse).
- Unbounded knapsack 2D → 1D by iterating capacity FORWARD (allows item reuse).
6 ā Complete C++ Implementations
6.1 ā Edit Distance (Levenshtein)
// LeetCode 72 ā Edit Distance
// Min operations (insert, delete, replace) to convert word1 to word2.
// Time: O(m*n) Space: O(n) rolling array
int minDistance(string s, string t) {
int m = s.size(), n = t.size();
vector<int> dp(n+1);
iota(dp.begin(), dp.end(), 0); // dp[j] = j (delete j chars from t)
for (int i = 1; i <= m; i++) {
int prev = dp[0]; // dp[i-1][j-1] before overwrite
dp[0] = i; // dp[i][0] = i (delete i chars from s)
for (int j = 1; j <= n; j++) {
int temp = dp[j];
if (s[i-1] == t[j-1])
dp[j] = prev; // chars match: no operation
else
dp[j] = 1 + min({prev, // replace
dp[j], // delete from s
dp[j-1]}); // insert into s
prev = temp;
}
}
return dp[n];
}6.2 ā Unique Paths on Grid
// LeetCode 62 ā Unique Paths
// Count paths from top-left to bottom-right moving only right or down.
// Time: O(m*n) Space: O(n)
int uniquePaths(int m, int n) {
vector<int> dp(n, 1); // first row: all 1s (only rightward path)
for (int i = 1; i < m; i++)
for (int j = 1; j < n; j++)
dp[j] += dp[j-1]; // paths from top + paths from left
return dp[n-1];
}
// Mathematical answer: C(m+n-2, m-1) ā choose m-1 down moves from m+n-2 total.
// LC 63 ā Unique Paths II (with obstacles)
int uniquePathsWithObstacles(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
vector<long long> dp(n, 0);
dp[0] = grid[0][0] == 0 ? 1 : 0;
for (int i = 0; i < m; i++) {
if (grid[i][0] == 1) dp[0] = 0; // obstacle blocks leftmost column
for (int j = 1; j < n; j++)
dp[j] = grid[i][j] == 1 ? 0 : dp[j] + dp[j-1];
}
return (int)dp[n-1];
}6.3 ā Word Break
// LeetCode 139 ā Word Break
// Can string s be segmented into words from wordDict?
// Time: O(n^2 * L) L=avg word length Space: O(n)
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> ws(wordDict.begin(), wordDict.end());
int n = s.size();
vector<bool> dp(n+1, false);
dp[0] = true; // empty string always segmentable
for (int i = 1; i <= n; i++) {
for (int j = 0; j < i; j++) {
// If prefix s[0..j-1] is breakable AND s[j..i-1] is a word
if (dp[j] && ws.count(s.substr(j, i-j))) {
dp[i] = true;
break; // found one valid split, no need to check more
}
}
}
return dp[n];
}7 ā Complexity Reference
| Algorithm | Time | Space |
|---|---|---|
| Fibonacci / Climbing Stairs | O(n) | O(1) rolling |
| Coin Change (min coins) | O(amount * n) | O(amount) |
| Coin Change II (count ways) | O(amount * n) | O(amount) |
| Longest Increasing Subsequence (O(n²)) | O(n²) | O(n) |
| LIS O(n log n) with patience sort | O(n log n) | O(n) |
| Longest Common Subsequence | O(m*n) | O(n) rolling |
| Edit Distance | O(m*n) | O(n) rolling |
| 0/1 Knapsack | O(n*W) | O(W) 1D |
| Unbounded Knapsack / Coin Change | O(amount*n) | O(amount) |
| Unique Paths on Grid | O(m*n) | O(n) |
| Word Break | O(n² * L) | O(n) |
| Stock with Cooldown / k transactions | O(n * k) | O(k) |
| Burst Balloons / Interval DP | O(n³) | O(n²) |
| Bitmask DP (TSP-style) | O(2^n * n²) | O(2^n * n) |
Pseudo-polynomial vs Polynomial
- Coin Change and Knapsack run in O(amount * n) or O(n * W). This looks polynomial but is actually pseudo-polynomial.
- Reason: the input size is O(log amount) bits, not O(amount). A truly polynomial algorithm would be O(n * log(amount)).
- In practice: if amount <= 10ā“ or W <= 10ā“, DP is fast enough. If amount ~ 10ā¹, DP is infeasible.
- Truly polynomial DP: LCS, LIS, Edit Distance are polynomial in input size n (length of the strings/arrays).
8 ā Solved Problem 1: Coin Change
Given an integer array coins and an integer amount, return the fewest number of coins needed to make up amount. If it is impossible, return -1. You may use each coin denomination an unlimited number of times.
OBSERVATIONS
- Greedy fails here. Example: coins=[1,5,6,9], amount=11. Greedy picks 9+1+1=3 coins. Optimal is 5+6=2 coins.
- Optimal substructure:
dp[i] = min coins for amount i = 1 + min(dp[i-c])for each coin c where c <= i. - Overlapping subproblems:
dp[6]is needed when computingdp[7],dp[8],dp[9],dp[11], ... Memoisingdp[6]avoids recomputing it. - Unlimited coin use = unbounded knapsack variant. Iterate amounts from 1 to target, try each coin.
- Initialise
dp[0]=0(base case: 0 coins for amount 0).dp[i] = amount+1as sentinel for 'unreachable'.
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
// dp[i] = minimum coins needed to make amount i
vector<int> dp(amount+1, amount+1); // sentinel: larger than any valid answer
dp[0] = 0; // base case
for (int i = 1; i <= amount; i++) {
for (int c : coins) {
if (c <= i) // coin c can contribute to amount i
dp[i] = min(dp[i], dp[i-c] + 1);
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
};Complexity: Time O(amount * n), Space O(amount)
9 ā Solved Problem 2: Longest Common Subsequence
Given two strings text1 and text2, return the length of their longest common subsequence (LCS). A subsequence is a sequence derived by deleting some characters without changing the relative order of the remaining characters.
OBSERVATIONS
- A subsequence is NOT a substring ā characters don't need to be contiguous.
- State definition:
dp[i][j]= length of LCS oftext1[0..i-1]andtext2[0..j-1]. 1-indexed to simplify base cases. - Recurrence: If
text1[i-1] == text2[j-1]: dp[i][j] = dp[i-1][j-1] + 1(extend LCS by matching char). Else:dp[i][j] = max(dp[i-1][j], dp[i][j-1])(skip one char from either string). - Base cases:
dp[0][j] = 0(empty text1) anddp[i][0] = 0(empty text2). Automatically initialised by vector constructor. - Fill order: row by row, left to right.
dp[i][j]only depends ondp[i-1][j-1],dp[i-1][j],dp[i][j-1]ā all already computed.
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int m = text1.size(), n = text2.size();
// Space-optimised: use two rows only
vector<int> prev(n+1, 0), curr(n+1, 0);
for (int i = 1; i <= m; i++) {
fill(curr.begin(), curr.end(), 0);
for (int j = 1; j <= n; j++) {
if (text1[i-1] == text2[j-1])
curr[j] = prev[j-1] + 1; // chars match
else
curr[j] = max(prev[j], curr[j-1]); // skip one
}
swap(prev, curr);
}
return prev[n];
}
};Complexity: Time O(m*n), Space O(n)
10 ā Common Mistakes & Edge Cases
10.1 ā State Definition Mistakes
- Imprecise state definition.
dp[i]must have an exact meaning. 'dp[i] = something about index i' is not enough ā write 'dp[i] = minimum cost to reach index i from index 0 using exactly i steps'. - Off-by-one in string/array DP. When using 1-indexed dp (dp[1..n] for string of length n), always access text[i-1] for the i-th character. Mixing 0-indexed and 1-indexed causes subtle bugs.
- Wrong base case. For coin change, initialising
dp[0] = 0is critical. For LCS, the entiredp[0][j]anddp[i][0]row/column must be 0. For path counting,dp[0][0] = 1. - Missing the 'impossible' sentinel. For coin change, initialise to amount+1 (not INT_MAX ā adding 1 to INT_MAX overflows). For paths, -1 or 0 are natural sentinels.
10.2 ā Knapsack Direction Mistakes
- 0/1 knapsack ā iterating capacity forward in the 1D optimisation. This allows the same item to be used multiple times (becomes unbounded knapsack). Always iterate capacity in REVERSE for 0/1 knapsack.
- Coin Change II (combinations) ā iterating loops in wrong order. Iterating coins in inner loop and amounts in outer counts permutations (e.g. [1,2] and [2,1] counted separately). Swap the loops: outer = coins, inner = amounts.
- Confusing 'at most W' capacity with 'exactly W'. For knapsack,
dp[W]gives the answer for capacity AT MOST W. For 'exactly W' problems, initialise with -infinity exceptdp[0]=0.
10.3 ā Edge Cases
- Empty string LCS: if either string is empty, LCS = 0. Base cases
dp[0][j]=dp[i][0]=0handle this. - No valid coin combination: return -1, not 0. Check if
dp[amount] > amountafter filling. amount = 0for coin change: always return 0 (zero coins needed).dp[0] = 0handles this.- Identical strings for LCS: LCS = the string itself.
dp[n][n] = n. - All-negative array for max subarray: Kadane returns the single maximum element (not 0). Initialise both currSum and maxSum to nums[0].
11 ā Common Interview Questions
| Problem | Key Implementation Detail |
|---|---|
| Climbing Stairs (LC 70) | Fibonacci pattern, O(1) space |
| House Robber (LC 198) | dp[i] = max(dp[i-1], dp[i-2]+nums[i]) |
| Coin Change (LC 322) | Unbounded knapsack, dp[i] = min(dp[i-c]+1) |
| Coin Change II (LC 518) | count combinations, outer=coins inner=amounts |
| Longest Increasing Subsequence (LC 300) | O(n²) DP or O(n log n) patience sort |
| Word Break (LC 139) | dp[i] = any dp[j] && s[j..i] in dict |
| Longest Common Subsequence (LC 1143) | Classic 2D DP, O(n) space opt |
| Edit Distance (LC 72) | 3-way recurrence: insert/delete/replace |
| Unique Paths (LC 62) | Grid path counting, O(n) space |
| Partition Equal Subset Sum (LC 416) | 0/1 knapsack with target = sum/2 |
| Best Time to Buy Stock with Cooldown (LC 309) | State machine DP |
| Burst Balloons (LC 312) | Interval DP, think last burst not first |
Chapter 10 ā Key Takeaways
- DP requires BOTH optimal substructure (optimal solution built from optimal sub-solutions) AND overlapping subproblems.
- Four steps: define state precisely, write recurrence, identify base cases, determine fill order.
- Top-down vs Bottom-up: Top-down = recursion + memo cache. Bottom-up = iterative table. Same complexity; bottom-up avoids stack overhead.
- 1D DP space opt: rolling variables when
dp[i]depends ondp[i-1]anddp[i-2]only. - 2D DP space opt: two 1D arrays (prev/curr) when
dp[i][j]depends only on row i-1. - 0/1 knapsack 1D: iterate capacity in REVERSE to prevent item reuse.
- Unbounded knapsack / Coin Change: iterate capacity FORWARD to allow item reuse.
- Coin Change: outer=amounts, inner=coins. Coin Change II: outer=coins, inner=amounts.
- LCS recurrence: match → diagonal+1. Mismatch → max(up, left).