feat(patterns): strategy tutorials

This commit is contained in:
2025-08-23 19:58:33 +01:00
parent 7bf6d1f472
commit 5fa210cc8e
2 changed files with 630 additions and 0 deletions

View File

@@ -0,0 +1,331 @@
name: Backtracking
slug: backtracking
difficulty_level: 3
description: >
Build solutions incrementally, exploring choices one at a time and abandoning
paths that fail to satisfy constraints. Backtracking systematically explores
all possibilities by making a choice, recursing, then undoing the choice.
when_to_use: |
- Generating all permutations or combinations
- Constraint satisfaction (Sudoku, N-Queens)
- Subset/partition problems
- Path finding with constraints
- Word search in grids
metaphor: |
Imagine navigating a maze by always trying the left path first. When you hit
a dead end, you backtrack to the last intersection and try the next option.
You systematically explore every possible route, backing up whenever you reach
an invalid state.
Another analogy: filling out a Sudoku puzzle. You try a number in an empty cell.
If it leads to a contradiction later, you erase it (backtrack) and try the next
number. If no number works, you backtrack further to a previous cell.
core_concept: |
Backtracking is a refined brute force that prunes invalid branches early.
The pattern follows a template:
1. **Choose**: Make a choice (add element to path, place a queen, etc.)
2. **Explore**: Recurse to explore consequences of that choice
3. **Unchoose**: Undo the choice (backtrack) to try alternatives
The key insight is that we build a **decision tree** where each node represents
a state and edges represent choices. Backtracking is a DFS of this tree, with
pruning of subtrees that can't lead to valid solutions.
**Pruning** is what makes backtracking efficient. By detecting invalid states
early, we avoid exploring exponentially many useless branches.
visualization: |
**Generating subsets of [1, 2, 3]:**
```
Decision tree (include or exclude each element):
[]
/ \
[1] []
/ \ / \
[1,2] [1] [2] []
/ \ / \ / \ / \
[1,2,3][1,2][1,3][1][2,3][2][3][]
Subsets: [], [1], [2], [3], [1,2], [1,3], [2,3], [1,2,3]
```
**N-Queens (N=4) with pruning:**
```
Place queens row by row, checking column and diagonal conflicts:
Row 0: Try col 0
Row 1: col 0 ❌ (same col)
col 1 ❌ (diagonal)
col 2 ✓
Row 2: col 0 ❌ (diagonal)
col 1 ❌ (col 1 attacks via diagonal)
col 2 ❌ (same col)
col 3 ❌ (diagonal)
Backtrack!
Row 1: col 3 ✓
Row 2: col 1 ✓
Row 3: col 0 ❌
col 1 ❌
col 2 ❌
col 3 ❌
Backtrack!
...
Eventually find: [1, 3, 0, 2] (queens at cols 1,3,0,2 for rows 0,1,2,3)
```
**Template visualization:**
```
backtrack(state):
if is_solution(state):
record_solution(state)
return
for choice in get_choices(state):
if is_valid(choice, state): ← PRUNE
make_choice(choice, state) ← CHOOSE
backtrack(state) ← EXPLORE
undo_choice(choice, state) ← UNCHOOSE
```
code_template: |
def generate_subsets(nums: list[int]) -> list[list[int]]:
"""Generate all subsets (power set)."""
result = []
def backtrack(start: int, path: list[int]):
result.append(path[:]) # Record current subset
for i in range(start, len(nums)):
path.append(nums[i]) # Choose
backtrack(i + 1, path) # Explore
path.pop() # Unchoose
backtrack(0, [])
return result
def generate_permutations(nums: list[int]) -> list[list[int]]:
"""Generate all permutations."""
result = []
used = [False] * len(nums)
def backtrack(path: list[int]):
if len(path) == len(nums):
result.append(path[:])
return
for i in range(len(nums)):
if used[i]:
continue
used[i] = True # Choose
path.append(nums[i])
backtrack(path) # Explore
path.pop() # Unchoose
used[i] = False
backtrack([])
return result
def combination_sum(candidates: list[int], target: int) -> list[list[int]]:
"""Find combinations that sum to target (can reuse elements)."""
result = []
def backtrack(start: int, path: list[int], remaining: int):
if remaining == 0:
result.append(path[:])
return
for i in range(start, len(candidates)):
if candidates[i] > remaining: # Prune
continue
path.append(candidates[i])
backtrack(i, path, remaining - candidates[i]) # Can reuse
path.pop()
candidates.sort() # Enable pruning
backtrack(0, [], target)
return result
def solve_n_queens(n: int) -> list[list[str]]:
"""Find all valid N-Queens solutions."""
result = []
cols = set()
diag1 = set() # row - col
diag2 = set() # row + col
def backtrack(row: int, queens: list[int]):
if row == n:
# Convert to board representation
board = []
for c in queens:
board.append('.' * c + 'Q' + '.' * (n - c - 1))
result.append(board)
return
for col in range(n):
if col in cols or (row - col) in diag1 or (row + col) in diag2:
continue # Prune: under attack
# Choose
cols.add(col)
diag1.add(row - col)
diag2.add(row + col)
queens.append(col)
backtrack(row + 1, queens) # Explore
# Unchoose
queens.pop()
cols.remove(col)
diag1.remove(row - col)
diag2.remove(row + col)
backtrack(0, [])
return result
def word_search(board: list[list[str]], word: str) -> bool:
"""Check if word exists in grid following adjacent cells."""
rows, cols = len(board), len(board[0])
def backtrack(r: int, c: int, i: int) -> bool:
if i == len(word):
return True
if (r < 0 or r >= rows or c < 0 or c >= cols
or board[r][c] != word[i]):
return False
# Choose: mark as visited
temp = board[r][c]
board[r][c] = '#'
# Explore neighbors
found = (backtrack(r + 1, c, i + 1) or
backtrack(r - 1, c, i + 1) or
backtrack(r, c + 1, i + 1) or
backtrack(r, c - 1, i + 1))
# Unchoose: restore
board[r][c] = temp
return found
for r in range(rows):
for c in range(cols):
if backtrack(r, c, 0):
return True
return False
recognition_signals:
- "all permutations"
- "all combinations"
- "all subsets"
- "generate all"
- "N-Queens"
- "Sudoku"
- "word search"
- "partition"
- "constraint satisfaction"
- "valid arrangements"
common_mistakes:
- title: Forgetting to unchoose (backtrack)
description: |
Not undoing the choice after exploring leaves state modified, causing
incorrect results for subsequent branches.
fix: |
Always pair choose with unchoose:
```python
path.append(choice) # Choose
backtrack(...) # Explore
path.pop() # Unchoose - MUST DO!
```
- title: Modifying state without copying
description: |
Adding the same path object to results multiple times—they all reference
the same list that keeps changing.
fix: |
Copy the path when recording:
```python
result.append(path[:]) # Shallow copy
# or
result.append(list(path))
```
- title: Not pruning effectively
description: |
Checking validity only at leaf nodes means exploring many invalid
branches that could have been cut early.
fix: |
Validate as early as possible:
```python
for choice in choices:
if is_valid(choice): # Prune BEFORE recursing
make_choice(choice)
backtrack(...)
```
- title: Wrong base case
description: |
Recording partial solutions as complete, or not recognizing when a
complete solution is reached.
fix: |
Clearly define what constitutes a complete solution:
- Permutations: path length equals input length
- Subsets: (all indices are complete solutions)
- N-Queens: placed N queens
variations:
- name: Subsets
description: |
Generate all subsets. Each element is either included or not. Record
at every node, not just leaves.
example: "Subsets, Subsets II (with duplicates)"
- name: Permutations
description: |
Generate all orderings. Track which elements are used. Record only at
leaves (when all elements used).
example: "Permutations, Permutations II"
- name: Combinations
description: |
Generate subsets of specific size k, or combinations that meet a target
sum. Use start index to avoid duplicates.
example: "Combinations, Combination Sum"
- name: Constraint satisfaction
description: |
Place elements satisfying constraints (non-attacking queens, valid
Sudoku). Heavy pruning based on constraints.
example: "N-Queens, Sudoku Solver"
- name: Path finding
description: |
Find paths in grids or graphs, marking visited cells temporarily.
Unmark when backtracking.
example: "Word Search, Unique Paths III"
related_patterns:
- dfs
- dynamic-programming
prerequisite_patterns:
- dfs

