feat(patterns): pattern taxonomy + is_optimal
This commit is contained in:
35
backend/alembic/versions/006_pattern_taxonomy.py
Normal file
35
backend/alembic/versions/006_pattern_taxonomy.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""add pattern taxonomy fields
|
||||
|
||||
Revision ID: 006
|
||||
Revises: 005
|
||||
Create Date: 2025-07-05
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "006"
|
||||
down_revision: str | None = "005"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add pattern_type and display_order to patterns table
|
||||
op.add_column("patterns", sa.Column("pattern_type", sa.String(50), nullable=True))
|
||||
op.add_column("patterns", sa.Column("display_order", sa.Integer(), nullable=True))
|
||||
|
||||
# Add is_optimal to question_patterns junction table
|
||||
op.add_column(
|
||||
"question_patterns",
|
||||
sa.Column("is_optimal", sa.Boolean(), server_default="false", nullable=False),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("question_patterns", "is_optimal")
|
||||
op.drop_column("patterns", "display_order")
|
||||
op.drop_column("patterns", "pattern_type")
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Backtracking
|
||||
slug: backtracking
|
||||
difficulty_level: 3
|
||||
pattern_type: algorithm
|
||||
display_order: 10
|
||||
|
||||
description: >
|
||||
Build solutions incrementally, exploring choices one at a time and abandoning
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: BFS (Breadth-First Search)
|
||||
slug: bfs
|
||||
difficulty_level: 3
|
||||
pattern_type: algorithm
|
||||
display_order: 6
|
||||
|
||||
description: >
|
||||
Level-by-level traversal using a queue, exploring all neighbors at the current
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Binary Search
|
||||
slug: binary-search
|
||||
difficulty_level: 2
|
||||
pattern_type: algorithm
|
||||
display_order: 4
|
||||
|
||||
description: >
|
||||
Efficiently search sorted data by repeatedly dividing the search space in half.
|
||||
@@ -200,3 +202,172 @@ related_patterns:
|
||||
- two-pointers
|
||||
|
||||
prerequisite_patterns: []
|
||||
|
||||
visualization_examples:
|
||||
- id: binary-search-find-target
|
||||
title: Find Target in Sorted Array
|
||||
input:
|
||||
nums: [1, 3, 5, 7, 9, 11, 13]
|
||||
target: 9
|
||||
code: |
|
||||
def binary_search(nums, target):
|
||||
left, right = 0, len(nums) - 1
|
||||
|
||||
while left <= right:
|
||||
mid = left + (right - left) // 2
|
||||
|
||||
if nums[mid] == target:
|
||||
return mid
|
||||
elif nums[mid] < target:
|
||||
left = mid + 1
|
||||
else:
|
||||
right = mid - 1
|
||||
|
||||
return -1
|
||||
steps:
|
||||
- id: step-1
|
||||
description: "Initialize pointers: left=0, right=6. Search space is entire array."
|
||||
structures:
|
||||
nums:
|
||||
type: array
|
||||
values:
|
||||
- { value: 1, state: default }
|
||||
- { value: 3, state: default }
|
||||
- { value: 5, state: default }
|
||||
- { value: 7, state: default }
|
||||
- { value: 9, state: default }
|
||||
- { value: 11, state: default }
|
||||
- { value: 13, state: default }
|
||||
pointers:
|
||||
left: 0
|
||||
right: 6
|
||||
variables:
|
||||
target: 9
|
||||
left: 0
|
||||
right: 6
|
||||
codeHighlight:
|
||||
startLine: 2
|
||||
endLine: 2
|
||||
|
||||
- id: step-2
|
||||
description: "Calculate mid=3. nums[3]=7 < target=9, so search right half."
|
||||
structures:
|
||||
nums:
|
||||
type: array
|
||||
values:
|
||||
- { value: 1, state: visited }
|
||||
- { value: 3, state: visited }
|
||||
- { value: 5, state: visited }
|
||||
- { value: 7, state: comparing }
|
||||
- { value: 9, state: default }
|
||||
- { value: 11, state: default }
|
||||
- { value: 13, state: default }
|
||||
pointers:
|
||||
left: 0
|
||||
mid: 3
|
||||
right: 6
|
||||
variables:
|
||||
target: 9
|
||||
left: 0
|
||||
right: 6
|
||||
mid: 3
|
||||
codeHighlight:
|
||||
startLine: 9
|
||||
endLine: 10
|
||||
|
||||
- id: step-3
|
||||
description: "Update left=4. New search space is indices 4-6."
|
||||
structures:
|
||||
nums:
|
||||
type: array
|
||||
values:
|
||||
- { value: 1, state: visited }
|
||||
- { value: 3, state: visited }
|
||||
- { value: 5, state: visited }
|
||||
- { value: 7, state: visited }
|
||||
- { value: 9, state: default }
|
||||
- { value: 11, state: default }
|
||||
- { value: 13, state: default }
|
||||
pointers:
|
||||
left: 4
|
||||
right: 6
|
||||
variables:
|
||||
target: 9
|
||||
left: 4
|
||||
right: 6
|
||||
codeHighlight:
|
||||
startLine: 4
|
||||
endLine: 4
|
||||
|
||||
- id: step-4
|
||||
description: "Calculate mid=5. nums[5]=11 > target=9, so search left half."
|
||||
structures:
|
||||
nums:
|
||||
type: array
|
||||
values:
|
||||
- { value: 1, state: visited }
|
||||
- { value: 3, state: visited }
|
||||
- { value: 5, state: visited }
|
||||
- { value: 7, state: visited }
|
||||
- { value: 9, state: default }
|
||||
- { value: 11, state: comparing }
|
||||
- { value: 13, state: visited }
|
||||
pointers:
|
||||
left: 4
|
||||
mid: 5
|
||||
right: 6
|
||||
variables:
|
||||
target: 9
|
||||
left: 4
|
||||
right: 6
|
||||
mid: 5
|
||||
codeHighlight:
|
||||
startLine: 11
|
||||
endLine: 12
|
||||
|
||||
- id: step-5
|
||||
description: "Update right=4. Search space narrowed to index 4 only."
|
||||
structures:
|
||||
nums:
|
||||
type: array
|
||||
values:
|
||||
- { value: 1, state: visited }
|
||||
- { value: 3, state: visited }
|
||||
- { value: 5, state: visited }
|
||||
- { value: 7, state: visited }
|
||||
- { value: 9, state: active }
|
||||
- { value: 11, state: visited }
|
||||
- { value: 13, state: visited }
|
||||
pointers:
|
||||
left: 4
|
||||
right: 4
|
||||
variables:
|
||||
target: 9
|
||||
left: 4
|
||||
right: 4
|
||||
codeHighlight:
|
||||
startLine: 4
|
||||
endLine: 4
|
||||
|
||||
- id: step-6
|
||||
description: "Calculate mid=4. nums[4]=9 == target=9. Found at index 4!"
|
||||
structures:
|
||||
nums:
|
||||
type: array
|
||||
values:
|
||||
- { value: 1, state: visited }
|
||||
- { value: 3, state: visited }
|
||||
- { value: 5, state: visited }
|
||||
- { value: 7, state: visited }
|
||||
- { value: 9, state: found }
|
||||
- { value: 11, state: visited }
|
||||
- { value: 13, state: visited }
|
||||
pointers:
|
||||
mid: 4
|
||||
variables:
|
||||
target: 9
|
||||
mid: 4
|
||||
result: 4
|
||||
codeHighlight:
|
||||
startLine: 7
|
||||
endLine: 8
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Bit Manipulation
|
||||
slug: bit-manipulation
|
||||
difficulty_level: 2
|
||||
pattern_type: technique
|
||||
display_order: 19
|
||||
|
||||
description: >
|
||||
Techniques using binary operations (AND, OR, XOR, NOT, shifts) to solve
|
||||
|
||||
92
backend/data/patterns/counting-sort.yaml
Normal file
92
backend/data/patterns/counting-sort.yaml
Normal file
@@ -0,0 +1,92 @@
|
||||
name: Counting / Bucket Sort
|
||||
slug: counting-sort
|
||||
difficulty_level: 2
|
||||
pattern_type: technique
|
||||
display_order: 20
|
||||
|
||||
description: >
|
||||
Exploit bounded value ranges to achieve linear time sorting or selection
|
||||
by using values as array indices.
|
||||
|
||||
when_to_use: |
|
||||
- Finding top K elements when frequencies are bounded
|
||||
- Sorting when values are in a known, limited range
|
||||
- Problems involving frequency counting with bounded inputs
|
||||
- Color sorting (Dutch National Flag)
|
||||
|
||||
metaphor: |
|
||||
Imagine sorting mail into numbered PO boxes. Instead of comparing letters
|
||||
to each other, you simply look at the box number and drop it in. If you
|
||||
have 100 boxes, sorting 1000 letters takes 1000 steps, not 1000 x log(1000).
|
||||
|
||||
core_concept: |
|
||||
When values are bounded within a known range [0, k], you can use the value
|
||||
itself as an index into an array of "buckets." This converts comparison-based
|
||||
O(n log n) sorting into O(n + k) counting operations.
|
||||
|
||||
The key insight: **bounded values = direct addressing is possible**.
|
||||
|
||||
code_template: |
|
||||
def bucket_sort_approach(nums: list[int], k: int) -> list[int]:
|
||||
# Create buckets indexed by value/frequency
|
||||
n = len(nums)
|
||||
buckets = [[] for _ in range(n + 1)] # n+1 for frequency 0 to n
|
||||
|
||||
# Count frequencies
|
||||
count = {}
|
||||
for num in nums:
|
||||
count[num] = count.get(num, 0) + 1
|
||||
|
||||
# Place elements in frequency buckets
|
||||
for num, freq in count.items():
|
||||
buckets[freq].append(num)
|
||||
|
||||
# Collect from highest frequency
|
||||
result = []
|
||||
for i in range(n, 0, -1):
|
||||
for num in buckets[i]:
|
||||
result.append(num)
|
||||
if len(result) == k:
|
||||
return result
|
||||
return result
|
||||
|
||||
recognition_signals:
|
||||
- "top k frequent"
|
||||
- "sort colors"
|
||||
- "values in range [0, n]"
|
||||
- "frequency bounded by array size"
|
||||
- "O(n) time required"
|
||||
- "counting occurrences"
|
||||
|
||||
common_mistakes:
|
||||
- title: Using Heap When Bucket Sort is Optimal
|
||||
description: |
|
||||
Heap gives O(n log k) but bucket sort gives O(n) when frequencies
|
||||
are bounded. Always check if values/frequencies have a known upper bound.
|
||||
fix: |
|
||||
Ask: "What's the maximum possible value/frequency?" If bounded by n,
|
||||
use bucket sort.
|
||||
|
||||
- title: Off-by-One in Bucket Array
|
||||
description: |
|
||||
Creating `n` buckets for frequencies 0 to n-1 misses frequency `n`
|
||||
(when all elements are identical).
|
||||
fix: |
|
||||
Create `n + 1` buckets to handle frequencies from 0 to n inclusive.
|
||||
|
||||
variations:
|
||||
- name: Top K Frequent Elements
|
||||
description: Use frequency as bucket index, collect from highest
|
||||
example: "top-k-frequent-elements"
|
||||
- name: Sort Colors (Dutch National Flag)
|
||||
description: Three buckets for 0, 1, 2
|
||||
example: "sort-colors"
|
||||
- name: H-Index
|
||||
description: Citation count buckets
|
||||
example: "h-index"
|
||||
|
||||
related_patterns:
|
||||
- heap
|
||||
- two-pointers
|
||||
|
||||
prerequisite_patterns: []
|
||||
96
backend/data/patterns/cyclic-sort.yaml
Normal file
96
backend/data/patterns/cyclic-sort.yaml
Normal file
@@ -0,0 +1,96 @@
|
||||
name: Cyclic Sort
|
||||
slug: cyclic-sort
|
||||
difficulty_level: 2
|
||||
pattern_type: algorithm
|
||||
display_order: 21
|
||||
|
||||
description: >
|
||||
Place each number at its "correct" index when dealing with arrays containing
|
||||
numbers in the range [1, n] or [0, n-1].
|
||||
|
||||
when_to_use: |
|
||||
- Array contains numbers from 1 to n (or 0 to n-1)
|
||||
- Finding missing/duplicate numbers in such arrays
|
||||
- Problems asking for O(1) space and O(n) time
|
||||
- "First missing positive" type problems
|
||||
|
||||
metaphor: |
|
||||
Imagine a row of n chairs numbered 1 to n, and n people each holding a
|
||||
ticket with their seat number. Instead of searching for each person's seat,
|
||||
you ask everyone to swap positions until they're in their ticketed seat.
|
||||
After at most n swaps, everyone is seated correctly.
|
||||
|
||||
core_concept: |
|
||||
For an array where values should map directly to indices (e.g., value 3
|
||||
belongs at index 2 if 1-indexed), repeatedly swap each element to its
|
||||
correct position until the array is "sorted."
|
||||
|
||||
Key insight: Each swap places at least one element correctly, so at most
|
||||
n swaps are needed -> O(n) time with O(1) space.
|
||||
|
||||
code_template: |
|
||||
def cyclic_sort(nums: list[int]) -> list[int]:
|
||||
"""Sort array where values are in range [1, n]."""
|
||||
i = 0
|
||||
while i < len(nums):
|
||||
# Correct index for value nums[i] (1-indexed value -> 0-indexed position)
|
||||
correct_idx = nums[i] - 1
|
||||
|
||||
# If not in correct position and not a duplicate, swap
|
||||
if nums[i] != nums[correct_idx]:
|
||||
nums[i], nums[correct_idx] = nums[correct_idx], nums[i]
|
||||
else:
|
||||
i += 1
|
||||
return nums
|
||||
|
||||
def find_missing(nums: list[int]) -> int:
|
||||
"""Find missing number after cyclic sort."""
|
||||
cyclic_sort(nums)
|
||||
for i, num in enumerate(nums):
|
||||
if num != i + 1:
|
||||
return i + 1
|
||||
return len(nums) + 1
|
||||
|
||||
recognition_signals:
|
||||
- "array of length n with values 1 to n"
|
||||
- "find the missing number"
|
||||
- "find the duplicate"
|
||||
- "first missing positive"
|
||||
- "O(1) extra space"
|
||||
- "in-place rearrangement"
|
||||
|
||||
common_mistakes:
|
||||
- title: Infinite Loop on Duplicates
|
||||
description: |
|
||||
Swapping endlessly when current value equals value at target index
|
||||
(both are duplicates).
|
||||
fix: |
|
||||
Check `nums[i] != nums[correct_idx]` before swapping, not just
|
||||
`nums[i] != correct_idx + 1`.
|
||||
|
||||
- title: Wrong Index Calculation
|
||||
description: |
|
||||
Confusing 0-indexed vs 1-indexed. If values are 1 to n, correct
|
||||
index is `value - 1`. If values are 0 to n-1, correct index equals value.
|
||||
fix: |
|
||||
Clearly define the mapping: `correct_idx = nums[i] - 1` for 1-indexed values.
|
||||
|
||||
variations:
|
||||
- name: Find Missing Number
|
||||
description: After cyclic sort, scan for index where value != index + 1
|
||||
example: "missing-number"
|
||||
- name: Find Duplicate
|
||||
description: After cyclic sort, find where value != expected
|
||||
example: "find-the-duplicate-number"
|
||||
- name: First Missing Positive
|
||||
description: Ignore negatives and values > n, then cyclic sort
|
||||
example: "first-missing-positive"
|
||||
- name: Find All Missing Numbers
|
||||
description: Collect all indices where value != expected
|
||||
example: "find-all-numbers-disappeared-in-an-array"
|
||||
|
||||
related_patterns:
|
||||
- counting-sort
|
||||
- fast-slow-pointers
|
||||
|
||||
prerequisite_patterns: []
|
||||
@@ -1,6 +1,8 @@
|
||||
name: DFS (Depth-First Search)
|
||||
slug: dfs
|
||||
difficulty_level: 3
|
||||
pattern_type: algorithm
|
||||
display_order: 7
|
||||
|
||||
description: >
|
||||
Explore as deep as possible along each branch before backtracking, using
|
||||
|
||||
97
backend/data/patterns/divide-and-conquer.yaml
Normal file
97
backend/data/patterns/divide-and-conquer.yaml
Normal file
@@ -0,0 +1,97 @@
|
||||
name: Divide and Conquer
|
||||
slug: divide-and-conquer
|
||||
difficulty_level: 3
|
||||
pattern_type: algorithm
|
||||
display_order: 23
|
||||
|
||||
description: >
|
||||
Break a problem into smaller subproblems, solve them independently,
|
||||
and combine results. Unlike DP, subproblems don't overlap.
|
||||
|
||||
when_to_use: |
|
||||
- Problems naturally split into independent halves
|
||||
- Merge sort style combining
|
||||
- Finding kth element efficiently
|
||||
- Tree-structured recursion without memoization needs
|
||||
|
||||
metaphor: |
|
||||
Imagine sorting a deck of cards by splitting it in half, having two
|
||||
friends each sort their half, then merging the sorted halves. Each
|
||||
friend can recursively split their half too. The key: halves are
|
||||
independent and combining sorted halves is easy.
|
||||
|
||||
core_concept: |
|
||||
Three steps:
|
||||
1. **Divide:** Split problem into smaller subproblems
|
||||
2. **Conquer:** Solve subproblems recursively (base case when trivial)
|
||||
3. **Combine:** Merge subproblem solutions into final answer
|
||||
|
||||
Time complexity often follows: T(n) = aT(n/b) + O(n^c)
|
||||
Solved by Master Theorem.
|
||||
|
||||
code_template: |
|
||||
def merge_sort(arr: list[int]) -> list[int]:
|
||||
"""Classic divide and conquer example."""
|
||||
if len(arr) <= 1:
|
||||
return arr
|
||||
|
||||
# Divide
|
||||
mid = len(arr) // 2
|
||||
left = merge_sort(arr[:mid])
|
||||
right = merge_sort(arr[mid:])
|
||||
|
||||
# Combine (merge)
|
||||
return merge(left, right)
|
||||
|
||||
def merge(left: list[int], right: list[int]) -> list[int]:
|
||||
result = []
|
||||
i = j = 0
|
||||
while i < len(left) and j < len(right):
|
||||
if left[i] <= right[j]:
|
||||
result.append(left[i])
|
||||
i += 1
|
||||
else:
|
||||
result.append(right[j])
|
||||
j += 1
|
||||
result.extend(left[i:])
|
||||
result.extend(right[j:])
|
||||
return result
|
||||
|
||||
recognition_signals:
|
||||
- "merge sort"
|
||||
- "find kth largest/smallest"
|
||||
- "count inversions"
|
||||
- "closest pair of points"
|
||||
- "problems on sorted arrays that can be split"
|
||||
|
||||
common_mistakes:
|
||||
- title: Confusing with Dynamic Programming
|
||||
description: |
|
||||
D&C subproblems are independent. DP subproblems overlap and need
|
||||
memoization. Using D&C on overlapping subproblems causes exponential time.
|
||||
fix: |
|
||||
Check: Do subproblems share computation? If yes, use DP. If no, use D&C.
|
||||
|
||||
- title: Inefficient Combine Step
|
||||
description: |
|
||||
If combining takes O(n^2), overall might not be better than brute force.
|
||||
fix: |
|
||||
Design O(n) or O(n log n) combine step. Merge sort's merge is O(n).
|
||||
|
||||
variations:
|
||||
- name: Merge Sort
|
||||
description: Sort by splitting, sorting halves, merging
|
||||
example: "sort-an-array"
|
||||
- name: Quick Select
|
||||
description: Find kth element by partitioning
|
||||
example: "kth-largest-element-in-an-array"
|
||||
- name: Count Inversions
|
||||
description: Count pairs where larger element precedes smaller
|
||||
example: "count-of-smaller-numbers-after-self"
|
||||
|
||||
related_patterns:
|
||||
- binary-search
|
||||
- dynamic-programming
|
||||
|
||||
prerequisite_patterns:
|
||||
- binary-search
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Dynamic Programming
|
||||
slug: dynamic-programming
|
||||
difficulty_level: 4
|
||||
pattern_type: technique
|
||||
display_order: 11
|
||||
|
||||
description: >
|
||||
Break problems into overlapping subproblems, storing results to avoid
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Fast & Slow Pointers
|
||||
slug: fast-slow-pointers
|
||||
difficulty_level: 2
|
||||
pattern_type: algorithm
|
||||
display_order: 3
|
||||
|
||||
description: >
|
||||
Use two pointers moving at different speeds to detect cycles, find midpoints,
|
||||
@@ -296,3 +298,188 @@ related_patterns:
|
||||
|
||||
prerequisite_patterns:
|
||||
- two-pointers
|
||||
|
||||
visualization_examples:
|
||||
- id: find-middle-of-list
|
||||
title: Find Middle of Linked List
|
||||
input:
|
||||
list: [1, 2, 3, 4, 5]
|
||||
code: |
|
||||
def find_middle(head):
|
||||
slow = fast = head
|
||||
|
||||
while fast and fast.next:
|
||||
slow = slow.next
|
||||
fast = fast.next.next
|
||||
|
||||
return slow
|
||||
steps:
|
||||
- id: step-1
|
||||
description: "Initialize slow and fast pointers at head (node 1)."
|
||||
structures:
|
||||
list:
|
||||
type: linkedlist
|
||||
nodes:
|
||||
- { value: 1, state: active }
|
||||
- { value: 2, state: default }
|
||||
- { value: 3, state: default }
|
||||
- { value: 4, state: default }
|
||||
- { value: 5, state: default }
|
||||
pointers:
|
||||
slow: 0
|
||||
fast: 0
|
||||
variables: {}
|
||||
codeHighlight:
|
||||
startLine: 2
|
||||
endLine: 2
|
||||
|
||||
- id: step-2
|
||||
description: "Move slow 1 step (to 2), fast 2 steps (to 3)."
|
||||
structures:
|
||||
list:
|
||||
type: linkedlist
|
||||
nodes:
|
||||
- { value: 1, state: visited }
|
||||
- { value: 2, state: active }
|
||||
- { value: 3, state: active }
|
||||
- { value: 4, state: default }
|
||||
- { value: 5, state: default }
|
||||
pointers:
|
||||
slow: 1
|
||||
fast: 2
|
||||
variables: {}
|
||||
codeHighlight:
|
||||
startLine: 5
|
||||
endLine: 6
|
||||
|
||||
- id: step-3
|
||||
description: "Move slow 1 step (to 3), fast 2 steps (to 5)."
|
||||
structures:
|
||||
list:
|
||||
type: linkedlist
|
||||
nodes:
|
||||
- { value: 1, state: visited }
|
||||
- { value: 2, state: visited }
|
||||
- { value: 3, state: active }
|
||||
- { value: 4, state: visited }
|
||||
- { value: 5, state: active }
|
||||
pointers:
|
||||
slow: 2
|
||||
fast: 4
|
||||
variables: {}
|
||||
codeHighlight:
|
||||
startLine: 5
|
||||
endLine: 6
|
||||
|
||||
- id: step-4
|
||||
description: "fast.next is null, loop ends. slow is at middle (node 3)."
|
||||
structures:
|
||||
list:
|
||||
type: linkedlist
|
||||
nodes:
|
||||
- { value: 1, state: visited }
|
||||
- { value: 2, state: visited }
|
||||
- { value: 3, state: found }
|
||||
- { value: 4, state: visited }
|
||||
- { value: 5, state: visited }
|
||||
pointers:
|
||||
slow: 2
|
||||
variables:
|
||||
result: 3
|
||||
codeHighlight:
|
||||
startLine: 8
|
||||
endLine: 8
|
||||
|
||||
- id: detect-cycle
|
||||
title: Detect Cycle in Linked List
|
||||
input:
|
||||
list: [1, 2, 3, 4, 5]
|
||||
cycle_at: 2
|
||||
code: |
|
||||
def has_cycle(head):
|
||||
slow = fast = head
|
||||
|
||||
while fast and fast.next:
|
||||
slow = slow.next
|
||||
fast = fast.next.next
|
||||
|
||||
if slow == fast:
|
||||
return True
|
||||
|
||||
return False
|
||||
steps:
|
||||
- id: step-1
|
||||
description: "List has a cycle: 5 points back to 3. Initialize pointers at head."
|
||||
structures:
|
||||
list:
|
||||
type: linkedlist
|
||||
nodes:
|
||||
- { value: 1, state: active }
|
||||
- { value: 2, state: default }
|
||||
- { value: 3, state: default, annotations: ["cycle start"] }
|
||||
- { value: 4, state: default }
|
||||
- { value: 5, state: default, annotations: ["→3"] }
|
||||
pointers:
|
||||
slow: 0
|
||||
fast: 0
|
||||
variables: {}
|
||||
codeHighlight:
|
||||
startLine: 2
|
||||
endLine: 2
|
||||
|
||||
- id: step-2
|
||||
description: "slow moves to 2, fast moves to 3."
|
||||
structures:
|
||||
list:
|
||||
type: linkedlist
|
||||
nodes:
|
||||
- { value: 1, state: visited }
|
||||
- { value: 2, state: active }
|
||||
- { value: 3, state: active, annotations: ["cycle start"] }
|
||||
- { value: 4, state: default }
|
||||
- { value: 5, state: default, annotations: ["→3"] }
|
||||
pointers:
|
||||
slow: 1
|
||||
fast: 2
|
||||
variables: {}
|
||||
codeHighlight:
|
||||
startLine: 5
|
||||
endLine: 6
|
||||
|
||||
- id: step-3
|
||||
description: "slow moves to 3, fast moves to 5."
|
||||
structures:
|
||||
list:
|
||||
type: linkedlist
|
||||
nodes:
|
||||
- { value: 1, state: visited }
|
||||
- { value: 2, state: visited }
|
||||
- { value: 3, state: active, annotations: ["cycle start"] }
|
||||
- { value: 4, state: visited }
|
||||
- { value: 5, state: active, annotations: ["→3"] }
|
||||
pointers:
|
||||
slow: 2
|
||||
fast: 4
|
||||
variables: {}
|
||||
codeHighlight:
|
||||
startLine: 5
|
||||
endLine: 6
|
||||
|
||||
- id: step-4
|
||||
description: "slow moves to 4, fast loops back to 4 (via 5→3→4). They meet!"
|
||||
structures:
|
||||
list:
|
||||
type: linkedlist
|
||||
nodes:
|
||||
- { value: 1, state: visited }
|
||||
- { value: 2, state: visited }
|
||||
- { value: 3, state: visited, annotations: ["cycle start"] }
|
||||
- { value: 4, state: found, annotations: ["meeting point"] }
|
||||
- { value: 5, state: visited, annotations: ["→3"] }
|
||||
pointers:
|
||||
"slow, fast": 3
|
||||
variables:
|
||||
cycle_detected: true
|
||||
codeHighlight:
|
||||
startLine: 8
|
||||
endLine: 9
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Greedy
|
||||
slug: greedy
|
||||
difficulty_level: 3
|
||||
pattern_type: technique
|
||||
display_order: 12
|
||||
|
||||
description: >
|
||||
Make locally optimal choices at each step, hoping to find a global optimum.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Heap / Priority Queue
|
||||
slug: heap
|
||||
difficulty_level: 3
|
||||
pattern_type: data_structure
|
||||
display_order: 16
|
||||
|
||||
description: >
|
||||
A data structure that efficiently maintains the minimum or maximum element,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Overlapping Intervals
|
||||
slug: intervals
|
||||
difficulty_level: 2
|
||||
pattern_type: technique
|
||||
display_order: 13
|
||||
|
||||
description: >
|
||||
Process and manipulate intervals (ranges) that may share common regions.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: LinkedList In-Place Reversal
|
||||
slug: linkedlist-reversal
|
||||
difficulty_level: 2
|
||||
pattern_type: technique
|
||||
display_order: 14
|
||||
|
||||
description: >
|
||||
Reverse linked list nodes in-place by manipulating pointers without allocating
|
||||
@@ -277,3 +279,130 @@ related_patterns:
|
||||
- two-pointers
|
||||
|
||||
prerequisite_patterns: []
|
||||
|
||||
visualization_examples:
|
||||
- id: reverse-linked-list
|
||||
title: Reverse Entire Linked List
|
||||
input:
|
||||
list: [1, 2, 3, 4]
|
||||
code: |
|
||||
def reverse_list(head):
|
||||
prev = None
|
||||
curr = head
|
||||
|
||||
while curr:
|
||||
next_node = curr.next
|
||||
curr.next = prev
|
||||
prev = curr
|
||||
curr = next_node
|
||||
|
||||
return prev
|
||||
steps:
|
||||
- id: step-1
|
||||
description: "Initialize prev=None, curr=head (node 1)."
|
||||
structures:
|
||||
list:
|
||||
type: linkedlist
|
||||
nodes:
|
||||
- { value: 1, state: active }
|
||||
- { value: 2, state: default }
|
||||
- { value: 3, state: default }
|
||||
- { value: 4, state: default }
|
||||
pointers:
|
||||
curr: 0
|
||||
variables:
|
||||
prev: "null"
|
||||
codeHighlight:
|
||||
startLine: 2
|
||||
endLine: 3
|
||||
|
||||
- id: step-2
|
||||
description: "Save next=2, reverse link: 1→null. Move pointers forward."
|
||||
structures:
|
||||
list:
|
||||
type: linkedlist
|
||||
nodes:
|
||||
- { value: 1, state: visited, annotations: ["→null"] }
|
||||
- { value: 2, state: active }
|
||||
- { value: 3, state: default }
|
||||
- { value: 4, state: default }
|
||||
pointers:
|
||||
prev: 0
|
||||
curr: 1
|
||||
variables:
|
||||
next_node: 2
|
||||
codeHighlight:
|
||||
startLine: 6
|
||||
endLine: 9
|
||||
|
||||
- id: step-3
|
||||
description: "Save next=3, reverse link: 2→1. Move pointers forward."
|
||||
structures:
|
||||
list:
|
||||
type: linkedlist
|
||||
nodes:
|
||||
- { value: 1, state: visited, annotations: ["→null"] }
|
||||
- { value: 2, state: visited, annotations: ["→1"] }
|
||||
- { value: 3, state: active }
|
||||
- { value: 4, state: default }
|
||||
pointers:
|
||||
prev: 1
|
||||
curr: 2
|
||||
variables:
|
||||
next_node: 3
|
||||
codeHighlight:
|
||||
startLine: 6
|
||||
endLine: 9
|
||||
|
||||
- id: step-4
|
||||
description: "Save next=4, reverse link: 3→2. Move pointers forward."
|
||||
structures:
|
||||
list:
|
||||
type: linkedlist
|
||||
nodes:
|
||||
- { value: 1, state: visited, annotations: ["→null"] }
|
||||
- { value: 2, state: visited, annotations: ["→1"] }
|
||||
- { value: 3, state: visited, annotations: ["→2"] }
|
||||
- { value: 4, state: active }
|
||||
pointers:
|
||||
prev: 2
|
||||
curr: 3
|
||||
variables:
|
||||
next_node: 4
|
||||
codeHighlight:
|
||||
startLine: 6
|
||||
endLine: 9
|
||||
|
||||
- id: step-5
|
||||
description: "Save next=null, reverse link: 4→3. curr becomes null, loop ends."
|
||||
structures:
|
||||
list:
|
||||
type: linkedlist
|
||||
nodes:
|
||||
- { value: 1, state: visited, annotations: ["→null"] }
|
||||
- { value: 2, state: visited, annotations: ["→1"] }
|
||||
- { value: 3, state: visited, annotations: ["→2"] }
|
||||
- { value: 4, state: found, annotations: ["→3", "new head"] }
|
||||
pointers:
|
||||
prev: 3
|
||||
variables:
|
||||
next_node: "null"
|
||||
codeHighlight:
|
||||
startLine: 11
|
||||
endLine: 11
|
||||
|
||||
- id: step-6
|
||||
description: "Return prev (node 4). Reversed list: 4→3→2→1→null"
|
||||
structures:
|
||||
reversed:
|
||||
type: linkedlist
|
||||
nodes:
|
||||
- { value: 4, state: found, annotations: ["head"] }
|
||||
- { value: 3, state: default }
|
||||
- { value: 2, state: default }
|
||||
- { value: 1, state: default }
|
||||
variables:
|
||||
result: "4→3→2→1→null"
|
||||
codeHighlight:
|
||||
startLine: 11
|
||||
endLine: 11
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Matrix Traversal
|
||||
slug: matrix-traversal
|
||||
difficulty_level: 3
|
||||
pattern_type: algorithm
|
||||
display_order: 9
|
||||
|
||||
description: >
|
||||
Navigate 2D grids systematically using DFS, BFS, or directional iteration.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Monotonic Stack
|
||||
slug: monotonic-stack
|
||||
difficulty_level: 3
|
||||
pattern_type: data_structure
|
||||
display_order: 15
|
||||
|
||||
description: >
|
||||
Maintain a stack where elements are always in sorted order (either increasing or
|
||||
@@ -267,3 +269,245 @@ related_patterns:
|
||||
- sliding-window
|
||||
|
||||
prerequisite_patterns: []
|
||||
|
||||
visualization_examples:
|
||||
- id: largest-rectangle-histogram
|
||||
title: "Largest Rectangle in Histogram"
|
||||
input:
|
||||
heights: [2, 1, 5, 6, 2, 3]
|
||||
code: |
|
||||
def largest_rectangle(heights):
|
||||
stack = [] # (index, height)
|
||||
max_area = 0
|
||||
|
||||
for i, h in enumerate(heights):
|
||||
start = i
|
||||
while stack and stack[-1][1] > h:
|
||||
idx, height = stack.pop()
|
||||
max_area = max(max_area, height * (i - idx))
|
||||
start = idx
|
||||
stack.append((start, h))
|
||||
|
||||
for idx, height in stack:
|
||||
max_area = max(max_area, height * (len(heights) - idx))
|
||||
|
||||
return max_area
|
||||
steps:
|
||||
- id: step-1
|
||||
description: "Goal: Find the largest rectangle that fits in this histogram. Each bar's height is shown, and we want to find the biggest rectangular area."
|
||||
structures:
|
||||
heights:
|
||||
type: histogram
|
||||
bars:
|
||||
- { value: 2, state: default }
|
||||
- { value: 1, state: default }
|
||||
- { value: 5, state: default }
|
||||
- { value: 6, state: default }
|
||||
- { value: 2, state: default }
|
||||
- { value: 3, state: default }
|
||||
variables:
|
||||
question: "What's the largest rectangle?"
|
||||
max_area: 0
|
||||
|
||||
- id: step-2
|
||||
description: "We scan left to right. Bar 0 (height 2) could be the shortest bar in some rectangle. Push it to stack. Format: h:height @start_index."
|
||||
structures:
|
||||
heights:
|
||||
type: histogram
|
||||
bars:
|
||||
- { value: 2, state: active }
|
||||
- { value: 1, state: default }
|
||||
- { value: 5, state: default }
|
||||
- { value: 6, state: default }
|
||||
- { value: 2, state: default }
|
||||
- { value: 3, state: default }
|
||||
pointers:
|
||||
i: 0
|
||||
stack (h:height @start):
|
||||
type: stack
|
||||
values:
|
||||
- { value: "h:2 @0", state: active }
|
||||
variables:
|
||||
max_area: 0
|
||||
|
||||
- id: step-3
|
||||
description: "Bar 1 (height 1) is SHORTER than bar 0 (height 2). Bar 0's rectangle can't extend further right! Pop it and calculate: width=1, area=2x1=2."
|
||||
structures:
|
||||
heights:
|
||||
type: histogram
|
||||
bars:
|
||||
- { value: 2, state: comparing }
|
||||
- { value: 1, state: active }
|
||||
- { value: 5, state: default }
|
||||
- { value: 6, state: default }
|
||||
- { value: 2, state: default }
|
||||
- { value: 3, state: default }
|
||||
pointers:
|
||||
i: 1
|
||||
rectangle:
|
||||
startIndex: 0
|
||||
endIndex: 0
|
||||
height: 2
|
||||
state: comparing
|
||||
label: "Area: 2"
|
||||
stack (h:height @start):
|
||||
type: stack
|
||||
values: []
|
||||
variables:
|
||||
max_area: 2
|
||||
calculation: "height(2) × width(1) = 2"
|
||||
|
||||
- id: step-4
|
||||
description: "Now push bar 1 (height 1). Since bar 0 was popped, bar 1's rectangle can extend back to index 0. Stack stores (0, 1)."
|
||||
structures:
|
||||
heights:
|
||||
type: histogram
|
||||
bars:
|
||||
- { value: 2, state: visited }
|
||||
- { value: 1, state: active }
|
||||
- { value: 5, state: default }
|
||||
- { value: 6, state: default }
|
||||
- { value: 2, state: default }
|
||||
- { value: 3, state: default }
|
||||
pointers:
|
||||
i: 1
|
||||
maxArea: 2
|
||||
stack (h:height @start):
|
||||
type: stack
|
||||
values:
|
||||
- { value: "h:1 @0", state: active }
|
||||
variables:
|
||||
max_area: 2
|
||||
insight: "Height 1 can extend back to index 0!"
|
||||
|
||||
- id: step-5
|
||||
description: "Bar 2 (height 5) is taller than bar 1. Taller bars might extend further right, so push it. Bar 3 (height 6) is also taller — push it too."
|
||||
structures:
|
||||
heights:
|
||||
type: histogram
|
||||
bars:
|
||||
- { value: 2, state: visited }
|
||||
- { value: 1, state: default }
|
||||
- { value: 5, state: active }
|
||||
- { value: 6, state: active }
|
||||
- { value: 2, state: default }
|
||||
- { value: 3, state: default }
|
||||
pointers:
|
||||
i: 3
|
||||
maxArea: 2
|
||||
stack (h:height @start):
|
||||
type: stack
|
||||
values:
|
||||
- { value: "h:6 @3", state: active }
|
||||
- { value: "h:5 @2", state: active }
|
||||
- { value: "h:1 @0", state: default }
|
||||
variables:
|
||||
max_area: 2
|
||||
stack_note: "Stack keeps increasing heights (bottom to top)"
|
||||
|
||||
- id: step-6
|
||||
description: "Bar 4 (height 2) is SHORTER than bar 3 (height 6). Pop bar 3: width=1, area=6x1=6. Still shorter than bar 2 (height 5), pop it: width=2, area=5x2=10!"
|
||||
structures:
|
||||
heights:
|
||||
type: histogram
|
||||
bars:
|
||||
- { value: 2, state: visited }
|
||||
- { value: 1, state: default }
|
||||
- { value: 5, state: comparing }
|
||||
- { value: 6, state: comparing }
|
||||
- { value: 2, state: active }
|
||||
- { value: 3, state: default }
|
||||
pointers:
|
||||
i: 4
|
||||
rectangle:
|
||||
startIndex: 2
|
||||
endIndex: 3
|
||||
height: 5
|
||||
state: found
|
||||
label: "Area: 10"
|
||||
maxArea: 10
|
||||
stack (h:height @start):
|
||||
type: stack
|
||||
values:
|
||||
- { value: "h:1 @0", state: default }
|
||||
variables:
|
||||
max_area: 10
|
||||
calculation: "height(5) × width(2) = 10"
|
||||
|
||||
- id: step-7
|
||||
description: "Push bar 4 at index 2 (it extends back to where bar 2 was). Bar 5 (height 3) is taller, so push it too."
|
||||
structures:
|
||||
heights:
|
||||
type: histogram
|
||||
bars:
|
||||
- { value: 2, state: visited }
|
||||
- { value: 1, state: default }
|
||||
- { value: 5, state: visited }
|
||||
- { value: 6, state: visited }
|
||||
- { value: 2, state: active }
|
||||
- { value: 3, state: active }
|
||||
pointers:
|
||||
i: 5
|
||||
maxArea: 10
|
||||
stack (h:height @start):
|
||||
type: stack
|
||||
values:
|
||||
- { value: "h:3 @5", state: active }
|
||||
- { value: "h:2 @2", state: active }
|
||||
- { value: "h:1 @0", state: default }
|
||||
variables:
|
||||
max_area: 10
|
||||
note: "h:2 starts @2 because it extends back"
|
||||
|
||||
- id: step-8
|
||||
description: "Done scanning! Process remaining stack: bar 5 (height 3) extends from index 5 to end (width 1), area=3. Bar 4 (height 2) extends from 2 to end (width 4), area=8."
|
||||
structures:
|
||||
heights:
|
||||
type: histogram
|
||||
bars:
|
||||
- { value: 2, state: visited }
|
||||
- { value: 1, state: visited }
|
||||
- { value: 5, state: visited }
|
||||
- { value: 6, state: visited }
|
||||
- { value: 2, state: comparing }
|
||||
- { value: 3, state: comparing }
|
||||
rectangle:
|
||||
startIndex: 2
|
||||
endIndex: 5
|
||||
height: 2
|
||||
state: comparing
|
||||
label: "Area: 8"
|
||||
maxArea: 10
|
||||
stack (h:height @start):
|
||||
type: stack
|
||||
values:
|
||||
- { value: "h:1 @0", state: default }
|
||||
variables:
|
||||
max_area: 10
|
||||
remaining: "Still processing stack..."
|
||||
|
||||
- id: step-9
|
||||
description: "Bar 1 (height 1) extends from index 0 to end (width 6), area=6. The largest rectangle has area 10 — the one spanning bars 2-3 with height 5!"
|
||||
structures:
|
||||
heights:
|
||||
type: histogram
|
||||
bars:
|
||||
- { value: 2, state: visited }
|
||||
- { value: 1, state: visited }
|
||||
- { value: 5, state: found }
|
||||
- { value: 6, state: found }
|
||||
- { value: 2, state: visited }
|
||||
- { value: 3, state: visited }
|
||||
rectangle:
|
||||
startIndex: 2
|
||||
endIndex: 3
|
||||
height: 5
|
||||
state: found
|
||||
label: "Max: 10"
|
||||
maxArea: 10
|
||||
stack (h:height @start):
|
||||
type: stack
|
||||
values: []
|
||||
variables:
|
||||
max_area: 10
|
||||
answer: "Largest rectangle = 10"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Prefix Sum
|
||||
slug: prefix-sum
|
||||
difficulty_level: 2
|
||||
pattern_type: technique
|
||||
display_order: 5
|
||||
|
||||
description: >
|
||||
Precompute cumulative sums to answer range sum queries in O(1) time. This
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Sliding Window
|
||||
slug: sliding-window
|
||||
difficulty_level: 2
|
||||
pattern_type: algorithm
|
||||
display_order: 2
|
||||
|
||||
description: >
|
||||
Maintain a window of elements that slides through the data, tracking a
|
||||
@@ -192,3 +194,129 @@ related_patterns:
|
||||
|
||||
prerequisite_patterns:
|
||||
- two-pointers
|
||||
|
||||
visualization_examples:
|
||||
- id: max-sum-subarray
|
||||
title: Maximum Sum Subarray of Size K
|
||||
input:
|
||||
nums: [2, 1, 5, 1, 3, 2]
|
||||
k: 3
|
||||
code: |
|
||||
def max_sum_subarray(nums, k):
|
||||
window_sum = sum(nums[:k])
|
||||
max_sum = window_sum
|
||||
|
||||
for i in range(k, len(nums)):
|
||||
window_sum += nums[i] - nums[i - k]
|
||||
max_sum = max(max_sum, window_sum)
|
||||
|
||||
return max_sum
|
||||
steps:
|
||||
- id: step-1
|
||||
description: "Initialize window with first k=3 elements: [2, 1, 5]. Sum = 8"
|
||||
structures:
|
||||
nums:
|
||||
type: array
|
||||
values:
|
||||
- { value: 2, state: active, annotations: ["window"] }
|
||||
- { value: 1, state: active, annotations: ["window"] }
|
||||
- { value: 5, state: active, annotations: ["window"] }
|
||||
- { value: 1, state: default }
|
||||
- { value: 3, state: default }
|
||||
- { value: 2, state: default }
|
||||
pointers:
|
||||
left: 0
|
||||
right: 2
|
||||
variables:
|
||||
window_sum: 8
|
||||
max_sum: 8
|
||||
codeHighlight:
|
||||
startLine: 2
|
||||
endLine: 3
|
||||
|
||||
- id: step-2
|
||||
description: "Slide window: remove 2, add 1. New window [1, 5, 1]. Sum = 7"
|
||||
structures:
|
||||
nums:
|
||||
type: array
|
||||
values:
|
||||
- { value: 2, state: visited }
|
||||
- { value: 1, state: active, annotations: ["window"] }
|
||||
- { value: 5, state: active, annotations: ["window"] }
|
||||
- { value: 1, state: active, annotations: ["window"] }
|
||||
- { value: 3, state: default }
|
||||
- { value: 2, state: default }
|
||||
pointers:
|
||||
left: 1
|
||||
right: 3
|
||||
variables:
|
||||
window_sum: 7
|
||||
max_sum: 8
|
||||
i: 3
|
||||
codeHighlight:
|
||||
startLine: 5
|
||||
endLine: 7
|
||||
|
||||
- id: step-3
|
||||
description: "Slide window: remove 1, add 3. New window [5, 1, 3]. Sum = 9. New max!"
|
||||
structures:
|
||||
nums:
|
||||
type: array
|
||||
values:
|
||||
- { value: 2, state: visited }
|
||||
- { value: 1, state: visited }
|
||||
- { value: 5, state: found, annotations: ["window"] }
|
||||
- { value: 1, state: found, annotations: ["window"] }
|
||||
- { value: 3, state: found, annotations: ["window"] }
|
||||
- { value: 2, state: default }
|
||||
pointers:
|
||||
left: 2
|
||||
right: 4
|
||||
variables:
|
||||
window_sum: 9
|
||||
max_sum: 9
|
||||
i: 4
|
||||
codeHighlight:
|
||||
startLine: 5
|
||||
endLine: 7
|
||||
|
||||
- id: step-4
|
||||
description: "Slide window: remove 5, add 2. New window [1, 3, 2]. Sum = 6"
|
||||
structures:
|
||||
nums:
|
||||
type: array
|
||||
values:
|
||||
- { value: 2, state: visited }
|
||||
- { value: 1, state: visited }
|
||||
- { value: 5, state: visited }
|
||||
- { value: 1, state: active, annotations: ["window"] }
|
||||
- { value: 3, state: active, annotations: ["window"] }
|
||||
- { value: 2, state: active, annotations: ["window"] }
|
||||
pointers:
|
||||
left: 3
|
||||
right: 5
|
||||
variables:
|
||||
window_sum: 6
|
||||
max_sum: 9
|
||||
i: 5
|
||||
codeHighlight:
|
||||
startLine: 5
|
||||
endLine: 7
|
||||
|
||||
- id: step-5
|
||||
description: "Loop complete. Maximum sum found is 9 (window [5, 1, 3])"
|
||||
structures:
|
||||
nums:
|
||||
type: array
|
||||
values:
|
||||
- { value: 2, state: visited }
|
||||
- { value: 1, state: visited }
|
||||
- { value: 5, state: found }
|
||||
- { value: 1, state: found }
|
||||
- { value: 3, state: found }
|
||||
- { value: 2, state: visited }
|
||||
variables:
|
||||
max_sum: 9
|
||||
codeHighlight:
|
||||
startLine: 9
|
||||
endLine: 9
|
||||
|
||||
104
backend/data/patterns/topological-sort.yaml
Normal file
104
backend/data/patterns/topological-sort.yaml
Normal file
@@ -0,0 +1,104 @@
|
||||
name: Topological Sort
|
||||
slug: topological-sort
|
||||
difficulty_level: 3
|
||||
pattern_type: algorithm
|
||||
display_order: 22
|
||||
|
||||
description: >
|
||||
Order vertices in a directed acyclic graph (DAG) such that for every
|
||||
edge u -> v, vertex u comes before v in the ordering.
|
||||
|
||||
when_to_use: |
|
||||
- Course prerequisites / task dependencies
|
||||
- Build order / compilation order
|
||||
- Detecting cycles in directed graphs
|
||||
- Any problem with "do A before B" constraints
|
||||
|
||||
metaphor: |
|
||||
Imagine getting dressed: you must put on underwear before pants, socks
|
||||
before shoes. Topological sort finds a valid order that respects all
|
||||
"must come before" rules. If there's a cycle (shirt requires jacket,
|
||||
jacket requires shirt), no valid order exists.
|
||||
|
||||
core_concept: |
|
||||
Two main approaches:
|
||||
|
||||
**Kahn's Algorithm (BFS):** Start with nodes having no incoming edges
|
||||
(in-degree 0). Process them, remove their edges, repeat. If all nodes
|
||||
processed, valid order exists.
|
||||
|
||||
**DFS-based:** Do DFS, add nodes to result when backtracking (post-order).
|
||||
Reverse at end. Cycle exists if we revisit a node in current path.
|
||||
|
||||
code_template: |
|
||||
from collections import deque
|
||||
|
||||
def topological_sort_bfs(n: int, edges: list[tuple[int, int]]) -> list[int]:
|
||||
"""Kahn's algorithm - returns empty list if cycle exists."""
|
||||
# Build adjacency list and in-degree count
|
||||
graph = [[] for _ in range(n)]
|
||||
in_degree = [0] * n
|
||||
|
||||
for u, v in edges: # u -> v (u must come before v)
|
||||
graph[u].append(v)
|
||||
in_degree[v] += 1
|
||||
|
||||
# Start with nodes having no prerequisites
|
||||
queue = deque([i for i in range(n) if in_degree[i] == 0])
|
||||
result = []
|
||||
|
||||
while queue:
|
||||
node = queue.popleft()
|
||||
result.append(node)
|
||||
|
||||
for neighbor in graph[node]:
|
||||
in_degree[neighbor] -= 1
|
||||
if in_degree[neighbor] == 0:
|
||||
queue.append(neighbor)
|
||||
|
||||
# If we processed all nodes, valid topological order exists
|
||||
return result if len(result) == n else []
|
||||
|
||||
recognition_signals:
|
||||
- "prerequisites"
|
||||
- "dependencies"
|
||||
- "ordering tasks"
|
||||
- "course schedule"
|
||||
- "build order"
|
||||
- "detect cycle in directed graph"
|
||||
- "do X before Y"
|
||||
|
||||
common_mistakes:
|
||||
- title: Forgetting Cycle Detection
|
||||
description: |
|
||||
Assuming input is always a valid DAG. Must check if all nodes were
|
||||
processed (BFS) or if back-edge exists (DFS).
|
||||
fix: |
|
||||
BFS: Check `len(result) == n`. DFS: Track "visiting" state separately
|
||||
from "visited."
|
||||
|
||||
- title: Wrong Edge Direction
|
||||
description: |
|
||||
Confusing "A depends on B" vs "A must come before B." These are
|
||||
opposite edge directions.
|
||||
fix: |
|
||||
Clarify: If A depends on B, edge is B -> A (B comes before A).
|
||||
|
||||
variations:
|
||||
- name: Course Schedule (cycle detection)
|
||||
description: Return true/false if valid ordering exists
|
||||
example: "course-schedule"
|
||||
- name: Course Schedule II (find order)
|
||||
description: Return the actual ordering
|
||||
example: "course-schedule-ii"
|
||||
- name: Alien Dictionary
|
||||
description: Infer ordering from sorted alien words
|
||||
example: "alien-dictionary"
|
||||
|
||||
related_patterns:
|
||||
- bfs
|
||||
- dfs
|
||||
|
||||
prerequisite_patterns:
|
||||
- bfs
|
||||
- dfs
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Binary Tree Traversal
|
||||
slug: tree-traversal
|
||||
difficulty_level: 2
|
||||
pattern_type: algorithm
|
||||
display_order: 8
|
||||
|
||||
description: >
|
||||
Visit all nodes in a binary tree in specific orders: preorder (root-left-right),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Trie
|
||||
slug: trie
|
||||
difficulty_level: 3
|
||||
pattern_type: data_structure
|
||||
display_order: 17
|
||||
|
||||
description: >
|
||||
A tree-like data structure for efficient string prefix operations. Each node
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Two Pointers
|
||||
slug: two-pointers
|
||||
difficulty_level: 2
|
||||
pattern_type: algorithm
|
||||
display_order: 1
|
||||
|
||||
description: >
|
||||
Use two pointers to traverse data from different positions, often moving
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Union Find
|
||||
slug: union-find
|
||||
difficulty_level: 3
|
||||
pattern_type: data_structure
|
||||
display_order: 18
|
||||
|
||||
description: >
|
||||
Track disjoint sets with efficient union and find operations. Union-Find
|
||||
|
||||
@@ -7,6 +7,7 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
@@ -16,6 +17,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from src.db.database import async_session_factory
|
||||
from src.models import Category, Difficulty, Explanation, Pattern, Question, Solution
|
||||
from src.models.question import QuestionPattern
|
||||
|
||||
|
||||
async def load_categories(session: AsyncSession, data_dir: Path) -> dict[str, Category]:
|
||||
@@ -133,6 +135,10 @@ async def _upsert_pattern(session: AsyncSession, item: dict[str, Any]) -> Patter
|
||||
# Interactive visualization examples
|
||||
pattern.visualization_examples = item.get("visualization_examples")
|
||||
|
||||
# Pattern classification
|
||||
pattern.pattern_type = item.get("pattern_type")
|
||||
pattern.display_order = item.get("display_order")
|
||||
|
||||
return pattern
|
||||
|
||||
|
||||
@@ -190,10 +196,31 @@ async def load_question(
|
||||
categories[cat_slug] for cat_slug in data.get("categories", []) if cat_slug in categories
|
||||
]
|
||||
|
||||
# Link patterns
|
||||
question.patterns = [
|
||||
patterns[pat_slug] for pat_slug in data.get("patterns", []) if pat_slug in patterns
|
||||
]
|
||||
# Clear existing pattern links to handle is_optimal changes
|
||||
await session.execute(
|
||||
sa.delete(QuestionPattern).where(QuestionPattern.question_id == question.id)
|
||||
)
|
||||
await session.flush()
|
||||
|
||||
# Link patterns with is_optimal support
|
||||
for pat_entry in data.get("patterns", []):
|
||||
# Support both formats:
|
||||
# Old: "heap" (string)
|
||||
# New: {slug: "heap", is_optimal: true} (dict)
|
||||
if isinstance(pat_entry, str):
|
||||
pat_slug = pat_entry
|
||||
is_optimal = False
|
||||
else:
|
||||
pat_slug = pat_entry["slug"]
|
||||
is_optimal = pat_entry.get("is_optimal", False)
|
||||
|
||||
if pat_slug in patterns:
|
||||
link = QuestionPattern(
|
||||
question_id=question.id,
|
||||
pattern_id=patterns[pat_slug].id,
|
||||
is_optimal=is_optimal,
|
||||
)
|
||||
session.add(link)
|
||||
|
||||
await session.flush()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user