Skip to main content

Dynamic Programming

Dynamic Programming (DP) is arguably the most feared topic in coding interviews. But it becomes manageable once you recognize the pattern: DP = recursion + memoization, applied to problems with overlapping subproblems.

When to Use DPโ€‹

Look for these signals:

  • Question asks for min/max/count of something
  • The problem can be broken into similar subproblems
  • The problem asks about all possible ways/paths
  • Keywords: "minimum number of", "maximum profit", "how many ways", "longest/shortest"

The DP Frameworkโ€‹

Step 1: Define the state

  • What does dp[i] represent?
  • "dp[i] = minimum cost to reach step i"
  • "dp[i][j] = length of LCS of first i chars of s1 and first j chars of s2"

Step 2: State transition

  • How does dp[i] depend on previous states?
  • This is the heart of DP โ€” usually requires thinking about the "last step"

Step 3: Base case

  • What's the smallest input where the answer is known?
  • Often dp[0] or dp[0][0]

Step 4: Answer

  • Usually dp[n] or dp[m][n]

Classic Problems: 1D DPโ€‹

Problem 1: Climbing Stairsโ€‹

You can climb 1 or 2 stairs at a time. How many distinct ways can you climb n stairs?

Think: The number of ways to reach step n = ways to reach step n-1 (then take 1 step) + ways to reach step n-2 (then take 2 steps).

function climbStairs(n) {
// State: dp[i] = number of ways to reach step i
// Transition: dp[i] = dp[i-1] + dp[i-2]
// Base: dp[1] = 1, dp[2] = 2

if (n <= 2) return n;

let prev2 = 1, prev1 = 2;

for (let i = 3; i <= n; i++) {
[prev2, prev1] = [prev1, prev1 + prev2];
}

return prev1;
}

// Examples:
// n=1: 1 way ([1])
// n=2: 2 ways ([1,1] or [2])
// n=3: 3 ways ([1,1,1], [1,2], [2,1])
// n=4: 5 ways

Problem 2: House Robberโ€‹

Rob houses in a row. Cannot rob adjacent houses. Maximize amount robbed.

State: dp[i] = max money robbing houses 0 to i Transition: dp[i] = max(dp[i-1], dp[i-2] + nums[i])

  • Either skip house i (take dp[i-1])
  • Or rob house i (add nums[i] to dp[i-2], since we can't touch i-1)
function rob(nums) {
if (nums.length === 1) return nums[0];

let prev2 = nums[0];
let prev1 = Math.max(nums[0], nums[1]);

for (let i = 2; i < nums.length; i++) {
const curr = Math.max(prev1, prev2 + nums[i]);
prev2 = prev1;
prev1 = curr;
}

return prev1;
}

// Example: [2, 7, 9, 3, 1]
// dp: [2, 7, 11, 11, 12]
// Answer: 12 (rob houses 0, 2, 4: 2+9+1=12)

Problem 3: Coin Changeโ€‹

Given coins of different denominations, find the minimum number of coins to make amount.

State: dp[i] = minimum coins to make amount i Transition: dp[i] = min(dp[i - coin] + 1) for each coin Base: dp[0] = 0

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];
}

// Example: coins = [1, 5, 11], amount = 15
// dp[0] = 0
// dp[1] = 1 (coin 1)
// dp[5] = 1 (coin 5)
// dp[10] = 2 (5+5)
// dp[11] = 1 (coin 11)
// dp[15] = 3 (11+3ร—1) ... actually wait
// dp[15] = min(dp[14]+1, dp[10]+1, dp[4]+1)
// = min(4+1, 2+1, 4+1) = 3 (coin 5+5+5)

Problem 4: Longest Increasing Subsequence (LIS)โ€‹

Find the length of the longest strictly increasing subsequence.

O(nยฒ) DP approach:

function lengthOfLIS(nums) {
const n = nums.length;
const dp = new Array(n).fill(1); // Every element is a subsequence of length 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);
}

O(n log n) with binary search โ€” for hard interviews:

function lengthOfLIS_optimized(nums) {
const tails = []; // tails[i] = smallest tail element of all LIS of length i+1

for (const num of nums) {
let left = 0, right = tails.length;

while (left < right) {
const mid = Math.floor((left + right) / 2);
if (tails[mid] < num) left = mid + 1;
else right = mid;
}

tails[left] = num; // Replace or extend
}

return tails.length;
}

