Break a problem into overlapping subproblems, solve each one once, and store the result. That's the whole idea โ trade memory for time by never doing the same work twice.
IntermediateDynamic Programming (DP) is a method for solving problems by breaking them into smaller subproblems โ smaller, self-similar versions of the original question. If those subproblems overlap (meaning a naive recursive approach would solve the same subproblem multiple times), DP saves each answer the first time and reuses it later. That's the core trick: compute once, use many times.
Richard Bellman invented dynamic programming in the 1950s while working at the RAND Corporation on optimization problems for the US Air Force. The name was deliberately vague. Bellman later explained that his boss hated the word "research" and mathematical terms made him uncomfortable, so Bellman chose "dynamic programming" because "dynamic" sounded energetic and hard to criticize, and "programming" referred to planning and scheduling (not computer programming). The name stuck, even though it tells you nothing about the technique.
Bellman also developed the Bellman equation, which is the foundation of modern reinforcement learning. Every time a self-driving car or game-playing AI makes a decision, it's solving a form of dynamic programming.
Two things must be true for DP to apply:
fib(5) recursively computes fib(3) twice and fib(2) three times. If subproblems don't overlap (like in merge sort โ each subarray is unique), you don't need DP; plain divide-and-conquer suffices.These three paradigms are often confused:
There are two approaches, and they produce the same results. Pick whichever fits the problem better (or whichever makes more sense to you โ there's no wrong choice).
Write the recursive solution first, then add a cache. When a function is called with arguments you've seen before, return the stored result instead of recomputing. The word memoization (not "memorization") comes from "memo" โ you're leaving a note to yourself.
@lru_cache to your recursive functionBuild a table starting from the smallest subproblems and work your way up. Each cell depends only on cells you've already filled in โ no recursion at all.
Start top-down to get the recurrence right, then convert to bottom-up if you need space optimization or to avoid stack overflow. In interviews, top-down is faster to write. In production, bottom-up is more predictable. Both are correct.
Watch how DP fills a table cell by cell. Switch between Fibonacci (1D) and Coin Change (tabulation) to see how dependencies flow. Hover over cells to see which earlier cells they depend on.
DP doesn't have a single Big-O โ it depends on the problem. But there's a formula that works every time:
Time = (number of subproblems) ร (work per subproblem)
Space = (number of subproblems stored)
For Fibonacci: n subproblems ร O(1) work each = O(n). For LCS: mรn subproblems ร O(1) work each = O(mn). For LIS with binary search: n subproblems ร O(log n) work each = O(n log n).
| Problem | Time | Space | Optimized Space |
|---|---|---|---|
| Fibonacci | O(n) | O(n) | O(1) |
| Climbing Stairs | O(n) | O(n) | O(1) |
| Coin Change | O(n ร m) | O(n) | โ |
| 0/1 Knapsack | O(n ร W) | O(n ร W) | O(W) |
| LCS | O(m ร n) | O(m ร n) | O(min(m,n)) |
| LIS | O(n log n) | O(n) | โ |
| Edit Distance | O(m ร n) | O(m ร n) | O(min(m,n)) |
| Matrix Chain Mult. | O(nยณ) | O(nยฒ) | โ |
# Top-down: recursive with memoization
def fib_memo(n, cache={}):
if n <= 1:
return n
if n in cache:
return cache[n]
# Store result so we never recompute fib(n)
cache[n] = fib_memo(n - 1) + fib_memo(n - 2)
return cache[n]
# Bottom-up: iterative tabulation
def fib_tab(n):
if n <= 1:
return n
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
# Each cell only depends on the two before it
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
# Space-optimized: we only need 2 variables
# Since dp[i] depends only on dp[i-1] and dp[i-2],
# there's no reason to keep the entire array
def fib_opt(n):
if n <= 1:
return n
prev2, prev1 = 0, 1
for _ in range(2, n + 1):
prev2, prev1 = prev1, prev2 + prev1
return prev1
def coin_change(coins, amount):
"""Find the fewest coins needed to make 'amount'.
dp[i] = minimum coins to make amount i.
Transition: dp[i] = min(dp[i - coin] + 1) for each coin.
This is unbounded knapsack โ each coin can be used unlimited times."""
# Initialize to infinity (no solution yet)
dp = [float('inf')] * (amount + 1)
dp[0] = 0 # Base case: 0 coins to make amount 0
for i in range(1, amount + 1):
for coin in coins:
if coin <= i and dp[i - coin] != float('inf'):
dp[i] = min(dp[i], dp[i - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
def knapsack(weights, values, capacity):
"""Maximize total value without exceeding capacity.
Each item can be taken at most once (0/1 = take or leave).
dp[i][w] = max value using first i items with capacity w."""
n = len(weights)
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for w in range(capacity + 1):
# Option 1: skip item i (don't take it)
dp[i][w] = dp[i - 1][w]
# Option 2: take item i (if it fits)
if weights[i - 1] <= w:
take = values[i - 1] + dp[i - 1][w - weights[i - 1]]
dp[i][w] = max(dp[i][w], take)
return dp[n][capacity]
# Space-optimized: 1D array, iterate capacity RIGHT to LEFT
# (to avoid using the same item twice in one row)
def knapsack_optimized(weights, values, capacity):
dp = [0] * (capacity + 1)
for i in range(len(weights)):
# MUST go right to left โ otherwise dp[w - weights[i]]
# would use the current item's row, not the previous one
for w in range(capacity, weights[i] - 1, -1):
dp[w] = max(dp[w], values[i] + dp[w - weights[i]])
return dp[capacity]
def lcs(text1, text2):
"""Find the length of the longest common subsequence.
A subsequence is a sequence derived by deleting some (or no)
elements without changing the order of remaining elements.
Example: lcs("abcde", "ace") = 3 (the subsequence "ace")."""
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]:
# Characters match โ extend the LCS by 1
dp[i][j] = dp[i - 1][j - 1] + 1
else:
# No match โ take the better of skipping either character
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
return dp[m][n]
def edit_distance(word1, word2):
"""Minimum operations (insert, delete, replace) to transform
word1 into word2. Powers autocorrect, spell checkers, and
DNA sequence alignment.
dp[i][j] = edit distance between word1[0..i-1] and word2[0..j-1]."""
m, n = len(word1), len(word2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
# Base cases: transforming to/from empty string
for i in range(m + 1):
dp[i][0] = i # Delete all characters from word1
for j in range(n + 1):
dp[0][j] = j # Insert all characters of word2
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i - 1] == word2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] # Characters match โ no cost
else:
dp[i][j] = 1 + min(
dp[i - 1][j], # Delete from word1
dp[i][j - 1], # Insert into word1
dp[i - 1][j - 1] # Replace in word1
)
return dp[m][n]
import bisect
def lis(nums):
"""Find the length of the longest strictly increasing subsequence.
The O(nยฒ) DP approach checks all previous elements for each position.
This O(n log n) approach uses patience sorting (like sorting cards).
tails[i] = smallest possible ending element of all increasing
subsequences of length i+1 found so far."""
tails = []
for num in nums:
pos = bisect.bisect_left(tails, num)
if pos == len(tails):
# num is bigger than everything โ extend longest subsequence
tails.append(num)
else:
# Replace to keep tails as small as possible
# This doesn't change the LIS length but keeps options open
# for future elements
tails[pos] = num
return len(tails)
When you type "recieve" and your phone suggests "receive," it's using edit distance. The algorithm computes the minimum number of character insertions, deletions, and replacements to transform one word into another, then suggests dictionary words with the smallest edit distance.
def autocorrect(typed_word, dictionary, max_distance=2):
"""Suggest corrections for a misspelled word.
Returns dictionary words within max_distance edits,
sorted by edit distance (closest first).
This is a simplified version of what your phone keyboard does.
Real autocorrect also considers key proximity, word frequency,
and context (what you typed before)."""
def edit_dist(w1, w2):
m, n = len(w1), len(w2)
# Quick length check โ if lengths differ by more than
# max_distance, we can skip the full DP computation
if abs(m - n) > max_distance:
return max_distance + 1
dp = list(range(n + 1)) # Space-optimized: single row
for i in range(1, m + 1):
prev = dp[0]
dp[0] = i
for j in range(1, n + 1):
temp = dp[j]
if w1[i-1] == w2[j-1]:
dp[j] = prev
else:
dp[j] = 1 + min(prev, dp[j], dp[j-1])
prev = temp
return dp[n]
# Score every dictionary word
suggestions = []
for word in dictionary:
dist = edit_dist(typed_word.lower(), word.lower())
if dist <= max_distance:
suggestions.append((dist, word))
suggestions.sort() # Sort by distance, then alphabetically
return [word for dist, word in suggestions]
# Example
dictionary = ["receive", "recipe", "receipt", "recite", "deceive",
"relieve", "believe", "retrieve", "achieve"]
print(autocorrect("recieve", dictionary))
# ['receive'] โ distance 1 (swap i and e)
print(autocorrect("receve", dictionary))
# ['receive', 'recite'] โ both distance 2
Edit distance is one of the most practically useful DP algorithms. Beyond autocorrect, it powers DNA sequence alignment (bioinformatics uses a weighted variant called the Needleman-Wunsch algorithm), plagiarism detection, and the Unix diff tool (which uses a variant of LCS).
Sorted by difficulty. Start with the first two to build intuition, then tackle the harder ones.
| Problem | Difficulty | Pattern | Link |
|---|---|---|---|
| Climbing Stairs | Easy | 1D DP (Fibonacci variant) | LC #70 |
| House Robber | Medium | 1D DP (take/skip) | LC #198 |
| Coin Change | Medium | Unbounded knapsack | LC #322 |
| Longest Increasing Subsequence | Medium | 1D DP / binary search | LC #300 |
| Longest Common Subsequence | Medium | 2D DP (two strings) | LC #1143 |
| Partition Equal Subset Sum | Medium | Knapsack / subset sum | LC #416 |
| Edit Distance | Medium | 2D DP (two strings) | LC #72 |
| Burst Balloons | Hard | Interval DP | LC #312 |