Skip to content

Divide and conquer

intermediateCommon atGoogleMetaAmazonTrading firmsRead firstRecursionComplexity analysisPractise in the Sandbox →

Divide and conquer splits a problem into a smaller copies of size n/b, solves each recursively, and combines the results with n^d extra work. The cost is decided by one comparison: a vs b^d. If they’re equal every level of the recursion tree costs the same, and you get Θ(n^d log n); merge sort’s Θ(n log n) is exactly this case.

You have to count the votes of a whole country. Counting alone would take forever. Instead you split the country into regions, give each region to a team, and each team splits its region again, down to single polling stations. Then the totals flow back up: stations add up into towns, towns into regions, regions into the national result.

That’s the whole pattern, in three moves:

  • Divide. Cut the input into smaller pieces of the same kind of problem.
  • Conquer. Solve each piece recursively. Tiny pieces (the base case) are solved directly.
  • Combine. Merge the pieces’ answers into the answer for the whole.

It pays off when combining is cheap compared to the brute force. Merge sort is the classic case: sorting is hard, but merging two sorted halves takes one linear pass.

The cost question is: how much work happens at each depth of the recursion, and which depth dominates? The visualization answers it.

Pick a recurrence (or a preset), then step through the levels of its recursion tree. The left side shows the subproblems at each depth; the right shows the total work at that depth. Watch the bars: do they grow, stay level, or shrink as you go down?

Recursion tree for T(n) = a·T(n/b) + n^d
step through
subproblems at each depth (box width ∝ size)work at this depth16168816444416222222221616
T(n)2·T(n/2) + n
a/b^d1.00
log_b a1.000
total work…
verdict…
level being addedwhere the work concentrates
Level 0 is the original call on n = 16. It does n = 16 work outside its recursive calls, then splits into 2 subproblems of size n/2.
1 / 6

Try these in order: Merge sort (bars stay level), Split-multiply, 4 products (bars grow: leaves win), Karatsuba (still growing, but more slowly), Quickselect (bars shrink: the root wins).

The problem says… Why divide and conquer fits
the answer for the whole can be built from answers for two halves That’s the combine step. Ask “if I knew the answer for each half, what’s missing?”
“sort”, “count pairs (i, j) with i < j such that…” Pairs split three ways: both left, both right, or crossing. Crossing pairs are often countable during a merge.
“k-th smallest”, “median”, “top k” without sorting everything Partition, then recurse into one side only (quickselect).
huge numbers, matrices, polynomials Splitting plus an algebra trick saves a recursive call (Karatsuba, Strassen, FFT).
a tree, a range, or an interval that splits naturally Segment trees, binary search and tree DP are all divide and conquer in disguise.
  1. Base case. If the input is small enough (one element, one digit, empty), solve it directly and return.
  2. Divide. Split the input into a pieces of about n/b each. Splitting by index is O(1); partitioning around a pivot is O(n).
  3. Conquer. Call yourself on each piece.
  4. Combine. Build the full answer from the pieces’ answers. This step usually holds the cleverness and most of the per-level cost.

Multiply two n-digit numbers. Write x = a·10^m + b and y = c·10^m + d. Then

x·y = ac·10^(2m) + (ad + bc)·10^m + bd

That needs four half-size products (ac, ad, bc, bd), and four products of size n/2 is T(n) = 4T(n/2) + n = Θ(n²): no better than schoolbook multiplication. Karatsuba’s 1962 trick is that (a+b)(c+d) − ac − bd = ad + bc, so three products are enough. T(n) = 3T(n/2) + n = Θ(n^log₂3) ≈ Θ(n^1.585).

