{}DSA Atlas

Greedy Algorithms

Make the locally best choice and never look back — when it works, how to prove it (exchange argument), and the classic greedy families.

Intermediate15 practice problems2 easy10 medium3 hard

The idea

A greedy algorithm builds the answer step by step, always taking the choice that looks best right now, and never reconsidering. It's usually simple and fast (often O(n log n) because of a sort) — but it's only correct for problems with the right structure.

When greedy works

Two properties:

  1. Greedy choice property: some optimal solution starts with the greedy choice.
  2. Optimal substructure: after making it, what remains is a smaller instance of the same problem.

Proving it: the exchange argument

Take any optimal solution that doesn't make the greedy choice. Show you can swap in the greedy choice without making it worse. Therefore an optimal solution with the greedy choice exists.

Example — activity selection (max non-overlapping intervals): pick the one that ends earliest. Let OPT's first interval be x, and greedy's be g with end(g) ≤ end(x). Replace x with g in OPT: g ends no later, so it can't conflict with anything that came after x. Same count, still valid. ∎

Family 1: Interval scheduling

Sort by end time; take an interval whenever it doesn't overlap the last one taken.

def max_non_overlapping(intervals):
    intervals.sort(key=lambda x: x[1])
    count, last_end = 0, float("-inf")
    for s, e in intervals:
        if s >= last_end:          # use > if touching endpoints overlap
            count += 1
            last_end = e
    return count

# Non-overlapping Intervals (min removals) = n - max_non_overlapping
int maxNonOverlapping(vector<vector<int>>& iv) {
    sort(iv.begin(), iv.end(), [](auto& a, auto& b) { return a[1] < b[1]; });
    int count = 0; long long lastEnd = LLONG_MIN;
    for (auto& x : iv)
        if (x[0] >= lastEnd) { count++; lastEnd = x[1]; }
    return count;
}

More in Intervals.

Family 2: Reachability (jump games)

Track the farthest point reachable so far.

Python
def can_jump(nums):
    farthest = 0
    for i, x in enumerate(nums):
        if i > farthest:
            return False
        farthest = max(farthest, i + x)
    return True

def min_jumps(nums):                 # Jump Game II: implicit BFS levels
    jumps = cur_end = farthest = 0
    for i in range(len(nums) - 1):
        farthest = max(farthest, i + nums[i])
        if i == cur_end:             # must jump to go further
            jumps += 1
            cur_end = farthest
    return jumps

Family 3: Sort, then match

Sort both sides and pair them in a consistent order.

Python
def find_content_children(greed, cookies):
    greed.sort(); cookies.sort()
    child = 0
    for c in cookies:
        if child < len(greed) and c >= greed[child]:
            child += 1
    return child

Two-sided greedy with pointers: Boats to Save People (heaviest with lightest), Bag of Tokens.

Family 4: Running balance with reset

Python
def can_complete_circuit(gas, cost):
    if sum(gas) < sum(cost):
        return -1
    tank = start = 0
    for i in range(len(gas)):
        tank += gas[i] - cost[i]
        if tank < 0:            # can't reach i+1 from any start in [start, i]
            start = i + 1
            tank = 0
    return start

Kadane's algorithm is the same idea: drop the running prefix when it goes negative.

Family 5: Two passes for two-sided constraints

Candy: each child with a higher rating than a neighbour gets more candy. Satisfy left neighbours in one pass, right neighbours in another, take the max.

Python
def candy(ratings):
    n = len(ratings)
    c = [1] * n
    for i in range(1, n):
        if ratings[i] > ratings[i - 1]:
            c[i] = c[i - 1] + 1
    for i in range(n - 2, -1, -1):
        if ratings[i] > ratings[i + 1]:
            c[i] = max(c[i], c[i + 1] + 1)
    return sum(c)

Family 6: Greedy + heap (“best available so far”)

When choices become available over time, keep them in a heap and take the best one when you must decide.

Python
import heapq

def min_refuel_stops(target, fuel, stations):
    heap, stops, i = [], 0, 0          # max-heap of fuel amounts passed
    stations.append((target, 0))
    for pos, gas in stations:
        while fuel < pos:              # can't reach pos: refuel retroactively
            if not heap:
                return -1
            fuel += -heapq.heappop(heap)
            stops += 1
        heapq.heappush(heap, -gas)
    return stops

Same shape: IPO, Course Schedule III (take courses by deadline, drop the longest when over time), Furthest Building You Can Reach.

Family 7: Monotonic stack greedy

“Remove k digits to make the smallest number” — a smaller digit earlier always wins, so pop larger previous digits while you can.

Python
def remove_k_digits(num, k):
    stack = []
    for d in num:
        while k and stack and stack[-1] > d:
            stack.pop()
            k -= 1
        stack.append(d)
    stack = stack[:len(stack) - k]        # still need to remove from the end
    return "".join(stack).lstrip("0") or "0"

Also: Remove Duplicate Letters, Create Maximum Number.

Family 8: Counting formulas

Task Scheduler: the most frequent task dictates the frame. With max frequency f and m tasks sharing it, the answer is max(len(tasks), (f − 1)(n + 1) + m).

Greedy vs DP: a quick test

ProblemGreedy?Why
Coin change with {1, 5, 10, 25}✓canonical coin system
Coin change with {1, 3, 4}, amount 6✗greedy 4+1+1 (3 coins) vs 3+3 (2) → DP
Fractional knapsack✓take by value/weight ratio
0/1 knapsack✗can't take fractions → DP
Activity selection✓exchange argument
Weighted interval scheduling✗weights break the argument → DP + binary search

Common mistakes

  • Choosing the wrong sort key (start vs end, ascending vs descending).
  • Not handling ties consistently (>= vs > for touching intervals).
  • Assuming greedy works without testing a counterexample.
  • Forgetting the feasibility pre-check (e.g. total gas < total cost).

Practice problems

Ordered roughly from warm-up to hard. Try each one for 25–40 minutes before peeking at the key idea; if you needed the hint, mark it “review” and redo it in a few days.

0 / 15 solved
Sort both; give each child the smallest cookie that satisfies them.
Easy
Simulate; for $20 prefer giving $10+$5 over three $5s.
Easy
Sum every positive day-to-day difference.
Medium
Track farthest reachable; fail if i > farthest.
Medium
BFS by levels: current range end and farthest; jump when i reaches the current end.
Medium
If total gas >= total cost an answer exists; reset the start whenever the running tank goes negative.
Medium
Extend the current part to the last occurrence of every char in it.
Medium
Always start a group from the smallest remaining card; use a counter + sorted keys.
Medium
Sort by end; keep an interval if it starts after the last kept end; remove the rest.
Medium
Answer = max(len(tasks), (maxFreq − 1)(n + 1) + countOfMaxFreq).
Medium
Track the range [lo, hi] of possible open counts; clamp lo at 0; fail if hi < 0.
Medium
Sort by end; shoot at the first end; skip every balloon starting at or before it.
Medium
Left-to-right pass for left neighbours, right-to-left for right neighbours; take the max.
Hard
Sort projects by capital; push affordable ones into a max-heap of profit; take the best k times.
Hard
Drive as far as possible; when stuck, retroactively refuel at the biggest station passed (max-heap).
Hard

Click the box to cycle: solved ✓ → needs review ↺ → not started. Links open LeetCode.