Skip to content

Commit f0b4e46

Browse files
Merge pull request #75 from indrasuthar07/subset-sum
feat: subset-sum visualizer
2 parents 37f10e7 + fb39e69 commit f0b4e46

File tree

4 files changed

+395
-0
lines changed

4 files changed

+395
-0
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
export function getSubsetSumSteps(arr = [], target = 0) {
2+
const n = arr.length;
3+
const steps = [];
4+
5+
const dp = Array.from({ length: n + 1 }, () =>
6+
Array(target + 1).fill(false)
7+
);
8+
9+
dp[0][0] = true;
10+
11+
steps.push({
12+
type: "init",
13+
message: "Initialize DP table: dp[0][0] = true (empty subset → sum 0)",
14+
dp: structuredClone(dp),
15+
});
16+
17+
for (let i = 1; i <= n; i++) {
18+
const val = arr[i - 1];
19+
20+
steps.push({
21+
type: "init_row",
22+
row: i,
23+
value: val,
24+
message: `Processing element arr[${i - 1}] = ${val}`,
25+
dp: structuredClone(dp),
26+
});
27+
28+
for (let sum = 0; sum <= target; sum++) {
29+
steps.push({
30+
type: "process_cell",
31+
i,
32+
sum,
33+
value: val,
34+
message: `Checking if we can form sum ${sum} using first ${i} elements`,
35+
dp: structuredClone(dp),
36+
});
37+
38+
if (dp[i - 1][sum]) {
39+
dp[i][sum] = true;
40+
steps.push({
41+
type: "true_set",
42+
i,
43+
sum,
44+
by: "exclude",
45+
message: `dp[${i}][${sum}] = true (exclude ${val})`,
46+
dp: structuredClone(dp),
47+
});
48+
}
49+
50+
if (sum >= val && dp[i - 1][sum - val]) {
51+
dp[i][sum] = true;
52+
steps.push({
53+
type: "true_set",
54+
i,
55+
sum,
56+
by: "include",
57+
message: `dp[${i}][${sum}] = true (include ${val})`,
58+
dp: structuredClone(dp),
59+
});
60+
}
61+
}
62+
63+
steps.push({
64+
type: "complete_row",
65+
row: i,
66+
message: `Completed row ${i} (processed arr[${i - 1}] = ${val})`,
67+
dp: structuredClone(dp),
68+
});
69+
}
70+
71+
const isPossible = dp[n][target];
72+
73+
if (!isPossible) {
74+
steps.push({
75+
type: "no_solution",
76+
message: `No subset found that adds up to target ${target}.`,
77+
dp: structuredClone(dp),
78+
});
79+
return { steps, solutions: [], solutionCount: 0 };
80+
}
81+
82+
const solution = [];
83+
let i = n, sum = target;
84+
85+
while (i > 0 && sum >= 0) {
86+
if (dp[i - 1][sum]) {
87+
steps.push({
88+
type: "decision",
89+
message: `arr[${i - 1}] = ${arr[i - 1]} was EXCLUDED`,
90+
dp: structuredClone(dp),
91+
});
92+
i--;
93+
} else {
94+
const val = arr[i - 1];
95+
solution.push(val);
96+
steps.push({
97+
type: "decision",
98+
message: `arr[${i - 1}] = ${val} was INCLUDED`,
99+
dp: structuredClone(dp),
100+
});
101+
sum -= val;
102+
i--;
103+
}
104+
}
105+
106+
steps.push({
107+
type: "solution",
108+
solution: solution.reverse(),
109+
message: `✅ Found solution subset: [${solution.join(", ")}]`,
110+
dp: structuredClone(dp),
111+
});
112+
113+
return {
114+
steps,
115+
solutions: [solution],
116+
solutionCount: 1,
117+
};
118+
}
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
import React, { useEffect, useRef, useState } from "react";
2+
import { getSubsetSumSteps } from "../../algorithms/Recursion/SubsetSum";
3+
4+
const MIN_SPEED = 50;
5+
const MAX_SPEED = 2000;
6+
7+
const Color = ({ color, text }) => (
8+
<div className="flex items-center gap-2">
9+
<div className={`w-4 h-4 rounded ${color}`} />
10+
<span className="text-gray-300 text-sm">{text}</span>
11+
</div>
12+
);
13+
14+
export default function SubsetSumVisualizer() {
15+
const [inputArray, setInputArray] = useState("3, 34, 4, 12, 5, 2");
16+
const [arr, setArr] = useState([3, 34, 4, 12, 5, 2]);
17+
const [target, setTarget] = useState(9);
18+
19+
const [steps, setSteps] = useState([]);
20+
const [idx, setIdx] = useState(0);
21+
const [playing, setPlaying] = useState(false);
22+
const [solutions, setSolutions] = useState([]);
23+
24+
const [speed, setSpeed] = useState(400);
25+
const timerRef = useRef(null);
26+
27+
const parseArray = () => {
28+
const parsed = inputArray
29+
.split(",")
30+
.map((s) => Number(s.trim()))
31+
.filter((n) => !isNaN(n));
32+
33+
return parsed.length ? parsed : null;
34+
};
35+
36+
const regenerate = () => {
37+
const res = getSubsetSumSteps(arr, target);
38+
setSteps(res.steps);
39+
setSolutions(res.solutions);
40+
setIdx(0);
41+
setPlaying(false);
42+
};
43+
44+
const handleStart = () => {
45+
const parsed = parseArray();
46+
if (!parsed) {
47+
alert("Invalid array");
48+
return;
49+
}
50+
51+
clearTimeout(timerRef.current);
52+
setPlaying(false);
53+
54+
setArr(parsed);
55+
56+
setTimeout(() => {
57+
regenerate();
58+
setIdx(0);
59+
setPlaying(true);
60+
}, 80);
61+
};
62+
const handlePauseResume = () => {
63+
if (steps.length === 0) return;
64+
clearTimeout(timerRef.current);
65+
setPlaying((p) => !p);
66+
};
67+
const handleReset = () => {
68+
clearTimeout(timerRef.current);
69+
setPlaying(false);
70+
setIdx(0);
71+
};
72+
useEffect(() => {
73+
clearTimeout(timerRef.current);
74+
75+
if (playing && idx < steps.length - 1) {
76+
timerRef.current = setTimeout(() => setIdx((i) => i + 1), speed);
77+
}
78+
79+
return () => clearTimeout(timerRef.current);
80+
}, [playing, idx, steps, speed]);
81+
82+
const step = steps[idx] || {};
83+
84+
const getCellColor = (i, s) => {
85+
if (!step.dp) return "bg-gray-700";
86+
87+
if (step.type === "process_cell" && step.i === i && step.sum === s)
88+
return "bg-blue-400 text-black";
89+
90+
if (step.type === "true_set" && step.i === i && step.sum === s)
91+
return step.by === "include"
92+
? "bg-green-500 text-black"
93+
: "bg-green-100 text-black";
94+
95+
return step.dp[i][s] ? "bg-green-700" : "bg-gray-800";
96+
};
97+
98+
return (
99+
<div className="min-h-screen bg-gray-900 text-white p-6 font-sans">
100+
<div className="max-w-6xl mx-auto">
101+
<h1 className="text-3xl font-bold text-center text-indigo-400 mb-6">
102+
Subset Sum Visualizer
103+
</h1>
104+
{/*Controls*/}
105+
<div className="bg-gray-800 p-4 rounded-lg mb-6 border border-gray-700">
106+
<div className="flex flex-wrap gap-4 items-end">
107+
<div className="flex-1 min-w-[200px]">
108+
<label className="text-sm">Array</label>
109+
<input
110+
value={inputArray}
111+
onChange={(e) => setInputArray(e.target.value)}
112+
className="w-full mt-1 p-2 rounded bg-gray-700 border border-gray-600"
113+
placeholder="comma-separated"
114+
/>
115+
</div>
116+
<div>
117+
<label className="text-sm">Target</label>
118+
<input
119+
type="number"
120+
value={target}
121+
onChange={(e) => setTarget(Number(e.target.value))}
122+
className="w-20 mt-1 p-2 rounded bg-gray-700 border border-gray-600"
123+
/>
124+
</div>
125+
126+
<div>
127+
<label className="text-sm">Speed (ms)</label>
128+
<input
129+
type="number"
130+
value={speed}
131+
min={MIN_SPEED}
132+
max={MAX_SPEED}
133+
onChange={(e) => setSpeed(Number(e.target.value))}
134+
className="w-24 mt-1 p-2 rounded bg-gray-700 border border-gray-600"
135+
/>
136+
</div>
137+
138+
<button
139+
onClick={handleStart}
140+
className="bg-indigo-600 px-4 py-2 rounded-md hover:bg-indigo-700"
141+
>
142+
Start
143+
</button>
144+
145+
<button
146+
onClick={handlePauseResume}
147+
disabled={steps.length === 0}
148+
className={`px-4 py-2 rounded-md ${
149+
playing ? "bg-yellow-500" : "bg-green-600"
150+
}`}
151+
>
152+
{playing ? "Pause" : "Resume"}
153+
</button>
154+
155+
<button
156+
onClick={handleReset}
157+
className="bg-red-600 px-4 py-2 rounded-md hover:bg-red-700"
158+
>
159+
Reset
160+
</button>
161+
</div>
162+
</div>
163+
164+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
165+
{/* array */}
166+
<div className="md:col-span-3 bg-gray-800 p-4 rounded-lg border border-gray-700 mb-4">
167+
<h2 className="text-lg font-semibold text-indigo-300 mb-2">
168+
Array Elements : (Each Row in DP corresponds to one element)
169+
</h2>
170+
171+
<div className="flex flex-wrap gap-4">
172+
{arr.map((v, i) => (
173+
<div
174+
key={i}
175+
className={`w-16 h-16 rounded-full flex items-center justify-center text-xl font-bold ${
176+
step.i - 1 === i
177+
? "bg-blue-400 text-black"
178+
: "bg-gray-700 text-white"
179+
}`}
180+
>
181+
{v}
182+
</div>
183+
))}
184+
</div>
185+
</div>
186+
187+
{/*dp table*/}
188+
<div className="md:col-span-2 bg-gray-800 p-4 rounded-lg border border-gray-700">
189+
<h2 className="text-lg font-semibold text-indigo-300 mb-3">
190+
DP Table (dp[i][sum])
191+
</h2>
192+
193+
{step.dp && (
194+
<div className="overflow-auto">
195+
<table className="border-collapse text-center">
196+
<thead>
197+
<tr>
198+
<th className="p-2 text-gray-300">i / sum</th>
199+
{step.dp[0].map((_, s) => (
200+
<th key={s} className="px-3 text-gray-300">
201+
{s}
202+
</th>
203+
))}
204+
</tr>
205+
</thead>
206+
207+
<tbody>
208+
{step.dp.map((row, i) => (
209+
<tr key={i}>
210+
<td className="px-3 py-2 text-gray-400">i={i}</td>
211+
{row.map((_, s) => (
212+
<td
213+
key={s}
214+
className={`w-12 h-12 border border-gray-700 ${getCellColor(
215+
i,
216+
s
217+
)}`}
218+
>
219+
{step.dp[i][s] ? "T" : "F"}
220+
</td>
221+
))}
222+
</tr>
223+
))}
224+
</tbody>
225+
</table>
226+
</div>
227+
)}
228+
229+
<div className="mt-5 bg-gray-900 p-4 rounded">
230+
<h3 className="text-teal-300 font-semibold mb-2">Explanation</h3>
231+
<p className="text-gray-300 text-sm">
232+
{step.message || "Press Start to begin."}
233+
</p>
234+
</div>
235+
</div>
236+
237+
<div className="bg-gray-800 p-4 rounded-lg border border-gray-700">
238+
<h3 className="text-teal-300 font-semibold mb-3">Solutions</h3>
239+
240+
{solutions.length > 0 ? (
241+
<div className="space-y-2 text-gray-200 text-sm">
242+
{solutions.map((s, i) => (
243+
<div key={i} className="p-2 bg-gray-900 rounded">
244+
# <b>Subset:</b> [{s.join(", ")}]
245+
</div>
246+
))}
247+
</div>
248+
) : (
249+
<div className="text-gray-400 text-sm">No solutions found yet.</div>
250+
)}
251+
252+
<h3 className="text-indigo-300 font-semibold mt-6 mb-2">Color</h3>
253+
<div className="flex flex-col gap-2 text-sm">
254+
<Color color="bg-blue-400" text="Current cell" />
255+
<Color color="bg-green-500" text="Set TRUE via include" />
256+
<Color color="bg-green-100" text="Set TRUE via exclude" />
257+
</div>
258+
</div>
259+
</div>
260+
</div>
261+
</div>
262+
);
263+
}

0 commit comments

Comments
 (0)