Dynamic Programming
Dynamic programming is the most feared interview topic, and the least understood. Engineers treat it as a category of exotic problems that require flashes of insight. It is not. DP is recursion with caching. If you can write a recursive solution, you can write a DP solution. The only new idea is: do not recompute the same thing twice.
What DP Actually Is
Every DP solution follows the same three steps:
1. Define the subproblem: What smaller question do I need to answer?
2. Write the recurrence: How does the answer to a bigger problem
relate to answers to smaller problems?
3. Add memoization (or build a table): Cache results so each
subproblem is solved exactly once.
That is it. Everything else — top-down vs bottom-up, state compression, optimization tricks — is refinement of these three steps.
Recognizing DP Problems
Two properties indicate a problem can be solved with DP:
Overlapping subproblems: The same subproblem is solved multiple times. This is what makes caching worthwhile. If every subproblem is unique, you just have plain recursion.
Optimal substructure: The optimal solution to the full problem can be built from optimal solutions to its subproblems.
Problem clues that suggest DP:
- "Find the minimum/maximum..."
- "Count the number of ways..."
- "Is it possible to..."
- "Find the longest/shortest..."
- Decision-making at each step (take or skip, include or exclude)
- Problem has clear stages or positions
Top-Down (Memoization)
Start with the recursive solution, then add a cache. This is the most natural approach and the one you should default to in interviews.
Problem: Fibonacci numbers
fib(0) = 0, fib(1) = 1
fib(n) = fib(n-1) + fib(n-2)
Naive recursion (exponential — O(2^n)):
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
With memoization (O(n)):
memo = {}
def fib(n):
if n in memo:
return memo[n]
if n <= 1:
return n
memo[n] = fib(n-1) + fib(n-2)
return memo[n]
The recursive call tree for fib(5) without memoization recomputes fib(3) twice, fib(2) three times, and fib(1) five times. Memoization ensures each value is computed exactly once.
Bottom-Up (Tabulation)
Instead of starting from the top and recursing down, start from the smallest subproblems and build up.
Fibonacci bottom-up:
dp = [0] * (n + 1)
dp[0] = 0
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
Space-optimized (only need the last 2 values):
prev2, prev1 = 0, 1
for i in range(2, n + 1):
curr = prev1 + prev2
prev2 = prev1
prev1 = curr
return prev1
Top-Down vs Bottom-Up
Top-down (memoization):
+ More natural — start with the recursive solution you already have
+ Only computes subproblems that are actually needed
+ Easier to reason about correctness
- Recursion stack can overflow for large inputs
- Function call overhead
Bottom-up (tabulation):
+ No recursion stack overhead
+ Can be space-optimized more easily
+ Slightly faster in practice (no function call overhead)
- Must figure out the correct iteration order
- Computes all subproblems, even unused ones
In interviews, default to top-down. It is faster to write and easier to explain. Switch to bottom-up if the interviewer asks for optimization or if the recursion depth is a concern.
Classic DP Patterns
The Knapsack Pattern
You have items with weights and values. You have a capacity. Maximize the total value without exceeding the capacity.
0/1 Knapsack: Each item can be taken or left.
Subproblem: dp[i][w] = max value using items 0..i with capacity w
Recurrence:
dp[i][w] = max(
dp[i-1][w], # skip item i
dp[i-1][w - weight[i]] + value[i] # take item i (if it fits)
)
Base case: dp[0][w] = 0 for all w (no items)
dp[i][0] = 0 for all i (no capacity)
Time: O(n * W). Space: O(n * W), optimizable to O(W).
The knapsack pattern applies to many problems beyond literal knapsacks:
- Subset sum: Can you select elements summing to target? (values = weights)
- Partition equal subset: Can you split array into two equal-sum subsets?
- Target sum with +/-: Assign + or - to each number to reach target.
Longest Common Subsequence (LCS)
Problem: Find the length of the longest subsequence common to two strings.
"abcde" and "ace" -> LCS is "ace", length 3
Subproblem: dp[i][j] = LCS of first i chars of s1 and first j chars of s2
Recurrence:
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1 # chars match, extend LCS
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1]) # skip one or the other
Base case: dp[0][j] = 0, dp[i][0] = 0
Time: O(m * n). Space: O(m * n), optimizable to O(min(m, n)).
LCS variants include: longest common substring (consecutive match), edit distance (add insert/delete/replace costs), and diff algorithms.
Coin Change
Problem: Find the fewest coins to make a target amount.
coins = [1, 5, 10, 25], target = 30
Subproblem: dp[amount] = fewest coins to make this amount
Recurrence:
dp[amount] = min(dp[amount - coin] + 1) for each coin <= amount
Base case: dp[0] = 0 (zero coins for zero amount)
Initialize: dp[1..target] = infinity (not yet achievable)
for amount in range(1, target + 1):
for coin in coins:
if coin <= amount and dp[amount - coin] != infinity:
dp[amount] = min(dp[amount], dp[amount - coin] + 1)
return dp[target] if dp[target] != infinity else -1
Time: O(amount * num_coins). Space: O(amount).
The "number of ways" variant uses addition instead of min:
dp[amount] += dp[amount - coin]
Grid Paths
Problem: Count the number of paths from top-left to bottom-right
of an m x n grid, moving only right or down.
Subproblem: dp[i][j] = number of paths to cell (i, j)
Recurrence:
dp[i][j] = dp[i-1][j] + dp[i][j-1] # from above + from left
Base case: dp[0][j] = 1, dp[i][0] = 1 # only one way along edges
Time: O(m * n). Space: O(m * n), optimizable to O(n).
Grid path variants include: obstacles (set dp = 0 for blocked cells), minimum cost path (use min instead of addition and add cell costs), and maximum path sum.
The DP Problem-Solving Process
When you encounter a DP problem in an interview, follow this process:
Step 1: Identify the decision at each step.
"Do I take this item or skip it?"
"Do I come from the left, above, or diagonal?"
"Which coin do I use next?"
Step 2: Define the state.
What information do I need to make the decision?
This becomes the parameters of your recursive function
or the dimensions of your DP table.
Step 3: Write the recurrence.
Express dp[current_state] in terms of dp[smaller_states].
Step 4: Identify the base cases.
What are the smallest subproblems with known answers?
Step 5: Implement top-down with memoization.
Write the recursive solution with a cache.
Step 6: Optimize if asked.
Convert to bottom-up. Reduce space if only recent states
are needed.
Walk through this process out loud in the interview. Even if you do not reach a perfect solution, showing the structured approach gives strong signal.
State Definition Strategies
The hardest part of DP is defining the right state. Some guidelines:
Common state variables:
- Index/position (which element are we considering?)
- Remaining capacity/budget
- Previous choice (when the current decision depends on the last)
- Boolean flags (have we used a special ability? is a condition met?)
If your recurrence does not work, your state is probably wrong.
Common fixes:
- Add a dimension (e.g., add "number of transactions so far")
- Change what the state represents
- Redefine the subproblem entirely
Worked Example: House Robber
Problem: Rob houses along a street. Cannot rob two adjacent houses.
Maximize total value.
houses = [2, 7, 9, 3, 1]
Step 1: Decision at house i: rob it or skip it.
Step 2: State: dp[i] = max money robbing from houses 0..i.
Step 3: Recurrence:
dp[i] = max(
dp[i-1], # skip house i
dp[i-2] + houses[i] # rob house i (can't rob i-1)
)
Step 4: Base cases: dp[0] = houses[0], dp[1] = max(houses[0], houses[1])
Step 5: Implementation:
dp = [0] * len(houses)
dp[0] = houses[0]
dp[1] = max(houses[0], houses[1])
for i in range(2, len(houses)):
dp[i] = max(dp[i-1], dp[i-2] + houses[i])
return dp[-1]
Trace:
dp[0] = 2
dp[1] = max(2, 7) = 7
dp[2] = max(7, 2 + 9) = 11
dp[3] = max(11, 7 + 3) = 11
dp[4] = max(11, 11 + 1) = 12
Answer: 12 (rob houses 0, 2, 4 -> 2 + 9 + 1 = 12)
Common Pitfalls
- Jumping to DP without trying recursion first: Always start with a brute-force recursive solution. Then add memoization. Trying to build a bottom-up table from scratch is error-prone for complex problems.
- Wrong state definition: If your recurrence does not produce correct results, the most likely problem is that your state does not capture enough information. Add dimensions before trying to fix the recurrence.
- Forgetting base cases: Every recursive path must eventually reach a base case. Missing base cases cause infinite recursion or wrong results.
- Off-by-one in table dimensions: If your state is dp[i] for items 0..n-1, the table needs n entries. If your state is dp[i][j] for a grid of size m x n, the table is m x n. Double-check dimensions.
- Not recognizing DP problems: If a greedy approach gives wrong results but the problem has optimal substructure, it is probably DP. Try a small counterexample for the greedy approach to confirm.
- Premature optimization: Do not try to space-optimize or convert to bottom-up during your first pass. Get a correct top-down solution first. Optimize only when asked or when the solution works correctly.
- Treating DP as a category of tricks: DP is a technique (cache recursive results), not a collection of problems to memorize. Understanding the technique lets you solve novel problems; memorizing solutions does not.
Key Takeaways
- DP is recursion with caching. If you can write the recursive solution, adding memoization gives you DP. That is the entire concept.
- Start with top-down memoization in interviews. It is faster to write, easier to explain, and naturally correct. Convert to bottom-up only if asked.
- Recognizing DP problems comes from the two properties: overlapping subproblems and optimal substructure. Practice identifying these before jumping to solutions.
- The four classic patterns (knapsack, LCS, coin change, grid paths) cover the vast majority of interview DP problems. Understand the recurrence for each, not just the code.
- Walk through the problem-solving process out loud: identify decisions, define state, write recurrence, establish base cases, implement. This structured approach gives strong interview signal even if you do not reach the optimal solution.