karatsuba.py
def karatsuba(x: int, y: int) -> int:
"""Multiply non-negative ints with 3 half-size products instead of 4. O(n^1.585) digit ops."""
if x < 10 or y < 10: # 1. base case: one side is a single digit
return x * y
m = max(len(str(x)), len(str(y))) // 2 # split point, in decimal digits
p = 10**m
a, b = divmod(x, p) # 2. divide: x = a*10^m + b, y = c*10^m + d
c, d = divmod(y, p)
ac = karatsuba(a, c) # 3. conquer: three recursive products...
bd = karatsuba(b, d)
ad_plus_bc = karatsuba(a + b, c + d) - ac - bd # ...because (a+b)(c+d) - ac - bd = ad + bc
return ac * p * p + ad_plus_bc * p + bd # 4. combine: shifts and adds only, O(n)
if __name__ == "__main__":
assert karatsuba(1234, 5678) == 7_006_652
assert karatsuba(0, 99) == 0
big_a, big_b = 3141592653589793238462643383279, 2718281828459045235360287471352
assert karatsuba(big_a, big_b) == big_a * big_b
print("ok")

Example 2: quickselect (divide, then conquer only one side)

Section titled “Example 2: quickselect (divide, then conquer only one side)”

To find the k-th smallest value you don’t need both halves sorted. Partition around a pivot (as in quicksort), see which side rank k falls on, and continue only there. Some books call this decrease and conquer, since only one subproblem survives.

quickselect.py
import random
def quickselect(xs: list[int], k: int) -> int:
"""k-th smallest value (0-based). Expected O(n), worst O(n^2). Leaves xs untouched."""
if not 0 <= k < len(xs):
raise IndexError("k out of range")
a = list(xs)
lo, hi = 0, len(a) - 1
while lo < hi:
p = random.randint(lo, hi) # 1. random pivot: no fixed input is always bad
a[p], a[hi] = a[hi], a[p]
store = lo
for i in range(lo, hi): # 2. partition: values < pivot go left
if a[i] < a[hi]:
a[i], a[store] = a[store], a[i]
store += 1
a[store], a[hi] = a[hi], a[store] # pivot now sits at its final sorted position
if k == store: # 3. keep only the side that holds rank k
return a[k]
if k < store:
hi = store - 1
else:
lo = store + 1
return a[lo]
if __name__ == "__main__":
data = [7, 2, 9, 4, 4, 1, 8]
assert [quickselect(data, k) for k in range(len(data))] == sorted(data)
assert quickselect([5], 0) == 5
assert data == [7, 2, 9, 4, 4, 1, 8] # input not modified
print("ok")

For a recurrence T(n) = a·T(n/b) + Θ(n^d) with a ≥ 1, b > 1:

  • Level i of the tree has a^i subproblems, each of size n/b^i, each doing (n/b^i)^d work.
  • So level i costs n^d · (a/b^d)^i. The levels form a geometric series with ratio r = a/b^d.
  • There are log_b n levels.

A geometric series is dominated by its largest term. That gives the three cases:

Compare Ratio r = a/b^d What dominates T(n) Example
a < b^d r < 1, bars shrink the root Θ(n^d) quickselect, good pivots: T(n/2) + n = Θ(n)
a = b^d r = 1, bars level every level equally Θ(n^d log n) merge sort: 2T(n/2) + n = Θ(n log n); binary search: T(n/2) + 1 = Θ(log n)
a > b^d r > 1, bars grow the leaves Θ(n^(log_b a)) Karatsuba: 3T(n/2) + n = Θ(n^1.585); tree traversal: 2T(n/2) + 1 = Θ(n)

Why the leaves case gives n^(log_b a): the tree has a^(log_b n) leaves, and a^(log_b n) = n^(log_b a) (take log of both sides). Each leaf does O(1) work.

Going from 4 recursive calls to 3 changes the exponent, not a constant, because the saving repeats at every level.

Digit multiplications: 4 products per split vs 3
drag the slider
4 recursive products
4^log₂n = n²
4,096
Karatsuba, 3 products
3^log₂n ≈ n^1.585
729
Both split each number in half, 6 times. Saving one product per call looks tiny, but it compounds at every level: Karatsuba does 5.6× fewer digit multiplications at n = 64 (the ratio is (4/3)^6).
Θ(n), Θ(n log n), Θ(n²): the three answers you'll derive most
drag n
2565127681.0kinput size n →
n = 16
O(n): 16 opsO(n log n): 64 opsO(n²): 256 ops