View File

@@ -0,0 +1,299 @@
name: Greedy
slug: greedy
difficulty_level: 3
description: >
Make locally optimal choices at each step, hoping to find a global optimum.
Greedy algorithms are simple and efficient but only work when the problem
has the greedy choice property—local optima lead to global optimum.
when_to_use: |
- Interval scheduling (activity selection)
- Huffman coding
- Minimum spanning tree (Prim's, Kruskal's)
- Shortest path with non-negative weights (Dijkstra's)
- Fractional knapsack
metaphor: |
Imagine eating at a buffet where you can only fill your plate once. The greedy
strategy: always take the food that looks most appealing right now. This works
if what looks best now is actually best overall—but fails if you fill up on
appetizers and miss the main course.
Another analogy: making change with the fewest coins. For US currency, always
using the largest coin that fits (quarter before dime before nickel) gives
optimal results. But with coins [1, 3, 4], making 6 cents: greedy gives
4+1+1=3 coins, while optimal is 3+3=2 coins.
core_concept: |
Greedy algorithms work by making the choice that seems best **at each step**
without reconsidering previous choices. This works when:
1. **Greedy choice property**: A locally optimal choice is part of some
globally optimal solution.
2. **Optimal substructure**: After making a greedy choice, the remaining
subproblem has the same structure as the original.
The key insight is recognizing when greedy works. Common patterns:
- **Sort by deadline/end time** for scheduling problems
- **Sort by ratio** (value/weight) for selection problems
- **Always pick the nearest/smallest/largest** when monotonicity guarantees optimality
When greedy doesn't work (like 0/1 Knapsack), use dynamic programming instead.
visualization: |
**Activity Selection (maximize non-overlapping activities):**
```
Activities: [(1,4), (3,5), (0,6), (5,7), (3,9), (5,9), (6,10), (8,11)]
Sort by end time: [(1,4), (3,5), (0,6), (5,7), (3,9), (5,9), (6,10), (8,11)]
Greedy selection:
- (1,4): Select ✓ (first activity)
- (3,5): Skip ✗ (overlaps with (1,4))
- (0,6): Skip ✗ (overlaps)
- (5,7): Select ✓ (starts after 4)
- (3,9): Skip ✗ (overlaps)
- (5,9): Skip ✗ (overlaps)
- (6,10): Skip ✗ (overlaps with (5,7))
- (8,11): Select ✓ (starts after 7)
Selected: [(1,4), (5,7), (8,11)] — 3 activities
```
**Why sort by end time?**
```
Intuition: Finishing early leaves maximum room for future activities.
If we picked an activity ending later but overlapping with one ending earlier:
- We'd block the same activities (both overlap with them)
- But we'd also potentially block more future activities
- So the earlier-ending activity is never worse
```
**Jump Game (can reach end?):**
```
Array: [2, 3, 1, 1, 4]
Greedy: Track farthest reachable position
i=0: farthest = max(0, 0+2) = 2
i=1: farthest = max(2, 1+3) = 4 ← can reach end!
i=2: farthest = max(4, 2+1) = 4
i=3: farthest = max(4, 3+1) = 4
i=4: reached end ✓
```
code_template: |
def activity_selection(activities: list[tuple[int, int]]) -> list[tuple]:
"""Select maximum non-overlapping activities."""
# Sort by end time
activities.sort(key=lambda x: x[1])
result = [activities[0]]
last_end = activities[0][1]
for start, end in activities[1:]:
if start >= last_end: # No overlap
result.append((start, end))
last_end = end
return result
def can_jump(nums: list[int]) -> bool:
"""Check if you can reach the last index."""
farthest = 0
for i in range(len(nums)):
if i > farthest:
return False # Can't reach this position
farthest = max(farthest, i + nums[i])
return True
def min_jumps(nums: list[int]) -> int:
"""Minimum jumps to reach the last index."""
if len(nums) <= 1:
return 0
jumps = 0
current_end = 0
farthest = 0
for i in range(len(nums) - 1):
farthest = max(farthest, i + nums[i])
if i == current_end:
jumps += 1
current_end = farthest
return jumps
def fractional_knapsack(capacity: int,
items: list[tuple[int, int]]) -> float:
"""Maximum value with fractional items. Items are (value, weight)."""
# Sort by value-to-weight ratio (descending)
items.sort(key=lambda x: x[0] / x[1], reverse=True)
total_value = 0
for value, weight in items:
if capacity >= weight:
total_value += value
capacity -= weight
else:
# Take fraction of this item
total_value += value * (capacity / weight)
break
return total_value
def min_meeting_rooms(intervals: list[list[int]]) -> int:
"""Minimum meeting rooms needed."""
events = []
for start, end in intervals:
events.append((start, 1)) # Start event
events.append((end, -1)) # End event
events.sort()
rooms = 0
max_rooms = 0
for _, delta in events:
rooms += delta
max_rooms = max(max_rooms, rooms)
return max_rooms
def partition_labels(s: str) -> list[int]:
"""Partition string so each letter appears in at most one part."""
last = {c: i for i, c in enumerate(s)}
partitions = []
start = end = 0
for i, c in enumerate(s):
end = max(end, last[c]) # Extend partition to include all of char c
if i == end: # Reached end of partition
partitions.append(end - start + 1)
start = i + 1
return partitions
def gas_station(gas: list[int], cost: list[int]) -> int:
"""Find starting station to complete circuit."""
total_surplus = 0
current_surplus = 0
start = 0
for i in range(len(gas)):
total_surplus += gas[i] - cost[i]
current_surplus += gas[i] - cost[i]
if current_surplus < 0:
# Can't reach next station from current start
start = i + 1
current_surplus = 0
return start if total_surplus >= 0 else -1
recognition_signals:
- "maximum/minimum number"
- "interval scheduling"
- "activity selection"
- "jump game"
- "gas station"
- "partition"
- "assign"
- "optimal"
- "greedy"
- "earliest/latest"
- "most/least"
common_mistakes:
- title: Applying greedy when it doesn't work
description: |
Not all optimization problems have the greedy choice property. Using
greedy on 0/1 Knapsack or Coin Change (with arbitrary coins) gives
suboptimal results.
fix: |
Verify greedy works by proving the greedy choice property, or test
against known cases. When in doubt, use dynamic programming.
- title: Wrong sorting criteria
description: |
Sorting by the wrong attribute (e.g., start time instead of end time
for activity selection) leads to suboptimal selections.
fix: |
Think about what greedy property you're exploiting. For "maximize
activities," ending early maximizes remaining time. For "minimize
lateness," sorting by deadline helps.
- title: Not handling edge cases
description: |
Empty input, single element, or already-solved cases often need
special handling.
fix: |
Check for edge cases before main algorithm:
```python
if not items:
return 0
if len(items) == 1:
return items[0]
```
- title: Greedy from wrong direction
description: |
Sometimes greedy works forward but not backward (or vice versa).
Processing in the wrong order gives wrong results.
fix: |
Consider both directions. For interval problems, usually sort by end
time and process forward. For some problems, working backward reveals
the greedy choice more clearly.
variations:
- name: Activity/interval selection
description: |
Select maximum non-overlapping intervals. Sort by end time, greedily
select if no overlap with previous.
example: "Activity Selection, Non-overlapping Intervals"
- name: Jump/reach problems
description: |
Track farthest reachable position, greedily extend reach.
example: "Jump Game, Jump Game II, Video Stitching"
- name: Assignment problems
description: |
Match items greedily based on some criteria (smallest to smallest,
largest to largest, etc.).
example: "Assign Cookies, Boats to Save People"
- name: Scheduling
description: |
Schedule tasks to minimize lateness or maximize throughput. Often
involves sorting by deadline or duration.
example: "Task Scheduler, Meeting Rooms, Car Pooling"
- name: Huffman coding
description: |
Greedily merge two lowest-frequency nodes to build optimal prefix-free
encoding tree.
example: "Huffman Coding (not on LeetCode, but classic)"
related_patterns:
- dynamic-programming
- intervals
prerequisite_patterns: []