1. Introduction and Motivation
Once you can derive a DP recurrence from first principles, the next skill is pattern recognition: most problems you will meet are a small number of recurring shapes wearing different clothing. DP problems fall into recognizable families based on the shape of their state space. 1D DP uses a single variable (array index, amount remaining, step count). 2D DP uses two variables (two string indices, grid coordinates, item × capacity). Recognizing the pattern lets you define the recurrence immediately.
2. Prerequisites
This lesson assumes you have already worked through Part 14: Dynamic Programming Foundations, where the discipline of turning a recursive brute force into a defined state, a recurrence, and a base case is built from first principles. Nothing here re-derives that methodology from scratch — each problem below states its dp definition and transition directly and moves straight into implementation.
If a recurrence below does not immediately make sense, that is the signal to go back to Part 14 and re-walk the state-design steps, rather than to memorize the code in front of you.
3. 1D DP: Climbing Stairs
You can climb 1 or 2 steps at a time. How many distinct ways to reach step $n$? This is structurally identical to Fibonacci: $dp[i] = dp[i-1] + dp[i-2]$. The "number of ways" at each step is the sum of ways to reach it from one step below (1 step) and two steps below (2 steps). State: $dp[i]$ = ways to reach step $i$. Transition: $dp[i] = dp[i-1] + dp[i-2]$. Base: $dp[0]=1, dp[1]=1$.
1D DP: Kadane's Algorithm and Min-Cost Climbing Stairs
Kadane's Algorithm (Maximum Subarray Sum)
Given an integer array, find the contiguous subarray with the largest sum. Every subarray ending at index $i$ either extends the best subarray ending at $i-1$, or starts fresh at $i$ — and once the running sum goes negative, dragging it forward can only shrink whatever comes next. State: $dp[i]$ = maximum sum of a subarray ending at index $i$. Transition: $dp[i] = \max(nums[i],\; dp[i-1] + nums[i])$. Base: $dp[0] = nums[0]$. Only $dp[i-1]$ is ever read, so the array collapses to a single rolling variable: $O(n)$ time, $O(1)$ space. The insight worth internalizing: whenever the running sum $dp[i-1]$ is negative, reset it to $nums[i]$ rather than carry the negative baggage forward — a negative prefix can never help a future sum, only hurt it.
As a slightly harder follow-up, the implementation below also tracks the start and end indices of the maximizing subarray itself, not just its sum — that means remembering where the current run began and only moving that marker forward on a reset, never on an ordinary extension.
Min-Cost Climbing Stairs
Section 9's Exercises and Mastery posed Min Cost Climbing Stairs (LeetCode 746) as a conceptual exercise: adapt the Climbing Stairs recurrence so each transition also pays a cost, then re-check the base cases. Here is that recurrence carried through to a full implementation. Each step $i$ carries a cost to leave it, and you may start standing on step 0 or step 1 for free; from there you always pay the cost of whichever step you leave. State: $dp[i]$ = minimum cost to reach step $i$. Transition: $dp[i] = cost[i] + \min(dp[i-1], dp[i-2])$. Base: $dp[0] = cost[0]$, $dp[1] = cost[1]$. The top of the staircase is one step past the last index, so the answer is $\min(dp[n-1], dp[n-2])$ — whichever of the last two steps is cheaper to leave from. Time: $O(n)$, Space: $O(1)$ once rolled to two variables, exactly like Climbing Stairs itself.
4. 1D DP: House Robber
Given an array of house values where adjacent houses cannot both be robbed (they trigger an alarm), maximize total loot. This is the classic "include/exclude" DP pattern: at each house $i$, you either rob it (adding its value to the best from $i-2$) or skip it (carrying forward the best from $i-1$). State: $dp[i]$ = max loot from houses $0..i$. Transition: $dp[i] = \max(dp[i-1],\; dp[i-2] + nums[i])$. This pattern appears in many variants: circular houses (LeetCode 213), binary tree robber (LeetCode 337).
5. 2D DP: Longest Common Subsequence
Given two strings, find the length of their longest common subsequence (characters in order but not necessarily contiguous). This is the gateway 2D DP problem — state is defined by two pointers into the two strings. If characters match, both advance; otherwise, try advancing each pointer separately and take the maximum. State: $dp[i][j]$ = LCS length of $text1[0..i-1]$ and $text2[0..j-1]$. Transition: if $text1[i-1] = text2[j-1]$, then $dp[i][j] = dp[i-1][j-1] + 1$; else $dp[i][j] = \max(dp[i-1][j], dp[i][j-1])$.
This pattern generalizes to Edit Distance (add insert/delete/replace costs), Shortest Common Supersequence, and many string-comparison problems.
6. 2D DP: Coin Change
Given coin denominations and a target amount, find the minimum number of coins to make that amount. This is an unbounded knapsack variant — each coin can be used unlimited times. Build up solutions from amount 0 to target: for each amount $a$, try every coin that fits and take the minimum. State: $dp[a]$ = fewest coins to make amount $a$. Transition: $dp[a] = \min(dp[a-coin] + 1)$ for each $coin \le a$. Unlike 0/1 Knapsack where items are used once, here we iterate over coins in the inner loop because reuse is allowed.
7. Knapsack Problem
The 0/1 Knapsack is the quintessential DP problem: given $n$ items each with a weight and value, maximize total value without exceeding a weight capacity. "0/1" means each item is either taken or left — no fractions. State: $dp[w]$ = max value achievable with capacity $w$. The key insight is traversing the capacity backwards in the 1D optimization — this prevents using the same item twice. The Unbounded Knapsack variant (items reusable) traverses forwards instead.
Knapsack Family
Many problems reduce to knapsack: Subset Sum (can you make target?), Partition Equal Subset Sum (split into two equal halves), Coin Change (unbounded knapsack with min count), and Target Sum (assign +/- signs). The 0/1 vs unbounded distinction determines inner loop direction.
8. Pattern Comparison Table
Each problem above states its recurrence in prose. This table collects them side by side so the shared shape — and the differences that matter — are visible at a glance.
| Problem | State Definition | Recurrence | Time | Space |
|---|---|---|---|---|
| Climbing Stairs | dp[i] = ways to reach step i | dp[i] = dp[i-1] + dp[i-2] | O(n) | O(1) |
| House Robber | dp[i] = max loot from houses 0..i | dp[i] = max(dp[i-1], dp[i-2] + nums[i]) | O(n) | O(1) |
| Kadane's Algorithm | dp[i] = max subarray sum ending at index i | dp[i] = max(nums[i], dp[i-1] + nums[i]) | O(n) | O(1) |
| Min-Cost Climbing Stairs | dp[i] = min cost to reach step i | dp[i] = cost[i] + min(dp[i-1], dp[i-2]) | O(n) | O(1) |
| Longest Common Subsequence | dp[i][j] = LCS length of text1[0..i-1] and text2[0..j-1] | dp[i][j] = dp[i-1][j-1] + 1 if characters match, else max(dp[i-1][j], dp[i][j-1]) | O(m·n) | O(m·n) |
| Coin Change | dp[a] = fewest coins to make amount a | dp[a] = min(dp[a-coin] + 1) over each coin ≤ a | O(amount·coins) | O(amount) |
| 0/1 Knapsack | dp[w] = max value achievable with capacity w | dp[w] = max(dp[w], dp[w-weight[i]] + value[i]), capacity traversed backwards per item | O(n·capacity) | O(capacity) |
9. Exercises and Mastery
Easy
- Min Cost Climbing Stairs (LeetCode 746): each step has a cost, and you may start at index 0 or index 1. Find the minimum total cost to reach the top. Adapt the Climbing Stairs recurrence so each transition also adds
cost[i], then verify your base cases still make sense with the new starting rule.
Medium
- Longest Palindromic Subsequence: run the Longest Common Subsequence algorithm between a string and its own reverse. Explain why the result equals the longest palindromic subsequence, and identify one situation (repeated characters) where you should double-check that the equivalence still holds.
Hard
- The 0/1 Knapsack section above already states the full recurrence: state
dp[w], transitiondp[w] = max(dp[w], dp[w-weight[i]] + value[i]), and the backwards-traversal justification. Trace it by hand instead of re-deriving it: withweights = [1, 2, 3, 4],values = [10, 20, 30, 40],capacity = 5, recompute thedparray forw = 0..5after each item is processed, and confirm the final answer is 50.
dp[] precisely, derive the recurrence, and state the time and space complexity — before writing any code.Review rhythm: tomorrow, rederive all five recurrences from memory without looking at the code; next week, implement Climbing Stairs, House Robber, and Coin Change from scratch in one sitting; several weeks later, solve Min Cost Climbing Stairs and Longest Palindromic Subsequence without hints. Finally, teach someone else why 0/1 Knapsack traverses capacity backwards while Coin Change and Unbounded Knapsack traverse it forwards.
10. Summary Cheat Sheet
- 1D DP uses a single index; 2D DP uses two indices (two strings, or item × capacity).
- Climbing Stairs and House Robber both reduce to a rolling recurrence over
dp[i-1]anddp[i-2]— O(n) time, O(1) space once space-optimized. - LCS is the gateway 2D DP problem: match and advance both pointers, or skip one and take the max.
- Coin Change is an unbounded knapsack in disguise — coins are reusable, so the coin loop stays inside the amount loop.
- 0/1 Knapsack traverses capacity backwards to enforce "use each item at most once"; Unbounded Knapsack traverses forwards to allow reuse.
- Subset Sum, Partition Equal Subset Sum, and Target Sum are all knapsack in disguise — the pattern, not the story, defines the recurrence.