Greedy Pattern: Algorithmic Intuition, Sub-patterns & C++ Guide
Make the choice that looks best right now, and prove that this never hurts later choices.
6 sub-patterns Β· 22 questions
What is this pattern?
A greedy algorithm builds the answer one choice at a time, always taking the choice that looks best right now, and never goes back to change it. It is fast β usually a sort plus one pass β but it is only correct when you can prove that the locally best choice is always part of some optimal answer.
The work in a greedy problem is not the code; it is choosing the right key and convincing yourself it is safe:
- Interval scheduling: take the activity that finishes earliest.
- Sort + exchange: sort by the key that a swap argument supports (ratio, cost difference, deadline, height).
- Reachability: keep only the farthest point reachable so far.
- Assignment: sort both sides and give each need the smallest resource that fits.
- Running balance: keep a running total; when it fails, everything since the last start is ruled out.
- Heap greedy: the best choice changes after each step, so keep candidates in a heap.
Two ways to prove a greedy: the exchange argument (take any optimal answer that differs from greedy; swap in greedy's choice; it is still valid and no worse) and greedy stays ahead (after every step, greedy is at least as good as any other solution after the same number of steps).
Packing a day of meetings into one room: if you always book the meeting that ends soonest, the room frees up as early as possible, which can only leave more room for the rest. Whatever the best schedule picked first, swapping it for the earliest-ending meeting never makes the schedule worse β that swap is the whole proof.
See it move
Green = taken, red = skipped because it overlaps the last taken activity, purple = the final choice.
activities = 3-5, 0-6, 1-4, 5-9, 8-9, 5-7step 1/9Six activities, written start-end. One person can do one at a time; an activity may start exactly when the previous one ends. Goal: as many as possible.
Blue window = the stretch driven from the current start, red = a stretch that ran out of gas (every start inside it is ruled out), purple = the answer.
gas = [3, 1, 2, 4, 1], cost = [2, 3, 3, 1, 1]step 1/7- sum of net
- 1
net[i] = gas[i] β cost[i] is what the tank gains by filling at i and driving to i + 1. The total is 1 β₯ 0, so the trip is possible from some start. Now find it in one pass.
How do I recognise it?
"Maximum number of non-overlapping activities / pairs / meetings" β sort by end.
"Minimum cost / maximum profit" where items can be ordered by one number (ratio, difference, deadline) and a swap of two neighbours is easy to compare.
"Can you reach the end", "minimum number of jumps" with only forward moves.
Two lists to match (children β cookies, players β trainers) with a "fits" rule.
A circular route or a running balance (gas, change, money) that must never go negative.
"Rearrange so no two equal are adjacent", "merge the two smallest repeatedly" β the best choice changes, so use a heap.
Looks like Greedy, but isn't
Best value/weight ratio first fails when items can't be split: with capacity 10 and items (w 6, v 7), (w 5, v 5), (w 5, v 5), ratio-first takes the 6 and scores 7; the two 5s score 10. Fractional knapsack is greedy because splitting removes that problem.
Largest coin first works for 1, 5, 10, 25 but not for 1, 3, 4: for 6 it gives 4 + 1 + 1 (3 coins) instead of 3 + 3 (2 coins). No exchange argument holds.
Nothing is being chosen or left out; every interval stays and you only sweep. Greedy interval scheduling decides which intervals to KEEP.
Dijkstra is greedy inside (always settle the closest node), but the pattern to learn there is the graph + heap structure and why negative edges break it.
"Farthest reach" only works when moves go one way. With both directions the reachable set isn't a prefix, so you need a visited set.
How would I discover it from scratch?
Don't memorise βuse greedyβ. Ask these questions in order β the pattern falls out.
Try every subset or every order: 2^N or N! candidates, keep the best valid one. Correct, and a great test oracle, but far too slow.
Sub-patterns
Same core idea, different rule for βvalidβ. Learn them in this order.
Interval scheduling
3 questionsChoose as many non-overlapping intervals as possible (or cut a string into as many parts as possible). Always commit to the interval that frees you up soonest.
Sort + exchange argument
4 questionsSort items by one key so that swapping any two neighbours out of order can't improve the answer, then take them in that order.
Reachability / farthest reach
3 questionsMoves only go forward and any index up to the frontier is usable, so the whole state is one number: the farthest index you can reach.
Resource assignment
3 questionsMatch items from two lists (needs and resources). Give each need the smallest resource that satisfies it, so bigger resources stay free for bigger needs.
Running balance / local rules
4 questionsWalk the input once with a running balance or a local rule; a failure tells you exactly which earlier choices are impossible.
Heap-assisted greedy
5 questionsThe best next choice depends on what you did so far, so keep the candidates in a heap (or an ordered map) and pop the current best each time.
Don't mix these up
Try to break the greedy key on a 3β4 item example. If it breaks and no other key survives, the choice depends on the future β DP. If you can write the swap argument, greedy.
Choosing the most non-overlapping intervals β by end. Merging intervals or counting rooms β by start (Intervals pattern).
Items can be cut β ratio greedy. Whole items only β DP over capacity.
Forward moves up to nums[i] β reachable set is a prefix, one variable. Exact jumps both ways β graph search with visited.
Exchange: start from an optimal answer and turn it into greedy's without losing. Stays ahead: show greedy is never behind after each step. Use whichever is easier to state for the problem.
Code templates (C++)
Interval scheduling: sort by end
Sort by end, keep the last taken end, take an interval when it starts after it. Use > or β₯ exactly as the problem defines touching intervals.
// Most non-overlapping intervals: take the one that ends first, again and again.
// Touching ([1,3] then [3,5]) allowed here; use > instead of >= if the problem forbids it.
int maxNonOverlapping(vector<pair<int,int>> iv) { // {start, end}
sort(iv.begin(), iv.end(), [](auto& a, auto& b) { return a.second < b.second; });
int count = 0;
long long lastEnd = LLONG_MIN;
for (auto& [s, e] : iv)
if (s >= lastEnd) { count++; lastEnd = e; } // fits after the last one taken
return count;
}
// Partition into most parts: a part must reach the last occurrence of each of its letters.
vector<int> partitionByLast(const string& s) {
int last[26] = {};
for (int i = 0; i < (int)s.size(); i++) last[s[i] - 'a'] = i;
vector<int> sizes;
int start = 0, end = 0;
for (int i = 0; i < (int)s.size(); i++) {
end = max(end, last[s[i] - 'a']);
if (i == end) { sizes.push_back(end - start + 1); start = i + 1; } // part closed
}
return sizes;
}Sort by an exchange key
The whole problem is the comparator. Compare ratios by cross-multiplying (v1 * w2 > v2 * w1, in long long) to avoid floating-point ties.
// The comparator IS the algorithm. Prove it with a swap: if a should come before b,
// swapping them in any answer must not make it better.
// Fractional knapsack: best value per weight first; compare ratios without division.
double fractional(vector<int> val, vector<int> wt, long long cap) {
int n = val.size();
vector<int> idx(n);
iota(idx.begin(), idx.end(), 0);
sort(idx.begin(), idx.end(), [&](int a, int b) {
return 1LL * val[a] * wt[b] > 1LL * val[b] * wt[a]; // val[a]/wt[a] > val[b]/wt[b]
});
double total = 0;
for (int i : idx) {
if (cap == 0) break;
long long take = min<long long>(cap, wt[i]);
total += (double)val[i] * take / wt[i];
cap -= take;
}
return total;
}
// Deadlines, one time unit each: by deadline, keep the best profits in a min-heap.
long long maxProfitJobs(vector<pair<int,int>> jobs) { // {deadline, profit}
sort(jobs.begin(), jobs.end());
priority_queue<int, vector<int>, greater<int>> kept;
for (auto& [d, p] : jobs) {
kept.push(p);
if ((int)kept.size() > d) kept.pop(); // too many jobs by time d: drop the cheapest
}
long long sum = 0;
while (!kept.empty()) { sum += kept.top(); kept.pop(); }
return sum;
}Farthest reach and jump levels
far = farthest reachable index. For the minimum number of jumps, treat [start, curEnd] as one BFS level and count a jump each time i passes curEnd.
// nums[i] = longest jump from i. Every index up to far is reachable.
bool canReachEnd(const vector<int>& nums) {
int far = 0;
for (int i = 0; i < (int)nums.size(); i++) {
if (i > far) return false; // a gap nobody can jump over
far = max(far, i + nums[i]);
}
return true;
}
// Fewest jumps: indexes reachable with k jumps form one block [.., curEnd] (a BFS level).
int minJumps(const vector<int>& nums) {
int jumps = 0, curEnd = 0, far = 0;
for (int i = 0; i + 1 < (int)nums.size(); i++) {
far = max(far, i + nums[i]);
if (i == curEnd) { jumps++; curEnd = far; } // level finished: one more jump
}
return jumps; // assumes the end is reachable
}Match smallest that fits
Sort both lists, walk two pointers. A resource too small for the current need is too small for every later need, so skip it for good.
// Each need gets the smallest resource that satisfies it; count the matches.
int matchSmallestFit(vector<int> need, vector<int> res) {
sort(need.begin(), need.end());
sort(res.begin(), res.end());
int i = 0, j = 0;
while (i < (int)need.size() && j < (int)res.size()) {
if (res[j] >= need[i]) i++; // match: this need is served
j++; // too small now = too small for all later needs
}
return i;
}
// Beat b[] in as many positions as possible by permuting a[] (Tian Ji):
// strongest opponent first; win with our strongest, or sacrifice our weakest.
vector<int> advantage(vector<int> a, const vector<int>& b) {
int n = a.size();
sort(a.begin(), a.end());
vector<int> order(n);
iota(order.begin(), order.end(), 0);
sort(order.begin(), order.end(), [&](int x, int y) { return b[x] > b[y]; });
vector<int> res(n);
int lo = 0, hi = n - 1;
for (int k : order) res[k] = a[hi] > b[k] ? a[hi--] : a[lo++];
return res;
}Running balance with restart
If the total is negative there is no answer. Otherwise, when the running tank drops below 0 at i, no start in start..i works: restart at i + 1.
// Circular route: net[i] = gain at i minus cost to go on. Return a start index or -1.
int startOfCircuit(const vector<int>& net) {
long long total = 0, tank = 0;
int start = 0;
for (int i = 0; i < (int)net.size(); i++) {
total += net[i];
tank += net[i];
if (tank < 0) { start = i + 1; tank = 0; } // no start in start..i can pass i
}
return total < 0 ? -1 : start;
}
// Rules on both neighbours: one pass for each side, then take the max.
long long twoPassMin(const vector<int>& r) { // Candy
int n = r.size();
vector<int> c(n, 1);
for (int i = 1; i < n; i++) if (r[i] > r[i - 1]) c[i] = c[i - 1] + 1;
for (int i = n - 2; i >= 0; i--) if (r[i] > r[i + 1]) c[i] = max(c[i], c[i + 1] + 1);
return accumulate(c.begin(), c.end(), 0LL);
}Heap greedy
Push all candidates, pop the current best, push back whatever the step creates (a merged node, a leftover count). Each pop is O(log N).
// Repeatedly merge the two smallest (Huffman cost, connecting ropes).
long long mergeTwoSmallest(const vector<int>& w) {
priority_queue<long long, vector<long long>, greater<long long>> pq(w.begin(), w.end());
long long cost = 0;
while (pq.size() > 1) {
long long a = pq.top(); pq.pop();
long long b = pq.top(); pq.pop();
cost += a + b;
pq.push(a + b); // the merged node may be the next smallest
}
return cost;
}
// Most frequent first, never the same as the last one (no two equal neighbours).
string noTwoAdjacent(const string& s) {
int cnt[26] = {};
for (char ch : s) cnt[ch - 'a']++;
priority_queue<pair<int,char>> pq; // {count, letter}, biggest count on top
for (int c = 0; c < 26; c++) if (cnt[c]) pq.push({cnt[c], char('a' + c)});
string out;
while (pq.size() >= 2) {
auto [c1, x] = pq.top(); pq.pop();
auto [c2, y] = pq.top(); pq.pop();
out += x; out += y; // two different letters per round
if (--c1) pq.push({c1, x});
if (--c2) pq.push({c2, y});
}
if (!pq.empty()) {
if (pq.top().first > 1) return ""; // one letter left over more than once
out += pq.top().second;
}
return out;
}