Break hard problems into simple subproblems. DP is just smart recursion -- and it is way less scary than it sounds.
Dynamic Programming (DP) is a technique for solving problems by breaking them into overlapping subproblems and storing the results so you never solve the same subproblem twice. That is literally all it is.
For a problem to be solvable with DP, it needs two key properties:
DP is NOT as scary as people make it sound. If you can write a recursive solution, you can convert it to DP. The hard part is defining what your subproblem is -- the actual coding is usually straightforward. Think of DP as "remembering your work so you don't redo it."
Consider computing Fibonacci(5). A naive recursive approach recalculates the same values over and over:
/*
fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \ / \ / \
fib(2) fib(1) ... ...
/ \
fib(1) fib(0)
fib(3) is computed 2 times
fib(2) is computed 3 times
This EXPLODES exponentially as n grows
*/
With DP, you compute each value once, store it, and look it up next time. That takes you from O(2^n) to O(n).
There are two ways to implement DP. Both give the same results -- they just approach the problem from different directions.
| Aspect | Top-Down (Memo) | Bottom-Up (Tab) |
|---|---|---|
| Direction | Big problem down to base cases | Base cases up to big problem |
| Implementation | Recursion + cache | Iterative + table |
| Ease of thought | Often easier to derive | Requires knowing computation order |
| Stack overflow risk | Yes, for large inputs | No |
| Space optimization | Harder | Easier (can drop old rows/values) |
| Computes unused states? | No (only what's needed) | Sometimes yes |
In interviews, start with top-down -- it is the natural extension of your recursive brute force. Once that works, mention you could convert to bottom-up for better space/performance. We will show both approaches for every problem below.
Use this 5-step framework for every DP problem. Seriously -- every single one.
Problem: "How many ways to reach step n if you can take 1 or 2 steps?"
Every DP tutorial starts with Fibonacci for a reason: it perfectly illustrates why DP exists. The Fibonacci sequence is 0, 1, 1, 2, 3, 5, 8, 13, 21, ... where each number is the sum of the two before it.
This is what NOT to do. Each call branches into two more, creating exponential work.
Python
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
# fib(40) takes SECONDS. fib(50) may never finish.
JavaScript
function fib(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
Same recursion, but we remember every result. Each fib(k) is computed exactly once.
Python
def fib(n, memo={}):
if n <= 1:
return n
if n in memo:
return memo[n]
memo[n] = fib(n - 1, memo) + fib(n - 2, memo)
return memo[n]
# fib(1000) returns instantly
JavaScript
function fib(n, memo = {}) {
if (n <= 1) return n;
if (memo[n] !== undefined) return memo[n];
memo[n] = fib(n - 1, memo) + fib(n - 2, memo);
return memo[n];
}
Build the table iteratively from the base cases up. No recursion at all.
Python
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]
JavaScript
function fib(n) {
if (n <= 1) return n;
const dp = new Array(n + 1).fill(0);
dp[1] = 1;
for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
Computing fib(6) bottom-up:
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| dp[i] | 0 | 1 | 1 | 2 | 3 | 5 | 8 |
Each cell is just the sum of the two cells to its left. That is it.
We only ever look at the last two values, so why store the whole array?
Python
def fib(n):
if n <= 1:
return n
prev2, prev1 = 0, 1
for _ in range(2, n + 1):
curr = prev1 + prev2
prev2 = prev1
prev1 = curr
return prev1
JavaScript
function fib(n) {
if (n <= 1) return n;
let prev2 = 0, prev1 = 1;
for (let i = 2; i <= n; i++) {
const curr = prev1 + prev2;
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
| Approach | Time | Space |
|---|---|---|
| Naive recursion | O(2^n) | O(n) |
| Top-down memo | O(n) | O(n) |
| Bottom-up table | O(n) | O(n) |
| Space-optimized | O(n) | O(1) |
Problem: You are climbing a staircase with n steps. Each time you can climb 1 or 2 steps. How many distinct ways can you reach the top?
Wait -- this is just Fibonacci! To reach step i, you either came from step i-1 (one step) or step i-2 (two steps). The total ways is the sum of both.
| Step | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| Ways | 1 | 1 | 2 | 3 | 5 | 8 |
There are 8 distinct ways to climb 5 stairs.
Python
def climbStairs(n, memo={}):
if n <= 1:
return 1
if n in memo:
return memo[n]
memo[n] = climbStairs(n - 1, memo) + climbStairs(n - 2, memo)
return memo[n]
JavaScript
function climbStairs(n, memo = {}) {
if (n <= 1) return 1;
if (memo[n] !== undefined) return memo[n];
memo[n] = climbStairs(n - 1, memo) + climbStairs(n - 2, memo);
return memo[n];
}
Python
def climbStairs(n):
if n <= 1:
return 1
prev2, prev1 = 1, 1
for _ in range(2, n + 1):
curr = prev1 + prev2
prev2 = prev1
prev1 = curr
return prev1
JavaScript
function climbStairs(n) {
if (n <= 1) return 1;
let prev2 = 1, prev1 = 1;
for (let i = 2; i <= n; i++) {
const curr = prev1 + prev2;
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
Time: O(n) | Space: O(1)
Problem: You are a robber planning to rob houses along a street. Each house has a certain amount of money. You cannot rob two adjacent houses (alarm triggers). Find the maximum amount you can rob.
At each house, you have two choices: skip it (take dp[i-1]) or rob it (take dp[i-2] + this house's money). Pick whichever is more.
| i | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| nums[i] | 2 | 7 | 9 | 3 | 1 |
| dp[i] | 2 | 7 | 11 | 11 | 12 |
dp[2] = max(7, 2+9) = 11. dp[3] = max(11, 7+3) = 11. dp[4] = max(11, 11+1) = 12. Rob houses 0, 2, 4 for $12.
Python
def rob(nums):
memo = {}
def dp(i):
if i < 0:
return 0
if i in memo:
return memo[i]
memo[i] = max(dp(i - 1), dp(i - 2) + nums[i])
return memo[i]
return dp(len(nums) - 1)
JavaScript
function rob(nums) {
const memo = {};
function dp(i) {
if (i < 0) return 0;
if (memo[i] !== undefined) return memo[i];
memo[i] = Math.max(dp(i - 1), dp(i - 2) + nums[i]);
return memo[i];
}
return dp(nums.length - 1);
}
Python
def rob(nums):
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
JavaScript
function rob(nums) {
let prev2 = 0, prev1 = 0;
for (const num of nums) {
const curr = Math.max(prev1, prev2 + num);
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
Time: O(n) | Space: O(1)
Problem: Find the contiguous subarray with the largest sum.
At each element, you decide: start a new subarray here, or extend the previous one. If the running sum is negative, start fresh.
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| nums[i] | -2 | 1 | -3 | 4 | -1 | 2 | 1 | -5 | 4 |
| dp[i] | -2 | 1 | -2 | 4 | 3 | 5 | 6 | 1 | 5 |
Maximum is 6 (subarray [4, -1, 2, 1]).
Python
def maxSubArray(nums):
max_sum = curr_sum = nums[0]
for num in nums[1:]:
curr_sum = max(num, curr_sum + num)
max_sum = max(max_sum, curr_sum)
return max_sum
JavaScript
function maxSubArray(nums) {
let maxSum = nums[0], currSum = nums[0];
for (let i = 1; i < nums.length; i++) {
currSum = Math.max(nums[i], currSum + nums[i]);
maxSum = Math.max(maxSum, currSum);
}
return maxSum;
}
Time: O(n) | Space: O(1)
Problem: Given coin denominations and a target amount, find the minimum number of coins needed. If impossible, return -1.
| Amount | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| dp[i] | 0 | 1 | 2 | 1 | 1 | 2 | 2 |
dp[6] = 2 (use coins 3 + 3). Note: greedy (4+1+1 = 3 coins) fails here!
Python
def coinChange(coins, amount):
memo = {}
def dp(remaining):
if remaining == 0:
return 0
if remaining < 0:
return float('inf')
if remaining in memo:
return memo[remaining]
memo[remaining] = min(dp(remaining - c) + 1 for c in coins)
return memo[remaining]
result = dp(amount)
return result if result != float('inf') else -1
JavaScript
function coinChange(coins, amount) {
const memo = {};
function dp(remaining) {
if (remaining === 0) return 0;
if (remaining < 0) return Infinity;
if (memo[remaining] !== undefined) return memo[remaining];
let min = Infinity;
for (const coin of coins) {
min = Math.min(min, dp(remaining - coin) + 1);
}
memo[remaining] = min;
return min;
}
const result = dp(amount);
return result === Infinity ? -1 : result;
}
Python
def coinChange(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
JavaScript
function coinChange(coins, amount) {
const dp = new Array(amount + 1).fill(Infinity);
dp[0] = 0;
for (let i = 1; i <= amount; i++) {
for (const coin of coins) {
if (coin <= i) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] === Infinity ? -1 : dp[amount];
}
Time: O(amount * coins) | Space: O(amount)
For coins [1, 3, 4] and amount 6, greedy picks 4+1+1 = 3 coins. DP correctly finds 3+3 = 2 coins. This is exactly why DP exists -- it explores all possibilities without brute-forcing every combination.
Problem: Find the length of the longest strictly increasing subsequence (not necessarily contiguous).
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| nums[i] | 10 | 9 | 2 | 5 | 3 | 7 | 101 | 18 |
| dp[i] | 1 | 1 | 1 | 2 | 2 | 3 | 4 | 4 |
LIS length = 4. One possible LIS: [2, 3, 7, 101] or [2, 5, 7, 101] or [2, 5, 7, 18].
Python
def lengthOfLIS(nums):
n = len(nums)
dp = [1] * n
for i in range(1, n):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
JavaScript
function lengthOfLIS(nums) {
const n = nums.length;
const dp = new Array(n).fill(1);
for (let i = 1; i < n; i++) {
for (let j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
return Math.max(...dp);
}
Time: O(n^2) | Space: O(n)
There is an O(n log n) solution using binary search + patience sorting. Know it exists and can explain the idea, but the O(n^2) DP solution is perfectly acceptable in most interviews. The O(n log n) version is rarely expected unless specifically asked.
When your state depends on two variables (two indices, a position in a grid, etc.), you need a 2D DP table.
Problem: Given an m x n grid, starting at top-left, you can only move right or down. How many unique paths to the bottom-right?
| col 0 | col 1 | col 2 | col 3 | |
|---|---|---|---|---|
| row 0 | 1 | 1 | 1 | 1 |
| row 1 | 1 | 2 | 3 | 4 |
| row 2 | 1 | 3 | 6 | 10 |
Answer: 10 unique paths. Each cell = sum of cell above + cell to the left.
Python
def uniquePaths(m, n):
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]
JavaScript
function uniquePaths(m, n) {
const dp = Array.from({ length: m }, () =>
new Array(n).fill(1)
);
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
Time: O(m * n) | Space: O(m * n), can be optimized to O(n) using a single row
Problem: Given two strings, find the length of their longest common subsequence.
| "" | a | c | e | |
|---|---|---|---|---|
| "" | 0 | 0 | 0 | 0 |
| a | 0 | 1 | 1 | 1 |
| b | 0 | 1 | 1 | 1 |
| c | 0 | 1 | 2 | 2 |
| d | 0 | 1 | 2 | 2 |
| e | 0 | 1 | 2 | 3 |
LCS length = 3. The LCS is "ace".
Python
def longestCommonSubsequence(text1, text2):
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
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
return dp[m][n]
JavaScript
function longestCommonSubsequence(text1, text2) {
const m = text1.length, n = text2.length;
const dp = Array.from({ length: m + 1 }, () =>
new Array(n + 1).fill(0)
);
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (text1[i - 1] === text2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
Time: O(m * n) | Space: O(m * n)
Problem: Given items with weights and values, and a knapsack with weight capacity W, maximize the total value you can carry. Each item can only be used once.
| item\cap | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| 0 items | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| item 1 (1,1) | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| item 2 (3,4) | 0 | 1 | 1 | 4 | 5 | 5 | 5 | 5 |
| item 3 (4,5) | 0 | 1 | 1 | 4 | 5 | 6 | 6 | 9 |
| item 4 (5,7) | 0 | 1 | 1 | 4 | 5 | 7 | 8 | 9 |
Max value = 9 (items 2 and 3: weight 3+4=7, value 4+5=9).
Python
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):
if weights[i - 1] <= w:
dp[i][w] = max(
dp[i - 1][w],
dp[i - 1][w - weights[i - 1]] + values[i - 1]
)
else:
dp[i][w] = dp[i - 1][w]
return dp[n][capacity]
JavaScript
function knapsack(weights, values, capacity) {
const n = weights.length;
const dp = Array.from({ length: n + 1 }, () =>
new Array(capacity + 1).fill(0)
);
for (let i = 1; i <= n; i++) {
for (let w = 0; w <= capacity; w++) {
if (weights[i - 1] <= w) {
dp[i][w] = Math.max(
dp[i - 1][w],
dp[i - 1][w - weights[i - 1]] + values[i - 1]
);
} else {
dp[i][w] = dp[i - 1][w];
}
}
}
return dp[n][capacity];
}
Time: O(n * W) | Space: O(n * W), can be optimized to O(W) using a 1D array
Most DP problems fall into one of these categories. Recognizing the category is half the battle.
| Category | Example Problems | Key Idea |
|---|---|---|
| Linear | Fibonacci, Climbing Stairs, House Robber | dp[i] depends on previous elements |
| Interval | Palindromic Substrings, Matrix Chain Mult. | dp[i][j] for range i to j |
| Grid | Unique Paths, Min Path Sum | dp[i][j] for grid position |
| String | LCS, Edit Distance | dp[i][j] for indices in two strings |
| Knapsack | 0/1 Knapsack, Coin Change, Subset Sum | Include or exclude item |
| Tree | House Robber III, Tree Diameter | DFS + memoization on tree nodes |
When you see a new DP problem, ask: "Which category is this?" If it involves a grid, try grid DP. If two strings, try string DP. If choosing to include/exclude items, try knapsack. This narrows down your approach immediately.
| Problem | Difficulty | Key Pattern |
|---|---|---|
| 70. Climbing Stairs | Easy | Fibonacci variant |
| 198. House Robber | Medium | Include/exclude adjacent |
| 322. Coin Change | Medium | Unbounded knapsack |
| 300. Longest Increasing Subsequence | Medium | LIS pattern |
| 139. Word Break | Medium | String segmentation |
| 91. Decode Ways | Medium | Fibonacci-like with conditions |
| Problem | Difficulty | Key Pattern |
|---|---|---|
| 62. Unique Paths | Medium | Grid traversal |
| 1143. Longest Common Subsequence | Medium | Two-string comparison |
| 72. Edit Distance | Medium | Two-string transformation |
| 494. Target Sum | Medium | Knapsack variant |
| Problem | Difficulty | Key Pattern |
|---|---|---|
| 0/1 Knapsack | Medium | Include/exclude with capacity |
| Unbounded Knapsack (Coin Change) | Medium | Reusable items with capacity |
Do them in this order: Climbing Stairs, House Robber, Coin Change, Unique Paths, LCS, then LIS. Each one builds on the concepts from the previous ones. After these six, the rest are variations.