Greedy Algorithms
Master the paradigm of short-term optimal decisions. Learn interval scheduling, prove correctness with exchange arguments, and conquer the sweeping line.
Section 1 — What Is a Greedy Algorithm?
A greedy algorithm builds a solution by making the locally optimal choice at each step without reconsidering past decisions. It never backtracks. For greedy to yield a globally optimal solution, the problem must satisfy two key properties.
Two Conditions for Greedy Correctness
- 1. GREEDY CHOICE PROPERTY: A globally optimal solution can be constructed by making locally optimal (greedy) choices. The greedy choice at step
kis safe — it is part of some optimal solution. - 2. OPTIMAL SUBSTRUCTURE: An optimal solution to the whole problem contains optimal solutions to its subproblems. After making the greedy choice, the remaining subproblem has the same structure.
- If BOTH conditions hold, greedy is correct. If either fails, greedy gives a wrong answer and you need DP or backtracking.
- Proving greedy correctness: use the exchange argument — assume an optimal solution makes a different choice; show that swapping to the greedy choice does not worsen the result.
1.1 — Greedy vs Dynamic Programming
| Dimension | Greedy | Dynamic Programming |
|---|---|---|
| Decision style | Make one irreversible choice per step | Explore all choices; store best subproblem results |
| Subproblem dependency | Each step independent of future | Subproblems may overlap; memoisation avoids recompute |
| Complexity (typical) | O(n log n) or O(n) | O(n^2) or O(n * capacity) |
| Space (typical) | O(1) or O(n) | O(n) to O(n^2) |
| When correct | Greedy choice property + optimal substructure | Optimal substructure alone (no greedy choice needed) |
| Classic problems | Activity selection, Huffman, Dijkstra, Jump Game | 0/1 Knapsack, Coin Change, LCS, Edit Distance |
| Key risk | Easy to construct a wrong greedy — always verify | Always correct if recurrence is right; harder to optimise |
Problem: make change for 41 cents using fewest coins (denominations: 25, 10, 5, 1).
Greedy: always pick the largest coin that does not exceed the remaining amount.
41-> pick 25 (remain 16) -> pick 10 (remain 6) -> pick 5 (remain 1) -> pick 1.- Result: 4 coins. This is optimal for standard US coin denominations.
BUT: for denominations {1, 3, 4}, greedy fails on amount 6. Greedy picks 4+1+1=3 coins; optimal is 3+3=2 coins.
This illustrates why greedy correctness must always be proven — it is not automatic.
Section 2 — Visual Diagrams: Core Greedy Patterns
Diagram 1 — Interval Scheduling (Activity Selection)
Interval Scheduling: Earliest-Finish-Time Greedy
Problem: select maximum number of non-overlapping intervals.
Intervals: [1,4], [3,5], [0,6], [5,7], [3,9], [5,10], [6,11], [8,12], [8,11], [2,14]
Greedy rule: ALWAYS pick the interval with the EARLIEST END TIME
that does not conflict with the last selected interval.
Proof: finishing early leaves maximum room for future intervals.
Sort by end time:
[1,4] [3,5] [0,6] [5,7] [3,9] [5,10] [6,11] [8,11] [8,12] [2,14]
Step 1: Pick [1,4]. last_end = 4.
Step 2: [3,5] start=3 < last_end=4 -> SKIP
Step 3: [0,6] start=0 < 4 -> SKIP
Step 4: [5,7] start=5 >= 4 -> PICK. last_end = 7.
Step 5: [3,9] start=3 < 7 -> SKIP
Step 6: [5,10] start=5 < 7 -> SKIP
Step 7: [6,11] start=6 < 7 -> SKIP
Step 8: [8,11] start=8 >= 7 -> PICK. last_end = 11.
Step 9: [8,12] start=8 < 11 -> SKIP
Step 10:[2,14] start=2 < 11 -> SKIP
Selected: [1,4], [5,7], [8,11] = 3 intervals. Optimal!
Diagram 2 — Jump Game
Jump Game: maxReach Greedy
nums = [2, 3, 1, 1, 4]
nums[i] = max jump length FROM index i.
Question: can you reach the last index?
Greedy: track 'maxReach' = farthest index reachable so far.
i=0: maxReach = max(0, 0+nums[0]) = max(0, 0+2) = 2
i=1: i=1 <= maxReach=2, ok. maxReach = max(2, 1+3) = 4
i=2: i=2 <= maxReach=4, ok. maxReach = max(4, 2+1) = 4
i=3: i=3 <= maxReach=4, ok. maxReach = max(4, 3+1) = 4
i=4: i=4 <= maxReach=4, ok. maxReach = max(4, 4+4) = 8
maxReach >= last index (4) -> return true.
Impossible example: nums = [3, 2, 1, 0, 4]
i=0: maxReach = 3
i=1: maxReach = max(3, 1+2) = 3
i=2: maxReach = max(3, 2+1) = 3
i=3: maxReach = max(3, 3+0) = 3
i=4: i=4 > maxReach=3 -> STUCK. Return false.
Key insight: if we ever reach a position where i > maxReach,
we are stuck in a 'zero island' with no way forward.
Diagram 3 — Kadane's Algorithm (Maximum Subarray)
Kadane's Algorithm: Maximum Subarray Trace
nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
Greedy insight: extend current subarray if it helps; restart if current sum < 0.
'currSum' = best sum ending at current index.
'maxSum' = best sum seen so far.
i=0: currSum = max(-2, -2) = -2. maxSum = -2
i=1: currSum = max( 1, -2+1)= 1. maxSum = 1
i=2: currSum = max(-3, 1-3)= -2. maxSum = 1
i=3: currSum = max( 4, -2+4)= 4. maxSum = 4
i=4: currSum = max(-1, 4-1)= 3. maxSum = 4
i=5: currSum = max( 2, 3+2)= 5. maxSum = 5
i=6: currSum = max( 1, 5+1)= 6. maxSum = 6 <- peak
i=7: currSum = max(-5, 6-5)= 1. maxSum = 6
i=8: currSum = max( 4, 1+4)= 5. maxSum = 6
Answer: maxSum = 6 (subarray [4, -1, 2, 1])
Greedy rule: if adding current element to currSum makes it negative,
it is better to start fresh from the current element (restart).
Section 3 — Real-World Use Cases
| Problem | Greedy Strategy | Real-World System |
|---|---|---|
| Activity / interval selection | Sort by end time; pick earliest-finishing | Conference room booking, CPU scheduling |
| Minimum spanning tree | Kruskal: pick cheapest safe edge | Network layout, road planning |
| Shortest path (no neg) | Dijkstra: expand closest unvisited | GPS navigation, IP routing |
| Huffman encoding | Merge lowest-freq symbols first | gzip compression, JPEG encoding |
| Fractional knapsack | Sort by value/weight; take highest ratio | Portfolio optimisation |
| Job scheduling (lateness) | Sort jobs by deadline ascending | OS task scheduling |
| Gas station circuit | Track surplus; restart on deficit | Logistics route planning |
Section 4 — Core Concepts & Algorithms
4.1 — Interval Scheduling & Merging
Maximum Non-Overlapping Intervals (Activity Selection)
// Activity Selection — O(n log n)
// Sort by end time; greedily pick earliest-finishing non-conflicting interval
int maxNonOverlapping(vector<vector<int>>& intervals) {
if (intervals.empty()) return 0;
sort(intervals.begin(), intervals.end(),
[](auto& a, auto& b){ return a[1] < b[1]; }); // sort by end time
int count = 1, lastEnd = intervals[0][1];
for (int i = 1; i < (int)intervals.size(); i++) {
if (intervals[i][0] >= lastEnd) {
count++; lastEnd = intervals[i][1];
}
}
return count;
}Merge Overlapping Intervals
// Merge Overlapping Intervals — O(n log n)
// Sort by start time; merge when current interval overlaps previous
vector<vector<int>> merge(vector<vector<int>>& intervals) {
sort(intervals.begin(), intervals.end()); // sort by start
vector<vector<int>> res;
for (auto& iv : intervals) {
if (res.empty() || iv[0] > res.back()[1]) res.push_back(iv);
else res.back()[1] = max(res.back()[1], iv[1]);
}
return res;
}4.2 — Jump Game Variants
// JUMP GAME I (LC 55) — Can you reach the last index?
bool canJump(vector<int>& nums) {
int maxReach = 0;
for (int i = 0; i < (int)nums.size(); i++) {
if (i > maxReach) return false; // stuck
maxReach = max(maxReach, i + nums[i]);
}
return true;
}
// JUMP GAME II (LC 45) — Minimum jumps to reach end
int jump(vector<int>& nums) {
int jumps = 0, currEnd = 0, farthest = 0;
for (int i = 0; i < (int)nums.size()-1; i++) {
farthest = max(farthest, i + nums[i]);
if (i == currEnd) { // reached end of jump range
jumps++; currEnd = farthest;
}
}
return jumps;
}4.3 — Kadane's Algorithm
// Kadane's Algorithm — O(n) O(1)
// LeetCode 53 — Maximum Subarray
int maxSubArray(vector<int>& nums) {
int currSum = nums[0], maxSum = nums[0];
for (int i = 1; i < (int)nums.size(); i++) {
currSum = max(nums[i], currSum + nums[i]);
maxSum = max(maxSum, currSum);
}
return maxSum;
}4.4 — Gas Station Circuit
// LeetCode 134 — Gas Station
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int totalSurplus = 0, currSurplus = 0, start = 0;
for (int i = 0; i < (int)gas.size(); i++) {
int diff = gas[i] - cost[i];
totalSurplus += diff; currSurplus += diff;
if (currSurplus < 0) { // Reset if deficit
start = i + 1; currSurplus = 0;
}
}
return totalSurplus >= 0 ? start : -1;
}4.5 — Huffman Encoding
// Huffman Encoding — O(n log n)
struct HNode {
int freq; char ch;
HNode *left, *right;
HNode(int f, char c) : freq(f), ch(c), left(nullptr), right(nullptr) {}
};
HNode* buildHuffman(unordered_map<char,int>& freq) {
auto cmp = [](HNode* a, HNode* b){ return a->freq > b->freq; };
priority_queue<HNode*, vector<HNode*>, decltype(cmp)> pq(cmp);
for (auto& [ch, f] : freq) pq.push(new HNode(f, ch));
while (pq.size() > 1) {
HNode* l = pq.top(); pq.pop();
HNode* r = pq.top(); pq.pop();
HNode* parent = new HNode(l->freq + r->freq, '\0');
parent->left = l; parent->right = r;
pq.push(parent);
}
return pq.top(); // root
}Section 5 — Pattern Recognition Guide
| If the problem asks... | Greedy Strategy | Sort / Order By |
|---|---|---|
| Max non-overlapping intervals | Pick earliest end time first | End time ascending |
| Min intervals to remove | n minus max non-overlapping | End time ascending |
| Min meeting rooms needed | Sweep events +1/-1 | Event time ascending |
| Can reach the last index? | Track maxReach | No sort — L to R |
| Min jumps to last index | Expand jump window greedily | No sort — L to R |
| Max contiguous subarray sum | Kadane: restart when sum < 0 | No sort — L to R |
| Best time to buy and sell stock | Track min price; max profit | No sort — L to R |
| Assign cookies to children | Match smallest sufficient cookie | Both arrays asc |
| Partition labels | Greedily extend to last occurence | Last occ map |
- SIGNAL 1: "Maximum number of..." or "Minimum number of..." with intervals/tasks.
- SIGNAL 2: Sorting the input by one dimension immediately unlocks a simple scan.
- SIGNAL 3: At each step there is an obvious "best" local choice.
- VERIFY: Assume optimal uses a different choice; show swapping to the greedy choice does not worsen the result.
Section 6 — Complete C++ Implementations
6.1 — Non-overlapping Intervals
// Minimum intervals to remove so remaining are non-overlapping
// Time: O(n log n) Space: O(1)
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
sort(intervals.begin(), intervals.end(),
[](auto& a, auto& b){ return a[1] < b[1]; });
int keep = 1, lastEnd = intervals[0][1];
for (int i = 1; i < (int)intervals.size(); i++) {
if (intervals[i][0] >= lastEnd) {
keep++; lastEnd = intervals[i][1];
}
}
return (int)intervals.size() - keep;
}6.2 — Partition Labels
// Partition Labels — O(n)
// Partition string so each letter appears in at most one part.
vector<int> partitionLabels(string s) {
int last[26] = {};
for (int i = 0; i < (int)s.size(); i++) last[s[i]-'a'] = i;
vector<int> result;
int start = 0, end = 0;
for (int i = 0; i < (int)s.size(); i++) {
end = max(end, last[s[i]-'a']); // extend partition
if (i == end) { // partition boundary found
result.push_back(end - start + 1);
start = i + 1;
}
}
return result;
}Section 7 — Complexity Reference
| Algorithm | Time | Space |
|---|---|---|
| Activity selection (max non-overlap) | O(n log n) | O(1) |
| Merge overlapping intervals | O(n log n) | O(n) |
| Minimum meeting rooms | O(n log n) | O(n) |
| Jump Game I/II | O(n) | O(1) |
| Kadane's (max subarray) | O(n) | O(1) |
| Gas station circuit | O(n) | O(1) |
| Partition labels | O(n) | O(1) |
| Fractional knapsack / Huffman | O(n log n) | O(n) |
Section 8 — Solved Problem 1: Jump Game
In DP form, this is reachability. To reach i, we need to reach some j < i where j + nums[j] >= i. The DP takes O(n^2).
Greedy Insight: If we can reach any index k, we can naturally reach any index < k. So we just need to track the single "farthest reachable index" (maxReach) scanned from left to right. If at index i, we find i > maxReach, we are stranded!
2. Approach Comparison
| Approach | Time | Space | Method |
|---|---|---|---|
| Brute Force DP | O(n^2) | O(n) | dp[i] = any(dp[j]) for valid jumps |
| Optimised Greedy | O(n) | O(1) | Update maxReach = max(maxReach, i+nums[i]) |
3. Optimized Greedy Approach
class Solution {
public:
bool canJump(vector<int>& nums) {
int maxReach = 0;
int n = nums.size();
for (int i = 0; i < n; i++) {
if (i > maxReach) {
return false; // stranded at i
}
maxReach = max(maxReach, i + nums[i]);
if (maxReach >= n - 1) {
return true; // early exit
}
}
return true;
}
};Section 9 — Solved Problem 2: Merge Intervals
Intervals can overlap in arbitrary orders. Standard greedy tells us: sort by START time.
If sorted by start time, overlapping intervals will always be strictly adjacent in the sorted array. If the current interval's start <= the previous interval's end, they overlap. We merge them by making the new end = max(end1, end2).
2. Approach Comparison
| Approach | Time | Space | Method |
|---|---|---|---|
| Graph Connected Components | O(n^2) | O(n^2) | Nodes are intervals; edges if overlap. |
| Sort and Scan (Greedy) | O(n log n) | O(n) | Sort by start, merge adjacent continuously. |
3. Optimized Greedy Approach
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
if (intervals.empty()) return {};
// 1. Sort by start coordinate
sort(intervals.begin(), intervals.end());
vector<vector<int>> merged;
merged.push_back(intervals[0]);
// 2. Scan and merge
for (int i = 1; i < intervals.size(); i++) {
int currentStart = intervals[i][0];
int currentEnd = intervals[i][1];
int lastEnd = merged.back()[1];
if (currentStart <= lastEnd) {
// Overlap: update ending
merged.back()[1] = max(lastEnd, currentEnd);
} else {
// No overlap: strictly later interval
merged.push_back(intervals[i]);
}
}
return merged;
}
};Section 10 — Common Mistakes & Edge Cases
- Assuming Greedy ALWAYS works: The #1 mistake is using a greedy approach where DP is required (e.g., 0/1 Knapsack where you greedily pick the highest value/weight ratio, but it leaves dead space). Always ask yourself: "Can I create a small test case where this greedy choice blocks the best answer?"
- Sorting by the wrong attribute in intervals: Non-overlapping/activity selection -> Sort by END time. Merging -> Sort by START time. Meeting rooms -> Split into events and sort by TIME.
- Tie-breaking incorrectly: E.g., in Meeting Rooms, if a meeting ends at time
tand another starts at timet, the end must be processed FIRST (-1 before +1) to avoid allocating a phantom extra room.
Edge Cases to Consider:
- Empty inputs `[]` or single element `[0]`.
- Arrays with all negative numbers (Kadane's should return the max single negative, not `0`).
- Fully enclosed intervals: `[[1, 10], [2, 5]]`. When merging, `lastEnd` must be `max(10, 5)` = 10.
Practice Problems
Recommended progression for Greedy Algorithms:
| # | Problem | Difficulty | Key Concept |
|---|---|---|---|
| 1 | 1323. Maximum 69 Number | Easy | Greedy string math |
| 2 | 1710. Maximum Units on a Truck | Easy | Fractional knapsack desc |
| 3 | 134. Gas Station | Medium | Cumulative diff tracker |
| 4 | 55. Jump Game | Medium | Track maxReach |
| 5 | 45. Jump Game II | Medium | Expand jump frontier ranges |
| 6 | 435. Non-overlapping Intervals | Medium | Sort by End time |
| 7 | 763. Partition Labels | Medium | Last occ map bound |
| 8 | 135. Candy | Hard | Two-pass greedy slopes |