Combination Sum (LC 39): distinct positive candidates, a target, unlimited reuse of each candidate, every combination that hits the target exactly once. The search is a tree — each node is “what is left to reach”, each edge is the candidate just taken — and the piece draws that tree as the search enters it, one event per step: enter a node, push a candidate, record an answer (green), hit a dead end (red), skip or break, pop back up. The path the recursion is standing on is lit blue; a white ring marks the current node.
Three controls, one per classic failure.
Start index is the correctness argument. Pass i (never look left, but
the same candidate may repeat) generates each multiset exactly once, in
non-decreasing order. Pass 0 restarts the loop at every level, and
[2, 2, 3], [2, 3, 2] and [3, 2, 2] all come out — the counter goes red
with the duplicate count, and the tree is visibly wider. Pass i + 1 takes
each candidate at most once, which is a different problem: the tree is small
and the answers that need a repeat are simply missing.
Prune is the constant factor. With the candidates sorted, break the moment one is larger than what is left: an amber ✂ stub at the node counts the siblings that were never tested. Continue skips the oversize candidate but keeps probing its larger siblings — grey stubs, all wasted. None recurses into the negative and discovers the dead end one level down, as a red node the parent could have known not to create. The header counts nodes entered and loop probes, so the three modes are numbers, not opinions.
Record is the aliasing bug. A copy of path is the right answer. Path
itself pushes a reference to the working array; the results column then
shows every entry as whatever path is right now, and when the search ends
and path has been popped back to empty, every answer reads []. The logic
was never wrong.
Type your own candidates and target (capped so the page stays responsive — a tree past 600 nodes is truncated and says so), step through, or let it run.
Reuse
src/prune.js is a framework-free ES module:
cleanCandidates(text, { max, lo, hi })— distinct positive integers out of free text, sorted, capped.combinationSum(candidates, target)— the plain answer, as you would write it in an interview.simulate(candidates, target, { dedup, prune, maxNodes })— runs the search once and returns the tree (nodes, each with parent, edge candidate, remaining, start index, status, and the frame it was entered and left on) plusframes, one per event, each carrying the working path, the call stack, and the running counters; and the ground truth to colour against (truth,distinct,dups,missing).dedupis'start','none'or'next';pruneis'break','continue'or'none'. Same trace-per-tick shape asledger,pincer,windowandastar-grid.layout(nodes)— a tidy top-down layout: leaves in DFS order, parents over the mean of their children.
No rendering or timers in the module; the canvas demo is reference code.
Gotchas
- The tree is laid out from the finished search, so nodes appear where they will end up and nothing reshuffles as the search grows into it.
- Recording mode is a display choice only — the search is identical, the results column just shows what an aliased list would read at each frame.
- The demo bundles its own copy of
prune.js(self-contained by contract); re-copy after editingsrc/.