You're standing at the bottom of a staircase. You can take 1 or 2 steps at a time. How many ways can you reach the top? If you try to brute-force this, you end up computing the same sub-problems hundreds of times. Dynamic Programming says: solve each sub-problem once, remember the answer, and reuse it. That single idea is worth a thousand interviews.
What is Dynamic Programming?
Dynamic Programming (DP) is an algorithmic technique for solving problems by breaking them into overlapping sub-problems, solving each sub-problem only once, and storing the result so it can be reused. It is most applicable when a problem has two properties:
- Optimal Substructure: The optimal solution of the problem can be built from optimal solutions of its sub-problems.
- Overlapping Sub-problems: The same sub-problems are solved multiple times during a naive recursive approach.
DP is not about a specific data structure or algorithm — it's a problem-solving paradigm. The two implementations (memoization and tabulation) are different ways of applying the same idea.
Two Ways to Implement DP
Top-Down (Memoization)
Start from the original problem and recurse down. Cache each result so you never solve the same sub-problem twice. Natural to write — just add a cache to your recursion.
Bottom-Up (Tabulation)
Start from the smallest sub-problems and build up to the answer iteratively. Uses a table (usually an array). No recursion, no stack overflow risk, often faster in practice.
Both approaches give the same answer. Choose memoization when the recursion is natural and not all sub-problems are needed. Choose tabulation when you want a clean iterative solution with O(1) space optimization potential.
Warm-Up: Fibonacci Numbers
The classic entry point into DP. The naive recursion is O(2ⁿ). DP drops it to O(n).
Naive Recursion — O(2ⁿ)
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
# fib(6) recomputes fib(2) five times, fib(3) three times…
# For n = 40: ~2 billion calls!
Top-Down DP (Memoization) — O(n) time, O(n) space
from functools import lru_cache
@lru_cache(maxsize=None)
def fib_memo(n):
if n <= 1:
return n
return fib_memo(n - 1) + fib_memo(n - 2)
# Each value computed exactly once, stored in cache.
# fib_memo(40) → instant.
Bottom-Up DP (Tabulation) — O(n) time, O(n) space
def fib_tab(n):
if n <= 1:
return n
dp = [0] * (n + 1)
dp[0], dp[1] = 0, 1
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
# dp table for n = 6:
# Index: 0 1 2 3 4 5 6
# Value: 0 1 1 2 3 5 8
Space-Optimised — O(n) time, O(1) space
def fib_opt(n):
if n <= 1:
return n
a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b
# Only two variables needed — no full table required.
Always ask: do I need the entire table, or just the last few values? Many DP problems can be space-optimised from O(n) or O(n²) down to O(1) or O(n) once you notice only a sliding window of the table is ever accessed.
Problem 1: Climbing Stairs
You can climb 1 or 2 steps at a time. Given n stairs, how many distinct ways can you reach the top?
Recurrence: ways(n) = ways(n-1) + ways(n-2) — identical structure to Fibonacci.
def climb_stairs(n):
if n <= 2:
return n
a, b = 1, 2
for _ in range(3, n + 1):
a, b = b, a + b
return b
# climb_stairs(5) → 8
| n (stairs) | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| ways(n) | 1 | 2 | 3 | 5 | 8 | 13 |
Problem 2: Coin Change (Minimum Coins)
Given coin denominations and a target amount, find the minimum number of coins needed to make that amount. If it's impossible, return -1.
Example: coins = [1, 3, 4], amount = 6 → answer is 2 (3 + 3).
Recurrence: dp[i] = min(dp[i - c] + 1) for each coin c where c ≤ i.
def coin_change(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for i in range(1, amount + 1):
for coin in coins:
if coin <= i:
dp[i] = min(dp[i], dp[i - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
print(coin_change([1, 3, 4], 6)) # → 2 (3+3)
print(coin_change([2], 3)) # → -1 (impossible)
print(coin_change([1, 5, 6, 9], 11)) # → 2 (5+6)
| Amount | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| dp[i] | 0 | 1 | 2 | 1 | 1 | 2 | 2 |
| How | base | 1 | 1+1 | 3 | 4 | 4+1 | 3+3 |
Problem 3: 0/1 Knapsack
Given n items, each with a weight and value, and a bag with capacity W — find the maximum value you can carry. Each item can be taken at most once.
Recurrence:
- If item weight > remaining capacity:
dp[i][w] = dp[i-1][w] - Else:
dp[i][w] = max(dp[i-1][w], dp[i-1][w - wt[i]] + val[i])
def knapsack(weights, values, capacity):
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):
dp[i][w] = dp[i - 1][w]
if weights[i - 1] <= w:
take = dp[i - 1][w - weights[i - 1]] + values[i - 1]
dp[i][w] = max(dp[i][w], take)
return dp[n][capacity]
weights = [1, 3, 4, 5]
values = [1, 4, 5, 7]
capacity = 7
print(knapsack(weights, values, capacity)) # → 9
| Item \ Cap | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| 0 (none) | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 (w=1,v=1) | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| 2 (w=3,v=4) | 0 | 1 | 1 | 4 | 5 | 5 | 5 | 5 |
| 3 (w=4,v=5) | 0 | 1 | 1 | 4 | 5 | 6 | 6 | 9 |
| 4 (w=5,v=7) | 0 | 1 | 1 | 4 | 5 | 7 | 8 | 9 |
The naive brute-force for Knapsack checks all 2ⁿ subsets — that's 1 billion for n=30. DP solves it in O(n × W) time and O(n × W) space, which for n=30, W=1000 is just 30,000 operations.
Problem 4: Longest Common Subsequence (LCS)
Given two strings, find the length of their longest common subsequence — characters that appear in the same relative order in both strings (not necessarily contiguous).
Example: s1 = "ABCBDAB", s2 = "BDCAB" → LCS = "BCAB" (length 4).
Recurrence:
- If
s1[i] == s2[j]:dp[i][j] = dp[i-1][j-1] + 1 - Else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
def lcs(s1, s2):
m, n = len(s1), len(s2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if s1[i - 1] == s2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
return dp[m][n]
print(lcs("ABCBDAB", "BDCAB")) # → 4
print(lcs("AGGTAB", "GXTXAYB")) # → 4
print(lcs("abcde", "ace")) # → 3
| "" | A | C | B | D | |
|---|---|---|---|---|---|
| "" | 0 | 0 | 0 | 0 | 0 |
| A | 0 | 1 | 1 | 1 | 1 |
| B | 0 | 1 | 1 | 2 | 2 |
| C | 0 | 1 | 2 | 2 | 2 |
| D | 0 | 1 | 2 | 2 | 3 |
Problem 5: Longest Increasing Subsequence (LIS)
Find the length of the longest subsequence of an array such that all elements are in strictly increasing order.
Example: [10, 9, 2, 5, 3, 7, 101, 18] → LIS = [2, 3, 7, 18] → length 4.
def lis(arr):
n = len(arr)
dp = [1] * n
for i in range(1, n):
for j in range(i):
if arr[j] < arr[i]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
print(lis([10, 9, 2, 5, 3, 7, 101, 18])) # → 4
print(lis([0, 1, 0, 3, 2, 3])) # → 4
print(lis([7, 7, 7, 7])) # → 1
| Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| arr[i] | 10 | 9 | 2 | 5 | 3 | 7 | 101 | 18 |
| dp[i] | 1 | 1 | 1 | 2 | 2 | 3 | 4 | 4 |
Problem 6: Matrix Chain Multiplication
Given a chain of matrices, find the most efficient order of multiplication to minimise the total number of scalar multiplications.
Recurrence: dp[i][j] = min over all k of (dp[i][k] + dp[k+1][j] + p[i-1]*p[k]*p[j])
def matrix_chain(p):
n = len(p) - 1
dp = [[0] * (n + 1) for _ in range(n + 1)]
for length in range(2, n + 1):
for i in range(1, n - length + 2):
j = i + length - 1
dp[i][j] = float('inf')
for k in range(i, j):
cost = dp[i][k] + dp[k+1][j] + p[i-1] * p[k] * p[j]
dp[i][j] = min(dp[i][j], cost)
return dp[1][n]
p = [40, 20, 30, 10, 30]
print(matrix_chain(p)) # → 26000
Brute force tries all possible parenthesisations — that's a Catalan number, which grows exponentially. DP solves it in O(n³) time and O(n²) space.
How to Approach Any DP Problem
Does it ask for a maximum/minimum value, count of ways, or true/false feasibility? These are classic DP signals.
What does dp[i] or dp[i][j] represent? The definition of your state is the core of your solution.
Express dp[i] in terms of smaller sub-problems. Think about what choices are available at each step.
What are the smallest sub-problems with known answers? These seed the rest of the table.
Memoization is easier to write first. Convert to tabulation if you need better space or want to avoid recursion depth limits.
Check whether you only access the previous row/column/element. If so, reduce the table to a 1D array or a few variables.
Common DP Patterns to Recognise
Linear DP
State depends on previous elements in a 1D array. Examples: Fibonacci, Climbing Stairs, LIS, Max Subarray Sum.
2D DP / Grid DP
State indexed by two variables. Examples: Knapsack, LCS, Edit Distance.
Interval DP
Fill a table diagonally over all sub-intervals. Examples: Matrix Chain Multiplication, Burst Balloons.
Tree DP
DP on a tree structure, often computing values bottom-up from leaves. Examples: Max Path Sum, House Robber III.
Bitmask DP
State includes a bitmask to track which items/nodes are visited. Examples: Travelling Salesman Problem.
Probability / Count DP
Count the number of ways or compute probabilities. Examples: Coin Change (ways), Unique Paths.
Complexity Summary
| Problem | Time | Space | Space-Optimised |
|---|---|---|---|
| Fibonacci | O(n) | O(n) | O(1) |
| Climbing Stairs | O(n) | O(n) | O(1) |
| Coin Change | O(n × amount) | O(amount) | O(amount) |
| 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²) | O(n) | O(n log n) with BSearch |
| Matrix Chain | O(n³) | O(n²) | O(n²) |
Practice challenge: Given coins = [1, 2, 5] and amount = 5, find the total number of ways to make that amount. Hint: it's a variation of Coin Change — what changes in the recurrence? (Answer: dp[i] += dp[i - coin] instead of min. For this example: 4 ways.)
Dynamic Programming is one of those topics that clicks suddenly — and once it does, you start seeing DP everywhere. Start with Fibonacci, then Climbing Stairs, then Coin Change. Work through Knapsack and LCS with pen and paper, filling the table manually. That muscle memory of "define state → write recurrence → fill table" is all you need to crack any DP problem, in any interview, at any company.