Chapter 21: Greedy Algorithms and Exchange Arguments
21.02Intermediate5 topics16 min2 interactive animations

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

Greedy fundamentals

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.

21.02.1

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.

StepActivity selection
Assume O optimal, different from greedyO's first interval f is not the earliest-finishing interval g
ExchangeReplace 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
ConclusionSome optimal schedule begins with g; recurse on the rest
21.02.2

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 input

Your input

Try
covered to 0

sorted by start

A
B
C
D
E
F
0123456789101112
Setup

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.

Step 1 / 10
Java
1static int minCover(int[][] iv, int from, int to) {
2 Arrays.sort(iv, Comparator.comparingInt(a -> a[0])); // by start
3 int count = 0, reach = from, i = 0;
4 while (reach < to) {
5 int farthest = reach;
6 while (i < iv.length && iv[i][0] <= reach) farthest = Math.max(farthest, iv[i++][1]); // all intervals that start in time
7 if (farthest == reach) return -1; // nothing extends the covered part: a gap
8 count++; reach = farthest; // take the one that reaches farthest
9 }
10 return count;
11}

Live state

from

0

to

12

reach

0

No condition evaluated on this step
Execution trace (dry run table)Show 10 steps
StepLineStateWhat happens
1L3from=0 to=12 reach=0Cover [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.
2L6reach=0 candidates=1 farthest=3Intervals A start at or before 0; the farthest end among them is 3.
3L8count=1 reach=3Take A: the covered part now reaches 3.
4L6reach=3 candidates=2 farthest=6Intervals B, C start at or before 3; the farthest end among them is 6.
5L8count=2 reach=6Take C: the covered part now reaches 6.
6L6reach=6 candidates=1 farthest=9Intervals D start at or before 6; the farthest end among them is 9.
7L8count=3 reach=9Take D: the covered part now reaches 9.
8L6reach=9 candidates=2 farthest=12Intervals E, F start at or before 9; the farthest end among them is 12.
9L8count=4 reach=12Take F: the covered part now reaches 12.
10L10return=4Covered [0, 12] with 4 intervals.
21.02.3

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 input

Your input

Try

edges, lightest first

1-2:1
1-3:2
0-2:3
0-1:4
2-3:4
2-4:5
3-4:7

parent[] (union-find)

0v0
1v1
2v2
3v3
4v4
Setup

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.

Step 1 / 13
Java
1static int kruskal(int n, int[][] edges) { // edges[i] = {u, v, weight}
2 Arrays.sort(edges, Comparator.comparingInt(e -> e[2])); // lightest first
3 int[] parent = new int[n]; for (int i = 0; i < n; i++) parent[i] = i;
4 int total = 0;
5 for (int[] e : edges) {
6 int a = find(parent, e[0]), b = find(parent, e[1]); // component leaders of the two ends
7 if (a != b) { parent[a] = b; total += e[2]; } // different components: keep the edge, merge them
8 } // same component: the edge would close a cycle, skip it
9 return total;
10}

Live state

vertices

5

edges

7

No condition evaluated on this step
Execution trace (dry run table)Show 13 steps
StepLineStateWhat happens
1L2vertices=5 edges=7Sort 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.
2L6edge=1-2:1 leader u=1 leader v=2 total=0Vertices 1 and 2 are in different components: keep the edge (weight 1).
3L7total=1 kept=1Merge the two components (parent[1] = 2). Total weight 1.
4L6edge=1-3:2 leader u=2 leader v=3 total=1Vertices 1 and 3 are in different components: keep the edge (weight 2).
5L7total=3 kept=2Merge the two components (parent[2] = 3). Total weight 3.
6L6edge=0-2:3 leader u=0 leader v=3 total=3Vertices 0 and 2 are in different components: keep the edge (weight 3).
7L7total=6 kept=3Merge the two components (parent[0] = 3). Total weight 6.
8L6edge=0-1:4 leader u=3 leader v=3 total=6Vertices 0 and 1 are already connected: this edge would close a cycle, skip it.
9L6edge=2-3:4 leader u=3 leader v=3 total=6Vertices 2 and 3 are already connected: this edge would close a cycle, skip it.
10L6edge=2-4:5 leader u=3 leader v=4 total=6Vertices 2 and 4 are in different components: keep the edge (weight 5).
11L7total=11 kept=4Merge the two components (parent[3] = 4). Total weight 11.
12L6edge=3-4:7 leader u=4 leader v=4 total=11Vertices 3 and 4 are already connected: this edge would close a cycle, skip it.
13L9return=11 edges=4 components=1Minimum spanning tree: 4 edges, total weight 11.
21.02.4

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.

Java
1static int selectWithInvariants(int[][] intervals) {
2 int[][] a = intervals.clone();
3 Arrays.sort(a, Comparator.comparingInt(x -> x[1]));
4 List<int[]> chosen = new ArrayList<>();
5 int lastEnd = Integer.MIN_VALUE;
6 for (int[] iv : a) {
7 if (iv[0] >= lastEnd) { chosen.add(iv); lastEnd = iv[1]; }
8 // invariant 1: chosen intervals are sorted and disjoint, lastEnd is the latest finish
9 for (int i = 1; i < chosen.size(); i++) {
10 if (chosen.get(i - 1)[1] > chosen.get(i)[0]) throw new AssertionError("overlap");
11 }
12 }
13 return chosen.size();
14}
21.02.5

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.

MatroidIndependent setsGreedy algorithm it explains
Graphic matroidEdge sets without a cycleKruskal's minimum spanning tree
Uniform matroid of rank kSets of at most k elementsPick the k largest values
Partition matroidAt most one element from each groupBest item per category
Not a matroidSets with total weight at most WGreedy knapsack is not optimal
Java
1// generic matroid greedy: the oracle says whether a set of element indices is independent
2static List<Integer> matroidGreedy(int[] weight, Predicate<List<Integer>> independent) {
3 Integer[] order = new Integer[weight.length];
4 for (int i = 0; i < order.length; i++) order[i] = i;
5 Arrays.sort(order, (x, y) -> weight[y] - weight[x]); // heaviest first
6 List<Integer> chosen = new ArrayList<>();
7 for (int i : order) {
8 chosen.add(i);
9 if (!independent.test(chosen)) chosen.remove(chosen.size() - 1);
10 }
11 return 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.