Divide and conquer and the master theorem
Solve divide-and-conquer recurrences with the master theorem.
Merge sort, quicksort, FFT, and matrix multiplication share a common shape: split a problem into smaller subproblems of the same kind, solve them recursively, combine the results[1]. The running time obeys a recurrence with a fixed form, and the master theorem reads off its closed-form solution[2].
The divide-and-conquer template
A divide-and-conquer algorithm has three steps at each level of recursion:
- Divide the problem of size into subproblems, each of size .
- Conquer by solving each subproblem recursively.
- Combine the results in work.
The running-time recurrence is
Merge sort fits with , , (merging). Binary search has , , .
The master theorem
Compare the per-level combine cost to the branching cost , the number of leaves in the recursion tree as a function of . The behavior splits into three cases.
Case 1 (leaves dominate). If for some , then
The work is concentrated at the leaves of the recursion tree.
Case 2 (balanced). If , every level does the same total work and there are levels:
Merge sort sits here: gives , matching , so .
Case 3 (root dominates). If for some (and a regularity condition for some and large ):
The work at the root dwarfs everything below.
Where the formula comes from
Imagine the recursion tree. The root has cost . It has children, each of cost . The next level has nodes of cost . At depth , there are nodes of cost . The tree has depth , with leaves.
Summing the costs by level:
When grows slowly (case 1), the sum is dominated by the leaf level. When matches the branching exactly (case 2), every level contributes the same and there are of them. When grows fast (case 3), the root dominates and the rest is a geometric series with declining terms. The three cases are the three ways this sum can behave.
Worked examples
- Merge sort: , case 2 with : .
- Binary search: , case 2 with : .
- Strassen's matrix multiplication: , case 1 with : , beating naive .
- Karatsuba multiplication: , case 1 with : .
Check it on arrays
import time
# Merge sort timed across input sizes: the ratio t(2n)/t(n) should approach 2,
# matching T(n) = n log n where the dominant factor doubles plus a tiny log term.
def merge_sort(a):
if len(a) <= 1: return a
m = len(a) // 2
L, R = merge_sort(a[:m]), merge_sort(a[m:])
out, i, j = [], 0, 0
while i < len(L) and j < len(R):
if L[i] <= R[j]: out.append(L[i]); i += 1
else: out.append(R[j]); j += 1
out.extend(L[i:]); out.extend(R[j:])
return out
import random
rng = random.Random(0)
sizes = [2**k for k in range(10, 16)]
times = []
for n in sizes:
data = [rng.randint(0, n) for _ in range(n)]
t0 = time.perf_counter()
merge_sort(data)
times.append(time.perf_counter() - t0)
for k, (n, t) in enumerate(zip(sizes, times)):
ratio = (times[k] / times[k-1]) if k > 0 else float("nan")
print(f"n={n:>6}: t={t:.4f}s t(n)/t(n/2) = {ratio:.2f} (n log n predicts 2*(log(2n)/log(n)))")
# Karatsuba multiplication (integer): T(n) = 3T(n/2) + O(n) -> n^{log2 3}
def karatsuba(x, y):
if x < 1 << 16 or y < 1 << 16: return x * y
n = max(x.bit_length(), y.bit_length())
half = n // 2
high_x, low_x = x >> half, x & ((1 << half) - 1)
high_y, low_y = y >> half, y & ((1 << half) - 1)
z2 = karatsuba(high_x, high_y)
z0 = karatsuba(low_x, low_y)
z1 = karatsuba(high_x + low_x, high_y + low_y) - z2 - z0
return (z2 << (2 * half)) + (z1 << half) + z0
a = (1 << 1000) - 1; b = (1 << 1000) + 1
print("karatsuba matches *:", karatsuba(a, b) == a * b)The merge-sort time ratios approach 2 as predicted by case 2 of the master theorem, and Karatsuba matches Python's built-in big-integer multiplication.
Exercises
Exercise 1. Solve with the master theorem. Identify , , , the branching exponent , the case, and the closed form.
Solution
Step 1. Read off the recurrence: subproblems, each of size with , and combine cost .
Step 2. Compute the branching exponent: , so the leaf count grows like .
Step 3. Compare to . Since for any , the leaves dominate: this is case 1.
Step 4. Case 1 gives .
Exercise 2. Derive the cost of (merge sort) directly by summing the recursion tree, without quoting the master theorem, and confirm it equals .
Solution
Step 1. At depth there are nodes, each of size , each costing . The total work at level is , independent of .
Step 2. The tree has depth (halving from down to size 1), so there are levels, each contributing :
Step 3. Dropping the constant and lower-order term, . Every level doing equal work is the signature of case 2.
Exercise 3 (code). Empirically estimate the branching exponent of Karatsuba. Count the number of recursive base-case multiplications Karatsuba performs on two -bit integers for , and check the count grows like (the exponent in disguise, since ).
Solution
import numpy as np
calls = 0
def karatsuba(x, y, base_bits):
global calls
if x.bit_length() <= base_bits or y.bit_length() <= base_bits:
calls += 1
return x * y
half = max(x.bit_length(), y.bit_length()) // 2
hx, lx = x >> half, x & ((1 << half) - 1)
hy, ly = y >> half, y & ((1 << half) - 1)
z2 = karatsuba(hx, hy, base_bits)
z0 = karatsuba(lx, ly, base_bits)
z1 = karatsuba(hx + lx, hy + ly, base_bits) - z2 - z0
return (z2 << (2 * half)) + (z1 << half) + z0
counts = []
for k in range(2, 8): # n = 2^k bits, split down to 1-bit base
calls = 0
x = (1 << (2**k)) - 1 # all-ones n-bit number
karatsuba(x, x, 1)
counts.append(calls)
ratios = [counts[i] / counts[i-1] for i in range(1, len(counts))]
# Each doubling of n triples the base-case count: three subproblems per level.
assert all(2.6 <= r <= 3.4 for r in ratios)
print("base-case counts:", counts)
print("successive ratios:", [round(r, 2) for r in ratios])Each doubling of the bit length roughly triples the number of base multiplies, the three-way branching that produces the runtime rather than .
The next lesson moves to a different combinatorial pattern: dynamic programming.
References
- [1]Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2009). Introduction to Algorithms (3rd ed.). MIT Press. The divide-and-conquer paradigm (Ch. 4).↩
- [2]Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2009). Introduction to Algorithms (3rd ed.). MIT Press. The master method for solving recurrences (§4.5).↩