From f6d4bc3a03c72a9efc556ac921678f323fd5cbfb Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Sat, 23 Aug 2025 19:58:33 +0100 Subject: [PATCH] feat(patterns): strategy tutorials --- backend/data/patterns/backtracking.yaml | 331 ++++++++++++++++++++++++ backend/data/patterns/greedy.yaml | 299 +++++++++++++++++++++ 2 files changed, 630 insertions(+) create mode 100644 backend/data/patterns/backtracking.yaml create mode 100644 backend/data/patterns/greedy.yaml diff --git a/backend/data/patterns/backtracking.yaml b/backend/data/patterns/backtracking.yaml new file mode 100644 index 0000000..140836d --- /dev/null +++ b/backend/data/patterns/backtracking.yaml @@ -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 diff --git a/backend/data/patterns/greedy.yaml b/backend/data/patterns/greedy.yaml new file mode 100644 index 0000000..3680638 --- /dev/null +++ b/backend/data/patterns/greedy.yaml @@ -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: []