21 — The Course Track: Orientation
This file starts a second track. Files 00 through 20 are the interview track: patterns, speed, pass the phone screen. Files 21 through 28 are the course track, written the way a graduate algorithms textbook is written: every algorithm arrives with the argument that justifies it.
They are different sports played with the same equipment.
| Interview track (00–20) | Course track (21–28) | |
|---|---|---|
| Deliverable | working code, fast | a written proof |
| Judged on | does it pass the tests | is every claim justified |
| Time budget | 25 minutes, live | days, alone, with references |
| Failure mode | you froze | you hand-waved |
| "Correct" means | no failing case found | correctness argued for all inputs |
| Typical answer | 30 lines of Python | one page of English and math |
You can be excellent at the first and lost in the second. The gap is not intelligence, it is that nobody ever taught you that the algorithm is the easy half and the proof is the half that counts.
How to read the course track
Read 21 through 28 in order. Each assumes the previous one. They assume nothing from files 00–20, so you can start here cold, but the interview track is a good source of concrete implementations for everything proved here.
Notation is written in plain text rather than typeset math, on purpose:
O(n^2),Theta(n log n),sum_{i=1}^{n}. You will read the typeset version in CLRS and write it out by hand. Learning to translate between the two is part of the skill.
21 Orientation -> 22 Asymptotics -> 23 Recurrences -> 24 Divide and Conquer -> 25 Dynamic Programming -> 26 Greedy -> 27 Graphs and Flow -> 28 NP-Completeness
21.1 What this material actually teaches
Strip away the topic list and a graduate algorithms course teaches exactly four skills, in this order of weight:
- Analyze a given algorithm's running time, rigorously, usually by setting up and solving a recurrence.
- Design an algorithm for a problem you have never seen, by recognizing which of five paradigms applies.
- Prove your algorithm correct, using induction, an exchange argument, or a cut-and-paste argument.
- Reduce one problem to another, both to solve things and to prove things are hard.
The topic list (sorting, graphs, flow, NP-completeness) is the vehicle. The four skills are the cargo. Almost every problem you will be set is one of the four wearing a costume.
The five paradigms, in one table
This is the decision tree the whole course hangs off. Memorize it now, understand it over the next seven files.
| Paradigm | Shape of the idea | Correctness proved by | Typical cost |
|---|---|---|---|
| Brute force / iterative | try everything, or sweep once | loop invariant + induction | O(n), O(n^2), or exponential |
| Divide and conquer | split into independent subproblems, recurse, combine | strong induction on size | solve a recurrence |
| Dynamic programming | overlapping subproblems, solve each once, memoize | cut-and-paste (optimal substructure) | (number of states) x (work per state) |
| Greedy | commit to a locally best choice, never reconsider | exchange argument, or greedy-stays-ahead | usually sort + linear scan |
| Reduction | turn my problem into one already solved | correctness of the mapping, both directions | cost of transform + cost of solver |
And the sixth thing, which is not a design paradigm but a proof technique: lower bounds, where you argue that no algorithm can beat some cost. That is the Omega side of the world, and it is where NP-completeness lives.
21.2 How to study this material
The gap between reading an algorithm and being able to reconstruct it is enormous, and reading more does not close it. Four habits do.
Write from memory. After finishing a section, close the file and write a one-page summary on blank paper. Recognition feels like knowledge and is not knowledge. If you cannot restate the master theorem's three cases without looking, you do not know it, however many times you have read them.
Attempt alone before discussing. Working through a hard problem with other people is genuinely useful, and it is useless if you have not first spent forty-five minutes stuck on it yourself. The struggle is where the learning happens; the discussion only consolidates it. Reversed, you watch someone else learn.
Redo solved problems cold. Once a week, take a problem you already solved and solve it again from a blank page with nothing open. This is the single activity that most reliably converts "I followed that" into "I can produce that".
Never leave a proof blank. When you cannot finish, write down what you do know: the definition of the thing being asked, the base case, the induction hypothesis, and an honest statement of what remains to be shown. A structured partial argument is worth far more than a blank page, and writing the skeleton often shows you the missing step.
That last habit is worth generalizing. Most of the difficulty in this material is not having ideas, it is knowing what a complete argument looks like. Once the shape is automatic, you can tell in seconds whether your own reasoning has a hole in it, which is most of the skill.
21.3 Using this file honestly
This is a textbook. Reading it to learn a technique is what textbooks are for, and it is the same act as reading CLRS or Erickson.
If you are taking a course, your instructor's rules on collaboration, outside sources, and citation govern, and they vary. Read them and follow them. The general principle that holds everywhere: learn the method from the explanation, then close it and solve the assigned problem yourself, in your own words. If a worked example here was close enough to something you were assigned that you leaned on it, cite it. Citing costs nothing.
There is also a purely practical argument. Anything you get through without building the skill is a loan, and it comes due the first time you have to produce the argument from a blank page with nothing open.
21.4 How to write a proof that holds up
This is the section to reread before every written solution. Most of what goes wrong here is not wrong ideas, it is correct ideas presented in a form a reader cannot verify.
The four-part answer
Every "design an algorithm for X" problem wants four things, usually in this order. Label them literally with headers. Someone checking your work should never have to guess which part they are looking at.
1. The idea, in English, in three sentences. Before any pseudocode. If a reader understands your approach from the first paragraph, everything after is confirmation and you will be read generously. If they have to reverse-engineer your intent from pseudocode, you will be read suspiciously.
Idea: we sort the intervals by right endpoint and repeatedly take the first interval that does not overlap what we have already taken. Sorting by right endpoint means each accepted interval leaves the maximum possible room for the rest.
2. The algorithm, as pseudocode. Not a language. Pseudocode. Indexed from 1 like CLRS if your course does. Use real variable names. It should be unambiguous enough that a competent person could implement it and short enough to read in thirty seconds.
3. Correctness proof. This is the biggest block and the one people amputate. Covered below.
4. Running time analysis. State it, then justify it line by line or by naming the dominant step. "Sorting is O(n log n), the scan is O(n), so the total is O(n log n)" is a complete answer. "O(n log n)" alone is not.
Choosing the right proof technique
| If your algorithm is... | Prove it with... |
|---|---|
| A loop | a loop invariant: state it, prove initialization, maintenance, termination |
| Recursive / divide and conquer | strong induction on the input size |
| Dynamic programming | optimal substructure via a cut-and-paste argument, then induction on the table order |
| Greedy | an exchange argument (transform any optimal solution into yours without making it worse) or greedy stays ahead |
| A reduction | show the mapping is computable, and prove both directions of "yes iff yes" |
| A lower bound | a decision tree count, or an adversary argument |
Each of these is developed with worked examples in the files that follow. What matters right now is that you know a menu exists and that picking from it is not optional.
The sentences that lose points
Cross these out of your vocabulary for the semester:
- "Clearly..." and "Obviously..." and "It is easy to see that..." These are exactly the places a careful reader looks for gaps. If it is genuinely obvious, one more clause proving it costs you a line. If it is not obvious, you just advertised the hole.
- "And so on..." and "...similarly for the other cases." Do the other cases, or say precisely why they are symmetric.
- "This works because it tries all possibilities." Does it? Prove that every possibility is reached.
- "By induction." Naming the technique is not applying it. Write the base case, write the hypothesis, write the step.
The template that never fails
For any induction:
Claim: P(n) holds for all n >= n0.
Base case: n = n0. [show P(n0) directly]
Induction hypothesis: assume P(k) holds for all n0 <= k < n.
(Use STRONG induction by default. It costs nothing extra and
recursive algorithms almost always need more than P(n-1).)
Induction step: [show P(n), using the hypothesis, and say exactly where
you used it]
Therefore P(n) holds for all n >= n0 by induction. QEDFill that skeleton in even when you cannot complete the step. A blank page scores zero. A skeleton with a correct base case, a correctly stated hypothesis, and an honest "we would now need to show X" often scores half.
21.5 Pseudocode conventions
Courses in this lineage use CLRS-style pseudocode. Match it and your work reads as fluent.
MERGE-SORT(A, p, r)
1 if p < r
2 q = floor((p + r) / 2)
3 MERGE-SORT(A, p, q)
4 MERGE-SORT(A, q + 1, r)
5 MERGE(A, p, q, r)The conventions worth copying:
- Arrays are 1-indexed, and
A[p..r]denotes the slice inclusive of both ends. This is different from Python and it will trip you up in exactly one place: the midpoint and the recursive boundaries. Be deliberate. - Indentation indicates block structure. No braces, no
end. =is assignment,==is comparison.- Line numbers, so your analysis can say "line 5 runs n times".
A.lengthfor the size of an array,x.keyfor attributes of an object.- Return with
return. Infinity is writtenINF. Swap isexchange A[i] with A[j].
Erickson's notes use a lighter style with a similar spirit. Either is fine; consistency within one answer is what is graded.
21.6 The mathematical toolkit you need on day one
Not a full treatment of discrete maths, just the specific facts that appear over and over. If any of these is unfamiliar, spend an hour on it now rather than losing a problem to it later.
Logarithms
log(ab) = log a + log b
log(a/b) = log a - log b
log(a^b) = b log a
a^(log_b c) = c^(log_b a) <- the weird one, and it does show up
log_b a = (log_c a) / (log_c b) <- change of baseThe change-of-base identity is why the base of a logarithm never matters inside asymptotic notation: changing base multiplies by the constant 1 / log_c b, and constants vanish. O(log_2 n), O(log_10 n), and O(ln n) are the same set. This is why we just write O(log n).
Two conventions from CLRS: lg n means log_2 n, and lg^k n means (lg n)^k, not lg applied k times (that is lg* n, the iterated logarithm, which is a different and much slower-growing thing).
The iterated logarithm is worth defining properly once, since it turns up in the analysis of union-find and nowhere else you will care about:
lg* n = 0 if n <= 1
lg* n = 1 + lg*(lg n) if n > 1In words: how many times you have to hit n with lg before it drops to 1 or below. It grows so slowly that lg* n <= 5 for every n you will ever meet, since lg*(2^65536) = 5. Formally lg* n = o(log log log n), and for practical purposes it is a constant that we are too honest to call one.
Summations you must know cold
sum_{i=1}^{n} 1 = n
sum_{i=1}^{n} i = n(n+1)/2 = Theta(n^2)
sum_{i=1}^{n} i^2 = n(n+1)(2n+1)/6 = Theta(n^3)
sum_{i=1}^{n} i^k = Theta(n^(k+1)) for constant k >= 0
sum_{i=0}^{n} x^i = (x^(n+1) - 1)/(x - 1) geometric, x != 1
sum_{i=0}^{inf} x^i = 1/(1 - x) for |x| < 1
sum_{i=1}^{n} 1/i = H_n = ln n + O(1) = Theta(log n), the harmonic sumThe geometric series is the single most important one here, for a reason worth stating early: a geometric series is dominated by its largest term, up to a constant factor. If a recursion tree's level costs form a geometric series, you do not have to sum it carefully, you just take the biggest level and multiply by a constant. That intuition is the entire content of the master theorem.
Factorials and Stirling
n! = 1 x 2 x ... x n
lg(n!) = Theta(n lg n) <- appears in the sorting lower bound
n! = sqrt(2 pi n) (n/e)^n (1 + Theta(1/n)) <- Stirling's approximationYou need lg(n!) = Theta(n lg n) in file 28. You almost never need full Stirling.
Counting facts
number of subsets of an n-set = 2^n
number of k-subsets of an n-set = C(n,k) = n! / (k! (n-k)!)
number of permutations of n items = n!
C(n,k) <= (en/k)^k <- useful boundProof by induction, contradiction, contraposition
- Induction: prove P(base), prove P(<n) implies P(n). Use strong induction by default.
- Two ways to count the same thing: if two expressions both count the elements of one set, they are equal. This is the cleanest proof of
sum_{i=1}^{n} i = n(n+1)/2. Both sides count pairs(j, k)withj < kdrawn fromn + 1people. The left side fixeskand counts the choices ofjbelow it. The right side picks any person, then a second, then divides by 2 because each pair got counted twice. No algebra, no induction, and it generalizes. - Contradiction: assume the negation, derive something false. The workhorse for lower bounds and for "no such algorithm exists".
- Contraposition: to prove "if A then B", prove "if not B then not A". These are logically identical and one direction is usually far easier.
One induction proof that is wrong, and worth staring at. Claim: all people have the same name. Base case, one person: one name, true. Inductive step: given n + 1 people P_1, ..., P_{n+1}, look at S_1 = {P_1, ..., P_n} and S_2 = {P_2, ..., P_{n+1}}. Each has size n, so by the hypothesis everyone in S_1 shares a name and everyone in S_2 shares a name. The two sets overlap, so all n + 1 names agree.
The conclusion is false, so find the broken step before reading on. It is the overlap. S_1 and S_2 share a member only when n >= 2, so the step never carries the base case n = 1 to n = 2, and the whole chain fails at its first link. The lesson generalizes: an inductive step that quietly assumes the input is large enough is the most common way a wrong proof looks right. When you write one, check it against the smallest case the step is supposed to handle, not against a comfortable large case.
Graph vocabulary
Assumed from day one, so make sure all of it is instant: vertex, edge, directed vs undirected, weighted, degree, in-degree, out-degree, path, simple path, cycle, connected, strongly connected, tree, forest, DAG, subgraph, spanning tree, bipartite, adjacency list, adjacency matrix. File 27 rebuilds these anyway, but they get used before then.
21.7 A worked example, end to end
To make section 21.4 concrete, here is one complete answer at the standard the course expects. The problem is deliberately easy so that the form is the only thing to study.
Problem. Given an array
A[1..n]of integers, find the maximum sum of any contiguous subarray. (The empty subarray, with sum 0, is allowed.)
Idea. Scan left to right, maintaining the best sum of a subarray that ends exactly at the current position. That value is either the current element alone or the current element appended to the best subarray ending one position earlier, whichever is larger. The answer is the maximum of those values over all positions.
Algorithm.
MAX-SUBARRAY(A)
1 best = 0
2 ending-here = 0
3 for i = 1 to A.length
4 ending-here = max(0, ending-here + A[i])
5 best = max(best, ending-here)
6 return bestCorrectness. We use a loop invariant.
Invariant. At the start of each iteration with index i, ending-here equals the maximum sum over all subarrays (possibly empty) ending at position i-1, and best equals the maximum sum over all subarrays (possibly empty) of A[1..i-1].
Initialization. Before the first iteration i = 1, so A[1..0] is empty. The only subarray ending at position 0 is the empty one, of sum 0, and the only subarray of an empty array is the empty one, of sum 0. Both variables are 0, so the invariant holds.
Maintenance. Assume the invariant before iteration i. Any subarray ending at position i is either empty (sum 0) or is a subarray ending at i-1 extended by A[i]. The best of the second kind has sum ending-here + A[i] by the invariant. So the maximum over subarrays ending at i is max(0, ending-here + A[i]), which is exactly what line 4 assigns. Every subarray of A[1..i] either ends at i or is a subarray of A[1..i-1], so the maximum over A[1..i] is the larger of the new ending-here and the old best, which is what line 5 assigns. The invariant therefore holds before iteration i+1.
Termination. The loop ends with i = n+1. By the invariant, best is the maximum sum over all subarrays of A[1..n], which is what line 6 returns. QED
Running time. The loop runs n times and each iteration does a constant number of arithmetic and comparison operations, so the total is Theta(n). Space is O(1) beyond the input.
Notice the proportions. Six lines of algorithm, four paragraphs of proof. That ratio is the whole difference between the two tracks. If your written solution is mostly code, you are answering the interview question, not this one.
21.8 What is in the rest of the track
| File | Covers | The one skill it builds |
|---|---|---|
| 22 — Asymptotics from Zero | O, Omega, Theta, o, omega, limits, loop counting, summations | reading a bound and knowing exactly what it does and does not claim |
| 23 — Recursion and Recurrences | reduction, the recursion template, Tower of Hanoi, substitution, recursion trees, master theorem, changing variables | writing a recursive algorithm, proving it correct, and turning it into a closed-form running time |
| 24 — Divide and Conquer | mergesort, quickselect, Karatsuba, Strassen, closest pair, sorting lower bound, pattern matching | designing a split-and-combine algorithm and proving it |
| 25 — Dynamic Programming | the recipe, optimal substructure, the standard problem zoo | going from recursive definition to table to correctness proof |
| 26 — Greedy | exchange arguments, greedy stays ahead, scheduling, Huffman, MST | proving that never reconsidering is safe |
| 27 — Graphs and Network Flow | BFS/DFS, topological sort, SCC, MST, shortest paths, Ford-Fulkerson, max-flow min-cut, matching | modelling a problem as a graph or a flow and citing the right theorem |
| 28 — NP-Completeness | P, NP, reductions, Cook-Levin, the standard hard problems, how to write a hardness proof | proving a problem hard instead of failing to solve it |
Start with 22 — Asymptotics from Zero. It assumes you have never seen a Big-O in your life.