Task Scheduler (LC 621) has a two-line closed form, and the line everybody
memorises — max(len, (f − 1)(n + 1) + m) — hides the picture that makes it
obvious. The most frequent letter, with f copies and n intervals of
cooldown between each pair, has to occupy f cells spaced n + 1 apart
before any other letter is placed. Draw those as f − 1 rows of width
n + 1 and one more cell: that is the frame, and it is a lower bound on
its own. Every other letter that also has count f adds a column to it
(m columns in all). The only question left is whether the remaining letters
settle into the frame’s empty cells or spill past its right edge — and once
anything spills, every row stretches and there is no idle interval anywhere,
so the answer is simply the number of tasks.
Three layers, top to bottom:
- Counts. One bar per letter, in placement order; the top count is amber and fixes the frame. A bar dims once all its copies are placed.
- The grid. The frame is drawn dashed before anything fills it —
f − 1gap rows ×n + 1, then a tail row ofmcells set slightly apart. The max letters fill whole columns first. The rest are taken by count descending and dropped in column-major (down a column, then the next); each placement is outlined white and the idle cells behind the walk fill in as the walk passes them. When a column lands pastn + 1the region is shaded purple: the rows are stretching and the idle count is 0. - The strip. The grid read row-major — the actual schedule, gap rows
padded with idle, tail row last. The result panel carries the frame bound
the whole time; at the end it says which side of
maxwon and how many idle intervals are left.
Two switches. Fill the rest → in arrival order keeps the frame and the
column walk but drops the remaining letters in the order they first appear
instead of by count. The bound is unchanged; the construction breaks: a
letter with count exactly f − 1 that starts mid-column wraps into the next
column, and its wrap pair reads one row minus one apart — n, one short of
the cooldown. The wrap case (AAABCC, n = 2) is the two-second
reproduction: the strip marks the pair red and says 2 apart — needs 3. The
sort is what guarantees that count-(f − 1) letters come first among the
rest and each gets a whole column. Final answer → frame only returns the
bound without comparing it to the task count; overflow (AAABBBCCCDDDEE,
n = 2) shows it returning 10 for 14 tasks, in red, with the stretched rows
above saying why.
Presets: the three LC examples, the wrap case, overflow, and random. The
tasks box takes any string of letters (up to 40); n goes to 12.
Reuse
src/frame.js is a framework-free ES module:
leastInterval(tasks, n)— the closed form, as you would write it in an interview.scheduleByHeap(tasks, n)— the tick-by-tick simulation (run the available letter with the most copies left; idle when none is available), the brute-force truth the formula is checked against. Returns the schedule withnullfor idle.violations(schedule, n)— every pair of identical letters closer thann + 1.placementOrder(tasks, order)— the letters in the order the construction places them: max letters first, then the rest by count or as given.simulate(tasks, n, { order, check })— one frame per copy placed with the grid snapshot, where it landed, whether it is a max letter, the idle cells left, the current row width and a one-sentence note; then a verdict frame with the schedule, its violations, the answer, the truth and whether they match.order: 'given'reproduces the wrap bug;check: falsethe missingmax. Same trace-per-tick shape ascircuit,cleaveandcoalesce.readRowMajor(grid, rows, n)— the schedule a grid encodes.
No rendering or timers in the module; the canvas demo is reference code.
Gotchas
- The wrap bug only bites a letter whose count is exactly
f − 1and that starts mid-column; with fewer copies the wrap pair is never adjacent rows, which is why'given'order is often right by luck. The verdict says so. - With
f = 1there are no gap rows at all; the grid is just the tail row and the answer is the task count. The frame caption says so rather than drawing an empty frame. - The demo bundles its own copy of
frame.js(self-contained by contract); re-copy after editingsrc/.