Two Larger Developments

Two non-trivial algorithms — the Welfare Crook and Saddleback search — derived end to end from their specifications, in the style of Gries’ larger program developments. The invariant is discovered first; the moves are justified by preservation; termination falls out of the bound.

This page works two full derivations. For the strategies these examples apply, read Developing programs & inventing invariants; for shorter warm-ups (integer division, linear search) see Worked examples — specification to program.

The small examples show the machinery on toy problems where you could have guessed the code anyway. The point of a larger development is to show the same discipline scaling to algorithms you would not want to guess — where a plausible-looking loop is subtly wrong, and only the invariant tells you which move is safe. Both problems below are classic Gries-chapter-20-style developments: the answer is a three-line loop, but every line is forced by a preservation argument you can check.

This is an original study summary for quick reference. Notation follows Gries and Dijkstra: {P} S {Q} for a Hoare triple, guarded commands (iffi, dood) for the programming language, and the five-part loop checklist (init, preserve, exit, bound>0, bound decreases) throughout. See the book for the full, rigorous treatment.

Contents

  1. The method, restated for hard problems
  2. Problem 1: The Welfare Crook
  3. — Specification
  4. — Finding the invariant
  5. — The derived program
  6. — Correctness & termination
  7. Problem 2: Saddleback search
  8. — Specification
  9. — Finding the invariant
  10. — The derived program
  11. — Correctness & termination
  12. Summary

The method, restated for hard problems

On a hard problem the temptation is to reach for a remembered pattern — “oh, this is the three-pointer trick” — and hope you place the pointers correctly. Gries’ discipline replaces that with a fixed sequence you can follow even when you have never seen the problem:

  1. Write the specification as two predicates — a precondition Q and a postcondition R, with quantifiers made explicit over the arrays.
  2. Invent an invariant P by weakening R — typically delete a conjunct, or enlarge the region a set of variables ranges over. P must be cheap to establish at the start and, together with a false guard, must imply R.
  3. Read off the guard B: the loop runs while P has not yet been narrowed to R.
  4. Choose a bound t: a non-negative integer expression the body strictly decreases, which forces termination.
  5. Derive the body as the only move that both preserves P and shrinks t. This is the step where the invariant earns its keep — it tells you which candidate move is safe and which quietly loses the answer.
The load-bearing idea in both examples below: the invariant asserts “the answer still lives inside a shrinking region.” Each move discards a slice of that region that provably cannot contain the answer. Because we only ever discard impossible slices, the answer stays trapped; because the region strictly shrinks, we must eventually corner it.

Problem 1: The Welfare Crook

Gries’ famous example (attributed to W. Feijen). Three lists of names are kept in three offices — say the unemployment office, the welfare office, and the payroll of a company. Each list is sorted ascending with no duplicates. Someone is known to be drawing unemployment, collecting welfare, and holding a job at once — so some value occurs in all three lists. Find it. Concretely: given ascending arrays a[0..na-1], b[0..nb-1], c[0..nc-1], find indices i, j, k with a[i] = b[j] = c[k].

Specification

The precondition records what we are told; the postcondition records the three indices we must produce.

# Specification — the Welfare Crook
# pre  Q:  a[0..na-1], b[0..nb-1], c[0..nc-1] each ascending,
#          and  (∃ x, p, q, r ::  a[p] = x ∧ b[q] = x ∧ c[r] = x)
#          i.e. SOME value occurs in all three arrays.
#
# post R:  0 ≤ i < na  ∧  0 ≤ j < nb  ∧  0 ≤ k < nc
#          ∧  a[i] = b[j]  ∧  b[j] = c[k]

Finding the invariant

The postcondition is a conjunction: the indices are in range and they point at a common value. Following delete a conjunct, we drop the equality conjunct a[i] = b[j] = c[k] and keep “in range.” But that alone is far too weak — it would let the loop wander anywhere. The craft of a larger development is picking the right weakening. We instead keep a stronger memory of what we know: a common value still exists at or beyond all three current indices.

P:  0 ≤ i ≤ na  ∧  0 ≤ j ≤ nb  ∧  0 ≤ k ≤ nc
   ∧  (∃ x ::  x occurs in a[i..na-1] ∧ x occurs in b[j..nb-1] ∧ x occurs in c[k..nc-1])

