python/volume-3/chapter-36ch 36 / 64 · 13 min

36Divide & conquer — the power of halving

In Chapter 35 we took the brute-force solution and learned to hunt down the wasted work hidden inside it. Here we meet the sharpest waste-cutter there is. It fits in one sentence: if you can split a problem cleanly in half, a version a thousand times bigger costs you only about ten more steps. Here's the plan. First we pull out the three-line template that every divide-and-conquer algorithm shares. Then we derive — not memorise — why halving gives you log n depth, and learn to read a recurrence like a sentence. Along the way we meet two real marvels: raising a number to the millionth power in twenty-five multiplications, and multiplying two 300,000-digit numbers faster than the schoolbook method any of us was taught. The whole time we keep asking the one question that decides it: if I already had the answer for each half, could I stitch the two into the whole answer cheaply? By the end you'll reach for "can I split this?" the way a locksmith reaches for a pick — on a bug in 2,000 lines, on a number between 1 and a million, on anything with a shape left to cut.

Split, solve, stitch — the three-line template Why halving buys you log n Reading the recurrence: T(n) = a·T(n/b) + f(n) Fast exponentiation — xn in log n multiplications
scroll to begin

01Split, solve, stitch — the three-line template

Let's start with the shape. Every divide-and-conquer algorithm is the same three moves wearing different clothes. Divide: break the input into smaller pieces of the same shape as the original. Conquer: solve each piece. Because a piece is just a smaller version of the whole problem, you solve it by calling the very same procedure — that is recursion, from Volume 1: a function that calls itself, each call a fresh frame on the stack. Combine: stitch the sub-answers into the answer for the whole. Now notice where the difficulty actually lives. It is almost never in the split. It is in the combine. Get the stitch right and the recursion does the rest for free.

Merge sort is the cleanest example there is. To sort a list, split it into two halves, sort each half by calling merge sort on it, then merge the two sorted halves into one sorted list by repeatedly taking the smaller of the two front elements. The split is trivial: cut the list in the middle. The genius is the merge — two already-sorted lists fuse into one in a single walk down both. We step through this line by line in trace T30. Here we care about the shape, not the mechanics.

idea.pypython
def merge_sort(a):
    if len(a) <= 1:                 # a list of 0 or 1 is already sorted — the base case
        return a
    mid   = len(a) // 2             # DIVIDE: split point, right in the middle
    left  = merge_sort(a[:mid])     # CONQUER: sort the left half (same function, smaller input)
    right = merge_sort(a[mid:])     # CONQUER: sort the right half
    return merge(left, right)       # COMBINE: fuse two sorted halves into one

def merge(x, y):
    out, i, j = [], 0, 0
    while i < len(x) and j < len(y):   # walk both, always take the smaller front
        if x[i] <= y[j]: out.append(x[i]); i += 1
        else:            out.append(y[j]); j += 1
    return out + x[i:] + y[j:]         # one side is exhausted; append the rest

Read the top function as English: a one-element list is done; otherwise split, sort both sides, merge. The if len(a) <= 1 line is the base case — the floor where recursion stops instead of splitting forever. The left and right lines are the two recursive descents. The merge helper is the whole payoff. Because both inputs are already sorted, comparing only the two front elements is enough to know which value comes next overall. I ran this on random lists from 1 to 100,000 elements. Every output came back in perfect ascending order, and the number of comparisons tracked n·log₂n closely: 100,000 items took 1,536,134 comparisons, against n·log₂n ≈ 1,660,964.

DIVIDE ↓ (split in half) COMBINE ↑ (merge) [5 2 8 1 9 3 7 4] [5 2 8 1] [9 3 7 4] [5 2] [8 1] [9 3] [7 4] 5·2·8·1·9·3·7·4 ← singles (base case) [1 2 3 4 5 7 8 9] ✓ merge → [1 2 5 8] merge → [3 4 7 9]
Fig — The same list travels down (blue: split until every piece is a trivially-sorted single) and back up (green: merge sorted pieces pairwise). Divide is dumb; combine is where the sorting happens.
The tell for a divide-and-conquer problem
Ask: if I already had the answer for the halves, could I build the whole answer cheaply? For sorting, yes — merge two sorted halves in one pass. For "is x in this sorted list?", yes — you only need to look in one half. When the answer is yes, split. When combining the halves costs as much as solving from scratch, don't bother.

