Home/Data Structures & Algorithms/Dynamic Programming: 1D, 2D & Grid

Lost your password? Please enter your email address. You will receive a link and will create a new password via email.
Please briefly explain why you feel this question should be reported.
Please briefly explain why you feel this answer should be reported.
Please briefly explain why you feel this user should be reported.
Knapsack problems, sequence alignment, interval DP, rolling memory reductions, and state-machine transitions.
Why does Patience Sorting solve Longest Increasing Subsequence in O(N log N) instead of O(N^2)?
This is one of the most common points of confusion when studying LIS. Let's clear up the mystery of why the tails array works even though its contents look 'wrong'. 1. What does the `tails` array actually represent? In Patience Sorting (inspired by solitaire card games): tails[k] stores the SMALLESTRead more
This is one of the most common points of confusion when studying LIS. Let’s clear up the mystery of why the
tailsarray works even though its contents look ‘wrong’.1. What does the `tails` array actually represent?
In Patience Sorting (inspired by solitaire card games):
tails[k]stores the SMALLEST ending value of an increasing subsequence of lengthk + 1found so far.Why do we care about the smallest ending value? Because in an increasing subsequence, the smaller the number you end with, the easier it is for future numbers to be bigger than it! You want to keep your options as open as possible.
2. The Step-by-Step Card Dealing Analogy
Suppose our array is:
[10, 9, 2, 5, 3, 7, 101, 18].10:tails = [10](Best subsequence of len 1 ends with 10).9: 9 < 10. Replace 10 with 9:tails = [9](Ending with 9 is strictly better than ending with 10).2: 2 < 9. Replace 9 with 2:tails = [2].5: 5 > 2! Extend!tails = [2, 5](Best len 1 ends in 2, best len 2 ends in 5).3: 3 < 5. Replace 5 with 3:tails = [2, 3](Now best len 2 ends in 3!).7: 7 > 3! Extend!tails = [2, 3, 7](Len 3).101: Extend!tails = [2, 3, 7, 101](Len 4).18: 18 < 101. Replace 101 with 18:tails = [2, 3, 7, 18].Total length of
tailsis 4. The answer is 4!3. Why the array contents might look scrambled, but length is ALWAYS correct
Imagine if after
[2, 3, 7, 18]we saw1. We would replace2with1, resulting intails = [1, 3, 7, 18].Notice that
[1, 3, 7, 18]might not be a valid subsequence from the original array. And that doesn’t matter!Replacing
2with1only prepares the board for a hypothetical future subsequence that starts with1. It does not change the fact that a valid subsequence of length 4 ([2, 3, 7, 18]) was already locked in!The length of
tailsonly increases when a number is strictly greater than ALL existing tail values. Replacements never shrink the array length!Clean Python 3.12 Implementation with bisect_left
Complexity Breakdown
- Time Complexity:
- Space Complexity:
See lessO(N log N). We iterate throughNelements, and for each element we perform binary search overtails(at most lengthN).N * log(N). For100,000elements, this finishes in 0.02 seconds (compared to ~45 seconds forO(N^2)).O(N)to hold thetailsarray.0/1 Knapsack: Why does reverse iteration turn O(N*W) space into O(W) space?
This is one of the most fundamental 'aha!' moments in dynamic programming. Let's walk through the memory mechanics so you never forget it. 1. The 2D State Transition In the classic 0/1 Knapsack, the formula is: dp[i][w] = max( dp[i-1][w], // Option A: Skip item i (take answer from previous row) dp[iRead more
This is one of the most fundamental ‘aha!’ moments in dynamic programming. Let’s walk through the memory mechanics so you never forget it.
1. The 2D State Transition
In the classic 0/1 Knapsack, the formula is:
Notice the critical detail: in Option B,
dp[i-1][w - weight[i]]comes from rowi-1(before itemiwas even considered). That is what guarantees you only take itemiat most once.2. Compressing to a 1D Array
Notice that to compute row
i, you only ever look at rowi-1. You don’t need rowsi-2,i-3, etc. So we can just reuse a single 1D array:dp[w].What happens if you iterate FORWARD (w = weight[i] to W)?
Suppose item 1 has
weight = 2, value = 10and capacity is6.w = 2:dp[2] = dp[0] + 10 = 10.w = 4:dp[4] = dp[4 - 2] + 10 = dp[2] + 10 = 10 + 10 = 20! (Wait, you just reused item 1 twice!)w = 6:dp[6] = dp[4] + 10 = 30! (You used item 1 three times!)Because you updated smaller weights first, larger weights read the already updated values from the current item. That turns it into Unbounded Knapsack (infinite items)!
What happens if you iterate BACKWARD (w = W down to weight[i])?
w = 6: readsdp[4](which is still 0 from the previous item!).dp[6] = 0 + 10 = 10.w = 4: readsdp[2](which is still 0!).dp[4] = 0 + 10 = 10.w = 2: readsdp[0](which is 0!).dp[2] = 0 + 10 = 10.By sweeping backwards, whenever you query
w - weight[i], that smaller index has not yet been touched for the current item. It still holds the pristine value from itemi-1!Production Python 3.12 Implementation
Summary Rule of Thumb
- 0/1 Knapsack (items used at most once) → Iterate Backward (
- Unbounded Knapsack / Coin Change (items can be reused infinitely) → Iterate Forward (
See lessW → weight).weight → W).