datarekha

Divide & Conquer

A three-step pattern that turns a hard problem into smaller copies of itself — and the reasoning for why it runs so efficiently.

6 min read Intermediate Data Structures & Algorithms Lesson 25 of 32

What you'll learn

  • The three-step pattern — divide, conquer, combine — and when each step matters
  • How a recurrence T(n) = a·T(n/b) + f(n) describes recursive work
  • Master Theorem intuition without the heavy algebra
  • Why the pattern maps straight onto parallel systems like MapReduce and Spark

Before you start

Divide and conquer is not one algorithm — it is a design pattern, and most of the fastest algorithms you already know wear it. The shape is always three steps: divide the problem into smaller copies of itself, conquer each one recursively (stopping at a base case small enough to answer directly), then combine the partial answers into the whole.

The quiet requirement that makes it work is that the subproblems are independent — solving one does not depend on solving another. That independence is what lets you split the work, and (as we will see) what lets you parallelise it.

You have met the pattern already. Binary search is the easy case: divide by comparing to the middle, conquer by recursing into the one half that can contain the target, and the combine step is nothing — the recursive call returns the answer directly. That is why it is O(log n). Merge sort uses all three steps for real: divide at the midpoint, sort each half, then combine by merging two sorted halves in O(n) — and that O(n) combine, repeated over log n levels, is exactly the O(n log n).

A less obvious case: maximum subarray

Given an array that may hold negatives, find the contiguous stretch with the largest sum. Split at the midpoint, and the best subarray is one of three things: entirely in the left half, entirely in the right half — or straddling the midpoint.

midpoint-234-121-54left halfright halfbest crossing subarray (3 + 4 − 1 + 2 + 1 = 9) spans the midpoint
Neither recursive call can find the crossing case — so the combine step grows outward from the midpoint in O(n) to catch it.

The two recursive calls handle the left-only and right-only cases; the combine step handles the crossing case in O(n) by expanding outward from the midpoint. The best of the three is the answer — O(n log n), better than the naive O(n²).

Fast exponentiation

Computing x to the power n looks like it needs n multiplications. Divide and conquer cuts it to O(log n) with one observation: if n is even, xⁿ = (x^(n/2))²; if odd, xⁿ = x · x^(n−1). Each step at least halves the exponent.

def pow_fast(x, n):
    if n == 0:
        return 1, 0                       # value, multiplication count
    if n % 2 == 0:
        half, ops = pow_fast(x, n // 2)
        return half * half, ops + 1       # one squaring
    rec, ops = pow_fast(x, n - 1)
    return x * rec, ops + 1

for n in [10, 50, 200, 1000, 5000]:
    _, ops = pow_fast(2, n)
    print(f"x**{n}: naive needs {n} multiplications, fast needs {ops}")
x**10: naive needs 10 multiplications, fast needs 5
x**50: naive needs 50 multiplications, fast needs 8
x**200: naive needs 200 multiplications, fast needs 10
x**1000: naive needs 1000 multiplications, fast needs 15
x**5000: naive needs 5000 multiplications, fast needs 17

Fifteen multiplications for x¹⁰⁰⁰ instead of a thousand — and the gap only widens, because it is O(log n) against O(n). (Karatsuba multiplication is the same trick on big numbers: split each at the midpoint, recurse on the pieces, and combine cleverly to use three sub-multiplications instead of four, dropping long multiplication from O(n²) to about O(n^1.585).)

Recurrences and where the work goes

Every divide-and-conquer cost is captured by a recurrence, T(n) = a·T(n/b) + f(n): you make a recursive calls, each on a piece of size n/b, plus f(n) work to divide and combine. The Master Theorem answers it, and you only need its intuition — which level of the recursion does most of the work? Picture the recursion as a tree. If the split-and-combine work f(n) grows slower than the number of leaves, the leaves dominate; if it grows faster, the root dominates; and if they grow at the same rate, every level does equal work and you pick up an extra log factor.

Merge sort is the equal case: a = b = 2 and f(n) = n, so each of the log n levels contributes the same O(n) of merging — O(n log n). Binary search is also equal, but with O(1) per level across log n levels — O(log n).

Practice

Quick check

0/3
Q1In T(n) = 2·T(n/2) + O(n), what does the O(n) term represent?
Q2Fast exponentiation computes x¹⁰⁰⁰ in about how many multiplications?
Q3What is the core requirement for divide and conquer to beat O(n²) when splitting into halves?

Sign in to track your progress

Completed lessons, your XP, level, and streak save to your account — it's free and takes a few seconds.

Practice this in an interview

All questions
Generate all subsets of a set — the power set — using backtracking.

At each step of a recursive walk through the input, you make a binary choice: include the current element or skip it. Recording the current path at every node of the recursion tree (not just the leaves) collects all 2^n subsets. A `start` index prevents duplicates by ensuring elements are only considered left-to-right.

Find all unique combinations of candidates that sum to a target, where each candidate may be used an unlimited number of times.

Use backtracking with a running total. At each step, try adding a candidate to the current path. If the total equals the target, record the path. If it exceeds the target, prune. Passing the same start index (not i+1) back into the recursion allows unlimited reuse of the same element.

Generate all permutations of a list of distinct integers using backtracking.

At each recursion level, swap one of the remaining (unused) elements into the current position, recurse to fill the rest, then swap back to restore the state. Alternatively, track a 'used' set and build the permutation in a separate path list. The result is all n! orderings.

Return an array where each element is the product of all other elements, without using division.

Make two passes: a left pass where each position accumulates the product of everything to its left, then a right pass (using a rolling variable) that multiplies in everything to its right. The two passes together give the complete product-of-all-others in O(n) time and O(1) extra space (excluding output).

Related lessons

Explore further

Skip to content