Dynamic Programming Fundamentals
The Mental Model
Dynamic Programming is smart recursion. Instead of solving the same subproblem multiple times, we solve it once and remember the answer. The key insight: if you can express the answer to a problem in terms of answers to smaller problems, DP might work. Analogy: Imagine computing Fibonacci numbers by hand. To get F(50), you need F(49) and F(48). To get F(49), you need F(48) and F(47). Notice F(48) is needed twice—and it explodes from there. Without memoization, computing F(50) requires about 2^50 operations. With memoization (writing each answer on a sticky note the first time you compute it), it takes exactly 50 additions. That is the difference between exponential and linear: DP trades memory for time.Pattern Recognition Signals:
- “Count the number of ways”
- “Find minimum/maximum cost”
- “Can we achieve X?” (feasibility)
- Problem has overlapping subproblems and optimal substructure
- Constraints suggest O(n²) or O(n × m) is acceptable
The DP Mindset
When you see a DP problem, think through this framework:1
Define the State
What information do I need to describe a subproblem?
- dp[i] = answer for first i elements
- dp[i][j] = answer for subproblem defined by i and j
2
Find the Transition
How does dp[i] relate to smaller subproblems?
- dp[i] = f(dp[i-1], dp[i-2], …)
3
Identify Base Cases
What are the simplest subproblems with known answers?
4
Determine the Order
Which subproblems must be solved before others?
5
Extract the Answer
Where in the DP table is the final answer?
Pattern 1: Linear DP
State: dp[i] = answer considering first i elements.Example: Fibonacci
Example: Climbing Stairs
Problem: n stairs, can climb 1 or 2 at a time. Count ways to reach top. State: dp[i] = number of ways to reach stair i Transition: dp[i] = dp[i-1] + dp[i-2] (come from i-1 or i-2)Pattern 2: Coin Change (Unbounded Knapsack)
Problem: Given coins, find minimum coins to make amount. State: dp[i] = minimum coins to make amount i Transition: dp[i] = min(dp[i - coin] + 1) for each coin Worked example: coins = [1, 3, 4], amount = 6Counting Ways
Problem: Count ways to make amount (combinations, not permutations). Critical subtlety: The loop order determines whether you count combinations or permutations.- Coins as outer loop (below): Each coin is considered once in order. Result: combinations. and are counted once.
- Amount as outer loop: For each amount, try all coins. Result: permutations. , , and are counted separately.
Pattern 3: 0/1 Knapsack
Problem: n items with weight and value. Maximize value in capacity W. Each item used at most once. State: dp[i][w] = max value using first i items with capacity w Transition:- Don’t take item i: dp[i][w] = dp[i-1][w]
- Take item i: dp[i][w] = dp[i-1][w - weight[i]] + value[i]
Pattern 4: Longest Increasing Subsequence (LIS)
Problem: Find length of longest strictly increasing subsequence. State: dp[i] = length of LIS ending at index i Transition: dp[i] = max(dp[j] + 1) for all j < i where arr[j] < arr[i]tails[i] stores the smallest ending element of all increasing subsequences of length i+1 found so far. This array is always sorted (a longer subsequence must end with a larger value). When a new element arrives, we binary search for where it fits: if it extends the longest subsequence, we append; otherwise, we replace an existing tail with a smaller value, “making room” for future extensions.
Walked example: arr = [3, 1, 4, 1, 5, 9, 2, 6]
- 3: tails = [3]
- 1: tails = [1] (replace 3—a subsequence of length 1 ending at 1 is better)
- 4: tails = [1, 4] (extends)
- 1: tails = [1, 4] (1 is already in position)
- 5: tails = [1, 4, 5] (extends)
- 9: tails = [1, 4, 5, 9] (extends)
- 2: tails = [1, 2, 5, 9] (replace 4—better potential for future extensions)
- 6: tails = [1, 2, 5, 6] (replace 9)
- LIS length = 4
Pattern 5: Longest Common Subsequence (LCS)
Problem: Find length of longest common subsequence of two strings. State: dp[i][j] = LCS length of s1[0..i-1] and s2[0..j-1] Transition:- If s1[i-1] == s2[j-1]: dp[i][j] = dp[i-1][j-1] + 1
- Else: dp[i][j] = max(dp[i-1][j], dp[i][j-1])
Top-Down vs Bottom-Up
Rule of Thumb: Start with top-down (easier to write), convert to bottom-up if needed for optimization.
Common Mistakes
Practice Problems
Beginner (1100-1300)
Intermediate (1300-1500)
Advanced (1500-1700)
Key Takeaways
State = Subproblem
The DP state must uniquely identify a subproblem.
Transition = Recursion
The transition is how you’d write the recursive relation.
Base Case = Smallest
Base cases are the smallest subproblems with known answers.
Space Optimize
If dp[i] only depends on dp[i-1], you only need O(1) or O(n) space.
Next Up
Chapter 12: DP on Grids & Advanced
Master 2D DP, interval DP, and bitmask DP for more complex problems.