diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ae69fdd --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,5 @@ +# Repository guidance + +- Do not use test-driven development in this repository unless the user explicitly requests it. +- For visualisation content or example-data changes, edit the source directly and run proportionate existing checks afterwards. +- Update an existing fixture only when the source change would otherwise leave it stale; do not add tests solely for content changes. diff --git a/README.md b/README.md index 29b6f0b..bcb1d29 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,19 @@ Interactive, step-by-step traces for common data-structure and algorithm pattern visualisation shows the example input and output alongside the changing queue, stack, heap, graph, grid, or array state. -[View the live site](https://algorithm-visualisations-nine.vercel.app) +**[Explore the live visualisations](https://algorithm-visualisations-nine.vercel.app)** + +[![Preview of the Algorithm Visualisations website](docs/githubpic.jpg)](https://algorithm-visualisations-nine.vercel.app) ## Features -- Fourteen interactive traces across trees, graphs, and arrays +- Fifteen interactive traces across trees, graphs, and arrays - Play, pause, previous, next, and restart controls -- Responsive layouts with light and dark colour schemes +- Responsive, dark-themed layouts for desktop and mobile - Standalone routes suitable for Notion embeds - Reference implementations with local syntax highlighting +- Category filters, direct LeetCode links, and one-click Notion URL copying +- Privacy-friendly Vercel Analytics ## Development @@ -37,6 +41,7 @@ npm run build src/ React catalogue and tests public/embed/ Trace configurations, runner, and shared styles public// Standalone route wrappers +docs/ Project preview image vercel.json Routing and browser security headers ``` diff --git a/docs/githubpic.jpg b/docs/githubpic.jpg new file mode 100644 index 0000000..8e3b201 Binary files /dev/null and b/docs/githubpic.jpg differ diff --git a/package-lock.json b/package-lock.json index be9cb67..2073a99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1319,9 +1319,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { diff --git a/public/embed/find-path-exists-in-graph.html b/public/embed/find-path-exists-in-graph.html index f8ba16a..ddadfaa 100644 --- a/public/embed/find-path-exists-in-graph.html +++ b/public/embed/find-path-exists-in-graph.html @@ -6,22 +6,34 @@ { "title": "Find if Path Exists in Graph — BFS", "url": "https://leetcode.com/problems/find-if-path-exists-in-graph/", - "input": "n = 3, edges = [[0, 1], [1, 2]], source = 0, destination = 2", + "input": "n = 6, edges = [[0, 1], [0, 2], [1, 3], [2, 4], [3, 4], [4, 5]], source = 0, destination = 5", "output": "True", "structure": "Queue (front → back)", "visual": { "type": "graph", "positions": { "0": [ - 40, + 30, 105 ], "1": [ - 225, - 105 + 150, + 35 ], "2": [ - 410, + 150, + 175 + ], + "3": [ + 285, + 35 + ], + "4": [ + 285, + 175 + ], + "5": [ + 420, 105 ] }, @@ -31,8 +43,24 @@ 1 ], [ - 1, + 0, 2 + ], + [ + 1, + 3 + ], + [ + 2, + 4 + ], + [ + 3, + 4 + ], + [ + 4, + 5 ] ] }, @@ -48,40 +76,97 @@ "map": "visited = {0}" }, { - "operation": "Dequeue 0 and discover neighbour 1", + "operation": "Dequeue 0 and discover both branches, 1 and 2", "active": [ - "0", - "1" + "1", + "2" ], "done": [ "0" ], "structure": [ + "1", + "2" + ], + "map": "visited = {0, 1, 2}" + }, + { + "operation": "Dequeue 1; skip visited 0 and discover 3", + "active": [ + "3" + ], + "done": [ + "0", "1" ], - "map": "visited = {0, 1}" + "structure": [ + "2", + "3" + ], + "map": "visited = {0, 1, 2, 3}" }, { - "operation": "Dequeue 1 and discover destination 2", + "operation": "Dequeue 2; skip visited 0 and discover 4", "active": [ + "4" + ], + "done": [ + "0", "1", "2" ], + "structure": [ + "3", + "4" + ], + "map": "visited = {0, 1, 2, 3, 4}" + }, + { + "operation": "Dequeue 3; neighbour 4 is already visited, so do not enqueue it again", + "active": [ + "3", + "4" + ], "done": [ "0", - "1" + "1", + "2", + "3" ], "structure": [ - "2" + "4" ], - "map": "visited = {0, 1, 2}" + "map": "visited suppression keeps one copy of 4 in the queue" }, { - "operation": "Dequeue 2: it is the destination", + "operation": "Dequeue 4; skip visited 2 and 3, then discover destination 5", + "active": [ + "5" + ], "done": [ "0", "1", - "2" + "2", + "3", + "4" + ], + "structure": [ + "5" + ], + "map": "visited = {0, 1, 2, 3, 4, 5}" + }, + { + "operation": "Dequeue 5: it is the destination", + "active": [ + "5" + ], + "done": [ + "0", + "1", + "2", + "3", + "4", + "5" ], "structure": [], "map": "return True" diff --git a/public/embed/graph-valid-tree.html b/public/embed/graph-valid-tree.html index 87d4b59..e49ef16 100644 --- a/public/embed/graph-valid-tree.html +++ b/public/embed/graph-valid-tree.html @@ -6,31 +6,35 @@ { "title": "Graph Valid Tree — BFS", "url": "https://leetcode.com/problems/graph-valid-tree/", - "input": "n = 5, edges = [[0, 1], [0, 2], [0, 3], [1, 4]]", - "output": "True", + "input": "n = 6, edges = [[0, 1], [0, 2], [1, 3], [2, 3], [4, 5]]", + "output": "False", "structure": "Queue of (node, parent)", "visual": { "type": "graph", "positions": { "0": [ - 50, + 35, 105 ], "1": [ - 205, + 170, 35 ], "2": [ - 205, - 105 + 170, + 175 ], "3": [ - 205, - 175 + 305, + 105 ], "4": [ - 375, - 35 + 400, + 55 + ], + "5": [ + 400, + 155 ] }, "edges": [ @@ -43,12 +47,16 @@ 2 ], [ - 0, + 1, 3 ], [ - 1, - 4 + 2, + 3 + ], + [ + 4, + 5 ] ] }, @@ -64,28 +72,26 @@ "map": "visited = {0}" }, { - "operation": "Visit 0 and enqueue each unseen neighbour", + "operation": "Visit 0; fake parent -1 matches no neighbour", "active": [ "0", "1", - "2", - "3" + "2" ], "done": [ "0" ], "structure": [ "(1, 0)", - "(2, 0)", - "(3, 0)" + "(2, 0)" ], - "map": "the parent prevents treating the return edge as a cycle" + "map": "visited = {0, 1, 2}" }, { - "operation": "Visit 1 and discover 4", + "operation": "Visit 1; skip parent 0, then discover 3", "active": [ "1", - "4" + "3" ], "done": [ "0", @@ -93,34 +99,40 @@ ], "structure": [ "(2, 0)", - "(3, 0)", - "(4, 1)" + "(3, 1)" ], - "map": "visited = {0, 1, 2, 3, 4}" + "map": "visited = {0, 1, 2, 3}" }, { - "operation": "Drain remaining nodes without seeing a non-parent repeat", + "operation": "Visit 2; skip parent 0, then inspect neighbour 3", + "active": [ + "2", + "3" + ], "done": [ "0", "1", - "2", - "3", - "4" + "2" + ], + "structure": [ + "(3, 1)" ], - "structure": [], - "map": "no cycle found" + "map": "3 is already visited and is not 2's parent" }, { - "operation": "All n nodes were visited", + "operation": "A non-parent repeat proves the 0–1–3–2–0 cycle", + "active": [ + "3" + ], "done": [ "0", "1", - "2", - "3", - "4" + "2" + ], + "structure": [ + "(3, 1)" ], - "structure": [], - "map": "len(visited) = n → True" + "map": "return False; disconnected nodes 4 and 5 were never reached" } ] } diff --git a/public/embed/minimum-health-to-reach-destination.html b/public/embed/minimum-health-to-reach-destination.html index 6855403..acbde93 100644 --- a/public/embed/minimum-health-to-reach-destination.html +++ b/public/embed/minimum-health-to-reach-destination.html @@ -6,8 +6,8 @@ { "title": "Minimum Health to Reach Destination", "url": "https://leetcode.com/problems/path-with-minimum-effort/", - "input": "edges = [[0, 1, 3], [1, 4, 5], [0, 2, 2], [2, 4, 8]]", - "output": "5", + "input": "n = 7, edges = [[0, 1, 4], [0, 2, 2], [1, 3, 6], [2, 3, 5], [2, 4, 8], [3, 5, 7], [4, 5, 3], [5, 6, 7]], source = 0, destination = 6", + "output": "7", "structure": "Queue (front → back)", "visual": { "type": "graph", @@ -17,38 +17,70 @@ 105 ], "1": [ - 195, - 35 + 145, + 30 ], "2": [ - 195, - 175 + 145, + 180 + ], + "3": [ + 260, + 70 ], "4": [ - 380, - 105 + 260, + 170 + ], + "5": [ + 370, + 120 + ], + "6": [ + 455, + 120 ] }, "edges": [ [ 0, 1, - "3" + "4" + ], + [ + 0, + 2, + "2" ], [ 1, - 4, - "5" + 3, + "6" ], [ - 0, 2, - "2" + 3, + "5" ], [ 2, 4, "8" + ], + [ + 3, + 5, + "7" + ], + [ + 4, + 5, + "3" + ], + [ + 5, + 6, + "7" ] ] }, @@ -65,14 +97,18 @@ ], "edgeStates": { "0-1": "allowed", - "1-4": "blocked", "0-2": "allowed", - "2-4": "blocked" + "1-3": "blocked", + "2-3": "blocked", + "2-4": "blocked", + "3-5": "blocked", + "4-5": "allowed", + "5-6": "blocked" }, "map": "binary search threshold = 4" }, { - "operation": "BFS cannot cross either red destination edge", + "operation": "BFS visits 1 and 2, but neither can advance", "done": [ "0", "1", @@ -81,62 +117,207 @@ "structure": [], "edgeStates": { "0-1": "allowed", - "1-4": "blocked", "0-2": "allowed", - "2-4": "blocked" + "1-3": "blocked", + "2-3": "blocked", + "2-4": "blocked", + "3-5": "blocked", + "4-5": "allowed", + "5-6": "blocked" }, "map": "health 4 fails → raise lower bound" }, { - "operation": "Try health = 5; edge 1 → 4 becomes available", + "operation": "Try health = 6; both branches connect to node 3", "active": [ "0", "1", - "4" + "2", + "3" ], "structure": [ "0" ], "edgeStates": { "0-1": "allowed", - "1-4": "allowed", "0-2": "allowed", - "2-4": "blocked" + "1-3": "allowed", + "2-3": "allowed", + "2-4": "blocked", + "3-5": "blocked", + "4-5": "allowed", + "5-6": "blocked" }, - "map": "health = 5" + "map": "binary search threshold = 6" }, { - "operation": "BFS reaches 4 through 0 → 1 → 4", + "operation": "BFS drains at node 3; edges 2 → 4 and 3 → 5 are blocked", "done": [ "0", "1", - "4" + "2", + "3" ], "structure": [], "edgeStates": { "0-1": "allowed", - "1-4": "allowed", "0-2": "allowed", - "2-4": "blocked" + "1-3": "allowed", + "2-3": "allowed", + "2-4": "blocked", + "3-5": "blocked", + "4-5": "allowed", + "5-6": "blocked" + }, + "map": "health 6 fails → raise lower bound to 7" + }, + { + "operation": "Try health = 7; start BFS from source 0", + "active": [ + "0" + ], + "structure": [ + "0" + ], + "edgeStates": { + "0-1": "allowed", + "0-2": "allowed", + "1-3": "allowed", + "2-3": "allowed", + "2-4": "blocked", + "3-5": "allowed", + "4-5": "allowed", + "5-6": "allowed" }, - "map": "health 5 works → search lower" + "map": "binary search threshold = 7" }, { - "operation": "Minimum sufficient initial health", + "operation": "Visit 0 and enqueue neighbours 1 and 2", + "active": [ + "1", + "2" + ], + "done": [ + "0" + ], + "structure": [ + "1", + "2" + ], + "edgeStates": { + "0-1": "allowed", + "0-2": "allowed", + "1-3": "allowed", + "2-3": "allowed", + "2-4": "blocked", + "3-5": "allowed", + "4-5": "allowed", + "5-6": "allowed" + }, + "map": "visited = {0, 1, 2}" + }, + { + "operation": "Visit 1 and enqueue 3; visit 2 but skip blocked edge to 4", + "active": [ + "3" + ], + "done": [ + "0", + "1", + "2" + ], + "structure": [ + "3" + ], + "edgeStates": { + "0-1": "allowed", + "0-2": "allowed", + "1-3": "allowed", + "2-3": "allowed", + "2-4": "blocked", + "3-5": "allowed", + "4-5": "allowed", + "5-6": "allowed" + }, + "map": "visited = {0, 1, 2, 3}" + }, + { + "operation": "Visit 3 and enqueue 5", + "active": [ + "5" + ], "done": [ "0", "1", "2", - "4" + "3" + ], + "structure": [ + "5" + ], + "edgeStates": { + "0-1": "allowed", + "0-2": "allowed", + "1-3": "allowed", + "2-3": "allowed", + "2-4": "blocked", + "3-5": "allowed", + "4-5": "allowed", + "5-6": "allowed" + }, + "map": "path so far: 0 → 1 → 3 → 5" + }, + { + "operation": "Visit 5 and enqueue destination 6", + "active": [ + "6" + ], + "done": [ + "0", + "1", + "2", + "3", + "5" + ], + "structure": [ + "4", + "6" + ], + "edgeStates": { + "0-1": "allowed", + "0-2": "allowed", + "1-3": "allowed", + "2-3": "allowed", + "2-4": "blocked", + "3-5": "allowed", + "4-5": "allowed", + "5-6": "allowed" + }, + "map": "edge 5 → 4 also enqueues 4 before 6" + }, + { + "operation": "Visit 4, then dequeue destination 6", + "done": [ + "0", + "1", + "2", + "3", + "4", + "5", + "6" ], "structure": [], "edgeStates": { "0-1": "allowed", - "1-4": "allowed", "0-2": "allowed", - "2-4": "blocked" + "1-3": "allowed", + "2-3": "allowed", + "2-4": "blocked", + "3-5": "allowed", + "4-5": "allowed", + "5-6": "allowed" }, - "map": "return 5" + "map": "health 7 works; binary search returns 7" } ] } diff --git a/public/embed/number-of-provinces-dfs.html b/public/embed/number-of-provinces-dfs.html index 6075908..693fe0d 100644 --- a/public/embed/number-of-provinces-dfs.html +++ b/public/embed/number-of-provinces-dfs.html @@ -6,29 +6,49 @@ { "title": "Number of Provinces — iterative DFS", "url": "https://leetcode.com/problems/number-of-provinces/", - "input": "isConnected = [[1, 1, 0], [1, 1, 0], [0, 0, 1]]", + "input": "isConnected = [[1, 1, 1, 0, 0], [1, 1, 1, 0, 0], [1, 1, 1, 0, 0], [0, 0, 0, 1, 1], [0, 0, 0, 1, 1]]", "output": "2 provinces", "structure": "Stack (top → right)", "visual": { "type": "graph", "positions": { "0": [ - 165, + 125, 65 ], "1": [ - 385, - 65 + 275, + 40 ], "2": [ - 275, - 178 + 220, + 165 + ], + "3": [ + 390, + 70 + ], + "4": [ + 410, + 175 ] }, "edges": [ [ "0", "1" + ], + [ + "0", + "2" + ], + [ + "1", + "2" + ], + [ + "3", + "4" ] ] }, @@ -41,54 +61,94 @@ "structure": [ "0" ], - "map": "Adjacency list\n0: [1]\n1: [0]\n2: []\nvisited = {0}" + "map": "Adjacency list\n0: [1, 2]\n1: [0, 2]\n2: [0, 1]\n3: [4]\n4: [3]\nvisited = {0}" }, { - "operation": "Pop city 0 and push its unvisited neighbour", + "operation": "Pop city 0 and push both unvisited neighbours", "active": [ "0", - "1" + "1", + "2" ], "done": [ "0" ], + "structure": [ + "1", + "2" + ], + "map": "visited = {0, 1, 2}\nstack = [1, 2]" + }, + { + "operation": "Pop city 2; both neighbours are already visited", + "active": [ + "2" + ], + "done": [ + "0", + "2" + ], "structure": [ "1" ], - "map": "visited = {0, 1}\nstack = [1]" + "map": "visited = {0, 1, 2}\nstack = [1]" }, { - "operation": "Pop city 1; its only neighbour is already visited", + "operation": "Pop city 1; province 1 is complete", "active": [ "1" ], "done": [ "0", - "1" + "1", + "2" ], "structure": [], - "map": "province 1 complete" + "map": "visited = {0, 1, 2}\nstack = []" }, { - "operation": "City 2 is unvisited: start province 2", + "operation": "City 3 is unvisited: start province 2", "active": [ + "3" + ], + "done": [ + "0", + "1", "2" ], + "structure": [ + "3" + ], + "map": "visited = {0, 1, 2, 3}\nprovinces = 2" + }, + { + "operation": "Pop city 3 and push its unvisited neighbour", + "active": [ + "3", + "4" + ], "done": [ "0", - "1" + "1", + "2", + "3" ], "structure": [ - "2" + "4" ], - "map": "visited = {0, 1, 2}\nprovinces = 2" + "map": "visited = {0, 1, 2, 3, 4}\nstack = [4]" }, { - "operation": "The stack drains; all cities are covered", + "operation": "Pop city 4; its neighbour is already visited", + "active": [ + "4" + ], "done": [ "0", "1", - "2" + "2", + "3", + "4" ], "structure": [], "map": "return 2 provinces" diff --git a/public/embed/shortest-path-binary-matrix.html b/public/embed/shortest-path-binary-matrix.html index 8e5c656..9a02bd1 100644 --- a/public/embed/shortest-path-binary-matrix.html +++ b/public/embed/shortest-path-binary-matrix.html @@ -6,24 +6,56 @@ { "title": "Shortest Path in Binary Matrix — BFS", "url": "https://leetcode.com/problems/shortest-path-in-binary-matrix/", - "input": "grid = [[0, 1], [1, 0]]", - "output": "2", + "input": "grid = [[0, 0, 1, 0, 0], [1, 0, 1, 0, 1], [0, 0, 0, 0, 0], [0, 1, 1, 1, 0], [0, 0, 0, 0, 0]]", + "output": "6", "structure": "Queue of (distance, row, col)", "visual": { "type": "grid", "rows": [ [ + 0, + 0, + 1, + 0, + 0 + ], + [ + 1, + 0, + 1, 0, 1 ], [ + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, 1, + 1, + 1, + 0 + ], + [ + 0, + 0, + 0, + 0, 0 ] ], "blocked": [ - "0,1", - "1,0" + "0,2", + "1,0", + "1,2", + "1,4", + "3,1", + "3,2", + "3,3" ] }, "states": [ @@ -38,39 +70,251 @@ "map": "visited = {(0, 0)}" }, { - "operation": "Pop (0, 0); diagonal movement is allowed", + "operation": "Pop (0, 0); enqueue its clear right and diagonal neighbours", "active": [ "0,0", + "0,1", "1,1" ], "done": [ "0,0" ], "structure": [ + "(2, 0, 1)", "(2, 1, 1)" ], - "map": "the seven other directions are blocked or out of bounds" + "map": "visited = {(0, 0), (0, 1), (1, 1)}" }, { - "operation": "First arrival at target is the shortest path", + "operation": "Finish distance 2; (1, 1) discovers three cells at distance 3", "active": [ - "1,1" + "2,0", + "2,1", + "2,2" ], "done": [ "0,0", + "0,1", "1,1" ], - "structure": [], - "map": "BFS explores in increasing distance order" + "structure": [ + "(3, 2, 1)", + "(3, 2, 2)", + "(3, 2, 0)" + ], + "map": "all queued cells now have distance 3" }, { - "operation": "Return its distance", + "operation": "Process the distance-3 layer and discover the next frontier", + "active": [ + "1,3", + "2,3", + "3,0" + ], "done": [ "0,0", - "1,1" + "0,1", + "1,1", + "2,0", + "2,1", + "2,2" + ], + "structure": [ + "(4, 3, 0)", + "(4, 2, 3)", + "(4, 1, 3)" + ], + "map": "blocked cells prevent a direct diagonal route" + }, + { + "operation": "Pop (3, 0); enqueue the two clear cells below it", + "active": [ + "3,0", + "4,0", + "4,1" + ], + "done": [ + "0,0", + "0,1", + "1,1", + "2,0", + "2,1", + "2,2", + "3,0" + ], + "structure": [ + "(4, 2, 3)", + "(4, 1, 3)", + "(5, 4, 0)", + "(5, 4, 1)" + ], + "map": "distance 4 remains at the front of the queue" + }, + { + "operation": "Pop (2, 3); enqueue (2, 4) and (3, 4)", + "active": [ + "2,3", + "2,4", + "3,4" + ], + "done": [ + "0,0", + "0,1", + "1,1", + "2,0", + "2,1", + "2,2", + "3,0", + "2,3" + ], + "structure": [ + "(4, 1, 3)", + "(5, 4, 0)", + "(5, 4, 1)", + "(5, 2, 4)", + "(5, 3, 4)" + ], + "map": "the frontier now reaches the right edge" + }, + { + "operation": "Pop (1, 3); enqueue the two clear cells above it", + "active": [ + "1,3", + "0,3", + "0,4" + ], + "done": [ + "0,0", + "0,1", + "1,1", + "2,0", + "2,1", + "2,2", + "3,0", + "2,3", + "1,3" + ], + "structure": [ + "(5, 4, 0)", + "(5, 4, 1)", + "(5, 2, 4)", + "(5, 3, 4)", + "(5, 0, 3)", + "(5, 0, 4)" + ], + "map": "every queued cell now has distance 5" + }, + { + "operation": "Process distance 5; (4, 1) discovers (4, 2)", + "active": [ + "4,1", + "4,2" + ], + "done": [ + "0,0", + "0,1", + "1,1", + "2,0", + "2,1", + "2,2", + "3,0", + "2,3", + "1,3", + "4,0", + "4,1" + ], + "structure": [ + "(5, 2, 4)", + "(5, 3, 4)", + "(5, 0, 3)", + "(5, 0, 4)", + "(6, 4, 2)" + ], + "map": "the next layer begins at distance 6" + }, + { + "operation": "Pop (3, 4); the target and (4, 3) enter the distance-6 layer", + "active": [ + "3,4", + "4,3", + "4,4" + ], + "done": [ + "0,0", + "0,1", + "1,1", + "2,0", + "2,1", + "2,2", + "3,0", + "2,3", + "1,3", + "4,0", + "4,1", + "2,4", + "3,4" + ], + "structure": [ + "(5, 0, 3)", + "(5, 0, 4)", + "(6, 4, 2)", + "(6, 4, 4)", + "(6, 4, 3)" + ], + "map": "the target is discovered, but BFS returns when it is popped" + }, + { + "operation": "Finish earlier queue entries, then pop target (4, 4)", + "active": [ + "4,4" + ], + "done": [ + "0,0", + "0,1", + "0,3", + "0,4", + "1,1", + "1,3", + "2,0", + "2,1", + "2,2", + "2,3", + "2,4", + "3,0", + "3,4", + "4,0", + "4,1", + "4,2", + "4,4" + ], + "structure": [ + "(6, 4, 3)" + ], + "map": "first arrival at the target is the shortest path" + }, + { + "operation": "Return the target's distance", + "done": [ + "0,0", + "0,1", + "0,3", + "0,4", + "1,1", + "1,3", + "2,0", + "2,1", + "2,2", + "2,3", + "2,4", + "3,0", + "3,4", + "4,0", + "4,1", + "4,2", + "4,4" ], "structure": [], - "map": "return 2" + "map": "return 6" } ] } diff --git a/public/embed/subarray-sum-equals-k.html b/public/embed/subarray-sum-equals-k.html new file mode 100644 index 0000000..6e608c0 --- /dev/null +++ b/public/embed/subarray-sum-equals-k.html @@ -0,0 +1,68 @@ + + + + + + + + diff --git a/public/embed/swim-in-rising-water.html b/public/embed/swim-in-rising-water.html index 273704b..3302246 100644 --- a/public/embed/swim-in-rising-water.html +++ b/public/embed/swim-in-rising-water.html @@ -6,84 +6,219 @@ { "title": "Swim in Rising Water — Binary Search + BFS", "url": "https://leetcode.com/problems/swim-in-rising-water/", - "input": "grid = [[0, 2], [1, 3]]", - "output": "3", + "input": "grid = [[0, 1, 2, 3, 4], [24, 23, 22, 21, 5], [12, 13, 14, 15, 16], [11, 17, 18, 19, 20], [10, 9, 8, 7, 6]]", + "output": "16", "structure": "Queue (front → back)", "visual": { "type": "grid", "rows": [ [ 0, - 2 + 1, + 2, + 3, + 4 ], [ - 1, - 3 + 24, + 23, + 22, + 21, + 5 + ], + [ + 12, + 13, + 14, + 15, + 16 + ], + [ + 11, + 17, + 18, + 19, + 20 + ], + [ + 10, + 9, + 8, + 7, + 6 ] ] }, "states": [ { - "operation": "Try time = 1; only elevations 0 and 1 are available", + "operation": "Try time = 12; BFS follows elevations 0 through 5", "active": [ + "1,4" + ], + "done": [ "0,0", - "1,0" + "0,1", + "0,2", + "0,3", + "0,4" ], "structure": [ - "(0, 0)" + "(1, 4)" ], - "map": "binary search: left = 0, right = 3, mid = 1" + "map": "binary search: left = 0, right = 24, mid = 12" }, { - "operation": "BFS cannot enter the destination at elevation 3", + "operation": "From elevation 5, both routes toward the target are still too high", + "done": [ + "0,0", + "0,1", + "0,2", + "0,3", + "0,4", + "1,4" + ], + "structure": [], + "map": "16 and 21 are blocked; time 12 fails → left = 13" + }, + { + "operation": "Try time = 18; elevation 16 opens the route into the spiral", "active": [ - "1,0" + "2,4", + "2,3" ], "done": [ "0,0", - "1,0" + "0,1", + "0,2", + "0,3", + "0,4", + "1,4", + "2,4" ], - "structure": [], - "map": "time 1 fails → left = 2" + "structure": [ + "(2, 3)" + ], + "map": "left = 13, right = 24, mid = 18" }, { - "operation": "Try time = 2; destination remains above water", + "operation": "BFS explores the open 17 and 18 branches, then discovers the destination", "active": [ + "3,0", + "4,0", + "4,4" + ], + "done": [ "0,0", "0,1", - "1,0" + "0,2", + "0,3", + "0,4", + "1,4", + "2,0", + "2,1", + "2,2", + "2,3", + "2,4", + "3,1", + "3,2", + "4,1", + "4,2", + "4,3" ], "structure": [ - "(0, 0)" + "(3, 0)", + "(4, 0)", + "(4, 4)" ], - "map": "mid = 2" + "map": "time 18 works → answer = 18, right = 17" }, { - "operation": "Try time = 3; BFS reaches the target", + "operation": "Try time = 15; BFS stalls at elevation 5 again", "active": [ + "1,4" + ], + "done": [ "0,0", "0,1", - "1,1" + "0,2", + "0,3", + "0,4", + "1,4" + ], + "structure": [], + "map": "left = 13, right = 17, mid = 15; 16 is blocked → left = 16" + }, + { + "operation": "Try time = 16; BFS crosses the exact bottleneck", + "active": [ + "2,4", + "2,3" ], "done": [ "0,0", - "0,1" + "0,1", + "0,2", + "0,3", + "0,4", + "1,4", + "2,4" + ], + "structure": [ + "(2, 3)" + ], + "map": "left = 16, right = 17, mid = 16" + }, + { + "operation": "The route continues around the spiral to the destination", + "active": [ + "4,4" + ], + "done": [ + "0,0", + "0,1", + "0,2", + "0,3", + "0,4", + "1,4", + "2,0", + "2,1", + "2,2", + "2,3", + "2,4", + "3,0", + "4,0", + "4,1", + "4,2", + "4,3" ], "structure": [ - "(1, 1)" + "(4, 4)" ], - "map": "mid = 3 works → search lower" + "map": "time 16 works → answer = 16, right = 15" }, { - "operation": "Smallest working time found", + "operation": "The search bounds cross; 16 is the smallest working time", "done": [ "0,0", "0,1", - "1,0", - "1,1" + "0,2", + "0,3", + "0,4", + "1,4", + "2,0", + "2,1", + "2,2", + "2,3", + "2,4", + "3,0", + "4,0", + "4,1", + "4,2", + "4,3", + "4,4" ], "structure": [], - "map": "return 3" + "map": "left = 16, right = 15 → return 16" } ] } diff --git a/public/embed/trace-runner.js b/public/embed/trace-runner.js index e2cf53e..2d377b3 100644 --- a/public/embed/trace-runner.js +++ b/public/embed/trace-runner.js @@ -235,6 +235,39 @@ function renderArray(state) { $("stage").replaceChildren(array); } +function renderPrefixPath(state) { + const path = createElement("div", "prefix-path"); + const checkpoints = c.visual.prefixSums; + + checkpoints.forEach((sum, checkpointIndex) => { + const checkpoint = createElement( + "div", + [ + "checkpoint", + state.active?.includes(checkpointIndex) ? "active" : "", + state.done?.includes(checkpointIndex) ? "done" : "", + state.match?.includes(checkpointIndex) ? "match" : "", + ].filter(Boolean).join(" "), + ); + checkpoint.append( + createElement("small", "", checkpointIndex === 0 ? "before nums" : `after index ${checkpointIndex - 1}`), + createElement("strong", "", `sum ${sum}`), + ); + path.append(checkpoint); + + if (checkpointIndex < checkpoints.length - 1) { + const segment = createElement("div", "prefix-segment"); + segment.textContent = `${c.visual.values[checkpointIndex] >= 0 ? "+" : ""}${c.visual.values[checkpointIndex]}`; + if (state.range?.includes(checkpointIndex)) { + segment.classList.add("range"); + } + path.append(segment); + } + }); + + $("stage").replaceChildren(path); +} + function renderGraph(state) { const positions = c.visual.positions; const graph = createElement("div", "graph"); @@ -302,6 +335,8 @@ function render() { renderGrid(state); } else if (c.visual.type === "array") { renderArray(state); + } else if (c.visual.type === "prefix-path") { + renderPrefixPath(state); } else if (c.visual.type === "graph") { renderGraph(state); } else { diff --git a/public/embed/trace.css b/public/embed/trace.css index 0d364d4..9ed7361 100644 --- a/public/embed/trace.css +++ b/public/embed/trace.css @@ -1,17 +1,72 @@ -:root { color-scheme: light dark; --bg: light-dark(#f8fafc, #101827); --surface: light-dark(#ffffff, #172033); --text: light-dark(#172033, #e7edf7); --muted: light-dark(#5e6b7d, #aab6c8); --line: light-dark(#d7dfeb, #34425b); --accent: light-dark(#1264d6, #7eb6ff); --active: light-dark(#e6f0ff, #193d70); --done: light-dark(#e6f7ec, #173b2a); --blocked: light-dark(#fde9e8, #572626); } +:root { + color-scheme: dark; + --bg: #0d111b; + --surface: #141a28; + --surface-raised: #1b2335; + --text: #edf2ff; + --muted: #9eabc2; + --line: #303b55; + --accent: #78a8ff; + --active: #2a2545; + --done: #133b35; + --blocked: #542936; + --green: #55d3a7; + --amber: #ffc76e; +} * { box-sizing: border-box; } -body { margin: 0; padding: 16px; background: var(--bg); color: var(--text); font: 15px/1.45 system-ui, sans-serif; } -.trace { max-width: 920px; margin: auto; display: grid; gap: 14px; } -h1 { font-size: 1.3rem; margin: 0; } a { color: var(--accent); } -.problem, .status, .structure, .stage { border: 1px solid var(--line); border-radius: 10px; padding: 12px; background: var(--surface); } -.problem { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } .label { color: var(--muted); font-size: .8rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; } code { white-space: normal; overflow-wrap: anywhere; } -.toolbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; } button { padding: 7px 12px; border: 1px solid var(--line); color: var(--text); background: var(--surface); border-radius: 7px; font: inherit; cursor: pointer; } button:hover { border-color: var(--accent); } .step { margin-left: auto; color: var(--muted); } -.stage { min-height: 190px; display: grid; place-items: center; } -.grid { display: grid; gap: 6px; } .cell, .node, .array-cell, .token { border: 1px solid var(--line); border-radius: 8px; padding: 8px; text-align: center; background: var(--surface); min-width: 40px; } .cell.active, .node.active, .array-cell.active { background: var(--active); border-color: var(--accent); } .cell.done, .node.done, .array-cell.done { background: var(--done); } .cell.blocked { background: var(--blocked); } -.graph { width: min(640px, 100%); min-height: 170px; position: relative; } .edge { position: absolute; height: 2px; transform-origin: 0 0; background: var(--line); } .edge.allowed { background: var(--accent); } .edge.blocked { background: var(--blocked); opacity: .85; } .edge-label { position: absolute; padding: 1px 5px; border: 1px solid var(--line); border-radius: 999px; background: var(--surface); color: var(--text); font: 700 .75rem/1 ui-monospace, SFMono-Regular, Menlo, monospace; transform: translate(-50%, -50%); } .edge-label.allowed { border-color: var(--accent); } .edge-label.blocked { border-color: var(--blocked); } .node { position: absolute; width: 48px; height: 48px; display: grid; place-items: center; border-radius: 50%; } -.array { display: flex; gap: 7px; flex-wrap: wrap; justify-content: center; } .array-cell small { display:block; color:var(--muted); font-size:.7rem; } -.structure { display: grid; gap: 8px; } .tokens { display: flex; gap: 8px; flex-wrap: wrap; } .token:first-child { border-color: var(--accent); } .empty { color: var(--muted); } -.reference-code { border: 1px solid var(--line); border-radius: 10px; background: var(--surface); overflow: hidden; } .reference-code summary { padding: 12px; color: var(--accent); cursor: pointer; font-weight: 700; } .reference-code pre { margin: 0; padding: 14px; border-top: 1px solid var(--line); overflow: auto; background: #0b1220; font: .82rem/1.55 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre; tab-size: 4; } .reference-code code { font: inherit; white-space: pre; overflow-wrap: normal; } .syntax-keyword { color: #c792ea; } .syntax-string { color: #c3e88d; } .syntax-number { color: #f78c6c; } .syntax-comment { color: #718096; font-style: italic; } -.state-text { font-weight: 650; } .detail { color: var(--muted); } .map { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; } -.graph { min-height: 240px; } -@media (max-width: 560px) { body { padding: 10px; } .problem { grid-template-columns: 1fr; } .step { margin-left: 0; } .graph { transform: scale(.9); transform-origin: top left; width: 111%; } } +html, body, iframe { width: 100%; min-height: 100%; margin: 0; border: 0; } +body { padding: 18px; color: var(--text); background: radial-gradient(circle at 85% -8%, #212b54 0, transparent 34rem), var(--bg); font: 15px/1.45 "Space Grotesk", system-ui, sans-serif; } +.trace { width: min(100%, 980px); margin: auto; display: grid; gap: 13px; } +.trace > header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px 16px; border: 1px solid var(--line); border-radius: 12px; background: linear-gradient(110deg, #1d2640, #161b2a 52%, #251d42); } +h1 { margin: 0; font-size: clamp(1.1rem, 3vw, 1.45rem); font-weight: 600; letter-spacing: -.025em; } +a { color: var(--accent); } +.problem, .status, .structure, .stage { border: 1px solid var(--line); border-radius: 12px; background: var(--surface); } +.problem { display: grid; grid-template-columns: 1fr 1fr; gap: 0; overflow: hidden; } +.problem > div { padding: 14px; } +.problem > div + div { border-left: 1px solid var(--line); } +.label { color: var(--muted); font: 700 .68rem/1 "IBM Plex Mono", ui-monospace, monospace; letter-spacing: .09em; text-transform: uppercase; } +code { white-space: normal; overflow-wrap: anywhere; color: #d3def3; font-family: "IBM Plex Mono", ui-monospace, monospace; } +.status { padding: 13px 15px; border-left: 3px solid var(--accent); background: #17243a; } +.state-text { font-weight: 650; } +.detail { margin-top: 3px; color: var(--muted); } +.toolbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; padding: 2px 0; } +button { padding: 8px 11px; border: 1px solid var(--line); border-radius: 7px; color: var(--text); background: var(--surface-raised); font: inherit; cursor: pointer; } +button:hover { border-color: var(--accent); background: #293756; } +button:focus-visible, a:focus-visible { outline: 3px solid var(--amber); outline-offset: 3px; } +.step { margin-left: auto; color: var(--muted); font: .72rem/1 "IBM Plex Mono", ui-monospace, monospace; } +.stage { min-height: 205px; padding: 16px; display: grid; place-items: center; overflow: hidden; background: #111725; } +.grid { display: grid; gap: 7px; } +.cell, .node, .array-cell, .token { min-width: 40px; padding: 8px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface-raised); text-align: center; } +.cell.active, .node.active, .array-cell.active { border-color: var(--amber); background: var(--active); box-shadow: 0 0 0 2px #ffc76e22; } +.cell.done, .node.done, .array-cell.done { border-color: #287c68; background: var(--done); } +.cell.blocked { border-color: #a75062; background: var(--blocked); } +.graph { position: relative; width: min(640px, 100%); min-height: 240px; } +.edge { position: absolute; height: 2px; transform-origin: 0 0; background: var(--line); } +.edge.allowed { background: var(--accent); } +.edge.blocked { background: #c66076; opacity: .9; } +.edge-label { position: absolute; padding: 2px 5px; border: 1px solid var(--line); border-radius: 5px; background: var(--surface-raised); color: var(--text); font: 700 .72rem/1 "IBM Plex Mono", ui-monospace, monospace; transform: translate(-50%, -50%); } +.edge-label.allowed { border-color: var(--accent); } +.edge-label.blocked { border-color: #c66076; } +.node { position: absolute; width: 48px; height: 48px; display: grid; place-items: center; border-radius: 50%; } +.array { display: flex; gap: 7px; flex-wrap: wrap; justify-content: center; } +.array-cell small, .cell small { display: block; color: var(--muted); font-size: .7rem; } +.prefix-path { display: flex; align-items: center; justify-content: flex-start; width: 100%; overflow-x: auto; padding: 8px 0; } +.checkpoint { min-width: 104px; padding: 8px; display: grid; gap: 4px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface-raised); text-align: center; } +.checkpoint small { color: var(--muted); font-size: .7rem; } +.checkpoint.active { border-color: var(--amber); background: var(--active); box-shadow: 0 0 0 2px #ffc76e22; } +.checkpoint.done { border-color: #287c68; background: var(--done); } +.checkpoint.match { border-color: var(--accent); box-shadow: 0 0 0 2px #78a8ff24; } +.prefix-segment { position: relative; min-width: 46px; height: 2px; display: grid; place-items: center; background: var(--line); color: var(--muted); font: 700 .78rem/1 "IBM Plex Mono", ui-monospace, monospace; } +.prefix-segment::before { position: absolute; inset: -13px 0; content: ""; } +.prefix-segment.range { background: var(--accent); color: var(--text); } +.structure { padding: 14px; display: grid; gap: 9px; } +.tokens { display: flex; gap: 8px; flex-wrap: wrap; } +.token:first-child { border-color: var(--green); background: var(--done); } +.empty { color: var(--muted); } +.map { color: #d3def3; font-family: "IBM Plex Mono", ui-monospace, monospace; white-space: pre-wrap; } +.reference-code { overflow: hidden; border: 1px solid var(--line); border-radius: 12px; background: var(--surface); } +.reference-code summary { padding: 13px 14px; color: var(--accent); cursor: pointer; font-weight: 700; } +.reference-code pre { margin: 0; padding: 15px; overflow: auto; border-top: 1px solid var(--line); background: #0a0f19; font: .8rem/1.6 "IBM Plex Mono", ui-monospace, monospace; white-space: pre; tab-size: 4; } +.reference-code code { font: inherit; white-space: pre; overflow-wrap: normal; } +.syntax-keyword { color: #c7a7ff; }.syntax-string { color: #a6e3b9; }.syntax-number { color: #ffb97a; }.syntax-comment { color: #7f8ca4; font-style: italic; } +@media (max-width: 560px) { body { padding: 10px; } .trace > header { align-items: flex-start; flex-direction: column; } .problem { grid-template-columns: 1fr; } .problem > div + div { border-top: 1px solid var(--line); border-left: 0; } .step { width: 100%; margin-left: 0; } .graph { transform: scale(.9); transform-origin: top left; width: 111%; } } diff --git a/public/embed/two-sum.html b/public/embed/two-sum.html index ef5c794..9a73857 100644 --- a/public/embed/two-sum.html +++ b/public/embed/two-sum.html @@ -6,39 +6,40 @@ { "title": "Two Sum — Hash Map", "url": "https://leetcode.com/problems/two-sum/", - "input": "nums = [2, 7, 11, 15], target = 9", - "output": "[0, 1]", + "input": "nums = [4, 1, 9, 6, 3], target = 9", + "output": "[3, 4]", "structure": "Hash map (value → index)", "visual": { "type": "array", "values": [ - 2, - 7, - 11, - 15 + 4, + 1, + 9, + 6, + 3 ] }, "states": [ { - "operation": "Read index 0: number 2 needs complement 7", + "operation": "Read index 0: number 4 needs complement 5", "active": [ 0 ], "structure": [], - "map": "diff = 9 − 2 = 7" + "map": "diff = 9 − 4 = 5" }, { - "operation": "7 is not stored yet; record 2 → 0", + "operation": "5 is not stored yet; record 4 → 0", "done": [ 0 ], "structure": [ - "2 → 0" + "4 → 0" ], - "map": "prevMap = {2: 0}" + "map": "prevMap = {4: 0}" }, { - "operation": "Read index 1: number 7 needs complement 2", + "operation": "Read index 1: number 1 needs complement 8", "active": [ 1 ], @@ -46,24 +47,123 @@ 0 ], "structure": [ - "2 → 0" + "4 → 0" ], - "map": "diff = 9 − 7 = 2" + "map": "diff = 9 − 1 = 8" }, { - "operation": "2 is in the map, so return its index and this index", - "active": [ + "operation": "8 is not stored yet; record 1 → 1", + "done": [ 0, 1 ], + "structure": [ + "4 → 0", + "1 → 1" + ], + "map": "prevMap = {4: 0, 1: 1}" + }, + { + "operation": "Read index 2: number 9 needs complement 0", + "active": [ + 2 + ], "done": [ 0, 1 ], "structure": [ - "2 → 0" + "4 → 0", + "1 → 1" + ], + "map": "diff = 9 − 9 = 0" + }, + { + "operation": "0 is not stored yet; record 9 → 2", + "done": [ + 0, + 1, + 2 + ], + "structure": [ + "4 → 0", + "1 → 1", + "9 → 2" + ], + "map": "prevMap = {4: 0, 1: 1, 9: 2}" + }, + { + "operation": "Read index 3: number 6 needs complement 3", + "active": [ + 3 + ], + "done": [ + 0, + 1, + 2 + ], + "structure": [ + "4 → 0", + "1 → 1", + "9 → 2" + ], + "map": "diff = 9 − 6 = 3" + }, + { + "operation": "3 is not stored yet; record 6 → 3", + "done": [ + 0, + 1, + 2, + 3 + ], + "structure": [ + "4 → 0", + "1 → 1", + "9 → 2", + "6 → 3" + ], + "map": "prevMap = {4: 0, 1: 1, 9: 2, 6: 3}" + }, + { + "operation": "Read index 4: number 3 needs complement 6", + "active": [ + 4 + ], + "done": [ + 0, + 1, + 2, + 3 + ], + "structure": [ + "4 → 0", + "1 → 1", + "9 → 2", + "6 → 3" + ], + "map": "diff = 9 − 3 = 6" + }, + { + "operation": "6 is in the map, so return its index and this index", + "active": [ + 3, + 4 + ], + "done": [ + 0, + 1, + 2, + 3, + 4 + ], + "structure": [ + "4 → 0", + "1 → 1", + "9 → 2", + "6 → 3" ], - "map": "return [0, 1]" + "map": "return [3, 4]" } ] } diff --git a/public/route.css b/public/route.css index 863d474..db925cd 100644 --- a/public/route.css +++ b/public/route.css @@ -1,12 +1,4 @@ -html, -body, -iframe { - width: 100%; - height: 100%; - margin: 0; - border: 0; -} - -body { - background: #07111f; -} +html, body, iframe { width: 100%; height: 100%; margin: 0; border: 0; } +html { background: #0d111b; } +body { background: radial-gradient(circle at 85% -8%, #212b54 0, transparent 34rem), #0d111b; } +iframe { display: block; } diff --git a/public/subarray-sum-equals-k/index.html b/public/subarray-sum-equals-k/index.html new file mode 100644 index 0000000..b0227cd --- /dev/null +++ b/public/subarray-sum-equals-k/index.html @@ -0,0 +1,12 @@ + + + + + + Subarray Sum Equals K + + + + + + diff --git a/src/App.tsx b/src/App.tsx index 9f90adc..cdf537c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -47,28 +47,43 @@ export default function App() { <>
-

Graham’s algorithm library

+
+

algorithm visualisations

+

15 interactive traces

+
-

See the state change.

+

A learning debugger for DSA

+

Trace the
state change.

- Interactive traces for the exact queues, stacks, trees, graphs, and loops that - make algorithm patterns click. + Step through the exact queue, stack, heap, graph, or window state that makes an + algorithm click.

-

- Notion-ready - - Copy a visualisation URL, then paste it into a /embed block. - -

+
+

+ Notion-ready + Copy a visualisation URL and paste it into a /embed block. +

-

Browse by pattern

+

Choose a pattern

Visualisations

@@ -118,7 +133,7 @@ export default function App() {
- Open visualisation + Open trace