Week 2, Wednesday
January 14, 2026
Testing: “It worked on these 10 inputs.”
Proof: “It works on all valid inputs.”
A loop is just induction in disguise.
| Induction | Loop Invariant |
|---|---|
| Base case | Initialization |
| Inductive step | Maintenance |
| “For all \(n\)” | Termination |
An invariant is a property that remains true.
“No matter how many times the loop runs, this statement stays true.”
| Step | What to show |
|---|---|
| Invariant | State \(P(i)\) for \(i \in \{0, 1, \ldots, n\}\) (your domain) |
| Base case | \(P(0)\) holds (first value in domain) |
| Inductive step | Assuming \(P(j)\) for all \(j\) in domain where \(j < i\), show \(P(i)\) |
| Termination | Plug in \(i = n\) → desired result |
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|
| 32 | 11 | 73 | 21 | 29 | 86 | 81 | 17 |
If a = 2, what should find_min return?
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 32 | 11 | 73 | 21 | 29 | 86 | 81 | 17 | 14 | 3 | 64 | 37 |
Invariant: At iteration \(i\) (for \(i \in \{a+1, \ldots, n\}\)), ret holds the index of the minimum value in arr[a:i].
At the first iteration, i = a+1 and ret = a.
The invariant claims ret is the index of the min in arr[a:a+1].
arr[a:a+1] contains only arr[a], and ret = a is its index. ✓
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 32 | 11 | 73 | 21 | 29 | 86 | 81 | 17 | 14 | 3 | 64 | 37 |
IH: Assume the invariant holds at the start of iteration \(i\): ret is the index of min in arr[a:i].
During iteration \(i\), we compare arr[i] to arr[ret]:
arr[i] < arr[ret] → we set ret = i, the new minarr[i] >= arr[ret] → we keep ret, still the minEither way, after iteration \(i\), ret is the index of min in arr[a:i+1]. ✓
Loop terminates when i = n (after processing the last element).
Plug into invariant: ret is index of min in arr[a:n].
But arr[a:n] is exactly arr[a:].
Therefore: ret is the index of the minimum in arr[a:]. ∎
Theorem: find_min(arr, a) returns the index of the minimum value in arr[a:].
Proof: By induction on iteration count.
ret is index of min in arr[a:i]ret = a is min of arr[a:a+1] ✓ret is min of arr[a:n] = arr[a:] ∎What’s the invariant?
Invariant: At iteration \(i\) (for \(i \in \{0, 1, \ldots, n\}\)), total equals the sum of arr[0:i].
Base case: At \(i = 0\), total = 0 = sum of arr[0:0]. ✓
Inductive step: Assume invariant holds at start of iteration \(i\): total = sum of arr[0:i].
During iteration \(i\), we add arr[i] to total.
After iteration \(i\): total = sum of arr[0:i] + arr[i] = sum of arr[0:i+1]. ✓
Termination: Plug in \(i = n\) → total = sum of arr[0:n] = sum of all. ∎
This uses our find_min helper!
Quick check: running time?
We already proved find_min(arr, a) returns the index of the minimum in arr[a:].
Now we can use that fact without re-proving it.
Prove small pieces. Compose them into bigger proofs.
Invariant: At iteration \(a\) (for \(a \in \{0, 1, \ldots, n\}\)), arr[0:a] is sorted and contains the \(a\) smallest elements.
Base case: At \(a = 0\), arr[0:0] is empty. ✓ (Trivially sorted, trivially smallest.)
Inductive step: Assume invariant holds at start of iteration \(a\): arr[0:a] is sorted and contains the \(a\) smallest elements.
During iteration \(a\):
find_min(arr, a) returns the index of the minimum in arr[a:] (by our earlier proof!)arr[a] holds the \((a+1)\)-th smallest elementAfter iteration \(a\): arr[0:a+1] is sorted and contains the \(a+1\) smallest elements. ✓
Termination: Plug in \(a = n\) → arr[0:n] is sorted. ∎
Decomposition is good system engineering.
| Software | Proofs | Data Pipelines |
|---|---|---|
| Write small, tested functions | Prove small helper lemmas | Build validated transformations |
| Compose into larger programs | Compose into larger proofs | Chain into workflows |
| Reuse without re-implementing | Reuse without re-proving | Reuse without re-validating |
Stating the invariant is often the hardest part.
Strategy: Ask “What property do I maintain that leads to my answer?”
| Algorithm Type | Invariant Pattern |
|---|---|
| Accumulation | “var holds result for elements seen so far” |
| Search | “If target exists, it’s in the remaining range” |
| Partition | “Elements before \(i\) satisfy property P” |
| Two-pointer | “Answer is not in the excluded region” |
Loop invariants aren’t just for proving code correct.
They’re a design tool.
“What should be true at each step?”
If you can’t state the invariant, you might not understand your own algorithm.
A wrong invariant can “prove” incorrect code correct.
The invariant must be:
Weak invariant: “At iteration \(a\), arr[0:a] is sorted.”
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 12 | 25 | 38 | 47 | 59 | 83 | 71 | 6 | 94 | 52 | 15 | 33 |
At iteration \(a = 5\), suppose we have:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 12 | 25 | 38 | 47 | 59 | 83 | 71 | 6 | 94 | 52 | 15 | 33 |
Is arr[0:5] = [12, 25, 38, 47, 59] sorted? Yes!
But is this correct? No! The element 6 is smaller than all of them.
Real bugs in real systems come from:
Loop invariants catch these before they ship.
Every time you write a loop, ask:
| Step | What to show |
|---|---|
| Invariant | State \(P(i)\) for \(i \in \{a, a+1, \ldots, n\}\) (your domain) |
| Base case | \(P(a)\) holds (first value in domain) |
| Inductive step | Assuming \(P(j)\) for all \(a \leq j < i\), show \(P(i)\) |
| Termination | Plug in \(i = n\) → desired result |
This is induction on the iteration count.
Wednesday: We’ll use loop invariants to prove a famous algorithm correct — and see why it took 17 years to get a bug-free version.
Before Wednesday, try stating invariants for:
reverse(arr) — reverse an array in place
is_sorted(arr) — check if array is sorted
count(arr, target) — count occurrences of target
Think: What’s true after processing \(i\) elements?