AnyLearn
All lessons

Greedy: Proving a Local Choice Is Globally Right

A greedy algorithm is three lines of code and a proof. This lesson covers the proof techniques that make it an algorithm rather than a heuristic: the exchange argument, greedy-stays-ahead, Huffman's merge, and the matroid theorem that says exactly when greedy is guaranteed.

Updated · AI-authored, review-gated · how lessons are made

Not signed in: your progress and quiz score won't be saved.
Progress1 / 9

The algorithm is trivial, the proof is the work

Every greedy algorithm has the same shape: sort by some key, walk the list, take whatever is still feasible. That part is three lines. It is never the difficult part, and it is never where the mistakes are.

The difficulty is entirely in two questions. Which key? and why does taking the best-looking option now never cost you later? Get the first wrong and the code still runs, returning answers that are plausible and sometimes even optimal on the cases you tried. Skip the second and you do not have an algorithm, you have a heuristic that you have not measured.

So greedy is best understood as a proof technique with a small amount of code attached. This lesson is about the proofs: two general-purpose argument shapes, and one theorem that settles the question exactly.

Full lesson text

All 9 steps on one page, for reading, reference, and search.

Show

1. The algorithm is trivial, the proof is the work

Every greedy algorithm has the same shape: sort by some key, walk the list, take whatever is still feasible. That part is three lines. It is never the difficult part, and it is never where the mistakes are.

The difficulty is entirely in two questions. Which key? and why does taking the best-looking option now never cost you later? Get the first wrong and the code still runs, returning answers that are plausible and sometimes even optimal on the cases you tried. Skip the second and you do not have an algorithm, you have a heuristic that you have not measured.

So greedy is best understood as a proof technique with a small amount of code attached. This lesson is about the proofs: two general-purpose argument shapes, and one theorem that settles the question exactly.

2. Interval scheduling, and three keys that fail

One resource, a set of requests each with a start and an end time, and the goal of accepting as many non-overlapping requests as possible. Four sorting keys suggest themselves, and intuition picks a losing one.

  • Earliest start. Requests [0,100][0,100], [1,2][1,2], [3,4][3,4]. Greedy takes the long one and accepts 1; the optimum is 2.
  • Shortest duration. Requests [0,10][0,10], [9,11][9,11], [10,20][10,20]. The shortest is [9,11][9,11], which blocks both others. Greedy accepts 1; the optimum is 2.
  • Fewest conflicts. Plausible, and it also fails, though the counterexample needs a larger construction.
  • Earliest finish time. This one is optimal.

The intuition behind the winner: finishing early is the only property that leaves the largest possible remainder of the timeline for everything still to come. Duration and start time both fail because neither controls when the resource is handed back.

3. The whole algorithm is the sort key

def schedule(requests):
    """requests: list of (start, end). Returns a maximum compatible subset."""
    chosen, last_end = [], float("-inf")
    for start, end in sorted(requests, key=lambda r: r[1]):   # by finish time
        if start >= last_end:
            chosen.append((start, end))
            last_end = end
    return chosen

The running time is O(nlogn)O(n \log n), dominated by the sort; the scan itself is linear. Compare that against solving the same problem by dynamic programming, which would need a table over request indices and a search for the last compatible request.

Nothing in this code hints that it is correct. Change r[1] to r[0] and you get the earliest-start version, which is wrong, and the diff is one character. The correctness lives in an argument that is nowhere in the file.

4. The exchange argument

The exchange argument never tries to prove greedy is optimal directly. It starts from an arbitrary optimal solution and transforms it into the greedy one, one swap at a time, showing that no swap ever makes it worse.

Step C is where the sort key earns its keep. Because greedy always takes the earliest available finish time, its choice frees the resource at least as early as whatever the optimal solution chose, so everything the optimal solution scheduled afterwards still fits.

The pattern generalises far beyond scheduling. Whenever you want to justify a greedy rule, the first thing to try is: assume an optimum that disagrees with me, and show I can force it to agree without penalty.

flowchart TD
A["Take any optimal solution O"] --> B["Find the first choice where O differs from greedy"]
B --> C["Greedy's request finishes no later than O's"]
C --> D["Swap greedy's request into O"]
D --> E["O is still conflict-free and still the same size"]
E --> F["Repeat: the disagreement moves one step later"]
F --> G["O has become greedy, so greedy is optimal too"]

5. Greedy stays ahead

The second standard proof shape, named this way in Kleinberg and Tardos's Algorithm Design, proves an invariant instead of performing swaps.

Let greedy's accepted requests be g1,g2,g_1, g_2, \ldots and any optimal solution's be o1,o2,o_1, o_2, \ldots, both in order of finish time. Claim: for every rr, greedy's rr-th request finishes no later than the optimum's rr-th.

It holds for r=1r = 1 because greedy takes the globally earliest finish. If it holds at r1r-1, then oro_r begins after or1o_{r-1} ends, which is at or after when gr1g_{r-1} ends, so oro_r was available when greedy made its rr-th pick. Greedy takes the earliest finisher among available requests, so grg_r finishes no later.

Now suppose the optimum has more requests than greedy. Then ok+1o_{k+1} was still available when greedy stopped, and greedy would have taken it.

6. Huffman: a greedy choice that is not a sort

Huffman's 1952 paper in the Proceedings of the IRE builds a minimum-redundancy prefix code, and its greedy choice is not a sorting key. It is a merge: repeatedly combine the two lowest-frequency symbols into one node whose frequency is their sum.

With frequencies a:45a{:}45, b:13b{:}13, c:12c{:}12, d:16d{:}16, e:9e{:}9, f:5f{:}5 out of 100, the merges run e+f=14e+f = 14, then c+b=25c+b = 25, then 14+d=3014+d = 30, then 25+30=5525+30 = 55, then 45+5545+55. The resulting code costs 224 bits per 100 symbols against 300 for a 3-bit fixed-length code.

