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
118 changes: 118 additions & 0 deletions src/algorithms/Recursion/SubsetSum.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
export function getSubsetSumSteps(arr = [], target = 0) {
const n = arr.length;
const steps = [];

const dp = Array.from({ length: n + 1 }, () =>
Array(target + 1).fill(false)
);

dp[0][0] = true;

steps.push({
type: "init",
message: "Initialize DP table: dp[0][0] = true (empty subset → sum 0)",
dp: structuredClone(dp),
});

for (let i = 1; i <= n; i++) {
const val = arr[i - 1];

steps.push({
type: "init_row",
row: i,
value: val,
message: `Processing element arr[${i - 1}] = ${val}`,
dp: structuredClone(dp),
});

for (let sum = 0; sum <= target; sum++) {
steps.push({
type: "process_cell",
i,
sum,
value: val,
message: `Checking if we can form sum ${sum} using first ${i} elements`,
dp: structuredClone(dp),
});

if (dp[i - 1][sum]) {
dp[i][sum] = true;
steps.push({
type: "true_set",
i,
sum,
by: "exclude",
message: `dp[${i}][${sum}] = true (exclude ${val})`,
dp: structuredClone(dp),
});
}

if (sum >= val && dp[i - 1][sum - val]) {
dp[i][sum] = true;
steps.push({
type: "true_set",
i,
sum,
by: "include",
message: `dp[${i}][${sum}] = true (include ${val})`,
dp: structuredClone(dp),
});
}
}

steps.push({
type: "complete_row",
row: i,
message: `Completed row ${i} (processed arr[${i - 1}] = ${val})`,
dp: structuredClone(dp),
});
}

const isPossible = dp[n][target];

if (!isPossible) {
steps.push({
type: "no_solution",
message: `No subset found that adds up to target ${target}.`,
dp: structuredClone(dp),
});
return { steps, solutions: [], solutionCount: 0 };
}

const solution = [];
let i = n, sum = target;

while (i > 0 && sum >= 0) {
if (dp[i - 1][sum]) {
steps.push({
type: "decision",
message: `arr[${i - 1}] = ${arr[i - 1]} was EXCLUDED`,
dp: structuredClone(dp),
});
i--;
} else {
const val = arr[i - 1];
solution.push(val);
steps.push({
type: "decision",
message: `arr[${i - 1}] = ${val} was INCLUDED`,
dp: structuredClone(dp),
});
sum -= val;
i--;
}
}

steps.push({
type: "solution",
solution: solution.reverse(),
message: `✅ Found solution subset: [${solution.join(", ")}]`,
dp: structuredClone(dp),
});

return {
steps,
solutions: [solution],
solutionCount: 1,
};
}
263 changes: 263 additions & 0 deletions src/components/Recursion/SubsetSumVisualizer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
import React, { useEffect, useRef, useState } from "react";
import { getSubsetSumSteps } from "../../algorithms/Recursion/SubsetSum";

const MIN_SPEED = 50;
const MAX_SPEED = 2000;

const Color = ({ color, text }) => (
<div className="flex items-center gap-2">
<div className={`w-4 h-4 rounded ${color}`} />
<span className="text-gray-300 text-sm">{text}</span>
</div>
);