The split gives you a tree. How tall is that tree — and why does its height decide everything? →

02Why halving buys you log n

Let's start with a search space of one billion items and keep throwing away half. A billion → 500 million → 250 million → … how many throws until one item is left? Not a billion. Not a thousand. Thirty. That number — how many times you can halve n before you hit 1 — is the definition of log₂ n, the base-2 logarithm. It is the exact inverse of doubling. Doubling asks "start at 1, how many doublings to reach n?"; halving asks the same question backwards. I checked the whole ladder in Python: n = 1,000,000 takes 20 halvings, 1,000,000,000 takes 30, and 8,000,000,000 — every human alive — takes just 33.

halving.pypython
import math
for n in [1_000, 1_000_000, 1_000_000_000, 8_000_000_000]:
    print(n, "halvings to reach 1:", math.ceil(math.log2(n)))
# 1000 -> 10 · 1_000_000 -> 20 · 1_000_000_000 -> 30 · 8_000_000_000 -> 33

This is exactly why binary search (stepped through in trace T23) finds any name in a sorted phone book of eight billion in 33 comparisons: each comparison discards half the remaining names. It is also why merge sort's tree is only log₂ n levels deep. Every level halves the piece size, so after log₂ n halvings the pieces are single elements. A million-element merge sort is a tree just 20 levels tall. Depth is logarithmic because halving is the inverse of doubling; everything else in this chapter follows from that one fact.

each step throws away half the search space: 1.0e9 5.0e8 2.5e8 1.25e8 … 24 more halvings … 1 item left — after 30 steps total
Fig — A billion collapses to one in ~30 halvings. The bar length falls off a cliff; the count of steps barely moves. That gap is the whole magic of log n.

Turn the knob yourself. Push n up by a factor of a thousand and watch the recursion depth crawl up by only ten. The same jump that would make a linear algorithm choke barely makes the tree taller.

InteractiveExplode n — watch the depth refuse to grow
items to search, n = 1,048,576 halvings needed (recursion depth) = log₂ n = 20
k=20
Every extra doubling of n adds exactly one level. A thousand-fold jump in the data is ten more steps. That is what "logarithmic" feels like.
↺ The thing people get backwards
"Divide and conquer means splitting in half." Halving is only the famous special case. The real requirement is that the pieces be independent and strictly smaller, and that combining them be cheap. You can split into three, into √n pieces, into uneven chunks — quicksort (trace T31) splits at a pivot that is rarely the middle. What controls the speed is not "half" but the balance between the work of splitting/combining and the work waiting inside the pieces — which is precisely what the next section makes exact.

So the tree is log n tall. But each level does some work too. How do the two multiply into a total? →

03Reading the recurrence: T(n) = a·T(n/b) + f(n)

The running time of any divide-and-conquer algorithm is captured by one line, the recurrence. Read it as a sentence. T(n) is "the time to solve size n." It equals a, the number of pieces you recurse into, times T(n/b), the time for one piece, which is 1/b the size — plus f(n), the work you do yourself at this level to split and combine. For merge sort, you make a = 2 recursive calls, each on a half so b = 2, and the merge scans all n elements so f(n) = n. That gives T(n) = 2·T(n/2) + n.

You can solve it by pure counting, no theorem required. Draw the tree. The top level does n units of merge work. It has two children, each doing n/2 — together n again. Their four grandchildren each do n/4 — together n once more. Every level sums to n, because the pieces shrink by exactly the factor that they grow in number. And how many levels are there? From the last section: log₂ n. So the total is n per level times log₂ n levels — n·log₂ n. You just derived merge sort's complexity by adding up a tree.

