Foundations/Recurrences and the Master Theorem

Lesson 1.54,109 words

Recurrences and the Master Theorem

Recursive and divide-and-conquer algorithms describe their own running time with a recurrence: T(n)T(n) in terms of TT on smaller inputs. We solve recurrences three ways — drawing the recursion tree, guessing-and-verifying by induction, and applying the Master Theorem — using merge sort as the running example, then handle unequal splits with Akra–Bazzi.

╌╌╌╌

When an algorithm solves a problem by calling smaller copies of itself, its running time obeys an equation that refers to itself: the cost on an input of size is some local work plus the cost of the recursive calls on smaller inputs. Such an equation is a recurrence. Counting loops, as in the previous lesson, no longer suffices; we need techniques to turn a recurrence into a closed -bound. This lesson develops three, in increasing order of power and precision, and closes with the Akra–Bazzi method for the uneven splits the Master Theorem cannot handle.

From a recursive algorithm to a recurrence

is the paradigm CLRS, Skiena, and Erickson all use to introduce recurrences. It has three steps: divide the instance into subproblems, conquer them by recursion, and combine their solutions. Merge sort splits the array in half, sorts each half recursively, and merges the two sorted halves.

Algorithm 1:Merge-Sort(A,p,r)\textsc{Merge-Sort}(A, p, r) — sort A[p..r]A[p..r]
  1. 1
    if p<rp < r then
  2. 2
    q(p+r)/2q \gets \floor{(p + r) / 2}
    midpoint
  3. 3
    call Merge-Sort(A,p,q)\textsc{Merge-Sort}(A, p, q)
    sort left half
  4. 4
    call Merge-Sort(A,q+1,r)\textsc{Merge-Sort}(A, q+1, r)
    sort right half
  5. 5
    call Merge(A,p,q,r)\textsc{Merge}(A, p, q, r)
    combine halves
  6. 6
    return AA

The subroutine walks the two sorted halves with two pointers, repeatedly copying the smaller front element into the output. It touches each of the elements a constant number of times, so it costs .

Now read the cost off the structure. On an array of size :

  • Divide is computing the midpoint, .
  • Conquer is two recursive calls, each on elements, costing .
  • Combine is the merge, .

Adding these (and noting that a one-element array is sorted at no cost) gives the recurrence

We can write this compactly, and as an inequality (since the combine step costs at most linear), as

and the bulk of the work is justifying that implication. This is the equation we must solve. (We freely write rather than and ; the floors and ceilings change the answer by lower-order amounts that the asymptotics absorb. Skiena and CLRS both justify dropping them.1) Throughout we also assume a constant base case, which lets us ignore the boundary condition when finding the asymptotic order.

Method 1: the recursion tree

The most intuitive method draws the recurrence. Each node is a subproblem labeled with the non-recursive work it does; its children are the subproblems it spawns. Summing all node labels gives .

For merge sort, the root does work and has two children of size . Each of those does work and has two children of size , and so on, until the leaves are size- subproblems.

Recursion tree for merge sort, . The right-hand column sums each level: nodes of size , each doing work, give at every level.

Every level sums to . The root level is ; the next level is ; the level below is . The subproblem sizes shrink by half each level, so the tree has levels (from size down to size ), and the bottom level holds the size- leaves. Therefore

This is the result: merge sort runs in time, strictly better than insertion sort's .2 The recursion tree also exposes why: the per-level work stays flat at while the depth is only logarithmic.

