Dynamic Programming
Back to DSA & Algorithms/00 - Roadmap Overview
What is Dynamic Programming?
Dynamic Programming (DP) is an optimization technique for problems with:
- Optimal substructure: The optimal solution contains optimal solutions to subproblems
- Overlapping subproblems: The same subproblems are solved multiple times
Instead of recomputing, DP stores results and reuses them.
The Mental Model
Imagine computing Fibonacci naively:
fib(5) = fib(4) + fib(3)
fib(4) = fib(3) + fib(2) ← fib(3) computed AGAIN
fib(3) = fib(2) + fib(1) ← fib(2) computed AGAIN
Without DP: ~15 function calls (exponential)
With DP: 5 lookups (linear)
Two Approaches
| Top-Down (Memoization) | Bottom-Up (Tabulation) | |
|---|---|---|
| Style | Recursive + cache | Iterative, fill table |
| Start from | Big problem → subproblems | Smallest subproblem → build up |
| Easier to | Think about (natural recursion) | Optimize space |
| Stack overflow risk | Yes (deep recursion) | No |
java// Top-Down: Add cache to recursion import java.util.HashMap; public class Fibonacci { private static HashMap<Integer, Integer> memo = new HashMap<>(); public static int fibTopDown(int n) { if (n <= 1) return n; if (memo.containsKey(n)) return memo.get(n); int result = fibTopDown(n - 1) + fibTopDown(n - 2); memo.put(n, result); return result; } // Bottom-Up: Fill table iteratively public static int fibBottomUp(int n) { if (n <= 1) return n; int[] dp = new int[n + 1]; dp[1] = 1; for (int i = 2; i <= n; i++) { dp[i] = dp[i - 1] + dp[i - 2]; } return dp[n]; } // Space-Optimized: Only keep what you need public static int fibOptimized(int n) { if (n <= 1) return n; int prev2 = 0, prev1 = 1; for (int i = 2; i <= n; i++) { int curr = prev1 + prev2; prev2 = prev1; prev1 = curr; } return prev1; } }
The DP Framework (for EVERY problem)
- Define the state: What variables describe a subproblem? →
dp[i],dp[i][j] - Find the transition: How does
dp[i]relate to smaller subproblems? - Determine base cases: What are the smallest subproblems?
- Identify the answer: Which cell in the table is the final answer?
- Optimize space (optional): Can you reduce the table to fewer rows/variables?
How to Identify DP Problems
Signal phrases:
- "Find the minimum cost to..."
- "Find the maximum profit from..."
- "How many ways to..."
- "Is it possible to..."
- "Find the longest/shortest subsequence/path..."
Ask: "Does making a choice now affect future choices?" If yes → likely DP.
Pattern 1: 1D DP (Linear Sequence)
What it is
State depends on previous elements in a sequence. dp[i] = answer considering the first i elements.
Climbing Stairs
java// How many ways to reach step n? Can climb 1 or 2 steps at a time. // State: dp[i] = number of ways to reach step i // Transition: dp[i] = dp[i-1] + dp[i-2] // (you can reach step i from step i-1 or step i-2) // Base: dp[0] = 1, dp[1] = 1 public static int climbStairs(int n) { if (n <= 2) return n; int prev2 = 1, prev1 = 2; for (int i = 3; i <= n; i++) { int curr = prev1 + prev2; prev2 = prev1; prev1 = curr; } return prev1; }
House Robber
java// Can't rob adjacent houses. Maximize stolen amount. // [2, 7, 9, 3, 1] → Rob houses 0, 2, 4 = 2+9+1=12 // State: dp[i] = max money from first i houses // Transition: dp[i] = max(dp[i-1], dp[i-2] + nums[i]) // Either skip house i (take dp[i-1]) // Or rob house i (take dp[i-2] + nums[i]) public static int rob(int[] nums) { if (nums.length == 0) return 0; if (nums.length == 1) return nums[0]; int prev2 = 0, prev1 = 0; for (int num : nums) { int curr = Math.max(prev1, prev2 + num); prev2 = prev1; prev1 = curr; } return prev1; } // Walkthrough: [2, 7, 9, 3, 1] // num=2: curr = max(0, 0+2) = 2. prev2=0, prev1=2 // num=7: curr = max(2, 0+7) = 7. prev2=2, prev1=7 // num=9: curr = max(7, 2+9) = 11. prev2=7, prev1=11 // num=3: curr = max(11, 7+3) = 11. prev2=11, prev1=11 // num=1: curr = max(11, 11+1) = 12. Answer: 12
Longest Increasing Subsequence (LIS)
java// Find length of longest strictly increasing subsequence. // [10, 9, 2, 5, 3, 7, 101, 18] → [2, 3, 7, 101] length 4 // O(n²) approach: // State: dp[i] = length of LIS ending at index i // Transition: dp[i] = max(dp[j] + 1) for all j < i where nums[j] < nums[i] import java.util.Arrays; import java.util.ArrayList; public static int lengthOfLIS(int[] nums) { int n = nums.length; int[] dp = new int[n]; Arrays.fill(dp, 1); // Every element is an LIS of length 1 for (int i = 1; i < n; i++) { for (int j = 0; j < i; j++) { if (nums[j] < nums[i]) { dp[i] = Math.max(dp[i], dp[j] + 1); } } } int max = 0; for (int val : dp) max = Math.max(max, val); return max; } // O(n log n) approach using binary search: // Maintain a "tails" array where tails[i] = smallest tail element // for an increasing subsequence of length i+1 public static int lengthOfLISFast(int[] nums) { ArrayList<Integer> tails = new ArrayList<>(); for (int num : nums) { int pos = Collections.binarySearch(tails, num); if (pos < 0) pos = -(pos + 1); // insertion point if (pos == tails.size()) { tails.add(num); } else { tails.set(pos, num); } } return tails.size(); }
Problems
- 70. Climbing Stairs - Easy
- 198. House Robber - Medium
- 213. House Robber II - Medium
- 91. Decode Ways - Medium
- 139. Word Break - Medium
- 300. Longest Increasing Subsequence - Medium
- 152. Maximum Product Subarray - Medium
Pattern 2: 2D DP (Grid / Two Sequences)
What it is
State depends on two variables. Either navigating a grid or comparing two sequences.
Grid: Unique Paths
java// Robot moves right or down. Count paths from top-left to bottom-right. // State: dp[i][j] = number of paths to reach cell (i, j) // Transition: dp[i][j] = dp[i-1][j] + dp[i][j-1] // Base: dp[0][j] = 1, dp[i][0] = 1 (only one way along edges) public static int uniquePaths(int m, int n) { int[][] dp = new int[m][n]; for (int i = 0; i < m; i++) dp[i][0] = 1; for (int j = 0; j < n; j++) dp[0][j] = 1; for (int i = 1; i < m; i++) { for (int j = 1; j < n; j++) { dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; } } return dp[m - 1][n - 1]; }
Two Sequences: Longest Common Subsequence
java// Find longest common subsequence of two strings. // "abcde" and "ace" → "ace" (length 3) // State: dp[i][j] = LCS length of text1[0..i-1] and text2[0..j-1] // Transition: // If text1.charAt(i-1) == text2.charAt(j-1): dp[i][j] = dp[i-1][j-1] + 1 (extend match) // Else: dp[i][j] = max(dp[i-1][j], dp[i][j-1]) (skip one character) public static int longestCommonSubsequence(String text1, String text2) { int m = text1.length(), n = text2.length(); int[][] dp = new int[m + 1][n + 1]; for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { if (text1.charAt(i - 1) == text2.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]; }
Edit Distance (Levenshtein)
java// Minimum operations (insert, delete, replace) to convert word1 → word2 // "horse" → "ros" = 3 // State: dp[i][j] = edit distance between word1[0..i-1] and word2[0..j-1] // Transition: // Match: dp[i][j] = dp[i-1][j-1] (no operation needed) // Replace: dp[i][j] = dp[i-1][j-1] + 1 // Insert: dp[i][j] = dp[i][j-1] + 1 (insert char from word2) // Delete: dp[i][j] = dp[i-1][j] + 1 (delete char from word1) public static int minDistance(String word1, String word2) { int m = word1.length(), n = word2.length(); int[][] dp = new int[m + 1][n + 1]; // Base cases for (int i = 0; i <= m; i++) dp[i][0] = i; // Delete all from word1 for (int j = 0; j <= n; j++) dp[0][j] = j; // Insert all from word2 for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { if (word1.charAt(i - 1) == word2.charAt(j - 1)) { dp[i][j] = dp[i - 1][j - 1]; } else { dp[i][j] = 1 + Math.min( dp[i - 1][j - 1], // Replace Math.min( dp[i - 1][j], // Delete dp[i][j - 1] // Insert ) ); } } } return dp[m][n]; }
Problems
- 62. Unique Paths - Medium
- 64. Minimum Path Sum - Medium
- 1143. Longest Common Subsequence - Medium
- 72. Edit Distance - Medium
- 97. Interleaving String - Medium
Pattern 3: Knapsack
What it is
Choose items with weights/values to maximize value within a capacity constraint.
0/1 Knapsack (Each Item Used At Most Once)
java// State: dp[i][w] = max value using first i items with capacity w // Transition: dp[i][w] = max( // dp[i-1][w], // Skip item i // dp[i-1][w - weight[i]] + value[i] // Take item i (if it fits) // ) 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 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]; } // Space optimization: only need previous row public static int knapsackOptimized(int[] weights, int[] values, int capacity) { int[] dp = new int[capacity + 1]; for (int i = 0; i < weights.length; i++) { for (int w = capacity; w >= weights[i]; w--) { // RIGHT TO LEFT! dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]); } } return dp[capacity]; } // Why right to left? To avoid using the same item twice. // If left to right: dp[w] might use updated dp[w - weight[i]] from THIS row // Right to left ensures we use values from the PREVIOUS row.
Unbounded Knapsack (Items Can Be Reused)
java// Coin Change: minimum coins to make amount // State: dp[a] = min coins to make amount a // Transition: dp[a] = min(dp[a - coin] + 1) for each coin public static int coinChange(int[] coins, int amount) { int[] dp = new int[amount + 1]; Arrays.fill(dp, Integer.MAX_VALUE); dp[0] = 0; for (int a = 1; a <= amount; a++) { for (int coin : coins) { if (coin <= a && dp[a - coin] != Integer.MAX_VALUE) { dp[a] = Math.min(dp[a], dp[a - coin] + 1); } } } return dp[amount] != Integer.MAX_VALUE ? dp[amount] : -1; }
Subset Sum / Partition
java// Can we partition array into two subsets with equal sum? // [1, 5, 11, 5] → {1, 5, 5} and {11}, both sum to 11 public static boolean canPartition(int[] nums) { int total = 0; for (int num : nums) total += num; if (total % 2 != 0) return false; // Odd total can't be split equally int target = total / 2; boolean[] dp = new boolean[target + 1]; dp[0] = true; for (int num : nums) { for (int j = target; j >= num; j--) { // Right to left (0/1) dp[j] = dp[j] || dp[j - num]; } } return dp[target]; }
Problems
- 416. Partition Equal Subset Sum - Medium
- 494. Target Sum - Medium
- 322. Coin Change - Medium
- 518. Coin Change II - Medium
Pattern 4: State Machine DP
What it is
Define states (like "holding stock", "in cooldown") and transitions between them.
Stock Problems
java// Buy and Sell Stock with Cooldown // States: hold (have stock), sold (just sold, must cooldown), rest (no stock, can buy) public static int maxProfit(int[] prices) { int hold = -prices[0]; // Bought on day 0 int sold = 0; // Just sold (in cooldown) int rest = 0; // No stock, free to buy for (int i = 1; i < prices.length; i++) { int prevHold = hold; hold = Math.max(hold, rest - prices[i]); // Keep holding OR buy rest = Math.max(rest, sold); // Keep resting OR cooldown finished sold = prevHold + prices[i]; // Sell what we're holding } return Math.max(sold, rest); } // Each day we transition between states: // rest → hold (buy) // hold → sold (sell) // sold → rest (cooldown ends) // hold → hold (keep holding) // rest → rest (keep resting)
Problems
- 121. Best Time to Buy and Sell Stock - Easy
- 122. Best Time to Buy and Sell Stock II - Medium
- 309. Best Time to Buy and Sell Stock with Cooldown - Medium
- 188. Best Time to Buy and Sell Stock IV - Hard
Pattern 5: Interval DP
What it is
Solve for all intervals [i, j], building from small intervals to large ones.
java// Longest Palindromic Subsequence public static int longestPalindromeSubseq(String s) { int n = s.length(); int[][] dp = new int[n][n]; for (int i = 0; i < n; i++) { dp[i][i] = 1; // Single characters are palindromes } for (int length = 2; length <= n; length++) { for (int i = 0; i <= n - length; i++) { int j = i + length - 1; if (s.charAt(i) == s.charAt(j)) { dp[i][j] = dp[i + 1][j - 1] + 2; } else { dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]); } } } return dp[0][n - 1]; }
Problems
- 516. Longest Palindromic Subsequence - Medium
- 5. Longest Palindromic Substring - Medium
- 312. Burst Balloons - Hard
Things You MUST Be Aware Of
1. Start with Brute Force Recursion
java// ALWAYS start by writing the recursive solution. // Then add memoization (HashMap<String, Integer> or int[][] memo with sentinel values). // Then convert to bottom-up if needed. // Don't try to jump straight to bottom-up — it's error-prone.
2. Space Optimization
java// If dp[i] only depends on dp[i-1] → use 2 rows or 2 variables // If dp[i][j] only depends on dp[i-1][j] and dp[i][j-1] → use 1 row // 0/1 knapsack: iterate RIGHT TO LEFT to avoid reusing items // Unbounded knapsack: iterate LEFT TO RIGHT (reuse is allowed)
3. Coin Change: Min vs Count
java// Minimum coins (optimization): dp[a] = Math.min(dp[a], dp[a - c] + 1) // Number of ways (counting): for each coin c: dp[a] += dp[a - c] // Different transitions for different questions!
4. The Hardest Part is Defining the State
java// If your state doesn't capture enough info, you'll get wrong answers. // If your state has too many dimensions, you'll be too slow. // Ask: "What do I need to know to make the next decision?"
DP Identification Cheat Sheet
| Signal | Pattern |
|---|---|
| "Min cost / max profit" | Optimization DP |
| "How many ways" | Counting DP |
| "Is it possible" | Boolean DP |
| "Longest/shortest subsequence" | LIS / LCS variants |
| "Partition into groups" | Knapsack / subset sum |
| "String matching / edit" | 2D DP on two strings |
| "Buy/sell with constraints" | State machine DP |
| "All intervals [i,j]" | Interval DP |