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)** diff --git a/docs/patterns/README.md b/docs/patterns/README.md index d9e2512..ab784b3 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** | Production-ready implementations 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` | *coming soon* | Exhaustive search with pruning | LeetCode 51, 52, 46, 78 | | `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_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 similarity index 99% rename from docs/patterns/backtracking_exploration_templates.md rename to docs/patterns/backtracking_exploration/templates.md index 8005f60..1c9bae2 100644 --- a/docs/patterns/backtracking_exploration_templates.md +++ b/docs/patterns/backtracking_exploration/templates.md @@ -26,7 +26,8 @@ This document presents the **canonical backtracking template** and all its major 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) +18. [LeetCode Problem Mapping](#18-leetcode-problem-mapping) +19. [Template Quick Reference](#19-template-quick-reference) --- @@ -117,6 +118,8 @@ Backtracking algorithms typically have exponential or factorial complexity becau --- +--- + ## 2. Base Template: Permutations (LeetCode 46) > **Problem**: Given an array of distinct integers, return all possible permutations. @@ -1291,9 +1294,22 @@ Is the problem asking for ALL solutions? --- -## 18. Template Quick Reference +## 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 | + +--- + +## 19. Template Quick Reference -### 18.1 Permutation Template +### 19.1 Permutation Template ```python def permute(nums): @@ -1318,7 +1334,7 @@ def permute(nums): return results ``` -### 18.2 Subset/Combination Template +### 19.2 Subset/Combination Template ```python def subsets(nums): @@ -1336,7 +1352,7 @@ def subsets(nums): return results ``` -### 18.3 Target Sum Template +### 19.3 Target Sum Template ```python def combination_sum(candidates, target): @@ -1358,7 +1374,7 @@ def combination_sum(candidates, target): return results ``` -### 18.4 Grid Search Template +### 19.4 Grid Search Template ```python def grid_search(grid, word): @@ -1388,22 +1404,13 @@ def grid_search(grid, word): 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* + +*Document generated for NeetCode Practice Framework โ€” API Kernel: BacktrackingExploration* \ No newline at end of file diff --git a/docs/patterns/sliding_window/intuition.md b/docs/patterns/sliding_window/intuition.md new file mode 100644 index 0000000..36c8bff --- /dev/null +++ b/docs/patterns/sliding_window/intuition.md @@ -0,0 +1,1069 @@ +# 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) $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) $L$ +- 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. + +#### 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?"* + +``` +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. + +#### 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?" + +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 + +#### 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 + +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 + +**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 | + +**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` + +--- + +## 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() + + ## 1. Explorer (R) always advances + for right, element in enumerate(sequence): + # Explorer: include new element + update_state_add(state, element) + + ## 2. Gatekeeper (L) acts to restore invariant + 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 | + +--- + +## 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.* + +When you see a problem about optimizing over contiguous sequences with incrementally checkable properties โ€” you've found your window. + +Let it slide. + +--- + +*For additional variations and template reference, see [templates.md](./templates.md).* 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/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/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/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 new file mode 100644 index 0000000..2bae9d0 --- /dev/null +++ b/meta/patterns/backtracking_exploration/_config.toml @@ -0,0 +1,40 @@ +# 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 = [ + "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 comparison, decision, mapping, templates) +footer_files = [ + "_deduplication.md", + "_pruning.md", + "_comparison.md", + "_decision.md", + "_mapping.md", + "_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/_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/_header.md b/meta/patterns/backtracking_exploration/_header.md new file mode 100644 index 0000000..095e1ed --- /dev/null +++ b/meta/patterns/backtracking_exploration/_header.md @@ -0,0 +1,95 @@ +# 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 + +### 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 +``` + +### 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) +``` + +### 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 | + +### 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. + +### 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 | + +--- 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 +``` + diff --git a/meta/patterns/backtracking_exploration/_templates.md b/meta/patterns/backtracking_exploration/_templates.md new file mode 100644 index 0000000..baf5251 --- /dev/null +++ b/meta/patterns/backtracking_exploration/_templates.md @@ -0,0 +1,100 @@ +## Template Quick Reference + +### 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 +``` + +### 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 +``` + +### 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 +``` + +### 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 + +``` + 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" + 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:** 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: