Master this topic with zero to advance depth.
Dynamic Programming is an optimization over plain recursion. It is used when a problem can be broken down into subproblems, and these subproblems are solved multiple times. Instead of re-computing, we store the results.
Recursion Tree (Fibonacci):
f(5)
/ \
f(4) f(3)
/ \ / \
f(3) f(2) f(2) f(1) <-- f(2) is redundant!DP Table (Tabulation):
Index: | 0 | 1 | 2 | 3 | 4 | 5 |
Value: | 0 | 1 | 1 | 2 | 3 | 5 | (dp[i] = dp[i-1] + dp[i-2])If dp[i] only depends on dp[i-1] and dp[i-2], we don't need a whole array. We can just use two variables! This reduces Space Complexity from O(N) to O(1).
Longest Increasing Subsequence
Given an integer array nums, return the length of the longest strictly increasing subsequence.
A subsequence is a sequence that can be derived from an array by deleting some or no elements without changing the order of the remaining elements.
nums = [10, 9, 2, 5, 3, 7, 101, 18]
LIS: [2, 3, 7, 18] or [2, 3, 7, 101]
Length: 4
DP Strategy:
dp[i] = length of LIS ending at index i
dp[i] = 1 + max(dp[j]) for all j < i AND nums[j] < nums[i]Examples
The longest increasing subsequence is [2, 3, 7, 101], therefore the length is 4.
Intuition
For each element nums[i], we look back at all previous elements nums[j]. If nums[i] > nums[j], then nums[i] can extend the increasing subsequence ending at j.
dp array where dp[i] = 1 (minimum length is always 1).i from 1 to n-1:
j from 0 to i-1:
nums[i] > nums[j]: dp[i] = max(dp[i], 1 + dp[j]).dp.Detailed Dry Run
nums = [10, 9, 2, 5]
| i | nums[i] | j loop | dp table | Max reached |
|---|---|---|---|---|
| 0 | 10 | - | [1, 1, 1, 1] | 1 |
| 1 | 9 | j=0 (9<10) | [1, 1, 1, 1] | 1 |
| 2 | 2 | j=0, 1 (2<9,10) | [1, 1, 1, 1] | 1 |
| 3 | 5 | j=2 (5>2) | [1, 1, 1, 2] | 2 |
Intuition
The brute force recursive approach recalculates the same subproblems repeatedly. We can cache results using a memo array so that each dp[i] is computed only once.
memo array with -1.solve(i), if memo[i] != -1, return it.dp[i] = 1 + max(solve(j)) for all j < i where nums[j] < nums[i].memo[i] = dp[i].Recursion Tree (with memoization):
solve(7): max over j < 7 where nums[j] < 18
- solve(6): 2 (cached at step 1)
- solve(5): 3 (cached at step 2)
- ... (all others are cached)Detailed Dry Run
nums = [10, 9, 2, 5]
| call | i | j checks | result |
|---|---|---|---|
| 1 | 0 | - | 1 |
| 2 | 1 | j=0 (9<10)=No | 1 |
| 3 | 2 | j=0,j=1 (2<9,10)=No | 1 |
| 4 | 3 | j=2 (5>2) | 2 (cached!) |
⚠️ Common Pitfalls & Tips
When computing max across all solve(i) calls, don't forget that lru_cache in Python caches the function but the outer max call still needs to iterate all indices.
Intuition
This is often called Patience Sorting. We maintain a list tails where tails[i] is the smallest tail of all increasing subsequences of length i+1.
x in nums:
x is larger than the largest tail, append it.x.tails is the LIS length.Detailed Dry Run
nums = [10, 9, 2, 5]
| x | tails | Action | Reason |
|---|---|---|---|
| 10 | [10] | Append | List empty |
| 9 | [9] | Replace 10 | 9 is smaller tail for len 1 |
| 2 | [2] | Replace 9 | 2 is smaller tail for len 1 |
| 5 | [2, 5] | Append | 5 > 2, starts len 2 |
⚠️ Common Pitfalls & Tips
The Binary Search approach only gives the length of the LIS, correctly. The tails array itself may NOT represent a valid LIS (e.g., it might be mixed from different subsequences), but its size will always be correct.
Unique Paths
There is a robot on an m x n grid. The robot is initially located at the top-left corner (grid[0][0]). The robot tries to move to the bottom-right corner (grid[m-1][n-1]). The robot can only move either down or right at any point in time.
Given the two integers m and n, return the number of possible unique paths that the robot can take to reach the bottom-right corner.
m=3, n=2
Grid:
(S) (1) (S=Start, E=End)
(1) (1)
(1) (E)
Paths:
1. Right -> Down -> Down
2. Down -> Down -> Right
3. Down -> Right -> Down
Total: 3Examples
From the top-left corner, there are a total of 3 ways to reach the bottom-right corner:
Intuition
The number of ways to reach cell (i, j) is the sum of ways to reach the cell above it (i-1, j) and the cell to its left (i, j-1).
dp[i][j] = dp[i-1][j] + dp[i][j-1].dp[0][j] = 1 and dp[i][0] = 1 because there is only one way to move along the edges (straight right or straight down).Detailed Dry Run
m=3, n=3
| Row | Col 0 | Col 1 | Col 2 | Action |
|---|---|---|---|---|
| 0 | 1 | 1 | 1 | Init Row 0 |
| 1 | 1 | 2 | 3 | 1+1=2, 2+1=3 |
| 2 | 1 | 3 | 6 | 1+2=3, 3+3=6 |
Intuition
The recursive brute force for grid path counting has overlapping subproblems. We cache each (i, j) state so we never recompute it.
Memo Grid (m=3, n=3):
| 0 | 1 | 1 |
| 1 | 2 | ? | <- memo[1][2] reused by (2,2)
| 1 | ? | ? |Detailed Dry Run
m=3, n=3. memo[2][2] = memo[1][2] + memo[2][1]. Both sub-calls are cached after first computation.
Intuition
To compute dp[i][j], we only need the value above it dp[i-1][j] (from the previous row) and the value to its left dp[i][j-1] (already computed in the current row). We can use a single row array.
row array of size n with 1s.m-1 times (for all rows except the first).n-1:
row[j] = row[j] + row[j-1].row[n-1].Detailed Dry Run
m=3, n=3
| Action | row array |
|---|---|
| Init | [1, 1, 1] |
| Row 1 | [1, (1+1)=2, (2+1)=3] |
| Row 2 | [1, (1+2)=3, (3+3)=6] |
⚠️ Common Pitfalls & Tips
Ensure the loop for columns starts at 1, as the first column always has 1 path (staying at the left edge).
Coin Change
You are given an integer array coins representing coins of different denominations and an integer amount representing a total amount of money.
Return the fewest number of coins that you need to make up that amount. If that amount of money cannot be made up by any combination of the coins, return -1.
You may assume that you have an infinite number of each kind of coin.
coins = [1, 2, 5], amount = 11
Optimal: 5 + 5 + 1 = 11 (3 coins)
DP Strategy:
To make amount 11, we can take:
- Coin 1: 1 + minCoins(10)
- Coin 2: 1 + minCoins(9)
- Coin 5: 1 + minCoins(6)
We take the minimum of these choices.Examples
11 = 5 + 5 + 1
Intuition
For each coin, we try taking it and then recursively solve for the remaining amount. We try all coins for each state and keep the minimum result.
solve(amt) = 1 + min(solve(amt - coin)) for all coin in coins.amt == 0, return 0. If amt < 0, return infinity.This is Recursive Exhaustive Search. It explores many redundant paths (e.g., to reach amount 5, you might do 2+2+1, 2+1+2, 1+2+2, etc.).
Detailed Dry Run
coins=[1, 2], amt=3
| Call | amt | Choices | Returns |
|---|---|---|---|
| 1 | 3 | 1+f(2), 1+f(1) | min(2, 2) = 2 |
| 2 | 2 | 1+f(1), 1+f(0) | min(2, 1) = 1 |
| 3 | 1 | 1+f(0), 1+f(-1) | min(1, inf) = 1 |
| 4 | 0 | - | 0 (Base Case) |
Intuition
The brute-force recursion recalculates the same amount values repeatedly. We store results in a memo map/array to skip recomputation.
coins=[1,2], amount=4
tree (without memo): | with memo:
f(4) | f(4) calls f(3), f(2)
/ \ | f(3) already done in f(4)!
f(3) f(2) | Skipped!
/ \ / \ |
f(2) f(1) f(1) f(0) |Detailed Dry Run
coins=[1,2], amount=3. memo[2] = min(1+memo[1], 1+memo[0]) = 1. memo[3] = min(1+memo[2], 1+memo[1]) = 2.
⚠️ Common Pitfalls & Tips
Use a sentinel value (not -1) to differentiate 'not computed yet' from 'impossible'. Using -2 or INT_MAX as the uncomputed signal works well.
Intuition
We build the solution for all amounts from 1 to targetAmount. For each amount i, we check which coin was the last one added. The minimum coins for i will be 1 + dp[i - coin].
dp array of size amount + 1 with a large value (e.g., amount + 1).dp[0] = 0 (base case).i from 1 to amount.i, loop through each coin.i - coin >= 0, update dp[i] = min(dp[i], 1 + dp[i - coin]).dp[amount] or -1 if unreachable.Detailed Dry Run
coins = [1, 2, 5], amount = 5
| i | Coins Loop | dp[i] calculation | Result |
|---|---|---|---|
| 1 | 1 | 1 + dp[0] = 1 | 1 |
| 2 | 1, 2 | min(1+dp[1], 1+dp[0]) | 1 |
| 3 | 1, 2 | min(1+dp[2], 1+dp[1]) | 2 |
| 4 | 1, 2 | min(1+dp[3], 1+dp[2]) | 2 |
| 5 | 1, 2, 5 | min(1+dp[4], 1+dp[3], 1+dp[0]) | 1 |
⚠️ Common Pitfalls & Tips
Be very careful with the initialization of the dp array. Using Integer.MAX_VALUE in Java or 1e9 can cause integer overflow when you do 1 + dp[i-coin]. Using amount + 1 is a safer 'infinity' for this problem.
House Robber
You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, the only constraint stopping you from robbing each of them is that adjacent houses have security systems connected and it will automatically contact the police if two adjacent houses were broken into on the same night.
Given an integer array nums representing the amount of money of each house, return the maximum amount of money you can rob tonight without alerting the police.
nums = [1, 2, 3, 1]
Choice 1: Rob House 0 (1) + House 2 (3) = 4
Choice 2: Rob House 1 (2) + House 3 (1) = 3
Result: 4
DP Decision:
At each house, you have two choices:
1. Rob this house: nums[i] + maxLoot from houses (i-2)
2. Skip this house: maxLoot from houses (i-1)Examples
Rob house 1 (money = 1) and then rob house 3 (money = 3). Total amount = 1 + 3 = 4.
Rob house 1 (money = 2), rob house 3 (money = 9) and rob house 5 (money = 1). Total amount = 2 + 9 + 1 = 12.
Intuition
For each house, we decide whether to rob it or skip it. If we rob it, we cannot rob the next house. If we skip it, we can rob the next house. We explore both paths and take the maximum.
rob(i) = max(nums[i] + rob(i-2), rob(i-1)).This is a Decision Based Recursion. It has many overlapping subproblems (e.g., rob(3) calls rob(1) and rob(2), and rob(2) also calls rob(1)).
Detailed Dry Run
nums = [1, 2, 3]
| Call | i | Returns | Action |
|---|---|---|---|
| 1 | 2 | max(3+f(0), f(1)) | Recurse |
| 2 | 0 | 1 | Base case |
| 3 | 1 | max(2+f(-1), f(0)) | Recurse |
| 4 | -1 | 0 | Base case |
Intuition
We cache the result of rob(i) so that each house is only evaluated once, eliminating the exponential redundancy.
nums = [1, 2, 3, 1]
Call Tree (Memoized):
rob(3) -> max(1 + rob(1), rob(2))
rob(2) -> max(3 + rob(0), rob(1)) | memo[2] = 4
rob(1) -> max(2 + rob(-1), rob(0)) | memo[1] = 2
rob(0) -> 1 (Base Case)Detailed Dry Run
nums = [2, 7, 9]. rob(2) = max(9+rob(0), rob(1)) = max(9+2, 7) = 11. Cached!
⚠️ Common Pitfalls & Tips
Unlike the brute force, memoization ensures O(N) time. But due to the call stack, very long arrays could cause stack overflow — prefer the iterative DP for production.
Intuition
We only need the results of the two previous houses to decide for the current one. Instead of an array, we use rob1 (max loot excluding adjacent) and rob2 (max loot including adjacent).
rob1 = 0 and rob2 = 0.n in nums.temp = max(n + rob1, rob2).rob1 = rob2.rob2 = temp.rob2.Detailed Dry Run
nums = [1, 2, 3, 1]
| i | n | rob1 | rob2 | Action |
|---|---|---|---|---|
| - | - | 0 | 0 | Init |
| 0 | 1 | 0 | 1 | max(1+0, 0) |
| 1 | 2 | 1 | 2 | max(2+0, 1) |
| 2 | 3 | 2 | 4 | max(3+1, 2) |
| 3 | 1 | 4 | 4 | max(1+2, 4) |
⚠️ Common Pitfalls & Tips
Ensure rob1 and rob2 are updated correctly in each iteration. rob1 should always be the max loot excluding the house immediately before the current one.
House Robber II
You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed. All houses at this place are arranged in a circle. That means the first house is the neighbor of the last one. Meanwhile, adjacent houses have a security system connected, and it will automatically contact the police if two adjacent houses were broken into on the same night.
Given an integer array nums representing the amount of money of each house, return the maximum amount of money you can rob tonight without alerting the police.
nums = [2, 3, 2]
Circle Layout:
(2) -- (3)
| |
( ) -- (2)
Options:
1. Rob House 0: Cannot rob House 1 or House 2. Result: 2
2. Rob House 1: Cannot rob House 0 or House 2. Result: 3
Result: 3Examples
You cannot rob house 1 (money = 2) and then rob house 3 (money = 2), because they are adjacent houses.
Intuition
To handle the circular constraint, we can either rob the first house (forcing us to skip the last) or skip the first house (allowing us to potentially rob the last). We run two separate recursions and compare results.
rob(nums, start, end) is a recursive function for a linear range.nums[0...n-1]:
0 to n-2.1 to n-1.max(Option 1, Option 2).Circular to Linear Reduction using Recursion.
Detailed Dry Run
nums = [2, 3, 2]
Intuition
We reduce the circular problem into two linear House Robber I problems (skip first house or skip last house) and apply memoization on each.
nums = [2, 3, 2]
Case 1: Skip last -> rob [2, 3] -> memoized linear rob
Case 2: Skip first -> rob [3, 2] -> memoized linear rob
Result: max(3, 3) = 3Detailed Dry Run
Call memoized robLinear on [2, 3] → 3. Call memoized robLinear on [3, 2] → 3. max = 3.
⚠️ Common Pitfalls & Tips
Since lru_cache in Python captures closure variables, use tuples as arguments for the circular-to-linear reduction to avoid cache collisions between the two sub-problems.
Intuition
The circular constraint means we can either rob the first house OR the last house, but never both. This simplifies the circular problem into two linear House Robber problems:
0 to n-2 (exclude last).1 to n-1 (exclude first).n=1, return nums[0].maxLoot1 = houseRobberLinear(nums[0...n-2]).maxLoot2 = houseRobberLinear(nums[1...n-1]).max(maxLoot1, maxLoot2).This is Reducing Circular to Linear. We reuse the standard House Robber logic twice.
Detailed Dry Run
nums = [2, 3, 2]
| Step | Subset | Max Loot (Linear) |
|---|---|---|
| 1 | [2, 3] | max(2, 3) = 3 |
| 2 | [3, 2] | max(3, 2) = 3 |
| Final | max(3, 3) | 3 |
⚠️ Common Pitfalls & Tips
Don't forget the base case for n=1. If you don't handle it, slicing or indexing might fail. Also, ensure both linear calls exclude at least one house to break the circular dependency.
Jump Game
You are given an integer array nums. You are initially positioned at the array's first index, and each element in the array represents your maximum jump length at that position.
Return true if you can reach the last index, or false otherwise.
nums = [2, 3, 1, 1, 4]
Index: 0 1 2 3 4
Value: 2 3 1 1 4
- From 0 (2): Can jump to 1 or 2.
- From 1 (3): Can jump to 2, 3, or 4 (Goal!).
Result: TrueExamples
Jump 1 step from index 0 to 1, then 3 steps to the last index.
You will always arrive at index 3 no matter what. Its maximum jump length is 0, which makes it impossible to reach the last index.
Intuition
We can store whether each index is 'reachable' in a DP table. An index i is reachable if there exists some j < i such that j is reachable AND j + nums[j] >= i.
canReach[i] = true if index i is reachable.canReach[0] = true.i, check all j < i to see if a jump can land on i.This is Linear DP. While effective, it leads to O(N^2) time.
Detailed Dry Run
nums = [2, 3, 1, 0, 4]
| i | nums[i] | j loop | canReach table | Status |
|---|---|---|---|---|
| 0 | 2 | - | [T, F, F, F, F] | Start |
| 1 | 3 | j=0 (0+2 >= 1) | [T, T, F, F, F] | OK |
| 2 | 1 | j=0 (0+2 >= 2) | [T, T, T, F, F] | OK |
| 4 | 4 | j=1 (1+3 >= 4) | [T, T, T, T, T] | REACHED |
Intuition
The standard DP approach checks all j < i. We can speed it up by also keeping a track of the farthest reachable index instead of building the full table — but for memoization we cache 'can we reach this index from the start?'.
nums = [2, 3, 1, 1, 4]
memo[0] = True, memo[1] = ?, memo[2] = ?
- Check: can we reach index 1? From 0, jump 1 or 2. Yes!
- Can we reach index 4? From 1, jump 3. Yes! memo[4] = True.Detailed Dry Run
nums = [2, 3, 1, 1, 4]. memo[1] = True (from 0). memo[4] = True (from 1). Early exit!
⚠️ Common Pitfalls & Tips
The memoized top-down approach is still O(N^2) in the worst case. The Greedy approach is O(N) and should be preferred for large inputs.
Intuition
We don't need to know all ways to reach an index, just the farthest index we can reach. If we can reach index i, we can also reach everything before it.
goal = nums.length - 1.i + nums[i] >= goal, then index i can reach the goal. Update the goal = i.goal == 0, return true.Detailed Dry Run
nums = [2, 3, 1, 1, 4]
| i | nums[i] | i + nums[i] | Goal | Update? |
|---|---|---|---|---|
| 4 | 4 | 8 | 4 | Init |
| 3 | 1 | 4 | 4 | 3+1 >= 4 (YES), Goal=3 |
| 2 | 1 | 3 | 3 | 2+1 >= 3 (YES), Goal=2 |
| 1 | 3 | 4 | 2 | 1+3 >= 2 (YES), Goal=1 |
| 0 | 2 | 2 | 1 | 0+2 >= 1 (YES), Goal=0 |
⚠️ Common Pitfalls & Tips
While this problem has a Greedy solution, many similar problems (like Jump Game II) are better solved with DP or BFS. Always check if a greedy choice leads to the global optimum.
Longest Common Subsequence
Given two strings text1 and text2, return the length of their longest common subsequence. If there is no common subsequence, return 0.
A subsequence of a string is a new string generated from the original string with some characters (can be none) deleted without changing the relative order of the remaining characters.
text1 = "abcde", text2 = "ace"
LCS is "ace", Length = 3
DP Grid Logic:
- If text1[i] == text2[j]:
1 + dp[i+1][j+1] (Match!)
- Else:
max(dp[i+1][j], dp[i][j+1]) (Skip one char)Examples
The longest common subsequence is "ace" and its length is 3.
Intuition
We compare characters of both strings starting from the beginning. If they match, we increment the count and move both pointers. If they don't match, we have two choices: skip a character from text1 or skip a character from text2. We take the maximum of these two paths.
solve(i, j): LCS of text1[i:] and text2[j:].text1[i] == text2[j]: 1 + solve(i+1, j+1).max(solve(i+1, j), solve(i, j+1)).This is Recursive Decision Tree. The time complexity is exponential because it explores the same (i, j) pairs multiple times.
Detailed Dry Run
text1="abc", text2="ace"
| i | j | Match? | Action | Returns |
|---|---|---|---|---|
| 0 | 0 | 'a'=='a' (Yes) | 1 + f(1, 1) | 1 + 2 = 3 |
| 1 | 1 | 'b'=='c' (No) | max(f(2, 1), f(1, 2)) | max(1, 2) = 2 |
| 1 | 2 | 'b'=='e' (No) | max(f(2, 2), f(1, 3)) | max(1, 0) = 1 |
Intuition
The recursive LCS solution re-evaluates the same (i, j) pairs many times. We add a 2D memo table indexed by string positions.
text1="abc", text2="ace"
Memo table after caching:
"" 'a' 'c' 'e'
"" 0 0 0 0
'a' 0 [3] - - <- solve(0,0)=3 cached!
'b' 0 - - -
'c' 0 - [1] -Detailed Dry Run
solve(0, 0): 'a'=='a', return 1+solve(1,1). solve(1,1): 'b'!='c', return max(solve(2,1), solve(1,2)).
⚠️ Common Pitfalls & Tips
Python's lru_cache automatically creates the 2D memo table. In Java, a 2D array initialized with -1 suffices for string indices.
Intuition
We use a 2D grid where dp[i][j] represents the LCS length for text1[i:] and text2[j:]. We fill the grid from the bottom-right towards the top-left (or top-left to bottom-right).
dp of size (m+1) x (n+1) with 0s.i (text1) and j (text2) in reverse.dp[i][j] = 1 + dp[i+1][j+1].dp[i][j] = max(dp[i+1][j], dp[i][j+1]).dp[0][0].Detailed Dry Run
text1="abc", text2="ace"
| Row (i) | Col (j) | Match? | dp[i][j] calculation | Value |
|---|---|---|---|---|
| 2 (c) | 2 (e) | No | max(dp[3][2], dp[2][3]) | 0 |
| 2 (c) | 1 (c) | Yes | 1 + dp[3][2] | 1 |
| 1 (b) | 1 (c) | No | max(dp[2][1], dp[1][2]) | 1 |
| 0 (a) | 0 (a) | Yes | 1 + dp[1][1] | 3 |
⚠️ Common Pitfalls & Tips
Remember to initialize the dp table with size (n+1) x (m+1) to handle the base cases (empty strings) easily without extra boundary checks.
Word Break
Given a string s and a dictionary of strings wordDict, return true if s can be segmented into a space-separated sequence of one or more dictionary words.
Note that the same word in the dictionary may be reused multiple times in the segmentation.
s = "leetcode", wordDict = ["leet", "code"]
s[0:4] = "leet" (Found!)
s[4:8] = "code" (Found!)
Result: True
DP Strategy:
dp[i] is true if s[0...i] can be segmented.
dp[i] = true if EXISTS j < i such that dp[j] is true AND s[j...i] is in wordDict.Examples
Return true because "leetcode" can be segmented as "leet code".
Return true because "applepenapple" can be segmented as "apple pen apple".
Intuition
Try every possible prefix of the string. If a prefix is in the dictionary, recursively check if the remaining suffix can also be segmented into dictionary words.
s, iterate i from 1 to len(s).s[0:i] is in wordDict:
canBreak(s[i:]).true.Detailed Dry Run
s = "abc", wordDict = ["a", "bc"]
Intuition
The brute-force recursion checks all prefixes and re-explores the same suffixes repeatedly. By caching results for each starting index, we eliminate redundant work.
s = "leetcode", dict = ["leet", "code"]
memo[0]=T -> "leet" matches -> memo[4]=?
memo[4]=T -> "code" matches -> memo[8]=TDetailed Dry Run
s="catsdog", dict=["cats","dog"]. memo[0]: "cats" -> memo[4]. memo[4]: "dog" -> memo[7]=T. Cached!
⚠️ Common Pitfalls & Tips
Use a Set for dictionary lookups (O(1)) instead of a list (O(L)). Without this, the overall complexity worsens significantly.
Intuition
We check every possible prefix of the string. If a prefix s[0...j] can be segmented (indicated by dp[j] = true), we then check if the remaining substring s[j...i] exists in the dictionary.
dp array of size n+1 with false.dp[0] = true (empty string is always valid).i from 1 to n:
j from 0 to i-1:
dp[j] is true AND s.substring(j, i) is in wordDict:
dp[i] = true and break.dp[n].Detailed Dry Run
s = "leetcode", dict = ["leet", "code"]
| i | j | Substring | dp[j] | Solution |
|---|---|---|---|---|
| 0 | - | - | T | Base Case |
| 4 | 0 | "leet" | T | dp[4] = True |
| 8 | 4 | "code" | T | dp[8] = True |
⚠️ Common Pitfalls & Tips
Be careful with substring indexing (inclusive start, exclusive end). Using a Set for dictionary lookup improves lookup time from O(W) to O(1) average.
Partition Equal Subset Sum
Given an integer array nums, return true if you can partition the array into two subsets such that the sum of the elements in both subsets is equal or false otherwise.
nums = [1, 5, 11, 5]
Total Sum = 22, Target = 11
Subsets:
[1, 5, 5] -> Sum 11
[11] -> Sum 11
Result: True
Logic:
Can we find a subset with sum equal to (TotalSum / 2)?Examples
The array can be partitioned as [1, 5, 5] and [11].
Intuition
This is a variation of the 0/1 Knapsack Problem. We want to find if there's a subset with sum = totalSum / 2.
totalSum is odd, return false (cannot partition equally).totalSum / 2.dp[i][j] = true if sum j can be formed using first i elements.dp[i][j] = dp[i-1][j] (exclude current) OR dp[i-1][j - nums[i]] (include current, if j >= nums[i]).Detailed Dry Run
nums = [1, 5, 5], Target = 5.5 (Skip, 11 is total) nums = [1, 2, 3], Target = 3
| i | x | Sum 0 | Sum 1 | Sum 2 | Sum 3 | Action |
|---|---|---|---|---|---|---|
| 0 | 1 | T | T | F | F | Init |
| 1 | 2 | T | T | T | T | dp[0][3] |
| Result | T | True! |
Intuition
The 2D DP bottom-up approach explores the entire grid. A top-down memoized approach explores only the reachable states. Both have the same complexity but memoization is more intuitive to derive.
nums=[1,5,11,5], target=11
solve(4, 11): can_pick(11)? No, but 11<11 is false for nums[3]=5
solve(3, 6): can_pick(11)? ...Detailed Dry Run
nums=[1,5,5,11], target=11. solve(3, 11) = take 11 = solve(2, 0) = True! Cached.
⚠️ Common Pitfalls & Tips
String keys for memoization in C++/Go/Java are slow. A 2D boolean array with i and t as indices is faster and preferred in practice.
Intuition
Since dp[i][j] only depends on the previous row dp[i-1], we can use a single row array. To avoid using the same element twice for the same sum (0/1 constraint), we iterate the sum backwards.
dp array of size target + 1 with false.dp[0] = true.num in nums:
j from target down to num:
dp[j] = dp[j] || dp[j - num].Detailed Dry Run
nums = [1, 2, 3], Target = 3
| num | Target 3 | Target 2 | Target 1 | Target 0 |
|---|---|---|---|---|
| - | F | F | F | T |
| 1 | F | F | T (0+1) | T |
| 2 | T (1+2) | T (0+2) | T | T |
| 3 | T (0+3) | T | T | T |
⚠️ Common Pitfalls & Tips
You must iterate the inner loop backwards (from target to num). If you iterate forwards, you might use the same element multiple times for the same sum (which is unbounded knapsack behavior, not 0/1).
Climbing Stairs
You are climbing a staircase. It takes n steps to reach the top. Each time you can either climb 1 or 2 steps. In how many distinct ways can you climb to the top?
n = 3
Ways:
1. 1 step + 1 step + 1 step
2. 1 step + 2 steps
3. 2 steps + 1 step
Total: 3 ways
Recursion Tree:
(3)
/ \
(2) (1)
/ \ |
(1) (0) (0)Examples
Intuition
To reach step n, you could have come from either step n-1 (by taking 1 step) or step n-2 (by taking 2 steps). Thus, the total ways . This is exactly the Fibonacci sequence.
This is Plain Recursion. The bottleneck is that it re-calculates the same steps many times, leading to an exponential time complexity.
Detailed Dry Run
| Call | n | Returns | Action |
|---|---|---|---|
| 1 | 3 | f(2) + f(1) | Recurse |
| 2 | 2 | f(1) + f(0) | Recurse |
| 3 | 1 | 1 | Base Case |
| 4 | 0 | 1 | Base Case |
Intuition
Cache the result of climb(n) using a memo table so each stair count is computed only once.
climb(5)
climb(4) --cached after first call!
climb(3) --cached after first call!
...
Only N unique calls made.Detailed Dry Run
climb(4) = climb(3) + climb(2). climb(3) = climb(2) + climb(1). Both branches cache climb(2)=2.
⚠️ Common Pitfalls & Tips
Python's lru_cache handles the caching automatically. In other languages, a HashMap or array of size N+1 is needed.
Intuition
Since we only need the last two results ( and ) to calculate the current one, we can use two variables instead of a full DP array. This is the Iterative approach with O(1) Space.
prev1 = 1 (for step 1) and prev2 = 1 (for step 0).n.current = prev1 + prev2.prev2 = prev1 and prev1 = current.Detailed Dry Run
n = 3
| i | current | prev1 | prev2 | Action |
|---|---|---|---|---|
| - | - | 1 | 1 | Init |
| 2 | 1+1=2 | 2 | 1 | Update variables |
| 3 | 2+1=3 | 3 | 2 | Update variables |
⚠️ Common Pitfalls & Tips
Be careful with base cases. If is considered 1 way (doing nothing), the logic holds. For , it's also 1 way. The loop should start from 2.
Regular Expression Matching
Given an input string s and a pattern p, implement regular expression matching with support for . and * where:
. Matches any single character.* Matches zero or more of the preceding element.The matching should cover the entire input string (not partial).
s = "aa", p = "a*"
- 'a*' can match "", "a", "aa", "aaa", etc.
- Since it matches "aa", result is True.
s = "ab", p = ".*"
- '.*' matches anything. Result is True.Examples
index 0 matches 'a', but index 1 does not. Pattern is exhausted.
Intuition
At each step, we check if the first character of the string matches the first character of the pattern. Special handling is needed for '*' which can match zero or more of the preceding element.
p is empty, return true if s is also empty.s[0] == p[0] or p[0] == '.').p[1] == '*':
isMatch(s, p[2:]).s and keep pattern: first_match && isMatch(s[1:], p).isMatch(s[1:], p[1:]).Detailed Dry Run
s = "aa", p = "a*"
Intuition
The recursive regex matching recalculates many overlapping (i, j) states. Adding a 2D memo array indexed by (si, pi) eliminates repeated work.
s="aa", p="a*"
solve(0,0): p[1]='*' -> zero match: solve(0,2); one+ match: solve(1,0)
solve(1,0): cached after first call!Detailed Dry Run
s="aa", p="a*". memo[0][0]: p[1]='*', try zero: memo[0][2]=T (empty matches empty). Return True.
⚠️ Common Pitfalls & Tips
The '*' operator makes regex DP tricky. Always first try the 'zero match' path (pi+2), then only try 'one+ match' if firstMatch is true to avoid infinite loops.
Intuition
We use a 2D table dp[i][j] to represent if s[0...i-1] matches p[0...j-1].
dp[0][0] = true (empty string matches empty pattern).dp[i][j] = dp[i][j-2].dp[i][j] = dp[i-1][j] if s[i-1] matches p[j-2].s[i-1] matches p[j-1] (either same char or '.'), dp[i][j] = dp[i-1][j-1].Detailed Dry Run
s = "aa", p = "a*"
| i\j | "" | 'a' | '*' |
|---|---|---|---|
| "" | T | F | T (zero 'a') |
| 'a' | F | T | T (one 'a') |
| 'a' | F | F | T (two 'a') |
⚠️ Common Pitfalls & Tips
Handling the empty string base case for patterns like a*b*c* is tricky. Ensure you initialize dp[0][j] correctly for '*' characters.
Burst Balloons
You are given n balloons, indexed from 0 to n - 1. Each balloon is painted with a number on it, represented by an array nums. You are asked to burst all the balloons.
If you burst the i-th balloon, you will get nums[i - 1] * nums[i] * nums[i + 1] coins. After bursting it, the i - 1-th and i + 1-th balloons become adjacent.
Find the maximum coins you can collect by bursting the balloons wisely. (Assume nums[-1] = nums[n] = 1).
nums = [3, 1, 5, 8]
Add boundary: [1, 3, 1, 5, 8, 1]
If we burst 1 first:
Coins = 3 * 1 * 5 = 15
Remaining: [1, 3, 5, 8, 1]
Wait! It's better to think: "Which balloon is burst LAST in range (i, j)?"Examples
nums = [3,1,5,8] -> [3,5,8] -> [3,8] -> [8] -> [] coins = 315 + 358 + 138 + 181 = 15 + 120 + 24 + 8 = 167
Intuition
Pick a balloon k to burst last in the interval (i, j). This splits the problem into two subproblems: bursting balloons in (i, k) and (k, j). Try all possible candidates for the last balloon.
solve(i, j) that returns max coins from interval [i, j].i > j, return 0.max(solve(i, k-1) + solve(k+1, j) + A[i-1]*A[k]*A[j+1]) for all k in [i, j].Detailed Dry Run
nums=[3, 1, 5] -> A=[1, 3, 1, 5, 1] solve(1, 3) where length is 3. Try k=2 as last: solve(1, 1) + solve(3, 3) + 111 = 3 + 25 + 1 = 29.
Intuition
The brute force re-solves the same (i, j) intervals. We add a 2D memo table to cache the max coins for each interval.
nums=[3,1,5,8], A=[1,3,1,5,8,1]
memo[1][4] = max coins for range (1..4)
After first computation, reused whenever (1,4) is subproblem!Detailed Dry Run
A=[1,3,1,5,8,1]. memo[1][4]: try k=1,2,3,4 as last balloon. k=3 gives 351+memo[1][2]+memo[4][4]=15+9+40=... Cached!
⚠️ Common Pitfalls & Tips
This is an interval DP problem. The key insight — picking the LAST balloon to burst — allows clean decomposition. Memoization here gives the same O(N^3) as bottom-up but is easier to reason about.
Intuition
Instead of deciding which balloon to burst first, we decide which balloon k is burst last in the interval (i, j). This allows us to split the problem into two independent subproblems: bursting balloons in (i, k) and (k, j).
1 at both ends.dp[i][j] = max coins from bursting balloons between index i and j (exclusive).len from 1 to n:
i from 1 to n - len + 1:
j = i + len - 1.k from i to j (last balloon to burst):
coins = nums[i-1] * nums[k] * nums[j+1] + dp[i][k-1] + dp[k+1][j].Detailed Dry Run
nums = [3, 1, 5] Boundary padded: [1, 3, 1, 5, 1]
| Interval | Last k | Calculation | Result |
|---|---|---|---|
| (1,1) | 1 | 131 + 0 + 0 | 3 |
| (2,2) | 2 | 315 + 0 + 0 | 15 |
| (3,3) | 3 | 551 + 0 + 0 | 25 |
| (1,2) | 1 | 135 + 0 + dp[2][2] = 15+15 | 30 |
| (1,2) | 2 | 115 + dp[1][1] + 0 = 5+3 | 8 |
⚠️ Common Pitfalls & Tips
The intuition for O(N^3) interval DP is often hard to find. Remember: "last item to be processed" is a common pattern for problems involving shrinking ranges (like Matrix Chain Multiplication).
Longest Valid Parentheses
Given a string containing just the characters '(' and ')', return the length of the longest valid (well-formed) parentheses substring.
s = ")()())"
Substrings:
() -> 2
()() -> 4 (Max)
DP Strategy:
dp[i] = length of longest valid parentheses ending at index i.Examples
The longest valid parentheses substring is "()".
The longest valid parentheses substring is "()()".
Intuition
We use a 1D DP array where dp[i] is the length of the longest valid parentheses ending at index i.
s[i] == '(', dp[i] = 0 (valid substring can't end with open paren).s[i] == ')':
s[i-1] == '(', then dp[i] = dp[i-2] + 2.s[i-1] == ')' and s[i - dp[i-1] - 1] == '(', then dp[i] = dp[i-1] + dp[i - dp[i-1] - 2] + 2.dp[i].Detailed Dry Run
s = "()()"
| i | char | Calculation | dp[i] |
|---|---|---|---|
| 0 | ( | - | 0 |
| 1 | ) | dp[0-1]+2 = 2 | 2 |
| 2 | ( | - | 0 |
| 3 | ) | dp[3-2]+2 = dp[1]+2 = 4 | 4 |
Intuition
Use a stack to track unmatched parenthesis indices. Whenever we find a matching pair, the valid length is i - stack.peek().
s = ")()())"
stack: [-1]
i=0 ')': stack = [] -> push 0? No, stack is empty -> push i=0
i=1 '(': stack = [0, 1]
i=2 ')': pop 1 -> stack = [0], len = 2-0=2
i=3 '(': stack = [0, 3]
i=4 ')': pop 3 -> stack = [0], len = 4-0=4
i=5 ')': pop 0 -> stack = [], push 5
Max = 4Detailed Dry Run
s="())". stack=[-1]. i=0 '(': push 0. i=1 ')': pop 0, len=1-(-1)=2, max=2. i=2 ')': pop -1, empty -> push 2. Result = 2.
⚠️ Common Pitfalls & Tips
Initialize the stack with -1 as a sentinel base index. Without this, first close parenthesis would fail to compute length (0 - nothing = invalid).
Intuition
We can optimize space to O(1) by scanning the string twice (left-to-right and right-to-left) while keeping track of left and right parentheses counts.
left++ for '(', right++ for ')'.left == right, current length is 2 * left. Update max.right > left, reset both to 0.left > right.Detailed Dry Run
s = "(()"
Edit Distance
Given two strings word1 and word2, return the minimum number of operations required to convert word1 to word2.
You have the following three operations permitted on a word:
word1 = "horse", word2 = "ros"
Steps:
1. horse -> rorse (replace 'h' with 'r')
2. rorse -> rose (remove 'r')
3. rose -> ros (remove 'e')
Result: 3
DP Strategy:
dp[i][j] = min distance between word1[0...i] and word2[0...j]Examples
horse -> rorse (replace 'h' with 'r') -> rose (remove 'r') -> ros (remove 'e')
Intuition
We build a 2D table where dp[i][j] is the minimum operations to convert word1[0...i] to word2[0...j].
word1[i-1] == word2[j-1], no operation is needed for the current character: dp[i][j] = dp[i-1][j-1].1 + dp[i][j-1]1 + dp[i-1][j]1 + dp[i-1][j-1]i (or j) deletions.Detailed Dry Run
word1 = "abc", word2 = "abd"
| i\j | "" | 'a' | 'b' | 'd' |
|---|---|---|---|---|
| "" | 0 | 1 | 2 | 3 |
| 'a' | 1 | 0 (match) | 1 | 2 |
| 'b' | 2 | 1 | 0 (match) | 1 |
| 'c' | 3 | 2 | 1 | 1 (replace 'c' with 'd') |
Intuition
The recursive brute force for edit distance has overlapping subproblems (i, j). Cache results to avoid recomputation.
word1='horse', word2='ros'
solve(0,0): 'h'!='r' -> min(solve(1,1), solve(1,0), solve(0,1))
solve(1,1): cached after first call!Detailed Dry Run
word1="ab", word2="a". solve(0,0): 'a'=='a' -> solve(1,1). solve(1,1): 'b'!='', return 1+solve(2,1)=1.
Intuition
To compute dp[i][j], we only need the current row and the previous row. Specifically, dp[i-1][j-1] (diagonal), dp[i-1][j] (up), and dp[i][j-1] (left).
curr array of size n+1 with base case values 0...n.word1. For each i:
dp[i-1][j-1] in a prev variable.curr[0] for the base case (conversions from word1[0...i] to empty string).word2 to update curr[j].curr[n].Detailed Dry Run
word1 = "abc", word2 = "abd" Focus on space reduction where we store only 1 row. Diag (prev) cache is critical to keep the value from the 'upper-left' cell before it was overwritten.
Frog Jump
A frog is crossing a river. The river is divided into units and at each unit there may or may not be a stone. The frog can jump on a stone, but it must not jump into the water.
Given a list of stones' positions (in units) in sorted ascending order, determine if the frog is able to cross the river by landing on the last stone. Initially, the frog is on the first stone and assume the first jump must be 1 unit.
If the frog's last jump was k units, then its next jump must be either k - 1, k, or k + 1 units. Note that the frog can only jump in the forward direction.
Stones: [0, 1, 3, 5, 6, 8, 12, 17]
Jump 1: 0 -> 1 (Size 1)
Jump 2: 1 -> 3 (Size 2, allowed: 1-1, 1, 1+1)
Jump 3: 3 -> 5 (Size 2, allowed: 1, 2, 3)
...Examples
0->1(1)->3(2)->5(2)->8(3)->12(4)->17(5)
Intuition
Try every valid jump from the current stone. For a stone at index i reached with a jump of k, the next jump can be k-1, k, or k+1.
pos, if we reached it with jump k, try positions pos + k - 1, pos + k, and pos + k + 1.true.Detailed Dry Run
stones = [0, 1, 3]
Intuition
Cache (position, lastJump) states to avoid recomputing the same frog position visited with the same jump size.
stones=[0,1,3,5,6], target=6
solve(1, 1): -> solve(3, 2) or solve(3, 1)
solve(3,2): cached after first call!Detailed Dry Run
Cache key=(pos, k). solve(1,1) -> solve(3,2). solve(3,2) -> solve(5,2) or solve(5,3). Cached when revisited.
Intuition
We maintain a map where the key is the stone's position and the value is a set of all possible jump sizes that could have landed the frog on that stone.
Map<Integer, Set<Integer>>. For each stone, create an empty set.0 to the first stone (base case).s:
k in map.get(s):
step in {k-1, k, k+1}:
step > 0 and map.containsKey(s + step):
step to the set for stone s + step.true.Detailed Dry Run
stones = [0, 1, 3]
| Stone | Jump Sizes |
|---|---|
| 0 | {1} (Initial jump is 1) |
| 1 | {1} (from 0+1) |
| 3 | {2} (from 1+2, k=1 -> k+1=2) |
| Result: True (Set for 3 is not empty) |
Minimum Path Sum in Grid
Given a m x n grid filled with non-negative numbers, find a path from top-left to bottom-right, which minimizes the sum of all numbers along its path.
Note: You can only move either down or right at any point in time.
Grid:
[1, 3, 1]
[1, 5, 1]
[4, 2, 1]
Paths:
1-3-1-1-1 = 7 (Min)
1-1-5-2-1 = 10
1-1-4-2-1 = 9Examples
Because the path 1 → 3 → 1 → 1 → 1 minimizes the sum.
Intuition
The minimum sum to reach cell (i, j) is the value of the current cell plus the minimum of the paths reaching from above (i-1, j) or from the left (i, j-1).
dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1]).dp[0][0] = grid[0][0].Detailed Dry Run
grid = [[1, 3], [1, 5]]
| Cell | Calculation | Sum |
|---|---|---|
| (0, 0) | Base | 1 |
| (0, 1) | 1 + grid[0][1] = 1+3 | 4 |
| (1, 0) | 1 + grid[1][0] = 1+1 | 2 |
| (1, 1) | 5 + min(dp[0][1], dp[1][0]) = 5 + min(4, 2) | 7 |
Intuition
The recursive brute force for grid path counting has overlapping subproblems. We cache each (i, j) state so we never recompute it.
Memo Grid (m=3, n=3):
| 0 | 1 | 1 |
| 1 | 2 | ? | <- memo[1][2] reused by (2,2)
| 1 | ? | ? |Detailed Dry Run
m=3, n=3. memo[2][2] = memo[1][2] + memo[2][1]. Both sub-calls are cached after first computation.
⚠️ Common Pitfalls & Tips
Recursion adds O(M+N) stack overhead compared to the iterative DP. For very large grids, prefer the iterative approach to avoid stack overflow.
Intuition
To compute dp[i][j], we only need the value above it (previous row) and the value to its left (current row). We can use a single array of size n.
row array of size n with Infinity.row[0] = 0 (starting point for accumulation).i (row):
row[0] += grid[i][0]j (col from 1 to n-1):
row[j] = grid[i][j] + min(row[j], row[j-1]).row[n-1].Detailed Dry Run
grid = [[1, 3], [1, 5]]
| Row | row array calculation | State |
|---|---|---|
| 0 | [1, 1+3=4] | [1, 4] |
| 1 | [1+1=2, 5+min(2, 4)=7] | [2, 7] |
Maximal Square
Given an m x n binary matrix filled with 0's and 1's, find the largest square containing only 1's and return its area.
Matrix:
1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0
The 2x2 square of 1s at (1,2) to (2,3) is one of the squares.
Wait, there is a larger one? No, area is 4.
DP Strategy:
dp[i][j] = side length of the largest square ending at (i, j).Examples
Intuition
A square of size k ending at (i, j) can only be formed if there are squares of size k-1 ending at (i-1, j), (i, j-1), and (i-1, j-1).
dp[i][j] represents the side length of the largest square ending at matrix[i][j].matrix[i][j] == '1':
dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1.maxSide * maxSide.Detailed Dry Run
Matrix: 1 1 1 1
| i\j | 0 | 1 |
|---|---|---|
| 0 | 1 | 1 |
| 1 | 1 | min(1, 1, 1)+1 = 2 |
| Result: 2^2 = 4 |
Intuition
Cache the result of dp(i, j) so each cell is computed only once. The recurrence: dp(i,j) = min(dp(i-1,j), dp(i,j-1), dp(i-1,j-1)) + 1 if matrix[i][j]=='1'.
Matrix: Memo:
1 1 1 1 1 1
1 1 1 -> 1 2 ?
1 1 1 1 ? 3 <- dp(2,2) = min(2,2,2)+1=3Detailed Dry Run
dp(2,2): matrix[2][2]='1'. min(dp(1,2)=2, dp(2,1)=2, dp(1,1)=2)+1=3. Area=9.
Intuition
We only need the previous row and the current row. Specifically, dp[i-1][j], dp[i][j-1], and the diagonal dp[i-1][j-1].
dp array of size n + 1.prev variable to store the diagonal value dp[i-1][j-1] from the previous row.Detailed Dry Run
Same logic as 2D but overwriting the same row array. 'prev' variable caches the upper-left (diagonal) value before it gets updated for the current row's calculation.
Palindrome Partitioning II
Given a string s, partition s such that every substring of the partition is a palindrome.
Return the minimum cuts needed for a palindrome partitioning of s.
s = "aab"
Valid partitions:
["a", "a", "b"] -> 2 cuts
["aa", "b"] -> 1 cut (Min)
Result: 1Examples
The palindrome partitioning ["aa","b"] could be produced using 1 cut.
Intuition
Try every possible split point for the string. If the prefix is a palindrome, recursively find the minimum cuts for the remaining suffix.
solve(i) returns min cuts for s[i...n-1].s[i...n-1] is a palindrome, return 0 (no more cuts needed).j from i to n-1:
s[i...j] is a palindrome:
res = min(res, 1 + solve(j + 1)).Detailed Dry Run
s = "aab"
Intuition
Cache the result of solve(i) (min cuts for suffix s[i:]) using a memo array. Also precompute palindrome check to avoid O(N) palindrome check every time.
s = "aab"
memo[2] = 0 ("b" is palindrome)
memo[1] = 1 ("ab" -> "a"|"b" = 1 cut, or "ab" not pal)
memo[0] = 1 ("aa"|"b" = 1 cut, memo[2] cached!)Detailed Dry Run
s="aab". solve(2)=0. solve(1)=1+solve(2)=1. solve(0): is_pal[0][1]=True -> 1+solve(2)=1. Result=1.
Intuition
We need two DP arrays. One 2D array isPal[i][j] to precompute if s[i...j] is a palindrome. Another 1D array cuts[i] to store the minimum cuts needed for s[0...i].
isPal[i][j] using standard palindrome DP: s[i] == s[j] && (j-i < 2 || isPal[i+1][j-1]).cuts[i] = i (max possible cuts: each char separate).j from 0 to n-1:
isPal[0][j] is true, cuts[j] = 0 (no cuts needed for entire prefix).i from 1 to j:
isPal[i][j] is true, cuts[j] = min(cuts[j], cuts[i-1] + 1).Detailed Dry Run
s = "aab" Precomputed isPal: [0,0]=T, [1,1]=T, [2,2]=T, [0,1]=T ("aa")
| j | isPal(0,j) | Action | cuts[j] |
|---|---|---|---|
| 0 | T | 0 cuts | 0 |
| 1 | T | 0 cuts | 0 |
| 2 | F | cuts[1]+1 ("aa" | "b") |
| Result: 1 |
Interleaving String
Given strings s1, s2, and s3, find whether s3 is formed by an interleaving of s1 and s2.
An interleaving of two strings s and t is a configuration where they are divided into non-empty substrings such that:
s = s1 + s2 + ... + snt = t1 + t2 + ... + tms1 + t1 + s2 + t2 + s3 + t3 + ... or t1 + s1 + t2 + s2 + t3 + s3 + ...s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
Characters from s1 (bold), s2 (italic):
a a d b b c b c a c
(a)(a)(d)(b)(b)(c)(b)(c)(a)(c)
s1 s1 s2 s2 s2 s1 s2 s1 s2 s1
Result: TrueExamples
Intuition
We use a 2D table dp[i][j] to represent if s3[0...i+j-1] is formed by interleaving s1[0...i-1] and s2[0...j-1].
dp[i][j] is true if:
dp[i-1][j] is true AND s1[i-1] == s3[i+j-1]dp[i][j-1] is true AND s2[j-1] == s3[i+j-1]dp[0][0] = true.dp[i][0] matches only against s1. dp[0][j] matches only against s2.Detailed Dry Run
s1="a", s2="b", s3="ab"
| i\j | "" | 'b' |
|---|---|---|
| "" | T | F (s2[0]!=s3[0]) |
| 'a' | T (s1[0]==s3[0]) | T (s1[0]==s3[0] or s2[0]==s3[1]) |
| Result: True |
Intuition
The 2D DP bottom-up approach fills the entire table. Top-down memoization only explores reachable states. Both have the same asymptotic complexity.
s1="a", s2="b", s3="ab"
solve(0,0): s1[0]='a'==s3[0] -> solve(1,0)
or s2[0]='b'!=s3[0] -> blocked
solve(1,0): s2[0]='b'==s3[1] -> solve(1,1)=TrueDetailed Dry Run
s1="a", s2="b", s3="ab". solve(0,0): 'a'=='a' -> solve(1,0). solve(1,0): 'b'=='b' -> solve(1,1)=True.
Intuition
Like most 2D matrix DP problems where dp[i][j] only depends on dp[i-1][j] and dp[i][j-1], we can reduce space to O(M) (where M is length of s2).
dp array of size s2.length + 1.dp[j] will represent dp[i][j].dp[j] is updated as (dp[j] && s1[i-1] == s3[i+j-1]) || (dp[j-1] && s2[i-1] == s3[i+j-1]).Detailed Dry Run
Same logic, but we only keep one row. The 'up' dependency comes from the array value before modification, and 'left' dependency comes from the array value after modification (current row).
Minimum Cost to Merge Stones
There are n piles of stones arranged in a row. The i-th pile has stones[i] stones.
A move consists of merging exactly k consecutive piles into one pile, and the cost of this move is equal to the total number of stones in these k piles.
Return the minimum cost to merge all piles of stones into one pile. If it is impossible, return -1.
stones = [3, 2, 4, 1], k = 2
[3, 2, 4, 1] -> [5, 4, 1] (cost 5)
[5, 4, 1] -> [5, 5] (cost 5)
[5, 5] -> [10] (cost 10)
Total Cost: 20 (Min)Examples
After merging 3 piles, we have 2 piles left. We need exactly k=3 piles to merge.
Intuition
Try all possible splitting points in the current range [i, j] to merge into m piles. This is a classic exhaustive search for partition problems.
solve(i, j, piles) returns min cost to merge stones[i...j] into piles piles.piles piles from [i, j], we can split into [i, k] (merged into 1 pile) and [k+1, j] (merged into piles-1 piles).solve(i, j, piles) = min(solve(i, k, 1) + solve(k + 1, j, piles - 1)).solve(i, i, 1) = 0.Detailed Dry Run
stones = [3, 2, 4], K = 3
Intuition
Cache the result of solve(i, j, m) — the minimum cost to merge stones[i..j] into m piles. This avoids recomputing the same interval+pile-count combinations.
solve(0,3,1) [merge ALL into 1 pile]
-> solve(0,1,1) + solve(2,3,1) [split and merge]
Both sub-calls cached after first computation!Detailed Dry Run
stones=[3,2,4], K=3. solve(0,2,1): need K=3 piles first. solve(0,2,3)=0 (3 stones, 3 piles is base). Cost=9. Cached.
Intuition
This is a complex interval DP problem. We want to find the min cost to merge stones in range [i, j] into p piles.
dp[i][j][m] means the min cost to merge stones in stones[i...j] into m piles.[i, j] into m piles, we can split it into [i, k] (merged into 1 pile) and [k+1, j] (merged into m-1 piles).dp[i][j][m] = min(dp[i][k][1] + dp[k+1][j][m-1]) for k in [i, j-1].dp[i][i][1] = 0 (single pile is already 1 pile).m == 1, the cost is dp[i][j][k] + sum(stones[i...j]).Detailed Dry Run
This is a classical matrix chain multiplication style problem. The state dp[i][j][m] helps track number of piles remaining, which is crucial because we can only merge EXACTLY K piles at once.
Help us improve! Report bugs or suggest new features on our Telegram group.