Skip to main content

Divide & Conquer

Overviewโ€‹

Divide and conquer breaks a problem into independent subproblems of the same kind, solves those recursively, and combines the results. The leverage comes from the subproblems being independent โ€” no shared state, no communication โ€” which is also what makes the pattern parallelise so naturally.

Three steps, always: divide, conquer, combine. Which step does the real work varies, and that variation is what distinguishes mergesort from quicksort.

Core Conceptsโ€‹

AlgorithmDivideConquerCombine
MergesortTrivial โ€” split in halfSort each halfMerge โ€” O(n)
QuicksortPartition โ€” O(n)Sort each sideTrivial โ€” nothing to do
Binary searchCompare to the midpointOne side onlyTrivial
Karatsuba multiplicationSplit the digits3 subproductsShift and add
Strassen's matrix multiplySplit into quadrants7 subproductsAdd submatrices

Mergesort and quicksort are exact mirrors: one does its work combining, the other dividing. Binary search is the degenerate case that discards a subproblem instead of solving it, which is why it is logarithmic rather than linear.

Architecture / Mechanismโ€‹

def divide_and_conquer(problem):
if is_small_enough(problem):
return solve_directly(problem) # base case
subproblems = divide(problem)
results = [divide_and_conquer(p) for p in subproblems]
return combine(results)

The Master Theoremโ€‹

For a recurrence T(n) = aยทT(n/b) + f(n) โ€” a subproblems, each of size n/b, plus f(n) work to divide and combine โ€” compare f(n) against n^(log_b a):

CaseConditionResult
1f(n) grows slowerT(n) = ฮ˜(n^(log_b a)) โ€” the leaves dominate
2f(n) grows at the same rateT(n) = ฮ˜(n^(log_b a) ยท log n) โ€” every level costs the same
3f(n) grows fasterT(n) = ฮ˜(f(n)) โ€” the root dominates

Worked examples:

RecurrenceAlgorithma, b, f(n)Result
T(n) = 2T(n/2) + O(n)Mergesort2, 2, nn^1 = n, case 2 โ†’ ฮ˜(n log n)
T(n) = T(n/2) + O(1)Binary search1, 2, 1n^0 = 1, case 2 โ†’ ฮ˜(log n)
T(n) = 2T(n/2) + O(1)Tree traversal2, 2, 1n^1 vs 1, case 1 โ†’ ฮ˜(n)
T(n) = 7T(n/2) + O(nยฒ)Strassen's7, 2, nยฒn^2.81, case 1 โ†’ ฮ˜(n^2.81)
T(n) = 3T(n/2) + O(n)Karatsuba3, 2, nn^1.58, case 1 โ†’ ฮ˜(n^1.58)

The last two are the interesting ones: both beat the obvious algorithm purely by reducing the number of subproblems โ€” Karatsuba does 3 multiplications where the schoolbook method does 4, Strassen 7 where the naive method does 8. Neither changes the subproblem size; the win is entirely in a.

Practical Usageโ€‹

# Maximum subarray, divide and conquer โ€” O(n log n)
# (Kadane's algorithm solves this in O(n); this version shows the pattern.)
def max_subarray(a, lo, hi):
if lo == hi:
return a[lo]
mid = (lo + hi) // 2
left = max_subarray(a, lo, mid) # entirely in the left half
right = max_subarray(a, mid + 1, hi) # entirely in the right half

# The third case: crossing the midpoint. This is the "combine" step.
best_left, total = float("-inf"), 0
for i in range(mid, lo - 1, -1):
total += a[i]
best_left = max(best_left, total)
best_right, total = float("-inf"), 0
for i in range(mid + 1, hi + 1):
total += a[i]
best_right = max(best_right, total)

return max(left, right, best_left + best_right)

Where the pattern shows up beyond sorting:

  • Parallel processing. MapReduce is divide and conquer with the subproblems distributed across machines; the independence of subproblems is exactly what makes the distribution safe.
  • Fast Fourier Transform โ€” O(n log n) instead of O(nยฒ), by splitting into even and odd indices.
  • Closest pair of points โ€” O(n log n) instead of the O(nยฒ) of checking all pairs.
  • Quickselect โ€” quicksort that recurses into only one side, giving O(n) average for the k-th smallest element.
  • Binary search and every balanced-tree operation.

Edge Cases & Pitfallsโ€‹

Divide and conquer requires independent subproblems

When subproblems overlap โ€” the same sub-computation appearing in several branches โ€” plain recursion recomputes it exponentially often. Naive Fibonacci is the standard demonstration: fib(n-1) and fib(n-2) share almost all their work, and the runtime is O(2โฟ) for an O(n) problem.

Overlapping subproblems mean you want dynamic programming, which is precisely divide and conquer plus memoisation.

  • The base case must be reachable. A "divide" that can produce an empty or full-size subproblem recurses forever. Quicksort's mid + 1 and mid - 1 exist for exactly this reason.
  • Recursion depth is O(log n) when balanced and O(n) when not. Unbalanced quicksort overflows the stack rather than merely running slowly.
  • Switch to an iterative algorithm at small sizes. Recursion overhead dominates below ~16 elements, which is why production sorts fall back to insertion sort.
  • The Master Theorem does not cover everything โ€” it requires subproblems of equal size and well-behaved f(n). Unequal splits need the Akraโ€“Bazzi method or a recursion tree.

Referencesโ€‹

  • Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms, Ch. 4 โ€” divide and conquer, the substitution and recursion-tree methods, and the Master Theorem with proof.
  • Karatsuba, A. (1962) โ€” the multiplication algorithm that first beat the schoolbook O(nยฒ) bound.
  • Strassen, V. (1969), "Gaussian elimination is not optimal" โ€” the matrix-multiplication result.

Books & Videosโ€‹

  • Bentley, J., Programming Pearls, Ch. 8 โ€” the maximum-subarray problem worked through four algorithms, including the divide-and-conquer one above.