Master the art of breaking complex problems into overlapping subproblems and storing their solutions. Learn both top-down (memoization) and bottom-up (tabulation) approaches with comprehensive C# implementations, then solve classic interview problems from 1D and 2D DP to state machines and interval DP.
Dynamic Programming is an algorithmic technique to solve complex problems by breaking them into overlapping subproblems and storing solutions to avoid recomputation. It applies when two key properties hold.
DP requires two essential properties:
Apply DP when: recursive structure with overlapping subproblems, need for optimal value (max/min/count), naive recursion is exponential, problem decomposes into independent subproblems.
| Aspect | Top-Down (Memo) | Bottom-Up (Tabulation) |
|---|---|---|
| Direction | Recursive toward base | Iterative base toward solution |
| Structure | Recursion + cache | Array/table |
| Cache | Lazy (needed states) | Eager (all states) |
| Stack Risk | Recursion overflow possible | No recursion overhead |
Write a recursive function, then cache results in a Dictionary to avoid recomputing subproblems. This mirrors natural problem thinking.
private Dictionary<int, int> memo = new Dictionary<int, int>(); private int SolveHelper(int n) { // Check cache first if (memo.ContainsKey(n)) return memo[n]; // Base cases if (n == 0) return 0; if (n == 1) return 1; // Recurrence: solve subproblems recursively int result = SolveHelper(n - 1) + SolveHelper(n - 2); // Memoize before returning memo[n] = result; return result; } public int Solve(int n) { memo.Clear(); return SolveHelper(n); }
Fibonacci Example: Compute fib(n) using memoization. Without caching, fib(5) recomputes fib(3) twice. With caching, each subproblem solves once.
For problems with multiple dimensions (e.g., grid, sequence), use nested Dictionary or 2D array:
private Dictionary<string, int> memo = new Dictionary<string, int>(); private int SolveHelper(int i, int j) { // Create composite key for 2D state string key = $"{i},{j}"; if (memo.ContainsKey(key)) return memo[key]; // Base cases and recurrence... int result = /* computation */ 0; memo[key] = result; return result; }
Build solutions iteratively from base cases upward. Fill a table/array with values, computing each state from previously solved states. No recursion, no stack overhead.
public int Solve(int n) { // Create DP table int[] dp = new int[n + 1]; // Base cases dp[0] = 0; if (n > 0) dp[1] = 1; // Fill table bottom-up for (int i = 2; i <= n; i++) { dp[i] = dp[i - 1] + dp[i - 2]; } return dp[n]; }
Fibonacci via Tabulation: Build dp table from bottom. dp[0]=0, dp[1]=1, then dp[i]=dp[i-1]+dp[i-2]. Naturally avoids recomputation.
public int Solve(int m, int n) { // Create 2D DP table int[][] dp = new int[m][]; for (int i = 0; i < m; i++) dp[i] = new int[n]; // Initialize base cases for (int i = 0; i < m; i++) dp[i][0] = 1; for (int j = 0; j < n; j++) dp[0][j] = 1; // Fill table bottom-up 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]; }
Many DP problems depend only on previous row/column. Replace O(m*n) space with O(n) using rolling arrays or variable optimization.
If current state depends only on the previous state, maintain just two arrays: previous and current.
public int Solve(int m, int n) { // Only keep current and previous rows int[] prev = new int[n]; int[] curr = new int[n]; // Initialize base case for row 0 for (int j = 0; j < n; j++) prev[j] = 1; // Fill rows from 1 to m-1 for (int i = 1; i < m; i++) { curr[0] = 1; for (int j = 1; j < n; j++) { curr[j] = prev[j] + curr[j-1]; } // Swap arrays int[] temp = prev; prev = curr; curr = temp; } return prev[n - 1]; }
For problems where only two previous values matter (not a full row/column), reduce to O(1) space with two variables.
public int FibOptimized(int n) { if (n == 0) return 0; if (n == 1) return 1; int prev2 = 0, prev1 = 1, curr = 0; for (int i = 2; i <= n; i++) { curr = prev1 + prev2; prev2 = prev1; prev1 = curr; } return curr; }
Problem: Climb n steps. At each step, climb 1 or 2 steps. How many distinct ways?
State Definition: dp[i] = number of ways to reach step i.
Recurrence Relation: dp[i] = dp[i-1] + dp[i-2] (reach step i from step i-1 or i-2).
Base Cases: dp[0] = 1 (one way to stay at step 0), dp[1] = 1 (one way to reach step 1).
public int ClimbStairs(int n) { if (n <= 1) return 1; int[] dp = new int[n + 1]; dp[0] = 1; dp[1] = 1; // Each step can be reached from step i-1 or i-2 for (int i = 2; i <= n; i++) { dp[i] = dp[i-1] + dp[i-2]; } return dp[n]; } // Space-optimized version: O(1) space, O(n) time public int ClimbStairsOptimized(int n) { if (n <= 1) return 1; int prev2 = 1, prev1 = 1; for (int i = 2; i <= n; i++) { int curr = prev1 + prev2; prev2 = prev1; prev1 = curr; } return prev1; }
Problem I: Rob houses in a line. Can't rob adjacent houses. Maximize loot. Problem II: Houses arranged in a circle—first and last are adjacent.
State: dp[i] = max money robbing houses 0..i.
Recurrence: dp[i] = max(dp[i-1], dp[i-2] + nums[i]) (skip or rob house i).
public int Rob(int[] nums) { if (nums == null || nums.Length == 0) return 0; if (nums.Length == 1) return nums[0]; int[] dp = new int[nums.Length]; dp[0] = nums[0]; dp[1] = Math.Max(nums[0], nums[1]); for (int i = 2; i < nums.Length; i++) { // Rob current house + max from i-2, or skip to take max from i-1 dp[i] = Math.Max(dp[i-1], dp[i-2] + nums[i]); } return dp[nums.Length - 1]; } // Space-optimized: only need prev2 and prev1 public int RobOptimized(int[] nums) { if (nums == null || nums.Length == 0) return 0; if (nums.Length == 1) return nums[0]; int prev2 = nums[0]; int prev1 = Math.Max(nums[0], nums[1]); for (int i = 2; i < nums.Length; i++) { int curr = Math.Max(prev1, prev2 + nums[i]); prev2 = prev1; prev1 = curr; } return prev1; } // House Robber II: houses in circle. Can't rob first and last together. public int RobII(int[] nums) { if (nums == null || nums.Length == 0) return 0; if (nums.Length == 1) return nums[0]; // Case 1: Rob houses 0 to n-2 (exclude last) int case1 = RobRange(nums, 0, nums.Length - 2); // Case 2: Rob houses 1 to n-1 (exclude first) int case2 = RobRange(nums, 1, nums.Length - 1); return Math.Max(case1, case2); } private int RobRange(int[] nums, int start, int end) { if (start > end) return 0; if (start == end) return nums[start]; int prev2 = 0, prev1 = nums[start]; for (int i = start + 1; i <= end; i++) { int curr = Math.Max(prev1, prev2 + nums[i]); prev2 = prev1; prev1 = curr; } return prev1; }
Problem: Given coins and amount. Find minimum number of coins to make amount. Return -1 if impossible.
State: dp[i] = minimum coins to make amount i.
Recurrence: dp[i] = 1 + min(dp[i-coin]) for each coin ≤ i.
Base Case: dp[0] = 0 (zero coins for zero amount).
public int CoinChange(int[] coins, int amount) { // dp[i] = min coins to make amount i int[] dp = new int[amount + 1]; // Initialize: mark impossible amounts for (int i = 1; i <= amount; i++) dp[i] = amount + 1; // Use amount+1 as "infinity" dp[0] = 0; // Fill table: for each amount for (int i = 1; i <= amount; i++) { // Try each coin foreach (int coin in coins) { if (coin <= i) { // Can use this coin dp[i] = Math.Min(dp[i], 1 + dp[i - coin]); } } } return dp[amount] > amount ? -1 : dp[amount]; }
Problem: Find longest strictly increasing subsequence. Return length.
State: dp[i] = length of LIS ending at index i.
Recurrence: dp[i] = 1 + max(dp[j]) for all j < i where nums[j] < nums[i].
public int LengthOfLIS(int[] nums) { if (nums == null || nums.Length == 0) return 0; int[] dp = new int[nums.Length]; int maxLen = 0; for (int i = 0; i < nums.Length; i++) { dp[i] = 1; // At minimum, LIS ending at i has length 1 // Check all previous elements for (int j = 0; j < i; j++) { if (nums[j] < nums[i]) { dp[i] = Math.Max(dp[i], 1 + dp[j]); } } maxLen = Math.Max(maxLen, dp[i]); } return maxLen; } // Approach 2: O(n log n) using binary search public int LengthOfLISOptimal(int[] nums) { if (nums == null || nums.Length == 0) return 0; List<int> tails = new List<int>(); // tails[i] = smallest tail element for LIS of length i+1 foreach (int num in nums) { int pos = tails.BinarySearch(num); if (pos < 0) { pos = ~pos; // Convert to insertion position } if (pos == tails.Count) { tails.Add(num); } else { tails[pos] = num; } } return tails.Count; }
Word Break: Can segment string using dictionary words? Decode Ways: Count number of ways to decode digit string (1=A, 2=B, ..., 26=Z).
State: dp[i] = true if s[0..i-1] can be segmented.
Recurrence: dp[i] = true if ∃j where dp[j]=true and s[j..i-1] is in dictionary.
public bool WordBreak(string s, IList<string> wordDict) { HashSet<string> dict = new HashSet<string>(wordDict); bool[] dp = new bool[s.Length + 1]; dp[0] = true; for (int i = 1; i <= s.Length; i++) { for (int j = 0; j < i; j++) { if (dp[j] && dict.Contains(s.Substring(j, i - j))) { dp[i] = true; break; } } } return dp[s.Length]; } // Decode Ways (LC 91) // State: dp[i] = number of ways to decode s[0..i-1] // Recurrence: dp[i] = dp[i-1] (if s[i-1] is 1-9) + dp[i-2] (if s[i-2..i-1] is 10-26) public int NumDecodings(string s) { if (s == null || s.Length == 0 || s[0] == '0') return 0; int n = s.Length; int[] dp = new int[n + 1]; dp[0] = 1; dp[1] = 1; for (int i = 2; i <= n; i++) { // Check single digit (1-9) if (s[i - 1] != '0') { dp[i] += dp[i - 1]; } // Check two digits (10-26) int twoDigit = int.Parse(s.Substring(i - 2, 2)); if (twoDigit >= 10 && twoDigit <= 26) { dp[i] += dp[i - 2]; } } return dp[n]; }
State: dp[i] = minimum perfect squares to sum to i.
public int NumSquares(int n) { int[] dp = new int[n + 1]; for (int i = 1; i <= n; i++) dp[i] = i; // At worst: i = 1+1+...+1 for (int i = 1; i <= n; i++) { // Try all perfect squares less than i for (int j = 1; j * j <= i; j++) { dp[i] = Math.Min(dp[i], 1 + dp[i - j * j]); } } return dp[n]; }
Can array be partitioned into two subsets with equal sum? Classic knapsack disguised as subset problem.
public bool CanPartition(int[] nums) { int sum = 0; foreach (int num in nums) sum += num; // Odd sum: impossible to split equally if (sum % 2 != 0) return false; int target = sum / 2; bool[] dp = new bool[target + 1]; dp[0] = true; foreach (int num in nums) { // Iterate backwards to avoid using same element twice for (int i = target; i >= num; i--) { dp[i] = dp[i] || dp[i - num]; } } return dp[target]; }
Unique Paths (LC 62) and Minimum Path Sum (LC 64)—fundamental 2D DP patterns for grids.
State: dp[i][j] = number of paths from (0,0) to (i,j).
Recurrence: dp[i][j] = dp[i-1][j] + dp[i][j-1] (from top or left).
public int UniquePaths(int m, int n) { int[][] dp = new int[m][]; for (int i = 0; i < m; i++) dp[i] = new int[n]; // Base case: first row and column have only 1 path for (int i = 0; i < m; i++) dp[i][0] = 1; for (int j = 0; j < n; j++) dp[0][j] = 1; // Fill table 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]; } // Minimum Path Sum (LC 64) // State: dp[i][j] = min sum path from (0,0) to (i,j) // Recurrence: dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1]) public int MinPathSum(int[][] grid) { int m = grid.Length; int n = grid[0].Length; int[][] dp = new int[m][]; for (int i = 0; i < m; i++) dp[i] = new int[n]; // Base cases dp[0][0] = grid[0][0]; for (int i = 1; i < m; i++) dp[i][0] = dp[i-1][0] + grid[i][0]; for (int j = 1; j < n; j++) dp[0][j] = dp[0][j-1] + grid[0][j]; for (int i = 1; i < m; i++) { for (int j = 1; j < n; j++) { dp[i][j] = grid[i][j] + Math.Min(dp[i-1][j], dp[i][j-1]); } } return dp[m - 1][n - 1]; }
Edit Distance (LC 72) and Longest Common Subsequence—fundamental string DP problems.
State: dp[i][j] = min operations to convert s1[0..i-1] to s2[0..j-1].
public int MinDistance(string word1, string word2) { int m = word1.Length; int n = word2.Length; int[][] dp = new int[m + 1][]; for (int i = 0; i <= m; i++) dp[i] = new int[n + 1]; // Base cases 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 (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], Math.Min(dp[i][j-1], dp[i-1][j-1])); } } } return dp[m][n]; }
State: dp[i][j] = length of LCS of text1[0..i-1] and text2[0..j-1].
public int LongestCommonSubsequence(string text1, string text2) { int m = text1.Length; int n = text2.Length; int[][] dp = new int[m + 1][]; for (int i = 0; i <= m; i++) dp[i] = new int[n + 1]; for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { if (text1[i - 1] == text2[j - 1]) { dp[i][j] = 1 + dp[i-1][j-1]; } else { dp[i][j] = Math.Max(dp[i-1][j], dp[i][j-1]); } } } return dp[m][n]; }
Classic 0/1 Knapsack with space optimization, and Longest Palindromic Substring (LC 5).
public int Knapsack(int[] weights, int[] values, int capacity) { int[] dp = new int[capacity + 1]; // For each item for (int i = 0; i < weights.Length; i++) { // Iterate backwards to avoid using item twice for (int w = capacity; w >= weights[i]; w--) { dp[w] = Math.Max(dp[w], dp[w - weights[i]] + values[i]); } } return dp[capacity]; } // Longest Palindromic Substring (LC 5) // State: dp[i][j] = true if s[i..j] is palindrome public string LongestPalindrome(string s) { if (s == null || s.Length < 1) return ""; int n = s.Length; bool[][] dp = new bool[n][]; for (int i = 0; i < n; i++) dp[i] = new bool[n]; int start = 0, maxLen = 1; // Single characters are palindromes for (int i = 0; i < n; i++) dp[i][i] = true; // Check palindromes of length 2 and more for (int len = 2; len <= n; len++) { for (int i = 0; i <= n - len; i++) { int j = i + len - 1; if (s[i] == s[j]) { dp[i][j] = (len == 2) || dp[i + 1][j - 1]; if (dp[i][j] && len > maxLen) { start = i; maxLen = len; } } } } return s.Substring(start, maxLen); }
Burst Balloons (LC 312) and Buy Sell Stock with Cooldown (LC 309)—advanced DP patterns for interviews.
State: dp[left][right] = max coins bursting balloons between left and right.
Key Insight: Think backwards—last balloon to burst determines the recurrence. When balloon k is last, left and right aren't adjacent until k bursts.
public int MaxCoins(int[] nums) { if (nums == null || nums.Length == 0) return 0; // Pad with 1s to handle boundary cases int[] balloons = new int[nums.Length + 2]; balloons[0] = 1; balloons[balloons.Length - 1] = 1; for (int i = 0; i < nums.Length; i++) balloons[i + 1] = nums[i]; int n = balloons.Length; int[][] dp = new int[n][]; for (int i = 0; i < n; i++) dp[i] = new int[n]; // len = distance between left and right for (int len = 2; len < n; len++) { for (int left = 0; left + len < n; left++) { int right = left + len; for (int k = left + 1; k < right; k++) { dp[left][right] = Math.Max( dp[left][right], dp[left][k] + balloons[left] * balloons[k] * balloons[right] + dp[k][right] ); } } } return dp[0][n - 1]; } // Buy Sell Stock with Cooldown (LC 309) // State machine: hold[i] = max profit on day i while holding stock // sold[i] = max profit on day i after selling // rest[i] = max profit on day i while not holding (cooling down) public int MaxProfitCooldown(int[] prices) { if (prices == null || prices.Length <= 1) return 0; int hold = -prices[0]; // Buy on day 0 int sold = 0; // No stock sold yet int rest = 0; // No action yet for (int i = 1; i < prices.Length; i++) { int prevHold = hold; int prevSold = sold; int prevRest = rest; // Today: hold stock (prev hold, or buy from rest) hold = Math.Max(prevHold, prevRest - prices[i]); // Today: sold stock (sell from hold) sold = prevHold + prices[i]; // Today: rest (cooldown from sold, or continue rest) rest = Math.Max(prevSold, prevRest); } // Final state: either sold (best) or resting (no stock) return Math.Max(sold, rest); }
Use bitmask to represent visited subsets. Example: Traveling Salesman Problem variant.
Concept: dp[mask][i] = optimal value when visited cities in 'mask' and currently at city i.
// Template: Subset DP with bitmask public int BitmaskDP(int n) { // dp[mask] = answer for subset represented by mask int[] dp = new int[1 << n]; // Iterate through all subsets for (int mask = 0; mask < (1 << n); mask++) { // Check if bit i is set in mask for (int i = 0; i < n; i++) { if (((mask >> i) & 1) == 1) { // Bit i is set int prevMask = mask ^ (1 << i); // Remove bit i dp[mask] = Math.Max(dp[mask], dp[prevMask] + /* value */ 0); } } } return dp[(1 << n) - 1]; // All bits set }
| Pattern | Characteristics | Example |
|---|---|---|
| Linear DP | Sequence of decisions | Climbing Stairs, House Robber |
| Coordinate DP | 2D grid, path counting | Unique Paths, Min Path Sum |
| Knapsack | Bounded selection (0/1, unbounded) | Coin Change, Partition Equal Subset |
| String DP | Subsequence/substring matching | LCS, Edit Distance, LIS |
| Interval DP | Optimal subinterval merging | Burst Balloons, Matrix Chain Mult. |
| State Machine | Multiple states per position | Stock with Cooldown, Max Product |
| Bitmask DP | Subset enumeration | TSP, Steiner Tree |
This chapter covers comprehensive DP solutions:
Climbing Stairs: dp[i] = dp[i-1] + dp[i-2]
House Robber: dp[i] = max(dp[i-1], dp[i-2] + nums[i])
Coin Change: dp[i] = 1 + min(dp[i-coin]) for all coins ≤ i
LIS: dp[i] = 1 + max(dp[j]) for all j < i where nums[j] < nums[i]
Edit Distance: dp[i][j] = dp[i-1][j-1] if s1[i-1]==s2[j-1], else 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
Knapsack: dp[w] = max(dp[w], dp[w-weight] + value)
Unique Paths: dp[i][j] = dp[i-1][j] + dp[i][j-1]
Communicate your thinking: "This problem has overlapping subproblems (fib(3) appears twice in fib(5)). Optimal substructure exists: shortest path A→C contains shortest A→B and B→C. Let me build a DP solution..."
State definition clarity: "I'll define dp[i] as the minimum coins needed to make amount i. Base case: dp[0] = 0. For each amount i, I try all coins..."
Optimization discussion: "This memoization approach is O(n) time but uses O(n) space. We could further optimize space to O(1) by tracking only prev2 and prev1 variables..."
Private helper function with clear parameter meaning: Use private int DP(int index, bool holding) rather than private int DP(int x, int y). Clarity matters in interviews.
Boundary condition checks first: Always validate null/empty input before accessing arrays.
Comment recurrence logic: Interviewers appreciate seeing you understand the transition, not just the code.
Test with examples: Walk through a small example (n=2 or n=3) while coding. This catches off-by-one errors.
Topics beyond this chapter's scope (but good for advanced interview prep):
When original dp is O(n²): If dp[i][j] depends only on dp[i-1][...], use rolling array (swap two 1D arrays each iteration).
When original dp is O(n): If dp[i] depends only on dp[i-1] and dp[i-2], use two variables.
Trade-off consideration: Optimization makes code harder to read. In interview, implement correct O(n²) solution first; offer space optimization as enhancement if time permits.
Count ways: dp[i] += (number of ways to reach i). Sum transitions.
Maximum value: dp[i] = max(dp[i], candidate). Compare transitions.
Minimum value: dp[i] = min(dp[i], candidate). Choose smallest transition.
Existence (true/false): dp[i] = OR of (all valid transitions). Boolean OR logic.
Greedy vs DP: Greedy optimal locally (never revisit choice). DP explores all paths. Use DP when greedy fails.
Divide & Conquer vs DP: D&C solves non-overlapping subproblems (mergesort). DP memoizes overlapping subproblems. Check for overlap.
BFS/DFS vs DP: Graph shortest path? Use BFS/Dijkstra. Counting paths with constraints? DP is cleaner.
Print table during execution: Add logging to see how dp table fills. Helps verify recurrence is correct.
Test edge cases: n=0, n=1, empty array, single element, all same value.
Verify base cases: Manually compute dp[0], dp[1], dp[2] by hand. Match your code output.
Check off-by-one: Are you using 0-indexed or 1-indexed? Verify loop bounds: i < n vs i <= n.
Memory limits: For n=10^5, O(n²) space = 10^10 bytes = ~9 GB (too much). Use space optimization.
Fibonacci naive recursion: fib(40) takes ~10 seconds (exponential branches).
Fibonacci with memoization: fib(40) takes microseconds (compute each once).
Fibonacci iterative DP: fib(40) takes microseconds, no recursion stack risk.
This demonstrates why DP is essential for interview performance—naive recursion will timeout.
Can you recognize these as DP problems?
Before submitting:
| Problem | States | Transitions | Total Time |
|---|---|---|---|
| 1D DP (climb, rob) | O(n) | O(1) | O(n) |
| Coin Change | O(amount) | O(coins) | O(amount·coins) |
| 2D Grid Paths | O(m·n) | O(1) | O(m·n) |
| LCS/Edit Distance | O(m·n) | O(1) | O(m·n) |
| LIS O(n²) | O(n) | O(n) | O(n²) |
| LIS O(n log n) | O(n) | O(log n) | O(n log n) |
| Bitmask DP | O(2ⁿ) | O(n) | O(n·2ⁿ) |
Memoization (Top-Down): Natural recursion, smaller state space needed, interview clarity preferred.
Tabulation (Bottom-Up): Avoiding recursion depth limit, full state space needed, production code optimization.
Space Optimization: When you only need previous row/column or constant space. Always measure benefit vs. code clarity.
Bioinformatics: Sequence alignment (DNA/protein matching) uses edit distance DP extensively.
Natural Language Processing: Language models use DP for part-of-speech tagging (Viterbi algorithm).
Computer Vision: Image segmentation and object tracking rely on dynamic programming principles.
Operations Research: Supply chain optimization, resource allocation use integer programming (DP-based).
Compiler Design: Grammar parsing uses DP (CYK algorithm for context-free languages).
Dynamic Programming is not just a coding technique—it's a problem-solving philosophy:
Mastering DP elevates your algorithmic thinking. You stop trying random approaches and systematically identify problem structure, define state, write recurrence. This discipline transfers across all coding interviews.
Week 1 - Foundations: Master Fibonacci, Climbing Stairs, simple 1D DP.
Week 2 - Classic Problems: House Robber, Coin Change, LIS. Understand both memoization and tabulation.
Week 3 - 2D DP: Grid paths, edit distance, LCS. Get comfortable with 2D table reasoning.
Week 4 - Optimization: Space optimization, binary search DP, more complex transitions.
Week 5 - Advanced: State machines (stock trading), interval DP (burst balloons), bitmask DP.
Week 6 - Mock Interviews: Solve new problems under time pressure. Practice explaining your solution.
You now have:
With this foundation, you're equipped to tackle DP problems confidently in technical interviews. Remember: when you see "find maximum/minimum/count," think DP.
| Problem Name | LeetCode # | Difficulty | Section | Key Concept |
|---|---|---|---|---|
| Climbing Stairs | 70 | Easy | 12.5 | 1D DP, simple recurrence |
| House Robber I | 198 | Easy | 12.6 | 1D DP, max choice |
| House Robber II | 213 | Medium | 12.6 | 1D DP, circular variant |
| Coin Change | 322 | Medium | 12.7 | Unbounded knapsack, min |
| Longest Increasing Subsequence | 300 | Medium | 12.8 | O(n²) and O(n log n) approaches |
| Word Break | 139 | Medium | 12.9 | String segmentation DP |
| Decode Ways | 91 | Medium | 12.9 | String counting DP |
| Maximum Product Subarray | 152 | Medium | 12.10 | State machine (max/min tracking) |
| Perfect Squares | 279 | Medium | 12.10 | Unbounded knapsack variant |
| Partition Equal Subset Sum | 416 | Medium | 12.10 | 0/1 Knapsack as boolean |
| Unique Paths | 62 | Medium | 12.11 | 2D DP, counting paths |
| Minimum Path Sum | 64 | Medium | 12.11 | 2D DP, minimum cost path |
| Edit Distance | 72 | Hard | 12.12 | 2D string DP, 3-way transitions |
| Longest Common Subsequence | 1143 | Medium | 12.12 | 2D string DP, 2-way transitions |
| Longest Palindromic Substring | 5 | Medium | 12.13 | 2D DP, interval check |
| Burst Balloons | 312 | Hard | 12.14 | Interval DP, last element thinking |
| Buy Sell Stock with Cooldown | 309 | Medium | 12.14 | State machine, 3 states |
Maximization: dp[i] = maximum value achievable up to i. Example: max profit from robbing houses.
Minimization: dp[i] = minimum cost/operations to reach i. Example: minimum coins for amount.
Counting: dp[i] = number of ways to achieve state i. Example: number of paths in grid.
Boolean: dp[i] = whether state i is reachable. Example: can word be segmented?
2D State: dp[i][j] = answer for subproblem with indices i and j. Example: dp[i][j] = LCS of first i and j characters.
3D State: dp[i][j][k] = answer with three independent dimensions. Example: stock buying with k transactions allowed.
Linear (single transition): dp[i] = f(dp[i-1]). Simple chain.
Tree (multiple transitions): dp[i] = max/min(dp[i-1], dp[i-2], ...). Choose best among options.
Aggregate (sum): dp[i] = sum(dp[all valid j]). Count all possibilities.
Conditional: dp[i] = dp[i-1] if condition, else dp[i-2] + cost. Decision based on matching.
2D recurrence: dp[i][j] depends on dp[i-1][j], dp[i][j-1], dp[i-1][j-1]. Multi-directional.
Zero-amount (knapsack): dp[0] = 0 (zero cost for zero amount).
Zero-length (string): dp[0] = 1 (one way to match empty string).
Single element (sequence): dp[1] = nums[0] or 1 (base step choice).
Impossible states: dp[i] = -1 or Integer.MAX_VALUE for unreachable states.
Grid boundaries: dp[0][0] = grid[0][0], dp[i][0] = sum of column 0, dp[0][j] = sum of row 0.
Forward iteration (i from 0 to n): Current state uses previous states. Example: dp[i] = f(dp[0..i-1]).
Backward iteration (i from n to 0): Used in 0/1 knapsack to avoid item reuse. Iterate weights backward.
Diagonal iteration: For interval DP, iterate by length (len = 2, 3, ..., n). Example: Burst Balloons.
Layered iteration: Outer loop by dimension 1, inner by dimension 2. Example: 2D grid problems.
Do you need entire DP table for other operations? Yes → Tabulation. No → Memoization.
Is recursion depth a concern (n > 10,000)? Yes → Tabulation (avoid stack overflow). No → Either works.
Is state space sparse (not all states needed)? Yes → Memoization (compute only needed). No → Tabulation.
Do you want to optimize memory late? Yes → Tabulation (easier to optimize after). No → Memoization for clarity.
Failure: TLE (Time Limit Exceeded): State space too large or transition too slow. Optimize recurrence or use advanced techniques (Convex Hull Trick).
Failure: MLE (Memory Limit Exceeded): DP table too large. Use space optimization (rolling array, variables).
Failure: Wrong Answer: Recurrence incorrect. Verify with hand example. Check base cases.
Failure: Stack Overflow: Recursion depth too deep. Switch to tabulation or increase stack size.
Failure: Integer Overflow: Intermediate values exceed int range. Use long or BigInteger.
By dimension:
By optimization goal:
By input type:
✓ Clear variable naming: "dp[i]" → "minCoinsToBuyAmount[i]". Be explicit.
✓ Correct base cases: Every DP solution must start correctly. Verify manually.
✓ Proper loop bounds: Off-by-one errors are common. Use i < n vs i <= n consistently.
✓ Efficient transitions: Recurrence should be O(1) per state if possible. Avoid nested loops inside transitions.
✓ Input validation: Handle null, empty, negative inputs gracefully.
✓ Complexity analysis: State the time and space complexity clearly.
✓ Readability: Comments on the recurrence logic. Code should be understandable without explanation.
✗ Overly optimized code: Balance optimization with clarity. A slow but correct solution beats a fast but wrong one.
Beginner: Start with 1D DP on single arrays (Climbing Stairs, House Robber). Implement both memoization and tabulation.
Intermediate: Move to 2D DP (Edit Distance, LCS). Solve knapsack variants. Understand space optimization.
Advanced: Tackle interval DP (Burst Balloons), state machines (Stock trading), bitmask DP (subset enumeration).
Expert: Study advanced optimization techniques (Convex Hull Trick, monotone queue DP, digit DP). Solve Codeforces rounds.
C++: Use vector<int> or array. Unordered_map for memoization. Fast execution.
Python: Use list or dict. @lru_cache decorator for automatic memoization. Great for prototyping.
Java: Use int[] or HashMap. Verbose but safe type system. Good for large-scale systems.
C#: Dictionary<K,V> for memoization, int[] for tabulation. Similar to Java.
JavaScript: Use Map or object. Flexible but beware of performance on large inputs.
Solve at least 2-3 problems from each category (1D, 2D, knapsack, string, interval, state machine). Aim for fluency: able to code solution in 20 minutes without reference. After mastery, continue solving new problems—DP patterns keep surprising even experts!