The tree is a derivation, not yet a proof — it asks us to trust the level sums and the level count. When a recurrence is irregular (unequal splits, work that isn't a clean power of ), the tree still gives a reliable guess, which we then certify with the next method.

recurrence_tree.pypython
import math
from dataclasses import dataclass

@dataclass(frozen=True)
class TreeLevel:
  """
    One level of the recursion tree.\n
    `depth` is 0 at the root; `node_count` is a^depth subproblems;\n
    `subproblem_size` is n / b^depth; `work` is the total non-recursive\n
    work summed across that level's nodes.\n
  """

  depth: int
  node_count: int
  subproblem_size: float
  work: float

@dataclass(frozen=True)
class RecursionTree:
  """
    The fully expanded tree for one input size.\n
    `levels` runs root-to-leaves; `total_work` sums their work; `depth` is\n
    the count of internal (non-leaf) levels, i.e. floor(log_b n); and\n
    `leaf_count` is the number of size-1 subproblems at the bottom.\n
  """

  levels: list[TreeLevel]
  total_work: float
  depth: int
  leaf_count: int

def build_recurrence_tree(
  input_size: int,
  subproblems: int,
  shrink_factor: int,
  combine_coefficient: float = 1.0,
  combine_exponent: float = 1.0,
) -> RecursionTree:
  """
    Expand T(n) = a T(n/b) + f(n) for n = `input_size`, with a =\n
    `subproblems`, b = `shrink_factor`, and a polynomial combine cost\n
    f(m) = `combine_coefficient` * m^`combine_exponent`.\n
    The tree descends until subproblems reach size 1, summing the work of\n
    every node so the total equals the recurrence's value at `input_size`.\n
  """
  # reject inputs the recurrence isn't defined for.
  if input_size < 1:
    raise ValueError("input size n must be >= 1")
  if subproblems < 1:
    raise ValueError("number of subproblems a must be >= 1")
  if shrink_factor < 2:
    raise ValueError("shrink factor b must be an integer >= 2")

  # accumulators for the expanded tree.
  levels: list[TreeLevel] = []
  total_work: float = 0.0

  # walk state: root is one node of size n at depth 0.
  current_size: float = float(input_size)
  node_count: int = 1
  depth: int = 0

  # expand internal levels: stop once subproblems have shrunk to size 1.
  while current_size > 1:
    # this level's nodes each pay f(size); sum it across the level.
    work_per_node: float = combine_coefficient * (current_size**combine_exponent)
    level_work: float = node_count * work_per_node

    levels.append(
      TreeLevel(
        depth=depth,
        node_count=node_count,
        subproblem_size=current_size,
        work=level_work,
      )
    )
    total_work += level_work

    # descend: a-fold more nodes, each b times smaller.
    node_count *= subproblems
    current_size /= shrink_factor
    depth += 1

  # the leaf level: size-1 subproblems each costing a constant (taken as 1).
  leaf_count: int = node_count
  leaf_work: float = float(leaf_count)

  levels.append(
    TreeLevel(
      depth=depth,
      node_count=leaf_count,
      subproblem_size=1.0,
      work=leaf_work,
    )
  )
  total_work += leaf_work

  return RecursionTree(
    levels=levels,
    total_work=total_work,
    depth=depth,
    leaf_count=leaf_count,
  )

def per_level_work(tree: RecursionTree) -> list[float]:
  """
    The work at each level, root first — the column the lesson sums.\n
    For merge sort (a = b = 2, f(n) = n) every internal level equals n;\n
    for a root-heavy recurrence it shrinks geometrically from the root.\n
  """
  return [level.work for level in tree.levels]

def tree_depth(input_size: int, shrink_factor: int) -> int:
  """
    The number of times n can be divided by b before reaching 1,\n
    i.e. floor(log_b n) — the count of internal levels in the tree.\n
  """
  if input_size < 1:
    raise ValueError("input size n must be >= 1")
  if shrink_factor < 2:
    raise ValueError("shrink factor b must be an integer >= 2")
  return int(math.floor(math.log(input_size, shrink_factor)))

Method 2: substitution (guess and verify)

The substitution method is the rigorous one: guess the form of the answer, then prove it by induction on . It is the only method that always works, and the only one that produces a complete proof.

We verify the guess for the merge-sort recurrence .

The substitution method: assume the bound on smaller inputs, substitute into the recurrence, and check that the leftover residual term lets the same bound re-emerge for .

A symmetric argument with the inequality reversed gives , and together they yield , confirming the tree.

Two warnings the standard references repeat:

  • Guess the right form. Substitution verifies a guess; it cannot invent one. Use the recursion tree (or the Master Theorem below) to find the candidate.
  • Land on the exact bound. The inductive step must end at the same inequality it assumed, with the same constant. Close enough plus a lower-order term is not a proof, as the next example shows.

A failing guess, and why it fails

Watch the method reject a wrong answer. Take the same recurrence, , and guess ; concretely, try to prove for some constant . Substitute the hypothesis :

It is tempting to declare victory here: , so we are done. That reasoning is circular hand-waving, and CLRS singles it out as the classic substitution error.3 The induction committed to the exact statement with one fixed constant that works for every . The step must therefore arrive at on the nose, and

which never holds. No choice of , however large, absorbs the leftover ; making bigger inflates both sides equally. The induction is stuck, and it is stuck for a good reason: the claim is false. We already know , which is not . The failed algebra is the method working as designed — a wrong guess leaves a residual that cannot be paid for.

Anatomy of a failed guess for . Guessing leaves a residual that no constant absorbs; the escape is a stronger hypothesis: raise the guess's order, or subtract a lower-order term.

The escape is to strengthen the guess. For this recurrence the honest fix is to raise its order to , reproducing the proof carried out above: the substitution then produces the residual , which is negative for and absorbs the linear term.

Strengthening by subtracting a lower-order term

A subtler failure mode: the guess has the right order and still gets stuck. Consider

The tree says : the per-level work is , a geometric series dominated by its last term, the leaves. So guess and substitute:

Off by — and no constant kills a leftover that survives every doubling of , for the same reason as before. Yet the guess's order is correct. The fix, which CLRS presents with this exact recurrence, is counterintuitive: strengthen the claim by subtracting a lower-order term.3 Guess

Substituting the stronger hypothesis on :

whenever . Choosing (and large enough to cover the base case) completes the induction. The stronger hypothesis helps rather than hurts because it is assumed on the subproblems too: each of the two recursive calls brings a credit, and the two credits pay for the of local work with one to spare. Proving less was impossible; proving more is easy.

substitution_method.pypython
import math
from dataclasses import dataclass
from typing import Callable

# floating residual comparisons tolerate tiny rounding either side of zero.
_TOLERANCE: float = 1e-9

@dataclass(frozen=True)
class StepFailure:
  """
    A single size where the inductive step failed.\n
    `size` is the n that broke; `recurrence_value` is a guess(n/b) + f(n);\n
    `guess_value` is guess(n); the step needs the former <= the latter.\n
  """

  size: float
  recurrence_value: float
  guess_value: float

@dataclass(frozen=True)
class SubstitutionReport:
  """
    The outcome of checking a guessed bound by substitution.\n
    `holds` is True when the inductive step held at every tested size;\n
    `failures` lists the offending sizes otherwise; `max_residual` is the\n
    largest value of (recurrence_value - guess_value) seen, which is <= 0\n
    exactly when the guess survives.\n
  """

  holds: bool
  failures: list[StepFailure]
  max_residual: float

def verify_substitution(
  guess: Callable[[float], float],
  subproblems: int,
  shrink_factor: float,
  combine: Callable[[float], float],
  sizes: range,
) -> SubstitutionReport:
  """
    Check the inductive step of the substitution method for the guessed\n
    upper bound `guess` against T(n) = a T(n/b) + f(n), where a =\n
    `subproblems`, b = `shrink_factor`, and f = `combine`.\n
    For each n in `sizes` it forms the substituted recurrence value\n
    a * guess(n/b) + f(n) and compares it to guess(n); the guess is\n
    certified when that residual stays <= 0 throughout.\n
  """
  # reject parameters outside the recurrence's domain.
  if subproblems < 1:
    raise ValueError("number of subproblems a must be >= 1")
  if shrink_factor <= 1:
    raise ValueError("shrink factor b must be > 1")

  # track the worst residual and the sizes where the step broke.
  failures: list[StepFailure] = []
  max_residual: float = -math.inf

  for size in sizes:
    # substitute the guess into the recurrence and measure the residual.
    recurrence_value: float = (
      subproblems * guess(size / shrink_factor) + combine(float(size))
    )
    guess_value: float = guess(float(size))
    residual: float = recurrence_value - guess_value
    max_residual = max(max_residual, residual)

    # a positive residual means the step broke at this size.
    if residual > _TOLERANCE:
      failures.append(
        StepFailure(
          size=float(size),
          recurrence_value=recurrence_value,
          guess_value=guess_value,
        )
      )

  return SubstitutionReport(
    holds=not failures,
    failures=failures,
    max_residual=max_residual,
  )

def smallest_constant_for_n_log_n(
  combine_coefficient: float, shrink_factor: float = 2.0
) -> float:
  """
    The least constant d for which the guess T(n) <= d*n*log2(n) survives\n
    the inductive step of the merge-sort recurrence T(n) = 2 T(n/2) + c*n.\n
    The lesson's algebra leaves the residual -(d - c)*n, non-positive\n
    exactly when d >= c, so the smallest working d equals the combine\n
    coefficient c. (Generalized: for b subproblems halving, d >= c / log2 b.)\n
  """
  # reject parameters outside the recurrence's domain.
  if combine_coefficient < 0:
    raise ValueError("combine coefficient c must be >= 0")
  if shrink_factor <= 1:
    raise ValueError("shrink factor b must be > 1")

  # residual -(d - c/log2 b)*n vanishes exactly when d hits this value.
  return combine_coefficient / math.log2(shrink_factor)

A second example: counting inversions

A second divide-and-conquer problem makes the point sharply. Its recurrence has the same shape as merge sort but a different combine cost, and the combine cost is the thing you must get right. An inversion of a list is a pair with but ; the number of inversions measures how far from sorted the list is (a sorted list has , a reversed list has ). The task: given , return .

The brute-force algorithm compares every pair and runs in . To beat it, mimic merge sort: split in half, recursively count inversions inside each half, then count the cross inversions, the pairs with one element in the left half and one in the right. That gives a recurrence of the merge-sort form,

The three kinds of inversion partition cleanly along the split. On , the inversions within each half are counted by recursion; the cross pairs, a left element greater than a right element, are what the combine step must tally.

Cross inversions on split into halves and . Each red arc is a cross inversion (a left element bigger than a right one): . Within-half inversions are handled by recursion; the combine step counts only these crossing pairs.

Counting cross inversions naively, with a double loop over the two halves, costs , so the recurrence becomes . Feed that to the recursion tree: the per-level work is now , which shrinks geometrically, so the root dominates and the tree sums to . That is no improvement. The split bought us nothing because the combine step is as expensive as the brute force.

Naive counting-inversions tree, : unlike merge sort's flat tree, the level sums shrink geometrically, so the root dominates and the total is .

The recurrence therefore sets a requirement: the combine step must run in , not . If we can count cross inversions in linear time, which one can, by counting them while merging the two sorted halves, the recurrence collapses to , the merge-sort recurrence, and we get . The recurrence both predicts the running time and tells you precisely how fast the combine step has to be for divide-and-conquer to pay off.

Method 3: the Master Theorem

Merge sort's recurrence is one instance of a common pattern. The Master Theorem solves every recurrence of the form

where and are constants and is the divide-and-combine work. Here is the number of subproblems, is each subproblem's size, and is the work done outside the recursion.

The theorem compares against the watershed function, the total cost of the leaves, which equals the number of leaves times the constant base-case cost. Which of the two dominates determines the answer.

The Master Theorem weighs the root's combine work against the leaves' total cost, the watershed : each leaf costs , and there are of them.

The intuition matches the recursion tree. Compare the work at the root, , to the work at the leaves, . In Case 1 the tree is leaf-heavy and the answer is the leaf count. In Case 3 the root work dwarfs everything below it and the answer is . In Case 2 the work is spread evenly across all levels, as we saw for merge sort, giving the extra factor.

Where the work concentrates in the Master Theorem's three cases. The answers, left to right: , , and .

Each panel stacks the per-level work from root (top) to leaves (bottom); the bar width is the work at that level. The case is decided by which end is heavier.

Why the cases hold: three trees

The theorem is a statement about geometric series, and the recursion tree makes the series visible.4 Unroll : level of the tree holds subproblems of size , each contributing of non-recursive work, so

and the leaf level contributes . Summing,

When is a polynomial, the level sums take a clean form:

a geometric series with ratio . Everything reduces to whether is above, at, or below — equivalently, whether is below, at, or above . Skiena states the theorem in exactly this three-way form.5

Case 1, leaves dominate (). Take : here , , , so . Reading the tree level by level:

doubling every level. A growing geometric series is dominated by its last term, so the total is within a constant factor of the bottom:

which matches the leaf level: leaves at each. The combine work is irrelevant; the answer is the leaf count, .

Case 1 tree for . Each node spawns four children of half the size, so the level sums double all the way down; the growing geometric series is dominated by its last term, the leaves. Total: .

Case 2, balanced (). Merge sort, : , , , so . This is the first tree we drew. The level sums are

— constant at for all levels. A flat series is just (number of terms) (term), so

Neither end of the tree wins; the factor is the number of levels, each pulling equal weight.

Case 3, root dominates (). The naive inversion-counting tree, : , , , so . The level sums

halve every level. A shrinking geometric series is dominated by its first term and bounded by a constant multiple of it:

so . The root alone already costs ; the entire tree below it costs at most as much again.

The ratio test doubles as a sanity check on concrete instances. For : , Case 1, answer . For : , Case 2, . For : , Case 3, .

Regularity and the gaps between the cases

Two fine-print clauses matter in practice.

The regularity condition. Case 3 additionally demands for some constant : the combine work one level down must be a constant factor smaller, which is precisely what makes the level sums a shrinking geometric series. For any polynomial that satisfies Case 3's growth bound the condition holds automatically — as in Example 4 below, where . It can fail only for contrived oscillating functions that are periodically tiny one level down; CLRS relegates such to the exercises.4 If regularity fails, the theorem does not apply and you must sum the tree by hand.

The gaps. The three cases do not cover every .4 Case 1 needs polynomially smaller than the watershed (smaller by a factor ), and Case 3 polynomially larger; a merely logarithmic separation falls into the crack between the cases. The standard example:

The watershed is , and is bigger than but not bigger by any — for every , . Case 2 fails since ; Case 3 fails since . The basic Master Theorem simply does not apply. The recursion tree still works: level sums to , so

— the sum is arithmetic, totaling . So the answer picks up a squared log, which none of the three cases predicts. (CLRS's chapter notes discuss extended versions that handle ; for this course, fall back to the tree is the reliable rule.)

Worked examples

Example 1, merge sort. . Here , , so . And , which is Case 2. Therefore

recovering exactly what the tree and substitution gave.

Example 2, binary search.: one subproblem of half size, constant work to pick the side. Here , , so . Then , Case 2 again, and

Example 3, leaf-dominated. . Now , , so . The combine work (take ) is polynomially smaller than the watershed, which is Case 1, so

The recursion has so many leaves ( of them) that they dominate the modest linear work per level.

Example 4, root-dominated. . Here , , watershed . The combine work is polynomially larger, a Case 3 candidate. Check regularity: with . Regularity holds, so

The root's quadratic work swamps the tree beneath it.

Unequal splits and Akra–Bazzi

The Master Theorem requires every subproblem to have the same size . Divide-and-conquer algorithms do not always split evenly: a partition step can split elements into a third and two-thirds, giving

No single fits, so the theorem does not apply. The recursion tree still works. Each node of size does work and splits into children of sizes and — which together are all of again. So every level where no branch has bottomed out sums to exactly ; once leaves start dropping out, levels sum to at most .

Recursion tree for . The two children of a size- node have sizes summing to , so every full level again sums to . The shallowest branch (all thirds) dies at depth , the deepest (all two-thirds) at depth ; both are , so .

The tree's depth is no longer uniform. The leftmost branch divides by each step and reaches size at depth ; the rightmost divides by only and survives until depth . Both depths are — logarithms to different constant bases differ by a constant factor — so

and substitution certifies the guess in the usual way. Erickson works this recurrence as the standard example of a tree the Master Theorem cannot handle.6

For a general tool, the Akra–Bazzi method solves the whole family

with different-sized subproblems and reasonable . Stated without proof: find the unique exponent with ; then

For the balance equation is , satisfied by (a third plus two-thirds is one). The integral is , so , agreeing with the tree. The method also handles floors, ceilings, and small perturbations of the subproblem sizes, which is why its answer can be trusted for the real -style recurrences that code produces. CLRS's chapter notes present Akra–Bazzi as the standard generalization of the Master Theorem;7 at this course's level, the balance-equation-plus-integral recipe is all you need, with the tree as a cross-check.

Choosing a method

The methods are complementary, and Erickson in particular urges fluency with all of them:8

  • Recursion tree: fastest for building intuition and guessing the answer; shows where the work concentrates, and handles uneven splits.
  • Master Theorem: fastest for getting the answer when the recurrence fits the template; no derivation needed, but it has gaps.
  • Akra–Bazzi: the heavier tool for unequal subproblem sizes, such as ; solve the balance equation, evaluate one integral.
  • Substitution: the rigorous method that always works and produces a proof; use it to certify a guess, or when the others do not apply.

In practice: sketch the tree to guess, apply the Master Theorem if it fits, and reach for substitution whenever you need a guarantee rather than a hunch.

Recurrences of other shapes

The recurrences here shrink by a constant factor, the divide-and-conquer signature. Linear recurrences with constant coefficients, like (Fibonacci), instead yield to their characteristic equation, whose roots give the closed form — Fibonacci's dominant root is the golden ratio, so .9 And the Akra–Bazzi method generalizes to the Akra–Bazzi–Leighton form, which admits lower-order perturbations inside each recursive call, putting the floor/ceiling hand-waving on rigorous footing.10 For anything that fits none of these, the recursion tree plus a substitution proof never stops applying.

Takeaways

  • A recursive algorithm induces a recurrence: = local work + cost of recursive calls on smaller inputs. Merge sort gives .
  • The recursion tree sums the per-node work; for merge sort every level costs across levels, giving .
  • Substitution guesses the form and proves it by induction; it is the only always-applicable, fully rigorous method. The step must land on the exact bound with the same constant — , which is is not a proof. Strengthen the hypothesis (raise the order, or subtract a lower-order term as in ) if a residual blocks the step.
  • The combine cost drives the answer. Counting inversions has the merge-sort shape , but a naive combine gives overall, for no gain. Only a linear combine recovers .
  • The Master Theorem solves by comparing to the watershed : leaves win (Case 1), they tie (Case 2, extra ), or the root wins (Case 3, needs regularity). Behind each case is a geometric series of level sums ; for the ratio against decides the case in one division.
  • The cases have gaps; when is only non-polynomially separated from the watershed, as in (which sums to ), fall back to the tree or substitution.
  • Unequal splits like escape the Master Theorem but not the tree: full levels still sum to over depth, giving . Akra–Bazzi generalizes: solve for , then integrate .

Footnotes

  1. Skiena, §2.7–2.10 — Logarithms, Recurrences, Divide-and-Conquer: justification for dropping floors and ceilings in recurrences since they perturb the answer by lower-order amounts.
  2. CLRS, Ch. 4 — Divide-and-Conquer: the recursion-tree derivation that merge sort runs in time.
  3. CLRS, Ch. 4 — Divide-and-Conquer: the substitution method's pitfalls — the , hence fallacy of not proving the exact inductive form, and the subtract-a-lower-order-term fix for . 2
  4. CLRS, Ch. 4 — Divide-and-Conquer: the Master Theorem for , the recursion-tree proof over the level sums , the regularity condition, and the gaps where the theorem does not apply. 2 3
  5. Skiena, §2.10 — Divide-and-Conquer Recurrences: the Master Theorem stated by comparing against , i.e. the ratio against .
  6. Erickson, Algorithms, Ch. 1 and the appendix on solving recurrences: level-by-level analysis of the uneven-split tree .
  7. CLRS, Ch. 4 chapter notes — the Akra–Bazzi method for divide-and-conquer recurrences with unequal subproblem sizes: the balance equation and the integral form of the solution.
  8. Erickson, Algorithms, Ch. 1–2 — Recursion; Backtracking & Divide-and-Conquer: the case for fluency with recursion trees, substitution, and the Master Theorem as complementary methods.
  9. CLRS, Ch. 4 problems and Appendix — linear recurrences and the characteristic-equation method; the Fibonacci recurrence has closed form with .
  10. Leighton, T. (1996). Notes on better master theorems for divide-and-conquer recurrences. — the Akra–Bazzi–Leighton generalization admitting lower-order perturbations (floors/ceilings) inside each subproblem.
Practice

╌╌ END ╌╌