Koko Eating Bananas (LC 875) is the canonical “binary search on the answer”
problem, and every explainer draws the piles. The piles are not the
interesting object. Nothing about them is sorted and sorting does not help.
The sorted thing is the predicate — can she finish every pile at speed
k within h hours? — which over k = 1 .. max(piles) reads F F … F T T … T with exactly one boundary, because hours(k) = Σ ⌈pile / k⌉ can only
fall as k rises. That strip is what the search is searching, so that is
what the piece draws.
Three layers, top to bottom:
- The piles, sliced. Each bar is cut into the hours it takes at the
current speed
k— slabs of heightk, the last one partial — and the total is compared againsthin the corner. Drag the speed slider and the slabs regrow; the check is the whole feasibility test, and it isO(n). - The strip. One cell per candidate speed. It starts dark, because the
search never builds it. Every probe — a hand-made one from the slider, or
the search’s own — paints its cell solid red or green and paints
everything monotonicity implies at half strength: every speed below a red
cell is red, every speed above a green cell is green. The rightmost cell is
green from the start with no probe behind it, because
k = max(piles)is one pile per hour and the constraints promiseh ≥ n. The invariant is learned by never being seen broken: you cannot make the strip hold a green cell left of a red one, however you drag. - The brackets. Each probe of the binary search is a row: the
[lo, hi]bracket it started from and the mid it tested, lit by the answer. Feasible keeps mid in range (hi = mid); infeasible discards it (lo = mid + 1). When the bracket closes, a white line marks the boundary — the first green cell — and the readout says how many cells were never looked at, which on the LC example is 25 of 30 and at the real constraints would be a billion minus thirty.
Two switches. lo = ⌈Σ piles / h⌉ replaces the lazy lower bound with the
one the constraints hand you — at most k bananas an hour means k · h ≥ Σ piles — drawn as an amber dashed line, and usually worth a probe. Reveal
strip ghosts the full predicate in behind the known cells, so you can see
that what the probes implied was true, and that nothing else was ever
needed.
Presets: the three LC examples and random piles. → probes the next mid,
space runs the search, r resets, v reveals the strip. The h slider
re-cuts the boundary; the strip is recomputed and the probes cleared.
Reuse
src/monotone.js is a framework-free ES module:
hoursAt(piles, k)—Σ ⌈p / k⌉, the feasibility check.bounds(piles, { tightLo, h })—{ lo, hi }:lo = 1, or⌈Σ piles / h⌉when tightened;hi = max(piles).strip(piles, h)— the whole predicate,[{ k, hours, ok }]for everykup tohi.firstFeasible(piles, h)— the answer by linear scan; the oracle.search(piles, h, opts)— the binary search as a trace:{ answer, probes: [{ lo, hi, mid, hours, ok, next }], start }.knowledge(probes, kMax)— what a set of{ k, ok }probes says about every cell ('T'/'F'probed,'t'/'f'implied,nullunknown), plussettled, the answer once the boundary is pinned, andunknown, the count of cells nothing has been said about.randomPiles(n, max, rng).
No rendering or timers in the module; the canvas demo is reference code.
Gotchas
- The strip has
max(piles)cells, so the demo keeps piles small (≤ 40). At the problem’s real constraints it would have 10⁹ cells, which is the point:strip()is there to be the thing you would never call. knowledgetreats the axiom probe (hi, feasible by argument) the same as any other; the demo adds it itself so the search’s first bracket is drawn against a strip that already knows its right end.- With
h < piles.lengthno speed works andfirstFeasiblereturnsnull; the demo’shslider is floored atnso that state is unreachable there. - The demo bundles its own copy of
monotone.js(self-contained by contract); re-copy after editingsrc/.