Module 02 — Mathematical Preliminaries
Source: The Art of Computer Programming, Vol. 1, 3rd ed., §1.2 — specifically §1.2.1 (induction), §1.2.3 (sums), §1.2.6 (binomial coefficients), §1.2.7 (harmonic numbers), §1.2.8 (Fibonacci numbers), §1.2.10 (analysis of an algorithm), and §1.2.11.1 (O-notation). Lab:
labs/module-02-math· Grade it:./grade 2This lesson is self-contained: you can complete the module without the book. If you own Vol. 1, read the sections above alongside — the lesson tells you where each idea lives.
Companion: this §1.2 is exactly what Concrete Mathematics (Graham–Knuth–Patashnik) expands into a whole course. If any tool here whets your appetite, ../../docs/concrete-mathematics.md maps the two together.
Module 01 ended with a promise: , the division count of Euclid's algorithm, was your first cost function, and we said Module 02 would build the toolkit that makes such functions tame. This is that toolkit. It culminates in the first complete algorithm analysis of the book — Knuth's Algorithm M for finding the maximum, whose "interesting quantity" turns out to average exactly , a harmonic number. Everything in this module (induction, closed-form sums, binomial identities, harmonic numbers, Fibonacci numbers, O-notation) is a tool you will reach for again in every later module, from quicksort's average case to hashing's load factors.
1. Mathematical induction (§1.2.1)
The proof pattern you used for Euclid's algorithm — an invariant plus a decreasing quantity — is mathematical induction wearing work clothes. The formal template: to prove a statement for all integers ,
- Basis. Prove .
- Induction. Prove that together imply .
Allowing all earlier cases (not just ) costs nothing and is often what you need — Knuth states it this way from the start (it is sometimes called strong induction, but it is the same principle).
Worked example. : . Basis: . Induction: assuming the sum of the first odd numbers is , adding the next odd number gives . ∎
The deeper point of §1.2.1 — worth internalizing now — is that induction is the method for proving algorithms correct. Knuth demonstrates it on extended Euclid (your Module 01, Stage 3): attach an assertion to each arrow of the flowchart, check that each step carries its incoming assertion to its outgoing one, and induction on the number of steps executed does the rest. Robert W. Floyd and C. A. R. Hoare later built program-verification logics on exactly this idea. Every invariant comment you write in this course is a little induction proof waiting to be completed.
2. Sums and the perturbation method (§1.2.3)
Analyses of algorithms produce sums; you need to turn sums into closed forms. Three you should know cold, and one method that generates such formulas on demand.
The arithmetic sum. . Pair the first and last terms (Gauss's schoolroom trick), or induct. Note the closed form is exact integer arithmetic: one of , is even, so the division by 2 is exact.
Sum of squares. .
Sum of cubes (Nicomachus). — the sum of the first cubes is the square of the -th triangular number. Induction makes short work of it: adding to and factoring gives .
The perturbation method
Rather than guess-and-induct, §1.2.3 teaches a machine for deriving closed forms: compute two ways, by splitting off the last term and by splitting off the first, then solve the resulting equation.
Derivation of the geometric sum :
split off the last term: S_{n+1} = S_n + x^(n+1)
split off the first term: S_{n+1} = 1 + x·(1 + x + ... + x^n) = 1 + x·S_n
equate and solve: S_n + x^(n+1) = 1 + x·S_n
S_n·(x − 1) = x^(n+1) − 1
S_n = (x^(n+1) − 1) / (x − 1) for x ≠ 1.
For the equation degenerates (both sides say ) and the answer is simply . The same two-way-split trick, applied to , produces that closed form too — try it (exercise table below). The division above is exact in the integers: divides because makes .
How to prove without inspiration: perturb . Split at both ends, expand , and the cubes cancel, leaving a linear equation for . Perturbation converts "I need a formula for degree " into "I know the formula for degree 's telescoping" — a genuinely mechanical method.
3. Binomial coefficients (§1.2.6)
Knuth calls — " choose ", written with over in parentheses — the most important quantity in the analysis of algorithms. Definition, for integers :
and when (no way to choose more things than you have). Combinatorially: the number of -element subsets of an -element set.
Four identities carry most of the weight. All have one-line combinatorial proofs — learn those, not the algebra:
Symmetry. . Choosing which elements are in is the same as choosing which are out.
Pascal's rule. . Fix one element : the -subsets either contain (choose the other from ) or they don't (choose all from ). This addition formula generates Pascal's triangle, and gives an induction proof that every is an integer — not obvious from the factorial quotient!
Row sums (the binomial theorem at ). : every subset has some size. With alternating signs, for — set , in .
Vandermonde's convolution. . Proof sketch: a committee of people drawn from men and women must contain men and women for exactly one ; sum over the ways. Or, in generating-function clothing: , and match the coefficient of on both sides — the left side's coefficient is exactly the convolution sum. This is your first taste of the generating- function method, which reappears in the analysis of Algorithm M below.
Computing C(n, k) exactly
The factorial formula overflows absurdly early () even when the answer is small. Two exact strategies:
- Multiplicative: evaluate for , left to right. Invariant: after step , — an integer by Pascal's-rule induction — so every division is exact. Multiply before dividing. Apply symmetry first so .
- Pascal: build triangle rows by addition only. Slower, but division-free.
Either is exact in u128 for all ; the largest value you'll meet is
4. Harmonic numbers (§1.2.7)
Define . These fractions are the fingerprint of divide-by-rank behavior — they appear whenever "the -th item matters with weight ", which in practice is constantly (quicksort, records in sequences, coupon collecting, treaps…).
Small values, exactly: , , , , , . Note the denominators are lcm-like and grow fast; in lowest terms is .
is unbounded — but barely. Group the terms in blocks of lengths :
H_{2^k} = 1 + (1/2) + (1/3 + 1/4) + (1/5 + ... + 1/8) + ...
Each parenthesized block of terms is at most and at least , so
Sandwiching between logarithms: grows like . The precise statement (§1.2.7, Eq. (3)) is one of Euler's gems:
where is Euler's constant — a number so mysterious that whether it is irrational is still an open problem. Your Stage 4 tests verify this expansion numerically: the error of the two-term approximation is squeezed between and .
"Almost log": for algorithm analysis, remember . When you see an expected cost of , read "natural log of , plus 0.577".
5. Fibonacci numbers (§1.2.8)
, , — Knuth's indexing, which we use everywhere (beware off-by-one against other books). First values:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 | 55 | 89 | 144 | 233 | 377 | 610 |
You met them already: consecutive Fibonacci numbers are the worst case of Euclid's algorithm (Lamé, Module 01, Stage 4). They also govern Fibonacci heaps, polyphase merge sorting (Vol. 3), and Fibonacci hashing (Vol. 3, §6.4).
Binet's closed form. Let (the golden ratio) and , the two roots of . Then
Proof sketch: both and satisfy the recurrence (because , same for ), hence so does any linear combination; choose the coefficients to match and , and induction finishes it. Since , the second term dies away geometrically, giving the remarkable statement
so Fibonacci growth is exponential with base — that is why
already exhausts a u128.
Identities you will implement against (all provable by induction):
- Addition law (§1.2.8, Eq. (6)): . Setting yields the fast doubling formulas and , which compute in arithmetic operations — worth trying as an extension.
- Cassini's identity (Eq. (8)): . (This is the algebra behind the classic -square-into--rectangle "paradox".)
- gcd law: — the Fibonacci sequence is a "strong divisibility sequence". Proof sketch: the addition law gives ... iterating, runs through a Euclid-like descent on the indices, so the gcd computation on values mirrors the gcd computation on subscripts. It is Euclid's algorithm and induction, twice over.
6. O-notation in one page (§1.2.11.1)
means: there exist a constant and a threshold with for all . Read as "some quantity we won't name, bounded by a constant times ".
The three rules of hygiene:
- Equations with O are one-way. is true; is nonsense. The convention: the left side is more specific than the right. You may transform left-to-right only.
- Constants are absorbed: ; ; ; is sharper than — carry the most precise form your argument can afford.
- O is an upper bound only. To say quicksort takes order on average you want (matching upper and lower bounds); O alone permits wild overestimates.
From this module's inventory: ; ; (via Stirling, §1.2.11.2); and — coming right up — the running time of Algorithm M is where only varies with the arrangement of the data.
7. Algorithm M and its analysis (§1.2.10)
Here everything converges. The algorithm itself is almost insultingly simple — find the maximum of n values — but Knuth chose it precisely so the analysis could be carried out completely, and it introduces the questions this whole book asks of every algorithm: what is the best case, the worst case, the average case, and the distribution around it?
Algorithm M (Find the maximum)
Given elements with , find and such that , where is the largest index attaining the maximum.
M1. [Initialize.] Set j <- n, k <- n - 1, m <- X[n].
M2. [All tested?] If k = 0, the algorithm terminates.
M3. [Compare.] If X[k] <= m, go to M5.
M4. [Change m.] Set j <- k, m <- X[k].
(m is the new current maximum.)
M5. [Decrease k.] Decrease k by one, return to M2.
Note the two conventions with teeth: the scan runs from the right end
leftward, and M3's test is <= — a tie does not displace the current
maximum, which is how j ends up as the largest maximizing index.
Hand trace on , (indices 1-based as in the book; your Rust returns ):
| step | k | X[k] | compare | j | m | A so far |
|---|---|---|---|---|---|---|
| M1 | — | — | — | 6 | 3 | 0 |
| M3 | 5 | 8 | 8 > 3 → M4 | 5 | 8 | 1 |
| M3 | 4 | 4 | 4 ≤ 8 | 5 | 8 | 1 |
| M3 | 3 | 9 | 9 > 8 → M4 | 3 | 9 | 2 |
| M3 | 2 | 2 | 2 ≤ 9 | 3 | 9 | 2 |
| M3 | 1 | 7 | 7 ≤ 9 | 3 | 9 | 2 |
| M2 | 0 | — | terminate | 3 | 9 | 2 |
The analysis
Count each step's executions. M1 runs once; M2, M3, M5 run a fixed number of times (M3 runs exactly times — every element except the last is compared exactly once). The only data-dependent quantity is
Best case (the maximum is already at position ); worst case (strictly decreasing input, so every comparison wins). But what is on average, over the orderings of distinct values, each equally likely?
The key observation. Step M4 fires at position exactly when is larger than everything to its right: . Call such a position a right-to-left maximum. Position is always one (it seeds in M1, and does not count toward ); so
. For each , look at the values . In a random permutation, each of them is equally likely to be the largest of the group, so
By linearity of expectation (indicators for ):
The average number of maximum-updates is logarithmic — that is harmonic numbers earning their keep. For , ; for , about 6.49. (Aside: the indicator events are in fact independent — knowing that beats everything to its right says nothing about the relative order within — which makes the next two results clean.)
The full distribution. Knuth pushes on to , and the generating function is beautiful. Let and . Condition on where the smallest element sits, or directly multiply the independent indicators: position contributes a factor — "no new maximum" with probability , "new maximum" (one factor of ) with probability . Hence
Sanity checks: ✓; the mean is ✓; the variance comes out to where (so the standard deviation is about — is tightly concentrated). Expanding the numerator polynomial defines the Stirling cycle numbers (Stirling numbers of the first kind), so
For : the counts of permutations with are exactly 120, 274, 225, 85, 15, 1 (total ) — your Stage 5 verifies this histogram by brute force over all 720 permutations, then confirms by Monte Carlo at . When a computation, a theorem, and a simulation all agree, you may start to believe all three.
8. Stage-by-stage lab guide
Open labs/module-02-math/src/lab.rs. Each stage below has a test file
labs/module-02-math/tests/stage_NN_*.rs; run ./grade 2 to take the
stages in order. Everything is exact u128/i128 integer arithmetic except
harmonic_f64. You may add private helpers (you will want a gcd).
Stage 1 — sum_first_n, sum_squares, sum_cubes, geometric_sum (§1.2.3)
Closed forms only — the tests call sum_first_n(10^12) and friends, where a
loop would take hours. Mind two things: exactness of division (order
operations so each division is exact: first, and for squares
multiply that by before dividing by 3) and the case of the
geometric sum, where the closed form divides by zero. geometric_sum(x, n)
sums for (so terms, and even at ). The
tests re-derive the perturbation identities
— if your function satisfies both, you have re-proved the closed
form.
Stage 2 — binomial (§1.2.6)
Exact in u128, zero when , no overflow anywhere for .
Use the multiplicative method (multiply-then-divide, symmetry first) or
build Pascal's triangle. The tests check Pascal's rule, symmetry, row sums
, alternating sums, Vandermonde's convolution over a grid, and the
anchor value — any f64 detour or premature division dies there.
Stage 3 — fibonacci (§1.2.8)
exact for (the u128 limit); panic with a message containing
"overflows" beyond that. A simple iteration is fine — but be careful not to
compute when ! The tests verify the table, , the addition law, Cassini, the gcd law over a
grid, and Binet's rounding property. Fast doubling is an optional flex.
Stage 4 — harmonic, harmonic_f64 (§1.2.7)
harmonic(n) returns as a reduced fraction (numerator, denominator).
Accumulate and reduce by the gcd
every step — unreduced denominators reach by and
things only get worse. Panic for (message containing "n >= 1").
harmonic_f64 is a plain float loop; summing from down to 1 keeps
rounding error negligible. The asymptotic test brackets
between and for up to —
Euler's expansion, verified on your own machine.
Stage 5 — find_max, find_max_counting (Algorithm 1.2.10M)
Implement Algorithm M step-faithfully: scan right-to-left, replace
only on a strict increase (M3 tests ), return the 0-based index
(so Knuth's minus one) — ties keep the rightmost maximum, and the tests
check exactly that. find_max_counting additionally returns , counting
executions of M4 only (M1's initialization is not a change). Then the tests
run §1.2.10's analysis against your code: the exact distribution
(120, 274, 225, 85, 15, 1) over all permutations of six elements, and a
10,000-trial Monte Carlo at whose mean must land within 0.08 of
. Panic on the empty slice (message containing "n >= 1").
9. Check your understanding
Answer before moving on (no code needed):
- The perturbation method applied to needs the geometric sum as an input. Derive the closed form and check it at .
- Why is the multiplicative method's division
c * (n-k+i) / ialways exact, and why wouldc * ((n-k+i) / i)be wrong? - (Two terms of the expansion suffice: .) How many terms of the harmonic series until the sum exceeds 100? (About — "unbounded, but barely.")
- In Algorithm M, why does need the values to be distinct? What happens to if all elements are equal?
- If Algorithm M scanned left-to-right instead (with vs chosen to keep the leftmost maximum), would the distribution of change? (No — by symmetry of random permutations under reversal.)
10. Exercises from the text
Ratings use Knuth's scale: 00 immediate · 10 a minute · 20 fifteen minutes
to an hour · 30 hours · 40 term project · 50 open research problem; M =
mathematically oriented, ▶ = especially instructive. Statements are
paraphrased; match by content if your printing numbers differ. Log your work
in course/module-02-math/exercises.md.
| Ex. | Rating | Statement (paraphrased) |
|---|---|---|
| 1.2.1-8 | M25 | Fermat's little theorem territory: prove for prime , by induction on using the binomial theorem. |
| 1.2.3-16 | M20 | Derive in closed form by the perturbation method (see check-question 1). |
| ▶1.2.6-10 | M20 | Show directly that divides the product of any consecutive integers — i.e., is an integer without invoking Pascal's rule. (Hint: count multiples of each prime, or interpret combinatorially.) |
| 1.2.6-21 | M20 | Prove Vandermonde's convolution both ways: committee-counting and coefficient-matching in . |
| 1.2.7-21 | M30 | Prove that is never an integer for . (Hint: exactly one term has the maximal power of 2 in its denominator.) |
| 1.2.8-25 | M22 | Derive the fast-doubling formulas , from the addition law, and use them for an fibonacci. |
| ▶1.2.10-... | M21 | Compute the variance of in Algorithm M from , and verify numerically for against the exact histogram. |
| 1.2.11.1-... | M15 | True or false, with proof or counterexample: (a) ; (b) ; (c) if then . |
Why it's done this way
Knuth spends a hundred pages on "preliminaries" because analysis of algorithms is applied discrete mathematics: every later cost function is a sum, every average is an expectation, every bound is an asymptotic statement. The module's shape — closed forms before asymptotics, exact rationals before floating approximations — mirrors his discipline: compute exactly first, approximate knowingly second. That is why stage 4 makes you build as an exact fraction before comparing it with .
In the real world
Harmonic numbers are the most-quoted constant in practical average-case analysis: the expected number of times a "best so far" record updates (Algorithm M — your stage 5) is , which is why streaming max/min loops and secretary-problem-style cutoff rules are cheap in practice. Binomial coefficients price out test-coverage combinatorics and reliability calculations. Fast-doubling Fibonacci is the toy case of evaluating any linear recurrence in time — the trick behind Markov-chain powers and path counting in graphs. And O-notation is simply the contract language of the industry; this module is where you learn to read its fine print (the constants, and where they hide).
Proof techniques you practiced
- Induction, stated honestly with base case and hypothesis — the engine under every identity here.
- The perturbation method — derive a sum by shifting it against itself (the geometric series here; quicksort's recurrence in Module 06).
- Double counting — Vandermonde's convolution counts one committee two ways.
- Linearity of expectation — without ever touching the (messy) distribution of itself.
- Exact-then-asymptotic — as a reduced fraction first, then the approximation with an error you can certify.
11. Where this leads
- is a template. The same indicator-plus-linearity argument gives quicksort's average comparisons (Module 06), the expected depth of a random BST (Module 07), and the analysis of records, cycles, and runs throughout Vols. 1–3.
- Stirling cycle numbers , met here as the distribution of , return as the permutation-cycle statistics of §1.3.3 and the analysis of in-situ permutation.
- Binomial coefficients are the currency of Module 08 (combinatorial generation: Algorithm 7.2.1.3T generates the combinations) and of every counting argument in Vol. 4.
- Fibonacci numbers reappear in polyphase merging (Vol. 3, §5.4.2) and Fibonacci hashing (§6.4); the golden ratio's continued fraction is why Fibonacci inputs are Euclid's worst case.
- The LCG you used to shuffle test data is the star of Module 04 (Vol. 2, Ch. 3) — where you'll finally test whether it deserves the name "random".