diff --git a/package-lock.json b/package-lock.json index 210d3cd..c952e7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,6 +74,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2453,6 +2454,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2462,6 +2464,7 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2497,6 +2500,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2649,6 +2653,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -2812,7 +2817,8 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "peer": true }, "node_modules/d3-color": { "version": "3.1.0", @@ -2871,6 +2877,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -3088,6 +3095,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4054,6 +4062,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "peer": true, "engines": { "node": ">=12" }, @@ -4079,6 +4088,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -4116,6 +4126,7 @@ "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -4124,6 +4135,7 @@ "version": "19.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -4152,6 +4164,7 @@ "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -4302,7 +4315,8 @@ "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -4661,6 +4675,7 @@ "version": "7.1.12", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", diff --git a/src/algorithms/graph/prim.js b/src/algorithms/graph/prim.js new file mode 100644 index 0000000..4e67546 --- /dev/null +++ b/src/algorithms/graph/prim.js @@ -0,0 +1,96 @@ +// src/algorithms/graph/prim.js + +// Build adjacency list from list of edges +function buildAdjacency(edges, n) { + const adj = Array.from({ length: n }, () => []); + for (const e of edges) { + const u = e.from - 1; + const v = e.to - 1; + const w = e.weight; + + adj[u].push({ u, v, w }); + adj[v].push({ u: v, v: u, w }); // undirected graph + } + return adj; +} + +/** + * Prim's Algorithm – Step Generator + * Yields one step at a time: + * type: "consider" | "add" | "skip" | "done" + * edge: {from, to, weight} + * visited: boolean[] + * frontier: edge[] + * mst: collected MST edges + */ +export function* primSteps(edges, nodeCount, startNode = 1) { + if (nodeCount === 0) { + yield { type: "done", mst: [], visited: [], frontier: [] }; + return; + } + + const adj = buildAdjacency(edges, nodeCount); + const visited = Array(nodeCount).fill(false); + const mst = []; + const frontier = []; + + const pushEdges = (u) => { + for (const { v, w } of adj[u]) { + if (!visited[v]) { + frontier.push({ from: u + 1, to: v + 1, weight: w }); + } + } + }; + + const startIdx = Math.max(1, Math.min(startNode, nodeCount)) - 1; + visited[startIdx] = true; + pushEdges(startIdx); + + while (mst.length < nodeCount - 1 && frontier.length > 0) { + frontier.sort((a, b) => a.weight - b.weight); + const edge = frontier.shift(); + + yield { + type: "consider", + edge, + visited: [...visited], + frontier: [...frontier], + mst: [...mst], + }; + + const u = edge.from - 1; + const v = edge.to - 1; + + if (visited[u] && visited[v]) { + yield { + type: "skip", + edge, + visited: [...visited], + frontier: [...frontier], + mst: [...mst], + }; + continue; + } + + const nextNode = visited[u] ? v : u; + visited[nextNode] = true; + + mst.push(edge); + pushEdges(nextNode); + + yield { + type: "add", + edge, + visited: [...visited], + frontier: [...frontier], + mst: [...mst], + }; + } + + yield { + type: "done", + mst: [...mst], + visited: [...visited], + frontier: [...frontier], + }; +} diff --git a/src/components/graph/PrimsVisualizer.jsx b/src/components/graph/PrimsVisualizer.jsx new file mode 100644 index 0000000..3658a16 --- /dev/null +++ b/src/components/graph/PrimsVisualizer.jsx @@ -0,0 +1,337 @@ +// src/components/graph/PrimsVisualizer.jsx + +import React, { useRef, useState } from "react"; +import { primSteps } from "../../algorithms/graph/prim"; + +export default function PrimsVisualizer() { + const svgRef = useRef(null); + + const [nodes, setNodes] = useState([]); + const [edges, setEdges] = useState([]); + const [selected, setSelected] = useState({ from: null, to: null }); + const [running, setRunning] = useState(false); + + const [startNode, setStartNode] = useState(1); + const [status, setStatus] = useState("Idle"); + const [stepIndex, setStepIndex] = useState(0); + + const [visitedSnap, setVisitedSnap] = useState([]); + const [frontierSnap, setFrontierSnap] = useState([]); + + // Node creation + const addNode = (e) => { + if (running) return; + + const rect = svgRef.current.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + const id = nodes.length + 1; + + setNodes([...nodes, { id, x, y, label: `N${id}` }]); + }; + + // Edge creation + const handleNodeClick = (id) => { + if (running) return; + + if (!selected.from) { + setSelected({ from: id, to: null }); + return; + } + + if (selected.from === id) return; + + const w = parseInt(prompt("Enter edge weight:"), 10); + if (!isNaN(w)) { + setEdges((prev) => [ + ...prev, + { + from: selected.from, + to: id, + weight: w, + colorClass: "stroke-gray-500", + }, + ]); + } + + setSelected({ from: null, to: null }); + }; + + // Start visualization + const start = async () => { + if (running || nodes.length < 2 || edges.length === 0) { + alert("Create nodes and edges first."); + return; + } + + setRunning(true); + setStatus("Running..."); + setStepIndex(0); + + const delay = 900; + const steps = primSteps(edges, nodes.length, startNode); + + for (const step of steps) { + const { type, edge, visited = [], frontier = [], mst = [] } = step; + + setVisitedSnap(visited); + setFrontierSnap(frontier); + + if (edge) { + setEdges((prev) => + prev.map((e) => + (e.from === edge.from && e.to === edge.to) || + (e.from === edge.to && e.to === edge.from) + ? { + ...e, + colorClass: + type === "consider" + ? "stroke-blue-400" + : type === "add" + ? "stroke-green-400" + : type === "skip" + ? "stroke-red-500" + : "stroke-gray-500", + } + : e + ) + ); + } + + setStatus( + type === "consider" + ? `Considering edge (${edge.from}, ${edge.to})` + : type === "add" + ? `Added edge (${edge.from}, ${edge.to})` + : type === "skip" + ? `Skipped edge (${edge.from}, ${edge.to})` + : `Completed! MST size = ${mst.length}` + ); + + await new Promise((r) => setTimeout(r, delay)); + setStepIndex((i) => i + 1); + } + + setRunning(false); + }; + + const reset = () => { + setNodes([]); + setEdges([]); + setSelected({ from: null, to: null }); + + setVisitedSnap([]); + setFrontierSnap([]); + + setStartNode(1); + setStatus("Idle"); + setStepIndex(0); + + setRunning(false); + }; + + // Render nodes + const renderNodes = () => + nodes.map((n, idx) => { + const isVisited = visitedSnap[idx] ?? false; + const isStart = n.id === startNode; + + return ( + handleNodeClick(n.id)} + className="cursor-pointer" + > + + + + {n.label} + + + ); + }); + + // Render edges + const renderEdges = () => + edges.map((e, i) => { + const a = nodes.find((n) => n.id === e.from); + const b = nodes.find((n) => n.id === e.to); + if (!a || !b) return null; + + const cx = (a.x + b.x) / 2; + const cy = (a.y + b.y) / 2; + + return ( + + + + + {e.weight} + + + ); + }); + + return ( +
+ + {/* 📘 Instructions */} +
+

+ How to Use Prim’s MST Visualizer +

+ +
+ + {/* Controls */} +
+ + + + + +
+ + {/* Status */} +
+

+ Status:{" "} + {status} +

+

+ Step:{" "} + {stepIndex} +

+
+ + {/* Layout */} +
+ {/* Left Panel */} +
+

+ Visited Nodes +

+ +
+ {nodes.map((n, idx) => ( + + {n.label} + + ))} +
+ +

+ Frontier (Min Edges) +

+ +
+ {frontierSnap.length > 0 ? ( + frontierSnap + .slice() + .sort((a, b) => a.weight - b.weight) + .map((e, idx) => ( +
+ + ({e.from}, {e.to}) + + w={e.weight} +
+ )) + ) : ( +

Empty

+ )} +
+
+ + {/* Canvas */} +
+ + {renderEdges()} + {renderNodes()} + +
+
+
+ ); +} diff --git a/src/pages/graph/GraphPage.jsx b/src/pages/graph/GraphPage.jsx index f8a09a9..f65c767 100644 --- a/src/pages/graph/GraphPage.jsx +++ b/src/pages/graph/GraphPage.jsx @@ -1,5 +1,6 @@ import React, { useState } from "react"; import { Network, Compass, Rocket } from "lucide-react"; + import BellmanFord from "./BellmanFord"; import UnionFindPage from "./UnionFind"; import Kruskal from "./Kruskal"; @@ -10,6 +11,8 @@ import BFS from "./BFS"; import KahnTopologicalSort from "./TopoSortKahn"; import DFSTopologicalSort from "./TopoSortDFS"; +import Prims from "./Prims"; + export default function GraphPage() { const [selectedAlgo, setSelectedAlgo] = useState(""); @@ -21,6 +24,8 @@ export default function GraphPage() { return ; case "kruskal": return ; + case "prim": + return ; case "floyd-warshall": return ; case "cycle-detection": @@ -69,6 +74,7 @@ export default function GraphPage() { +