Correctness proofs
How to prove a greedy algorithm correct: the exchange argument, the staying-ahead proof, the cut property, loop invariants and a first look at matroids.
Learning goals
- Carry out an exchange argument in four steps
- Prove a greedy algorithm optimal by showing it stays ahead of any other solution
- State the cut property and see why Kruskal and Prim are safe
- Write the invariant that a greedy loop maintains
- Recognise a matroid and the problems for which greedy is then always correct
Prerequisites
A greedy algorithm without a proof is a guess. The proofs are short, they follow a few templates, and once you know the templates you can also see where a plausible rule will break.
Exchange argument
The **exchange argument** takes any optimal solution and gradually turns it into the greedy solution without making it worse. If that is always possible, the greedy solution is optimal too. The recipe has four steps:
1. Let O be an optimal solution and G the greedy solution; assume they differ. 2. Find the first place where they differ. 3. Exchange: change O at that place to agree with G, and show the result is still feasible and no worse. 4. Repeat; after finitely many exchanges O has become G.
For activity selection, let O be an optimal schedule sorted by finish time, and let g be the greedy interval, the earliest finish overall. If O does not start with g, replace its first interval f by g. Since g finishes no later than f, g cannot overlap the second interval of O. The schedule is still valid and still has the same number of intervals, so an optimal schedule containing g exists: the greedy-choice property.
| Step | Activity selection |
|---|---|
| Assume O optimal, different from greedy | O's first interval f is not the earliest-finishing interval g |
| Exchange | Replace f by g in O |
| Still feasible? | Yes: end(g) <= end(f) <= start of O's next interval |
| Still optimal? | Yes: same number of intervals |
| Conclusion | Some optimal schedule begins with g; recurse on the rest |
Staying-ahead proof
The **staying-ahead** proof compares the greedy solution with any other solution step by step and shows that, after every step, greedy is at least as far along by a measurable amount. At the end, being as far along as possible means being optimal.
Interval covering is the cleanest example. Cover the range [0, 12] with the fewest intervals. Define the reach after k intervals as the rightmost point covered. Greedy always picks, among intervals that start no later than the current reach, the one that reaches farthest. By induction greedy's reach after k picks is at least the reach of any other selection of k intervals: the base case is trivial, and in the step any other solution's next interval starts at or before its own reach, which is at most greedy's reach, so greedy can pick an interval at least as far-reaching. When some other solution covers the whole range with k intervals, greedy's reach after k picks is at least 12 too.
Run the animation and watch the reach marker: after each pick it moves as far right as any available interval allows.
Cover a range with the fewest intervals
Interactive · edit the inputsorted by start
Cover [0, 12] using as few intervals as possible. Repeatedly look at every interval that starts within the covered part and take the one that reaches farthest.
1static int minCover(int[][] iv, int from, int to) {2Arrays.sort(iv, Comparator.comparingInt(a -> a[0])); // by start3int count = 0, reach = from, i = 0;4while (reach < to) {5int farthest = reach;6while (i < iv.length && iv[i][0] <= reach) farthest = Math.max(farthest, iv[i++][1]); // all intervals that start in time7if (farthest == reach) return -1; // nothing extends the covered part: a gap8count++; reach = farthest; // take the one that reaches farthest9}10return count;11}
Live state
from
0
to
12
reach
0
Execution trace (dry run table)Show 10 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | from=0 to=12 reach=0 | Cover [0, 12] using as few intervals as possible. Repeatedly look at every interval that starts within the covered part and take the one that reaches farthest. |
| 2 | L6 | reach=0 candidates=1 farthest=3 | Intervals A start at or before 0; the farthest end among them is 3. |
| 3 | L8 | count=1 reach=3 | Take A: the covered part now reaches 3. |
| 4 | L6 | reach=3 candidates=2 farthest=6 | Intervals B, C start at or before 3; the farthest end among them is 6. |
| 5 | L8 | count=2 reach=6 | Take C: the covered part now reaches 6. |
| 6 | L6 | reach=6 candidates=1 farthest=9 | Intervals D start at or before 6; the farthest end among them is 9. |
| 7 | L8 | count=3 reach=9 | Take D: the covered part now reaches 9. |
| 8 | L6 | reach=9 candidates=2 farthest=12 | Intervals E, F start at or before 9; the farthest end among them is 12. |
| 9 | L8 | count=4 reach=12 | Take F: the covered part now reaches 12. |
| 10 | L10 | return=4 | Covered [0, 12] with 4 intervals. |
Cut property
For graph problems the exchange argument takes the form of the **cut property**. A *cut* splits the vertices into two non-empty sets. Of all edges that cross the cut, the **lightest** one belongs to some minimum spanning tree (every minimum spanning tree if the lightest edge is unique).
Proof by exchange: suppose a minimum spanning tree T does not contain the lightest crossing edge e. Adding e to T creates a cycle, and that cycle must cross the cut again through another edge f, which is at least as heavy as e. Swap f for e: the result is a spanning tree no heavier than T, and it contains e.
Both algorithms in the graph lesson are applications of the cut property. Kruskal's takes the lightest edge overall that joins two different components, a cut that separates one component from the rest. Prim's grows a single tree and takes the lightest edge that leaves it, the cut between the tree and everything else. The animation shows Kruskal's sorted edge list with each accept or reject decision.
Kruskal's minimum spanning tree
Interactive · edit the inputedges, lightest first
parent[] (union-find)
Sort the edges by weight and take each one unless it joins two vertices that are already connected: that would close a cycle. A union-find structure answers 'already connected?' quickly.
1static int kruskal(int n, int[][] edges) { // edges[i] = {u, v, weight}2Arrays.sort(edges, Comparator.comparingInt(e -> e[2])); // lightest first3int[] parent = new int[n]; for (int i = 0; i < n; i++) parent[i] = i;4int total = 0;5for (int[] e : edges) {6int a = find(parent, e[0]), b = find(parent, e[1]); // component leaders of the two ends7if (a != b) { parent[a] = b; total += e[2]; } // different components: keep the edge, merge them8} // same component: the edge would close a cycle, skip it9return total;10}
Live state
vertices
5
edges
7
Execution trace (dry run table)Show 13 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | vertices=5 edges=7 | Sort the edges by weight and take each one unless it joins two vertices that are already connected: that would close a cycle. A union-find structure answers 'already connected?' quickly. |
| 2 | L6 | edge=1-2:1 leader u=1 leader v=2 total=0 | Vertices 1 and 2 are in different components: keep the edge (weight 1). |
| 3 | L7 | total=1 kept=1 | Merge the two components (parent[1] = 2). Total weight 1. |
| 4 | L6 | edge=1-3:2 leader u=2 leader v=3 total=1 | Vertices 1 and 3 are in different components: keep the edge (weight 2). |
| 5 | L7 | total=3 kept=2 | Merge the two components (parent[2] = 3). Total weight 3. |
| 6 | L6 | edge=0-2:3 leader u=0 leader v=3 total=3 | Vertices 0 and 2 are in different components: keep the edge (weight 3). |
| 7 | L7 | total=6 kept=3 | Merge the two components (parent[0] = 3). Total weight 6. |
| 8 | L6 | edge=0-1:4 leader u=3 leader v=3 total=6 | Vertices 0 and 1 are already connected: this edge would close a cycle, skip it. |
| 9 | L6 | edge=2-3:4 leader u=3 leader v=3 total=6 | Vertices 2 and 3 are already connected: this edge would close a cycle, skip it. |
| 10 | L6 | edge=2-4:5 leader u=3 leader v=4 total=6 | Vertices 2 and 4 are in different components: keep the edge (weight 5). |
| 11 | L7 | total=11 kept=4 | Merge the two components (parent[3] = 4). Total weight 11. |
| 12 | L6 | edge=3-4:7 leader u=4 leader v=4 total=11 | Vertices 3 and 4 are already connected: this edge would close a cycle, skip it. |
| 13 | L9 | return=11 edges=4 components=1 | Minimum spanning tree: 4 edges, total weight 11. |
Greedy invariant
A **greedy invariant** is a statement about the partial solution that is true before every iteration and, together with the loop's exit condition, implies the answer is optimal. It is the loop-invariant technique from earlier chapters applied to optimality. A typical pair for activity selection:
(1) The chosen intervals are pairwise non-overlapping and lastEnd is the finish time of the latest chosen one. (2) Every interval skipped so far overlaps a chosen interval, or is impossible to add without removing a chosen one. (3) There is an optimal solution that agrees with every choice made so far. Part (3) is the invariant the exchange argument establishes one step at a time.
Writing invariants as assert statements while developing is a cheap safety net: run the algorithm on random inputs with the checks on, and a violated invariant points at the decision that was wrong.
1static int selectWithInvariants(int[][] intervals) {2int[][] a = intervals.clone();3Arrays.sort(a, Comparator.comparingInt(x -> x[1]));4List<int[]> chosen = new ArrayList<>();5int lastEnd = Integer.MIN_VALUE;6for (int[] iv : a) {7if (iv[0] >= lastEnd) { chosen.add(iv); lastEnd = iv[1]; }8// invariant 1: chosen intervals are sorted and disjoint, lastEnd is the latest finish9for (int i = 1; i < chosen.size(); i++) {10if (chosen.get(i - 1)[1] > chosen.get(i)[0]) throw new AssertionError("overlap");11}12}13return chosen.size();14}
Matroid viewpoint introduction
Many correct greedy algorithms share one structure. A **matroid** is a ground set together with a family of *independent* sets that satisfies three rules: the empty set is independent, every subset of an independent set is independent, and if one independent set is larger than another, an element can be moved from the larger into the smaller while staying independent (the exchange property).
The payoff is a theorem: on a matroid, the greedy algorithm that scans elements in order of decreasing weight and keeps each element that preserves independence always finds a maximum-weight independent set. Kruskal's algorithm is exactly this algorithm on the *graphic matroid*, where independent sets are the edge sets without cycles. Choosing the k heaviest items is the greedy algorithm on the *uniform matroid*.
You rarely need to prove the matroid axioms in practice, but recognising the structure tells you that greedy works, and failing the exchange property tells you why it does not: the 0/1 knapsack's feasible sets, those whose total weight fits in the bag, do not satisfy it.
| Matroid | Independent sets | Greedy algorithm it explains |
|---|---|---|
| Graphic matroid | Edge sets without a cycle | Kruskal's minimum spanning tree |
| Uniform matroid of rank k | Sets of at most k elements | Pick the k largest values |
| Partition matroid | At most one element from each group | Best item per category |
| Not a matroid | Sets with total weight at most W | Greedy knapsack is not optimal |
1// generic matroid greedy: the oracle says whether a set of element indices is independent2static List<Integer> matroidGreedy(int[] weight, Predicate<List<Integer>> independent) {3Integer[] order = new Integer[weight.length];4for (int i = 0; i < order.length; i++) order[i] = i;5Arrays.sort(order, (x, y) -> weight[y] - weight[x]); // heaviest first6List<Integer> chosen = new ArrayList<>();7for (int i : order) {8chosen.add(i);9if (!independent.test(chosen)) chosen.remove(chosen.size() - 1);10}11return chosen;12}13// uniform matroid of rank k: independent means size <= k, so this returns the k heaviest elements
Practice ladder
Now try it yourself
Easy
- Maximum Units on a Truck
- Lemonade Change
Medium
- Partition Labels
- Minimum Number of Arrows to Burst Balloons
Advanced
- Min Cost to Connect All Points
- Maximum Performance of a Team
Key takeaways
- An exchange argument turns any optimal solution into the greedy one, one difference at a time, without losing quality.
- A staying-ahead proof shows greedy is at least as far along as any other solution after every step.
- The cut property (the lightest edge across a cut is safe) justifies Kruskal's and Prim's algorithms.
- State the loop invariant, including 'some optimal solution agrees with my choices so far'.
- On a matroid, greedy by weight is always optimal; Kruskal is the graphic-matroid case.