From 1b7088c5c1067a9da505a8534ff9f2032afd0620 Mon Sep 17 00:00:00 2001 From: lufftw Date: Sun, 14 Dec 2025 13:05:21 +0800 Subject: [PATCH 01/11] docs: enhance backtracking_exploration pattern documentation - Add comprehensive core concepts section in _header.md - Expand _templates.md with detailed explanations for each template - Include usage scenarios, complexity analysis, and LeetCode problems - Add template variants (duplicates handling, constraints, etc.) --- .../intuition.md} | 0 .../backtracking_exploration/templates.md | 608 +++++++ .../backtracking_exploration_templates.md | 1409 ----------------- .../backtracking_exploration/_config.toml | 23 + .../backtracking_exploration/_header.md | 101 ++ .../backtracking_exploration/_templates.md | 490 ++++++ tools/generate_pattern_docs.py | 16 +- tools/generate_pattern_docs.toml | 4 +- tools/patterndocs/files.py | 5 +- 9 files changed, 1242 insertions(+), 1414 deletions(-) rename docs/patterns/{backtracking_exploration_intuition.md => backtracking_exploration/intuition.md} (100%) create mode 100644 docs/patterns/backtracking_exploration/templates.md delete mode 100644 docs/patterns/backtracking_exploration_templates.md create mode 100644 meta/patterns/backtracking_exploration/_config.toml create mode 100644 meta/patterns/backtracking_exploration/_header.md create mode 100644 meta/patterns/backtracking_exploration/_templates.md diff --git a/docs/patterns/backtracking_exploration_intuition.md b/docs/patterns/backtracking_exploration/intuition.md similarity index 100% rename from docs/patterns/backtracking_exploration_intuition.md rename to docs/patterns/backtracking_exploration/intuition.md diff --git a/docs/patterns/backtracking_exploration/templates.md b/docs/patterns/backtracking_exploration/templates.md new file mode 100644 index 0000000..912b0f1 --- /dev/null +++ b/docs/patterns/backtracking_exploration/templates.md @@ -0,0 +1,608 @@ +# Backtracking Exploration Patterns: Complete Reference + +> **API Kernel**: `BacktrackingExploration` +> **Core Mechanism**: Systematically explore all candidate solutions by building them incrementally, abandoning paths that violate constraints (pruning), and undoing choices to try alternatives. + +This document presents the **canonical backtracking template** and all its major variations. Each implementation follows consistent naming conventions and includes detailed algorithmic explanations. + +--- + +## Table of Contents + +1. [Core Concepts](#1-core-concepts) +2. [Template Quick Reference](#2-template-quick-reference) + +--- + +## 1. Core Concepts + +### 1.1 The Backtracking Process + +Backtracking is a systematic search technique that builds solutions incrementally and abandons partial solutions that cannot lead to valid complete solutions. + +``` +Backtracking State: +┌─────────────────────────────────────────────────────────┐ +│ [choice₁] → [choice₂] → [choice₃] → ... → [choiceₙ] │ +│ │ │ │ │ │ +│ └───────────┴───────────┴──────────────┘ │ +│ Path (current partial solution) │ +│ │ +│ When constraint violated: │ +│ Backtrack: undo last choice, try next alternative │ +└─────────────────────────────────────────────────────────┘ +``` + +### 1.2 Universal Template Structure + +```python +def backtracking_template(problem_state): + """ + Generic backtracking template. + + Key components: + 1. Base Case: Check if current path is a complete solution + 2. Pruning: Abandon paths that violate constraints + 3. Choices: Generate all valid choices at current state + 4. Make Choice: Add choice to path, update state + 5. Recurse: Explore further with updated state + 6. Backtrack: Undo choice, restore state + """ + results = [] + + def backtrack(path, state): + # BASE CASE: Check if solution is complete + if is_complete(path, state): + results.append(path[:]) # Copy path + return + + # PRUNING: Abandon invalid paths early + if violates_constraints(path, state): + return + + # CHOICES: Generate all valid choices + for choice in generate_choices(path, state): + # MAKE CHOICE: Add to path, update state + path.append(choice) + update_state(state, choice) + + # RECURSE: Explore further + backtrack(path, state) + + # BACKTRACK: Undo choice, restore state + path.pop() + restore_state(state, choice) + + backtrack([], initial_state) + return results +``` + +### 1.3 Backtracking Family Overview + +| Sub-Pattern | Key Characteristic | Primary Use Case | +|-------------|-------------------|------------------| +| **Permutation** | All elements used, order matters | Generate all arrangements | +| **Subset/Combination** | Select subset, order doesn't matter | Generate all subsets/combinations | +| **Target Sum** | Constraint on sum/value | Find combinations meeting target | +| **Grid Search** | 2D space exploration | Path finding, word search | +| **Constraint Satisfaction** | Multiple constraints | N-Queens, Sudoku | + +### 1.4 When to Use Backtracking + +- **Exhaustive Search**: Need to explore all possible solutions +- **Constraint Satisfaction**: Multiple constraints must be satisfied simultaneously +- **Decision Problem**: Need to find ANY valid solution (can optimize with early return) +- **Enumeration**: Need to list ALL valid solutions +- **Pruning Opportunity**: Can eliminate large portions of search space early + +### 1.5 Why It Works + +Backtracking systematically explores the solution space by: +1. **Building incrementally**: Each recursive call extends the current partial solution +2. **Pruning early**: Invalid paths are abandoned immediately, saving computation +3. **Exploring exhaustively**: All valid paths are explored through recursion +4. **Undoing choices**: Backtracking allows exploring alternative paths from the same state + +The key insight is that by maintaining state and undoing choices, we can explore all possibilities without storing all partial solutions explicitly. + +--- + +--- + +## 2. Template Quick Reference + +### 2.1 Permutation Template + +> **Strategy**: Generate all arrangements where all elements are used and order matters. +> **Key Insight**: Use a `used` array to track which elements are already in the current path. +> **Time Complexity**: O(n! × n) — n! permutations, each takes O(n) to copy. + +#### When to Use + +- Generate all **arrangements** of elements +- Order matters (e.g., [1,2,3] ≠ [2,1,3]) +- All elements must be used exactly once +- No duplicates in input (or handle duplicates with sorting + skipping) + +#### Template + +```python +def permute(nums): + """ + Generate all permutations of nums. + + Time Complexity: O(n! × n) - n! permutations, O(n) to copy each + Space Complexity: O(n) - recursion depth + path storage + """ + results = [] + used = [False] * len(nums) + + def backtrack(path): + # BASE CASE: Complete permutation found + if len(path) == len(nums): + results.append(path[:]) # Copy path + return + + # CHOICES: Try all unused elements + for i in range(len(nums)): + if used[i]: + continue # Skip already used elements + + # MAKE CHOICE + used[i] = True + path.append(nums[i]) + + # RECURSE + backtrack(path) + + # BACKTRACK + path.pop() + used[i] = False + + backtrack([]) + return results +``` + +#### Handling Duplicates + +```python +def permute_unique(nums): + """Handle duplicates by sorting and skipping same values.""" + nums.sort() + results = [] + used = [False] * len(nums) + + def backtrack(path): + if len(path) == len(nums): + results.append(path[:]) + return + + for i in range(len(nums)): + if used[i]: + continue + # Skip duplicates: if same as previous and previous not used + if i > 0 and nums[i] == nums[i-1] and not used[i-1]: + continue + + used[i] = True + path.append(nums[i]) + backtrack(path) + path.pop() + used[i] = False + + backtrack([]) + return results +``` + +#### Complexity Notes + +| Aspect | Analysis | +|--------|----------| +| Time | O(n! × n) — n! permutations, O(n) to copy each | +| Space | O(n) — recursion depth + path storage | +| Pruning | Early termination possible if only need existence check | + +#### LeetCode Problems + +| ID | Problem | Variation | +|----|---------|-----------| +| 46 | Permutations | Basic permutation | +| 47 | Permutations II | Handle duplicates | +| 60 | Permutation Sequence | Find k-th permutation (math) | + +--- + +### 2.2 Subset/Combination Template + +> **Strategy**: Generate all subsets/combinations where order doesn't matter. +> **Key Insight**: Use `start` index to avoid duplicates and ensure order. +> **Time Complexity**: O(2ⁿ × n) — 2ⁿ subsets, each takes O(n) to copy. + +#### When to Use + +- Generate all **subsets** or **combinations** +- Order doesn't matter (e.g., [1,2] = [2,1]) +- Elements can be skipped +- Often combined with constraints (size limit, sum, etc.) + +#### Template + +```python +def subsets(nums): + """ + Generate all subsets of nums. + + Time Complexity: O(2ⁿ × n) - 2ⁿ subsets, O(n) to copy each + Space Complexity: O(n) - recursion depth + path storage + """ + results = [] + + def backtrack(start, path): + # COLLECT: Add current path (every node is a valid subset) + results.append(path[:]) + + # CHOICES: Try elements starting from 'start' + for i in range(start, len(nums)): + # MAKE CHOICE + path.append(nums[i]) + + # RECURSE: Next start is i+1 (no reuse) + backtrack(i + 1, path) + + # BACKTRACK + path.pop() + + backtrack(0, []) + return results +``` + +#### Combination with Size Constraint + +```python +def combine(n, k): + """Generate all combinations of k numbers from [1..n].""" + results = [] + + def backtrack(start, path): + # BASE CASE: Reached desired size + if len(path) == k: + results.append(path[:]) + return + + # PRUNING: Not enough elements remaining + if len(path) + (n - start + 1) < k: + return + + for i in range(start, n + 1): + path.append(i) + backtrack(i + 1, path) + path.pop() + + backtrack(1, []) + return results +``` + +#### Complexity Notes + +| Aspect | Analysis | +|--------|----------| +| Time | O(2ⁿ × n) — 2ⁿ subsets, O(n) to copy each | +| Space | O(n) — recursion depth + path storage | +| Optimization | Can prune early if size constraint exists | + +#### LeetCode Problems + +| ID | Problem | Variation | +|----|---------|-----------| +| 78 | Subsets | All subsets | +| 90 | Subsets II | Handle duplicates | +| 77 | Combinations | Size-k combinations | +| 39 | Combination Sum | With target sum | + +--- + +### 2.3 Target Sum Template + +> **Strategy**: Find combinations that sum to a target value. +> **Key Insight**: Track remaining sum and prune negative paths early. +> **Time Complexity**: O(2ⁿ) worst case, often much better with pruning. + +#### When to Use + +- Find combinations meeting a **target sum** +- Elements can be reused (or not, depending on problem) +- Early pruning possible when sum exceeds target +- Often combined with sorting for better pruning + +#### Template (With Reuse) + +```python +def combination_sum(candidates, target): + """ + Find all combinations that sum to target. + Elements can be reused. + + Time Complexity: O(2ⁿ) worst case, better with pruning + Space Complexity: O(target) - recursion depth + """ + results = [] + + def backtrack(start, path, remaining): + # BASE CASE: Found valid combination + if remaining == 0: + results.append(path[:]) + return + + # PRUNING: Sum exceeds target + if remaining < 0: + return + + for i in range(start, len(candidates)): + # MAKE CHOICE + path.append(candidates[i]) + + # RECURSE: Start from i (allow reuse) + backtrack(i, path, remaining - candidates[i]) + + # BACKTRACK + path.pop() + + backtrack(0, [], target) + return results +``` + +#### Template (No Reuse) + +```python +def combination_sum2(candidates, target): + """Elements cannot be reused. Handle duplicates.""" + candidates.sort() + results = [] + + def backtrack(start, path, remaining): + if remaining == 0: + results.append(path[:]) + return + if remaining < 0: + return + + for i in range(start, len(candidates)): + # Skip duplicates + if i > start and candidates[i] == candidates[i-1]: + continue + + path.append(candidates[i]) + backtrack(i + 1, path, remaining - candidates[i]) # i+1: no reuse + path.pop() + + backtrack(0, [], target) + return results +``` + +#### Complexity Notes + +| Aspect | Analysis | +|--------|----------| +| Time | O(2ⁿ) worst case, often O(2^(target/min)) with pruning | +| Space | O(target/min) — recursion depth | +| Pruning | Very effective when sorted + early termination | + +#### LeetCode Problems + +| ID | Problem | Variation | +|----|---------|-----------| +| 39 | Combination Sum | Allow reuse | +| 40 | Combination Sum II | No reuse, handle duplicates | +| 216 | Combination Sum III | Size-k constraint | +| 377 | Combination Sum IV | Count ways (DP better) | + +--- + +### 2.4 Grid Search Template + +> **Strategy**: Explore 2D grid to find paths matching a pattern. +> **Key Insight**: Mark visited cells temporarily, restore after backtracking. +> **Time Complexity**: O(m × n × 4^L) where L is pattern length. + +#### When to Use + +- **2D grid exploration** problems +- **Path finding** with constraints +- **Word search** in grid +- Need to avoid revisiting same cell in current path +- Often combined with early return for existence check + +#### Template + +```python +def exist(grid, word): + """ + Check if word exists in grid (can move 4-directionally). + + Time Complexity: O(m × n × 4^L) - L is word length + Space Complexity: O(L) - recursion depth + """ + rows, cols = len(grid), len(grid[0]) + + def backtrack(r, c, index): + # BASE CASE: Found complete word + if index == len(word): + return True + + # BOUNDARY CHECK + if r < 0 or r >= rows or c < 0 or c >= cols: + return False + + # CONSTRAINT CHECK: Character doesn't match + if grid[r][c] != word[index]: + return False + + # MARK VISITED: Temporarily mark to avoid reuse in current path + temp = grid[r][c] + grid[r][c] = '#' + + # EXPLORE: Try all 4 directions + for dr, dc in [(-1,0), (1,0), (0,-1), (0,1)]: + if backtrack(r + dr, c + dc, index + 1): + # Found solution, restore and return + grid[r][c] = temp + return True + + # BACKTRACK: Restore cell + grid[r][c] = temp + return False + + # Try starting from each cell + for r in range(rows): + for c in range(cols): + if backtrack(r, c, 0): + return True + return False +``` + +#### Alternative: Using Visited Set + +```python +def exist_with_set(grid, word): + """Alternative using visited set (more memory but clearer).""" + rows, cols = len(grid), len(grid[0]) + + def backtrack(r, c, index, visited): + if index == len(word): + return True + if (r < 0 or r >= rows or c < 0 or c >= cols or + (r, c) in visited or grid[r][c] != word[index]): + return False + + visited.add((r, c)) + for dr, dc in [(-1,0), (1,0), (0,-1), (0,1)]: + if backtrack(r + dr, c + dc, index + 1, visited): + return True + visited.remove((r, c)) + return False + + for r in range(rows): + for c in range(cols): + if backtrack(r, c, 0, set()): + return True + return False +``` + +#### Complexity Notes + +| Aspect | Analysis | +|--------|----------| +| Time | O(m × n × 4^L) — L is pattern length, 4 directions | +| Space | O(L) — recursion depth (or O(m×n) with visited set) | +| Optimization | Early return when existence check only | + +#### LeetCode Problems + +| ID | Problem | Variation | +|----|---------|-----------| +| 79 | Word Search | Basic grid search | +| 212 | Word Search II | Multiple words (Trie + backtrack) | +| 130 | Surrounded Regions | Flood fill variant | +| 200 | Number of Islands | DFS on grid | + +--- + +### 2.5 Constraint Satisfaction Template + +> **Strategy**: Solve problems with multiple constraints (N-Queens, Sudoku). +> **Key Insight**: Check constraints before making choice, prune aggressively. +> **Time Complexity**: Varies, often exponential but heavily pruned. + +#### When to Use + +- **Multiple constraints** must be satisfied simultaneously +- **Placement problems** (N-Queens, Sudoku) +- Can check validity before making choice +- Often benefits from constraint propagation + +#### Template (N-Queens) + +```python +def solve_n_queens(n): + """ + Place n queens on n×n board so none attack each other. + + Time Complexity: O(n!) worst case, much better with pruning + Space Complexity: O(n) - recursion depth + board storage + """ + results = [] + board = [['.' for _ in range(n)] for _ in range(n)] + + def is_valid(row, col): + """Check if placing queen at (row, col) is valid.""" + # Check column + for i in range(row): + if board[i][col] == 'Q': + return False + + # Check diagonal: top-left to bottom-right + i, j = row - 1, col - 1 + while i >= 0 and j >= 0: + if board[i][j] == 'Q': + return False + i -= 1 + j -= 1 + + # Check diagonal: top-right to bottom-left + i, j = row - 1, col + 1 + while i >= 0 and j < n: + if board[i][j] == 'Q': + return False + i -= 1 + j += 1 + + return True + + def backtrack(row): + # BASE CASE: Placed all queens + if row == n: + results.append([''.join(row) for row in board]) + return + + # CHOICES: Try each column in current row + for col in range(n): + # PRUNING: Check validity before placing + if not is_valid(row, col): + continue + + # MAKE CHOICE + board[row][col] = 'Q' + + # RECURSE + backtrack(row + 1) + + # BACKTRACK + board[row][col] = '.' + + backtrack(0) + return results +``` + +#### Complexity Notes + +| Aspect | Analysis | +|--------|----------| +| Time | O(n!) worst case, heavily pruned in practice | +| Space | O(n²) — board storage + O(n) recursion | +| Optimization | Constraint checking before placement is crucial | + +#### LeetCode Problems + +| ID | Problem | Variation | +|----|---------|-----------| +| 51 | N-Queens | Basic constraint satisfaction | +| 52 | N-Queens II | Count solutions only | +| 37 | Sudoku Solver | 9×9 grid with 3×3 boxes | + + + +--- + + + +*Document generated for NeetCode Practice Framework — API Kernel: BacktrackingExploration* \ No newline at end of file diff --git a/docs/patterns/backtracking_exploration_templates.md b/docs/patterns/backtracking_exploration_templates.md deleted file mode 100644 index 8005f60..0000000 --- a/docs/patterns/backtracking_exploration_templates.md +++ /dev/null @@ -1,1409 +0,0 @@ -# Backtracking Exploration Patterns: Complete Reference - -> **API Kernel**: `BacktrackingExploration` -> **Core Mechanism**: Systematically explore all candidate solutions by building them incrementally, abandoning paths that violate constraints (pruning), and undoing choices to try alternatives. - -This document presents the **canonical backtracking template** and all its major variations. Each implementation follows consistent naming conventions and includes detailed algorithmic explanations. - ---- - -## Table of Contents - -1. [Core Concepts](#1-core-concepts) -2. [Base Template: Permutations (LeetCode 46)](#2-base-template-permutations-leetcode-46) -3. [Variation: Permutations with Duplicates (LeetCode 47)](#3-variation-permutations-with-duplicates-leetcode-47) -4. [Variation: Subsets (LeetCode 78)](#4-variation-subsets-leetcode-78) -5. [Variation: Subsets with Duplicates (LeetCode 90)](#5-variation-subsets-with-duplicates-leetcode-90) -6. [Variation: Combinations (LeetCode 77)](#6-variation-combinations-leetcode-77) -7. [Variation: Combination Sum (LeetCode 39)](#7-variation-combination-sum-leetcode-39) -8. [Variation: Combination Sum II (LeetCode 40)](#8-variation-combination-sum-ii-leetcode-40) -9. [Variation: Combination Sum III (LeetCode 216)](#9-variation-combination-sum-iii-leetcode-216) -10. [Variation: N-Queens (LeetCode 51/52)](#10-variation-n-queens-leetcode-5152) -11. [Variation: Palindrome Partitioning (LeetCode 131)](#11-variation-palindrome-partitioning-leetcode-131) -12. [Variation: Restore IP Addresses (LeetCode 93)](#12-variation-restore-ip-addresses-leetcode-93) -13. [Variation: Word Search (LeetCode 79)](#13-variation-word-search-leetcode-79) -14. [Deduplication Strategies](#14-deduplication-strategies) -15. [Pruning Techniques](#15-pruning-techniques) -16. [Pattern Comparison Table](#16-pattern-comparison-table) -17. [When to Use Backtracking](#17-when-to-use-backtracking) -18. [Template Quick Reference](#18-template-quick-reference) - ---- - -## 1. Core Concepts - -### 1.1 What is Backtracking? - -Backtracking is a **systematic trial-and-error** approach that incrementally builds candidates to the solutions and abandons a candidate ("backtracks") as soon as it determines that the candidate cannot lead to a valid solution. - -``` -Decision Tree Visualization: - - [] - ┌────────┼────────┐ - [1] [2] [3] - ┌──┴──┐ ┌──┴──┐ ┌──┴──┐ - [1,2] [1,3] [2,1] [2,3] [3,1] [3,2] - │ │ │ │ │ │ - [1,2,3] ... (continue building) - ↓ - SOLUTION FOUND → collect and backtrack -``` - -### 1.2 The Three-Step Pattern: Choose → Explore → Unchoose - -Every backtracking algorithm follows this fundamental pattern: - -```python -def backtrack(state, choices): - """ - Core backtracking template. - - 1. BASE CASE: Check if current state is a complete solution - 2. RECURSIVE CASE: For each available choice: - a) CHOOSE: Make a choice and update state - b) EXPLORE: Recursively explore with updated state - c) UNCHOOSE: Undo the choice (backtrack) - """ - # BASE CASE: Is this a complete solution? - if is_solution(state): - collect_solution(state) - return - - # RECURSIVE CASE: Try each choice - for choice in get_available_choices(state, choices): - # CHOOSE: Make this choice - apply_choice(state, choice) - - # EXPLORE: Recurse with updated state - backtrack(state, remaining_choices(choices, choice)) - - # UNCHOOSE: Undo the choice (restore state) - undo_choice(state, choice) -``` - -### 1.3 Key Invariants - -| Invariant | Description | -|-----------|-------------| -| **State Consistency** | After backtracking, state must be exactly as before the choice was made | -| **Exhaustive Exploration** | Every valid solution must be reachable through some path | -| **Pruning Soundness** | Pruned branches must not contain any valid solutions | -| **No Duplicates** | Each unique solution must be generated exactly once | - -### 1.4 Time Complexity Discussion - -Backtracking algorithms typically have exponential or factorial complexity because they explore the entire solution space: - -| Problem Type | Typical Complexity | Output Size | -|--------------|-------------------|-------------| -| Permutations | O(n! × n) | n! | -| Subsets | O(2^n × n) | 2^n | -| Combinations C(n,k) | O(C(n,k) × k) | C(n,k) | -| N-Queens | O(n!) | variable | - -**Important**: The complexity is often **output-sensitive** — if there are many solutions, generating them all is inherently expensive. - -### 1.5 Sub-Pattern Classification - -| Sub-Pattern | Key Characteristic | Examples | -|-------------|-------------------|----------| -| **Permutation** | Used/visited tracking | LeetCode 46, 47 | -| **Subset/Combination** | Start-index canonicalization | LeetCode 78, 90, 77 | -| **Target Search** | Remaining/target pruning | LeetCode 39, 40, 216 | -| **Constraint Satisfaction** | Row-by-row with constraint sets | LeetCode 51, 52 | -| **String Partitioning** | Cut positions with validity | LeetCode 131, 93 | -| **Grid/Path Search** | Visited marking and undo | LeetCode 79 | - ---- - -## 2. Base Template: Permutations (LeetCode 46) - -> **Problem**: Given an array of distinct integers, return all possible permutations. -> **Sub-Pattern**: Permutation Enumeration with used tracking. -> **Key Insight**: At each position, try all unused elements. - -### 2.1 Implementation - -```python -def permute(nums: list[int]) -> list[list[int]]: - """ - Generate all permutations of distinct integers. - - Algorithm: - - Build permutation position by position - - Track which elements have been used with a boolean array - - At each position, try every unused element - - When path length equals nums length, we have a complete permutation - - Time Complexity: O(n! × n) - - n! permutations to generate - - O(n) to copy each permutation - - Space Complexity: O(n) - - Recursion depth is n - - Used array is O(n) - - Output space not counted - - Args: - nums: Array of distinct integers - - Returns: - All possible permutations - """ - results: list[list[int]] = [] - n = len(nums) - - # State: Current permutation being built - path: list[int] = [] - - # Tracking: Which elements are already used in current path - used: list[bool] = [False] * n - - def backtrack() -> None: - # BASE CASE: Permutation is complete - if len(path) == n: - results.append(path[:]) # Append a copy - return - - # RECURSIVE CASE: Try each unused element - for i in range(n): - if used[i]: - continue # Skip already used elements - - # CHOOSE: Add element to permutation - path.append(nums[i]) - used[i] = True - - # EXPLORE: Recurse to fill next position - backtrack() - - # UNCHOOSE: Remove element (backtrack) - path.pop() - used[i] = False - - backtrack() - return results -``` - -### 2.2 Why This Works - -The `used` array ensures each element appears exactly once in each permutation. The decision tree has: -- Level 0: n choices -- Level 1: n-1 choices -- Level k: n-k choices -- Total leaves: n! - -### 2.3 Trace Example - -``` -Input: [1, 2, 3] - -backtrack(path=[], used=[F,F,F]) -├─ CHOOSE 1 → backtrack(path=[1], used=[T,F,F]) -│ ├─ CHOOSE 2 → backtrack(path=[1,2], used=[T,T,F]) -│ │ └─ CHOOSE 3 → backtrack(path=[1,2,3], used=[T,T,T]) -│ │ → SOLUTION: [1,2,3] -│ └─ CHOOSE 3 → backtrack(path=[1,3], used=[T,F,T]) -│ └─ CHOOSE 2 → backtrack(path=[1,3,2], used=[T,T,T]) -│ → SOLUTION: [1,3,2] -├─ CHOOSE 2 → ... → [2,1,3], [2,3,1] -└─ CHOOSE 3 → ... → [3,1,2], [3,2,1] - -Output: [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]] -``` - -### 2.4 Common Pitfalls - -| Pitfall | Problem | Solution | -|---------|---------|----------| -| Forgetting to copy | All results point to same list | Use `path[:]` or `list(path)` | -| Not unmarking used | Elements appear multiple times | Always set `used[i] = False` after recursion | -| Modifying during iteration | Concurrent modification errors | Iterate over indices, not elements | - ---- - -## 3. Variation: Permutations with Duplicates (LeetCode 47) - -> **Problem**: Given an array with duplicate integers, return all unique permutations. -> **Delta from Base**: Add same-level deduplication after sorting. -> **Key Insight**: Skip duplicate elements at the same tree level. - -### 3.1 Implementation - -```python -def permute_unique(nums: list[int]) -> list[list[int]]: - """ - Generate all unique permutations of integers that may contain duplicates. - - Algorithm: - - Sort the array to bring duplicates together - - Use same-level deduplication: skip a duplicate if its previous - occurrence wasn't used (meaning we're at the same decision level) - - Deduplication Rule: - - If nums[i] == nums[i-1] and used[i-1] == False, skip nums[i] - - This ensures we only use the first occurrence of a duplicate - at each level of the decision tree - - Time Complexity: O(n! × n) in worst case (all unique) - Space Complexity: O(n) - - Args: - nums: Array of integers (may contain duplicates) - - Returns: - All unique permutations - """ - results: list[list[int]] = [] - n = len(nums) - - # CRITICAL: Sort to bring duplicates together - nums.sort() - - path: list[int] = [] - used: list[bool] = [False] * n - - def backtrack() -> None: - if len(path) == n: - results.append(path[:]) - return - - for i in range(n): - if used[i]: - continue - - # DEDUPLICATION: Skip duplicates at the same tree level - # Condition: Current equals previous AND previous is unused - # (unused previous means we're trying duplicate at same level) - if i > 0 and nums[i] == nums[i - 1] and not used[i - 1]: - continue - - path.append(nums[i]) - used[i] = True - - backtrack() - - path.pop() - used[i] = False - - backtrack() - return results -``` - -### 3.2 Deduplication Logic Explained - -``` -Input: [1, 1, 2] (sorted) -Indices: [0, 1, 2] - -Without deduplication, we'd get: -- Path using indices [0,1,2] → [1,1,2] -- Path using indices [1,0,2] → [1,1,2] ← DUPLICATE! - -With deduplication (skip if nums[i]==nums[i-1] and !used[i-1]): -- When i=1 and used[0]=False: skip (same level, use i=0 first) -- When i=1 and used[0]=True: proceed (different subtree) - -This ensures we always pick the leftmost duplicate first at each level. -``` - ---- - -## 4. Variation: Subsets (LeetCode 78) - -> **Problem**: Given an array of distinct integers, return all possible subsets. -> **Sub-Pattern**: Subset enumeration with start-index canonicalization. -> **Key Insight**: Use a start index to avoid revisiting previous elements. - -### 4.1 Implementation - -```python -def subsets(nums: list[int]) -> list[list[int]]: - """ - Generate all subsets (power set) of distinct integers. - - Algorithm: - - Each subset is a collection of elements with no ordering - - To avoid duplicates like {1,2} and {2,1}, enforce canonical ordering - - Use start_index to only consider elements at or after current position - - Every intermediate path is a valid subset (collect at every node) - - Key Insight: - - Unlike permutations, subsets don't need a "used" array - - The start_index inherently prevents revisiting previous elements - - Time Complexity: O(n × 2^n) - - 2^n subsets to generate - - O(n) to copy each subset - - Space Complexity: O(n) for recursion depth - - Args: - nums: Array of distinct integers - - Returns: - All possible subsets - """ - results: list[list[int]] = [] - n = len(nums) - path: list[int] = [] - - def backtrack(start_index: int) -> None: - # COLLECT: Every path (including empty) is a valid subset - results.append(path[:]) - - # EXPLORE: Only consider elements from start_index onwards - for i in range(start_index, n): - # CHOOSE - path.append(nums[i]) - - # EXPLORE: Move start_index forward to enforce ordering - backtrack(i + 1) - - # UNCHOOSE - path.pop() - - backtrack(0) - return results -``` - -### 4.2 Why Start Index Works - -``` -Input: [1, 2, 3] - -Decision tree with start_index: -[] ← start=0, collect [] -├─ [1] ← start=1, collect [1] -│ ├─ [1,2] ← start=2, collect [1,2] -│ │ └─ [1,2,3] ← start=3, collect [1,2,3] -│ └─ [1,3] ← start=3, collect [1,3] -├─ [2] ← start=2, collect [2] -│ └─ [2,3] ← start=3, collect [2,3] -└─ [3] ← start=3, collect [3] - -Total: 8 subsets = 2^3 ✓ -``` - -The start_index ensures: -- We never pick element i after already having an element j > i -- This enforces a canonical ordering (ascending by index) -- Each subset is generated exactly once - ---- - -## 5. Variation: Subsets with Duplicates (LeetCode 90) - -> **Problem**: Given an array with duplicates, return all unique subsets. -> **Delta from Subsets**: Sort + same-level deduplication. -> **Key Insight**: Skip duplicate values at the same recursion level. - -### 5.1 Implementation - -```python -def subsets_with_dup(nums: list[int]) -> list[list[int]]: - """ - Generate all unique subsets from integers that may contain duplicates. - - Algorithm: - - Sort to bring duplicates together - - Use same-level deduplication: skip if current equals previous - in the same iteration loop - - Deduplication Condition: - - Skip nums[i] if i > start_index AND nums[i] == nums[i-1] - - This prevents choosing the same value twice at the same tree level - - Time Complexity: O(n × 2^n) worst case - Space Complexity: O(n) - - Args: - nums: Array of integers (may contain duplicates) - - Returns: - All unique subsets - """ - results: list[list[int]] = [] - n = len(nums) - - # CRITICAL: Sort to bring duplicates together - nums.sort() - - path: list[int] = [] - - def backtrack(start_index: int) -> None: - results.append(path[:]) - - for i in range(start_index, n): - # DEDUPLICATION: Skip duplicates at same level - # i > start_index ensures we're not skipping the first occurrence - if i > start_index and nums[i] == nums[i - 1]: - continue - - path.append(nums[i]) - backtrack(i + 1) - path.pop() - - backtrack(0) - return results -``` - -### 5.2 Deduplication Visualization - -``` -Input: [1, 2, 2] (sorted) - -Without deduplication: -[] -├─ [1] → [1,2] → [1,2,2] -│ → [1,2] ← choosing second 2 -├─ [2] → [2,2] -└─ [2] ← DUPLICATE of above! - -With deduplication (skip if i > start and nums[i] == nums[i-1]): -[] -├─ [1] → [1,2] → [1,2,2] -│ ↑ i=2, start=2, 2==2 but i==start, proceed -│ → [1,2] skipped (i=2 > start=1, 2==2) -├─ [2] → [2,2] -└─ skip (i=2 > start=0, 2==2) - -Result: [[], [1], [1,2], [1,2,2], [2], [2,2]] -``` - ---- - -## 6. Variation: Combinations (LeetCode 77) - -> **Problem**: Given n and k, return all combinations of k numbers from [1..n]. -> **Sub-Pattern**: Fixed-size subset enumeration. -> **Delta from Subsets**: Only collect when path length equals k. - -### 6.1 Implementation - -```python -def combine(n: int, k: int) -> list[list[int]]: - """ - Generate all combinations of k numbers from range [1, n]. - - Algorithm: - - Similar to subsets, but only collect when path has exactly k elements - - Use start_index to enforce canonical ordering - - Add pruning: stop early if remaining elements can't fill path to k - - Pruning Optimization: - - If we need (k - len(path)) more elements, we need at least that many - elements remaining in [i, n] - - Elements remaining = n - i + 1 - - Prune when: n - i + 1 < k - len(path) - - Equivalently: stop loop when i > n - (k - len(path)) + 1 - - Time Complexity: O(k × C(n,k)) - Space Complexity: O(k) - - Args: - n: Range upper bound [1..n] - k: Size of each combination - - Returns: - All combinations of k numbers from [1..n] - """ - results: list[list[int]] = [] - path: list[int] = [] - - def backtrack(start: int) -> None: - # BASE CASE: Combination is complete - if len(path) == k: - results.append(path[:]) - return - - # PRUNING: Calculate upper bound for current loop - # We need (k - len(path)) more elements - # Available elements from start to n is (n - start + 1) - # Stop when available < needed - need = k - len(path) - - for i in range(start, n - need + 2): # n - need + 1 + 1 for range - path.append(i) - backtrack(i + 1) - path.pop() - - backtrack(1) - return results -``` - -### 6.2 Pruning Analysis - -``` -n=4, k=2 - -Without pruning: -start=1: try 1,2,3,4 - start=2: try 2,3,4 - start=3: try 3,4 - start=4: try 4 ← only 1 element left, need 1 more → works - start=5: empty ← wasted call - -With pruning (need=2, loop until n-need+2=4): -start=1: try 1,2,3 (not 4, because 4→[] would fail) - ... - -This eliminates branches that can't possibly lead to valid combinations. -``` - ---- - -## 7. Variation: Combination Sum (LeetCode 39) - -> **Problem**: Find combinations that sum to target. Elements can be reused. -> **Sub-Pattern**: Target search with element reuse. -> **Key Insight**: Don't increment start_index when allowing reuse. - -### 7.1 Implementation - -```python -def combination_sum(candidates: list[int], target: int) -> list[list[int]]: - """ - Find all combinations that sum to target. Each number can be used unlimited times. - - Algorithm: - - Track remaining target (target - current sum) - - When remaining = 0, found a valid combination - - Allow reuse by NOT incrementing start_index when recursing - - Prune when remaining < 0 (overshot target) - - Key Difference from Combinations: - - Reuse allowed: recurse with same index i, not i+1 - - This means we can pick the same element multiple times - - Time Complexity: O(n^(t/m)) where t=target, m=min(candidates) - - Branching factor up to n at each level - - Depth up to t/m (using smallest element repeatedly) - - Space Complexity: O(t/m) for recursion depth - - Args: - candidates: Array of distinct positive integers - target: Target sum - - Returns: - All unique combinations that sum to target - """ - results: list[list[int]] = [] - path: list[int] = [] - - # Optional: Sort for consistent output order - candidates.sort() - - def backtrack(start_index: int, remaining: int) -> None: - # BASE CASE: Found valid combination - if remaining == 0: - results.append(path[:]) - return - - # PRUNING: Overshot target - if remaining < 0: - return - - for i in range(start_index, len(candidates)): - # PRUNING: If current candidate exceeds remaining, - # all subsequent (if sorted) will too - if candidates[i] > remaining: - break - - path.append(candidates[i]) - - # REUSE ALLOWED: Recurse with same index i - backtrack(i, remaining - candidates[i]) - - path.pop() - - backtrack(0, target) - return results -``` - -### 7.2 Reuse vs No-Reuse Comparison - -| Aspect | With Reuse (LC 39) | Without Reuse (LC 40) | -|--------|-------------------|----------------------| -| Recurse with | `backtrack(i, ...)` | `backtrack(i+1, ...)` | -| Same element | Can appear multiple times | Can appear at most once | -| Deduplication | Not needed (distinct) | Needed (may have duplicates) | - ---- - -## 8. Variation: Combination Sum II (LeetCode 40) - -> **Problem**: Find combinations that sum to target. Each element used at most once. Input may have duplicates. -> **Delta from Combination Sum**: No reuse + duplicate handling. -> **Key Insight**: Sort + same-level skip for duplicates. - -### 8.1 Implementation - -```python -def combination_sum2(candidates: list[int], target: int) -> list[list[int]]: - """ - Find all unique combinations that sum to target. Each number used at most once. - Input may contain duplicates. - - Algorithm: - - Sort to bring duplicates together - - Use start_index to prevent reuse (i+1 when recursing) - - Same-level deduplication: skip if current == previous at same level - - Deduplication Rule: - - Skip candidates[i] if i > start_index AND candidates[i] == candidates[i-1] - - This prevents generating duplicate combinations - - Time Complexity: O(2^n) worst case - Space Complexity: O(n) - - Args: - candidates: Array of positive integers (may have duplicates) - target: Target sum - - Returns: - All unique combinations summing to target - """ - results: list[list[int]] = [] - path: list[int] = [] - - # CRITICAL: Sort for deduplication - candidates.sort() - - def backtrack(start_index: int, remaining: int) -> None: - if remaining == 0: - results.append(path[:]) - return - - if remaining < 0: - return - - for i in range(start_index, len(candidates)): - # DEDUPLICATION: Skip same value at same level - if i > start_index and candidates[i] == candidates[i - 1]: - continue - - # PRUNING: Current exceeds remaining (sorted, so break) - if candidates[i] > remaining: - break - - path.append(candidates[i]) - - # NO REUSE: Recurse with i+1 - backtrack(i + 1, remaining - candidates[i]) - - path.pop() - - backtrack(0, target) - return results -``` - ---- - -## 9. Variation: Combination Sum III (LeetCode 216) - -> **Problem**: Find k numbers from [1-9] that sum to n. Each number used at most once. -> **Delta from Combination Sum II**: Fixed count k + bounded range [1-9]. -> **Key Insight**: Dual constraint — both count and sum must be satisfied. - -### 9.1 Implementation - -```python -def combination_sum3(k: int, n: int) -> list[list[int]]: - """ - Find all combinations of k numbers from [1-9] that sum to n. - - Algorithm: - - Fixed size k (must have exactly k numbers) - - Fixed sum n (must sum to exactly n) - - Range is [1-9], all distinct, no reuse - - Pruning Strategies: - 1. If current sum exceeds n, stop - 2. If path length exceeds k, stop - 3. If remaining numbers can't fill to k, stop - - Time Complexity: O(C(9,k) × k) - Space Complexity: O(k) - - Args: - k: Number of elements required - n: Target sum - - Returns: - All valid combinations - """ - results: list[list[int]] = [] - path: list[int] = [] - - def backtrack(start: int, remaining: int) -> None: - # BASE CASE: Have k numbers - if len(path) == k: - if remaining == 0: - results.append(path[:]) - return - - # PRUNING: Not enough numbers left to fill path - if 9 - start + 1 < k - len(path): - return - - for i in range(start, 10): - # PRUNING: Current number too large - if i > remaining: - break - - path.append(i) - backtrack(i + 1, remaining - i) - path.pop() - - backtrack(1, n) - return results -``` - ---- - -## 10. Variation: N-Queens (LeetCode 51/52) - -> **Problem**: Place n queens on an n×n board so no two queens attack each other. -> **Sub-Pattern**: Constraint satisfaction with row-by-row placement. -> **Key Insight**: Track columns and diagonals as constraint sets. - -### 10.1 Implementation - -```python -def solve_n_queens(n: int) -> list[list[str]]: - """ - Find all solutions to the N-Queens puzzle. - - Algorithm: - - Place queens row by row (one queen per row guaranteed) - - Track three constraints: - 1. Columns: No two queens in same column - 2. Main diagonals (↘): row - col is constant - 3. Anti-diagonals (↙): row + col is constant - - Use hash sets for O(1) constraint checking - - Key Insight: - - Row-by-row placement eliminates row conflicts by construction - - Only need to check column and diagonal conflicts - - Time Complexity: O(n!) - - At row 0: n choices - - At row 1: at most n-1 choices - - ... and so on - - Space Complexity: O(n) for constraint sets and recursion - - Args: - n: Board size - - Returns: - All valid board configurations as string arrays - """ - results: list[list[str]] = [] - - # State: queen_cols[row] = column where queen is placed - queen_cols: list[int] = [-1] * n - - # Constraint sets for O(1) conflict checking - used_cols: set[int] = set() - used_diag_main: set[int] = set() # row - col - used_diag_anti: set[int] = set() # row + col - - def backtrack(row: int) -> None: - # BASE CASE: All queens placed - if row == n: - results.append(build_board(queen_cols, n)) - return - - # Try each column in current row - for col in range(n): - # Calculate diagonal identifiers - diag_main = row - col - diag_anti = row + col - - # CONSTRAINT CHECK (pruning) - if col in used_cols: - continue - if diag_main in used_diag_main: - continue - if diag_anti in used_diag_anti: - continue - - # CHOOSE: Place queen - queen_cols[row] = col - used_cols.add(col) - used_diag_main.add(diag_main) - used_diag_anti.add(diag_anti) - - # EXPLORE: Move to next row - backtrack(row + 1) - - # UNCHOOSE: Remove queen - queen_cols[row] = -1 - used_cols.discard(col) - used_diag_main.discard(diag_main) - used_diag_anti.discard(diag_anti) - - backtrack(0) - return results - - -def build_board(queen_cols: list[int], n: int) -> list[str]: - """Convert queen positions to board representation.""" - board = [] - for col in queen_cols: - row = '.' * col + 'Q' + '.' * (n - col - 1) - board.append(row) - return board -``` - -### 10.2 Diagonal Identification - -``` -Main diagonal (↘): cells where row - col is constant - (0,0) (1,1) (2,2) → row - col = 0 - (0,1) (1,2) (2,3) → row - col = -1 - (1,0) (2,1) (3,2) → row - col = 1 - -Anti-diagonal (↙): cells where row + col is constant - (0,2) (1,1) (2,0) → row + col = 2 - (0,3) (1,2) (2,1) (3,0) → row + col = 3 -``` - -### 10.3 N-Queens II (Count Only) - -```python -def total_n_queens(n: int) -> int: - """Count solutions without building boards.""" - count = 0 - - used_cols: set[int] = set() - used_diag_main: set[int] = set() - used_diag_anti: set[int] = set() - - def backtrack(row: int) -> None: - nonlocal count - if row == n: - count += 1 - return - - for col in range(n): - dm, da = row - col, row + col - if col in used_cols or dm in used_diag_main or da in used_diag_anti: - continue - - used_cols.add(col) - used_diag_main.add(dm) - used_diag_anti.add(da) - - backtrack(row + 1) - - used_cols.discard(col) - used_diag_main.discard(dm) - used_diag_anti.discard(da) - - backtrack(0) - return count -``` - ---- - -## 11. Variation: Palindrome Partitioning (LeetCode 131) - -> **Problem**: Partition a string such that every substring is a palindrome. -> **Sub-Pattern**: String segmentation with validity check. -> **Key Insight**: Try all cut positions, validate each segment. - -### 11.1 Implementation - -```python -def partition(s: str) -> list[list[str]]: - """ - Partition string so every part is a palindrome. - - Algorithm: - - Try cutting at each position from current start - - Check if prefix is palindrome; if yes, recurse on suffix - - When start reaches end of string, we have a valid partition - - Key Insight: - - Each "choice" is where to cut the string - - Only proceed if the cut-off prefix is a palindrome - - Optimization: - - Precompute palindrome status with DP for O(1) checks - - Without precompute: O(n) per check, O(n^3) total - - With precompute: O(n^2) preprocessing, O(1) per check - - Time Complexity: O(n × 2^n) worst case - - 2^(n-1) possible partitions (n-1 cut positions) - - O(n) to copy each partition - - Space Complexity: O(n) for recursion - - Args: - s: Input string - - Returns: - All palindrome partitionings - """ - results: list[list[str]] = [] - path: list[str] = [] - n = len(s) - - # Precompute: is_palindrome[i][j] = True if s[i:j+1] is palindrome - is_palindrome = [[False] * n for _ in range(n)] - for i in range(n - 1, -1, -1): - for j in range(i, n): - if s[i] == s[j]: - if j - i <= 2: - is_palindrome[i][j] = True - else: - is_palindrome[i][j] = is_palindrome[i + 1][j - 1] - - def backtrack(start: int) -> None: - # BASE CASE: Reached end of string - if start == n: - results.append(path[:]) - return - - # Try each end position for current segment - for end in range(start, n): - # VALIDITY CHECK: Only proceed if palindrome - if not is_palindrome[start][end]: - continue - - path.append(s[start:end + 1]) - backtrack(end + 1) - path.pop() - - backtrack(0) - return results -``` - ---- - -## 12. Variation: Restore IP Addresses (LeetCode 93) - -> **Problem**: Return all valid IP addresses that can be formed from a digit string. -> **Sub-Pattern**: String segmentation with multi-constraint validity. -> **Key Insight**: Fixed 4 segments, each 1-3 digits, value 0-255, no leading zeros. - -### 12.1 Implementation - -```python -def restore_ip_addresses(s: str) -> list[str]: - """ - Generate all valid IP addresses from a digit string. - - Constraints per segment: - 1. Length: 1-3 characters - 2. Value: 0-255 - 3. No leading zeros (except "0" itself) - - Algorithm: - - Exactly 4 segments required - - Try 1, 2, or 3 characters for each segment - - Validate each segment before proceeding - - Pruning: - - Early termination if remaining chars can't form remaining segments - - Min remaining = segments_left × 1 - - Max remaining = segments_left × 3 - - Time Complexity: O(3^4 × n) = O(81 × n) = O(n) - - At most 3 choices per segment, 4 segments - - O(n) to validate/copy - - Space Complexity: O(4) = O(1) for path - - Args: - s: String of digits - - Returns: - All valid IP addresses - """ - results: list[str] = [] - segments: list[str] = [] - n = len(s) - - def is_valid_segment(segment: str) -> bool: - """Check if segment is a valid IP octet.""" - if not segment: - return False - if len(segment) > 1 and segment[0] == '0': - return False # No leading zeros - if int(segment) > 255: - return False - return True - - def backtrack(start: int, segment_count: int) -> None: - # PRUNING: Check remaining length bounds - remaining = n - start - remaining_segments = 4 - segment_count - - if remaining < remaining_segments: # Too few chars - return - if remaining > remaining_segments * 3: # Too many chars - return - - # BASE CASE: 4 segments formed - if segment_count == 4: - if start == n: # Used all characters - results.append('.'.join(segments)) - return - - # Try 1, 2, or 3 character segments - for length in range(1, 4): - if start + length > n: - break - - segment = s[start:start + length] - - if not is_valid_segment(segment): - continue - - segments.append(segment) - backtrack(start + length, segment_count + 1) - segments.pop() - - backtrack(0, 0) - return results -``` - ---- - -## 13. Variation: Word Search (LeetCode 79) - -> **Problem**: Find if a word exists in a grid by traversing adjacent cells. -> **Sub-Pattern**: Grid/Path DFS with visited marking. -> **Key Insight**: Mark visited, explore neighbors, unmark on backtrack. - -### 13.1 Implementation - -```python -def exist(board: list[list[str]], word: str) -> bool: - """ - Check if word exists in grid by traversing adjacent cells. - - Algorithm: - - Start DFS from each cell that matches word[0] - - Mark current cell as visited (modify in-place or use set) - - Try all 4 directions for next character - - Unmark on backtrack - - Key Insight: - - Each cell can be used at most once per path - - In-place marking (temporary modification) is efficient - - Pruning: - - Early return on mismatch - - Can add frequency check: if board doesn't have enough of each char - - Time Complexity: O(m × n × 4^L) where L = len(word) - - m×n starting positions - - 4 choices at each step, depth L - - Space Complexity: O(L) for recursion depth - - Args: - board: 2D character grid - word: Target word to find - - Returns: - True if word can be formed - """ - if not board or not board[0]: - return False - - rows, cols = len(board), len(board[0]) - word_len = len(word) - - # Directions: up, down, left, right - directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] - - def backtrack(row: int, col: int, index: int) -> bool: - # BASE CASE: All characters matched - if index == word_len: - return True - - # BOUNDARY CHECK - if row < 0 or row >= rows or col < 0 or col >= cols: - return False - - # CHARACTER CHECK - if board[row][col] != word[index]: - return False - - # MARK AS VISITED (in-place modification) - original = board[row][col] - board[row][col] = '#' # Temporary marker - - # EXPLORE: Try all 4 directions - for dr, dc in directions: - if backtrack(row + dr, col + dc, index + 1): - # Found! Restore and return - board[row][col] = original - return True - - # UNMARK (backtrack) - board[row][col] = original - return False - - # Try starting from each cell - for r in range(rows): - for c in range(cols): - if board[r][c] == word[0]: - if backtrack(r, c, 0): - return True - - return False -``` - -### 13.2 In-Place Marking vs Visited Set - -| Approach | Pros | Cons | -|----------|------|------| -| In-place (`#`) | O(1) space, fast | Modifies input temporarily | -| Visited set | Clean, no mutation | O(L) space for coordinates | - ---- - -## 14. Deduplication Strategies - -### 14.1 Strategy Comparison - -| Strategy | When to Use | Example | -|----------|-------------|---------| -| **Sorting + Same-Level Skip** | Input has duplicates | Permutations II, Subsets II | -| **Start Index** | Subsets/Combinations (order doesn't matter) | Subsets, Combinations | -| **Used Array** | Permutations (all elements, order matters) | Permutations | -| **Canonical Ordering** | Implicit via index ordering | All subset-like problems | - -### 14.2 Same-Level Skip Pattern - -```python -# Sort first, then skip duplicates at same level -nums.sort() - -for i in range(start, n): - # Skip if current equals previous at same tree level - if i > start and nums[i] == nums[i - 1]: - continue - # ... process nums[i] -``` - -### 14.3 Used Array Pattern - -```python -# For permutations with duplicates -if i > 0 and nums[i] == nums[i - 1] and not used[i - 1]: - continue -# This ensures we use duplicates in order (leftmost first) -``` - ---- - -## 15. Pruning Techniques - -### 15.1 Pruning Categories - -| Category | Description | Example | -|----------|-------------|---------| -| **Feasibility Bound** | Remaining elements can't satisfy constraints | Combinations: not enough elements left | -| **Target Bound** | Current path already exceeds target | Combination Sum: sum > target | -| **Constraint Propagation** | Future choices are forced/impossible | N-Queens: no valid columns left | -| **Sorted Early Exit** | If sorted, larger elements also fail | Combination Sum with sorted candidates | - -### 15.2 Pruning Patterns - -```python -# 1. Not enough elements left (Combinations) -if remaining_elements < elements_needed: - return - -# 2. Exceeded target (Combination Sum) -if current_sum > target: - return - -# 3. Sorted early break (when candidates sorted) -if candidates[i] > remaining: - break # All subsequent are larger - -# 4. Length/count bound -if len(path) > max_allowed: - return -``` - ---- - -## 16. Pattern Comparison Table - -| Problem | Sub-Pattern | State | Dedup Strategy | Pruning | -|---------|-------------|-------|----------------|---------| -| Permutations (46) | Permutation | used[] | None (distinct) | None | -| Permutations II (47) | Permutation | used[] | Sort + level skip | Same-level | -| Subsets (78) | Subset | start_idx | Index ordering | None | -| Subsets II (90) | Subset | start_idx | Sort + level skip | Same-level | -| Combinations (77) | Combination | start_idx | Index ordering | Count bound | -| Combination Sum (39) | Target Search | start_idx | None (distinct) | Target bound | -| Combination Sum II (40) | Target Search | start_idx | Sort + level skip | Target + level | -| Combination Sum III (216) | Target Search | start_idx | None (1-9 distinct) | Count + target | -| N-Queens (51) | Constraint | constraint sets | Row-by-row | Constraints | -| Palindrome Part. (131) | Segmentation | start_idx | None | Validity check | -| IP Addresses (93) | Segmentation | start_idx, count | None | Length bounds | -| Word Search (79) | Grid Path | visited | Path uniqueness | Boundary + char | - ---- - -## 17. When to Use Backtracking - -### 17.1 Problem Indicators - -✅ **Use backtracking when:** -- Need to enumerate all solutions (permutations, combinations, etc.) -- Decision tree structure (sequence of choices) -- Constraints can be checked incrementally -- Solution can be built piece by piece - -❌ **Consider alternatives when:** -- Only need count (use DP with counting) -- Only need one solution (may use greedy or simple DFS) -- Optimization problem (consider DP or greedy) -- State space is too large even with pruning - -### 17.2 Decision Guide - -``` -Is the problem asking for ALL solutions? -├── Yes → Does solution have natural ordering/structure? -│ ├── Permutation → Use used[] array -│ ├── Subset/Combination → Use start_index -│ ├── Grid path → Use visited marking -│ └── Constraint satisfaction → Use constraint sets -└── No → Need single solution or count? - ├── Single solution → Simple DFS may suffice - └── Count → Consider DP -``` - ---- - -## 18. Template Quick Reference - -### 18.1 Permutation Template - -```python -def permute(nums): - results = [] - used = [False] * len(nums) - - def backtrack(path): - if len(path) == len(nums): - results.append(path[:]) - return - - for i in range(len(nums)): - if used[i]: - continue - used[i] = True - path.append(nums[i]) - backtrack(path) - path.pop() - used[i] = False - - backtrack([]) - return results -``` - -### 18.2 Subset/Combination Template - -```python -def subsets(nums): - results = [] - - def backtrack(start, path): - results.append(path[:]) # Collect at every node - - for i in range(start, len(nums)): - path.append(nums[i]) - backtrack(i + 1, path) # i+1 for no reuse - path.pop() - - backtrack(0, []) - return results -``` - -### 18.3 Target Sum Template - -```python -def combination_sum(candidates, target): - results = [] - - def backtrack(start, path, remaining): - if remaining == 0: - results.append(path[:]) - return - if remaining < 0: - return - - for i in range(start, len(candidates)): - path.append(candidates[i]) - backtrack(i, path, remaining - candidates[i]) # i for reuse - path.pop() - - backtrack(0, [], target) - return results -``` - -### 18.4 Grid Search Template - -```python -def grid_search(grid, word): - rows, cols = len(grid), len(grid[0]) - - def backtrack(r, c, index): - if index == len(word): - return True - if r < 0 or r >= rows or c < 0 or c >= cols: - return False - if grid[r][c] != word[index]: - return False - - temp = grid[r][c] - grid[r][c] = '#' - - for dr, dc in [(-1,0), (1,0), (0,-1), (0,1)]: - if backtrack(r + dr, c + dc, index + 1): - grid[r][c] = temp - return True - - grid[r][c] = temp - return False - - for r in range(rows): - for c in range(cols): - if backtrack(r, c, 0): - return True - return False -``` - ---- - -## LeetCode Problem Mapping - -| Sub-Pattern | Problems | -|-------------|----------| -| **Permutation Enumeration** | 46. Permutations, 47. Permutations II | -| **Subset/Combination** | 78. Subsets, 90. Subsets II, 77. Combinations | -| **Target Search** | 39. Combination Sum, 40. Combination Sum II, 216. Combination Sum III | -| **Constraint Satisfaction** | 51. N-Queens, 52. N-Queens II, 37. Sudoku Solver | -| **String Partitioning** | 131. Palindrome Partitioning, 93. Restore IP Addresses, 140. Word Break II | -| **Grid/Path Search** | 79. Word Search, 212. Word Search II | - ---- - -*Document generated for NeetCode Practice Framework — API Kernel: BacktrackingExploration* - diff --git a/meta/patterns/backtracking_exploration/_config.toml b/meta/patterns/backtracking_exploration/_config.toml new file mode 100644 index 0000000..dfe1518 --- /dev/null +++ b/meta/patterns/backtracking_exploration/_config.toml @@ -0,0 +1,23 @@ +# Pattern Documentation Configuration +# Controls the order of files when composing templates.md + +# Header files (appear first) +header_files = [ + "_header.md" +] + +# Problem files (appear in middle, ordered by LeetCode number or custom order) +problem_files = [ +] + +# Footer files (appear last, typically templates) +footer_files = [ + "_templates.md" +] + +# Output configuration +# Generate to: docs/patterns/backtracking_exploration/templates.md +[output] +subdirectory = "backtracking_exploration" +filename = "templates.md" + diff --git a/meta/patterns/backtracking_exploration/_header.md b/meta/patterns/backtracking_exploration/_header.md new file mode 100644 index 0000000..9bbb78b --- /dev/null +++ b/meta/patterns/backtracking_exploration/_header.md @@ -0,0 +1,101 @@ +# Backtracking Exploration Patterns: Complete Reference + +> **API Kernel**: `BacktrackingExploration` +> **Core Mechanism**: Systematically explore all candidate solutions by building them incrementally, abandoning paths that violate constraints (pruning), and undoing choices to try alternatives. + +This document presents the **canonical backtracking template** and all its major variations. Each implementation follows consistent naming conventions and includes detailed algorithmic explanations. + +--- + +## Core Concepts + +### The Backtracking Process + +Backtracking is a systematic search technique that builds solutions incrementally and abandons partial solutions that cannot lead to valid complete solutions. + +``` +Backtracking State: +┌─────────────────────────────────────────────────────────┐ +│ [choice₁] → [choice₂] → [choice₃] → ... → [choiceₙ] │ +│ │ │ │ │ │ +│ └───────────┴───────────┴──────────────┘ │ +│ Path (current partial solution) │ +│ │ +│ When constraint violated: │ +│ Backtrack: undo last choice, try next alternative │ +└─────────────────────────────────────────────────────────┘ +``` + +### Universal Template Structure + +```python +def backtracking_template(problem_state): + """ + Generic backtracking template. + + Key components: + 1. Base Case: Check if current path is a complete solution + 2. Pruning: Abandon paths that violate constraints + 3. Choices: Generate all valid choices at current state + 4. Make Choice: Add choice to path, update state + 5. Recurse: Explore further with updated state + 6. Backtrack: Undo choice, restore state + """ + results = [] + + def backtrack(path, state): + # BASE CASE: Check if solution is complete + if is_complete(path, state): + results.append(path[:]) # Copy path + return + + # PRUNING: Abandon invalid paths early + if violates_constraints(path, state): + return + + # CHOICES: Generate all valid choices + for choice in generate_choices(path, state): + # MAKE CHOICE: Add to path, update state + path.append(choice) + update_state(state, choice) + + # RECURSE: Explore further + backtrack(path, state) + + # BACKTRACK: Undo choice, restore state + path.pop() + restore_state(state, choice) + + backtrack([], initial_state) + return results +``` + +### Backtracking Family Overview + +| Sub-Pattern | Key Characteristic | Primary Use Case | +|-------------|-------------------|------------------| +| **Permutation** | All elements used, order matters | Generate all arrangements | +| **Subset/Combination** | Select subset, order doesn't matter | Generate all subsets/combinations | +| **Target Sum** | Constraint on sum/value | Find combinations meeting target | +| **Grid Search** | 2D space exploration | Path finding, word search | +| **Constraint Satisfaction** | Multiple constraints | N-Queens, Sudoku | + +### When to Use Backtracking + +- **Exhaustive Search**: Need to explore all possible solutions +- **Constraint Satisfaction**: Multiple constraints must be satisfied simultaneously +- **Decision Problem**: Need to find ANY valid solution (can optimize with early return) +- **Enumeration**: Need to list ALL valid solutions +- **Pruning Opportunity**: Can eliminate large portions of search space early + +### Why It Works + +Backtracking systematically explores the solution space by: +1. **Building incrementally**: Each recursive call extends the current partial solution +2. **Pruning early**: Invalid paths are abandoned immediately, saving computation +3. **Exploring exhaustively**: All valid paths are explored through recursion +4. **Undoing choices**: Backtracking allows exploring alternative paths from the same state + +The key insight is that by maintaining state and undoing choices, we can explore all possibilities without storing all partial solutions explicitly. + +--- diff --git a/meta/patterns/backtracking_exploration/_templates.md b/meta/patterns/backtracking_exploration/_templates.md new file mode 100644 index 0000000..b3c9d5f --- /dev/null +++ b/meta/patterns/backtracking_exploration/_templates.md @@ -0,0 +1,490 @@ +## Template Quick Reference + +### Permutation Template + +> **Strategy**: Generate all arrangements where all elements are used and order matters. +> **Key Insight**: Use a `used` array to track which elements are already in the current path. +> **Time Complexity**: O(n! × n) — n! permutations, each takes O(n) to copy. + +#### When to Use + +- Generate all **arrangements** of elements +- Order matters (e.g., [1,2,3] ≠ [2,1,3]) +- All elements must be used exactly once +- No duplicates in input (or handle duplicates with sorting + skipping) + +#### Template + +```python +def permute(nums): + """ + Generate all permutations of nums. + + Time Complexity: O(n! × n) - n! permutations, O(n) to copy each + Space Complexity: O(n) - recursion depth + path storage + """ + results = [] + used = [False] * len(nums) + + def backtrack(path): + # BASE CASE: Complete permutation found + if len(path) == len(nums): + results.append(path[:]) # Copy path + return + + # CHOICES: Try all unused elements + for i in range(len(nums)): + if used[i]: + continue # Skip already used elements + + # MAKE CHOICE + used[i] = True + path.append(nums[i]) + + # RECURSE + backtrack(path) + + # BACKTRACK + path.pop() + used[i] = False + + backtrack([]) + return results +``` + +#### Handling Duplicates + +```python +def permute_unique(nums): + """Handle duplicates by sorting and skipping same values.""" + nums.sort() + results = [] + used = [False] * len(nums) + + def backtrack(path): + if len(path) == len(nums): + results.append(path[:]) + return + + for i in range(len(nums)): + if used[i]: + continue + # Skip duplicates: if same as previous and previous not used + if i > 0 and nums[i] == nums[i-1] and not used[i-1]: + continue + + used[i] = True + path.append(nums[i]) + backtrack(path) + path.pop() + used[i] = False + + backtrack([]) + return results +``` + +#### Complexity Notes + +| Aspect | Analysis | +|--------|----------| +| Time | O(n! × n) — n! permutations, O(n) to copy each | +| Space | O(n) — recursion depth + path storage | +| Pruning | Early termination possible if only need existence check | + +#### LeetCode Problems + +| ID | Problem | Variation | +|----|---------|-----------| +| 46 | Permutations | Basic permutation | +| 47 | Permutations II | Handle duplicates | +| 60 | Permutation Sequence | Find k-th permutation (math) | + +--- + +### Subset/Combination Template + +> **Strategy**: Generate all subsets/combinations where order doesn't matter. +> **Key Insight**: Use `start` index to avoid duplicates and ensure order. +> **Time Complexity**: O(2ⁿ × n) — 2ⁿ subsets, each takes O(n) to copy. + +#### When to Use + +- Generate all **subsets** or **combinations** +- Order doesn't matter (e.g., [1,2] = [2,1]) +- Elements can be skipped +- Often combined with constraints (size limit, sum, etc.) + +#### Template + +```python +def subsets(nums): + """ + Generate all subsets of nums. + + Time Complexity: O(2ⁿ × n) - 2ⁿ subsets, O(n) to copy each + Space Complexity: O(n) - recursion depth + path storage + """ + results = [] + + def backtrack(start, path): + # COLLECT: Add current path (every node is a valid subset) + results.append(path[:]) + + # CHOICES: Try elements starting from 'start' + for i in range(start, len(nums)): + # MAKE CHOICE + path.append(nums[i]) + + # RECURSE: Next start is i+1 (no reuse) + backtrack(i + 1, path) + + # BACKTRACK + path.pop() + + backtrack(0, []) + return results +``` + +#### Combination with Size Constraint + +```python +def combine(n, k): + """Generate all combinations of k numbers from [1..n].""" + results = [] + + def backtrack(start, path): + # BASE CASE: Reached desired size + if len(path) == k: + results.append(path[:]) + return + + # PRUNING: Not enough elements remaining + if len(path) + (n - start + 1) < k: + return + + for i in range(start, n + 1): + path.append(i) + backtrack(i + 1, path) + path.pop() + + backtrack(1, []) + return results +``` + +#### Complexity Notes + +| Aspect | Analysis | +|--------|----------| +| Time | O(2ⁿ × n) — 2ⁿ subsets, O(n) to copy each | +| Space | O(n) — recursion depth + path storage | +| Optimization | Can prune early if size constraint exists | + +#### LeetCode Problems + +| ID | Problem | Variation | +|----|---------|-----------| +| 78 | Subsets | All subsets | +| 90 | Subsets II | Handle duplicates | +| 77 | Combinations | Size-k combinations | +| 39 | Combination Sum | With target sum | + +--- + +### Target Sum Template + +> **Strategy**: Find combinations that sum to a target value. +> **Key Insight**: Track remaining sum and prune negative paths early. +> **Time Complexity**: O(2ⁿ) worst case, often much better with pruning. + +#### When to Use + +- Find combinations meeting a **target sum** +- Elements can be reused (or not, depending on problem) +- Early pruning possible when sum exceeds target +- Often combined with sorting for better pruning + +#### Template (With Reuse) + +```python +def combination_sum(candidates, target): + """ + Find all combinations that sum to target. + Elements can be reused. + + Time Complexity: O(2ⁿ) worst case, better with pruning + Space Complexity: O(target) - recursion depth + """ + results = [] + + def backtrack(start, path, remaining): + # BASE CASE: Found valid combination + if remaining == 0: + results.append(path[:]) + return + + # PRUNING: Sum exceeds target + if remaining < 0: + return + + for i in range(start, len(candidates)): + # MAKE CHOICE + path.append(candidates[i]) + + # RECURSE: Start from i (allow reuse) + backtrack(i, path, remaining - candidates[i]) + + # BACKTRACK + path.pop() + + backtrack(0, [], target) + return results +``` + +#### Template (No Reuse) + +```python +def combination_sum2(candidates, target): + """Elements cannot be reused. Handle duplicates.""" + candidates.sort() + results = [] + + def backtrack(start, path, remaining): + if remaining == 0: + results.append(path[:]) + return + if remaining < 0: + return + + for i in range(start, len(candidates)): + # Skip duplicates + if i > start and candidates[i] == candidates[i-1]: + continue + + path.append(candidates[i]) + backtrack(i + 1, path, remaining - candidates[i]) # i+1: no reuse + path.pop() + + backtrack(0, [], target) + return results +``` + +#### Complexity Notes + +| Aspect | Analysis | +|--------|----------| +| Time | O(2ⁿ) worst case, often O(2^(target/min)) with pruning | +| Space | O(target/min) — recursion depth | +| Pruning | Very effective when sorted + early termination | + +#### LeetCode Problems + +| ID | Problem | Variation | +|----|---------|-----------| +| 39 | Combination Sum | Allow reuse | +| 40 | Combination Sum II | No reuse, handle duplicates | +| 216 | Combination Sum III | Size-k constraint | +| 377 | Combination Sum IV | Count ways (DP better) | + +--- + +### Grid Search Template + +> **Strategy**: Explore 2D grid to find paths matching a pattern. +> **Key Insight**: Mark visited cells temporarily, restore after backtracking. +> **Time Complexity**: O(m × n × 4^L) where L is pattern length. + +#### When to Use + +- **2D grid exploration** problems +- **Path finding** with constraints +- **Word search** in grid +- Need to avoid revisiting same cell in current path +- Often combined with early return for existence check + +#### Template + +```python +def exist(grid, word): + """ + Check if word exists in grid (can move 4-directionally). + + Time Complexity: O(m × n × 4^L) - L is word length + Space Complexity: O(L) - recursion depth + """ + rows, cols = len(grid), len(grid[0]) + + def backtrack(r, c, index): + # BASE CASE: Found complete word + if index == len(word): + return True + + # BOUNDARY CHECK + if r < 0 or r >= rows or c < 0 or c >= cols: + return False + + # CONSTRAINT CHECK: Character doesn't match + if grid[r][c] != word[index]: + return False + + # MARK VISITED: Temporarily mark to avoid reuse in current path + temp = grid[r][c] + grid[r][c] = '#' + + # EXPLORE: Try all 4 directions + for dr, dc in [(-1,0), (1,0), (0,-1), (0,1)]: + if backtrack(r + dr, c + dc, index + 1): + # Found solution, restore and return + grid[r][c] = temp + return True + + # BACKTRACK: Restore cell + grid[r][c] = temp + return False + + # Try starting from each cell + for r in range(rows): + for c in range(cols): + if backtrack(r, c, 0): + return True + return False +``` + +#### Alternative: Using Visited Set + +```python +def exist_with_set(grid, word): + """Alternative using visited set (more memory but clearer).""" + rows, cols = len(grid), len(grid[0]) + + def backtrack(r, c, index, visited): + if index == len(word): + return True + if (r < 0 or r >= rows or c < 0 or c >= cols or + (r, c) in visited or grid[r][c] != word[index]): + return False + + visited.add((r, c)) + for dr, dc in [(-1,0), (1,0), (0,-1), (0,1)]: + if backtrack(r + dr, c + dc, index + 1, visited): + return True + visited.remove((r, c)) + return False + + for r in range(rows): + for c in range(cols): + if backtrack(r, c, 0, set()): + return True + return False +``` + +#### Complexity Notes + +| Aspect | Analysis | +|--------|----------| +| Time | O(m × n × 4^L) — L is pattern length, 4 directions | +| Space | O(L) — recursion depth (or O(m×n) with visited set) | +| Optimization | Early return when existence check only | + +#### LeetCode Problems + +| ID | Problem | Variation | +|----|---------|-----------| +| 79 | Word Search | Basic grid search | +| 212 | Word Search II | Multiple words (Trie + backtrack) | +| 130 | Surrounded Regions | Flood fill variant | +| 200 | Number of Islands | DFS on grid | + +--- + +### Constraint Satisfaction Template + +> **Strategy**: Solve problems with multiple constraints (N-Queens, Sudoku). +> **Key Insight**: Check constraints before making choice, prune aggressively. +> **Time Complexity**: Varies, often exponential but heavily pruned. + +#### When to Use + +- **Multiple constraints** must be satisfied simultaneously +- **Placement problems** (N-Queens, Sudoku) +- Can check validity before making choice +- Often benefits from constraint propagation + +#### Template (N-Queens) + +```python +def solve_n_queens(n): + """ + Place n queens on n×n board so none attack each other. + + Time Complexity: O(n!) worst case, much better with pruning + Space Complexity: O(n) - recursion depth + board storage + """ + results = [] + board = [['.' for _ in range(n)] for _ in range(n)] + + def is_valid(row, col): + """Check if placing queen at (row, col) is valid.""" + # Check column + for i in range(row): + if board[i][col] == 'Q': + return False + + # Check diagonal: top-left to bottom-right + i, j = row - 1, col - 1 + while i >= 0 and j >= 0: + if board[i][j] == 'Q': + return False + i -= 1 + j -= 1 + + # Check diagonal: top-right to bottom-left + i, j = row - 1, col + 1 + while i >= 0 and j < n: + if board[i][j] == 'Q': + return False + i -= 1 + j += 1 + + return True + + def backtrack(row): + # BASE CASE: Placed all queens + if row == n: + results.append([''.join(row) for row in board]) + return + + # CHOICES: Try each column in current row + for col in range(n): + # PRUNING: Check validity before placing + if not is_valid(row, col): + continue + + # MAKE CHOICE + board[row][col] = 'Q' + + # RECURSE + backtrack(row + 1) + + # BACKTRACK + board[row][col] = '.' + + backtrack(0) + return results +``` + +#### Complexity Notes + +| Aspect | Analysis | +|--------|----------| +| Time | O(n!) worst case, heavily pruned in practice | +| Space | O(n²) — board storage + O(n) recursion | +| Optimization | Constraint checking before placement is crucial | + +#### LeetCode Problems + +| ID | Problem | Variation | +|----|---------|-----------| +| 51 | N-Queens | Basic constraint satisfaction | +| 52 | N-Queens II | Count solutions only | +| 37 | Sudoku Solver | 9×9 grid with 3×3 boxes | + diff --git a/tools/generate_pattern_docs.py b/tools/generate_pattern_docs.py index 4bb1764..052469a 100644 --- a/tools/generate_pattern_docs.py +++ b/tools/generate_pattern_docs.py @@ -44,12 +44,24 @@ META_PATTERNS_DIR, OUTPUT_DIR, ) +from patterndocs.files import load_config def generate_pattern_doc(pattern_name: str, dry_run: bool = False) -> bool: """Generate documentation for a specific pattern.""" source_dir = META_PATTERNS_DIR / pattern_name - output_file = OUTPUT_DIR / f"{pattern_name}.md" + + # Load pattern config to check for custom output path + pattern_config = load_config(source_dir) + output_config = pattern_config.get("output", {}) + + # Determine output file path + if output_config.get("subdirectory") and output_config.get("filename"): + # Custom output path: docs/patterns// + output_file = OUTPUT_DIR / output_config["subdirectory"] / output_config["filename"] + else: + # Default output path: docs/patterns/.md + output_file = OUTPUT_DIR / f"{pattern_name}.md" if not source_dir.exists(): print(f"Error: Source directory not found: {source_dir}") @@ -97,7 +109,7 @@ def generate_pattern_doc(pattern_name: str, dry_run: bool = False) -> bool: return True # Write output - OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + output_file.parent.mkdir(parents=True, exist_ok=True) output_file.write_text(document, encoding="utf-8") print(f" Written: {len(document)} characters") return True diff --git a/tools/generate_pattern_docs.toml b/tools/generate_pattern_docs.toml index b0c0e8f..f26ea30 100644 --- a/tools/generate_pattern_docs.toml +++ b/tools/generate_pattern_docs.toml @@ -6,11 +6,11 @@ # This is the single source of truth for directory-to-kernel mappings. [kernel_mapping] sliding_window = "SubstringSlidingWindow" +two_pointers = "TwoPointersTraversal" +backtracking_exploration = "BacktrackingExploration" bfs_grid = "GridBFSMultiSource" -backtracking = "BacktrackingExploration" k_way_merge = "KWayMerge" binary_search = "BinarySearchBoundary" -two_pointers = "TwoPointersTraversal" linked_list_reversal = "LinkedListInPlaceReversal" monotonic_stack = "MonotonicStack" prefix_sum = "PrefixSumRangeQuery" diff --git a/tools/patterndocs/files.py b/tools/patterndocs/files.py index c808893..7fec235 100644 --- a/tools/patterndocs/files.py +++ b/tools/patterndocs/files.py @@ -35,12 +35,13 @@ def get_default_file_order() -> tuple[list[str], list[str]]: STRUCTURAL_FILES_FOOTER = ["_comparison.md", "_decision.md", "_mapping.md", "_templates.md"] -def load_config(source_dir: Path) -> dict[str, list[str]]: +def load_config(source_dir: Path) -> dict: """ Load file ordering configuration from _config.toml if it exists. Returns: Dictionary with 'header_files', 'problem_files', 'footer_files' lists, + and optionally 'output' configuration, or empty dict if config file doesn't exist. """ config_path = source_dir / "_config.toml" @@ -59,6 +60,8 @@ def load_config(source_dir: Path) -> dict[str, list[str]]: result["problem_files"] = config["problem_files"] if "footer_files" in config: result["footer_files"] = config["footer_files"] + if "output" in config: + result["output"] = config["output"] return result except Exception as e: From 64fa2390253afface0b0dfc2197a01462f0bbf91 Mon Sep 17 00:00:00 2001 From: lufftw Date: Sun, 14 Dec 2025 13:19:38 +0800 Subject: [PATCH 02/11] refactor(backtracking_exploration): rename files to descriptive names - Replace generic "variant" and "base" suffixes with problem-specific names - Align naming convention with sliding_window pattern - Update _config.toml to reference new filenames - Improve readability: filenames now indicate problem content at a glance --- .../0039_combination_sum.md | 77 ++++++++++ .../0040_combination_sum_ii.md | 67 ++++++++ .../0046_permutations.md | 104 +++++++++++++ .../0047_permutations_duplicates.md | 85 ++++++++++ .../backtracking_exploration/0051_n_queens.md | 145 ++++++++++++++++++ .../0077_combinations.md | 78 ++++++++++ .../backtracking_exploration/0078_subsets.md | 81 ++++++++++ .../0079_word_search.md | 94 ++++++++++++ .../0090_subsets_duplicates.md | 79 ++++++++++ .../0093_restore_ip.md | 88 +++++++++++ .../0131_palindrome_partitioning.md | 73 +++++++++ .../0216_combination_sum_iii.md | 60 ++++++++ .../backtracking_exploration/_comparison.md | 17 ++ .../backtracking_exploration/_config.toml | 19 ++- .../backtracking_exploration/_decision.md | 30 ++++ .../_deduplication.md | 33 ++++ .../backtracking_exploration/_mapping.md | 11 ++ .../backtracking_exploration/_pruning.md | 31 ++++ 18 files changed, 1171 insertions(+), 1 deletion(-) create mode 100644 meta/patterns/backtracking_exploration/0039_combination_sum.md create mode 100644 meta/patterns/backtracking_exploration/0040_combination_sum_ii.md create mode 100644 meta/patterns/backtracking_exploration/0046_permutations.md create mode 100644 meta/patterns/backtracking_exploration/0047_permutations_duplicates.md create mode 100644 meta/patterns/backtracking_exploration/0051_n_queens.md create mode 100644 meta/patterns/backtracking_exploration/0077_combinations.md create mode 100644 meta/patterns/backtracking_exploration/0078_subsets.md create mode 100644 meta/patterns/backtracking_exploration/0079_word_search.md create mode 100644 meta/patterns/backtracking_exploration/0090_subsets_duplicates.md create mode 100644 meta/patterns/backtracking_exploration/0093_restore_ip.md create mode 100644 meta/patterns/backtracking_exploration/0131_palindrome_partitioning.md create mode 100644 meta/patterns/backtracking_exploration/0216_combination_sum_iii.md create mode 100644 meta/patterns/backtracking_exploration/_comparison.md create mode 100644 meta/patterns/backtracking_exploration/_decision.md create mode 100644 meta/patterns/backtracking_exploration/_deduplication.md create mode 100644 meta/patterns/backtracking_exploration/_mapping.md create mode 100644 meta/patterns/backtracking_exploration/_pruning.md diff --git a/meta/patterns/backtracking_exploration/0039_combination_sum.md b/meta/patterns/backtracking_exploration/0039_combination_sum.md new file mode 100644 index 0000000..2f40bf0 --- /dev/null +++ b/meta/patterns/backtracking_exploration/0039_combination_sum.md @@ -0,0 +1,77 @@ +## Variation: Combination Sum (LeetCode 39) + +> **Problem**: Find combinations that sum to target. Elements can be reused. +> **Sub-Pattern**: Target search with element reuse. +> **Key Insight**: Don't increment start_index when allowing reuse. + +### Implementation + +```python +def combination_sum(candidates: list[int], target: int) -> list[list[int]]: + """ + Find all combinations that sum to target. Each number can be used unlimited times. + + Algorithm: + - Track remaining target (target - current sum) + - When remaining = 0, found a valid combination + - Allow reuse by NOT incrementing start_index when recursing + - Prune when remaining < 0 (overshot target) + + Key Difference from Combinations: + - Reuse allowed: recurse with same index i, not i+1 + - This means we can pick the same element multiple times + + Time Complexity: O(n^(t/m)) where t=target, m=min(candidates) + - Branching factor up to n at each level + - Depth up to t/m (using smallest element repeatedly) + + Space Complexity: O(t/m) for recursion depth + + Args: + candidates: Array of distinct positive integers + target: Target sum + + Returns: + All unique combinations that sum to target + """ + results: list[list[int]] = [] + path: list[int] = [] + + # Optional: Sort for consistent output order + candidates.sort() + + def backtrack(start_index: int, remaining: int) -> None: + # BASE CASE: Found valid combination + if remaining == 0: + results.append(path[:]) + return + + # PRUNING: Overshot target + if remaining < 0: + return + + for i in range(start_index, len(candidates)): + # PRUNING: If current candidate exceeds remaining, + # all subsequent (if sorted) will too + if candidates[i] > remaining: + break + + path.append(candidates[i]) + + # REUSE ALLOWED: Recurse with same index i + backtrack(i, remaining - candidates[i]) + + path.pop() + + backtrack(0, target) + return results +``` + +### Reuse vs No-Reuse Comparison + +| Aspect | With Reuse (LC 39) | Without Reuse (LC 40) | +|--------|-------------------|----------------------| +| Recurse with | `backtrack(i, ...)` | `backtrack(i+1, ...)` | +| Same element | Can appear multiple times | Can appear at most once | +| Deduplication | Not needed (distinct) | Needed (may have duplicates) | + diff --git a/meta/patterns/backtracking_exploration/0040_combination_sum_ii.md b/meta/patterns/backtracking_exploration/0040_combination_sum_ii.md new file mode 100644 index 0000000..963e49f --- /dev/null +++ b/meta/patterns/backtracking_exploration/0040_combination_sum_ii.md @@ -0,0 +1,67 @@ +## Variation: Combination Sum II (LeetCode 40) + +> **Problem**: Find combinations that sum to target. Each element used at most once. Input may have duplicates. +> **Delta from Combination Sum**: No reuse + duplicate handling. +> **Key Insight**: Sort + same-level skip for duplicates. + +### Implementation + +```python +def combination_sum2(candidates: list[int], target: int) -> list[list[int]]: + """ + Find all unique combinations that sum to target. Each number used at most once. + Input may contain duplicates. + + Algorithm: + - Sort to bring duplicates together + - Use start_index to prevent reuse (i+1 when recursing) + - Same-level deduplication: skip if current == previous at same level + + Deduplication Rule: + - Skip candidates[i] if i > start_index AND candidates[i] == candidates[i-1] + - This prevents generating duplicate combinations + + Time Complexity: O(2^n) worst case + Space Complexity: O(n) + + Args: + candidates: Array of positive integers (may have duplicates) + target: Target sum + + Returns: + All unique combinations summing to target + """ + results: list[list[int]] = [] + path: list[int] = [] + + # CRITICAL: Sort for deduplication + candidates.sort() + + def backtrack(start_index: int, remaining: int) -> None: + if remaining == 0: + results.append(path[:]) + return + + if remaining < 0: + return + + for i in range(start_index, len(candidates)): + # DEDUPLICATION: Skip same value at same level + if i > start_index and candidates[i] == candidates[i - 1]: + continue + + # PRUNING: Current exceeds remaining (sorted, so break) + if candidates[i] > remaining: + break + + path.append(candidates[i]) + + # NO REUSE: Recurse with i+1 + backtrack(i + 1, remaining - candidates[i]) + + path.pop() + + backtrack(0, target) + return results +``` + diff --git a/meta/patterns/backtracking_exploration/0046_permutations.md b/meta/patterns/backtracking_exploration/0046_permutations.md new file mode 100644 index 0000000..693ca3d --- /dev/null +++ b/meta/patterns/backtracking_exploration/0046_permutations.md @@ -0,0 +1,104 @@ +## Base Template: Permutations (LeetCode 46) + +> **Problem**: Given an array of distinct integers, return all possible permutations. +> **Sub-Pattern**: Permutation Enumeration with used tracking. +> **Key Insight**: At each position, try all unused elements. + +### Implementation + +```python +def permute(nums: list[int]) -> list[list[int]]: + """ + Generate all permutations of distinct integers. + + Algorithm: + - Build permutation position by position + - Track which elements have been used with a boolean array + - At each position, try every unused element + - When path length equals nums length, we have a complete permutation + + Time Complexity: O(n! × n) + - n! permutations to generate + - O(n) to copy each permutation + + Space Complexity: O(n) + - Recursion depth is n + - Used array is O(n) + - Output space not counted + + Args: + nums: Array of distinct integers + + Returns: + All possible permutations + """ + results: list[list[int]] = [] + n = len(nums) + + # State: Current permutation being built + path: list[int] = [] + + # Tracking: Which elements are already used in current path + used: list[bool] = [False] * n + + def backtrack() -> None: + # BASE CASE: Permutation is complete + if len(path) == n: + results.append(path[:]) # Append a copy + return + + # RECURSIVE CASE: Try each unused element + for i in range(n): + if used[i]: + continue # Skip already used elements + + # CHOOSE: Add element to permutation + path.append(nums[i]) + used[i] = True + + # EXPLORE: Recurse to fill next position + backtrack() + + # UNCHOOSE: Remove element (backtrack) + path.pop() + used[i] = False + + backtrack() + return results +``` + +### Why This Works + +The `used` array ensures each element appears exactly once in each permutation. The decision tree has: +- Level 0: n choices +- Level 1: n-1 choices +- Level k: n-k choices +- Total leaves: n! + +### Trace Example + +``` +Input: [1, 2, 3] + +backtrack(path=[], used=[F,F,F]) +├─ CHOOSE 1 → backtrack(path=[1], used=[T,F,F]) +│ ├─ CHOOSE 2 → backtrack(path=[1,2], used=[T,T,F]) +│ │ └─ CHOOSE 3 → backtrack(path=[1,2,3], used=[T,T,T]) +│ │ → SOLUTION: [1,2,3] +│ └─ CHOOSE 3 → backtrack(path=[1,3], used=[T,F,T]) +│ └─ CHOOSE 2 → backtrack(path=[1,3,2], used=[T,T,T]) +│ → SOLUTION: [1,3,2] +├─ CHOOSE 2 → ... → [2,1,3], [2,3,1] +└─ CHOOSE 3 → ... → [3,1,2], [3,2,1] + +Output: [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]] +``` + +### Common Pitfalls + +| Pitfall | Problem | Solution | +|---------|---------|----------| +| Forgetting to copy | All results point to same list | Use `path[:]` or `list(path)` | +| Not unmarking used | Elements appear multiple times | Always set `used[i] = False` after recursion | +| Modifying during iteration | Concurrent modification errors | Iterate over indices, not elements | + diff --git a/meta/patterns/backtracking_exploration/0047_permutations_duplicates.md b/meta/patterns/backtracking_exploration/0047_permutations_duplicates.md new file mode 100644 index 0000000..7177022 --- /dev/null +++ b/meta/patterns/backtracking_exploration/0047_permutations_duplicates.md @@ -0,0 +1,85 @@ +## Variation: Permutations with Duplicates (LeetCode 47) + +> **Problem**: Given an array with duplicate integers, return all unique permutations. +> **Delta from Base**: Add same-level deduplication after sorting. +> **Key Insight**: Skip duplicate elements at the same tree level. + +### Implementation + +```python +def permute_unique(nums: list[int]) -> list[list[int]]: + """ + Generate all unique permutations of integers that may contain duplicates. + + Algorithm: + - Sort the array to bring duplicates together + - Use same-level deduplication: skip a duplicate if its previous + occurrence wasn't used (meaning we're at the same decision level) + + Deduplication Rule: + - If nums[i] == nums[i-1] and used[i-1] == False, skip nums[i] + - This ensures we only use the first occurrence of a duplicate + at each level of the decision tree + + Time Complexity: O(n! × n) in worst case (all unique) + Space Complexity: O(n) + + Args: + nums: Array of integers (may contain duplicates) + + Returns: + All unique permutations + """ + results: list[list[int]] = [] + n = len(nums) + + # CRITICAL: Sort to bring duplicates together + nums.sort() + + path: list[int] = [] + used: list[bool] = [False] * n + + def backtrack() -> None: + if len(path) == n: + results.append(path[:]) + return + + for i in range(n): + if used[i]: + continue + + # DEDUPLICATION: Skip duplicates at the same tree level + # Condition: Current equals previous AND previous is unused + # (unused previous means we're trying duplicate at same level) + if i > 0 and nums[i] == nums[i - 1] and not used[i - 1]: + continue + + path.append(nums[i]) + used[i] = True + + backtrack() + + path.pop() + used[i] = False + + backtrack() + return results +``` + +### Deduplication Logic Explained + +``` +Input: [1, 1, 2] (sorted) +Indices: [0, 1, 2] + +Without deduplication, we'd get: +- Path using indices [0,1,2] → [1,1,2] +- Path using indices [1,0,2] → [1,1,2] ← DUPLICATE! + +With deduplication (skip if nums[i]==nums[i-1] and !used[i-1]): +- When i=1 and used[0]=False: skip (same level, use i=0 first) +- When i=1 and used[0]=True: proceed (different subtree) + +This ensures we always pick the leftmost duplicate first at each level. +``` + diff --git a/meta/patterns/backtracking_exploration/0051_n_queens.md b/meta/patterns/backtracking_exploration/0051_n_queens.md new file mode 100644 index 0000000..7601ca0 --- /dev/null +++ b/meta/patterns/backtracking_exploration/0051_n_queens.md @@ -0,0 +1,145 @@ +## Variation: N-Queens (LeetCode 51/52) + +> **Problem**: Place n queens on an n×n board so no two queens attack each other. +> **Sub-Pattern**: Constraint satisfaction with row-by-row placement. +> **Key Insight**: Track columns and diagonals as constraint sets. + +### Implementation + +```python +def solve_n_queens(n: int) -> list[list[str]]: + """ + Find all solutions to the N-Queens puzzle. + + Algorithm: + - Place queens row by row (one queen per row guaranteed) + - Track three constraints: + 1. Columns: No two queens in same column + 2. Main diagonals (↘): row - col is constant + 3. Anti-diagonals (↙): row + col is constant + - Use hash sets for O(1) constraint checking + + Key Insight: + - Row-by-row placement eliminates row conflicts by construction + - Only need to check column and diagonal conflicts + + Time Complexity: O(n!) + - At row 0: n choices + - At row 1: at most n-1 choices + - ... and so on + + Space Complexity: O(n) for constraint sets and recursion + + Args: + n: Board size + + Returns: + All valid board configurations as string arrays + """ + results: list[list[str]] = [] + + # State: queen_cols[row] = column where queen is placed + queen_cols: list[int] = [-1] * n + + # Constraint sets for O(1) conflict checking + used_cols: set[int] = set() + used_diag_main: set[int] = set() # row - col + used_diag_anti: set[int] = set() # row + col + + def backtrack(row: int) -> None: + # BASE CASE: All queens placed + if row == n: + results.append(build_board(queen_cols, n)) + return + + # Try each column in current row + for col in range(n): + # Calculate diagonal identifiers + diag_main = row - col + diag_anti = row + col + + # CONSTRAINT CHECK (pruning) + if col in used_cols: + continue + if diag_main in used_diag_main: + continue + if diag_anti in used_diag_anti: + continue + + # CHOOSE: Place queen + queen_cols[row] = col + used_cols.add(col) + used_diag_main.add(diag_main) + used_diag_anti.add(diag_anti) + + # EXPLORE: Move to next row + backtrack(row + 1) + + # UNCHOOSE: Remove queen + queen_cols[row] = -1 + used_cols.discard(col) + used_diag_main.discard(diag_main) + used_diag_anti.discard(diag_anti) + + backtrack(0) + return results + + +def build_board(queen_cols: list[int], n: int) -> list[str]: + """Convert queen positions to board representation.""" + board = [] + for col in queen_cols: + row = '.' * col + 'Q' + '.' * (n - col - 1) + board.append(row) + return board +``` + +### Diagonal Identification + +``` +Main diagonal (↘): cells where row - col is constant + (0,0) (1,1) (2,2) → row - col = 0 + (0,1) (1,2) (2,3) → row - col = -1 + (1,0) (2,1) (3,2) → row - col = 1 + +Anti-diagonal (↙): cells where row + col is constant + (0,2) (1,1) (2,0) → row + col = 2 + (0,3) (1,2) (2,1) (3,0) → row + col = 3 +``` + +### N-Queens II (Count Only) + +```python +def total_n_queens(n: int) -> int: + """Count solutions without building boards.""" + count = 0 + + used_cols: set[int] = set() + used_diag_main: set[int] = set() + used_diag_anti: set[int] = set() + + def backtrack(row: int) -> None: + nonlocal count + if row == n: + count += 1 + return + + for col in range(n): + dm, da = row - col, row + col + if col in used_cols or dm in used_diag_main or da in used_diag_anti: + continue + + used_cols.add(col) + used_diag_main.add(dm) + used_diag_anti.add(da) + + backtrack(row + 1) + + used_cols.discard(col) + used_diag_main.discard(dm) + used_diag_anti.discard(da) + + backtrack(0) + return count +``` + diff --git a/meta/patterns/backtracking_exploration/0077_combinations.md b/meta/patterns/backtracking_exploration/0077_combinations.md new file mode 100644 index 0000000..7b59ed2 --- /dev/null +++ b/meta/patterns/backtracking_exploration/0077_combinations.md @@ -0,0 +1,78 @@ +## Variation: Combinations (LeetCode 77) + +> **Problem**: Given n and k, return all combinations of k numbers from [1..n]. +> **Sub-Pattern**: Fixed-size subset enumeration. +> **Delta from Subsets**: Only collect when path length equals k. + +### Implementation + +```python +def combine(n: int, k: int) -> list[list[int]]: + """ + Generate all combinations of k numbers from range [1, n]. + + Algorithm: + - Similar to subsets, but only collect when path has exactly k elements + - Use start_index to enforce canonical ordering + - Add pruning: stop early if remaining elements can't fill path to k + + Pruning Optimization: + - If we need (k - len(path)) more elements, we need at least that many + elements remaining in [i, n] + - Elements remaining = n - i + 1 + - Prune when: n - i + 1 < k - len(path) + - Equivalently: stop loop when i > n - (k - len(path)) + 1 + + Time Complexity: O(k × C(n,k)) + Space Complexity: O(k) + + Args: + n: Range upper bound [1..n] + k: Size of each combination + + Returns: + All combinations of k numbers from [1..n] + """ + results: list[list[int]] = [] + path: list[int] = [] + + def backtrack(start: int) -> None: + # BASE CASE: Combination is complete + if len(path) == k: + results.append(path[:]) + return + + # PRUNING: Calculate upper bound for current loop + # We need (k - len(path)) more elements + # Available elements from start to n is (n - start + 1) + # Stop when available < needed + need = k - len(path) + + for i in range(start, n - need + 2): # n - need + 1 + 1 for range + path.append(i) + backtrack(i + 1) + path.pop() + + backtrack(1) + return results +``` + +### Pruning Analysis + +``` +n=4, k=2 + +Without pruning: +start=1: try 1,2,3,4 + start=2: try 2,3,4 + start=3: try 3,4 + start=4: try 4 ← only 1 element left, need 1 more → works + start=5: empty ← wasted call + +With pruning (need=2, loop until n-need+2=4): +start=1: try 1,2,3 (not 4, because 4→[] would fail) + ... + +This eliminates branches that can't possibly lead to valid combinations. +``` + diff --git a/meta/patterns/backtracking_exploration/0078_subsets.md b/meta/patterns/backtracking_exploration/0078_subsets.md new file mode 100644 index 0000000..f0a9d67 --- /dev/null +++ b/meta/patterns/backtracking_exploration/0078_subsets.md @@ -0,0 +1,81 @@ +## Variation: Subsets (LeetCode 78) + +> **Problem**: Given an array of distinct integers, return all possible subsets. +> **Sub-Pattern**: Subset enumeration with start-index canonicalization. +> **Key Insight**: Use a start index to avoid revisiting previous elements. + +### Implementation + +```python +def subsets(nums: list[int]) -> list[list[int]]: + """ + Generate all subsets (power set) of distinct integers. + + Algorithm: + - Each subset is a collection of elements with no ordering + - To avoid duplicates like {1,2} and {2,1}, enforce canonical ordering + - Use start_index to only consider elements at or after current position + - Every intermediate path is a valid subset (collect at every node) + + Key Insight: + - Unlike permutations, subsets don't need a "used" array + - The start_index inherently prevents revisiting previous elements + + Time Complexity: O(n × 2^n) + - 2^n subsets to generate + - O(n) to copy each subset + + Space Complexity: O(n) for recursion depth + + Args: + nums: Array of distinct integers + + Returns: + All possible subsets + """ + results: list[list[int]] = [] + n = len(nums) + path: list[int] = [] + + def backtrack(start_index: int) -> None: + # COLLECT: Every path (including empty) is a valid subset + results.append(path[:]) + + # EXPLORE: Only consider elements from start_index onwards + for i in range(start_index, n): + # CHOOSE + path.append(nums[i]) + + # EXPLORE: Move start_index forward to enforce ordering + backtrack(i + 1) + + # UNCHOOSE + path.pop() + + backtrack(0) + return results +``` + +### Why Start Index Works + +``` +Input: [1, 2, 3] + +Decision tree with start_index: +[] ← start=0, collect [] +├─ [1] ← start=1, collect [1] +│ ├─ [1,2] ← start=2, collect [1,2] +│ │ └─ [1,2,3] ← start=3, collect [1,2,3] +│ └─ [1,3] ← start=3, collect [1,3] +├─ [2] ← start=2, collect [2] +│ └─ [2,3] ← start=3, collect [2,3] +└─ [3] ← start=3, collect [3] + +Total: 8 subsets = 2^3 ✓ +``` + +The start_index ensures: +- We never pick element i after already having an element j > i +- This enforces a canonical ordering (ascending by index) +- Each subset is generated exactly once + diff --git a/meta/patterns/backtracking_exploration/0079_word_search.md b/meta/patterns/backtracking_exploration/0079_word_search.md new file mode 100644 index 0000000..bfae067 --- /dev/null +++ b/meta/patterns/backtracking_exploration/0079_word_search.md @@ -0,0 +1,94 @@ +## Variation: Word Search (LeetCode 79) + +> **Problem**: Find if a word exists in a grid by traversing adjacent cells. +> **Sub-Pattern**: Grid/Path DFS with visited marking. +> **Key Insight**: Mark visited, explore neighbors, unmark on backtrack. + +### Implementation + +```python +def exist(board: list[list[str]], word: str) -> bool: + """ + Check if word exists in grid by traversing adjacent cells. + + Algorithm: + - Start DFS from each cell that matches word[0] + - Mark current cell as visited (modify in-place or use set) + - Try all 4 directions for next character + - Unmark on backtrack + + Key Insight: + - Each cell can be used at most once per path + - In-place marking (temporary modification) is efficient + + Pruning: + - Early return on mismatch + - Can add frequency check: if board doesn't have enough of each char + + Time Complexity: O(m × n × 4^L) where L = len(word) + - m×n starting positions + - 4 choices at each step, depth L + + Space Complexity: O(L) for recursion depth + + Args: + board: 2D character grid + word: Target word to find + + Returns: + True if word can be formed + """ + if not board or not board[0]: + return False + + rows, cols = len(board), len(board[0]) + word_len = len(word) + + # Directions: up, down, left, right + directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] + + def backtrack(row: int, col: int, index: int) -> bool: + # BASE CASE: All characters matched + if index == word_len: + return True + + # BOUNDARY CHECK + if row < 0 or row >= rows or col < 0 or col >= cols: + return False + + # CHARACTER CHECK + if board[row][col] != word[index]: + return False + + # MARK AS VISITED (in-place modification) + original = board[row][col] + board[row][col] = '#' # Temporary marker + + # EXPLORE: Try all 4 directions + for dr, dc in directions: + if backtrack(row + dr, col + dc, index + 1): + # Found! Restore and return + board[row][col] = original + return True + + # UNMARK (backtrack) + board[row][col] = original + return False + + # Try starting from each cell + for r in range(rows): + for c in range(cols): + if board[r][c] == word[0]: + if backtrack(r, c, 0): + return True + + return False +``` + +### In-Place Marking vs Visited Set + +| Approach | Pros | Cons | +|----------|------|------| +| In-place (`#`) | O(1) space, fast | Modifies input temporarily | +| Visited set | Clean, no mutation | O(L) space for coordinates | + diff --git a/meta/patterns/backtracking_exploration/0090_subsets_duplicates.md b/meta/patterns/backtracking_exploration/0090_subsets_duplicates.md new file mode 100644 index 0000000..48507e9 --- /dev/null +++ b/meta/patterns/backtracking_exploration/0090_subsets_duplicates.md @@ -0,0 +1,79 @@ +## Variation: Subsets with Duplicates (LeetCode 90) + +> **Problem**: Given an array with duplicates, return all unique subsets. +> **Delta from Subsets**: Sort + same-level deduplication. +> **Key Insight**: Skip duplicate values at the same recursion level. + +### Implementation + +```python +def subsets_with_dup(nums: list[int]) -> list[list[int]]: + """ + Generate all unique subsets from integers that may contain duplicates. + + Algorithm: + - Sort to bring duplicates together + - Use same-level deduplication: skip if current equals previous + in the same iteration loop + + Deduplication Condition: + - Skip nums[i] if i > start_index AND nums[i] == nums[i-1] + - This prevents choosing the same value twice at the same tree level + + Time Complexity: O(n × 2^n) worst case + Space Complexity: O(n) + + Args: + nums: Array of integers (may contain duplicates) + + Returns: + All unique subsets + """ + results: list[list[int]] = [] + n = len(nums) + + # CRITICAL: Sort to bring duplicates together + nums.sort() + + path: list[int] = [] + + def backtrack(start_index: int) -> None: + results.append(path[:]) + + for i in range(start_index, n): + # DEDUPLICATION: Skip duplicates at same level + # i > start_index ensures we're not skipping the first occurrence + if i > start_index and nums[i] == nums[i - 1]: + continue + + path.append(nums[i]) + backtrack(i + 1) + path.pop() + + backtrack(0) + return results +``` + +### Deduplication Visualization + +``` +Input: [1, 2, 2] (sorted) + +Without deduplication: +[] +├─ [1] → [1,2] → [1,2,2] +│ → [1,2] ← choosing second 2 +├─ [2] → [2,2] +└─ [2] ← DUPLICATE of above! + +With deduplication (skip if i > start and nums[i] == nums[i-1]): +[] +├─ [1] → [1,2] → [1,2,2] +│ ↑ i=2, start=2, 2==2 but i==start, proceed +│ → [1,2] skipped (i=2 > start=1, 2==2) +├─ [2] → [2,2] +└─ skip (i=2 > start=0, 2==2) + +Result: [[], [1], [1,2], [1,2,2], [2], [2,2]] +``` + diff --git a/meta/patterns/backtracking_exploration/0093_restore_ip.md b/meta/patterns/backtracking_exploration/0093_restore_ip.md new file mode 100644 index 0000000..f5d3e7d --- /dev/null +++ b/meta/patterns/backtracking_exploration/0093_restore_ip.md @@ -0,0 +1,88 @@ +## Variation: Restore IP Addresses (LeetCode 93) + +> **Problem**: Return all valid IP addresses that can be formed from a digit string. +> **Sub-Pattern**: String segmentation with multi-constraint validity. +> **Key Insight**: Fixed 4 segments, each 1-3 digits, value 0-255, no leading zeros. + +### Implementation + +```python +def restore_ip_addresses(s: str) -> list[str]: + """ + Generate all valid IP addresses from a digit string. + + Constraints per segment: + 1. Length: 1-3 characters + 2. Value: 0-255 + 3. No leading zeros (except "0" itself) + + Algorithm: + - Exactly 4 segments required + - Try 1, 2, or 3 characters for each segment + - Validate each segment before proceeding + + Pruning: + - Early termination if remaining chars can't form remaining segments + - Min remaining = segments_left × 1 + - Max remaining = segments_left × 3 + + Time Complexity: O(3^4 × n) = O(81 × n) = O(n) + - At most 3 choices per segment, 4 segments + - O(n) to validate/copy + + Space Complexity: O(4) = O(1) for path + + Args: + s: String of digits + + Returns: + All valid IP addresses + """ + results: list[str] = [] + segments: list[str] = [] + n = len(s) + + def is_valid_segment(segment: str) -> bool: + """Check if segment is a valid IP octet.""" + if not segment: + return False + if len(segment) > 1 and segment[0] == '0': + return False # No leading zeros + if int(segment) > 255: + return False + return True + + def backtrack(start: int, segment_count: int) -> None: + # PRUNING: Check remaining length bounds + remaining = n - start + remaining_segments = 4 - segment_count + + if remaining < remaining_segments: # Too few chars + return + if remaining > remaining_segments * 3: # Too many chars + return + + # BASE CASE: 4 segments formed + if segment_count == 4: + if start == n: # Used all characters + results.append('.'.join(segments)) + return + + # Try 1, 2, or 3 character segments + for length in range(1, 4): + if start + length > n: + break + + segment = s[start:start + length] + + if not is_valid_segment(segment): + continue + + segments.append(segment) + backtrack(start + length, segment_count + 1) + segments.pop() + + backtrack(0, 0) + return results +``` + diff --git a/meta/patterns/backtracking_exploration/0131_palindrome_partitioning.md b/meta/patterns/backtracking_exploration/0131_palindrome_partitioning.md new file mode 100644 index 0000000..18094ad --- /dev/null +++ b/meta/patterns/backtracking_exploration/0131_palindrome_partitioning.md @@ -0,0 +1,73 @@ +## Variation: Palindrome Partitioning (LeetCode 131) + +> **Problem**: Partition a string such that every substring is a palindrome. +> **Sub-Pattern**: String segmentation with validity check. +> **Key Insight**: Try all cut positions, validate each segment. + +### Implementation + +```python +def partition(s: str) -> list[list[str]]: + """ + Partition string so every part is a palindrome. + + Algorithm: + - Try cutting at each position from current start + - Check if prefix is palindrome; if yes, recurse on suffix + - When start reaches end of string, we have a valid partition + + Key Insight: + - Each "choice" is where to cut the string + - Only proceed if the cut-off prefix is a palindrome + + Optimization: + - Precompute palindrome status with DP for O(1) checks + - Without precompute: O(n) per check, O(n^3) total + - With precompute: O(n^2) preprocessing, O(1) per check + + Time Complexity: O(n × 2^n) worst case + - 2^(n-1) possible partitions (n-1 cut positions) + - O(n) to copy each partition + + Space Complexity: O(n) for recursion + + Args: + s: Input string + + Returns: + All palindrome partitionings + """ + results: list[list[str]] = [] + path: list[str] = [] + n = len(s) + + # Precompute: is_palindrome[i][j] = True if s[i:j+1] is palindrome + is_palindrome = [[False] * n for _ in range(n)] + for i in range(n - 1, -1, -1): + for j in range(i, n): + if s[i] == s[j]: + if j - i <= 2: + is_palindrome[i][j] = True + else: + is_palindrome[i][j] = is_palindrome[i + 1][j - 1] + + def backtrack(start: int) -> None: + # BASE CASE: Reached end of string + if start == n: + results.append(path[:]) + return + + # Try each end position for current segment + for end in range(start, n): + # VALIDITY CHECK: Only proceed if palindrome + if not is_palindrome[start][end]: + continue + + path.append(s[start:end + 1]) + backtrack(end + 1) + path.pop() + + backtrack(0) + return results +``` + diff --git a/meta/patterns/backtracking_exploration/0216_combination_sum_iii.md b/meta/patterns/backtracking_exploration/0216_combination_sum_iii.md new file mode 100644 index 0000000..bf7efcb --- /dev/null +++ b/meta/patterns/backtracking_exploration/0216_combination_sum_iii.md @@ -0,0 +1,60 @@ +## Variation: Combination Sum III (LeetCode 216) + +> **Problem**: Find k numbers from [1-9] that sum to n. Each number used at most once. +> **Delta from Combination Sum II**: Fixed count k + bounded range [1-9]. +> **Key Insight**: Dual constraint — both count and sum must be satisfied. + +### Implementation + +```python +def combination_sum3(k: int, n: int) -> list[list[int]]: + """ + Find all combinations of k numbers from [1-9] that sum to n. + + Algorithm: + - Fixed size k (must have exactly k numbers) + - Fixed sum n (must sum to exactly n) + - Range is [1-9], all distinct, no reuse + + Pruning Strategies: + 1. If current sum exceeds n, stop + 2. If path length exceeds k, stop + 3. If remaining numbers can't fill to k, stop + + Time Complexity: O(C(9,k) × k) + Space Complexity: O(k) + + Args: + k: Number of elements required + n: Target sum + + Returns: + All valid combinations + """ + results: list[list[int]] = [] + path: list[int] = [] + + def backtrack(start: int, remaining: int) -> None: + # BASE CASE: Have k numbers + if len(path) == k: + if remaining == 0: + results.append(path[:]) + return + + # PRUNING: Not enough numbers left to fill path + if 9 - start + 1 < k - len(path): + return + + for i in range(start, 10): + # PRUNING: Current number too large + if i > remaining: + break + + path.append(i) + backtrack(i + 1, remaining - i) + path.pop() + + backtrack(1, n) + return results +``` + diff --git a/meta/patterns/backtracking_exploration/_comparison.md b/meta/patterns/backtracking_exploration/_comparison.md new file mode 100644 index 0000000..e8c19ce --- /dev/null +++ b/meta/patterns/backtracking_exploration/_comparison.md @@ -0,0 +1,17 @@ +## Pattern Comparison Table + +| Problem | Sub-Pattern | State | Dedup Strategy | Pruning | +|---------|-------------|-------|----------------|---------| +| Permutations (46) | Permutation | used[] | None (distinct) | None | +| Permutations II (47) | Permutation | used[] | Sort + level skip | Same-level | +| Subsets (78) | Subset | start_idx | Index ordering | None | +| Subsets II (90) | Subset | start_idx | Sort + level skip | Same-level | +| Combinations (77) | Combination | start_idx | Index ordering | Count bound | +| Combination Sum (39) | Target Search | start_idx | None (distinct) | Target bound | +| Combination Sum II (40) | Target Search | start_idx | Sort + level skip | Target + level | +| Combination Sum III (216) | Target Search | start_idx | None (1-9 distinct) | Count + target | +| N-Queens (51) | Constraint | constraint sets | Row-by-row | Constraints | +| Palindrome Part. (131) | Segmentation | start_idx | None | Validity check | +| IP Addresses (93) | Segmentation | start_idx, count | None | Length bounds | +| Word Search (79) | Grid Path | visited | Path uniqueness | Boundary + char | + diff --git a/meta/patterns/backtracking_exploration/_config.toml b/meta/patterns/backtracking_exploration/_config.toml index dfe1518..2bae9d0 100644 --- a/meta/patterns/backtracking_exploration/_config.toml +++ b/meta/patterns/backtracking_exploration/_config.toml @@ -8,10 +8,27 @@ header_files = [ # Problem files (appear in middle, ordered by LeetCode number or custom order) problem_files = [ + "0046_permutations.md", + "0047_permutations_duplicates.md", + "0078_subsets.md", + "0090_subsets_duplicates.md", + "0077_combinations.md", + "0039_combination_sum.md", + "0040_combination_sum_ii.md", + "0216_combination_sum_iii.md", + "0051_n_queens.md", + "0131_palindrome_partitioning.md", + "0093_restore_ip.md", + "0079_word_search.md" ] -# Footer files (appear last, typically templates) +# Footer files (appear last, typically comparison, decision, mapping, templates) footer_files = [ + "_deduplication.md", + "_pruning.md", + "_comparison.md", + "_decision.md", + "_mapping.md", "_templates.md" ] diff --git a/meta/patterns/backtracking_exploration/_decision.md b/meta/patterns/backtracking_exploration/_decision.md new file mode 100644 index 0000000..a5c0ceb --- /dev/null +++ b/meta/patterns/backtracking_exploration/_decision.md @@ -0,0 +1,30 @@ +## When to Use Backtracking + +### Problem Indicators + +✅ **Use backtracking when:** +- Need to enumerate all solutions (permutations, combinations, etc.) +- Decision tree structure (sequence of choices) +- Constraints can be checked incrementally +- Solution can be built piece by piece + +❌ **Consider alternatives when:** +- Only need count (use DP with counting) +- Only need one solution (may use greedy or simple DFS) +- Optimization problem (consider DP or greedy) +- State space is too large even with pruning + +### Decision Guide + +``` +Is the problem asking for ALL solutions? +├── Yes → Does solution have natural ordering/structure? +│ ├── Permutation → Use used[] array +│ ├── Subset/Combination → Use start_index +│ ├── Grid path → Use visited marking +│ └── Constraint satisfaction → Use constraint sets +└── No → Need single solution or count? + ├── Single solution → Simple DFS may suffice + └── Count → Consider DP +``` + diff --git a/meta/patterns/backtracking_exploration/_deduplication.md b/meta/patterns/backtracking_exploration/_deduplication.md new file mode 100644 index 0000000..9b2f637 --- /dev/null +++ b/meta/patterns/backtracking_exploration/_deduplication.md @@ -0,0 +1,33 @@ +## Deduplication Strategies + +### Strategy Comparison + +| Strategy | When to Use | Example | +|----------|-------------|---------| +| **Sorting + Same-Level Skip** | Input has duplicates | Permutations II, Subsets II | +| **Start Index** | Subsets/Combinations (order doesn't matter) | Subsets, Combinations | +| **Used Array** | Permutations (all elements, order matters) | Permutations | +| **Canonical Ordering** | Implicit via index ordering | All subset-like problems | + +### Same-Level Skip Pattern + +```python +# Sort first, then skip duplicates at same level +nums.sort() + +for i in range(start, n): + # Skip if current equals previous at same tree level + if i > start and nums[i] == nums[i - 1]: + continue + # ... process nums[i] +``` + +### Used Array Pattern + +```python +# For permutations with duplicates +if i > 0 and nums[i] == nums[i - 1] and not used[i - 1]: + continue +# This ensures we use duplicates in order (leftmost first) +``` + diff --git a/meta/patterns/backtracking_exploration/_mapping.md b/meta/patterns/backtracking_exploration/_mapping.md new file mode 100644 index 0000000..48c903f --- /dev/null +++ b/meta/patterns/backtracking_exploration/_mapping.md @@ -0,0 +1,11 @@ +## LeetCode Problem Mapping + +| Sub-Pattern | Problems | +|-------------|----------| +| **Permutation Enumeration** | 46. Permutations, 47. Permutations II | +| **Subset/Combination** | 78. Subsets, 90. Subsets II, 77. Combinations | +| **Target Search** | 39. Combination Sum, 40. Combination Sum II, 216. Combination Sum III | +| **Constraint Satisfaction** | 51. N-Queens, 52. N-Queens II, 37. Sudoku Solver | +| **String Partitioning** | 131. Palindrome Partitioning, 93. Restore IP Addresses, 140. Word Break II | +| **Grid/Path Search** | 79. Word Search, 212. Word Search II | + diff --git a/meta/patterns/backtracking_exploration/_pruning.md b/meta/patterns/backtracking_exploration/_pruning.md new file mode 100644 index 0000000..b255932 --- /dev/null +++ b/meta/patterns/backtracking_exploration/_pruning.md @@ -0,0 +1,31 @@ +## Pruning Techniques + +### Pruning Categories + +| Category | Description | Example | +|----------|-------------|---------| +| **Feasibility Bound** | Remaining elements can't satisfy constraints | Combinations: not enough elements left | +| **Target Bound** | Current path already exceeds target | Combination Sum: sum > target | +| **Constraint Propagation** | Future choices are forced/impossible | N-Queens: no valid columns left | +| **Sorted Early Exit** | If sorted, larger elements also fail | Combination Sum with sorted candidates | + +### Pruning Patterns + +```python +# 1. Not enough elements left (Combinations) +if remaining_elements < elements_needed: + return + +# 2. Exceeded target (Combination Sum) +if current_sum > target: + return + +# 3. Sorted early break (when candidates sorted) +if candidates[i] > remaining: + break # All subsequent are larger + +# 4. Length/count bound +if len(path) > max_allowed: + return +``` + From b55d7ce2beb7b4e408afd768a8cd758597290fb7 Mon Sep 17 00:00:00 2001 From: lufftw Date: Sun, 14 Dec 2025 14:13:08 +0800 Subject: [PATCH 03/11] feat(pattern-docs): introduce dual-track pattern documentation --- docs/patterns/README.md | 2 +- .../backtracking_exploration/templates.md | 1546 +++++++++++++---- .../backtracking_exploration/_header.md | 128 +- .../backtracking_exploration/_templates.md | 398 +---- 4 files changed, 1243 insertions(+), 831 deletions(-) diff --git a/docs/patterns/README.md b/docs/patterns/README.md index d9e2512..1375332 100644 --- a/docs/patterns/README.md +++ b/docs/patterns/README.md @@ -14,7 +14,7 @@ This directory contains comprehensive documentation for each **API Kernel** and | `SubstringSlidingWindow` | [sliding_window.md](sliding_window.md) | Dynamic window over sequences | LeetCode 3, 76, 159, 209, 340, 438, 567 | | `TwoPointersTraversal` | [two_pointers.md](two_pointers.md) | Two pointer traversal patterns | LeetCode 1, 11, 15, 16, 21, 26, 27, 75, 88, 125, 141, 142, 167, 202, 283, 680, 876 | | `GridBFSMultiSource` | *coming soon* | Multi-source BFS on grids | LeetCode 994, 286, 542 | -| `BacktrackingExploration` | *coming soon* | Exhaustive search with pruning | LeetCode 51, 52, 46, 78 | +| `BacktrackingExploration` | [backtracking_exploration.md](backtracking_exploration.md) | Exhaustive search with pruning | LeetCode 39, 40, 46, 47, 51, 77, 78, 79, 90, 93, 131, 216 | | `KWayMerge` | *coming soon* | Merge K sorted sequences | LeetCode 23, 21, 88 | | `BinarySearchBoundary` | *coming soon* | Binary search boundaries | LeetCode 4, 33, 34, 35 | | `LinkedListInPlaceReversal` | *coming soon* | In-place linked list reversal | LeetCode 25, 206, 92 | diff --git a/docs/patterns/backtracking_exploration/templates.md b/docs/patterns/backtracking_exploration/templates.md index 912b0f1..1c9bae2 100644 --- a/docs/patterns/backtracking_exploration/templates.md +++ b/docs/patterns/backtracking_exploration/templates.md @@ -10,594 +10,1402 @@ This document presents the **canonical backtracking template** and all its major ## Table of Contents 1. [Core Concepts](#1-core-concepts) -2. [Template Quick Reference](#2-template-quick-reference) +2. [Base Template: Permutations (LeetCode 46)](#2-base-template-permutations-leetcode-46) +3. [Variation: Permutations with Duplicates (LeetCode 47)](#3-variation-permutations-with-duplicates-leetcode-47) +4. [Variation: Subsets (LeetCode 78)](#4-variation-subsets-leetcode-78) +5. [Variation: Subsets with Duplicates (LeetCode 90)](#5-variation-subsets-with-duplicates-leetcode-90) +6. [Variation: Combinations (LeetCode 77)](#6-variation-combinations-leetcode-77) +7. [Variation: Combination Sum (LeetCode 39)](#7-variation-combination-sum-leetcode-39) +8. [Variation: Combination Sum II (LeetCode 40)](#8-variation-combination-sum-ii-leetcode-40) +9. [Variation: Combination Sum III (LeetCode 216)](#9-variation-combination-sum-iii-leetcode-216) +10. [Variation: N-Queens (LeetCode 51/52)](#10-variation-n-queens-leetcode-5152) +11. [Variation: Palindrome Partitioning (LeetCode 131)](#11-variation-palindrome-partitioning-leetcode-131) +12. [Variation: Restore IP Addresses (LeetCode 93)](#12-variation-restore-ip-addresses-leetcode-93) +13. [Variation: Word Search (LeetCode 79)](#13-variation-word-search-leetcode-79) +14. [Deduplication Strategies](#14-deduplication-strategies) +15. [Pruning Techniques](#15-pruning-techniques) +16. [Pattern Comparison Table](#16-pattern-comparison-table) +17. [When to Use Backtracking](#17-when-to-use-backtracking) +18. [LeetCode Problem Mapping](#18-leetcode-problem-mapping) +19. [Template Quick Reference](#19-template-quick-reference) --- ## 1. Core Concepts -### 1.1 The Backtracking Process +### 1.1 What is Backtracking? -Backtracking is a systematic search technique that builds solutions incrementally and abandons partial solutions that cannot lead to valid complete solutions. +Backtracking is a **systematic trial-and-error** approach that incrementally builds candidates to the solutions and abandons a candidate ("backtracks") as soon as it determines that the candidate cannot lead to a valid solution. ``` -Backtracking State: -┌─────────────────────────────────────────────────────────┐ -│ [choice₁] → [choice₂] → [choice₃] → ... → [choiceₙ] │ -│ │ │ │ │ │ -│ └───────────┴───────────┴──────────────┘ │ -│ Path (current partial solution) │ -│ │ -│ When constraint violated: │ -│ Backtrack: undo last choice, try next alternative │ -└─────────────────────────────────────────────────────────┘ +Decision Tree Visualization: + + [] + ┌────────┼────────┐ + [1] [2] [3] + ┌──┴──┐ ┌──┴──┐ ┌──┴──┐ + [1,2] [1,3] [2,1] [2,3] [3,1] [3,2] + │ │ │ │ │ │ + [1,2,3] ... (continue building) + ↓ + SOLUTION FOUND → collect and backtrack ``` -### 1.2 Universal Template Structure +### 1.2 The Three-Step Pattern: Choose → Explore → Unchoose + +Every backtracking algorithm follows this fundamental pattern: ```python -def backtracking_template(problem_state): +def backtrack(state, choices): """ - Generic backtracking template. - - Key components: - 1. Base Case: Check if current path is a complete solution - 2. Pruning: Abandon paths that violate constraints - 3. Choices: Generate all valid choices at current state - 4. Make Choice: Add choice to path, update state - 5. Recurse: Explore further with updated state - 6. Backtrack: Undo choice, restore state + Core backtracking template. + + 1. BASE CASE: Check if current state is a complete solution + 2. RECURSIVE CASE: For each available choice: + a) CHOOSE: Make a choice and update state + b) EXPLORE: Recursively explore with updated state + c) UNCHOOSE: Undo the choice (backtrack) """ - results = [] + # BASE CASE: Is this a complete solution? + if is_solution(state): + collect_solution(state) + return - def backtrack(path, state): - # BASE CASE: Check if solution is complete - if is_complete(path, state): - results.append(path[:]) # Copy path - return + # RECURSIVE CASE: Try each choice + for choice in get_available_choices(state, choices): + # CHOOSE: Make this choice + apply_choice(state, choice) - # PRUNING: Abandon invalid paths early - if violates_constraints(path, state): - return + # EXPLORE: Recurse with updated state + backtrack(state, remaining_choices(choices, choice)) - # CHOICES: Generate all valid choices - for choice in generate_choices(path, state): - # MAKE CHOICE: Add to path, update state - path.append(choice) - update_state(state, choice) - - # RECURSE: Explore further - backtrack(path, state) - - # BACKTRACK: Undo choice, restore state - path.pop() - restore_state(state, choice) - - backtrack([], initial_state) - return results + # UNCHOOSE: Undo the choice (restore state) + undo_choice(state, choice) ``` -### 1.3 Backtracking Family Overview +### 1.3 Key Invariants -| Sub-Pattern | Key Characteristic | Primary Use Case | -|-------------|-------------------|------------------| -| **Permutation** | All elements used, order matters | Generate all arrangements | -| **Subset/Combination** | Select subset, order doesn't matter | Generate all subsets/combinations | -| **Target Sum** | Constraint on sum/value | Find combinations meeting target | -| **Grid Search** | 2D space exploration | Path finding, word search | -| **Constraint Satisfaction** | Multiple constraints | N-Queens, Sudoku | +| Invariant | Description | +|-----------|-------------| +| **State Consistency** | After backtracking, state must be exactly as before the choice was made | +| **Exhaustive Exploration** | Every valid solution must be reachable through some path | +| **Pruning Soundness** | Pruned branches must not contain any valid solutions | +| **No Duplicates** | Each unique solution must be generated exactly once | -### 1.4 When to Use Backtracking +### 1.4 Time Complexity Discussion -- **Exhaustive Search**: Need to explore all possible solutions -- **Constraint Satisfaction**: Multiple constraints must be satisfied simultaneously -- **Decision Problem**: Need to find ANY valid solution (can optimize with early return) -- **Enumeration**: Need to list ALL valid solutions -- **Pruning Opportunity**: Can eliminate large portions of search space early +Backtracking algorithms typically have exponential or factorial complexity because they explore the entire solution space: -### 1.5 Why It Works +| Problem Type | Typical Complexity | Output Size | +|--------------|-------------------|-------------| +| Permutations | O(n! × n) | n! | +| Subsets | O(2^n × n) | 2^n | +| Combinations C(n,k) | O(C(n,k) × k) | C(n,k) | +| N-Queens | O(n!) | variable | -Backtracking systematically explores the solution space by: -1. **Building incrementally**: Each recursive call extends the current partial solution -2. **Pruning early**: Invalid paths are abandoned immediately, saving computation -3. **Exploring exhaustively**: All valid paths are explored through recursion -4. **Undoing choices**: Backtracking allows exploring alternative paths from the same state +**Important**: The complexity is often **output-sensitive** — if there are many solutions, generating them all is inherently expensive. -The key insight is that by maintaining state and undoing choices, we can explore all possibilities without storing all partial solutions explicitly. +### 1.5 Sub-Pattern Classification ---- +| Sub-Pattern | Key Characteristic | Examples | +|-------------|-------------------|----------| +| **Permutation** | Used/visited tracking | LeetCode 46, 47 | +| **Subset/Combination** | Start-index canonicalization | LeetCode 78, 90, 77 | +| **Target Search** | Remaining/target pruning | LeetCode 39, 40, 216 | +| **Constraint Satisfaction** | Row-by-row with constraint sets | LeetCode 51, 52 | +| **String Partitioning** | Cut positions with validity | LeetCode 131, 93 | +| **Grid/Path Search** | Visited marking and undo | LeetCode 79 | --- -## 2. Template Quick Reference - -### 2.1 Permutation Template - -> **Strategy**: Generate all arrangements where all elements are used and order matters. -> **Key Insight**: Use a `used` array to track which elements are already in the current path. -> **Time Complexity**: O(n! × n) — n! permutations, each takes O(n) to copy. +--- -#### When to Use +## 2. Base Template: Permutations (LeetCode 46) -- Generate all **arrangements** of elements -- Order matters (e.g., [1,2,3] ≠ [2,1,3]) -- All elements must be used exactly once -- No duplicates in input (or handle duplicates with sorting + skipping) +> **Problem**: Given an array of distinct integers, return all possible permutations. +> **Sub-Pattern**: Permutation Enumeration with used tracking. +> **Key Insight**: At each position, try all unused elements. -#### Template +### 2.1 Implementation ```python -def permute(nums): +def permute(nums: list[int]) -> list[list[int]]: """ - Generate all permutations of nums. + Generate all permutations of distinct integers. - Time Complexity: O(n! × n) - n! permutations, O(n) to copy each - Space Complexity: O(n) - recursion depth + path storage + Algorithm: + - Build permutation position by position + - Track which elements have been used with a boolean array + - At each position, try every unused element + - When path length equals nums length, we have a complete permutation + + Time Complexity: O(n! × n) + - n! permutations to generate + - O(n) to copy each permutation + + Space Complexity: O(n) + - Recursion depth is n + - Used array is O(n) + - Output space not counted + + Args: + nums: Array of distinct integers + + Returns: + All possible permutations """ - results = [] - used = [False] * len(nums) + results: list[list[int]] = [] + n = len(nums) - def backtrack(path): - # BASE CASE: Complete permutation found - if len(path) == len(nums): - results.append(path[:]) # Copy path + # State: Current permutation being built + path: list[int] = [] + + # Tracking: Which elements are already used in current path + used: list[bool] = [False] * n + + def backtrack() -> None: + # BASE CASE: Permutation is complete + if len(path) == n: + results.append(path[:]) # Append a copy return - # CHOICES: Try all unused elements - for i in range(len(nums)): + # RECURSIVE CASE: Try each unused element + for i in range(n): if used[i]: continue # Skip already used elements - # MAKE CHOICE - used[i] = True + # CHOOSE: Add element to permutation path.append(nums[i]) + used[i] = True - # RECURSE - backtrack(path) + # EXPLORE: Recurse to fill next position + backtrack() - # BACKTRACK + # UNCHOOSE: Remove element (backtrack) path.pop() used[i] = False - backtrack([]) + backtrack() return results ``` -#### Handling Duplicates +### 2.2 Why This Works + +The `used` array ensures each element appears exactly once in each permutation. The decision tree has: +- Level 0: n choices +- Level 1: n-1 choices +- Level k: n-k choices +- Total leaves: n! + +### 2.3 Trace Example + +``` +Input: [1, 2, 3] + +backtrack(path=[], used=[F,F,F]) +├─ CHOOSE 1 → backtrack(path=[1], used=[T,F,F]) +│ ├─ CHOOSE 2 → backtrack(path=[1,2], used=[T,T,F]) +│ │ └─ CHOOSE 3 → backtrack(path=[1,2,3], used=[T,T,T]) +│ │ → SOLUTION: [1,2,3] +│ └─ CHOOSE 3 → backtrack(path=[1,3], used=[T,F,T]) +│ └─ CHOOSE 2 → backtrack(path=[1,3,2], used=[T,T,T]) +│ → SOLUTION: [1,3,2] +├─ CHOOSE 2 → ... → [2,1,3], [2,3,1] +└─ CHOOSE 3 → ... → [3,1,2], [3,2,1] + +Output: [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]] +``` + +### 2.4 Common Pitfalls + +| Pitfall | Problem | Solution | +|---------|---------|----------| +| Forgetting to copy | All results point to same list | Use `path[:]` or `list(path)` | +| Not unmarking used | Elements appear multiple times | Always set `used[i] = False` after recursion | +| Modifying during iteration | Concurrent modification errors | Iterate over indices, not elements | + +--- + +## 3. Variation: Permutations with Duplicates (LeetCode 47) + +> **Problem**: Given an array with duplicate integers, return all unique permutations. +> **Delta from Base**: Add same-level deduplication after sorting. +> **Key Insight**: Skip duplicate elements at the same tree level. + +### 3.1 Implementation ```python -def permute_unique(nums): - """Handle duplicates by sorting and skipping same values.""" +def permute_unique(nums: list[int]) -> list[list[int]]: + """ + Generate all unique permutations of integers that may contain duplicates. + + Algorithm: + - Sort the array to bring duplicates together + - Use same-level deduplication: skip a duplicate if its previous + occurrence wasn't used (meaning we're at the same decision level) + + Deduplication Rule: + - If nums[i] == nums[i-1] and used[i-1] == False, skip nums[i] + - This ensures we only use the first occurrence of a duplicate + at each level of the decision tree + + Time Complexity: O(n! × n) in worst case (all unique) + Space Complexity: O(n) + + Args: + nums: Array of integers (may contain duplicates) + + Returns: + All unique permutations + """ + results: list[list[int]] = [] + n = len(nums) + + # CRITICAL: Sort to bring duplicates together nums.sort() - results = [] - used = [False] * len(nums) - def backtrack(path): - if len(path) == len(nums): + path: list[int] = [] + used: list[bool] = [False] * n + + def backtrack() -> None: + if len(path) == n: results.append(path[:]) return - for i in range(len(nums)): + for i in range(n): if used[i]: continue - # Skip duplicates: if same as previous and previous not used - if i > 0 and nums[i] == nums[i-1] and not used[i-1]: + + # DEDUPLICATION: Skip duplicates at the same tree level + # Condition: Current equals previous AND previous is unused + # (unused previous means we're trying duplicate at same level) + if i > 0 and nums[i] == nums[i - 1] and not used[i - 1]: continue - used[i] = True path.append(nums[i]) - backtrack(path) + used[i] = True + + backtrack() + path.pop() used[i] = False - backtrack([]) + backtrack() return results ``` -#### Complexity Notes +### 3.2 Deduplication Logic Explained -| Aspect | Analysis | -|--------|----------| -| Time | O(n! × n) — n! permutations, O(n) to copy each | -| Space | O(n) — recursion depth + path storage | -| Pruning | Early termination possible if only need existence check | - -#### LeetCode Problems +``` +Input: [1, 1, 2] (sorted) +Indices: [0, 1, 2] -| ID | Problem | Variation | -|----|---------|-----------| -| 46 | Permutations | Basic permutation | -| 47 | Permutations II | Handle duplicates | -| 60 | Permutation Sequence | Find k-th permutation (math) | +Without deduplication, we'd get: +- Path using indices [0,1,2] → [1,1,2] +- Path using indices [1,0,2] → [1,1,2] ← DUPLICATE! ---- +With deduplication (skip if nums[i]==nums[i-1] and !used[i-1]): +- When i=1 and used[0]=False: skip (same level, use i=0 first) +- When i=1 and used[0]=True: proceed (different subtree) -### 2.2 Subset/Combination Template +This ensures we always pick the leftmost duplicate first at each level. +``` -> **Strategy**: Generate all subsets/combinations where order doesn't matter. -> **Key Insight**: Use `start` index to avoid duplicates and ensure order. -> **Time Complexity**: O(2ⁿ × n) — 2ⁿ subsets, each takes O(n) to copy. +--- -#### When to Use +## 4. Variation: Subsets (LeetCode 78) -- Generate all **subsets** or **combinations** -- Order doesn't matter (e.g., [1,2] = [2,1]) -- Elements can be skipped -- Often combined with constraints (size limit, sum, etc.) +> **Problem**: Given an array of distinct integers, return all possible subsets. +> **Sub-Pattern**: Subset enumeration with start-index canonicalization. +> **Key Insight**: Use a start index to avoid revisiting previous elements. -#### Template +### 4.1 Implementation ```python -def subsets(nums): +def subsets(nums: list[int]) -> list[list[int]]: """ - Generate all subsets of nums. + Generate all subsets (power set) of distinct integers. + + Algorithm: + - Each subset is a collection of elements with no ordering + - To avoid duplicates like {1,2} and {2,1}, enforce canonical ordering + - Use start_index to only consider elements at or after current position + - Every intermediate path is a valid subset (collect at every node) + + Key Insight: + - Unlike permutations, subsets don't need a "used" array + - The start_index inherently prevents revisiting previous elements - Time Complexity: O(2ⁿ × n) - 2ⁿ subsets, O(n) to copy each - Space Complexity: O(n) - recursion depth + path storage + Time Complexity: O(n × 2^n) + - 2^n subsets to generate + - O(n) to copy each subset + + Space Complexity: O(n) for recursion depth + + Args: + nums: Array of distinct integers + + Returns: + All possible subsets """ - results = [] + results: list[list[int]] = [] + n = len(nums) + path: list[int] = [] - def backtrack(start, path): - # COLLECT: Add current path (every node is a valid subset) + def backtrack(start_index: int) -> None: + # COLLECT: Every path (including empty) is a valid subset results.append(path[:]) - # CHOICES: Try elements starting from 'start' - for i in range(start, len(nums)): - # MAKE CHOICE + # EXPLORE: Only consider elements from start_index onwards + for i in range(start_index, n): + # CHOOSE path.append(nums[i]) - # RECURSE: Next start is i+1 (no reuse) - backtrack(i + 1, path) + # EXPLORE: Move start_index forward to enforce ordering + backtrack(i + 1) - # BACKTRACK + # UNCHOOSE path.pop() - backtrack(0, []) + backtrack(0) return results ``` -#### Combination with Size Constraint +### 4.2 Why Start Index Works + +``` +Input: [1, 2, 3] + +Decision tree with start_index: +[] ← start=0, collect [] +├─ [1] ← start=1, collect [1] +│ ├─ [1,2] ← start=2, collect [1,2] +│ │ └─ [1,2,3] ← start=3, collect [1,2,3] +│ └─ [1,3] ← start=3, collect [1,3] +├─ [2] ← start=2, collect [2] +│ └─ [2,3] ← start=3, collect [2,3] +└─ [3] ← start=3, collect [3] + +Total: 8 subsets = 2^3 ✓ +``` + +The start_index ensures: +- We never pick element i after already having an element j > i +- This enforces a canonical ordering (ascending by index) +- Each subset is generated exactly once + +--- + +## 5. Variation: Subsets with Duplicates (LeetCode 90) + +> **Problem**: Given an array with duplicates, return all unique subsets. +> **Delta from Subsets**: Sort + same-level deduplication. +> **Key Insight**: Skip duplicate values at the same recursion level. + +### 5.1 Implementation ```python -def combine(n, k): - """Generate all combinations of k numbers from [1..n].""" - results = [] +def subsets_with_dup(nums: list[int]) -> list[list[int]]: + """ + Generate all unique subsets from integers that may contain duplicates. - def backtrack(start, path): - # BASE CASE: Reached desired size + Algorithm: + - Sort to bring duplicates together + - Use same-level deduplication: skip if current equals previous + in the same iteration loop + + Deduplication Condition: + - Skip nums[i] if i > start_index AND nums[i] == nums[i-1] + - This prevents choosing the same value twice at the same tree level + + Time Complexity: O(n × 2^n) worst case + Space Complexity: O(n) + + Args: + nums: Array of integers (may contain duplicates) + + Returns: + All unique subsets + """ + results: list[list[int]] = [] + n = len(nums) + + # CRITICAL: Sort to bring duplicates together + nums.sort() + + path: list[int] = [] + + def backtrack(start_index: int) -> None: + results.append(path[:]) + + for i in range(start_index, n): + # DEDUPLICATION: Skip duplicates at same level + # i > start_index ensures we're not skipping the first occurrence + if i > start_index and nums[i] == nums[i - 1]: + continue + + path.append(nums[i]) + backtrack(i + 1) + path.pop() + + backtrack(0) + return results +``` + +### 5.2 Deduplication Visualization + +``` +Input: [1, 2, 2] (sorted) + +Without deduplication: +[] +├─ [1] → [1,2] → [1,2,2] +│ → [1,2] ← choosing second 2 +├─ [2] → [2,2] +└─ [2] ← DUPLICATE of above! + +With deduplication (skip if i > start and nums[i] == nums[i-1]): +[] +├─ [1] → [1,2] → [1,2,2] +│ ↑ i=2, start=2, 2==2 but i==start, proceed +│ → [1,2] skipped (i=2 > start=1, 2==2) +├─ [2] → [2,2] +└─ skip (i=2 > start=0, 2==2) + +Result: [[], [1], [1,2], [1,2,2], [2], [2,2]] +``` + +--- + +## 6. Variation: Combinations (LeetCode 77) + +> **Problem**: Given n and k, return all combinations of k numbers from [1..n]. +> **Sub-Pattern**: Fixed-size subset enumeration. +> **Delta from Subsets**: Only collect when path length equals k. + +### 6.1 Implementation + +```python +def combine(n: int, k: int) -> list[list[int]]: + """ + Generate all combinations of k numbers from range [1, n]. + + Algorithm: + - Similar to subsets, but only collect when path has exactly k elements + - Use start_index to enforce canonical ordering + - Add pruning: stop early if remaining elements can't fill path to k + + Pruning Optimization: + - If we need (k - len(path)) more elements, we need at least that many + elements remaining in [i, n] + - Elements remaining = n - i + 1 + - Prune when: n - i + 1 < k - len(path) + - Equivalently: stop loop when i > n - (k - len(path)) + 1 + + Time Complexity: O(k × C(n,k)) + Space Complexity: O(k) + + Args: + n: Range upper bound [1..n] + k: Size of each combination + + Returns: + All combinations of k numbers from [1..n] + """ + results: list[list[int]] = [] + path: list[int] = [] + + def backtrack(start: int) -> None: + # BASE CASE: Combination is complete if len(path) == k: results.append(path[:]) return - # PRUNING: Not enough elements remaining - if len(path) + (n - start + 1) < k: - return + # PRUNING: Calculate upper bound for current loop + # We need (k - len(path)) more elements + # Available elements from start to n is (n - start + 1) + # Stop when available < needed + need = k - len(path) - for i in range(start, n + 1): + for i in range(start, n - need + 2): # n - need + 1 + 1 for range path.append(i) - backtrack(i + 1, path) + backtrack(i + 1) path.pop() - backtrack(1, []) + backtrack(1) return results ``` -#### Complexity Notes +### 6.2 Pruning Analysis -| Aspect | Analysis | -|--------|----------| -| Time | O(2ⁿ × n) — 2ⁿ subsets, O(n) to copy each | -| Space | O(n) — recursion depth + path storage | -| Optimization | Can prune early if size constraint exists | +``` +n=4, k=2 -#### LeetCode Problems +Without pruning: +start=1: try 1,2,3,4 + start=2: try 2,3,4 + start=3: try 3,4 + start=4: try 4 ← only 1 element left, need 1 more → works + start=5: empty ← wasted call -| ID | Problem | Variation | -|----|---------|-----------| -| 78 | Subsets | All subsets | -| 90 | Subsets II | Handle duplicates | -| 77 | Combinations | Size-k combinations | -| 39 | Combination Sum | With target sum | +With pruning (need=2, loop until n-need+2=4): +start=1: try 1,2,3 (not 4, because 4→[] would fail) + ... ---- - -### 2.3 Target Sum Template +This eliminates branches that can't possibly lead to valid combinations. +``` -> **Strategy**: Find combinations that sum to a target value. -> **Key Insight**: Track remaining sum and prune negative paths early. -> **Time Complexity**: O(2ⁿ) worst case, often much better with pruning. +--- -#### When to Use +## 7. Variation: Combination Sum (LeetCode 39) -- Find combinations meeting a **target sum** -- Elements can be reused (or not, depending on problem) -- Early pruning possible when sum exceeds target -- Often combined with sorting for better pruning +> **Problem**: Find combinations that sum to target. Elements can be reused. +> **Sub-Pattern**: Target search with element reuse. +> **Key Insight**: Don't increment start_index when allowing reuse. -#### Template (With Reuse) +### 7.1 Implementation ```python -def combination_sum(candidates, target): +def combination_sum(candidates: list[int], target: int) -> list[list[int]]: """ - Find all combinations that sum to target. - Elements can be reused. + Find all combinations that sum to target. Each number can be used unlimited times. + + Algorithm: + - Track remaining target (target - current sum) + - When remaining = 0, found a valid combination + - Allow reuse by NOT incrementing start_index when recursing + - Prune when remaining < 0 (overshot target) + + Key Difference from Combinations: + - Reuse allowed: recurse with same index i, not i+1 + - This means we can pick the same element multiple times + + Time Complexity: O(n^(t/m)) where t=target, m=min(candidates) + - Branching factor up to n at each level + - Depth up to t/m (using smallest element repeatedly) - Time Complexity: O(2ⁿ) worst case, better with pruning - Space Complexity: O(target) - recursion depth + Space Complexity: O(t/m) for recursion depth + + Args: + candidates: Array of distinct positive integers + target: Target sum + + Returns: + All unique combinations that sum to target """ - results = [] + results: list[list[int]] = [] + path: list[int] = [] - def backtrack(start, path, remaining): + # Optional: Sort for consistent output order + candidates.sort() + + def backtrack(start_index: int, remaining: int) -> None: # BASE CASE: Found valid combination if remaining == 0: results.append(path[:]) return - # PRUNING: Sum exceeds target + # PRUNING: Overshot target if remaining < 0: return - for i in range(start, len(candidates)): - # MAKE CHOICE + for i in range(start_index, len(candidates)): + # PRUNING: If current candidate exceeds remaining, + # all subsequent (if sorted) will too + if candidates[i] > remaining: + break + path.append(candidates[i]) - # RECURSE: Start from i (allow reuse) - backtrack(i, path, remaining - candidates[i]) + # REUSE ALLOWED: Recurse with same index i + backtrack(i, remaining - candidates[i]) - # BACKTRACK path.pop() - backtrack(0, [], target) + backtrack(0, target) return results ``` -#### Template (No Reuse) +### 7.2 Reuse vs No-Reuse Comparison + +| Aspect | With Reuse (LC 39) | Without Reuse (LC 40) | +|--------|-------------------|----------------------| +| Recurse with | `backtrack(i, ...)` | `backtrack(i+1, ...)` | +| Same element | Can appear multiple times | Can appear at most once | +| Deduplication | Not needed (distinct) | Needed (may have duplicates) | + +--- + +## 8. Variation: Combination Sum II (LeetCode 40) + +> **Problem**: Find combinations that sum to target. Each element used at most once. Input may have duplicates. +> **Delta from Combination Sum**: No reuse + duplicate handling. +> **Key Insight**: Sort + same-level skip for duplicates. + +### 8.1 Implementation ```python -def combination_sum2(candidates, target): - """Elements cannot be reused. Handle duplicates.""" +def combination_sum2(candidates: list[int], target: int) -> list[list[int]]: + """ + Find all unique combinations that sum to target. Each number used at most once. + Input may contain duplicates. + + Algorithm: + - Sort to bring duplicates together + - Use start_index to prevent reuse (i+1 when recursing) + - Same-level deduplication: skip if current == previous at same level + + Deduplication Rule: + - Skip candidates[i] if i > start_index AND candidates[i] == candidates[i-1] + - This prevents generating duplicate combinations + + Time Complexity: O(2^n) worst case + Space Complexity: O(n) + + Args: + candidates: Array of positive integers (may have duplicates) + target: Target sum + + Returns: + All unique combinations summing to target + """ + results: list[list[int]] = [] + path: list[int] = [] + + # CRITICAL: Sort for deduplication candidates.sort() - results = [] - def backtrack(start, path, remaining): + def backtrack(start_index: int, remaining: int) -> None: if remaining == 0: results.append(path[:]) return + if remaining < 0: return - for i in range(start, len(candidates)): - # Skip duplicates - if i > start and candidates[i] == candidates[i-1]: + for i in range(start_index, len(candidates)): + # DEDUPLICATION: Skip same value at same level + if i > start_index and candidates[i] == candidates[i - 1]: continue + # PRUNING: Current exceeds remaining (sorted, so break) + if candidates[i] > remaining: + break + path.append(candidates[i]) - backtrack(i + 1, path, remaining - candidates[i]) # i+1: no reuse + + # NO REUSE: Recurse with i+1 + backtrack(i + 1, remaining - candidates[i]) + path.pop() - backtrack(0, [], target) + backtrack(0, target) + return results +``` + +--- + +## 9. Variation: Combination Sum III (LeetCode 216) + +> **Problem**: Find k numbers from [1-9] that sum to n. Each number used at most once. +> **Delta from Combination Sum II**: Fixed count k + bounded range [1-9]. +> **Key Insight**: Dual constraint — both count and sum must be satisfied. + +### 9.1 Implementation + +```python +def combination_sum3(k: int, n: int) -> list[list[int]]: + """ + Find all combinations of k numbers from [1-9] that sum to n. + + Algorithm: + - Fixed size k (must have exactly k numbers) + - Fixed sum n (must sum to exactly n) + - Range is [1-9], all distinct, no reuse + + Pruning Strategies: + 1. If current sum exceeds n, stop + 2. If path length exceeds k, stop + 3. If remaining numbers can't fill to k, stop + + Time Complexity: O(C(9,k) × k) + Space Complexity: O(k) + + Args: + k: Number of elements required + n: Target sum + + Returns: + All valid combinations + """ + results: list[list[int]] = [] + path: list[int] = [] + + def backtrack(start: int, remaining: int) -> None: + # BASE CASE: Have k numbers + if len(path) == k: + if remaining == 0: + results.append(path[:]) + return + + # PRUNING: Not enough numbers left to fill path + if 9 - start + 1 < k - len(path): + return + + for i in range(start, 10): + # PRUNING: Current number too large + if i > remaining: + break + + path.append(i) + backtrack(i + 1, remaining - i) + path.pop() + + backtrack(1, n) + return results +``` + +--- + +## 10. Variation: N-Queens (LeetCode 51/52) + +> **Problem**: Place n queens on an n×n board so no two queens attack each other. +> **Sub-Pattern**: Constraint satisfaction with row-by-row placement. +> **Key Insight**: Track columns and diagonals as constraint sets. + +### 10.1 Implementation + +```python +def solve_n_queens(n: int) -> list[list[str]]: + """ + Find all solutions to the N-Queens puzzle. + + Algorithm: + - Place queens row by row (one queen per row guaranteed) + - Track three constraints: + 1. Columns: No two queens in same column + 2. Main diagonals (↘): row - col is constant + 3. Anti-diagonals (↙): row + col is constant + - Use hash sets for O(1) constraint checking + + Key Insight: + - Row-by-row placement eliminates row conflicts by construction + - Only need to check column and diagonal conflicts + + Time Complexity: O(n!) + - At row 0: n choices + - At row 1: at most n-1 choices + - ... and so on + + Space Complexity: O(n) for constraint sets and recursion + + Args: + n: Board size + + Returns: + All valid board configurations as string arrays + """ + results: list[list[str]] = [] + + # State: queen_cols[row] = column where queen is placed + queen_cols: list[int] = [-1] * n + + # Constraint sets for O(1) conflict checking + used_cols: set[int] = set() + used_diag_main: set[int] = set() # row - col + used_diag_anti: set[int] = set() # row + col + + def backtrack(row: int) -> None: + # BASE CASE: All queens placed + if row == n: + results.append(build_board(queen_cols, n)) + return + + # Try each column in current row + for col in range(n): + # Calculate diagonal identifiers + diag_main = row - col + diag_anti = row + col + + # CONSTRAINT CHECK (pruning) + if col in used_cols: + continue + if diag_main in used_diag_main: + continue + if diag_anti in used_diag_anti: + continue + + # CHOOSE: Place queen + queen_cols[row] = col + used_cols.add(col) + used_diag_main.add(diag_main) + used_diag_anti.add(diag_anti) + + # EXPLORE: Move to next row + backtrack(row + 1) + + # UNCHOOSE: Remove queen + queen_cols[row] = -1 + used_cols.discard(col) + used_diag_main.discard(diag_main) + used_diag_anti.discard(diag_anti) + + backtrack(0) return results + + +def build_board(queen_cols: list[int], n: int) -> list[str]: + """Convert queen positions to board representation.""" + board = [] + for col in queen_cols: + row = '.' * col + 'Q' + '.' * (n - col - 1) + board.append(row) + return board +``` + +### 10.2 Diagonal Identification + +``` +Main diagonal (↘): cells where row - col is constant + (0,0) (1,1) (2,2) → row - col = 0 + (0,1) (1,2) (2,3) → row - col = -1 + (1,0) (2,1) (3,2) → row - col = 1 + +Anti-diagonal (↙): cells where row + col is constant + (0,2) (1,1) (2,0) → row + col = 2 + (0,3) (1,2) (2,1) (3,0) → row + col = 3 +``` + +### 10.3 N-Queens II (Count Only) + +```python +def total_n_queens(n: int) -> int: + """Count solutions without building boards.""" + count = 0 + + used_cols: set[int] = set() + used_diag_main: set[int] = set() + used_diag_anti: set[int] = set() + + def backtrack(row: int) -> None: + nonlocal count + if row == n: + count += 1 + return + + for col in range(n): + dm, da = row - col, row + col + if col in used_cols or dm in used_diag_main or da in used_diag_anti: + continue + + used_cols.add(col) + used_diag_main.add(dm) + used_diag_anti.add(da) + + backtrack(row + 1) + + used_cols.discard(col) + used_diag_main.discard(dm) + used_diag_anti.discard(da) + + backtrack(0) + return count ``` -#### Complexity Notes +--- -| Aspect | Analysis | -|--------|----------| -| Time | O(2ⁿ) worst case, often O(2^(target/min)) with pruning | -| Space | O(target/min) — recursion depth | -| Pruning | Very effective when sorted + early termination | +## 11. Variation: Palindrome Partitioning (LeetCode 131) -#### LeetCode Problems +> **Problem**: Partition a string such that every substring is a palindrome. +> **Sub-Pattern**: String segmentation with validity check. +> **Key Insight**: Try all cut positions, validate each segment. -| ID | Problem | Variation | -|----|---------|-----------| -| 39 | Combination Sum | Allow reuse | -| 40 | Combination Sum II | No reuse, handle duplicates | -| 216 | Combination Sum III | Size-k constraint | -| 377 | Combination Sum IV | Count ways (DP better) | +### 11.1 Implementation + +```python +def partition(s: str) -> list[list[str]]: + """ + Partition string so every part is a palindrome. + + Algorithm: + - Try cutting at each position from current start + - Check if prefix is palindrome; if yes, recurse on suffix + - When start reaches end of string, we have a valid partition + + Key Insight: + - Each "choice" is where to cut the string + - Only proceed if the cut-off prefix is a palindrome + + Optimization: + - Precompute palindrome status with DP for O(1) checks + - Without precompute: O(n) per check, O(n^3) total + - With precompute: O(n^2) preprocessing, O(1) per check + + Time Complexity: O(n × 2^n) worst case + - 2^(n-1) possible partitions (n-1 cut positions) + - O(n) to copy each partition + + Space Complexity: O(n) for recursion + + Args: + s: Input string + + Returns: + All palindrome partitionings + """ + results: list[list[str]] = [] + path: list[str] = [] + n = len(s) + + # Precompute: is_palindrome[i][j] = True if s[i:j+1] is palindrome + is_palindrome = [[False] * n for _ in range(n)] + for i in range(n - 1, -1, -1): + for j in range(i, n): + if s[i] == s[j]: + if j - i <= 2: + is_palindrome[i][j] = True + else: + is_palindrome[i][j] = is_palindrome[i + 1][j - 1] + + def backtrack(start: int) -> None: + # BASE CASE: Reached end of string + if start == n: + results.append(path[:]) + return + + # Try each end position for current segment + for end in range(start, n): + # VALIDITY CHECK: Only proceed if palindrome + if not is_palindrome[start][end]: + continue + + path.append(s[start:end + 1]) + backtrack(end + 1) + path.pop() + + backtrack(0) + return results +``` --- -### 2.4 Grid Search Template +## 12. Variation: Restore IP Addresses (LeetCode 93) -> **Strategy**: Explore 2D grid to find paths matching a pattern. -> **Key Insight**: Mark visited cells temporarily, restore after backtracking. -> **Time Complexity**: O(m × n × 4^L) where L is pattern length. +> **Problem**: Return all valid IP addresses that can be formed from a digit string. +> **Sub-Pattern**: String segmentation with multi-constraint validity. +> **Key Insight**: Fixed 4 segments, each 1-3 digits, value 0-255, no leading zeros. -#### When to Use +### 12.1 Implementation -- **2D grid exploration** problems -- **Path finding** with constraints -- **Word search** in grid -- Need to avoid revisiting same cell in current path -- Often combined with early return for existence check +```python +def restore_ip_addresses(s: str) -> list[str]: + """ + Generate all valid IP addresses from a digit string. + + Constraints per segment: + 1. Length: 1-3 characters + 2. Value: 0-255 + 3. No leading zeros (except "0" itself) + + Algorithm: + - Exactly 4 segments required + - Try 1, 2, or 3 characters for each segment + - Validate each segment before proceeding + + Pruning: + - Early termination if remaining chars can't form remaining segments + - Min remaining = segments_left × 1 + - Max remaining = segments_left × 3 + + Time Complexity: O(3^4 × n) = O(81 × n) = O(n) + - At most 3 choices per segment, 4 segments + - O(n) to validate/copy + + Space Complexity: O(4) = O(1) for path + + Args: + s: String of digits + + Returns: + All valid IP addresses + """ + results: list[str] = [] + segments: list[str] = [] + n = len(s) + + def is_valid_segment(segment: str) -> bool: + """Check if segment is a valid IP octet.""" + if not segment: + return False + if len(segment) > 1 and segment[0] == '0': + return False # No leading zeros + if int(segment) > 255: + return False + return True + + def backtrack(start: int, segment_count: int) -> None: + # PRUNING: Check remaining length bounds + remaining = n - start + remaining_segments = 4 - segment_count + + if remaining < remaining_segments: # Too few chars + return + if remaining > remaining_segments * 3: # Too many chars + return + + # BASE CASE: 4 segments formed + if segment_count == 4: + if start == n: # Used all characters + results.append('.'.join(segments)) + return + + # Try 1, 2, or 3 character segments + for length in range(1, 4): + if start + length > n: + break + + segment = s[start:start + length] + + if not is_valid_segment(segment): + continue + + segments.append(segment) + backtrack(start + length, segment_count + 1) + segments.pop() + + backtrack(0, 0) + return results +``` + +--- -#### Template +## 13. Variation: Word Search (LeetCode 79) + +> **Problem**: Find if a word exists in a grid by traversing adjacent cells. +> **Sub-Pattern**: Grid/Path DFS with visited marking. +> **Key Insight**: Mark visited, explore neighbors, unmark on backtrack. + +### 13.1 Implementation ```python -def exist(grid, word): +def exist(board: list[list[str]], word: str) -> bool: """ - Check if word exists in grid (can move 4-directionally). + Check if word exists in grid by traversing adjacent cells. + + Algorithm: + - Start DFS from each cell that matches word[0] + - Mark current cell as visited (modify in-place or use set) + - Try all 4 directions for next character + - Unmark on backtrack + + Key Insight: + - Each cell can be used at most once per path + - In-place marking (temporary modification) is efficient + + Pruning: + - Early return on mismatch + - Can add frequency check: if board doesn't have enough of each char + + Time Complexity: O(m × n × 4^L) where L = len(word) + - m×n starting positions + - 4 choices at each step, depth L + + Space Complexity: O(L) for recursion depth - Time Complexity: O(m × n × 4^L) - L is word length - Space Complexity: O(L) - recursion depth + Args: + board: 2D character grid + word: Target word to find + + Returns: + True if word can be formed """ - rows, cols = len(grid), len(grid[0]) + if not board or not board[0]: + return False - def backtrack(r, c, index): - # BASE CASE: Found complete word - if index == len(word): + rows, cols = len(board), len(board[0]) + word_len = len(word) + + # Directions: up, down, left, right + directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] + + def backtrack(row: int, col: int, index: int) -> bool: + # BASE CASE: All characters matched + if index == word_len: return True # BOUNDARY CHECK - if r < 0 or r >= rows or c < 0 or c >= cols: + if row < 0 or row >= rows or col < 0 or col >= cols: return False - # CONSTRAINT CHECK: Character doesn't match - if grid[r][c] != word[index]: + # CHARACTER CHECK + if board[row][col] != word[index]: return False - # MARK VISITED: Temporarily mark to avoid reuse in current path - temp = grid[r][c] - grid[r][c] = '#' + # MARK AS VISITED (in-place modification) + original = board[row][col] + board[row][col] = '#' # Temporary marker # EXPLORE: Try all 4 directions - for dr, dc in [(-1,0), (1,0), (0,-1), (0,1)]: - if backtrack(r + dr, c + dc, index + 1): - # Found solution, restore and return - grid[r][c] = temp + for dr, dc in directions: + if backtrack(row + dr, col + dc, index + 1): + # Found! Restore and return + board[row][col] = original return True - # BACKTRACK: Restore cell - grid[r][c] = temp + # UNMARK (backtrack) + board[row][col] = original return False # Try starting from each cell for r in range(rows): for c in range(cols): - if backtrack(r, c, 0): - return True + if board[r][c] == word[0]: + if backtrack(r, c, 0): + return True + return False ``` -#### Alternative: Using Visited Set +### 13.2 In-Place Marking vs Visited Set + +| Approach | Pros | Cons | +|----------|------|------| +| In-place (`#`) | O(1) space, fast | Modifies input temporarily | +| Visited set | Clean, no mutation | O(L) space for coordinates | + +--- + +## 14. Deduplication Strategies + +### 14.1 Strategy Comparison + +| Strategy | When to Use | Example | +|----------|-------------|---------| +| **Sorting + Same-Level Skip** | Input has duplicates | Permutations II, Subsets II | +| **Start Index** | Subsets/Combinations (order doesn't matter) | Subsets, Combinations | +| **Used Array** | Permutations (all elements, order matters) | Permutations | +| **Canonical Ordering** | Implicit via index ordering | All subset-like problems | + +### 14.2 Same-Level Skip Pattern ```python -def exist_with_set(grid, word): - """Alternative using visited set (more memory but clearer).""" - rows, cols = len(grid), len(grid[0]) - - def backtrack(r, c, index, visited): - if index == len(word): - return True - if (r < 0 or r >= rows or c < 0 or c >= cols or - (r, c) in visited or grid[r][c] != word[index]): - return False - - visited.add((r, c)) - for dr, dc in [(-1,0), (1,0), (0,-1), (0,1)]: - if backtrack(r + dr, c + dc, index + 1, visited): - return True - visited.remove((r, c)) - return False - - for r in range(rows): - for c in range(cols): - if backtrack(r, c, 0, set()): - return True - return False +# Sort first, then skip duplicates at same level +nums.sort() + +for i in range(start, n): + # Skip if current equals previous at same tree level + if i > start and nums[i] == nums[i - 1]: + continue + # ... process nums[i] +``` + +### 14.3 Used Array Pattern + +```python +# For permutations with duplicates +if i > 0 and nums[i] == nums[i - 1] and not used[i - 1]: + continue +# This ensures we use duplicates in order (leftmost first) ``` -#### Complexity Notes +--- -| Aspect | Analysis | -|--------|----------| -| Time | O(m × n × 4^L) — L is pattern length, 4 directions | -| Space | O(L) — recursion depth (or O(m×n) with visited set) | -| Optimization | Early return when existence check only | +## 15. Pruning Techniques -#### LeetCode Problems +### 15.1 Pruning Categories -| ID | Problem | Variation | -|----|---------|-----------| -| 79 | Word Search | Basic grid search | -| 212 | Word Search II | Multiple words (Trie + backtrack) | -| 130 | Surrounded Regions | Flood fill variant | -| 200 | Number of Islands | DFS on grid | +| Category | Description | Example | +|----------|-------------|---------| +| **Feasibility Bound** | Remaining elements can't satisfy constraints | Combinations: not enough elements left | +| **Target Bound** | Current path already exceeds target | Combination Sum: sum > target | +| **Constraint Propagation** | Future choices are forced/impossible | N-Queens: no valid columns left | +| **Sorted Early Exit** | If sorted, larger elements also fail | Combination Sum with sorted candidates | + +### 15.2 Pruning Patterns + +```python +# 1. Not enough elements left (Combinations) +if remaining_elements < elements_needed: + return + +# 2. Exceeded target (Combination Sum) +if current_sum > target: + return + +# 3. Sorted early break (when candidates sorted) +if candidates[i] > remaining: + break # All subsequent are larger + +# 4. Length/count bound +if len(path) > max_allowed: + return +``` --- -### 2.5 Constraint Satisfaction Template +## 16. Pattern Comparison Table + +| Problem | Sub-Pattern | State | Dedup Strategy | Pruning | +|---------|-------------|-------|----------------|---------| +| Permutations (46) | Permutation | used[] | None (distinct) | None | +| Permutations II (47) | Permutation | used[] | Sort + level skip | Same-level | +| Subsets (78) | Subset | start_idx | Index ordering | None | +| Subsets II (90) | Subset | start_idx | Sort + level skip | Same-level | +| Combinations (77) | Combination | start_idx | Index ordering | Count bound | +| Combination Sum (39) | Target Search | start_idx | None (distinct) | Target bound | +| Combination Sum II (40) | Target Search | start_idx | Sort + level skip | Target + level | +| Combination Sum III (216) | Target Search | start_idx | None (1-9 distinct) | Count + target | +| N-Queens (51) | Constraint | constraint sets | Row-by-row | Constraints | +| Palindrome Part. (131) | Segmentation | start_idx | None | Validity check | +| IP Addresses (93) | Segmentation | start_idx, count | None | Length bounds | +| Word Search (79) | Grid Path | visited | Path uniqueness | Boundary + char | -> **Strategy**: Solve problems with multiple constraints (N-Queens, Sudoku). -> **Key Insight**: Check constraints before making choice, prune aggressively. -> **Time Complexity**: Varies, often exponential but heavily pruned. +--- + +## 17. When to Use Backtracking + +### 17.1 Problem Indicators + +✅ **Use backtracking when:** +- Need to enumerate all solutions (permutations, combinations, etc.) +- Decision tree structure (sequence of choices) +- Constraints can be checked incrementally +- Solution can be built piece by piece + +❌ **Consider alternatives when:** +- Only need count (use DP with counting) +- Only need one solution (may use greedy or simple DFS) +- Optimization problem (consider DP or greedy) +- State space is too large even with pruning + +### 17.2 Decision Guide + +``` +Is the problem asking for ALL solutions? +├── Yes → Does solution have natural ordering/structure? +│ ├── Permutation → Use used[] array +│ ├── Subset/Combination → Use start_index +│ ├── Grid path → Use visited marking +│ └── Constraint satisfaction → Use constraint sets +└── No → Need single solution or count? + ├── Single solution → Simple DFS may suffice + └── Count → Consider DP +``` + +--- -#### When to Use +## 18. LeetCode Problem Mapping + +| Sub-Pattern | Problems | +|-------------|----------| +| **Permutation Enumeration** | 46. Permutations, 47. Permutations II | +| **Subset/Combination** | 78. Subsets, 90. Subsets II, 77. Combinations | +| **Target Search** | 39. Combination Sum, 40. Combination Sum II, 216. Combination Sum III | +| **Constraint Satisfaction** | 51. N-Queens, 52. N-Queens II, 37. Sudoku Solver | +| **String Partitioning** | 131. Palindrome Partitioning, 93. Restore IP Addresses, 140. Word Break II | +| **Grid/Path Search** | 79. Word Search, 212. Word Search II | + +--- -- **Multiple constraints** must be satisfied simultaneously -- **Placement problems** (N-Queens, Sudoku) -- Can check validity before making choice -- Often benefits from constraint propagation +## 19. Template Quick Reference -#### Template (N-Queens) +### 19.1 Permutation Template ```python -def solve_n_queens(n): - """ - Place n queens on n×n board so none attack each other. - - Time Complexity: O(n!) worst case, much better with pruning - Space Complexity: O(n) - recursion depth + board storage - """ +def permute(nums): results = [] - board = [['.' for _ in range(n)] for _ in range(n)] - - def is_valid(row, col): - """Check if placing queen at (row, col) is valid.""" - # Check column - for i in range(row): - if board[i][col] == 'Q': - return False - - # Check diagonal: top-left to bottom-right - i, j = row - 1, col - 1 - while i >= 0 and j >= 0: - if board[i][j] == 'Q': - return False - i -= 1 - j -= 1 - - # Check diagonal: top-right to bottom-left - i, j = row - 1, col + 1 - while i >= 0 and j < n: - if board[i][j] == 'Q': - return False - i -= 1 - j += 1 - - return True + used = [False] * len(nums) - def backtrack(row): - # BASE CASE: Placed all queens - if row == n: - results.append([''.join(row) for row in board]) + def backtrack(path): + if len(path) == len(nums): + results.append(path[:]) return - # CHOICES: Try each column in current row - for col in range(n): - # PRUNING: Check validity before placing - if not is_valid(row, col): + for i in range(len(nums)): + if used[i]: continue - - # MAKE CHOICE - board[row][col] = 'Q' - - # RECURSE - backtrack(row + 1) - - # BACKTRACK - board[row][col] = '.' + used[i] = True + path.append(nums[i]) + backtrack(path) + path.pop() + used[i] = False - backtrack(0) + backtrack([]) + return results +``` + +### 19.2 Subset/Combination Template + +```python +def subsets(nums): + results = [] + + def backtrack(start, path): + results.append(path[:]) # Collect at every node + + for i in range(start, len(nums)): + path.append(nums[i]) + backtrack(i + 1, path) # i+1 for no reuse + path.pop() + + backtrack(0, []) return results ``` -#### Complexity Notes +### 19.3 Target Sum Template + +```python +def combination_sum(candidates, target): + results = [] + + def backtrack(start, path, remaining): + if remaining == 0: + results.append(path[:]) + return + if remaining < 0: + return + + for i in range(start, len(candidates)): + path.append(candidates[i]) + backtrack(i, path, remaining - candidates[i]) # i for reuse + path.pop() + + backtrack(0, [], target) + return results +``` -| Aspect | Analysis | -|--------|----------| -| Time | O(n!) worst case, heavily pruned in practice | -| Space | O(n²) — board storage + O(n) recursion | -| Optimization | Constraint checking before placement is crucial | +### 19.4 Grid Search Template -#### LeetCode Problems +```python +def grid_search(grid, word): + rows, cols = len(grid), len(grid[0]) + + def backtrack(r, c, index): + if index == len(word): + return True + if r < 0 or r >= rows or c < 0 or c >= cols: + return False + if grid[r][c] != word[index]: + return False + + temp = grid[r][c] + grid[r][c] = '#' + + for dr, dc in [(-1,0), (1,0), (0,-1), (0,1)]: + if backtrack(r + dr, c + dc, index + 1): + grid[r][c] = temp + return True + + grid[r][c] = temp + return False + + for r in range(rows): + for c in range(cols): + if backtrack(r, c, 0): + return True + return False -| ID | Problem | Variation | -|----|---------|-----------| -| 51 | N-Queens | Basic constraint satisfaction | -| 52 | N-Queens II | Count solutions only | -| 37 | Sudoku Solver | 9×9 grid with 3×3 boxes | +``` diff --git a/meta/patterns/backtracking_exploration/_header.md b/meta/patterns/backtracking_exploration/_header.md index 9bbb78b..095e1ed 100644 --- a/meta/patterns/backtracking_exploration/_header.md +++ b/meta/patterns/backtracking_exploration/_header.md @@ -9,93 +9,87 @@ This document presents the **canonical backtracking template** and all its major ## Core Concepts -### The Backtracking Process +### What is Backtracking? -Backtracking is a systematic search technique that builds solutions incrementally and abandons partial solutions that cannot lead to valid complete solutions. +Backtracking is a **systematic trial-and-error** approach that incrementally builds candidates to the solutions and abandons a candidate ("backtracks") as soon as it determines that the candidate cannot lead to a valid solution. ``` -Backtracking State: -┌─────────────────────────────────────────────────────────┐ -│ [choice₁] → [choice₂] → [choice₃] → ... → [choiceₙ] │ -│ │ │ │ │ │ -│ └───────────┴───────────┴──────────────┘ │ -│ Path (current partial solution) │ -│ │ -│ When constraint violated: │ -│ Backtrack: undo last choice, try next alternative │ -└─────────────────────────────────────────────────────────┘ +Decision Tree Visualization: + + [] + ┌────────┼────────┐ + [1] [2] [3] + ┌──┴──┐ ┌──┴──┐ ┌──┴──┐ + [1,2] [1,3] [2,1] [2,3] [3,1] [3,2] + │ │ │ │ │ │ + [1,2,3] ... (continue building) + ↓ + SOLUTION FOUND → collect and backtrack ``` -### Universal Template Structure +### The Three-Step Pattern: Choose → Explore → Unchoose + +Every backtracking algorithm follows this fundamental pattern: ```python -def backtracking_template(problem_state): +def backtrack(state, choices): """ - Generic backtracking template. + Core backtracking template. - Key components: - 1. Base Case: Check if current path is a complete solution - 2. Pruning: Abandon paths that violate constraints - 3. Choices: Generate all valid choices at current state - 4. Make Choice: Add choice to path, update state - 5. Recurse: Explore further with updated state - 6. Backtrack: Undo choice, restore state + 1. BASE CASE: Check if current state is a complete solution + 2. RECURSIVE CASE: For each available choice: + a) CHOOSE: Make a choice and update state + b) EXPLORE: Recursively explore with updated state + c) UNCHOOSE: Undo the choice (backtrack) """ - results = [] + # BASE CASE: Is this a complete solution? + if is_solution(state): + collect_solution(state) + return - def backtrack(path, state): - # BASE CASE: Check if solution is complete - if is_complete(path, state): - results.append(path[:]) # Copy path - return + # RECURSIVE CASE: Try each choice + for choice in get_available_choices(state, choices): + # CHOOSE: Make this choice + apply_choice(state, choice) - # PRUNING: Abandon invalid paths early - if violates_constraints(path, state): - return + # EXPLORE: Recurse with updated state + backtrack(state, remaining_choices(choices, choice)) - # CHOICES: Generate all valid choices - for choice in generate_choices(path, state): - # MAKE CHOICE: Add to path, update state - path.append(choice) - update_state(state, choice) - - # RECURSE: Explore further - backtrack(path, state) - - # BACKTRACK: Undo choice, restore state - path.pop() - restore_state(state, choice) - - backtrack([], initial_state) - return results + # UNCHOOSE: Undo the choice (restore state) + undo_choice(state, choice) ``` -### Backtracking Family Overview +### Key Invariants + +| Invariant | Description | +|-----------|-------------| +| **State Consistency** | After backtracking, state must be exactly as before the choice was made | +| **Exhaustive Exploration** | Every valid solution must be reachable through some path | +| **Pruning Soundness** | Pruned branches must not contain any valid solutions | +| **No Duplicates** | Each unique solution must be generated exactly once | -| Sub-Pattern | Key Characteristic | Primary Use Case | -|-------------|-------------------|------------------| -| **Permutation** | All elements used, order matters | Generate all arrangements | -| **Subset/Combination** | Select subset, order doesn't matter | Generate all subsets/combinations | -| **Target Sum** | Constraint on sum/value | Find combinations meeting target | -| **Grid Search** | 2D space exploration | Path finding, word search | -| **Constraint Satisfaction** | Multiple constraints | N-Queens, Sudoku | +### Time Complexity Discussion -### When to Use Backtracking +Backtracking algorithms typically have exponential or factorial complexity because they explore the entire solution space: -- **Exhaustive Search**: Need to explore all possible solutions -- **Constraint Satisfaction**: Multiple constraints must be satisfied simultaneously -- **Decision Problem**: Need to find ANY valid solution (can optimize with early return) -- **Enumeration**: Need to list ALL valid solutions -- **Pruning Opportunity**: Can eliminate large portions of search space early +| Problem Type | Typical Complexity | Output Size | +|--------------|-------------------|-------------| +| Permutations | O(n! × n) | n! | +| Subsets | O(2^n × n) | 2^n | +| Combinations C(n,k) | O(C(n,k) × k) | C(n,k) | +| N-Queens | O(n!) | variable | -### Why It Works +**Important**: The complexity is often **output-sensitive** — if there are many solutions, generating them all is inherently expensive. -Backtracking systematically explores the solution space by: -1. **Building incrementally**: Each recursive call extends the current partial solution -2. **Pruning early**: Invalid paths are abandoned immediately, saving computation -3. **Exploring exhaustively**: All valid paths are explored through recursion -4. **Undoing choices**: Backtracking allows exploring alternative paths from the same state +### Sub-Pattern Classification -The key insight is that by maintaining state and undoing choices, we can explore all possibilities without storing all partial solutions explicitly. +| Sub-Pattern | Key Characteristic | Examples | +|-------------|-------------------|----------| +| **Permutation** | Used/visited tracking | LeetCode 46, 47 | +| **Subset/Combination** | Start-index canonicalization | LeetCode 78, 90, 77 | +| **Target Search** | Remaining/target pruning | LeetCode 39, 40, 216 | +| **Constraint Satisfaction** | Row-by-row with constraint sets | LeetCode 51, 52 | +| **String Partitioning** | Cut positions with validity | LeetCode 131, 93 | +| **Grid/Path Search** | Visited marking and undo | LeetCode 79 | --- diff --git a/meta/patterns/backtracking_exploration/_templates.md b/meta/patterns/backtracking_exploration/_templates.md index b3c9d5f..baf5251 100644 --- a/meta/patterns/backtracking_exploration/_templates.md +++ b/meta/patterns/backtracking_exploration/_templates.md @@ -2,62 +2,8 @@ ### Permutation Template -> **Strategy**: Generate all arrangements where all elements are used and order matters. -> **Key Insight**: Use a `used` array to track which elements are already in the current path. -> **Time Complexity**: O(n! × n) — n! permutations, each takes O(n) to copy. - -#### When to Use - -- Generate all **arrangements** of elements -- Order matters (e.g., [1,2,3] ≠ [2,1,3]) -- All elements must be used exactly once -- No duplicates in input (or handle duplicates with sorting + skipping) - -#### Template - ```python def permute(nums): - """ - Generate all permutations of nums. - - Time Complexity: O(n! × n) - n! permutations, O(n) to copy each - Space Complexity: O(n) - recursion depth + path storage - """ - results = [] - used = [False] * len(nums) - - def backtrack(path): - # BASE CASE: Complete permutation found - if len(path) == len(nums): - results.append(path[:]) # Copy path - return - - # CHOICES: Try all unused elements - for i in range(len(nums)): - if used[i]: - continue # Skip already used elements - - # MAKE CHOICE - used[i] = True - path.append(nums[i]) - - # RECURSE - backtrack(path) - - # BACKTRACK - path.pop() - used[i] = False - - backtrack([]) - return results -``` - -#### Handling Duplicates - -```python -def permute_unique(nums): - """Handle duplicates by sorting and skipping same values.""" - nums.sort() results = [] used = [False] * len(nums) @@ -69,10 +15,6 @@ def permute_unique(nums): for i in range(len(nums)): if used[i]: continue - # Skip duplicates: if same as previous and previous not used - if i > 0 and nums[i] == nums[i-1] and not used[i-1]: - continue - used[i] = True path.append(nums[i]) backtrack(path) @@ -83,408 +25,76 @@ def permute_unique(nums): return results ``` -#### Complexity Notes - -| Aspect | Analysis | -|--------|----------| -| Time | O(n! × n) — n! permutations, O(n) to copy each | -| Space | O(n) — recursion depth + path storage | -| Pruning | Early termination possible if only need existence check | - -#### LeetCode Problems - -| ID | Problem | Variation | -|----|---------|-----------| -| 46 | Permutations | Basic permutation | -| 47 | Permutations II | Handle duplicates | -| 60 | Permutation Sequence | Find k-th permutation (math) | - ---- - ### Subset/Combination Template -> **Strategy**: Generate all subsets/combinations where order doesn't matter. -> **Key Insight**: Use `start` index to avoid duplicates and ensure order. -> **Time Complexity**: O(2ⁿ × n) — 2ⁿ subsets, each takes O(n) to copy. - -#### When to Use - -- Generate all **subsets** or **combinations** -- Order doesn't matter (e.g., [1,2] = [2,1]) -- Elements can be skipped -- Often combined with constraints (size limit, sum, etc.) - -#### Template - ```python def subsets(nums): - """ - Generate all subsets of nums. - - Time Complexity: O(2ⁿ × n) - 2ⁿ subsets, O(n) to copy each - Space Complexity: O(n) - recursion depth + path storage - """ results = [] def backtrack(start, path): - # COLLECT: Add current path (every node is a valid subset) - results.append(path[:]) + results.append(path[:]) # Collect at every node - # CHOICES: Try elements starting from 'start' for i in range(start, len(nums)): - # MAKE CHOICE path.append(nums[i]) - - # RECURSE: Next start is i+1 (no reuse) - backtrack(i + 1, path) - - # BACKTRACK + backtrack(i + 1, path) # i+1 for no reuse path.pop() backtrack(0, []) return results ``` -#### Combination with Size Constraint - -```python -def combine(n, k): - """Generate all combinations of k numbers from [1..n].""" - results = [] - - def backtrack(start, path): - # BASE CASE: Reached desired size - if len(path) == k: - results.append(path[:]) - return - - # PRUNING: Not enough elements remaining - if len(path) + (n - start + 1) < k: - return - - for i in range(start, n + 1): - path.append(i) - backtrack(i + 1, path) - path.pop() - - backtrack(1, []) - return results -``` - -#### Complexity Notes - -| Aspect | Analysis | -|--------|----------| -| Time | O(2ⁿ × n) — 2ⁿ subsets, O(n) to copy each | -| Space | O(n) — recursion depth + path storage | -| Optimization | Can prune early if size constraint exists | - -#### LeetCode Problems - -| ID | Problem | Variation | -|----|---------|-----------| -| 78 | Subsets | All subsets | -| 90 | Subsets II | Handle duplicates | -| 77 | Combinations | Size-k combinations | -| 39 | Combination Sum | With target sum | - ---- - ### Target Sum Template -> **Strategy**: Find combinations that sum to a target value. -> **Key Insight**: Track remaining sum and prune negative paths early. -> **Time Complexity**: O(2ⁿ) worst case, often much better with pruning. - -#### When to Use - -- Find combinations meeting a **target sum** -- Elements can be reused (or not, depending on problem) -- Early pruning possible when sum exceeds target -- Often combined with sorting for better pruning - -#### Template (With Reuse) - ```python def combination_sum(candidates, target): - """ - Find all combinations that sum to target. - Elements can be reused. - - Time Complexity: O(2ⁿ) worst case, better with pruning - Space Complexity: O(target) - recursion depth - """ results = [] def backtrack(start, path, remaining): - # BASE CASE: Found valid combination if remaining == 0: results.append(path[:]) return - - # PRUNING: Sum exceeds target if remaining < 0: return for i in range(start, len(candidates)): - # MAKE CHOICE path.append(candidates[i]) - - # RECURSE: Start from i (allow reuse) - backtrack(i, path, remaining - candidates[i]) - - # BACKTRACK + backtrack(i, path, remaining - candidates[i]) # i for reuse path.pop() backtrack(0, [], target) return results ``` -#### Template (No Reuse) - -```python -def combination_sum2(candidates, target): - """Elements cannot be reused. Handle duplicates.""" - candidates.sort() - results = [] - - def backtrack(start, path, remaining): - if remaining == 0: - results.append(path[:]) - return - if remaining < 0: - return - - for i in range(start, len(candidates)): - # Skip duplicates - if i > start and candidates[i] == candidates[i-1]: - continue - - path.append(candidates[i]) - backtrack(i + 1, path, remaining - candidates[i]) # i+1: no reuse - path.pop() - - backtrack(0, [], target) - return results -``` - -#### Complexity Notes - -| Aspect | Analysis | -|--------|----------| -| Time | O(2ⁿ) worst case, often O(2^(target/min)) with pruning | -| Space | O(target/min) — recursion depth | -| Pruning | Very effective when sorted + early termination | - -#### LeetCode Problems - -| ID | Problem | Variation | -|----|---------|-----------| -| 39 | Combination Sum | Allow reuse | -| 40 | Combination Sum II | No reuse, handle duplicates | -| 216 | Combination Sum III | Size-k constraint | -| 377 | Combination Sum IV | Count ways (DP better) | - ---- - ### Grid Search Template -> **Strategy**: Explore 2D grid to find paths matching a pattern. -> **Key Insight**: Mark visited cells temporarily, restore after backtracking. -> **Time Complexity**: O(m × n × 4^L) where L is pattern length. - -#### When to Use - -- **2D grid exploration** problems -- **Path finding** with constraints -- **Word search** in grid -- Need to avoid revisiting same cell in current path -- Often combined with early return for existence check - -#### Template - ```python -def exist(grid, word): - """ - Check if word exists in grid (can move 4-directionally). - - Time Complexity: O(m × n × 4^L) - L is word length - Space Complexity: O(L) - recursion depth - """ +def grid_search(grid, word): rows, cols = len(grid), len(grid[0]) def backtrack(r, c, index): - # BASE CASE: Found complete word if index == len(word): return True - - # BOUNDARY CHECK if r < 0 or r >= rows or c < 0 or c >= cols: return False - - # CONSTRAINT CHECK: Character doesn't match if grid[r][c] != word[index]: return False - # MARK VISITED: Temporarily mark to avoid reuse in current path temp = grid[r][c] grid[r][c] = '#' - # EXPLORE: Try all 4 directions for dr, dc in [(-1,0), (1,0), (0,-1), (0,1)]: if backtrack(r + dr, c + dc, index + 1): - # Found solution, restore and return grid[r][c] = temp return True - # BACKTRACK: Restore cell grid[r][c] = temp return False - # Try starting from each cell for r in range(rows): for c in range(cols): if backtrack(r, c, 0): return True return False -``` - -#### Alternative: Using Visited Set - -```python -def exist_with_set(grid, word): - """Alternative using visited set (more memory but clearer).""" - rows, cols = len(grid), len(grid[0]) - - def backtrack(r, c, index, visited): - if index == len(word): - return True - if (r < 0 or r >= rows or c < 0 or c >= cols or - (r, c) in visited or grid[r][c] != word[index]): - return False - - visited.add((r, c)) - for dr, dc in [(-1,0), (1,0), (0,-1), (0,1)]: - if backtrack(r + dr, c + dc, index + 1, visited): - return True - visited.remove((r, c)) - return False - - for r in range(rows): - for c in range(cols): - if backtrack(r, c, 0, set()): - return True - return False -``` - -#### Complexity Notes - -| Aspect | Analysis | -|--------|----------| -| Time | O(m × n × 4^L) — L is pattern length, 4 directions | -| Space | O(L) — recursion depth (or O(m×n) with visited set) | -| Optimization | Early return when existence check only | - -#### LeetCode Problems - -| ID | Problem | Variation | -|----|---------|-----------| -| 79 | Word Search | Basic grid search | -| 212 | Word Search II | Multiple words (Trie + backtrack) | -| 130 | Surrounded Regions | Flood fill variant | -| 200 | Number of Islands | DFS on grid | - ---- - -### Constraint Satisfaction Template - -> **Strategy**: Solve problems with multiple constraints (N-Queens, Sudoku). -> **Key Insight**: Check constraints before making choice, prune aggressively. -> **Time Complexity**: Varies, often exponential but heavily pruned. - -#### When to Use - -- **Multiple constraints** must be satisfied simultaneously -- **Placement problems** (N-Queens, Sudoku) -- Can check validity before making choice -- Often benefits from constraint propagation - -#### Template (N-Queens) -```python -def solve_n_queens(n): - """ - Place n queens on n×n board so none attack each other. - - Time Complexity: O(n!) worst case, much better with pruning - Space Complexity: O(n) - recursion depth + board storage - """ - results = [] - board = [['.' for _ in range(n)] for _ in range(n)] - - def is_valid(row, col): - """Check if placing queen at (row, col) is valid.""" - # Check column - for i in range(row): - if board[i][col] == 'Q': - return False - - # Check diagonal: top-left to bottom-right - i, j = row - 1, col - 1 - while i >= 0 and j >= 0: - if board[i][j] == 'Q': - return False - i -= 1 - j -= 1 - - # Check diagonal: top-right to bottom-left - i, j = row - 1, col + 1 - while i >= 0 and j < n: - if board[i][j] == 'Q': - return False - i -= 1 - j += 1 - - return True - - def backtrack(row): - # BASE CASE: Placed all queens - if row == n: - results.append([''.join(row) for row in board]) - return - - # CHOICES: Try each column in current row - for col in range(n): - # PRUNING: Check validity before placing - if not is_valid(row, col): - continue - - # MAKE CHOICE - board[row][col] = 'Q' - - # RECURSE - backtrack(row + 1) - - # BACKTRACK - board[row][col] = '.' - - backtrack(0) - return results ``` -#### Complexity Notes - -| Aspect | Analysis | -|--------|----------| -| Time | O(n!) worst case, heavily pruned in practice | -| Space | O(n²) — board storage + O(n) recursion | -| Optimization | Constraint checking before placement is crucial | - -#### LeetCode Problems - -| ID | Problem | Variation | -|----|---------|-----------| -| 51 | N-Queens | Basic constraint satisfaction | -| 52 | N-Queens II | Count solutions only | -| 37 | Sudoku Solver | 9×9 grid with 3×3 boxes | - From a54c976ffe632c35a406ade28b213ad493eef36b Mon Sep 17 00:00:00 2001 From: lufftw Date: Sun, 14 Dec 2025 14:27:56 +0800 Subject: [PATCH 04/11] feat(pattern-docs): introduce dual-track pattern documentation --- .../{sliding_window.md => sliding_window/templates.md} | 0 .../patterns/{two_pointers.md => two_pointers/templates.md} | 0 meta/patterns/sliding_window/_config.toml | 5 +++++ meta/patterns/two_pointers/_config.toml | 6 ++++++ 4 files changed, 11 insertions(+) rename docs/patterns/{sliding_window.md => sliding_window/templates.md} (100%) rename docs/patterns/{two_pointers.md => two_pointers/templates.md} (100%) diff --git a/docs/patterns/sliding_window.md b/docs/patterns/sliding_window/templates.md similarity index 100% rename from docs/patterns/sliding_window.md rename to docs/patterns/sliding_window/templates.md diff --git a/docs/patterns/two_pointers.md b/docs/patterns/two_pointers/templates.md similarity index 100% rename from docs/patterns/two_pointers.md rename to docs/patterns/two_pointers/templates.md diff --git a/meta/patterns/sliding_window/_config.toml b/meta/patterns/sliding_window/_config.toml index 962f7ee..be04fc1 100644 --- a/meta/patterns/sliding_window/_config.toml +++ b/meta/patterns/sliding_window/_config.toml @@ -24,3 +24,8 @@ footer_files = [ "_templates.md" ] +# Output configuration +# Generate to: docs/patterns/sliding_window/templates.md +[output] +subdirectory = "sliding_window" +filename = "templates.md" diff --git a/meta/patterns/two_pointers/_config.toml b/meta/patterns/two_pointers/_config.toml index 394c335..b4b473f 100644 --- a/meta/patterns/two_pointers/_config.toml +++ b/meta/patterns/two_pointers/_config.toml @@ -22,3 +22,9 @@ footer_files = [ "_templates.md" ] +# Output configuration +# Generate to: docs/patterns/two_pointers/templates.md +[output] +subdirectory = "two_pointers" +filename = "templates.md" + From 258ffcf70bd48a569e55243aae07e2d4bd2cf02d Mon Sep 17 00:00:00 2001 From: lufftw Date: Sun, 14 Dec 2025 14:38:56 +0800 Subject: [PATCH 05/11] docs: add sliding window intuition guide for pattern recognition Create intuition.md as a complementary document to templates.md that focuses on building pattern-level understanding before code: - Introduce Explorer/Gatekeeper mental model for window boundaries - Frame invariants as "promises" the window must keep - Describe maximize/minimize modes with vivid metaphors - Add pattern recognition flowchart and problem mapping table - Defer code until after conceptual understanding is established --- docs/patterns/sliding_window/intuition.md | 287 ++++++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 docs/patterns/sliding_window/intuition.md diff --git a/docs/patterns/sliding_window/intuition.md b/docs/patterns/sliding_window/intuition.md new file mode 100644 index 0000000..92ed659 --- /dev/null +++ b/docs/patterns/sliding_window/intuition.md @@ -0,0 +1,287 @@ +# Sliding Window: Pattern Intuition Guide + +> *"The window is a moving lens of attention — it forgets the past to focus on what matters now."* + +--- + +## The Situation That Calls for a Window + +Imagine you're walking through a long corridor, and you can only see through a rectangular frame you carry with you. As you move forward, new things enter your view on the right, and old things disappear on the left. + +**This is the essence of Sliding Window.** + +You encounter this pattern whenever: +- You're scanning through a sequence (string, array, stream) +- You care about a **contiguous portion** of that sequence +- The answer depends on properties of that portion +- Those properties can be **updated incrementally** as the portion shifts + +The key insight: *You don't need to remember everything — only what's currently in view.* + +--- + +## The Two Forces at Play + +Every sliding window algorithm is a dance between two opposing forces: + +### The Explorer (Right Boundary) +- Always moves forward, never backward +- Discovers new territory +- Adds new elements to consideration +- Asks: *"What happens if I include this?"* + +### The Gatekeeper (Left Boundary) +- Follows behind, cleaning up +- Removes elements that no longer serve the goal +- Enforces the rules of what's allowed +- Asks: *"Must I let go of something to stay valid?"* + +The Explorer is eager and expansive. The Gatekeeper is disciplined and selective. Together, they maintain a **window of validity** that slides through the sequence. + +--- + +## The Invariant: The Window's Promise + +At every moment, the window makes a promise — an **invariant** that must always be true: + +| Problem Type | The Promise | +|--------------|-------------| +| Longest unique substring | *"Every character in my view appears exactly once"* | +| At most K distinct | *"I contain no more than K different characters"* | +| Minimum covering substring | *"I contain everything required"* | +| Sum at least target | *"My total meets or exceeds the goal"* | + +**This promise is sacred.** The moment it's broken, the Gatekeeper must act — shrinking the window until the promise is restored. + +--- + +## The Irreversible Truth + +Here's what makes sliding window work: **the Explorer never retreats.** + +Once the right boundary passes an element, that element has been "seen." We may include it or exclude it from our current window, but we never go back to re-examine it as a potential starting point... unless the Gatekeeper releases it. + +This one-directional march is what gives us O(n) time complexity. Each element enters the window at most once and exits at most once. No element is visited more than twice across the entire algorithm. + +The irreversibility creates efficiency: *past decisions don't haunt us.* + +--- + +## The Two Modes of Seeking + +Depending on what you're optimizing, the dance changes: + +### Mode 1: Maximize the Window +*"How large can my view become while staying valid?"* + +``` +Process: +1. Explorer advances, adding new element +2. If promise breaks → Gatekeeper advances until promise restored +3. Record the current window size (this is a candidate answer) +4. Repeat + +The window EXPANDS freely, CONTRACTS only when forced. +``` + +**Mental image**: Stretching a rubber band until it's about to snap, then easing off just enough. + +### Mode 2: Minimize the Window +*"How small can my view become while still being valid?"* + +``` +Process: +1. Explorer advances until promise becomes TRUE +2. While promise holds → Gatekeeper advances, shrinking window +3. Record the window size just before promise breaks +4. Repeat + +The window EXPANDS until valid, then CONTRACTS aggressively. +``` + +**Mental image**: Tightening a noose around the minimal solution. + +--- + +## Pattern Recognition: "Is This a Sliding Window Problem?" + +Ask yourself these questions: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 1. Am I looking for a CONTIGUOUS subarray or substring? │ +│ └── No? → Not sliding window │ +│ │ +│ 2. Can I describe a PROPERTY that makes a window valid? │ +│ └── No? → Probably not sliding window │ +│ │ +│ 3. Can I UPDATE that property in O(1) when I add/remove │ +│ a single element? │ +│ └── No? → Sliding window won't give O(n) │ +│ │ +│ 4. Is the answer about OPTIMIZING that window │ +│ (longest, shortest, exists)? │ +│ └── Yes to all? → SLIDING WINDOW. │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## The Three Window Shapes + +### Shape 1: Variable Window, Maximize +**Story**: *"I want the biggest room that still follows the rules."* + +- Invariant: Some constraint must not be violated +- Strategy: Grow greedily, shrink reluctantly +- Answer: Largest valid window seen + +**Classic problems**: Longest substring without repeating characters, longest with at most K distinct + +### Shape 2: Variable Window, Minimize +**Story**: *"I want the smallest container that still holds everything I need."* + +- Invariant: Some requirement must be satisfied +- Strategy: Grow until valid, shrink aggressively +- Answer: Smallest valid window seen + +**Classic problems**: Minimum window substring, minimum size subarray sum + +### Shape 3: Fixed Window +**Story**: *"I'm looking through a frame of exact size — does it ever show what I'm looking for?"* + +- Invariant: Window size exactly K +- Strategy: Add one, remove one, check condition +- Answer: Whether/where condition is met + +**Classic problems**: Find all anagrams, check permutation inclusion + +--- + +## The State: What the Window Remembers + +The window isn't just boundaries — it carries **state** about its contents: + +| What You're Tracking | State Structure | Update Cost | +|---------------------|-----------------|-------------| +| Character uniqueness | Last-seen index map | O(1) | +| Character frequencies | Count map | O(1) | +| Distinct count | Map + size | O(1) | +| Running sum | Single integer | O(1) | +| Requirement satisfaction | "Have" vs "Need" counters | O(1) | + +The magic of sliding window is that these states are **incrementally maintainable**. Adding an element updates the state. Removing an element reverses that update. No full recomputation needed. + +--- + +## Visualizing the Dance + +``` +Sequence: [ a b c a b c b b ] + ↑ + Both start here + +Step 1: [ a ] Explorer sees 'a' + L R + +Step 2: [ a b ] Explorer sees 'b' + L R + +Step 3: [ a b c ] Explorer sees 'c' + L R + +Step 4: [ a b c a ] Explorer sees 'a' — duplicate! + L R + + Gatekeeper must act: Move L past the first 'a' + + [ b c a ] Promise restored + L R + +Step 5: [ b c a b ] Explorer sees 'b' — duplicate! + L R + + Gatekeeper moves: + + [ c a b ] Promise restored + L R +``` + +Notice: The Explorer always advances. The Gatekeeper only moves when the promise breaks. Together, they visit each element at most twice. + +--- + +## The Moment of Recognition + +You're reading a problem. You see phrases like: +- *"contiguous subarray"* +- *"substring"* +- *"longest/shortest"* +- *"at most K"* +- *"containing all of"* + +And you feel it: *This is about maintaining something over a moving portion.* + +That's your cue. The Explorer and Gatekeeper are ready. The window wants to slide. + +--- + +## From Intuition to Implementation + +Only now — after the dance is clear — does code become useful. + +The template is always the same skeleton: + +```python +def sliding_window(sequence): + state = initial_state() + left = 0 + answer = initial_answer() + + for right, element in enumerate(sequence): + # Explorer: include new element + update_state_add(state, element) + + # Gatekeeper: enforce the promise + while promise_is_broken(state): + update_state_remove(state, sequence[left]) + left += 1 + + # Record: this window is valid + answer = consider(answer, left, right) + + return answer +``` + +The variations come from: +1. **What is the promise?** (determines the while condition) +2. **What state do we track?** (determines the data structure) +3. **What are we optimizing?** (determines how we update the answer) + +--- + +## Quick Reference: Problem → Pattern Mapping + +| When You See... | Think... | Window Type | +|----------------|----------|-------------| +| "Longest substring with unique chars" | Uniqueness promise | Maximize | +| "Longest with at most K distinct" | Count limit promise | Maximize | +| "Minimum window containing all of T" | Coverage promise | Minimize | +| "Subarray sum ≥ target" | Threshold promise | Minimize | +| "Contains permutation" | Exact match promise | Fixed | +| "Find all anagrams" | Exact match, collect all | Fixed | + +--- + +## The Pattern in One Sentence + +> *Sliding Window is the art of maintaining a valid contiguous view by advancing eagerly and retreating only when necessary.* + +When you see a problem about optimizing over contiguous sequences with incrementally checkable properties — you've found your window. + +Let it slide. + +--- + +*For detailed implementations and code examples, see [templates.md](./templates.md).* + From c14a6ab3c090a2d633d1907fc342a06e91f2d871 Mon Sep 17 00:00:00 2001 From: lufftw Date: Sun, 14 Dec 2025 14:44:38 +0800 Subject: [PATCH 06/11] docs: enhance sliding window intuition guide with visual diagrams Improve intuition.md with practical visualizations and examples: - Add ASCII flowcharts for Maximize and Minimize window modes with color-coded R (Explorer) and L (Gatekeeper) movements - Convert "Visualizing the Dance" to structured table format with Step, R, State, L move, Window, and Result columns - Add concept-to-code comments linking Explorer/Gatekeeper roles to for-loop and while-loop in template - Add Fixed Window (K=3) example trace after Shape 3 demonstrating constant-size sliding behavior --- docs/patterns/sliding_window/intuition.md | 274 +++++++++++++++++++--- 1 file changed, 239 insertions(+), 35 deletions(-) diff --git a/docs/patterns/sliding_window/intuition.md b/docs/patterns/sliding_window/intuition.md index 92ed659..9128468 100644 --- a/docs/patterns/sliding_window/intuition.md +++ b/docs/patterns/sliding_window/intuition.md @@ -24,13 +24,13 @@ The key insight: *You don't need to remember everything — only what's currentl Every sliding window algorithm is a dance between two opposing forces: -### The Explorer (Right Boundary) +### The Explorer (Right Boundary) $R$ - Always moves forward, never backward - Discovers new territory - Adds new elements to consideration - Asks: *"What happens if I include this?"* -### The Gatekeeper (Left Boundary) +### The Gatekeeper (Left Boundary) $L$ - Follows behind, cleaning up - Removes elements that no longer serve the goal - Enforces the rules of what's allowed @@ -86,6 +86,94 @@ The window EXPANDS freely, CONTRACTS only when forced. **Mental image**: Stretching a rubber band until it's about to snap, then easing off just enough. +#### Flowchart: Maximize Window + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Example: Longest Substring Without Repeating Characters │ +│ Sequence: [ a b c a b ] Promise: "All chars unique" │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────┐ │ +│ │START│ │ +│ └──┬──┘ │ +│ ▼ │ +│ ╔══════════════════════════════╗ │ +│ ║ 🟢 R advances (Explorer) ║ ◀─────────────────────────┐ │ +│ ║ Add element to state ║ │ │ +│ ╚═══════════════╤══════════════╝ │ │ +│ ▼ │ │ +│ ┌─────────────────────┐ │ │ +│ │ Promise broken? │ │ │ +│ │ (duplicate found?) │ │ │ +│ └────────┬────────────┘ │ │ +│ Yes │ No │ │ +│ ┌──────────┴──────────┐ │ │ +│ ▼ ▼ │ │ +│ ╔═══════════════════╗ ┌─────────────────────┐ │ │ +│ ║ 🔴 L advances ║ │ ✅ Update answer: │ │ │ +│ ║ (Gatekeeper) ║ │ max(ans, R-L+1) │ │ │ +│ ║ Remove from state ║ └──────────┬──────────┘ │ │ +│ ╚═════════╤═════════╝ │ │ │ +│ │ │ │ │ +│ ▼ │ │ │ +│ ┌────────────────┐ │ │ │ +│ │Promise restored?│ │ │ │ +│ └───────┬────────┘ │ │ │ +│ No │ Yes │ │ │ +│ ┌───────┴───────┐ │ │ │ +│ │ ▼ ▼ │ │ +│ │ ┌──────────────────────────────┐ │ │ +│ │ │ More elements? │ │ │ +│ │ └─────────────┬────────────────┘ │ │ +│ │ Yes │ No │ │ +│ │ ┌───────────┴───────────┐ │ │ +│ ▼ │ ▼ │ │ +│ 🔴 L++ │ ┌─────────┐ │ │ +│ (repeat) └──────────────────┤ DONE │ │ │ +│ └─────────┘ │ │ +│ │ │ +└──────────────────────────────────────────────────────────────┴──────────────┘ + +Visual Trace: +═══════════════════════════════════════════════════════════════════════════════ + + Sequence: a b c a b + [0] [1] [2] [3] [4] + + Step 1: 🟢R→ + [ a ] max = 1 + L,R + + Step 2: 🟢R→ + [ a b ] max = 2 + L R + + Step 3: 🟢R→ + [ a b c ] max = 3 + L R + + Step 4: 🟢R→ + [ a b c a ] ❌ 'a' duplicate! + L R + │ + 🔴L→ 🔴L→ ▼ + [ b c a ] max = 3 (restored) + L R + + Step 5: 🟢R→ + [ b c a b ] ❌ 'b' duplicate! + L R + │ + 🔴L→ 🔴L→ ▼ + [ c a b ] max = 3 (final) + L R + +Legend: 🟢 = R expands (green) 🔴 = L contracts (red) ❌ = promise broken +``` + +--- + ### Mode 2: Minimize the Window *"How small can my view become while still being valid?"* @@ -101,6 +189,105 @@ The window EXPANDS until valid, then CONTRACTS aggressively. **Mental image**: Tightening a noose around the minimal solution. +#### Flowchart: Minimize Window + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Example: Minimum Size Subarray Sum ≥ 7 │ +│ Sequence: [ 2 3 1 2 4 3 ] Promise: "Sum ≥ target" │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────┐ │ +│ │START│ │ +│ └──┬──┘ │ +│ ▼ │ +│ ╔══════════════════════════════╗ │ +│ ║ 🟢 R advances (Explorer) ║ ◀─────────────────────────┐ │ +│ ║ Add to sum ║ │ │ +│ ╚═══════════════╤══════════════╝ │ │ +│ ▼ │ │ +│ ┌─────────────────────┐ │ │ +│ │ Promise satisfied? │ │ │ +│ │ (sum ≥ target?) │ │ │ +│ └────────┬────────────┘ │ │ +│ No │ Yes │ │ +│ ┌──────────┴──────────────────────┐ │ │ +│ │ ▼ │ │ +│ │ ┌────────────────────────────────────────────────┐ │ │ +│ │ │ WHILE promise still holds: │ │ │ +│ │ │ ┌──────────────────────────────────────────┐ │ │ │ +│ │ │ │ ✅ Update answer: min(ans, R-L+1) │ │ │ │ +│ │ │ │ 🔴 L advances (Gatekeeper) │ │ │ │ +│ │ │ │ Subtract from sum │ │ │ │ +│ │ │ └──────────────────────────────────────────┘ │ │ │ +│ │ └────────────────────────────────────────────────┘ │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ ┌──────────────────────────────┐ │ │ +│ └────────►│ More elements? │ │ │ +│ └─────────────┬────────────────┘ │ │ +│ Yes │ No │ │ +│ ┌─────────┴─────────┐ │ │ +│ │ ▼ │ │ +│ │ ┌─────────┐ │ │ +│ └──────────────┤ DONE │ │ │ +│ └─────────┘ │ │ +│ │ │ +└──────────────────────────────────────────────────────────────┴──────────────┘ + +Visual Trace: +═══════════════════════════════════════════════════════════════════════════════ + + Sequence: 2 3 1 2 4 3 target = 7 + [0] [1] [2] [3] [4] [5] + + Step 1: 🟢R→ + [ 2 ] sum=2 < 7 min = ∞ + L,R (keep expanding) + + Step 2: 🟢R→ + [ 2 3 ] sum=5 < 7 min = ∞ + L R (keep expanding) + + Step 3: 🟢R→ + [ 2 3 1 ] sum=6 < 7 min = ∞ + L R (keep expanding) + + Step 4: 🟢R→ + [ 2 3 1 2 ] sum=8 ≥ 7 ✅ VALID! + L R + │ + 🔴L→ ▼ Record: min = 4 + [ 3 1 2 ] sum=6 < 7 (stop contracting) + L R + + Step 5: 🟢R→ + [ 3 1 2 4 ] sum=10 ≥ 7 ✅ + L R + │ + 🔴L→ ▼ Record: min = 4 + [ 1 2 4 ] sum=7 ≥ 7 ✅ + L R + │ + 🔴L→ ▼ Record: min = 3 + [ 2 4 ] sum=6 < 7 (stop) + L R + + Step 6: 🟢R→ + [ 2 4 3 ] sum=9 ≥ 7 ✅ + L R + │ + 🔴L→ ▼ Record: min = 3 + [ 4 3 ] sum=7 ≥ 7 ✅ + L R + │ + 🔴L→ ▼ Record: min = 2 ✨ FINAL + [ 3 ] sum=3 < 7 (stop) + L,R + +Legend: 🟢 = R expands 🔴 = L contracts ✅ = promise satisfied +``` + --- ## Pattern Recognition: "Is This a Sliding Window Problem?" @@ -156,6 +343,34 @@ Ask yourself these questions: **Classic problems**: Find all anagrams, check permutation inclusion +#### Fixed Window Example Trace (K=3) + +``` +Problem: Find maximum sum of any subarray of size K=3 +Sequence: [ 1 4 2 10 2 3 1 0 20 ] + +┌──────┬───────┬─────────┬──────────────────┬───────────────┬────────────────┐ +│ Step │ R→ │ sum │ L action │ Window [L,R] │ Max Sum │ +├──────┼───────┼─────────┼──────────────────┼───────────────┼────────────────┤ +│ 0 │ 1 │ 1 │ — │ [1] │ (building...) │ +│ 1 │ 4 │ 5 │ — │ [1,4] │ (building...) │ +│ 2 │ 2 │ 7 │ — │ [1,4,2] │ 7 ✨ │ +│ 3 │ 10 │ 7+10=17 │ 🔴 remove 1 → 16 │ [4,2,10] │ 16 │ +│ 4 │ 2 │ 16+2=18 │ 🔴 remove 4 → 14 │ [2,10,2] │ 16 │ +│ 5 │ 3 │ 14+3=17 │ 🔴 remove 2 → 15 │ [10,2,3] │ 16 │ +│ 6 │ 1 │ 15+1=16 │ 🔴 remove 10→ 6 │ [2,3,1] │ 16 │ +│ 7 │ 0 │ 6+0=6 │ 🔴 remove 2 → 4 │ [3,1,0] │ 16 │ +│ 8 │ 20 │ 4+20=24 │ 🔴 remove 3 → 21 │ [1,0,20] │ 21 ✨ │ +└──────┴───────┴─────────┴──────────────────┴───────────────┴────────────────┘ + +Key insight: Once R reaches index 2 (K-1), every subsequent step: + 1. 🟢 R advances → add new element + 2. 🔴 L advances → remove oldest element (exactly K steps behind) + 3. Window size stays constant at K=3 + +Answer: Maximum sum = 21 (subarray [1, 0, 20]) +``` + --- ## The State: What the Window Remembers @@ -176,38 +391,27 @@ The magic of sliding window is that these states are **incrementally maintainabl ## Visualizing the Dance -``` -Sequence: [ a b c a b c b b ] - ↑ - Both start here - -Step 1: [ a ] Explorer sees 'a' - L R - -Step 2: [ a b ] Explorer sees 'b' - L R - -Step 3: [ a b c ] Explorer sees 'c' - L R - -Step 4: [ a b c a ] Explorer sees 'a' — duplicate! - L R - - Gatekeeper must act: Move L past the first 'a' - - [ b c a ] Promise restored - L R - -Step 5: [ b c a b ] Explorer sees 'b' — duplicate! - L R - - Gatekeeper moves: - - [ c a b ] Promise restored - L R -``` +**Problem**: Longest substring without repeating characters +**Input**: `"abcabcbb"` — Find the longest window where all characters are unique. + +| Step | $R$ (char) | State: `last_seen` | $L$ move? | Window `[L, R]` | Max Length | +|:----:|:----------:|:-------------------|:---------:|:---------------:|:----------:| +| 0 | `a` | `{a:0}` | — | `[0,0]` = "a" | 1 | +| 1 | `b` | `{a:0, b:1}` | — | `[0,1]` = "ab" | 2 | +| 2 | `c` | `{a:0, b:1, c:2}` | — | `[0,2]` = "abc" | 3 | +| 3 | `a` | `{a:3, b:1, c:2}` | 🔴 `L→1` (skip past old 'a') | `[1,3]` = "bca" | 3 | +| 4 | `b` | `{a:3, b:4, c:2}` | 🔴 `L→2` (skip past old 'b') | `[2,4]` = "cab" | 3 | +| 5 | `c` | `{a:3, b:4, c:5}` | 🔴 `L→3` (skip past old 'c') | `[3,5]` = "abc" | 3 | +| 6 | `b` | `{a:3, b:6, c:5}` | 🔴 `L→5` (skip past old 'b') | `[5,6]` = "cb" | 3 | +| 7 | `b` | `{a:3, b:7, c:5}` | 🔴 `L→7` (skip past old 'b') | `[7,7]` = "b" | 3 | -Notice: The Explorer always advances. The Gatekeeper only moves when the promise breaks. Together, they visit each element at most twice. +**Answer**: 3 (substring `"abc"`) + +**Key observations**: +- $R$ (Explorer) advances every single step — never skips, never retreats +- $L$ (Gatekeeper) only moves when a duplicate is found in the current window +- The jump optimization: $L$ jumps directly to `last_seen[char] + 1` instead of incrementing one by one +- Window length = `R - L + 1` --- @@ -238,11 +442,12 @@ def sliding_window(sequence): left = 0 answer = initial_answer() + ## 1. Explorer (R) always advances for right, element in enumerate(sequence): # Explorer: include new element update_state_add(state, element) - # Gatekeeper: enforce the promise + ## 2. Gatekeeper (L) acts to restore invariant while promise_is_broken(state): update_state_remove(state, sequence[left]) left += 1 @@ -284,4 +489,3 @@ Let it slide. --- *For detailed implementations and code examples, see [templates.md](./templates.md).* - From 00135e96c91370b8d871a6851d13397dd4ec34e3 Mon Sep 17 00:00:00 2001 From: lufftw Date: Sun, 14 Dec 2025 15:27:39 +0800 Subject: [PATCH 07/11] docs(sliding_window): add complete LeetCode implementations to intuition.md - Add 5 runnable solutions: LC 3, 340, 76, 438, 209 - Include detailed time/space complexity analysis per problem - Add verification code with test cases for each solution - Add definitive O(n) complexity analysis section - Use semantic, project-level variable naming throughout --- docs/patterns/sliding_window/intuition.md | 580 +++++++++++++++++++++- 1 file changed, 579 insertions(+), 1 deletion(-) diff --git a/docs/patterns/sliding_window/intuition.md b/docs/patterns/sliding_window/intuition.md index 9128468..36c8bff 100644 --- a/docs/patterns/sliding_window/intuition.md +++ b/docs/patterns/sliding_window/intuition.md @@ -478,6 +478,584 @@ The variations come from: --- +## Complete Implementations: From Intuition to Code + +Now we translate intuition into production-ready code. Each solution demonstrates the Explorer-Gatekeeper dance with explicit state management and complexity guarantees. + +--- + +### Problem 1: Longest Substring Without Repeating Characters (LeetCode 3) + +**The Promise**: *"Every character in my view appears exactly once."* + +**Why This Problem Matters**: This is the canonical sliding window problem. Master it, and you understand the pattern. + +```python +def length_of_longest_substring(s: str) -> int: + """ + Find the length of the longest substring without repeating characters. + + Intuition: + We maintain a window [left, right] where all characters are unique. + The Explorer (right pointer) advances one character at a time. + When a duplicate is detected, the Gatekeeper (left pointer) jumps + directly past the previous occurrence — no incremental crawling needed. + + The Jump Optimization: + Instead of shrinking one position at a time (while loop), we record + each character's last-seen index. When we see a duplicate, we can + teleport the left boundary to skip all characters up to and including + the previous occurrence. This eliminates redundant comparisons. + + Time Complexity: O(n) + - The right pointer visits each character exactly once: O(n) + - The left pointer only moves forward (never backward): amortized O(n) + - Dictionary operations (get, set): O(1) per operation + - Total: O(n) where n = len(s) + + Space Complexity: O(min(n, σ)) + - σ = size of character set (26 for lowercase, 128 for ASCII, etc.) + - In practice, O(1) for fixed alphabets, O(n) for arbitrary Unicode + + Args: + s: Input string to search + + Returns: + Length of the longest substring with all unique characters + + Examples: + >>> length_of_longest_substring("abcabcbb") + 3 # "abc" + >>> length_of_longest_substring("bbbbb") + 1 # "b" + >>> length_of_longest_substring("pwwkew") + 3 # "wke" + """ + # State: Maps each character to its most recent index + # This enables O(1) duplicate detection and O(1) jump calculation + last_seen_at: dict[str, int] = {} + + # Window boundaries: [left, right] inclusive + left = 0 + max_length = 0 + + # Explorer advances through every position + for right, char in enumerate(s): + # Duplicate detection: Is this char already in our current window? + # Key insight: We only care if the previous occurrence is at or after 'left' + # Characters before 'left' are outside our window — they don't count + if char in last_seen_at and last_seen_at[char] >= left: + # Gatekeeper acts: Jump past the previous occurrence + # The +1 ensures we exclude the duplicate itself + left = last_seen_at[char] + 1 + + # Record this character's position for future duplicate detection + last_seen_at[char] = right + + # The window [left, right] is now guaranteed unique + # Update our answer if this window is the largest seen + current_length = right - left + 1 + max_length = max(max_length, current_length) + + return max_length + + +# Verification +if __name__ == "__main__": + test_cases = [ + ("abcabcbb", 3), + ("bbbbb", 1), + ("pwwkew", 3), + ("", 0), + ("au", 2), + ("dvdf", 3), + ] + for s, expected in test_cases: + result = length_of_longest_substring(s) + status = "✓" if result == expected else "✗" + print(f"{status} Input: {s!r:15} → {result} (expected {expected})") +``` + +**Complexity Deep Dive**: + +| Operation | Count | Cost | Total | +|-----------|-------|------|-------| +| Right pointer advances | n | O(1) | O(n) | +| Left pointer advances | ≤ n (total) | O(1) | O(n) | +| Dictionary lookup/update | n | O(1) average | O(n) | +| **Total** | | | **O(n)** | + +The left pointer never retreats. Each character index is visited by `left` at most once across the entire algorithm, giving us the O(n) guarantee. + +--- + +### Problem 2: Longest Substring with At Most K Distinct Characters (LeetCode 340) + +**The Promise**: *"I contain no more than K different characters."* + +**The Difference from Problem 1**: We can't jump — we must shrink incrementally because removing one character might still leave us with too many distinct characters. + +```python +def length_of_longest_substring_k_distinct(s: str, k: int) -> int: + """ + Find the length of the longest substring with at most K distinct characters. + + Intuition: + The window [left, right] maintains at most K distinct characters. + When adding a character causes distinct count to exceed K, we shrink + from the left until we're back to K or fewer distinct characters. + + Why We Can't Jump: + Unlike the unique-character problem, removing one character doesn't + guarantee we restore validity. We might need to remove several + characters before the distinct count drops. Hence, we use a while-loop. + + State Design: + We use a frequency map rather than a last-seen-index map because: + 1. We need to know when a character's count drops to zero (to decrement distinct count) + 2. The len(frequency_map) tells us the distinct count directly + + Time Complexity: O(n) + - Right pointer: n iterations, O(1) per iteration + - Left pointer: moves at most n times total (amortized) + - Each character enters and exits the window at most once + + Space Complexity: O(K) + - The frequency map contains at most K+1 entries at any time + - Before we shrink, we might briefly have K+1 entries + + Args: + s: Input string + k: Maximum number of distinct characters allowed + + Returns: + Length of the longest valid substring + + Examples: + >>> length_of_longest_substring_k_distinct("eceba", 2) + 3 # "ece" + >>> length_of_longest_substring_k_distinct("aa", 1) + 2 # "aa" + """ + if k == 0: + return 0 + + # State: Frequency count of each character in current window + # The length of this dict = number of distinct characters + char_count: dict[str, int] = {} + + left = 0 + max_length = 0 + + for right, char in enumerate(s): + # Explorer adds new character to window + char_count[char] = char_count.get(char, 0) + 1 + + # Gatekeeper shrinks window while we have too many distinct characters + # This is a while-loop, not an if — we may need multiple shrinks + while len(char_count) > k: + left_char = s[left] + char_count[left_char] -= 1 + + # Critical: Remove from dict when count reaches zero + # This keeps len(char_count) accurate for distinct count + if char_count[left_char] == 0: + del char_count[left_char] + + left += 1 + + # Window is now valid: at most K distinct characters + max_length = max(max_length, right - left + 1) + + return max_length + + +# Verification +if __name__ == "__main__": + test_cases = [ + (("eceba", 2), 3), + (("aa", 1), 2), + (("a", 0), 0), + (("aabbcc", 2), 4), + ] + for (s, k), expected in test_cases: + result = length_of_longest_substring_k_distinct(s, k) + status = "✓" if result == expected else "✗" + print(f"{status} Input: {s!r}, k={k} → {result} (expected {expected})") +``` + +**Engineering Note**: The deletion `del char_count[left_char]` is essential. Without it, `len(char_count)` would count characters with zero frequency, breaking our invariant check. + +--- + +### Problem 3: Minimum Window Substring (LeetCode 76) + +**The Promise**: *"I contain all required characters with sufficient frequency."* + +**The Paradigm Shift**: Now we're *minimizing*, not maximizing. We expand until valid, then shrink aggressively while staying valid. + +```python +def min_window(s: str, t: str) -> str: + """ + Find the minimum window in s that contains all characters of t. + + Intuition: + Phase 1 (Expand): Explorer advances until window contains all of t. + Phase 2 (Contract): Gatekeeper shrinks window while it remains valid. + Record the smallest valid window, then continue exploring. + + The Satisfied Counter Optimization: + Naively checking "do we have all characters?" requires O(|t|) per step. + Instead, we track how many unique characters have met their quota. + When `chars_satisfied == chars_required`, the window is valid. + This reduces per-step cost from O(|t|) to O(1). + + Time Complexity: O(|s| + |t|) + - Building need_count: O(|t|) + - Main loop: O(|s|) — each character enters and exits once + - All dictionary operations: O(1) each + + Space Complexity: O(|t|) + - need_count: O(unique chars in t) + - have_count: O(unique chars in t) — we only track needed chars + + Args: + s: Source string to search in + t: Target string containing required characters + + Returns: + Minimum window substring, or "" if no valid window exists + + Examples: + >>> min_window("ADOBECODEBANC", "ABC") + "BANC" + >>> min_window("a", "a") + "a" + >>> min_window("a", "aa") + "" + """ + if not t or not s: + return "" + + # Build the requirement: what characters do we need, and how many of each? + need_count: dict[str, int] = {} + for char in t: + need_count[char] = need_count.get(char, 0) + 1 + + # State: what characters do we have in current window? + have_count: dict[str, int] = {} + + # Optimization: Track satisfaction at character level + # chars_satisfied = count of unique characters meeting their quota + chars_satisfied = 0 + chars_required = len(need_count) # number of unique characters in t + + # Answer tracking + min_length = float('inf') + result_start = 0 + + left = 0 + + for right, char in enumerate(s): + # Explorer: Add character to window + have_count[char] = have_count.get(char, 0) + 1 + + # Did adding this character satisfy a requirement? + # We check for exact equality to avoid double-counting + if char in need_count and have_count[char] == need_count[char]: + chars_satisfied += 1 + + # Gatekeeper: Try to shrink while window remains valid + while chars_satisfied == chars_required: + # Current window is valid — record if it's the smallest + window_length = right - left + 1 + if window_length < min_length: + min_length = window_length + result_start = left + + # Remove leftmost character + left_char = s[left] + have_count[left_char] -= 1 + + # Did removing break a requirement? + if left_char in need_count and have_count[left_char] < need_count[left_char]: + chars_satisfied -= 1 + + left += 1 + + if min_length == float('inf'): + return "" + return s[result_start : result_start + min_length] + + +# Verification +if __name__ == "__main__": + test_cases = [ + (("ADOBECODEBANC", "ABC"), "BANC"), + (("a", "a"), "a"), + (("a", "aa"), ""), + (("aa", "aa"), "aa"), + ] + for (s, t), expected in test_cases: + result = min_window(s, t) + status = "✓" if result == expected else "✗" + print(f"{status} s={s!r}, t={t!r} → {result!r} (expected {expected!r})") +``` + +**Complexity Breakdown**: + +| Phase | Operations | Complexity | +|-------|------------|------------| +| Build `need_count` | Iterate over t | O(\|t\|) | +| Expand (right pointer) | Each char enters once | O(\|s\|) | +| Contract (left pointer) | Each char exits at most once | O(\|s\|) | +| **Total** | | **O(\|s\| + \|t\|)** | + +--- + +### Problem 4: Find All Anagrams in a String (LeetCode 438) + +**The Promise**: *"I contain exactly the same character frequencies as the pattern."* + +**Fixed Window Property**: Since anagrams have the same length, we maintain a window of exactly `len(p)`. + +```python +def find_anagrams(s: str, p: str) -> list[int]: + """ + Find all starting indices of p's anagrams in s. + + Intuition: + An anagram has the exact same character frequencies as the pattern. + Since length must match, we use a fixed-size window of len(p). + At each position, check if window frequencies match pattern frequencies. + + The Match Counter Optimization: + Instead of comparing two frequency maps (O(26) for lowercase), + we track how many characters have matching frequencies. + When all match, we've found an anagram. + + Careful State Transitions: + When adding a character: + - If it now matches the pattern frequency: matches++ + - If it just exceeded the pattern frequency: matches-- + When removing a character: + - If it was matching and now isn't: matches-- + - If it was exceeding and now matches: matches++ + + Time Complexity: O(|s| + |p|) + - Build pattern frequency: O(|p|) + - Slide window over s: O(|s|) with O(1) per step + + Space Complexity: O(1) + - Two frequency maps bounded by alphabet size (26 for lowercase) + + Args: + s: Source string to search in + p: Pattern string (looking for its anagrams) + + Returns: + List of starting indices where anagrams of p begin + + Examples: + >>> find_anagrams("cbaebabacd", "abc") + [0, 6] + >>> find_anagrams("abab", "ab") + [0, 1, 2] + """ + result: list[int] = [] + + pattern_len = len(p) + source_len = len(s) + + if pattern_len > source_len: + return result + + # Build pattern frequency map + pattern_freq: dict[str, int] = {} + for char in p: + pattern_freq[char] = pattern_freq.get(char, 0) + 1 + + # Window frequency map + window_freq: dict[str, int] = {} + + # Track how many characters have matching frequencies + chars_matched = 0 + chars_to_match = len(pattern_freq) + + for right in range(source_len): + # Add character at right edge + right_char = s[right] + window_freq[right_char] = window_freq.get(right_char, 0) + 1 + + # Update match count for added character + if right_char in pattern_freq: + if window_freq[right_char] == pattern_freq[right_char]: + chars_matched += 1 + elif window_freq[right_char] == pattern_freq[right_char] + 1: + # We just went from matching to exceeding + chars_matched -= 1 + + # Remove character at left edge when window exceeds pattern length + left = right - pattern_len + if left >= 0: + left_char = s[left] + + # Update match count for removed character BEFORE decrementing + if left_char in pattern_freq: + if window_freq[left_char] == pattern_freq[left_char]: + # We're about to break this match + chars_matched -= 1 + elif window_freq[left_char] == pattern_freq[left_char] + 1: + # Removing brings us from exceeding to matching + chars_matched += 1 + + window_freq[left_char] -= 1 + if window_freq[left_char] == 0: + del window_freq[left_char] + + # Check for anagram when window size equals pattern size + if right >= pattern_len - 1 and chars_matched == chars_to_match: + result.append(right - pattern_len + 1) + + return result + + +# Verification +if __name__ == "__main__": + test_cases = [ + (("cbaebabacd", "abc"), [0, 6]), + (("abab", "ab"), [0, 1, 2]), + (("aaaaaaaaaa", "aaaaaaaaaaaaa"), []), + ] + for (s, p), expected in test_cases: + result = find_anagrams(s, p) + status = "✓" if result == expected else "✗" + print(f"{status} s={s!r}, p={p!r} → {result} (expected {expected})") +``` + +--- + +### Problem 5: Minimum Size Subarray Sum (LeetCode 209) + +**The Promise**: *"My sum is at least the target."* + +**Numeric Sliding Window**: Instead of tracking character frequencies, we track a running sum. The principle remains identical. + +```python +def min_subarray_len(target: int, nums: list[int]) -> int: + """ + Find the minimal length of a subarray whose sum is >= target. + + Intuition: + This is the numeric equivalent of minimum window substring. + Expand until sum >= target, then shrink while sum stays >= target. + Track the smallest window that ever achieves the target. + + Why Positive Numbers Matter: + This algorithm works because all elements are positive. + Adding an element always increases the sum. + Removing an element always decreases the sum. + This monotonicity is what makes sliding window viable. + + Caution for Interviews: + If the array can contain negatives, sliding window doesn't work! + You'd need a different approach (prefix sums + monotonic deque). + + Time Complexity: O(n) + - Right pointer: visits each element once + - Left pointer: moves forward only, at most n times total + - Each element enters and exits the window at most once + + Space Complexity: O(1) + - Only a few integer variables (sum, pointers, min_length) + + Args: + target: The minimum sum we need to achieve + nums: Array of positive integers + + Returns: + Minimum length of valid subarray, or 0 if impossible + + Examples: + >>> min_subarray_len(7, [2, 3, 1, 2, 4, 3]) + 2 # [4, 3] + >>> min_subarray_len(11, [1, 1, 1, 1, 1, 1, 1, 1]) + 0 # Impossible + """ + n = len(nums) + if n == 0: + return 0 + + # State: Running sum of current window [left, right] + window_sum = 0 + + left = 0 + min_length = float('inf') + + for right, num in enumerate(nums): + # Explorer: Expand window by including nums[right] + window_sum += num + + # Gatekeeper: Shrink while sum meets target + # We want the SMALLEST valid window, so shrink aggressively + while window_sum >= target: + # Current window is valid — record its size + current_length = right - left + 1 + min_length = min(min_length, current_length) + + # Remove leftmost element + window_sum -= nums[left] + left += 1 + + return min_length if min_length != float('inf') else 0 + + +# Verification +if __name__ == "__main__": + test_cases = [ + ((7, [2, 3, 1, 2, 4, 3]), 2), + ((4, [1, 4, 4]), 1), + ((11, [1, 1, 1, 1, 1, 1, 1, 1]), 0), + ((15, [1, 2, 3, 4, 5]), 5), + ] + for (target, nums), expected in test_cases: + result = min_subarray_len(target, nums) + status = "✓" if result == expected else "✗" + print(f"{status} target={target}, nums={nums} → {result} (expected {expected})") +``` + +**The O(n) Guarantee Visualized**: + +``` +Element: [2] [3] [1] [2] [4] [3] + ↓ ↓ ↓ ↓ ↓ ↓ +Right visits: ✓ ✓ ✓ ✓ ✓ ✓ = 6 times +Left visits: ✓ ✓ ✓ ✓ ✓ ✓ = 6 times (at most) + ───────── +Total operations: ≤ 2n = O(n) +``` + +--- + +## Time Complexity: The Definitive Analysis + +Every sliding window algorithm achieves O(n) through the **two-pointer invariant**: + +``` +Both pointers only move forward → Each element enters the window once, exits once +``` + +| Algorithm | Per-Element Cost | Total Visits | Complexity | +|-----------|------------------|--------------|------------| +| Right pointer advance | O(1) | n | O(n) | +| Left pointer advance | O(1) | ≤ n | O(n) | +| State update (add/remove) | O(1) | 2n | O(n) | +| **Combined** | | | **O(n)** | + +**The Amortized Argument**: While the inner `while` loop might run multiple times for a single `right` advance, the total number of `left` advances across the *entire* algorithm is bounded by n. This is because `left` starts at 0 and can only increase, never decrease, and can never exceed n. + +--- + ## The Pattern in One Sentence > *Sliding Window is the art of maintaining a valid contiguous view by advancing eagerly and retreating only when necessary.* @@ -488,4 +1066,4 @@ Let it slide. --- -*For detailed implementations and code examples, see [templates.md](./templates.md).* +*For additional variations and template reference, see [templates.md](./templates.md).* From 6904dd57d99a1ed3f145ff98f2e6ea4a67d1e34b Mon Sep 17 00:00:00 2001 From: lufftw Date: Sun, 14 Dec 2025 16:11:00 +0800 Subject: [PATCH 08/11] docs(two_pointers): add intuition.md for pattern recognition Create comprehensive pattern intuition document with: - Six two-pointer shapes with vivid mental models - Invariant and irreversibility principles explained - Visual traces for opposite, same-direction, fast-slow, and Dutch flag - Pattern recognition flowchart for instant identification - Templates derived from intuition, not as starting points --- docs/patterns/two_pointers/intuition.md | 738 ++++++++++++++++++++++++ tools/README.md | 1 + 2 files changed, 739 insertions(+) create mode 100644 docs/patterns/two_pointers/intuition.md diff --git a/docs/patterns/two_pointers/intuition.md b/docs/patterns/two_pointers/intuition.md new file mode 100644 index 0000000..592cf08 --- /dev/null +++ b/docs/patterns/two_pointers/intuition.md @@ -0,0 +1,738 @@ +# Two Pointers: Pattern Intuition Guide + +> *"Two points of attention, moving in coordinated rhythm — each step permanently narrows the world of possibilities."* + +--- + +## The Situation That Calls for Two Pointers + +Imagine you're standing at the edge of a long corridor with doors on both sides. You know the answer lies somewhere in this corridor, but checking every possible pair of doors would take forever. + +Then you realize: you don't need to check everything. You can place one hand on the leftmost door and one on the rightmost door. Based on what you find, you know which hand to move. With each movement, doors behind you become irrelevant — forever excluded from consideration. + +**This is the essence of Two Pointers.** + +You encounter this pattern whenever: +- You're working with a **sorted** or **ordered** sequence +- You need to find **pairs, tuples, or regions** with certain properties +- The relationship between elements is **monotonic** — changing one pointer predictably affects the outcome +- You can **eliminate possibilities** based on the current state + +The key insight: *You're not searching — you're eliminating. Every pointer movement permanently shrinks the problem.* + +--- + +## The Invariant: The Space Between + +Every two pointers algorithm maintains a **sacred region** — the space where the answer must exist. + +``` +┌───────────────────────────────────────────────────────────────┐ +│ │ +│ [excluded] ← left ═══════ answer space ═══════ right → [excluded] │ +│ │ +│ Once excluded, never reconsidered. │ +│ The region between pointers is the ONLY remaining hope. │ +│ │ +└───────────────────────────────────────────────────────────────┘ +``` + +The invariant says: *If a valid answer exists, it lies within the current boundaries.* Moving a pointer is a declaration: "I've proven that nothing behind this pointer can be part of the answer." + +**This is what makes two pointers work**: each movement is a proof of exclusion. You're not guessing — you're eliminating with certainty. + +--- + +## The Irreversible Decision + +Here's the crucial insight that separates two pointers from brute force: + +> **Once a pointer moves, it never moves back.** + +When you advance `left` from position 3 to position 4, you've permanently decided: "No valid answer involves position 3 as the left element." This decision is irreversible. + +This one-directional march is what transforms O(n²) into O(n). Instead of checking all n² pairs, each of the 2n pointer positions is visited at most once. + +The irreversibility creates efficiency: *you burn bridges as you cross them.* + +--- + +## The Six Shapes of Two Pointers + +Two pointer problems come in six distinct flavors. Recognizing the shape tells you exactly how to position and move the pointers. + +--- + +### Shape 1: Opposite Approach — "Closing the Gap" + +**The situation**: Two sentinels stand at opposite ends of a corridor. They walk toward each other, meeting somewhere in the middle. + +**What it feels like**: You're squeezing from both ends. The search space shrinks from the outside in. + +**The mental model**: +``` +Initial: L ═══════════════════════════════════ R + ↓ ↓ +Step 1: L ═════════════════════════════ R + ↓ +Step 2: L ═════════════════════════ R + ↓ +Step 3: L ═══════════════════ R + ... +Final: L R (or L crosses R) +``` + +**The decision rule**: Based on the current pair's property: +- If the combined value is **too small** → move `left` right (seek larger) +- If the combined value is **too large** → move `right` left (seek smaller) +- If it matches → record and continue (or return immediately) + +**Why it works**: Sorted order creates monotonicity. Moving `left` right can only *increase* its contribution. Moving `right` left can only *decrease* its contribution. This gives you precise control. + +**Classic problems**: Two Sum II, Container With Most Water, 3Sum + +--- + +### Shape 2: Same Direction — "The Writer Following the Reader" + +**The situation**: Two people walk the same corridor. One is a **Reader** who examines every door. The other is a **Writer** who only records the doors worth keeping. + +**What it feels like**: You're filtering in-place. The Reader advances relentlessly; the Writer only moves when something passes the test. + +**The mental model**: +``` +Initial: [a] [b] [c] [d] [e] [f] + W R + ↓ + Reader examines 'a' + 'a' passes → Writer takes it, both advance + +Step 2: [a] [b] [c] [d] [e] [f] + W R + ↓ + Reader examines 'b' + 'b' fails → only Reader advances + +Step 3: [a] [b] [c] [d] [e] [f] + W R + ↓ + Reader examines 'c' + 'c' passes → Writer takes it, both advance + +Final: [a] [c] [x] [x] [x] [x] + ↑ + New logical end (write position) +``` + +**The decision rule**: +- Reader always advances +- Writer only advances when the current element should be kept +- Elements are copied from Reader position to Writer position + +**Why it works**: The Writer index marks the boundary of "good" elements. Everything before Writer is the output; everything at or after is either unprocessed or discarded. + +**The invariant**: `arr[0:write]` contains exactly the valid elements seen so far, in their original order. + +**Classic problems**: Remove Duplicates, Remove Element, Move Zeroes + +--- + +### Shape 3: Fast and Slow — "The Tortoise and the Hare" + +**The situation**: Two runners on a track. One runs twice as fast as the other. If the track is a loop, the fast runner will eventually lap the slow one. + +**What it feels like**: You're detecting a cycle by observing when speeds converge. + +**The mental model**: +``` +Linear track (no cycle): + Slow: 1 step per turn + Fast: 2 steps per turn + + Fast reaches the end first → No cycle + + +Circular track (cycle exists): + ┌───────────────────────────┐ + │ │ + ↓ │ + [A] → [B] → [C] → [D] → [E] ─┘ + S F + + Fast enters cycle first + Slow eventually enters + Fast "chases" slow from behind + Gap closes by 1 each step + They MUST meet inside the cycle +``` + +**The decision rule**: +- Slow moves 1 step +- Fast moves 2 steps +- If they meet → cycle exists +- If Fast reaches null → no cycle + +**Why it works**: If there's a cycle, once both pointers are inside, the relative distance changes by 1 each iteration. Since the cycle length is finite, they must eventually collide. + +**Finding the cycle start** (Phase 2): +- When they meet, reset Slow to head +- Move both at speed 1 +- They meet again at the cycle start + +This works because of the mathematical relationship between the meeting point and the cycle entry. + +**Classic problems**: Linked List Cycle, Happy Number, Find Duplicate Number + +--- + +### Shape 4: Partitioning — "The Bouncer Sorting the Queue" + +**The situation**: A bouncer at a club entrance directs each person to one of three sections: left, middle, or right. Each person is examined once and placed in their final position. + +**What it feels like**: You're sorting without sorting — classifying elements into regions in a single pass. + +**The mental model** (Dutch National Flag): +``` +┌────────────────────────────────────────────────────────────────┐ +│ │ +│ [ < pivot ] [ = pivot ] [ unclassified ] [ > pivot ]│ +│ └───────────┘ └───────────┘ └────────────────┘ └─────────┘│ +│ 0 low-1 low mid-1 mid high high+1 n-1│ +│ ↑ │ +│ examine this │ +│ │ +└────────────────────────────────────────────────────────────────┘ +``` + +**Three pointers, three regions**: +- `low`: boundary between "less than" and "equal to" +- `mid`: the examiner, scanning the unclassified region +- `high`: boundary between "unclassified" and "greater than" + +**The decision rule**: +- If `arr[mid] < pivot`: swap with `low`, advance both `low` and `mid` +- If `arr[mid] > pivot`: swap with `high`, retreat `high` only (the swapped element needs examination) +- If `arr[mid] == pivot`: advance `mid` only + +**Why it works**: Each element is placed in its final region. The `mid` pointer only advances when we're certain the element at `mid` belongs to the middle or has been swapped from a known region. + +**Classic problems**: Sort Colors, Partition Array, Sort By Parity + +--- + +### Shape 5: Dedup Enumeration — "Pinning Down the Triangle" + +**The situation**: You need to find all unique triplets (or quadruplets) with a target property. You've seen Two Sum with a hash map — now imagine finding *all* Two Sum pairs, without duplicates, inside a loop. + +**What it feels like**: You pin down one corner, then use opposite pointers to sweep the remaining candidates. + +**The mental model**: +``` +Find all triplets summing to 0: + +For each i (the anchor): + ┌─────────────────────────────────────────────────┐ + │ nums[i] is FIXED for this iteration │ + │ │ + │ [anchor] [left ═══════════════════ right] │ + │ i i+1 n-1 │ + │ │ + │ Use opposite pointers to find pairs │ + │ that complete the triplet │ + │ │ + └─────────────────────────────────────────────────┘ +``` + +**The deduplication insight**: After sorting: +- Skip `i` if `nums[i] == nums[i-1]` (don't anchor at duplicate) +- After finding a triplet, skip `left` forward while `nums[left] == nums[left+1]` +- After finding a triplet, skip `right` backward while `nums[right] == nums[right-1]` + +**Why it works**: Sorting enables two things: +1. The opposite-pointer technique for finding pairs in O(n) +2. Adjacent duplicates can be skipped to avoid duplicate triplets + +**The irreversible truth**: Once anchor `i` is processed, all triplets starting with `nums[i]` are found. Moving to `i+1` permanently excludes `nums[i]` from being an anchor again. + +**Classic problems**: 3Sum, 4Sum, 3Sum Closest + +--- + +### Shape 6: Merge — "Two Rivers Joining" + +**The situation**: Two sorted streams need to become one. You hold a cup at the head of each stream. You pour from whichever cup has the smaller value. + +**What it feels like**: You're interleaving two sorted sequences into a single sorted sequence. + +**The mental model**: +``` +Stream 1: [1] [3] [5] [7] + ↑ + i + +Stream 2: [2] [4] [6] + ↑ + j + +Output: [1] + └── smaller of arr1[i] and arr2[j] + +After pouring 1: +Stream 1: [1] [3] [5] [7] + ↑ + i + +Output: [1] [2] + └── now arr2[j] is smaller +``` + +**The decision rule**: +- Compare `arr1[i]` and `arr2[j]` +- Take the smaller one, advance that pointer +- When one stream empties, pour the remainder of the other + +**Why it works**: Both streams are sorted. The smallest unpoured element must be at one of the two heads. By always taking the smaller head, we maintain sorted order in the output. + +**In-place variant** (LeetCode 88): Write from the end to avoid overwriting unprocessed elements. + +**Classic problems**: Merge Sorted Array, Merge Two Sorted Lists, Squares of Sorted Array + +--- + +## Pattern Recognition: "Is This a Two Pointers Problem?" + +Ask yourself these questions: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ │ +│ 1. Is the sequence SORTED (or can I sort it)? │ +│ └── No? → Two pointers unlikely (consider hash map) │ +│ │ +│ 2. Am I looking for PAIRS or TUPLES with a property? │ +│ └── Yes? → Opposite pointers or dedup enumeration │ +│ │ +│ 3. Do I need to modify the array IN-PLACE? │ +│ └── Yes? → Same-direction (writer pattern) │ +│ │ +│ 4. Am I detecting a CYCLE in a sequence? │ +│ └── Yes? → Fast-slow pointers │ +│ │ +│ 5. Am I PARTITIONING by some property? │ +│ └── Yes? → Dutch flag pattern │ +│ │ +│ 6. Am I MERGING two sorted sequences? │ +│ └── Yes? → Merge pattern │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## The Moment of Recognition + +You're reading a problem. You notice: + +- *"Given a **sorted** array..."* — Sorting enables deterministic pointer movement +- *"Find **two elements** that sum to..."* — Pair search screams opposite pointers +- *"Remove ... **in-place** with O(1) extra space"* — Same-direction writer +- *"Detect if there's a **cycle**..."* — Fast-slow pointers +- *"**Sort** the array so that all X come before Y..."* — Partitioning +- *"**Merge** two sorted..."* — Merge pattern + +And you feel it: *This is a two pointers problem. I know exactly which shape.* + +That's the goal. Instant recognition. No hesitation. + +--- + +## Why O(n)? The Amortized Argument + +Every two pointers algorithm achieves O(n) through the same principle: + +> **Each pointer only moves forward.** + +Consider opposite pointers: +- `left` starts at 0, can only increase, ends at most at n-1: **at most n moves** +- `right` starts at n-1, can only decrease, ends at most at 0: **at most n moves** +- Total moves: **at most 2n = O(n)** + +Consider same-direction: +- `read` visits each position exactly once: **n moves** +- `write` moves at most once per `read` move: **at most n moves** +- Total moves: **at most 2n = O(n)** + +The magic: *No element is ever reconsidered.* This is the irreversibility that transforms quadratic brute force into linear elegance. + +--- + +## Visual Intuition: The Six Shapes Side by Side + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ OPPOSITE POINTERS │ +│ L → → → → → → → → → → → ← ← ← ← ← ← ← ← ← ← ← R │ +│ Closing in from both ends │ +├─────────────────────────────────────────────────────────────────────────┤ +│ SAME DIRECTION (WRITER) │ +│ W → → → → → (selective) │ +│ R → → → → → → → → → → (relentless) │ +│ Writer follows reader, keeping only what passes │ +├─────────────────────────────────────────────────────────────────────────┤ +│ FAST-SLOW │ +│ S → → → → → → → │ +│ F → → → → → → → → → → → → → → │ +│ If they meet, there's a cycle │ +├─────────────────────────────────────────────────────────────────────────┤ +│ PARTITIONING (DUTCH FLAG) │ +│ [ < ] [ = ] [ ? ] [ > ] │ +│ low mid high │ +│ Three regions, one-pass classification │ +├─────────────────────────────────────────────────────────────────────────┤ +│ DEDUP ENUMERATION │ +│ [anchor] L → → → → → → → → → ← ← ← ← ← ← R │ +│ i opposite pointers in subarray │ +│ Fix one, sweep the rest │ +├─────────────────────────────────────────────────────────────────────────┤ +│ MERGE │ +│ [1] [3] [5] → │ +│ [2] [4] [6] → ══> [1] [2] [3] [4] [5] [6] │ +│ Two sorted streams becoming one │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Detailed Traces: Seeing the Pattern in Motion + +### Trace 1: Opposite Pointers — Two Sum II + +**Problem**: Find two numbers in sorted array that sum to target. +**Input**: `nums = [2, 7, 11, 15]`, `target = 9` + +``` +┌────────────────────────────────────────────────────────────────────────┐ +│ Step 0: [2] [7] [11] [15] target = 9 │ +│ L R sum = 2 + 15 = 17 │ +│ 17 > 9 → move R left │ +├────────────────────────────────────────────────────────────────────────┤ +│ Step 1: [2] [7] [11] [15] │ +│ L R sum = 2 + 11 = 13 │ +│ 13 > 9 → move R left │ +├────────────────────────────────────────────────────────────────────────┤ +│ Step 2: [2] [7] [11] [15] │ +│ L R sum = 2 + 7 = 9 │ +│ 9 == 9 → FOUND! Return [1, 2] │ +└────────────────────────────────────────────────────────────────────────┘ + +Key observations: +• We never examined (2,11) or (7,11) or (7,15) — they were eliminated! +• Each step provably excluded possibilities based on monotonicity. +``` + +--- + +### Trace 2: Same-Direction — Remove Duplicates + +**Problem**: Remove duplicates from sorted array in-place. +**Input**: `nums = [1, 1, 2, 2, 2, 3]` + +``` +┌────────────────────────────────────────────────────────────────────────┐ +│ Initial: [1] [1] [2] [2] [2] [3] │ +│ W R │ +│ │ +│ Step 0: nums[R]=1, nums[W-1]=undefined → KEEP │ +│ write_index becomes 1 │ +│ [1] [1] [2] [2] [2] [3] │ +│ W R │ +│ │ +│ Step 1: nums[R]=1 == nums[W-1]=1 → SKIP │ +│ [1] [1] [2] [2] [2] [3] │ +│ W R │ +│ │ +│ Step 2: nums[R]=2 != nums[W-1]=1 → KEEP │ +│ [1] [2] [2] [2] [2] [3] │ +│ W R │ +│ │ +│ Step 3: nums[R]=2 == nums[W-1]=2 → SKIP │ +│ [1] [2] [2] [2] [2] [3] │ +│ W R │ +│ │ +│ Step 4: nums[R]=2 == nums[W-1]=2 → SKIP │ +│ [1] [2] [2] [2] [2] [3] │ +│ W R │ +│ │ +│ Step 5: nums[R]=3 != nums[W-1]=2 → KEEP │ +│ [1] [2] [3] [2] [2] [3] │ +│ W R (done) │ +│ │ +│ Result: First 3 elements [1, 2, 3] are the unique values │ +└────────────────────────────────────────────────────────────────────────┘ + +Invariant maintained throughout: +• nums[0:W] contains exactly the unique elements seen so far +• Elements are in their original sorted order +``` + +--- + +### Trace 3: Fast-Slow — Cycle Detection + +**Problem**: Detect if linked list has a cycle. +**Input**: `1 → 2 → 3 → 4 → 5 → 3` (cycle back to node 3) + +``` +┌────────────────────────────────────────────────────────────────────────┐ +│ The linked list: │ +│ │ +│ 1 → 2 → 3 → 4 → 5 │ +│ ↑ │ │ +│ └───────┘ │ +│ │ +├────────────────────────────────────────────────────────────────────────┤ +│ Step 0: S=1, F=1 (start) │ +│ │ +│ Step 1: S=2 (moved 1) │ +│ F=3 (moved 2) │ +│ │ +│ Step 2: S=3 (moved 1) │ +│ F=5 (moved 2) │ +│ │ +│ Step 3: S=4 (moved 1) │ +│ F=4 (moved 2: 5→3→4) ← F wrapped around the cycle! │ +│ │ +│ Step 4: S=5 (moved 1) │ +│ F=3 (moved 2: 4→5→3) │ +│ │ +│ Step 5: S=3 (moved 1: 5→3) │ +│ F=5 (moved 2: 3→4→5) │ +│ │ +│ Step 6: S=4 (moved 1) │ +│ F=4 (moved 2: 5→3→4) │ +│ │ +│ S == F → CYCLE DETECTED! │ +└────────────────────────────────────────────────────────────────────────┘ + +Why they MUST meet: +• Once both are in the cycle, Fast "chases" Slow +• The gap closes by 1 each step (Fast gains 1 on Slow) +• Maximum steps until collision = cycle length +``` + +--- + +### Trace 4: Dutch National Flag — Sort Colors + +**Problem**: Sort array containing only 0s, 1s, and 2s. +**Input**: `nums = [2, 0, 2, 1, 1, 0]` + +``` +┌────────────────────────────────────────────────────────────────────────┐ +│ Legend: low (L), mid (M), high (H) │ +│ Regions: [0..L) = 0s, [L..M) = 1s, [M..H] = unclassified, (H..] = 2s │ +├────────────────────────────────────────────────────────────────────────┤ +│ Initial: [2] [0] [2] [1] [1] [0] │ +│ L,M H │ +│ ↑ examine 2: >1 → swap with H, H-- │ +│ │ +│ Step 1: [0] [0] [2] [1] [1] [2] │ +│ L,M H │ +│ ↑ examine 0: <1 → swap with L, L++, M++ │ +│ │ +│ Step 2: [0] [0] [2] [1] [1] [2] │ +│ L,M H │ +│ ↑ examine 0: <1 → swap with L (self), L++, M++ │ +│ │ +│ Step 3: [0] [0] [2] [1] [1] [2] │ +│ L,M H │ +│ ↑ examine 2: >1 → swap with H, H-- │ +│ │ +│ Step 4: [0] [0] [1] [1] [2] [2] │ +│ L,M H │ +│ ↑ examine 1: =1 → M++ │ +│ │ +│ Step 5: [0] [0] [1] [1] [2] [2] │ +│ L M,H │ +│ ↑ examine 1: =1 → M++ │ +│ │ +│ Step 6: [0] [0] [1] [1] [2] [2] │ +│ L M (M > H, done!) │ +│ H │ +│ │ +│ Result: [0, 0, 1, 1, 2, 2] — sorted in single pass! │ +└────────────────────────────────────────────────────────────────────────┘ + +Key insight: +• When swapping with high, we DON'T advance mid — the swapped element is unclassified +• When swapping with low, we DO advance mid — the swapped element is known to be 0 or 1 +``` + +--- + +## From Intuition to Implementation + +Only now — after the patterns feel inevitable — does code become useful. + +### Opposite Pointers Template + +```python +def opposite_pointers(arr): + """ + Two pointers approaching from opposite ends. + Use when: sorted array, finding pairs with target property. + """ + left, right = 0, len(arr) - 1 + + while left < right: + current = evaluate(arr, left, right) + + if current == target: + return record_answer(left, right) + elif current < target: + left += 1 # Need larger: move left forward + else: + right -= 1 # Need smaller: move right backward + + return no_answer_found +``` + +### Same-Direction (Writer) Template + +```python +def same_direction(arr): + """ + Writer follows reader, keeping valid elements. + Use when: in-place modification, filtering, deduplication. + """ + write = 0 + + for read in range(len(arr)): + if should_keep(arr, read, write): + arr[write] = arr[read] + write += 1 + + return write # New logical length +``` + +### Fast-Slow Template + +```python +def fast_slow(head): + """ + Detect cycle using speed differential. + Use when: cycle detection, finding midpoint. + """ + slow = fast = head + + while fast and fast.next: + slow = slow.next + fast = fast.next.next + + if slow == fast: + return True # Cycle detected + + return False # No cycle +``` + +### Dutch National Flag Template + +```python +def partition_three_way(arr, pivot=1): + """ + Partition into three regions in single pass. + Use when: sorting by category, three-way partition. + """ + low, mid, high = 0, 0, len(arr) - 1 + + while mid <= high: + if arr[mid] < pivot: + arr[low], arr[mid] = arr[mid], arr[low] + low += 1 + mid += 1 + elif arr[mid] > pivot: + arr[mid], arr[high] = arr[high], arr[mid] + high -= 1 # Don't advance mid — swapped element is unknown + else: + mid += 1 +``` + +### Merge Template + +```python +def merge_sorted(arr1, arr2): + """ + Merge two sorted arrays into one. + Use when: combining sorted sequences. + """ + i, j = 0, 0 + result = [] + + while i < len(arr1) and j < len(arr2): + if arr1[i] <= arr2[j]: + result.append(arr1[i]) + i += 1 + else: + result.append(arr2[j]) + j += 1 + + result.extend(arr1[i:]) + result.extend(arr2[j:]) + return result +``` + +--- + +## Quick Reference: Shape Selection Guide + +| When You See... | Think... | Shape | +|----------------|----------|-------| +| "Sorted array, find pair with sum X" | Closing the gap | Opposite | +| "Remove/modify in-place with O(1) space" | Writer follows reader | Same-Direction | +| "Detect cycle in linked list" | Tortoise and hare | Fast-Slow | +| "Sort array with only 2-3 distinct values" | Bouncer sorting queue | Partitioning | +| "Find all unique triplets summing to X" | Pin one, sweep the rest | Dedup Enumeration | +| "Merge two sorted arrays" | Two rivers joining | Merge | + +--- + +## Common Pitfalls + +### Pitfall 1: Forgetting the Sorted Prerequisite + +Two pointers works because sorting creates monotonicity. If you need two pointers but the array isn't sorted, sort it first (if allowed). + +### Pitfall 2: Off-by-One in Termination + +- Opposite: `while left < right` (not `<=`) for pair problems +- Same-direction: `for read in range(len(arr))` visits all elements +- Fast-slow: check `fast and fast.next` before advancing + +### Pitfall 3: Not Handling Duplicates in Enumeration + +For 3Sum and similar, always skip duplicates at both the anchor level and the pointer level: +```python +if i > 0 and nums[i] == nums[i-1]: + continue # Skip duplicate anchor +``` + +### Pitfall 4: Advancing Mid in Dutch Flag After High Swap + +When you swap `arr[mid]` with `arr[high]`, the new value at `mid` is unclassified. Don't advance `mid` — examine it in the next iteration. + +--- + +## The Two Pointers Mantra + +> **One invariant: the answer lies between.** +> **One rule: once passed, never reconsidered.** +> **One result: O(n) elegance from O(n²) brute force.** + +When you see sorted sequences and pair-finding, think of the sentinels. When you see in-place modification, think of the writer following the reader. When you see cycles, think of the tortoise and hare. + +The pattern is always the same: *coordinated movement, irreversible exclusion, linear time.* + +--- + +*For implementation templates and problem mappings, see [templates.md](./templates.md).* + diff --git a/tools/README.md b/tools/README.md index dfa7e30..0de82b8 100644 --- a/tools/README.md +++ b/tools/README.md @@ -83,6 +83,7 @@ Checks all solution files for Pure Polymorphic Architecture compliance. python tools/check_solutions.py # Standard check python tools/check_solutions.py --verbose # Show fix suggestions python tools/check_solutions.py --list-warnings # List only files with warnings +python tools/check_solutions.py --show-warnings # Show warnings with suggestions ``` **Checks Performed:** From 04c2bf8cdc5fa3fae65b8973c358dcdad9c77ed6 Mon Sep 17 00:00:00 2001 From: lufftw Date: Sun, 14 Dec 2025 16:20:29 +0800 Subject: [PATCH 09/11] docs: add two-path navigation (Intuition + Templates) to pattern README - Add "How to Use This Documentation" section explaining learning paths - Update pattern table with direct links to intuition.md and templates.md - Apply to sliding_window, two_pointers, and backtracking_exploration --- docs/patterns/README.md | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/docs/patterns/README.md b/docs/patterns/README.md index 1375332..376103a 100644 --- a/docs/patterns/README.md +++ b/docs/patterns/README.md @@ -7,14 +7,27 @@ This directory contains comprehensive documentation for each **API Kernel** and --- +## How to Use This Documentation + +Each pattern provides **two learning paths** to help you master the concepts: + +| Path | Purpose | Best For | +|------|---------|----------| +| 💡 **Intuition** | Understand the "why" through stories and visual explanations | First-time learners, building mental models | +| 🛠️ **Templates** | Copy-paste ready code with problem-specific variations | Interview prep, quick reference | + +**Recommended approach**: Start with Intuition to build understanding, then use Templates for implementation. + +--- + ## Available Pattern Guides -| API Kernel | Document | Description | Problems | -|------------|----------|-------------|----------| -| `SubstringSlidingWindow` | [sliding_window.md](sliding_window.md) | Dynamic window over sequences | LeetCode 3, 76, 159, 209, 340, 438, 567 | -| `TwoPointersTraversal` | [two_pointers.md](two_pointers.md) | Two pointer traversal patterns | LeetCode 1, 11, 15, 16, 21, 26, 27, 75, 88, 125, 141, 142, 167, 202, 283, 680, 876 | +| API Kernel | Learning Resources | Description | Problems | +|------------|-------------------|-------------|----------| +| `SubstringSlidingWindow` | 💡 [Intuition](sliding_window/intuition.md) · 🛠️ [Templates](sliding_window/templates.md) | Dynamic window over sequences | LeetCode 3, 76, 159, 209, 340, 438, 567 | +| `TwoPointersTraversal` | 💡 [Intuition](two_pointers/intuition.md) · 🛠️ [Templates](two_pointers/templates.md) | Two pointer traversal patterns | LeetCode 1, 11, 15, 16, 21, 26, 27, 75, 88, 125, 141, 142, 167, 202, 283, 680, 876 | +| `BacktrackingExploration` | 💡 [Intuition](backtracking_exploration/intuition.md) · 🛠️ [Templates](backtracking_exploration/templates.md) | Exhaustive search with pruning | LeetCode 39, 40, 46, 47, 51, 77, 78, 79, 90, 93, 131, 216 | | `GridBFSMultiSource` | *coming soon* | Multi-source BFS on grids | LeetCode 994, 286, 542 | -| `BacktrackingExploration` | [backtracking_exploration.md](backtracking_exploration.md) | Exhaustive search with pruning | LeetCode 39, 40, 46, 47, 51, 77, 78, 79, 90, 93, 131, 216 | | `KWayMerge` | *coming soon* | Merge K sorted sequences | LeetCode 23, 21, 88 | | `BinarySearchBoundary` | *coming soon* | Binary search boundaries | LeetCode 4, 33, 34, 35 | | `LinkedListInPlaceReversal` | *coming soon* | In-place linked list reversal | LeetCode 25, 206, 92 | From bda851f7996d2fd70861a059d86251ec79042912 Mon Sep 17 00:00:00 2001 From: lufftw Date: Sun, 14 Dec 2025 16:24:00 +0800 Subject: [PATCH 10/11] docs: improve Templates description wording in patterns README Change "Copy-paste ready code" to "Production-ready implementations" for a more professional tone. --- docs/patterns/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/README.md b/docs/patterns/README.md index 376103a..ab784b3 100644 --- a/docs/patterns/README.md +++ b/docs/patterns/README.md @@ -14,7 +14,7 @@ Each pattern provides **two learning paths** to help you master the concepts: | Path | Purpose | Best For | |------|---------|----------| | 💡 **Intuition** | Understand the "why" through stories and visual explanations | First-time learners, building mental models | -| 🛠️ **Templates** | Copy-paste ready code with problem-specific variations | Interview prep, quick reference | +| 🛠️ **Templates** | Production-ready implementations with problem-specific variations | Interview prep, quick reference | **Recommended approach**: Start with Intuition to build understanding, then use Templates for implementation. From 9b34c7d5e2726ff139661e9955c5d9769addd241 Mon Sep 17 00:00:00 2001 From: lufftw Date: Sun, 14 Dec 2025 16:28:28 +0800 Subject: [PATCH 11/11] docs: update pattern documentation to two-path learning structure - Add Intuition and Templates learning paths for each pattern - Update BacktrackingExploration from "coming soon" to available - Add LinkedListInPlaceReversal and MonotonicStack as upcoming patterns - Update README.md and README_zh-TW.md with new table format --- README.md | 27 +++++++++++++++++---------- README_zh-TW.md | 19 +++++++++++++------ 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index c94186c..65fcdc3 100644 --- a/README.md +++ b/README.md @@ -343,16 +343,23 @@ Our **AI Ontology Analyzer** processes the entire knowledge graph — API Kernel > **"Don't memorize 200 problems. Master 10 patterns."** -Each API Kernel has a dedicated pattern guide with **base template**, **variations**, and **copy-paste ready code**. - -| API Kernel | Guide | Problems | -|:-----------|:-----:|:---------| -| `SubstringSlidingWindow` | [📖](docs/patterns/sliding_window.md) | LeetCode 3, 76, 159, 209, 340, 438, 567 | -| `TwoPointersTraversal` | [📖](docs/patterns/two_pointers.md) | LeetCode 1, 11, 15, 16, 21, 26, 27, 75, 88, 125, 141, 142, 167, 202, 283, 680, 876 | -| `GridBFSMultiSource` | *soon* | LeetCode 994, 286, 542 | -| `BacktrackingExploration` | *soon* | LeetCode 51, 52, 46, 78 | -| `KWayMerge` | *soon* | LeetCode 23, 21, 88 | -| `BinarySearchBoundary` | *soon* | LeetCode 4, 33, 34, 35 | +Each pattern provides **two learning paths**: + +| Path | Purpose | Best For | +|:-----|:--------|:---------| +| 💡 **Intuition** | Understand the "why" through stories and visual explanations | First-time learners, building mental models | +| 🛠️ **Templates** | Production-ready implementations with problem-specific variations | Interview prep, quick reference | + +| API Kernel | Learning Resources | Problems | +|:-----------|:-------------------|:---------| +| `SubstringSlidingWindow` | 💡 [Intuition](docs/patterns/sliding_window/intuition.md) · 🛠️ [Templates](docs/patterns/sliding_window/templates.md) | LeetCode 3, 76, 159, 209, 340, 438, 567 | +| `TwoPointersTraversal` | 💡 [Intuition](docs/patterns/two_pointers/intuition.md) · 🛠️ [Templates](docs/patterns/two_pointers/templates.md) | LeetCode 1, 11, 15, 16, 21, 26, 27, 75, 88, 125, 141, 142, 167, 202, 283, 680, 876 | +| `BacktrackingExploration` | 💡 [Intuition](docs/patterns/backtracking_exploration/intuition.md) · 🛠️ [Templates](docs/patterns/backtracking_exploration/templates.md) | LeetCode 39, 40, 46, 47, 51, 77, 78, 79, 90, 93, 131, 216 | +| `GridBFSMultiSource` | *coming soon* | LeetCode 994, 286, 542 | +| `KWayMerge` | *coming soon* | LeetCode 23, 21, 88 | +| `BinarySearchBoundary` | *coming soon* | LeetCode 4, 33, 34, 35 | +| `LinkedListInPlaceReversal` | *coming soon* | LeetCode 25, 206, 92 | +| `MonotonicStack` | *coming soon* | LeetCode 84, 85, 496 | 👉 **[View All Pattern Guides →](docs/patterns/README.md)** diff --git a/README_zh-TW.md b/README_zh-TW.md index 809b8cc..497c766 100644 --- a/README_zh-TW.md +++ b/README_zh-TW.md @@ -343,16 +343,23 @@ scripts\run_tests.bat 0001_two_sum > **「不要死背 200 道題。掌握 10 個模式。」** -每個 API 核心都有專屬的模式指南,包含**基礎模板**、**變體**和**可直接複製的程式碼**。 +每個模式提供**兩條學習路徑**: -| API 核心 | 指南 | 題目 | -|:---------|:----:|:-----| -| `SubstringSlidingWindow` | [📖](docs/patterns/sliding_window.md) | LeetCode 3, 76, 159, 209, 340, 438, 567 | -| `TwoPointersTraversal` | [📖](docs/patterns/two_pointers.md) | LeetCode 1, 11, 15, 16, 21, 26, 27, 75, 88, 125, 141, 142, 167, 202, 283, 680, 876 | +| 路徑 | 目的 | 適合對象 | +|:-----|:-----|:---------| +| 💡 **直覺理解** | 透過故事和視覺化解釋理解「為什麼」 | 初學者、建立心智模型 | +| 🛠️ **模板** | 生產級實作與問題專屬變體 | 面試準備、快速參考 | + +| API 核心 | 學習資源 | 題目 | +|:---------|:---------|:-----| +| `SubstringSlidingWindow` | 💡 [直覺理解](docs/patterns/sliding_window/intuition.md) · 🛠️ [模板](docs/patterns/sliding_window/templates.md) | LeetCode 3, 76, 159, 209, 340, 438, 567 | +| `TwoPointersTraversal` | 💡 [直覺理解](docs/patterns/two_pointers/intuition.md) · 🛠️ [模板](docs/patterns/two_pointers/templates.md) | LeetCode 1, 11, 15, 16, 21, 26, 27, 75, 88, 125, 141, 142, 167, 202, 283, 680, 876 | +| `BacktrackingExploration` | 💡 [直覺理解](docs/patterns/backtracking_exploration/intuition.md) · 🛠️ [模板](docs/patterns/backtracking_exploration/templates.md) | LeetCode 39, 40, 46, 47, 51, 77, 78, 79, 90, 93, 131, 216 | | `GridBFSMultiSource` | *即將推出* | LeetCode 994, 286, 542 | -| `BacktrackingExploration` | *即將推出* | LeetCode 51, 52, 46, 78 | | `KWayMerge` | *即將推出* | LeetCode 23, 21, 88 | | `BinarySearchBoundary` | *即將推出* | LeetCode 4, 33, 34, 35 | +| `LinkedListInPlaceReversal` | *即將推出* | LeetCode 25, 206, 92 | +| `MonotonicStack` | *即將推出* | LeetCode 84, 85, 496 | 👉 **[查看所有模式指南 →](docs/patterns/README.md)**