Let’s state the problem: place a b×a∈N
grid (rows × columns) into a w×h∈R+
rectangle which fits n∈N
squares such that the side length of the squares s∈R+
is maximized and the grid does not overflow the rectangle. Since the
side length is maximized, the grid will fit in the rectangle’s full
width or height, or both if a perfect fit is possible. Formally:
sw∈Sw
is a fit‑width solution, sh∈Sh
a fit‑height solution. The final solution s
is the desired maximal side length. Our target is to build an algorithm
to find s.
Observe that for any fixed b
we can always take a=⌈bn⌉
to obtain a grid which fits n
squares, since clearly n≤ab.
For this grid (a,b)
let s′=min{aw,bh}
its corresponding side length; it’s easy to see that s′∈Sw∪Sh.
We can just test all 1≤b≤n
and pick the maximum s′
to obtain the solution s:
functionfillGrid(n, w, h) {
let s = 0;
for (let b = 1; b <= n; b++) {
s = Math.max(s, Math.min(w / Math.ceil(n / b), h / b));
}
return s;
}
The asymptotic complexity is O(n).
To improve this, notice that if a≤n
there are obviously O(n)
distinct values of a.
If a>n:
⌈bn⌉>n⟹bn+1>n⟹b<n−1n=n+1+n−11.
Therefore a>n⟹b≤O(n),
so in all cases there are O(n)
distinct a
values. Clever incrementing of b
could then achieve complexity O(n).
But for my grid with n=1020
squares this isn’t good enough – can we do better?
The solution statement as it stands juggles a lot of variables. We
can rewrite the grid dimension constraints using only a,b,w,h:
aw
is the column count of the fit‑width grid, bh
the row count of the fit‑height grid. They are minimized since
the side length is inversely proportional. The side length is a trivial
result of the grid dimensions; only the algorithm for finding aw
and bh
remains to be developed.
Let’s take a look at a fit‑width grid with dimensions (aw,bw)∈N2.bw
can be abstracted away:
Since the second comparison term is non‑increasing in a,
respectively in b,
it follows that every a>aw
and b>bh
would satisfy the solution condition. The algorithm idea is thus the
following: starting from an initial guess a0≤aw
(and b0≤bh)
test every value until the solution condition holds, i.e. loop
while:
Only the initial guesses a0
and b0
remain to be made. From the solution condition of aw:
aw≥r⌈awn⌉⟹aw2≥rn⟹aw≥rn.
Since aw∈N
it can therefore be no smaller than a0=⌈a∗⌉,
with a∗:=rn.
Likewise, bw≥⌈b∗⌉,
with b∗:=rn.
Using the loop condition above, an algorithm can finally be built:
functionfillGrid(n, w, h) {
const r = w / h
let a = Math.ceil(Math.sqrt(r * n))
for (; a < r * Math.ceil(n / a); a++)
let b = Math.ceil(Math.sqrt(n / r))
for (; r * b < Math.ceil(n / b); b++)
returnMath.max(w / a, h / b)
}
To analyze the runtime complexity, observe that since a0≤a<r⌈an⌉≤r⌈a0n⌉,a
is incremented at most ⌈r⌈a0n⌉−a0⌉
times. An upper bound on the iteration count is:
This means at most ⌊r⌋+1
iterations. This bound is tight: for n=33,r=8.31
the fit‑width loop does 1=⌊r⌋+1
iteration, reaching the upper bound. Similarly for the second loop we
get a tight upper bound of ⌊r1⌋+1
iterations. The runtime is therefore O(R)
with R=max{r,r1}.
One could binary search over [a0,a0+⌊r⌋+1]
and [b0,b0+⌊r1⌋+1]
to reduce complexity to O(logR).
My n=1020
grid is a piece of cake now, but my r=2↑↑6
grid still requires around 1019700
iterations. There are 1080
atoms in the observable universe. Can we do better?
Let’s return to the initial algorithm. It iterates through all
solutions (a=⌈bn⌉,b)
for 1≤b≤n.
It must find the optimal s:
any other a′>a
is worse, as at each step the side length s′=min{a′w,bh}≤min{aw,bh},
and any other b′>n
does not help, since it would correspond to a solution (1,b′)
with s′=min{w,b′h}≤min{w,nh},
with the latter value being the side length of (1,n),
a solution that’s tested.
Consider then what happens when b≤b∗=rn:
b≤rn⟹rb≤bn≤a⟹bh≥aw⟹s′=⌈bn⌉w.
Every solution tested is fit‑width and to maximize the side length we
must take b=⌊b∗⌋.
Consider now b≥b∗:
b≥rn⟹rb≥bn⟹(1)rb>⌈bn⌉∨(2)⌈bn⌉≥rb.
In case (1)
the solution (a=⌈bn⌉,b)
is clearly fit‑height, so s′=bh
is maximized when b≥b∗
by b=⌈b∗⌉.
To tackle case (2)
we’ll assume that r≥1;
it then follows:
Under both assumptions about b
and r
we have b∗≤b<b∗+1,
thus, simply by definition of the ceiling function, b=⌈b∗⌉.
Hence when r≥1
only b∈{⌊b∗⌋,⌈b∗⌉}
can ever maximize the side length.
To tackle r≤1,
notice the symmetry of the problem with respect to r.
Taking r′:=r1
just rotates the original rectangle by 90∘
– the side length of the squares remains the same, the number of rows
and columns swap. Using the result above, for r≤1
the optimal grid (a,b)
must have a∈{⌊a∗⌋,⌈a∗⌉},
with a∗=rn=r′n,
and (a,b)=(b′,a′),
where (a′,b′)
is the optimal grid for r′.
With this we have exhaustively covered the input domain. The O(1)
algorithm we’ve all been waiting for is…
functionfillGrid(n, w, h) {
const r = w / h;
if (r < 1) returnfillGrid(n, h, w);
consts = (b) => Math.min(w / Math.ceil(n / b), h / b);
const bs = Math.sqrt(n / r);
returnMath.max(s(Math.floor(bs)), s(Math.ceil(bs)));
}
If you’ve found the proofs hard to follow, use a graph: plot the
functions xn
and rx
and the solution points (a,b)
and interpret geometrically. Here’s Desmos.
For additional context, this problem is a particular case of
integer programming. There exist general algorithms to solve
such problems but the best time complexity they can achieve here is
still O(logR).
Lastly, a challenge! Prove that when a fit‑width solution (aw,bw)
is the best, i.e. sw>sh
for all (ah,bh)
fit‑height, there is no b=bw
such that (aw,b)
is fit‑width. Check Uniqueness of fit‑width solution if you get stuck.