Divide and Conquer
Split a problem into independent subproblems of the same shape, solve them recursively, and combine the answers; the Master theorem tells you whether the split pays off.
Overview
Divide and conquer solves an instance of size n by (1) dividing it into a subproblems of size n/b, (2) conquering each recursively, and (3) combining their results in f(n) time. The running time obeys the recurrence T(n) = a · T(n/b) + f(n), and the whole art lies in making a, b, and f(n) such that the recurrence beats the obvious algorithm.
Canonical instances: Merge Sort (a=2, b=2, f=O(n) → O(n log n)), Binary Search (a=1, b=2, f=O(1) → O(log n)), Quick Sort (expected 2T(n/2) + O(n) after a random pivot), Karatsuba multiplication (a=3, b=2, f=O(n) → O(n^1.585)), Strassen (a=7, b=2 → O(n^2.81)), and the Closest Pair of Points of points (2T(n/2) + O(n) after a sort).
The Master theorem compares f(n) against n^(log_b a), the total work of the leaves. Three cases: if the leaves dominate (f(n) polynomially smaller) the answer is Θ(n^(log_b a)); if they tie, Θ(n^(log_b a) · log n); if the combine step dominates (f(n) polynomially larger and regular), Θ(f(n)).
D&C differs from Dynamic Programming in one word: independent. When subproblems overlap, recomputing them is exponential and Memoization (Top-Down DP) is required; when they are disjoint, plain recursion is optimal and the recursion tree is the whole story.
Intuition
A mental model before the formal terms.
Sorting a deck of 1,000 cards alone is slow; splitting it into two piles, handing each to a friend who does the same recursively, and then zipping two sorted piles together takes log₂ 1000 ≈ 10 rounds of zipping, each touching every card once. That is 10 × 1000 operations instead of 1000 × 1000.
Picture the recursion tree: the root does f(n) work, its a children each do f(n/b), and so on for log_b n levels. Sum the work per level. If each level does the same work (a · f(n/b) = f(n), as in merge sort), the total is f(n) · log n. If the work shrinks per level (binary search: one child, constant work), the root dominates. If it grows per level (Karatsuba: three children but only half-size inputs), the leaves dominate and the exponent log_b a is the answer.
Karatsuba is the surprising one: multiplying two n-digit numbers naively needs n² digit products. Splitting each into halves gives 4 half-size products — no gain (4T(n/2) + O(n) = O(n²)). Karatsuba's trick computes the middle term from the other two with three multiplications instead of four, and log₂ 3 ≈ 1.585 < 2. Reducing a by one changes the exponent.
How it works
- Divide: choose a split. Usually by index (
mid = (lo + hi) / 2), sometimes by value (quick sort partitions around a pivot), sometimes by structure (left/right subtree, x-coordinate median). - Conquer: recurse on each part. Base case: an instance small enough to solve directly (size 0 or 1, or a small constant where insertion sort beats recursion).
- Combine: merge results. This is where the algorithm-specific insight lives — merging sorted halves, checking the strip across the split line in closest-pair, adding shifted partial products in Karatsuba.
- Analyze with the recurrence
T(n) = a T(n/b) + f(n): identifya,b,f(n); comparef(n)ton^(log_b a); read off the Master theorem case. If the split is unbalanced (quick sort worst caseT(n-1) + O(n)), the theorem does not apply and the tree isO(n)deep.
Why it works
Correctness is by strong induction on n: the base case is solved directly; assuming all smaller instances are solved correctly, the combine step produces a correct answer for size n. The combine step must be a true function of subresults — this is exactly why the subproblems must be independent.
Efficiency comes from balance. Splitting into equal halves gives depth log_b n; if every level does O(n) work the total is O(n log n), and log n is tiny (≈ 30 for n = 10^9). An unbalanced split (sizes 1 and n−1) gives depth n and destroys the gain, which is why Quick Sort needs a random or median-of-three pivot.
Master theorem, informally: count leaves. a^(log_b n) = n^(log_b a) leaves each doing constant work. If f(n) grows slower than that, the leaves win; faster, the root wins; equal, every level ties and you pay log n levels.
Recognition
How to tell a problem wants this.
- The problem on the whole input equals a cheap function of the problem on two halves ("count inversions", "maximum subarray", "closest pair", "skyline").
- Sorted input or a monotone property lets you discard half the input per step (
O(log n)— Binary Search and friends). - Constraints of
n ≤ 10^5..10^6with a quadratic brute force — you needn log n, and the halves are independent (if they overlap, think Dynamic Programming). - Arithmetic on huge numbers or matrices where the naive algorithm is
n²orn³— Karatsuba/Strassen-style splits reduce the exponent.
Interactive visualization
Play, step, change the input. ← → and space work too.
Showing the closely related Merge Sort visualization.
1mergeSort(a, lo, hi):2 if lo >= hi: return3 mid = (lo + hi) // 24 mergeSort(a, lo, mid)5 mergeSort(a, mid+1, hi)6 merge(a, lo, mid, hi):7 i = lo, j = mid+1, buf = []8 while i <= mid and j <= hi:9 if a[i] <= a[j]: buf.push(a[i++]) else buf.push(a[j++])10 append leftovers of both halves to buf11 copy buf back into a[lo..hi]Pseudocode
1solve(problem):2 if size(problem) <= BASE: return direct(problem)3 parts = divide(problem) # a parts of size n/b4 results = [solve(p) for p in parts] # independent recursion5 return combine(results) # f(n) work6 7# T(n) = a T(n/b) + f(n); compare f(n) with n^(log_b a):8# f smaller -> Θ(n^(log_b a)) leaves dominate (Karatsuba)9# f equal -> Θ(n^(log_b a) log n) every level ties (merge sort)10# f larger -> Θ(f(n)) root dominates (rare)Implementations
1# Divide and conquer: split into independent subproblems of the same shape,2# solve recursively, combine. The Master theorem for T(n) = a*T(n/b) + O(n^d)3# says the cost is decided by comparing d against log_b(a):4# d > log_b(a) -> O(n^d) (the combine dominates)5# d == log_b(a) -> O(n^d log n) (every level costs the same)6# d < log_b(a) -> O(n^(log_b a)) (the leaves dominate)7 8 91 · Merge sort: a=2, b=2, d=1, so d == log_2(2) and the cost is O(n log n)10def merge_sort(a: list[int], buf: list[int], lo: int, hi: int) -> None:11 if hi - lo <= 1:12 return13 mid = (lo + hi) // 214 merge_sort(a, buf, lo, mid)15 merge_sort(a, buf, mid, hi)16 17 i, j, k = lo, mid, lo18 while i < mid and j < hi:19 if a[j] < a[i]:20 buf[k] = a[j]21 j += 122 else:23 buf[k] = a[i]24 i += 125 k += 126 while i < mid:27 buf[k] = a[i]28 i += 129 k += 130 while j < hi:31 buf[k] = a[j]32 j += 133 k += 134 a[lo:hi] = buf[lo:hi]35 36 372 · Counting inversions is merge sort with one extra line in the combine38def count_inversions(a: list[int], buf: list[int], lo: int, hi: int) -> int:39 if hi - lo <= 1:40 return 041 mid = (lo + hi) // 242 total = count_inversions(a, buf, lo, mid) + count_inversions(a, buf, mid, hi)43 44 i, j, k = lo, mid, lo45 while i < mid and j < hi:46 if a[j] < a[i]:47 total += mid - i # a[i..mid) are all greater than a[j]48 buf[k] = a[j]49 j += 150 else:51 buf[k] = a[i]52 i += 153 k += 154 while i < mid:55 buf[k] = a[i]56 i += 157 k += 158 while j < hi:59 buf[k] = a[j]60 j += 161 k += 162 a[lo:hi] = buf[lo:hi]63 return total64 65 663 · Karatsuba: a=3, b=2, d=1, so log_2(3) ~ 1.585 > 1 and the LEAVES67# dominate — O(n^1.585) instead of the schoolbook O(n^2)68def karatsuba(x: int, y: int) -> int:69 if x < 10 or y < 10:70 return x * y71 n = len(str(max(x, y)))72 half = n // 273 p = 10**half74 75 a, b = divmod(x, p)76 c, d = divmod(y, p)774 · Three multiplications instead of four is the whole trick78 ac = karatsuba(a, c)79 bd = karatsuba(b, d)80 abcd = karatsuba(a + b, c + d) - ac - bd81 return ac * p * p + abcd * p + bd82 83 845 · When the split does NOT pay off: a=1, b=2, d=0 gives O(log n), which85# is binary search — the same recurrence shape with a trivial combine86def binary_search_rec(a: list[int], target: int, lo: int, hi: int) -> int:87 if lo >= hi:88 return -189 mid = (lo + hi) // 290 if a[mid] == target:91 return mid92 return binary_search_rec(a, target, mid + 1, hi) if a[mid] < target else binary_search_rec(a, target, lo, mid)a[lo:hi] = buf[lo:hi]is slice assignment, which copies the merged range back in one C-level operation rather than an element loop.divmod(x, p)splits the operand into high and low halves in a single call.- Python integers are unbounded, so Karatsuba works on arbitrarily large operands with no type ceremony — the only language here where the algorithm can be demonstrated at the size where it actually matters.
len(str(max(x, y)))counts decimal digits exactly, whichmath.log10would not for very large integers.(lo + hi) // 2needs no overflow guard, since Python integers do not overflow.
CPython already uses Karatsuba internally for int multiplication above about 70 digits, so the hand-written version is slower than x * y.
- CPython's built-in
intmultiplication switches to Karatsuba above roughly 70 digits, which makes this implementation educational rather than useful. - Slice assignment
a[lo:hi] = buf[lo:hi]is a single C-level copy and much faster than a Python element loop. divmodreturns both halves in one call, which is both faster and clearer than separate//and%.- Recursion depth is log n, so
RecursionErroris not a concern even for very large inputs.
- Using
math.log10to count digits of a huge integer, which loses precision and can be off by one. - Writing the copy-back as an element loop instead of slice assignment.
- Expecting the hand-written Karatsuba to beat
x * y, which already uses it.
- Karatsuba is only demonstrable at a useful scale in Python, whose integers are unbounded; JS/TS need
BigInt(which brings a number-mixing hazard), and C++long longcaps out far below where the algorithm helps. - Copying a range back: Python slice assignment is one call, C++
std::copyis one call, and JS/TS need an explicit loop or an allocatingslice. - CPython already uses Karatsuba internally above ~70 digits, so the hand-written version is strictly educational there — the other three languages have no such built-in.
- Overflow-safe midpoints matter in C++ and JS/TS (
lo + ((hi - lo) >> 1)) and are unnecessary in Python.
Complexity
T(n) = a T(n/b) + f(n). Binary search O(log n); merge sort / closest pair O(n log n); Karatsuba O(n^1.585); Strassen O(n^2.81). Unbalanced splits (quick sort worst case) degrade to O(n²).
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Subproblems are independent and a cheap combine step exists: sorting, counting inversions, closest pair, skyline, polynomial/integer multiplication, matrix multiplication.
- Half of the input can be discarded per step (
a = 1): Binary Search, Quickselect (expected), finding a peak element. - The recursion tree is balanced, so depth is
O(log n)and the stack stays small.
- Subproblems overlap (Fibonacci, edit distance, knapsack): naive D&C recomputes them exponentially many times — use Memoization (Top-Down DP) or Tabulation (Bottom-Up DP).
- The combine step costs as much as the brute force. Splitting an array to find its maximum is
2T(n/2) + O(1) = O(n)— no better than a loop, with recursion overhead. Four half-size multiplications (4T(n/2) + O(n)) are stillO(n²); D&C only pays off whena < b^kfor the brute-force exponentk. - A linear scan already achieves the lower bound (e.g. Kadane's Algorithm finds the max subarray in
O(n); the D&C version isO(n log n)). - Tiny inputs: recursion overhead dominates. Real merge sorts switch to Insertion Sort below ~16 elements, and Karatsuba only beats schoolbook multiplication for numbers of hundreds of digits.
Alternatives
Common mistakes
- Applying the Master theorem to unbalanced splits (
T(n) = T(n−1) + O(n)) or tof(n)that is only logarithmically larger thann^(log_b a)— neither fits the three cases. - Forgetting the crossing case in the combine step (max subarray, closest pair): the answer may straddle the split line.
- Using
(lo + hi) / 2in fixed-width integers — overflows for large indices; uselo + (hi − lo) / 2. - Copying subarrays at every level (
a[:mid]in Python) — correct, but addsO(n)allocation per level; index ranges over one buffer are the idiomatic fix. - Confusing D&C with DP: if you find yourself solving the same
(i, j)twice, stop and memoize.
Interview patterns
- Count inversions / reverse pairs / count of smaller numbers after self: merge sort with counting during merge.
- Maximum subarray via D&C (then contrast with Kadane's Algorithm).
- Median of two sorted arrays: binary-search partition —
a = 1, b = 2. - Skyline problem: merge two skylines like merging sorted lists.
- Pow(x, n) and matrix exponentiation (Fast Exponentiation):
T(n) = T(n/2) + O(1). - Being asked to "state the recurrence and solve it" — say
a,b,f(n), and the Master theorem case out loud.
- Recognizing the approach from an array and a targetIntermediate
- Deciding whether O(n²) can be improvedIntermediate
- Where does O(n log n) come from?Beginner
- Average case versus worst caseIntermediate
- Minimum Size Subarray SumIntermediate
- Kth Largest Element in an ArrayIntermediate
- Search in Rotated Sorted ArrayIntermediate