The exchange argument again: in some optimal tree the two rarest symbols are siblings at maximum depth. If they are not, swap them there. Moving rarer symbols deeper and commoner ones shallower cannot increase the total, so an optimal tree with that shape exists, which is exactly what the merge assumes.

7. Matroids: when greedy is guaranteed, exactly

Ad hoc proofs invite the question of whether there is a general criterion. There is. A matroid is a ground set EE with a family I\mathcal{I} of subsets called independent, satisfying:

  1. The empty set is independent.
  2. Hereditary: any subset of an independent set is independent.
  3. Exchange: if AA and BB are independent and A<B|A| < |B|, some element of BB can be added to AA keeping it independent.

The Rado-Edmonds theorem, from Edmonds's 1971 paper Matroids and the greedy algorithm in Mathematical Programming, says: for a hereditary system, the greedy algorithm finds a maximum-weight independent set for every linear weight function precisely when the system is a matroid. The condition is not merely sufficient, it is necessary.

Kruskal's 1956 minimum spanning tree algorithm is exactly this. Sets of edges containing no cycle form the graphic matroid, so sorting edges by weight and adding whatever keeps the set acyclic is optimal by the theorem, with no bespoke proof required.

8. Where the guarantee runs out

Outside a matroid, greedy loses its exactness guarantee, and what remains depends on the structure.

Sometimes you get an approximation instead. Maximising a monotone submodular function under a cardinality constraint is not a matroid problem, and greedy is not optimal, but it is provably within a factor of 11/e1 - 1/e of the best possible. The submodular-optimization path develops that case.

Sometimes an assumption is quietly load-bearing. Dijkstra's 1959 shortest-path algorithm is greedy: repeatedly finalise the nearest unfinalised vertex. That is correct only because edge weights are non-negative, which is what makes distances monotone along a path. Introduce a single negative edge and a vertex you already finalised can later be improved. The repair is not a better greedy rule, it is a different technique: Bellman-Ford relaxes every edge V1|V|-1 times, and it is dynamic programming.

9. Can you just test whether greedy works?

Trying examples is not evidence, but for restricted families the question is genuinely decidable.

Change-making is the clean case. Given a set of coin denominations, is the greedy algorithm optimal for every target? Chang and Gill gave a procedure polynomial in the size of the largest coin; Kozen and Zaks asked whether one exists that is polynomial in the size of the input. Pearson answered yes in Operations Research Letters in 2005, by characterising the smallest counterexample and showing it must lie among O(n2)O(n^2) candidate values for nn denominations. Test those, and you know.

Two things are worth separating here. Deciding whether greedy is optimal for a coin system is polynomial. Actually making optimal change in an arbitrary system is NP-hard. So a cheap test tells you whether you may use the cheap algorithm, and when the answer is no, you fall back to dynamic programming.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. For interval scheduling, why does sorting by shortest duration fail?
    • It cannot break ties when two requests have equal length
    • A short request can sit across the boundary of two longer ones and block both
    • It requires the requests to be sorted twice, which changes the result
    • Short requests tend to arrive later, so the scan misses them
  2. What does an exchange argument actually prove?
    • That greedy's running time matches the dynamic programming solution
    • That the problem has no optimal substructure and so needs greedy
    • That any optimal solution can be transformed into the greedy one without getting worse
    • That the greedy solution is unique among all optimal solutions
  3. According to the Rado-Edmonds theorem, when does greedy find a maximum-weight independent set for every linear weight function?
    • Whenever the independent sets are closed under taking subsets
    • Whenever the weight function is non-negative
    • Whenever the ground set can be sorted in O(n log n) time
    • Exactly when the independence system is a matroid
  4. Why does Dijkstra's algorithm require non-negative edge weights?
    • Because a negative edge means an already finalised vertex could later be reached more cheaply
    • Because the priority queue cannot store negative keys
    • Because negative weights create cycles that the algorithm cannot detect
    • Because the shortest path would otherwise not be unique
  5. What did Pearson's 2005 result establish about coin systems?
    • That greedy change-making is optimal for any system containing a 1-unit coin
    • That whether greedy is optimal for a given system can be decided in polynomial time
    • That optimal change-making is solvable in polynomial time for all systems
    • That every coin system has a counterexample if it has at least four denominations

Related lessons

Computer Science
advanced

Conditioning and Stability: Whose Fault Is the Error

Two different things can make a numerical answer inaccurate: the problem may amplify any input perturbation, or the algorithm may introduce its own. They are separate quantities, one belongs to the problem and one to the code, and only the second is fixable. This lesson separates them and shows what follows.

8 steps·~12 min
Computer Science
advanced

Cancellation: Where the Digits Actually Go

Every operation is correctly rounded, yet results can be wrong in the first digit. The reason is that subtracting nearly equal numbers is exact and still catastrophic: it does not create error, it promotes error that was already there. This lesson builds that mechanism and the summation techniques that recover the lost accuracy.

8 steps·~12 min
Computer Science
advanced

Count-Min, and Why Sketches Compose

The count-min sketch estimates how often an item occurred using a fixed grid of counters, and it never underestimates. This lesson builds it, states the error bound that makes it usable, shows where it is useless, and ends on the property shared by all three structures that explains why they run distributed systems.

8 steps·~12 min
Computer Science
advanced

HyperLogLog: Counting Distinct Items in Kilobytes

Counting distinct items exactly needs memory proportional to the count. HyperLogLog answers the same question in a fixed twelve kilobytes, for cardinalities into the billions, by measuring an improbable event rather than storing anything. This lesson builds that idea from the leading-zeros intuition up.

8 steps·~12 min