From 13bab6361890bdb8fb8b3b7b8e44368fe54c52ea Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Mon, 8 Sep 2025 16:03:14 +0100 Subject: [PATCH] feat(patterns): pattern taxonomy + is_optimal --- .../alembic/versions/006_pattern_taxonomy.py | 35 +++ backend/data/patterns/backtracking.yaml | 2 + backend/data/patterns/bfs.yaml | 2 + backend/data/patterns/binary-search.yaml | 171 ++++++++++++ backend/data/patterns/bit-manipulation.yaml | 2 + backend/data/patterns/counting-sort.yaml | 92 +++++++ backend/data/patterns/cyclic-sort.yaml | 96 +++++++ backend/data/patterns/dfs.yaml | 2 + backend/data/patterns/divide-and-conquer.yaml | 97 +++++++ .../data/patterns/dynamic-programming.yaml | 2 + backend/data/patterns/fast-slow-pointers.yaml | 187 ++++++++++++++ backend/data/patterns/greedy.yaml | 2 + backend/data/patterns/heap.yaml | 2 + backend/data/patterns/intervals.yaml | 2 + .../data/patterns/linkedlist-reversal.yaml | 129 +++++++++ backend/data/patterns/matrix-traversal.yaml | 2 + backend/data/patterns/monotonic-stack.yaml | 244 ++++++++++++++++++ backend/data/patterns/prefix-sum.yaml | 2 + backend/data/patterns/sliding-window.yaml | 128 +++++++++ backend/data/patterns/topological-sort.yaml | 104 ++++++++ backend/data/patterns/tree-traversal.yaml | 2 + backend/data/patterns/trie.yaml | 2 + backend/data/patterns/two-pointers.yaml | 2 + backend/data/patterns/union-find.yaml | 2 + backend/scripts/load_data.py | 35 ++- frontend/src/app/patterns/page.tsx | 110 ++++++-- .../components/questions/question-card.tsx | 1 + frontend/src/types/index.ts | 3 + 28 files changed, 1434 insertions(+), 26 deletions(-) create mode 100644 backend/alembic/versions/006_pattern_taxonomy.py create mode 100644 backend/data/patterns/counting-sort.yaml create mode 100644 backend/data/patterns/cyclic-sort.yaml create mode 100644 backend/data/patterns/divide-and-conquer.yaml create mode 100644 backend/data/patterns/topological-sort.yaml diff --git a/backend/alembic/versions/006_pattern_taxonomy.py b/backend/alembic/versions/006_pattern_taxonomy.py new file mode 100644 index 0000000..d513090 --- /dev/null +++ b/backend/alembic/versions/006_pattern_taxonomy.py @@ -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") diff --git a/backend/data/patterns/backtracking.yaml b/backend/data/patterns/backtracking.yaml index 140836d..b2c198d 100644 --- a/backend/data/patterns/backtracking.yaml +++ b/backend/data/patterns/backtracking.yaml @@ -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 diff --git a/backend/data/patterns/bfs.yaml b/backend/data/patterns/bfs.yaml index b8c3055..d10ba93 100644 --- a/backend/data/patterns/bfs.yaml +++ b/backend/data/patterns/bfs.yaml @@ -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 diff --git a/backend/data/patterns/binary-search.yaml b/backend/data/patterns/binary-search.yaml index 908cf06..0dd0939 100644 --- a/backend/data/patterns/binary-search.yaml +++ b/backend/data/patterns/binary-search.yaml @@ -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 diff --git a/backend/data/patterns/bit-manipulation.yaml b/backend/data/patterns/bit-manipulation.yaml index afe8ec0..701e3e4 100644 --- a/backend/data/patterns/bit-manipulation.yaml +++ b/backend/data/patterns/bit-manipulation.yaml @@ -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 diff --git a/backend/data/patterns/counting-sort.yaml b/backend/data/patterns/counting-sort.yaml new file mode 100644 index 0000000..03c7fd8 --- /dev/null +++ b/backend/data/patterns/counting-sort.yaml @@ -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: [] diff --git a/backend/data/patterns/cyclic-sort.yaml b/backend/data/patterns/cyclic-sort.yaml new file mode 100644 index 0000000..6b89aca --- /dev/null +++ b/backend/data/patterns/cyclic-sort.yaml @@ -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: [] diff --git a/backend/data/patterns/dfs.yaml b/backend/data/patterns/dfs.yaml index dd94413..66af7ac 100644 --- a/backend/data/patterns/dfs.yaml +++ b/backend/data/patterns/dfs.yaml @@ -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 diff --git a/backend/data/patterns/divide-and-conquer.yaml b/backend/data/patterns/divide-and-conquer.yaml new file mode 100644 index 0000000..58c22b5 --- /dev/null +++ b/backend/data/patterns/divide-and-conquer.yaml @@ -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 diff --git a/backend/data/patterns/dynamic-programming.yaml b/backend/data/patterns/dynamic-programming.yaml index 094a747..6e8fbcc 100644 --- a/backend/data/patterns/dynamic-programming.yaml +++ b/backend/data/patterns/dynamic-programming.yaml @@ -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 diff --git a/backend/data/patterns/fast-slow-pointers.yaml b/backend/data/patterns/fast-slow-pointers.yaml index 4649fb5..e2c3669 100644 --- a/backend/data/patterns/fast-slow-pointers.yaml +++ b/backend/data/patterns/fast-slow-pointers.yaml @@ -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 diff --git a/backend/data/patterns/greedy.yaml b/backend/data/patterns/greedy.yaml index 3680638..ecedf47 100644 --- a/backend/data/patterns/greedy.yaml +++ b/backend/data/patterns/greedy.yaml @@ -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. diff --git a/backend/data/patterns/heap.yaml b/backend/data/patterns/heap.yaml index 2356dc0..aac133d 100644 --- a/backend/data/patterns/heap.yaml +++ b/backend/data/patterns/heap.yaml @@ -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, diff --git a/backend/data/patterns/intervals.yaml b/backend/data/patterns/intervals.yaml index d593a65..eebd010 100644 --- a/backend/data/patterns/intervals.yaml +++ b/backend/data/patterns/intervals.yaml @@ -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. diff --git a/backend/data/patterns/linkedlist-reversal.yaml b/backend/data/patterns/linkedlist-reversal.yaml index 941b114..c2438db 100644 --- a/backend/data/patterns/linkedlist-reversal.yaml +++ b/backend/data/patterns/linkedlist-reversal.yaml @@ -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 diff --git a/backend/data/patterns/matrix-traversal.yaml b/backend/data/patterns/matrix-traversal.yaml index 039ae4a..a994a0e 100644 --- a/backend/data/patterns/matrix-traversal.yaml +++ b/backend/data/patterns/matrix-traversal.yaml @@ -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. diff --git a/backend/data/patterns/monotonic-stack.yaml b/backend/data/patterns/monotonic-stack.yaml index 7baec82..7aee146 100644 --- a/backend/data/patterns/monotonic-stack.yaml +++ b/backend/data/patterns/monotonic-stack.yaml @@ -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" diff --git a/backend/data/patterns/prefix-sum.yaml b/backend/data/patterns/prefix-sum.yaml index a0ee79b..cf0c101 100644 --- a/backend/data/patterns/prefix-sum.yaml +++ b/backend/data/patterns/prefix-sum.yaml @@ -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 diff --git a/backend/data/patterns/sliding-window.yaml b/backend/data/patterns/sliding-window.yaml index a4c69e7..f9cb9ea 100644 --- a/backend/data/patterns/sliding-window.yaml +++ b/backend/data/patterns/sliding-window.yaml @@ -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 diff --git a/backend/data/patterns/topological-sort.yaml b/backend/data/patterns/topological-sort.yaml new file mode 100644 index 0000000..97acc4d --- /dev/null +++ b/backend/data/patterns/topological-sort.yaml @@ -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 diff --git a/backend/data/patterns/tree-traversal.yaml b/backend/data/patterns/tree-traversal.yaml index 119103a..e226650 100644 --- a/backend/data/patterns/tree-traversal.yaml +++ b/backend/data/patterns/tree-traversal.yaml @@ -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), diff --git a/backend/data/patterns/trie.yaml b/backend/data/patterns/trie.yaml index 2cec517..34f5b57 100644 --- a/backend/data/patterns/trie.yaml +++ b/backend/data/patterns/trie.yaml @@ -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 diff --git a/backend/data/patterns/two-pointers.yaml b/backend/data/patterns/two-pointers.yaml index a631f2e..0aca1c0 100644 --- a/backend/data/patterns/two-pointers.yaml +++ b/backend/data/patterns/two-pointers.yaml @@ -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 diff --git a/backend/data/patterns/union-find.yaml b/backend/data/patterns/union-find.yaml index 4720029..78f2b4d 100644 --- a/backend/data/patterns/union-find.yaml +++ b/backend/data/patterns/union-find.yaml @@ -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 diff --git a/backend/scripts/load_data.py b/backend/scripts/load_data.py index 93dd4e0..62b81ac 100644 --- a/backend/scripts/load_data.py +++ b/backend/scripts/load_data.py @@ -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() diff --git a/frontend/src/app/patterns/page.tsx b/frontend/src/app/patterns/page.tsx index 874b762..884cfd5 100644 --- a/frontend/src/app/patterns/page.tsx +++ b/frontend/src/app/patterns/page.tsx @@ -1,10 +1,39 @@ import { getPatterns } from "@/lib/api"; import Link from "next/link"; +import type { Pattern } from "@/types"; export const dynamic = "force-dynamic"; +const PATTERN_TYPE_ORDER = ["algorithm", "technique", "data_structure"]; +const PATTERN_TYPE_LABELS: Record = { + algorithm: "Algorithmic Patterns", + technique: "Techniques", + data_structure: "Data Structure Patterns", +}; + +function PatternCard({ pattern }: { pattern: Pattern }) { + return ( + +
+

{pattern.name}

+ + {pattern.question_count} questions + +
+ {pattern.description && ( +

+ {pattern.description} +

+ )} + + ); +} + export default async function PatternsPage() { - let patterns; + let patterns: Pattern[]; try { const response = await getPatterns(); patterns = response.items; @@ -19,6 +48,29 @@ export default async function PatternsPage() { ); } + // Group patterns by type + const groupedPatterns = patterns.reduce( + (acc, pattern) => { + const type = pattern.pattern_type || "other"; + if (!acc[type]) acc[type] = []; + acc[type].push(pattern); + return acc; + }, + {} as Record + ); + + // Sort each group by display_order + for (const type of Object.keys(groupedPatterns)) { + groupedPatterns[type].sort( + (a, b) => (a.display_order ?? 999) - (b.display_order ?? 999) + ); + } + + // Check if we have typed patterns to display grouped + const hasTypedPatterns = PATTERN_TYPE_ORDER.some( + (type) => groupedPatterns[type]?.length > 0 + ); + return (

Algorithmic Patterns

@@ -27,27 +79,41 @@ export default async function PatternsPage() { interviews.

-
- {patterns.map((pattern) => ( - -
-

{pattern.name}

- - {pattern.question_count} questions - -
- {pattern.description && ( -

- {pattern.description} -

- )} - - ))} -
+ {hasTypedPatterns ? ( + <> + {PATTERN_TYPE_ORDER.map( + (type) => + groupedPatterns[type]?.length > 0 && ( +
+

+ {PATTERN_TYPE_LABELS[type]} +

+
+ {groupedPatterns[type].map((pattern) => ( + + ))} +
+
+ ) + )} + {groupedPatterns["other"]?.length > 0 && ( +
+

Other Patterns

+
+ {groupedPatterns["other"].map((pattern) => ( + + ))} +
+
+ )} + + ) : ( +
+ {patterns.map((pattern) => ( + + ))} +
+ )}
); } diff --git a/frontend/src/components/questions/question-card.tsx b/frontend/src/components/questions/question-card.tsx index 7f5e7de..084499a 100644 --- a/frontend/src/components/questions/question-card.tsx +++ b/frontend/src/components/questions/question-card.tsx @@ -39,6 +39,7 @@ export function QuestionCard({ question }: QuestionCardProps) { {question.patterns.map((p) => ( {p.name} + {p.is_optimal && } ))} {question.leetcode_id && ( diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 56a5a20..ab7fb2d 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -10,6 +10,7 @@ export interface PatternBrief { id: string; name: string; slug: string; + is_optimal?: boolean; } export interface Category extends CategoryBrief { @@ -21,6 +22,8 @@ export interface Pattern extends PatternBrief { description: string | null; when_to_use: string | null; question_count: number; + pattern_type?: string; + display_order?: number; } // Pattern Tutorial Types