Skip to content

Commit 2aa3ff3

Browse files
feat: Knapsack-Variants-Visualizer
1 parent 139fe1c commit 2aa3ff3

File tree

4 files changed

+380
-0
lines changed

4 files changed

+380
-0
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// src/algorithms/dynamic-programming/knapsackAlgo.js
2+
3+
// Implements 0/1 Knapsack (top-down with memo and bottom-up DP)
4+
// Both functions return an object { steps, result } where
5+
// - steps is an array of visual step objects shaped similarly to fibonacciAlgo.js
6+
// - result is the maximum value achievable
7+
8+
function clone2D(arr) {
9+
return arr.map(row => Array.isArray(row) ? [...row] : row);
10+
}
11+
12+
export function knapsackTopDown(weights, values, W) {
13+
const n = weights.length;
14+
// memo dimensions: n x (W+1), initialize null
15+
const memo = Array.from({ length: n }, () => new Array(W + 1).fill(null));
16+
const steps = [];
17+
18+
function solve(i, cap) {
19+
steps.push({
20+
type: "call",
21+
index: { i, cap },
22+
memo: clone2D(memo),
23+
message: `Calling solve(i=${i}, cap=${cap}).`
24+
});
25+
26+
if (i < 0 || cap <= 0) {
27+
steps.push({
28+
type: "base_case",
29+
index: { i, cap },
30+
value: 0,
31+
memo: clone2D(memo),
32+
message: `Base case: i=${i} or cap=${cap} -> 0.`
33+
});
34+
return 0;
35+
}
36+
37+
if (memo[i][cap] !== null) {
38+
steps.push({
39+
type: "memo_hit",
40+
index: { i, cap },
41+
value: memo[i][cap],
42+
memo: clone2D(memo),
43+
message: `Memoization hit: memo[${i}][${cap}] = ${memo[i][cap]}.`
44+
});
45+
return memo[i][cap];
46+
}
47+
48+
// Option 1: skip current item
49+
const without = solve(i - 1, cap);
50+
51+
// Option 2: take current item (if it fits)
52+
let withItem = -Infinity;
53+
if (weights[i] <= cap) {
54+
withItem = values[i] + solve(i - 1, cap - weights[i]);
55+
}
56+
57+
const res = Math.max(without, withItem);
58+
memo[i][cap] = res;
59+
60+
steps.push({
61+
type: "store",
62+
index: { i, cap },
63+
value: res,
64+
memo: clone2D(memo),
65+
message: `Computed memo[${i}][${cap}] = max(${without}, ${withItem === -Infinity ? "-inf" : withItem}) = ${res}.`
66+
});
67+
68+
return res;
69+
}
70+
71+
const result = solve(n - 1, W);
72+
73+
// Map steps to visualSteps (use memo as the array shown)
74+
const visualSteps = steps.map(step => ({
75+
array: step.memo,
76+
currentIndex: step.index,
77+
message: step.message,
78+
value: step.value
79+
}));
80+
81+
return { steps: visualSteps, result };
82+
}
83+
84+
export function knapsackBottomUp(weights, values, W) {
85+
const n = weights.length;
86+
// dp dimensions: (n+1) x (W+1)
87+
const dp = Array.from({ length: n + 1 }, () => new Array(W + 1).fill(0));
88+
const steps = [];
89+
90+
// Optionally push initial state
91+
steps.push({
92+
type: "init",
93+
index: { i: 0, w: 0 },
94+
dp: clone2D(dp),
95+
message: `Initializing dp table with zeros.`
96+
});
97+
98+
// Build DP table
99+
for (let i = 1; i <= n; i++) {
100+
for (let w = 0; w <= W; w++) {
101+
if (weights[i - 1] <= w) {
102+
const take = values[i - 1] + dp[i - 1][w - weights[i - 1]];
103+
const skip = dp[i - 1][w];
104+
dp[i][w] = Math.max(skip, take);
105+
steps.push({
106+
type: "compute",
107+
index: { i, w },
108+
value: dp[i][w],
109+
dp: clone2D(dp),
110+
message: `dp[${i}][${w}] = max(dp[${i - 1}][${w}] = ${skip}, dp[${i - 1}][${w - weights[i - 1]}] + ${values[i - 1]} = ${take}) = ${dp[i][w]}.`
111+
});
112+
} else {
113+
dp[i][w] = dp[i - 1][w];
114+
steps.push({
115+
type: "compute",
116+
index: { i, w },
117+
value: dp[i][w],
118+
dp: clone2D(dp),
119+
message: `dp[${i}][${w}] = dp[${i - 1}][${w}] = ${dp[i][w]} (item ${i - 1} doesn't fit).`
120+
});
121+
}
122+
}
123+
}
124+
125+
const visualSteps = steps.map(step => ({
126+
array: step.dp || step.memo,
127+
currentIndex: step.index,
128+
message: step.message,
129+
value: step.value
130+
}));
131+
132+
return { steps: visualSteps, result: dp[n][W] };
133+
}
134+
135+
// Default export is optional — keep named exports to match other algorithm files
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import React, { useState, useMemo, useEffect, useRef } from "react";
2+
import { knapsackTopDown, knapsackBottomUp } from "../../algorithms/dynamic-programming/knapsackAlgo";
3+
4+
// Simple DP grid renderer for 2D dp/memo tables
5+
const DPGrid = ({ array, currentIndex }) => {
6+
if (!Array.isArray(array) || array.length === 0) return null;
7+
8+
const rows = array.length;
9+
const cols = array[0].length;
10+
11+
return (
12+
<div className="mt-4 overflow-auto">
13+
<h3 className="text-xl font-semibold mb-3 text-blue-400">DP Table</h3>
14+
<div className="inline-block border rounded-lg bg-gray-800 p-3">
15+
<table className="table-auto border-collapse">
16+
<thead>
17+
<tr>
18+
<th className="p-2 border text-sm text-gray-300">i \ w</th>
19+
{Array.from({ length: cols }).map((_, c) => (
20+
<th key={c} className="p-2 border text-sm text-gray-300">{c}</th>
21+
))}
22+
</tr>
23+
</thead>
24+
<tbody>
25+
{array.map((row, r) => (
26+
<tr key={r} className="text-center">
27+
<td className="p-2 border text-sm text-gray-300">{r}</td>
28+
{row.map((cell, c) => {
29+
const isActive = currentIndex && currentIndex.i === r && currentIndex.w === c;
30+
return (
31+
<td
32+
key={c}
33+
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"}`}
34+
>
35+
{cell === null ? "?" : cell}
36+
</td>
37+
);
38+
})}
39+
</tr>
40+
))}
41+
</tbody>
42+
</table>
43+
</div>
44+
</div>
45+
);
46+
};
47+
48+
const SPEED_OPTIONS = {
49+
"Slow": 1500,
50+
"Medium": 500,
51+
"Fast": 200,
52+
};
53+
54+
export default function KnapsackVisualizer() {
55+
const [weightsInput, setWeightsInput] = useState("3,4,2");
56+
const [valuesInput, setValuesInput] = useState("4,5,3");
57+
const [capacity, setCapacity] = useState(6);
58+
const [algorithm, setAlgorithm] = useState("topDown");
59+
const [steps, setSteps] = useState([]);
60+
const [currentStep, setCurrentStep] = useState(0);
61+
const [isPlaying, setIsPlaying] = useState(false);
62+
const [speed, setSpeed] = useState(SPEED_OPTIONS["Medium"]);
63+
const timerRef = useRef(null);
64+
65+
// Parse inputs
66+
const weights = useMemo(() => weightsInput.split(",").map(s => Number(s.trim())).filter(v => !Number.isNaN(v)), [weightsInput]);
67+
const values = useMemo(() => valuesInput.split(",").map(s => Number(s.trim())).filter(v => !Number.isNaN(v)), [valuesInput]);
68+
69+
const handleCompute = () => {
70+
if (weights.length === 0 || values.length === 0) {
71+
alert("Please provide weights and values.");
72+
return;
73+
}
74+
if (weights.length !== values.length) {
75+
alert("Weights and values arrays must have the same length.");
76+
return;
77+
}
78+
if (capacity < 0 || capacity > 200) {
79+
alert("Please enter capacity between 0 and 200 for visualization.");
80+
return;
81+
}
82+
83+
setIsPlaying(false);
84+
85+
const { steps: newSteps, result } = algorithm === "topDown"
86+
? knapsackTopDown(weights, values, capacity)
87+
: knapsackBottomUp(weights, values, capacity);
88+
89+
setSteps(newSteps);
90+
setCurrentStep(0);
91+
};
92+
93+
useEffect(() => {
94+
handleCompute();
95+
// eslint-disable-next-line react-hooks/exhaustive-deps
96+
}, [algorithm]);
97+
98+
useEffect(() => {
99+
if (isPlaying && currentStep < steps.length - 1) {
100+
timerRef.current = setInterval(() => {
101+
setCurrentStep(prev => prev + 1);
102+
}, speed);
103+
} else if (currentStep === steps.length - 1) {
104+
setIsPlaying(false);
105+
}
106+
107+
return () => clearInterval(timerRef.current);
108+
}, [isPlaying, currentStep, steps.length, speed]);
109+
110+
const togglePlay = () => {
111+
if (currentStep === steps.length - 1) {
112+
setCurrentStep(0);
113+
setIsPlaying(true);
114+
} else setIsPlaying(!isPlaying);
115+
};
116+
117+
const handleNext = () => {
118+
setIsPlaying(false);
119+
if (currentStep < steps.length - 1) setCurrentStep(currentStep + 1);
120+
};
121+
122+
const handlePrev = () => {
123+
setIsPlaying(false);
124+
if (currentStep > 0) setCurrentStep(currentStep - 1);
125+
};
126+
127+
const currentState = useMemo(() => steps[currentStep] || {}, [steps, currentStep]);
128+
const isFinalStep = steps.length > 0 && currentStep === steps.length - 1;
129+
130+
const finalResult = isFinalStep && currentState.array ? (
131+
// For bottom-up dp, result is dp[n][W] (last row, last col). For top-down memo, we may need to compute same.
132+
(() => {
133+
const arr = currentState.array;
134+
if (!Array.isArray(arr)) return null;
135+
const lastRow = arr[arr.length - 1];
136+
return Array.isArray(lastRow) ? lastRow[lastRow.length - 1] : null;
137+
})()
138+
) : null;
139+
140+
return (
141+
<div className="p-6 min-h-screen bg-gray-900 text-gray-100 font-sans">
142+
<div className="max-w-5xl mx-auto">
143+
<h1 className="text-4xl font-extrabold mb-8 text-indigo-400 text-center drop-shadow-lg">Knapsack Variants</h1>
144+
145+
<div className="mb-8 p-4 rounded-xl bg-gray-800 border border-gray-700 shadow-inner group">
146+
<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>
147+
</div>
148+
149+
<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">
150+
<div className="w-full md:w-1/3">
151+
<label className="text-gray-300">Weights (comma separated):</label>
152+
<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" />
153+
</div>
154+
155+
<div className="w-full md:w-1/3">
156+
<label className="text-gray-300">Values (comma separated):</label>
157+
<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" />
158+
</div>
159+
160+
<div className="flex items-center gap-3">
161+
<label className="text-gray-300">Capacity (W):</label>
162+
<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" />
163+
</div>
164+
165+
<div className="flex items-center gap-3">
166+
<label className="text-gray-300">Algorithm:</label>
167+
<select value={algorithm} onChange={(e) => setAlgorithm(e.target.value)} className="p-2 rounded-lg bg-gray-700 text-white border border-gray-600">
168+
<option value="topDown">Top-Down (Memoization)</option>
169+
<option value="bottomUp">Bottom-Up (Tabulation)</option>
170+
</select>
171+
</div>
172+
173+
<button onClick={handleCompute} className="bg-indigo-600 hover:bg-indigo-700 text-white font-bold px-6 py-3 rounded-xl">Re-Visualize</button>
174+
</div>
175+
176+
{steps.length > 0 ? (
177+
<>
178+
<div className="flex flex-wrap justify-between items-center mb-6 p-4 rounded-xl bg-gray-800 border border-gray-700 shadow-lg">
179+
<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>
180+
181+
<div className="flex gap-2">
182+
<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>
183+
<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>
184+
</div>
185+
186+
<div className="flex items-center gap-2">
187+
<label className="text-gray-300">Speed:</label>
188+
<select value={speed} onChange={(e) => setSpeed(Number(e.target.value))} className="p-2 rounded-lg bg-gray-700 text-white border border-gray-600">
189+
{Object.entries(SPEED_OPTIONS).map(([label, ms]) => (
190+
<option key={label} value={ms}>{label}</option>
191+
))}
192+
</select>
193+
</div>
194+
</div>
195+
196+
<div className="text-center mb-4">
197+
<p className="text-2xl font-bold text-yellow-400">Step <b>{currentStep + 1}</b> / <b>{steps.length}</b></p>
198+
</div>
199+
200+
<div className="border border-gray-700 p-6 rounded-xl bg-gray-800 shadow-2xl">
201+
<div className="mb-6 p-4 rounded-lg bg-gray-700 border-l-4 border-teal-400 shadow-inner">
202+
<p className="text-teal-400 font-medium text-md uppercase tracking-wide">Current Action</p>
203+
<p className="text-xl mt-2 text-gray-200 leading-relaxed">{currentState.message || 'Starting computation...'}</p>
204+
</div>
205+
206+
{currentState.array && (
207+
<DPGrid array={currentState.array} currentIndex={currentState.currentIndex || currentState.index} />
208+
)}
209+
210+
{isFinalStep && finalResult !== null && (
211+
<div className="mt-8 p-5 rounded-xl bg-green-900 border border-green-700 text-center shadow-lg">
212+
<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>
213+
</div>
214+
)}
215+
216+
<details className="mt-8 text-sm text-gray-400">
217+
<summary className="cursor-pointer hover:text-gray-200 text-md font-medium">Click to View Raw Step Data (for debugging)</summary>
218+
<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>
219+
</details>
220+
</div>
221+
</>
222+
) : (
223+
<div className="text-center p-12 bg-gray-800 rounded-xl text-gray-400 text-xl shadow-xl border border-gray-700">
224+
<p className="mb-4">Welcome to the Knapsack Variants Visualizer!</p>
225+
<p>Provide weights and values (comma-separated) and a capacity to start.</p>
226+
</div>
227+
)}
228+
</div>
229+
</div>
230+
);
231+
}

