Dynamic Programming & Greedy Algorithms: Complete Guide

Introduction
Dynamic Programming and Greedy Algorithms are the two major paradigms for solving optimization problems — finding the best, cheapest, longest, shortest, or most efficient solution. They're also among the most feared topics in coding interviews.
Here's the good news: both paradigms follow a pattern. DP breaks a problem into overlapping subproblems and stores results to avoid redundant work. Greedy makes the locally optimal choice at each step, trusting that local optimality leads to global optimality. Once you internalize these patterns, problems that seemed impossible start clicking.
In coding interviews, DP problems appear in roughly 25-30% of technical interviews. They're considered the hardest category — but they're also the most formulaic. This post gives you a systematic framework to tackle any DP problem, plus a clear understanding of when greedy is the right (and simpler) approach.
This is the final post in our DSA series. Everything you've learned — arrays, linked lists, trees, graphs, heaps, sorting — comes together here. DP and greedy algorithms build on all those data structures.
What You'll Learn
✅ Understand when to use DP vs greedy vs brute force
✅ Master the 5-step DP framework for any problem
✅ Implement memoization (top-down) and tabulation (bottom-up)
✅ Solve 1D and 2D DP problems systematically
✅ Recognize common DP patterns (knapsack, subsequence, grid, string)
✅ Understand greedy choice property and when greedy works
✅ Solve 10 classic DP + 4 greedy interview problems step-by-step
Prerequisites
- Understanding of Big O Notation & Complexity Analysis
- Familiarity with Problem-Solving Patterns
- Completed Arrays & Strings and Trees & BSTs
- Basic recursion knowledge
1. Dynamic Programming Fundamentals
1.1 What Is Dynamic Programming?
Dynamic Programming (DP) is an optimization technique that solves complex problems by breaking them into overlapping subproblems, solving each subproblem once, and storing the result for future use.
Think of it as recursion + caching. Without DP, a recursive solution recomputes the same subproblems exponentially. With DP, each subproblem is solved once and reused.
In the tree above, fib(3) and fib(2) are computed multiple times. DP eliminates this redundancy.
1.2 Two Conditions for DP
A problem can be solved with DP if it has:
- Overlapping subproblems: The same smaller problems are solved repeatedly
- Optimal substructure: The optimal solution to the problem can be built from optimal solutions of its subproblems
| Condition | What It Means | Example |
|---|---|---|
| Overlapping subproblems | Same computation repeated | Fibonacci: fib(3) computed many times |
| Optimal substructure | Optimal answer uses optimal sub-answers | Shortest path: shortest A→C uses shortest A→B + B→C |
1.3 Memoization vs Tabulation
| Approach | Direction | Implementation | Pros | Cons |
|---|---|---|---|---|
| Memoization | Top-down | Recursion + cache (hashmap/array) | Intuitive, only solves needed subproblems | Stack overflow risk, function call overhead |
| Tabulation | Bottom-up | Iterative, fill table from base cases | No recursion overhead, easy to optimize space | Must solve all subproblems, order matters |
Rule of thumb: Start with memoization (easier to think about), then convert to tabulation if needed for optimization.
1.4 The 5-Step DP Framework
Every DP problem follows this systematic approach:
- Define the state — What does
dp[i](ordp[i][j]) represent? - Define recurrence relation — How to compute
dp[i]from previously computed states? - Identify base cases — What are the trivially solvable smallest subproblems?
- Decide computation order — Top-down (memoization) or bottom-up (tabulation)?
- Optimize space — Can you reduce from 2D to 1D? From array to two variables?
2. 1D Dynamic Programming
Problem 1: Climbing Stairs
You are climbing a staircase with
nsteps. Each time you can climb 1 or 2 steps. How many distinct ways can you reach the top?
5-step framework:
- State:
dp[i]= number of ways to reach stepi - Recurrence:
dp[i] = dp[i-1] + dp[i-2](come from 1 step back or 2 steps back) - Base cases:
dp[0] = 1,dp[1] = 1 - Order: Bottom-up (0 → n)
- Space optimization: Only need previous two values → O(1) space
// TypeScript - Climbing Stairs
// Memoization (top-down)
function climbStairsMemo(n: number): number {
const memo = new Map<number, number>();
function dp(i: number): number {
if (i <= 1) return 1;
if (memo.has(i)) return memo.get(i)!;
const result = dp(i - 1) + dp(i - 2);
memo.set(i, result);
return result;
}
return dp(n);
}
// Tabulation (bottom-up)
function climbStairsTab(n: number): number {
if (n <= 1) return 1;
const dp = new Array(n + 1);
dp[0] = 1;
dp[1] = 1;
for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
// Space-optimized
function climbStairs(n: number): number {
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;
}
console.log(climbStairs(5)); // 8# Python - Climbing Stairs
from functools import lru_cache
# Memoization (top-down)
def climb_stairs_memo(n: int) -> int:
@lru_cache(maxsize=None)
def dp(i: int) -> int:
if i <= 1:
return 1
return dp(i - 1) + dp(i - 2)
return dp(n)
# Tabulation (bottom-up)
def climb_stairs_tab(n: int) -> int:
if n <= 1:
return 1
dp = [0] * (n + 1)
dp[0] = dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
# Space-optimized
def climb_stairs(n: int) -> int:
if n <= 1:
return 1
prev2, prev1 = 1, 1
for _ in range(2, n + 1):
prev2, prev1 = prev1, prev1 + prev2
return prev1
print(climb_stairs(5)) # 8Time: O(n). Space: O(1) optimized.
Problem 2: House Robber
You are a robber planning to rob houses along a street. Each house has money. You cannot rob two adjacent houses. What is the maximum money you can rob?
5-step framework:
- State:
dp[i]= max money robbing from houses0..i - Recurrence:
dp[i] = max(dp[i-1], dp[i-2] + nums[i])— skip house i, or rob it (+ skip i-1) - Base cases:
dp[0] = nums[0],dp[1] = max(nums[0], nums[1]) - Order: Bottom-up (0 → n)
- Space: O(1) — only need two previous values
// TypeScript - House Robber
function rob(nums: number[]): number {
if (nums.length === 0) return 0;
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;
}
console.log(rob([2, 7, 9, 3, 1])); // 12 (rob houses 0, 2, 4)# Python - House Robber
def rob(nums: list[int]) -> int:
if not nums:
return 0
if len(nums) == 1:
return nums[0]
prev2, prev1 = nums[0], max(nums[0], nums[1])
for i in range(2, len(nums)):
prev2, prev1 = prev1, max(prev1, prev2 + nums[i])
return prev1
print(rob([2, 7, 9, 3, 1])) # 12Time: O(n). Space: O(1).
Problem 3: Coin Change
Given coins of different denominations and a total amount, find the fewest number of coins needed to make up that amount. Return -1 if impossible.
5-step framework:
- State:
dp[amount]= minimum coins to makeamount - Recurrence:
dp[i] = min(dp[i - coin] + 1)for each coin - Base cases:
dp[0] = 0 - Order: Bottom-up (0 → amount)
- Space: O(amount)
// TypeScript - Coin Change
function coinChange(coins: number[], amount: number): number {
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 - coin] !== Infinity) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] === Infinity ? -1 : dp[amount];
}
console.log(coinChange([1, 5, 10, 25], 30)); // 2 (25 + 5)
console.log(coinChange([2], 3)); // -1# Python - Coin Change
def coin_change(coins: list[int], amount: int) -> int:
dp = [float("inf")] * (amount + 1)
dp[0] = 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
print(coin_change([1, 5, 10, 25], 30)) # 2
print(coin_change([2], 3)) # -1Time: O(amount × coins). Space: O(amount).
Problem 4: Longest Increasing Subsequence (LIS)
Given an integer array, find the length of the longest strictly increasing subsequence.
5-step framework:
- State:
dp[i]= length of LIS ending at indexi - Recurrence:
dp[i] = max(dp[j] + 1)for allj < iwherenums[j] < nums[i] - Base cases:
dp[i] = 1for alli(each element alone is a subsequence) - Order: Bottom-up (0 → n)
- Answer:
max(dp)— LIS can end at any index
// TypeScript - Longest Increasing Subsequence
// O(n²) DP solution
function lengthOfLIS(nums: number[]): number {
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);
}
// O(n log n) binary search optimization
function lengthOfLISOptimal(nums: number[]): number {
const tails: number[] = []; // smallest tail for each LIS length
for (const num of nums) {
let lo = 0, hi = tails.length;
while (lo < hi) {
const mid = Math.floor((lo + hi) / 2);
if (tails[mid] < num) lo = mid + 1;
else hi = mid;
}
tails[lo] = num;
}
return tails.length;
}
console.log(lengthOfLIS([10, 9, 2, 5, 3, 7, 101, 18])); // 4 → [2,3,7,101]# Python - Longest Increasing Subsequence
# O(n²) DP solution
def length_of_lis(nums: list[int]) -> int:
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)
# O(n log n) binary search optimization
import bisect
def length_of_lis_optimal(nums: list[int]) -> int:
tails = []
for num in nums:
pos = bisect.bisect_left(tails, num)
if pos == len(tails):
tails.append(num)
else:
tails[pos] = num
return len(tails)
print(length_of_lis([10, 9, 2, 5, 3, 7, 101, 18])) # 4Time: O(n²) basic, O(n log n) optimized. Space: O(n).
3. 2D Dynamic Programming
Problem 5: Unique Paths
A robot starts at the top-left corner of an
m × ngrid. It can only move right or down. How many unique paths exist to reach the bottom-right corner?
5-step framework:
- State:
dp[i][j]= number of paths to reach cell(i, j) - Recurrence:
dp[i][j] = dp[i-1][j] + dp[i][j-1](from above + from left) - Base cases:
dp[0][j] = 1,dp[i][0] = 1(only one way along edges) - Order: Row by row, left to right
- Space: O(n) — only need current and previous row → single row suffices
// TypeScript - Unique Paths
function uniquePaths(m: number, n: number): number {
const dp = new Array(n).fill(1);
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
dp[j] += dp[j - 1]; // dp[j] already has value from row above
}
}
return dp[n - 1];
}
console.log(uniquePaths(3, 7)); // 28# Python - Unique Paths
def unique_paths(m: int, n: int) -> int:
dp = [1] * n
for i in range(1, m):
for j in range(1, n):
dp[j] += dp[j - 1]
return dp[n - 1]
print(unique_paths(3, 7)) # 28Time: O(m × n). Space: O(n).
Problem 6: Longest Common Subsequence (LCS)
Given two strings, find the length of their longest common subsequence — the longest sequence of characters that appears in both strings in the same order (not necessarily contiguous).
5-step framework:
- State:
dp[i][j]= LCS length oftext1[0..i-1]andtext2[0..j-1] - Recurrence:
- 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])
- If
- Base cases:
dp[0][j] = 0,dp[i][0] = 0 - Order: Row by row
- Space: O(n) with rolling array
// TypeScript - Longest Common Subsequence
function longestCommonSubsequence(text1: string, text2: string): number {
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];
}
console.log(longestCommonSubsequence("abcde", "ace")); // 3 → "ace"# Python - Longest Common Subsequence
def longest_common_subsequence(text1: str, text2: str) -> int:
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]
print(longest_common_subsequence("abcde", "ace")) # 3Time: O(m × n). Space: O(m × n), optimizable to O(n).
Problem 7: Edit Distance
Given two strings
word1andword2, return the minimum number of operations (insert, delete, replace) to convertword1toword2.
5-step framework:
- State:
dp[i][j]= min operations to convertword1[0..i-1]toword2[0..j-1] - Recurrence:
- If
word1[i-1] == word2[j-1]:dp[i][j] = dp[i-1][j-1](no operation) - Else:
dp[i][j] = 1 + min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1])(replace, delete, insert)
- If
- Base cases:
dp[i][0] = i(delete all),dp[0][j] = j(insert all) - Order: Row by row
// TypeScript - Edit Distance
function minDistance(word1: string, word2: string): number {
const m = word1.length, n = word2.length;
const dp = Array.from({ length: m + 1 }, () => new 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 (word1[i - 1] === word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = 1 + Math.min(
dp[i - 1][j - 1], // replace
dp[i - 1][j], // delete
dp[i][j - 1] // insert
);
}
}
}
return dp[m][n];
}
console.log(minDistance("horse", "ros")); // 3
console.log(minDistance("intention", "execution")); // 5# Python - Edit Distance
def min_distance(word1: str, word2: str) -> int:
m, n = len(word1), len(word2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
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 word1[i - 1] == word2[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
dp[i][j] = 1 + min(
dp[i - 1][j - 1], # replace
dp[i - 1][j], # delete
dp[i][j - 1], # insert
)
return dp[m][n]
print(min_distance("horse", "ros")) # 3
print(min_distance("intention", "execution")) # 5Time: O(m × n). Space: O(m × n).
Problem 8: 0/1 Knapsack
Given
nitems with weights and values, and a knapsack of capacityW, find the maximum value you can carry without exceeding the weight limit. Each item can be taken at most once.
5-step framework:
- State:
dp[i][w]= max value using items0..i-1with capacityw - Recurrence:
dp[i][w] = max(dp[i-1][w], dp[i-1][w - weight[i]] + value[i])- Skip item
i, or take it (if it fits)
- Skip item
- Base cases:
dp[0][w] = 0for allw - Order: Item by item, capacity 0 → W
- Space: O(W) with 1D optimization (iterate capacity backwards)
// TypeScript - 0/1 Knapsack
function knapsack(weights: number[], values: number[], capacity: number): number {
const n = weights.length;
// Space-optimized 1D DP
const dp = new Array(capacity + 1).fill(0);
for (let i = 0; i < n; i++) {
// Iterate backwards to avoid using item i twice
for (let w = capacity; w >= weights[i]; w--) {
dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
}
}
return dp[capacity];
}
console.log(knapsack([2, 3, 4, 5], [3, 4, 5, 6], 8)); // 10# Python - 0/1 Knapsack
def knapsack(weights: list[int], values: list[int], capacity: int) -> int:
n = len(weights)
dp = [0] * (capacity + 1)
for i in range(n):
# Iterate backwards to ensure each item used at most once
for w in range(capacity, weights[i] - 1, -1):
dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
return dp[capacity]
print(knapsack([2, 3, 4, 5], [3, 4, 5, 6], 8)) # 10Time: O(n × W). Space: O(W).
Problem 9: Partition Equal Subset Sum
Given an integer array, determine if it can be partitioned into two subsets with equal sum.
Approach: This is a 0/1 knapsack variant. Target = totalSum / 2. Can we find a subset that sums to target?
// TypeScript - Partition Equal Subset Sum
function canPartition(nums: number[]): boolean {
const total = nums.reduce((a, b) => a + b, 0);
if (total % 2 !== 0) return false;
const target = total / 2;
const dp = new Array(target + 1).fill(false);
dp[0] = true;
for (const num of nums) {
for (let j = target; j >= num; j--) {
dp[j] = dp[j] || dp[j - num];
}
}
return dp[target];
}
console.log(canPartition([1, 5, 11, 5])); // true → [1,5,5] and [11]
console.log(canPartition([1, 2, 3, 5])); // false# Python - Partition Equal Subset Sum
def can_partition(nums: list[int]) -> bool:
total = sum(nums)
if total % 2 != 0:
return False
target = total // 2
dp = [False] * (target + 1)
dp[0] = True
for num in nums:
for j in range(target, num - 1, -1):
dp[j] = dp[j] or dp[j - num]
return dp[target]
print(can_partition([1, 5, 11, 5])) # True
print(can_partition([1, 2, 3, 5])) # FalseTime: O(n × target). Space: O(target).
Problem 10: Word Break
Given a string
sand a dictionary of words, returntrueifscan be segmented into dictionary words.
5-step framework:
- State:
dp[i]= cans[0..i-1]be segmented? - Recurrence:
dp[i] = trueif there existsj < iwheredp[j]is true ands[j..i-1]is in the dictionary - Base case:
dp[0] = true(empty string)
// TypeScript - Word Break
function wordBreak(s: string, wordDict: string[]): boolean {
const words = new Set(wordDict);
const n = s.length;
const dp = new Array(n + 1).fill(false);
dp[0] = true;
for (let i = 1; i <= n; i++) {
for (let j = 0; j < i; j++) {
if (dp[j] && words.has(s.slice(j, i))) {
dp[i] = true;
break;
}
}
}
return dp[n];
}
console.log(wordBreak("leetcode", ["leet", "code"])); // true
console.log(wordBreak("applepenapple", ["apple", "pen"])); // true# Python - Word Break
def word_break(s: str, word_dict: list[str]) -> bool:
words = set(word_dict)
n = len(s)
dp = [False] * (n + 1)
dp[0] = True
for i in range(1, n + 1):
for j in range(i):
if dp[j] and s[j:i] in words:
dp[i] = True
break
return dp[n]
print(word_break("leetcode", ["leet", "code"])) # True
print(word_break("applepenapple", ["apple", "pen"])) # TrueTime: O(n² × k) where k = average word length. Space: O(n).
4. Common DP Patterns
| Pattern | State Definition | Examples |
|---|---|---|
| Linear (1D) | dp[i] = answer for first i elements | Climbing stairs, House Robber, Coin Change |
| Two-string (2D) | dp[i][j] = answer for s1[0..i] and s2[0..j] | LCS, Edit Distance |
| Grid (2D) | dp[i][j] = answer for cell (i,j) | Unique Paths, Min Path Sum |
| Knapsack | dp[i][w] = answer using items 0..i with capacity w | 0/1 Knapsack, Subset Sum |
| Interval | dp[i][j] = answer for range [i..j] | Burst Balloons, Matrix Chain |
| Subsequence | dp[i] = answer for subsequence ending at i | LIS, Maximum Subarray |
| State machine | dp[i][state] = answer at position i in given state | Best Time to Buy/Sell Stock |
Pattern Recognition Tips
- "Minimum / maximum / count of ways" → likely DP
- "Can this be done?" (yes/no) → DP with boolean states
- "Partition into subsets" → Knapsack variant
- "Subsequence / substring" → 1D or 2D DP
- "Two strings comparison" → 2D DP (LCS / Edit Distance pattern)
- "Grid traversal with constraints" → 2D DP
5. Greedy Algorithms
5.1 What Is a Greedy Algorithm?
A greedy algorithm makes the locally optimal choice at each step, hoping to find the global optimum. Unlike DP, it never reconsiders past choices.
5.2 Greedy vs DP
| Property | Greedy | Dynamic Programming |
|---|---|---|
| Approach | Local optimal → global optimal | Solve all subproblems → combine |
| Reconsiders? | Never | Compares all options |
| Proof needed? | Must prove greedy works | Optimal substructure suffices |
| Complexity | Usually O(n log n) or O(n) | Usually O(n²) or O(n × W) |
| Example | Fractional knapsack | 0/1 knapsack |
Key insight: Greedy is simpler and faster — but only works when you can prove that the locally optimal choice leads to the globally optimal solution. When unsure, use DP.
5.3 When Greedy Works
Greedy works when the problem has:
- Greedy choice property: A locally optimal choice leads to a globally optimal solution
- Optimal substructure: Optimal solution contains optimal solutions to subproblems
6. Classic Greedy Problems
Problem 11: Jump Game
Given an array where each element represents the maximum jump length from that position, determine if you can reach the last index.
Greedy insight: Track the farthest index reachable. If you can't reach the current index, return false.
// TypeScript - Jump Game
function canJump(nums: number[]): boolean {
let farthest = 0;
for (let i = 0; i < nums.length; i++) {
if (i > farthest) return false;
farthest = Math.max(farthest, i + nums[i]);
}
return true;
}
console.log(canJump([2, 3, 1, 1, 4])); // true
console.log(canJump([3, 2, 1, 0, 4])); // false# Python - Jump Game
def can_jump(nums: list[int]) -> bool:
farthest = 0
for i in range(len(nums)):
if i > farthest:
return False
farthest = max(farthest, i + nums[i])
return True
print(can_jump([2, 3, 1, 1, 4])) # True
print(can_jump([3, 2, 1, 0, 4])) # FalseTime: O(n). Space: O(1).
Problem 12: Jump Game II
Given the same setup, find the minimum number of jumps to reach the last index.
Greedy insight: Use BFS-like levels. At each "level", find the farthest you can reach. Each level = one jump.
// TypeScript - Jump Game II
function jump(nums: number[]): number {
let jumps = 0;
let currentEnd = 0;
let farthest = 0;
for (let i = 0; i < nums.length - 1; i++) {
farthest = Math.max(farthest, i + nums[i]);
if (i === currentEnd) {
jumps++;
currentEnd = farthest;
}
}
return jumps;
}
console.log(jump([2, 3, 1, 1, 4])); // 2# Python - Jump Game II
def jump(nums: list[int]) -> int:
jumps = 0
current_end = 0
farthest = 0
for i in range(len(nums) - 1):
farthest = max(farthest, i + nums[i])
if i == current_end:
jumps += 1
current_end = farthest
return jumps
print(jump([2, 3, 1, 1, 4])) # 2Time: O(n). Space: O(1).
Problem 13: Activity Selection / Non-overlapping Intervals
Given intervals
[start, end], find the maximum number of non-overlapping intervals.
Greedy insight: Sort by end time. Always pick the interval that ends earliest — this leaves the most room for future intervals.
// TypeScript - Non-overlapping Intervals (return min removals)
function eraseOverlapIntervals(intervals: number[][]): number {
intervals.sort((a, b) => a[1] - b[1]); // Sort by end time
let count = 0;
let prevEnd = -Infinity;
for (const [start, end] of intervals) {
if (start >= prevEnd) {
prevEnd = end; // Keep this interval
} else {
count++; // Remove this interval (overlaps)
}
}
return count;
}
console.log(eraseOverlapIntervals([[1,2],[2,3],[3,4],[1,3]])); // 1# Python - Non-overlapping Intervals
def erase_overlap_intervals(intervals: list[list[int]]) -> int:
intervals.sort(key=lambda x: x[1]) # Sort by end time
count = 0
prev_end = float("-inf")
for start, end in intervals:
if start >= prev_end:
prev_end = end
else:
count += 1
return count
print(erase_overlap_intervals([[1,2],[2,3],[3,4],[1,3]])) # 1Time: O(n log n). Space: O(1).
Problem 14: Gas Station
There are
ngas stations in a circle.gas[i]is the gas at stationi,cost[i]is the gas needed to travel to stationi+1. Find the starting station index for a full loop, or -1 if impossible.
Greedy insight: If total gas ≥ total cost, a solution exists. Start from the station where the running tank first goes negative — the next station is the answer.
// TypeScript - Gas Station
function canCompleteCircuit(gas: number[], cost: number[]): number {
let totalTank = 0;
let currentTank = 0;
let start = 0;
for (let i = 0; i < gas.length; i++) {
const net = gas[i] - cost[i];
totalTank += net;
currentTank += net;
if (currentTank < 0) {
start = i + 1;
currentTank = 0;
}
}
return totalTank >= 0 ? start : -1;
}
console.log(canCompleteCircuit([1,2,3,4,5], [3,4,5,1,2])); // 3# Python - Gas Station
def can_complete_circuit(gas: list[int], cost: list[int]) -> int:
total_tank = 0
current_tank = 0
start = 0
for i in range(len(gas)):
net = gas[i] - cost[i]
total_tank += net
current_tank += net
if current_tank < 0:
start = i + 1
current_tank = 0
return start if total_tank >= 0 else -1
print(can_complete_circuit([1,2,3,4,5], [3,4,5,1,2])) # 3Time: O(n). Space: O(1).
7. DP vs Greedy Decision Guide
| Problem Type | Use Greedy | Use DP |
|---|---|---|
| Fractional knapsack | ✅ Take by value/weight ratio | — |
| 0/1 knapsack | ❌ Greedy fails | ✅ |
| Jump Game (can reach?) | ✅ Track farthest | — |
| Coin Change (min coins) | ❌ Greedy fails for arbitrary coins | ✅ |
| Activity Selection | ✅ Sort by end time | — |
| Longest Increasing Subsequence | — | ✅ |
| Interval scheduling | ✅ Sort by end time | — |
| Edit Distance | — | ✅ |
8. Complexity Reference
DP Problems
| Problem | Time | Space | Space Optimized |
|---|---|---|---|
| Climbing Stairs | O(n) | O(n) | O(1) |
| House Robber | O(n) | O(n) | O(1) |
| Coin Change | O(n × amount) | O(amount) | — |
| LIS | O(n²) / O(n log n) | O(n) | — |
| Unique Paths | O(m × n) | O(m × n) | O(n) |
| LCS | O(m × n) | O(m × n) | O(n) |
| Edit Distance | O(m × n) | O(m × n) | O(n) |
| 0/1 Knapsack | O(n × W) | O(n × W) | O(W) |
| Partition Subset | O(n × sum) | O(sum) | — |
| Word Break | O(n²) | O(n) | — |
Greedy Problems
| Problem | Time | Space |
|---|---|---|
| Jump Game | O(n) | O(1) |
| Jump Game II | O(n) | O(1) |
| Non-overlapping Intervals | O(n log n) | O(1) |
| Gas Station | O(n) | O(1) |
9. Practice Problems
Easy (Build Foundation)
| # | Problem | Pattern | Approach |
|---|---|---|---|
| 1 | Climbing Stairs | 1D DP | dp[i] = dp[i-1] + dp[i-2] |
| 2 | Min Cost Climbing Stairs | 1D DP | dp[i] = cost[i] + min(dp[i-1], dp[i-2]) |
| 3 | Maximum Subarray | Kadane's / DP | Track max ending here |
| 4 | Best Time to Buy and Sell Stock | Greedy | Track min price, max profit |
| 5 | Is Subsequence | Greedy / Two pointer | Match characters greedily |
Medium (Build Confidence)
| # | Problem | Pattern | Approach |
|---|---|---|---|
| 6 | House Robber | 1D DP | max(skip, rob + dp[i-2]) |
| 7 | Coin Change | Knapsack | min coins for each amount |
| 8 | Longest Increasing Subsequence | Subsequence DP | dp[i] = max(dp[j]+1) for j < i |
| 9 | Unique Paths | Grid DP | dp[i][j] = dp[i-1][j] + dp[i][j-1] |
| 10 | Longest Common Subsequence | Two-string DP | Match or max(skip) |
| 11 | Word Break | 1D DP | dp[i] = any dp[j] && s[j:i] in dict |
| 12 | Partition Equal Subset Sum | Knapsack | Subset sum = total/2 |
| 13 | Jump Game | Greedy | Track farthest reachable |
| 14 | Jump Game II | Greedy (BFS) | Level-by-level jumps |
| 15 | Non-overlapping Intervals | Greedy | Sort by end, count kept |
| 16 | Gas Station | Greedy | Track tank and start |
Hard (Master Level)
| # | Problem | Pattern | Approach |
|---|---|---|---|
| 17 | Edit Distance | Two-string DP | insert/delete/replace min |
| 18 | Regular Expression Matching | Two-string DP | Handle '.' and '*' cases |
| 19 | Burst Balloons | Interval DP | dp[i][j] = max(burst last) |
| 20 | Longest Valid Parentheses | 1D DP / Stack | Track valid ranges |
Summary
Dynamic Programming and Greedy Algorithms are the two pillars of optimization in computer science — and the final boss of coding interviews.
The DP framework (5 steps):
- Define the state
- Write the recurrence relation
- Identify base cases
- Choose computation order (top-down or bottom-up)
- Optimize space
Common DP patterns:
- 1D: Climbing stairs, House Robber, Coin Change, Word Break
- 2D grid: Unique Paths, Min Path Sum
- Two-string: LCS, Edit Distance
- Knapsack: 0/1 Knapsack, Subset Sum, Coin Change
When to use greedy:
- You can prove local optimal = global optimal
- Sorting + one pass usually works
- Examples: Jump Game, Activity Selection, Gas Station
When to use DP:
- Overlapping subproblems exist
- You need to consider all possibilities
- Greedy doesn't work (e.g., 0/1 knapsack, coin change with arbitrary denominations)
This concludes our DSA series! You now have the complete toolkit — from arrays and strings through trees, graphs, heaps, sorting, and now DP and greedy. The key to mastery is practice: pick problems from the tables above, apply the frameworks, and build pattern recognition through repetition.
Previous: Heaps & Priority Queues
Series Overview: Data Structures & Algorithms Roadmap
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.