Table of Contents:
What is Dynamic Programming?
Dynamic Programming (DP) is a problem-solving strategy—not just an algorithm. It applies when problems have:
- Optimal substructure: Solutions can be built from optimal solutions of subproblems
- Overlapping subproblems: The same subproblems are solved repeatedly
Core Idea: Cache expensive computations to avoid redundant work.
Key Characteristics
DP problems typically feature:
- Recursion recomputing the same subproblems (exponential time)
- Choices to make (take/skip, include/exclude)
- Definable states (e.g.,
dp[i][j]= max value using firstiitems with weightj)
Top-Down vs Bottom-Up
| Top-Down (Memoization) | Bottom-Up (Tabulation) |
|---|---|
| Recursion + cache | Iterative, fills table from base |
| Intuitive (mimics recursion) | No stack overflow risk |
Example: @lru_cache decorator | Example: nested loops |
Basic Patterns
Fibonacci Sequence
Naive recursion recomputes: fib(5) = fib(4) + fib(3), but fib(3) is computed twice.
Top-Down:
def fib(n: int) -> int:
memo = {}
def helper(x):
if x in memo:
return memo[x]
if x <= 1:
return x
memo[x] = helper(x - 1) + helper(x - 2)
return memo[x]
return helper(n)
Bottom-Up:
def fib(n: int) -> int:
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]
LeetCode: 509. Fibonacci Number
Climbing Stairs
Problem: Climb 1 or 2 steps per move. Count distinct ways to reach the top.
This is Fibonacci!
ways(n) = ways(n-1) + ways(n-2)
def climbStairs(n: int) -> int:
if n <= 2:
return n
a, b = 1, 2
for _ in range(3, n + 1):
a, b = b, a + b
return b
LeetCode: 70. Climbing Stairs
1D DP Problems
House Robber
State: dp[i] = max money robbed up to house i
Choice: Rob or skip?
- Rob:
dp[i-2] + nums[i] - Skip:
dp[i-1]
def rob(nums: List[int]) -> int:
prev2, prev1 = 0, 0
for num in nums:
prev2, prev1 = prev1, max(prev1, prev2 + num)
return prev1
LeetCode: 198. House Robber
Maximum Subarray (Kadane’s Algorithm)
State: dp[i] = max sum ending at index i
Transition: dp[i] = max(nums[i], dp[i-1] + nums[i])
def maxSubArray(nums: List[int]) -> int:
current_sum = max_sum = nums[0]
for num in nums[1:]:
current_sum = max(num, current_sum + num)
max_sum = max(max_sum, current_sum)
return max_sum
LeetCode: 53. Maximum Subarray
2D DP Problems
Unique Paths
State: dp[i][j] = paths to reach cell (i, j)
Transition: dp[i][j] = dp[i-1][j] + dp[i][j-1]
def uniquePaths(m: int, n: int) -> int:
dp = [[1] * n for _ in range(m)]
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i-1][j] + dp[i][j-1]
return dp[m-1][n-1]
LeetCode: 62. Unique Paths
Longest Common Subsequence
State: dp[i][j] = LCS length of text1[:i] and text2[:j]
Transition:
- Match:
dp[i][j] = 1 + dp[i-1][j-1] - Mismatch:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
def longestCommonSubsequence(text1: str, text2: str) -> int:
m, n = len(text1), len(text2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i-1] == text2[j-1]:
dp[i][j] = 1 + dp[i-1][j-1]
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
LeetCode: 1143. Longest Common Subsequence
Knapsack Variants
0/1 Knapsack
State: dp[w] = max value with weight limit w
Transition: For each item, either take it or skip it.
def knapsack(weights, values, W):
dp = [0] * (W + 1)
for i in range(len(values)):
for w in range(W, weights[i] - 1, -1):
dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
return dp[W]
LeetCode: 416. Partition Equal Subset Sum
Coin Change
State: dp[a] = min coins to make amount a
Transition: Try each coin, pick minimum.
def coinChange(coins: List[int], amount: int) -> int:
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for a in range(1, amount + 1):
for coin in coins:
if coin <= a:
dp[a] = min(dp[a], 1 + dp[a - coin])
return dp[amount] if dp[amount] != float('inf') else -1
LeetCode: 322. Coin Change
Advanced Concepts
DP with Bitmasking
Track subsets using bitmasks (e.g., TSP, subset problems).
Example: 943. Find the Shortest Superstring
State: dp[mask][i] = min length using strings in mask, ending with i
DP on Trees
Example: 337. House Robber III
Return (rob_root, skip_root) for each node.
def rob(root: TreeNode) -> int:
def dfs(node):
if not node:
return (0, 0)
left_rob, left_skip = dfs(node.left)
right_rob, right_skip = dfs(node.right)
rob = node.val + left_skip + right_skip
skip = max(left_rob, left_skip) + max(right_rob, right_skip)
return (rob, skip)
return max(dfs(root))
Digit DP
Count numbers ≤ N with specific digit properties.
States: (position, tight_constraint, started, digit_mask)
from functools import lru_cache
def countSpecialNumbers(n: int) -> int:
s = str(n)
@lru_cache(maxsize=None)
def dp(pos, tight, started, mask):
if pos == len(s):
return 1 if started else 0
limit = int(s[pos]) if tight else 9
res = 0
for d in range(0, limit + 1):
if started and (mask >> d) & 1:
continue # digit already used
new_mask = mask | (1 << d) if (started or d > 0) else mask
res += dp(pos + 1, tight and (d == limit), started or (d > 0), new_mask)
return res
return dp(0, True, False, 0)
How to Approach DP
- Identify DP: Look for “max/min”, “count ways”, “optimal”
- Define state: What does
dp[...]mean? - Write recurrence: How to compute from previous states?
- Base cases: When does recursion stop?
- Choose approach: Top-down (memoization) or bottom-up (tabulation)?
- Optimize space: Can you reduce dimensions?
Recommended Learning Path
Easy: 70, 509, 198, 53, 62
Medium: 64, 322, 1143, 416, 1137
Hard: 818, 943, 1012, 1420
Pro Tip: Start with brute force → add memoization → convert to tabulation → optimize space.
Master DP through consistent practice. Every pattern you learn unlocks a dozen problems.