diff --git a/package-lock.json b/package-lock.json index 7343cb3..210d3cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "tailwind-merge": "^3.3.1" }, "devDependencies": { + "@commitlint/config-conventional": "^20.0.0", "@eslint/js": "^9.36.0", "@types/node": "^24.7.1", "@types/react": "^19.1.16", @@ -308,6 +309,47 @@ "node": ">=6.9.0" } }, + "node_modules/@commitlint/config-conventional": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.0.0.tgz", + "integrity": "sha512-q7JroPIkDBtyOkVe9Bca0p7kAUYxZMxkrBArCfuD3yN4KjRAenP9PmYwnn7rsw8Q+hHq1QB2BRmBh0/Z19ZoJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.0.0", + "conventional-changelog-conventionalcommits": "^7.0.2" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-20.0.0.tgz", + "integrity": "sha512-bVUNBqG6aznYcYjTjnc3+Cat/iBgbgpflxbIBTnsHTX0YVpnmINPEkSRWymT2Q8aSH3Y7aKnEbunilkYe8TybA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/conventional-commits-parser": "^5.0.0", + "chalk": "^5.3.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/types/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.11", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", @@ -2117,6 +2159,16 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/conventional-commits-parser": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.2.tgz", + "integrity": "sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/d3": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", @@ -2509,6 +2561,13 @@ "node": ">=10" } }, + "node_modules/array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "dev": true, + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.4.21", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", @@ -2692,12 +2751,36 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz", + "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2872,6 +2955,19 @@ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.240", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.240.tgz", @@ -3415,6 +3511,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", diff --git a/package.json b/package.json index 8ae4e6b..611f96e 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "tailwind-merge": "^3.3.1" }, "devDependencies": { + "@commitlint/config-conventional": "^20.0.0", "@eslint/js": "^9.36.0", "@types/node": "^24.7.1", "@types/react": "^19.1.16", diff --git a/src/algorithms/dynamic-programming/longestCommonSubsequence.js b/src/algorithms/dynamic-programming/longestCommonSubsequence.js new file mode 100644 index 0000000..f12db5a --- /dev/null +++ b/src/algorithms/dynamic-programming/longestCommonSubsequence.js @@ -0,0 +1,57 @@ +export function longestCommonSubsequenceSteps(a = "", b = "") { + const m = a.length; + const n = b.length; + + // ✅ initialize DP table + const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0)); + const steps = []; + + const snapshot = () => dp.map(row => [...row]); + + // ✅ filling dp table + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + if (a[i - 1] === b[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + steps.push({ + dp: snapshot(), + active: { i, j }, + match: { i, j }, + message: `Characters match '${a[i - 1]}' at A[${i - 1}] & B[${j - 1}] → dp[${i}][${j}] = dp[${i - 1}][${j - 1}] + 1 = ${dp[i][j]}` + }); + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + steps.push({ + dp: snapshot(), + active: { i, j }, + message: `No match. dp[${i}][${j}] = max(dp[${i - 1}][${j}] = ${dp[i - 1][j]}, dp[${i}][${j - 1}] = ${dp[i][j - 1]}) = ${dp[i][j]}` + }); + } + } + } + + // ✅ traceback to get sequence + let i = m, j = n; + let lcsChars = []; + const path = []; + + while (i > 0 && j > 0) { + if (a[i - 1] === b[j - 1]) { + lcsChars.push(a[i - 1]); + path.push({ i, j }); + i--; + j--; + } else if (dp[i - 1][j] >= dp[i][j - 1]) i--; + else j--; + } + + const finalPath = path.reverse(); + steps.push({ + dp: snapshot(), + finalPath, + message: `Final LCS sequence = '${lcsChars.reverse().join('')}'`, + sequence: lcsChars.join('') + }); + + return steps; +} diff --git a/src/components/dynamic-programming/LCSVisualizer.jsx b/src/components/dynamic-programming/LCSVisualizer.jsx new file mode 100644 index 0000000..dd8ea78 --- /dev/null +++ b/src/components/dynamic-programming/LCSVisualizer.jsx @@ -0,0 +1,220 @@ +import React, { useState, useEffect, useRef } from "react"; +import { longestCommonSubsequenceSteps } from "../../algorithms/dynamic-programming/longestCommonSubsequence"; + +const DPGrid = ({ dp, active, match, finalPath }) => { + if (!dp || dp.length === 0) return null; + + return ( +
+

DP Table

+
+ + + {dp.map((row, i) => ( + + {row.map((cell, j) => { + const isActive = active && active.i === i && active.j === j; + const isMatch = match && match.i === i && match.j === j; + const isFinal = finalPath?.some(p => p.i === i && p.j === j); + const color = isFinal + ? "bg-yellow-600" + : isMatch + ? "bg-green-600" + : isActive + ? "bg-blue-600" + : "bg-gray-700"; + + return ( + + ); + })} + + ))} + +
+ {cell} +
+
+
+ ); +}; + +const SPEEDS = { Slow: 1200, Medium: 600, Fast: 250 }; + +export default function LCSVisualizer() { + const [a, setA] = useState("ABCBDAB"); + const [b, setB] = useState("BDCAB"); + const [steps, setSteps] = useState([]); + const [stepIndex, setStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(SPEEDS.Medium); + const timer = useRef(null); + + const handleStart = () => { + const s = longestCommonSubsequenceSteps(a, b); + setSteps(s); + setStepIndex(0); + setIsPlaying(false); + }; + + useEffect(() => { + if (isPlaying && stepIndex < steps.length - 1) { + timer.current = setInterval(() => { + setStepIndex(i => i + 1); + }, speed); + } else clearInterval(timer.current); + + return () => clearInterval(timer.current); + }, [isPlaying, stepIndex, steps.length, speed]); + + const togglePlay = () => { + if (stepIndex === steps.length - 1) setStepIndex(0); + setIsPlaying(!isPlaying); + }; + + const current = steps[stepIndex] || {}; + const finalLCS = stepIndex === steps.length - 1 ? current.sequence : ""; + + return ( +
+
+

+ Longest Common Subsequence (LCS) +

+ +
+

+ This visualizer shows how the DP table for LCS is filled step by step and how the final sequence is reconstructed. +

+
+ +
+
+ + setA(e.target.value)} + className="w-full mt-2 p-2 rounded-lg bg-gray-700 border border-gray-600 text-white" + /> +
+ +
+ + setB(e.target.value)} + className="w-full mt-2 p-2 rounded-lg bg-gray-700 border border-gray-600 text-white" + /> +
+ + +
+ + {steps.length > 0 ? ( + <> +
+ + +
+ + +
+ +
+ + +
+
+ +
+

+ Step {stepIndex + 1} / {steps.length} +

+
+ +
+
+

+ Current Action +

+

+ {current.message || "Processing..."} +

+
+ + {current.dp && ( + + )} + + {finalLCS && ( +
+

+ 🎉 Final LCS ={" "} + {finalLCS} +

+
+ )} +
+ + ) : ( +
+

Welcome to the LCS Visualizer!

+

Enter two strings and click “Start Visualization”.

+
+ )} +
+
+ ); +} diff --git a/src/pages/dynamic-programming/DyanmicProgrammingPage.jsx b/src/pages/dynamic-programming/DyanmicProgrammingPage.jsx index 59664a7..4852b78 100644 --- a/src/pages/dynamic-programming/DyanmicProgrammingPage.jsx +++ b/src/pages/dynamic-programming/DyanmicProgrammingPage.jsx @@ -4,6 +4,7 @@ import Levenshtein from "./Levenshtein"; import MatrixChainMultiplication from "./MatrixChainMultiplication"; import FibonacciSequence from "./FibonacciSequence"; import Knapsack from "./Knapsack"; +import LCSPage from "./LCS"; export default function DynamicProgrammingPage() { const [selectedAlgo, setSelectedAlgo] = useState(""); @@ -35,6 +36,12 @@ export default function DynamicProgrammingPage() { ); + case "LongestCommonSubsequence": + return ( +
+ +
+ ); default: return (
@@ -97,6 +104,7 @@ export default function DynamicProgrammingPage() { +