Read the last conjunct carefully: it says the “still-searchable” tails of the three arrays — the parts from the current indices onward — still share a common value. The precondition establishes P for free at i = j = k = 0, because then the tails are the whole arrays, which we are told share a value.

The guard. We are done exactly when the three current elements already agree. So we loop while they do not:

B:  ¬(a[i] = b[j] ∧ b[j] = c[k])

The bound. Every move advances one index and none ever retreats, so the total distance left to travel only shrinks:

t:  (na − i) + (nb − j) + (nc − k)

Why advancing the minimum is the only safe move

This is the crux of the whole development — the step you cannot guess safely without the invariant. Suppose the guard holds, so the three elements are not all equal. Then they are not all the same value, so one of a[i], b[j], c[k] is strictly the smallest (if two tie for smallest they still cannot equal the third, or the guard would be false). Say a[i] is that unique-or-tied minimum. Claim: a[i] cannot be the common value x promised by the invariant.

Argument. The invariant says some x occurs in all three tails, in particular in b[j..nb-1] and c[k..nc-1]. Because b and c are ascending, every element of those tails is ≥ b[j] and ≥ c[k] respectively. So the common value satisfies x ≥ b[j] and x ≥ c[k]. But a[i] is strictly less than at least one of b[j], c[k] (it is the minimum and not all three are equal), hence a[i] < x. Therefore a[i] ≠ x, and since a is ascending, no element of a[i..na-1] equal to a[i] can be x either — the crook’s value must lie strictly to the right of i. It is therefore safe to advance i: doing so discards only a[i], which we just proved is not the common value, so the “common value still exists in all three tails” conjunct survives. The same argument holds by symmetry for whichever array holds the minimum.

Why the naive move is wrong. A tempting alternative — “advance the index with the smallest, and if two are equal advance both” — is fine, but advancing anything other than a minimum is not. If you advanced the array holding the largest element, you might step over the very position where the common value sits, and the invariant’s existential conjunct would be destroyed. The invariant is what makes the distinction visible: only discarding a proven-too-small element preserves it.

The derived program

✓ Derived program — three-pointer, correct by construction
# {a, b, c ascending; a common value occurs in all three}
i := 0 ;  j := 0 ;  k := 0 ;
# invariant P: a common value occurs in a[i..], b[j..], c[k..]
# bound     t: (na-i) + (nb-j) + (nc-k)
do a[i] ≠ b[j] or b[j] ≠ c[k] →
     # advance whichever index holds a strict minimum:
     if a[i] ≤ b[j] and a[i] ≤ c[k] → i := i + 1
     [] b[j] ≤ a[i] and b[j] ≤ c[k] → j := j + 1
     [] c[k] ≤ a[i] and c[k] ≤ b[j] → k := k + 1
     fi
od
# {a[i] = b[j] = c[k]}  — i, j, k index the common value
✓ The invariant preserved (body check)
# before body: guard holds, so not all equal.
# Some element is a (weak) minimum; the inner IF's
# guards cover every case (≥ is total), so the
# command never aborts — at least one arm is live.
#
# Say a[i] is the chosen minimum and a[i] ≠ x.
# Common value x satisfies x ≥ b[j] and x ≥ c[k],
# and a[i] < x (min, not all equal) ⇒ a[i] ≠ x.
# a ascending ⇒ x lies in a[i+1..na-1].
# So x still occurs in all three tails after i:=i+1
#   ⇒ P preserved.                             ✓
# One index rose by 1 ⇒ t dropped by 1.        ✓

Note the inner if uses in every guard, so ties are handled and the guards are exhaustive (for any three values at least one is the other two). Whichever arm fires advances an index whose element is a minimum — exactly the safe move justified above.

Correctness & termination

Discharge the five-part checklist.

