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/dynamic-programming/rodCutting.js b/src/algorithms/dynamic-programming/rodCutting.js new file mode 100644 index 0000000..2c89616 --- /dev/null +++ b/src/algorithms/dynamic-programming/rodCutting.js @@ -0,0 +1,151 @@ +export function rodCuttingTopDown(prices) { + const n = prices.length; + const memo = new Array(n + 1).fill(null); + const steps = []; + + function solve(k) { + steps.push({ + array: [...memo], + currentIndex: k, + readingIndices: [], + priceIndex: null, + message: `Calling solve(k = ${k}).` + }); + + if (k === 0) { + steps.push({ + array: [...memo], + currentIndex: k, + readingIndices: [], + priceIndex: null, + message: `Base case: solve(0) = 0.` + }); + return 0; + } + + if (memo[k] !== null) { + steps.push({ + array: [...memo], + currentIndex: k, + readingIndices: [], + priceIndex: null, + message: `Memo hit: solve(${k}) is already computed as ${memo[k]}.` + }); + return memo[k]; + } + + let currentMaxProfit = -1; + + for (let j = 1; j <= k; j++) { + steps.push({ + array: [...memo], + currentIndex: k, + readingIndices: [], + priceIndex: j - 1, + message: `... for k = ${k}, trying cut j = ${j}. Need to find solve(${k - j}).` + }); + + const remainingProfit = solve(k - j); + let profit = prices[j - 1] + remainingProfit; + + steps.push({ + array: [...memo], + currentIndex: k, + readingIndices: [k - j], + priceIndex: j - 1, + message: `... cut j = ${j} gives profit = prices[${j - 1}] (${prices[j-1]}) + solve(${k - j}) (${remainingProfit}) = ${profit}.` + }); + + if (profit > currentMaxProfit) { + currentMaxProfit = profit; + } + } + + memo[k] = currentMaxProfit; + + steps.push({ + array: [...memo], + currentIndex: k, + readingIndices: [], + priceIndex: null, + message: `Computed solve(${k}) = ${currentMaxProfit}. Storing in memo[${k}].` + }); + + return currentMaxProfit; + } + + const result = solve(n); + + steps.push({ + array: [...memo], + currentIndex: n, + readingIndices: [], + priceIndex: null, + message: `Computation complete. Max profit for a rod of length ${n} is ${result}.` + }); + + return { steps, result }; +} + + +export function rodCuttingBottomUp(prices) { + const n = prices.length; + const dp = new Array(n + 1).fill(0); + const steps = []; + + steps.push({ + array: [...dp], + currentIndex: 0, + readingIndices: [], + priceIndex: null, + message: `Base case: Max profit for length 0 is 0. dp[0] = 0.` + }); + + for (let i = 1; i <= n; i++) { + let currentMaxProfit = -1; + + steps.push({ + array: [...dp], + currentIndex: i, + readingIndices: [], + priceIndex: null, + message: `Calculating max profit for length i = ${i}.` + }); + + for (let j = 1; j <= i; j++) { + let profit = prices[j - 1] + dp[i - j]; + + steps.push({ + array: [...dp], + currentIndex: i, + readingIndices: [i - j], + priceIndex: j - 1, + message: `... trying cut j = ${j}. Profit = prices[${j - 1}] (${prices[j-1]}) + dp[${i - j}] (${dp[i-j]}) = ${profit}.` + }); + + if (profit > currentMaxProfit) { + currentMaxProfit = profit; + } + } + + dp[i] = currentMaxProfit; + + steps.push({ + array: [...dp], + currentIndex: i, + readingIndices: [], + priceIndex: null, + message: `Max profit for length ${i} is ${currentMaxProfit}. Storing in dp[${i}].` + }); + } + + steps.push({ + array: [...dp], + currentIndex: n, + readingIndices: [], + priceIndex: null, + message: `Computation complete. Max profit for a rod of length ${n} is ${dp[n]}.` + }); + + return { steps, result: dp[n] }; +} \ No newline at end of file diff --git a/src/components/dynamic-programming/RodCuttingVisualizer.jsx b/src/components/dynamic-programming/RodCuttingVisualizer.jsx new file mode 100644 index 0000000..b634415 --- /dev/null +++ b/src/components/dynamic-programming/RodCuttingVisualizer.jsx @@ -0,0 +1,306 @@ +import React, { useState, useMemo, useEffect, useRef } from "react"; +import { rodCuttingTopDown, rodCuttingBottomUp } from "@/algorithms/dynamic-programming/rodCutting"; + +const PricesArray = ({ prices, currentIndex }) => { + return ( +
+

Prices Array (P[i])

+
+ {prices.map((value, index) => ( +
+
P({index})
+
{value}
+
+ ))} +
+
+ ); +}; + +const DPArray = ({ array, currentIndex, readingIndices = [], algorithm }) => { + const isMemoization = algorithm === "topDown"; + const label = isMemoization ? "Memo Table (Profit[i])" : "DP Array (Profit[i])"; + + return ( +
+

{label}

+
+ {array.map((value, index) => { + const isCurrent = index === currentIndex; + const isReading = readingIndices.includes(index); + + let cellClass = "bg-gray-700 border-gray-600 text-gray-200 hover:border-gray-500"; + if (isCurrent) { + cellClass = "bg-blue-600 border-blue-400 shadow-lg text-white font-bold transform scale-105"; + } else if (isReading) { + cellClass = "bg-yellow-600 border-yellow-400 text-white"; + } + + return ( +
+
Profit({index})
+
{value === null ? '?' : value}
+
+ ); + })} +
+
+ ); +}; + +const SPEED_OPTIONS = { + "Slow": 1500, + "Medium": 500, + "Fast": 200, +}; + +export default function RodCuttingVisualizer() { + const [priceInput, setPriceInput] = useState("1, 5, 8, 9, 10, 17, 17, 20"); + const [prices, setPrices] = useState([1, 5, 8, 9, 10, 17, 17, 20]); + const [algorithm, setAlgorithm] = useState("bottomUp"); + + const [steps, setSteps] = useState([]); + const [currentStep, setCurrentStep] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(SPEED_OPTIONS["Medium"]); + const timerRef = useRef(null); + + const handleCompute = () => { + const parsedPrices = priceInput + .split(',') + .map(s => Number(s.trim())) + .filter(n => !isNaN(n) && n >= 0); + + if (parsedPrices.length === 0 || parsedPrices.length > 15) { + alert("Please enter a valid, comma-separated list of 1 to 15 prices."); + return; + } + + setPrices(parsedPrices); + setIsPlaying(false); + + const { steps: newSteps } = + algorithm === "topDown" + ? rodCuttingTopDown(parsedPrices) + : rodCuttingBottomUp(parsedPrices); + + setSteps(newSteps); + setCurrentStep(0); + }; + + useEffect(() => { + handleCompute(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [algorithm]); + + useEffect(() => { + if (isPlaying && currentStep < steps.length - 1) { + timerRef.current = setInterval(() => { + setCurrentStep((prevStep) => prevStep + 1); + }, speed); + } else if (currentStep === steps.length - 1) { + setIsPlaying(false); + } + + return () => { + clearInterval(timerRef.current); + }; + }, [isPlaying, currentStep, steps.length, speed]); + + const togglePlay = () => { + if (currentStep === steps.length - 1) { + setCurrentStep(0); + setIsPlaying(true); + } else { + setIsPlaying(!isPlaying); + } + }; + + const handleNext = () => { + setIsPlaying(false); + if (currentStep < steps.length - 1) setCurrentStep(currentStep + 1); + }; + + const handlePrev = () => { + setIsPlaying(false); + if (currentStep > 0) setCurrentStep(currentStep - 1); + }; + + const currentState = useMemo(() => steps[currentStep] || {}, [steps, currentStep]); + const n = prices.length; + const isFinalStep = steps.length > 0 && currentStep === steps.length - 1; + const finalResult = isFinalStep && currentState.array ? currentState.array[n] : null; + + + return ( +
+
+

+ Rod Cutting Algorithm +

+ +
+ + ❓ What is the Rod Cutting Problem? + +
+

+ Given a rod of length n and an array of prices for rods of length 1 to n, + determine the maximum value obtainable by cutting up the rod and selling the pieces. +

+
    +
  • + Top-Down (Memoization): A recursive approach. To solve for length `k`, it tries all cuts `j` and recursively finds the best profit for the remaining piece (`k-j`), storing results to avoid re-computation. +
  • +
  • + Bottom-Up (Tabulation): An iterative approach. It fills a DP table from length 0 up to `n`. To find the profit for length `i`, it looks at the *already computed* profits for smaller lengths. +
  • +
+
+
+ +
+
+ + setPriceInput(e.target.value)} + placeholder="e.g., 1, 5, 8, 9" + /> +
+ +
+ + +
+ + +
+ + {steps.length > 0 ? ( + <> +
+ +
+ + +
+
+ + +
+
+ +
+

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

+
+ +
+
+

Current Action

+

+ {currentState.message || 'Starting computation...'} +

+
+ + + + {currentState.array && ( + + )} + + {isFinalStep && finalResult !== null && ( +
+

+ 🎉 Final Result: Max Profit for length {n} = {finalResult} +

+
+ )} + +
+ + Click to View Raw Step Data (for debugging) + +
+                                    {JSON.stringify(currentState, null, 2)}
+                                
+
+ +
+ + ) : ( +
+

Welcome to the Rod Cutting DP Visualizer!

+

Enter a list of Prices and select an Algorithm above.

+

Click Re-Visualize to begin.

+
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/src/pages/dynamic-programming/DyanmicProgrammingPage.jsx b/src/pages/dynamic-programming/DyanmicProgrammingPage.jsx index 13744ed..e306929 100644 --- a/src/pages/dynamic-programming/DyanmicProgrammingPage.jsx +++ b/src/pages/dynamic-programming/DyanmicProgrammingPage.jsx @@ -7,6 +7,7 @@ import Knapsack from "./Knapsack"; import PascalTriangle from "./PascalTriangle"; import LCSPage from "./LCS"; import CoinChange from "./CoinChange"; // ✅ Added import +import RodCutting from "./RodCutting"; export default function DynamicProgrammingPage() { const [selectedAlgo, setSelectedAlgo] = useState(""); @@ -56,6 +57,12 @@ export default function DynamicProgrammingPage() { ); + case "RodCutting": + return ( +
+ +
+ ); default: return (
@@ -121,6 +128,7 @@ export default function DynamicProgrammingPage() { {/* ✅ Added */} +