All RoadmapsDSA Mastery › Chapter 7
Chapter 7 · Intermediate · Prereq: Chapter 6

Greedy Algorithms

Master the paradigm of short-term optimal decisions. Learn interval scheduling, prove correctness with exchange arguments, and conquer the sweeping line.

10 Sections 8 Practice Problems Intermediate ← Back to Roadmap

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 k is 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

DimensionGreedyDynamic Programming
Decision styleMake one irreversible choice per stepExplore all choices; store best subproblem results
Subproblem dependencyEach step independent of futureSubproblems 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 correctGreedy choice property + optimal substructureOptimal substructure alone (no greedy choice needed)
Classic problemsActivity selection, Huffman, Dijkstra, Jump Game0/1 Knapsack, Coin Change, LCS, Edit Distance
Key riskEasy to construct a wrong greedy — always verifyAlways correct if recurrence is right; harder to optimise
💰 Real-World Analogy: Making Change

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)

Intervals
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 trace
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 trace
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

ProblemGreedy StrategyReal-World System
Activity / interval selectionSort by end time; pick earliest-finishingConference room booking, CPU scheduling
Minimum spanning treeKruskal: pick cheapest safe edgeNetwork layout, road planning
Shortest path (no neg)Dijkstra: expand closest unvisitedGPS navigation, IP routing
Huffman encodingMerge lowest-freq symbols firstgzip compression, JPEG encoding
Fractional knapsackSort by value/weight; take highest ratioPortfolio optimisation
Job scheduling (lateness)Sort jobs by deadline ascendingOS task scheduling
Gas station circuitTrack surplus; restart on deficitLogistics route planning

Section 4 — Core Concepts & Algorithms

4.1 — Interval Scheduling & Merging

Maximum Non-Overlapping Intervals (Activity Selection)

C++
// 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

C++
// 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

C++
// 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

C++
// 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

C++
// 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

C++
// 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 StrategySort / Order By
Max non-overlapping intervalsPick earliest end time firstEnd time ascending
Min intervals to removen minus max non-overlappingEnd time ascending
Min meeting rooms neededSweep events +1/-1Event time ascending
Can reach the last index?Track maxReachNo sort — L to R
Min jumps to last indexExpand jump window greedilyNo sort — L to R
Max contiguous subarray sumKadane: restart when sum < 0No sort — L to R
Best time to buy and sell stockTrack min price; max profitNo sort — L to R
Assign cookies to childrenMatch smallest sufficient cookieBoth arrays asc
Partition labelsGreedily extend to last occurenceLast occ map
🔍 How to Recognise a Greedy Problem
  • 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

C++
// 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

C++
// 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

AlgorithmTimeSpace
Activity selection (max non-overlap)O(n log n)O(1)
Merge overlapping intervalsO(n log n)O(n)
Minimum meeting roomsO(n log n)O(n)
Jump Game I/IIO(n)O(1)
Kadane's (max subarray)O(n)O(1)
Gas station circuitO(n)O(1)
Partition labelsO(n)O(1)
Fractional knapsack / HuffmanO(n log n)O(n)

Section 8 — Solved Problem 1: Jump Game

1. Observations & Core Idea

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

ApproachTimeSpaceMethod
Brute Force DPO(n^2)O(n)dp[i] = any(dp[j]) for valid jumps
Optimised GreedyO(n)O(1)Update maxReach = max(maxReach, i+nums[i])

3. Optimized Greedy Approach

C++
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

1. Observations & Core Idea

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

ApproachTimeSpaceMethod
Graph Connected ComponentsO(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

C++
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 t and another starts at time t, the end must be processed FIRST (-1 before +1) to avoid allocating a phantom extra room.
Warning

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:

#ProblemDifficultyKey Concept
11323. Maximum 69 NumberEasyGreedy string math
21710. Maximum Units on a TruckEasyFractional knapsack desc
3134. Gas StationMediumCumulative diff tracker
455. Jump GameMediumTrack maxReach
545. Jump Game IIMediumExpand jump frontier ranges
6435. Non-overlapping IntervalsMediumSort by End time
7763. Partition LabelsMediumLast occ map bound
8135. CandyHardTwo-pass greedy slopes