#ObligationDischarge
1Q ⇒ wp(init, P)At i=j=k=0 the tails are the full arrays; the precondition says they share a value, so the existential conjunct holds and all indices are in range.
2{P ∧ B} S {P}The body advances an index whose element is a proven minimum and hence provably not the common value; the shared value survives in all three tails. Argued above.
3P ∧ ¬B ⇒ R¬B is a[i]=b[j] ∧ b[j]=c[k] — exactly the equality half of R. And the indices are strictly in range on exit (see the note below), giving the range half.
4P ∧ B ⇒ t > 0While looping we still advance some index, so at least one of i, j, k is below its length; hence t = (na-i)+(nb-j)+(nc-k) > 0. See the no-overrun argument.
5{P ∧ B} S {t decreases}Every arm increments exactly one index by 1 and none decreases, so t drops by exactly 1 each iteration.

Why an index never runs off the end (no array overrun). This is the subtle point that termination and safety both lean on. Suppose the loop is about to advance i because a[i] is the minimum. The invariant guarantees the common value x occurs in a[i..na-1], and we proved a[i] ≠ x, so x occurs strictly within a[i+1..na-1] — which means i+1 ≤ na-1, i.e. i is safely incrementable and stays ≤ na-1. The invariant’s existential conjunct is precisely what forbids stepping past the array. This is the same phenomenon as the linear-search example: the “value is present” precondition is doing design work, here guaranteeing every advance lands on a real element.

Termination. t is a non-negative integer (each index is bounded by its array length) that strictly decreases every iteration, so the loop cannot run forever; it must reach a state where ¬B, i.e. all three elements agree. Combined with obligation 3, that state satisfies R. The algorithm is totally correct, and it does so in at most na + nb + nc iterations — linear in the combined input size.

Problem 2: Saddleback search

A matrix M[0..m-1][0..n-1] is sorted ascending along every row and down every column. A value x is known to be present. Find a cell (r, c) with M[r][c] = x. The name is Gries’: you search from a saddle corner — the top-right — where moving one way only increases values and the other only decreases them, so each comparison eliminates a whole row or a whole column at once.

Specification

# Specification — Saddleback search
# pre  Q:  (∀ p, s ::  s < n-1  ⇒  M[p][s] ≤ M[p][s+1])   (rows ascending)
#          (∀ p, s ::  p < m-1  ⇒  M[p][s] ≤ M[p+1][s])   (cols ascending)
#          (∃ p, s ::  0 ≤ p < m  ∧  0 ≤ s < n  ∧  M[p][s] = x)  (x present)
#
# post R:  0 ≤ r < m  ∧  0 ≤ c < n  ∧  M[r][c] = x

Finding the invariant

Following enlarge the range: instead of tracking a single cell, track a sub-rectangle that still must contain x, and shrink it. Start the cursor at the top-right corner (r, c) = (0, n-1). The invariant asserts that x, which we are told exists, still lies in the rectangle of rows r..m-1 and columns 0..c:

P:  0 ≤ r ≤ m  ∧  −1 ≤ c ≤ n−1
   ∧  (∃ p, s ::  r ≤ p < m ∧ 0 ≤ s ≤ c ∧ M[p][s] = x)

At the start r = 0, c = n-1, the rectangle is the entire matrix, so the precondition establishes P immediately. The cursor (r, c) sits at the top-right corner of the live rectangle: everything above row r and everything right of column c has already been eliminated.

The guard. We stop when the cursor is on the target. We loop while it is not:

B:  M[r][c] ≠ x

The bound. Each move either drops a column (c := c-1) or drops a row (r := r+1); the number of rows-plus-columns still live only shrinks:

t:  c + (m − 1 − r)

Why each move preserves the invariant

At the cursor M[r][c], compare with x. Two cases when it differs:

Why the top-right corner is essential. The corner is the one cell that is simultaneously the maximum of its column-tail and the minimum-region boundary of its row — that is what lets a single comparison rule out an entire row or column. From the top-left or bottom-right corner both directions move the value the same way, so one comparison cannot cleanly eliminate a full line, and the invariant would not be preservable by a single-cell step.

The derived program

✓ Derived program — start top-right, eliminate a line each step
# {rows ascending; cols ascending; x is present in M}
r := 0 ;  c := n - 1 ;
# invariant P: x lies in rows r..m-1, cols 0..c
# bound     t: c + (m - 1 - r)
do M[r][c] ≠ x →
     if M[r][c] > x → c := c - 1   # value too big: drop column c
     [] M[r][c] < x → r := r + 1   # value too small: drop row r
     fi
