Take the best-looking step and never look back
A greedy algorithm makes the choice that looks best right now and never reconsiders it. No exploring alternatives, no backtracking, no table of results - just walk forward taking the locally best option each time.
The appeal is obvious. Greedy solutions are short, fast, and usually take a single pass. Where backtracking explores an exponential space and dynamic programming fills a table, greedy walks straight through in O(n log n) or better.
The catch is equally obvious once stated: the locally best step is not always part of the globally best answer. Sometimes taking a slightly worse step now opens a much better path later. Greedy cannot see that, because it never looks back.
So the entire skill in this chapter is not writing greedy code - that part is easy. It is knowing whether greedy is allowed on the problem in front of you.
Imagine it like this: Hiking uphill in fog by always stepping in the steepest upward direction. You will certainly reach a peak. Whether it is the highest peak is a completely different question.
- greedy
- - always taking the best immediate option, never revisiting
- local optimum
- - the best choice at this step, considered alone
- global optimum
- - the best possible final answer
Where it works, and where it fails
Coin change makes the difference concrete, and it is worth burning into memory because the same code is right or wrong depending on the input rather than the logic.
Making change with coins of 1, 5, 10 and 25, greedy is perfect: always take the largest coin that fits. For 30 it takes 25 then 5 - two coins, and no arrangement does better.
Now change the coin set to 1, 3 and 4, and ask for 6. Greedy takes 4, then 1, then 1 - three coins. But 3 + 3 is two coins. The greedy choice of 4 was locally best and globally wrong, because taking it made the remainder awkward.
Nothing about the algorithm changed. Only the coin set did. That is why 'greedy works for coin change' is a statement about your coins, not about coin change.
Coins {1, 3, 4}, and you need 6. Greedy takes the biggest coin that fits first. How many coins does it use, and is that optimal?
int greedyCoins(int[] coins, int amount) { // coins DESC
int used = 0;
for (int c : coins) {
while (amount >= c) { amount -= c; used++; }
}
return amount == 0 ? used : -1;
}
greedyCoins(new int[]{25, 10, 5, 1}, 30);
greedyCoins(new int[]{4, 3, 1}, 6);coins {25,10,5,1}, amount 30
take 25 -> 5 left
take 5 -> 0 left
greedy = 2 coins optimal = 2 CORRECT
coins {4,3,1}, amount 6
take 4 -> 2 left
take 1 -> 1 left
take 1 -> 0 left
greedy = 3 coins optimal = 2 (3+3) WRONG- canonical coin system
- - a coin set where greedy always gives the fewest coins - most real currencies are one
How to know greedy is safe
Two properties must hold. They sound abstract, so take them slowly - between them they explain every greedy proof you will ever read.
Greedy choice property: some optimal solution contains the greedy first choice. Not that greedy stumbles into the optimum by luck, but that taking the greedy step never rules the optimum out.
Optimal substructure: once you have taken that step, the rest of the problem is a smaller version of the same problem, and solving that optimally gives you an optimal whole.
In practice you prove the first with an exchange argument: take any optimal solution that does not start with the greedy choice, swap in the greedy choice, and show the result is no worse. If you can do that, greedy is safe. If the swap makes things worse, you have just found your counterexample - and you need dynamic programming instead, which is the next chapter.
Before writing greedy code, spend five minutes trying to break it. Build a small input where the obvious first step leads somewhere bad - the coin example is four numbers long. If you cannot break it in five minutes, greedy is probably fine. If you can, you just saved yourself shipping a subtly wrong answer.
- Greedy = best local step, never reconsidered
- It needs the greedy-choice property AND optimal substructure
- Prove it with an exchange argument, or break it with a counterexample
- When greedy fails, dynamic programming is usually the answer
- exchange argument
- - showing that swapping in the greedy choice never makes a solution worse
- optimal substructure
- - an optimal answer contains optimal answers to its sub-problems
The exchange argument, done concretely
'Prove greedy is safe' sounds like mathematics you do not have time for in an interview. In practice it is one sentence, and it is worth walking through once on a real problem.
Take interval scheduling: pick the most non-overlapping meetings. Greedy takes the one that finishes earliest. Here is the argument that it is safe.
Suppose some optimal schedule exists that does not start with the greedy choice. Swap its first meeting for the greedy one. The greedy meeting finishes no later than the one you removed, so it cannot conflict with anything the optimal schedule took afterwards. The swapped schedule is still valid and still the same size - so it is also optimal, and it starts with the greedy choice.
That is the whole technique: show that swapping in the greedy choice never makes things worse. If you can, greedy is safe. If the swap does make things worse, you have just constructed your counterexample - which is equally useful, because it tells you to reach for dynamic programming instead.
// greedy: sort by END time, take anything that starts after the last finish Arrays.sort(meetings, Comparator.comparingInt(m -> m[1]));
meetings (1,3) (2,5) (4,7) (6,8) (8,9) greedy by END: (1,3) (4,7) (8,9) -> 3 meetings greedy by START: (1,3) (4,7) (8,9) -> 3 (works here) greedy by SHORTEST: (2,5)... -> 2 BROKEN counterexample for shortest-first: (1,4) (3,5) (4,7) shortest is (3,5), which blocks BOTH others -> 1 meeting correct answer: (1,4) and (4,7) -> 2 meetings
- counterexample
- - a small input where the greedy rule provably misses the best answer
Recap
Greedy takes the best-looking step and never reconsiders. That makes it fast and short - and frequently wrong.
It is safe when two properties hold: some optimal solution contains the greedy first choice, and the remaining problem is a smaller version of the same problem.
The practical routine is to spend five minutes actively trying to break your rule on a tiny input before committing to it. The coin example is four numbers long; the interval counterexample is three. If you cannot break it quickly, greedy is probably fine.
- Greedy: best local step, never revisited
- Needs the greedy-choice property and optimal substructure
- Prove with an exchange argument, or break it with a counterexample
- Try to break it BEFORE writing it - counterexamples are tiny
- When greedy fails, dynamic programming is usually the answer
Your turn
Reading is not learning. Open each one in NeonFlow and watch your own code run, step by step.
Break three greedy rules
Three plausible rules for interval scheduling: earliest start, shortest duration, earliest end. Two of them are wrong. Find inputs that break them - the counterexamples are three intervals long.
import java.util.*;
public class BreakGreedy {
static int schedule(int[][] meetings, Comparator<int[]> rule) {
int[][] copy = meetings.clone();
Arrays.sort(copy, rule);
int taken = 0, freeFrom = Integer.MIN_VALUE;
for (int[] m : copy) {
if (m[0] >= freeFrom) { taken++; freeFrom = m[1]; }
}
return taken;
}
public static void main(String[] args) {
int[][] a = {{1,4},{3,5},{4,7}};
System.out.println("by end: " + schedule(a, Comparator.comparingInt(m -> m[1])));
System.out.println("by start: " + schedule(a, Comparator.comparingInt(m -> m[0])));
System.out.println("by duration: " + schedule(a, Comparator.comparingInt(m -> m[1]-m[0])));
// TODO: which rules give 2, and which gives 1? Then design your own
// counterexample that breaks "earliest start".
}
}Find the coin set that breaks greedy
Greedy coin change is optimal for {25,10,5,1} and wrong for {4,3,1}. Write a checker that compares greedy against a brute-force minimum, then search small amounts for a mismatch.
import java.util.*;
public class BreakCoins {
static int greedy(int[] coins, int amount) {
int used = 0;
for (int c : coins) while (amount >= c) { amount -= c; used++; }
return amount == 0 ? used : -1;
}
static int best(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, Integer.MAX_VALUE - 1);
dp[0] = 0;
for (int i = 1; i <= amount; i++)
for (int c : coins)
if (c <= i) dp[i] = Math.min(dp[i], dp[i - c] + 1);
return dp[amount] > amount ? -1 : dp[amount];
}
public static void main(String[] args) {
int[] coins = {4, 3, 1}; // must be descending for greedy
// TODO: for amounts 1..20, print any amount where greedy != best
}
}