Begin Algo
Greedy · 01 / 05

Greedy PrinciplesWhen greedy is correct

The greedy-choice property and exchange arguments.

Used for: Deciding whether a problem can be greedy; if not, reach for DP

Time complexity
Space complexity
DifficultyIntermediate
PrerequisitesBig-O Notation

01Why it exists

Will taking the best option right now come back to bite you?

Scheduling meetings, making change, compressing a file, planning a route — a great many problems have an obvious "take whatever looks best right now" approach. It runs fast and the code is short, but sometimes it returns the wrong answer, and it does so quietly: no exception, no warning.

Why this fitsGreedy is not one algorithm, it is a way of making decisions. What this lesson teaches is the test: when does "locally best" add up to "globally best", and when does it not? Judge it right and you get the fastest solution available; judge it wrong and you move to DP or to search.

The first fork in an interview question

Faced with an optimisation problem, you have a couple of minutes to decide between greedy and DP. Take the wrong turn and either you finish the greedy solution only to be handed a counterexample, or you spend half the interview on a DP when a single sort would have done.

Why this fitsThere is a fixed routine: guess a greedy rule, look for a counterexample, and if none turns up, try to prove the rule with an exchange argument. This lesson walks through that routine once, and every greedy solution in the lessons that follow is confirmed the same way.

The data is too big for a DP table

DP is guaranteed correct, but the number of states is often O(n²) or worse. With millions of inputs, neither the memory nor the time holds up.

Why this fitsA problem whose greedy rule can be proved correct usually needs nothing more than a sort and a single scan: O(n log n) time and O(1) extra space. That is why greedy is worth learning — it is the cheapest optimisation method there is, as long as you know when it applies.

Reach for it when you see:Take the largest / smallest / earliest at every step, sort then scan once, never look back, locally optimal, exchange argument, counterexample.

02The core idea

The shape of a greedy algorithm never varies: sort the candidates by some rule, look at them one at a time, take whatever fits, and never revisit a decision. It never backtracks and never compares alternative selections, so it usually costs no more than O(n log n) for the sort plus O(n) for the scan. The price of that speed is that it is not always right, and whether a particular greedy rule is right depends on whether the problem has two properties.

The first is the greedy choice property: some optimal solution starts with the same move greedy makes. The second is optimal substructure: what remains after that first move is a problem of the same shape but smaller, and its optimal solution, with the first move attached, is optimal for the original problem. When both hold, every greedy step keeps the partial answer consistent with some optimal solution, so running all the way to the end produces one.

The standard tool for proving the greedy choice property is the exchange argument. Take any optimal solution O and find the first place where it differs from the greedy solution G; replace O's choice there with G's, and argue that O is still valid afterwards and no worse than before. That shows an optimal solution consistent with greedy exists. In interval scheduling, greedy takes the earliest-finishing interval G₁, and the first interval to finish in any optimal solution, O₁, cannot finish before G₁ does, so swapping O₁ for G₁ still leaves room for everything after it. Going the other way, both "earliest start" and "shortest" have counterexamples: an exchange there may have to give back two intervals or more, and the argument cannot be completed.

In practice the order of work is: guess a rule, hunt for a counterexample on small inputs (by hand, or against a brute-force solution), and switch rules or switch to DP the moment you find one; only when nothing turns up do you attempt the exchange argument. A common trap is treating "it passed a few test cases" as a proof, since a greedy rule usually fails only on particular inputs — [1, 3, 4] in the making-change problem is the classic example. Another is thinking of greedy and DP as opposites: greedy is really a special case of DP, and when the best transition out of every state can be decided without looking at the alternatives, DP degenerates into greedy.

03The algorithm

  1. 1Write the problem as a sequence of choices, where each choice leaves behind a smaller problem of the same shape.
  2. 2Guess a sort key (earliest finish, smallest, best ratio, …), sort by it, scan through, and take whatever fits.
  3. 3Hunt for a counterexample on small inputs: work 5 to 10 elements through by hand, or write a brute-force solution and compare on n ≤ 15. One counterexample means a different key, or no greedy at all.
  4. 4If nothing turns up, build an exchange argument: take any optimal solution O, replace its first choice that differs from the greedy one with the greedy choice, and argue that the result is still valid and no worse.
  5. 5Confirm the optimal substructure: after the first choice, what is left has the same shape. With both properties in hand the greedy answer is optimal, and the cost is usually O(n log n).