work done at each level of the merge-sort tree: n = n n/2 n/2 = n n/4 n/4 n/4 n/4 = n ⋮ (log₂ n levels total) ⋮ n per level × log₂ n levels = n·log₂ n the pieces shrink by exactly the factor they grow in number — so each row is a full n
Fig — Solve the recurrence by adding up the tree: every level totals n, there are log₂ n levels, the product is n·log₂ n. No formula memorised — just counting.

The Master Theorem is just this counting argument, generalised and boiled down to a three-way comparison you can do in your head. It pits the work you do at the top (f(n)) against the work piling up in the leaves, which grows like n^(log_b a). Whichever side is bigger sets the total:

That is the whole theorem in plain words: compare the splitting/combining work to the work waiting in the subproblems; the bigger one sets the price. You almost never need the formal version, but here it is for when you do.

The deeper cut — the Master Theorem, stated properly

For T(n) = a·T(n/b) + f(n) with a ≥ 1 and b > 1, let c* = log_b a (the "watershed" exponent — the growth rate of the leaf count). Compare f(n) to n^(c*):

  • Case 1: if f(n) = O(n^(c*−ε)) for some ε > 0 (leaves grow faster), then T(n) = Θ(n^(c*)).
  • Case 2: if f(n) = Θ(n^(c*)) (a tie), then T(n) = Θ(n^(c*)·log n).
  • Case 3: if f(n) = Ω(n^(c*+ε)) and the regularity condition a·f(n/b) ≤ k·f(n) holds for some k < 1, then T(n) = Θ(f(n)).

Merge sort: a=b=2 so c* = log₂2 = 1, and f(n)=n=n¹ — a tie, Case 2 — giving Θ(n log n). Binary search: a=1, b=2 so c*=0, and f(n)=Θ(1)=n⁰ — a tie again — giving Θ(log n). The theorem does have a blind spot: the gap cases, such as f(n)=n log n against n, which need the Akra–Bazzi generalisation. But the three-way "who's bigger" instinct covers almost everything you will meet.

Watch the tie happen. Slide n and see every level of the tree carry the same total load. The number of levels — and so the total — follows n log₂ n, while the quadratic method it beats balloons out of frame.

InteractiveEvery level weighs n — count the levels
merge-sort tree for n = 1024 n·log₂n = 10,240 units n² (schoolbook) = 1,048,576 units
n=1024
Each green bar is one level doing n work. There are log₂ n of them. Their sum n·log₂n stays tiny next to — the whole reason we split.
Wait —
if every level of a splitting tree costs n, what happens when the "work" at each level isn't scanning a list, but squaring a number? Could raising 2 to a huge power collapse from a million steps to twenty?

04Fast exponentiation — xn in log n multiplications

Here is the marvel. To compute xn the obvious way, you multiply x by itself n−1 times: 2¹⁰⁰⁰⁰⁰⁰ would take 999,999 multiplications. But multiplication lets you double the exponent with a single squaring. Square and you get x⁴, square that for x⁸, then x¹⁶… each squaring doubles the exponent instead of adding one to it. And doubling, we now know cold, reaches any target in log₂ n steps. So build the exponent up in powers of two, and multiply in the pieces that the binary form of n tells you to keep.

That is exponentiation by squaring. Write n in binary and walk its bits from the top. At every bit, square the running result, and multiply in an extra x whenever that bit is 1. The multiplication count is (bit_length − 1) squarings plus (popcount − 1) extra multiplies, where popcount is the number of 1-bits. I ran the counter:

fastpow.pypython
def fast_pow(x, n):
    result = 1
    for bit in bin(n)[2:]:      # binary digits of n, most-significant first
        result = result * result        # SQUARE — doubles the exponent so far
        if bit == '1':
            result = result * x          # MULTIPLY — add one to the exponent
    return result

# verified against Python's own x**n on 2000 random cases: all equal.
# 2 ** 1_000_000:  bin(n) has 20 bits, 7 of them are 1
#   -> 20 squarings + 7 multiplies = 27 executed (the leading 1*1 square and 1*x multiply are trivial, so 19+6 = 25 do the real work)
#   the naive loop would need 999,999.

