diff --git a/README.md b/README.md index 150e3a4..150c53f 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,14 @@ So far there are these segments - Graph Traversal (BFS / DFS) - Shortest Path - Minimum Spanning Tree +- Connectivity +- Network Flow - Prime Numbers - Sorting Algorithms - N Queen - Convex Hull - Binary Search Game +- Binary Search Tree - Recursion Tree - Turing Machine - Game of Life @@ -37,6 +40,8 @@ I have implemented a total of `30+ algorithms` so far. And will try to add more - Recursive Maze Creation - Data Structures - Linked List (insert, delete, search, reverse — singly & doubly) +- Trees + - Binary Search Tree (insert, delete, search with animated re-layout) - Graph Traversal - BFS - DFS @@ -46,6 +51,14 @@ I have implemented a total of `30+ algorithms` so far. And will try to add more - Minimum Spanning Tree - Kruskal - Prim +- Connectivity + - Connected Components + - Strongly Connected Components (Tarjan) + - Weakly Connected Components +- Network Flow + - Edmonds-Karp + - Ford-Fulkerson + - Min Cut - Sorting - Bubble sort - Selection sort @@ -83,6 +96,8 @@ I am not sure if anyone would like to contribute to this project or not. But any - Jun 2026: Added Linked List visualizer (singly & doubly) with staged insert/delete animations - Jun 2026: Added interactive Graph Traversal (BFS / DFS) built on React Flow - Jun 2026: Added Shortest Path (Dijkstra / Bellman-Ford) and Minimum Spanning Tree (Kruskal / Prim) on a shared, reusable graph workspace +- Jun 2026: Added Connectivity (components / SCC / WCC) and Network Flow (Edmonds-Karp / Ford-Fulkerson, max flow / min cut) +- Jun 2026: Added a reusable SVG tree component with a Binary Search Tree visualizer, and migrated the Recursion Tree onto it ### Acknowledgement diff --git a/public/images/bst.png b/public/images/bst.png new file mode 100644 index 0000000..d7d90bb Binary files /dev/null and b/public/images/bst.png differ diff --git a/public/images/connectivity.png b/public/images/connectivity.png new file mode 100644 index 0000000..37ed0bd Binary files /dev/null and b/public/images/connectivity.png differ diff --git a/public/images/network-flow.png b/public/images/network-flow.png new file mode 100644 index 0000000..18f2618 Binary files /dev/null and b/public/images/network-flow.png differ diff --git a/src/app/about/page.jsx b/src/app/about/page.jsx index 8d6df05..7d54002 100644 --- a/src/app/about/page.jsx +++ b/src/app/about/page.jsx @@ -15,12 +15,20 @@ const algorithms = [ "Linked List — insert, delete, search, reverse (singly & doubly)", ], }, + { + category: "Trees", + items: [ + "Binary Search Tree — insert, delete, search with animated re-layout", + ], + }, { category: "Interactive Graphs", items: [ "Graph Traversal — BFS / DFS", "Shortest Path — Dijkstra & Bellman-Ford (with negative-cycle detection)", "Minimum Spanning Tree — Kruskal & Prim", + "Connectivity — Connected Components, Strongly & Weakly Connected", + "Network Flow — max flow / min cut (Edmonds-Karp & Ford-Fulkerson)", ], }, { diff --git a/src/app/bst/page.jsx b/src/app/bst/page.jsx new file mode 100644 index 0000000..4ee4cfa --- /dev/null +++ b/src/app/bst/page.jsx @@ -0,0 +1,45 @@ +"use client"; + +import { useState } from 'react'; +import Navbar from '@/components/navbar'; +import { fromValues, insertActions, deleteActions, searchActions } from '@/lib/algorithms/bst'; +import { binaryLayout } from '@/components/tree/layout'; +import { useTreeEditor } from '@/components/tree/use-tree-editor'; +import TreeCanvas from '@/components/tree/tree-canvas'; +import TreeMenu from '@/components/tree/tree-menu'; + +export default function Bst() { + const [initialTree] = useState(() => fromValues([50, 30, 70, 20, 40, 60, 80])); + const g = useTreeEditor({ initialTree }); + + return ( +
+ +
+ g.run(insertActions(g.getContext().tree, v))} + onDelete={(v) => g.run(deleteActions(g.getContext().tree, v))} + onSearch={(v) => g.run(searchActions(g.getContext().tree, v))} + onClear={g.clear} + onSpeedChange={g.setSpeed} + /> +
+ {g.status && ( +
+ {g.status} +
+ )} + +
+
+
+ ); +} diff --git a/src/app/components/algorithm-cards.jsx b/src/app/components/algorithm-cards.jsx index 26d35c1..c28c7f7 100644 --- a/src/app/components/algorithm-cards.jsx +++ b/src/app/components/algorithm-cards.jsx @@ -25,6 +25,21 @@ const algorithms = [ title: 'Minimum Spanning Tree', description: "Build a weighted graph and watch Kruskal and Prim grow the minimum spanning tree", image: '/AlgorithmVisualizer/images/mst.png?height=200&width=300' + },{ + id: 'connectivity', + title: 'Connectivity', + description: "Build a graph and color its connected components and strongly connected components", + image: '/AlgorithmVisualizer/images/connectivity.png?height=200&width=300' + },{ + id: 'network-flow', + title: 'Network Flow', + description: "Compute max flow / min cut with Edmonds-Karp and Ford-Fulkerson on a capacity network", + image: '/AlgorithmVisualizer/images/network-flow.png?height=200&width=300' + },{ + id: 'bst', + title: 'Binary Search Tree', + description: "Insert, delete, and search on a BST with animated tree restructuring", + image: '/AlgorithmVisualizer/images/bst.png?height=200&width=300' }, { id: 'recursion-tree', diff --git a/src/app/connectivity/page.jsx b/src/app/connectivity/page.jsx new file mode 100644 index 0000000..c38ae35 --- /dev/null +++ b/src/app/connectivity/page.jsx @@ -0,0 +1,61 @@ +"use client"; + +import { useState } from 'react'; +import { ReactFlowProvider } from '@xyflow/react'; +import Navbar from '@/components/navbar'; +import { adjacency } from '@/lib/algorithms/graph'; +import { CONNECTIVITY_PRESETS, connectedComponentsActions, sccActions } from '@/lib/algorithms/connectivity'; +import { useGraphEditor } from '@/components/graph/use-graph-editor'; +import GraphCanvas from '@/components/graph/graph-canvas'; +import GraphMenu from '@/components/graph/graph-menu'; + +function ConnectivityInner() { + const g = useGraphEditor({ initialPreset: CONNECTIVITY_PRESETS[0] }); + const [algo, setAlgo] = useState(0); + + // Undirected: just Connected Components. Directed: SCC (strongly) + WCC (weakly). + const algorithms = g.directed + ? ['Strongly Connected (SCC)', 'Weakly Connected (WCC)'] + : ['Connected Components']; + + const onDirectedChange = (val) => { g.setDirected(val); setAlgo(0); }; + + const onVisualize = () => { + const { nodes, edges, directed } = g.getContext(); + const ids = nodes.map((n) => n.id); + // SCC only when directed + first option; everything else is the + // (undirected) connected-components computation — i.e. CC or WCC. + g.run(directed && algo === 0 + ? sccActions(adjacency(edges, true), ids) + : connectedComponentsActions(adjacency(edges, false), ids)); + }; + + return ( +
+ +
+ g.loadPreset(CONNECTIVITY_PRESETS[i])} + onSpeedChange={g.setSpeed} + onVisualize={onVisualize} + onClear={g.clear} + /> + +
+
+ ); +} + +export default function Connectivity() { + return ( + + + + ); +} diff --git a/src/app/network-flow/page.jsx b/src/app/network-flow/page.jsx new file mode 100644 index 0000000..16e0b31 --- /dev/null +++ b/src/app/network-flow/page.jsx @@ -0,0 +1,50 @@ +"use client"; + +import { useState } from 'react'; +import { ReactFlowProvider } from '@xyflow/react'; +import Navbar from '@/components/navbar'; +import { FLOW_PRESETS, maxFlowActions } from '@/lib/algorithms/networkFlow'; +import { useGraphEditor } from '@/components/graph/use-graph-editor'; +import GraphCanvas from '@/components/graph/graph-canvas'; +import GraphMenu from '@/components/graph/graph-menu'; + +function NetworkFlowInner() { + const g = useGraphEditor({ weighted: true, initialDirected: true, initialPreset: FLOW_PRESETS[0] }); + const [algo, setAlgo] = useState(0); // 0 = Edmonds-Karp, 1 = Ford-Fulkerson + + const onVisualize = () => { + const { edges, startId, finishId } = g.getContext(); + g.run(maxFlowActions(edges, startId, finishId, algo === 1 ? 'ff' : 'ek')); + }; + + return ( +
+ +
+ g.loadPreset(FLOW_PRESETS[i])} + onSpeedChange={g.setSpeed} + onVisualize={onVisualize} + onClear={g.clear} + /> + +
+
+ ); +} + +export default function NetworkFlow() { + return ( + + + + ); +} diff --git a/src/app/recursion-tree/Tree.js b/src/app/recursion-tree/Tree.js deleted file mode 100644 index 84edd46..0000000 --- a/src/app/recursion-tree/Tree.js +++ /dev/null @@ -1,235 +0,0 @@ -// draw tree class functions start :/ - - -export class Tree{ - constructor(node=0,children=[],label="") { - this.id = 0; - this.node = node; - this.label = label; - this.width = node.length; - this.children = children; - } -} - -export class DrawTree{ - constructor(tree,parent=undefined,depth=0,number=1) { - this.x =-1; - this.y = depth; - this.tree = tree; - this.children = []; - for( let i=0;i{ - if( this.thread!==undefined ) return this.thread; - if( this.children.length!==0 ) return this.children[0]; - return undefined; - return this.thread || this.children.length && this.children[0]; - } - right = ()=>{ - if( this.thread ) return this.thread; - if( this.children.length ) return this.children[this.children.length-1]; - return undefined; - return this.thread || this.children.length && this.children[-1]; - } - lbrother = ()=>{ - let n = undefined; - if( this.parent ){ - // for(let node in this.parent.children) - for(let i=0;i{ - if( !this._lmost_sibling && this.parent && this!==this.parent.children[0] ){ - this._lmost_sibling = this.parent.children[0]; - } - return this._lmost_sibling; - } - - -} - - - - -export function buchheim(tree) { - let dt = firstwalk(new DrawTree(tree)) - let min = second_walk(dt) - if (min < 0) { - third_walk(dt, -min); - } - return dt -} - -function third_walk(tree, n) { - tree.x += n; - //for (let c in tree.children) - for(let i=0;i 0) { - move_subtree(ancestor(vil, v, default_ancestor), v, shift); - sir = sir + shift; - sor = sor + shift; - } - sil += vil.mod; - sir += vir.mod; - sol += vol.mod; - sor += vor.mod; - } - if (vil.right() && !vor.right()) { - vor.thread = vil.right(); - vor.mod += sil - sor; - } else { - if (vir.left() && !vol.left()) { - vol.thread = vir.left(); - vol.mod += sir - sol; - } - default_ancestor = v - } - } - return default_ancestor -} - -function move_subtree(wl, wr, shift) { - let subtrees = wr.number - wl.number; - // console.log(wl.tree, "is conflicted with", wr.tree, 'moving', subtrees, 'shift', shift); - // print wl, wr, wr.number, wl.number, shift, subtrees, shift / subtrees - wr.change -= shift / subtrees; - wr.shift += shift; - wl.change += shift / subtrees; - wr.x += shift; - wr.mod += shift; -} - -function execute_shifts(v) { - let shift, change; - shift = change = 0; - // for (let w in v.children[:: - 1]) - for(let i=v.children.length-1;i>=0;i--){ - let w = v.children[i]; - //console.log("shift:", w.tree.node, shift, w.change); - w.x += shift; - w.mod += shift; - change += w.change; - shift += w.shift + change; - } -} - -function ancestor(vil, v, default_ancestor) { - - if (vil.ancestor in v.parent.children){ - - return vil.ancestor; - } - else - return default_ancestor; -} - -function second_walk(v, m = 0, depth = 0, min = undefined) { - v.x += m; - v.y = depth; - - if (min === undefined || v.x < min) - min = v.x; - - // for (let w in v.children) - for(let i=0;i - - - - - - - - - - {edges.map((edge, cellidx) => ( - - ))} - {vertices.map((vertex, cellidx) => ( - - ))} - - - ); -} diff --git a/src/app/recursion-tree/edge.jsx b/src/app/recursion-tree/edge.jsx deleted file mode 100644 index 2d9d026..0000000 --- a/src/app/recursion-tree/edge.jsx +++ /dev/null @@ -1,57 +0,0 @@ -import { useEffect, useRef } from 'react'; - -export default function Edge({ id, pos }) { - const prevX1 = useRef(pos.x1); - - useEffect(() => { - document.getElementById('vbanim1' + id)?.beginElement(); - document.getElementById('vbanim2' + id)?.beginElement(); - }, []); - - useEffect(() => { - if (prevX1.current !== pos.x1) { - prevX1.current = pos.x1; - document.getElementById('vbanim1' + id)?.beginElement(); - document.getElementById('vbanim2' + id)?.beginElement(); - } - }, [pos.x1]); - - const getEndX = () => { - let { x1, y1, x2, y2 } = pos; - let l = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); - let r = 6.5; - return (x2 * (l - r) + x1 * r) / l; - }; - - const getEndY = () => { - let { x1, y1, x2, y2 } = pos; - let l = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); - let r = 6.5; - return (y2 * (l - r) + y1 * r) / l; - }; - - return ( - - - - - - - ); -} diff --git a/src/app/recursion-tree/fib.jsx b/src/app/recursion-tree/fib.jsx index 8f10281..6fa56bf 100644 --- a/src/app/recursion-tree/fib.jsx +++ b/src/app/recursion-tree/fib.jsx @@ -1,79 +1,99 @@ -import {Tree,buchheim} from './Tree'; -export function getTree(n,algo=0,r=0){ - if(algo === 0) - return buchheim( fib(n) ); - else if( algo === 1 ) - return buchheim(NcR(n,r)); - else if( algo === 2 ) - return buchheim(derangement(n)); - else if( algo === 3 ) - return buchheim(bigmod(n,r)); - else if( algo === 4 ) - return buchheim(stirling2(n,r)); -} +// Recursion-tree builders + action planner for the tree component. +// +// Each builder returns a logical tree of nodes shaped for the tree component: +// { id, value, ret, children: [] } +// where `value` is the call label shown in the node and `ret` is the return +// value, revealed as the node's secondary label on the way back up. + +let counter = 0; +const nid = () => 'r' + counter++; -function fib(n){ - let tree = new Tree(n,[],n+""); - if( n <2 ) return tree; - tree.children.push( fib(n-1) ); - tree.children.push( fib(n-2) ); - tree.node = tree.children[0].node+tree.children[1].node; - return tree; +export function getTree(n, algo = 0, r = 0) { + counter = 0; + if (algo === 0) return fib(n); + if (algo === 1) return NcR(n, r); + if (algo === 2) return derangement(n); + if (algo === 3) return bigmod(n, r); + return stirling2(n, r); } -function NcR(n,r){ - if (r > n) - return new Tree(-1,[],"("+n+","+r+")"); +// today's DFS reveal, expressed as an action list: lay out the full tree once +// (hidden), then reveal each node as it's entered (current → waiting → done) +// and set its return value as the secondary label on the way back up. +export function recursionActions(root) { + const actions = [{ type: 'setTree', tree: root, hidden: true }]; + const visit = (node) => { + actions.push({ type: 'markNode', id: node.id, state: 'current' }); + for (const child of node.children) { + actions.push({ type: 'markNode', id: node.id, state: 'normal' }); // waiting on a child + visit(child); + actions.push({ type: 'markNode', id: node.id, state: 'current' }); // control returned + } + actions.push({ type: 'setLabel', id: node.id, secondary: '= ' + node.ret }); + actions.push({ type: 'markNode', id: node.id, state: 'visited' }); + }; + visit(root); + return actions; +} - if (n === r) - return new Tree(1,[],"("+n+","+r+")"); +function fib(n) { + const node = { id: nid(), value: '' + n, ret: n, children: [] }; + if (n < 2) return node; + node.children.push(fib(n - 1)); + node.children.push(fib(n - 2)); + node.ret = node.children[0].ret + node.children[1].ret; + return node; +} - if (r === 0) - return new Tree(1,[],"("+n+","+r+")");; +function NcR(n, r) { + const label = '(' + n + ',' + r + ')'; + if (r > n) return { id: nid(), value: label, ret: -1, children: [] }; + if (n === r) return { id: nid(), value: label, ret: 1, children: [] }; + if (r === 0) return { id: nid(), value: label, ret: 1, children: [] }; // nCr(n, r) = nCr(n - 1, r - 1) + nCr(n - 1, r) - let tree = new Tree(0,[],"("+n+","+r+")"); - tree.children.push( NcR(n-1,r-1) ); - tree.children.push( NcR(n-1,r) ); - tree.node = tree.children[0].node+tree.children[1].node; - return tree; + const node = { id: nid(), value: label, ret: 0, children: [] }; + node.children.push(NcR(n - 1, r - 1)); + node.children.push(NcR(n - 1, r)); + node.ret = node.children[0].ret + node.children[1].ret; + return node; } -function derangement(n){ - if( n == 0 ) return new Tree(1,[],n+""); - if( n == 1 ) return new Tree(0,[],n+""); - let tree = new Tree(0,[],n+""); - tree.children.push( derangement(n-1) ); - tree.children.push( derangement(n-2) ); - tree.node = (n-1)*(tree.children[0].node+tree.children[1].node); - return tree; +function derangement(n) { + const label = '' + n; + if (n === 0) return { id: nid(), value: label, ret: 1, children: [] }; + if (n === 1) return { id: nid(), value: label, ret: 0, children: [] }; + const node = { id: nid(), value: label, ret: 0, children: [] }; + node.children.push(derangement(n - 1)); + node.children.push(derangement(n - 2)); + node.ret = (n - 1) * (node.children[0].ret + node.children[1].ret); + return node; } -function bigmod(n,r){ - if( r === 0 ) return new Tree(1,[],"("+n+","+r+")"); - if( r === 1 ) return new Tree(n,[],"("+n+","+r+")"); - let tree = new Tree(1,[],"("+n+","+r+")"); - if( r%2 === 1 ){ - tree.children.push( bigmod(n,(r-1)/2 ) ); - tree.children.push( bigmod(n,(r-1)/2) ); - tree.children.push( bigmod(n,1) ); - }else{ - tree.children.push( bigmod(n,r/2 ) ); - tree.children.push( bigmod(n,r/2) ); +function bigmod(n, r) { + const label = '(' + n + ',' + r + ')'; + if (r === 0) return { id: nid(), value: label, ret: 1, children: [] }; + if (r === 1) return { id: nid(), value: label, ret: n, children: [] }; + const node = { id: nid(), value: label, ret: 1, children: [] }; + if (r % 2 === 1) { + node.children.push(bigmod(n, (r - 1) / 2)); + node.children.push(bigmod(n, (r - 1) / 2)); + node.children.push(bigmod(n, 1)); + } else { + node.children.push(bigmod(n, r / 2)); + node.children.push(bigmod(n, r / 2)); } - for(let i=0;i { - if (isRunningRef.current) return; - setIsRunning(true); - isRunningRef.current = true; - - let tree = getTree(n, algo, r); - setEdges([]); - setVertices([]); - setOffset(tree.x); - verticesRef.current = []; - edgesRef.current = []; - await recur(tree, undefined); - setIsRunning(false); - isRunningRef.current = false; - }; - - const recur = async (node, parent) => { - let verts = verticesRef.current; - let currentIdx = verts.length; - - let newVertex; - if (parent !== undefined) { - newVertex = node.children.length - ? { label: node.tree.label, val: 0, x: node.x, y: node.y, px: parent.x, py: parent.y, completed: false } - : { label: node.tree.label, val: node.tree.node, x: node.x, y: node.y, px: parent.x, py: parent.y, completed: false }; - - verts = [...verts, newVertex]; - verticesRef.current = verts; - setVertices([...verts]); - setCurrent(currentIdx); - - let newEdge = { x1: parent.x, y1: parent.y, x2: node.x, y2: node.y }; - edgesRef.current = [...edgesRef.current, newEdge]; - setEdges([...edgesRef.current]); - } else { - newVertex = node.children.length - ? { label: node.tree.label, val: 0, x: node.x, y: node.y, px: node.x, py: node.y, completed: false } - : { label: node.tree.label, val: node.tree.node, x: node.x, y: node.y, px: node.x, py: node.y, completed: false }; - - verts = [...verts, newVertex]; - verticesRef.current = verts; - setVertices([...verts]); - setCurrent(currentIdx); - } - await sleep(500); - - for (let i = 0; i < node.children.length; i++) { - await recur(node.children[i], node); - setCurrent(currentIdx); - await sleep(500); - } - - let updatedVerts = [...verticesRef.current]; - updatedVerts[currentIdx] = { ...updatedVerts[currentIdx], val: node.tree.node, completed: true }; - verticesRef.current = updatedVerts; - setVertices([...updatedVerts]); - }; + const onStart = () => g.run(recursionActions(getTree(n, algo, r))); return (
@@ -86,24 +24,24 @@ export default function Graph() { setN={setN} setR={setR} setAlgo={setAlgo} - onStart={addNumber} - disabled={isRunning} + onStart={onStart} + disabled={g.isRunning} /> -
-
- -
+
+ {g.status && ( +
+ {g.status} +
+ )} +
); } - -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} diff --git a/src/app/recursion-tree/vertex.jsx b/src/app/recursion-tree/vertex.jsx deleted file mode 100644 index 39081aa..0000000 --- a/src/app/recursion-tree/vertex.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import { useEffect } from 'react'; - -export default function Vertex({ id, pos, current, completed, label, ret }) { - useEffect(() => { - if (id === 0) return; - document.getElementById('cxanim' + id)?.beginElement(); - document.getElementById('cyanim' + id)?.beginElement(); - document.getElementById('tanim' + id)?.beginElement(); - }, []); - - const fillColor = current ? '#f59e0b' : completed ? '#334155' : '#0d9488'; - const strokeColor = current ? '#d97706' : completed ? '#475569' : '#0f766e'; - const textColor = '#f8fafc'; - - return ( - - - - - - - - N:{label} - R:{ret} - - - ); -} diff --git a/src/components/graph/floating-edge.jsx b/src/components/graph/floating-edge.jsx index 40e21d2..ca3b208 100644 --- a/src/components/graph/floating-edge.jsx +++ b/src/components/graph/floating-edge.jsx @@ -1,5 +1,5 @@ import { createContext, useContext, useState } from 'react'; -import { BaseEdge, EdgeLabelRenderer, getStraightPath, useInternalNode } from '@xyflow/react'; +import { BaseEdge, EdgeLabelRenderer, getStraightPath, useInternalNode, useStore } from '@xyflow/react'; // Edge that connects node centers (no handles), trimmed to each node's border. // Stroke color encodes the edge state; during traversal a big arrow is drawn at @@ -31,13 +31,28 @@ export default function FloatingEdge({ id, source, target, markerEnd, data, sele const setWeight = useContext(EdgeWeightContext); const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(''); + // true when an opposite edge (target -> source) also exists + const hasOpposite = useStore((s) => s.edges.some((e) => e.source === target && e.target === source)); const sourceNode = useInternalNode(source); const targetNode = useInternalNode(target); if (!sourceNode || !targetNode) return null; - const sc = center(sourceNode); - const tc = center(targetNode); + let sc = center(sourceNode); + let tc = center(targetNode); + + // For a bidirectional pair, shift each edge to its own side (parallel) so the + // two arrows + labels don't overlap. Each edge's stable perpendicular points + // opposite to its twin's, so they separate cleanly. + if (hasOpposite) { + const dxx = tc.x - sc.x; + const dyy = tc.y - sc.y; + const dl = Math.hypot(dxx, dyy) || 1; + const ox = (-dyy / dl) * 6; + const oy = (dxx / dl) * 6; + sc = { x: sc.x + ox, y: sc.y + oy }; + tc = { x: tc.x + ox, y: tc.y + oy }; + } const state = data?.state || 'normal'; const travelTo = data?.travelTo; @@ -138,7 +153,8 @@ export default function FloatingEdge({ id, source, target, markerEnd, data, sele style={{ width: 36, border: 'none', outline: 'none', font: 'inherit', textAlign: 'center' }} /> ) : ( - data.weight + // show "flow / capacity" during a flow run, else just the capacity + data.flow != null ? `${data.flow} / ${data.weight}` : data.weight )} diff --git a/src/components/graph/graph-menu.jsx b/src/components/graph/graph-menu.jsx index 1daaf43..7a50a92 100644 --- a/src/components/graph/graph-menu.jsx +++ b/src/components/graph/graph-menu.jsx @@ -49,6 +49,9 @@ export default function GraphMenu({ )} 1100 - s * 10; -// preset -> { nodes, edges, startId } with the first node marked as start +// preset -> { nodes, edges, startId, finishId }. The start/finish roles come +// from preset.source / preset.sink when given, else start = first node. function seed(preset) { const { nodes, edges } = toFlow(preset); - if (nodes[0]) nodes[0] = { ...nodes[0], data: { ...nodes[0].data, role: 'start' } }; - return { nodes, edges, startId: nodes[0]?.id ?? null }; + const startId = preset.source ?? nodes[0]?.id ?? null; + const finishId = preset.sink ?? null; + const withRoles = nodes.map((n) => { + if (n.id === startId) return { ...n, data: { ...n.data, role: 'start' } }; + if (n.id === finishId) return { ...n, data: { ...n.data, role: 'finish' } }; + return n; + }); + return { nodes: withRoles, edges, startId, finishId }; } -export function useGraphEditor({ weighted = false, initialPreset }) { - const [initial] = useState(() => seed(initialPreset)); +export function useGraphEditor({ weighted = false, initialDirected = false, initialPreset }) { + const [initial] = useState(() => { + const s = seed(initialPreset); + if (initialDirected) s.edges = s.edges.map((e) => ({ ...e, markerEnd: ARROW })); + return s; + }); const [nodes, setNodes, onNodesChange] = useNodesState(initial.nodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initial.edges); - const [directed, setDirectedState] = useState(false); + const [directed, setDirectedState] = useState(initialDirected); const [mode, setMode] = useState('idle'); const [status, setStatus] = useState(''); const [isRunning, setIsRunning] = useState(false); @@ -36,9 +47,9 @@ export function useGraphEditor({ weighted = false, initialPreset }) { const speedRef = useRef(toDelay(50)); const modeRef = useRef('idle'); const pendingEdgeRef = useRef(null); - const directedRef = useRef(false); + const directedRef = useRef(initialDirected); const startIdRef = useRef(initial.startId); - const finishIdRef = useRef(null); + const finishIdRef = useRef(initial.finishId); const labelRef = useRef(0); useEffect(() => { nodesRef.current = nodes; }, [nodes]); @@ -52,9 +63,13 @@ export function useGraphEditor({ weighted = false, initialPreset }) { setEdges((es) => es.map((e) => (e.id === id ? { ...e, data: { ...e.data, state, travelTo: to ?? null } } : e))); const setDist = (id, dist) => setNodes((ns) => ns.map((n) => (n.id === id ? { ...n, data: { ...n.data, dist } } : n))); + const colorNode = (id, color) => + setNodes((ns) => ns.map((n) => (n.id === id ? { ...n, data: { ...n.data, color } } : n))); + const setFlow = (id, flow) => + setEdges((es) => es.map((e) => (e.id === id ? { ...e, data: { ...e.data, flow } } : e))); const clearMarks = () => { - setNodes((ns) => ns.map((n) => ({ ...n, data: { ...n.data, state: 'normal', dist: undefined } }))); - setEdges((es) => es.map((e) => ({ ...e, data: { ...e.data, state: 'normal', travelTo: null } }))); + setNodes((ns) => ns.map((n) => ({ ...n, data: { ...n.data, state: 'normal', dist: undefined, color: undefined } }))); + setEdges((es) => es.map((e) => ({ ...e, data: { ...e.data, state: 'normal', travelTo: null, flow: undefined } }))); setStatus(''); }; @@ -62,6 +77,8 @@ export function useGraphEditor({ weighted = false, initialPreset }) { if (action.type === 'markNode') markNode(action.id, action.state); else if (action.type === 'markEdge') markEdge(action.id, action.state, action.to); else if (action.type === 'setDist') setDist(action.id, action.dist); + else if (action.type === 'colorNode') colorNode(action.id, action.color); + else if (action.type === 'setFlow') setFlow(action.id, action.flow); else if (action.type === 'status') setStatus(action.text); else if (action.type === 'clear') clearMarks(); }; @@ -215,14 +232,28 @@ export function useGraphEditor({ weighted = false, initialPreset }) { const setDirected = (val) => { directedRef.current = val; setDirectedState(val); - setEdges((es) => es.map((e) => ({ ...e, markerEnd: val ? ARROW : undefined }))); + setEdges((es) => { + let next = es; + // an undirected graph shouldn't keep both A->B and B->A: collapse + // reverse-duplicate pairs (keep the first of each unordered pair) + if (!val) { + const seen = new Set(); + next = es.filter((e) => { + const key = e.source < e.target ? `${e.source}~${e.target}` : `${e.target}~${e.source}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + } + return next.map((e) => ({ ...e, markerEnd: val ? ARROW : undefined })); + }); }; const loadPreset = (preset) => { if (isRunningRef.current) return; const g = seed(preset); const e = directedRef.current ? g.edges.map((x) => ({ ...x, markerEnd: ARROW })) : g.edges; nodesRef.current = g.nodes; edgesRef.current = e; - startIdRef.current = g.startId; finishIdRef.current = null; + startIdRef.current = g.startId; finishIdRef.current = g.finishId; labelRef.current = 0; setNodes(g.nodes); setEdges(e); setModeBoth('idle'); setStatus(''); pendingEdgeRef.current = null; diff --git a/src/components/tree/layout.js b/src/components/tree/layout.js new file mode 100644 index 0000000..6932489 --- /dev/null +++ b/src/components/tree/layout.js @@ -0,0 +1,215 @@ +// Tree layout helpers. A layout takes a logical tree and returns +// { pos: { [id]: { x, y } }, cols, depth } +// where x is a column index (0..cols-1) and y is the depth. The canvas scales +// these into pixels. Binary layout preserves strict left/right (x by in-order +// index) which a tidy n-ary layout would not. (Buchheim is added in phase 2.) + +// children of a node, handling both binary (left/right) and n-ary (children[]) +export function childrenOf(node) { + if (node.children) return node.children.filter(Boolean); + return [node.left, node.right].filter(Boolean); +} + +export function flattenTree(root) { + const nodes = []; + const edges = []; + const walk = (node, parent) => { + if (!node) return; + nodes.push(node); + if (parent) edges.push({ parent: parent.id, child: node.id }); + for (const c of childrenOf(node)) walk(c, node); + }; + walk(root, null); + return { nodes, edges }; +} + +// In-order x, depth y — keeps a single child on its correct side. +export function binaryLayout(root) { + const pos = {}; + let idx = 0; + let depth = 0; + const walk = (node, d) => { + if (!node) return; + walk(node.left, d + 1); + pos[node.id] = { x: idx++, y: d }; + if (d > depth) depth = d; + walk(node.right, d + 1); + }; + walk(root, 0); + return { pos, cols: idx, depth }; +} + +// Buchheim tidy-tree layout for general n-ary trees (recursion trees). Ported +// from the recursion-tree's original Tree.js. Returns the same +// { pos, cols, depth } shape as binaryLayout; x positions are normalised so the +// leftmost node sits at x = 0. +export function buchheimLayout(root) { + const dt = buchheim(root); + const pos = {}; + let minX = Infinity; + let maxX = -Infinity; + let depth = 0; + const collect = (d) => { + pos[d.tree.id] = { x: d.x, y: d.y }; + if (d.x < minX) minX = d.x; + if (d.x > maxX) maxX = d.x; + if (d.y > depth) depth = d.y; + for (const c of d.children) collect(c); + }; + collect(dt); + for (const id in pos) pos[id].x -= minX; // normalise so leftmost is 0 + return { pos, cols: maxX - minX + 1, depth }; +} + +class DrawTree { + constructor(tree, parent = undefined, depth = 0, number = 1) { + this.x = -1; + this.y = depth; + this.tree = tree; + this.children = (tree.children || []).map( + (child, i) => new DrawTree(child, this, depth + 1, i + 1), + ); + this.parent = parent; + this.thread = undefined; + this.mod = 0; + this.ancestor = this; + this.change = 0; + this.shift = 0; + this._lmost_sibling = undefined; + this.number = number; + } + + left = () => this.thread || (this.children.length ? this.children[0] : undefined); + right = () => this.thread || (this.children.length ? this.children[this.children.length - 1] : undefined); + + lbrother = () => { + let n; + if (this.parent) { + for (const node of this.parent.children) { + if (node === this) return n; + n = node; + } + } + return n; + }; + + get_lmost_sibling = () => { + if (!this._lmost_sibling && this.parent && this !== this.parent.children[0]) { + this._lmost_sibling = this.parent.children[0]; + } + return this._lmost_sibling; + }; +} + +function buchheim(tree) { + const dt = firstwalk(new DrawTree(tree)); + const min = second_walk(dt); + if (min < 0) third_walk(dt, -min); + return dt; +} + +function third_walk(tree, n) { + tree.x += n; + for (const c of tree.children) third_walk(c, n); +} + +function firstwalk(v, distance = 1) { + if (v.children.length === 0) { + v.x = v.get_lmost_sibling() ? v.lbrother().x + distance : 0; + } else { + let default_ancestor = v.children[0]; + for (const w of v.children) { + firstwalk(w); + default_ancestor = apportion(w, default_ancestor, distance); + } + execute_shifts(v); + const midpoint = (v.children[0].x + v.children[v.children.length - 1].x) / 2; + const w = v.lbrother(); + if (w) { + v.x = w.x + distance; + v.mod = v.x - midpoint; + } else { + v.x = midpoint; + } + } + return v; +} + +function apportion(v, default_ancestor, distance) { + const w = v.lbrother(); + if (w !== undefined) { + let vir = v; + let vor = v; + let vil = w; + let vol = v.get_lmost_sibling(); + let sir = v.mod; + let sor = v.mod; + let sil = vil.mod; + let sol = vol.mod; + while (vil.right() && vir.left()) { + vil = vil.right(); + vir = vir.left(); + vol = vol.left(); + vor = vor.right(); + vor.ancestor = v; + const shift = vil.x + sil - (vir.x + sir) + distance; + if (shift > 0) { + move_subtree(ancestor(vil, v, default_ancestor), v, shift); + sir += shift; + sor += shift; + } + sil += vil.mod; + sir += vir.mod; + sol += vol.mod; + sor += vor.mod; + } + if (vil.right() && !vor.right()) { + vor.thread = vil.right(); + vor.mod += sil - sor; + } else { + if (vir.left() && !vol.left()) { + vol.thread = vir.left(); + vol.mod += sir - sol; + } + default_ancestor = v; + } + } + return default_ancestor; +} + +function move_subtree(wl, wr, shift) { + const subtrees = wr.number - wl.number; + wr.change -= shift / subtrees; + wr.shift += shift; + wl.change += shift / subtrees; + wr.x += shift; + wr.mod += shift; +} + +function execute_shifts(v) { + let shift = 0; + let change = 0; + for (let i = v.children.length - 1; i >= 0; i--) { + const w = v.children[i]; + w.x += shift; + w.mod += shift; + change += w.change; + shift += w.shift + change; + } +} + +// The original shipped a simplified Buchheim where this always fell through to +// default_ancestor; kept as-is so the recursion-tree layout is unchanged. +function ancestor(_vil, _v, default_ancestor) { + return default_ancestor; +} + +function second_walk(v, m = 0, depth = 0, min = undefined) { + v.x += m; + v.y = depth; + if (min === undefined || v.x < min) min = v.x; + for (const w of v.children) { + min = second_walk(w, m + v.mod, depth + 1, min); + } + return min; +} diff --git a/src/components/tree/tree-canvas.jsx b/src/components/tree/tree-canvas.jsx new file mode 100644 index 0000000..08bfd61 --- /dev/null +++ b/src/components/tree/tree-canvas.jsx @@ -0,0 +1,117 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import TreeNode from './tree-node'; +import TreeEdge from './tree-edge'; +import { flattenTree } from './layout'; + +// Renders a logical tree. On layout change, node positions are tweened with a +// single requestAnimationFrame loop and BOTH nodes and edges read the same +// interpolated coordinates each frame — so edges stay attached to the nodes +// (SVG line endpoints can't be CSS-transitioned, which is why we tween in JS). + +const COLGAP = 46; +const ROWGAP = 66; +const PAD = 28; +const DURATION = 600; + +const ease = (t) => (t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2); + +export default function TreeCanvas({ tree, layout, nodeState = {}, edgeState = {}, labels = {} }) { + const [disp, setDisp] = useState(new Map()); // id -> currently displayed {x, y} + const [box, setBox] = useState(null); // tweened viewBox { W, H } + const rafRef = useRef(0); + + const model = useMemo(() => { + if (!tree) return null; + const { pos, cols, depth } = layout(tree); + const { nodes, edges } = flattenTree(tree); + const W = PAD * 2 + Math.max(cols - 1, 0) * COLGAP; + const H = PAD * 2 + depth * ROWGAP; + const target = new Map(); + for (const n of nodes) { + target.set(n.id, { x: PAD + pos[n.id].x * COLGAP, y: PAD + pos[n.id].y * ROWGAP }); + } + return { nodes, edges, W, H, target }; + }, [tree, layout]); + + // signature of target positions; the tween restarts only when these change + const sig = model + ? [...model.target.entries()].map(([id, p]) => `${id}:${p.x},${p.y}`).join('|') + : ''; + + useEffect(() => { + if (!model) return undefined; + // existing nodes slide from where they are now; a NEW node emerges from + // its parent's current spot and slides in with everyone else (so it + // doesn't pop into place ahead of the nodes that still need to shift) + const parentOf = new Map(); + for (const e of model.edges) parentOf.set(e.child, e.parent); + const startPos = new Map(); + for (const n of model.nodes) { + if (disp.has(n.id)) { + startPos.set(n.id, disp.get(n.id)); + } else { + const pid = parentOf.get(n.id); + const from = (pid && (disp.get(pid) || model.target.get(pid))) || model.target.get(n.id); + startPos.set(n.id, from); + } + } + // tween the viewBox too, so the auto-fit rescale on growth happens + // smoothly in lockstep with the nodes (an instant rescale would make + // nodes appear to arrive at different times in screen space) + const startBox = box || { W: model.W, H: model.H }; + const start = performance.now(); + cancelAnimationFrame(rafRef.current); + const step = (now) => { + const t = Math.min(1, (now - start) / DURATION); + const e = ease(t); + const next = new Map(); + for (const n of model.nodes) { + const s = startPos.get(n.id); + const g = model.target.get(n.id); + next.set(n.id, { x: s.x + (g.x - s.x) * e, y: s.y + (g.y - s.y) * e }); + } + setDisp(next); + setBox({ W: startBox.W + (model.W - startBox.W) * e, H: startBox.H + (model.H - startBox.H) * e }); + if (t < 1) rafRef.current = requestAnimationFrame(step); + }; + rafRef.current = requestAnimationFrame(step); + return () => cancelAnimationFrame(rafRef.current); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sig]); + + if (!model) { + return
empty tree
; + } + + const at = (id) => disp.get(id) || model.target.get(id); + const vbW = box ? box.W : model.W; + const vbH = box ? box.H : model.H; + + return ( + + + + + + + {model.edges.map((e) => { + if (nodeState[e.parent] === 'hidden' || nodeState[e.child] === 'hidden') return null; + const a = at(e.parent); + const b = at(e.child); + return ; + })} + {model.nodes.map((n) => { + const p = at(n.id); + return ( + + ); + })} + + ); +} diff --git a/src/components/tree/tree-edge.jsx b/src/components/tree/tree-edge.jsx new file mode 100644 index 0000000..32e7b97 --- /dev/null +++ b/src/components/tree/tree-edge.jsx @@ -0,0 +1,24 @@ +import { TREE_NODE_R } from './tree-node'; + +// Parent→child line, trimmed to the node radius. Transitions so it follows +// nodes as they slide on re-layout. + +const STROKE = { tree: '#f59e0b', path: '#10b981', normal: '#64748b' }; + +export default function TreeEdge({ x1, y1, x2, y2, state }) { + const dx = x2 - x1; + const dy = y2 - y1; + const len = Math.hypot(dx, dy) || 1; + const ux = dx / len; + const uy = dy / len; + return ( + + ); +} diff --git a/src/components/tree/tree-menu.jsx b/src/components/tree/tree-menu.jsx new file mode 100644 index 0000000..f5337cc --- /dev/null +++ b/src/components/tree/tree-menu.jsx @@ -0,0 +1,60 @@ +import { useState } from 'react'; +import { CustomSlider } from '@/components/custom-slider'; +import { Button } from '@/components/ui/button'; +import { Plus, Minus, Search, RotateCcw } from 'lucide-react'; + +// Sidebar for tree visualizers: a value field + insert/delete/search actions, +// clear, and a speed slider. Callers pass the op handlers (they get the number). + +export default function TreeMenu({ title, disabled, onInsert, onDelete, onSearch, onClear, onSpeedChange }) { + const [value, setValue] = useState(''); + const num = () => Number(value); + const valid = value.trim() !== '' && Number.isFinite(num()); + + return ( +
+

