Dynamic Programming (1D)
Solve optimization and counting problems by breaking them into overlapping subproblems, storing results to avoid recomputation. Master the 3-step recipe: define state, write recurrence, set base cases. Covers Fibonacci-style, take/skip, knapsack, subsequence, and subarray archetypes.
Learn & Reference
Understanding the Pattern
Dynamic Programming (1D) Pattern
Dynamic Programming (DP) is an optimization technique for problems with two key properties:
- Overlapping subproblems — the same smaller problem is solved multiple times
- Optimal substructure — the optimal solution builds from optimal solutions to subproblems
Fibonacci without DP: With DP (bottom-up):
fib(5) dp[0] = 0
├─ fib(4) dp[1] = 1
│ ├─ fib(3) dp[2] = dp[0] + dp[1] = 1
│ │ ├─ fib(2) ← computed 3x! dp[3] = dp[1] + dp[2] = 2
│ │ └─ fib(1) dp[4] = dp[2] + dp[3] = 3
│ └─ fib(2) ← again dp[5] = dp[3] + dp[4] = 5
└─ fib(3) ← again
... Each subproblem solved exactly once!
The 3-Step Recipe
Every 1D DP problem follows these steps:
Step 1: Define State
What does dp[i] represent? This is the hardest and most important step.
- "The number of ways to reach step i"
- "The maximum amount we can rob from houses 0..i"
- "The minimum coins needed to make amount i"
Step 2: Write Recurrence
How does dp[i] relate to previous states?
- Climbing stairs:
dp[i] = dp[i-1] + dp[i-2] - House robber:
dp[i] = max(dp[i-1], dp[i-2] + nums[i]) - Coin change:
dp[i] = min(dp[i-c] + 1)for each coinc
Step 3: Set Base Cases
What are the trivially known values?
- Climbing stairs:
dp[0] = 1, dp[1] = 1 - House robber:
dp[0] = nums[0], dp[1] = max(nums[0], nums[1]) - Coin change:
dp[0] = 0(zero coins for amount zero)
Top-Down vs Bottom-Up
Top-down (memoization): Start from the target, recurse down, cache results.
def fib(n, memo={}):
if n in memo: return memo[n]
if n <= 1: return n
memo[n] = fib(n-1) + fib(n-2)
return memo[n]
Bottom-up (tabulation): Fill a table from base cases up to the target.
def fib(n):
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
Bottom-up is generally preferred in interviews: no recursion overhead, easier to optimize space, and the iteration direction is explicit.
Five 1D DP Archetypes
1. Fibonacci-Style
State depends on a fixed number of previous states. Classic: climbing stairs, tiling problems.
dp[i] = dp[i-1] + dp[i-2]
2. Take/Skip (Decision at Each Element)
For each element, choose to include it or skip it. Classic: house robber, paint houses.
dp[i] = max(dp[i-1], dp[i-2] + value[i])
3. Knapsack (Optimizing Over Choices)
For each capacity/target value, try all items. Classic: coin change, subset sum.
dp[j] = min(dp[j], dp[j - coin] + 1)
4. Subsequence
dp[i] depends on all dp[j] where j < i and some condition holds. Classic: LIS, longest divisible subset.
dp[i] = max(dp[j] + 1) for all j < i where nums[j] < nums[i]
5. Subarray Tracking (Kadane-style)
Track a running property that can reset. Classic: max subarray sum, max product subarray.
curMax = max(nums[i], curMax + nums[i])
Space Optimization
When dp[i] only depends on the last 1-2 values, replace the array with variables:
# Before: O(n) space
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i-1] + dp[i-2]
# After: O(1) space
prev2, prev1 = 0, 1
for i in range(2, n + 1):
curr = prev1 + prev2
prev2, prev1 = prev1, curr
Common Mistakes
- Wrong base case — Off-by-one errors in
dp[0]vsdp[1]are the #1 DP bug. Always verify your base cases against the problem's smallest inputs (n=0, n=1). - Wrong iteration direction — Bottom-up must iterate so that dependencies are already computed. For knapsack-style problems, iterating in the wrong direction can use values from the current round instead of the previous round.
- Missing transitions — Forgetting a case in the recurrence (e.g., only considering "take" but not "skip"). Write out all choices explicitly before coding.
- Returning dp[n] vs dp[n-1] — If dp is 0-indexed and represents "answer for first i items", the answer is
dp[n], notdp[n-1]. Match your indexing to your state definition. - Space optimization bugs — When reducing to O(1) space, update variables in the wrong order and overwrite values you still need. Always update in the correct sequence and use temporary variables.
When to Use
Template
1# --- Bottom-Up DP Template ---2def solve(n, ...):3# Step 1: Define state — what does dp[i] represent?4dp = [0] * (n + 1)56# Step 3: Base cases7dp[0] = base_value8# dp[1] = ...910# Step 2: Fill using recurrence11for i in range(1, n + 1):12# dp[i] = f(dp[i-1], dp[i-2], ...)13pass1415return dp[n]1617# --- Space-Optimized Template ---18def solve_optimized(n, ...):19# When dp[i] only depends on last 1-2 values20prev2 = base021prev1 = base122for i in range(2, n + 1):23curr = f(prev1, prev2)24prev2, prev1 = prev1, curr25return prev1
Syntax Reference
1# --- DP Array Initialization ---2dp = [0] * (n + 1) # n+1 slots, all zero3dp = [float('inf')] * (n + 1) # For minimization problems4dp = [False] * (n + 1) # For boolean DP (can reach?)5dp[0] = base_value # Set base case67# --- Bottom-Up Iteration ---8for i in range(1, n + 1): # Fill left to right9dp[i] = recurrence(dp[i-1], dp[i-2], ...)1011# --- Space-Optimized (2 variables) ---12prev2, prev1 = base0, base113for i in range(2, n + 1):14curr = f(prev1, prev2)15prev2, prev1 = prev1, curr16# Answer: prev11718# --- Top-Down with Memoization ---19from functools import lru_cache2021@lru_cache(maxsize=None)22def solve(i):23if i <= base: return base_value24return recurrence(solve(i-1), solve(i-2), ...)
Common Recipes
1# Recipe 1: Climbing Stairs (Fibonacci-style)2def climb_stairs(n):3if n <= 2:4return n5prev2, prev1 = 1, 26for i in range(3, n + 1):7curr = prev1 + prev28prev2, prev1 = prev1, curr9return prev11011# Recipe 2: House Robber (Take/Skip)12def rob(nums):13if len(nums) == 1:14return nums[0]15prev2, prev1 = nums[0], max(nums[0], nums[1])16for i in range(2, len(nums)):17curr = max(prev1, prev2 + nums[i])18prev2, prev1 = prev1, curr19return prev12021# Recipe 3: Coin Change (Unbounded Knapsack)22def coin_change(coins, amount):23dp = [float('inf')] * (amount + 1)24dp[0] = 025for coin in coins:26for j in range(coin, amount + 1):27dp[j] = min(dp[j], dp[j - coin] + 1)28return dp[amount] if dp[amount] != float('inf') else -12930# Recipe 4: Longest Increasing Subsequence31def length_of_lis(nums):32n = len(nums)33dp = [1] * n34for i in range(1, n):35for j in range(i):36if nums[j] < nums[i]:37dp[i] = max(dp[i], dp[j] + 1)38return max(dp)
Complexity
| Archetype | Time | Space | Example |
|---|---|---|---|
| Fibonacci-style | O(n) | O(1)* | Climbing Stairs, Tribonacci |
| Take/Skip | O(n) | O(1)* | House Robber, Paint Houses |
| Knapsack (unbounded) | O(n · k) | O(n) | Coin Change (k = coins) |
| Subsequence (O(n²)) | O(n²) | O(n) | LIS, Longest Divisible Subset |
| Subarray tracking | O(n) | O(1) | Max Subarray, Max Product Subarray |
Watch Out
- ✗Wrong base case — Off-by-one errors in `dp[0]` vs `dp[1]` are the #1 DP bug. Always verify your base cases against the problem's smallest inputs (n=0, n=1).
- ✗Wrong iteration direction — Bottom-up must iterate so that dependencies are already computed. For knapsack-style problems, iterating in the wrong direction can use values from the current round instead of the previous round.
- ✗Missing transitions — Forgetting a case in the recurrence (e.g., only considering "take" but not "skip"). Write out all choices explicitly before coding.
- ✗Returning dp[n] vs dp[n-1] — If dp is 0-indexed and represents "answer for first i items", the answer is `dp[n]`, not `dp[n-1]`. Match your indexing to your state definition.
- ✗Space optimization bugs — When reducing to O(1) space, update variables in the wrong order and overwrite values you still need. Always update in the correct sequence and use temporary variables.