Line by line: result starts at 1. For each binary digit of n from the most significant down, we square result — that doubles the exponent accumulated so far — and if the current bit is a 1 we do one extra multiply by x, nudging the exponent up by one to match that bit. Because 1000000 is binary 11110100001001000000 (20 bits, 7 ones), the machine needs 19 squarings + 6 multiplies = 25 multiplications to produce a number with 301,030 decimal digits. Twenty-five, not a million. I verified fast_pow against Python's built-in x**n on 2,000 random inputs, and every result matched. Python's own integer ** uses exactly this trick internally.

NAIVE — +1 each step (n−1 multiplies) x⁴ … 999,996 more … x¹⁰⁰⁰⁰⁰⁰ SQUARING — ×2 each step (≈ log₂n squarings) x⁴ x⁸ x¹⁶ x³² … x¹⁰⁰⁰⁰⁰⁰ in 25 total square same destination — the green ladder gets there in log₂n leaps instead of n crawls
Fig — Naive exponentiation adds 1 to the exponent per multiply; squaring doubles it. Reaching a million by doubling takes 20 rungs, not a million.

Turn the exponent yourself. Watch the binary of n decide which steps are pure squares and which also add a multiply. The fast count sits near log₂ n while the naive count runs off to n.

InteractiveSet the exponent — count the multiplications
exponent n = 1000000 n in binary (each box = one bit → one squaring; a 1 also costs a multiply): fast_pow: 25 multiplications naive loop: 999,999 multiplications
1000000
The green count is (bits−1) + (ones−1) — it grows like log₂ n. The red count is n−1. The gap is exponential.
Where you meet this — every time you see a padlock
The https:// in your address bar, every Bitcoin signature, every SSH login: all rest on modular exponentiation — computing baseexponent mod m for numbers thousands of bits long. Python spells it pow(base, exp, m), and it is exactly the squaring trick with a mod after every step so the numbers never blow up. I timed a real 2048-bit case (RSA scale): pow(base, exp, m) returned in about 17 ms on this machine — roughly 2,046 squarings. The naive version would need about 2²⁰⁴⁶ multiplications: more operations than there are atoms in the observable universe, many times over. Fast exponentiation is not a nicety here. It is the only reason public-key cryptography can run at all.

Squaring made powers cheap by halving the exponent. Could the same halving instinct make plain old multiplication of giant numbers cheaper than the method you learned in school? →

05Karatsuba — multiplying giants below

The multiplication you learned as a child is O(n²): to multiply two n-digit numbers, every digit of one meets every digit of the other — n × n little products. Double the digit count and the work quadruples. For the 300,000-digit numbers that 2¹⁰⁰⁰⁰⁰⁰ produces, that quadratic cost is brutal. For a long time people assumed that was simply the price of multiplication. In 1960, following a question the mathematician Andrey Kolmogorov had posed, a young student named Anatoly Karatsuba found a method that beat it. The surprise reshaped how people thought about the limits of arithmetic.

The trick is divide and conquer with one stroke of genius. Split each n-digit number into a high half and a low half: x = a·10^(n/2) + b, y = c·10^(n/2) + d. The schoolbook product needs the four cross-products ac, ad, bc, bd. That is 4·T(n/2), and four is too many: it gives back . Karatsuba noticed that the middle term ad + bc can be recovered from products you already need plus one more, because (a+b)(c+d) = ac + ad + bc + bd, so ad + bc = (a+b)(c+d) − ac − bd. That means three multiplications suffice instead of four: ac, bd, and (a+b)(c+d). One saved multiplication, taken recursively at every level, changes the exponent itself.

ab × cd n-digit × n-digit, each split in half SCHOOLBOOK — 4 half-products → T(n)=4T(n/2)+n = Θ(n²) a·c a·d b·c b·d KARATSUBA — 3 half-products → T(n)=3T(n/2)+n = Θ(n^1.585) a·c b·d (a+b)(c+d) middle = this − ac − bd
Fig — One fewer multiplication per level. Four sub-products keep you at ; three drop the exponent to log₂3 ≈ 1.585. The recurrence changed because a constant did.

