algorithms3 min
the biggest square that fits
divide and conquer, with a farm to split into equal squares: shrink the problem until the answer is obvious, why that turns out to be Euclid's algorithm, and why splitting in half beats one at a time.
a farmer has a plot of land, 1680m by 640m, and wants to split it into equal square plots, as big as possible. how big?
you could try sizes until one fits. or you could shrink the problem until it's easy, which is what divide and conquer means:
- find the base case: the simplest version, where the answer is obvious.
- shrink the problem until it becomes the base case.
the base case here: if one side is a multiple of the other, the answer is the shorter side. a 50m by 25m plot is two 25m squares.
#shrinking the farm
the 1680m by 640m plot isn't a base case. so cut out the biggest squares that fit, 640m each: two of them, and a 400m by 640m strip is left. and here's the trick: any square that splits that strip evenly also splits the whole farm evenly, since the rest is made of 640m squares built on the same side. so now the problem is just the strip.
the biggest square in the strip is 400m, leaving 400m by 240m. then 240m, leaving 240m by 160m. then 160m, leaving 160m by 80m, and 160 is a multiple of 80. base case. the answer is 80m: the farm splits into 168 squares of 80m by 80m.
that's Euclid's algorithm for the greatest common divisor, more than 2,000 years old. in code, the remainder does the cutting:
function gcd(a, b) {
return b === 0 ? a : gcd(b, a % b);
}
gcd(1680, 640); // 80#one at a time, or halves
the same recipe works on code. adding up a list: the base case is an empty list, which adds up to 0. to shrink it, take one item off:
function sum(list, i = 0) {
if (i === list.length) return 0;
return list[i] + sum(list, i + 1);
}it works, and it's as deep as the list is long: every item is another call waiting on the call stack. with 100,000 numbers, Node gave up: Maximum call stack size exceeded.
shrink it by half instead, and the depth drops to the number of times the list can be halved:
function sum(list, from = 0, to = list.length) {
if (to - from === 0) return 0;
if (to - from === 1) return list[from];
const mid = Math.floor((from + to) / 2);
return sum(list, from, mid) + sum(list, mid, to);
}the same 100,000 numbers, 18 calls deep. how you shrink the problem decides how deep you go. it's the idea behind merge sort and quicksort, which split a list in two and sort each half the same way.
the code from the first version of this post, with more exercises, is in my algorithms repo.