// Example: [10, 9, 2, 5, 3, 7, 101, 18]
// tails: [10] โ†’ [9] โ†’ [2] โ†’ [2,5] โ†’ [2,3] โ†’ [2,3,7] โ†’ [2,3,7,101] โ†’ [2,3,7,18]
// LIS length: 4

Classic Problems: 2D DPโ€‹

Problem 5: Longest Common Subsequence (LCS)โ€‹

Find the length of the longest subsequence common to both strings.

State: dp[i][j] = length of LCS of s1[0..i-1] and s2[0..j-1]

Transition:

  • 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])
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];
}

// text1 = "abcde", text2 = "ace"
// LCS = "ace", length = 3

Problem 6: Edit Distanceโ€‹

Minimum operations (insert, delete, replace) to transform word1 to word2.

function minDistance(word1, word2) {
const m = word1.length, n = word2.length;
const dp = Array.from({ length: m + 1 }, (_, i) =>
Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
);

for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (word1[i - 1] === word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1]; // No operation needed
} else {
dp[i][j] = 1 + Math.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];
}

// word1 = "horse", word2 = "ros"
// horse โ†’ rorse (replace hโ†’r)
// rorse โ†’ rose (delete r)
// rose โ†’ ros (delete e)
// Edit distance = 3

Problem 7: 0/1 Knapsackโ€‹

Given items with weights and values, maximize value in a knapsack of capacity W.

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++) {
// Don't take item i
dp[i][w] = dp[i - 1][w];

// Take item i (if it fits)
if (weights[i - 1] <= w) {
dp[i][w] = Math.max(dp[i][w], dp[i - 1][w - weights[i - 1]] + values[i - 1]);
}
}
}

return dp[n][capacity];
}

DP Decision Treeโ€‹

Use this to decide your DP approach:

Is the problem asking for min/max/count?
โ”‚
โ””โ”€โ”€ YES: Can it be broken into subproblems?
โ”‚
โ””โ”€โ”€ YES: Do subproblems overlap?
โ”‚
โ”œโ”€โ”€ YES: Use DP!
โ”‚ โ”‚
โ”‚ โ”œโ”€โ”€ 1D array? โ†’ Check previous 1-2 states
โ”‚ โ”œโ”€โ”€ 2D grid? โ†’ Check row above and left
โ”‚ โ””โ”€โ”€ On a tree? โ†’ Post-order DFS + memo
โ”‚
โ””โ”€โ”€ NO: Use divide & conquer (merge sort, etc.)

Common DP Patternsโ€‹

PatternExample Problems
Linear DPClimbing Stairs, Fibonacci, House Robber
Interval DPBurst Balloons, Minimum Cost to Merge Stones
KnapsackCoin Change, Partition Equal Subset Sum, Target Sum
SubsequenceLCS, LIS, Distinct Subsequences
Path in GridMinimum Path Sum, Unique Paths
String DPEdit Distance, Palindrome Partitioning
Tree DPHouse Robber III, Diameter of Binary Tree

Practice Problems by Difficultyโ€‹

Easy:

  • LeetCode 70 โ€” Climbing Stairs
  • LeetCode 121 โ€” Best Time to Buy and Sell Stock
  • LeetCode 198 โ€” House Robber
  • LeetCode 338 โ€” Counting Bits

Medium:

  • LeetCode 322 โ€” Coin Change
  • LeetCode 300 โ€” Longest Increasing Subsequence
  • LeetCode 1143 โ€” Longest Common Subsequence
  • LeetCode 518 โ€” Coin Change 2
  • LeetCode 494 โ€” Target Sum
  • LeetCode 416 โ€” Partition Equal Subset Sum
  • LeetCode 139 โ€” Word Break
  • LeetCode 62 โ€” Unique Paths

Hard:

  • LeetCode 72 โ€” Edit Distance
  • LeetCode 312 โ€” Burst Balloons
  • LeetCode 10 โ€” Regular Expression Matching
  • LeetCode 115 โ€” Distinct Subsequences
  • LeetCode 188 โ€” Best Time to Buy and Sell Stock IV