Greedy fundamentals
What a greedy algorithm is: committing to a locally best choice, the greedy-choice property, optimal substructure, how greedy differs from dynamic programming, and how to find counterexamples.
Learning goals
- Explain a greedy algorithm as a sequence of irrevocable local choices
- State the greedy-choice property and optimal substructure and check them for a problem
- Tell greedy, dynamic programming and backtracking apart by what they do with a choice
- Hunt for counterexamples with small inputs and a brute-force checker
Prerequisites
A greedy algorithm never looks back. That makes it the fastest and simplest design you will meet, and also the easiest to get wrong: the rule that feels obviously right is often subtly false.
Locally optimal decision
A **greedy algorithm** builds a solution one decision at a time. At each step it takes whatever looks best right now by some simple rule, and it never reconsiders. Contrast this with backtracking from the previous chapter, which tries a choice and undoes it, and with dynamic programming, which compares several choices before committing.
Activity selection is the standard first example: given time intervals, choose as many that do not overlap as possible. The locally best choice is to take the interval that **finishes earliest**, because it leaves the most room for the rest. After taking it, discard everything that overlaps it and repeat. Run the animation and watch the same decision repeated: look at the next interval, keep it if it starts at or after the last chosen finish, skip it otherwise.
Notice what is missing: no recursion, no table, no undo. After sorting, a single pass makes every decision, so the whole algorithm costs O(n log n). That speed is the payoff for the proof obligation the next lessons describe.
1static int selectActivities(int[][] intervals) {2int[][] a = intervals.clone();3Arrays.sort(a, Comparator.comparingInt(x -> x[1])); // earliest finish first4int count = 0, lastEnd = Integer.MIN_VALUE;5for (int[] iv : a) {6if (iv[0] >= lastEnd) { // compatible with everything chosen so far7count++;8lastEnd = iv[1]; // the decision is final: never undone9}10}11return count;12}
Activity selection: which greedy rule works?
Interactive · edit the inputsorted by earliest finish time
Sort the 8 intervals by earliest finish time (optimal). Then scan once and take every interval that starts at or after the end of the last chosen one.
1static int select(int[][] iv, int mode) { // mode 0: earliest finish (optimal), 1: earliest start, 2: shortest2Comparator<int[]> rule = mode == 0 ? Comparator.comparingInt(a -> a[1])3: mode == 1 ? Comparator.comparingInt(a -> a[0])4: Comparator.comparingInt(a -> a[1] - a[0]);5Arrays.sort(iv, rule);6int count = 0, lastEnd = Integer.MIN_VALUE;7for (int[] cur : iv) {8if (cur[0] >= lastEnd) { // compatible with everything chosen so far (half-open intervals)9count++; lastEnd = cur[1]; // choose it10} // otherwise skip it11}12return count;13}
Live state
intervals
8
rule
earliest finish time (optimal)
Execution trace (dry run table)Show 13 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L5 | intervals=8 rule=earliest finish time (optimal) | Sort the 8 intervals by earliest finish time (optimal). Then scan once and take every interval that starts at or after the end of the last chosen one. |
| 2 | L8 | start=1 end=4 last end=none | [1, 4) does not overlap the last choice: take it. |
| 3 | L9 | chosen=1 last end=4 | Chosen A = [1, 4). The last end is now 4. |
| 4 | L8 | start=3 end=5 last end=4 | [3, 5) starts before 4: it overlaps a chosen interval, skip it. |
| 5 | L8 | start=0 end=6 last end=4 | [0, 6) starts before 4: it overlaps a chosen interval, skip it. |
| 6 | L8 | start=5 end=7 last end=4 | [5, 7) does not overlap the last choice: take it. |
| 7 | L9 | chosen=2 last end=7 | Chosen D = [5, 7). The last end is now 7. |
| 8 | L8 | start=3 end=9 last end=7 | [3, 9) starts before 7: it overlaps a chosen interval, skip it. |
| 9 | L8 | start=5 end=9 last end=7 | [5, 9) starts before 7: it overlaps a chosen interval, skip it. |
| 10 | L8 | start=6 end=10 last end=7 | [6, 10) starts before 7: it overlaps a chosen interval, skip it. |
| 11 | L8 | start=8 end=11 last end=7 | [8, 11) does not overlap the last choice: take it. |
| 12 | L9 | chosen=3 last end=11 | Chosen H = [8, 11). The last end is now 11. |
| 13 | L12 | chosen=3 removed=5 optimum=3 return=3 | Chosen 3 intervals, which is optimal (5 must be removed to leave a non-overlapping set). |
Greedy-choice property
The **greedy-choice property** says: there is always an optimal solution that contains the greedy choice. If this holds, taking the greedy choice loses nothing, so the algorithm can commit to it and continue with the smaller problem that remains.
The property is a claim about the problem and the rule together, not about the problem alone. Earliest finish time has it for interval scheduling, earliest start time does not. The best value-to-weight ratio has it for the fractional knapsack, but not for the 0/1 knapsack where items cannot be split. Proving the property is the work of the next lesson group: the exchange argument and its relatives.
| Problem | Greedy rule | Greedy-choice property |
|---|---|---|
| Activity selection | Earliest finish time | Holds |
| Fractional knapsack | Highest value per weight | Holds |
| Huffman coding | Merge the two lightest trees | Holds |
| Minimum spanning tree | Lightest edge that crosses a cut | Holds (cut property) |
| 0/1 knapsack | Highest value per weight | Fails |
| Coin change, arbitrary coins | Largest coin that fits | Fails (for example coins 1, 3, 4) |
| Shortest path with negative edges | Settle the closest vertex | Fails |
Optimal substructure
**Optimal substructure** means that after the greedy choice is made, the remaining problem is a smaller instance of the same problem, and an optimal solution to it combines with the choice to give an optimal solution to the whole. For activity selection, once the earliest-finishing interval is taken, what remains is activity selection on the intervals that start at or after its finish.
Substructure alone is not enough for greedy. Dynamic programming problems have it too (the shortest path through a vertex is built from shortest paths to its predecessors). What distinguishes greedy is that one particular subproblem is guaranteed to be the right one, so the others never need to be solved. Greedy needs both properties; dynamic programming needs only optimal substructure plus overlapping subproblems.
Greedy vs DP
Both techniques exploit optimal substructure. Dynamic programming evaluates **every** candidate first choice, solves the subproblem each leaves, and keeps the best. Greedy picks the one candidate its rule favours and solves only that subproblem. When the greedy-choice property holds the answers agree and greedy is cheaper; when it fails, only dynamic programming is correct.
Coin change shows both sides. Run the animation on the “Canonical system (US coins)” preset: greedy takes 25, 25, 10, 1, 1, 1 for 63 and uses 6 coins, which is optimal. Switch to “Greedy fails (1, 3, 4 for 6)”: greedy takes 4, 1, 1 for 3 coins while 3 + 3 uses only 2. The rule did not change; the coin system did.
| Greedy | Dynamic programming | Backtracking | |
|---|---|---|---|
| Choices examined per step | One | All, results stored | All, one after another |
| Revisits a choice | Never | Compares stored results | Undoes and retries |
| Needs a proof | Yes | Only a correct recurrence | No |
| Typical cost | O(n log n) | Polynomial in the table size | Exponential |
Greedy coin change vs the optimum
Interactive · edit the inputgreedy picks · remaining 6
Make 6 with the coins [4, 3, 1]. Greedy takes as many of the largest coin as fit, then moves to the next coin.
1static int greedyCoins(int[] coins, int amount) { // coins sorted in descending order; -1 if stuck2int count = 0;3for (int c : coins) {4count += amount / c; // as many of the biggest coin as fit5amount %= c;6}7return amount == 0 ? count : -1;8}
Live state
amount
6
coins
[4, 3, 1]
Execution trace (dry run table)Show 5 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | amount=6 coins=[4, 3, 1] | Make 6 with the coins [4, 3, 1]. Greedy takes as many of the largest coin as fit, then moves to the next coin. |
| 2 | L4 | coin=4 used=1 remaining=2 | 1 × 4 = 4: 2 remains. |
| 3 | L4 | coin=3 used=0 remaining=2 | 3 is larger than the remaining 2: skip it. |
| 4 | L4 | coin=1 used=2 remaining=0 | 2 × 1 = 2: 0 remains. |
| 5 | L7 | return=3 optimum=2 | Greedy used 3 coins but the optimum is 2: greedy is wrong for this coin system. |
Counterexamples
A greedy rule is guilty until proven innocent, and the quickest way to convict it is a small counterexample. Try the rules for activity selection in the animation by changing the rule option: earliest start picks the long interval that begins first and blocks everything else (the “Earliest start fails” preset takes 1 interval where 3 fit), and shortest duration picks a short interval sitting in the middle of two longer compatible ones (the “Shortest duration fails” preset takes 1 where 2 fit).
A systematic way to search: write the greedy algorithm, write a slow brute-force version that is obviously correct, and compare them on every input of a small size, or on thousands of random small inputs. A mismatch is a counterexample you can read by eye. Counterexamples are usually tiny: three to five items are almost always enough.
1// brute force for activity selection: try every subset (n <= 20)2static int bruteMaxActivities(int[][] a) {3int n = a.length, best = 0;4for (int mask = 0; mask < (1 << n); mask++) {5boolean ok = true;6for (int i = 0; i < n && ok; i++) {7if ((mask >> i & 1) == 0) continue;8for (int j = i + 1; j < n && ok; j++) {9if ((mask >> j & 1) == 0) continue;10ok = a[i][1] <= a[j][0] || a[j][1] <= a[i][0]; // disjoint, touching allowed11}12}13if (ok) best = Math.max(best, Integer.bitCount(mask));14}15return best;16}
Activity selection: which greedy rule works?
Interactive · edit the inputsorted by earliest finish time
Sort the 8 intervals by earliest finish time (optimal). Then scan once and take every interval that starts at or after the end of the last chosen one.
1static int select(int[][] iv, int mode) { // mode 0: earliest finish (optimal), 1: earliest start, 2: shortest2Comparator<int[]> rule = mode == 0 ? Comparator.comparingInt(a -> a[1])3: mode == 1 ? Comparator.comparingInt(a -> a[0])4: Comparator.comparingInt(a -> a[1] - a[0]);5Arrays.sort(iv, rule);6int count = 0, lastEnd = Integer.MIN_VALUE;7for (int[] cur : iv) {8if (cur[0] >= lastEnd) { // compatible with everything chosen so far (half-open intervals)9count++; lastEnd = cur[1]; // choose it10} // otherwise skip it11}12return count;13}
Live state
intervals
8
rule
earliest finish time (optimal)
Execution trace (dry run table)Show 13 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L5 | intervals=8 rule=earliest finish time (optimal) | Sort the 8 intervals by earliest finish time (optimal). Then scan once and take every interval that starts at or after the end of the last chosen one. |
| 2 | L8 | start=1 end=4 last end=none | [1, 4) does not overlap the last choice: take it. |
| 3 | L9 | chosen=1 last end=4 | Chosen A = [1, 4). The last end is now 4. |
| 4 | L8 | start=3 end=5 last end=4 | [3, 5) starts before 4: it overlaps a chosen interval, skip it. |
| 5 | L8 | start=0 end=6 last end=4 | [0, 6) starts before 4: it overlaps a chosen interval, skip it. |
| 6 | L8 | start=5 end=7 last end=4 | [5, 7) does not overlap the last choice: take it. |
| 7 | L9 | chosen=2 last end=7 | Chosen D = [5, 7). The last end is now 7. |
| 8 | L8 | start=3 end=9 last end=7 | [3, 9) starts before 7: it overlaps a chosen interval, skip it. |
| 9 | L8 | start=5 end=9 last end=7 | [5, 9) starts before 7: it overlaps a chosen interval, skip it. |
| 10 | L8 | start=6 end=10 last end=7 | [6, 10) starts before 7: it overlaps a chosen interval, skip it. |
| 11 | L8 | start=8 end=11 last end=7 | [8, 11) does not overlap the last choice: take it. |
| 12 | L9 | chosen=3 last end=11 | Chosen H = [8, 11). The last end is now 11. |
| 13 | L12 | chosen=3 removed=5 optimum=3 return=3 | Chosen 3 intervals, which is optimal (5 must be removed to leave a non-overlapping set). |
Practice ladder
Now try it yourself
Easy
- Assign Cookies
- Best Time to Buy and Sell Stock II
Medium
- Jump Game
- Partition Labels
Advanced
- Candy
- Task Scheduler
Key takeaways
- A greedy algorithm makes one irrevocable local choice per step, usually after sorting or with a heap.
- It is correct only when the greedy-choice property and optimal substructure both hold.
- Greedy looks at one candidate, dynamic programming at all of them, backtracking at all of them with undo.
- Test any greedy rule against a brute-force checker on small inputs before trusting it.