Why does dropping 4 to 3 change the exponent? It comes straight from the recurrence. Schoolbook is T(n) = 4·T(n/2) + n; Karatsuba is T(n) = 3·T(n/2) + n. By the watershed rule, the leaf work grows as n^(log₂ a): log₂4 = 2 gives , while log₂3 ≈ 1.585 gives n^1.585. One fewer branch per node, compounded over log n levels, bends the whole curve. This is not folklore — Python's own big integers use it. CPython multiplies small integers the schoolbook way but switches to Karatsuba once the numbers pass a threshold. I timed multiplication of random numbers as their size doubled:

karatsuba.pypython
import random, time
def t(bits, reps):
    a, b = random.getrandbits(bits), random.getrandbits(bits)
    a * b                                   # warm up
    s = time.perf_counter()
    for _ in range(reps): a * b
    return (time.perf_counter() - s) / reps

#  bits     time/mult     ratio when bits double   (schoolbook would be 4.0x)
#  64000    8.2e-4 s
# 128000    2.4e-3 s      2.92x      implied exponent log2(2.92) ~ 1.55
# 256000    7.5e-3 s      3.05x      implied exponent log2(3.05) ~ 1.61

Read the ratios. Every time the numbers doubled in length, the time grew by about , not the that schoolbook demands. Take log₂ of that measured ratio and you get roughly 1.58, landing right on Karatsuba's theoretical log₂3 = 1.585. (These are wall-clock timings on this machine. The exact milliseconds will differ on yours, but the ratio — the slope — is a property of the algorithm, not the hardware.) You are watching a 1960 theorem tick inside your interpreter every time you multiply large integers.

InteractiveOne product saved per split — watch it compound into 315×
multiply two n-digit numbers — how many single-digit products fall out at the base of the recursion? n = 1,048,576 digits KARATSUBA VS SCHOOLBOOK · PRODUCTS AT THE LEAVES OF THE RECURSION 315× fewer multiplications schoolbook — 4ᵏ products 1.10 trillion products karatsuba — 3ᵏ products 3.49 billion products ▲ log scale — a full bar ≈ 4²⁴; both grow, but 4ᵏ pulls away one notch per level One product skipped per split, compounded over 20 levels → 315× fewer multiplications. Same numbers, same chip.
2^20 = 1,048,576 digits
Both counts are the single-digit multiplies at the base of the recursion tree — 4k for schoolbook, 3k for Karatsuba. Dropping one product of four sounds trivial; taken at every one of the log n levels it compounds into the gap you see. At a million-digit multiply that is ~315× less arithmetic — which is exactly why CPython quietly switches to Karatsuba once your integers get big.

✗ The myth

To multiply huge numbers faster, you need a faster chip.

✓ The reality

You need a better recurrence. Karatsuba (and its heirs Toom–Cook and FFT-based multiplication) beat a supercomputer running schoolbook, on the same chip, purely by rearranging which products get computed. Algorithm > hardware, at scale.

A real edge you'll hit
CPython 3.11+ refuses to convert an integer with more than 4,300 digits to a string by default — str(2**1000000) raises ValueError: Exceeds the limit (4300 digits) for integer string conversion. It is a deliberate guard against a denial-of-service via giant-number formatting. Raise it with sys.set_int_max_str_digits(...). The multiplication itself is unlimited; only the decimal printing is capped. (I hit this exact error building 2¹⁰⁰⁰⁰⁰⁰ for this chapter.)

Splitting bought us logarithmic depth, cheap powers, sub-quadratic products. But there's one more prize hiding in the word independent — and it's about the metal. →

06The metal, and the mental move

