Lesson 1 · Algorithmic Paradigms

Divide and Conquer

Divide and conquer is a paradigm that breaks a complex problem into identical, smaller subproblems, recursively solves them, and combines their solutions.

Divide and Conquer concept diagramA visual explanation of the layout and operations shown in this lesson.split the interval, solve both halves, combine their answers2947left max = 9right max = 7combinemax(9,7) = 9the return arrows are the combine phase, not another generic tree
1

Three Steps, and the Condition They Need

Divide and conquer solves a problem in three stages. Divide the input into smaller subproblems of the same kind. Conquer each by recursing, until a base case is small enough to answer directly. Combine the sub-results into an answer for the whole.

The condition that makes this work — and the one most often left unstated — is that the subproblems must be independent. Each is solved without reference to the others, and no subproblem appears twice.

That independence is precisely what separates divide and conquer from dynamic programming. Both split a problem recursively, but DP applies when subproblems overlap and results can be cached for reuse. In divide and conquer there is nothing to cache, because each subproblem is encountered once. Merge sort's two halves share no elements; naive Fibonacci's two calls overlap enormously, which is why one is divide and conquer and the other is a DP problem in disguise.

Independence has a practical payoff beyond the analysis: subproblems that do not depend on one another can be solved in parallel. Merge sort, quicksort, and Strassen's matrix multiplication all parallelise naturally along their recursive structure, which is why the paradigm matters in multicore and distributed settings.

A second condition is worth naming: the division must make progress. Each subproblem must be strictly smaller, and the base case must be reachable from every input. A recursion that halves a range but never reduces it below two, or that recurses on the same size after a bad pivot choice, does not terminate.

  • Divide into smaller instances, recurse, then combine the results
  • Subproblems must be independent and non-overlapping
  • Overlapping subproblems mean dynamic programming instead
  • Independence is also what makes the paradigm parallelisable
2

Where the Work Sits

A useful way to compare algorithms in this family is to ask which of the three steps does the real work. The answer differs, and it explains most of their properties.

Merge sort does trivial dividing — split at the midpoint, O(1) — and all its work in the combine: merging two sorted halves costs O(n). The recurrence is T(n) = 2T(n/2) + O(n), giving O(n log n). Because the merge compares elements in order, the sort is stable; because it needs somewhere to write the merged output, it needs O(n) auxiliary space.

Quicksort inverts this. The divide does all the work — partitioning around a pivot costs O(n) — and the combine is nothing at all, since the parts are already in the correct relative positions. The recurrence is the same when the pivot splits evenly, giving O(n log n), but a consistently extreme pivot gives T(n) = T(n−1) + O(n) and O(n²). Partitioning in place is why it needs only O(log n) stack space and why it is fastest in practice: the partition scan is cache-friendly.

The worst case is not hypothetical. A naive first-element pivot degrades to O(n²) on already-sorted input, which is common. Median-of-three or a random pivot fixes it, and randomisation also defends against deliberately crafted adversarial input.

Binary search is the minimal case: dividing is O(1), only one subproblem is recursed into rather than both, and combining is nothing. T(n) = T(n/2) + O(1) gives O(log n).

Other members worth recognising. Karatsuba multiplication reduces four subproblems to three by an algebraic trick, taking n-digit multiplication from O(n²) to O(n^1.585). Strassen's algorithm does the same for matrices, reducing eight recursive multiplications to seven for O(n^2.807). Closest pair of points achieves O(n log n) where brute force is O(n²), with the clever part being a combine step that only examines a narrow strip around the dividing line.

  • Merge sort: trivial split, expensive merge, stable, O(n) space
  • Quicksort: expensive partition, no combine, in place, O(n²) worst case
  • Binary search recurses into only one half — O(log n)
  • Karatsuba and Strassen reduce the number of subproblems algebraically
3

Recurrences and the Master Theorem

The cost of a divide-and-conquer algorithm is expressed as a recurrence: T(n) = a · T(n/b) + f(n), where a is the number of subproblems, n/b is each one's size, and f(n) is the cost of dividing and combining.

The Master Theorem solves this family by comparing f(n) against n^(log_b a) — the total work at the leaves of the recursion tree.

Case 1: if f(n) is polynomially smaller than n^(log_b a), the leaves dominate and T(n) = Θ(n^(log_b a)). Binary tree traversal, at T(n) = 2T(n/2) + O(1), gives Θ(n).

Case 2: if they are the same order, every level costs the same and the log n levels multiply, giving T(n) = Θ(n^(log_b a) · log n). Merge sort's 2T(n/2) + O(n) gives Θ(n log n) — the most-quoted instance.

Case 3: if f(n) is polynomially larger and satisfies a regularity condition, the root dominates and T(n) = Θ(f(n)).

The theorem has real boundaries worth knowing, since exam questions often target them. It does not apply when the subproblems have unequal sizes — T(n) = T(n/3) + T(2n/3) + O(n) needs the Akra–Bazzi method or a recursion tree. It does not apply when f(n) is not polynomially comparable to n^(log_b a), as with T(n) = 2T(n/2) + n log n, which falls in the gap between cases 2 and 3.

