name: Dynamic Programming slug: dynamic-programming difficulty_level: 4 pattern_type: technique display_order: 11 description: > Break problems into overlapping subproblems, storing results to avoid recomputation. This transforms exponential time complexity into polynomial by trading space for time. when_to_use: | - Optimization problems (min/max) - Counting problems - Problems with optimal substructure - Sequence alignment - Knapsack-type problems metaphor: | Imagine building with LEGO bricks. Instead of reconstructing the same base structure every time you try a new top, you save your work. Each completed substructure becomes a building block for larger structures. Another analogy: calculating Fibonacci numbers. To find fib(5), you need fib(4) and fib(3). But fib(4) also needs fib(3). Rather than recalculating fib(3) twice, save it the first time and reuse it. core_concept: | Dynamic programming requires two properties: 1. **Optimal substructure**: The optimal solution contains optimal solutions to its subproblems. 2. **Overlapping subproblems**: The same subproblems are solved multiple times in a naive recursive approach. The key insight is identifying the **state**—what information do you need to solve a subproblem? And the **transition**—how do you combine smaller subproblems into larger ones? Two implementation approaches: - **Top-down (memoization)**: Recursive with caching - **Bottom-up (tabulation)**: Iterative, filling a table from base cases visualization: | **Example: Fibonacci with memoization** ``` Without memoization (exponential calls): fib(5) / \ fib(4) fib(3) / \ / \ fib(3) fib(2) fib(2) fib(1) / \ fib(2) fib(1) ... With memoization: fib(5) → fib(4) → fib(3) → fib(2) → fib(1) ↓ ↓ use cached use cached fib(3) fib(2) ``` **Example: Coin Change (minimum coins for amount 11)** ``` Coins: [1, 5, 6] Amount: 11 dp[0] = 0 (base case: 0 coins for amount 0) dp[1] = dp[0] + 1 = 1 (use coin 1) dp[5] = min(dp[4]+1, dp[0]+1) = 1 (use coin 5) dp[6] = min(dp[5]+1, dp[0]+1) = 1 (use coin 6) dp[11] = min(dp[10]+1, dp[6]+1, dp[5]+1) = min(?, 2, 3) = 2 (6 + 5) ``` code_template: | # Top-down (memoization) from functools import lru_cache def solve_top_down(n: int) -> int: @lru_cache(maxsize=None) def dp(state): # Base case if base_condition(state): return base_value # Recursive case with memoization result = initial_value for choice in choices(state): subproblem = dp(next_state(state, choice)) result = combine(result, subproblem) return result return dp(initial_state(n)) # Bottom-up (tabulation) def solve_bottom_up(n: int) -> int: # Initialize DP table dp = [initial_value] * (n + 1) # Base case dp[0] = base_value # Fill table iteratively for i in range(1, n + 1): for choice in choices(i): if valid(i, choice): dp[i] = combine(dp[i], dp[prev_state(i, choice)]) return dp[n] # 2D DP example (Longest Common Subsequence) def lcs(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] recognition_signals: - "minimum/maximum" - "count ways" - "can you reach" - "optimal" - "longest/shortest" - "number of ways" - "subset sum" - "partition" - "knapsack" - "sequence" - "subsequence" common_mistakes: - title: Incorrect state definition description: | Choosing a state that doesn't capture all necessary information leads to incorrect transitions or missing cases. fix: | Ask: "What do I need to know to solve this subproblem?" The answer defines your state. Test with small examples to verify. - title: Wrong base case description: | Incorrect initialization causes wrong answers to propagate through the entire DP table. fix: | Think about the smallest/simplest subproblem. What's the answer when there's nothing left to consider? Start from there. - title: Off-by-one in 2D DP description: | Confusion about whether dp[i] represents the first i elements or the element at index i causes index errors. fix: | Be consistent. Common convention: dp[i] = answer for first i elements, so dp[0] = empty case. Indices in strings/arrays are 0-based. - title: Forgetting to handle impossible cases description: | Not returning infinity for minimum problems or 0 for counting when a state is unreachable gives wrong aggregations. fix: | Initialize dp with appropriate "impossible" values (infinity for min, -infinity for max, 0 for counting). Return -1 if final answer is still impossible. - title: Space complexity not optimized description: | Using O(n*m) space when only the previous row/column is needed wastes memory on large inputs. fix: | If dp[i] only depends on dp[i-1], use two arrays (current and previous) or even a single array updated carefully. variations: - name: 1D DP description: | Single dimension state, typically indexed by position or remaining capacity. Common for linear sequences. example: "Climbing Stairs, House Robber, Coin Change" - name: 2D DP description: | Two-dimensional state, often for comparing two sequences or tracking two variables (position and capacity). example: "Longest Common Subsequence, Edit Distance, 0/1 Knapsack" - name: Interval DP description: | State represents a range [i, j]. Solve for all subranges and combine. Often O(n^3) time. example: "Burst Balloons, Matrix Chain Multiplication" - name: Bitmask DP description: | State includes a bitmask representing a subset. Used when order matters among a small set of items. example: "Traveling Salesman, Shortest Superstring" - name: DP on Trees description: | State associated with tree nodes. Transition from children to parent (or vice versa). example: "House Robber III, Binary Tree Maximum Path Sum" related_patterns: - greedy - backtracking prerequisite_patterns: - backtracking