Divide and Conquer
Split a problem into halves, solve each half the same way, then combine. Count disagreements between two rankings fast and read the cost of any split.
On this page
You and a friend each rank the same six films. You want one number that says how much you disagree. A natural one: count the pairs of films you put in opposite order. If you rank Film A above Film B and your friend ranks B above A, that pair is one disagreement.
Number the films 1 to 6 in your order. Then write your friend's list, favourite first, using your numbers: [3, 6, 5, 1, 4, 2]. A disagreement is any bigger number that appears before a smaller one. Checking every pair works, but that is n(n − 1) / 2 comparisons, about 45 billion for a catalogue of 300,000 films. Divide and conquer does it in O(n log n).
The idea: split, solve, combine
You have already met this pattern twice. Binary search throws away half the range and solves the other half. Merge sort sorts each half and merges them. Both follow the same three steps:
- Divide the input into smaller parts, usually two halves.
- Conquer each part by calling the same function on it.
- Combine the answers for the parts into the answer for the whole.
The first two steps are almost free to write. All the thinking goes into the combine step: given the answers for each half, what is still missing, and how do you get it cheaply?
Counting disagreements
Split the list into [3, 6, 5] and [1, 4, 2]. Every disagreement is one of three kinds: both numbers in the left half, both in the right half, or one in each. The recursive calls count the first two kinds. The combine step only has to count the pairs that cross the middle.
Counting crossing pairs one by one would still be slow. Here is the trick: the crossing count does not depend on the order inside each half. So you are free to sort each half first, and the recursion can hand back sorted halves for nothing, because merging them is exactly what merge sort does anyway.
During the merge, every left item came before every right item in your friend's list. So when a right item is smaller than the front of the left half, it is also smaller than everything still waiting there, and each of those is a disagreement. Add how many are waiting, in one step.
merge left and right, counting as you go:while both halves have items: if left[i] <= right[j]: take left[i] else: cross += items still waiting in left take right[j]copy whatever remains- left (sorted)03i1526right (sorted)01j1224merged
- inside
- 2
- cross
- 0
1The halves came back sorted, with 1 disagreement inside each: 6 before 5 on the left, 4 before 2 on the right. Now count the pairs that cross the middle.
- left (sorted)03i1526right (sorted)0112j24merged01
- inside
- 2
- cross
- 3
21 < 3, so 1 goes next. It came from the right half, so it jumps over 3, 5 and 6, which are still waiting on the left. Each jump is a disagreement: add 3.
- left (sorted)03i1526right (sorted)011224jmerged0112
- inside
- 2
- cross
- 6
32 < 3, so take 2. All three left items are still waiting, so it jumps over 3 of them again. Add 3.
- left (sorted)0315i26right (sorted)011224jmerged011223
- inside
- 2
- cross
- 6
43 < 4, so take 3. It comes from the left, so it jumps over nothing. Add 0.
- left (sorted)0315i26right (sorted)011224jmerged01122334
- inside
- 2
- cross
- 8
54 < 5, so take 4. Only 5 and 6 are still waiting, so add 2.
- left (sorted)031526iright (sorted)011224jmerged011223344556
- inside
- 2
- cross
- 8
6The right half is empty. Copy 5 and 6 across. Nothing is left for them to jump over.
- merged011223344556
- inside
- 2
- cross
- 8
- total
- 10
7Total: 2 inside the halves + 8 across = 10 disagreements out of 15 pairs. The merge found the 8 crossing ones without comparing every pair.
// Returns [sorted copy of a, number of disagreements in a].
function sortAndCount(a) {
if (a.length <= 1) return [a, 0];
const mid = Math.floor(a.length / 2);
const [left, leftCount] = sortAndCount(a.slice(0, mid));
const [right, rightCount] = sortAndCount(a.slice(mid));
const merged = [];
let i = 0, j = 0, cross = 0;
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) {
merged.push(left[i++]);
} else {
cross += left.length - i; // right[j] jumps every waiting left item
merged.push(right[j++]);
}
}
while (i < left.length) merged.push(left[i++]);
while (j < right.length) merged.push(right[j++]);
return [merged, leftCount + rightCount + cross];
}
sortAndCount([3, 6, 5, 1, 4, 2])[1]; // 10# Returns (sorted copy of a, number of disagreements in a).
def sort_and_count(a):
if len(a) <= 1:
return a, 0
mid = len(a) // 2
left, left_count = sort_and_count(a[:mid])
right, right_count = sort_and_count(a[mid:])
merged = []
i = j = cross = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
merged.append(left[i])
i += 1
else:
cross += len(left) - i # right[j] jumps every waiting left item
merged.append(right[j])
j += 1
merged.extend(left[i:])
merged.extend(right[j:])
return merged, left_count + right_count + cross
sort_and_count([3, 6, 5, 1, 4, 2])[1] # 10Note the <=. When two values are equal, take the left one first. They are not a disagreement, so nothing should be counted for them.
| Approach | Time | Space | Why |
|---|---|---|---|
| Check every pair | O(n²) | O(1) | n(n − 1) / 2 comparisons. |
| Divide and conquer | O(n log n) | O(n) | log n levels of halving, and the merges on each level touch all n items once. Same shape as merge sort. |
Reading the cost of any split
You do not need a formula to price a divide and conquer algorithm. Draw the tree of calls and add up the work level by level. Two things decide the answer: how many smaller calls each call makes, and how much the combine step costs.
| Each call makes | Combine costs | Total | Example |
|---|---|---|---|
| 1 call on half | O(1) | O(log n) | binary search, fast power below |
| 1 call on half | O(n) | O(n) | finding the median by partitioning as in quicksort and keeping only the side that holds it (quickselect), on average: n + n/2 + n/4 + … ≈ 2n |
| 2 calls on halves | O(1) | O(n) | largest value by splitting in two |
| 2 calls on halves | O(n) | O(n log n) | merge sort, counting disagreements |
One half is enough: fast power
Sometimes the two halves are the same problem, so you solve it once. To compute 3¹³, note that 3¹³ = 3⁶ × 3⁶ × 3. And 3⁶ = 3³ × 3³, and so on. Each call halves the exponent and does one or two multiplications.
// x to the power n, for a whole number n >= 0
function power(x, n) {
if (n === 0) return 1;
const half = power(x, Math.floor(n / 2)); // solve the half once
return n % 2 === 0 ? half * half : half * half * x;
}
power(3, 13); // 1594323# x to the power n, for a whole number n >= 0
def power(x, n):
if n == 0:
return 1
half = power(x, n // 2) # solve the half once
return half * half if n % 2 == 0 else half * half * x
power(3, 13) # 1594323The exponent goes 13, 6, 3, 1, 0: five calls and 7 multiplications, where a plain loop needs 12. For an exponent of a billion, 31 calls. That is the first row of the table: one call on half, constant combine, O(log n).
Common mistakes
Check yourself
During a merge, the left half is [2, 7, 9] with i on 7, and the right half's next item is 4. How many disagreements does taking 4 add?
An algorithm splits into two halves, recurses on both, and combines in O(1). What is its total cost?
Why does counting disagreements sort each half first?
What to practise next
Quicksort is divide and conquer with the work moved to the divide step. Next in this module, Intervals puts sorting to work on ranges of time.