The recursion stack is as deep as the tree: O(log n) for balanced splits, O(n) for lopsided ones (quicksort on sorted input with a first-element pivot). Merge sort also needs an O(n) buffer for merging. Quickselect, written as a loop like above, uses O(1) extra beyond its copy.

  • Merge-based counting. While merging two sorted halves, each element taken from the right half “jumps over” all remaining left elements. Counting those jumps counts crossing pairs in O(n) per level, O(n log n) total. Many “count pairs where…” problems reduce to this.
  • Partition-based selection. Quickselect is expected O(n). Median-of-medians (Blum, Floyd, Pratt, Rivest, Tarjan, 1973) guarantees O(n) worst case, but with a big constant; in practice a random pivot wins.
  • Closest pair of points. Split by x-coordinate, solve each side, then check only a thin strip around the dividing line. T(n) = 2T(n/2) + O(n) = O(n log n).
  • Matrix and polynomial tricks. Strassen multiplies matrices with 7 half-size products instead of 8: Θ(n^2.807). The FFT multiplies polynomials in O(n log n).
  • Divide and conquer on a structure. Binary search halves a range; a segment tree stores the combine step’s results so queries reuse them; tree recursion splits at each node.
  • No progress toward the base case. A split that can produce a piece of the same size (e.g. mid = (lo + hi) // 2 with the wrong +1) recurses forever. Check the two-element input by hand.
  • Expensive splitting by accident. Slicing (xs[:mid] in Python, slice in JS) copies. Copying at every level adds O(n) per level, which is fine for merge sort but turns an O(log n) binary search into O(n). Pass indices instead.
  • Forgetting the crossing case. For “count/find something over pairs or ranges”, the answer is left + right + crossing. The crossing part is the hard bit and the whole point.
  • Stack depth. Python’s default recursion limit is 1000. Lopsided splits on 10^5 elements will blow it. Recurse on the smaller side and loop on the larger one, or go iterative.
  • Bad pivots. A fixed pivot (first or last element) makes quicksort and quickselect O(n²) on sorted input. Randomize.
  • State the three steps out loud: “I’ll split into halves, solve each recursively, and combine with a linear merge.”
  • Write the recurrence before quoting a complexity: “T(n) = 2T(n/2) + O(n), so a = 2, b = 2, d = 1; a = b^d, so Θ(n log n).” That one line shows you can derive it, not just recall it.
  • Name the space cost: recursion depth plus any buffers.
  • Expect follow-ups: “Can you do it without recursion?” (bottom-up merge sort), “What’s the worst case?” (bad pivots), “What if the data doesn’t fit in memory?” (external merge sort: sort chunks, then k-way merge them with a heap).
Check yourself
pick one
T(n) = 2T(n/2) + O(1): a function that recurses into both halves and does constant work per call (like summing a binary tree). What is T(n)?
Check yourself
pick one
You find a way to multiply n-digit numbers using 5 products of size n/3 plus linear work. Is it faster than Karatsuba's Θ(n^1.585)?
Check yourself
pick one
Quickselect with a lucky pivot every time recurses into one half and partitions in linear time: T(n) = T(n/2) + n. Why is the total Θ(n) and not Θ(n log n)?

Classic textbook exercises, not gym questions:

  • Merge sort, both top-down and bottom-up (no recursion: merge runs of width 1, 2, 4, …).
  • Find the majority element (appears more than n/2 times) by divide and conquer: the majority of the whole is the majority of one half.
  • Closest pair of points in the plane in O(n log n).
  • Build a balanced binary search tree from a sorted array.
  • Raise a 2×2 matrix to the n-th power in O(log n) multiplications, and use it to compute Fibonacci numbers.