od
# {0 ≤ r < m and 0 ≤ c < n and M[r][c] = x}
✓ The invariant preserved (body check)
# before body: guard holds, so M[r][c] ≠ x.
# Guards M[r][c]>x and M[r][c]<x are exhaustive
# under M[r][c]≠x ⇒ IF never aborts.
#
# case > x:  col c (rows r..m-1) all ≥ M[r][c] > x
#           ⇒ x not in col c ⇒ x in cols 0..c-1
#           ⇒ P holds after c:=c-1.            ✓
# case < x:  row r (cols 0..c) all ≤ M[r][c] < x
#           ⇒ x not in row r ⇒ x in rows r+1..
#           ⇒ P holds after r:=r+1.            ✓
# either move drops t by exactly 1.            ✓

Correctness & termination

#ObligationDischarge
1Q ⇒ wp(init, P)At r=0, c=n-1 the rectangle is the whole matrix; the precondition says x is present, so the existential conjunct holds.
2{P ∧ B} S {P}Each branch eliminates a full row or column proven not to contain x, so x stays inside the smaller rectangle. Argued above.
3P ∧ ¬B ⇒ R¬B is M[r][c] = x. The cursor stays in range throughout (no-overrun below), so 0 ≤ r < m and 0 ≤ c < n hold, giving all of R.
4P ∧ B ⇒ t > 0If t = c + (m-1-r) = 0 then c = 0 and r = m-1: a one-cell rectangle. The existential conjunct then forces M[r][c] = x, i.e. ¬B. So while B holds, t > 0.
5{P ∧ B} S {t decreases}Either c falls by 1 or r rises by 1; both drop t = c + (m-1-r) by exactly 1.

Why the cursor never leaves the matrix (no array overrun). We must know c := c-1 never underflows below 0 and r := r+1 never overflows past m-1. Obligation 4 supplies exactly this: whenever the guard still holds, t > 0, so the rectangle has more than one cell, so at least one of the moves has room. More precisely, in the > x case the existential conjunct guarantees x lies in columns 0..c but not in column c, so it lies in 0..c-1, forcing c ≥ 1 — the decrement is safe. Symmetrically the < x case forces r ≤ m-2, so the increment is safe. As with the crook, the “x is present” precondition, carried in the invariant, is what guarantees the cursor stays in bounds.

Termination. t = c + (m-1-r) starts at (n-1) + (m-1), is bounded below by 0, and strictly decreases each step, so the loop runs at most m + n - 2 times before reaching ¬B. Combined with obligation 3, exit implies R. Saddleback search is totally correct and runs in O(m + n) — each step retires a whole row or column, which is exactly why it beats a naive per-cell scan.

These patterns are what interview questions reward — here they are derived, not memorized. The “three-pointer merge” and the “start at a corner and eliminate a line” tricks are staples of coding interviews, usually presented as clever moves to recall. What the two developments above show is that neither is a trick: each is the forced consequence of an invariant that says “the answer still lives in a shrinking region.” If you can state that invariant, the safe move and the termination bound both follow mechanically — and you can reconstruct the algorithm under pressure instead of hoping you remember it.

Summary

ProblemInvariant ideaWhy each step is safe
The Welfare Crook (3 ascending arrays, find common value)A common value still occurs in all three tails a[i..], b[j..], c[k..]; indices only advance.The strictly-smallest of the three current elements cannot be the common value (the value is the other two current elements), so advancing past it discards only a proven non-answer — the shared value stays in all three tails.
Saddleback search (row/col-sorted matrix, find x)Target x still lies in the sub-rectangle rows r..m-1, cols 0..c; cursor sits at its top-right corner.At the corner, M[r][c] > x means the whole live column is it (cols ascending down) so drop the column; M[r][c] < x means the whole live row is it (rows ascending right) so drop the row — each move eliminates a line that provably cannot hold x.
The recurring theme, now at scale: the invariant is the design. Both algorithms reduce to “keep the answer trapped in a region and shrink the region by discarding only impossible slices.” State that invariant precisely and the guard, the safe move, the bound, and the termination argument all fall out — the finished program is correct by construction, not by testing. See developing programs for the strategies and basic examples for the warm-ups these build on.