Classic DP Problems
TL;DR
Five essential DP problems dominate interviews and real-world scenarios. 0/1 Knapsack: dp[i][w] = max value using first i items with capacity w; choose to include item or skip. Longest Common Subsequence: dp[i][j] = longest common subsequence for first i and j characters; match characters or take max of alternatives. Longest Increasing Subsequence: dp[i] = longest increasing subsequence ending at index i; extend all shorter subsequences. Coin Change: dp[i] = minimum coins to make amount i; try all coins and pick minimum. Edit Distance: dp[i][j] = minimum operations to transform first i characters to first j characters; insert, delete, or replace. All follow tabulation pattern: define state, establish recurrence, initialize base cases, fill table systematically.
Core Concepts
The Five Classic Problems
These problems appear frequently in interviews and algorithm competitions. Understanding their patterns helps solve variations and new problems.
Problem Classification:
- Optimization: Knapsack, Coin Change
- Subsequence: LCS, LIS
- Distance: Edit Distance
Common Traits:
- Clear state definition with 1-2 dimensions
- Simple transitions from previous states
- Straightforward base cases
- Polynomial time complexity (O(n²) or O(n·m))
Knapsack Problem Variants
The 0/1 Knapsack problem has two main interpretations:
- Weight-centric: Given item weights and values, maximize value within weight limit
- Capacity-centric: Given items with weights and values, select subset maximizing value
Unbounded Knapsack (variation) allows selecting same item multiple times.
String Algorithms
LCS and Edit Distance solve classic string problems. Both use 2D DP tables where dimensions represent string lengths. Alignment concepts apply to sequence analysis, plagiarism detection, and bioinformatics.
Key Algorithms
0/1 Knapsack Pattern
State: dp[i][w] = maximum value using first i items with weight limit w
Recurrence:
dp[i][w] = max(
dp[i-1][w], // skip item i
dp[i-1][w-weight[i]] + value[i] // take item i
)
Base cases: dp[0][w] = 0 for all w (no items, no value)
LCS Pattern
State: dp[i][j] = length of longest common subsequence for first i chars of str1 and first j chars of str2
Recurrence:
if str1[i-1] == str2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
LIS Pattern
State: dp[i] = length of longest increasing subsequence ending at index i
Recurrence:
dp[i] = max(dp[j] + 1) for all j < i where arr[j] < arr[i]
dp[i] = 1 if no such j exists
Coin Change Pattern
State: dp[i] = minimum coins needed to make amount i
Recurrence:
dp[i] = min(dp[i - coin] + 1) for all coins where coin <= i
Edit Distance Pattern
State: dp[i][j] = minimum operations to transform first i chars of str1 to first j chars of str2
Recurrence:
if str1[i-1] == str2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = 1 + min(
dp[i-1][j], // delete from str1
dp[i][j-1], // insert into str1
dp[i-1][j-1] // replace
)
Practical Example
- Python
- Java
- TypeScript
# 0/1 Knapsack Problem
def knapsack(weights, values, capacity):
n = len(weights)
# dp[i][w] = max value using first i items with capacity w
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for w in range(capacity + 1):
# Skip item i
dp[i][w] = dp[i-1][w]
# Take item i if it fits
if weights[i-1] <= w:
dp[i][w] = max(dp[i][w],
dp[i-1][w - weights[i-1]] + values[i-1])
return dp[n][capacity]
# Longest Common Subsequence
def lcs(str1, str2):
m, n = len(str1), len(str2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if str1[i-1] == str2[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]
# Longest Increasing Subsequence
def lis(arr):
n = len(arr)
# dp[i] = length of LIS ending at index i
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) if n > 0 else 0
# Coin Change
def coin_change(coins, amount):
# dp[i] = minimum coins to make amount i
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
# Edit Distance
def edit_distance(str1, str2):
m, n = len(str1), len(str2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
# Base cases: transform from/to empty string
for i in range(m + 1):
dp[i][0] = i
for j in range(n + 1):
dp[0][j] = j
for i in range(1, m + 1):
for j in range(1, n + 1):
if str1[i-1] == str2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = 1 + min(
dp[i-1][j], # delete
dp[i][j-1], # insert
dp[i-1][j-1] # replace
)
return dp[m][n]
# Tests
print(knapsack([2, 3, 4, 5], [3, 4, 5, 6], 8)) # 13
print(lcs("ABCDGH", "AEDFHR")) # 3 (ADH)
print(lis([10, 9, 2, 5, 3, 7, 101, 18])) # 4 (2,3,7,101)
print(coin_change([1, 2, 5], 5)) # 1 (one 5-coin)
print(edit_distance("horse", "ros")) # 3
public class ClassicDPProblems {
// 0/1 Knapsack Problem
public static int knapsack(int[] weights, int[] values, int capacity) {
int n = weights.length;
int[][] dp = new int[n + 1][capacity + 1];
for (int i = 1; i <= n; i++) {
for (int w = 0; w <= capacity; w++) {
dp[i][w] = dp[i-1][w]; // skip item
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];
}
// Longest Common Subsequence
public static int lcs(String str1, String str2) {
int m = str1.length(), n = str2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (str1.charAt(i-1) == str2.charAt(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];
}
// Longest Increasing Subsequence
public static int lis(int[] arr) {
int n = arr.length;
int[] dp = new int[n];
java.util.Arrays.fill(dp, 1);
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
if (arr[j] < arr[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
return java.util.Arrays.stream(dp).max().orElse(0);
}
// Coin Change
public static int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
java.util.Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
if (coin <= i && dp[i - coin] != Integer.MAX_VALUE) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
}
// Edit Distance
public static int editDistance(String str1, String str2) {
int m = str1.length(), n = str2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 0; i <= m; i++) dp[i][0] = i;
for (int j = 0; j <= n; j++) dp[0][j] = j;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (str1.charAt(i-1) == str2.charAt(j-1)) {
dp[i][j] = dp[i-1][j-1];
} else {
dp[i][j] = 1 + Math.min(
Math.min(dp[i-1][j], dp[i][j-1]),
dp[i-1][j-1]
);
}
}
}
return dp[m][n];
}
public static void main(String[] args) {
System.out.println(knapsack(new int[]{2,3,4,5},
new int[]{3,4,5,6}, 8)); // 13
System.out.println(lcs("ABCDGH", "AEDFHR")); // 3
System.out.println(lis(new int[]{10,9,2,5,3,7,101,18})); // 4
System.out.println(coinChange(new int[]{1,2,5}, 5)); // 1
System.out.println(editDistance("horse", "ros")); // 3
}
}
// 0/1 Knapsack Problem
function knapsack(weights: number[], values: number[], capacity: number): number {
const n = weights.length;
const dp = Array(n + 1).fill(0).map(() => Array(capacity + 1).fill(0));
for (let i = 1; i <= n; i++) {
for (let w = 0; w <= capacity; w++) {
dp[i][w] = dp[i-1][w]; // skip item
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];
}
// Longest Common Subsequence
function lcs(str1: string, str2: string): number {
const m = str1.length, n = str2.length;
const dp = Array(m + 1).fill(0).map(() => Array(n + 1).fill(0));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (str1[i-1] === str2[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];
}
// Longest Increasing Subsequence
function lis(arr: number[]): number {
const n = arr.length;
const dp = Array(n).fill(1);
for (let i = 1; i < n; i++) {
for (let j = 0; j < i; j++) {
if (arr[j] < arr[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
return Math.max(...dp);
}
// Coin Change
function coinChange(coins: number[], amount: number): number {
const dp = 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];
}
// Edit Distance
function editDistance(str1: string, str2: string): number {
const m = str1.length, n = str2.length;
const dp = Array(m + 1).fill(0).map(() => Array(n + 1).fill(0));
for (let i = 0; i <= m; i++) dp[i][0] = i;
for (let j = 0; j <= n; j++) dp[0][j] = j;
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (str1[i-1] === str2[j-1]) {
dp[i][j] = dp[i-1][j-1];
} else {
dp[i][j] = 1 + Math.min(
dp[i-1][j],
dp[i][j-1],
dp[i-1][j-1]
);
}
}
}
return dp[m][n];
}
console.log(knapsack([2,3,4,5], [3,4,5,6], 8)); // 13
console.log(lcs("ABCDGH", "AEDFHR")); // 3
console.log(lis([10,9,2,5,3,7,101,18])); // 4
console.log(coinChange([1,2,5], 5)); // 1
console.log(editDistance("horse", "ros")); // 3
Common Pitfalls
Incorrect state definition: Using wrong dimensions for DP table leads to index errors. Verify dimensions match problem parameters (items vs capacity, string lengths, etc.).
Off-by-one in recurrence: When accessing arr[i-1] from dp[i], ensure indexing is consistent. String and array indices need careful handling.
Forgetting base cases: Incomplete initialization leaves undefined states. For 2D problems, initialize full first row and column.
Unbounded vs 0/1 confusion: Knapsack variations require different recurrence. Unbounded allows reusing items; 0/1 does not.
Inefficient LIS implementation: O(n²) is standard for simple version but can optimize to O(n log n) with binary search and patience sorting.
Not tracking actual solution: DP computes optimal value but not the choices. If solution reconstruction needed, track parent pointers.
Self-Check
- Why does 0/1 Knapsack require 2D DP while Coin Change uses 1D? What's the key difference?
- How would you reconstruct the actual longest common subsequence string, not just its length?
- For Longest Increasing Subsequence, why is LIS ending at each position a useful state definition?
One Takeaway
Classic DP problems share a common pattern: define state to represent subproblem, establish recurrence relating state to smaller subproblems, initialize base cases, and fill table systematically. Master these five problems and you'll recognize similar patterns in new problems. The key insight is choosing the right state—once you have that, the recurrence usually becomes obvious. Practice reconstructing solutions from DP tables to develop intuition for DP state transitions.
References
- Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2009). Introduction to Algorithms (3rd ed.). MIT Press.
- Kleinberg, J., & Tardos, É. (2005). Algorithm Design. Pearson Education.