feat(patterns): tutorial system
This commit is contained in:
224
backend/data/patterns/dynamic-programming.yaml
Normal file
224
backend/data/patterns/dynamic-programming.yaml
Normal file
@@ -0,0 +1,224 @@
|
||||
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
|
||||
Reference in New Issue
Block a user