export default function SubsetSumVisualizer() {
const [inputArray, setInputArray] = useState("3, 34, 4, 12, 5, 2");
const [arr, setArr] = useState([3, 34, 4, 12, 5, 2]);
const [target, setTarget] = useState(9);

const [steps, setSteps] = useState([]);
const [idx, setIdx] = useState(0);
const [playing, setPlaying] = useState(false);
const [solutions, setSolutions] = useState([]);

const [speed, setSpeed] = useState(400);
const timerRef = useRef(null);

const parseArray = () => {
const parsed = inputArray
.split(",")
.map((s) => Number(s.trim()))
.filter((n) => !isNaN(n));

return parsed.length ? parsed : null;
};

const regenerate = () => {
const res = getSubsetSumSteps(arr, target);
setSteps(res.steps);
setSolutions(res.solutions);
setIdx(0);
setPlaying(false);
};

const handleStart = () => {
const parsed = parseArray();
if (!parsed) {
alert("Invalid array");
return;
}

clearTimeout(timerRef.current);
setPlaying(false);

setArr(parsed);

setTimeout(() => {
regenerate();
setIdx(0);
setPlaying(true);
}, 80);
};
const handlePauseResume = () => {
if (steps.length === 0) return;
clearTimeout(timerRef.current);
setPlaying((p) => !p);
};
const handleReset = () => {
clearTimeout(timerRef.current);
setPlaying(false);
setIdx(0);
};
useEffect(() => {
clearTimeout(timerRef.current);

if (playing && idx < steps.length - 1) {
timerRef.current = setTimeout(() => setIdx((i) => i + 1), speed);
}

return () => clearTimeout(timerRef.current);
}, [playing, idx, steps, speed]);

const step = steps[idx] || {};

const getCellColor = (i, s) => {
if (!step.dp) return "bg-gray-700";

if (step.type === "process_cell" && step.i === i && step.sum === s)
return "bg-blue-400 text-black";

if (step.type === "true_set" && step.i === i && step.sum === s)
return step.by === "include"
? "bg-green-500 text-black"
: "bg-green-100 text-black";

return step.dp[i][s] ? "bg-green-700" : "bg-gray-800";
};

return (
<div className="min-h-screen bg-gray-900 text-white p-6 font-sans">
<div className="max-w-6xl mx-auto">
<h1 className="text-3xl font-bold text-center text-indigo-400 mb-6">
Subset Sum Visualizer
</h1>
{/*Controls*/}
<div className="bg-gray-800 p-4 rounded-lg mb-6 border border-gray-700">
<div className="flex flex-wrap gap-4 items-end">
<div className="flex-1 min-w-[200px]">
<label className="text-sm">Array</label>
<input
value={inputArray}
onChange={(e) => setInputArray(e.target.value)}
className="w-full mt-1 p-2 rounded bg-gray-700 border border-gray-600"
placeholder="comma-separated"
/>
</div>
<div>
<label className="text-sm">Target</label>
<input
type="number"
value={target}
onChange={(e) => setTarget(Number(e.target.value))}
className="w-20 mt-1 p-2 rounded bg-gray-700 border border-gray-600"
/>
</div>

<div>
<label className="text-sm">Speed (ms)</label>
<input
type="number"
value={speed}
min={MIN_SPEED}
max={MAX_SPEED}
onChange={(e) => setSpeed(Number(e.target.value))}
className="w-24 mt-1 p-2 rounded bg-gray-700 border border-gray-600"
/>
</div>

<button
onClick={handleStart}
className="bg-indigo-600 px-4 py-2 rounded-md hover:bg-indigo-700"
>
Start
</button>

<button
onClick={handlePauseResume}
disabled={steps.length === 0}
className={`px-4 py-2 rounded-md ${
playing ? "bg-yellow-500" : "bg-green-600"
}`}
>
{playing ? "Pause" : "Resume"}
</button>

<button
onClick={handleReset}
className="bg-red-600 px-4 py-2 rounded-md hover:bg-red-700"
>
Reset
</button>
</div>
</div>

<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* array */}
<div className="md:col-span-3 bg-gray-800 p-4 rounded-lg border border-gray-700 mb-4">
<h2 className="text-lg font-semibold text-indigo-300 mb-2">
Array Elements : (Each Row in DP corresponds to one element)
</h2>

<div className="flex flex-wrap gap-4">
{arr.map((v, i) => (
<div
key={i}
className={`w-16 h-16 rounded-full flex items-center justify-center text-xl font-bold ${
step.i - 1 === i
? "bg-blue-400 text-black"
: "bg-gray-700 text-white"
}`}
>
{v}
</div>
))}
</div>
</div>

{/*dp table*/}
<div className="md:col-span-2 bg-gray-800 p-4 rounded-lg border border-gray-700">
<h2 className="text-lg font-semibold text-indigo-300 mb-3">
DP Table (dp[i][sum])
</h2>

{step.dp && (
<div className="overflow-auto">
<table className="border-collapse text-center">
<thead>
<tr>
<th className="p-2 text-gray-300">i / sum</th>
{step.dp[0].map((_, s) => (
<th key={s} className="px-3 text-gray-300">
{s}
</th>
))}
</tr>
</thead>

<tbody>
{step.dp.map((row, i) => (
<tr key={i}>
<td className="px-3 py-2 text-gray-400">i={i}</td>
{row.map((_, s) => (
<td
key={s}
className={`w-12 h-12 border border-gray-700 ${getCellColor(
i,
s
)}`}
>
{step.dp[i][s] ? "T" : "F"}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)}

<div className="mt-5 bg-gray-900 p-4 rounded">
<h3 className="text-teal-300 font-semibold mb-2">Explanation</h3>
<p className="text-gray-300 text-sm">
{step.message || "Press Start to begin."}
</p>
</div>
</div>

<div className="bg-gray-800 p-4 rounded-lg border border-gray-700">
<h3 className="text-teal-300 font-semibold mb-3">Solutions</h3>

{solutions.length > 0 ? (
<div className="space-y-2 text-gray-200 text-sm">
{solutions.map((s, i) => (
<div key={i} className="p-2 bg-gray-900 rounded">
# <b>Subset:</b> [{s.join(", ")}]
</div>
))}
</div>
) : (
<div className="text-gray-400 text-sm">No solutions found yet.</div>
)}

<h3 className="text-indigo-300 font-semibold mt-6 mb-2">Color</h3>
<div className="flex flex-col gap-2 text-sm">
<Color color="bg-blue-400" text="Current cell" />
<Color color="bg-green-500" text="Set TRUE via include" />
<Color color="bg-green-100" text="Set TRUE via exclude" />
</div>
</div>
</div>
</div>
</div>
);
}
Loading
Loading