Greedy PrinciplesWhen greedy is correct
The greedy-choice property and exchange arguments.
Used for: Deciding whether a problem can be greedy; if not, reach for DP
01Why it exists
Scheduling meetings, making change, compressing a file, planning a route — a great many problems have an obvious "take whatever looks best right now" approach. It runs fast and the code is short, but sometimes it returns the wrong answer, and it does so quietly: no exception, no warning.
Why this fitsGreedy is not one algorithm, it is a way of making decisions. What this lesson teaches is the test: when does "locally best" add up to "globally best", and when does it not? Judge it right and you get the fastest solution available; judge it wrong and you move to DP or to search.
Faced with an optimisation problem, you have a couple of minutes to decide between greedy and DP. Take the wrong turn and either you finish the greedy solution only to be handed a counterexample, or you spend half the interview on a DP when a single sort would have done.
Why this fitsThere is a fixed routine: guess a greedy rule, look for a counterexample, and if none turns up, try to prove the rule with an exchange argument. This lesson walks through that routine once, and every greedy solution in the lessons that follow is confirmed the same way.
DP is guaranteed correct, but the number of states is often O(n²) or worse. With millions of inputs, neither the memory nor the time holds up.
Why this fitsA problem whose greedy rule can be proved correct usually needs nothing more than a sort and a single scan: O(n log n) time and O(1) extra space. That is why greedy is worth learning — it is the cheapest optimisation method there is, as long as you know when it applies.
Reach for it when you see:Take the largest / smallest / earliest at every step, sort then scan once, never look back, locally optimal, exchange argument, counterexample.
02The core idea
The shape of a greedy algorithm never varies: sort the candidates by some rule, look at them one at a time, take whatever fits, and never revisit a decision. It never backtracks and never compares alternative selections, so it usually costs no more than O(n log n) for the sort plus O(n) for the scan. The price of that speed is that it is not always right, and whether a particular greedy rule is right depends on whether the problem has two properties.
The first is the greedy choice property: some optimal solution starts with the same move greedy makes. The second is optimal substructure: what remains after that first move is a problem of the same shape but smaller, and its optimal solution, with the first move attached, is optimal for the original problem. When both hold, every greedy step keeps the partial answer consistent with some optimal solution, so running all the way to the end produces one.
The standard tool for proving the greedy choice property is the exchange argument. Take any optimal solution O and find the first place where it differs from the greedy solution G; replace O's choice there with G's, and argue that O is still valid afterwards and no worse than before. That shows an optimal solution consistent with greedy exists. In interval scheduling, greedy takes the earliest-finishing interval G₁, and the first interval to finish in any optimal solution, O₁, cannot finish before G₁ does, so swapping O₁ for G₁ still leaves room for everything after it. Going the other way, both "earliest start" and "shortest" have counterexamples: an exchange there may have to give back two intervals or more, and the argument cannot be completed.
In practice the order of work is: guess a rule, hunt for a counterexample on small inputs (by hand, or against a brute-force solution), and switch rules or switch to DP the moment you find one; only when nothing turns up do you attempt the exchange argument. A common trap is treating "it passed a few test cases" as a proof, since a greedy rule usually fails only on particular inputs — [1, 3, 4] in the making-change problem is the classic example. Another is thinking of greedy and DP as opposites: greedy is really a special case of DP, and when the best transition out of every state can be decided without looking at the alternatives, DP degenerates into greedy.
03The algorithm
- 1Write the problem as a sequence of choices, where each choice leaves behind a smaller problem of the same shape.
- 2Guess a sort key (earliest finish, smallest, best ratio, …), sort by it, scan through, and take whatever fits.
- 3Hunt for a counterexample on small inputs: work 5 to 10 elements through by hand, or write a brute-force solution and compare on n ≤ 15. One counterexample means a different key, or no greedy at all.
- 4If nothing turns up, build an exchange argument: take any optimal solution O, replace its first choice that differs from the greedy one with the greedy choice, and argue that the result is still valid and no worse.
- 5Confirm the optimal substructure: after the first choice, what is left has the same shape. With both properties in hand the greedy answer is optimal, and the cost is usually O(n log n).
04Interactive demo
The same seven intervals under three greedy rules, stepping through what each rule takes and what it skips. The optimum is four. The panel at the bottom right explains why the rule works or why it fails: the correct one gets an exchange argument, and each wrong one gets a concrete counterexample.
Exchange argument: let O₁ be the earliest-finishing interval of any optimal solution, and let G₁ be the first interval the greedy rule takes. G₁ finishes earliest of all intervals, so its finish time is ≤ that of O₁. Swap O₁ for G₁: every other interval in the solution starts after O₁ ends, so nothing clashes and the count is unchanged. An optimal solution beginning with G₁ therefore exists, and repeating the argument on the remaining intervals shows that every greedy choice agrees with some optimal solution.
05Code
A generic sort-then-scan skeleton with three interchangeable sort keys, plus a brute-force solution to check against. This is exactly how you hunt for counterexamples: run greedy and brute force side by side on small inputs. So that all three rules can share it, the skeleton compares every candidate against all the intervals chosen so far; once you have committed to sorting by finish time, comparing against the last chosen one is enough, and the Interval Scheduling lesson writes it that way in O(n log n).
# Interval scheduling: one skeleton, three sort keys, and only "earliest finish" is correct
def schedule(intervals, key):
"""intervals is a list of (start, end); key decides the greedy order.
Returns the chosen intervals, none of which overlap (touching endpoints are allowed)."""
chosen = []
for s, e in sorted(intervals, key=key):
# Only take an interval that clashes with none of the chosen ones. Comparing against the
# last finish time alone is not enough: sorting by length can hand us an earlier interval later
if all(e <= cs or s >= ce for cs, ce in chosen):
chosen.append((s, e))
return chosen
def by_start(iv): # earliest start: wrong
return iv[0]
def by_length(iv): # shortest: wrong
return (iv[1] - iv[0], iv[0])
def by_end(iv): # earliest finish: correct
return iv[1]
# Brute force: enumerate every subset on small inputs and see whether greedy reached the optimum
# Run this first when no exchange argument comes to mind; a counterexample saves you the proof
from itertools import combinations
def brute_force(intervals):
n = len(intervals)
for r in range(n, 0, -1): # largest sizes first, so the first feasible one is optimal
for subset in combinations(intervals, r):
ok = all(a[1] <= b[0] or b[1] <= a[0] for a, b in combinations(subset, 2))
if ok:
return r
return 0
if __name__ == "__main__":
ivs = [(0, 12), (1, 5), (4, 7), (6, 10), (11, 14), (13, 16), (15, 18)]
print(brute_force(ivs)) # 4
for name, key in [("start", by_start), ("length", by_length), ("end", by_end)]:
print(name, len(schedule(ivs, key))) # start 2, length 3, end 406Practice
- LeetCode 455Assign Cookies (sort, then two pointers)Easy
- LeetCode 1029Two City Scheduling (sort by the difference)Medium
- LeetCode 763Partition LabelsMedium
- LeetCode 406Queue Reconstruction by HeightMedium
- LeetCode 621Task SchedulerMedium
- LeetCode 135Candy (two greedy passes)Hard