Dynamic Programming (DP) solves complex problems by breaking them into overlapping subproblems and storing the result of each so it's never recomputed. It is essentially recursion + memoization, or equivalently, smart iteration with a table.
Two approaches:
- Top-down (memoization) — write the recursive solution, cache results in a dict/array.
- Bottom-up (tabulation) — fill a table iteratively from the smallest subproblems up.
The key insight: dp[i] = answer for input of size i, built from smaller dp values.
- You see overlapping subproblems (same sub-input computed multiple times in brute force)
- The problem has optimal substructure (optimal solution contains optimal solutions to subproblems)
- Phrases like: "minimum/maximum", "number of ways", "is it possible to", "longest/shortest"
- You have already written a recursive solution that is too slow
# Top-down
from functools import lru_cache
@lru_cache(maxsize=None)
def dp(i):
if base_case(i):
return base_value
return combine(dp(i-1), dp(i-2), ...) # recurrence relation
# Bottom-up
dp = [0] * (n + 1)
dp[0] = base_value_0
dp[1] = base_value_1
for i in range(2, n + 1):
dp[i] = combine(dp[i-1], dp[i-2], ...)Given n stairs, you can climb 1 or 2 steps at a time. How many distinct ways to reach the top?
def climbStairs(n: int) -> int:
if n <= 2:
return n
prev2, prev1 = 1, 2
for _ in range(3, n + 1):
curr = prev1 + prev2
prev2 = prev1
prev1 = curr
return prev1- To reach stair
i, you either came fromi-1(one step) ori-2(two steps). - Recurrence:
dp[i] = dp[i-1] + dp[i-2]— this is Fibonacci! - We optimize space by only keeping the last two values.
- Time: O(n) | Space: O(1)
Given houses with money, you cannot rob two adjacent houses. Find the maximum you can rob.
def rob(nums: list[int]) -> int:
if not nums:
return 0
if len(nums) == 1:
return nums[0]
prev2, prev1 = 0, 0
for num in nums:
curr = max(prev1, prev2 + num)
prev2 = prev1
prev1 = curr
return prev1- At each house: either skip it (take
prev1) or rob it (takeprev2 + num). prev2= best result two houses ago,prev1= best result from previous house.- We don't need the full array — just the last two states.
- Time: O(n) | Space: O(1)
Given two strings, find the length of their longest common subsequence.
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] = dp[i-1][j-1] + 1 # characters match: extend LCS
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1]) # skip one character
return dp[m][n]dp[i][j]= length of LCS oftext1[:i]andtext2[:j].- If the current characters match, we extend the LCS from the diagonal.
- If not, we take the best from skipping one character in either string.
- Time: O(m·n) | Space: O(m·n) — can be reduced to O(n)
Given coin denominations and an amount, find the minimum number of coins to make that amount.
def coinChange(coins: list[int], amount: int) -> int:
dp = [float('inf')] * (amount + 1)
dp[0] = 0 # 0 coins needed for amount 0
for a in range(1, amount + 1):
for coin in coins:
if coin <= a:
dp[a] = min(dp[a], dp[a - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1dp[a]= minimum coins to make amounta.- For each amount, try every coin: if we use this coin, we need
dp[a - coin] + 1coins total. - Initialize with
infto mean "impossible", exceptdp[0] = 0. - Time: O(amount · len(coins)) | Space: O(amount)
Given an array, find the length of the longest strictly increasing subsequence.
import bisect
def lengthOfLIS(nums: list[int]) -> int:
tails = [] # tails[i] = smallest tail element of all increasing subsequences of length i+1
for num in nums:
pos = bisect.bisect_left(tails, num)
if pos == len(tails):
tails.append(num)
else:
tails[pos] = num # replace to keep tails as small as possible
return len(tails)tailsis not the actual LIS — it's a "patience sort" structure that tracks the smallest possible tail for each length.bisect_leftfinds wherenumshould go. If it extends the longest subsequence, append. Otherwise, replace the existing tail to maintain the best possible values for future elements.len(tails)= length of the LIS.- Time: O(n log n) | Space: O(n)
| Pattern | Recurrence | Example |
|---|---|---|
| Linear (1D) | dp[i] = f(dp[i-1], dp[i-2]) |
Climbing Stairs, House Robber |
| 2D grid/string | dp[i][j] = f(dp[i-1][j], dp[i][j-1]) |
LCS, Edit Distance |
| Knapsack | dp[i] = max/min(dp[i], dp[i-w] + v) |
Coin Change, Partition Equal Subset |
| Interval DP | dp[i][j] = f(dp[i][k], dp[k+1][j]) |
Burst Balloons, Matrix Chain |
| State machine | dp[state] = transition |
Buy/Sell Stock with Cooldown |
- Identify the state: what info is needed to describe a subproblem uniquely?
- Write the recurrence: how does
dp[i]relate to smallerdpvalues? - Define base cases: what is the answer for the smallest input?
- Determine the order: ensure smaller subproblems are solved before larger ones.
- Optimize space: if
dp[i]only depends ondp[i-1]anddp[i-2], you only need two variables.