Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions src/algorithms/dynamic-programming/knapsackAlgo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// src/algorithms/dynamic-programming/knapsackAlgo.js

// Implements 0/1 Knapsack (top-down with memo and bottom-up DP)
// Both functions return an object { steps, result } where
// - steps is an array of visual step objects shaped similarly to fibonacciAlgo.js
// - result is the maximum value achievable

function clone2D(arr) {
return arr.map(row => Array.isArray(row) ? [...row] : row);
}

export function knapsackTopDown(weights, values, W) {
const n = weights.length;
// memo dimensions: n x (W+1), initialize null
const memo = Array.from({ length: n }, () => new Array(W + 1).fill(null));
const steps = [];

function solve(i, cap) {
steps.push({
type: "call",
index: { i, cap },
memo: clone2D(memo),
message: `Calling solve(i=${i}, cap=${cap}).`
});

if (i < 0 || cap <= 0) {
steps.push({
type: "base_case",
index: { i, cap },
value: 0,
memo: clone2D(memo),
message: `Base case: i=${i} or cap=${cap} -> 0.`
});
return 0;
}

if (memo[i][cap] !== null) {
steps.push({
type: "memo_hit",
index: { i, cap },
value: memo[i][cap],
memo: clone2D(memo),
message: `Memoization hit: memo[${i}][${cap}] = ${memo[i][cap]}.`
});
return memo[i][cap];
}

// Option 1: skip current item
const without = solve(i - 1, cap);

// Option 2: take current item (if it fits)
let withItem = -Infinity;
if (weights[i] <= cap) {
withItem = values[i] + solve(i - 1, cap - weights[i]);
}

const res = Math.max(without, withItem);
memo[i][cap] = res;

steps.push({
type: "store",
index: { i, cap },
value: res,
memo: clone2D(memo),
message: `Computed memo[${i}][${cap}] = max(${without}, ${withItem === -Infinity ? "-inf" : withItem}) = ${res}.`
});

return res;
}

const result = solve(n - 1, W);

// Map steps to visualSteps (use memo as the array shown)
const visualSteps = steps.map(step => ({
array: step.memo,
currentIndex: step.index,
message: step.message,
value: step.value
}));

return { steps: visualSteps, result };
}

export function knapsackBottomUp(weights, values, W) {
const n = weights.length;
// dp dimensions: (n+1) x (W+1)
const dp = Array.from({ length: n + 1 }, () => new Array(W + 1).fill(0));
const steps = [];

// Optionally push initial state
steps.push({
type: "init",
index: { i: 0, w: 0 },
dp: clone2D(dp),
message: `Initializing dp table with zeros.`
});

// Build DP table
for (let i = 1; i <= n; i++) {
for (let w = 0; w <= W; w++) {
if (weights[i - 1] <= w) {
const take = values[i - 1] + dp[i - 1][w - weights[i - 1]];
const skip = dp[i - 1][w];
dp[i][w] = Math.max(skip, take);
steps.push({
type: "compute",
index: { i, w },
value: dp[i][w],
dp: clone2D(dp),
message: `dp[${i}][${w}] = max(dp[${i - 1}][${w}] = ${skip}, dp[${i - 1}][${w - weights[i - 1]}] + ${values[i - 1]} = ${take}) = ${dp[i][w]}.`
});
} else {
dp[i][w] = dp[i - 1][w];
steps.push({
type: "compute",
index: { i, w },
value: dp[i][w],
dp: clone2D(dp),
message: `dp[${i}][${w}] = dp[${i - 1}][${w}] = ${dp[i][w]} (item ${i - 1} doesn't fit).`
});
}
}
}

const visualSteps = steps.map(step => ({
array: step.dp || step.memo,
currentIndex: step.index,
message: step.message,
value: step.value
}));

return { steps: visualSteps, result: dp[n][W] };
}

// Default export is optional — keep named exports to match other algorithm files
231 changes: 231 additions & 0 deletions src/components/dynamic-programming/KnapsackVisualizer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import React, { useState, useMemo, useEffect, useRef } from "react";
import { knapsackTopDown, knapsackBottomUp } from "../../algorithms/dynamic-programming/knapsackAlgo";

