Dynamic Programming
DP has a reputation for being hard, but it's really just "smart recursion." Instead of solving the same subproblem over and over, you solve it once and remember the answer. That's it.
The Core Idea
Break your problem into smaller subproblems. Solve those. Combine the answers. The magic is that subproblems overlap - the same smaller problem appears many times, so remembering answers saves massive time.
DP works when: (1) the problem has optimal substructure (optimal solution uses optimal solutions to subproblems), and (2) overlapping subproblems (same subproblem computed multiple times).
Two Approaches
1. Top-Down (Memoization)
Start from the main problem, 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]
2. Bottom-Up (Tabulation)
Start from the smallest subproblems, build up:
def fib(n):
if n <= 1:
return 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]
The DP Recipe
- Define the subproblem - What does
dp[i]represent? - Find the recurrence - How does
dp[i]relate to smaller subproblems? - Identify base cases - What are the smallest subproblems?
- Decide the order - Which subproblems to solve first?
- Extract the answer - Where is the final answer?
Classic Example: Climbing Stairs
You can climb 1 or 2 steps. How many ways to reach step n?
def climbStairs(n):
if n <= 2:
return n
# dp[i] = ways to reach step i
prev1, prev2 = 1, 2
for i in range(3, n + 1):
curr = prev1 + prev2 # From i-1 or i-2
prev1, prev2 = prev2, curr
return prev2
Common DP Problem Types
- Counting - How many ways to do X?
- Optimization - Minimum cost, maximum profit
- Yes/No - Is it possible to achieve X?
- Longest/Shortest - LCS, LIS, shortest path
If DP feels hard, start with a plain recursive solution. Get it working. Then add memoization. You've just done DP.