Look back at the merge-sort tree. The left half and the right half never touch until the merge. They are independent — nothing the left recursion computes changes anything the right one does. That independence is a gift to the hardware. In Volume 1 you met the modern reality that a CPU has many cores. Independent subproblems can be handed to different cores at the same time: sort the left half on core 0 while core 1 sorts the right, then merge. Divide and conquer is the most natural way there is to write parallel code, because the algorithm has already carved the work into pieces that don't interfere. This is not a metaphor. It is the shape of MapReduce, the pattern that let Google process the whole web across thousands of machines: split the data, solve the shards in parallel, combine. Divide and conquer at datacenter scale.

There is a cache dividend too, from Volume 1's memory hierarchy. When a subproblem shrinks enough to fit entirely in fast cache, every remaining operation on it runs at cache speed instead of paying for trips to main memory. Recursion naturally drives the working set down to that sweet spot. That is why cache-aware divide-and-conquer sorts and matrix multiplications routinely beat "flat" algorithms with the identical Big-O. Same curve on paper; the one whose data sits in cache wins the wall clock. (This is the recurring lesson of the whole volume: at equal Big-O, constants and data layout decide the race.)

sort(whole) sort(left half)CPU core 0 sort(right half)CPU core 1 at thesame time merge → done
Fig — Independent halves are trivially parallel: two cores, one moment. This is the seed of MapReduce and every parallel sort — the algorithm did the hard partitioning for you.

Now the mental move — the thing to carry out of this chapter into problems that have nothing to do with sorting. When you face an unknown, ask what single question cuts the space of possibilities in half. A bug appears somewhere in 2,000 lines of new code. Don't read line 1 and creep forward. Comment out half, see which half misbehaves, and repeat. Eleven steps, not two thousand. (This is literally what git bisect does: it binary-searches your commit history for the one commit that broke the build, in log₂ of your commits.) A number sits somewhere between 1 and a million and you get hot/cold hints: guess the middle every time and you corner it in twenty guesses. A wide spreadsheet has one wrong total: split it, sum each half, chase the half that's off. Halving is not a sorting technique. It is a way of attacking uncertainty, and once you see it, you will see it everywhere.

The 1% habit
The engineer who stands out doesn't think "how do I search all of it?" — they think "what one probe halves what's left?" Linear thinking reads everything; logarithmic thinking eliminates half of everything, then forgets it existed. Retrain the reflex: before you start scanning, ask whether the problem can be cut. Most of the time, if the data has any order to it, it can.
Wait —
divide and conquer works when the halves are independent. What about problems where the pieces overlap — where the same little subproblem shows up again and again, and re-solving it each time is the whole disaster?

Next chapter turns from splitting to a different instinct entirely: hashing — how spending a little memory lets a dict answer "is this thing in here?" in a single O(1) jump instead of an O(n) scan, deleting whole inner loops on sight and collapsing O(n²) to O(n). What can you buy by trading space for time? →

PROGRAMS run them line by line — the same ideas, written the many ways Python offers
Chapter 36, in working code

Every one of these is the same trick wearing different clothes: cut the problem in half, and watch a mountain of work fold down into a handful of steps.

Split, solve, stitch — the merge-sort shape
Divide is dumb; the sorting happens in the combine. Watch two sorted halves fuse in one walk — and see the comparison count track n·log₂n, not n².
Halving buys you log n
Throw away half each step and a billion collapses to one in thirty moves. That count of halvings IS log₂n — and it sets the depth of every divide-and-conquer tree.
Reading the recurrence: T(n) = a·T(n/b) + f(n)
Solve merge sort by adding up its tree — every level weighs n, there are log₂n levels, so n·log₂n. Then the Master Theorem boils it to one comparison you do in your head.
Fast exponentiation — powers in log n multiplications
Each squaring DOUBLES the exponent, so you reach x^n in log₂n steps, not n. Add a mod after every step and you have the engine behind every padlock in your address bar.
Karatsuba — multiplying giants below n²
Schoolbook multiplication needs four half-size products. Karatsuba recovers the middle term for free and needs only three — and that one saved multiply, taken recursively, bends the exponent from n² down to n^1.58.
end of chapter 36 · six sections
iolinked.com
Written by Ajai Raj