// Simple DP grid renderer for 2D dp/memo tables
const DPGrid = ({ array, currentIndex }) => {
if (!Array.isArray(array) || array.length === 0) return null;

const rows = array.length;
const cols = array[0].length;

return (
<div className="mt-4 overflow-auto">
<h3 className="text-xl font-semibold mb-3 text-blue-400">DP Table</h3>
<div className="inline-block border rounded-lg bg-gray-800 p-3">
<table className="table-auto border-collapse">
<thead>
<tr>
<th className="p-2 border text-sm text-gray-300">i \ w</th>
{Array.from({ length: cols }).map((_, c) => (
<th key={c} className="p-2 border text-sm text-gray-300">{c}</th>
))}
</tr>
</thead>
<tbody>
{array.map((row, r) => (
<tr key={r} className="text-center">
<td className="p-2 border text-sm text-gray-300">{r}</td>
{row.map((cell, c) => {
const isActive = currentIndex && currentIndex.i === r && currentIndex.w === c;
return (
<td
key={c}
className={`p-3 border w-20 h-12 align-middle ${isActive ? "bg-blue-600 text-white font-bold scale-105 transform" : "bg-gray-700 text-gray-200"}`}
>
{cell === null ? "?" : cell}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};

const SPEED_OPTIONS = {
"Slow": 1500,
"Medium": 500,
"Fast": 200,
};

export default function KnapsackVisualizer() {
const [weightsInput, setWeightsInput] = useState("3,4,2");
const [valuesInput, setValuesInput] = useState("4,5,3");
const [capacity, setCapacity] = useState(6);
const [algorithm, setAlgorithm] = useState("topDown");
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);

// Parse inputs
const weights = useMemo(() => weightsInput.split(",").map(s => Number(s.trim())).filter(v => !Number.isNaN(v)), [weightsInput]);
const values = useMemo(() => valuesInput.split(",").map(s => Number(s.trim())).filter(v => !Number.isNaN(v)), [valuesInput]);

const handleCompute = () => {
if (weights.length === 0 || values.length === 0) {
alert("Please provide weights and values.");
return;
}
if (weights.length !== values.length) {
alert("Weights and values arrays must have the same length.");
return;
}
if (capacity < 0 || capacity > 200) {
alert("Please enter capacity between 0 and 200 for visualization.");
return;
}

setIsPlaying(false);

const { steps: newSteps, result } = algorithm === "topDown"
? knapsackTopDown(weights, values, capacity)
: knapsackBottomUp(weights, values, capacity);

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(prev => prev + 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 isFinalStep = steps.length > 0 && currentStep === steps.length - 1;

const finalResult = isFinalStep && currentState.array ? (
// For bottom-up dp, result is dp[n][W] (last row, last col). For top-down memo, we may need to compute same.
(() => {
const arr = currentState.array;
if (!Array.isArray(arr)) return null;
const lastRow = arr[arr.length - 1];
return Array.isArray(lastRow) ? lastRow[lastRow.length - 1] : null;
})()
) : null;

return (
<div className="p-6 min-h-screen bg-gray-900 text-gray-100 font-sans">
<div className="max-w-5xl mx-auto">
<h1 className="text-4xl font-extrabold mb-8 text-indigo-400 text-center drop-shadow-lg">Knapsack Variants</h1>

<div className="mb-8 p-4 rounded-xl bg-gray-800 border border-gray-700 shadow-inner group">
<p className="text-gray-300">This visualizer supports 0/1 Knapsack (Top-Down memoization and Bottom-Up tabulation). Provide comma-separated arrays for weights and values, and a capacity (W).</p>
</div>

<div className="flex flex-wrap justify-center items-center gap-5 mb-8 p-6 rounded-xl bg-gray-800 shadow-2xl border border-gray-700">
<div className="w-full md:w-1/3">
<label className="text-gray-300">Weights (comma separated):</label>
<input value={weightsInput} onChange={(e) => setWeightsInput(e.target.value)} className="w-full mt-2 p-2 rounded-lg bg-gray-700 text-white border border-gray-600" />
</div>

<div className="w-full md:w-1/3">
<label className="text-gray-300">Values (comma separated):</label>
<input value={valuesInput} onChange={(e) => setValuesInput(e.target.value)} className="w-full mt-2 p-2 rounded-lg bg-gray-700 text-white border border-gray-600" />
</div>

<div className="flex items-center gap-3">
<label className="text-gray-300">Capacity (W):</label>
<input type="number" value={capacity} onChange={(e) => setCapacity(Math.max(0, Number(e.target.value)))} className="w-24 p-2 rounded-lg bg-gray-700 text-white border border-gray-600" />
</div>

<div className="flex items-center gap-3">
<label className="text-gray-300">Algorithm:</label>
<select value={algorithm} onChange={(e) => setAlgorithm(e.target.value)} className="p-2 rounded-lg bg-gray-700 text-white border border-gray-600">
<option value="topDown">Top-Down (Memoization)</option>
<option value="bottomUp">Bottom-Up (Tabulation)</option>
</select>
</div>

<button onClick={handleCompute} className="bg-indigo-600 hover:bg-indigo-700 text-white font-bold px-6 py-3 rounded-xl">Re-Visualize</button>
</div>

{steps.length > 0 ? (
<>
<div className="flex flex-wrap justify-between items-center mb-6 p-4 rounded-xl bg-gray-800 border border-gray-700 shadow-lg">
<button onClick={togglePlay} className={`px-5 py-2 rounded-lg font-semibold text-lg ${isPlaying ? "bg-red-600" : "bg-green-600"} text-white`}>{isFinalStep && !isPlaying ? "Replay ▶️" : isPlaying ? "Pause ⏸️" : "Play ▶️"}</button>

<div className="flex gap-2">
<button onClick={handlePrev} disabled={currentStep === 0} className={`px-3 py-2 rounded-lg font-semibold ${currentStep > 0 ? "bg-purple-600 text-white" : "bg-gray-600 text-gray-400"}`}>&lt; Prev</button>
<button onClick={handleNext} disabled={currentStep === steps.length - 1} className={`px-3 py-2 rounded-lg font-semibold ${currentStep < steps.length - 1 ? "bg-purple-600 text-white" : "bg-gray-600 text-gray-400"}`}>Next &gt;</button>
</div>

<div className="flex items-center gap-2">
<label className="text-gray-300">Speed:</label>
<select value={speed} onChange={(e) => setSpeed(Number(e.target.value))} className="p-2 rounded-lg bg-gray-700 text-white border border-gray-600">
{Object.entries(SPEED_OPTIONS).map(([label, ms]) => (
<option key={label} value={ms}>{label}</option>
))}
</select>
</div>
</div>

<div className="text-center mb-4">
<p className="text-2xl font-bold text-yellow-400">Step <b>{currentStep + 1}</b> / <b>{steps.length}</b></p>
</div>

<div className="border border-gray-700 p-6 rounded-xl bg-gray-800 shadow-2xl">
<div className="mb-6 p-4 rounded-lg bg-gray-700 border-l-4 border-teal-400 shadow-inner">
<p className="text-teal-400 font-medium text-md uppercase tracking-wide">Current Action</p>
<p className="text-xl mt-2 text-gray-200 leading-relaxed">{currentState.message || 'Starting computation...'}</p>
</div>

{currentState.array && (
<DPGrid array={currentState.array} currentIndex={currentState.currentIndex || currentState.index} />
)}

{isFinalStep && finalResult !== null && (
<div className="mt-8 p-5 rounded-xl bg-green-900 border border-green-700 text-center shadow-lg">
<p className="text-green-400 text-2xl font-extrabold flex items-center justify-center gap-3">🎉 Final Result: Max Value = <span className="text-green-200 text-3xl">{finalResult}</span></p>
</div>
)}

<details className="mt-8 text-sm text-gray-400">
<summary className="cursor-pointer hover:text-gray-200 text-md font-medium">Click to View Raw Step Data (for debugging)</summary>
<pre className="bg-gray-900 p-4 rounded-lg mt-3 overflow-auto text-xs border border-gray-700 shadow-inner max-h-60">{JSON.stringify(currentState, null, 2)}</pre>
</details>
</div>
</>
) : (
<div className="text-center p-12 bg-gray-800 rounded-xl text-gray-400 text-xl shadow-xl border border-gray-700">
<p className="mb-4">Welcome to the Knapsack Variants Visualizer!</p>
<p>Provide weights and values (comma-separated) and a capacity to start.</p>
</div>
)}
</div>
</div>
);
}
8 changes: 8 additions & 0 deletions src/pages/dynamic-programming/DyanmicProgrammingPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Grid, Brain, Layers, Code, Menu, X } from "lucide-react";
import Levenshtein from "./Levenshtein";
import MatrixChainMultiplication from "./MatrixChainMultiplication";
import FibonacciSequence from "./FibonacciSequence";
import Knapsack from "./Knapsack";

export default function DynamicProgrammingPage() {
const [selectedAlgo, setSelectedAlgo] = useState("");
Expand All @@ -28,6 +29,12 @@ export default function DynamicProgrammingPage() {
<FibonacciSequence />
</div>
);
case "Knapsack":
return (
<div className="md:w-full w-screen overflow-clip p-2">
<Knapsack/>
</div>
);
default:
return (
<div className="flex flex-col items-center justify-center text-center p-6 min-h-screen bg-gray-950">
Expand Down Expand Up @@ -89,6 +96,7 @@ export default function DynamicProgrammingPage() {
<option value="Levenshtein">Levenshtein Distance</option>
<option value="MatrixChainMultiplication">Matrix Chain Multiplication</option>
<option value="Fibonacci">Fibonacci Sequence</option>
<option value="Knapsack">Knapsack</option>
</select>

<button
Expand Down
Loading
Loading