{title}

+ +
+
+
+ Operation +
+
+
+ + setValue(e.target.value)} + disabled={disabled} + className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50" + /> +
+
+ + +
+ + +
+ +
+
+
+ Actions +
+
+ +
+
+ ); +} diff --git a/src/components/tree/tree-node.jsx b/src/components/tree/tree-node.jsx new file mode 100644 index 0000000..f6d771b --- /dev/null +++ b/src/components/tree/tree-node.jsx @@ -0,0 +1,46 @@ +// SVG tree node: a circle with a primary value and an optional secondary label. +// Position is a CSS transform transition so nodes slide when the tree re-lays +// out; a one-shot SMIL fade-in plays when a node first appears. + +const FILL = { + normal: ['#0d9488', '#0f766e'], + current: ['#f59e0b', '#d97706'], + visited: ['#334155', '#475569'], + found: ['#10b981', '#059669'], + path: ['#10b981', '#059669'], + remove: ['#f43f5e', '#be123c'], +}; + +const R = 16; + +export default function TreeNode({ x, y, value, secondary, state }) { + if (state === 'hidden') return null; + const [bg, border] = FILL[state] || FILL.normal; + // shrink the primary label so longer values (e.g. recursion call labels + // like "(4,3)") still fit inside the circle + const len = String(value ?? '').length; + const fontSize = len <= 2 ? 13 : len <= 4 ? 10 : 8; + + return ( + + + + + {value} + + {secondary != null && ( + + {secondary} + + )} + + ); +} + +export const TREE_NODE_R = R; diff --git a/src/components/tree/use-tree-editor.js b/src/components/tree/use-tree-editor.js new file mode 100644 index 0000000..54ce98a --- /dev/null +++ b/src/components/tree/use-tree-editor.js @@ -0,0 +1,67 @@ +import { useState, useRef } from 'react'; +import { flattenTree } from './layout'; + +// Reusable tree editor + action-log executor (SVG tree counterpart of +// useGraphEditor). Holds the logical tree + visual marks and plays an action +// list with a delay. Algorithm-agnostic: callers build actions and call run(). + +const toDelay = (s) => 1100 - s * 10; + +export function useTreeEditor({ initialTree = null } = {}) { + const [tree, setTreeState] = useState(initialTree); + const [nodeState, setNodeState] = useState({}); + const [edgeState, setEdgeState] = useState({}); + const [labels, setLabels] = useState({}); + const [status, setStatus] = useState(''); + const [isRunning, setIsRunning] = useState(false); + + const treeRef = useRef(initialTree); + const isRunningRef = useRef(false); + const speedRef = useRef(toDelay(50)); + + const applyTree = (t) => { treeRef.current = t; setTreeState(t); }; + const clearMarks = () => { setNodeState({}); setEdgeState({}); setLabels({}); setStatus(''); }; + + const applyAction = (a) => { + if (a.type === 'setTree') { + applyTree(a.tree); + // recursion reveals a fixed tree: lay it out once, hidden, then + // un-hide nodes via markNode as the walk visits them + if (a.hidden && a.tree) { + const hidden = {}; + for (const n of flattenTree(a.tree).nodes) hidden[n.id] = 'hidden'; + setNodeState(hidden); + } + } else if (a.type === 'markNode') setNodeState((s) => ({ ...s, [a.id]: a.state })); + else if (a.type === 'markEdge') setEdgeState((s) => ({ ...s, [a.childId]: a.state })); + else if (a.type === 'setLabel') setLabels((s) => ({ ...s, [a.id]: a.secondary })); + else if (a.type === 'status') setStatus(a.text); + else if (a.type === 'clear') clearMarks(); + }; + + const run = async (actions) => { + if (!actions || !actions.length || isRunningRef.current) return; + isRunningRef.current = true; + setIsRunning(true); + clearMarks(); + for (const a of actions) { + applyAction(a); + // a structural change triggers the ~0.6s re-layout slide; always give + // it time to finish, even when the speed slider is set fast + const delay = a.type === 'setTree' ? Math.max(speedRef.current, 650) : speedRef.current; + await sleep(delay); + } + isRunningRef.current = false; + setIsRunning(false); + }; + + const getContext = () => ({ tree: treeRef.current }); + const clear = () => { if (isRunningRef.current) return; applyTree(null); clearMarks(); }; + const setSpeed = (s) => { speedRef.current = toDelay(s); }; + + return { tree, nodeState, edgeState, labels, status, isRunning, run, getContext, clear, setSpeed }; +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/src/lib/algorithms/bst.js b/src/lib/algorithms/bst.js new file mode 100644 index 0000000..8a28b9a --- /dev/null +++ b/src/lib/algorithms/bst.js @@ -0,0 +1,101 @@ +// Binary Search Tree — immutable tree ops (preserving node ids so the renderer +// can animate) and action-log planners for insert / delete / search. +// +// Node: { id, value, left, right }. Actions use setTree (re-layout) + markNode. + +let counter = 0; +const makeNode = (value) => ({ id: `b${++counter}`, value, left: null, right: null }); + +export function fromValues(values) { + let root = null; + for (const v of values) root = insert(root, v).root; + return root; +} + +// insert returning { root, id, inserted } — clones the path, keeps other ids +function insert(root, value) { + if (!root) { + const n = makeNode(value); + return { root: n, id: n.id, inserted: true }; + } + if (value === root.value) return { root, id: root.id, inserted: false }; + if (value < root.value) { + const r = insert(root.left, value); + return { root: { ...root, left: r.root }, id: r.id, inserted: r.inserted }; + } + const r = insert(root.right, value); + return { root: { ...root, right: r.root }, id: r.id, inserted: r.inserted }; +} + +// immutable delete; two-child case copies the in-order successor's value into +// the node (id preserved) and removes the successor +function removeValue(root, value) { + if (!root) return null; + if (value < root.value) return { ...root, left: removeValue(root.left, value) }; + if (value > root.value) return { ...root, right: removeValue(root.right, value) }; + if (!root.left) return root.right; + if (!root.right) return root.left; + let s = root.right; + while (s.left) s = s.left; + return { ...root, value: s.value, right: removeValue(root.right, s.value) }; +} + +function pathTo(root, value) { + const path = []; + let cur = root; + while (cur) { + path.push(cur); + if (value === cur.value) break; + cur = value < cur.value ? cur.left : cur.right; + } + return path; +} + +export function insertActions(tree, value) { + const actions = []; + for (const n of pathTo(tree, value)) actions.push({ type: 'markNode', id: n.id, state: 'current' }); + const { root, id, inserted } = insert(tree, value); + if (!inserted) { + actions.push({ type: 'status', text: `${value} is already in the tree` }); + actions.push({ type: 'clear' }); + return actions; + } + for (const n of pathTo(tree, value)) actions.push({ type: 'markNode', id: n.id, state: 'normal' }); + actions.push({ type: 'setTree', tree: root }); + actions.push({ type: 'markNode', id, state: 'found' }); + actions.push({ type: 'status', text: `Inserted ${value}` }); + return actions; +} + +export function searchActions(tree, value) { + const actions = []; + let cur = tree; + while (cur) { + actions.push({ type: 'markNode', id: cur.id, state: 'current' }); + if (value === cur.value) { + actions.push({ type: 'markNode', id: cur.id, state: 'found' }); + actions.push({ type: 'status', text: `Found ${value}` }); + return actions; + } + actions.push({ type: 'markNode', id: cur.id, state: 'visited' }); + cur = value < cur.value ? cur.left : cur.right; + } + actions.push({ type: 'status', text: `${value} not found` }); + return actions; +} + +export function deleteActions(tree, value) { + const actions = []; + const path = pathTo(tree, value); + for (const n of path) actions.push({ type: 'markNode', id: n.id, state: 'current' }); + const target = path[path.length - 1]; + if (!target || target.value !== value) { + actions.push({ type: 'status', text: `${value} not found` }); + actions.push({ type: 'clear' }); + return actions; + } + actions.push({ type: 'markNode', id: target.id, state: 'remove' }); + actions.push({ type: 'setTree', tree: removeValue(tree, value) }); + actions.push({ type: 'status', text: `Deleted ${value}` }); + return actions; +} diff --git a/src/lib/algorithms/connectivity.js b/src/lib/algorithms/connectivity.js new file mode 100644 index 0000000..4967ebc --- /dev/null +++ b/src/lib/algorithms/connectivity.js @@ -0,0 +1,136 @@ +// Connectivity — presets, a color palette, and planners for Connected +// Components (undirected / weakly connected) and Strongly Connected Components +// (Tarjan, directed). Both emit the shared action log and use `colorNode` to +// tint each component a distinct color. + +export const COMPONENT_PALETTE = [ + '#0ea5e9', '#f97316', '#a855f7', '#22c55e', + '#ec4899', '#eab308', '#14b8a6', '#ef4444', +]; + +export const CONNECTIVITY_PRESETS = [ + { + name: 'Islands', + nodes: [ + { id: 'n1', x: 90, y: 80, label: 'A' }, + { id: 'n2', x: 210, y: 70, label: 'B' }, + { id: 'n3', x: 150, y: 170, label: 'C' }, + { id: 'n4', x: 350, y: 80, label: 'D' }, + { id: 'n5', x: 470, y: 90, label: 'E' }, + { id: 'n6', x: 410, y: 190, label: 'F' }, + { id: 'n7', x: 170, y: 300, label: 'G' }, + { id: 'n8', x: 310, y: 300, label: 'H' }, + ], + edges: [ + ['n1', 'n2'], ['n2', 'n3'], ['n1', 'n3'], + ['n4', 'n5'], ['n5', 'n6'], ['n4', 'n6'], + ['n7', 'n8'], + ], + }, + { + name: 'Connected', + nodes: [ + { id: 'n1', x: 110, y: 90, label: 'A' }, + { id: 'n2', x: 250, y: 70, label: 'B' }, + { id: 'n3', x: 390, y: 110, label: 'C' }, + { id: 'n4', x: 180, y: 230, label: 'D' }, + { id: 'n5', x: 340, y: 240, label: 'E' }, + ], + edges: [['n1', 'n2'], ['n2', 'n3'], ['n1', 'n4'], ['n4', 'n5'], ['n3', 'n5']], + }, + { + name: 'SCC demo (use Directed)', + nodes: [ + { id: 'n1', x: 90, y: 70, label: 'A' }, + { id: 'n2', x: 210, y: 60, label: 'B' }, + { id: 'n3', x: 150, y: 170, label: 'C' }, + { id: 'n4', x: 330, y: 80, label: 'D' }, + { id: 'n5', x: 450, y: 70, label: 'E' }, + { id: 'n6', x: 400, y: 180, label: 'F' }, + { id: 'n7', x: 250, y: 290, label: 'G' }, + { id: 'n8', x: 400, y: 300, label: 'H' }, + ], + edges: [ + ['n1', 'n2'], ['n2', 'n3'], ['n3', 'n1'], // SCC: A B C + ['n3', 'n4'], // connector + ['n4', 'n5'], ['n5', 'n6'], ['n6', 'n4'], // SCC: D E F + ['n6', 'n7'], // connector + ['n7', 'n8'], ['n8', 'n7'], // SCC: G H + ], + }, +]; + +// BFS flood-fill; one palette color per component. +export function connectedComponentsActions(adj, nodeIds) { + const ids = [...nodeIds].sort(); + const visited = new Set(); + const actions = []; + let comp = 0; + + for (const start of ids) { + if (visited.has(start)) continue; + const color = COMPONENT_PALETTE[comp % COMPONENT_PALETTE.length]; + comp++; + const queue = [start]; + visited.add(start); + while (queue.length) { + const u = queue.shift(); + actions.push({ type: 'markNode', id: u, state: 'current' }); + for (const { node: v } of adj[u] || []) { + if (!visited.has(v)) { visited.add(v); queue.push(v); } + } + actions.push({ type: 'colorNode', id: u, color }); + } + } + actions.push({ type: 'status', text: `${comp} component${comp === 1 ? '' : 's'}` }); + return actions; +} + +// Tarjan's algorithm. Shows the low-link value on each node and colors each SCC +// when its root is finalized. +export function sccActions(adj, nodeIds) { + const ids = [...nodeIds].sort(); + const actions = []; + const index = {}; + const low = {}; + const onStack = new Set(); + const stack = []; + let idx = 0; + let comp = 0; + + const strongconnect = (u) => { + index[u] = idx; + low[u] = idx; + idx++; + stack.push(u); + onStack.add(u); + actions.push({ type: 'markNode', id: u, state: 'current' }); + actions.push({ type: 'setDist', id: u, dist: low[u] }); + + for (const { node: v } of adj[u] || []) { + if (index[v] === undefined) { + strongconnect(v); + if (low[v] < low[u]) { low[u] = low[v]; actions.push({ type: 'setDist', id: u, dist: low[u] }); } + } else if (onStack.has(v)) { + if (index[v] < low[u]) { low[u] = index[v]; actions.push({ type: 'setDist', id: u, dist: low[u] }); } + } + } + + if (low[u] === index[u]) { + const color = COMPONENT_PALETTE[comp % COMPONENT_PALETTE.length]; + comp++; + let w; + do { + w = stack.pop(); + onStack.delete(w); + actions.push({ type: 'colorNode', id: w, color }); + } while (w !== u); + } + }; + + for (const u of ids) { + if (index[u] === undefined) strongconnect(u); + } + actions.push({ type: 'status', text: `${comp} strongly-connected component${comp === 1 ? '' : 's'}` }); + return actions; +} diff --git a/src/lib/algorithms/networkFlow.js b/src/lib/algorithms/networkFlow.js new file mode 100644 index 0000000..3e0c40c --- /dev/null +++ b/src/lib/algorithms/networkFlow.js @@ -0,0 +1,172 @@ +// Network Flow — presets and max-flow planners (Edmonds-Karp / Ford-Fulkerson). +// +// Directed graph; edge weight = capacity. Emits the shared action log, using +// `setFlow` to show flow/capacity and `markEdge`/`markNode` to animate the +// augmenting path. Finishes with a min-cut highlight. Phase 1: flow values are +// faithful; backward (cancel) steps show as a forward edge's flow decreasing. + +export const FLOW_PRESETS = [ + { + // classic CLRS max-flow network (max flow = 23) + name: 'Classic (s→t)', + source: 'n1', + sink: 'n6', + nodes: [ + { id: 'n1', x: 60, y: 160, label: 's' }, + { id: 'n2', x: 210, y: 70, label: 'a' }, + { id: 'n3', x: 210, y: 250, label: 'b' }, + { id: 'n4', x: 370, y: 70, label: 'c' }, + { id: 'n5', x: 370, y: 250, label: 'd' }, + { id: 'n6', x: 520, y: 160, label: 't' }, + ], + edges: [ + ['n1', 'n2', 16], ['n1', 'n3', 13], ['n2', 'n3', 10], ['n3', 'n2', 4], + ['n2', 'n4', 12], ['n4', 'n3', 9], ['n3', 'n5', 14], ['n5', 'n4', 7], + ['n4', 'n6', 20], ['n5', 'n6', 4], + ], + }, + { + name: 'Diamond', + source: 'n1', + sink: 'n4', + nodes: [ + { id: 'n1', x: 90, y: 160, label: 's' }, + { id: 'n2', x: 260, y: 70, label: 'a' }, + { id: 'n3', x: 260, y: 250, label: 'b' }, + { id: 'n4', x: 430, y: 160, label: 't' }, + ], + edges: [ + ['n1', 'n2', 10], ['n1', 'n3', 5], ['n2', 'n3', 4], + ['n2', 'n4', 6], ['n3', 'n4', 10], + ], + }, +]; + +const key = (a, b) => `${a}|${b}`; + +export function maxFlowActions(edges, sourceId, sinkId, variant = 'ek') { + const actions = []; + if (sourceId == null || sinkId == null || sourceId === sinkId) { + actions.push({ type: 'status', text: 'Set a source (S) and a sink (F)' }); + return actions; + } + + const cap = {}; + const edgeIdOf = {}; + const nbr = {}; + for (const e of edges) { + const c = e.data?.weight ?? 1; + cap[key(e.source, e.target)] = (cap[key(e.source, e.target)] || 0) + c; + edgeIdOf[key(e.source, e.target)] = e.id; + (nbr[e.source] = nbr[e.source] || new Set()).add(e.target); + (nbr[e.target] = nbr[e.target] || new Set()).add(e.source); + } + const residual = { ...cap }; + const getRes = (a, b) => residual[key(a, b)] ?? 0; + const neighbors = (u) => [...(nbr[u] || [])].sort(); + const realEdge = (a, b) => edgeIdOf[key(a, b)] ?? edgeIdOf[key(b, a)]; + + // reconstruct path of [a,b] steps from a prev map + const rebuild = (prev) => { + const path = []; + let cur = sinkId; + while (cur !== sourceId) { + path.push([prev[cur], cur]); + cur = prev[cur]; + } + return path.reverse(); + }; + + const findBFS = () => { + const prev = {}; + const visited = new Set([sourceId]); + const q = [sourceId]; + while (q.length) { + const u = q.shift(); + if (u === sinkId) break; + for (const w of neighbors(u)) { + if (!visited.has(w) && getRes(u, w) > 0) { visited.add(w); prev[w] = u; q.push(w); } + } + } + return visited.has(sinkId) ? rebuild(prev) : null; + }; + + const findDFS = () => { + const prev = {}; + const visited = new Set([sourceId]); + const stack = [sourceId]; + while (stack.length) { + const u = stack.pop(); + if (u === sinkId) break; + for (const w of neighbors(u)) { + if (!visited.has(w) && getRes(u, w) > 0) { visited.add(w); prev[w] = u; stack.push(w); } + } + } + return visited.has(sinkId) ? rebuild(prev) : null; + }; + + const find = variant === 'ff' ? findDFS : findBFS; + + // start every edge at flow 0 + for (const e of edges) actions.push({ type: 'setFlow', id: e.id, flow: 0 }); + + let total = 0; + let pathNo = 0; + while (true) { + const path = find(); + if (!path) break; + pathNo += 1; + + let bottleneck = Infinity; + for (const [a, b] of path) bottleneck = Math.min(bottleneck, getRes(a, b)); + + // highlight the augmenting path + for (const [a, b] of path) { + const eid = realEdge(a, b); + if (eid) actions.push({ type: 'markEdge', id: eid, state: 'path' }); + actions.push({ type: 'markNode', id: a, state: 'current' }); + actions.push({ type: 'markNode', id: b, state: 'current' }); + } + actions.push({ type: 'status', text: `Augmenting path ${pathNo}: bottleneck ${bottleneck}` }); + + // push flow along the path + for (const [a, b] of path) { + residual[key(a, b)] = getRes(a, b) - bottleneck; + residual[key(b, a)] = getRes(b, a) + bottleneck; + } + total += bottleneck; + + // refresh flow labels on real edges + for (const e of edges) { + const flow = Math.max(0, (cap[key(e.source, e.target)] || 0) - getRes(e.source, e.target)); + actions.push({ type: 'setFlow', id: e.id, flow }); + } + actions.push({ type: 'status', text: `Total flow: ${total}` }); + + // clear the path highlight before the next iteration + for (const [a, b] of path) { + const eid = realEdge(a, b); + if (eid) actions.push({ type: 'markEdge', id: eid, state: 'normal' }); + actions.push({ type: 'markNode', id: a, state: 'normal' }); + actions.push({ type: 'markNode', id: b, state: 'normal' }); + } + } + + // min cut: nodes reachable from source in the final residual graph + const reach = new Set([sourceId]); + const q = [sourceId]; + while (q.length) { + const u = q.shift(); + for (const w of neighbors(u)) { + if (!reach.has(w) && getRes(u, w) > 0) { reach.add(w); q.push(w); } + } + } + for (const u of reach) actions.push({ type: 'colorNode', id: u, color: '#0ea5e9' }); + for (const e of edges) { + if (reach.has(e.source) && !reach.has(e.target)) { + actions.push({ type: 'markEdge', id: e.id, state: 'negcycle' }); + } + } + actions.push({ type: 'status', text: `Max flow = ${total} · min cut` }); + return actions; +}