04Interactive demo

The same seven intervals under three greedy rules, stepping through what each rule takes and what it skips. The optimum is four. The panel at the bottom right explains why the rule works or why it fails: the correct one gets an exchange argument, and each wrong one gets a concrete counterexample.

Greedy rule
7 intervals · optimum is 4
Earliest finishSort by finish time, then take every interval that does not clash with the ones already chosen
024681012141618A012B15S47C610D1114E1316F1518
Result
Greedy 0 / optimum 4
Blue intervals were chosen and dashed ones were skipped. Rules that process intervals in time order also draw an amber line at the finish time of the last chosen interval. At the end, green labels mark the optimal solution.
Why it works

Exchange argument: let O₁ be the earliest-finishing interval of any optimal solution, and let G₁ be the first interval the greedy rule takes. G₁ finishes earliest of all intervals, so its finish time is ≤ that of O₁. Swap O₁ for G₁: every other interval in the solution starts after O₁ ends, so nothing clashes and the count is unchanged. An optimal solution beginning with G₁ therefore exists, and repeating the argument on the remaining intervals shows that every greedy choice agrees with some optimal solution.

Step 0/8Strategy "Earliest finish": Sort by finish time, then take every interval that does not clash with the ones already chosen. The sorted order is B → S → C → A → D → E → F.

05Code

A generic sort-then-scan skeleton with three interchangeable sort keys, plus a brute-force solution to check against. This is exactly how you hunt for counterexamples: run greedy and brute force side by side on small inputs. So that all three rules can share it, the skeleton compares every candidate against all the intervals chosen so far; once you have committed to sorting by finish time, comparing against the last chosen one is enough, and the Interval Scheduling lesson writes it that way in O(n log n).

# Interval scheduling: one skeleton, three sort keys, and only "earliest finish" is correct
def schedule(intervals, key):
    """intervals is a list of (start, end); key decides the greedy order.
    Returns the chosen intervals, none of which overlap (touching endpoints are allowed)."""
    chosen = []
    for s, e in sorted(intervals, key=key):
        # Only take an interval that clashes with none of the chosen ones. Comparing against the
        # last finish time alone is not enough: sorting by length can hand us an earlier interval later
        if all(e <= cs or s >= ce for cs, ce in chosen):
            chosen.append((s, e))
    return chosen


def by_start(iv):                             # earliest start: wrong
    return iv[0]

def by_length(iv):                            # shortest: wrong
    return (iv[1] - iv[0], iv[0])

def by_end(iv):                               # earliest finish: correct
    return iv[1]


# Brute force: enumerate every subset on small inputs and see whether greedy reached the optimum
# Run this first when no exchange argument comes to mind; a counterexample saves you the proof
from itertools import combinations

def brute_force(intervals):
    n = len(intervals)
    for r in range(n, 0, -1):                 # largest sizes first, so the first feasible one is optimal
        for subset in combinations(intervals, r):
            ok = all(a[1] <= b[0] or b[1] <= a[0] for a, b in combinations(subset, 2))
            if ok:
                return r
    return 0


if __name__ == "__main__":
    ivs = [(0, 12), (1, 5), (4, 7), (6, 10), (11, 14), (13, 16), (15, 18)]
    print(brute_force(ivs))                   # 4
    for name, key in [("start", by_start), ("length", by_length), ("end", by_end)]:
        print(name, len(schedule(ivs, key)))  # start 2, length 3, end 4

06Practice

  • LeetCode 455Assign Cookies (sort, then two pointers)Easy
  • LeetCode 1029Two City Scheduling (sort by the difference)Medium
  • LeetCode 763Partition LabelsMedium
  • LeetCode 406Queue Reconstruction by HeightMedium
  • LeetCode 621Task SchedulerMedium
  • LeetCode 135Candy (two greedy passes)Hard