225 lines
6.6 KiB
YAML
225 lines
6.6 KiB
YAML
name: Dynamic Programming
|
|
slug: dynamic-programming
|
|
difficulty_level: 4
|
|
|
|
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
|