core idea
Greedy makes one irreversible, locally-best choice per step and never reconsiders it — no backtracking, no trying alternatives, no remembering the runner-up. That only pays off when you can prove the local choice never costs you the global optimum. Skip the proof and greedy still runs, still returns an answer, and is sometimes silently wrong — which is what makes it more dangerous to reach for on instinct than a pattern that simply fails loudly. The proof almost always takes one of three shapes:
| proof style | the argument | example below |
|---|---|---|
| exchange argument | swapping the greedy choice for any other choice can’t improve the answer | sort-by-end interval scheduling (see intervals) |
| never-regret | once made, no information that arrives later can make this choice worse than it already is | Best Time to Buy and Sell Stock, Jump Game |
| restart-past-failure | if the run dies at i, no start between the current candidate and i survives either — jump the candidate past all of them |
Gas Station |
when to reach for it
- A single forward pass with one running variable — a minimum, a frontier, a running total — is enough. You never need to remember every option seen, only the best one so far.
- The question asks for a max/min produced by an irreversible sequence of choices: “maximum profit from one transaction”, “minimum jumps to the end”, “can you complete the circuit”.
- You can state the exchange or never-regret argument in one sentence. If you can’t, you likely need dynamic programming instead — DP keeps every option alive until it’s provably dominated; greedy throws options away the moment it moves on.
- Sorting first turns the problem into one clean pass — intervals is the deepest example of this move.
universal templates
Track & decide — the shape behind three of the four problems below:
public int TrackAndDecide(int[] nums)
{
var best = InitialBest(); // the answer if we stopped right now
var running = Seed(nums[0]); // the one number greedy needs to remember
for (int i = 1; i < nums.Length; i++)
{
best = Combine(best, running, nums[i]); // score this step against what's tracked
running = Update(running, nums[i]); // fold today into the running invariant
}
return best;
}
running is a minimum (cheapest price seen), a maximum (furthest reachable index), or a
running sum (fuel remaining) depending on the problem — the shape of the loop never changes,
only what Update and Combine compute.
Extend a boundary until it closes — Partition Labels’ shape:
public List<int> ExtendUntilClose(string s, Dictionary<char, int> lastIndex)
{
var result = new List<int>();
int start = 0, end = 0;
for (int i = 0; i < s.Length; i++)
{
end = Math.Max(end, lastIndex[s[i]]); // widen to cover this item's whole span
if (i == end) // nothing after i can pull the boundary further
{
result.Add(end - start + 1);
start = i + 1;
}
}
return result;
}
prove it before you code it
Before writing the loop, say the proof out loud: “if I’d made a different choice here, could it ever beat this one?” A clean no means greedy is safe. If the honest answer is “well, it depends what comes later” — it doesn’t work, and reaching for dynamic programming is the right move, not a fallback to feel bad about.
problems
Four problems, three proof shapes: buy-sell-stock and jump-game are both never-regret arguments with different running invariants, gas-station is restart-past-failure, and partition-labels is the boundary-close template wearing an interval-merge costume.
Track the cheapest buy so far; every day asks "sell today?"
Furthest-reachable frontier — one variable, one pass.
If you run dry at i, no start before i works — restart at i+1.
Greedy + hashmap: extend the partition to each char's last index.
cheat sheet — greedy
recognize it
- a single forward pass with one running variable (a min, a frontier, a running total) is all the state you need — no need to remember every option seen, just the best one
- the question asks for a max/min produced by an irreversible sequence of choices:
maximum profit from one transaction,minimum jumps,can you complete the circuit - you can state a never-regret or exchange argument in one sentence (
swapping this choice for any other can't help) — if you can't, it's probablydynamic-programming, not greedy - sorting first turns the problem into one clean left-to-right pass
key tricks
Math.Max/Math.Minfold replaces a whole DP table whendp[i]only ever depends ondp[i-1]— that's the buy-sell-stock/Kadane compression- restart-past-failure: if the run fails starting at
i, every start< ialso fails, so jump straight tostart = i + 1and reset — no need to re-check the discarded range - extend-the-boundary-until-it-closes:
end = Math.Max(end, lastIndex[x]), cut the momenti == end - frontier tracking:
farthest = Math.Max(farthest, i + nums[i]), bail the instanti > farthest - when the greedy proof doesn't hold, don't force it — fall back to the full DP table and compare costs (
Jump Gameshows both side by side)
common bugs
- assuming greedy always works because the loop compiles and returns something plausible — it needs an actual never-regret/exchange proof, not vibes
- forgetting the floor/failure case: unreachable frontier isn't
falseunless you check it,total < costmust map to-1, an all-decreasing price array must floor at0 - getting the order of
UpdatevsCombinebackwards on a min/max fold — e.g. updatingminPricebefore scoring today's profit can silently change what a step computes - off-by-one on when a greedy boundary is allowed to close:
i == endvsi >= end, ornums.Lengthvsnums.Length - 1as the goal index