~/searching/greedy
Greedy algorithms
Make the choice that looks best right now and never undo it. Works when you can prove that choice is always safe.
Sort by the right key, then take each item that still fits. For the most non-overlapping intervals, the key is the end time.
An exchange argument shows the locally best choice can always be part of some best answer.
O(n log n)
O(n) for the sorted copy
You’ll recognise it when
- You pick the most meetings, events or jobs that don’t clash, or the fewest to remove so the rest don’t.
- Sorting the input by one key and making a single pass feels like it should work.
- The problem hints at an order: earliest deadline, smallest first, farthest reach.
nis up to 10⁵ or more, so trying combinations or an O(n²) table is too slow.
The neighbour is dynamic programming: when a locally best choice can turn out wrong later (intervals with weights, odd coin systems), you need DP instead.
The idea
You run one meeting room and have a pile of booking requests. To fit in the most meetings, which one should you book first? Not the one that starts first, and not the shortest: the one that finishes first. It hands the room back soonest, which leaves the most time for everyone else.
That’s the whole greedy pattern: make the choice that looks best right now, commit to it, and never go back. It’s fast and short to write. The catch is that “looks best” is often wrong, so a greedy is only as good as the argument that its choice is safe.
How it works
Each interval [start, end) is a meeting from start up to, but not including, end, so one that ends at 5 and one that starts at 5 don’t clash. The graphic follows the steps as you scroll; you can also edit the intervals, or switch the strategy to start or shortest to watch the wrong orders lose.
- Start with nothing booked:
count = 0, andlast_end = −∞because the room is free from the beginning of time. - Sort by end time. The meeting that frees the room soonest comes first.
- Go down the list. For each interval, compare its
startwithlast_end, the end of the last meeting you took. - If
start >= last_end, the room is free: take it, add one tocount, and movelast_endto its end. - If
start < last_end, it overlaps the meeting you just took: skip it. Nothing is lost, because the one you kept ends no later. - The long meeting A
[0, 10)starts first, so a start-time greedy would grab it and block almost everything. Here it’s simply skipped. - C starts at exactly 9, where F ended. Touching isn’t overlapping, so it’s taken. That’s why the test is
>=, not>. - At the end of the list,
countis the answer: 4 meetings.
Why the first pick is safe (the exchange argument). Let g be the interval that ends first. Take any best answer and look at its earliest-ending interval o. Swap o for g: since g ends no later than o, it can’t clash with anything that came after o, so the answer is still valid and just as big. So some best answer contains g. Commit to it, throw away everything that overlaps it, and the rest is the same problem on what’s left, where the same argument applies again.
def max_non_overlapping(intervals):
"""Most intervals [start, end) you can keep with no two overlapping.
Intervals that only touch, like [1, 3) and [3, 5), don't overlap."""
count, last_end = 0, float("-inf")
by_end = sorted(intervals, key=lambda iv: iv[1])
for start, end in by_end:
if start < last_end:
continue
count += 1
last_end = end
return count#include <algorithm>
#include <climits>
#include <utility>
#include <vector>
using namespace std;
// Most intervals [start, end) you can keep with no two overlapping.
// Intervals that only touch, like [1, 3) and [3, 5), don't overlap.
int maxNonOverlapping(vector<pair<long long, long long>> intervals) {
int count = 0;
long long last_end = LLONG_MIN;
sort(intervals.begin(), intervals.end(),
[](const auto& a, const auto& b) { return a.second < b.second; });
for (auto [start, end] : intervals) {
if (start < last_end)
continue;
count++;
last_end = end;
}
return count;
}import java.util.Arrays;
import java.util.Comparator;
class Intervals {
// Most intervals [start, end) you can keep with no two overlapping.
// Intervals that only touch, like [1, 3) and [3, 5), don't overlap.
static int maxNonOverlapping(long[][] intervals) {
int count = 0;
long last_end = Long.MIN_VALUE;
long[][] byEnd = intervals.clone();
Arrays.sort(byEnd, Comparator.comparingLong(iv -> iv[1]));
for (long[] iv : byEnd) {
long start = iv[0], end = iv[1];
if (start < last_end)
continue;
count++;
last_end = end;
}
return count;
}
}“Remove as few intervals as possible so the rest don’t overlap” is the same problem in disguise: the answer is n - count.
Orders that fail. Sort by start and the default input keeps 2: A [0, 10) starts first and blocks the room until 10. Shortest first fails too: with [1, 5), [4, 7), [6, 10) it picks the short [4, 7), which clashes with both others, and keeps 1 instead of 2. Neither choice survives the swap in the proof: a meeting that starts early or is short can still end late.
Greedy isn’t a universal trick. Making change with coins 1, 3 and 4 for 6, “largest coin first” pays 4 + 1 + 1, three coins, when 3 + 3 needs two. For coin systems like that, and for 0/1 knapsack, you need DP.
Why it’s O(n log n)
Sorting takes O(n log n). The pass after it looks at each interval once and does one comparison, so it’s O(n). Sorting dominates.
Space is O(n) for the sorted copy, or O(1) extra if you may sort the input in place. There’s no table and no conflict graph, which is what makes greedy so much cheaper than DP when it works.
Common mistakes
Sorting by start (or by the whole pair)
sorted(intervals) sorts pairs by their first element, the start. That’s the start-time greedy, and one long early interval wrecks it.
by_end = sorted(intervals) # ✗ sorts by start
by_end = sorted(intervals, key=lambda iv: iv[1]) # ✓ sorts by end
Getting touching intervals wrong
Decide first whether [1, 3) and [3, 5) clash. For half-open intervals they don’t, so the test is start >= last_end. With > you drop every interval that begins exactly when the last one ended.
if start > last_end: # ✗ rejects [3, 5) after [1, 3)
if start >= last_end: # ✓ touching is fine
Starting last_end at 0
If times can be negative, last_end = 0 makes the first interval that starts below 0 look like a clash. Start at minus infinity (or the smallest value your type holds).
last_end = 0 # ✗ skips [-5, -2)
last_end = float("-inf") # ✓ the room is free from the start
A comparator that overflows
In Java, (a, b) -> (int) (a[1] - b[1]) overflows when ends differ by more than about 2 × 10⁹, and the sort order silently breaks. Use Long.compare or Comparator.comparingLong.
Arrays.sort(iv, (a, b) -> (int) (a[1] - b[1])); // ✗ can overflow
Arrays.sort(iv, Comparator.comparingLong(a -> a[1])); // ✓
Variations
- Minimum arrows to burst balloons. Sort balloons by end and shoot at the end of the first one; it pops everything that starts by then. Balloons are closed intervals, so touching ones share a point and one arrow gets both: shoot a new arrow only when
start > last_end. - Jump game. Walk left to right keeping
farthest, the furthest index you can reach so far. If you ever stand ati > farthest, you’re stuck; otherwise updatefarthest = max(farthest, i + nums[i]). - Gas station. If total gas is less than total cost, there’s no answer. Otherwise run a tank from station 0; whenever it goes negative after station
i, no station from your start up toican work, so restart ati + 1with an empty tank. - Huffman coding. To build the shortest prefix code, repeatedly merge the two least frequent symbols, using a heap. An exchange argument again shows the two rarest can always be the deepest pair of leaves.
- Greedy or DP? Try to break the greedy on a few small inputs first. If an exchange argument works, greedy wins. If a small counterexample appears (weighted intervals, coins 1, 3, 4), use dynamic programming.
Check yourself
5 quick questions. Pick an answer to see why it's right or wrong.
-
1
You want to keep the most meetings in one room with no two overlapping. You’ll sort them and then take each one that still fits. What do you sort by?
The meeting that ends first hands the room back soonest. Swap it into any best answer in place of that answer’s first meeting and nothing breaks, so it’s always safe to take. Shortest-first sounds sensible but fails when a short meeting sits across two longer ones that don’t clash with each other.
-
2
The same greedy, fed two different orders. What does this print?
def pick(ivs):count, last_end = 0, float("-inf")for s, e in ivs:if s >= last_end:count, last_end = count + 1, ereturn countivs = [(1, 10), (2, 3), (4, 5)]print(pick(sorted(ivs)), pick(sorted(ivs, key=lambda iv: iv[1])))sorted(ivs)sorts by start, so(1, 10)goes first and blocks the other two: 1. Sorted by end,(2, 3)and(4, 5)both fit: 2. One long interval that starts early is the classic way to break start-time greedy. -
3
Intervals are half-open, so
[1, 2)and[2, 3)don’t overlap. This counts the fewest removals so the rest don’t overlap; the right answer here is 1. What does it print?def removals(ivs):keep, last_end = 0, float("-inf")for s, e in sorted(ivs, key=lambda iv: iv[1]):if s > last_end:keep, last_end = keep + 1, ereturn len(ivs) - keepprint(removals([(1, 2), (2, 3), (3, 4), (1, 3)]))With
>,(2, 3)is rejected because 2 isn’t greater than the last end, 2. The greedy keeps only(1, 2)and(3, 4)and reports 2 removals. With>=it keeps three and removes just(1, 3). Decide whether touching counts as overlap before you write the comparison. -
4
Coins
[1, 3, 4], amount 6. What does this print: the greedy coin count, then the true minimum?def greedy(coins, amt):n = 0for c in sorted(coins, reverse=True):n += amt // camt %= creturn ndef best(coins, amt):dp = [0] + [float("inf")] * amtfor a in range(1, amt + 1):dp[a] = min(dp[a - c] + 1 for c in coins if c <= a)return dp[amt]print(greedy([1, 3, 4], 6), best([1, 3, 4], 6))Largest-first pays 4 + 1 + 1, three coins, but 3 + 3 needs only two. Largest-first works for coin systems like 1, 5, 10, 25, not for arbitrary ones. When you can’t prove a greedy is safe, look for a small counterexample like this, and fall back to DP.
-
5
Which of these is not solved correctly by a simple greedy?
In 0/1 knapsack the item with the best value per kilo can waste room that two other items would fill better, so you need DP over capacity. Fractional knapsack is the tempting wrong answer: there greedy by value per kilo is right, because you can always top up with part of an item.