2Brute2Best
πŸ”₯0

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

Remember this Β· Greedy
Trigger
Maximise count / minimise cost with a natural "best next step", sorting makes the choice obvious, reachability.
Think
Is there a choice I can always make safely without looking ahead?
Key question
Why can't the greedy choice be worse than the optimal one? (Try to break it with a small example.)
A

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.

B

See it move

Green = taken, red = skipped because it overlaps the last taken activity, purple = the final choice.

Interval scheduling: earliest finish firstactivities = 3-5, 0-6, 1-4, 5-9, 8-9, 5-7step 1/9
given
0
1
2
3
4
5
3-5
0-6
1-4
5-9
8-9
5-7
activities as start-end

Six 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.

Running balance: restart after a failuregas = [3, 1, 2, 4, 1], cost = [2, 3, 3, 1, 1]step 1/7
gas
0
1
2
3
4
3
1
2
4
1
cost
0
1
2
3
4
2
3
3
1
1
net
0
1
2
3
4
1
-2
-1
3
0
Totals
sum of net
1
total = 1 β‰₯ 0, so a start exists

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.

C

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

0/1 knapsack (take an item whole or not at all)
β†’ DP β€” Knapsack

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.

Coin change with arbitrary coins
β†’ DP β€” Knapsack (unbounded)

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.

Merge overlapping intervals / count rooms
β†’ Intervals pattern

Nothing is being chosen or left out; every interval stays and you only sweep. Greedy interval scheduling decides which intervals to KEEP.

Shortest path in a weighted graph
β†’ Shortest Paths (Dijkstra)

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.

Jump game where jumps can go left or right
β†’ Graph BFS / DFS

"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.

D

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.

E

Sub-patterns

Same core idea, different rule for β€œvalid”. Learn them in this order.

F

Don't mix these up

Greedy vs DP

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.

Sort by end vs Sort by start

Choosing the most non-overlapping intervals β†’ by end. Merging intervals or counting rooms β†’ by start (Intervals pattern).

Fractional knapsack vs 0/1 knapsack

Items can be cut β†’ ratio greedy. Whole items only β†’ DP over capacity.

Jump Game (farthest reach) vs Jump Game III (BFS)

Forward moves up to nums[i] β†’ reachable set is a prefix, one variable. Exact jumps both ways β†’ graph search with visited.

Exchange argument vs Stays ahead

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.

G

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.

interval-greedy-template.cpp
// 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.

exchange-template.cpp
// 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.

reach-template.cpp
// 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.

assignment-template.cpp
// 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.

balance-template.cpp
// 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).

heap-greedy-template.cpp
// 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;
}