feat(patterns): strategy tutorials
This commit is contained in:
299
backend/data/patterns/greedy.yaml
Normal file
299
backend/data/patterns/greedy.yaml
Normal 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: []
|
||||
Reference in New Issue
Block a user