When the theorem does not apply, draw the recursion tree: compute the work at each level and the number of levels, then sum. For merge sort, each level does O(n) work and there are log n levels — the same Θ(n log n), derived without the theorem. This method always works and is worth being able to do, since it explains why the answer is what it is rather than merely producing it.

Standard recurrences and their solutions
RecurrenceSolutionAlgorithm
T(n) = T(n/2) + O(1)O(log n)Binary search
T(n) = 2T(n/2) + O(1)O(n)Tree traversal
T(n) = 2T(n/2) + O(n)O(n log n)Merge sort, average quicksort
T(n) = T(n−1) + O(n)O(n²)Worst-case quicksort
T(n) = 3T(n/2) + O(n)O(n^1.585)Karatsuba
T(n) = 7T(n/2) + O(n²)O(n^2.807)Strassen
  • T(n) = a·T(n/b) + f(n) — subproblem count, size, and combine cost
  • Compare f(n) against n^(log_b a) to pick the case
  • Unequal subproblem sizes fall outside the Master Theorem
  • Drawing the recursion tree always works and shows why
Implementation

Divide, conquer, and combine a maximum

def maximum(a, lo, hi):
    if lo == hi:
        return a[lo]
    mid = (lo + hi) // 2
    return max(maximum(a, lo, mid), maximum(a, mid + 1, hi))

a = [2, 9, 4, 7]
print("Maximum:", maximum(a, 0, len(a) - 1))
#include <iostream>
#include <vector>
#include <queue>
#include <map>
#include <set>
#include <string>
#include <algorithm>
#include <functional>
#include <tuple>
#include <array>
#include <numeric>
using namespace std;
int maximum(vector<int>&a,int l,int r)
{
    if(l==r)return a[l];
    int m=(l+r)/2;
    return max(maximum(a,l,m),maximum(a,m+1,r));
}
int main()
{
    vector<int>a=
    {
        2,9,4,7
    };
    cout<<"Maximum: "<<maximum(a,0,3);
}
class Main
{
    static int maximum(int[]a,int l,int r)
    {
        if(l==r)return a[l];
        int m=(l+r)/2;
        return Math.max(maximum(a,l,m),maximum(a,m+1,r));
    }
    public static void main(String[]z)
    {
        int[]a=
        {
            2,9,4,7
        };
        System.out.print("Maximum: "+maximum(a,0,3));
    }
}
Watch it run

Step through it

Running on [2,9,4,7]

Output
Read all 13 Steps
  1. Whole problem Find the maximum of four values.
  2. Divide at midpoint Split into equal halves.
  3. Left subproblem Solve [2,9] independently.
  4. Divide left Split until size one.
  5. Left base 2 A one-element maximum is 2.
  6. Left base 9 The sibling base answer is 9.
  7. Combine left max(2,9)=9.
  8. Right subproblem Now solve [4,7].
  9. Divide right Split this half.
  10. Right bases 4 and 7 Base answers are 4 and 7.
  11. Combine right max(4,7)=7.
  12. Combine root max(9,7)=9 combines the two solutions.
  13. Solve recurrence T(n)=2T(n/2)+O(1)=O(n).
4

Base Cases and the Costs Complexity Hides

Two implementation matters decide whether a correct-looking recursion actually works.

Base cases must be reachable and correct. A recursion on ranges must handle the empty range and the single-element range, and it must be verified that the division genuinely shrinks every input — a range of two that splits into a range of two and an empty one never terminates. Off-by-one errors in the midpoint or in the sub-range bounds are where this fails, and testing arrays of size 0, 1, 2 and 3 catches almost all of them.

Base cases should also be tuned, not just correct. Recursing all the way to single elements pays function-call overhead for work that a simple loop does faster. Real sorting implementations switch to insertion sort below roughly 16 elements, where its small constants beat the recursion. This is why library sorts outperform textbook implementations of the same algorithm.

The drawbacks of the paradigm are worth stating plainly. Recursion costs stack space proportional to the depth — O(log n) when the division is balanced, O(n) when it degenerates, which is the case that overflows the stack. Function call overhead is a real constant factor on small inputs. And some algorithms, merge sort among them, need auxiliary memory that an iterative equivalent would not.

Not every problem suits the paradigm either. If the subproblems overlap, use dynamic programming. If the combine step costs more than the division saves, the recursion is not worth it. And if a single linear scan solves the problem, splitting it is added complexity for nothing.

A closing observation worth carrying: divide and conquer usually improves the asymptotic cost, and the practical speedup often comes from somewhere the notation does not show. Quicksort beats heap sort at the same O(n log n) because partitioning reads memory sequentially. The theory tells you which algorithms are worth implementing; measurement tells you which one to ship.

  • Verify the base case is reachable and that every split shrinks the input
  • Switch to insertion sort below ~16 elements — libraries all do
  • Recursion costs O(depth) stack; degenerate splits overflow it
  • Overlapping subproblems mean DP; a linear scan means neither