LC 994 Rotting Oranges, drawn as the thing it actually is: one wave with several origins, not several races. A grid of fresh fruit, a few rotten cells, and a clock on the frame. Every minute the rot advances one ring from every rotten cell simultaneously, and a fresh cell belongs to whichever front reaches it first — wearing that front’s colour forever, because nothing ever re-rots.
The clock lives on the frame, not in the cells, on purpose. A per-cell
timestamp invites you to read the board as a distance map, which is true but
backwards; what multi-source BFS does is tick a single global minute while
the whole frontier moves at once. The tick strip along the top edge is that
minute. The answer to the problem is simply where the strip ends — or −1
if, at the end, something is still green and outlined red.
Three things to watch:
- Minute zero is already full. Before the first step, every rotten cell is in the queue. That is the entire difference between this and a plain BFS, and it is the line people forget: seed all sources, then loop.
- Seams. Where two fronts meet, a light line is drawn between the cells. Neither side ever crosses it — a cell claimed by the pink front at minute 3 is not re-claimed by the orange one at minute 4 — which is why one pass visits each cell exactly once.
- The price of taking turns. The legend compares visits: one BFS over all sources against one BFS per source with a per-cell minimum afterwards. The answer comes out the same (the min is the min), the work does not — and the tempting shortcut, “each source’s furthest reach, then the max”, is shown with a ✗ because it overestimates.
Sliders set how many cells start rotten and how many are empty; click any cell
to cycle it fresh → rotten → empty and the wave recomputes; space steps,
R rerolls.
Reuse
src/all-at-once.js is a framework-free ES module:
makeGrid(cols, rows, { sources, emptyShare, rng })— a board in LC 994’s encoding (0 empty, 1 fresh, 2 rotten), sources placed far apart.rot(grid)— multi-source BFS, returned asrings(what rots at each minute, with the owning source),dist,owner,stuck,minutes(the LC answer,-1if anything is stuck) andvisits.rotOneAtATime(grid)— the per-source version for comparison: sameminutes, inflatedvisits, plusnaive(max of per-source reaches).mulberry32(seed)— a small seeded PRNG so rerolls are reproducible.
No rendering, no timers in the module. The canvas drawing and controls in
demo/ are reference code.
Gotchas
rotmarks a cell at enqueue time, which is what makesvisitsequal the number of rotted cells. Mark at dequeue and the same cell can enter the queue from two fronts — the seams would still look right, the visit count would not.minutesisrings.length - 1, not the number of loop iterations: an implementation that increments the clock every pass through the outer loop reports one too many on any board whose last ring rots nothing.- The demo bundles its own copy of
all-at-once.js(self-contained by contract); re-copy after editingsrc/.


