<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. DSA
  3. Algorithm design
All lessons in this course

Lesson 40 of 50 15 minAdvanced

Divide and conquer

Split a problem into smaller copies of itself, solve them recursively and combine the results: fast exponentiation, maximum subarray, counting inversions, the master theorem, and parallelism.

Divide and conquer solves a problem in three moves: divide it into smaller problems of the same kind, conquer each one recursively (tiny ones are solved directly), and combine their answers. You have seen it already: binary search, merge sort and quick sort are all divide and conquer.

AlgorithmDivideConquerCombine
Binary searchLook at the middleOne halfNothing
Merge sortSplit in halfBoth halvesMerge: O(n)
Quick sortPartition around a pivot: O(n)Both sidesNothing
Fast powerHalve the exponentOne halfSquare it

Fast exponentiation

Computing xⁿ by multiplying n times is O(n). But x¹³ = x⁶ · x⁶ · x, and x⁶ = x³ · x³. Solving the half-size problem once and squaring it gives O(log n) multiplications. With a modulo after each step, the numbers stay small; this is the heart of RSA encryption.

FastPower.cs
Console.WriteLine(Power(3, 13));Console.WriteLine(Power(2, 62));Console.WriteLine(ModPow(7, 1_000_000_000, 1_000_000_007)); static long Power(long x, int n){    if (n == 0) return 1;    long half = Power(x, n / 2);                        // one problem of half the size    return n % 2 == 0 ? half * half : half * half * x;  // combine} static long ModPow(long x, long n, long mod)            // the same idea, as a loop{    long result = 1;    x %= mod;    while (n > 0)    {        if ((n & 1) == 1) result = result * x % mod;    // this bit of n is set        x = x * x % mod;        n >>= 1;    }    return result;}
Output
1594323
4611686018427387904
312556845

A billion as the exponent needs only about 30 squarings. .NET has this built in as BigInteger.ModPow.

How fast is a divide-and-conquer algorithm?

Write the cost as a recurrence: T(n) = a·T(n/b) + f(n), where a is the number of subproblems, n/b their size and f(n) the work to divide and combine. Merge sort is T(n) = 2T(n/2) + n. Draw the recursion tree and add up each level:

Interactive

Where does the work go?

A divide-and-conquer algorithm splits a problem of size n into a pieces of size n / b and does nd work to split and combine. Each bar is one level of the recursion, for n = 64.

T(n) = 2T(n/2) + n  ·  sort both halves, then merge in linear time
level 01 × size 6464
level 12 × size 3264
level 24 × size 1664
level 38 × size 864
level 416 × size 464
level 532 × size 264
level 664 × size 164
Total work 448 (n = 64, so n log₂ n = 384, n² = 4096)
Every level does the same amount of work (log_b a = 1.00 = d), and there are log n levels: O(n log n).

The master theorem summarises the three cases for f(n) = nᵈ, comparing d with log_b a:

CaseMeaningResultExample
d > log_b aTop level dominatesO(nᵈ)Quickselect: O(n)
d = log_b aEvery level equalO(nᵈ log n)Merge sort: O(n log n); binary search: O(log n)
d < log_b aLeaves dominateO(n^(log_b a))Karatsuba: O(n^1.58)

The lesson of Karatsuba: multiplying two n-digit numbers by splitting each in half naively needs 4 half-size products, which is still O(n²). A clever identity needs only 3, and that alone makes it O(n^1.58). Reducing the number of subproblems matters more than speeding up the combine step.

Maximum subarray

Daily profit or loss on a project: which run of consecutive days earned the most? Split the days in half. The best run is entirely in the left half, entirely in the right half, or it crosses the middle. The first two are recursive calls; the crossing one is the best sum ending at the middle plus the best sum starting just after it, found in O(n). T(n) = 2T(n/2) + n = O(n log n).

MaxSubarray.cs
int[] profit = { 3, -8, 5, -1, 6, -9, 4, 2, -3 };Console.WriteLine(MaxSub(profit, 0, profit.Length - 1)); static int MaxSub(int[] a, int lo, int hi){    if (lo == hi) return a[lo];                          // one day: nothing to split    int mid = lo + (hi - lo) / 2;    int left = MaxSub(a, lo, mid);    int right = MaxSub(a, mid + 1, hi);     int sum = 0, bestLeft = int.MinValue;                // best run ending at mid    for (int i = mid; i >= lo; i--) { sum += a[i]; bestLeft = Math.Max(bestLeft, sum); }    sum = 0;    int bestRight = int.MinValue;                        // best run starting at mid + 1    for (int i = mid + 1; i <= hi; i++) { sum += a[i]; bestRight = Math.Max(bestRight, sum); }     return Math.Max(Math.Max(left, right), bestLeft + bestRight);}
Output
10

Divide and conquer runs in parallel

The subproblems are independent, so they can run on different CPU cores. Parallel.Invoke runs two halves at once; below a size threshold, do the work sequentially, because starting a task costs more than adding a few thousand numbers.

ParallelSum.cs
int[] data = Enumerable.Range(1, 1_000_000).ToArray();Console.WriteLine(Sum(data, 0, data.Length)); static long Sum(int[] a, int lo, int hi)                 // sums a[lo..hi){    if (hi - lo <= 50_000)                               // small: just loop    {        long s = 0;        for (int i = lo; i < hi; i++) s += a[i];        return s;    }    int mid = lo + (hi - lo) / 2;    long left = 0, right = 0;    Parallel.Invoke(() => left = Sum(a, lo, mid), () => right = Sum(a, mid, hi));    return left + right;}
Output
500000500000
Quick check

T(n) = 2T(n/2) + n. What is the running time?

Quick check

How many multiplications does fast exponentiation need for x¹⁰²⁴?

Practice

Count the inversions in an array: pairs (i, j) with i < j but a[i] > a[j]. It measures how unsorted the data is. { 5, 3, 8, 1, 4 } has 6. Do it in O(n log n) by changing merge sort: when an item from the right half is placed before the items still waiting in the left half, each of those forms an inversion with it.

Show a hint

In the merge step, when right[j] < left[i], all of left[i..] are bigger than right[j]: add left.Length - i to the count.

Show the solution
Solution.cs
Console.WriteLine(CountInversions(new[] { 5, 3, 8, 1, 4 }));Console.WriteLine(CountInversions(new[] { 1, 2, 3, 4 }));Console.WriteLine(CountInversions(new[] { 4, 3, 2, 1 })); static long CountInversions(int[] a){    if (a.Length < 2) return 0;    int mid = a.Length / 2;    var left = a[..mid];    var right = a[mid..];    long count = CountInversions(left) + CountInversions(right);   // both halves are now sorted     int i = 0, j = 0, k = 0;    while (i < left.Length && j < right.Length)    {        if (left[i] <= right[j]) a[k++] = left[i++];        else        {            count += left.Length - i;      // right[j] jumps ahead of every remaining left item            a[k++] = right[j++];        }    }    while (i < left.Length) a[k++] = left[i++];    while (j < right.Length) a[k++] = right[j++];    return count;}
Output
6
0
6

Remember

  • Divide into smaller copies, conquer recursively, combine the answers.
  • Costs follow recurrences T(n) = aT(n/b) + f(n); the master theorem gives the result.
  • Fewer subproblems beats a faster combine step (Karatsuba: 3 instead of 4).
  • Independent subproblems run naturally in parallel, above a size threshold.
0 of 50 lessons completed