Day 10: Dynamic Programming & Greedy Algorithms
What You'll Learn Today
- Fundamentals of Dynamic Programming (DP)
- Top-down (memoization) vs. bottom-up (tabulation)
- The greedy approach
- When to use DP vs. greedy
Dynamic Programming (DP)
Dynamic Programming breaks a problem into overlapping subproblems, solves each one only once, and stores the results for reuse.
flowchart TB
subgraph NaiveRecursion["Naive Recursion (Redundant Work)"]
F5["fib(5)"] --> F4["fib(4)"]
F5 --> F3a["fib(3)"]
F4 --> F3b["fib(3)"]
F4 --> F2a["fib(2)"]
F3a --> F2b["fib(2)"]
F3a --> F1a["fib(1)"]
F3b --> F2c["fib(2)"]
F3b --> F1b["fib(1)"]
end
style F3a fill:#ef4444,color:#fff
style F3b fill:#ef4444,color:#fff
style F2a fill:#f59e0b,color:#fff
style F2b fill:#f59e0b,color:#fff
style F2c fill:#f59e0b,color:#fff
When DP Applies
- Overlapping subproblems: The same computation occurs multiple times
- Optimal substructure: Optimal solutions to subproblems combine to form the overall optimal solution
Top-Down (Memoization)
Implemented with recursion + caching.
function fibMemo(n, memo = new Map()) {
if (n <= 1) return n;
if (memo.has(n)) return memo.get(n);
const result = fibMemo(n - 1, memo) + fibMemo(n - 2, memo);
memo.set(n, result);
return result;
}
console.log(fibMemo(50)); // 12586269025
Bottom-Up (Tabulation)
Solve smaller subproblems first and build up.
function fibTable(n) {
if (n <= 1) return n;
const dp = [0, 1];
for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
// Space-optimized: O(1) space
function fibOptimized(n) {
if (n <= 1) return n;
let prev = 0, curr = 1;
for (let i = 2; i <= n; i++) {
[prev, curr] = [curr, prev + curr];
}
return curr;
}
Top-Down vs. Bottom-Up
| Feature | Top-Down | Bottom-Up |
|---|---|---|
| Implementation | Recursion + memo | Loop + table |
| Subproblems | Computes only needed ones | Computes all |
| Stack usage | Uses recursion stack | None |
| Intuition | Natural recursive thinking | Requires table design |
Classic DP Problems
1. Climbing Stairs
Count ways to climb n stairs taking 1 or 2 steps at a time.
function climbStairs(n) {
if (n <= 2) return n;
const dp = [0, 1, 2];
for (let i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
console.log(climbStairs(5)); // 8
2. Coin Change
Find the minimum number of coins to make a target amount.
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 - coin] + 1 < dp[i]) {
dp[i] = dp[i - coin] + 1;
}
}
}
return dp[amount] === Infinity ? -1 : dp[amount];
}
console.log(coinChange([1, 5, 10, 25], 36)); // 3 (25 + 10 + 1)
console.log(coinChange([2], 3)); // -1
flowchart LR
subgraph DP["dp array (coins=[1,5,10], amount=11)"]
D0["dp[0]\n0"]
D1["dp[1]\n1"]
D5["dp[5]\n1"]
D10["dp[10]\n1"]
D11["dp[11]\n2"]
end
D0 -->|"+1"| D1
D0 -->|"+5"| D5
D0 -->|"+10"| D10
D10 -->|"+1"| D11
style D0 fill:#22c55e,color:#fff
style D11 fill:#3b82f6,color:#fff
3. Longest Common Subsequence (LCS)
function longestCommonSubsequence(text1, text2) {
const m = text1.length;
const 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
4. 0/1 Knapsack
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];
}
const weights = [2, 3, 4, 5];
const values = [3, 4, 5, 6];
console.log(knapsack(weights, values, 8)); // 10
5. Longest Increasing Subsequence (LIS)
function lengthOfLIS(nums) {
const dp = new Array(nums.length).fill(1);
for (let i = 1; i < nums.length; 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);
}
console.log(lengthOfLIS([10, 9, 2, 5, 3, 7, 101, 18])); // 4
Greedy Algorithms
A greedy algorithm makes the locally optimal choice at each step, aiming for a globally optimal result.
flowchart TB
subgraph Greedy["Greedy Approach"]
S["Problem"] --> C1["Choice 1\n(pick best)"]
C1 --> C2["Choice 2\n(pick best)"]
C2 --> C3["Choice 3\n(pick best)"]
C3 --> R["Solution"]
end
style Greedy fill:#22c55e,color:#fff
When Greedy Works
- Greedy choice property: A locally optimal choice leads to a globally optimal solution
- Optimal substructure: Optimal solutions to subproblems contribute to the overall optimum
Greedy Examples
1. Coin Change (Greedy)
function minCoinsGreedy(coins, amount) {
coins.sort((a, b) => b - a);
let count = 0;
const used = [];
for (const coin of coins) {
while (amount >= coin) {
amount -= coin;
count++;
used.push(coin);
}
}
return amount === 0 ? { count, used } : null;
}
console.log(minCoinsGreedy([1, 5, 10, 25], 36));
// { count: 3, used: [25, 10, 1] }
Note: Greedy doesn't guarantee optimal results for all coin sets. For [1, 3, 4] with amount 6, greedy gives 4+1+1=3 coins, but the optimal answer is 3+3=2 coins.
2. Activity Selection
Select the maximum number of non-overlapping activities.
function activitySelection(activities) {
activities.sort((a, b) => a.end - b.end);
const selected = [activities[0]];
let lastEnd = activities[0].end;
for (let i = 1; i < activities.length; i++) {
if (activities[i].start >= lastEnd) {
selected.push(activities[i]);
lastEnd = activities[i].end;
}
}
return selected;
}
const activities = [
{ name: "A", start: 1, end: 4 },
{ name: "B", start: 3, end: 5 },
{ name: "C", start: 0, end: 6 },
{ name: "D", start: 5, end: 7 },
{ name: "E", start: 3, end: 9 },
{ name: "F", start: 5, end: 9 },
{ name: "G", start: 6, end: 10 },
{ name: "H", start: 8, end: 11 },
];
console.log(activitySelection(activities));
// [A, D, H] — 3 activities
3. Fractional Knapsack
function fractionalKnapsack(items, capacity) {
items.sort((a, b) => (b.value / b.weight) - (a.value / a.weight));
let totalValue = 0;
let remaining = capacity;
for (const item of items) {
if (remaining <= 0) break;
const take = Math.min(item.weight, remaining);
totalValue += (take / item.weight) * item.value;
remaining -= take;
}
return totalValue;
}
const items = [
{ weight: 10, value: 60 },
{ weight: 20, value: 100 },
{ weight: 30, value: 120 },
];
console.log(fractionalKnapsack(items, 50)); // 240
DP vs. Greedy
| Feature | Dynamic Programming | Greedy |
|---|---|---|
| Approach | Solves all subproblems | Picks local optimum |
| Optimality | Always guaranteed | Not always guaranteed |
| Complexity | Usually higher | Usually lower |
| Implementation | Table/memoization | Simple loop |
| Examples | 0/1 Knapsack, LCS | Activity selection, coin change |
flowchart TB
Q1{"Optimal substructure?"}
Q1 -->|No| BF["Brute Force"]
Q1 -->|Yes| Q2{"Greedy choice\nproperty?"}
Q2 -->|Yes| G["Greedy"]
Q2 -->|No| Q3{"Overlapping\nsubproblems?"}
Q3 -->|Yes| DP["Dynamic Programming"]
Q3 -->|No| DC["Divide & Conquer"]
style G fill:#22c55e,color:#fff
style DP fill:#3b82f6,color:#fff
style DC fill:#8b5cf6,color:#fff
style BF fill:#ef4444,color:#fff
DP Solution Template
// 1. Define state: dp[i] = ...
// 2. Define transition: dp[i] = f(dp[i-1], ...)
// 3. Define base case: dp[0] = ...
// 4. Define answer: dp[n] or max(dp)
function dpTemplate(input) {
const n = input.length;
// Step 1 & 3: Initialize dp with base cases
const dp = new Array(n).fill(0);
dp[0] = /* base case */;
// Step 2: Fill dp table
for (let i = 1; i < n; i++) {
dp[i] = /* transition using dp[0..i-1] */;
}
// Step 4: Return answer
return dp[n - 1];
}
Summary
| Concept | Description |
|---|---|
| Dynamic Programming | Stores and reuses subproblem results |
| Memoization | Top-down DP with recursion + cache |
| Tabulation | Bottom-up DP filling a table from small to large |
| Greedy | Picks the locally optimal choice at each step |
| Optimal Substructure | Subproblem optima compose the overall optimum |
Key Takeaways
- DP requires both overlapping subproblems and optimal substructure
- Bottom-up avoids recursion stack and enables space optimization
- Greedy is more efficient than DP but applies to fewer problems
- Identifying the right technique for a problem is critical
Practice Problems
Problem 1: Basic
Implement a function to count the number of ways to make a target amount using given coin denominations (use DP).
Problem 2: Intermediate
Find the number of unique paths in an m×n grid from top-left to bottom-right (can only move right or down).
Challenge
Implement a function to find the minimum edit distance (Levenshtein distance) between two strings. Allowed operations: insert, delete, replace.
References
Congratulations! You've completed the 10-day journey through data structures and algorithms. Use this knowledge as a foundation and continue practicing on platforms like LeetCode and AtCoder.