diff --git a/.agents/skills/visualization/SKILL.md b/.agents/skills/visualization/SKILL.md index dae3c33..154e61c 100644 --- a/.agents/skills/visualization/SKILL.md +++ b/.agents/skills/visualization/SKILL.md @@ -29,12 +29,104 @@ When creating or updating the visualization, rigidly follow these rules: 7. **No Array Index Labels:** Do not add "idx" labels (or similar index markings) to array visual elements. Keep the array representations clean. 8. **Text Color:** Ensure the color of the text used in visualizations has high contrast with its background and respects dark mode by using theme-aware classes (e.g., `text-foreground`, `text-primary-foreground`). 9. **Top-Aligned Controls:** The `SimpleStepControls` component MUST be placed at the top of the visualization, above the main content grid, to ensure immediate user access to navigation. -10. **Use AnimatedCodeEditor:** Always use the `AnimatedCodeEditor` component when an implementation code block is required within the visualization. -11. **Minimal Code Editor Container:** Do NOT include a redundant `div` wrapper, "Implementation" heading, or extra layout containers (like `flex-1 overflow-auto`) within the `Card` that encloses the `AnimatedCodeEditor`. The editor should be the primary child of its container card. -12. **Test Case Selection:** Only implement multiple test cases (and the selection UI) if explicitly requested by the user. If not requested, one robust default case is sufficient. When multiple cases ARE requested, provide a premium, visually stunning UI (e.g., an animated sliding toggle with icons from `lucide-react`) to switch between them. Switching cases MUST reset the execution state (`currentStepIndex` back to 0, `isPlaying` to false). Place these controls at the top, typically next to or just below the `StepControls`. +10. **Use VisualizationCodePanel (NOT AnimatedCodeEditor directly):** Always use the `VisualizationCodePanel` component for the right-column code/pseudocode panel. See the multi-language pattern below. +11. **Minimal Code Editor Container:** Do NOT include a redundant `div` wrapper, "Implementation" heading, or extra layout containers within the container that encloses the code panel. +12. **Test Case Selection:** Only implement multiple test cases (and the selection UI) if explicitly requested by the user. 13. **Pedagogical Layout:** Ensure that the descriptive commentary box is placed **at the bottom** of the visualization to provide immediate context for the current state, and the `VariablePanel` MUST be placed **below** the commentary box. +14. **Include `pseudoStep`** in every Step object (see multi-language pattern below). -### 3. Verification +### 3. Multi-Language + Pseudocode Pattern (REQUIRED for all visualization updates) -- Double-check that there are absolutely no comments inside the code block string. +Every new or updated visualization MUST follow this pattern. See TwoSumVisualization.tsx (or similar) as the canonical reference. + +#### Rule: Hardcoded Multi-Language Code (No DB calls at runtime) +- Always support all 4 languages (TypeScript, Python, Java, C++) and Logic pseudocode. +- The exact optimized code implementations must be hardcoded directly inside the visualization component file via the `languages` map. +- The hardcoded code must match the database's optimized implementations exactly (typically provided by the user or fetched from the local clean database codes JSON). Do not make any structural, logic, variable naming, or API changes. The code logic must be identical to the database. +- The code panel strings must not contain any comments or blank lines. +- The step highlight line numbers must be aligned to the clean code offsets exactly. + +#### What the visualization file IS responsible for: +1. **`languages`** — the actual comment-free, optimized code strings for each of the 4 supported languages. +2. **`pseudoSteps`** — one language-agnostic pseudo-statement per step. +3. **`stepLineNumbers`** — dynamically built line mappings that match the `steps` array length exactly. + +#### Step type +```ts +interface Step { + // ... data fields specific to this algorithm ... + explanation: string; // narrative sentence explaining what/why + pseudoStep: string; // language-agnostic pseudo-statement (see style guide) +} +``` + +#### Language code map +```ts +import type { VisualizationLanguageMap, StepLineNumberMap } from '@/types/visualization'; + +const languages: VisualizationLanguageMap = { + python: `...`, // Python implementation (no comments) + typescript: `...`, // TypeScript implementation (no comments) + java: `...`, // Java implementation (no comments) + cpp: `...`, // C++ implementation (no comments) +}; +``` + +#### Dynamic Step and Line Number Generation +Because algorithms have loops and dynamic step counts, **do NOT hardcode `stepLineNumbers` as a static array**. Generate it dynamically alongside the `steps` array so their lengths are guaranteed to match perfectly. + +```ts +function generateVisualizationData() { + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { typescript: [], python: [], java: [], cpp: [] }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + // Example step creation + steps.push({ ... }); + addLines(2, 2, 2, 2); // line numbers for ts, py, java, cpp for this exact step + + // ... (loops and logic) ... + + return { steps, stepLineNumbers }; +} +``` + +#### JSX (right column) +```tsx +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; + +export const MyVisualization = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const pseudoSteps = steps.map(s => s.pseudoStep); + + return ( + // ... layout ... + + ); +} +``` + +#### Pseudocode style guide +- Use ALL-CAPS keywords: `SET`, `FOR`, `IF`, `ELSE`, `RETURN`, `WHILE`, `CALL` +- Use plain English for data structures: `seen = {} (empty map)`, `stack = []` +- Show concrete values where helpful: `complement = target − nums[i] → 18 − 2 = 16` +- Keep each statement to one line (under 80 chars) +- Use arrows for results: `→ YES ✓` / `→ NO ✗` + +### 4. Verification + +- Double-check that there are absolutely no comments inside the code block strings. - Verify array/pointer/variable states correspond exactly to the active execution step. +- Verify all 4 language code blocks are present in `languages` (Python, TypeScript, Java, C++). +- Verify `stepLineNumbers` are built dynamically using an `addLines()` helper alongside every `steps.push()` so the arrays never fall out of sync. diff --git a/fix_303.cjs b/fix_303.cjs deleted file mode 100644 index 6bfc1aa..0000000 --- a/fix_303.cjs +++ /dev/null @@ -1,17 +0,0 @@ -const { createClient } = require('@supabase/supabase-js'); -require('dotenv').config(); -const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY); - -async function run() { - const { data: nData } = await supabase.from('algorithms').select('id, implementations').eq('id', 'nth-highest-salary').single(); - let code = nData.implementations[0].code[0].code; - console.log('BEFORE:', code); - code = code.replace(/OFFSET \(N \- 1\)/g, 'OFFSET 1'); - code = code.replace(/\"getNthHighestSalary\(N\)\"/g, '\"nth_highest_salary\"'); - console.log('AFTER:', code); - - const newImpl = [...nData.implementations]; - newImpl[0].code[0].code = code; - await supabase.from('algorithms').update({ implementations: newImpl }).eq('id', 'nth-highest-salary'); -} -run(); diff --git a/public/sitemap.xml b/public/sitemap.xml index 9fd0846..a13ebb7 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -2,1501 +2,1501 @@ https://rulcode.com/ - 2026-06-27 + 2026-06-28 daily 1.0 https://rulcode.com/about - 2026-06-27 + 2026-06-28 monthly 0.5 https://rulcode.com/blind75 - 2026-06-27 + 2026-06-28 monthly 0.5 https://rulcode.com/blog - 2026-06-27 + 2026-06-28 monthly 0.5 https://rulcode.com/guides - 2026-06-27 + 2026-06-28 monthly 0.5 https://rulcode.com/problem/add-and-search-word - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/spiral-matrix - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/bellman-ford - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/merge-triplets-to-form-target-triplet - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/gcd-euclidean - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/find-minimum-in-rotated-sorted-array - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/longest-palindromic-substring - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/daily-temperatures - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/sieve-eratosthenes - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/rotate-image - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/find-the-duplicate-number - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/task-scheduler - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/sliding-window-maximum - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/xor-trick - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/last-stone-weight - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/nth-highest-salary - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/maximum-product-subarray - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/sudoku-solver - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/subsets-ii - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/redundant-connection - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/non-overlapping-intervals - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/reorder-list - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/palindrome-partitioning - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/k-closest-points-to-origin - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/matrix-path-dp - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/tarjans - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/longest-increasing-subsequence - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/car-fleet - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/activity-selection - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/insert-interval - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/karatsuba - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/container-with-most-water - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/topological-sort - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/number-of-islands - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/manachers - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/letter-combinations-of-a-phone-number - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/binary-lifting - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/target-sum-ways - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/longest-repeating-character-replacement - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/diameter-of-binary-tree - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/word-search-grid - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/lru-cache - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/search-in-rotated-sorted-array - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/segment-tree - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/word-break-problem - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/trapping-rain-water - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/permutation-in-string - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/top-k-frequent-elements - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/meeting-rooms - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/distinct-subsequences - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/modular-exponentiation - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/evaluate-reverse-polish-notation - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/customers-who-never-order - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/same-tree - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/department-highest-salary - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/dutch-national-flag - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/single-number - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/dfs-preorder - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/huffman-encoding - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/binary-tree-level-order-traversal - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/prefix-sum - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/floyd-warshall - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/best-time-to-buy-and-sell-stock - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/burst-balloons - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/monotonic-stack - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/coin-change - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/3sum - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/department-top-three-salaries - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/unique-paths - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/reverse-linked-list - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/invert-binary-tree - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/regular-expression-matching - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/word-search - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/happy-number - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/jump-game-ii - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/reverse-nodes-in-k-group - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/number-of-1-bits - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/dijkstras - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/valid-parentheses - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/rising-temperature - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/serialize-tree - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/union-find - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/consecutive-numbers - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/decode-ways - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/jump-game - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/valid-anagram - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/powx-n - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/union-by-rank - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/group-anagrams - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/maximum-depth-of-binary-tree - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/min-stack - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/merge-k-sorted-lists - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/search-a-2d-matrix - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/kadanes-algorithm - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/maximum-subarray - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/koko-eating-bananas - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/find-the-highest-altitude - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/contains-duplicate - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/valid-palindrome - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/design-twitter - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/rotate-array - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/employees-earning-more-than-their-managers - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/construct-binary-tree-from-preorder-and-inorder-traversal - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/rabin-karp - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/merge-intervals - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/max-area-of-island - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/factory-method-notification-system - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/lcs - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/time-based-key-value-store - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/alien-dictionary - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/counting-bits - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/merge-k-lists - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/merge-sorted-lists - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/kth-largest - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/middle-node - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/implement-trie - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/rotting-oranges - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/combination-sum-ii - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/count-bits - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/lis - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/dfs-inorder - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/sparse-table - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/n-queens - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/pacific-atlantic-water-flow - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/swim-in-rising-water - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/word-search-ii - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/combination-sum - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/duplicate-emails - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/graph-dfs - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/delete-duplicate-emails - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/employee-bonus - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/subtree-of-another-tree - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/reconstruct-itinerary - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/trie - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/graph-bfs - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/sliding-window - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/game-play-analysis-i - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/game-play-analysis-iv - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/managers-with-at-least-5-direct-reports - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/knapsack-01 - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/bfs-level-order - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/longest-substring-without-repeating-characters - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/prims - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/encode-and-decode-strings - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/longest-increasing-path-in-a-matrix - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/binary-tree-maximum-path-sum - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/interleaving-string - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/plus-one - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/palindromic-substrings - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/a-star - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/best-time-to-buy-and-sell-stock-with-cooldown - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/multiply-strings - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/kruskals - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/coin-change-ii - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/interval-scheduling - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/minimum-window-substring - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/reverse-integer - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/generate-parentheses - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/lowest-common-ancestor-of-bst - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/reverse-linked-list-ii - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/add-two-numbers - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/course-schedule - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/sum-of-two-integers - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/median-of-two-sorted-arrays - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/validate-binary-search-tree - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/reverse-bits - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/minimum-interval-to-include-each-query - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/meeting-rooms-ii - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/binary-search - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/detect-squares - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/climbing-stairs - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/clone-graph - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/lca - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/graph-valid-tree - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/word-break - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/find-median-from-data-stream - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/kth-smallest-element-in-a-bst - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/two-sum - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/gas-station - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/fenwick-tree - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/detect-cycle-in-a-linked-list - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/hand-of-straights - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/cyclic-sort - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/fast-slow-pointers - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/recover-bst - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/course-schedule-ii - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/subset-generation-bits - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/two-pointers - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/kth-largest-element-in-a-stream - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/edit-distance - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/longest-consecutive-sequence - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/binary-tree-right-side-view - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/combine-two-tables - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/partition-equal-subset - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/subsets - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/balanced-binary-tree - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/remove-nth-node-from-end-of-list - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/permutations - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/serialize-and-deserialize-binary-tree - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/dfs-postorder - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/house-robber-ii - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/bst-insert - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/partition-equal-subset-sum - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/second-highest-salary - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/kmp - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/partition-labels - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/product-of-array-except-self - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/number-of-connected-components-in-an-undirected-graph - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/min-cost-climbing-stairs - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/house-robber - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/word-ladder - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/rank-scores - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/missing-number - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/longest-common-subsequence - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/merge-two-sorted-lists - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/surrounded-regions - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/set-matrix-zeroes - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/combinations - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/problem/count-good-nodes-in-binary-tree - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/blog/master-blind-75-visual-algorithms - 2026-06-27 + 2026-06-28 weekly 0.7 https://rulcode.com/blog/lru-cache-complete-guide - 2026-06-27 + 2026-06-28 weekly 0.7 https://rulcode.com/blog/dynamic-programming-for-people-who-hate-dynamic-programming - 2026-06-27 + 2026-06-28 weekly 0.7 https://rulcode.com/guides/time-complexity - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/guides/space-complexity - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/guides/fundamentals/core-data-structures - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/guides/fundamentals/linked-list - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/guides/fundamentals/trees - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/guides/fundamentals/trie - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/guides/fundamentals/graphs - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/guides/patterns/arrays-hashing - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/guides/patterns/two-pointers - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/guides/patterns/frequency-counter - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/guides/patterns/prefix-sum - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/guides/patterns/sliding-window - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/guides/patterns/stack - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/guides/patterns/binary-search - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/guides/patterns/cyclic-sort - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/guides/patterns/merge-sort - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/guides/patterns/recursion - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/guides/patterns/backtracking - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/guides/patterns/merge-intervals - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/guides/patterns/dynamic-programming - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/guides/patterns/what-is-database - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/guides/patterns/types-of-databases - 2026-06-27 + 2026-06-28 weekly 0.8 https://rulcode.com/guides/patterns/database-terminology - 2026-06-27 + 2026-06-28 weekly 0.8 diff --git a/src/components/CodeRunner/LanguageSelector.tsx b/src/components/CodeRunner/LanguageSelector.tsx index 91c2d84..113910a 100644 --- a/src/components/CodeRunner/LanguageSelector.tsx +++ b/src/components/CodeRunner/LanguageSelector.tsx @@ -17,9 +17,9 @@ interface LanguageSelectorProps { } const languages: { id: Language; name: string; icon: React.ElementType }[] = [ + { id: 'python', name: 'Python', icon: Hash }, { id: 'cpp', name: 'C++', icon: Code2 }, { id: 'java', name: 'Java', icon: Coffee }, - { id: 'python', name: 'Python', icon: Hash }, { id: 'typescript', name: 'TypeScript', icon: FileJson }, { id: 'sql', name: 'SQLite', icon: Database }, ]; diff --git a/src/components/algorithm/ProblemDescriptionPanel.tsx b/src/components/algorithm/ProblemDescriptionPanel.tsx index 83347ea..6b6f018 100644 --- a/src/components/algorithm/ProblemDescriptionPanel.tsx +++ b/src/components/algorithm/ProblemDescriptionPanel.tsx @@ -817,7 +817,7 @@ export const ProblemDescriptionPanel = React.memo(
{(algorithm?.id && (hasVisualization(algorithm.id) || renderBlind75Visualization(algorithm.id))) ? (
- {renderVizFromMapping(algorithm.id) || renderBlind75Visualization(algorithm.id)} + {renderVizFromMapping(algorithm.id) || renderBlind75Visualization(algorithm.id)}
) : ( diff --git a/src/components/visualizations/algorithms/AStarVisualization.tsx b/src/components/visualizations/algorithms/AStarVisualization.tsx index 7ba002d..cf5cea7 100644 --- a/src/components/visualizations/algorithms/AStarVisualization.tsx +++ b/src/components/visualizations/algorithms/AStarVisualization.tsx @@ -1,8 +1,8 @@ -import { useEffect, useRef, useState } from "react"; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; -import { StepControls } from "../shared/StepControls"; -import { VariablePanel } from "../shared/VariablePanel"; +import { useEffect, useRef, useState } from 'react'; +import { StepControls } from '../shared/StepControls'; +import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Cell { x: number; @@ -18,259 +18,469 @@ interface Step { closedSet: Cell[]; current: Cell | null; path: Cell[]; - message: string; - lineNumber: number; + explanation: string; + pseudoStep: string; + variables: Record; } -export const AStarVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const intervalRef = useRef(null); - - const code = `function aStar(grid: number[][], start: [number, number], goal: [number, number]): [number, number][] { - const rows = grid.length - const cols = grid[0].length - - const heuristic = (x: number, y: number) => - Math.abs(x - goal[0]) + Math.abs(y - goal[1]) - - const dirs = [[0,1],[1,0],[0,-1],[-1,0]] - - const openSet: [number, number, number][] = [[heuristic(...start), start[0], start[1]]] - - const cameFrom = new Map() - const gScore = new Map() - const closedSet = new Set() - - const startKey = start[0]*cols + start[1] - gScore.set(startKey, 0) - - while(openSet.length) { - openSet.sort((a,b)=>a[0]-b[0]) - const [, x, y] = openSet.shift()! - - const key = x*cols + y - - if(closedSet.has(key)) continue - closedSet.add(key) - - if(x === goal[0] && y === goal[1]) { - const path: [number,number][] = [] - let curr = key - - while(curr !== startKey) { - const cx = Math.floor(curr/cols) - const cy = curr%cols - path.unshift([cx,cy]) - curr = cameFrom.get(curr)! - } - - path.unshift(start) - return path +const languages: VisualizationLanguageMap = { + typescript: `function aStar(grid: number[][], start: [number, number], goal: [number, number]): [number, number][] { + const rows = grid.length + const cols = grid[0].length + const heuristic = (x: number, y: number) => + Math.abs(x - goal[0]) + Math.abs(y - goal[1]) + const dirs = [[0,1],[1,0],[0,-1],[-1,0]] + const openSet: [number, number, number][] = [[heuristic(...start), start[0], start[1]]] + const cameFrom = new Map() + const gScore = new Map() + const closedSet = new Set() + const startKey = start[0]*cols + start[1] + gScore.set(startKey, 0) + while(openSet.length) { + openSet.sort((a,b)=>a[0]-b[0]) + const [, x, y] = openSet.shift()! + const key = x*cols + y + if(closedSet.has(key)) continue + closedSet.add(key) + if(x === goal[0] && y === goal[1]) { + const path: [number,number][] = [] + let curr = key + while(curr !== startKey) { + const cx = Math.floor(curr/cols) + const cy = curr%cols + path.unshift([cx,cy]) + curr = cameFrom.get(curr)! + } + path.unshift(start) + return path + } + for(const [dx,dy] of dirs) { + const nx = x+dx + const ny = y+dy + if(nx<0 || ny<0 || nx>=rows || ny>=cols || grid[nx][ny]===1) continue + const neighborKey = nx*cols+ny + const tentative = gScore.get(key)! + 1 + if(!gScore.has(neighborKey) || tentative < gScore.get(neighborKey)!) { + cameFrom.set(neighborKey, key) + gScore.set(neighborKey, tentative) + const f = tentative + heuristic(nx,ny) + openSet.push([f,nx,ny]) + } + } } - - for(const [dx,dy] of dirs) { - const nx = x+dx - const ny = y+dy - - if(nx<0 || ny<0 || nx>=rows || ny>=cols || grid[nx][ny]===1) continue - - const neighborKey = nx*cols+ny - const tentative = gScore.get(key)! + 1 - - if(!gScore.has(neighborKey) || tentative < gScore.get(neighborKey)!) { - cameFrom.set(neighborKey, key) - gScore.set(neighborKey, tentative) - - const f = tentative + heuristic(nx,ny) - openSet.push([f,nx,ny]) - } + return [] +}`, + python: `def a_star(grid, start, goal): + rows, cols = len(grid), len(grid[0]) + def heuristic(row, col): + return abs(row - goal[0]) + abs(col - goal[1]) + directions = [(0,1),(1,0),(0,-1),(-1,0)] + open_set = [[heuristic(start[0], start[1]), start[0], start[1]]] + parent = {} + g_score = {} + closed_set = set() + start_key = start[0] * cols + start[1] + g_score[start_key] = 0 + while open_set: + open_set.sort(key=lambda x: x[0]) + _, current_row, current_col = open_set.pop(0) + current_key = current_row * cols + current_col + if current_key in closed_set: + continue + closed_set.add(current_key) + if current_row == goal[0] and current_col == goal[1]: + path = [] + current = current_key + while current != start_key: + row = current // cols + col = current % cols + path.insert(0, [row, col]) + current = parent[current] + path.insert(0, start) + return path + for direction_row, direction_col in directions: + neighbor_row = current_row + direction_row + neighbor_col = current_col + direction_col + if ( + neighbor_row < 0 or + neighbor_col < 0 or + neighbor_row >= rows or + neighbor_col >= cols or + grid[neighbor_row][neighbor_col] == 1 + ): + continue + neighbor_key = neighbor_row * cols + neighbor_col + tentative_g = g_score[current_key] + 1 + if ( + neighbor_key not in g_score or + tentative_g < g_score[neighbor_key] + ): + parent[neighbor_key] = current_key + g_score[neighbor_key] = tentative_g + f_score = tentative_g + heuristic(neighbor_row, neighbor_col) + open_set.append( + [f_score, neighbor_row, neighbor_col] + ) + return []`, + java: `public static class Solution { + int orderCounter = 0; + public List aStar(int[][] grid, int[] start, int[] goal) { + int rows = grid.length, cols = grid[0].length; + PriorityQueue openSet = + new PriorityQueue<>((a, b) -> a.fScore != b.fScore ? a.fScore - b.fScore : a.order - b.order); + Map gScore = new HashMap<>(); + Map parent = new HashMap<>(); + boolean[][] closed = new boolean[rows][cols]; + int startKey = start[0] * cols + start[1]; + int goalKey = goal[0] * cols + goal[1]; + gScore.put(startKey, 0); + openSet.add(new Node(start[0], start[1], heuristic(start, goal), orderCounter++)); + int[][] directions = {{0,1},{1,0},{0,-1},{-1,0}}; + while (!openSet.isEmpty()) { + Node current = openSet.poll(); + if (closed[current.row][current.col]) + continue; + int currentKey = current.row * cols + current.col; + if (currentKey == goalKey) + return buildPath(parent, currentKey, cols); + closed[current.row][current.col] = true; + for (int[] direction : directions) { + int neighborRow = current.row + direction[0]; + int neighborCol = current.col + direction[1]; + if ( + neighborRow < 0 || + neighborCol < 0 || + neighborRow >= rows || + neighborCol >= cols || + grid[neighborRow][neighborCol] == 1 + ) continue; + int neighborKey = neighborRow * cols + neighborCol; + int newGScore = gScore.get(currentKey) + 1; + if ( + !gScore.containsKey(neighborKey) || + newGScore < gScore.get(neighborKey) + ) { + gScore.put(neighborKey, newGScore); + parent.put(neighborKey, currentKey); + int fScore = + newGScore + + Math.abs(neighborRow - goal[0]) + + Math.abs(neighborCol - goal[1]); + openSet.add( + new Node( + neighborRow, + neighborCol, + fScore, + orderCounter++ + ) + ); + } + } + } + return new ArrayList<>(); } - } - - return [] -}`; - - const generateSteps = () => { - const rows = 6; - const cols = 8; - const grid = Array(rows) - .fill(0) - .map(() => Array(cols).fill(".")); - - // Add obstacles matching the '1' representation in user's TS code. - // In our visually mapped logic, "#" translates to obstacle in UI rendering block. - grid[2][3] = "#"; - grid[2][4] = "#"; - grid[3][4] = "#"; - - const start: [number, number] = [0, 0]; - const goal: [number, number] = [5, 7]; - - const heuristic = (x: number, y: number) => - Math.abs(x - goal[0]) + Math.abs(y - goal[1]); - - const dirs = [[0, 1], [1, 0], [0, -1], [-1, 0]]; - - const newSteps: Step[] = []; - const openSet: [number, number, number][] = [ - [heuristic(start[0], start[1]), start[0], start[1]], - ]; - - // We preserve Cell structure for UI mappings - const openSetCells = new Map(); - openSetCells.set(start[0] * cols + start[1], { x: start[0], y: start[1], f: heuristic(start[0], start[1]), g: 0, h: heuristic(start[0], start[1]) }); - - const closedSet = new Set(); - const cameFrom = new Map(); - const gScore = new Map(); + int heuristic(int[] a, int[] b) { + return Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]); + } + List buildPath(Map parent, int currentKey, int cols) { + List path = new ArrayList<>(); + while (true) { + path.add(0, new int[]{currentKey / cols, currentKey % cols}); + if (!parent.containsKey(currentKey)) break; + currentKey = parent.get(currentKey); + } + return path; + } + static class Node { + int row; + int col; + int fScore; + int order; + Node(int row, int col, int fScore, int order) { + this.row = row; + this.col = col; + this.fScore = fScore; + this.order = order; + } + } +}`, + cpp: `class Solution { +public: + int orderCounter = 0; + struct AStarNode { + int row, col, fScore, order; + }; + struct Compare { + bool operator()(const AStarNode& a, const AStarNode& b) const { + if (a.fScore != b.fScore) return a.fScore > b.fScore; + return a.order > b.order; + } + }; + vector> aStar(vector>& grid, vector& start, vector& goal) { + int rows = grid.size(), cols = grid[0].size(); + priority_queue, Compare> openSet; + unordered_map gScore; + unordered_map parent; + vector> closed(rows, vector(cols, false)); + int startKey = start[0] * cols + start[1]; + int goalKey = goal[0] * cols + goal[1]; + gScore[startKey] = 0; + openSet.push({start[0], start[1], heuristic(start, goal), orderCounter++}); + int directions[4][2] = {{0,1},{1,0},{0,-1},{-1,0}}; + while (!openSet.empty()) { + AStarNode current = openSet.top(); + openSet.pop(); + if (closed[current.row][current.col]) + continue; + int currentKey = current.row * cols + current.col; + if (currentKey == goalKey) + return buildPath(parent, currentKey, cols); + closed[current.row][current.col] = true; + for (auto& d : directions) { + int nr = current.row + d[0]; + int nc = current.col + d[1]; + if ( + nr < 0 || nc < 0 || + nr >= rows || nc >= cols || + grid[nr][nc] == 1 + ) continue; + int key = nr * cols + nc; + int newG = gScore[currentKey] + 1; + if (!gScore.count(key) || newG < gScore[key]) { + gScore[key] = newG; + parent[key] = currentKey; + int f = newG + abs(nr - goal[0]) + abs(nc - goal[1]); + openSet.push({nr, nc, f, orderCounter++}); + } + } + } + return {}; + } + int heuristic(vector& a, vector& b) { + return abs(a[0] - b[0]) + abs(a[1] - b[1]); + } + vector> buildPath(unordered_map& parent, int key, int cols) { + vector> path; + while (true) { + path.insert(path.begin(), { key / cols, key % cols }); + if (!parent.count(key)) break; + key = parent[key]; + } + return path; + } +};`, +}; - const startKey = start[0] * cols + start[1]; - gScore.set(startKey, 0); +function generateVisualizationData() { + const rows = 6; + const cols = 8; + const grid = Array(rows) + .fill(0) + .map(() => Array(cols).fill('.')); + + // Obstacles mapping + grid[2][3] = '#'; + grid[2][4] = '#'; + grid[3][4] = '#'; + + const start: [number, number] = [0, 0]; + const goal: [number, number] = [5, 7]; + + const heuristic = (x: number, y: number) => Math.abs(x - goal[0]) + Math.abs(y - goal[1]); + const dirs = [[0, 1], [1, 0], [0, -1], [-1, 0]]; + + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { typescript: [], python: [], java: [], cpp: [] }; + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - newSteps.push({ - grid: grid.map((row) => [...row]), + const openSet: [number, number, number][] = []; + const openSetCells = new Map(); + const closedSet = new Set(); + const cameFrom = new Map(); + const gScore = new Map(); + + const startKey = start[0] * cols + start[1]; + gScore.set(startKey, 0); + const startF = heuristic(start[0], start[1]); + openSet.push([startF, start[0], start[1]]); + openSetCells.set(startKey, { x: start[0], y: start[1], g: 0, h: startF, f: startF }); + + steps.push({ + grid: grid.map(row => [...row]), + openSet: Array.from(openSetCells.values()), + closedSet: [], + current: null, + path: [], + explanation: 'Initialize open set with start node S and closed set as empty.', + pseudoStep: 'Initialize openSet, closedSet, gScore, cameFrom', + variables: { openSetSize: 1, closedSetSize: 0, current: 'None', gScore: 0 } + }); + addLines(7, 6, 13, 22); + + while (openSet.length > 0) { + openSet.sort((a, b) => a[0] - b[0]); + const [fScore, x, y] = openSet.shift()!; + const key = x * cols + y; + + const currentCell = openSetCells.get(key) || { x, y, g: gScore.get(key) || 0, h: heuristic(x, y), f: fScore }; + openSetCells.delete(key); + + steps.push({ + grid: grid.map(row => [...row]), openSet: Array.from(openSetCells.values()), - closedSet: [], - current: null, + closedSet: Array.from(closedSet).map(k => { + const cx = Math.floor(k / cols); + const cy = k % cols; + return { x: cx, y: cy, g: gScore.get(k) || 0, h: heuristic(cx, cy), f: (gScore.get(k) || 0) + heuristic(cx, cy) }; + }), + current: currentCell, path: [], - message: "Initialize A* structures: start, open set, closed set, scores", - lineNumber: 17, + explanation: `Select node (${x}, ${y}) with the lowest f-score = ${currentCell.f} (g = ${currentCell.g}, h = ${currentCell.h}) from the open set.`, + pseudoStep: 'Pop node with lowest f-score from openSet', + variables: { openSetSize: openSet.length, closedSetSize: closedSet.size, current: `(${x}, ${y})`, f: currentCell.f, g: currentCell.g, h: currentCell.h } }); + addLines(15, 14, 16, 25); - while (openSet.length > 0) { - newSteps.push({ - grid: grid.map((row) => [...row]), + if (closedSet.has(key)) { + steps.push({ + grid: grid.map(row => [...row]), openSet: Array.from(openSetCells.values()), - closedSet: Array.from(closedSet).map(k => ({ x: Math.floor(k / cols), y: k % cols, f: 0, g: 0, h: 0 })), - current: null, + closedSet: Array.from(closedSet).map(k => { + const cx = Math.floor(k / cols); + const cy = k % cols; + return { x: cx, y: cy, g: gScore.get(k) || 0, h: heuristic(cx, cy), f: (gScore.get(k) || 0) + heuristic(cx, cy) }; + }), + current: currentCell, path: [], - message: "Sort openSet by lowest f-score", - lineNumber: 20, + explanation: `Node (${x}, ${y}) has already been evaluated and is in the closed set. Skip it.`, + pseudoStep: 'IF current in closedSet THEN continue', + variables: { openSetSize: openSet.length, closedSetSize: closedSet.size, current: `(${x}, ${y})` } }); + addLines(17, 16, 17, 27); + continue; + } - openSet.sort((a, b) => a[0] - b[0]); - const [fScore, x, y] = openSet.shift()!; - - const key = x * cols + y; - const currentCell = openSetCells.get(key) || { x, y, f: fScore, g: gScore.get(key) || 0, h: heuristic(x, y) }; - openSetCells.delete(key); + closedSet.add(key); + steps.push({ + grid: grid.map(row => [...row]), + openSet: Array.from(openSetCells.values()), + closedSet: Array.from(closedSet).map(k => { + const cx = Math.floor(k / cols); + const cy = k % cols; + return { x: cx, y: cy, g: gScore.get(k) || 0, h: heuristic(cx, cy), f: (gScore.get(k) || 0) + heuristic(cx, cy) }; + }), + current: currentCell, + path: [], + explanation: `Add node (${x}, ${y}) to the closed set.`, + pseudoStep: 'Add current to closedSet', + variables: { openSetSize: openSet.length, closedSetSize: closedSet.size, current: `(${x}, ${y})` } + }); + addLines(18, 18, 22, 32); + + if (x === goal[0] && y === goal[1]) { + const path: Cell[] = []; + let curr = key; + while (curr !== startKey) { + const cx = Math.floor(curr / cols); + const cy = curr % cols; + path.unshift({ x: cx, y: cy, g: gScore.get(curr) || 0, h: heuristic(cx, cy), f: (gScore.get(curr) || 0) + heuristic(cx, cy) }); + curr = cameFrom.get(curr)!; + } + path.unshift({ x: start[0], y: start[1], g: 0, h: heuristic(start[0], start[1]), f: heuristic(start[0], start[1]) }); - newSteps.push({ - grid: grid.map((row) => [...row]), + steps.push({ + grid: grid.map(row => [...row]), openSet: Array.from(openSetCells.values()), - closedSet: Array.from(closedSet).map(k => ({ x: Math.floor(k / cols), y: k % cols, f: 0, g: 0, h: 0 })), + closedSet: Array.from(closedSet).map(k => { + const cx = Math.floor(k / cols); + const cy = k % cols; + return { x: cx, y: cy, g: gScore.get(k) || 0, h: heuristic(cx, cy), f: (gScore.get(k) || 0) + heuristic(cx, cy) }; + }), current: currentCell, - path: [], - message: `Current node (${x}, ${y}) with f=${currentCell.f}`, - lineNumber: 21, + path, + explanation: 'Goal reached! Reconstructed path by backtracking via cameFrom parent pointers.', + pseudoStep: 'Reconstruct path from goal to start', + variables: { openSetSize: openSet.length, closedSetSize: closedSet.size, current: `(${x}, ${y})`, pathLength: path.length } }); + addLines(19, 19, 20, 30); + break; + } - if (closedSet.has(key)) { - newSteps.push({ - grid: grid.map((row) => [...row]), - openSet: Array.from(openSetCells.values()), - closedSet: Array.from(closedSet).map(k => ({ x: Math.floor(k / cols), y: k % cols, f: 0, g: 0, h: 0 })), - current: currentCell, - path: [], - message: `Node (${x}, ${y}) already evaluated (closed), skipping`, - lineNumber: 25, - }); + for (const [dx, dy] of dirs) { + const nx = x + dx; + const ny = y + dy; + + if (nx < 0 || ny < 0 || nx >= rows || ny >= cols || grid[nx][ny] === '#') { continue; } - closedSet.add(key); - newSteps.push({ - grid: grid.map((row) => [...row]), + const neighborKey = nx * cols + ny; + const tentative = (gScore.get(key) || 0) + 1; + + steps.push({ + grid: grid.map(row => [...row]), openSet: Array.from(openSetCells.values()), - closedSet: Array.from(closedSet).map(k => ({ x: Math.floor(k / cols), y: k % cols, f: 0, g: 0, h: 0 })), + closedSet: Array.from(closedSet).map(k => { + const cx = Math.floor(k / cols); + const cy = k % cols; + return { x: cx, y: cy, g: gScore.get(k) || 0, h: heuristic(cx, cy), f: (gScore.get(k) || 0) + heuristic(cx, cy) }; + }), current: currentCell, path: [], - message: `Marking node (${x}, ${y}) as evaluated (added to closed set)`, - lineNumber: 26, + explanation: `Evaluate neighbor (${nx}, ${ny}). Tentative g-score: ${tentative}.`, + pseudoStep: `Check neighbor (${nx}, ${ny})`, + variables: { openSetSize: openSet.length, closedSetSize: closedSet.size, current: `(${x}, ${y})`, neighbor: `(${nx}, ${ny})`, tentativeG: tentative } }); + addLines(31, 29, 23, 33); - if (x === goal[0] && y === goal[1]) { - const path: Cell[] = []; - let curr = key; - while (curr !== startKey) { - const cx = Math.floor(curr / cols); - const cy = curr % cols; - path.unshift({ x: cx, y: cy, f: 0, g: 0, h: 0 }); - curr = cameFrom.get(curr)!; - } - path.unshift({ x: start[0], y: start[1], f: 0, g: 0, h: 0 }); - - newSteps.push({ - grid: grid.map((row) => [...row]), - openSet: Array.from(openSetCells.values()), - closedSet: Array.from(closedSet).map(k => ({ x: Math.floor(k / cols), y: k % cols, f: 0, g: 0, h: 0 })), - current: currentCell, - path, - message: "Goal reached! Reconstructed path by backtracking via cameFrom", - lineNumber: 28, - }); - break; - } - - for (const [dx, dy] of dirs) { - const nx = x + dx; - const ny = y + dy; + if (!gScore.has(neighborKey) || tentative < (gScore.get(neighborKey) || 0)) { + cameFrom.set(neighborKey, key); + gScore.set(neighborKey, tentative); - // Skip bounds and obstacles - if (nx < 0 || ny < 0 || nx >= rows || ny >= cols || grid[nx][ny] === "#") { - continue; - } + const f = tentative + heuristic(nx, ny); + openSet.push([f, nx, ny]); + openSetCells.set(neighborKey, { x: nx, y: ny, g: tentative, h: heuristic(nx, ny), f }); - newSteps.push({ - grid: grid.map((row) => [...row]), + steps.push({ + grid: grid.map(row => [...row]), openSet: Array.from(openSetCells.values()), - closedSet: Array.from(closedSet).map(k => ({ x: Math.floor(k / cols), y: k % cols, f: 0, g: 0, h: 0 })), + closedSet: Array.from(closedSet).map(k => { + const cx = Math.floor(k / cols); + const cy = k % cols; + return { x: cx, y: cy, g: gScore.get(k) || 0, h: heuristic(cx, cy), f: (gScore.get(k) || 0) + heuristic(cx, cy) }; + }), current: currentCell, path: [], - message: `Checking neighbor (${nx}, ${ny})`, - lineNumber: 43, + explanation: `Updated shorter path to neighbor (${nx}, ${ny}): g = ${tentative}, h = ${heuristic(nx, ny)}, f = ${f}. Add to open set.`, + pseudoStep: `Update neighbor: gScore = ${tentative}, fScore = ${f}`, + variables: { openSetSize: openSet.length, closedSetSize: closedSet.size, current: `(${x}, ${y})`, neighbor: `(${nx}, ${ny})`, fScore: f } }); - - const neighborKey = nx * cols + ny; - const tentative = gScore.get(key)! + 1; - - if (!gScore.has(neighborKey) || tentative < gScore.get(neighborKey)!) { - cameFrom.set(neighborKey, key); - gScore.set(neighborKey, tentative); - - const f = tentative + heuristic(nx, ny); - openSet.push([f, nx, ny]); - - openSetCells.set(neighborKey, { x: nx, y: ny, g: tentative, h: heuristic(nx, ny), f }); - - newSteps.push({ - grid: grid.map((row) => [...row]), - openSet: Array.from(openSetCells.values()), - closedSet: Array.from(closedSet).map(k => ({ x: Math.floor(k / cols), y: k % cols, f: 0, g: 0, h: 0 })), - current: currentCell, - path: [], - message: `Updated better path to neighbor (${nx}, ${ny}): g=${tentative}, h=${heuristic(nx, ny)}, f=${f}`, - lineNumber: 57, - }); - } + addLines(41, 49, 45, 47); } } + } - setSteps(newSteps); - setCurrentStepIndex(0); - }; + return { steps, stepLineNumbers }; +} - useEffect(() => { - generateSteps(); - }, []); +export const AStarVisualization = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); + + const cols = 8; useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { intervalRef.current = setInterval(() => { - setCurrentStepIndex((prev) => { + setCurrentStepIndex(prev => { if (prev >= steps.length - 1) { setIsPlaying(false); return prev; @@ -288,20 +498,17 @@ export const AStarVisualization = () => { const handlePlay = () => setIsPlaying(true); const handlePause = () => setIsPlaying(false); - const handleStepForward = () => - currentStepIndex < steps.length - 1 && - setCurrentStepIndex((prev) => prev + 1); - const handleStepBack = () => - currentStepIndex > 0 && setCurrentStepIndex((prev) => prev - 1); + const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(p => p + 1); + const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(p => p - 1); const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); - generateSteps(); }; if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return (
@@ -320,78 +527,98 @@ export const AStarVisualization = () => {
-
-
- {currentStep.grid.map((row, rowIdx) => ( -
- {row.map((cell, colIdx) => { +
+

A* Grid Map

+
+
+ {currentStep.grid.map((row, rowIdx) => + row.map((cell, colIdx) => { const isStart = rowIdx === 0 && colIdx === 0; const isGoal = rowIdx === 5 && colIdx === 7; - const isCurrent = - currentStep.current?.x === rowIdx && - currentStep.current?.y === colIdx; - const inPath = currentStep.path.some( - (p) => p.x === rowIdx && p.y === colIdx, - ); - const inOpen = currentStep.openSet.some( - (c) => c.x === rowIdx && c.y === colIdx, - ); - const inClosed = currentStep.closedSet.some( - (c) => c.x === rowIdx && c.y === colIdx, - ); + const isCurrent = currentStep.current?.x === rowIdx && currentStep.current?.y === colIdx; + const inPath = currentStep.path.some(p => p.x === rowIdx && p.y === colIdx); + const openCell = currentStep.openSet.find(c => c.x === rowIdx && c.y === colIdx); + const closedCell = currentStep.closedSet.find(c => c.x === rowIdx && c.y === colIdx); + const isObstacle = cell === '#'; + + let cellClass = 'bg-card text-muted-foreground border-border'; + if (isObstacle) { + cellClass = 'bg-slate-800 text-transparent border-slate-700 select-none shadow-inner'; + } else if (inPath) { + cellClass = 'bg-green-500 text-white border-green-600 font-bold shadow-md'; + } else if (isCurrent) { + cellClass = 'bg-primary text-primary-foreground border-primary-foreground font-bold ring-2 ring-primary ring-offset-2 ring-offset-background'; + } else if (isStart) { + cellClass = 'bg-blue-500 text-white border-blue-600 font-bold'; + } else if (isGoal) { + cellClass = 'bg-red-500 text-white border-red-600 font-bold'; + } else if (openCell) { + cellClass = 'bg-amber-500/10 text-amber-500 border-amber-500/50 shadow-sm'; + } else if (closedCell) { + cellClass = 'bg-slate-500/10 text-slate-400 border-slate-500/30'; + } + + const cellData = openCell || closedCell || (isCurrent ? currentStep.current : null) || (inPath ? currentStep.path.find(p => p.x === rowIdx && p.y === colIdx) : null); return (
- {cell === "#" ? "" : isStart ? "S" : isGoal ? "G" : ""} + {!isObstacle && cellData && ( +
+ g:{cellData.g} + h:{cellData.h} +
+ )} + +
+ {isStart ? 'S' : isGoal ? 'G' : isObstacle ? '' : cellData ? cellData.f : ''} +
+ + {!isObstacle && ( +
+ {rowIdx},{colIdx} +
+ )}
); - })} -
- ))} + }) + )} +
+
+
+ Start (S) + Goal (G) + Current + Shortest Path + Open Set + Closed Set + Obstacle
-

- {currentStep.message} -

-
-
- +

{currentStep.explanation}

-
-
-
+ +
); diff --git a/src/components/visualizations/algorithms/ActivitySelectionVisualization.tsx b/src/components/visualizations/algorithms/ActivitySelectionVisualization.tsx index 97341dd..b2b7669 100644 --- a/src/components/visualizations/algorithms/ActivitySelectionVisualization.tsx +++ b/src/components/visualizations/algorithms/ActivitySelectionVisualization.tsx @@ -1,8 +1,10 @@ -import React, { useEffect, useRef, useState } from "react"; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; -import { StepControls } from "../shared/StepControls"; -import { VariablePanel } from "../shared/VariablePanel"; +import React, { useState } from 'react'; +import { Info } from 'lucide-react'; +import { SimpleStepControls } from '../shared/SimpleStepControls'; +import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import { VisualizationLayout } from '../shared/VisualizationLayout'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Interval { start: number; @@ -16,309 +18,376 @@ interface Step { removed: number[]; current: number; lastEndTime: number; - message: string; - lineNumber: number; + explanation: string; + pseudoStep: string; } -export const ActivitySelectionVisualization: React.FC = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1000); - const intervalRef = useRef(null); - - const code = `function eraseOverlapIntervals(intervals: number[][]): number { +const languages: VisualizationLanguageMap = { + typescript: `function eraseOverlapIntervals(intervals: number[][]): number { if (!intervals || intervals.length === 0) { return 0; } - intervals.sort((a, b) => a[1] - b[1]); - let nonOverlappingCount = 1; let lastEndTime = intervals[0][1]; - for (let i = 1; i < intervals.length; i++) { const currentStartTime = intervals[i][0]; const currentEndTime = intervals[i][1]; - if (currentStartTime >= lastEndTime) { nonOverlappingCount++; lastEndTime = currentEndTime; } } - return intervals.length - nonOverlappingCount; -}`; - - const generateSteps = () => { - const rawIntervals = [ - [1, 4], - [3, 5], - [0, 6], - [5, 7], - [8, 9], - [5, 9], - ]; - const intervals: Interval[] = rawIntervals.map((arr, index) => ({ - start: arr[0], - end: arr[1], - index, - })); - - const newSteps: Step[] = []; - - newSteps.push({ - intervals: [...intervals], - selected: [], - removed: [], - current: -1, - lastEndTime: -1, - message: "Initial set of intervals. We want to find the maximum number of non-overlapping intervals.", - lineNumber: 1, +}`, + + python: `def eraseOverlapIntervals(intervals: List[List[int]]) -> int: + if not intervals: + return 0 + intervals.sort(key=lambda x: x[1]) + count = 1 + end = intervals[0][1] + for i in range(1, len(intervals)): + if intervals[i][0] >= end: + count += 1 + end = intervals[i][1] + return len(intervals) - count`, + + java: `public int eraseOverlapIntervals(int[][] intervals) { + if (intervals == null || intervals.length == 0) { + return 0; + } + Arrays.sort(intervals, (a, b) -> Integer.compare(a[1], b[1])); + int count = 1; + int end = intervals[0][1]; + for (int i = 1; i < intervals.length; i++) { + if (intervals[i][0] >= end) { + count++; + end = intervals[i][1]; + } + } + return intervals.length - count; +}`, + + cpp: `int eraseOverlapIntervals(vector>& intervals) { + if (intervals.empty()) { + return 0; + } + sort(intervals.begin(), intervals.end(), [](const auto& a, const auto& b) { + return a[1] < b[1]; }); + int count = 1; + int end = intervals[0][1]; + for (int i = 1; i < intervals.size(); i++) { + if (intervals[i][0] >= end) { + count++; + end = intervals[i][1]; + } + } + return intervals.size() - count; +}` +}; - // Sorting Step - intervals.sort((a, b) => a.end - b.end); +function generateVisualizationData() { + const rawIntervals = [ + [1, 4], + [2, 3], + [3, 5], + [0, 6], + [5, 7], + [8, 9], + [5, 9], + [6, 8] + ]; + const intervals: Interval[] = rawIntervals.map((arr, index) => ({ + start: arr[0], + end: arr[1], + index + })); + + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - newSteps.push({ - intervals: [...intervals], - selected: [], - removed: [], - current: -1, - lastEndTime: -1, - message: "Greedy Strategy: Sort intervals by their end times (earliest end time first) to leave as much room as possible for others.", - lineNumber: 6, - }); + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - let nonOverlappingCount = 1; - let lastEndTime = intervals[0].end; - const selected: number[] = [0]; - const removed: number[] = []; + // Initial State + steps.push({ + intervals: [...intervals], + selected: [], + removed: [], + current: -1, + lastEndTime: -1, + explanation: 'Start activity selection. We want to find the minimum intervals to remove to eliminate overlap.', + pseudoStep: 'START eraseOverlapIntervals(intervals)', + }); + addLines(1, 1, 1, 1); + + // Sorting + intervals.sort((a, b) => a.end - b.end); + steps.push({ + intervals: [...intervals], + selected: [], + removed: [], + current: -1, + lastEndTime: -1, + explanation: 'Greedy Choice: Sort the intervals by their end times in ascending order. Choosing activities that end earliest leaves maximum room for subsequent activities.', + pseudoStep: 'SORT intervals BY end_time ASC', + }); + addLines(5, 4, 5, 5); - newSteps.push({ + let nonOverlappingCount = 1; + let lastEndTime = intervals[0].end; + const selected: number[] = [0]; + const removed: number[] = []; + + steps.push({ + intervals: [...intervals], + selected: [...selected], + removed: [...removed], + current: 0, + lastEndTime, + explanation: `Select the first sorted interval (ends at ${lastEndTime}). This is our baseline non-overlapping activity.`, + pseudoStep: `SET count = 1, end = intervals[0].end (${lastEndTime})`, + }); + addLines(7, 6, 7, 9); + + for (let i = 1; i < intervals.length; i++) { + const currentStartTime = intervals[i].start; + const currentEndTime = intervals[i].end; + + steps.push({ intervals: [...intervals], selected: [...selected], removed: [...removed], - current: 0, + current: i, lastEndTime, - message: `Pick the first interval (ends at ${lastEndTime}). This interval ends earliest, so it's a safe greedy choice.`, - lineNumber: 9, + explanation: `Check interval ${i} ([${currentStartTime}, ${currentEndTime}]): Does its start time (${currentStartTime}) come after or equal to the last activity's end time (${lastEndTime})?`, + pseudoStep: `IF intervals[${i}].start (${currentStartTime}) >= end (${lastEndTime})`, }); + addLines(11, 8, 9, 11); - for (let i = 1; i < intervals.length; i++) { - const currentStartTime = intervals[i].start; - const currentEndTime = intervals[i].end; + if (currentStartTime >= lastEndTime) { + nonOverlappingCount++; + lastEndTime = currentEndTime; + selected.push(i); - // STEP: Comparison - newSteps.push({ + steps.push({ intervals: [...intervals], selected: [...selected], removed: [...removed], current: i, lastEndTime, - message: `Checking interval ${i}: Does its start time (${currentStartTime}) come after or at the last end time (${lastEndTime})?`, - lineNumber: 15, + explanation: `Yes! ${currentStartTime} >= ${lastEndTime - (intervals[i].end === lastEndTime ? 0 : 0)}. Select this interval and update the lastEndTime to ${lastEndTime}.`, + pseudoStep: `SET count = ${nonOverlappingCount}, end = ${lastEndTime}`, }); - - if (currentStartTime >= lastEndTime) { - nonOverlappingCount++; - lastEndTime = currentEndTime; - selected.push(i); - - newSteps.push({ - intervals: [...intervals], - selected: [...selected], - removed: [...removed], - current: i, - lastEndTime, - message: `Yes! ${currentStartTime} >= ${lastEndTime - (intervals[i - 1].end === lastEndTime ? 0 : 0)}. Update lastEndTime to ${lastEndTime} and increment count.`, - lineNumber: 17, - }); - } else { - removed.push(i); - newSteps.push({ - intervals: [...intervals], - selected: [...selected], - removed: [...removed], - current: i, - lastEndTime, - message: `No, ${currentStartTime} < ${lastEndTime}. This interval overlaps with our selected set, so we must remove it.`, - lineNumber: 15, - }); - } - } - - newSteps.push({ - intervals: [...intervals], - selected: [...selected], - removed: [...removed], - current: -1, - lastEndTime, - message: `Done! Maximum non-overlapping intervals = ${nonOverlappingCount}. Minimum removals = ${intervals.length} - ${nonOverlappingCount} = ${intervals.length - nonOverlappingCount}.`, - lineNumber: 21, - }); - - setSteps(newSteps); - }; - - useEffect(() => { - generateSteps(); - }, []); - - useEffect(() => { - if (isPlaying && currentStepIndex < steps.length - 1) { - intervalRef.current = window.setInterval(() => { - setCurrentStepIndex((prev) => { - if (prev >= steps.length - 1) { - setIsPlaying(false); - return prev; - } - return prev + 1; - }); - }, speed); + addLines(13, 10, 11, 13); } else { - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } + removed.push(i); + steps.push({ + intervals: [...intervals], + selected: [...selected], + removed: [...removed], + current: i, + lastEndTime, + explanation: `No! ${currentStartTime} < ${lastEndTime}. This activity overlaps with our selected set. We must remove it to resolve overlapping.`, + pseudoStep: `OVERLAP! Remove interval ${i}`, + }); + addLines(11, 8, 9, 11); } + } - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - }; - }, [isPlaying, currentStepIndex, steps.length, speed]); + steps.push({ + intervals: [...intervals], + selected: [...selected], + removed: [...removed], + current: -1, + lastEndTime, + explanation: `Completed loop. Total non-overlapping = ${nonOverlappingCount}. Minimum intervals to remove is total length (${intervals.length}) - non-overlapping (${nonOverlappingCount}) = ${intervals.length - nonOverlappingCount}.`, + pseudoStep: `RETURN total_len - count → ${intervals.length} - ${nonOverlappingCount} = ${intervals.length - nonOverlappingCount}`, + }); + addLines(16, 11, 14, 16); + + return { steps, stepLineNumbers }; +} + +export const ActivitySelectionVisualization: React.FC = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); - const handlePlay = () => setIsPlaying(true); - const handlePause = () => setIsPlaying(false); - const handleStepForward = () => { - if (currentStepIndex < steps.length - 1) - setCurrentStepIndex(currentStepIndex + 1); - }; - const handleStepBack = () => { - if (currentStepIndex > 0) setCurrentStepIndex(currentStepIndex - 1); - }; const handleReset = () => { setCurrentStepIndex(0); - setIsPlaying(false); }; - if (steps.length === 0) return null; - const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map((s) => s.pseudoStep); return ( -
- - -
-
-
-

Timeline Visualization

-
-
-
- Keep -
-
-
- Remove -
-
-
- Last End + +
+
+

Timeline Visualization

+
+
+
+ Keep +
+
+
+ Remove +
+
+
+ Last End +
-
-
- {/* Time markers */} -
- {[0, 2, 4, 6, 8, 10].map(t => {t})} -
+
+ {/* Time markers */} +
+ {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((t) => ( + {t} + ))} +
- {/* Intervals List */} -
- {currentStep.intervals.map((activity, idx) => { - const isCurrent = idx === currentStep.current; - const isSelected = currentStep.selected.includes(idx); - const isRemoved = currentStep.removed.includes(idx); - - return ( -
-
- I{idx} -
- {/* Background guide */} -
- - {/* Interval Box */} -
+ {currentStep.intervals.map((activity, idx) => { + const isCurrent = idx === currentStep.current; + const isSelected = currentStep.selected.includes(idx); + const isRemoved = currentStep.removed.includes(idx); + + return ( +
+
+ - {activity.start}-{activity.end} + I{idx} + +
+ {/* Background guide */} +
+ + {/* Interval Box */} +
+ + {activity.start}-{activity.end} + +
+ ); + })} +
+ + {/* lastEndTime Indicator */} + {currentStep.lastEndTime !== -1 && ( +
+
+ {currentStep.lastEndTime}
- ); - })} +
+ )}
+
- {/* lastEndTime Indicator */} - {currentStep.lastEndTime !== -1 && ( -
-
- {currentStep.lastEndTime} + {/* Commentary Panel */} +
+
+
+
+ + + + + + Algorithm Commentary + +
+
+ Step {currentStepIndex + 1} of {steps.length}
- )} -
-
-

{currentStep.message}

+
+
+ +
+
+

+ Current Action +

+
+ {currentStep.explanation} +
+
+
+
- - + } + controls={ + -
-
+ } + /> ); }; + +export default ActivitySelectionVisualization; diff --git a/src/components/visualizations/algorithms/AlienDictionaryVisualization.tsx b/src/components/visualizations/algorithms/AlienDictionaryVisualization.tsx index cbe09f9..ba73aec 100644 --- a/src/components/visualizations/algorithms/AlienDictionaryVisualization.tsx +++ b/src/components/visualizations/algorithms/AlienDictionaryVisualization.tsx @@ -1,9 +1,10 @@ -import { useState, useMemo } from 'react'; +import React, { useState, useMemo } from 'react'; import { Card } from '@/components/ui/card'; -import { VisualizationLayout } from '../shared/VisualizationLayout'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import { VisualizationLayout } from '../shared/VisualizationLayout'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; import { Info, CheckCircle2, ArrowRight } from 'lucide-react'; interface Step { @@ -18,308 +19,489 @@ interface Step { word1: string | null; word2: string | null; currentChar: string | null; - currentLoop: string; - message: string; - lineNumber: number; + explanation: string; isMatch?: boolean; + pseudoStep: string; } -export const AlienDictionaryVisualization = () => { - const [currentStepIndex, setCurrentStepIndex] = useState(0); - - const code = `function alienOrder(words: string[]): string { - const adj: Map = new Map(); - const inDegree: Map = new Map(); - +const languages: VisualizationLanguageMap = { + typescript: `function alienOrder(words: string[]): string { + const adj = new Map(); + const inDegree = new Map(); for (const word of words) { for (const char of word) { inDegree.set(char, 0); + adj.set(char, []); } } - for (let i = 0; i < words.length - 1; i++) { - const word1 = words[i]; - const word2 = words[i + 1]; - const minLength = Math.min(word1.length, word2.length); - - for (let j = 0; j < minLength; j++) { - if (word1[j] !== word2[j]) { - if (!adj.has(word1[j])) { - adj.set(word1[j], []); - } - - if (!adj.get(word1[j])!.includes(word2[j])) { - adj.get(word1[j])!.push(word2[j]); - inDegree.set(word2[j], inDegree.get(word2[j])! + 1); + const w1 = words[i]; + const w2 = words[i + 1]; + const len = Math.min(w1.length, w2.length); + let found = false; + for (let j = 0; j < len; j++) { + if (w1[j] !== w2[j]) { + if (!adj.get(w1[j])!.includes(w2[j])) { + adj.get(w1[j])!.push(w2[j]); + inDegree.set(w2[j], inDegree.get(w2[j])! + 1); } + found = true; break; } - if (j === minLength - 1 && word1.length > word2.length) { - return ""; - } } + if (!found && w1.length > w2.length) return ""; } - const queue: string[] = []; - for (const [char, degree] of inDegree) { - if (degree === 0) { - queue.push(char); - } + for (const [char, degree] of inDegree.entries()) { + if (degree === 0) queue.push(char); } - let result = ""; - let count = 0; - while (queue.length > 0) { const char = queue.shift()!; result += char; - count++; - - if (adj.has(char)) { - for (const neighbor of adj.get(char)!) { - inDegree.set(neighbor, inDegree.get(neighbor)! - 1); - if (inDegree.get(neighbor) === 0) { - queue.push(neighbor); - } + for (const neighbor of adj.get(char)!) { + inDegree.set(neighbor, inDegree.get(neighbor)! - 1); + if (inDegree.get(neighbor) === 0) { + queue.push(neighbor); } } } + return result.length === inDegree.size ? result : ""; +}`, - if (count !== inDegree.size) { - return ""; - } + python: `def alienOrder(words: list[str]) -> str: + adj = {c: set() for w in words for c in w} + inDegree = {c: 0 for w in words for c in w} + for i in range(len(words) - 1): + w1, w2 = words[i], words[i + 1] + minLen = min(len(w1), len(w2)) + if len(w1) > len(w2) and w1[:minLen] == w2[:minLen]: + return "" + for j in range(minLen): + if w1[j] != w2[j]: + if w2[j] not in adj[w1[j]]: + adj[w1[j]].add(w2[j]) + inDegree[w2[j]] += 1 + break + queue = [c for c in inDegree if inDegree[c] == 0] + result = [] + while queue: + c = queue.pop(0) + result.append(c) + for neighbor in adj[c]: + inDegree[neighbor] -= 1 + if inDegree[neighbor] == 0: + queue.append(neighbor) + if len(result) < len(inDegree): + return "" + return "".join(result)`, - return result; -}`; + java: `public static class Solution { + public String alienOrder(String[] words) { + Map> adj = new HashMap<>(); + Map inDegree = new HashMap<>(); + for (String word : words) { + for (char c : word.toCharArray()) { + inDegree.put(c, 0); + adj.put(c, new HashSet<>()); + } + } + for (int i = 0; i < words.length - 1; i++) { + String w1 = words[i]; + String w2 = words[i + 1]; + int minLen = Math.min(w1.length(), w2.length()); + if (w1.length() > w2.length() && w1.substring(0, minLen).equals(w2.substring(0, minLen))) { + return ""; + } + for (int j = 0; j < minLen; j++) { + char parent = w1.charAt(j); + char child = w2.charAt(j); + if (parent != child) { + if (!adj.get(parent).contains(child)) { + adj.get(parent).add(child); + inDegree.put(child, inDegree.get(child) + 1); + } + break; + } + } + } + Queue queue = new LinkedList<>(); + for (char c : inDegree.keySet()) { + if (inDegree.get(c) == 0) { + queue.offer(c); + } + } + StringBuilder sb = new StringBuilder(); + while (!queue.isEmpty()) { + char c = queue.poll(); + sb.append(c); + for (char neighbor : adj.get(c)) { + inDegree.put(neighbor, inDegree.get(neighbor) - 1); + if (inDegree.get(neighbor) == 0) { + queue.offer(neighbor); + } + } + } + return sb.length() == inDegree.size() ? sb.toString() : ""; + } +}`, + + cpp: `class Solution { +public: + string alienOrder(vector& words) { + unordered_map> adj; + unordered_map inDegree; + for (const string& w : words) { + for (char c : w) { + inDegree[c] = 0; + adj[c] = unordered_set(); + } + } + for (size_t i = 0; i < words.size() - 1; i++) { + string w1 = words[i], w2 = words[i + 1]; + size_t minLen = min(w1.size(), w2.size()); + if (w1.size() > w2.size() && w1.compare(0, minLen, w2, 0, minLen) == 0) { + return ""; + } + for (size_t j = 0; j < minLen; j++) { + char parent = w1[j], child = w2[j]; + if (parent != child) { + if (!adj[parent].count(child)) { + adj[parent].insert(child); + inDegree[child]++; + } + break; + } + } + } + queue q; + for (auto const& [c, degree] : inDegree) { + if (degree == 0) q.push(c); + } + string result = ""; + while (!q.empty()) { + char c = q.front(); q.pop(); + result += c; + for (char neighbor : adj[c]) { + inDegree[neighbor]--; + if (inDegree[neighbor] == 0) q.push(neighbor); + } + } + return result.size() == inDegree.size() ? result : ""; + } +};` +}; + +export const AlienDictionaryVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + + const { steps, stepLineNumbers } = useMemo(() => { + const stepsList: Step[] = []; + const stepLines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLines.typescript!.push(ts); + stepLines.python!.push(py); + stepLines.java!.push(java); + stepLines.cpp!.push(cpp); + }; - const steps: Step[] = useMemo(() => { - const s: Step[] = []; const words = ["wrt", "wrf", "er", "ett", "rftt"]; - - const adj: Map = new Map(); - const inDegree: Map = new Map(); + const adj = new Map(); + const inDegree = new Map(); const queue: string[] = []; let result = ""; let count = 0; - const snap = ( - msg: string, line: number, isMatch: boolean = false, - i: number | null = null, j: number | null = null, - word1: string | null = null, word2: string | null = null, - currentChar: string | null = null, currentLoop: string = "" + const makeSnapshot = ( + msg: string, + pseudo: string, + ts: number, + py: number, + java: number, + cpp: number, + isMatch: boolean = false, + i: number | null = null, + j: number | null = null, + word1: string | null = null, + word2: string | null = null, + currentChar: string | null = null ) => { - s.push({ + stepsList.push({ words: [...words], adj: Object.fromEntries(adj), inDegree: Object.fromEntries(inDegree), queue: [...queue], result, count, - i, j, word1, word2, currentChar, currentLoop, - message: msg, - lineNumber: line, - isMatch + i, j, word1, word2, currentChar, + explanation: msg, + isMatch, + pseudoStep: pseudo }); + addLines(ts, py, java, cpp); }; - snap("Initialize adjacency list (adj) and inDegree Map.", 2, false); + // Step 1: Start + makeSnapshot("Start the Alien Dictionary algorithm to determine character ordering.", "START alienOrder(words)", 1, 1, 2, 3); - snap("Iterate over all words to initialize unique characters in inDegree Map to 0.", 5, false, null, null, null, null, null, "init"); + // Step 2: Init + makeSnapshot("Initialize adjacency list and in-degree maps.", "SET adj = {}, inDegree = {}", 2, 2, 3, 4); + + // Populate unique characters for (const word of words) { for (const char of word) { if (!inDegree.has(char)) { - inDegree.set(char, 0); + inDegree.set(char, 0); + adj.set(char, []); } } - snap(`Setup tracking for unique letters found in word "${word}".`, 7, false, null, null, null, null, null, "init"); } + makeSnapshot("Initialize all unique characters in the dictionary with an in-degree of 0.", "FOR char in words → inDegree[char] = 0", 4, 3, 5, 6); - snap("Start comparing adjacent words to construct the directed graph edges.", 11, false, null, null, null, null, null, "build"); + // Build graph edges for (let i = 0; i < words.length - 1; i++) { - const word1 = words[i]; - const word2 = words[i + 1]; - const minLength = Math.min(word1.length, word2.length); - - snap(`Comparing adjacent words: "${word1}" and "${word2}"`, 12, false, i, null, word1, word2, null, "build"); - - for (let j = 0; j < minLength; j++) { - snap(`Compare characters at index ${j}: '${word1[j]}' vs '${word2[j]}'`, 16, false, i, j, word1, word2, null, "build"); - - if (word1[j] !== word2[j]) { - const c1 = word1[j]; - const c2 = word2[j]; - - if (!adj.has(c1)) { - adj.set(c1, []); - snap(`First time seeing '${c1}' as a source. Initializing its adjacency row.`, 19, false, i, j, word1, word2, c1, "build"); - } - - if (!adj.get(c1)!.includes(c2)) { - adj.get(c1)!.push(c2); - inDegree.set(c2, inDegree.get(c2)! + 1); - snap(`Found a difference! Add directed edge ${c1} -> ${c2} to Graph and increment inDegree for ${c2}.`, 24, true, i, j, word1, word2, c1, "build"); - } else { - snap(`Mismatch ${c1} != ${c2} found, but edge ${c1} -> ${c2} already exists! Skipping edge.`, 22, false, i, j, word1, word2, c1, "build"); - } - - snap(`Words resolved. Break character loop and proceed to next adjacent word pair.`, 26, false, i, j, word1, word2, null, "build"); - break; - } + const w1 = words[i]; + const w2 = words[i + 1]; + const len = Math.min(w1.length, w2.length); + let found = false; + + makeSnapshot(`Compare adjacent words: "${w1}" and "${w2}"`, `FOR i = ${i} (Compare "${w1}" and "${w2}")`, 10, 4, 11, 12, false, i, null, w1, w2); + + for (let j = 0; j < len; j++) { + const c1 = w1[j]; + const c2 = w2[j]; + makeSnapshot(`Compare character index ${j}: '${c1}' vs '${c2}'`, `IF word1[${j}] != word2[${j}]`, 16, 10, 21, 20, false, i, j, w1, w2); + + if (c1 !== c2) { + if (!adj.get(c1)!.includes(c2)) { + adj.get(c1)!.push(c2); + inDegree.set(c2, inDegree.get(c2)! + 1); + makeSnapshot( + `First difference found: '${c1}' must come before '${c2}'. Add edge ${c1} → ${c2} and increment in-degree of '${c2}'.`, + `adj[${c1}].add(${c2}) & inDegree[${c2}]++`, + 18, 12, 23, 22, true, i, j, w1, w2, c1 + ); + } + found = true; + makeSnapshot(`Edge established. Stop comparing characters for this pair.`, "BREAK character loop", 22, 14, 26, 25, false, i, j, w1, w2); + break; } + } + + if (!found && w1.length > w2.length) { + makeSnapshot( + `Invalid dictionary order! "${w1}" has prefix "${w2}" but is longer. Return empty string.`, + `IF w1 has prefix w2 AND len(w1) > len(w2) → RETURN ""`, + 25, 7, 15, 15, true, i, null, w1, w2 + ); + return { steps: stepsList, stepLineNumbers: stepLines }; + } } - snap("Identify characters completely ready with NO dependencies (InDegree === 0).", 34, false, null, null, null, null, null, "bfs"); + // Roots for (const [char, degree] of inDegree.entries()) { - if (degree === 0) { - queue.push(char); - snap(`Character '${char}' has inDegree 0. Pushing to roots Queue.`, 37, true, null, null, null, null, char, "bfs"); - } + if (degree === 0) { + queue.push(char); + } } + makeSnapshot("Enqueue all characters with 0 in-degree (no dependencies).", "FOR c in inDegree IF degree == 0 → enqueue(c)", 28, 15, 31, 30); - snap("Commence Topological Sort using BFS processing roots.", 41, false, null, null, null, null, null, "bfs"); - + // BFS while (queue.length > 0) { - snap(`Queue has elements: [${queue.join(', ')}]`, 44, false, null, null, null, null, queue[0], "bfs"); - - const char = queue.shift()!; - result += char; - count++; - - snap(`Shifted '${char}' from queue. Appended to Result.`, 46, true, null, null, null, null, char, "bfs"); - - if (adj.has(char)) { - const neighbors = adj.get(char)!; - snap(`Fetching downstream edges starting from '${char}': [${neighbors.join(', ')}]`, 49, false, null, null, null, null, char, "bfs"); - for (const neighbor of neighbors) { - inDegree.set(neighbor, inDegree.get(neighbor)! - 1); - snap(`Decremented InDegree constraint on '${neighbor}'. Remaining: ${inDegree.get(neighbor)}`, 51, true, null, null, null, null, neighbor, "bfs"); - - if (inDegree.get(neighbor) === 0) { - queue.push(neighbor); - snap(`Constraint removed! In-Degree of '${neighbor}' reached 0. Enqueue '${neighbor}'.`, 53, true, null, null, null, null, neighbor, "bfs"); - } - } + const char = queue.shift()!; + result += char; + count++; + + makeSnapshot( + `Pop character '${char}' from queue. Add to sorted result and increment visited count.`, + `DEQUEUE '${char}' & result += '${char}'`, + 33, 18, 38, 35, true, null, null, null, null, char + ); + + const neighbors = adj.get(char) || []; + for (const neighbor of neighbors) { + inDegree.set(neighbor, inDegree.get(neighbor)! - 1); + const nextDegree = inDegree.get(neighbor)!; + + makeSnapshot( + `Decrement in-degree of neighbor '${neighbor}' to ${nextDegree}.`, + `inDegree[${neighbor}]-- → ${nextDegree}`, + 36, 21, 41, 38, false, null, null, null, null, neighbor + ); + + if (nextDegree === 0) { + queue.push(neighbor); + makeSnapshot( + `In-degree of neighbor '${neighbor}' reached 0. Add it to the queue.`, + `IF inDegree[${neighbor}] == 0 → enqueue(${neighbor})`, + 38, 23, 43, 39, true, null, null, null, null, neighbor + ); } + } } - snap(`Topological sort complete! Validated count ${count} nodes against size ${inDegree.size}.`, 59, false, null, null, null, null, null, "bfs"); - snap(`Successfully verified full Alien lexical order mapping: "${result}"`, 63, true, null, null, null, null, null, "bfs"); + const isValid = result.length === inDegree.size; + makeSnapshot( + `Topological sort finished. Visited ${result.length} characters of ${inDegree.size} unique characters. Is it valid? ${isValid ? "YES" : "NO"}`, + `RETURN result.length == size ? result : ""`, + 42, 26, 47, 42, true, null, null, null, null, null + ); - return s; + return { steps: stepsList, stepLineNumbers: stepLines }; }, []); + const handleReset = () => { + setCurrentStepIndex(0); + }; + const step = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( - -

- Dictionary Analysis +
+ +

+ Alien Dictionary Words

-
- {step?.words.map((word, wIdx) => { - const isActivePair = step.i !== null && (wIdx === step.i || wIdx === step.i + 1); - return ( -
- {word.split('').map((ch, charIdx) => { - const isComparing = isActivePair && step.j === charIdx; - const isMismatch = isComparing && step.isMatch; - return ( -
- {ch} -
- ) - })} +
+ {step?.words.map((word, wIdx) => { + const isActivePair = step.i !== null && (wIdx === step.i || wIdx === step.i + 1); + return ( +
+ {word.split('').map((ch, charIdx) => { + const isComparing = isActivePair && step.j === charIdx; + const isMismatch = isComparing && step.isMatch; + return ( +
+ {ch}
- ); - })} - {step?.i !== null && ( -
- )} + ) + })} +
+ ); + })} + {step?.i !== null && ( +
+ )}
- +
-
-

- InDegree -

-
- {step?.inDegree && Object.entries(step.inDegree).map(([char, degree]) => ( -
-
- {char} -
- {degree} -
- ))} +
+

+ In-Degree Constraints +

+
+ {step?.inDegree && Object.entries(step.inDegree).map(([char, degree]) => ( +
+
+ {char} +
+ {degree}
+ ))}
-
-

- Graph (Adj) -

-
- {step?.adj && Object.entries(step.adj).map(([char, neighbors]) => ( -
-
- {char} -
- -
- {neighbors.length === 0 && none} - {neighbors.map((n, idx) => ( -
- {n} -
- ))} -
-
- ))} +
+ +
+

+ Graph (Adjacency) +

+
+ {step?.adj && Object.entries(step.adj).map(([char, neighbors]) => ( +
+
+ {char} +
+ +
+ {neighbors.length === 0 && none} + {neighbors.map((n, idx) => ( +
+ {n} +
+ ))} +
+ ))}
+
- -
-
- {step?.isMatch ? : } + {/* Commentary Panel */} + +
+
+
+ + + + + + Algorithm Commentary + +
+
+ Step {currentStepIndex + 1} of {steps.length} +
-
-

- Step Logic -

-

- {step?.message || ''} -

+ +
+
+ {step?.isMatch ? : } +
+
+

+ Current Action +

+
+ {step?.explanation || ''} +
+
-
- } - rightContent={ -
- +
} + rightContent={ + + } controls={ ; } +const languages: VisualizationLanguageMap = { + typescript: `function levelOrder(root: TreeNode | null): number[][] { + if (!root) return []; + const result: number[][] = []; + const queue: TreeNode[] = [root]; + while (queue.length > 0) { + const levelSize = queue.length; + const level: number[] = []; + for (let i = 0; i < levelSize; i++) { + const node = queue.shift()!; + level.push(node.val); + if (node.left) queue.push(node.left); + if (node.right) queue.push(node.right); + } + result.push(level); + } + return result; +}`, + + python: `from collections import deque +def levelOrder(root: TreeNode) -> list[list[int]]: + if not root: + return [] + result = [] + queue = deque([root]) + while queue: + level_size = len(queue) + level = [] + for _ in range(level_size): + node = queue.popleft() + level.append(node.val) + if node.left: + queue.append(node.left) + if node.right: + queue.append(node.right) + result.append(level) + return result`, + + java: `public static class Solution{ + public List> levelOrder(TreeNode root) { + List> result = new ArrayList<>(); + if (root == null) { + return result; + } + Queue queue = new LinkedList<>(); + queue.offer(root); + while (!queue.isEmpty()) { + int levelSize = queue.size(); + List currentLevel = new ArrayList<>(); + for (int i = 0; i < levelSize; i++) { + TreeNode node = queue.poll(); + currentLevel.add(node.val); + if (node.left != null) { + queue.offer(node.left); + } + if (node.right != null) { + queue.offer(node.right); + } + } + result.add(currentLevel); + } + return result; + } +}`, + + cpp: `class Solution { +public: + vector < vector < int >> levelOrder(TreeNode * root) { + if (!root) return {}; + vector < vector < int >> result; + queue < TreeNode *> q; + q.push(root); + while (!q.empty()) { + int levelSize = q.size(); + vector < int > level; + for (int i = 0; i < levelSize; i++) { + TreeNode * node = q.front(); + q.pop(); + level.push_back(node -> val); + if (node -> left) q.push(node -> left); + if (node -> right) q.push(node -> right); + } + result.push_back(level); + } + return result; + } +};`, +}; + export const BFSLevelOrderVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); const [tree, setTree] = useState(null); - const intervalRef = useRef(null); - - const code = `function levelOrder(root) { - if (!root) return []; - - const result = []; - const queue = [root]; - - while (queue.length > 0) { - const levelSize = queue.length; - const level = []; - - for (let i = 0; i < levelSize; i++) { - const node = queue.shift(); - level.push(node.val); - - if (node.left) queue.push(node.left); - if (node.right) queue.push(node.right); - } - - result.push(level); - } - - return result; -}`; const createTree = (): TreeNode => { return { @@ -86,80 +147,88 @@ export const BFSLevelOrderVisualization = () => { calculatePositions(root, 200, 50, 80); setTree(root); - const newSteps: Step[] = []; + const steps: Step[] = []; const visited: number[] = []; const queue: TreeNode[] = [root]; - newSteps.push({ - visited: [], - queue: [root.val], - current: null, - currentLevel: 0, - message: 'Start BFS Level Order: Use queue to traverse level by level', - lineNumber: 5 - }); + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + const addStep = (currentNode: number | null, lvl: number, msg: string, pseudo: string, ts_l: number, py_l: number, java_l: number, cpp_l: number) => { + steps.push({ + currentNode, + currentLevel: lvl, + queue: queue.map(n => n.val), + visited: [...visited], + message: msg, + pseudoStep: pseudo, + variables: { + currentNode: currentNode ?? 'null', + level: lvl, + queue: `[${queue.map(n => n.val).join(', ')}]`, + visited: `[${visited.join(', ')}]` + } + }); + addLines(ts_l, py_l, java_l, cpp_l); + }; + + // Initial root check + addStep(null, 0, 'Check if tree root is null (it is not).', 'if (!root) → NO', 2, 3, 4, 4); + // Initialize result + addStep(null, 0, 'Initialize result array.', 'result = []', 3, 5, 3, 5); + // Enqueue root + addStep(null, 0, 'Enqueue the root node to start BFS.', 'queue = [root]', 4, 6, 8, 7); let level = 0; while (queue.length > 0) { + addStep(null, level, `Queue is not empty. Process level ${level}.`, 'while (queue not empty) → YES', 5, 7, 9, 8); + const levelSize = queue.length; + addStep(null, level, `Level size is ${levelSize}. We will process ${levelSize} nodes at this level.`, `levelSize = ${levelSize}`, 6, 8, 10, 9); for (let i = 0; i < levelSize; i++) { const node = queue.shift()!; visited.push(node.val); - newSteps.push({ - visited: [...visited], - queue: queue.map(n => n.val), - current: node.val, - currentLevel: level, - message: `Process node ${node.val} at level ${level}`, - lineNumber: 13 - }); + addStep(node.val, level, `Dequeue node ${node.val} from queue.`, 'node = queue.shift()', 9, 11, 13, 12); + addStep(node.val, level, `Add node ${node.val} value to current level list.`, `level.push(${node.val})`, 10, 12, 14, 14); if (node.left) { queue.push(node.left); - newSteps.push({ - visited: [...visited], - queue: queue.map(n => n.val), - current: node.val, - currentLevel: level, - message: `Add left child ${node.left.val} to queue`, - lineNumber: 16 - }); + addStep(node.val, level, `Enqueue left child node ${node.left.val}.`, `queue.push(node.left)`, 11, 14, 16, 15); } if (node.right) { queue.push(node.right); - newSteps.push({ - visited: [...visited], - queue: queue.map(n => n.val), - current: node.val, - currentLevel: level, - message: `Add right child ${node.right.val} to queue`, - lineNumber: 17 - }); + addStep(node.val, level, `Enqueue right child node ${node.right.val}.`, `queue.push(node.right)`, 12, 16, 19, 16); } } + addStep(null, level, `Level ${level} traversal finished. Append current level list to result.`, 'result.push(level)', 14, 17, 22, 18); level++; } - newSteps.push({ - visited, - queue: [], - current: null, - currentLevel: level, - message: `Complete! Visited: [${visited.join(', ')}]`, - lineNumber: 23 - }); + addStep(null, level, `Queue is empty. Return final level order traversal: [${visited.join(', ')}]`, 'return result', 16, 18, 24, 20); - setSteps(newSteps); - setCurrentStepIndex(0); + return { steps, stepLineNumbers }; }; - useEffect(() => { - generateSteps(); - }, []); + const [{ steps, stepLineNumbers }] = useState(generateSteps); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { @@ -187,16 +256,20 @@ export const BFSLevelOrderVisualization = () => { const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); - generateSteps(); }; if (steps.length === 0 || !tree) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); const renderTree = (node: TreeNode | null): JSX.Element | null => { if (!node || node.x === undefined || node.y === undefined) return null; + const isVisited = currentStep.visited.includes(node.val); + const isCurrent = currentStep.currentNode === node.val; + const isInQueue = currentStep.queue.includes(node.val); + return ( {node.left && node.left.x !== undefined && node.left.y !== undefined && ( @@ -208,12 +281,12 @@ export const BFSLevelOrderVisualization = () => { { y={node.y} textAnchor="middle" dy=".3em" - className={`font- ${currentStep.visited.includes(node.val) || currentStep.current === node.val || currentStep.queue.includes(node.val) - ? 'fill-white' - : 'fill-foreground' - }`} + className={`text-sm font-semibold ${isVisited || isCurrent || isInQueue ? 'fill-white' : 'fill-foreground'}`} > {node.val} @@ -253,6 +323,7 @@ export const BFSLevelOrderVisualization = () => { />
+ {/* Left: visual tree + commentary box + variable panel */}
@@ -260,54 +331,54 @@ export const BFSLevelOrderVisualization = () => {
-
-

{currentStep.message}

-
-
-
-

Queue:

+
+

Queue

{currentStep.queue.map((val, idx) => ( -
+
{val}
))} + {currentStep.queue.length === 0 &&
Empty
}
-
-

Visited:

-
+
+

Visited

+
{currentStep.visited.map((val, idx) => ( -
+
{val}
))} + {currentStep.visited.length === 0 &&
Empty
}
- -
-
-
-
- - -
- - +
+

{currentStep.message}

+ +
+ {/* Right: code / pseudocode panel */} +
); diff --git a/src/components/visualizations/algorithms/BSTInsertVisualization.tsx b/src/components/visualizations/algorithms/BSTInsertVisualization.tsx index 81ae401..9818d2e 100644 --- a/src/components/visualizations/algorithms/BSTInsertVisualization.tsx +++ b/src/components/visualizations/algorithms/BSTInsertVisualization.tsx @@ -1,8 +1,8 @@ import { useEffect, useRef, useState } from 'react'; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface TreeNode { val: number; @@ -17,24 +17,16 @@ interface Step { current: number | null; insertValue: number; message: string; - lineNumber: number; + pseudoStep: string; variables: Record; } -export const BSTInsertVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const intervalRef = useRef(null); - - const code = `function insertIntoBST(root: TreeNode | null, val: number): TreeNode | null { +const languages: VisualizationLanguageMap = { + typescript: `function insertIntoBST(root: TreeNode | null, val: number): TreeNode | null { if (!root) { return new TreeNode(val); } - let cur: TreeNode = root; - while (true) { if (val > cur.val) { if (!cur.right) { @@ -50,7 +42,80 @@ export const BSTInsertVisualization = () => { cur = cur.left; } } -}`; +}`, + + python: `def insertIntoBST(root, val): + if not root: + return TreeNode(val) + cur = root + while True: + if val > cur.val: + if not cur.right: + cur.right = TreeNode(val) + return root + cur = cur.right + else: + if not cur.left: + cur.left = TreeNode(val) + return root + cur = cur.left`, + + java: `public static class Solution { + public TreeNode insertIntoBST(TreeNode root, int val) { + if (root == null) { + return new TreeNode(val); + } + TreeNode cur = root; + while (true) { + if (val > cur.val) { + if (cur.right == null) { + cur.right = new TreeNode(val); + return root; + } + cur = cur.right; + } else { + if (cur.left == null) { + cur.left = new TreeNode(val); + return root; + } + cur = cur.left; + } + } + } +}`, + + cpp: `class Solution { +public: + TreeNode* insertIntoBST(TreeNode* root, int val) { + if (root == nullptr) { + return new TreeNode(val); + } + TreeNode* cur = root; + while (true) { + if (val > cur->val) { + if (cur->right == nullptr) { + cur->right = new TreeNode(val); + return root; + } + cur = cur->right; + } + else { + if (cur->left == nullptr) { + cur->left = new TreeNode(val); + return root; + } + cur = cur->left; + } + } + } +};`, +}; + +export const BSTInsertVisualization = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); const deepClone = (node: TreeNode | null): TreeNode | null => { if (!node) return null; @@ -87,181 +152,103 @@ export const BSTInsertVisualization = () => { }; const val = 5; - const newSteps: Step[] = []; + const steps: Step[] = []; const tree = deepClone(initialTree); calculatePositions(tree, 200, 40, 80); - // Step 1: Function entry - newSteps.push({ - tree: deepClone(tree), - current: null, - insertValue: val, - message: `Starting insertion of value ${val} into BST.`, - lineNumber: 1, - variables: { val, root: 'TreeNode' } - }); - - // Step 2: Check if !root - newSteps.push({ - tree: deepClone(tree), - current: null, - insertValue: val, - message: `Checking if the tree is empty.`, - lineNumber: 2, - variables: { val, root: tree ? 'TreeNode' : 'null' } - }); + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - if (!tree) { - // (This case won't be hit with our initialTree but for completeness) - newSteps.push({ - tree: { val, left: null, right: null, x: 200, y: 40 }, - current: val, + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + const addStep = (currentNode: number | null, activeTree: TreeNode | null, msg: string, pseudo: string, ts_l: number, py_l: number, java_l: number, cpp_l: number) => { + steps.push({ + tree: deepClone(activeTree), + current: currentNode, insertValue: val, - message: `Tree is empty. Creating new root with value ${val}.`, - lineNumber: 3, - variables: { val } + message: msg, + pseudoStep: pseudo, + variables: { + val, + root: activeTree ? 'TreeNode' : 'null', + 'cur.val': currentNode ?? 'null' + } }); - setSteps(newSteps); - return; + addLines(ts_l, py_l, java_l, cpp_l); + }; + + // 1. Function entry + addStep(null, tree, `Starting insertion of value ${val} into BST.`, `START insertIntoBST(root, val = ${val})`, 1, 1, 2, 3); + + // 2. Check if !root + addStep(null, tree, `Checking if the tree is empty.`, 'if (!root)', 2, 2, 3, 4); + + if (!tree) { + addStep(val, { val, left: null, right: null, x: 200, y: 40 }, `Tree is empty. Creating new root with value ${val}.`, 'return new TreeNode(val)', 3, 3, 4, 5); + return { steps, stepLineNumbers }; } - // Step 6: Initialize cur + // 3. Initialize cur let cur = tree; - newSteps.push({ - tree: deepClone(tree), - current: cur.val, - insertValue: val, - message: `Initialize 'cur' pointer to the root (${cur.val}).`, - lineNumber: 6, - variables: { val, 'cur.val': cur.val } - }); + addStep(cur.val, tree, `Initialize 'cur' pointer to the root (${cur.val}).`, 'cur = root', 5, 4, 6, 7); while (true) { - // Step 8: While(true) - newSteps.push({ - tree: deepClone(tree), - current: cur.val, - insertValue: val, - message: `Starting tree traversal from node ${cur.val}.`, - lineNumber: 8, - variables: { val, 'cur.val': cur.val } - }); + // while(true) + addStep(cur.val, tree, `Traversing from node ${cur.val}.`, 'while (true)', 6, 5, 7, 8); - // Step 9: Check val > cur.val - newSteps.push({ - tree: deepClone(tree), - current: cur.val, - insertValue: val, - message: `Comparing ${val} with current node value ${cur.val}.`, - lineNumber: 9, - variables: { val, 'cur.val': cur.val, condition: `${val} > ${cur.val}` } - }); + // Compare val > cur.val + addStep(cur.val, tree, `Compare insert value ${val} with current node value ${cur.val}.`, `if (val > cur.val) → ${val} > ${cur.val} → ${val > cur.val ? 'YES ✓' : 'NO ✗'}`, 7, 6, 8, 9); if (val > cur.val) { - // Step 10: Check !cur.right - newSteps.push({ - tree: deepClone(tree), - current: cur.val, - insertValue: val, - message: `Check if node ${cur.val} has a right child.`, - lineNumber: 10, - variables: { val, 'cur.val': cur.val, 'cur.right': cur.right ? cur.right.val : 'null' } - }); + // Check !cur.right + addStep(cur.val, tree, `Check if node ${cur.val} has a right child.`, 'if (!cur.right)', 8, 7, 9, 10); if (!cur.right) { - // Step 11: Insert right + // Insert right cur.right = { val, left: null, right: null }; calculatePositions(tree, 200, 40, 80); - newSteps.push({ - tree: deepClone(tree), - current: cur.val, - insertValue: val, - message: `Right child is null. Inserting ${val} as right child of ${cur.val}.`, - lineNumber: 11, - variables: { val, 'cur.val': cur.val, 'new node': val } - }); - // Step 12: Return root - newSteps.push({ - tree: deepClone(tree), - current: null, - insertValue: val, - message: `Insertion complete. Returning root.`, - lineNumber: 12, - variables: { val } - }); + addStep(cur.val, tree, `Right child is null. Inserting ${val} as right child of ${cur.val}.`, `cur.right = new TreeNode(${val})`, 9, 8, 10, 11); + // Return root + addStep(null, tree, `Insertion complete. Returning root of BST.`, 'return root', 10, 9, 11, 12); break; } - // Step 14: cur = cur.right + // cur = cur.right cur = cur.right; - newSteps.push({ - tree: deepClone(tree), - current: cur.val, - insertValue: val, - message: `${val} > previous node value. Moving to right child (${cur.val}).`, - lineNumber: 14, - variables: { val, 'cur.val': cur.val } - }); + addStep(cur.val, tree, `${val} > previous node value. Moving to right child (${cur.val}).`, 'cur = cur.right', 12, 10, 13, 14); } else { - // Step 16: Check !cur.left - newSteps.push({ - tree: deepClone(tree), - current: cur.val, - insertValue: val, - message: `Check if node ${cur.val} has a left child.`, - lineNumber: 16, - variables: { val, 'cur.val': cur.val, 'cur.left': cur.left ? cur.left.val : 'null' } - }); + // Check !cur.left + addStep(cur.val, tree, `Check if node ${cur.val} has a left child.`, 'if (!cur.left)', 14, 12, 15, 17); if (!cur.left) { - // Step 17: Insert left + // Insert left cur.left = { val, left: null, right: null }; calculatePositions(tree, 200, 40, 80); - newSteps.push({ - tree: deepClone(tree), - current: cur.val, - insertValue: val, - message: `Left child is null. Inserting ${val} as left child of ${cur.val}.`, - lineNumber: 17, - variables: { val, 'cur.val': cur.val, 'new node': val } - }); - // Step 18: Return root - newSteps.push({ - tree: deepClone(tree), - current: null, - insertValue: val, - message: `Insertion complete. Returning root.`, - lineNumber: 18, - variables: { val } - }); + addStep(cur.val, tree, `Left child is null. Inserting ${val} as left child of ${cur.val}.`, `cur.left = new TreeNode(${val})`, 15, 13, 16, 18); + // Return root + addStep(null, tree, `Insertion complete. Returning root of BST.`, 'return root', 16, 14, 17, 19); break; } - // Step 20: cur = cur.left + // cur = cur.left cur = cur.left; - newSteps.push({ - tree: deepClone(tree), - current: cur.val, - insertValue: val, - message: `${val} <= previous node value. Moving to left child (${cur.val}).`, - lineNumber: 20, - variables: { val, 'cur.val': cur.val } - }); + addStep(cur.val, tree, `${val} <= previous node value. Moving to left child (${cur.val}).`, 'cur = cur.left', 18, 15, 19, 21); } } - setSteps(newSteps); - setCurrentStepIndex(0); + return { steps, stepLineNumbers }; }; - useEffect(() => { - generateSteps(); - }, []); + const [{ steps, stepLineNumbers }] = useState(generateSteps); useEffect(() => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } - if (isPlaying && currentStepIndex < steps.length - 1) { intervalRef.current = setInterval(() => { setCurrentStepIndex(prev => { @@ -271,14 +258,12 @@ export const BSTInsertVisualization = () => { } return prev + 1; }); - }, 1000 / speed); + }, 1200 / speed); + } else { + if (intervalRef.current) clearInterval(intervalRef.current); } - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } + if (intervalRef.current) clearInterval(intervalRef.current); }; }, [isPlaying, currentStepIndex, steps.length, speed]); @@ -289,12 +274,12 @@ export const BSTInsertVisualization = () => { const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); - generateSteps(); }; if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); const renderTree = (node: TreeNode | null): JSX.Element | null => { if (!node || node.x === undefined || node.y === undefined) return null; @@ -326,11 +311,11 @@ export const BSTInsertVisualization = () => { { y={node.y} textAnchor="middle" dy=".3em" - className={`font-medium transition-colors duration-300 ${currentStep.current === node.val || node.val === currentStep.insertValue - ? 'fill-primary-foreground' + className={`text-sm font-semibold transition-colors duration-300 ${currentStep.current === node.val || node.val === currentStep.insertValue + ? 'fill-white' : 'fill-foreground' }`} > @@ -369,28 +354,35 @@ export const BSTInsertVisualization = () => { />
+ {/* Left: visual tree + commentary box + variable panel */}
-
- +
+ {currentStep.tree && renderTree(currentStep.tree)}
-
+

{currentStep.message}

-
- -
+
- + {/* Right: code / pseudocode panel */} +
); diff --git a/src/components/visualizations/algorithms/BellmanFordVisualization.tsx b/src/components/visualizations/algorithms/BellmanFordVisualization.tsx index e4ab0bd..ee3bdec 100644 --- a/src/components/visualizations/algorithms/BellmanFordVisualization.tsx +++ b/src/components/visualizations/algorithms/BellmanFordVisualization.tsx @@ -1,8 +1,8 @@ import { useEffect, useRef, useState } from 'react'; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Edge { from: number; @@ -16,190 +16,276 @@ interface Step { edges: Edge[]; currentEdge: Edge | null; iteration: number; - message: string; - lineNumber: number; + explanation: string; + pseudoStep: string; + variables: Record; } -export const BellmanFordVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const intervalRef = useRef(null); +const languages: VisualizationLanguageMap = { + typescript: `function findCheapestPrice(n: number, flights: number[][], src: number, dst: number, k: number): number { + const prices: number[] = new Array(n).fill(Infinity); + prices[src] = 0; + for (let i = 0; i <= k; i++) { + const tmpPrices = [...prices]; + for (const [s, d, p] of flights) { + if (prices[s] === Infinity) continue; + if (prices[s] + p < tmpPrices[d]) { + tmpPrices[d] = prices[s] + p; + } + } + for (let j = 0; j < n; j++) { + prices[j] = tmpPrices[j]; + } + } + return prices[dst] === Infinity ? -1 : prices[dst]; +}`, + python: `def findCheapestPrice(n: int, flights: list[list[int]], src: int, dst: int, k: int) -> int: + prices = [float('inf')] * n + prices[src] = 0 + for i in range(k + 1): + tmp_prices = prices[:] + for s, d, p in flights: + if prices[s] == float('inf'): + continue + if prices[s] + p < tmp_prices[d]: + tmp_prices[d] = prices[s] + p + prices = tmp_prices[:] + return prices[dst] if prices[dst] != float('inf') else -1`, + java: `public static class Solution { + public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) { + int[] prices = new int[n]; + Arrays.fill(prices, Integer.MAX_VALUE); + prices[src] = 0; + for (int i = 0; i <= k; i++) { + int[] tmpPrices = Arrays.copyOf(prices, n); + for (int[] flight : flights) { + int s = flight[0]; + int d = flight[1]; + int p = flight[2]; + if (prices[s] == Integer.MAX_VALUE) continue; + if (prices[s] + p < tmpPrices[d]) { + tmpPrices[d] = prices[s] + p; + } + } + prices = Arrays.copyOf(tmpPrices, n); + } + return prices[dst] == Integer.MAX_VALUE ? -1 : prices[dst]; + } +}`, + cpp: `class Solution { +public: + int findCheapestPrice(int n, vector>& flights, int src, int dst, int k) { + vector prices(n, INT_MAX); + prices[src] = 0; + for (int i = 0; i <= k; i++) { + vector tmpPrices = prices; + for (auto& flight : flights) { + int s = flight[0]; + int d = flight[1]; + int p = flight[2]; + if (prices[s] == INT_MAX) continue; + if (prices[s] + p < tmpPrices[d]) { + tmpPrices[d] = prices[s] + p; + } + } + prices = tmpPrices; + } + return prices[dst] == INT_MAX ? -1 : prices[dst]; + } +};`, +}; - const code = `function findCheapestPrice(n, flights, src, dst, k) { - const prices = new Array(n).fill(Infinity); - prices[src] = 0; +function generateVisualizationData() { + const n = 4; + const src = 0; + const dst = 3; + const k = 1; + const edges: Edge[] = [ + { from: 0, to: 1, weight: 100 }, + { from: 1, to: 2, weight: 100 }, + { from: 0, to: 2, weight: 500 }, + { from: 2, to: 3, weight: 100 } + ]; - for (let i = 0; i <= k; i++) { - const tmpPrices = [...prices]; + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { typescript: [], python: [], java: [], cpp: [] }; + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - for (const [s, d, p] of flights) { - if (prices[s] === Infinity) continue; + let prices = Array(n).fill(Infinity); - if (prices[s] + p < tmpPrices[d]) { - tmpPrices[d] = prices[s] + p; - } - } - - for (let j = 0; j < n; j++) { - prices[j] = tmpPrices[j]; - } - } + steps.push({ + prices: [...prices], + tmpPrices: [...prices], + edges, + currentEdge: null, + iteration: -1, + explanation: `Initialize prices array with Infinity for all nodes.`, + pseudoStep: `SET prices = [Infinity] * ${n}`, + variables: { iteration: 'Init', prices: '[]', tmpPrices: '[]' } + }); + addLines(2, 2, 4, 4); - return prices[dst] === Infinity ? -1 : prices[dst]; -}`; - - const generateSteps = () => { - const n = 4; - const src = 0; - const dst = 3; - const k = 1; - const edges: Edge[] = [ - { from: 0, to: 1, weight: 100 }, - { from: 1, to: 2, weight: 100 }, - { from: 0, to: 2, weight: 500 }, - { from: 2, to: 3, weight: 100 } - ]; - - const newSteps: Step[] = []; - let prices = Array(n).fill(Infinity); - prices[src] = 0; + prices[src] = 0; + steps.push({ + prices: [...prices], + tmpPrices: [...prices], + edges, + currentEdge: null, + iteration: -1, + explanation: `Set the price to reach the source node ${src} to 0.`, + pseudoStep: `SET prices[src] = 0 → prices[0] = 0`, + variables: { iteration: 'Init', prices: `[${prices.map(p => p === Infinity ? '∞' : p).join(', ')}]` } + }); + addLines(3, 3, 5, 5); - newSteps.push({ + for (let i = 0; i <= k; i++) { + let tmpPrices = [...prices]; + steps.push({ prices: [...prices], - tmpPrices: [...prices], + tmpPrices: [...tmpPrices], edges, currentEdge: null, - iteration: -1, - message: `Initialize prices: src ${src} is 0, others are Infinity`, - lineNumber: 1 + iteration: i, + explanation: `Iteration ${i} (up to ${i} stops). Copy prices array to tmpPrices.`, + pseudoStep: `FOR i = ${i} (stops <= ${k}): SET tmpPrices = [...prices]`, + variables: { iteration: i, prices: `[${prices.map(p => p === Infinity ? '∞' : p).join(', ')}]`, tmpPrices: `[${tmpPrices.map(p => p === Infinity ? '∞' : p).join(', ')}]` } }); + addLines(5, 5, 7, 7); - for (let i = 0; i <= k; i++) { - let tmpPrices = [...prices]; - - newSteps.push({ + for (const edge of edges) { + steps.push({ prices: [...prices], tmpPrices: [...tmpPrices], edges, - currentEdge: null, + currentEdge: edge, iteration: i, - message: `Iteration ${i} (up to ${i} stops): Copy prices to tmpPrices`, - lineNumber: 6 + explanation: `Inspect flight edge (${edge.from} → ${edge.to}) with price ${edge.weight}. Check if source ${edge.from} is reachable.`, + pseudoStep: `FOR flight IN flights: check if prices[${edge.from}] != Infinity`, + variables: { iteration: i, s: edge.from, d: edge.to, p: edge.weight, prices: `[${prices.map(p => p === Infinity ? '∞' : p).join(', ')}]`, tmpPrices: `[${tmpPrices.map(p => p === Infinity ? '∞' : p).join(', ')}]` } }); + addLines(6, 6, 8, 8); - for (const edge of edges) { - newSteps.push({ + if (prices[edge.from] === Infinity) { + steps.push({ prices: [...prices], tmpPrices: [...tmpPrices], edges, currentEdge: edge, iteration: i, - message: `Check flight ${edge.from}→${edge.to} (price ${edge.weight})`, - lineNumber: 8 + explanation: `Source node ${edge.from} is currently unreachable. Skip flight.`, + pseudoStep: `IF prices[${edge.from}] == Infinity → CONTINUE`, + variables: { iteration: i, s: edge.from, d: edge.to } }); - - if (prices[edge.from] === Infinity) { - newSteps.push({ - prices: [...prices], - tmpPrices: [...tmpPrices], - edges, - currentEdge: edge, - iteration: i, - message: `Node ${edge.from} is unreachable, skipping...`, - lineNumber: 9 - }); - continue; - } - - const newPrice = prices[edge.from] + edge.weight; - if (newPrice < tmpPrices[edge.to]) { - tmpPrices[edge.to] = newPrice; - newSteps.push({ - prices: [...prices], - tmpPrices: [...tmpPrices], - edges, - currentEdge: edge, - iteration: i, - message: `Found cheaper path to ${edge.to} via ${edge.from}: ${newPrice}`, - lineNumber: 12 - }); - } + addLines(7, 8, 12, 12); + continue; } - for (let j = 0; j < n; j++) { - prices[j] = tmpPrices[j]; - } + const newCost = prices[edge.from] + edge.weight; + const isCheaper = newCost < tmpPrices[edge.to]; - newSteps.push({ + steps.push({ prices: [...prices], tmpPrices: [...tmpPrices], edges, - currentEdge: null, + currentEdge: edge, iteration: i, - message: `End of iteration ${i}: Update prices from tmpPrices`, - lineNumber: 17 + explanation: `Check if price via ${edge.from} (${prices[edge.from]} + ${edge.weight} = ${newCost}) is cheaper than current tmpPrices[${edge.to}] (${tmpPrices[edge.to] === Infinity ? '∞' : tmpPrices[edge.to]}).`, + pseudoStep: `IF prices[${edge.from}] + ${edge.weight} < tmpPrices[${edge.to}] → ${isCheaper ? 'YES ✓' : 'NO ✗'}`, + variables: { iteration: i, s: edge.from, d: edge.to, newCost, currentDestCost: tmpPrices[edge.to] === Infinity ? '∞' : tmpPrices[edge.to] } }); + addLines(8, 9, 13, 13); + + if (isCheaper) { + tmpPrices[edge.to] = newCost; + steps.push({ + prices: [...prices], + tmpPrices: [...tmpPrices], + edges, + currentEdge: edge, + iteration: i, + explanation: `Cheaper path found! Update tmpPrices[${edge.to}] to ${newCost}.`, + pseudoStep: `SET tmpPrices[${edge.to}] = ${newCost}`, + variables: { iteration: i, s: edge.from, d: edge.to, tmpPrices: `[${tmpPrices.map(p => p === Infinity ? '∞' : p).join(', ')}]` } + }); + addLines(9, 10, 14, 14); + } + } + + for (let j = 0; j < n; j++) { + prices[j] = tmpPrices[j]; } - newSteps.push({ + steps.push({ prices: [...prices], - tmpPrices: [...prices], + tmpPrices: [...tmpPrices], edges, currentEdge: null, - iteration: k + 1, - message: `Algorithm complete. Cheapest price to ${dst}: ${prices[dst] === Infinity ? -1 : prices[dst]}`, - lineNumber: 21 + iteration: i, + explanation: `Update prices array with tmpPrices at the end of iteration ${i}.`, + pseudoStep: `prices = tmpPrices`, + variables: { iteration: i, prices: `[${prices.map(p => p === Infinity ? '∞' : p).join(', ')}]` } }); + addLines(13, 11, 17, 17); + } - setSteps(newSteps); - setCurrentStepIndex(0); - }; + steps.push({ + prices: [...prices], + tmpPrices: [...prices], + edges, + currentEdge: null, + iteration: k, + explanation: `Iterations completed. Check if destination ${dst} is reachable. Price is ${prices[dst] === Infinity ? 'Infinity' : prices[dst]}.`, + pseudoStep: `RETURN prices[dst] == Infinity ? -1 : prices[dst] → ${prices[dst] === Infinity ? -1 : prices[dst]}`, + variables: { prices: `[${prices.map(p => p === Infinity ? '∞' : p).join(', ')}]`, finalPrice: prices[dst] === Infinity ? -1 : prices[dst] } + }); + addLines(16, 12, 19, 19); + + return { steps, stepLineNumbers }; +} - useEffect(() => { - generateSteps(); - }, []); +const nodePositions = [ + { x: 50, y: 110 }, + { x: 175, y: 50 }, + { x: 175, y: 170 }, + { x: 300, y: 110 } +]; + +export const BellmanFordVisualization = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { intervalRef.current = setInterval(() => { setCurrentStepIndex(prev => { - if (prev >= steps.length - 1) { - setIsPlaying(false); - return prev; - } + if (prev >= steps.length - 1) { setIsPlaying(false); return prev; } return prev + 1; }); }, 1000 / speed); } else { if (intervalRef.current) clearInterval(intervalRef.current); } - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - }; + return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [isPlaying, currentStepIndex, steps.length, speed]); const handlePlay = () => setIsPlaying(true); const handlePause = () => setIsPlaying(false); - const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(prev => prev + 1); - const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(prev => prev - 1); - const handleReset = () => { - setCurrentStepIndex(0); - setIsPlaying(false); - generateSteps(); - }; + const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(p => p + 1); + const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(p => p - 1); + const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); }; if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; - - const nodePositions = [ - { x: 50, y: 110 }, - { x: 175, y: 50 }, - { x: 175, y: 170 }, - { x: 300, y: 110 } - ]; + const pseudoSteps = steps.map(s => s.pseudoStep); return (
@@ -218,18 +304,18 @@ export const BellmanFordVisualization = () => {
-
- +
+ - + @@ -247,14 +333,13 @@ export const BellmanFordVisualization = () => { y1={from.y} x2={to.x} y2={to.y} - className={`transition-all duration-300 ${isCurrent ? 'stroke-primary stroke-[3px]' : 'stroke-border stroke-[2px]' - }`} + className={`transition-all duration-200 ${isCurrent ? 'stroke-primary stroke-3' : 'stroke-border stroke-2'}`} markerEnd="url(#arrowhead)" /> {edge.weight} @@ -263,54 +348,60 @@ export const BellmanFordVisualization = () => { ); })} - {nodePositions.map((pos, idx) => ( - - - - {idx} - - - {currentStep.tmpPrices[idx] === Infinity ? '∞' : currentStep.tmpPrices[idx]} - - - ))} + {nodePositions.map((pos, idx) => { + const isReached = currentStep.prices[idx] !== Infinity; + return ( + + + + {idx} + + + {currentStep.tmpPrices[idx] === Infinity ? '∞' : currentStep.tmpPrices[idx]} + + + ); + })}
-

{currentStep.message}

-
-
- p === Infinity ? '∞' : p).join(', '), - 'Tmp Prices': currentStep.tmpPrices.map(p => p === Infinity ? '∞' : p).join(', ') - }} - /> +

{currentStep.explanation}

-
-
- + p === Infinity ? '∞' : p).join(', '), + 'Tmp Prices': currentStep.tmpPrices.map(p => p === Infinity ? '∞' : p).join(', '), + ...currentStep.variables + }} + />
+ +
); diff --git a/src/components/visualizations/algorithms/BestTimeToBuyAndSellStockVisualization.tsx b/src/components/visualizations/algorithms/BestTimeToBuyAndSellStockVisualization.tsx index 02ae08e..64f2e40 100644 --- a/src/components/visualizations/algorithms/BestTimeToBuyAndSellStockVisualization.tsx +++ b/src/components/visualizations/algorithms/BestTimeToBuyAndSellStockVisualization.tsx @@ -1,269 +1,288 @@ -import { useEffect, useRef, useState } from 'react'; -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; -import { StepControls } from '../shared/StepControls'; +import { useState } from 'react'; +import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import { VisualizationLayout } from '../shared/VisualizationLayout'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; +import { Card } from '@/components/ui/card'; interface Step { array: number[]; highlights: number[]; - minIndex: number; - currentIndex: number; variables: Record; explanation: string; - lineNumber: number; + pseudoStep: string; } -export const BestTimeToBuyAndSellStockVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const intervalRef = useRef(null); +const languages: VisualizationLanguageMap = { + typescript: `function maxProfit(prices: number[]): number { + let l = 0; + let r = 1; + let maxProfit = 0; + while (r < prices.length) { + if (prices[l] < prices[r]) { + maxProfit = Math.max(maxProfit, prices[r] - prices[l]); + } else { + l = r; + } + r++; + } + return maxProfit; +}`, + + python: `def maxProfit(prices: List[int]) -> int: + l = 0 + r = 1 + max_profit = 0 + while r < len(prices): + if prices[l] < prices[r]: + max_profit = max(max_profit, prices[r] - prices[l]) + else: + l = r + r += 1 + return max_profit`, + + java: `public static class Solution { + public int maxProfit(int[] prices) { + int l = 0; + int r = 1; + int maxProfit = 0; + while (r < prices.length) { + if (prices[l] < prices[r]) { + maxProfit = Math.max(maxProfit, prices[r] - prices[l]); + } else { + l = r; + } + r++; + } + return maxProfit; + } +}`, + + cpp: `class Solution { +public: + int maxProfit(vector& prices) { + int l = 0; + int r = 1; + int maxProfit = 0; + while (r < prices.size()) { + if (prices[l] < prices[r]) { + int profit = prices[r] - prices[l]; + maxProfit = max(maxProfit, profit); + } else { + l = r; + } + r++; + } + return maxProfit; + } +};`, +}; + +function generateVisualizationData() { + const prices = [7, 1, 5, 3, 6, 4]; + const steps: Step[] = []; + + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - const code = `function maxProfit(prices: number[]): number { let l = 0; let r = 1; let maxProfit = 0; - - while(r < prices.length){ - if(prices[l] < prices[r]){ - maxProfit = Math.max(maxProfit, prices[r] - prices[l]); - } else { - l = r; - } - r = r + 1; - } - return maxProfit; -}`; - const generateSteps = () => { - const prices = [7, 1, 5, 3, 6, 4]; - const newSteps: Step[] = []; + // Init L + steps.push({ + array: [...prices], + highlights: [l], + variables: { l, r: '-', maxProfit: '-' }, + explanation: 'Initialize left pointer l = 0 (potential buy day)', + pseudoStep: 'SET l = 0' + }); + addLines(2, 2, 3, 4); - let l = 0; - let r = 1; - let maxProfit = 0; + // Init R + steps.push({ + array: [...prices], + highlights: [l, r], + variables: { l, r, maxProfit: '-' }, + explanation: 'Initialize right pointer r = 1 (potential sell day)', + pseudoStep: 'SET r = 1' + }); + addLines(3, 3, 4, 5); - newSteps.push({ - array: [...prices], - highlights: [l], - minIndex: l, - currentIndex: -1, - variables: { l, r: '-', maxProfit: '-' }, - explanation: 'Initialize left pointer l to 0', - lineNumber: 2 - }); + // Init maxProfit + steps.push({ + array: [...prices], + highlights: [l, r], + variables: { l, r, maxProfit }, + explanation: 'Initialize maxProfit to 0', + pseudoStep: 'SET maxProfit = 0' + }); + addLines(4, 4, 5, 6); - newSteps.push({ + while (r < prices.length) { + steps.push({ array: [...prices], highlights: [l, r], - minIndex: l, - currentIndex: r, - variables: { l, r, maxProfit: '-' }, - explanation: 'Initialize right pointer r to 1', - lineNumber: 3 + variables: { l, r, maxProfit }, + explanation: `Check loop condition: r (${r}) < prices.length (${prices.length})`, + pseudoStep: `WHILE r (${r}) < prices.length (${prices.length}) → YES ✓` }); + addLines(5, 5, 6, 7); - newSteps.push({ + steps.push({ array: [...prices], highlights: [l, r], - minIndex: l, - currentIndex: r, - variables: { l, r, maxProfit }, - explanation: 'Initialize maxProfit to 0', - lineNumber: 4 + variables: { l, r, maxProfit, 'prices[l]': prices[l], 'prices[r]': prices[r] }, + explanation: `Check if prices[l] ($${prices[l]}) < prices[r] ($${prices[r]}) (is buying at day l and selling at day r profitable?)`, + pseudoStep: `IF prices[l] ($${prices[l]}) < prices[r] ($${prices[r]}) → ${prices[l] < prices[r] ? 'YES ✓' : 'NO ✗'}` }); + addLines(6, 6, 7, 8); - while (r < prices.length) { - newSteps.push({ - array: [...prices], - highlights: [l, r], - minIndex: l, - currentIndex: r, - variables: { l, r, maxProfit }, - explanation: `Check loop condition: r (${r}) < prices.length (${prices.length})`, - lineNumber: 6 - }); + if (prices[l] < prices[r]) { + const profit = prices[r] - prices[l]; + const oldMaxProfit = maxProfit; + maxProfit = Math.max(maxProfit, profit); - newSteps.push({ + steps.push({ array: [...prices], highlights: [l, r], - minIndex: l, - currentIndex: r, - variables: { l, r, maxProfit, 'prices[l]': prices[l], 'prices[r]': prices[r] }, - explanation: `Check if prices[l] (${prices[l]}) < prices[r] (${prices[r]})`, - lineNumber: 7 + variables: { l, r, maxProfit, profit }, + explanation: `Profitable! Calculate profit = prices[r] - prices[l] = $${prices[r]} - $${prices[l]} = $${profit}. Update maxProfit = max(${oldMaxProfit}, ${profit}) = $${maxProfit}.`, + pseudoStep: `SET maxProfit = MAX(maxProfit, prices[r] - prices[l]) → MAX(${oldMaxProfit}, ${profit}) = ${maxProfit}` }); - - if (prices[l] < prices[r]) { - const profit = prices[r] - prices[l]; - const oldMaxProfit = maxProfit; - maxProfit = Math.max(maxProfit, profit); - - newSteps.push({ - array: [...prices], - highlights: [l, r], - minIndex: l, - currentIndex: r, - variables: { l, r, maxProfit, profit }, - explanation: `prices[l] < prices[r], profit = ${profit}. Update maxProfit = max(${oldMaxProfit}, ${profit}) = ${maxProfit}`, - lineNumber: 8 - }); - } else { - l = r; - newSteps.push({ - array: [...prices], - highlights: [l, r], - minIndex: l, - currentIndex: r, - variables: { l, r, maxProfit }, - explanation: `prices[l] >= prices[r], move left pointer l to r (${r})`, - lineNumber: 10 - }); - } - - newSteps.push({ + addLines(7, 7, 8, 9); + } else { + l = r; + steps.push({ array: [...prices], highlights: [l, r], - minIndex: l, - currentIndex: r, variables: { l, r, maxProfit }, - explanation: `Increment r to ${r + 1}`, - lineNumber: 12 + explanation: `Not profitable (buying at $${prices[l]} >= selling at $${prices[r]}). Shift left pointer l to r (${r}) to buy at this lower price.`, + pseudoStep: `ELSE: l = r → l = ${r}` }); - r = r + 1; + addLines(9, 9, 10, 12); } - newSteps.push({ + steps.push({ array: [...prices], - highlights: [l], - minIndex: l, - currentIndex: r, + highlights: [l, r], variables: { l, r, maxProfit }, - explanation: `Check loop condition: r (${r}) < prices.length (${prices.length}). False, loop ends.`, - lineNumber: 6 - }); - - newSteps.push({ - array: [...prices], - highlights: [], - minIndex: -1, - currentIndex: -1, - variables: { maxProfit }, - explanation: `Return maxProfit = ${maxProfit}`, - lineNumber: 14 + explanation: `Increment right pointer r to examine the next day's price.`, + pseudoStep: `SET r = r + 1 → ${r + 1}` }); + addLines(11, 10, 12, 14); + r++; + } - setSteps(newSteps); - setCurrentStepIndex(0); - }; + steps.push({ + array: [...prices], + highlights: [], + variables: { l, r, maxProfit }, + explanation: `Check loop condition: r (${r}) < prices.length (${prices.length}). False, exit loop.`, + pseudoStep: `WHILE r (${r}) < prices.length (${prices.length}) → NO ✗` + }); + addLines(5, 5, 6, 7); - useEffect(() => { - generateSteps(); - }, []); + steps.push({ + array: [...prices], + highlights: [], + variables: { maxProfit }, + explanation: `Return the maximum profit found: $${maxProfit}`, + pseudoStep: `RETURN maxProfit → ${maxProfit}` + }); + addLines(13, 11, 14, 16); - useEffect(() => { - if (isPlaying && currentStepIndex < steps.length - 1) { - intervalRef.current = setInterval(() => { - setCurrentStepIndex(prev => { - if (prev >= steps.length - 1) { - setIsPlaying(false); - return prev; - } - return prev + 1; - }); - }, 1000 / speed); - } else { - if (intervalRef.current) clearInterval(intervalRef.current); - } - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - }; - }, [isPlaying, currentStepIndex, steps.length, speed]); + return { steps, stepLineNumbers }; +} - const handlePlay = () => setIsPlaying(true); - const handlePause = () => setIsPlaying(false); - const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(prev => prev + 1); - const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(prev => prev - 1); - const handleReset = () => { - setCurrentStepIndex(0); - setIsPlaying(false); - generateSteps(); - }; +export const BestTimeToBuyAndSellStockVisualization = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); const maxPrice = Math.max(...currentStep.array); return ( -
- - -
-
-
-
- {currentStep.array.map((value, idx) => { - // In our generated steps variables: - // l is stored in variables.l - // r is stored in variables.r - const l = currentStep.variables.l; - const r = currentStep.variables.r; - - const isL = idx === l; - const isR = idx === r; + +
+

+ Best Time to Buy and Sell Stock (Sliding Window) +

+ +
+ {currentStep.array.map((value, idx) => { + const l = currentStep.variables.l; + const r = currentStep.variables.r; + const isL = idx === l; + const isR = idx === r; - return ( -
- {isL &&
L
} - {isR &&
R
} -
+ {isL &&
L
} + {isR &&
R
} +
- ${value} - {idx} -
- ); - })} -
+ style={{ height: `${(value / maxPrice) * 100}%`, minHeight: '20px' }} + /> + ${value} + {idx} +
+ ); + })} +
+
-
-

{currentStep.explanation}

+
+ +

+ Commentary +

+

+ {currentStep.explanation} +

+
+
- -
-

Strategy: Sliding Window / Two Pointers

-
-

• Initialize Left (buy) at 0 and Right (sell) at 1

-

• Determine if strictly profitable (prices[l] < prices[r])

-

• If profitable, update maxProfit (prices[r] - prices[l])

-

• If not profitable, move Left pointer to Right

-

• Always increment Right pointer

-
-
- -
- - -
-
+ } + rightContent={ + setCurrentStepIndex(0)} + /> + } + controls={ + + } + /> ); }; diff --git a/src/components/visualizations/algorithms/BinaryLiftingVisualization.tsx b/src/components/visualizations/algorithms/BinaryLiftingVisualization.tsx index 9f09983..6ca3306 100644 --- a/src/components/visualizations/algorithms/BinaryLiftingVisualization.tsx +++ b/src/components/visualizations/algorithms/BinaryLiftingVisualization.tsx @@ -1,16 +1,17 @@ import { useState, useMemo } from 'react'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; import { motion, AnimatePresence } from 'framer-motion'; import { Card } from '@/components/ui/card'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { up: number[][]; depth: number[]; - highlightedLines: number[]; explanation: string; + pseudoStep: string; variables: Record; activeNodes: number[]; phase: 'precompute' | 'kthAncestor' | 'lca' | 'done'; @@ -29,16 +30,12 @@ const TREE: number[][] = [ ]; const ROOT = 0; -export const BinaryLiftingVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); - - const code = `const LOG = 4; - +const languages: VisualizationLanguageMap = { + typescript: `const LOG = 4; function binaryLifting(graph: number[][], root: number) { const n = graph.length; const up: number[][] = Array(n).fill(null).map(() => Array(LOG).fill(-1)); const depth: number[] = Array(n).fill(0); - function dfs(node: number, parent: number) { up[node][0] = parent; for (let j = 1; j < LOG; j++) { @@ -53,9 +50,7 @@ function binaryLifting(graph: number[][], root: number) { } } } - dfs(root, -1); - function kthAncestor(node: number, k: number): number { for (let j = 0; j < LOG; j++) { if ((k & (1 << j)) !== 0) { @@ -65,7 +60,6 @@ function binaryLifting(graph: number[][], root: number) { } return node; } - function lca(u: number, v: number): number { if (depth[u] < depth[v]) [u, v] = [v, u]; let diff = depth[u] - depth[v]; @@ -79,73 +73,269 @@ function binaryLifting(graph: number[][], root: number) { } return up[u][0]; } - return { up, depth, kthAncestor, lca }; -}`; +}`, + + python: `import math +def solve_lca(graph, node=None, k=None, node1=None, node2=None): + n = len(graph) + LOG = 4 + up = [[-1] * LOG for _ in range(n)] + depth = [0] * n + def dfs(node, parent): + up[node][0] = parent + for j in range(1, LOG): + if up[node][j - 1] != -1: + up[node][j] = up[up[node][j - 1]][j - 1] + for child in graph[node]: + if child != parent: + depth[child] = depth[node] + 1 + dfs(child, node) + dfs(0, -1) + def kth_ancestor(node, k): + for j in range(LOG): + if (k & (1 << j)) != 0: + node = up[node][j] + if node == -1: break + return node + def lca(u, v): + if depth[u] < depth[v]: + u, v = v, u + diff = depth[u] - depth[v] + u = kth_ancestor(u, diff) + if u == v: return u + for j in range(LOG - 1, -1, -1): + if up[u][j] != up[v][j]: + u = up[u][j] + v = up[v][j] + return up[u][0] + if node is not None and k is not None: + return kth_ancestor(node, k) + elif node1 is not None and node2 is not None: + return lca(node1, node2) + return None`, + + java: `public static class Solution { + static class TreeAncestor { + private int[][] up; + private int[] depth; + private int LOG = 4; + public TreeAncestor(int n, int[] parent) { + up = new int[n][LOG]; + depth = new int[n]; + List> graph = new ArrayList<>(); + for (int i = 0; i < n; i++) { + graph.add(new ArrayList<>()); + } + for (int i = 0; i < n; i++) { + if (parent[i] != -1) { + graph.get(i).add(parent[i]); + graph.get(parent[i]).add(i); + } + Arrays.fill(up[i], -1); + } + int root = 0; + for (int i = 0; i < n; ++i) { + if (parent[i] == -1) { + root = i; + break; + } + } + dfs(root, -1, graph); + } + private void dfs(int node, int parent, List> graph) { + up[node][0] = parent; + for (int j = 1; j < LOG; j++) { + if (up[node][j - 1] != -1) { + up[node][j] = up[up[node][j - 1]][j - 1]; + } + } + for (int child = 0; child < graph.get(node).size(); child++) { + int realChild = graph.get(node).get(child); + if (realChild != parent) { + depth[realChild] = depth[node] + 1; + dfs(realChild, node, graph); + } + } + } + public int kthAncestor(int node, int k) { + for (int j = 0; j < LOG; j++) { + if ((k & (1 << j)) != 0) { + node = up[node][j]; + if (node == -1) break; + } + } + return node; + } + public int lca(int u, int v) { + if (depth[u] < depth[v]) { + int temp = u; + u = v; + v = temp; + } + int diff = depth[u] - depth[v]; + u = kthAncestor(u, diff); + if (u == v) return u; + for (int j = LOG - 1; j >= 0; j--) { + if (up[u][j] != up[v][j]) { + u = up[u][j]; + v = up[v][j]; + } + } + return up[u][0]; + } + } +}`, + + cpp: `class Solution { +public: +class BinaryLifting { +private: + int n, LOG = 4; + vector> up; + vector depth; + void dfs(int node, int parent, vector>& graph) { + up[node][0] = parent; + for (int j = 1; j < LOG; j++) { + if (up[node][j - 1] != -1) { + up[node][j] = up[up[node][j - 1]][j - 1]; + } + } + for (int child : graph[node]) { + if (child != parent) { + depth[child] = depth[node] + 1; + dfs(child, node, graph); + } + } + } +public: + BinaryLifting(vector>& graph, int root = 0) { + n = graph.size(); + up.assign(n, vector(LOG, -1)); + depth.assign(n, 0); + dfs(root, -1, graph); + } + int kthAncestor(int node, int k) { + for (int j = 0; j < LOG; j++) { + if ((k & (1 << j)) != 0) { + node = up[node][j]; + if (node == -1) break; + } + } + return node; + } + int lca(int u, int v) { + if (depth[u] < depth[v]) swap(u, v); + int diff = depth[u] - depth[v]; + u = kthAncestor(u, diff); + if (u == v) return u; + for (int j = LOG - 1; j >= 0; j--) { + if (up[u][j] != up[v][j]) { + u = up[u][j]; + v = up[v][j]; + } + } + return up[u][0]; + } +}; +};`, +}; + +export const BinaryLiftingVisualization = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); - const steps: Step[] = useMemo(() => { + const { steps, stepLineNumbers } = useMemo(() => { const s: Step[] = []; const n = TREE.length; const up: number[][] = Array(n).fill(null).map(() => Array(LOG).fill(-1)); const depth: number[] = Array(n).fill(0); - s.push({ - up: up.map(row => [...row]), depth: [...depth], - highlightedLines: [3, 4, 5, 6], - explanation: "Initialize binary lifting table 'up' and 'depth' array.", - variables: { n, LOG, root: ROOT }, - activeNodes: [], - phase: 'precompute' - }); + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - const dfs = (node: number, parent: number) => { + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + const snap = ( + currentNode: number, + activeEdge: [number, number] | null, + explanation: string, + pseudoStep: string, + ts: number, py: number, java: number, cpp: number, + variables: Record, + phase: Step['phase'] + ) => { s.push({ up: up.map(row => [...row]), depth: [...depth], - highlightedLines: [8, 9], - explanation: `DFS visiting node ${node}. Set its immediate parent up[${node}][0] = ${parent}.`, - variables: { node, parent, depth: depth[node] }, - activeNodes: [node], - phase: 'precompute' + explanation, + pseudoStep, + variables, + activeNodes: currentNode !== -1 ? [currentNode] : [], + phase, }); + addLines(ts, py, java, cpp); + }; + + snap( + -1, null, + "Initialize binary lifting table 'up' and 'depth' array.", + "SET up = Table(n, LOG), depth = Array(n).fill(0)", + 3, 3, 4, 34, + { n, LOG, root: ROOT }, 'precompute' + ); + + const dfs = (node: number, parent: number) => { + snap( + node, null, + `DFS visiting node ${node}. Set its immediate parent up[${node}][0] = ${parent}.`, + `dfs(node = ${node}, parent = ${parent}) : SET up[${node}][0] = ${parent}`, + 6, 7, 33, 7, + { node, parent, depth: depth[node] }, 'precompute' + ); up[node][0] = parent; for (let j = 1; j < LOG; j++) { - s.push({ - up: up.map(row => [...row]), depth: [...depth], - highlightedLines: [10, 11], - explanation: `Compute 2^${j}-th ancestor for node ${node}.`, - variables: { node, j, '2^j': 1 << j, 'up[node][j-1]': up[node][j - 1] }, - activeNodes: [node], - phase: 'precompute' - }); + snap( + node, null, + `Compute 2^${j}-th ancestor (up[${node}][${j}]) using dynamic programming.`, + `FOR j = 1 to LOG-1`, + 8, 9, 34, 8, + { node, j, '2^j': 1 << j, 'up[node][j-1]': up[node][j - 1] }, 'precompute' + ); if (up[node][j - 1] !== -1) { const midway = up[node][j - 1]; const ancestor = up[midway][j - 1]; up[node][j] = ancestor; - s.push({ - up: up.map(row => [...row]), depth: [...depth], - highlightedLines: [12], - explanation: `up[${node}][${j}] = up[up[${node}][${j - 1}]][${j - 1}] = up[${midway}][${j - 1}] = ${ancestor}.`, - variables: { node, j, midway, ancestor }, - activeNodes: [node, midway, ancestor].filter(id => id !== -1), - phase: 'precompute' - }); + snap( + node, null, + `up[${node}][${j}] = up[up[${node}][${j - 1}]][${j - 1}] = up[${midway}][${j - 1}] = ${ancestor}.`, + `SET up[${node}][${j}] = up[${midway}][${j - 1}] → ${ancestor}`, + 10, 11, 36, 10, + { node, j, midway, ancestor }, 'precompute' + ); } } for (const child of TREE[node]) { if (child !== parent) { depth[child] = depth[node] + 1; - s.push({ - up: up.map(row => [...row]), depth: [...depth], - highlightedLines: [15, 16, 17], - explanation: `Recurse to child ${child}. Set depth[${child}] = ${depth[child]}.`, - variables: { node, child, 'depth[child]': depth[child] }, - activeNodes: [node, child], - phase: 'precompute' - }); + snap( + node, null, + `Recurse to child ${child}. Set depth[${child}] = depth[${node}] + 1 = ${depth[child]}.`, + `IF child (${child}) != parent (${parent}): depth[${child}] = ${depth[child]}; CALL dfs(${child})`, + 13, 12, 39, 13, + { node, child, 'depth[child]': depth[child] }, 'precompute' + ); dfs(child, node); } } @@ -158,62 +348,57 @@ function binaryLifting(graph: number[][], root: number) { const startNode = node; if (!isLcaCall) { - s.push({ - up: up.map(row => [...row]), depth: [...depth], - highlightedLines: [25], - explanation: `kthAncestor(node=${node}, k=${k}): Start finding the ${k}-th ancestor.`, - variables: { node, k }, - activeNodes: [node], - phase: 'kthAncestor' - }); + snap( + node, null, + `kthAncestor(node=${node}, k=${k}): Start finding the ${k}-th ancestor.`, + `CALL kthAncestor(node = ${node}, k = ${k})`, + 21, 17, 53, 38, + { node, k }, 'kthAncestor' + ); } for (let j = 0; j < LOG; j++) { const bitSet = (k & (1 << j)) !== 0; - s.push({ - up: up.map(row => [...row]), depth: [...depth], - highlightedLines: [26, 27], - explanation: `Checking bit ${j} (2^${j} = ${1 << j}) of k=${k}. It is ${bitSet ? 'set' : 'not set'}.`, - variables: { curr, k, j, '2^j': 1 << j, bitSet }, - activeNodes: [curr], - phase: isLcaCall ? 'lca' : 'kthAncestor' - }); + snap( + curr, null, + `Checking if bit ${j} (value ${1 << j}) of k=${k} is set: ${bitSet ? 'YES' : 'NO'}.`, + `FOR j = 0 to LOG-1: IF (k & 2^j (${1 << j})) != 0 → ${bitSet ? 'YES' : 'NO'}`, + 22, 18, 54, 39, + { curr, k, j, '2^j': 1 << j, bitSet }, isLcaCall ? 'lca' : 'kthAncestor' + ); if (bitSet) { const prev = curr; curr = up[curr][j]; - s.push({ - up: up.map(row => [...row]), depth: [...depth], - highlightedLines: [28], - explanation: `Bit ${j} is set. Jump 2^${j} steps from ${prev} to ${curr}.`, - variables: { curr, k, j, prev }, - activeNodes: [curr !== -1 ? curr : prev], - phase: isLcaCall ? 'lca' : 'kthAncestor' - }); + snap( + curr, null, + `Bit ${j} is set. Lift node from ${prev} to its 2^${j}-th ancestor: ${curr}.`, + `SET node = up[node][j] → ${curr}`, + 24, 20, 55, 41, + { curr, k, j, prev }, isLcaCall ? 'lca' : 'kthAncestor' + ); if (curr === -1) { - s.push({ - up: up.map(row => [...row]), depth: [...depth], - highlightedLines: [29], - explanation: `Reached beyond root. break.`, - variables: { curr, k }, - activeNodes: [], - phase: isLcaCall ? 'lca' : 'kthAncestor' - }); + snap( + curr, null, + `Reached beyond the root (null ancestor). Break.`, + `IF node == -1 → BREAK`, + 25, 21, 56, 42, + { curr, k }, isLcaCall ? 'lca' : 'kthAncestor' + ); break; } } } if (!isLcaCall) { - s.push({ - up: up.map(row => [...row]), depth: [...depth], - highlightedLines: [32], - explanation: `Finished kthAncestor. The ${k}-th ancestor of ${startNode} is ${curr}.`, - variables: { startNode, k, result: curr }, - activeNodes: [curr].filter(id => id !== -1), - phase: 'kthAncestor' - }); + snap( + curr, null, + `Finished kthAncestor. The ${k}-th ancestor of ${startNode} is ${curr}.`, + `RETURN node → ${curr}`, + 28, 22, 59, 45, + { startNode, k, result: curr }, 'kthAncestor' + ); } return curr; }; @@ -222,87 +407,80 @@ function binaryLifting(graph: number[][], root: number) { let currU = u; let currV = v; - s.push({ - up: up.map(row => [...row]), depth: [...depth], - highlightedLines: [35], - explanation: `lca(u=${u}, v=${v}): Find the lowest common ancestor.`, - variables: { u, v, 'depth[u]': depth[u], 'depth[v]': depth[v] }, - activeNodes: [u, v], - phase: 'lca' - }); + snap( + u, null, + `lca(u=${u}, v=${v}): Find the lowest common ancestor of ${u} and ${v}.`, + `CALL lca(u = ${u}, v = ${v})`, + 30, 23, 62, 47, + { u, v, 'depth[u]': depth[u], 'depth[v]': depth[v] }, 'lca' + ); if (depth[currU] < depth[currV]) { - s.push({ - up: up.map(row => [...row]), depth: [...depth], - highlightedLines: [36], - explanation: `depth[u] < depth[v]. Swapping u and v.`, - variables: { u: currV, v: currU }, - activeNodes: [currU, currV], - phase: 'lca' - }); + snap( + currU, null, + `depth[u] (${depth[currU]}) < depth[v] (${depth[currV]}). Swap u and v to keep depth[u] >= depth[v].`, + `IF depth[u] < depth[v]: SWAP u, v`, + 31, 24, 63, 48, + { u: currV, v: currU }, 'lca' + ); [currU, currV] = [currV, currU]; } const diff = depth[currU] - depth[currV]; if (diff > 0) { - s.push({ - up: up.map(row => [...row]), depth: [...depth], - highlightedLines: [37, 38], - explanation: `Depths are different (diff=${diff}). Moving u up to the same depth as v.`, - variables: { u: currU, v: currV, diff }, - activeNodes: [currU, currV], - phase: 'lca' - }); + snap( + currU, null, + `Depths are different (diff=${diff}). Lift u up by ${diff} levels to align depths.`, + `SET diff = depth[u] - depth[v] (${diff}); SET u = kthAncestor(u, diff)`, + 32, 26, 67, 50, + { u: currU, v: currV, diff }, 'lca' + ); currU = kthAncestor(currU, diff, true); } if (currU === currV) { - s.push({ - up: up.map(row => [...row]), depth: [...depth], - highlightedLines: [39], - explanation: `u and v are now the same node (${currU}). This is the LCA.`, - variables: { lca: currU }, - activeNodes: [currU], - phase: 'lca' - }); + snap( + currU, null, + `u and v are now at the same node (${currU}). LCA is found.`, + `IF u == v: RETURN u → ${currU}`, + 34, 28, 69, 51, + { lca: currU }, 'lca' + ); return currU; } for (let j = LOG - 1; j >= 0; j--) { - s.push({ - up: up.map(row => [...row]), depth: [...depth], - highlightedLines: [40, 41], - explanation: `Checking 2^${j}-th ancestors. up[u][${j}]=${up[currU][j]}, up[v][${j}]=${up[currV][j]}.`, - variables: { u: currU, v: currV, j, 'up[u][j]': up[currU][j], 'up[v][j]': up[currV][j] }, - activeNodes: [currU, currV], - phase: 'lca' - }); + snap( + currU, null, + `Checking 2^${j}-th ancestors of u and v. up[u][${j}] = ${up[currU][j]}, up[v][${j}] = ${up[currV][j]}.`, + `FOR j = LOG-1 down to 0: IF up[u][j] != up[v][j]`, + 35, 29, 70, 52, + { u: currU, v: currV, j, 'up[u][j]': up[currU][j], 'up[v][j]': up[currV][j] }, 'lca' + ); if (up[currU][j] !== up[currV][j]) { const prevU = currU; const prevV = currV; currU = up[currU][j]; currV = up[currV][j]; - s.push({ - up: up.map(row => [...row]), depth: [...depth], - highlightedLines: [42, 43], - explanation: `Ancestors are different. Jump both nodes up by 2^${j} to ${currU} and ${currV}.`, - variables: { u: currU, v: currV, j, prevU, prevV }, - activeNodes: [currU, currV], - phase: 'lca' - }); + snap( + currU, null, + `Ancestors are different. Lift both nodes up by 2^${j} to ${currU} and ${currV}.`, + `SET u = up[u][j], v = up[v][j] → u = ${currU}, v = ${currV}`, + 37, 31, 72, 54, + { u: currU, v: currV, j, prevU, prevV }, 'lca' + ); } } const result = up[currU][0]; - s.push({ - up: up.map(row => [...row]), depth: [...depth], - highlightedLines: [46], - explanation: `LCA is the immediate parent of the current nodes: up[${currU}][0] = ${result}.`, - variables: { u: currU, v: currV, result }, - activeNodes: [result], - phase: 'lca' - }); + snap( + result, null, + `Ancestors are now equal. LCA is the immediate parent of u and v: up[u][0] = ${result}.`, + `RETURN up[u][0] → ${result}`, + 41, 33, 76, 58, + { u: currU, v: currV, result }, 'lca' + ); return result; }; @@ -310,19 +488,19 @@ function binaryLifting(graph: number[][], root: number) { kthAncestor(7, 3); lca(6, 4); - s.push({ - up: up.map(row => [...row]), depth: [...depth], - highlightedLines: [49], - explanation: `Binary Lifting visualization finished.`, - variables: {}, - activeNodes: [], - phase: 'done' - }); + snap( + -1, null, + `Binary Lifting visualization finished.`, + `DONE`, + 43, 38, 76, 58, + {}, 'done' + ); - return s; + return { steps: s, stepLineNumbers }; }, []); - const step = steps[currentStep] || steps[0]; + const currentStep = steps[currentStepIndex] || steps[0]; + const pseudoSteps = steps.map(s => s.pseudoStep); // Tree layout calculation const treePositions = useMemo(() => { @@ -359,113 +537,117 @@ function binaryLifting(graph: number[][], root: number) { return ( - -

- Tree Structure & Depths -

- - {treeEdges.map((edge, i) => ( - - ))} - {treePositions.map((pos, i) => { - const isActive = step.activeNodes.includes(i); - return ( - - - - {i} - - - d:{step.depth[i]} - - - ); - })} - -
- - -

- Binary Lifting Table (up[node][j]) -

-
- - - - - {Array.from({ length: LOG }).map((_, j) => ( - - ))} - - - - {step.up.map((row, i) => ( - - - {row.map((val, j) => ( - +
+
+ +

+ Tree Structure & Depths +

+ + {treeEdges.map((edge, i) => ( + + ))} + {treePositions.map((pos, i) => { + const isActive = currentStep.activeNodes.includes(i); + return ( + + + + {i} + + + d:{currentStep.depth[i]} + + + ); + })} + +
+ + +

+ Binary Lifting Table (up[node][j]) +

+
+
node \ j2^{j}
{i} - {val === -1 ? '-' : val} -
+ + + + {Array.from({ length: LOG }).map((_, j) => ( + ))} - ))} - -
node \ j2^{j}
-
-
- - -
-

Step Explanation

-

{step.explanation}

- + + + {currentStep.up.map((row, i) => ( + + {i} + {row.map((val, j) => ( + + {val === -1 ? '-' : val} + + ))} + + ))} + + +
+
+
+ +
+ +
+

Step Explanation

+

{currentStep.explanation}

+ + +
} rightContent={ -
- - -
+ setCurrentStepIndex(0)} + /> } controls={ } /> diff --git a/src/components/visualizations/algorithms/BinarySearchVisualization.tsx b/src/components/visualizations/algorithms/BinarySearchVisualization.tsx index b061909..b62fcd7 100644 --- a/src/components/visualizations/algorithms/BinarySearchVisualization.tsx +++ b/src/components/visualizations/algorithms/BinarySearchVisualization.tsx @@ -1,8 +1,10 @@ import { useEffect, useRef, useState } from 'react'; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import { motion, AnimatePresence } from 'framer-motion'; +import { Info } from 'lucide-react'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { array: number[]; @@ -11,201 +13,282 @@ interface Step { mid: number; target: number; found: boolean; - message: string; - lineNumber: number; + explanation: string; + pseudoStep: string; + variables: Record; } -export const BinarySearchVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const intervalRef = useRef(null); +// ─── DB Codes (no comments, exact match) ──────────────────────────────── - const code = `function binarySearch(arr: number[], target: number): number { +const languages: VisualizationLanguageMap = { + typescript: `function search(nums: number[], target: number): number { let left = 0; - let right = arr.length - 1; - + let right = nums.length - 1; while (left <= right) { - const mid = Math.floor((left + right) / 2); - - if (arr[mid] === target) { + const mid = left + Math.floor((right - left) / 2); + if (nums[mid] === target) { return mid; - } else if (arr[mid] < target) { + } + else if (nums[mid] < target) { left = mid + 1; - } else { + } + else { right = mid - 1; } } - return -1; -}`; +}`, + + python: `def search(nums: list[int], target: int) -> int: + left = 0 + right = 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`, + + java: `public int search(int[] nums, int target) { + int left = 0; + int right = nums.length - 1; + while (left <= right) { + int mid = left + (right - left) / 2; + if (nums[mid] == target) { + return mid; + } + else if (nums[mid] < target) { + left = mid + 1; + } + else { + right = mid - 1; + } + } + return -1; + }`, + + cpp: `int search(std::vector& nums, int target) { + int left = 0; + int right = nums.size() - 1; + while (left <= right) { + int mid = left + (right - left) / 2; + if (nums[mid] == target) { + return mid; + } + else if (nums[mid] < target) { + left = mid + 1; + } + else { + right = mid - 1; + } + } + return -1; +}` +}; + +// ─── Step generator ────────────────────────────────────────────────────────── + +function generateVisualizationData() { + const array = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]; + const target = 13; + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - const generateSteps = () => { - const array = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]; - const target = 13; - const newSteps: Step[] = []; - let left = 0; - let right = array.length - 1; + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + // Initial State + steps.push({ + array: [...array], + left: -1, + right: -1, + mid: -1, + target, + found: false, + explanation: 'Start Binary Search on a sorted array.', + pseudoStep: 'START search(nums, target)', + variables: { target, left: '-', right: '-', mid: '-', searchSpace: array.length } + }); + addLines(1, 1, 1, 1); + + let left = 0; + steps.push({ + array: [...array], + left: 0, + right: -1, + mid: -1, + target, + found: false, + explanation: 'Initialize the left pointer to the first index (0).', + pseudoStep: 'SET left = 0', + variables: { target, left: 0, right: '-', mid: '-', searchSpace: array.length } + }); + addLines(2, 2, 2, 2); + + let right = array.length - 1; + steps.push({ + array: [...array], + left, + right, + mid: -1, + target, + found: false, + explanation: `Initialize the right pointer to the last index (${right}).`, + pseudoStep: `SET right = arr.length − 1 → ${right}`, + variables: { target, left, right, mid: '-', searchSpace: array.length } + }); + addLines(3, 3, 3, 3); - // Line 1: Function entry - newSteps.push({ + while (left <= right) { + steps.push({ array: [...array], - left: -1, - right: -1, + left, + right, mid: -1, target, found: false, - message: `Starting Binary Search for target ${target}.`, - lineNumber: 1 + explanation: `Check loop condition: left (${left}) <= right (${right}) is true.`, + pseudoStep: `WHILE left <= right → ${left} <= ${right} → YES ✓`, + variables: { target, left, right, mid: '-', searchSpace: right - left + 1 } }); + addLines(4, 4, 4, 4); - // Line 2: Initialize left - newSteps.push({ + const mid = left + Math.floor((right - left) / 2); + steps.push({ array: [...array], - left: 0, - right: -1, - mid: -1, + left, + right, + mid, target, found: false, - message: 'Initialize left pointer at index 0.', - lineNumber: 2 + explanation: `Calculate mid index: left + floor((right - left) / 2) = ${mid}.`, + pseudoStep: `SET mid = left + (right - left) / 2 → ${mid}`, + variables: { target, left, right, mid, 'arr[mid]': array[mid], searchSpace: right - left + 1 } }); + addLines(5, 5, 5, 5); - // Line 3: Initialize right - newSteps.push({ + steps.push({ array: [...array], - left: 0, - right: array.length - 1, - mid: -1, + left, + right, + mid, target, found: false, - message: `Initialize right pointer at index ${array.length - 1}.`, - lineNumber: 3 + explanation: `Compare value at mid arr[${mid}] (${array[mid]}) with target (${target}).`, + pseudoStep: `IF arr[mid] == target → ${array[mid]} == ${target} → ${array[mid] === target ? 'YES ✓' : 'NO ✗'}`, + variables: { target, left, right, mid, 'arr[mid]': array[mid], searchSpace: right - left + 1 } }); + addLines(6, 6, 6, 6); - while (left <= right) { - // Line 5: While condition - newSteps.push({ + if (array[mid] === target) { + steps.push({ array: [...array], left, right, - mid: -1, + mid, + target, + found: true, + explanation: `Target found at index ${mid}! Returning the index.`, + pseudoStep: `RETURN mid → ${mid}`, + variables: { target, left, right, mid, 'arr[mid]': array[mid], searchSpace: right - left + 1, result: mid } + }); + addLines(7, 7, 7, 7); + break; + } else if (array[mid] < target) { + steps.push({ + array: [...array], + left, + right, + mid, target, found: false, - message: `Check condition: left (${left}) <= right (${right}) is true.`, - lineNumber: 5 + explanation: `Since arr[mid] (${array[mid]}) < target (${target}), search the right half.`, + pseudoStep: `ELSE IF arr[mid] < target → ${array[mid]} < ${target} → YES ✓`, + variables: { target, left, right, mid, 'arr[mid]': array[mid], searchSpace: right - left + 1 } }); + addLines(9, 8, 9, 9); - const mid = Math.floor((left + right) / 2); - // Line 6: Calculate mid - newSteps.push({ + left = mid + 1; + steps.push({ array: [...array], left, right, mid, target, found: false, - message: `Calculate mid: Math.floor((${left} + ${right}) / 2) = ${mid}.`, - lineNumber: 6 + explanation: `Update left pointer to mid + 1 = ${left}.`, + pseudoStep: 'left = mid + 1', + variables: { target, left, right, mid, searchSpace: right - left + 1 } }); - - // Line 8: Check if arr[mid] === target - newSteps.push({ + addLines(10, 9, 10, 10); + } else { + steps.push({ array: [...array], left, right, mid, target, found: false, - message: `Compare arr[${mid}] (${array[mid]}) with target (${target}).`, - lineNumber: 8 + explanation: `Since arr[mid] (${array[mid]}) > target (${target}), search the left half.`, + pseudoStep: `ELSE IF arr[mid] < target → ${array[mid]} < ${target} → NO ✗`, + variables: { target, left, right, mid, 'arr[mid]': array[mid], searchSpace: right - left + 1 } }); + addLines(9, 8, 9, 9); - if (array[mid] === target) { - newSteps.push({ - array: [...array], - left, - right, - mid, - target, - found: true, - message: `Success! arr[${mid}] matches target ${target}. Returning index ${mid}.`, - lineNumber: 9 - }); - break; - } else if (array[mid] < target) { - // Line 10: Check if arr[mid] < target - newSteps.push({ - array: [...array], - left, - right, - mid, - target, - found: false, - message: `${array[mid]} is less than ${target}. Search the right half.`, - lineNumber: 10 - }); - // Line 11: left = mid + 1 - left = mid + 1; - newSteps.push({ - array: [...array], - left, - right, - mid, - target, - found: false, - message: `Update left pointer to mid + 1 = ${left}.`, - lineNumber: 11 - }); - } else { - // Line 12: arr[mid] > target - newSteps.push({ - array: [...array], - left, - right, - mid, - target, - found: false, - message: `${array[mid]} is greater than ${target}. Search the left half.`, - lineNumber: 12 - }); - // Line 13: right = mid - 1 - right = mid - 1; - newSteps.push({ - array: [...array], - left, - right, - mid, - target, - found: false, - message: `Update right pointer to mid - 1 = ${right}.`, - lineNumber: 13 - }); - } - } - - if (left > right && newSteps[newSteps.length - 1].lineNumber !== 9) { - // Line 17: Return -1 - newSteps.push({ + right = mid - 1; + steps.push({ array: [...array], left, right, - mid: -1, + mid, target, found: false, - message: `Search range exhausted (left > right). Target ${target} not found. Returning -1.`, - lineNumber: 17 + explanation: `Update right pointer to mid - 1 = ${right}.`, + pseudoStep: 'right = mid - 1', + variables: { target, left, right, mid, searchSpace: right - left + 1 } }); + addLines(13, 11, 13, 13); } + } - setSteps(newSteps); - setCurrentStepIndex(0); - }; + if (left > right) { + steps.push({ + array: [...array], + left, + right, + mid: -1, + target, + found: false, + explanation: 'Search space exhausted. The target was not found in the array.', + pseudoStep: 'RETURN -1', + variables: { target, left, right, mid: '-', searchSpace: 0, result: -1 } + }); + addLines(16, 12, 16, 16); + } - useEffect(() => { - generateSteps(); - }, []); + return { steps, stepLineNumbers }; +} + +// ─── Component ─────────────────────────────────────────────────────────────── + +export const BinarySearchVisualization = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { @@ -217,7 +300,7 @@ export const BinarySearchVisualization = () => { } return prev + 1; }); - }, 1000 / speed); + }, 1200 / speed); } else { if (intervalRef.current) clearInterval(intervalRef.current); } @@ -228,18 +311,18 @@ export const BinarySearchVisualization = () => { const handlePlay = () => setIsPlaying(true); const handlePause = () => setIsPlaying(false); - const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(prev => prev + 1); - const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(prev => prev - 1); + const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(p => p + 1); + const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(p => p - 1); const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); - generateSteps(); }; if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; const getMaxValue = () => Math.max(...currentStep.array); + const pseudoSteps = steps.map(s => s.pseudoStep); return (
@@ -257,39 +340,56 @@ export const BinarySearchVisualization = () => { />
+ {/* Left Column: Visual simulator, Commentary, and Variables */}
-
-
+
+
{currentStep.array.map((value, index) => { + const isLeft = index === currentStep.left; + const isRight = index === currentStep.right; const isMid = index === currentStep.mid; - const isInRange = index >= currentStep.left && index <= currentStep.right; - const isOutOfRange = !isInRange; + const isFound = currentStep.found && isMid; + const inRange = index >= currentStep.left && index <= currentStep.right; + const isPointersSet = currentStep.left !== -1 && currentStep.right !== -1; return ( -
- {index === currentStep.left && ( -
L
- )} - {index === currentStep.right && ( -
R
- )} - {isMid && ( -
MID
- )} -
+
+
+ {isLeft && ( + Left + )} + {isMid && ( + Mid + )} + {isRight && ( + Right + )} +
{value} @@ -300,28 +400,62 @@ export const BinarySearchVisualization = () => {
-
-

{currentStep.message}

-
-
- - = 0 ? currentStep.mid : 'calculating...', - target: currentStep.target, - 'arr[mid]': currentStep.mid >= 0 ? currentStep.array[currentStep.mid] : 'N/A', - searchSpace: currentStep.right - currentStep.left + 1 - }} - /> + {/* Commentary Panel */} +
+
+
+
+ + + + + + Algorithm Commentary + +
+
+ Step {currentStepIndex + 1} of {steps.length} +
+
+ +
+
+ +
+
+

+ Current Action +

+ + + {currentStep.explanation} + + +
+
+
+ +
- + {/* Right Column: Code & Pseudocode Display */} +
- -
); }; +export default BinarySearchVisualization; diff --git a/src/components/visualizations/algorithms/ClimbingStairsVisualization.tsx b/src/components/visualizations/algorithms/ClimbingStairsVisualization.tsx index bf66191..1db4286 100644 --- a/src/components/visualizations/algorithms/ClimbingStairsVisualization.tsx +++ b/src/components/visualizations/algorithms/ClimbingStairsVisualization.tsx @@ -1,192 +1,22 @@ -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import { Card } from '@/components/ui/card'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; -import { motion } from 'framer-motion'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { array: number[]; + highlighting: number[]; variables: Record; explanation: string; - highlightedLines: number[]; - lineExecution: string; - highlighting: number[]; + pseudoStep: string; + calc?: string; } -export const ClimbingStairsVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); - - const steps: Step[] = [ - { - array: [1], // Stair 0 - variables: { n: 5 }, - explanation: "Starting with n = 5. We need to find the number of ways to climb 5 stairs. At step 0 (ground), there is mathematically 1 way to be there.", - highlightedLines: [1], - lineExecution: "function climbStairs(n: number): number {", - highlighting: [0] - }, - { - array: [1, 1], // Stair 0, Stair 1 - variables: { n: 5, one: 1, two: 1 }, - explanation: "Initialize one = 1 and two = 1.\n'one' tracks ways to reach the current top step, 'two' tracks ways for the step before it.", - highlightedLines: [2, 3], - lineExecution: "let one = 1;\nlet two = 1;", - highlighting: [0, 1] - }, - // i = 0 - { - array: [1, 1], - variables: { n: 5, one: 1, two: 1, i: 0 }, - explanation: "Start loop from i = 0 to n - 1 (which is 4).", - highlightedLines: [4], - lineExecution: "for (let i = 0; i < n - 1; i++)", - highlighting: [1] - }, - { - array: [1, 1], - variables: { n: 5, one: 1, two: 1, i: 0, temp: 1 }, - explanation: "Store current value of 'one' in 'temp'.", - highlightedLines: [5], - lineExecution: "const temp = one;", - highlighting: [1] - }, - { - array: [1, 1, 2], - variables: { n: 5, one: 2, two: 1, i: 0, temp: 1 }, - explanation: "Calculate ways to reach step 2.\nIt is the sum of ways to reach step 1 + ways to reach step 0 (1 + 1 = 2).", - highlightedLines: [6], - lineExecution: "one = one + two;", - highlighting: [2] - }, - { - array: [1, 1, 2], - variables: { n: 5, one: 2, two: 1, i: 0, temp: 1 }, - explanation: "Shift 'two' forward to 'temp' (the old step 1).", - highlightedLines: [7], - lineExecution: "two = temp;", - highlighting: [2] - }, - // i = 1 - { - array: [1, 1, 2], - variables: { n: 5, one: 2, two: 1, i: 1 }, - explanation: "Loop iteration i = 1 (Targeting step 3).", - highlightedLines: [4], - lineExecution: "for (let i = 0; i < n - 1; i++)", - highlighting: [2] - }, - { - array: [1, 1, 2], - variables: { n: 5, one: 2, two: 1, i: 1, temp: 2 }, - explanation: "Store 'one' (2) in 'temp'.", - highlightedLines: [5], - lineExecution: "const temp = one;", - highlighting: [2] - }, - { - array: [1, 1, 2, 3], - variables: { n: 5, one: 3, two: 1, i: 1, temp: 2 }, - explanation: "Calculate ways to reach step 3.\nIt is the sum of ways to reach step 2 + ways to reach step 1 (2 + 1 = 3).", - highlightedLines: [6], - lineExecution: "one = one + two;", - highlighting: [3] - }, - { - array: [1, 1, 2, 3], - variables: { n: 5, one: 3, two: 2, i: 1, temp: 2 }, - explanation: "Shift 'two' forward to 'temp' (the old step 2).", - highlightedLines: [7], - lineExecution: "two = temp;", - highlighting: [3] - }, - // i = 2 - { - array: [1, 1, 2, 3], - variables: { n: 5, one: 3, two: 2, i: 2 }, - explanation: "Loop iteration i = 2 (Targeting step 4).", - highlightedLines: [4], - lineExecution: "for (let i = 0; i < n - 1; i++)", - highlighting: [3] - }, - { - array: [1, 1, 2, 3], - variables: { n: 5, one: 3, two: 2, i: 2, temp: 3 }, - explanation: "Store 'one' (3) in 'temp'.", - highlightedLines: [5], - lineExecution: "const temp = one;", - highlighting: [3] - }, - { - array: [1, 1, 2, 3, 5], - variables: { n: 5, one: 5, two: 2, i: 2, temp: 3 }, - explanation: "Calculate ways to reach step 4.\nIt is the sum of ways to reach step 3 + ways to reach step 2 (3 + 2 = 5).", - highlightedLines: [6], - lineExecution: "one = one + two;", - highlighting: [4] - }, - { - array: [1, 1, 2, 3, 5], - variables: { n: 5, one: 5, two: 3, i: 2, temp: 3 }, - explanation: "Shift 'two' forward to 'temp' (the old step 3).", - highlightedLines: [7], - lineExecution: "two = temp;", - highlighting: [4] - }, - // i = 3 - { - array: [1, 1, 2, 3, 5], - variables: { n: 5, one: 5, two: 3, i: 3 }, - explanation: "Loop iteration i = 3 (Final calculation targeting step 5).", - highlightedLines: [4], - lineExecution: "for (let i = 0; i < n - 1; i++)", - highlighting: [4] - }, - { - array: [1, 1, 2, 3, 5], - variables: { n: 5, one: 5, two: 3, i: 3, temp: 5 }, - explanation: "Store 'one' (5) in 'temp'.", - highlightedLines: [5], - lineExecution: "const temp = one;", - highlighting: [4] - }, - { - array: [1, 1, 2, 3, 5, 8], - variables: { n: 5, one: 8, two: 3, i: 3, temp: 5 }, - explanation: "Calculate ways to reach step 5.\nIt is the sum of ways to reach step 4 + ways to reach step 3 (5 + 3 = 8).", - highlightedLines: [6], - lineExecution: "one = one + two;", - highlighting: [5] - }, - { - array: [1, 1, 2, 3, 5, 8], - variables: { n: 5, one: 8, two: 5, i: 3, temp: 5 }, - explanation: "Shift 'two' forward to 'temp'.", - highlightedLines: [7], - lineExecution: "two = temp;", - highlighting: [5] - }, - // End - { - array: [1, 1, 2, 3, 5, 8], - variables: { n: 5, one: 8, two: 5, i: 4 }, - explanation: "Loop condition i < n - 1 is false. Loop completes.", - highlightedLines: [4], - lineExecution: "i < n - 1 -> false", - highlighting: [5] - }, - { - array: [1, 1, 2, 3, 5, 8], - variables: { n: 5, one: 8 }, - explanation: "Return 'one', which is 8.\nThere are 8 distinct ways to climb 5 stairs.", - highlightedLines: [9], - lineExecution: "return one;", - highlighting: [5] - } - ]; - - const code = `function climbStairs(n: number): number { +const languages: VisualizationLanguageMap = { + typescript: `function climbStairs(n: number): number { let one = 1; let two = 1; for (let i = 0; i < n - 1; i++) { @@ -195,112 +25,236 @@ export const ClimbingStairsVisualization = () => { two = temp; } return one; -}`; +}`, + python: `def climbStairs(n: int) -> int: + one, two = 1, 1 + for _ in range(n - 1): + temp = one + one = one + two + two = temp + return one`, + java: `public int climbStairs(int n) { + int one = 1; + int two = 1; + for (int i = 0; i < n - 1; i++) { + int temp = one; + one = one + two; + two = temp; + } + return one; +}`, + cpp: `int climbStairs(int n) { + int one = 1; + int two = 1; + for (int i = 0; i < n - 1; i++) { + int temp = one; + one = one + two; + two = temp; + } + return one; +}` +}; - const step = steps[currentStep]; - - // Total stairs is n=5, plus step 0 (ground). - const maxStairsCount = 6; +export const ClimbingStairsVisualization = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const n = 5; + + const { steps, stepLineNumbers } = useMemo(() => { + const s: Step[] = []; + const lines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + lines.typescript!.push(ts); + lines.python!.push(py); + lines.java!.push(java); + lines.cpp!.push(cpp); + }; + + // Initial state + s.push({ + array: [1, 1], + highlighting: [0, 1], + variables: { n, one: 1, two: 1 }, + explanation: "Initialize one = 1 and two = 1. 'one' represents the ways to reach the current stair (stair 1), and 'two' represents the ways to reach the previous stair (stair 0).", + pseudoStep: "SET one = 1, two = 1" + }); + addLines(2, 2, 2, 2); + + let one = 1; + let two = 1; + const currentArray = [1, 1]; + + for (let i = 0; i < n - 1; i++) { + // Loop check step + s.push({ + array: [...currentArray], + highlighting: [i + 1], + variables: { n, one, two, i }, + explanation: `Check loop condition: i = ${i} < ${n - 1} (n - 1 = 4). We will compute ways to reach stair ${i + 2}.`, + pseudoStep: `FOR i = 0 TO n-2 (i = ${i})` + }); + addLines(4, 3, 4, 4); + + // temp = one + const temp = one; + s.push({ + array: [...currentArray], + highlighting: [i + 1], + variables: { n, one, two, i, temp }, + explanation: `Store current 'one' value in temp: temp = ${one}.`, + pseudoStep: `SET temp = one (${one})` + }); + addLines(5, 4, 5, 5); + + // one = one + two + one = one + two; + currentArray.push(one); + s.push({ + array: [...currentArray], + highlighting: [i + 2], + variables: { n, one, two, i, temp }, + explanation: `Update 'one' as the sum of the last two stairs: one = one + two → ${temp} + ${two} = ${one}. The number of ways to reach stair ${i + 2} is ${one}.`, + pseudoStep: `SET one = one + two (${temp} + ${two} = ${one})`, + calc: `${temp} + ${two} = ${one}` + }); + addLines(6, 5, 6, 6); + + // two = temp + two = temp; + s.push({ + array: [...currentArray], + highlighting: [i + 2], + variables: { n, one, two, i, temp }, + explanation: `Shift 'two' to take the previous value of 'one' stored in temp: two = ${two}.`, + pseudoStep: `SET two = temp (${two})` + }); + addLines(7, 6, 7, 7); + } + + // Return step + s.push({ + array: [...currentArray], + highlighting: [n], + variables: { n, one, two }, + explanation: `Loop terminates. Return the value of 'one' = ${one}, which represents the total number of ways to climb ${n} stairs.`, + pseudoStep: `RETURN one (${one})` + }); + addLines(9, 7, 9, 9); + + return { steps: s, stepLineNumbers: lines }; + }, []); + + const step = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); + const maxStairsCount = 6; // Stair 0 up to Stair 5 return ( - {/* Staircase Visual */} -
- -

- Climbing Stairs (n = 5) +
+
+ +

+ Climbing Stairs (n = {n})

- -
- {Array.from({ length: maxStairsCount }).map((_, idx) => { - const isCalculated = idx < step.array.length; - const val = isCalculated ? step.array[idx] : "?"; - const isCurrent = step.highlighting.includes(idx); - - return ( -
- {/* The Person Climber */} - {isCurrent && ( -
-
🚶
-
- )} - - {/* Ways Value Tag */} -
- {val} +
+ {Array.from({ length: maxStairsCount }).map((_, idx) => { + const isCalculated = idx < step.array.length; + const val = isCalculated ? step.array[idx] : "?"; + const isCurrent = step.highlighting.includes(idx); + + return ( +
+ {isCurrent && ( +
+
🚶
+ )} - {/* The Stair Block */} -
-
- S{idx} -
+
+ {val} +
+ +
+
+ S{idx}
- ); - })} +
+ ); + })}
+ + {step.calc && ( + +

Calculation

+

{step.calc}

+
+ )}
-
- -
-
-

- Current Execution -

-
- {step.lineExecution} -
-
-
-

- Commentary -

-

- {step.explanation} -

-
-
+
+ +

Step Explanation

+

+ {step.explanation} +

-
-
- } - rightContent={ -
-
- -
-
+ + +

+ Why this works +

+

+ To reach the n-th stair, we could have taken 1 step from the (n-1)-th stair or 2 steps from the (n-2)-th stair. +

+

+ Thus, the number of ways to reach stair n is the sum of ways to reach stair n-1 and n-2: `ways(n) = ways(n-1) + ways(n-2)`. +

+

+ This is exactly the Fibonacci sequence, which we compute iteratively in O(1) space using two variables. +

+
} + rightContent={ + setCurrentStepIndex(0)} + /> + } controls={ } /> diff --git a/src/components/visualizations/algorithms/CloneGraphVisualization.tsx b/src/components/visualizations/algorithms/CloneGraphVisualization.tsx index b7823ad..93d5c48 100644 --- a/src/components/visualizations/algorithms/CloneGraphVisualization.tsx +++ b/src/components/visualizations/algorithms/CloneGraphVisualization.tsx @@ -2,8 +2,10 @@ import React, { useState, useMemo } from 'react'; import { Card } from '@/components/ui/card'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; +import { Info } from 'lucide-react'; interface NodeState { id: number; @@ -14,18 +16,14 @@ interface NodeState { interface Step { originalNodes: NodeState[]; clonedNodes: NodeState[]; - oldToNewKeys: number[]; dfsStack: number[]; variables: Record; explanation: string; - highlightedLines: number[]; - lineExecution: string; + pseudoStep: string; } -export const CloneGraphVisualization: React.FC = () => { - const [currentStep, setCurrentStep] = useState(0); - - const code = `class Node { +const languages: VisualizationLanguageMap = { + typescript: `class Node { val: number neighbors: Node[] constructor(val?: number, neighbors?: Node[]) { @@ -47,10 +45,84 @@ function cloneGraph(node: Node | null): Node | null { return copy } return node ? dfs(node) : null -}`; +}`, + + python: `class Node: + def __init__(self, val = 0, neighbors = None): + self.val = val + self.neighbors = neighbors if neighbors is not None else [] + +def cloneGraph(node: 'Node') -> 'Node': + oldToNew = {} + def dfs(node): + if node in oldToNew: + return oldToNew[node] + copy = Node(node.val) + oldToNew[node] = copy + for nei in node.neighbors: + copy.neighbors.append(dfs(nei)) + return copy + return dfs(node) if node else None`, + + java: `public static class Solution { + private Map oldToNew = new HashMap<>(); + public Node cloneGraph(Node node) { + if (node == null) return null; + return dfs(node); + } + private Node dfs(Node node) { + if (oldToNew.containsKey(node)) { + return oldToNew.get(node); + } + Node copy = new Node(node.val); + oldToNew.put(node, copy); + for (Node nei : node.neighbors) { + copy.neighbors.add(dfs(nei)); + } + return copy; + } +}`, + + cpp: `class Solution { +public: + unordered_map visited; + Node* dfs(Node* node) { + if (visited.count(node)) { + return visited[node]; + } + Node* clone = new Node(node->val); + visited[node] = clone; + for (Node* neighbor : node->neighbors) { + clone->neighbors.push_back(dfs(neighbor)); + } + return clone; + } + Node* cloneGraph(Node* node) { + if (!node) return nullptr; + return dfs(node); + } +};` +}; + +export const CloneGraphVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); - const steps = useMemo(() => { + const { steps, stepLineNumbers } = useMemo(() => { const stepsList: Step[] = []; + const stepLines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLines.typescript!.push(ts); + stepLines.python!.push(py); + stepLines.java!.push(java); + stepLines.cpp!.push(cpp); + }; + const adjList: Record = { 1: [2, 4], 2: [1, 3], @@ -61,181 +133,261 @@ function cloneGraph(node: Node | null): Node | null { const oldToNew = new Map(); const stack: number[] = []; - const getStep = (explanation: string, lineExecution: string, highlightedLines: number[], currentId: number | null): Step => ({ - originalNodes: [1, 2, 3, 4].map(id => ({ id, cloned: oldToNew.has(id), highlighted: id === currentId })), - clonedNodes: [1, 2, 3, 4].filter(id => oldToNew.has(id)).map(id => ({ id, cloned: true, highlighted: id === currentId })), - oldToNewKeys: Array.from(oldToNew.keys()), - dfsStack: [...stack], - variables: { - node: currentId ? `node(${currentId})` : "null", - "oldToNew.size": oldToNew.size, - stack: stack.join(" → ") - }, - explanation, - lineExecution, - highlightedLines - }); - - stepsList.push(getStep("Given a reference to Node 1 in a cycle (1-2-3-4-1). Deep copy the entire graph.", "function cloneGraph(node: Node | null): Node | null {", [9], null)); - - stepsList.push(getStep("Initialize a Map to store mappings between original nodes and their newly created clones.", "const oldToNew = new Map()", [10], null)); + const makeStep = (explanation: string, pseudoStep: string, ts: number, py: number, java: number, cpp: number, currentId: number | null): Step => { + return { + originalNodes: [1, 2, 3, 4].map(id => ({ id, cloned: oldToNew.has(id), highlighted: id === currentId })), + clonedNodes: [1, 2, 3, 4].filter(id => oldToNew.has(id)).map(id => ({ id, cloned: true, highlighted: id === currentId })), + dfsStack: [...stack], + variables: { + node: currentId ? `Node(${currentId})` : "null", + "oldToNew.size": oldToNew.size, + stack: stack.join(" → ") || "empty" + }, + explanation, + pseudoStep + }; + }; + + // Step 1: Start + stepsList.push(makeStep( + "Given a reference to Node 1 in a connected cyclic graph. Deep copy the entire graph.", + "START cloneGraph(node=1)", + 22, 16, 4, 16, null + )); + addLines(22, 16, 4, 16); + + // Step 2: Init map + stepsList.push(makeStep( + "Initialize oldToNew Map to keep track of mapping between original nodes and their cloned nodes.", + "SET oldToNew = {}", + 10, 6, 2, 3, null + )); + addLines(10, 6, 2, 3); const solve = (id: number) => { stack.push(id); - stepsList.push(getStep(`Enter dfs(node ${id}). Check if this node is already in our map.`, "function dfs(node: Node): Node {", [11], id)); + + // Step 3: Enter DFS + stepsList.push(makeStep( + `Enter dfs(Node ${id}).`, + `CALL dfs(node=${id})`, + 11, 7, 7, 4, id + )); + addLines(11, 7, 7, 4); + + // Step 4: Check if already cloned + const exists = oldToNew.has(id); + stepsList.push(makeStep( + `Check if Node ${id} has already been cloned.`, + `IF oldToNew has node ${id} → ${exists ? "YES ✓" : "NO ✗"}`, + 12, 8, 8, 5, id + )); + addLines(12, 8, 8, 5); - if (oldToNew.has(id)) { - stepsList.push(getStep(`Node ${id} already cloned! Returning the existing clone from oldToNew map.`, "return oldToNew.get(node)!", [12, 13], id)); + if (exists) { + stepsList.push(makeStep( + `Node ${id} is already in the map. Return the cloned copy to prevent infinite loops.`, + `RETURN oldToNew[${id}]`, + 13, 9, 9, 6, id + )); + addLines(13, 9, 9, 6); stack.pop(); return; } - stepsList.push(getStep(`Node ${id} has not been seen. Creating a new cloned Node(${id}).`, "const copy = new Node(node.val)", [15], id)); - + // Step 6: Create copy + stepsList.push(makeStep( + `Node ${id} not found in map. Create a new cloned Node with value ${id}.`, + `SET copy = Node(${id})`, + 15, 10, 11, 8, id + )); + addLines(15, 10, 11, 8); + + // Step 7: Store mapping oldToNew.set(id, true); - stepsList.push(getStep(`Storing mapping: original ${id} → clone ${id}. This prevents infinite cycles.`, "oldToNew.set(node, copy)", [16], id)); + stepsList.push(makeStep( + `Map Node ${id} to its clone copy.`, + `oldToNew[${id}] = copy`, + 16, 11, 12, 9, id + )); + addLines(16, 11, 12, 9); + // Step 8: Loop neighbors for (const nei of adjList[id]) { - stepsList.push(getStep(`Processing neighbor ${nei} of node ${id}.`, `for (const nei of node.neighbors) {`, [17], id)); - stepsList.push(getStep(`Call dfs for neighbor ${nei}.`, `copy.neighbors.push(dfs(nei))`, [18], id)); + stepsList.push(makeStep( + `Iterate neighbors: current neighbor is Node ${nei}.`, + `FOR neighbor of Node ${id} → neighbor = ${nei}`, + 17, 12, 13, 10, id + )); + addLines(17, 12, 13, 10); + + stepsList.push(makeStep( + `Call dfs recursively for neighbor Node ${nei}.`, + `CALL dfs(node=${nei})`, + 18, 13, 14, 11, id + )); + addLines(18, 13, 14, 11); + solve(nei); - stepsList.push(getStep(`Returned to node ${id}. Neighbor ${nei} processed and linked.`, `copy.neighbors.push(dfs(nei))`, [18], id)); + + stepsList.push(makeStep( + `Returned from dfs(${nei}). Link cloned neighbor Node ${nei} to copy of Node ${id}.`, + `LINK neighbor ${nei} to copy of ${id}`, + 18, 13, 14, 11, id + )); + addLines(18, 13, 14, 11); } - stepsList.push(getStep(`Cloning complete for node ${id} and its neighbors. Returning clone.`, "return copy", [20], id)); + // Step 10: Return copy + stepsList.push(makeStep( + `DFS complete for Node ${id}. Return the cloned copy.`, + `RETURN copy of ${id}`, + 20, 14, 16, 13, id + )); + addLines(20, 14, 16, 13); stack.pop(); }; solve(1); - stepsList.push(getStep("Final clone of Node 1 returned. Deep copy complete.", "return node ? dfs(node) : null", [22], null)); + stepsList.push(makeStep( + "Final cloned Graph of Node 1 returned successfully.", + "RETURN copy of Node 1", + 22, 16, 5, 17, null + )); + addLines(22, 16, 5, 17); - return stepsList; + return { steps: stepsList, stepLineNumbers: stepLines }; }, []); - const step = steps[currentStep]; + const handleReset = () => { + setCurrentStepIndex(0); + }; + + const step = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( -
-

- Clone Graph (DFS + Map) -

- -
- {/* Original Graph */} -
-

Original Graph

-
- {step.originalNodes.map((node) => ( -
- {node.id} - {node.id === 1 &&
Start
} -
- ))} - {/* SVG Connections for a square graph */} - - - - - - +
+
+ {/* Original Graph */} + +

Original Graph

+
+ {step.originalNodes.map((node) => ( +
+ {node.id} + {node.id === 1 &&
Start
}
-
- - {/* Cloned Graph Nodes */} -
-

Cloned Nodes

-
- {[1, 2, 3, 4].map((id) => { - const clonedNode = step.clonedNodes.find(n => n.id === id); - return ( -
- {clonedNode ? id : ""} -
- ); - })} -
-
+ ))} + {/* SVG Connections for a square graph */} + + + + + +
+ - {step.dfsStack.length > 0 && ( -
-

Recursion Stack

-
- {step.dfsStack.map((id, pos) => ( - -
- dfs({id}) -
- {pos < step.dfsStack.length - 1 && } -
- ))} -
-
- )} + {/* Cloned Graph Nodes */} + +

Cloned Nodes

+
+ {[1, 2, 3, 4].map((id) => { + const clonedNode = step.clonedNodes.find(n => n.id === id); + return ( +
+ {clonedNode ? id : ""} +
+ ); + })} +
-
- -
-
-

- Current Execution -

-
- {step.lineExecution} + {step.dfsStack.length > 0 && ( + +

Recursion Stack

+
+ {step.dfsStack.map((id, pos) => ( + +
+ dfs({id})
-
-
-

- Commentary -

-

- {step.explanation} -

+ {pos < step.dfsStack.length - 1 && } + + ))} +
+
+ )} + + {/* Commentary Panel */} + +
+
+
+ + + + + + Algorithm Commentary + +
+
+ Step {currentStepIndex + 1} of {steps.length} +
+
+ +
+
+ +
+
+

+ Current Action +

+
+ {step.explanation}
- -
+
+
+ + +
} rightContent={ -
-
- -
- -
- -
-
+ } controls={ } /> diff --git a/src/components/visualizations/algorithms/CoinChangeVisualization.tsx b/src/components/visualizations/algorithms/CoinChangeVisualization.tsx index 191fb27..ab64a5d 100644 --- a/src/components/visualizations/algorithms/CoinChangeVisualization.tsx +++ b/src/components/visualizations/algorithms/CoinChangeVisualization.tsx @@ -1,264 +1,22 @@ -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; -import { motion } from 'framer-motion'; import { Card } from '@/components/ui/card'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { - array: (number | string)[]; + array: number[]; highlighting: number[]; variables: Record; explanation: string; - highlightedLines: number[]; - lineExecution: string; + pseudoStep: string; + calc?: string; } -export const CoinChangeVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); - - const coins = [1, 2, 5]; - const amount = 6; - - // Helper to init array with logic value - const INF = 7; // amount + 1 - - const steps: Step[] = [ - { - array: [], - highlighting: [], - variables: { coins: '[1, 2, 5]', amount: 6 }, - explanation: "Starting with coins [1, 2, 5] and target amount 6.\nOur goal is to find the minimum number of coins to sum to exactly 6.", - highlightedLines: [1], - lineExecution: "function coinChange(...) {" - }, - { - array: [7, 7, 7, 7, 7, 7, 7], // 0 to 6 - highlighting: [], - variables: { coins: '[1, 2, 5]', amount: 6, 'amount + 1': 7 }, - explanation: "Initialize tracking array 'dp' of size 7 (amount + 1).\nWe fill it with 7 (representing Infinity, an impossible state) initially.", - highlightedLines: [2], - lineExecution: "const dp: number[] = new Array(amount + 1).fill(amount + 1);" - }, - { - array: [0, 7, 7, 7, 7, 7, 7], - highlighting: [0], - variables: { amount: 6 }, - explanation: "Base case setup: It takes exactly 0 coins to make an amount of 0.", - highlightedLines: [3], - lineExecution: "dp[0] = 0;" - }, - // a = 1 - { - array: [0, 7, 7, 7, 7, 7, 7], - highlighting: [1], - variables: { a: 1, amount: 6 }, - explanation: "Outer loop starts: Target amount 'a' = 1.\nWe want to find the minimum coins to make 1.", - highlightedLines: [4], - lineExecution: "for (let a = 1; a <= amount; a++)" - }, - { - array: [0, 7, 7, 7, 7, 7, 7], - highlighting: [1], - variables: { a: 1, coin: 1 }, - explanation: "Inner loop starts: Try using the coin of value 1.", - highlightedLines: [5], - lineExecution: "for (const coin of coins) // coin = 1" - }, - { - array: [0, 7, 7, 7, 7, 7, 7], - highlighting: [1], - variables: { a: 1, coin: 1 }, - explanation: "Check: Is current amount minus coin >= 0? (1 - 1 = 0). Yes, we can use this coin.", - highlightedLines: [6], - lineExecution: "if (a - coin >= 0)" - }, - { - array: [0, 1, 7, 7, 7, 7, 7], - highlighting: [1], - variables: { a: 1, coin: 1, 'dp[1]': 1 }, - explanation: "We take 1 coin + the optimal coins for the remainder (dp[1-1] -> dp[0] -> 0).\nTotal coins = 1. Update dp[1] to Math.min(Infinity, 1) = 1.", - highlightedLines: [7], - lineExecution: "dp[a] = Math.min(dp[a], 1 + dp[a - coin]);" - }, - // a = 2 - { - array: [0, 1, 7, 7, 7, 7, 7], - highlighting: [2], - variables: { a: 2 }, - explanation: "Move to target amount 'a' = 2.", - highlightedLines: [4], - lineExecution: "for (let a = 1; a <= amount; a++)" - }, - { - array: [0, 1, 7, 7, 7, 7, 7], - highlighting: [2], - variables: { a: 2, coin: 1 }, - explanation: "Try coin = 1. Can we use it? (2 - 1 >= 0). Yes.", - highlightedLines: [5, 6], - lineExecution: "if (a - coin >= 0)" - }, - { - array: [0, 1, 2, 7, 7, 7, 7], - highlighting: [2], - variables: { a: 2, coin: 1, 'dp[2]': 2 }, - explanation: "Update dp[2] to minimum between current (Infinity) and 1 + optimal for remainder (dp[1] = 1).\ndp[2] = 2.", - highlightedLines: [7], - lineExecution: "dp[2] = 2" - }, - { - array: [0, 1, 2, 7, 7, 7, 7], - highlighting: [2], - variables: { a: 2, coin: 2 }, - explanation: "Try the next coin = 2. Can we use it? (2 - 2 >= 0). Yes.", - highlightedLines: [5, 6], - lineExecution: "if (a - coin >= 0)" - }, - { - array: [0, 1, 1, 7, 7, 7, 7], - highlighting: [2], - variables: { a: 2, coin: 2, 'dp[2]': 1 }, - explanation: "1 coin + optimal remainder dp[0] (0 coins) = 1 total coin.\nMath.min(previous 2 coins, 1 coin) = 1. Much more efficient!", - highlightedLines: [7], - lineExecution: "dp[2] = 1" - }, - // a = 3 - { - array: [0, 1, 1, 7, 7, 7, 7], - highlighting: [3], - variables: { a: 3 }, - explanation: "Move to target amount 'a' = 3.", - highlightedLines: [4], - lineExecution: "for (let a = 1; a <= amount; a++)" - }, - { - array: [0, 1, 1, 2, 7, 7, 7], - highlighting: [3], - variables: { a: 3, coin: 1, 'dp[3]': 2 }, - explanation: "Try coin = 1: dp[3] = Math.min(Infinity, 1 + dp[2]) = 1 + 1 = 2.", - highlightedLines: [7], - lineExecution: "dp[3] = 2" - }, - { - array: [0, 1, 1, 2, 7, 7, 7], - highlighting: [3], - variables: { a: 3, coin: 2, 'dp[3]': 2 }, - explanation: "Try coin = 2: dp[3] = Math.min(2, 1 + dp[1]) = Math.min(2, 2) = 2. No improvement.", - highlightedLines: [7], - lineExecution: "dp[3] = 2" - }, - // a = 4 - { - array: [0, 1, 1, 2, 7, 7, 7], - highlighting: [4], - variables: { a: 4 }, - explanation: "Move to target amount 'a' = 4.", - highlightedLines: [4], - lineExecution: "for (let a = 1; a <= amount; a++)" - }, - { - array: [0, 1, 1, 2, 3, 7, 7], - highlighting: [4], - variables: { a: 4, coin: 1, 'dp[4]': 3 }, - explanation: "Try coin = 1: dp[4] = 1 + dp[3] = 3.", - highlightedLines: [7], - lineExecution: "dp[4] = 3" - }, - { - array: [0, 1, 1, 2, 2, 7, 7], - highlighting: [4], - variables: { a: 4, coin: 2, 'dp[4]': 2 }, - explanation: "Try coin = 2: dp[4] = 1 + dp[2] (1) = 2. Improved!", - highlightedLines: [7], - lineExecution: "dp[4] = 2" - }, - // a = 5 - { - array: [0, 1, 1, 2, 2, 7, 7], - highlighting: [5], - variables: { a: 5 }, - explanation: "Move to target amount 'a' = 5.", - highlightedLines: [4], - lineExecution: "for (let a = 1; a <= amount; a++)" - }, - { - array: [0, 1, 1, 2, 2, 3, 7], - highlighting: [5], - variables: { a: 5, coin: 1, 'dp[5]': 3 }, - explanation: "Try coin = 1: dp[5] = 1 + dp[4] = 3.", - highlightedLines: [7], - lineExecution: "dp[5] = 3" - }, - { - array: [0, 1, 1, 2, 2, 3, 7], - highlighting: [5], - variables: { a: 5, coin: 2, 'dp[5]': 3 }, - explanation: "Try coin = 2: dp[5] = 1 + dp[3] = 3. No change.", - highlightedLines: [7], - lineExecution: "dp[5] = 3" - }, - { - array: [0, 1, 1, 2, 2, 1, 7], - highlighting: [5], - variables: { a: 5, coin: 5, 'dp[5]': 1 }, - explanation: "Try coin = 5: dp[5] = 1 + dp[0] (0) = 1. Direct match with exact coin!", - highlightedLines: [7], - lineExecution: "dp[5] = 1" - }, - // a = 6 - { - array: [0, 1, 1, 2, 2, 1, 7], - highlighting: [6], - variables: { a: 6 }, - explanation: "Move to target amount 'a' = 6 (Last iteration).", - highlightedLines: [4], - lineExecution: "for (let a = 1; a <= amount; a++)" - }, - { - array: [0, 1, 1, 2, 2, 1, 2], - highlighting: [6], - variables: { a: 6, coin: 1, 'dp[6]': 2 }, - explanation: "Try coin = 1: dp[6] = 1 + dp[5] (1) = 2.", - highlightedLines: [7], - lineExecution: "dp[6] = 2" - }, - { - array: [0, 1, 1, 2, 2, 1, 2], - highlighting: [6], - variables: { a: 6, coin: 2, 'dp[6]': 2 }, - explanation: "Try coin = 2: dp[6] = 1 + dp[4] (2) = 3. Math.min(2, 3) = 2. Keep 2.", - highlightedLines: [7], - lineExecution: "dp[6] = 1 + dp[4] (3) >= 2" - }, - { - array: [0, 1, 1, 2, 2, 1, 2], - highlighting: [6], - variables: { a: 6, coin: 5, 'dp[6]': 2 }, - explanation: "Try coin = 5: dp[6] = 1 + dp[1] (1) = 2. Same value, no change.", - highlightedLines: [7], - lineExecution: "dp[6] = 1 + dp[1] (2) >= 2" - }, - // End - { - array: [0, 1, 1, 2, 2, 1, 2], - highlighting: [], - variables: { amount: 6 }, - explanation: "Outer loop finished perfectly. We now check the final return condition.", - highlightedLines: [11], - lineExecution: "return dp[amount] !== amount + 1 ..." - }, - { - array: [0, 1, 1, 2, 2, 1, 2], - highlighting: [6], - variables: { result: 2 }, - explanation: "Since dp[6] is 2 (which is not Infinity), we return 2.\nOptimal solution is e.g. 5 + 1.", - highlightedLines: [11], - lineExecution: "return 2" - } - ]; - - const code = `function coinChange(coins: number[], amount: number): number { +const languages: VisualizationLanguageMap = { + typescript: `function coinChange(coins: number[], amount: number): number { const dp: number[] = new Array(amount + 1).fill(amount + 1); dp[0] = 0; for (let a = 1; a <= amount; a++) { @@ -269,73 +27,237 @@ export const CoinChangeVisualization = () => { } } return dp[amount] !== amount + 1 ? dp[amount] : -1; -}`; +}`, + python: `def coinChange(coins, amount): + dp = [amount + 1] * (amount + 1) + dp[0] = 0 + for a in range(1, amount + 1): + for coin in coins: + if a - coin >= 0: + dp[a] = min(dp[a], 1 + dp[a - coin]) + return dp[amount] if dp[amount] != amount + 1 else -1`, + java: `public static class Solution { + public int coinChange(int[] coins, int amount) { + int[] dp = new int[amount + 1]; + Arrays.fill(dp, amount + 1); + dp[0] = 0; + for (int a = 1; a <= amount; a++) { + for (int coin : coins) { + if (a - coin >= 0) { + dp[a] = Math.min(dp[a], 1 + dp[a - coin]); + } + } + } + return dp[amount] != amount + 1 ? dp[amount] : -1; + } +}`, + cpp: `class Solution { + public: + int coinChange(vector& coins, int amount) { + vector dp(amount + 1, amount + 1); + dp[0] = 0; + for (int a = 1; a <= amount; a++) { + for (int coin : coins) { + if (a - coin >= 0) { + dp[a] = min(dp[a], 1 + dp[a - coin]); + } + } + } + return dp[amount] != amount + 1 ? dp[amount] : -1; +} +};` +}; + +export const CoinChangeVisualization = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const coins = [1, 2, 5]; + const amount = 6; + const INF = amount + 1; // 7 + + const { steps, stepLineNumbers } = useMemo(() => { + const s: Step[] = []; + const lines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + lines.typescript!.push(ts); + lines.python!.push(py); + lines.java!.push(java); + lines.cpp!.push(cpp); + }; + + const dp = new Array(amount + 1).fill(INF); + + // Initial state + s.push({ + array: [...dp], + highlighting: [], + variables: { coins: '[1, 2, 5]', amount }, + explanation: `Initialize DP table 'dp' of size ${amount + 1}. All values are filled with ${INF} (representing infinity).`, + pseudoStep: `SET dp = array of size ${amount + 1} filled with ∞` + }); + addLines(2, 2, 4, 4); + + // Base case + dp[0] = 0; + s.push({ + array: [...dp], + highlighting: [0], + variables: { coins: '[1, 2, 5]', amount, 'dp[0]': 0 }, + explanation: "Base Case: To make an amount of 0, we need exactly 0 coins. So dp[0] = 0.", + pseudoStep: "SET dp[0] = 0" + }); + addLines(3, 3, 5, 5); + + // Loop steps + for (let a = 1; a <= amount; a++) { + s.push({ + array: [...dp], + highlighting: [a], + variables: { coins: '[1, 2, 5]', amount, a }, + explanation: `Outer loop: Target amount a = ${a}. We will try to find the minimum coins to make this amount.`, + pseudoStep: `FOR a = 1 TO ${amount} (a = ${a})` + }); + addLines(4, 4, 6, 6); + + for (const coin of coins) { + s.push({ + array: [...dp], + highlighting: [a], + variables: { coins: '[1, 2, 5]', amount, a, coin }, + explanation: `Try coin denomination: ${coin}.`, + pseudoStep: `FOR EACH coin IN coins (coin = ${coin})` + }); + addLines(5, 5, 7, 7); - const step = steps[currentStep]; + const checkCondition = a - coin >= 0; + s.push({ + array: [...dp], + highlighting: [a], + variables: { coins: '[1, 2, 5]', amount, a, coin }, + explanation: `Check if coin ${coin} can be used: a - coin = ${a} - ${coin} = ${a - coin} >= 0? ${checkCondition ? "YES ✓" : "NO ✗"}`, + pseudoStep: `IF a - coin >= 0 (${a} - ${coin} = ${a - coin} >= 0?)` + }); + addLines(6, 6, 8, 8); - const displayArray = step.array.map(v => v === 7 ? '∞' : v); + if (checkCondition) { + const prevVal = dp[a]; + const optionVal = 1 + dp[a - coin]; + dp[a] = Math.min(dp[a], optionVal); + + s.push({ + array: [...dp], + highlighting: [a, a - coin], + variables: { + coins: '[1, 2, 5]', + amount, + a, + coin, + 'dp[a]': dp[a], + 'dp[a-coin]': dp[a - coin], + calc: `min(${prevVal === INF ? "∞" : prevVal}, 1 + ${dp[a - coin]})` + }, + explanation: `Update dp[${a}] to the minimum of its current value (${prevVal === INF ? "∞" : prevVal}) and using this coin: 1 + dp[${a - coin}] (1 + ${dp[a - coin]} = ${optionVal}). New dp[${a}] = ${dp[a]}.`, + pseudoStep: `SET dp[a] = min(dp[a], 1 + dp[a - coin])`, + calc: `dp[${a}] = min(${prevVal === INF ? "∞" : prevVal}, 1 + ${dp[a - coin]}) = ${dp[a]}` + }); + addLines(7, 7, 9, 9); + } + } + } + + // Return checks + const result = dp[amount]; + s.push({ + array: [...dp], + highlighting: [amount], + variables: { coins: '[1, 2, 5]', amount, 'dp[amount]': result }, + explanation: `Check if target amount can be formed: dp[${amount}] is ${result === INF ? "∞" : result}.`, + pseudoStep: `IF dp[amount] != ∞` + }); + addLines(11, 8, 13, 13); + + s.push({ + array: [...dp], + highlighting: [amount], + variables: { coins: '[1, 2, 5]', amount, result: result === INF ? -1 : result }, + explanation: `Return final result: ${result === INF ? -1 : result}.`, + pseudoStep: `RETURN ${result === INF ? -1 : result}` + }); + addLines(11, 8, 13, 13); + + return { steps: s, stepLineNumbers: lines }; + }, []); + + const step = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( -
+
+
-
-
-

Available Coins

-
- {coins.map((coinValue, idx) => { - const isTesting = step.variables.coin === coinValue; - return ( -
-
- {coinValue} -
- ) - })} -
-
-
-

Objective

-
-
Target
-
{amount}
-
-
-
+
+
+

Available Coins

+
+ {coins.map((coinValue, idx) => { + const isTesting = step.variables.coin === coinValue; + return ( +
+
+ {coinValue} +
+ ); + })} +
+
+
+

Target Amount

+
+ {amount} +
+
+
-

- DP Tracking Array (Min Coins Needed) +

+ DP Tracking Array (Min Coins Needed)

- +
- {displayArray.map((value, index) => { + {step.array.map((value, index) => { const isHighlighted = step.highlighting.includes(index); const isBase = index === 0; - + const displayVal = value === INF ? '∞' : value; + return ( -
-
+
- {value} + {displayVal}
- + Amt {index}
@@ -343,52 +265,56 @@ export const CoinChangeVisualization = () => { })}
+ + {step.calc && ( + +

Calculation

+

{step.calc}

+
+ )}
-
- -
-
-

- Current Execution -

-
- {step.lineExecution} -
-
-
-

- Commentary -

-

- {step.explanation} -

-
-
+
+ +

Step Explanation

+

+ {step.explanation} +

+
+ + + + +

+ Why this works +

+

+ To make amount `a`, we can try using each coin denomination. If we use coin `c`, we are left with amount `a - c`. +

+

+ The minimum number of coins to make `a` using coin `c` is `1 + dp[a - c]`. We take the minimum across all possible coins. +

+

+ This bottom-up dynamic programming approach solves smaller subproblems first, building up to the target `amount`. +

-
} rightContent={ -
-
- -
-
- -
-
+ setCurrentStepIndex(0)} + /> } controls={ } /> diff --git a/src/components/visualizations/algorithms/CombinationsVisualization.tsx b/src/components/visualizations/algorithms/CombinationsVisualization.tsx index e3b0881..8aca0c2 100644 --- a/src/components/visualizations/algorithms/CombinationsVisualization.tsx +++ b/src/components/visualizations/algorithms/CombinationsVisualization.tsx @@ -1,8 +1,8 @@ -import React, { useEffect, useRef, useState } from 'react'; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; -import { StepControls } from '../shared/StepControls'; -import { VariablePanel } from '../shared/VariablePanel'; +import React, { useState, useEffect, useRef } from "react"; +import { StepControls } from "../shared/StepControls"; +import { VariablePanel } from "../shared/VariablePanel"; +import { VisualizationCodePanel } from "../shared/VisualizationCodePanel"; +import type { StepLineNumberMap, VisualizationLanguageMap } from "@/types/visualization"; interface Step { n: number; @@ -11,124 +11,211 @@ interface Step { i: number | null; res: number[][]; message: string; - lineNumber: number; + pseudoStep: string; } -export const CombinationsVisualization: React.FC = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1000); - const intervalRef = useRef(null); - - const code = `function combine(n: number, k: number): number[][] { +const languages: VisualizationLanguageMap = { + typescript: `function combine(n: number, k: number): number[][] { const res: number[][] = []; - function backtrack(start: number, comb: number[]) { if (comb.length === k) { res.push([...comb]); return; } - for (let i = start; i <= n; i++) { comb.push(i); backtrack(i + 1, comb); comb.pop(); } } - backtrack(1, []); return res; -}`; +}`, + python: `def combine(n: int, k: int) -> list[list[int]]: + res: list[list[int]] = [] + def backtrack(start: int, comb: list[int]) -> None: + if len(comb) == k: + res.append(comb.copy()) + return + for i in range(start, n + 1): + comb.append(i) + backtrack(i + 1, comb) + comb.pop() + backtrack(1, []) + return res`, + java: `public static class Solution { + public List> combine(int n, int k) { + List> res = new ArrayList<>(); + backtrack(n, k, 1, new ArrayList<>(), res); + return res; + } + private void backtrack(int n, int k, int start, List comb, List> res) { + if (comb.size() == k) { + res.add(new ArrayList<>(comb)); + return; + } + for (int i = start; i <= n; i++) { + comb.add(i); + backtrack(n, k, i + 1, comb, res); + comb.remove(comb.size() - 1); + } + } +}`, + cpp: `class Solution { +public: + vector> combine(int n, int k) { + vector> res; + vector comb; + backtrack(n, k, 1, comb, res); + return res; + } +private: + void backtrack(int n, int k, int start, vector& comb, vector>& res) { + if (comb.size() == k) { + res.push_back(comb); + return; + } + for (int i = start; i <= n; i++) { + comb.push_back(i); + backtrack(n, k, i + 1, comb, res); + comb.pop_back(); + } + } +};` +}; + +export const CombinationsVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); - const generateSteps = () => { + const generateStepsData = () => { const n = 4; const k = 2; - const newSteps: Step[] = []; + const steps: Step[] = []; const res: number[][] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - newSteps.push({ + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + steps.push({ n, k, comb: [], i: null, res: [], message: "Initialize result array res = []", - lineNumber: 2 + pseudoStep: "SET res = []" }); + addLines(2, 2, 3, 4); + + steps.push({ + n, k, comb: [], i: null, res: [], + message: "Initiate backtracking from index 1", + pseudoStep: "CALL backtrack(start = 1, comb = [])" + }); + addLines(14, 11, 4, 6); function backtrack(start: number, comb: number[]) { - newSteps.push({ + steps.push({ n, k, comb: [...comb], i: null, res: res.map(r => [...r]), message: `Calling backtrack(start=${start}, comb=[${comb.join(", ")}])`, - lineNumber: 4 + pseudoStep: `CALL backtrack(start = ${start}, comb = [${comb.join(", ")}])` }); + addLines(3, 3, 7, 10); - newSteps.push({ + steps.push({ n, k, comb: [...comb], i: null, res: res.map(r => [...r]), message: `Condition check: comb.length === k (${comb.length} === ${k})`, - lineNumber: 5 + pseudoStep: `IF comb.length === k → ${comb.length} === ${k} ?` }); + addLines(4, 4, 8, 11); if (comb.length === k) { res.push([...comb]); - newSteps.push({ + steps.push({ n, k, comb: [...comb], i: null, res: res.map(r => [...r]), message: `Base case reached. Added [${comb.join(", ")}] to result.`, - lineNumber: 6 + pseudoStep: `ADD comb copy to res → res.push([${comb.join(", ")}])` }); - newSteps.push({ + addLines(5, 5, 9, 12); + + steps.push({ n, k, comb: [...comb], i: null, res: res.map(r => [...r]), message: "Return from recursive call", - lineNumber: 7 + pseudoStep: "RETURN" }); + addLines(6, 6, 10, 13); return; } for (let i = start; i <= n; i++) { - newSteps.push({ + steps.push({ n, k, comb: [...comb], i: i, res: res.map(r => [...r]), message: `Iterating: i = ${i}`, - lineNumber: 10 + pseudoStep: `FOR i = ${i} to ${n}` }); + addLines(8, 7, 12, 15); comb.push(i); - newSteps.push({ + steps.push({ n, k, comb: [...comb], i: i, res: res.map(r => [...r]), message: `Included ${i} in current combination`, - lineNumber: 11 + pseudoStep: `ADD i (${i}) to comb` + }); + addLines(9, 8, 13, 16); + + steps.push({ + n, k, comb: [...comb], i: i, res: res.map(r => [...r]), + message: `Recursive call backtrack(i + 1 = ${i + 1})`, + pseudoStep: `CALL backtrack(start = ${i + 1}, comb)` }); + addLines(10, 9, 14, 17); backtrack(i + 1, comb); const popped = comb.pop(); - newSteps.push({ + steps.push({ n, k, comb: [...comb], i: i, res: res.map(r => [...r]), message: `Backtracked: Removed ${popped} from current combination`, - lineNumber: 13 + pseudoStep: `REMOVE last element (${popped}) from comb (backtrack)` }); + addLines(11, 10, 15, 18); } } - newSteps.push({ - n, k, comb: [], i: null, res: [], - message: "Initiate backtracking from 1", - lineNumber: 17 - }); backtrack(1, []); - newSteps.push({ + steps.push({ n, k, comb: [], i: null, res: res.map(r => [...r]), message: "End backtracking. Return all combinations.", - lineNumber: 18 + pseudoStep: "RETURN res" + }); + addLines(15, 12, 5, 7); + + const lastStep = steps[steps.length - 1]; + steps.push({ + ...lastStep, + message: "Algorithm Complete!", + pseudoStep: "DONE" }); + addLines(15, 12, 5, 7); - setSteps(newSteps); + return { steps, stepLineNumbers }; }; - useEffect(() => { - generateSteps(); - }, []); + const { steps, stepLineNumbers } = generateStepsData(); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { - intervalRef.current = window.setInterval(() => { + intervalRef.current = setInterval(() => { setCurrentStepIndex((prev) => { if (prev >= steps.length - 1) { setIsPlaying(false); @@ -136,7 +223,7 @@ export const CombinationsVisualization: React.FC = () => { } return prev + 1; }); - }, speed); + }, 1000 / speed); } else { if (intervalRef.current) { clearInterval(intervalRef.current); @@ -165,6 +252,7 @@ export const CombinationsVisualization: React.FC = () => { if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map((s) => s.pseudoStep); return (
@@ -176,75 +264,86 @@ export const CombinationsVisualization: React.FC = () => { onReset={handleReset} isPlaying={isPlaying} currentStep={currentStepIndex} - totalSteps={steps.length} + totalSteps={steps.length - 1} speed={speed} onSpeedChange={setSpeed} />
- -
-

Numbers: 1 to {currentStep.n}, Select {currentStep.k}

-
- {Array.from({ length: currentStep.n }, (_, i) => i + 1).map((val) => ( -
- {val} -
- ))} -
- -

Current Combination ({currentStep.comb.length}/{currentStep.k})

-
- {currentStep.comb.length > 0 ? ( - currentStep.comb.map((val, idx) => ( +
+
+

Numbers: 1 to {currentStep.n}, Select {currentStep.k}

+
+ {Array.from({ length: currentStep.n }, (_, i) => i + 1).map((val) => (
{val}
- )) - ) : ( -
Empty
- )} + ))} +
+ +

Current Combination ({currentStep.comb.length}/{currentStep.k})

+
+ {currentStep.comb.length > 0 ? ( + currentStep.comb.map((val, idx) => ( +
+ {val} +
+ )) + ) : ( +
Empty
+ )} +
+ +

Result (res) - Total: {currentStep.res.length}

+
+ {currentStep.res.length > 0 ? ( + currentStep.res.map((comb, idx) => ( +
+ [{comb.join(', ')}] +
+ )) + ) : ( +
No combinations found yet.
+ )} +
-

Result (res) - Total: {currentStep.res.length}

-
- {currentStep.res.length > 0 ? ( - currentStep.res.map((comb, idx) => ( -
- [{comb.join(', ')}] -
- )) - ) : ( -
No combinations found yet.
- )} +
+

Algorithm Logic

+

+ {currentStep.message} +

-
-

{currentStep.message}

-
- -
- -
+
- +
); diff --git a/src/components/visualizations/algorithms/ContainerWithMostWaterVisualization.tsx b/src/components/visualizations/algorithms/ContainerWithMostWaterVisualization.tsx index 064976d..c4be5b4 100644 --- a/src/components/visualizations/algorithms/ContainerWithMostWaterVisualization.tsx +++ b/src/components/visualizations/algorithms/ContainerWithMostWaterVisualization.tsx @@ -1,23 +1,10 @@ -import { useState, useMemo } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; +import { useEffect, useState } from 'react'; +import { Card } from '@/components/ui/card'; import { VisualizationLayout } from '../shared/VisualizationLayout'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; -import { Card } from '@/components/ui/card'; -import { - Zap, - Target, - ArrowRight, - ArrowLeft, - Repeat, - CheckCircle2, - Info, - Maximize, - MoveHorizontal, - Droplets, - ArrowDown -} from 'lucide-react'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { left: number; @@ -26,396 +13,415 @@ interface Step { width: number | '-'; currentHeight: number | '-'; currentArea: number | '-'; - message: string; - lineNumber: number; - phase: 'init' | 'loop' | 'calc-width' | 'calc-height' | 'calc-area' | 'update-max' | 'move' | 'done'; - insight?: string; - isNewMax?: boolean; + explanation: string; + pseudoStep: string; + highlights: number[]; } +const languages: VisualizationLanguageMap = { + typescript: `function maxArea(height: number[]): number { + let left = 0, right = height.length - 1; + let maxArea = 0; + while (left < right) { + const width = right - left; + const h = Math.min(height[left], height[right]); + const area = width * h; + maxArea = Math.max(maxArea, area); + if (height[left] < height[right]) { + left++; + } else { + right--; + } + } + return maxArea; +}`, + python: `def maxArea(height: List[int]) -> int: + left, right = 0, len(height) - 1 + max_area = 0 + while left < right: + width = right - left + h = min(height[left], height[right]) + area = width * h + max_area = max(max_area, area) + if height[left] < height[right]: + left += 1 + else: + right -= 1 + return max_area`, + java: `public static class Solution { + public int maxArea(int[] height) { + int left = 0, right = height.length - 1; + int maxArea = 0; + while (left < right) { + int width = right - left; + int h = Math.min(height[left], height[right]); + int area = width * h; + maxArea = Math.max(maxArea, area); + if (height[left] < height[right]) { + left++; + } else { + right--; + } + } + return maxArea; + } +}`, + cpp: `class Solution { +public: + int maxArea(vector& height) { + int left = 0, right = height.size() - 1; + int maxArea = 0; + while (left < right) { + int width = right - left; + int h = min(height[left], height[right]); + int area = width * h; + maxArea = max(maxArea, area); + if (height[left] < height[right]) { + left++; + } else { + right--; + } + } + return maxArea; + } +};` +}; + export const ContainerWithMostWaterVisualization = () => { + const [steps, setSteps] = useState([]); + const [stepLineNumbers, setStepLineNumbers] = useState({ + typescript: [], + python: [], + java: [], + cpp: [] + }); const [currentStepIndex, setCurrentStepIndex] = useState(0); const heights = [1, 8, 6, 2, 5, 4, 8, 3, 7]; const maxHeight = Math.max(...heights); - const code = `function maxArea(height: number[]): number { - let left = 0, right = height.length - 1; - let maxArea = 0; - - while (left < right) { - const width = right - left; - const h = Math.min(height[left], height[right]); - const area = width * h; + useEffect(() => { + const generatedSteps: Step[] = []; + const stepLines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - maxArea = Math.max(maxArea, area); + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLines.typescript!.push(ts); + stepLines.python!.push(py); + stepLines.java!.push(java); + stepLines.cpp!.push(cpp); + }; - if (height[left] < height[right]) { - left++; - } else { - right--; - } - } - return maxArea; -}`; + // Function Entry + generatedSteps.push({ + left: 0, + right: heights.length - 1, + maxArea: 0, + width: '-', + currentHeight: '-', + currentArea: '-', + explanation: "Find the container that holds the most water using the two-pointer approach.", + pseudoStep: "FUNCTION maxArea(height)", + highlights: [] + }); + addLines(1, 1, 2, 3); - const steps: Step[] = useMemo(() => { - const s: Step[] = []; let left = 0; let right = heights.length - 1; - let maxArea = 0; - // Phase: Initialization - s.push({ + // Pointer Init + generatedSteps.push({ left, right, - maxArea, + maxArea: 0, width: '-', currentHeight: '-', currentArea: '-', - message: "Initialize pointers mapping to the ends of the array. Our goal is to maximize the area between these lines.", - lineNumber: 2, - phase: 'init', - insight: "The area is limited by the shorter line, so we'll start with the maximum possible width." + explanation: `Initialize pointers to outer boundary lines: left = ${left}, right = ${right}.`, + pseudoStep: "SET left = 0, right = height.length - 1", + highlights: [left, right] }); + addLines(2, 2, 3, 4); - s.push({ + let maxArea = 0; + + // maxArea Init + generatedSteps.push({ left, right, maxArea, width: '-', currentHeight: '-', currentArea: '-', - message: `Initialize maxArea to 0.`, - lineNumber: 3, - phase: 'init' + explanation: `Initialize maxArea to ${maxArea}.`, + pseudoStep: "SET maxArea = 0", + highlights: [] }); + addLines(3, 3, 4, 5); while (left < right) { // Loop Check - s.push({ + generatedSteps.push({ left, right, maxArea, width: '-', currentHeight: '-', currentArea: '-', - message: `Condition left (${left}) < right (${right}) is true. Proceed to calculate area.`, - lineNumber: 5, - phase: 'loop' + explanation: `Loop check: left (${left}) < right (${right}). Proceed to compute container capacity.`, + pseudoStep: "WHILE left < right", + highlights: [left, right] }); + addLines(4, 4, 5, 6); const width = right - left; - s.push({ + + // Width calculation + generatedSteps.push({ left, right, maxArea, width, currentHeight: '-', currentArea: '-', - message: `Calculate width: right (${right}) - left (${left}) = ${width}.`, - lineNumber: 6, - phase: 'calc-width' + explanation: `Calculate container width: right (${right}) - left (${left}) = ${width}.`, + pseudoStep: "SET width = right - left", + highlights: [left, right] }); + addLines(5, 5, 6, 7); const h = Math.min(heights[left], heights[right]); - s.push({ + + // Height calculation + generatedSteps.push({ left, right, maxArea, width, currentHeight: h, currentArea: '-', - message: `Calculate height: min(${heights[left]}, ${heights[right]}) = ${h}.`, - lineNumber: 7, - phase: 'calc-height', - insight: "The water level cannot exceed the shorter of the two lines." + explanation: `Calculate container height (limited by the shorter line): min(heights[left], heights[right]) = min(${heights[left]}, ${heights[right]}) = ${h}.`, + pseudoStep: "SET h = min(height[left], height[right])", + highlights: [left, right] }); + addLines(6, 6, 7, 8); const area = width * h; - s.push({ + + // Area calculation + generatedSteps.push({ left, right, maxArea, width, currentHeight: h, currentArea: area, - message: `Current area: width (${width}) × height (${h}) = ${area}.`, - lineNumber: 8, - phase: 'calc-area' + explanation: `Calculate current container area: width (${width}) * height (${h}) = ${area}.`, + pseudoStep: "SET area = width * h", + highlights: [left, right] }); + addLines(7, 7, 8, 9); - const isNewMax = area > maxArea; + const oldMaxArea = maxArea; maxArea = Math.max(maxArea, area); - s.push({ + + // maxArea Update + generatedSteps.push({ left, right, maxArea, width, currentHeight: h, currentArea: area, - message: isNewMax - ? `New maximum area found! Updated maxArea to ${maxArea}.` - : `Current area (${area}) is not greater than maxArea (${maxArea}).`, - lineNumber: 10, - phase: 'update-max', - isNewMax, - insight: isNewMax ? "We've found a larger container!" : "No improvement over the global best." + explanation: area > oldMaxArea + ? `Current area ${area} is greater than maxArea (${oldMaxArea}). Update maxArea = ${maxArea}.` + : `Current area ${area} is not greater than maxArea (${oldMaxArea}). Keep maxArea = ${maxArea}.`, + pseudoStep: "SET maxArea = max(maxArea, area)", + highlights: [left, right] }); + addLines(8, 8, 9, 10); + + // Pointer decision check + generatedSteps.push({ + left, + right, + maxArea, + width, + currentHeight: h, + currentArea: area, + explanation: `Decide which pointer to move: Is heights[left] (${heights[left]}) < heights[right] (${heights[right]})?`, + pseudoStep: "IF height[left] < height[right]", + highlights: [left, right] + }); + addLines(9, 9, 10, 11); if (heights[left] < heights[right]) { - s.push({ + left++; + generatedSteps.push({ left, right, maxArea, - width, - currentHeight: h, - currentArea: area, - message: `heights[left] (${heights[left]}) < heights[right] (${heights[right]}). Move the shorter pointer inward to potentially find a taller line.`, - lineNumber: 13, - phase: 'move', - insight: "To possibly find a larger area with a smaller width, we must increase the height. Only moving the shorter line can do this." + width: '-', + currentHeight: '-', + currentArea: '-', + explanation: `Since the left line is shorter, we shift the left pointer right to try and find a taller line. Set left = ${left}.`, + pseudoStep: "SET left = left + 1", + highlights: [left, right] }); - left++; + addLines(10, 10, 11, 12); } else { - s.push({ + right--; + generatedSteps.push({ left, right, maxArea, - width, - currentHeight: h, - currentArea: area, - message: `heights[right] (${heights[right]}) ≤ heights[left] (${heights[left]}). Move the shorter pointer inward.`, - lineNumber: 15, - phase: 'move', - insight: "Moving the taller line would only reduce the width without being able to increase the height (which is already limited by the shorter line)." + width: '-', + currentHeight: '-', + currentArea: '-', + explanation: `Since the right line is shorter or equal, we shift the right pointer left to try and find a taller line. Set right = ${right}.`, + pseudoStep: "SET right = right - 1", + highlights: [left, right] }); - right--; + addLines(12, 12, 13, 14); } } - s.push({ + // Done + generatedSteps.push({ left, right, maxArea, - width: 0, - currentHeight: 0, - currentArea: 0, - message: `Execution complete. Maximum area found is ${maxArea}.`, - lineNumber: 18, - phase: 'done', - insight: "The Two-Pointer approach allows us to find the maximum area in O(n) time by optimally reducing the search space." + width: '-', + currentHeight: '-', + currentArea: '-', + explanation: `Pointers met. Loop terminated. The maximum container area found is ${maxArea}.`, + pseudoStep: "RETURN maxArea", + highlights: [] }); + addLines(15, 13, 16, 17); - return s; + setSteps(generatedSteps); + setStepLineNumbers(stepLines); }, []); - const currentStep = steps[currentStepIndex] || steps[0]; + if (steps.length === 0) return null; + + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( + } leftContent={ -
- - {/* Header Info */} -
-

- +
+
+ +

Container Volume Maximizer

-
-
- - Phase: {currentStep.phase.replace('-', ' ')} -
-
-
- - {/* Pointer & Line Visualization */} -
- {/* X-Axis Label */} -
- x = 0 - x-axis (index) - x = {heights.length - 1} -
- {/* Overlay for Water Fill to ensure perfect alignment */} -
- - {currentStep.currentArea !== '-' && currentStep.width !== '-' && ( - -
- - )} - -
+ {/* Water Visualizer Box */} +
+ {/* Visual Area Overlay */} + {currentStep.currentArea !== '-' && currentStep.width !== '-' && currentStep.left < currentStep.right && ( +
+ )} + + {/* Bars */} +
+ {heights.map((h, i) => { + const isLeft = i === currentStep.left; + const isRight = i === currentStep.right; + const isPointer = isLeft || isRight; + const barHeight = (h / maxHeight) * 100; - {heights.map((h, i) => { - const isLeft = i === currentStep.left; - const isRight = i === currentStep.right; - const isMember = isLeft || isRight; - const barHeight = (h / maxHeight) * 100; - - return ( -
- {/* Floating Pointers Pinpointing Line Tops */} - - {isMember && ( - -
- {isLeft ? 'LEFT' : 'RIGHT'} + return ( +
+ {/* Pointer Label */} + {isPointer && ( +
+ {isLeft ? 'L' : 'R'}
-
- - )} - - - {/* Height Value */} -
- {h} -
- - {/* The Line (from (i,0) to (i,height[i])) */} - -
- ); - })} -
- - {/* Stats Grid */} -
-
- - width - - {currentStep.width} -
-
- - height - - {currentStep.currentHeight} -
-
- - area - - {currentStep.currentArea} -
-
- - best - - {currentStep.maxArea} -
-
- + )} - -
-
- {currentStep.isNewMax ? : } -
-
-

- Reasoning Insight -

-

- {currentStep.message} -

-
-
-
-
- } - rightContent={ -
-
- -
+ {/* Line height number */} +
+ {h} +
- -

- - Algorithm intuition -

- -
- {currentStep.insight ? ( -
-
- -
-

- {currentStep.insight} -

-
- ) : ( -
-
- -
-

- Optimizing search space... -

-
- )} + {/* Bar Pillar */} +
-
-
- left height - {heights[currentStep.left]} -
-
- right height - {heights[currentStep.right]} + {/* Index */} + [{i}] +
+ ); + })}
-
-
+ +
+ +
+ +
+

Step Explanation

+

{currentStep.explanation}

+ + + +
} - controls={ - setCurrentStepIndex(0)} /> } /> ); }; + diff --git a/src/components/visualizations/algorithms/ContainsDuplicateVisualization.tsx b/src/components/visualizations/algorithms/ContainsDuplicateVisualization.tsx index ca30f55..358a9e2 100644 --- a/src/components/visualizations/algorithms/ContainsDuplicateVisualization.tsx +++ b/src/components/visualizations/algorithms/ContainsDuplicateVisualization.tsx @@ -1,9 +1,10 @@ -import React, { useState, useMemo } from 'react'; +import React, { useState } from 'react'; import { Card } from '@/components/ui/card'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { nums: number[]; @@ -11,111 +12,174 @@ interface Step { seen: Set; variables: Record; explanation: string; - highlightedLines: number[]; - lineExecution: string; + pseudoStep: string; } -export const ContainsDuplicateVisualization: React.FC = () => { - const [currentStep, setCurrentStep] = useState(0); +const languages: VisualizationLanguageMap = { + typescript: `function containsDuplicate(nums: number[]): boolean { + const seen = new Set(); + for (const num of nums) { + if (seen.has(num)) { + return true; + } + seen.add(num); + } + return false; +}`, - const nums = [1, 2, 3, 1]; + python: `def containsDuplicate(nums: List[int]) -> bool: + seen = set() + for num in nums: + if num in seen: + return True + seen.add(num) + return False`, - const code = `function containsDuplicate(nums: number[]): boolean { - const seen = new Set(); - for (let i = 0; i < nums.length; i++) { - if (seen.has(nums[i])) { - return true; + java: `public static class Solution { + public boolean containsDuplicate(int[] nums) { + Set seen = new HashSet<>(); + for (int num : nums) { + if (seen.contains(num)) { + return true; + } + seen.add(num); + } + return false; } - seen.add(nums[i]); - } - return false; -}`; +}`, - const steps = useMemo(() => { - const stepsList: Step[] = []; - const seen = new Set(); + cpp: `class Solution { +public: + bool containsDuplicate(vector& nums) { + unordered_set seen; + for (int num : nums) { + if (seen.count(num)) { + return true; + } + seen.insert(num); + } + return false; + } +};`, +}; - stepsList.push({ +function generateVisualizationData() { + const nums = [1, 2, 3, 1]; + const steps: Step[] = []; + const seen = new Set(); + + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + // 1. Initial State + steps.push({ + nums, + highlights: [], + seen: new Set(seen), + variables: { nums: `[${nums.join(', ')}]`, seen: '{}' }, + explanation: "Given an array of integers, check if any value appears at least twice.", + pseudoStep: "CALL containsDuplicate(nums)" + }); + addLines(1, 1, 2, 3); + + // 2. Initialize Set + steps.push({ + nums, + highlights: [], + seen: new Set(seen), + variables: { nums: `[${nums.join(', ')}]`, seen: '{}' }, + explanation: "Initialize an empty set to keep track of the numbers we have seen so far.", + pseudoStep: "SET seen = {} (empty set)" + }); + addLines(2, 2, 3, 4); + + for (let i = 0; i < nums.length; i++) { + const num = nums[i]; + + // Loop header + steps.push({ nums, - highlights: [], + highlights: [i], seen: new Set(seen), - variables: { nums: `[${nums.join(', ')}]` }, - explanation: "Given an array of integers, check if any value appears at least twice.", - lineExecution: "function containsDuplicate(nums: number[]): boolean {", - highlightedLines: [1] + variables: { i, num, seen: `{${Array.from(seen).join(', ')}}` }, + explanation: `Examine the next number in the array: ${num} at index ${i}.`, + pseudoStep: `FOR num = ${num} in nums` }); + addLines(3, 3, 4, 5); + + const isDuplicate = seen.has(num); - stepsList.push({ + // Check condition + steps.push({ nums, - highlights: [], + highlights: [i], seen: new Set(seen), - variables: { seen: "{}" }, - explanation: "Initialize an empty Set to store numbers we encounter.", - lineExecution: "const seen = new Set();", - highlightedLines: [2] + variables: { i, num, seen: `{${Array.from(seen).join(', ')}}`, "seen.has(num)": isDuplicate }, + explanation: `Check if ${num} is already present in our 'seen' set.`, + pseudoStep: `IF num (${num}) IN seen → ${isDuplicate ? 'YES ✓' : 'NO ✗'}` }); + addLines(4, 4, 5, 6); - for (let i = 0; i < nums.length; i++) { - stepsList.push({ + if (isDuplicate) { + steps.push({ nums, highlights: [i], seen: new Set(seen), - variables: { i, "nums[i]": nums[i], seen: `{${Array.from(seen).join(', ')}}` }, - explanation: `Iteration i = ${i}: Examining the value ${nums[i]}.`, - lineExecution: "for (let i = 0; i < nums.length; i++) {", - highlightedLines: [3] - }); - - const duplicate = seen.has(nums[i]); - stepsList.push({ - nums, - highlights: [i], - seen: new Set(seen), - variables: { "seen.has(nums[i])": duplicate }, - explanation: `Check if ${nums[i]} is already in our 'seen' set. ${duplicate ? "Yes, it is!" : "No, not yet."}`, - lineExecution: "if (seen.has(nums[i])) {", - highlightedLines: [4] - }); - - if (duplicate) { - stepsList.push({ - nums, - highlights: [i], - seen: new Set(seen), - variables: { result: true }, - explanation: `Found a duplicate! Since ${nums[i]} exists in the set, we return true.`, - lineExecution: "return true;", - highlightedLines: [5] - }); - return stepsList; - } - - seen.add(nums[i]); - stepsList.push({ - nums, - highlights: [i], - seen: new Set(seen), - variables: { seen: `{${Array.from(seen).join(', ')}}` }, - explanation: `Add ${nums[i]} to the 'seen' set and continue to the next index.`, - lineExecution: "seen.add(nums[i]);", - highlightedLines: [7] + variables: { i, num, seen: `{${Array.from(seen).join(', ')}}`, result: true }, + explanation: `Found a duplicate! Since ${num} is already in the set, we return true.`, + pseudoStep: `RETURN true` }); + addLines(5, 5, 6, 7); + return { steps, stepLineNumbers }; } - stepsList.push({ + seen.add(num); + + // Add to set + steps.push({ nums, - highlights: [], + highlights: [i], seen: new Set(seen), - variables: { result: false }, - explanation: "Finished loop without finding any duplicates. Return false.", - lineExecution: "return false;", - highlightedLines: [10] + variables: { i, num, seen: `{${Array.from(seen).join(', ')}}` }, + explanation: `${num} is not a duplicate. Add it to the 'seen' set and continue.`, + pseudoStep: `CALL seen.add(${num})` }); + addLines(7, 6, 8, 9); + } - return stepsList; - }, [nums]); + // Return false if no duplicates + steps.push({ + nums, + highlights: [], + seen: new Set(seen), + variables: { seen: `{${Array.from(seen).join(', ')}}`, result: false }, + explanation: "Finished iterating through the array without finding any duplicates. Return false.", + pseudoStep: "RETURN false" + }); + addLines(9, 7, 10, 11); - const step = steps[currentStep]; + return { steps, stepLineNumbers }; +} + +export const ContainsDuplicateVisualization: React.FC = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + + if (steps.length === 0) return null; + + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( {

Input Array

- {nums.map((num, idx) => { - const isCurrent = step.highlights.includes(idx); + {currentStep.nums.map((num, idx) => { + const isCurrent = currentStep.highlights.includes(idx); return (
{

Seen Set Content

- {step.seen.size === 0 ? ( + {currentStep.seen.size === 0 ? ( Empty Set ) : ( - Array.from(step.seen).map((val) => ( + Array.from(currentStep.seen).map((val) => (
{val}
@@ -165,50 +229,33 @@ export const ContainsDuplicateVisualization: React.FC = () => {
-
- -
-
-

- Current Execution -

-
- {step.lineExecution} -
-
-
-

- Commentary -

-

- {step.explanation} -

-
-
-
+
+ +

+ Commentary +

+

+ {currentStep.explanation} +

+
+
} rightContent={ -
-
- -
- -
- -
-
+ setCurrentStepIndex(0)} + /> } controls={ } /> diff --git a/src/components/visualizations/algorithms/CountingBitsVisualization.tsx b/src/components/visualizations/algorithms/CountingBitsVisualization.tsx index 7b7bef3..834fcdf 100644 --- a/src/components/visualizations/algorithms/CountingBitsVisualization.tsx +++ b/src/components/visualizations/algorithms/CountingBitsVisualization.tsx @@ -1,293 +1,254 @@ -import { useState } from 'react'; -import { SimpleArrayVisualization } from '../shared/SimpleArrayVisualization'; -import { StepControls } from '../shared/StepControls'; -import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import { useEffect, useState } from 'react'; import { Card } from '@/components/ui/card'; +import { VisualizationLayout } from '../shared/VisualizationLayout'; +import { SimpleStepControls } from '../shared/SimpleStepControls'; +import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { array: number[]; - highlighting: number[]; + highlights: number[]; variables: Record; explanation: string; - highlightedLines: number[]; + pseudoStep: string; } +const languages: VisualizationLanguageMap = { + typescript: `function countBits(n: number): number[] { + const ans: number[] = new Array(n + 1).fill(0); + let offset = 1; + for (let i = 1; i <= n; i++) { + if (i === offset * 2) { + offset = i; + } + ans[i] = 1 + ans[i - offset]; + } + return ans; +}`, + python: `def countBits(n: int): + ans = [0] * (n + 1) + offset = 1 + for i in range(1, n + 1): + if i == offset * 2: + offset = i + ans[i] = 1 + ans[i - offset] + return ans`, + java: `public int[] countBits(int n) { + int[] ans = new int[n + 1]; + int offset = 1; + for (int i = 1; i <= n; i++) { + if (i == offset * 2) { + offset = i; + } + ans[i] = 1 + ans[i - offset]; + } + return ans; +}`, + cpp: `vector countBits(int n) { + vector ans(n + 1, 0); + int offset = 1; + for (int i = 1; i <= n; i++) { + if (i == offset * 2) { + offset = i; + } + ans[i] = 1 + ans[i - offset]; + } + return ans; +}` +}; + export const CountingBitsVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); + const [steps, setSteps] = useState([]); + const [stepLineNumbers, setStepLineNumbers] = useState({ + typescript: [], + python: [], + java: [], + cpp: [] + }); + const [currentStepIndex, setCurrentStepIndex] = useState(0); - const steps: Step[] = [ - { - array: [0, 0, 0, 0, 0, 0, 0, 0, 0], - highlighting: [], - variables: { n: 8 }, - explanation: "Starting countBits with n = 8.", - highlightedLines: [1] - }, - { - array: [0, 0, 0, 0, 0, 0, 0, 0, 0], - highlighting: [], - variables: { n: 8, dp: "[0,0,0,0,0,0,0,0,0]" }, - explanation: "Initialize dp array of size n + 1 (9) with zeros.", - highlightedLines: [2] - }, - { - array: [0, 0, 0, 0, 0, 0, 0, 0, 0], - highlighting: [], - variables: { n: 8, dp: "[0,0,0,0,0,0,0,0,0]", offset: 1 }, - explanation: "Initialize offset = 1.", - highlightedLines: [3] - }, - { - array: [0, 0, 0, 0, 0, 0, 0, 0, 0], - highlighting: [1], - variables: { n: 8, dp: "[0,...]", offset: 1, i: 1 }, - explanation: "Start loop at i = 1.", - highlightedLines: [5] - }, - { - array: [0, 0, 0, 0, 0, 0, 0, 0, 0], - highlighting: [1], - variables: { n: 8, dp: "[0,...]", offset: 1, i: 1 }, - explanation: "Check power of 2: offset * 2 (2) === i (1)? False.", - highlightedLines: [6] - }, - { - array: [0, 1, 0, 0, 0, 0, 0, 0, 0], - highlighting: [1, 0], - variables: { n: 8, dp: "[0,1,...]", offset: 1, i: 1 }, - explanation: "dp[1] = 1 + dp[1 - 1] = 1 + dp[0] = 1 + 0 = 1.", - highlightedLines: [9] - }, - { - array: [0, 1, 0, 0, 0, 0, 0, 0, 0], - highlighting: [2], - variables: { n: 8, dp: "[0,1,...]", offset: 1, i: 2 }, - explanation: "Loop i = 2.", - highlightedLines: [5] - }, - { - array: [0, 1, 0, 0, 0, 0, 0, 0, 0], - highlighting: [2], - variables: { n: 8, dp: "[0,1,...]", offset: 1, i: 2 }, - explanation: "Check power of 2: offset * 2 (2) === i (2)? True.", - highlightedLines: [6] - }, - { - array: [0, 1, 0, 0, 0, 0, 0, 0, 0], - highlighting: [2], - variables: { n: 8, dp: "[0,1,...]", offset: 2, i: 2 }, - explanation: "Update offset = i = 2.", - highlightedLines: [7] - }, - { - array: [0, 1, 1, 0, 0, 0, 0, 0, 0], - highlighting: [2, 0], - variables: { n: 8, dp: "[0,1,1,...]", offset: 2, i: 2 }, - explanation: "dp[2] = 1 + dp[2 - 2] = 1 + dp[0] = 1 + 0 = 1.", - highlightedLines: [9] - }, - { - array: [0, 1, 1, 0, 0, 0, 0, 0, 0], - highlighting: [3], - variables: { n: 8, dp: "[0,1,1,...]", offset: 2, i: 3 }, - explanation: "Loop i = 3.", - highlightedLines: [5] - }, - { - array: [0, 1, 1, 0, 0, 0, 0, 0, 0], - highlighting: [3], - variables: { n: 8, dp: "[0,1,1,...]", offset: 2, i: 3 }, - explanation: "Check power of 2: offset * 2 (4) === i (3)? False.", - highlightedLines: [6] - }, - { - array: [0, 1, 1, 2, 0, 0, 0, 0, 0], - highlighting: [3, 1], - variables: { n: 8, dp: "[0,1,1,2,...]", offset: 2, i: 3 }, - explanation: "dp[3] = 1 + dp[3 - 2] = 1 + dp[1] = 1 + 1 = 2.", - highlightedLines: [9] - }, - { - array: [0, 1, 1, 2, 0, 0, 0, 0, 0], - highlighting: [4], - variables: { n: 8, dp: "[0,1,1,2,...]", offset: 2, i: 4 }, - explanation: "Loop i = 4.", - highlightedLines: [5] - }, - { - array: [0, 1, 1, 2, 0, 0, 0, 0, 0], - highlighting: [4], - variables: { n: 8, dp: "[0,1,1,2,...]", offset: 2, i: 4 }, - explanation: "Check power of 2: offset * 2 (4) === i (4)? True.", - highlightedLines: [6] - }, - { - array: [0, 1, 1, 2, 0, 0, 0, 0, 0], - highlighting: [4], - variables: { n: 8, dp: "[0,1,1,2,...]", offset: 4, i: 4 }, - explanation: "Update offset = i = 4.", - highlightedLines: [7] - }, - { - array: [0, 1, 1, 2, 1, 0, 0, 0, 0], - highlighting: [4, 0], - variables: { n: 8, dp: "[0,1,1,2,1,...]", offset: 4, i: 4 }, - explanation: "dp[4] = 1 + dp[4 - 4] = 1 + dp[0] = 1 + 0 = 1.", - highlightedLines: [9] - }, - { - array: [0, 1, 1, 2, 1, 0, 0, 0, 0], - highlighting: [5], - variables: { n: 8, dp: "[0,1,1,2,1,...]", offset: 4, i: 5 }, - explanation: "Loop i = 5.", - highlightedLines: [5] - }, - { - array: [0, 1, 1, 2, 1, 2, 0, 0, 0], - highlighting: [5, 1], - variables: { n: 8, dp: "[0,1,1,2,1,2,...]", offset: 4, i: 5 }, - explanation: "dp[5] = 1 + dp[5 - 4] = 1 + dp[1] = 1 + 1 = 2.", - highlightedLines: [9] - }, - { - array: [0, 1, 1, 2, 1, 2, 0, 0, 0], - highlighting: [6], - variables: { n: 8, dp: "[0,1,1,2,1,2,...]", offset: 4, i: 6 }, - explanation: "Loop i = 6.", - highlightedLines: [5] - }, - { - array: [0, 1, 1, 2, 1, 2, 2, 0, 0], - highlighting: [6, 2], - variables: { n: 8, dp: "[0,1,1,2,1,2,2,...]", offset: 4, i: 6 }, - explanation: "dp[6] = 1 + dp[6 - 4] = 1 + dp[2] = 1 + 1 = 2.", - highlightedLines: [9] - }, - { - array: [0, 1, 1, 2, 1, 2, 2, 0, 0], - highlighting: [7], - variables: { n: 8, dp: "[0,1,1,2,1,2,2,...]", offset: 4, i: 7 }, - explanation: "Loop i = 7.", - highlightedLines: [5] - }, - { - array: [0, 1, 1, 2, 1, 2, 2, 3, 0], - highlighting: [7, 3], - variables: { n: 8, dp: "[0,1,1,2,1,2,2,3,...]", offset: 4, i: 7 }, - explanation: "dp[7] = 1 + dp[7 - 4] = 1 + dp[3] = 1 + 2 = 3.", - highlightedLines: [9] - }, - { - array: [0, 1, 1, 2, 1, 2, 2, 3, 0], - highlighting: [8], - variables: { n: 8, dp: "[0,1,1,2,1,2,2,3,...]", offset: 4, i: 8 }, - explanation: "Loop i = 8.", - highlightedLines: [5] - }, - { - array: [0, 1, 1, 2, 1, 2, 2, 3, 0], - highlighting: [8], - variables: { n: 8, dp: "[0,1,1,2,1,2,2,3,...]", offset: 4, i: 8 }, - explanation: "Check power of 2: offset * 2 (8) === i (8)? True.", - highlightedLines: [6] - }, - { - array: [0, 1, 1, 2, 1, 2, 2, 3, 0], - highlighting: [8], - variables: { n: 8, dp: "[0,1,1,2,1,2,2,3,...]", offset: 8, i: 8 }, - explanation: "Update offset = i = 8.", - highlightedLines: [7] - }, - { - array: [0, 1, 1, 2, 1, 2, 2, 3, 1], - highlighting: [8, 0], - variables: { n: 8, dp: "[0,1,1,2,1,2,2,3,1]", offset: 8, i: 8 }, - explanation: "dp[8] = 1 + dp[8 - 8] = 1 + dp[0] = 1 + 0 = 1.", - highlightedLines: [9] - }, - { - array: [0, 1, 1, 2, 1, 2, 2, 3, 1], - highlighting: [], - variables: { n: 8, dp: "[0,1,1,2,1,2,2,3,1]", offset: 8, i: 9 }, - explanation: "Loop condition i <= n (9 <= 8) is false. Exit loop.", - highlightedLines: [5] - }, - { - array: [0, 1, 1, 2, 1, 2, 2, 3, 1], - highlighting: [], - variables: { n: 8, dp: "[0,1,1,2,1,2,2,3,1]" }, - explanation: "Return dp array.", - highlightedLines: [11] - } - ]; + useEffect(() => { + const n = 8; + const generatedSteps: Step[] = []; + const stepLines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLines.typescript!.push(ts); + stepLines.python!.push(py); + stepLines.java!.push(java); + stepLines.cpp!.push(cpp); + }; + + const dp = new Array(n + 1).fill(0); + + // Function Entry + generatedSteps.push({ + array: [...dp], + highlights: [], + variables: { n, i: '-', offset: '-' }, + explanation: `Count set bits for all numbers from 0 to ${n}.`, + pseudoStep: "FUNCTION countBits(n)" + }); + addLines(1, 1, 1, 1); + + // Array Init + generatedSteps.push({ + array: [...dp], + highlights: [], + variables: { n, i: '-', offset: '-' }, + explanation: `Initialize ans array of size ${n + 1} with 0s.`, + pseudoStep: "SET ans = [0] * (n + 1)" + }); + addLines(2, 2, 2, 2); - const code = `function countBits(n: number): number[] { - const dp: number[] = new Array(n + 1).fill(0); let offset = 1; + // Offset Init + generatedSteps.push({ + array: [...dp], + highlights: [], + variables: { n, i: '-', offset }, + explanation: "Initialize offset to 1 to track the latest power of 2.", + pseudoStep: "SET offset = 1" + }); + addLines(3, 3, 3, 3); for (let i = 1; i <= n; i++) { - if (offset * 2 === i) { - offset = i; - } - dp[i] = 1 + dp[i - offset]; + // Loop Check i + generatedSteps.push({ + array: [...dp], + highlights: [i], + variables: { n, i, offset }, + explanation: `Processing number i = ${i}.`, + pseudoStep: `FOR i = ${i} to ${n}` + }); + addLines(4, 4, 4, 4); + + // Power of 2 check + generatedSteps.push({ + array: [...dp], + highlights: [i], + variables: { n, i, offset }, + explanation: `Check if i (${i}) is the next power of 2 (i === offset * 2 = ${offset * 2}).`, + pseudoStep: `IF i == offset * 2` + }); + addLines(5, 5, 5, 5); + + if (i === offset * 2) { + offset = i; + generatedSteps.push({ + array: [...dp], + highlights: [i], + variables: { n, i, offset }, + explanation: `New power of 2 detected. Update offset to ${offset}.`, + pseudoStep: "SET offset = i" + }); + addLines(6, 6, 6, 6); + } + + dp[i] = 1 + dp[i - offset]; + generatedSteps.push({ + array: [...dp], + highlights: [i, i - offset], + variables: { n, i, offset }, + explanation: `Calculate set bits for ${i} using dynamic programming: ans[${i}] = 1 + ans[${i} - ${offset}] = 1 + ans[${i - offset}] = ${dp[i]}.`, + pseudoStep: `SET ans[i] = 1 + ans[i - offset]` + }); + addLines(8, 7, 8, 8); } - return dp; -}`; - const step = steps[currentStep]; + // Return + generatedSteps.push({ + array: [...dp], + highlights: [], + variables: { n, i: '-', offset }, + explanation: `Return the final computed ans array: [${dp.join(', ')}].`, + pseudoStep: "RETURN ans" + }); + addLines(10, 8, 10, 10); + + setSteps(generatedSteps); + setStepLineNumbers(stepLines); + }, []); + + if (steps.length === 0) return null; + + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( -
- { }} - onPause={() => { }} - onStepForward={() => currentStep < steps.length - 1 && setCurrentStep(prev => prev + 1)} - onStepBack={() => currentStep > 0 && setCurrentStep(prev => prev - 1)} - onReset={() => setCurrentStep(0)} - speed={1} - onSpeedChange={() => { }} - currentStep={currentStep} - totalSteps={steps.length - 1} - /> + + } + leftContent={ +
+
+ +

+ Counting Bits (DP) +

-
-
- - - +
+
+
DP Array (ans)
+
+ {currentStep.array.map((value, index) => { + const isHighlighted = currentStep.highlights.includes(index); + return ( +
+
+ {value} +
+ [{index}] +
+ ); + })} +
+
- -
-
Explanation:
-
- {step.explanation} +
+
DP Relation Logic:
+
• Any number i can be broken into: 1 + ans[i - offset].
+
• The 1 represents the most significant bit (the offset power of 2).
+
ans[i - offset] retrieves the set bits for the remaining value.
+
-
-
- - -

DP Strategy (Offset):

-
-

• Offset tracks the largest power of 2 less than or equal to i.

-

• Bits for i = 1 (most significant bit) + bits for (i - offset).

-
-
+ +
- +
+ +
+

Step Explanation

+

{currentStep.explanation}

+ + +
- - setCurrentStepIndex(0)} /> -
-
+ } + /> ); }; \ No newline at end of file diff --git a/src/components/visualizations/algorithms/CyclicSortVisualization.tsx b/src/components/visualizations/algorithms/CyclicSortVisualization.tsx index 6f964ab..26bf016 100644 --- a/src/components/visualizations/algorithms/CyclicSortVisualization.tsx +++ b/src/components/visualizations/algorithms/CyclicSortVisualization.tsx @@ -1,96 +1,209 @@ import { useEffect, useRef, useState } from 'react'; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { array: number[]; i: number; correctIndex: number; - message: string; - lineNumber: number; + explanation: string; + pseudoStep: string; + variables: Record; comparingIndices: number[]; isSwap?: boolean; } -export const CyclicSortVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const intervalRef = useRef(null); +// ─── Hardcoded code per language (no comments) ────────────────────────────── - const code = `function cyclicSort(nums) { +const languages: VisualizationLanguageMap = { + typescript: `function cyclicSort(nums: number[]): number[] { let i = 0; - while (i < nums.length) { const correctIndex = nums[i] - 1; - if (nums[i] !== nums[correctIndex]) { - // Swap to correct position [nums[i], nums[correctIndex]] = [nums[correctIndex], nums[i]]; } else { i++; } } - return nums; -}`; - - const generateSteps = () => { - const nums = [3, 1, 5, 4, 2]; - const newSteps: Step[] = []; - let i = 0; - - const addStep = (arr: number[], msg: string, line: number, currentI: number, targetIdx: number, comparing: number[] = [], swap: boolean = false) => { - newSteps.push({ - array: [...arr], - i: currentI, - correctIndex: targetIdx, - message: msg, - lineNumber: line, - comparingIndices: comparing, - isSwap: swap - }); - }; +}`, + + python: `def cyclic_sort(nums): + i = 0 + while i < len(nums): + correct_index = nums[i] - 1 + if nums[i] != nums[correct_index]: + nums[i], nums[correct_index] = nums[correct_index], nums[i] + else: + i += 1 + return nums`, + + java: `public static class Solution { + public int[] cyclicSort(int[] nums) { + int i = 0; + while (i < nums.length) { + int correctIndex = nums[i] - 1; + if (nums[i] != nums[correctIndex]) { + int temp = nums[i]; + nums[i] = nums[correctIndex]; + nums[correctIndex] = temp; + } else { + i++; + } + } + return nums; + } +}`, + + cpp: `class Solution { + public: + void cyclicSort(vector &nums) { + int i = 0; + while (i < nums.size()) { + int correctIdx = nums[i] - 1; + if (nums[i] != nums[correctIdx]) { + swap(nums[i], nums[correctIdx]); + } else { + i++; + } + } + } +};`, +}; + +// ─── Step generator ────────────────────────────────────────────────────────── + +function generateVisualizationData() { + const nums = [3, 1, 5, 4, 2]; + const steps: Step[] = []; + const array = [...nums]; + let i = 0; + + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + steps.push({ + array: [...array], + i: 0, + correctIndex: -1, + explanation: 'Start cyclic sort: place each number at its correct index (value - 1).', + pseudoStep: 'START cyclicSort', + variables: { i: 0, 'nums[i]': array[0], correctIndex: '-', 'nums[correctIndex]': '-', array: [...array] }, + comparingIndices: [], + isSwap: false + }); + addLines(2, 2, 3, 4); + + while (i < array.length) { + const val = array[i]; + const correctIndex = val - 1; - addStep(nums, 'Start cyclic sort: place each number at its correct index (value - 1)', 1, 0, -1); + steps.push({ + array: [...array], + i, + correctIndex: -1, + explanation: `Check loop condition: i (${i}) < nums.length (${array.length}) is true.`, + pseudoStep: `WHILE i < nums.length → ${i} < ${array.length} → YES ✓`, + variables: { i, 'nums[i]': val, correctIndex: '-', 'nums[correctIndex]': '-', array: [...array] }, + comparingIndices: [i], + isSwap: false + }); + addLines(3, 3, 4, 5); - while (i < nums.length) { - const val = nums[i]; - const correctIndex = val - 1; + steps.push({ + array: [...array], + i, + correctIndex, + explanation: `Value nums[i] = ${val}. Its correct index is value - 1 = ${correctIndex}.`, + pseudoStep: `SET correctIndex = nums[i] − 1 → ${val} − 1 = ${correctIndex}`, + variables: { i, 'nums[i]': val, correctIndex, 'nums[correctIndex]': array[correctIndex], array: [...array] }, + comparingIndices: [i], + isSwap: false + }); + addLines(4, 4, 5, 6); - // Calculate correctIndex - addStep(nums, `Index i=${i}, value=${val}. Should be at index ${val}-1 = ${correctIndex}`, 5, i, correctIndex, [i]); + steps.push({ + array: [...array], + i, + correctIndex, + explanation: `Compare nums[i] (${val}) with nums[correctIndex] (${array[correctIndex]}).`, + pseudoStep: `IF nums[i] != nums[correctIndex] → ${val} != ${array[correctIndex]} → ${val !== array[correctIndex] ? 'YES ✓' : 'NO ✗'}`, + variables: { i, 'nums[i]': val, correctIndex, 'nums[correctIndex]': array[correctIndex], array: [...array] }, + comparingIndices: [i, correctIndex], + isSwap: false + }); + addLines(5, 5, 6, 7); - // Highlight both for comparison - addStep(nums, `Checking if nums[i] (${val}) is at its correct position nums[${correctIndex}] (${nums[correctIndex]})`, 7, i, correctIndex, [i, correctIndex]); + if (array[i] !== array[correctIndex]) { + const prevIVal = array[i]; + const prevCorrectVal = array[correctIndex]; + [array[i], array[correctIndex]] = [array[correctIndex], array[i]]; - if (nums[i] !== nums[correctIndex]) { - // Prepare for swap - addStep(nums, `Mismatch found! Swapping ${nums[i]} with ${nums[correctIndex]}`, 9, i, correctIndex, [i, correctIndex], true); + steps.push({ + array: [...array], + i, + correctIndex, + explanation: `Mismatch found! Swap elements at indices ${i} (${prevIVal}) and ${correctIndex} (${prevCorrectVal}).`, + pseudoStep: `SWAP nums[i], nums[correctIndex] → swap index ${i} & ${correctIndex}`, + variables: { i, 'nums[i]': array[i], correctIndex, 'nums[correctIndex]': array[correctIndex], array: [...array] }, + comparingIndices: [i, correctIndex], + isSwap: true + }); + addLines(6, 6, 7, 8); + } else { + i++; - // Finalize swap - [nums[i], nums[correctIndex]] = [nums[correctIndex], nums[i]]; - addStep(nums, `Swapped elements. Now nums[${correctIndex}] = ${nums[correctIndex]}`, 9, i, correctIndex, [i, correctIndex]); - } else { - // Increment i - addStep(nums, `${val} is already at index ${i}. Move to next index`, 11, i, correctIndex, [i]); - i++; - addStep(nums, `Incrementing i to ${i}`, 11, i, -1); - } + steps.push({ + array: [...array], + i, + correctIndex, + explanation: `${val} is already at its correct index (${val - 1}). Increment i to ${i}.`, + pseudoStep: 'ELSE: i++ (value is in place, move index forward)', + variables: { i: i < array.length ? i : 'done', 'nums[i]': i < array.length ? array[i] : '-', correctIndex, 'nums[correctIndex]': array[correctIndex], array: [...array] }, + comparingIndices: [i - 1], + isSwap: false + }); + addLines(8, 8, 11, 10); } + } - addStep(nums, 'Complete! All elements are at their correct indices', 15, i, -1); + steps.push({ + array: [...array], + i, + correctIndex: -1, + explanation: 'Loop ends. All elements are sorted and placed at their correct indices.', + pseudoStep: 'RETURN nums', + variables: { i: 'done', 'nums[i]': '-', correctIndex: '-', 'nums[correctIndex]': '-', array: [...array], result: JSON.stringify(array) }, + comparingIndices: [], + isSwap: false + }); + addLines(11, 9, 14, 12); - setSteps(newSteps); - setCurrentStepIndex(0); - }; + return { steps, stepLineNumbers }; +} - useEffect(() => { - generateSteps(); - }, []); +// ─── Component ─────────────────────────────────────────────────────────────── + +export const CyclicSortVisualization = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { @@ -113,17 +226,17 @@ export const CyclicSortVisualization = () => { const handlePlay = () => setIsPlaying(true); const handlePause = () => setIsPlaying(false); - const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(prev => prev + 1); - const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(prev => prev - 1); + const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(p => p + 1); + const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(p => p - 1); const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); - generateSteps(); }; if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return (
@@ -141,33 +254,34 @@ export const CyclicSortVisualization = () => { />
+ {/* Left: visual state */}
-
+
{currentStep.array.map((value, index) => ( -
+
{value}
-
+
Idx {index} -
+
{index === currentStep.i && ( - i + i )} {index === currentStep.correctIndex && ( - target + Target )}
@@ -176,24 +290,40 @@ export const CyclicSortVisualization = () => {
-
-

{currentStep.message}

+
+

{currentStep.explanation}

-
- = 0 ? currentStep.correctIndex : '-', - 'nums[target]': currentStep.correctIndex >= 0 ? currentStep.array[currentStep.correctIndex] : '-', - array: currentStep.array - }} - /> +
+

Cyclic Sort Strategy:

+
+

• Applicable when numbers are in a defined range (e.g. 1 to n)

+

• Iterate through array: value `x` should be at index `x - 1`

+

• If element is not at its correct index, swap it with the element at its correct index

+

• Do not increment `i` on swap; re-evaluate new element at index `i` until it is in place

+

• Time: O(n) · Space: O(1)

+
+ + = 0 ? currentStep.correctIndex : '-', + 'nums[target]': currentStep.correctIndex >= 0 ? currentStep.array[currentStep.correctIndex] : '-', + array: JSON.stringify(currentStep.array) + }} + />
- + {/* Right: code / pseudocode panel */} +
); diff --git a/src/components/visualizations/algorithms/DFSInorderVisualization.tsx b/src/components/visualizations/algorithms/DFSInorderVisualization.tsx index c457c46..76d8a28 100644 --- a/src/components/visualizations/algorithms/DFSInorderVisualization.tsx +++ b/src/components/visualizations/algorithms/DFSInorderVisualization.tsx @@ -1,8 +1,8 @@ import { useEffect, useRef, useState } from 'react'; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface TreeNode { val: number; @@ -14,34 +14,71 @@ interface TreeNode { interface Step { visited: number[]; - current: number | null; + currentNode: number | null; stack: number[]; message: string; - lineNumber: number; + pseudoStep: string; + variables: Record; } -export const DFSInorderVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const [tree, setTree] = useState(null); - const intervalRef = useRef(null); - - const code = `function inorder(root) { - const result = []; - - function traverse(node) { +const languages: VisualizationLanguageMap = { + typescript: `function inorderTraversal(root: TreeNode | null): number[] { + const result: number[] = []; + function dfs(node: TreeNode | null) { if (!node) return; - - traverse(node.left); // Visit left - result.push(node.val); // Visit root - traverse(node.right); // Visit right + dfs(node.left); + result.push(node.val); + dfs(node.right); } - - traverse(root); + dfs(root); return result; -}`; +}`, + + python: `def inorderTraversal(root): + result = [] + def dfs(node): + if not node: + return + dfs(node.left) + result.append(node.val) + dfs(node.right) + dfs(root) + return result`, + + java: `public static class Solution { + private void dfs(TreeNode node, List result) { + if (node == null) { + return; + } + dfs(node.left, result); + result.add(node.val); + dfs(node.right, result); + } + public List inorderTraversal(TreeNode root) { + List result = new ArrayList<>(); + dfs(root, result); + return result; + } + }`, + + cpp: `class Solution { +public: + vector < int > inorderTraversal(TreeNode * root) { + vector < int > result; + dfs(root, result); + return result; + } + void dfs(TreeNode * node, vector & result) { + if (!node) return; + dfs(node -> left, result); + result.push_back(node -> val); + dfs(node -> right, result); + } +};`, +}; + +export const DFSInorderVisualization = () => { + const [tree, setTree] = useState(null); const createTree = (): TreeNode => { return { @@ -72,77 +109,88 @@ export const DFSInorderVisualization = () => { calculatePositions(root, 200, 50, 80); setTree(root); - const newSteps: Step[] = []; + const steps: Step[] = []; const visited: number[] = []; const stack: number[] = []; - const traverse = (node: TreeNode | null) => { - if (!node) return; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - stack.push(node.val); - newSteps.push({ - visited: [...visited], - current: node.val, + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + const addStep = (currentNode: number | null, msg: string, pseudo: string, ts_l: number, py_l: number, java_l: number, cpp_l: number) => { + steps.push({ + currentNode, stack: [...stack], - message: `Visit node ${node.val}, go left first`, - lineNumber: 7 // traverse(node.left) + visited: [...visited], + message: msg, + pseudoStep: pseudo, + variables: { + currentNode: currentNode ?? 'null', + stack: `[${stack.join(', ')}]`, + visited: `[${visited.join(', ')}]`, + 'inorder result': visited.join(' → ') || 'empty' + } }); + addLines(ts_l, py_l, java_l, cpp_l); + }; - traverse(node.left); + // 1. Initialize result + addStep(null, 'Initialize result array.', 'result = []', 2, 2, 11, 4); + // 2. Call dfs(root) + addStep(null, 'Invoke dfs(root) to start traversal.', 'dfs(root)', 9, 9, 12, 5); + + const dfs = (node: TreeNode | null) => { + if (!node) { + addStep(null, 'Node is null. Backtrack.', 'IF node is null → return', 4, 4, 3, 9); + addStep(null, 'Return from null node.', 'return', 4, 5, 4, 9); + return; + } + + // Check if (!node) evaluates to false + stack.push(node.val); + addStep(node.val, `Check if node is null: node (${node.val}) is not null.`, 'IF node is null → NO', 4, 4, 3, 9); + + // Recurse left + addStep(node.val, `Recurse into left subtree of node ${node.val}.`, `dfs(node.left)`, 5, 6, 6, 10); + dfs(node.left); + + // Visit node (inorder) visited.push(node.val); - newSteps.push({ - visited: [...visited], - current: node.val, - stack: [...stack], - message: `Process node ${node.val} (left done, process root)`, - lineNumber: 8 // result.push(node.val) - }); + addStep(node.val, `Visit node ${node.val}. Append it to the result.`, `result.push(${node.val})`, 6, 7, 7, 11); - newSteps.push({ - visited: [...visited], - current: node.val, - stack: [...stack], - message: `Traverse right for node ${node.val}`, - lineNumber: 9 // traverse(node.right) - }); - traverse(node.right); + // Recurse right + addStep(node.val, `Recurse into right subtree of node ${node.val}.`, `dfs(node.right)`, 7, 8, 8, 12); + dfs(node.right); + // Backtrack stack.pop(); - newSteps.push({ - visited: [...visited], - current: node.val, - stack: [...stack], - message: `Node ${node.val} complete`, - lineNumber: 10 // function end - }); + addStep(node.val, `Finished visiting node ${node.val}. Pop from stack and backtrack.`, 'End dfs(node) → Backtrack', 8, 8, 9, 13); }; - newSteps.push({ - visited: [], - current: null, - stack: [], - message: 'Start DFS Inorder: Left → Root → Right', - lineNumber: 12 // traverse(root) - }); - - traverse(root); - - newSteps.push({ - visited, - current: null, - stack: [], - message: `Complete! Inorder: [${visited.join(', ')}]`, - lineNumber: 13 // return result - }); - - setSteps(newSteps); - setCurrentStepIndex(0); + dfs(root); + + // Return result + addStep(null, `Traversal complete. Return inorder result: [${visited.join(', ')}]`, 'return result', 10, 10, 13, 6); + + return { steps, stepLineNumbers }; }; - useEffect(() => { - generateSteps(); - }, []); + const [{ steps, stepLineNumbers }] = useState(generateSteps); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { @@ -170,16 +218,20 @@ export const DFSInorderVisualization = () => { const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); - generateSteps(); }; if (steps.length === 0 || !tree) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); const renderTree = (node: TreeNode | null): JSX.Element | null => { if (!node || node.x === undefined || node.y === undefined) return null; + const isVisited = currentStep.visited.includes(node.val); + const isCurrent = currentStep.currentNode === node.val; + const isInStack = currentStep.stack.includes(node.val); + return ( {node.left && node.left.x !== undefined && node.left.y !== undefined && ( @@ -191,12 +243,14 @@ export const DFSInorderVisualization = () => { @@ -205,7 +259,7 @@ export const DFSInorderVisualization = () => { y={node.y} textAnchor="middle" dy=".3em" - className={`font- ${currentStep.visited.includes(node.val) || currentStep.current === node.val ? 'fill-white' : 'fill-foreground'}`} + className={`text-sm font-semibold ${isVisited || isCurrent ? 'fill-white' : 'fill-foreground'}`} > {node.val} @@ -231,40 +285,62 @@ export const DFSInorderVisualization = () => { />
-
+ {/* Left: visual tree + commentary box + variable panel */} +
{renderTree(tree)}
-
-

{currentStep.message}

-
+
+
+

Call Stack

+
+ {currentStep.stack.map((val, i) => ( +
+ dfs({val}) +
+ ))} + {currentStep.stack.length === 0 &&
Empty
} +
+
-
-

Visited Order:

-
- {currentStep.visited.map((val, idx) => ( -
- {val} -
- ))} +
+

Visited

+
+ {currentStep.visited.map((val, i) => ( +
+ {val} +
+ ))} + {currentStep.visited.length === 0 &&
Empty
} +
-
-
+
+

{currentStep.message}

+
+ -
+ + {/* Right: code / pseudocode panel */} +
); diff --git a/src/components/visualizations/algorithms/DFSPostorderVisualization.tsx b/src/components/visualizations/algorithms/DFSPostorderVisualization.tsx index 36b9db2..853d309 100644 --- a/src/components/visualizations/algorithms/DFSPostorderVisualization.tsx +++ b/src/components/visualizations/algorithms/DFSPostorderVisualization.tsx @@ -1,8 +1,8 @@ import { useEffect, useRef, useState } from 'react'; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface TreeNode { val: number; @@ -14,34 +14,71 @@ interface TreeNode { interface Step { visited: number[]; - current: number | null; + currentNode: number | null; stack: number[]; message: string; - lineNumber: number; + pseudoStep: string; + variables: Record; } -export const DFSPostorderVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const [tree, setTree] = useState(null); - const intervalRef = useRef(null); - - const code = `function postorder(root) { - const result = []; - - function traverse(node) { +const languages: VisualizationLanguageMap = { + typescript: `function postorderTraversal(root: TreeNode | null): number[] { + const result: number[] = []; + function dfs(node: TreeNode | null) { if (!node) return; - - traverse(node.left); // Visit left - traverse(node.right); // Visit right - result.push(node.val); // Visit root + dfs(node.left); + dfs(node.right); + result.push(node.val); } - - traverse(root); + dfs(root); return result; -}`; +}`, + + python: `def postorderTraversal(root): + result = [] + def dfs(node): + if not node: + return + dfs(node.left) + dfs(node.right) + result.append(node.val) + dfs(root) + return result`, + + java: `public static class Solution { + private void dfs(TreeNode node, List result) { + if (node == null) { + return; + } + dfs(node.left, result); + dfs(node.right, result); + result.add(node.val); + } + public List postorderTraversal(TreeNode root) { + List result = new ArrayList<>(); + dfs(root, result); + return result; + } + }`, + + cpp: `class Solution { + public: + vector < int > postorderTraversal(TreeNode * root) { + vector < int > result; + dfs(root, result); + return result; + } + void dfs(TreeNode * node, vector & result) { + if (!node) return; + dfs(node -> left, result); + dfs(node -> right, result); + result.push_back(node -> val); + } +};`, +}; + +export const DFSPostorderVisualization = () => { + const [tree, setTree] = useState(null); const createTree = (): TreeNode => { return { @@ -72,79 +109,88 @@ export const DFSPostorderVisualization = () => { calculatePositions(root, 200, 50, 80); setTree(root); - const newSteps: Step[] = []; + const steps: Step[] = []; const visited: number[] = []; const stack: number[] = []; - const traverse = (node: TreeNode | null) => { - if (!node) return; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - stack.push(node.val); - newSteps.push({ - visited: [...visited], - current: node.val, + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + const addStep = (currentNode: number | null, msg: string, pseudo: string, ts_l: number, py_l: number, java_l: number, cpp_l: number) => { + steps.push({ + currentNode, stack: [...stack], - message: `Visit node ${node.val}, go left first`, - lineNumber: 7 // traverse(node.left) + visited: [...visited], + message: msg, + pseudoStep: pseudo, + variables: { + currentNode: currentNode ?? 'null', + stack: `[${stack.join(', ')}]`, + visited: `[${visited.join(', ')}]`, + 'postorder result': visited.join(' → ') || 'empty' + } }); + addLines(ts_l, py_l, java_l, cpp_l); + }; - traverse(node.left); + // 1. Initialize result + addStep(null, 'Initialize result array.', 'result = []', 2, 2, 11, 4); - newSteps.push({ - visited: [...visited], - current: node.val, - stack: [...stack], - message: `Traverse right for node ${node.val}`, - lineNumber: 8 // traverse(node.right) - }); + // 2. Call dfs(root) + addStep(null, 'Invoke dfs(root) to start traversal.', 'dfs(root)', 9, 9, 12, 5); + + const dfs = (node: TreeNode | null) => { + if (!node) { + addStep(null, 'Node is null. Backtrack.', 'IF node is null → return', 4, 4, 3, 9); + addStep(null, 'Return from null node.', 'return', 4, 5, 4, 9); + return; + } + + // Check if (!node) evaluates to false + stack.push(node.val); + addStep(node.val, `Check if node is null: node (${node.val}) is not null.`, 'IF node is null → NO', 4, 4, 3, 9); + + // Recurse left + addStep(node.val, `Recurse into left subtree of node ${node.val}.`, `dfs(node.left)`, 5, 6, 6, 10); + dfs(node.left); - traverse(node.right); + // Recurse right + addStep(node.val, `Recurse into right subtree of node ${node.val}.`, `dfs(node.right)`, 6, 7, 7, 11); + dfs(node.right); + // Visit node (postorder) visited.push(node.val); - newSteps.push({ - visited: [...visited], - current: node.val, - stack: [...stack], - message: `Process node ${node.val} (both children done)`, - lineNumber: 9 // result.push(node.val) - }); + addStep(node.val, `Visit node ${node.val}. Append it to the result.`, `result.push(${node.val})`, 7, 8, 8, 12); + // Backtrack stack.pop(); - - newSteps.push({ - visited: [...visited], - current: node.val, - stack: [...stack], - message: `Node ${node.val} complete (added to result)`, - lineNumber: 10 // end of traverse function - }); + addStep(node.val, `Finished visiting node ${node.val}. Pop from stack and backtrack.`, 'End dfs(node) → Backtrack', 8, 8, 9, 13); }; - newSteps.push({ - visited: [], - current: null, - stack: [], - message: 'Start DFS Postorder: Left → Right → Root', - lineNumber: 12 // traverse(root) - }); - - traverse(root); - - newSteps.push({ - visited, - current: null, - stack: [], - message: `Complete! Postorder: [${visited.join(', ')}]`, - lineNumber: 13 // return result - }); - - setSteps(newSteps); - setCurrentStepIndex(0); + dfs(root); + + // Return result + addStep(null, `Traversal complete. Return postorder result: [${visited.join(', ')}]`, 'return result', 10, 10, 13, 6); + + return { steps, stepLineNumbers }; }; - useEffect(() => { - generateSteps(); - }, []); + const [{ steps, stepLineNumbers }] = useState(generateSteps); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { @@ -172,16 +218,20 @@ export const DFSPostorderVisualization = () => { const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); - generateSteps(); }; if (steps.length === 0 || !tree) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); const renderTree = (node: TreeNode | null): JSX.Element | null => { if (!node || node.x === undefined || node.y === undefined) return null; + const isVisited = currentStep.visited.includes(node.val); + const isCurrent = currentStep.currentNode === node.val; + const isInStack = currentStep.stack.includes(node.val); + return ( {node.left && node.left.x !== undefined && node.left.y !== undefined && ( @@ -193,12 +243,14 @@ export const DFSPostorderVisualization = () => { @@ -207,7 +259,7 @@ export const DFSPostorderVisualization = () => { y={node.y} textAnchor="middle" dy=".3em" - className={`font- ${currentStep.visited.includes(node.val) || currentStep.current === node.val ? 'fill-white' : 'fill-foreground'}`} + className={`text-sm font-semibold ${isVisited || isCurrent ? 'fill-white' : 'fill-foreground'}`} > {node.val} @@ -233,6 +285,7 @@ export const DFSPostorderVisualization = () => { />
+ {/* Left: visual tree + commentary box + variable panel */}
@@ -240,33 +293,54 @@ export const DFSPostorderVisualization = () => {
-
-

{currentStep.message}

-
+
+
+

Call Stack

+
+ {currentStep.stack.map((val, i) => ( +
+ dfs({val}) +
+ ))} + {currentStep.stack.length === 0 &&
Empty
} +
+
-
-

Visited

-
- {currentStep.visited.map((val, idx) => ( -
- {val} -
- ))} +
+

Visited

+
+ {currentStep.visited.map((val, i) => ( +
+ {val} +
+ ))} + {currentStep.visited.length === 0 &&
Empty
} +
-
-
+
+

{currentStep.message}

+
+ -
+ + {/* Right: code / pseudocode panel */} +
); diff --git a/src/components/visualizations/algorithms/DFSPreorderVisualization.tsx b/src/components/visualizations/algorithms/DFSPreorderVisualization.tsx index 2000b55..4780ebd 100644 --- a/src/components/visualizations/algorithms/DFSPreorderVisualization.tsx +++ b/src/components/visualizations/algorithms/DFSPreorderVisualization.tsx @@ -1,7 +1,8 @@ import { useState, useEffect, useRef } from 'react'; import { VariablePanel } from '../shared/VariablePanel'; import { StepControls } from '../shared/StepControls'; -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface TreeNode { val: number; @@ -14,29 +15,67 @@ interface Step { stack: number[]; visited: number[]; message: string; - lineNumber: number; + pseudoStep: string; + variables: Record; } -export const DFSPreorderVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const intervalRef = useRef(null); - - const code = `function preorderDFS(node) { - if (!node) return; - - // Visit root - result.push(node.val); - - // Traverse left subtree - preorderDFS(node.left); - - // Traverse right subtree - preorderDFS(node.right); -}`; +const languages: VisualizationLanguageMap = { + typescript: `function preorderTraversal(root: TreeNode | null): number[] { + const result: number[] = []; + function dfs(node: TreeNode | null) { + if (!node) return; + result.push(node.val); + dfs(node.left); + dfs(node.right); + } + dfs(root); + return result; +}`, + + python: `def preorderTraversal(root): + result = [] + def dfs(node): + if not node: + return + result.append(node.val) + dfs(node.left) + dfs(node.right) + dfs(root) + return result`, + + java: `public static class Solution { + private void dfs(TreeNode node, List result) { + if (node == null) { + return; + } + result.add(node.val); + dfs(node.left, result); + dfs(node.right, result); + } + public List preorderTraversal(TreeNode root) { + List result = new ArrayList<>(); + dfs(root, result); + return result; + } +}`, + + cpp: `class Solution { +public: + vector < int > preorderTraversal(TreeNode * root) { + vector < int > result; + dfs(root, result); + return result; + } + void dfs(TreeNode * node, vector & result) { + if (!node) return; + result.push_back(node -> val); + dfs(node -> left, result); + dfs(node -> right, result); + } +};`, +}; +export const DFSPreorderVisualization = () => { // Tree structure: 1 -> 2,3 -> 4,5,6,7 const tree: TreeNode = { val: 1, @@ -45,101 +84,89 @@ export const DFSPreorderVisualization = () => { }; const generateSteps = () => { - const newSteps: Step[] = []; + const steps: Step[] = []; const visited: number[] = []; const stack: number[] = []; - newSteps.push({ - currentNode: null, - stack: [], - visited: [], - message: 'Starting DFS Preorder traversal from root', - lineNumber: 1 - }); + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + const addStep = (currentNode: number | null, msg: string, pseudo: string, ts_l: number, py_l: number, java_l: number, cpp_l: number) => { + steps.push({ + currentNode, + stack: [...stack], + visited: [...visited], + message: msg, + pseudoStep: pseudo, + variables: { + currentNode: currentNode ?? 'null', + stack: `[${stack.join(', ')}]`, + visited: `[${visited.join(', ')}]`, + 'preorder result': visited.join(' → ') || 'empty' + } + }); + addLines(ts_l, py_l, java_l, cpp_l); + }; + + // 1. Initialize result + addStep(null, 'Initialize result array.', 'result = []', 2, 2, 11, 4); + + // 2. Call dfs(root) + addStep(null, 'Invoke dfs(root) to start traversal.', 'dfs(root)', 9, 9, 12, 5); const dfs = (node: TreeNode | null, depth: number = 0) => { + // Check if node is null if (!node) { - newSteps.push({ - currentNode: null, - stack: [...stack], - visited: [...visited], - message: 'Node is null, return', - lineNumber: 2 - }); + addStep(null, 'Node is null. Backtrack.', 'IF node is null → return', 4, 4, 3, 9); + addStep(null, 'Return from null node.', 'return', 4, 5, 4, 9); return; } - // Visit root (preorder) - stack.push(node.val); - newSteps.push({ - currentNode: node.val, - stack: [...stack], - visited: [...visited], - message: `Visiting node ${node.val} (depth ${depth})`, - lineNumber: 5 - }); + // Check if (!node) condition (which evaluates to false) + addStep(node.val, `Check if node is null: node (${node.val}) is not null.`, 'IF node is null → NO', 4, 4, 3, 9); + // Visit node (preorder) visited.push(node.val); - newSteps.push({ - currentNode: node.val, - stack: [...stack], - visited: [...visited], - message: `Added ${node.val} to result (Root)`, - lineNumber: 5 - }); + stack.push(node.val); + addStep(node.val, `Visit node ${node.val}. Append it to the result.`, `result.push(${node.val})`, 5, 6, 6, 10); - // Traverse left subtree - if (node.left) { - newSteps.push({ - currentNode: node.val, - stack: [...stack], - visited: [...visited], - message: `Recursing to left child ${node.left.val}`, - lineNumber: 8 - }); - dfs(node.left, depth + 1); - } + // Recurse left + addStep(node.val, `Recurse into left subtree of node ${node.val}.`, `dfs(node.left)`, 6, 7, 7, 11); + dfs(node.left, depth + 1); - // Traverse right subtree - if (node.right) { - newSteps.push({ - currentNode: node.val, - stack: [...stack], - visited: [...visited], - message: `Recursing to right child ${node.right.val}`, - lineNumber: 11 - }); - dfs(node.right, depth + 1); - } + // Recurse right + addStep(node.val, `Recurse into right subtree of node ${node.val}.`, `dfs(node.right)`, 7, 8, 8, 12); + dfs(node.right, depth + 1); // Backtrack stack.pop(); - newSteps.push({ - currentNode: node.val, - stack: [...stack], - visited: [...visited], - message: `Backtracking from node ${node.val}`, - lineNumber: 12 - }); + addStep(node.val, `Finished visiting node ${node.val}. Pop from stack and backtrack.`, 'End dfs(node) → Backtrack', 8, 8, 9, 13); }; dfs(tree); - newSteps.push({ - currentNode: null, - stack: [], - visited: [...visited], - message: `Complete! Preorder: [${visited.join(', ')}]`, - lineNumber: 12 - }); + // Return result + addStep(null, `Traversal complete. Return preorder result: [${visited.join(', ')}]`, 'return result', 10, 10, 13, 6); - setSteps(newSteps); - setCurrentStepIndex(0); + return { steps, stepLineNumbers }; }; - useEffect(() => { - generateSteps(); - }, []); + const [{ steps, stepLineNumbers }] = useState(generateSteps); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { @@ -167,12 +194,12 @@ export const DFSPreorderVisualization = () => { const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); - generateSteps(); }; if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); const renderTree = (node: TreeNode | null, x: number, y: number, offset: number): JSX.Element[] => { if (!node) return []; @@ -208,7 +235,7 @@ export const DFSPreorderVisualization = () => { strokeWidth="2" className="transition-all duration-300" /> - + {node.val} @@ -233,6 +260,7 @@ export const DFSPreorderVisualization = () => { />
+ {/* Left: visual tree + commentary box + variable panel */}
@@ -242,25 +270,26 @@ export const DFSPreorderVisualization = () => {
-

Call Stack

+

Call Stack

{currentStep.stack.map((val, i) => ( -
+
dfs({val})
))} - {currentStep.stack.length === 0 &&
Empty
} + {currentStep.stack.length === 0 &&
Empty
}
-

Visited

+

Visited

{currentStep.visited.map((val, i) => ( -
+
{val}
))} + {currentStep.visited.length === 0 &&
Empty
}
@@ -268,19 +297,25 @@ export const DFSPreorderVisualization = () => {

{currentStep.message}

-
-
-
+ + {/* Right: code / pseudocode panel */} +
); diff --git a/src/components/visualizations/algorithms/DecodeWaysVisualization.tsx b/src/components/visualizations/algorithms/DecodeWaysVisualization.tsx index 05dafa8..63c2004 100644 --- a/src/components/visualizations/algorithms/DecodeWaysVisualization.tsx +++ b/src/components/visualizations/algorithms/DecodeWaysVisualization.tsx @@ -2,8 +2,9 @@ import React, { useState, useMemo } from 'react'; import { Card } from '@/components/ui/card'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { s: string; @@ -13,21 +14,17 @@ interface Step { stack: number[]; variables: Record; explanation: string; - highlightedLines: number[]; - lineExecution: string; + pseudoStep: string; + calc?: string; } -export const DecodeWaysVisualization: React.FC = () => { - const [currentStep, setCurrentStep] = useState(0); - const s = "11106"; - - const code = `function numDecodings(s: string): number { +const languages: VisualizationLanguageMap = { + typescript: `function numDecodings(s: string): number { const dp: Map = new Map(); dp.set(s.length, 1); function dfs(i: number): number { if (dp.has(i)) return dp.get(i)!; if (s[i] === '0') return 0; - let res = dfs(i + 1); if ( i + 1 < s.length && @@ -35,19 +32,109 @@ export const DecodeWaysVisualization: React.FC = () => { ) { res += dfs(i + 2); } - dp.set(i, res); return res; } return dfs(0); -}`; +}`, + python: `def numDecodings(s: str) -> int: + dp = {len(s): 1} + def dfs(i: int) -> int: + if i in dp: + return dp[i] + if s[i] == '0': + return 0 + res = dfs(i + 1) + if ( + i + 1 < len(s) and + (s[i] == '1' or (s[i] == '2' and s[i + 1] in "0123456")) + ): + res += dfs(i + 2) + dp[i] = res + return res + return dfs(0)`, + java: `public static class Solution { + public int dfs(int i, String s, Map dp) { + if (dp.containsKey(i)) { + return dp.get(i); + } + if (i == s.length()) { + return 1; + } + if (s.charAt(i) == '0') { + return 0; + } + int res = dfs(i + 1, s, dp); + if ( + i + 1 < s.length() && + (s.charAt(i) == '1' || + (s.charAt(i) == '2' && s.charAt(i + 1) <= '6')) + ) { + res += dfs(i + 2, s, dp); + } + dp.put(i, res); + return res; + } + public int numDecodings(String s) { + Map dp = new HashMap<>(); + dp.put(s.length(), 1); + return dfs(0, s, dp); + } +}`, + cpp: `class Solution { + public: + int dfs(int i, string& s, unordered_map& dp) { + if (dp.count(i)) { + return dp[i]; + } + if (i == s.size()) { + return 1; + } + if (s[i] == '0') { + return 0; + } + int res = 0; + res += dfs(i + 1, s, dp); + if ( + i + 1 < s.size() && + (s[i] == '1' || (s[i] == '2' && s[i + 1] <= '6')) + ) { + res += dfs(i + 2, s, dp); + } + dp[i] = res; + return res; + } + int numDecodings(string s) { + unordered_map dp; + return dfs(0, s, dp); + } +};` +}; - const steps = useMemo(() => { +export const DecodeWaysVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const s = "11106"; + + const { steps, stepLineNumbers } = useMemo(() => { const stepsList: Step[] = []; const memo = new Map(); const stack: number[] = []; const n = s.length; + const lines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + lines.typescript!.push(ts); + lines.python!.push(py); + lines.java!.push(java); + lines.cpp!.push(cpp); + }; + // 1. Initial Function call stepsList.push({ s, memo: new Map(memo), @@ -55,11 +142,12 @@ export const DecodeWaysVisualization: React.FC = () => { res: null, stack: [...stack], variables: { s: `"${s}"` }, - explanation: `Problem: Count the number of ways to decode "${s}" using the mapping 1=A, 2=B, ..., 26=Z. We will use Depth-First Search with Memoization to explore all valid decoding paths.`, - lineExecution: "function numDecodings(s: string): number {", - highlightedLines: [1] + explanation: `Determine number of ways to decode "${s}" using mapping 1=A, ..., 26=Z. We use DFS with memoization.`, + pseudoStep: "CALL numDecodings(s)" }); + addLines(1, 1, 23, 24); + // 2. Setup map and base case memo.set(n, 1); stepsList.push({ s, @@ -67,15 +155,15 @@ export const DecodeWaysVisualization: React.FC = () => { i: null, res: null, stack: [...stack], - variables: { "memo[5]": 1 }, - explanation: `Initialize the memoization Map. Base case: Reaching the end of the string (index ${n}) is a successful decoding. dp.set(${n}, 1).`, - lineExecution: "const dp: Map = new Map();\ndp.set(s.length, 1);", - highlightedLines: [2, 3] + variables: { "dp[5]": 1 }, + explanation: `Initialize DP memo map. Base Case: Reaching index ${n} (end of string) is 1 valid path: dp.set(${n}, 1).`, + pseudoStep: `SET dp[${n}] = 1` }); + addLines(3, 2, 24, 25); const solve = (i: number): number => { stack.push(i); - + stepsList.push({ s, memo: new Map(memo), @@ -83,10 +171,10 @@ export const DecodeWaysVisualization: React.FC = () => { res: null, stack: [...stack], variables: { i, stack: stack.join(" → ") }, - explanation: `Enter dfs(${i}). We are looking at the character '${s[i]}' at index ${i}. First, check the memo Map to see if we've already solved this subproblem.`, - lineExecution: "function dfs(i: number): number {", - highlightedLines: [4] + explanation: `Call dfs(${i}). Inspect character '${s[i]}' at index ${i}. Check if computed in memo map.`, + pseudoStep: `CALL dfs(${i})` }); + addLines(4, 3, 2, 3); if (memo.has(i)) { const val = memo.get(i)!; @@ -96,11 +184,11 @@ export const DecodeWaysVisualization: React.FC = () => { i, res: val, stack: [...stack], - variables: { i, "memo.get(i)": val }, - explanation: `MEMO HIT: The result for index ${i} is already in our Map (${val}). We return this value to skip re-computation.`, - lineExecution: "if (dp.has(i)) return dp.get(i)!;", - highlightedLines: [5] + variables: { i, "dp.get(i)": val }, + explanation: `Memo Hit! dp[${i}] has value ${val}. Return ${val}.`, + pseudoStep: `RETURN dp[${i}] (${val})` }); + addLines(5, 4, 3, 4); stack.pop(); return val; } @@ -113,10 +201,10 @@ export const DecodeWaysVisualization: React.FC = () => { res: 0, stack: [...stack], variables: { i, "s[i]": "0" }, - explanation: `Invalid leading zero: The character at index ${i} is '0'. A valid decoding cannot start with '0'. This path returns 0 ways.`, - lineExecution: "if (s[i] === '0') return 0;", - highlightedLines: [6] + explanation: `Leading zero check: s[${i}] is '0'. Since a valid letter code cannot start with '0', return 0.`, + pseudoStep: "RETURN 0" }); + addLines(6, 6, 9, 10); stack.pop(); return 0; } @@ -128,13 +216,13 @@ export const DecodeWaysVisualization: React.FC = () => { res: null, stack: [...stack], variables: { i, next_call: `dfs(${i + 1})` }, - explanation: `Valid single-digit decoding: Taking the number ${s[i]} as a letter (e.g., '${s[i]}' → '${String.fromCharCode(64 + parseInt(s[i]))}'). Now, call dfs(${i + 1}) to explore the rest of the string.`, - lineExecution: "let res = dfs(i + 1);", - highlightedLines: [8] + explanation: `Single digit path: Decode '${s[i]}' as a single letter. Call dfs(${i + 1}) to process the remainder.`, + pseudoStep: `SET res = dfs(${i + 1})` }); - + addLines(7, 8, 12, 14); + let res = solve(i + 1); - + stepsList.push({ s, memo: new Map(memo), @@ -142,12 +230,13 @@ export const DecodeWaysVisualization: React.FC = () => { res, stack: [...stack], variables: { i, res }, - explanation: `Returned to dfs(${i}). The single-digit path starting at index ${i} found ${res} ways. Now, check if taking two digits starting at index ${i} forms a valid letter code (10–26).`, - lineExecution: "let res = dfs(i + 1);", - highlightedLines: [8] + explanation: `Returned to dfs(${i}). Single-digit branch yielded ${res} way(s). Now check if two digits starting at index ${i} are valid (10-26).`, + pseudoStep: `SET res = ${res}` }); + addLines(7, 8, 12, 14); - if (i + 1 < n && (s[i] === '1' || (s[i] === '2' && '0123456'.includes(s[i + 1])))) { + const isValidDouble = i + 1 < n && (s[i] === '1' || (s[i] === '2' && '0123456'.includes(s[i + 1]))); + if (isValidDouble) { const combined = s.substring(i, i + 2); stepsList.push({ s, @@ -155,24 +244,26 @@ export const DecodeWaysVisualization: React.FC = () => { i, res, stack: [...stack], - variables: { i, combined_digits: combined, next_call: `dfs(${i + 2})` }, - explanation: `Valid two-digit decoding: The digits "${combined}" form a valid letter (10–26). Now, call dfs(${i + 2}) to explore this branch.`, - lineExecution: "res += dfs(i + 2);", - highlightedLines: [13] + variables: { i, combined, next_call: `dfs(${i + 2})` }, + explanation: `Two-digit path: "${combined}" is valid (10–26). Call dfs(${i + 2}) to find ways from this branch.`, + pseudoStep: `res += dfs(${i + 2})` }); + addLines(12, 13, 18, 19); + const res2 = solve(i + 2); res += res2; + stepsList.push({ s, memo: new Map(memo), i, res, stack: [...stack], - variables: { i, ways_from_combined: res2, total_ways_so_far: res }, - explanation: `Returned to dfs(${i}). The two-digit path ("${combined}") added ${res2} more way(s). The current total for index ${i} is now ${res}.`, - lineExecution: "res += dfs(i + 2);", - highlightedLines: [13] + variables: { i, ways_from_combined: res2, total_ways: res }, + explanation: `Returned to dfs(${i}). Two-digit path added ${res2} way(s). New total: ${res}.`, + pseudoStep: `SET res = ${res}` }); + addLines(12, 13, 18, 19); } else { const combined = i + 1 < n ? s.substring(i, i + 2) : "n/a"; stepsList.push({ @@ -181,13 +272,13 @@ export const DecodeWaysVisualization: React.FC = () => { i, res, stack: [...stack], - variables: { i, combined_digits: combined }, - explanation: combined !== "n/a" - ? `Two-digit check: "${combined}" is NOT a valid letter code (>26 or starting with '0'). Skipping this branch.` - : `Two-digit check: End of string reached. No more pairs possible.`, - lineExecution: "if (i + 1 < s.length && ...)", - highlightedLines: [10, 11] + variables: { i, combined }, + explanation: combined !== "n/a" + ? `Two-digit path check: "${combined}" is NOT valid (>26 or starting with 0). Skip two-digit branch.` + : `Two-digit path check: End of string reached, cannot take two digits.`, + pseudoStep: "IF isValidDouble -> NO ✗" }); + addLines(8, 9, 13, 15); } memo.set(i, res); @@ -197,12 +288,12 @@ export const DecodeWaysVisualization: React.FC = () => { i, res, stack: [...stack], - variables: { i, final_subproblem_result: res }, - explanation: `Completed subproblem for index ${i}. The final count of ways to decode "${s.substring(i)}" is ${res}. We store this in our memo and return to the caller.`, - lineExecution: "dp.set(i, res);\nreturn res;", - highlightedLines: [16, 17] + variables: { i, final_res: res }, + explanation: `Save subproblem result: dp.set(${i}, ${res}). Return ${res} ways.`, + pseudoStep: `SET dp[${i}] = ${res}, RETURN` }); - + addLines(14, 14, 20, 21); + stack.pop(); return res; }; @@ -214,10 +305,10 @@ export const DecodeWaysVisualization: React.FC = () => { res: null, stack: [], variables: { initial_call: "dfs(0)" }, - explanation: "Now calling the recursive DFS function starting at the very beginning of the string (index 0).", - lineExecution: "return dfs(0);", - highlightedLines: [20] + explanation: "Launch recursive DFS traversal starting at index 0.", + pseudoStep: "RETURN dfs(0)" }); + addLines(17, 16, 26, 26); const finalRes = solve(0); @@ -228,52 +319,66 @@ export const DecodeWaysVisualization: React.FC = () => { res: finalRes, stack: [], variables: { final_result: finalRes }, - explanation: `ALGORITHM COMPLETE. The final result for dfs(0) is ${finalRes}. There are exactly ${finalRes} different ways to decode "${s}". (Paths: AAJF, KJF)`, - lineExecution: "return final_result;", - highlightedLines: [20] + explanation: `Algorithm Complete! dfs(0) returned ${finalRes}. There are exactly ${finalRes} ways to decode "${s}".`, + pseudoStep: `RETURN ${finalRes}` }); + addLines(17, 16, 26, 26); - return stepsList; - }, [s]); + return { steps: stepsList, stepLineNumbers: lines }; + }, []); - const step = steps[currentStep]; + const step = steps[currentStepIndex]; + const pseudoSteps = steps.map((s) => s.pseudoStep); return ( -
-

- Decode Ways (DFS + Memoization) -

+
+
-

Input String Inspection

+

+ Input String (s = "{s}") +

{s.split('').map((char, idx) => { const isProcessing = step.i === idx; const isInStack = step.stack.includes(idx); - + return ( -
-
+
{char}
- {isProcessing &&
DFS({idx})
} - {!isProcessing && isInStack &&
Wait
} + {isProcessing && ( + + dfs({idx}) + + )} + {!isProcessing && isInStack && ( + + Wait + + )}
); })} -
-
+
+
END
@@ -281,24 +386,32 @@ export const DecodeWaysVisualization: React.FC = () => {
-

Memoization Table (dp Map)

-
+

+ Memoization Table (dp Map) +

+
{Array.from({ length: s.length + 1 }).map((_, idx) => { const hasValue = step.memo.has(idx); const val = step.memo.get(idx); const isProcessing = step.i === idx; - + return ( -
-
+
- dp[{idx}] - {hasValue ? val : "?"} + + dp[{idx}] + + + {hasValue ? val : "?"} +
); @@ -307,69 +420,82 @@ export const DecodeWaysVisualization: React.FC = () => {
{step.stack.length > 0 && ( -
-

Active Call Stack

+
+

+ Active Call Stack +

{step.stack.map((idx, pos) => ( -
+
dfs({idx})
- {pos < step.stack.length - 1 && } + {pos < step.stack.length - 1 && ( + + )} ))}
)} + + {step.calc && ( + +

Calculation

+

{step.calc}

+
+ )}
-
- -
-
-

- Current Execution -

-
- {step.lineExecution} -
-
-
-

- Commentary -

-

- {step.explanation} -

-
-
-
+
+ +

Step Explanation

+

+ {step.explanation} +

+
+ + + + +

+ Why this works +

+

+ To decode a string, we can try taking 1 digit (if valid, i.e., not '0') or 2 digits (if valid, between 10 and 26). +

+

+ The subproblem `dfs(i)` returns the number of valid decodings starting at index `i`. +

+

+ Without memoization, the call tree has overlapping subproblems that lead to exponential O(2^N) time. +

+

+ By storing computed subproblem states in a memo map, we ensure each index is solved at most once, reducing time complexity to O(N). +

+
} rightContent={ -
-
- -
- -
- -
-
+ setCurrentStepIndex(0)} + /> } controls={ } /> ); }; + +export default DecodeWaysVisualization; diff --git a/src/components/visualizations/algorithms/DijkstrasVisualization.tsx b/src/components/visualizations/algorithms/DijkstrasVisualization.tsx index 43ac800..7748a05 100644 --- a/src/components/visualizations/algorithms/DijkstrasVisualization.tsx +++ b/src/components/visualizations/algorithms/DijkstrasVisualization.tsx @@ -1,8 +1,8 @@ import { useEffect, useRef, useState } from 'react'; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { distances: Record; @@ -10,234 +10,334 @@ interface Step { minHeap: [number, number][]; currentNode: number | null; maxTime: number; - message: string; - lineNumber: number; + explanation: string; + pseudoStep: string; + variables: Record; } -export const DijkstrasVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const intervalRef = useRef(null); - - const code = `function networkDelayTime(times: number[][], n: number, k: number): number { - const adj: Map = new Map(); +const languages: VisualizationLanguageMap = { + typescript: `function networkDelayTime(times: number[][], n: number, k: number): number { + const edges: Map = new Map(); for (const [u, v, w] of times) { - if (!adj.has(u)) adj.set(u, []); - adj.get(u)!.push([v, w]) + if (!edges.has(u)) { + edges.set(u, []); + } + edges.get(u)!.push([v, w]); } - const minHeap: [number, number][] = [[0, k]]; const visit = new Set(); let t = 0; - while (minHeap.length > 0) { minHeap.sort((a, b) => a[0] - b[0]); const [w1, n1] = minHeap.shift()!; - if (visit.has(n1)) continue; visit.add(n1); t = Math.max(t, w1); - - if (adj.has(n1)) { - for (const [n2, w2] of adj.get(n1)!) { - if (!visit.has(n2)) { - minHeap.push([w1 + w2, n2]); - } + const neighbors = edges.get(n1) || []; + for (const [n2, w2] of neighbors) { + if (!visit.has(n2)) { + minHeap.push([w1 + w2, n2]); } } } - return visit.size === n ? t : -1; -}`; - - const generateSteps = () => { - const n = 4; - const k = 2; - const times = [[2, 1, 1], [2, 3, 1], [3, 4, 1]]; +}`, + python: `def networkDelayTime(times: list[list[int]], n: int, k: int) -> int: + edges = {} + for u, v, w in times: + if u not in edges: + edges[u] = [] + edges[u].append((v, w)) + minHeap = [(0, k)] + visit = set() + t = 0 + while minHeap: + minHeap.sort() + w1, n1 = minHeap.pop(0) + if n1 in visit: + continue + visit.add(n1) + t = max(t, w1) + if n1 in edges: + for n2, w2 in edges[n1]: + if n2 not in visit: + minHeap.append((w1 + w2, n2)) + return t if len(visit) == n else -1`, + java: `public static class Solution { + public int networkDelayTime(int[][] times, int n, int k) { + Map> edges = new HashMap<>(); + for (int[] time : times) { + int u = time[0]; + int v = time[1]; + int w = time[2]; + edges.computeIfAbsent(u, key -> new ArrayList<>()).add(new int[]{v, w}); + } + PriorityQueue minHeap = new PriorityQueue<>(Comparator.comparingInt(a -> a[0])); + minHeap.offer(new int[]{0, k}); + Set visit = new HashSet<>(); + int t = 0; + while (!minHeap.isEmpty()) { + int[] current = minHeap.poll(); + int w1 = current[0]; + int n1 = current[1]; + if (visit.contains(n1)) continue; + visit.add(n1); + t = Math.max(t, w1); + List neighbors = edges.getOrDefault(n1, new ArrayList<>()); + for (int[] neighbor : neighbors) { + int n2 = neighbor[0]; + int w2 = neighbor[1]; + if (!visit.contains(n2)) { + minHeap.offer(new int[]{w1 + w2, n2}); + } + } + } + return visit.size() == n ? t : -1; + } +}`, + cpp: `class Solution { +public: + int networkDelayTime(vector>& times, int n, int k) { + unordered_map>> edges; + for (auto &time : times) { + int u = time[0]; + int v = time[1]; + int w = time[2]; + edges[u].push_back({v, w}); + } + priority_queue< + pair, + vector>, + greater> + > minHeap; + minHeap.push({0, k}); + set visit; + int t = 0; + while (!minHeap.empty()) { + auto [w1, n1] = minHeap.top(); + minHeap.pop(); + if (visit.count(n1)) continue; + visit.insert(n1); + t = max(t, w1); + for (auto &[n2, w2] : edges[n1]) { + if (!visit.count(n2)) { + minHeap.push({w1 + w2, n2}); + } + } + } + return visit.size() == n ? t : -1; + } +};`, +}; - const newSteps: Step[] = []; - const edges: Map = new Map(); - const distances: Record = {}; - const visit = new Set(); - let t = 0; +function generateVisualizationData() { + const n = 4; + const k = 2; + const times = [[2, 1, 1], [2, 3, 1], [3, 4, 1]]; + + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { typescript: [], python: [], java: [], cpp: [] }; + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - newSteps.push({ + const edges: Map = new Map(); + const distances: Record = {}; + const visit = new Set(); + let t = 0; + + // Initialize + steps.push({ + distances: { ...distances }, + visited: Array.from(visit), + minHeap: [], + currentNode: null, + maxTime: t, + explanation: 'Initialize the edges adjacency list.', + pseudoStep: 'SET edges = empty Map()', + variables: { t, visited: '{}', minHeap: '[]' } + }); + addLines(2, 2, 3, 4); + + for (const [u, v, w] of times) { + if (!edges.has(u)) edges.set(u, []); + edges.get(u)!.push([v, w]); + } + + const minHeap: [number, number][] = [[0, k]]; + distances[k] = 0; + + steps.push({ + distances: { ...distances }, + visited: Array.from(visit), + minHeap: [...minHeap], + currentNode: null, + maxTime: t, + explanation: `Start Dijkstra from source node k=${k} with distance/time 0.`, + pseudoStep: `SET minHeap = [[0, k]]`, + variables: { t, visited: '{}', minHeap: `[[0, ${k}]]` } + }); + addLines(9, 7, 11, 16); + + while (minHeap.length > 0) { + steps.push({ distances: { ...distances }, visited: Array.from(visit), - minHeap: [], + minHeap: [...minHeap].sort((a, b) => a[0] - b[0]), currentNode: null, maxTime: t, - message: 'Initializing adjacency list...', - lineNumber: 2 + explanation: 'Sort the min-heap to pick the node with the smallest cumulative delay.', + pseudoStep: 'minHeap.sort()', + variables: { t, visited: `{${Array.from(visit).join(', ')}}`, minHeap: `[${minHeap.map(h => `[${h[0]},${h[1]}]`).join(', ')}]` } }); + addLines(13, 11, 15, 20); - for (const [u, v, w] of times) { - if (!edges.has(u)) edges.set(u, []); - edges.get(u)!.push([v, w]); - } + minHeap.sort((a, b) => a[0] - b[0]); + const [w1, n1] = minHeap.shift()!; - const minHeap: [number, number][] = [[0, k]]; - newSteps.push({ + steps.push({ distances: { ...distances }, visited: Array.from(visit), minHeap: [...minHeap], - currentNode: null, + currentNode: n1, maxTime: t, - message: `Starting Dijkstra from node ${k} with time 0.`, - lineNumber: 8 + explanation: `Dequeue node ${n1} with time ${w1} from min-heap.`, + pseudoStep: `SET [w1, n1] = minHeap.shift() → [${w1}, ${n1}]`, + variables: { t, n1, w1, visited: `{${Array.from(visit).join(', ')}}`, minHeap: `[${minHeap.map(h => `[${h[0]},${h[1]}]`).join(', ')}]` } }); + addLines(14, 12, 15, 21); - while (minHeap.length > 0) { - newSteps.push({ - distances: { ...distances }, - visited: Array.from(visit), - minHeap: [...minHeap], - currentNode: null, - maxTime: t, - message: 'Sorting minHeap by time.', - lineNumber: 15 - }); - minHeap.sort((a, b) => a[0] - b[0]); + steps.push({ + distances: { ...distances }, + visited: Array.from(visit), + minHeap: [...minHeap], + currentNode: n1, + maxTime: t, + explanation: `Check if node ${n1} has been visited already.`, + pseudoStep: `IF node ${n1} IN visit → ${visit.has(n1) ? 'YES ✓ (skip)' : 'NO ✗'}`, + variables: { t, n1, visited: `{${Array.from(visit).join(', ')}}` } + }); + addLines(15, 13, 18, 22); - const [w1, n1] = minHeap.shift()!; - newSteps.push({ - distances: { ...distances }, - visited: Array.from(visit), - minHeap: [...minHeap], - currentNode: n1, - maxTime: t, - message: `Popped node ${n1} with current time ${w1}.`, - lineNumber: 17 - }); + if (visit.has(n1)) continue; - if (visit.has(n1)) { - newSteps.push({ - distances: { ...distances }, - visited: Array.from(visit), - minHeap: [...minHeap], - currentNode: n1, - maxTime: t, - message: `Node ${n1} already visited. Skipping.`, - lineNumber: 20 - }); - continue; - } + visit.add(n1); + t = Math.max(t, w1); + distances[n1] = w1; - visit.add(n1); - distances[n1] = w1; - t = Math.max(t, w1); - newSteps.push({ + steps.push({ + distances: { ...distances }, + visited: Array.from(visit), + minHeap: [...minHeap], + currentNode: n1, + maxTime: t, + explanation: `Mark node ${n1} as visited. Update max delay time t = max(${t}, ${w1}) = ${t}.`, + pseudoStep: `visit.add(${n1}); t = max(t, ${w1}) → ${t}`, + variables: { t, n1, visited: `{${Array.from(visit).join(', ')}}` } + }); + addLines(16, 15, 19, 23); + + const neighbors = edges.get(n1) || []; + for (const [n2, w2] of neighbors) { + steps.push({ distances: { ...distances }, visited: Array.from(visit), minHeap: [...minHeap], currentNode: n1, - maxTime: t, - message: `Marking node ${n1} as visited. Updated max time t = ${t}.`, - lineNumber: 24 + explanation: `Inspect neighbor ${n2} of node ${n1} with edge weight ${w2}.`, + pseudoStep: `FOR [n2, w2] OF neighbors(${n1}): inspect neighbor ${n2}`, + variables: { t, n1, n2, w2 } }); + addLines(19, 18, 22, 25); + + if (!visit.has(n2)) { + minHeap.push([w1 + w2, n2]); + if (distances[n2] === undefined || w1 + w2 < distances[n2]) { + distances[n2] = w1 + w2; + } - const neighbors = edges.get(n1) || []; - if (neighbors.length > 0) { - newSteps.push({ + steps.push({ distances: { ...distances }, visited: Array.from(visit), minHeap: [...minHeap], currentNode: n1, - maxTime: t, - message: `Checking neighbors of node ${n1}.`, - lineNumber: 27 + explanation: `Neighbor ${n2} not visited. Push updated distance ${w1 + w2} for node ${n2} to min-heap.`, + pseudoStep: `minHeap.push([${w1 + w2}, ${n2}])`, + variables: { t, n1, n2, w2, minHeap: `[${minHeap.map(h => `[${h[0]},${h[1]}]`).join(', ')}]` } }); - } - - for (const [n2, w2] of neighbors) { - if (!visit.has(n2)) { - minHeap.push([w1 + w2, n2]); - newSteps.push({ - distances: { ...distances }, - visited: Array.from(visit), - minHeap: [...minHeap], - currentNode: n1, - maxTime: t, - message: `Adding node ${n2} to heap with distance ${w1 + w2}.`, - lineNumber: 29 - }); - } + addLines(21, 20, 26, 27); } } + } + + steps.push({ + distances: { ...distances }, + visited: Array.from(visit), + minHeap: [], + currentNode: null, + maxTime: t, + explanation: `Min-heap empty. Check if all ${n} nodes are reachable. Visited count = ${visit.size}.`, + pseudoStep: `RETURN visit.size == n ? t : -1 → ${visit.size === n ? t : -1}`, + variables: { t, visitedCount: visit.size, finalResult: visit.size === n ? t : -1 } + }); + addLines(25, 21, 30, 31); + + return { steps, stepLineNumbers }; +} - newSteps.push({ - distances: { ...distances }, - visited: Array.from(visit), - minHeap: [...minHeap], - currentNode: null, - maxTime: t, - message: visit.size === n ? `All nodes reachable. Max time: ${t}` : 'Not all nodes reached.', - lineNumber: 31 - }); - - setSteps(newSteps); - setCurrentStepIndex(0); +const nodePositions = [ + { x: 50, y: 150 }, // 1 + { x: 150, y: 50 }, // 2 + { x: 250, y: 150 }, // 3 + { x: 350, y: 50 } // 4 +]; + +const getEdgeLine = (start: { x: number; y: number }, end: { x: number; y: number }, offset = 18) => { + const dx = end.x - start.x; + const dy = end.y - start.y; + const length = Math.sqrt(dx * dx + dy * dy); + return { + x1: start.x + (dx / length) * offset, + y1: start.y + (dy / length) * offset, + x2: end.x - (dx / length) * offset, + y2: end.y - (dy / length) * offset, }; +}; - useEffect(() => { - generateSteps(); - }, []); +export const DijkstrasVisualization = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { intervalRef.current = setInterval(() => { setCurrentStepIndex(prev => { - if (prev >= steps.length - 1) { - setIsPlaying(false); - return prev; - } + if (prev >= steps.length - 1) { setIsPlaying(false); return prev; } return prev + 1; }); }, 1000 / speed); } else { if (intervalRef.current) clearInterval(intervalRef.current); } - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - }; + return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [isPlaying, currentStepIndex, steps.length, speed]); const handlePlay = () => setIsPlaying(true); const handlePause = () => setIsPlaying(false); - const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(prev => prev + 1); - const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(prev => prev - 1); - const handleReset = () => { - setCurrentStepIndex(0); - setIsPlaying(false); - generateSteps(); - }; + const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(p => p + 1); + const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(p => p - 1); + const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); }; if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; - - // Node positions for a simple graph loop - const nodePositions = [ - { x: 50, y: 150 }, // 1 - { x: 150, y: 50 }, // 2 - { x: 250, y: 150 }, // 3 - { x: 350, y: 50 } // 4 - ]; - - // Helper to calculate edge line with offset so marker doesn't overlap circle - const getEdgeLine = (start: { x: number; y: number }, end: { x: number; y: number }, offset = 22) => { - const dx = end.x - start.x; - const dy = end.y - start.y; - const length = Math.sqrt(dx * dx + dy * dy); - return { - x1: start.x + (dx / length) * offset, - y1: start.y + (dy / length) * offset, - x2: end.x - (dx / length) * offset, - y2: end.y - (dy / length) * offset, - }; - }; + const pseudoSteps = steps.map(s => s.pseudoStep); return (
@@ -256,9 +356,8 @@ export const DijkstrasVisualization = () => {
-
- - {/* Edges */} +
+ {[ { from: 2, to: 1, weight: 1 }, { from: 2, to: 3, weight: 1 }, @@ -269,14 +368,26 @@ export const DijkstrasVisualization = () => { const { x1, y1, x2, y2 } = getEdgeLine(start, end, 18); return ( - - {edge.weight} + + + {edge.weight} + ); })} - - + + @@ -290,33 +401,64 @@ export const DijkstrasVisualization = () => { cx={pos.x} cy={pos.y} r="18" - className={`transition-all duration-300 ${isCurrent ? 'fill-primary stroke-primary' : isVisited ? 'fill-green-500 stroke-green-500' : 'fill-muted stroke-border'}`} + className={`transition-all duration-200 ${isCurrent + ? 'fill-primary stroke-primary' + : isVisited + ? 'fill-green-500 stroke-green-500' + : 'fill-muted stroke-border' + }`} strokeWidth="2" /> - {nodeNum} - {currentStep.distances[nodeNum] !== undefined ? `d=${currentStep.distances[nodeNum]}` : 'd=∞'} + + {nodeNum} + + + {currentStep.distances[nodeNum] !== undefined ? `d=${currentStep.distances[nodeNum]}` : 'd=∞'} + ); })} + +
+ Visited + Current + Unvisited +
-

{currentStep.message}

+

{currentStep.explanation}

`[${h[0]},${h[1]}]`).join(', ')}]`, + ...currentStep.variables }} />
-
- -
+
); diff --git a/src/components/visualizations/algorithms/DutchNationalFlagVisualization.tsx b/src/components/visualizations/algorithms/DutchNationalFlagVisualization.tsx index f9bc0f8..d11666d 100644 --- a/src/components/visualizations/algorithms/DutchNationalFlagVisualization.tsx +++ b/src/components/visualizations/algorithms/DutchNationalFlagVisualization.tsx @@ -1,30 +1,26 @@ import { useEffect, useRef, useState } from 'react'; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { array: number[]; low: number; mid: number; high: number; - message: string; - lineNumber: number; + explanation: string; + pseudoStep: string; + variables: Record; } -export const DutchNationalFlagVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const intervalRef = useRef(null); +// ─── Hardcoded code per language (no comments) ────────────────────────────── - const code = `function sortColors(nums: number[]): void { +const languages: VisualizationLanguageMap = { + typescript: `function dutchNationalFlag(nums: number[]): void { let low = 0; let mid = 0; let high = nums.length - 1; - while (mid <= high) { if (nums[mid] === 0) { [nums[low], nums[mid]] = [nums[mid], nums[low]]; @@ -37,186 +33,259 @@ export const DutchNationalFlagVisualization = () => { high--; } } -}`; +}`, + + python: `def dutch_national_flag(nums): + low = 0 + mid = 0 + high = len(nums) - 1 + while mid <= high: + if nums[mid] == 0: + nums[low], nums[mid] = nums[mid], nums[low] + low += 1 + mid += 1 + elif nums[mid] == 1: + mid += 1 + else: + nums[mid], nums[high] = nums[high], nums[mid] + high -= 1`, + + java: `public void sortColors(int[] nums) { + int low = 0, mid = 0, high = nums.length - 1; + while (mid <= high) { + if (nums[mid] == 0) { + int tmp = nums[low]; + nums[low] = nums[mid]; + nums[mid] = tmp; + low++; + mid++; + } + else if (nums[mid] == 1) { + mid++; + } + else { + int tmp = nums[mid]; + nums[mid] = nums[high]; + nums[high] = tmp; + high--; + } + } +}`, - const generateSteps = () => { - const initialArray = [2, 0, 2, 1, 1, 0, 2, 1, 0]; - const newSteps: Step[] = []; - const nums = [...initialArray]; - let low = 0, mid = 0, high = nums.length - 1; + cpp: `void dutchNationalFlag(vector& nums) { + int low = 0; + int mid = 0; + int high = nums.size() - 1; + while (mid <= high) { + if (nums[mid] == 0) { + swap(nums[low], nums[mid]); + low++; + mid++; + } + else if (nums[mid] == 1) { + mid++; + } + else { + swap(nums[mid], nums[high]); + high--; + } + } +}`, +}; - // Line 1: Function entry - newSteps.push({ - array: [...nums], - low: -1, - mid: -1, - high: -1, - message: 'Starting Dutch National Flag algorithm (Sort Colors).', - lineNumber: 1 - }); +// ─── Step generator ────────────────────────────────────────────────────────── + +function generateVisualizationData() { + const initialArray = [2, 0, 2, 1, 1, 0, 2, 1, 0]; + const steps: Step[] = []; + const nums = [...initialArray]; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - // Line 2: Initialize low - low = 0; - newSteps.push({ - array: [...nums], - low, - mid: -1, - high: -1, - message: 'Initialize low pointer at index 0.', - lineNumber: 2 - }); + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + let low = 0; + steps.push({ + array: [...nums], + low: 0, + mid: -1, + high: -1, + explanation: 'Initialize low pointer to 0.', + pseudoStep: 'SET low = 0', + variables: { low: 0, mid: '-', high: '-', 'nums[mid]': '-' } + }); + addLines(2, 2, 2, 2); + + let mid = 0; + steps.push({ + array: [...nums], + low, + mid: 0, + high: -1, + explanation: 'Initialize mid pointer to 0.', + pseudoStep: 'SET mid = 0', + variables: { low, mid: 0, high: '-', 'nums[mid]': nums[0] } + }); + addLines(3, 3, 2, 3); + + let high = nums.length - 1; + steps.push({ + array: [...nums], + low, + mid, + high, + explanation: `Initialize high pointer to the last index (${high}).`, + pseudoStep: `SET high = nums.length − 1 → ${high}`, + variables: { low, mid, high, 'nums[mid]': nums[mid] } + }); + addLines(4, 4, 2, 4); - // Line 3: Initialize mid - mid = 0; - newSteps.push({ + while (mid <= high) { + steps.push({ array: [...nums], low, mid, - high: -1, - message: 'Initialize mid pointer at index 0.', - lineNumber: 3 + high, + explanation: `Check loop condition: mid (${mid}) <= high (${high}) is true.`, + pseudoStep: `WHILE mid <= high → ${mid} <= ${high} → YES ✓`, + variables: { low, mid, high, 'nums[mid]': nums[mid] } }); + addLines(5, 5, 3, 5); - // Line 4: Initialize high - high = nums.length - 1; - newSteps.push({ + steps.push({ array: [...nums], low, mid, high, - message: `Initialize high pointer at index ${high}.`, - lineNumber: 4 + explanation: `Compare current value nums[mid] (${nums[mid]}) with 0.`, + pseudoStep: `IF nums[mid] == 0 → ${nums[mid]} == 0 → ${nums[mid] === 0 ? 'YES ✓' : 'NO ✗'}`, + variables: { low, mid, high, 'nums[mid]': nums[mid] } }); + addLines(6, 6, 4, 6); - while (mid <= high) { - // Line 6: While condition - newSteps.push({ + if (nums[mid] === 0) { + const prevLowVal = nums[low]; + const prevMidVal = nums[mid]; + [nums[low], nums[mid]] = [nums[mid], nums[low]]; + steps.push({ array: [...nums], low, mid, high, - message: `Check condition: mid (${mid}) <= high (${high}) is true.`, - lineNumber: 6 + explanation: `nums[mid] is 0. Swap nums[low] (${prevLowVal}) and nums[mid] (${prevMidVal}) to move it to the low section.`, + pseudoStep: `SWAP nums[low], nums[mid] → swap index ${low} & ${mid}`, + variables: { low, mid, high, 'nums[mid]': nums[mid] } }); + addLines(7, 7, 5, 7); - // Line 7: Check if nums[mid] === 0 - newSteps.push({ + low++; + steps.push({ array: [...nums], low, mid, high, - message: `Check if nums[mid] (${nums[mid]}) is 0.`, - lineNumber: 7 + explanation: `Increment low pointer to ${low}.`, + pseudoStep: 'low++ (move low boundary right)', + variables: { low, mid, high, 'nums[mid]': nums[mid] } }); + addLines(8, 8, 8, 8); - if (nums[mid] === 0) { - // Line 8: Swap low and mid - [nums[low], nums[mid]] = [nums[mid], nums[low]]; - newSteps.push({ - array: [...nums], - low, - mid, - high, - message: `nums[mid] is 0. Swap nums[low] and nums[mid].`, - lineNumber: 8 - }); - - // Line 9: low++ - low++; - newSteps.push({ - array: [...nums], - low, - mid, - high, - message: 'Move low pointer forward.', - lineNumber: 9 - }); - - // Line 10: mid++ - mid++; - newSteps.push({ - array: [...nums], - low, - mid, - high, - message: 'Move mid pointer forward.', - lineNumber: 10 - }); - - } else if (nums[mid] === 1) { - // Line 11: Check if nums[mid] === 1 - newSteps.push({ - array: [...nums], - low, - mid, - high, - message: `nums[mid] is 1. Already in the middle region.`, - lineNumber: 11 - }); + mid++; + steps.push({ + array: [...nums], + low, + mid, + high, + explanation: `Increment mid pointer to ${mid}.`, + pseudoStep: 'mid++ (move mid pointer right)', + variables: { low, mid, high, 'nums[mid]': mid <= high ? nums[mid] : 'done' } + }); + addLines(9, 9, 9, 9); + } else { + steps.push({ + array: [...nums], + low, + mid, + high, + explanation: `Compare current value nums[mid] (${nums[mid]}) with 1.`, + pseudoStep: `ELSE IF nums[mid] == 1 → ${nums[mid]} == 1 → ${nums[mid] === 1 ? 'YES ✓' : 'NO ✗'}`, + variables: { low, mid, high, 'nums[mid]': nums[mid] } + }); + addLines(10, 10, 11, 11); - // Line 12: mid++ + if (nums[mid] === 1) { mid++; - newSteps.push({ + steps.push({ array: [...nums], low, mid, high, - message: 'Move mid pointer forward.', - lineNumber: 12 + explanation: `nums[mid] is 1, which belongs in the middle. Just increment mid pointer to ${mid}.`, + pseudoStep: 'mid++ (value is 1, keep in middle)', + variables: { low, mid, high, 'nums[mid]': mid <= high ? nums[mid] : 'done' } }); - + addLines(11, 11, 12, 12); } else { - // Line 13: nums[mid] is 2 - newSteps.push({ - array: [...nums], - low, - mid, - high, - message: `nums[mid] is 2. Needs to go to the high region.`, - lineNumber: 11 // Mapping else if check or the logic below - }); - - // Line 14: Swap mid and high + const prevHighVal = nums[high]; + const prevMidVal = nums[mid]; [nums[mid], nums[high]] = [nums[high], nums[mid]]; - newSteps.push({ + steps.push({ array: [...nums], low, mid, high, - message: `Swap nums[mid] and nums[high].`, - lineNumber: 14 + explanation: `nums[mid] is 2. Swap nums[mid] (${prevMidVal}) and nums[high] (${prevHighVal}) to move it to the high section.`, + pseudoStep: `SWAP nums[mid], nums[high] → swap index ${mid} & ${high}`, + variables: { low, mid, high, 'nums[mid]': nums[mid] } }); + addLines(13, 13, 15, 15); - // Line 15: high-- high--; - newSteps.push({ + steps.push({ array: [...nums], low, mid, high, - message: 'Move high pointer backward. Mid stays current to check swapped value.', - lineNumber: 15 + explanation: `Decrement high pointer to ${high}. Mid stays current to inspect the swapped value.`, + pseudoStep: 'high-- (move high boundary left)', + variables: { low, mid, high, 'nums[mid]': nums[mid] } }); + addLines(14, 14, 18, 16); } } + } - // Line 18: Completion - newSteps.push({ - array: [...nums], - low, - mid, - high, - message: 'Algorithm complete. Array is now sorted.', - lineNumber: 18 - }); + steps.push({ + array: [...nums], + low, + mid, + high, + explanation: 'Algorithm finished. All colors sorted: Red (0), White (1), and Blue (2).', + pseudoStep: 'RETURN (sorted)', + variables: { low, mid, high, 'nums[mid]': 'done' } + }); + addLines(17, 14, 21, 19); + + return { steps, stepLineNumbers }; +} - setSteps(newSteps); - setCurrentStepIndex(0); - }; +// ─── Component ─────────────────────────────────────────────────────────────── - useEffect(() => { - generateSteps(); - }, []); +export const DutchNationalFlagVisualization = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { @@ -239,17 +308,17 @@ export const DutchNationalFlagVisualization = () => { const handlePlay = () => setIsPlaying(true); const handlePause = () => setIsPlaying(false); - const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(prev => prev + 1); - const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(prev => prev - 1); + const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(p => p + 1); + const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(p => p - 1); const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); - generateSteps(); }; if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); const getColorClass = (value: number) => { switch (value) { @@ -276,6 +345,7 @@ export const DutchNationalFlagVisualization = () => { />
+ {/* Left: visual state */}
@@ -286,13 +356,13 @@ export const DutchNationalFlagVisualization = () => { return (
-
- {isLow &&
LOW
} - {isMid &&
MID
} - {isHigh &&
HIGH
} +
+ {isLow &&
Low
} + {isMid &&
Mid
} + {isHigh &&
High
}
{
- 0 (Red) + 0 (Red)
- 1 (White) + 1 (White)
- 2 (Blue) + 2 (Blue)
-

{currentStep.message}

+

{currentStep.explanation}

-
- +
+

Dutch National Flag Strategy:

+
+

• Divide the array into three sections: low (0s), mid (1s), and high (2s)

+

• Pointers track boundaries: `low` (end of 0s), `mid` (current element), `high` (start of 2s)

+

• Swap element at `mid` to its correct region and update boundaries accordingly

+

• Time: O(n) · Space: O(1) in-place

+
+ + = 0 && currentStep.mid <= currentStep.high ? currentStep.array[currentStep.mid] : 'done' + }} + />
- + {/* Right: code / pseudocode panel */} +
- -
); }; diff --git a/src/components/visualizations/algorithms/EditDistanceVisualization.tsx b/src/components/visualizations/algorithms/EditDistanceVisualization.tsx index 49cd2d2..a9b0370 100644 --- a/src/components/visualizations/algorithms/EditDistanceVisualization.tsx +++ b/src/components/visualizations/algorithms/EditDistanceVisualization.tsx @@ -1,8 +1,9 @@ -import React, { useEffect, useRef, useState } from "react"; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; -import { StepControls } from "../shared/StepControls"; -import { VariablePanel } from "../shared/VariablePanel"; +import React, { useState } from 'react'; +import { Info } from 'lucide-react'; +import { SimpleStepControls } from '../shared/SimpleStepControls'; +import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { dp: number[][]; @@ -12,287 +13,383 @@ interface Step { word2: string; operation: string; message: string; - lineNumber: number; + explanation: string; + pseudoStep: string; } -export const EditDistanceVisualization: React.FC = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1000); - const intervalRef = useRef(null); - - const code = `function minDistance(word1: string, word2: string): number { - const m = word1.length; - const n = word2.length; - - const cache: number[][] = Array.from({ length: m + 1 }, () => - new Array(n + 1).fill(Infinity) - ); - - for (let j = 0; j <= n; j++) { - cache[m][j] = n - j; +const languages: VisualizationLanguageMap = { + typescript: `function minDistance(word1: string, word2: string): number { + const m = word1.length; + const n = word2.length; + const cache: number[][] = Array.from({ length: m + 1 }, () => + new Array(n + 1).fill(Infinity) + ); + for (let j = 0; j <= n; j++) { + cache[m][j] = n - j; + } + for (let i = 0; i <= m; i++) { + cache[i][n] = m - i; + } + for (let i = m - 1; i >= 0; i--) { + for (let j = n - 1; j >= 0; j--) { + if (word1[i] === word2[j]) { + cache[i][j] = cache[i + 1][j + 1]; + } else { + cache[i][j] = + 1 + + Math.min( + cache[i + 1][j], + cache[i][j + 1], + cache[i + 1][j + 1] + ); + } } + } + return cache[0][0]; +}`, - for (let i = 0; i <= m; i++) { - cache[i][n] = m - i; - } + python: `def minDistance(word1: str, word2: str) -> int: + m = len(word1) + n = len(word2) + cache = [[float('inf')] * (n + 1) for _ in range(m + 1)] + for j in range(n + 1): + cache[m][j] = n - j + for i in range(m + 1): + cache[i][n] = m - i + for i in range(m - 1, -1, -1): + for j in range(n - 1, -1, -1): + if word1[i] == word2[j]: + cache[i][j] = cache[i + 1][j + 1] + else: + cache[i][j] = 1 + min( + cache[i + 1][j], + cache[i][j + 1], + cache[i + 1][j + 1] + ) + return cache[0][0]`, - for (let i = m - 1; i >= 0; i--) { - for (let j = n - 1; j >= 0; j--) { - if (word1[i] === word2[j]) { - cache[i][j] = cache[i + 1][j + 1]; - } else { - cache[i][j] = - 1 + - Math.min( + java: `public static class Solution { + public int minDistance(String word1, String word2) { + int m = word1.length(); + int n = word2.length(); + int[][] cache = new int[m + 1][n + 1]; + for (int j = 0; j <= n; j++) { + cache[m][j] = n - j; + } + for (int i = 0; i <= m; i++) { + cache[i][n] = m - i; + } + for (int i = m - 1; i >= 0; i--) { + for (int j = n - 1; j >= 0; j--) { + if (word1.charAt(i) == word2.charAt(j)) { + cache[i][j] = cache[i + 1][j + 1]; + } else { + cache[i][j] = 1 + Math.min( cache[i + 1][j], - cache[i][j + 1], - cache[i + 1][j + 1] + Math.min(cache[i][j + 1], cache[i + 1][j + 1]) ); + } + } + } + return cache[0][0]; + } +}`, + + cpp: `class Solution { +public: + int minDistance(string word1, string word2) { + int m = word1.length(), n = word2.length(); + vector> dp(m + 1, vector(n + 1)); + for (int i = 0; i <= m; i++) dp[i][0] = i; + for (int j = 0; j <= n; j++) dp[0][j] = j; + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + if (word1[i - 1] == word2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = min({ + dp[i - 1][j], + dp[i][j - 1], + dp[i - 1][j - 1] + }) + 1; + } } } + return dp[m][n]; } +};` +}; - return cache[0][0]; -}`; +function generateVisualizationData() { + const word1 = 'horse'; + const word2 = 'ros'; + const m = word1.length; + const n = word2.length; - const generateSteps = () => { - const word1 = "horse"; - const word2 = "ros"; - const m = word1.length; - const n = word2.length; + const cache = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0)); + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - const cache = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0)); - const newSteps: Step[] = []; + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - // Base cases - for (let j = 0; j <= n; j++) { - cache[m][j] = n - j; - } - newSteps.push({ - dp: cache.map((row) => [...row]), - i: m, - j: -1, - word1, - word2, - operation: "init", - message: "Base case: if first word is empty, we must insert all characters of second word", - lineNumber: 10, - }); + // Base cases + for (let j = 0; j <= n; j++) { + cache[m][j] = n - j; + } + steps.push({ + dp: cache.map((row) => [...row]), + i: m, + j: -1, + word1, + word2, + operation: 'init', + message: 'Base case: if first word is empty, we must insert all remaining characters.', + explanation: 'Initialize the base case where word1 is empty (index m). The edit distance is the number of characters in word2 to insert.', + pseudoStep: 'SET cache[m][j] = n - j (Insertions)', + }); + addLines(7, 5, 6, 7); - for (let i = 0; i <= m; i++) { - cache[i][n] = m - i; - } - newSteps.push({ - dp: cache.map((row) => [...row]), - i: -1, - j: n, - word1, - word2, - operation: "init", - message: "Base case: if second word is empty, we must delete all characters of first word", - lineNumber: 14, - }); + for (let i = 0; i <= m; i++) { + cache[i][n] = m - i; + } + steps.push({ + dp: cache.map((row) => [...row]), + i: -1, + j: n, + word1, + word2, + operation: 'init', + message: 'Base case: if second word is empty, we must delete all remaining characters.', + explanation: 'Initialize the base case where word2 is empty (index n). The edit distance is the number of characters in word1 to delete.', + pseudoStep: 'SET cache[i][n] = m - i (Deletions)', + }); + addLines(10, 7, 9, 6); + + // Main DP + for (let i = m - 1; i >= 0; i--) { + for (let j = n - 1; j >= 0; j--) { + // Comparison Step + steps.push({ + dp: cache.map((row) => [...row]), + i, + j, + word1, + word2, + operation: 'compare', + message: `Comparing: word1[${i}] ('${word1[i]}') and word2[${j}] ('${word2[j]}')`, + explanation: `Compare character '${word1[i]}' at index ${i} with '${word2[j]}' at index ${j}.`, + pseudoStep: `IF word1[${i}] == word2[${j}] → '${word1[i]}' == '${word2[j]}'?`, + }); + addLines(15, 11, 14, 10); - // Main DP - for (let i = m - 1; i >= 0; i--) { - for (let j = n - 1; j >= 0; j--) { - newSteps.push({ + if (word1[i] === word2[j]) { + cache[i][j] = cache[i + 1][j + 1]; + steps.push({ dp: cache.map((row) => [...row]), i, j, word1, word2, - operation: "compare", - message: `Comparing '${word1[i]}' with '${word2[j]}'`, - lineNumber: 19, + operation: 'match', + message: `Match! '${word1[i]}' === '${word2[j]}'. dp[${i}][${j}] = dp[${i + 1}][${j + 1}] = ${cache[i][j]}`, + explanation: `Characters match! No operation needed. Inherit the edit distance from diagonal subproblem.`, + pseudoStep: `SET cache[${i}][${j}] = cache[${i + 1}][${j + 1}]`, }); + addLines(16, 12, 15, 11); + } else { + const deleteOp = cache[i + 1][j]; + const insertOp = cache[i][j + 1]; + const replaceOp = cache[i + 1][j + 1]; + cache[i][j] = 1 + Math.min(deleteOp, insertOp, replaceOp); - if (word1[i] === word2[j]) { - cache[i][j] = cache[i + 1][j + 1]; - newSteps.push({ - dp: cache.map((row) => [...row]), - i, - j, - word1, - word2, - operation: "match", - message: `Match! '${word1[i]}' === '${word2[j]}'. Edit distance is cache[${i + 1}][${j + 1}] = ${cache[i][j]}`, - lineNumber: 20, - }); - } else { - const deleteOp = cache[i + 1][j]; - const insertOp = cache[i][j + 1]; - const replaceOp = cache[i + 1][j + 1]; - cache[i][j] = 1 + Math.min(deleteOp, insertOp, replaceOp); + const minOp = Math.min(deleteOp, insertOp, replaceOp); + const opName = minOp === deleteOp ? 'delete' : minOp === insertOp ? 'insert' : 'replace'; - const minOp = Math.min(deleteOp, insertOp, replaceOp); - const opName = minOp === deleteOp ? "delete" : minOp === insertOp ? "insert" : "replace"; - - newSteps.push({ - dp: cache.map((row) => [...row]), - i, - j, - word1, - word2, - operation: opName, - message: `'${word1[i]}' !== '${word2[j]}'. Min(del=${deleteOp}, ins=${insertOp}, rep=${replaceOp}) + 1 = ${cache[i][j]}`, - lineNumber: 22, - }); - } + steps.push({ + dp: cache.map((row) => [...row]), + i, + j, + word1, + word2, + operation: opName, + message: `'${word1[i]}' !== '${word2[j]}'. Min(del=${deleteOp}, ins=${insertOp}, rep=${replaceOp}) + 1 = ${cache[i][j]}`, + explanation: `Mismatch! Take the minimum of Delete (${deleteOp}), Insert (${insertOp}), or Replace (${replaceOp}) and add 1 operation cost. Chosen: ${opName.toUpperCase()}.`, + pseudoStep: `SET cache[${i}][${j}] = 1 + MIN(Delete, Insert, Replace)`, + }); + addLines(18, 14, 17, 12); } } + } - newSteps.push({ - dp: cache.map((row) => [...row]), - i: 0, - j: 0, - word1, - word2, - operation: "complete", - message: `Minimum edit distance: ${cache[0][0]}`, - lineNumber: 33, - }); - - setSteps(newSteps); - }; - - useEffect(() => { - generateSteps(); - }, []); + // Complete Step + steps.push({ + dp: cache.map((row) => [...row]), + i: 0, + j: 0, + word1, + word2, + operation: 'complete', + message: `Minimum edit distance calculated: ${cache[0][0]}`, + explanation: `Edit distance calculation complete. The minimum distance to transform '${word1}' to '${word2}' is stored at cache[0][0], which is ${cache[0][0]}.`, + pseudoStep: 'RETURN cache[0][0]', + }); + addLines(28, 19, 23, 20); - useEffect(() => { - if (isPlaying && currentStepIndex < steps.length - 1) { - intervalRef.current = window.setInterval(() => { - setCurrentStepIndex((prev) => { - if (prev >= steps.length - 1) { - setIsPlaying(false); - return prev; - } - return prev + 1; - }); - }, speed); - } else { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - } + return { steps, stepLineNumbers }; +} - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - }; - }, [isPlaying, currentStepIndex, steps.length, speed]); +export const EditDistanceVisualization: React.FC = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); - const handlePlay = () => setIsPlaying(true); - const handlePause = () => setIsPlaying(false); - const handleStepForward = () => { - if (currentStepIndex < steps.length - 1) { - setCurrentStepIndex(currentStepIndex + 1); - } - }; - const handleStepBack = () => { - if (currentStepIndex > 0) { - setCurrentStepIndex(currentStepIndex - 1); - } - }; const handleReset = () => { setCurrentStepIndex(0); - setIsPlaying(false); }; - if (steps.length === 0) return null; - const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map((s) => s.pseudoStep); return ( -
- + -
-
-

Edit Distance DP Table

-
- - - - - {currentStep.word2.split("").map((char, idx) => ( - - ))} - - - - - {currentStep.dp.map((row, i) => ( - - - {row.map((val, j) => ( - +
+ {/* Left Column: Visual State */} +
+
+

+ Edit Distance DP Table +

+
+
- {char} -
{idx}
-
- ∅ -
{currentStep.word2.length}
-
- {i < currentStep.word1.length ? ( - <> - {currentStep.word1[i]} -
{i}
- - ) : "∅"} -
= 0 - ? "bg-green-500/5 text-green-600" - : "" - }`} - > - {val === Infinity ? "∞" : val} -
+ + + + {currentStep.word2.split('').map((char, idx) => ( + ))} + - ))} - -
+ {char} +
{idx}
+
+ ∅ +
{currentStep.word2.length}
+
+ + + {currentStep.dp.map((row, rIdx) => ( + + + {rIdx < currentStep.word1.length ? ( + <> + {currentStep.word1[rIdx]} +
{rIdx}
+ + ) : ( + '∅' + )} + + {row.map((val, cIdx) => { + const isCurrent = rIdx === currentStep.i && cIdx === currentStep.j; + const isInit = currentStep.operation === 'init'; + + let cellClass = 'text-muted-foreground/30'; + if (isCurrent) { + cellClass = 'bg-primary/20 ring-2 ring-primary ring-inset font-bold text-primary scale-105'; + } else if (val !== Infinity && val >= 0) { + cellClass = isInit ? 'bg-muted/40 text-foreground/75' : 'bg-green-500/5 text-green-600 dark:text-green-400'; + } + + return ( + + {val === Infinity ? '∞' : val} + + ); + })} + + ))} + + +
-
-

{currentStep.message}

+ {/* Commentary Panel */} +
+
+
+
+ + + + + + Algorithm Commentary + +
+
+ Step {currentStepIndex + 1} of {steps.length} +
+
+ +
+
+ +
+
+

+ Current Action +

+
+ {currentStep.explanation} +
+
+
+
+ = 0 && currentStep.i < currentStep.word1.length ? currentStep.word1[currentStep.i] : "-", - "word2[j]": currentStep.j >= 0 && currentStep.j < currentStep.word2.length ? currentStep.word2[currentStep.j] : "-", - result: currentStep.dp[0][0], + 'word1[i]': currentStep.i >= 0 && currentStep.i < currentStep.word1.length ? currentStep.word1[currentStep.i] : '-', + 'word2[j]': currentStep.j >= 0 && currentStep.j < currentStep.word2.length ? currentStep.word2[currentStep.j] : '-', + ans: currentStep.dp[0][0] }} />
- + {/* Right Column: Code Display */} +
+ +
); }; + +export default EditDistanceVisualization; diff --git a/src/components/visualizations/algorithms/FastSlowPointersVisualization.tsx b/src/components/visualizations/algorithms/FastSlowPointersVisualization.tsx index 90ec5e2..b8cc368 100644 --- a/src/components/visualizations/algorithms/FastSlowPointersVisualization.tsx +++ b/src/components/visualizations/algorithms/FastSlowPointersVisualization.tsx @@ -1,229 +1,277 @@ import React, { useEffect, useRef, useState } from 'react'; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; - +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; import { LayoutList, Hash } from 'lucide-react'; import { Button } from '@/components/ui/button'; -interface ListNode { - val: number; - next: ListNode | null; -} - interface Step { nodes: number[]; slow: number | null; fast: number | null; - fastIntermediate: number | null; // For showing the first step of fast pointer - message: string; - lineNumber: number; + fastIntermediate: number | null; + explanation: string; + pseudoStep: string; isMeeting: boolean; result: boolean | null; movingPointer: 'slow' | 'fast' | 'both' | 'none'; hasCycle: boolean; + variables: Record; } -export const FastSlowPointersVisualization: React.FC = () => { - const [testCase, setTestCase] = useState<'with-cycle' | 'no-cycle'>('with-cycle'); - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1000); - const intervalRef = useRef(null); +// ─── Hardcoded code per language (no comments) ────────────────────────────── - const code = `function hasCycle(head: ListNode | null): boolean { +const languages: VisualizationLanguageMap = { + typescript: `function hasCycle(head: ListNode | null): boolean { let slow: ListNode | null = head; let fast: ListNode | null = head; - while (fast !== null && fast.next !== null) { slow = slow!.next; fast = fast.next.next; - if (slow === fast) { return true; } } - return false; -}`; - - const generateSteps = (currentTestCase: 'with-cycle' | 'no-cycle') => { - const hasCycle = currentTestCase === 'with-cycle'; - const nodeValues = hasCycle ? [3, 2, 0, -4] : [1, 2, 3, 4, 5]; - const cycleStartIdx = 1; - const newSteps: Step[] = []; - - const getNext = (curr: number) => { - if (curr < 0) return -1; // null - if (curr < nodeValues.length - 1) return curr + 1; - return hasCycle ? cycleStartIdx : -1; - }; +}`, + + python: `def hasCycle(head: ListNode) -> bool: + slow, fast = head, head + while fast and fast.next: + slow = slow.next + fast = fast.next.next + if slow == fast: + return True + return False`, + + java: `public static class Solution { + public boolean hasCycle(ListNode head) { + if (head == null) { + return false; + } + ListNode slow = head; + ListNode fast = head; + while (fast != null && fast.next != null) { + slow = slow.next; + fast = fast.next.next; + if (slow == fast) { + return true; + } + } + return false; + } +}`, + + cpp: `class Solution { + public: + bool hasCycle(ListNode* head) { + ListNode* slow = head; + ListNode* fast = head; + while (fast && fast->next) { + slow = slow->next; + fast = fast->next->next; + if (slow == fast) { + return true; + } + } + return false; + } +};`, +}; + +// ─── Step generator ────────────────────────────────────────────────────────── + +function generateVisualizationData(currentTestCase: 'with-cycle' | 'no-cycle') { + const hasCycle = currentTestCase === 'with-cycle'; + const nodeValues = hasCycle ? [3, 2, 0, -4] : [1, 2, 3, 4, 5]; + const cycleStartIdx = 1; + const steps: Step[] = []; + + const getNext = (curr: number) => { + if (curr < 0) return -1; + if (curr < nodeValues.length - 1) return curr + 1; + return hasCycle ? cycleStartIdx : -1; + }; - const addStep = (params: Partial) => { - newSteps.push({ - nodes: nodeValues, - slow: null, - fast: null, - fastIntermediate: null, - message: '', - lineNumber: 1, // Default to function signature - isMeeting: false, - result: null, - movingPointer: 'none', - hasCycle, - ...params - }); - }; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - let slow: number | null = 0; - let fast: number | null = 0; + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - // Line 2: let slow = head - addStep({ - slow: 0, - message: 'Initialize slow pointer at head (index 0)', - lineNumber: 2, - movingPointer: 'slow' + const addStep = (params: Partial, ts_l: number, py_l: number, java_l: number, cpp_l: number) => { + steps.push({ + nodes: nodeValues, + slow: null, + fast: null, + fastIntermediate: null, + explanation: '', + pseudoStep: '', + isMeeting: false, + result: null, + movingPointer: 'none', + hasCycle, + variables: {}, + ...params }); + addLines(ts_l, py_l, java_l, cpp_l); + }; - // Line 3: let fast = head + let slow: number | null = 0; + let fast: number | null = 0; + + addStep({ + slow: 0, + explanation: 'Initialize slow pointer to the head of the list (index 0).', + pseudoStep: 'SET slow = head', + movingPointer: 'slow', + }, 2, 2, 6, 4); + + addStep({ + slow: 0, + fast: 0, + explanation: 'Initialize fast pointer to the head of the list (index 0).', + pseudoStep: 'SET fast = head', + movingPointer: 'fast', + }, 3, 2, 7, 5); + + while (true) { addStep({ - slow: 0, - fast: 0, - message: 'Initialize fast pointer at head (index 0)', - lineNumber: 3, - movingPointer: 'fast' - }); - - while (true) { - // Line 5: while condition - addStep({ - slow, - fast, - message: 'Checking loop condition: fast and fast.next are non-null', - lineNumber: 5 - }); - - if (fast === -1 || getNext(fast) === -1) { - // Loop exit condition met - addStep({ - slow, - fast, - message: 'Loop ends because fast or fast.next reached null (end of list).', - lineNumber: 5 - }); - break; - } + slow, + fast, + explanation: 'Check if fast pointer and its next node are not null.', + pseudoStep: `WHILE fast != null AND fast.next != null → fast = index ${fast === -1 ? 'null' : fast}`, + movingPointer: 'none' + }, 4, 3, 8, 6); - // Line 6: slow = slow.next - slow = getNext(slow!); + if (fast === -1 || getNext(fast) === -1) { addStep({ slow, fast, - message: `Slow pointer moves 1 step to node ${nodeValues[slow]}`, - lineNumber: 6, - movingPointer: 'slow' - }); + explanation: 'Loop ends because fast pointer or its next pointer reached null.', + pseudoStep: 'Loop ends (fast or fast.next is null)', + movingPointer: 'none' + }, 4, 3, 8, 6); + break; + } - // Line 7: fast = fast.next.next - const fastStep1 = getNext(fast!); - fast = fastStep1 === -1 ? -1 : getNext(fastStep1); + slow = getNext(slow!); + addStep({ + slow, + fast, + explanation: `Advance slow pointer by 1 step to index ${slow} (value ${nodeValues[slow]}).`, + pseudoStep: `SET slow = slow.next → index ${slow}`, + movingPointer: 'slow' + }, 5, 4, 9, 7); - addStep({ - slow, - fast, - message: fast === -1 - ? 'Fast pointer moves 2 steps and reaches the end of the list' - : `Fast pointer moves 2 steps to node ${nodeValues[fast]}`, - lineNumber: 7, - movingPointer: 'fast' - }); - - // Line 9: if (slow === fast) - addStep({ - slow, - fast, - message: fast === -1 - ? `Checking if slow (${nodeValues[slow]}) equals fast (null)` - : `Checking if slow (${nodeValues[slow]}) equals fast (${nodeValues[fast]})`, - lineNumber: 9 - }); - - if (slow === fast) { - // Line 10: return true - addStep({ - slow, - fast, - isMeeting: true, - message: 'Slow and fast pointers met! Cycle detected.', - lineNumber: 10, - result: true - }); - break; - } + const fastStep1 = getNext(fast!); + fast = fastStep1 === -1 ? -1 : getNext(fastStep1); + addStep({ + slow, + fast, + explanation: fast === -1 + ? 'Advance fast pointer by 2 steps to null.' + : `Advance fast pointer by 2 steps to index ${fast} (value ${nodeValues[fast]}).`, + pseudoStep: `SET fast = fast.next.next → index ${fast === -1 ? 'null' : fast}`, + movingPointer: 'fast' + }, 6, 5, 10, 8); - if (newSteps.length > 100) break; // Safety against infinite loops - } + addStep({ + slow, + fast, + explanation: fast === -1 + ? `Compare slow pointer (${nodeValues[slow]}) and fast pointer (null).` + : `Compare slow pointer (${nodeValues[slow]}) and fast pointer (${nodeValues[fast]}).`, + pseudoStep: `IF slow == fast → ${slow === fast ? 'YES ✓' : 'NO ✗'}`, + movingPointer: 'none' + }, 7, 6, 11, 9); - if (fast === -1 || getNext(fast) === -1) { - // Line 14: return false + if (slow === fast) { addStep({ slow, fast, - message: 'No cycle detected. Reached the end of the list.', - lineNumber: 14, - result: false - }); + isMeeting: true, + explanation: 'Slow and fast pointers met at the same node! A cycle is detected.', + pseudoStep: 'RETURN True (cycle found)', + result: true, + movingPointer: 'none' + }, 8, 7, 12, 10); + break; } + } - setSteps(newSteps); - setCurrentStepIndex(0); - setIsPlaying(false); - }; + if (fast === -1 || getNext(fast) === -1) { + addStep({ + slow, + fast, + explanation: 'Fast pointer reached the end of the list. No cycle detected.', + pseudoStep: 'RETURN False (no cycle)', + result: false, + movingPointer: 'none' + }, 11, 8, 15, 13); + } + + steps.forEach(s => { + s.variables = { + slow: s.slow !== null && s.slow >= 0 ? `Node ${s.nodes[s.slow]}` : 'null', + fast: s.fast !== null && s.fast >= 0 ? `Node ${s.nodes[s.fast]}` : 'null', + 'slow === fast': s.slow === s.fast && s.slow !== null ? 'TRUE' : 'FALSE', + result: s.result === null ? 'in progress...' : s.result ? 'TRUE (Cycle Found)' : 'FALSE (No Cycle)' + }; + }); + + return { steps, stepLineNumbers }; +} + +// ─── Component ─────────────────────────────────────────────────────────────── + +export const FastSlowPointersVisualization: React.FC = () => { + const [testCase, setTestCase] = useState<'with-cycle' | 'no-cycle'>('with-cycle'); + const [{ steps, stepLineNumbers }, setVizData] = useState(() => generateVisualizationData(testCase)); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); useEffect(() => { - generateSteps(testCase); + const data = generateVisualizationData(testCase); + setVizData(data); + setCurrentStepIndex(0); + setIsPlaying(false); }, [testCase]); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { - intervalRef.current = window.setInterval(() => { - setCurrentStepIndex((prev) => { + intervalRef.current = setInterval(() => { + setCurrentStepIndex(prev => { if (prev >= steps.length - 1) { setIsPlaying(false); return prev; } return prev + 1; }); - }, speed); + }, 1000 / speed); } else { - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } + if (intervalRef.current) clearInterval(intervalRef.current); } - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } + if (intervalRef.current) clearInterval(intervalRef.current); }; }, [isPlaying, currentStepIndex, steps.length, speed]); const handlePlay = () => setIsPlaying(true); const handlePause = () => setIsPlaying(false); - const handleStepForward = () => { - if (currentStepIndex < steps.length - 1) { - setCurrentStepIndex(currentStepIndex + 1); - } - }; - const handleStepBack = () => { - if (currentStepIndex > 0) { - setCurrentStepIndex(currentStepIndex - 1); - } - }; + const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(p => p + 1); + const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(p => p - 1); const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); @@ -232,23 +280,22 @@ export const FastSlowPointersVisualization: React.FC = () => { if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return (
-
- -
+
@@ -272,29 +319,21 @@ export const FastSlowPointersVisualization: React.FC = () => {
+ {/* Left: visual state */}
-
- {/* Background Decoration */} -
- - - -
- - - +
{currentStep.nodes.map((val, idx) => (
{val} @@ -303,17 +342,16 @@ export const FastSlowPointersVisualization: React.FC = () => { {/* Pointer Labels */}
{idx === currentStep.slow && currentStep.slow !== null && ( -
- SLOW -
+ + Slow + )} {(idx === currentStep.fast || idx === currentStep.fastIntermediate) && (currentStep.fast !== null || currentStep.fastIntermediate !== null) && ( -
- FAST -
+ + Fast + )}
-
{idx < currentStep.nodes.length - 1 ? ( @@ -332,27 +370,33 @@ export const FastSlowPointersVisualization: React.FC = () => {
))}
+
-
-

- {currentStep.message} -

+
+

{currentStep.explanation}

+
+ +
+

Fast & Slow Pointers Strategy:

+
+

• Move two pointers at different speeds (slow by 1 node, fast by 2 nodes)

+

• If a cycle exists, the fast pointer will eventually meet the slow pointer

+

• If no cycle, fast pointer will reach null (end of list)

+

• Time: O(n) · Space: O(1)

- +
-
- -
+ {/* Right: code / pseudocode panel */} +
); diff --git a/src/components/visualizations/algorithms/FenwickTreeVisualization.tsx b/src/components/visualizations/algorithms/FenwickTreeVisualization.tsx index 844fef6..f33e7ff 100644 --- a/src/components/visualizations/algorithms/FenwickTreeVisualization.tsx +++ b/src/components/visualizations/algorithms/FenwickTreeVisualization.tsx @@ -1,8 +1,10 @@ -import React, { useEffect, useRef, useState } from "react"; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; -import { StepControls } from "../shared/StepControls"; +import React, { useState, useMemo } from "react"; +import { SimpleStepControls } from "../shared/SimpleStepControls"; import { VariablePanel } from "../shared/VariablePanel"; +import { VisualizationCodePanel } from "../shared/VisualizationCodePanel"; +import { VisualizationLayout } from "../shared/VisualizationLayout"; +import { Card } from "@/components/ui/card"; +import type { StepLineNumberMap, VisualizationLanguageMap } from "@/types/visualization"; interface Step { array: number[]; @@ -12,22 +14,15 @@ interface Step { value: number | string; sum: number | string; message: string; - lineNumber: number; + pseudoStep: string; highlightedTreeIndex: number | null; highlightedArrayIndex: number | null; } -export const FenwickTreeVisualization: React.FC = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1000); - const intervalRef = useRef(null); - - const code = `function solution(nums: number[]): number { +const languages: VisualizationLanguageMap = { + typescript: `function solution(nums: number[]): number { const n = nums.length; const tree = new Array(n + 1).fill(0); - function update(index: number, value: number) { index++; while (index <= n) { @@ -35,7 +30,6 @@ export const FenwickTreeVisualization: React.FC = () => { index += index & -index; } } - function query(index: number): number { index++; let sum = 0; @@ -45,47 +39,109 @@ export const FenwickTreeVisualization: React.FC = () => { } return sum; } - for (let i = 0; i < n; i++) { update(i, nums[i]); } - return query(n - 1); -}`; +}`, + python: `def solution(nums): + n = len(nums) + tree = [0] * (n + 1) + def update(index, value): + index += 1 + while index <= n: + tree[index] += value + index += index & -index + def query(index): + index += 1 + s = 0 + while index > 0: + s += tree[index] + index -= index & -index + return s + for i in range(n): + update(i, nums[i]) + return query(n - 1)`, + java: `public static class Solution { + public int solution(int[] nums) { + int n = nums.length; + int[] tree = new int[n + 1]; + for (int i = 0; i < n; i++) { + update(tree, n, i, nums[i]); + } + return query(tree, n, n - 1); + } + void update(int[] tree, int n, int index, int value) { + index++; + while (index <= n) { + tree[index] += value; + index += index & -index; + } + } + int query(int[] tree, int n, int index) { + index++; + int sum = 0; + while (index > 0) { + sum += tree[index]; + index -= index & -index; + } + return sum; + } +}`, + cpp: `class Solution { +public: +int update(vector& tree, int n, int index, int value) { + index++; + while (index <= n) { + tree[index] += value; + index += index & -index; + } + return 0; +} +int query(vector& tree, int index) { + index++; + int sum = 0; + while (index > 0) { + sum += tree[index]; + index -= index & -index; + } + return sum; +} +int solution(vector& nums) { + int n = nums.size(); + vector tree(n + 1, 0); + for (let i = 0; i < n; i++) { + update(tree, n, i, nums[i]); + } + return query(tree, n - 1); +} +};` +}; - const generateSteps = () => { +export const FenwickTreeVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + + const { steps, stepLineNumbers } = useMemo(() => { const nums = [3, 2, -1, 6, 5]; const newSteps: Step[] = []; const n = nums.length; const tree: number[] = new Array(n + 1).fill(0); - // Initial state - newSteps.push({ - array: [...nums], - tree: [...tree], - operation: "init", - index: "none", - value: "none", - sum: "none", - message: "Initialize the Fenwick Tree process.", - lineNumber: 1, - highlightedTreeIndex: null, - highlightedArrayIndex: null, - }); + const lines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - newSteps.push({ - array: [...nums], - tree: [...tree], - operation: "init", - index: "none", - value: "none", - sum: "none", - message: `Set n = nums.length (${n}).`, - lineNumber: 2, - highlightedTreeIndex: null, - highlightedArrayIndex: null, - }); + const addLines = (ts: number, py: number, java: number, cpp: number) => { + lines.typescript!.push(ts); + lines.python!.push(py); + lines.java!.push(java); + lines.cpp!.push(cpp); + }; + // Initial state newSteps.push({ array: [...nums], tree: [...tree], @@ -93,11 +149,12 @@ export const FenwickTreeVisualization: React.FC = () => { index: "none", value: "none", sum: "none", - message: "Initialize tree array with size n+1, filled with 0s.", - lineNumber: 3, + message: "Initialize empty Fenwick Tree of size n+1 with all 0s.", + pseudoStep: "SET tree = [0, 0, 0, 0, 0, 0] (size = n + 1)", highlightedTreeIndex: null, highlightedArrayIndex: null, }); + addLines(3, 3, 4, 22); // Build phase for (let i = 0; i < n; i++) { @@ -109,10 +166,11 @@ export const FenwickTreeVisualization: React.FC = () => { value: nums[i], sum: "none", message: `Loop iteration i = ${i}. Preparing to update tree with nums[${i}] = ${nums[i]}.`, - lineNumber: 23, + pseudoStep: `FOR i = 0 TO n-1 (i = ${i})`, highlightedTreeIndex: null, highlightedArrayIndex: i, }); + addLines(20, 16, 5, 23); newSteps.push({ array: [...nums], @@ -121,11 +179,12 @@ export const FenwickTreeVisualization: React.FC = () => { index: i, value: nums[i], sum: "none", - message: `Call update(${i}, ${nums[i]}).`, - lineNumber: 24, + message: `Call update(index = ${i}, value = ${nums[i]}).`, + pseudoStep: `CALL update(index = ${i}, value = ${nums[i]})`, highlightedTreeIndex: null, highlightedArrayIndex: i, }); + addLines(21, 17, 6, 24); // Update function let idx = i; @@ -138,11 +197,12 @@ export const FenwickTreeVisualization: React.FC = () => { index: idx, value: val, sum: "none", - message: `Inside update(index=${idx}, value=${val}).`, - lineNumber: 5, + message: `Inside update(index = ${idx}, value = ${val}).`, + pseudoStep: `FUNCTION update(index = ${idx}, value = ${val})`, highlightedTreeIndex: null, highlightedArrayIndex: idx, }); + addLines(4, 4, 10, 3); idx++; newSteps.push({ @@ -152,11 +212,12 @@ export const FenwickTreeVisualization: React.FC = () => { index: idx, value: val, sum: "none", - message: `Increment index by 1 for 1-based indexing. index = ${idx}.`, - lineNumber: 6, + message: `Convert to 1-based indexing: index = index + 1 = ${idx}.`, + pseudoStep: `SET index = index + 1 (${idx})`, highlightedTreeIndex: idx, highlightedArrayIndex: null, }); + addLines(5, 5, 11, 4); while (idx <= n) { newSteps.push({ @@ -166,11 +227,12 @@ export const FenwickTreeVisualization: React.FC = () => { index: idx, value: val, sum: "none", - message: `Check if index (${idx}) <= n (${n}).`, - lineNumber: 7, + message: `Check loop condition: index (${idx}) <= n (${n}).`, + pseudoStep: `WHILE index <= n (${idx} <= ${n}) → YES ✓`, highlightedTreeIndex: idx, highlightedArrayIndex: null, }); + addLines(6, 6, 12, 5); tree[idx] += val; newSteps.push({ @@ -180,11 +242,12 @@ export const FenwickTreeVisualization: React.FC = () => { index: idx, value: val, sum: "none", - message: `Add value ${val} to tree[${idx}]. tree[${idx}] is now ${tree[idx]}.`, - lineNumber: 8, + message: `Add value ${val} to tree[${idx}]. tree[${idx}] becomes ${tree[idx]}.`, + pseudoStep: `SET tree[${idx}] = tree[${idx}] + value (${tree[idx]})`, highlightedTreeIndex: idx, highlightedArrayIndex: null, }); + addLines(7, 7, 13, 6); const oldIdx = idx; idx += idx & -idx; @@ -195,11 +258,12 @@ export const FenwickTreeVisualization: React.FC = () => { index: idx, value: val, sum: "none", - message: `Update index: ${oldIdx} + (${oldIdx} & ${-oldIdx}) = ${idx}.`, - lineNumber: 9, + message: `Add LSB of index: ${oldIdx} + (${oldIdx} & -${oldIdx}) = ${idx}.`, + pseudoStep: `SET index = index + (index & -index) (${idx})`, highlightedTreeIndex: idx <= n ? idx : null, highlightedArrayIndex: null, }); + addLines(8, 8, 14, 7); } newSteps.push({ @@ -209,11 +273,12 @@ export const FenwickTreeVisualization: React.FC = () => { index: idx, value: val, sum: "none", - message: `index (${idx}) > n (${n}), loop terminates.`, - lineNumber: 7, + message: `Loop condition check: index (${idx}) <= n (${n}) → false. Loop terminates.`, + pseudoStep: `WHILE index <= n (${idx} <= ${n}) → NO ✗`, highlightedTreeIndex: null, highlightedArrayIndex: null, }); + addLines(6, 6, 12, 5); } // Query phase @@ -225,11 +290,12 @@ export const FenwickTreeVisualization: React.FC = () => { index: queryIdx, value: "none", sum: "none", - message: `Finished building tree. Now query prefix sum up to index ${queryIdx}.`, - lineNumber: 27, + message: `Fenwick Tree built. Now perform query to get sum up to index ${queryIdx}.`, + pseudoStep: `CALL query(index = ${queryIdx})`, highlightedTreeIndex: null, highlightedArrayIndex: queryIdx, }); + addLines(23, 18, 8, 26); let qIdx = queryIdx; newSteps.push({ @@ -239,11 +305,12 @@ export const FenwickTreeVisualization: React.FC = () => { index: qIdx, value: "none", sum: "none", - message: `Inside query(index=${qIdx}).`, - lineNumber: 13, + message: `Inside query(index = ${qIdx}).`, + pseudoStep: `FUNCTION query(index = ${qIdx})`, highlightedTreeIndex: null, highlightedArrayIndex: null, }); + addLines(11, 9, 17, 11); qIdx++; newSteps.push({ @@ -253,11 +320,12 @@ export const FenwickTreeVisualization: React.FC = () => { index: qIdx, value: "none", sum: "none", - message: `Increment index by 1. index = ${qIdx}.`, - lineNumber: 14, + message: `Convert to 1-based indexing: index = index + 1 = ${qIdx}.`, + pseudoStep: `SET index = index + 1 (${qIdx})`, highlightedTreeIndex: qIdx, highlightedArrayIndex: null, }); + addLines(12, 10, 18, 12); let currentSum = 0; newSteps.push({ @@ -267,11 +335,12 @@ export const FenwickTreeVisualization: React.FC = () => { index: qIdx, value: "none", sum: currentSum, - message: `Initialize sum = 0.`, - lineNumber: 15, + message: "Initialize prefix sum accumulator variable sum = 0.", + pseudoStep: "SET sum = 0", highlightedTreeIndex: qIdx, highlightedArrayIndex: null, }); + addLines(13, 11, 19, 13); while (qIdx > 0) { newSteps.push({ @@ -281,11 +350,12 @@ export const FenwickTreeVisualization: React.FC = () => { index: qIdx, value: "none", sum: currentSum, - message: `Check if index (${qIdx}) > 0.`, - lineNumber: 16, + message: `Check query condition: index (${qIdx}) > 0.`, + pseudoStep: `WHILE index > 0 (${qIdx} > 0) → YES ✓`, highlightedTreeIndex: qIdx, highlightedArrayIndex: null, }); + addLines(14, 12, 20, 14); currentSum += tree[qIdx]; newSteps.push({ @@ -295,11 +365,12 @@ export const FenwickTreeVisualization: React.FC = () => { index: qIdx, value: "none", sum: currentSum, - message: `Add tree[${qIdx}] (${tree[qIdx]}) to sum. sum = ${currentSum}.`, - lineNumber: 17, + message: `Add tree[${qIdx}] (${tree[qIdx]}) to sum. sum is now ${currentSum}.`, + pseudoStep: `SET sum = sum + tree[${qIdx}] (${currentSum})`, highlightedTreeIndex: qIdx, highlightedArrayIndex: null, }); + addLines(15, 13, 21, 15); const oldQIdx = qIdx; qIdx -= qIdx & -qIdx; @@ -310,11 +381,12 @@ export const FenwickTreeVisualization: React.FC = () => { index: qIdx, value: "none", sum: currentSum, - message: `Update index: ${oldQIdx} - (${oldQIdx} & ${-oldQIdx}) = ${qIdx}.`, - lineNumber: 18, + message: `Subtract LSB of index: ${oldQIdx} - (${oldQIdx} & -${oldQIdx}) = ${qIdx}.`, + pseudoStep: `SET index = index - (index & -index) (${qIdx})`, highlightedTreeIndex: qIdx > 0 ? qIdx : null, highlightedArrayIndex: null, }); + addLines(16, 14, 22, 16); } newSteps.push({ @@ -324,11 +396,12 @@ export const FenwickTreeVisualization: React.FC = () => { index: qIdx, value: "none", sum: currentSum, - message: `index is 0, loop terminates.`, - lineNumber: 16, + message: `Loop condition check: index (${qIdx}) > 0 → false. Loop terminates.`, + pseudoStep: `WHILE index > 0 (${qIdx} > 0) → NO ✗`, highlightedTreeIndex: null, highlightedArrayIndex: null, }); + addLines(14, 12, 20, 14); newSteps.push({ array: [...nums], @@ -337,11 +410,12 @@ export const FenwickTreeVisualization: React.FC = () => { index: qIdx, value: "none", sum: currentSum, - message: `Return final sum: ${currentSum}.`, - lineNumber: 20, + message: `Return prefix sum result: ${currentSum}.`, + pseudoStep: `RETURN sum (${currentSum})`, highlightedTreeIndex: null, highlightedArrayIndex: null, }); + addLines(18, 15, 24, 18); newSteps.push({ array: [...nums], @@ -350,146 +424,109 @@ export const FenwickTreeVisualization: React.FC = () => { index: queryIdx, value: "none", sum: currentSum, - message: `Prefix sum result for index ${queryIdx} is ${currentSum}.`, - lineNumber: 27, + message: `Prefix sum query completed for index ${queryIdx}. Result is ${currentSum}.`, + pseudoStep: `RETURN sum (${currentSum})`, highlightedTreeIndex: null, highlightedArrayIndex: null, }); + addLines(23, 18, 8, 26); - setSteps(newSteps); - }; - - useEffect(() => { - generateSteps(); + return { steps: newSteps, stepLineNumbers: lines }; }, []); - useEffect(() => { - if (isPlaying && currentStepIndex < steps.length - 1) { - intervalRef.current = window.setInterval(() => { - setCurrentStepIndex((prev) => { - if (prev >= steps.length - 1) { - setIsPlaying(false); - return prev; - } - return prev + 1; - }); - }, speed); - } else { - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } - } + const step = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - }; - }, [isPlaying, currentStepIndex, steps.length, speed]); - - const handlePlay = () => setIsPlaying(true); - const handlePause = () => setIsPlaying(false); - const handleStepForward = () => { - if (currentStepIndex < steps.length - 1) - setCurrentStepIndex(currentStepIndex + 1); - }; - const handleStepBack = () => { - if (currentStepIndex > 0) setCurrentStepIndex(currentStepIndex - 1); - }; - const handleReset = () => { - setCurrentStepIndex(0); - setIsPlaying(false); - }; - - if (steps.length === 0) return null; - - const currentStep = steps[currentStepIndex]; + if (!step) return null; return ( -
- - -
-
-
-

Original Array

-
- {currentStep.array.map((val, idx) => ( -
-
- [{idx}] -
-
- {val} -
-
- ))} -
-
- -
-

Fenwick Tree (1-indexed)

-
- {currentStep.tree.slice(1).map((val, idx) => { - const treeIdx = idx + 1; - return ( -
-
- [{treeIdx}] -
-
- {val} + +
+ +
+

Original Array

+
+ {step.array.map((val, idx) => ( +
+
+ [{idx}] +
+
+ {val} +
-
- ); - })} -
+ ))} +
+
+ +
+

Fenwick Tree (1-indexed)

+
+ {step.tree.slice(1).map((val, idx) => { + const treeIdx = idx + 1; + return ( +
+
+ [{treeIdx}] +
+
+ {val} +
+
+ ); + })} +
+
+
-
-

{currentStep.message}

-
+
+ +

Step Explanation

+

{step.message}

+
-
- - setCurrentStepIndex(0)} + /> + } + controls={ + -
-
+ } + /> ); }; diff --git a/src/components/visualizations/algorithms/FindMinimumInRotatedSortedArrayVisualization.tsx b/src/components/visualizations/algorithms/FindMinimumInRotatedSortedArrayVisualization.tsx index 0e77260..a7d8cf8 100644 --- a/src/components/visualizations/algorithms/FindMinimumInRotatedSortedArrayVisualization.tsx +++ b/src/components/visualizations/algorithms/FindMinimumInRotatedSortedArrayVisualization.tsx @@ -1,235 +1,295 @@ -import { useState, useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { Card } from '@/components/ui/card'; import { VisualizationLayout } from '../shared/VisualizationLayout'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; -import { motion } from 'framer-motion'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { array: number[]; highlights: number[]; variables: Record; explanation: string; - highlightedLines: number[]; - lineExecution: string; + pseudoStep: string; } +const languages: VisualizationLanguageMap = { + typescript: `function findMin(nums: number[]): number { + let left = 0, right = nums.length - 1; + if (nums.length === 0) return 0; + while (left < right) { + const mid = Math.floor((left + right) / 2); + if (nums[mid] > nums[right]) { + left = mid + 1; + } else { + right = mid; + } + } + return nums[left]; +}`, + python: `def findMin(nums: List[int]) -> int: + left, right = 0, len(nums) - 1 + if len(nums) == 0: + return 0 + while left < right: + mid = (left + right) // 2 + if nums[mid] > nums[right]: + left = mid + 1 + else: + right = mid + return nums[left]`, + java: `public static class Solution { + public int findMin(int[] nums) { + int left = 0, right = nums.length - 1; + if (nums.length == 0) return 0; + while (left < right) { + int mid = left + (right - left) / 2; + if (nums[mid] > nums[right]) { + left = mid + 1; + } else { + right = mid; + } + } + return nums[left]; + } +}`, + cpp: `class Solution { +public: + int findMin(vector& nums) { + int left = 0, right = nums.size() - 1; + if (nums.size() == 0) return 0; + while (left < right) { + int mid = left + (right - left) / 2; + if (nums[mid] > nums[right]) { + left = mid + 1; + } else { + right = mid; + } + } + return nums[left]; + } +};` +}; + export const FindMinimumInRotatedSortedArrayVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); const [steps, setSteps] = useState([]); - - const code = `function findMin(nums: number[]): number { - let left = 0, right = nums.length - 1; - - while (left < right) { - const mid = Math.floor((left + right) / 2); - - if (nums[mid] > nums[right]) { - left = mid + 1; - } else { - right = mid; - } - } - - return nums[left]; -}`; + const [stepLineNumbers, setStepLineNumbers] = useState({ + typescript: [], + python: [], + java: [], + cpp: [] + }); + const [currentStepIndex, setCurrentStepIndex] = useState(0); useEffect(() => { - const generateSteps = () => { - const nums = [4, 5, 6, 7, 0, 1, 2]; - const newSteps: Step[] = []; - let left = 0; - let right = nums.length - 1; - let mid = 0; - - // Step 1: Initialization(Start) - newSteps.push({ - array: [...nums], - highlights: [], - variables: { left: '?', right: '?', mid: '?' }, - explanation: "Starting with rotated sorted array [4,5,6,7,0,1,2]. Goal: Find minimum in O(log n) time.", - highlightedLines: [1], - lineExecution: "function findMin(nums: number[]): number {" - }); + const nums = [4, 5, 6, 7, 0, 1, 2]; + const n = nums.length; + const generatedSteps: Step[] = []; + const stepLines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLines.typescript!.push(ts); + stepLines.python!.push(py); + stepLines.java!.push(java); + stepLines.cpp!.push(cpp); + }; + + // Function Entry + generatedSteps.push({ + array: [...nums], + highlights: [], + variables: { left: '-', right: '-', mid: '-' }, + explanation: "Initialize binary search to find the minimum element in the rotated sorted array.", + pseudoStep: "FUNCTION findMin(nums)" + }); + addLines(1, 1, 2, 3); + + // Pointer Init + let left = 0; + let right = n - 1; + generatedSteps.push({ + array: [...nums], + highlights: [left, right], + variables: { left, right, mid: '-' }, + explanation: `Initialize two pointers: left = ${left}, right = ${right}. Search space: [${left}...${right}].`, + pseudoStep: `SET left = 0, right = nums.length - 1` + }); + addLines(2, 2, 3, 4); + + // Empty array check + generatedSteps.push({ + array: [...nums], + highlights: [], + variables: { left, right, mid: '-' }, + explanation: `Check if the array is empty: nums.length (${n}) === 0? False.`, + pseudoStep: "IF nums.length == 0" + }); + addLines(3, 3, 4, 5); - // Step 2: Declare pointers - newSteps.push({ + let mid = 0; + while (left < right) { + // Loop Check + generatedSteps.push({ array: [...nums], highlights: [left, right], - variables: { left, right, mid: '?' }, - explanation: `Initialize pointers: left = ${left}, right = ${right} (last index). Search space: [${left}...${right}].`, - highlightedLines: [2], - lineExecution: `let left = 0, right = nums.length - 1;` + variables: { left, right, mid: mid || '-' }, + explanation: `Check if left (${left}) < right (${right}): Yes, continue search.`, + pseudoStep: "WHILE left < right" }); + addLines(4, 5, 5, 6); - while (left < right) { - // Loop check - newSteps.push({ - array: [...nums], - highlights: [left, right], - variables: { left, right, mid: mid || '?' }, - explanation: `Check loop condition: left (${left}) < right (${right})? Yes.`, - highlightedLines: [4], - lineExecution: "while (left < right)" - }); + mid = Math.floor((left + right) / 2); + generatedSteps.push({ + array: [...nums], + highlights: [left, mid, right], + variables: { left, right, mid }, + explanation: `Calculate middle index: mid = (${left} + ${right}) / 2 = ${mid}.`, + pseudoStep: `SET mid = (left + right) / 2` + }); + addLines(5, 6, 6, 7); - mid = Math.floor((left + right) / 2); + // Check condition + generatedSteps.push({ + array: [...nums], + highlights: [mid, right], + variables: { left, right, mid }, + explanation: `Compare nums[mid] (${nums[mid]}) > nums[right] (${nums[right]}) to determine which half is sorted.`, + pseudoStep: `IF nums[mid] > nums[right]` + }); + addLines(6, 7, 7, 8); - // Calc mid - newSteps.push({ + if (nums[mid] > nums[right]) { + left = mid + 1; + generatedSteps.push({ array: [...nums], - highlights: [left, mid, right], + highlights: [left, right], variables: { left, right, mid }, - explanation: `Calculate mid index: Math.floor((${left} + ${right}) / 2) = ${mid}.`, - highlightedLines: [5], - lineExecution: "const mid = Math.floor((left + right) / 2);" + explanation: `Since nums[mid] (${nums[mid]}) > nums[right] (${nums[right - 1 === mid ? right : right]}), the pivot is in the right half. Minimum must be after mid. Set left = mid + 1 = ${left}.`, + pseudoStep: "SET left = mid + 1" }); - - // Check condition - newSteps.push({ + addLines(7, 8, 8, 9); + } else { + right = mid; + generatedSteps.push({ array: [...nums], - highlights: [mid, right], + highlights: [left, right], variables: { left, right, mid }, - explanation: `Compare nums[mid] (${nums[mid]}) > nums[right] (${nums[right]})? ${nums[mid] > nums[right] ? 'Yes' : 'No'}.`, - highlightedLines: [7], - lineExecution: `if (nums[mid] > nums[right])` + explanation: `Since nums[mid] (${nums[mid]}) <= nums[right] (${nums[right]}), the right half is sorted normally. Minimum is at or before mid. Set right = mid = ${right}.`, + pseudoStep: "SET right = mid" }); - - if (nums[mid] > nums[right]) { - newSteps.push({ - array: [...nums], - highlights: [left, right], - variables: { left, right, mid }, - explanation: `Since ${nums[mid]} > ${nums[right]}, minimum is strictly to the right. New left = mid + 1 = ${mid + 1}.`, - highlightedLines: [8], - lineExecution: "left = mid + 1;" - }); - left = mid + 1; - } else { - newSteps.push({ - array: [...nums], - highlights: [left, right], - variables: { left, right, mid }, - explanation: `Since ${nums[mid]} <= ${nums[right]}, minimum is to the left (or is mid). New right = mid = ${mid}.`, - highlightedLines: [10], - lineExecution: "right = mid;" - }); - right = mid; - } + addLines(9, 10, 10, 11); } + } - // Loop exit - newSteps.push({ - array: [...nums], - highlights: [left, right], - variables: { left, right, mid }, - explanation: `Check loop condition: left (${left}) < right (${right})? No. Loop terminates.`, - highlightedLines: [4], - lineExecution: "while (left < right)" - }); - - // Return - newSteps.push({ - array: [...nums], - highlights: [left], - variables: { left, right, mid, result: nums[left] }, - explanation: `Return nums[left] = nums[${left}] = ${nums[left]}. Found minimum!`, - highlightedLines: [14], - lineExecution: "return nums[left];" - }); + // Loop End Check + generatedSteps.push({ + array: [...nums], + highlights: [left, right], + variables: { left, right, mid }, + explanation: `Check loop condition: left (${left}) < right (${right}): False. Loop terminates.`, + pseudoStep: "WHILE left < right" + }); + addLines(4, 5, 5, 6); - setSteps(newSteps); - }; + // Return + generatedSteps.push({ + array: [...nums], + highlights: [left], + variables: { left, right, mid, result: nums[left] }, + explanation: `Pointers converged. Minimum element is nums[left] = nums[${left}] = ${nums[left]}.`, + pseudoStep: "RETURN nums[left]" + }); + addLines(12, 11, 13, 14); - generateSteps(); + setSteps(generatedSteps); + setStepLineNumbers(stepLines); }, []); if (steps.length === 0) return null; - const step = steps[currentStep]; + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( + } leftContent={ - <> - - +
+
+ +

+ Find Minimum in Rotated Sorted Array +

+
-

Find Minimum in Rotated Sorted Array

-
- {step.array.map((value, index) => { - const l = step.variables.left; - const r = step.variables.right; - const m = step.variables.mid; - - const isL = index === l; - const isR = index === r; - const isM = index === m; - - return ( -
-
- {isL && L} - {isM && M} - {isR && R} -
-
- {value} +
+
Rotated Array (nums)
+
+ {currentStep.array.map((value, index) => { + const l = currentStep.variables.left; + const r = currentStep.variables.right; + const m = currentStep.variables.mid; + + const isL = index === l; + const isR = index === r; + const isM = index === m; + + return ( +
+
+ {isL && L} + {isM && M} + {isR && R} +
+
+ {value} +
+ [{index}]
- [{index}] -
- ); - })} + ); + })} +
- - - - -
-
Current Execution:
-
- {step.lineExecution} -
-
- {step.explanation} -
-
+
+ +
+ +
+

Step Explanation

+

{currentStep.explanation}

- - - - - - + +
+
} rightContent={ - - } - controls={ - setCurrentStepIndex(0)} /> } /> ); }; + diff --git a/src/components/visualizations/algorithms/FloydWarshallVisualization.tsx b/src/components/visualizations/algorithms/FloydWarshallVisualization.tsx index cee65ab..7e8fdca 100644 --- a/src/components/visualizations/algorithms/FloydWarshallVisualization.tsx +++ b/src/components/visualizations/algorithms/FloydWarshallVisualization.tsx @@ -1,184 +1,238 @@ import { useEffect, useRef, useState } from 'react'; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { matrix: number[][]; k: number; i: number; j: number; - message: string; - lineNumber: number; + explanation: string; + pseudoStep: string; + variables: Record; } -export const FloydWarshallVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const intervalRef = useRef(null); +const languages: VisualizationLanguageMap = { + typescript: `function floydWarshall(n: number, edges: [number, number, number][]): number[][] { + const dist = Array.from({ length: n }, () => Array(n).fill(Infinity)); + for (let i = 0; i < n; i++) dist[i][i] = 0; + for (const [u, v, w] of edges) { + dist[u][v] = Math.min(dist[u][v], w); + } + for (let k = 0; k < n; k++) { + for (let i = 0; i < n; i++) { + for (let j = 0; j < n; j++) { + dist[i][j] = Math.min(dist[i][j], dist[i][k] + dist[k][j]); + } + } + } + return dist; +}`, + python: `def floydWarshall(n: int, edges: list[list[int]]) -> list[list[int]]: + dist = [[float('inf')] * n for _ in range(n)] + for i in range(n): + dist[i][i] = 0 + for u, v, w in edges: + dist[u][v] = min(dist[u][v], w) + for k in range(n): + for i in range(n): + for j in range(n): + dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]) + return dist`, + java: `class Solution { + public int[][] floydWarshall(int n, int[][] edges) { + int[][] dist = new int[n][n]; + for (int i = 0; i < n; i++) { + Arrays.fill(dist[i], 1000000000); + dist[i][i] = 0; + } + for (int[] edge : edges) { + int u = edge[0]; + int v = edge[1]; + int w = edge[2]; + dist[u][v] = Math.min(dist[u][v], w); + } + for (int k = 0; k < n; k++) { + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + dist[i][j] = Math.min(dist[i][j], dist[i][k] + dist[k][j]); + } + } + } + return dist; + } +}`, + cpp: `class Solution { +public: + vector> floydWarshall(int n, vector>& edges) { + vector> dist(n, vector(n, 1e9)); + for (int i = 0; i < n; i++) dist[i][i] = 0; + for (auto& edge : edges) { + int u = edge[0]; + int v = edge[1]; + int w = edge[2]; + dist[u][v] = min(dist[u][v], w); + } + for (int k = 0; k < n; k++) { + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]); + } + } + } + return dist; + } +};`, +}; - const code = `function floydWarshall(n: number, edges: [number, number, number][]): number[][] { - const dist = Array.from({ length: n }, () => Array(n).fill(Infinity)); +function generateVisualizationData() { + const INF = Infinity; + const n = 4; + const edges: [number, number, number][] = [ + [0, 1, 3], + [0, 3, 7], + [1, 0, 8], + [1, 2, 2], + [2, 0, 5], + [2, 3, 1], + [3, 0, 2] + ]; + + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { typescript: [], python: [], java: [], cpp: [] }; + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - for (let i = 0; i < n; i++) dist[i][i] = 0; + const dist = Array.from({ length: n }, () => Array(n).fill(INF)); + + steps.push({ + matrix: dist.map(row => [...row]), + k: -1, + i: -1, + j: -1, + explanation: 'Initialize the distance matrix with Infinity for all pairs of vertices.', + pseudoStep: 'SET dist = 2D Array of Infinity', + variables: { k: 'N/A', i: 'N/A', j: 'N/A' } + }); + addLines(2, 2, 3, 4); + + for (let i = 0; i < n; i++) { + dist[i][i] = 0; + steps.push({ + matrix: dist.map(row => [...row]), + k: -1, + i, + j: i, + explanation: `Set the diagonal distance from node ${i} to itself to 0.`, + pseudoStep: `SET dist[${i}][${i}] = 0`, + variables: { k: 'N/A', i, j: i } + }); + addLines(3, 3, 6, 5); + } for (const [u, v, w] of edges) { dist[u][v] = Math.min(dist[u][v], w); - } - - for (let k = 0; k < n; k++) { - for (let i = 0; i < n; i++) { - for (let j = 0; j < n; j++) { - dist[i][j] = Math.min(dist[i][j], dist[i][k] + dist[k][j]); - } - } - } - - return dist; -}`; - - - const generateSteps = () => { - const INF = Infinity; - const n = 4; - const edges: [number, number, number][] = [ - [0, 1, 3], - [0, 3, 7], - [1, 0, 8], - [1, 2, 2], - [2, 0, 5], - [2, 3, 1], - [3, 0, 2] - ]; - - const newSteps: Step[] = []; - const dist = Array.from({ length: n }, () => Array(n).fill(INF)); - - newSteps.push({ + steps.push({ matrix: dist.map(row => [...row]), k: -1, - i: -1, - j: -1, - message: 'Initializing distance matrix with Infinity', - lineNumber: 2 + i: u, + j: v, + explanation: `Populate the distance matrix with the direct edge weight from node ${u} to node ${v} (weight ${w}).`, + pseudoStep: `SET dist[${u}][${v}] = min(dist[${u}][${v}], ${w}) → ${dist[u][v]}`, + variables: { k: 'N/A', i: u, j: v } }); + addLines(4, 5, 12, 9); + } + // Iterate intermediate nodes + for (let k = 0; k < n; k++) { for (let i = 0; i < n; i++) { - dist[i][i] = 0; - newSteps.push({ - matrix: dist.map(row => [...row]), - k: -1, - i, - j: i, - message: `Setting distance from node ${i} to itself as 0`, - lineNumber: 4 - }); - } - - for (const [u, v, w] of edges) { - dist[u][v] = Math.min(dist[u][v], w); - newSteps.push({ - matrix: dist.map(row => [...row]), - k: -1, - i: u, - j: v, - message: `Processing edge (${u}, ${v}) with weight ${w}`, - lineNumber: 7 - }); - } + for (let j = 0; j < n; j++) { + // Skip diagonal or unreachable intermediate states + if (i === j) continue; + const currentVal = dist[i][j]; + const viaVal = dist[i][k] + dist[k][j]; + const isBetter = viaVal < currentVal; + + steps.push({ + matrix: dist.map(row => [...row]), + k, + i, + j, + explanation: `Compare path from node ${i} to node ${j} directly vs via intermediate node ${k}. Direct: ${currentVal === INF ? '∞' : currentVal}. Via ${k}: ${dist[i][k] === INF ? '∞' : dist[i][k]} + ${dist[k][j] === INF ? '∞' : dist[k][j]} = ${viaVal === INF ? '∞' : viaVal}.`, + pseudoStep: `IF dist[${i}][${k}] + dist[${k}][${j}] < dist[${i}][${j}] → ${isBetter ? 'YES ✓' : 'NO ✗'}`, + variables: { k, i, j, current: currentVal === INF ? '∞' : currentVal, via: viaVal === INF ? '∞' : viaVal } + }); + addLines(10, 10, 17, 17); - for (let k = 0; k < n; k++) { - newSteps.push({ - matrix: dist.map(row => [...row]), - k, - i: -1, - j: -1, - message: `Using node ${k} as intermediate node`, - lineNumber: 10 - }); - - for (let i = 0; i < n; i++) { - for (let j = 0; j < n; j++) { - const direct = dist[i][j]; - const throughK = dist[i][k] + dist[k][j]; - - newSteps.push({ + if (isBetter) { + dist[i][j] = viaVal; + steps.push({ matrix: dist.map(row => [...row]), k, i, j, - message: `Check path ${i}→${k}→${j}: ${dist[i][k] === INF ? '∞' : dist[i][k]} + ${dist[k][j] === INF ? '∞' : dist[k][j]} = ${throughK >= INF ? '∞' : throughK} vs ${direct === INF ? '∞' : direct}`, - lineNumber: 13 + explanation: `Found a shorter path! Update dist[${i}][${j}] to ${viaVal}.`, + pseudoStep: `SET dist[${i}][${j}] = ${viaVal}`, + variables: { k, i, j } }); - - if (throughK < dist[i][j]) { - dist[i][j] = throughK; - newSteps.push({ - matrix: dist.map(row => [...row]), - k, - i, - j, - message: `Update dist[${i}][${j}] = ${throughK}`, - lineNumber: 13 - }); - } + addLines(10, 10, 17, 17); } } } + } - newSteps.push({ - matrix: dist.map(row => [...row]), - k: n, - i: -1, - j: -1, - message: 'All pairs shortest paths computed!', - lineNumber: 18 - }); - - setSteps(newSteps); - setCurrentStepIndex(0); - }; - + steps.push({ + matrix: dist.map(row => [...row]), + k: n - 1, + i: -1, + j: -1, + explanation: 'Floyd-Warshall algorithm complete! Shortest paths for all pairs have been calculated.', + pseudoStep: 'RETURN dist', + variables: { k: 'done', i: 'done', j: 'done' } + }); + addLines(14, 11, 21, 21); + + return { steps, stepLineNumbers }; +} - useEffect(() => { - generateSteps(); - }, []); +export const FloydWarshallVisualization = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { intervalRef.current = setInterval(() => { setCurrentStepIndex(prev => { - if (prev >= steps.length - 1) { - setIsPlaying(false); - return prev; - } + if (prev >= steps.length - 1) { setIsPlaying(false); return prev; } return prev + 1; }); }, 1000 / speed); } else { if (intervalRef.current) clearInterval(intervalRef.current); } - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - }; + return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [isPlaying, currentStepIndex, steps.length, speed]); const handlePlay = () => setIsPlaying(true); const handlePause = () => setIsPlaying(false); - const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(prev => prev + 1); - const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(prev => prev - 1); - const handleReset = () => { - setCurrentStepIndex(0); - setIsPlaying(false); - generateSteps(); - }; + const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(p => p + 1); + const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(p => p - 1); + const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); }; if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return (
@@ -197,51 +251,68 @@ export const FloydWarshallVisualization = () => {
-
-
- +
+

Distance Matrix

+
+
{currentStep.matrix.map((row, i) => ( - {row.map((cell, j) => ( - - - ))} + {row.map((cell, j) => { + const isCurrentCell = i === currentStep.i && j === currentStep.j; + const isKNode = i === currentStep.k || j === currentStep.k; + const isRowOrCol = (i === currentStep.i || j === currentStep.j) && currentStep.i !== -1; + + return ( + + ); + })} ))}
- {cell === Infinity ? '∞' : cell} - + {cell === Infinity ? '∞' : cell} +
+
+ Current Cell [i][j] + k-Path [i][k] or [k][j] + Standard +
-

{currentStep.message}

-
-
- = 0 ? currentStep.k : 'N/A', - i: currentStep.i >= 0 ? currentStep.i : 'N/A', - j: currentStep.j >= 0 ? currentStep.j : 'N/A' - }} - /> +

{currentStep.explanation}

+ + = 0 ? currentStep.k : 'N/A', + i: currentStep.i >= 0 ? currentStep.i : 'N/A', + j: currentStep.j >= 0 ? currentStep.j : 'N/A', + ...currentStep.variables + }} + />
- +
- -
); }; diff --git a/src/components/visualizations/algorithms/GCDVisualization.tsx b/src/components/visualizations/algorithms/GCDVisualization.tsx index 31b4640..d9e65b1 100644 --- a/src/components/visualizations/algorithms/GCDVisualization.tsx +++ b/src/components/visualizations/algorithms/GCDVisualization.tsx @@ -1,191 +1,242 @@ import { useState, useMemo } from 'react'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; import { motion, AnimatePresence } from 'framer-motion'; import { Card } from '@/components/ui/card'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { a: number; b: number; temp: number | null; explanation: string; - highlightedLines: number[]; + pseudoStep: string; variables: Record; } +const languages: VisualizationLanguageMap = { + typescript: `function gcd(a: number, b: number): number { + while (b) { + const temp = b; + b = a % b; + a = temp; + } + return a; +}`, + python: `def gcd_euclidean(a, b): + while(b): + a, b = b, a % b + return a`, + java: `public static int gcd(int a, int b) { + while (b != 0) { + int temp = b; + b = a % b; + a = temp; + } + return a; +}`, + cpp: `class Solution { +public: + int gcd(int a, int b) { + while (b != 0) { + int temp = b; + b = a % b; + a = temp; + } + return a; + } +};` +}; + export const GCDVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); + const [currentStepIndex, setCurrentStepIndex] = useState(0); const initialA = 48; const initialB = 18; - const steps: Step[] = useMemo(() => { + const { steps, stepLineNumbers } = useMemo(() => { const s: Step[] = []; + const lines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + lines.typescript!.push(ts); + lines.python!.push(py); + lines.java!.push(java); + lines.cpp!.push(cpp); + }; + let a = initialA; let b = initialB; s.push({ a, b, temp: null, explanation: `Starting GCD with a = ${a}, b = ${b}.`, - highlightedLines: [1], + pseudoStep: `CALL gcd(a = ${a}, b = ${b})`, variables: { a, b } }); + addLines(1, 1, 1, 3); while (b) { s.push({ a, b, temp: null, explanation: `Checking while condition: b (${b}) is non-zero.`, - highlightedLines: [2], + pseudoStep: `WHILE b != 0 (${b} != 0) → YES ✓`, variables: { a, b } }); + addLines(2, 2, 2, 4); const temp = b; s.push({ a, b, temp, explanation: `Store current b in temp: temp = ${temp}.`, - highlightedLines: [3], + pseudoStep: `SET temp = b (temp = ${temp})`, variables: { a, b, temp } }); + addLines(3, 2, 3, 5); const remainder = a % b; b = remainder; s.push({ a, b, temp, explanation: `Calculate remainder: b = a % b = ${a} % ${temp} = ${remainder}.`, - highlightedLines: [4], + pseudoStep: `SET b = a % b (b = ${a} % ${temp} = ${remainder})`, variables: { a, b, temp, remainder } }); + addLines(4, 3, 4, 6); a = temp; s.push({ a, b, temp, explanation: `Update a to the previous value of b (stored in temp): a = ${a}.`, - highlightedLines: [5], + pseudoStep: `SET a = temp (a = ${temp})`, variables: { a, b, temp } }); + addLines(5, 3, 5, 7); } s.push({ a, b, temp: null, explanation: "b is now 0. The GCD is the current value of a.", - highlightedLines: [2], + pseudoStep: "WHILE b != 0 (0 != 0) → NO ✗", variables: { a, b } }); + addLines(2, 2, 2, 4); s.push({ a, b, temp: null, explanation: `Return GCD = ${a}.`, - highlightedLines: [7], + pseudoStep: `RETURN a (a = ${a})`, variables: { result: a } }); + addLines(7, 4, 7, 9); - return s; + return { steps: s, stepLineNumbers: lines }; }, [initialA, initialB]); - const code = `function gcd(a: number, b: number): number { - while (b) { - const temp = b; - b = a % b; - a = temp; - } - return a; -}`; - - const step = steps[currentStep]; + const step = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( - -
- - - -
- -

Division Algorithm Status

- -
-
-
Value A
- - {step.a} - +
+
+ +
+ + +
-
-
%
- - - - - -
+

Division Algorithm Status

-
-
Value B
- - {step.b} - -
-
+
+
+
Value A
+ + {step.a} + +
-
- - {step.temp !== null && ( +
+
%
- Temp (Stored B) - {step.temp} + + + - )} - - -
- Modulo (A % B) - - {step.variables.remainder !== undefined ? step.variables.remainder : (step.b !== 0 ? step.a % step.b : '0')} - -
-
- +
- -
-

Euclidean Insight

-

{step.explanation}

- +
+
Value B
+ + {step.b} + +
+
- +
+ + {step.temp !== null && ( + + Temp (Stored B) + {step.temp} + + )} + + +
+ Modulo (A % B) + + {step.variables.remainder !== undefined ? step.variables.remainder : (step.b !== 0 ? step.a % step.b : '0')} + +
+
+
+
+ +
+ +
+

Euclidean Insight

+

{step.explanation}

+ + + +
} rightContent={ - setCurrentStepIndex(0)} /> } controls={ } /> diff --git a/src/components/visualizations/algorithms/GasStationVisualization.tsx b/src/components/visualizations/algorithms/GasStationVisualization.tsx index 5414e49..5d0f98e 100644 --- a/src/components/visualizations/algorithms/GasStationVisualization.tsx +++ b/src/components/visualizations/algorithms/GasStationVisualization.tsx @@ -1,7 +1,9 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { StepControls } from '../shared/StepControls'; -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import React, { useState } from 'react'; +import { Info } from 'lucide-react'; +import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { gas: number[]; @@ -11,186 +13,268 @@ interface Step { current: number; totalGas: number; totalCost: number; - message: string; - lineNumber: number; + explanation: string; + pseudoStep: string; } -export const GasStationVisualization: React.FC = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1000); - const intervalRef = useRef(null); - - const code = `function canCompleteCircuit(gas: number[], cost: number[]): number { +const languages: VisualizationLanguageMap = { + typescript: `function canCompleteCircuit(gas: number[], cost: number[]): number { const totalGas = gas.reduce((a, b) => a + b, 0); const totalCost = cost.reduce((a, b) => a + b, 0); - if (totalGas < totalCost) return -1; - let total = 0; let res = 0; - for (let i = 0; i < gas.length; i++) { total += gas[i] - cost[i]; - if (total < 0) { total = 0; res = i + 1; } } - return res; -}`; +}`, + + python: `def canCompleteCircuit(gas: list[int], cost: list[int]) -> int: + if sum(gas) < sum(cost): + return -1 + total = 0 + res = 0 + for i in range(len(gas)): + total += gas[i] - cost[i] + if total < 0: + total = 0 + res = i + 1 + return res`, + + java: `public static class Solution { + public int canCompleteCircuit(int[] gas, int[] cost) { + int sumGas = 0, sumCost = 0; + for (int i = 0; i < gas.length; i++) { + sumGas += gas[i]; + sumCost += cost[i]; + } + if (sumGas < sumCost) return -1; + int total = 0, res = 0; + for (int i = 0; i < gas.length; i++) { + total += gas[i] - cost[i]; + if (total < 0) { + total = 0; + res = i + 1; + } + } + return res; + } +}`, + + cpp: `class Solution { +public: + int canCompleteCircuit(vector& gas, vector& cost) { + int totalGas = 0, totalCost = 0; + for (int i = 0; i < gas.size(); i++) { + totalGas += gas[i]; + totalCost += cost[i]; + } + if (totalGas < totalCost) return -1; + int total = 0, res = 0; + for (int i = 0; i < gas.size(); i++) { + total += gas[i] - cost[i]; + if (total < 0) { + total = 0; + res = i + 1; + } + } + return res; + } +};` +}; - const generateSteps = () => { - const gas = [1, 2, 3, 4, 5]; - const cost = [3, 4, 5, 1, 2]; - const newSteps: Step[] = []; +function generateVisualizationData() { + const gas = [1, 2, 3, 4, 5]; + const cost = [3, 4, 5, 1, 2]; + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - newSteps.push({ - gas, cost, res: 0, total: 0, current: -1, totalGas: 0, totalCost: 0, - message: 'Calculate total gas available.', - lineNumber: 2 - }); + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - const totalGas = gas.reduce((a, b) => a + b, 0); - newSteps.push({ - gas, cost, res: 0, total: 0, current: -1, totalGas, totalCost: 0, - message: 'Calculate total cost to travel between stations.', - lineNumber: 3 - }); + const totalGas = gas.reduce((a, b) => a + b, 0); + const totalCost = cost.reduce((a, b) => a + b, 0); - const totalCost = cost.reduce((a, b) => a + b, 0); - newSteps.push({ - gas, cost, res: 0, total: 0, current: -1, totalGas, totalCost, - message: 'Check if total gas < total cost is true.', - lineNumber: 5 + steps.push({ + gas, + cost, + res: 0, + total: 0, + current: -1, + totalGas, + totalCost: 0, + explanation: 'First, sum all the gas available globally to check feasibility.', + pseudoStep: `START canCompleteCircuit() -> totalGas = sum(gas) = ${totalGas}`, + }); + addLines(2, 2, 3, 4); + + steps.push({ + gas, + cost, + res: 0, + total: 0, + current: -1, + totalGas, + totalCost, + explanation: 'Sum all the cost required globally to travel between all stations.', + pseudoStep: `SET totalCost = sum(cost) = ${totalCost}`, + }); + addLines(3, 2, 4, 5); + + steps.push({ + gas, + cost, + res: 0, + total: 0, + current: -1, + totalGas, + totalCost, + explanation: `Check if total gas (${totalGas}) is less than total cost (${totalCost}).`, + pseudoStep: `IF totalGas (${totalGas}) < totalCost (${totalCost})`, + }); + addLines(4, 2, 8, 9); + + if (totalGas < totalCost) { + steps.push({ + gas, + cost, + res: 0, + total: 0, + current: -1, + totalGas, + totalCost, + explanation: 'Not enough gas globally to complete any circuit. Return -1.', + pseudoStep: 'RETURN -1', }); + addLines(4, 3, 8, 9); + return { steps, stepLineNumbers }; + } - if (totalGas < totalCost) { - newSteps.push({ - gas, cost, res: 0, total: 0, current: -1, totalGas, totalCost, - message: 'Not enough gas globally. Return -1.', - lineNumber: 5 - }); - setSteps(newSteps); - return; - } + let total = 0; + let res = 0; - let total = 0; - newSteps.push({ - gas, cost, res: 0, total: total, current: -1, totalGas, totalCost, - message: 'Initialize current tank total = 0.', - lineNumber: 7 - }); + steps.push({ + gas, + cost, + res, + total, + current: -1, + totalGas, + totalCost, + explanation: 'Initialize current tank total to 0 and candidate starting station index to 0.', + pseudoStep: 'SET total = 0, res = 0', + }); + addLines(5, 4, 9, 10); - let res = 0; - newSteps.push({ - gas, cost, res: res, total: total, current: -1, totalGas, totalCost, - message: 'Initialize potential starting station res = 0.', - lineNumber: 8 + for (let i = 0; i < gas.length; i++) { + steps.push({ + gas, + cost, + res, + total, + current: i, + totalGas, + totalCost, + explanation: `Iteration i=${i}: Arrived at station ${i}.`, + pseudoStep: `FOR i = ${i}`, }); + addLines(7, 6, 10, 11); - for (let i = 0; i < gas.length; i++) { - newSteps.push({ - gas, cost, res: res, total: total, current: i, totalGas, totalCost, - message: `Iteration i=${i}: Arrived at station ${i}.`, - lineNumber: 10 - }); - - total += gas[i] - cost[i]; - newSteps.push({ - gas, cost, res: res, total: total, current: i, totalGas, totalCost, - message: `Update tank total += gas[${i}] - cost[${i}].`, - lineNumber: 11 - }); + total += gas[i] - cost[i]; + steps.push({ + gas, + cost, + res, + total, + current: i, + totalGas, + totalCost, + explanation: `Pump gas[${i}] (+${gas[i]}) and pay cost[${i}] (-${cost[i]}). Tank level becomes ${total}.`, + pseudoStep: `SET total = total + gas[${i}] - cost[${i}] → ${total}`, + }); + addLines(8, 7, 11, 12); + + steps.push({ + gas, + cost, + res, + total, + current: i, + totalGas, + totalCost, + explanation: `Check if current tank level is negative: ${total} < 0?`, + pseudoStep: `IF total (${total}) < 0`, + }); + addLines(9, 8, 12, 13); - newSteps.push({ - gas, cost, res: res, total: total, current: i, totalGas, totalCost, - message: `Check if current tank total < 0.`, - lineNumber: 13 + if (total < 0) { + total = 0; + res = i + 1; + steps.push({ + gas, + cost, + res, + total, + current: i, + totalGas, + totalCost, + explanation: `Tank is empty! Reset current tank total to 0 and set potential starting station to index ${res}.`, + pseudoStep: `SET total = 0, res = ${res}`, }); - - if (total < 0) { - total = 0; - newSteps.push({ - gas, cost, res: res, total: total, current: i, totalGas, totalCost, - message: `Tank exhausted! Reset total = 0.`, - lineNumber: 14 - }); - - res = i + 1; - newSteps.push({ - gas, cost, res: res, total: total, current: i, totalGas, totalCost, - message: `Set new potential starting point res = ${res}.`, - lineNumber: 15 - }); - } + addLines(11, 10, 14, 15); } + } - newSteps.push({ - gas, cost, res: res, total: total, current: -1, totalGas, totalCost, - message: `Loop complete. Return starting index ${res}.`, - lineNumber: 19 - }); - - setSteps(newSteps); - }; - - useEffect(() => { - generateSteps(); - }, []); - - useEffect(() => { - if (isPlaying && currentStepIndex < steps.length - 1) { - intervalRef.current = window.setInterval(() => { - setCurrentStepIndex((prev) => { - if (prev >= steps.length - 1) { - setIsPlaying(false); - return prev; - } - return prev + 1; - }); - }, speed); - } else { - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } - } + steps.push({ + gas, + cost, + res, + total, + current: -1, + totalGas, + totalCost, + explanation: `Circuit can be completed. Return the starting station index: ${res}.`, + pseudoStep: `RETURN res → ${res}`, + }); + addLines(14, 11, 17, 18); + + return { steps, stepLineNumbers }; +} - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - }; - }, [isPlaying, currentStepIndex, steps.length, speed]); +export const GasStationVisualization: React.FC = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); - const handlePlay = () => setIsPlaying(true); - const handlePause = () => setIsPlaying(false); - const handleStepForward = () => { - if (currentStepIndex < steps.length - 1) setCurrentStepIndex(currentStepIndex + 1); - }; - const handleStepBack = () => { - if (currentStepIndex > 0) setCurrentStepIndex(currentStepIndex - 1); - }; const handleReset = () => { setCurrentStepIndex(0); - setIsPlaying(false); }; - if (steps.length === 0) return null; - const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map((s) => s.pseudoStep); - const renderCircularVisualization = () => { + const renderCircularRoute = () => { const n = currentStep.gas.length; const cx = 150; const cy = 150; - const radius = 100; + const radius = 95; return (
- + {/* Edges */} {currentStep.gas.map((_, i) => { const nextI = (i + 1) % n; @@ -202,12 +286,11 @@ export const GasStationVisualization: React.FC = () => { const x2 = cx + radius * Math.cos(angle2); const y2 = cy + radius * Math.sin(angle2); - // Make the path a slight curve instead of a straight line through the center const dx = x2 - x1; const dy = y2 - y1; const dist = Math.sqrt(dx * dx + dy * dy); - const nodeRadius = 26; + const nodeRadius = 24; const padding = nodeRadius + 4; const reqPaddingRatio = padding / dist; @@ -216,42 +299,55 @@ export const GasStationVisualization: React.FC = () => { const endX = x2 - dx * reqPaddingRatio; const endY = y2 - dy * reqPaddingRatio; - // Output mid point mapped out a little radially const midAngle = ((i + 0.5) * 2 * Math.PI) / n - Math.PI / 2; - const midX = cx + (radius + 20) * Math.cos(midAngle); - const midY = cy + (radius + 20) * Math.sin(midAngle); + const midX = cx + (radius + 18) * Math.cos(midAngle); + const midY = cy + (radius + 18) * Math.sin(midAngle); const isTraveling = currentStep.current === i; return ( - - + + - {/* Cost badge */} -{currentStep.cost[i]} @@ -267,105 +363,188 @@ export const GasStationVisualization: React.FC = () => { const isStart = i === currentStep.res; const isCurrent = i === currentStep.current; + let nodeClass = 'fill-card stroke-border'; + if (isCurrent) { + nodeClass = 'fill-primary/20 stroke-primary'; + } else if (isStart) { + nodeClass = 'fill-green-500/20 stroke-green-500'; + } + return ( - - + + S{i} - + +{gas} {isCurrent && ( - + )} ); })} - {/* Center Info text */} - - {currentStep.total >= 0 ? "Tank" : "Empty!"} + {/* Center Info Panel */} + + {currentStep.total >= 0 ? 'Tank Level' : 'Empty!'} - = 0 ? "fill-primary" : "fill-red-500"}`}> + = 0 ? 'fill-primary' : 'fill-red-500' + }`} + > {currentStep.total} -
+
); }; return ( -
- + -
-
-
-

Circular Route

+
+ {/* Left Column: Visual State */} +
+
+

+ Circular Route +

+

+ Find the starting gas station index from which we can complete a full circle without running out of gas. +

+ + {renderCircularRoute()} + +
+
+
+ Start candidate +
+
+ {currentStep.res} +
+
+
+
+ Current Tank +
+
= 0 ? 'text-primary' : 'text-red-500' + }`} + > + {currentStep.total} +
+
+
+
+ Global Fuel +
+
= currentStep.totalCost ? 'text-green-500' : 'text-red-500' + }`} + > + {currentStep.totalGas - currentStep.totalCost} +
+
+
-

- There are n gas stations along a circular route. You have a car with an unlimited gas tank. - It costs cost[i] to travel to the next station, and you pump gas[i] when you arrive. -

- - {renderCircularVisualization()} - -
-
-
Start Station (res)
-
{currentStep.res}
-
-
-
Current Tank (total)
-
= 0 ? 'text-primary' : 'text-red-500'}`}> - {currentStep.total} + {/* Commentary Panel */} +
+
+
+
+ + + + + + Algorithm Commentary + +
+
+ Step {currentStepIndex + 1} of {steps.length} +
-
-
-
Total Balance
-
= currentStep.totalCost ? 'text-green-500' : 'text-red-500'}`}> - {currentStep.totalGas - currentStep.totalCost} + +
+
+ +
+
+

+ Current Action +

+
+ {currentStep.explanation} +
+
-
-

{currentStep.message}

-
+
- + {/* Right Column: Code Display */} +
+ +
- -
); }; + +export default GasStationVisualization; diff --git a/src/components/visualizations/algorithms/GraphBFSVisualization.tsx b/src/components/visualizations/algorithms/GraphBFSVisualization.tsx index 1f7e18b..a074305 100644 --- a/src/components/visualizations/algorithms/GraphBFSVisualization.tsx +++ b/src/components/visualizations/algorithms/GraphBFSVisualization.tsx @@ -1,10 +1,9 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import { useEffect, useRef, useState } from 'react'; import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { GraphDiagram } from '../GraphDiagram'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { currentNode: number | null; @@ -12,147 +11,210 @@ interface Step { visited: number[]; queue: number[]; result: number[]; - message: string; - lineNumber: number; + explanation: string; + pseudoStep: string; + variables: Record; } -export const GraphBFSVisualization: React.FC = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const intervalRef = useRef(null); +const languages: VisualizationLanguageMap = { + typescript: `function bfs(graph: number[][], start: number): number[] { + const visited = new Set(); + const queue: number[] = [start]; + const result: number[] = []; + visited.add(start); + while (queue.length > 0) { + const node = queue.shift()!; + result.push(node); + for (const neighbor of graph[node]) { + if (!visited.has(neighbor)) { + visited.add(neighbor); + queue.push(neighbor); + } + } + } + return result; +}`, + python: `def bfs(graph: list[list[int]], start: int) -> list[int]: + visited = set() + queue = [start] + result = [] + visited.add(start) + while queue: + node = queue.pop(0) + result.append(node) + for neighbor in graph[node]: + if neighbor not in visited: + visited.add(neighbor) + queue.append(neighbor) + return result`, + java: `class Solution { + public List bfs(List> graph, int start) { + Set visited = new HashSet<>(); + Queue queue = new LinkedList<>(); + List result = new ArrayList<>(); + visited.add(start); + queue.offer(start); + while (!queue.isEmpty()) { + int node = queue.poll(); + result.add(node); + for (int neighbor : graph.get(node)) { + if (!visited.contains(neighbor)) { + visited.add(neighbor); + queue.offer(neighbor); + } + } + } + return result; + } +}`, + cpp: `class Solution { +public: + vector bfs(vector>& graph, int start) { + unordered_set visited; + queue q; + vector result; + visited.insert(start); + q.push(start); + while (!q.empty()) { + int node = q.front(); + q.pop(); + result.push_back(node); + for (int neighbor : graph[node]) { + if (visited.find(neighbor) == visited.end()) { + visited.insert(neighbor); + q.push(neighbor); + } + } + } + return result; + } +};`, +}; - const code = `function bfs(graph: number[][], start: number): number[] { +function generateVisualizationData() { + const graph: number[][] = [ + [1, 2], // 0 + [0, 3], // 1 + [0], // 2 + [1, 4], // 3 + [3, 5], // 4 + [4] // 5 + ]; + + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { typescript: [], python: [], java: [], cpp: [] }; const visited = new Set(); - const queue: number[] = [start]; + const queue: number[] = []; const result: number[] = []; - visited.add(start); - - while (queue.length > 0) { - const node = queue.shift()!; - result.push(node); + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - for (const neighbor of graph[node]) { - if (!visited.has(neighbor)) { - visited.add(neighbor); - queue.push(neighbor); + const pushStep = ( + msg: string, + pseudo: string, + ts: number, + py: number, + java: number, + cpp: number, + current: number | null, + neighbor: number | null = null + ) => { + steps.push({ + currentNode: current, + activeNeighbor: neighbor, + visited: Array.from(visited), + queue: [...queue], + result: [...result], + explanation: msg, + pseudoStep: pseudo, + variables: { + node: current !== null ? String(current) : '-', + neighbor: neighbor !== null ? String(neighbor) : 'none', + visited: `{${Array.from(visited).join(', ')}}`, + queue: `[${queue.join(', ')}]`, + result: `[${result.join(', ')}]` } - } - } - - return result; -}`; - - const generateSteps = () => { - const graph: number[][] = [ - [1, 2], // 0 - [0, 3, 4], // 1 - [0, 5], // 2 - [1], // 3 - [1], // 4 - [2] // 5 - ]; - - const newSteps: Step[] = []; - const visited = new Set(); - const queue: number[] = []; - const result: number[] = []; - - const pushStep = (msg: string, line: number, current: number | null, neighbor: number | null = null) => { - newSteps.push({ - currentNode: current, - activeNeighbor: neighbor, - visited: Array.from(visited), - queue: [...queue], - result: [...result], - message: msg, - lineNumber: line - }); - }; - - // Initialization steps - pushStep('Initialize visited set.', 2, null); - - queue.push(0); - pushStep('Initialize queue with starting node 0.', 3, null); + }); + addLines(ts, py, java, cpp); + }; - pushStep('Initialize empty result array.', 4, null); + // Init + pushStep('Initialize visited set.', 'SET visited = {}', 2, 2, 3, 5, null); + queue.push(0); + pushStep('Initialize queue with starting node 0.', 'SET queue = [0]', 3, 3, 4, 6, null); + pushStep('Initialize empty result array.', 'SET result = []', 4, 4, 5, 7, null); - visited.add(0); - pushStep('Mark start node 0 as visited.', 6, null); + visited.add(0); + pushStep('Mark starting node 0 as visited.', 'visited.add(0)', 5, 5, 6, 8, null); - while (queue.length > 0) { - pushStep('Check if queue is not empty.', 8, null); + while (queue.length > 0) { + pushStep('Check if queue is not empty.', 'WHILE queue IS NOT EMPTY', 6, 6, 8, 10, null); - const node = queue.shift()!; - pushStep(`Dequeue node ${node} to process.`, 9, node); + const node = queue.shift()!; + pushStep(`Dequeue node ${node} to process.`, `SET node = dequeue(queue) → ${node}`, 7, 7, 9, 11, node); - result.push(node); - pushStep(`Add node ${node} to the traversal result.`, 10, node); + result.push(node); + pushStep(`Add node ${node} to result array.`, `result.push(${node})`, 8, 8, 10, 12, node); - const neighbors = graph[node] || []; - pushStep(`Check neighbors of node ${node}: [${neighbors.join(', ')}].`, 12, node); + const neighbors = graph[node] || []; + pushStep(`Iterate neighbors of node ${node}: [${neighbors.join(', ')}].`, `FOR neighbor IN neighbors of ${node}`, 9, 9, 11, 13, node); - for (const neighbor of neighbors) { - pushStep(`Check if neighbor ${neighbor} has been visited.`, 13, node, neighbor); + for (const neighbor of neighbors) { + pushStep(`Check if neighbor ${neighbor} is visited.`, `IF neighbor ${neighbor} NOT IN visited`, 10, 10, 12, 14, node, neighbor); - if (!visited.has(neighbor)) { - visited.add(neighbor); - pushStep(`Neighbor ${neighbor} not visited. Mark it as visited.`, 14, node, neighbor); + if (!visited.has(neighbor)) { + visited.add(neighbor); + pushStep(`Neighbor ${neighbor} not visited. Mark as visited.`, `visited.add(${neighbor})`, 11, 11, 13, 15, node, neighbor); - queue.push(neighbor); - pushStep(`Enqueue neighbor ${neighbor} for future processing.`, 15, node, neighbor); - } else { - pushStep(`Neighbor ${neighbor} already visited. Skipping.`, 13, node, neighbor); - } + queue.push(neighbor); + pushStep(`Push neighbor ${neighbor} to queue.`, `queue.push(${neighbor})`, 12, 12, 14, 16, node, neighbor); + } else { + pushStep(`Neighbor ${neighbor} already visited. Skip.`, `SKIP neighbor ${neighbor}`, 10, 10, 12, 14, node, neighbor); } } + } - pushStep('Queue is empty. BFS traversal complete.', 18, null); - pushStep(`Returning final result: [${result.join(', ')}].`, 20, null); + pushStep(`BFS traversal complete! Return final order: [${result.join(', ')}].`, `RETURN result → [${result.join(', ')}]`, 16, 16, 18, 20, null); - setSteps(newSteps); - setCurrentStepIndex(0); - }; + return { steps, stepLineNumbers }; +} - useEffect(() => { - generateSteps(); - }, []); +export const GraphBFSVisualization = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { intervalRef.current = setInterval(() => { setCurrentStepIndex(prev => { - if (prev >= steps.length - 1) { - setIsPlaying(false); - return prev; - } + if (prev >= steps.length - 1) { setIsPlaying(false); return prev; } return prev + 1; }); - }, 1200 / speed); + }, 1000 / speed); } else { if (intervalRef.current) clearInterval(intervalRef.current); } - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - } + return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [isPlaying, currentStepIndex, steps.length, speed]); const handlePlay = () => setIsPlaying(true); const handlePause = () => setIsPlaying(false); - const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(prev => prev + 1); - const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(prev => prev - 1); - const handleReset = () => { - setCurrentStepIndex(0); - setIsPlaying(false); - generateSteps(); - }; + const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(p => p + 1); + const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(p => p - 1); + const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); }; if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return (
@@ -171,91 +233,39 @@ export const GraphBFSVisualization: React.FC = () => {
-
+
- -
+
-
- Current +
+ Current Node
-
+
Visited
- {currentStep.activeNeighbor !== null && ( -
-
- Checking Neighbor -
- )}
-
-

Algorithm Queue

-
- - {currentStep.queue.length === 0 ? ( - - Queue is empty - - ) : ( - currentStep.queue.map((node, i) => ( - - {node} - - )) - )} - -
-
- -
- - - "{currentStep.message}" - - +
+

{currentStep.explanation}

-
- -
+
-
- -
+
); diff --git a/src/components/visualizations/algorithms/GraphDFSVisualization.tsx b/src/components/visualizations/algorithms/GraphDFSVisualization.tsx index 6f3e5dd..07e2d42 100644 --- a/src/components/visualizations/algorithms/GraphDFSVisualization.tsx +++ b/src/components/visualizations/algorithms/GraphDFSVisualization.tsx @@ -1,160 +1,285 @@ -import React, { useEffect, useRef, useState } from 'react'; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import { useEffect, useRef, useState } from 'react'; import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { GraphDiagram } from '../GraphDiagram'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { currentNode: number | null; neighbor: number | null; visited: number[]; recursionStack: number[]; - message: string; - lineNumber: number; + result: number[]; + explanation: string; + pseudoStep: string; + variables: Record; } -export const GraphDFSVisualization: React.FC = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1000); - const intervalRef = useRef(null); +const languages: VisualizationLanguageMap = { + typescript: `function dfs(graph: number[][], start: number): number[] { + const visited = new Set(); + const result: number[] = []; + function explore(node: number) { + visited.add(node); + result.push(node); + for (const neighbor of graph[node]) { + if (!visited.has(neighbor)) { + explore(neighbor); + } + } + } + explore(start); + return result; +}`, + python: `def dfs(graph: list[list[int]], start: int) -> list[int]: + visited = set() + result = [] + def explore(node: int): + visited.add(node) + result.append(node) + for neighbor in graph[node]: + if neighbor not in visited: + explore(neighbor) + explore(start) + return result`, + java: `class Solution { + private void explore(List> graph, int node, Set visited, List result) { + visited.add(node); + result.add(node); + for (int neighbor : graph.get(node)) { + if (!visited.contains(neighbor)) { + explore(graph, neighbor, visited, result); + } + } + } + public List dfs(List> graph, int start) { + Set visited = new HashSet<>(); + List result = new ArrayList<>(); + explore(graph, start, visited, result); + return result; + } +}`, + cpp: `class Solution { +private: + void explore(vector>& graph, int node, unordered_set& visited, vector& result) { + visited.insert(node); + result.push_back(node); + for (int neighbor : graph[node]) { + if (visited.find(neighbor) == visited.end()) { + explore(graph, neighbor, visited, result); + } + } + } +public: + vector dfs(vector>& graph, int start) { + unordered_set visited; + vector result; + explore(graph, start, visited, result); + return result; + } +};`, +}; - const code = `function dfs(graph: number[][], start: number): number[] { +function generateVisualizationData() { + const graph: number[][] = [ + [1, 2], // 0 + [0, 3], // 1 + [0], // 2 + [1, 4], // 3 + [3, 5], // 4 + [4] // 5 + ]; + + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { typescript: [], python: [], java: [], cpp: [] }; const visited = new Set(); const result: number[] = []; + const recursionStack: number[] = []; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + const pushStep = ( + msg: string, + pseudo: string, + ts: number, + py: number, + java: number, + cpp: number, + current: number | null, + neighbor: number | null = null + ) => { + steps.push({ + currentNode: current, + neighbor, + visited: Array.from(visited), + recursionStack: [...recursionStack], + result: [...result], + explanation: msg, + pseudoStep: pseudo, + variables: { + node: current !== null ? String(current) : '-', + neighbor: neighbor !== null ? String(neighbor) : 'none', + visited: `{${Array.from(visited).join(', ')}}`, + stack: `[${recursionStack.join(', ')}]`, + result: `[${result.join(', ')}]` + } + }); + addLines(ts, py, java, cpp); + }; + + // Init + pushStep( + 'Initialize visited set and result array.', + 'SET visited = {}, result = []', + 2, 2, 11, 19, + null + ); + + const explore = (node: number) => { + recursionStack.push(node); + pushStep( + `Enter explore function for node ${node}. Push to recursion stack.`, + `CALL explore(${node})`, + 4, 4, 2, 3, + node + ); - function explore(node: number) { visited.add(node); + pushStep( + `Mark node ${node} as visited.`, + `visited.add(${node})`, + 5, 5, 3, 4, + node + ); + result.push(node); + pushStep( + `Add node ${node} to traversal result order.`, + `result.push(${node})`, + 6, 6, 4, 5, + node + ); + + const neighbors = graph[node] || []; + pushStep( + `Iterating over neighbors of node ${node}: [${neighbors.join(', ')}].`, + `FOR neighbor IN neighbors of ${node}`, + 7, 7, 5, 6, + node + ); + + for (const neighbor of neighbors) { + pushStep( + `Checking if neighbor ${neighbor} is already visited.`, + `IF neighbor ${neighbor} NOT IN visited`, + 8, 8, 6, 7, + node, neighbor + ); - for (const neighbor of graph[node]) { if (!visited.has(neighbor)) { + pushStep( + `Neighbor ${neighbor} not visited. Recursing explore(${neighbor}).`, + `explore(${neighbor})`, + 9, 9, 7, 8, + node, neighbor + ); explore(neighbor); + + pushStep( + `Returned from explore(${neighbor}) to node ${node}.`, + `Returned to explore(${node})`, + 10, 10, 8, 9, + node + ); + } else { + pushStep( + `Neighbor ${neighbor} already visited. Skip.`, + `SKIP neighbor ${neighbor}`, + 8, 8, 6, 7, + node, neighbor + ); } } - } - - explore(start); - return result; -}`; - - const generateSteps = () => { - const graph: number[][] = [ - [1, 2], // 0 - [0, 3, 4], // 1 - [0, 5], // 2 - [1], // 3 - [1], // 4 - [2] // 5 - ]; - - const newSteps: Step[] = []; - const visited = new Set(); - const result: number[] = []; - const recursionStack: number[] = []; - - const pushStep = (msg: string, line: number, current: number | null, neighbor: number | null = null) => { - newSteps.push({ - currentNode: current, - neighbor, - visited: Array.from(visited), - recursionStack: [...recursionStack], - message: msg, - lineNumber: line - }); - }; - - // Initialize - pushStep('Initialize visited set and result array.', 2, null); - - const explore = (node: number) => { - recursionStack.push(node); - - // Line 5: Enter explore - pushStep(`Entering explore function for node ${node}.`, 5, node); - - // Line 6: visited.add - visited.add(node); - pushStep(`Marking node ${node} as visited.`, 6, node); - - // Line 7: result.push - result.push(node); - pushStep(`Adding node ${node} to the result traversal.`, 7, node); - - // Line 9: for loop - const neighbors = graph[node] || []; - pushStep(`Iterating over neighbors of node ${node}: [${neighbors.join(', ')}].`, 9, node); - - for (const neighbor of neighbors) { - // Line 10: if (!visited.has) - pushStep(`Checking if neighbor ${neighbor} is visited.`, 10, node, neighbor); - - if (!visited.has(neighbor)) { - // Line 11: explore(neighbor) - pushStep(`Neighbor ${neighbor} not visited. Recursing into explore(${neighbor}).`, 11, node, neighbor); - explore(neighbor); - - // Line 12: End of if (returned from recursion) - pushStep(`Returned from explore(${neighbor}) to node ${node}.`, 12, node); - } else { - pushStep(`Neighbor ${neighbor} already visited. Skipping.`, 10, node, neighbor); - } - } - // Line 14: End of explore - pushStep(`Finished exploring node ${node}. Backtracking...`, 14, node); - recursionStack.pop(); - }; + pushStep( + `Finished exploring node ${node}. Backtrack and pop from stack.`, + `BACKTRACK: Pop ${node}`, + 11, 10, 10, 11, + node + ); + recursionStack.pop(); + }; - // Line 16: explore(start) - pushStep("Starting DFS traversal from initial node 0.", 16, null); - explore(0); + pushStep( + 'Starting DFS traversal from initial node 0.', + 'CALL explore(start_node → 0)', + 13, 11, 13, 21, + null + ); + explore(0); - // Line 17: return result - pushStep(`DFS complete! Final visit order: ${result.join(', ')}.`, 17, null); + pushStep( + `DFS complete! Return final visited order: [${result.join(', ')}].`, + `RETURN result → [${result.join(', ')}]`, + 14, 12, 14, 22, + null + ); - setSteps(newSteps); - setCurrentStepIndex(0); - }; + return { steps, stepLineNumbers }; +} - useEffect(() => { - generateSteps(); - }, []); +export const GraphDFSVisualization = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { intervalRef.current = setInterval(() => { - setCurrentStepIndex((prev) => { - if (prev >= steps.length - 1) { - setIsPlaying(false); - return prev; - } + setCurrentStepIndex(prev => { + if (prev >= steps.length - 1) { setIsPlaying(false); return prev; } return prev + 1; }); - }, 1000 / (speed / 1000)); // speed is likely mapped differently in StepControls, but I'll maintain responsiveness + }, 1000 / speed); } else { if (intervalRef.current) clearInterval(intervalRef.current); } - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - }; + return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [isPlaying, currentStepIndex, steps.length, speed]); const handlePlay = () => setIsPlaying(true); const handlePause = () => setIsPlaying(false); - const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(prev => prev + 1); - const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(prev => prev - 1); - const handleReset = () => { - setCurrentStepIndex(0); - setIsPlaying(false); - generateSteps(); - }; + const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(p => p + 1); + const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(p => p - 1); + const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); }; if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); + + const getHighlightEdges = (stack: number[], current: number | null, neighbor: number | null) => { + const activeEdges = new Set(); + for (let i = 0; i < stack.length - 1; i++) { + const u = stack[i]; + const v = stack[i + 1]; + activeEdges.add(`${u}-${v}`); + activeEdges.add(`${v}-${u}`); + } + if (current !== null && neighbor !== null) { + activeEdges.add(`${current}-${neighbor}`); + activeEdges.add(`${neighbor}-${current}`); + } + return activeEdges; + }; return (
@@ -165,23 +290,23 @@ export const GraphDFSVisualization: React.FC = () => { onStepForward={handleStepForward} onStepBack={handleStepBack} onReset={handleReset} - speed={speed / 1000} - onSpeedChange={(s) => setSpeed(s * 1000)} + speed={speed} + onSpeedChange={setSpeed} currentStep={currentStepIndex} totalSteps={steps.length - 1} />
-
+
- -
+
Current Node @@ -193,44 +318,19 @@ export const GraphDFSVisualization: React.FC = () => {
-
-

"{currentStep.message}"

+
+

{currentStep.explanation}

-
-
-

Local Variables

- -
-
-

Recursion Stack

-
- {currentStep.recursionStack.length === 0 ? ( -

Stack empty

- ) : ( -
- {currentStep.recursionStack.map((node, i) => ( -
- explore({node}) -
- ))} -
- )} -
-
-
+
-
diff --git a/src/components/visualizations/algorithms/GraphValidTreeVisualization.tsx b/src/components/visualizations/algorithms/GraphValidTreeVisualization.tsx index e64a2dd..a093dd9 100644 --- a/src/components/visualizations/algorithms/GraphValidTreeVisualization.tsx +++ b/src/components/visualizations/algorithms/GraphValidTreeVisualization.tsx @@ -1,10 +1,10 @@ -import { useState, useMemo } from 'react'; +import React, { useState, useMemo } from 'react'; import { Card } from '@/components/ui/card'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; -import { motion } from 'framer-motion'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; import { CheckCircle2, Info, ArrowRight } from 'lucide-react'; interface Step { @@ -18,46 +18,118 @@ interface Step { u: number | null; v: number | null; result: boolean | null; - message: string; - lineNumber: number; + explanation: string; isMatch?: boolean; + pseudoStep: string; } -export const GraphValidTreeVisualization = () => { - const [currentStepIndex, setCurrentStepIndex] = useState(0); - - const code = `function validTree(n: number, edges: number[][]): boolean { - if (edges.length !== n - 1) return false; - - const graph: Map = new Map(); - for (let i = 0; i < n; i++) { - graph.set(i, []); - } - +const languages: VisualizationLanguageMap = { + typescript: `function validTree(n: number, edges: number[][]): boolean { + if (n === 0) return true; + const adj: Map = new Map(); + for (let i = 0; i < n; i++) adj.set(i, []); for (const [u, v] of edges) { - graph.get(u)!.push(v); - graph.get(v)!.push(u); + adj.get(u)!.push(v); + adj.get(v)!.push(u); } - - const visited: Set = new Set(); - - function dfs(node: number, parent: number): void { - visited.add(node); - - for (const neighbor of graph.get(node)!) { + const visit = new Set(); + function dfs(node: number, parent: number): boolean { + if (visit.has(node)) return false; + visit.add(node); + for (const neighbor of adj.get(node)!) { if (neighbor === parent) continue; - if (visited.has(neighbor)) return; - dfs(neighbor, node); + if (!dfs(neighbor, node)) return false; } + return true; } - - dfs(0, -1); - - return visited.size === n; -}`; + return dfs(0, -1) && n === visit.size; +}`, + + python: `def validTree(n: int, edges: list[list[int]]) -> bool: + if not n: return True + adj = {i: [] for i in range(n)} + for n1, n2 in edges: + adj[n1].append(n2) + adj[n2].append(n1) + visit = set() + def dfs(node: int, prev: int) -> bool: + if node in visit: return False + visit.add(node) + for neighbor in adj[node]: + if neighbor == prev: continue + if not dfs(neighbor, node): return False + return True + return dfs(0, -1) and n == len(visit)`, + + java: `public static class Solution { + public boolean validTree(int n, int[][] edges) { + if (n == 0) return true; + List> adj = new ArrayList<>(); + for (int i = 0; i < n; i++) adj.add(new ArrayList<>()); + for (int[] edge : edges) { + adj.get(edge[0]).add(edge[1]); + adj.get(edge[1]).add(edge[0]); + } + Set visited = new HashSet<>(); + if (!dfs(0, -1, adj, visited)) return false; + return visited.size() == n; + } + private boolean dfs(int node, int parent, List> adj, Set visited) { + if (visited.contains(node)) return false; + visited.add(node); + for (int neighbor : adj.get(node)) { + if (neighbor == parent) continue; + if (!dfs(neighbor, node, adj, visited)) return false; + } + return true; + } +}`, + + cpp: `class Solution { +public: + bool validTree(int n, vector>& edges) { + if (n == 0) return true; + vector> adj(n); + for (const auto& edge : edges) { + adj[edge[0]].push_back(edge[1]); + adj[edge[1]].push_back(edge[0]); + } + unordered_set visited; + if (!dfs(0, -1, adj, visited)) return false; + return visited.size() == n; + } +private: + bool dfs(int node, int parent, vector>& adj, unordered_set& visited) { + if (visited.count(node)) return false; + visited.insert(node); + for (int neighbor : adj[node]) { + if (neighbor == parent) continue; + if (!dfs(neighbor, node, adj, visited)) return false; + } + return true; + } +};` +}; - const steps: Step[] = useMemo(() => { +export const GraphValidTreeVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + + const { steps, stepLineNumbers } = useMemo(() => { const s: Step[] = []; + const stepLines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLines.typescript!.push(ts); + stepLines.python!.push(py); + stepLines.java!.push(java); + stepLines.cpp!.push(cpp); + }; + const n = 5; const edges = [[0, 1], [0, 2], [0, 3], [1, 4]]; @@ -65,10 +137,19 @@ export const GraphValidTreeVisualization = () => { const visitedSet: Set = new Set(); let finalResult: boolean | null = null; - const snap = ( - msg: string, line: number, isMatch: boolean = false, - currentNode: number | null = null, parent: number | null = null, neighbor: number | null = null, - u: number | null = null, v: number | null = null + const makeSnapshot = ( + msg: string, + pseudo: string, + ts: number, + py: number, + java: number, + cpp: number, + isMatch: boolean = false, + currentNode: number | null = null, + parent: number | null = null, + neighbor: number | null = null, + u: number | null = null, + v: number | null = null ) => { s.push({ n, @@ -77,183 +158,253 @@ export const GraphValidTreeVisualization = () => { visited: Array.from(visitedSet), currentNode, parent, neighbor, u, v, result: finalResult, - message: msg, - lineNumber: line, - isMatch + explanation: msg, + isMatch, + pseudoStep: pseudo }); + addLines(ts, py, java, cpp); }; - snap(`Begin validation. A valid tree must be connected and acyclic.`, 1, false); - - if (edges.length !== n - 1) { - finalResult = false; - snap(`Edge count (${edges.length}) must strictly equal n-1 (${n-1}) to be a valid tree.`, 2, true); - return s; - } - snap(`Edge count strictly verifies n-1 condition (${edges.length} === ${n-1}). Valid!`, 2, true); + // Step 1: Start + makeSnapshot("Start Graph Valid Tree validation. A valid tree must be connected and acyclic.", "START validTree(n, edges)", 1, 1, 2, 3); + makeSnapshot("Check base case: if nodes count is 0, return true.", "IF n == 0 → NO ✗", 2, 2, 3, 4); - snap(`Initialize Adjacency List for tracking graph bidirectional mappings.`, 4, false); + // Step 3: Initialize adjacency list for (let i = 0; i < n; i++) { graphInstance.set(i, []); - snap(`Initialized node ${i} row to empty array.`, 6, false); } + makeSnapshot("Initialize adjacency list to represent nodes 0 to n-1.", "FOR i = 0 to n-1 → adj[i] = []", 4, 3, 5, 5); - snap(`Looping through defined edges matrix to establish 2-way associations.`, 9, false); + // Step 4: Populate edges for (const [u, v] of edges) { graphInstance.get(u)!.push(v); graphInstance.get(v)!.push(u); - snap(`Mapped bidirection Edge ${u} <-> ${v}`, 11, false, null, null, null, u, v); } + makeSnapshot( + `Populate adjacency list with bidirectional edges.`, + `FOR [u, v] in edges → adj[u].add(v), adj[v].add(u)`, + 5, 4, 6, 6, true + ); - snap(`Instantiate Visited tracking set memory bounds.`, 14, false); + // Step 5: Visited set + makeSnapshot("Initialize visited set to keep track of visited nodes.", "SET visit = {}", 9, 7, 10, 10); function dfsSim(node: number, parent: number): boolean { - snap(`Invoked DFS search recursively handling Node [${node}]. Source Parent bounds tracking [${parent}].`, 16, false, node, parent); - + const alreadyVisited = visitedSet.has(node); + makeSnapshot( + `DFS: check if Node ${node} is already visited.`, + `IF node ${node} in visit → ${alreadyVisited ? "YES ✓" : "NO ✗"}`, + 11, 9, 15, 16, false, node, parent + ); + + if (alreadyVisited) { + makeSnapshot( + `DFS: Node ${node} has already been visited. Cycle detected! Return false.`, + "RETURN false", + 11, 9, 15, 16, true, node, parent + ); + return false; + } + visitedSet.add(node); - snap(`Tracked Node ${node} globally as visited preventing infinite cyclic recursion!`, 17, true, node, parent); + makeSnapshot( + `DFS: Mark Node ${node} as visited.`, + `visit.add(${node})`, + 12, 10, 16, 17, true, node, parent + ); + + const neighbors = graphInstance.get(node) || []; + for (const neighbor of neighbors) { + makeSnapshot( + `DFS: Process neighbor ${neighbor} of Node ${node}.`, + `FOR neighbor of ${node} → neighbor = ${neighbor}`, + 13, 11, 17, 18, false, node, parent, neighbor + ); - const neighbors = graphInstance.get(node)!; - for (let numIdx = 0; numIdx < neighbors.length; numIdx++) { - const neighbor = neighbors[numIdx]; - snap(`Scanning downstream neighbor connection: [${neighbor}] extending from source Node [${node}].`, 20, false, node, parent, neighbor); - if (neighbor === parent) { - snap(`Neighbor ${neighbor} is our incoming direction Parent vertex. Bypassing recursion.`, 20, true, node, parent, neighbor); + makeSnapshot( + `DFS: Neighbor ${neighbor} is the incoming parent node of ${node}. Skip it.`, + `IF neighbor == parent → ${neighbor} == ${parent} → Skip`, + 14, 12, 18, 19, false, node, parent, neighbor + ); continue; } - if (visitedSet.has(neighbor)) { - snap(`DANGER: Visited cyclic loop overlap detected at ${neighbor}! Short circuit returning false!`, 21, true, node, parent, neighbor); - return false; - } - - snap(`Neighbor ${neighbor} natively verified as safe. Proceeding down branch.`, 22, false, node, parent, neighbor); + makeSnapshot( + `DFS: Recursively call DFS for neighbor Node ${neighbor}.`, + `CALL dfs(${neighbor}, ${node})`, + 15, 13, 19, 20, false, node, parent, neighbor + ); const res = dfsSim(neighbor, node); if (!res) return false; } - - snap(`Terminating leaf processing bounds for ${node}. Branch successful!`, 23, true, node, parent); + + makeSnapshot(`DFS: Finished visiting Node ${node} and all its subtrees. Return true.`, "RETURN true", 17, 14, 21, 22, true, node, parent); return true; } - snap(`Commence root branch DFS targeting vertex [0]. Initiating cyclic tracing.`, 26, false); - dfsSim(0, -1); + makeSnapshot("Start DFS traversal from root Node 0, with no parent (-1).", "CALL dfs(0, -1)", 19, 15, 11, 11); + const dfsResult = dfsSim(0, -1); - finalResult = visitedSet.size === n; - snap(`Final check: Traversed bounds mapping Size ${visitedSet.size} compared against Graph total nodes ${n}. Evaluation: ${finalResult}.`, 28, true); + finalResult = dfsResult && visitedSet.size === n; + makeSnapshot( + `Check if DFS found no cycles (${dfsResult}) and all nodes were connected (${visitedSet.size} visited nodes == total ${n}). Result: ${finalResult}.`, + `RETURN dfs(0, -1) AND size == n → ${finalResult}`, + 19, 15, 12, 12, true + ); - return s; + return { steps: s, stepLineNumbers: stepLines }; }, []); + const handleReset = () => { + setCurrentStepIndex(0); + }; + const step = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( - -

- Graph Topology & Processing +
+ +

+ Graph Topology & Visited Memory

-
- Set Memory (Visited) -
- {Array.from({ length: step.n }).map((_, i) => { - const isVisited = step.visited.includes(i); - return ( -
- {i} -
- ); - })} -
+
+ Visited +
+ {Array.from({ length: step.n }).map((_, i) => { + const isVisited = step.visited.includes(i); + const isCurrent = step.currentNode === i; + return ( +
+ {i} +
+ ); + })}
+
-
- Associations List (Adj) -
- {Object.entries(step.graph).map(([nodeStr, neighbors]) => { - const node = parseInt(nodeStr); - const isActiveNode = step.currentNode === node; +
+ Adjacency List (Adj) +
+ {Object.entries(step.graph).map(([nodeStr, neighbors]) => { + const node = parseInt(nodeStr); + const isActiveNode = step.currentNode === node; + return ( +
+
+ {node} +
+ +
+ {neighbors.length === 0 && Empty} + {neighbors.map((n, idx) => { + const isTargetNeighbor = isActiveNode && step.neighbor === n; return ( -
-
- {node} -
- -
- {neighbors.length === 0 && Empty} - {neighbors.map((n, idx) => { - const isTargetNeighbor = isActiveNode && step.neighbor === n; - return ( -
- {n} -
- ); - })} -
-
+
+ {n} +
); - })} -
+ })} +
+
+ ); + })}
+
- -
- Edges Matrix Check -
- {step.edges.map(([u, v], idx) => { - const isCurrentPair = step.u === u && step.v === v; - return ( -
- [{u},{v}] -
- ) - })} -
-
+ +
+ Edges Matrix +
+ {step.edges.map(([u, v], idx) => { + const isCurrentPair = step.u === u && step.v === v; + return ( +
+ [{u},{v}] +
+ ) + })} +
+
- -
-
- {step?.isMatch ? : } + {/* Commentary Panel */} + +
+
+
+ + + + + + Algorithm Commentary + +
+
+ Step {currentStepIndex + 1} of {steps.length} +
-
-

- Algorithm Tracker -

-

- {step?.message || ''} -

+ +
+
+ {step?.isMatch ? : } +
+
+

+ Current Action +

+
+ {step?.explanation || ''} +
+
-
- } - rightContent={ -
- +
} + rightContent={ + + } controls={ ; result?: string[][]; - message: string; - highlightedLines: number[]; + explanation: string; + pseudoStep: string; } -export const GroupAnagramsVisualization = () => { - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1000); - const intervalRef = useRef(null); - - const cases = [ - { name: "Default Case", strs: ["eat", "tea", "tan", "ate", "nat", "bat"], icon: }, - { name: "Empty Strings", strs: ["", "", ""], icon: } - ]; - const [selectedCaseIndex, setSelectedCaseIndex] = useState(0); - - const code = `function groupAnagrams(strs: string[]): string[][] { +const USE_CASES = [ + { name: "Default Case", strs: ["eat", "tea", "tan", "ate", "nat", "bat"], icon: }, + { name: "Empty Strings", strs: ["", "", ""], icon: } +]; + +const languages: VisualizationLanguageMap = { + python: `from collections import defaultdict + +def group_anagrams(strs: list[str]) -> list[list[str]]: + anagram_groups = defaultdict(list) + for s in strs: + count = [0] * 26 + for char in s: + count[ord(char) - ord('a')] += 1 + key = tuple(count) + anagram_groups[key].append(s) + return list(anagram_groups.values())`, + + typescript: `function groupAnagrams(strs: string[]): string[][] { const map = new Map(); - for (const s of strs) { const count = new Array(26).fill(0); - for (const c of s) { const index = c.charCodeAt(0) - 'a'.charCodeAt(0); count[index]++; } - let key = ""; for (const num of count) { key += num + "#"; } - if (!map.has(key)) { map.set(key, []); } map.get(key)!.push(s); } - return Array.from(map.values()); -}`; +}`, + + java: `public class Solution { + public List> groupAnagrams(String[] strs) { + Map> map = new HashMap<>(); + for (String s : strs) { + int[] count = new int[26]; + for (char c : s.toCharArray()) { + int index = c - 'a'; + count[index]++; + } + StringBuilder keyBuilder = new StringBuilder(); + for (int num : count) { + keyBuilder.append(num).append("#"); + } + String key = keyBuilder.toString(); + map.putIfAbsent(key, new ArrayList<>()); + map.get(key).add(s); + } + return new ArrayList<>(map.values()); + } +}`, + + cpp: `class Solution { +public: + vector> groupAnagrams(vector& strs) { + unordered_map> anagrams; + for (const string& s : strs) { + string key = s; + sort(key.begin(), key.end()); + anagrams[key].push_back(s); + } + vector> result; + for (auto& [key, group] : anagrams) { + result.push_back(group); + } + return result; + } +};` +}; - const generateSteps = (strs: string[]) => { - const steps: Step[] = []; - const map: Record = {}; +const generateSteps = (strs: string[]) => { + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - steps.push({ - strs, - map: { ...map }, - message: "Initialize an empty Map to store anagram groups.", - highlightedLines: [2] - }); + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - for (let i = 0; i < strs.length; i++) { - const s = strs[i]; - const count = new Array(26).fill(0); - - steps.push({ - strs, - currentStr: s, - currentIndex: i, - map: { ...map }, - message: `Process string: '${s}' at index ${i}.`, - highlightedLines: [4] - }); - - steps.push({ - strs, - currentStr: s, - currentIndex: i, - count: [...count], - map: { ...map }, - message: "Initialize frequency array with 26 zeros.", - highlightedLines: [5] - }); - - steps.push({ - strs, - currentStr: s, - currentIndex: i, - count: [...count], - map: { ...map }, - message: `Iterate through characters in '${s}'.`, - highlightedLines: [7] - }); - - for (const c of s) { - const charIdx = c.charCodeAt(0) - 'a'.charCodeAt(0); - count[charIdx]++; - steps.push({ - strs, - currentStr: s, - currentIndex: i, - count: [...count], - map: { ...map }, - message: `Update count for '${c}' at index ${charIdx}.`, - highlightedLines: [8, 9] - }); - } - - let key = ""; - steps.push({ - strs, - currentStr: s, - currentIndex: i, - count: [...count], - key: "", - map: { ...map }, - message: "Initialize an empty key string.", - highlightedLines: [12] - }); - - for (const num of count) { - key += num + "#"; - } - - steps.push({ - strs, - currentStr: s, - currentIndex: i, - count: [...count], - key, - map: { ...map }, - message: `Constructed key signature: '${key.substring(0, 15)}...'`, - highlightedLines: [13, 14, 15] - }); - - steps.push({ - strs, - currentStr: s, - currentIndex: i, - key, - map: { ...map }, - message: `Check if map contains key: '${key.substring(0, 10)}...'`, - highlightedLines: [17] - }); - - if (!map[key]) { - map[key] = []; - steps.push({ - strs, - currentStr: s, - currentIndex: i, - key, - map: JSON.parse(JSON.stringify(map)), - message: "Key not found. Create a new entry.", - highlightedLines: [18] - }); - } - - map[key].push(s); - steps.push({ - strs, - currentStr: s, - currentIndex: i, - key, - map: JSON.parse(JSON.stringify(map)), - message: `Add '${s}' to the group.`, - highlightedLines: [20] - }); - } + const map: Record = {}; + const addStep = (msg: string, pseudo: string, tsLine: number, pyLine: number, javaLine: number, cppLine: number, extra: Partial = {}) => { steps.push({ strs, map: JSON.parse(JSON.stringify(map)), - result: Object.values(map), - message: "Return all anagram groups as an array of arrays.", - highlightedLines: [23] + currentStr: extra.currentStr, + currentIndex: extra.currentIndex, + count: extra.count, + key: extra.key, + result: extra.result, + explanation: msg, + pseudoStep: pseudo }); - - return steps; + addLines(tsLine, pyLine, javaLine, cppLine); }; - const [steps, setSteps] = useState([]); + // 1. Init Map + addStep("Initialize map to store anagram groups.", "SET map = {}", 2, 4, 3, 4); - useEffect(() => { - setSteps(generateSteps(cases[selectedCaseIndex].strs)); - setCurrentStepIndex(0); - setIsPlaying(false); - }, [selectedCaseIndex]); - - useEffect(() => { - if (isPlaying && currentStepIndex < steps.length - 1) { - intervalRef.current = window.setInterval(() => { - setCurrentStepIndex((prev) => { - if (prev >= steps.length - 1) { - setIsPlaying(false); - return prev; - } - return prev + 1; - }); - }, speed); - } else if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; + for (let i = 0; i < strs.length; i++) { + const s = strs[i]; + const count = new Array(26).fill(0); + + // 2. Loop start + addStep(`Process string "${s}" at index ${i}.`, `FOR s IN strs [i = ${i}]`, 3, 5, 4, 5, { currentStr: s, currentIndex: i }); + + // 3. Init count + addStep("Initialize character frequency array count (size 26).", "SET count = [0] * 26", 4, 6, 5, 6, { currentStr: s, currentIndex: i, count: [...count] }); + + for (const c of s) { + const charIdx = c.charCodeAt(0) - 'a'.charCodeAt(0); + count[charIdx]++; + } + // 4. Counting complete step + addStep(`Populate character counts for "${s}".`, `FOR char IN s`, 5, 7, 6, 5, { currentStr: s, currentIndex: i, count: [...count] }); + + let key = ""; + for (const num of count) { + key += num + "#"; } - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - }; - }, [isPlaying, currentStepIndex, steps.length, speed]); + // 5. Construct key signature + addStep(`Constructed key signature: "${key.substring(0, 10)}..."`, `SET key = tuple(count)`, 9, 9, 10, 6, { currentStr: s, currentIndex: i, count: [...count], key }); + + // 6. Check existence + addStep(`Check map for key: "${key.substring(0, 8)}..."`, `IF key NOT IN map`, 13, 10, 15, 8, { currentStr: s, currentIndex: i, count: [...count], key }); + + if (!map[key]) { + map[key] = []; + addStep("Key not found in map. Initialize new anagram group list.", "SET map[key] = []", 14, 10, 15, 8, { currentStr: s, currentIndex: i, count: [...count], key }); + } + + map[key].push(s); + addStep(`Append "${s}" to group associated with key.`, "map[key].append(s)", 16, 10, 16, 8, { currentStr: s, currentIndex: i, count: [...count], key }); + } + + // 7. Return complete + addStep("Extraction complete. Return all grouped anagram lists.", "RETURN map.values()", 18, 11, 18, 14, { result: Object.values(map) }); + + return { steps, stepLineNumbers }; +}; + +export const GroupAnagramsVisualization = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [selectedCaseIndex, setSelectedCaseIndex] = useState(0); + const selectedCase = USE_CASES[selectedCaseIndex]; + + const { steps, stepLineNumbers } = useMemo(() => { + return generateSteps(selectedCase.strs); + }, [selectedCase]); + + if (steps.length === 0) return null; + + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); const handleCaseChange = (index: number) => { - if (index === selectedCaseIndex) return; setSelectedCaseIndex(index); + setCurrentStepIndex(0); }; - if (steps.length === 0) return null; - const step = steps[currentStepIndex]; - return (
-
- setIsPlaying(true)} - onPause={() => setIsPlaying(false)} - onStepForward={() => setCurrentStepIndex(prev => Math.min(steps.length - 1, prev + 1))} - onStepBack={() => setCurrentStepIndex(prev => Math.max(0, prev - 1))} - onReset={() => { setCurrentStepIndex(0); setIsPlaying(false); }} - speed={speed} - onSpeedChange={setSpeed} - /> - + {/* Case selections / Controls at Top */} +
- {cases.map((testCase, idx) => ( + {USE_CASES.map((testCase, idx) => ( ))}
+
+ +
-
-
- - {/* Input Array */} -
-

Input Strings

-
- {step.strs.map((s, idx) => ( + + + {/* Input Array */} +
+

Input Strings

+
+ {currentStep.strs.map((s, idx) => ( + + "{s}" + + ))} +
+
+ + {/* Count Array */} + + {currentStep.count && ( - "{s}" +

+ Frequency Count for "{currentStep.currentStr}" +

+
+ {currentStep.count.map((count, idx) => { + const char = String.fromCharCode(97 + idx); + if (count === 0 && !['a', 'e', 't', 'n', 'b'].includes(char)) return null; + return ( +
0 ? 'bg-primary/10 border-primary/30' : 'bg-muted/30 border-transparent' + }`} + > + {char} + 0 ? 'text-primary' : 'text-muted-foreground/50'}`}> + {count} + +
+ ); + })} +
- ))} + )} +
+ + {/* Map Visualization */} +
+

Anagram Map

+
+ + {Object.keys(currentStep.map).length === 0 ? ( +

Initializing map...

+ ) : ( + Object.entries(currentStep.map).map(([key, group]) => ( + +
+ {key.substring(0, 8)}... +
+
+ {group.map((s, sIdx) => ( + + "{s}" + + ))} +
+
+ )) + )} +
+
-
- {/* Count Array */} - - {step.count && ( + {/* Final Result */} + {currentStep.result && ( -

- Frequency Count for "{step.currentStr}" +

+
+ Final Result

-
- {step.count.map((count, idx) => { - const char = String.fromCharCode(97 + idx); - if (count === 0 && !['a', 'e', 't', 'n', 'b'].includes(char)) return null; - return ( -
0 ? 'bg-primary/10 border-primary/30' : 'bg-muted/30 border-transparent' - }`} - > - {char} - 0 ? 'text-primary' : 'text-muted-foreground/50'}`}> - {count} - +
+ {currentStep.result.map((group, idx) => ( +
+ #{idx + 1} +
+ [{group.map(s => `"${s}"`).join(', ')}]
- ); - })} +
+ ))}
)} - - - {/* Map Visualization */} -
-

Anagram Map

-
- - {Object.keys(step.map).length === 0 ? ( -

Initializing map...

- ) : ( - Object.entries(step.map).map(([key, group], idx) => ( - -
- {key.substring(0, 8)}... -
-
- {group.map((s, sIdx) => ( - - "{s}" - - ))} -
-
- )) - )} -
+ + + {/* Descriptive Commentary Box (at the bottom) */} +
+
+
+ Process Step
+ {currentStep.explanation}
- {/* Final Result */} - {step.result && ( - -

- - Final Result -

-
- {step.result.map((group, idx) => ( -
- #{idx + 1} -
- [{group.map(s => `"${s}"`).join(', ')}] -
-
- ))} -
-
- )} - - - {/* Commentary Above VariablePanel */} -
-

- {step.message} -

+ {/* Variable Panel (below the commentary box) */} +
+ +
- - setCurrentStepIndex(0)} /> -
- - -
+ } + />
); }; diff --git a/src/components/visualizations/algorithms/HouseRobberIIVisualization.tsx b/src/components/visualizations/algorithms/HouseRobberIIVisualization.tsx index 447ab1c..e5abd1a 100644 --- a/src/components/visualizations/algorithms/HouseRobberIIVisualization.tsx +++ b/src/components/visualizations/algorithms/HouseRobberIIVisualization.tsx @@ -2,8 +2,9 @@ import React, { useState, useMemo } from 'react'; import { Card } from '@/components/ui/card'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { nums: number[]; @@ -16,16 +17,12 @@ interface Step { caseName: string; variables: Record; explanation: string; - lineExecution: string; - highlightedLines: number[]; + pseudoStep: string; + calc?: string; } -export const HouseRobberIIVisualization: React.FC = () => { - const [currentStep, setCurrentStep] = useState(0); - - const nums = [4, 2, 3, 1]; - - const code = `function rob(nums: number[]): number { +const languages: VisualizationLanguageMap = { + typescript: `function rob(nums: number[]): number { if (nums.length === 1) return nums[0]; const robLinear = (start: number, end: number): number => { let rob1 = 0; @@ -40,12 +37,83 @@ export const HouseRobberIIVisualization: React.FC = () => { const case1 = robLinear(0, nums.length - 2); const case2 = robLinear(1, nums.length - 1); return Math.max(case1, case2); -}`; +}`, + python: `def rob(nums): + if len(nums) == 1: + return nums[0] + def rob_linear(start, end): + rob1 = 0 + rob2 = 0 + for i in range(start, end + 1): + temp = max(rob1 + nums[i], rob2) + rob1 = rob2 + rob2 = temp + return rob2 + return max( + rob_linear(0, len(nums) - 2), + rob_linear(1, len(nums) - 1) + )`, + java: `public static class Solution { + public int rob(int[] nums) { + if (nums.length == 1) return nums[0]; + int case1 = robLinear(nums, 0, nums.length - 2); + int case2 = robLinear(nums, 1, nums.length - 1); + return Math.max(case1, case2); + } + private int robLinear(int[] nums, int start, int end) { + int rob1 = 0; + int rob2 = 0; + for (int i = start; i <= end; i++) { + int temp = Math.max(rob1 + nums[i], rob2); + rob1 = rob2; + rob2 = temp; + } + return rob2; + } +}`, + cpp: `class Solution { +public: + int robLinear(vector& nums, int start, int end) { + int rob1 = 0; + int rob2 = 0; + for (int i = start; i <= end; i++) { + int temp = max(rob1 + nums[i], rob2); + rob1 = rob2; + rob2 = temp; + } + return rob2; + } + int rob(vector& nums) { + if (nums.size() == 1) return nums[0]; + int case1 = robLinear(nums, 0, nums.size() - 2); + int case2 = robLinear(nums, 1, nums.size() - 1); + return max(case1, case2); + } +};` +}; + +export const HouseRobberIIVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const nums = [4, 2, 3, 1]; - const steps = useMemo(() => { + const { steps, stepLineNumbers } = useMemo(() => { const stepsList: Step[] = []; const n = nums.length; + const lines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + const addLines = (ts: number, py: number, java: number, cpp: number) => { + lines.typescript!.push(ts); + lines.python!.push(py); + lines.java!.push(java); + lines.cpp!.push(cpp); + }; + + // 1. Initial entry stepsList.push({ nums, i: null, @@ -56,29 +124,12 @@ export const HouseRobberIIVisualization: React.FC = () => { temp: null, caseName: "", variables: { nums: `[${nums.join(', ')}]` }, - explanation: "Function started. Input houses array represents a circular street.", - lineExecution: "function rob(nums: number[]): number {", - highlightedLines: [1] + explanation: "Function started. Since the houses are circular, robbing both the first and last houses is forbidden.", + pseudoStep: "CALL rob(nums)" }); + addLines(1, 1, 2, 13); - if (n === 1) { - stepsList.push({ - nums, - i: 0, - start: null, - end: null, - rob1: null, - rob2: null, - temp: null, - caseName: "", - variables: { length: 1, result: nums[0] }, - explanation: "Only one house exists. Max money is simply the value of this house.", - lineExecution: "if (nums.length === 1) return nums[0];", - highlightedLines: [2] - }); - return stepsList; - } - + // 2. Helper def stepsList.push({ nums, i: null, @@ -89,15 +140,16 @@ export const HouseRobberIIVisualization: React.FC = () => { temp: null, caseName: "", variables: { length: n }, - explanation: "Define a helper function 'robLinear' to calculate the maximum money robbed for a linear subarray of houses.", - lineExecution: "const robLinear = (start: number, end: number): number => {", - highlightedLines: [3] + explanation: "Define a helper method 'robLinear' to rob a linear sequence of houses within a specified range.", + pseudoStep: "DEFINE robLinear(start, end)" }); + addLines(3, 4, 8, 3); const simulateCase = (start: number, end: number, caseLabel: string) => { let rob1 = 0; let rob2 = 0; + // Call helper step stepsList.push({ nums, i: null, @@ -108,11 +160,12 @@ export const HouseRobberIIVisualization: React.FC = () => { temp: null, caseName: caseLabel, variables: { start, end }, - explanation: `Processing ${caseLabel}: Houses in range [${start}, ${end}]. This scenario ignores house ${start === 0 ? n - 1 : 0} to handle the circular constraint.`, - lineExecution: `const ${caseLabel} = robLinear(${start}, nums.length - ${start === 0 ? 2 : 1});`, - highlightedLines: caseLabel === "case1" ? [13] : [14] + explanation: `Start ${caseLabel}: Rob houses from index ${start} to ${end}. This handles the circle constraint by excluding one boundary house.`, + pseudoStep: `CALL robLinear(${start}, ${end})` }); + addLines(caseLabel === "case1" ? 13 : 14, caseLabel === "case1" ? 13 : 14, caseLabel === "case1" ? 4 : 5, caseLabel === "case1" ? 14 : 15); + // Initialize rob1, rob2 stepsList.push({ nums, i: null, @@ -122,14 +175,16 @@ export const HouseRobberIIVisualization: React.FC = () => { rob2: 0, temp: null, caseName: caseLabel, - variables: { rob1: 0, rob2: 0 }, - explanation: "Initialize rob1 = 0 and rob2 = 0 for the linear pass.", - lineExecution: "let rob1 = 0;\nlet rob2 = 0;", - highlightedLines: [4, 5] + variables: { start, end, rob1: 0, rob2: 0 }, + explanation: `Within robLinear(${start}, ${end}): Initialize rob1 = 0 (representing maximum up to i-2) and rob2 = 0 (representing maximum up to i-1).`, + pseudoStep: "SET rob1 = 0, rob2 = 0" }); + addLines(4, 5, 9, 4); for (let i = start; i <= end; i++) { const money = nums[i]; + + // Loop check step stepsList.push({ nums, i, @@ -140,10 +195,10 @@ export const HouseRobberIIVisualization: React.FC = () => { temp: null, caseName: caseLabel, variables: { i, money, rob1, rob2 }, - explanation: `Inner loop iteration for house ${i}. Comparing robbing house ${i} vs. skipping it.`, - lineExecution: "for (let i = start; i <= end; i++) {", - highlightedLines: [6] + explanation: `Loop iteration: i = ${i}, money = $${money}. Decide whether to rob house ${i} or skip it.`, + pseudoStep: `FOR i = ${start} TO ${end} (i = ${i})` }); + addLines(6, 7, 11, 6); const temp = Math.max(rob1 + money, rob2); stepsList.push({ @@ -155,11 +210,12 @@ export const HouseRobberIIVisualization: React.FC = () => { rob2, temp, caseName: caseLabel, - variables: { money, rob1, rob2, "money+rob1": money + rob1, "max": temp }, - explanation: `Calculate temp: max(money + rob1, rob2) = max(${money} + ${rob1}, ${rob2}) = ${temp}.`, - lineExecution: "const temp = Math.max(rob1 + nums[i], rob2);", - highlightedLines: [7] + variables: { i, money, rob1, rob2, 'money + rob1': money + rob1, temp }, + explanation: `Calculate temp: Either rob house ${i} ($${money} + rob1: $${rob1} = $${money + rob1}) or skip it (rob2: $${rob2}). Max is $${temp}.`, + pseudoStep: `SET temp = MAX(rob1 + nums[i], rob2) (${temp})`, + calc: `max(${rob1} + ${money}, ${rob2}) = ${temp}` }); + addLines(7, 8, 12, 7); rob1 = rob2; stepsList.push({ @@ -171,11 +227,11 @@ export const HouseRobberIIVisualization: React.FC = () => { rob2, temp, caseName: caseLabel, - variables: { rob1 }, - explanation: `Shift window: rob1 becomes old rob2 (${rob1}).`, - lineExecution: "rob1 = rob2;", - highlightedLines: [8] + variables: { i, money, rob1, rob2, temp }, + explanation: `Shift window: rob1 becomes rob2 ($${rob1}).`, + pseudoStep: `SET rob1 = rob2 (${rob1})` }); + addLines(8, 9, 13, 8); rob2 = temp; stepsList.push({ @@ -187,13 +243,14 @@ export const HouseRobberIIVisualization: React.FC = () => { rob2, temp, caseName: caseLabel, - variables: { rob2 }, - explanation: `Shift window: rob2 becomes temp (${temp}).`, - lineExecution: "rob2 = temp;", - highlightedLines: [9] + variables: { i, money, rob1, rob2, temp }, + explanation: `Shift window: rob2 becomes temp ($${temp}).`, + pseudoStep: `SET rob2 = temp (${temp})` }); + addLines(9, 10, 14, 9); } + // Return step stepsList.push({ nums, i: null, @@ -204,10 +261,10 @@ export const HouseRobberIIVisualization: React.FC = () => { temp: null, caseName: caseLabel, variables: { [`${caseLabel}_result`]: rob2 }, - explanation: `Pass complete. The maximum for houses in range [${start}, ${end}] is ${rob2}.`, - lineExecution: "return rob2;", - highlightedLines: [11] + explanation: `Completed linear pass for range [${start}, ${end}]. Return rob2 = $${rob2}.`, + pseudoStep: `RETURN rob2 (${rob2})` }); + addLines(11, 11, 16, 11); return rob2; }; @@ -215,6 +272,7 @@ export const HouseRobberIIVisualization: React.FC = () => { const res1 = simulateCase(0, n - 2, "case1"); const res2 = simulateCase(1, n - 1, "case2"); + // Final return step stepsList.push({ nums, i: null, @@ -224,131 +282,145 @@ export const HouseRobberIIVisualization: React.FC = () => { rob2: null, temp: null, caseName: "comparison", - variables: { case1: res1, case2: res2, result: Math.max(res1, res2) }, - explanation: `Finally, return the maximum of Case 1 (${res1}) and Case 2 (${res2}): ${Math.max(res1, res2)}.`, - lineExecution: "return Math.max(case1, case2);", - highlightedLines: [15] + variables: { case1: res1, case2: res2, max: Math.max(res1, res2) }, + explanation: `Return the overall maximum of both cases: max(case1: $${res1}, case2: $${res2}) = $${Math.max(res1, res2)}.`, + pseudoStep: `RETURN MAX(case1, case2) (${Math.max(res1, res2)})`, + calc: `max(${res1}, ${res2}) = ${Math.max(res1, res2)}` }); + addLines(15, 12, 6, 17); - return stepsList; - }, [nums]); + return { steps: stepsList, stepLineNumbers: lines }; + }, []); - const step = steps[currentStep]; + const step = steps[currentStepIndex]; + const pseudoSteps = steps.map((s) => s.pseudoStep); return ( -
-

- House Robber II (Circular) -

+
+
-
- + - {step.caseName ? step.caseName.toUpperCase() : "START"} + {step.caseName ? step.caseName.toUpperCase() : "Circular Setup"}
- -
+ +
{nums.map((money, idx) => { const isInRange = step.start !== null && step.end !== null && idx >= step.start && idx <= step.end; const isCurrent = idx === step.i && step.i !== null; const isExcluded = step.start !== null && !isInRange; - - return ( -
-
- ${money} -
+ return ( +
{isCurrent && ( -
- Robber +
+ 👤
)} + +
+ 🏠 + ${money} +
+ House {idx}
); })}
{step.rob1 !== null && step.rob2 !== null && ( -
-
-

rob1

-
${step.rob1}
+
+
+

+ rob1 (Max up to i-2) +

+
${step.rob1}
-
-

rob2

-
${step.rob2}
+
+

+ rob2 (Max up to i-1) +

+
${step.rob2}
)} + + {step.calc && ( + +

Calculation

+

{step.calc}

+
+ )}
-
- -
-
-

- Current Execution -

-
- {step.lineExecution} -
-
-
-

- Commentary -

-

- {step.explanation} -

-
-
-
+
+ +

Step Explanation

+

+ {step.explanation} +

+
+ + + + +

+ Why this works +

+

+ Since the houses are in a circle, we cannot rob both the first and last houses. +

+

+ This circular problem can be simplified into two linear subproblems (House Robber I): +

+
    +
  • Case 1: Rob houses 0 to n-2 (skipping the last house).
  • +
  • Case 2: Rob houses 1 to n-1 (skipping the first house).
  • +
+

+ Taking the maximum of these two linear cases yields the optimal circular solution. +

+
} rightContent={ -
-
- -
- -
- -
-
+ setCurrentStepIndex(0)} + /> } controls={ } /> ); }; + +export default HouseRobberIIVisualization; diff --git a/src/components/visualizations/algorithms/HouseRobberVisualization.tsx b/src/components/visualizations/algorithms/HouseRobberVisualization.tsx index 20c7a4b..3c9ddfb 100644 --- a/src/components/visualizations/algorithms/HouseRobberVisualization.tsx +++ b/src/components/visualizations/algorithms/HouseRobberVisualization.tsx @@ -2,8 +2,9 @@ import React, { useState, useMemo } from 'react'; import { Card } from '@/components/ui/card'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { nums: number[]; @@ -14,16 +15,12 @@ interface Step { i: number; variables: Record; explanation: string; - lineExecution: string; - highlightedLines: number[]; + pseudoStep: string; + calc?: string; } -export const HouseRobberVisualization: React.FC = () => { - const [currentStep, setCurrentStep] = useState(0); - - const nums = [4, 2, 3, 1]; - - const code = `function rob(nums: number[]): number { +const languages: VisualizationLanguageMap = { + typescript: `function rob(nums: number[]): number { let rob1 = 0; let rob2 = 0; for (const money of nums) { @@ -32,14 +29,66 @@ export const HouseRobberVisualization: React.FC = () => { rob2 = temp; } return rob2; -}`; +}`, + python: `def rob(nums): + rob1, rob2 = 0, 0 + for money in nums: + temp = max(money + rob1, rob2) + rob1 = rob2 + rob2 = temp + return rob2`, + java: `public static class Solution { + public int rob(int[] nums) { + int rob1 = 0; + int rob2 = 0; + for (int money : nums) { + int temp = Math.max(money + rob1, rob2); + rob1 = rob2; + rob2 = temp; + } + return rob2; + } +}`, + cpp: `class Solution { +public: +int rob(vector& nums) { + int rob1 = 0; + int rob2 = 0; + for (int money : nums) { + int temp = max(money + rob1, rob2); + rob1 = rob2; + rob2 = temp; + } + return rob2; +} +};` +}; + +export const HouseRobberVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const nums = [4, 2, 3, 1]; + + const { steps, stepLineNumbers } = useMemo(() => { + const s: Step[] = []; + const lines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + lines.typescript!.push(ts); + lines.python!.push(py); + lines.java!.push(java); + lines.cpp!.push(cpp); + }; - const steps = useMemo(() => { - const stepsList: Step[] = []; let rob1 = 0; let rob2 = 0; - stepsList.push({ + // Initial state + s.push({ nums, rob1: 0, rob2: 0, @@ -47,85 +96,77 @@ export const HouseRobberVisualization: React.FC = () => { temp: null, i: -1, variables: { nums: `[${nums.join(', ')}]` }, - explanation: "Function started. Input houses array received.", - lineExecution: "function rob(nums: number[]): number {", - highlightedLines: [1] - }); - - stepsList.push({ - nums, - rob1: 0, - rob2: 0, - currentMoney: null, - temp: null, - i: -1, - variables: { rob1: 0, rob2: 0 }, - explanation: "Initialize rob1 = 0 (max money found 2 houses back) and rob2 = 0 (max money found up to previous house).", - lineExecution: "let rob1 = 0;\nlet rob2 = 0;", - highlightedLines: [2, 3] + explanation: "Initialize rob1 = 0 (representing the max money 2 houses back) and rob2 = 0 (representing the max money up to the previous house).", + pseudoStep: "SET rob1 = 0, rob2 = 0" }); + addLines(2, 2, 3, 4); for (let i = 0; i < nums.length; i++) { const money = nums[i]; - stepsList.push({ + // Loop check step + s.push({ nums, rob1, rob2, currentMoney: money, temp: null, i, - variables: { money, rob1, rob2 }, - explanation: `Entering loop to process house at index ${i} containing $${money}.`, - lineExecution: "for (const money of nums) {", - highlightedLines: [4] + variables: { money, rob1, rob2, i }, + explanation: `Process house ${i} containing $${money}.`, + pseudoStep: `FOR EACH money IN nums (money = ${money})` }); + addLines(4, 3, 5, 6); + // Math.max calculation const temp = Math.max(money + rob1, rob2); - - stepsList.push({ + s.push({ nums, rob1, rob2, currentMoney: money, temp, i, - variables: { money, rob1, rob2, "money+rob1": money + rob1, "max": temp }, - explanation: `Decide whether to rob house ${i}.\nIf robbed: Current money ($${money}) + rob1 ($${rob1}) = $${money + rob1}.\nIf skipped: Use rob2 ($${rob2}).\nResulting in temp = $${temp}.`, - lineExecution: "const temp = Math.max(money + rob1, rob2);", - highlightedLines: [5] + variables: { money, rob1, rob2, 'money + rob1': money + rob1, temp }, + explanation: `Decide whether to rob house ${i}. Robbing yields: money + rob1 = ${money} + ${rob1} = ${money + rob1}. Skipping yields: rob2 = ${rob2}. Max is ${temp}.`, + pseudoStep: `SET temp = MAX(money + rob1, rob2) (${temp})`, + calc: `max(${money} + ${rob1}, ${rob2}) = ${temp}` }); + addLines(5, 4, 6, 7); + // rob1 = rob2 rob1 = rob2; - stepsList.push({ + s.push({ nums, rob1, rob2, currentMoney: money, temp, i, - variables: { rob1, rob2 }, - explanation: `Update rob1 to rob2 ($${rob2}). This prepares the window for the next iteration.`, - lineExecution: "rob1 = rob2;", - highlightedLines: [6] + variables: { money, rob1, rob2, temp }, + explanation: `Update rob1 to hold the value of rob2 (max money up to previous house): rob1 = ${rob1}.`, + pseudoStep: `SET rob1 = rob2 (${rob1})` }); + addLines(6, 5, 7, 8); + // rob2 = temp rob2 = temp; - stepsList.push({ + s.push({ nums, rob1, rob2, currentMoney: money, temp, i, - variables: { rob1, rob2, temp }, - explanation: `Update rob2 to temp ($${temp}), representing the new maximum value robbable including houses up to this point.`, - lineExecution: "rob2 = temp;", - highlightedLines: [7] + variables: { money, rob1, rob2, temp }, + explanation: `Update rob2 to hold the new maximum sum computed in temp: rob2 = ${rob2}.`, + pseudoStep: `SET rob2 = temp (${rob2})` }); + addLines(7, 6, 8, 9); } - stepsList.push({ + // Return step + s.push({ nums, rob1, rob2, @@ -133,116 +174,130 @@ export const HouseRobberVisualization: React.FC = () => { temp: null, i: nums.length, variables: { result: rob2 }, - explanation: `Finished processing all houses. The maximum amount robbed is stored in rob2: $${rob2}.`, - lineExecution: "return rob2;", - highlightedLines: [10] + explanation: `All houses processed. Return rob2 = $${rob2} as the maximum money that can be robbed without alerting the police.`, + pseudoStep: `RETURN rob2 (${rob2})` }); + addLines(9, 7, 10, 11); - return stepsList; - }, [nums]); + return { steps: s, stepLineNumbers: lines }; + }, []); - const step = steps[currentStep]; + const step = steps[currentStepIndex]; + const pseudoSteps = steps.map((s) => s.pseudoStep); return ( -
-

- House Robber (Bottom-Up) -

+
+
-
+

+ Neighborhood Houses +

+
{nums.map((money, idx) => { const isCurrent = idx === step.i; const isPast = idx < step.i; - + return ( -
-
- ${money} -
- +
{isCurrent && ( -
- Robber +
+ 👤
)} + +
+ 🏠 + ${money} +
+ House {idx}
); })}
-
-
-

rob1 (i-2)

-
${step.rob1}
+
+
+

+ rob1 (Max up to i-2) +

+
${step.rob1}
-
-

rob2 (i-1)

-
${step.rob2}
+
+

+ rob2 (Max up to i-1) +

+
${step.rob2}
+ + {step.calc && ( + +

Calculation

+

{step.calc}

+
+ )}
-
- -
-
-

- Current Execution -

-
- {step.lineExecution} -
-
-
-

- Commentary -

-

- {step.explanation} -

-
-
-
+
+ +

Step Explanation

+

+ {step.explanation} +

+
+ + + + +

+ Why this works +

+

+ For each house, the robber can choose to rob it or skip it. +

+

+ Robbing the current house means adding its money to the maximum money robbed from houses up to `i - 2` (`rob1`). +

+

+ Skipping means carrying over the maximum money robbed from houses up to `i - 1` (`rob2`). +

+

+ We keep track of these two values using rolling variables `rob1` and `rob2` to achieve O(N) time and O(1) space complexity. +

+
} rightContent={ -
-
- -
- -
- -
-
+ setCurrentStepIndex(0)} + /> } controls={ } /> ); }; + +export default HouseRobberVisualization; diff --git a/src/components/visualizations/algorithms/HuffmanCodingVisualization.tsx b/src/components/visualizations/algorithms/HuffmanCodingVisualization.tsx index 8586540..aafe1a2 100644 --- a/src/components/visualizations/algorithms/HuffmanCodingVisualization.tsx +++ b/src/components/visualizations/algorithms/HuffmanCodingVisualization.tsx @@ -1,41 +1,36 @@ -import React, { useEffect, useRef, useState } from "react"; -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; -import { StepControls } from "../shared/StepControls"; -import { VariablePanel } from "../shared/VariablePanel"; +import React, { useState } from 'react'; +import { Info } from 'lucide-react'; +import { SimpleStepControls } from '../shared/SimpleStepControls'; +import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface HuffmanNode { - char: string; - freq: number; - left: HuffmanNode | null; - right: HuffmanNode | null; - id: string; + char: string; + freq: number; + left: HuffmanNode | null; + right: HuffmanNode | null; + id: string; } interface Step { - freqMap: Record; - heap: HuffmanNode[]; - activeIds: string[]; - codes: Record; - message: string; - lineNumber: number; + freqMap: Record; + heap: HuffmanNode[]; + activeIds: string[]; + codes: Record; + message: string; + explanation: string; + pseudoStep: string; } -export const HuffmanCodingVisualization: React.FC = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1000); - const intervalRef = useRef(null); - - const code = `function huffmanEncoding(text: string): Map { +const languages: VisualizationLanguageMap = { + typescript: `function huffmanEncoding(text: string): Map { const freqMap = new Map(); for (const char of text) { freqMap.set(char, (freqMap.get(char) || 0) + 1); } - const heap: HuffmanNode[] = Array.from(freqMap.entries()) .map(([char, freq]) => new HuffmanNode(char, freq)); - while (heap.length > 1) { heap.sort((a, b) => a.freq - b.freq); const left = heap.shift()!; @@ -43,7 +38,6 @@ export const HuffmanCodingVisualization: React.FC = () => { const parent = new HuffmanNode('', left.freq + right.freq, left, right); heap.push(parent); } - const codes = new Map(); function buildCodes(node: HuffmanNode | null, code: string): void { if (!node) return; @@ -55,10 +49,8 @@ export const HuffmanCodingVisualization: React.FC = () => { buildCodes(node.right, code + '1'); } buildCodes(heap[0], ''); - return codes; } - class HuffmanNode { constructor( public char: string, @@ -66,343 +58,529 @@ class HuffmanNode { public left: HuffmanNode | null = null, public right: HuffmanNode | null = null ) {} -}`; - - const generateSteps = () => { - const text = "ABRACADABRA"; - const newSteps: Step[] = []; - - // 1. Frequency Map - const freqMap: Record = {}; - newSteps.push({ - freqMap: {}, - heap: [], - activeIds: [], - codes: {}, - message: `Start with text: "${text}". Calculate frequencies of each character.`, - lineNumber: 1, - }); - - for (let i = 0; i < text.length; i++) { - const char = text[i]; - freqMap[char] = (freqMap[char] || 0) + 1; - newSteps.push({ - freqMap: { ...freqMap }, - heap: [], - activeIds: [], - codes: {}, - message: `Counted '${char}'. Current frequencies: ${JSON.stringify(freqMap)}`, - lineNumber: 4, - }); +}`, + + python: `import heapq +def huffman_encoding(text): + if not text: + return {} + freq_map = {} + for char in text: + freq_map[char] = freq_map.get(char, 0) + 1 + heap = [[freq, char] for char, freq in freq_map.items()] + heapq.heapify(heap) + while len(heap) > 1: + freq1, char1 = heapq.heappop(heap) + freq2, char2 = heapq.heappop(heap) + merged_node = [freq1 + freq2, [char1, char2]] + heapq.heappush(heap, merged_node) + huffman_codes = {} + def generate_codes(node, code): + if isinstance(node[1], str): + huffman_codes[node[1]] = code or '0' + return + generate_codes([node[0], node[1][0]], code + '0') + generate_codes([node[0], node[1][1]], code + '1') + generate_codes(heap[0], '') + return huffman_codes`, + + java: `class HuffmanNode { + char character; + int frequency; + HuffmanNode left; + HuffmanNode right; + HuffmanNode(char character, int frequency) { + this.character = character; + this.frequency = frequency; + this.left = null; + this.right = null; + } +} +public static class Solution { + public Map huffmanEncoding(String text) { + Map frequencyMap = new HashMap<>(); + for (char c : text.toCharArray()) { + frequencyMap.put(c, frequencyMap.getOrDefault(c, 0) + 1); } - - // 2. Initial Heap - const nodes: HuffmanNode[] = Object.entries(freqMap).map(([char, freq]) => ({ - char, - freq, - left: null, - right: null, - id: `leaf-${char}` - })); - - newSteps.push({ - freqMap: { ...freqMap }, - heap: [...nodes], - activeIds: [], - codes: {}, - message: "Create initial nodes from the frequency map and add them to the heap.", - lineNumber: 7, - }); - - // 3. While loop - Building Tree - let heap = [...nodes]; - - while (heap.length > 1) { - // Sort - heap.sort((a, b) => a.freq - b.freq); - newSteps.push({ - freqMap: { ...freqMap }, - heap: heap.map(n => ({ ...n })), - activeIds: [], - codes: {}, - message: "Sort the heap to find the two nodes with the smallest frequencies.", - lineNumber: 11, - }); - - // Shift 2 - const left = heap.shift()!; - const right = heap.shift()!; - - newSteps.push({ - freqMap: { ...freqMap }, - heap: [left, right, ...heap].map(n => ({ ...n })), - activeIds: [left.id, right.id], - codes: {}, - message: `Extract the two smallest nodes: '${left.char || 'internal'}' (${left.freq}) and '${right.char || 'internal'}' (${right.freq}).`, - lineNumber: 12, - }); - - const parent: HuffmanNode = { - char: '', - freq: left.freq + right.freq, - left, - right, - id: `node-${left.id}-${right.id}` - }; - - heap.push(parent); - - newSteps.push({ - freqMap: { ...freqMap }, - heap: heap.map(n => ({ ...n })), - activeIds: [parent.id], - codes: {}, - message: `Create a parent node with sum frequency ${parent.freq} and push it back to the heap.`, - lineNumber: 15, - }); + PriorityQueue priorityQueue = new PriorityQueue<>(Comparator.comparingInt(node -> node.frequency)); + for (Map.Entry entry : frequencyMap.entrySet()) { + priorityQueue.add(new HuffmanNode(entry.getKey(), entry.getValue())); } - - // 4. Build Codes - const codes: Record = {}; - const traverse = (node: HuffmanNode | null, code: string) => { - if (!node) return; - - newSteps.push({ - freqMap: { ...freqMap }, - heap: [...heap], - activeIds: [node.id], - codes: { ...codes }, - message: `Visiting ${node.char ? `'${node.char}'` : 'internal node'}. Current path code: ${code || 'root'}`, - lineNumber: 21, - }); - - if (!node.left && !node.right) { - codes[node.char] = code || '0'; - newSteps.push({ - freqMap: { ...freqMap }, - heap: [...heap], - activeIds: [node.id], - codes: { ...codes }, - message: `Leaf node '${node.char}' reached. Huffman code: ${codes[node.char]}`, - lineNumber: 24, - }); - return; - } - - traverse(node.left, code + '0'); - traverse(node.right, code + '1'); - }; - - traverse(heap[0], ''); - - newSteps.push({ - freqMap: { ...freqMap }, - heap: [...heap], - activeIds: [], - codes: { ...codes }, - message: "Huffman encoding completed! Final codes generated for each character.", - lineNumber: 31, - }); - - setSteps(newSteps); - setCurrentStepIndex(0); - }; - - useEffect(() => { - generateSteps(); - }, []); - - useEffect(() => { - if (isPlaying && currentStepIndex < steps.length - 1) { - intervalRef.current = window.setInterval(() => { - setCurrentStepIndex((prev) => { - if (prev >= steps.length - 1) { - setIsPlaying(false); - return prev; - } - return prev + 1; - }); - }, speed); - } else { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } + while (priorityQueue.size() > 1) { + HuffmanNode left = priorityQueue.poll(); + HuffmanNode right = priorityQueue.poll(); + HuffmanNode parent = new HuffmanNode('\\0', left.frequency + right.frequency); + parent.left = left; + parent.right = right; + priorityQueue.add(parent); } - - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - }; - }, [isPlaying, currentStepIndex, steps.length, speed]); - - const handlePlay = () => setIsPlaying(true); - const handlePause = () => setIsPlaying(false); - const handleStepForward = () => { - if (currentStepIndex < steps.length - 1) { - setCurrentStepIndex(currentStepIndex + 1); + HuffmanNode root = priorityQueue.poll(); + Map huffmanCodes = new HashMap<>(); + buildHuffmanCodes(root, "", huffmanCodes); + return huffmanCodes; + } + private void buildHuffmanCodes(HuffmanNode node, String code, Map huffmanCodes) { + if (node == null) { + return; } - }; - const handleStepBack = () => { - if (currentStepIndex > 0) { - setCurrentStepIndex(currentStepIndex - 1); + if (node.left == null && node.right == null) { + huffmanCodes.put(node.character, code.isEmpty() ? "0" : code); + return; } + buildHuffmanCodes(node.left, code + "0", huffmanCodes); + buildHuffmanCodes(node.right, code + "1", huffmanCodes); + } +}`, + + cpp: `struct HuffmanNode { + char ch; + int freq; + HuffmanNode *left, *right; + HuffmanNode(char c, int f) : ch(c), freq(f), left(nullptr), right(nullptr) {} +}; +struct Compare { + bool operator()(HuffmanNode* a, HuffmanNode* b) { + return a->freq > b->freq; + } +}; +map huffmanEncoding(string text) { + map freqMap; + for (char c : text) freqMap[c]++; + priority_queue, Compare> minHeap; + for (auto [ch, freq] : freqMap) { + minHeap.push(new HuffmanNode(ch, freq)); + } + while (minHeap.size() > 1) { + auto left = minHeap.top(); minHeap.pop(); + auto right = minHeap.top(); minHeap.pop(); + auto parent = new HuffmanNode('\\0', left->freq + right->freq); + parent->left = left; + parent->right = right; + minHeap.push(parent); + } + map codes; + function buildCodes = [&](HuffmanNode* node, string code) { + if (!node) return; + if (!node->left && !node->right) { + codes[node->ch] = code.empty() ? "0" : code; + return; + } + buildCodes(node->left, code + "0"); + buildCodes(node->right, code + "1"); }; - const handleReset = () => { - setCurrentStepIndex(0); - setIsPlaying(false); - }; + buildCodes(minHeap.top(), ""); + return codes; +}` +}; - if (steps.length === 0) return null; - - const currentStep = steps[currentStepIndex]; - - // Tree rendering helper - const renderTree = (node: HuffmanNode | null, x: number, y: number, offset: number, depth: number = 0): JSX.Element | null => { - if (!node) return null; - - const isActive = currentStep.activeIds.includes(node.id); - - return ( - - {node.left && ( - <> - - 0 - {renderTree(node.left, x - offset, y + 50, offset / 1.8, depth + 1)} - - )} - {node.right && ( - <> - - 1 - {renderTree(node.right, x + offset, y + 50, offset / 1.8, depth + 1)} - - )} - - - {node.char || "∑"} - - - {node.freq} - - - ); +function generateVisualizationData() { + const text = 'ABRACADABRA'; + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + steps.push({ + freqMap: {}, + heap: [], + activeIds: [], + codes: {}, + message: `Start with text: "${text}". Calculate frequencies of each character.`, + explanation: 'Count the occurrence of each unique character in the string.', + pseudoStep: `START huffmanEncoding(text="${text}")`, + }); + addLines(1, 2, 14, 12); + + const freqMap: Record = {}; + for (let i = 0; i < text.length; i++) { + const char = text[i]; + freqMap[char] = (freqMap[char] || 0) + 1; + } + + steps.push({ + freqMap: { ...freqMap }, + heap: [], + activeIds: [], + codes: {}, + message: 'Finished counting character frequencies.', + explanation: `Frequency Map built: A:${freqMap.A}, B:${freqMap.B}, R:${freqMap.R}, C:${freqMap.C}, D:${freqMap.D}.`, + pseudoStep: `SET freqMap = ${JSON.stringify(freqMap)}`, + }); + addLines(3, 7, 16, 14); + + const nodes: HuffmanNode[] = Object.entries(freqMap).map(([char, freq]) => ({ + char, + freq, + left: null, + right: null, + id: `leaf-${char}` + })); + + steps.push({ + freqMap: { ...freqMap }, + heap: [...nodes], + activeIds: [], + codes: {}, + message: 'Create leaf nodes and add them to priority queue (heap).', + explanation: 'Wrap each character and frequency in a HuffmanNode, initializing the priority queue.', + pseudoStep: 'SET heap = map(freqMap) to HuffmanNodes', + }); + addLines(6, 8, 19, 15); + + let heap = [...nodes]; + while (heap.length > 1) { + heap.sort((a, b) => a.freq - b.freq); + + steps.push({ + freqMap: { ...freqMap }, + heap: heap.map((n) => ({ ...n })), + activeIds: [], + codes: {}, + message: 'Sort queue to bring nodes with the lowest frequencies to the front.', + explanation: 'Priority queue sort by frequency ascending to greedily retrieve the two least frequent nodes.', + pseudoStep: 'SORT heap BY freq ASC', + }); + addLines(9, 10, 23, 19); + + const left = heap.shift()!; + const right = heap.shift()!; + + steps.push({ + freqMap: { ...freqMap }, + heap: [left, right, ...heap].map((n) => ({ ...n })), + activeIds: [left.id, right.id], + codes: {}, + message: `Extract two smallest: '${left.char || 'node'}' (${left.freq}) and '${right.char || 'node'}' (${right.freq}).`, + explanation: `Dequeue the two nodes with minimal frequencies: left: ${left.char || 'internal'}(${left.freq}), right: ${right.char || 'internal'}(${right.freq}).`, + pseudoStep: `EXTRACT MIN left, right FROM heap`, + }); + addLines(10, 11, 24, 20); + + const parent: HuffmanNode = { + char: '', + freq: left.freq + right.freq, + left, + right, + id: `node-${left.id}-${right.id}` }; + heap.push(parent); + + steps.push({ + freqMap: { ...freqMap }, + heap: heap.map((n) => ({ ...n })), + activeIds: [parent.id], + codes: {}, + message: `Create parent with sum frequency ${parent.freq} and push back to heap.`, + explanation: `Build internal parent node with frequency ${parent.freq} (left.freq + right.freq).`, + pseudoStep: `INSERT parent(${parent.freq}) with left, right into heap`, + }); + addLines(12, 13, 26, 22); + } + + const codes: Record = {}; + const traverse = (node: HuffmanNode | null, code: string) => { + if (!node) return; + + steps.push({ + freqMap: { ...freqMap }, + heap: [...heap], + activeIds: [node.id], + codes: { ...codes }, + message: `Visit node '${node.char || 'internal'}'. Path code: ${code || 'root'}`, + explanation: `Traverse the tree recursively. Current path code is '${code || 'root'}'.`, + pseudoStep: `buildCodes(node='${node.char || 'internal'}', code='${code}')`, + }); + addLines(16, 16, 36, 28); + + if (!node.left && !node.right) { + codes[node.char] = code || '0'; + steps.push({ + freqMap: { ...freqMap }, + heap: [...heap], + activeIds: [node.id], + codes: { ...codes }, + message: `Leaf reached! Code for '${node.char}' = ${codes[node.char]}`, + explanation: `Leaf node '${node.char}' reached. Assign its path string '${codes[node.char]}' as its Huffman encoding code.`, + pseudoStep: `SET codes['${node.char}'] = '${code}'`, + }); + addLines(19, 18, 41, 31); + return; + } + + traverse(node.left, code + '0'); + traverse(node.right, code + '1'); + }; + + traverse(heap[0], ''); + + steps.push({ + freqMap: { ...freqMap }, + heap: [...heap], + activeIds: [], + codes: { ...codes }, + message: 'Huffman codes generated successfully!', + explanation: 'Tree traversal complete. Return map of character-to-binary-code mappings.', + pseudoStep: 'RETURN codes', + }); + addLines(26, 23, 34, 38); + + return { steps, stepLineNumbers }; +} + +export const HuffmanCodingVisualization: React.FC = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + + const handleReset = () => { + setCurrentStepIndex(0); + }; + + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map((s) => s.pseudoStep); + + const renderTree = ( + node: HuffmanNode | null, + x: number, + y: number, + offset: number, + depth: number = 0 + ): JSX.Element | null => { + if (!node) return null; + + const isActive = currentStep.activeIds.includes(node.id); + return ( -
- + {node.left && ( + <> + - -
-
-
-

Huffman Tree Workspace

- -
-
-

Frequency Map

-
- {Object.entries(currentStep.freqMap).map(([char, freq]) => ( -
- {char} - × {freq} -
- ))} -
-
- - {Object.keys(currentStep.codes).length > 0 && ( -
-

Generated Codes

-
- {Object.entries(currentStep.codes).map(([char, code]) => ( -
- {char}: - {code} -
- ))} -
-
- )} -
- -
- - {/* Horizontal guide for heap */} - - - {currentStep.heap.map((root, idx) => { - const xOffset = (Math.max(500, currentStep.heap.length * 120) / (currentStep.heap.length + 1)) * (idx + 1); - return ( - - Tree {idx + 1} - {renderTree(root, xOffset, 60, currentStep.heap.length > 3 ? 40 : 80)} - - ) - })} - -
- -
-

{currentStep.message}

-
+ + 0 + + {renderTree(node.left, x - offset, y + 50, offset / 1.8, depth + 1)} + + )} + {node.right && ( + <> + + + 1 + + {renderTree(node.right, x + offset, y + 50, offset / 1.8, depth + 1)} + + )} + + + {node.char || '∑'} + + + {node.freq} + + + ); + }; + + return ( +
+ + +
+ {/* Left Column: Visual State */} +
+
+

+ Huffman Tree Workspace +

+ +
+
+

+ Frequency Map +

+
+ {Object.entries(currentStep.freqMap).map(([char, freq]) => ( +
+ {char} + × {freq}
+ ))} +
+
+ + {Object.keys(currentStep.codes).length > 0 && ( +
+

+ Generated Codes +

+
+ {Object.entries(currentStep.codes).map(([char, code]) => ( +
+ {char}: + {code} +
+ ))} +
+
+ )} +
- 1 ? "Building Tree" : "Extracting Codes", - "Characters Encoded": Object.keys(currentStep.codes).length, - }} - /> +
+ + {/* Horizontal guide for heap */} + + + {currentStep.heap.map((root, idx) => { + const xOffset = + (Math.max(500, currentStep.heap.length * 120) / (currentStep.heap.length + 1)) * + (idx + 1); + return ( + + + Tree {idx + 1} + + {renderTree(root, xOffset, 40, currentStep.heap.length > 3 ? 40 : 80)} + + ); + })} + +
+
+ + {/* Commentary Panel */} +
+
+
+
+ + + + + + Algorithm Commentary +
+
+ Step {currentStepIndex + 1} of {steps.length} +
+
-
- +
+
+ +
+
+

+ Current Action +

+
+ {currentStep.explanation} +
+
+
+ + 1 + ? 'Building Tree' + : 'Codes Generation', + chars_encoded: Object.keys(currentStep.codes).length + }} + />
- ); + + {/* Right Column: Code Display */} +
+ +
+
+
+ ); }; + +export default HuffmanCodingVisualization; diff --git a/src/components/visualizations/algorithms/InsertIntervalVisualization.tsx b/src/components/visualizations/algorithms/InsertIntervalVisualization.tsx index c500935..5c9e98c 100644 --- a/src/components/visualizations/algorithms/InsertIntervalVisualization.tsx +++ b/src/components/visualizations/algorithms/InsertIntervalVisualization.tsx @@ -1,10 +1,10 @@ -import React, { useState, useEffect } from 'react'; -import { Card } from '@/components/ui/card'; +import React, { useState } from 'react'; +import { Info } from 'lucide-react'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; -import { motion } from 'framer-motion'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { intervals: [number, number][]; @@ -14,20 +14,14 @@ interface Step { merged: [number, number] | null; variables: Record; explanation: string; - highlightedLines: number[]; - lineExecution: string; + pseudoStep: string; } -export const InsertIntervalVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStep, setCurrentStep] = useState(0); - - const code = `function insert(intervals: number[][], newInterval: number[]): number[][] { +const languages: VisualizationLanguageMap = { + typescript: `function insert(intervals: number[][], newInterval: number[]): number[][] { const res: number[][] = []; - for (let i = 0; i < intervals.length; i++) { const curr = intervals[i]; - if (newInterval[1] < curr[0]) { res.push(newInterval); return res.concat(intervals.slice(i)); @@ -40,204 +34,273 @@ export const InsertIntervalVisualization = () => { ]; } } - res.push(newInterval); return res; -}`; +}`, + + python: `def insert(intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]: + res: List[List[int]] = [] + for i in range(len(intervals)): + curr = intervals[i] + if newInterval[1] < curr[0]: + res.append(newInterval) + return res + intervals[i:] + elif newInterval[0] > curr[1]: + res.append(curr) + else: + newInterval = [ + min(newInterval[0], curr[0]), + max(newInterval[1], curr[1]) + ] + res.append(newInterval) + return res`, + + java: `public static class Solution { + public int[][] insert(int[][] intervals, int[] newInterval) { + List result = new ArrayList<>(); + for (int i = 0; i < intervals.length; i++) { + int[] current = intervals[i]; + if (newInterval[1] < current[0]) { + result.add(newInterval); + for (int j = i; j < intervals.length; j++) { + result.add(intervals[j]); + } + return result.toArray(new int[result.size()][]); + } else if (newInterval[0] > current[1]) { + result.add(current); + } else { + newInterval = new int[]{ + Math.min(newInterval[0], current[0]), + Math.max(newInterval[1], current[1]) + }; + } + } + result.add(newInterval); + return result.toArray(new int[result.size()][]); + } +}`, + + cpp: `class Solution { +public: + vector> insert(vector>& intervals, vector& newInterval) { + vector> res; + for (int i = 0; i < intervals.size(); i++) { + if (newInterval[1] < intervals[i][0]) { + res.push_back(newInterval); + res.insert(res.end(), intervals.begin() + i, intervals.end()); + return res; + } else if (newInterval[0] > intervals[i][1]) { + res.push_back(intervals[i]); + } else { + newInterval = { + min(newInterval[0], intervals[i][0]), + max(newInterval[1], intervals[i][1]) + }; + } + } + res.push_back(newInterval); + return res; + } +};` +}; - const generateSteps = () => { - // Example tailored for rich step demonstration: overlapping and non-overlapping edges - const intervals: [number, number][] = [[1, 2], [3, 5], [6, 7], [8, 10], [12, 16]]; - let newInterval: [number, number] = [4, 8]; - const initialNewInterval: [number, number] = [...newInterval]; +function generateVisualizationData() { + const intervals: [number, number][] = [[1, 2], [3, 5], [6, 7], [8, 10], [12, 16]]; + let newInterval: [number, number] = [4, 8]; + + const steps: Step[] = []; + const res: [number, number][] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - const newSteps: Step[] = []; - const res: [number, number][] = []; + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + steps.push({ + intervals: [...intervals], + newInterval: [...newInterval], + result: [...res], + currentIdx: -1, + merged: null, + variables: { intervals: JSON.stringify(intervals), newInterval: JSON.stringify(newInterval) }, + explanation: `Insert new interval [${newInterval[0]}, ${newInterval[1]}] into sorted intervals.`, + pseudoStep: `START insert(newInterval=[${newInterval[0]}, ${newInterval[1]}])`, + }); + addLines(1, 1, 2, 3); + + steps.push({ + intervals: [...intervals], + newInterval: [...newInterval], + result: [...res], + currentIdx: -1, + merged: null, + variables: { res: JSON.stringify(res) }, + explanation: 'Initialize empty result array.', + pseudoStep: 'SET res = []', + }); + addLines(2, 2, 3, 4); + + for (let i = 0; i < intervals.length; i++) { + const curr = intervals[i]; - newSteps.push({ + steps.push({ intervals: [...intervals], newInterval: [...newInterval], result: [...res], - currentIdx: -1, + currentIdx: i, merged: null, - variables: { intervals: JSON.stringify(intervals), newInterval: JSON.stringify(newInterval) }, - explanation: `Insert new interval [${newInterval[0]}, ${newInterval[1]}] into sorted intervals.`, - highlightedLines: [1], - lineExecution: "function insert(intervals, newInterval)" + variables: { i, curr: JSON.stringify(curr) }, + explanation: `Iteration ${i}: Current interval under review is [${curr[0]}, ${curr[1]}].`, + pseudoStep: `FOR i = ${i} (curr = [${curr[0]}, ${curr[1]}])`, }); + addLines(3, 3, 4, 5); - newSteps.push({ + steps.push({ intervals: [...intervals], newInterval: [...newInterval], result: [...res], - currentIdx: -1, + currentIdx: i, merged: null, - variables: { res: JSON.stringify(res) }, - explanation: "Initialize empty result array.", - highlightedLines: [2], - lineExecution: "const res: number[][] = [];" + variables: { 'newInterval[1]': newInterval[1], 'curr[0]': curr[0] }, + explanation: `Check if newInterval ends before current starts (newInterval[1] < curr[0]): ${newInterval[1]} < ${curr[0]}?`, + pseudoStep: `IF newInterval.end (${newInterval[1]}) < curr.start (${curr[0]})`, }); + addLines(5, 5, 6, 6); - for (let i = 0; i < intervals.length; i++) { - const curr = intervals[i]; - - newSteps.push({ + if (newInterval[1] < curr[0]) { + res.push([...newInterval]); + steps.push({ intervals: [...intervals], newInterval: [...newInterval], result: [...res], currentIdx: i, merged: null, - variables: { i, curr: JSON.stringify(curr) }, - explanation: `Iteration ${i}: current array interval is [${curr[0]}, ${curr[1]}].`, - highlightedLines: [4], - lineExecution: `for (let i = ${i}; ...)` + variables: { res: JSON.stringify(res) }, + explanation: `True! newInterval [${newInterval[0]}, ${newInterval[1]}] finishes before current [${curr[0]}, ${curr[1]}]. Push newInterval to result.`, + pseudoStep: `PUSH newInterval [${newInterval[0]}, ${newInterval[1]}]`, }); + addLines(6, 6, 7, 7); - // Case 1 step - newSteps.push({ + const finalRes = res.concat(intervals.slice(i)) as [number, number][]; + steps.push({ intervals: [...intervals], newInterval: [...newInterval], - result: [...res], + result: [...finalRes], currentIdx: i, merged: null, - variables: { 'newInterval[1]': newInterval[1], 'curr[0]': curr[0] }, - explanation: `Check if newInterval comes before current (newInterval[1] < curr[0]): ${newInterval[1]} < ${curr[0]} -> ${newInterval[1] < curr[0]}`, - highlightedLines: [7], - lineExecution: `if (${newInterval[1]} < ${curr[0]})` + variables: { res: JSON.stringify(finalRes) }, + explanation: 'All subsequent intervals can be appended directly. Concatenate remainder and return.', + pseudoStep: 'RETURN res + remaining_intervals', }); + addLines(7, 7, 11, 9); - if (newInterval[1] < curr[0]) { - res.push([...newInterval]); - newSteps.push({ - intervals: [...intervals], - newInterval: [...newInterval], - result: [...res], - currentIdx: i, - merged: null, - variables: { res: JSON.stringify(res) }, - explanation: `True. Push newInterval [${newInterval[0]}, ${newInterval[1]}] into result.`, - highlightedLines: [8], - lineExecution: "res.push(newInterval);" - }); - - const finalRes = res.concat(intervals.slice(i)) as [number, number][]; - newSteps.push({ - intervals: [...intervals], - newInterval: [...newInterval], - result: [...finalRes], - currentIdx: i, - merged: null, - variables: { res: JSON.stringify(finalRes) }, - explanation: `Return result concatenated with remaining intervals.`, - highlightedLines: [9], - lineExecution: "return res.concat(intervals.slice(i));" - }); - - setSteps(newSteps); - setCurrentStep(0); - return; - } + return { steps, stepLineNumbers }; + } - // Case 2 step - newSteps.push({ + steps.push({ + intervals: [...intervals], + newInterval: [...newInterval], + result: [...res], + currentIdx: i, + merged: null, + variables: { 'newInterval[0]': newInterval[0], 'curr[1]': curr[1] }, + explanation: `Check if newInterval starts after current ends (newInterval[0] > curr[1]): ${newInterval[0]} > ${curr[1]}?`, + pseudoStep: `ELSE IF newInterval.start (${newInterval[0]}) > curr.end (${curr[1]})`, + }); + addLines(8, 8, 12, 10); + + if (newInterval[0] > curr[1]) { + res.push([...curr]); + steps.push({ intervals: [...intervals], newInterval: [...newInterval], result: [...res], currentIdx: i, merged: null, - variables: { 'newInterval[0]': newInterval[0], 'curr[1]': curr[1] }, - explanation: `Check if newInterval comes after current (newInterval[0] > curr[1]): ${newInterval[0]} > ${curr[1]} -> ${newInterval[0] > curr[1]}`, - highlightedLines: [10], - lineExecution: `else if (${newInterval[0]} > ${curr[1]})` + variables: { res: JSON.stringify(res) }, + explanation: `True! Push current interval [${curr[0]}, ${curr[1]}] to result as it lies entirely before newInterval.`, + pseudoStep: `PUSH curr [${curr[0]}, ${curr[1]}]`, }); + addLines(9, 9, 13, 11); + } else { + steps.push({ + intervals: [...intervals], + newInterval: [...newInterval], + result: [...res], + currentIdx: i, + merged: null, + variables: {}, + explanation: `Neither before nor after -> Overlap! Merge current [${curr[0]}, ${curr[1]}] and newInterval [${newInterval[0]}, ${newInterval[1]}].`, + pseudoStep: 'ELSE → OVERLAP!', + }); + addLines(10, 10, 14, 12); - if (newInterval[0] > curr[1]) { - res.push([...curr]); - newSteps.push({ - intervals: [...intervals], - newInterval: [...newInterval], - result: [...res], - currentIdx: i, - merged: null, - variables: { res: JSON.stringify(res) }, - explanation: `True. Push current interval [${curr[0]}, ${curr[1]}] into result.`, - highlightedLines: [11], - lineExecution: "res.push(curr);" - }); - } - else { - // Case 3 step - newSteps.push({ - intervals: [...intervals], - newInterval: [...newInterval], - result: [...res], - currentIdx: i, - merged: null, - variables: {}, - explanation: `Neither before nor after -> Overlap! We must merge them.`, - highlightedLines: [12], - lineExecution: "else { // overlapping logic" - }); - - const minStart = Math.min(newInterval[0], curr[0]); - const maxEnd = Math.max(newInterval[1], curr[1]); - - // To illustrate the change purely for the explanation - const prevStart = newInterval[0]; - const prevEnd = newInterval[1]; - - newInterval = [minStart, maxEnd]; - - newSteps.push({ - intervals: [...intervals], - newInterval: [...newInterval], - result: [...res], - currentIdx: i, - merged: [...newInterval], - variables: { newInterval: JSON.stringify(newInterval) }, - explanation: `Merged newInterval becomes [Math.min(${prevStart}, ${curr[0]}), Math.max(${prevEnd}, ${curr[1]})] = [${newInterval[0]}, ${newInterval[1]}].`, - highlightedLines: [13, 14, 15, 16], - lineExecution: `newInterval = [${newInterval[0]}, ${newInterval[1]}]` - }); - } + const minStart = Math.min(newInterval[0], curr[0]); + const maxEnd = Math.max(newInterval[1], curr[1]); + const prevStart = newInterval[0]; + const prevEnd = newInterval[1]; + newInterval = [minStart, maxEnd]; + + steps.push({ + intervals: [...intervals], + newInterval: [...newInterval], + result: [...res], + currentIdx: i, + merged: [...newInterval], + variables: { newInterval: JSON.stringify(newInterval) }, + explanation: `Merged newInterval becomes: [min(${prevStart}, ${curr[0]}), max(${prevEnd}, ${curr[1]})] = [${newInterval[0]}, ${newInterval[1]}].`, + pseudoStep: `SET newInterval = [min_start, max_end] → [${newInterval[0]}, ${newInterval[1]}]`, + }); + addLines(11, 11, 15, 13); } + } - res.push([...newInterval]); - newSteps.push({ - intervals: [...intervals], - newInterval: [...newInterval], - result: [...res], - currentIdx: -1, - merged: [...newInterval], - variables: { res: JSON.stringify(res) }, - explanation: `Loop ended. Push the finalized newInterval [${newInterval[0]}, ${newInterval[1]}] to result.`, - highlightedLines: [20], - lineExecution: "res.push(newInterval);" - }); + res.push([...newInterval]); + steps.push({ + intervals: [...intervals], + newInterval: [...newInterval], + result: [...res], + currentIdx: -1, + merged: [...newInterval], + variables: { res: JSON.stringify(res) }, + explanation: `Loop completed. Push the final merged newInterval [${newInterval[0]}, ${newInterval[1]}] to result.`, + pseudoStep: `PUSH newInterval [${newInterval[0]}, ${newInterval[1]}]`, + }); + addLines(17, 15, 21, 19); + + steps.push({ + intervals: [...intervals], + newInterval: [...newInterval], + result: [...res], + currentIdx: -1, + merged: null, + variables: { final: JSON.stringify(res) }, + explanation: 'Return final merged intervals list.', + pseudoStep: 'RETURN res', + }); + addLines(18, 16, 22, 20); + + return { steps, stepLineNumbers }; +} - newSteps.push({ - intervals: [...intervals], - newInterval: [...newInterval], - result: [...res], - currentIdx: -1, - merged: null, - variables: { final: JSON.stringify(res) }, - explanation: `Return final result.`, - highlightedLines: [21], - lineExecution: "return res;" - }); +export const InsertIntervalVisualization: React.FC = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); - setSteps(newSteps); - setCurrentStep(0); + const handleReset = () => { + setCurrentStepIndex(0); }; - useEffect(() => { - generateSteps(); - }, []); - - if (steps.length === 0) return null; - const step = steps[currentStep]; - + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map((s) => s.pseudoStep); const TIMELINE_MAX = 20; const renderTimelineInterval = (label: string, interval: [number, number], colorClass: string) => { @@ -245,12 +308,12 @@ export const InsertIntervalVisualization = () => { const widthPct = ((interval[1] - interval[0]) / TIMELINE_MAX) * 100; return ( -
- {label} -
-
+
+ {label} +
+
{ return ( -
- -

Original Intervals

-
- {step.intervals.map((interval, idx) => ( -
- {renderTimelineInterval( - `I${idx}`, - interval, - idx === step.currentIdx - ? 'bg-primary/20 border-2 border-primary text-primary scale-105 z-10' - : 'bg-secondary/50 border-2 border-secondary text-foreground' - )} -
- ))} -
-
+
+
+

Original Intervals

+
+ {currentStep.intervals.map((interval, idx) => ( +
+ {renderTimelineInterval( + `I${idx}`, + interval, + idx === currentStep.currentIdx + ? 'bg-primary/20 border border-primary text-primary scale-[1.01] z-10' + : 'bg-muted/40 border border-border text-foreground/80' + )} +
+ ))} +
-
- -

New Interval

- {renderTimelineInterval( - 'New', - step.newInterval, - 'bg-accent/20 border-2 border-accent text-accent-foreground' - )} -
+
+

New Interval

+ {renderTimelineInterval( + 'New', + currentStep.newInterval, + 'bg-accent/20 border border-accent text-accent-foreground' + )}
- {step.result.length > 0 && ( -
- -

Result

-
- {step.result.map((interval, idx) => ( + {currentStep.result.length > 0 && ( +
+

Result

+
+ {currentStep.result.map((interval, idx) => { + const isMerged = + currentStep.merged && + interval[0] === currentStep.merged[0] && + interval[1] === currentStep.merged[1]; + return (
{renderTimelineInterval( `R${idx}`, interval, - step.merged && - interval[0] === step.merged[0] && - interval[1] === step.merged[1] - ? 'bg-green-500/20 border-2 border-green-500 text-green-700 dark:text-green-400' - : 'bg-blue-500/20 border-2 border-blue-500 text-blue-700 dark:text-blue-400' + isMerged + ? 'bg-green-500/20 border border-green-500 text-green-600 dark:text-green-400 font-bold' + : 'bg-blue-500/10 border border-blue-500/40 text-blue-600 dark:text-blue-400' )}
- ))} -
- + ); + })} +
)} -
- -
-
Current Execution:
-
- {step.lineExecution} + {/* Commentary Panel */} +
+
+
+
+ + + + + + Algorithm Commentary +
-
- {step.explanation} +
+ Step {currentStepIndex + 1} of {steps.length}
- -
-
- +
+
+ +
+
+

+ Current Action +

+
+ {currentStep.explanation} +
+
+
+
+ +
} rightContent={ - } controls={ } /> ); }; + +export default InsertIntervalVisualization; diff --git a/src/components/visualizations/algorithms/JumpGameVisualization.tsx b/src/components/visualizations/algorithms/JumpGameVisualization.tsx index 6dd1d49..bb32328 100644 --- a/src/components/visualizations/algorithms/JumpGameVisualization.tsx +++ b/src/components/visualizations/algorithms/JumpGameVisualization.tsx @@ -2,8 +2,10 @@ import React, { useState, useMemo } from 'react'; import { Card } from '@/components/ui/card'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; +import { Info } from 'lucide-react'; interface Step { nums: number[]; @@ -11,19 +13,11 @@ interface Step { goal: number | null; variables: Record; explanation: string; - highlightedLines: number[]; - lineExecution: string; + pseudoStep: string; } -export const JumpGameVisualization: React.FC = () => { - const [currentStep, setCurrentStep] = useState(0); - const [caseType, setCaseType] = useState<'reachable' | 'unreachable'>('reachable'); - - const nums = useMemo(() => - caseType === 'reachable' ? [2, 3, 1, 1, 4] : [3, 2, 1, 0, 4], - [caseType]); - - const code = `function canJump(nums: number[]): boolean { +const languages: VisualizationLanguageMap = { + typescript: `function canJump(nums: number[]): boolean { let goal = nums.length - 1; for (let i = nums.length - 1; i >= 0; i--) { if (i + nums[i] >= goal) { @@ -31,10 +25,65 @@ export const JumpGameVisualization: React.FC = () => { } } return goal === 0; -}`; +}`, + + python: `def canJump(nums: list[int]) -> bool: + goal = len(nums) - 1 + for i in range(len(nums) - 1, -1, -1): + if i + nums[i] >= goal: + goal = i + return goal == 0`, + + java: `public static class Solution { + public boolean canJump(int[] nums) { + int goal = nums.length - 1; + for (int i = nums.length - 1; i >= 0; i--) { + if (i + nums[i] >= goal) { + goal = i; + } + } + return goal == 0; + } +}`, + + cpp: `class Solution { +public: + bool canJump(vector& nums) { + int goal = nums.size() - 1; + for (int i = nums.size() - 1; i >= 0; i--) { + if (i + nums[i] >= goal) { + goal = i; + } + } + return goal == 0; + } +};` +}; + +export const JumpGameVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [caseType, setCaseType] = useState<'reachable' | 'unreachable'>('reachable'); + + const nums = useMemo(() => + caseType === 'reachable' ? [2, 3, 1, 1, 4] : [3, 2, 1, 0, 4], + [caseType]); - const steps = useMemo(() => { + const { steps, stepLineNumbers } = useMemo(() => { const stepsList: Step[] = []; + const stepLines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLines.typescript!.push(ts); + stepLines.python!.push(py); + stepLines.java!.push(java); + stepLines.cpp!.push(cpp); + }; + const n = nums.length; let goal = n - 1; @@ -43,10 +92,10 @@ export const JumpGameVisualization: React.FC = () => { i: null, goal: null, variables: { nums: `[${nums.join(', ')}]` }, - explanation: `Use Case: ${caseType.toUpperCase()}. Can we reach the last index? We work backward from target index ${n-1}.`, - lineExecution: "function canJump(nums: number[]): boolean {", - highlightedLines: [1] + explanation: `Check if we can reach the last index. We use a greedy strategy working backward from target index ${n-1}.`, + pseudoStep: `START canJump(nums=[${nums.join(', ')}])`, }); + addLines(1, 1, 2, 3); stepsList.push({ nums, @@ -54,9 +103,9 @@ export const JumpGameVisualization: React.FC = () => { goal: n - 1, variables: { goal: n - 1 }, explanation: `Initially, the target we must reach is the last index (${n - 1}).`, - lineExecution: "let goal = nums.length - 1;", - highlightedLines: [2] + pseudoStep: `SET goal = ${n - 1}`, }); + addLines(2, 2, 3, 4); for (let i = n - 1; i >= 0; i--) { stepsList.push({ @@ -64,10 +113,10 @@ export const JumpGameVisualization: React.FC = () => { i, goal, variables: { i, goal }, - explanation: `Checking index ${i}. Target goal post is at index ${goal}.`, - lineExecution: "for (let i = nums.length - 1; i >= 0; i--) {", - highlightedLines: [3] + explanation: `Checking index ${i}. Current goal post is at index ${goal}.`, + pseudoStep: `FOR i = ${i} down to 0`, }); + addLines(3, 3, 4, 5); const reach = i + nums[i]; const canReach = reach >= goal; @@ -77,10 +126,10 @@ export const JumpGameVisualization: React.FC = () => { i, goal, variables: { i, "nums[i]": nums[i], reach, goal, result: canReach }, - explanation: `Index ${i} jumps to ${reach}. Is this >= ${goal}? ${canReach ? "Yes." : "No."}`, - lineExecution: "if (i + nums[i] >= goal) {", - highlightedLines: [4] + explanation: `Index ${i} has jump size ${nums[i]}, reaching up to index ${reach}. Is reach >= goal? ${canReach ? "Yes." : "No."}`, + pseudoStep: `IF i + nums[i] >= goal → ${i} + ${nums[i]} >= ${goal} → ${canReach ? "YES ✓" : "NO ✗"}`, }); + addLines(4, 4, 5, 6); if (canReach) { goal = i; @@ -89,10 +138,10 @@ export const JumpGameVisualization: React.FC = () => { i, goal, variables: { goal }, - explanation: `Updated: Index ${i} can reach the goal, so ${i} becomes the new target index.`, - lineExecution: "goal = i;", - highlightedLines: [5] + explanation: `Since index ${i} can reach the current goal, update the goal to ${i}. Any index that reaches ${i} can now reach the end.`, + pseudoStep: `SET goal = ${i}`, }); + addLines(5, 5, 6, 7); } } @@ -101,29 +150,34 @@ export const JumpGameVisualization: React.FC = () => { i: null, goal, variables: { goal, result: goal === 0 }, - explanation: `Finished. Did we reach index 0? ${goal === 0 ? "YES (Successful)" : "NO (Failed)"}. Final result: ${goal === 0}.`, - lineExecution: "return goal === 0;", - highlightedLines: [8] + explanation: `Finished checking all indices. The leftmost index that can reach the end is ${goal}. Can we reach it from the start? ${goal === 0 ? "YES" : "NO"}.`, + pseudoStep: `RETURN goal === 0 → ${goal === 0 ? "TRUE" : "FALSE"}`, }); + addLines(8, 6, 9, 10); - return stepsList; + return { steps: stepsList, stepLineNumbers: stepLines }; }, [nums, caseType]); const handleCaseToggle = (type: 'reachable' | 'unreachable') => { setCaseType(type); - setCurrentStep(0); + setCurrentStepIndex(0); }; - const step = steps[currentStep]; + const handleReset = () => { + setCurrentStepIndex(0); + }; + + const step = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( -
+
+
-
-

- Jump Game (Greedy Strategy) -

- -
-
- {nums.map((num, idx) => { - const isGoal = idx === step.goal; - const isCurrent = idx === step.i; - const isPassed = step.goal !== null && idx > step.goal; - - return ( -
-
- {num} -
-
- {isGoal &&
Goal
} - {!isGoal && isCurrent &&
i
} -
-
- ); - })} + +

+ Array State & Goal Post +

+
+ {nums.map((num, idx) => { + const isGoal = idx === step.goal; + const isCurrent = idx === step.i; + const isPassed = step.goal !== null && idx > step.goal; + + return ( +
+
+ {num} +
+
+ {isGoal &&
Goal
} + {!isGoal && isCurrent &&
i
} +
+
+ ); + })} +
+
+ + {/* Commentary Panel */} + +
+
+
+ + + + + + Algorithm Commentary + +
+
+ Step {currentStepIndex + 1} of {steps.length}
- {step.goal !== null && ( -
-
-
Current Target
-
Index {step.goal}
-
-
-
Dist to Start
-
{step.goal}
-
+
+
+
- )} - -
- -
- -
-
-

- Current Execution -

-
- {step.lineExecution} -
-
-
-

- Commentary -

-

- {step.explanation} -

+
+

+ Current Action +

+
+ {step.explanation}
- -
+
+
+ + +
} rightContent={ -
-
- -
- -
- -
-
+ } controls={ } /> diff --git a/src/components/visualizations/algorithms/KMPVisualization.tsx b/src/components/visualizations/algorithms/KMPVisualization.tsx index 0d6c766..30ac77c 100644 --- a/src/components/visualizations/algorithms/KMPVisualization.tsx +++ b/src/components/visualizations/algorithms/KMPVisualization.tsx @@ -1,10 +1,11 @@ import { useState, useMemo } from 'react'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; import { motion, AnimatePresence } from 'framer-motion'; import { Card } from '@/components/ui/card'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { text: string; @@ -13,7 +14,7 @@ interface Step { i: number; j: number; explanation: string; - highlightedLines: number[]; + pseudoStep: string; variables: Record; phase: 'init' | 'lps' | 'search' | 'match' | 'mismatch' | 'done'; lps_i?: number; @@ -22,41 +23,217 @@ interface Step { matchedRange?: [number, number]; } +const languages: VisualizationLanguageMap = { + typescript: `function solution(text: string, pattern: string): number { + if (pattern.length === 0) return 0; + if (text.length === 0) return -1; + function buildLPS(p: string): number[] { + const lps = new Array(p.length).fill(0); + let len = 0; + for (let i = 1; i < p.length;) { + if (p[i] === p[len]) { + lps[i++] = ++len; + } + else if (len !== 0) { + len = lps[len - 1]; + } + else { + lps[i++] = 0; + } + } + return lps; + } + const lps = buildLPS(pattern); + let i = 0; + let j = 0; + while (i < text.length) { + if (text[i] === pattern[j]) { + i++; + j++; + } + if (j === pattern.length) + return i - j; + else if (i < text.length && text[i] !== pattern[j]) { + if (j !== 0) + j = lps[j - 1]; + else + i++; + } + } + return -1; +}`, + python: `def solution(text, pattern): + if pattern == "": + return 0 + if text == "": + return -1 + def build_lps(p): + lps = [0] * len(p) + length = 0 + i = 1 + while i < len(p): + if p[i] == p[length]: + length += 1 + lps[i] = length + i += 1 + else: + if length != 0: + length = lps[length - 1] + else: + lps[i] = 0 + i += 1 + return lps + lps = build_lps(pattern) + i = 0 + j = 0 + while i < len(text): + if text[i] == pattern[j]: + i += 1 + j += 1 + if j == len(pattern): + return i - j + elif i < len(text) and text[i] != pattern[j]: + if j != 0: + j = lps[j - 1] + else: + i += 1 + return -1`, + java: `public static class Solution { + public int solution(String text, String pattern) { + if (pattern.length() == 0) + return 0; + if (text.length() == 0) + return -1; + int[] lps = buildLPS(pattern); + int i = 0, j = 0; + while (i < text.length()) { + if (text.charAt(i) == pattern.charAt(j)) { + i++; + j++; + } + if (j == pattern.length()) + return i - j; + else if (i < text.length() && text.charAt(i) != pattern.charAt(j)) { + if (j != 0) + j = lps[j - 1]; + else + i++; + } + } + return -1; + } + int[] buildLPS(String pattern) { + int[] lps = new int[pattern.length()]; + int len = 0; + for (int i = 1; i < pattern.length();) { + if (pattern.charAt(i) == pattern.charAt(len)) { + lps[i++] = ++len; + } + else if (len != 0) + len = lps[len - 1]; + else + lps[i++] = 0; + } + return lps; + } +}`, + cpp: `class Solution { +public: +vector buildLPS(string pattern) { + int m = pattern.size(); + vector lps(m, 0); + int len = 0; + for (int i = 1; i < m;) { + if (pattern[i] == pattern[len]) { + lps[i++] = ++len; + } else { + if (len != 0) { + len = lps[len - 1]; + } else { + lps[i++] = 0; + } + } + } + return lps; +} +int solution(string text, string pattern) { + if (pattern.empty()) + return 0; + if (text.empty()) + return -1; + vector lps = buildLPS(pattern); + int i = 0, j = 0; + while (i < text.size()) { + if (text[i] == pattern[j]) { + i++; + j++; + } + if (j == pattern.size()) + return i - j; + else if (i < text.size() && text[i] != pattern[j]) { + if (j != 0) + j = lps[j - 1]; + else + i++; + } + } + return -1; +} +};` +}; + export const KMPVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); + const [currentStepIndex, setCurrentStepIndex] = useState(0); const textInput = "ababcabcabababd"; const patternInput = "ababd"; - const steps: Step[] = useMemo(() => { + const { steps, stepLineNumbers } = useMemo(() => { const s: Step[] = []; const text = textInput; const pattern = patternInput; + const lines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + lines.typescript!.push(ts); + lines.python!.push(py); + lines.java!.push(java); + lines.cpp!.push(cpp); + }; + s.push({ text, pattern, lps: [], i: -1, j: -1, explanation: "Initialize KMP search function.", - highlightedLines: [1], + pseudoStep: `CALL solution(text = "${text}", pattern = "${pattern}")`, variables: { text, pattern }, phase: 'init' }); + addLines(1, 1, 2, 20); s.push({ text, pattern, lps: [], i: -1, j: -1, explanation: "Check if pattern is empty. If so, it's found at index 0.", - highlightedLines: [2], + pseudoStep: `IF pattern.length == 0 (len = ${pattern.length})`, variables: { patternLength: pattern.length }, phase: 'init' }); - if (pattern.length === 0) return s; + addLines(2, 2, 3, 21); + if (pattern.length === 0) return { steps: s, stepLineNumbers: lines }; s.push({ text, pattern, lps: [], i: -1, j: -1, explanation: "Check if text is empty. If pattern is not empty, it cannot be found.", - highlightedLines: [3], + pseudoStep: `IF text.length == 0 (len = ${text.length})`, variables: { textLength: text.length }, phase: 'init' }); - if (text.length === 0) return s; + addLines(3, 4, 5, 23); + if (text.length === 0) return { steps: s, stepLineNumbers: lines }; const lps = new Array(pattern.length).fill(0); let len = 0; @@ -64,38 +241,42 @@ export const KMPVisualization = () => { s.push({ text, pattern, lps: [...lps], i: -1, j: -1, explanation: "Call buildLPS to construct the LPS array.", - highlightedLines: [22], + pseudoStep: "SET lps = CALL buildLPS(pattern)", variables: { pattern }, phase: 'lps' }); + addLines(20, 22, 7, 25); s.push({ text, pattern, lps: [...lps], i: -1, j: -1, explanation: "Initialize LPS array with zeros.", - highlightedLines: [6], + pseudoStep: `SET lps = [0, ..., 0] (size = ${pattern.length})`, variables: { lps: `[${lps.join(',')}]` }, phase: 'lps' }); + addLines(5, 7, 26, 5); s.push({ text, pattern, lps: [...lps], i: -1, j: -1, explanation: "Initialize len to 0.", - highlightedLines: [7], + pseudoStep: "SET len = 0", variables: { len }, phase: 'lps' }); + addLines(6, 8, 27, 6); for (let l_i = 1; l_i < pattern.length;) { s.push({ text, pattern, lps: [...lps], i: -1, j: -1, explanation: `Comparing pattern[${l_i}] ('${pattern[l_i]}') with pattern[${len}] ('${pattern[len]}').`, - highlightedLines: [8, 9], + pseudoStep: `IF pattern[${l_i}] == pattern[${len}] ('${pattern[l_i]}' == '${pattern[len]}') → ${pattern[l_i] === pattern[len] ? 'YES ✓' : 'NO ✗'}`, variables: { i: l_i, len, "pattern[i]": pattern[l_i], "pattern[len]": pattern[len] }, phase: 'lps', lps_i: l_i, lps_len: len, matchStatus: pattern[l_i] === pattern[len] ? 'match' : 'mismatch' }); + addLines(8, 11, 29, 8); if (pattern[l_i] === pattern[len]) { len++; @@ -103,126 +284,137 @@ export const KMPVisualization = () => { s.push({ text, pattern, lps: [...lps], i: -1, j: -1, explanation: `Characters match! Increment len to ${len} and set lps[${l_i}] = ${len}.`, - highlightedLines: [10], + pseudoStep: `SET lps[${l_i}] = ${len}`, variables: { i: l_i, len, lps: `[${lps.join(',')}]` }, phase: 'lps', lps_i: l_i, lps_len: len, matchStatus: 'match' }); + addLines(9, 12, 30, 9); l_i++; } else if (len !== 0) { const oldLen = len; len = lps[len - 1]; s.push({ text, pattern, lps: [...lps], i: -1, j: -1, - explanation: `Mismatch and len != 0. Update len to lps[${oldLen - 1}] = ${len}.`, - highlightedLines: [12, 13], + explanation: `Mismatch and len != 0. Fallback: update len to lps[${oldLen - 1}] = ${len}.`, + pseudoStep: `SET len = lps[len - 1] (len = ${len})`, variables: { i: l_i, oldLen, newLen: len }, phase: 'lps', lps_i: l_i, lps_len: len, matchStatus: 'mismatch' }); + addLines(12, 17, 33, 12); } else { lps[l_i] = 0; s.push({ text, pattern, lps: [...lps], i: -1, j: -1, - explanation: `Mismatch and len == 0. Set lps[${l_i}] = 0 and move to next character.`, - highlightedLines: [15, 16], + explanation: `Mismatch and len == 0. Set lps[${l_i}] = 0 and move to next index.`, + pseudoStep: `SET lps[${l_i}] = 0`, variables: { i: l_i, len, lps: `[${lps.join(',')}]` }, phase: 'lps', lps_i: l_i, lps_len: len, matchStatus: 'mismatch' }); + addLines(15, 19, 35, 14); l_i++; } } s.push({ text, pattern, lps: [...lps], i: -1, j: -1, - explanation: "LPS array construction complete. Return lps.", - highlightedLines: [19], + explanation: "LPS array construction complete. Return lps array.", + pseudoStep: "RETURN lps", variables: { lps: `[${lps.join(',')}]` }, phase: 'lps' }); + addLines(18, 21, 37, 18); let i = 0; let j = 0; s.push({ text, pattern, lps: [...lps], i, j, - explanation: "Initialize pointers i=0 (text) and j=0 (pattern).", - highlightedLines: [23, 24], + explanation: "Initialize text pointer i = 0 and pattern pointer j = 0.", + pseudoStep: "SET i = 0, j = 0", variables: { i, j }, phase: 'search' }); + addLines(21, 23, 8, 26); while (i < text.length) { s.push({ text, pattern, lps: [...lps], i, j, explanation: `Comparing text[${i}] ('${text[i]}') with pattern[${j}] ('${pattern[j]}').`, - highlightedLines: [26, 27], + pseudoStep: `IF text[${i}] == pattern[${j}] ('${text[i]}' == '${pattern[j]}') → ${text[i] === pattern[j] ? 'YES ✓' : 'NO ✗'}`, variables: { i, j, "text[i]": text[i], "pattern[j]": pattern[j] }, phase: 'search', matchStatus: text[i] === pattern[j] ? 'match' : 'mismatch' }); + addLines(24, 26, 10, 28); if (text[i] === pattern[j]) { i++; j++; s.push({ text, pattern, lps: [...lps], i, j, - explanation: `Characters match! Increment both i to ${i} and j to ${j}.`, - highlightedLines: [28, 29], + explanation: `Characters match! Increment i to ${i} and j to ${j}.`, + pseudoStep: "SET i = i + 1, j = j + 1", variables: { i, j }, phase: 'match', matchStatus: 'match' }); + addLines(25, 27, 11, 29); } if (j === pattern.length) { s.push({ text, pattern, lps: [...lps], i, j, - explanation: `Full pattern match found! Returning start index i - j = ${i - j}.`, - highlightedLines: [31, 32], + explanation: `Pattern matched completely! Return start index i - j = ${i - j}.`, + pseudoStep: `RETURN i - j (${i - j})`, variables: { startIdx: i - j }, phase: 'done', matchedRange: [i - j, i - 1] }); - return s; + addLines(29, 30, 15, 33); + return { steps: s, stepLineNumbers: lines }; } else if (i < text.length && text[i] !== pattern[j]) { s.push({ text, pattern, lps: [...lps], i, j, - explanation: `Mismatch found at text[${i}] and pattern[${j}].`, - highlightedLines: [33], + explanation: `Mismatch at text[${i}] and pattern[${j}].`, + pseudoStep: `IF text[${i}] != pattern[${j}] → YES ✓`, variables: { i, j, "text[i]": text[i], "pattern[j]": pattern[j] }, phase: 'mismatch', matchStatus: 'mismatch' }); + addLines(30, 31, 16, 34); if (j !== 0) { const oldJ = j; j = lps[j - 1]; s.push({ text, pattern, lps: [...lps], i, j, - explanation: `j != 0. Use LPS to shift pattern. new j = lps[${oldJ - 1}] = ${j}.`, - highlightedLines: [34, 35], + explanation: `j != 0. Shift pattern based on LPS value: new j = lps[${oldJ - 1}] = ${j}.`, + pseudoStep: `SET j = lps[j - 1] (j = ${j})`, variables: { oldJ, newJ: j }, phase: 'mismatch', matchStatus: 'none' }); + addLines(32, 33, 18, 36); } else { i++; s.push({ text, pattern, lps: [...lps], i, j, - explanation: `j == 0. Cannot shift pattern further. Incrementing i to ${i}.`, - highlightedLines: [37], + explanation: `j == 0. Shift pattern is not possible. Incrementing i to ${i}.`, + pseudoStep: "SET i = i + 1", variables: { i, j }, phase: 'mismatch', matchStatus: 'none' }); + addLines(34, 34, 20, 38); } } } @@ -230,247 +422,213 @@ export const KMPVisualization = () => { s.push({ text, pattern, lps: [...lps], i, j, explanation: "Search complete. Pattern not found in text.", - highlightedLines: [40], + pseudoStep: "RETURN -1", variables: {}, phase: 'done' }); + addLines(37, 36, 23, 41); - return s; + return { steps: s, stepLineNumbers: lines }; }, [textInput, patternInput]); - const code = `function solution(text: string, pattern: string): number { - if (pattern.length === 0) return 0; - if (text.length === 0) return -1; - - function buildLPS(p: string): number[] { - const lps = new Array(p.length).fill(0); - let len = 0; - for (let i = 1; i < p.length;) { - if (p[i] === p[len]) { - lps[i++] = ++len; - } - else if (len !== 0) { - len = lps[len - 1]; - } - else { - lps[i++] = 0; - } - } - return lps; - } - - const lps = buildLPS(pattern); - let i = 0; - let j = 0; - - while (i < text.length) { - if (text[i] === pattern[j]) { - i++; - j++; - } - if (j === pattern.length) - return i - j; - else if (i < text.length && text[i] !== pattern[j]) { - if (j !== 0) - j = lps[j - 1]; - else - i++; - } - } - return -1; -}`; - - const step = steps[currentStep] || steps[steps.length - 1]; + const step = steps[currentStepIndex] || steps[steps.length - 1]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( - -

KMP Search

- -
-
-

Text Array

-
- {step.text.split('').map((char, idx) => { - const isCurrent = (idx === step.i && (step.phase === 'search' || step.phase === 'match' || step.phase === 'mismatch')); - const isMatchedResult = step.matchedRange && idx >= step.matchedRange[0] && idx <= step.matchedRange[1]; - const isMatch = isCurrent && step.matchStatus === 'match'; - const isMismatch = isCurrent && step.matchStatus === 'mismatch'; - - return ( -
- +
+ +

KMP Search

+ +
+
+

Text Array

+
+ {step.text.split('').map((char, idx) => { + const isCurrent = (idx === step.i && (step.phase === 'search' || step.phase === 'match' || step.phase === 'mismatch')); + const isMatchedResult = step.matchedRange && idx >= step.matchedRange[0] && idx <= step.matchedRange[1]; + const isMatch = isCurrent && step.matchStatus === 'match'; + const isMismatch = isCurrent && step.matchStatus === 'mismatch'; + + return ( +
+ + {char} + + + {isCurrent && ( + +
+ i + + )} + +
+ {idx} +
+
+ ); + })} +
+
+ +
+

Pattern Matching

+
+ {step.pattern.split('').map((char, idx) => { + const isCurrent = idx === step.j && (step.phase === 'search' || step.phase === 'match' || step.phase === 'mismatch'); + const isLPSMatch = step.phase === 'lps' && idx === step.lps_i; + const isLPSLen = step.phase === 'lps' && idx === step.lps_len; + const isMatch = isCurrent && step.matchStatus === 'match'; + const isMismatch = isCurrent && step.matchStatus === 'mismatch'; + + return ( +
+ - {char} - - - {isCurrent && ( - -
- i - - )} - -
- {idx} + : isCurrent + ? "rgb(59, 130, 246)" + : (isLPSMatch || isLPSLen ? "rgb(168, 85, 247)" : "rgba(255, 255, 255, 0.1)"), + scale: isCurrent || isLPSMatch || isLPSLen ? 1.05 : 1, + y: isCurrent ? 2 : 0 + }} + className={`w-10 h-10 border-2 rounded-lg flex items-center justify-center text-sm font-bold transition-all backdrop-blur-md + ${isCurrent ? 'z-10 ring-2 ring-blue-500/20' : 'z-0'} + `} + > + {char} + + + {isCurrent && ( + + j +
+ + )} + {(isLPSMatch || isLPSLen) && ( + + + {isLPSMatch ? 'i' : 'len'} + +
+ + )} + +
+ {idx} +
-
- ); - })} + ); + })} +
-
-
-

Pattern Matching

-
- {step.pattern.split('').map((char, idx) => { - const isCurrent = idx === step.j && (step.phase === 'search' || step.phase === 'match' || step.phase === 'mismatch'); - const isLPSMatch = step.phase === 'lps' && idx === step.lps_i; - const isLPSLen = step.phase === 'lps' && idx === step.lps_len; - const isMatch = isCurrent && step.matchStatus === 'match'; - const isMismatch = isCurrent && step.matchStatus === 'mismatch'; - - return ( -
+
+

LPS (Longest Prefix Suffix) Array

+
+ {step.pattern.split('').map((_, idx) => ( +
+
[{idx}]
- {char} + {step.lps[idx] !== undefined ? step.lps[idx] : 0} - - {isCurrent && ( - - j -
- - )} - {(isLPSMatch || isLPSLen) && ( - - - {isLPSMatch ? 'i' : 'len'} - -
- - )} - -
- {idx} -
- ); - })} -
-
- -
-

LPS (Longest Prefix Suffix) Array

-
- {step.pattern.split('').map((_, idx) => ( -
-
[{idx}]
- - {step.lps[idx] !== undefined ? step.lps[idx] : 0} - -
- ))} + ))} +
-
- - - -
-

Explanation

-

{step.explanation}

- - - + +
+ +
+ +
+

Explanation

+

{step.explanation}

+ + + +
} rightContent={ - setCurrentStepIndex(0)} /> } controls={ } /> diff --git a/src/components/visualizations/algorithms/KaratsubaVisualization.tsx b/src/components/visualizations/algorithms/KaratsubaVisualization.tsx index 7e34edc..1833838 100644 --- a/src/components/visualizations/algorithms/KaratsubaVisualization.tsx +++ b/src/components/visualizations/algorithms/KaratsubaVisualization.tsx @@ -1,10 +1,11 @@ import { useState, useMemo } from 'react'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; import { motion, AnimatePresence } from 'framer-motion'; import { Card } from '@/components/ui/card'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { x: number; @@ -19,322 +20,393 @@ interface Step { halfN?: number; result?: number; explanation: string; - highlightedLines: number[]; + pseudoStep: string; variables: Record; depth: number; callStack: string[]; } +const languages: VisualizationLanguageMap = { + typescript: `function karatsuba(x: number, y: number): number { + const xStr = x.toString(); + const yStr = y.toString(); + const n = Math.max(xStr.length, yStr.length); + if (n <= 1) { + return x * y; + } + const halfN = Math.floor(n / 2); + const a = parseInt(xStr.slice(0, xStr.length - halfN) || '0'); + const b = parseInt(xStr.slice(xStr.length - halfN) || '0'); + const c = parseInt(yStr.slice(0, yStr.length - halfN) || '0'); + const d = parseInt(yStr.slice(yStr.length - halfN) || '0'); + const ac = karatsuba(a, c); + const bd = karatsuba(b, d); + const ad_plus_bc = karatsuba(a + b, c + d) - ac - bd; + const result = ac * Math.pow(10, 2 * halfN) + ad_plus_bc * Math.pow(10, halfN) + bd; + return result; +}`, + python: `def karatsuba(x, y): + if x < 10 or y < 10: + return x * y + n = max(len(str(x)), len(str(y))) + n_half = n // 2 + a = x // (10 ** n_half) + b = x % (10 ** n_half) + c = y // (10 ** n_half) + d = y % (10 ** n_half) + ac = karatsuba(a, c) + bd = karatsuba(b, d) + ad_plus_bc = karatsuba(a + b, c + d) - ac - bd + result = ac * (10 ** (2 * n_half)) + ad_plus_bc * (10 ** n_half) + bd + return result`, + java: `public static long karatsuba(long x, long y) { + if (x < 10 || y < 10) { + return x * y; + } + int n = Math.max(String.valueOf(x).length(), String.valueOf(y).length()); + int halfN = (n + 1) / 2; + long a = x / (long)Math.pow(10, halfN); + long b = x % (long)Math.pow(10, halfN); + long c = y / (long)Math.pow(10, halfN); + long d = y % (long)Math.pow(10, halfN); + long ac = karatsuba(a, c); + long bd = karatsuba(b, d); + long ad_plus_bc = karatsuba(a + b, c + d) - ac - bd; + return ac * (long)Math.pow(10, 2 * halfN) + ad_plus_bc * (long)Math.pow(10, halfN) + bd; +}`, + cpp: `class Solution { +public: +long long karatsuba(long long x, long long y) { + if (x < 10 || y < 10) { + return x * y; + } + int n = max(to_string(x).length(), to_string(y).length()); + int m = n / 2; + long long power = 1; + for (int i = 0; i < m; i++) power *= 10; + long long a = x / power; + long long b = x % power; + long long c = y / power; + long long d = y % power; + long long ac = karatsuba(a, c); + long long bd = karatsuba(b, d); + long long adbc = karatsuba(a + b, c + d) - ac - bd; + long long power2m = power * power; + return ac * power2m + adbc * power + bd; +} +};` +}; + export const KaratsubaVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); - const [inputX, setInputX] = useState(1234); - const [inputY, setInputY] = useState(5678); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [inputX] = useState(1234); + const [inputY] = useState(5678); - const steps: Step[] = useMemo(() => { + const { steps, stepLineNumbers } = useMemo(() => { const s: Step[] = []; + const lines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + lines.typescript!.push(ts); + lines.python!.push(py); + lines.java!.push(java); + lines.cpp!.push(cpp); + }; - const karatsuba = (x: number, y: number, depth: number, stack: string[]): number => { + const runKaratsuba = (x: number, y: number, depth: number, stack: string[]): number => { const currentStack = [...stack, `karatsuba(${x}, ${y})`]; s.push({ x, y, depth, callStack: currentStack, explanation: `Calling karatsuba(x=${x}, y=${y}) at depth ${depth}.`, - highlightedLines: [1], + pseudoStep: `CALL karatsuba(x = ${x}, y = ${y})`, variables: { x, y, depth } }); + addLines(1, 1, 1, 3); const xStr = x.toString(); const yStr = y.toString(); s.push({ x, y, depth, callStack: currentStack, - explanation: `Convert numbers to strings: xStr="${xStr}", yStr="${yStr}".`, - highlightedLines: [2, 3], + explanation: `Convert numbers to strings for digit operations: xStr="${xStr}", yStr="${yStr}".`, + pseudoStep: `SET xStr = "${xStr}", yStr = "${yStr}"`, variables: { xStr, yStr } }); + addLines(2, 4, 5, 7); const n = Math.max(xStr.length, yStr.length); s.push({ x, y, depth, callStack: currentStack, - explanation: `Determine max length n = ${n}.`, - highlightedLines: [5], + explanation: `Determine maximum digit length n = ${n}.`, + pseudoStep: `SET n = max(length(x), length(y)) (n = ${n})`, variables: { xStr, yStr, n } }); + addLines(4, 4, 5, 7); if (n <= 1) { const res = x * y; s.push({ x, y, depth, callStack: currentStack, - explanation: `Base case: n <= 1. Multiply directly: ${x} * ${y} = ${res}.`, - highlightedLines: [7, 8, 9], + explanation: `Base case reached: n <= 1. Multiply directly: ${x} * ${y} = ${res}.`, + pseudoStep: `IF n <= 1 → RETURN x * y (${x} * ${y} = ${res})`, variables: { x, y, result: res } }); + addLines(6, 3, 3, 5); return res; } const halfN = Math.floor(n / 2); s.push({ x, y, depth, callStack: currentStack, halfN, - explanation: `Calculate halfN = floor(${n} / 2) = ${halfN}.`, - highlightedLines: [11], + explanation: `Calculate partition split point halfN = floor(${n} / 2) = ${halfN}.`, + pseudoStep: `SET halfN = n / 2 (halfN = ${halfN})`, variables: { n, halfN } }); + addLines(8, 5, 6, 8); const a = parseInt(xStr.slice(0, xStr.length - halfN) || '0'); const b = parseInt(xStr.slice(xStr.length - halfN) || '0'); s.push({ x, y, depth, callStack: currentStack, halfN, a, b, - explanation: `Split x into a = ${a} and b = ${b}.`, - highlightedLines: [13, 14], + explanation: `Split multiplier x into most significant half a = ${a} and least significant half b = ${b}.`, + pseudoStep: `SET a = x[high] (${a}), b = x[low] (${b})`, variables: { xStr, halfN, a, b } }); + addLines(9, 6, 7, 11); const c = parseInt(yStr.slice(0, yStr.length - halfN) || '0'); const d = parseInt(yStr.slice(yStr.length - halfN) || '0'); s.push({ x, y, depth, callStack: currentStack, halfN, a, b, c, d, - explanation: `Split y into c = ${c} and d = ${d}.`, - highlightedLines: [15, 16], + explanation: `Split multiplier y into most significant half c = ${c} and least significant half d = ${d}.`, + pseudoStep: `SET c = y[high] (${c}), d = y[low] (${d})`, variables: { yStr, halfN, c, d } }); + addLines(11, 8, 9, 13); s.push({ x, y, depth, callStack: currentStack, halfN, a, b, c, d, - explanation: `Recursively compute ac = karatsuba(a=${a}, c=${c}).`, - highlightedLines: [18], + explanation: `Recursively compute product of high halves: ac = karatsuba(a=${a}, c=${c}).`, + pseudoStep: `SET ac = CALL karatsuba(a, c) (a = ${a}, c = ${c})`, variables: { a, c } }); - const ac = karatsuba(a, c, depth + 1, currentStack); + addLines(13, 10, 11, 15); + const ac = runKaratsuba(a, c, depth + 1, currentStack); s.push({ x, y, depth, callStack: currentStack, halfN, a, b, c, d, ac, - explanation: `Recursively compute bd = karatsuba(b=${b}, d=${d}).`, - highlightedLines: [19], + explanation: `Recursively compute product of low halves: bd = karatsuba(b=${b}, d=${d}).`, + pseudoStep: `SET bd = CALL karatsuba(b, d) (b = ${b}, d = ${d})`, variables: { b, d, ac } }); - const bd = karatsuba(b, d, depth + 1, currentStack); + addLines(14, 11, 12, 16); + const bd = runKaratsuba(b, d, depth + 1, currentStack); s.push({ x, y, depth, callStack: currentStack, halfN, a, b, c, d, ac, bd, - explanation: `Compute ad_plus_bc = karatsuba(a+b=${a + b}, c+d=${c + d}) - ac - bd.`, - highlightedLines: [20], + explanation: `Compute combined inner product recursively: (a+b)(c+d) - ac - bd.`, + pseudoStep: `SET ad_plus_bc = CALL karatsuba(a+b, c+d) - ac - bd`, variables: { a_plus_b: a + b, c_plus_d: c + d, ac, bd } }); - const ad_plus_bc = karatsuba(a + b, c + d, depth + 1, currentStack) - ac - bd; + addLines(15, 12, 13, 17); + const ad_plus_bc = runKaratsuba(a + b, c + d, depth + 1, currentStack) - ac - bd; const result = ac * Math.pow(10, 2 * halfN) + ad_plus_bc * Math.pow(10, halfN) + bd; s.push({ x, y, depth, callStack: currentStack, halfN, a, b, c, d, ac, bd, ad_plus_bc, result, - explanation: `Combine results: ${ac} * 10^${2 * halfN} + ${ad_plus_bc} * 10^${halfN} + ${bd} = ${result}.`, - highlightedLines: [22], + explanation: `Combine intermediate products using Karatsuba equation: ${ac}*10^${2 * halfN} + ${ad_plus_bc}*10^${halfN} + ${bd} = ${result}.`, + pseudoStep: `SET result = ac*10^(2*halfN) + (ad+bc)*10^halfN + bd`, variables: { ac, bd, ad_plus_bc, halfN, result } }); + addLines(16, 13, 14, 19); s.push({ x, y, depth, callStack: currentStack, halfN, a, b, c, d, ac, bd, ad_plus_bc, result, - explanation: `Returning result ${result} for karatsuba(${x}, ${y}).`, - highlightedLines: [24], + explanation: `Returning computed product result = ${result} for karatsuba(${x}, ${y}).`, + pseudoStep: `RETURN result (result = ${result})`, variables: { result } }); + addLines(17, 13, 14, 19); return result; }; - karatsuba(inputX, inputY, 0, []); - return s; + runKaratsuba(inputX, inputY, 0, []); + return { steps: s, stepLineNumbers: lines }; }, [inputX, inputY]); - const code = `function karatsuba(x: number, y: number): number { - const xStr = x.toString(); - const yStr = y.toString(); - - const n = Math.max(xStr.length, yStr.length); - - if (n <= 1) { - return x * y; - } - - const halfN = Math.floor(n / 2); - - const a = parseInt(xStr.slice(0, xStr.length - halfN) || '0'); - const b = parseInt(xStr.slice(xStr.length - halfN) || '0'); - const c = parseInt(yStr.slice(0, yStr.length - halfN) || '0'); - const d = parseInt(yStr.slice(yStr.length - halfN) || '0'); - - const ac = karatsuba(a, c); - const bd = karatsuba(b, d); - const ad_plus_bc = karatsuba(a + b, c + d) - ac - bd; - - const result = ac * Math.pow(10, 2 * halfN) + ad_plus_bc * Math.pow(10, halfN) + bd; - - return result; -}`; - - const step = steps[currentStep] || steps[0]; + const step = steps[currentStepIndex] || steps[0]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( - -

Multiplication breakdown

+
+
+ +

Multiplication breakdown

-
- {/* Current numbers being multiplied */} - -
- {step.x} - Multiplier X -
- × -
- {step.y} - Multiplier Y -
-
+
+ +
+ {step.x} + Multiplier X +
+ × +
+ {step.y} + Multiplier Y +
+
- {/* Splitting representation */} - - {step.a !== undefined && ( - -
-
- Split X - 10^{step.halfN} -
-
-
- {step.a} + + {step.a !== undefined && ( + +
+
+ Split X + 10^{step.halfN}
-
- {step.b} +
+
+ {step.a} +
+
+ {step.b} +
+
+
+ a (high) + b (low)
-
- a (high) - b (low) -
-
-
-
- Split Y - 10^{step.halfN} -
-
-
- {step.c} +
+
+ Split Y + 10^{step.halfN}
-
- {step.d} +
+
+ {step.c} +
+
+ {step.d} +
+
+
+ c (high) + d (low)
-
- c (high) - d (low) + + )} + + + + {(step.ac !== undefined || step.bd !== undefined || step.ad_plus_bc !== undefined) && ( + +
+ ac + {step.ac ?? '?'}
-
- - )} - - - {/* Recursive components */} - - {(step.ac !== undefined || step.bd !== undefined || step.ad_plus_bc !== undefined) && ( - -
- ac - {step.ac ?? '?'} -
-
- bd - {step.bd ?? '?'} -
-
- ad+bc - {step.ad_plus_bc ?? '?'} -
-
- )} -
- - {/* Final Result */} - - {step.result !== undefined && ( +
+ bd + {step.bd ?? '?'} +
+
+ ad+bc + {step.ad_plus_bc ?? '?'} +
+ + )} +
+ + + {step.result !== undefined && ( + +
Intermediate Result
+
{step.result}
+
+ )} +
+
+ +
+ +
+ +
+

Algorithm Logic

+
+ {step.callStack.map((_, i) => ( +
+ ))} +
+
+

{step.explanation}

+ + + + + +

+ Recursion Stack + Depth: {step.depth} +

+
+ {step.callStack.map((call, idx) => ( -
Intermediate Result
-
{step.result}
+ {idx} + {call}
- )} - -
-
- - -
-

Algorithm Logic

-
- {step.callStack.map((_, i) => ( -
))}
-
-

{step.explanation}

- - - - - {/* Call Stack Visualization */} - -

- Recursion Stack - Depth: {step.depth} -

-
- {step.callStack.map((call, idx) => ( - - {idx} - {call} - - ))} -
-
+ +
} rightContent={ - setCurrentStepIndex(0)} /> } controls={ } /> diff --git a/src/components/visualizations/algorithms/KnapsackVisualization.tsx b/src/components/visualizations/algorithms/KnapsackVisualization.tsx index 20c7fe8..f1b13f1 100644 --- a/src/components/visualizations/algorithms/KnapsackVisualization.tsx +++ b/src/components/visualizations/algorithms/KnapsackVisualization.tsx @@ -1,8 +1,9 @@ -import React, { useEffect, useRef, useState } from "react"; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; -import { StepControls } from "../shared/StepControls"; -import { VariablePanel } from "../shared/VariablePanel"; +import React, { useState } from 'react'; +import { Info } from 'lucide-react'; +import { SimpleStepControls } from '../shared/SimpleStepControls'; +import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { dp: number[][]; @@ -11,28 +12,19 @@ interface Step { coinValue: number; w: number; value: number; - message: string; - lineNumber: number; + explanation: string; + pseudoStep: string; } -export const KnapsackVisualization: React.FC = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1000); - const intervalRef = useRef(null); - - const code = `function change(amount: number, coins: number[]): number { +const languages: VisualizationLanguageMap = { + typescript: `function change(amount: number, coins: number[]): number { let dp: number[] = new Array(amount + 1).fill(0); dp[0] = 1; - for (let i = coins.length - 1; i >= 0; i--) { const nextDP: number[] = new Array(amount + 1).fill(0); nextDP[0] = 1; - for (let a = 1; a <= amount; a++) { nextDP[a] = dp[a]; - if (a - coins[i] >= 0) { nextDP[a] += nextDP[a - coins[i]]; } @@ -40,186 +32,215 @@ export const KnapsackVisualization: React.FC = () => { dp = nextDP; } return dp[amount]; -}`; +}`, - const generateSteps = () => { - const coins = [1, 2, 5]; - const amount = 5; - const n = coins.length; + python: `def change(amount: int, coins: list[int]) -> int: + dp = [0] * (amount + 1) + dp[0] = 1 + for i in range(len(coins) - 1, -1, -1): + next_dp = [0] * (amount + 1) + next_dp[0] = 1 + for a in range(1, amount + 1): + next_dp[a] = dp[a] + if a - coins[i] >= 0: + next_dp[a] += next_dp[a - coins[i]] + dp = next_dp + return dp[amount]`, - let historicalDP: number[][] = []; - let rowLabels: string[] = ["Init"]; - const newSteps: Step[] = []; + java: `public static class Solution { + public int change(int amount, int[] coins) { + int[] dp = new int[amount + 1]; + dp[0] = 1; + for (int i = coins.length - 1; i >= 0; i--) { + int[] nextDP = new int[amount + 1]; + nextDP[0] = 1; + for (int a = 1; a <= amount; a++) { + nextDP[a] = dp[a]; + if (a - coins[i] >= 0) { + nextDP[a] += nextDP[a - coins[i]]; + } + } + dp = nextDP; + } + return dp[amount]; + } +}`, - let currentDP: number[] = new Array(amount + 1).fill(0); - currentDP[0] = 1; - historicalDP.push([...currentDP]); + cpp: `class Solution { +public: + int change(int amount, vector& coins) { + vector dp(amount + 1, 0); + dp[0] = 1; + for (int i = coins.size() - 1; i >= 0; i--) { + vector nextDP(amount + 1, 0); + nextDP[0] = 1; + for (int a = 1; a <= amount; a++) { + nextDP[a] = dp[a]; + if (a - coins[i] >= 0) { + nextDP[a] += nextDP[a - coins[i]]; + } + } + dp = nextDP; + } + return dp[amount]; + } +};` +}; + +function generateVisualizationData() { + const coins = [1, 2, 5]; + const amount = 5; + const n = coins.length; + + const historicalDP: number[][] = []; + const rowLabels: string[] = ['Init']; + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - newSteps.push({ + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + let currentDP: number[] = new Array(amount + 1).fill(0); + currentDP[0] = 1; + historicalDP.push([...currentDP]); + + steps.push({ + dp: historicalDP.map((row) => [...row]), + rowLabels: [...rowLabels], + i: -1, + coinValue: 0, + w: 0, + value: currentDP[0], + explanation: 'Initialize dp array. Base case: there is 1 way to make amount 0 using no coins.', + pseudoStep: 'SET dp[0] = 1', + }); + addLines(3, 3, 4, 5); + + for (let i = n - 1; i >= 0; i--) { + const coin = coins[i]; + const nextDP: number[] = new Array(amount + 1).fill(0); + nextDP[0] = 1; + + historicalDP.push([...nextDP]); + rowLabels.push(`Coin ${coin}`); + + steps.push({ dp: historicalDP.map((row) => [...row]), rowLabels: [...rowLabels], - i: -1, - coinValue: 0, + i, + coinValue: coin, w: 0, - value: currentDP[0], - message: "Initialize dp array. Base case: 1 way to make amount 0.", - lineNumber: 3, + value: nextDP[0], + explanation: `Process coin ${coin}. Initialize nextDP[0] to 1 because there is always 1 way to make amount 0.`, + pseudoStep: `SET nextDP[0] = 1 FOR coin = ${coin}`, }); + addLines(6, 5, 6, 7); - for (let i = n - 1; i >= 0; i--) { - const coin = coins[i]; - const nextDP: number[] = new Array(amount + 1).fill(0); - nextDP[0] = 1; - - historicalDP.push([...nextDP]); - rowLabels.push(`Coin ${coin}`); + for (let a = 1; a <= amount; a++) { + nextDP[a] = currentDP[a]; + historicalDP[historicalDP.length - 1][a] = nextDP[a]; - newSteps.push({ + steps.push({ dp: historicalDP.map((row) => [...row]), rowLabels: [...rowLabels], i, coinValue: coin, - w: 0, - value: nextDP[0], - message: `Processing coin ${coin}: Initialize nextDP base case for 0 amount.`, - lineNumber: 7, + w: a, + value: nextDP[a], + explanation: `Amount ${a}: Exclude coin ${coin}. Inherit ${currentDP[a]} ways from the previous DP row.`, + pseudoStep: `SET nextDP[${a}] = dp[${a}]`, }); + addLines(10, 8, 9, 10); - for (let a = 1; a <= amount; a++) { - nextDP[a] = currentDP[a]; + if (a - coin >= 0) { + nextDP[a] += nextDP[a - coin]; historicalDP[historicalDP.length - 1][a] = nextDP[a]; - newSteps.push({ + steps.push({ dp: historicalDP.map((row) => [...row]), rowLabels: [...rowLabels], i, coinValue: coin, w: a, value: nextDP[a], - message: `Amount ${a}: Exclude coin ${coin}, inherit ${currentDP[a]} ways from prev DP row.`, - lineNumber: 10, + explanation: `Amount ${a}: Include coin ${coin}. Add nextDP[${a - coin}] (${nextDP[a - coin]} ways) -> Total: ${nextDP[a]}`, + pseudoStep: `SET nextDP[${a}] = nextDP[${a}] + nextDP[${a - coin}]`, }); - - if (a - coin >= 0) { - nextDP[a] += nextDP[a - coin]; - historicalDP[historicalDP.length - 1][a] = nextDP[a]; - - newSteps.push({ - dp: historicalDP.map((row) => [...row]), - rowLabels: [...rowLabels], - i, - coinValue: coin, - w: a, - value: nextDP[a], - message: `Amount ${a}: Include coin ${coin}, adding ${nextDP[a - coin]} ways -> Total: ${nextDP[a]}`, - lineNumber: 13, - }); - } + addLines(13, 10, 11, 12); } - - currentDP = [...nextDP]; - newSteps.push({ - dp: historicalDP.map((row) => [...row]), - rowLabels: [...rowLabels], - i, - coinValue: coin, - w: amount, - value: currentDP[amount], - message: `Finished passes for coin ${coin}, dp = nextDP.`, - lineNumber: 16, - }); } - newSteps.push({ + currentDP = [...nextDP]; + steps.push({ dp: historicalDP.map((row) => [...row]), rowLabels: [...rowLabels], - i: -1, - coinValue: 0, + i, + coinValue: coin, w: amount, value: currentDP[amount], - message: `Finished computations. Total ways to make ${amount} is ${currentDP[amount]}.`, - lineNumber: 18, + explanation: `Finished passes for coin ${coin}. Copy nextDP back to dp for the next iteration.`, + pseudoStep: 'SET dp = nextDP', }); + addLines(16, 11, 14, 15); + } - setSteps(newSteps); - setCurrentStepIndex(0); - }; + steps.push({ + dp: historicalDP.map((row) => [...row]), + rowLabels: [...rowLabels], + i: -1, + coinValue: 0, + w: amount, + value: currentDP[amount], + explanation: `All coins processed. The final cell dp[${amount}] holds the total distinct combinations: ${currentDP[amount]}.`, + pseudoStep: `RETURN dp[${amount}]`, + }); + addLines(18, 12, 16, 17); - useEffect(() => { - generateSteps(); - }, []); - - useEffect(() => { - if (isPlaying && currentStepIndex < steps.length - 1) { - intervalRef.current = window.setInterval(() => { - setCurrentStepIndex((prev) => { - if (prev >= steps.length - 1) { - setIsPlaying(false); - return prev; - } - return prev + 1; - }); - }, speed); - } else { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - } + return { steps, stepLineNumbers }; +} + +export const KnapsackVisualization: React.FC = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - }; - }, [isPlaying, currentStepIndex, steps.length, speed]); - - const handlePlay = () => setIsPlaying(true); - const handlePause = () => setIsPlaying(false); - const handleStepForward = () => { - if (currentStepIndex < steps.length - 1) { - setCurrentStepIndex(currentStepIndex + 1); - } - }; - const handleStepBack = () => { - if (currentStepIndex > 0) { - setCurrentStepIndex(currentStepIndex - 1); - } - }; const handleReset = () => { setCurrentStepIndex(0); - setIsPlaying(false); }; - if (steps.length === 0) return null; - const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map((s) => s.pseudoStep); return ( -
- + -
-
-
-

Ways DP Table

-
- +
+ {/* Left Column: Visual State */} +
+
+

Ways DP Table

+
+
- + {currentStep.dp[0].map((_, w) => ( - ))} @@ -228,53 +249,103 @@ export const KnapsackVisualization: React.FC = () => { {currentStep.dp.map((row, rowIdx) => ( - - {row.map((val, a) => ( - - ))} + {row.map((val, a) => { + const isCurrentCell = rowIdx === currentStep.dp.length - 1 && a === currentStep.w; + const isPrevRowCell = rowIdx === currentStep.dp.length - 2 && a === currentStep.w; + const isSubtractCell = + rowIdx === currentStep.dp.length - 1 && + currentStep.coinValue > 0 && + a === currentStep.w - currentStep.coinValue; + + let bgClass = ''; + if (isCurrentCell) { + bgClass = 'bg-primary/20 font-bold text-primary'; + } else if (isPrevRowCell) { + bgClass = 'bg-blue-500/10 italic text-blue-600 dark:text-blue-400'; + } else if (isSubtractCell) { + bgClass = 'bg-green-500/20 font-bold text-green-600 dark:text-green-400'; + } else if (val > 0) { + bgClass = 'bg-muted/30 text-foreground/80'; + } else { + bgClass = 'text-muted-foreground/40'; + } + + return ( + + ); + })} ))}
Coins \ Amt + Coins \ Amt + + {w}
+ {currentStep.rowLabels[rowIdx]} 0 && a === currentStep.w - currentStep.coinValue - ? "bg-green-500/20 font-bold text-green-500" - : val > 0 - ? "bg-green-500/5" - : "" - } `} - > - {val} - + {val} +
+
-
-

{currentStep.message}

-
-
- + {/* Commentary Panel */} +
+
+
+
+ + + + + + Algorithm Commentary + +
+
+ Step {currentStepIndex + 1} of {steps.length} +
+
+ +
+
+ +
+
+

+ Current Action +

+
+ {currentStep.explanation} +
+
+
+ +
- + {/* Right Column: Code Display */} +
+ +
); }; + +export default KnapsackVisualization; diff --git a/src/components/visualizations/algorithms/KruskalsVisualization.tsx b/src/components/visualizations/algorithms/KruskalsVisualization.tsx index 7add663..91be75c 100644 --- a/src/components/visualizations/algorithms/KruskalsVisualization.tsx +++ b/src/components/visualizations/algorithms/KruskalsVisualization.tsx @@ -1,8 +1,8 @@ -import { useEffect, useRef, useState } from "react"; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; -import { StepControls } from "../shared/StepControls"; -import { VariablePanel } from "../shared/VariablePanel"; +import { useEffect, useRef, useState } from 'react'; +import { StepControls } from '../shared/StepControls'; +import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Edge { from: number; @@ -17,42 +17,29 @@ interface Step { parent: number[]; mstEdges: Edge[]; currentEdge: Edge | null; - message: string; - lineNumber: number; + explanation: string; + pseudoStep: string; + variables: Record; } -export const KruskalsVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const intervalRef = useRef(null); - - const code = `function minCostConnectPoints(points: number[][]): number { +const languages: VisualizationLanguageMap = { + typescript: `function minCostConnectPoints(points: number[][]): number { const n = points.length; - function manhattanDistance(p1: number[], p2: number[]): number { return Math.abs(p1[0] - p2[0]) + Math.abs(p1[1] - p2[1]); } - const edges: number[][] = []; for (let i = 0; i < n; i++) { for (let j = i + 1; j < n; j++) { edges.push([i, j, manhattanDistance(points[i], points[j])]); } } - edges.sort((a, b) => a[2] - b[2]); - const parent: number[] = Array(n).fill(0).map((_, i) => i); - function find(i: number): number { - if (parent[i] === i) { - return i; - } + if (parent[i] === i) return i; return parent[i] = find(parent[i]); } - function union(i: number, j: number): void { const rootI = find(i); const rootJ = find(j); @@ -60,196 +47,434 @@ export const KruskalsVisualization = () => { parent[rootI] = rootJ; } } - let mstCost = 0; let edgesUsed = 0; - for (const edge of edges) { const u = edge[0]; const v = edge[1]; const weight = edge[2]; - if (find(u) !== find(v)) { union(u, v); mstCost += weight; edgesUsed++; - if (edgesUsed === n - 1) { - break; + if (edgesUsed === n - 1) break; + } + } + return mstCost; +}`, + python: `def min_cost_connect_points(points: list[list[int]]) -> int: + def manhattan_distance(p1, p2): + return abs(p1[0] - p2[0]) + abs(p1[1] - p2[1]) + n = len(points) + edges = [] + for i in range(n): + for j in range(i + 1, n): + edges.append((manhattan_distance(points[i], points[j]), i, j)) + edges.sort() + parent = list(range(n)) + def find(i): + if parent[i] == i: + return i + parent[i] = find(parent[i]) + return parent[i] + def union(i, j): + root_i = find(i) + root_j = find(j) + if root_i != root_j: + parent[root_i] = root_j + return True + return False + min_cost = 0 + num_edges = 0 + for cost, i, j in edges: + if union(i, j): + min_cost += cost + num_edges += 1 + if num_edges == n - 1: + break + return min_cost`, + java: `class Solution { + private int find(int i, int[] parent) { + if (parent[i] == i) return i; + return parent[i] = find(parent[i], parent); + } + private boolean union(int i, int j, int[] parent) { + int rootI = find(i, parent); + int rootJ = find(j, parent); + if (rootI != rootJ) { + parent[rootI] = rootJ; + return true; + } + return false; + } + public int minCostConnectPoints(int[][] points) { + int n = points.length; + List edges = new ArrayList<>(); + for (int i = 0; i < n; i++) { + for (int j = i + 1; j < n; j++) { + int dist = Math.abs(points[i][0] - points[j][0]) + Math.abs(points[i][1] - points[j][1]); + edges.add(new int[]{dist, i, j}); } } + edges.sort((a, b) -> a[0] - b[0]); + int[] parent = new int[n]; + for (int i = 0; i < n; i++) parent[i] = i; + int mstCost = 0; + int edgesUsed = 0; + for (int[] edge : edges) { + if (union(edge[1], edge[2], parent)) { + mstCost += edge[0]; + edgesUsed++; + if (edgesUsed == n - 1) break; + } + } + return mstCost; + } +}`, + cpp: `class Solution { +private: + int find(int i, vector& parent) { + if (parent[i] == i) return i; + return parent[i] = find(parent[i], parent); + } + bool unite(int i, int j, vector& parent) { + int rootI = find(i, parent); + int rootJ = find(j, parent); + if (rootI != rootJ) { + parent[rootI] = rootJ; + return true; + } + return false; + } +public: + int minCostConnectPoints(vector>& points) { + int n = points.size(); + vector> edges; + for (int i = 0; i < n; i++) { + for (int j = i + 1; j < n; j++) { + int dist = abs(points[i][0] - points[j][0]) + abs(points[i][1] - points[j][1]); + edges.push_back({dist, i, j}); + } + } + sort(edges.begin(), edges.end()); + vector parent(n); + for (int i = 0; i < n; i++) parent[i] = i; + int mstCost = 0; + int edgesUsed = 0; + for (auto& edge : edges) { + if (unite(edge[1], edge[2], parent)) { + mstCost += edge[0]; + edgesUsed++; + if (edgesUsed == n - 1) break; + } + } + return mstCost; } +};`, +}; - return mstCost; -} -`; - - const generateSteps = () => { - const points = [ - [0, 0], - [2, 2], - [3, 10], - [5, 2], - [7, 0], - ]; - const n = points.length; - const allEdges: Edge[] = []; +function generateVisualizationData() { + const points = [ + [0, 0], + [2, 2], + [3, 10], + [5, 2], + [7, 0], + ]; + const n = points.length; + const allEdges: Edge[] = []; - // All possible edges - for (let i = 0; i < n; i++) { - for (let j = i + 1; j < n; j++) { - const cost = - Math.abs(points[i][0] - points[j][0]) + - Math.abs(points[i][1] - points[j][1]); - allEdges.push({ from: i, to: j, weight: cost }); - } + for (let i = 0; i < n; i++) { + for (let j = i + 1; j < n; j++) { + const cost = Math.abs(points[i][0] - points[j][0]) + Math.abs(points[i][1] - points[j][1]); + allEdges.push({ from: i, to: j, weight: cost }); } + } + + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { typescript: [], python: [], java: [], cpp: [] }; + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - const newSteps: Step[] = []; - let parent = Array(n) - .fill(0) - .map((_, i) => i); - let mstEdges: Edge[] = []; + steps.push({ + edges: allEdges.map(e => ({ ...e })), + parent: Array.from({ length: n }, (_, i) => i), + mstEdges: [], + currentEdge: null, + explanation: `Calculate Manhattan distances for all ${allEdges.length} possible edges between the ${n} points.`, + pseudoStep: 'SET edges = calculate_all_edges(points)', + variables: { mstCost: 0, edgesUsed: 0, parent: Array.from({ length: n }, (_, i) => i).join(', ') } + }); + addLines(6, 5, 17, 19); + + const sortedEdges = [...allEdges].sort((a, b) => a.weight - b.weight); + + steps.push({ + edges: sortedEdges.map(e => ({ ...e })), + parent: Array.from({ length: n }, (_, i) => i), + mstEdges: [], + currentEdge: null, + explanation: 'Sort all edges in ascending order of their weights.', + pseudoStep: 'edges.sort()', + variables: { mstCost: 0, edgesUsed: 0 } + }); + addLines(12, 9, 24, 26); + + const parent = Array.from({ length: n }, (_, i) => i); + const mstEdges: Edge[] = []; + let mstCost = 0; + let edgesUsed = 0; + + const traceFind = (i: number, edge: Edge, idx: number, label: string): number => { + steps.push({ + edges: sortedEdges.map((e, index) => ({ + ...e, + considered: index === idx, + selected: mstEdges.some(me => (me.from === e.from && me.to === e.to)) + })), + parent: [...parent], + mstEdges: [...mstEdges], + currentEdge: { ...edge }, + explanation: `find(${label} Node ${i}): start searching for its root representative.`, + pseudoStep: `CALL find(${i})`, + variables: { u: edge.from, v: edge.to, weight: edge.weight, mstCost, edgesUsed, node: i } + }); + addLines(14, 11, 2, 3); + + if (parent[i] === i) { + steps.push({ + edges: sortedEdges.map((e, index) => ({ + ...e, + considered: index === idx, + selected: mstEdges.some(me => (me.from === e.from && me.to === e.to)) + })), + parent: [...parent], + mstEdges: [...mstEdges], + currentEdge: { ...edge }, + explanation: `Node ${i} is its own parent. Return root = ${i}.`, + pseudoStep: `RETURN ${i}`, + variables: { u: edge.from, v: edge.to, weight: edge.weight, mstCost, edgesUsed, node: i, root: i } + }); + addLines(15, 12, 3, 4); + return i; + } - newSteps.push({ - edges: allEdges.map((e) => ({ ...e })), + const oldParent = parent[i]; + steps.push({ + edges: sortedEdges.map((e, index) => ({ + ...e, + considered: index === idx, + selected: mstEdges.some(me => (me.from === e.from && me.to === e.to)) + })), parent: [...parent], - mstEdges: [], - currentEdge: null, - message: "Create all possible edges using Manhattan distance", - lineNumber: 11, + mstEdges: [...mstEdges], + currentEdge: { ...edge }, + explanation: `Node ${i} parent is ${oldParent} (not itself). Recurse to find root of ${oldParent}.`, + pseudoStep: `find(parent[${i}] = ${oldParent})`, + variables: { u: edge.from, v: edge.to, weight: edge.weight, mstCost, edgesUsed, node: i, parent: oldParent } }); + addLines(16, 14, 4, 5); - const sortedEdges = [...allEdges].sort((a, b) => a.weight - b.weight); + const root = traceFind(oldParent, edge, idx, label); + parent[i] = root; - newSteps.push({ - edges: sortedEdges.map((e) => ({ ...e })), + steps.push({ + edges: sortedEdges.map((e, index) => ({ + ...e, + considered: index === idx, + selected: mstEdges.some(me => (me.from === e.from && me.to === e.to)) + })), parent: [...parent], - mstEdges: [], - currentEdge: null, - message: "Sort edges by weight (ascending)", - lineNumber: 15, + mstEdges: [...mstEdges], + currentEdge: { ...edge }, + explanation: `Path compression: set parent of node ${i} directly to root ${root}.`, + pseudoStep: `SET parent[${i}] = ${root}`, + variables: { u: edge.from, v: edge.to, weight: edge.weight, mstCost, edgesUsed, node: i, parent: root } }); + addLines(16, 14, 4, 5); - const find = (x: number, p: number[]): number => { - if (p[x] !== x) p[x] = find(p[x], p); - return p[x]; - }; + return root; + }; + + for (let idx = 0; idx < sortedEdges.length; idx++) { + const edge = sortedEdges[idx]; + const u = edge.from; + const v = edge.to; + + steps.push({ + edges: sortedEdges.map((e, index) => ({ + ...e, + considered: index === idx, + selected: mstEdges.some(me => (me.from === e.from && me.to === e.to)) + })), + parent: [...parent], + mstEdges: [...mstEdges], + currentEdge: { ...edge }, + explanation: `Inspect edge (${u} - ${v}) with weight ${edge.weight}. Run Union-Find checks.`, + pseudoStep: `FOR edge IN edges: check if find(${u}) != find(${v})`, + variables: { u, v, weight: edge.weight, mstCost, edgesUsed } + }); + addLines(27, 25, 29, 31); - let totalCost = 0, - edgesUsed = 0; + const rootU = traceFind(u, edge, idx, 'u'); + const rootV = traceFind(v, edge, idx, 'v'); - for (const edge of sortedEdges) { - newSteps.push({ - edges: sortedEdges.map((e) => ({ ...e, considered: e === edge })), + steps.push({ + edges: sortedEdges.map((e, index) => ({ + ...e, + considered: index === idx, + selected: mstEdges.some(me => (me.from === e.from && me.to === e.to)) + })), + parent: [...parent], + mstEdges: [...mstEdges], + currentEdge: { ...edge }, + explanation: `Compare roots: root of ${u} is ${rootU}, root of ${v} is ${rootV}.`, + pseudoStep: `IF root(${u}) != root(${v}) → ${rootU !== rootV ? 'True' : 'False'}`, + variables: { u, v, rootU, rootV, weight: edge.weight, mstCost, edgesUsed } + }); + addLines(31, 26, 30, 32); + + if (rootU !== rootV) { + parent[rootU] = rootV; + mstEdges.push({ ...edge }); + mstCost += edge.weight; + edgesUsed++; + + steps.push({ + edges: sortedEdges.map((e, index) => ({ + ...e, + selected: mstEdges.some(me => (me.from === e.from && me.to === e.to)) + })), parent: [...parent], mstEdges: [...mstEdges], - currentEdge: edge, - message: `Consider edge ${edge.from}-${edge.to} (weight: ${edge.weight})`, - lineNumber: 37, + currentEdge: { ...edge }, + explanation: `Roots are different. Union sets by attaching root ${rootU} to root ${rootV}.`, + pseudoStep: `union(${u}, ${v}): SET parent[${rootU}] = ${rootV}`, + variables: { u, v, weight: edge.weight, mstCost, edgesUsed } }); + addLines(32, 26, 30, 32); - const pCopy = [...parent]; - const rootU = find(edge.from, pCopy); - const rootV = find(edge.to, pCopy); - - if (rootU !== rootV) { - parent[rootU] = rootV; - mstEdges.push({ ...edge, selected: true }); - totalCost += edge.weight; - edgesUsed++; - - newSteps.push({ - edges: sortedEdges.map((e) => ({ ...e })), - parent: [...parent], - mstEdges: [...mstEdges], - currentEdge: edge, - message: `Union: root ${rootU} connected to ${rootV}. Add to MST. (Total: ${totalCost})`, - lineNumber: 43, - }); + steps.push({ + edges: sortedEdges.map((e, index) => ({ + ...e, + selected: mstEdges.some(me => (me.from === e.from && me.to === e.to)) + })), + parent: [...parent], + mstEdges: [...mstEdges], + currentEdge: { ...edge }, + explanation: `Add edge (${u} - ${v}) to MST. Current cost is ${mstCost}.`, + pseudoStep: `SET mstCost += ${edge.weight} → ${mstCost}`, + variables: { u, v, weight: edge.weight, mstCost, edgesUsed } + }); + addLines(33, 27, 31, 33); - if (edgesUsed === n - 1) { - newSteps.push({ - edges: sortedEdges.map((e) => ({ ...e })), - parent: [...parent], - mstEdges: [...mstEdges], - currentEdge: edge, - message: `n - 1 edges reached, break`, - lineNumber: 47, - }); - break; - } - } else { - newSteps.push({ - edges: sortedEdges.map((e) => ({ ...e })), + steps.push({ + edges: sortedEdges.map((e, index) => ({ + ...e, + selected: mstEdges.some(me => (me.from === e.from && me.to === e.to)) + })), + parent: [...parent], + mstEdges: [...mstEdges], + currentEdge: { ...edge }, + explanation: `Edges in MST is now ${edgesUsed}. Check completion.`, + pseudoStep: `IF edgesUsed (${edgesUsed}) == n - 1 (${n - 1})`, + variables: { u, v, weight: edge.weight, mstCost, edgesUsed } + }); + addLines(35, 29, 33, 35); + + if (edgesUsed === n - 1) { + steps.push({ + edges: sortedEdges.map((e) => ({ + ...e, + selected: mstEdges.some(me => (me.from === e.from && me.to === e.to)) + })), parent: [...parent], mstEdges: [...mstEdges], - currentEdge: edge, - message: `Roots match (${rootU}), skip edge ${edge.from}-${edge.to} to avoid cycle`, - lineNumber: 42, + currentEdge: null, + explanation: `All ${n - 1} edges added. Minimum Spanning Tree is complete!`, + pseudoStep: 'BREAK', + variables: { mstCost, edgesUsed } }); + addLines(35, 30, 33, 35); + break; } + } else { + steps.push({ + edges: sortedEdges.map((e, index) => ({ + ...e, + selected: mstEdges.some(me => (me.from === e.from && me.to === e.to)) + })), + parent: [...parent], + mstEdges: [...mstEdges], + currentEdge: { ...edge }, + explanation: `Roots are the same (${rootU}). Adding this edge forms a cycle, so skip it.`, + pseudoStep: 'SKIP edge (Cycle detected)', + variables: { u, v, weight: edge.weight, mstCost, edgesUsed } + }); + addLines(31, 26, 30, 32); } + } + + steps.push({ + edges: sortedEdges.map((e) => ({ + ...e, + selected: mstEdges.some(me => (me.from === e.from && me.to === e.to)) + })), + parent: [...parent], + mstEdges: [...mstEdges], + currentEdge: null, + explanation: `Kruskal's algorithm finished. Total MST cost is ${mstCost}.`, + pseudoStep: `RETURN mstCost → ${mstCost}`, + variables: { mstCost, edgesUsed } + }); + addLines(38, 31, 36, 38); + + return { steps, stepLineNumbers }; +} - newSteps.push({ - edges: sortedEdges.map((e) => ({ ...e })), - parent: [...parent], - mstEdges: [...mstEdges], - currentEdge: null, - message: `MST complete! Total cost: ${totalCost}`, - lineNumber: 52, - }); - - setSteps(newSteps); - setCurrentStepIndex(0); - }; +const nodePositions = [ + { x: 50, y: 230 }, + { x: 130, y: 190 }, + { x: 170, y: 50 }, + { x: 250, y: 190 }, + { x: 350, y: 230 }, +]; - useEffect(() => { - generateSteps(); - }, []); +export const KruskalsVisualization = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { intervalRef.current = setInterval(() => { - setCurrentStepIndex((prev) => { - if (prev >= steps.length - 1) { - setIsPlaying(false); - return prev; - } + setCurrentStepIndex(prev => { + if (prev >= steps.length - 1) { setIsPlaying(false); return prev; } return prev + 1; }); }, 1000 / speed); } else { if (intervalRef.current) clearInterval(intervalRef.current); } - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - }; + return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [isPlaying, currentStepIndex, steps.length, speed]); const handlePlay = () => setIsPlaying(true); const handlePause = () => setIsPlaying(false); - const handleStepForward = () => - currentStepIndex < steps.length - 1 && - setCurrentStepIndex((prev) => prev + 1); - const handleStepBack = () => - currentStepIndex > 0 && setCurrentStepIndex((prev) => prev - 1); - const handleReset = () => { - setCurrentStepIndex(0); - setIsPlaying(false); - generateSteps(); - }; + const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(p => p + 1); + const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(p => p - 1); + const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); }; if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; - - const nodePositions = [ - { x: 50, y: 230 }, // [0,0] - { x: 150, y: 190 }, // [2,2] - { x: 200, y: 30 }, // [3,10] - { x: 300, y: 190 }, // [5,2] - { x: 350, y: 230 }, // [7,0] - ]; + const pseudoSteps = steps.map(s => s.pseudoStep); return (
@@ -268,26 +493,18 @@ export const KruskalsVisualization = () => {
-
- +
+ {currentStep.edges.map((edge, idx) => { const from = nodePositions[edge.from]; const to = nodePositions[edge.to]; const inMST = currentStep.mstEdges.some( - (e) => - (e.from === edge.from && e.to === edge.to) || - (e.from === edge.to && e.to === edge.from), + (e) => (e.from === edge.from && e.to === edge.to) || (e.from === edge.to && e.to === edge.from) ); const isCurrent = currentStep.currentEdge && - ((currentStep.currentEdge.from === edge.from && - currentStep.currentEdge.to === edge.to) || - (currentStep.currentEdge.from === edge.to && - currentStep.currentEdge.to === edge.from)); + ((currentStep.currentEdge.from === edge.from && currentStep.currentEdge.to === edge.to) || + (currentStep.currentEdge.from === edge.to && currentStep.currentEdge.to === edge.from)); return ( @@ -296,20 +513,20 @@ export const KruskalsVisualization = () => { y1={from.y} x2={to.x} y2={to.y} - className={`transition-all duration-300 ${inMST - ? "stroke-green-500" + className={`transition-all duration-200 ${inMST + ? 'stroke-green-500' : isCurrent - ? "stroke-primary" - : "stroke-muted-foreground/50" + ? 'stroke-primary stroke-2' + : 'stroke-muted-foreground/30' }`} strokeWidth={inMST ? 3 : isCurrent ? 3 : 1} - strokeDasharray={inMST ? "0" : "4"} + strokeDasharray={inMST ? '0' : '4'} /> - {inMST && ( + {(inMST || isCurrent) && ( {edge.weight} @@ -333,7 +550,7 @@ export const KruskalsVisualization = () => { y={pos.y} textAnchor="middle" dy=".3em" - className="fill-foreground text-[10px] font-" + className="fill-foreground text-[10px] font-bold" > {idx} @@ -348,34 +565,35 @@ export const KruskalsVisualization = () => { ))} -
-
-

- {currentStep.message} -

+
+ MST Edge + Current Check + Vertex +
-
- sum + e.weight, - 0, - ), - "Union-Find (Parent)": currentStep.parent.join(", "), - }} - /> + +
+

{currentStep.explanation}

-
-
- sum + e.weight, 0), + "Union-Find": currentStep.parent.join(', '), + ...currentStep.variables + }} />
+ +
); diff --git a/src/components/visualizations/algorithms/KthLargestVisualization.tsx b/src/components/visualizations/algorithms/KthLargestVisualization.tsx index b9414b3..009660d 100644 --- a/src/components/visualizations/algorithms/KthLargestVisualization.tsx +++ b/src/components/visualizations/algorithms/KthLargestVisualization.tsx @@ -1,10 +1,10 @@ -import { useState, useEffect } from 'react'; -import { SimpleStepControls } from '../shared/SimpleStepControls'; -import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; -import { VisualizationLayout } from '../shared/VisualizationLayout'; -import { motion, AnimatePresence } from 'framer-motion'; -import { Card } from '@/components/ui/card'; +import React, { useState, useEffect, useRef } from "react"; +import { StepControls } from "../shared/StepControls"; +import { VariablePanel } from "../shared/VariablePanel"; +import { VisualizationCodePanel } from "../shared/VisualizationCodePanel"; +import { motion, AnimatePresence } from "framer-motion"; +import { Card } from "@/components/ui/card"; +import type { StepLineNumberMap, VisualizationLanguageMap } from "@/types/visualization"; interface Step { nums: number[]; @@ -14,228 +14,361 @@ interface Step { i: number; pivot: number | null; explanation: string; - highlightedLines: number[]; variables: Record; + pseudoStep: string; } -export const KthLargestVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); - const [steps, setSteps] = useState([]); - const initialNums = [3, 2, 1, 5, 6, 4]; - const targetK = 2; - - const code = `function findKthLargest(nums: number[], k: number): number { - k = nums.length - k; - - function quickSelect(l: number, r: number): number { - const pivot = nums[r]; - let p = l; - - for (let i = l; i < r; i++) { - if (nums[i] <= pivot) { - [nums[p], nums[i]] = [nums[i], nums[p]]; - p++; - } +const languages: VisualizationLanguageMap = { + typescript: `function findKthLargest(nums: number[], k: number): number { + k = nums.length - k; + function quickSelect(l: number, r: number): number { + const pivot = nums[r]; + let p = l; + for (let i = l; i < r; i++) { + if (nums[i] <= pivot) { + [nums[p], nums[i]] = [nums[i], nums[p]]; + p++; + } + } + [nums[p], nums[r]] = [nums[r], nums[p]]; + if (p > k) { + return quickSelect(l, p - 1); + } else if (p < k) { + return quickSelect(p + 1, r); + } else { + return nums[p]; + } } - - [nums[p], nums[r]] = [nums[r], nums[p]]; - - if (p > k) { - return quickSelect(l, p - 1); - } else if (p < k) { - return quickSelect(p + 1, r); - } else { - return nums[p]; + return quickSelect(0, nums.length - 1); +}`, + python: `def findKthLargest(nums: list[int], k: int) -> int: + k = len(nums) - k + def quickSelect(l: int, r: int) -> int: + pivot = nums[r] + p = l + for i in range(l, r): + if nums[i] <= pivot: + nums[p], nums[i] = nums[i], nums[p] + p += 1 + nums[p], nums[r] = nums[r], nums[p] + if p > k: + return quickSelect(l, p - 1) + elif p < k: + return quickSelect(p + 1, r) + else: + return nums[p] + return quickSelect(0, len(nums) - 1)`, + java: `public static class Solution { + public int findKthLargest(int[] nums, int k) { + k = nums.length - k; + return quickSelect(nums, 0, nums.length - 1, k); } - } + private int quickSelect(int[] nums, int l, int r, int k) { + int pivot = nums[r]; + int p = l; + for (int i = l; i < r; i++) { + if (nums[i] <= pivot) { + swap(nums, p, i); + p++; + } + } + swap(nums, p, r); + if (p > k) { + return quickSelect(nums, l, p - 1, k); + } else if (p < k) { + return quickSelect(nums, p + 1, r, k); + } else { + return nums[p]; + } + } + private void swap(int[] nums, int i, int j) { + int temp = nums[i]; + nums[i] = nums[j]; + nums[j] = temp; + } +}`, + cpp: `class Solution { +public: + int findKthLargest(vector& nums, int k) { + k = nums.size() - k; + return quickSelect(nums, 0, nums.size() - 1, k); + } +private: + int quickSelect(vector& nums, int l, int r, int k) { + int pivot = nums[r]; + int p = l; + for (int i = l; i < r; i++) { + if (nums[i] <= pivot) { + swap(nums[p], nums[i]); + p++; + } + } + swap(nums[p], nums[r]); + if (p > k) { + return quickSelect(nums, l, p - 1, k); + } + else if (p < k) { + return quickSelect(nums, p + 1, r, k); + } + else { + return nums[p]; + } + } +};` +}; - return quickSelect(0, nums.length - 1); -}`; +export const KthLargestVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); + + const initialNums = [3, 2, 1, 5, 6, 4]; + const targetK = 2; - const generateSteps = () => { - const s: Step[] = []; + const generateStepsData = () => { + const steps: Step[] = []; const nums = [...initialNums]; const n = nums.length; let k = n - targetK; - // Step 1: Function Entry - s.push({ - nums: [...nums], - l: -1, r: -1, p: -1, i: -1, pivot: null, + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + steps.push({ + nums: [...nums], l: -1, r: -1, p: -1, i: -1, pivot: null, explanation: `Starting findKthLargest with k = ${targetK}.`, - highlightedLines: [1], - variables: { nums: `[${nums.join(', ')}]`, k: targetK } + variables: { nums: `[${nums.join(', ')}]`, k: targetK }, + pseudoStep: `CALL findKthLargest(nums, k = ${targetK})` }); + addLines(1, 1, 2, 3); - // Step 2: Transform k - s.push({ - nums: [...nums], - l: -1, r: -1, p: -1, i: -1, pivot: null, + steps.push({ + nums: [...nums], l: -1, r: -1, p: -1, i: -1, pivot: null, explanation: `Transform k to index: k = ${n} - ${targetK} = ${k}. We need the element at index ${k} in a sorted array.`, - highlightedLines: [2], - variables: { targetIndex: k } + variables: { targetIndex: k }, + pseudoStep: `SET k = nums.length - k → ${k}` }); + addLines(2, 2, 3, 4); - // Step 3: Return quickSelect call - s.push({ - nums: [...nums], - l: -1, r: -1, p: -1, i: -1, pivot: null, + steps.push({ + nums: [...nums], l: -1, r: -1, p: -1, i: -1, pivot: null, explanation: `Calling quickSelect(0, ${n - 1}) to find the element at index ${k}.`, - highlightedLines: [26], - variables: { l: 0, r: n - 1, targetIndex: k } + variables: { l: 0, r: n - 1, targetIndex: k }, + pseudoStep: `CALL quickSelect(l = 0, r = ${n - 1})` }); + addLines(21, 17, 4, 5); const quickSelect = (l: number, r: number) => { - // Step: QuickSelect Entry - s.push({ - nums: [...nums], - l, r, p: -1, i: -1, pivot: null, - explanation: `Entering quickSelect(l=${l}, r=${r}).`, - highlightedLines: [4], - variables: { l, r, targetIndex: k } + steps.push({ + nums: [...nums], l, r, p: -1, i: -1, pivot: null, + explanation: `Entering quickSelect(l = ${l}, r = ${r}).`, + variables: { l, r, targetIndex: k }, + pseudoStep: `CALL quickSelect(l = ${l}, r = ${r})` }); + addLines(3, 3, 6, 8); const pivot = nums[r]; - // Step: Choose Pivot - s.push({ - nums: [...nums], - l, r, p: -1, i: -1, pivot, + steps.push({ + nums: [...nums], l, r, p: -1, i: -1, pivot, explanation: `Chosen pivot: nums[r] = ${pivot}.`, - highlightedLines: [5], - variables: { pivot, r } + variables: { pivot, r }, + pseudoStep: `SET pivot = nums[r] → ${pivot}` }); + addLines(4, 4, 7, 9); let p = l; - // Step: Init p - s.push({ - nums: [...nums], - l, r, p, i: -1, pivot, + steps.push({ + nums: [...nums], l, r, p, i: -1, pivot, explanation: `Initializing partition pointer p = ${l}.`, - highlightedLines: [6], - variables: { p, l } + variables: { p, l }, + pseudoStep: `SET p = l → ${l}` }); + addLines(5, 5, 8, 10); for (let i = l; i < r; i++) { - // Step: Loop Condition - s.push({ - nums: [...nums], - l, r, p, i, pivot, + steps.push({ + nums: [...nums], l, r, p, i, pivot, explanation: `Checking loop condition: i = ${i} < r = ${r}.`, - highlightedLines: [8], - variables: { i, r, p } + variables: { i, r, p }, + pseudoStep: `FOR i = ${i} to ${r - 1}` }); + addLines(6, 6, 9, 11); - // Step: If condition - s.push({ - nums: [...nums], - l, r, p, i, pivot, + steps.push({ + nums: [...nums], l, r, p, i, pivot, explanation: `Is nums[i] (${nums[i]}) <= pivot (${pivot})?`, - highlightedLines: [9], - variables: { "nums[i]": nums[i], pivot } + variables: { "nums[i]": nums[i], pivot }, + pseudoStep: `IF nums[i] <= pivot → ${nums[i]} <= ${pivot} ?` }); + addLines(7, 7, 10, 12); if (nums[i] <= pivot) { const valI = nums[i]; const valP = nums[p]; [nums[p], nums[i]] = [nums[i], nums[p]]; - // Step: Swap - s.push({ - nums: [...nums], - l, r, p, i, pivot, + steps.push({ + nums: [...nums], l, r, p, i, pivot, explanation: `Yes, swap nums[p] (${valP}) and nums[i] (${valI}).`, - highlightedLines: [10], - variables: { p, i, "nums[p]": valI, "nums[i]": valP } + variables: { p, i, "nums[p]": valI, "nums[i]": valP }, + pseudoStep: `SWAP nums[p], nums[i] → swap(${valP}, ${valI})` }); + addLines(8, 8, 11, 13); p++; - // Step: Increment p - s.push({ - nums: [...nums], - l, r, p, i, pivot, + steps.push({ + nums: [...nums], l, r, p, i, pivot, explanation: `Increment p to ${p}.`, - highlightedLines: [11], - variables: { p } + variables: { p }, + pseudoStep: `SET p = p + 1 → ${p}` }); + addLines(9, 9, 12, 14); } } - // Step: Final Swap const valP = nums[p]; const valR = nums[r]; [nums[p], nums[r]] = [nums[r], nums[p]]; - s.push({ - nums: [...nums], - l, r, p, i: -1, pivot, + steps.push({ + nums: [...nums], l, r, p, i: -1, pivot, explanation: `Loop end. Swap pivot nums[r] (${valR}) with nums[p] (${valP}). Pivot is now at index ${p}.`, - highlightedLines: [15], - variables: { p, r, pivotPlacement: `nums[${p}] = ${valR}` } + variables: { p, r, pivotPlacement: `nums[${p}] = ${valR}` }, + pseudoStep: `SWAP nums[p], nums[r] → swap(${valP}, ${valR})` }); + addLines(12, 10, 15, 17); - // Step: Check Branches - s.push({ - nums: [...nums], - l, r, p, i: -1, pivot, + steps.push({ + nums: [...nums], l, r, p, i: -1, pivot, explanation: `Comparing pivot index p (${p}) with target k (${k}).`, - highlightedLines: [17], - variables: { p, k } + variables: { p, k }, + pseudoStep: `IF p > k → ${p} > ${k} ?` }); + addLines(13, 11, 16, 18); if (p > k) { - s.push({ - nums: [...nums], - l, r, p, i: -1, pivot, + steps.push({ + nums: [...nums], l, r, p, i: -1, pivot, explanation: `p (${p}) > k (${k}). Element is in the left subarray.`, - highlightedLines: [18], - variables: { nextRange: `[${l}, ${p - 1}]` } + variables: { nextRange: `[${l}, ${p - 1}]` }, + pseudoStep: `CALL quickSelect(l = ${l}, r = ${p - 1})` }); + addLines(14, 12, 17, 19); quickSelect(l, p - 1); } else if (p < k) { - s.push({ - nums: [...nums], - l, r, p, i: -1, pivot, + steps.push({ + nums: [...nums], l, r, p, i: -1, pivot, explanation: `p (${p}) < k (${k}). Element is in the right subarray.`, - highlightedLines: [20], - variables: { nextRange: `[${p + 1}, ${r}]` } + variables: { nextRange: `[${p + 1}, ${r}]` }, + pseudoStep: `CALL quickSelect(l = ${p + 1}, r = ${r})` }); + addLines(16, 14, 19, 22); quickSelect(p + 1, r); } else { - s.push({ - nums: [...nums], - l, r, p, i: -1, pivot, + steps.push({ + nums: [...nums], l, r, p, i: -1, pivot, explanation: `p (${p}) === k (${k}). Found the element!`, - highlightedLines: [22], - variables: { result: nums[p] } + variables: { result: nums[p] }, + pseudoStep: `RETURN nums[p] → ${nums[p]}` }); + addLines(18, 16, 21, 25); } }; quickSelect(0, n - 1); - setSteps(s); + + const lastStep = steps[steps.length - 1]; + steps.push({ + ...lastStep, + explanation: "Algorithm Complete!", + pseudoStep: "DONE" + }); + addLines(18, 16, 21, 25); + + return { steps, stepLineNumbers }; }; + const { steps, stepLineNumbers } = generateStepsData(); + useEffect(() => { - generateSteps(); - }, []); + if (isPlaying && currentStepIndex < steps.length - 1) { + intervalRef.current = setInterval(() => { + setCurrentStepIndex((prev) => { + if (prev >= steps.length - 1) { + setIsPlaying(false); + return prev; + } + return prev + 1; + }); + }, 1000 / speed); + } else { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + } + + return () => { + if (intervalRef.current) clearInterval(intervalRef.current); + }; + }, [isPlaying, currentStepIndex, steps.length, speed]); + + const handlePlay = () => setIsPlaying(true); + const handlePause = () => setIsPlaying(false); + const handleStepForward = () => { + if (currentStepIndex < steps.length - 1) setCurrentStepIndex(currentStepIndex + 1); + }; + const handleStepBack = () => { + if (currentStepIndex > 0) setCurrentStepIndex(currentStepIndex - 1); + }; + const handleReset = () => { + setCurrentStepIndex(0); + setIsPlaying(false); + }; if (steps.length === 0) return null; - const step = steps[currentStep]; + + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map((s) => s.pseudoStep); return ( - - +
+ +
+
+

Visual Partitioning

- {step.nums.map((num, idx) => { - const isPivot = idx === step.r && step.i === -1; // Before final swap, pivot is at r - const isCurrentPivot = num === step.pivot && idx === step.p; - const isP = idx === step.p; - const isI = idx === step.i; - const inRange = idx >= step.l && idx <= step.r; - const targetMatch = idx === (initialNums.length - targetK) && step.highlightedLines.includes(17); + {currentStep.nums.map((num, idx) => { + const isPivot = idx === currentStep.r && currentStep.i === -1; + const isCurrentPivot = num === currentStep.pivot && idx === currentStep.p; + const isP = idx === currentStep.p; + const isI = idx === currentStep.i; + const inRange = idx >= currentStep.l && idx <= currentStep.r; + const targetMatch = idx === (initialNums.length - targetK) && currentStepIndex === steps.length - 2; return ( { layout initial={{ opacity: 0, scale: 0.8 }} animate={{ - opacity: inRange || step.l === -1 ? 1 : 0.4, + opacity: inRange || currentStep.l === -1 ? 1 : 0.4, scale: isI || isP || targetMatch ? 1.1 : 1, backgroundColor: targetMatch - ? "green" - : isP ? "green" - : isI ? "purple" - : isPivot ? "var(--primary)" : "var(--card)", - borderColor: isPivot || isCurrentPivot ? "var(--primary)" : "var(--border)" + ? "#22c55e" + : isP + ? "#22c55e" + : isI + ? "#a855f7" + : isPivot + ? "hsl(var(--primary))" + : "var(--card)", + borderColor: isPivot || isCurrentPivot ? "hsl(var(--primary))" : "var(--border)" }} - className={`w-14 h-14 rounded-xl flex flex-col items-center justify-center border-2 transition-all relative ${isP ? "shadow-lg z-10" : "" - }`} + className={`w-12 h-12 rounded-lg flex flex-col items-center justify-center border-2 transition-all relative ${ + isP || isI || targetMatch || isPivot + ? "shadow-lg z-10 text-white font-bold" + : "text-foreground" + }`} > - {num} + {num}
- {isP && p} - {isI && i} - {targetMatch && ★ K} + {isP && p} + {isI && i} + {targetMatch && ★ K}
); @@ -270,43 +410,37 @@ export const KthLargestVisualization = () => {
Pivot - {step.pivot !== null ? step.pivot : '-'} + {currentStep.pivot !== null ? currentStep.pivot : "-"}
Target Index - {initialNums.length - targetK} + {initialNums.length - targetK}
- +

Algorithm Logic

-

{step.explanation}

- +

{currentStep.explanation}

+
- + - -

p pointer: Elements to the left of p are guaranteed to be ≤ pivot.

-

i pointer: Currently scanning element at this index.

+ +

p pointer: Elements to the left of p are guaranteed to be ≤ pivot.

+

i pointer: Currently scanning element at this index.

Pivot: Element used to partition the array.

- } - rightContent={ - - } - controls={ - - } - /> +
+
); }; diff --git a/src/components/visualizations/algorithms/LCSVisualization.tsx b/src/components/visualizations/algorithms/LCSVisualization.tsx index abeb96e..5c136be 100644 --- a/src/components/visualizations/algorithms/LCSVisualization.tsx +++ b/src/components/visualizations/algorithms/LCSVisualization.tsx @@ -1,8 +1,10 @@ -import React, { useEffect, useRef, useState } from "react"; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; -import { StepControls } from "../shared/StepControls"; -import { VariablePanel } from "../shared/VariablePanel"; +import React, { useState, useMemo } from 'react'; +import { Card } from '@/components/ui/card'; +import { SimpleStepControls } from '../shared/SimpleStepControls'; +import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import { VisualizationLayout } from '../shared/VisualizationLayout'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { dp: number[][]; @@ -10,281 +12,316 @@ interface Step { j: number; text1: string; text2: string; - lcs: string; message: string; - lineNumber: number; - // For highlighting specific comparison cells - highlightI?: number; - highlightJ?: number; + explanation: string; + pseudoStep: string; } -export const LCSVisualization: React.FC = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1000); - const intervalRef = useRef(null); - - const code = `function longestCommonSubsequence(text1: string, text2: string): number { - const m = text1.length; - const n = text2.length; - - const dp: number[][] = Array.from({ length: m + 1 }, () => - new Array(n + 1).fill(0) - ); - - for (let i = m - 1; i >= 0; i--) { - for (let j = n - 1; j >= 0; j--) { - if (text1[i] === text2[j]) { - dp[i][j] = 1 + dp[i + 1][j + 1]; - } else { - dp[i][j] = Math.max(dp[i][j + 1], dp[i + 1][j]); +const languages: VisualizationLanguageMap = { + typescript: `function longestCommonSubsequence(text1: string, text2: string): number { + const m = text1.length; + const n = text2.length; + const dp: number[][] = Array.from({ length: m + 1 }, () => + new Array(n + 1).fill(0) + ); + for (let i = m - 1; i >= 0; i--) { + for (let j = n - 1; j >= 0; j--) { + if (text1[i] === text2[j]) { + dp[i][j] = 1 + dp[i + 1][j + 1]; + } else { + dp[i][j] = Math.max(dp[i][j + 1], dp[i + 1][j]); + } + } + } + return dp[0][0]; +}`, + python: `def longestCommonSubsequence(text1: str, text2: str) -> int: + m = len(text1) + n = len(text2) + dp = [[0] * (n + 1) for _ in range(m + 1)] + for i in range(m - 1, -1, -1): + for j in range(n - 1, -1, -1): + if text1[i] == text2[j]: + dp[i][j] = 1 + dp[i + 1][j + 1] + else: + dp[i][j] = max(dp[i][j + 1], dp[i + 1][j]) + return dp[0][0]`, + java: `public static class Solution { + public int longestCommonSubsequence(String text1, String text2) { + int m = text1.length(); + int n = text2.length(); + int[][] dp = new int[m + 1][n + 1]; + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + if (text1.charAt(i - 1) == text2.charAt(j - 1)) { + dp[i][j] = 1 + dp[i - 1][j - 1]; + } else { + dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]); + } } } + return dp[m][n]; } +}`, + cpp: `class Solution { +public: + int longestCommonSubsequence(string text1, string text2) { + int m = text1.length(), n = text2.length(); + vector> dp(m + 1, vector(n + 1, 0)); + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + if (text1[i - 1] == text2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + return dp[m][n]; + } +};` +}; - return dp[0][0]; -}`; - +export const LCSVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); - const generateSteps = () => { - // Example strings - const text1 = "abcde"; - const text2 = "ace"; + const { steps, stepLineNumbers } = useMemo(() => { + const text1 = 'abcde'; + const text2 = 'ace'; const m = text1.length; const n = text2.length; - // Initialize DP table (m+1) x (n+1) filled with 0 const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0)); - const newSteps: Step[] = []; + const s: Step[] = []; + const lines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + lines.typescript!.push(ts); + lines.python!.push(py); + lines.java!.push(java); + lines.cpp!.push(cpp); + }; // Step 1: Initialization - newSteps.push({ + s.push({ dp: dp.map((row) => [...row]), i: -1, j: -1, text1, text2, - lcs: "", - message: "Initialize DP table with 0s. dp[i][j] stores LCS length of text1[i:] and text2[j:]", - lineNumber: 5, + message: 'Initialize DP table with 0s.', + explanation: 'Initialize a 2D array dp of size (m+1) x (n+1) with all values set to 0. dp[i][j] will store the LCS of text1[i:] and text2[j:].', + pseudoStep: 'SET dp = [m+1][n+1] matrix of 0s', }); + addLines(3, 4, 5, 5); for (let i = m - 1; i >= 0; i--) { for (let j = n - 1; j >= 0; j--) { - newSteps.push({ + // Comparison Step + s.push({ dp: dp.map((row) => [...row]), i, j, text1, text2, - lcs: "", - message: `Checking current positions: i=${i}, j=${j} ('${text1[i]}', '${text2[j]}')`, - lineNumber: 11, - highlightI: i, - highlightJ: j + message: `Checking: text1[${i}] ('${text1[i]}') and text2[${j}] ('${text2[j]}')`, + explanation: `Compare character '${text1[i]}' at text1[${i}] with '${text2[j]}' at text2[${j}].`, + pseudoStep: `IF text1[${i}] == text2[${j}] → '${text1[i]}' == '${text2[j]}'?`, }); + addLines(9, 7, 8, 8); if (text1[i] === text2[j]) { dp[i][j] = 1 + dp[i + 1][j + 1]; - newSteps.push({ + s.push({ dp: dp.map((row) => [...row]), i, j, text1, text2, - lcs: "", - message: `Match found! '${text1[i]}' == '${text2[j]}'. dp[${i}][${j}] = 1 + dp[${i + 1}][${j + 1}] = ${dp[i][j]}`, - lineNumber: 12, + message: `Match! dp[${i}][${j}] = 1 + dp[${i + 1}][${j + 1}] = ${dp[i][j]}`, + explanation: `Characters match! Increment the LCS length by 1 from the diagonal neighbor dp[${i + 1}][${j + 1}].`, + pseudoStep: `SET dp[${i}][${j}] = 1 + dp[${i + 1}][${j + 1}]`, }); + addLines(10, 8, 9, 9); } else { + s.push({ + dp: dp.map((row) => [...row]), + i, + j, + text1, + text2, + message: `No match. Entering else block.`, + explanation: `Characters do not match. Take the maximum LCS length from either omitting text1[${i}] (dp[${i + 1}][${j}]) or text2[${j}] (dp[${i}][${j + 1}]).`, + pseudoStep: `ELSE → SET dp[${i}][${j}] = MAX(dp[${i}][${j + 1}], dp[${i + 1}][${j}])`, + }); + addLines(11, 9, 10, 10); + dp[i][j] = Math.max(dp[i][j + 1], dp[i + 1][j]); - newSteps.push({ + s.push({ dp: dp.map((row) => [...row]), i, j, text1, text2, - lcs: "", message: `No match. dp[${i}][${j}] = max(dp[${i}][${j + 1}], dp[${i + 1}][${j}]) = ${dp[i][j]}`, - lineNumber: 14, + explanation: `Characters do not match. Take the maximum LCS length from either omitting text1[${i}] (dp[${i + 1}][${j}]) or text2[${j}] (dp[${i}][${j + 1}]).`, + pseudoStep: `SET dp[${i}][${j}] = MAX(dp[${i}][${j + 1}], dp[${i + 1}][${j}])`, }); + addLines(12, 10, 11, 11); } } } - newSteps.push({ + // Final Step + s.push({ dp: dp.map((row) => [...row]), i: 0, j: 0, text1, text2, - lcs: "", - message: `The Longest Common Subsequence length is stored in dp[0][0] = ${dp[0][0]}`, - lineNumber: 19, + message: `Result: dp[0][0] = ${dp[0][0]}`, + explanation: `The LCS computation is complete. The length of the longest common subsequence is stored in the top-left cell dp[0][0], which is ${dp[0][0]}.`, + pseudoStep: 'RETURN dp[0][0]', }); + addLines(16, 11, 15, 15); - setSteps(newSteps); - - }; - - useEffect(() => { - generateSteps(); + return { steps: s, stepLineNumbers: lines }; }, []); - useEffect(() => { - if (isPlaying && currentStepIndex < steps.length - 1) { - intervalRef.current = window.setInterval(() => { - setCurrentStepIndex((prev) => { - if (prev >= steps.length - 1) { - setIsPlaying(false); - return prev; - } - return prev + 1; - }); - }, speed); - } else { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - } - - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - }; - }, [isPlaying, currentStepIndex, steps.length, speed]); - - const handlePlay = () => setIsPlaying(true); - const handlePause = () => setIsPlaying(false); - const handleStepForward = () => { - if (currentStepIndex < steps.length - 1) { - setCurrentStepIndex(currentStepIndex + 1); - } - }; - const handleStepBack = () => { - if (currentStepIndex > 0) { - setCurrentStepIndex(currentStepIndex - 1); - } - }; - const handleReset = () => { - setCurrentStepIndex(0); - setIsPlaying(false); - }; - - if (steps.length === 0) return null; - - const currentStep = steps[currentStepIndex]; + const step = steps[currentStepIndex]; + const pseudoSteps = steps.map((s) => s.pseudoStep); return ( -
- -
-
-

- LCS Table (Bottom-Up) -

-
- - - - {/* Corner Cell */} - - - {/* Columns: text2 chars + empty suffix */} - {currentStep.text2.split("").map((char, idx) => ( - - ))} - - - - - {currentStep.dp.map((row, rowIdx) => ( - - {/* Row Header: text1 char or empty suffix */} - + +
+ +

+ LCS Table (Bottom-Up) +

+
+
- {char} -
{idx}
-
- ∅ -
{currentStep.text2.length}
-
- {rowIdx < currentStep.text1.length ? ( - <> - {currentStep.text1[rowIdx]} -
{rowIdx}
- - ) : "∅"} -
+ + + + {step.text2.split('').map((char, idx) => ( + + ))} + + + + + {step.dp.map((row, rowIdx) => ( + + + {row.map((val, colIdx) => { + const isCurrent = rowIdx === step.i && colIdx === step.j; + const isDependency = + step.i !== -1 && + ((rowIdx === step.i + 1 && colIdx === step.j + 1) || + (rowIdx === step.i && colIdx === step.j + 1) || + (rowIdx === step.i + 1 && colIdx === step.j)); - {/* DP Values */} - {row.map((val, colIdx) => { - const isCurrent = rowIdx === currentStep.i && colIdx === currentStep.j; - // Determine if this cell is part of the calculation dependencies - // current check is at (i, j). - // Dependencies are (i+1, j+1), (i, j+1), (i+1, j) - const isDependency = currentStep.i !== -1 && ( - (rowIdx === currentStep.i + 1 && colIdx === currentStep.j + 1) || - (rowIdx === currentStep.i && colIdx === currentStep.j + 1) || - (rowIdx === currentStep.i + 1 && colIdx === currentStep.j) - ); + let cellClass = 'text-muted-foreground/40'; + if (isCurrent) { + cellClass = 'bg-primary/20 ring-2 ring-primary ring-inset font-bold text-primary scale-105'; + } else if (isDependency) { + cellClass = 'bg-blue-500/10 text-blue-600 dark:text-blue-400 font-medium'; + } else if (val > 0) { + cellClass = 'bg-green-500/5 text-green-600 dark:text-green-400'; + } - return ( - - ); - })} - - ))} - -
+ {char} +
{idx}
+
+ ∅ +
{step.text2.length}
+
+ {rowIdx < step.text1.length ? ( + <> + {step.text1[rowIdx]} +
{rowIdx}
+ + ) : ( + '∅' + )} +
0 - ? "bg-green-500/5 text-green-600 dark:text-green-400" - : "" - }`} - > - {val} -
+ return ( + + {val} + + ); + })} + + ))} + + +
+
-
-

{currentStep.message}

-
-
+
+ +

Step Explanation

+

+ {step.explanation} +

+
+ = 0 && currentStep.i < currentStep.text1.length ? currentStep.text1[currentStep.i] : "-", - "text2[j]": currentStep.j >= 0 && currentStep.j < currentStep.text2.length ? currentStep.text2[currentStep.j] : "-", - "dp[i][j]": currentStep.i !== -1 ? currentStep.dp[currentStep.i][currentStep.j] : currentStep.dp[0][0], + i: step.i !== -1 ? step.i : '-', + j: step.j !== -1 ? step.j : '-', + 'text1[i]': step.i >= 0 && step.i < step.text1.length ? step.text1[step.i] : '-', + 'text2[j]': step.j >= 0 && step.j < step.text2.length ? step.text2[step.j] : '-', + 'dp[i][j]': step.i !== -1 ? step.dp[step.i][step.j] : step.dp[0][0] }} /> + + +

+ Why this works +

+

+ LCS can be solved by comparing characters from the end of both strings. +

+

+ If characters match, the LCS length increases by 1 plus the LCS of the remaining suffixes: `1 + dp[i+1][j+1]`. +

+

+ If they mismatch, the LCS length is the maximum value found by either skipping `text1[i]` or `text2[j]`: `max(dp[i][j+1], dp[i+1][j])`. +

+
- - setCurrentStepIndex(0)} + /> + } + controls={ + -
-
+ } + /> ); }; + +export default LCSVisualization; diff --git a/src/components/visualizations/algorithms/LISVisualization.tsx b/src/components/visualizations/algorithms/LISVisualization.tsx index 3e0fb02..6936411 100644 --- a/src/components/visualizations/algorithms/LISVisualization.tsx +++ b/src/components/visualizations/algorithms/LISVisualization.tsx @@ -1,27 +1,23 @@ -import React, { useEffect, useRef, useState } from "react"; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; -import { SimpleStepControls } from "../shared/SimpleStepControls"; -import { VariablePanel } from "../shared/VariablePanel"; -import { VisualizationLayout } from "../shared/VisualizationLayout"; -import { Card } from "@/components/ui/card"; +import React, { useState, useMemo } from 'react'; +import { Card } from '@/components/ui/card'; +import { SimpleStepControls } from '../shared/SimpleStepControls'; +import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import { VisualizationLayout } from '../shared/VisualizationLayout'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { array: number[]; dp: number[]; - currentIndex: number; // i - compareIndex?: number; // j + currentIndex: number; + compareIndex?: number; maxLength: number; explanation: string; - lineExecution: string; - lineNumber: number[]; + pseudoStep: string; } -export const LISVisualization: React.FC = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - - const code = `function lengthOfLIS(nums: number[]): number { +const languages: VisualizationLanguageMap = { + typescript: `function lengthOfLIS(nums: number[]): number { const n = nums.length; const LIS: number[] = new Array(n).fill(1); for (let i = n - 1; i >= 0; i--) { @@ -32,103 +28,158 @@ export const LISVisualization: React.FC = () => { } } return Math.max(...LIS); -}`; +}`, + python: `def lengthOfLIS(nums): + n = len(nums) + LIS = [1] * n + for i in range(n - 1, -1, -1): + for j in range(i + 1, n): + if nums[i] < nums[j]: + LIS[i] = max(LIS[i], 1 + LIS[j]) + return max(LIS)`, + java: `public static class Solution { + public int lengthOfLIS(int[] nums) { + int n = nums.length; + int[] LIS = new int[n]; + for (int i = 0; i < n; i++) { + LIS[i] = 1; + } + for (int i = n - 1; i >= 0; i--) { + for (int j = i + 1; j < n; j++) { + if (nums[i] < nums[j]) { + LIS[i] = Math.max(LIS[i], 1 + LIS[j]); + } + } + } + int maxLength = 0; + for (int i = 0; i < n; i++) { + maxLength = Math.max(maxLength, LIS[i]); + } + return maxLength; + } +}`, + cpp: `class Solution { +public: + int lengthOfLIS(vector& nums) { + int n = nums.size(); + vector LIS(n, 1); + for (int i = n - 1; i >= 0; i--) { + for (int j = i + 1; j < n; j++) { + if (nums[i] < nums[j]) { + LIS[i] = max(LIS[i], 1 + LIS[j]); + } + } + } + return *max_element(LIS.begin(), LIS.end()); + } +};` +}; + +export const LISVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); - const generateSteps = () => { + const { steps, stepLineNumbers } = useMemo(() => { const nums = [10, 9, 2, 5, 3, 7, 101, 18]; const n = nums.length; const LIS = new Array(n).fill(1); - const newSteps: Step[] = []; + const s: Step[] = []; + const lines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + lines.typescript!.push(ts); + lines.python!.push(py); + lines.java!.push(java); + lines.cpp!.push(cpp); + }; - newSteps.push({ + s.push({ array: [...nums], dp: [...LIS], currentIndex: -1, + compareIndex: undefined, maxLength: 1, - explanation: "Initialize LIS array with 1s.\nEvery individual element is technically an increasing subsequence of length 1 by itself.", - lineExecution: "const LIS: number[] = new Array(n).fill(1);", - lineNumber: [3], + explanation: 'Initialize LIS array with 1s. Every single element is an increasing subsequence of length 1.', + pseudoStep: 'SET LIS = [1] * n', }); + addLines(3, 3, 4, 5); for (let i = n - 1; i >= 0; i--) { - newSteps.push({ + s.push({ array: [...nums], dp: [...LIS], currentIndex: i, + compareIndex: undefined, maxLength: Math.max(...LIS), - explanation: `Outer loop moves backward: i = ${i} (Base Value: ${nums[i]}).\nWe are finding the longest increasing subsequence that STARTS precisely at index ${i}.`, - lineExecution: "for (let i = n - 1; i >= 0; i--)", - lineNumber: [4], + explanation: `Outer loop moves backward: i = ${i} (value = ${nums[i]}). We compute LIS starting at index ${i}.`, + pseudoStep: `FOR i = ${i} DOWNTO 0`, }); + addLines(4, 4, 8, 6); for (let j = i + 1; j < n; j++) { if (nums[i] < nums[j]) { const oldVal = LIS[i]; LIS[i] = Math.max(LIS[i], 1 + LIS[j]); - - newSteps.push({ + s.push({ array: [...nums], dp: [...LIS], currentIndex: i, compareIndex: j, maxLength: Math.max(...LIS), - explanation: `Compare nums[${i}] (${nums[i]}) with nums[${j}] (${nums[j]}).\nYes, ${nums[i]} < ${nums[j]}.\nUpdate LIS[${i}] = max(${oldVal}, 1 + LIS[${j}]) becomes ${LIS[i]}!`, - lineExecution: "if (nums[i] < nums[j]) { LIS[i] = Math.max(...) }", - lineNumber: [6, 7], + explanation: `Compare nums[${i}] (${nums[i]}) < nums[${j}] (${nums[j]}) -> YES. Update LIS[${i}] = max(${oldVal}, 1 + LIS[${j}]) = ${LIS[i]}.`, + pseudoStep: `IF nums[${i}] < nums[${j}] → YES ✓ → SET LIS[${i}] = MAX(LIS[${i}], 1 + LIS[${j}])`, }); + addLines(7, 7, 11, 9); } else { - newSteps.push({ + s.push({ array: [...nums], dp: [...LIS], currentIndex: i, compareIndex: j, maxLength: Math.max(...LIS), - explanation: `Compare nums[${i}] (${nums[i]}) with nums[${j}] (${nums[j]}).\nNo, ${nums[i]} is NOT strictly less than ${nums[j]}, unable to add to sequence. Skip.`, - lineExecution: "if (nums[i] < nums[j])", - lineNumber: [6], + explanation: `Compare nums[${i}] (${nums[i]}) < nums[${j}] (${nums[j]}) -> NO. Cannot extend the subsequence.`, + pseudoStep: `IF nums[${i}] < nums[${j}] → NO ✗`, }); + addLines(6, 6, 10, 8); } } } - newSteps.push({ + s.push({ array: [...nums], dp: [...LIS], currentIndex: -1, + compareIndex: undefined, maxLength: Math.max(...LIS), - explanation: `The loops have completed computing lengths from right to left.\nThe overall Longest Increasing Subsequence length is the maximum value tracked completely across the DP array: ${Math.max(...LIS)}.`, - lineExecution: "return Math.max(...LIS);", - lineNumber: [11], + explanation: `Calculations complete. The maximum length in LIS array is ${Math.max(...LIS)}.`, + pseudoStep: `RETURN MAX(LIS) → ${Math.max(...LIS)}`, }); + addLines(11, 8, 19, 13); - setSteps(newSteps); - }; - - useEffect(() => { - generateSteps(); + return { steps: s, stepLineNumbers: lines }; }, []); - if (steps.length === 0) return null; - - const currentStep = steps[currentStepIndex]; - const maxVal = Math.max(...currentStep.array); + const step = steps[currentStepIndex]; + const pseudoSteps = steps.map((s) => s.pseudoStep); + const maxVal = Math.max(...step.array); return ( -
-

- Bottom-Up Approach -

- -

- Input Array (nums) +
+
+ +

+ Input Array (nums)

- {currentStep.array.map((value, idx) => { - const isI = idx === currentStep.currentIndex; - const isJ = idx === currentStep.compareIndex; + {step.array.map((value, idx) => { + const isI = idx === step.currentIndex; + const isJ = idx === step.compareIndex; return (
{ style={{ minWidth: '32px' }} >
{value}
-
- {isI &&
i
} - {isJ &&
j
} - {(!isI && !isJ) &&
} +
+ {isI && i} + {isJ && j}
); })}
-

- DP Array (Longest Sequence Starting Here) +

+ DP Array (Longest Sequence Starting Here)

- {currentStep.dp.map((length, idx) => { - const isI = idx === currentStep.currentIndex; - const isJ = idx === currentStep.compareIndex; - - return ( -
-
1 - ? "bg-green-300 border-green-500 dark:bg-green-800 dark:border-green-600" - : "bg-white dark:bg-card border-gray-300 dark:border-border" - }`} - > - {length} -
-
- ); + {step.dp.map((length, idx) => { + const isI = idx === step.currentIndex; + const isJ = idx === step.compareIndex; + + let cellClass = 'bg-muted/30 border-border text-muted-foreground/85'; + if (isI) { + cellClass = 'bg-orange-500/20 border-orange-500 shadow-lg text-orange-600 dark:text-orange-400 font-bold'; + } else if (isJ) { + cellClass = 'bg-blue-500/20 border-blue-500 shadow-lg text-blue-600 dark:text-blue-400 font-bold'; + } else if (length > 1) { + cellClass = 'bg-green-500/10 border-green-500/40 text-green-600 dark:text-green-400'; + } + + return ( +
+
+ {length} +
+
+ ); })}
-
- -
-
-

- Current Execution -

-
- {currentStep.lineExecution} -
-
-
-

- Commentary -

-

- {currentStep.explanation} -

-
-
+
+ +

Step Explanation

+

+ {step.explanation} +

+
+ + + + +

+ Why this works +

+

+ For each element `nums[i]`, we look at all elements `nums[j]` to its right. If `nums[i] < nums[j]`, then `nums[i]` can extend the increasing subsequence starting at `j`. +

+

+ We compute `LIS[i] = max(LIS[i], 1 + LIS[j])` for all valid `j`. +

+

+ The final result is the maximum value in the entire `LIS` array. +

} rightContent={ -
-
- -
- -
- -
-
+ setCurrentStepIndex(0)} + /> } controls={ { /> ); }; + +export default LISVisualization; diff --git a/src/components/visualizations/algorithms/LRUCacheVisualization.tsx b/src/components/visualizations/algorithms/LRUCacheVisualization.tsx index 540a0e4..461b75f 100644 --- a/src/components/visualizations/algorithms/LRUCacheVisualization.tsx +++ b/src/components/visualizations/algorithms/LRUCacheVisualization.tsx @@ -1,11 +1,10 @@ -import { Pause, Play, RotateCcw, SkipBack, SkipForward } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; - -import { Button } from "@/components/ui/button"; -import { Slider } from "@/components/ui/slider"; +import { useEffect, useState } from "react"; +import { Card } from "@/components/ui/card"; import { VariablePanel } from "../shared/VariablePanel"; import { VisualizationLayout } from "../shared/VisualizationLayout"; -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import { VisualizationCodePanel } from "../shared/VisualizationCodePanel"; +import { SimpleStepControls } from "../shared/SimpleStepControls"; +import type { StepLineNumberMap, VisualizationLanguageMap } from "@/types/visualization"; interface DLLNode { key: number; @@ -25,7 +24,8 @@ interface Step { tail: number | null; message: string; detailedMessage: string; - highlightedLine: number; + explanation: string; + pseudoStep: string; highlightedNode?: number; highlightedHashMapKey?: number; evictedNode?: number; @@ -35,27 +35,31 @@ interface Step { animationType: "none" | "move" | "create" | "delete" | "update" | "search"; } -const codeExamples = { +const languages: VisualizationLanguageMap = { typescript: `class LRUCache { + private capacity: number; + private cache: Map; + private head: DLLNode | null; + private tail: DLLNode | null; constructor(capacity: number) { this.capacity = capacity; this.cache = new Map(); + this.head = null; + this.tail = null; } - get(key: number): number { if (!this.cache.has(key)) return -1; const node = this.cache.get(key)!; this.moveToHead(node); return node.value; } - put(key: number, value: number): void { if (this.cache.has(key)) { const node = this.cache.get(key)!; node.value = value; this.moveToHead(node); } else { - const newNode = { key, value }; + const newNode = { key, value, prev: null, next: null }; this.cache.set(key, newNode); this.addToHead(newNode); if (this.cache.size > this.capacity) { @@ -64,19 +68,16 @@ const codeExamples = { } } } - private moveToHead(node: DLLNode): void { this.removeNode(node); this.addToHead(node); } - private removeNode(node: DLLNode): void { if (node.prev) node.prev.next = node.next; if (node.next) node.next.prev = node.prev; if (node === this.head) this.head = node.next; if (node === this.tail) this.tail = node.prev; } - private addToHead(node: DLLNode): void { node.next = this.head; node.prev = null; @@ -84,26 +85,230 @@ const codeExamples = { this.head = node; if (!this.tail) this.tail = node; } - private removeTail(): DLLNode { const removed = this.tail!; this.removeNode(removed); return removed; } +} +interface DLLNode { + key: number; + value: number; + prev: DLLNode | null; + next: DLLNode | null; }`, + python: `class DLLNode: + def __init__(self, key, value): + self.key = key + self.value = value + self.prev = None + self.next = None +class LRUCache: + def __init__(self, capacity: int): + self.capacity = capacity + self.cache = {} + self.head = None + self.tail = None + def get(self, key: int) -> int: + if key not in self.cache: + return -1 + node = self.cache[key] + self._move_to_head(node) + return node.value + def put(self, key: int, value: int) -> None: + if key in self.cache: + node = self.cache[key] + node.value = value + self._move_to_head(node) + else: + new_node = DLLNode(key, value) + self.cache[key] = new_node + self._add_to_head(new_node) + if len(self.cache) > self.capacity: + removed_node = self._remove_tail() + del self.cache[removed_node.key] + def _move_to_head(self, node: DLLNode) -> None: + self._remove_node(node) + self._add_to_head(node) + def _remove_node(self, node: DLLNode) -> None: + if node.prev: + node.prev.next = node.next + if node.next: + node.next.prev = node.prev + if node == self.head: + self.head = node.next + if node == self.tail: + self.tail = node.prev + def _add_to_head(self, node: DLLNode) -> None: + node.next = self.head + node.prev = None + if self.head: + self.head.prev = node + self.head = node + if not self.tail: + self.tail = node + def _remove_tail(self) -> DLLNode: + removed_node = self.tail + self._remove_node(removed_node) + return removed_node`, + java: `import java.util.HashMap; +import java.util.Map; +class LRUCache { + private int capacity; + private Map cache; + private Node head, tail; + class Node { + int key, value; + Node prev, next; + Node(int key, int value) { + this.key = key; + this.value = value; + } + } + public LRUCache(int capacity) { + this.capacity = capacity; + this.cache = new HashMap<>(); + this.head = null; + this.tail = null; + } + public int get(int key) { + if (!cache.containsKey(key)) { + return -1; + } + Node node = cache.get(key); + moveToHead(node); + return node.value; + } + public void put(int key, int value) { + if (cache.containsKey(key)) { + Node node = cache.get(key); + node.value = value; + moveToHead(node); + } else { + Node newNode = new Node(key, value); + cache.put(key, newNode); + addToHead(newNode); + if (cache.size() > capacity) { + Node removed = removeTail(); + cache.remove(removed.key); + } + } + } + private void moveToHead(Node node) { + removeNode(node); + addToHead(node); + } + private void removeNode(Node node) { + if (node.prev != null) { + node.prev.next = node.next; + } else { + head = node.next; + } + if (node.next != null) { + node.next.prev = node.prev; + } else { + tail = node.prev; + } + } + private void addToHead(Node node) { + node.next = head; + node.prev = null; + if (head != null) { + head.prev = node; + } + head = node; + if (tail == null) { + tail = node; + } + } + private Node removeTail() { + Node removed = tail; + removeNode(tail); + return removed; + } +}`, + cpp: `#include +using namespace std; +class DLLNode { + public: + int key; + int value; + DLLNode* prev; + DLLNode* next; + DLLNode(int k, int v): key(k), value(v), prev(nullptr), next(nullptr) { } +}; +class LRUCache { + private: + int capacity; + unordered_map cache; + DLLNode* head; + DLLNode* tail; + public: + LRUCache(int capacity) { + this->capacity = capacity; + head = nullptr; + tail = nullptr; + } + int get(int key) { + if (cache.find(key) == cache.end()) { + return -1; + } + DLLNode* node = cache[key]; + moveToHead(node); + return node->value; + } + void put(int key, int value) { + if (cache.find(key) != cache.end()) { + DLLNode* node = cache[key]; + node->value = value; + moveToHead(node); + } else { + DLLNode* newNode = new DLLNode(key, value); + cache[key] = newNode; + addToHead(newNode); + if (cache.size() > capacity) { + DLLNode* removed = removeTail(); + cache.erase(removed->key); + delete removed; + } + } + } + private: + void moveToHead(DLLNode* node) { + removeNode(node); + addToHead(node); + } + void removeNode(DLLNode* node) { + if (node->prev) node->prev->next = node->next; + if (node->next) node->next->prev = node->prev; + if (node == head) head = node->next; + if (node == tail) tail = node->prev; + } + void addToHead(DLLNode* node) { + node->next = head; + node->prev = nullptr; + if (head) head->prev = node; + head = node; + if (!tail) tail = node; + } + DLLNode* removeTail() { + DLLNode* removed = tail; + removeNode(removed); + return removed; + } +};`, }; export const LRUCacheVisualization = () => { const [steps, setSteps] = useState([]); + const [stepLineNumbers, setStepLineNumbers] = useState({ + typescript: [], + python: [], + java: [], + cpp: [], + }); const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const [language, setLanguage] = useState< - "typescript" | "python" | "java" | "cpp" - >("typescript"); - const intervalRef = useRef(null); - - // Generate granular steps for demonstration + useEffect(() => { const capacity = 3; const operations = [ @@ -118,18 +323,32 @@ export const LRUCacheVisualization = () => { ]; const generatedSteps: Step[] = []; + const stepLines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [], + }; + const cache = new Map(); let nodes: DLLNode[] = []; let head: number | null = null; let tail: number | null = null; + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLines.typescript!.push(ts); + stepLines.python!.push(py); + stepLines.java!.push(java); + stepLines.cpp!.push(cpp); + }; + const createStep = ( type: "get" | "put", key: number, value: number | undefined, message: string, detailedMessage: string, - highlightedLine: number, + pseudoStep: string, substep: number, totalSubsteps: number, animationType: Step["animationType"], @@ -149,7 +368,8 @@ export const LRUCacheVisualization = () => { tail, message, detailedMessage, - highlightedLine, + explanation: detailedMessage, + pseudoStep, highlightedNode, highlightedHashMapKey, evictedNode, @@ -162,37 +382,175 @@ export const LRUCacheVisualization = () => { const generateGetSteps = (key: number) => { const hasKey = cache.has(key); - if (hasKey) { const nodeIdx = cache.get(key)!; - const value = nodes[nodeIdx].value; + const val = nodes[nodeIdx].value; let substep = 1; const totalSubsteps = 11; - generatedSteps.push(createStep("get", key, undefined, `Called get(${key})`, `Starting GET operation for key ${key}`, 7, substep++, totalSubsteps, "none")); - generatedSteps.push(createStep("get", key, undefined, `Checking HashMap for key ${key}...`, `Searching in HashMap to see if key ${key} exists`, 8, substep++, totalSubsteps, "search", undefined, undefined, key)); - generatedSteps.push(createStep("get", key, undefined, `Key ${key} found in HashMap!`, `HashMap contains key ${key}, pointing to node at index ${nodeIdx}`, 8, substep++, totalSubsteps, "search", undefined, nodeIdx, key)); - generatedSteps.push(createStep("get", key, undefined, `Retrieved node from HashMap`, `Got reference to node: {key: ${key}, value: ${value}}`, 9, substep++, totalSubsteps, "update", undefined, nodeIdx)); - generatedSteps.push(createStep("get", key, undefined, `Need to move node to HEAD`, `Mark this node as most recently used by moving it to the front of the list`, 10, substep++, totalSubsteps, "none", undefined, nodeIdx)); + generatedSteps.push( + createStep( + "get", + key, + undefined, + `Called get(${key})`, + `Starting GET operation for key ${key}`, + `CALL get(key = ${key})`, + substep++, + totalSubsteps, + "none" + ) + ); + addLines(12, 13, 21, 23); + + generatedSteps.push( + createStep( + "get", + key, + undefined, + `Checking HashMap for key ${key}...`, + `Searching in HashMap to see if key ${key} exists`, + `IF key = ${key} IN cache`, + substep++, + totalSubsteps, + "search", + undefined, + undefined, + key + ) + ); + addLines(13, 14, 22, 24); + + generatedSteps.push( + createStep( + "get", + key, + undefined, + `Key ${key} found in HashMap!`, + `HashMap contains key ${key}, pointing to node at index ${nodeIdx}`, + `node = cache[${key}] → FOUND ✓`, + substep++, + totalSubsteps, + "search", + undefined, + nodeIdx, + key + ) + ); + addLines(14, 16, 25, 27); + + generatedSteps.push( + createStep( + "get", + key, + undefined, + `Retrieved node from HashMap`, + `Got reference to node: {key: ${key}, value: ${val}}`, + `SET node = cache[${key}]`, + substep++, + totalSubsteps, + "update", + undefined, + nodeIdx + ) + ); + addLines(14, 16, 25, 27); + + generatedSteps.push( + createStep( + "get", + key, + undefined, + `Need to move node to HEAD`, + `Mark this node as most recently used by moving it to the front of the list`, + `CALL moveToHead(node)`, + substep++, + totalSubsteps, + "none", + undefined, + nodeIdx + ) + ); + addLines(15, 17, 26, 28); const node = nodes[nodeIdx]; const isAlreadyHead = head === nodeIdx; if (!isAlreadyHead) { - generatedSteps.push(createStep("get", key, undefined, `Disconnecting node from current position...`, `Removing node from its current position in the doubly linked list`, 31, substep++, totalSubsteps, "delete", undefined, nodeIdx)); + generatedSteps.push( + createStep( + "get", + key, + undefined, + `Disconnecting node from current position...`, + `Removing node from its current position in the doubly linked list`, + `CALL removeNode(node)`, + substep++, + totalSubsteps, + "delete", + undefined, + nodeIdx + ) + ); + addLines(37, 34, 48, 52); if (node.prev !== null) { - generatedSteps.push(createStep("get", key, undefined, `Updating previous node's next pointer`, `Setting node[${node.prev}].next = ${node.next}`, 36, substep++, totalSubsteps, "update", undefined, node.prev)); + generatedSteps.push( + createStep( + "get", + key, + undefined, + `Updating previous node's next pointer`, + `Setting node[${node.prev}].next = ${node.next}`, + `SET node.prev.next = node.next`, + substep++, + totalSubsteps, + "update", + undefined, + node.prev + ) + ); + addLines(38, 36, 50, 53); nodes[node.prev].next = node.next; } if (node.next !== null) { - generatedSteps.push(createStep("get", key, undefined, `Updating next node's prev pointer`, `Setting node[${node.next}].prev = ${node.prev}`, 37, substep++, totalSubsteps, "update", undefined, node.next)); + generatedSteps.push( + createStep( + "get", + key, + undefined, + `Updating next node's prev pointer`, + `Setting node[${node.next}].prev = ${node.prev}`, + `SET node.next.prev = node.prev`, + substep++, + totalSubsteps, + "update", + undefined, + node.next + ) + ); + addLines(39, 38, 55, 54); nodes[node.next].prev = node.prev; } if (head === nodeIdx) head = node.next; if (tail === nodeIdx) tail = node.prev; - generatedSteps.push(createStep("get", key, undefined, `Reconnecting node at HEAD position...`, `Adding node to the front of the doubly linked list`, 32, substep++, totalSubsteps, "create", undefined, nodeIdx)); + generatedSteps.push( + createStep( + "get", + key, + undefined, + `Reconnecting node at HEAD position...`, + `Adding node to the front of the doubly linked list`, + `CALL addToHead(node)`, + substep++, + totalSubsteps, + "create", + undefined, + nodeIdx + ) + ); + addLines(43, 43, 60, 58); node.next = head; node.prev = null; @@ -200,51 +558,262 @@ export const LRUCacheVisualization = () => { head = nodeIdx; if (tail === null) tail = nodeIdx; - generatedSteps.push(createStep("get", key, undefined, `Updated HEAD pointer and reconnected links`, `Node is now at HEAD position with all pointers correctly updated`, 40, substep++, totalSubsteps, "update", undefined, head!)); + generatedSteps.push( + createStep( + "get", + key, + undefined, + `Updated HEAD pointer and reconnected links`, + `Node is now at HEAD position with all pointers correctly updated`, + `SET head = node`, + substep++, + totalSubsteps, + "update", + undefined, + head! + ) + ); + addLines(47, 48, 66, 62); } else { - generatedSteps.push(createStep("get", key, undefined, `Node already at HEAD position`, `No movement needed - node is already the most recently used`, 18, substep++, totalSubsteps, "none", undefined, nodeIdx)); + generatedSteps.push( + createStep( + "get", + key, + undefined, + `Node already at HEAD position`, + `No movement needed - node is already the most recently used`, + `node IS ALREADY head → SKIP`, + substep++, + totalSubsteps, + "none", + undefined, + nodeIdx + ) + ); + addLines(15, 17, 26, 28); } - generatedSteps.push(createStep("get", key, undefined, `Returning value: ${value}`, `GET operation complete - returning node value ${value}`, 11, substep++, totalSubsteps, "none", value, nodeIdx)); + generatedSteps.push( + createStep( + "get", + key, + undefined, + `Returning value: ${val}`, + `GET operation complete - returning node value ${val}`, + `RETURN node.value → ${val}`, + substep++, + totalSubsteps, + "none", + val, + nodeIdx + ) + ); + addLines(16, 18, 27, 29); } else { let substep = 1; const totalSubsteps = 3; - generatedSteps.push(createStep("get", key, undefined, `Checking HashMap for key ${key}...`, `Searching in HashMap to see if key ${key} exists`, 8, substep++, totalSubsteps, "search")); - generatedSteps.push(createStep("get", key, undefined, `Key ${key} not found in cache!`, `HashMap does not contain key ${key} - cache miss`, 8, substep++, totalSubsteps, "none", -1)); - generatedSteps.push(createStep("get", key, undefined, `Returning -1 (cache miss)`, `GET operation complete - key not found, returning -1`, 8, substep++, totalSubsteps, "none", -1)); + generatedSteps.push( + createStep( + "get", + key, + undefined, + `Checking HashMap for key ${key}...`, + `Searching in HashMap to see if key ${key} exists`, + `IF key = ${key} IN cache`, + substep++, + totalSubsteps, + "search" + ) + ); + addLines(13, 14, 22, 24); + + generatedSteps.push( + createStep( + "get", + key, + undefined, + `Key ${key} not found in cache!`, + `HashMap does not contain key ${key} - cache miss`, + `node NOT IN cache → MISS ✗`, + substep++, + totalSubsteps, + "none", + -1 + ) + ); + addLines(13, 14, 23, 25); + + generatedSteps.push( + createStep( + "get", + key, + undefined, + `Returning -1 (cache miss)`, + `GET operation complete - key not found, returning -1`, + `RETURN -1`, + substep++, + totalSubsteps, + "none", + -1 + ) + ); + addLines(13, 15, 23, 25); } }; - const generatePutSteps = (key: number, value: number) => { + const generatePutSteps = (key: number, val: number) => { const hasKey = cache.has(key); - if (hasKey) { const nodeIdx = cache.get(key)!; let substep = 1; const totalSubsteps = 10; - generatedSteps.push(createStep("put", key, value, `Called put(${key}, ${value})`, `Starting PUT operation for key ${key} with value ${value}`, 14, substep++, totalSubsteps, "none")); - generatedSteps.push(createStep("put", key, value, `Checking if key ${key} exists...`, `Searching HashMap to check if key already exists in cache`, 15, substep++, totalSubsteps, "search", undefined, undefined, key)); - generatedSteps.push(createStep("put", key, value, `Key ${key} found in cache`, `Key exists - will update the value instead of creating new node`, 15, substep++, totalSubsteps, "search", undefined, nodeIdx, key)); - generatedSteps.push(createStep("put", key, value, `Retrieved node from HashMap`, `Got reference to existing node at index ${nodeIdx}`, 16, substep++, totalSubsteps, "update", undefined, nodeIdx)); + generatedSteps.push( + createStep( + "put", + key, + val, + `Called put(${key}, ${val})`, + `Starting PUT operation for key ${key} with value ${val}`, + `CALL put(key = ${key}, value = ${val})`, + substep++, + totalSubsteps, + "none" + ) + ); + addLines(18, 19, 29, 31); + + generatedSteps.push( + createStep( + "put", + key, + val, + `Checking if key ${key} exists...`, + `Searching HashMap to check if key already exists in cache`, + `IF key = ${key} IN cache`, + substep++, + totalSubsteps, + "search", + undefined, + undefined, + key + ) + ); + addLines(19, 20, 30, 32); + + generatedSteps.push( + createStep( + "put", + key, + val, + `Key ${key} found in cache`, + `Key exists - will update the value instead of creating new node`, + `node = cache[${key}] → FOUND ✓`, + substep++, + totalSubsteps, + "search", + undefined, + nodeIdx, + key + ) + ); + addLines(19, 20, 30, 32); + + generatedSteps.push( + createStep( + "put", + key, + val, + `Retrieved node from HashMap`, + `Got reference to existing node at index ${nodeIdx}`, + `SET node = cache[${key}]`, + substep++, + totalSubsteps, + "update", + undefined, + nodeIdx + ) + ); + addLines(20, 21, 31, 33); const oldValue = nodes[nodeIdx].value; - nodes[nodeIdx].value = value; - generatedSteps.push(createStep("put", key, value, `Updated value: ${oldValue} → ${value}`, `Changed node value from ${oldValue} to ${value}`, 17, substep++, totalSubsteps, "update", undefined, nodeIdx)); - generatedSteps.push(createStep("put", key, value, `Moving node to HEAD...`, `Mark this node as most recently used by moving to front`, 18, substep++, totalSubsteps, "move", undefined, nodeIdx)); + nodes[nodeIdx].value = val; + generatedSteps.push( + createStep( + "put", + key, + val, + `Updated value: ${oldValue} → ${val}`, + `Changed node value from ${oldValue} to ${val}`, + `SET node.value = ${val}`, + substep++, + totalSubsteps, + "update", + undefined, + nodeIdx + ) + ); + addLines(21, 22, 32, 34); + + generatedSteps.push( + createStep( + "put", + key, + val, + `Moving node to HEAD...`, + `Mark this node as most recently used by moving to front`, + `CALL moveToHead(node)`, + substep++, + totalSubsteps, + "move", + undefined, + nodeIdx + ) + ); + addLines(22, 23, 33, 35); const node = nodes[nodeIdx]; const isAlreadyHead = head === nodeIdx; if (!isAlreadyHead) { - generatedSteps.push(createStep("put", key, value, `Disconnecting from current position...`, `Removing node from its current position in the list`, 31, substep++, totalSubsteps, "delete", undefined, nodeIdx)); + generatedSteps.push( + createStep( + "put", + key, + val, + `Disconnecting from current position...`, + `Removing node from its current position in the list`, + `CALL removeNode(node)`, + substep++, + totalSubsteps, + "delete", + undefined, + nodeIdx + ) + ); + addLines(37, 34, 48, 52); if (node.prev !== null) nodes[node.prev].next = node.next; if (node.next !== null) nodes[node.next].prev = node.prev; if (head === nodeIdx) head = node.next; if (tail === nodeIdx) tail = node.prev; - generatedSteps.push(createStep("put", key, value, `Reconnecting at HEAD position...`, `Adding node to front of the doubly linked list`, 32, substep++, totalSubsteps, "create", undefined, nodeIdx)); + generatedSteps.push( + createStep( + "put", + key, + val, + `Reconnecting at HEAD position...`, + `Adding node to front of the doubly linked list`, + `CALL addToHead(node)`, + substep++, + totalSubsteps, + "create", + undefined, + nodeIdx + ) + ); + addLines(43, 43, 60, 58); node.next = head; node.prev = null; @@ -252,68 +821,382 @@ export const LRUCacheVisualization = () => { head = nodeIdx; if (tail === null) tail = nodeIdx; - generatedSteps.push(createStep("put", key, value, `Updated HEAD and all pointers`, `Node is now at HEAD with all links correctly set`, 40, substep++, totalSubsteps, "update", undefined, head!)); + generatedSteps.push( + createStep( + "put", + key, + val, + `Updated HEAD and all pointers`, + `Node is now at HEAD with all links correctly set`, + `SET head = node`, + substep++, + totalSubsteps, + "update", + undefined, + head! + ) + ); + addLines(47, 48, 66, 62); } else { - generatedSteps.push(createStep("put", key, value, `Node already at HEAD`, `No movement needed - already most recently used`, 25, substep++, totalSubsteps, "none", undefined, nodeIdx)); + generatedSteps.push( + createStep( + "put", + key, + val, + `Node already at HEAD`, + `No movement needed - already most recently used`, + `node IS ALREADY head → SKIP`, + substep++, + totalSubsteps, + "none", + undefined, + nodeIdx + ) + ); + addLines(22, 23, 33, 35); } - generatedSteps.push(createStep("put", key, value, `PUT operation complete`, `Successfully updated key ${key} with value ${value}`, 18, substep++, totalSubsteps, "none", undefined, head!)); + generatedSteps.push( + createStep( + "put", + key, + val, + `PUT operation complete`, + `Successfully updated key ${key} with value ${val}`, + `RETURN`, + substep++, + totalSubsteps, + "none", + undefined, + head! + ) + ); + addLines(22, 23, 33, 35); } else { const needsEviction = cache.size >= capacity; const totalSubsteps = needsEviction ? 18 : 12; let substep = 1; - generatedSteps.push(createStep("put", key, value, `Called put(${key}, ${value})`, `Starting PUT operation for new key ${key} with value ${value}`, 14, substep++, totalSubsteps, "none")); - generatedSteps.push(createStep("put", key, value, `Checking if key ${key} exists...`, `Searching HashMap to check if key already in cache`, 15, substep++, totalSubsteps, "search")); - generatedSteps.push(createStep("put", key, value, `Key ${key} not found - creating new node`, `Key doesn't exist, will create and insert new node`, 20, substep++, totalSubsteps, "none")); - generatedSteps.push(createStep("put", key, value, `Creating new node {key: ${key}, value: ${value}}`, `Allocating new doubly linked list node with provided key and value`, 20, substep++, totalSubsteps, "create")); + generatedSteps.push( + createStep( + "put", + key, + val, + `Called put(${key}, ${val})`, + `Starting PUT operation for new key ${key} with value ${val}`, + `CALL put(key = ${key}, value = ${val})`, + substep++, + totalSubsteps, + "none" + ) + ); + addLines(18, 19, 29, 31); + + generatedSteps.push( + createStep( + "put", + key, + val, + `Checking if key ${key} exists...`, + `Searching HashMap to check if key already in cache`, + `IF key = ${key} IN cache`, + substep++, + totalSubsteps, + "search" + ) + ); + addLines(19, 20, 30, 32); + + generatedSteps.push( + createStep( + "put", + key, + val, + `Key ${key} not found - creating new node`, + `Key doesn't exist, will create and insert new node`, + `new_node = DLLNode(${key}, ${val})`, + substep++, + totalSubsteps, + "none" + ) + ); + addLines(24, 25, 35, 37); + + generatedSteps.push( + createStep( + "put", + key, + val, + `Creating new node {key: ${key}, value: ${val}}`, + `Allocating new doubly linked list node with provided key and value`, + `SET cache[${key}] = new_node`, + substep++, + totalSubsteps, + "create" + ) + ); + addLines(24, 25, 35, 37); const newIdx = nodes.length; - const newNode: DLLNode = { key, value, prev: null, next: head }; + const newNode: DLLNode = { key, value: val, prev: null, next: head }; nodes.push(newNode); cache.set(key, newIdx); - generatedSteps.push(createStep("put", key, value, `Added entry to HashMap: ${key} → node[${newIdx}]`, `HashMap now maps key ${key} to node index ${newIdx}`, 21, substep++, totalSubsteps, "create", undefined, newIdx, key)); - generatedSteps.push(createStep("put", key, value, `Adding new node to HEAD of DLL...`, `Inserting node at the front of the doubly linked list`, 22, substep++, totalSubsteps, "create", undefined, newIdx)); - generatedSteps.push(createStep("put", key, value, `Setting node.next = ${head !== null ? `node[${head}]` : "null"}`, `New node's next pointer points to current HEAD`, 43, substep++, totalSubsteps, "update", undefined, newIdx)); - generatedSteps.push(createStep("put", key, value, `Setting node.prev = null`, `New node's prev pointer is null (it will be the first node)`, 44, substep++, totalSubsteps, "update", undefined, newIdx)); + generatedSteps.push( + createStep( + "put", + key, + val, + `Added entry to HashMap: ${key} → node[${newIdx}]`, + `HashMap now maps key ${key} to node index ${newIdx}`, + `SET cache[${key}] = new_node`, + substep++, + totalSubsteps, + "create", + undefined, + newIdx, + key + ) + ); + addLines(25, 26, 36, 38); + + generatedSteps.push( + createStep( + "put", + key, + val, + `Adding new node to HEAD of DLL...`, + `Inserting node at the front of the doubly linked list`, + `CALL addToHead(new_node)`, + substep++, + totalSubsteps, + "create", + undefined, + newIdx + ) + ); + addLines(26, 27, 37, 39); + + generatedSteps.push( + createStep( + "put", + key, + val, + `Setting node.next = ${head !== null ? `node[${head}]` : "null"}`, + `New node's next pointer points to current HEAD`, + `SET new_node.next = head`, + substep++, + totalSubsteps, + "update", + undefined, + newIdx + ) + ); + addLines(44, 44, 61, 59); + + generatedSteps.push( + createStep( + "put", + key, + val, + `Setting node.prev = null`, + `New node's prev pointer is null (it will be the first node)`, + `SET new_node.prev = null`, + substep++, + totalSubsteps, + "update", + undefined, + newIdx + ) + ); + addLines(45, 45, 62, 60); if (head !== null) { nodes[head].prev = newIdx; - generatedSteps.push(createStep("put", key, value, `Updating old HEAD's prev pointer`, `Old HEAD node[${head}] now points back to new node`, 45, substep++, totalSubsteps, "update", undefined, head)); + generatedSteps.push( + createStep( + "put", + key, + val, + `Updating old HEAD's prev pointer`, + `Old HEAD node[${head}] now points back to new node`, + `SET head.prev = new_node`, + substep++, + totalSubsteps, + "update", + undefined, + head + ) + ); + addLines(46, 47, 64, 61); } head = newIdx; - generatedSteps.push(createStep("put", key, value, `Updated HEAD pointer to node[${newIdx}]`, `HEAD now points to the newly inserted node`, 46, substep++, totalSubsteps, "update", undefined, head)); + generatedSteps.push( + createStep( + "put", + key, + val, + `Updated HEAD pointer to node[${newIdx}]`, + `HEAD now points to the newly inserted node`, + `SET head = new_node`, + substep++, + totalSubsteps, + "update", + undefined, + head + ) + ); + addLines(47, 48, 66, 62); if (tail === null) { tail = newIdx; - generatedSteps.push(createStep("put", key, value, `Updated TAIL pointer (list was empty)`, `TAIL also points to node[${newIdx}] since it's the only node`, 47, substep++, totalSubsteps, "update", undefined, tail)); + generatedSteps.push( + createStep( + "put", + key, + val, + `Updated TAIL pointer (list was empty)`, + `TAIL also points to node[${newIdx}] since it's the only node`, + `SET tail = new_node`, + substep++, + totalSubsteps, + "update", + undefined, + tail + ) + ); + addLines(48, 50, 68, 63); } if (cache.size > capacity) { - generatedSteps.push(createStep("put", key, value, `Cache full (size > ${capacity}). Evicting LRU item...`, `Number of items exceeds capacity - need to remove the least recently used node`, 23, substep++, totalSubsteps, "none")); - generatedSteps.push(createStep("put", key, value, `Identifying node to remove (TAIL node)...`, `The node at the end of the list (TAIL) is the least recently used`, 24, substep++, totalSubsteps, "none", undefined, tail!)); + generatedSteps.push( + createStep( + "put", + key, + val, + `Cache full (size > ${capacity}). Evicting LRU item...`, + `Number of items exceeds capacity - need to remove the least recently used node`, + `IF cache.size > capacity`, + substep++, + totalSubsteps, + "none" + ) + ); + addLines(27, 28, 38, 40); + + generatedSteps.push( + createStep( + "put", + key, + val, + `Identifying node to remove (TAIL node)...`, + `The node at the end of the list (TAIL) is the least recently used`, + `SET removed = removeTail()`, + substep++, + totalSubsteps, + "none", + undefined, + tail! + ) + ); + addLines(28, 29, 39, 41); const tailIdx = tail!; const tailNode = nodes[tailIdx]; - generatedSteps.push(createStep("put", key, value, `Removing TAIL node from DLL...`, `Calling removeTail() to disconnect the node at the end`, 41, substep++, totalSubsteps, "delete", undefined, tailIdx)); + generatedSteps.push( + createStep( + "put", + key, + val, + `Removing TAIL node from DLL...`, + `Calling removeTail() to disconnect the node at the end`, + `CALL removeTail()`, + substep++, + totalSubsteps, + "delete", + undefined, + tailIdx + ) + ); + addLines(50, 51, 71, 65); - // removeNode logic if (tailNode.prev !== null) { - generatedSteps.push(createStep("put", key, value, `Updating previous node's next pointer`, `Setting node[${tailNode.prev}].next = null`, 36, substep++, totalSubsteps, "update", undefined, tailNode.prev)); + generatedSteps.push( + createStep( + "put", + key, + val, + `Updating previous node's next pointer`, + `Setting node[${tailNode.prev}].next = null`, + `SET tail.prev.next = null`, + substep++, + totalSubsteps, + "update", + undefined, + tailNode.prev + ) + ); + addLines(38, 36, 50, 53); nodes[tailNode.prev].next = null; } tail = tailNode.prev; - generatedSteps.push(createStep("put", key, value, `Updated TAIL pointer to previous node`, `TAIL now points to node[${tail}]`, 39, substep++, totalSubsteps, "update", undefined, tail!)); + generatedSteps.push( + createStep( + "put", + key, + val, + `Updated TAIL pointer to previous node`, + `TAIL now points to node[${tail}]`, + `SET tail = tail.prev`, + substep++, + totalSubsteps, + "update", + undefined, + tail! + ) + ); + addLines(41, 42, 57, 56); - generatedSteps.push(createStep("put", key, value, `Deleting key ${tailNode.key} from HashMap`, `HashMap entry removed - eviction complete`, 25, substep++, totalSubsteps, "delete", undefined, tailIdx, undefined, tailIdx)); + generatedSteps.push( + createStep( + "put", + key, + val, + `Deleting key ${tailNode.key} from HashMap`, + `HashMap entry removed - eviction complete`, + `DELETE cache[removed.key]`, + substep++, + totalSubsteps, + "delete", + undefined, + tailIdx, + undefined, + tailIdx + ) + ); + addLines(29, 30, 40, 42); cache.delete(tailNode.key); } - generatedSteps.push(createStep("put", key, value, `PUT operation complete`, `Successfully inserted new key ${key}`, 27, substep++, totalSubsteps, "none", undefined, head!)); + generatedSteps.push( + createStep( + "put", + key, + val, + `PUT operation complete`, + `Successfully inserted new key ${key}`, + `RETURN`, + substep++, + totalSubsteps, + "none", + undefined, + head! + ) + ); + addLines(18, 19, 29, 31); } }; @@ -326,267 +1209,188 @@ export const LRUCacheVisualization = () => { }); setSteps(generatedSteps); + setStepLineNumbers(stepLines); }, []); - // Auto-play logic - useEffect(() => { - if (isPlaying && currentStepIndex < steps.length - 1) { - intervalRef.current = setInterval(() => { - setCurrentStepIndex((prev) => { - if (prev >= steps.length - 1) { - setIsPlaying(false); - return prev; - } - return prev + 1; - }); - }, 1000 / speed); - } else { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - } - - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - }; - }, [isPlaying, currentStepIndex, steps.length, speed]); - - const handlePlay = () => setIsPlaying(true); - const handlePause = () => setIsPlaying(false); - const handleStepForward = () => { - if (currentStepIndex < steps.length - 1) { - setCurrentStepIndex((prev) => prev + 1); - } - }; - const handleStepBack = () => { - if (currentStepIndex > 0) { - setCurrentStepIndex((prev) => prev - 1); - } - }; - const handleReset = () => { - setCurrentStepIndex(0); - setIsPlaying(false); - }; - if (steps.length === 0) return null; - const currentStep = steps[currentStepIndex]; + const currentStep = steps[currentStepIndex] || steps[0]; + const pseudoSteps = steps.map((s) => s.pseudoStep); + + // Traverse doubly linked list in actual sequence from HEAD to TAIL + const orderedNodes: number[] = []; + let currentIdx = currentStep.head; + const visited = new Set(); + while (currentIdx !== null && !visited.has(currentIdx)) { + visited.add(currentIdx); + orderedNodes.push(currentIdx); + currentIdx = currentStep.nodes[currentIdx].next; + } return ( -
- - - - -
- -
- - Step {currentStepIndex + 1} / {steps.length} - -
- Speed: - setSpeed(val[0])} - min={0.5} - max={3} - step={0.5} - className="w-24" - /> - {speed}x -
-
-
+ } leftContent={ -
- {/* Operation Display */} -
-

Current Operation

-

- {currentStep.operation} -

-

- {currentStep.message} -

-

- {currentStep.detailedMessage} -

-
- Substep {currentStep.substep} of {currentStep.totalSubsteps} -
-
+
+
+ {/* Operation Display */} + +

+ Current Operation +

+

+ {currentStep.operation} +

+
+ Substep {currentStep.substep} of {currentStep.totalSubsteps} +
+
- {/* HashMap Visualization */} -
-

HashMap (Cache)

-
- {Array.from(currentStep.hashMap.entries()).map( - ([key, nodeIdx]) => ( + {/* HashMap Visualization */} + +

+ HashMap (Cache Map) +

+
+ {Array.from(currentStep.hashMap.entries()).map(([key, nodeIdx]) => (
-
Key
-
{key}
-
→ node[{nodeIdx}]
+
Key
+
{key}
+
+ → node[{nodeIdx}] +
- ) + ))} +
+ {currentStep.hashMap.size === 0 && ( +

+ Empty cache +

)} -
- {currentStep.hashMap.size === 0 && ( -

- Empty cache -

- )} -
+ - {/* Doubly Linked List Visualization */} -
-

- Doubly Linked List (LRU Order) -

-
- {currentStep.head !== null && ( -
- HEAD → -
- )} - {currentStep.nodes.map((node, idx) => { - let currentIdx: number | null = currentStep.head; - const orderedNodes: number[] = []; - while (currentIdx !== null) { - orderedNodes.push(currentIdx); - currentIdx = currentStep.nodes[currentIdx].next; - } - - if (!orderedNodes.includes(idx)) return null; - - const isHighlighted = currentStep.highlightedNode === idx; - const isEvicted = currentStep.evictedNode === idx; - const animType = currentStep.animationType; - - let colorClass = "border-border bg-muted/30"; - if (isEvicted) { - colorClass = "border-destructive bg-destructive/20 animate-pulse"; - } else if (isHighlighted) { - if (animType === "create") { - colorClass = "border-success bg-success/20 scale-110"; - } else if (animType === "delete") { - colorClass = "border-destructive bg-destructive/10 scale-90"; - } else if (animType === "move") { - colorClass = "border-primary bg-primary/20 scale-105 animate-pulse"; - } else if (animType === "update") { - colorClass = "border-secondary bg-secondary/20 scale-105"; - } else if (animType === "search") { - colorClass = "border-primary bg-primary/10 scale-105"; - } else { - colorClass = "border-primary bg-primary/20 scale-110 shadow-lg"; + {/* Doubly Linked List Visualization */} + +

+ Doubly Linked List (Most to Least Recently Used) +

+
+ {currentStep.head !== null && ( +
+ HEAD +
+ )} + {orderedNodes.map((nodeIdx, displayIdx) => { + const node = currentStep.nodes[nodeIdx]; + const isHighlighted = currentStep.highlightedNode === nodeIdx; + const isEvicted = currentStep.evictedNode === nodeIdx; + const animType = currentStep.animationType; + + let colorClass = "border-border bg-muted/30"; + if (isEvicted) { + colorClass = "border-destructive bg-destructive/20 animate-pulse scale-95"; + } else if (isHighlighted) { + if (animType === "create") { + colorClass = "border-green-500 bg-green-500/10 scale-105"; + } else if (animType === "delete") { + colorClass = "border-destructive bg-destructive/10 scale-95"; + } else if (animType === "move") { + colorClass = "border-primary bg-primary/20 scale-105"; + } else if (animType === "update") { + colorClass = "border-amber-500 bg-amber-500/10 scale-105"; + } else if (animType === "search") { + colorClass = "border-blue-500 bg-blue-500/10 scale-105"; + } else { + colorClass = "border-primary bg-primary/15 scale-105"; + } } - } - - return ( -
-
-
- K: {node.key} -
-
- V: {node.value} -
- {node.prev !== null && ( -
- ← prev -
+ + return ( +
+ {displayIdx > 0 && ( +
)} - {node.next !== null && ( -
- next → +
+
+ Key: {node.key}
- )} +
+ Val: {node.value} +
+
+ [{nodeIdx}] +
+
- {node.next !== null && orderedNodes.includes(node.next) && ( -
- )} + ); + })} + {currentStep.tail !== null && ( +
+ TAIL
- ); - })} - {currentStep.tail !== null && ( -
- ← TAIL -
- )} -
- {currentStep.head === null && ( -

- Empty list + )} + {currentStep.head === null && ( +

+ Empty list +

+ )} +
+ +
+ + {/* Commentary & Variable Panel in mt-auto */} +
+ +
+

+ Step Explanation +

+

+ {currentStep.explanation}

- )} + +
} rightContent={ -
- - -
+ setCurrentStepIndex(0)} + /> } /> ); diff --git a/src/components/visualizations/algorithms/LongestConsecutiveSequenceVisualization.tsx b/src/components/visualizations/algorithms/LongestConsecutiveSequenceVisualization.tsx index 50aa35c..d1f9e99 100644 --- a/src/components/visualizations/algorithms/LongestConsecutiveSequenceVisualization.tsx +++ b/src/components/visualizations/algorithms/LongestConsecutiveSequenceVisualization.tsx @@ -1,381 +1,372 @@ -import { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { Card } from '@/components/ui/card'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; -import { motion } from 'framer-motion'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; +import { Info } from 'lucide-react'; interface Step { nums: number[]; - numSet: Set; + numSet: number[]; currentNum: number | null; checking: number | null; longestStreak: number; currentStreak: number; variables: Record; explanation: string; - highlightedLines: number[]; - lineExecution: string; + pseudoStep: string; } -export const LongestConsecutiveSequenceVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); - - const nums = [100, 4, 200, 1, 3, 2]; - - const steps: Step[] = [ - { - nums, - numSet: new Set(), - currentNum: null, - checking: null, - longestStreak: 0, - currentStreak: 0, - variables: { nums: '[100,4,200,1,3,2]' }, - explanation: "Given an unsorted array of integers nums, return the length of the longest consecutive elements sequence.", - highlightedLines: [1], - lineExecution: "function longestConsecutive(nums: number[]): number" - }, - { - nums, - numSet: new Set(), - currentNum: null, - checking: null, - longestStreak: 0, - currentStreak: 0, - variables: { length: 6 }, - explanation: "Check edge case: nums.length > 0, continue.", - highlightedLines: [2], - lineExecution: "if (nums.length === 0) return 0; // false" - }, - { - nums, - numSet: new Set([100, 4, 200, 1, 3, 2]), - currentNum: null, - checking: null, - longestStreak: 0, - currentStreak: 0, - variables: { numSet: '{1,2,3,4,100,200}' }, - explanation: "Create a Set from the array. This allows us to check if a number exists in O(1) time, which is key to maintaining O(n) overall complexity.", - highlightedLines: [4], - lineExecution: "const numSet = new Set(nums);" - }, - { - nums, - numSet: new Set([100, 4, 200, 1, 3, 2]), - currentNum: null, - checking: null, - longestStreak: 0, - currentStreak: 0, - variables: { longestStreak: 0 }, - explanation: "Initialize longestStreak = 0 to track maximum consecutive length found.", - highlightedLines: [5], - lineExecution: "let longestStreak = 0;" - }, - { - nums, - numSet: new Set([100, 4, 200, 1, 3, 2]), - currentNum: 1, - checking: 0, - longestStreak: 0, - currentStreak: 0, - variables: { num: 1, 'num-1': 0 }, - explanation: "We only start counting a sequence from its smallest element. Since 0 is not in the set, 1 must be the start of a potential sequence.", - highlightedLines: [7, 8], - lineExecution: "for (const num of numSet) if (!numSet.has(num - 1)) // !has(0) -> true" - }, - { - nums, - numSet: new Set([100, 4, 200, 1, 3, 2]), - currentNum: 1, - checking: null, - longestStreak: 0, - currentStreak: 1, - variables: { currentNum: 1, currentStreak: 1 }, - explanation: "1 starts sequence. Initialize: currentNum=1, currentStreak=1.", - highlightedLines: [9, 10], - lineExecution: "let currentNum = num; let currentStreak = 1;" - }, - { - nums, - numSet: new Set([100, 4, 200, 1, 3, 2]), - currentNum: 2, - checking: 2, - longestStreak: 0, - currentStreak: 2, - variables: { 'checking': 2, found: true }, - explanation: "Check the set for the next consecutive number (2). It exists! We increment the current streak to 2.", - highlightedLines: [12, 13, 14], - lineExecution: "while (numSet.has(currentNum + 1)) currentNum++; // 2 exists" - }, - { - nums, - numSet: new Set([100, 4, 200, 1, 3, 2]), - currentNum: 3, - checking: 3, - longestStreak: 0, - currentStreak: 3, - variables: { currentNum: 3, currentStreak: 3 }, - explanation: "Check: does 3 exist? Yes! Increment: currentNum=3, currentStreak=3.", - highlightedLines: [12, 13, 14], - lineExecution: "while (numSet.has(3 + 1)) // 3 exists" - }, - { - nums, - numSet: new Set([100, 4, 200, 1, 3, 2]), - currentNum: 4, - checking: 4, - longestStreak: 0, - currentStreak: 4, - variables: { currentNum: 4, currentStreak: 4 }, - explanation: "Check: does 4 exist? Yes! Increment: currentNum=4, currentStreak=4.", - highlightedLines: [12, 13, 14], - lineExecution: "while (numSet.has(4 + 1)) // 4 exists" - }, - { - nums, - numSet: new Set([100, 4, 200, 1, 3, 2]), - currentNum: 4, - checking: 5, - longestStreak: 0, - currentStreak: 4, - variables: { 'checking': 5, found: false }, - explanation: "Check for 5. It's not in the set, so the current sequence [1, 2, 3, 4] is complete with a length of 4.", - highlightedLines: [12], - lineExecution: "while (numSet.has(4 + 1)) // false, exit loop" - }, - { - nums, - numSet: new Set([100, 4, 200, 1, 3, 2]), - currentNum: 4, - checking: null, - longestStreak: 4, - currentStreak: 4, - variables: { longestStreak: 4 }, - explanation: "Update longestStreak: max(0, 4) = 4. Found sequence [1,2,3,4].", - highlightedLines: [17], - lineExecution: "longestStreak = Math.max(longestStreak, currentStreak); // max(0,4) = 4" - }, - { - nums, - numSet: new Set([100, 4, 200, 1, 3, 2]), - currentNum: 2, - checking: 1, - longestStreak: 4, - currentStreak: 0, - variables: { num: 2, 'num-1': 1 }, - explanation: "Check num=2. Since 1 is in the set, 2 is already part of a sequence we've either processed or will process from its start. Skip it to avoid redundant work.", - highlightedLines: [7, 8], - lineExecution: "if (!numSet.has(num - 1)) // !has(1) -> false, skip" - }, - { - nums, - numSet: new Set([100, 4, 200, 1, 3, 2]), - currentNum: 3, - checking: 2, - longestStreak: 4, - currentStreak: 0, - variables: { num: 3 }, - explanation: "Checking 3. Since 2 is in the set, we know 3 is part of a sequence that starts at a number smaller than 3. We skip it, knowing we've already counted it when we processed its sequence starting from 1.", - highlightedLines: [7, 8], - lineExecution: "if (!numSet.has(2)) // false, skip" - }, - { - nums, - numSet: new Set([100, 4, 200, 1, 3, 2]), - currentNum: 4, - checking: 3, - longestStreak: 4, - currentStreak: 0, - variables: { num: 4 }, - explanation: "Checking 4. Since 3 is in the set, 4 is not the start of a sequence. Skipping to maintain O(n) complexity, ensuring each number is only part of one while-loop iteration.", - highlightedLines: [7, 8], - lineExecution: "if (!numSet.has(3)) // false, skip" - }, - { - nums, - numSet: new Set([100, 4, 200, 1, 3, 2]), - currentNum: 100, - checking: 99, - longestStreak: 4, - currentStreak: 1, - variables: { num: 100 }, - explanation: "Checking 100. 99 is not in the set, so 100 starts a sequence. We check for 101, which isn't there, so this sequence has length 1.", - highlightedLines: [7, 8, 9, 10, 12], - lineExecution: "!numSet.has(99) -> true; !numSet.has(101) -> exit; streak = 1" - }, - { - nums, - numSet: new Set([100, 4, 200, 1, 3, 2]), - currentNum: 200, - checking: 199, - longestStreak: 4, - currentStreak: 1, - variables: { num: 200 }, - explanation: "Checking 200. Since 199 is not in the set, 200 starts its own sequence. We check for 201, find it's also missing, so this sequence length is 1.", - highlightedLines: [7, 8, 9, 10, 12], - lineExecution: "!numSet.has(199) -> true; !numSet.has(201) -> exit; streak = 1" - }, - { - nums, - numSet: new Set([100, 4, 200, 1, 3, 2]), - currentNum: null, - checking: null, - longestStreak: 4, - currentStreak: 0, - variables: { result: 4, sequence: '[1,2,3,4]' }, - explanation: "All numbers in the set have been checked. The longest consecutive sequence found was [1, 2, 3, 4] with a length of 4.", - highlightedLines: [21], - lineExecution: "return longestStreak; // 4" - }, - { - nums, - numSet: new Set([100, 4, 200, 1, 3, 2]), - currentNum: null, - checking: null, - longestStreak: 4, - currentStreak: 0, - variables: { length: 4, complexity: 'O(n)' }, - explanation: "Visualization complete. By only starting sequences from elements with no predecessor, we ensure each element is visited at most twice (once in the main loop, once in a while loop), achieving O(n) time complexity.", - highlightedLines: [21], - lineExecution: "Result: 4" - } - ]; - - const code = `function longestConsecutive(nums: number[]): number { +const languages: VisualizationLanguageMap = { + typescript: `function longestConsecutive(nums: number[]): number { if (nums.length === 0) return 0; - const numSet = new Set(nums); - let longestStreak = 0; - - for (const num of numSet) { + let longest = 0; + for (const num of nums) { if (!numSet.has(num - 1)) { let currentNum = num; let currentStreak = 1; - while (numSet.has(currentNum + 1)) { currentNum++; currentStreak++; } - - longestStreak = Math.max(longestStreak, currentStreak); + longest = Math.max(longest, currentStreak); } } - - return longestStreak; -}`; + return longest; +}`, + + python: `def longestConsecutive(nums: list[int]) -> int: + if not nums: + return 0 + numSet = set(nums) + longest = 0 + for num in nums: + if (num - 1) not in numSet: + currentNum = num + currentStreak = 1 + while (currentNum + 1) in numSet: + currentNum += 1 + currentStreak += 1 + longest = max(longest, currentStreak) + return longest`, + + java: `public static class Solution { + public int longestConsecutive(int[] nums) { + if (nums == null || nums.length == 0) return 0; + Set numSet = new HashSet<>(); + for (int num : nums) { + numSet.add(num); + } + int longest = 0; + for (int num : nums) { + if (!numSet.contains(num - 1)) { + int currentNum = num; + int currentStreak = 1; + while (numSet.contains(currentNum + 1)) { + currentNum++; + currentStreak++; + } + longest = Math.max(longest, currentStreak); + } + } + return longest; + } +}`, + + cpp: `class Solution { +public: + int longestConsecutive(vector& nums) { + unordered_set numSet(nums.begin(), nums.end()); + int longest = 0; + for (int num : nums) { + if (!numSet.count(num - 1)) { + int currentNum = num; + int currentStreak = 1; + while (numSet.count(currentNum + 1)) { + currentNum++; + currentStreak++; + } + longest = max(longest, currentStreak); + } + } + return longest; + } +};` +}; + +export const LongestConsecutiveSequenceVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + + const nums = useMemo(() => [100, 4, 200, 1, 3, 2], []); + + const { steps, stepLineNumbers } = useMemo(() => { + const stepsList: Step[] = []; + const stepLines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLines.typescript!.push(ts); + stepLines.python!.push(py); + stepLines.java!.push(java); + stepLines.cpp!.push(cpp); + }; + + let longestStreak = 0; + const numSet = Array.from(new Set(nums)).sort((a, b) => a - b); + const setObj = new Set(nums); + + const makeSnapshot = ( + msg: string, + pseudo: string, + ts: number, + py: number, + java: number, + cpp: number, + currentNum: number | null, + checking: number | null, + currentStreak: number, + vars: Record + ) => { + stepsList.push({ + nums, + numSet: ts >= 3 ? numSet : [], + currentNum, + checking, + longestStreak, + currentStreak, + variables: { + ...vars, + longest: longestStreak, + streak: currentStreak + }, + explanation: msg, + pseudoStep: pseudo + }); + addLines(ts, py, java, cpp); + }; + + // Step 1: Start + makeSnapshot( + "Find the length of the longest consecutive elements sequence in the unsorted array.", + "START longestConsecutive(nums)", + 1, 1, 2, 3, null, null, 0, {} + ); + + // Step 2: Check empty + makeSnapshot( + "Check if input array is empty.", + "IF nums is empty → NO ✗", + 2, 2, 3, 4, null, null, 0, {} + ); + + // Step 3: Create set + makeSnapshot( + "Insert all numbers into a HashSet to allow O(1) time complexity lookups.", + "SET numSet = Set(nums)", + 3, 3, 4, 5, null, null, 0, { numSet: `{${numSet.join(',')}}` } + ); + + // Step 4: Initialize longest + makeSnapshot( + "Initialize longest streak counter to 0.", + "SET longest = 0", + 4, 5, 8, 5, null, null, 0, {} + ); + + // Loop elements + for (const num of nums) { + makeSnapshot( + `Iterate array: check element ${num}.`, + `FOR num = ${num}`, + 5, 6, 9, 6, num, null, 0, {} + ); + + const hasPrev = setObj.has(num - 1); + makeSnapshot( + `Check if ${num} is the start of a consecutive sequence (i.e. ${num - 1} is NOT in set).`, + `IF num - 1 NOT in numSet → ${num - 1} not in set? → ${!hasPrev ? "YES ✓" : "NO ✗"}`, + 6, 7, 10, 7, num, num - 1, 0, {} + ); + + if (!hasPrev) { + let currentNum = num; + let currentStreak = 1; + + makeSnapshot( + `Since ${num - 1} is not in the set, ${num} is the start of a sequence. Initialize current sequence state.`, + `SET currentNum = ${num}, currentStreak = 1`, + 7, 8, 11, 8, num, null, currentStreak, { currentNum } + ); + + while (setObj.has(currentNum + 1)) { + const nextVal = currentNum + 1; + makeSnapshot( + `Check if next consecutive value ${nextVal} exists in set.`, + `IF currentNum + 1 in numSet → ${nextVal} in set? → YES ✓`, + 9, 10, 13, 10, num, nextVal, currentStreak, { currentNum } + ); + + currentNum++; + currentStreak++; + + makeSnapshot( + `Found ${currentNum}. Increment currentNum to ${currentNum} and currentStreak to ${currentStreak}.`, + `SET currentNum = ${currentNum}, currentStreak = ${currentStreak}`, + 10, 11, 14, 11, num, null, currentStreak, { currentNum } + ); + } - const step = steps[currentStep]; + const nextVal = currentNum + 1; + makeSnapshot( + `Check if next consecutive value ${nextVal} exists in set.`, + `IF currentNum + 1 in numSet → ${nextVal} in set? → NO ✗`, + 9, 10, 13, 10, num, nextVal, currentStreak, { currentNum } + ); + + const oldLongest = longestStreak; + longestStreak = Math.max(longestStreak, currentStreak); + makeSnapshot( + `Update longest streak: max(${oldLongest}, ${currentStreak}) = ${longestStreak}.`, + `SET longest = max(longest, currentStreak) → ${longestStreak}`, + 13, 13, 17, 14, num, null, currentStreak, { currentNum } + ); + } else { + makeSnapshot( + `Since ${num - 1} exists in the set, ${num} cannot be the start of a consecutive sequence (it is processed in a larger sequence). Skip.`, + `Skip ${num}`, + 6, 7, 10, 7, num, null, 0, {} + ); + } + } + + makeSnapshot( + `Finished checking all numbers. Return the longest consecutive sequence length: ${longestStreak}.`, + `RETURN longest → ${longestStreak}`, + 16, 14, 20, 17, null, null, 0, {} + ); + + return { steps: stepsList, stepLineNumbers: stepLines }; + }, [nums]); + + const handleReset = () => { + setCurrentStepIndex(0); + }; + + const step = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( -
- -

Original Array

-
- {step.nums.map((num, idx) => ( +
+ +

+ Array Elements & Visited States +

+
+ {nums.map((num, idx) => { + const isCurrent = num === step.currentNum; + const isChecking = num === step.checking; + return (
{num}
- ))} + ); + })} +
+
+ + {step.numSet.length > 0 && ( + +

+ Number HashSet (O(1) lookups) +

+
+ {step.numSet.map((num, idx) => { + const isCurrent = num === step.currentNum; + const isChecking = num === step.checking; + return ( +
+ {num} +
+ ); + })}
-
- - {step.numSet.size > 0 && ( -
- -

Number Set (O(1) lookup)

-
- {Array.from(step.numSet) - .sort((a, b) => a - b) - .map((num, idx) => ( -
- {num} -
- ))} -
-
-
)} - {step.longestStreak > 0 && ( -
- -
-
- Longest Streak: {step.longestStreak} -
- {step.currentStreak > 0 && ( -
- Current Streak: {step.currentStreak} -
- )} + {/* Commentary Panel */} + +
+
+
+ + + + + + Algorithm Commentary +
- -
- )} +
+ Step {currentStepIndex + 1} of {steps.length} +
+
-
- -
-
Current Execution:
-
- {step.lineExecution} +
+
+
-
- {step.explanation} +
+

+ Current Action +

+
+ {step.explanation} +
- -
- -
- -
- +
+ + + +
} rightContent={ - } controls={ } /> diff --git a/src/components/visualizations/algorithms/LongestRepeatingCharacterReplacementVisualization.tsx b/src/components/visualizations/algorithms/LongestRepeatingCharacterReplacementVisualization.tsx index f638ce0..a1eb0a3 100644 --- a/src/components/visualizations/algorithms/LongestRepeatingCharacterReplacementVisualization.tsx +++ b/src/components/visualizations/algorithms/LongestRepeatingCharacterReplacementVisualization.tsx @@ -1,10 +1,11 @@ -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import { Card } from '@/components/ui/card'; import { motion } from 'framer-motion'; import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { SimpleStepControls } from '../shared/SimpleStepControls'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; import { VisualizationLayout } from '../shared/VisualizationLayout'; +import type { VisualizationLanguageMap, StepLineNumberMap } from '@/types/visualization'; interface Step { l: number; @@ -12,454 +13,176 @@ interface Step { maxf: number; res: number; count: Record; - highlightedLines: number[]; - message: string; + explanation: string; + pseudoStep: string; } -export const LongestRepeatingCharacterReplacementVisualization = () => { - const s = "AABABBA"; - const k = 1; +const languages: VisualizationLanguageMap = { + python: `def characterReplacement(s: str, k: int) -> int: + count = {} + res = 0 + l = 0 + maxf = 0 + for r in range(len(s)): + count[s[r]] = 1 + count.get(s[r], 0) + maxf = max(maxf, count[s[r]]) + while (r - l + 1) - maxf > k: + count[s[l]] -= 1 + l += 1 + res = max(res, r - l + 1) + return res`, - const code = `function characterReplacement(s: string, k: number): number { + typescript: `function characterReplacement(s: string, k: number): number { const count: { [char: string]: number } = {}; let res = 0; let l = 0; let maxf = 0; - for (let r = 0; r < s.length; r++) { count[s[r]] = 1 + (count[s[r]] || 0); maxf = Math.max(maxf, count[s[r]]); + while ((r - l + 1) - maxf > k) { + count[s[l]] -= 1; + l += 1; + } + res = Math.max(res, r - l + 1); + } + return res; +}`, + + java: `public class Solution { + public int characterReplacement(String s, int k) { + int[] count = new int[26]; + int res = 0; + int l = 0; + int maxf = 0; + for (int r = 0; r < s.length(); r++) { + count[s.charAt(r) - 'A']++; + maxf = Math.max(maxf, count[s.charAt(r) - 'A']); + while ((r - l + 1) - maxf > k) { + count[s.charAt(l) - 'A']--; + l++; + } + res = Math.max(res, r - l + 1); + } + return res; + } +}`, + + cpp: `class Solution { +public: + int characterReplacement(string s, int k) { + unordered_map count; + int res = 0; + int l = 0; + int maxf = 0; + for (int r = 0; r < s.length(); r++) { + char currentChar = s[r]; + count[currentChar]++; + maxf = max(maxf, count[currentChar]); + while ((r - l + 1) - maxf > k) { + count[s[l]]--; + l += 1; + } + res = max(res, r - l + 1); + } + return res; + } +};` +}; + +const generateVisualizationData = () => { + const s = "AABABBA"; + const k = 1; + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + const count: Record = {}; + let res = 0; + let l = 0; + let maxf = 0; + + const addStep = (msg: string, pseudo: string, tsLine: number, pyLine: number, javaLine: number, cppLine: number) => { + steps.push({ + l, + r: steps.length === 0 ? -1 : r, + maxf, + res, + count: { ...count }, + explanation: msg, + pseudoStep: pseudo + }); + addLines(tsLine, pyLine, javaLine, cppLine); + }; + + // 1. Initial State + addStep("Initialize variables: l = 0, res = 0, count = {}, maxf = 0.", "CALL characterReplacement(s, k)", 2, 2, 3, 4); + + let r = 0; + for (r = 0; r < s.length; r++) { + // 2. Loop start + addStep(`Increment right pointer r to ${r}. Character at s[${r}] is '${s[r]}'.`, `FOR r = ${r}`, 6, 6, 7, 8); + + // 3. Increment frequency + count[s[r]] = (count[s[r]] || 0) + 1; + addStep(`Increment frequency of '${s[r]}'. count['${s[r]}'] becomes ${count[s[r]]}.`, `SET count[s[r]] = count[s[r]] + 1`, 7, 7, 8, 10); + + // 4. Update maxf + maxf = Math.max(maxf, count[s[r]]); + addStep(`Update maxf to max(maxf, count['${s[r]}']) = ${maxf}.`, `SET maxf = max(maxf, count[s[r]])`, 8, 8, 9, 11); + // 5. Shrink window loop check while ((r - l + 1) - maxf > k) { + addStep(`Window size (${r - l + 1}) - maxf (${maxf}) = ${(r - l + 1) - maxf} > k (${k}). Window is invalid, need to shrink.`, `WHILE (r - l + 1) - maxf > k`, 9, 9, 10, 12); + + // Decrement count count[s[l]] -= 1; + if (count[s[l]] === 0) delete count[s[l]]; + addStep(`Decrement count of leftmost character s[${l}] ('${s[l]}').`, `SET count[s[l]] = count[s[l]] - 1`, 10, 10, 11, 13); + + // Increment l l += 1; + addStep(`Move left pointer l to ${l}.`, `SET l = l + 1`, 11, 11, 12, 14); } + // Check loop finished + addStep(`Window size (${r - l + 1}) - maxf (${maxf}) = ${(r - l + 1) - maxf} <= k (${k}). Window is valid.`, `WHILE (r - l + 1) - maxf > k → FALSE ✗`, 9, 9, 10, 12); + // Update result res = Math.max(res, r - l + 1); + addStep(`Update maximum length res = max(res, window size) = ${res}.`, `SET res = max(res, r - l + 1)`, 13, 12, 14, 16); } - return res; -}`; + // Final return + addStep(`Algorithm completed. Return the maximum length of repeating character substring: ${res}.`, `RETURN res`, 15, 13, 16, 18); - const steps: Step[] = [ - { - l: 0, - r: -1, - maxf: 0, - res: 0, - count: {}, - highlightedLines: [2, 3, 4, 5], - message: "Initialize variables: l = 0, res = 0, count = {}, maxf = 0." - }, - { - l: 0, - r: 0, - maxf: 0, - res: 0, - count: {}, - highlightedLines: [7], - message: "Start loop with r = 0. Character at s[0] is 'A'." - }, - { - l: 0, - r: 0, - maxf: 0, - res: 0, - count: { A: 1 }, - highlightedLines: [8], - message: "Increment frequency of 'A'. count['A'] becomes 1." - }, - { - l: 0, - r: 0, - maxf: 1, - res: 0, - count: { A: 1 }, - highlightedLines: [9], - message: "Update maxf: max(0, 1) = 1." - }, - { - l: 0, - r: 0, - maxf: 1, - res: 0, - count: { A: 1 }, - highlightedLines: [11], - message: "Window size (1) - maxf (1) = 0. Since 0 <= k (1), the window is valid." - }, - { - l: 0, - r: 0, - maxf: 1, - res: 1, - count: { A: 1 }, - highlightedLines: [16], - message: "Update res: max(0, 1) = 1." - }, - { - l: 0, - r: 1, - maxf: 1, - res: 1, - count: { A: 1 }, - highlightedLines: [7], - message: "Increment r to 1. Character at s[1] is 'A'." - }, - { - l: 0, - r: 1, - maxf: 1, - res: 1, - count: { A: 2 }, - highlightedLines: [8], - message: "Increment frequency of 'A'. count['A'] becomes 2." - }, - { - l: 0, - r: 1, - maxf: 2, - res: 1, - count: { A: 2 }, - highlightedLines: [9], - message: "Update maxf: max(1, 2) = 2." - }, - { - l: 0, - r: 1, - maxf: 2, - res: 1, - count: { A: 2 }, - highlightedLines: [11], - message: "Window size (2) - maxf (2) = 0 <= k. Window is valid." - }, - { - l: 0, - r: 1, - maxf: 2, - res: 2, - count: { A: 2 }, - highlightedLines: [16], - message: "Update res: max(1, 2) = 2." - }, - { - l: 0, - r: 2, - maxf: 2, - res: 2, - count: { A: 2 }, - highlightedLines: [7], - message: "Increment r to 2. Character at s[2] is 'B'." - }, - { - l: 0, - r: 2, - maxf: 2, - res: 2, - count: { A: 2, B: 1 }, - highlightedLines: [8], - message: "Increment frequency of 'B'. count['B'] becomes 1." - }, - { - l: 0, - r: 2, - maxf: 2, - res: 2, - count: { A: 2, B: 1 }, - highlightedLines: [9], - message: "Update maxf: max(2, 1) = 2." - }, - { - l: 0, - r: 2, - maxf: 2, - res: 2, - count: { A: 2, B: 1 }, - highlightedLines: [11], - message: "Window size (3) - maxf (2) = 1 <= k. Window is valid (one character can be replaced)." - }, - { - l: 0, - r: 2, - maxf: 2, - res: 3, - count: { A: 2, B: 1 }, - highlightedLines: [16], - message: "Update res: max(2, 3) = 3." - }, - { - l: 0, - r: 3, - maxf: 2, - res: 3, - count: { A: 2, B: 1 }, - highlightedLines: [7], - message: "Increment r to 3. Character at s[3] is 'A'." - }, - { - l: 0, - r: 3, - maxf: 2, - res: 3, - count: { A: 3, B: 1 }, - highlightedLines: [8], - message: "Increment frequency of 'A'. count['A'] becomes 3." - }, - { - l: 0, - r: 3, - maxf: 3, - res: 3, - count: { A: 3, B: 1 }, - highlightedLines: [9], - message: "Update maxf: max(2, 3) = 3." - }, - { - l: 0, - r: 3, - maxf: 3, - res: 3, - count: { A: 3, B: 1 }, - highlightedLines: [11], - message: "Window size (4) - maxf (3) = 1 <= k. Window is valid." - }, - { - l: 0, - r: 3, - maxf: 3, - res: 4, - count: { A: 3, B: 1 }, - highlightedLines: [16], - message: "Update res: max(3, 4) = 4." - }, - { - l: 0, - r: 4, - maxf: 3, - res: 4, - count: { A: 3, B: 1 }, - highlightedLines: [7], - message: "Increment r to 4. Character at s[4] is 'B'." - }, - { - l: 0, - r: 4, - maxf: 3, - res: 4, - count: { A: 3, B: 2 }, - highlightedLines: [8], - message: "Increment frequency of 'B'. count['B'] becomes 2." - }, - { - l: 0, - r: 4, - maxf: 3, - res: 4, - count: { A: 3, B: 2 }, - highlightedLines: [9], - message: "Update maxf: max(3, 2) = 3." - }, - { - l: 0, - r: 4, - maxf: 3, - res: 4, - count: { A: 3, B: 2 }, - highlightedLines: [11], - message: "Window size (5) - maxf (3) = 2 > k. Window is invalid, need to shrink." - }, - { - l: 0, - r: 4, - maxf: 3, - res: 4, - count: { A: 2, B: 2 }, - highlightedLines: [12], - message: "Decrement count of character at l (index 0): 'A'." - }, - { - l: 1, - r: 4, - maxf: 3, - res: 4, - count: { A: 2, B: 2 }, - highlightedLines: [13], - message: "Increment l to 1." - }, - { - l: 1, - r: 4, - maxf: 3, - res: 4, - count: { A: 2, B: 2 }, - highlightedLines: [11], - message: "Window size (4) - maxf (3) = 1 <= k. Window is now valid." - }, - { - l: 1, - r: 4, - maxf: 3, - res: 4, - count: { A: 2, B: 2 }, - highlightedLines: [16], - message: "Update res: max(4, 4) = 4." - }, - { - l: 1, - r: 5, - maxf: 3, - res: 4, - count: { A: 2, B: 2 }, - highlightedLines: [7], - message: "Increment r to 5. Character at s[5] is 'B'." - }, - { - l: 1, - r: 5, - maxf: 3, - res: 4, - count: { A: 2, B: 3 }, - highlightedLines: [8], - message: "Increment frequency of 'B'. count['B'] becomes 3." - }, - { - l: 1, - r: 5, - maxf: 3, - res: 4, - count: { A: 2, B: 3 }, - highlightedLines: [9], - message: "Update maxf: max(3, 3) = 3." - }, - { - l: 1, - r: 5, - maxf: 3, - res: 4, - count: { A: 2, B: 3 }, - highlightedLines: [11], - message: "Window size (5) - maxf (3) = 2 > k. Window is invalid." - }, - { - l: 1, - r: 5, - maxf: 3, - res: 4, - count: { A: 1, B: 3 }, - highlightedLines: [12], - message: "Decrement count of character at l (index 1): 'A'." - }, - { - l: 2, - r: 5, - maxf: 3, - res: 4, - count: { A: 1, B: 3 }, - highlightedLines: [13], - message: "Increment l to 2." - }, - { - l: 2, - r: 5, - maxf: 3, - res: 4, - count: { A: 1, B: 3 }, - highlightedLines: [11], - message: "Window size (4) - maxf (3) = 1 <= k. Window is valid." - }, - { - l: 2, - r: 5, - maxf: 3, - res: 4, - count: { A: 1, B: 3 }, - highlightedLines: [16], - message: "Update res: max(4, 4) = 4." - }, - { - l: 2, - r: 6, - maxf: 3, - res: 4, - count: { A: 1, B: 3 }, - highlightedLines: [7], - message: "Increment r to 6. Character at s[6] is 'A'." - }, - { - l: 2, - r: 6, - maxf: 3, - res: 4, - count: { A: 2, B: 3 }, - highlightedLines: [8], - message: "Increment frequency of 'A'. count['A'] becomes 2." - }, - { - l: 2, - r: 6, - maxf: 3, - res: 4, - count: { A: 2, B: 3 }, - highlightedLines: [9], - message: "Update maxf: max(3, 2) = 3." - }, - { - l: 2, - r: 6, - maxf: 3, - res: 4, - count: { A: 2, B: 3 }, - highlightedLines: [11], - message: "Window size (5) - maxf (3) = 2 > k. Window is invalid." - }, - { - l: 2, - r: 6, - maxf: 3, - res: 4, - count: { A: 2, B: 2 }, - highlightedLines: [12], - message: "Decrement count of character at l (index 2): 'B'." - }, - { - l: 3, - r: 6, - maxf: 3, - res: 4, - count: { A: 2, B: 2 }, - highlightedLines: [13], - message: "Increment l to 3." - }, - { - l: 3, - r: 6, - maxf: 3, - res: 4, - count: { A: 2, B: 2 }, - highlightedLines: [11], - message: "Window size (4) - maxf (3) = 1 <= k. Window is valid." - }, - { - l: 3, - r: 6, - maxf: 3, - res: 4, - count: { A: 2, B: 2 }, - highlightedLines: [16], - message: "Update res: max(4, 4) = 4." - }, - { - l: 3, - r: 6, - maxf: 3, - res: 4, - count: { A: 2, B: 2 }, - highlightedLines: [19], - message: "Loop finished. Final result is 4." - } - ]; + return { steps, stepLineNumbers }; +}; + +export const LongestRepeatingCharacterReplacementVisualization = () => { + const s = "AABABBA"; + const k = 1; const [currentStepIndex, setCurrentStepIndex] = useState(0); + + const { steps, stepLineNumbers } = useMemo(() => { + return generateVisualizationData(); + }, []); + + if (steps.length === 0) return null; + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( { /> } leftContent={ - -
+
+

Input String: "{s}" (k={k})

-
+
{s.split('').map((char, idx) => { const isInWindow = idx >= currentStep.l && idx <= currentStep.r; const isLeft = idx === currentStep.l; @@ -509,7 +232,19 @@ export const LongestRepeatingCharacterReplacementVisualization = () => { })}
+ + {/* Descriptive Commentary Box (at the bottom) */} +
+
+
+ Process Step +
+ {currentStep.explanation} +
+ + {/* Variable Panel (below the commentary box) */} +
{ ...(Object.keys(currentStep.count).length > 0 && { count: JSON.stringify(currentStep.count) }) }} /> - - -

{currentStep.message}

-
- +
} rightContent={ - setCurrentStepIndex(0)} /> } /> diff --git a/src/components/visualizations/algorithms/LongestSubstringWithoutRepeatingCharactersVisualization.tsx b/src/components/visualizations/algorithms/LongestSubstringWithoutRepeatingCharactersVisualization.tsx index 039b7b5..1dd1284 100644 --- a/src/components/visualizations/algorithms/LongestSubstringWithoutRepeatingCharactersVisualization.tsx +++ b/src/components/visualizations/algorithms/LongestSubstringWithoutRepeatingCharactersVisualization.tsx @@ -1,10 +1,11 @@ import { useState, useMemo } from 'react'; import { Card } from '@/components/ui/card'; -import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VisualizationLayout } from '../shared/VisualizationLayout'; import { CheckCircle2, SplitSquareHorizontal, Navigation } from 'lucide-react'; +import type { VisualizationLanguageMap, StepLineNumberMap } from '@/types/visualization'; interface Step { s: string; @@ -12,142 +13,195 @@ interface Step { r: number | null; res: number; charSet: string[]; - message: string; - lineNumber: number; + explanation: string; + pseudoStep: string; isMatch?: boolean; + variables: Record; } -export const LongestSubstringWithoutRepeatingCharactersVisualization = () => { - const [currentStepIndex, setCurrentStepIndex] = useState(0); +const languages: VisualizationLanguageMap = { + python: `def lengthOfLongestSubstring(s: str) -> int: + char_set = set() + l = 0 + res = 0 + for r in range(len(s)): + while s[r] in char_set: + char_set.remove(s[l]) + l += 1 + char_set.add(s[r]) + res = max(res, r - l + 1) + return res`, - const code = `function lengthOfLongestSubstring(s: string): number { + typescript: `function lengthOfLongestSubstring(s: string): number { const charSet = new Set(); let l = 0; let res = 0; - for (let r = 0; r < s.length; r++) { while (charSet.has(s[r])) { charSet.delete(s[l]); l++; } - charSet.add(s[r]); res = Math.max(res, r - l + 1); } - return res; -}`; - - const steps: Step[] = useMemo(() => { - const sArray: Step[] = []; - const s = "pwwkew"; - - let l = 0; - let res = 0; - const charSet = new Set(); - - const snap = (msg: string, line: number, isMatch: boolean = false, overrideR: number | null = null) => { - sArray.push({ - s, - l: line > 3 ? l : null, - r: overrideR, - res, - charSet: Array.from(charSet), - message: msg, - lineNumber: line, - isMatch - }); - }; - - snap(`Execution parsing target substring input string. Memory mappings allocated natively.`, 1, false); - - snap(`Initialized active tracking Hash Set isolating character limits exclusively.`, 2, false); - - snap(`Bound Sliding Window structural constraints initiating lower boundary bounds 'l'.`, 3, false); - - snap(`Stored numerical accumulator dynamically mapping maximum constraints length!`, 4, false); - - for (let r = 0; r < s.length; r++) { - snap(`Advancing window upper frontier boundary natively tracking string scan coordinate 'r'.`, 6, false, r); - - const currentChar = s[r]; - - snap(`Evaluating window compliance checking set natively mapping '${currentChar}'.`, 7, false, r); - - while (charSet.has(s[r])) { - snap(`Collision Detected! The active sliding window already contains '${s[r]}'! Loop Triggered.`, 7, true, r); - - const dropChar = s[l]; - charSet.delete(dropChar); - snap(`Ejecting left-most boundary character '${dropChar}' out of isolated sliding hash set pool globally!`, 8, true, r); - - l++; - snap(`Shrinking structural window limits bounding. Advancing boundary coordinate 'l' natively.`, 9, false, r); - - snap(`Re-evaluating collision status matching native constraint parameters.`, 7, false, r); +}`, + + java: `public class Solution { + public int lengthOfLongestSubstring(String s) { + java.util.Set charSet = new java.util.HashSet<>(); + int l = 0; + int res = 0; + for (int r = 0; r < s.length(); r++) { + while (charSet.contains(s.charAt(r))) { + charSet.remove(s.charAt(l)); + l++; + } + charSet.add(s.charAt(r)); + res = Math.max(res, r - l + 1); } - - charSet.add(s[r]); - snap(`Safe character evaluated securely mapping structural boundaries targeting '${s[r]}'! Adding to tracking pool.`, 12, true, r); - - res = Math.max(res, r - l + 1); - snap(`Dynamically checked bounding string dimensions evaluating maximum numerical capacities. Value limits updated to [${res}].`, 13, false, r); + return res; } - - snap(`Traversed completely validating array sweep bounds securely. Final numerical boundaries generated.`, 16, true, null); +}`, + + cpp: `class Solution { +public: + int lengthOfLongestSubstring(string s) { + unordered_map charIndex; + int maxLength = 0; + int left = 0; + for (int right = 0; right < s.length(); right++) { + char c = s[right]; + if (charIndex.find(c) != charIndex.end() && charIndex[c] >= left) { + left = charIndex[c] + 1; + } + charIndex[c] = right; + maxLength = max(maxLength, right - left + 1); + } + return maxLength; + } +};` +}; + +const generateStepsData = () => { + const s = "pwwkew"; + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - return sArray; + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + let l = 0; + let res = 0; + const charSet = new Set(); + + const addStep = (msg: string, pseudo: string, tsLine: number, pyLine: number, javaLine: number, cppLine: number, isMatch: boolean = false, overrideR: number | null = null) => { + const activeL = tsLine > 3 ? l : null; + steps.push({ + s, + l: activeL, + r: overrideR, + res, + charSet: Array.from(charSet), + explanation: msg, + pseudoStep: pseudo, + isMatch, + variables: { + s: `"${s}"`, + charSet: `{${Array.from(charSet).join(', ')}}`, + l: activeL !== null ? activeL : 'null', + r: overrideR !== null ? overrideR : 'null', + res + } + }); + addLines(tsLine, pyLine, javaLine, cppLine); + }; + + addStep("Initialize sliding window boundaries and character tracking set.", "CALL lengthOfLongestSubstring(s)", 2, 2, 3, 4); + addStep("Initialize left pointer bounds l = 0.", "SET l = 0", 3, 3, 4, 6); + addStep("Initialize max substring length accumulator res = 0.", "SET res = 0", 4, 4, 5, 5); + + for (let r = 0; r < s.length; r++) { + addStep(`Advance right pointer r to index ${r} ('${s[r]}').`, `FOR r = ${r} TO len(s) - 1`, 5, 5, 6, 7, false, r); + + const currentChar = s[r]; + addStep(`Check if '${currentChar}' causes a collision in the current window.`, `WHILE s[r] IN char_set`, 6, 6, 7, 9, false, r); + + while (charSet.has(s[r])) { + addStep(`Collision! '${s[r]}' is already in set. Shrink window.`, `WHILE s[r] IN char_set → TRUE`, 6, 6, 7, 9, true, r); + + const dropChar = s[l]; + charSet.delete(dropChar); + addStep(`Remove leftmost character '${dropChar}' from set.`, `char_set.remove(s[l])`, 7, 7, 8, 10, true, r); + + l++; + addStep(`Advance left pointer l to ${l}.`, "SET l = l + 1", 8, 8, 9, 10, false, r); + addStep(`Re-evaluate collision for '${s[r]}'.`, `WHILE s[r] IN char_set`, 6, 6, 7, 9, false, r); + } + + charSet.add(s[r]); + addStep(`No collision. Add '${s[r]}' to set.`, "char_set.add(s[r])", 10, 9, 11, 12, true, r); + + res = Math.max(res, r - l + 1); + addStep(`Update max length res = max(res, window size) = ${res}.`, `SET res = max(res, r - l + 1)`, 11, 10, 12, 13, false, r); + } + + addStep(`Algorithm completed. Return the max length: ${res}.`, `RETURN res → ${res}`, 13, 11, 14, 15, true, null); + + return { steps, stepLineNumbers }; +}; + +export const LongestSubstringWithoutRepeatingCharactersVisualization = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + + const { steps, stepLineNumbers } = useMemo(() => { + return generateStepsData(); }, []); - const step = steps[currentStepIndex]; + if (steps.length === 0) return null; + + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( - +

- Dynamic Variable Sliding Window Expansion + Sliding Window Visualizer

- {step.s.split('').map((char, index) => { - const isLeftPointer = step.l === index; - const isRightPointer = step.r === index; - - // Compute if this character is currently inside the active substring boundary conceptually - // Conceptually it is inside if l <= index <= r (or just below r if we haven't added it yet depending on exact step line) - const isInActiveWindow = step.l !== null && step.r !== null && index >= step.l && index <= step.r && !(step.lineNumber === 7 && isRightPointer && step.charSet.length < (step.r - step.l + 1)); + {currentStep.s.split('').map((char, index) => { + const isLeftPointer = currentStep.l === index; + const isRightPointer = currentStep.r === index; - let cellStyle = "border-slate-300 bg-white text-slate-800 scale-100 opacity-60"; + const isInActiveWindow = currentStep.l !== null && currentStep.r !== null && index >= currentStep.l && index <= currentStep.r; - if (isInActiveWindow && !(step.lineNumber >= 7 && step.lineNumber <= 9 && isRightPointer)) { - cellStyle = "border-primary bg-primary/10 text-primary scale-110 shadow-md font-bold"; - } + let cellStyle = "border-border bg-muted/30 text-muted-foreground scale-100 opacity-60"; - // Active collision - if (step.lineNumber >= 7 && step.lineNumber <= 9 && isRightPointer) { - cellStyle = "border-red-500 bg-red-100 text-red-900 scale-110 shadow-lg font-bold border-2"; + if (isInActiveWindow) { + cellStyle = "border-primary bg-primary/10 text-foreground scale-110 shadow-sm font-bold"; } - // Being evaluated safe - if (step.lineNumber >= 12 && isRightPointer) { - cellStyle = "border-green-500 bg-green-100 text-green-900 scale-110 shadow-md font-bold"; - } - - // Left element being evaluated or dropped - if ((step.lineNumber === 8 || step.lineNumber === 9) && index === (step.lineNumber === 9 ? step.l! - 1 : step.l)) { - cellStyle = "border-orange-500 bg-orange-100 text-orange-900 scale-110 border-[3px] font-bold"; - } - return (
{(isLeftPointer && isRightPointer) ? "L, R" : isLeftPointer ? "L" : isRightPointer ? "R" : ""}
-
+
{char}
@@ -156,72 +210,45 @@ export const LongestSubstringWithoutRepeatingCharactersVisualization = () => {
-
+
Active Hash Set Memory (charSet) -
- {step.charSet.length === 0 ? ( - Set is natively empty +
+ {currentStep.charSet.length === 0 ? ( + Set is empty ) : ( - step.charSet.map((c, i) => ( -
+ currentStep.charSet.map((c, i) => ( +
{c}
)) )}
- -
-
-
- Collision Target -
-
-
- Ejected Bounds -
-
-
- Safe Evaluated -
-
- -
-
- {step?.isMatch ? : step?.lineNumber >= 7 && step?.lineNumber <= 9 ? : } -
-
-

- Execution Detail Tracker -

-

- {step?.message || ''} -

-
+ {/* Descriptive Commentary Box (at the bottom) */} +
+
+
+ Process Step
- + {currentStep.explanation} +
- + {/* Variable Panel (below the commentary box) */} +
+ +
} rightContent={ -
- -
+ setCurrentStepIndex(0)} + /> } controls={ ; } -export const LowestCommonAncestorBSTVisualization = () => { - const code = `function lowestCommonAncestor(root, p, q) { - let cur = root; - +const languages: VisualizationLanguageMap = { + typescript: `function lowestCommonAncestor( + root: TreeNode | null, + p: TreeNode, + q: TreeNode +): TreeNode | null { + let cur = root while (cur) { if (p.val > cur.val && q.val > cur.val) { - cur = cur.right; - } else if (p.val < cur.val && q.val < cur.val) { - cur = cur.left; - } else { - return cur; + cur = cur.right + } + else if (p.val < cur.val && q.val < cur.val) { + cur = cur.left + } + else { + return cur } } + return null +}`, + + python: `def lowestCommonAncestor(root, p, q): + cur = root + while cur: + if p.val > cur.val and q.val > cur.val: + cur = cur.right + elif p.val < cur.val and q.val < cur.val: + cur = cur.left + else: + return cur + return None`, + + java: `public static class Solution { + public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { + TreeNode cur = root; + while (cur != null) { + if (p.val > cur.val && q.val > cur.val) { + cur = cur.right; + } + else if (p.val < cur.val && q.val < cur.val) { + cur = cur.left; + } + else { + return cur; + } + } + return null; + } +}`, + + cpp: `class Solution { +public: + TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { + TreeNode* cur = root; + while (cur) { + if (p->val > cur->val && q->val > cur->val) { + cur = cur->right; + } + else if (p->val < cur->val && q->val < cur->val) { + cur = cur->left; + } + else { + return cur; + } + } + return nullptr; + } +};`, +}; + +export const LowestCommonAncestorBSTVisualization = () => { + const generateSteps = () => { + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + const addStep = ( + currentNode: number | null, + p: number, + q: number, + found: boolean, + msg: string, + pseudo: string, + ts_l: number, py_l: number, java_l: number, cpp_l: number + ) => { + steps.push({ + currentNode, + p, + q, + found, + message: msg, + pseudoStep: pseudo, + variables: { + currentNode: currentNode ?? 'null', + p, + q, + found: found ? 'TRUE' : 'FALSE' + } + }); + addLines(ts_l, py_l, java_l, cpp_l); + }; + + // Run Example 1: LCA(3, 5) -> 4 + const p1 = 3, q1 = 5; + addStep(null, p1, q1, false, "Find LCA of nodes 3 and 5 in the BST.", "START lowestCommonAncestor(root, p=3, q=5)", 1, 1, 2, 3); + addStep(6, p1, q1, false, "Initialize 'cur' with the root node (6).", "cur = root", 6, 2, 3, 4); + addStep(6, p1, q1, false, "Check if 'cur' is not null.", "while (cur)", 7, 3, 4, 5); + addStep(6, p1, q1, false, "Compare target values with current node 6.", "if (p.val > cur.val && q.val > cur.val)", 8, 4, 5, 6); + addStep(6, p1, q1, false, "Check if both p and q are in the left subtree of 6.", "else if (p.val < cur.val && q.val < cur.val)", 11, 6, 8, 9); + addStep(6, p1, q1, false, "Both nodes are smaller than 6. Move 'cur' left.", "cur = cur.left", 12, 7, 9, 10); + + addStep(2, p1, q1, false, "Check if 'cur' (2) is not null.", "while (cur)", 7, 3, 4, 5); + addStep(2, p1, q1, false, "Check if both p and q are in the right subtree of 2.", "if (p.val > cur.val && q.val > cur.val)", 8, 4, 5, 6); + addStep(2, p1, q1, false, "Both nodes are larger than 2. Move 'cur' right.", "cur = cur.right", 9, 5, 6, 7); + + addStep(4, p1, q1, false, "Check if 'cur' (4) is not null.", "while (cur)", 7, 3, 4, 5); + addStep(4, p1, q1, false, "Check if both target values are in the right subtree of 4.", "if (p.val > cur.val && q.val > cur.val)", 8, 4, 5, 6); + addStep(4, p1, q1, false, "Check if both target values are in the left subtree of 4.", "else if (p.val < cur.val && q.val < cur.val)", 11, 6, 8, 9); + addStep(4, p1, q1, false, "Neither condition met. Split point found at 4.", "else", 14, 8, 11, 12); + addStep(4, p1, q1, true, "Node 4 is the Lowest Common Ancestor. Return cur.", "return cur", 15, 9, 12, 13); + + // Run Example 2: LCA(2, 4) -> 2 + const p2 = 2, q2 = 4; + addStep(null, p2, q2, false, "Find LCA of nodes 2 and 4 (2 is ancestor of 4).", "START lowestCommonAncestor(root, p=2, q=4)", 1, 1, 2, 3); + addStep(6, p2, q2, false, "Initialize 'cur' with the root node (6).", "cur = root", 6, 2, 3, 4); + addStep(6, p2, q2, false, "Check if 'cur' (6) is not null.", "while (cur)", 7, 3, 4, 5); + addStep(6, p2, q2, false, "Compare target values with current node 6.", "if (p.val > cur.val && q.val > cur.val)", 8, 4, 5, 6); + addStep(6, p2, q2, false, "Check if both p and q are in the left subtree of 6.", "else if (p.val < cur.val && q.val < cur.val)", 11, 6, 8, 9); + addStep(6, p2, q2, false, "Both nodes are smaller than 6. Move 'cur' left.", "cur = cur.left", 12, 7, 9, 10); + + addStep(2, p2, q2, false, "Check if 'cur' (2) is not null.", "while (cur)", 7, 3, 4, 5); + addStep(2, p2, q2, false, "Check if both p and q are in the right subtree of 2.", "if (p.val > cur.val && q.val > cur.val)", 8, 4, 5, 6); + addStep(2, p2, q2, false, "Check if both p and q are in the left subtree of 2.", "else if (p.val < cur.val && q.val < cur.val)", 11, 6, 8, 9); + addStep(2, p2, q2, false, "One value is cur.val (2). Split point found at 2.", "else", 14, 8, 11, 12); + addStep(2, p2, q2, true, "Node 2 is the Lowest Common Ancestor. Return cur.", "return cur", 15, 9, 12, 13); - return null; -}`; - - // Example 1: LCA(3, 5) -> 4 - const steps1: Step[] = [ - { currentNode: null, p: 3, q: 5, found: false, message: "Find LCA of nodes 3 and 5 in the BST.", lineNumber: 1, variables: { p: '3', q: '5', root: '6' } }, - { currentNode: 6, p: 3, q: 5, found: false, message: "Initialize 'cur' with the root node (6).", lineNumber: 2, variables: { cur: '6', p: '3', q: '5' } }, - { currentNode: 6, p: 3, q: 5, found: false, message: "While 'cur' is not null, traverse the tree.", lineNumber: 4, variables: { cur: '6' } }, - { currentNode: 6, p: 3, q: 5, found: false, message: "Compare p(3) and q(5) with cur(6).", lineNumber: 5, variables: { p: '3', q: '5', cur: '6', 'both > cur': false } }, - { currentNode: 6, p: 3, q: 5, found: false, message: "Check if both p(3) and q(5) are less than cur(6).", lineNumber: 7, variables: { p: '3', q: '5', cur: '6', 'both < cur': true } }, - { currentNode: 6, p: 3, q: 5, found: false, message: "Both are smaller. Move 'cur' to the left child (2).", lineNumber: 8, variables: { cur: '6', next: 'left (2)' } }, - { currentNode: 2, p: 3, q: 5, found: false, message: "Updated 'cur' to 2. Continue loop.", lineNumber: 4, variables: { cur: '2' } }, - { currentNode: 2, p: 3, q: 5, found: false, message: "Check if both p(3) and q(5) are greater than cur(2).", lineNumber: 5, variables: { p: '3', q: '5', cur: '2', 'both > cur': true } }, - { currentNode: 2, p: 3, q: 5, found: false, message: "Both are greater. Move 'cur' to the right child (4).", lineNumber: 6, variables: { cur: '2', next: 'right (4)' } }, - { currentNode: 4, p: 3, q: 5, found: false, message: "Updated 'cur' to 4. Continue loop.", lineNumber: 4, variables: { cur: '4' } }, - { currentNode: 4, p: 3, q: 5, found: false, message: "Check if both nodes are in the same subtree.", lineNumber: 5, variables: { p: '3', q: '5', cur: '4', 'both > cur': false } }, - { currentNode: 4, p: 3, q: 5, found: false, message: "Check if both nodes are in the same subtree.", lineNumber: 7, variables: { p: '3', q: '5', cur: '4', 'both < cur': false } }, - { currentNode: 4, p: 3, q: 5, found: false, message: "Neither condition met. Split point found at 4!", lineNumber: 10, variables: { cur: '4', status: 'split' } }, - { currentNode: 4, p: 3, q: 5, found: true, message: "Node 4 is the Lowest Common Ancestor.", lineNumber: 10, variables: { LCA: '4' } } - ]; - - // Example 2: LCA(2, 4) -> 2 - const steps2: Step[] = [ - { currentNode: null, p: 2, q: 4, found: false, message: "Find LCA of nodes 2 and 4 (where 2 is an ancestor of 4).", lineNumber: 1, variables: { p: '2', q: '4', root: '6' } }, - { currentNode: 6, p: 2, q: 4, found: false, message: "Initialize 'cur' with the root node (6).", lineNumber: 2, variables: { cur: '6' } }, - { currentNode: 6, p: 2, q: 4, found: false, message: "Check traversal conditions at node 6.", lineNumber: 7, variables: { p: '2', q: '4', cur: '6', 'both < cur': true } }, - { currentNode: 6, p: 2, q: 4, found: false, message: "Both are smaller. Move to left child (2).", lineNumber: 8, variables: { next: 'left (2)' } }, - { currentNode: 2, p: 2, q: 4, found: false, message: "Updated 'cur' to 2. Continue loop.", lineNumber: 4, variables: { cur: '2' } }, - { currentNode: 2, p: 2, q: 4, found: false, message: "At node 2, p(2) matches cur(2).", lineNumber: 5, variables: { p: '2', q: '4', cur: '2', 'both > cur': false } }, - { currentNode: 2, p: 2, q: 4, found: false, message: "At node 2, p(2) matches cur(2).", lineNumber: 7, variables: { p: '2', q: '4', cur: '2', 'both < cur': false } }, - { currentNode: 2, p: 2, q: 4, found: false, message: "Split/Found point identified at 2.", lineNumber: 10, variables: { cur: '2', status: 'LCA node reached' } }, - { currentNode: 2, p: 2, q: 4, found: true, message: "Node 2 is the Lowest Common Ancestor.", lineNumber: 10, variables: { LCA: '2' } } - ]; - - const allSteps = [...steps1, ...steps2]; - - const [idx, setIdx] = useState(0); + return { steps, stepLineNumbers }; + }; + + const [{ steps, stepLineNumbers }] = useState(generateSteps); + const [currentStepIndex, setCurrentStepIndex] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const [speed, setSpeed] = useState(1); - const step = allSteps[idx]; const intervalRef = useRef(null); useEffect(() => { - if (isPlaying && idx < allSteps.length - 1) { + if (isPlaying && currentStepIndex < steps.length - 1) { intervalRef.current = setInterval(() => { - setIdx(prev => { - if (prev >= allSteps.length - 1) { + setCurrentStepIndex(prev => { + if (prev >= steps.length - 1) { setIsPlaying(false); return prev; } @@ -87,18 +187,23 @@ export const LowestCommonAncestorBSTVisualization = () => { } return () => { if (intervalRef.current) clearInterval(intervalRef.current); - } - }, [isPlaying, idx, allSteps.length, speed]); + }; + }, [isPlaying, currentStepIndex, steps.length, speed]); const handlePlay = () => setIsPlaying(true); const handlePause = () => setIsPlaying(false); - const handleStepForward = () => idx < allSteps.length - 1 && setIdx(prev => prev + 1); - const handleStepBack = () => idx > 0 && setIdx(prev => prev - 1); + const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(prev => prev + 1); + const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(prev => prev - 1); const handleReset = () => { - setIdx(0); + setCurrentStepIndex(0); setIsPlaying(false); }; + if (steps.length === 0) return null; + + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); + const renderTree = () => { const nodes = [ { x: 200, y: 40, val: 6, left: 2, right: 8 }, @@ -135,22 +240,22 @@ export const LowestCommonAncestorBSTVisualization = () => { ))} {nodes.map((pos) => { - const isCurrent = step.currentNode === pos.val; - const isTarget = pos.val === step.p || pos.val === step.q; - const isLCA = step.found && step.currentNode === pos.val; + const isCurrent = currentStep.currentNode === pos.val; + const isTarget = pos.val === currentStep.p || pos.val === currentStep.q; + const isLCA = currentStep.found && currentStep.currentNode === pos.val; return ( { y={pos.y} textAnchor="middle" dy=".3em" - className={`text-sm font-medium transition-colors duration-300 ${isTarget || isCurrent || isLCA ? 'fill-white' : 'fill-foreground'}`} + className={`text-sm font-semibold transition-colors duration-300 ${isTarget || isCurrent || isLCA ? 'fill-white' : 'fill-foreground'}`} > {pos.val} {isTarget && !isLCA && !isCurrent && ( - {pos.val === step.p ? 'p' : 'q'} + {pos.val === currentStep.p ? 'p' : 'q'} )} @@ -195,11 +300,12 @@ export const LowestCommonAncestorBSTVisualization = () => { onReset={handleReset} speed={speed} onSpeedChange={setSpeed} - currentStep={idx} - totalSteps={allSteps.length - 1} + currentStep={currentStepIndex} + totalSteps={steps.length - 1} />
+ {/* Left: visual tree + commentary box + variable panel */}
@@ -215,26 +321,38 @@ export const LowestCommonAncestorBSTVisualization = () => { {renderTree()}
-
+
- {step.message} + {currentStep.message}
-
- -
+
- + {/* Right: code / pseudocode panel */} +
); diff --git a/src/components/visualizations/algorithms/ManachersVisualization.tsx b/src/components/visualizations/algorithms/ManachersVisualization.tsx index 9360e53..091de77 100644 --- a/src/components/visualizations/algorithms/ManachersVisualization.tsx +++ b/src/components/visualizations/algorithms/ManachersVisualization.tsx @@ -1,10 +1,11 @@ import { useState, useMemo } from 'react'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; import { motion } from 'framer-motion'; import { Card } from '@/components/ui/card'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { processed: string; @@ -15,26 +16,167 @@ interface Step { mirror: number; maxLen: number; explanation: string; - highlightedLines: number[]; + pseudoStep: string; variables: Record; phase: 'init' | 'preprocess' | 'mirror' | 'expand' | 'update_boundary' | 'update_max' | 'done'; } +const languages: VisualizationLanguageMap = { + typescript: `function longestPalindromeManachers(s: string): number { + if (s.length === 0) return 0; + const processed = "#" + s.split("").join("#") + "#"; + const n = processed.length; + const radius = new Array(n).fill(0); + let center = 0; + let right = 0; + let maxLen = 0; + for (let i = 0; i < n; i++) { + const mirror = 2 * center - i; + if (i < right) { + radius[i] = Math.min(right - i, radius[mirror]); + } + while ( + i - radius[i] - 1 >= 0 && + i + radius[i] + 1 < n && + processed[i - radius[i] - 1] === processed[i + radius[i] + 1] + ) { + radius[i]++; + } + if (i + radius[i] > right) { + center = i; + right = i + radius[i]; + } + maxLen = Math.max(maxLen, radius[i]); + } + return maxLen; +}`, + + python: `def longestPalindromeManachers(s: str) -> int: + if len(s) == 0: + return 0 + processed = "#" + "#".join(s) + "#" + n = len(processed) + radius = [0] * n + center = 0 + right = 0 + max_len = 0 + for i in range(n): + mirror = 2 * center - i + if i < right: + radius[i] = min(right - i, radius[mirror]) + while ( + i - radius[i] - 1 >= 0 and + i + radius[i] + 1 < n and + processed[i - radius[i] - 1] == processed[i + radius[i] + 1] + ): + radius[i] += 1 + if i + radius[i] > right: + center = i + right = i + radius[i] + max_len = max(max_len, radius[i]) + return max_len`, + + java: `public static class Solution { + public int longestPalindromeManachers(String s) { + if (s.length() == 0) return 0; + StringBuilder processed = new StringBuilder("#"); + for (char c : s.toCharArray()) { + processed.append(c).append("#"); + } + int n = processed.length(); + int[] radius = new int[n]; + int center = 0; + int right = 0; + int maxLen = 0; + for (int i = 0; i < n; i++) { + int mirror = 2 * center - i; + if (i < right) { + radius[i] = Math.min(right - i, radius[mirror]); + } + while ( + i - radius[i] - 1 >= 0 && + i + radius[i] + 1 < n && + processed.charAt(i - radius[i] - 1) == processed.charAt(i + radius[i] + 1) + ) { + radius[i]++; + } + if (i + radius[i] > right) { + center = i; + right = i + radius[i]; + } + maxLen = Math.max(maxLen, radius[i]); + } + return maxLen; + } +}`, + + cpp: `class Solution { +public: + int longestPalindromeManachers(string s) { + if (s.length() == 0) return 0; + string processed = "#"; + for (char c : s) { + processed += c; + processed += "#"; + } + int n = processed.size(); + vector radius(n, 0); + int center = 0; + int right = 0; + int maxLen = 0; + for (int i = 0; i < n; i++) { + int mirror = 2 * center - i; + if (i < right) { + radius[i] = min(right - i, radius[mirror]); + } + while ( + i - radius[i] - 1 >= 0 && + i + radius[i] + 1 < n && + processed[i - radius[i] - 1] == processed[i + radius[i] + 1] + ) { + radius[i]++; + } + if (i + radius[i] > right) { + center = i; + right = i + radius[i]; + } + maxLen = max(maxLen, radius[i]); + } + return maxLen; + } +};`, +}; + export const ManachersVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); + const [currentStepIndex, setCurrentStepIndex] = useState(0); const inputStr = 'abaabad'; - const steps: Step[] = useMemo(() => { + const { steps, stepLineNumbers } = useMemo(() => { const s = inputStr; const s_steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + s_steps.push({ processed: '', radius: [], i: -1, center: 0, right: 0, mirror: -1, maxLen: 0, - explanation: 'Start longestPalindromeManachers. Check if input is empty.', - highlightedLines: [1, 2], + explanation: 'Start longestPalindromeManachers. Check if input string is empty.', + pseudoStep: 'IF len(s) == 0 → NO ✗', variables: { s, 'length': s.length }, phase: 'init' }); + addLines(2, 2, 3, 3); const processed = '#' + s.split('').join('#') + '#'; const n = processed.length; @@ -45,49 +187,54 @@ export const ManachersVisualization = () => { s_steps.push({ processed, radius: [...radius], i: -1, center, right, mirror: -1, maxLen, - explanation: `Preprocess: insert '#' between chars. processed = "${processed}"`, - highlightedLines: [4, 5], + explanation: `Preprocess string to handle even/odd lengths by inserting '#'. processed = "${processed}"`, + pseudoStep: `SET processed = "#" + join(s, "#") + "#"`, variables: { processed, n }, phase: 'preprocess' }); + addLines(3, 4, 4, 4); s_steps.push({ processed, radius: [...radius], i: -1, center, right, mirror: -1, maxLen, - explanation: 'Initialize radius array, center = 0, right = 0, maxLen = 0.', - highlightedLines: [7, 9, 10, 11], + explanation: 'Initialize radius array, and pointers: center = 0, right = 0, maxLen = 0.', + pseudoStep: 'SET radius = [0...n], center = 0, right = 0, maxLen = 0', variables: { center, right, maxLen }, phase: 'preprocess' }); + addLines(6, 7, 9, 9); for (let i = 0; i < n; i++) { const mirror = 2 * center - i; s_steps.push({ processed, radius: [...radius], i, center, right, mirror, maxLen, - explanation: `Iteration i=${i} (char="${processed[i]}"): compute mirror = 2*${center}-${i} = ${mirror}.`, - highlightedLines: [13, 14], + explanation: `Iteration i=${i} (char="${processed[i]}"): calculate mirror position across current center: mirror = 2 * center - i = 2 * ${center} - ${i} = ${mirror}.`, + pseudoStep: `FOR i = ${i} : SET mirror = 2 * center - i → ${mirror}`, variables: { i, 'processed[i]': processed[i], center, mirror }, phase: 'mirror' }); + addLines(10, 11, 13, 13); if (i < right) { radius[i] = Math.min(right - i, radius[mirror] ?? 0); s_steps.push({ processed, radius: [...radius], i, center, right, mirror, maxLen, - explanation: `i=${i} < right=${right}: use symmetry. radius[${i}] = min(${right - i}, ${radius[mirror] ?? 0}) = ${radius[i]}.`, - highlightedLines: [16, 17], + explanation: `Since i (${i}) < right (${right}), initialize radius[${i}] using symmetry: min(right - i, radius[mirror]) = min(${right - i}, ${radius[mirror] ?? 0}) = ${radius[i]}.`, + pseudoStep: `IF i < right: radius[i] = MIN(right - i, radius[mirror]) → ${radius[i]}`, variables: { 'right-i': right - i, 'radius[mirror]': radius[mirror] ?? 0, 'radius[i]': radius[i] }, phase: 'mirror' }); + addLines(12, 13, 15, 15); } s_steps.push({ processed, radius: [...radius], i, center, right, mirror, maxLen, - explanation: `Try expanding palindrome at i=${i} with current radius ${radius[i]}.`, - highlightedLines: [20, 21, 22, 23], + explanation: `Attempt to expand the palindrome centered at i=${i} with starting radius ${radius[i]}.`, + pseudoStep: `WHILE characters at bounds match`, variables: { i, 'radius[i]': radius[i] }, phase: 'expand' }); + addLines(14, 14, 17, 17); while ( i - radius[i] - 1 >= 0 && @@ -97,11 +244,12 @@ export const ManachersVisualization = () => { radius[i]++; s_steps.push({ processed, radius: [...radius], i, center, right, mirror, maxLen, - explanation: `Match: processed[${i - radius[i]}]="${processed[i - radius[i]]}" == processed[${i + radius[i]}]="${processed[i + radius[i]]}". Expand radius to ${radius[i]}.`, - highlightedLines: [25], + explanation: `Characters match: processed[${i - radius[i]}]="${processed[i - radius[i]]}" == processed[${i + radius[i]}]="${processed[i + radius[i]]}". Increment radius to ${radius[i]}.`, + pseudoStep: `SET radius[${i}] = ${radius[i]}`, variables: { i, 'radius[i]': radius[i], left: i - radius[i], rightEdge: i + radius[i] }, phase: 'expand' }); + addLines(19, 19, 20, 20); } if (i + radius[i] > right) { @@ -109,80 +257,46 @@ export const ManachersVisualization = () => { right = i + radius[i]; s_steps.push({ processed, radius: [...radius], i, center, right, mirror, maxLen, - explanation: `i + radius[i] = ${i + radius[i]} > right. Update center=${center}, right=${right}.`, - highlightedLines: [28, 29, 30], + explanation: `Palindrome centered at ${i} extends past current right boundary (${i + radius[i]} > ${right - radius[i]}). Update center = ${center}, right = ${right}.`, + pseudoStep: `IF i + radius[i] > right: SET center = ${center}, right = ${right}`, variables: { center, right }, phase: 'update_boundary' }); + addLines(22, 21, 23, 22); } maxLen = Math.max(maxLen, radius[i]); s_steps.push({ processed, radius: [...radius], i, center, right, mirror, maxLen, - explanation: `Update maxLen = max(${maxLen}, ${radius[i]}) = ${maxLen}.`, - highlightedLines: [33], + explanation: `Update maximum palindrome radius found so far: maxLen = max(${maxLen}, ${radius[i]}) = ${maxLen}.`, + pseudoStep: `SET maxLen = MAX(maxLen, radius[i]) → ${maxLen}`, variables: { maxLen, 'radius[i]': radius[i] }, phase: 'update_max' }); + addLines(25, 23, 26, 25); } s_steps.push({ processed, radius: [...radius], i: -1, center, right, mirror: -1, maxLen, - explanation: `Done! Longest palindrome length = maxLen = ${maxLen}.`, - highlightedLines: [36], + explanation: `Completed processing all elements. Return the maximum palindrome radius found: ${maxLen}.`, + pseudoStep: `RETURN maxLen → ${maxLen}`, variables: { maxLen, result: maxLen }, phase: 'done' }); + addLines(27, 24, 28, 27); - return s_steps; + return { steps: s_steps, stepLineNumbers }; }, [inputStr]); - const code = `function longestPalindromeManachers(s: string): number { - if (s.length === 0) return 0; - - const processed = "#" + s.split("").join("#") + "#"; - const n = processed.length; - - const radius = new Array(n).fill(0); - - let center = 0; - let right = 0; - let maxLen = 0; - - for (let i = 0; i < n; i++) { - const mirror = 2 * center - i; - - if (i < right) { - radius[i] = Math.min(right - i, radius[mirror]); - } - - while ( - i - radius[i] - 1 >= 0 && - i + radius[i] + 1 < n && - processed[i - radius[i] - 1] === processed[i + radius[i] + 1] - ) { - radius[i]++; - } - - if (i + radius[i] > right) { - center = i; - right = i + radius[i]; - } - - maxLen = Math.max(maxLen, radius[i]); - } - - return maxLen; -}`; - - const step = steps[currentStep] || steps[0]; + const currentStep = steps[currentStepIndex] || steps[0]; + const pseudoSteps = steps.map(s => s.pseudoStep); const getCharColor = (idx: number) => { - if (step.i === -1) return 'none'; - const lo = step.i - (step.radius[step.i] || 0); - const hi = step.i + (step.radius[step.i] || 0); - if (idx === step.i) return 'current'; - if (idx === step.mirror && step.mirror >= 0) return 'mirror'; + if (currentStep.i === -1) return 'none'; + const lo = currentStep.i - (currentStep.radius[currentStep.i] || 0); + const hi = currentStep.i + (currentStep.radius[currentStep.i] || 0); + if (idx === currentStep.i) return 'current'; + if (idx === currentStep.mirror && currentStep.mirror >= 0) return 'mirror'; if (idx >= lo && idx <= hi) return 'inPalin'; return 'none'; }; @@ -190,115 +304,122 @@ export const ManachersVisualization = () => { return ( - -

Manacher's Algorithm

- -
-
-

Processed String

-
- {step.processed.split('').map((char, idx) => { - const color = getCharColor(idx); - return ( -
- {idx} - - {char} - - {idx === step.i && step.i !== -1 && ( -
- i -
- )} - {idx === step.mirror && step.mirror >= 0 && step.mirror !== step.i && ( -
- m -
- )} -
- ); - })} +
+
+ +

+ Manacher's Algorithm (Longest Palindrome) +

+ +
+
+

Processed String

+
+ {currentStep.processed.split('').map((char, idx) => { + const color = getCharColor(idx); + return ( +
+ {idx} + + {char} + + {idx === currentStep.i && currentStep.i !== -1 && ( +
+ i +
+ )} + {idx === currentStep.mirror && currentStep.mirror >= 0 && currentStep.mirror !== currentStep.i && ( +
+ m +
+ )} +
+ ); + })} +
-
-
-

Radius Array

-
- {step.processed.split('').map((_, idx) => { - const val = step.radius[idx] ?? 0; - const isActive = idx === step.i; - const isMax = val === step.maxLen && val > 0; - return ( -
- - {val} - -
- ); - })} +
+

Radius Array

+
+ {currentStep.processed.split('').map((_, idx) => { + const val = currentStep.radius[idx] ?? 0; + const isActive = idx === currentStep.i; + const isMax = val === currentStep.maxLen && val > 0; + return ( +
+ + {val} + +
+ ); + })} +
-
-
- {[ - { label: 'center', value: step.center, color: '#84cc16' }, - { label: 'right', value: step.right, color: '#60a5fa' }, - { label: 'maxLen', value: step.maxLen, color: '#a78bfa' }, - ].map(({ label, value, color }) => ( -
-
{label}
-
{value}
-
- ))} +
+ {[ + { label: 'center', value: currentStep.center, color: '#84cc16' }, + { label: 'right', value: currentStep.right, color: '#60a5fa' }, + { label: 'maxLen', value: currentStep.maxLen, color: '#a78bfa' }, + ].map(({ label, value, color }) => ( +
+
{label}
+
{value}
+
+ ))} +
-
-
- - -
-

Algorithm Step

-

{step.explanation}

- - - + +
+ +
+ +
+

Algorithm Step

+

{currentStep.explanation}

+ + +
} rightContent={ - setCurrentStepIndex(0)} /> } controls={ } /> diff --git a/src/components/visualizations/algorithms/MatrixPathVisualization.tsx b/src/components/visualizations/algorithms/MatrixPathVisualization.tsx index fbaceaf..4ef292d 100644 --- a/src/components/visualizations/algorithms/MatrixPathVisualization.tsx +++ b/src/components/visualizations/algorithms/MatrixPathVisualization.tsx @@ -1,9 +1,9 @@ -import React, { useState, useMemo } from 'react'; -import { Card } from '@/components/ui/card'; +import React, { useState } from 'react'; +import { Info } from 'lucide-react'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; -import { VisualizationLayout } from '../shared/VisualizationLayout'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { dp: number[][]; @@ -11,223 +11,308 @@ interface Step { j: number | null; variables: Record; explanation: string; - highlightedLines: number[]; - lineExecution: string; + pseudoStep: string; } -export const MatrixPathVisualization: React.FC = () => { - const [currentStep, setCurrentStep] = useState(0); - - const m = 3; - const n = 5; // Using a slightly smaller grid for better visibility - - const code = `function uniquePaths(m: number, n: number): number { - let row = new Array(n).fill(1); +const languages: VisualizationLanguageMap = { + typescript: `function uniquePaths(m: number, n: number): number { + let row: number[] = new Array(n).fill(1); for (let i = 0; i < m - 1; i++) { - const newRow = new Array(n).fill(1); + const newRow: number[] = new Array(n).fill(1); for (let j = n - 2; j >= 0; j--) { newRow[j] = newRow[j + 1] + row[j]; } row = newRow; } return row[0]; -}`; - - const steps = useMemo(() => { - const stepsList: Step[] = []; - let rowValues = new Array(n).fill(1); - const displayGrid = Array.from({ length: m }, () => Array(n).fill(0)); - - // Initial displayGrid setup (last row is all 1s from the start of the logic) - for (let c = 0; c < n; c++) displayGrid[m - 1][c] = 1; - - stepsList.push({ - dp: displayGrid.map(r => [...r]), - i: null, - j: null, - variables: { m, n }, - explanation: `Problem: Find unique paths from (0,0) to (${m - 1},${n - 1}). We use DP to track paths from the target back to the start.`, - lineExecution: "function uniquePaths(m: number, n: number): number {", - highlightedLines: [1] - }); +}`, - stepsList.push({ - dp: displayGrid.map(r => [...r]), - i: m - 1, - j: null, - variables: { "row.length": n, values: "all 1s" }, - explanation: "Initialize the base row (bottom-most) with 1s. This represents having 1 path to reach the target from any cell in the last row.", - lineExecution: "let row = new Array(n).fill(1);", - highlightedLines: [2] - }); + python: `def uniquePaths(m: int, n: int) -> int: + row = [1] * n + for i in range(m - 1): + newRow = [1] * n + for j in range(n - 2, -1, -1): + newRow[j] = newRow[j + 1] + row[j] + row = newRow + return row[0]`, - for (let i = 0; i < m - 1; i++) { - const currentRowIdx = m - 2 - i; - const newRowValues = new Array(n).fill(1); - displayGrid[currentRowIdx][n - 1] = 1; + java: `public static class Solution { + public int uniquePaths(int m, int n) { + int[] row = new int[n]; + java.util.Arrays.fill(row, 1); + for (int i = 0; i < m - 1; i++) { + int[] newRow = new int[n]; + java.util.Arrays.fill(newRow, 1); + for (int j = n - 2; j >= 0; j--) { + newRow[j] = newRow[j + 1] + row[j]; + } + row = newRow; + } + return row[0]; + } +}`, - stepsList.push({ - dp: displayGrid.map(r => [...r]), - i: currentRowIdx, - j: n - 1, - variables: { i, currentRowIdx, "newRow[last]": 1 }, - explanation: `Processing row ${currentRowIdx}. Initialize 'newRow' with 1s. The rightmost cell (column ${n-1}) always has only 1 edge path.`, - lineExecution: "for (let i = 0; i < m - 1; i++) {\n const newRow = new Array(n).fill(1);", - highlightedLines: [3, 4] - }); + cpp: `class Solution { +public: + int uniquePaths(int m, int n) { + vector row(n, 1); + for (int i = 0; i < m - 1; i++) { + vector newRow(n, 1); + for (int j = n - 2; j >= 0; j--) { + newRow[j] = newRow[j + 1] + row[j]; + } + row = newRow; + } + return row[0]; + } +};` +}; - for (let j = n - 2; j >= 0; j--) { - const fromRight = newRowValues[j + 1]; - const fromBottom = rowValues[j]; - newRowValues[j] = fromRight + fromBottom; - displayGrid[currentRowIdx][j] = newRowValues[j]; - - stepsList.push({ - dp: displayGrid.map(r => [...r]), - i: currentRowIdx, - j, - variables: { j, fromRight, fromBottom, "result": newRowValues[j] }, - explanation: `Calculate newRow[${j}]: Sum the paths from the right (${fromRight}) and bottom (${fromBottom}) to get ${newRowValues[j]}.`, - lineExecution: "newRow[j] = newRow[j + 1] + row[j];", - highlightedLines: [6] - }); - } - - rowValues = [...newRowValues]; - stepsList.push({ - dp: displayGrid.map(r => [...r]), +function generateVisualizationData() { + const m = 3; + const n = 5; + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + let rowValues = new Array(n).fill(1); + const displayGrid = Array.from({ length: m }, () => Array(n).fill(0)); + for (let c = 0; c < n; c++) displayGrid[m - 1][c] = 1; + + steps.push({ + dp: displayGrid.map((r) => [...r]), + i: null, + j: null, + variables: { m, n }, + explanation: `Find unique paths from (0,0) to (${m - 1},${n - 1}). Start calculations.`, + pseudoStep: `START uniquePaths(m=${m}, n=${n})`, + }); + addLines(1, 1, 2, 3); + + steps.push({ + dp: displayGrid.map((r) => [...r]), + i: m - 1, + j: null, + variables: { 'row.length': n, values: 'all 1s' }, + explanation: 'Initialize the base row (bottom-most) with 1s. There is 1 unique path to target from any cell in the target row.', + pseudoStep: 'SET row = [1] * n', + }); + addLines(2, 2, 3, 4); + + for (let i = 0; i < m - 1; i++) { + const currentRowIdx = m - 2 - i; + const newRowValues = new Array(n).fill(1); + displayGrid[currentRowIdx][n - 1] = 1; + + steps.push({ + dp: displayGrid.map((r) => [...r]), + i: currentRowIdx, + j: n - 1, + variables: { i, currentRowIdx, 'newRow[last]': 1 }, + explanation: `Move to row ${currentRowIdx}. Initialize newRow with 1s. The rightmost column cell always has only 1 path (going straight down).`, + pseudoStep: `FOR i = ${i} (Row ${currentRowIdx}) → SET newRow[${n - 1}] = 1`, + }); + addLines(4, 4, 6, 6); + + for (let j = n - 2; j >= 0; j--) { + const fromRight = newRowValues[j + 1]; + const fromBottom = rowValues[j]; + newRowValues[j] = fromRight + fromBottom; + displayGrid[currentRowIdx][j] = newRowValues[j]; + + steps.push({ + dp: displayGrid.map((r) => [...r]), i: currentRowIdx, - j: null, - variables: { "updated_row": `[${rowValues.join(', ')}]` }, - explanation: `Row ${currentRowIdx} completed. We now replace the previous 'row' with our computed 'newRow' and move up.`, - lineExecution: "row = newRow;", - highlightedLines: [8] + j, + variables: { j, fromRight, fromBottom, result: newRowValues[j] }, + explanation: `Compute cell newRow[${j}] by adding paths from right (${fromRight}) and bottom (${fromBottom}) -> ${newRowValues[j]}.`, + pseudoStep: `SET newRow[${j}] = newRow[${j + 1}] + row[${j}] → ${fromRight} + ${fromBottom} = ${newRowValues[j]}`, }); + addLines(6, 6, 9, 8); } - stepsList.push({ - dp: displayGrid.map(r => [...r]), - i: 0, - j: 0, - variables: { result: rowValues[0] }, - explanation: `Finished! The total number of unique paths from (0,0) to bottom-right is stored in row[0], which is ${rowValues[0]}.`, - lineExecution: "return row[0];", - highlightedLines: [10] + rowValues = [...newRowValues]; + steps.push({ + dp: displayGrid.map((r) => [...r]), + i: currentRowIdx, + j: null, + variables: { updated_row: `[${rowValues.join(', ')}]` }, + explanation: `Finished row ${currentRowIdx}. Replace row values with the newly computed newRow.`, + pseudoStep: 'SET row = newRow', }); + addLines(8, 7, 11, 10); + } - return stepsList; - }, [m, n]); + steps.push({ + dp: displayGrid.map((r) => [...r]), + i: 0, + j: 0, + variables: { result: rowValues[0] }, + explanation: `Target path computation complete. The cell row[0] stores the total unique paths: ${rowValues[0]}.`, + pseudoStep: `RETURN row[0] → ${rowValues[0]}`, + }); + addLines(10, 8, 13, 12); - const step = steps[currentStep]; + return { steps, stepLineNumbers }; +} + +export const MatrixPathVisualization: React.FC = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + + const handleReset = () => { + setCurrentStepIndex(0); + }; + + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map((s) => s.pseudoStep); + + const m = 3; + const n = 5; return ( - -
-

+
+ + +
+ {/* Left Column: Visual State */} +
+
+

Unique Paths (Bottom-Up DP Grid) -

- -
- {step.dp.map((row, i) => ( - row.map((val, j) => { - const isProcessing = i === step.i && j === step.j; - const isComputed = val > 0 && !isProcessing; - const isCurrentRow = i === step.i && step.j === null; - const isDependency = step.i !== null && step.j !== null && ( - (i === step.i && j === step.j + 1) || (i === step.i + 1 && j === step.j) - ); - - return ( -
- {i === 0 && j === 0 && Start} - {i === m-1 && j === n-1 && Target} - {val > 0 ? val : ""} -
- ); - }) - ))} +

+
+ {currentStep.dp.map((row, rIdx) => + row.map((val, cIdx) => { + const isProcessing = rIdx === currentStep.i && cIdx === currentStep.j; + const isComputed = val > 0 && !isProcessing; + const isCurrentRow = rIdx === currentStep.i && currentStep.j === null; + const isDependency = + currentStep.i !== null && + currentStep.j !== null && + ((rIdx === currentStep.i && cIdx === currentStep.j + 1) || + (rIdx === currentStep.i + 1 && cIdx === currentStep.j)); + + let cellClass = 'border-border bg-muted/20 text-muted-foreground/30'; + if (isProcessing) { + cellClass = 'border-orange-500 bg-orange-500/20 text-orange-600 dark:text-orange-400 font-bold scale-110 z-10'; + } else if (isDependency) { + cellClass = 'border-blue-500/40 bg-blue-500/10 text-blue-600 dark:text-blue-400'; + } else if (isCurrentRow) { + cellClass = 'border-orange-500/20 bg-orange-500/5 text-foreground/70'; + } else if (isComputed) { + cellClass = 'border-green-500/40 bg-green-500/10 text-green-600 dark:text-green-400'; + } + + return ( +
+ {rIdx === 0 && cIdx === 0 && ( + + Start + + )} + {rIdx === m - 1 && cIdx === n - 1 && ( + + Target + + )} + {val > 0 ? val : ''} +
+ ); + }) + )} +
+ +
+
+
+ Current +
+
+
+ Dependencies
+
+
+ Computed +
+
+
-
+ {/* Commentary Panel */} +
+
+
-
- Current + + + + + + Algorithm Commentary +
-
-
- Dependencies -
-
-
- Computed +
+ Step {currentStepIndex + 1} of {steps.length}
- -
-
- -
-
-

- Current Execution -

-
- {step.lineExecution} -
-
-
-

- Commentary -

-

- {step.explanation} -

+
+
+ +
+
+

+ Current Action +

+
+ {currentStep.explanation}
- +
+
+ +
- } - rightContent={ -
-
- -
- -
- -
+ + {/* Right Column: Code Display */} +
+
- } - controls={ - - } - /> +
+
); }; + +export default MatrixPathVisualization; diff --git a/src/components/visualizations/algorithms/MaximumProductSubarrayVisualization.tsx b/src/components/visualizations/algorithms/MaximumProductSubarrayVisualization.tsx index d0ae6f7..8c47c5e 100644 --- a/src/components/visualizations/algorithms/MaximumProductSubarrayVisualization.tsx +++ b/src/components/visualizations/algorithms/MaximumProductSubarrayVisualization.tsx @@ -3,9 +3,10 @@ import { motion, AnimatePresence } from 'framer-motion'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { Card } from '@/components/ui/card'; -import { Zap, Info, Hash, Target } from 'lucide-react'; +import { Zap, Info, Hash } from 'lucide-react'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { array: number[]; @@ -15,38 +16,99 @@ interface Step { currentMin: number; tempMax?: number; message: string; - lineNumber: number; curMaxRange: [number, number]; // [start, end] bestRange: [number, number]; // [start, end] phase: 'init' | 'prep' | 'update-max' | 'update-min' | 'update-global' | 'done'; isRecordUpdate: boolean; + pseudoStep: string; } +const languages: VisualizationLanguageMap = { + typescript: `function maxProduct(nums: number[]): number { + if (nums.length === 0) return 0; + let maxProduct = nums[0]; + let currentMax = nums[0]; + let currentMin = nums[0]; + for (let i = 1; i < nums.length; i++) { + const num = nums[i]; + const tempMax = currentMax; + currentMax = Math.max( + num, + num * currentMax, + num * currentMin + ); + currentMin = Math.min( + num, + num * tempMax, + num * currentMin + ); + maxProduct = Math.max(maxProduct, currentMax); + } + return maxProduct; +}`, + python: `def maxProduct(nums: List[int]) -> int: + max_product = nums[0] + current_max = nums[0] + current_min = nums[0] + for i in range(1, len(nums)): + num = nums[i] + temp_max = current_max + current_max = max(num, num * current_max, num * current_min) + current_min = min(num, num * temp_max, num * current_min) + max_product = max(max_product, current_max) + return max_product`, + java: `public static class Solution { + public int maxProduct(int[] nums) { + int maxProduct = nums[0]; + int currentMax = nums[0]; + int currentMin = nums[0]; + for (int i = 1; i < nums.length; i++) { + int num = nums[i]; + int tempMax = currentMax; + currentMax = Math.max(num, Math.max(num * currentMax, num * currentMin)); + currentMin = Math.min(num, Math.min(num * tempMax, num * currentMin)); + maxProduct = Math.max(maxProduct, currentMax); + } + return maxProduct; + } +}`, + cpp: `class Solution{ +public: + int maxProduct(vector& nums) { + int maxProduct = nums[0]; + int currentMax = nums[0]; + int currentMin = nums[0]; + for (int i = 1; i < nums.size(); i++) { + int num = nums[i]; + int tempMax = currentMax; + currentMax = max({num, num * currentMax, num * currentMin}); + currentMin = min({num, num * tempMax, num * currentMin}); + maxProduct = max(maxProduct, currentMax); + } + return maxProduct; + } +};` +}; + export const MaximumProductSubarrayVisualization = () => { const [currentStepIndex, setCurrentStepIndex] = useState(0); - const code = `function maxProduct(nums: number[]): number { - if (nums.length === 0) return 0; - let maxProduct = nums[0]; - let currentMax = nums[0]; - let currentMin = nums[0]; - - for (let i = 1; i < nums.length; i++) { - const num = nums[i]; - const tempMax = currentMax; - - currentMax = Math.max(num, num * currentMax, num * currentMin); - currentMin = Math.min(num, num * tempMax, num * currentMin); - - maxProduct = Math.max(maxProduct, currentMax); - } - - return maxProduct; -}`; - - const steps: Step[] = useMemo(() => { + const stepsData = useMemo(() => { const nums = [2, 3, -2, 4, -1]; const s: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; let maxProduct = nums[0]; let currentMax = nums[0]; @@ -63,58 +125,81 @@ export const MaximumProductSubarrayVisualization = () => { currentMax, currentMin, message: `Step 1: Initialization. We start with the first element (${nums[0]}) as our initial max product, current max, and current min.`, - lineNumber: 3, curMaxRange: [0, 0], bestRange: [0, 0], phase: 'init', - isRecordUpdate: false + isRecordUpdate: false, + pseudoStep: `SET maxProduct = ${nums[0]}, currentMax = ${nums[0]}, currentMin = ${nums[0]}` }); + addLines(3, 2, 3, 4); for (let i = 1; i < nums.length; i++) { - const num = nums[i]; - const tempMax = currentMax; + const num = nums[i]; + const tempMax = currentMax; - s.push({ - array: [...nums], - i, - maxProduct, - currentMax, - currentMin, - tempMax, - message: `i = ${i}: We pick ${num} and store our currentMax (${tempMax}) in 'tempMax'.`, - lineNumber: 9, - curMaxRange: [curMaxStart, i - 1], - bestRange: [bestStart, bestEnd], - phase: 'prep', - isRecordUpdate: false - }); + s.push({ + array: [...nums], + i, + maxProduct, + currentMax, + currentMin, + tempMax, + message: `i = ${i}: We pick ${num} and store our currentMax (${tempMax}) in 'tempMax'.`, + curMaxRange: [curMaxStart, i - 1], + bestRange: [bestStart, bestEnd], + phase: 'prep', + isRecordUpdate: false, + pseudoStep: `SET tempMax = ${tempMax}` + }); + addLines(8, 7, 8, 9); - const oldMax = currentMax; - const oldMin = currentMin; - currentMax = Math.max(num, num * oldMax, num * oldMin); - - if (currentMax === num) curMaxStart = i; - else if (currentMax === num * oldMin) curMaxStart = curMinStartActual; + const oldMax = currentMax; + const oldMin = currentMin; + currentMax = Math.max(num, num * oldMax, num * oldMin); + + if (currentMax === num) curMaxStart = i; + else if (currentMax === num * oldMin) curMaxStart = curMinStartActual; - s.push({ - array: [...nums], - i, - maxProduct, - currentMax, - currentMin: oldMin, - tempMax, - message: `Update currentMax: max(num, num * currentMax, num * currentMin) = ${currentMax}.`, - lineNumber: 11, - curMaxRange: [curMaxStart, i], - bestRange: [bestStart, bestEnd], - phase: 'update-max', - isRecordUpdate: false - }); + s.push({ + array: [...nums], + i, + maxProduct, + currentMax, + currentMin: oldMin, + tempMax, + message: `Update currentMax: max(num, num * currentMax, num * currentMin) = ${currentMax}.`, + curMaxRange: [curMaxStart, i], + bestRange: [bestStart, bestEnd], + phase: 'update-max', + isRecordUpdate: false, + pseudoStep: `SET currentMax = Math.max(${num}, ${num * oldMax}, ${num * oldMin})` + }); + addLines(9, 8, 9, 10); + + currentMin = Math.min(num, num * tempMax, num * oldMin); + if (currentMin === num) curMinStartActual = i; + else if (currentMin === num * tempMax) curMinStartActual = curMaxStart; + + s.push({ + array: [...nums], + i, + maxProduct, + currentMax, + currentMin, + tempMax, + message: `Update currentMin: min(num, num * tempMax, num * currentMin) = ${currentMin}.`, + curMaxRange: [curMaxStart, i], + bestRange: [bestStart, bestEnd], + phase: 'update-min', + isRecordUpdate: false, + pseudoStep: `SET currentMin = Math.min(${num}, ${num * tempMax}, ${num * oldMin})` + }); + addLines(14, 9, 10, 11); - currentMin = Math.min(num, num * tempMax, num * oldMin); - if (currentMin === num) curMinStartActual = i; - else if (currentMin === num * tempMax) curMinStartActual = curMaxStart; - + if (currentMax > maxProduct) { + maxProduct = currentMax; + bestStart = curMaxStart; + bestEnd = i; s.push({ array: [...nums], i, @@ -122,33 +207,15 @@ export const MaximumProductSubarrayVisualization = () => { currentMax, currentMin, tempMax, - message: `Update currentMin: min(num, num * tempMax, num * currentMin) = ${currentMin}.`, - lineNumber: 12, + message: `🔥 New global maximum found! Updating maxProduct to ${maxProduct}.`, curMaxRange: [curMaxStart, i], bestRange: [bestStart, bestEnd], - phase: 'update-min', - isRecordUpdate: false + phase: 'update-global', + isRecordUpdate: true, + pseudoStep: `SET maxProduct = Math.max(${maxProduct}, ${currentMax})` }); - - if (currentMax > maxProduct) { - maxProduct = currentMax; - bestStart = curMaxStart; - bestEnd = i; - s.push({ - array: [...nums], - i, - maxProduct, - currentMax, - currentMin, - tempMax, - message: `🔥 New global maximum found! Updating maxProduct to ${maxProduct}.`, - lineNumber: 14, - curMaxRange: [curMaxStart, i], - bestRange: [bestStart, bestEnd], - phase: 'update-global', - isRecordUpdate: true - }); - } + addLines(19, 10, 11, 12); + } } s.push({ @@ -158,53 +225,64 @@ export const MaximumProductSubarrayVisualization = () => { currentMax, currentMin, message: `Final result: The maximum product found is ${maxProduct}.`, - lineNumber: 17, curMaxRange: [-1, -1], bestRange: [bestStart, bestEnd], phase: 'done', - isRecordUpdate: false + isRecordUpdate: false, + pseudoStep: `RETURN maxProduct → ${maxProduct}` }); + addLines(21, 11, 13, 14); - return s; + return { steps: s, stepLineNumbers }; }, []); + const { steps, stepLineNumbers } = stepsData; const currentStep = steps[currentStepIndex] || steps[0]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( + } leftContent={ -
- -
-

- - Visualizing Product Extremes -

-
+
+
+ +
+

+ + Visualizing Product Extremes +

+
-
-
-
+
+
+
{currentStep.array.map((value, index) => { const isCurrent = index === currentStep.i; const isInBestRange = index >= currentStep.bestRange[0] && index <= currentStep.bestRange[1]; const maxVal = Math.max(...currentStep.array.map(Math.abs), 1); - const normalizedHeight = (Math.abs(value) / maxVal) * 60 + 20; + const normalizedHeight = (Math.abs(value) / maxVal) * 50 + 20; return (
-
- {value >= 0 && ( - - )} +
+ {value >= 0 && ( + + )}
{ > {value} -
- {value < 0 && ( - - )} +
+ {value < 0 && ( + + )}
-
+
IDX {index}
@@ -233,82 +311,64 @@ export const MaximumProductSubarrayVisualization = () => { })}
-
-
- -
-
- currentMax - {currentStep.currentMax} +
-
- currentMin - {currentStep.currentMin} -
- - - Global Best - - - {currentStep.maxProduct} - - -
- - -
-
- +
+ currentMax + {currentStep.currentMax} +
+
+ currentMin + {currentStep.currentMin} +
+ - {currentStep.isRecordUpdate ? '🚀' : (currentStep.phase === 'prep' ? '🔍' : '💡')} + + Global Best + + + {currentStep.maxProduct} +
-
-

- Step Insight -

-

- {currentStep.message} -

-
+ +
+ +
+ +
+

Step Explanation

+

{currentStep.message}

+ + = currentStep.array.length ? 'N/A' : currentStep.i, + value_n: currentStep.i >= currentStep.array.length ? 'N/A' : currentStep.array[currentStep.i], + maxProd: currentStep.maxProduct, + 'tempMax': currentStep.tempMax ?? 'N/A', + curMax: currentStep.currentMax, + curMin: currentStep.currentMin, + }} + /> +
+ +

+ The Negative insight: A negative number multiplied by a large negative value can suddenly become our new maximum. +

- -
- } - rightContent={ -
- - = currentStep.array.length ? 'N/A' : currentStep.i, - value_n: currentStep.i >= currentStep.array.length ? 'N/A' : currentStep.array[currentStep.i], - maxProd: currentStep.maxProduct, - 'tempMax': currentStep.tempMax ?? 'N/A', - curMax: currentStep.currentMax, - curMin: currentStep.currentMin, - }} - /> -
- -

- The Negative insight: A negative number multiplied by a large negative value can suddenly become our new maximum. -

} - controls={ - setCurrentStepIndex(0)} /> } /> diff --git a/src/components/visualizations/algorithms/MaximumSubarrayVisualization.tsx b/src/components/visualizations/algorithms/MaximumSubarrayVisualization.tsx index 73310c4..1c55bfb 100644 --- a/src/components/visualizations/algorithms/MaximumSubarrayVisualization.tsx +++ b/src/components/visualizations/algorithms/MaximumSubarrayVisualization.tsx @@ -1,176 +1,261 @@ -import { useState, useMemo } from 'react'; +import { useEffect, useState, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; -import { Card } from '@/components/ui/card'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; import { Flame, Play, Search, RefreshCcw, PlusCircle, CheckCircle2, ArrowRight, Sparkles } from 'lucide-react'; +import { Card } from '@/components/ui/card'; interface Step { array: number[]; i: number; maxSub: number; curSum: number; - message: string; - lineNumber: number; - curRange: [number, number]; // [start, end] - bestRange: [number, number]; // [start, end] + explanation: string; + pseudoStep: string; + curRange: [number, number]; + bestRange: [number, number]; phase: 'init' | 'loop' | 'check' | 'update' | 'done'; isMaxUpdate: boolean; + variables: Record; } -export const MaximumSubarrayVisualization = () => { - const [currentStepIndex, setCurrentStepIndex] = useState(0); - - const code = `function maxSubArray(nums: number[]): number { +const languages: VisualizationLanguageMap = { + typescript: `function maxSubArray(nums: number[]): number { let maxSub = nums[0]; let curSum = 0; - - for (let i = 0; i < nums.length; i++) { - const n = nums[i]; - - // If current sum becomes negative, reset it + for (const n of nums) { if (curSum < 0) { curSum = 0; } - curSum += n; maxSub = Math.max(maxSub, curSum); } - return maxSub; -}`; +}`, + python: `def maxSubArray(nums): + maxSub = nums[0] + curSum = 0 + for n in nums: + if curSum < 0: + curSum = 0 + curSum += n + maxSub = max(maxSub, curSum) + return maxSub`, + java: `public int maxSubArray(int[] nums) { + int maxSub = nums[0]; + int curSum = 0; + for (int n : nums) { + if (curSum < 0) { + curSum = 0; + } + curSum += n; + maxSub = Math.max(maxSub, curSum); + } + return maxSub; +}`, + cpp: `int maxSubArray(const vector& nums) { + int maxSub = nums[0]; + int curSum = 0; + for (int n : nums) { + if (curSum < 0) { + curSum = 0; + } + curSum += n; + maxSub = max(maxSub, curSum); + } + return maxSub; +}` +}; + +function generateVisualizationData() { + const nums = [2, -1, 3, -4, 2]; + const steps: Step[] = []; + + let maxSub = nums[0]; + let curSum = 0; + let curStart = 0; + let bestStart = 0; + let bestEnd = 0; + + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - const steps: Step[] = useMemo(() => { - const nums = [2, -1, 3, -4, 2]; - const s: Step[] = []; + steps.push({ + array: [...nums], + i: -1, + maxSub, + curSum, + explanation: `Step 1: Initialization. We set maxSub to the first element (${nums[0]}). The goal is to find a contiguous range with the highest sum.`, + pseudoStep: `SET maxSub = nums[0] → ${nums[0]}`, + curRange: [-1, -1], + bestRange: [0, 0], + phase: 'init', + isMaxUpdate: false, + variables: { i: '-', n: '-', curSum: 0, maxSub, bestRange: '[0, 0]' } + }); + addLines(2, 2, 2, 2); + + steps.push({ + array: [...nums], + i: -1, + maxSub, + curSum, + explanation: `Initialize curSum to 0.`, + pseudoStep: `SET curSum = 0`, + curRange: [-1, -1], + bestRange: [0, 0], + phase: 'init', + isMaxUpdate: false, + variables: { i: '-', n: '-', curSum: 0, maxSub, bestRange: '[0, 0]' } + }); + addLines(3, 3, 3, 3); - let maxSub = nums[0]; - let curSum = 0; - let curStart = 0; - let bestStart = 0; - let bestEnd = 0; + for (let i = 0; i < nums.length; i++) { + const n = nums[i]; - // Phase: Initialization - s.push({ + steps.push({ array: [...nums], - i: -1, + i, maxSub, curSum, - message: `Step 1: Initialization. We set maxSub to the first element (${nums[0]}) and initialize curSum to 0. The goal is to find a contiguous range with the highest sum.`, - lineNumber: 1, - curRange: [-1, -1], - bestRange: [0, 0], - phase: 'init', - isMaxUpdate: false + explanation: `Iteration i = ${i}: Examining the element ${n}.`, + pseudoStep: `FOR n in nums → n = ${n}`, + curRange: curSum < 0 ? [-1, -1] : [curStart, i - 1], + bestRange: [bestStart, bestEnd], + phase: 'loop', + isMaxUpdate: false, + variables: { i, n, curSum, maxSub, bestRange: `[${bestStart}, ${bestEnd}]` } }); + addLines(4, 4, 4, 4); - for (let i = 0; i < nums.length; i++) { - const n = nums[i]; + steps.push({ + array: [...nums], + i, + maxSub, + curSum, + explanation: `Check if our running current sum is negative: curSum (${curSum}) < 0.`, + pseudoStep: `IF curSum < 0 → ${curSum} < 0 → ${curSum < 0 ? 'YES ✓' : 'NO ✗'}`, + curRange: curSum < 0 ? [-1, -1] : [curStart, i - 1], + bestRange: [bestStart, bestEnd], + phase: 'check', + isMaxUpdate: false, + variables: { i, n, curSum, maxSub, bestRange: `[${bestStart}, ${bestEnd}]` } + }); + addLines(5, 5, 5, 5); - // Phase: Loop Start - s.push({ + if (curSum < 0) { + curSum = 0; + curStart = i; + steps.push({ array: [...nums], i, maxSub, curSum, - message: `Iteration i = ${i}: Examining the element ${n}. We need to decide whether to include this element in our current subarray or start a new one.`, - lineNumber: 5, - curRange: curSum < 0 ? [-1, -1] : [curStart, i - 1], + explanation: `Since curSum is negative, we "reset" the current subarray and start fresh from index ${i} (curSum = 0).`, + pseudoStep: 'curSum = 0 (reset sum)', + curRange: [curStart, i - 1], bestRange: [bestStart, bestEnd], - phase: 'loop', - isMaxUpdate: false + phase: 'check', + isMaxUpdate: false, + variables: { i, n, curSum, maxSub, bestRange: `[${bestStart}, ${bestEnd}]` } }); + addLines(6, 6, 6, 6); + } - // Phase: Check curSum - if (curSum < 0) { - const oldStart = curStart; - curSum = 0; - curStart = i; - s.push({ - array: [...nums], - i, - maxSub, - curSum, - message: `Decision Point: The previous curSum was negative. Since any sum plus a negative value is worse than the sum itself, we "reset" the current subarray and start fresh from index ${i}.`, - lineNumber: 10, - curRange: [curStart, i - 1], - bestRange: [bestStart, bestEnd], - phase: 'check', - isMaxUpdate: false - }); - } - - // Phase: Add n - curSum += n; - s.push({ + curSum += n; + steps.push({ + array: [...nums], + i, + maxSub, + curSum, + explanation: `Add the current number ${n} to our running sum. curSum becomes ${curSum}.`, + pseudoStep: `curSum += n → curSum = ${curSum}`, + curRange: [curStart, i], + bestRange: [bestStart, bestEnd], + phase: 'update', + isMaxUpdate: false, + variables: { i, n, curSum, maxSub, bestRange: `[${bestStart}, ${bestEnd}]` } + }); + addLines(8, 7, 8, 8); + + if (curSum > maxSub) { + const oldMax = maxSub; + maxSub = curSum; + bestStart = curStart; + bestEnd = i; + steps.push({ array: [...nums], i, maxSub, curSum, - message: `Action: We add the current number ${n} to our running sum. curSum becomes ${curSum}. This range [${curStart}...${i}] is now our "candidate" for the maximum sum.`, - lineNumber: 13, + explanation: `🔥 Breakthrough! The current sum (${curSum}) is greater than our previous maxSub (${oldMax}). We update the "Best So Far" record and store this range: [${bestStart}...${bestEnd}].`, + pseudoStep: `SET maxSub = max(maxSub, curSum) → ${maxSub}`, curRange: [curStart, i], bestRange: [bestStart, bestEnd], phase: 'update', - isMaxUpdate: false + isMaxUpdate: true, + variables: { i, n, curSum, maxSub, bestRange: `[${bestStart}, ${bestEnd}]` } }); - - // Phase: Update maxSub - if (curSum > maxSub) { - const oldMax = maxSub; - maxSub = curSum; - bestStart = curStart; - bestEnd = i; - s.push({ - array: [...nums], - i, - maxSub, - curSum, - message: `🔥 Breakthrough! The current sum (${curSum}) is greater than our previous maxSub (${oldMax}). We update the "Best So Far" record and store this range: [${bestStart}...${bestEnd}].`, - lineNumber: 14, - curRange: [curStart, i], - bestRange: [bestStart, bestEnd], - phase: 'update', - isMaxUpdate: true - }); - } else { - s.push({ - array: [...nums], - i, - maxSub, - curSum, - message: `Note: The current sum (${curSum}) did not exceed our record (${maxSub}). We keep searching but remember that ${maxSub} is the highest sum we've seen so far.`, - lineNumber: 14, - curRange: [curStart, i], - bestRange: [bestStart, bestEnd], - phase: 'update', - isMaxUpdate: false - }); - } + addLines(9, 8, 9, 9); + } else { + steps.push({ + array: [...nums], + i, + maxSub, + curSum, + explanation: `The current sum (${curSum}) did not exceed our record (${maxSub}). We keep searching but remember that ${maxSub} is the highest sum we've seen so far.`, + pseudoStep: `maxSub remains ${maxSub}`, + curRange: [curStart, i], + bestRange: [bestStart, bestEnd], + phase: 'update', + isMaxUpdate: false, + variables: { i, n, curSum, maxSub, bestRange: `[${bestStart}, ${bestEnd}]` } + }); + addLines(9, 8, 9, 9); } + } - // Phase: Done - s.push({ - array: [...nums], - i: nums.length, - maxSub, - curSum, - message: `Final Conclusion: We've scanned the entire array. The algorithm guarantees that the subsegment from index ${bestStart} to ${bestEnd} yields the maximum possible sum of ${maxSub}.`, - lineNumber: 17, - curRange: [-1, -1], - bestRange: [bestStart, bestEnd], - phase: 'done', - isMaxUpdate: false - }); + steps.push({ + array: [...nums], + i: nums.length, + maxSub, + curSum, + explanation: `Final Conclusion: We've scanned the entire array. The algorithm guarantees that the subsegment from index ${bestStart} to ${bestEnd} yields the maximum possible sum of ${maxSub}.`, + pseudoStep: `RETURN maxSub → ${maxSub}`, + curRange: [-1, -1], + bestRange: [bestStart, bestEnd], + phase: 'done', + isMaxUpdate: false, + variables: { i: 'done', n: '-', curSum, maxSub, bestRange: `[${bestStart}, ${bestEnd}]`, result: maxSub } + }); + addLines(11, 9, 11, 11); + + return { steps, stepLineNumbers }; +} + +export const MaximumSubarrayVisualization = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); - return s; - }, []); + if (steps.length === 0) return null; - const currentStep = steps[currentStepIndex] || steps[0]; + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); - // Determine phase-specific styles and labels const phaseDetails = useMemo(() => { if (currentStep.isMaxUpdate) { return { @@ -179,7 +264,6 @@ export const MaximumSubarrayVisualization = () => { color: 'text-amber-400', bgColor: 'bg-amber-500/10', borderColor: 'border-amber-500/40 shadow-[0_0_15px_rgba(245,158,11,0.15)]', - glowColor: 'from-amber-500/15 via-orange-500/5 to-transparent', accentBg: 'bg-gradient-to-b from-amber-400 via-orange-500 to-amber-600 shadow-[0_0_12px_rgba(245,158,11,0.4)]', dotColor: 'bg-amber-400' }; @@ -192,7 +276,6 @@ export const MaximumSubarrayVisualization = () => { color: 'text-blue-400', bgColor: 'bg-blue-500/10', borderColor: 'border-blue-500/20', - glowColor: 'from-blue-500/10 via-transparent to-transparent', accentBg: 'bg-blue-500', dotColor: 'bg-blue-400' }; @@ -203,7 +286,6 @@ export const MaximumSubarrayVisualization = () => { color: 'text-indigo-400', bgColor: 'bg-indigo-500/10', borderColor: 'border-indigo-500/20', - glowColor: 'from-indigo-500/10 via-transparent to-transparent', accentBg: 'bg-indigo-500', dotColor: 'bg-indigo-400' }; @@ -214,7 +296,6 @@ export const MaximumSubarrayVisualization = () => { color: 'text-rose-400', bgColor: 'bg-rose-500/10', borderColor: 'border-rose-500/20', - glowColor: 'from-rose-500/10 via-transparent to-transparent', accentBg: 'bg-rose-500', dotColor: 'bg-rose-400' }; @@ -225,7 +306,6 @@ export const MaximumSubarrayVisualization = () => { color: 'text-violet-400', bgColor: 'bg-violet-500/10', borderColor: 'border-violet-500/20', - glowColor: 'from-violet-500/10 via-transparent to-transparent', accentBg: 'bg-violet-500', dotColor: 'bg-violet-400' }; @@ -236,7 +316,6 @@ export const MaximumSubarrayVisualization = () => { color: 'text-emerald-400', bgColor: 'bg-emerald-500/10', borderColor: 'border-emerald-500/20', - glowColor: 'from-emerald-500/10 via-transparent to-transparent', accentBg: 'bg-emerald-500', dotColor: 'bg-emerald-400' }; @@ -247,7 +326,6 @@ export const MaximumSubarrayVisualization = () => { color: 'text-zinc-400', bgColor: 'bg-zinc-500/10', borderColor: 'border-zinc-500/20', - glowColor: 'from-zinc-500/10 via-transparent to-transparent', accentBg: 'bg-zinc-500', dotColor: 'bg-zinc-400' }; @@ -256,12 +334,11 @@ export const MaximumSubarrayVisualization = () => { const IconComponent = phaseDetails.icon; - // Track state changes const stateMutations = useMemo(() => { if (currentStepIndex === 0) return null; const prev = steps[currentStepIndex - 1]; const mutations = []; - + if (prev.curSum !== currentStep.curSum) { mutations.push({ name: 'curSum', @@ -294,21 +371,21 @@ export const MaximumSubarrayVisualization = () => { return parts.map((part, idx) => { if (part === 'curSum') { return ( - + curSum ); } if (part === 'maxSub') { return ( - + maxSub ); } if (part.startsWith('[') && part.endsWith(']')) { return ( - + {part} ); @@ -326,229 +403,231 @@ export const MaximumSubarrayVisualization = () => { return ( + } leftContent={ -
- -

- Visualizing Subarray Sum Growth -

- -
- - {currentStep.array.map((value, index) => { - const isCurrent = index === currentStep.i; - const isInCurRange = index >= currentStep.curRange[0] && index <= currentStep.curRange[1]; - const isInBestRange = index >= currentStep.bestRange[0] && index <= currentStep.bestRange[1]; - - return ( -
- {/* Bar visualization */} - - - {/* Value Box */} +
+
+ +

+ Visualizing Subarray Sum Growth +

+ +
+ + {currentStep.array.map((value, index) => { + const isCurrent = index === currentStep.i; + const isInBestRange = index >= currentStep.bestRange[0] && index <= currentStep.bestRange[1]; + + return ( +
+ + + + {value} + + + idx {index} + {index} + + {isInBestRange && ( + + )} +
+ ); + })} +
+ + {currentStep.curRange[0] !== -1 && ( +
+ - {value} + Sum: {currentStep.curSum} + +
+ )} +
- {/* Index Label */} - idx {index} - {index} +
+
+
+
+ Best Subarray Found (Overall Max) +
+
+
+ Current Subarray (Running Total) +
+
- {/* Best Range Marker (Bottom) */} - {isInBestRange && ( - - )} -
- ); - })} - - - {/* Current Subarray Overlay with more visible colors */} - {currentStep.curRange[0] !== -1 && ( -
+
- - Sum: {currentStep.curSum} - + Record Max SubSum + + {currentStep.maxSub} +
- )} -
- - {/* Legend and Stats */} -
-
-
-
- Best Subarray Found (Overall Max) -
-
-
- Current Subarray (Running Total) -
- -
- - Record Max SubSum - - {currentStep.maxSub} - - -
-
- - - -
- {/* Header: Phase Title, Pulsing Indicator, and Step Progress */} -
-
- {/* Status Indicator Dot */} - - - - - - {phaseDetails.label} - -
- - {/* Step counter */} -
- Step {currentStepIndex + 1} of {steps.length} + +
+ +
+ +
+
+
+ + + + + + {phaseDetails.label} + +
+ +
+ Step {currentStepIndex + 1} of {steps.length} +
-
- {/* Main Content Layout */} -
- {/* Left Side: Icon Container */} - - - - - {/* Right Side: Title & Commentary Text */} -
-

- Algorithm commentary -

- - - {formatMessageText(currentStep.message)} - - +
+ + + + +
+

+ Algorithm commentary +

+ + + {formatMessageText(currentStep.explanation)} + + +
-
- {/* State Mutations Sub-Panel */} - {stateMutations && stateMutations.length > 0 && ( - - {stateMutations.map((mutation, idx) => ( -
- {mutation.name} -
- {mutation.old} - - {mutation.new} + {stateMutations && stateMutations.length > 0 && ( + + {stateMutations.map((mutation, idx) => ( +
+ {mutation.name} +
+ {mutation.old} + + {mutation.new} +
-
- ))} - - )} + ))} + + )} +
+ + +
+

Kadane's Algorithm Strategy:

+
+

• Track running current sum (`curSum`) and overall maximum sum (`maxSub`)

+

• If `curSum` falls below 0 → reset it to 0 (since negative values hurt subsequent sums)

+

• Add the current element to `curSum` and update `maxSub` if it exceeds the record

+

• Time: O(n) · Space: O(1)

+
- + + = currentStep.array.length ? '-' : currentStep.i, + value_n: currentStep.i === -1 || currentStep.i >= currentStep.array.length ? '-' : currentStep.array[currentStep.i], + curSum: currentStep.curSum, + bestMax: currentStep.maxSub, + bestRange: `[${currentStep.bestRange[0]}, ${currentStep.bestRange[1]}]` + }} + /> +
} rightContent={ -
- - = currentStep.array.length ? 'N/A' : currentStep.i, - value_n: currentStep.i === -1 || currentStep.i >= currentStep.array.length ? 'N/A' : currentStep.array[currentStep.i], - curSum: currentStep.curSum, - bestMax: currentStep.maxSub, - bestRange: `[${currentStep.bestRange[0]}, ${currentStep.bestRange[1]}]` - }} - /> -
- } - controls={ - setCurrentStepIndex(0)} /> } /> ); }; +export default MaximumSubarrayVisualization; diff --git a/src/components/visualizations/algorithms/MeetingRoomsIIVisualization.tsx b/src/components/visualizations/algorithms/MeetingRoomsIIVisualization.tsx index bff8873..8cc89d2 100644 --- a/src/components/visualizations/algorithms/MeetingRoomsIIVisualization.tsx +++ b/src/components/visualizations/algorithms/MeetingRoomsIIVisualization.tsx @@ -1,9 +1,10 @@ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useMemo } from 'react'; import { Card } from '@/components/ui/card'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; -import { StepControls } from '../shared/StepControls'; +import { SimpleStepControls } from '../shared/SimpleStepControls'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { Button } from '@/components/ui/button'; +import type { VisualizationLanguageMap, StepLineNumberMap } from '@/types/visualization'; interface Meeting { start: number; @@ -19,8 +20,8 @@ interface Step { count: number; res: number; activeMeetings: Meeting[]; - message: string; - highlightedLines: number[]; + explanation: string; + pseudoStep: string; variables: Record; } @@ -42,264 +43,365 @@ const USE_CASES = [ } ]; -export const MeetingRoomsIIVisualization = () => { - const [useCaseIdx, setUseCaseIdx] = useState(0); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - - const currentCase = USE_CASES[useCaseIdx]; - - const steps = useMemo(() => { - const intervals = currentCase.intervals; - const startArr = [...intervals.map(i => i[0])].sort((a, b) => a - b); - const endArr = [...intervals.map(i => i[1])].sort((a, b) => a - b); +const languages: VisualizationLanguageMap = { + python: `def minMeetingRooms(intervals): + start = sorted([i[0] for i in intervals]) + end = sorted([i[1] for i in intervals]) + res = 0 + count = 0 + s = 0 + e = 0 + while s < len(intervals): + if start[s] < end[e]: + s += 1 + count += 1 + else: + e += 1 + count -= 1 + res = max(res, count) + return res`, + typescript: `function minMeetingRooms(intervals: number[][]): number { + const start = intervals.map(i => i[0]).sort((a, b) => a - b); + const end = intervals.map(i => i[1]).sort((a, b) => a - b); + let res = 0, count = 0; + let s = 0, e = 0; + while (s < intervals.length) { + if (start[s] < end[e]) { + s++; + count++; + } else { + e++; + count--; + } + res = Math.max(res, count); + } + return res; +}`, + java: `public class Solution { + public int minMeetingRooms(int[][] intervals) { + int[] start = new int[intervals.length]; + int[] end = new int[intervals.length]; + for (int i = 0; i < intervals.length; i++) { + start[i] = intervals[i][0]; + end[i] = intervals[i][1]; + } + Arrays.sort(start); + Arrays.sort(end); + int res = 0; + int count = 0; + int s = 0; + int e = 0; + while (s < intervals.length) { + if (start[s] < end[e]) { + s++; + count++; + } else { + e++; + count--; + } + res = Math.max(res, count); + } + return res; + } +}`, + cpp: `class Solution { +public: + int minMeetingRooms(vector>& intervals) { + int n = intervals.size(); + vector start(n), end(n); + for (int i = 0; i < n; i++) { + start[i] = intervals[i][0]; + end[i] = intervals[i][1]; + } + sort(start.begin(), start.end()); + sort(end.begin(), end.end()); + int res = 0; + int count = 0; + int s = 0; + int e = 0; + while (s < n) { + if (start[s] < end[e]) { + s++; + count++; + } else { + e++; + count--; + } + res = max(res, count); + } + return res; + } +};`, +}; - const steps: Step[] = []; +const generateVisualizationData = (intervals: number[][]) => { + const startArr = [...intervals.map(i => i[0])].sort((a, b) => a - b); + const endArr = [...intervals.map(i => i[1])].sort((a, b) => a - b); - // Initial state / Signature - steps.push({ - startTimes: startArr, - endTimes: endArr, - s: 0, - e: 0, - count: 0, - res: 0, - activeMeetings: [], - message: "Initialize the meeting rooms manager.", - highlightedLines: [1], - variables: { "intervals": JSON.stringify(intervals) } - }); + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { typescript: [], python: [], java: [], cpp: [] }; - let res = 0; - let count = 0; - let s = 0; - let e = 0; + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - // Line 2: start sort - steps.push({ - startTimes: startArr, - endTimes: endArr, - s, e, count, res, - activeMeetings: [], - message: "Sort all start times to process meetings in chronological order.", - highlightedLines: [2], - variables: { "start": JSON.stringify(startArr) } - }); + // 1. Initial State / Signature + steps.push({ + startTimes: startArr, + endTimes: endArr, + s: 0, + e: 0, + count: 0, + res: 0, + activeMeetings: [], + explanation: "Initialize the meeting rooms manager.", + pseudoStep: "CALL minMeetingRooms(intervals)", + variables: { intervals: JSON.stringify(intervals) } + }); + addLines(1, 1, 2, 3); + + // 2. Sort start times + steps.push({ + startTimes: startArr, + endTimes: endArr, + s: 0, + e: 0, + count: 0, + res: 0, + activeMeetings: [], + explanation: "Sort all start times in ascending order to process meetings chronologically.", + pseudoStep: "SET start = sorted(start_times)", + variables: { start: JSON.stringify(startArr) } + }); + addLines(2, 2, 9, 10); + + // 3. Sort end times + steps.push({ + startTimes: startArr, + endTimes: endArr, + s: 0, + e: 0, + count: 0, + res: 0, + activeMeetings: [], + explanation: "Sort all end times in ascending order to know when rooms become available.", + pseudoStep: "SET end = sorted(end_times)", + variables: { end: JSON.stringify(endArr) } + }); + addLines(3, 3, 10, 11); + + // 4. Initialize res, count + steps.push({ + startTimes: startArr, + endTimes: endArr, + s: 0, + e: 0, + count: 0, + res: 0, + activeMeetings: [], + explanation: "Initialize peak occupancy (res) and current active count to 0.", + pseudoStep: "SET res = 0, count = 0", + variables: { res: 0, count: 0 } + }); + addLines(4, 4, 11, 12); + + // 5. Initialize pointers s, e + steps.push({ + startTimes: startArr, + endTimes: endArr, + s: 0, + e: 0, + count: 0, + res: 0, + activeMeetings: [], + explanation: "Initialize pointers s (for start times) and e (for end times) to 0.", + pseudoStep: "SET s = 0, e = 0", + variables: { s: 0, e: 0 } + }); + addLines(5, 6, 13, 14); + + let res = 0; + let count = 0; + let s = 0; + let e = 0; + const activeMeetings: Meeting[] = []; - // Line 3: end sort + while (s < intervals.length) { + // 6. Loop Condition Check steps.push({ startTimes: startArr, endTimes: endArr, - s, e, count, res, - activeMeetings: [], - message: "Sort all end times to know when rooms become available.", - highlightedLines: [3], - variables: { "end": JSON.stringify(endArr) } + s, + e, + count, + res, + activeMeetings: [...activeMeetings], + explanation: `Check if there are remaining meetings to start (s = ${s} < ${intervals.length}).`, + pseudoStep: `WHILE s < ${intervals.length} → s = ${s}`, + variables: { s, e, count, res } }); + addLines(6, 8, 15, 16); - // Line 5: res, count init - steps.push({ - startTimes: startArr, - endTimes: endArr, - s, e, count, res, - activeMeetings: [], - message: "Initialize the room count and peak occupancy to 0.", - highlightedLines: [5], - variables: { "res": 0, "count": 0 } - }); + const isStartBeforeEnd = startArr[s] < endArr[e]; - // Line 6: s, e init + // 7. If Condition Check steps.push({ startTimes: startArr, endTimes: endArr, - s: 0, e: 0, count, res, - activeMeetings: [], - message: "Set pointers for start times (s) and end times (e) to the beginning.", - highlightedLines: [6], - variables: { "s": 0, "e": 0 } + s, + e, + count, + res, + activeMeetings: [...activeMeetings], + explanation: `Compare next start time (${startArr[s]}) with earliest ending time (${endArr[e]}).`, + pseudoStep: `IF start[${s}] < end[${e}] → ${startArr[s]} < ${endArr[e]} ?`, + variables: { "start[s]": startArr[s], "end[e]": endArr[e], "result": isStartBeforeEnd } }); + addLines(7, 9, 16, 17); - const activeMeetings: Meeting[] = []; - - while (s < intervals.length) { - // While check (line 8) - steps.push({ - startTimes: startArr, - endTimes: endArr, - s, e, count, res, - activeMeetings: [...activeMeetings], - message: `Current start pointer s is ${s}. Processing meeting starting at ${startArr[s]}.`, - highlightedLines: [8], - variables: { "s": s, "n": intervals.length } - }); - - const isStartBeforeEnd = startArr[s] < endArr[e]; + if (isStartBeforeEnd) { + s++; + count++; + activeMeetings.push({ start: startArr[s - 1], end: endArr[e], id: s - 1 }); - // If check (line 9) + // 8. If True Branch (Allocate Room) steps.push({ startTimes: startArr, endTimes: endArr, - s, e, count, res, + s, + e, + count, + res, activeMeetings: [...activeMeetings], - message: `Is next start (${startArr[s]}) < next earliest end (${endArr[e]})?`, - highlightedLines: [9], - variables: { "start[s]": startArr[s], "end[e]": endArr[e], "result": isStartBeforeEnd } + explanation: "Yes. Next meeting starts before the earliest ends. Allocate a new room. Increment count and s.", + pseudoStep: `SET s = ${s}, count = ${count} → allocate room`, + variables: { s, e, count, res } }); + addLines(8, 10, 17, 18); + } else { + e++; + count--; + if (activeMeetings.length > 0) activeMeetings.shift(); - if (isStartBeforeEnd) { - s++; - count++; - activeMeetings.push({ start: startArr[s - 1], end: endArr[e], id: s - 1 }); - - steps.push({ - startTimes: startArr, - endTimes: endArr, - s, e, count, res, - activeMeetings: [...activeMeetings], - message: "Yes. A new room must be allocated before any is freed. Increment 'count'.", - highlightedLines: [10, 11], - variables: { "count": count, "s": s } - }); - } else { - e++; - count--; - if (activeMeetings.length > 0) activeMeetings.shift(); - - steps.push({ - startTimes: startArr, - endTimes: endArr, - s, e, count, res, - activeMeetings: [...activeMeetings], - message: "No. A room has been freed. Decrement 'count' and move end pointer e.", - highlightedLines: [13, 14], - variables: { "count": count, "e": e } - }); - } - - const prevRes = res; - res = Math.max(res, count); - - // res = Math.max (line 17) + // 9. If False Branch (Free Room) steps.push({ startTimes: startArr, endTimes: endArr, - s, e, count, res, + s, + e, + count, + res, activeMeetings: [...activeMeetings], - message: res > prevRes - ? `Updated peak room occupancy: ${res}.` - : `Current occupancy (${count}) does not exceed peak (${res}).`, - highlightedLines: [17], - variables: { "count": count, "res": res } + explanation: "No. A meeting ended before or at the same time the next one starts. Free a room. Decrement count and increment e.", + pseudoStep: `SET e = ${e}, count = ${count} → free room`, + variables: { s, e, count, res } }); + addLines(11, 13, 20, 21); } - // Final while check failed (Line 8) - steps.push({ - startTimes: startArr, - endTimes: endArr, - s, e, count, res, - activeMeetings: [], - message: "All meetings processed (s >= intervals.length).", - highlightedLines: [8], - variables: { "s": s, "n": intervals.length } - }); + res = Math.max(res, count); - // Return (line 20) + // 10. Update res steps.push({ startTimes: startArr, endTimes: endArr, - s, e, count, res, - activeMeetings: [], - message: `Final result: minimum of ${res} rooms required.`, - highlightedLines: [20], - variables: { "return": res } + s, + e, + count, + res, + activeMeetings: [...activeMeetings], + explanation: `Update peak occupancy: max(res, count) = max(${res}, ${count}) = ${res}.`, + pseudoStep: `SET res = MAX(res, count) → res = ${res}`, + variables: { s, e, count, res } }); + addLines(14, 15, 23, 24); + } - return steps; - }, [currentCase]); - - const currentStep = steps[currentStepIndex] || steps[steps.length - 1]; - - const code = `function minMeetingRooms(intervals: number[][]): number { - const start = intervals.map(i => i[0]).sort((a, b) => a - b); - const end = intervals.map(i => i[1]).sort((a, b) => a - b); + // 11. Loop Terminated + steps.push({ + startTimes: startArr, + endTimes: endArr, + s, + e, + count, + res, + activeMeetings: [], + explanation: "All start times processed. Loop terminates.", + pseudoStep: `WHILE s < ${intervals.length} → FALSE ✗`, + variables: { s, e, count, res } + }); + addLines(6, 8, 15, 16); + + // 12. Return result + steps.push({ + startTimes: startArr, + endTimes: endArr, + s, + e, + count, + res, + activeMeetings: [], + explanation: `Return the peak number of rooms required: ${res}.`, + pseudoStep: `RETURN res → ${res}`, + variables: { return: res } + }); + addLines(16, 16, 25, 26); + + return { steps, stepLineNumbers }; +}; - let res = 0, count = 0; - let s = 0, e = 0; +export const MeetingRoomsIIVisualization = () => { + const [useCaseIdx, setUseCaseIdx] = useState(0); + const [currentStepIndex, setCurrentStepIndex] = useState(0); - while (s < intervals.length) { - if (start[s] < end[e]) { - s++; - count++; - } else { - e++; - count--; - } + const currentCase = USE_CASES[useCaseIdx]; - res = Math.max(res, count); - } + const { steps, stepLineNumbers } = useMemo(() => { + return generateVisualizationData(currentCase.intervals); + }, [currentCase]); - return res; -}`; - - useEffect(() => { - let timer: NodeJS.Timeout; - if (isPlaying && currentStepIndex < steps.length - 1) { - timer = setTimeout(() => { - setCurrentStepIndex(prev => prev + 1); - }, 1500 / speed); - } else { - setIsPlaying(false); - } - return () => clearTimeout(timer); - }, [isPlaying, currentStepIndex, steps.length, speed]); + const currentStep = steps[currentStepIndex] || steps[steps.length - 1]; + const pseudoSteps = steps.map(s => s.pseudoStep); const handleUseCaseChange = (idx: number) => { setUseCaseIdx(idx); setCurrentStepIndex(0); - setIsPlaying(false); }; return ( -
+
{/* Controls at Top */} -
-
+
+
{USE_CASES.map((uc, idx) => ( ))}
-
- + setCurrentStepIndex(prev => Math.min(steps.length - 1, prev + 1))} - onStepBack={() => setCurrentStepIndex(prev => Math.max(0, prev - 1))} - isPlaying={isPlaying} - onPlay={() => setIsPlaying(true)} - onPause={() => setIsPlaying(false)} - onReset={() => { - setCurrentStepIndex(0); - setIsPlaying(false); - }} - speed={speed} - onSpeedChange={setSpeed} + totalSteps={steps.length} + onStepChange={setCurrentStepIndex} />
- + {/* Left Column: Visual Representation & Variables & Commentary */} +
@@ -315,6 +417,7 @@ export const MeetingRoomsIIVisualization = () => {
+ {/* Start Times Row */}
Start Times @@ -324,12 +427,13 @@ export const MeetingRoomsIIVisualization = () => { {currentStep.startTimes.map((time, idx) => (
{time}
@@ -337,6 +441,7 @@ export const MeetingRoomsIIVisualization = () => {
+ {/* End Times Row */}
End Times @@ -346,12 +451,13 @@ export const MeetingRoomsIIVisualization = () => { {currentStep.endTimes.map((time, idx) => (
{time}
@@ -360,6 +466,7 @@ export const MeetingRoomsIIVisualization = () => {
+ {/* Visual Action Indicator */}
@@ -384,12 +491,13 @@ export const MeetingRoomsIIVisualization = () => {
-
+
{currentStep.s < currentStep.startTimes.length && currentStep.e < currentStep.endTimes.length ? (currentStep.startTimes[currentStep.s] < currentStep.endTimes[currentStep.e] ? "Allocate New Room" @@ -401,27 +509,30 @@ export const MeetingRoomsIIVisualization = () => {
-
-
+ {/* Descriptive Commentary Box (at the bottom) */} +
+
Process Step
- {currentStep.message} + {currentStep.explanation}
-
+ {/* Variable Panel (below the commentary box) */} +
- - setCurrentStepIndex(0)} />
- -
+
); }; \ No newline at end of file diff --git a/src/components/visualizations/algorithms/MeetingRoomsVisualization.tsx b/src/components/visualizations/algorithms/MeetingRoomsVisualization.tsx index 4b4198b..c49c164 100644 --- a/src/components/visualizations/algorithms/MeetingRoomsVisualization.tsx +++ b/src/components/visualizations/algorithms/MeetingRoomsVisualization.tsx @@ -1,9 +1,11 @@ -import React, { useState, useEffect } from 'react'; -import { Card } from '@/components/ui/card'; +import React, { useState, useMemo } from 'react'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; -import { StepControls } from '../shared/StepControls'; +import { SimpleStepControls } from '../shared/SimpleStepControls'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import { VisualizationLayout } from '../shared/VisualizationLayout'; +import { Info } from 'lucide-react'; import { Button } from '@/components/ui/button'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Interval { start: number; @@ -17,8 +19,8 @@ interface Step { previous: number; hasOverlap: boolean; isSorted: boolean; - message: string; - highlightedLines: number[]; + explanation: string; + pseudoStep: string; variables: Record; } @@ -35,298 +37,329 @@ const USE_CASES = [ } ]; -export const MeetingRoomsVisualization = () => { - const [useCaseIdx, setUseCaseIdx] = useState(0); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); // Changed to 1-3x as per StepControls - - const currentCase = USE_CASES[useCaseIdx]; - - const generateSteps = (rawIntervals: number[][]): Step[] => { - const intervals: Interval[] = rawIntervals.map((arr, index) => ({ - start: arr[0], - end: arr[1], - originalIndex: index, - })); +const languages: VisualizationLanguageMap = { + typescript: `function canAttendMeetings(intervals: number[][]): boolean { + if (intervals.length <= 1) return true; + intervals.sort((a, b) => a[0] - b[0]); + for (let i = 1; i < intervals.length; i++) { + if (intervals[i][0] < intervals[i - 1][1]) { + return false; + } + } + return true; +}`, + + python: `def canAttendMeetings(intervals: list[list[int]]) -> bool: + if len(intervals) <= 1: + return True + intervals.sort(key=lambda x: x[0]) + for i in range(1, len(intervals)): + if intervals[i][0] < intervals[i - 1][1]: + return False + return True`, + + java: `public boolean canAttendMeetings(int[][] intervals) { + if (intervals.length <= 1) { + return true; + } + Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0])); + for (int i = 1; i < intervals.length; i++) { + if (intervals[i][0] < intervals[i - 1][1]) { + return false; + } + } + return true; +}`, + + cpp: `bool canAttendMeetings(vector>& intervals) { + if (intervals.size() <= 1) return true; + sort(intervals.begin(), intervals.end(), [](const auto& a, const auto& b) { + return a[0] < b[0]; + }); + for (size_t i = 1; i < intervals.size(); i++) { + if (intervals[i][0] < intervals[i - 1][1]) { + return false; + } + } + return true; +}` +}; + +function generateVisualizationData(rawIntervals: number[][]) { + const intervals: Interval[] = rawIntervals.map((arr, index) => ({ + start: arr[0], + end: arr[1], + originalIndex: index, + })); + + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - const steps: Step[] = []; + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - // Step: Initial State - steps.push({ - intervals: [...intervals], - current: -1, - previous: -1, - hasOverlap: false, - isSorted: false, - message: "Check if we have 0 or 1 meetings. If so, return true immediately.", - highlightedLines: [2], - variables: { "intervals.length": intervals.length, "Result": "Continue" } - }); + steps.push({ + intervals: [...intervals], + current: -1, + previous: -1, + hasOverlap: false, + isSorted: false, + explanation: "First, verify list size. If there's 0 or 1 meetings, the schedule is instantly valid.", + pseudoStep: "IF intervals.length <= 1 RETURN true", + variables: { "intervals.length": intervals.length } + }); + addLines(2, 2, 2, 2); + + steps.push({ + intervals: [...intervals], + current: -1, + previous: -1, + hasOverlap: false, + isSorted: false, + explanation: "Sort all meetings by start time to evaluate intervals sequentially.", + pseudoStep: "SORT intervals BY start_time ASC", + variables: { "action": "sorting" } + }); + addLines(3, 4, 5, 3); + + const sortedIntervals = [...intervals].sort((a, b) => a.start - b.start); + steps.push({ + intervals: sortedIntervals, + current: -1, + previous: -1, + hasOverlap: false, + isSorted: true, + explanation: "Meetings are successfully sorted by start time.", + pseudoStep: "SORT COMPLETE", + variables: { "sorted": true } + }); + addLines(3, 4, 5, 3); + + for (let i = 1; i < sortedIntervals.length; i++) { + const prev = sortedIntervals[i - 1]; + const cur = sortedIntervals[i]; + const overlap = cur.start < prev.end; - // Step: Sorting (before) steps.push({ - intervals: [...intervals], - current: -1, - previous: -1, + intervals: sortedIntervals, + current: i, + previous: i - 1, hasOverlap: false, - isSorted: false, - message: "Sort the meetings by start time to process them chronologically.", - highlightedLines: [4], - variables: { "Action": "Sorting..." } + isSorted: true, + explanation: `Iteration i = ${i}: Compare meeting ${i} ([${cur.start}, ${cur.end}]) with meeting ${i - 1} ([${prev.start}, ${prev.end}]).`, + pseudoStep: `FOR i = ${i} TO intervals.length - 1`, + variables: { i, "current_meeting": `[${cur.start}, ${cur.end}]`, "previous_meeting": `[${prev.start}, ${prev.end}]` } }); + addLines(4, 5, 6, 6); - // Step: Sorting (after) - const sortedIntervals = [...intervals].sort((a, b) => a.start - b.start); steps.push({ intervals: sortedIntervals, - current: -1, - previous: -1, + current: i, + previous: i - 1, hasOverlap: false, isSorted: true, - message: "Meetings are now sorted by their start times.", - highlightedLines: [4], - variables: { "Sorted": "True" } + explanation: `Check if current start (${cur.start}) is less than previous end (${prev.end}): ${cur.start} < ${prev.end}?`, + pseudoStep: `IF current.start < previous.end`, + variables: { i, "curr.start": cur.start, "prev.end": prev.end } }); + addLines(5, 6, 7, 7); - // Loop steps - for (let i = 1; i < sortedIntervals.length; i++) { - const prev = sortedIntervals[i - 1]; - const cur = sortedIntervals[i]; - const overlap = cur.start < prev.end; - - // i check - steps.push({ - intervals: sortedIntervals, - current: i, - previous: i - 1, - hasOverlap: false, - isSorted: true, - message: `Loop iteration i = ${i}. Comparing meeting ${i} with meeting ${i - 1}.`, - highlightedLines: [6], - variables: { "i": i, "intervals.length": sortedIntervals.length } - }); - - // destructure cur - steps.push({ - intervals: sortedIntervals, - current: i, - previous: i - 1, - hasOverlap: false, - isSorted: true, - message: `Current meeting: [${cur.start}, ${cur.end}].`, - highlightedLines: [7], - variables: { "start": cur.start, "end": cur.end } - }); - - // destructure prev + if (overlap) { steps.push({ intervals: sortedIntervals, current: i, previous: i - 1, - hasOverlap: false, + hasOverlap: true, isSorted: true, - message: `Previous meeting: [${prev.start}, ${prev.end}].`, - highlightedLines: [8], - variables: { "prevStart": prev.start, "prevEnd": prev.end } + explanation: `Overlap detected! Meeting starts at ${cur.start} before previous finishes at ${prev.end}. Return false.`, + pseudoStep: `RETURN false`, + variables: { "conflict": true, "return": false } }); - - // if check - steps.push({ - intervals: sortedIntervals, - current: i, - previous: i - 1, - hasOverlap: false, - isSorted: true, - message: `Is start (${cur.start}) < previous end (${prev.end})?`, - highlightedLines: [10], - variables: { "check": `${cur.start} < ${prev.end}`, "result": overlap.toString() } - }); - - if (overlap) { - steps.push({ - intervals: sortedIntervals, - current: i, - previous: i - 1, - hasOverlap: true, - isSorted: true, - message: `OVERLAP! Person cannot attend both meetings.`, - highlightedLines: [11], - variables: { "Conflict": "Overlap found", "Return": "false" } - }); - return steps; - } + addLines(6, 7, 8, 8); + return { steps, stepLineNumbers }; } + } - // Final Success - steps.push({ - intervals: sortedIntervals, - current: -1, - previous: -1, - hasOverlap: false, - isSorted: true, - message: "Checked all consecutive pairs. No overlaps found!", - highlightedLines: [15], - variables: { "Return": "true" } - }); - - return steps; - }; + steps.push({ + intervals: sortedIntervals, + current: -1, + previous: -1, + hasOverlap: false, + isSorted: true, + explanation: "Checked all consecutive intervals. No overlaps found. Return true.", + pseudoStep: "RETURN true", + variables: { "return": true } + }); + addLines(9, 8, 11, 11); + + return { steps, stepLineNumbers }; +} - const steps = generateSteps(currentCase.intervals); - const currentStep = steps[currentStepIndex] || steps[steps.length - 1]; +export const MeetingRoomsVisualization: React.FC = () => { + const [useCaseIdx, setUseCaseIdx] = useState(0); + const currentCase = USE_CASES[useCaseIdx]; + const { steps, stepLineNumbers } = useMemo(() => generateVisualizationData(currentCase.intervals), [useCaseIdx]); + const [currentStepIndex, setCurrentStepIndex] = useState(0); - const code = `function canAttendMeetings(intervals: number[][]): boolean { - if (intervals.length <= 1) return true; - - intervals.sort((a, b) => a[0] - b[0]); - - for (let i = 1; i < intervals.length; i++) { - const [start, end] = intervals[i]; - const [prevStart, prevEnd] = intervals[i - 1]; - - if (start < prevEnd) { - return false; - } - } - - return true; -}`; - - useEffect(() => { - let timer: NodeJS.Timeout; - if (isPlaying && currentStepIndex < steps.length - 1) { - timer = setTimeout(() => { - setCurrentStepIndex(prev => prev + 1); - }, 2000 / speed); - } else { - setIsPlaying(false); - } - return () => clearTimeout(timer); - }, [isPlaying, currentStepIndex, steps.length, speed]); + const handleReset = () => { + setCurrentStepIndex(0); + }; const handleUseCaseChange = (idx: number) => { setUseCaseIdx(idx); setCurrentStepIndex(0); - setIsPlaying(false); }; + const currentStep = steps[currentStepIndex] || steps[steps.length - 1]; + const pseudoSteps = steps.map((s) => s.pseudoStep); + return ( -
-
-
- {USE_CASES.map((uc, idx) => ( - - ))} -
+ +
+ {USE_CASES.map((uc, idx) => ( + + ))} +
-
- setCurrentStepIndex(prev => Math.min(steps.length - 1, prev + 1))} - onStepBack={() => setCurrentStepIndex(prev => Math.max(0, prev - 1))} - isPlaying={isPlaying} - onPlay={() => setIsPlaying(true)} - onPause={() => setIsPlaying(false)} - onReset={() => { - setCurrentStepIndex(0); - setIsPlaying(false); - }} - speed={speed} - onSpeedChange={setSpeed} - /> -
-
- -
- -
-
-
-
- Normal -
-
-
- Conflict +
+
+

Timeline

+
+
+
+ Comparing +
+
+
+ Conflict +
-
-
-
- {[0, 5, 10, 15, 20, 25, 30].map(t => {t})} -
+
+
+ {[0, 5, 10, 15, 20, 25, 30].map(t => {t})} +
-
- {currentStep.intervals.map((interval, idx) => { - const isCurrent = idx === currentStep.current; - const isPrevious = idx === currentStep.previous; - const isConflicting = (isCurrent || isPrevious) && currentStep.hasOverlap; - - return ( -
- M{idx} -
-
+ {currentStep.intervals.map((interval, idx) => { + const isCurrent = idx === currentStep.current; + const isPrevious = idx === currentStep.previous; + const isConflicting = (isCurrent || isPrevious) && currentStep.hasOverlap; + + return ( +
+ M{idx} +
+
- {interval.start}-{interval.end} + style={{ + left: `${(interval.start / 30) * 100}%`, + width: `${((interval.end - interval.start) / 30) * 100}%`, + }} + > + {interval.start}-{interval.end} +
+ ); + })} +
+ + {currentStep.previous !== -1 && currentStep.current !== -1 && ( +
+
+ End: {currentStep.intervals[currentStep.previous].end}
- ); - })} +
+ )}
+
- {currentStep.previous !== -1 && currentStep.current !== -1 && ( -
-
- End: {currentStep.intervals[currentStep.previous].end} +
+
+
+
+ + + + + + Algorithm Commentary + +
+
+ Step {currentStepIndex + 1} of {steps.length}
- )} -
-
- {currentStep.message} +
+
+ +
+
+

+ Current Action +

+
+ {currentStep.explanation} +
+
+
+
- - - + } + rightContent={ + + } + controls={ + -
-
+ } + /> ); -}; \ No newline at end of file +}; + +export default MeetingRoomsVisualization; \ No newline at end of file diff --git a/src/components/visualizations/algorithms/MergeKSortedListsVisualization.tsx b/src/components/visualizations/algorithms/MergeKSortedListsVisualization.tsx index ce62b1e..57efb1c 100644 --- a/src/components/visualizations/algorithms/MergeKSortedListsVisualization.tsx +++ b/src/components/visualizations/algorithms/MergeKSortedListsVisualization.tsx @@ -1,16 +1,12 @@ import React, { useState, useMemo } from 'react'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; import { motion, AnimatePresence } from 'framer-motion'; import { Card } from '@/components/ui/card'; import { ArrowRight } from 'lucide-react'; - -interface ListNode { - val: number; - next: ListNode | null; -} +import type { VisualizationLanguageMap, StepLineNumberMap } from '@/types/visualization'; interface Step { allLists: number[][]; @@ -19,258 +15,408 @@ interface Step { mergedBuilder: number[]; phase: 'global' | 'merging' | 'complete'; explanation: string; - highlightedLines: number[]; + pseudoStep: string; variables: Record; } -export const MergeKSortedListsVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); +// ─── Hardcoded code per language (No comments, no blank lines) ─────────────── +const languages: VisualizationLanguageMap = { + python: `def mergeKLists(lists: List[Optional[ListNode]]) -> Optional[ListNode]: + def mergeTwoLists(l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]: + dummy = ListNode() + tail = dummy + while l1 and l2: + if l1.val < l2.val: + tail.next = l1 + l1 = l1.next + else: + tail.next = l2 + l2 = l2.next + tail = tail.next + if l1: + tail.next = l1 + elif l2: + tail.next = l2 + return dummy.next + if not lists: + return None + if len(lists) == 1: + return lists[0] + mid = len(lists) // 2 + left = mergeKLists(lists[:mid]) + right = mergeKLists(lists[mid:]) + return mergeTwoLists(left, right)`, + + typescript: `function mergeKLists(lists: Array): ListNode | null { + if (!lists || lists.length === 0) { + return null; + } + function mergeTwoLists(l1: ListNode | null, l2: ListNode | null): ListNode | null { + const dummy = new ListNode(); + let tail: ListNode = dummy; + while (l1 !== null && l2 !== null) { + if (l1.val < l2.val) { + tail.next = l1; + l1 = l1.next; + } else { + tail.next = l2; + l2 = l2.next; + } + tail = tail.next!; + } + if (l1 !== null) { + tail.next = l1; + } else if (l2 !== null) { + tail.next = l2; + } + return dummy.next; + } + let mergedList: ListNode | null = null; + for (let i = 0; i < lists.length; i++) { + mergedList = mergeTwoLists(mergedList, lists[i]); + } + return mergedList; +}`, + + java: `public class Solution { + public ListNode mergeKLists(ListNode[] lists) { + if (lists == null || lists.length == 0) { + return null; + } + return mergeKListsHelper(lists, 0, lists.length - 1); + } + private ListNode mergeKListsHelper(ListNode[] lists, int start, int end) { + if (start == end) { + return lists[start]; + } + int mid = start + (end - start) / 2; + ListNode left = mergeKListsHelper(lists, start, mid); + ListNode right = mergeKListsHelper(lists, mid + 1, end); + return mergeTwoLists(left, right); + } + private ListNode mergeTwoLists(ListNode l1, ListNode l2) { + ListNode dummy = new ListNode(); + ListNode tail = dummy; + while (l1 != null && l2 != null) { + if (l1.val < l2.val) { + tail.next = l1; + l1 = l1.next; + } else { + tail.next = l2; + l2 = l2.next; + } + tail = tail.next; + } + if (l1 != null) { + tail.next = l1; + } else { + tail.next = l2; + } + return dummy.next; + } +}`, + + cpp: `class Solution { +public: + ListNode* mergeKLists(vector& lists) { + auto cmp = [](ListNode* a, ListNode* b) { return a->val > b->val; }; + priority_queue, decltype(cmp)> heap(cmp); + for (ListNode* head : lists) { + if (head) { + heap.push(head); + } + } + ListNode* dummy = new ListNode(0); + ListNode* current = dummy; + while (!heap.empty()) { + ListNode* node = heap.top(); + heap.pop(); + current->next = node; + current = current->next; + if (node->next) { + heap.push(node->next); + } + } + return dummy->next; + } +};` +}; + +// ─── Step Generator ────────────────────────────────────────────────────────── +function generateVisualizationData() { const initialLists = [ [1, 4, 5], [1, 3, 4], [2, 6] ]; - const steps: Step[] = useMemo(() => { - const s: Step[] = []; - let lists: number[][] = initialLists.map(l => [...l]); - - s.push({ + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + let lists: number[][] = initialLists.map(l => [...l]); + + steps.push({ + allLists: lists.map(l => [...l]), + l1: null, + l2: null, + mergedBuilder: [], + phase: 'global', + explanation: "Starting mergeKLists with sorted linked lists.", + pseudoStep: "CALL mergeKLists(lists)", + variables: { totalLists: lists.length } + }); + addLines(1, 1, 2, 3); + + steps.push({ + allLists: lists.map(l => [...l]), + l1: null, + l2: null, + mergedBuilder: [], + phase: 'global', + explanation: "Check for base case: if the input array of lists is empty or null.", + pseudoStep: "IF lists IS EMPTY -> RETURN null", + variables: { totalLists: lists.length } + }); + addLines(2, 18, 3, 6); + + while (lists.length > 1) { + steps.push({ allLists: lists.map(l => [...l]), - l1: null, l2: null, mergedBuilder: [], + l1: null, + l2: null, + mergedBuilder: [], phase: 'global', - explanation: "Starting mergeKLists with sorted linked lists.", - highlightedLines: [1], - variables: { totalLists: lists.length } + explanation: `Main loop: lists.length = ${lists.length} > 1. Start a new round of pairwise merging. This Divide & Conquer strategy reduces the number of lists logarithmically.`, + pseudoStep: `WHILE lists.length = ${lists.length} > 1`, + variables: { currentListsCount: lists.length } }); + addLines(26, 22, 12, 13); - s.push({ + const mergedLists: number[][] = []; + steps.push({ allLists: lists.map(l => [...l]), - l1: null, l2: null, mergedBuilder: [], + l1: null, + l2: null, + mergedBuilder: [], phase: 'global', - explanation: "Check for base case: if lists array is empty or null.", - highlightedLines: [2], - variables: { totalLists: lists.length } + explanation: "Initialize an empty array to store the results of this round's merges.", + pseudoStep: "SET mergedLists = []", + variables: { mergedProgress: "[]" } }); + addLines(25, 22, 12, 11); - while (lists.length > 1) { - s.push({ - allLists: lists.map(l => [...l]), - l1: null, l2: null, mergedBuilder: [], - phase: 'global', - explanation: `Main loop: lists.length = ${lists.length} > 1. Start a new round of pairwise merging. This Divide & Conquer strategy reduces the number of lists logarithmically.`, - highlightedLines: [6], - variables: { currentListsCount: lists.length } - }); + for (let i = 0; i < lists.length; i += 2) { + const l1 = lists[i]; + const l2 = (i + 1 < lists.length) ? lists[i + 1] : null; - const mergedLists: number[][] = []; - s.push({ + steps.push({ allLists: lists.map(l => [...l]), - l1: null, l2: null, mergedBuilder: [], - phase: 'global', - explanation: "Initialize an empty array to store the results of this round's merges.", - highlightedLines: [7], - variables: { mergedProgress: "[]" } + l1, + l2, + mergedBuilder: [], + phase: 'merging', + explanation: `Pick adjacent lists (at index ${i} and ${i + 1}) to merge.`, + pseudoStep: `FOR i = ${i}: l1 = lists[${i}], l2 = lists[${i + 1}]`, + variables: { i, l1Size: l1?.length || 0, l2Size: l2?.length || 0 } }); + addLines(27, 23, 13, 14); - for (let i = 0; i < lists.length; i += 2) { - const l1 = lists[i]; - const l2 = (i + 1 < lists.length) ? lists[i + 1] : null; + const merged: number[] = []; + if (l1 && l2) { + let ptr1 = 0; + let ptr2 = 0; - s.push({ + steps.push({ allLists: lists.map(l => [...l]), - l1, l2, mergedBuilder: [], + l1, + l2, + mergedBuilder: [], phase: 'merging', - explanation: `Pick two adjacent lists (at index ${i} and ${i + 1}) to merge into a single sorted list.`, - highlightedLines: [9, 10, 11], - variables: { i, l1Size: l1?.length || 0, l2Size: l2?.length || 0 } + explanation: "Call mergeTwoLists. Create a dummy node as a placeholder for the head of the new merged list.", + pseudoStep: "CALL mergeTwoLists(l1, l2) -> dummy = {val: 0, next: null}", + variables: { l1: `[${l1.join(',')}]`, l2: `[${l2.join(',')}]`, dummy: "initialized" } }); + addLines(6, 3, 18, 11); - // Simulating mergeTwoLists - const merged: number[] = []; - if (l1 && l2) { - let ptr1 = 0; - let ptr2 = 0; - - s.push({ + while (ptr1 < l1.length && ptr2 < l2.length) { + steps.push({ allLists: lists.map(l => [...l]), - l1, l2, mergedBuilder: [], + l1: l1.slice(ptr1), + l2: l2.slice(ptr2), + mergedBuilder: [...merged], phase: 'merging', - explanation: "Call mergeTwoLists. Create a dummy node to act as a placeholder for the head of the new merged list.", - highlightedLines: [20, 21], - variables: { l1: `[${l1.join(',')}]`, l2: `[${l2.join(',')}]`, dummy: "initialized" } + explanation: `Iteration check: compare head values: l1 = ${l1[ptr1]}, l2 = ${l2[ptr2]}.`, + pseudoStep: `WHILE l1 AND l2 -> COMPARE l1.val (${l1[ptr1]}) AND l2.val (${l2[ptr2]})`, + variables: { val1: l1[ptr1], val2: l2[ptr2] } }); + addLines(8, 5, 20, 13); - while (ptr1 < l1.length && ptr2 < l2.length) { - s.push({ + if (l1[ptr1] < l2[ptr2]) { + const val = l1[ptr1]; + steps.push({ allLists: lists.map(l => [...l]), - l1: l1.slice(ptr1), l2: l2.slice(ptr2), mergedBuilder: [...merged], + l1: l1.slice(ptr1), + l2: l2.slice(ptr2), + mergedBuilder: [...merged], phase: 'merging', - explanation: `Iteration check: both lists still have nodes. Compare current values: ${l1[ptr1]} and ${l2[ptr2]}.`, - highlightedLines: [23, 24], - variables: { val1: l1[ptr1], val2: l2[ptr2] } + explanation: `Value ${val} is smaller than ${l2[ptr2]}. Point tail.next to l1 node.`, + pseudoStep: `IF l1.val (${val}) < l2.val (${l2[ptr2]}) -> tail.next = l1`, + variables: { chosen: val } }); + addLines(10, 7, 22, 16); - if (l1[ptr1] < l2[ptr2]) { - const val = l1[ptr1]; - s.push({ - allLists: lists.map(l => [...l]), - l1: l1.slice(ptr1), l2: l2.slice(ptr2), mergedBuilder: [...merged], - phase: 'merging', - explanation: `${val} is smaller than ${l2[ptr2]}. Point tail.next to the node from list 1.`, - highlightedLines: [25], - variables: { chosen: val } - }); - - merged.push(val); - ptr1++; - s.push({ - allLists: lists.map(l => [...l]), - l1: l1.slice(ptr1), l2: l2.slice(ptr2), mergedBuilder: [...merged], - phase: 'merging', - explanation: `Shift list 1 pointer to the next node and advance the tail.`, - highlightedLines: [26, 31], - variables: { ptr1, mergedSize: merged.length } - }); - } else { - const val = l2[ptr2]; - s.push({ - allLists: lists.map(l => [...l]), - l1: l1.slice(ptr1), l2: l2.slice(ptr2), mergedBuilder: [...merged], - phase: 'merging', - explanation: `${val} is smaller than or equal to ${l1[ptr1]}. Point tail.next to the node from list 2.`, - highlightedLines: [28], - variables: { chosen: val } - }); - - merged.push(val); - ptr2++; - s.push({ - allLists: lists.map(l => [...l]), - l1: l1.slice(ptr1), l2: l2.slice(ptr2), mergedBuilder: [...merged], - phase: 'merging', - explanation: `Shift list 2 pointer to the next node and advance the tail.`, - highlightedLines: [29, 31], - variables: { ptr2, mergedSize: merged.length } - }); - } - } + merged.push(val); + ptr1++; + steps.push({ + allLists: lists.map(l => [...l]), + l1: l1.slice(ptr1), + l2: l2.slice(ptr2), + mergedBuilder: [...merged], + phase: 'merging', + explanation: "Advance l1 pointer and move tail forward to the newly added node.", + pseudoStep: "SET l1 = l1.next, tail = tail.next", + variables: { ptr1, mergedSize: merged.length } + }); + addLines(11, 8, 23, 17); + } else { + const val = l2[ptr2]; + steps.push({ + allLists: lists.map(l => [...l]), + l1: l1.slice(ptr1), + l2: l2.slice(ptr2), + mergedBuilder: [...merged], + phase: 'merging', + explanation: `Value ${val} is smaller than or equal to ${l1[ptr1]}. Point tail.next to l2 node.`, + pseudoStep: `ELSE -> tail.next = l2`, + variables: { chosen: val } + }); + addLines(13, 10, 25, 16); - if (ptr1 < l1.length || ptr2 < l2.length) { - const remaining = (ptr1 < l1.length) ? l1.slice(ptr1) : l2.slice(ptr2); - s.push({ + merged.push(val); + ptr2++; + steps.push({ allLists: lists.map(l => [...l]), - l1: ptr1 < l1.length ? l1.slice(ptr1) : null, - l2: ptr2 < l2.length ? l2.slice(ptr2) : null, + l1: l1.slice(ptr1), + l2: l2.slice(ptr2), mergedBuilder: [...merged], phase: 'merging', - explanation: "One list is empty. Attach all remaining nodes from the other list in O(1) time by re-linking the next pointer.", - highlightedLines: [34], - variables: { remainingSize: remaining.length } + explanation: "Advance l2 pointer and move tail forward to the newly added node.", + pseudoStep: "SET l2 = l2.next, tail = tail.next", + variables: { ptr2, mergedSize: merged.length } }); - merged.push(...remaining); + addLines(14, 11, 26, 17); } + } - s.push({ - allLists: lists.map(l => [...l]), - l1: null, l2: null, mergedBuilder: [...merged], - phase: 'merging', - explanation: "Merge pair complete. Return dummy.next to get the new head.", - highlightedLines: [35], - variables: { mergedList: `[${merged.join(',')}]` } - }); - } else { - // If l2 is null, just push l1 - merged.push(...(l1 || [])); - s.push({ + if (ptr1 < l1.length || ptr2 < l2.length) { + const remaining = (ptr1 < l1.length) ? l1.slice(ptr1) : l2.slice(ptr2); + steps.push({ allLists: lists.map(l => [...l]), - l1: null, l2: null, mergedBuilder: [...merged], + l1: ptr1 < l1.length ? l1.slice(ptr1) : null, + l2: ptr2 < l2.length ? l2.slice(ptr2) : null, + mergedBuilder: [...merged], phase: 'merging', - explanation: "Only one list left in this round (odd number of lists). No merge needed, just pass it to the next round.", - highlightedLines: [12], - variables: { merged: `[${merged.join(',')}]` } + explanation: "One list is empty. Link remaining nodes of the other list directly to tail.next.", + pseudoStep: "SET tail.next = l1 OR l2", + variables: { remainingSize: remaining.length } }); + addLines(18, 13, 30, 18); + merged.push(...remaining); } - mergedLists.push(merged); - s.push({ + steps.push({ allLists: lists.map(l => [...l]), - l1: null, l2: null, mergedBuilder: [], - phase: 'global', - explanation: `Store the result of the merge back into our pool of sorted lists. Current pool size: ${mergedLists.length}`, - highlightedLines: [12], - variables: { mergedListsSize: mergedLists.length } + l1: null, + l2: null, + mergedBuilder: [...merged], + phase: 'merging', + explanation: "Merge pair complete. Return dummy.next as the head of this merged list.", + pseudoStep: "RETURN dummy.next", + variables: { mergedList: `[${merged.join(',')}]` } }); + addLines(23, 17, 35, 22); + } else { + merged.push(...(l1 || [])); + steps.push({ + allLists: lists.map(l => [...l]), + l1: null, + l2: null, + mergedBuilder: [...merged], + phase: 'merging', + explanation: "Only one list left in this round (odd list count). Pass it forward directly.", + pseudoStep: "ADD l1 to mergedLists", + variables: { merged: `[${merged.join(',')}]` } + }); + addLines(29, 20, 9, 22); } - lists = mergedLists; - s.push({ + mergedLists.push(merged); + steps.push({ allLists: lists.map(l => [...l]), - l1: null, l2: null, mergedBuilder: [], + l1: null, + l2: null, + mergedBuilder: [], phase: 'global', - explanation: "Round complete. We've replaced the original lists with their merged results, effectively halving the number of lists.", - highlightedLines: [14], - variables: { remainingLists: lists.length } + explanation: `Append merged list to our list pool. Pool size: ${mergedLists.length}`, + pseudoStep: "APPEND merged list to mergedLists", + variables: { mergedListsSize: mergedLists.length } }); + addLines(27, 25, 15, 13); } - s.push({ + lists = mergedLists; + steps.push({ allLists: lists.map(l => [...l]), - l1: null, l2: null, mergedBuilder: [], - phase: 'complete', - explanation: "Process complete! All lists have been merged into a single sorted linked list. Return the final head.", - highlightedLines: [17], - variables: { finalSize: lists[0]?.length || 0 } + l1: null, + l2: null, + mergedBuilder: [], + phase: 'global', + explanation: "Iteration round complete. We replace original lists with merged results, halving list count.", + pseudoStep: "SET lists = mergedLists", + variables: { remainingLists: lists.length } }); + addLines(26, 25, 15, 13); + } + + steps.push({ + allLists: lists.map(l => [...l]), + l1: null, + l2: null, + mergedBuilder: [], + phase: 'complete', + explanation: "All lists merged into a single sorted list. Return the final head.", + pseudoStep: "RETURN lists[0]", + variables: { finalSize: lists[0]?.length || 0 } + }); + addLines(29, 25, 6, 22); + + return { steps, stepLineNumbers }; +} - return s; - }, [initialLists]); - - const code = `function mergeKLists(lists: Array): ListNode | null { - if (!lists || lists.length === 0) { - return null; - } - - while (lists.length > 1) { - const mergedLists: Array = []; - - for (let i = 0; i < lists.length; i += 2) { - const l1 = lists[i] || null; - const l2 = (i + 1 < lists.length) ? lists[i + 1] : null; - mergedLists.push(mergeTwoLists(l1, l2)); - } - lists = mergedLists; - } - - return lists[0] || null; - - function mergeTwoLists(l1: ListNode | null, l2: ListNode | null): ListNode | null { - const dummy = { val: -1, next: null }; - let tail = dummy; - - while (l1 && l2) { - if (l1.val < l2.val) { - tail.next = l1; - l1 = l1.next; - } else { - tail.next = l2; - l2 = l2.next; - } - tail = tail.next; - } - - tail.next = l1 || l2; - return dummy.next; - } -}`; +export const MergeKSortedListsVisualization = () => { + const { steps, stepLineNumbers } = useMemo(generateVisualizationData, []); + const [currentStepIndex, setCurrentStepIndex] = useState(0); - const step = steps[currentStep]; + const step = steps[currentStepIndex]; + const pseudoSteps = useMemo(() => steps.map(s => s.pseudoStep), [steps]); return ( - +

Iterative Process

{step.allLists.map((list, idx) => ( @@ -286,7 +432,7 @@ export const MergeKSortedListsVisualization = () => {
{val} @@ -303,14 +449,15 @@ export const MergeKSortedListsVisualization = () => {
- + {step.phase === 'merging' && ( - +

Merging Sandbox

@@ -343,8 +490,9 @@ export const MergeKSortedListsVisualization = () => { {v} @@ -354,14 +502,6 @@ export const MergeKSortedListsVisualization = () => { )} ))} - {(step.l1?.length || 0 > 0 || step.l2?.length || 0 > 0) && step.mergedBuilder.length > 0 && ( - - - - )}
@@ -370,41 +510,43 @@ export const MergeKSortedListsVisualization = () => { )} - -

- - Educational Insight -

-
-

- {step.explanation} -

-
-
Why Divide & Conquer?
-
    -
  • Time Complexity: O(N log k) where N is total nodes and k is number of lists.
  • -
  • Space Complexity: O(1) in this iterative version as we reuse pointers.
  • -
  • Mechanism: Instead of merging 1st into 2nd, then 2nd into 3rd (O(N*k)), we merge in pairs (O(N log k)).
  • -
-
+ {/* Descriptive Commentary Box (at the bottom) */} +
+
+
+ Process Step
- + {step.explanation} +
+ +
+
Complexity Overview:
+
+

Time: O(N log k) where N is total nodes across all lists, and k is the number of lists.

+

Space: O(1) auxiliary space (excluding the output list) since we merge lists in-place.

+
+
- + {/* Variable Panel (below the commentary box) */} +
+ +
} rightContent={ - setCurrentStepIndex(0)} /> } controls={ } /> diff --git a/src/components/visualizations/algorithms/MergeSortLinkedListVisualization.tsx b/src/components/visualizations/algorithms/MergeSortLinkedListVisualization.tsx index 5d1eeb1..7e5d012 100644 --- a/src/components/visualizations/algorithms/MergeSortLinkedListVisualization.tsx +++ b/src/components/visualizations/algorithms/MergeSortLinkedListVisualization.tsx @@ -1,9 +1,10 @@ import { useEffect, useRef, useState } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { ArrowRight, Info } from 'lucide-react'; -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface ListNodeData { id: string; @@ -19,42 +20,105 @@ interface Step { tailId: string | null; allNodes: Record; message: string; - lineNumber: number; + pseudoStep: string; highlightNodes: string[]; variables: Record; } +// ─── DB Codes (no modification, exact match) ──────────────────────────────── + +const languages: VisualizationLanguageMap = { + typescript: `function mergeTwoLists(list1: ListNode | null, list2: ListNode | null): ListNode | null { + const dummy = new ListNode(); + let tail = dummy; + while (list1 && list2) { + if (list1.val < list2.val) { + tail.next = list1; + list1 = list1.next; + } else { + tail.next = list2; + list2 = list2.next; + } + tail = tail.next; + } + if (list1) { + tail.next = list1; + } else if (list2) { + tail.next = list2; + } + return dummy.next; +}`, + + python: `def mergeTwoLists(list1, list2): + dummy = ListNode() + tail = dummy + while list1 and list2: + if list1.val < list2.val: + tail.next = list1 + list1 = list1.next + else: + tail.next = list2 + list2 = list2.next + tail = tail.next + if list1: + tail.next = list1 + elif list2: + tail.next = list2 + return dummy.next`, + + java: `public static class Solution { + public ListNode mergeTwoLists(ListNode list1, ListNode list2) { + ListNode dummy = new ListNode(); + ListNode tail = dummy; + while (list1 != null && list2 != null) { + if (list1.val < list2.val) { + tail.next = list1; + list1 = list1.next; + } else { + tail.next = list2; + list2 = list2.next; + } + tail = tail.next; + } + if (list1 != null) { + tail.next = list1; + } else { + tail.next = list2; + } + return dummy.next; + } +}`, + + cpp: `class Solution { +public: + ListNode * mergeTwoLists(ListNode * list1, ListNode * list2) { + ListNode dummy(0); + ListNode * curr = & dummy; + while (list1 && list2) { + if (list1 -> val <= list2 -> val) { + curr -> next = list1; + list1 = list1 -> next; + } else { + curr -> next = list2; + list2 = list2 -> next; + } + curr = curr -> next; + } + curr -> next = list1 ? list1 : list2; + return dummy.next; + } +};` +}; + + export const MergeSortLinkedListVisualization = () => { const [steps, setSteps] = useState([]); + const [stepLineNumbers, setStepLineNumbers] = useState({}); const [currentStepIndex, setCurrentStepIndex] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const [speed, setSpeed] = useState(1); const intervalRef = useRef(null); - const code = `function mergeTwoLists(list1, list2) { - const dummy = new ListNode(); - let tail = dummy; - - while (list1 && list2) { - if (list1.val < list2.val) { - tail.next = list1; - list1 = list1.next; - } else { - tail.next = list2; - list2 = list2.next; - } - tail = tail.next; - } - - if (list1) { - tail.next = list1; - } else if (list2) { - tail.next = list2; - } - - return dummy.next; -}`; - const generateSteps = () => { const l1Vals = [1, 3, 5]; const l2Vals = [2, 4, 6]; @@ -78,16 +142,22 @@ export const MergeSortLinkedListVisualization = () => { let list2 = createList(l2Vals, 'l2'); const newSteps: Step[] = []; - - const cloneNodes = (nodes: Record) => { - const clone: Record = {}; - for (const id in nodes) { - clone[id] = { ...nodes[id] }; - } - return clone; + const lines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] }; - const addStep = (msg: string, line: number, extra: Partial = {}) => { + const addStep = ( + msg: string, + pseudo: string, + tsLine: number, + pyLine: number, + javaLine: number, + cppLine: number, + extra: Partial = {} + ) => { newSteps.push({ list1HeadId: list1, list2HeadId: list2, @@ -96,7 +166,7 @@ export const MergeSortLinkedListVisualization = () => { tailId: extra.tailId || null, allNodes: cloneNodes(allNodes), message: msg, - lineNumber: line, + pseudoStep: pseudo, highlightNodes: extra.highlightNodes || [], variables: { list1: list1 ? allNodes[list1].val : 'null', @@ -105,71 +175,141 @@ export const MergeSortLinkedListVisualization = () => { ...extra.variables } }); + lines.typescript!.push(tsLine); + lines.python!.push(pyLine); + lines.java!.push(javaLine); + lines.cpp!.push(cppLine); + }; + + const cloneNodes = (nodes: Record) => { + const clone: Record = {}; + for (const id in nodes) { + clone[id] = { ...nodes[id] }; + } + return clone; }; // Initial State - addStep('Start with two sorted linked lists', 1); + addStep( + 'Start with two sorted linked lists', + 'START mergeTwoLists(list1, list2)', + 1, 1, 2, 3 + ); // const dummy = new ListNode(); const dummyId = 'dummy'; allNodes[dummyId] = { id: dummyId, val: 0, nextId: null }; let tailId = dummyId; - addStep('Create a dummy node to simplify merging', 2, { dummyId, tailId }); + addStep( + 'Create a dummy node to simplify merging', + 'SET dummy = ListNode()', + 2, 2, 3, 4, + { dummyId, tailId } + ); // let tail = dummy; - addStep('Initialize tail pointer to dummy node', 3, { dummyId, tailId }); + addStep( + 'Initialize tail pointer to dummy node', + 'SET tail = dummy', + 3, 3, 4, 5, + { dummyId, tailId } + ); while (list1 && list2) { - addStep(`Compare list1 (${allNodes[list1].val}) and list2 (${allNodes[list2].val})`, 5, { dummyId, tailId, highlightNodes: [list1, list2] }); + addStep( + `Compare list1 (${allNodes[list1].val}) and list2 (${allNodes[list2].val})`, + `WHILE list1 AND list2 → ${allNodes[list1].val} vs ${allNodes[list2].val}`, + 4, 4, 5, 6, + { dummyId, tailId, highlightNodes: [list1, list2] } + ); + + addStep( + `Check if list1 value is smaller: ${allNodes[list1].val} < ${allNodes[list2].val}`, + `IF list1.val < list2.val → ${allNodes[list1].val} < ${allNodes[list2].val}`, + 5, 5, 6, 7, + { dummyId, tailId, highlightNodes: [list1, list2] } + ); if (allNodes[list1].val < allNodes[list2].val) { - // tail.next = list1; const currentL1 = list1; allNodes[tailId].nextId = currentL1; - addStep(`${allNodes[list1].val} < ${allNodes[list2].val}, point tail.next to list1 node`, 7, { dummyId, tailId, highlightNodes: [currentL1] }); + addStep( + `${allNodes[list1].val} < ${allNodes[list2].val}, point tail.next to list1 node`, + 'SET tail.next = list1', + 6, 6, 7, 8, + { dummyId, tailId, highlightNodes: [currentL1] } + ); - // list1 = list1.next; list1 = allNodes[list1].nextId; - addStep('Advance list1 pointer', 8, { dummyId, tailId }); + addStep( + 'Advance list1 pointer', + 'SET list1 = list1.next', + 7, 7, 8, 9, + { dummyId, tailId } + ); } else { - // tail.next = list2; const currentL2 = list2; allNodes[tailId].nextId = currentL2; - addStep(`${allNodes[list2].val} <= ${allNodes[list1].val}, point tail.next to list2 node`, 10, { dummyId, tailId, highlightNodes: [currentL2] }); + addStep( + `${allNodes[list2].val} <= ${allNodes[list1].val}, point tail.next to list2 node`, + 'SET tail.next = list2', + 9, 9, 10, 11, + { dummyId, tailId, highlightNodes: [currentL2] } + ); - // list2 = list2.next; list2 = allNodes[list2].nextId; - addStep('Advance list2 pointer', 11, { dummyId, tailId }); + addStep( + 'Advance list2 pointer', + 'SET list2 = list2.next', + 10, 10, 11, 12, + { dummyId, tailId } + ); } - // tail = tail.next; tailId = allNodes[tailId].nextId!; - addStep('Advance tail pointer to the newly added node', 13, { dummyId, tailId }); + addStep( + 'Advance tail pointer to the newly added node', + 'SET tail = tail.next', + 12, 11, 13, 14, + { dummyId, tailId } + ); } if (list1) { - // tail.next = list1; allNodes[tailId].nextId = list1; - addStep('list2 is exhausted, attach remaining list1 nodes', 17, { dummyId, tailId, highlightNodes: [list1] }); + addStep( + 'list2 is exhausted, attach remaining list1 nodes', + 'SET tail.next = list1 (attach remaining)', + 15, 13, 16, 16, + { dummyId, tailId, highlightNodes: [list1] } + ); - // Advance tail to end for visualization let curr = list1; while (allNodes[curr].nextId) curr = allNodes[curr].nextId!; tailId = curr; } else if (list2) { - // tail.next = list2; allNodes[tailId].nextId = list2; - addStep('list1 is exhausted, attach remaining list2 nodes', 19, { dummyId, tailId, highlightNodes: [list2] }); + addStep( + 'list1 is exhausted, attach remaining list2 nodes', + 'SET tail.next = list2 (attach remaining)', + 17, 15, 18, 16, + { dummyId, tailId, highlightNodes: [list2] } + ); - // Advance tail to end for visualization let curr = list2; while (allNodes[curr].nextId) curr = allNodes[curr].nextId!; tailId = curr; } - addStep('Merge complete! Return dummy.next as the new head', 22, { dummyId, tailId }); + addStep( + 'Merge complete! Return dummy.next as the new head', + 'RETURN dummy.next', + 19, 16, 20, 17, + { dummyId, tailId } + ); setSteps(newSteps); + setStepLineNumbers(lines); setCurrentStepIndex(0); }; @@ -187,7 +327,7 @@ export const MergeSortLinkedListVisualization = () => { } return prev + 1; }); - }, 1000 / speed); + }, 1200 / speed); } else { if (intervalRef.current) clearInterval(intervalRef.current); } @@ -203,14 +343,14 @@ export const MergeSortLinkedListVisualization = () => { const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); - generateSteps(); }; if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); - const renderNode = (nodeId: string, label?: string, isSpecial: boolean = false) => { + const renderNode = (nodeId: string, label?: string) => { const node = currentStep.allNodes[nodeId]; if (!node) return null; @@ -237,21 +377,21 @@ export const MergeSortLinkedListVisualization = () => {
{isDummy ? 'D' : node.val} {isTail && ( -
+
TAIL
)}
{node.nextId && ( -
- +
+
)}
@@ -274,8 +414,6 @@ export const MergeSortLinkedListVisualization = () => { const list1Nodes = getListNodes(currentStep.list1HeadId); const list2Nodes = getListNodes(currentStep.list2HeadId); - - // For merged list, we start from dummy const mergedNodes = currentStep.dummyId ? getListNodes(currentStep.dummyId) : []; return ( @@ -294,12 +432,13 @@ export const MergeSortLinkedListVisualization = () => { />
-
-
+ {/* Left Column: Visual Simulator and Commentary */} +
+
{/* List 1 */}
- Lists + List 1 {currentStep.list1HeadId === null && (null)}
@@ -312,7 +451,7 @@ export const MergeSortLinkedListVisualization = () => { {/* List 2 */}
- List 2 + List 2 {currentStep.list2HeadId === null && (null)}
@@ -323,9 +462,9 @@ export const MergeSortLinkedListVisualization = () => {
{/* Merged List */} -
+
- Merged (via tail) + Merged (via tail) {mergedNodes.length === 0 && (initializing...)}
@@ -336,44 +475,60 @@ export const MergeSortLinkedListVisualization = () => {
- - -

{currentStep.message}

-
+ {/* Commentary Panel */} +
+
+
+
+ + + + + + Algorithm Commentary + +
+
+ Step {currentStepIndex + 1} of {steps.length} +
+
+ +
+
+ +
+
+

+ Current Action +

+ + + {currentStep.message} + + +
+
+
+
-
- - -
-

Algorithm Logic

-
    -
  • - - Use a dummy node to avoid edge cases with the head of the list. -
  • -
  • - - The tail pointer always tracks the end of the newly formed list. -
  • -
  • - - Compare heads of both lists and connect the smaller one to tail.next. -
  • -
  • - - Once one list is empty, attach the remaining nodes of the other list in O(1) time. -
  • -
-
-
+ {/* Right Column: Code & Pseudocode Display */} +
); diff --git a/src/components/visualizations/algorithms/MiddleNodeVisualization.tsx b/src/components/visualizations/algorithms/MiddleNodeVisualization.tsx index 848573e..2cb5d9b 100644 --- a/src/components/visualizations/algorithms/MiddleNodeVisualization.tsx +++ b/src/components/visualizations/algorithms/MiddleNodeVisualization.tsx @@ -1,10 +1,11 @@ import React, { useEffect, useRef, useState } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { ArrowRight, Info, LayoutList, Hash } from 'lucide-react'; -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; import { Button } from '@/components/ui/button'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface ListNodeData { id: string; @@ -18,32 +19,67 @@ interface Step { fastId: string | null; allNodes: Record; message: string; - lineNumber: number; + pseudoStep: string; highlightNodes: string[]; variables: Record; isComplete: boolean; } +// ─── DB Codes (no modification, exact match) ──────────────────────────────── + +const languages: VisualizationLanguageMap = { + typescript: `function middleNode(head: ListNode | null): ListNode | null { + let slow = head; + let fast = head; + while(fast && fast.next){ + slow = slow.next; + fast = fast.next.next; + } + return slow; +}`, + + python: `def middleNode(head): + slow = head + fast = head + while fast and fast.next: + slow = slow.next + fast = fast.next.next + return slow`, + + java: `public static class Solution { + public ListNode middleNode(ListNode head) { + ListNode slow = head; + ListNode fast = head; + while (fast != null && fast.next != null) { + slow = slow.next; + fast = fast.next.next; + } + return slow; + } +}`, + + cpp: `class Solution { +public: + ListNode * middleNode(ListNode * head) { + ListNode * slow = head, * fast = head; + while (fast && fast -> next) { + slow = slow -> next; + fast = fast -> next -> next; + } + return slow; + } +};` +}; + export const MiddleNodeVisualization: React.FC = () => { const [listType, setListType] = useState<'odd' | 'even'>('odd'); const [steps, setSteps] = useState([]); + const [stepLineNumbers, setStepLineNumbers] = useState({}); const [currentStepIndex, setCurrentStepIndex] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const [speed, setSpeed] = useState(1); const intervalRef = useRef(null); - const code = `function middleNode(head: ListNode | null): ListNode | null { - let slow = head; - let fast = head; - - while (fast && fast.next) { - slow = slow!.next; - fast = fast.next.next; - } - - return slow; -}`; - const generateSteps = (type: 'odd' | 'even') => { const vals = type === 'odd' ? [1, 2, 3, 4, 5] : [1, 2, 3, 4, 5, 6]; const allNodes: Record = {}; @@ -59,15 +95,31 @@ export const MiddleNodeVisualization: React.FC = () => { } const newSteps: Step[] = []; + const lines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - const addStep = (msg: string, line: number, slow: string | null, fast: string | null, extra: any = {}) => { + const addStep = ( + msg: string, + pseudo: string, + tsLine: number, + pyLine: number, + javaLine: number, + cppLine: number, + slow: string | null, + fast: string | null, + extra: any = {} + ) => { newSteps.push({ headId, slowId: slow, fastId: fast, allNodes: { ...allNodes }, message: msg, - lineNumber: line, + pseudoStep: pseudo, highlightNodes: extra.highlightNodes || [], variables: { slow: slow ? allNodes[slow].val : 'null', @@ -76,50 +128,92 @@ export const MiddleNodeVisualization: React.FC = () => { }, isComplete: !!extra.isComplete }); + lines.typescript!.push(tsLine); + lines.python!.push(pyLine); + lines.java!.push(javaLine); + lines.cpp!.push(cppLine); }; // Initial State - addStep(`Find the middle of a linked list with ${vals.length} nodes (${type} length)`, 1, null, null); + addStep( + `Find the middle of a linked list with ${vals.length} nodes (${type} length)`, + 'START middleNode(head)', + 1, 1, 2, 3, + null, null + ); // let slow = head; let slowId: string | null = headId; - addStep('Initialize slow pointer at head', 2, slowId, null); + addStep( + 'Initialize slow pointer at head', + 'SET slow = head', + 2, 2, 3, 4, + slowId, null + ); // let fast = head; let fastId: string | null = headId; - addStep('Initialize fast pointer at head', 3, slowId, fastId); + addStep( + 'Initialize fast pointer at head', + 'SET fast = head', + 3, 3, 4, 4, + slowId, fastId + ); // while (fast && fast.next) while (fastId && allNodes[fastId]?.nextId) { - addStep('Check loop condition: fast and fast.next are both not null', 5, slowId, fastId, { - highlightNodes: [fastId, allNodes[fastId].nextId].filter(Boolean) - }); + addStep( + 'Check loop condition: fast and fast.next are both not null', + `WHILE fast AND fast.next → ${allNodes[fastId].val} and ${allNodes[allNodes[fastId].nextId!].val}`, + 4, 4, 5, 5, + slowId, fastId, + { highlightNodes: [fastId, allNodes[fastId].nextId].filter(Boolean) } + ); // slow = slow.next; slowId = allNodes[slowId!].nextId; - addStep('Move slow pointer forward by one node', 6, slowId, fastId, { highlightNodes: [slowId] }); + addStep( + 'Move slow pointer forward by one node', + 'SET slow = slow.next', + 5, 5, 6, 6, + slowId, fastId, + { highlightNodes: [slowId] } + ); // fast = fast.next.next; const nextId = allNodes[fastId!].nextId; fastId = nextId ? allNodes[nextId].nextId : null; - addStep('Move fast pointer forward by two nodes (jump to next.next)', 7, slowId, fastId, { - highlightNodes: [fastId].filter(Boolean) - }); + addStep( + 'Move fast pointer forward by two nodes (jump to next.next)', + 'SET fast = fast.next.next', + 6, 6, 7, 7, + slowId, fastId, + { highlightNodes: [fastId].filter(Boolean) } + ); } // Loop end const endMsg = !fastId ? 'Loop finished: fast pointer reached null' : 'Loop finished: fast.next is null'; - addStep(endMsg, 5, slowId, fastId); + addStep( + endMsg, + 'WHILE loop finished', + 4, 4, 5, 5, + slowId, fastId + ); // return slow; - addStep(`Middle node found! Returning node with value ${allNodes[slowId!].val}.`, 10, slowId, fastId, { - isComplete: true, - highlightNodes: [slowId] - }); + addStep( + `Middle node found! Returning node with value ${allNodes[slowId!].val}.`, + 'RETURN slow', + 8, 7, 9, 9, + slowId, fastId, + { isComplete: true, highlightNodes: [slowId] } + ); setSteps(newSteps); + setStepLineNumbers(lines); setCurrentStepIndex(0); setIsPlaying(false); }; @@ -138,7 +232,7 @@ export const MiddleNodeVisualization: React.FC = () => { } return prev + 1; }); - }, 1000 / speed); + }, 1200 / speed); } else { if (intervalRef.current) clearInterval(intervalRef.current); } @@ -154,12 +248,12 @@ export const MiddleNodeVisualization: React.FC = () => { const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); - generateSteps(listType); }; if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); const getNodes = () => { const nodes: string[] = []; @@ -210,8 +304,9 @@ export const MiddleNodeVisualization: React.FC = () => {
-
-
+ {/* Left Column: Visual simulator, Commentary, and Variables */} +
+
{allNodeIds.map((id, index) => { @@ -233,15 +328,15 @@ export const MiddleNodeVisualization: React.FC = () => { {/* Pointers Container */}
- {isFast && ( + {isFast && ( - FAST + Fast )} {isSlow && ( @@ -250,9 +345,9 @@ export const MiddleNodeVisualization: React.FC = () => { initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }} - className="bg-primary text-primary-foreground text-[10px] px-2 py-0.5 rounded-full font-bold shadow-sm" + className="text-[10px] font-bold text-blue-500" > - SLOW + Slow )} @@ -308,58 +403,62 @@ export const MiddleNodeVisualization: React.FC = () => {
- -
- -
-

{currentStep.message}

-
- - -
- -
- - -
-

- How it Works -

+ {/* Commentary Panel */} +
-
-

The Tortoise and the Hare

-

- This algorithm uses two pointers moving at different speeds. By the time the fast pointer reaches the end, the slow pointer will be exactly at the middle. -

+
+
+ + + + + + Algorithm Commentary + +
+
+ Step {currentStepIndex + 1} of {steps.length} +
-
    -
  • -
    1
    - Slow moves forward one node at a time. -
  • -
  • -
    2
    - Fast moves twice as fast, skipping one node each turn. -
  • -
  • -
    3
    - When Fast hits the end (null) or its next is null, the loop ends. -
  • -
-
-

- * Note: For even-length lists, this implementation returns the second middle node. -

+ +
+
+ +
+
+

+ Current Action +

+ + + {currentStep.message} + + +
+ +
+ + {/* Right Column: Code & Pseudocode Display */} +
); -}; \ No newline at end of file +}; +export default MiddleNodeVisualization; \ No newline at end of file diff --git a/src/components/visualizations/algorithms/MinimumWindowSubstringVisualization.tsx b/src/components/visualizations/algorithms/MinimumWindowSubstringVisualization.tsx index f9bf1cf..0329e4d 100644 --- a/src/components/visualizations/algorithms/MinimumWindowSubstringVisualization.tsx +++ b/src/components/visualizations/algorithms/MinimumWindowSubstringVisualization.tsx @@ -1,9 +1,12 @@ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useMemo } from 'react'; import { Card } from '@/components/ui/card'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; -import { StepControls } from '../shared/StepControls'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import { SimpleStepControls } from '../shared/SimpleStepControls'; +import { VisualizationLayout } from '../shared/VisualizationLayout'; import { Button } from '@/components/ui/button'; +import { motion } from 'framer-motion'; +import type { VisualizationLanguageMap, StepLineNumberMap } from '@/types/visualization'; interface Step { s: string; @@ -16,8 +19,8 @@ interface Step { resLen: number; countT: Record; window: Record; - message: string; - highlightedLines: number[]; + explanation: string; + pseudoStep: string; variables: Record; } @@ -42,197 +45,37 @@ const USE_CASES = [ } ]; -export const MinimumWindowSubstringVisualization = () => { - const [useCaseIdx, setUseCaseIdx] = useState(0); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - - const currentCase = USE_CASES[useCaseIdx]; - - const steps = useMemo(() => { - const s = currentCase.s; - const t = currentCase.t; - const steps: Step[] = []; - - // Signature - steps.push({ - s, t, l: 0, r: -1, have: 0, need: 0, res: [-1, -1], resLen: Infinity, - countT: {}, window: {}, - message: "Initialize the minWindow algorithm search.", - highlightedLines: [1], - variables: { s, t } - }); - - // Line 2: if (t === "") return ""; - steps.push({ - s, t, l: 0, r: -1, have: 0, need: 0, res: [-1, -1], resLen: Infinity, - countT: {}, window: {}, - message: `Check if target string t is empty. t = "${t}"`, - highlightedLines: [2], - variables: { t } - }); - - if (t === "") { - steps.push({ - s, t, l: 0, r: -1, have: 0, need: 0, res: [-1, -1], resLen: Infinity, - countT: {}, window: {}, - message: "Target string is empty, returning empty string.", - highlightedLines: [2], - variables: { return: "" } - }); - return steps; - } - - const countT: Record = {}; - const window: Record = {}; - - // Lines 3-4: Init records - steps.push({ - s, t, l: 0, r: -1, have: 0, need: 0, res: [-1, -1], resLen: Infinity, - countT: { ...countT }, window: { ...window }, - message: "Initialize frequency maps for target characters and current window.", - highlightedLines: [3, 4], - variables: { countT: {}, window: {} } - }); - - // Lines 5-7: Populate countT - for (const c of t) { - countT[c] = (countT[c] || 0) + 1; - steps.push({ - s, t, l: 0, r: -1, have: 0, need: 0, res: [-1, -1], resLen: Infinity, - countT: { ...countT }, window: { ...window }, - message: `Counting character '${c}' in target string t.`, - highlightedLines: [5, 6], - variables: { countT: { ...countT } } - }); - } - - // Lines 8-9: have, need - let have = 0; - const need = Object.keys(countT).length; - steps.push({ - s, t, l: 0, r: -1, have, need, res: [-1, -1], resLen: Infinity, - countT: { ...countT }, window: { ...window }, - message: `Initialize 'have' to 0. Number of unique characters needed: ${need}.`, - highlightedLines: [8, 9], - variables: { have, need } - }); - - // Lines 10-12: res, resLen, l - let res: [number, number] = [-1, -1]; - let resLen = Infinity; - let l = 0; - steps.push({ - s, t, l, r: -1, have, need, res, resLen, - countT: { ...countT }, window: { ...window }, - message: "Initialize tracking variables and the left pointer 'l'.", - highlightedLines: [10, 11, 12], - variables: { res, resLen, l } - }); - - // Loop - for (let r = 0; r < s.length; r++) { - const c = s[r]; - window[c] = (window[c] || 0) + 1; - - steps.push({ - s, t, l, r, have, need, res, resLen, - countT: { ...countT }, window: { ...window }, - message: `Expand window by moving right pointer to index ${r} ('${c}').`, - highlightedLines: [13, 14, 15], - variables: { r, c, "window[c]": window[c] } - }); - - if (c in countT && window[c] === countT[c]) { - have++; - steps.push({ - s, t, l, r, have, need, res, resLen, - countT: { ...countT }, window: { ...window }, - message: `Character '${c}' count matches target frequency! Increment 'have'.`, - highlightedLines: [16, 17], - variables: { have, need } - }); - } - - while (have === need) { - steps.push({ - s, t, l, r, have, need, res, resLen, - countT: { ...countT }, window: { ...window }, - message: `All required characters found (${have}/${need}). Checking if current window is smaller.`, - highlightedLines: [19], - variables: { have, need, currentSize: r - l + 1, resLen } - }); - - if ((r - l + 1) < resLen) { - res = [l, r]; - resLen = r - l + 1; - steps.push({ - s, t, l, r, have, need, res, resLen, - countT: { ...countT }, window: { ...window }, - message: `Found smaller window! New minimum length: ${resLen}. Substring: "${s.slice(l, r + 1)}"`, - highlightedLines: [20, 21, 22], - variables: { res, resLen } - }); - } - - const leftChar = s[l]; - window[leftChar]--; - - steps.push({ - s, t, l, r, have, need, res, resLen, - countT: { ...countT }, window: { ...window }, - message: `Shrinking window by removing '${leftChar}' from index ${l}.`, - highlightedLines: [24, 25], - variables: { l, leftChar, "window[leftChar]": window[leftChar] } - }); - - if (leftChar in countT && window[leftChar] < countT[leftChar]) { - have--; - steps.push({ - s, t, l, r, have, need, res, resLen, - countT: { ...countT }, window: { ...window }, - message: `Window no longer contains enough '${leftChar}'. Decrement 'have'.`, - highlightedLines: [26, 27], - variables: { have, need } - }); - } - - l++; - steps.push({ - s, t, l, r, have, need, res, resLen, - countT: { ...countT }, window: { ...window }, - message: "Moving left pointer to search for a potentially smaller valid window.", - highlightedLines: [29], - variables: { l } - }); - } - } - - // Final Lines - steps.push({ - s, t, l, r: s.length - 1, have, need, res, resLen, - countT: { ...countT }, window: { ...window }, - message: "Finished iterating through the string. Preparing final result.", - highlightedLines: [32], - variables: { res } - }); - - const resultStr = resLen !== Infinity ? s.slice(res[0], res[1] + 1) : ""; - steps.push({ - s, t, l, r: s.length - 1, have, need, res, resLen, - countT: { ...countT }, window: { ...window }, - message: `Final result: "${resultStr}"`, - highlightedLines: [33], - variables: { return: resultStr } - }); - - return steps; - }, [currentCase]); - - const currentStep = steps[currentStepIndex] || steps[steps.length - 1]; - - const code = `function minWindow(s: string, t: string): string { +const languages: VisualizationLanguageMap = { + python: `def minWindow(s: str, t: str) -> str: + if not t: + return "" + countT = {} + window = {} + for c in t: + countT[c] = countT.get(c, 0) + 1 + have = 0 + need = len(countT) + res = [-1, -1] + resLen = float('inf') + l = 0 + for r in range(len(s)): + c = s[r] + window[c] = window.get(c, 0) + 1 + if c in countT and window[c] == countT[c]: + have += 1 + while have == need: + if (r - l + 1) < resLen: + res = [l, r] + resLen = r - l + 1 + leftChar = s[l] + window[leftChar] -= 1 + if leftChar in countT and window[leftChar] < countT[leftChar]: + have -= 1 + l += 1 + start, end = res + return s[start:end+1] if resLen != float('inf') else ""`, + + typescript: `function minWindow(s: string, t: string): string { if (t === "") return ""; const countT: Record = {}; const window: Record = {}; @@ -265,28 +108,221 @@ export const MinimumWindowSubstringVisualization = () => { } const [start, end] = res; return resLen !== Infinity ? s.slice(start, end + 1) : ""; -}`; - - useEffect(() => { - let timer: NodeJS.Timeout; - if (isPlaying && currentStepIndex < steps.length - 1) { - timer = setTimeout(() => { - setCurrentStepIndex(prev => prev + 1); - }, 1000 / speed); - } else { - setIsPlaying(false); +}`, + + java: `public class Solution { + public String minWindow(String s, String t) { + if (t.equals("")) return ""; + java.util.Map countT = new java.util.HashMap<>(); + java.util.Map window = new java.util.HashMap<>(); + for (char c : t.toCharArray()) { + countT.put(c, countT.getOrDefault(c, 0) + 1); + } + int have = 0; + int need = countT.size(); + int[] res = {-1, -1}; + int resLen = Integer.MAX_VALUE; + int l = 0; + for (int r = 0; r < s.length(); r++) { + char c = s.charAt(r); + window.put(c, window.getOrDefault(c, 0) + 1); + if (countT.containsKey(c) && window.get(c).equals(countT.get(c))) { + have++; + } + while (have == need) { + if ((r - l + 1) < resLen) { + res[0] = l; + res[1] = r; + resLen = r - l + 1; + } + char leftChar = s.charAt(l); + window.put(leftChar, window.get(leftChar) - 1); + if (countT.containsKey(leftChar) && window.get(leftChar) < countT.get(leftChar)) { + have--; + } + l++; + } + } + int start = res[0]; + int end = res[1]; + return resLen != Integer.MAX_VALUE ? s.substring(start, end + 1) : ""; + } +}`, + + cpp: `class Solution { +public: + string minWindow(string s, string t) { + if (s.empty() || t.empty()) return ""; + unordered_map need, window; + for (char c : t) { + need[c]++; + } + int required = need.size(); + int formed = 0; + int left = 0; + int minLen = INT_MAX; + int minLeft = 0; + for (int right = 0; right < s.length(); right++) { + char c = s[right]; + window[c]++; + if (need.find(c) != need.end() && window[c] == need[c]) { + formed++; + } + while (left <= right && formed == required) { + if (right - left + 1 < minLen) { + minLen = right - left + 1; + minLeft = left; + } + char leftChar = s[left]; + window[leftChar]--; + if (need.find(leftChar) != need.end() && window[leftChar] < need[leftChar]) { + formed--; + } + left++; + } + } + return minLen == INT_MAX ? "" : s.substr(minLeft, minLen); + } +};` +}; + +export const MinimumWindowSubstringVisualization = () => { + const [useCaseIdx, setUseCaseIdx] = useState(0); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + + const currentCase = USE_CASES[useCaseIdx]; + + const { steps, stepLineNumbers } = useMemo(() => { + const s = currentCase.s; + const t = currentCase.t; + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + const addStep = (msg: string, pseudo: string, tsLine: number, pyLine: number, javaLine: number, cppLine: number, extra: Partial = {}) => { + steps.push({ + s, t, + l: extra.hasOwnProperty('l') ? extra.l! : l, + r: extra.hasOwnProperty('r') ? extra.r! : r, + have: extra.hasOwnProperty('have') ? extra.have! : have, + need: extra.hasOwnProperty('need') ? extra.need! : need, + res: extra.res || res, + resLen: extra.hasOwnProperty('resLen') ? extra.resLen! : resLen, + countT: extra.countT || { ...countT }, + window: extra.window || { ...window }, + explanation: msg, + pseudoStep: pseudo, + variables: { + s, t, + l: extra.hasOwnProperty('l') ? extra.l! : l, + r: extra.hasOwnProperty('r') ? extra.r! : r, + have: extra.hasOwnProperty('have') ? extra.have! : have, + need: extra.hasOwnProperty('need') ? extra.need! : need, + resLen: extra.hasOwnProperty('resLen') ? (extra.resLen === Infinity ? "Infinity" : extra.resLen!) : (resLen === Infinity ? "Infinity" : resLen) + } + }); + addLines(tsLine, pyLine, javaLine, cppLine); + }; + + let have = 0; + let need = 0; + let l = 0; + let r = -1; + let res: [number, number] = [-1, -1]; + let resLen = Infinity; + const countT: Record = {}; + const window: Record = {}; + + // 1. Signature + addStep("Initialize the minWindow algorithm search.", "CALL minWindow(s, t)", 1, 1, 2, 3); + + // 2. Empty check + addStep(`Check if target string t is empty. t = "${t}"`, "IF t == \"\" -> RETURN \"\"", 2, 2, 3, 4); + + if (t !== "") { + // 3. Init structures + addStep("Initialize frequency maps countT and window.", "SET countT = {}, window = {}", 3, 4, 4, 5); + + // 4. Count target characters + for (const c of t) { + countT[c] = (countT[c] || 0) + 1; + addStep(`Count character '${c}' in target string t.`, `SET countT[${c}] = countT[${c}] + 1`, 5, 6, 6, 6); + } + + need = Object.keys(countT).length; + // 5. Initialize have/need + addStep(`Initialize 'have' to 0. Unique characters to match: ${need}.`, `SET have = 0, need = ${need}`, 8, 8, 9, 9); + + // 6. Initialize pointers + addStep("Initialize window result trackers and left pointer l = 0.", "SET res = [-1, -1], resLen = Infinity, l = 0", 10, 10, 11, 11); + + // Loop + for (r = 0; r < s.length; r++) { + const c = s[r]; + window[c] = (window[c] || 0) + 1; + addStep(`Expand sliding window: move right pointer to index ${r} ('${c}').`, `FOR r = ${r} TO len(s) - 1`, 13, 13, 14, 14); + + if (c in countT && window[c] === countT[c]) { + have++; + addStep(`Frequency of '${c}' in window meets target. Increment 'have' to ${have}.`, `SET have = have + 1`, 16, 16, 17, 17); + } + + while (have === need) { + addStep(`All character requirements met (${have}/${need}). Check window validity.`, "WHILE have == need", 19, 18, 20, 20); + + if ((r - l + 1) < resLen) { + res = [l, r]; + resLen = r - l + 1; + addStep(`Current window [${l}, ${r}] ("${s.slice(l, r + 1)}") is smaller than best seen. Update min.`, `SET res = [l, r], resLen = r - l + 1 → ${resLen}`, 20, 19, 21, 21); + } + + const leftChar = s[l]; + window[leftChar]--; + addStep(`Shrink window from left. Decrement count of '${leftChar}'.`, `SET window[${leftChar}] = window[${leftChar}] - 1`, 24, 22, 26, 25); + + if (leftChar in countT && window[leftChar] < countT[leftChar]) { + have--; + addStep(`Frequency of '${leftChar}' falls below target. Decrement 'have' to ${have}.`, `SET have = have - 1`, 26, 24, 28, 27); + } + + l++; + addStep(`Advance left pointer l to index ${l}.`, "SET l = l + 1", 29, 26, 31, 30); + } + // loop check false + addStep(`Window [${l}, ${r}] lacks required characters (${have}/${need}). Expand window next.`, "WHILE have == need → FALSE ✗", 19, 18, 20, 20); + } + + // 7. Complete final check + addStep("All elements processed. Prepare final minimum substring result.", "RETURN", 32, 27, 34, 33); } - return () => clearTimeout(timer); - }, [isPlaying, currentStepIndex, steps.length, speed]); + + const resultStr = resLen !== Infinity ? s.slice(res[0], res[1] + 1) : ""; + addStep(`Final result substring is "${resultStr}".`, `RETURN result → "${resultStr}"`, 33, 28, 36, 33, { r: s.length - 1 }); + + return { steps, stepLineNumbers }; + }, [currentCase]); + + const currentStep = steps[currentStepIndex] || steps[steps.length - 1]; + const pseudoSteps = steps.map(s => s.pseudoStep); const handleUseCaseChange = (idx: number) => { setUseCaseIdx(idx); setCurrentStepIndex(0); - setIsPlaying(false); }; const getCharStyle = (idx: number) => { - const isCurrentR = idx === currentStep.variables.r; + const isCurrentR = idx === currentStep.r; const isCurrentL = idx === currentStep.l; const isInWindow = idx >= currentStep.l && idx <= currentStep.r && currentStep.r !== -1; const isInResult = currentStep.res[0] !== -1 && idx >= currentStep.res[0] && idx <= currentStep.res[1]; @@ -305,7 +341,8 @@ export const MinimumWindowSubstringVisualization = () => { return (
-
+ {/* Controls / Case selection */} +
{USE_CASES.map((uc, idx) => ( ))}
-
- + setCurrentStepIndex(prev => Math.min(steps.length - 1, prev + 1))} - onStepBack={() => setCurrentStepIndex(prev => Math.max(0, prev - 1))} - isPlaying={isPlaying} - onPlay={() => setIsPlaying(true)} - onPause={() => setIsPlaying(false)} - onReset={() => { - setCurrentStepIndex(0); - setIsPlaying(false); - }} - speed={speed} - onSpeedChange={setSpeed} + totalSteps={steps.length} + onStepChange={setCurrentStepIndex} />
- + {/* Left column visual representation */} +
Target Characters (t) @@ -360,7 +388,7 @@ export const MinimumWindowSubstringVisualization = () => { Source String (s)
L: {currentStep.l} - R: {currentStep.variables.r !== undefined ? currentStep.variables.r : '-'} + R: {currentStep.r !== -1 ? currentStep.r : '-'}
@@ -408,27 +436,28 @@ export const MinimumWindowSubstringVisualization = () => {
-
- -
-
+ {/* Descriptive Commentary Box (at the bottom) */} +
+
- Commentary + Process Step
-

- {currentStep.message} -

+ {currentStep.explanation}
+ {/* Variable Panel (below the commentary box) */}
- setCurrentStepIndex(0)} />
diff --git a/src/components/visualizations/algorithms/MissingNumberVisualization.tsx b/src/components/visualizations/algorithms/MissingNumberVisualization.tsx index 03ad55b..41d2c3d 100644 --- a/src/components/visualizations/algorithms/MissingNumberVisualization.tsx +++ b/src/components/visualizations/algorithms/MissingNumberVisualization.tsx @@ -1,187 +1,204 @@ -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import { Card } from '@/components/ui/card'; -import { StepControls } from '../shared/StepControls'; +import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import { VisualizationLayout } from '../shared/VisualizationLayout'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; -export const MissingNumberVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); +interface Step { + array: number[]; + highlighting: number[]; + variables: Record; + explanation: string; + pseudoStep: string; + calc?: string; +} +const languages: VisualizationLanguageMap = { + typescript: `function missingNumber(nums: number[]): number { + let res = nums.length; + for (let i = 0; i < nums.length; i++) { + res += i - nums[i]; + } + return res; +}`, + python: `def missingNumber(nums) -> int: + res = len(nums) + for i in range(len(nums)): + res += (i - nums[i]) + return res`, + java: `public static class Solution { + public int missingNumber(int[] nums) { + int res = nums.length; + for (int i = 0; i < nums.length; i++) { + res += i - nums[i]; + } + return res; + } +}`, + cpp: `class Solution { +public: + int missingNumber(vector& nums) { + int res = nums.size(); + for (int i = 0; i < nums.size(); i++) { + res += i - nums[i]; + } + return res; + } +};` +}; + +export const MissingNumberVisualization = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); const nums = [3, 0, 1]; - const steps = [ - { + const { steps, stepLineNumbers } = useMemo(() => { + const s: Step[] = []; + const lines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + lines.typescript!.push(ts); + lines.python!.push(py); + lines.java!.push(java); + lines.cpp!.push(cpp); + }; + + s.push({ array: nums, highlighting: [], variables: { res: 3, n: 3 }, explanation: "Initialize res = n = 3. This accounts for the missing index n in the 0..n range.", - highlightedLine: 2 - }, - { - array: nums, - highlighting: [], - variables: { res: 3, i: 0, 'nums[0]': 3 }, - explanation: "Start loop at i = 0. We will compute diff = i - nums[i].", - highlightedLine: 3 - }, - { - array: nums, - highlighting: [0], - variables: { res: 3, i: 0, 'nums[0]': 3, diff: -3 }, - explanation: "Calculate difference: i - nums[i] = 0 - 3 = -3.", - highlightedLine: 4, - calc: '0 - 3 = -3' - }, - { - array: nums, - highlighting: [0], - variables: { res: 0, i: 0, 'nums[i]': 3, prevRes: 3 }, - explanation: "Update res: res += -3 → 3 + (-3) = 0.", - highlightedLine: 4, - calc: '3 + (-3) = 0' - }, - { - array: nums, - highlighting: [], - variables: { res: 0, i: 1, 'nums[1]': 0 }, - explanation: "Loop: i = 1. We will compute diff = i - nums[i].", - highlightedLine: 3 - }, - { - array: nums, - highlighting: [1], - variables: { res: 0, i: 1, 'nums[1]': 0, diff: 1 }, - explanation: "Calculate difference: i - nums[i] = 1 - 0 = 1.", - highlightedLine: 4, - calc: '1 - 0 = 1' - }, - { - array: nums, - highlighting: [1], - variables: { res: 1, i: 1, 'nums[i]': 0, prevRes: 0 }, - explanation: "Update res: res += 1 → 0 + 1 = 1.", - highlightedLine: 4, - calc: '0 + 1 = 1' - }, - { - array: nums, - highlighting: [], - variables: { res: 1, i: 2, 'nums[2]': 1 }, - explanation: "Loop: i = 2. We will compute diff = i - nums[i].", - highlightedLine: 3 - }, - { - array: nums, - highlighting: [2], - variables: { res: 1, i: 2, 'nums[2]': 1, diff: 1 }, - explanation: "Calculate difference: i - nums[i] = 2 - 1 = 1.", - highlightedLine: 4, - calc: '2 - 1 = 1' - }, - { - array: nums, - highlighting: [2], - variables: { res: 2, i: 2, 'nums[i]': 1, prevRes: 1 }, - explanation: "Update res: res += 1 → 1 + 1 = 2.", - highlightedLine: 4, - calc: '1 + 1 = 2' - }, - { - array: nums, - highlighting: [], - variables: { res: 2 }, - explanation: "Loop finished. The accumulated result is the missing number: 2.", - highlightedLine: 6, - calc: 'Result: 2' - } - ]; + pseudoStep: "SET res = nums.length (res = 3)", + calc: "res = 3" + }); + addLines(2, 2, 3, 4); - const code = `function missingNumber(nums: number[]): number { - let res = nums.length; for (let i = 0; i < nums.length; i++) { - res += i - nums[i]; + s.push({ + array: nums, + highlighting: [], + variables: { res: s[s.length - 1].variables.res, i, 'nums[i]': nums[i] }, + explanation: `Start loop at index i = ${i}. We will compute the difference: i - nums[i].`, + pseudoStep: `FOR i = 0 TO n-1 (i = ${i})`, + }); + addLines(3, 3, 4, 5); + + const diff = i - nums[i]; + s.push({ + array: nums, + highlighting: [i], + variables: { res: s[s.length - 1].variables.res, i, 'nums[i]': nums[i], diff }, + explanation: `Calculate difference: i - nums[i] = ${i} - ${nums[i]} = ${diff}.`, + pseudoStep: `SET diff = i - nums[i] (${i} - ${nums[i]} = ${diff})`, + calc: `${i} - ${nums[i]} = ${diff}` + }); + addLines(4, 4, 5, 6); + + const prevRes = s[s.length - 2].variables.res; + const nextRes = prevRes + diff; + s.push({ + array: nums, + highlighting: [i], + variables: { res: nextRes, i, 'nums[i]': nums[i], prevRes }, + explanation: `Update res by adding difference: res += diff → ${prevRes} + (${diff}) = ${nextRes}.`, + pseudoStep: `SET res = res + diff (res = ${prevRes} + (${diff}) = ${nextRes})`, + calc: `${prevRes} + (${diff}) = ${nextRes}` + }); + addLines(4, 4, 5, 6); } - return res; -}`; - const step = steps[currentStep]; + const finalRes = s[s.length - 1].variables.res; + s.push({ + array: nums, + highlighting: [], + variables: { res: finalRes }, + explanation: `Loop finished. The accumulated result is the missing number: ${finalRes}.`, + pseudoStep: `RETURN res (res = ${finalRes})`, + calc: `Result = ${finalRes}` + }); + addLines(6, 5, 7, 8); + + return { steps: s, stepLineNumbers: lines }; + }, []); + + const step = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( -
- {}} - onPause={() => {}} - onStepForward={() => currentStep < steps.length - 1 && setCurrentStep(prev => prev + 1)} - onStepBack={() => currentStep > 0 && setCurrentStep(prev => prev - 1)} - onReset={() => setCurrentStep(0)} - speed={1} - onSpeedChange={() => {}} - currentStep={currentStep} - totalSteps={steps.length - 1} - /> - -
-
- -

Input Array (nums)

-
- {step.array.map((value, index) => ( -
-
- {value} + +
+ +

Input Array (nums)

+
+ {step.array.map((value, index) => ( +
+
+ {value} +
+ i = {index} +
+ ))} +
+
+ n = {nums.length}
- i = {index} -
- ))} -
-
- n = {nums.length}
-
-
+ - {step.calc && ( - -

Calculation

-

{step.calc}

+ {step.calc && ( + +

Calculation

+

{step.calc}

+
+ )} +
+ +
+ +

Step Explanation

+

{step.explanation}

- )} - -
-
Explanation:
-
- {step.explanation} -
-
-
- - - - -

Why this works?

-
-

Consider the sum of indices [0...n] and sum of values in array.

-

Missing Number = Sum(0...n) - Sum(nums)

-

This approach computes this difference incrementally to avoid separate loops or potential overflow (though less of an issue here than complex multiplication).

-

res ends up being accumulating `n + (0-nums[0]) + (1-nums[1]) ...` which effectively re-arranges to `(n + 0 + 1 + ... ) - (nums[0] + nums[1] + ...)`.

-
-
-
+ - +

Why this works

+

Consider the sum of indices [0...n] and sum of values in array. Missing Number = Sum(0...n) - Sum(nums).

+

This approach computes this difference incrementally: `res` accumulates `n + (0-nums[0]) + (1-nums[1]) + ...` which re-arranges to `(n + 0 + 1 + ... ) - (nums[0] + nums[1] + ...)`, avoiding overflow.

+ +
+
+ } + rightContent={ + setCurrentStepIndex(0)} + /> + } + controls={ + -
-
+ } + /> ); }; \ No newline at end of file diff --git a/src/components/visualizations/algorithms/ModularExponentiationVisualization.tsx b/src/components/visualizations/algorithms/ModularExponentiationVisualization.tsx index 9144b26..8201ba9 100644 --- a/src/components/visualizations/algorithms/ModularExponentiationVisualization.tsx +++ b/src/components/visualizations/algorithms/ModularExponentiationVisualization.tsx @@ -1,232 +1,311 @@ import { useState, useMemo } from 'react'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; import { Card } from '@/components/ui/card'; import { motion } from 'framer-motion'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { - base: number; - exponent: number; - modulus: number; - result: number; - currentBase: number; - currentExponent: number; - explanation: string; - highlightedLines: number[]; - variables: Record; + base: number; + exponent: number; + modulus: number; + result: number; + currentBase: number; + currentExponent: number; + explanation: string; + pseudoStep: string; + variables: Record; } -export const ModularExponentiationVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); - const initialBase = 3; - const initialExponent = 13; - const initialModulus = 7; - - const steps: Step[] = useMemo(() => { - const s: Step[] = []; - let base = initialBase; - let exponent = initialExponent; - let modulus = initialModulus; - let result = 1; - - s.push({ - base: initialBase, exponent: initialExponent, modulus: initialModulus, - result: 1, currentBase: base, currentExponent: exponent, - explanation: `Starting modularExponentiation with base=${base}, exponent=${exponent}, modulus=${modulus}.`, - highlightedLines: [1], - variables: { base, exponent, modulus } - }); - - if (modulus === 1) { - s.push({ - base: initialBase, exponent: initialExponent, modulus: initialModulus, - result: 1, currentBase: base, currentExponent: exponent, - explanation: `Modulus is 1, return 0.`, - highlightedLines: [2], - variables: { modulus } - }); - return s; +const languages: VisualizationLanguageMap = { + typescript: `function modularExponentiation(base: number, exponent: number, modulus: number): number { + if (modulus === 1) return 0; + let result: number = 1; + base = base % modulus; + while (exponent > 0) { + if (exponent % 2 === 1) { + result = (result * base) % modulus; + } + exponent = Math.floor(exponent / 2); + base = (base * base) % modulus; + } + return result; +}`, + python: `def modular_exponentiation(base: int, exponent: int, modulus: int) -> int: + result = 1 + base %= modulus + while exponent > 0: + if exponent % 2 == 1: + result = (result * base) % modulus + base = (base * base) % modulus + exponent //= 2 + return result`, + java: `static long modularExponentiation(long base, long exponent, long modulus) { + long result = 1; + base = base % modulus; + while (exponent > 0) { + if (exponent % 2 == 1) { + result = (result * base) % modulus; } - - result = 1; - s.push({ - base: initialBase, exponent: initialExponent, modulus: initialModulus, - result, currentBase: base, currentExponent: exponent, - explanation: `Initializing result = 1.`, - highlightedLines: [3], - variables: { result } - }); - - const oldBase = base; + base = (base * base) % modulus; + exponent = exponent / 2; + } + return result; +}`, + cpp: `class Solution { +public: + long long modularExponentiation(long long base, long long exponent, long long modulus) { + long long result = 1; base = base % modulus; - s.push({ - base: initialBase, exponent: initialExponent, modulus: initialModulus, - result, currentBase: base, currentExponent: exponent, - explanation: `Reducing base modulo modulus: ${oldBase} % ${modulus} = ${base}.`, - highlightedLines: [4], - variables: { base, modulus } - }); - while (exponent > 0) { - s.push({ - base: initialBase, exponent: initialExponent, modulus: initialModulus, - result, currentBase: base, currentExponent: exponent, - explanation: `Checking loop condition: exponent (${exponent}) > 0.`, - highlightedLines: [5], - variables: { exponent } - }); - - s.push({ - base: initialBase, exponent: initialExponent, modulus: initialModulus, - result, currentBase: base, currentExponent: exponent, - explanation: `Checking if exponent is odd: ${exponent} % 2 === ${exponent % 2}.`, - highlightedLines: [6], - variables: { exponent, "exponent % 2": exponent % 2 } - }); - - if (exponent % 2 === 1) { - const oldResult = result; + if (exponent % 2 == 1) { result = (result * base) % modulus; - s.push({ - base: initialBase, exponent: initialExponent, modulus: initialModulus, - result, currentBase: base, currentExponent: exponent, - explanation: `Exponent is odd. Updating result: (${oldResult} * ${base}) % ${modulus} = ${result}.`, - highlightedLines: [7], - variables: { result, base, modulus } - }); } - - const oldExponent = exponent; - exponent = Math.floor(exponent / 2); - s.push({ - base: initialBase, exponent: initialExponent, modulus: initialModulus, - result, currentBase: base, currentExponent: exponent, - explanation: `Halving exponent: floor(${oldExponent} / 2) = ${exponent}.`, - highlightedLines: [9], - variables: { exponent } - }); - - const oldBaseLoop = base; + exponent = exponent >> 1; base = (base * base) % modulus; - s.push({ - base: initialBase, exponent: initialExponent, modulus: initialModulus, - result, currentBase: base, currentExponent: exponent, - explanation: `Squaring base modulo modulus: (${oldBaseLoop} * ${oldBaseLoop}) % ${modulus} = ${base}.`, - highlightedLines: [10], - variables: { base, modulus } - }); } + return result; + } +};` +}; + +export const ModularExponentiationVisualization = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const initialBase = 3; + const initialExponent = 13; + const initialModulus = 7; + + const { steps, stepLineNumbers } = useMemo(() => { + const s: Step[] = []; + const lines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + lines.typescript!.push(ts); + lines.python!.push(py); + lines.java!.push(java); + lines.cpp!.push(cpp); + }; + + let base = initialBase; + let exponent = initialExponent; + let modulus = initialModulus; + let result = 1; + + s.push({ + base: initialBase, exponent: initialExponent, modulus: initialModulus, + result: 1, currentBase: base, currentExponent: exponent, + explanation: `Starting modularExponentiation with base=${base}, exponent=${exponent}, modulus=${modulus}.`, + pseudoStep: `CALL modularExponentiation(base = ${base}, exp = ${exponent}, mod = ${modulus})`, + variables: { base, exponent, modulus } + }); + addLines(1, 1, 1, 3); + if (modulus === 1) { + s.push({ + base: initialBase, exponent: initialExponent, modulus: initialModulus, + result: 1, currentBase: base, currentExponent: exponent, + explanation: `Modulus is 1, return 0.`, + pseudoStep: `IF modulus == 1 → RETURN 0`, + variables: { modulus } + }); + addLines(2, 1, 1, 3); + return { steps: s, stepLineNumbers: lines }; + } + + result = 1; + s.push({ + base: initialBase, exponent: initialExponent, modulus: initialModulus, + result, currentBase: base, currentExponent: exponent, + explanation: `Initializing result = 1.`, + pseudoStep: `SET result = 1`, + variables: { result } + }); + addLines(3, 2, 2, 4); + + const oldBase = base; + base = base % modulus; + s.push({ + base: initialBase, exponent: initialExponent, modulus: initialModulus, + result, currentBase: base, currentExponent: exponent, + explanation: `Reducing base modulo modulus: ${oldBase} % ${modulus} = ${base}.`, + pseudoStep: `SET base = base % modulus (${oldBase} % ${modulus} = ${base})`, + variables: { base, modulus } + }); + addLines(4, 3, 3, 5); + + while (exponent > 0) { + s.push({ + base: initialBase, exponent: initialExponent, modulus: initialModulus, + result, currentBase: base, currentExponent: exponent, + explanation: `Checking loop condition: exponent (${exponent}) > 0.`, + pseudoStep: `WHILE exponent > 0 (${exponent} > 0) → YES ✓`, + variables: { exponent } + }); + addLines(5, 4, 4, 6); + + s.push({ + base: initialBase, exponent: initialExponent, modulus: initialModulus, + result, currentBase: base, currentExponent: exponent, + explanation: `Checking if exponent is odd: ${exponent} % 2 === ${exponent % 2}.`, + pseudoStep: `IF exponent % 2 == 1 (${exponent} % 2 == ${exponent % 2}) → ${exponent % 2 === 1 ? 'YES ✓' : 'NO ✗'}`, + variables: { exponent, "exponent % 2": exponent % 2 } + }); + addLines(6, 5, 5, 7); + + if (exponent % 2 === 1) { + const oldResult = result; + result = (result * base) % modulus; s.push({ - base: initialBase, exponent: initialExponent, modulus: initialModulus, - result, currentBase: base, currentExponent: exponent, - explanation: `Exponent is 0. Loop finished. Returning result = ${result}.`, - highlightedLines: [5, 12], - variables: { result } + base: initialBase, exponent: initialExponent, modulus: initialModulus, + result, currentBase: base, currentExponent: exponent, + explanation: `Exponent is odd. Updating result: (${oldResult} * ${base}) % ${modulus} = ${result}.`, + pseudoStep: `SET result = (result * base) % modulus (result = (${oldResult} * ${base}) % ${modulus} = ${result})`, + variables: { result, base, modulus } }); + addLines(7, 6, 6, 8); + } - return s; - }, [initialBase, initialExponent, initialModulus]); + const oldExponent = exponent; + exponent = Math.floor(exponent / 2); + s.push({ + base: initialBase, exponent: initialExponent, modulus: initialModulus, + result, currentBase: base, currentExponent: exponent, + explanation: `Halving exponent: floor(${oldExponent} / 2) = ${exponent}.`, + pseudoStep: `SET exponent = exponent / 2 (${oldExponent} / 2 = ${exponent})`, + variables: { exponent } + }); + addLines(9, 8, 9, 10); - const code = `function modularExponentiation(base: number, exponent: number, modulus: number): number { - if (modulus === 1) return 0; - let result: number = 1; - base = base % modulus; - while (exponent > 0) { - if (exponent % 2 === 1) { - result = (result * base) % modulus; + const oldBaseLoop = base; + base = (base * base) % modulus; + s.push({ + base: initialBase, exponent: initialExponent, modulus: initialModulus, + result, currentBase: base, currentExponent: exponent, + explanation: `Squaring base modulo modulus: (${oldBaseLoop} * ${oldBaseLoop}) % ${modulus} = ${base}.`, + pseudoStep: `SET base = (base * base) % modulus (base = (${oldBaseLoop}^2) % ${modulus} = ${base})`, + variables: { base, modulus } + }); + addLines(10, 7, 8, 11); } - exponent = Math.floor(exponent / 2); - base = (base * base) % modulus; - } - return result; -}`; - - const step = steps[currentStep]; - - return ( - - -

Calculation State

-
-
- Result - - {step.result} - -
-
- Exponent - - {step.currentExponent} - -
-
- Base - - {step.currentBase} - -
-
- Modulus - {step.modulus} -
-
- -
-
- - - -
-
Current Formula
-
- {step.result} × {step.currentBase}{step.currentExponent} mod {step.modulus} -
-
-
- - -

Step Explanation

-

{step.explanation}

-
- - + + s.push({ + base: initialBase, exponent: initialExponent, modulus: initialModulus, + result, currentBase: base, currentExponent: exponent, + explanation: `Exponent is 0. Loop finished. Returning result = ${result}.`, + pseudoStep: `WHILE exponent > 0 (0 > 0) → NO ✗`, + variables: { result } + }); + addLines(5, 4, 4, 6); + + s.push({ + base: initialBase, exponent: initialExponent, modulus: initialModulus, + result, currentBase: base, currentExponent: exponent, + explanation: `Return final result = ${result}.`, + pseudoStep: `RETURN result (result = ${result})`, + variables: { result } + }); + addLines(12, 9, 11, 13); + + return { steps: s, stepLineNumbers: lines }; + }, [initialBase, initialExponent, initialModulus]); + + const step = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); + + return ( + +
+ +

Calculation State

+
+
+ Result + + {step.result} +
- } - rightContent={ - - } - controls={ - - } +
+ Exponent + + {step.currentExponent} + +
+
+ Base + + {step.currentBase} + +
+
+ Modulus + {step.modulus} +
+
+ +
+
+ + + +
+
Current Formula State
+
+ {step.result} × {step.currentBase}{step.currentExponent} mod {step.modulus} +
+
+
+
+ +
+ +

Step Explanation

+

{step.explanation}

+
+ + +
+
+ } + rightContent={ + setCurrentStepIndex(0)} + /> + } + controls={ + - ); + } + /> + ); }; diff --git a/src/components/visualizations/algorithms/MonotonicStackVisualization.tsx b/src/components/visualizations/algorithms/MonotonicStackVisualization.tsx index 956bc98..ea35e45 100644 --- a/src/components/visualizations/algorithms/MonotonicStackVisualization.tsx +++ b/src/components/visualizations/algorithms/MonotonicStackVisualization.tsx @@ -1,10 +1,9 @@ -import { useState, useMemo } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import { SimpleStepControls } from '../shared/SimpleStepControls'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; -import { VisualizationLayout } from '../shared/VisualizationLayout'; -import { Card } from '@/components/ui/card'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { heights: number[]; @@ -16,181 +15,344 @@ interface Step { maxArea: number; activeRange: [number, number] | null; explanation: string; - highlightedLines: number[]; + pseudoStep: string; variables: Record; } -export const MonotonicStackVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); - const heights = [2, 1, 5, 6, 2, 3]; +// ─── Hardcoded code per language (no comments) ────────────────────────────── - const steps = useMemo(() => { - const s: Step[] = []; - const stack: number[] = []; +const languages: VisualizationLanguageMap = { + typescript: `function largestRectangleArea(heights: number[]): number { let maxArea = 0; + const stack: number[] = []; + for (let i = 0; i <= heights.length; i++) { + while ( + stack.length > 0 && + (i === heights.length || heights[stack[stack.length - 1]] >= heights[i]) + ) { + const top = stack.pop()!; + const width = + stack.length === 0 + ? i + : i - stack[stack.length - 1] - 1; + const area = heights[top] * width; + maxArea = Math.max(maxArea, area); + } + stack.push(i); + } + return maxArea; +}`, + + python: `def largestRectangleArea(heights): + stack = [] + max_area = 0 + n = len(heights) + for i in range(n + 1): + while stack and (i == n or heights[stack[-1]] >= heights[i]): + top_index = stack.pop() + height = heights[top_index] + width = i if not stack else i - stack[-1] - 1 + area = height * width + max_area = max(max_area, area) + stack.append(i) + return max_area`, + + java: `public int largestRectangleArea(int[] heights) { + int maxArea = 0; + Stack stack = new Stack<>(); + for (int i = 0; i <= heights.length; i++) { + while (!stack.isEmpty() && + (i == heights.length || heights[stack.peek()] >= heights[i])) { + int top = stack.pop(); + int width = stack.isEmpty() ? i : i - stack.peek() - 1; + int area = heights[top] * width; + maxArea = Math.max(maxArea, area); + } + stack.push(i); + } + return maxArea; +}`, + + cpp: `int largestRectangleArea(vector& heights) { + int maxArea = 0; + stack st; + int n = heights.size(); + for (int i = 0; i <= n; i++) { + while (!st.empty() && + (i == n || heights[st.top()] >= heights[i])) { + int top = st.top(); + st.pop(); + int width = st.empty() ? i : i - st.top() - 1; + int area = heights[top] * width; + maxArea = max(maxArea, area); + } + st.push(i); + } + return maxArea; +}`, +}; + +// ─── Step generator ────────────────────────────────────────────────────────── + +function generateVisualizationData() { + const heights = [2, 1, 5, 6, 2, 3]; + const steps: Step[] = []; + const stack: number[] = []; + let maxArea = 0; - // Initial state - s.push({ + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + steps.push({ + heights, + stack: [], + currentIndex: -1, + topIndex: -1, + width: 0, + area: 0, + maxArea: 0, + activeRange: null, + explanation: "Initialize maxArea to 0 and an empty stack to store indices.", + pseudoStep: "SET maxArea = 0, stack = []", + variables: { maxArea: 0, stack: '[]', i: '-' } + }); + addLines(2, 2, 2, 2); + + for (let i = 0; i <= heights.length; i++) { + const h = i === heights.length ? 0 : heights[i]; + + steps.push({ heights, - stack: [], - currentIndex: -1, + stack: [...stack], + currentIndex: i, topIndex: -1, width: 0, area: 0, - maxArea: 0, + maxArea, activeRange: null, - explanation: "Initialize maxArea to 0 and an empty stack to store indices.", - highlightedLines: [1, 2, 3], - variables: { maxArea: 0, stack: '[]' } + explanation: i === heights.length + ? "Reached the end of the histogram. Process the remaining bars in the stack by treating the end as a bar of height 0." + : `Loop iteration: i = ${i}, current height = ${h}. Check if this bar breaks our increasing monotonic property in the stack.`, + pseudoStep: i === heights.length + ? `FOR i = ${i} (end of list, height = 0)` + : `FOR i = ${i}: height = ${h}`, + variables: { i, h, maxArea, stack: `[${stack.join(', ')}]` } }); + addLines(4, 5, 4, 5); - for (let i = 0; i <= heights.length; i++) { - const currentHeight = i === heights.length ? 0 : heights[i]; + while (stack.length > 0 && heights[stack[stack.length - 1]] >= h) { + const topIndex = stack[stack.length - 1]; + const h_top = heights[topIndex]; - s.push({ + steps.push({ heights, stack: [...stack], currentIndex: i, - topIndex: -1, + topIndex: topIndex, width: 0, area: 0, maxArea, activeRange: null, - explanation: i === heights.length - ? "Reached the end of the histogram. Let's process the remaining bars in the stack by treating the end as a bar of height 0." - : `Processing bar at index ${i} with height ${currentHeight}. We check if this bar breaks our increasing (monotonic) property in the stack.`, - highlightedLines: [5, 6, 7], - variables: { i, currentHeight, stack: `[${stack.join(', ')}]`, maxArea } + explanation: `The bar at index ${i} (height ${h}) is shorter than or equal to the bar at the top of stack (index ${topIndex}, height ${h_top}). This means index ${topIndex} cannot extend its rectangle further.`, + pseudoStep: `WHILE stack not empty AND stack.top >= ${h} → ${h_top} >= ${h} → YES ✓`, + variables: { i, h, top: topIndex, topHeight: h_top, maxArea, stack: `[${stack.join(', ')}]` } }); + addLines(5, 6, 5, 6); - while (stack.length > 0 && (i === heights.length || heights[stack[stack.length - 1]] >= heights[i])) { - const topIndex = stack.pop()!; - const h = heights[topIndex]; - const w = stack.length === 0 ? i : i - stack[stack.length - 1] - 1; - const area = h * w; - const leftBoundary = stack.length === 0 ? 0 : stack[stack.length - 1] + 1; - const rightBoundary = i - 1; - - s.push({ - heights, - stack: [...stack, topIndex], // Show it's about to be popped - currentIndex: i, - topIndex: topIndex, - width: 0, - area: 0, - maxArea, - activeRange: null, - explanation: `The bar at index ${i} (height ${currentHeight}) is shorter than or equal to the bar at the top of our stack (index ${topIndex}, height ${h}). This means index ${topIndex} cannot extend its rectangle any further to the right.`, - highlightedLines: [6, 7, 8], - variables: { i, currentHeight, top: topIndex, topHeight: h, stack: `[${[...stack, topIndex].join(', ')}]` } - }); + stack.pop(); + steps.push({ + heights, + stack: [...stack], + currentIndex: i, + topIndex: topIndex, + width: 0, + area: 0, + maxArea, + activeRange: null, + explanation: `Pop index ${topIndex} from stack. We will calculate the largest rectangle where this bar (height = ${h_top}) is the shortest bar.`, + pseudoStep: `SET top = stack.pop() → pop index ${topIndex}`, + variables: { i, h, popped: topIndex, height: h_top, maxArea, stack: `[${stack.join(', ')}]` } + }); + addLines(9, 7, 7, 9); - s.push({ - heights, - stack: [...stack], - currentIndex: i, - topIndex: topIndex, - width: w, - area: area, - maxArea, - activeRange: [leftBoundary, rightBoundary], - explanation: `We pop ${topIndex} and calculate the area. The height is ${h}. The width is ${w} (from index ${leftBoundary} to ${rightBoundary}). The area is ${h} * ${w} = ${area}.`, - highlightedLines: [9, 10, 11, 12, 13, 14], - variables: { height: h, width: w, area, currentStack: `[${stack.join(', ')}]` } - }); + const w = stack.length === 0 ? i : i - stack[stack.length - 1] - 1; + const leftBoundary = stack.length === 0 ? 0 : stack[stack.length - 1] + 1; + const rightBoundary = i - 1; - const prevMax = maxArea; - maxArea = Math.max(maxArea, area); - s.push({ - heights, - stack: [...stack], - currentIndex: i, - topIndex: topIndex, - width: w, - area: area, - maxArea, - activeRange: [leftBoundary, rightBoundary], - explanation: area > prevMax - ? `New maximum area found! ${area} is greater than ${prevMax}.` - : `The current area ${area} is not greater than our maximum ${prevMax}. maxArea remains ${maxArea}.`, - highlightedLines: [15], - variables: { area, maxArea } - }); - } + steps.push({ + heights, + stack: [...stack], + currentIndex: i, + topIndex: topIndex, + width: w, + area: 0, + maxArea, + activeRange: [leftBoundary, rightBoundary], + explanation: stack.length === 0 + ? `Stack is empty. The width extends from index 0 to ${i - 1} (width = ${i}).` + : `Stack is not empty. The width extends from index ${leftBoundary} to ${rightBoundary} (width = ${i} − ${stack[stack.length - 1]} − 1 = ${w}).`, + pseudoStep: stack.length === 0 + ? `SET width = i → ${i}` + : `SET width = i − stack.top − 1 → ${i} − ${stack[stack.length - 1]} − 1 = ${w}`, + variables: { i, h, height: h_top, width: w, maxArea, stack: `[${stack.join(', ')}]` } + }); + addLines(10, 9, 8, 10); - if (i < heights.length) { - stack.push(i); - s.push({ - heights, - stack: [...stack], - currentIndex: i, - topIndex: -1, - width: 0, - area: 0, - maxArea, - activeRange: null, - explanation: `Now that all bars taller than ${heights[i]} are processed, we push index ${i} onto the stack. This maintains our non-decreasing property.`, - highlightedLines: [17], - variables: { pushed: i, stack: `[${stack.join(', ')}]` } - }); - } + const area = h_top * w; + steps.push({ + heights, + stack: [...stack], + currentIndex: i, + topIndex: topIndex, + width: w, + area: area, + maxArea, + activeRange: [leftBoundary, rightBoundary], + explanation: `Calculate area: height (${h_top}) * width (${w}) = ${area}.`, + pseudoStep: `SET area = heights[top] * width → ${h_top} * ${w} = ${area}`, + variables: { i, h, height: h_top, width: w, area, maxArea, stack: `[${stack.join(', ')}]` } + }); + addLines(14, 10, 9, 11); + + const prevMax = maxArea; + maxArea = Math.max(maxArea, area); + steps.push({ + heights, + stack: [...stack], + currentIndex: i, + topIndex: topIndex, + width: w, + area: area, + maxArea, + activeRange: [leftBoundary, rightBoundary], + explanation: area > prevMax + ? `New maximum area found! ${area} is greater than ${prevMax}. Update maxArea = ${maxArea}.` + : `The current area ${area} is not greater than our maximum ${prevMax}. maxArea remains ${maxArea}.`, + pseudoStep: `maxArea = max(maxArea, area) → max(${prevMax}, ${area}) = ${maxArea}`, + variables: { i, h, area, maxArea, stack: `[${stack.join(', ')}]` } + }); + addLines(15, 11, 10, 12); } - s.push({ - heights, - stack: [], - currentIndex: heights.length, - topIndex: -1, - width: 0, - area: 0, - maxArea, - activeRange: null, - explanation: `Algorithm finished. The largest rectangular area found in the histogram is ${maxArea}.`, - highlightedLines: [19], - variables: { maxArea } - }); + if (i < heights.length) { + stack.push(i); + steps.push({ + heights, + stack: [...stack], + currentIndex: i, + topIndex: -1, + width: 0, + area: 0, + maxArea, + activeRange: null, + explanation: `Push current index ${i} onto the stack. This maintains our increasing monotonic property.`, + pseudoStep: `stack.push(${i})`, + variables: { i, h, maxArea, stack: `[${stack.join(', ')}]` } + }); + addLines(17, 12, 12, 14); + } + } - return s; - }, []); + steps.push({ + heights, + stack: [], + currentIndex: heights.length, + topIndex: -1, + width: 0, + area: 0, + maxArea, + activeRange: null, + explanation: `Algorithm finished. The largest rectangular area in the histogram is ${maxArea}.`, + pseudoStep: `RETURN maxArea → ${maxArea}`, + variables: { maxArea, result: maxArea } + }); + addLines(19, 13, 14, 16); - const code = `function largestRectangleArea(heights: number[]): number { - let maxArea = 0; - const stack: number[] = []; + return { steps, stepLineNumbers }; +} - for (let i = 0; i <= heights.length; i++) { - const h = i === heights.length ? 0 : heights[i]; - while (stack.length > 0 && heights[stack[stack.length - 1]] >= h) { - const top = stack.pop()!; - const width = stack.length === 0 - ? i - : i - stack[stack.length - 1] - 1; - - const area = heights[top] * width; - maxArea = Math.max(maxArea, area); - } - stack.push(i); +// ─── Component ─────────────────────────────────────────────────────────────── + +export const MonotonicStackVisualization = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); + + useEffect(() => { + if (isPlaying && currentStepIndex < steps.length - 1) { + intervalRef.current = setInterval(() => { + setCurrentStepIndex(prev => { + if (prev >= steps.length - 1) { + setIsPlaying(false); + return prev; + } + return prev + 1; + }); + }, 1200 / speed); + } else { + if (intervalRef.current) clearInterval(intervalRef.current); } - return maxArea; -}`; + return () => { + if (intervalRef.current) clearInterval(intervalRef.current); + }; + }, [isPlaying, currentStepIndex, steps.length, speed]); + + const handlePlay = () => setIsPlaying(true); + const handlePause = () => setIsPlaying(false); + const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(p => p + 1); + const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(p => p - 1); + const handleReset = () => { + setCurrentStepIndex(0); + setIsPlaying(false); + }; - const step = steps[currentStep]; - const maxH = Math.max(...heights); + if (steps.length === 0) return null; + + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); + const maxH = Math.max(...currentStep.heights); return ( - - -

Largest Rectangle in Histogram

- -
- {/* Histogram Bars */} +
+ + +
+ {/* Left: visual state */} +
+
+

Largest Rectangle in Histogram

+ +
- {heights.map((h, idx) => { - const isCurrent = idx === step.currentIndex; - const isInStack = step.stack.includes(idx); - const isTop = idx === step.topIndex; - const isActiveRange = step.activeRange && idx >= step.activeRange[0] && idx <= step.activeRange[1]; + {currentStep.heights.map((h, idx) => { + const isCurrent = idx === currentStep.currentIndex; + const isInStack = currentStep.stack.includes(idx); + const isTop = idx === currentStep.topIndex; + const isActiveRange = currentStep.activeRange && idx >= currentStep.activeRange[0] && idx <= currentStep.activeRange[1]; return (
@@ -198,7 +360,7 @@ export const MonotonicStackVisualization = () => { animate={{ height: `${(h / maxH) * 100}%`, backgroundColor: isTop - ? "rgba(249, 115, 22, 0.4)" // Orange highlight for height provider + ? "rgba(249, 115, 22, 0.4)" : isActiveRange ? "rgba(var(--primary), 0.1)" : isInStack @@ -234,27 +396,27 @@ export const MonotonicStackVisualization = () => { {/* Rectangle Overlay */} - {step.activeRange && ( + {currentStep.activeRange && (
- Area: {step.area} + Area: {currentStep.area}
- w: {step.width} × h: {heights[step.topIndex]} + w: {currentStep.width} × h: {currentStep.heights[currentStep.topIndex]}
@@ -264,8 +426,8 @@ export const MonotonicStackVisualization = () => {
{/* X-Axis Labels */} -
- {heights.map((_, idx) => ( +
+ {currentStep.heights.map((_, idx) => (
{idx}
@@ -273,19 +435,19 @@ export const MonotonicStackVisualization = () => {
-
+
-

Monotonic Stack (Indices)

-
+

Monotonic Stack (Indices)

+
- {step.stack.map((idx, sIdx) => ( + {currentStep.stack.map((idx, sIdx) => ( { ))} - {step.stack.length === 0 && Stack is empty} + {currentStep.stack.length === 0 && Stack is empty}
-
-

Global Max

-
- {step.maxArea} +
+

Global Max

+
+ {currentStep.maxArea}
- - - -
-

Algorithm Insights

-

- {step.explanation} -

- -
- } - rightContent={ -
- + +
+

{currentStep.explanation}

+
+ +
+

Monotonic Stack Strategy:

+
+

• Store indices of bars in a stack representing non-decreasing heights

+

• Pop bars when current bar breaks increasing order (its height is shorter)

+

• For popped bar, width extends from next item in stack to current index `i`

+

• Time: O(n) · Space: O(n)

+
+
+ + = 0 && currentStep.currentIndex <= currentStep.heights.length ? currentStep.currentIndex : '-', + stack: `[${currentStep.stack.join(', ')}]`, + topIndex: currentStep.topIndex >= 0 ? currentStep.topIndex : '-', + height: currentStep.topIndex >= 0 ? currentStep.heights[currentStep.topIndex] : '-', + width: currentStep.width || '-', + area: currentStep.area || '-', + maxArea: currentStep.maxArea + }} /> -
- } - controls={ - - } - /> +
+
); }; diff --git a/src/components/visualizations/algorithms/NQueensVisualization.tsx b/src/components/visualizations/algorithms/NQueensVisualization.tsx index d580c4c..2874d4c 100644 --- a/src/components/visualizations/algorithms/NQueensVisualization.tsx +++ b/src/components/visualizations/algorithms/NQueensVisualization.tsx @@ -1,8 +1,8 @@ -import React, { useEffect, useRef, useState } from 'react'; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; -import { StepControls } from '../shared/StepControls'; -import { VariablePanel } from '../shared/VariablePanel'; +import React, { useState, useEffect, useRef } from "react"; +import { StepControls } from "../shared/StepControls"; +import { VariablePanel } from "../shared/VariablePanel"; +import { VisualizationCodePanel } from "../shared/VisualizationCodePanel"; +import type { StepLineNumberMap, VisualizationLanguageMap } from "@/types/visualization"; interface Step { n: number; @@ -14,64 +14,176 @@ interface Step { negDiagSet: number[]; allSolutions: string[][]; message: string; - lineNumber: number; + pseudoStep: string; } -export const NQueensVisualization: React.FC = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1000); - const intervalRef = useRef(null); - - const code = `function solveNQueens(n: number): string[][] { +const languages: VisualizationLanguageMap = { + typescript: `function solveNQueens(n: number): string[][] { const col = new Set(); const posDiag = new Set(); const negDiag = new Set(); const res: string[][] = []; const board: string[][] = Array.from({ length: n }, () => Array(n).fill('.')); - function backtrack(r: number): void { if (r === n) { const copy = board.map(row => row.join("")); res.push(copy); return; } - for (let c = 0; c < n; c++) { if (col.has(c) || posDiag.has(r + c) || negDiag.has(r - c)) { continue; } - col.add(c); posDiag.add(r + c); negDiag.add(r - c); board[r][c] = "Q"; - backtrack(r + 1); - col.delete(c); posDiag.delete(r + c); negDiag.delete(r - c); board[r][c] = "."; } } - backtrack(0); return res; -}`; +}`, + python: `def solveNQueens(n): + col = set() + posDiag = set() + negDiag = set() + res = [] + board = [['.'] * n for _ in range(n)] + def backtrack(r): + if r == n: + copy = [''.join(row) for row in board] + res.append(copy) + return + for c in range(n): + if c in col or (r + c) in posDiag or (r - c) in negDiag: + continue + col.add(c) + posDiag.add(r + c) + negDiag.add(r - c) + board[r][c] = 'Q' + backtrack(r + 1) + col.remove(c) + posDiag.remove(r + c) + negDiag.remove(r - c) + board[r][c] = '.' + backtrack(0) + return res`, + java: `public static class Solution { + public List> solveNQueens(int n) { + List> res = new ArrayList<>(); + char[][] board = new char[n][n]; + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + board[i][j] = '.'; + } + } + Set col = new HashSet<>(); + Set posDiag = new HashSet<>(); + Set negDiag = new HashSet<>(); + backtrack(board, 0, col, posDiag, negDiag, res, n); + return res; + } + private void backtrack(char[][] board, int r, Set col, Set posDiag, Set negDiag, List> res, int n) { + if (r == n) { + List solution = new ArrayList<>(); + for (int i = 0; i < n; i++) { + solution.add(new String(board[i])); + } + res.add(solution); + return; + } + for (int c = 0; c < n; c++) { + if (col.contains(c) || posDiag.contains(r + c) || negDiag.contains(r - c)) { + continue; + } + col.add(c); + posDiag.add(r + c); + negDiag.add(r - c); + board[r][c] = 'Q'; + backtrack(board, r + 1, col, posDiag, negDiag, res, n); + col.remove(c); + posDiag.remove(r + c); + negDiag.remove(r - c); + board[r][c] = '.'; + } + } +}`, + cpp: `class Solution { +public: + vector> solveNQueens(int n) { + vector> res; + vector board(n, string(n, '.')); + unordered_set col; + unordered_set posDiag; + unordered_set negDiag; + backtrack(board, 0, col, posDiag, negDiag, res, n); + return res; + } +private: + void backtrack(vector& board, + int r, + unordered_set& col, + unordered_set& posDiag, + unordered_set& negDiag, + vector>& res, + int n) { + if (r == n) { + res.push_back(board); + return; + } + for (int c = 0; c < n; c++) { + if (col.count(c) || posDiag.count(r + c) || negDiag.count(r - c)) { + continue; + } + col.insert(c); + posDiag.insert(r + c); + negDiag.insert(r - c); + board[r][c] = 'Q'; + backtrack(board, r + 1, col, posDiag, negDiag, res, n); + col.erase(c); + posDiag.erase(r + c); + negDiag.erase(r - c); + board[r][c] = '.'; + } + } +};` +}; + +export const NQueensVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); - const generateSteps = () => { + const generateStepsData = () => { const n = 4; - const newSteps: Step[] = []; + const steps: Step[] = []; const col = new Set(); const posDiag = new Set(); const negDiag = new Set(); const res: string[][] = []; const board: string[][] = Array.from({ length: n }, () => Array(n).fill('.')); + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - const addStep = (row: number, c: number, message: string, line: number) => { - newSteps.push({ + const addStep = (row: number, c: number, message: string, pseudoStep: string, ts: number, py: number, java: number, cpp: number) => { + steps.push({ n, board: board.map(r => [...r]), row, @@ -81,24 +193,31 @@ export const NQueensVisualization: React.FC = () => { negDiagSet: Array.from(negDiag), allSolutions: [...res], message, - lineNumber: line + pseudoStep }); + addLines(ts, py, java, cpp); }; function backtrack(r: number): void { - addStep(r, -1, `Checking row ${r}`, 9); + addStep(r, -1, `Checking row ${r}`, `CALL backtrack(r = ${r})`, 7, 7, 16, 13); + + addStep(r, -1, `Check base case: r === n`, `IF r === n → ${r} === ${n} ?`, 8, 8, 17, 20); + if (r === n) { const copy = board.map(row => row.join("")); res.push(copy); - addStep(r, -1, `Found a solution! Total solutions: ${res.length}`, 11); + addStep(r, -1, `Found a solution! Added to solutions list.`, `ADD board configuration to res`, 10, 10, 22, 21); + addStep(r, -1, `Return from recursive call.`, `RETURN`, 11, 11, 23, 22); return; } for (let c = 0; c < n; c++) { - addStep(r, c, `Checking column ${c} in row ${r}`, 15); - addStep(r, c, `Checking if safety constraints are met at (${r}, ${c})`, 16); + addStep(r, c, `Checking column ${c} in row ${r}`, `FOR c = ${c} to ${n - 1}`, 13, 12, 25, 24); + + addStep(r, c, `Checking if cell (${r}, ${c}) is under attack`, `IF col(${c}) OR posDiag(${r + c}) OR negDiag(${r - c}) IN sets`, 14, 13, 26, 25); + if (col.has(c) || posDiag.has(r + c) || negDiag.has(r - c)) { - addStep(r, c, `Position (${r}, ${c}) is unsafe. Skipping column ${c}.`, 17); + addStep(r, c, `Cell (${r}, ${c}) is under attack. Skipping.`, `CONTINUE`, 15, 14, 27, 26); continue; } @@ -106,34 +225,41 @@ export const NQueensVisualization: React.FC = () => { posDiag.add(r + c); negDiag.add(r - c); board[r][c] = "Q"; - addStep(r, c, `Placing queen at (${r}, ${c}) and updating sets`, 23); + addStep(r, c, `Placing queen at (${r}, ${c}) and updating constraint sets`, `SET board[${r}][${c}] = 'Q'`, 20, 18, 32, 31); - addStep(r, c, `Moving to the next row (r=${r + 1})`, 25); + addStep(r, c, `Recursively solve for next row r = ${r + 1}`, `CALL backtrack(r = ${r + 1})`, 21, 19, 33, 32); backtrack(r + 1); col.delete(c); posDiag.delete(r + c); negDiag.delete(r - c); board[r][c] = "."; - addStep(r, c, `Backtracking: Removing queen from (${r}, ${c}) and updating sets`, 30); + addStep(r, c, `Backtrack: Removing queen from (${r}, ${c})`, `RESET board[${r}][${c}] = '.'`, 25, 23, 37, 36); } } - addStep(-1, -1, `Starting N-Queens algorithm for n=${n}`, 1); - addStep(-1, -1, `Initializing sets and empty board`, 6); - addStep(0, -1, `Calling backtrack(0)`, 34); + addStep(-1, -1, `Starting N-Queens algorithm for n = ${n}`, `CALL solveNQueens(n = ${n})`, 1, 1, 2, 3); + addStep(-1, -1, `Initializing sets and empty board`, `SET col = {}, posDiag = {}, negDiag = {}, board = [${n}x${n}]`, 2, 2, 4, 4); + addStep(0, -1, `Calling backtrack(r = 0)`, `CALL backtrack(r = 0)`, 28, 24, 13, 9); backtrack(0); - addStep(-1, -1, `Algorithm complete. Found ${res.length} solutions.`, 35); - setSteps(newSteps); + addStep(-1, -1, `Algorithm complete. Found ${res.length} solutions.`, `RETURN res`, 29, 25, 14, 10); + + const lastStep = steps[steps.length - 1]; + steps.push({ + ...lastStep, + message: `Complete! Total solutions found: ${res.length}`, + pseudoStep: "DONE" + }); + addLines(29, 25, 14, 10); + + return { steps, stepLineNumbers }; }; - useEffect(() => { - generateSteps(); - }, []); + const { steps, stepLineNumbers } = generateStepsData(); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { - intervalRef.current = window.setInterval(() => { + intervalRef.current = setInterval(() => { setCurrentStepIndex((prev) => { if (prev >= steps.length - 1) { setIsPlaying(false); @@ -141,7 +267,7 @@ export const NQueensVisualization: React.FC = () => { } return prev + 1; }); - }, speed); + }, 1000 / speed); } else { if (intervalRef.current) { clearInterval(intervalRef.current); @@ -170,6 +296,7 @@ export const NQueensVisualization: React.FC = () => { if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map((s) => s.pseudoStep); return (
@@ -181,101 +308,122 @@ export const NQueensVisualization: React.FC = () => { onReset={handleReset} isPlaying={isPlaying} currentStep={currentStepIndex} - totalSteps={steps.length} + totalSteps={steps.length - 1} speed={speed} onSpeedChange={setSpeed} />
-
-

{currentStep.n}-Queens Board

+
+
+

{currentStep.n}-Queens Board

-
- {currentStep.board.map((row, r) => - row.map((cell, c) => ( -
+ {currentStep.board.map((row, r) => + row.map((cell, c) => ( +
- {cell === 'Q' ? '♛' : ''} -
- )) - )} -
+ > + {cell === "Q" ? "♛" : ""} +
+ )) + )} +
-
-
-

Occupied Sets

-
-
- col: -
- {currentStep.colSet.length > 0 ? currentStep.colSet.map(v => {v}) : '-'} +
+
+

Occupied Sets

+
+
+ col: +
+ {currentStep.colSet.length > 0 + ? currentStep.colSet.map(v => {v}) + : "-"} +
-
-
- posDiag (r+c): -
- {currentStep.posDiagSet.length > 0 ? currentStep.posDiagSet.map(v => {v}) : '-'} +
+ posDiag (r+c): +
+ {currentStep.posDiagSet.length > 0 + ? currentStep.posDiagSet.map(v => {v}) + : "-"} +
-
-
- negDiag (r-c): -
- {currentStep.negDiagSet.length > 0 ? currentStep.negDiagSet.map(v => {v}) : '-'} +
+ negDiag (r-c): +
+ {currentStep.negDiagSet.length > 0 + ? currentStep.negDiagSet.map(v => {v}) + : "-"} +
-
-
-

Solutions Found: {currentStep.allSolutions.length}

-
- {currentStep.allSolutions.length === 0 &&

No solutions found yet...

} - {currentStep.allSolutions.map((solution, idx) => ( -
-
Solution {idx + 1}
-
- {solution.map((row, r) => - row.split('').map((cell, c) => ( -
+

Solutions Found: {currentStep.allSolutions.length}

+
+ {currentStep.allSolutions.length === 0 && ( +

No solutions found yet...

+ )} + {currentStep.allSolutions.map((solution, idx) => ( +
+
Solution {idx + 1}
+
+ {solution.map((row, r) => + row.split("").map((cell, c) => ( +
- {cell === 'Q' ? '♛' : ''} -
- )) - )} + > + {cell === "Q" ? "♛" : ""} +
+ )) + )} +
-
- ))} + ))} +
-
-

{currentStep.message}

-
-
- +
+

Algorithm Logic

+

+ {currentStep.message} +

+ +
- + +
); diff --git a/src/components/visualizations/algorithms/NumberOf1BitsVisualization.tsx b/src/components/visualizations/algorithms/NumberOf1BitsVisualization.tsx index 072d728..f36f251 100644 --- a/src/components/visualizations/algorithms/NumberOf1BitsVisualization.tsx +++ b/src/components/visualizations/algorithms/NumberOf1BitsVisualization.tsx @@ -1,329 +1,304 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; +import { Card } from '@/components/ui/card'; +import { VisualizationLayout } from '../shared/VisualizationLayout'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; -import { VisualizationLayout } from '../shared/VisualizationLayout'; -import { motion } from 'framer-motion'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { variables: Record; explanation: string; - highlightedLines: number[]; - lineExecution: string; + pseudoStep: string; bits: string[]; - checkingBit: number; + bitsPrev?: string[]; + showPrev?: boolean; } +const languages: VisualizationLanguageMap = { + typescript: `function hammingWeight(n: number): number { + let count = 0; + while (n !== 0) { + n = n & (n - 1); + count++; + } + return count; +}`, + python: `def hammingWeight(n: int) -> int: + count = 0 + while n != 0: + n = n & (n - 1) + count += 1 + return count`, + java: `public static class Solution { + public int hammingWeight(int n) { + int count = 0; + while (n != 0) { + n = n & (n - 1); + count++; + } + return count; + } +}`, + cpp: `class Solution { +public: + int hammingWeight(unsigned int n) { + int count = 0; + while (n != 0) { + n = n & (n - 1); + count++; + } + return count; + } +};` +}; + export const NumberOf1BitsVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); + const [steps, setSteps] = useState([]); + const [stepLineNumbers, setStepLineNumbers] = useState({ + typescript: [], + python: [], + java: [], + cpp: [] + }); + const [currentStepIndex, setCurrentStepIndex] = useState(0); - const steps: Step[] = [ - { - variables: { n: 11, nBinary: '00001011', count: '?' }, - explanation: "Starting function with n = 11 (binary: 00001011). Will count the number of 1 bits.", - highlightedLines: [1], - lineExecution: "function hammingWeight(n: number): number {", - bits: ['0', '0', '0', '0', '1', '0', '1', '1'], - checkingBit: -1 - }, - { - variables: { n: 11, nBinary: '00001011', count: 0 }, - explanation: "Initialize count = 0. This will track the number of 1 bits found.", - highlightedLines: [2], - lineExecution: "let count = 0;", - bits: ['0', '0', '0', '0', '1', '0', '1', '1'], - checkingBit: -1 - }, - { - variables: { n: 11, nBinary: '00001011', count: 0 }, - explanation: "Check loop condition: n (11) !== 0? Yes, enter loop.", - highlightedLines: [3], - lineExecution: "while (n !== 0)", - bits: ['0', '0', '0', '0', '1', '0', '1', '1'], - checkingBit: -1 - }, - { - variables: { n: 11, nBinary: '00001011', count: 0, rightmost: 1 }, - explanation: "Check rightmost bit: n & 1 = 00001011 & 00000001 = 1.", - highlightedLines: [4], - lineExecution: "count += n & 1", - bits: ['0', '0', '0', '0', '1', '0', '1', '1'], - checkingBit: 7 - }, - { - variables: { n: 11, nBinary: '00001011', count: 1, rightmost: 1 }, - explanation: "Rightmost bit is 1, add to count. count = 0 + 1 = 1.", - highlightedLines: [4], - lineExecution: "count = 1", - bits: ['0', '0', '0', '0', '1', '0', '1', '1'], - checkingBit: 7 - }, - { - variables: { n: 11, nBinary: '00001011', count: 1 }, - explanation: "Unsigned right shift: n >>>= 1. Shifts all bits right by 1 position.", - highlightedLines: [5], - lineExecution: "n >>>= 1", + useEffect(() => { + const generatedSteps: Step[] = []; + const stepLines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLines.typescript!.push(ts); + stepLines.python!.push(py); + stepLines.java!.push(java); + stepLines.cpp!.push(cpp); + }; + + // Step 0: start + generatedSteps.push({ + variables: { n: 11, nBinary: '00001011', count: '-' }, + explanation: "Start counting set bits of n = 11 (binary 00001011) using Brian Kernighan's Algorithm.", + pseudoStep: "FUNCTION hammingWeight(n)", + bits: ['0', '0', '0', '0', '1', '0', '1', '1'] + }); + addLines(1, 1, 3, 4); + + // Step 1: count init + let count = 0; + generatedSteps.push({ + variables: { n: 11, nBinary: '00001011', count }, + explanation: `Initialize count = ${count}.`, + pseudoStep: "SET count = 0", + bits: ['0', '0', '0', '0', '1', '0', '1', '1'] + }); + addLines(2, 2, 4, 5); + + // Step 2: Loop check 1 + generatedSteps.push({ + variables: { n: 11, nBinary: '00001011', count }, + explanation: "Check if n !== 0. Since n is 11, we enter the loop.", + pseudoStep: "WHILE n != 0", + bits: ['0', '0', '0', '0', '1', '0', '1', '1'] + }); + addLines(3, 3, 5, 6); + + // Step 3: n = n & (n - 1) + generatedSteps.push({ + variables: { n: 11, nBinary: '00001011', count, prevN: 10, prevNBinary: '00001010' }, + explanation: "n & (n - 1) clears the rightmost set bit: 11 (00001011) & 10 (00001010) = 10 (00001010).", + pseudoStep: "SET n = n & (n - 1)", bits: ['0', '0', '0', '0', '1', '0', '1', '1'], - checkingBit: -1 - }, - { - variables: { n: 5, nBinary: '00000101', count: 1 }, - explanation: "After shift: n = 5 (binary: 00000101). Lost the rightmost bit.", - highlightedLines: [5], - lineExecution: "n = 5", - bits: ['0', '0', '0', '0', '0', '1', '0', '1'], - checkingBit: -1 - }, - { - variables: { n: 5, nBinary: '00000101', count: 1 }, - explanation: "Check loop condition: n (5) !== 0? Yes, continue.", - highlightedLines: [3], - lineExecution: "while (n !== 0)", - bits: ['0', '0', '0', '0', '0', '1', '0', '1'], - checkingBit: -1 - }, - { - variables: { n: 5, nBinary: '00000101', count: 1, rightmost: 1 }, - explanation: "Check rightmost bit: n & 1 = 00000101 & 00000001 = 1.", - highlightedLines: [4], - lineExecution: "count += n & 1", - bits: ['0', '0', '0', '0', '0', '1', '0', '1'], - checkingBit: 7 - }, - { - variables: { n: 5, nBinary: '00000101', count: 2, rightmost: 1 }, - explanation: "Rightmost bit is 1, add to count. count = 1 + 1 = 2.", - highlightedLines: [4], - lineExecution: "count = 2", - bits: ['0', '0', '0', '0', '0', '1', '0', '1'], - checkingBit: 7 - }, - { - variables: { n: 5, nBinary: '00000101', count: 2 }, - explanation: "Unsigned right shift: n >>>= 1.", - highlightedLines: [5], - lineExecution: "n >>>= 1", - bits: ['0', '0', '0', '0', '0', '1', '0', '1'], - checkingBit: -1 - }, - { - variables: { n: 2, nBinary: '00000010', count: 2 }, - explanation: "After shift: n = 2 (binary: 00000010).", - highlightedLines: [5], - lineExecution: "n = 2", - bits: ['0', '0', '0', '0', '0', '0', '1', '0'], - checkingBit: -1 - }, - { - variables: { n: 2, nBinary: '00000010', count: 2 }, - explanation: "Check loop condition: n (2) !== 0? Yes, continue.", - highlightedLines: [3], - lineExecution: "while (n !== 0)", - bits: ['0', '0', '0', '0', '0', '0', '1', '0'], - checkingBit: -1 - }, - { - variables: { n: 2, nBinary: '00000010', count: 2, rightmost: 0 }, - explanation: "Check rightmost bit: n & 1 = 00000010 & 00000001 = 0.", - highlightedLines: [4], - lineExecution: "count += n & 1", - bits: ['0', '0', '0', '0', '0', '0', '1', '0'], - checkingBit: 7 - }, - { - variables: { n: 2, nBinary: '00000010', count: 2, rightmost: 0 }, - explanation: "Rightmost bit is 0, don't add. count = 2 + 0 = 2.", - highlightedLines: [4], - lineExecution: "count = 2", - bits: ['0', '0', '0', '0', '0', '0', '1', '0'], - checkingBit: 7 - }, - { - variables: { n: 2, nBinary: '00000010', count: 2 }, - explanation: "Unsigned right shift: n >>>= 1.", - highlightedLines: [5], - lineExecution: "n >>>= 1", - bits: ['0', '0', '0', '0', '0', '0', '1', '0'], - checkingBit: -1 - }, - { - variables: { n: 1, nBinary: '00000001', count: 2 }, - explanation: "After shift: n = 1 (binary: 00000001).", - highlightedLines: [5], - lineExecution: "n = 1", - bits: ['0', '0', '0', '0', '0', '0', '0', '1'], - checkingBit: -1 - }, - { - variables: { n: 1, nBinary: '00000001', count: 2 }, - explanation: "Check loop condition: n (1) !== 0? Yes, continue.", - highlightedLines: [3], - lineExecution: "while (n !== 0)", - bits: ['0', '0', '0', '0', '0', '0', '0', '1'], - checkingBit: -1 - }, - { - variables: { n: 1, nBinary: '00000001', count: 2, rightmost: 1 }, - explanation: "Check rightmost bit: n & 1 = 00000001 & 00000001 = 1.", - highlightedLines: [4], - lineExecution: "count += n & 1", - bits: ['0', '0', '0', '0', '0', '0', '0', '1'], - checkingBit: 7 - }, - { - variables: { n: 1, nBinary: '00000001', count: 3, rightmost: 1 }, - explanation: "Rightmost bit is 1, add to count. count = 2 + 1 = 3.", - highlightedLines: [4], - lineExecution: "count = 3", - bits: ['0', '0', '0', '0', '0', '0', '0', '1'], - checkingBit: 7 - }, - { - variables: { n: 1, nBinary: '00000001', count: 3 }, - explanation: "Unsigned right shift: n >>>= 1.", - highlightedLines: [5], - lineExecution: "n >>>= 1", - bits: ['0', '0', '0', '0', '0', '0', '0', '1'], - checkingBit: -1 - }, - { - variables: { n: 0, nBinary: '00000000', count: 3 }, - explanation: "After shift: n = 0 (binary: 00000000). All bits processed.", - highlightedLines: [5], - lineExecution: "n = 0", - bits: ['0', '0', '0', '0', '0', '0', '0', '0'], - checkingBit: -1 - }, - { - variables: { n: 0, nBinary: '00000000', count: 3 }, - explanation: "Check loop condition: n (0) !== 0? No, exit loop.", - highlightedLines: [3], - lineExecution: "while (n !== 0) -> false", - bits: ['0', '0', '0', '0', '0', '0', '0', '0'], - checkingBit: -1 - }, - { - variables: { n: 0, nBinary: '00000000', count: 3 }, - explanation: "Return count = 3. The number 11 has three 1-bits!", - highlightedLines: [7], - lineExecution: "return count = 3", - bits: ['0', '0', '0', '0', '0', '0', '0', '0'], - checkingBit: -1 - }, - { - variables: { n: 0, nBinary: '00000000', count: 3, result: 3 }, - explanation: "Algorithm complete! Found 3 one-bits. Time: O(log n), Space: O(1).", - highlightedLines: [7], - lineExecution: "Result: 3", - bits: ['0', '0', '0', '0', '0', '0', '0', '0'], - checkingBit: -1 - } - ]; + bitsPrev: ['0', '0', '0', '0', '1', '0', '1', '0'], + showPrev: true + }); + addLines(4, 4, 6, 7); + + // Step 4: count++ + count++; + generatedSteps.push({ + variables: { n: 10, nBinary: '00001010', count }, + explanation: `Increment count of set bits: count = ${count}.`, + pseudoStep: "SET count = count + 1", + bits: ['0', '0', '0', '0', '1', '0', '1', '0'] + }); + addLines(5, 5, 7, 8); + + // Step 5: Loop check 2 + generatedSteps.push({ + variables: { n: 10, nBinary: '00001010', count }, + explanation: "Check if n !== 0. Since n is 10, continue loop.", + pseudoStep: "WHILE n != 0", + bits: ['0', '0', '0', '0', '1', '0', '1', '0'] + }); + addLines(3, 3, 5, 6); + + // Step 6: n = n & (n - 1) + generatedSteps.push({ + variables: { n: 10, nBinary: '00001010', count, prevN: 9, prevNBinary: '00001001' }, + explanation: "Clear rightmost set bit: 10 (00001010) & 9 (00001001) = 8 (00001000).", + pseudoStep: "SET n = n & (n - 1)", + bits: ['0', '0', '0', '0', '1', '0', '1', '0'], + bitsPrev: ['0', '0', '0', '0', '1', '0', '0', '1'], + showPrev: true + }); + addLines(4, 4, 6, 7); + + // Step 7: count++ + count++; + generatedSteps.push({ + variables: { n: 8, nBinary: '00001000', count }, + explanation: `Increment count of set bits: count = ${count}.`, + pseudoStep: "SET count = count + 1", + bits: ['0', '0', '0', '0', '1', '0', '0', '0'] + }); + addLines(5, 5, 7, 8); - const code = `function hammingWeight(n: number): number { - let count = 0; - while (n !== 0) { - count += n & 1; - n >>>= 1; - } - return count; -}`; + // Step 8: Loop check 3 + generatedSteps.push({ + variables: { n: 8, nBinary: '00001000', count }, + explanation: "Check if n !== 0. Since n is 8, continue loop.", + pseudoStep: "WHILE n != 0", + bits: ['0', '0', '0', '0', '1', '0', '0', '0'] + }); + addLines(3, 3, 5, 6); - const step = steps[currentStep]; + // Step 9: n = n & (n - 1) + generatedSteps.push({ + variables: { n: 8, nBinary: '00001000', count, prevN: 7, prevNBinary: '00000111' }, + explanation: "Clear rightmost set bit: 8 (00001000) & 7 (00000111) = 0 (00000000).", + pseudoStep: "SET n = n & (n - 1)", + bits: ['0', '0', '0', '0', '1', '0', '0', '0'], + bitsPrev: ['0', '0', '0', '0', '0', '1', '1', '1'], + showPrev: true + }); + addLines(4, 4, 6, 7); + + // Step 10: count++ + count++; + generatedSteps.push({ + variables: { n: 0, nBinary: '00000000', count }, + explanation: `Increment count of set bits: count = ${count}.`, + pseudoStep: "SET count = count + 1", + bits: ['0', '0', '0', '0', '0', '0', '0', '0'] + }); + addLines(5, 5, 7, 8); + + // Step 11: Loop check 4 + generatedSteps.push({ + variables: { n: 0, nBinary: '00000000', count }, + explanation: "Check if n !== 0. Since n is 0, exit loop.", + pseudoStep: "WHILE n != 0", + bits: ['0', '0', '0', '0', '0', '0', '0', '0'] + }); + addLines(3, 3, 5, 6); + + // Step 12: Return + generatedSteps.push({ + variables: { n: 0, nBinary: '00000000', count, result: count }, + explanation: `Return the final count of set bits: ${count}.`, + pseudoStep: "RETURN count", + bits: ['0', '0', '0', '0', '0', '0', '0', '0'] + }); + addLines(7, 6, 8, 9); + + setSteps(generatedSteps); + setStepLineNumbers(stepLines); + }, []); + + if (steps.length === 0) return null; + + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( + } leftContent={ - <> - -
-

Binary Representation

-
- {step.bits.map((bit, idx) => ( - - {bit} - - ))} -
-
-

{step.variables.nBinary}

-

Decimal: {step.variables.n}

-
-
-
+
+
+ +

+ Number of 1 Bits (Hamming Weight) +

- -
-
-
Current Execution:
-
- {step.lineExecution} +
+
+
+ Value of n (Binary) +
+
+ {currentStep.bits.map((bit, idx) => ( +
+ {bit} +
+ ))} +
+
+ Decimal: {currentStep.variables.n} +
-
- {step.explanation} + + {currentStep.showPrev && currentStep.bitsPrev && ( +
+
+ Value of n - 1 (Binary) +
+
+ {currentStep.bitsPrev.map((bit, idx) => ( +
+ {bit} +
+ ))} +
+
+ Decimal: {currentStep.variables.prevN} +
+
+ )} + +
+
Brian Kernighan's Algorithm Intuition:
+
• The operation n & (n - 1) clears the least significant (rightmost) set bit of n.
+
• Instead of checking all 32 bits of an integer, we only loop as many times as there are 1 bits.
+
• This makes the algorithm run in O(k) time where k is the number of 1 bits.
-
- + +
- -
-

Bit Manipulation:

-
-

• n & 1: Gets the rightmost bit (0 or 1)

-

• n {'>>>='} 1: Unsigned right shift by 1

-

• Check each bit until n becomes 0

-

• Time: O(log n), Space: O(1)

-
-
-
+
+ +
+

Step Explanation

+

{currentStep.explanation}

+ - - - - + +
+
} rightContent={ - - } - controls={ - setCurrentStepIndex(0)} /> } /> diff --git a/src/components/visualizations/algorithms/NumberOfConnectedComponentsVisualization.tsx b/src/components/visualizations/algorithms/NumberOfConnectedComponentsVisualization.tsx index 2485197..15237d9 100644 --- a/src/components/visualizations/algorithms/NumberOfConnectedComponentsVisualization.tsx +++ b/src/components/visualizations/algorithms/NumberOfConnectedComponentsVisualization.tsx @@ -1,30 +1,29 @@ -import { useState, useMemo } from 'react'; +import React, { useState, useMemo } from 'react'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; import { Card } from '@/components/ui/card'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; +import { Info, CheckCircle2 } from 'lucide-react'; interface Step { parent: number[]; rank: number[]; result: number; - highlightedLines: number[]; explanation: string; variables: Record; activeNodes: number[]; + pseudoStep: string; } const N = 5; const EDGES: number[][] = [[0, 1], [1, 2], [3, 4]]; -export const NumberOfConnectedComponentsVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); - - const code = `function countComponents(n: number, edges: number[][]): number { +const languages: VisualizationLanguageMap = { + typescript: `function countComponents(n: number, edges: number[][]): number { const par: number[] = Array.from({ length: n }, (_, i) => i); const rank: number[] = new Array(n).fill(1); - const find = (n1: number): number => { let res = n1; while (res !== par[res]) { @@ -33,13 +32,10 @@ export const NumberOfConnectedComponentsVisualization = () => { } return res; }; - const union = (n1: number, n2: number): number => { const p1 = find(n1); const p2 = find(n2); - if (p1 === p2) return 0; - if (rank[p2] > rank[p1]) { par[p1] = p2; rank[p2] += rank[p1]; @@ -47,197 +43,286 @@ export const NumberOfConnectedComponentsVisualization = () => { par[p2] = p1; rank[p1] += rank[p2]; } - return 1; }; - let res = n; for (const [n1, n2] of edges) { res -= union(n1, n2); } return res; -}`; +}`, + + python: `def count_components(n: int, edges: list[list[int]]) -> int: + par = list(range(n)) + rank = [1] * n + def find(n1: int) -> int: + res = n1 + while res != par[res]: + par[res] = par[par[res]] + res = par[res] + return res + def union(n1: int, n2: int) -> int: + p1 = find(n1) + p2 = find(n2) + if p1 == p2: + return 0 + if rank[p2] > rank[p1]: + par[p1] = p2 + rank[p2] += rank[p1] + else: + par[p2] = p1 + rank[p1] += rank[p2] + return 1 + res = n + for n1, n2 in edges: + res -= union(n1, n2) + return res`, + + java: `public static class Solution { + public int countComponents(int n, int[][] edges) { + int[] par = new int[n]; + int[] rank = new int[n]; + for (int i = 0; i < n; i++) { + par[i] = i; + rank[i] = 1; + } + int res = n; + for (int[] edge : edges) { + res -= union(edge[0], edge[1], par, rank); + } + return res; + } + private int union(int n1, int n2, int[] par, int[] rank) { + int p1 = find(n1, par); + int p2 = find(n2, par); + if (p1 == p2) return 0; + if (rank[p2] > rank[p1]) { + par[p1] = p2; + rank[p2] += rank[p1]; + } else { + par[p2] = p1; + rank[p1] += rank[p2]; + } + return 1; + } + private int find(int n1, int[] par) { + int res = n1; + while (res != par[res]) { + par[res] = par[par[res]]; + res = par[res]; + } + return res; + } +}`, + + cpp: `class Solution { +public: + int countComponents(int n, vector>& edges) { + vector par(n); + vector rank(n); + for (int i = 0; i < n; i++) { + par[i] = i; + rank[i] = 1; + } + int res = n; + for (auto& edge : edges) { + res -= unite(edge[0], edge[1], par, rank); + } + return res; + } +private: + int find(int n1, vector& par) { + int res = n1; + while (res != par[res]) { + par[res] = par[par[res]]; + res = par[res]; + } + return res; + } + int unite(int n1, int n2, vector& par, vector& rank) { + int p1 = find(n1, par); + int p2 = find(n2, par); + if (p1 == p2) return 0; + if (rank[p2] > rank[p1]) { + par[p1] = p2; + rank[p2] += rank[p1]; + } else { + par[p2] = p1; + rank[p1] += rank[p2]; + } + return 1; + } +};` +}; - const steps: Step[] = useMemo(() => { +export const NumberOfConnectedComponentsVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + + const { steps, stepLineNumbers } = useMemo(() => { const s: Step[] = []; + const stepLines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLines.typescript!.push(ts); + stepLines.python!.push(py); + stepLines.java!.push(java); + stepLines.cpp!.push(cpp); + }; + const par = Array.from({ length: N }, (_, i) => i); const rank = new Array(N).fill(1); let res = N; - s.push({ - parent: [...par], - rank: [...rank], - result: res, - highlightedLines: [1], - explanation: "Initialize countComponents with a graph of n nodes.", - variables: { n: N, res }, - activeNodes: [] - }); - - s.push({ - parent: [...par], - rank: [...rank], - result: res, - highlightedLines: [2], - explanation: "Initialize parent array: each node is its own parent initially.", - variables: { par: `[${par.join(',')}]` }, - activeNodes: [] - }); - - s.push({ - parent: [...par], - rank: [...rank], - result: res, - highlightedLines: [3], - explanation: "Initialize rank array to keep track of component sizes.", - variables: { rank: `[${rank.join(',')}]` }, - activeNodes: [] - }); - - s.push({ - parent: [...par], - rank: [...rank], - result: res, - highlightedLines: [28], - explanation: "Initially, the number of components equals the number of nodes.", - variables: { res }, - activeNodes: [] - }); - - const findFn = (n1: number): number => { + const makeSnapshot = ( + msg: string, + pseudo: string, + ts: number, + py: number, + java: number, + cpp: number, + activeNodes: number[], + vars: Record + ) => { s.push({ parent: [...par], rank: [...rank], result: res, - highlightedLines: [5, 6], - explanation: `Find root of node ${n1}.`, - variables: { n1, traversal: n1 }, - activeNodes: [n1] + explanation: msg, + variables: { + ...vars, + parent: `[${par.join(', ')}]`, + rank: `[${rank.join(', ')}]`, + res + }, + activeNodes, + pseudoStep: pseudo }); + addLines(ts, py, java, cpp); + }; + + // Step 1: Start + makeSnapshot( + "Start countComponents algorithm on undirected graph.", + "START countComponents(n, edges)", + 1, 1, 2, 3, [], {} + ); + + // Step 2: Init parent + makeSnapshot( + "Initialize parent array: each node starts as its own representative root.", + "SET par = [0..n-1]", 2, 2, 5, 4, [], {} + ); + + // Step 3: Init rank + makeSnapshot( + "Initialize rank array: each component starts with a size/rank of 1.", + "SET rank = [1]*n", 3, 3, 6, 5, [], {} + ); + + // Step 4: Init result + makeSnapshot( + "Initially, the number of connected components is equal to the number of nodes.", + "SET res = n", 25, 22, 9, 10, [], {} + ); + const findFn = (n1: number): number => { let curr = n1; while (curr !== par[curr]) { const oldPar = par[curr]; const gPar = par[oldPar]; - s.push({ - parent: [...par], - rank: [...rank], - result: res, - highlightedLines: [8, 9], - explanation: `Path compression: make ${curr} point to its grandparent ${gPar}.`, - variables: { curr, parent: oldPar, grandparent: gPar }, - activeNodes: [curr, oldPar, gPar] - }); + + // Path compression snapshot + makeSnapshot( + `Find root of Node ${n1}. Path compression: make Node ${curr} point to grandparent ${gPar}.`, + `SET par[${curr}] = par[par[${curr}]]`, + 7, 8, 11, 20, [curr, oldPar, gPar], { n1, curr } + ); + par[curr] = gPar; curr = par[curr]; } - - s.push({ - parent: [...par], - rank: [...rank], - result: res, - highlightedLines: [12], - explanation: `Root found: ${curr}.`, - variables: { root: curr }, - activeNodes: [curr] - }); return curr; }; const unionFn = (n1: number, n2: number): number => { - s.push({ - parent: [...par], - rank: [...rank], - result: res, - highlightedLines: [15, 16, 17], - explanation: `Union operation for nodes ${n1} and ${n2}. First, find their roots.`, - variables: { n1, n2 }, - activeNodes: [n1, n2] - }); + makeSnapshot( + `Union nodes ${n1} and ${n2}. First, find their representatives.`, + `union(${n1}, ${n2})`, + 12, 10, 15, 25, [n1, n2], { n1, n2 } + ); const p1 = findFn(n1); const p2 = findFn(n2); - s.push({ - parent: [...par], - rank: [...rank], - result: res, - highlightedLines: [19], - explanation: `Roots are p1=${p1}, p2=${p2}. ${p1 === p2 ? 'Already connected.' : 'Merging components.'}`, - variables: { p1, p2 }, - activeNodes: [p1, p2] - }); + const alreadyConnected = p1 === p2; + makeSnapshot( + `Representatives are p1=${p1}, p2=${p2}. Are they already connected? ${alreadyConnected ? "YES" : "NO"}.`, + `IF p1 == p2 → ${alreadyConnected ? "YES ✓" : "NO ✗"}`, + 15, 13, 18, 28, [p1, p2], { p1, p2 } + ); if (p1 === p2) return 0; if (rank[p2] > rank[p1]) { par[p1] = p2; rank[p2] += rank[p1]; - s.push({ - parent: [...par], - rank: [...rank], - result: res, - highlightedLines: [21, 22, 23], - explanation: `Attach smaller rank tree (${p1}) under larger rank tree (${p2}).`, - variables: { [`par[${p1}]`]: p2, [`rank[${p2}]`]: rank[p2] }, - activeNodes: [p1, p2] - }); + makeSnapshot( + `Union: Attach component ${p1} under root ${p2} because ${p2} has a larger rank.`, + `SET par[${p1}] = ${p2}`, + 18, 16, 20, 30, [p1, p2], { p1, p2 } + ); } else { par[p2] = p1; rank[p1] += rank[p2]; - s.push({ - parent: [...par], - rank: [...rank], - result: res, - highlightedLines: [24, 25, 26], - explanation: `Attach tree under root ${p1}. Update rank[${p1}] to ${rank[p1]}.`, - variables: { [`par[${p2}]`]: p1, [`rank[${p1}]`]: rank[p1] }, - activeNodes: [p1, p2] - }); + makeSnapshot( + `Union: Attach component ${p2} under root ${p1} because ${p1} has a larger or equal rank.`, + `SET par[${p2}] = ${p1}`, + 21, 19, 23, 33, [p1, p2], { p1, p2 } + ); } return 1; }; for (const [n1, n2] of EDGES) { - s.push({ - parent: [...par], - rank: [...rank], - result: res, - highlightedLines: [29, 30], - explanation: `Process edge: [${n1}, ${n2}].`, - variables: { n1, n2, res }, - activeNodes: [n1, n2] - }); + makeSnapshot(`Process edge [${n1}, ${n2}].`, `FOR edge = [${n1}, ${n2}]`, 26, 23, 10, 11, [n1, n2], { n1, n2 }); - const merged = unionFn(n1, n2); - if (merged) { + const united = unionFn(n1, n2); + if (united) { res -= 1; - s.push({ - parent: [...par], - rank: [...rank], - result: res, - highlightedLines: [31], - explanation: `Merging successful. Decrement component count to ${res}.`, - variables: { res }, - activeNodes: [] - }); + makeSnapshot( + `Union successful. Decrement the component count to ${res}.`, + `SET res = res - 1 → ${res}`, + 27, 24, 11, 12, [], {} + ); + } else { + makeSnapshot( + `Nodes ${n1} and ${n2} are already in the same component. Component count remains ${res}.`, + `No decrement`, + 15, 13, 18, 28, [], {} + ); } } - s.push({ - parent: [...par], - rank: [...rank], - result: res, - highlightedLines: [34], - explanation: `Algorithm finished. Total connected components: ${res}.`, - variables: { res }, - activeNodes: [] - }); + makeSnapshot( + `Graph traversal completed. Total connected components: ${res}.`, + `RETURN res → ${res}`, + 29, 25, 13, 14, [], {}, true + ); - return s; + return { steps: s, stepLineNumbers: stepLines }; }, []); - const step = steps[currentStep] || steps[0]; + const handleReset = () => { + setCurrentStepIndex(0); + }; + + const step = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); const computePositions = useMemo(() => { const children: Record = {}; @@ -283,89 +368,113 @@ export const NumberOfConnectedComponentsVisualization = () => { const getNodeStyle = (i: number) => { if (step.activeNodes.includes(i)) return { fill: '#3b82f622', stroke: '#3b82f6', text: '#3b82f6' }; if (step.parent[i] === i) return { fill: '#10b98111', stroke: '#10b981', text: '#10b981' }; - return { fill: '#1e1e1e', stroke: '#444444', text: '#eeeeee' }; + return { fill: 'transparent', stroke: '#444444', text: '#eeeeee' }; }; return ( -
- -

Connected Components Forest

- - - {step.parent.map((p, i) => { - if (p === i) return null; - const src = computePositions[i]; - const dst = computePositions[p]; - const active = step.activeNodes.includes(i) || step.activeNodes.includes(p); - return ( - +

Connected Components Forest

+ + + {step.parent.map((p, i) => { + if (p === i) return null; + const src = computePositions[i]; + const dst = computePositions[p]; + const active = step.activeNodes.includes(i) || step.activeNodes.includes(p); + return ( + + ); + })} + + {Array.from({ length: N }, (_, i) => i).map(i => { + const { x, y } = computePositions[i]; + const s = getNodeStyle(i); + const isActive = step.activeNodes.includes(i); + const isRoot = step.parent[i] === i; + const r = isActive ? 21 : 18; + + return ( + + {isRoot && ROOT} + - ); - })} - - {Array.from({ length: N }, (_, i) => i).map(i => { - const { x, y } = computePositions[i]; - const s = getNodeStyle(i); - const isActive = step.activeNodes.includes(i); - const isRoot = step.parent[i] === i; - const r = isActive ? 21 : 18; - - return ( - - {isRoot && ROOT} - - {i} - - ); - })} - - -
- Root - Active - Node + {i} + + ); + })} + + +
+ Root + Active + Node +
+ + + {/* Commentary Panel */} + +
+
+
+ + + + + + Algorithm Commentary + +
+
+ Step {currentStepIndex + 1} of {steps.length} +
+
+ +
+
+ +
+
+

+ Current Action +

+
+ {step.explanation} +
+
- -
- -
- -
-

Logic

-

{step.explanation}

- -
- -
- -
+
+
+ +
} rightContent={ - } controls={ } /> diff --git a/src/components/visualizations/algorithms/NumberOfIslandsVisualization.tsx b/src/components/visualizations/algorithms/NumberOfIslandsVisualization.tsx index 9eaedce..280b707 100644 --- a/src/components/visualizations/algorithms/NumberOfIslandsVisualization.tsx +++ b/src/components/visualizations/algorithms/NumberOfIslandsVisualization.tsx @@ -1,9 +1,10 @@ -import { useState, useMemo } from 'react'; +import React, { useState, useMemo } from 'react'; import { Card } from '@/components/ui/card'; -import { VisualizationLayout } from '../shared/VisualizationLayout'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import { VisualizationLayout } from '../shared/VisualizationLayout'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; import { Info, CheckCircle2, Droplets } from 'lucide-react'; interface Step { @@ -12,22 +13,19 @@ interface Step { count: number; grid: string[][]; originalGrid: string[][]; - message: string; - lineNumber: number; + explanation: string; isMatch?: boolean; + pseudoStep: string; } -export const NumberOfIslandsVisualization = () => { - const [currentStepIndex, setCurrentStepIndex] = useState(0); - - const code = `function numIslands(grid: string[][]): number { +const languages: VisualizationLanguageMap = { + typescript: `function numIslands(grid: string[][]): number { if (grid.length === 0) return 0; - let count = 0; const rows = grid.length; const cols = grid[0].length; - + let count = 0; function dfs(r: number, c: number) { - if (r < 0 || c < 0 || r >= rows || c >= cols || grid[r][c] === '0') { + if (r < 0 || r >= rows || c < 0 || c >= cols || grid[r][c] === '0') { return; } grid[r][c] = '0'; @@ -36,7 +34,6 @@ export const NumberOfIslandsVisualization = () => { dfs(r, c + 1); dfs(r, c - 1); } - for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { if (grid[r][c] === '1') { @@ -46,94 +43,219 @@ export const NumberOfIslandsVisualization = () => { } } return count; -}`; +}`, + + python: `def numIslands(grid: list[list[str]]) -> int: + if not grid: + return 0 + rows = len(grid) + cols = len(grid[0]) + count = 0 + def dfs(r, c): + if r < 0 or r >= rows or c < 0 or c >= cols or grid[r][c] == '0': + return + grid[r][c] = '0' + dfs(r + 1, c) + dfs(r - 1, c) + dfs(r, c + 1) + dfs(r, c - 1) + for r in range(rows): + for c in range(cols): + if grid[r][c] == '1': + count += 1 + dfs(r, c) + return count`, + + java: `public static class Solution { + public int numIslands(char[][] grid) { + if (grid == null || grid.length == 0) return 0; + int rows = grid.length; + int cols = grid[0].length; + int count = 0; + for (int r = 0; r < rows; r++) { + for (int c = 0; c < cols; c++) { + if (grid[r][c] == '1') { + count++; + dfs(r, c, grid, rows, cols); + } + } + } + return count; + } + private void dfs(int r, int c, char[][] grid, int rows, int cols) { + if (r < 0 || r >= rows || c < 0 || c >= cols || grid[r][c] == '0') { + return; + } + grid[r][c] = '0'; + dfs(r + 1, c, grid, rows, cols); + dfs(r - 1, c, grid, rows, cols); + dfs(r, c + 1, grid, rows, cols); + dfs(r, c - 1, grid, rows, cols); + } +}`, + + cpp: `class Solution { +public: + int numIslands(vector>& grid) { + if (grid.empty()) return 0; + int rows = grid.size(); + int cols = grid[0].size(); + int count = 0; + for (int r = 0; r < rows; r++) { + for (int c = 0; c < cols; c++) { + if (grid[r][c] == '1') { + count++; + dfs(r, c, grid, rows, cols); + } + } + } + return count; + } +private: + void dfs(int r, int c, vector>& grid, int rows, int cols) { + if (r < 0 || r >= rows || c < 0 || c >= cols || grid[r][c] == '0') { + return; + } + grid[r][c] = '0'; + dfs(r + 1, c, grid, rows, cols); + dfs(r - 1, c, grid, rows, cols); + dfs(r, c + 1, grid, rows, cols); + dfs(r, c - 1, grid, rows, cols); + } +};` +}; + +export const NumberOfIslandsVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + + const { steps, stepLineNumbers } = useMemo(() => { + const stepsList: Step[] = []; + const stepLines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLines.typescript!.push(ts); + stepLines.python!.push(py); + stepLines.java!.push(java); + stepLines.cpp!.push(cpp); + }; - const steps: Step[] = useMemo(() => { - const s: Step[] = []; const gridRaw = [ ['1', '1', '0'], ['0', '0', '0'], ['0', '0', '1'] ]; - // Copy for mutating during sim const grid = gridRaw.map(r => [...r]); - // Copy for visualization to track what is original land vs water vs submerged const originalGrid = gridRaw.map(r => [...r]); let count = 0; const rows = grid.length; const cols = grid[0].length; - const snapshot = (msg: string, line: number, r: number | null, c: number | null, isMatch: boolean = false) => { - s.push({ - r, c, count, - grid: grid.map(row => [...row]), - originalGrid, - message: msg, - lineNumber: line, - isMatch + const makeSnapshot = (msg: string, pseudo: string, ts: number, py: number, java: number, cpp: number, r: number | null, c: number | null, isMatch: boolean = false) => { + stepsList.push({ + r, c, count, + grid: grid.map(row => [...row]), + originalGrid, + explanation: msg, + isMatch, + pseudoStep: pseudo }); + addLines(ts, py, java, cpp); }; - snapshot("Start Number of Islands algorithm.", 2, null, null); - snapshot("Initialize count to 0.", 3, null, null); + // Step 1: Start + makeSnapshot("Start scanning grid for islands. Check if the grid is empty.", "START numIslands()", 2, 2, 3, 4, null, null); + makeSnapshot("Initialize count to 0, representing zero islands found so far.", "SET count = 0", 5, 6, 6, 7, null, null); function dfs(r: number, c: number) { - snapshot(`[DFS] Evaluate condition: bounds valid? cell === '1'?`, 8, r, c); - + makeSnapshot( + `DFS: Check if (${r}, ${c}) is within bounds and is a land cell ('1').`, + `IF outOfBounds OR grid[${r}][${c}] == '0'`, + 7, 8, 18, 20, r, c + ); + if (r < 0 || c < 0 || r >= rows || c >= cols || grid[r][c] === '0') { - snapshot(`[DFS] Condition failed (out of bounds or water). Return.`, 9, r, c); + makeSnapshot( + `DFS: Cell (${r}, ${c}) is either out of bounds or water. Return.`, + "RETURN", + 8, 9, 19, 21, r, c + ); return; } - + grid[r][c] = '0'; - snapshot(`[DFS] Submerge land at (${r}, ${c}) to '0' to avoid loops.`, 11, r, c, true); + makeSnapshot( + `DFS: Mark (${r}, ${c}) as visited by sinking it (change to '0').`, + `SET grid[${r}][${c}] = '0'`, + 10, 10, 21, 23, r, c, true + ); - snapshot(`[DFS] Explore Down from (${r}, ${c})`, 12, r, c); + makeSnapshot(`DFS: Explore cell below (${r + 1}, ${c}).`, `CALL dfs(${r + 1}, ${c})`, 11, 11, 22, 24, r, c); dfs(r + 1, c); - - snapshot(`[DFS] Explore Up from (${r}, ${c})`, 13, r, c); + + makeSnapshot(`DFS: Explore cell above (${r - 1}, ${c}).`, `CALL dfs(${r - 1}, ${c})`, 12, 12, 23, 25, r, c); dfs(r - 1, c); - - snapshot(`[DFS] Explore Right from (${r}, ${c})`, 14, r, c); + + makeSnapshot(`DFS: Explore cell to the right (${r}, ${c + 1}).`, `CALL dfs(${r}, ${c + 1})`, 13, 13, 24, 26, r, c); dfs(r, c + 1); - - snapshot(`[DFS] Explore Left from (${r}, ${c})`, 15, r, c); + + makeSnapshot(`DFS: Explore cell to the left (${r}, ${c - 1}).`, `CALL dfs(${r}, ${c - 1})`, 14, 14, 25, 27, r, c); dfs(r, c - 1); } - snapshot("Start scanning the grid row by row.", 18, null, null); for (let r = 0; r < rows; r++) { - snapshot(`Outer Loop: Scanning row r=${r}`, 18, r, null); + makeSnapshot(`Outer Loop: Scan row ${r}.`, `FOR r = ${r}`, 16, 15, 7, 8, r, null); for (let c = 0; c < cols; c++) { - snapshot(`Inner Loop: Visiting cell c=${c}`, 19, r, c); - + makeSnapshot(`Inner Loop: Visiting cell (${r}, ${c}).`, `FOR c = ${c}`, 17, 16, 8, 9, r, c); + const isLand = grid[r][c] === '1'; - snapshot(`Evaluate: is grid[${r}][${c}] === '1'? ${isLand}`, 20, r, c); - + makeSnapshot( + `Check if cell (${r}, ${c}) is unvisited land ('1').`, + `IF grid[${r}][${c}] == '1' → ${isLand ? "YES ✓" : "NO ✗"}`, + 18, 17, 9, 10, r, c + ); + if (isLand) { count++; - snapshot(`Found unexplored land '1' at (${r}, ${c})! Incrementing count to ${count}.`, 21, r, c, true); - - snapshot(`Initiating recursive DFS sink flow for this island.`, 22, r, c, true); + makeSnapshot( + `New island detected! Increment island count to ${count}.`, + `SET count = ${count}`, + 19, 18, 10, 11, r, c, true + ); + + makeSnapshot( + `Call DFS starting from (${r}, ${c}) to submerge all adjacent land cells.`, + `CALL dfs(${r}, ${c})`, + 20, 19, 11, 12, r, c, true + ); dfs(r, c); } } } - snapshot(`Execution Complete! Total distinct islands: ${count}`, 26, null, null, true); + makeSnapshot(`Finished scanning. Total islands found: ${count}.`, "RETURN count", 24, 20, 15, 16, null, null, true); - return s; + return { steps: stepsList, stepLineNumbers: stepLines }; }, []); + const handleReset = () => { + setCurrentStepIndex(0); + }; + const step = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( - -

- Number of Islands + +

+ Number of Islands Grid

@@ -142,21 +264,26 @@ export const NumberOfIslandsVisualization = () => { {step?.originalGrid && step.originalGrid.map((row, r) => (
{row.map((val, c) => { - const currentVal = step.grid[r][c]; // '1' or '0' + const currentVal = step.grid[r][c]; const isCurrent = step.r === r && step.c === c; const isOriginalLand = val === '1'; - // Submerged means it was 1 but now 0 const isSubmerged = isOriginalLand && currentVal === '0'; const isActiveLand = currentVal === '1'; return (
- {currentVal} + {currentVal}
- [{r},{c}] + [{r},{c}]
); })} @@ -166,52 +293,71 @@ export const NumberOfIslandsVisualization = () => {
-
-
-
Land (1) +
+
+
Land ('1')
-
-
Water (0) +
+
Water ('0')
-
-
Sunk / Visited +
+
Visited / Sunk
- -
-
- {step?.isMatch ? : } + {/* Commentary Panel */} + +
+
+
+ + + + + + Algorithm Commentary + +
+
+ Step {currentStepIndex + 1} of {steps.length} +
-
-

- Step Logic -

-

- {step?.message || ''} -

+ +
+
+ {step?.isMatch ? : } +
+
+

+ Current Action +

+
+ {step?.explanation || ''} +
+
-
- } - rightContent={ -
- +
} + rightContent={ + + } controls={ { - const [currentStepIndex, setCurrentStepIndex] = useState(0); - - const code = `function pacificAtlantic(heights: number[][]): number[][] { +const languages: VisualizationLanguageMap = { + typescript: `function pacificAtlantic(heights: number[][]): number[][] { const ROWS = heights.length; const COLS = heights[0].length; const pac = new Set(); const atl = new Set(); - function dfs(r: number, c: number, visit: Set, prevHeight: number) { const key = \`\${r},\${c}\`; if (visit.has(key) || r < 0 || c < 0 || r >= ROWS || c >= COLS || heights[r][c] < prevHeight) { @@ -37,17 +35,14 @@ export const PacificAtlanticVisualization = () => { dfs(r, c + 1, visit, heights[r][c]); dfs(r, c - 1, visit, heights[r][c]); } - for (let c = 0; c < COLS; c++) { dfs(0, c, pac, heights[0][c]); dfs(ROWS - 1, c, atl, heights[ROWS - 1][c]); } - for (let r = 0; r < ROWS; r++) { dfs(r, 0, pac, heights[r][0]); dfs(r, COLS - 1, atl, heights[r][COLS - 1]); } - const res: number[][] = []; for (let r = 0; r < ROWS; r++) { for (let c = 0; c < COLS; c++) { @@ -58,48 +53,179 @@ export const PacificAtlanticVisualization = () => { } } return res; -}`; +}`, - const heights = [[2, 1], [1, 2]]; + python: `def pacificAtlantic(heights: list[list[int]]) -> list[list[int]]: + ROWS = len(heights) + COLS = len(heights[0]) + pac = set() + atl = set() + def dfs(r, c, visit, prevHeight): + if (r, c) in visit or r < 0 or c < 0 or r >= ROWS or c >= COLS or heights[r][c] < prevHeight: + return + visit.add((r, c)) + dfs(r + 1, c, visit, heights[r][c]) + dfs(r - 1, c, visit, heights[r][c]) + dfs(r, c + 1, visit, heights[r][c]) + dfs(r, c - 1, visit, heights[r][c]) + for c in range(COLS): + dfs(0, c, pac, heights[0][c]) + dfs(ROWS - 1, c, atl, heights[ROWS - 1][c]) + for r in range(ROWS): + dfs(r, 0, pac, heights[r][0]) + dfs(r, COLS - 1, atl, heights[r][COLS - 1]) + res = [] + for r in range(ROWS): + for c in range(COLS): + if (r, c) in pac and (r, c) in atl: + res.append([r, c]) + return res`, + + java: `public static class Solution { + public List> pacificAtlantic(int[][] heights) { + if (heights == null || heights.length == 0) return new ArrayList<>(); + int ROWS = heights.length; + int COLS = heights[0].length; + Set pac = new HashSet<>(); + Set atl = new HashSet<>(); + for (int c = 0; c < COLS; c++) { + dfs(0, c, pac, heights, heights[0][c], ROWS, COLS); + dfs(ROWS - 1, c, atl, heights, heights[ROWS - 1][c], ROWS, COLS); + } + for (int r = 0; r < ROWS; r++) { + dfs(r, 0, pac, heights, heights[r][0], ROWS, COLS); + dfs(r, COLS - 1, atl, heights, heights[r][COLS - 1], ROWS, COLS); + } + List> result = new ArrayList<>(); + for (int r = 0; r < ROWS; r++) { + for (int c = 0; c < COLS; c++) { + String key = r + "," + c; + if (pac.contains(key) && atl.contains(key)) { + result.add(Arrays.asList(r, c)); + } + } + } + return result; + } + private void dfs(int r, int c, Set visit, int[][] heights, int prevHeight, int ROWS, int COLS) { + if (r < 0 || c < 0 || r >= ROWS || c >= COLS) return; + String key = r + "," + c; + if (visit.contains(key) || heights[r][c] < prevHeight) return; + visit.add(key); + dfs(r + 1, c, visit, heights, heights[r][c], ROWS, COLS); + dfs(r - 1, c, visit, heights, heights[r][c], ROWS, COLS); + dfs(r, c + 1, visit, heights, heights[r][c], ROWS, COLS); + dfs(r, c - 1, visit, heights, heights[r][c], ROWS, COLS); + } +}`, + + cpp: `class Solution { +public: + vector> pacificAtlantic(vector>& heights) { + if (heights.empty()) return {}; + int ROWS = heights.size(); + int COLS = heights[0].size(); + unordered_set pac; + unordered_set atl; + for (int c = 0; c < COLS; c++) { + dfs(0, c, pac, heights, heights[0][c], ROWS, COLS); + dfs(ROWS - 1, c, atl, heights, heights[ROWS - 1][c], ROWS, COLS); + } + for (int r = 0; r < ROWS; r++) { + dfs(r, 0, pac, heights, heights[r][0], ROWS, COLS); + dfs(r, COLS - 1, atl, heights, heights[r][COLS - 1], ROWS, COLS); + } + vector> result; + for (int r = 0; r < ROWS; r++) { + for (int c = 0; c < COLS; c++) { + string key = to_string(r) + "," + to_string(c); + if (pac.count(key) && atl.count(key)) { + result.push_back({r, c}); + } + } + } + return result; + } +private: + void dfs(int r, int c, unordered_set& visit, vector>& heights, int prevHeight, int ROWS, int COLS) { + string key = to_string(r) + "," + to_string(c); + if (r < 0 || c < 0 || r >= ROWS || c >= COLS || visit.count(key) || heights[r][c] < prevHeight) { + return; + } + visit.insert(key); + dfs(r + 1, c, visit, heights, heights[r][c], ROWS, COLS); + dfs(r - 1, c, visit, heights, heights[r][c], ROWS, COLS); + dfs(r, c + 1, visit, heights, heights[r][c], ROWS, COLS); + dfs(r, c - 1, visit, heights, heights[r][c], ROWS, COLS); + } +};` +}; + +export const PacificAtlanticVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + + const heights = useMemo(() => [[2, 1], [1, 2]], []); + + const { steps, stepLineNumbers } = useMemo(() => { + const stepsList: Step[] = []; + const stepLines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLines.typescript!.push(ts); + stepLines.python!.push(py); + stepLines.java!.push(java); + stepLines.cpp!.push(cpp); + }; - const steps: Step[] = useMemo(() => { - const s: Step[] = []; const ROWS = heights.length; const COLS = heights[0].length; const pac = new Set(); const atl = new Set(); const res: number[][] = []; - const snapshot = (msg: string, line: number, r: number | null, c: number | null, isMatch: boolean = false) => { - s.push({ + const snapshot = (msg: string, pseudo: string, ts: number, py: number, java: number, cpp: number, r: number | null, c: number | null, isMatch: boolean = false) => { + stepsList.push({ r, c, pac: Array.from(pac), atl: Array.from(atl), - message: msg, - lineNumber: line, + explanation: msg, res: [...res.map(x => [...x])], - isMatch + isMatch, + pseudoStep: pseudo }); + addLines(ts, py, java, cpp); }; - snapshot("Initialize ROWS=2, COLS=2", 2, null, null); - snapshot("Initialize Pacific and Atlantic sets.", 4, null, null); + snapshot("Initialize dimensions ROWS = 2, COLS = 2.", "SET ROWS=2, COLS=2", 2, 2, 4, 5, null, null); + snapshot("Initialize sets pac and atl to keep track of reachable cells.", "SET pac = {}, atl = {}", 4, 4, 6, 7, null, null); function dfs(r: number, c: number, visitSet: 'pac' | 'atl', prevHeight: number) { const visit = visitSet === 'pac' ? pac : atl; const key = `${r},${c}`; - if (r < 0 || c < 0 || r >= ROWS || c >= COLS) return; // Silent skip for strict bounds - if (visit.has(key)) return; // Silent skip for visited to keep steps low + if (r < 0 || c < 0 || r >= ROWS || c >= COLS) return; + if (visit.has(key)) return; - // Meaningful check snapshot: when water can't flow uphill if (heights[r][c] < prevHeight) { - snapshot(`Height ${heights[r][c]} < ${prevHeight}. Water can't flow uphill in reverse. Stop bounds at (${r}, ${c}).`, 9, r, c); + snapshot( + `Water cannot flow uphill in reverse. Height at (${r}, ${c}) is ${heights[r][c]}, which is less than ${prevHeight}.`, + `IF heights[${r}][${c}] < prevHeight → ${heights[r][c]} < ${prevHeight} → YES ✗`, + 8, 7, 30, 31, r, c + ); return; } visit.add(key); - snapshot(`Passed checks! Added (${r}, ${c}) to ${visitSet === 'pac' ? 'Pacific' : 'Atlantic'} ocean flow trace.`, 12, r, c, true); + snapshot( + `Added (${r}, ${c}) to ${visitSet === 'pac' ? 'Pacific' : 'Atlantic'} reachable set.`, + `visit.add((${r}, ${c}))`, + 11, 9, 31, 34, r, c, true + ); dfs(r + 1, c, visitSet, heights[r][c]); dfs(r - 1, c, visitSet, heights[r][c]); @@ -107,49 +233,69 @@ export const PacificAtlanticVisualization = () => { dfs(r, c - 1, visitSet, heights[r][c]); } - snapshot("Iterate columns for top/bottom DFS starts.", 19, null, null); + // Iterate columns (top & bottom edges) for (let c = 0; c < COLS; c++) { - snapshot(`Start Pacific DFS from top row (0, ${c})`, 20, 0, c); + snapshot(`Process column ${c} for top/bottom edge starts.`, `FOR c = ${c}`, 17, 14, 8, 9, null, null); + + snapshot(`Start Pacific DFS from top edge (0, ${c})`, `CALL dfs(0, ${c}, Pacific)`, 18, 15, 9, 10, 0, c); dfs(0, c, 'pac', heights[0][c]); - snapshot(`Start Atlantic DFS from bottom row (${ROWS - 1}, ${c})`, 21, ROWS - 1, c); + snapshot(`Start Atlantic DFS from bottom edge (${ROWS - 1}, ${c})`, `CALL dfs(${ROWS - 1}, ${c}, Atlantic)`, 19, 16, 10, 11, ROWS - 1, c); dfs(ROWS - 1, c, 'atl', heights[ROWS - 1][c]); } - snapshot("Iterate rows for left/right DFS starts.", 24, null, null); + // Iterate rows (left & right edges) for (let r = 0; r < ROWS; r++) { - snapshot(`Start Pacific DFS from left col (${r}, 0)`, 25, r, 0); + snapshot(`Process row ${r} for left/right edge starts.`, `FOR r = ${r}`, 21, 17, 12, 13, null, null); + + snapshot(`Start Pacific DFS from left edge (${r}, 0)`, `CALL dfs(${r}, 0, Pacific)`, 22, 18, 13, 14, r, 0); dfs(r, 0, 'pac', heights[r][0]); - snapshot(`Start Atlantic DFS from right col (${r}, ${COLS - 1})`, 26, r, COLS - 1); + snapshot(`Start Atlantic DFS from right edge (${r}, ${COLS - 1})`, `CALL dfs(${r}, ${COLS - 1}, Atlantic)`, 23, 19, 14, 15, r, COLS - 1); dfs(r, COLS - 1, 'atl', heights[r][COLS - 1]); } - snapshot("Initialize result array.", 29, null, null); + snapshot("Initialize result array. Iterate to find intersections.", "SET res = []", 25, 20, 16, 17, null, null); for (let r = 0; r < ROWS; r++) { for (let c = 0; c < COLS; c++) { const key = `${r},${c}`; - snapshot(`Check intersection for cell (${r}, ${c})`, 33, r, c); - if (pac.has(key) && atl.has(key)) { + const hasBoth = pac.has(key) && atl.has(key); + + snapshot( + `Check if (${r}, ${c}) is in both pac and atl sets.`, + `IF (${r}, ${c}) in pac AND atl → ${hasBoth ? "YES ✓" : "NO ✗"}`, + 29, 23, 20, 21, r, c + ); + + if (hasBoth) { res.push([r, c]); - snapshot(`Cell (${r}, ${c}) flows to BOTH! Adding to res.`, 34, r, c, true); + snapshot( + `Cell (${r}, ${c}) can flow to both oceans. Add to results.`, + `res.push([${r}, ${c}])`, + 30, 24, 21, 22, r, c, true + ); } } } - snapshot(`Execution complete! Returned ${JSON.stringify(res)}`, 38, null, null, true); + snapshot(`Completed. Return list of cells: ${JSON.stringify(res)}`, "RETURN res", 34, 25, 25, 26, null, null, true); + + return { steps: stepsList, stepLineNumbers: stepLines }; + }, [heights]); - return s; - }, []); + const handleReset = () => { + setCurrentStepIndex(0); + }; const step = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( - -

+ +

Grid Water Flow Simulation

@@ -167,7 +313,6 @@ export const PacificAtlanticVisualization = () => {
{row.map((val, c) => { const key = `${r},${c}`; - // Explicit safety check, fall back to empty array if step isn't initialized yet const pacSet = step?.pac || []; const atlSet = step?.atl || []; @@ -179,9 +324,16 @@ export const PacificAtlanticVisualization = () => { return (
- {val} + {val}
[{r},{c}]
@@ -193,54 +345,73 @@ export const PacificAtlanticVisualization = () => {

-
-
-
Pacific +
+
+
Pacific
-
-
Atlantic +
+
Atlantic
-
-
Both +
+
Both
- -
-
- {step?.isMatch ? : } + {/* Commentary Panel */} + +
+
+
+ + + + + + Algorithm Commentary + +
+
+ Step {currentStepIndex + 1} of {steps.length} +
-
-

- Step Logic -

-

- {step?.message || ''} -

+ +
+
+ {step?.isMatch ? : } +
+
+

+ Current Action +

+
+ {step?.explanation || ''} +
+
-
- } - rightContent={ -
- +
} + rightContent={ + + } controls={ { - const [currentStep, setCurrentStep] = useState(0); - const [activeTestCase, setActiveTestCase] = useState(0); - - const code = `function canPartition(nums: number[]): boolean { - const total = nums.reduce((sum, num) => sum + num, 0); - - if (total % 2 !== 0) { - return false; +const languages: VisualizationLanguageMap = { + typescript: `function canPartition(nums: number[]): boolean { + const total = nums.reduce((a, b) => a + b, 0); + if (total % 2 !== 0) return false; + let dp = new Set(); + dp.add(0); + const target = total / 2; + for (let i = nums.length - 1; i >= 0; i--) { + const nextDP = new Set(); + for (const t of dp) { + if (t + nums[i] === target) { + return true; + } + nextDP.add(t + nums[i]); + nextDP.add(t); + } + dp = nextDP; } + return dp.has(target); +}`, - const target = total / 2; - const dp: boolean[] = new Array(target + 1).fill(false); - dp[0] = true; + python: `def canPartition(nums: list[int]) -> bool: + total = sum(nums) + if total % 2 != 0: + return False + dp = {0} + target = total / 2 + for i in range(len(nums) - 1, -1, -1): + next_dp = set() + for t in dp: + if t + nums[i] == target: + return True + next_dp.add(t + nums[i]) + next_dp.add(t) + dp = next_dp + return target in dp`, - for (const num of nums) { - for (let s = target; s >= num; s--) { - dp[s] = dp[s] || dp[s - num]; + java: `public static class Solution { + public boolean canPartition(int[] nums) { + int total = 0; + for (int num : nums) { + total += num; + } + if (total % 2 != 0) return false; + Set dp = new HashSet<>(); + dp.add(0); + int target = total / 2; + for (int i = nums.length - 1; i >= 0; i--) { + Set nextDP = new HashSet<>(); + for (int t : dp) { + if (t + nums[i] == target) { + return true; + } + nextDP.add(t + nums[i]); + nextDP.add(t); + } + dp = nextDP; + } + return dp.contains(target); } - } +}`, - return dp[target]; -}`; + cpp: `class Solution { +public: + bool canPartition(vector& nums) { + int total = 0; + for (int num : nums) { + total += num; + } + if (total % 2 != 0) return false; + unordered_set dp; + dp.insert(0); + int target = total / 2; + for (int i = nums.size() - 1; i >= 0; i--) { + unordered_set nextDP; + for (int t : dp) { + if (t + nums[i] == target) { + return true; + } + nextDP.insert(t + nums[i]); + nextDP.insert(t); + } + dp = nextDP; + } + return dp.count(target); + } +};` +}; - const testCases = [ - { id: 'case1', name: 'Example 1 (Possible)', nums: [1, 5, 11, 5] }, - { id: 'case2', name: 'Example 2 (Odd Sum)', nums: [1, 2, 3, 5] } - ]; +function generateVisualizationData() { + const nums = [3, 2, 2, 4, 5]; + const total = 16; + const target = 8; - const steps = useMemo(() => { - const stepsList: Step[] = []; - const nums = testCases[activeTestCase].nums; - const total = nums.reduce((a, b) => a + b, 0); + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - stepsList.push({ - dp: [], nums, currentNumIdx: -1, currentSum: null, target: null, totalSum: total, - message: `First, we calculate the total sum of all numbers. The sum of [${nums.join(', ')}] is ${total}.`, - highlightedLines: [2] - }); + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - if (total % 2 !== 0) { - stepsList.push({ - dp: [], nums, currentNumIdx: -1, currentSum: null, target: null, totalSum: total, - message: `The total sum ${total} is an odd number. It's mathematically impossible to divide an odd sum into two equal integer halves. We instantly return false.`, - highlightedLines: [4, 5] - }); - return stepsList; - } + let dp = new Set(); + dp.add(0); - const target = total / 2; - stepsList.push({ - dp: [], nums, currentNumIdx: -1, currentSum: null, target, totalSum: total, - message: `The total sum is even (${total}). We need to find if ANY subset of numbers adds up to exactly exactly half of it (${target}). If we can form ${target}, the remaining numbers naturally form the other half!`, - highlightedLines: [8] - }); + steps.push({ + dp: Array.from(dp), + currentNumIdx: -1, + currentVal: null, + nextDp: [], + target, + totalSum: total, + explanation: 'Sum of nums: 3+2+2+4+5 = 16. Sum is even. Target subset sum is 16 / 2 = 8.', + pseudoStep: `START canPartition() -> target = ${target}`, + }); + addLines(2, 2, 3, 4); - const dp = new Array(target + 1).fill(false); - dp[0] = true; + steps.push({ + dp: Array.from(dp), + currentNumIdx: -1, + currentVal: null, + nextDp: [], + target, + totalSum: total, + explanation: 'Initialize reachable sums Set (dp) with 0. A sum of 0 is always possible by taking no items.', + pseudoStep: 'SET dp = {0}', + }); + addLines(5, 5, 9, 10); - stepsList.push({ - dp: [...dp], nums, currentNumIdx: -1, currentSum: null, target, totalSum: total, - message: `We initialize a Dynamic Programming (DP) array up to our target (${target}). We set dp[0] to true, because a sum of 0 is always achievable by picking no numbers.`, - highlightedLines: [9, 10] - }); + for (let i = nums.length - 1; i >= 0; i--) { + const num = nums[i]; + const nextDP = new Set(); - for (let idx = 0; idx < nums.length; idx++) { - const num = nums[idx]; - - stepsList.push({ - dp: [...dp], nums, currentNumIdx: idx, currentSum: null, target, totalSum: total, - message: `We now pick the number ${num}. We will iterate backwards from our target down to ${num} to see what new sums we can form by adding ${num} to previously reachable sums.`, - highlightedLines: [12] - }); + steps.push({ + dp: Array.from(dp), + currentNumIdx: i, + currentVal: num, + nextDp: [], + target, + totalSum: total, + explanation: `Examine num = ${num} at index ${i}. Create nextDP Set to accumulate new sums.`, + pseudoStep: `FOR i = ${i} (num = ${num})`, + }); + addLines(7, 7, 11, 12); - for (let s = target; s >= num; s--) { - const canReachWithoutNum = dp[s]; - const canReachWithNum = dp[s - num]; - - stepsList.push({ - dp: [...dp], nums, currentNumIdx: idx, currentSum: s, target, totalSum: total, - message: `Checking if we can form sum ${s}... We look back at dp[${s} - ${num}] (which is dp[${s - num}]). Is it reachable?`, - highlightedLines: [13, 14] + let foundTarget = false; + for (const t of dp) { + const sumWithNum = t + num; + if (sumWithNum === target) { + foundTarget = true; + nextDP.add(sumWithNum); + nextDP.add(t); + steps.push({ + dp: Array.from(dp), + currentNumIdx: i, + currentVal: num, + nextDp: Array.from(nextDP), + target, + totalSum: total, + explanation: `Add current number ${num} to sum ${t} -> ${sumWithNum}. This matches our target ${target}!`, + pseudoStep: `IF ${t} + ${num} == ${target} → YES ✓`, }); - - if (canReachWithNum && !canReachWithoutNum) { - dp[s] = true; - stepsList.push({ - dp: [...dp], nums, currentNumIdx: idx, currentSum: s, target, totalSum: total, - message: `Yes! We previously formed the sum ${s - num}. By adding our current number ${num}, we can now reach the sum ${s}! We update dp[${s}] to true.`, - highlightedLines: [14] - }); - } else if (canReachWithoutNum) { - stepsList.push({ - dp: [...dp], nums, currentNumIdx: idx, currentSum: s, target, totalSum: total, - message: `The sum ${s} was already reachable from previous numbers. We just keep it true.`, - highlightedLines: [14] - }); - } + addLines(11, 11, 15, 15); + break; + } else { + nextDP.add(sumWithNum); + nextDP.add(t); + steps.push({ + dp: Array.from(dp), + currentNumIdx: i, + currentVal: num, + nextDp: Array.from(nextDP), + target, + totalSum: total, + explanation: `Add ${num} to sum ${t} -> ${sumWithNum} (not target). Add both ${sumWithNum} and ${t} to nextDP.`, + pseudoStep: `ADD ${sumWithNum} and ${t} to nextDP`, + }); + addLines(13, 12, 17, 18); } } - stepsList.push({ - dp: [...dp], nums, currentNumIdx: -1, currentSum: null, target, totalSum: total, - message: `We've considered all numbers. We check our final goal: dp[${target}]. It is ${dp[target]}, so we return ${dp[target]}!`, - highlightedLines: [18] - }); - - return stepsList; - }, [activeTestCase]); - - const step = steps[currentStep]; + dp = nextDP; + if (foundTarget) { + break; + } - const renderDPArray = () => { - if (step.dp.length === 0) return ( -
- DP array not yet initialized -
- ); + steps.push({ + dp: Array.from(dp), + currentNumIdx: i, + currentVal: num, + nextDp: [], + target, + totalSum: total, + explanation: `Update dp Set with nextDP values: {${Array.from(dp).join(', ')}}.`, + pseudoStep: 'SET dp = nextDP', + }); + addLines(16, 14, 20, 21); + } - return ( -
- {step.dp.map((isReachable, idx) => { - const currentNum = step.currentNumIdx >= 0 ? step.nums[step.currentNumIdx] : null; - const isCurrentSum = step.currentSum === idx; - const isLookbackSum = step.currentSum !== null && currentNum !== null && idx === step.currentSum - currentNum; - - let borderColor = "border-border"; - let bgColor = "bg-muted"; - let textColor = "text-muted-foreground"; - let label = "F"; + if (dp.has(target)) { + steps.push({ + dp: Array.from(dp), + currentNumIdx: -1, + currentVal: null, + nextDp: [], + target, + totalSum: total, + explanation: `LCS subset partition target ${target} found! Return true.`, + pseudoStep: 'RETURN true', + }); + addLines(18, 15, 22, 23); + } else { + steps.push({ + dp: Array.from(dp), + currentNumIdx: -1, + currentVal: null, + nextDp: [], + target, + totalSum: total, + explanation: `Target subset sum ${target} was not reachable. Return false.`, + pseudoStep: 'RETURN false', + }); + addLines(18, 15, 22, 23); + } - if (isReachable) { - borderColor = "border-green-500/50"; - bgColor = "bg-green-500/10"; - textColor = "text-green-500"; - label = "T"; - } + return { steps, stepLineNumbers }; +} - if (isCurrentSum) { - borderColor = "border-primary ring-2 ring-primary ring-offset-2"; - } else if (isLookbackSum) { - borderColor = "border-amber-500 ring-2 ring-amber-500 ring-offset-1"; - textColor = "text-amber-600 dark:text-amber-400 font-bold"; - } +export const PartitionEqualSubsetVisualization: React.FC = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); - return ( - - {idx} - - {label} - - - ); - })} -
- ); + const handleReset = () => { + setCurrentStepIndex(0); }; + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map((s) => s.pseudoStep); + const numsList = [3, 2, 2, 4, 5]; + return ( - - - -
- {testCases.map((tc, idx) => ( - - ))} -
-
- } - leftContent={ -
-
-

- - Partition Equal Subset Sum -

- -
- - - DP Array State - - {step.target !== null && ( - Target: {step.target} - )} +
+ + +
+ {/* Left Column: Visual State */} +
+
+
+

+ Input Numbers +

+
+ {numsList.map((num, idx) => { + const isCurrent = idx === currentStep.currentNumIdx; + return ( +
+ {num} + i={idx} +
+ ); + })}
- {renderDPArray()} - -
-
-
- Reachable (True) +
+ +
+
+

+ Current reachable sums (dp) +

+
+ {currentStep.dp.map((sum) => ( +
+ {sum} +
+ ))}
-
-
- Unreachable (False) +
+ +
+

+ Updated sums (nextDp) +

+
+ {currentStep.nextDp.length > 0 ? ( + currentStep.nextDp.map((sum) => ( +
+ {sum} +
+ )) + ) : ( + - + )}
+
+
+
+ + {/* Commentary Panel */} +
+
+
-
- Current Target Sum (s) + + + + + + Algorithm Commentary +
-
-
- Lookback Sum (s - num) +
+ Step {currentStepIndex + 1} of {steps.length}
- -
-
- -

Commentary

-

- {step.message} -

-
- - -

- Input Numbers -

-
- {step.nums.map((num, idx) => { - const isCurrent = step.currentNumIdx === idx; - const isProcessed = step.currentNumIdx > idx; - return ( - - {num} - - ); - })} +
+
+
- +
+

+ Current Action +

+
+ {currentStep.explanation} +
+
+
+
= 0 ? step.nums[step.currentNumIdx] : "N/A", - "current sum check (s)": step.currentSum !== null ? step.currentSum : "N/A", - "lookback sum (s - num)": (step.currentSum !== null && step.currentNumIdx >= 0) ? step.currentSum - step.nums[step.currentNumIdx] : "N/A", + i: currentStep.currentNumIdx !== -1 ? currentStep.currentNumIdx : '-', + num: currentStep.currentVal !== null ? currentStep.currentVal : '-', + target: currentStep.target, + total_sum: currentStep.totalSum }} />
- } - rightContent={ - - + - - } - /> +
+
+
); }; + +export default PartitionEqualSubsetVisualization; diff --git a/src/components/visualizations/algorithms/PermutationsVisualization.tsx b/src/components/visualizations/algorithms/PermutationsVisualization.tsx index b52ae74..b9e94d6 100644 --- a/src/components/visualizations/algorithms/PermutationsVisualization.tsx +++ b/src/components/visualizations/algorithms/PermutationsVisualization.tsx @@ -1,8 +1,8 @@ -import React, { useEffect, useRef, useState } from 'react'; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; -import { StepControls } from '../shared/StepControls'; -import { VariablePanel } from '../shared/VariablePanel'; +import React, { useState, useEffect, useRef } from "react"; +import { StepControls } from "../shared/StepControls"; +import { VariablePanel } from "../shared/VariablePanel"; +import { VisualizationCodePanel } from "../shared/VisualizationCodePanel"; +import type { StepLineNumberMap, VisualizationLanguageMap } from "@/types/visualization"; interface Step { nums: number[]; @@ -10,135 +10,216 @@ interface Step { perms: number[][]; result: number[][]; message: string; - lineNumber: number; + pseudoStep: string; } -export const PermutationsVisualization: React.FC = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1000); - const intervalRef = useRef(null); - - const code = `function permute(nums: number[]): number[][] { +const languages: VisualizationLanguageMap = { + typescript: `function permute(nums: number[]): number[][] { const result: number[][] = []; - if (nums.length === 1) { return [nums.slice()]; } - for (let i = 0; i < nums.length; i++) { const n = nums.shift()!; - const perms = permute(nums); - for (const perm of perms) { perm.push(n); } - result.push(...perms); - nums.push(n); } - return result; -}`; +}`, + python: `def permute(nums): + result = [] + if len(nums) == 1: + return [nums[:]] + for i in range(len(nums)): + n = nums.pop(0) + perms = permute(nums) + for perm in perms: + perm.append(n) + result.extend(perms) + nums.append(n) + return result`, + java: `public static class Solution { + public List> permute(int[] nums) { + List> result = new ArrayList<>(); + if (nums.length == 1) { + List baseList = new ArrayList<>(); + baseList.add(nums[0]); + List> baseResult = new ArrayList<>(); + baseResult.add(new ArrayList<>(baseList)); + return baseResult; + } + for (int i = 0; i < nums.length; i++) { + int[] remainingNums = new int[nums.length - 1]; + int index = 0; + for (int j = 0; j < nums.length; j++) { + if (i != j) { + remainingNums[index++] = nums[j]; + } + } + List> subPermutations = permute(remainingNums); + for (List subPermutation : subPermutations) { + subPermutation.add(nums[i]); + } + result.addAll(subPermutations); + } + return result; + } +}`, + cpp: `class Solution { +public: + vector> permute(vector& nums) { + vector> result; + if (nums.size() == 1) { + return {nums}; + } + for (int i = 0; i < nums.size(); i++) { + vector remainingNums; + for (int j = 0; j < nums.size(); j++) { + if (i != j) { + remainingNums.push_back(nums[j]); + } + } + vector> subPermutations = permute(remainingNums); + for (auto &perm : subPermutations) { + perm.push_back(nums[i]); + } + result.insert(result.end(), subPermutations.begin(), subPermutations.end()); + } + return result; + } +};` +}; - const generateSteps = () => { +export const PermutationsVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); + + const generateStepsData = () => { const originalArr = [1, 2, 3]; - const newSteps: Step[] = []; + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; function runPermute(numsRef: number[]) { function simulatePermute(currentNums: number[]): number[][] { const result: number[][] = []; - newSteps.push({ + steps.push({ nums: [...currentNums], n: null, perms: [], result: [...result], message: `Call permute([${currentNums.join(', ')}])`, - lineNumber: 1 + pseudoStep: `CALL permute(nums = [${currentNums.join(', ')}])` }); + addLines(1, 1, 2, 3); - newSteps.push({ + steps.push({ nums: [...currentNums], n: null, perms: [], result: [...result], message: `Initialize frame result array`, - lineNumber: 2 + pseudoStep: 'SET result = []' }); + addLines(2, 2, 3, 4); - if (currentNums.length === 1) { - newSteps.push({ - nums: [...currentNums], n: null, perms: [], result: [...result], - message: `Base case: nums.length === 1`, - lineNumber: 4 - }); + steps.push({ + nums: [...currentNums], n: null, perms: [], result: [...result], + message: `Check base case: nums.length === 1`, + pseudoStep: `IF nums.length === 1 → ${currentNums.length === 1 ? 'YES ✓' : 'NO ✗'}` + }); + addLines(3, 3, 4, 5); + if (currentNums.length === 1) { const baseReturn = [currentNums.slice()]; - newSteps.push({ + steps.push({ nums: [...currentNums], n: null, perms: [], result: [...result], - message: `Return base copy: [[${currentNums[0]}]]`, - lineNumber: 5 + message: `Base case met! Return copy [[${currentNums[0]}]]`, + pseudoStep: `RETURN [[${currentNums[0]}]]` }); + addLines(4, 4, 9, 6); return baseReturn; } const initialLen = currentNums.length; for (let i = 0; i < initialLen; i++) { - newSteps.push({ + steps.push({ nums: [...currentNums], n: null, perms: [], result: [...result], - message: `Iterate i=${i} / ${initialLen} over nums`, - lineNumber: 8 + message: `Iterate i = ${i} over nums`, + pseudoStep: `FOR i = ${i} to ${initialLen - 1}` }); + addLines(6, 5, 11, 8); const n = currentNums.shift()!; - newSteps.push({ + steps.push({ nums: [...currentNums], n, perms: [], result: [...result], - message: `Shift out head element n=${n}. nums is now: [${currentNums.join(', ')}]`, - lineNumber: 9 + message: `Shift out head element n = ${n}. Remaining: [${currentNums.join(', ')}]`, + pseudoStep: `SET n = nums.shift() → n = ${n}` }); + addLines(7, 6, 16, 13); - newSteps.push({ + steps.push({ nums: [...currentNums], n, perms: [], result: [...result], message: `Recursively calculate perms for remaining [${currentNums.join(', ')}]`, - lineNumber: 11 + pseudoStep: `CALL permute([${currentNums.join(', ')}])` }); + addLines(8, 7, 19, 15); const perms = simulatePermute(currentNums); - newSteps.push({ + steps.push({ nums: [...currentNums], n, perms: perms.map(p => [...p]), result: [...result], - message: `Received child perms. Appending n=${n} to each.`, - lineNumber: 13 + message: `Received child perms. Appending n = ${n} to each.`, + pseudoStep: `FOR each perm IN perms` }); + addLines(9, 8, 20, 16); for (const perm of perms) { perm.push(n); } - newSteps.push({ + steps.push({ nums: [...currentNums], n, perms: perms.map(p => [...p]), result: [...result], - message: `Appended n=${n} to permutations.`, - lineNumber: 14 + message: `Appended n = ${n} to permutations.`, + pseudoStep: `APPEND n (${n}) to perm` }); + addLines(10, 9, 21, 17); result.push(...perms); - newSteps.push({ + steps.push({ nums: [...currentNums], n, perms: perms.map(p => [...p]), result: [...result], message: `Pushed merged perms into current frame result array.`, - lineNumber: 17 + pseudoStep: `PUSH perms to result` }); + addLines(12, 10, 23, 19); currentNums.push(n); - newSteps.push({ + steps.push({ nums: [...currentNums], n: null, perms: [], result: [...result], - message: `Restore n=${n} back to tail of nums array: [${currentNums.join(', ')}]`, - lineNumber: 19 + message: `Restore n = ${n} back to tail of nums: [${currentNums.join(', ')}]`, + pseudoStep: `RESTORE nums.push(n)` }); + addLines(13, 11, 24, 20); } - newSteps.push({ + steps.push({ nums: [...currentNums], n: null, perms: [], result: [...result], message: `Frame loop complete, returning result matrix.`, - lineNumber: 22 + pseudoStep: 'RETURN result' }); + addLines(15, 12, 25, 21); return result; } @@ -147,24 +228,22 @@ export const PermutationsVisualization: React.FC = () => { runPermute(originalArr); - // Final cap step - const lastStep = newSteps[newSteps.length - 1]; - newSteps.push({ + const lastStep = steps[steps.length - 1]; + steps.push({ ...lastStep, message: 'Algorithm Complete!', - lineNumber: 22, + pseudoStep: 'DONE' }); + addLines(15, 12, 25, 21); - setSteps(newSteps); + return { steps, stepLineNumbers }; }; - useEffect(() => { - generateSteps(); - }, []); + const { steps, stepLineNumbers } = generateStepsData(); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { - intervalRef.current = window.setInterval(() => { + intervalRef.current = setInterval(() => { setCurrentStepIndex((prev) => { if (prev >= steps.length - 1) { setIsPlaying(false); @@ -172,7 +251,7 @@ export const PermutationsVisualization: React.FC = () => { } return prev + 1; }); - }, speed); + }, 1000 / speed); } else { if (intervalRef.current) { clearInterval(intervalRef.current); @@ -201,6 +280,7 @@ export const PermutationsVisualization: React.FC = () => { if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map((s) => s.pseudoStep); return (
@@ -212,68 +292,75 @@ export const PermutationsVisualization: React.FC = () => { onReset={handleReset} isPlaying={isPlaying} currentStep={currentStepIndex} - totalSteps={steps.length} + totalSteps={steps.length - 1} speed={speed} onSpeedChange={setSpeed} />
- -
-

Frame nums

-
- {currentStep.nums.length > 0 ? ( - currentStep.nums.map((val, idx) => ( -
- {val} +
+
+

Frame nums

+
+ {currentStep.nums.length > 0 ? ( + currentStep.nums.map((val, idx) => ( +
+ {val} +
+ )) + ) : ( +
Empty Array
+ )} +
+ +

Extracted 'n'

+
+ {currentStep.n !== null ? ( +
+ {currentStep.n}
- )) - ) : ( -
Empty Array
- )} -
- -

Extracted 'n'

-
- {currentStep.n !== null ? ( -
- {currentStep.n} -
- ) : ( -
None
- )} + ) : ( +
None
+ )} +
+ +

Frame Result ({currentStep.result.length})

+
+ {currentStep.result.map((perm, idx) => ( +
+ [{perm.join(', ')}] +
+ ))} +
-

Frame Result ({currentStep.result.length})

-
- {currentStep.result.map((perm, idx) => ( -
- [{perm.join(', ')}] -
- ))} +
+

Algorithm Logic

+

+ {currentStep.message} +

-
-

{currentStep.message}

-
-
- -
+
- +
- -
); }; diff --git a/src/components/visualizations/algorithms/PrefixSumVisualization.tsx b/src/components/visualizations/algorithms/PrefixSumVisualization.tsx index b5dd3e5..4efe90d 100644 --- a/src/components/visualizations/algorithms/PrefixSumVisualization.tsx +++ b/src/components/visualizations/algorithms/PrefixSumVisualization.tsx @@ -1,121 +1,179 @@ import { useEffect, useRef, useState } from 'react'; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import { motion, AnimatePresence } from 'framer-motion'; +import { Info } from 'lucide-react'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { originalArray: number[]; prefixArray: number[]; currentIndex: number; - sum: number; - message: string; - lineNumber: number; + sum: number | string; + explanation: string; + pseudoStep: string; + variables: Record; } -export const PrefixSumVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const intervalRef = useRef(null); +// ─── DB Codes (no comments, exact match) ──────────────────────────────── - const code = `function prefixSum(arr: number[]): number[] { - const prefix = [arr[0]]; - - for (let i = 1; i < arr.length; i++) { - prefix[i] = prefix[i - 1] + arr[i]; +const languages: VisualizationLanguageMap = { + typescript: `function prefixSum(nums: number[]): number[] { + if (!nums || nums.length === 0) { + return []; + } + const prefix: number[] = new Array(nums.length).fill(0); + prefix[0] = nums[0]; + for (let i = 1; i < nums.length; i++) { + prefix[i] = prefix[i - 1] + nums[i]; } - return prefix; -}`; +}`, - const generateSteps = () => { - const array = [3, 1, 4, 1, 5, 9, 2]; - const newSteps: Step[] = []; - const prefix: number[] = []; + python: `def prefix_sum(nums): + n = len(nums) + prefix_sum_array = [0] * n + if n > 0: + prefix_sum_array[0] = nums[0] + for i in range(1, n): + prefix_sum_array[i] = prefix_sum_array[i - 1] + nums[i] + return prefix_sum_array`, - // Line 1: Function entry - newSteps.push({ + java: `public int[] prefixSum(int[] nums) { + if (nums == null || nums.length == 0) { + return new int[0]; + } + int n = nums.length; + int[] prefixSum = new int[n]; + prefixSum[0] = nums[0]; + for (int i = 1; i < n; i++) { + prefixSum[i] = prefixSum[i - 1] + nums[i]; + } + return prefixSum; +}`, + + cpp: `vector prefixSum(const vector& nums) { + if (nums.empty()) { + return {}; + } + vector prefixSums(nums.size(), 0); + prefixSums[0] = nums[0]; + for (size_t i = 1; i < nums.size(); i++) { + prefixSums[i] = prefixSums[i - 1] + nums[i]; + } + return prefixSums; +}` +}; + +// ─── Step generator ────────────────────────────────────────────────────────── + +function generateVisualizationData() { + const array = [3, 1, 4, 1, 5, 9, 2]; + const steps: Step[] = []; + const prefix: number[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + // Initial State + steps.push({ + originalArray: [...array], + prefixArray: [...prefix], + currentIndex: -1, + sum: '-', + explanation: 'Start prefix sum calculation.', + pseudoStep: 'START prefixSum(nums)', + variables: { i: '-', 'arr[i]': '-', 'prefix[i-1]': '-', sum: '-', prefixLength: 0 } + }); + addLines(1, 1, 1, 1); + + prefix.push(array[0]); + steps.push({ + originalArray: [...array], + prefixArray: [...prefix], + currentIndex: 0, + sum: array[0], + explanation: `Initialize the first prefix element: prefix[0] = arr[0] = ${array[0]}.`, + pseudoStep: `SET prefix[0] = arr[0] → ${array[0]}`, + variables: { i: 0, 'arr[i]': array[0], 'prefix[i-1]': '-', sum: array[0], prefixLength: 1 } + }); + addLines(6, 5, 7, 6); + + for (let i = 1; i < array.length; i++) { + steps.push({ originalArray: [...array], - prefixArray: [], - currentIndex: -1, - sum: 0, - message: 'Starting Prefix Sum calculation.', - lineNumber: 1 + prefixArray: [...prefix], + currentIndex: i, + sum: prefix[i - 1], + explanation: `Loop check: i = ${i}. We will compute the prefix sum up to index ${i}.`, + pseudoStep: `FOR i = ${i} to arr.length − 1 → i = ${i} < ${array.length} → YES ✓`, + variables: { i, 'arr[i]': array[i], 'prefix[i-1]': prefix[i - 1], sum: prefix[i - 1], prefixLength: prefix.length } }); + addLines(7, 6, 8, 7); - // Line 2: Initialize prefix with first element - prefix.push(array[0]); - newSteps.push({ + const currentVal = array[i]; + const prevSum = prefix[i - 1]; + const newSum = prevSum + currentVal; + + steps.push({ originalArray: [...array], prefixArray: [...prefix], - currentIndex: 0, - sum: array[0], - message: `Initialize prefix array with the first element: prefix[0] = arr[0] = ${array[0]}.`, - lineNumber: 2 + currentIndex: i, + sum: newSum, + explanation: `Calculate sum: prefix[${i - 1}] (${prevSum}) + arr[${i}] (${currentVal}) = ${newSum}.`, + pseudoStep: `SET prefix[i] = prefix[i-1] + arr[i] → ${prevSum} + ${currentVal} = ${newSum}`, + variables: { i, 'arr[i]': currentVal, 'prefix[i-1]': prevSum, sum: newSum, prefixLength: prefix.length } }); + addLines(8, 7, 9, 8); - for (let i = 1; i < array.length; i++) { - // Line 4: For loop check - newSteps.push({ - originalArray: [...array], - prefixArray: [...prefix], - currentIndex: i, - sum: prefix[i - 1], - message: `Iteration i = ${i}. We will calculate prefix[${i}] using the previous sum.`, - lineNumber: 4 - }); - - const currentVal = array[i]; - const prevSum = prefix[i - 1]; - const newSum = prevSum + currentVal; - - // Line 5: Calculate and assign - newSteps.push({ - originalArray: [...array], - prefixArray: [...prefix], - currentIndex: i, - sum: newSum, - message: `Calculate prefix[${i}]: prefix[${i - 1}] (${prevSum}) + arr[${i}] (${currentVal}) = ${newSum}.`, - lineNumber: 5 - }); - - prefix.push(newSum); - newSteps.push({ - originalArray: [...array], - prefixArray: [...prefix], - currentIndex: i, - sum: newSum, - message: `prefix[${i}] is now stored as ${newSum}.`, - lineNumber: 5 - }); - } - - // Line 8: Return - newSteps.push({ + prefix.push(newSum); + steps.push({ originalArray: [...array], prefixArray: [...prefix], - currentIndex: array.length - 1, - sum: prefix[prefix.length - 1], - message: 'Prefix sum array calculation complete.', - lineNumber: 8 + currentIndex: i, + sum: newSum, + explanation: `Store prefix[${i}] = ${newSum}.`, + pseudoStep: `prefix[${i}] = ${newSum}`, + variables: { i, 'arr[i]': currentVal, 'prefix[i-1]': prevSum, sum: newSum, prefixLength: prefix.length } }); + addLines(8, 7, 9, 8); + } - setSteps(newSteps); - setCurrentStepIndex(0); - }; + steps.push({ + originalArray: [...array], + prefixArray: [...prefix], + currentIndex: array.length - 1, + sum: prefix[prefix.length - 1], + explanation: 'Calculation complete. Return the final prefix sum array.', + pseudoStep: 'RETURN prefix', + variables: { i: '-', 'arr[i]': '-', 'prefix[i-1]': '-', sum: prefix[prefix.length - 1], prefixLength: prefix.length } + }); + addLines(10, 8, 11, 10); - useEffect(() => { - generateSteps(); - }, []); + return { steps, stepLineNumbers }; +} - useEffect(() => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } +// ─── Component ─────────────────────────────────────────────────────────────── + +export const PrefixSumVisualization = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); + useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { intervalRef.current = setInterval(() => { setCurrentStepIndex(prev => { @@ -125,31 +183,29 @@ export const PrefixSumVisualization = () => { } return prev + 1; }); - }, 1000 / speed); + }, 1200 / speed); + } else { + if (intervalRef.current) clearInterval(intervalRef.current); } - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } + if (intervalRef.current) clearInterval(intervalRef.current); }; }, [isPlaying, currentStepIndex, steps.length, speed]); const handlePlay = () => setIsPlaying(true); const handlePause = () => setIsPlaying(false); - const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(prev => prev + 1); - const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(prev => prev - 1); + const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(p => p + 1); + const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(p => p - 1); const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); - generateSteps(); }; if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; const getMaxValue = () => Math.max(...currentStep.originalArray, ...currentStep.prefixArray); + const pseudoSteps = steps.map(s => s.pseudoStep); return (
@@ -167,27 +223,28 @@ export const PrefixSumVisualization = () => { />
+ {/* Left Column: Visual state */}
-
+
-

Original Array

-
+

Original Array

+
{currentStep.originalArray.map((value, index) => { const isActive = index === currentStep.currentIndex; return (
{isActive && ( -
- CURRENT +
+ Current
)} -
+
- + {value}
@@ -197,21 +254,21 @@ export const PrefixSumVisualization = () => {
-

Prefix Sum Array

-
+

Prefix Sum Array

+
{currentStep.originalArray.map((_, index) => { const value = currentStep.prefixArray[index]; const hasValue = value !== undefined; const isActive = index === currentStep.currentIndex; return (
-
+
- + {hasValue ? value : ''}
@@ -221,29 +278,62 @@ export const PrefixSumVisualization = () => {
-
-

{currentStep.message}

-
- + {/* Commentary Panel */} +
+
+
+
+ + + + + + Algorithm Commentary + +
+
+ Step {currentStepIndex + 1} of {steps.length} +
+
-
- = 0 ? currentStep.currentIndex : 'N/A', - 'arr[i]': currentStep.currentIndex >= 0 ? currentStep.originalArray[currentStep.currentIndex] : 'N/A', - 'prefix[i-1]': currentStep.currentIndex > 0 ? currentStep.prefixArray[currentStep.currentIndex - 1] : 'N/A', - sum: currentStep.sum, - prefixLength: currentStep.prefixArray.length - }} - /> +
+
+ +
+
+

+ Current Action +

+ + + {currentStep.explanation} + + +
+
+
-
-
- - +
+ + {/* Right Column: Code & Pseudocode Display */} +
); }; +export default PrefixSumVisualization; diff --git a/src/components/visualizations/algorithms/PrimsVisualization.tsx b/src/components/visualizations/algorithms/PrimsVisualization.tsx index 7f3881e..6da1b9f 100644 --- a/src/components/visualizations/algorithms/PrimsVisualization.tsx +++ b/src/components/visualizations/algorithms/PrimsVisualization.tsx @@ -1,8 +1,8 @@ -import { useEffect, useRef, useState } from "react"; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; -import { StepControls } from "../shared/StepControls"; -import { VariablePanel } from "../shared/VariablePanel"; +import { useEffect, useRef, useState } from 'react'; +import { StepControls } from '../shared/StepControls'; +import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Edge { from: number; @@ -15,262 +15,396 @@ interface Step { edges: Edge[]; mstEdges: Edge[]; currentNode: number | null; - message: string; - lineNumber: number; + minHeap: [number, number][]; + explanation: string; + pseudoStep: string; + variables: Record; } -export const PrimsVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const intervalRef = useRef(null); +const languages: VisualizationLanguageMap = { + typescript: `function minCostConnectPoints(points: number[][]): number { + const N = points.length; + const adj: Map = new Map(); + for (let i = 0; i < N; i++) { + adj.set(i, []); + } + for (let i = 0; i < N; i++) { + const [x1, y1] = points[i]; + for (let j = i + 1; j < N; j++) { + const [x2, y2] = points[j]; + const dist = Math.abs(x1 - x2) + Math.abs(y1 - y2); + adj.get(i)!.push([dist, j]); + adj.get(j)!.push([dist, i]); + } + } + let result = 0; + const visit = new Set(); + const minHeap: [number, number][] = [[0, 0]]; + while (visit.size < N) { + minHeap.sort((a, b) => a[0] - b[0]); + const [cost, node] = minHeap.shift()!; + if (visit.has(node)) continue; + result += cost; + visit.add(node); + for (const [neiCost, nei] of adj.get(node)!) { + if (!visit.has(nei)) { + minHeap.push([neiCost, nei]); + } + } + } + return result; +}`, + python: `def minCostConnectPoints(points): + N = len(points) + def manhattan_distance(p1, p2): + return abs(p1[0] - p2[0]) + abs(p1[1] - p2[1]) + edges = [] + for i in range(N): + for j in range(i + 1, N): + dist = manhattan_distance(points[i], points[j]) + edges.append((dist, i, j)) + edges.sort() + parent = list(range(N)) + rank = [0] * N + def find(i): + if parent[i] == i: + return i + parent[i] = find(parent[i]) + return parent[i] + def union(i, j): + root_i = find(i) + root_j = find(j) + if root_i != root_j: + if rank[root_i] < rank[root_j]: + parent[root_i] = root_j + elif rank[root_i] > rank[root_j]: + parent[root_j] = root_i + else: + parent[root_j] = root_i + rank[root_i] += 1 + return True + return False + min_cost = 0 + num_edges = 0 + for cost, u, v in edges: + if union(u, v): + min_cost += cost + num_edges += 1 + if num_edges == N - 1: + break + return min_cost`, + java: `public static class Solution { + public int minCostConnectPoints(int[][] points) { + int N = points.length; + Map> adj = new HashMap<>(); + for (int i = 0; i < N; i++) { + adj.put(i, new ArrayList<>()); + } + for (int i = 0; i < N; i++) { + int x1 = points[i][0]; + int y1 = points[i][1]; + for (int j = i + 1; j < N; j++) { + int x2 = points[j][0]; + int y2 = points[j][1]; + int dist = Math.abs(x1 - x2) + Math.abs(y1 - y2); + adj.get(i).add(new int[]{dist, j}); + adj.get(j).add(new int[]{dist, i}); + } + } + int result = 0; + Set visit = new HashSet<>(); + PriorityQueue minHeap = new PriorityQueue<>(Comparator.comparingInt(a -> a[0])); + minHeap.offer(new int[]{0, 0}); + while (visit.size() < N) { + int[] curr = minHeap.poll(); + int cost = curr[0]; + int node = curr[1]; + if (visit.contains(node)) continue; + result += cost; + visit.add(node); + for (int[] neighbor : adj.get(node)) { + int neiCost = neighbor[0]; + int nei = neighbor[1]; + if (!visit.contains(nei)) { + minHeap.offer(new int[]{neiCost, nei}); + } + } + } + return result; + } +}`, + cpp: `class Solution { +public: + int minCostConnectPoints(vector>& points) { + int N = points.size(); + unordered_map>> adj; + for (int i = 0; i < N; i++) { + adj[i] = {}; + } + for (int i = 0; i < N; i++) { + int x1 = points[i][0]; + int y1 = points[i][1]; + for (int j = i + 1; j < N; j++) { + int x2 = points[j][0]; + int y2 = points[j][1]; + int dist = abs(x1 - x2) + abs(y1 - y2); + adj[i].push_back({dist, j}); + adj[j].push_back({dist, i}); + } + } + int result = 0; + unordered_set visit; + priority_queue< + pair, + vector>, + greater> + > minHeap; + minHeap.push({0, 0}); + while (visit.size() < N) { + auto [cost, node] = minHeap.top(); + minHeap.pop(); + if (visit.count(node)) continue; + result += cost; + visit.insert(node); + for (auto &[neiCost, nei] : adj[node]) { + if (!visit.count(nei)) { + minHeap.push({neiCost, nei}); + } + } + } + return result; + } +};`, +}; - const code = `function minCostConnectPoints(points: number[][]): number { +function generateVisualizationData() { + const points = [ + [0, 0], + [2, 2], + [3, 10], + [5, 2], + [7, 0], + ]; const N = points.length; + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { typescript: [], python: [], java: [], cpp: [] }; + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; const adj: Map = new Map(); - for (let i = 0; i < N; i++) { adj.set(i, []); } + steps.push({ + visited: Array(N).fill(false), + edges: [], + mstEdges: [], + currentNode: null, + minHeap: [], + explanation: "Initialize an empty adjacency list mapping each node to its neighbors.", + pseudoStep: "SET adj = empty Map()", + variables: { result: 0, visitedCount: 0 } + }); + addLines(3, 5, 4, 5); + + const allEdges: Edge[] = []; for (let i = 0; i < N; i++) { const [x1, y1] = points[i]; - for (let j = i + 1; j < N; j++) { const [x2, y2] = points[j]; - const dist = Math.abs(x1 - x2) + Math.abs(y1 - y2); - adj.get(i)!.push([dist, j]); adj.get(j)!.push([dist, i]); + allEdges.push({ from: i, to: j, weight: dist }); } } + steps.push({ + visited: Array(N).fill(false), + edges: [...allEdges], + mstEdges: [], + currentNode: null, + minHeap: [], + explanation: `Build the graph: compute Manhattan distances between all pairs of nodes. Total edges calculated: ${allEdges.length}.`, + pseudoStep: "CALL buildGraph()", + variables: { result: 0, visitedCount: 0 } + }); + addLines(7, 6, 8, 9); + let result = 0; const visit = new Set(); - const minHeap: [number, number][] = [[0, 0]]; + const mstEdges: Edge[] = []; + + steps.push({ + visited: Array(N).fill(false), + edges: [...allEdges], + mstEdges: [], + currentNode: null, + minHeap: [...minHeap], + explanation: "Initialize result to 0, visited set, and insert starting node 0 with cost 0 into the min-heap.", + pseudoStep: "SET result = 0; visit = {}; minHeap = [[0, 0]]", + variables: { result, visitedCount: 0, minHeap: "[[0, 0]]" } + }); + addLines(16, 31, 19, 20); + + const edgeSource: Record = {}; while (visit.size < N) { + steps.push({ + visited: Array.from({ length: N }, (_, idx) => visit.has(idx)), + edges: [...allEdges], + mstEdges: [...mstEdges], + currentNode: null, + minHeap: [...minHeap].sort((a, b) => a[0] - b[0]), + explanation: `Check if visited count (${visit.size}) is less than total nodes (${N}). Sort heap to find min edge.`, + pseudoStep: "WHILE visit.size < N", + variables: { result, visitedCount: visit.size, minHeap: `[${minHeap.map(h => `[${h[0]},${h[1]}]`).join(', ')}]` } + }); + addLines(19, 33, 23, 28); + minHeap.sort((a, b) => a[0] - b[0]); const [cost, node] = minHeap.shift()!; - if (visit.has(node)) continue; - - result += cost; - visit.add(node); - - for (const [neiCost, nei] of adj.get(node)!) { - if (!visit.has(nei)) { - minHeap.push([neiCost, nei]); - } - } - } - - return result; -} -`; - - const generateSteps = () => { - const points = [ - [0, 0], - [2, 2], - [3, 10], - [5, 2], - [7, 0], - ]; - const N = points.length; - const newSteps: Step[] = []; - - const adj: Map = new Map(); - for (let i = 0; i < N; i++) { - adj.set(i, []); - } + steps.push({ + visited: Array.from({ length: N }, (_, idx) => visit.has(idx)), + edges: [...allEdges], + mstEdges: [...mstEdges], + currentNode: node, + minHeap: [...minHeap], + explanation: `Extract the edge with minimum cost from heap: node ${node} with cost ${cost}.`, + pseudoStep: `SET [cost, node] = minHeap.pop() → [${cost}, ${node}]`, + variables: { result, visitedCount: visit.size, cost, node, minHeap: `[${minHeap.map(h => `[${h[0]},${h[1]}]`).join(', ')}]` } + }); + addLines(21, 33, 24, 30); - newSteps.push({ - visited: Array(N).fill(false), - edges: [], - mstEdges: [], - currentNode: null, - message: "Initialize adjacency list", - lineNumber: 6, + steps.push({ + visited: Array.from({ length: N }, (_, idx) => visit.has(idx)), + edges: [...allEdges], + mstEdges: [...mstEdges], + currentNode: node, + minHeap: [...minHeap], + explanation: `Check if node ${node} is already visited.`, + pseudoStep: `IF node ${node} IN visit → ${visit.has(node) ? 'YES ✓' : 'NO ✗'}`, + variables: { result, visitedCount: visit.size, node, isVisited: String(visit.has(node)) } }); + addLines(22, 34, 27, 31); - const allEdges: Edge[] = []; - for (let i = 0; i < N; i++) { - const [x1, y1] = points[i]; - for (let j = i + 1; j < N; j++) { - const [x2, y2] = points[j]; - const dist = Math.abs(x1 - x2) + Math.abs(y1 - y2); - adj.get(i)!.push([dist, j]); - adj.get(j)!.push([dist, i]); - allEdges.push({ from: i, to: j, weight: dist }); - } + if (visit.has(node)) { + continue; } - newSteps.push({ - visited: Array(N).fill(false), - edges: [...allEdges], - mstEdges: [], - currentNode: null, - message: "Graph built with Manhattan distances", - lineNumber: 19, - }); + if (cost > 0) { + const fromNode = edgeSource[node]; + mstEdges.push({ from: fromNode, to: node, weight: cost }); + } - let result = 0; - const visit = new Set(); - const minHeap: [number, number, number | null][] = [[0, 0, null]]; // internally keep parent - const mstEdges: Edge[] = []; + result += cost; + visit.add(node); - newSteps.push({ - visited: Array(N).fill(false), + steps.push({ + visited: Array.from({ length: N }, (_, idx) => visit.has(idx)), edges: [...allEdges], - mstEdges: [], - currentNode: 0, - message: "Start Prim's from node 0 (cost 0)", - lineNumber: 25, + mstEdges: [...mstEdges], + currentNode: node, + minHeap: [...minHeap], + explanation: `Add cost ${cost} to total cost. Mark node ${node} as visited. result is now ${result}.`, + pseudoStep: `result += ${cost}; visit.add(${node})`, + variables: { result, visitedCount: visit.size, node } }); + addLines(23, 35, 28, 32); - while (visit.size < N && minHeap.length > 0) { - newSteps.push({ - visited: Array(N) - .fill(false) - .map((_, i) => visit.has(i)), - edges: [...allEdges], - mstEdges: [...mstEdges], - currentNode: null, - message: "Check if all nodes are visited", - lineNumber: 27, - }); - - minHeap.sort((a, b) => a[0] - b[0]); - const [cost, node, parent] = minHeap.shift()!; - - newSteps.push({ - visited: Array(N) - .fill(false) - .map((_, i) => visit.has(i)), + const neighbors = adj.get(node) || []; + for (const [neiCost, nei] of neighbors) { + steps.push({ + visited: Array.from({ length: N }, (_, idx) => visit.has(idx)), edges: [...allEdges], mstEdges: [...mstEdges], currentNode: node, - message: `Extract minimum: node ${node} with cost ${cost}`, - lineNumber: 29, + minHeap: [...minHeap], + explanation: `Inspect neighbor ${nei} of node ${node} (edge cost: ${neiCost}). Check if visited.`, + pseudoStep: `FOR [neiCost, nei] OF adj[${node}]: check if nei ${nei} in visit`, + variables: { result, visitedCount: visit.size, node, nei, neiCost } }); + addLines(25, 33, 30, 34); - if (visit.has(node)) { - newSteps.push({ - visited: Array(N) - .fill(false) - .map((_, i) => visit.has(i)), + if (!visit.has(nei)) { + minHeap.push([neiCost, nei]); + edgeSource[nei] = node; + steps.push({ + visited: Array.from({ length: N }, (_, idx) => visit.has(idx)), edges: [...allEdges], mstEdges: [...mstEdges], currentNode: node, - message: `Node ${node} already visited, skipping`, - lineNumber: 31, + minHeap: [...minHeap], + explanation: `Neighbor ${nei} not visited. Push edge to neighbor (${node} - ${nei}) with weight ${neiCost} into min-heap.`, + pseudoStep: `minHeap.push([${neiCost}, ${nei}])`, + variables: { result, visitedCount: visit.size, node, nei, neiCost } }); - continue; - } - - result += cost; - if (parent !== null) { - mstEdges.push({ from: parent, to: node, weight: cost }); - } - visit.add(node); - - newSteps.push({ - visited: Array(N) - .fill(false) - .map((_, i) => visit.has(i)), - edges: [...allEdges], - mstEdges: [...mstEdges], - currentNode: node, - message: `Add node ${node} to MST (total cost: ${result})`, - lineNumber: 34, - }); - - for (const [neiCost, nei] of adj.get(node)!) { - if (!visit.has(nei)) { - minHeap.push([neiCost, nei, node]); - newSteps.push({ - visited: Array(N) - .fill(false) - .map((_, i) => visit.has(i)), - edges: [...allEdges], - mstEdges: [...mstEdges], - currentNode: nei, - message: `Push neighbor ${nei} with cost ${neiCost} to min-heap`, - lineNumber: 38, - }); - } + addLines(27, 33, 34, 36); } } + } - newSteps.push({ - visited: Array(N) - .fill(false) - .map((_, i) => visit.has(i)), - edges: [...allEdges], - mstEdges: [...mstEdges], - currentNode: null, - message: `Minimum Spanning Tree complete! Total cost: ${result}`, - lineNumber: 43, - }); + steps.push({ + visited: Array(N).fill(true), + edges: [...allEdges], + mstEdges: [...mstEdges], + currentNode: null, + minHeap: [], + explanation: `All nodes visited. Prim's algorithm completed. Total MST weight is ${result}.`, + pseudoStep: `RETURN result → ${result}`, + variables: { result, visitedCount: N } + }); + addLines(31, 39, 38, 40); + + return { steps, stepLineNumbers }; +} - setSteps(newSteps); - setCurrentStepIndex(0); - }; +const nodePositions = [ + { x: 50, y: 230 }, + { x: 150, y: 190 }, + { x: 200, y: 30 }, + { x: 300, y: 190 }, + { x: 350, y: 230 }, +]; - useEffect(() => { - generateSteps(); - }, []); +export const PrimsVisualization = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { intervalRef.current = setInterval(() => { - setCurrentStepIndex((prev) => { - if (prev >= steps.length - 1) { - setIsPlaying(false); - return prev; - } + setCurrentStepIndex(prev => { + if (prev >= steps.length - 1) { setIsPlaying(false); return prev; } return prev + 1; }); }, 1000 / speed); } else { if (intervalRef.current) clearInterval(intervalRef.current); } - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - }; + return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [isPlaying, currentStepIndex, steps.length, speed]); const handlePlay = () => setIsPlaying(true); const handlePause = () => setIsPlaying(false); - const handleStepForward = () => - currentStepIndex < steps.length - 1 && - setCurrentStepIndex((prev) => prev + 1); - const handleStepBack = () => - currentStepIndex > 0 && setCurrentStepIndex((prev) => prev - 1); - const handleReset = () => { - setCurrentStepIndex(0); - setIsPlaying(false); - generateSteps(); - }; + const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(p => p + 1); + const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(p => p - 1); + const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); }; if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; - - // Points mapped to SVG coordinates for visualization - // points = [[0,0], [2,2], [3,10], [5,2], [7,0]] - const nodePositions = [ - { x: 50, y: 230 }, // [0,0] - { x: 150, y: 190 }, // [2,2] - { x: 200, y: 30 }, // [3,10] - { x: 300, y: 190 }, // [5,2] - { x: 350, y: 230 }, // [7,0] - ]; + const pseudoSteps = steps.map(s => s.pseudoStep); return (
@@ -289,19 +423,13 @@ export const PrimsVisualization = () => {
-
- +
+ {currentStep.edges.map((edge, idx) => { const from = nodePositions[edge.from]; const to = nodePositions[edge.to]; const inMST = currentStep.mstEdges.some( - (e) => - (e.from === edge.from && e.to === edge.to) || - (e.from === edge.to && e.to === edge.from), + (e) => (e.from === edge.from && e.to === edge.to) || (e.from === edge.to && e.to === edge.from) ); return ( @@ -311,16 +439,14 @@ export const PrimsVisualization = () => { y1={from.y} x2={to.x} y2={to.y} - className={`transition-all duration-300 ${inMST ? "stroke-green-500" : "stroke-muted-foreground/50" - }`} - strokeWidth={inMST ? 3 : 1} - strokeDasharray={inMST ? "0" : "4"} + className={`transition-all duration-200 ${inMST ? 'stroke-green-500 stroke-3' : 'stroke-muted-foreground/30 stroke-1'}`} + strokeDasharray={inMST ? '0' : '4'} /> {inMST && ( {edge.weight} @@ -330,80 +456,74 @@ export const PrimsVisualization = () => { ); })} - {nodePositions.map((pos, idx) => ( - - - - {idx} - - - ( - {[ - [0, 0], - [2, 2], - [3, 10], - [5, 2], - [7, 0], - ][idx].join(",")} - ) - - - ))} + {nodePositions.map((pos, idx) => { + const isVisited = currentStep.visited[idx]; + const isCurrent = currentStep.currentNode === idx; + + return ( + + + + {idx} + + + ({[ [0,0], [2,2], [3,10], [5,2], [7,0] ][idx].join(',')}) + + + ); + })} + +
+ In MST / Visited + Current Node + Unvisited Node +
-

- {currentStep.message} -

-
-
- v).length, - "MST Edges": currentStep.mstEdges.length, - "Total Weight": currentStep.mstEdges.reduce( - (sum, e) => sum + e.weight, - 0, - ), - }} - /> +

{currentStep.explanation}

-
-
- v).length, + "MST Edges": currentStep.mstEdges.length, + "Total Weight": currentStep.mstEdges.reduce((sum, e) => sum + e.weight, 0), + "Min Heap": `[${currentStep.minHeap.map(h => `[${h[0]},${h[1]}]`).join(', ')}]` + }} />
+ +
); diff --git a/src/components/visualizations/algorithms/ProductOfArrayExceptSelfVisualization.tsx b/src/components/visualizations/algorithms/ProductOfArrayExceptSelfVisualization.tsx index a416271..69e5cfe 100644 --- a/src/components/visualizations/algorithms/ProductOfArrayExceptSelfVisualization.tsx +++ b/src/components/visualizations/algorithms/ProductOfArrayExceptSelfVisualization.tsx @@ -3,7 +3,8 @@ import { Card } from '@/components/ui/card'; import { VisualizationLayout } from '../shared/VisualizationLayout'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { array: number[]; @@ -11,205 +12,323 @@ interface Step { highlights: number[]; variables: Record; explanation: string; - highlightedLine: number; + pseudoStep: string; } +const languages: VisualizationLanguageMap = { + typescript: `function productExceptSelf(nums: number[]): number[] { + const n = nums.length; + const result = new Array(n).fill(1); + let leftProduct = 1; + for (let i = 0; i < n; i++) { + result[i] = leftProduct; + leftProduct *= nums[i]; + } + let rightProduct = 1; + for (let i = n - 1; i >= 0; i--) { + result[i] *= rightProduct; + rightProduct *= nums[i]; + } + return result; +}`, + python: `def productExceptSelf(nums: List[int]) -> List[int]: + n = len(nums) + result = [1] * n + left_product = 1 + for i in range(n): + result[i] = left_product + left_product *= nums[i] + right_product = 1 + for i in range(n - 1, -1, -1): + result[i] *= right_product + right_product *= nums[i] + return result`, + java: `public static class Solution { + public int[] productExceptSelf(int[] nums) { + int n = nums.length; + int[] result = new int[n]; + for (int i = 0; i < n; i++) { + result[i] = 1; + } + int leftProduct = 1; + for (int i = 0; i < n; i++) { + result[i] = leftProduct; + leftProduct *= nums[i]; + } + int rightProduct = 1; + for (int i = n - 1; i >= 0; i--) { + result[i] *= rightProduct; + rightProduct *= nums[i]; + } + return result; + } +}`, + cpp: `class Solution { +public: + vector productExceptSelf(vector& nums) { + int n = nums.size(); + vector result(n, 1); + int leftProduct = 1; + for (int i = 0; i < n; i++) { + result[i] = leftProduct; + leftProduct *= nums[i]; + } + int rightProduct = 1; + for (int i = n - 1; i >= 0; i--) { + result[i] *= rightProduct; + rightProduct *= nums[i]; + } + return result; + } +};` +}; + export const ProductOfArrayExceptSelfVisualization = () => { const [steps, setSteps] = useState([]); + const [stepLineNumbers, setStepLineNumbers] = useState({ + typescript: [], + python: [], + java: [], + cpp: [] + }); const [currentStepIndex, setCurrentStepIndex] = useState(0); - const code = `function productExceptSelf(nums: number[]): number[] { - const n = nums.length; - const result = new Array(n).fill(1); - let leftProduct = 1; - for (let i = 0; i < n; i++) { - result[i] = leftProduct; - leftProduct *= nums[i]; - } - let rightProduct = 1; - for (let i = n - 1; i >= 0; i--) { - result[i] *= rightProduct; - rightProduct *= nums[i]; - } - return result; -}`; - - const generateSteps = () => { + useEffect(() => { const array = [1, 2, 3, 4]; const n = array.length; - const newSteps: Step[] = []; + const generatedSteps: Step[] = []; + const stepLines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLines.typescript!.push(ts); + stepLines.python!.push(py); + stepLines.java!.push(java); + stepLines.cpp!.push(cpp); + }; let result = [1, 1, 1, 1]; let leftProduct = 1; let rightProduct = 1; - // Line 2 - newSteps.push({ - array, result: [...result], - highlights: [], variables: { i: '-', leftProduct: '-', rightProduct: '-' }, - explanation: `Get length of array: n = ${n}`, - highlightedLine: 2 + // Get length + generatedSteps.push({ + array, + result: [...result], + highlights: [], + variables: { i: '-', leftProduct: '-', rightProduct: '-' }, + explanation: `Get the length of the array: n = ${n}`, + pseudoStep: `SET n = nums.length` }); + addLines(2, 2, 3, 4); - // Line 3 - newSteps.push({ - array, result: [...result], - highlights: [], variables: { i: '-', leftProduct: '-', rightProduct: '-' }, + // Initialize result + generatedSteps.push({ + array, + result: [...result], + highlights: [], + variables: { i: '-', leftProduct: '-', rightProduct: '-' }, explanation: `Initialize result array of size ${n} with 1s.`, - highlightedLine: 3 + pseudoStep: `SET result = [1, 1, 1, 1]` }); + addLines(3, 3, 4, 5); - // Line 4 - newSteps.push({ - array, result: [...result], - highlights: [], variables: { i: '-', leftProduct, rightProduct: '-' }, - explanation: `Initialize leftProduct to 1.`, - highlightedLine: 4 + // Initialize leftProduct + generatedSteps.push({ + array, + result: [...result], + highlights: [], + variables: { i: '-', leftProduct, rightProduct: '-' }, + explanation: `Initialize leftProduct variable to 1.`, + pseudoStep: `SET leftProduct = 1` }); + addLines(4, 4, 8, 6); for (let i = 0; i < n; i++) { - // Line 5 - newSteps.push({ - array, result: [...result], - highlights: [i], variables: { i, leftProduct, rightProduct: '-' }, - explanation: `Left Pass (i=${i}): Check loop condition.`, - highlightedLine: 5 - }); - - // Line 6 - result = [...result]; - result[i] = leftProduct; - newSteps.push({ - array, result: [...result], - highlights: [i], variables: { i, leftProduct, rightProduct: '-' }, - explanation: `Store current leftProduct in result[${i}]. result[${i}] = ${leftProduct}.`, - highlightedLine: 6 - }); - - // Line 7 - leftProduct *= array[i]; - newSteps.push({ - array, result: [...result], - highlights: [i], variables: { i, leftProduct, rightProduct: '-' }, - explanation: `Update leftProduct by multiplying with nums[${i}]. leftProduct = ${leftProduct}.`, - highlightedLine: 7 - }); + // Loop condition + generatedSteps.push({ + array, + result: [...result], + highlights: [i], + variables: { i, leftProduct, rightProduct: '-' }, + explanation: `Left pass: processing index i = ${i}.`, + pseudoStep: `FOR i = ${i} to ${n - 1}` + }); + addLines(5, 5, 9, 7); + + // result[i] = leftProduct + result = [...result]; + result[i] = leftProduct; + generatedSteps.push({ + array, + result: [...result], + highlights: [i], + variables: { i, leftProduct, rightProduct: '-' }, + explanation: `Store current leftProduct in result[${i}] = ${leftProduct}.`, + pseudoStep: `SET result[${i}] = leftProduct` + }); + addLines(6, 6, 10, 8); + + // leftProduct *= nums[i] + leftProduct *= array[i]; + generatedSteps.push({ + array, + result: [...result], + highlights: [i], + variables: { i, leftProduct, rightProduct: '-' }, + explanation: `Multiply leftProduct by current element array[${i}] (${array[i]}) to get next leftProduct = ${leftProduct}.`, + pseudoStep: `SET leftProduct = leftProduct * nums[${i}]` + }); + addLines(7, 7, 11, 9); } - // Line 9 - newSteps.push({ - array, result: [...result], - highlights: [], variables: { i: '-', leftProduct, rightProduct }, - explanation: `Initialize rightProduct to 1.`, - highlightedLine: 9 + // Initialize rightProduct + generatedSteps.push({ + array, + result: [...result], + highlights: [], + variables: { i: '-', leftProduct, rightProduct }, + explanation: `Initialize rightProduct variable to 1.`, + pseudoStep: `SET rightProduct = 1` }); + addLines(9, 8, 13, 11); for (let i = n - 1; i >= 0; i--) { - // Line 10 - newSteps.push({ - array, result: [...result], - highlights: [i], variables: { i, leftProduct, rightProduct }, - explanation: `Right Pass (i=${i}): Check loop condition.`, - highlightedLine: 10 - }); - - // Line 11 - result = [...result]; - result[i] *= rightProduct; - newSteps.push({ - array, result: [...result], - highlights: [i], variables: { i, leftProduct, rightProduct }, - explanation: `Multiply result[${i}] by rightProduct. result[${i}] = ${result[i]}.`, - highlightedLine: 11 - }); - - // Line 12 - rightProduct *= array[i]; - newSteps.push({ - array, result: [...result], - highlights: [i], variables: { i, leftProduct, rightProduct }, - explanation: `Update rightProduct by multiplying with nums[${i}]. rightProduct = ${rightProduct}.`, - highlightedLine: 12 - }); + // Loop condition + generatedSteps.push({ + array, + result: [...result], + highlights: [i], + variables: { i, leftProduct, rightProduct }, + explanation: `Right pass: processing index i = ${i} from the end.`, + pseudoStep: `FOR i = ${i} down to 0` + }); + addLines(10, 9, 14, 12); + + // result[i] *= rightProduct + result = [...result]; + result[i] *= rightProduct; + generatedSteps.push({ + array, + result: [...result], + highlights: [i], + variables: { i, leftProduct, rightProduct }, + explanation: `Multiply current result[${i}] by rightProduct. result[${i}] = ${result[i]}.`, + pseudoStep: `SET result[${i}] = result[${i}] * rightProduct` + }); + addLines(11, 10, 15, 13); + + // rightProduct *= nums[i] + rightProduct *= array[i]; + generatedSteps.push({ + array, + result: [...result], + highlights: [i], + variables: { i, leftProduct, rightProduct }, + explanation: `Multiply rightProduct by current element array[${i}] (${array[i]}) to get next rightProduct = ${rightProduct}.`, + pseudoStep: `SET rightProduct = rightProduct * nums[${i}]` + }); + addLines(12, 11, 16, 14); } - // Line 14 - newSteps.push({ - array, result: [...result], - highlights: [], variables: { i: '-', leftProduct, rightProduct }, - explanation: `Return the final result array: [${result.join(', ')}].`, - highlightedLine: 14 + // Return result + generatedSteps.push({ + array, + result: [...result], + highlights: [], + variables: { i: '-', leftProduct, rightProduct }, + explanation: `Final result computed: [${result.join(', ')}].`, + pseudoStep: `RETURN result` }); + addLines(14, 12, 18, 16); - setSteps(newSteps); - setCurrentStepIndex(0); - }; - - useEffect(() => { - generateSteps(); + setSteps(generatedSteps); + setStepLineNumbers(stepLines); }, []); if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( + } leftContent={ - <> - -
-

Product of Array Except Self

- -
-
Original Array
-
- {currentStep.array.map((value, index) => ( -
-
+
+ +

+ Product of Array Except Self +

+ +
+
+
Original Array (nums)
+
+ {currentStep.array.map((value, index) => ( +
+
- {value} + {value} +
+ [{index}]
- [{index}] -
- ))} + ))} +
-
-
-
Result Array
-
- {currentStep.result.map((value, index) => ( -
-
+
Result Array
+
+ {currentStep.result.map((value, index) => ( +
+
- {value} + {value} +
+ [{index}]
-
- ))} + ))} +
+ +
-
- {currentStep.explanation} -
-
- - - +
+ +
+

Step Explanation

+

{currentStep.explanation}

+ + +
+
} rightContent={ - - } - controls={ - setCurrentStepIndex(0)} /> } /> diff --git a/src/components/visualizations/algorithms/RabinKarpVisualization.tsx b/src/components/visualizations/algorithms/RabinKarpVisualization.tsx index f961168..404e7b5 100644 --- a/src/components/visualizations/algorithms/RabinKarpVisualization.tsx +++ b/src/components/visualizations/algorithms/RabinKarpVisualization.tsx @@ -1,10 +1,11 @@ import { useState, useMemo } from 'react'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; import { motion } from 'framer-motion'; import { Card } from '@/components/ui/card'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { text: string; @@ -13,7 +14,7 @@ interface Step { i: number; j: number; explanation: string; - highlightedLines: number[]; + pseudoStep: string; variables: Record; phase: 'init' | 'lps' | 'search' | 'match' | 'mismatch' | 'done'; lps_i?: number; @@ -21,23 +22,19 @@ interface Step { result: number[]; } -const code = `function solution(text: string, pattern: string): number[] { +const languages: VisualizationLanguageMap = { + typescript: `function solution(text: string, pattern: string): number[] { const result: number[] = []; const n = text.length; const m = pattern.length; - if (pattern === "") { for (let i = 0; i < n; i++) result.push(i); return result; } - if (n === 0 || m > n) return []; - const lps: number[] = new Array(m).fill(0); - let len = 0; let i = 1; - while (i < m) { if (pattern[i] === pattern[len]) { len++; @@ -52,16 +49,13 @@ const code = `function solution(text: string, pattern: string): number[] { } } } - let j = 0; i = 0; - while (i < n) { if (text[i] === pattern[j]) { i++; j++; } - if (j === m) { result.push(i - j); j = lps[j - 1]; @@ -73,16 +67,155 @@ const code = `function solution(text: string, pattern: string): number[] { } } } - return result; -}`; +}`, + + python: `def solution(text: str, pattern: str): + n = len(text) + m = len(pattern) + result = [] + if pattern == "": + return list(range(n)) + if n == 0 or m > n: + return [] + lps = [0] * m + length = 0 + i = 1 + while i < m: + if pattern[i] == pattern[length]: + length += 1 + lps[i] = length + i += 1 + else: + if length != 0: + length = lps[length - 1] + else: + lps[i] = 0 + i += 1 + i = 0 + j = 0 + while i < n: + if text[i] == pattern[j]: + i += 1 + j += 1 + if j == m: + result.append(i - j) + j = lps[j - 1] + elif i < n and text[i] != pattern[j]: + if j != 0: + j = lps[j - 1] + else: + i += 1 + return result`, + + java: `public static class Solution { + public static List solution(String text, String pattern) { + List result = new ArrayList<>(); + int n = text.length(); + int m = pattern.length(); + if (pattern.equals("")) { + for (int i = 0; i < n; i++) result.add(i); + return result; + } + if (n == 0 || m > n) return result; + int[] lps = new int[m]; + int len = 0; + int i = 1; + while (i < m) { + if (pattern.charAt(i) == pattern.charAt(len)) { + len++; + lps[i] = len; + i++; + } else { + if (len != 0) { + len = lps[len - 1]; + } else { + lps[i] = 0; + i++; + } + } + } + i = 0; + int j = 0; + while (i < n) { + if (text.charAt(i) == pattern.charAt(j)) { + i++; + j++; + } + if (j == m) { + result.add(i - j); + j = lps[j - 1]; + } + else if (i < n && text.charAt(i) != pattern.charAt(j)) { + if (j != 0) + j = lps[j - 1]; + else + i++; + } + } + return result; + } +}`, + + cpp: `class Solution { +public: + vector solution(string text, string pattern) { + vector result; + int n = text.length(); + int m = pattern.length(); + if (pattern == "") { + for (int i = 0; i < n; i++) result.push_back(i); + return result; + } + if (n == 0 || m > n) return result; + vector lps(m, 0); + int len = 0; + int i = 1; + while (i < m) { + if (pattern[i] == pattern[len]) { + len++; + lps[i] = len; + i++; + } + else { + if (len != 0) { + len = lps[len - 1]; + } + else { + lps[i] = 0; + i++; + } + } + } + int j = 0; + i = 0; + while (i < n) { + if (text[i] == pattern[j]) { + i++; + j++; + } + if (j == m) { + result.push_back(i - j); + j = lps[j - 1]; + } + else if (i < n && text[i] != pattern[j]) { + if (j != 0) + j = lps[j - 1]; + else + i++; + } + } + return result; + } +};`, +}; export const RabinKarpVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); + const [currentStepIndex, setCurrentStepIndex] = useState(0); const textInput = 'ababcabcabababd'; const patternInput = 'aba'; - const steps: Step[] = useMemo(() => { + const { steps, stepLineNumbers } = useMemo(() => { const s: Step[] = []; const text = textInput; const pattern = patternInput; @@ -90,9 +223,24 @@ export const RabinKarpVisualization = () => { const m = pattern.length; let currentResult: number[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + const push = ( explanation: string, - highlightedLines: number[], + pseudoStep: string, + ts: number, py: number, java: number, cpp: number, variables: Record, phase: Step['phase'], i: number, @@ -108,189 +256,203 @@ export const RabinKarpVisualization = () => { i, j, explanation, - highlightedLines, + pseudoStep, variables, phase, lps_i, lps_len, result: [...currentResult], }); + addLines(ts, py, java, cpp); }; - push('Initialize result array, n, and m.', [1, 2, 3, 4], { n, m, result: '[]' }, 'init', -1, -1, []); + // 1. Init + push( + 'Initialize result array, n, and m.', + 'SET result = [], n = text.length, m = pattern.length', + 2, 2, 3, 4, + { n, m, result: '[]' }, + 'init', -1, -1, [] + ); if (pattern.length === 0) { - push('Pattern is empty, push all indices to result.', [6, 7, 8], { pattern: '""' }, 'done', -1, -1, []); - return s; + push( + 'Pattern is empty, push all indices to result.', + 'RETURN all indices [0..n-1]', + 5, 5, 6, 7, + { pattern: '""' }, + 'done', -1, -1, [] + ); + return { steps: s, stepLineNumbers }; } - push('Pattern is not empty, proceed.', [6], { pattern }, 'init', -1, -1, []); if (n === 0 || m > n) { - push('Text is empty or pattern longer than text, return [].', [11], { n, m }, 'done', -1, -1, []); - return s; + push( + 'Text is empty or pattern is longer than text. Return empty result.', + 'RETURN []', + 9, 7, 10, 11, + { n, m }, + 'done', -1, -1, [] + ); + return { steps: s, stepLineNumbers }; } - push('Text length and pattern length are valid.', [11], { n, m }, 'init', -1, -1, []); const lps: number[] = new Array(m).fill(0); - push('Initialize LPS array with zeros.', [13], { lps: `[${lps.join(',')}]` }, 'lps', -1, -1, lps); + push( + 'Initialize LPS array with zeros.', + 'SET lps = [0, 0, ...]', + 10, 9, 11, 12, + { lps: `[${lps.join(',')}]` }, + 'lps', -1, -1, lps + ); let len = 0; let i = 1; - push('Set len=0, i=1 to begin LPS computation.', [15, 16], { len, i }, 'lps', -1, -1, lps); + push( + 'Set len = 0, i = 1 to begin LPS computation.', + 'SET len = 0, i = 1', + 11, 10, 12, 13, + { len, i }, + 'lps', -1, -1, lps + ); while (i < m) { push( - `Loop: i=${i} < m=${m}. Comparing pattern[${i}]='${pattern[i]}' with pattern[${len}]='${pattern[len]}'.`, - [18, 19], + `Compare pattern[${i}]='${pattern[i]}' with pattern[${len}]='${pattern[len]}' to compute LPS.`, + `WHILE i (${i}) < m (${m}) : IF pattern[i] == pattern[len]`, + 13, 12, 14, 15, { i, len, 'pattern[i]': pattern[i], 'pattern[len]': pattern[len] }, - 'lps', - -1, - -1, - lps, - i, - len + 'lps', -1, -1, lps, i, len ); if (pattern[i] === pattern[len]) { len++; lps[i] = len; push( - `Match! len→${len}, lps[${i}]=${len}. Increment i→${i + 1}.`, - [20, 21, 22], + `Match! Increment len to ${len}, set lps[${i}] = ${len}, and increment i to ${i + 1}.`, + `SET len = ${len}, lps[${i}] = ${len}, i = i + 1`, + 14, 13, 15, 16, { i, len, lps: `[${lps.join(',')}]` }, - 'lps', - -1, - -1, - lps, - i, - len + 'lps', -1, -1, lps, i, len ); i++; } else if (len !== 0) { const prev = len; len = lps[len - 1]; push( - `Mismatch, len≠0. Set len=lps[${prev - 1}]=${len}. i stays ${i}.`, - [24, 25], + `Mismatch and len > 0. Fallback len = lps[len-1] = lps[${prev - 1}] = ${len}.`, + `SET len = lps[${prev - 1}] = ${len}`, + 19, 18, 20, 22, { i, oldLen: prev, newLen: len }, - 'lps', - -1, - -1, - lps, - i, - len + 'lps', -1, -1, lps, i, len ); } else { lps[i] = 0; push( - `Mismatch, len=0. Set lps[${i}]=0. Increment i→${i + 1}.`, - [27, 28], + `Mismatch and len == 0. Set lps[${i}] = 0 and increment i to ${i + 1}.`, + `SET lps[${i}] = 0, i = i + 1`, + 22, 21, 23, 26, { i, len, lps: `[${lps.join(',')}]` }, - 'lps', - -1, - -1, - lps, - i, - len + 'lps', -1, -1, lps, i, len ); i++; } } - push('LPS construction complete.', [13], { lps: `[${lps.join(',')}]` }, 'lps', -1, -1, lps); + push( + 'LPS table construction complete.', + 'LPS COMPLETED', + 10, 9, 11, 12, + { lps: `[${lps.join(',')}]` }, + 'lps', -1, -1, lps + ); let j = 0; i = 0; - push('Set j=0, i=0 for pattern search phase.', [32, 33], { i, j }, 'search', i, j, lps); + push( + 'Initialize pointers j = 0 for pattern and i = 0 for text.', + 'SET j = 0, i = 0', + 27, 23, 28, 31, + { i, j }, + 'search', i, j, lps + ); while (i < n) { push( - `Search loop: i=${i}, j=${j}. text[${i}]='${text[i]}' vs pattern[${j}]='${pattern[j]}'.`, - [35, 36], + `Search loop: compare text[${i}]='${text[i]}' with pattern[${j}]='${pattern[j]}'.`, + `WHILE i (${i}) < n (${n}) : IF text[i] == pattern[j]`, + 29, 25, 30, 33, { i, j, 'text[i]': text[i], 'pattern[j]': pattern[j] }, - 'search', - i, - j, - lps + 'search', i, j, lps ); if (text[i] === pattern[j]) { i++; j++; push( - `Characters match. i→${i}, j→${j}.`, - [37, 38], + `Characters match! Advance pointers: i to ${i}, j to ${j}.`, + `SET i = i + 1, j = j + 1`, + 30, 26, 31, 34, { i, j }, - 'match', - i, - j, - lps + 'match', i, j, lps ); } if (j === m) { currentResult = [...currentResult, i - j]; push( - `Pattern found at index ${i - j}! Push to result. Use LPS to shift: j=lps[${j - 1}]=${lps[j - 1]}.`, - [42, 43], + `Full pattern match found at index ${i - j}! Save index and reset j = lps[j - 1] = ${lps[j - 1]}.`, + `ADD ${i - j} TO result; SET j = lps[${j - 1}] = ${lps[j - 1]}`, + 34, 29, 35, 38, { matchIdx: i - j, result: `[${currentResult.join(',')}]`, newJ: lps[j - 1] }, - 'match', - i, - j, - lps + 'match', i, j, lps ); j = lps[j - 1]; } else if (i < n && text[i] !== pattern[j]) { push( - `Mismatch: text[${i}]='${text[i]}' ≠ pattern[${j}]='${pattern[j]}'.`, - [44, 45], + `Mismatch: text[${i}]='${text[i]}' != pattern[${j}]='${pattern[j]}'.`, + `IF text[i] != pattern[j]`, + 37, 32, 39, 42, { i, j, 'text[i]': text[i], 'pattern[j]': pattern[j] }, - 'mismatch', - i, - j, - lps + 'mismatch', i, j, lps ); + if (j !== 0) { const oldJ = j; j = lps[j - 1]; push( - `j≠0. Shift pattern: j=lps[${oldJ - 1}]=${j}.`, - [46, 47], + `j > 0. Shift pattern based on LPS: reset j = lps[j - 1] = ${j}.`, + `SET j = lps[${oldJ - 1}] = ${j}`, + 38, 33, 40, 44, { oldJ, newJ: j }, - 'mismatch', - i, - j, - lps + 'mismatch', i, j, lps ); } else { i++; push( - `j=0. Advance i→${i}.`, - [49, 50], + `j == 0. Advance text pointer i to ${i}.`, + `SET i = i + 1`, + 40, 35, 42, 46, { i, j }, - 'mismatch', - i, - j, - lps + 'mismatch', i, j, lps ); } } } push( - `Search complete. All matches: [${currentResult.join(', ')}].`, - [54], + `Search finished. Return all matching starting indices: [${currentResult.join(', ')}].`, + 'RETURN result', + 45, 37, 46, 49, { result: `[${currentResult.join(',')}]` }, - 'done', - i, - j, - lps + 'done', i, j, lps ); - return s; + return { steps: s, stepLineNumbers }; }, [textInput, patternInput]); - const step = steps[currentStep] || steps[steps.length - 1]; + const currentStep = steps[currentStepIndex] || steps[steps.length - 1]; + const pseudoSteps = steps.map(s => s.pseudoStep); const phaseColor = { init: 'rgba(var(--primary), 0.1)', @@ -299,7 +461,7 @@ export const RabinKarpVisualization = () => { match: 'rgba(34, 197, 94, 0.15)', mismatch: 'rgba(239, 68, 68, 0.15)', done: 'rgba(34, 197, 94, 0.15)', - }[step.phase]; + }[currentStep.phase]; return ( {

- KMP String Search + KMP String Search (Rabin-Karp slug)

@@ -315,11 +477,11 @@ export const RabinKarpVisualization = () => {

Text

- {step.text.split('').map((char, idx) => { - const isI = idx === step.i && (step.phase === 'search' || step.phase === 'match' || step.phase === 'mismatch'); - const isMatched = step.result.some((start) => idx >= start && idx < start + step.pattern.length); - const isCurrent = step.phase === 'search' || step.phase === 'match' || step.phase === 'mismatch' - ? idx >= step.i - step.j && idx < step.i - step.j + step.pattern.length && step.j > 0 + {currentStep.text.split('').map((char, idx) => { + const isI = idx === currentStep.i && (currentStep.phase === 'search' || currentStep.phase === 'match' || currentStep.phase === 'mismatch'); + const isMatched = currentStep.result.some((start) => idx >= start && idx < start + currentStep.pattern.length); + const isCurrent = currentStep.phase === 'search' || currentStep.phase === 'match' || currentStep.phase === 'mismatch' + ? idx >= currentStep.i - currentStep.j && idx < currentStep.i - currentStep.j + currentStep.pattern.length && currentStep.j > 0 : false; return ( @@ -358,10 +520,10 @@ export const RabinKarpVisualization = () => {

Pattern

- {step.pattern.split('').map((char, idx) => { - const isJ = idx === step.j && (step.phase === 'search' || step.phase === 'match' || step.phase === 'mismatch'); - const isLpsI = step.phase === 'lps' && idx === step.lps_i; - const isLpsLen = step.phase === 'lps' && idx === step.lps_len; + {currentStep.pattern.split('').map((char, idx) => { + const isJ = idx === currentStep.j && (currentStep.phase === 'search' || currentStep.phase === 'match' || currentStep.phase === 'mismatch'); + const isLpsI = currentStep.phase === 'lps' && idx === currentStep.lps_i; + const isLpsLen = currentStep.phase === 'lps' && idx === currentStep.lps_len; return (
@@ -403,23 +565,23 @@ export const RabinKarpVisualization = () => { LPS Array

- {step.pattern.split('').map((_, idx) => ( + {currentStep.pattern.split('').map((_, idx) => (
[{idx}] - {step.lps[idx] ?? 0} + {currentStep.lps[idx] ?? 0}
))} @@ -427,13 +589,13 @@ export const RabinKarpVisualization = () => {
{/* Result Array */} - {step.result.length > 0 && ( + {currentStep.result.length > 0 && (

Matches Found

- {step.result.map((idx, k) => ( + {currentStep.result.map((idx, k) => ( { >

Step

-

{step.explanation}

+

{currentStep.explanation}

- +
} rightContent={ - setCurrentStepIndex(0)} /> } controls={ } /> diff --git a/src/components/visualizations/algorithms/RecoverBSTVisualization.tsx b/src/components/visualizations/algorithms/RecoverBSTVisualization.tsx index d2b7582..bc5682c 100644 --- a/src/components/visualizations/algorithms/RecoverBSTVisualization.tsx +++ b/src/components/visualizations/algorithms/RecoverBSTVisualization.tsx @@ -1,8 +1,8 @@ import { useEffect, useRef, useState } from 'react'; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface TreeNode { val: number; @@ -19,37 +19,121 @@ interface Step { second: number | null; prev: number | null; message: string; - lineNumber: number; + pseudoStep: string; + variables: Record; } -export const RecoverBSTVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const intervalRef = useRef(null); - - const code = `function recoverTree(root) { - let first = null, second = null; - let prev = null; - - function inorder(node) { +const languages: VisualizationLanguageMap = { + typescript: `function recoverTree(root: TreeNode | null): void { + let first: TreeNode | null = null; + let second: TreeNode | null = null; + let prev: TreeNode | null = null; + function inorder(node: TreeNode | null): void { if (!node) return; - inorder(node.left); - if (prev && prev.val > node.val) { if (!first) first = prev; second = node; } prev = node; - inorder(node.right); } - inorder(root); - [first.val, second.val] = [second.val, first.val]; -}`; + if (first && second) { + [first.val, second.val] = [second.val, first.val]; + } +}`, + + python: `def recoverTree(root): + first = None + second = None + prev = None + def inorder(node): + nonlocal first, second, prev + if not node: + return + inorder(node.left) + if prev and prev.val > node.val: + if not first: + first = prev + second = node + prev = node + inorder(node.right) + inorder(root) + if first and second: + first.val, second.val = second.val, first.val`, + + java: `public static class Solution { + private TreeNode first; + private TreeNode second; + private TreeNode prev; + public void recoverTree(TreeNode root) { + first = null; + second = null; + prev = null; + inorder(root); + if (first != null && second != null) { + int temp = first.val; + first.val = second.val; + second.val = temp; + } + } + private void inorder(TreeNode node) { + if (node == null) { + return; + } + inorder(node.left); + if (prev != null && prev.val > node.val) { + if (first == null) { + first = prev; + } + second = node; + } + prev = node; + inorder(node.right); + } +}`, + + cpp: `class Solution { +private: + TreeNode* first; + TreeNode* second; + TreeNode* prev; +public: + void recoverTree(TreeNode* root) { + first = nullptr; + second = nullptr; + prev = nullptr; + inorder(root); + if (first != nullptr && second != nullptr) { + int temp = first->val; + first->val = second->val; + second->val = temp; + } + } +private: + void inorder(TreeNode* node) { + if (node == nullptr) { + return; + } + inorder(node->left); + if (prev != nullptr && prev->val > node->val) { + if (first == nullptr) { + first = prev; + } + second = node; + } + prev = node; + inorder(node->right); + } +};`, +}; + +export const RecoverBSTVisualization = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); const deepClone = (node: TreeNode | null): TreeNode | null => { if (!node) return null; @@ -71,7 +155,7 @@ export const RecoverBSTVisualization = () => { }; const generateSteps = () => { - const tree: TreeNode = { + const initialTree: TreeNode = { val: 3, left: { val: 1, left: null, right: null }, right: { @@ -81,73 +165,108 @@ export const RecoverBSTVisualization = () => { } }; - const newSteps: Step[] = []; + const steps: Step[] = []; + const tree = deepClone(initialTree); + calculatePositions(tree, 200, 50, 80); + let first: TreeNode | null = null; let second: TreeNode | null = null; let prev: TreeNode | null = null; - // Helper to push positioned steps - const pushStep = (msg: string, line: number, current: number | null) => { + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + const addStep = (currentNode: number | null, msg: string, pseudo: string, ts_l: number, py_l: number, java_l: number, cpp_l: number) => { const currentTree = deepClone(tree); calculatePositions(currentTree, 200, 50, 80); - newSteps.push({ + steps.push({ tree: currentTree, - current, + current: currentNode, first: first?.val || null, second: second?.val || null, prev: prev?.val || null, message: msg, - lineNumber: line + pseudoStep: pseudo, + variables: { + currentNode: currentNode ?? 'null', + prev: prev?.val ?? 'null', + first: first?.val ?? 'null', + second: second?.val ?? 'null' + } }); + addLines(ts_l, py_l, java_l, cpp_l); }; + // Initialize variables + addStep(null, 'Initializing tracking variables (first, second, prev).', 'first = null, second = null, prev = null', 2, 2, 6, 8); + + // Call inorder + addStep(null, 'Starting inorder traversal of the tree.', 'inorder(root)', 15, 16, 9, 11); + const inorder = (node: TreeNode | null) => { - if (!node) return; + if (!node) { + addStep(null, 'Node is null. Backtrack.', 'if (!node) → return', 6, 7, 17, 20); + addStep(null, 'Return from null node.', 'return', 6, 8, 18, 21); + return; + } + + // Check if node is null (NO) + addStep(node.val, `Checking if node (${node.val}) is null.`, 'if (!node) → NO', 6, 7, 17, 20); - pushStep(`Going left from node ${node.val}.`, 8, node.val); + // Go left + addStep(node.val, `Traverse left subtree of node ${node.val}.`, 'inorder(node.left)', 7, 9, 20, 23); inorder(node.left); - pushStep(`Visiting node ${node.val}.${prev ? ` Comparing with prev (${prev.val}).` : ''}`, 10, node.val); + // Compare + addStep(node.val, `Visit node ${node.val}.${prev ? ` Compare prev (${prev.val}) with current (${node.val}).` : ''}`, 'if (prev && prev.val > node.val)', 8, 10, 21, 24); if (prev && prev.val > node.val) { + addStep(node.val, `BST violation found: prev (${prev.val}) > current (${node.val}).`, 'if (!first)', 9, 11, 22, 25); if (!first) { first = prev; - pushStep(`Violation found: prev(${prev.val}) > current(${node.val}). Marking first as ${first.val}.`, 11, node.val); + addStep(node.val, `First incorrect node identified as prev node (${first.val}).`, `first = prev → ${first.val}`, 9, 12, 23, 26); } second = node; - pushStep(`Marking second as current node (${second.val}).`, 12, node.val); + addStep(node.val, `Second incorrect node identified as current node (${second.val}).`, `second = node → ${second.val}`, 10, 13, 25, 28); } + // Update prev prev = node; - pushStep(`Updating prev to ${prev.val}.`, 14, node.val); + addStep(node.val, `Update prev pointer to current node ${prev.val}.`, `prev = node → ${prev.val}`, 12, 14, 27, 30); - pushStep(`Going right from node ${node.val}.`, 16, node.val); + // Go right + addStep(node.val, `Traverse right subtree of node ${node.val}.`, 'inorder(node.right)', 13, 15, 28, 31); inorder(node.right); }; - pushStep("Initializing tracking variables.", 2, null); - pushStep("Starting inorder traversal.", 19, null); - inorder(tree); if (first && second) { - pushStep(`Identifying nodes to swap: ${first.val} and ${second.val}.`, 20, null); + addStep(null, `Identify incorrect nodes: ${first.val} and ${second.val}.`, 'if (first && second)', 16, 17, 10, 12); const val1 = first.val; const val2 = second.val; first.val = val2; second.val = val1; - pushStep(`Swapping values: ${val1} ↔ ${val2}.`, 20, null); - pushStep("BST recovery complete!", 21, null); + addStep(null, `Swap values: ${val1} ↔ ${val2} to restore BST properties.`, `swap(first.val, second.val) → ${val2} ↔ ${val1}`, 17, 18, 11, 13); + addStep(null, 'BST recovery complete!', 'RETURN', 19, 18, 15, 17); } - setSteps(newSteps); - setCurrentStepIndex(0); + return { steps, stepLineNumbers }; }; - useEffect(() => { - generateSteps(); - }, []); + const [{ steps, stepLineNumbers }] = useState(generateSteps); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { @@ -159,7 +278,7 @@ export const RecoverBSTVisualization = () => { } return prev + 1; }); - }, 1000 / speed); + }, 1200 / speed); } else { if (intervalRef.current) clearInterval(intervalRef.current); } @@ -175,12 +294,12 @@ export const RecoverBSTVisualization = () => { const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); - generateSteps(); }; if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); const renderTree = (node: TreeNode | null): JSX.Element | null => { if (!node || node.x === undefined || node.y === undefined) return null; @@ -196,11 +315,11 @@ export const RecoverBSTVisualization = () => { { y={node.y} textAnchor="middle" dy=".3em" - className={`text-sm font-medium transition-colors duration-300 ${node.val === currentStep.first || node.val === currentStep.second || currentStep.current === node.val + className={`text-sm font-semibold transition-colors duration-300 ${node.val === currentStep.first || node.val === currentStep.second || currentStep.current === node.val ? 'fill-white' : 'fill-foreground' }`} @@ -239,6 +358,7 @@ export const RecoverBSTVisualization = () => { />
+ {/* Left: visual tree + commentary box + variable panel */}
@@ -246,23 +366,28 @@ export const RecoverBSTVisualization = () => {
-
+

{currentStep.message}

-
- -
+
- + {/* Right: code / pseudocode panel */} +
); diff --git a/src/components/visualizations/algorithms/RemoveNthNodeVisualization.tsx b/src/components/visualizations/algorithms/RemoveNthNodeVisualization.tsx index 263a9cc..acf55c0 100644 --- a/src/components/visualizations/algorithms/RemoveNthNodeVisualization.tsx +++ b/src/components/visualizations/algorithms/RemoveNthNodeVisualization.tsx @@ -2,227 +2,308 @@ import { useState, useMemo } from 'react'; import { Card } from '@/components/ui/card'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; -import { CheckCircle2, Info, ArrowRight } from 'lucide-react'; +import { ArrowRight } from 'lucide-react'; +import type { VisualizationLanguageMap, StepLineNumberMap } from '@/types/visualization'; interface Step { list: number[]; left: number | null; // index mapping relative to `list` (-1 means dummy) right: number | null; // index mapping relative to `list` nVar: number; - message: string; + explanation: string; + pseudoStep: string; lineNumber: number; isMatch?: boolean; toRemove: number | null; removed: boolean; } -export const RemoveNthNodeVisualization = () => { - const [currentStepIndex, setCurrentStepIndex] = useState(0); - - const code = `function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null { +const languages: VisualizationLanguageMap = { + python: `def removeNthFromEnd(head: ListNode, n: int) -> ListNode: + dummy = ListNode(0, head) + left = dummy + right = head + while n > 0 and right: + right = right.next + n -= 1 + while right: + left = left.next + right = right.next + left.next = left.next.next + return dummy.next`, + typescript: `function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null { const dummy = new ListNode(0, head); let left: ListNode | null = dummy; let right: ListNode | null = head; - while (n > 0 && right !== null) { right = right.next; n -= 1; } - while (right !== null) { left = left!.next; right = right.next; } - left!.next = left!.next!.next; return dummy.next; -}`; - - const steps: Step[] = useMemo(() => { - const s: Step[] = []; - const list = [1, 2, 3, 4, 5]; - const targetN = 2; - - let nVar = targetN; - let left: number | null = null; - let right: number | null = null; - - const snap = (msg: string, line: number, isMatch: boolean = false, toRemove: number | null = null, removed: boolean = false, overrideList?: number[]) => { - s.push({ - list: overrideList ? [...overrideList] : [...list], - left, - right: right !== null && right >= list.length ? null : right, - nVar, - message: msg, - lineNumber: line, - isMatch, - toRemove, - removed - }); - }; - - snap(`Execution starts.`, 1, false); - - snap(`Initialize dummy node to handle potential head removals natively.`, 2, false); - - left = -1; - snap(`Set left pointer anchoring securely to the initialized dummy Node.`, 3, false); - - right = 0; - snap(`Initialize right pointer bounds matching the core head of the list.`, 4, false); - - while (true) { - snap(`Verifying span constraints. Processing while loop requirement: n (${nVar}) > 0 AND right is valid.`, 6, false); - if (nVar > 0 && right !== null && right < list.length) { - right += 1; - snap(`Evaluated true. Shifted right pointer forward 1 span segment.`, 7, false); - nVar -= 1; - snap(`Decremented spacing constraint 'n' tracking variable to ${nVar}.`, 8, true); - } else { - snap(`Evaluated false. Distance constraint span requirement safely established! Breaking span loop.`, 6, false); - break; +}`, + java: `public class Solution { + public ListNode removeNthFromEnd(ListNode head, int n) { + ListNode dummy = new ListNode(0, head); + ListNode left = dummy; + ListNode right = head; + while (n > 0 && right != null) { + right = right.next; + n--; } + while (right != null) { + left = left.next; + right = right.next; + } + left.next = left.next.next; + return dummy.next; } - - while (true) { - snap(`Verifying dual sweep conditions: Does right limit pointer exist?`, 11, false); - if (right !== null && right < list.length) { - left += 1; - snap(`Sweep evaluated true. Progressing left pointer.`, 12, false); - right += 1; - snap(`Progressing right pointer synchronously maintaining exact constraint gap!`, 13, true); - } else { - snap(`Sweep evaluated false! Right pointer breached final null bounding box. Processing halted.`, 11, false); - break; +}`, + cpp: `class Solution { +public: + ListNode* removeNthFromEnd(ListNode* head, int n) { + ListNode* dummy = new ListNode(0, head); + ListNode* left = dummy; + ListNode* right = head; + while (n > 0 && right != nullptr) { + right = right->next; + n--; + } + while (right != nullptr) { + left = left->next; + right = right->next; } + ListNode* toDelete = left->next; + left->next = left->next->next; + delete toDelete; + ListNode* result = dummy->next; + delete dummy; + return result; + } +};`, +}; + +const generateVisualizationData = () => { + const s: Step[] = []; + const list = [1, 2, 3, 4, 5]; + const targetN = 2; + + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + let nVar = targetN; + let left: number | null = null; + let right: number | null = null; + + const snap = (msg: string, pseudo: string, tsLine: number, pyLine: number, javaLine: number, cppLine: number, isMatch: boolean = false, toRemove: number | null = null, removed: boolean = false, overrideList?: number[]) => { + s.push({ + list: overrideList ? [...overrideList] : [...list], + left, + right: right !== null && right >= list.length ? null : right, + nVar, + explanation: msg, + pseudoStep: pseudo, + lineNumber: tsLine, + isMatch, + toRemove, + removed + }); + addLines(tsLine, pyLine, javaLine, cppLine); + }; + + snap("Execution starts.", "CALL removeNthFromEnd(head, n)", 1, 1, 2, 3, false); + + snap("Initialize dummy node to handle potential head removals.", "SET dummy = ListNode(0, head)", 2, 2, 3, 4, false); + + left = -1; + snap("Initialize left pointer starting at the dummy node.", "SET left = dummy", 3, 3, 4, 5, false); + + right = 0; + snap("Initialize right pointer starting at the head node.", "SET right = head", 4, 4, 5, 6, false); + + while (true) { + snap(`Verify constraint check: is n (${nVar}) > 0 AND right is valid?`, `WHILE n > 0 AND right ≠ null → ${nVar} > 0?`, 5, 5, 6, 7, false); + if (nVar > 0 && right !== null && right < list.length) { + right += 1; + snap("True. Advance the right pointer forward by one node.", "SET right = right.next", 6, 6, 7, 8, false); + nVar -= 1; + snap(`Decrement tracking variable n to ${nVar}.`, `SET n = n - 1 → n = ${nVar}`, 7, 7, 8, 9, true); + } else { + break; + } + } + + while (true) { + snap("Verify dual sweep: does right pointer exist?", "WHILE right ≠ null", 9, 8, 10, 11, false); + if (right !== null && right < list.length) { + left += 1; + snap("True. Move left pointer forward.", "SET left = left.next", 10, 9, 11, 12, false); + right += 1; + snap("Move right pointer forward synchronously to maintain spacing.", "SET right = right.next", 11, 10, 12, 13, true); + } else { + break; } + } - const toRemoveTarget = left! + 1; - snap(`Left pointer arrived prior to the deletion target safely. Processing next chain mapping detatch.`, 16, true, toRemoveTarget, false); + const toRemoveTarget = left! + 1; + snap("Left pointer is now positioned just before the node to delete.", "SET left.next = left.next.next", 13, 11, 14, 16, true, toRemoveTarget, false); - const resultList = [...list]; - resultList.splice(toRemoveTarget, 1); - - snap(`Node detached perfectly. Return resulting dummy chained bounds!`, 17, true, null, true, resultList); + const resultList = [...list]; + resultList.splice(toRemoveTarget, 1); + + snap("Node removed. Return the head of the modified list (dummy.next).", "RETURN dummy.next", 14, 12, 15, 20, true, null, true, resultList); + + return { steps: s, stepLineNumbers }; +}; - return s; +export const RemoveNthNodeVisualization = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + + const { steps, stepLineNumbers } = useMemo(() => { + return generateVisualizationData(); }, []); - const step = steps[currentStepIndex]; + const step = steps[currentStepIndex] || steps[steps.length - 1]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( - +

Pointers Topology Reference

-
- {step.left === -1 ? 'L' : ''} -
- D -
+
+ + {step.left === -1 ? 'L' : ''} + +
+ D
- - - {step.list.map((val, idx) => { - const isLeft = step.left === (step.removed && idx >= step.toRemove! ? idx + 1 : idx); - const isRight = step.right === (step.removed && idx >= step.toRemove! ? idx + 1 : idx); - const isRemovedTarget = step.toRemove === idx && !step.removed; - - return ( -
-
- - {isLeft && isRight ? 'L,R' : isLeft ? 'L' : isRight ? 'R' : ''} - -
- {val} -
-
- {idx < step.list.length - 1 && ( - - )} +
+ + + {step.list.map((val, idx) => { + const isLeft = step.left === (step.removed && idx >= step.toRemove! ? idx + 1 : idx); + const isRight = step.right === (step.removed && idx >= step.toRemove! ? idx + 1 : idx); + const isRemovedTarget = step.toRemove === idx && !step.removed; + + return ( +
+
+ + {isLeft && isRight ? 'L,R' : isLeft ? 'L' : isRight ? 'R' : ''} + +
+ {val}
- ); - })} - - -
- {step.right === null && step.left !== null ? 'R' : ''} -
- NUL +
+ {idx < step.list.length - 1 && ( + + )}
+ ); + })} + + +
+ + {step.right === null && step.left !== null ? 'R' : ''} + +
+ NUL
+
-
+
-
- Left Pointer +
+ Left Pointer
-
- Right Pointer +
+ Right Pointer
-
- To Remove +
+ To Remove
- -
-
- {step?.isMatch ? : } -
-
-

- Execution Detail Tracker -

-

- {step?.message || ''} -

-
+ {/* Descriptive Commentary Box (at the bottom) */} +
+
+
+ Process Step
- + {step.explanation} +
- ${step.list[step.left]}` : 'null', - "right": step.right !== null ? `node[${step.right}] -> ${step.list[step.right]}` : 'null', - }} - /> + {/* Variable Panel (below the commentary box) */} +
+ ${step.list[step.left]}` : 'null', + "right": step.right !== null ? `node[${step.right}] -> ${step.list[step.right]}` : 'null', + }} + /> +
} rightContent={ -
- -
+ setCurrentStepIndex(0)} + /> } controls={ ; - message: string; + explanation: string; + pseudoStep: string; lineNumber: number; variables: Record; } -export const ReorderListVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const intervalRef = useRef(null); - - const code = `function reorderList(head: ListNode | null): void { +const languages: VisualizationLanguageMap = { + python: `def reorderList(head: Optional[ListNode]) -> Optional[ListNode]: + if not head or not head.next: + return head + slow = head + fast = head + while fast and fast.next: + slow = slow.next + fast = fast.next.next + second = slow.next + slow.next = None + prev = None + curr = second + while curr: + nxt = curr.next + curr.next = prev + prev = curr + curr = nxt + second = prev + first = head + while second: + tmp1 = first.next + tmp2 = second.next + first.next = second + second.next = tmp1 + first = tmp1 + second = tmp2`, + + typescript: `function reorderList(head: ListNode | null): void { if (!head || !head.next) return; let slow: ListNode | null = head; let fast: ListNode | null = head; @@ -58,14 +82,99 @@ export const ReorderListVisualization = () => { firstHalfCurrent = firstHalfNext; secondHalfCurrent = secondHalfNext; } -}`; +}`, + + java: `public class Solution { + public void reorderList(ListNode head) { + if (head == null || head.next == null) { + return; + } + ListNode slow = head; + ListNode fast = head; + while (fast != null && fast.next != null) { + slow = slow.next; + fast = fast.next.next; + } + ListNode second = slow.next; + slow.next = null; + ListNode prev = null; + ListNode curr = second; + while (curr != null) { + ListNode next = curr.next; + curr.next = prev; + prev = curr; + curr = next; + } + second = prev; + ListNode first = head; + while (second != null) { + ListNode tmp1 = first.next; + ListNode tmp2 = second.next; + first.next = second; + second.next = tmp1; + first = tmp1; + second = tmp2; + } + } +}`, + + cpp: `class Solution { + public: + void reorderList(ListNode* head) { + if (head == nullptr || head->next == nullptr) { + return; + } + ListNode* slow = head; + ListNode* fast = head; + while (fast != nullptr && fast->next != nullptr) { + slow = slow->next; + fast = fast->next->next; + } + ListNode* second = slow->next; + slow->next = nullptr; + ListNode* prev = nullptr; + ListNode* curr = second; + while (curr != nullptr) { + ListNode* next = curr->next; + curr->next = prev; + prev = curr; + curr = next; + } + second = prev; + ListNode* first = head; + while (second != nullptr) { + ListNode* tmp1 = first->next; + ListNode* tmp2 = second->next; + first->next = second; + second->next = tmp1; + first = tmp1; + second = tmp2; + } + } +};` +}; + +const generateVisualizationData = () => { + const list = [1, 2, 3, 4, 5]; + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - const generateSteps = () => { - const list = [1, 2, 3, 4, 5]; - const newSteps: Step[] = []; - let connections: Record = { 0: 1, 1: 2, 2: 3, 3: 4, 4: null }; + let connections: Record = { 0: 1, 1: 2, 2: 3, 3: 4, 4: null }; - const createSnap = (overrides: Partial) => ({ + const createSnap = (overrides: Partial, tsLine: number, pyLine: number, javaLine: number, cppLine: number) => { + steps.push({ list: [...list], phase: overrides.phase || 'find-middle', slow: null, @@ -79,285 +188,295 @@ export const ReorderListVisualization = () => { firstHalfNext: null, secondHalfNext: null, connections: { ...connections }, - message: '', - lineNumber: 1, - variables: {}, + explanation: overrides.explanation || '', + pseudoStep: overrides.pseudoStep || '', + lineNumber: tsLine, + variables: overrides.variables || {}, ...overrides - } as Step); - - newSteps.push(createSnap({ - message: "Starting reorder list algorithm with input [1, 2, 3, 4, 5].", - lineNumber: 1, - variables: { head: '[1, 2, 3, 4, 5]' } - })); - - newSteps.push(createSnap({ - message: "Check if the list is empty or has only one node. If so, no reordering is needed.", - lineNumber: 2, - variables: { head: 'Node 1', "head.next": 'Node 2' } - })); - - let slow = 0, fast = 0; - newSteps.push(createSnap({ - slow, - message: "Initialize slow pointer to the head of the list.", - lineNumber: 3, - variables: { head: 'Node 1', slow: 'Node 1' } - })); - - newSteps.push(createSnap({ - slow, fast, - message: "Initialize fast pointer to the head. We'll use these to find the middle of the list.", - lineNumber: 4, - variables: { slow: 'Node 1', fast: 'Node 1' } - })); - - while (fast !== null && connections[fast] !== null) { - newSteps.push(createSnap({ - slow, fast, - message: "The fast pointer can still move forward. Proceed with finding the middle.", - lineNumber: 5, - variables: { fast: `Node ${list[fast]}`, "fast.next": `Node ${list[connections[fast]!]}` } - })); - - slow = connections[slow]!; - newSteps.push(createSnap({ - slow, fast, - message: "Advance slow by one step. It moves at half the speed of the fast pointer.", - lineNumber: 6, - variables: { slow: `Node ${list[slow]}`, fast: `Node ${list[fast]}` } - })); - - fast = connections[connections[fast]!]!; - newSteps.push(createSnap({ - slow, fast, - message: "Advance fast by two steps. When fast reaches the end, slow will be at the middle.", - lineNumber: 7, - variables: { slow: `Node ${list[slow]}`, fast: fast !== null ? `Node ${list[fast]}` : 'null' } - })); - } + }); + addLines(tsLine, pyLine, javaLine, cppLine); + }; - newSteps.push(createSnap({ + // 1. Initial State + createSnap({ + explanation: "Starting reorder list algorithm with input [1, 2, 3, 4, 5].", + pseudoStep: "CALL reorderList(head)", + variables: { head: '[1, 2, 3, 4, 5]' } + }, 1, 1, 2, 3); + + // 2. Check head/next null + createSnap({ + explanation: "Check if the list is empty or has only one node. If so, return head.", + pseudoStep: "IF head IS null OR head.next IS null -> RETURN", + variables: { head: 'Node 1', "head.next": 'Node 2' } + }, 2, 2, 3, 4); + + let slow = 0, fast = 0; + // 3. Initialize slow + createSnap({ + slow, + explanation: "Initialize slow pointer to the head of the list.", + pseudoStep: "SET slow = head", + variables: { head: 'Node 1', slow: 'Node 1' } + }, 3, 4, 6, 7); + + // 4. Initialize fast + createSnap({ + slow, fast, + explanation: "Initialize fast pointer to the head of the list.", + pseudoStep: "SET fast = head", + variables: { slow: 'Node 1', fast: 'Node 1' } + }, 4, 5, 7, 8); + + while (fast !== null && connections[fast] !== null) { + // 5. Loop check + createSnap({ slow, fast, - message: "The fast pointer has reached the end/tail. The slow pointer is now at the middle of the list.", - lineNumber: 5, - variables: { fast: fast !== null ? `Node ${list[fast]}` : 'null', slow: `Node ${list[slow]}` } - })); - - let secondHalfHead: number | null = connections[slow]; - newSteps.push(createSnap({ - slow, secondHalfHead, - message: "Identify the start of the second half of the list, which begins after the slow pointer.", - lineNumber: 9, - variables: { slow: `Node ${list[slow]}`, secondHalfHead: secondHalfHead !== null ? `Node ${list[secondHalfHead]}` : 'null' } - })); - - connections[slow] = null; - newSteps.push(createSnap({ - slow, secondHalfHead, - message: "Break the link between the first and second halves to treat them as separate lists.", - lineNumber: 10, - variables: { "slow.next": 'null', secondHalfHead: `Node ${list[secondHalfHead!]}` } - })); - - let prev: number | null = null; - newSteps.push(createSnap({ - phase: 'reverse', - prev, - message: "Begin reversing the second half. Initialize prev to null to serve as the new tail.", - lineNumber: 11, - variables: { prev: 'null' } - })); - - let current: number | null = secondHalfHead; - newSteps.push(createSnap({ + explanation: "Check while loop condition: fast and fast.next are not null.", + pseudoStep: `WHILE fast AND fast.next → Node ${list[fast]} ≠ null`, + variables: { fast: `Node ${list[fast]}`, "fast.next": `Node ${list[connections[fast]!]}` } + }, 5, 6, 8, 9); + + // 6. Move slow + slow = connections[slow]!; + createSnap({ + slow, fast, + explanation: "Advance slow pointer by one node.", + pseudoStep: "SET slow = slow.next", + variables: { slow: `Node ${list[slow]}`, fast: `Node ${list[fast]}` } + }, 6, 7, 9, 10); + + // 7. Move fast + fast = connections[connections[fast]!]!; + createSnap({ + slow, fast, + explanation: "Advance fast pointer by two nodes.", + pseudoStep: "SET fast = fast.next.next", + variables: { slow: `Node ${list[slow]}`, fast: fast !== null ? `Node ${list[fast]}` : 'null' } + }, 7, 8, 10, 11); + } + + // 8. Loop check failed + createSnap({ + slow, fast, + explanation: "Fast pointer reached the end of the list. Loop terminates.", + pseudoStep: "WHILE fast AND fast.next → FALSE ✗", + variables: { fast: fast !== null ? `Node ${list[fast]}` : 'null', slow: `Node ${list[slow]}` } + }, 5, 6, 8, 9); + + let secondHalfHead: number | null = connections[slow]; + // 9. Get head of second half + createSnap({ + slow, secondHalfHead, + explanation: "Set secondHalfHead to the node after slow.", + pseudoStep: "SET second = slow.next", + variables: { slow: `Node ${list[slow]}`, secondHalfHead: secondHalfHead !== null ? `Node ${list[secondHalfHead]}` : 'null' } + }, 9, 9, 12, 13); + + // 10. Split first and second halves + connections[slow] = null; + createSnap({ + slow, secondHalfHead, + explanation: "Disconnect the first half from the second half by setting slow.next to null.", + pseudoStep: "SET slow.next = null", + variables: { "slow.next": 'null', secondHalfHead: `Node ${list[secondHalfHead!]}` } + }, 10, 10, 13, 14); + + let prev: number | null = null; + // 11. Reversal prev = null + createSnap({ + phase: 'reverse', + prev, + explanation: "Begin reversing the second half. Initialize prev pointer to null.", + pseudoStep: "SET prev = null", + variables: { prev: 'null' } + }, 11, 11, 14, 15); + + let current: number | null = secondHalfHead; + // 12. Reversal curr = second + createSnap({ + phase: 'reverse', + current, prev, + explanation: "Initialize current pointer to the head of the second half.", + pseudoStep: "SET curr = second", + variables: { current: `Node ${list[current!]}`, prev: 'null' } + }, 12, 12, 15, 16); + + while (current !== null) { + // 13. Reversal loop check + createSnap({ phase: 'reverse', current, prev, - message: "Start reversal from the head of the second half.", - lineNumber: 12, - variables: { current: `Node ${list[current!]}`, prev: 'null' } - })); - - while (current !== null) { - newSteps.push(createSnap({ - phase: 'reverse', - current, prev, - message: "Continue reversing while we have nodes left in the second half.", - lineNumber: 13, - variables: { current: `Node ${list[current]}` } - })); - - let nextNode: number | null = connections[current]; - newSteps.push(createSnap({ - phase: 'reverse', - current, prev, nextNode, - message: "Temporarily store the next node so we don't lose the rest of the list.", - lineNumber: 14, - variables: { current: `Node ${list[current]}`, next: nextNode !== null ? `Node ${list[nextNode]}` : 'null' } - })); - - connections[current] = prev; - newSteps.push(createSnap({ - phase: 'reverse', - current, prev, nextNode, - message: "Reverse the link: the current node now points to the previous node.", - lineNumber: 15, - variables: { "current.next": prev !== null ? `Node ${list[prev]}` : 'null' } - })); - - prev = current; - newSteps.push(createSnap({ - phase: 'reverse', - current, prev, nextNode, - message: "Move the prev pointer forward to the current node.", - lineNumber: 16, - variables: { prev: `Node ${list[prev]}` } - })); - - current = nextNode; - newSteps.push(createSnap({ - phase: 'reverse', - current, prev, - message: "Move the current pointer forward to the next node to continue reversal.", - lineNumber: 17, - variables: { current: current !== null ? `Node ${list[current]}` : 'null' } - })); - } + explanation: "Loop condition: is current pointer not null?", + pseudoStep: `WHILE curr → Node ${list[current]} ≠ null`, + variables: { current: `Node ${list[current]}` } + }, 13, 13, 16, 17); + + let nextNode: number | null = connections[current]; + // 14. Reversal store next + createSnap({ + phase: 'reverse', + current, prev, nextNode, + explanation: "Temporarily store current's next node.", + pseudoStep: "SET next = curr.next", + variables: { current: `Node ${list[current]}`, next: nextNode !== null ? `Node ${list[nextNode]}` : 'null' } + }, 14, 14, 17, 18); + + connections[current] = prev; + // 15. Reversal point back + createSnap({ + phase: 'reverse', + current, prev, nextNode, + explanation: "Point current's next link backward to the prev node.", + pseudoStep: "SET curr.next = prev", + variables: { "current.next": prev !== null ? `Node ${list[prev]}` : 'null' } + }, 15, 15, 18, 19); - secondHalfHead = prev; - newSteps.push(createSnap({ + prev = current; + // 16. Reversal move prev + createSnap({ phase: 'reverse', - secondHalfHead, - message: "Reversal complete. The second half is now reversed, and secondHalfHead points to its new head.", - lineNumber: 19, - variables: { secondHalfHead: `Node ${list[secondHalfHead!]}` } - })); - - let firstHalfCurrent: number | null = 0; - newSteps.push(createSnap({ - phase: 'merge', - firstHalfCurrent, - message: "Now we merge the two halves. Initialize firstHalfCurrent to the head of the first half.", - lineNumber: 20, - variables: { firstHalfCurrent: 'Node 1' } - })); - - let secondHalfCurrent: number | null = secondHalfHead; - newSteps.push(createSnap({ + current, prev, nextNode, + explanation: "Move prev pointer forward to current.", + pseudoStep: "SET prev = curr", + variables: { prev: `Node ${list[prev]}` } + }, 16, 16, 19, 20); + + current = nextNode; + // 17. Reversal move curr + createSnap({ + phase: 'reverse', + current, prev, + explanation: "Move current pointer forward to the stored next node.", + pseudoStep: "SET curr = next", + variables: { current: current !== null ? `Node ${list[current]}` : 'null' } + }, 17, 17, 20, 21); + } + + // 18. Reversal complete loop check failed + secondHalfHead = prev; + createSnap({ + phase: 'reverse', + secondHalfHead, + explanation: "Current is null. Reversal is complete. Set secondHalfHead to the new head (prev).", + pseudoStep: "SET second = prev", + variables: { secondHalfHead: `Node ${list[secondHalfHead!]}` } + }, 19, 18, 22, 23); + + let firstHalfCurrent: number | null = 0; + // 19. Merge first = head + createSnap({ + phase: 'merge', + firstHalfCurrent, + explanation: "Initialize firstHalfCurrent pointer to the head of the first half.", + pseudoStep: "SET first = head", + variables: { firstHalfCurrent: 'Node 1' } + }, 20, 19, 23, 24); + + let secondHalfCurrent: number | null = secondHalfHead; + // 20. Merge second = prev + createSnap({ + phase: 'merge', + firstHalfCurrent, secondHalfCurrent, + explanation: "Initialize secondHalfCurrent pointer to the head of the reversed second half.", + pseudoStep: "SET second = prev", + variables: { firstHalfCurrent: 'Node 1', secondHalfCurrent: `Node ${list[secondHalfCurrent!]}` } + }, 21, 18, 22, 23); + + while (secondHalfCurrent !== null) { + // 21. Merge loop check + createSnap({ phase: 'merge', firstHalfCurrent, secondHalfCurrent, - message: "Initialize secondHalfCurrent to the head of the reversed second half.", - lineNumber: 21, - variables: { firstHalfCurrent: 'Node 1', secondHalfCurrent: `Node ${list[secondHalfCurrent!]}` } - })); - - while (secondHalfCurrent !== null) { - newSteps.push(createSnap({ - phase: 'merge', - firstHalfCurrent, secondHalfCurrent, - message: "Continue merging while there are nodes remaining in the second half.", - lineNumber: 22, - variables: { secondHalfCurrent: `Node ${list[secondHalfCurrent]}` } - })); - - let firstHalfNext: number | null = connections[firstHalfCurrent!]; - newSteps.push(createSnap({ - phase: 'merge', - firstHalfCurrent, secondHalfCurrent, firstHalfNext, - message: "Save the next node in the first half to maintain the structure during the merge.", - lineNumber: 23, - variables: { firstHalfNext: firstHalfNext !== null ? `Node ${list[firstHalfNext]}` : 'null' } - })); - - let secondHalfNext: number | null = connections[secondHalfCurrent]; - newSteps.push(createSnap({ - phase: 'merge', - firstHalfCurrent, secondHalfCurrent, firstHalfNext, secondHalfNext, - message: "Save the next node in the reversed second half.", - lineNumber: 24, - variables: { secondHalfNext: secondHalfNext !== null ? `Node ${list[secondHalfNext]}` : 'null' } - })); - - connections[firstHalfCurrent!] = secondHalfCurrent; - newSteps.push(createSnap({ - phase: 'merge', - firstHalfCurrent, secondHalfCurrent, firstHalfNext, secondHalfNext, - message: "Insert the node from the second half between current first half nodes.", - lineNumber: 25, - variables: { "firstHalfCurrent.next": `Node ${list[secondHalfCurrent]}` } - })); - - connections[secondHalfCurrent] = firstHalfNext; - newSteps.push(createSnap({ - phase: 'merge', - firstHalfCurrent, secondHalfCurrent, firstHalfNext, secondHalfNext, - message: "Set the next pointer of the inserted node to point back to the first half's continuation.", - lineNumber: 26, - variables: { "secondHalfCurrent.next": firstHalfNext !== null ? `Node ${list[firstHalfNext]}` : 'null' } - })); - - firstHalfCurrent = firstHalfNext; - newSteps.push(createSnap({ - phase: 'merge', - firstHalfCurrent, secondHalfCurrent, firstHalfNext, secondHalfNext, - message: "Advance the first half pointer forward.", - lineNumber: 27, - variables: { firstHalfCurrent: firstHalfCurrent !== null ? `Node ${list[firstHalfCurrent]}` : 'null' } - })); - - secondHalfCurrent = secondHalfNext; - newSteps.push(createSnap({ - phase: 'merge', - firstHalfCurrent, secondHalfCurrent, - message: "Advance the second half pointer for the next iteration of the merge.", - lineNumber: 28, - variables: { secondHalfCurrent: secondHalfCurrent !== null ? `Node ${list[secondHalfCurrent]}` : 'null' } - })); - } + explanation: "Loop condition: is secondHalfCurrent not null?", + pseudoStep: `WHILE second → Node ${list[secondHalfCurrent]} ≠ null`, + variables: { secondHalfCurrent: `Node ${list[secondHalfCurrent]}` } + }, 22, 20, 24, 25); + + let firstHalfNext: number | null = connections[firstHalfCurrent!]; + // 22. Merge store first next + createSnap({ + phase: 'merge', + firstHalfCurrent, secondHalfCurrent, firstHalfNext, + explanation: "Temporarily store the next node in the first half.", + pseudoStep: "SET tmp1 = first.next", + variables: { firstHalfNext: firstHalfNext !== null ? `Node ${list[firstHalfNext]}` : 'null' } + }, 23, 21, 25, 26); + + let secondHalfNext: number | null = connections[secondHalfCurrent]; + // 23. Merge store second next + createSnap({ + phase: 'merge', + firstHalfCurrent, secondHalfCurrent, firstHalfNext, secondHalfNext, + explanation: "Temporarily store the next node in the second half.", + pseudoStep: "SET tmp2 = second.next", + variables: { secondHalfNext: secondHalfNext !== null ? `Node ${list[secondHalfNext]}` : 'null' } + }, 24, 22, 26, 27); + + connections[firstHalfCurrent!] = secondHalfCurrent; + // 24. Merge point first to second + createSnap({ + phase: 'merge', + firstHalfCurrent, secondHalfCurrent, firstHalfNext, secondHalfNext, + explanation: "Connect current node in first half to current node in second half.", + pseudoStep: "SET first.next = second", + variables: { "firstHalfCurrent.next": `Node ${list[secondHalfCurrent]}` } + }, 25, 23, 27, 28); + + connections[secondHalfCurrent] = firstHalfNext; + // 25. Merge point second to first next + createSnap({ + phase: 'merge', + firstHalfCurrent, secondHalfCurrent, firstHalfNext, secondHalfNext, + explanation: "Connect current node in second half to original next node in first half.", + pseudoStep: "SET second.next = tmp1", + variables: { "secondHalfCurrent.next": firstHalfNext !== null ? `Node ${list[firstHalfNext]}` : 'null' } + }, 26, 24, 28, 29); + + firstHalfCurrent = firstHalfNext; + // 26. Merge move first + createSnap({ + phase: 'merge', + firstHalfCurrent, secondHalfCurrent, firstHalfNext, secondHalfNext, + explanation: "Advance first pointer forward.", + pseudoStep: "SET first = tmp1", + variables: { firstHalfCurrent: firstHalfCurrent !== null ? `Node ${list[firstHalfCurrent]}` : 'null' } + }, 27, 25, 29, 30); - newSteps.push(createSnap({ + secondHalfCurrent = secondHalfNext; + // 27. Merge move second + createSnap({ phase: 'merge', firstHalfCurrent, secondHalfCurrent, - message: "Reordering is complete. The list is now alternating between the start and the end.", - lineNumber: 22, - variables: { secondHalfCurrent: 'null' } - })); + explanation: "Advance second pointer forward.", + pseudoStep: "SET second = tmp2", + variables: { secondHalfCurrent: secondHalfCurrent !== null ? `Node ${list[secondHalfCurrent]}` : 'null' } + }, 28, 26, 30, 31); + } - setSteps(newSteps); - }; + // 28. Merge loop check failed / completed + createSnap({ + phase: 'merge', + firstHalfCurrent, secondHalfCurrent, + explanation: "Reordering complete. Alternating links are successfully established.", + pseudoStep: "RETURN", + variables: { secondHalfCurrent: 'null' } + }, 22, 20, 24, 25); - useEffect(() => { - generateSteps(); - }, []); + return { steps, stepLineNumbers }; +}; - useEffect(() => { - let timer: NodeJS.Timeout; - if (isPlaying && currentStepIndex < steps.length - 1) { - timer = setTimeout(() => { - setCurrentStepIndex(prev => prev + 1); - }, 1500 / speed); - } else { - setIsPlaying(false); - } - return () => clearTimeout(timer); - }, [isPlaying, currentStepIndex, steps.length, speed]); +export const ReorderListVisualization = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); - const handlePlay = () => setIsPlaying(true); - const handlePause = () => setIsPlaying(false); - const handleStepForward = () => { - if (currentStepIndex < steps.length - 1) setCurrentStepIndex(currentStepIndex + 1); - }; - const handleStepBack = () => { - if (currentStepIndex > 0) setCurrentStepIndex(currentStepIndex - 1); - }; - const handleReset = () => { - setCurrentStepIndex(0); - setIsPlaying(false); - }; + const { steps, stepLineNumbers } = useMemo(() => { + return generateVisualizationData(); + }, []); if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); // Calculate logical order for the merge phase to show 1 -> 5 -> 2 -> 4 -> 3 sequence const getOrderedIndices = () => { @@ -385,28 +504,14 @@ export const ReorderListVisualization = () => { const displayIndices = getOrderedIndices(); return ( -
- - - -
+

- {currentStep.phase === 'find-middle' && } - {currentStep.phase === 'reverse' && } - {currentStep.phase === 'merge' && } + {currentStep.phase === 'find-middle' && } + {currentStep.phase === 'reverse' && } + {currentStep.phase === 'merge' && } {currentStep.phase === 'find-middle' && 'Phase 1: Find Middle'} {currentStep.phase === 'reverse' && 'Phase 2: Reverse Second Half'} {currentStep.phase === 'merge' && 'Phase 3: Merge Alternately'} @@ -537,16 +642,38 @@ export const ReorderListVisualization = () => { )}

-
-

{currentStep.message}

+ {/* Descriptive Commentary Box (at the bottom) */} +
+
+
+ Process Step +
+ {currentStep.explanation}
- + {/* Variable Panel (below the commentary box) */} +
+ +
- - -
-
+ } + rightContent={ + setCurrentStepIndex(0)} + /> + } + controls={ + + } + /> ); }; diff --git a/src/components/visualizations/algorithms/ReverseBitsVisualization.tsx b/src/components/visualizations/algorithms/ReverseBitsVisualization.tsx index 20a8b40..3afc8f4 100644 --- a/src/components/visualizations/algorithms/ReverseBitsVisualization.tsx +++ b/src/components/visualizations/algorithms/ReverseBitsVisualization.tsx @@ -2,87 +2,121 @@ import { useState, useMemo } from 'react'; import { Card } from '@/components/ui/card'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; -import { motion } from 'framer-motion'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { variables: Record; explanation: string; - highlightedLines: number[]; - lineExecution: string; + pseudoStep: string; bitsN: string[]; bitsResult: string[]; activeBitIndex?: number; // The 'i' index in n (visual index 31-i) targetBitIndex?: number; // The '31-i' index in res (visual index i) } -export const ReverseBitsVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); - - // User provided code - const code = `function reverseBits(n: number): number { +const languages: VisualizationLanguageMap = { + typescript: `function reverseBits(n: number): number { let res = 0; for (let i = 0; i < 32; i++) { const bit = (n >> i) & 1; res |= bit << (31 - i); } return res >>> 0; -}`; +}`, + python: `def reverseBits(n: int) -> int: + res = 0 + for i in range(32): + bit = (n >> i) & 1 + res |= bit << (31 - i) + return res`, + java: `public static class Solution { + public int reverseBits(int n) { + int res = 0; + for (int i = 0; i < 32; i++) { + int bit = (n >> i) & 1; + res |= bit << (31 - i); + } + return res; + } +}`, + cpp: `class Solution { +public: + uint32_t reverseBits(uint32_t n) { + uint32_t res = 0; + for (int i = 0; i < 32; i++) { + uint32_t bit = (n >> i) & 1; + res |= bit << (31 - i); + } + return res; + } +};` +}; - const steps = useMemo(() => { +export const ReverseBitsVisualization = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + + const { steps, stepLineNumbers } = useMemo(() => { const generatedSteps: Step[] = []; - // Example number n = 11 (binary ...00000000000000000000000000001011) const nVal = 11; let res = 0; - // Helper to get 32-bit string array + const lines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + lines.typescript!.push(ts); + lines.python!.push(py); + lines.java!.push(java); + lines.cpp!.push(cpp); + }; + const toBits = (num: number) => (num >>> 0).toString(2).padStart(32, '0').split(''); // Initial state generatedSteps.push({ variables: { n: nVal, res: 0 }, - explanation: "Initialize res = 0. We will build the Reversed result bit by bit.", - highlightedLines: [2], - lineExecution: "let res = 0;", + explanation: "Initialize res = 0. We will build the reversed result bit by bit.", + pseudoStep: "SET res = 0", bitsN: toBits(nVal), bitsResult: toBits(res) }); + addLines(2, 2, 3, 4); for (let i = 0; i < 32; i++) { - // Visual mappings: - // Array index 0 is MSB (bit 31), Array index 31 is LSB (bit 0). - // Logic 'i' is bit position from LSB (0..31). - // So bit 'i' is located at array index 31 - i. const visualActiveIndex = 31 - i; - // Step: Loop Check + // Loop check generatedSteps.push({ variables: { n: nVal, res, i }, - explanation: `Loop i = ${i}. Processing bit at position ${i} of n. We shift 'n' right by 'i' positions to bring the target bit to the least significant position.`, - highlightedLines: [3], - lineExecution: `for (let i = 0; i < 32; i++)`, + explanation: `Loop i = ${i}. Processing bit at position ${i} of n. We shift 'n' right by 'i' positions to bring the target bit to the LSB position.`, + pseudoStep: `FOR i = 0 TO 31 (i = ${i})`, bitsN: toBits(nVal), bitsResult: toBits(res), activeBitIndex: visualActiveIndex }); + addLines(3, 3, 4, 5); - // Step: Extract Bit + // Extract bit const bit = (nVal >> i) & 1; generatedSteps.push({ variables: { n: nVal, res, i, bit }, explanation: `Extract i-th bit: (n >> ${i}) & 1 = ${bit}. The bitwise AND with 1 isolates the single bit we care about.`, - highlightedLines: [4], - lineExecution: `const bit = (n >> i) & 1;`, + pseudoStep: `SET bit = (n >> ${i}) & 1 (bit = ${bit})`, bitsN: toBits(nVal), bitsResult: toBits(res), activeBitIndex: visualActiveIndex }); + addLines(4, 4, 5, 6); - // Step: Place Bit + // Place bit const targetPos = 31 - i; - // Target bit position visual index: 31 - targetPos = 31 - (31-i) = i. const visualTargetIndex = i; const shiftVal = bit << targetPos; res |= shiftVal; @@ -90,35 +124,36 @@ export const ReverseBitsVisualization = () => { generatedSteps.push({ variables: { n: nVal, res: res >>> 0, i, bit }, explanation: `Place bit at reversed position ${targetPos}.\nSet bit ${targetPos} in 'res' to ${bit} using bitwise OR (|) after shifting the bit left by ${targetPos}.`, - highlightedLines: [5], - lineExecution: `res |= bit << (31 - i);`, + pseudoStep: `SET res = res | (bit << ${targetPos}) (res = ${res >>> 0})`, bitsN: toBits(nVal), bitsResult: toBits(res), activeBitIndex: visualActiveIndex, targetBitIndex: visualTargetIndex }); + addLines(5, 5, 6, 7); } // Final result generatedSteps.push({ variables: { n: nVal, res: res >>> 0 }, explanation: "Loop complete. Return unsigned 32-bit result. Using '>>> 0' forces JavaScript to treat 'res' as an unsigned 32-bit integer, avoiding negative representations.", - highlightedLines: [7], - lineExecution: "return res >>> 0;", + pseudoStep: "RETURN res >>> 0", bitsN: toBits(nVal), bitsResult: toBits(res) }); + addLines(7, 6, 8, 9); - return generatedSteps; + return { steps: generatedSteps, stepLineNumbers: lines }; }, []); - const step = steps[currentStep] || steps[0]; + const step = steps[currentStepIndex] || steps[0]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( -
+
+

Input n (32 bits)

@@ -141,9 +176,7 @@ export const ReverseBitsVisualization = () => { 0 (LSB)
-
-

Result res (32 bits)

@@ -168,51 +201,30 @@ export const ReverseBitsVisualization = () => {
-
- -
-
-

- Current Execution -

-
- {step.lineExecution} -
-
-
-

- Commentary -

-

- {step.explanation} -

-
-
+
+ +

Step Explanation

+

{step.explanation}

-
-
- } - rightContent={ -
-
- -
- {/* Variable Panel */} -
} + rightContent={ + setCurrentStepIndex(0)} + /> + } controls={ } /> diff --git a/src/components/visualizations/algorithms/ReverseLinkedListVisualization.tsx b/src/components/visualizations/algorithms/ReverseLinkedListVisualization.tsx index 25438ae..1603b94 100644 --- a/src/components/visualizations/algorithms/ReverseLinkedListVisualization.tsx +++ b/src/components/visualizations/algorithms/ReverseLinkedListVisualization.tsx @@ -1,8 +1,9 @@ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useMemo } from 'react'; import { Card } from '@/components/ui/card'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; -import { StepControls } from '../shared/StepControls'; +import { SimpleStepControls } from '../shared/SimpleStepControls'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { VisualizationLanguageMap, StepLineNumberMap } from '@/types/visualization'; interface Step { nodes: number[]; @@ -10,200 +11,262 @@ interface Step { current: number | null; next: number | null; reversedLinks: Set; - message: string; - lineNumber: number; + explanation: string; + pseudoStep: string; variables: Record; } -export const ReverseLinkedListVisualization = () => { - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); +const languages: VisualizationLanguageMap = { + python: `def reverseList(head): + prev = None + curr = head + while curr: + next_node = curr.next + curr.next = prev + prev = curr + curr = next_node + return prev`, + typescript: `function reverseList(head: ListNode | null): ListNode | null { + let prev: ListNode | null = null; + let curr: ListNode | null = head; + while (curr !== null) { + const nextNode: ListNode | null = curr.next; + curr.next = prev; + prev = curr; + curr = nextNode; + } + return prev; +}`, + java: `public class Solution { + public ListNode reverseList(ListNode head) { + ListNode prev = null; + ListNode curr = head; + while (curr != null) { + ListNode nextNode = curr.next; + curr.next = prev; + prev = curr; + curr = nextNode; + } + return prev; + } +}`, + cpp: `class Solution { +public: + ListNode* reverseList(ListNode* head) { + ListNode* prev = nullptr; + ListNode* curr = head; + ListNode* nextNode = nullptr; + while (curr != nullptr) { + nextNode = curr->next; + curr->next = prev; + prev = curr; + curr = nextNode; + } + return prev; + } +};`, +}; +const generateVisualizationData = () => { const list = [1, 2, 3, 4, 5]; + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { typescript: [], python: [], java: [], cpp: [] }; + const reversedLinks = new Set(); + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + // 1. Initial State / Signature + steps.push({ + nodes: list, + prev: null, + current: null, + next: null, + reversedLinks: new Set(reversedLinks), + explanation: "Start with the head of the list.", + pseudoStep: "CALL reverseList(head)", + variables: { head: "Node 1" } + }); + addLines(1, 1, 2, 3); - const steps = useMemo(() => { - const newSteps: Step[] = []; - const reversedLinks = new Set(); + // 2. Initialize prev = null + steps.push({ + nodes: list, + prev: null, + current: null, + next: null, + reversedLinks: new Set(reversedLinks), + explanation: "Initialize 'prev' pointer as null.", + pseudoStep: "SET prev = null", + variables: { prev: "null" } + }); + addLines(2, 2, 3, 4); - // Initial State - newSteps.push({ + // 3. Initialize curr = head + steps.push({ + nodes: list, + prev: null, + current: 0, + next: null, + reversedLinks: new Set(reversedLinks), + explanation: "Initialize 'curr' pointer as the head of the list.", + pseudoStep: "SET curr = head", + variables: { prev: "null", curr: "Node 1" } + }); + addLines(3, 3, 4, 5); + + let prev: number | null = null; + let current: number | null = 0; + + while (current !== null) { + // 4. Loop check + steps.push({ nodes: list, - prev: null, - current: null, + prev, + current, next: null, reversedLinks: new Set(reversedLinks), - message: "Start with the head of the list.", - lineNumber: 1, - variables: { head: "Node 1" } + explanation: `Check if current node is not null. Current: Node ${current + 1}`, + pseudoStep: `WHILE curr ≠ null → Node ${current + 1} ≠ null`, + variables: { + prev: prev !== null ? `Node ${prev + 1}` : "null", + curr: `Node ${current + 1}` + } }); + addLines(4, 4, 5, 7); - // let prev = null; - newSteps.push({ + // 5. Store next + const next: number | null = current + 1 < list.length ? current + 1 : null; + steps.push({ nodes: list, - prev: null, - current: null, - next: null, + prev, + current, + next, reversedLinks: new Set(reversedLinks), - message: "Initialize 'prev' pointer as null.", - lineNumber: 2, - variables: { prev: "null" } + explanation: `Store the next node (Node ${next !== null ? next + 1 : "null"}) before reversing the link pointer.`, + pseudoStep: `SET next = curr.next → Node ${next !== null ? next + 1 : "null"}`, + variables: { + prev: prev !== null ? `Node ${prev + 1}` : "null", + curr: `Node ${current + 1}`, + next: next !== null ? `Node ${next + 1}` : "null" + } }); + addLines(5, 5, 6, 8); - // let current = head; - newSteps.push({ + // 6. Reverse pointer + reversedLinks.add(current); + steps.push({ nodes: list, - prev: null, - current: 0, - next: null, + prev, + current, + next, reversedLinks: new Set(reversedLinks), - message: "Initialize 'current' pointer as the head node.", - lineNumber: 3, - variables: { prev: "null", current: "Node 1" } + explanation: `Point current node's next to 'prev' (Node ${prev !== null ? prev + 1 : "null"}). Pointer reversed!`, + pseudoStep: `SET curr.next = prev → Node ${prev !== null ? prev + 1 : "null"}`, + variables: { + prev: prev !== null ? `Node ${prev + 1}` : "null", + curr: `Node ${current + 1}`, + next: next !== null ? `Node ${next + 1}` : "null" + } }); + addLines(6, 6, 7, 9); - let prev: number | null = null; - let current: number | null = 0; - - while (current !== null) { - // while (current !== null) - newSteps.push({ - nodes: list, - prev, - current, - next: null, - reversedLinks: new Set(reversedLinks), - message: `Check if current node is not null. Current: Node ${current + 1}`, - lineNumber: 4, - variables: { prev: prev !== null ? `Node ${prev + 1}` : "null", current: `Node ${current + 1}` } - }); - - // let next = current.next; - const next: number | null = current + 1 < list.length ? current + 1 : null; - newSteps.push({ - nodes: list, - prev, - current, - next, - reversedLinks: new Set(reversedLinks), - message: `Store the next node (Node ${next !== null ? next + 1 : "null"}) before reversing the pointer.`, - lineNumber: 5, - variables: { prev: prev !== null ? `Node ${prev + 1}` : "null", current: `Node ${current + 1}`, next: next !== null ? `Node ${next + 1}` : "null" } - }); - - // current.next = prev; - reversedLinks.add(current); - newSteps.push({ - nodes: list, - prev, - current, - next, - reversedLinks: new Set(reversedLinks), - message: `Point current node's next to 'prev' (Node ${prev !== null ? prev + 1 : "null"}). Pointer reversed!`, - lineNumber: 6, - variables: { prev: prev !== null ? `Node ${prev + 1}` : "null", current: `Node ${current + 1}`, next: next !== null ? `Node ${next + 1}` : "null" } - }); - - // prev = current; - prev = current; - newSteps.push({ - nodes: list, - prev, - current, - next, - reversedLinks: new Set(reversedLinks), - message: "Move 'prev' pointer forward to the current node.", - lineNumber: 7, - variables: { prev: `Node ${prev + 1}`, current: `Node ${current + 1}`, next: next !== null ? `Node ${next + 1}` : "null" } - }); - - // current = next; - current = next; - newSteps.push({ - nodes: list, - prev, - current, - next: null, // Reset next visually for the next iteration start - reversedLinks: new Set(reversedLinks), - message: "Move 'current' pointer forward to the next node.", - lineNumber: 8, - variables: { prev: `Node ${prev + 1}`, current: current !== null ? `Node ${current + 1}` : "null" } - }); - } - - // Final while check - newSteps.push({ + // 7. Move prev + prev = current; + steps.push({ nodes: list, prev, - current: null, - next: null, + current, + next, reversedLinks: new Set(reversedLinks), - message: "Current is null, the loop ends.", - lineNumber: 4, - variables: { prev: `Node ${prev + 1}`, current: "null" } + explanation: "Move 'prev' pointer forward to the current node.", + pseudoStep: `SET prev = curr → Node ${prev + 1}`, + variables: { + prev: `Node ${prev + 1}`, + curr: `Node ${current + 1}`, + next: next !== null ? `Node ${next + 1}` : "null" + } }); + addLines(7, 7, 8, 10); - // return prev; - newSteps.push({ + // 8. Move curr + current = next; + steps.push({ nodes: list, prev, - current: null, + current, next: null, reversedLinks: new Set(reversedLinks), - message: "Return 'prev' as the new head of the reversed list.", - lineNumber: 10, - variables: { return: `Node ${prev! + 1}` } + explanation: "Move 'curr' pointer forward to the stored next node.", + pseudoStep: `SET curr = next → Node ${current !== null ? current + 1 : "null"}`, + variables: { + prev: `Node ${prev + 1}`, + curr: current !== null ? `Node ${current + 1}` : "null" + } }); + addLines(8, 8, 9, 11); + } - return newSteps; - }, []); + // 9. Loop check failed + steps.push({ + nodes: list, + prev, + current: null, + next: null, + reversedLinks: new Set(reversedLinks), + explanation: "Current is null. The traversal is complete and loop terminates.", + pseudoStep: "WHILE curr ≠ null → FALSE ✗", + variables: { + prev: `Node ${prev + 1}`, + curr: "null" + } + }); + addLines(4, 4, 5, 7); - const currentStep = steps[currentStepIndex]; + // 10. Return result + steps.push({ + nodes: list, + prev, + current: null, + next: null, + reversedLinks: new Set(reversedLinks), + explanation: `Return 'prev' (Node ${prev + 1}) as the new head of the reversed list.`, + pseudoStep: `RETURN prev → Node ${prev + 1}`, + variables: { return: `Node ${prev + 1}` } + }); + addLines(10, 9, 11, 13); - useEffect(() => { - let timer: NodeJS.Timeout; - if (isPlaying && currentStepIndex < steps.length - 1) { - timer = setTimeout(() => { - setCurrentStepIndex(prev => prev + 1); - }, 1500 / speed); - } else { - setIsPlaying(false); - } - return () => clearTimeout(timer); - }, [isPlaying, currentStepIndex, steps.length, speed]); + return { steps, stepLineNumbers }; +}; - const code = `function reverseList(head: ListNode | null): ListNode | null { - let prev = null; - let current = head; - while (current !== null) { - let next = current.next; - current.next = prev; - prev = current; - current = next; - } - return prev; -}`; +export const ReverseLinkedListVisualization = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + + const { steps, stepLineNumbers } = useMemo(() => { + return generateVisualizationData(); + }, []); + + const currentStep = steps[currentStepIndex] || steps[steps.length - 1]; + const pseudoSteps = steps.map(s => s.pseudoStep); return (
- setIsPlaying(true)} - onPause={() => setIsPlaying(false)} - onStepForward={() => setCurrentStepIndex(prev => Math.min(steps.length - 1, prev + 1))} - onStepBack={() => setCurrentStepIndex(prev => Math.max(0, prev - 1))} - onReset={() => { - setCurrentStepIndex(0); - setIsPlaying(false); - }} - isPlaying={isPlaying} - currentStep={currentStepIndex} - totalSteps={steps.length - 1} - speed={speed} - onSpeedChange={setSpeed} - /> + {/* Controls at Top */} +
+ +
-
+ {/* Left Column: Visual Representation & Variables & Commentary */} +

Linked List View

@@ -232,14 +295,15 @@ export const ReverseLinkedListVisualization = () => { return (
-
+ ? "bg-blue-500/20 border-blue-500 text-foreground" + : "bg-muted border-border text-foreground" + }`}> {val}
{idx < currentStep.nodes.length - 1 && ( @@ -253,17 +317,28 @@ export const ReverseLinkedListVisualization = () => {
-
-

{currentStep.message}

+ {/* Descriptive Commentary Box (at the bottom) */} +
+
+
+ Process Step +
+ {currentStep.explanation}
- + {/* Variable Panel (below the commentary box) */} +
+ +
- setCurrentStepIndex(0)} />
diff --git a/src/components/visualizations/algorithms/RotateArrayVisualization.tsx b/src/components/visualizations/algorithms/RotateArrayVisualization.tsx index b3b4faa..5106736 100644 --- a/src/components/visualizations/algorithms/RotateArrayVisualization.tsx +++ b/src/components/visualizations/algorithms/RotateArrayVisualization.tsx @@ -1,132 +1,226 @@ import { useEffect, useRef, useState } from 'react'; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { array: number[]; k: number; - message: string; + explanation: string; + pseudoStep: string; + variables: Record; highlightIndices: number[]; - lineNumber: number; left?: number; right?: number; phase?: string; } -export const RotateArrayVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const intervalRef = useRef(null); +// ─── Hardcoded code per language (no comments) ────────────────────────────── - const code = `function rotate(nums, k) { - const n = nums.length; - k = k % n; - if (k === 0) return; - - // 1. Reverse entire array - reverse(nums, 0, n - 1); - - // 2. Reverse first k elements - reverse(nums, 0, k - 1); - - // 3. Reverse remaining elements - reverse(nums, k, n - 1); -} - -function reverse(nums, left, right) { - while (left < right) { - [nums[left], nums[right]] = [nums[right], nums[left]]; - left++; - right--; +const languages: VisualizationLanguageMap = { + typescript: `function rotate(nums: number[], k: number): void { + k = k % nums.length; + let l = 0; + let r = nums.length - 1; + while (l < r) { + [nums[l], nums[r]] = [nums[r], nums[l]]; + l++; + r--; + } + l = 0; + r = k - 1; + while (l < r) { + [nums[l], nums[r]] = [nums[r], nums[l]]; + l++; + r--; + } + l = k; + r = nums.length - 1; + while (l < r) { + [nums[l], nums[r]] = [nums[r], nums[l]]; + l++; + r--; + } +}`, + + python: `def rotate(nums: List[int], k: int) -> None: + n = len(nums) + if n == 0: + return + k = k % n + def reverse(l, r): + while l < r: + nums[l], nums[r] = nums[r], nums[l] + l += 1 + r -= 1 + reverse(0, n - 1) + reverse(0, k - 1) + reverse(k, n - 1)`, + + java: `public static class Solution { + public void rotate(int[] nums, Integer k) { + if (nums == null || nums.length == 0) return; + if (k == null) k = 0; + int n = nums.length; + k = k % n; + reverse(nums, 0, n - 1); + reverse(nums, 0, k - 1); + reverse(nums, k, n - 1); } -}`; - - const reverse = (arr: number[], left: number, right: number) => { + private void reverse(int[] nums, int l, int r) { + while (l < r) { + int temp = nums[l]; + nums[l] = nums[r]; + nums[r] = temp; + l++; + r--; + } + } +}`, + + cpp: `class Solution { +public: + void rotate(vector &nums, int k) { + if (nums.empty()) return; + int n = nums.size(); + k = k % n; + reverse(nums, 0, n - 1); + reverse(nums, 0, k - 1); + reverse(nums, k, n - 1); + } + void reverse(vector &nums, int left, int right) { while (left < right) { - [arr[left], arr[right]] = [arr[right], arr[left]]; - left++; - right--; + swap(nums[left++], nums[right--]); } +} +};`, +}; + +// ─── Step generator ────────────────────────────────────────────────────────── + +function generateVisualizationData() { + const nums = [1, 2, 3, 4, 5, 6, 7]; + const n = nums.length; + let k = 3; + const steps: Step[] = []; + const currentArray = [...nums]; + + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] }; - const generateSteps = () => { - const nums = [1, 2, 3, 4, 5, 6, 7]; - const n = nums.length; - let k = 3; - const newSteps: Step[] = []; + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - const addStep = (arr: number[], msg: string, line: number, l?: number, r?: number, highlights: number[] = [], phase?: string) => { - newSteps.push({ - array: [...arr], + const addStep = (arr: number[], msg: string, line: number, l?: number, r?: number, highlights: number[] = [], phase?: string, ts_l = line, py_l = line, java_l = line, cpp_l = line) => { + steps.push({ + array: [...arr], + k, + explanation: msg, + pseudoStep: msg, + variables: { + phase: phase || '-', k, - message: msg, - highlightIndices: highlights, - lineNumber: line, - left: l, - right: r, - phase - }); - }; - - // Initial state - addStep(nums, `Initial array. Rotate by k=${k}`, 1); + left: l !== undefined ? l : '-', + right: r !== undefined ? r : '-', + 'nums[left]': l !== undefined && l >= 0 && l < arr.length ? arr[l] : '-', + 'nums[right]': r !== undefined && r >= 0 && r < arr.length ? arr[r] : '-' + }, + highlightIndices: highlights, + left: l, + right: r, + phase + }); + addLines(ts_l, py_l, java_l, cpp_l); + }; - // k = k % n - addStep(nums, `Calculate k = ${k}%${n} = ${k % n}`, 2); - k = k % n; + addStep(currentArray, `Initial array. Rotate by k = ${k}`, 1, undefined, undefined, [], '-', 1, 1, 2, 3); + steps[steps.length - 1].pseudoStep = `START rotate: k = ${k}`; - if (k === 0) { - addStep(nums, "k is 0, no rotation needed", 3); - setSteps(newSteps); - return; + k = k % n; + addStep(currentArray, `Calculate k = k % n → ${k} % ${n} = ${k}`, 2, undefined, undefined, [], '-', 2, 5, 6, 6); + steps[steps.length - 1].pseudoStep = `SET k = k % n → ${k}`; + + const performReverse = ( + arr: number[], + left: number, + right: number, + ts: { start: number; loop: number; swap: number; move: number }, + py: { start: number; loop: number; swap: number; move: number }, + java: { start: number; loop: number; swap: number; move: number }, + cpp: { start: number; loop: number; swap: number; move: number }, + phaseName: string + ) => { + let l = left; + let r = right; + + addStep(arr, `${phaseName}: Start reversing from index ${l} to ${r}`, ts.start, l, r, [], phaseName, ts.start, py.start, java.start, cpp.start); + steps[steps.length - 1].pseudoStep = `CALL reverse(nums, ${l}, ${r})`; + + while (l < r) { + addStep(arr, `${phaseName}: Check condition: left (${l}) < right (${r}) is true.`, ts.loop, l, r, [l, r], phaseName, ts.loop, py.loop, java.loop, cpp.loop); + steps[steps.length - 1].pseudoStep = `WHILE left < right → ${l} < ${r} → YES ✓`; + + const prevLVal = arr[l]; + const prevRVal = arr[r]; + [arr[l], arr[r]] = [arr[r], arr[l]]; + addStep(arr, `${phaseName}: Swap nums[left] (${prevLVal}) and nums[right] (${prevRVal})`, ts.swap, l, r, [l, r], phaseName, ts.swap, py.swap, java.swap, cpp.swap); + steps[steps.length - 1].pseudoStep = `SWAP nums[left], nums[right] → index ${l} & ${r}`; + + l++; + r--; + addStep(arr, `${phaseName}: Move boundaries inwards: left++, right--.`, ts.move, l, r, [], phaseName, ts.move, py.move, java.move, cpp.move); + steps[steps.length - 1].pseudoStep = `left++, right-- → left = ${l}, right = ${r}`; } - const performReverse = (arr: number[], left: number, right: number, startLine: number, phaseName: string) => { - let l = left; - let r = right; - - addStep(arr, `${phaseName}: Start reversing from index ${l} to ${r}`, startLine, l, r, [], phaseName); - - while (l < r) { - // Highlight before swap - addStep(arr, `${phaseName}: Comparing indices ${l} and ${r}`, 18, l, r, [l, r], phaseName); - - // Swap - [arr[l], arr[r]] = [arr[r], arr[l]]; - addStep(arr, `${phaseName}: Swapped elements at ${l} and ${r}`, 19, l, r, [l, r], phaseName); - - // Increment/Decrement - l++; - r--; - addStep(arr, `${phaseName}: Move pointers`, 20, l, r, [], phaseName); - } - addStep(arr, `${phaseName}: Finished`, startLine, l, r, [], phaseName); - }; - - const currentArray = [...nums]; - - // Step 1: Reverse entire - performReverse(currentArray, 0, n - 1, 6, "Reverse Entire Array"); + addStep(arr, `${phaseName}: Loop terminated because left (${l}) < right (${r}) is false.`, ts.loop, l, r, [], phaseName, ts.loop, py.loop, java.loop, cpp.loop); + steps[steps.length - 1].pseudoStep = `WHILE left < right → ${l} < ${r} → NO ✗`; + }; - // Step 2: Reverse first k - performReverse(currentArray, 0, k - 1, 9, "Reverse First K Elements"); + performReverse(currentArray, 0, n - 1, + { start: 3, loop: 5, swap: 6, move: 7 }, + { start: 11, loop: 7, swap: 8, move: 9 }, + { start: 7, loop: 12, swap: 13, move: 16 }, + { start: 7, loop: 12, swap: 13, move: 13 }, + "Reverse Entire Array" + ); + performReverse(currentArray, 0, k - 1, + { start: 10, loop: 12, swap: 13, move: 14 }, + { start: 12, loop: 7, swap: 8, move: 9 }, + { start: 8, loop: 12, swap: 13, move: 16 }, + { start: 8, loop: 12, swap: 13, move: 13 }, + "Reverse First K Elements" + ); + performReverse(currentArray, k, n - 1, + { start: 17, loop: 19, swap: 20, move: 21 }, + { start: 13, loop: 7, swap: 8, move: 9 }, + { start: 9, loop: 12, swap: 13, move: 16 }, + { start: 9, loop: 12, swap: 13, move: 13 }, + "Reverse Remaining Elements" + ); - // Step 3: Reverse remaining - performReverse(currentArray, k, n - 1, 12, "Reverse Remaining Elements"); + addStep(currentArray, "Rotation complete!", 24, undefined, undefined, [], '-', 24, 13, 10, 10); + steps[steps.length - 1].pseudoStep = "RETURN (rotation done)"; - addStep(currentArray, "Rotation complete!", 14); + return { steps, stepLineNumbers }; +} - setSteps(newSteps); - setCurrentStepIndex(0); - }; +// ─── Component ─────────────────────────────────────────────────────────────── - useEffect(() => { - generateSteps(); - }, []); +export const RotateArrayVisualization = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { @@ -149,17 +243,17 @@ function reverse(nums, left, right) { const handlePlay = () => setIsPlaying(true); const handlePause = () => setIsPlaying(false); - const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(prev => prev + 1); - const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(prev => prev - 1); + const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(p => p + 1); + const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(p => p - 1); const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); - generateSteps(); }; if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return (
@@ -177,19 +271,20 @@ function reverse(nums, left, right) { />
+ {/* Left: visual state */}
- {currentStep.phase && ( -
+ {currentStep.phase && currentStep.phase !== '-' && ( +
{currentStep.phase}
)}
{currentStep.array.map((value, index) => ( -
+
{index} {/* Pointers */} -
+
{index === currentStep.left && ( - L + Left )} {index === currentStep.right && ( - R + Right )}
@@ -215,24 +310,39 @@ function reverse(nums, left, right) {
-
-

{currentStep.message}

+
+

{currentStep.explanation}

-
- +
+

Rotate Array In-Place Strategy:

+
+

• Step 1: Reverse the entire array of size n

+

• Step 2: Reverse the first k elements (indices 0 to k-1)

+

• Step 3: Reverse the remaining n-k elements (indices k to n-1)

+

• Time: O(n) · Space: O(1) in-place without extra space

+
+ +
- + {/* Right: code / pseudocode panel */} +
); diff --git a/src/components/visualizations/algorithms/RotateImageVisualization.tsx b/src/components/visualizations/algorithms/RotateImageVisualization.tsx index 46ad42f..d9ccced 100644 --- a/src/components/visualizations/algorithms/RotateImageVisualization.tsx +++ b/src/components/visualizations/algorithms/RotateImageVisualization.tsx @@ -1,10 +1,12 @@ -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import { motion } from 'framer-motion'; import { Card } from '@/components/ui/card'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { SimpleStepControls } from '../shared/SimpleStepControls'; +import { VisualizationLayout } from '../shared/VisualizationLayout'; import { RotateCw } from 'lucide-react'; +import type { VisualizationLanguageMap, StepLineNumberMap } from '@/types/visualization'; interface Step { matrix: number[][]; @@ -13,227 +15,198 @@ interface Step { swapWith: [number, number][]; variables: Record; explanation: string; - highlightedLines: number[]; - lineExecution: string; + pseudoStep: string; } -export const RotateImageVisualization = () => { - const [currentStepIndex, setCurrentStepIndex] = useState(0); +const languages: VisualizationLanguageMap = { + python: `def rotate(matrix: list[list[int]]) -> None: + l = 0 + r = len(matrix) - 1 + while l < r: + for i in range(r - l): + top = l + bottom = r + topLeft = matrix[top][l + i] + matrix[top][l + i] = matrix[bottom - i][l] + matrix[bottom - i][l] = matrix[bottom][r - i] + matrix[bottom][r - i] = matrix[top + i][r] + matrix[top + i][r] = topLeft + r -= 1 + l += 1`, + + typescript: `function rotate(matrix: number[][]): void { + let l = 0; + let r = matrix.length - 1; + while (l < r) { + for (let i = 0; i < r - l; i++) { + let top = l; + let bottom = r; + let topLeft = matrix[top][l + i]; + matrix[top][l + i] = matrix[bottom - i][l]; + matrix[bottom - i][l] = matrix[bottom][r - i]; + matrix[bottom][r - i] = matrix[top + i][r]; + matrix[top + i][r] = topLeft; + } + r--; + l++; + } +}`, + + java: `public class Solution { + public void rotate(int[][] matrix) { + int n = matrix.length; + for (int i = 0; i < (n + 1) / 2; i++) { + for (int j = 0; j < n / 2; j++) { + int temp = matrix[n - 1 - j][i]; + matrix[n - 1 - j][i] = matrix[n - 1 - i][n - j - 1]; + matrix[n - 1 - i][n - j - 1] = matrix[j][n - 1 - i]; + matrix[j][n - 1 - i] = matrix[i][j]; + matrix[i][j] = temp; + } + } + } +}`, - const steps: Step[] = [ - { - matrix: [[1, 2, 3], [4, 5, 6], [7, 8, 9]], - currentRow: -1, - currentCol: -1, - swapWith: [], - variables: { l: 0, r: 2 }, - explanation: "We start by defining the boundaries of our current layer. Column 0 is our left (l) boundary, and column 2 is our right (r) boundary.", - highlightedLines: [2, 3], - lineExecution: "let l = 0; let r = matrix.length - 1;", - }, - { - matrix: [[1, 2, 3], [4, 5, 6], [7, 8, 9]], - currentRow: -1, - currentCol: -1, - swapWith: [], - variables: { l: 0, r: 2 }, - explanation: "Rotation happens layer by layer. We continue as long as the left boundary is less than the right (l < r), meaning there's still a layer to process.", - highlightedLines: [5], - lineExecution: "while (l < r)", - }, - { - matrix: [[1, 2, 3], [4, 5, 6], [7, 8, 9]], - currentRow: -1, - currentCol: -1, - swapWith: [], - variables: { l: 0, r: 2, i: 0 }, - explanation: "In each layer, we rotate elements in groups of four. The index 'i' tracks our current position relative to the corners. We start at i = 0.", - highlightedLines: [6], - lineExecution: "for (let i = 0; i < r - l; i++)", - }, - { - matrix: [[1, 2, 3], [4, 5, 6], [7, 8, 9]], - currentRow: -1, - currentCol: -1, - swapWith: [], - variables: { l: 0, r: 2, i: 0, top: 0, bottom: 2 }, - explanation: "For a square matrix, the top row index is 'l' and the bottom row index is 'r'. We'll use these to pinpoint the four elements to swap.", - highlightedLines: [7, 8], - lineExecution: "let top = l; let bottom = r;", - }, - { - matrix: [[1, 2, 3], [4, 5, 6], [7, 8, 9]], - currentRow: 0, - currentCol: 0, - swapWith: [], - variables: { l: 0, r: 2, i: 0, top: 0, bottom: 2, topLeft: 1 }, - explanation: "1. SAVE TOP-LEFT: We store matrix[0][0] (value 1) in 'topLeft'. We do this because it's about to be overwritten in the rotation cycle.", - highlightedLines: [10], - lineExecution: "let topLeft = matrix[top][l + i];", - }, - { - matrix: [[7, 2, 3], [4, 5, 6], [7, 8, 9]], - currentRow: 0, - currentCol: 0, - swapWith: [[2, 0]], - variables: { l: 0, r: 2, i: 0, top: 0, bottom: 2, topLeft: 1 }, - explanation: "2. BOTTOM-LEFT TO TOP-LEFT: We move matrix[2][0] (7) up to the top-left position. Notice how the left wall moves up to the top wall.", - highlightedLines: [11], - lineExecution: "matrix[top][l + i] = matrix[bottom - i][l];", - }, - { - matrix: [[7, 2, 3], [4, 5, 6], [9, 8, 9]], - currentRow: 2, - currentCol: 0, - swapWith: [[2, 2]], - variables: { l: 0, r: 2, i: 0, top: 0, bottom: 2, topLeft: 1 }, - explanation: "3. BOTTOM-RIGHT TO BOTTOM-LEFT: We shift matrix[2][2] (9) over to the bottom-left corner. The bottom wall is moving leftward.", - highlightedLines: [12], - lineExecution: "matrix[bottom - i][l] = matrix[bottom][r - i];", - }, - { - matrix: [[7, 2, 3], [4, 5, 6], [9, 8, 3]], - currentRow: 2, - currentCol: 2, - swapWith: [[0, 2]], - variables: { l: 0, r: 2, i: 0, top: 0, bottom: 2, topLeft: 1 }, - explanation: "4. TOP-RIGHT TO BOTTOM-RIGHT: We bring matrix[0][2] (3) down to the bottom-right. The right wall is moving downward.", - highlightedLines: [13], - lineExecution: "matrix[bottom][r - i] = matrix[top + i][r];", - }, - { - matrix: [[7, 2, 1], [4, 5, 6], [9, 8, 3]], - currentRow: 0, - currentCol: 2, - swapWith: [], - variables: { l: 0, r: 2, i: 0, top: 0, bottom: 2, topLeft: 1 }, - explanation: "5. TOP-LEFT TO TOP-RIGHT: Finally, we place our saved 'topLeft' value (1) into the top-right position. The 4-way rotation for the corners is now complete!", - highlightedLines: [14], - lineExecution: "matrix[top + i][r] = topLeft;", - }, - { - matrix: [[7, 2, 1], [4, 5, 6], [9, 8, 3]], - currentRow: -1, - currentCol: -1, - swapWith: [], - variables: { l: 0, r: 2, i: 1 }, - explanation: "Now we increment 'i' to 1. This offset allows us to rotate the middle elements of the walls (the elements between the corners).", - highlightedLines: [6], - lineExecution: "for (let i = 0; i < r - l; i++)", - }, - { - matrix: [[7, 2, 1], [4, 5, 6], [9, 8, 3]], - currentRow: 0, - currentCol: 1, - swapWith: [], - variables: { l: 0, r: 2, i: 1, top: 0, bottom: 2, topLeft: 2 }, - explanation: "1. SAVE TOP: We save matrix[0][1] (value 2). With i=1, are targeting the second element in each 'wall'.", - highlightedLines: [10], - lineExecution: "let topLeft = matrix[top][l + i];", - }, - { - matrix: [[7, 4, 1], [4, 5, 6], [9, 8, 3]], - currentRow: 0, - currentCol: 1, - swapWith: [[1, 0]], - variables: { l: 0, r: 2, i: 1, top: 0, bottom: 2, topLeft: 2 }, - explanation: "2. LEFT TO TOP: matrix[1][0] (4) moves to the top wall at matrix[0][1]. The index 'bottom - i' correctly picks the 'middle' left element.", - highlightedLines: [11], - lineExecution: "matrix[top][l + i] = matrix[bottom - i][l];", - }, - { - matrix: [[7, 4, 1], [8, 5, 6], [9, 8, 3]], - currentRow: 1, - currentCol: 0, - swapWith: [[2, 1]], - variables: { l: 0, r: 2, i: 1, top: 0, bottom: 2, topLeft: 2 }, - explanation: "3. BOTTOM TO LEFT: matrix[2][1] (8) moves to matrix[1][0]. All elements on the bottom shift leftward toward the left wall.", - highlightedLines: [12], - lineExecution: "matrix[bottom - i][l] = matrix[bottom][r - i];", - }, - { - matrix: [[7, 4, 1], [8, 5, 6], [9, 6, 3]], - currentRow: 2, - currentCol: 1, - swapWith: [[1, 2]], - variables: { l: 0, r: 2, i: 1, top: 0, bottom: 2, topLeft: 2 }, - explanation: "4. RIGHT TO BOTTOM: matrix[1][2] (6) moves to matrix[2][1]. All elements on the right shift downward toward the bottom wall.", - highlightedLines: [13], - lineExecution: "matrix[bottom][r - i] = matrix[top + i][r];", - }, - { - matrix: [[7, 4, 1], [8, 5, 2], [9, 6, 3]], - currentRow: 1, - currentCol: 2, - swapWith: [], - variables: { l: 0, r: 2, i: 1, top: 0, bottom: 2, topLeft: 2 }, - explanation: "5. TOP TO RIGHT: The saved value (2) is placed at matrix[1][2]. The middle elements have now finished their clockwise rotation.", - highlightedLines: [14], - lineExecution: "matrix[top + i][r] = topLeft;", - }, - { - matrix: [[7, 4, 1], [8, 5, 2], [9, 6, 3]], - currentRow: -1, - currentCol: -1, - swapWith: [], - variables: { l: 0, r: 2, i: 2 }, - explanation: "We've reached the end of the inner loop (i = 2, which equals r - l). The outermost layer is now fully rotated.", - highlightedLines: [6], - lineExecution: "for (let i = 0; i < r - l; i++)", - }, - { - matrix: [[7, 4, 1], [8, 5, 2], [9, 6, 3]], - currentRow: -1, - currentCol: -1, - swapWith: [], - variables: { l: 1, r: 1 }, - explanation: "Outer layer done! We move inward by incrementing l (to 1) and decrementing r (to 1). We are now targeting the inner core of the matrix.", - highlightedLines: [16, 17], - lineExecution: "r--; l++;", - }, - { - matrix: [[7, 4, 1], [8, 5, 2], [9, 6, 3]], - currentRow: -1, - currentCol: -1, - swapWith: [], - variables: { l: 1, r: 1 }, - explanation: "Condition l < r (1 < 1) is now false. In a 3x3 matrix, the center element doesn't need to be rotated as it remains in the same spot.", - highlightedLines: [5], - lineExecution: "while (l < r)", - }, - { - matrix: [[7, 4, 1], [8, 5, 2], [9, 6, 3]], - currentRow: -1, - currentCol: -1, - swapWith: [], - variables: {}, - explanation: "SUCCESS: The matrix has been rotated 90° clockwise in-place using O(1) extra space. Every wall has shifted exactly one position clockwise.", - highlightedLines: [1], - lineExecution: "// rotation complete", + cpp: `class Solution { +public: + void rotate(vector>& matrix) { + int n = matrix.size(); + for (int i = 0; i < (n + 1) / 2; i++) { + for (int j = 0; j < n / 2; j++) { + int temp = matrix[n - 1 - j][i]; + matrix[n - 1 - j][i] = matrix[n - 1 - i][n - j - 1]; + matrix[n - 1 - i][n - j - 1] = matrix[j][n - 1 - i]; + matrix[j][n - 1 - i] = matrix[i][j]; + matrix[i][j] = temp; + } + } } - ]; +};` +}; + +const generateVisualizationData = () => { + const initialMatrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]; + const n = initialMatrix.length; + const matrix = initialMatrix.map(row => [...row]); + + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - const code = `function rotate(matrix: number[][]): void { let l = 0; - let r = matrix.length - 1; + let r = n - 1; + + const addStep = (msg: string, pseudo: string, tsLine: number, pyLine: number, javaLine: number, cppLine: number, extra: Partial = {}) => { + steps.push({ + matrix: matrix.map(row => [...row]), + currentRow: extra.currentRow ?? -1, + currentCol: extra.currentCol ?? -1, + swapWith: extra.swapWith ?? [], + variables: { + l, + r, + ...extra.variables + }, + explanation: msg, + pseudoStep: pseudo + }); + addLines(tsLine, pyLine, javaLine, cppLine); + }; + + // 1. Initial State + addStep("Initialize left and right pointers to define outer boundary layer.", "CALL rotate(matrix)", 2, 2, 3, 4); while (l < r) { + // 2. Loop check + addStep(`Outer loop check: is left (${l}) < right (${r})?`, `WHILE l < r → ${l} < ${r}`, 4, 4, 4, 5); + for (let i = 0; i < r - l; i++) { - let top = l; - let bottom = r; + const top = l; + const bottom = r; + // 3. Inner loop check + addStep(`Process cell group. Offset i = ${i}. Boundaries: top = ${top}, bottom = ${bottom}.`, `FOR i = ${i} TO ${r - l - 1}`, 5, 5, 5, 6, { + variables: { top, bottom, i } + }); + + // 4. Save top-left + const topLeftVal = matrix[top][l + i]; + addStep( + `1. Save top-left element matrix[${top}][${l + i}] = ${topLeftVal} in a temporary variable.`, + `SET temp = matrix[top][l + i] → ${topLeftVal}`, + 8, 8, 6, 7, + { currentRow: top, currentCol: l + i, variables: { top, bottom, i, temp: topLeftVal } } + ); - let topLeft = matrix[top][l + i]; - matrix[top][l + i] = matrix[bottom - i][l]; - matrix[bottom - i][l] = matrix[bottom][r - i]; - matrix[bottom][r - i] = matrix[top + i][r]; - matrix[top + i][r] = topLeft; + // 5. Bottom-left to top-left + matrix[top][l + i] = matrix[bottom - i][l]; + addStep( + `2. Move bottom-left element matrix[${bottom - i}][${l}] = ${matrix[bottom - i][l]} to top-left.`, + `SET matrix[top][l + i] = matrix[bottom - i][l]`, + 9, 9, 7, 8, + { currentRow: top, currentCol: l + i, swapWith: [[bottom - i, l]], variables: { top, bottom, i, temp: topLeftVal } } + ); + + // 6. Bottom-right to bottom-left + matrix[bottom - i][l] = matrix[bottom][r - i]; + addStep( + `3. Move bottom-right element matrix[${bottom}][${r - i}] = ${matrix[bottom][r - i]} to bottom-left.`, + `SET matrix[bottom - i][l] = matrix[bottom][r - i]`, + 10, 10, 8, 9, + { currentRow: bottom - i, currentCol: l, swapWith: [[bottom, r - i]], variables: { top, bottom, i, temp: topLeftVal } } + ); + + // 7. Top-right to bottom-right + matrix[bottom][r - i] = matrix[top + i][r]; + addStep( + `4. Move top-right element matrix[${top + i}][${r}] = ${matrix[top + i][r]} to bottom-right.`, + `SET matrix[bottom][r - i] = matrix[top + i][r]`, + 11, 11, 9, 10, + { currentRow: bottom, currentCol: r - i, swapWith: [[top + i, r]], variables: { top, bottom, i, temp: topLeftVal } } + ); + + // 8. Temp to top-right + matrix[top + i][r] = topLeftVal; + addStep( + `5. Move saved temp value (${topLeftVal}) to top-right matrix[${top + i}][${r}].`, + `SET matrix[top + i][r] = temp`, + 12, 12, 10, 11, + { currentRow: top + i, currentCol: r, variables: { top, bottom, i, temp: topLeftVal } } + ); } - r--; - l++; + + l += 1; + r -= 1; + addStep(`Move layer boundaries inward. New left = ${l}, right = ${r}.`, "SET r = r - 1, l = l + 1", 14, 13, 4, 5); } -}`; - const currentStep = steps[Math.min(currentStepIndex, steps.length - 1)]; + // Final Complete + addStep("In-place 90° clockwise rotation is complete.", "RETURN", 17, 14, 13, 13); + + return { steps, stepLineNumbers }; +}; + +export const RotateImageVisualization = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + + const { steps, stepLineNumbers } = useMemo(() => { + return generateVisualizationData(); + }, []); + + if (steps.length === 0) return null; + + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); const getCellColor = (row: number, col: number) => { if (!currentStep?.swapWith) return 'bg-muted text-foreground border-border'; @@ -241,33 +214,23 @@ export const RotateImageVisualization = () => { return 'bg-destructive/80 text-destructive-foreground border-destructive shadow-lg'; } if (row === currentStep.currentRow && col === currentStep.currentCol) { - return 'bg-primary text-primary-foreground border-primary shadow-lg scale-110'; + return 'bg-primary text-primary-foreground border-primary shadow-lg scale-110 z-10'; } return 'bg-muted text-foreground border-border'; }; - if (!currentStep?.matrix) { - return
Loading visualization...
; - } - return ( -
- - -
-
- -

+ + +

Matrix State

-
+
{currentStep.matrix.map((row, rowIdx) => (
{row.map((cell, colIdx) => ( @@ -281,7 +244,7 @@ export const RotateImageVisualization = () => { opacity: 1 }} transition={{ type: "spring", stiffness: 300, damping: 25 }} - className={`w-12 h-12 flex items-center justify-center font-mono text-lg font-bold rounded-lg border-2 transition-colors ${getCellColor(rowIdx, colIdx)}`} + className={`w-12 h-12 flex items-center justify-center font-mono text-lg font-bold rounded-lg border transition-colors ${getCellColor(rowIdx, colIdx)}`} > {cell} @@ -289,33 +252,40 @@ export const RotateImageVisualization = () => {
))}
- -
- -

- {currentStep.explanation} -

-
- - -
-
- - n >= 1 && n <= code.split('\n').length)} - /> - -

-
+ {/* Descriptive Commentary Box (at the bottom) */} +
+
+
+ Process Step +
+ {currentStep.explanation} +
+ + {/* Variable Panel (below the commentary box) */} +
+ +
+
+ } + rightContent={ + setCurrentStepIndex(0)} + /> + } + controls={ + + } + /> ); }; \ No newline at end of file diff --git a/src/components/visualizations/algorithms/SearchInRotatedSortedArrayVisualization.tsx b/src/components/visualizations/algorithms/SearchInRotatedSortedArrayVisualization.tsx index aea5c4c..6934c78 100644 --- a/src/components/visualizations/algorithms/SearchInRotatedSortedArrayVisualization.tsx +++ b/src/components/visualizations/algorithms/SearchInRotatedSortedArrayVisualization.tsx @@ -1,274 +1,394 @@ -import { useState, useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { Card } from '@/components/ui/card'; import { VisualizationLayout } from '../shared/VisualizationLayout'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { array: number[]; highlights: number[]; variables: Record; explanation: string; - highlightedLine: number; + pseudoStep: string; } +const languages: VisualizationLanguageMap = { + typescript: `function search(nums: number[], target: number): number { + let left = 0, right = nums.length - 1; + while (left <= right) { + const mid = Math.floor((left + right) / 2); + if (nums[mid] === target) { + return mid; + } + if (nums[left] <= nums[mid]) { + if (nums[left] <= target && target < nums[mid]) { + right = mid - 1; + } else { + left = mid + 1; + } + } else { + if (nums[mid] < target && target <= nums[right]) { + left = mid + 1; + } else { + right = mid - 1; + } + } + } + return -1; +}`, + python: `def search(nums: List[int], target: int) -> int: + left, right = 0, len(nums) - 1 + while left <= right: + mid = (left + right) // 2 + if nums[mid] == target: + return mid + if nums[left] <= nums[mid]: + if nums[left] <= target < nums[mid]: + right = mid - 1 + else: + left = mid + 1 + else: + if nums[mid] < target <= nums[right]: + left = mid + 1 + else: + right = mid - 1 + return -1`, + java: `public static class Solution { + public int search(int[] nums, int target) { + int left = 0, right = nums.length - 1; + while (left <= right) { + int mid = left + (right - left) / 2; + if (nums[mid] == target) { + return mid; + } + if (nums[left] <= nums[mid]) { + if (nums[left] <= target && target < nums[mid]) { + right = mid - 1; + } else { + left = mid + 1; + } + } else { + if (nums[mid] < target && target <= nums[right]) { + left = mid + 1; + } else { + right = mid - 1; + } + } + } + return -1; + } +}`, + cpp: `class Solution { +public: + int search(vector& nums, int target) { + int left = 0, right = nums.size() - 1; + while (left <= right) { + int mid = left + (right - left) / 2; + if (nums[mid] == target) { + return mid; + } + if (nums[left] <= nums[mid]) { + if (nums[left] <= target && target < nums[mid]) { + right = mid - 1; + } else { + left = mid + 1; + } + } else { + if (nums[mid] < target && target <= nums[right]) { + left = mid + 1; + } else { + right = mid - 1; + } + } + } + return -1; + } +};` +}; + export const SearchInRotatedSortedArrayVisualization = () => { - const [currentStepIndex, setCurrentStepIndex] = useState(0); const [steps, setSteps] = useState([]); + const [stepLineNumbers, setStepLineNumbers] = useState({ + typescript: [], + python: [], + java: [], + cpp: [] + }); + const [currentStepIndex, setCurrentStepIndex] = useState(0); const target = 0; - const code = `function search(nums: number[], target: number): number { - let left = 0, right = nums.length - 1; - while (left <= right) { - const mid = Math.floor((left + right) / 2); - if (nums[mid] === target) { - return mid; - } - if (nums[left] <= nums[mid]) { - if (nums[left] <= target && target < nums[mid]) { - right = mid - 1; - } else { - left = mid + 1; - } - } else { - if (nums[mid] < target && target <= nums[right]) { - left = mid + 1; - } else { - right = mid - 1; - } - } - } - return -1; -}`; - useEffect(() => { - const generateSteps = () => { - const nums = [4, 5, 6, 7, 0, 1, 2]; - const newSteps: Step[] = []; + const nums = [4, 5, 6, 7, 0, 1, 2]; + const generatedSteps: Step[] = []; + const stepLines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLines.typescript!.push(ts); + stepLines.python!.push(py); + stepLines.java!.push(java); + stepLines.cpp!.push(cpp); + }; - let left = 0; - let right = nums.length - 1; + // Function entry + generatedSteps.push({ + array: [...nums], + highlights: [], + variables: { left: '-', right: '-', mid: '-', target }, + explanation: `Search for target ${target} in the rotated sorted array using modified binary search.`, + pseudoStep: "FUNCTION search(nums, target)" + }); + addLines(1, 1, 2, 3); - // Line 2 - newSteps.push({ + let left = 0; + let right = nums.length - 1; + + // Pointer Init + generatedSteps.push({ + array: [...nums], + highlights: [left, right], + variables: { left, right, mid: '-', target }, + explanation: `Initialize pointers: left = ${left}, right = ${right}. Search range: [${left}...${right}].`, + pseudoStep: "SET left = 0, right = nums.length - 1" + }); + addLines(2, 2, 3, 4); + + while (left <= right) { + // Loop Check + generatedSteps.push({ array: [...nums], highlights: [left, right], - variables: { left, right, mid: '-', target, found: 'false' }, - explanation: `Initialize pointers: left=${left}, right=${right}. Search range: [${left}...${right}].`, - highlightedLine: 2 + variables: { left, right, mid: '-', target }, + explanation: `Condition left (${left}) <= right (${right}) is True. Continue search loop.`, + pseudoStep: "WHILE left <= right" }); + addLines(3, 3, 4, 5); - while (left <= right) { - // Line 3: while check - newSteps.push({ - array: [...nums], - highlights: [left, right], - variables: { left, right, mid: '-', target, found: 'false' }, - explanation: `Condition left (${left}) <= right (${right}) is True. Enter loop.`, - highlightedLine: 3 - }); + const mid = Math.floor((left + right) / 2); - const mid = Math.floor((left + right) / 2); - - // Line 4: Calculate mid - newSteps.push({ - array: [...nums], - highlights: [left, mid, right], - variables: { left, right, mid, target, found: 'false' }, - explanation: `Calculate mid = floor((${left} + ${right}) / 2) = ${mid}. nums[mid] = ${nums[mid]}.`, - highlightedLine: 4 - }); + // Mid calculation + generatedSteps.push({ + array: [...nums], + highlights: [left, mid, right], + variables: { left, right, mid, target }, + explanation: `Calculate middle index: mid = floor((${left} + ${right}) / 2) = ${mid}. nums[mid] = ${nums[mid]}.`, + pseudoStep: "SET mid = (left + right) / 2" + }); + addLines(4, 4, 5, 6); - // Line 5: if (nums[mid] === target) - newSteps.push({ + // Target Check + generatedSteps.push({ + array: [...nums], + highlights: [mid], + variables: { left, right, mid, target }, + explanation: `Compare nums[mid] (${nums[mid]}) with target (${target}).`, + pseudoStep: "IF nums[mid] == target" + }); + addLines(5, 5, 6, 7); + + if (nums[mid] === target) { + generatedSteps.push({ array: [...nums], highlights: [mid], - variables: { left, right, mid, target, found: 'false' }, - explanation: `Check if nums[mid] (${nums[mid]}) === target (${target}).`, - highlightedLine: 5 + variables: { left, right, mid, target, result: mid }, + explanation: `Target found at index ${mid}! Return ${mid}.`, + pseudoStep: "RETURN mid" }); + addLines(6, 6, 7, 8); + break; + } - if (nums[mid] === target) { - // Line 6 - newSteps.push({ - array: [...nums], - highlights: [mid], - variables: { left, right, mid, target, found: 'true' }, - explanation: `Yes! Found target at index ${mid}. Return ${mid}.`, - highlightedLine: 6 - }); - break; // Stop generator here as we return - } + // Check which half is sorted + generatedSteps.push({ + array: [...nums], + highlights: [left, mid], + variables: { left, right, mid, target }, + explanation: `Determine which half of the array is sorted. Check if nums[left] (${nums[left]}) <= nums[mid] (${nums[mid]}).`, + pseudoStep: "IF nums[left] <= nums[mid]" + }); + addLines(8, 7, 9, 10); - // Line 8: if (nums[left] <= nums[mid]) - newSteps.push({ + if (nums[left] <= nums[mid]) { + // Left half is sorted + generatedSteps.push({ array: [...nums], highlights: [left, mid], - variables: { left, right, mid, target, found: 'false' }, - explanation: `Since not target, determine which half is sorted. Is left half sorted? Check nums[left] (${nums[left]}) <= nums[mid] (${nums[mid]}).`, - highlightedLine: 8 + variables: { left, right, mid, target }, + explanation: `Left half is sorted. Check if target (${target}) lies in the range [nums[left], nums[mid]).`, + pseudoStep: "IF nums[left] <= target AND target < nums[mid]" }); + addLines(9, 8, 10, 11); - if (nums[left] <= nums[mid]) { - // Left half is sorted - // Line 9: if (nums[left] <= target && target < nums[mid]) - newSteps.push({ + if (nums[left] <= target && target < nums[mid]) { + right = mid - 1; + generatedSteps.push({ array: [...nums], - highlights: [left, mid], - variables: { left, right, mid, target, found: 'false' }, - explanation: `Left half is sorted. Is target in this range? Check ${nums[left]} <= ${target} < ${nums[mid]}.`, - highlightedLine: 9 + highlights: [left, right], + variables: { left, right, mid, target }, + explanation: `Target lies in left half. Shrink search space to left side by updating right = mid - 1 = ${right}.`, + pseudoStep: "SET right = mid - 1" }); - - if (nums[left] <= target && target < nums[mid]) { - right = mid - 1; - // Line 10 - newSteps.push({ - array: [...nums], - highlights: [left, right], // right changed - variables: { left, right, mid, target, found: 'false' }, - explanation: `Target is in the left sorted half. Move right pointer to mid - 1 = ${right}.`, - highlightedLine: 10 - }); - } else { - left = mid + 1; - // Line 12 - newSteps.push({ - array: [...nums], - highlights: [left, right], // left changed - variables: { left, right, mid, target, found: 'false' }, - explanation: `Target is NOT in the left sorted half. Move left pointer to mid + 1 = ${left}.`, - highlightedLine: 12 - }); - } + addLines(10, 9, 11, 12); } else { - // Right half is sorted - // Line 15: if (nums[mid] < target && target <= nums[right]) - newSteps.push({ + left = mid + 1; + generatedSteps.push({ array: [...nums], - highlights: [mid, right], - variables: { left, right, mid, target, found: 'false' }, - explanation: `Right half is sorted. Is target in this range? Check ${nums[mid]} < ${target} <= ${nums[right]}.`, - highlightedLine: 15 + highlights: [left, right], + variables: { left, right, mid, target }, + explanation: `Target does not lie in left half. Search right side by updating left = mid + 1 = ${left}.`, + pseudoStep: "SET left = mid + 1" }); - - if (nums[mid] < target && target <= nums[right]) { - left = mid + 1; - // Line 16 - newSteps.push({ - array: [...nums], - highlights: [left, right], - variables: { left, right, mid, target, found: 'false' }, - explanation: `Target is in the right sorted half. Move left pointer to mid + 1 = ${left}.`, - highlightedLine: 16 - }); - } else { - right = mid - 1; - // Line 18 - newSteps.push({ - array: [...nums], - highlights: [left, right], - variables: { left, right, mid, target, found: 'false' }, - explanation: `Target is NOT in the right sorted half. Move right pointer to mid - 1 = ${right}.`, - highlightedLine: 18 - }); - } + addLines(12, 11, 13, 14); } - } - - if (left > right) { - // Line 3: Failed condition - newSteps.push({ + } else { + // Right half is sorted + generatedSteps.push({ array: [...nums], - highlights: [left, right], - variables: { left, right, mid: '-', target, found: 'false' }, - explanation: `Condition left (${left}) <= right (${right}) is False. Loop ends.`, - highlightedLine: 3 + highlights: [mid, right], + variables: { left, right, mid, target }, + explanation: `Right half is sorted. Check if target (${target}) lies in the range (nums[mid], nums[right]].`, + pseudoStep: "IF nums[mid] < target AND target <= nums[right]" }); + addLines(15, 13, 16, 17); - // Line 22 - newSteps.push({ - array: [...nums], - highlights: [], - variables: { left, right, mid: '-', target, found: 'false' }, - explanation: `Target not found in array. Return -1.`, - highlightedLine: 22 - }); + if (nums[mid] < target && target <= nums[right]) { + left = mid + 1; + generatedSteps.push({ + array: [...nums], + highlights: [left, right], + variables: { left, right, mid, target }, + explanation: `Target lies in right half. Search right side by updating left = mid + 1 = ${left}.`, + pseudoStep: "SET left = mid + 1" + }); + addLines(16, 14, 17, 18); + } else { + right = mid - 1; + generatedSteps.push({ + array: [...nums], + highlights: [left, right], + variables: { left, right, mid, target }, + explanation: `Target does not lie in right half. Search left side by updating right = mid - 1 = ${right}.`, + pseudoStep: "SET right = mid - 1" + }); + addLines(18, 16, 19, 20); + } } + } - setSteps(newSteps); - }; + if (left > right) { + // Loop Terminated + generatedSteps.push({ + array: [...nums], + highlights: [], + variables: { left, right, mid: '-', target }, + explanation: `Condition left (${left}) <= right (${right}) is False. Loop terminated.`, + pseudoStep: "WHILE left <= right" + }); + addLines(3, 3, 4, 5); + + // Return -1 + generatedSteps.push({ + array: [...nums], + highlights: [], + variables: { left, right, mid: '-', target, result: -1 }, + explanation: `Target ${target} was not found in the array. Return -1.`, + pseudoStep: "RETURN -1" + }); + addLines(22, 17, 23, 24); + } - generateSteps(); + setSteps(generatedSteps); + setStepLineNumbers(stepLines); }, []); if (steps.length === 0) return null; - const step = steps[currentStepIndex]; + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( + } leftContent={ - <> - -
-

Search in Rotated Sorted Array

-
+
+
+ +

+ Search in Rotated Sorted Array +

+
Target: {target}
-
- {step.array.map((value, index) => { - const l = step.variables.left; - const r = step.variables.right; - const m = step.variables.mid; - - const isL = index === l; - const isR = index === r; - const isM = index === m; - return ( -
-
- {isL && L} - {isM && M} - {isR && R} -
-
- {value} -
- [{index}] -
- ); - })} -
-
- {step.explanation} +
+
+
Rotated Array (nums)
+
+ {currentStep.array.map((value, index) => { + const l = currentStep.variables.left; + const r = currentStep.variables.right; + const m = currentStep.variables.mid; + + const isL = index === l; + const isR = index === r; + const isM = index === m; + + return ( +
+
+ {isL && L} + {isM && M} + {isR && R} +
+
+ {value} +
+ [{index}] +
+ ); + })} +
+
-
-
- - + +
+ +
+ +
+

Step Explanation

+

{currentStep.explanation}

+ + +
+
} rightContent={ - - } - controls={ - setCurrentStepIndex(0)} /> } /> ); }; + diff --git a/src/components/visualizations/algorithms/SegmentTreeVisualization.tsx b/src/components/visualizations/algorithms/SegmentTreeVisualization.tsx index 673cde4..87b7c70 100644 --- a/src/components/visualizations/algorithms/SegmentTreeVisualization.tsx +++ b/src/components/visualizations/algorithms/SegmentTreeVisualization.tsx @@ -1,10 +1,11 @@ import { useState, useMemo } from 'react'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; import { motion, AnimatePresence } from 'framer-motion'; import { Card } from '@/components/ui/card'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { nums: number[]; @@ -18,152 +19,299 @@ interface Step { p1?: number; p2?: number; explanation: string; - highlightedLines: number[]; + pseudoStep: string; variables: Record; phase: 'init' | 'build' | 'query' | 'result' | 'done'; } +const languages: VisualizationLanguageMap = { + typescript: `function solution(nums: number[], left: number, right: number): number { + const n = nums.length; + const tree = new Array(4 * n).fill(0); + buildTree(tree, nums, 0, 0, n - 1); + return queryTree(tree, 0, 0, n - 1, left, right); +} +function buildTree( + tree: number[], + nums: number[], + node: number, + start: number, + end: number +): void { + if (start === end) { + tree[node] = nums[start]; + return; + } + const mid = Math.floor((start + end) / 2); + buildTree(tree, nums, 2 * node + 1, start, mid); + buildTree(tree, nums, 2 * node + 2, mid + 1, end); + tree[node] = tree[2 * node + 1] + tree[2 * node + 2]; +} +function queryTree( + tree: number[], + node: number, + start: number, + end: number, + left: number, + right: number +): number { + if (left > end || right < start) return 0; + if (left <= start && end <= right) return tree[node]; + const mid = Math.floor((start + end) / 2); + const p1 = queryTree(tree, 2 * node + 1, start, mid, left, right); + const p2 = queryTree(tree, 2 * node + 2, mid + 1, end, left, right); + return p1 + p2; +}`, + python: `def buildTree(tree, nums, node, start, end): + if start == end: + tree[node] = nums[start] + return + mid = (start + end) // 2 + buildTree(tree, nums, 2 * node + 1, start, mid) + buildTree(tree, nums, 2 * node + 2, mid + 1, end) + tree[node] = tree[2 * node + 1] + tree[2 * node + 2] +def queryTree(tree, node, start, end, left, right): + if left > end or right < start: + return 0 + if left <= start and end <= right: + return tree[node] + mid = (start + end) // 2 + p1 = queryTree(tree, 2 * node + 1, start, mid, left, right) + p2 = queryTree(tree, 2 * node + 2, mid + 1, end, left, right) + return p1 + p2 +def solution(nums, left, right): + n = len(nums) + tree = [0] * (4 * n) + buildTree(tree, nums, 0, 0, n - 1) + return queryTree(tree, 0, 0, n - 1, left, right)`, + java: `public static class Solution { + public int solution(int[] nums, int left, int right) { + int n = nums.length; + int[] tree = new int[4 * n]; + buildTree(tree, nums, 0, 0, n - 1); + return queryTree(tree, 0, 0, n - 1, left, right); + } + void buildTree(int[] tree, int[] nums, int node, int start, int end) { + if (start == end) { + tree[node] = nums[start]; + return; + } + int mid = (start + end) / 2; + buildTree(tree, nums, 2 * node + 1, start, mid); + buildTree(tree, nums, 2 * node + 2, mid + 1, end); + tree[node] = tree[2 * node + 1] + tree[2 * node + 2]; + } + int queryTree(int[] tree, int node, int start, int end, int left, int right) { + if (left > end || right < start) + return 0; + if (left <= start && end <= right) + return tree[node]; + int mid = (start + end) / 2; + int p1 = queryTree(tree, 2 * node + 1, start, mid, left, right); + int p2 = queryTree(tree, 2 * node + 2, mid + 1, end, left, right); + return p1 + p2; + } +}`, + cpp: `class Solution { + public: + int solution(vector& nums, int left, int right) { + int n = nums.size(); + vector tree(4 * n, 0); + buildTree(tree, nums, 0, 0, n - 1); + return queryTree(tree, 0, 0, n - 1, left, right); + } + void buildTree(vector& tree, vector& nums, int node, int start, int end) { + if (start == end) { + tree[node] = nums[start]; + return; + } + int mid = (start + end) / 2; + buildTree(tree, nums, 2 * node + 1, start, mid); + buildTree(tree, nums, 2 * node + 2, mid + 1, end); + tree[node] = tree[2 * node + 1] + tree[2 * node + 2]; + } + int queryTree(vector& tree, int node, int start, int end, int left, int right) { + if (left > end || right < start) + return 0; + if (left <= start && end <= right) + return tree[node]; + int mid = (start + end) / 2; + int p1 = queryTree(tree, 2 * node + 1, start, mid, left, right); + int p2 = queryTree(tree, 2 * node + 2, mid + 1, end, left, right); + return p1 + p2; + } +};` +}; + export const SegmentTreeVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); + const [currentStepIndex, setCurrentStepIndex] = useState(0); const nums = [1, 3, 5, 7]; const queryRange = { left: 1, right: 3 }; - const steps: Step[] = useMemo(() => { + const { steps, stepLineNumbers } = useMemo(() => { const s: Step[] = []; + const lines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + lines.typescript!.push(ts); + lines.python!.push(py); + lines.java!.push(java); + lines.cpp!.push(cpp); + }; + const tree: (number | null)[] = new Array(4 * nums.length).fill(null); // Initial step s.push({ nums, tree: [...tree], node: 0, start: 0, end: nums.length - 1, explanation: "Initialize input array and empty segment tree.", - highlightedLines: [1, 2, 3], + pseudoStep: "SET tree = [0, ..., 0] (size = 4 * n)", variables: { nums: `[${nums.join(',')}]`, n: nums.length }, phase: 'init' }); + addLines(3, 20, 4, 5); - const buildTree = (tree: number[], nums: number[], node: number, start: number, end: number) => { + const buildTree = (treeArr: number[], numsArr: number[], node: number, start: number, end: number) => { s.push({ - nums, tree: [...tree], node, start, end, + nums: numsArr, tree: [...treeArr], node, start, end, explanation: `Building tree for range [${start}, ${end}] at node ${node}.`, - highlightedLines: [8], + pseudoStep: `CALL buildTree(node = ${node}, start = ${start}, end = ${end})`, variables: { node, start, end }, phase: 'build' }); + addLines(13, 1, 8, 9); if (start === end) { - tree[node] = nums[start]; + treeArr[node] = numsArr[start]; s.push({ - nums, tree: [...tree], node, start, end, - explanation: `Leaf node reached: index ${start} (value ${nums[start]}). Setting tree[${node}] = ${nums[start]}.`, - highlightedLines: [9, 10, 11], - variables: { node, start, value: nums[start] }, + nums: numsArr, tree: [...treeArr], node, start, end, + explanation: `Leaf node reached: index ${start} (value ${numsArr[start]}). Setting tree[${node}] = ${numsArr[start]}.`, + pseudoStep: `IF start == end → SET tree[${node}] = nums[${start}] (${numsArr[start]})`, + variables: { node, start, value: numsArr[start] }, phase: 'build' }); + addLines(15, 3, 10, 11); return; } const mid = Math.floor((start + end) / 2); s.push({ - nums, tree: [...tree], node, start, end, mid, + nums: numsArr, tree: [...treeArr], node, start, end, mid, explanation: `Calculate mid = floor((${start} + ${end}) / 2) = ${mid}.`, - highlightedLines: [14], + pseudoStep: `SET mid = (start + end) / 2 (mid = ${mid})`, variables: { start, end, mid }, phase: 'build' }); + addLines(18, 5, 13, 14); s.push({ - nums, tree: [...tree], node, start, end, mid, + nums: numsArr, tree: [...treeArr], node, start, end, mid, explanation: `Recursively build left subtree for range [${start}, ${mid}].`, - highlightedLines: [15], + pseudoStep: `CALL buildTree(leftChild = ${2 * node + 1}, start = ${start}, mid = ${mid})`, variables: { node: 2 * node + 1, start, end: mid }, phase: 'build' }); - buildTree(tree, nums, 2 * node + 1, start, mid); + addLines(19, 6, 14, 15); + buildTree(treeArr, numsArr, 2 * node + 1, start, mid); s.push({ - nums, tree: [...tree], node, start, end, mid, + nums: numsArr, tree: [...treeArr], node, start, end, mid, explanation: `Recursively build right subtree for range [${mid + 1}, ${end}].`, - highlightedLines: [16], + pseudoStep: `CALL buildTree(rightChild = ${2 * node + 2}, mid+1 = ${mid + 1}, end = ${end})`, variables: { node: 2 * node + 2, start: mid + 1, end }, phase: 'build' }); - buildTree(tree, nums, 2 * node + 2, mid + 1, end); + addLines(20, 7, 15, 16); + buildTree(treeArr, numsArr, 2 * node + 2, mid + 1, end); - tree[node] = tree[2 * node + 1] + tree[2 * node + 2]; + treeArr[node] = treeArr[2 * node + 1] + treeArr[2 * node + 2]; s.push({ - nums, tree: [...tree], node, start, end, mid, - explanation: `Set tree[${node}] = tree[${2 * node + 1}] (${tree[2 * node + 1]}) + tree[${2 * node + 2}] (${tree[2 * node + 2]}) = ${tree[node]}.`, - highlightedLines: [17], - variables: { node, leftChild: tree[2 * node + 1], rightChild: tree[2 * node + 2], sum: tree[node] }, + nums: numsArr, tree: [...treeArr], node, start, end, mid, + explanation: `Set node tree[${node}] to sum of children: ${treeArr[2 * node + 1]} + ${treeArr[2 * node + 2]} = ${treeArr[node]}.`, + pseudoStep: `SET tree[${node}] = tree[left] + tree[right] (${treeArr[2 * node + 1]} + ${treeArr[2 * node + 2]} = ${treeArr[node]})`, + variables: { node, leftChild: treeArr[2 * node + 1], rightChild: treeArr[2 * node + 2], sum: treeArr[node] }, phase: 'build' }); + addLines(21, 8, 16, 17); }; - const queryTree = (tree: number[], node: number, start: number, end: number, left: number, right: number): number => { + const queryTree = (treeArr: number[], node: number, start: number, end: number, left: number, right: number): number => { s.push({ - nums, tree: [...tree], node, start, end, left, right, + nums, tree: [...treeArr], node, start, end, left, right, explanation: `Querying range [${left}, ${right}] against node ${node} (range [${start}, ${end}]).`, - highlightedLines: [20], + pseudoStep: `CALL queryTree(node = ${node}, [${start}, ${end}], query = [${left}, ${right}])`, variables: { node, start, end, left, right }, phase: 'query' }); + addLines(30, 9, 18, 19); if (left > end || right < start) { s.push({ - nums, tree: [...tree], node, start, end, left, right, - explanation: `Query range [${left}, ${right}] is outside segment [${start}, ${end}]. Returning 0.`, - highlightedLines: [21], + nums, tree: [...treeArr], node, start, end, left, right, + explanation: `Query range [${left}, ${right}] is completely outside segment [${start}, ${end}]. Returning 0.`, + pseudoStep: "IF query outside segment → RETURN 0", variables: { left, right, start, end }, phase: 'query' }); + addLines(31, 10, 19, 20); return 0; } if (left <= start && end <= right) { s.push({ - nums, tree: [...tree], node, start, end, left, right, - explanation: `Query range [${left}, ${right}] fully covers segment [${start}, ${end}]. Returning tree[${node}] = ${tree[node]}.`, - highlightedLines: [22], - variables: { left, start, end, right, value: tree[node] }, + nums, tree: [...treeArr], node, start, end, left, right, + explanation: `Query range [${left}, ${right}] fully covers segment [${start}, ${end}]. Returning tree[${node}] = ${treeArr[node]}.`, + pseudoStep: `IF segment fully inside query → RETURN tree[${node}] (${treeArr[node]})`, + variables: { left, start, end, right, value: treeArr[node] }, phase: 'query' }); - return tree[node]; + addLines(32, 12, 21, 22); + return treeArr[node]; } const mid = Math.floor((start + end) / 2); s.push({ - nums, tree: [...tree], node, start, end, left, right, mid, - explanation: `Partial overlap. Calculate mid = ${mid}.`, - highlightedLines: [24], + nums, tree: [...treeArr], node, start, end, left, right, mid, + explanation: `Partial overlap. Split range using mid = ${mid}.`, + pseudoStep: `SET mid = (start + end) / 2 (mid = ${mid})`, variables: { mid }, phase: 'query' }); + addLines(33, 14, 23, 24); s.push({ - nums, tree: [...tree], node, start, end, left, right, mid, + nums, tree: [...treeArr], node, start, end, left, right, mid, explanation: `Recursively query left child for range [${left}, ${right}].`, - highlightedLines: [25], + pseudoStep: `SET p1 = CALL queryTree(leftChild = ${2 * node + 1}, [${start}, ${mid}])`, variables: { node: 2 * node + 1, start, end: mid }, phase: 'query' }); - const p1 = queryTree(tree, 2 * node + 1, start, mid, left, right); + addLines(34, 15, 24, 25); + const p1 = queryTree(treeArr, 2 * node + 1, start, mid, left, right); s.push({ - nums, tree: [...tree], node, start, end, left, right, mid, p1, + nums, tree: [...treeArr], node, start, end, left, right, mid, p1, explanation: `Recursively query right child for range [${left}, ${right}].`, - highlightedLines: [26], + pseudoStep: `SET p2 = CALL queryTree(rightChild = ${2 * node + 2}, [${mid + 1}, ${end}])`, variables: { node: 2 * node + 2, start: mid + 1, end, p1 }, phase: 'query' }); - const p2 = queryTree(tree, 2 * node + 2, mid + 1, end, left, right); + addLines(35, 16, 25, 26); + const p2 = queryTree(treeArr, 2 * node + 2, mid + 1, end, left, right); s.push({ - nums, tree: [...tree], node, start, end, left, right, mid, p1, p2, + nums, tree: [...treeArr], node, start, end, left, right, mid, p1, p2, explanation: `Combine results from children: ${p1} + ${p2} = ${p1 + p2}.`, - highlightedLines: [28], + pseudoStep: `RETURN p1 + p2 (${p1} + ${p2} = ${p1 + p2})`, variables: { p1, p2, sum: p1 + p2 }, phase: 'query' }); + addLines(36, 17, 26, 27); return p1 + p2; }; @@ -173,51 +321,27 @@ export const SegmentTreeVisualization = () => { s.push({ nums, tree: [...tempTree], node: 0, start: 0, end: nums.length - 1, left: queryRange.left, right: queryRange.right, explanation: `Starting range sum query from index ${queryRange.left} to ${queryRange.right}.`, - highlightedLines: [5], + pseudoStep: `CALL queryTree(0, query = [${queryRange.left}, ${queryRange.right}])`, variables: { left: queryRange.left, right: queryRange.right }, phase: 'query' }); + addLines(5, 22, 6, 7); const result = queryTree(tempTree, 0, 0, nums.length - 1, queryRange.left, queryRange.right); s.push({ nums, tree: [...tempTree], node: 0, start: 0, end: nums.length - 1, left: queryRange.left, right: queryRange.right, explanation: `Query result: ${result}. Segment Tree operations complete.`, - highlightedLines: [5], + pseudoStep: `RETURN result (${result})`, variables: { result }, phase: 'done' }); + addLines(5, 22, 6, 7); - return s; + return { steps: s, stepLineNumbers: lines }; }, []); - const code = `function solution(nums: number[], left: number, right: number): number { - const n = nums.length; - const tree = new Array(4 * n).fill(0); - buildTree(tree, nums, 0, 0, n - 1); - return queryTree(tree, 0, 0, n - 1, left, right); -} - -function buildTree(tree: number[], nums: number[], node: number, start: number, end: number): void { - if (start === end) { - tree[node] = nums[start]; - return; - } - const mid = Math.floor((start + end) / 2); - buildTree(tree, nums, 2 * node + 1, start, mid); - buildTree(tree, nums, 2 * node + 2, mid + 1, end); - tree[node] = tree[2 * node + 1] + tree[2 * node + 2]; -} - -function queryTree(tree: number[], node: number, start: number, end: number, left: number, right: number): number { - if (left > end || right < start) return 0; - if (left <= start && end <= right) return tree[node]; - const mid = Math.floor((start + end) / 2); - const p1 = queryTree(tree, 2 * node + 1, start, mid, left, right); - const p2 = queryTree(tree, 2 * node + 2, mid + 1, end, left, right); - return p1 + p2; -}`; - - const step = steps[currentStep] || steps[0]; + const step = steps[currentStepIndex] || steps[0]; + const pseudoSteps = steps.map(s => s.pseudoStep); const renderTree = (node: number, start: number, end: number, x: number, y: number, level: number) => { const isLeaf = start === end; @@ -280,60 +404,66 @@ function queryTree(tree: number[], node: number, start: number, end: number, lef return ( - -

Segment Tree

- -
- - {renderTree(0, 0, nums.length - 1, 200, 40, 0)} - -
- -
-

Input Array

-
- {nums.map((num, idx) => { - const isFocus = idx >= step.start && idx <= step.end; - const isQuery = step.left !== undefined && idx >= step.left && idx <= step.right!; - return ( -
- {num} - {idx} -
- ); - })} +
+
+ +

Segment Tree

+ +
+ + {renderTree(0, 0, nums.length - 1, 200, 40, 0)} +
-
- - -
-

Step Explanation

-

{step.explanation}

- - - +
+

Input Array

+
+ {nums.map((num, idx) => { + const isFocus = idx >= step.start && idx <= step.end; + const isQuery = step.left !== undefined && idx >= step.left && idx <= step.right!; + return ( +
+ {num} + {idx} +
+ ); + })} +
+
+ +
+ +
+ +
+

Step Explanation

+

{step.explanation}

+ + + +
} rightContent={ - setCurrentStepIndex(0)} /> } controls={ } /> diff --git a/src/components/visualizations/algorithms/SerializeTreeVisualization.tsx b/src/components/visualizations/algorithms/SerializeTreeVisualization.tsx index 6a8efa9..999de43 100644 --- a/src/components/visualizations/algorithms/SerializeTreeVisualization.tsx +++ b/src/components/visualizations/algorithms/SerializeTreeVisualization.tsx @@ -1,9 +1,8 @@ import { useEffect, useRef, useState } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface TreeNode { val: number | null; @@ -21,71 +20,169 @@ interface Step { vals: string[]; i: number; message: string; - lineNumber: number; + pseudoStep: string; visitedNodes: Set; builtNodes: Set; + variables: Record; } -export const SerializeTreeVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const [tree, setTree] = useState(null); - const intervalRef = useRef(null); - - const code = `// Encodes a tree to a single string. -function serialize(root: TreeNode | null): string { - const res: string[] = []; +const languages: VisualizationLanguageMap = { + typescript: `function solution(root: TreeNode | null): TreeNode | null { + function serialize(root: TreeNode | null): string { + const res: string[] = [] + function dfs(node: TreeNode | null) { + if (!node) { + res.push("N") + return + } + res.push(String(node.val)) + dfs(node.left) + dfs(node.right) + } + dfs(root) + return res.join(",") + } + function deserialize(data: string): TreeNode | null { + const vals = data.split(",") + let i = 0 + function dfs(): TreeNode | null { + if (vals[i] === "N") { + i++ + return null + } + const node = new TreeNode(parseInt(vals[i])) + i++ + node.left = dfs() + node.right = dfs() + return node + } + return dfs() + } + const encoded = serialize(root) + return deserialize(encoded) +}`, + + python: `def solution(root: TreeNode | None) -> TreeNode | None: + def serialize(root: TreeNode | None) -> str: + res = [] + def dfs(node: TreeNode | None): + if not node: + res.append("N") + return + res.append(str(node.val)) + dfs(node.left) + dfs(node.right) + dfs(root) + return ",".join(res) + def deserialize(data: str) -> TreeNode | None: + vals = data.split(",") + i = 0 + def dfs() -> TreeNode | None: + nonlocal i + if vals[i] == "N": + i += 1 + return None + node = TreeNode(int(vals[i])) + i += 1 + node.left = dfs() + node.right = dfs() + return node + return dfs() + encoded = serialize(root) + return deserialize(encoded)`, + + java: `public static class Solution { + public TreeNode solution(TreeNode root) { + String encoded = serialize(root); + return deserialize(encoded); + } + private String serialize(TreeNode root) { + List res = new ArrayList<>(); + dfsSerialize(root, res); + return String.join(",", res); + } + private void dfsSerialize(TreeNode node, List res) { + if (node == null) { + res.add("N"); + return; + } + res.add(String.valueOf(node.val)); + dfsSerialize(node.left, res); + dfsSerialize(node.right, res); + } + private TreeNode deserialize(String data) { + String[] vals = data.split(","); + int[] i = {0}; + return dfsDeserialize(vals, i); + } + private TreeNode dfsDeserialize(String[] vals, int[] i) { + if (vals[i[0]].equals("N")) { + i[0]++; + return null; + } + TreeNode node = new TreeNode(Integer.parseInt(vals[i[0]])); + i[0]++; + node.left = dfsDeserialize(vals, i); + node.right = dfsDeserialize(vals, i); + return node; + } +}`, - function dfs(node: TreeNode | null): void { - // If node is null, store marker "N" + cpp: `class Solution { +public: + TreeNode* solution(TreeNode* root) { + string encoded = serialize(root); + return deserialize(encoded); + } +private: + void dfsSerialize(TreeNode* node, vector& res) { if (!node) { - res.push("N"); + res.push_back("N"); return; } - - // Store current node value - res.push(node.val.toString()); - - // Traverse left subtree - dfs(node.left); - - // Traverse right subtree - dfs(node.right); + res.push_back(to_string(node->val)); + dfsSerialize(node->left, res); + dfsSerialize(node->right, res); } - - dfs(root); - return res.join(","); -} - -// Decodes your encoded data to tree. -function deserialize(data: string): TreeNode | null { - const vals = data.split(","); - let i = 0; - - function dfs(): TreeNode | null { - // If value is "N", it represents null node - if (vals[i] === "N") { + string serialize(TreeNode* root) { + vector res; + dfsSerialize(root, res); + string result = ""; + for (int i = 0; i < res.size(); i++) { + if (i) result += ","; + result += res[i]; + } + return result; + } + TreeNode* dfsDeserialize(vector& vals, int& i) { + if (vals[i] == "N") { i++; - return null; + return NULL; } - - // Create node with current value - const node = new TreeNode(parseInt(vals[i])); + TreeNode* node = new TreeNode(stoi(vals[i])); i++; - - // Recursively build left subtree - node.left = dfs(); - - // Recursively build right subtree - node.right = dfs(); - + node->left = dfsDeserialize(vals, i); + node->right = dfsDeserialize(vals, i); return node; } + TreeNode* deserialize(string data) { + vector vals; + string temp; + stringstream ss(data); + while (getline(ss, temp, ',')) { + vals.push_back(temp); + } + int i = 0; + return dfsDeserialize(vals, i); + } +};`, +}; - return dfs(); -}`; +export const SerializeTreeVisualization = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); const createTree = (): TreeNode => { return { @@ -117,116 +214,95 @@ function deserialize(data: string): TreeNode | null { const generateSteps = () => { const root = createTree(); calculatePositions(root, 200, 50, 100); - setTree(root); - const newSteps: Step[] = []; + const steps: Step[] = []; const res: string[] = []; const visitedNodes = new Set(); - // SERIALIZE PHASE + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + const addStep = ( + phase: 'serialize' | 'deserialize', + currentNodeId: string | null, + msg: string, + pseudo: string, + ts_l: number, py_l: number, java_l: number, cpp_l: number, + iVal: number, + valsVal: string[] = [], + built: Set = new Set() + ) => { + steps.push({ + phase, + currentId: currentNodeId, + serialized: [...res], + vals: [...valsVal], + i: iVal, + message: msg, + pseudoStep: pseudo, + visitedNodes: new Set(visitedNodes), + builtNodes: new Set(built), + variables: phase === 'serialize' ? { + phase: 'Serialize', + 'res': res.join(','), + 'res.length': res.length + } : { + phase: 'Deserialize', + 'i': iVal, + 'vals[i]': valsVal[iVal] || 'null', + 'vals': valsVal.join(',') + } + }); + addLines(ts_l, py_l, java_l, cpp_l); + }; + + // 1. Start serialize + addStep('serialize', null, 'Start serialization of binary tree.', 'serialize(root)', 13, 11, 8, 19, 0); + const serializeDfs = (node: TreeNode | null) => { if (!node) { res.push("N"); - newSteps.push({ - phase: 'serialize', - currentId: null, - serialized: [...res], - vals: [], - i: 0, - message: 'Node is null, store marker "N"', - lineNumber: 7, - visitedNodes: new Set(visitedNodes), - builtNodes: new Set() - }); + addStep('serialize', null, 'Node is null. Push "N" marker.', 'res.push("N")', 6, 6, 13, 10, 0); return; } visitedNodes.add(node.id!); res.push(node.val!.toString()); - newSteps.push({ - phase: 'serialize', - currentId: node.id!, - serialized: [...res], - vals: [], - i: 0, - message: `Visit node with value ${node.val}, store in array`, - lineNumber: 13, - visitedNodes: new Set(visitedNodes), - builtNodes: new Set() - }); + addStep('serialize', node.id!, `Visit node with value ${node.val}. Append it to result.`, `res.push(${node.val})`, 9, 8, 16, 13, 0); - newSteps.push({ - phase: 'serialize', - currentId: node.id!, - serialized: [...res], - vals: [], - i: 0, - message: `Traverse left child of ${node.val}`, - lineNumber: 16, - visitedNodes: new Set(visitedNodes), - builtNodes: new Set() - }); + addStep('serialize', node.id!, `Recursively serialize left subtree of node ${node.val}.`, 'dfs(node.left)', 10, 9, 17, 14, 0); serializeDfs(node.left); - newSteps.push({ - phase: 'serialize', - currentId: node.id!, - serialized: [...res], - vals: [], - i: 0, - message: `Traverse right child of ${node.val}`, - lineNumber: 19, - visitedNodes: new Set(visitedNodes), - builtNodes: new Set() - }); + addStep('serialize', node.id!, `Recursively serialize right subtree of node ${node.val}.`, 'dfs(node.right)', 11, 10, 18, 15, 0); serializeDfs(node.right); }; - newSteps.push({ - phase: 'serialize', - currentId: null, - serialized: [], - vals: [], - i: 0, - message: 'Start serialization', - lineNumber: 1, - visitedNodes: new Set(), - builtNodes: new Set() - }); - serializeDfs(root); const finalSerialized = res.join(","); - newSteps.push({ - phase: 'serialize', - currentId: null, - serialized: [...res], - vals: [], - i: 0, - message: `Serialization complete: "${finalSerialized}"`, - lineNumber: 23, - visitedNodes: new Set(visitedNodes), - builtNodes: new Set() - }); - - // DESERIALIZE PHASE + addStep('serialize', null, `Serialization finished. Encoded tree string: "${finalSerialized}"`, 'return res.join(",")', 14, 12, 9, 25, 0); + + // 2. Start deserialize const vals = finalSerialized.split(","); let i = 0; const builtNodes = new Set(); + addStep('deserialize', null, 'Start deserialization of encoded string.', 'deserialize(encoded)', 30, 26, 23, 46, i, vals, builtNodes); + const deserializeDfs = (nodeTemplate: TreeNode | null): TreeNode | null => { if (vals[i] === "N") { - newSteps.push({ - phase: 'deserialize', - currentId: null, - serialized: [...res], - vals: [...vals], - i: i, - message: `vals[${i}] is "N", represents null node`, - lineNumber: 33, - visitedNodes: new Set(visitedNodes), - builtNodes: new Set(builtNodes) - }); + addStep('deserialize', null, `vals[${i}] is "N". Return null node.`, 'if (vals[i] === "N") → YES ✓', 20, 18, 26, 28, i, vals, builtNodes); i++; return null; } @@ -234,83 +310,32 @@ function deserialize(data: string): TreeNode | null { const nodeVal = parseInt(vals[i]); const currentId = nodeTemplate?.id || `d${i}`; - newSteps.push({ - phase: 'deserialize', - currentId: currentId, - serialized: [...res], - vals: [...vals], - i: i, - message: `Create node with current value ${nodeVal}`, - lineNumber: 39, - visitedNodes: new Set(visitedNodes), - builtNodes: new Set(builtNodes) - }); - + addStep('deserialize', currentId, `Create new tree node with value ${nodeVal}.`, `node = new TreeNode(${nodeVal})`, 24, 21, 30, 32, i, vals, builtNodes); builtNodes.add(currentId); i++; - newSteps.push({ - phase: 'deserialize', - currentId: currentId, - serialized: [...res], - vals: [...vals], - i: i, - message: `Recursively build left subtree for node ${nodeVal}`, - lineNumber: 43, - visitedNodes: new Set(visitedNodes), - builtNodes: new Set(builtNodes) - }); + addStep('deserialize', currentId, `Recursively rebuild left child for node ${nodeVal}.`, 'node.left = dfs()', 26, 23, 32, 34, i, vals, builtNodes); deserializeDfs(nodeTemplate?.left || null); - newSteps.push({ - phase: 'deserialize', - currentId: currentId, - serialized: [...res], - vals: [...vals], - i: i, - message: `Recursively build right subtree for node ${nodeVal}`, - lineNumber: 46, - visitedNodes: new Set(visitedNodes), - builtNodes: new Set(builtNodes) - }); + addStep('deserialize', currentId, `Recursively rebuild right child for node ${nodeVal}.`, 'node.right = dfs()', 27, 24, 33, 35, i, vals, builtNodes); deserializeDfs(nodeTemplate?.right || null); return { val: nodeVal, left: null, right: null }; }; - newSteps.push({ - phase: 'deserialize', - currentId: null, - serialized: [...res], - vals: [...vals], - i: 0, - message: 'Start deserialization', - lineNumber: 27, - visitedNodes: new Set(visitedNodes), - builtNodes: new Set() - }); - deserializeDfs(root); - newSteps.push({ - phase: 'deserialize', - currentId: null, - serialized: [...res], - vals: [...vals], - i: i, - message: 'Deserialization complete! Tree reconstructed.', - lineNumber: 52, - visitedNodes: new Set(visitedNodes), - builtNodes: new Set(builtNodes) - }); - - setSteps(newSteps); - setCurrentStepIndex(0); + addStep('deserialize', null, 'Deserialization complete! Tree reconstructed successfully.', 'return root', 33, 28, 4, 5, i, vals, builtNodes); + + return { steps, stepLineNumbers }; }; - useEffect(() => { - generateSteps(); - }, []); + const [{ steps, stepLineNumbers }] = useState(generateSteps); + const [tree] = useState(() => { + const root = createTree(); + calculatePositions(root, 200, 50, 100); + return root; + }); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { @@ -322,7 +347,7 @@ function deserialize(data: string): TreeNode | null { } return prev + 1; }); - }, 1000 / speed); + }, 1200 / speed); } else { if (intervalRef.current) clearInterval(intervalRef.current); } @@ -338,12 +363,12 @@ function deserialize(data: string): TreeNode | null { const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); - generateSteps(); }; if (steps.length === 0 || !tree) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); const renderTree = (node: TreeNode | null): JSX.Element | null => { if (!node || node.x === undefined || node.y === undefined) return null; @@ -352,7 +377,6 @@ function deserialize(data: string): TreeNode | null { const isVisited = currentStep.visitedNodes.has(node.id!); const isBuilt = currentStep.builtNodes.has(node.id!); - // In deserialize phase, only show nodes that are being built or already built const shouldShow = currentStep.phase === 'serialize' || isBuilt || isActive; return ( @@ -363,7 +387,7 @@ function deserialize(data: string): TreeNode | null { stroke="currentColor" strokeWidth="2" className={`transition-all duration-300 ${shouldShow && (currentStep.phase === 'serialize' || (isBuilt && currentStep.builtNodes.has(node.left.id!))) ? 'text-border opacity-100' - : 'text-border opacity-20' + : 'text-border opacity-10' }`} /> )} @@ -373,7 +397,7 @@ function deserialize(data: string): TreeNode | null { stroke="currentColor" strokeWidth="2" className={`transition-all duration-300 ${shouldShow && (currentStep.phase === 'serialize' || (isBuilt && currentStep.builtNodes.has(node.right.id!))) ? 'text-border opacity-100' - : 'text-border opacity-20' + : 'text-border opacity-10' }`} /> )} @@ -381,10 +405,13 @@ function deserialize(data: string): TreeNode | null { cx={node.x} cy={node.y} r="20" - className={`transition-all duration-500 ${isActive ? 'fill-primary stroke-primary scale-110 shadow-lg' : - (currentStep.phase === 'serialize' && isVisited) ? 'fill-primary/20 stroke-primary' : - isBuilt ? 'fill-green-500/20 stroke-green-500' : - 'fill-background stroke-black' + className={`transition-all duration-500 ${isActive + ? 'fill-primary stroke-primary animate-pulse' + : (currentStep.phase === 'serialize' && isVisited) + ? 'fill-primary/20 stroke-primary' + : isBuilt + ? 'fill-green-600/20 stroke-green-600' + : 'fill-card stroke-border' }`} strokeWidth="2" /> @@ -393,9 +420,11 @@ function deserialize(data: string): TreeNode | null { y={node.y} textAnchor="middle" dy=".3em" - className={`font-bold transition-all duration-300 ${isActive ? 'fill-white' : - (currentStep.phase === 'serialize' && isVisited) || isBuilt ? 'fill-foreground' : - 'fill-black' + className={`text-sm font-semibold transition-all duration-300 ${isActive + ? 'fill-white' + : (currentStep.phase === 'serialize' && isVisited) || isBuilt + ? 'fill-foreground' + : 'fill-muted-foreground' }`} > {node.val} @@ -408,7 +437,6 @@ function deserialize(data: string): TreeNode | null { return (
-
+ {/* Left: visual tree + commentary box + variable panel */}
- + {renderTree(tree)} -
-
+
+
Serialize
-
+
Deserialize
- - -

{currentStep.message}

-
-
- -
+
+

{currentStep.message}

+
+ +

{currentStep.phase === 'serialize' ? 'Result Array (res):' : 'Values Array (vals):'} {currentStep.phase === 'deserialize' && ( @@ -475,36 +495,17 @@ function deserialize(data: string): TreeNode | null {

-
- -
+
-
-
- -
- -
-

Pro Tip: Watch how the marker "N" is used for null nodes in preorder traversal (Root → Left → Right). This allows unique reconstruction of the tree without extra info.

-
-
+ {/* Right: code / pseudocode panel */} +
); diff --git a/src/components/visualizations/algorithms/SetMatrixZeroesVisualization.tsx b/src/components/visualizations/algorithms/SetMatrixZeroesVisualization.tsx index f2ef4e5..89ecdc9 100644 --- a/src/components/visualizations/algorithms/SetMatrixZeroesVisualization.tsx +++ b/src/components/visualizations/algorithms/SetMatrixZeroesVisualization.tsx @@ -1,289 +1,363 @@ -import { useEffect, useRef, useState } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { Info } from 'lucide-react'; +import { useState, useMemo } from 'react'; import { Card } from '@/components/ui/card'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; -import { StepControls } from '../shared/StepControls'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import { SimpleStepControls } from '../shared/SimpleStepControls'; +import { VisualizationLayout } from '../shared/VisualizationLayout'; +import type { VisualizationLanguageMap, StepLineNumberMap } from '@/types/visualization'; interface Step { matrix: number[][]; variables: Record; explanation: string; - highlightedLines: number[]; + pseudoStep: string; phase: string; currentRow?: number; currentCol?: number; } -export const SetMatrixZeroesVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const intervalRef = useRef(null); - - const code = `function setZeroes(matrix: number[][]): void { - const m = matrix.length, n = matrix[0].length; - let firstRowZero = false, firstColZero = false; - - for (let i = 0; i < m; i++) { - if (matrix[i][0] === 0) { - firstColZero = true; - } - } - - for (let j = 0; j < n; j++) { - if (matrix[0][j] === 0) { - firstRowZero = true; - } - } - - for (let i = 1; i < m; i++) { - for (let j = 1; j < n; j++) { - if (matrix[i][j] === 0) { - matrix[i][0] = 0; - matrix[0][j] = 0; +const languages: VisualizationLanguageMap = { + python: `def setZeroes(matrix): + ROWS = len(matrix) + COLS = len(matrix[0]) + rowZero = False + for r in range(ROWS): + for c in range(COLS): + if matrix[r][c] == 0: + matrix[0][c] = 0 + if r > 0: + matrix[r][0] = 0 + else: + rowZero = True + for r in range(1, ROWS): + for c in range(1, COLS): + if matrix[0][c] == 0 or matrix[r][0] == 0: + matrix[r][c] = 0 + if matrix[0][0] == 0: + for r in range(ROWS): + matrix[r][0] = 0 + if rowZero: + for c in range(COLS): + matrix[0][c] = 0 + return matrix`, + + typescript: `function setZeroes(matrix: number[][]): number[][] { + const ROWS = matrix.length; + const COLS = matrix[0].length; + let rowZero = false; + for (let r = 0; r < ROWS; r++) { + for (let c = 0; c < COLS; c++) { + if (matrix[r][c] === 0) { + matrix[0][c] = 0; + if (r > 0) { + matrix[r][0] = 0; + } else { + rowZero = true; + } } } } - - for (let i = 1; i < m; i++) { - for (let j = 1; j < n; j++) { - if (matrix[i][0] === 0 || matrix[0][j] === 0) { - matrix[i][j] = 0; + for (let r = 1; r < ROWS; r++) { + for (let c = 1; c < COLS; c++) { + if (matrix[0][c] === 0 || matrix[r][0] === 0) { + matrix[r][c] = 0; } } } - - if (firstColZero) { - for (let i = 0; i < m; i++) { - matrix[i][0] = 0; + if (matrix[0][0] === 0) { + for (let r = 0; r < ROWS; r++) { + matrix[r][0] = 0; } } - - if (firstRowZero) { - for (let j = 0; j < n; j++) { - matrix[0][j] = 0; + if (rowZero) { + for (let c = 0; c < COLS; c++) { + matrix[0][c] = 0; } } -}`; - - const generateSteps = () => { - const initialMatrix = [ - [1, 1, 1], - [1, 0, 1], - [1, 1, 1] - ]; - const m = initialMatrix.length; - const n = initialMatrix[0].length; - const matrix = initialMatrix.map(row => [...row]); - const newSteps: Step[] = []; + return matrix; +}`, + + java: `public class Solution { + public int[][] setZeroes(int[][] matrix) { + int ROWS = matrix.length; + int COLS = matrix[0].length; + boolean rowZero = false; + for (int r = 0; r < ROWS; r++) { + for (int c = 0; c < COLS; c++) { + if (matrix[r][c] == 0) { + matrix[0][c] = 0; + if (r > 0) { + matrix[r][0] = 0; + } else { + rowZero = true; + } + } + } + } + for (int r = 1; r < ROWS; r++) { + for (int c = 1; c < COLS; c++) { + if (matrix[0][c] == 0 || matrix[r][0] == 0) { + matrix[r][c] = 0; + } + } + } + if (matrix[0][0] == 0) { + for (int r = 0; r < ROWS; r++) { + matrix[r][0] = 0; + } + } + if (rowZero) { + for (int c = 0; c < COLS; c++) { + matrix[0][c] = 0; + } + } + return matrix; + } +}`, + + cpp: `class Solution { +public: + vector> setZeroes(vector>& matrix) { + int ROWS = matrix.size(); + int COLS = matrix[0].size(); + bool rowZero = false; + for (int r = 0; r < ROWS; r++) { + for (int c = 0; c < COLS; c++) { + if (matrix[r][c] == 0) { + matrix[0][c] = 0; + if (r > 0) { + matrix[r][0] = 0; + } else { + rowZero = true; + } + } + } + } + for (int r = 1; r < ROWS; r++) { + for (int c = 1; c < COLS; c++) { + if (matrix[0][c] == 0 || matrix[r][0] == 0) { + matrix[r][c] = 0; + } + } + } + if (matrix[0][0] == 0) { + for (int r = 0; r < ROWS; r++) { + matrix[r][0] = 0; + } + } + if (rowZero) { + for (int c = 0; c < COLS; c++) { + matrix[0][c] = 0; + } + } + return matrix; + } +};` +}; + +const generateVisualizationData = () => { + const initialMatrix = [ + [1, 1, 1], + [1, 0, 1], + [1, 1, 1] + ]; + const m = initialMatrix.length; + const n = initialMatrix[0].length; + const matrix = initialMatrix.map(row => [...row]); + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - const addStep = (msg: string, lines: number[], phase: string, extra: Partial = {}) => { - newSteps.push({ - matrix: matrix.map(row => [...row]), - explanation: msg, - highlightedLines: lines, - phase, - variables: { - m, - n, - ...extra.variables - }, - currentRow: extra.currentRow, - currentCol: extra.currentCol - }); - }; + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - // 1. Init - addStep("Initialize dimensions and track if the first row/column should be zeroed.", [1, 2, 3], "Initialization", { - variables: { firstRowZero: false, firstColZero: false } + const addStep = (msg: string, pseudo: string, tsLine: number, pyLine: number, javaLine: number, cppLine: number, phase: string, extra: Partial = {}) => { + steps.push({ + matrix: matrix.map(row => [...row]), + explanation: msg, + pseudoStep: pseudo, + phase, + variables: { + m, + n, + ...extra.variables + }, + currentRow: extra.currentRow, + currentCol: extra.currentCol }); + addLines(tsLine, pyLine, javaLine, cppLine); + }; - let firstRowZero = false; - let firstColZero = false; - - // 2. Check first column - for (let i = 0; i < m; i++) { - addStep(`Checking matrix[${i}][0] in the first column.`, [5, 6], "Check First Column", { - currentRow: i, - currentCol: 0, - variables: { firstRowZero, firstColZero, i } - }); - if (matrix[i][0] === 0) { - firstColZero = true; - addStep("Found a 0 in the first column. Set firstColZero to true.", [7], "Check First Column", { - currentRow: i, - currentCol: 0, - variables: { firstRowZero, firstColZero, i } - }); - } - } - - // 3. Check first row - for (let j = 0; j < n; j++) { - addStep(`Checking matrix[0][${j}] in the first row.`, [11, 12], "Check First Row", { - currentRow: 0, - currentCol: j, - variables: { firstRowZero, firstColZero, j } - }); - if (matrix[0][j] === 0) { - firstRowZero = true; - addStep("Found a 0 in the first row. Set firstRowZero to true.", [13], "Check First Row", { - currentRow: 0, - currentCol: j, - variables: { firstRowZero, firstColZero, j } - }); - } - } + // 1. Init + addStep( + "Initialize dimensions and set rowZero flag to false.", + "CALL setZeroes(matrix)", + 1, 1, 2, 3, + "Initialization", + { variables: { rowZero: false } } + ); - // 4. Mark cells - for (let i = 1; i < m; i++) { - for (let j = 1; j < n; j++) { - addStep(`Scanning inner cell matrix[${i}][${j}].`, [17, 18], "Marking Markers", { - currentRow: i, - currentCol: j, - variables: { firstRowZero, firstColZero, i, j } - }); - if (matrix[i][j] === 0) { - matrix[i][0] = 0; - matrix[0][j] = 0; - addStep(`Found 0 at [${i}][${j}]. Use first row/column to mark this row and column.`, [19, 20, 21], "Marking Markers", { - currentRow: i, - currentCol: j, - variables: { firstRowZero, firstColZero, i, j } - }); + let rowZero = false; + + // 2. Scan and mark + for (let r = 0; r < m; r++) { + for (let c = 0; c < n; c++) { + addStep( + `Scanning cell matrix[${r}][${c}]. Check if it is zero.`, + `IF matrix[${r}][${c}] == 0`, + 7, 7, 8, 9, + "Determine zeros", + { currentRow: r, currentCol: c, variables: { rowZero, r, c } } + ); + if (matrix[r][c] === 0) { + matrix[0][c] = 0; + addStep( + `Found 0 at [${r}][${c}]. Mark column header matrix[0][${c}] as 0.`, + `SET matrix[0][${c}] = 0`, + 8, 8, 9, 10, + "Determine zeros", + { currentRow: r, currentCol: c, variables: { rowZero, r, c } } + ); + + if (r > 0) { + matrix[r][0] = 0; + addStep( + `Since row ${r} > 0, mark row header matrix[${r}][0] as 0.`, + `SET matrix[${r}][0] = 0`, + 10, 10, 11, 12, + "Determine zeros", + { currentRow: r, currentCol: c, variables: { rowZero, r, c } } + ); + } else { + rowZero = true; + addStep( + "Since this is the first row, set rowZero flag to true.", + "SET rowZero = true", + 12, 12, 13, 14, + "Determine zeros", + { currentRow: r, currentCol: c, variables: { rowZero, r, c } } + ); } } } + } - // 5. Zero out using markers - for (let i = 1; i < m; i++) { - for (let j = 1; j < n; j++) { - addStep(`Checking markers for matrix[${i}][${j}].`, [25, 26], "Zeroing Inner", { - currentRow: i, - currentCol: j, - variables: { firstRowZero, firstColZero, i, j, 'matrix[i][0]': matrix[i][0], 'matrix[0][j]': matrix[0][j] } - }); - if (matrix[i][0] === 0 || matrix[0][j] === 0) { - matrix[i][j] = 0; - addStep(`Marker found in either row ${i} or column ${j}. Setting matrix[${i}][${j}] to 0.`, [27], "Zeroing Inner", { - currentRow: i, - currentCol: j, - variables: { firstRowZero, firstColZero, i, j } - }); - } + // 3. Zero inner cells + for (let r = 1; r < m; r++) { + for (let c = 1; c < n; c++) { + addStep( + `Checking markers for inner cell [${r}][${c}]. Row marker matrix[${r}][0]: ${matrix[r][0]}, Column marker matrix[0][${c}]: ${matrix[0][c]}.`, + `IF matrix[0][${c}] == 0 OR matrix[${r}][0] == 0`, + 19, 15, 20, 21, + "Zeroing Inner", + { currentRow: r, currentCol: c, variables: { rowZero, r, c } } + ); + if (matrix[0][c] === 0 || matrix[r][0] === 0) { + matrix[r][c] = 0; + addStep( + `Marker found. Set matrix[${r}][${c}] to 0.`, + `SET matrix[${r}][${c}] = 0`, + 20, 16, 21, 22, + "Zeroing Inner", + { currentRow: r, currentCol: c, variables: { rowZero, r, c } } + ); } } + } - // 6. Finalize Column - addStep("Finally, check if the first column itself needs to be zeroed.", [31], "Finalize Column", { - variables: { firstRowZero, firstColZero } - }); - if (firstColZero) { - for (let i = 0; i < m; i++) { - matrix[i][0] = 0; - addStep(`Zeroing first column: matrix[${i}][0] = 0.`, [33], "Finalize Column", { - currentRow: i, - currentCol: 0, - variables: { firstRowZero, firstColZero, i } - }); - } + // 4. Handle first column + addStep( + "Check if the top-left cell matrix[0][0] is marked as 0, which dictates if the first column should be zeroed.", + "IF matrix[0][0] == 0", + 24, 17, 25, 26, + "Handle first column", + { variables: { rowZero } } + ); + if (matrix[0][0] === 0) { + for (let r = 0; r < m; r++) { + matrix[r][0] = 0; + addStep( + `Zeroing first column: setting matrix[${r}][0] = 0.`, + `SET matrix[${r}][0] = 0`, + 26, 19, 28, 29, + "Handle first column", + { currentRow: r, currentCol: 0, variables: { rowZero, r } } + ); } + } - // 7. Finalize Row - addStep("Check if the first row itself needs to be zeroed.", [36], "Finalize Row", { - variables: { firstRowZero, firstColZero } - }); - if (firstRowZero) { - for (let j = 0; j < n; j++) { - matrix[0][j] = 0; - addStep(`Zeroing first row: matrix[0][${j}] = 0.`, [38], "Finalize Row", { - currentRow: 0, - currentCol: j, - variables: { firstRowZero, firstColZero, j } - }); - } + // 5. Handle first row + addStep( + "Check if rowZero flag is true to determine if the first row should be zeroed.", + "IF rowZero", + 29, 20, 30, 31, + "Handle first row", + { variables: { rowZero } } + ); + if (rowZero) { + for (let c = 0; c < n; c++) { + matrix[0][c] = 0; + addStep( + `Zeroing first row: setting matrix[0][${c}] = 0.`, + `SET matrix[0][${c}] = 0`, + 31, 22, 32, 33, + "Handle first row", + { currentRow: 0, currentCol: c, variables: { rowZero, c } } + ); } + } - addStep("Matrix updated successfully in-place with O(1) extra space.", [], "Complete", { - variables: { firstRowZero, firstColZero } - }); - - setSteps(newSteps); - setCurrentStepIndex(0); - }; + // 6. Complete + addStep( + "In-place Set Matrix Zeroes is now complete.", + "RETURN", + 34, 23, 35, 36, + "Complete", + { variables: { rowZero } } + ); - useEffect(() => { - generateSteps(); - }, []); + return { steps, stepLineNumbers }; +}; - useEffect(() => { - if (isPlaying && currentStepIndex < steps.length - 1) { - intervalRef.current = setInterval(() => { - setCurrentStepIndex(prev => { - if (prev >= steps.length - 1) { - setIsPlaying(false); - return prev; - } - return prev + 1; - }); - }, 1000 / speed); - } else { - if (intervalRef.current) clearInterval(intervalRef.current); - } - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - }; - }, [isPlaying, currentStepIndex, steps.length, speed]); +export const SetMatrixZeroesVisualization = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); - const handlePlay = () => setIsPlaying(true); - const handlePause = () => setIsPlaying(false); - const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(prev => prev + 1); - const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(prev => prev - 1); - const handleReset = () => { - setCurrentStepIndex(0); - setIsPlaying(false); - generateSteps(); - }; + const { steps, stepLineNumbers } = useMemo(() => { + return generateVisualizationData(); + }, []); if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); const getCellColor = (row: number, col: number) => { const isCurrent = row === currentStep.currentRow && col === currentStep.currentCol; const value = currentStep.matrix[row][col]; - if (isCurrent) return 'bg-yellow-400 border-yellow-600 text-black z-10 scale-110'; - if (value === 0) return 'bg-red-100 border-red-300 text-black'; - if (row === 0 || col === 0) return 'bg-blue-50 border-blue-200 text-black'; - return 'bg-white border-slate-200 text-black'; + if (isCurrent) return 'bg-yellow-400 border-yellow-600 text-black z-10 scale-110 shadow-md'; + if (value === 0) return 'bg-red-100 dark:bg-red-950/20 border-red-300 dark:border-red-900/50 text-foreground'; + if (row === 0 || col === 0) return 'bg-blue-50 dark:bg-blue-950/10 border-blue-200 dark:border-blue-900/30 text-foreground'; + return 'bg-card border-border text-foreground'; }; return ( -
- - -
+ -
+
Phase: {currentStep.phase}
-
+
{ row.map((cell, colIdx) => (
{cell}
@@ -304,55 +378,37 @@ export const SetMatrixZeroesVisualization = () => {
- - -

{currentStep.explanation}

-
- - -
- -
-
- + {/* Descriptive Commentary Box (at the bottom) */} +
+
+
+ Process Step +
+ {currentStep.explanation}
- -

Algorithm Strategy

-
    -
  • - - To achieve O(1) space, we use the first row and first column of the matrix itself as markers. -
  • -
  • - - We handle the first row and column separately using flags to avoid data loss. -
  • -
  • - - First, overlap flags are determined for row 0 and column 0. -
  • -
  • - - Then, scan the inner matrix and mark matrix[i][0] and matrix[0][j] if matrix[i][j] is 0. -
  • -
  • - - Finally, zero out the cells based on those markers and the initial flags. -
  • -
-
+ {/* Variable Panel (below the commentary box) */} +
+ +
-
-
+ } + rightContent={ + setCurrentStepIndex(0)} + /> + } + controls={ + + } + /> ); }; \ No newline at end of file diff --git a/src/components/visualizations/algorithms/SieveOfEratosthenesVisualization.tsx b/src/components/visualizations/algorithms/SieveOfEratosthenesVisualization.tsx index b222ee0..d3e8f91 100644 --- a/src/components/visualizations/algorithms/SieveOfEratosthenesVisualization.tsx +++ b/src/components/visualizations/algorithms/SieveOfEratosthenesVisualization.tsx @@ -1,276 +1,355 @@ import { useState, useMemo } from 'react'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; import { motion, AnimatePresence } from 'framer-motion'; import { Card } from '@/components/ui/card'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { - isPrime: boolean[]; - primes: number[]; - i: number | null; - j: number | null; - explanation: string; - highlightedLines: number[]; - variables: Record; + isPrime: boolean[]; + primes: number[]; + i: number | null; + j: number | null; + explanation: string; + pseudoStep: string; + variables: Record; } +const languages: VisualizationLanguageMap = { + typescript: `function sieveOfEratosthenes(n: number): number[] { + const isPrime = new Array(n + 1).fill(true); + isPrime[0] = isPrime[1] = false; + for (let i = 2; i * i <= n; i++) { + if (isPrime[i]) { + for (let j = i * i; j <= n; j += i) { + isPrime[j] = false; + } + } + } + const primes: number[] = []; + for (let i = 2; i <= n; i++) { + if (isPrime[i]) primes.push(i); + } + return primes; +}`, + python: `def sieve_of_eratosthenes(n): + is_prime = [True] * (n + 1) + is_prime[0] = is_prime[1] = False + for i in range(2, int(n**0.5) + 1): + if is_prime[i]: + for j in range(i*i, n + 1, i): + is_prime[j] = False + primes = [i for i in range(2, n + 1) if is_prime[i]] + return primes`, + java: `public static class Solution { + public static List sieveOfEratosthenes(int n) { + boolean[] isPrime = new boolean[n + 1]; + Arrays.fill(isPrime, true); + isPrime[0] = isPrime[1] = false; + for (int i = 2; i * i <= n; i++) { + if (isPrime[i]) { + for (int j = i * i; j <= n; j += i) { + isPrime[j] = false; + } + } + } + List primes = new ArrayList<>(); + for (int i = 2; i <= n; i++) { + if (isPrime[i]) { + primes.add(i); + } + } + return primes; + } +}`, + cpp: `class Solution { +public: + vector < int > sieveOfEratosthenes(int n) { + vector < bool > isPrime(n + 1, true); + isPrime[0] = isPrime[1] = false; + for (int i = 2; i * i <= n; i++) { + if (isPrime[i]) { + for (int j = i * i; j <= n; j += i) { + isPrime[j] = false; + } + } + } + vector < int > primes; + for (int i = 2; i <= n; i++) { + if (isPrime[i]) primes.push_back(i); + } + return primes; + } +};` +}; + export const SieveOfEratosthenesVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); - const n = 30; // Using 30 for clear visualization + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const n = 30; // Using 30 for clear visualization - const steps: Step[] = useMemo(() => { - const s: Step[] = []; - const isPrime = new Array(n + 1).fill(true); - const primesFound: number[] = []; + const { steps, stepLineNumbers } = useMemo(() => { + const s: Step[] = []; + const lines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - s.push({ - isPrime: [...isPrime], - primes: [], - i: null, - j: null, - explanation: `Initializing isPrime array of size ${n + 1} with all values as true.`, - highlightedLines: [2], - variables: { n } - }); + const addLines = (ts: number, py: number, java: number, cpp: number) => { + lines.typescript!.push(ts); + lines.python!.push(py); + lines.java!.push(java); + lines.cpp!.push(cpp); + }; - isPrime[0] = isPrime[1] = false; - s.push({ - isPrime: [...isPrime], - primes: [], - i: null, - j: null, - explanation: "Setting isPrime[0] and isPrime[1] to false since 0 and 1 are not prime numbers.", - highlightedLines: [3], - variables: { "isPrime[0]": false, "isPrime[1]": false } - }); + const isPrime = new Array(n + 1).fill(true); + const primesFound: number[] = []; - for (let i = 2; i * i <= n; i++) { - s.push({ - isPrime: [...isPrime], - primes: [], - i, - j: null, - explanation: `Checking if i = ${i} is prime. Loop condition i*i <= n (${i * i} <= ${n}) is true.`, - highlightedLines: [4], - variables: { i, "i*i": i * i, n } - }); + s.push({ + isPrime: [...isPrime], + primes: [], + i: null, + j: null, + explanation: `Initializing isPrime array of size ${n + 1} with all values as true.`, + pseudoStep: `SET isPrime = [true, ..., true] (size = ${n + 1})`, + variables: { n } + }); + addLines(2, 2, 3, 4); - s.push({ - isPrime: [...isPrime], - primes: [], - i, - j: null, - explanation: `Is isPrime[${i}] true? ${isPrime[i] ? "Yes, it is prime." : "No, it's already marked composite."}`, - highlightedLines: [5], - variables: { i, "isPrime[i]": isPrime[i] } - }); + isPrime[0] = isPrime[1] = false; + s.push({ + isPrime: [...isPrime], + primes: [], + i: null, + j: null, + explanation: "Setting isPrime[0] and isPrime[1] to false since 0 and 1 are not prime numbers.", + pseudoStep: "SET isPrime[0] = isPrime[1] = false", + variables: { "isPrime[0]": false, "isPrime[1]": false } + }); + addLines(3, 3, 5, 5); - if (isPrime[i]) { - for (let j = i * i; j <= n; j += i) { - s.push({ - isPrime: [...isPrime], - primes: [], - i, - j, - explanation: `Starting inner loop to mark multiples of ${i}. Initializing j = i * i = ${j}.`, - highlightedLines: [6], - variables: { i, j } - }); + for (let i = 2; i * i <= n; i++) { + s.push({ + isPrime: [...isPrime], + primes: [], + i, + j: null, + explanation: `Checking if i = ${i} is prime. Loop condition i*i <= n (${i * i} <= ${n}) is true.`, + pseudoStep: `FOR i = 2 TO sqrt(n) (i = ${i})`, + variables: { i, "i*i": i * i, n } + }); + addLines(4, 4, 6, 6); - isPrime[j] = false; - s.push({ - isPrime: [...isPrime], - primes: [], - i, - j, - explanation: `Marking j = ${j} as false (composite).`, - highlightedLines: [7], - variables: { i, j, "isPrime[j]": false } - }); - } - } - } + s.push({ + isPrime: [...isPrime], + primes: [], + i, + j: null, + explanation: `Checking if isPrime[${i}] is true: ${isPrime[i] ? "Yes, it is prime." : "No, it's composite."}`, + pseudoStep: `IF isPrime[${i}] → ${isPrime[i] ? "YES ✓" : "NO ✗"}`, + variables: { i, "isPrime[i]": isPrime[i] } + }); + addLines(5, 5, 7, 7); - // After the loop, the code continues to collect primes - s.push({ + if (isPrime[i]) { + for (let j = i * i; j <= n; j += i) { + s.push({ isPrime: [...isPrime], primes: [], - i: null, - j: null, - explanation: "Calculations complete. Initializing empty primes array to collect results.", - highlightedLines: [11], - variables: {} - }); - - for (let i = 2; i <= n; i++) { - s.push({ - isPrime: [...isPrime], - primes: [...primesFound], - i, - j: null, - explanation: `Checking isPrime[${i}] to see if it should be added to the result.`, - highlightedLines: [12, 13], - variables: { i, "isPrime[i]": isPrime[i] } - }); + i, + j, + explanation: `Starting inner loop to mark multiples of ${i}. Initializing j = i * i = ${j}.`, + pseudoStep: `FOR j = i*i TO n (j = ${j})`, + variables: { i, j } + }); + addLines(6, 6, 8, 8); - if (isPrime[i]) { - primesFound.push(i); - s.push({ - isPrime: [...isPrime], - primes: [...primesFound], - i, - j: null, - explanation: `isPrime[${i}] is true, adding ${i} to primes array.`, - highlightedLines: [13], - variables: { i, primes: `[${primesFound.join(', ')}]` } - }); - } + isPrime[j] = false; + s.push({ + isPrime: [...isPrime], + primes: [], + i, + j, + explanation: `Marking j = ${j} as false (composite).`, + pseudoStep: `SET isPrime[${j}] = false`, + variables: { i, j, "isPrime[j]": false } + }); + addLines(7, 7, 9, 9); } + } + } - s.push({ - isPrime: [...isPrime], - primes: [...primesFound], - i: null, - j: null, - explanation: `Returning the final array of prime numbers: [${primesFound.join(', ')}].`, - highlightedLines: [15], - variables: { totalPrimes: primesFound.length } - }); + s.push({ + isPrime: [...isPrime], + primes: [], + i: null, + j: null, + explanation: "Calculations complete. Initializing empty primes array to collect results.", + pseudoStep: "SET primes = []", + variables: {} + }); + addLines(11, 8, 13, 13); - return s; - }, [n]); + for (let i = 2; i <= n; i++) { + s.push({ + isPrime: [...isPrime], + primes: [...primesFound], + i, + j: null, + explanation: `Checking isPrime[${i}] to see if it should be added to the result.`, + pseudoStep: `FOR i = 2 TO n (i = ${i})`, + variables: { i, "isPrime[i]": isPrime[i] } + }); + addLines(12, 8, 14, 14); - const code = `function sieveOfEratosthenes(n: number): number[] { - const isPrime = new Array(n + 1).fill(true); - isPrime[0] = isPrime[1] = false; - for (let i = 2; i * i <= n; i++) { - if (isPrime[i]) { - for (let j = i * i; j <= n; j += i) { - isPrime[j] = false; + if (isPrime[i]) { + primesFound.push(i); + s.push({ + isPrime: [...isPrime], + primes: [...primesFound], + i, + j: null, + explanation: `isPrime[${i}] is true, adding ${i} to primes array.`, + pseudoStep: `CALL primes.push(${i})`, + variables: { i, primes: `[${primesFound.join(', ')}]` } + }); + addLines(13, 8, 15, 15); } } - } - const primes: number[] = []; - for (let i = 2; i <= n; i++) { - if (isPrime[i]) primes.push(i); - } - return primes; -}`; - const step = steps[currentStep]; + s.push({ + isPrime: [...isPrime], + primes: [...primesFound], + i: null, + j: null, + explanation: `Returning the final array of prime numbers: [${primesFound.join(', ')}].`, + pseudoStep: `RETURN primes`, + variables: { totalPrimes: primesFound.length } + }); + addLines(15, 9, 19, 17); - return ( - - -

Number Sieve (0 to {n})

-
- - {step.isPrime.map((isPrime, idx) => { - const isI = idx === step.i; - const isJ = idx === step.j; - const isResult = step.primes.includes(idx); + return { steps: s, stepLineNumbers: lines }; + }, [n]); - let bgColor = "var(--card)"; - let borderColor = "var(--border)"; + const step = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); - if (isI) { - bgColor = "rgba(147, 51, 234, 0.2)"; // purple - borderColor = "rgb(147, 51, 234)"; - } else if (isJ) { - bgColor = "rgba(239, 68, 68, 0.2)"; // red - borderColor = "rgb(239, 68, 68)"; - } else if (isResult) { - bgColor = "rgba(34, 197, 94, 0.3)"; // green - borderColor = "rgb(34, 197, 94)"; - } else if (!isPrime && idx > 1) { - bgColor = "rgba(239, 68, 68, 0.1)"; // light red - borderColor = "rgba(239, 68, 68, 0.3)"; - } else if (isPrime && idx > 1 && step.i === null && currentStep > 2) { - // After main sieve loop, show remaining primes - bgColor = "rgba(34, 197, 94, 0.1)"; - } + return ( + +
+ +

Number Sieve (0 to {n})

+
+ + {step.isPrime.map((isPrimeVal, idx) => { + const isI = idx === step.i; + const isJ = idx === step.j; + const isResult = step.primes.includes(idx); - return ( - - 1 ? "text-muted-foreground line-through" : ""}`}>{idx} -
- {isI && i} - {isJ && mark} -
-
- ); - })} -
-
+ let bgColor = "var(--card)"; + let borderColor = "var(--border)"; + + if (isI) { + bgColor = "rgba(147, 51, 234, 0.2)"; // purple + borderColor = "rgb(147, 51, 234)"; + } else if (isJ) { + bgColor = "rgba(239, 68, 68, 0.2)"; // red + borderColor = "rgb(239, 68, 68)"; + } else if (isResult) { + bgColor = "rgba(34, 197, 94, 0.3)"; // green + borderColor = "rgb(34, 197, 94)"; + } else if (!isPrimeVal && idx > 1) { + bgColor = "rgba(239, 68, 68, 0.1)"; // light red + borderColor = "rgba(239, 68, 68, 0.3)"; + } else if (isPrimeVal && idx > 1 && step.i === null && currentStepIndex > 2) { + // After main sieve loop, show remaining primes + bgColor = "rgba(34, 197, 94, 0.1)"; + } -
-
-
- Current i -
-
-
- Marking j -
-
-
- Prime Result -
-
-
- Composite -
+ return ( + + 1 ? "text-muted-foreground line-through" : ""}`}>{idx} +
+ {isI && i} + {isJ && mark}
- +
+ ); + })} + +
- -

Execution Flow

-

{step.explanation}

-
+
+
+
+ Current i +
+
+
+ Marking j +
+
+
+ Prime Result +
+
+
+ Composite +
+
+
+
+ +
+ +

Execution Flow

+

{step.explanation}

+
- + - {step.primes.length > 0 && ( - -

Primes Array

-
- {step.primes.map(p => ( - {p} - ))} -
-
- )} + {step.primes.length > 0 && ( + +

Primes Array

+
+ {step.primes.map(p => ( + {p} + ))}
- } - rightContent={ - - } - controls={ - - } +
+ )} +
+
+ } + rightContent={ + setCurrentStepIndex(0)} /> - ); + } + controls={ + + } + /> + ); }; diff --git a/src/components/visualizations/algorithms/SlidingWindowMaxVisualization.tsx b/src/components/visualizations/algorithms/SlidingWindowMaxVisualization.tsx index 57bf22f..c6723c2 100644 --- a/src/components/visualizations/algorithms/SlidingWindowMaxVisualization.tsx +++ b/src/components/visualizations/algorithms/SlidingWindowMaxVisualization.tsx @@ -1,10 +1,11 @@ -import { useState, useMemo } from 'react'; -import { SimpleStepControls } from '../shared/SimpleStepControls'; +import { useEffect, useRef, useState, useMemo } from 'react'; +import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; import { motion, AnimatePresence } from 'framer-motion'; import { Card } from '@/components/ui/card'; +import type { VisualizationLanguageMap, StepLineNumberMap } from '@/types/visualization'; interface Step { nums: number[]; @@ -14,156 +15,276 @@ interface Step { q: number[]; output: number[]; explanation: string; - highlightedLines: number[]; + pseudoStep: string; variables: Record; phase: 'init' | 'pop' | 'push' | 'shift' | 'result' | 'done'; } -export const SlidingWindowMaxVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); - const nums = [1, 3, -1, -3, 5, 3, 6, 7, 2, 4, 1, 6]; - const k = 3; - - const steps: Step[] = useMemo(() => { - const s: Step[] = []; +// ─── Hardcoded code per language (No comments, no blank lines) ─────────────── +const languages: VisualizationLanguageMap = { + typescript: `function maxSlidingWindow(nums: number[], k: number): number[] { const output: number[] = []; - let q: number[] = []; + const q: number[] = []; let l = 0; let r = 0; + while (r < nums.length) { + while (q.length && nums[q[q.length - 1]] < nums[r]) { + q.pop(); + } + q.push(r); + if (l > q[0]) { + q.shift(); + } + if ((r + 1) >= k) { + output.push(nums[q[0]]); + l += 1; + } + r += 1; + } + return output; +}`, + + python: `def maxSlidingWindow(nums: list[int], k: int) -> list[int]: + output = [] + q = [] + l = 0 + r = 0 + while r < len(nums): + while q and nums[q[-1]] < nums[r]: + q.pop() + q.append(r) + if l > q[0]: + q.pop(0) + if (r + 1) >= k: + output.append(nums[q[0]]) + l += 1 + r += 1 + return output`, + + java: `public int[] maxSlidingWindow(int[] nums, int k) { + if (nums.length == 0) return new int[0]; + int[] output = new int[nums.length - k + 1]; + Deque q = new ArrayDeque<>(); + int l = 0; + int r = 0; + int idx = 0; + while (r < nums.length) { + while (!q.isEmpty() && nums[q.peekLast()] < nums[r]) { + q.pollLast(); + } + q.offerLast(r); + if (l > q.peekFirst()) { + q.pollFirst(); + } + if ((r + 1) >= k) { + output[idx++] = nums[q.peekFirst()]; + l += 1; + } + r += 1; + } + return output; +}`, + + cpp: `vector maxSlidingWindow(vector& nums, int k) { + vector output; + deque q; + int l = 0; + int r = 0; + while (r < (int)nums.size()) { + while (!q.empty() && nums[q.back()] < nums[r]) { + q.pop_back(); + } + q.push_back(r); + if (l > q.front()) { + q.pop_front(); + } + if ((r + 1) >= k) { + output.push_back(nums[q.front()]); + l += 1; + } + r += 1; + } + return output; +}` +}; - s.push({ - nums, k, l, r, q: [], output: [], - explanation: "Initialize output array and monotonic deque (stores indices of elements).", - highlightedLines: [1, 2, 3], - variables: { nums: `[${nums.join(',')}]`, k }, - phase: 'init' - }); - - s.push({ - nums, k, l, r, q: [], output: [], - explanation: "Initialize pointers 'l' (left) and 'r' (right).", - highlightedLines: [5, 6], - variables: { l, r }, +// ─── Step Generator ────────────────────────────────────────────────────────── +function generateVisualizationData() { + const nums = [1, 3, -1, -3, 5, 3, 6, 7, 2, 4, 1, 6]; + const k = 3; + const steps: Step[] = []; + const output: number[] = []; + let q: number[] = []; + let l = 0; + let r = 0; + + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + steps.push({ + nums, k, l, r, q: [], output: [], + explanation: "Initialize output array and monotonic deque (stores indices of elements).", + pseudoStep: "SET output = [], q = [] (monotonic deque)", + variables: { nums: `[${nums.join(',')}]`, k }, + phase: 'init' + }); + addLines(2, 2, 3, 2); + + steps.push({ + nums, k, l, r, q: [], output: [], + explanation: "Initialize pointers 'l' (left) and 'r' (right).", + pseudoStep: "SET l = 0, r = 0", + variables: { l, r }, + phase: 'init' + }); + addLines(4, 4, 5, 4); + + while (r < nums.length) { + steps.push({ + nums, k, l, r, q: [...q], output: [...output], + explanation: `Checking window ending at index ${r} (value = ${nums[r]}).`, + pseudoStep: `WHILE r (${r}) < nums.length (${nums.length})`, + variables: { l, r, currentVal: nums[r] }, phase: 'init' }); + addLines(6, 6, 8, 6); - while (r < nums.length) { - s.push({ + // Monotonic deque: remove indices of smaller values from back + while (q.length && nums[q[q.length - 1]] < nums[r]) { + const poppedIdx = q[q.length - 1]; + steps.push({ nums, k, l, r, q: [...q], output: [...output], - explanation: `Checking window ending at index ${r} (value = ${nums[r]}).`, - highlightedLines: [8], - variables: { l, r, currentVal: nums[r] }, - phase: 'init' + explanation: `Value at back (${nums[poppedIdx]}) < current value (${nums[r]}). Popping smaller element from deque.`, + pseudoStep: `WHILE q.length AND nums[q.back] (${nums[poppedIdx]}) < nums[r] (${nums[r]}) -> POP`, + variables: { backVal: nums[poppedIdx], currentVal: nums[r] }, + phase: 'pop' }); + addLines(7, 7, 9, 7); + q.pop(); + } - // Monotonic deque: remove indices of smaller values from back - while (q.length && nums[q[q.length - 1]] < nums[r]) { - const poppedIdx = q[q.length - 1]; - s.push({ - nums, k, l, r, q: [...q], output: [...output], - explanation: `Value at back (${nums[poppedIdx]}) < current value (${nums[r]}). Popping smaller element.`, - highlightedLines: [9, 10], - variables: { backVal: nums[poppedIdx], currentVal: nums[r] }, - phase: 'pop' - }); - q.pop(); - } + q.push(r); + steps.push({ + nums, k, l, r, q: [...q], output: [...output], + explanation: `Push index ${r} to deque. Deque stores indices in decreasing order of corresponding values.`, + pseudoStep: `PUSH index r (${r}) to q`, + variables: { q: `[${q.join(',')}]` }, + phase: 'push' + }); + addLines(10, 9, 12, 10); - q.push(r); - s.push({ + // Remove index if it's out of window bounds + if (l > q[0]) { + steps.push({ nums, k, l, r, q: [...q], output: [...output], - explanation: `Push index ${r} to deque. Deque always stores elements in decreasing order.`, - highlightedLines: [13], - variables: { q: `[${q.join(',')}]` }, - phase: 'push' + explanation: `Index ${q[0]} is outside the current window [${l}, ${r}]. Removing from front of deque.`, + pseudoStep: `IF l (${l}) > q.front (${q[0]}) -> SHIFT/POP front`, + variables: { l, outOfWindow: q[0] }, + phase: 'shift' }); - - // Remove index if it's out of window bounds - if (l > q[0]) { - s.push({ - nums, k, l, r, q: [...q], output: [...output], - explanation: `Index ${q[0]} is outside the current window window [${l}, ${r}]. Removing from front.`, - highlightedLines: [15, 16], - variables: { l, outOfWindow: q[0] }, - phase: 'shift' - }); - q.shift(); - } - - // Check if window has reached size k - if ((r + 1) >= k) { - s.push({ - nums, k, l, r, q: [...q], output: [...output], - explanation: `Window size is ${k}. Maximum element is at front of deque: nums[${q[0]}] = ${nums[q[0]]}.`, - highlightedLines: [19, 20], - variables: { max: nums[q[0]], window: `[${l}, ${r}]` }, - phase: 'result' - }); - output.push(nums[q[0]]); - - l += 1; - s.push({ - nums, k, l, r, q: [...q], output: [...output], - explanation: `Record the max and slide 'l' forward to prepare for next window.`, - highlightedLines: [21], - variables: { l, output: `[${output.join(',')}]` }, - phase: 'result' - }); - } - - r += 1; - if (r < nums.length) { - s.push({ - nums, k, l, r, q: [...q], output: [...output], - explanation: `Slide 'r' forward.`, - highlightedLines: [24], - variables: { r }, - phase: 'init' - }); - } + addLines(11, 10, 13, 11); + q.shift(); } - s.push({ - nums, k, l, r: nums.length - 1, q: [...q], output: [...output], - explanation: "Process finished. Returning all window maximums.", - highlightedLines: [27], - variables: { result: `[${output.join(',')}]` }, - phase: 'done' - }); - - return s; - }, []); - - const code = `function maxSlidingWindow(nums: number[], k: number): number[] { - const output: number[] = []; - const q: number[] = []; - - let l = 0; - let r = 0; + // Check if window has reached size k + if ((r + 1) >= k) { + steps.push({ + nums, k, l, r, q: [...q], output: [...output], + explanation: `Window size has reached ${k}. The maximum element is at the front of deque: nums[${q[0]}] = ${nums[q[0]]}.`, + pseudoStep: `IF (r + 1) >= k -> APPEND nums[q.front] (${nums[q[0]]})`, + variables: { max: nums[q[0]], window: `[${l}, ${r}]` }, + phase: 'result' + }); + addLines(14, 12, 16, 14); + output.push(nums[q[0]]); - while (r < nums.length) { - while (q.length && nums[q[q.length - 1]] < nums[r]) { - q.pop(); - } + l += 1; + steps.push({ + nums, k, l, r, q: [...q], output: [...output], + explanation: `Increment left pointer 'l' to slide the window forward.`, + pseudoStep: "SET l = l + 1", + variables: { l, output: `[${output.join(',')}]` }, + phase: 'result' + }); + addLines(16, 14, 18, 16); + } - q.push(r); + r += 1; + if (r < nums.length) { + steps.push({ + nums, k, l, r, q: [...q], output: [...output], + explanation: `Slide the right pointer 'r' forward.`, + pseudoStep: "SET r = r + 1", + variables: { r }, + phase: 'init' + }); + addLines(18, 15, 20, 18); + } + } + + steps.push({ + nums, k, l, r: nums.length - 1, q: [...q], output: [...output], + explanation: "Process finished. Returning all window maximums.", + pseudoStep: "RETURN output", + variables: { result: `[${output.join(',')}]` }, + phase: 'done' + }); + addLines(20, 16, 22, 20); + + return { steps, stepLineNumbers }; +} - if (l > q[0]) { - q.shift(); - } +export const SlidingWindowMaxVisualization = () => { + const { steps, stepLineNumbers } = useMemo(generateVisualizationData, []); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); - if ((r + 1) >= k) { - output.push(nums[q[0]]); - l += 1; - } + const nums = [1, 3, -1, -3, 5, 3, 6, 7, 2, 4, 1, 6]; + const k = 3; - r += 1; + useEffect(() => { + if (isPlaying && currentStepIndex < steps.length - 1) { + intervalRef.current = setInterval(() => { + setCurrentStepIndex(prev => { + if (prev >= steps.length - 1) { + setIsPlaying(false); + return prev; + } + return prev + 1; + }); + }, 1000 / speed); + } else { + if (intervalRef.current) clearInterval(intervalRef.current); } - - return output; -}`; - - const step = steps[currentStep]; + return () => { + if (intervalRef.current) clearInterval(intervalRef.current); + }; + }, [isPlaying, currentStepIndex, steps.length, speed]); + + const handlePlay = () => setIsPlaying(true); + const handlePause = () => setIsPlaying(false); + const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(p => p + 1); + const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(p => p - 1); + const handleReset = () => { + setCurrentStepIndex(0); + setIsPlaying(false); + }; + + const step = steps[currentStepIndex]; + const pseudoSteps = useMemo(() => steps.map(s => s.pseudoStep), [steps]); return ( { ${isInWindow && nextIsInWindow && !isR ? '' : 'mr-2'} `} > - {/* Window Label over L */} {isL && (
Window @@ -209,7 +329,7 @@ export const SlidingWindowMaxVisualization = () => { ? "10px" : (isL ? "10px 0 0 10px" : (isR ? "0 10px 10px 0" : (isInWindow ? "0px" : "10px"))) }} - transition={{ type: "spring", stiffness: 400, damping: 30 }} + transition={{ duration: 0.1 }} className={`w-10 h-10 border-2 flex items-center justify-center text-sm font-black transition-all ${isInWindow && !isL && !isR ? 'border-x-0' : ''} ${isInWindow && isL && !isR ? 'border-r-0' : ''} @@ -227,6 +347,7 @@ export const SlidingWindowMaxVisualization = () => { initial={{ y: 5, opacity: 0 }} animate={{ y: 0, opacity: 1 }} exit={{ y: -5, opacity: 0 }} + transition={{ duration: 0.1 }} className="text-[10px] font-black text-primary uppercase" > L @@ -238,6 +359,7 @@ export const SlidingWindowMaxVisualization = () => { initial={{ y: 5, opacity: 0 }} animate={{ y: 0, opacity: 1 }} exit={{ y: -5, opacity: 0 }} + transition={{ duration: 0.1 }} className="text-[10px] font-black text-blue-500 uppercase" > R @@ -261,9 +383,10 @@ export const SlidingWindowMaxVisualization = () => { {step.q.map((idx, qIdx) => ( { key={idx} initial={{ scale: 0 }} animate={{ scale: 1 }} + transition={{ duration: 0.1 }} className="w-8 h-8 bg-primary/20 border-2 border-primary/40 rounded-lg flex items-center justify-center font-bold text-primary text-xs" > {val} @@ -295,27 +419,34 @@ export const SlidingWindowMaxVisualization = () => {
- -
-

Algorithm Step

-

{step.explanation}

- +
+

{step.explanation}

+
} rightContent={ - } controls={ - } /> diff --git a/src/components/visualizations/algorithms/SlidingWindowVisualization.tsx b/src/components/visualizations/algorithms/SlidingWindowVisualization.tsx index 638e330..db436fb 100644 --- a/src/components/visualizations/algorithms/SlidingWindowVisualization.tsx +++ b/src/components/visualizations/algorithms/SlidingWindowVisualization.tsx @@ -1,8 +1,8 @@ import { useEffect, useRef, useState } from 'react'; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { array: number[]; @@ -12,17 +12,13 @@ interface Step { windowSum: number; maxSum: number; message: string; - lineNumber: number; + pseudoStep: string; } -export const SlidingWindowVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const intervalRef = useRef(null); +// ─── Hardcoded code per language ───────────────────────────────────────────── - const code = `function maxSumSubarray(arr: number[], k: number): number { +const languages: VisualizationLanguageMap = { + typescript: `function maxSumSubarray(arr: number[], k: number): number { let maxSum = 0; let windowSum = 0; @@ -37,175 +33,260 @@ export const SlidingWindowVisualization = () => { } return maxSum; -}`; +}`, + + python: `def max_sum_subarray(arr: List[int], k: int) -> int: + max_sum = 0 + window_sum = 0 + + for i in range(k): + window_sum += arr[i] + max_sum = window_sum + + for i in range(k, len(arr)): + window_sum = window_sum - arr[i - k] + arr[i] + max_sum = max(max_sum, window_sum) + + return max_sum`, + + java: `public static class Solution { + public int maxSumSubarray(int[] arr, int k) { + int maxSum = 0; + int windowSum = 0; + + for (int i = 0; i < k; i++) { + windowSum += arr[i]; + } + maxSum = windowSum; + + for (int i = k; i < arr.length; i++) { + windowSum = windowSum - arr[i - k] + arr[i]; + maxSum = Math.max(maxSum, windowSum); + } + + return maxSum; + } +}`, + + cpp: `class Solution { +public: + int maxSumSubarray(vector& arr, int k) { + int maxSum = 0; + int windowSum = 0; + + for (int i = 0; i < k; i++) { + windowSum += arr[i]; + } + maxSum = windowSum; + + for (int i = k; i < arr.size(); i++) { + windowSum = windowSum - arr[i - k] + arr[i]; + maxSum = max(maxSum, windowSum); + } + + return maxSum; + } +};` +}; + +// ─── Step Generator ────────────────────────────────────────────────────────── + +function generateVisualizationData() { + const array = [2, 1, 5, 1, 3, 2, 4, 7, 1]; + const k = 3; + const steps: Step[] = []; + let windowSum = 0; + let maxSum = 0; + + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - const generateSteps = () => { - const array = [2, 1, 5, 1, 3, 2, 4, 7, 1]; - const k = 3; - const newSteps: Step[] = []; - let windowSum = 0; - let maxSum = 0; + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - // Line 1: Function entry - newSteps.push({ + // Line 1: Function entry + steps.push({ + array: [...array], + windowStart: -1, + windowEnd: -1, + windowSize: k, + windowSum: 0, + maxSum: 0, + message: `Starting Maximum Sum Subarray with k = ${k}.`, + pseudoStep: `START maxSumSubarray(arr, k = ${k})` + }); + addLines(1, 1, 2, 3); + + // Line 2: Initialize maxSum + steps.push({ + array: [...array], + windowStart: -1, + windowEnd: -1, + windowSize: k, + windowSum: 0, + maxSum: 0, + message: 'Initialize maxSum = 0.', + pseudoStep: 'SET maxSum = 0' + }); + addLines(2, 2, 3, 4); + + // Line 3: Initialize windowSum + steps.push({ + array: [...array], + windowStart: -1, + windowEnd: -1, + windowSize: k, + windowSum: 0, + maxSum: 0, + message: 'Initialize windowSum = 0.', + pseudoStep: 'SET windowSum = 0' + }); + addLines(3, 3, 4, 5); + + // Building first window + for (let i = 0; i < k; i++) { + // Line 5: For loop check + steps.push({ array: [...array], - windowStart: -1, - windowEnd: -1, + windowStart: 0, + windowEnd: i - 1, windowSize: k, - windowSum: 0, - maxSum: 0, - message: `Starting Maximum Sum Subarray with k = ${k}.`, - lineNumber: 1 + windowSum, + maxSum, + message: `Building first window: i = ${i}.`, + pseudoStep: `FOR i = 0 to k-1 → i = ${i} < ${k} → YES ✓` }); + addLines(5, 5, 6, 7); - // Line 2: Initialize maxSum - newSteps.push({ + windowSum += array[i]; + // Line 6: Add to windowSum + steps.push({ array: [...array], - windowStart: -1, - windowEnd: -1, + windowStart: 0, + windowEnd: i, windowSize: k, - windowSum: 0, - maxSum: 0, - message: 'Initialize maxSum = 0.', - lineNumber: 2 + windowSum, + maxSum, + message: `Add arr[${i}] (${array[i]}) to windowSum. windowSum is now ${windowSum}.`, + pseudoStep: `SET windowSum = windowSum + arr[i] → windowSum = ${windowSum}` }); + addLines(6, 6, 7, 8); + } - // Line 3: Initialize windowSum - newSteps.push({ + maxSum = windowSum; + // Line 8: Set initial maxSum + steps.push({ + array: [...array], + windowStart: 0, + windowEnd: k - 1, + windowSize: k, + windowSum, + maxSum, + message: `First window built. Set initial maxSum = windowSum = ${maxSum}.`, + pseudoStep: `SET maxSum = windowSum → maxSum = ${maxSum}` + }); + addLines(8, 7, 9, 10); + + // Sliding window + for (let i = k; i < array.length; i++) { + const oldIndex = i - k; + const newIndex = i; + + // Line 10: For loop check + steps.push({ array: [...array], - windowStart: -1, - windowEnd: -1, + windowStart: oldIndex, + windowEnd: newIndex - 1, windowSize: k, - windowSum: 0, - maxSum: 0, - message: 'Initialize windowSum = 0.', - lineNumber: 3 + windowSum, + maxSum, + message: `Sliding window: i = ${i}.`, + pseudoStep: `FOR i = k to arr.length-1 → i = ${i} < ${array.length} → YES ✓` }); + addLines(10, 9, 11, 12); - // Building first window - for (let i = 0; i < k; i++) { - // Line 5: For loop check - newSteps.push({ - array: [...array], - windowStart: 0, - windowEnd: i - 1, - windowSize: k, - windowSum, - maxSum, - message: `Building first window: i = ${i}.`, - lineNumber: 5 - }); - - windowSum += array[i]; - // Line 6: Add to windowSum - newSteps.push({ - array: [...array], - windowStart: 0, - windowEnd: i, - windowSize: k, - windowSum, - maxSum, - message: `Add arr[${i}] (${array[i]}) to windowSum. windowSum is now ${windowSum}.`, - lineNumber: 6 - }); - } + // Line 11: Subtract old, add new + const oldVal = array[oldIndex]; + const newVal = array[newIndex]; - maxSum = windowSum; - // Line 8: Set initial maxSum - newSteps.push({ + // Show subtraction first + steps.push({ array: [...array], - windowStart: 0, - windowEnd: k - 1, + windowStart: oldIndex + 1, + windowEnd: newIndex - 1, windowSize: k, - windowSum, + windowSum: windowSum - oldVal, maxSum, - message: `First window built. Set initial maxSum = windowSum = ${maxSum}.`, - lineNumber: 8 + message: `Remove arr[${oldIndex}] (${oldVal}) from windowSum.`, + pseudoStep: `windowSum = windowSum − arr[i−k] → windowSum = ${windowSum - oldVal}` }); + addLines(11, 10, 12, 13); - // Sliding window - for (let i = k; i < array.length; i++) { - const oldIndex = i - k; - const newIndex = i; - - // Line 10: For loop check - newSteps.push({ - array: [...array], - windowStart: oldIndex, - windowEnd: newIndex - 1, - windowSize: k, - windowSum, - maxSum, - message: `Sliding window: i = ${i}.`, - lineNumber: 10 - }); - - // Line 11: Subtract old, add new - const oldVal = array[oldIndex]; - const newVal = array[newIndex]; - - // Show subtraction first - newSteps.push({ - array: [...array], - windowStart: oldIndex + 1, - windowEnd: newIndex - 1, - windowSize: k, - windowSum: windowSum - oldVal, - maxSum, - message: `Remove arr[${oldIndex}] (${oldVal}) from windowSum.`, - lineNumber: 11 - }); - - windowSum = windowSum - oldVal + newVal; - - // Show addition - newSteps.push({ - array: [...array], - windowStart: oldIndex + 1, - windowEnd: newIndex, - windowSize: k, - windowSum, - maxSum, - message: `Add arr[${newIndex}] (${newVal}) to windowSum. windowSum is now ${windowSum}.`, - lineNumber: 11 - }); - - // Line 12: Update maxSum - const prevMax = maxSum; - maxSum = Math.max(maxSum, windowSum); - newSteps.push({ - array: [...array], - windowStart: oldIndex + 1, - windowEnd: newIndex, - windowSize: k, - windowSum, - maxSum, - message: windowSum > prevMax - ? `New maximum found! ${windowSum} > ${prevMax}. Update maxSum = ${maxSum}.` - : `${windowSum} is not greater than ${prevMax}. maxSum remains ${maxSum}.`, - lineNumber: 12 - }); - } + windowSum = windowSum - oldVal + newVal; - // Line 15: Return - newSteps.push({ + // Show addition + steps.push({ array: [...array], - windowStart: array.length - k, - windowEnd: array.length - 1, + windowStart: oldIndex + 1, + windowEnd: newIndex, windowSize: k, windowSum, maxSum, - message: `Algorithm complete. The maximum sum subarray of size ${k} has sum ${maxSum}.`, - lineNumber: 15 + message: `Add arr[${newIndex}] (${newVal}) to windowSum. windowSum is now ${windowSum}.`, + pseudoStep: `windowSum = windowSum + arr[i] → windowSum = ${windowSum}` }); + addLines(11, 10, 12, 13); - setSteps(newSteps); - setCurrentStepIndex(0); - }; + // Line 12: Update maxSum + const prevMax = maxSum; + maxSum = Math.max(maxSum, windowSum); + steps.push({ + array: [...array], + windowStart: oldIndex + 1, + windowEnd: newIndex, + windowSize: k, + windowSum, + maxSum, + message: windowSum > prevMax + ? `New maximum found! ${windowSum} > ${prevMax}. Update maxSum = ${maxSum}.` + : `${windowSum} is not greater than ${prevMax}. maxSum remains ${maxSum}.`, + pseudoStep: `SET maxSum = max(maxSum, windowSum) → maxSum = ${maxSum}` + }); + addLines(12, 11, 13, 14); + } - useEffect(() => { - generateSteps(); - }, []); + // Line 15: Return + steps.push({ + array: [...array], + windowStart: array.length - k, + windowEnd: array.length - 1, + windowSize: k, + windowSum, + maxSum, + message: `Algorithm complete. The maximum sum subarray of size ${k} has sum ${maxSum}.`, + pseudoStep: `RETURN maxSum → ${maxSum}` + }); + addLines(15, 13, 16, 17); + + return { steps, stepLineNumbers }; +} + +// ─── Component ─────────────────────────────────────────────────────────────── + +export const SlidingWindowVisualization = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { @@ -219,40 +300,26 @@ export const SlidingWindowVisualization = () => { }); }, 1000 / speed); } else { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } + if (intervalRef.current) clearInterval(intervalRef.current); } - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } + if (intervalRef.current) clearInterval(intervalRef.current); }; }, [isPlaying, currentStepIndex, steps.length, speed]); const handlePlay = () => setIsPlaying(true); const handlePause = () => setIsPlaying(false); - const handleStepForward = () => { - if (currentStepIndex < steps.length - 1) { - setCurrentStepIndex(prev => prev + 1); - } - }; - const handleStepBack = () => { - if (currentStepIndex > 0) { - setCurrentStepIndex(prev => prev - 1); - } - }; + const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(p => p + 1); + const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(p => p - 1); const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); - generateSteps(); }; if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; - const getMaxValue = () => Math.max(...currentStep.array); + const pseudoSteps = steps.map(s => s.pseudoStep); return (
@@ -271,8 +338,20 @@ export const SlidingWindowVisualization = () => {
-
-
+
+
+ {/* Sliding Window Border Outline Overlay */} + {currentStep.windowStart !== -1 && currentStep.windowEnd !== -1 && ( +
+ )} {currentStep.array.map((value, index) => { const isInWindow = index >= currentStep.windowStart && index <= currentStep.windowEnd; const isWindowStart = index === currentStep.windowStart; @@ -281,38 +360,26 @@ export const SlidingWindowVisualization = () => { return (
{isWindowStart && ( -
- START -
+ + Start + )} {isWindowEnd && ( -
- END -
+ + End + )}
- {isInWindow && ( -
- )} -
- {value} - +
); })} @@ -324,7 +391,6 @@ export const SlidingWindowVisualization = () => {
- { }} />
- -
- -
diff --git a/src/components/visualizations/algorithms/SparseTableVisualization.tsx b/src/components/visualizations/algorithms/SparseTableVisualization.tsx index 4d9ccc5..3cdf21c 100644 --- a/src/components/visualizations/algorithms/SparseTableVisualization.tsx +++ b/src/components/visualizations/algorithms/SparseTableVisualization.tsx @@ -1,18 +1,19 @@ import { useState, useMemo } from 'react'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; import { Card } from '@/components/ui/card'; import { motion, AnimatePresence } from 'framer-motion'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { nums: number[]; st: number[][]; // [i][j] k: number; n: number; - highlightedLines: number[]; explanation: string; + pseudoStep: string; variables: Record; activeIndices: number[]; // indices in nums currently being processed activeSTCells: [number, number][]; // [i, j] cells in st currently being processed/updated @@ -20,69 +21,177 @@ interface Step { overlappingRanges: [[number, number], [number, number]] | null; } +const languages: VisualizationLanguageMap = { + typescript: `function log2(x: number): number { + return Math.log(x) / Math.log(2); +} +function solution(nums: number[], L: number, R: number): number { + const n = nums.length; + const k = Math.floor(log2(n)); + const st: number[][] = Array.from({ length: n }, () => new Array(k + 1).fill(0)); + for (let i = 0; i < n; i++) { + st[i][0] = nums[i]; + } + for (let j = 1; j <= k; j++) { + for (let i = 0; i + (1 << j) <= n; i++) { + st[i][j] = Math.min(st[i][j - 1], st[i + (1 << (j - 1))][j - 1]); + } + } + const len = R - L + 1; + const j = Math.floor(log2(len)); + return Math.min(st[L][j], st[R - (1 << j) + 1][j]); +}`, + python: `import math +def solution(nums, L, R): + if not nums: + return None + n = len(nums) + k = int(math.log2(n)) + st = [[0] * (k + 1) for _ in range(n)] + for i in range(n): + st[i][0] = nums[i] + j = 1 + while (1 << j) <= n: + i = 0 + while i + (1 << j) <= n: + st[i][j] = min(st[i][j - 1], st[i + (1 << (j - 1))][j - 1]) + i += 1 + j += 1 + length = R - L + 1 + j = int(math.log2(length)) + return min(st[L][j], st[R - (1 << j) + 1][j])`, + java: `public static class Solution { + public int solution(int[] nums, int L, int R) { + if (nums.length == 0) + return -1; + int n = nums.length; + int k = (int)(Math.log(n) / Math.log(2)); + int[][] st = new int[n][k + 1]; + for (int i = 0; i < n; i++) { + st[i][0] = nums[i]; + } + for (int j = 1; j <= k; j++) { + for (int i = 0; i + (1 << j) <= n; i++) { + st[i][j] = Math.min( + st[i][j - 1], + st[i + (1 << (j - 1))][j - 1] + ); + } + } + int len = R - L + 1; + int j = (int)(Math.log(len) / Math.log(2)); + return Math.min(st[L][j], st[R - (1 << j) + 1][j]); + } +}`, + cpp: `class Solution { +public: + int solution(vector& nums, int L, int R) { + int n = nums.size(); + vector log(n + 1); + log[1] = 0; + for (int i = 2; i <= n; i++) + log[i] = log[i / 2] + 1; + int k = log[n]; + vector> st(n, vector(k + 1)); + for (int i = 0; i < n; i++) + st[i][0] = nums[i]; + for (int j = 1; j <= k; j++) { + for (int i = 0; i + (1 << j) <= n; i++) { + st[i][j] = min( + st[i][j - 1], + st[i + (1 << (j - 1))][j - 1] + ); + } + } + int j = log[R - L + 1]; + return min(st[L][j], st[R - (1 << j) + 1][j]); + } +};` +}; + export const SparseTableVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); + const [currentStepIndex, setCurrentStepIndex] = useState(0); const nums = [7, 2, 3, 0, 5, 10, 3, 12, 18]; const L = 1; const R = 6; - const steps: Step[] = useMemo(() => { + const { steps, stepLineNumbers } = useMemo(() => { const s: Step[] = []; const n = nums.length; const log2 = (x: number) => Math.log(x) / Math.log(2); const k = Math.floor(log2(n)); const st: number[][] = Array.from({ length: n }, () => new Array(k + 1).fill(null as any)); + const lines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + lines.typescript!.push(ts); + lines.python!.push(py); + lines.java!.push(java); + lines.cpp!.push(cpp); + }; + s.push({ nums, st: st.map(row => [...row]), k, n, - highlightedLines: [1, 2, 3], - explanation: "Helper function to calculate base-2 logarithm.", + explanation: "Define base-2 logarithm function for lookup range math.", + pseudoStep: "FUNCTION log2(x)", variables: {}, activeIndices: [], activeSTCells: [], queryRange: null, overlappingRanges: null }); + addLines(1, 1, 1, 1); s.push({ nums, st: st.map(row => [...row]), k, n, - highlightedLines: [5], - explanation: `Starting solution with nums=[${nums.join(', ')}], L=${L}, R=${R}.`, + explanation: `Starting sparse table construction for nums = [${nums.join(', ')}], L = ${L}, R = ${R}.`, + pseudoStep: `CALL solution(nums, L = ${L}, R = ${R})`, variables: { nums, L, R }, activeIndices: [], activeSTCells: [], queryRange: null, overlappingRanges: null }); + addLines(4, 2, 2, 3); s.push({ nums, st: st.map(row => [...row]), k, n, - highlightedLines: [6], - explanation: `Set n = nums.length = ${n}.`, + explanation: `Determine size n = ${n}.`, + pseudoStep: `SET n = length(nums) (n = ${n})`, variables: { n, L, R }, activeIndices: [], activeSTCells: [], queryRange: null, overlappingRanges: null }); + addLines(5, 5, 5, 4); s.push({ nums, st: st.map(row => [...row]), k, n, - highlightedLines: [7], - explanation: `Calculate k = floor(log2(${n})) = ${k}. Sparse table will have ${k + 1} columns.`, + explanation: `Calculate sparse table columns count: k = floor(log2(n)) = floor(log2(${n})) = ${k}.`, + pseudoStep: `SET k = floor(log2(n)) (k = ${k})`, variables: { n, k, L, R }, activeIndices: [], activeSTCells: [], queryRange: null, overlappingRanges: null }); + addLines(6, 6, 6, 9); s.push({ nums, st: st.map(row => [...row]), k, n, - highlightedLines: [8], - explanation: `Initialize sparse table 'st' with size ${n} x ${k + 1}.`, + explanation: `Initialize empty sparse table 'st' of size ${n} x ${k + 1}.`, + pseudoStep: `SET st = empty 2D array (${n} x ${k + 1})`, variables: { n, k, L, R }, activeIndices: [], activeSTCells: [], queryRange: null, overlappingRanges: null }); + addLines(7, 7, 7, 10); for (let i = 0; i < n; i++) { st[i][0] = nums[i]; s.push({ nums, st: st.map(row => [...row]), k, n, - highlightedLines: [10, 11, 12], - explanation: `Base case: st[${i}][0] = nums[${i}] = ${nums[i]}. Level 0 represents ranges of length 2^0 = 1.`, + explanation: `Base Case: st[${i}][0] = nums[${i}] = ${nums[i]}. j=0 represents range length 2^0 = 1.`, + pseudoStep: `SET st[${i}][0] = nums[${i}] (${nums[i]})`, variables: { i, n, k }, activeIndices: [i], activeSTCells: [[i, 0]], queryRange: null, overlappingRanges: null }); + addLines(9, 9, 9, 12); } for (let j = 1; j <= k; j++) { @@ -93,33 +202,36 @@ export const SparseTableVisualization = () => { s.push({ nums, st: st.map(row => [...row]), k, n, - highlightedLines: [14, 15, 16], explanation: `Level ${j} (length 2^${j}=${1 << j}): st[${i}][${j}] = min(st[${leftIdx}][${j - 1}], st[${rightIdx}][${j - 1}]) = min(${st[leftIdx][j - 1]}, ${st[rightIdx][j - 1]}) = ${st[i][j]}.`, + pseudoStep: `SET st[${i}][${j}] = min(st[${i}][${j-1}], st[${i + (1<<(j-1))}][${j-1}])`, variables: { i, j, '2^j': 1 << j, '2^(j-1)': 1 << (j - 1) }, activeIndices: Array.from({ length: 1 << j }, (_, idx) => i + idx), activeSTCells: [[leftIdx, j - 1], [rightIdx, j - 1], [i, j]], queryRange: null, overlappingRanges: null }); + addLines(13, 14, 13, 15); } } const len = R - L + 1; s.push({ nums, st: st.map(row => [...row]), k, n, - highlightedLines: [20], explanation: `Query length = R - L + 1 = ${R} - ${L} + 1 = ${len}.`, + pseudoStep: `SET len = R - L + 1 (len = ${len})`, variables: { L, R, len }, activeIndices: [], activeSTCells: [], queryRange: [L, R], overlappingRanges: null }); + addLines(16, 17, 19, 21); const queryJ = Math.floor(log2(len)); s.push({ nums, st: st.map(row => [...row]), k, n, - highlightedLines: [21], - explanation: `Largest power of 2 <= length: j = floor(log2(${len})) = ${queryJ}.`, + explanation: `Largest power of 2 fitting in range: j = floor(log2(${len})) = ${queryJ}.`, + pseudoStep: `SET j = floor(log2(len)) (j = ${queryJ})`, variables: { L, R, len, j: queryJ }, activeIndices: [], activeSTCells: [], queryRange: [L, R], overlappingRanges: null }); + addLines(17, 18, 20, 21); const result = Math.min(st[L][queryJ], st[R - (1 << queryJ) + 1][queryJ]); const overlap1: [number, number] = [L, L + (1 << queryJ) - 1]; @@ -127,163 +239,146 @@ export const SparseTableVisualization = () => { s.push({ nums, st: st.map(row => [...row]), k, n, - highlightedLines: [23], - explanation: `Range [${L}, ${R}] is covered by two overlapping segments of length 2^${queryJ}=${1 << queryJ}: [${overlap1[0]}, ${overlap1[1]}] (st[${L}][${queryJ}]) and [${overlap2[0]}, ${overlap2[1]}] (st[${overlap2[0]}][${queryJ}]). Minimum is ${result}.`, + explanation: `Query range [${L}, ${R}] covered by two overlapping ranges of size 2^${queryJ}=${1 << queryJ}: [${overlap1[0]}, ${overlap1[1]}] (st[${L}][${queryJ}] = ${st[L][queryJ]}) and [${overlap2[0]}, ${overlap2[1]}] (st[${overlap2[0]}][${queryJ}] = ${st[overlap2[0]][queryJ]}). Minimum is ${result}.`, + pseudoStep: `RETURN min(st[L][j], st[R - 2^j + 1][j]) (min(${st[L][queryJ]}, ${st[overlap2[0]][queryJ]}) = ${result})`, variables: { L, R, j: queryJ, result }, activeIndices: [], activeSTCells: [[L, queryJ], [overlap2[0], queryJ]], queryRange: [L, R], overlappingRanges: [overlap1, overlap2] }); + addLines(18, 19, 21, 22); - return s; + return { steps: s, stepLineNumbers: lines }; }, [nums, L, R]); - const code = `function log2(x: number): number { - return Math.log(x) / Math.log(2); -} - -function solution(nums: number[], L: number, R: number): number { - const n = nums.length; - const k = Math.floor(log2(n)); - const st: number[][] = Array.from({ length: n }, () => new Array(k + 1).fill(0)); - - for (let i = 0; i < n; i++) { - st[i][0] = nums[i]; - } - - for (let j = 1; j <= k; j++) { - for (let i = 0; i + (1 << j) <= n; i++) { - st[i][j] = Math.min(st[i][j - 1], st[i + (1 << (j - 1))][j - 1]); - } - } - - const len = R - L + 1; - const j = Math.floor(log2(len)); - - return Math.min(st[L][j], st[R - (1 << j) + 1][j]); -}`; - - const step = steps[currentStep]; + const step = steps[currentStepIndex] || steps[0]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( - -

Sparse Table (Range Minimum Query)

- -
-
Input Array (nums)
-
- {nums.map((val, idx) => { - const isActive = step.activeIndices.includes(idx); - const isQuery = step.queryRange && idx >= step.queryRange[0] && idx <= step.queryRange[1]; - const inOverlap1 = step.overlappingRanges && idx >= step.overlappingRanges[0][0] && idx <= step.overlappingRanges[0][1]; - const inOverlap2 = step.overlappingRanges && idx >= step.overlappingRanges[1][0] && idx <= step.overlappingRanges[1][1]; - - return ( -
- {idx} - {val} - - {inOverlap1 && ( - - )} - {inOverlap2 && ( - - )} - -
- ); - })} +
+
+ +

Sparse Table (Range Minimum Query)

+ +
+
Input Array (nums)
+
+ {nums.map((val, idx) => { + const isActive = step.activeIndices.includes(idx); + const isQuery = step.queryRange && idx >= step.queryRange[0] && idx <= step.queryRange[1]; + const inOverlap1 = step.overlappingRanges && idx >= step.overlappingRanges[0][0] && idx <= step.overlappingRanges[0][1]; + const inOverlap2 = step.overlappingRanges && idx >= step.overlappingRanges[1][0] && idx <= step.overlappingRanges[1][1]; + + return ( +
+ {idx} + {val} + + {inOverlap1 && ( + + )} + {inOverlap2 && ( + + )} + +
+ ); + })} +
-
- -
-
Dynamic Programming Table (st[i][j])
- - - - - {Array.from({ length: step.k + 1 }).map((_, j) => ( - - ))} - - - - {step.st.map((row, i) => ( - - - {row.map((val, j) => { - const isActive = step.activeSTCells.some(([ci, cj]) => ci === i && cj === j); - const isUpdate = isActive && step.activeSTCells[step.activeSTCells.length - 1][0] === i && step.activeSTCells[step.activeSTCells.length - 1][1] === j; - return ( - - ); - })} +
+
Dynamic Programming Table (st[i][j])
+
i \ j - 2{j} -
{i} - {val ?? '-'} -
+ + + + {Array.from({ length: step.k + 1 }).map((_, j) => ( + + ))} - ))} - -
i \ j + 2{j} +
-
- -
-
-
- Range 1 + + + {step.st.map((row, i) => ( + + {i} + {row.map((val, j) => { + const isActive = step.activeSTCells.some(([ci, cj]) => ci === i && cj === j); + const isUpdate = isActive && step.activeSTCells[step.activeSTCells.length - 1][0] === i && step.activeSTCells[step.activeSTCells.length - 1][1] === j; + + return ( + + {val ?? '-'} + + ); + })} + + ))} + +
-
-
- Range 2 + +
+
+
+ Range 1 +
+
+
+ Range 2 +
-
- + +
- -

Step Explanation

-

{step.explanation}

-
+
+ +

Step Explanation

+

{step.explanation}

+
- + +
} rightContent={ - setCurrentStepIndex(0)} /> } controls={ } /> diff --git a/src/components/visualizations/algorithms/SpiralMatrixVisualization.tsx b/src/components/visualizations/algorithms/SpiralMatrixVisualization.tsx index 099e7a6..cb720eb 100644 --- a/src/components/visualizations/algorithms/SpiralMatrixVisualization.tsx +++ b/src/components/visualizations/algorithms/SpiralMatrixVisualization.tsx @@ -1,9 +1,11 @@ -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import { motion } from 'framer-motion'; import { Card } from '@/components/ui/card'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { SimpleStepControls } from '../shared/SimpleStepControls'; +import { VisualizationLayout } from '../shared/VisualizationLayout'; +import type { VisualizationLanguageMap, StepLineNumberMap } from '@/types/visualization'; interface Step { matrix: number[][]; @@ -13,370 +15,284 @@ interface Step { currentCol: number; variables: Record; explanation: string; - highlightedLines: number[]; - lineExecution: string; + pseudoStep: string; direction: string; } -export const SpiralMatrixVisualization = () => { - const [currentStepIndex, setCurrentStepIndex] = useState(0); +const languages: VisualizationLanguageMap = { + python: `def spiralOrder(matrix): + res = [] + left, right = 0, len(matrix[0]) + top, bottom = 0, len(matrix) + while left < right and top < bottom: + for i in range(left, right): + res.append(matrix[top][i]) + top += 1 + for i in range(top, bottom): + res.append(matrix[i][right - 1]) + right -= 1 + if not (left < right and top < bottom): + break + for i in range(right - 1, left - 1, -1): + res.append(matrix[bottom - 1][i]) + bottom -= 1 + for i in range(bottom - 1, top - 1, -1): + res.append(matrix[i][left]) + left += 1 + return res`, - const initialMatrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]; - - const steps: Step[] = [ - { - matrix: initialMatrix, - result: [], - visited: [[false, false, false], [false, false, false], [false, false, false]], - currentRow: -1, - currentCol: -1, - variables: { matrix: '3x3' }, - explanation: "Traverse a 3x3 matrix in spiral order: right → down → left → up.", - highlightedLines: [1], - lineExecution: "function spiralOrder(matrix: number[][])", - direction: "init" - }, - { - matrix: initialMatrix, - result: [], - visited: [[false, false, false], [false, false, false], [false, false, false]], - currentRow: -1, - currentCol: -1, - variables: { m: 3, n: 3 }, - explanation: "Initialize dimensions: 3 rows (m) and 3 columns (n).", - highlightedLines: [2], - lineExecution: "const m = matrix.length, n = matrix[0].length;", - direction: "init" - }, - { - matrix: initialMatrix, - result: [], - visited: [[false, false, false], [false, false, false], [false, false, false]], - currentRow: -1, - currentCol: -1, - variables: { top: 0, bottom: 2, left: 0, right: 2 }, - explanation: "Define boundaries: top = 0, bottom = 2, left = 0, right = 2.", - highlightedLines: [3], - lineExecution: "let top = 0, bottom = m - 1, left = 0, right = n - 1;", - direction: "init" - }, - { - matrix: initialMatrix, - result: [], - visited: [[false, false, false], [false, false, false], [false, false, false]], - currentRow: -1, - currentCol: -1, - variables: { result: '[]' }, - explanation: "Initialize an empty list to store the elements in spiral order.", - highlightedLines: [4], - lineExecution: "const result: number[] = [];", - direction: "init" - }, - { - matrix: initialMatrix, - result: [], - visited: [[false, false, false], [false, false, false], [false, false, false]], - currentRow: 0, - currentCol: 0, - variables: { direction: 'RIGHT', col: 0, row: 0 }, - explanation: "Move right along the top row from the 'left' boundary to the 'right' boundary.", - highlightedLines: [7], - lineExecution: "for (let col = left; col <= right; col++)", - direction: "right" - }, - { - matrix: initialMatrix, - result: [1], - visited: [[true, false, false], [false, false, false], [false, false, false]], - currentRow: 0, - currentCol: 0, - variables: { add: 1, result: '[1]' }, - explanation: "Collect the element at matrix[top][0], which is 1.", - highlightedLines: [8], - lineExecution: "result.push(matrix[top][col]);", - direction: "right" - }, - { - matrix: initialMatrix, - result: [1, 2], - visited: [[true, true, false], [false, false, false], [false, false, false]], - currentRow: 0, - currentCol: 1, - variables: { add: 2, result: '[1, 2]' }, - explanation: "Collect the next element in the top row: 2.", - highlightedLines: [8], - lineExecution: "result.push(matrix[top][col]);", - direction: "right" - }, - { - matrix: initialMatrix, - result: [1, 2, 3], - visited: [[true, true, true], [false, false, false], [false, false, false]], - currentRow: 0, - currentCol: 2, - variables: { add: 3, result: '[1, 2, 3]' }, - explanation: "Collect 3. The top row traversal is complete.", - highlightedLines: [8], - lineExecution: "result.push(matrix[top][col]);", - direction: "right" - }, - { - matrix: initialMatrix, - result: [1, 2, 3], - visited: [[true, true, true], [false, false, false], [false, false, false]], - currentRow: 0, - currentCol: 2, - variables: { top: 1, action: 'increment top' }, - explanation: "Shrink the top boundary: top = 1. We won't visit the first row again.", - highlightedLines: [10], - lineExecution: "top++;", - direction: "right" - }, - { - matrix: initialMatrix, - result: [1, 2, 3], - visited: [[true, true, true], [false, false, false], [false, false, false]], - currentRow: 1, - currentCol: 2, - variables: { direction: 'DOWN', row: 1 }, - explanation: "Move down along the right column from 'top' to 'bottom'.", - highlightedLines: [12], - lineExecution: "for (let row = top; row <= bottom; row++)", - direction: "down" - }, - { - matrix: initialMatrix, - result: [1, 2, 3, 6], - visited: [[true, true, true], [false, false, true], [false, false, false]], - currentRow: 1, - currentCol: 2, - variables: { add: 6, result: '[1, 2, 3, 6]' }, - explanation: "Collect the element matrix[1][right], which is 6.", - highlightedLines: [13], - lineExecution: "result.push(matrix[row][right]);", - direction: "down" - }, - { - matrix: initialMatrix, - result: [1, 2, 3, 6, 9], - visited: [[true, true, true], [false, false, true], [false, false, true]], - currentRow: 2, - currentCol: 2, - variables: { add: 9, result: '[1, 2, 3, 6, 9]' }, - explanation: "Collect 9. The right column traversal is complete.", - highlightedLines: [13], - lineExecution: "result.push(matrix[row][right]);", - direction: "down" - }, - { - matrix: initialMatrix, - result: [1, 2, 3, 6, 9], - visited: [[true, true, true], [false, false, true], [false, false, true]], - currentRow: 2, - currentCol: 2, - variables: { right: 1, action: 'decrement right' }, - explanation: "Shrink the right boundary: right = 1.", - highlightedLines: [15], - lineExecution: "right--;", - direction: "down" - }, - { - matrix: initialMatrix, - result: [1, 2, 3, 6, 9], - visited: [[true, true, true], [false, false, true], [false, false, true]], - currentRow: 2, - currentCol: 1, - variables: { direction: 'LEFT', col: 1 }, - explanation: "Since top <= bottom, move left along the bottom row from 'right' to 'left'.", - highlightedLines: [18], - lineExecution: "for (let col = right; col >= left; col--)", - direction: "left" - }, - { - matrix: initialMatrix, - result: [1, 2, 3, 6, 9, 8], - visited: [[true, true, true], [false, false, true], [false, true, true]], - currentRow: 2, - currentCol: 1, - variables: { add: 8, result: '[1, 2, 3, 6, 9, 8]' }, - explanation: "Collect matrix[bottom][1], which is 8.", - highlightedLines: [19], - lineExecution: "result.push(matrix[bottom][col]);", - direction: "left" - }, - { - matrix: initialMatrix, - result: [1, 2, 3, 6, 9, 8, 7], - visited: [[true, true, true], [false, false, true], [true, true, true]], - currentRow: 2, - currentCol: 0, - variables: { add: 7, result: '[1, 2, 3, 6, 9, 8, 7]' }, - explanation: "Collect 7. The bottom row traversal is complete.", - highlightedLines: [19], - lineExecution: "result.push(matrix[bottom][col]);", - direction: "left" - }, - { - matrix: initialMatrix, - result: [1, 2, 3, 6, 9, 8, 7], - visited: [[true, true, true], [false, false, true], [true, true, true]], - currentRow: 2, - currentCol: 0, - variables: { bottom: 1, action: 'decrement bottom' }, - explanation: "Shrink the bottom boundary: bottom = 1.", - highlightedLines: [21], - lineExecution: "bottom--;", - direction: "left" - }, - { - matrix: initialMatrix, - result: [1, 2, 3, 6, 9, 8, 7], - visited: [[true, true, true], [false, false, true], [true, true, true]], - currentRow: 1, - currentCol: 0, - variables: { direction: 'UP', row: 1 }, - explanation: "Since left <= right, move up along the left column from 'bottom' to 'top'.", - highlightedLines: [25], - lineExecution: "for (let row = bottom; row >= top; row--)", - direction: "up" - }, - { - matrix: initialMatrix, - result: [1, 2, 3, 6, 9, 8, 7, 4], - visited: [[true, true, true], [true, false, true], [true, true, true]], - currentRow: 1, - currentCol: 0, - variables: { add: 4, result: '[1, 2, 3, 6, 9, 8, 7, 4]' }, - explanation: "Collect matrix[1][left], which is 4.", - highlightedLines: [26], - lineExecution: "result.push(matrix[row][left]);", - direction: "up" - }, - { - matrix: initialMatrix, - result: [1, 2, 3, 6, 9, 8, 7, 4], - visited: [[true, true, true], [true, false, true], [true, true, true]], - currentRow: 1, - currentCol: 0, - variables: { left: 1, action: 'increment left' }, - explanation: "Shrink the left boundary: left = 1.", - highlightedLines: [28], - lineExecution: "left++;", - direction: "up" - }, - { - matrix: initialMatrix, - result: [1, 2, 3, 6, 9, 8, 7, 4], - visited: [[true, true, true], [true, false, true], [true, true, true]], - currentRow: 1, - currentCol: 1, - variables: { check: 'top=1, bottom=1, left=1, right=1', continue: true }, - explanation: "All boundaries are 1. The loop continue condition is still met.", - highlightedLines: [6], - lineExecution: "while (top <= bottom && left <= right)", - direction: "check" - }, - { - matrix: initialMatrix, - result: [1, 2, 3, 6, 9, 8, 7, 4], - visited: [[true, true, true], [true, false, true], [true, true, true]], - currentRow: 1, - currentCol: 1, - variables: { direction: 'RIGHT', col: 1 }, - explanation: "Traverse the remaining cells in the inner matrix starting with RIGHT.", - highlightedLines: [7], - lineExecution: "for (let col = left; col <= right; col++)", - direction: "right" - }, - { - matrix: initialMatrix, - result: [1, 2, 3, 6, 9, 8, 7, 4, 5], - visited: [[true, true, true], [true, true, true], [true, true, true]], - currentRow: 1, - currentCol: 1, - variables: { add: 5, result: '[1, ..., 5]' }, - explanation: "Collect the central element: 5.", - highlightedLines: [8], - lineExecution: "result.push(matrix[top][col]);", - direction: "right" - }, - { - matrix: initialMatrix, - result: [1, 2, 3, 6, 9, 8, 7, 4, 5], - visited: [[true, true, true], [true, true, true], [true, true, true]], - currentRow: -1, - currentCol: -1, - variables: { check: 'top=2, bottom=1', stop: true }, - explanation: "The boundaries have crossed (top > bottom). The spiral traversal is finished.", - highlightedLines: [6], - lineExecution: "while (top <= bottom && left <= right)", - direction: "complete" - }, - { - matrix: initialMatrix, - result: [1, 2, 3, 6, 9, 8, 7, 4, 5], - visited: [[true, true, true], [true, true, true], [true, true, true]], - currentRow: -1, - currentCol: -1, - variables: { result: '[1, 2, 3, 6, 9, 8, 7, 4, 5]' }, - explanation: "Return the final sequence of elements.", - highlightedLines: [31], - lineExecution: "return result;", - direction: "complete" - }, - { - matrix: initialMatrix, - result: [1, 2, 3, 6, 9, 8, 7, 4, 5], - visited: [[true, true, true], [true, true, true], [true, true, true]], - currentRow: -1, - currentCol: -1, - variables: { time: 'O(m*n)', space: 'O(1)' }, - explanation: "Complexity: O(m×n) time to visit each cell once, and O(1) extra space excluding the output.", - highlightedLines: [31], - lineExecution: "return result;", - direction: "complete" + typescript: `function spiralOrder(matrix: number[][]): number[] { + const res: number[] = []; + if (matrix.length === 0) return res; + let left = 0; + let right = matrix[0].length; + let top = 0; + let bottom = matrix.length; + while (left < right && top < bottom) { + for (let i = left; i < right; i++) { + res.push(matrix[top][i]); } - ]; + top += 1; + for (let i = top; i < bottom; i++) { + res.push(matrix[i][right - 1]); + } + right -= 1; + if (!(left < right && top < bottom)) { + break; + } + for (let i = right - 1; i >= left; i--) { + res.push(matrix[bottom - 1][i]); + } + bottom -= 1; + for (let i = bottom - 1; i >= top; i--) { + res.push(matrix[i][left]); + } + left += 1; + } + return res; +}`, + + java: `public class Solution { + public List spiralOrder(int[][] matrix) { + List res = new ArrayList<>(); + int left = 0; + int right = matrix[0].length; + int top = 0; + int bottom = matrix.length; + while (left < right && top < bottom) { + for (int i = left; i < right; i++) { + res.add(matrix[top][i]); + } + top += 1; + for (int i = top; i < bottom; i++) { + res.add(matrix[i][right - 1]); + } + right -= 1; + if (!(left < right && top < bottom)) { + break; + } + for (int i = right - 1; i >= left; i--) { + res.add(matrix[bottom - 1][i]); + } + bottom -= 1; + for (int i = bottom - 1; i >= top; i--) { + res.add(matrix[i][left]); + } + left += 1; + } + return res; + } +}`, + + cpp: `class Solution { +public: + vector spiralOrder(vector>& matrix) { + vector res; + int left = 0; + int right = matrix[0].size(); + int top = 0; + int bottom = matrix.size(); + while (left < right && top < bottom) { + for (int i = left; i < right; i++) { + res.push_back(matrix[top][i]); + } + top += 1; + for (int i = top; i < bottom; i++) { + res.push_back(matrix[i][right - 1]); + } + right -= 1; + if (!(left < right && top < bottom)) { + break; + } + for (int i = right - 1; i >= left; i--) { + res.push_back(matrix[bottom - 1][i]); + } + bottom -= 1; + for (int i = bottom - 1; i >= top; i--) { + res.push_back(matrix[i][left]); + } + left += 1; + } + return res; + } +};` +}; + +const generateVisualizationData = () => { + const matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]; + const m = matrix.length; + const n = matrix[0].length; + + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - const code = `function spiralOrder(matrix: number[][]): number[] { - const m = matrix.length, n = matrix[0].length; - let top = 0, bottom = m - 1, left = 0, right = n - 1; const result: number[] = []; - - while (top <= bottom && left <= right) { - for (let col = left; col <= right; col++) { - result.push(matrix[top][col]); + const visited = [ + [false, false, false], + [false, false, false], + [false, false, false] + ]; + + let left = 0; + let right = n; + let top = 0; + let bottom = m; + + let currentRow = -1; + let currentCol = -1; + let direction = "init"; + + const addStep = (msg: string, pseudo: string, tsLine: number, pyLine: number, javaLine: number, cppLine: number) => { + steps.push({ + matrix: matrix.map(row => [...row]), + result: [...result], + visited: visited.map(row => [...row]), + currentRow, + currentCol, + variables: { + left, + right, + top, + bottom, + currentRow: currentRow >= 0 ? currentRow : "none", + currentCol: currentCol >= 0 ? currentCol : "none", + resultSize: result.length + }, + explanation: msg, + pseudoStep: pseudo, + direction + }); + addLines(tsLine, pyLine, javaLine, cppLine); + }; + + // 1. Initial State + addStep("Traverse a 3x3 matrix in spiral order.", "CALL spiralOrder(matrix)", 1, 1, 2, 3); + + // 2. Initialize boundaries + addStep("Initialize boundaries: left = 0, right = 3 (exclusive), top = 0, bottom = 3 (exclusive).", "SET left = 0, right = 3, top = 0, bottom = 3", 4, 3, 4, 5); + + while (left < right && top < bottom) { + direction = "check"; + addStep("Loop condition: is left < right AND top < bottom?", "WHILE left < right AND top < bottom", 8, 5, 8, 9); + + // Left to right + direction = "right"; + for (let i = left; i < right; i++) { + currentCol = i; + currentRow = top; + result.push(matrix[currentRow][currentCol]); + visited[currentRow][currentCol] = true; + addStep(`Traverse right on top row. Collect cell matrix[${currentRow}][${currentCol}] = ${matrix[currentRow][currentCol]}.`, `APPEND matrix[top][${i}]`, 10, 7, 10, 11); } - top++; - - for (let row = top; row <= bottom; row++) { - result.push(matrix[row][right]); + top += 1; + currentRow = -1; + currentCol = -1; + addStep("Finished top row traversal. Move top boundary inward.", "SET top = top + 1", 12, 8, 12, 13); + + // Top to bottom + direction = "down"; + for (let i = top; i < bottom; i++) { + currentCol = right - 1; + currentRow = i; + result.push(matrix[currentRow][currentCol]); + visited[currentRow][currentCol] = true; + addStep(`Traverse down on right column. Collect cell matrix[${currentRow}][${currentCol}] = ${matrix[currentRow][currentCol]}.`, `APPEND matrix[${i}][right - 1]`, 14, 10, 14, 15); } - right--; - - if (top <= bottom) { - for (let col = right; col >= left; col--) { - result.push(matrix[bottom][col]); - } - bottom--; + right -= 1; + currentRow = -1; + currentCol = -1; + addStep("Finished right column traversal. Move right boundary inward.", "SET right = right - 1", 16, 11, 16, 17); + + // Check crossed + direction = "check"; + addStep("Check if the submatrix boundaries have crossed to prevent duplicate traversal.", "IF NOT (left < right AND top < bottom) -> BREAK", 17, 12, 17, 18); + if (!(left < right && top < bottom)) { + break; } - - if (left <= right) { - for (let row = bottom; row >= top; row--) { - result.push(matrix[row][left]); - } - left++; + + // Right to left + direction = "left"; + for (let i = right - 1; i >= left; i--) { + currentCol = i; + currentRow = bottom - 1; + result.push(matrix[currentRow][currentCol]); + visited[currentRow][currentCol] = true; + addStep(`Traverse left on bottom row. Collect cell matrix[${currentRow}][${currentCol}] = ${matrix[currentRow][currentCol]}.`, `APPEND matrix[bottom - 1][${i}]`, 21, 15, 21, 22); } + bottom -= 1; + currentRow = -1; + currentCol = -1; + addStep("Finished bottom row traversal. Move bottom boundary inward.", "SET bottom = bottom - 1", 23, 16, 23, 24); + + // Bottom to top + direction = "up"; + for (let i = bottom - 1; i >= top; i--) { + currentCol = left; + currentRow = i; + result.push(matrix[currentRow][currentCol]); + visited[currentRow][currentCol] = true; + addStep(`Traverse up on left column. Collect cell matrix[${currentRow}][${currentCol}] = ${matrix[currentRow][currentCol]}.`, `APPEND matrix[${i}][left]`, 25, 18, 25, 26); + } + left += 1; + currentRow = -1; + currentCol = -1; + addStep("Finished left column traversal. Move left boundary inward.", "SET left = left + 1", 27, 19, 27, 28); } - return result; -}`; - const currentStep = steps[Math.min(currentStepIndex, steps.length - 1)]; + direction = "complete"; + addStep("Traversals complete. Return the elements in spiral order.", "RETURN res", 29, 20, 29, 30); + + return { steps, stepLineNumbers }; +}; + +export const SpiralMatrixVisualization = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + + const { steps, stepLineNumbers } = useMemo(() => { + return generateVisualizationData(); + }, []); + + if (steps.length === 0) return null; + + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); const getCellColor = (row: number, col: number) => { if (!currentStep?.matrix || !currentStep?.visited?.[row]) return 'bg-muted text-muted-foreground border-border'; if (row === currentStep.currentRow && col === currentStep.currentCol) { - return 'bg-primary text-primary-foreground border-primary shadow-lg'; + return 'bg-primary text-primary-foreground border-primary shadow-lg scale-105 z-10'; } if (currentStep.visited[row][col]) { return 'bg-secondary/60 text-secondary-foreground border-secondary'; @@ -395,111 +311,82 @@ export const SpiralMatrixVisualization = () => { }; return ( -
- - -
- -

Spiral Matrix

-
- -
- Direction: - - {getDirectionArrow(currentStep.direction)} {currentStep.direction.toUpperCase()} - -
-
-
- {currentStep.matrix.map((row, rowIdx) => ( - row.map((cell, colIdx) => ( -
- {cell} -
- )) - ))} -
+ +
+
+ Direction: + + {getDirectionArrow(currentStep.direction)} {currentStep.direction.toUpperCase()} + +
+
+
+ {currentStep.matrix.map((row, rowIdx) => ( + row.map((cell, colIdx) => ( +
+ {cell} +
+ )) + ))}
- - - -
Spiral Order Result:
+
+ +
+
Spiral Order Result:
{currentStep.result.map((num, idx) => (
{num}
))}
- - - -
Executing:
- {currentStep.lineExecution} -
- - - {currentStep.explanation} - - - - - +
+
+ + {/* Descriptive Commentary Box (at the bottom) */} +
+
+
+ Process Step +
+ {currentStep.explanation} +
+ + {/* Variable Panel (below the commentary box) */} +
+
- - - - n >= 1 && n <= code.split('\n').length)} - /> - -
-
+
+ } + rightContent={ + setCurrentStepIndex(0)} + /> + } + controls={ + + } + /> ); }; \ No newline at end of file diff --git a/src/components/visualizations/algorithms/SubsetBitsVisualization.tsx b/src/components/visualizations/algorithms/SubsetBitsVisualization.tsx index 053b593..c03c0c4 100644 --- a/src/components/visualizations/algorithms/SubsetBitsVisualization.tsx +++ b/src/components/visualizations/algorithms/SubsetBitsVisualization.tsx @@ -1,10 +1,10 @@ -import { useState, useMemo } from 'react'; -import { SimpleStepControls } from '../shared/SimpleStepControls'; -import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; -import { VisualizationLayout } from '../shared/VisualizationLayout'; -import { motion, AnimatePresence } from 'framer-motion'; -import { Card } from '@/components/ui/card'; +import React, { useState, useEffect, useRef } from "react"; +import { StepControls } from "../shared/StepControls"; +import { VariablePanel } from "../shared/VariablePanel"; +import { VisualizationCodePanel } from "../shared/VisualizationCodePanel"; +import { motion, AnimatePresence } from "framer-motion"; +import { Card } from "@/components/ui/card"; +import type { StepLineNumberMap, VisualizationLanguageMap } from "@/types/visualization"; interface Step { nums: number[]; @@ -14,193 +14,268 @@ interface Step { currentSubset: number[]; allSubsets: number[][]; explanation: string; - highlightedLines: number[]; - variables: Record; + pseudoStep: string; } -export const SubsetBitsVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); +const languages: VisualizationLanguageMap = { + typescript: `function subsets(nums: number[]): number[][] { + const res: number[][] = []; + const n = nums.length; + for (let mask = 0; mask < (1 << n); mask++) { + const subset: number[] = []; + for (let i = 0; i < n; i++) { + if (mask & (1 << i)) { + subset.push(nums[i]); + } + } + res.push(subset); + } + return res; +}`, + python: `def subsets(nums): + res = [] + n = len(nums) + for mask in range(1 << n): + subset = [] + for i in range(n): + if mask & (1 << i): + subset.append(nums[i]) + res.append(subset) + return res`, + java: `public static class Solution { + public List> subsets(int[] nums) { + List> res = new ArrayList<>(); + int n = nums.length; + for (int mask = 0; mask < (1 << n); mask++) { + List subset = new ArrayList<>(); + for (int i = 0; i < n; i++) { + if ((mask & (1 << i)) != 0) { + subset.add(nums[i]); + } + } + res.add(subset); + } + return res; + } +}`, + cpp: `class Solution { +public: + vector> subsets(vector& nums) { + vector> res; + int n = nums.size(); + for (int mask = 0; mask < (1 << n); mask++) { + vector subset; + for (int i = 0; i < n; i++) { + if ((mask & (1 << i)) != 0) { + subset.push_back(nums[i]); + } + } + res.push_back(subset); + } + return res; + } +};` +}; + +export const SubsetBitsVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); + const nums = [1, 2, 3]; - const steps: Step[] = useMemo(() => { - const s: Step[] = []; + const generateStepsData = () => { + const steps: Step[] = []; const res: number[][] = []; const n = nums.length; - // Initialization - s.push({ - nums, - mask: -1, - i: -1, - bitSet: false, - currentSubset: [], - allSubsets: [], + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + steps.push({ + nums, mask: -1, i: -1, bitSet: false, currentSubset: [], allSubsets: [], explanation: "Starting subset generation with nums = [1, 2, 3].", - highlightedLines: [1], - variables: { nums: "[1, 2, 3]", res: "[]" } + pseudoStep: "CALL subsets(nums = [1, 2, 3])" }); + addLines(1, 1, 2, 3); - s.push({ - nums, - mask: -1, - i: -1, - bitSet: false, - currentSubset: [], - allSubsets: [], - explanation: "Initialize an empty array to store results.", - highlightedLines: [2], - variables: { nums: "[1, 2, 3]", res: "[]" } + steps.push({ + nums, mask: -1, i: -1, bitSet: false, currentSubset: [], allSubsets: [], + explanation: "Initialize an empty array res to store the subsets.", + pseudoStep: "SET res = []" }); + addLines(2, 2, 3, 4); - s.push({ - nums, - mask: -1, - i: -1, - bitSet: false, - currentSubset: [], - allSubsets: [], + steps.push({ + nums, mask: -1, i: -1, bitSet: false, currentSubset: [], allSubsets: [], explanation: "Get the number of elements: n = 3.", - highlightedLines: [3], - variables: { nums: "[1, 2, 3]", res: "[]", n: 3 } + pseudoStep: "SET n = 3" }); + addLines(3, 3, 4, 5); for (let mask = 0; mask < (1 << n); mask++) { const binary = mask.toString(2).padStart(n, '0'); - s.push({ - nums, - mask, - i: -1, - bitSet: false, - currentSubset: [], + steps.push({ + nums, mask, i: -1, bitSet: false, currentSubset: [], allSubsets: [...res.map(sub => [...sub])], explanation: `Outer loop: mask = ${mask} (${binary} in binary).`, - highlightedLines: [5], - variables: { n: 3, mask: `${mask} (${binary})`, res: `size ${res.length}` } + pseudoStep: `FOR mask = ${mask} to ${ (1 << n) - 1 }` }); + addLines(4, 4, 5, 6); const subset: number[] = []; - s.push({ - nums, - mask, - i: -1, - bitSet: false, - currentSubset: [], + steps.push({ + nums, mask, i: -1, bitSet: false, currentSubset: [], allSubsets: [...res.map(sub => [...sub])], explanation: "Initialize an empty array for the current subset.", - highlightedLines: [6], - variables: { mask: `${mask} (${binary})`, subset: "[]" } + pseudoStep: "SET subset = []" }); + addLines(5, 5, 6, 7); for (let i = 0; i < n; i++) { - s.push({ - nums, - mask, - i, - bitSet: (mask & (1 << i)) !== 0, - currentSubset: [...subset], - allSubsets: [...res.map(sub => [...sub])], - explanation: `Inner loop: checking bit i = ${i}.`, - highlightedLines: [8], - variables: { mask: `${mask} (${binary})`, i, subset: `[${subset.join(', ')}]` } + steps.push({ + nums, mask, i, bitSet: (mask & (1 << i)) !== 0, + currentSubset: [...subset], allSubsets: [...res.map(sub => [...sub])], + explanation: `Inner loop: check if element at index i = ${i} should be included.`, + pseudoStep: `FOR i = ${i} to ${n - 1}` }); + addLines(6, 6, 7, 8); const bitIsSet = (mask & (1 << i)) !== 0; - s.push({ - nums, - mask, - i, - bitSet: bitIsSet, - currentSubset: [...subset], + steps.push({ + nums, mask, i, bitSet: bitIsSet, currentSubset: [...subset], allSubsets: [...res.map(sub => [...sub])], - explanation: `Checking if bit ${i} is set: (mask & (1 << ${i})) is ${bitIsSet ? 'non-zero' : 'zero'}.`, - highlightedLines: [9], - variables: { mask: `${mask} (${binary})`, i, bitIsSet } + explanation: `Check if bit ${i} is set in mask: (mask & (1 << ${i})) is ${bitIsSet ? 'non-zero' : 'zero'}.`, + pseudoStep: `IF mask & (1 << ${i}) → ${bitIsSet ? 'YES ✓' : 'NO ✗'}` }); + addLines(7, 7, 8, 9); if (bitIsSet) { subset.push(nums[i]); - s.push({ - nums, - mask, - i, - bitSet: true, - currentSubset: [...subset], + steps.push({ + nums, mask, i, bitSet: true, currentSubset: [...subset], allSubsets: [...res.map(sub => [...sub])], explanation: `Bit ${i} is set. Adding nums[${i}] = ${nums[i]} to the subset.`, - highlightedLines: [10], - variables: { mask: `${mask} (${binary})`, i, subset: `[${subset.join(', ')}]` } + pseudoStep: `ADD nums[${i}] (${nums[i]}) to subset` }); + addLines(8, 8, 9, 10); } } res.push([...subset]); - s.push({ - nums, - mask, - i: -1, - bitSet: false, - currentSubset: [...subset], + steps.push({ + nums, mask, i: -1, bitSet: false, currentSubset: [...subset], allSubsets: [...res.map(sub => [...sub])], explanation: `Finished building subset [${subset.join(', ')}]. Adding it to result res.`, - highlightedLines: [14], - variables: { mask: `${mask} (${binary})`, res: `size ${res.length}` } + pseudoStep: `ADD subset copy to res → res.push([${subset.join(', ')}])` }); + addLines(11, 9, 12, 13); } - s.push({ - nums, - mask: -1, - i: -1, - bitSet: false, - currentSubset: [], + steps.push({ + nums, mask: -1, i: -1, bitSet: false, currentSubset: [], allSubsets: [...res.map(sub => [...sub])], explanation: "Algorithm complete. Returning all subsets.", - highlightedLines: [17], - variables: { totalSubsets: res.length } + pseudoStep: "RETURN res" }); + addLines(13, 10, 14, 15); - return s; - }, [nums]); - - const code = `function subsets(nums: number[]): number[][] { - const res: number[][] = []; - const n = nums.length; + const lastStep = steps[steps.length - 1]; + steps.push({ + ...lastStep, + explanation: "Algorithm Complete!", + pseudoStep: "DONE" + }); + addLines(13, 10, 14, 15); - for (let mask = 0; mask < (1 << n); mask++) { - const subset: number[] = []; + return { steps, stepLineNumbers }; + }; - for (let i = 0; i < n; i++) { - if (mask & (1 << i)) { - subset.push(nums[i]); - } - } + const { steps, stepLineNumbers } = generateStepsData(); - res.push(subset); + useEffect(() => { + if (isPlaying && currentStepIndex < steps.length - 1) { + intervalRef.current = setInterval(() => { + setCurrentStepIndex((prev) => { + if (prev >= steps.length - 1) { + setIsPlaying(false); + return prev; + } + return prev + 1; + }); + }, 1000 / speed); + } else { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } } - return res; -}`; + return () => { + if (intervalRef.current) clearInterval(intervalRef.current); + }; + }, [isPlaying, currentStepIndex, steps.length, speed]); + + const handlePlay = () => setIsPlaying(true); + const handlePause = () => setIsPlaying(false); + const handleStepForward = () => { + if (currentStepIndex < steps.length - 1) setCurrentStepIndex(currentStepIndex + 1); + }; + const handleStepBack = () => { + if (currentStepIndex > 0) setCurrentStepIndex(currentStepIndex - 1); + }; + const handleReset = () => { + setCurrentStepIndex(0); + setIsPlaying(false); + }; - const step = steps[currentStep]; + if (steps.length === 0) return null; + + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map((s) => s.pseudoStep); return ( - - +
+ +
+
+

Bitmask Status

- Current Mask (Decimal) - {step.mask === -1 ? 'N/A' : step.mask} + Current Mask (Decimal) + {currentStep.mask === -1 ? 'N/A' : currentStep.mask}
Binary Representation (Bits for positions 0, 1, 2)
{[0, 1, 2].map((bitIdx) => { - const isBitActive = step.mask !== -1 && (step.mask & (1 << bitIdx)) !== 0; - const isCurrentBitBeingChecked = step.i === bitIdx; + const isBitActive = currentStep.mask !== -1 && (currentStep.mask & (1 << bitIdx)) !== 0; + const isCurrentBitBeingChecked = currentStep.i === bitIdx; return (
{ ? "var(--accent)" : isBitActive ? "var(--primary)" : "var(--muted)", }} - className={`w-14 h-14 rounded-xl flex items-center justify-center border-2 transition-colors ${isBitActive ? "border-primary text-primary-foreground shadow-[0_0_15px_rgba(var(--primary),0.3)]" : "border-border text-muted-foreground" - }`} + className={`w-14 h-14 rounded-xl flex items-center justify-center border-2 transition-colors ${ + isBitActive + ? "border-primary text-primary-foreground shadow-[0_0_15px_rgba(var(--primary),0.3)]" + : "border-border text-muted-foreground" + }`} > {isBitActive ? '1' : '0'} @@ -224,14 +302,14 @@ export const SubsetBitsVisualization = () => {
- +

Current Working Subset

- {step.currentSubset.length === 0 ? ( + {currentStep.currentSubset.length === 0 ? ( Empty Set (∅) ) : ( - step.currentSubset.map((val) => ( + currentStep.currentSubset.map((val) => ( {
- -

Explanation

-

{step.explanation}

-
+
+

Algorithm Logic

+

{currentStep.explanation}

+
- -

All Subsets Found ({step.allSubsets.length})

+
+

All Subsets Found ({currentStep.allSubsets.length})

- {step.allSubsets.map((sub, idx) => ( - + {currentStep.allSubsets.map((sub, idx) => ( + [{sub.join(', ') || '∅'}] ))}
- - +
+
- } - rightContent={ - - } - controls={ - - } - /> +
+
); }; diff --git a/src/components/visualizations/algorithms/SubsetsVisualization.tsx b/src/components/visualizations/algorithms/SubsetsVisualization.tsx index 72eee2a..fd52758 100644 --- a/src/components/visualizations/algorithms/SubsetsVisualization.tsx +++ b/src/components/visualizations/algorithms/SubsetsVisualization.tsx @@ -1,8 +1,8 @@ -import React, { useEffect, useRef, useState } from "react"; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import React, { useState, useEffect, useRef } from "react"; import { StepControls } from "../shared/StepControls"; import { VariablePanel } from "../shared/VariablePanel"; +import { VisualizationCodePanel } from "../shared/VisualizationCodePanel"; +import type { StepLineNumberMap, VisualizationLanguageMap } from "@/types/visualization"; interface Step { nums: number[]; @@ -10,136 +10,207 @@ interface Step { i: number; res: number[][]; message: string; - lineNumber: number; + pseudoStep: string; } -export const SubsetsVisualization: React.FC = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1000); - const intervalRef = useRef(null); - - const code = `function subsets(nums: number[]): number[][] { +const languages: VisualizationLanguageMap = { + typescript: `function subsets(nums: number[]): number[][] { const res: number[][] = []; const subset: number[] = []; - function dfs(i: number) { if (i >= nums.length) { res.push([...subset]); return; } - subset.push(nums[i]); dfs(i + 1); - subset.pop(); dfs(i + 1); } - dfs(0); return res; -}`; +}`, + python: `def subsets(nums): + res = [] + subset = [] + def dfs(i): + if i >= len(nums): + res.append(subset.copy()) + return + subset.append(nums[i]) + dfs(i + 1) + subset.pop() + dfs(i + 1) + dfs(0) + return res`, + java: `public static class Solution { + public List> subsets(int[] nums) { + List> res = new ArrayList<>(); + dfs(0, nums, new ArrayList<>(), res); + return res; + } + private void dfs(int i, int[] nums, List subset, List> res) { + if (i == nums.length) { + res.add(new ArrayList<>(subset)); + return; + } + subset.add(nums[i]); + dfs(i + 1, nums, subset, res); + subset.remove(subset.size() - 1); + dfs(i + 1, nums, subset, res); + } +}`, + cpp: `class Solution { +public: + vector> subsets(vector& nums) { + vector> res; + vector subset; + dfs(0, nums, subset, res); + return res; + } +private: + void dfs(int i, vector& nums, vector& subset, vector>& res) { + if (i == nums.size()) { + res.push_back(subset); + return; + } + subset.push_back(nums[i]); + dfs(i + 1, nums, subset, res); + subset.pop_back(); + dfs(i + 1, nums, subset, res); + } +};` +}; + +export const SubsetsVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); - const generateSteps = () => { + const generateStepsData = () => { const nums = [1, 2, 3]; - const newSteps: Step[] = []; + const steps: Step[] = []; const res: number[][] = []; const subset: number[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - newSteps.push({ + steps.push({ nums, subset: [...subset], i: 0, res: [...res], message: 'Initialize res and subset arrays', - lineNumber: 2 + pseudoStep: 'SET res = [], subset = []' }); + addLines(2, 2, 3, 4); + + steps.push({ + nums, subset: [...subset], i: 0, res: [...res], + message: 'Start initial DFS call from index 0', + pseudoStep: 'CALL dfs(0)' + }); + addLines(14, 12, 4, 6); function dfs(i: number) { - newSteps.push({ + steps.push({ nums, subset: [...subset], i, res: res.map((s) => [...s]), message: `dfs(${i}) called`, - lineNumber: 5 + pseudoStep: `CALL dfs(i = ${i})` }); + addLines(4, 4, 7, 10); - newSteps.push({ + steps.push({ nums, subset: [...subset], i, res: res.map((s) => [...s]), message: `Check base case: i >= nums.length -> ${i} >= ${nums.length}`, - lineNumber: 6 + pseudoStep: `IF i >= nums.length → ${i} >= ${nums.length} ?` }); + addLines(5, 5, 8, 11); if (i >= nums.length) { res.push([...subset]); - newSteps.push({ + steps.push({ nums, subset: [...subset], i, res: res.map((s) => [...s]), message: `Base case met! Push copy of current subset [${subset.join(', ')}] to res`, - lineNumber: 7 + pseudoStep: `ADD subset copy to res → res.push([${subset.join(', ')}])` }); + addLines(6, 6, 9, 12); - newSteps.push({ + steps.push({ nums, subset: [...subset], i, res: res.map((s) => [...s]), message: `Return from dfs(${i})`, - lineNumber: 8 + pseudoStep: `RETURN` }); + addLines(7, 7, 10, 13); return; } subset.push(nums[i]); - newSteps.push({ + steps.push({ nums, subset: [...subset], i, res: res.map((s) => [...s]), message: `Include nums[${i}] (${nums[i]}) in subset. subset is now [${subset.join(', ')}]`, - lineNumber: 11 + pseudoStep: `ADD nums[${i}] (${nums[i]}) to subset` }); + addLines(9, 8, 12, 15); - newSteps.push({ + steps.push({ nums, subset: [...subset], i, res: res.map((s) => [...s]), message: `Recursive call dfs(${i + 1}) to explore inclusion branch`, - lineNumber: 12 + pseudoStep: `CALL dfs(i + 1 = ${i + 1})` }); + addLines(10, 9, 13, 16); dfs(i + 1); const popped = subset.pop(); - newSteps.push({ + steps.push({ nums, subset: [...subset], i, res: res.map((s) => [...s]), message: `Backtrack: pop ${popped} from subset. subset is now [${subset.join(', ')}]`, - lineNumber: 14 + pseudoStep: `REMOVE last element (${popped}) from subset (backtrack)` }); + addLines(11, 10, 14, 17); - newSteps.push({ + steps.push({ nums, subset: [...subset], i, res: res.map((s) => [...s]), message: `Recursive call dfs(${i + 1}) to explore exclusion branch`, - lineNumber: 15 + pseudoStep: `CALL dfs(i + 1 = ${i + 1})` }); + addLines(12, 11, 15, 18); dfs(i + 1); - newSteps.push({ + steps.push({ nums, subset: [...subset], i, res: res.map((s) => [...s]), message: `dfs(${i}) execution finished, returning to caller.`, - lineNumber: 16 + pseudoStep: `RETURN` }); + addLines(13, 12, 16, 19); } - newSteps.push({ - nums, subset: [...subset], i: 0, res: [...res], - message: 'Start initial DFS call from index 0', - lineNumber: 18 - }); dfs(0); - newSteps.push({ + steps.push({ nums, subset: [...subset], i: 0, res: [...res], message: 'DFS complete. Return final result.', - lineNumber: 19 + pseudoStep: 'RETURN res' }); + addLines(15, 13, 5, 7); - setSteps(newSteps); + return { steps, stepLineNumbers }; }; - useEffect(() => { - generateSteps(); - }, []); + const { steps, stepLineNumbers } = generateStepsData(); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { - intervalRef.current = window.setInterval(() => { + intervalRef.current = setInterval(() => { setCurrentStepIndex((prev) => { if (prev >= steps.length - 1) { setIsPlaying(false); @@ -147,7 +218,7 @@ export const SubsetsVisualization: React.FC = () => { } return prev + 1; }); - }, speed); + }, 1000 / speed); } else { if (intervalRef.current) { clearInterval(intervalRef.current); @@ -177,6 +248,7 @@ export const SubsetsVisualization: React.FC = () => { if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map((s) => s.pseudoStep); return (
@@ -188,76 +260,83 @@ export const SubsetsVisualization: React.FC = () => { onReset={handleReset} isPlaying={isPlaying} currentStep={currentStepIndex} - totalSteps={steps.length} + totalSteps={steps.length - 1} speed={speed} onSpeedChange={setSpeed} />
-
-

Input Array (nums)

-
- {currentStep.nums.map((val, idx) => ( -
- {val} -
- ))} -
- -

Current Subset

-
- {currentStep.subset.length > 0 ? ( - currentStep.subset.map((val, idx) => ( +
+
+

Input Array (nums)

+
+ {currentStep.nums.map((val, idx) => (
{val}
- )) - ) : ( -
Empty []
- )} + ))} +
+ +

Current Subset

+
+ {currentStep.subset.length > 0 ? ( + currentStep.subset.map((val, idx) => ( +
+ {val} +
+ )) + ) : ( +
Empty []
+ )} +
+ +

+ Result Array `res` ({currentStep.res.length}) +

+
+ {currentStep.res.map((sub, idx) => ( +
+ [{sub.join(", ")}] +
+ ))} +
-

- Result Array `res` ({currentStep.res.length}) -

-
- {currentStep.res.map((sub, idx) => ( -
- [{sub.join(", ")}] -
- ))} +
+

Algorithm Logic

+

+ {currentStep.message} +

-
-

{currentStep.message}

-
-
- -
+
-
diff --git a/src/components/visualizations/algorithms/SudokuSolverVisualization.tsx b/src/components/visualizations/algorithms/SudokuSolverVisualization.tsx index 4899ef4..79d6460 100644 --- a/src/components/visualizations/algorithms/SudokuSolverVisualization.tsx +++ b/src/components/visualizations/algorithms/SudokuSolverVisualization.tsx @@ -1,8 +1,8 @@ -import React, { useEffect, useRef, useState } from 'react'; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; -import { StepControls } from '../shared/StepControls'; -import { VariablePanel } from '../shared/VariablePanel'; +import React, { useState, useEffect, useRef } from "react"; +import { StepControls } from "../shared/StepControls"; +import { VariablePanel } from "../shared/VariablePanel"; +import { VisualizationCodePanel } from "../shared/VisualizationCodePanel"; +import type { StepLineNumberMap, VisualizationLanguageMap } from "@/types/visualization"; interface Step { board: number[][]; @@ -10,24 +10,16 @@ interface Step { col: number; value: number; message: string; - lineNumber: number; solved: boolean; + pseudoStep: string; } -export const SudokuSolverVisualization: React.FC = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(500); - const intervalRef = useRef(null); - - const code = `function solveSudoku(board: number[][]): boolean { +const languages: VisualizationLanguageMap = { + typescript: `function solveSudoku(board: number[][]): boolean { function isValid(row: number, col: number, num: number): boolean { - // Check row and column for (let i = 0; i < 9; i++) { if (board[row][i] === num || board[i][col] === num) return false; } - // Check 3x3 box const boxRow = Math.floor(row / 3) * 3; const boxCol = Math.floor(col / 3) * 3; for (let i = 0; i < 3; i++) { @@ -37,7 +29,6 @@ export const SudokuSolverVisualization: React.FC = () => { } return true; } - function solve(): boolean { for (let row = 0; row < 9; row++) { for (let col = 0; col < 9; col++) { @@ -46,7 +37,7 @@ export const SudokuSolverVisualization: React.FC = () => { if (isValid(row, col, num)) { board[row][col] = num; if (solve()) return true; - board[row][col] = 0; // Backtrack + board[row][col] = 0; } } return false; @@ -55,20 +46,135 @@ export const SudokuSolverVisualization: React.FC = () => { } return true; } - return solve(); -}`; +}`, + python: `def solveSudoku(board): + def isValid(row, col, num): + for i in range(9): + if board[row][i] == num or board[i][col] == num: + return False + boxRow = (row // 3) * 3 + boxCol = (col // 3) * 3 + for i in range(3): + for j in range(3): + if board[boxRow + i][boxCol + j] == num: + return False + return True + def solve(): + for r in range(9): + for c in range(9): + if board[r][c] == 0: + for num in range(1, 10): + if isValid(r, c, num): + board[r][c] = num + if solve(): + return True + board[r][c] = 0 + return False + return True + solve()`, + java: `public static class Solution { + public void solveSudoku(int[][] board) { + solve(board); + } + private boolean solve(int[][] board) { + for (int r = 0; r < 9; r++) { + for (int c = 0; c < 9; c++) { + if (board[r][c] == 0) { + for (int num = 1; num <= 9; num++) { + if (isValid(board, r, c, num)) { + board[r][c] = num; + if (solve(board)) return true; + board[r][c] = 0; + } + } + return false; + } + } + } + return true; + } + private boolean isValid(int[][] board, int row, int col, int num) { + for (int i = 0; i < 9; i++) { + if (board[row][i] == num || board[i][col] == num) return false; + } + int boxRow = (row / 3) * 3; + int boxCol = (col / 3) * 3; + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + if (board[boxRow + i][boxCol + j] == num) return false; + } + } + return true; + } +}`, + cpp: `class Solution { +public: + void solveSudoku(vector>& board) { + solve(board); + } +private: + bool solve(vector>& board) { + for (int r = 0; r < 9; r++) { + for (int c = 0; c < 9; c++) { + if (board[r][c] == 0) { + for (int num = 1; num <= 9; num++) { + if (isValid(board, r, c, num)) { + board[r][c] = num; + if (solve(board)) return true; + board[r][c] = 0; + } + } + return false; + } + } + } + return true; + } + bool isValid(vector>& board, int row, int col, int num) { + for (int i = 0; i < 9; i++) { + if (board[row][i] == num || board[i][col] == num) return false; + } + int boxRow = (row / 3) * 3; + int boxCol = (col / 3) * 3; + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + if (board[boxRow + i][boxCol + j] == num) return false; + } + } + return true; + } +};` +}; + +export const SudokuSolverVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); - const generateSteps = () => { - // Simplified 4x4 Sudoku for visualization + const generateStepsData = () => { const board = [ [0, 2, 0, 4], [4, 0, 2, 0], [0, 4, 0, 2], [2, 0, 4, 0] ]; - const newSteps: Step[] = []; + const steps: Step[] = []; const size = 4; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; function isValid(row: number, col: number, num: number): boolean { for (let i = 0; i < size; i++) { @@ -89,40 +195,54 @@ export const SudokuSolverVisualization: React.FC = () => { for (let col = 0; col < size; col++) { if (board[row][col] === 0) { for (let num = 1; num <= size; num++) { - newSteps.push({ + steps.push({ board: board.map(r => [...r]), row, col, value: num, - message: `Trying ${num} at (${row}, ${col})`, - lineNumber: 21, - solved: false + message: `Checking if ${num} is valid at (${row}, ${col})`, + solved: false, + pseudoStep: `IF isValid(row = ${row}, col = ${col}, num = ${num})` }); + addLines(20, 18, 10, 12); if (isValid(row, col, num)) { board[row][col] = num; - newSteps.push({ + steps.push({ board: board.map(r => [...r]), row, col, value: num, message: `Placed ${num} at (${row}, ${col})`, - lineNumber: 23, - solved: false + solved: false, + pseudoStep: `SET board[${row}][${col}] = ${num}` + }); + addLines(21, 19, 11, 13); + + steps.push({ + board: board.map(r => [...r]), + row, + col, + value: num, + message: `Recursively solve Sudoku with updated board`, + solved: false, + pseudoStep: `CALL solve()` }); + addLines(22, 20, 12, 14); if (solve()) return true; board[row][col] = 0; - newSteps.push({ + steps.push({ board: board.map(r => [...r]), row, col, value: 0, - message: `Backtrack: Remove from (${row}, ${col})`, - lineNumber: 25, - solved: false + message: `Backtrack: Remove ${num} from (${row}, ${col})`, + solved: false, + pseudoStep: `SET board[${row}][${col}] = 0 (backtrack)` }); + addLines(23, 22, 13, 15); } } return false; @@ -130,29 +250,37 @@ export const SudokuSolverVisualization: React.FC = () => { } } - newSteps.push({ + steps.push({ board: board.map(r => [...r]), row: -1, col: -1, value: 0, - message: 'Sudoku solved!', - lineNumber: 31, - solved: true + message: "Sudoku solved!", + solved: true, + pseudoStep: "RETURN True" }); + addLines(30, 24, 20, 22); return true; } solve(); - setSteps(newSteps); + + const lastStep = steps[steps.length - 1]; + steps.push({ + ...lastStep, + message: "Solving process complete.", + pseudoStep: "DONE" + }); + addLines(30, 24, 20, 22); + + return { steps, stepLineNumbers }; }; - useEffect(() => { - generateSteps(); - }, []); + const { steps, stepLineNumbers } = generateStepsData(); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { - intervalRef.current = window.setInterval(() => { + intervalRef.current = setInterval(() => { setCurrentStepIndex((prev) => { if (prev >= steps.length - 1) { setIsPlaying(false); @@ -160,7 +288,7 @@ export const SudokuSolverVisualization: React.FC = () => { } return prev + 1; }); - }, speed); + }, 1000 / speed); } else { if (intervalRef.current) { clearInterval(intervalRef.current); @@ -189,6 +317,7 @@ export const SudokuSolverVisualization: React.FC = () => { if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map((s) => s.pseudoStep); return (
@@ -200,49 +329,61 @@ export const SudokuSolverVisualization: React.FC = () => { onReset={handleReset} isPlaying={isPlaying} currentStep={currentStepIndex} - totalSteps={steps.length} + totalSteps={steps.length - 1} speed={speed} onSpeedChange={setSpeed} />
+
+
+

4x4 Sudoku Board (Simplified)

-
-

4x4 Sudoku Board (Simplified)

- -
- {currentStep.board.map((row, r) => - row.map((cell, c) => ( -
+ {currentStep.board.map((row, r) => + row.map((cell, c) => ( +
- {cell !== 0 ? cell : ''} -
- )) - )} + > + {cell !== 0 ? cell : ""} +
+ )) + )} +
-
-

{currentStep.message}

-
-
- = 0 ? currentStep.row : 'done', - 'current col': currentStep.col >= 0 ? currentStep.col : 'done', - 'trying value': currentStep.value || 'none', - 'solved': String(currentStep.solved) - }} - /> +
+

Algorithm Logic

+

+ {currentStep.message} +

+ = 0 ? currentStep.row : "done", + "current col": currentStep.col >= 0 ? currentStep.col : "done", + "trying value": currentStep.value || "none", + "solved": String(currentStep.solved) + }} + />
- + +
); diff --git a/src/components/visualizations/algorithms/SumOfTwoIntegersVisualization.tsx b/src/components/visualizations/algorithms/SumOfTwoIntegersVisualization.tsx index 409400a..ef0a4d6 100644 --- a/src/components/visualizations/algorithms/SumOfTwoIntegersVisualization.tsx +++ b/src/components/visualizations/algorithms/SumOfTwoIntegersVisualization.tsx @@ -1,240 +1,379 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; +import { Card } from '@/components/ui/card'; +import { VisualizationLayout } from '../shared/VisualizationLayout'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; -import { VisualizationLayout } from '../shared/VisualizationLayout'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; + +interface Step { + variables: Record; + explanation: string; + pseudoStep: string; + bitsA: string[]; + bitsB: string[]; + bitsCarry?: string[]; +} + +const languages: VisualizationLanguageMap = { + typescript: `function getSum(a: number, b: number): number { + while (b !== 0) { + const carry = a & b; + a = a ^ b; + b = carry << 1; + } + return a; +}`, + python: `def getSum(a: int, b: int) -> int: + MASK = 0xFFFFFFFF + MAX = 0x7FFFFFFF + while b != 0: + carry = (a & b) & MASK + a = (a ^ b) & MASK + b = (carry << 1) & MASK + return a if a <= MAX else ~(a ^ MASK)`, + java: `public static class Solution { + public int getSum(int a, int b) { + while (b != 0) { + int carry = a & b; + a = a ^ b; + b = carry << 1; + } + return a; + } +}`, + cpp: `class Solution { +public: + int getSum(int a, int b) { + while (b != 0) { + int carry = a & b; + a = a ^ b; + b = carry << 1; + } + return a; + } +};` +}; export const SumOfTwoIntegersVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); + const [steps, setSteps] = useState([]); + const [stepLineNumbers, setStepLineNumbers] = useState({ + typescript: [], + python: [], + java: [], + cpp: [] + }); + const [currentStepIndex, setCurrentStepIndex] = useState(0); - const steps = [ - { + useEffect(() => { + const generatedSteps: Step[] = []; + const stepLines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLines.typescript!.push(ts); + stepLines.python!.push(py); + stepLines.java!.push(java); + stepLines.cpp!.push(cpp); + }; + + // Step 0: start + generatedSteps.push({ variables: { a: 3, b: 5, aBinary: '0011', bBinary: '0101', carry: '-', result: '-' }, - explanation: "Start: a = 3 (binary: 0011), b = 5 (binary: 0101). Goal: Add without + operator", - highlightedLine: 0, + explanation: "Start bitwise addition: a = 3 (binary: 0011), b = 5 (binary: 0101). Goal is to sum without the '+' operator.", + pseudoStep: "FUNCTION getSum(a, b)", bitsA: ['0', '0', '1', '1'], bitsB: ['0', '1', '0', '1'] - }, - { + }); + addLines(1, 1, 2, 3); + + // Step 1: while check + generatedSteps.push({ variables: { a: 3, b: 5, aBinary: '0011', bBinary: '0101', carry: '-', result: '-' }, - explanation: "Enter while loop: b ≠ 0, so continue processing", - highlightedLine: 1, + explanation: "Check loop condition: b !== 0. Since b is 5, we enter the loop.", + pseudoStep: "WHILE b != 0", bitsA: ['0', '0', '1', '1'], bitsB: ['0', '1', '0', '1'] - }, - { + }); + addLines(2, 4, 3, 4); + + // Step 2: carry + generatedSteps.push({ variables: { a: 3, b: 5, aBinary: '0011', bBinary: '0101', carry: '0001', result: '-' }, - explanation: "Calculate carry: a & b = 0011 & 0101 = 0001 (where both bits are 1)", - highlightedLine: 2, + explanation: "Calculate carry by performing bitwise AND: carry = a & b = 0011 & 0101 = 0001 (carry occurs where both bits are 1).", + pseudoStep: "SET carry = a & b", bitsA: ['0', '0', '1', '1'], bitsB: ['0', '1', '0', '1'], bitsCarry: ['0', '0', '0', '1'] - }, - { + }); + addLines(3, 5, 4, 5); + + // Step 3: a = a ^ b + generatedSteps.push({ variables: { a: 6, b: 5, aBinary: '0110', bBinary: '0101', carry: '0001', result: '-' }, - explanation: "Calculate sum without carry: a = a ^ b = 0011 ^ 0101 = 0110 (XOR adds bits ignoring carry)", - highlightedLine: 3, + explanation: "Calculate XOR sum (sum ignoring carries): a = a ^ b = 0011 ^ 0101 = 0110 (6).", + pseudoStep: "SET a = a ^ b", bitsA: ['0', '1', '1', '0'], bitsB: ['0', '1', '0', '1'] - }, - { + }); + addLines(4, 6, 5, 6); + + // Step 4: b = carry << 1 + generatedSteps.push({ variables: { a: 6, b: 2, aBinary: '0110', bBinary: '0010', carry: '0001', result: '-' }, - explanation: "Shift carry left: b = carry << 1 = 0001 << 1 = 0010 (move carry to next position)", - highlightedLine: 4, + explanation: "Shift carry left by 1 bit to prepare it for addition at the next higher position: b = carry << 1 = 0010 (2).", + pseudoStep: "SET b = carry << 1", bitsA: ['0', '1', '1', '0'], bitsB: ['0', '0', '1', '0'] - }, - { + }); + addLines(5, 7, 6, 7); + + // Step 5: loop check + generatedSteps.push({ variables: { a: 6, b: 2, aBinary: '0110', bBinary: '0010', carry: '-', result: '-' }, - explanation: "Loop again: b = 2 ≠ 0, continue processing", - highlightedLine: 1, + explanation: "Loop check: b is 2, which is not 0, so continue adding carries.", + pseudoStep: "WHILE b != 0", bitsA: ['0', '1', '1', '0'], bitsB: ['0', '0', '1', '0'] - }, - { + }); + addLines(2, 4, 3, 4); + + // Step 6: carry + generatedSteps.push({ variables: { a: 6, b: 2, aBinary: '0110', bBinary: '0010', carry: '0010', result: '-' }, - explanation: "Calculate carry: a & b = 0110 & 0010 = 0010", - highlightedLine: 2, + explanation: "Calculate new carry: carry = a & b = 0110 & 0010 = 0010.", + pseudoStep: "SET carry = a & b", bitsA: ['0', '1', '1', '0'], bitsB: ['0', '0', '1', '0'], bitsCarry: ['0', '0', '1', '0'] - }, - { + }); + addLines(3, 5, 4, 5); + + // Step 7: a = a ^ b + generatedSteps.push({ variables: { a: 4, b: 2, aBinary: '0100', bBinary: '0010', carry: '0010', result: '-' }, - explanation: "Sum without carry: a = a ^ b = 0110 ^ 0010 = 0100", - highlightedLine: 3, + explanation: "Apply XOR sum: a = a ^ b = 0110 ^ 0010 = 0100 (4).", + pseudoStep: "SET a = a ^ b", bitsA: ['0', '1', '0', '0'], bitsB: ['0', '0', '1', '0'] - }, - { + }); + addLines(4, 6, 5, 6); + + // Step 8: b = carry << 1 + generatedSteps.push({ variables: { a: 4, b: 4, aBinary: '0100', bBinary: '0100', carry: '0010', result: '-' }, - explanation: "Shift carry: b = carry << 1 = 0010 << 1 = 0100", - highlightedLine: 4, + explanation: "Shift carry left: b = carry << 1 = 0010 << 1 = 0100 (4).", + pseudoStep: "SET b = carry << 1", bitsA: ['0', '1', '0', '0'], bitsB: ['0', '1', '0', '0'] - }, - { + }); + addLines(5, 7, 6, 7); + + // Step 9: loop check + generatedSteps.push({ variables: { a: 4, b: 4, aBinary: '0100', bBinary: '0100', carry: '-', result: '-' }, - explanation: "Loop again: b = 4 ≠ 0, continue", - highlightedLine: 1, + explanation: "Loop check: b is 4, which is not 0, so continue adding carries.", + pseudoStep: "WHILE b != 0", bitsA: ['0', '1', '0', '0'], bitsB: ['0', '1', '0', '0'] - }, - { + }); + addLines(2, 4, 3, 4); + + // Step 10: carry + generatedSteps.push({ variables: { a: 4, b: 4, aBinary: '0100', bBinary: '0100', carry: '0100', result: '-' }, - explanation: "Calculate carry: a & b = 0100 & 0100 = 0100", - highlightedLine: 2, + explanation: "Calculate carry: carry = a & b = 0100 & 0100 = 0100.", + pseudoStep: "SET carry = a & b", bitsA: ['0', '1', '0', '0'], bitsB: ['0', '1', '0', '0'], bitsCarry: ['0', '1', '0', '0'] - }, - { + }); + addLines(3, 5, 4, 5); + + // Step 11: a = a ^ b + generatedSteps.push({ variables: { a: 0, b: 4, aBinary: '0000', bBinary: '0100', carry: '0100', result: '-' }, - explanation: "Sum without carry: a = a ^ b = 0100 ^ 0100 = 0000", - highlightedLine: 3, + explanation: "Apply XOR sum: a = a ^ b = 0100 ^ 0100 = 0000 (0).", + pseudoStep: "SET a = a ^ b", bitsA: ['0', '0', '0', '0'], bitsB: ['0', '1', '0', '0'] - }, - { + }); + addLines(4, 6, 5, 6); + + // Step 12: b = carry << 1 + generatedSteps.push({ variables: { a: 0, b: 8, aBinary: '0000', bBinary: '1000', carry: '0100', result: '-' }, - explanation: "Shift carry: b = carry << 1 = 0100 << 1 = 1000", - highlightedLine: 4, + explanation: "Shift carry left: b = carry << 1 = 0100 << 1 = 1000 (8).", + pseudoStep: "SET b = carry << 1", bitsA: ['0', '0', '0', '0'], bitsB: ['1', '0', '0', '0'] - }, - { + }); + addLines(5, 7, 6, 7); + + // Step 13: loop check + generatedSteps.push({ variables: { a: 0, b: 8, aBinary: '0000', bBinary: '1000', carry: '-', result: '-' }, - explanation: "Loop again: b = 8 ≠ 0, continue", - highlightedLine: 1, + explanation: "Loop check: b is 8, which is not 0, so continue adding carries.", + pseudoStep: "WHILE b != 0", bitsA: ['0', '0', '0', '0'], bitsB: ['1', '0', '0', '0'] - }, - { + }); + addLines(2, 4, 3, 4); + + // Step 14: carry + generatedSteps.push({ variables: { a: 0, b: 8, aBinary: '0000', bBinary: '1000', carry: '0000', result: '-' }, - explanation: "Calculate carry: a & b = 0000 & 1000 = 0000 (no overlap)", - highlightedLine: 2, + explanation: "Calculate carry: carry = a & b = 0000 & 1000 = 0000 (no carry produced).", + pseudoStep: "SET carry = a & b", bitsA: ['0', '0', '0', '0'], bitsB: ['1', '0', '0', '0'], bitsCarry: ['0', '0', '0', '0'] - }, - { + }); + addLines(3, 5, 4, 5); + + // Step 15: a = a ^ b + generatedSteps.push({ variables: { a: 8, b: 8, aBinary: '1000', bBinary: '1000', carry: '0000', result: '-' }, - explanation: "Sum: a = a ^ b = 0000 ^ 1000 = 1000", - highlightedLine: 3, + explanation: "Apply XOR sum: a = a ^ b = 0000 ^ 1000 = 1000 (8).", + pseudoStep: "SET a = a ^ b", bitsA: ['1', '0', '0', '0'], bitsB: ['1', '0', '0', '0'] - }, - { + }); + addLines(4, 6, 5, 6); + + // Step 16: b = carry << 1 + generatedSteps.push({ variables: { a: 8, b: 0, aBinary: '1000', bBinary: '0000', carry: '0000', result: '-' }, - explanation: "Shift carry: b = carry << 1 = 0000 << 1 = 0000", - highlightedLine: 4, + explanation: "Shift carry left: b = carry << 1 = 0000 << 1 = 0000 (0).", + pseudoStep: "SET b = carry << 1", bitsA: ['1', '0', '0', '0'], bitsB: ['0', '0', '0', '0'] - }, - { + }); + addLines(5, 7, 6, 7); + + // Step 17: loop exit & return + generatedSteps.push({ variables: { a: 8, b: 0, aBinary: '1000', bBinary: '0000', carry: '0000', result: 8 }, - explanation: "Exit loop: b = 0. Return a = 8. Success! 3 + 5 = 8", - highlightedLine: 5, + explanation: "Loop check: b is 0. Exit loop and return a = 8.", + pseudoStep: "RETURN a", bitsA: ['1', '0', '0', '0'], bitsB: ['0', '0', '0', '0'] - } - ]; - - const code = `function getSum(a: number, b: number): number { - while (b !== 0) { - const carry = a & b; - a = a ^ b; - b = carry << 1; - } - return a; -}`; - - const step = steps[currentStep]; - - const leftContent = ( - <> -
-
-
-

a (binary)

-
- {step.bitsA.map((bit, idx) => ( -
- {bit} -
- ))} -
-

{step.variables.aBinary} = {step.variables.a}

-
+ }); + addLines(7, 8, 8, 9); -
-

b (carry)

-
- {step.bitsB.map((bit, idx) => ( -
- {bit} -
- ))} -
-

{step.variables.bBinary} = {step.variables.b}

-
-
+ setSteps(generatedSteps); + setStepLineNumbers(stepLines); + }, []); + + if (steps.length === 0) return null; + + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); + + return ( + + } + leftContent={ +
+
+ +

+ Sum of Two Integers (Bitwise) +

+ +
+
+
+
Variable a (XOR Sum)
+
+ {currentStep.bitsA.map((bit, idx) => ( +
+ {bit} +
+ ))} +
+
+ Val: {currentStep.variables.a} (Binary: {currentStep.variables.aBinary}) +
+
- {step.bitsCarry && ( -
-

Carry Calculation

-
- {step.bitsCarry.map((bit, idx) => ( -
- {bit} +
+
Variable b (Carry Shift)
+
+ {currentStep.bitsB.map((bit, idx) => ( +
+ {bit} +
+ ))} +
+
+ Val: {currentStep.variables.b} (Binary: {currentStep.variables.bBinary}) +
+
- ))} -
-

{step.variables.carry}

-
- )} -
- -
-

{step.explanation}

-
- -
-

Bit Operations:

-
-

• XOR (^): Adds bits without carry (0^0=0, 0^1=1, 1^0=1, 1^1=0)

-

• AND (&): Finds where both bits are 1 (needs carry)

-

• Left shift (<<): Moves carry to next position

-
-
- - - ); + {currentStep.bitsCarry && ( +
+
Calculated Carry (a & b)
+
+ {currentStep.bitsCarry.map((bit, idx) => ( +
+ {bit} +
+ ))} +
+
+ Binary Carry: {currentStep.variables.carry} +
+
+ )} - const rightContent = ( - <> - - - ); +
+
Bitwise Operations Quick Guide:
+
XOR (a ^ b): Adds bits without carry (0^0=0, 1^0=1, 1^1=0).
+
AND (a & b): Identifies carry positions where both bits are 1.
+
Left Shift (carry << 1): Moves carry bits to the next higher position to be added.
+
+
+
+
- const controls = ( - - ); +
+ +
+

Step Explanation

+

{currentStep.explanation}

+ - return ( - +
+
+ } + rightContent={ + setCurrentStepIndex(0)} + /> + } /> ); }; \ No newline at end of file diff --git a/src/components/visualizations/algorithms/TarjansVisualization.tsx b/src/components/visualizations/algorithms/TarjansVisualization.tsx index a0aaece..e78b3f2 100644 --- a/src/components/visualizations/algorithms/TarjansVisualization.tsx +++ b/src/components/visualizations/algorithms/TarjansVisualization.tsx @@ -1,10 +1,11 @@ import { useState, useMemo } from 'react'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; import { motion, AnimatePresence } from 'framer-motion'; import { Card } from '@/components/ui/card'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { disc: number[]; @@ -16,7 +17,7 @@ interface Step { currentNode: number; activeEdge: [number, number] | null; explanation: string; - highlightedLines: number[]; + pseudoStep: string; variables: Record; phase: 'init' | 'visit' | 'recurse' | 'backtrack' | 'backedge' | 'scc' | 'done'; } @@ -50,10 +51,175 @@ function sccIndexFor(sccs: number[][], node: number): number { return sccs.findIndex(scc => scc.includes(node)); } +const languages: VisualizationLanguageMap = { + typescript: `function tarjanSCC(graph: number[][]): number[][] { + const n = graph.length; + const disc = Array(n).fill(-1); + const low = Array(n).fill(-1); + const onStack = Array(n).fill(false); + const stack: number[] = []; + const sccs: number[][] = []; + let time = 0; + function dfs(u: number) { + disc[u] = low[u] = time++; + stack.push(u); + onStack[u] = true; + for (const v of graph[u]) { + if (disc[v] === -1) { + dfs(v); + low[u] = Math.min(low[u], low[v]); + } else if (onStack[v]) { + low[u] = Math.min(low[u], disc[v]); + } + } + if (low[u] === disc[u]) { + const scc: number[] = []; + let w: number; + do { + w = stack.pop()!; + onStack[w] = false; + scc.push(w); + } while (w !== u); + sccs.push(scc); + } + } + for (let i = 0; i < n; i++) { + if (disc[i] === -1) { + dfs(i); + } + } + return sccs; +}`, + + python: `def tarjanSCC(graph): + n = len(graph) + disc = [-1] * n + low = [-1] * n + onStack = [False] * n + stack = [] + sccs = [] + time = 0 + def dfs(u): + nonlocal time + disc[u] = low[u] = time + time += 1 + stack.append(u) + onStack[u] = True + for v in graph[u]: + if disc[v] == -1: + dfs(v) + low[u] = min(low[u], low[v]) + elif onStack[v]: + low[u] = min(low[u], disc[v]) + if low[u] == disc[u]: + scc = [] + while True: + w = stack.pop() + onStack[w] = False + scc.append(w) + if w == u: + break + sccs.append(scc) + for i in range(n): + if disc[i] == -1: + dfs(i) + return sccs`, + + java: `public static class Solution { + public List> tarjanSCC(List> graph) { + int n = graph.size(); + int[] disc = new int[n]; + int[] low = new int[n]; + boolean[] onStack = new boolean[n]; + Stack stack = new Stack<>(); + List> sccs = new ArrayList<>(); + int time = 0; + Arrays.fill(disc, -1); + Arrays.fill(low, -1); + for (int i = 0; i < n; i++) { + if (disc[i] == -1) { + dfs(graph, i, disc, low, onStack, stack, sccs, time = 0); + } + } + return sccs; + } + private void dfs(List> graph, int u, int[] disc, int[] low, boolean[] onStack, Stack stack, List> sccs, int time) { + disc[u] = low[u] = time++; + stack.push(u); + onStack[u] = true; + for (int v : graph.get(u)) { + if (disc[v] == -1) { + dfs(graph, v, disc, low, onStack, stack, sccs, time); + low[u] = Math.min(low[u], low[v]); + } else if (onStack[v]) { + low[u] = Math.min(low[u], disc[v]); + } + } + if (low[u] == disc[u]) { + List scc = new ArrayList<>(); + int w; + do { + w = stack.pop(); + onStack[w] = false; + scc.add(w); + } while (w != u); + sccs.add(scc); + } + } +}`, + + cpp: `class Solution { +private: + int time; + vector disc, low; + vector onStack; + stack st; + vector> sccs; + void dfs(int u, vector>& graph) { + disc[u] = low[u] = time++; + st.push(u); + onStack[u] = true; + for (int v : graph[u]) { + if (disc[v] == -1) { + dfs(v, graph); + low[u] = min(low[u], low[v]); + } else if (onStack[v]) { + low[u] = min(low[u], disc[v]); + } + } + if (low[u] == disc[u]) { + vector scc; + int w; + do { + w = st.top(); + st.pop(); + onStack[w] = false; + scc.push_back(w); + } while (w != u); + sccs.push_back(scc); + } + } +public: + vector> tarjanSCC(vector>& graph) { + int n = graph.size(); + time = 0; + disc.assign(n, -1); + low.assign(n, -1); + onStack.assign(n, false); + for (int i = 0; i < n; i++) { + if (disc[i] == -1) { + dfs(i, graph); + } + } + return sccs; + } +};`, +}; + export const TarjansVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); + const [currentStepIndex, setCurrentStepIndex] = useState(0); - const steps: Step[] = useMemo(() => { + const { steps, stepLineNumbers } = useMemo(() => { const s: Step[] = []; const graph = GRAPH; const n = graph.length; @@ -64,11 +230,26 @@ export const TarjansVisualization = () => { const sccs: number[][] = []; let time = 0; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + const snap = ( currentNode: number, activeEdge: [number, number] | null, explanation: string, - highlightedLines: number[], + pseudoStep: string, + ts: number, py: number, java: number, cpp: number, variables: Record, phase: Step['phase'] ) => { @@ -82,39 +263,94 @@ export const TarjansVisualization = () => { currentNode, activeEdge, explanation, - highlightedLines, + pseudoStep, variables, phase, }); + addLines(ts, py, java, cpp); }; - snap(-1, null, 'Initialize: n, disc, low, onStack, stack, sccs, time = 0.', [2, 3, 4, 5, 6, 7, 8], { n }, 'init'); + snap( + -1, null, + 'Initialize discovery times, low values, onStack table, stack, sccs list, and global clock time = 0.', + 'SET disc = [-1...], low = [-1...], onStack = [false...], stack = [], sccs = [], time = 0', + 2, 2, 2, 25, + { n }, 'init' + ); function dfs(u: number) { disc[u] = low[u] = time++; - snap(u, null, `dfs(${u}): disc[${u}] = low[${u}] = ${disc[u]}, time → ${time}.`, [11], { [`disc[${u}]`]: disc[u], [`low[${u}]`]: low[u], time }, 'visit'); + snap( + u, null, + `dfs(${u}): Set discovery time and low-link value disc[${u}] = low[${u}] = ${disc[u]}. Increment clock time to ${time}.`, + `SET disc[${u}] = low[${u}] = time++ → ${disc[u]}`, + 10, 11, 16, 8, + { [`disc[${u}]`]: disc[u], [`low[${u}]`]: low[u], time }, 'visit' + ); stack.push(u); onStack[u] = true; - snap(u, null, `Push ${u} onto stack. onStack[${u}] = true.`, [12, 13], { stack: `[${stack.join(',')}]`, [`onStack[${u}]`]: true }, 'visit'); + snap( + u, null, + `Push node ${u} onto stack and mark as onStack[${u}] = true.`, + `CALL stack.push(${u}); SET onStack[${u}] = true`, + 11, 13, 17, 9, + { stack: `[${stack.join(',')}]`, [`onStack[${u}]`]: true }, 'visit' + ); for (const v of graph[u]) { - snap(u, [u, v], `Check neighbor v = ${v} of u = ${u}.`, [15], { u, v, [`disc[${v}]`]: disc[v] }, 'recurse'); + snap( + u, [u, v], + `Explore directed edge: ${u} → ${v}. Checking neighbor v = ${v}.`, + `FOR neighbor v = ${v} of u = ${u}`, + 13, 15, 19, 11, + { u, v, [`disc[${v}]`]: disc[v] }, 'recurse' + ); if (disc[v] === -1) { - snap(u, [u, v], `disc[${v}] === -1: recurse dfs(${v}).`, [16, 17], { u, v }, 'recurse'); + snap( + u, [u, v], + `Neighbor ${v} is unvisited (disc[${v}] == -1). Recursively call dfs(${v}).`, + `IF disc[${v}] == -1: CALL dfs(${v})`, + 14, 16, 20, 12, + { u, v }, 'recurse' + ); dfs(v); low[u] = Math.min(low[u], low[v]); - snap(u, [u, v], `Backtrack to ${u}: low[${u}] = min(low[${u}], low[${v}]) = ${low[u]}.`, [18], { [`low[${u}]`]: low[u], [`low[${v}]`]: low[v] }, 'backtrack'); + snap( + u, [u, v], + `Backtrack from ${v} to ${u}. Update low[${u}] = min(low[${u}], low[${v}]) = ${low[u]}.`, + `SET low[${u}] = MIN(low[${u}], low[${v}] (${low[v]})) → ${low[u]}`, + 16, 18, 22, 14, + { [`low[${u}]`]: low[u], [`low[${v}]`]: low[v] }, 'backtrack' + ); } else if (onStack[v]) { low[u] = Math.min(low[u], disc[v]); - snap(u, [u, v], `v = ${v} is on stack (back edge). low[${u}] = min(low[${u}], disc[${v}]) = ${low[u]}.`, [19, 20], { [`low[${u}]`]: low[u], [`disc[${v}]`]: disc[v] }, 'backedge'); + snap( + u, [u, v], + `Neighbor ${v} is on stack. Back edge found! Update low[${u}] = min(low[${u}], disc[${v}] (${disc[v]})) = ${low[u]}.`, + `ELSE IF onStack[${v}]: SET low[${u}] = MIN(low[${u}], disc[${v}] (${disc[v]})) → ${low[u]}`, + 17, 19, 24, 15, + { [`low[${u}]`]: low[u], [`disc[${v}]`]: disc[v] }, 'backedge' + ); } else { - snap(u, [u, v], `v = ${v} already visited and not on stack. Skip.`, [19], { v, onStack: false }, 'recurse'); + snap( + u, [u, v], + `Neighbor ${v} is already visited and not on the stack. Skip.`, + `ELSE: skip neighbor ${v}`, + 13, 15, 19, 11, + { v, onStack: false }, 'recurse' + ); } } - snap(u, null, `Check if low[${u}](${low[u]}) === disc[${u}](${disc[u]}): ${low[u] === disc[u] ? 'YES — root of SCC!' : 'NO — not root.'}`, [24], { [`low[${u}]`]: low[u], [`disc[${u}]`]: disc[u] }, 'scc'); + snap( + u, null, + `Finished exploring neighbors of ${u}. Check if low[${u}] (${low[u]}) == disc[${u}] (${disc[u]}).`, + `IF low[${u}] (${low[u]}) == disc[${u}] (${disc[u]})`, + 21, 21, 27, 19, + { [`low[${u}]`]: low[u], [`disc[${u}]`]: disc[u] }, 'scc' + ); if (low[u] === disc[u]) { const scc: number[] = []; @@ -123,244 +359,235 @@ export const TarjansVisualization = () => { w = stack.pop()!; onStack[w] = false; scc.push(w); - snap(u, null, `Pop ${w} from stack → add to SCC.`, [28, 29, 30], { w, stack: `[${stack.join(',')}]` }, 'scc'); + snap( + u, null, + `Pop ${w} from stack and add to current Strongly Connected Component.`, + `POP w from stack → w = ${w}; SET onStack[w] = false`, + 24, 23, 29, 21, + { w, stack: `[${stack.join(',')}]` }, 'scc' + ); } while (w !== u); sccs.push(scc); - snap(u, null, `SCC complete: [${scc.join(', ')}]. Total SCCs: ${sccs.length}.`, [32], { scc: `[${scc.join(',')}]`, totalSCCs: sccs.length }, 'scc'); + snap( + u, null, + `Strongly Connected Component complete: [${scc.join(', ')}].`, + `ADD scc [${scc.join(',')}] to sccs list`, + 29, 29, 33, 25, + { scc: `[${scc.join(',')}]`, totalSCCs: sccs.length }, 'scc' + ); } } - snap(-1, null, 'Outer loop: iterate over all vertices.', [36], { i: 0, n }, 'init'); + snap( + -1, null, + 'Outer loop: iterate over all vertices to find unvisited roots.', + 'FOR i = 0 to n-1', + 32, 30, 10, 27, + { i: 0, n }, 'init' + ); for (let i = 0; i < n; i++) { - snap(i, null, `i = ${i}: disc[${i}] === -1? ${disc[i] === -1 ? 'YES — call dfs(' + i + ').' : 'NO — skip.'}`, [36, 37], { i, [`disc[${i}]`]: disc[i] }, 'init'); + snap( + i, null, + `Check if vertex ${i} is visited: disc[${i}] === -1?`, + `IF disc[${i}] == -1 → ${disc[i] === -1 ? 'YES ✓' : 'NO ✗'}`, + 33, 31, 11, 28, + { i, [`disc[${i}]`]: disc[i] }, 'init' + ); if (disc[i] === -1) { dfs(i); } } - snap(-1, null, `Done! Returning ${sccs.length} SCCs: ${sccs.map(c => '[' + c.join(',') + ']').join(', ')}.`, [42], { sccs: sccs.length }, 'done'); + snap( + -1, null, + `Finished! Tarjan's algorithm successfully found ${sccs.length} SCCs.`, + `RETURN sccs → size ${sccs.length}`, + 37, 33, 15, 31, + { sccs: sccs.length }, 'done' + ); - return s; + return { steps: s, stepLineNumbers }; }, []); - const code = `function tarjanSCC(graph: number[][]): number[][] { - const n = graph.length; - const disc = Array(n).fill(-1); - const low = Array(n).fill(-1); - const onStack = Array(n).fill(false); - const stack: number[] = []; - const sccs: number[][] = []; - let time = 0; - - function dfs(u: number) { - disc[u] = low[u] = time++; - stack.push(u); - onStack[u] = true; - - for (const v of graph[u]) { - if (disc[v] === -1) { - dfs(v); - low[u] = Math.min(low[u], low[v]); - } else if (onStack[v]) { - low[u] = Math.min(low[u], disc[v]); - } - } - - if (low[u] === disc[u]) { - const scc: number[] = []; - let w: number; - do { - w = stack.pop()!; - onStack[w] = false; - scc.push(w); - } while (w !== u); - sccs.push(scc); - } - } - - for (let i = 0; i < n; i++) { - if (disc[i] === -1) { - dfs(i); - } - } - - return sccs; -}`; - - const step = steps[currentStep]; + const currentStep = steps[currentStepIndex] || steps[0]; + const pseudoSteps = steps.map(s => s.pseudoStep); const getNodeStyle = (idx: number) => { - const sccIdx = sccIndexFor(step.sccs, idx); + const sccIdx = sccIndexFor(currentStep.sccs, idx); if (sccIdx !== -1) return SCC_PALETTE[sccIdx % SCC_PALETTE.length]; - if (idx === step.currentNode) return { fill: '#f59e0b', stroke: '#d97706', text: '#000' }; - if (step.onStack[idx]) return { fill: '#8b5cf6', stroke: '#7c3aed', text: '#fff' }; - if (step.disc[idx] !== -1) return { fill: '#22c55e', stroke: '#16a34a', text: '#fff' }; + if (idx === currentStep.currentNode) return { fill: '#f59e0b', stroke: '#d97706', text: '#000' }; + if (currentStep.onStack[idx]) return { fill: '#8b5cf6', stroke: '#7c3aed', text: '#fff' }; + if (currentStep.disc[idx] !== -1) return { fill: '#22c55e', stroke: '#16a34a', text: '#fff' }; return { fill: '#1e293b', stroke: '#475569', text: '#94a3b8' }; }; const isActiveEdge = (u: number, v: number) => - step.activeEdge && step.activeEdge[0] === u && step.activeEdge[1] === v; + currentStep.activeEdge && currentStep.activeEdge[0] === u && currentStep.activeEdge[1] === v; return ( - -

- Graph — Tarjan's SCC -

- - - - - - - - - - - {GRAPH.flatMap((neighbors, u) => - neighbors.map(v => { - const from = NODE_POSITIONS[u]; - const to = NODE_POSITIONS[v]; - const angle = Math.atan2(to.y - from.y, to.x - from.x); - const r = 20; - const x1 = from.x + Math.cos(angle) * r; - const y1 = from.y + Math.sin(angle) * r; - const x2 = to.x - Math.cos(angle) * r; - const y2 = to.y - Math.sin(angle) * r; - const active = isActiveEdge(u, v); +
+
+ +

+ Graph — Tarjan's SCC +

+ + + + + + + + + + + {GRAPH.flatMap((neighbors, u) => + neighbors.map(v => { + const from = NODE_POSITIONS[u]; + const to = NODE_POSITIONS[v]; + const angle = Math.atan2(to.y - from.y, to.x - from.x); + const r = 20; + const x1 = from.x + Math.cos(angle) * r; + const y1 = from.y + Math.sin(angle) * r; + const x2 = to.x - Math.cos(angle) * r; + const y2 = to.y - Math.sin(angle) * r; + const active = isActiveEdge(u, v); + return ( + + ); + }) + )} + + {NODE_POSITIONS.map((pos, idx) => { + const style = getNodeStyle(idx); return ( - - ); - }) - )} - - {NODE_POSITIONS.map((pos, idx) => { - const style = getNodeStyle(idx); - return ( - - - - {idx} - - {step.disc[idx] !== -1 && ( - - d:{step.disc[idx]} l:{step.low[idx]} + + + + {idx} - )} - - ); - })} - - -
- - Unvisited - - - On Stack - - - Current - - - Visited - - - SCC - -
-
- -
- -

- Stack -

-
- - {step.stack.length > 0 - ? step.stack.map((n, i) => ( - - {n} - - )) - : empty} - + {currentStep.disc[idx] !== -1 && ( + + d:{currentStep.disc[idx]} l:{currentStep.low[idx]} + + )} + + ); + })} + + +
+ + Unvisited + + + On Stack + + + Current + + + Visited + + + SCC +
- -

- SCCs Found -

-
- - {step.sccs.length > 0 - ? step.sccs.map((scc, i) => ( - - [{scc.join(', ')}] - - )) - : none yet} - -
-
+
+ +

+ Stack +

+
+ + {currentStep.stack.length > 0 + ? currentStep.stack.map((n, i) => ( + + {n} + + )) + : empty} + +
+
+ + +

+ SCCs Found +

+
+ + {currentStep.sccs.length > 0 + ? currentStep.sccs.map((scc, i) => ( + + [{scc.join(', ')}] + + )) + : none yet} + +
+
+
- -
-

Step

-

{step.explanation}

- - - +
+ +

Step

+

{currentStep.explanation}

+
+ +
} rightContent={ - setCurrentStepIndex(0)} /> } controls={ } /> diff --git a/src/components/visualizations/algorithms/ThreeSumVisualization.tsx b/src/components/visualizations/algorithms/ThreeSumVisualization.tsx index 9ce5f38..7d6f3a2 100644 --- a/src/components/visualizations/algorithms/ThreeSumVisualization.tsx +++ b/src/components/visualizations/algorithms/ThreeSumVisualization.tsx @@ -1,23 +1,10 @@ -import { useState, useMemo } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; +import { useEffect, useState } from 'react'; +import { Card } from '@/components/ui/card'; import { VisualizationLayout } from '../shared/VisualizationLayout'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; -import { Card } from '@/components/ui/card'; -import { - Zap, - Target, - ArrowRight, - ArrowLeft, - Repeat, - CheckCircle2, - Search, - MoveRight, - Info, - Hash, - MoveLeft -} from 'lucide-react'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { array: number[]; @@ -27,48 +14,149 @@ interface Step { currentSum: number | '-'; target: number | '-'; result: number[][]; - message: string; - lineNumber: number; - phase: 'sorting' | 'loop' | 'check-dupe' | 'pointers-init' | 'calc-sum' | 'match' | 'move' | 'skip-dupe' | 'done'; + explanation: string; + pseudoStep: string; highlights: number[]; - insight?: string; - isMatch?: boolean; } +const languages: VisualizationLanguageMap = { + typescript: `function threeSum(nums: number[]): number[][] { + nums.sort((a, b) => a - b); + const result: number[][] = []; + for (let i = 0; i < nums.length - 2; i++) { + if (i > 0 && nums[i] === nums[i - 1]) { + continue; + } + let left = i + 1, right = nums.length - 1; + const target = -nums[i]; + while (left < right) { + const currentSum = nums[left] + nums[right]; + if (currentSum === target) { + result.push([nums[i], nums[left], nums[right]]); + while (left < right && nums[left] === nums[left + 1]) left++; + while (left < right && nums[right] === nums[right - 1]) right--; + left++; + right--; + } else if (currentSum < target) { + left++; + } else { + right--; + } + } + } + return result; +}`, + python: `def threeSum(nums: List[int]) -> List[List[int]]: + nums.sort() + result = [] + for i in range(len(nums) - 2): + if i > 0 and nums[i] == nums[i - 1]: + continue + left, right = i + 1, len(nums) - 1 + target = -nums[i] + while left < right: + current_sum = nums[left] + nums[right] + if current_sum == target: + result.append([nums[i], nums[left], nums[right]]) + while left < right and nums[left] == nums[left + 1]: + left += 1 + while left < right and nums[right] == nums[right - 1]: + right -= 1 + left += 1 + right -= 1 + elif current_sum < target: + left += 1 + else: + right -= 1 + return result`, + java: `public static class Solution { + public List> threeSum(int[] nums) { + Arrays.sort(nums); + List> result = new ArrayList<>(); + for (int i = 0; i < nums.length - 2; i++) { + if (i > 0 && nums[i] == nums[i - 1]) { + continue; + } + int left = i + 1, right = nums.length - 1; + int target = -nums[i]; + while (left < right) { + int currentSum = nums[left] + nums[right]; + if (currentSum == target) { + result.add(Arrays.asList(nums[i], nums[left], nums[right])); + while (left < right && nums[left] == nums[left + 1]) left++; + while (left < right && nums[right] == nums[right - 1]) right--; + left++; + right--; + } else if (currentSum < target) { + left++; + } else { + right--; + } + } + } + return result; + } +}`, + cpp: `class Solution { +public: + vector> threeSum(vector& nums) { + sort(nums.begin(), nums.end()); + vector> result; + for (int i = 0; i < nums.size() - 2; i++) { + if (i > 0 && nums[i] == nums[i - 1]) { + continue; + } + int left = i + 1, right = nums.size() - 1; + int target = -nums[i]; + while (left < right) { + int currentSum = nums[left] + nums[right]; + if (currentSum == target) { + result.push_back({nums[i], nums[left], nums[right]}); + while (left < right && nums[left] == nums[left + 1]) left++; + while (left < right && nums[right] == nums[right - 1]) right--; + left++; + right--; + } else if (currentSum < target) { + left++; + } else { + right--; + } + } + } + return result; + } +};` +}; + export const ThreeSumVisualization = () => { + const [steps, setSteps] = useState([]); + const [stepLineNumbers, setStepLineNumbers] = useState({ + typescript: [], + python: [], + java: [], + cpp: [] + }); const [currentStepIndex, setCurrentStepIndex] = useState(0); - const code = `function threeSum(nums: number[]): number[][] { - nums.sort((a, b) => a - b); - const result: number[][] = []; - - for (let i = 0; i < nums.length - 2; i++) { - if (i > 0 && nums[i] === nums[i - 1]) continue; - - let left = i + 1, right = nums.length - 1; - while (left < right) { - const sum = nums[i] + nums[left] + nums[right]; - if (sum === 0) { - result.push([nums[i], nums[left], nums[right]]); - while (left < right && nums[left] === nums[left + 1]) left++; - while (left < right && nums[right] === nums[right - 1]) right--; - left++; right--; - } else if (sum < 0) { - left++; - } else { - right--; - } - } - } - return result; -}`; - - const steps: Step[] = useMemo(() => { + useEffect(() => { const nums = [-1, 0, 1, 2, -1, -4]; - const s: Step[] = []; - - // Initial State - s.push({ + const generatedSteps: Step[] = []; + const stepLines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLines.typescript!.push(ts); + stepLines.python!.push(py); + stepLines.java!.push(java); + stepLines.cpp!.push(cpp); + }; + + // Initial state / Sort + generatedSteps.push({ array: [...nums], i: -1, left: -1, @@ -76,16 +164,17 @@ export const ThreeSumVisualization = () => { currentSum: '-', target: '-', result: [], - message: "The 3Sum problem asks for all unique triplets that sum to zero. Our first step is to sort the input array.", - lineNumber: 2, - phase: 'sorting', - highlights: [], - insight: "Sorting is crucial because it allows us to use the Two-Pointer approach and easily skip duplicate elements." + explanation: "Sort the input array to enable the two-pointer strategy and easily skip duplicates.", + pseudoStep: "SORT nums", + highlights: [] }); + addLines(2, 2, 3, 4); - // Sort nums.sort((a, b) => a - b); - s.push({ + const result: number[][] = []; + + // Result init + generatedSteps.push({ array: [...nums], i: -1, left: -1, @@ -93,191 +182,262 @@ export const ThreeSumVisualization = () => { currentSum: '-', target: '-', result: [], - message: `Array sorted: [${nums.join(', ')}]. Now we can use one fixed element (i) and search for the other two using pointers.`, - lineNumber: 3, - phase: 'sorting', + explanation: "Initialize an empty list to store the unique triplets that sum to 0.", + pseudoStep: "SET result = []", highlights: [] }); - - const result: number[][] = []; + addLines(3, 3, 4, 5); for (let i = 0; i < nums.length - 2; i++) { // Loop Check i - s.push({ + generatedSteps.push({ array: [...nums], i, left: -1, right: -1, currentSum: '-', - target: 0, + target: '-', result: [...result.map(r => [...r])], - message: `Outer loop iteration: i = ${i}, nums[i] = ${nums[i]}.`, - lineNumber: 5, - phase: 'loop', + explanation: `Outer loop: Fix first element at index i = ${i} (value = ${nums[i]}).`, + pseudoStep: `FOR i = ${i} to ${nums.length - 3}`, highlights: [i] }); + addLines(4, 4, 5, 6); if (i > 0 && nums[i] === nums[i - 1]) { - s.push({ + generatedSteps.push({ array: [...nums], i, left: -1, right: -1, currentSum: '-', - target: 0, + target: '-', result: [...result.map(r => [...r])], - message: `nums[${i}] (${nums[i]}) is the same as nums[${i - 1}]. Skip it to avoid duplicate triplets.`, - lineNumber: 6, - phase: 'check-dupe', - highlights: [i, i - 1], - insight: "If we use the same starting element again, we would find exactly the same triplets we already found." + explanation: `Duplicate starting element detected (nums[i] === nums[i - 1] === ${nums[i]}). Skip to avoid duplicates.`, + pseudoStep: `IF i > 0 AND nums[i] == nums[i - 1]`, + highlights: [i, i - 1] }); + addLines(5, 5, 6, 7); continue; } let left = i + 1; let right = nums.length - 1; + let targetVal = -nums[i]; - s.push({ + // Pointer Init + generatedSteps.push({ array: [...nums], i, left, right, currentSum: '-', - target: 0, + target: targetVal, result: [...result.map(r => [...r])], - message: `Initialize pointers: left = ${left} (after i), right = ${right} (end of array).`, - lineNumber: 8, - phase: 'pointers-init', + explanation: `Initialize two pointers: left = ${left} (index after i), right = ${right} (end of array).`, + pseudoStep: `SET left = i + 1, right = nums.length - 1`, highlights: [i, left, right] }); + addLines(8, 7, 9, 10); + + // Target Init + generatedSteps.push({ + array: [...nums], + i, + left, + right, + currentSum: '-', + target: targetVal, + result: [...result.map(r => [...r])], + explanation: `Target sum for the two pointers is negative of the fixed element: target = -(${nums[i]}) = ${targetVal}.`, + pseudoStep: `SET target = -nums[i]`, + highlights: [i] + }); + addLines(9, 8, 10, 11); while (left < right) { - const sum = nums[i] + nums[left] + nums[right]; + // Inner Loop Check + generatedSteps.push({ + array: [...nums], + i, + left, + right, + currentSum: '-', + target: targetVal, + result: [...result.map(r => [...r])], + explanation: `Inner loop check: left (${left}) < right (${right}). Continue searching.`, + pseudoStep: "WHILE left < right", + highlights: [left, right] + }); + addLines(10, 9, 11, 12); - s.push({ + const currentSum = nums[left] + nums[right]; + + // Sum Calculation + generatedSteps.push({ array: [...nums], i, left, right, - currentSum: sum, - target: 0, + currentSum, + target: targetVal, result: [...result.map(r => [...r])], - message: `Calculating sum: ${nums[i]} + ${nums[left]} + ${nums[right]} = ${sum}.`, - lineNumber: 10, - phase: 'calc-sum', - highlights: [i, left, right] + explanation: `Calculate sum of the elements at left and right pointers: nums[left] (${nums[left]}) + nums[right] (${nums[right]}) = ${currentSum}.`, + pseudoStep: "SET currentSum = nums[left] + nums[right]", + highlights: [left, right] }); + addLines(11, 10, 12, 13); - if (sum === 0) { + // Compare Sum with Target + generatedSteps.push({ + array: [...nums], + i, + left, + right, + currentSum, + target: targetVal, + result: [...result.map(r => [...r])], + explanation: `Compare currentSum (${currentSum}) with target (${targetVal}).`, + pseudoStep: "IF currentSum == target", + highlights: [left, right] + }); + addLines(12, 11, 13, 14); + + if (currentSum === targetVal) { result.push([nums[i], nums[left], nums[right]]); - - s.push({ + + generatedSteps.push({ array: [...nums], i, left, right, - currentSum: sum, - target: 0, + currentSum, + target: targetVal, result: [...result.map(r => [...r])], - message: `Match found! [${nums[i]}, ${nums[left]}, ${nums[right]}] sums to 0. Added to result.`, - lineNumber: 12, - phase: 'match', - highlights: [i, left, right], - isMatch: true, - insight: "One triplet found. Now we need to skip any identical values for 'left' and 'right' to maintain uniqueness." + explanation: `Found triplet: [${nums[i]}, ${nums[left]}, ${nums[right]}]. Add to results.`, + pseudoStep: `result.push([nums[i], nums[left], nums[right]])`, + highlights: [i, left, right] }); + addLines(13, 12, 14, 15); - // Skip duplicate lefts + // Skip Duplicate Left if (left < right && nums[left] === nums[left + 1]) { - s.push({ + generatedSteps.push({ array: [...nums], i, left, right, - currentSum: sum, - target: 0, + currentSum, + target: targetVal, result: [...result.map(r => [...r])], - message: `nums[${left}] is ${nums[left]}, and the next is also ${nums[left + 1]}. Skipping...`, - lineNumber: 13, - phase: 'skip-dupe', + explanation: `Duplicate left value detected: nums[left] === nums[left+1] === ${nums[left]}. Skip duplicate lefts.`, + pseudoStep: "WHILE left < right AND nums[left] == nums[left + 1]", highlights: [left, left + 1] }); + addLines(14, 13, 15, 16); while (left < right && nums[left] === nums[left + 1]) left++; } - // Skip duplicate rights + // Skip Duplicate Right if (left < right && nums[right] === nums[right - 1]) { - s.push({ + generatedSteps.push({ array: [...nums], i, left, right, - currentSum: sum, - target: 0, + currentSum, + target: targetVal, result: [...result.map(r => [...r])], - message: `nums[${right}] is ${nums[right]}, and the previous was also ${nums[right - 1]}. Skipping...`, - lineNumber: 14, - phase: 'skip-dupe', + explanation: `Duplicate right value detected: nums[right] === nums[right-1] === ${nums[right]}. Skip duplicate rights.`, + pseudoStep: "WHILE left < right AND nums[right] == nums[right - 1]", highlights: [right, right - 1] }); + addLines(15, 15, 16, 17); while (left < right && nums[right] === nums[right - 1]) right--; } left++; right--; - - s.push({ + generatedSteps.push({ array: [...nums], i, left, right, currentSum: '-', - target: 0, + target: targetVal, result: [...result.map(r => [...r])], - message: `Advanced both pointers to search for more pairs.`, - lineNumber: 15, - phase: 'move', - highlights: [i, left, right] + explanation: `Move left and right pointers inward to find new unique pairs.`, + pseudoStep: "SET left = left + 1, right = right - 1", + highlights: [left, right] }); + addLines(16, 17, 17, 18); - } else if (sum < 0) { - s.push({ + } else if (currentSum < targetVal) { + generatedSteps.push({ array: [...nums], i, left, right, - currentSum: sum, - target: 0, + currentSum, + target: targetVal, result: [...result.map(r => [...r])], - message: `Sum (${sum}) is less than zero. Since the array is sorted, we need a larger value. Move 'left' inward.`, - lineNumber: 17, - phase: 'move', - highlights: [i, left, right], - insight: "Increasing 'left' increases the total sum in a sorted array." + explanation: `Since currentSum (${currentSum}) < target (${targetVal}), we need a larger sum. Move left pointer right.`, + pseudoStep: "ELSE IF currentSum < target", + highlights: [left] }); + addLines(18, 19, 19, 20); + left++; + generatedSteps.push({ + array: [...nums], + i, + left, + right, + currentSum: '-', + target: targetVal, + result: [...result.map(r => [...r])], + explanation: `Increment left to ${left}.`, + pseudoStep: "SET left = left + 1", + highlights: [left] + }); + addLines(19, 20, 20, 21); + } else { - s.push({ + // currentSum > target + generatedSteps.push({ array: [...nums], i, left, right, - currentSum: sum, - target: 0, + currentSum, + target: targetVal, result: [...result.map(r => [...r])], - message: `Sum (${sum}) is greater than zero. Since the array is sorted, we need a smaller value. Move 'right' inward.`, - lineNumber: 19, - phase: 'move', - highlights: [i, left, right], - insight: "Decreasing 'right' decreases the total sum in a sorted array." + explanation: `Since currentSum (${currentSum}) > target (${targetVal}), we need a smaller sum. Move right pointer left.`, + pseudoStep: "ELSE right--", + highlights: [right] }); + addLines(18, 19, 19, 20); + right--; + generatedSteps.push({ + array: [...nums], + i, + left, + right, + currentSum: '-', + target: targetVal, + result: [...result.map(r => [...r])], + explanation: `Decrement right to ${right}.`, + pseudoStep: "SET right = right - 1", + highlights: [right] + }); + addLines(21, 22, 22, 23); } } } - s.push({ + // Done + generatedSteps.push({ array: [...nums], i: -1, left: -1, @@ -285,221 +445,114 @@ export const ThreeSumVisualization = () => { currentSum: '-', target: '-', result: [...result.map(r => [...r])], - message: `Execution finished. Found ${result.length} unique triplets.`, - lineNumber: 23, - phase: 'done', - highlights: [], - insight: "The Two-Pointer approach allowed us to solve this in O(n²) time complexity." + explanation: `Algorithm finished. Unique triplets that sum to 0: [${result.map(r => `[${r.join(', ')}]`).join(', ')}].`, + pseudoStep: "RETURN result", + highlights: [] }); + addLines(25, 23, 26, 27); - return s; + setSteps(generatedSteps); + setStepLineNumbers(stepLines); }, []); - const currentStep = steps[currentStepIndex] || steps[0]; + if (steps.length === 0) return null; + + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( + } leftContent={ -
- - {/* Header Info */} -
-

- - Three-Pointers Search +
+
+ +

+ 3Sum Triplet Finder

-
-
- - Phase: {currentStep.phase.replace('-', ' ')} -
-
-
- - {/* Pointer Visualization */} -
-
-
- - {currentStep.array.map((value, index) => { - const isI = index === currentStep.i; - const isL = index === currentStep.left; - const isR = index === currentStep.right; - const isHighlighted = currentStep.highlights.includes(index); - - return ( -
- {/* Top Pointer Labels */} -
- - {isI && ( - - i -
- - )} - {isL && ( - - L -
- - )} - {isR && ( - - R -
- - )} - -
- - {/* Value Box */} - - {value} - - -
- IDX {index} -
+ +
+
+
Sorted Array
+
+ {currentStep.array.map((value, index) => { + const isI = index === currentStep.i; + const isL = index === currentStep.left; + const isR = index === currentStep.right; + + return ( +
+
+ {isI && i} + {isL && L} + {isR && R} +
+
+ {value}
- ); - })} - + [{index}] +
+ ); + })}
-
-
- - {/* Stats Grid */} -
-
- currentSum - {currentStep.currentSum} -
-
- - target - - {currentStep.target} -
-
- triplets - {currentStep.result.length} -
-
- +
- -
-
- {currentStep.isMatch ? : } -
-
-

- Step Logic -

-

- {currentStep.message} -

+ {currentStep.result.length > 0 && ( +
+
Unique Triplets Found
+
+ {currentStep.result.map((triplet, index) => ( +
+ [{triplet.join(', ')}] +
+ ))} +
+
+ )}
-
-
-
- } - rightContent={ -
-
- +
- -

- - Computational Insight -

- -
- {currentStep.insight ? ( -
-
- -
-

- {currentStep.insight} -

-
- ) : ( -
-
- -
-

- Searching for matches... -

-
- )} +
+ +
+

Step Explanation

+

{currentStep.explanation}

+ -
-
- fixed (i) - {currentStep.i === -1 ? 'N/A' : currentStep.array[currentStep.i]} -
-
- left (L) - {currentStep.left === -1 ? 'N/A' : currentStep.array[currentStep.left]} -
-
- right (R) - {currentStep.right === -1 ? 'N/A' : currentStep.array[currentStep.right]} -
-
- - results - - {currentStep.result.length} -
-
-
-
+ +
} - controls={ - setCurrentStepIndex(0)} /> } /> ); }; +; diff --git a/src/components/visualizations/algorithms/TimeMapVisualization.tsx b/src/components/visualizations/algorithms/TimeMapVisualization.tsx index f0f1aee..443664f 100644 --- a/src/components/visualizations/algorithms/TimeMapVisualization.tsx +++ b/src/components/visualizations/algorithms/TimeMapVisualization.tsx @@ -279,10 +279,10 @@ export const TimeMapVisualization = () => {
{/* Pointer indicators */} -
- {isLeft && L} - {isRight && R} - {isMid && M} +
+ {isLeft && Left} + {isMid && Mid} + {isRight && Right}
); diff --git a/src/components/visualizations/algorithms/TopologicalSortVisualization.tsx b/src/components/visualizations/algorithms/TopologicalSortVisualization.tsx index ea0dd32..5b9ab59 100644 --- a/src/components/visualizations/algorithms/TopologicalSortVisualization.tsx +++ b/src/components/visualizations/algorithms/TopologicalSortVisualization.tsx @@ -1,80 +1,182 @@ -import React, { useEffect, useState, useMemo } from 'react'; - -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; -import { SimpleStepControls } from '../shared/SimpleStepControls'; -import { VariablePanel } from '../shared/VariablePanel'; -import { VisualizationLayout } from '../shared/VisualizationLayout'; -import { Card } from '@/components/ui/card'; -import { motion } from 'framer-motion'; +import { useEffect, useState, useMemo } from "react"; +import { Card } from "@/components/ui/card"; +import { VariablePanel } from "../shared/VariablePanel"; +import { VisualizationLayout } from "../shared/VisualizationLayout"; +import { VisualizationCodePanel } from "../shared/VisualizationCodePanel"; +import { SimpleStepControls } from "../shared/SimpleStepControls"; +import { motion } from "framer-motion"; +import type { StepLineNumberMap, VisualizationLanguageMap } from "@/types/visualization"; interface Step { currentNode: number | null; inDegree: number[]; queue: number[]; result: number[]; - message: string; - lineNumber: number; + explanation: string; + pseudoStep: string; + variables: Record; + activeNode: number | null; } -export const TopologicalSortVisualization: React.FC = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - - - const code = `function topologicalSort(graph: Map): number[] { +const languages: VisualizationLanguageMap = { + typescript: `function topologicalSort(graph: Map): number[] { const indegrees: number[] = new Array(graph.size).fill(0); - for (const node of graph.keys()) { for (const neighbor of graph.get(node) || []) { indegrees[neighbor]++; } } - const queue: number[] = []; - for (let i = 0; i < graph.size; i++) { if (indegrees[i] === 0) { queue.push(i); } } - const result: number[] = []; - while (queue.length > 0) { const node: number = queue.shift()!; result.push(node); - for (const neighbor of graph.get(node) || []) { indegrees[neighbor]--; - if (indegrees[neighbor] === 0) { queue.push(neighbor); } } } - if (result.length !== graph.size) { return []; } - return result; -}`; +}`, + python: `from collections import deque +def topological_sort(graph: dict[int, list[int]]) -> list[int]: + indegrees = [0] * len(graph) + for node in graph: + for neighbor in graph[node]: + indegrees[neighbor] += 1 + queue = deque([node for node in graph if indegrees[node] == 0]) + result = [] + while queue: + node = queue.popleft() + result.append(node) + for neighbor in graph[node]: + indegrees[neighbor] -= 1 + if indegrees[neighbor] == 0: + queue.append(neighbor) + if len(result) != len(graph): + return [] + return result`, + java: `import java.util.*; +public static class Solution { + public int[] topologicalSort(Map> graph) { + int numNodes = graph.size(); + int[] indegrees = new int[numNodes]; + for (int node : graph.keySet()) { + for (int neighbor : graph.get(node) != null ? graph.get(node) : new ArrayList<>()) { + indegrees[neighbor]++; + } + } + Queue queue = new LinkedList<>(); + for (int i = 0; i < numNodes; i++) { + if (indegrees[i] == 0) { + queue.offer(i); + } + } + int[] result = new int[numNodes]; + int index = 0; + while (!queue.isEmpty()) { + int node = queue.poll(); + result[index++] = node; + if (graph.containsKey(node)) { + for (int neighbor : graph.get(node)) { + indegrees[neighbor]--; + if (indegrees[neighbor] == 0) { + queue.offer(neighbor); + } + } + } + } + if (index != numNodes) { + return new int[0]; + } + return result; + } +}`, + cpp: `#include +#include +using namespace std; +class Solution { +public: + vector topologicalSort(int numCourses, vector>& prerequisites) { + vector> graph(numCourses); + vector inDegree(numCourses, 0); + for (auto& p : prerequisites) { + graph[p[1]].push_back(p[0]); + inDegree[p[0]]++; + } + queue q; + for (int i = 0; i < numCourses; i++) { + if (inDegree[i] == 0) q.push(i); + } + vector result; + while (!q.empty()) { + int node = q.front(); + q.pop(); + result.push_back(node); + for (int neighbor : graph[node]) { + if (--inDegree[neighbor] == 0) { + q.push(neighbor); + } + } + } + return result.size() == numCourses ? result : vector(); + } +};`, +}; - const generateSteps = () => { - const graphValues: Record = { +export const TopologicalSortVisualization: React.FC = () => { + const [steps, setSteps] = useState([]); + const [stepLineNumbers, setStepLineNumbers] = useState({ + typescript: [], + python: [], + java: [], + cpp: [], + }); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + + const graphValues: Record = useMemo( + () => ({ 0: [2, 3], 1: [3, 4], 2: [3], 3: [4], - 4: [] - }; - const numNodes = 5; + 4: [], + }), + [] + ); + + const numNodes = 5; + + useEffect(() => { const graph = new Map(); for (const key in graphValues) { graph.set(Number(key), graphValues[key]); } const newSteps: Step[] = []; + const stepLines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [], + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLines.typescript!.push(ts); + stepLines.python!.push(py); + stepLines.java!.push(java); + stepLines.cpp!.push(cpp); + }; const indegrees: number[] = new Array(numNodes).fill(0); newSteps.push({ @@ -82,9 +184,16 @@ export const TopologicalSortVisualization: React.FC = () => { inDegree: [...indegrees], queue: [], result: [], - message: "Initialize in-degrees to 0", - lineNumber: 2 + explanation: "Initialize in-degrees to 0 for all nodes.", + pseudoStep: "SET indegrees = [0, 0, 0, 0, 0]", + variables: { + currentNode: "null", + queue: "[]", + result: "[]", + }, + activeNode: null, }); + addLines(2, 3, 5, 8); for (const [node, neighbors] of graph.entries()) { for (const neighbor of neighbors) { @@ -97,9 +206,16 @@ export const TopologicalSortVisualization: React.FC = () => { inDegree: [...indegrees], queue: [], result: [], - message: `Calculated in-degrees: [${indegrees.join(', ')}]`, - lineNumber: 4 + explanation: `Calculate incoming degrees (in-degrees) for all nodes by traversing edges: [${indegrees.join(", ")}].`, + pseudoStep: "CALC indegrees for all nodes", + variables: { + currentNode: "null", + queue: "[]", + result: "[]", + }, + activeNode: null, }); + addLines(3, 4, 6, 9); const queue: number[] = []; newSteps.push({ @@ -107,9 +223,16 @@ export const TopologicalSortVisualization: React.FC = () => { inDegree: [...indegrees], queue: [...queue], result: [], - message: "Initialize empty queue", - lineNumber: 10 + explanation: "Initialize an empty queue to store nodes with an in-degree of 0.", + pseudoStep: "SET queue = []", + variables: { + currentNode: "null", + queue: "[]", + result: "[]", + }, + activeNode: null, }); + addLines(8, 7, 11, 13); for (let i = 0; i < numNodes; i++) { if (indegrees[i] === 0) { @@ -119,9 +242,16 @@ export const TopologicalSortVisualization: React.FC = () => { inDegree: [...indegrees], queue: [...queue], result: [], - message: `Node ${i} has in-degree 0, adding to queue`, - lineNumber: 14 + explanation: `Node ${i} has in-degree 0 (no dependencies), add it to the queue.`, + pseudoStep: `CALL queue.push(${i}) → indegrees[${i}] == 0`, + variables: { + currentNode: "null", + queue: `[${queue.join(", ")}]`, + result: "[]", + }, + activeNode: i, }); + addLines(11, 7, 14, 15); } } @@ -131,9 +261,16 @@ export const TopologicalSortVisualization: React.FC = () => { inDegree: [...indegrees], queue: [...queue], result: [...result], - message: "Initialize empty result list", - lineNumber: 18 + explanation: "Initialize an empty list to record the topological ordering.", + pseudoStep: "SET result = []", + variables: { + currentNode: "null", + queue: `[${queue.join(", ")}]`, + result: "[]", + }, + activeNode: null, }); + addLines(14, 8, 17, 17); while (queue.length > 0) { const node = queue.shift()!; @@ -142,9 +279,16 @@ export const TopologicalSortVisualization: React.FC = () => { inDegree: [...indegrees], queue: [...queue], result: [...result], - message: `Dequeued node ${node}`, - lineNumber: 21 + explanation: `Remove node ${node} from the front of the queue to process it next.`, + pseudoStep: `SET node = queue.pop() → ${node}`, + variables: { + currentNode: node, + queue: `[${queue.join(", ")}]`, + result: `[${result.join(", ")}]`, + }, + activeNode: node, }); + addLines(16, 10, 20, 19); result.push(node); newSteps.push({ @@ -152,9 +296,16 @@ export const TopologicalSortVisualization: React.FC = () => { inDegree: [...indegrees], queue: [...queue], result: [...result], - message: `Added node ${node} to result`, - lineNumber: 22 + explanation: `Add node ${node} to the topological sort result.`, + pseudoStep: `CALL result.push(${node})`, + variables: { + currentNode: node, + queue: `[${queue.join(", ")}]`, + result: `[${result.join(", ")}]`, + }, + activeNode: node, }); + addLines(17, 11, 21, 21); for (const neighbor of graph.get(node) || []) { indegrees[neighbor]--; @@ -163,9 +314,16 @@ export const TopologicalSortVisualization: React.FC = () => { inDegree: [...indegrees], queue: [...queue], result: [...result], - message: `Decremented in-degree of neighbor ${neighbor} to ${indegrees[neighbor]}`, - lineNumber: 25 + explanation: `Decrement in-degree of neighbor node ${neighbor} to ${indegrees[neighbor]} since node ${node} is processed.`, + pseudoStep: `SET indegrees[${neighbor}] = ${indegrees[neighbor]}`, + variables: { + currentNode: node, + queue: `[${queue.join(", ")}]`, + result: `[${result.join(", ")}]`, + }, + activeNode: neighbor, }); + addLines(19, 13, 24, 23); if (indegrees[neighbor] === 0) { queue.push(neighbor); @@ -174,9 +332,16 @@ export const TopologicalSortVisualization: React.FC = () => { inDegree: [...indegrees], queue: [...queue], result: [...result], - message: `Neighbor ${neighbor} in-degree reached 0, adding to queue`, - lineNumber: 28 + explanation: `Neighbor ${neighbor} now has in-degree 0 (all dependencies resolved), add it to the queue.`, + pseudoStep: `CALL queue.push(${neighbor})`, + variables: { + currentNode: node, + queue: `[${queue.join(", ")}]`, + result: `[${result.join(", ")}]`, + }, + activeNode: neighbor, }); + addLines(21, 15, 26, 24); } } } @@ -186,92 +351,109 @@ export const TopologicalSortVisualization: React.FC = () => { inDegree: [...indegrees], queue: [...queue], result: [...result], - message: "Final topological order computed", - lineNumber: 37 + explanation: `Topological ordering computed successfully: [${result.join(" → ")}].`, + pseudoStep: "RETURN result", + variables: { + currentNode: "null", + queue: "[]", + result: `[${result.join(", ")}]`, + }, + activeNode: null, }); + addLines(28, 18, 34, 28); setSteps(newSteps); - }; + setStepLineNumbers(stepLines); + }, [graphValues]); - useEffect(() => { - generateSteps(); - }, []); + if (steps.length === 0) return null; - const currentStep = steps[currentStepIndex]; - if (!currentStep) return null; + const currentStep = steps[currentStepIndex] || steps[0]; + const pseudoSteps = steps.map((s) => s.pseudoStep); return ( + } leftContent={ -
- -

- Topological Sort Visualization -

+
+
+ +

+ Node States & In-degrees +

+ +
+ {currentStep.inDegree.map((degree, idx) => { + const isActive = currentStep.activeNode === idx; + const isProcessed = currentStep.result.includes(idx); + const isQueued = currentStep.queue.includes(idx); + + let nodeColorClass = "bg-card border-border text-muted-foreground"; + if (isActive) { + nodeColorClass = "bg-primary/20 border-primary text-primary font-bold"; + } else if (isProcessed) { + nodeColorClass = "bg-green-500/20 border-green-500 text-green-500 font-bold"; + } else if (isQueued) { + nodeColorClass = "bg-blue-500/20 border-blue-500 text-blue-500 font-bold"; + } + + return ( +
+ + {idx} + +
+ In-degree: {degree} +
+
+ ); + })} +
-
- {currentStep.inDegree.map((degree, idx) => ( -
- - {idx} - -
- IN:{degree} -
+
+
+ Queue: + [{currentStep.queue.join(", ")}] +
+
+ Result (Order): + [{currentStep.result.join(" → ")}]
- ))} -
- -
-
- Queue: - [{currentStep.queue.join(', ')}] -
-
- Result: - [{currentStep.result.join(' → ')}]
-
- - - -
-

Step Explanation

-

{currentStep.message}

- + +
+ +
+ +
+

Step Explanation

+

{currentStep.explanation}

+ + +
} rightContent={ -
- - -
- } - controls={ - setCurrentStepIndex(0)} /> } /> diff --git a/src/components/visualizations/algorithms/TrieVisualization.tsx b/src/components/visualizations/algorithms/TrieVisualization.tsx index 39d478c..4e7165b 100644 --- a/src/components/visualizations/algorithms/TrieVisualization.tsx +++ b/src/components/visualizations/algorithms/TrieVisualization.tsx @@ -1,254 +1,729 @@ -import React, { useEffect, useRef, useState } from 'react'; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import { useEffect, useRef, useState } from 'react'; import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; - -interface TrieNode { - char: string; - children: Map; - isEnd: boolean; - x?: number; - y?: number; -} +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { - currentNode: string[]; - operation: 'insert' | 'search'; + activeNodeId: string | null; + activePath: string[]; + insertedNodes: string[]; + endOfWordNodes: string[]; + operation: 'insert' | 'search' | 'startsWith'; word: string; currentIndex: number; found: boolean | null; - message: string; - lineNumber: number; + explanation: string; + pseudoStep: string; + variables: Record; } -export const TrieVisualization: React.FC = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1000); - const intervalRef = useRef(null); - - const code = `class TrieNode { - constructor() { - this.children = {}; - this.isEndOfWord = false; - } +const languages: VisualizationLanguageMap = { + typescript: `class TrieNode { + children = new Map(); + endOfWord = false; } - class Trie { - constructor() { - this.root = new TrieNode(); - } - - insert(word) { - let node = this.root; - for (let char of word) { - if (!node.children[char]) { - node.children[char] = new TrieNode(); - } - node = node.children[char]; + root = new TrieNode(); + insert(word: string): void { + let cur = this.root; + for (const c of word) { + if (!cur.children.has(c)) { + cur.children.set(c, new TrieNode()); + } + cur = cur.children.get(c)!; + } + cur.endOfWord = true; } - node.isEndOfWord = true; - } - - search(word) { - let node = this.root; - for (let char of word) { - if (!node.children[char]) { - return false; - } - node = node.children[char]; + search(word: string): boolean { + let cur = this.root; + for (const c of word) { + if (!cur.children.has(c)) { + return false; + } + cur = cur.children.get(c)!; + } + return cur.endOfWord; + } + startsWith(prefix: string): boolean { + let cur = this.root; + for (const c of prefix) { + if (!cur.children.has(c)) { + return false; + } + cur = cur.children.get(c)!; + } + return true; } - return node.isEndOfWord; +}`, + python: `class TrieNode: + def __init__(self): + self.children = {} + self.endOfWord = False +class Trie: + def __init__(self): + self.root = TrieNode() + def insert(self, word: str) -> None: + cur = self.root + for c in word: + if c not in cur.children: + cur.children[c] = TrieNode() + cur = cur.children[c] + cur.endOfWord = True + def search(self, word: str) -> bool: + cur = self.root + for c in word: + if c not in cur.children: + return False + cur = cur.children[c] + return cur.endOfWord + def startsWith(self, prefix: str) -> bool: + cur = self.root + for c in prefix: + if c not in cur.children: + return False + cur = cur.children[c] + return True`, + java: `class TrieNode { + Map children = new HashMap<>(); + boolean isEnd = false; +} +class Trie { + private TrieNode root = new TrieNode(); + public void insert(String word) { + TrieNode node = root; + for (char c : word.toCharArray()) { + if (!node.children.containsKey(c)) { + node.children.put(c, new TrieNode()); + } + node = node.children.get(c); + } + node.isEnd = true; + } + public boolean search(String word) { + TrieNode node = root; + for (char c : word.toCharArray()) { + if (!node.children.containsKey(c)) return false; + node = node.children.get(c); + } + return node.isEnd; + } + public boolean startsWith(String prefix) { + TrieNode node = root; + for (char c : prefix.toCharArray()) { + if (!node.children.containsKey(c)) return false; + node = node.children.get(c); + } + return true; + } +}`, + cpp: `class TrieNode { +public: + unordered_map children; + bool isEnd = false; +}; +class Trie { + TrieNode* root; +public: + Trie() { root = new TrieNode(); } + void insert(string word) { + TrieNode* node = root; + for (char c : word) { + if (!node->children[c]) { + node->children[c] = new TrieNode(); + } + node = node->children[c]; + } + node->isEnd = true; + } + bool search(string word) { + TrieNode* node = root; + for (char c : word) { + if (!node->children[c]) return false; + node = node->children[c]; + } + return node->isEnd; + } + bool startsWith(string prefix) { + TrieNode* node = root; + for (char c : prefix) { + if (!node->children[c]) return false; + node = node->children[c]; + } + return true; + } +};`, +}; + +function generateVisualizationData() { + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { typescript: [], python: [], java: [], cpp: [] }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + steps.push({ + activeNodeId: 'root', + activePath: ['root'], + insertedNodes: ['root'], + endOfWordNodes: [], + operation: 'insert', + word: '', + currentIndex: -1, + found: null, + explanation: 'Initialize the Trie with a root node.', + pseudoStep: 'SET root = new TrieNode()', + variables: { cur: 'root', c: '-', word: '-' } + }); + addLines(6, 7, 7, 8); + + const inserted: string[] = ['root']; + const endOfWord: string[] = []; + + // --- INSERT "cat" --- + const word1 = "cat"; + steps.push({ + activeNodeId: 'root', + activePath: ['root'], + insertedNodes: [...inserted], + endOfWordNodes: [...endOfWord], + operation: 'insert', + word: word1, + currentIndex: -1, + found: null, + explanation: `Start inserting "${word1}". Set pointer 'cur' to root.`, + pseudoStep: `CALL insert("${word1}"): cur = root`, + variables: { cur: 'root', c: '-', word: word1 } + }); + addLines(7, 8, 8, 10); + + let path = ""; + for (let i = 0; i < word1.length; i++) { + const c = word1[i]; + const prevPath = path; + path += c; + inserted.push(path); + + const currentPathArray = ['root', ...Array.from(path).map((_, idx) => path.substring(0, idx + 1))]; + + steps.push({ + activeNodeId: path, + activePath: [...currentPathArray], + insertedNodes: [...inserted], + endOfWordNodes: [...endOfWord], + operation: 'insert', + word: word1, + currentIndex: i, + found: null, + explanation: `Character '${c}' is not a child of current node. Create new TrieNode for '${c}'.`, + pseudoStep: `IF '${c}' NOT IN cur.children → Create node and move to it`, + variables: { cur: prevPath || 'root', c, word: word1 } + }); + addLines(9, 10, 10, 12); + + steps.push({ + activeNodeId: path, + activePath: [...currentPathArray], + insertedNodes: [...inserted], + endOfWordNodes: [...endOfWord], + operation: 'insert', + word: word1, + currentIndex: i, + found: null, + explanation: `Move pointer 'cur' to the child node corresponding to '${c}'.`, + pseudoStep: `SET cur = cur.children['${c}']`, + variables: { cur: path, c, word: word1 } + }); + addLines(12, 12, 13, 14); } -}`; - - const generateSteps = () => { - const newSteps: Step[] = []; - const words = ['cat', 'car', 'card']; - - // Insert operations - words.forEach(word => { - const path: string[] = []; - for (let i = 0; i < word.length; i++) { - path.push(word[i]); - newSteps.push({ - currentNode: [...path], - operation: 'insert', - word, - currentIndex: i, - found: null, - message: `Inserting "${word}": Adding character '${word[i]}'`, - lineNumber: 13 - }); - } - newSteps.push({ - currentNode: [...path], - operation: 'insert', - word, - currentIndex: word.length, - found: null, - message: `Inserted "${word}": Marked as end of word`, - lineNumber: 16 - }); + endOfWord.push(word1); + steps.push({ + activeNodeId: word1, + activePath: ['root', 'c', 'ca', 'cat'], + insertedNodes: [...inserted], + endOfWordNodes: [...endOfWord], + operation: 'insert', + word: word1, + currentIndex: word1.length, + found: null, + explanation: `Finished inserting "${word1}". Mark current node as the end of a word.`, + pseudoStep: `SET cur.endOfWord = True`, + variables: { cur: word1, c: '-', word: word1 } + }); + addLines(14, 13, 15, 16); + + // --- INSERT "car" --- + const word2 = "car"; + steps.push({ + activeNodeId: 'root', + activePath: ['root'], + insertedNodes: [...inserted], + endOfWordNodes: [...endOfWord], + operation: 'insert', + word: word2, + currentIndex: -1, + found: null, + explanation: `Start inserting "${word2}". Set pointer 'cur' to root.`, + pseudoStep: `CALL insert("${word2}"): cur = root`, + variables: { cur: 'root', c: '-', word: word2 } + }); + addLines(7, 8, 8, 10); + + steps.push({ + activeNodeId: 'c', + activePath: ['root', 'c'], + insertedNodes: [...inserted], + endOfWordNodes: [...endOfWord], + operation: 'insert', + word: word2, + currentIndex: 0, + found: null, + explanation: `Character 'c' already exists in children map. Traverse into it.`, + pseudoStep: `IF 'c' IN cur.children → Move to it`, + variables: { cur: 'c', c: 'c', word: word2 } + }); + addLines(12, 12, 13, 14); + + steps.push({ + activeNodeId: 'ca', + activePath: ['root', 'c', 'ca'], + insertedNodes: [...inserted], + endOfWordNodes: [...endOfWord], + operation: 'insert', + word: word2, + currentIndex: 1, + found: null, + explanation: `Character 'a' already exists in children map. Traverse into it.`, + pseudoStep: `IF 'a' IN cur.children → Move to it`, + variables: { cur: 'ca', c: 'a', word: word2 } + }); + addLines(12, 12, 13, 14); + + inserted.push("car"); + steps.push({ + activeNodeId: 'car', + activePath: ['root', 'c', 'ca', 'car'], + insertedNodes: [...inserted], + endOfWordNodes: [...endOfWord], + operation: 'insert', + word: word2, + currentIndex: 2, + found: null, + explanation: `Character 'r' is not a child of current node. Create new TrieNode for 'r'.`, + pseudoStep: `IF 'r' NOT IN cur.children → Create node and move to it`, + variables: { cur: 'ca', c: 'r', word: word2 } + }); + addLines(9, 10, 10, 12); + + steps.push({ + activeNodeId: 'car', + activePath: ['root', 'c', 'ca', 'car'], + insertedNodes: [...inserted], + endOfWordNodes: [...endOfWord], + operation: 'insert', + word: word2, + currentIndex: 2, + found: null, + explanation: `Move pointer 'cur' to the child node corresponding to 'r'.`, + pseudoStep: `SET cur = cur.children['r']`, + variables: { cur: 'car', c: 'r', word: word2 } + }); + addLines(12, 12, 13, 14); + + endOfWord.push("car"); + steps.push({ + activeNodeId: 'car', + activePath: ['root', 'c', 'ca', 'car'], + insertedNodes: [...inserted], + endOfWordNodes: [...endOfWord], + operation: 'insert', + word: word2, + currentIndex: word2.length, + found: null, + explanation: `Finished inserting "${word2}". Mark current node as the end of a word.`, + pseudoStep: `SET cur.endOfWord = True`, + variables: { cur: 'car', c: '-', word: word2 } + }); + addLines(14, 13, 15, 16); + + // --- SEARCH "cat" --- + const search1 = "cat"; + steps.push({ + activeNodeId: 'root', + activePath: ['root'], + insertedNodes: [...inserted], + endOfWordNodes: [...endOfWord], + operation: 'search', + word: search1, + currentIndex: -1, + found: null, + explanation: `Start searching for "${search1}". Set pointer 'cur' to root.`, + pseudoStep: `CALL search("${search1}"): cur = root`, + variables: { cur: 'root', c: '-', word: search1 } + }); + addLines(16, 15, 17, 18); + + const searchPath1 = ["c", "ca", "cat"]; + for (let i = 0; i < search1.length; i++) { + const c = search1[i]; + const node = searchPath1[i]; + const currentPathArray = ['root', ...searchPath1.slice(0, i + 1)]; + steps.push({ + activeNodeId: node, + activePath: [...currentPathArray], + insertedNodes: [...inserted], + endOfWordNodes: [...endOfWord], + operation: 'search', + word: search1, + currentIndex: i, + found: null, + explanation: `Character '${c}' is in children. Move pointer 'cur' to child node.`, + pseudoStep: `IF '${c}' IN cur.children → Move to it`, + variables: { cur: node, c, word: search1 } }); + addLines(20, 19, 21, 22); + } - // Search operations - ['cat', 'cap'].forEach(word => { - const path: string[] = []; - let found = true; - for (let i = 0; i < word.length; i++) { - path.push(word[i]); - const exists = (word === 'cat') || (i < 2); - if (!exists) found = false; - - newSteps.push({ - currentNode: [...path], - operation: 'search', - word, - currentIndex: i, - found: exists ? null : false, - message: exists - ? `Searching "${word}": Found '${word[i]}'` - : `Searching "${word}": Character '${word[i]}' not found`, - lineNumber: 22 - }); + steps.push({ + activeNodeId: 'cat', + activePath: ['root', 'c', 'ca', 'cat'], + insertedNodes: [...inserted], + endOfWordNodes: [...endOfWord], + operation: 'search', + word: search1, + currentIndex: search1.length, + found: true, + explanation: `Reached end of search word "${search1}". Current node is marked as end of word. Return True (Word Found).`, + pseudoStep: `RETURN cur.endOfWord → True`, + variables: { cur: 'cat', c: '-', word: search1, result: 'True' } + }); + addLines(22, 20, 23, 24); - if (!exists) break; - } - - if (found) { - newSteps.push({ - currentNode: [...path], - operation: 'search', - word, - currentIndex: word.length, - found: true, - message: `Search "${word}": Found!`, - lineNumber: 27 - }); - } + // --- STARTSWITH "ca" --- + const prefix1 = "ca"; + steps.push({ + activeNodeId: 'root', + activePath: ['root'], + insertedNodes: [...inserted], + endOfWordNodes: [...endOfWord], + operation: 'startsWith', + word: prefix1, + currentIndex: -1, + found: null, + explanation: `Start prefix search for "${prefix1}". Set pointer 'cur' to root.`, + pseudoStep: `CALL startsWith("${prefix1}"): cur = root`, + variables: { cur: 'root', c: '-', word: prefix1 } + }); + addLines(25, 80, 111, 147); + + const prefixPath1 = ["c", "ca"]; + for (let i = 0; i < prefix1.length; i++) { + const c = prefix1[i]; + const node = prefixPath1[i]; + const currentPathArray = ['root', ...prefixPath1.slice(0, i + 1)]; + steps.push({ + activeNodeId: node, + activePath: [...currentPathArray], + insertedNodes: [...inserted], + endOfWordNodes: [...endOfWord], + operation: 'startsWith', + word: prefix1, + currentIndex: i, + found: null, + explanation: `Character '${c}' is in children. Move pointer 'cur' to child node.`, + pseudoStep: `IF '${c}' IN cur.children → Move to it`, + variables: { cur: node, c, word: prefix1 } }); + addLines(29, 82, 113, 149); + } - setSteps(newSteps); - }; + steps.push({ + activeNodeId: 'ca', + activePath: ['root', 'c', 'ca'], + insertedNodes: [...inserted], + endOfWordNodes: [...endOfWord], + operation: 'startsWith', + word: prefix1, + currentIndex: prefix1.length, + found: true, + explanation: `Successfully matched entire prefix "${prefix1}". Return True (Prefix Found).`, + pseudoStep: `RETURN True`, + variables: { cur: 'ca', c: '-', word: prefix1, result: 'True' } + }); + addLines(34, 85, 116, 152); - useEffect(() => { - generateSteps(); - }, []); + // --- SEARCH "cap" --- + const search2 = "cap"; + steps.push({ + activeNodeId: 'root', + activePath: ['root'], + insertedNodes: [...inserted], + endOfWordNodes: [...endOfWord], + operation: 'search', + word: search2, + currentIndex: -1, + found: null, + explanation: `Start searching for "${search2}". Set pointer 'cur' to root.`, + pseudoStep: `CALL search("${search2}"): cur = root`, + variables: { cur: 'root', c: '-', word: search2 } + }); + addLines(16, 15, 17, 18); + + steps.push({ + activeNodeId: 'c', + activePath: ['root', 'c'], + insertedNodes: [...inserted], + endOfWordNodes: [...endOfWord], + operation: 'search', + word: search2, + currentIndex: 0, + found: null, + explanation: `Character 'c' is in children. Move pointer 'cur' to child node.`, + pseudoStep: `IF 'c' IN cur.children → Move to it`, + variables: { cur: 'c', c: 'c', word: search2 } + }); + addLines(20, 19, 21, 22); + + steps.push({ + activeNodeId: 'ca', + activePath: ['root', 'c', 'ca'], + insertedNodes: [...inserted], + endOfWordNodes: [...endOfWord], + operation: 'search', + word: search2, + currentIndex: 1, + found: null, + explanation: `Character 'a' is in children. Move pointer 'cur' to child node.`, + pseudoStep: `IF 'a' IN cur.children → Move to it`, + variables: { cur: 'ca', c: 'a', word: search2 } + }); + addLines(20, 19, 21, 22); + + steps.push({ + activeNodeId: 'ca', + activePath: ['root', 'c', 'ca'], + insertedNodes: [...inserted], + endOfWordNodes: [...endOfWord], + operation: 'search', + word: search2, + currentIndex: 2, + found: false, + explanation: `Character 'p' not found in children map of 'ca'. Search fails. Return False (Word Not Found).`, + pseudoStep: `IF 'p' NOT IN cur.children → RETURN False`, + variables: { cur: 'ca', c: 'p', word: search2, result: 'False' } + }); + addLines(19, 18, 20, 21); + + return { steps, stepLineNumbers }; +} + +const nodePositions: Record = { + root: { x: 200, y: 30, label: 'root' }, + c: { x: 200, y: 85, label: 'c' }, + ca: { x: 200, y: 140, label: 'a' }, + cat: { x: 130, y: 195, label: 't' }, + car: { x: 270, y: 195, label: 'r' } +}; + +const treeEdges = [ + { from: 'root', to: 'c' }, + { from: 'c', to: 'ca' }, + { from: 'ca', to: 'cat' }, + { from: 'ca', to: 'car' } +]; + +export const TrieVisualization = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { - intervalRef.current = window.setInterval(() => { - setCurrentStepIndex((prev) => { - if (prev >= steps.length - 1) { - setIsPlaying(false); - return prev; - } + intervalRef.current = setInterval(() => { + setCurrentStepIndex(prev => { + if (prev >= steps.length - 1) { setIsPlaying(false); return prev; } return prev + 1; }); - }, speed); + }, 1000 / speed); } else { - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } + if (intervalRef.current) clearInterval(intervalRef.current); } - - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - }; + return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [isPlaying, currentStepIndex, steps.length, speed]); const handlePlay = () => setIsPlaying(true); const handlePause = () => setIsPlaying(false); - const handleStepForward = () => { - if (currentStepIndex < steps.length - 1) { - setCurrentStepIndex(currentStepIndex + 1); - } - }; - const handleStepBack = () => { - if (currentStepIndex > 0) { - setCurrentStepIndex(currentStepIndex - 1); - } - }; - const handleReset = () => { - setCurrentStepIndex(0); - setIsPlaying(false); - }; + const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(p => p + 1); + const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(p => p - 1); + const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); }; if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return (
-
-
-

- {currentStep.operation === 'insert' ? 'Inserting' : 'Searching'}: {currentStep.word} -

-
-
-
Trie Structure
-
- {currentStep.currentNode.map((char, idx) => ( -
- {char} -
- ))} -
+
+
+
+
+

+ {currentStep.operation}: "{currentStep.word}" +

{currentStep.found !== null && ( -
- {currentStep.found ? '✓ Found' : '✗ Not Found'} -
+ + {currentStep.found ? 'FOUND ✓' : 'NOT FOUND ✗'} + )}
+ + {treeEdges.map((edge, idx) => { + const fromNode = nodePositions[edge.from]; + const toNode = nodePositions[edge.to]; + + const fromIdx = currentStep.activePath.indexOf(edge.from); + const toIdx = currentStep.activePath.indexOf(edge.to); + const isActiveEdge = fromIdx !== -1 && toIdx !== -1 && toIdx === fromIdx + 1; + + const isFinalSuccess = currentStep.found === true && currentStep.activePath.includes(edge.to); + const isFinalFailure = currentStep.found === false && currentStep.activePath.includes(edge.to); + + let strokeClass = 'stroke-border stroke-1'; + if (isFinalSuccess) { + strokeClass = 'stroke-green-500 stroke-2'; + } else if (isFinalFailure) { + strokeClass = 'stroke-red-500 stroke-2'; + } else if (isActiveEdge) { + strokeClass = 'stroke-primary stroke-2'; + } + + return ( + + ); + })} + + {Object.entries(nodePositions).map(([id, pos]) => { + const isCreated = currentStep.insertedNodes.includes(id); + const isActive = currentStep.activeNodeId === id; + const isEnd = currentStep.endOfWordNodes.includes(id); + const inActivePath = currentStep.activePath.includes(id); + + if (!isCreated) return null; + + let circleClass = 'fill-card stroke-border'; + let textClass = 'fill-foreground'; + + if (isActive) { + if (currentStep.found === true) { + circleClass = 'fill-green-500 stroke-green-600 animate-pulse'; + textClass = 'fill-white font-bold'; + } else if (currentStep.found === false) { + circleClass = 'fill-red-500 stroke-red-600 animate-pulse'; + textClass = 'fill-white font-bold'; + } else { + circleClass = 'fill-primary stroke-primary'; + textClass = 'fill-primary-foreground font-bold'; + } + } else if (inActivePath) { + if (currentStep.found === true) { + circleClass = 'fill-green-500/20 stroke-green-500/50'; + textClass = 'fill-green-600 font-medium'; + } else if (currentStep.found === false) { + circleClass = 'fill-red-500/20 stroke-red-500/50'; + textClass = 'fill-red-600 font-medium'; + } else { + circleClass = 'fill-primary/10 stroke-primary/30'; + textClass = 'fill-primary font-medium'; + } + } else if (isEnd) { + circleClass = 'fill-card stroke-green-500 stroke-2'; + } + + return ( + + + {isEnd && ( + + )} + + {pos.label} + + + ); + })} + +
+ Active Node + End of Word + Found State + Not Found State +
-
-

{currentStep.message}

+
+

{currentStep.explanation}

- + +
- +
- - -
); }; diff --git a/src/components/visualizations/algorithms/TwoPointersVisualization.tsx b/src/components/visualizations/algorithms/TwoPointersVisualization.tsx index e1e818d..1854585 100644 --- a/src/components/visualizations/algorithms/TwoPointersVisualization.tsx +++ b/src/components/visualizations/algorithms/TwoPointersVisualization.tsx @@ -1,33 +1,30 @@ import { useEffect, useRef, useState } from 'react'; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import { motion, AnimatePresence } from 'framer-motion'; +import { Info } from 'lucide-react'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { array: number[]; left: number; right: number; - sum: number; + sum: number | string; target: number; - message: string; - lineNumber: number; + explanation: string; + pseudoStep: string; + variables: Record; } -export const TwoPointersVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const intervalRef = useRef(null); +// ─── DB Codes (no comments, exact match) ──────────────────────────────── - const code = `function twoSum(arr: number[], target: number): number[] { +const languages: VisualizationLanguageMap = { + typescript: `function twoPointers(arr: number[], target: number): number[] { let left = 0; let right = arr.length - 1; - while (left < right) { const sum = arr[left] + arr[right]; - if (sum === target) { return [left, right]; } else if (sum < target) { @@ -36,164 +33,249 @@ export const TwoPointersVisualization = () => { right--; } } - return [-1, -1]; -}`; +}`, + + python: `def twoPointers(arr: List[int], target: int) -> List[int]: + left = 0 + right = len(arr) - 1 + while left < right: + sum = arr[left] + arr[right] + if sum == target: + return [left, right] + elif sum < target: + left += 1 + else: + right -= 1 + return [-1, -1]`, + + java: `public int[] twoPointers(int[] arr, int target) { + if (arr == null || arr.length < 2) { + return new int[]{-1, -1}; + } + int left = 0; + int right = arr.length - 1; + while (left < right) { + int sum = arr[left] + arr[right]; + if (sum == target) { + return new int[]{left, right}; + } else if (sum < target) { + left++; + } else { + right--; + } + } + return new int[]{-1, -1}; +}`, + + cpp: `std::vector twoPointers(std::vector& arr, int target) { + if (arr.empty() || arr.size() < 2) { + return {-1, -1}; + } + int left = 0; + int right = arr.size() - 1; + while (left < right) { + int sum = arr[left] + arr[right]; + if (sum == target) { + return {left, right}; + } else if (sum < target) { + left++; + } else { + right--; + } + } + return {-1, -1}; +}` +}; + +// ─── Step generator ────────────────────────────────────────────────────────── + +function generateVisualizationData() { + const array = [1, 3, 4, 6, 8, 9, 11, 12, 15]; + const target = 15; + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - const generateSteps = () => { - const array = [1, 3, 4, 6, 8, 9, 11, 12, 15]; - const target = 15; - const newSteps: Step[] = []; - let left = 0; - let right = array.length - 1; + // Initial State + steps.push({ + array: [...array], + left: -1, + right: -1, + sum: '-', + target, + explanation: 'Start of Two Pointers search in sorted array.', + pseudoStep: 'START twoPointers(arr, target)', + variables: { target, left: '-', right: '-', sum: '-' } + }); + addLines(1, 1, 1, 1); - // Line 1: Function entry (implied) - newSteps.push({ + let left = 0; + steps.push({ + array: [...array], + left: 0, + right: -1, + sum: '-', + target, + explanation: 'Initialize the left pointer to the first index (0).', + pseudoStep: 'SET left = 0', + variables: { target, left: 0, right: '-', sum: '-' } + }); + addLines(2, 2, 5, 5); + + let right = array.length - 1; + steps.push({ + array: [...array], + left, + right, + sum: '-', + target, + explanation: `Initialize the right pointer to the last index (${right}).`, + pseudoStep: `SET right = arr.length − 1 → ${right}`, + variables: { target, left, right, sum: '-' } + }); + addLines(3, 3, 6, 6); + + while (left < right) { + steps.push({ array: [...array], - left: -1, - right: -1, - sum: 0, + left, + right, + sum: '-', target, - message: `Starting Two-Sum with Target = ${target}. Initializing pointers.`, - lineNumber: 1 + explanation: `Check loop condition: left (${left}) < right (${right}) is true.`, + pseudoStep: `WHILE left < right → ${left} < ${right} → YES ✓`, + variables: { target, left, right, sum: '-' } }); + addLines(4, 4, 7, 7); - // Line 2: Initialize left - left = 0; - newSteps.push({ + const sum = array[left] + array[right]; + steps.push({ array: [...array], left, - right: -1, - sum: 0, + right, + sum, target, - message: 'Initialize left pointer at index 0.', - lineNumber: 2 + explanation: `Calculate sum of values: arr[left] (${array[left]}) + arr[right] (${array[right]}) = ${sum}.`, + pseudoStep: `SET sum = arr[left] + arr[right] → ${array[left]} + ${array[right]} = ${sum}`, + variables: { target, left, right, sum, 'arr[left]': array[left], 'arr[right]': array[right] } }); + addLines(5, 5, 8, 8); - // Line 3: Initialize right - right = array.length - 1; - newSteps.push({ + steps.push({ array: [...array], left, right, - sum: 0, + sum, target, - message: `Initialize right pointer at index ${right} (end of array).`, - lineNumber: 3 + explanation: `Compare sum (${sum}) with target (${target}).`, + pseudoStep: `IF sum == target → ${sum} == ${target} → ${sum === target ? 'YES ✓' : 'NO ✗'}`, + variables: { target, left, right, sum, 'arr[left]': array[left], 'arr[right]': array[right] } }); + addLines(6, 6, 9, 9); - while (left < right) { - // Line 5: While condition - newSteps.push({ + if (sum === target) { + steps.push({ array: [...array], left, right, - sum: 0, + sum, target, - message: `Check condition: left (${left}) < right (${right}) is true.`, - lineNumber: 5 + explanation: `Target found! The pointers at indices [${left}, ${right}] sum to the target.`, + pseudoStep: `RETURN [left, right] → [${left}, ${right}]`, + variables: { target, left, right, sum, 'arr[left]': array[left], 'arr[right]': array[right], result: `[${left}, ${right}]` } }); - - const sum = array[left] + array[right]; - // Line 6: Calculate sum - newSteps.push({ + addLines(7, 7, 10, 10); + break; + } else if (sum < target) { + steps.push({ array: [...array], left, right, sum, target, - message: `Calculate sum: arr[${left}] (${array[left]}) + arr[${right}] (${array[right]}) = ${sum}.`, - lineNumber: 6 + explanation: `Since sum (${sum}) < target (${target}), we need a larger sum. Move left pointer rightward.`, + pseudoStep: `ELSE IF sum < target → ${sum} < ${target} → YES ✓`, + variables: { target, left, right, sum, 'arr[left]': array[left], 'arr[right]': array[right] } }); + addLines(8, 8, 11, 11); - // Line 8: Check if sum equals target - newSteps.push({ + left++; + steps.push({ array: [...array], left, right, sum, target, - message: `Compare sum (${sum}) with target (${target}).`, - lineNumber: 8 + explanation: `Increment left pointer: left is now ${left}.`, + pseudoStep: 'left++ (increment left pointer)', + variables: { target, left, right, sum, 'arr[left]': array[left], 'arr[right]': array[right] } }); + addLines(9, 9, 12, 12); + } else { + steps.push({ + array: [...array], + left, + right, + sum, + target, + explanation: `Since sum (${sum}) > target (${target}), we need a smaller sum. Move right pointer leftward.`, + pseudoStep: `ELSE IF sum < target → ${sum} < ${target} → NO ✗`, + variables: { target, left, right, sum, 'arr[left]': array[left], 'arr[right]': array[right] } + }); + addLines(8, 8, 11, 11); - if (sum === target) { - newSteps.push({ - array: [...array], - left, - right, - sum, - target, - message: `Success! ${sum} matches target ${target}. Returning indices [${left}, ${right}].`, - lineNumber: 9 - }); - break; - } else if (sum < target) { - // Line 10: Check if sum < target - newSteps.push({ - array: [...array], - left, - right, - sum, - target, - message: `${sum} is less than ${target}. We need a larger sum, so move left pointer right.`, - lineNumber: 10 - }); - // Line 11: left++ - left++; - newSteps.push({ - array: [...array], - left, - right, - sum, - target, - message: `Incremented left pointer to index ${left}.`, - lineNumber: 11 - }); - } else { - // Line 12: Sum > target - newSteps.push({ - array: [...array], - left, - right, - sum, - target, - message: `${sum} is greater than ${target}. We need a smaller sum, so move right pointer left.`, - lineNumber: 12 - }); - // Line 13: right-- - right--; - newSteps.push({ - array: [...array], - left, - right, - sum, - target, - message: `Decremented right pointer to index ${right}.`, - lineNumber: 13 - }); - } - } - - if (left >= right && newSteps[newSteps.length - 1].lineNumber !== 9) { - newSteps.push({ + right--; + steps.push({ array: [...array], left, right, - sum: 0, + sum, target, - message: 'Target not found in the array.', - lineNumber: 17 + explanation: `Decremented right pointer: right is now ${right}.`, + pseudoStep: 'right-- (decrement right pointer)', + variables: { target, left, right, sum, 'arr[left]': array[left], 'arr[right]': array[right] } }); + addLines(11, 11, 14, 14); } + } - setSteps(newSteps); - setCurrentStepIndex(0); - }; + if (left >= right) { + steps.push({ + array: [...array], + left, + right, + sum: '-', + target, + explanation: 'Pointers crossed or met. Target sum was not found in the array.', + pseudoStep: 'RETURN [-1, -1]', + variables: { target, left, right, sum: '-', result: '[-1, -1]' } + }); + addLines(14, 12, 17, 17); + } - useEffect(() => { - generateSteps(); - }, []); + return { steps, stepLineNumbers }; +} + +// ─── Component ─────────────────────────────────────────────────────────────── + +export const TwoPointersVisualization = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { @@ -205,42 +287,29 @@ export const TwoPointersVisualization = () => { } return prev + 1; }); - }, 1000 / speed); + }, 1200 / speed); } else { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } + if (intervalRef.current) clearInterval(intervalRef.current); } - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } + if (intervalRef.current) clearInterval(intervalRef.current); }; }, [isPlaying, currentStepIndex, steps.length, speed]); const handlePlay = () => setIsPlaying(true); const handlePause = () => setIsPlaying(false); - const handleStepForward = () => { - if (currentStepIndex < steps.length - 1) { - setCurrentStepIndex(prev => prev + 1); - } - }; - const handleStepBack = () => { - if (currentStepIndex > 0) { - setCurrentStepIndex(prev => prev - 1); - } - }; + const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(p => p + 1); + const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(p => p - 1); const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); - generateSteps(); }; if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; const getMaxValue = () => Math.max(...currentStep.array); + const pseudoSteps = steps.map(s => s.pseudoStep); return (
@@ -258,9 +327,10 @@ export const TwoPointersVisualization = () => { />
+ {/* Left Column: Visualizer, Commentary, and Variables */}
-
-
+
+
{currentStep.array.map((value, index) => { const isLeft = index === currentStep.left; const isRight = index === currentStep.right; @@ -272,19 +342,19 @@ export const TwoPointersVisualization = () => { className="flex flex-col items-center gap-2 flex-1 max-w-[60px] relative" > {isLeft && ( -
- LEFT +
+ Left
)} {isRight && ( -
- RIGHT +
+ Right
)}
{ }} /> {value} @@ -303,33 +373,62 @@ export const TwoPointersVisualization = () => {
-
-

{currentStep.message}

-
-
- - -
-
+ {/* Commentary Panel */} +
+
+
+
+ + + + + + Algorithm Commentary + +
+
+ Step {currentStepIndex + 1} of {steps.length} +
+
-
+
+
+ +
+
+

+ Current Action +

+ + + {currentStep.explanation} + + +
+
+
+
- +
+ + {/* Right Column: Code & Pseudocode Display */} +
); }; +export default TwoPointersVisualization; diff --git a/src/components/visualizations/algorithms/TwoSumVisualization.tsx b/src/components/visualizations/algorithms/TwoSumVisualization.tsx index bb5711c..9638964 100644 --- a/src/components/visualizations/algorithms/TwoSumVisualization.tsx +++ b/src/components/visualizations/algorithms/TwoSumVisualization.tsx @@ -1,24 +1,30 @@ import { useEffect, useRef, useState } from 'react'; -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { array: number[]; highlights: number[]; variables: Record; explanation: string; - lineNumber: number; + pseudoStep: string; } -export const TwoSumVisualization = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1); - const intervalRef = useRef(null); +// ─── Hardcoded code per language ───────────────────────────────────────────── + +const languages: VisualizationLanguageMap = { + python: `def two_sum(nums: list[int], target: int) -> list[int]: + seen = {} + for i, num in enumerate(nums): + complement = target - num + if complement in seen: + return [seen[complement], i] + seen[num] = i + return [-1, -1]`, - const code = `function twoSum(nums: number[], target: number): number[] { + typescript: `function twoSum(nums: number[], target: number): number[] { const seen = new Map(); for (let i = 0; i < nums.length; i++) { const complement = target - nums[i]; @@ -27,129 +33,155 @@ export const TwoSumVisualization = () => { } seen.set(nums[i], i); } - return [-1,-1]; -}`; + return [-1, -1]; +}`, + + java: `public int[] twoSum(int[] nums, int target) { + Map seen = new HashMap<>(); + for (int i = 0; i < nums.length; i++) { + int complement = target - nums[i]; + if (seen.containsKey(complement)) { + return new int[] { seen.get(complement), i }; + } + seen.put(nums[i], i); + } + return new int[] { -1, -1 }; +}`, + + cpp: `vector twoSum(vector& nums, int target) { + unordered_map seen; + for (int i = 0; i < (int)nums.size(); i++) { + int complement = target - nums[i]; + if (seen.count(complement)) { + return { seen[complement], i }; + } + seen[nums[i]] = i; + } + return { -1, -1 }; +}`, +}; - const generateSteps = () => { - const nums = [2, 7, 10, 9, 11]; - const target = 18; - const newSteps: Step[] = []; - const seen = new Map(); +// ─── Step generator ────────────────────────────────────────────────────────── + +function generateVisualizationData() { + const nums = [2, 7, 10, 9, 11]; + const target = 18; + const steps: Step[] = []; + const seen = new Map(); + + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - // Line 2: const seen = new Map(); - newSteps.push({ + steps.push({ + array: [...nums], + highlights: [], + variables: { target, seen: '{}', i: '-', complement: '-' }, + explanation: `Initialize an empty hash map 'seen'. Target is ${target}.`, + pseudoStep: 'SET seen = {} (empty hash map)', + }); + addLines(2, 2, 2, 2); // init + + for (let i = 0; i < nums.length; i++) { + steps.push({ array: [...nums], - highlights: [], - variables: { target, seen: '{}', i: '-', complement: '-' }, - explanation: `Initialize an empty hash map 'seen' to store numbers and their indices. Target is ${target}.`, - lineNumber: 2 + highlights: [i], + variables: { target, seen: JSON.stringify(Object.fromEntries(seen)), i, 'nums[i]': nums[i], complement: '-' }, + explanation: `Iteration i=${i}: inspect nums[${i}] = ${nums[i]}.`, + pseudoStep: `FOR i = ${i}: inspect nums[${i}] = ${nums[i]}`, }); + addLines(3, 3, 3, 3); // loop - for (let i = 0; i < nums.length; i++) { - // Line 3: for (let i = 0; i < nums.length; i++) { - newSteps.push({ - array: [...nums], - highlights: [i], - variables: { target, seen: JSON.stringify(Object.fromEntries(seen)), i, 'nums[i]': nums[i], complement: '-' }, - explanation: `Iteration i=${i}: Process nums[${i}] = ${nums[i]}.`, - lineNumber: 3 - }); + const complement = target - nums[i]; + + steps.push({ + array: [...nums], + highlights: [i], + variables: { target, seen: JSON.stringify(Object.fromEntries(seen)), i, 'nums[i]': nums[i], complement }, + explanation: `complement = ${target} − ${nums[i]} = ${complement}. This is the value we need to find in 'seen'.`, + pseudoStep: `SET complement = target − nums[i] → ${target} − ${nums[i]} = ${complement}`, + }); + addLines(4, 4, 4, 4); // compute complement - const complement = target - nums[i]; + steps.push({ + array: [...nums], + highlights: [i], + variables: { target, seen: JSON.stringify(Object.fromEntries(seen)), i, 'nums[i]': nums[i], complement }, + explanation: `Check if complement (${complement}) is in 'seen'.`, + pseudoStep: `IF complement (${complement}) IN seen → ${seen.has(complement) ? 'YES ✓' : 'NO ✗'}`, + }); + addLines(5, 5, 5, 5); // check if in seen - // Line 4: const complement = target - nums[i]; - newSteps.push({ + if (seen.has(complement)) { + const j = seen.get(complement)!; + steps.push({ array: [...nums], - highlights: [i], - variables: { target, seen: JSON.stringify(Object.fromEntries(seen)), i, 'nums[i]': nums[i], complement }, - explanation: `Calculate complement: ${target} - ${nums[i]} = ${complement}. This is the number we need to find in 'seen'.`, - lineNumber: 4 + highlights: [j, i], + variables: { target, seen: JSON.stringify(Object.fromEntries(seen)), i, j, 'nums[i]': nums[i], 'nums[j]': nums[j], result: `[${j}, ${i}]` }, + explanation: `Found! nums[${j}] + nums[${i}] = ${target}. Return [${j}, ${i}].`, + pseudoStep: `FOUND complement at index ${j} → RETURN [${j}, ${i}]`, }); - - // Line 5: if (seen.has(complement)) { - newSteps.push({ + addLines(6, 6, 6, 6); // found (return) + break; + } else { + steps.push({ array: [...nums], highlights: [i], variables: { target, seen: JSON.stringify(Object.fromEntries(seen)), i, 'nums[i]': nums[i], complement }, - explanation: `Check if 'seen' has the complement (${complement}).`, - lineNumber: 5 + explanation: `${complement} not in 'seen'. Store seen[${nums[i]}] = ${i}.`, + pseudoStep: `ELSE: seen[${nums[i]}] = ${i} (store num → index)`, }); - - if (seen.has(complement)) { - const j = seen.get(complement)!; - // Line 6: return [seen.get(complement)!, i]; - newSteps.push({ - array: [...nums], - highlights: [j, i], - variables: { target, seen: JSON.stringify(Object.fromEntries(seen)), i, j, 'nums[i]': nums[i], 'nums[j]': nums[j], result: `[${j}, ${i}]` }, - explanation: `Found ${complement} at index ${j}! The numbers at indices ${j} and ${i} add up to ${target}. Return [${j}, ${i}].`, - lineNumber: 6 - }); - break; // Stop generator here as we return - } else { - // Line 8: seen.set(nums[i], i); - newSteps.push({ - array: [...nums], - highlights: [i], - variables: { target, seen: JSON.stringify(Object.fromEntries(seen)), i, 'nums[i]': nums[i], complement, action: `seen.set(${nums[i]}, ${i})` }, - explanation: `${complement} is not in 'seen'. Add current number ${nums[i]} to 'seen' with its index ${i}.`, - lineNumber: 8 - }); - seen.set(nums[i], i); - } + addLines(8, 7, 8, 8); // store in seen + seen.set(nums[i], i); } + } - if (!newSteps[newSteps.length - 1]?.variables.result) { - // Line 10: return [-1,-1]; - newSteps.push({ - array: [...nums], - highlights: [], - variables: { target, seen: JSON.stringify(Object.fromEntries(seen)), i: '-', complement: '-' }, - explanation: `Loop completed and no sum was found. Return [-1, -1].`, - lineNumber: 10 - }); - } + return { steps, stepLineNumbers }; +} - setSteps(newSteps); - setCurrentStepIndex(0); - }; +// ─── Component ─────────────────────────────────────────────────────────────── - useEffect(() => { - generateSteps(); - }, []); +export const TwoSumVisualization = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { intervalRef.current = setInterval(() => { setCurrentStepIndex(prev => { - if (prev >= steps.length - 1) { - setIsPlaying(false); - return prev; - } + if (prev >= steps.length - 1) { setIsPlaying(false); return prev; } return prev + 1; }); }, 1000 / speed); } else { if (intervalRef.current) clearInterval(intervalRef.current); } - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - }; + return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [isPlaying, currentStepIndex, steps.length, speed]); const handlePlay = () => setIsPlaying(true); const handlePause = () => setIsPlaying(false); - const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(prev => prev + 1); - const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(prev => prev - 1); - const handleReset = () => { - setCurrentStepIndex(0); - setIsPlaying(false); - generateSteps(); - }; + const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(p => p + 1); + const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(p => p - 1); + const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); }; if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return (
@@ -167,43 +199,50 @@ export const TwoSumVisualization = () => { />
+ {/* Left: visual state */}
{currentStep.array.map((value, idx) => (
-
- {value} + }`}> + {value}
- [{idx}] + [{idx}]
))}
-

{currentStep.explanation}

+

{currentStep.explanation}

-

Hash Map Strategy:

+

Hash Map Strategy:

-

• For each number, calculate its complement (target - num)

-

• Check if complement exists in hash map

-

• If yes: found the pair! If no: store current number

-

• Time: O(n), Space: O(n)

+

• For each number, calculate complement = target − num

+

• If complement is in map → found the pair

+

• Otherwise → store current number in map

+

• Time: O(n) · Space: O(n)

- + {/* Right: code / pseudocode panel */} +
); diff --git a/src/components/visualizations/algorithms/UnionFindByRankVisualization.tsx b/src/components/visualizations/algorithms/UnionFindByRankVisualization.tsx index 744e935..a8b571b 100644 --- a/src/components/visualizations/algorithms/UnionFindByRankVisualization.tsx +++ b/src/components/visualizations/algorithms/UnionFindByRankVisualization.tsx @@ -1,41 +1,38 @@ import { useState, useMemo } from 'react'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; import { motion } from 'framer-motion'; import { Card } from '@/components/ui/card'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { - parent: number[]; - rank: number[]; - results: number[]; - highlightedLines: number[]; - explanation: string; - variables: Record; - activeNodes: number[]; - phase: 'init' | 'find' | 'union' | 'results' | 'done'; + parent: number[]; + rank: number[]; + results: number[]; + explanation: string; + pseudoStep: string; + variables: Record; + activeNodes: number[]; + phase: 'init' | 'find' | 'union' | 'results' | 'done'; } const N = 6; const UNION_OPS: number[][] = [[0, 1], [2, 3], [4, 5], [0, 2], [0, 4]]; const FIND_OPS: number[] = [1, 5]; -export const UnionFindByRankVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); - - const code = `function solveUnionFind( +const languages: VisualizationLanguageMap = { + typescript: `function solveUnionFind( union_operations: number[][], find_operations: number[], n: number ): number[] { const parent: number[] = new Array(n); const rank: number[] = new Array(n).fill(0); - for (let i = 0; i < n; i++) { parent[i] = i; } - function find(i: number): number { if (parent[i] === i) { return i; @@ -43,11 +40,9 @@ export const UnionFindByRankVisualization = () => { parent[i] = find(parent[i]); return parent[i]; } - function union(x: number, y: number): void { const rootX = find(x); const rootY = find(y); - if (rootX !== rootY) { if (rank[rootX] < rank[rootY]) { parent[rootX] = rootY; @@ -59,454 +54,598 @@ export const UnionFindByRankVisualization = () => { } } } - for (const operation of union_operations) { union(operation[0], operation[1]); } - const results: number[] = []; for (const node of find_operations) { results.push(find(node)); } - return results; -}`; +}`, + + python: `def solve_union_find(union_operations, find_operations, n): + parent = list(range(n)) + rank = [0] * n + def find(i): + if parent[i] != i: + parent[i] = find(parent[i]) + return parent[i] + def union(x, y): + rootX = find(x) + rootY = find(y) + if rootX != rootY: + if rank[rootX] < rank[rootY]: + parent[rootX] = rootY + elif rank[rootX] > rank[rootY]: + parent[rootY] = rootX + else: + parent[rootY] = rootX + rank[rootX] += 1 + for x, y in union_operations: + union(x, y) + results = [] + for node in find_operations: + results.append(find(node)) + return results`, + + java: `static int find(int[] parent, int i) { + if (parent[i] == i) { + return i; + } + return parent[i] = find(parent, parent[i]); +} +static void union(int[] parent, int[] rank, int x, int y) { + int rootX = find(parent, x); + int rootY = find(parent, y); + if (rootX != rootY) { + if (rank[rootX] < rank[rootY]) { + parent[rootX] = rootY; + } else if (rank[rootX] > rank[rootY]) { + parent[rootY] = rootX; + } else { + parent[rootY] = rootX; + rank[rootX]++; + } + } +} +static int[] solveUnionFind(int[][] union_operations, int[] find_operations, int n) { + int[] parent = new int[n]; + int[] rank = new int[n]; + for (int i = 0; i < n; i++) { + parent[i] = i; + rank[i] = 0; + } + for (int[] operation : union_operations) { + union(parent, rank, operation[0], operation[1]); + } + int[] results = new int[find_operations.length]; + for (int i = 0; i < find_operations.length; i++) { + results[i] = find(parent, find_operations[i]); + } + return results; +}`, - const steps: Step[] = useMemo(() => { - const s: Step[] = []; - const parent: number[] = new Array(N); - const rank: number[] = new Array(N).fill(0); - const results: number[] = []; + cpp: `int findRoot(vector& parent, int i) { + if (parent[i] != i) { + parent[i] = findRoot(parent, parent[i]); + } + return parent[i]; +} +void unionSet(vector& parent, vector& rank, int x, int y) { + int rootX = findRoot(parent, x); + int rootY = findRoot(parent, y); + if (rootX != rootY) { + if (rank[rootX] < rank[rootY]) { + parent[rootX] = rootY; + } + else if (rank[rootX] > rank[rootY]) { + parent[rootY] = rootX; + } + else { + parent[rootY] = rootX; + rank[rootX]++; + } + } +} +vector solveUnionFind(vector>& union_operations, + vector& find_operations, + int n) { + vector parent(n); + vector rank(n, 0); + for (int i = 0; i < n; i++) { + parent[i] = i; + } + for (auto& op : union_operations) { + unionSet(parent, rank, op[0], op[1]); + } + vector results; + for (int node : find_operations) { + results.push_back(findRoot(parent, node)); + } + return results; +}`, +}; - s.push({ - parent: [...parent], rank: [...rank], results: [], - highlightedLines: [1, 2, 3, 4, 5], - explanation: `Initialize solveUnionFind with n=${N}.`, - variables: { n: N, union_operations: JSON.stringify(UNION_OPS), find_operations: JSON.stringify(FIND_OPS) }, - activeNodes: [], - phase: 'init' - }); +export const UnionFindByRankVisualization = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + + const { steps, stepLineNumbers } = useMemo(() => { + const s: Step[] = []; + const parent: number[] = new Array(N); + const rank: number[] = new Array(N).fill(0); + const results: number[] = []; + + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + s.push({ + parent: [...parent], rank: [...rank], results: [], + explanation: `Initialize solveUnionFind with n=${N}.`, + pseudoStep: `CALL solveUnionFind(union_operations, find_operations, n = ${N})`, + variables: { n: N, union_operations: JSON.stringify(UNION_OPS), find_operations: JSON.stringify(FIND_OPS) }, + activeNodes: [], + phase: 'init' + }); + addLines(1, 1, 21, 23); + + s.push({ + parent: [...parent], rank: [...rank], results: [], + explanation: `Initialize parent and rank arrays. Ranks start at 0.`, + pseudoStep: `SET parent = Array(n), rank = Array(n).fill(0)`, + variables: { parent: 'uninitialized', rank: `[${rank.join(', ')}]` }, + activeNodes: [], + phase: 'init' + }); + addLines(6, 2, 22, 26); + + for (let i = 0; i < N; i++) { + parent[i] = i; + } + + s.push({ + parent: [...parent], rank: [...rank], results: [], + explanation: `Set each node's parent to itself: [${parent.join(', ')}].`, + pseudoStep: `FOR i = 0 to n-1: SET parent[i] = i`, + variables: { parent: `[${parent.join(', ')}]` }, + activeNodes: Array.from({ length: N }, (_, index) => index), + phase: 'init' + }); + addLines(8, 2, 24, 28); + + const findFn = (i: number, isRecursiveCall = false): number => { + if (!isRecursiveCall) { s.push({ - parent: [...parent], rank: [...rank], results: [], - highlightedLines: [6, 7], - explanation: `Initialize parent and rank arrays. Ranks start at 0.`, - variables: { parent: 'uninitialized', rank: `[${rank.join(', ')}]` }, - activeNodes: [], - phase: 'init' + parent: [...parent], rank: [...rank], results: [...results], + explanation: `find(${i}): starting search for representative.`, + pseudoStep: `CALL find(i = ${i})`, + variables: { i, 'parent[i]': parent[i] }, + activeNodes: [i], + phase: 'find' }); + addLines(11, 4, 1, 1); + } - for (let i = 0; i < N; i++) { - parent[i] = i; - } - + s.push({ + parent: [...parent], rank: [...rank], results: [...results], + explanation: `Checking if node ${i} is its own parent (root).`, + pseudoStep: `IF parent[${i}] == ${i}`, + variables: { i, 'parent[i]': parent[i] }, + activeNodes: [i], + phase: 'find' + }); + addLines(12, 5, 2, 2); + + if (parent[i] === i) { s.push({ - parent: [...parent], rank: [...rank], results: [], - highlightedLines: [9, 10, 11], - explanation: `Set each node's parent to itself: [${parent.join(', ')}].`, - variables: { parent: `[${parent.join(', ')}]` }, - activeNodes: Array.from({ length: N }, (_, index) => index), - phase: 'init' + parent: [...parent], rank: [...rank], results: [...results], + explanation: `Node ${i} is the root. Returning ${i}.`, + pseudoStep: `RETURN ${i}`, + variables: { i, root: i }, + activeNodes: [i], + phase: 'find' }); + addLines(13, 5, 3, 3); + return i; + } - const findFn = (i: number, isRecursiveCall = false): number => { - if (!isRecursiveCall) { - s.push({ - parent: [...parent], rank: [...rank], results: [...results], - highlightedLines: [13], - explanation: `find(${i}): starting search for representative.`, - variables: { i, 'parent[i]': parent[i] }, - activeNodes: [i], - phase: 'find' - }); - } - - s.push({ - parent: [...parent], rank: [...rank], results: [...results], - highlightedLines: [14], - explanation: `Checking if node ${i} is its own parent (root).`, - variables: { i, 'parent[i]': parent[i] }, - activeNodes: [i], - phase: 'find' - }); - - if (parent[i] === i) { - s.push({ - parent: [...parent], rank: [...rank], results: [...results], - highlightedLines: [15], - explanation: `Node ${i} is the root. Returning ${i}.`, - variables: { i, root: i }, - activeNodes: [i], - phase: 'find' - }); - return i; - } - - s.push({ - parent: [...parent], rank: [...rank], results: [...results], - highlightedLines: [17], - explanation: `Node ${i} is not root. Recursively find root of parent ${parent[i]} and apply path compression.`, - variables: { i, 'parent[i]': parent[i] }, - activeNodes: [i, parent[i]], - phase: 'find' - }); - - const root = findFn(parent[i], true); - parent[i] = root; - - s.push({ - parent: [...parent], rank: [...rank], results: [...results], - highlightedLines: [17, 18], - explanation: `Path compression: set parent of ${i} to ${root}. Returning ${root}.`, - variables: { i, 'parent[i]': parent[i], root }, - activeNodes: [i, root], - phase: 'find' - }); - - return root; - }; - - const unionFn = (x: number, y: number): void => { - s.push({ - parent: [...parent], rank: [...rank], results: [...results], - highlightedLines: [21], - explanation: `union(${x}, ${y}): merging sets containing ${x} and ${y}.`, - variables: { x, y }, - activeNodes: [x, y], - phase: 'union' - }); - - s.push({ - parent: [...parent], rank: [...rank], results: [...results], - highlightedLines: [22], - explanation: `Finding root of x (${x}).`, - variables: { x, y }, - activeNodes: [x], - phase: 'union' - }); - const rootX = findFn(x); - - s.push({ - parent: [...parent], rank: [...rank], results: [...results], - highlightedLines: [23], - explanation: `Finding root of y (${y}).`, - variables: { x, y, rootX }, - activeNodes: [y], - phase: 'union' - }); - const rootY = findFn(y); - - s.push({ - parent: [...parent], rank: [...rank], results: [...results], - highlightedLines: [25], - explanation: `Checking if roots are different: rootX=${rootX}, rootY=${rootY}.`, - variables: { x, y, rootX, rootY }, - activeNodes: [rootX, rootY], - phase: 'union' - }); - - if (rootX !== rootY) { - s.push({ - parent: [...parent], rank: [...rank], results: [...results], - highlightedLines: [26], - explanation: `Comparing ranks: rank[${rootX}]=${rank[rootX]}, rank[${rootY}]=${rank[rootY]}.`, - variables: { rootX, rootY, 'rank[rootX]': rank[rootX], 'rank[rootY]': rank[rootY] }, - activeNodes: [rootX, rootY], - phase: 'union' - }); - - if (rank[rootX] < rank[rootY]) { - parent[rootX] = rootY; - s.push({ - parent: [...parent], rank: [...rank], results: [...results], - highlightedLines: [27], - explanation: `rank[${rootX}] < rank[${rootY}]. Attach tree ${rootX} to ${rootY}.`, - variables: { rootX, rootY, [`parent[${rootX}]`]: rootY }, - activeNodes: [rootX, rootY], - phase: 'union' - }); - } else if (rank[rootX] > rank[rootY]) { - parent[rootY] = rootX; - s.push({ - parent: [...parent], rank: [...rank], results: [...results], - highlightedLines: [28, 29], - explanation: `rank[${rootX}] > rank[${rootY}]. Attach tree ${rootY} to ${rootX}.`, - variables: { rootX, rootY, [`parent[${rootY}]`]: rootX }, - activeNodes: [rootX, rootY], - phase: 'union' - }); - } else { - parent[rootY] = rootX; - rank[rootX]++; - s.push({ - parent: [...parent], rank: [...rank], results: [...results], - highlightedLines: [30, 31, 32], - explanation: `Ranks equal. Attach tree ${rootY} to ${rootX} and increment rank of ${rootX} to ${rank[rootX]}.`, - variables: { rootX, rootY, [`parent[${rootY}]`]: rootX, [`rank[${rootX}]`]: rank[rootX] }, - activeNodes: [rootX, rootY], - phase: 'union' - }); - } - } else { - s.push({ - parent: [...parent], rank: [...rank], results: [...results], - highlightedLines: [25], - explanation: `Roots are the same (${rootX}). Already in the same set.`, - variables: { rootX, rootY }, - activeNodes: [rootX], - phase: 'union' - }); - } - }; - - for (const op of UNION_OPS) { - s.push({ - parent: [...parent], rank: [...rank], results: [...results], - highlightedLines: [37, 38], - explanation: `Processing union operation [${op[0]}, ${op[1]}].`, - variables: { op: JSON.stringify(op) }, - activeNodes: [op[0], op[1]], - phase: 'union' - }); - unionFn(op[0], op[1]); - } + s.push({ + parent: [...parent], rank: [...rank], results: [...results], + explanation: `Node ${i} is not root. Recursively find root of parent ${parent[i]} and apply path compression.`, + pseudoStep: `SET parent[${i}] = find(parent[${i}] = ${parent[i]}) (Path Compression)`, + variables: { i, 'parent[i]': parent[i] }, + activeNodes: [i, parent[i]], + phase: 'find' + }); + addLines(15, 6, 5, 3); + + const root = findFn(parent[i], true); + parent[i] = root; + + s.push({ + parent: [...parent], rank: [...rank], results: [...results], + explanation: `Path compression: set parent of ${i} to ${root}. Returning ${root}.`, + pseudoStep: `RETURN parent[${i}] = ${root}`, + variables: { i, 'parent[i]': parent[i], root }, + activeNodes: [i, root], + phase: 'find' + }); + addLines(16, 7, 5, 5); + + return root; + }; + const unionFn = (x: number, y: number): void => { + s.push({ + parent: [...parent], rank: [...rank], results: [...results], + explanation: `union(${x}, ${y}): merging sets containing ${x} and ${y}.`, + pseudoStep: `CALL union(x = ${x}, y = ${y})`, + variables: { x, y }, + activeNodes: [x, y], + phase: 'union' + }); + addLines(18, 8, 7, 7); + + s.push({ + parent: [...parent], rank: [...rank], results: [...results], + explanation: `Finding root of x (${x}).`, + pseudoStep: `SET rootX = find(${x})`, + variables: { x, y }, + activeNodes: [x], + phase: 'union' + }); + addLines(19, 9, 8, 8); + const rootX = findFn(x); + + s.push({ + parent: [...parent], rank: [...rank], results: [...results], + explanation: `Finding root of y (${y}).`, + pseudoStep: `SET rootY = find(${y})`, + variables: { x, y, rootX }, + activeNodes: [y], + phase: 'union' + }); + addLines(20, 10, 9, 9); + const rootY = findFn(y); + + s.push({ + parent: [...parent], rank: [...rank], results: [...results], + explanation: `Checking if roots are different: rootX=${rootX}, rootY=${rootY}.`, + pseudoStep: `IF rootX (${rootX}) != rootY (${rootY}) → ${rootX !== rootY ? 'YES ✓' : 'NO ✗'}`, + variables: { x, y, rootX, rootY }, + activeNodes: [rootX, rootY], + phase: 'union' + }); + addLines(21, 11, 10, 10); + + if (rootX !== rootY) { s.push({ - parent: [...parent], rank: [...rank], results: [...results], - highlightedLines: [41], - explanation: `All union operations completed. Initializing results array.`, - variables: { results: '[]' }, - activeNodes: [], - phase: 'results' + parent: [...parent], rank: [...rank], results: [...results], + explanation: `Comparing ranks: rank[${rootX}]=${rank[rootX]}, rank[${rootY}]=${rank[rootY]}.`, + pseudoStep: `IF rank[rootX] (${rank[rootX]}) < rank[rootY] (${rank[rootY]})`, + variables: { rootX, rootY, 'rank[rootX]': rank[rootX], 'rank[rootY]': rank[rootY] }, + activeNodes: [rootX, rootY], + phase: 'union' }); + addLines(22, 12, 11, 11); - for (const node of FIND_OPS) { - s.push({ - parent: [...parent], rank: [...rank], results: [...results], - highlightedLines: [42, 43], - explanation: `Performing find operation for node ${node}.`, - variables: { node, results: JSON.stringify(results) }, - activeNodes: [node], - phase: 'results' - }); - const root = findFn(node); - results.push(root); - s.push({ - parent: [...parent], rank: [...rank], results: [...results], - highlightedLines: [43], - explanation: `find(${node}) returned ${root}. Adding to results.`, - variables: { results: JSON.stringify(results) }, - activeNodes: [node, root], - phase: 'results' - }); + if (rank[rootX] < rank[rootY]) { + parent[rootX] = rootY; + s.push({ + parent: [...parent], rank: [...rank], results: [...results], + explanation: `rank[${rootX}] < rank[${rootY}]. Attach tree ${rootX} to ${rootY}.`, + pseudoStep: `SET parent[rootX] = rootY → parent[${rootX}] = ${rootY}`, + variables: { rootX, rootY, [`parent[${rootX}]`]: rootY }, + activeNodes: [rootX, rootY], + phase: 'union' + }); + addLines(23, 13, 12, 12); + } else if (rank[rootX] > rank[rootY]) { + parent[rootY] = rootX; + s.push({ + parent: [...parent], rank: [...rank], results: [...results], + explanation: `rank[${rootX}] > rank[${rootY}]. Attach tree ${rootY} to ${rootX}.`, + pseudoStep: `SET parent[rootY] = rootX → parent[${rootY}] = ${rootX}`, + variables: { rootX, rootY, [`parent[${rootY}]`]: rootX }, + activeNodes: [rootX, rootY], + phase: 'union' + }); + addLines(25, 15, 14, 15); + } else { + parent[rootY] = rootX; + rank[rootX]++; + s.push({ + parent: [...parent], rank: [...rank], results: [...results], + explanation: `Ranks equal. Attach tree ${rootY} to ${rootX} and increment rank of ${rootX} to ${rank[rootX]}.`, + pseudoStep: `SET parent[rootY] = rootX, rank[rootX]++ → rank[${rootX}] = ${rank[rootX]}`, + variables: { rootX, rootY, [`parent[${rootY}]`]: rootX, [`rank[${rootX}]`]: rank[rootX] }, + activeNodes: [rootX, rootY], + phase: 'union' + }); + addLines(27, 17, 16, 18); } - + } else { s.push({ - parent: [...parent], rank: [...rank], results: [...results], - highlightedLines: [46], - explanation: `Algorithm finished. Final results: [${results.join(', ')}].`, - variables: { results: JSON.stringify(results) }, - activeNodes: [], - phase: 'done' + parent: [...parent], rank: [...rank], results: [...results], + explanation: `Roots are the same (${rootX}). Already in the same set.`, + pseudoStep: `ELSE: roots match, do nothing`, + variables: { rootX, rootY }, + activeNodes: [rootX], + phase: 'union' }); + addLines(21, 11, 10, 10); + } + }; - return s; - }, []); + for (const op of UNION_OPS) { + s.push({ + parent: [...parent], rank: [...rank], results: [...results], + explanation: `Processing union operation [${op[0]}, ${op[1]}].`, + pseudoStep: `FOR operation in union_operations: union(${op[0]}, ${op[1]})`, + variables: { op: JSON.stringify(op) }, + activeNodes: [op[0], op[1]], + phase: 'union' + }); + addLines(32, 19, 28, 31); + unionFn(op[0], op[1]); + } - const step = steps[currentStep] || steps[0]; + s.push({ + parent: [...parent], rank: [...rank], results: [...results], + explanation: `All union operations completed. Initializing results array.`, + pseudoStep: `SET results = []`, + variables: { results: '[]' }, + activeNodes: [], + phase: 'results' + }); + addLines(35, 21, 31, 34); + + for (const node of FIND_OPS) { + s.push({ + parent: [...parent], rank: [...rank], results: [...results], + explanation: `Performing find operation for node ${node}.`, + pseudoStep: `FOR node = ${node} in find_operations`, + variables: { node, results: JSON.stringify(results) }, + activeNodes: [node], + phase: 'results' + }); + addLines(36, 22, 32, 35); + const root = findFn(node); + results.push(root); + s.push({ + parent: [...parent], rank: [...rank], results: [...results], + explanation: `find(${node}) returned root ${root}. Adding to results.`, + pseudoStep: `ADD find(${node}) = ${root} TO results`, + variables: { results: JSON.stringify(results) }, + activeNodes: [node, root], + phase: 'results' + }); + addLines(37, 23, 33, 36); + } - const computePositions = useMemo<{ x: number; y: number }[]>(() => { - const children: Record = {}; - for (let i = 0; i < N; i++) { - if (step.parent[i] !== i && step.parent[i] !== undefined) { - const p = step.parent[i]; - if (!children[p]) children[p] = []; - children[p].push(i); - } - } + s.push({ + parent: [...parent], rank: [...rank], results: [...results], + explanation: `Algorithm finished. Final results: [${results.join(', ')}].`, + pseudoStep: `RETURN results`, + variables: { results: JSON.stringify(results) }, + activeNodes: [], + phase: 'done' + }); + addLines(39, 24, 35, 38); + + return { steps: s, stepLineNumbers }; + }, []); + + const currentStep = steps[currentStepIndex] || steps[0]; + const pseudoSteps = steps.map(s => s.pseudoStep); + + const computePositions = useMemo<{ x: number; y: number }[]>(() => { + const children: Record = {}; + for (let i = 0; i < N; i++) { + if (currentStep.parent[i] !== i && currentStep.parent[i] !== undefined) { + const p = currentStep.parent[i]; + if (!children[p]) children[p] = []; + children[p].push(i); + } + } - const rootNodes = Array.from({ length: N }, (_, i) => i).filter(i => step.parent[i] === i || step.parent[i] === undefined); - const pos: { x: number; y: number }[] = new Array(N).fill(null).map(() => ({ x: 0, y: 0 })); - const svgW = 520; - const colW = svgW / (rootNodes.length + 1); - - rootNodes.forEach((root, ri) => { - const rootX = colW * (ri + 1); - pos[root] = { x: rootX, y: 55 }; - - const kids = children[root] || []; - const spread = Math.min(colW * 0.7, 70); - const step_x = kids.length > 1 ? spread / (kids.length - 1) : 0; - const startX = kids.length > 1 ? rootX - spread / 2 : rootX; - - kids.forEach((child, ci) => { - const cx = startX + step_x * ci; - pos[child] = { x: cx, y: 145 }; - const grandkids = children[child] || []; - const gSpread = 50; - const gStep = grandkids.length > 1 ? gSpread / (grandkids.length - 1) : 0; - const gStartX = grandkids.length > 1 ? cx - gSpread / 2 : cx; - grandkids.forEach((gc, gi) => { - pos[gc] = { x: gStartX + gStep * gi, y: 220 }; - }); - }); + const rootNodes = Array.from({ length: N }, (_, i) => i).filter(i => currentStep.parent[i] === i || currentStep.parent[i] === undefined); + const pos: { x: number; y: number }[] = new Array(N).fill(null).map(() => ({ x: 0, y: 0 })); + const svgW = 520; + const colW = svgW / (rootNodes.length + 1); + + rootNodes.forEach((root, ri) => { + const rootX = colW * (ri + 1); + pos[root] = { x: rootX, y: 55 }; + + const kids = children[root] || []; + const spread = Math.min(colW * 0.7, 70); + const step_x = kids.length > 1 ? spread / (kids.length - 1) : 0; + const startX = kids.length > 1 ? rootX - spread / 2 : rootX; + + kids.forEach((child, ci) => { + const cx = startX + step_x * ci; + pos[child] = { x: cx, y: 145 }; + const grandkids = children[child] || []; + const gSpread = 50; + const gStep = grandkids.length > 1 ? gSpread / (grandkids.length - 1) : 0; + const gStartX = grandkids.length > 1 ? cx - gSpread / 2 : cx; + grandkids.forEach((gc, gi) => { + pos[gc] = { x: gStartX + gStep * gi, y: 220 }; }); + }); + }); - return pos; - }, [step.parent]); - - const treeEdges = useMemo(() => { - const edges: { from: number; to: number }[] = []; - for (let i = 0; i < N; i++) { - if (step.parent[i] !== i && step.parent[i] !== undefined) edges.push({ from: i, to: step.parent[i] }); - } - return edges; - }, [step.parent]); - - const getNodeStyle = (i: number) => { - if (step.activeNodes.includes(i)) return { isDefault: false, fill: '#84cc1622', stroke: '#84cc16', text: '#84cc16' }; - if (step.parent[i] === i) return { isDefault: false, fill: '#34d39922', stroke: '#34d399', text: '#34d399' }; - return { isDefault: true, fill: '', stroke: '', text: '' }; - }; + return pos; + }, [currentStep.parent]); - return ( - - -

- Union-Find Forest (Rank + Path Compression) -

- - - {treeEdges.map(({ from, to }, idx) => { - const fpos = computePositions[from]; - const tpos = computePositions[to]; - const active = step.activeNodes.includes(from) || step.activeNodes.includes(to); - return ( - - ); - })} - - {Array.from({ length: N }, (_, i) => i).map((i) => { - const pos = computePositions[i]; - const { isDefault, fill, stroke, text } = getNodeStyle(i); - const isActive = step.activeNodes.includes(i); - const isRoot = step.parent[i] === i; - const r = isActive ? 22 : 18; - return ( - - {isRoot && ( - - ROOT - - )} - - - {i} - - - rank:{step.rank[i]} - - - ); - })} - - -
- - - Root - - - - Active - - - - Node - -
-
- - -

parent[ ] & rank[ ]

-
- {Array.from({ length: N }, (_, i) => i).map((i) => { - const isActive = step.activeNodes.includes(i); - const isRoot = step.parent[i] === i; - return ( - - node {i} - p:{step.parent[i] !== undefined ? step.parent[i] : '?'} - r:{step.rank[i]} - - ); - })} -
-
- -
- -
-

Step

-

{step.explanation}

- - - -
-

Results

-
- {step.results.length === 0 ? No results yet : - step.results.map((res, idx) => ( - - {res} - - )) - } -
- -
-
- } - rightContent={ -
- { + const edges: { from: number; to: number }[] = []; + for (let i = 0; i < N; i++) { + if (currentStep.parent[i] !== i && currentStep.parent[i] !== undefined) edges.push({ from: i, to: currentStep.parent[i] }); + } + return edges; + }, [currentStep.parent]); + + const getNodeStyle = (i: number) => { + if (currentStep.activeNodes.includes(i)) return { isDefault: false, fill: '#84cc1622', stroke: '#84cc16', text: '#84cc16' }; + if (currentStep.parent[i] === i) return { isDefault: false, fill: '#34d39922', stroke: '#34d399', text: '#34d399' }; + return { isDefault: true, fill: '', stroke: '', text: '' }; + }; + + return ( + +
+ +

+ Union-Find Forest (Rank + Path Compression) +

+ + + {treeEdges.map(({ from, to }, idx) => { + const fpos = computePositions[from]; + const tpos = computePositions[to]; + const active = currentStep.activeNodes.includes(from) || currentStep.activeNodes.includes(to); + return ( + - + ); + })} + + {Array.from({ length: N }, (_, i) => i).map((i) => { + const pos = computePositions[i]; + const { isDefault, fill, stroke, text } = getNodeStyle(i); + const isActive = currentStep.activeNodes.includes(i); + const isRoot = currentStep.parent[i] === i; + const r = isActive ? 22 : 18; + return ( + + {isRoot && ( + + ROOT + + )} + + + {i} + + + rank:{currentStep.rank[i]} + + + ); + })} + + +
+ + + Root + + + + Active + + + + Node + +
+
+ + +

parent[ ] & rank[ ]

+
+ {Array.from({ length: N }, (_, i) => i).map((i) => { + const isActive = currentStep.activeNodes.includes(i); + const isRoot = currentStep.parent[i] === i; + return ( + + node {i} + p:{currentStep.parent[i] !== undefined ? currentStep.parent[i] : '?'} + r:{currentStep.rank[i]} + + ); + })} +
+
+
+ +
+
+ +
+

Step

+

{currentStep.explanation}

+ + + +
+

Results

+
+ {currentStep.results.length === 0 ? No results yet : + currentStep.results.map((res, idx) => ( + + {res} + + )) + }
- } - controls={ - - } + +
+ +
+
+ } + rightContent={ + setCurrentStepIndex(0)} + /> + } + controls={ + - ); + } + /> + ); }; diff --git a/src/components/visualizations/algorithms/UnionFindVisualization.tsx b/src/components/visualizations/algorithms/UnionFindVisualization.tsx index defa473..8bfbfb8 100644 --- a/src/components/visualizations/algorithms/UnionFindVisualization.tsx +++ b/src/components/visualizations/algorithms/UnionFindVisualization.tsx @@ -1,17 +1,15 @@ -import { useState, useMemo } from 'react'; -import { SimpleStepControls } from '../shared/SimpleStepControls'; +import { useEffect, useRef, useState, useMemo } from 'react'; +import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; -import { VisualizationLayout } from '../shared/VisualizationLayout'; -import { motion } from 'framer-motion'; -import { Card } from '@/components/ui/card'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { parent: number[]; rank: number[]; result: number; - highlightedLines: number[]; explanation: string; + pseudoStep: string; variables: Record; activeNodes: number[]; phase: 'init' | 'find' | 'union' | 'done'; @@ -20,227 +18,366 @@ interface Step { const N = 6; const EDGES: number[][] = [[0, 1], [0, 2], [3, 4], [3, 5]]; -export const UnionFindVisualization = () => { - const [currentStep, setCurrentStep] = useState(0); - - const code = `function countComponents(n: number, edges: number[][]): number { - const parent: number[] = Array.from({ length: n }, (_, i) => i); - const rank: number[] = new Array(n).fill(1); - - function find(node: number): number { - let res = node; - while (res !== parent[res]) { - parent[res] = parent[parent[res]]; - res = parent[res]; +const languages: VisualizationLanguageMap = { + typescript: `function countComponents(n: number, edges: number[][]): number { + const parent: number[] = Array.from({ length: n }, (_, i) => i); + const rank: number[] = new Array(n).fill(1); + function find(node: number): number { + let res = node; + while (res !== parent[res]) { + parent[res] = parent[parent[res]]; + res = parent[res]; + } + return res; } - return res; - } - - function union(n1: number, n2: number): number { - const p1 = find(n1); - const p2 = find(n2); - - if (p1 === p2) { - return 0; + function union(n1: number, n2: number): number { + const p1 = find(n1); + const p2 = find(n2); + if (p1 === p2) return 0; + if (rank[p2] > rank[p1]) { + parent[p1] = p2; + rank[p2] += rank[p1]; + } else { + parent[p2] = p1; + rank[p1] += rank[p2]; + } + return 1; } - - if (rank[p2] > rank[p1]) { - parent[p1] = p2; - rank[p2] += rank[p1]; - } else { - parent[p2] = p1; - rank[p1] += rank[p2]; + let result = n; + for (const [n1, n2] of edges) { + result -= union(n1, n2); } + return result; +}`, + python: `def countComponents(n: int, edges: list[list[int]]) -> int: + parent = list(range(n)) + rank = [1] * n + def find(node: int) -> int: + res = node + while res != parent[res]: + parent[res] = parent[parent[res]] + res = parent[res] + return res + def union(n1: int, n2: int) -> int: + p1 = find(n1) + p2 = find(n2) + if p1 == p2: + return 0 + if rank[p2] > rank[p1]: + parent[p1] = p2 + rank[p2] += rank[p1] + else: + parent[p2] = p1 + rank[p1] += rank[p2] + return 1 + result = n + for n1, n2 in edges: + result -= union(n1, n2) + return result`, + java: `class Solution { + private int find(int node, int[] parent) { + int res = node; + while (res != parent[res]) { + parent[res] = parent[parent[res]]; + res = parent[res]; + } + return res; + } + private int union(int n1, int n2, int[] parent, int[] rank) { + int p1 = find(n1, parent); + int p2 = find(n2, parent); + if (p1 == p2) return 0; + if (rank[p2] > rank[p1]) { + parent[p1] = p2; + rank[p2] += rank[p1]; + } else { + parent[p2] = p1; + rank[p1] += rank[p2]; + } + return 1; + } + public int countComponents(int n, int[][] edges) { + int[] parent = new int[n]; + int[] rank = new int[n]; + for (int i = 0; i < n; i++) { + parent[i] = i; + rank[i] = 1; + } + int result = n; + for (int[] edge : edges) { + result -= union(edge[0], edge[1], parent, rank); + } + return result; + } +}`, + cpp: `class Solution { +private: + int find(int node, vector& parent) { + int res = node; + while (res != parent[res]) { + parent[res] = parent[parent[res]]; + res = parent[res]; + } + return res; + } + int unite(int n1, int n2, vector& parent, vector& rank) { + int p1 = find(n1, parent); + int p2 = find(n2, parent); + if (p1 == p2) return 0; + if (rank[p2] > rank[p1]) { + parent[p1] = p2; + rank[p2] += rank[p1]; + } else { + parent[p2] = p1; + rank[p1] += rank[p2]; + } + return 1; + } +public: + int countComponents(int n, vector>& edges) { + vector parent(n); + vector rank(n, 1); + for (int i = 0; i < n; i++) parent[i] = i; + int result = n; + for (auto& edge : edges) { + result -= unite(edge[0], edge[1], parent, rank); + } + return result; + } +};`, +}; - return 1; - } - - let result = n; - - for (const [n1, n2] of edges) { - result -= union(n1, n2); - } +function generateVisualizationData() { + const steps: Step[] = []; + const parent = Array.from({ length: N }, (_, i) => i); + const rank = new Array(N).fill(1); + let result = N; + + const stepLineNumbers: StepLineNumberMap = { typescript: [], python: [], java: [], cpp: [] }; + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; - return result; -}`; + steps.push({ + parent: [...parent], rank: [...rank], result, + explanation: `Start countComponents with n=${N} nodes. Result component count initialized to ${N}.`, + pseudoStep: `SET result = n → ${N}`, + variables: { n: N, result }, + activeNodes: [], + phase: 'init' + }); + addLines(25, 22, 30, 29); + + steps.push({ + parent: [...parent], rank: [...rank], result, + explanation: `Initialize parent: each node is its own representative.`, + pseudoStep: `SET parent[i] = i → [${parent.join(',')}]`, + variables: { parent: `[${parent.join(',')}]` }, + activeNodes: parent, + phase: 'init' + }); + addLines(2, 2, 27, 28); + + steps.push({ + parent: [...parent], rank: [...rank], result, + explanation: `Initialize rank: each set starts with size/rank 1.`, + pseudoStep: `SET rank[i] = 1 → [${rank.join(',')}]`, + variables: { rank: `[${rank.join(',')}]` }, + activeNodes: [], + phase: 'init' + }); + addLines(3, 3, 28, 27); + + const findFn = (node: number): number => { + steps.push({ + parent: [...parent], rank: [...rank], result, + explanation: `find(${node}): starting traversal to find root of node ${node}.`, + pseudoStep: `CALL find(${node}): res = ${node}`, + variables: { node, res: node }, + activeNodes: [node], + phase: 'find' + }); + addLines(4, 4, 2, 3); - const steps: Step[] = useMemo(() => { - const s: Step[] = []; - const parent = Array.from({ length: N }, (_, i) => i); - const rank = new Array(N).fill(1); - let result = N; + let res = node; + while (res !== parent[res]) { + const oldParent = parent[res]; + const gParent = parent[oldParent]; + steps.push({ + parent: [...parent], rank: [...rank], result, + explanation: `res=${res} is not root (parent[res]=${oldParent}). Path compression: update parent[${res}] to grandparent ${gParent}.`, + pseudoStep: `parent[${res}] = parent[parent[${res}]] → ${gParent}`, + variables: { res, 'parent[res]': oldParent, 'grandparent': gParent }, + activeNodes: [res, oldParent, gParent], + phase: 'find' + }); + addLines(7, 7, 5, 6); + parent[res] = gParent; + res = parent[res]; + } - s.push({ + steps.push({ parent: [...parent], rank: [...rank], result, - highlightedLines: [1], - explanation: `Start countComponents: n=${N}, result=${result}.`, - variables: { n: N, result }, - activeNodes: [], - phase: 'init' + explanation: `Found root of node ${node} is ${res}.`, + pseudoStep: `RETURN root → ${res}`, + variables: { root: res }, + activeNodes: [res], + phase: 'find' }); + addLines(10, 9, 8, 8); + return res; + }; - s.push({ + const unionFn = (n1: number, n2: number): number => { + steps.push({ parent: [...parent], rank: [...rank], result, - highlightedLines: [2], - explanation: `Initialize parent: each node is its own representative.`, - variables: { parent: `[${parent.join(',')}]` }, - activeNodes: parent, - phase: 'init' + explanation: `union(${n1}, ${n2}): find roots of both nodes to merge.`, + pseudoStep: `CALL union(${n1}, ${n2})`, + variables: { n1, n2 }, + activeNodes: [n1, n2], + phase: 'union' }); + addLines(12, 10, 10, 11); - s.push({ + const p1 = findFn(n1); + const p2 = findFn(n2); + + steps.push({ parent: [...parent], rank: [...rank], result, - highlightedLines: [3], - explanation: `Initialize rank: each set starts with size 1.`, - variables: { rank: `[${rank.join(',')}]` }, - activeNodes: [], - phase: 'init' + explanation: `p1 = find(${n1}) → ${p1}. p2 = find(${n2}) → ${p2}.`, + pseudoStep: `IF p1 (${p1}) == p2 (${p2})`, + variables: { p1, p2 }, + activeNodes: [p1, p2], + phase: 'union' }); + addLines(15, 13, 13, 14); - const findFn = (node: number): number => { - s.push({ - parent: [...parent], rank: [...rank], result, - highlightedLines: [5, 6], - explanation: `find(${node}): starting traversal at res=${node}.`, - variables: { node, res: node }, - activeNodes: [node], - phase: 'find' - }); + if (p1 === p2) return 0; - let res = node; - while (res !== parent[res]) { - const oldParent = parent[res]; - const gParent = parent[oldParent]; - s.push({ - parent: [...parent], rank: [...rank], result, - highlightedLines: [7, 8], - explanation: `res=${res} not root. parent[${res}] = grandparent ${gParent}.`, - variables: { res, 'parent[res]': oldParent, 'grandparent': gParent }, - activeNodes: [res, oldParent, gParent], - phase: 'find' - }); - parent[res] = gParent; - res = parent[res]; - } - - s.push({ - parent: [...parent], rank: [...rank], result, - highlightedLines: [11], - explanation: `Root found = ${res}.`, - variables: { root: res }, - activeNodes: [res], - phase: 'find' - }); - return res; - }; + steps.push({ + parent: [...parent], rank: [...rank], result, + explanation: `p1 and p2 are different roots. Compare ranks: rank[${p2}] (${rank[p2]}) and rank[${p1}] (${rank[p1]}).`, + pseudoStep: `IF rank[${p2}] > rank[${p1}]`, + variables: { p1, p2, [`rank[${p1}]`]: rank[p1], [`rank[${p2}]`]: rank[p2] }, + activeNodes: [p1, p2], + phase: 'union' + }); + addLines(16, 15, 14, 15); - const unionFn = (n1: number, n2: number): number => { - s.push({ + if (rank[p2] > rank[p1]) { + parent[p1] = p2; + rank[p2] += rank[p1]; + steps.push({ parent: [...parent], rank: [...rank], result, - highlightedLines: [14, 15], - explanation: `union(${n1}, ${n2}): finding roots...`, - variables: { n1, n2 }, - activeNodes: [n1, n2], + explanation: `rank[${p2}] > rank[${p1}]. Attach root ${p1} to root ${p2}. rank[${p2}] becomes ${rank[p2]}.`, + pseudoStep: `SET parent[${p1}] = ${p2}; rank[${p2}] += rank[${p1}]`, + variables: { [`parent[${p1}]`]: p2, [`rank[${p2}]`]: rank[p2] }, + activeNodes: [p1, p2], phase: 'union' }); - - const p1 = findFn(n1); - const p2 = findFn(n2); - - s.push({ + addLines(17, 16, 15, 16); + } else { + parent[p2] = p1; + rank[p1] += rank[p2]; + steps.push({ parent: [...parent], rank: [...rank], result, - highlightedLines: [16, 18], - explanation: `p1=${p1}, p2=${p2}. ${p1 === p2 ? 'Already in same component.' : 'Different components — merging.'}`, - variables: { p1, p2 }, + explanation: `rank[${p1}] >= rank[${p2}]. Attach root ${p2} to root ${p1}. rank[${p1}] becomes ${rank[p1]}.`, + pseudoStep: `ELSE: parent[${p2}] = ${p1}; rank[${p1}] += rank[${p2}]`, + variables: { [`parent[${p2}]`]: p1, [`rank[${p1}]`]: rank[p1] }, activeNodes: [p1, p2], phase: 'union' }); + addLines(20, 19, 18, 19); + } + return 1; + }; - if (p1 === p2) return 0; - - if (rank[p2] > rank[p1]) { - parent[p1] = p2; - rank[p2] += rank[p1]; - s.push({ - parent: [...parent], rank: [...rank], result, - highlightedLines: [22, 23, 24], - explanation: `rank[${p2}] > rank[${p1}]. Attach ${p1} to ${p2}. rank[${p2}] updated to ${rank[p2]}.`, - variables: { [`parent[${p1}]`]: p2, [`rank[${p2}]`]: rank[p2] }, - activeNodes: [p1, p2], - phase: 'union' - }); - } else { - parent[p2] = p1; - rank[p1] += rank[p2]; - s.push({ - parent: [...parent], rank: [...rank], result, - highlightedLines: [25, 26, 27], - explanation: `rank[${p1}] >= rank[${p2}]. Attach ${p2} to ${p1}. rank[${p1}] updated to ${rank[p1]}.`, - variables: { [`parent[${p2}]`]: p1, [`rank[${p1}]`]: rank[p1] }, - activeNodes: [p1, p2], - phase: 'union' - }); - } - return 1; - }; - - s.push({ + for (const [n1, n2] of EDGES) { + steps.push({ parent: [...parent], rank: [...rank], result, - highlightedLines: [33], - explanation: `Set result = n = ${N}.`, - variables: { result }, - activeNodes: [], - phase: 'init' + explanation: `Process edge [${n1}, ${n2}] in the graph.`, + pseudoStep: `FOR edge IN edges → inspect [${n1}, ${n2}]`, + variables: { edge: `[${n1},${n2}]`, result }, + activeNodes: [n1, n2], + phase: 'union' }); + addLines(26, 23, 31, 30); - for (const [n1, n2] of EDGES) { - s.push({ - parent: [...parent], rank: [...rank], result, - highlightedLines: [35], - explanation: `Processing edge [${n1}, ${n2}].`, - variables: { edge: `[${n1},${n2}]`, result }, - activeNodes: [n1, n2], - phase: 'union' - }); - const decrement = unionFn(n1, n2); - result -= decrement; - s.push({ - parent: [...parent], rank: [...rank], result, - highlightedLines: [36], - explanation: `Decrementing components by ${decrement}. result = ${result}.`, - variables: { decrement, result }, - activeNodes: [], - phase: 'union' - }); - } + const decrement = unionFn(n1, n2); + result -= decrement; - s.push({ + steps.push({ parent: [...parent], rank: [...rank], result, - highlightedLines: [39], - explanation: `Done! Total components found: ${result}.`, - variables: { result }, + explanation: `Edge processed. Reduced number of connected components by ${decrement}. Current result = ${result}.`, + pseudoStep: `result -= union(${n1}, ${n2}) → ${result}`, + variables: { decrement, result }, activeNodes: [], - phase: 'done' + phase: 'union' }); + addLines(27, 24, 32, 31); + } - return s; - }, []); + steps.push({ + parent: [...parent], rank: [...rank], result, + explanation: `Finished processing all edges. Remaining components: ${result}.`, + pseudoStep: `RETURN result → ${result}`, + variables: { result }, + activeNodes: [], + phase: 'done' + }); + addLines(29, 25, 34, 33); + + return { steps, stepLineNumbers }; +} - const step = steps[currentStep] || steps[0]; +export const UnionFindVisualization = () => { + const [{ steps, stepLineNumbers }] = useState(generateVisualizationData); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); + + useEffect(() => { + if (isPlaying && currentStepIndex < steps.length - 1) { + intervalRef.current = setInterval(() => { + setCurrentStepIndex(prev => { + if (prev >= steps.length - 1) { setIsPlaying(false); return prev; } + return prev + 1; + }); + }, 1000 / speed); + } else { + if (intervalRef.current) clearInterval(intervalRef.current); + } + return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; + }, [isPlaying, currentStepIndex, steps.length, speed]); + + const handlePlay = () => setIsPlaying(true); + const handlePause = () => setIsPlaying(false); + const handleStepForward = () => currentStepIndex < steps.length - 1 && setCurrentStepIndex(p => p + 1); + const handleStepBack = () => currentStepIndex > 0 && setCurrentStepIndex(p => p - 1); + const handleReset = () => { setCurrentStepIndex(0); setIsPlaying(false); }; + + if (steps.length === 0) return null; + + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); - const computePositions = useMemo<{ x: number, y: number }[]>(() => { + const computePositions = useMemo<{ x: number; y: number }[]>(() => { const children: Record = {}; for (let i = 0; i < N; i++) { - if (step.parent[i] !== i) { - if (!children[step.parent[i]]) children[step.parent[i]] = []; - children[step.parent[i]].push(i); + if (currentStep.parent[i] !== i) { + if (!children[currentStep.parent[i]]) children[currentStep.parent[i]] = []; + children[currentStep.parent[i]].push(i); } } - const rootNodes = Array.from({ length: N }, (_, i) => i).filter(i => step.parent[i] === i); - const pos: { x: number, y: number }[] = new Array(N).fill(null).map(() => ({ x: 0, y: 0 })); + const rootNodes = Array.from({ length: N }, (_, i) => i).filter(i => currentStep.parent[i] === i); + const pos: { x: number; y: number }[] = new Array(N).fill(null).map(() => ({ x: 0, y: 0 })); - const svgW = 500; + const svgW = 400; const colW = svgW / (rootNodes.length + 1); rootNodes.forEach((root, idx) => { @@ -248,110 +385,124 @@ export const UnionFindVisualization = () => { pos[root] = { x: rx, y: 50 }; const kids = children[root] || []; - const spread = Math.min(colW * 0.8, 80); + const spread = Math.min(colW * 0.8, 60); const startX = kids.length > 1 ? rx - spread / 2 : rx; const stepX = kids.length > 1 ? spread / (kids.length - 1) : 0; kids.forEach((k, ki) => { const kx = startX + stepX * ki; - pos[k] = { x: kx, y: 140 }; + pos[k] = { x: kx, y: 130 }; const grandkids = children[k] || []; - const gSpread = 50; + const gSpread = 40; const gStartX = grandkids.length > 1 ? kx - gSpread / 2 : kx; const gStepX = grandkids.length > 1 ? gSpread / (grandkids.length - 1) : 0; grandkids.forEach((gk, gi) => { - pos[gk] = { x: gStartX + gStepX * gi, y: 210 }; + pos[gk] = { x: gStartX + gStepX * gi, y: 200 }; }); }); }); return pos; - }, [step.parent]); + }, [currentStep.parent]); const getNodeStyle = (i: number) => { - if (step.activeNodes.includes(i)) return { fill: '#84cc1622', stroke: '#84cc16', text: '#84cc16' }; - if (step.parent[i] === i) return { fill: '#34d39911', stroke: '#34d399', text: '#34d399' }; - return { fill: '#1e1e1e', stroke: '#444444', text: '#eeeeee' }; + if (currentStep.activeNodes.includes(i)) return { fill: 'rgba(132, 204, 22, 0.2)', stroke: '#84cc16', text: '#84cc16' }; + if (currentStep.parent[i] === i) return { fill: 'rgba(52, 211, 153, 0.1)', stroke: '#34d399', text: '#34d399' }; + return { fill: 'transparent', stroke: 'currentColor', text: 'currentColor' }; }; return ( - - -

Disjoint Set Forest

- - - {step.parent.map((p, i) => { +
+ + +
+
+
+

Disjoint Set Forest

+ + {/* Lines */} + {currentStep.parent.map((p, i) => { if (p === i) return null; const src = computePositions[i]; const dst = computePositions[p]; - const active = step.activeNodes.includes(i) || step.activeNodes.includes(p); + if (!src || !dst) return null; + const active = currentStep.activeNodes.includes(i) || currentStep.activeNodes.includes(p); return ( ); })} + {/* Circles */} {Array.from({ length: N }, (_, i) => i).map(i => { - const { x, y } = computePositions[i]; + const pos = computePositions[i]; + if (!pos) return null; const s = getNodeStyle(i); - const isActive = step.activeNodes.includes(i); - const isRoot = step.parent[i] === i; - const r = isActive ? 21 : 18; + const isActive = currentStep.activeNodes.includes(i); + const isRoot = currentStep.parent[i] === i; + const r = isActive ? 15 : 12; return ( - {isRoot && ROOT} + {isRoot && ( + + ROOT + + )} - {i} - rank:{step.rank[i]} + + {i} + + + rank:{currentStep.rank[i]} + ); })} -
- Root - Active - Node +
+ Root + Active + Node
- +
- -
-

Step Explanation

-

{step.explanation}

- +
+

{currentStep.explanation}

+
- +
- } - rightContent={ - - } - controls={ - - } - /> +
+
); }; diff --git a/src/components/visualizations/algorithms/ValidAnagramVisualization.tsx b/src/components/visualizations/algorithms/ValidAnagramVisualization.tsx index 8848db5..038dbf9 100644 --- a/src/components/visualizations/algorithms/ValidAnagramVisualization.tsx +++ b/src/components/visualizations/algorithms/ValidAnagramVisualization.tsx @@ -1,11 +1,13 @@ -import React, { useEffect, useState, useRef } from 'react'; +import React, { useState, useMemo } from 'react'; import { Card } from '@/components/ui/card'; import { motion, AnimatePresence } from 'framer-motion'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; -import { StepControls } from '../shared/StepControls'; import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; +import { SimpleStepControls } from '../shared/SimpleStepControls'; +import { VisualizationLayout } from '../shared/VisualizationLayout'; import { Button } from '@/components/ui/button'; import { CheckCircle2, XCircle } from 'lucide-react'; +import type { VisualizationLanguageMap, StepLineNumberMap } from '@/types/visualization'; interface Step { s: string; @@ -15,217 +17,196 @@ interface Step { i: number; highlightChar?: string; compareChar?: string; - message: string; - lineNumber: number; + explanation: string; + pseudoStep: string; isAnagram?: boolean; } -export const ValidAnagramVisualization: React.FC = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1000); - const intervalRef = useRef(null); - - const cases = [ - { name: "Valid Anagram", s: "anagram", t: "nagaram", icon: }, - { name: "Not an Anagram", s: "rat", t: "car", icon: } - ]; - const [selectedCaseIndex, setSelectedCaseIndex] = useState(0); - - const code = `function isAnagram(s: string, t: string): boolean { +const languages: VisualizationLanguageMap = { + python: `def isAnagram(s: str, t: str) -> bool: + if len(s) != len(t): + return False + s_count = {} + t_count = {} + for i in range(len(s)): + s_count[s[i]] = s_count.get(s[i], 0) + 1 + t_count[t[i]] = t_count.get(t[i], 0) + 1 + for char in s_count: + if s_count[char] != t_count.get(char, 0): + return False + return True`, + + typescript: `function isAnagram(s: string, t: string): boolean { if (s.length !== t.length) { return false; } - const sCount: Record = {}; const tCount: Record = {}; - for (let i = 0; i < s.length; i++) { const charS = s[i]; const charT = t[i]; sCount[charS] = (sCount[charS] || 0) + 1; tCount[charT] = (tCount[charT] || 0) + 1; } - for (const char in sCount) { if (sCount[char] !== tCount[char]) { return false; } } - return true; -}`; +}`, + + java: `public class Solution { + public boolean isAnagram(String s, String t) { + s = s.replaceAll("\\\\s", "").toLowerCase(); + t = t.replaceAll("\\\\s", "").toLowerCase(); + if (s.length() != t.length()) { + return false; + } + int[] count = new int[26]; + for (int i = 0; i < s.length(); i++) { + count[s.charAt(i) - 'a']++; + count[t.charAt(i) - 'a']--; + } + for (int c : count) { + if (c != 0) { + return false; + } + } + return true; + } +}`, + + cpp: `class Solution { +public: + bool isAnagram(string s, string t) { + if (s.length() != t.length()) { + return false; + } + vector count(26, 0); + for (char c : s) { + count[c - 'a']++; + } + for (char c : t) { + count[c - 'a']--; + if (count[c - 'a'] < 0) { + return false; + } + } + return true; + } +};` +}; - const generateSteps = (s: string, t: string) => { - const allSteps: Step[] = []; - const sCount: Record = {}; - const tCount: Record = {}; +const generateSteps = (s: string, t: string) => { + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - allSteps.push({ - s, t, sCount: { ...sCount }, tCount: { ...tCount }, i: -1, - message: `Case: s = "${s}", t = "${t}".`, - lineNumber: 1 - }); + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + const sCount: Record = {}; + const tCount: Record = {}; - allSteps.push({ - s, t, sCount: { ...sCount }, tCount: { ...tCount }, i: -1, - message: `Check if lengths match: s.length (${s.length}) vs t.length (${t.length}).`, - lineNumber: 2 + const addStep = (msg: string, pseudo: string, tsLine: number, pyLine: number, javaLine: number, cppLine: number, extra: Partial = {}) => { + steps.push({ + s, t, + sCount: { ...sCount }, + tCount: { ...tCount }, + i: extra.hasOwnProperty('i') ? extra.i! : -1, + highlightChar: extra.highlightChar, + compareChar: extra.compareChar, + explanation: msg, + pseudoStep: pseudo, + isAnagram: extra.isAnagram }); + addLines(tsLine, pyLine, javaLine, cppLine); + }; - if (s.length !== t.length) { - allSteps.push({ - s, t, sCount: { ...sCount }, tCount: { ...tCount }, i: -1, - message: `Lengths are unequal! They cannot be anagrams.`, - lineNumber: 3, - isAnagram: false - }); - return allSteps; - } + // 1. Initial + addStep(`Case: s = "${s}", t = "${t}".`, "CALL isAnagram(s, t)", 1, 1, 2, 3); - allSteps.push({ - s, t, sCount: { ...sCount }, tCount: { ...tCount }, i: -1, - message: "Initialize frequency map sCount.", - lineNumber: 6 - }); + // 2. Length check + addStep(`Check if lengths match: s.length (${s.length}) vs t.length (${t.length}).`, "IF len(s) != len(t)", 2, 2, 5, 4); - allSteps.push({ - s, t, sCount: { ...sCount }, tCount: { ...tCount }, i: -1, - message: "Initialize frequency map tCount.", - lineNumber: 7 - }); + if (s.length !== t.length) { + addStep("Lengths are unequal! They cannot be anagrams.", "RETURN False", 3, 3, 6, 5, { isAnagram: false }); + return { steps, stepLineNumbers }; + } - for (let i = 0; i < s.length; i++) { - const charS = s[i]; - const charT = t[i]; - - allSteps.push({ - s, t, sCount: { ...sCount }, tCount: { ...tCount }, i, - message: `Iteration i = ${i}: Processing characters.`, - lineNumber: 9 - }); - - allSteps.push({ - s, t, sCount: { ...sCount }, tCount: { ...tCount }, i, - highlightChar: charS, - message: `Extract s[${i}] = '${charS}'.`, - lineNumber: 10 - }); - - allSteps.push({ - s, t, sCount: { ...sCount }, tCount: { ...tCount }, i, - highlightChar: charT, - message: `Extract t[${i}] = '${charT}'.`, - lineNumber: 11 - }); - - sCount[charS] = (sCount[charS] || 0) + 1; - allSteps.push({ - s, t, sCount: { ...sCount }, tCount: { ...tCount }, i, - highlightChar: charS, - message: `Update sCount: Increment count for '${charS}'.`, - lineNumber: 12 - }); - - tCount[charT] = (tCount[charT] || 0) + 1; - allSteps.push({ - s, t, sCount: { ...sCount }, tCount: { ...tCount }, i, - highlightChar: charT, - message: `Update tCount: Increment count for '${charT}'.`, - lineNumber: 13 - }); - } + // 3. Init structures + addStep("Initialize frequency map sCount.", "SET s_count = {}", 5, 4, 8, 7); + addStep("Initialize frequency map tCount.", "SET t_count = {}", 6, 5, 8, 7); - allSteps.push({ - s, t, sCount: { ...sCount }, tCount: { ...tCount }, i: -1, - message: "Counting finished. Now compare frequencies.", - lineNumber: 16 - }); + for (let i = 0; i < s.length; i++) { + const charS = s[i]; + const charT = t[i]; - const chars = Object.keys(sCount); - for (const char of chars) { - allSteps.push({ - s, t, sCount: { ...sCount }, tCount: { ...tCount }, i: -1, - compareChar: char, - message: `Compare count for '${char}': sCount (${sCount[char]}) vs tCount (${tCount[char] || 0}).`, - lineNumber: 17 - }); - - if (sCount[char] !== tCount[char]) { - allSteps.push({ - s, t, sCount: { ...sCount }, tCount: { ...tCount }, i: -1, - compareChar: char, - message: `Mismatch found for '${char}'! Return false.`, - lineNumber: 18, - isAnagram: false - }); - return allSteps; - } - } + addStep(`Iteration i = ${i}: Processing characters.`, `FOR i = ${i} TO len(s) - 1`, 7, 6, 9, 8, { i }); + addStep(`Extract s[${i}] = '${charS}'.`, `GET s[${i}]`, 8, 6, 10, 9, { i, highlightChar: charS }); + addStep(`Extract t[${i}] = '${charT}'.`, `GET t[${i}]`, 9, 6, 11, 11, { i, highlightChar: charT }); - allSteps.push({ - s, t, sCount: { ...sCount }, tCount: { ...tCount }, i: -1, - message: "All counts match! Return true.", - lineNumber: 22, - isAnagram: true - }); + sCount[charS] = (sCount[charS] || 0) + 1; + addStep(`Update sCount: Increment count for '${charS}'.`, `SET s_count['${charS}'] = s_count['${charS}'] + 1`, 10, 7, 10, 9, { i, highlightChar: charS }); - return allSteps; - }; + tCount[charT] = (tCount[charT] || 0) + 1; + addStep(`Update tCount: Increment count for '${charT}'.`, `SET t_count['${charT}'] = t_count['${charT}'] + 1`, 11, 8, 11, 12, { i, highlightChar: charT }); + } + + addStep("Counting finished. Now compare frequencies.", "FOR char IN s_count", 13, 9, 13, 11); + + const chars = Object.keys(sCount); + for (const char of chars) { + addStep(`Compare count for '${char}': sCount (${sCount[char]}) vs tCount (${tCount[char] || 0}).`, `IF s_count['${char}'] != t_count['${char}']`, 14, 10, 14, 13, { compareChar: char }); - useEffect(() => { - const selectedCase = cases[selectedCaseIndex]; - setSteps(generateSteps(selectedCase.s, selectedCase.t)); - }, [selectedCaseIndex]); - - useEffect(() => { - if (isPlaying && currentStepIndex < steps.length - 1) { - intervalRef.current = window.setInterval(() => { - setCurrentStepIndex((prev) => { - if (prev >= steps.length - 1) { - setIsPlaying(false); - return prev; - } - return prev + 1; - }); - }, speed); - } else if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; + if (sCount[char] !== tCount[char]) { + addStep(`Mismatch found for '${char}'! Return false.`, "RETURN False", 15, 11, 15, 14, { compareChar: char, isAnagram: false }); + return { steps, stepLineNumbers }; } - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - }; - }, [isPlaying, currentStepIndex, steps.length, speed]); + } + + addStep("All counts match! Return true.", "RETURN True", 18, 12, 18, 17, { isAnagram: true }); + + return { steps, stepLineNumbers }; +}; + +export const ValidAnagramVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + + const cases = [ + { name: "Valid Anagram", s: "anagram", t: "nagaram", icon: }, + { name: "Not an Anagram", s: "rat", t: "car", icon: } + ]; + const [selectedCaseIndex, setSelectedCaseIndex] = useState(0); + const selectedCase = cases[selectedCaseIndex]; + + const { steps, stepLineNumbers } = useMemo(() => { + return generateSteps(selectedCase.s, selectedCase.t); + }, [selectedCase]); + + if (steps.length === 0) return null; + + const currentStep = steps[currentStepIndex] || steps[steps.length - 1]; + const pseudoSteps = steps.map(s => s.pseudoStep); const handleCaseChange = (index: number) => { - if (index === selectedCaseIndex) return; setSelectedCaseIndex(index); setCurrentStepIndex(0); - setIsPlaying(false); - }; - - const currentStep = steps[currentStepIndex] || { - s: "", t: "", sCount: {}, tCount: {}, i: -1, message: "Initializing...", lineNumber: 1 }; return (
-
- setIsPlaying(true)} - onPause={() => setIsPlaying(false)} - onStepForward={() => setCurrentStepIndex(prev => Math.min(steps.length - 1, prev + 1))} - onStepBack={() => setCurrentStepIndex(prev => Math.max(0, prev - 1))} - onReset={() => { setCurrentStepIndex(0); setIsPlaying(false); }} - isPlaying={isPlaying} - currentStep={currentStepIndex} - totalSteps={steps.length - 1} - speed={speed} - onSpeedChange={setSpeed} - /> - + {/* Case selections / Controls at Top */} +
{cases.map((testCase, idx) => ( ))}
+
+ +
-
-
-
-
-

String s

-
- {currentStep.s.split('').map((char, idx) => ( -
- {char} -
- ))} -
-
- -
-

String t

-
- {currentStep.t.split('').map((char, idx) => ( -
- {char} + + +
+
+

String s

+
+ {currentStep.s.split('').map((char, idx) => ( +
+ {char} +
+ ))}
- ))} -
-
-
- -
- -

sCount

-
- - {Object.keys(currentStep.sCount).length === 0 ? ( -

Empty

- ) : ( - Object.entries(currentStep.sCount).map(([char, count]) => ( - + +
+

String t

+
+ {currentStep.t.split('').map((char, idx) => ( +
- '{char}' - {count} - - )) - )} - + {char} +
+ ))} +
+
-
- -

tCount

-
- - {Object.keys(currentStep.tCount).length === 0 ? ( -

Empty

- ) : ( - Object.entries(currentStep.tCount).map(([char, count]) => ( - - '{char}' - {count} - - )) - )} -
+
+ +

sCount

+
+ + {Object.keys(currentStep.sCount).length === 0 ? ( +

Empty

+ ) : ( + Object.entries(currentStep.sCount).map(([char, count]) => ( + + '{char}' + {count} + + )) + )} +
+
+
+ + +

tCount

+
+ + {Object.keys(currentStep.tCount).length === 0 ? ( +

Empty

+ ) : ( + Object.entries(currentStep.tCount).map(([char, count]) => ( + + '{char}' + {count} + + )) + )} +
+
+
-
+ {/* Descriptive Commentary Box (at the bottom) */} +
+
+
+ Process Step +
+ {currentStep.explanation} +
-
-

{currentStep.message}

+ {/* Variable Panel (below the commentary box) */} +
+ +
- - setCurrentStepIndex(0)} /> -
- - -
+ } + />
); }; diff --git a/src/components/visualizations/algorithms/ValidPalindromeVisualization.tsx b/src/components/visualizations/algorithms/ValidPalindromeVisualization.tsx index ce08c66..7df1fdf 100644 --- a/src/components/visualizations/algorithms/ValidPalindromeVisualization.tsx +++ b/src/components/visualizations/algorithms/ValidPalindromeVisualization.tsx @@ -1,9 +1,11 @@ -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import { Card } from '@/components/ui/card'; import { CheckCircle2, XCircle } from 'lucide-react'; import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { SimpleStepControls } from '../shared/SimpleStepControls'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationLayout } from '../shared/VisualizationLayout'; +import type { VisualizationLanguageMap, StepLineNumberMap } from '@/types/visualization'; interface Step { s: string; @@ -11,16 +13,31 @@ interface Step { r: number; currentCharL?: string; currentCharR?: string; - comment: string; - highlightedLines: number[]; + explanation: string; + pseudoStep: string; isValid?: boolean; } -export const ValidPalindromeVisualization = () => { - const code = `function isPalindrome(s: string): boolean { +const languages: VisualizationLanguageMap = { + python: `def isPalindrome(s: str) -> bool: + def alphaNum(c: str) -> bool: + return c.isalnum() + l = 0 + r = len(s) - 1 + while l < r: + while l < r and not alphaNum(s[l]): + l += 1 + while r > l and not alphaNum(s[r]): + r -= 1 + if s[l].lower() != s[r].lower(): + return False + l += 1 + r -= 1 + return True`, + + typescript: `function isPalindrome(s: string): boolean { let l = 0; let r = s.length - 1; - while (l < r) { while (l < r && !alphaNum(s[l])) { l++; @@ -44,62 +61,160 @@ function alphaNum(c: string): boolean { (code >= 97 && code <= 122) || (code >= 48 && code <= 57) ); -}`; - - const cases = { - valid: { - steps: [ - { s: "race car", l: 0, r: 7, comment: "Initialize pointers at the start (l=0) and end (r=7).", highlightedLines: [2, 3] }, - { s: "race car", l: 0, r: 7, comment: "Check main loop condition: 0 < 7 is true.", highlightedLines: [5] }, - { s: "race car", l: 0, r: 7, currentCharL: 'r', comment: "Left pointer check: 'r' is alphanumeric, so we skip the l-skip loop.", highlightedLines: [6] }, - { s: "race car", l: 0, r: 7, currentCharR: 'r', comment: "Right pointer check: 'r' is alphanumeric, so we skip the r-skip loop.", highlightedLines: [9] }, - { s: "race car", l: 0, r: 7, currentCharL: 'r', currentCharR: 'r', comment: "Compare 'r' and 'r' (case-insensitive). They match!", highlightedLines: [12] }, - { s: "race car", l: 1, r: 6, comment: "Matching characters found! Increment left and decrement right.", highlightedLines: [15, 16] }, - { s: "race car", l: 1, r: 6, comment: "Next iteration: 1 < 6 is true.", highlightedLines: [5] }, - { s: "race car", l: 1, r: 6, currentCharL: 'a', comment: "Left pointer at 'a' is alphanumeric.", highlightedLines: [6] }, - { s: "race car", l: 1, r: 6, currentCharR: 'a', comment: "Right pointer at 'a' is alphanumeric.", highlightedLines: [9] }, - { s: "race car", l: 1, r: 6, currentCharL: 'a', currentCharR: 'a', comment: "Characters 'a' and 'a' match.", highlightedLines: [12] }, - { s: "race car", l: 2, r: 5, comment: "Increment l, decrement r.", highlightedLines: [15, 16] }, - { s: "race car", l: 2, r: 5, comment: "Next iteration: 2 < 5 is true.", highlightedLines: [5] }, - { s: "race car", l: 2, r: 5, currentCharL: 'c', comment: "Left pointer at 'c' is alphanumeric.", highlightedLines: [6] }, - { s: "race car", l: 2, r: 5, currentCharR: 'c', comment: "Right pointer at 'c' is alphanumeric.", highlightedLines: [9] }, - { s: "race car", l: 2, r: 5, currentCharL: 'c', currentCharR: 'c', comment: "Characters 'c' and 'c' match.", highlightedLines: [12] }, - { s: "race car", l: 3, r: 4, comment: "Increment l, decrement r.", highlightedLines: [15, 16] }, - { s: "race car", l: 3, r: 4, comment: "Next iteration: 3 < 4 is true.", highlightedLines: [5] }, - { s: "race car", l: 3, r: 4, currentCharL: 'e', comment: "Left pointer at 'e' is alphanumeric.", highlightedLines: [6] }, - { s: "race car", l: 3, r: 4, currentCharR: ' ', comment: "Right pointer check: space (' ') is NOT alphanumeric.", highlightedLines: [9] }, - { s: "race car", l: 3, r: 3, currentCharR: ' ', comment: "Decrement right pointer to skip the space.", highlightedLines: [10] }, - { s: "race car", l: 3, r: 3, comment: "End of r-skip loop: r is no longer greater than l.", highlightedLines: [9] }, - { s: "race car", l: 3, r: 3, currentCharL: 'e', currentCharR: 'e', comment: "Compare 'e' and 'e'. Match!", highlightedLines: [12] }, - { s: "race car", l: 4, r: 2, comment: "Increment l, decrement r.", highlightedLines: [15, 16] }, - { s: "race car", l: 4, r: 2, comment: "Check main loop: 4 < 2 is false. Pointers have crossed.", highlightedLines: [5] }, - { s: "race car", l: 4, r: 2, isValid: true, comment: "The string is a valid palindrome!", highlightedLines: [18] } - ] - }, - invalid: { - steps: [ - { s: "race a car", l: 0, r: 9, comment: "Initialize pointers: l=0, r=9.", highlightedLines: [2, 3] }, - { s: "race a car", l: 0, r: 9, comment: "0 < 9 is true.", highlightedLines: [5] }, - { s: "race a car", l: 0, r: 9, currentCharL: 'r', currentCharR: 'r', comment: "Match 'r' at ends.", highlightedLines: [12] }, - { s: "race a car", l: 1, r: 8, comment: "Move pointers center-ward.", highlightedLines: [15, 16] }, - { s: "race a car", l: 1, r: 8, comment: "Next iteration: 1 < 8.", highlightedLines: [5] }, - { s: "race a car", l: 1, r: 8, currentCharL: 'a', currentCharR: 'a', comment: "Match 'a' at next position.", highlightedLines: [12] }, - { s: "race a car", l: 2, r: 7, comment: "Move pointers.", highlightedLines: [15, 16] }, - { s: "race a car", l: 2, r: 7, comment: "Next iteration: 2 < 7.", highlightedLines: [5] }, - { s: "race a car", l: 2, r: 7, currentCharL: 'c', currentCharR: 'c', comment: "Match 'c'.", highlightedLines: [12] }, - { s: "race a car", l: 3, r: 6, comment: "Move pointers.", highlightedLines: [15, 16] }, - { s: "race a car", l: 3, r: 6, comment: "Next iteration: 3 < 6.", highlightedLines: [5] }, - { s: "race a car", l: 3, r: 6, currentCharL: 'e', currentCharR: 'a', comment: "Compare 'e' and 'a'. No match!", highlightedLines: [12] }, - { s: "race a car", l: 3, r: 6, isValid: false, comment: "Found a mismatch. Return false.", highlightedLines: [13] } - ] +}`, + + java: `public class Solution { + public boolean isPalindrome(String s) { + int l = 0; + int r = s.length - 1; + while (l < r) { + while (l < r && !alphaNum(s.charAt(l))) { + l++; + } + while (r > l && !alphaNum(s.charAt(r))) { + r--; + } + if (s.charAt(l) != s.charAt(r) && Character.toLowerCase(s.charAt(l)) != Character.toLowerCase(s.charAt(r))) { + return false; + } + l++; + r--; + } + return true; } + + private boolean alphaNum(char c) { + return Character.isLetterOrDigit(c); + } +}`, + + cpp: `class Solution { +public: + bool isPalindrome(string s) { + int left = 0, right = s.length() - 1; + while (left < right) { + while (left < right && !isalnum(s[left])) { + left++; + } + while (left < right && !isalnum(s[right])) { + right--; + } + if (tolower(s[left]) != tolower(s[right])) { + return false; + } + left++; + right--; + } + return true; + } +};` +}; + +const generateStepsData = (s: string) => { + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + const alphaNum = (c: string) => { + const code = c.charCodeAt(0); + return ( + (code >= 65 && code <= 90) || + (code >= 97 && code <= 122) || + (code >= 48 && code <= 57) + ); + }; + + let l = 0; + let r = s.length - 1; + + const addStep = (msg: string, pseudo: string, tsLine: number, pyLine: number, javaLine: number, cppLine: number, extra: Partial = {}) => { + steps.push({ + s, + l, + r, + currentCharL: extra.currentCharL, + currentCharR: extra.currentCharR, + explanation: msg, + pseudoStep: pseudo, + isValid: extra.isValid + }); + addLines(tsLine, pyLine, javaLine, cppLine); }; + // 1. Initial State + addStep("Initialize left and right pointers at both ends of the string.", "SET l = 0, r = len(s) - 1", 2, 4, 3, 4); + + while (l < r) { + // 2. Loop check + addStep(`Loop check: is left (${l}) < right (${r})?`, `WHILE l < r → ${l} < ${r}`, 4, 6, 5, 5); + + // 3. Skip left non-alphanumeric + addStep(`Check if character at left pointer s[${l}] ('${s[l]}') is alphanumeric.`, "WHILE l < r AND NOT alphaNum(s[l])", 5, 7, 6, 6, { currentCharL: s[l] }); + while (l < r && !alphaNum(s[l])) { + l++; + addStep(`s[${l - 1}] is non-alphanumeric. Increment left pointer to ${l}.`, "SET l = l + 1", 6, 8, 7, 7); + addStep(`Check if character at left pointer s[${l}] ('${s[l]}') is alphanumeric.`, "WHILE l < r AND NOT alphaNum(s[l])", 5, 7, 6, 6, { currentCharL: s[l] }); + } + + // 4. Skip right non-alphanumeric + addStep(`Check if character at right pointer s[${r}] ('${s[r]}') is alphanumeric.`, "WHILE r > l AND NOT alphaNum(s[r])", 8, 9, 9, 9, { currentCharR: s[r] }); + while (r > l && !alphaNum(s[r])) { + r--; + addStep(`s[${r + 1}] is non-alphanumeric. Decrement right pointer to ${r}.`, "SET r = r - 1", 9, 10, 10, 10); + addStep(`Check if character at right pointer s[${r}] ('${s[r]}') is alphanumeric.`, "WHILE r > l AND NOT alphaNum(s[r])", 8, 9, 9, 9, { currentCharR: s[r] }); + } + + // 5. Compare characters + const charL = s[l].toLowerCase(); + const charR = s[r].toLowerCase(); + addStep( + `Compare characters (case-insensitive): '${charL}' vs '${charR}'.`, + `IF s[l].lower() != s[r].lower() → '${charL}' != '${charR}'`, + 11, 11, 12, 12, + { currentCharL: s[l], currentCharR: s[r] } + ); + + if (charL !== charR) { + addStep("Characters do not match! The string is not a palindrome.", "RETURN False", 12, 12, 13, 13, { isValid: false }); + return { steps, stepLineNumbers }; + } + + // 6. Move pointers center-ward + l++; + r--; + addStep(`Match! Move both pointers center-ward. New left = ${l}, right = ${r}.`, "SET l = l + 1, r = r - 1", 14, 13, 15, 15); + } + + // 7. Complete final check + addStep("Pointers crossed. All compared character pairs matched successfully.", "RETURN True", 17, 15, 18, 18, { isValid: true }); + + return { steps, stepLineNumbers }; +}; + +export const ValidPalindromeVisualization = () => { const [caseId, setCaseId] = useState<'valid' | 'invalid'>('valid'); const [currentStepIndex, setCurrentStepIndex] = useState(0); - const steps = cases[caseId].steps; - const currentStep = steps[Math.min(currentStepIndex, steps.length - 1)]; + const { steps, stepLineNumbers } = useMemo(() => { + const targetStr = caseId === 'valid' ? "race car" : "race a car"; + return generateStepsData(targetStr); + }, [caseId]); + + if (steps.length === 0) return null; + + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); const handleCaseChange = (newCase: 'valid' | 'invalid') => { setCaseId(newCase); @@ -108,92 +223,111 @@ function alphaNum(c: string): boolean { return (
-
- - -
+ {/* Case selections / Controls at Top */} +
+
+
+ +
-
-
- -
-

String Inspection

-
- {currentStep.s.split('').map((char, idx) => { - const isLeft = idx === currentStep.l; - const isRight = idx === currentStep.r; - return ( -
currentStep.r - ? 'bg-muted border-transparent text-muted-foreground opacity-50' - : 'bg-card border-border text-foreground' - }`} - > - {char} -
- ); - })} -
-
-
-
- Left Pointer -
-
-
- Right Pointer + + +
+

String Inspection

+
+ {currentStep.s.split('').map((char, idx) => { + const isLeft = idx === currentStep.l; + const isRight = idx === currentStep.r; + return ( +
currentStep.r + ? 'bg-muted border-transparent text-muted-foreground opacity-50' + : 'bg-card border-border text-foreground' + }`} + > + {char} +
+ ); + })}
-
-
- Meeting Point +
+
+
+ Left Pointer +
+
+
+ Right Pointer +
+
+
+ Meeting Point +
-
-
-
-

- Step {currentStepIndex}: - {currentStep.comment} -

+ {currentStep.isValid !== undefined && ( +
+ + {currentStep.isValid ? '✓ Valid Palindrome' : '✗ Not a Palindrome'} + +
+ )} + + + {/* Descriptive Commentary Box (at the bottom) */} +
+
+
+ Process Step
+ {currentStep.explanation} +
+ {/* Variable Panel (below the commentary box) */} +
- - {currentStep.isValid !== undefined && ( -
- - {currentStep.isValid ? '✓ Valid Palindrome' : '✗ Not a Palindrome'} - -
- )}
- -
- -
- + } + rightContent={ + setCurrentStepIndex(0)} /> -
-
+ } + />
); }; - - diff --git a/src/components/visualizations/algorithms/ValidParenthesesVisualization.tsx b/src/components/visualizations/algorithms/ValidParenthesesVisualization.tsx index 58cace8..0d96b04 100644 --- a/src/components/visualizations/algorithms/ValidParenthesesVisualization.tsx +++ b/src/components/visualizations/algorithms/ValidParenthesesVisualization.tsx @@ -1,9 +1,11 @@ -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import { Card } from '@/components/ui/card'; import { motion, AnimatePresence } from 'framer-motion'; import { VariablePanel } from '../shared/VariablePanel'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { SimpleStepControls } from '../shared/SimpleStepControls'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationLayout } from '../shared/VisualizationLayout'; +import type { VisualizationLanguageMap, StepLineNumberMap } from '@/types/visualization'; interface Step { s: string; @@ -12,22 +14,37 @@ interface Step { currentChar?: string; action: string; isValid?: boolean; - message: string; - highlightedLines: number[]; + explanation: string; + pseudoStep: string; } -export const ValidParenthesesVisualization = () => { - const code = `function isValid(s: string): boolean { +const languages: VisualizationLanguageMap = { + python: `def isValid(s: str) -> bool: + stack = [] + closeToOpen = { + ")": "(", + "]": "[", + "}": "{" + } + for c in s: + if c in closeToOpen: + if stack and stack[-1] == closeToOpen[c]: + stack.pop() + else: + return False + else: + stack.append(c) + return len(stack) == 0`, + + typescript: `function isValid(s: string): boolean { const stack: string[] = []; const map: Record = { ')': '(', '}': '{', ']': '[' }; - for (let i = 0; i < s.length; i++) { const char = s[i]; - if (char in map) { if (stack.length === 0 || stack[stack.length - 1] !== map[char]) { return false; @@ -37,265 +54,176 @@ export const ValidParenthesesVisualization = () => { stack.push(char); } } - return stack.length === 0; -}`; +}`, + + java: `public class Solution { + public boolean isValid(String s) { + if (s == null || s.length() == 0) { + return true; + } + Stack stack = new Stack<>(); + HashMap map = new HashMap<>(); + map.put(')', '('); + map.put(']', '['); + map.put('}', '{'); + for (char c : s.toCharArray()) { + if (map.containsKey(c)) { + if (!stack.isEmpty() && stack.peek().equals(map.get(c))) { + stack.pop(); + } else { + return false; + } + } else { + stack.push(c); + } + } + return stack.isEmpty(); + } +}`, + + cpp: `class Solution { +public: + bool isValid(string s) { + stack st; + unordered_map mapping = { + {')', '('}, {'}', '{'}, {']', '['} + }; + for (char c : s) { + if (mapping.find(c) != mapping.end()) { + if (st.empty() || st.top() != mapping[c]) { + return false; + } + st.pop(); + } else { + st.push(c); + } + } + return st.empty(); + } +};` +}; + +const generateSteps = (s: string) => { + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + const stack: string[] = []; + const map: Record = { + ')': '(', + '}': '{', + ']': '[' + }; + + const addStep = (msg: string, pseudo: string, tsLine: number, pyLine: number, javaLine: number, cppLine: number, extra: Partial = {}) => { + steps.push({ + s, + idx: extra.hasOwnProperty('idx') ? extra.idx! : -1, + stack: [...stack], + currentChar: extra.currentChar, + action: extra.action || "processing", + isValid: extra.isValid, + explanation: msg, + pseudoStep: pseudo + }); + addLines(tsLine, pyLine, javaLine, cppLine); + }; + + // 1. Initial State + addStep("Initialize an empty stack to track unmatched opening brackets.", "SET stack = []", 2, 2, 6, 4); + + // 2. Define map + addStep("Define lookup map for closing-to-opening bracket matches.", "SET map = { ')':'(', '}':'{', ']':'[' }", 3, 3, 7, 5); + + let i = 0; + for (i = 0; i < s.length; i++) { + const char = s[i]; - const steps: Step[] = [ - { - s: "({[]})", - idx: -1, - stack: [], - action: "initialization", - message: "First, we initialize an empty stack to keep track of opening brackets as we encounter them.", - highlightedLines: [2] - }, - { - s: "({[]})", - idx: -1, - stack: [], - action: "initialization", - message: "We also define a map that links each closing bracket to its corresponding opening bracket.", - highlightedLines: [3, 4, 5, 6, 7] - }, - // Iteration 0: '(' - { - s: "({[]})", - idx: 0, - stack: [], - currentChar: "(", - action: "loop", - message: "Iteration 0: We start the loop for the first character in the string.", - highlightedLines: [9] - }, - { - s: "({[]})", - idx: 0, - stack: [], - currentChar: "(", - action: "assignment", - message: "The current character is '('.", - highlightedLines: [10] - }, - { - s: "({[]})", - idx: 0, - stack: [], - currentChar: "(", - action: "checking", - message: "We check if '(' is a closing bracket by looking it up in our 'map'.", - highlightedLines: [12] - }, - { - s: "({[]})", - idx: 0, - stack: [], - currentChar: "(", - action: "pushing", - message: "'(' is not in the map (it's an opening bracket), so we move to the else block to push it onto the stack.", - highlightedLines: [17, 18] - }, - { - s: "({[]})", - idx: 0, - stack: ["("], - currentChar: "(", - action: "pushed", - message: "The stack now contains: ['('].", - highlightedLines: [18] - }, - // Iteration 1: '{' - { - s: "({[]})", - idx: 1, - stack: ["("], - currentChar: "{", - action: "loop", - message: "Iteration 1: Moving to the next character in the string.", - highlightedLines: [9] - }, - { - s: "({[]})", - idx: 1, - stack: ["("], - currentChar: "{", - action: "assignment", - message: "The current character is now '{'.", - highlightedLines: [10] - }, - { - s: "({[]})", - idx: 1, - stack: ["("], - currentChar: "{", - action: "checking", - message: "Checking if '{' is a closing bracket. It's not.", - highlightedLines: [12] - }, - { - s: "({[]})", - idx: 1, - stack: ["(", "{"], - currentChar: "{", - action: "pushed", - message: "'{' is pushed onto the stack. Stack: ['(', '{'].", - highlightedLines: [18] - }, - // Iteration 2: '[' - { - s: "({[]})", - idx: 2, - stack: ["(", "{"], - currentChar: "[", - action: "loop", - message: "Iteration 2: Next character.", - highlightedLines: [9] - }, - { - s: "({[]})", - idx: 2, - stack: ["(", "{", "["], - currentChar: "[", - action: "pushed", - message: "'[' is an opening bracket, so we push it. Stack: ['(', '{', '['].", - highlightedLines: [18] - }, - // Iteration 3: ']' - { - s: "({[]})", - idx: 3, - stack: ["(", "{", "["], - currentChar: "]", - action: "loop", - message: "Iteration 3: Reached a closing bracket ']'.", - highlightedLines: [9, 10] - }, - { - s: "({[]})", - idx: 3, - stack: ["(", "{", "["], - currentChar: "]", - action: "checking", - message: "']' IS in the map, so we enter the matching logic.", - highlightedLines: [12] - }, - { - s: "({[]})", - idx: 3, - stack: ["(", "{", "["], - currentChar: "]", - action: "validating", - message: "We check if the stack is empty or if the top element matches the opening bracket for ']'.", - highlightedLines: [13] - }, - { - s: "({[]})", - idx: 3, - stack: ["(", "{", "["], - currentChar: "]", - action: "validating", - message: "The top of the stack is '[', which matches map[']'].", - highlightedLines: [13] - }, - { - s: "({[]})", - idx: 3, - stack: ["(", "{"], - currentChar: "]", - action: "popping", - message: "Match found! We pop '[' from the stack.", - highlightedLines: [16] - }, - // Iteration 4: '}' - { - s: "({[]})", - idx: 4, - stack: ["(", "{"], - currentChar: "}", - action: "checking", - message: "Iteration 4: char='}'. It's in the map.", - highlightedLines: [12] - }, - { - s: "({[]})", - idx: 4, - stack: ["(", "{"], - currentChar: "}", - action: "validating", - message: "Stack top '{' matches map['}'].", - highlightedLines: [13] - }, - { - s: "({[]})", - idx: 4, - stack: ["("], - currentChar: "}", - action: "popping", - message: "Popping '{'. Stack now: ['('].", - highlightedLines: [16] - }, - // Iteration 5: ')' - { - s: "({[]})", - idx: 5, - stack: ["("], - currentChar: ")", - action: "checking", - message: "Iteration 5: char=')'. Matches stack top '('.", - highlightedLines: [12, 13] - }, - { - s: "({[]})", - idx: 5, - stack: [], - currentChar: ")", - action: "popping", - message: "Popping '('. The stack is now empty.", - highlightedLines: [16] - }, - // Completion - { - s: "({[]})", - idx: 6, - stack: [], - action: "loop end", - message: "The string has been fully processed.", - highlightedLines: [20] - }, - { - s: "({[]})", - idx: 6, - stack: [], - isValid: true, - action: "final check", - message: "Since the stack is empty, all parentheses were correctly matched. Return true.", - highlightedLines: [22] + // 3. Loop start + addStep(`Iteration ${i}: Process character '${char}'.`, `FOR c IN s [i = ${i}]`, 8, 8, 11, 8, { idx: i, currentChar: char, action: "loop" }); + + // 4. Check if char in map + addStep(`Check if '${char}' is a closing bracket.`, `IF c IN map`, 10, 9, 12, 9, { idx: i, currentChar: char, action: "checking" }); + + if (char in map) { + // 5. Check stack top + const expected = map[char]; + const actual = stack[stack.length - 1]; + const match = stack.length > 0 && actual === expected; + + addStep( + `Validate stack top: expected opening '${expected}', found '${actual || "empty"}'.`, + `IF stack AND stack[-1] == map[c]`, + 11, 10, 13, 10, + { idx: i, currentChar: char, action: "checking" } + ); + + if (match) { + stack.pop(); + addStep(`Match found! Pop '${expected}' from stack.`, "stack.pop()", 14, 11, 14, 13, { idx: i, currentChar: char, action: "popping" }); + } else { + addStep(`Mismatch or empty stack for '${char}'! Return false.`, "RETURN False", 12, 13, 16, 11, { idx: i, currentChar: char, action: "checking", isValid: false }); + return { steps, stepLineNumbers }; + } + } else { + // 6. Push opening bracket + stack.push(char); + addStep(`'${char}' is an opening bracket. Push onto stack.`, "stack.append(c)", 16, 15, 19, 15, { idx: i, currentChar: char, action: "pushing" }); } - ]; + } + + // 7. Loop complete + addStep("All characters processed. Check if stack is empty.", "IF len(stack) == 0", 19, 16, 22, 18); + + const isValid = stack.length === 0; + addStep( + isValid ? "Stack is empty. All brackets matched successfully!" : `Stack has unmatched opening brackets: ${stack.join(', ')}`, + `RETURN len(stack) == 0 → ${isValid}`, + 19, 16, 22, 18, + { isValid } + ); + return { steps, stepLineNumbers }; +}; +export const ValidParenthesesVisualization = () => { const [currentStepIndex, setCurrentStepIndex] = useState(0); + const s = "({[]})"; + + const { steps, stepLineNumbers } = useMemo(() => { + return generateSteps(s); + }, [s]); + + if (steps.length === 0) return null; + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map(s => s.pseudoStep); return ( -
- - -
+
-

Input String

+

Input String

{currentStep.s.split('').map((char, idx) => (
{
-

Stack

-
+

Stack

+
{currentStep.stack.length > 0 ? (
@@ -317,9 +245,9 @@ export const ValidParenthesesVisualization = () => { @@ -328,7 +256,7 @@ export const ValidParenthesesVisualization = () => { ))}
) : ( -
+
Stack is empty
)} @@ -336,46 +264,57 @@ export const ValidParenthesesVisualization = () => {
-
-
-

- Step {currentStepIndex}: - {currentStep.message} -

+ {currentStep.isValid !== undefined && ( +
+ + {currentStep.isValid ? '✓ Valid Parentheses' : '✗ Invalid Structure'} +
+ )} + - - - {currentStep.isValid !== undefined && ( -
- - {currentStep.isValid ? '✓ Valid Parentheses' : '✗ Invalid Structure'} - -
- )} + {/* Descriptive Commentary Box (at the bottom) */} +
+
+
+ Process Step
- -
+ {currentStep.explanation} +
-
- + {/* Variable Panel (below the commentary box) */} +
+ +
-
-
+ } + rightContent={ + setCurrentStepIndex(0)} + /> + } + controls={ + + } + /> ); }; \ No newline at end of file diff --git a/src/components/visualizations/algorithms/WordBreakVisualization.tsx b/src/components/visualizations/algorithms/WordBreakVisualization.tsx index 3bd7bd5..0a646b1 100644 --- a/src/components/visualizations/algorithms/WordBreakVisualization.tsx +++ b/src/components/visualizations/algorithms/WordBreakVisualization.tsx @@ -2,8 +2,9 @@ import React, { useState, useMemo } from 'react'; import { Card } from '@/components/ui/card'; import { SimpleStepControls } from '../shared/SimpleStepControls'; import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from '../shared/AnimatedCodeEditor'; +import { VisualizationCodePanel } from '../shared/VisualizationCodePanel'; import { VisualizationLayout } from '../shared/VisualizationLayout'; +import type { StepLineNumberMap, VisualizationLanguageMap } from '@/types/visualization'; interface Step { s: string; @@ -13,20 +14,14 @@ interface Step { currentSubstring: string; variables: Record; explanation: string; - lineExecution: string; - highlightedLines: number[]; + pseudoStep: string; } -export const WordBreakVisualization: React.FC = () => { - const [currentStep, setCurrentStep] = useState(0); - - const s = "leetcode"; - const wordDict = ["leet", "code"]; - - const code = `function wordBreak(s: string, wordDict: string[]): boolean { +const languages: VisualizationLanguageMap = { + typescript: `function wordBreak(s: string, wordDict: string[]): boolean { const wordSet = new Set(wordDict); const n = s.length; - const dp: boolean[] = new Array(n + 1).fill(false); + const dp = new Array(n + 1).fill(false); dp[0] = true; for (let i = 1; i <= n; i++) { for (let j = 0; j < i; j++) { @@ -37,38 +32,94 @@ export const WordBreakVisualization: React.FC = () => { } } return dp[n]; -}`; +}`, + python: `def wordBreak(s: str, wordDict: List[str]) -> bool: + word_set = set(wordDict) + n = len(s) + dp = [False] * (n + 1) + dp[0] = True + for i in range(1, n + 1): + for j in range(i): + if dp[j] and s[j:i] in word_set: + dp[i] = True + break + return dp[n]`, + java: `public static class Solution { + public boolean wordBreak(String s, List wordDict) { + Set wordSet = new HashSet<>(wordDict); + int n = s.length(); + boolean[] dp = new boolean[n + 1]; + dp[0] = true; + for (int i = 1; i <= n; i++) { + for (int j = 0; j < i; j++) { + if (dp[j] && wordSet.contains(s.substring(j, i))) { + dp[i] = true; + break; + } + } + } + return dp[n]; + } +}`, + cpp: `class Solution { +public: + bool wordBreak(string s, vector& wordDict) { + unordered_set wordSet(wordDict.begin(), wordDict.end()); + int n = s.length(); + vector dp(n + 1, false); + dp[0] = true; + for (int i = 1; i <= n; i++) { + for (int j = 0; j < i; j++) { + if (dp[j] && wordSet.count(s.substr(j, i - j))) { + dp[i] = true; + break; + } + } + } + return dp[n]; + } +};` +}; - const steps = useMemo(() => { +export const WordBreakVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + + const s = "leetcode"; + const wordDict = ["leet", "code"]; + + const { steps, stepLineNumbers } = useMemo(() => { const stepsList: Step[] = []; const n = s.length; const wordSet = new Set(wordDict); const dp = new Array(n + 1).fill(false); + const lines: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; - stepsList.push({ - s, - dp: [...dp], - i: 0, - j: -1, - currentSubstring: '', - variables: { s: `"${s}"`, wordDict: `["${wordDict.join('", "')}"]` }, - explanation: "Function started. Input string and dictionary received.", - lineExecution: "function wordBreak(s: string, wordDict: string[]): boolean {", - highlightedLines: [1] - }); + const addLines = (ts: number, py: number, java: number, cpp: number) => { + lines.typescript!.push(ts); + lines.python!.push(py); + lines.java!.push(java); + lines.cpp!.push(cpp); + }; + // 1. Initial State / Setup stepsList.push({ s, dp: [...dp], i: 0, j: -1, currentSubstring: '', - variables: { wordSet: 'Set(...)', n }, - explanation: `Initialize wordSet and determine length of string n = ${n}.`, - lineExecution: "const wordSet = new Set(wordDict);\nconst n = s.length;", - highlightedLines: [2, 3] + variables: { s: `"${s}"`, wordDict: `["${wordDict.join('", "')}"]` }, + explanation: "Initialize the word set for fast lookups and get the input string length.", + pseudoStep: "SET wordSet = Set(wordDict), n = s.length" }); + addLines(2, 2, 3, 4); + // 2. DP Array Initialization dp[0] = true; stepsList.push({ s, @@ -77,11 +128,12 @@ export const WordBreakVisualization: React.FC = () => { j: -1, currentSubstring: '', variables: { n, dp: '[true, false, ...]', 'dp[0]': true }, - explanation: "Initialize DP array of size n + 1 with false. Set dp[0] = true as the base case representing an empty prefix string.", - lineExecution: "const dp: boolean[] = new Array(n + 1).fill(false);\ndp[0] = true;", - highlightedLines: [4, 5] + explanation: "Initialize DP array of size n + 1. dp[0] = true since an empty string is always valid.", + pseudoStep: "SET dp[0] = true" }); + addLines(5, 5, 6, 7); + // 3. Loops for (let i = 1; i <= n; i++) { stepsList.push({ s, @@ -90,10 +142,10 @@ export const WordBreakVisualization: React.FC = () => { j: -1, currentSubstring: '', variables: { n, i }, - explanation: `Outer loop iteration checking prefix of length i = ${i}. We analyze the substring up to character ${i}.`, - lineExecution: "for (let i = 1; i <= n; i++) {", - highlightedLines: [6] + explanation: `Outer loop: Checking prefix of length i = ${i}. We analyze substring up to index ${i}.`, + pseudoStep: `FOR i = 1 TO n (i = ${i})` }); + addLines(6, 6, 7, 8); for (let j = 0; j < i; j++) { const sub = s.substring(j, i); @@ -107,10 +159,10 @@ export const WordBreakVisualization: React.FC = () => { j, currentSubstring: sub, variables: { i, j, substring: `"${sub}"`, 'dp[j]': dpVal }, - explanation: `Inner loop evaluating split position j = ${j}.\nIs prefix up to j valid? (dp[${j}] = ${dpVal})\nWe extract substring "${sub}" starting from index ${j} to ${i}.`, - lineExecution: "for (let j = 0; j < i; j++) {", - highlightedLines: [7] + explanation: `Inner loop split position j = ${j}. Is prefix s[0..j-1] segmentable? (dp[${j}] = ${dpVal}). We extract candidate suffix: "${sub}".`, + pseudoStep: `FOR j = 0 TO i-1 (j = ${j})` }); + addLines(7, 7, 8, 9); stepsList.push({ s, @@ -119,10 +171,10 @@ export const WordBreakVisualization: React.FC = () => { j, currentSubstring: sub, variables: { i, j, substring: `"${sub}"`, 'dp[j]': dpVal, inDict }, - explanation: `Verify validity: Is prefix up to ${j} segmented (dp[${j}]) AND is "${sub}" present in our valid wordSet?\nBoth conditions must be true.`, - lineExecution: "if (dp[j] && wordSet.has(s.substring(j, i))) {", - highlightedLines: [8] + explanation: `Check condition: Is dp[${j}] true AND is "${sub}" in the dictionary? dp[${j}] && wordSet.has("${sub}") → ${dpVal} && ${inDict} → ${dpVal && inDict ? "YES ✓" : "NO ✗"}`, + pseudoStep: `IF dp[j] AND wordSet.has(s[j..i-1])` }); + addLines(8, 8, 9, 10); if (dpVal && inDict) { dp[i] = true; @@ -133,15 +185,16 @@ export const WordBreakVisualization: React.FC = () => { j, currentSubstring: sub, variables: { i, j, substring: `"${sub}"`, 'dp[i]': true }, - explanation: `Match successful! dp[${j}] is true and "${sub}" exists in dictionary. \nWe can safely conclude prefix of length ${i} is successfully segmented. Set dp[${i}] = true.`, - lineExecution: "dp[i] = true;\nbreak;", - highlightedLines: [9, 10] + explanation: `Condition met! dp[${j}] is true and "${sub}" exists in dictionary. This means the prefix s[0..${i}-1] can be segmented. Set dp[${i}] = true.`, + pseudoStep: `SET dp[i] = true, BREAK` }); + addLines(9, 9, 10, 11); break; } } } + // 4. Return Statement stepsList.push({ s, dp: [...dp], @@ -149,48 +202,46 @@ export const WordBreakVisualization: React.FC = () => { j: -1, currentSubstring: '', variables: { result: dp[n] }, - explanation: `All prefix evaluations completed. Final outcome is retrieved from dp[${n}], resulting in ${dp[n]}.`, - lineExecution: "return dp[n];", - highlightedLines: [14] + explanation: `All subproblems solved. Return dp[${n}] = ${dp[n]}.`, + pseudoStep: `RETURN dp[n] (${dp[n]})` }); + addLines(14, 11, 15, 16); - return stepsList; - }, [s, wordDict]); + return { steps: stepsList, stepLineNumbers: lines }; + }, []); - const step = steps[currentStep]; + const step = steps[currentStepIndex]; + const pseudoSteps = steps.map((s) => s.pseudoStep); return ( -
-

- Word Break Visualization -

+
+
-
- Dictionary: [{wordDict.map(w => `"${w}"`).join(', ')}] +
+ Dictionary: [{wordDict.map(w => `"${w}"`).join(', ')}]
- -

- Input String (s="leetcode") + +

+ Input String (s = "{s}")

-
+
{s.split('').map((char, idx) => { - let charBoxClass = "bg-white dark:bg-card border-gray-300 dark:border-border text-black dark:text-white"; + let charBoxClass = "bg-muted/30 border-border text-foreground"; const isCurrentRange = step.j !== -1 && idx >= step.j && idx < step.i; const isProcessed = idx < step.i; if (isCurrentRange) { - charBoxClass = "bg-blue-300 border-blue-500 shadow-lg text-black"; + charBoxClass = "bg-blue-500/20 border-blue-500 text-blue-600 dark:text-blue-400 font-bold scale-105 shadow-md"; } else if (isProcessed) { - charBoxClass = "bg-green-300 border-green-500 text-black"; + charBoxClass = "bg-green-500/10 border-green-500/50 text-green-600 dark:text-green-400"; } return (
{char}
@@ -198,86 +249,86 @@ export const WordBreakVisualization: React.FC = () => { })}
-

- DP Array (can segment prefix of length i) +

+ DP Array (Prefix of length i is segmentable)

-
+
{step.dp.map((val, idx) => { - const isI = idx === step.i && step.i !== -1; - const isJ = idx === step.j && step.j !== -1; - - let dpBoxClass = val ? 'bg-green-300 border-green-500 text-black' : 'bg-white dark:bg-card border-gray-300 dark:border-border text-black dark:text-white'; - - if (isI) { - dpBoxClass = 'bg-orange-300 border-orange-500 text-black shadow-lg'; - } else if (isJ) { - dpBoxClass = 'bg-blue-300 border-blue-500 text-black shadow-lg'; - } + const isI = idx === step.i && step.i !== -1; + const isJ = idx === step.j && step.j !== -1; - return ( -
-
- {val ? 'T' : 'F'} -
-
- {isI &&
i
} - {isJ &&
j
} -
-
- ); + let dpBoxClass = val + ? 'bg-green-500/10 border-green-500/40 text-green-600 dark:text-green-400' + : 'bg-muted/30 border-border text-muted-foreground/60'; + + if (isI) { + dpBoxClass = 'bg-orange-500/20 border-orange-500 text-orange-600 dark:text-orange-400 font-bold scale-105 shadow-md'; + } else if (isJ) { + dpBoxClass = 'bg-blue-500/20 border-blue-500 text-blue-600 dark:text-blue-400 font-bold scale-105 shadow-md'; + } + + return ( +
+
+ {val ? 'T' : 'F'} +
+ + i = {idx} + +
+ ); })}
-
- -
-
-

- Current Execution -

-
- {step.lineExecution} -
-
-
-

- Commentary -

-

- {step.explanation} -

-
-
+
+ +

Step Explanation

+

+ {step.explanation} +

+
+ + + + +

+ Why this works +

+

+ `dp[i]` is true if the prefix of length `i` (`s[0...i-1]`) can be segmented. +

+

+ To check this, we look at all split points `j` from 0 to `i - 1`. If `dp[j]` is true (prefix `s[0...j-1]` is valid) and the suffix `s[j...i-1]` is in the dictionary, then `dp[i]` is set to true. +

+

+ If we find any valid segmentation, we break early to optimize. +

} rightContent={ -
-
- -
- -
- -
-
+ setCurrentStepIndex(0)} + /> } controls={ } /> ); -}; \ No newline at end of file +}; + +export default WordBreakVisualization; \ No newline at end of file diff --git a/src/components/visualizations/algorithms/WordSearchVisualization.tsx b/src/components/visualizations/algorithms/WordSearchVisualization.tsx index 56a1e82..083b61c 100644 --- a/src/components/visualizations/algorithms/WordSearchVisualization.tsx +++ b/src/components/visualizations/algorithms/WordSearchVisualization.tsx @@ -1,10 +1,9 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { motion } from 'framer-motion'; -import { Card } from '@/components/ui/card'; -import { VariablePanel } from '../shared/VariablePanel'; -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; -import { StepControls } from '../shared/StepControls'; -import { Search } from 'lucide-react'; +import React, { useState, useEffect, useRef } from "react"; +import { StepControls } from "../shared/StepControls"; +import { VariablePanel } from "../shared/VariablePanel"; +import { VisualizationCodePanel } from "../shared/VisualizationCodePanel"; +import type { StepLineNumberMap, VisualizationLanguageMap } from "@/types/visualization"; +import { Search } from "lucide-react"; interface Step { board: string[][]; @@ -15,26 +14,18 @@ interface Step { path: Set; found: boolean; message: string; - lineNumber: number; + pseudoStep: string; } -export const WordSearchVisualization: React.FC = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1000); - const intervalRef = useRef(null); - - const code = `function exist(board: string[][], word: string): boolean { +const languages: VisualizationLanguageMap = { + typescript: `function exist(board: string[][], word: string): boolean { const ROWS = board.length; const COLS = board[0].length; const path = new Set(); - function dfs(r: number, c: number, i: number): boolean { if (i === word.length) { return true; } - if ( r < 0 || c < 0 || @@ -45,20 +36,15 @@ export const WordSearchVisualization: React.FC = () => { ) { return false; } - path.add(\`\${r},\${c}\`); - - const res = + const res = dfs(r + 1, c, i + 1) || dfs(r - 1, c, i + 1) || dfs(r, c + 1, i + 1) || dfs(r, c - 1, i + 1); - path.delete(\`\${r},\${c}\`); - return res; } - for (let r = 0; r < ROWS; r++) { for (let c = 0; c < COLS; c++) { if (dfs(r, c, 0)) { @@ -66,55 +52,178 @@ export const WordSearchVisualization: React.FC = () => { } } } - return false; -}`; +}`, + python: `def exist(board: list[list[str]], word: str) -> bool: + ROWS, COLS = len(board), len(board[0]) + path = set() + def dfs(r, c, i): + if i == len(word): + return True + if (r < 0 or c < 0 or r >= ROWS or c >= COLS or + board[r][c] != word[i] or (r, c) in path): + return False + path.add((r, c)) + res = (dfs(r + 1, c, i + 1) or + dfs(r - 1, c, i + 1) or + dfs(r, c + 1, i + 1) or + dfs(r, c - 1, i + 1)) + path.remove((r, c)) + return res + for r in range(ROWS): + for c in range(COLS): + if dfs(r, c, 0): + return True + return False`, + java: `public static class Solution { + public boolean exist(char[][] board, String word) { + int ROWS = board.length; + int COLS = board[0].length; + boolean[][] path = new boolean[ROWS][COLS]; + for (int r = 0; r < ROWS; r++) { + for (int c = 0; c < COLS; c++) { + if (dfs(board, r, c, word, 0, path)) { + return true; + } + } + } + return false; + } + private boolean dfs(char[][] board, int r, int c, String word, int i, boolean[][] path) { + if (i == word.length()) { + return true; + } + if ( + r < 0 || + c < 0 || + r >= board.length || + c >= board[0].length || + board[r][c] != word.charAt(i) || + path[r][c] + ) { + return false; + } + path[r][c] = true; + boolean res = + dfs(board, r + 1, c, word, i + 1, path) || + dfs(board, r - 1, c, word, i + 1, path) || + dfs(board, r, c + 1, word, i + 1, path) || + dfs(board, r, c - 1, word, i + 1, path); + path[r][c] = false; + return res; + } +}`, + cpp: `class Solution { +public: + bool exist(vector>& board, string word) { + int ROWS = board.size(); + int COLS = board[0].size(); + vector> path(ROWS, vector(COLS, false)); + for (int r = 0; r < ROWS; r++) { + for (int c = 0; c < COLS; c++) { + if (dfs(board, r, c, word, 0, path)) { + return true; + } + } + } + return false; + } +private: + bool dfs(vector>& board, int r, int c, string& word, int i, vector>& path) { + if (i == word.length()) { + return true; + } + if ( + r < 0 || + c < 0 || + r >= board.size() || + c >= board[0].size() || + board[r][c] != word[i] || + path[r][c] + ) { + return false; + } + path[r][c] = true; + bool res = + dfs(board, r + 1, c, word, i + 1, path) || + dfs(board, r - 1, c, word, i + 1, path) || + dfs(board, r, c + 1, word, i + 1, path) || + dfs(board, r, c - 1, word, i + 1, path); + path[r][c] = false; + return res; + } +};` +}; + +export const WordSearchVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); - const generateSteps = () => { + const generateStepsData = () => { const board = [ - ['A', 'B', 'C', 'E'], - ['S', 'F', 'C', 'S'], - ['A', 'D', 'E', 'E'] + ["A", "B", "C", "E"], + ["S", "F", "C", "S"], + ["A", "D", "E", "E"] ]; - const word = 'ABCCED'; + const word = "ABCCED"; const ROWS = board.length; const COLS = board[0].length; const path = new Set(); - const newSteps: Step[] = []; - - newSteps.push({ + const steps: Step[] = []; + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + steps.push({ board, word, r: -1, c: -1, i: 0, path: new Set(), found: false, message: "Initialize dimensions and the path tracker for word search.", - lineNumber: 1 + pseudoStep: "SET ROWS = board.length, COLS = board[0].length, path = {}" }); + addLines(2, 2, 3, 4); function dfs(r: number, c: number, i: number): boolean { - newSteps.push({ + steps.push({ board, word, r, c, i, path: new Set(path), found: false, - message: `Visiting [${r}, ${c}]. Checking if board character matches target word index ${i}.`, - lineNumber: 6 + message: `dfs(r = ${r}, c = ${c}, i = ${i}) called`, + pseudoStep: `CALL dfs(r = ${r}, c = ${c}, i = ${i})` }); + addLines(5, 4, 15, 17); - newSteps.push({ + steps.push({ board, word, r, c, i, path: new Set(path), found: false, - message: `Base case check: If index ${i} equals word length, we've found the entire word.`, - lineNumber: 7 + message: `Check base case: i === word.length (${i} === ${word.length})`, + pseudoStep: `IF i == word.length → ${i} == ${word.length} ?` }); + addLines(6, 5, 16, 18); if (i === word.length) { - newSteps.push({ + steps.push({ board, word, r, c, i, path: new Set(path), found: true, message: "Success! Every character in the word has been found in sequence.", - lineNumber: 8 + pseudoStep: "RETURN True" }); + addLines(7, 6, 17, 19); return true; } - newSteps.push({ + steps.push({ board, word, r, c, i, path: new Set(path), found: false, - message: "Verifying current cell: checking boundaries, matching character, and avoiding revisited cells.", - lineNumber: 11 + message: `Validate cell [${r}, ${c}]`, + pseudoStep: `IF outOfBounds OR charMismatch OR alreadyVisited` }); + addLines(9, 7, 19, 21); if ( r < 0 || c < 0 || r >= ROWS || c >= COLS || @@ -122,29 +231,32 @@ export const WordSearchVisualization: React.FC = () => { ) { let reason = ""; if (r < 0 || c < 0 || r >= ROWS || c >= COLS) reason = "out of bounds"; - else if (board[r][c] !== word[i]) reason = `character '${board[r][c]}' does not match '${word[i]}'`; - else if (path.has(`${r},${c}`)) reason = "cell already used in current path"; + else if (board[r][c] !== word[i]) reason = `char '${board[r][c]}' != '${word[i]}'`; + else if (path.has(`${r},${c}`)) reason = "already in path"; - newSteps.push({ + steps.push({ board, word, r, c, i, path: new Set(path), found: false, - message: `Backtrack: current path is invalid because ${reason}.`, - lineNumber: 19 + message: `Backtrack: Cell [${r}, ${c}] invalid because: ${reason}.`, + pseudoStep: "RETURN False" }); + addLines(17, 9, 27, 29); return false; } path.add(`${r},${c}`); - newSteps.push({ + steps.push({ board, word, r, c, i, path: new Set(path), found: false, - message: `Character match found! Adding [${r}, ${c}] to the visited path and exploring neighbors.`, - lineNumber: 22 + message: `Character match found! Adding [${r}, ${c}] to the visited path.`, + pseudoStep: `SET path = path ∪ {(${r}, ${c})}` }); + addLines(19, 10, 29, 31); - newSteps.push({ + steps.push({ board, word, r, c, i, path: new Set(path), found: false, - message: "Initiating recursive search in all 4 adjacent directions (Down, Up, Right, Left).", - lineNumber: 24 + message: "Recursive search in Down, Up, Right, Left directions.", + pseudoStep: `SET res = dfs(r+1,c) OR dfs(r-1,c) OR dfs(r,c+1) OR dfs(r,c-1)` }); + addLines(20, 11, 30, 32); const res = dfs(r + 1, c, i + 1) || @@ -155,17 +267,19 @@ export const WordSearchVisualization: React.FC = () => { if (res) return true; path.delete(`${r},${c}`); - newSteps.push({ + steps.push({ board, word, r, c, i, path: new Set(path), found: false, - message: `Cleaning up: All neighbors failed from [${r}, ${c}]. Removing cell from path (backtracking).`, - lineNumber: 30 + message: `All neighbors failed from [${r}, ${c}]. Removing cell from path (backtracking).`, + pseudoStep: `SET path = path - {(${r}, ${c})}` }); + addLines(25, 15, 35, 37); - newSteps.push({ + steps.push({ board, word, r, c, i, path: new Set(path), found: false, - message: "Returning from this recursive call to try other options.", - lineNumber: 32 + message: "Returning from this call.", + pseudoStep: "RETURN False" }); + addLines(26, 16, 36, 38); return res; } @@ -174,11 +288,12 @@ export const WordSearchVisualization: React.FC = () => { for (let r = 0; r < ROWS; r++) { for (let c = 0; c < COLS; c++) { if (found) break; - newSteps.push({ + steps.push({ board, word, r, c, i: 0, path: new Set(), found: false, - message: `Starting a new DFS traversal from starting board cell [${r}, ${c}].`, - lineNumber: 37 + message: `Starting a new DFS traversal from cell [${r}, ${c}].`, + pseudoStep: `FOR [r, c] = [${r}, ${c}] → CALL dfs(${r}, ${c}, 0)` }); + addLines(30, 19, 8, 9); if (dfs(r, c, 0)) { found = true; break; @@ -187,61 +302,82 @@ export const WordSearchVisualization: React.FC = () => { } if (!found) { - newSteps.push({ + steps.push({ board, word, r: -1, c: -1, i: 0, path: new Set(), found: false, - message: "Traversal complete. The word was not found anywhere on the board.", - lineNumber: 43 + message: "Traversal complete. The word was not found.", + pseudoStep: "RETURN False" }); + addLines(35, 21, 13, 14); } - setSteps(newSteps); + const lastStep = steps[steps.length - 1]; + steps.push({ + ...lastStep, + message: found ? "Word found!" : "Word not found.", + pseudoStep: "DONE" + }); + addLines(found ? 31 : 35, found ? 20 : 21, found ? 9 : 13, found ? 10 : 14); + + return { steps, stepLineNumbers }; }; - useEffect(() => { - generateSteps(); - }, []); + const { steps, stepLineNumbers } = generateStepsData(); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { - intervalRef.current = window.setInterval(() => { + intervalRef.current = setInterval(() => { setCurrentStepIndex((prev) => (prev < steps.length - 1 ? prev + 1 : prev)); - }, speed); + }, 1000 / speed); } else { if (intervalRef.current) clearInterval(intervalRef.current); } return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [isPlaying, currentStepIndex, steps.length, speed]); - const currentStep = steps[currentStepIndex] || steps[0]; - if (!currentStep) return null; + const handlePlay = () => setIsPlaying(true); + const handlePause = () => setIsPlaying(false); + const handleStepForward = () => { + if (currentStepIndex < steps.length - 1) setCurrentStepIndex(currentStepIndex + 1); + }; + const handleStepBack = () => { + if (currentStepIndex > 0) setCurrentStepIndex(currentStepIndex - 1); + }; + const handleReset = () => { + setCurrentStepIndex(0); + setIsPlaying(false); + }; + + if (steps.length === 0) return null; + const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map((s) => s.pseudoStep); const isCellInPath = (r: number, c: number) => currentStep.path.has(`${r},${c}`); return (
setIsPlaying(true)} - onPause={() => setIsPlaying(false)} - onStepForward={() => currentStepIndex < steps.length - 1 && setCurrentStepIndex(currentStepIndex + 1)} - onStepBack={() => currentStepIndex > 0 && setCurrentStepIndex(currentStepIndex - 1)} - onReset={() => { setCurrentStepIndex(0); setIsPlaying(false); }} + onPlay={handlePlay} + onPause={handlePause} + onStepForward={handleStepForward} + onStepBack={handleStepBack} + onReset={handleReset} isPlaying={isPlaying} currentStep={currentStepIndex} - totalSteps={steps.length} + totalSteps={steps.length - 1} speed={speed} onSpeedChange={setSpeed} />
- -
- -

Word Search Visualizer

-
+
+
+
+ +

Word Search Grid

+
-
-
-
+
+
{ return (
{char} @@ -271,9 +407,9 @@ export const WordSearchVisualization: React.FC = () => {
-
- Target String -
+
+ Target String +
{currentStep.word.split('').map((char, idx) => { const isFound = idx < currentStep.i; const isCurrent = idx === currentStep.i; @@ -281,9 +417,9 @@ export const WordSearchVisualization: React.FC = () => { return (
@@ -294,35 +430,34 @@ export const WordSearchVisualization: React.FC = () => {
+
-
-
-

- {currentStep.message} -

-
- - -
+
+

Algorithm Logic

+

+ {currentStep.message} +

- - - - +
+ +
); diff --git a/src/components/visualizations/algorithms/XORTrickVisualization.tsx b/src/components/visualizations/algorithms/XORTrickVisualization.tsx index 4b70cce..c9ead22 100644 --- a/src/components/visualizations/algorithms/XORTrickVisualization.tsx +++ b/src/components/visualizations/algorithms/XORTrickVisualization.tsx @@ -1,8 +1,8 @@ -import React, { useEffect, useRef, useState } from "react"; - -import { AnimatedCodeEditor } from "../shared/AnimatedCodeEditor"; +import React, { useState, useEffect, useRef } from "react"; import { StepControls } from "../shared/StepControls"; import { VariablePanel } from "../shared/VariablePanel"; +import { VisualizationCodePanel } from "../shared/VisualizationCodePanel"; +import type { StepLineNumberMap, VisualizationLanguageMap } from "@/types/visualization"; interface Step { nums: number[]; @@ -13,120 +13,138 @@ interface Step { binaryResult: string; binaryValue: string; message: string; - lineNumber: number; + pseudoStep: string; } -export const XORTrickVisualization: React.FC = () => { - const [steps, setSteps] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [speed, setSpeed] = useState(1000); - const intervalRef = useRef(null); - - const code = `function singleNumber(nums: number[]): number { +const languages: VisualizationLanguageMap = { + typescript: `function singleNumber(nums: number[]): number { let result = 0; - for (let i = 0; i < nums.length; i++) { result ^= nums[i]; } - return result; -}`; +}`, + python: `def singleNumber(nums): + res = 0 + for n in nums: + res ^= n + return res`, + java: `public static class Solution { + public int singleNumber(int[] nums) { + int res = 0; + for (int n : nums) { + res = n ^ res; + } + return res; + } +}`, + cpp: `class Solution { +public: + int singleNumber(vector& nums) { + int res = 0; + for (int n : nums) { + res = res ^ n; + } + return res; + } +};` +}; + +export const XORTrickVisualization: React.FC = () => { + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const intervalRef = useRef(null); - const generateSteps = () => { + const generateStepsData = () => { const nums = [4, 2, 4, 5, 2]; - const newSteps: Step[] = []; + const steps: Step[] = []; let result = 0; - const toBinary = (n: number) => n.toString(2).padStart(4, "0"); - newSteps.push({ - nums, - i: -1, - result: 0, - prevResult: 0, - currentValue: 0, - binaryResult: toBinary(0), - binaryValue: "0000", + const stepLineNumbers: StepLineNumberMap = { + typescript: [], + python: [], + java: [], + cpp: [] + }; + + const addLines = (ts: number, py: number, java: number, cpp: number) => { + stepLineNumbers.typescript!.push(ts); + stepLineNumbers.python!.push(py); + stepLineNumbers.java!.push(java); + stepLineNumbers.cpp!.push(cpp); + }; + + steps.push({ + nums, i: -1, result: 0, prevResult: 0, currentValue: 0, + binaryResult: toBinary(0), binaryValue: "0000", message: "Starting the singleNumber function.", - lineNumber: 1, + pseudoStep: "CALL singleNumber(nums)" }); + addLines(1, 1, 2, 3); - newSteps.push({ - nums, - i: -1, - result: 0, - prevResult: 0, - currentValue: 0, - binaryResult: toBinary(0), - binaryValue: "0000", + steps.push({ + nums, i: -1, result: 0, prevResult: 0, currentValue: 0, + binaryResult: toBinary(0), binaryValue: "0000", message: "Initializing result = 0. XOR identity property: 0 ^ x = x.", - lineNumber: 2, + pseudoStep: "SET result = 0" }); + addLines(2, 2, 3, 4); for (let i = 0; i < nums.length; i++) { - newSteps.push({ - nums, - i, - result, - prevResult: result, - currentValue: nums[i], - binaryResult: toBinary(result), - binaryValue: toBinary(nums[i]), - message: `Loop iteration i = ${i}. Checking loop condition i < nums.length.`, - lineNumber: 4, + steps.push({ + nums, i, result, prevResult: result, currentValue: nums[i], + binaryResult: toBinary(result), binaryValue: toBinary(nums[i]), + message: `Loop iteration i = ${i}. Checking loop condition.`, + pseudoStep: `FOR element IN nums → nums[${i}] = ${nums[i]}` }); + addLines(3, 3, 4, 5); const prevResult = result; result ^= nums[i]; - newSteps.push({ - nums, - i, - result, - prevResult, - currentValue: nums[i], - binaryResult: toBinary(result), - binaryValue: toBinary(nums[i]), + steps.push({ + nums, i, result, prevResult, currentValue: nums[i], + binaryResult: toBinary(result), binaryValue: toBinary(nums[i]), message: `XORing result (${prevResult}) with nums[${i}] (${nums[i]}). Result is now ${result}.`, - lineNumber: 5, + pseudoStep: `SET result = result ^ nums[${i}] → ${prevResult} ^ ${nums[i]} = ${result}` }); + addLines(4, 4, 5, 6); } - newSteps.push({ - nums, - i: nums.length, - result, - prevResult: result, - currentValue: 0, - binaryResult: toBinary(result), - binaryValue: "0000", + steps.push({ + nums, i: nums.length, result, prevResult: result, currentValue: 0, + binaryResult: toBinary(result), binaryValue: "0000", message: "Loop finished. All numbers have been XORed.", - lineNumber: 4, + pseudoStep: "END FOR" }); + addLines(3, 3, 4, 5); - newSteps.push({ - nums, - i: -1, - result, - prevResult: result, - currentValue: 0, - binaryResult: toBinary(result), - binaryValue: "0000", + steps.push({ + nums, i: -1, result, prevResult: result, currentValue: 0, + binaryResult: toBinary(result), binaryValue: "0000", message: `Returning the final result: ${result}. This is the number that appeared only once.`, - lineNumber: 8, + pseudoStep: `RETURN result → ${result}` + }); + addLines(6, 5, 7, 8); + + const lastStep = steps[steps.length - 1]; + steps.push({ + ...lastStep, + message: "Algorithm Complete!", + pseudoStep: "DONE" }); + addLines(6, 5, 7, 8); - setSteps(newSteps); + return { steps, stepLineNumbers }; }; - useEffect(() => { - generateSteps(); - }, []); + const { steps, stepLineNumbers } = generateStepsData(); useEffect(() => { if (isPlaying && currentStepIndex < steps.length - 1) { - intervalRef.current = window.setInterval(() => { + intervalRef.current = setInterval(() => { setCurrentStepIndex((prev) => { if (prev >= steps.length - 1) { setIsPlaying(false); @@ -134,7 +152,7 @@ export const XORTrickVisualization: React.FC = () => { } return prev + 1; }); - }, speed); + }, 1000 / speed); } else { if (intervalRef.current) { clearInterval(intervalRef.current); @@ -150,8 +168,7 @@ export const XORTrickVisualization: React.FC = () => { const handlePlay = () => setIsPlaying(true); const handlePause = () => setIsPlaying(false); const handleStepForward = () => { - if (currentStepIndex < steps.length - 1) - setCurrentStepIndex(currentStepIndex + 1); + if (currentStepIndex < steps.length - 1) setCurrentStepIndex(currentStepIndex + 1); }; const handleStepBack = () => { if (currentStepIndex > 0) setCurrentStepIndex(currentStepIndex - 1); @@ -164,6 +181,7 @@ export const XORTrickVisualization: React.FC = () => { if (steps.length === 0) return null; const currentStep = steps[currentStepIndex]; + const pseudoSteps = steps.map((s) => s.pseudoStep); return (
@@ -175,77 +193,78 @@ export const XORTrickVisualization: React.FC = () => { onReset={handleReset} isPlaying={isPlaying} currentStep={currentStepIndex} - totalSteps={steps.length} + totalSteps={steps.length - 1} speed={speed} onSpeedChange={setSpeed} />
-
-
-

Input Array (nums)

-
- {currentStep.nums.map((val, idx) => ( -
+
+
+

Input Array (nums)

+
+ {currentStep.nums.map((val, idx) => ( +
- {val} - - idx:{idx} - -
- ))} + > + {val} +
+ ))} +
-
-
-

Bitwise XOR Operation

-
-
- Previous Result: -
- {currentStep.prevResult} - {currentStep.binaryResult} +
+

Bitwise XOR Operation

+
+
+ Previous Result: +
+ {currentStep.prevResult} + {currentStep.binaryResult} +
-
-
-
- - ^ - - nums[{currentStep.i === -1 || currentStep.i >= currentStep.nums.length ? "x" : currentStep.i}]: -
-
- - {currentStep.i === -1 || currentStep.i >= currentStep.nums.length ? "0" : currentStep.currentValue} - - - {currentStep.i === -1 || currentStep.i >= currentStep.nums.length ? "0000" : currentStep.binaryValue} - +
+
+ + ^ + + nums[{currentStep.i === -1 || currentStep.i >= currentStep.nums.length ? "x" : currentStep.i}]: +
+
+ + {currentStep.i === -1 || currentStep.i >= currentStep.nums.length ? "0" : currentStep.currentValue} + + + {currentStep.i === -1 || currentStep.i >= currentStep.nums.length ? "0000" : currentStep.binaryValue} + +
-
-
- New Result: -
- - {currentStep.result} - - - {currentStep.binaryResult} - +
+ New Result: +
+ + {currentStep.result} + + + {currentStep.binaryResult} + +
-

+

Algorithm Logic

+

{currentStep.message}

@@ -260,10 +279,12 @@ export const XORTrickVisualization: React.FC = () => { />
-
diff --git a/src/components/visualizations/shared/AnimatedCodeEditor.tsx b/src/components/visualizations/shared/AnimatedCodeEditor.tsx index 13a90f2..d349788 100644 --- a/src/components/visualizations/shared/AnimatedCodeEditor.tsx +++ b/src/components/visualizations/shared/AnimatedCodeEditor.tsx @@ -11,6 +11,16 @@ interface AnimatedCodeEditorProps { language: string; highlightedLines?: number[]; className?: string; + /** + * When true, the built-in top bar (language label + eye/copy buttons) is hidden. + * Use this when a parent component (e.g. VisualizationCodePanel) provides its own header. + * In this mode, pass `isBlurred` and `onBlurChange` for controlled blur state. + */ + hideHeader?: boolean; + /** Controlled blur state — only used when hideHeader=true */ + isBlurred?: boolean; + /** Called when blur should toggle — only used when hideHeader=true */ + onBlurChange?: (val: boolean) => void; } const getLanguageDisplayName = (lang: string) => { @@ -29,16 +39,22 @@ export const AnimatedCodeEditor = ({ code, language, highlightedLines = [], - className = '' + className = '', + hideHeader = false, + isBlurred: isBlurredProp, + onBlurChange, }: AnimatedCodeEditorProps) => { const { theme, resolvedTheme } = useTheme(); const colorRef = useRef(null); - const [primaryColor, setPrimaryColor] = useState('#84CC16'); // Fallback to the green from index.css + const [primaryColor, setPrimaryColor] = useState('#84CC16'); const [isReady, setIsReady] = useState(false); const [mounted, setMounted] = useState(false); - const [isBlurred, setIsBlurred] = useState(false); + const [isBlurredInternal, setIsBlurredInternal] = useState(false); const [copied, setCopied] = useState(false); + // When hideHeader=true, blur is controlled externally. Otherwise use internal state. + const isBlurred = hideHeader ? (isBlurredProp ?? false) : isBlurredInternal; + const handleCopy = async () => { try { await navigator.clipboard.writeText(code); @@ -50,38 +66,31 @@ export const AnimatedCodeEditor = ({ } }; + // Internal blur state management (only active when header is shown) useEffect(() => { + if (hideHeader) return; const checkBlurred = () => { const saved = localStorage.getItem('visualization-code-blurred'); - if (saved !== null) { - setIsBlurred(saved === 'true'); - } + if (saved !== null) setIsBlurredInternal(saved === 'true'); }; - checkBlurred(); - - const handleBlurEvent = () => { - checkBlurred(); - }; - - window.addEventListener('code-blur-change', handleBlurEvent); - return () => { - window.removeEventListener('code-blur-change', handleBlurEvent); - }; - }, []); + window.addEventListener('code-blur-change', checkBlurred); + return () => window.removeEventListener('code-blur-change', checkBlurred); + }, [hideHeader]); const handleBlurChange = (val: boolean) => { - setIsBlurred(val); - localStorage.setItem('visualization-code-blurred', String(val)); - window.dispatchEvent(new Event('code-blur-change')); + if (hideHeader && onBlurChange) { + onBlurChange(val); + } else { + setIsBlurredInternal(val); + localStorage.setItem('visualization-code-blurred', String(val)); + window.dispatchEvent(new Event('code-blur-change')); + } }; - useEffect(() => { - setMounted(true); - }, []); + useEffect(() => { setMounted(true); }, []); useEffect(() => { - // Small delay to ensure styles are applied and we get the correct primary color const timer = setTimeout(() => { if (colorRef.current) { const style = window.getComputedStyle(colorRef.current); @@ -94,7 +103,6 @@ export const AnimatedCodeEditor = ({ return () => clearTimeout(timer); }, [resolvedTheme]); - // Use a stable default during SSR/hydration, then switch to actual theme once mounted const isDark = mounted ? (resolvedTheme || theme) === 'dark' : false; return ( @@ -105,49 +113,40 @@ export const AnimatedCodeEditor = ({ className={`rounded-lg border border-border overflow-hidden bg-card ${className}`} >
-
- {getLanguageDisplayName(language)} -
- {!isReady && ( -
-
- Initializing... -
- )} - -
- {/* Eye Toggle Button */} - - {/* Copy Button */} - + {/* Built-in header — shown only when not controlled by a parent */} + {!hideHeader && ( +
+ {getLanguageDisplayName(language)} +
+ {!isReady && ( +
+
+ Initializing... +
+ )} +
+ + +
-
+ )} +
{!isReady && (
@@ -166,7 +165,7 @@ export const AnimatedCodeEditor = ({
{isBlurred && (
-
-
+
Step {currentStep} / {totalSteps}
@@ -106,7 +106,7 @@ export const StepControls = ({
-
+
Speed: {speed.toFixed(1)}x
diff --git a/src/components/visualizations/shared/VariablePanel.tsx b/src/components/visualizations/shared/VariablePanel.tsx index 9aef02e..5bfcd89 100644 --- a/src/components/visualizations/shared/VariablePanel.tsx +++ b/src/components/visualizations/shared/VariablePanel.tsx @@ -1,13 +1,15 @@ interface VariablePanelProps { - variables: Record; + variables?: Record | null; } export const VariablePanel = ({ variables }: VariablePanelProps) => { + const entries = variables ? Object.entries(variables) : []; + return (

Variables

- {Object.entries(variables).map(([key, value]) => { + {entries.map(([key, value]) => { const renderValue = (val: any) => { if (val === null) return 'null'; if (val === undefined) return 'undefined'; diff --git a/src/components/visualizations/shared/VisualizationCodePanel.tsx b/src/components/visualizations/shared/VisualizationCodePanel.tsx new file mode 100644 index 0000000..1463f1a --- /dev/null +++ b/src/components/visualizations/shared/VisualizationCodePanel.tsx @@ -0,0 +1,232 @@ +'use client'; + +import React, { useMemo, useState, useEffect } from 'react'; +import { AnimatedCodeEditor } from './AnimatedCodeEditor'; +import { PseudocodeView } from './PseudocodeView'; +import { + VISUALIZATION_LANGUAGES, + LANGUAGE_LABELS, + DEFAULT_LANGUAGE_PRIORITY, + type VisualizationLanguage, + type VisualizationLanguageMap, + type StepLineNumberMap, +} from '@/types/visualization'; +import { Code2, GitBranch, Eye, EyeOff, Copy, Check } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { toast } from 'sonner'; +import { LanguageSelector } from '@/components/CodeRunner/LanguageSelector'; + +interface VisualizationCodePanelProps { + /** + * Hardcoded code per language, written directly in the visualization file. + * Only the languages present here will appear in the dropdown. + */ + languages: VisualizationLanguageMap; + + /** + * Per-language line number for each step. + * `typescript` is the required fallback key — other languages are optional. + * If a language is absent, the `typescript` line numbers are used instead. + */ + stepLineNumbers: StepLineNumberMap; + + /** Language-agnostic pseudo-statements — one per step, in step order */ + pseudoSteps: string[]; + + activeStepIndex: number; + className?: string; + onLanguageChange?: (lang: VisualizationLanguage) => void; +} + +// ─── Component ──────────────────────────────────────────────────────────────── + +/** + * VisualizationCodePanel + * + * Single unified header: + * [ Code] [🌿 Pseudocode] [Language ▼] [👁] [📋] + * + * - Code text is hardcoded in the visualization file (via `languages` prop). + * - Language dropdown shows only the languages present in `languages`. + * - Default language: Python if available, else TypeScript. + * - Eye/Copy only shown in Code mode. + */ +export const VisualizationCodePanel = ({ + languages, + stepLineNumbers, + pseudoSteps, + activeStepIndex, + className = '', + onLanguageChange, +}: VisualizationCodePanelProps) => { + + const [viewMode, setViewMode] = useState<'code' | 'pseudocode'>('code'); + const [isBlurred, setIsBlurred] = useState(false); + const [copied, setCopied] = useState(false); + + // Sync blur state with localStorage so it stays consistent with other panels + useEffect(() => { + const sync = () => { + const saved = localStorage.getItem('visualization-code-blurred'); + if (saved !== null) setIsBlurred(saved === 'true'); + }; + sync(); + window.addEventListener('code-blur-change', sync); + return () => window.removeEventListener('code-blur-change', sync); + }, []); + + const handleBlurChange = (val: boolean) => { + setIsBlurred(val); + localStorage.setItem('visualization-code-blurred', String(val)); + window.dispatchEvent(new Event('code-blur-change')); + }; + + // Languages that actually have code provided + const availableLanguages = useMemo( + () => VISUALIZATION_LANGUAGES.filter((lang) => !!languages[lang]), + [languages], + ); + + // Default: first from priority list that has code + const defaultLanguage = useMemo(() => { + for (const lang of DEFAULT_LANGUAGE_PRIORITY) { + if (languages[lang]) return lang; + } + return 'typescript'; + }, [languages]); + + const [activeLanguage, setActiveLanguage] = useState(defaultLanguage); + + // Re-sync with default language or load from localStorage on mount/update + useEffect(() => { + const saved = localStorage.getItem('visualization-active-language') as VisualizationLanguage | null; + if (saved && languages[saved]) { + setActiveLanguage(saved); + } else { + setActiveLanguage(defaultLanguage); + } + }, [defaultLanguage, languages]); + + const highlightedLine = useMemo(() => { + const lineMap = stepLineNumbers[activeLanguage] ?? stepLineNumbers.typescript; + return lineMap?.[activeStepIndex] ?? 1; + }, [stepLineNumbers, activeLanguage, activeStepIndex]); + + const activeCode = languages[activeLanguage] ?? ''; + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(activeCode); + setCopied(true); + toast.success('Code copied to clipboard!'); + setTimeout(() => setCopied(false), 2000); + } catch { + toast.error('Failed to copy code'); + } + }; + + if (availableLanguages.length === 0 && pseudoSteps.length === 0) return null; + + return ( +
+ + {/* ── Single unified header bar ── */} +
+ + {/* Left: view mode tabs */} +
+ {viewMode === 'code' ? ( + /* Active Code Tab - renders LanguageSelector dropdown */ +
+ { + const newLang = lang as VisualizationLanguage; + setActiveLanguage(newLang); + localStorage.setItem('visualization-active-language', newLang); + onLanguageChange?.(newLang); + }} + availableLanguages={availableLanguages as any[]} + /> +
+ ) : ( + /* Inactive Code Tab - renders a simple Tab button */ + + )} + + {/* Logic Tab */} + +
+ + {/* Spacer */} +
+ + {/* Right: eye + copy — Code mode only */} + {viewMode === 'code' && ( +
+ + {/* Eye toggle */} + + + {/* Copy */} + +
+ )} +
+ + {/* ── Content ── */} + {viewMode === 'code' ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/src/components/visualizations/shared/VisualizationLayout.tsx b/src/components/visualizations/shared/VisualizationLayout.tsx index 2db644a..a4ac49b 100644 --- a/src/components/visualizations/shared/VisualizationLayout.tsx +++ b/src/components/visualizations/shared/VisualizationLayout.tsx @@ -14,7 +14,7 @@ export const VisualizationLayout = ({ return (
{/* Top Controls */} -
+
{controls}
diff --git a/src/types/visualization.ts b/src/types/visualization.ts new file mode 100644 index 0000000..1b842a8 --- /dev/null +++ b/src/types/visualization.ts @@ -0,0 +1,64 @@ +/** + * Types for multi-language and pseudocode visualization support. + * + * Architecture overview: + * - Each visualization file provides `stepLineNumbers` (per-language line indices per step) + * and `languages` (hardcoded code per language as a fallback). + * - Code text can also come from the DB `algorithm.implementations` field. + * - The `VisualizationCodePanel` component merges both sources and renders + * either a code editor (with language tabs) or a pseudocode list view. + */ + +/** Languages supported in visualization code panels */ +export type VisualizationLanguage = 'typescript' | 'python' | 'java' | 'cpp'; + +/** Ordered list of all supported visualization languages */ +export const VISUALIZATION_LANGUAGES: VisualizationLanguage[] = [ + 'python', + 'cpp', + 'java', + 'typescript', +]; + +/** Display labels for each language */ +export const LANGUAGE_LABELS: Record = { + typescript: 'TypeScript', + python: 'Python', + java: 'Java', + cpp: 'C++', +}; + +/** + * Priority order when picking the default language to display. + * Python is preferred; TypeScript is the final guaranteed fallback. + */ +export const DEFAULT_LANGUAGE_PRIORITY: VisualizationLanguage[] = [ + 'python', + 'cpp', + 'java', + 'typescript', +]; + +/** + * A map from language → code string. + * Used by visualization files to provide hardcoded fallback code. + * Keys are optional — only the languages the author has written are required. + */ +export type VisualizationLanguageMap = Partial>; + +/** + * Per-step, per-language line number mapping. + * + * The array index corresponds to the step index in the visualization. + * Example: + * stepLineNumbers = { + * typescript: [2, 3, 4, 5, 5, 6, 8], // line to highlight at each step + * python: [1, 2, 3, 4, 4, 5, 7], + * } + * + * If a language key is absent, the `typescript` entry is used as a fallback. + * `typescript` is therefore the required key. + */ +export type StepLineNumberMap = { typescript: number[] } & Partial< + Record +>; diff --git a/src/utils/blind75Visualizations.tsx b/src/utils/blind75Visualizations.tsx index 31264e3..f165226 100644 --- a/src/utils/blind75Visualizations.tsx +++ b/src/utils/blind75Visualizations.tsx @@ -55,6 +55,7 @@ const blind75Map: Record import("@/components/visualizations/algorithms/CourseScheduleVisualization").then(m => ({ default: m.CourseScheduleVisualization }))), "course-schedule-ii": React.lazy(() => import("@/components/visualizations/algorithms/CourseScheduleIIVisualization").then(m => ({ default: m.CourseScheduleIIVisualization }))), "pacific-atlantic": React.lazy(() => import("@/components/visualizations/algorithms/PacificAtlanticVisualization").then(m => ({ default: m.PacificAtlanticVisualization }))), + "pacific-atlantic-water-flow": React.lazy(() => import("@/components/visualizations/algorithms/PacificAtlanticVisualization").then(m => ({ default: m.PacificAtlanticVisualization }))), "num-islands": React.lazy(() => import("@/components/visualizations/algorithms/NumberOfIslandsVisualization").then(m => ({ default: m.NumberOfIslandsVisualization }))), "number-of-islands": React.lazy(() => import("@/components/visualizations/algorithms/NumberOfIslandsVisualization").then(m => ({ default: m.NumberOfIslandsVisualization }))), "longest-consecutive-sequence": React.lazy(() => import("@/components/visualizations/algorithms/LongestConsecutiveSequenceVisualization").then(m => ({ default: m.LongestConsecutiveSequenceVisualization }))), diff --git a/src/utils/visualizationMapping.tsx b/src/utils/visualizationMapping.tsx index 7f53b19..d51dab7 100644 --- a/src/utils/visualizationMapping.tsx +++ b/src/utils/visualizationMapping.tsx @@ -67,8 +67,8 @@ export const visualizationMap: Record = { 'copy-list-with-random-pointer': dynamic(() => import('@/components/visualizations/algorithms/CopyListWithRandomPointerVisualization').then(m => m.CopyListWithRandomPointerVisualization), { ssr: false }), // Graph Algorithms - 'graph-dfs': dynamic(() => import('@/components/visualizations/GraphVisualization').then(m => m.GraphVisualization), { ssr: false }), - 'graph-bfs': dynamic(() => import('@/components/visualizations/GraphVisualization').then(m => m.GraphVisualization), { ssr: false }), + 'graph-dfs': dynamic(() => import('@/components/visualizations/algorithms/GraphDFSVisualization').then(m => m.GraphDFSVisualization), { ssr: false }), + 'graph-bfs': dynamic(() => import('@/components/visualizations/algorithms/GraphBFSVisualization').then(m => m.GraphBFSVisualization), { ssr: false }), 'number-of-islands': dynamic(() => import('@/components/visualizations/algorithms/NumberOfIslandsVisualization').then(m => m.NumberOfIslandsVisualization), { ssr: false }), 'max-area-of-island': dynamic(() => import('@/components/visualizations/algorithms/MaxAreaOfIslandVisualization').then(m => m.MaxAreaOfIslandVisualization), { ssr: false }), 'reconstruct-itinerary': dynamic(() => import('@/components/visualizations/algorithms/ReconstructItineraryVisualization').then(m => m.ReconstructItineraryVisualization), { ssr: false }), diff --git a/test_304.sql b/test_304.sql deleted file mode 100644 index c4b44c4..0000000 --- a/test_304.sql +++ /dev/null @@ -1,16 +0,0 @@ -.headers on -.mode list -.separator |_|_| -.nullvalue ___NULL___ -CREATE TABLE Scores ( - id INT PRIMARY KEY, - score DECIMAL(3, 2) -); -INSERT INTO Scores (id, score) VALUES (1, 3.5), (2, 4), (3, 3.5), (4, 2), (5, 4); -SELECT - score, - DENSE_RANK() OVER (ORDER BY score DESC) AS "rank" -FROM - Scores -ORDER BY - score DESC; \ No newline at end of file diff --git a/test_judge0.js b/test_judge0.js deleted file mode 100644 index 39ce1e2..0000000 --- a/test_judge0.js +++ /dev/null @@ -1,5 +0,0 @@ -import axios from 'axios'; -axios.post('https://ce.judge0.com/submissions?wait=true', { - language_id: 82, - source_code: ".headers on\n.mode list\n.separator '|_|_|'\n.nullvalue '___NULL___'\nCREATE TABLE T (a INT, b TEXT);\nINSERT INTO T VALUES(1, NULL);\nINSERT INTO T VALUES(2, 'A|B');\nSELECT * FROM T;" -}).then(r => console.log(r.data)).catch(console.error);