Dynamic Programming
DP solves problems by breaking them into overlapping subproblems, solving each subproblem once, and storing the result to avoid recomputation.
- Replaces exponential brute-force recursion with polynomial time by caching subproblem results (memoization) or filling a table iteratively (tabulation)
- Two styles: top-down memoization adds caching to a recursive solution, bottom-up tabulation fills a dp array from base cases upward
- Reach for it when the problem asks for count of ways, optimal value, or feasibility, and each choice affects what future choices are available
Quick Revision - Must Do Problems
If short on time, solve only these. They cover ~90% of DP patterns.
| # | Problem | Pattern | Why It's Essential | Time to Solve |
|---|---|---|---|---|
| 1 | Climbing Stairs [B75-70] | Linear 1D DP | Fibonacci base — simplest DP, warm-up for every session | 5 min |
| 2 | Jump Game [B75-55] | Greedy / Linear DP | Teaches reachability tracking — common DP/greedy overlap | 10 min |
| 3 | House Robber [B75-198] | Linear 1D DP | Classic include/exclude at each index — foundation of all rob problems | 10 min |
| 4 | House Robber II [B75-213] | Linear 1D DP | Circular constraint — teaches two-pass trick | 15 min |
| 5 | Unique Paths [B75-62] | 2D Grid DP | Simplest 2D DP — every cell = sum of paths from above and left | 10 min |
| 6 | Coin Change [B75-322] | Unbounded Knapsack | Most common unbounded knapsack — infinite supply, minimize count | 15 min |
| 7 | Decode Ways [B75-91] | Linear String DP | Tricky 1-digit and 2-digit decode choices with leading-zero edge cases | 20 min |
| 8 | Word Break [B75-139] | String DP | Reachability DP with substring dict lookup | 20 min |
| 9 | Partition Equal Subset Sum | 0/1 Knapsack | Key knapsack pattern — backward iteration prevents reuse | 20 min |
| 10 | Longest Increasing Subsequence [B75-300] | Sequence DP | Look-back pattern — dp[i] = best ending at i | 20 min |
| 11 | Combination Sum IV [B75-377] | Unbounded Count DP | Count permutations summing to target — outer loop on target | 15 min |
| 12 | Longest Common Subsequence [B75-1143] | 2D String DP | Foundation of edit distance and diff algorithms | 20 min |
Practice Tips
How to Identify a DP Problem
- Problem asks for "number of ways", "minimum cost", "maximum value", or "is it possible"
- Choices at one step affect or constrain future choices (optimal substructure)
- Brute force is exponential but the same subproblems recur (overlapping subproblems)
- Keywords: "count", "minimum", "maximum", "can you reach", "how many distinct"
DP vs. Other Techniques
| If you see... | Use... | Not... |
|---|---|---|
| Count ways / feasibility, choices affect future | DP | Greedy (greedy commits without full exploration) |
| Subset selection with a capacity | DP (knapsack) | Backtracking (too slow for large inputs) |
| Sorted array, optimal subsequence length | DP (sequence) | Two Pointers (requires fixed window structure) |
| Reachability in grid or sequence | DP or BFS | BFS only if each step has equal cost |
Interview Approach
- Define state: what does
dp[i]ordp[i][j]represent in plain English? - Write the recurrence: how does
dp[i]depend on smaller subproblems? - Set base cases: what are
dp[0]anddp[1](ordp[0][j]anddp[i][0])? - Choose fill order: left-to-right (forward) or right-to-left (backward for 0/1 knapsack)?
- Identify the answer: is it
dp[n],dp[n-1], max over alldp[i], or something else?
Formula Cheatsheet
| Pattern | dp[i] means |
Recurrence | Base cases |
|---|---|---|---|
| Linear 1D (count ways) | ways to reach position i | dp[i-1] + dp[i-2] |
dp[0]=1, dp[1]=1 |
| Linear 1D (min cost) | min cost to reach position i | min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2]) |
dp[0]=0, dp[1]=0 |
| Unbounded knapsack | min coins for amount i | min(dp[i-coin] + 1) over all coins |
dp[0]=0 |
| Unbounded count | ways to make amount i | dp[i] + dp[i-coin] over all coins |
dp[0]=1 |
| 0/1 Knapsack | can we reach capacity i | dp[i] || dp[i-num] (backward loop) |
dp[0]=true |
| Sequence (LIS-style) | best result ending at index i | max(dp[j]+1) for all j<i where valid |
dp[i]=1 |
| 2D Grid | ways/value at cell (i,j) | dp[i-1][j] + dp[i][j-1] |
row 0 and col 0 = 1 |
| 2D String (LCS) | LCS length of first i and j chars | dp[i-1][j-1]+1 if match, else max(dp[i-1][j], dp[i][j-1]) |
row 0 and col 0 = 0 |
5 Main Patterns
Pattern 1: Linear 1D DP
// dp[i] depends on 1 or 2 previous states
int[] dp = new int[n + 1];
dp[0] = BASE_0;
dp[1] = BASE_1;
for (int i = 2; i <= n; i++) {
dp[i] = combine(dp[i-1], dp[i-2]);
}
Pattern 2: 2D Grid / String DP
// dp[i][j] depends on dp[i-1][j], dp[i][j-1], or dp[i-1][j-1]
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (match(i, j)) {
dp[i][j] = dp[i-1][j-1] + 1;
}
else {
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
}
}
}
Pattern 3: 0/1 Knapsack (Subset DP)
// Each item used at most once — iterate BACKWARD to prevent reuse
boolean[] dp = new boolean[target + 1];
dp[0] = true;
for (int num : nums) {
for (int i = target; i >= num; i--) { // backward = no reuse
dp[i] = dp[i] || dp[i - num];
}
}
Pattern 4: Unbounded Knapsack
// Items can be reused — iterate FORWARD (inner loop)
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1); // sentinel for "unreachable"
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
if (i >= coin) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
Pattern 5: Sequence DP (LIS-style)
// dp[i] = best result of sequence ending at index i
// Must look back at all j < i
int[] dp = new int[n];
Arrays.fill(dp, 1); // every element is length-1 sequence
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
int ans = 0;
for (int val : dp) {
ans = Math.max(ans, val);
}
General Templates
Space-Optimized 1D DP
// When dp[i] only needs dp[i-1] and dp[i-2]
int prev2 = BASE_0, prev1 = BASE_1;
for (int i = 2; i <= n; i++) {
int curr = combine(prev1, prev2);
prev2 = prev1;
prev1 = curr;
}
return prev1;
0/1 Knapsack (Minimize)
// dp[i] = min cost to achieve capacity i
int[] dp = new int[capacity + 1];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
for (int item : items) {
for (int i = capacity; i >= item; i--) { // backward
if (dp[i - item] != Integer.MAX_VALUE) {
dp[i] = Math.min(dp[i], dp[i - item] + cost);
}
}
}
Problems in this guide
Problem 1: Climbing Stairs ⭐ MUST DO [B75-70]
| Difficulty | Time to Solve | Pattern |
|---|---|---|
| Easy | 5-10 min | Linear 1D DP |
You can climb 1 or 2 steps at a time. Return the number of distinct ways to reach step n.
Example:
Input: n = 2 → 2 (1+1 or 2)
Input: n = 3 → 3 (1+1+1, 1+2, 2+1)
Input: n = 5 → 8
Key Insight:
- To reach stair
i, you either came from stairi-1(took 1 step) or stairi-2(took 2 steps). So the total ways =dp[i-1] + dp[i-2]. This is the Fibonacci sequence. - Formula:
dp[i] = dp[i-1] + dp[i-2], base casesdp[0]=1, dp[1]=1
Solution:
public 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;
}
Complexity:
| Time | O(n) — single pass |
| Space | O(1) — two variables |
Variants — same pattern, different formula:
Allow 3 steps (1, 2, or 3):
// dp[i] = dp[i-1] + dp[i-2] + dp[i-3]
// base: dp[0]=1, dp[1]=1, dp[2]=2
// climbStairs(7) = 44
dp[i] = dp[i-1] + dp[i-2] + dp[i-3];
Minimum cost climbing stairs (LC #746): Each stair has a cost. You can start from index 0 or 1. Reach the top (index n) with minimum cost.
// dp[i] = min cost to reach position i
// dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2])
// base: dp[0]=0, dp[1]=0 (free to start at either)
// cost=[1,100,1,1,100,1] → answer: 4
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
int[] dp = new int[n + 1];
for (int i = 2; i <= n; i++) {
dp[i] = Math.min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2]);
}
return dp[n];
}
Problem 2: Jump Game ⭐ MUST DO [B75-55]
| Difficulty | Time to Solve | Pattern |
|---|---|---|
| Medium | 10-15 min | Greedy / Linear DP |
Given an array where nums[i] is your max jump length from index i, return true if you can reach the last index.
Example:
Input: [2, 3, 1, 1, 4] → true (0→1→4)
Input: [3, 2, 1, 0, 4] → false (always land on index 3 where nums[3]=0)
Key Insight:
- Track the farthest index reachable at each step. If the current index i exceeds maxReach, you are stuck — you cannot reach index i or anything beyond it.
Solution:
public boolean canJump(int[] nums) {
int maxReach = 0;
for (int i = 0; i < nums.length; i++) {
if (i > maxReach) {
return false; // can't reach index i
}
maxReach = Math.max(maxReach, i + nums[i]);
}
return true;
}
Complexity:
| Time | O(n) — single pass |
| Space | O(1) |
Problem 3: House Robber ⭐ MUST DO [B75-198]
| Difficulty | Time to Solve | Pattern |
|---|---|---|
| Medium | 10-15 min | Linear 1D DP |
Rob houses in a row without robbing two adjacent ones. Return the maximum amount you can rob.
Example:
Input: [1, 2, 3, 1] → 4 (rob houses 1 and 3)
Input: [2, 7, 9, 3, 1] → 12 (rob houses 1, 3, 5)
Key Insight:
- At each house, decide: skip it (take the previous max) or rob it (take prev-prev max + current value). The max of those two choices is the best you can do through house i.
Solution:
public int rob(int[] nums) {
int prev2 = 0, prev1 = 0;
for (int num : nums) {
int curr = Math.max(prev1, prev2 + num);
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
Complexity:
| Time | O(n) — single pass |
| Space | O(1) |
Problem 4: House Robber II ⭐ MUST DO [B75-213]
| Difficulty | Time to Solve | Pattern |
|---|---|---|
| Medium | 15-20 min | Linear 1D DP |
Same as House Robber but houses are arranged in a circle — the first and last are adjacent. Return the maximum amount.
Example:
Input: [2, 3, 2] → 3 (can't rob both house 1 and 3)
Input: [1, 2, 3, 1] → 4
Key Insight:
- You cannot rob both the first and last house. Run the linear House Robber twice: once on nums[0..n-2] (exclude last), once on nums[1..n-1] (exclude first). Take the max.
Solution:
public int rob(int[] nums) {
int n = nums.length;
if (n == 1) {
return nums[0];
}
return Math.max(robRange(nums, 0, n - 2), robRange(nums, 1, n - 1));
}
private int robRange(int[] nums, int start, int end) {
int prev2 = 0, prev1 = 0;
for (int i = start; i <= end; i++) {
int curr = Math.max(prev1, prev2 + nums[i]);
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
Complexity:
| Time | O(n) — two linear passes |
| Space | O(1) |
Problem 5: Unique Paths ⭐ MUST DO [B75-62]
| Difficulty | Time to Solve | Pattern |
|---|---|---|
| Medium | 10-15 min | 2D Grid DP |
Count all unique paths from top-left to bottom-right of an m × n grid, moving only right or down.
Example:
Input: m = 3, n = 7 → 28
Input: m = 3, n = 2 → 3
Key Insight:
- dp[i][j] = dp[i-1][j] + dp[i][j-1]: paths to any cell equal the sum of paths from the cell above and the cell to the left. The entire first row and first column are 1 (only one way to reach them).
Solution:
public int uniquePaths(int m, int n) {
int[] dp = new int[n];
Arrays.fill(dp, 1); // top row: 1 path to each cell
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[j] += dp[j - 1]; // above (dp[j]) + left (dp[j-1])
}
}
return dp[n - 1];
}
Complexity:
| Time | O(m × n) |
| Space | O(n) — 1D rolling array |
Problem 6: Coin Change ⭐ MUST DO [B75-322]
| Difficulty | Time to Solve | Pattern |
|---|---|---|
| Medium | 15-20 min | Unbounded Knapsack |
Return the fewest coins needed to make up the amount. Coins can be used any number of times. Return -1 if impossible.
Example:
Input: coins = [1, 5, 11], amount = 15 → 3 (5+5+5)
Input: coins = [2], amount = 3 → -1
Key Insight:
- Unbounded knapsack: coins are reusable. Fill dp left to right. dp[i] = minimum coins to make amount i. Try every coin at each amount — if
dp[i - coin]is reachable, dp[i] might be updated.
Solution:
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1); // sentinel for "unreachable"
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
if (i >= coin) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
Complexity:
| Time | O(amount × coins) |
| Space | O(amount) |
Problem 7: Decode Ways ⭐ MUST DO [B75-91]
| Difficulty | Time to Solve | Pattern |
|---|---|---|
| Medium | 20-25 min | Linear String DP |
A string of digits can be decoded as letters (1→A through 26→Z). Return the number of ways to decode the string.
Example:
Input: "12" → 2 ("AB" or "L")
Input: "226" → 3 ("BZ", "VF", "BBF")
Input: "06" → 0 (leading zero — invalid)
Key Insight:
- dp[i] = number of ways to decode s[0..i-1]. At each position, check two cases: the single digit s[i-1] (valid if not '0'), and the two-digit number s[i-2..i-1] (valid if in range 10–26). Add dp[i-1] or dp[i-2] accordingly.
Solution:
public int numDecodings(String s) {
int n = s.length();
int[] dp = new int[n + 1];
dp[0] = 1; // empty prefix: 1 way
dp[1] = s.charAt(0) == '0' ? 0 : 1;
for (int i = 2; i <= n; i++) {
int oneDigit = Integer.parseInt(s.substring(i - 1, i));
int twoDigit = Integer.parseInt(s.substring(i - 2, i));
if (oneDigit >= 1) {
dp[i] += dp[i - 1]; // valid single digit
}
if (twoDigit >= 10 && twoDigit <= 26) {
dp[i] += dp[i - 2]; // valid two digit
}
}
return dp[n];
}
Complexity:
| Time | O(n) — single pass |
| Space | O(n) — dp array |
Problem 8: Word Break ⭐ MUST DO [B75-139]
| Difficulty | Time to Solve | Pattern |
|---|---|---|
| Medium | 20-25 min | String DP |
Return true if string s can be segmented into a space-separated sequence of dictionary words.
Example:
Input: s = "leetcode", wordDict = ["leet","code"] → true
Input: s = "catsandog", wordDict = ["cats","dog","sand","and","cat"] → false
Key Insight:
- dp[i] = true if s[0..i-1] can be fully segmented. For each end position i, scan all start positions j: if dp[j] is true and s[j..i-1] is in the dictionary, then dp[i] = true.
Solution:
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> dict = new HashSet<>(wordDict);
boolean[] dp = new boolean[s.length() + 1];
dp[0] = true;
for (int i = 1; i <= s.length(); i++) {
for (int j = 0; j < i; j++) {
if (dp[j] && dict.contains(s.substring(j, i))) {
dp[i] = true;
break;
}
}
}
return dp[s.length()];
}
Complexity:
| Time | O(n² × m) where m is average word length (substring creation cost) |
| Space | O(n + d) where d is dictionary size |
Problem 9: Partition Equal Subset Sum ⭐ MUST DO
| Difficulty | Time to Solve | Pattern |
|---|---|---|
| Medium | 20-25 min | 0/1 Knapsack |
Determine if an array can be partitioned into two subsets with equal sum.
Example:
Input: [1, 5, 11, 5] → true ({1,5,5} and {11})
Input: [1, 2, 3, 5] → false
Key Insight:
- Equivalent to: can we find a subset that sums to total/2? If total is odd, immediately false.
- Use 0/1 knapsack on a boolean dp array. Iterate backward so each element is considered at most once.
Solution:
public boolean canPartition(int[] nums) {
int total = 0;
for (int n : nums) {
total += n;
}
if (total % 2 != 0) {
return false;
}
int target = total / 2;
boolean[] dp = new boolean[target + 1];
dp[0] = true;
for (int num : nums) {
for (int i = target; i >= num; i--) { // backward = no reuse
dp[i] = dp[i] || dp[i - num];
}
}
return dp[target];
}
Complexity:
| Time | O(n × target) where target = total/2 |
| Space | O(target) |
Problem 10: Longest Increasing Subsequence ⭐ MUST DO [B75-300]
| Difficulty | Time to Solve | Pattern |
|---|---|---|
| Medium | 20-25 min | Sequence DP |
Return the length of the longest strictly increasing subsequence.
Example:
Input: [10, 9, 2, 5, 3, 7, 101, 18] → 4 ([2, 3, 7, 101])
Input: [0, 1, 0, 3, 2, 3] → 4 ([0, 1, 2, 3])
Key Insight:
- dp[i] = length of the LIS ending exactly at index i. For each i, scan all j < i: if nums[j] < nums[i], dp[i] = max(dp[i], dp[j] + 1).
- The answer is the max over all dp[i] — the LIS can end at any index, not just the last.
Solution:
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
Arrays.fill(dp, 1); // every element is a length-1 subsequence
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
int max = 0;
for (int val : dp) {
max = Math.max(max, val);
}
return max;
}
Complexity:
| Time | O(n²) — for each i, scan all j < i |
| Space | O(n) — dp array |
Problem 11: Combination Sum IV ⭐ MUST DO [B75-377]
| Difficulty | Time to Solve | Pattern |
|---|---|---|
| Medium | 15-20 min | Unbounded Count DP |
Return the number of ordered sequences (order matters) that add up to target. Numbers can be reused.
Example:
Input: nums = [1, 2, 3], target = 4 → 7
(1+1+1+1, 1+1+2, 1+2+1, 1+3, 2+1+1, 2+2, 3+1)
Key Insight:
- Order matters, so this counts permutations, not combinations. The key is to loop over target values in the outer loop and items in the inner loop — this lets you count all orderings.
- dp[i] = number of ordered sequences summing to i.
Solution:
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target + 1];
dp[0] = 1; // one way to make 0: empty sequence
for (int i = 1; i <= target; i++) {
for (int num : nums) {
if (i >= num) {
dp[i] += dp[i - num];
}
}
}
return dp[target];
}
Complexity:
| Time | O(target × nums) |
| Space | O(target) |
Problem 12: Longest Common Subsequence ⭐ MUST DO [B75-1143]
| Difficulty | Time to Solve | Pattern |
|---|---|---|
| Medium | 20-25 min | 2D String DP |
Return the length of the longest common subsequence of two strings.
Example:
Input: text1 = "abcde", text2 = "ace" → 3 ("ace")
Input: text1 = "abc", text2 = "abc" → 3
Input: text1 = "abc", text2 = "def" → 0
Key Insight:
- dp[i][j] = LCS length of text1[0..i-1] and text2[0..j-1]. When characters match, extend the LCS from the diagonal. When they don't, take the best of skipping one character from either string.
Solution:
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length(), n = text2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[i][j] = dp[i-1][j-1] + 1;
}
else {
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
}
}
}
return dp[m][n];
}
Complexity:
| Time | O(m × n) |
| Space | O(m × n) — 2D table |
Quick Reference Table
| # | Problem | Pattern | Key Technique | Time |
|---|---|---|---|---|
| 1 | Climbing Stairs ⭐ [B75-70] | Linear 1D DP | dp[i] = dp[i-1] + dp[i-2] | O(n) |
| 2 | Jump Game ⭐ [B75-55] | Greedy/DP | Track maxReach; return false if i > maxReach | O(n) |
| 3 | House Robber ⭐ [B75-198] | Linear 1D DP | max(skip, rob + prev-prev) | O(n) |
| 4 | House Robber II ⭐ [B75-213] | Linear 1D DP | Two-pass: skip first or skip last | O(n) |
| 5 | Unique Paths ⭐ [B75-62] | 2D Grid DP | dp[i][j] = above + left, 1D rolling array | O(m×n) |
| 6 | Coin Change ⭐ [B75-322] | Unbounded Knapsack | Forward fill, sentinel = amount+1 | O(A×C) |
| 7 | Decode Ways ⭐ [B75-91] | Linear String DP | 1-digit and 2-digit decode checks | O(n) |
| 8 | Word Break ⭐ [B75-139] | String DP | Reachability via substring dict lookup | O(n²) |
| 9 | Partition Equal Subset Sum ⭐ | 0/1 Knapsack | Backward fill, boolean dp | O(n×S) |
| 10 | LIS ⭐ [B75-300] | Sequence DP | dp[i] = max(dp[j]+1) for j<i, nums[j]<nums[i] | O(n²) |
| 11 | Combination Sum IV ⭐ [B75-377] | Unbounded Count | Outer=target, inner=nums (permutations) | O(T×N) |
| 12 | LCS ⭐ [B75-1143] | 2D String DP | Match: +1 from diagonal; no match: max skip | O(m×n) |
When to Use Each Pattern
Linear 1D DP
- Problem involves a sequence and the current state depends on 1–2 previous states
- Classic signals: "can't pick adjacent", "ways to reach step n", "count valid decodings"
- Space-optimize by keeping only prev1 and prev2 when the full array isn't needed
2D Grid / String DP
- Grid path problems where you move right or down (Unique Paths, Min Path Sum)
- Two-string matching problems (LCS, edit distance, longest palindromic subsequence)
- dp dimensions = lengths of the two inputs; extra row/col for the empty-string base case
0/1 Knapsack (Subset DP)
- Selecting items with a budget where each item is used at most once
- Iterate the capacity dimension backward — this prevents reusing the same item
- Boolean variant: "can we achieve exactly X sum?"; integer variant: min/max value at capacity X
Unbounded Knapsack
- Items are reusable (coins, tiles, numbers from an array)
- Iterate the capacity dimension forward — reuse is allowed
- Outer loop on capacity/target, inner loop on items (for min/max problems)
- For count-of-permutations: outer loop on target, inner loop on items
Sequence DP (LIS-style)
- Finding the optimal subsequence in an array — length, count, or value
- dp[i] = best result of a sequence ending at index i, not best up to i
- Answer is max over all dp[i], not dp[n-1], since the optimal subsequence can end anywhere
Common Mistakes to Avoid
General DP Mistakes
- Wrong state definition — dp[i] must have a clear, precise meaning; if you can't state it in plain English, the recurrence will be wrong
- Wrong answer location — the answer is not always dp[n]; for LIS it is the max over all dp[i]; for grid DP it is dp[m-1][n-1]
- Off-by-one in array sizing — for string/array inputs, use
dp[n+1]with dp[0] as the empty-prefix base case
1D DP Mistakes
- Wrong base cases — Climbing Stairs needs dp[1]=1 and dp[2]=2; Decode Ways needs dp[0]=1 (empty string); always verify the first two values manually
- Decode Ways: not handling '0' — a single '0' cannot be decoded; forgetting this gives wrong answers for inputs like "10" or "30"
Knapsack Mistakes
- Forward instead of backward in 0/1 knapsack — iterating forward allows using the same item multiple times; always iterate backward for 0/1 (each item once)
- Forgetting the unreachable sentinel — in Coin Change, initialize dp with
amount + 1(not 0); initializing with 0 makes every amount appear reachable
2D DP Mistakes
- Skipping base row/column initialization — for grid DP, the first row and column must be set before the main fill loop; they have only one path each
Sequence DP Mistakes
- Returning dp[n-1] for LIS — the LIS can end at any index; return
max over all dp[i], not dp[n-1] - Wrong loop order for Combination Sum IV — order matters (permutations); the outer loop must be over target values, not items; items-outer would count combinations instead