feat(patterns): tutorial system

This commit is contained in:
2025-08-07 00:41:51 +01:00
parent 3fd3021d6e
commit deb2f64ea7
15 changed files with 1386 additions and 45 deletions

View 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