Stack bricks off the edge of a bench and reach as far as you can. There is one rule and it is the only rule:
the centre of mass of everything above a cut must lie within the footprint of whatever is directly below it.
Apply it at every horizontal cut in the stack, plus one more at the bench, and
that is the entire model. src/overhang.mjs is a hundred lines of code — the
rule, the two legal moves in closed form, and the reference builders — and
nothing else. No solver, no timestep, no collisions.
The first thing you will do, and why it does not work
Lay a brick. Push it out until it is about to go, which is half a length. Lay another on top, push it out until it is about to go. Lay another. This is the obvious strategy, everybody plays it, and it never gets anywhere:
| bricks | greedy on top | greedy underneath | H(n)/2 | what the last brick bought |
|---|---|---|---|---|
| 1 | 0.5000 | 0.5000 | 0.5000 | — |
| 2 | 0.5000 | 0.7500 | 0.7500 | 0.0000 |
| 4 | 0.5000 | 1.0417 | 1.0417 | 0.0000 |
| 18 | 0.5000 | 1.7476 | 1.7476 | 0.0000 |
| 1000 | 0.5000 | 3.7427 | 3.7427 | 0.0000 |
Brick one buys half a length. Bricks two through one thousand buy, between them, nothing at all — not a little, not diminishing returns, zero — and the reason is two lines of arithmetic:
- A maximal placement leaves the stack’s centre of mass exactly on the bench edge. That is what maximal means here.
- So the next brick’s own centre of mass may not go past the edge either, or the average walks off the bench. A brick whose middle sits on the edge reaches 0.5 past it. Which is where you already were.
The demo’s watch: greedy on top button plays this out: eighteen bricks
placed perfectly legally, ending as a flush column hanging exactly half off,
with the optimum drawn in dashes beside it.
And it is not the first brick’s fault. Hold brick one back to d and then play
greedily on top of it, and the run reaches min(d + 0.5, 1 − d):
| first brick reaches | 2 bricks | 4 bricks | 12 bricks | 40 bricks |
|---|---|---|---|---|
| 0.5000 | 0.5000 | 0.5000 | 0.5000 | 0.5000 |
| 0.3000 | 0.7000 | 0.7000 | 0.7000 | 0.7000 |
| 0.2500 | 0.7500 | 0.7500 | 0.7500 | 0.7500 |
| 0.2000 | 0.7000 | 0.7000 | 0.7000 | 0.7000 |
| 0.0833 | 0.5833 | 0.5833 | 0.5833 | 0.5833 |
Sweeping every first placement, the ceiling of the entire greedy-on-top strategy is 0.750, reached with two bricks, and the rows are flat, so brick three onward is decoration. Forty bricks could have done 2.139.
The same greed, pointed the other way
Now build downward. The tower you have is a rigid object; slide a fresh brick under it and push the whole assembly out. Take everything on offer, every time — the identical rule that just failed.
Two constraints survive the rigid shift (the joints inside the tower ride along unchanged), and they give the move’s size directly:
d ≤ −C_all + 1/(2(n+1))
For a tower already maximal, C_all = 0 and the shift is exactly 1/(2(n+1)).
Measured against the closed form, brick by brick:
| brick #k | measured shift | 1/(2k) | reach after |
|---|---|---|---|
| 1 | 0.500000 | 0.500000 | 0.5000 |
| 2 | 0.250000 | 0.250000 | 0.7500 |
| 3 | 0.166667 | 0.166667 | 0.9167 |
| 4 | 0.125000 | 0.125000 | 1.0417 |
| 10 | 0.050000 | 0.050000 | 1.4645 |
Which sums to ½·H(n), the harmonic series, which diverges. Downward there
is no ceiling at all, and greed is not merely adequate, it is exactly
optimal at every single step.
Same bricks. Same physics. Same “take the most you can” instruction. 0.500 one way, unbounded the other. The build order is not a detail of how you get to the tower — it is the thing that decides whether the tower is reachable.
Why the orders are not symmetric
The two stacks are the same object. demo/ will draw either. The asymmetry
is entirely in what each move has to know.
Going down, a move only ever changes things below what is already placed. The bricks above it keep their relative positions, their joints keep their margins, and the move cannot invalidate a decision you already made. Local information is complete information.
Going up, every brick you lay constrains every brick still to come — and by how much depends on how many are still coming, which you do not know. Lay the optimal eight-stack from the bottom and ask, at each step, how far that brick was allowed to go against how far it actually went:
| brick (from bottom) | placed at | furthest legal | held back by | j/(2(j+1)) |
|---|---|---|---|---|
| 1 | -0.93750 | -0.50000 | 0.43750 | 0.43750 |
| 2 | -0.86607 | -0.43750 | 0.42857 | 0.42857 |
| 4 | -0.68274 | -0.28274 | 0.40000 | 0.40000 |
| 6 | -0.39107 | -0.05774 | 0.33333 | 0.33333 |
| 7 | -0.14107 | 0.10893 | 0.25000 | 0.25000 |
| 8 | 0.35893 | 0.35893 | 0.00000 | 0.00000 |
Not one brick but the last takes what it is offered, and the holdback is
j/(2(j+1)) where j is how many bricks are still coming — a quarter of a
length with one left to place, climbing toward half a length the deeper the
pile. Downward that column is zero all the way down.
So the upward builder is not playing badly. They are playing a game that requires a number they were never given, and the penalty for guessing it wrong is steep. Rows are what the builder planned for, columns what the pile actually held:
| planned for | got 2 | got 4 | got 6 | got 10 | got 18 | best for that count |
|---|---|---|---|---|---|---|
| 2 | 0.750 | — | — | — | — | 0.750 |
| 4 | 0.292 | 1.042 | — | — | — | 1.042 |
| 10 | 0.106 | 0.239 | 0.423 | 1.464 | — | 1.464 |
| 18 | 0.057 | 0.122 | 0.196 | 0.389 | 1.748 | 1.748 |
| 40 | 0.025 | 0.052 | 0.080 | 0.142 | 0.294 | 2.139 |
The diagonal is optimal; everything off it is a bad guess about the depth of a pile. Plan for forty, get ten, and you reach 0.142 — worse than one brick laid on its own and left alone.
The price of a metre
Unbounded is not the same as cheap. H(n) ≈ ln n + γ inverts to n ≈ e^(2R−γ), and the useful reading is the derivative:
| reach (L) | bricks | × previous | e^(2R−γ) |
|---|---|---|---|
| 0.50 | 1 | — | 1.5 |
| 1.00 | 4 | 4.000 | 4.1 |
| 1.50 | 11 | 2.750 | 11.3 |
| 2.00 | 31 | 2.818 | 30.7 |
| 2.50 | 83 | 2.677 | 83.3 |
| 3.00 | 227 | 2.735 | 226.5 |
| 4.00 | 1,674 | 2.718 | 1673.7 |
| 5.00 | 12,367 | 2.718 | 12367.0 |
Every extra half-length of reach costs e times as many bricks, and it is the
same factor every time, forever. Nobody put 2.71828 in this file. It is what
you get when the thing you are summing is 1/k and you ask how many terms buy
a fixed amount. Clearing ten lengths would take 272 million bricks.
The optimum is a knife edge everywhere at once
This is the bit that surprised me when I drew it. In the optimal stack there is no slack anywhere — every joint is simultaneously on the point of letting go:
| n = 8, cut | load CoM | support right edge | margin |
|---|---|---|---|
| bench | -0.000000 | 0.000000 | 4.2e-17 |
| 2 on 1 | 0.062500 | 0.062500 | 4.9e-17 |
| 5 on 4 | 0.317262 | 0.317262 | 5.6e-17 |
| 8 on 7 | 0.858929 | 0.858929 | 1.1e-16 |
Turn on balance in the demo and you can see it: a chevron at every joint,
each one sitting exactly on the right-hand edge of the brick beneath it, all
the way down. That is what the 0.000 readout means, and it is why the tower
looks like it should not be standing. It should not. It is at the boundary.
H(n)/2 is a theorem for single-file stacks (Paul B. Johnson, Leaning Tower of
Lire, 1955), so the search below is a check on my model rather than on the
mathematics. Perturb the optimum randomly, throw away everything that falls
over, and see how far the survivors get:
| bricks | ceiling H(n)/2 | tries | still standing | best of those | beat it? |
|---|---|---|---|---|---|
| 3 | 0.91667 | 60,000 | 6,992 | 0.91666 | no |
| 5 | 1.14167 | 60,000 | 1,399 | 1.14165 | no |
| 8 | 1.35893 | 60,000 | 122 | 1.35887 | no |
| 12 | 1.55161 | 60,000 | 3 | 1.50263 | no |
The survivor count collapsing from 6,992 to 3 is the knife edge showing up again from the other side: at twelve bricks, essentially no random nudge leaves the thing standing.
What is physics here and what isn’t
- Physics: the stability rule, and everything derived from it — the 0.500
trap,
1/(2k), thej/(2(j+1))holdback, the zero margins, which joint fails and where it pivots. Rigid bodies, point contacts, no friction needed because nothing here slides. - A modelling choice: one brick per level. That is the classical problem and
where
H(n)/2is the proven answer. Allow bricks side by side — real counterweights, bricks bridging two others — and the ceiling is no longer logarithmic butΘ(n^⅓)(Paterson & Zwick, Overhang, 2009), which is enormously better: the famous picture is a fat parabolic buttress, not a staircase. Supporting that means solving for contact forces across a network rather than checking one centre of mass per cut, and the demo does not do it. Every number above is the single-file answer, and the single-file answer is not the best a pile of bricks can do. - A game, not a claim: the toppling animation. A real stack going over is a multi-body collision problem; this one rotates the failing sub-stack about its pivot under gravity and stops caring after that. It is there so the fail state has a shape.
- A table, not physics: the eighteen-brick supply and the flags at 0.5, 1.0 and 1.5 lengths. The bench is 1.25 lengths wide, which matters only because an absurdly narrow bench would fail on its own edge first.
Playing it
Pointer or arrow keys to aim, click or Space to place, M to switch between
on top and underneath, R to rebuild. The ghost brick is green while
the placement stands and red while it doesn’t, so toppling is always a choice.
optimum draws the best stack your remaining bricks could reach; balance
draws the centre-of-mass chevrons. Three autopilots — greedy on top (0.500),
planned on top (1.748, and only because it was told the count in advance),
and greedy underneath (1.748, knowing nothing) — are the argument in about
nine seconds.
Reuse
src/overhang.mjs is framework-free and has no canvas in it. A stack is just
an array of left-edge positions, bottom brick first, in brick lengths, with the
bench edge at x = 0. isStable() / failingJoint() are the rule;
maxOnTop() and minOnTop() give the legal window for a new top brick in
closed form (every cut contributes one linear bound, and the answer is the
tightest); maxUnderShift() / slideUnder() are the downward move;
harmonicStack(), greedyTop(), greedyUnder() and plannedTop() are the
reference builders. bricksFor() inverts the price curve.
The demo is demo/index.html with its own copy of the module (ADR-0002) and a
window.__demo hook the rig drives. node scripts/measure.mjs prints every
table above and ends in a self-check. node scripts/screenshot-demo.mjs
regenerates the thumb and media and doubles as the smoke test — it drives the
real keyboard and pointer paths in a browser and asserts that four spacebars
underneath land on H(4)/2 = 1.0417, and that eighteen greedy bricks on top
land on 0.500.





