Appearance
Greedy
Quick Revisit
- USE WHEN: Building a solution piece-by-piece where the locally optimal choice is globally optimal
- INVARIANT: After each greedy step, the partial solution is a subset of some optimal solution (exchange argument)
- TIME: O(n log n) typical (sort + scan) | SPACE: O(n)
- CLASSIC BUG: Assuming greedy works without verifying the exchange argument—leads to WA on tricky cases
- PRACTICE: Practice Ladder
The greedy paradigm: build a solution piece by piece, always picking the locally optimal choice and never looking back. When the greedy-choice property holds, this produces a globally optimal solution with almost no overhead—sort once, scan once, done.
Difficulty: Intermediate | Prerequisites: Sorting and Searching, Binary Search | CF Rating Range: 1000–1800 | Contest Frequency: Common
Contents
- Intuition
- C++ STL Reference
- Implementation (Contest-Ready)
- Operations Reference
- Problem Patterns & Tricks
- The Exchange Argument—Proving Your Greedy is Correct
- Contest Cheat Sheet
- Gotchas & Debugging
- Self-Test
- Practice Problems
- Further Reading
- Rating Progression
- Before You Code Checklist
- What Would You Do If...?
- The Mistake That Teaches
- Debug This
- Code Reading Exercise
- What to Solve Next
- Solve With Me—CF 1325D
Intuition
You have 6 activities, each with a start and end time:
text
Activity Start End
-------- ----- ---
A 0 3
B 1 4
C 2 5
D 4 7
E 6 9
F 8 11Goal: pick the maximum number of non-overlapping activities.
Naive approach: enumerate every subset of activities, check for overlaps, keep the largest valid set. With
Brute force is a dead end. We need a rule that commits to one choice at a time, left to right, without revisiting.
Always pick the activity that finishes earliest among those compatible with your current selection—this leaves the maximum room for future activities.
Think of it like parking cars bumper-to-bumper: if every car is as short as possible, you fit the most. The key insight is that finishing early is strictly better than finishing late—it never costs you anything and always leaves more room.
This "earliest end time" rule is the greedy choice. It is local (considers only the next best activity) yet produces a global optimum. The strategy breaks when the problem lacks the greedy-choice property—for instance, 0/1 knapsack, where taking the locally best item can block a superior combination.
Checkpoint: Why does sorting by start time (earliest start first) fail for interval scheduling maximization, while sorting by end time works?
Think first, then click
A job that starts earliest might span a very long time, blocking many short jobs. Sorting by end time guarantees each picked activity frees the timeline as early as possible, leaving maximum room for future picks. The exchange argument proves: swapping any optimal choice for the earlier-ending greedy choice never reduces the count.
Visual Walkthrough
Sort the 6 activities by end time, then greedily select:
text
Timeline: 0 1 2 3 4 5 6 7 8 9 10 11
|---|---|---|---|---|---|---|---|---|---|---|---|
Sorted by end time:
A: [--------) end=3
B: [--------) end=4
C: [--------) end=5
D: [--------) end=7
E: [--------) end=9
F: [--------) end=11
Step 1: last_end = -inf. A starts at 0 >= -inf. PICK A. last_end = 3.
Step 2: B starts at 1 < 3. SKIP (overlaps A).
Step 3: C starts at 2 < 3. SKIP (overlaps A).
Step 4: D starts at 4 >= 3. PICK D. last_end = 7.
Step 5: E starts at 6 < 7. SKIP (overlaps D).
Step 6: F starts at 8 >= 7. PICK F. last_end = 11.
Selected: {A, D, F} -- 3 activities (6 comparisons, done)
0 1 2 3 4 5 6 7 8 9 10 11
|---|---|---|---|---|---|---|---|---|---|---|---|
[A-------) [D-----------) [F-----------)Total work:
The Invariant
Invariant (exchange argument): After each greedy step, the set of chosen activities is a subset of some optimal solution.
Exchange argument—interval scheduling proof:
- Let
be the greedy solution (sorted by end time). - Let
be an optimal solution with , also sorted by end time. - Claim:
for every . - Proof by induction. Base:
has the earliest end time of all activities, so . - Inductive step: assume
. Then every activity compatible with is also compatible with . Greedy picks the compatible activity with the earliest end time, so . - Since each greedy choice finishes no later than the corresponding optimal choice, greedy selects at least as many activities:
. Combined with (optimality of ), we get .
Generalized exchange argument template (works beyond interval scheduling):
text
+------------------------------------------------------------------+
| EXCHANGE ARGUMENT TEMPLATE |
| |
| 1. Let G be the greedy solution and O be any optimal solution. |
| 2. Find the first position i where G and O differ: |
| G chooses element g, O chooses element o (g != o). |
| 3. Construct O' by swapping o for g in O. |
| 4. Show that O' is still feasible (satisfies all constraints). |
| 5. Show that cost(O') <= cost(O) (O' is no worse). |
| This is the KEY step: argue from the greedy criterion |
| why g is at least as good as o in this position. |
| 6. Repeat: O' agrees with G on one more position. |
| After finitely many swaps, O becomes G with no loss. |
| Therefore G is optimal. |
+------------------------------------------------------------------+Step 5 is where the problem-specific reasoning lives. For interval scheduling:
More Exchange Argument Examples
The exchange argument is the backbone of every greedy proof. The same four-step template works across radically different problem types—the only thing that changes is the algebra in step 3. Three more worked examples make this concrete.
Example A: Job Scheduling with Deadlines (Minimizing Lateness)
Problem:
Greedy rule: Sort jobs by deadline
Exchange argument proof:
- Let
be the EDF schedule. Suppose some optimal schedule has an inversion: two adjacent jobs where is scheduled before but . - Construct
by swapping and in . All other jobs are unaffected. - Feasibility: Both
and still run; the total processing time is unchanged. The swap does not affect any other job's completion time. - Cost analysis: Let
be the start time of the earlier job. Before the swap: , . After the swap: , . - Job
's completion time rises from to , and job 's drops from to . - The new lateness of
is . The old lateness of was . Since , we have , so the swap does not increase .
- Job
- Each swap removes one inversion. After finitely many swaps
becomes EDF with no worse. Therefore EDF is optimal.
Example B: Coin Change—Where Greedy Fails
Problem: Make change for amount
Greedy rule (largest-coin-first): Always pick the largest denomination
Greedy attempt for
text
Remaining = 6. Largest <= 6 is 4. Take 4. Remaining = 2.
Remaining = 2. Largest <= 2 is 1. Take 1. Remaining = 1.
Remaining = 1. Largest <= 1 is 1. Take 1. Remaining = 0.
Greedy answer: 4 + 1 + 1 = 3 coins.Optimal:
Why the exchange argument breaks: Try step 5 of the template. Suppose optimal uses coin
Lesson: When you try to write the exchange argument and step 5 fails—the swap makes things worse—this is a concrete signal the problem needs DP. For coin change with arbitrary denominations, the correct approach is
Example C: Minimizing Total Weighted Completion Time
Problem:
Greedy rule: Sort by
Exchange argument proof (adjacent swap):
- Suppose adjacent jobs
are scheduled with before . Let be the time before both start. -then- cost contribution: . -then- cost contribution: . - Subtract: (
-first) ( -first) . -before- is better . - If any adjacent pair violates this order, swapping them strictly improves the objective. An optimal schedule has no such violations, so it is sorted by
. Therefore the greedy (sorted) schedule is optimal.
This is the Smith's Rule proof and is one of the cleanest examples of an adjacent-swap exchange argument. Note how the algebra directly produces the sorting key.
Greedy Proof Template (Contest Fill-in-the-Blank)
In a contest, you rarely write a formal proof—but you must convince yourself the greedy works before coding. Use this fill-in-the-blank template mentally (or on scratch paper):
text
+------------------------------------------------------------------+
| GREEDY PROOF TEMPLATE -- CONTEST VERSION |
+------------------------------------------------------------------+
| |
| PROBLEM: ____________________________________________ |
| |
| GREEDY RULE: Sort by ______________ and then ______________. |
| |
| EXCHANGE ARGUMENT: |
| |
| 1. ASSUME: An optimal solution O differs from greedy G at |
| position i. O uses element [o] and G uses element [g]. |
| |
| 2. SWAP: Replace [o] with [g] in O to get O'. |
| |
| 3. FEASIBILITY: O' is still valid because ___________________ |
| _________________________________________________________. |
| |
| 4. COST: O' is no worse because __________________________ |
| _________________________________________________________. |
| (This is where the sorting criterion matters: |
| because [g] was chosen by greedy, we know _____________ |
| _____________________ which implies cost(O') <= cost(O).) |
| |
| 5. CONCLUSION: Each swap makes O agree with G on one more |
| position without worsening cost -> G is optimal. |
| |
| FILLED EXAMPLE (Interval Scheduling): |
| GREEDY RULE: Sort by end time ascending, pick earliest finish. |
| FEASIBILITY: g ends <= o ends, so all activities after o |
| in O are still compatible with g. |
| COST: Same count -- we replaced one interval with one interval.|
| KEY PROPERTY: end(g) <= end(o) because greedy picks earliest. |
+------------------------------------------------------------------+How to use in contest:
- Write your greedy rule in one sentence.
- Try to fill in the FEASIBILITY and COST blanks. If you can fill both in under 2 minutes, code the greedy.
- If you get stuck at COST (you cannot explain why the swap is not worse), stop—the problem likely needs DP.
How to Identify a Greedy Problem / Discover the Sorting Key
- Can you sort by some key? Look for a natural ordering—end times, deadlines, ratios, differences. If two elements
and are adjacent, ask: "when is -before- better than -before- ?" The answer defines your comparator. - Can you prove the greedy choice is safe via exchange argument? Walk through the template above. If you cannot fill in step 5—if you cannot explain why the greedy choice is at least as good—the problem likely needs DP.
- Does the problem have optimal substructure? After making the greedy choice, the remaining subproblem must have the same structure. If the greedy choice changes the subproblem in complex ways (e.g., 0/1 knapsack: taking an item changes remaining capacity and eliminates that item), greedy alone will not suffice.
Greedy vs DP Trade-Off
Both greedy and DP exploit optimal substructure, but greedy needs something more: the greedy-choice property—a locally optimal choice must also be part of a globally optimal solution. When that property holds, the gains are dramatic:
| Aspect | Greedy | DP |
|---|---|---|
| Time | ||
| Space | ||
| Implementation | Short, simple | Longer, more complex |
| Correctness | Requires exchange-argument proof | Correct by construction (recurrence) |
Rule of thumb: try greedy first. If you can sketch the exchange argument in your head in under two minutes, code the greedy. If you can't—or if the problem forces you to track additional state like remaining capacity—switch to DP.
What the Code Won't Teach You
The sorting key IS the exchange argument, made concrete. When you compare two adjacent elements
text
Deriving the sort key from the exchange argument:
Problem: minimize total weighted completion time
Jobs: (p_i, w_i) = processing time and weight
Compare two adjacent orderings:
Order A: job i then job j Order B: job j then job i
+--------+--------+ +--------+--------+
| job i | job j | | job j | job i |
+--------+--------+ +--------+--------+
T T+p_i T+p_i+p_j T T+p_j T+p_j+p_i
Cost A: w_i*(T+p_i) + w_j*(T+p_i+p_j)
Cost B: w_j*(T+p_j) + w_i*(T+p_j+p_i)
A - B = w_j*p_i - w_i*p_j
A is better when: w_j*p_i < w_i*p_j
p_i/w_i < p_j/w_j
--> Sort key: ascending p_i/w_i
--> Comparator: (long long)a.p * b.w < (long long)b.p * a.wGreedy fails exactly when the problem has non-trivial state. Greedy commits without memory. If "which choice is best" depends on what you've already chosen—if the problem has state—greedy is flying blind. That is the formal reason 0/1 knapsack needs DP: taking item A shrinks the remaining capacity, which changes which items are worth taking next. The greedy has no way to account for this.
text
Greedy has NO memory: DP tracks STATE:
Fractional knapsack: 0/1 knapsack:
"Take best ratio item" "Take item A or not?"
| / \
v Take A Skip A
Remaining capacity cap = W-w_A cap = W
doesn't matter -- DIFFERENT SAME
we can take a fraction remaining items remaining items
of the next item too. --> state matters --> state matters
Greedy works because DP needed because
fractions mean no choices affect
"wasted" capacity. future options.Knowing why greedy works is half the battle. The other half is recognizing which problems admit a greedy solution at all.
When to Reach for This
Trigger phrases in problem statements:
- "maximize the number of non-overlapping ..."
- "minimize the total cost / completion time / penalty"
- "schedule ... to minimize / maximize ..."
- "choose items ... greedily" (rare but explicit)
- "sort and select"
Counter-examples—problems that look greedy but need DP:
| Problem | Why greedy fails | Correct approach |
|---|---|---|
| 0/1 Knapsack | Taking the highest-ratio item can block a better combination | DP on capacity |
| Longest Increasing Subsequence | Greedily taking the smallest next element does not guarantee length; patient sorting works but is essentially DP | DP with binary search, |
| Coin Change (arbitrary denominations) | Largest-coin-first fails for denominations like | DP on amount |
| Edit Distance | No local choice rule leads to a global optimum | DP on two strings |
If you find yourself wanting to "undo" a greedy choice, or needing to track a second dimension of state, that is a strong signal you need DP—or at minimum, greedy with a regret/undo mechanism via priority queue.
Greedy vs DP Decision Tree
Choosing between greedy and DP is one of the most consequential decisions in competitive programming. The table below puts concrete problems side by side with the reasoning that separates them.
| Problem | Greedy? | DP? | Distinguishing Feature |
|---|---|---|---|
| Activity Selection (max non-overlapping intervals) | Yes | Works but overkill | Greedy choice: always pick earliest finish. Exchange argument shows no interval the greedy skips could improve the count. |
| 0/1 Knapsack (indivisible items, maximize value) | No | Yes | No greedy ordering works. Taking the best ratio item may waste capacity. Must track remaining capacity as a state dimension. |
| Fractional Knapsack (divisible items) | Yes | Overkill | Items are divisible—sort by value/weight ratio and pour. No capacity "waste" possible, so the greedy choice is always safe. |
| Coin Change (min coins, arbitrary denominations) | No | Yes | Greedy (largest-first) fails for non-canonical denomination sets (e.g., |
| Huffman Coding (optimal prefix-free code) | Yes | Not natural | Merging the two cheapest nodes is provably optimal (exchange argument on tree structure). No DP state needed. |
| Weighted Job Scheduling (max weight, non-overlapping) | No | Yes | Unlike unweighted version, different weights mean you cannot just pick earliest finish—a long heavy job may beat two light ones. DP on sorted end times with binary search. |
| Minimum Spanning Tree (Kruskal's / Prim's) | Yes | Not applicable | Matroid structure guarantees greedy works. Adding the cheapest safe edge never creates a problem (cut property). |
| Longest Common Subsequence | No | Yes | No local choice rule works—matching character |
| Minimize Maximum Lateness (single machine, deadlines) | Yes | Overkill | EDF (earliest deadline first)—adjacent swap argument shows inversions always increase max lateness. |
| Subset Sum (does a subset sum to | No | Yes | Greedy (largest-first) can overshoot or undershoot. Must track reachable sums as DP states. |
Decision heuristic—ask these questions in order:
text
1. Can I sort by some key and make irrevocable choices?
├─ YES -> Try exchange argument (2 min mental check)
│ ├─ Argument works -> GREEDY
│ └─ Argument fails -> DP (or greedy + regret)
└─ NO -> Probably DP (or search / flow)
2. Does the problem force me to track extra state?
(remaining capacity, subset membership, two positions in two strings)
├─ YES -> DP
└─ NO -> Likely greedy
3. Does the problem have a "divisibility" or "matroid" structure?
(fractional items, matroid intersection, spanning trees)
├─ YES -> Greedy
└─ NO -> Investigate furtherCheckpoint: You're solving a problem and your greedy approach gives the wrong answer on a small test case. How do you decide whether to fix the greedy strategy or switch to DP?
Think first, then click
Try the exchange argument: can you always swap an optimal solution's choice for your greedy choice without making it worse? If you can't construct this argument—or if you find a counterexample where undoing a greedy choice would help—the problem likely lacks the greedy-choice property and needs DP. Key signal: if you need to "undo" a choice or track a second state dimension (like remaining capacity), use DP.
C++ STL Reference
| Function / Class | Header | What it does | Time Complexity |
|---|---|---|---|
sort(first, last) | <algorithm> | Sort range ascending | |
sort(first, last, cmp) | <algorithm> | Sort with custom comparator | |
stable_sort(first, last, cmp) | <algorithm> | Stable sort (preserves equal-element order) | |
nth_element(first, nth, last) | <algorithm> | Partition so nth element is in sorted position | |
partial_sort(first, mid, last) | <algorithm> | Sort only first mid - first elements | |
max_element(first, last) | <algorithm> | Iterator to maximum element | |
min_element(first, last) | <algorithm> | Iterator to minimum element | |
accumulate(first, last, init) | <numeric> | Sum of range | |
priority_queue<T> | <queue> | Max-heap for greedy selection | |
priority_queue<T, vec, greater<T>> | <queue> | Min-heap | |
set<T> / multiset<T> | <set> | Ordered container for greedy with predecessor/successor | |
lower_bound(first, last, val) | <algorithm> | First element | |
upper_bound(first, last, val) | <algorithm> | First element $> $ val in sorted range |
Implementation (Contest-Ready)
Version 1: Minimal—Activity Selection (Interval Scheduling Maximization)
cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<pair<int,int>> a(n); // {end, start}
for (auto& [e, s] : a) cin >> s >> e;
sort(a.begin(), a.end());
int cnt = 0, last = INT_MIN;
for (auto& [e, s] : a) {
if (s >= last) {
cnt++;
last = e;
}
}
cout << cnt << "\n";
}Version 2: Explained—Interval Scheduling with Full Trace
cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
// Store intervals as {end, start} so default pair sort
// sorts by end time first, breaking ties by start time.
// This is the key greedy insight: always pick the activity
// that finishes earliest among compatible ones.
vector<pair<int,int>> intervals(n);
for (auto& [end, start] : intervals) {
cin >> start >> end;
}
// Sort by end time (first element of pair).
// Exchange argument: if greedy picks Y (ends at e_Y) and
// optimal picks X (ends at e_X >= e_Y) at the same step,
// swapping X for Y in optimal leaves at least as much room,
// so greedy never does worse.
sort(intervals.begin(), intervals.end());
int count = 0;
int last_end = INT_MIN; // end time of last selected activity
for (auto& [end, start] : intervals) {
// "Compatible" means this activity starts at or after
// the last selected activity ended.
// Use >= for closed intervals [start, end],
// use > if intervals are half-open [start, end).
if (start >= last_end) {
count++;
last_end = end;
}
}
cout << count << "\n";
return 0;
}Operations Reference
| Operation | Time | Space |
|---|---|---|
| Sort | ||
| Single greedy pass over sorted array | ||
| Greedy with priority queue (e.g., Huffman) | ||
| Greedy with set/multiset lookups | ||
| Exchange-argument proof (your mental effort) |
Typical total:
Problem Patterns & Tricks
Interval Scheduling (Maximum Non-Overlapping)
Sort by end time, greedily pick the earliest-finishing compatible interval. Classic exchange-argument proof.
cpp
sort(intervals.begin(), intervals.end()); // {end, start}
int cnt = 0, last = INT_MIN;
for (auto [e, s] : intervals)
if (s >= last) { cnt++; last = e; }Problems: CF 1141F2, CSES Movie Festival
Interval Covering (Minimum Intervals to Cover a Range)
Sort by start time. Greedily extend coverage as far right as possible using the best available interval.
text
INTERVAL COVERING -- always extend as far right as possible:
Goal: cover range [L=0, R=10]
Intervals (sorted by start):
A: [0-------4)
B: [1----------6)
C: [3---------7)
D: [5-----------9)
E: [8-----------11)
Step 1: cur=0. Scan intervals starting <= 0.
A starts at 0: extends to 4. best=4.
PICK A. cur=4.
Step 2: cur=4. Scan intervals starting <= 4.
B starts at 1: extends to 6.
C starts at 3: extends to 7. best=7.
PICK C. cur=7.
Step 3: cur=7. Scan intervals starting <= 7.
D starts at 5: extends to 9.
E starts at 8: starts AFTER cur? No, 8 > 7.
best=9. PICK D. cur=9.
Step 4: cur=9. Scan intervals starting <= 9.
E starts at 8: extends to 11 >= R=10. best=11.
PICK E. cur=11 >= R. DONE.
Selected: {A, C, D, E} = 4 intervals.cpp
sort(intervals.begin(), intervals.end()); // {start, end}
int cnt = 0, i = 0, cur = L; // L = left boundary to cover
while (cur < R) { // R = right boundary
int best = cur;
while (i < n && intervals[i].first <= cur)
best = max(best, intervals[i++].second);
if (best == cur) { /* impossible */ break; }
cur = best;
cnt++;
}Problems: CF 1029C, CSES Movie Festival II
Fractional Knapsack
Sort by value/weight ratio descending. Take as much as possible of each item. Only works for fractional variant (not 0/1).
cpp
sort(items.begin(), items.end(), [](auto& a, auto& b) {
return (double)a.v / a.w > (double)b.v / b.w;
});
double ans = 0, cap = W;
for (auto& [v, w] : items) {
double take = min(cap, (double)w);
ans += take * ((double)v / w);
cap -= take;
if (cap <= 0) break;
}Problems: CF 1974C, classical fractional knapsack
Greedy + Sort by Derived Key
Many problems require sorting by a non-obvious key derived from the input. The exchange argument tells you what to sort by: compare two adjacent elements and determine which ordering is better.
Example: Minimize total weighted completion time. Job
cpp
// Exchange argument: swap adjacent jobs i, j.
// i before j: cost contribution includes w_j * p_i
// j before i: cost contribution includes w_i * p_j
// i before j is better when w_j * p_i < w_i * p_j
// i.e., p_i / w_i < p_j / w_j
sort(jobs.begin(), jobs.end(), [](auto& a, auto& b) {
return (long long)a.p * b.w < (long long)b.p * a.w;
});Greedy with Priority Queue (Huffman-Style)
Repeatedly merge the two smallest (or cheapest) elements. Use a min-heap.
Worked example—tricky case with equal elements:
text
Merge costs for a = [2, 2, 3, 3]:
Heap: {2, 2, 3, 3}
Step 1: pop 2, 2. Merge cost = 4. Push 4.
Heap: {3, 3, 4}. Total cost = 4.
Step 2: pop 3, 3. Merge cost = 6. Push 6.
Heap: {4, 6}. Total cost = 4 + 6 = 10.
Step 3: pop 4, 6. Merge cost = 10. Push 10.
Heap: {10}. Total cost = 4 + 6 + 10 = 20.
WRONG approach (merge largest first):
Step 1: merge 3,3 = 6. cost=6. Heap: {2, 2, 6}
Step 2: merge 2,6 = 8. cost=14. Heap: {2, 8}
Step 3: merge 2,8 = 10. cost=24. Heap: {10}
Total = 24 > 20. Merging smallest first wins!
+-------------------+-------------------+
| Min-heap (correct) | Max-heap (wrong) |
| Total cost = 20 | Total cost = 24 |
+-------------------+-------------------+cpp
priority_queue<long long, vector<long long>, greater<>> pq;
for (int x : a) pq.push(x);
long long cost = 0;
while (pq.size() > 1) {
long long x = pq.top(); pq.pop();
long long y = pq.top(); pq.pop();
cost += x + y;
pq.push(x + y);
}Problems: CF 1760F, CSES Stick Divisions
Greedy with Regret (Undo Last Choice)
Sometimes greedy needs a "regret" mechanism: make a greedy choice, but allow undoing it later if a better option appears. Typically implemented with a priority queue.
cpp
// Hire k workers with minimum cost; can fire and rehire.
priority_queue<int> pq; // max-heap of hired costs
long long total = 0;
sort(workers.begin(), workers.end()); // by some criterion
for (auto& [key, cost] : workers) {
pq.push(cost);
total += cost;
if ((int)pq.size() > k) {
total -= pq.top(); // undo the most expensive hire
pq.pop();
}
}The Exchange Argument — Proving Your Greedy is Correct
Most greedy ideas that "feel right" are wrong. At CF 1400+ you will routinely encounter problems where a plausible greedy fails on a hidden edge case. The exchange argument is a reusable proof framework that lets you either confirm your greedy is correct or discover that it isn't—before you waste 45 minutes coding the wrong approach.
If you cannot construct the exchange argument in under two minutes of mental effort, treat that as strong evidence the problem needs DP. This framework is your single most valuable meta-tool for greedy problems.
The Framework (Four Steps)
- Assume an optimal solution
exists that differs from your greedy solution . - Locate the first position
where and differ. chose element at position ; chose element . - Swap: Construct
by replacing with at position in . Show two things: - Feasibility:
is still a valid solution. - Non-worsening:
(or for maximization).
- Feasibility:
- Induct: Each swap makes
agree with on one more position without degrading quality. After finitely many swaps, becomes . Therefore is optimal.
The key intellectual work happens in step 3—that is where the sorting criterion earns its keep.
Worked Example 1: Activity Selection (Interval Scheduling Maximization)
Problem: Given
Greedy rule: Always pick the interval that ends earliest among those compatible with the last selected interval.
Exchange argument:
Let
Replace
text
Timeline
──────────────────────────────────────────►
OPT: ├── o_{j-1} ──┤ ├──── o_j ────┤ ├── o_{j+1} ──┤
↑ still compatible
Swap: ├── o_{j-1} ──┤ ├─ g_j ─┤ ├── o_{j+1} ──┤
↑ ↑ g_j ends earlier,
ends earlier so gap is even wider- Feasibility:
starts after ends (same as did, since and agreed up to ). And ends no later than , so is still compatible. - Non-worsening: We replaced one interval with one interval—same count.
After at most
Worked Example 2: Minimum Coins (Canonical vs Non-Canonical Systems)
Problem: US coin system
Greedy rule: Always use the largest denomination that fits.
Exchange argument sketch: Suppose
Counterexample—non-canonical system
text
Greedy: 4 + 1 + 1 = 3 coins <- picks largest first
Optimal: 3 + 3 = 2 coins <- fewer coins!Try the exchange argument:
When the Exchange Argument Fails—Use DP
If you sit down to write the swap argument and:
- the swap breaks feasibility (changes the sum, violates a constraint), or
- the swap worsens cost (you need extra compensating moves),
then your greedy is almost certainly wrong. This is not a failure of technique—it is the framework doing its job. The problem likely has optimal substructure but not the greedy-choice property, which is the textbook signature of a DP problem.
Rule of thumb: if a greedy choice at step
Contest Cheat Sheet
text
+--------------------------------------------------------------+
| GREEDY CHEAT SHEET |
+--------------------------------------------------------------+
| WHEN TO USE: |
| - "maximize/minimize" + no DP substructure visible |
| - exchange argument works for adjacent elements |
| - problem screams "sort then scan" |
+--------------------------------------------------------------+
| RECIPE: |
| 1. Identify the greedy criterion (what to sort by) |
| 2. Prove via exchange argument (swap two adjacent) |
| 3. Sort + single pass (or sort + priority queue) |
+--------------------------------------------------------------+
| KEY PATTERNS: |
| - Interval scheduling -> sort by END time |
| - Interval covering -> sort by START time |
| - Fractional knapsack -> sort by VALUE/WEIGHT desc |
| - Weighted scheduling -> sort by p_i/w_i asc |
| - Merge cheapest -> min-heap (Huffman) |
| - Regret/undo -> greedy + max-heap for removal |
+--------------------------------------------------------------+
| COMPLEXITY: O(n log n) sort + O(n) pass = O(n log n) |
| SPACE: O(n) for input, O(1)-O(n) extra |
+--------------------------------------------------------------+
| DANGER: |
| - Can't prove exchange arg? Probably need DP. |
| - 0/1 knapsack is NOT greedy. |
| - Ties in sort key: define tiebreaker or prove it irrelevant|
+--------------------------------------------------------------+Gotchas & Debugging
Greedy Is Wrong (Most Common Bug)
The judge found a counterexample before you did. When a greedy solution gets WA, the right move is to construct that counterexample yourself—not to tweak the comparator and resubmit. Compare greedy output against brute force on random small inputs:
text
Is your greedy correct? Follow this check:
Greedy solution
|
v
Try exchange argument (2 min)
|
+----+----+
| |
WORKS FAILS --> Stop. Use DP.
|
v
Stress test vs brute force
|
+----+----+
| |
MATCH MISMATCH --> Found counterexample!
| Analyze and fix sort key.
v
Submit with confidencecpp
// Stress test: brute force vs greedy
for (int iter = 0; iter < 100000; iter++) {
auto input = gen_random();
auto ans_brute = brute(input);
auto ans_greedy = greedy(input);
if (ans_brute != ans_greedy) {
print(input); // found counterexample
break;
}
}Wrong Sorting Comparator
Custom comparators must define a strict weak ordering: if cmp(a, b) is true, cmp(b, a) must be false. Using <= instead of < causes undefined behavior in sort.
cpp
// WRONG: causes UB with equal elements
sort(a.begin(), a.end(), [](int x, int y) { return x <= y; });
// CORRECT:
sort(a.begin(), a.end(), [](int x, int y) { return x < y; });Integer Overflow in Comparator
When sorting by a ratio like
cpp
// WRONG: floating-point precision issues
sort(a.begin(), a.end(), [](auto& x, auto& y) {
return (double)x.p / x.w < (double)y.p / y.w;
});
// CORRECT: cross-multiply (watch for overflow -> use long long)
sort(a.begin(), a.end(), [](auto& x, auto& y) {
return (long long)x.p * y.w < (long long)y.p * x.w;
});Closed vs Open Intervals
Interval scheduling: does [1, 3] overlap with [3, 5]? Depends on the problem. Read carefully whether endpoints are inclusive. Use >= for [closed, closed] and > for [closed, open).
Not Handling Equal Elements
When two elements have the same greedy key, the tiebreaker can matter. Example: if sorting by deadline and two jobs share a deadline, processing order between them might affect feasibility.
Forgetting to Use long long
Greedy accumulation (sums, costs) often overflows int. If
cpp
long long total = 0; // NOT int
for (auto& x : a) total += x;Greedy on Multisets—Erasing by Value
When using multiset in a greedy algorithm, ms.erase(val) removes all copies. Erase one copy with:
cpp
auto it = ms.find(val);
if (it != ms.end()) ms.erase(it); // erase ONE copyMental Traps
"I can construct the exchange argument after coding." The exchange argument must come before coding, not as a post-hoc justification. Code first and you will rationalize your approach. Confirmation bias is powerful: once you've spent 20 minutes on an implementation, your brain will find a way to believe the proof holds even when it doesn't.
text
WRONG workflow: RIGHT workflow:
Read problem Read problem
| |
v v
"Looks greedy" "Looks greedy"
| |
v v
CODE (20 min) EXCHANGE ARGUMENT (2 min)
| |
v +----+----+
Submit --> WA | |
| WORKS FAILS
v | |
"Let me prove it..." CODE --> DP
| (15 min)
v
Can't prove it,
wasted 20 min"If I can't find a counterexample, it must be correct." Failure to find a counterexample is not a proof—it is just evidence that your manual tests weren't adversarial enough. Some greedy algorithms fail only on inputs that are hard to construct by hand. The exchange argument provides the actual proof; manual testing provides only false confidence.
text
"No counterexample found" != "Correct"
Example: Coin change with {1, 3, 4}, target = 6
Quick manual tests:
target=1: greedy 1 optimal 1 MATCH
target=3: greedy 3 optimal 3 MATCH
target=4: greedy 4 optimal 4 MATCH
target=5: greedy 4+1 optimal 3+1+1? No, 4+1=2 coins. MATCH
"All tests pass, greedy works!"
target=6: greedy 4+1+1=3 coins
optimal 3+3 =2 coins <-- FAILS!
The counterexample wasn't in your quick tests.
Exchange argument would have caught this:
Swap coin 3 for coin 4 --> sum overshoots --> STEP 3 FAILS"This problem looks like interval scheduling." Many greedy problems superficially resemble interval scheduling (sort + scan). But the specific criterion—what to sort by, what makes a choice "compatible"—depends on the exact problem. Misidentifying the pattern and applying the wrong sorting key is a common failure.
text
Same structure, DIFFERENT sort keys:
+---------------------------+-------------------+
| Problem | Sort by |
+---------------------------+-------------------+
| Max non-overlapping | END time (asc) |
| Min intervals to cover | START time (asc) |
| Min lateness | DEADLINE (asc) |
| Min weighted completion | p_i/w_i (asc) |
+---------------------------+-------------------+
Using "sort by end time" on a deadline problem = WA.
The exchange argument tells you WHICH key is correct.Self-Test
Before moving on, verify you can do these without looking at notes:
- [ ] State the exchange argument template (4 steps) from memory and apply it to a new problem: "schedule
jobs with durations, minimize total completion time." - [ ] Explain why 0/1 knapsack fails with greedy but fractional knapsack doesn't—using the concept of "state" and "irrevocable choices."
- [ ] Write the greedy for "interval scheduling maximization" (sort by end time, scan), verify it on a 5-interval example.
- [ ] Name the bug in
sort(a, a+n, [](auto x, auto y){ return x.ratio <= y.ratio; })and fix it. - [ ] Describe the "greedy with regret" pattern using a priority queue—when is it needed and what does it look like?
- [ ] Given two adjacent jobs with
and , derive which order is better using cross-multiplication.
Socratic Checkpoints
Checkpoint: How do you prove a greedy algorithm is correct?
Think, then click
The two standard techniques are the exchange argument and the greedy stays ahead lemma. In the exchange argument, you take any optimal solution that differs from the greedy solution, find the first point of difference, and show that swapping to match the greedy choice doesn't make the solution worse—eventually transforming OPT into the greedy solution without losing optimality. In "greedy stays ahead," you prove by induction that after each step, the greedy solution is at least as good as any other solution at that point. Both approaches require identifying what "greedy choice" means and why it's safe.
Checkpoint: When does a greedy approach fail, and how can you recognize it?
Think, then click
Greedy fails when a locally optimal choice leads to a globally suboptimal solution—i.e., when you need to "sacrifice now to gain later." Classic warning signs: (1) the problem has overlapping subproblems (suggests DP instead), (2) small examples exist where the greedy choice doesn't lead to the best answer (always test on 3–4 cases!), (3) the problem requires combining items with complex interactions (e.g., 0/1 knapsack—taking a lighter item now may block a more valuable heavy item later). If you can't construct an exchange argument, it's likely not greedy.
Practice Problems
| # | Problem | Source | Difficulty | Key Idea |
|---|---|---|---|---|
| 1 | Minimize the Permutation | CF 1256A | 800 | Greedy swap to minimize lexicographic order |
| 2 | Ehab and another construction | CF 1088B | 1000 | Simple greedy construction |
| 3 | Movie Festival | CSES | 1200 | Classic interval scheduling, sort by end |
| 4 | Tasks and Deadlines | CSES | 1300 | Sort by duration, exchange argument proof |
| 5 | Stick Divisions | CSES | 1400 | Reverse Huffman—min-heap merge |
| 6 | Making a String | CF 1353D | 1400 | Greedy sort by cost, accumulate |
| 7 | Two Divisors | CF 1366D | 1500 | Greedy factorization + construction |
| 8 | Playlist | CF 1141F2 | 1600 | Sort + greedy selection with constraints |
| 9 | Buses on a Staircase | CF 1029C | 1700 | Interval covering / greedy scan |
| 10 | Voting | CF 1251E2 | 1800 | Greedy with regret, priority queue |
Designing Test Cases
Greedy solutions either work perfectly or fail catastrophically—there's rarely a middle ground. The goal of testing is to find the counterexample before the judge does.
Minimal cases:
- Empty input:
n = 0. Does your code handle it without crashing? Many greedy solutions skip this and segfault ona[0]. - Single element:
n = 1. The answer is usually trivial. Verifies your loop/sort doesn't break on degenerate input.
Edge cases:
- All equal elements:
a = [5, 5, 5, 5]. Many greedy comparators behave differently when elements are tied—especially if your sort isn't stable or your comparator isn't strict weak ordering. - Already sorted / reverse sorted: If your greedy relies on sorting, test what happens when input is already in the "right" or "wrong" order.
- Adversarial case designed to break wrong greedy: This is the most important test. Think: "What would break the first greedy idea I had?" For example, in activity selection, an interval that starts earliest but is extremely long. In coin change, denominations that aren't powers of each other.
- Duplicates in the decision criterion: Two items with the same priority but different secondary attributes.
Stress test (run locally before submitting):
cpp
// Compare greedy answer against brute force for small inputs.
mt19937 rng(42);
for (int iter = 0; iter < 100000; iter++) {
int n = rng() % 10 + 1;
// generate random problem instance (items, intervals, etc.)
int greedy_ans = solve_greedy(/* ... */);
int bf_ans = solve_brute(/* ... */); // try all n! permutations or 2^n subsets
assert(greedy_ans == bf_ans);
}Keep n <= 10 so brute force over permutations (10! ~= 3.6M) or subsets (2^{10} = 1024) stays fast. If the assert fires, you've found a counterexample—print it, study it, and fix your greedy (or switch to DP).
Pro tip: When your greedy passes the stress test but you're not sure about correctness, try
n <= 15with 1M iterations. If it survives, your greedy is almost certainly correct—or the counterexample is very rare and unlikely to appear in judge tests.
Further Reading
The core insight of greedy is deceptively simple: if making the locally optimal choice at every step never needs to be undone, the strategy is globally optimal. The hard part is not the code—it is proving (via exchange argument) that "local beats global." The fingerprint to recognize: optimization problem + sorting reveals a "never regret" ordering → greedy. A useful mnemonic: "Sort, grab the best, never look back—but prove it first."
Greedy algorithms have ancient roots (the coin-change heuristic was used by merchants for centuries) and were formally analyzed by Jack Edmonds in the 1960s through matroid theory, which characterizes exactly when greedy gives optimal solutions. The classic activity selection proof appears in Cormen et al.'s Introduction to Algorithms (1990).
- cp-algorithms: Greedy algorithms—general reference for exchange arguments and scheduling.
- CF Blog: Greedy problems and exchange arguments—community discussion of proof techniques.
- USACO Guide: Greedy with Sorting—introductory greedy problems with editorial.
- Jeff Erickson: Greedy Algorithms (textbook chapter)—rigorous treatment of exchange arguments and matroids.
Cross-references in this guidebook:
- Sorting and Searching—prerequisite; most greedy starts with a sort.
- Binary Search—greedy + binary search is a common combined technique.
- Coordinate Compression—next topic; often paired with greedy on intervals.
- Dynamic Programming—1D Introduction—when greedy fails, DP is usually the answer.
- Contribution Technique—when the "greedy" intuition is really about counting contributions.
- DP vs Greedy Guide—systematic guide for choosing between the two.
Rating Progression
- CF 1200: Sort by one criterion and iterate: interval scheduling by end time, coin change with largest denomination first.
- CF 1500: Exchange argument proofs; sort by ratio or custom comparator; greedy + binary search hybrid.
- CF 1800: Greedy with regret (priority queues to undo decisions); multi-criteria greedy with tiebreakers; greedy on trees.
- CF 2100: Matroid intersection; greedy + DP hybrid where greedy handles one dimension; adversarial greedy arguments.
Before You Code Checklist
- [ ] Identified the greedy criterion: what am I sorting by, and why is that ordering optimal?
- [ ] Sketched an exchange argument: if I swap two adjacent elements in my ordering, does the answer get worse (or at least not better)?
- [ ] Checked for counterexamples: does a small hand-crafted input break the greedy? (Try 3-5 elements.)
- [ ] Verified the comparator is a strict weak ordering (irreflexive, transitive, asymmetric)—broken comparators cause undefined behavior in
sort. - [ ] Considered whether greedy is correct at all—could this be a DP or flow problem in disguise?
What Would You Do If...?
- Your greedy passes all samples but fails on a hidden test with just 4 elements—how would you systematically find a counterexample?
- You realize mid-contest that your greedy needs to "undo" a previous choice—what data structure supports this "greedy with regret" pattern?
- The problem has two competing objectives (minimize cost and maximize coverage)—when does greedy still work, and when must you switch to DP?
The Mistake That Teaches
A contestant sorted jobs by deadline for a scheduling problem, greedily assigning each to the latest available slot. It passed samples and even stress tests up to
Debug This
Bug 1:
cpp
// Interval scheduling: pick max non-overlapping intervals
sort(intervals.begin(), intervals.end(), [](auto& a, auto& b) {
return a.start < b.start; // Bug: wrong sort key
});
int cnt = 0, last_end = -1;
for (auto& [s, e] : intervals) {
if (s >= last_end) { cnt++; last_end = e; }
}Answer
Should sort by end time, not start time. Sorting by start time doesn't guarantee maximum non-overlapping intervals—a long interval starting early can block many short ones. The classic proof requires earliest-end-time-first.
Bug 2:
cpp
// Custom comparator for sorting
sort(a.begin(), a.end(), [](int x, int y) {
return x <= y; // Bug here
});Answer
The comparator uses <= instead of <. This violates strict weak ordering (it's not irreflexive: comp(x, x) returns true). This is undefined behavior in C++ and can cause crashes, infinite loops, or wrong output depending on the STL implementation.
Bug 3:
cpp
// Greedy: assign tasks to minimize max lateness
// Each task has processing time p[i] and deadline d[i]
sort(tasks.begin(), tasks.end(), [](auto& a, auto& b) {
return a.d - a.p < b.d - b.p; // Bug: wrong criterion
});
int time = 0, max_late = 0;
for (auto& t : tasks) {
time += t.p;
max_late = max(max_late, time - t.d);
}Answer
To minimize maximum lateness, sort by deadline d alone (Earliest Deadline First), not by d - p. The EDF ordering is provable via exchange argument; sorting by slack d - p does not yield optimal results.
Code Reading Exercise
What does this code compute? Read it carefully before checking the answer.
cpp
int solve(vector<pair<int, int>>& intervals) {
sort(intervals.begin(), intervals.end(),
[](auto& a, auto& b) { return a.second < b.second; });
int count = 0, last = INT_MIN;
for (auto& [s, e] : intervals) {
if (s >= last) {
count++;
last = e;
}
}
return count;
}Answer
This finds the maximum number of non-overlapping intervals (the activity selection problem). Sorting by end time and greedily picking the earliest-finishing compatible interval is provably optimal via exchange argument. The condition `s >= last` treats intervals sharing an endpoint as non-overlapping. This runs in O(n log n) due to the sort.
What does this code compute?
cpp
bool solve(vector<int>& a) {
int n = a.size(), reach = 0;
for (int i = 0; i < n; i++) {
if (i > reach) return false;
reach = max(reach, i + a[i]);
}
return reach >= n - 1;
}Answer
This determines whether you can reach the last index of the array, where a[i] is the maximum jump length from position i (the "Jump Game" problem). The greedy insight: maintain the farthest reachable index. At each position, if you haven't been reached (i > reach), it's impossible. Otherwise, extend the reach. No backtracking needed—O(n) time.
What to Solve Next
- Complete Search—when greedy fails, brute force with pruning is the safety net.
- DP vs Greedy Guide—side-by-side comparison for when you're unsure.
- Practice Ladder—drill greedy problems by rating.
Solve With Me — CF 1325D "Ehab the Xor-ist"
Problem: find the shortest array whose elements XOR to
Immediate observations: XOR of all elements is
Now the greedy construction. Simplest case: can I do it with one element? Only if
Otherwise, can I always do it with two elements? I need
We have
What if
Three elements:
So the algorithm: if
What to recognize next time: When a problem asks you to construct elements with a given XOR and sum, the identity