src/pages/dynamic-programming/DyanmicProgrammingPage.jsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Grid, Brain, Layers, Code, Menu, X } from "lucide-react";
33
import Levenshtein from "./Levenshtein";
44
import MatrixChainMultiplication from "./MatrixChainMultiplication";
55
import FibonacciSequence from "./FibonacciSequence";
6+
import Knapsack from "./Knapsack";
67

78
export default function DynamicProgrammingPage() {
89
const [selectedAlgo, setSelectedAlgo] = useState("");
@@ -28,6 +29,12 @@ export default function DynamicProgrammingPage() {
2829
<FibonacciSequence />
2930
</div>
3031
);
32+
case "Knapsack":
33+
return (
34+
<div className="md:w-full w-screen overflow-clip p-2">
35+
<Knapsack/>
36+
</div>
37+
);
3138
default:
3239
return (
3340
<div className="flex flex-col items-center justify-center text-center p-6 min-h-screen bg-gray-950">
@@ -89,6 +96,7 @@ export default function DynamicProgrammingPage() {
8996
<option value="Levenshtein">Levenshtein Distance</option>
9097
<option value="MatrixChainMultiplication">Matrix Chain Multiplication</option>
9198
<option value="Fibonacci">Fibonacci Sequence</option>
99+
<option value="Knapsack">Knapsack</option>
92100
</select>
93101

94102
<button

0 commit comments

Comments
 (0)