CMP-301 · Lesson 2 of 12 · 20 min

Greedy algorithms and exchange arguments

A greedy algorithm builds a solution one piece at a time, always taking the choice that looks best right now and never reconsidering it. Greedy algorithms are fast, simple and easy to implement, which makes them the first thing an engineer tries. Their danger is that they are often wrong: a choice that looks best locally can block a better solution later. The skill this lesson teaches is telling the two cases apart, by finding either a proof that the greedy choice is always safe or a small counterexample showing it is not.

Jonah's first real assignment is the upload window. Each NS-4 node can transmit its daily batch only while its modem holds a cellular connection, and the server's ingest process can handle one upload stream at a time on the cheap off-peak link. Each node requests a window, a start time and a finish time, during which it can upload. Dana asks how many requests can be accepted on that single link without overlaps. This is the interval scheduling problem, and it is the cleanest place to see how greedy reasoning works.

Interval scheduling

The input is a set of n intervals, each with a start time s and a finish time f. Two intervals are compatible if they do not overlap. The goal is to choose as many mutually compatible intervals as possible. Several greedy rules suggest themselves: take the interval that starts first, the shortest one, the one that conflicts with the fewest others, or the one that finishes first. Only one of them is always optimal, and small examples rule out the others quickly.

Eight horizontal bars on a time axis from 0 to 12, labeled a to h. The earliest-finish greedy rule picks interval b ending at 3, then e ending at 6, then h ending at 10, shown in green. The remaining intervals are gray and each overlaps a chosen one.
Earliest finish time first: after each choice, discard everything that overlaps it and repeat. Here it selects three compatible windows, the maximum possible.

Earliest start fails when one long interval starts first and blocks many short ones. Shortest first fails when a short interval sits across the boundary of two longer compatible intervals. Fewest conflicts fails on a more elaborate example that is worth constructing once by hand. Earliest finish time works, and the intuition is that finishing early leaves the most room for everything else. Intuition is not proof, so here is the proof.

The exchange argument

Sort the intervals by finish time and let the greedy algorithm choose g₁, g₂, …, g_k, always taking the next interval that starts no earlier than the last chosen one finishes. Let o₁, o₂, …, o_m be any optimal solution, also listed in order of finish time. The claim is that greedy stays ahead: for every r ≤ k, the r-th greedy interval finishes no later than the r-th optimal interval.

\[f(g_r) \le f(o_r) \quad \text{for all } r = 1, \dots, k\]

The proof is by induction. For r = 1, greedy picks the interval with the earliest finish time of all, so f(g₁) ≤ f(o₁). Suppose f(g_{r−1}) ≤ f(o_{r−1}). The optimal interval o_r starts after o_{r−1} finishes, hence after g_{r−1} finishes, so o_r was available to greedy at step r. Greedy picked the available interval with the earliest finish, so f(g_r) ≤ f(o_r). Now suppose greedy chose fewer intervals than optimal, k < m. Then o_{k+1} starts after o_k finishes, which is after g_k finishes, so it was still available and greedy would not have stopped. Therefore k = m and greedy is optimal.

This style of proof, showing that greedy is never behind any optimal solution at any step, is called a stays-ahead argument. The closely related exchange argument takes an optimal solution that differs from greedy's and swaps one of its choices for greedy's choice without making it worse, repeating until the two agree. Both establish the same thing: making the greedy choice never rules out an optimal solution.

Greedy agrees with exhaustive search on the upload windows from the figure.
# Interval scheduling: earliest finish time first, checked against brute force
from itertools import combinations

def greedy(intervals):
    chosen, last_end = [], float('-inf')
    for s, f, name in sorted(intervals, key=lambda iv: iv[1]):
        if s >= last_end:
            chosen.append(name); last_end = f
    return chosen

def compatible(ivs):
    ivs = sorted(ivs, key=lambda iv: iv[0])
    return all(ivs[i][1] <= ivs[i + 1][0] for i in range(len(ivs) - 1))

def brute(intervals):
    for k in range(len(intervals), 0, -1):
        for sub in combinations(intervals, k):
            if compatible(sub):
                return k
    return 0

windows = [(0, 6, 'a'), (1, 3, 'b'), (2, 8, 'c'), (3, 6, 'e'),
           (4, 7, 'd'), (5, 9, 'f'), (6, 10, 'h'), (8, 11, 'g')]
g = greedy(windows)
print("greedy picks:", g, "count", len(g))
print("brute-force optimum:", brute(windows))
Output
greedy picks: ['b', 'e', 'h'] count 3
brute-force optimum: 3

The algorithm costs O(n log n) for the sort and O(n) for the scan. Dana's 400 requests take microseconds. The brute force, which tries subsets from largest to smallest, is fine for eight intervals and hopeless for forty, but as lesson 1 recommended it makes an excellent test oracle.

Scheduling to minimize lateness

A second scheduling problem shows a different greedy rule and a cleaner exchange argument. The server must run a set of report jobs one after another on a single worker. Job j takes t_j minutes and has a deadline d_j. If a job finishes at time f_j, its lateness is max(0, f_j − d_j). The goal is to order the jobs so that the largest lateness is as small as possible. The rule that works is earliest deadline first: sort by deadline and run the jobs in that order with no idle time.

The exchange argument goes like this. An inversion in a schedule is a pair of jobs where the one with the later deadline runs first. Any schedule with an inversion has an adjacent inversion, two consecutive jobs i then j with d_i > d_j. Swapping them does not increase the maximum lateness: job j finishes earlier, so its lateness cannot grow, and job i now finishes when j used to finish, at a time f, with lateness f − d_i, which is less than j's old lateness f − d_j because d_i > d_j. Each swap removes one inversion, so after finitely many swaps any optimal schedule becomes the earliest-deadline schedule without getting worse.

Worked problem

Ordering the overnight report jobs

Four report jobs have durations and deadlines (minutes after midnight): A takes 30 with deadline 60, B takes 20 with deadline 40, C takes 50 with deadline 140, and D takes 40 with deadline 90. Find the earliest-deadline schedule and its maximum lateness.

Show solution
  1. Sort by deadline: B (40), A (60), D (90), C (140).
  2. Finish times: B at 20, A at 50, D at 90, C at 140.
  3. Lateness: B max(0, 20 − 40) = 0; A max(0, 50 − 60) = 0; D 90 − 90 = 0; C 140 − 140 = 0.
  4. Maximum lateness is 0, so every report is on time.

Answer: Running the jobs in the order they were submitted, A, B, C, D, would finish B at 50 and D at 140, making D 50 minutes late. The greedy order wins without any search.

Huffman coding

Greedy ideas also build data structures. Each NS-4 record includes a vehicle class code, one of eight classes from motorcycle to multi-unit truck, and a fixed 3-bit code wastes space because most vehicles are passenger cars. A prefix code assigns shorter bit strings to common symbols and longer ones to rare symbols, with no codeword a prefix of another, so the bit stream can be decoded without separators. Huffman's algorithm finds the prefix code with the smallest average length.

The algorithm keeps a priority queue of trees, initially one single-node tree per symbol weighted by its frequency. It repeatedly removes the two lightest trees, joins them under a new root whose weight is their sum, and puts the result back. After n − 1 merges one tree remains; reading 0 for each left branch and 1 for each right branch from the root to a leaf gives that symbol's codeword. With a binary heap the algorithm runs in O(n log n).

A Huffman tree for five vehicle classes. Leaves are car 0.62, light truck 0.18, bus 0.04, motorcycle 0.03 and heavy truck 0.13. Motorcycle and bus merge into 0.07, which merges with heavy truck into 0.20, which merges with light truck into 0.38, which merges with car at the root of weight 1.00. Edges are labeled 0 and 1.
Huffman merges the two lightest trees at each step. The common class, car, gets a one-bit code; rare classes sit deeper.

Why is merging the two rarest symbols safe? In an optimal prefix code tree, the two deepest leaves are siblings, and swapping the rarest symbols into those positions cannot increase the average length because they move deeper only by displacing symbols that are at least as frequent. So some optimal tree has the two rarest symbols as siblings, merging them reduces the problem to one with n − 1 symbols, and induction finishes the proof.

Huffman codes for the vehicle class field, with the average length.
# Huffman coding for vehicle class codes (frequencies from a week of counts)
import heapq

freq = {'car': 0.62, 'light_truck': 0.18, 'heavy_truck': 0.13,
        'bus': 0.04, 'motorcycle': 0.03}

heap = [(w, i, sym) for i, (sym, w) in enumerate(sorted(freq.items()))]
heapq.heapify(heap)
counter = len(heap)
while len(heap) > 1:
    w1, _, a = heapq.heappop(heap)
    w2, _, b = heapq.heappop(heap)
    heapq.heappush(heap, (w1 + w2, counter, (a, b))); counter += 1

codes = {}
def walk(node, prefix):
    if isinstance(node, str):
        codes[node] = prefix or '0'
    else:
        walk(node[0], prefix + '0'); walk(node[1], prefix + '1')
walk(heap[0][2], '')

avg = sum(freq[s] * len(c) for s, c in codes.items())
for s in sorted(codes, key=lambda s: -freq[s]):
    print(f"{s:12s} {freq[s]:.2f}  {codes[s]}")
print(f"average bits per record: {avg:.2f} (fixed-length code: 3)")
Output
car          0.62  1
light_truck  0.18  00
heavy_truck  0.13  011
bus          0.04  0101
motorcycle   0.03  0100
average bits per record: 1.65 (fixed-length code: 3)

The average of about 1.7 bits replaces 3 bits per record, which across 400 nodes and roughly 6,000 records per node per day saves about three million bits daily before any other compression. The real saving is larger because Maren's firmware also run-length encodes the empty intervals overnight, but Huffman's idea, that rare symbols should pay for the brevity of common ones, is the core of most compression formats, including the deflate algorithm used in compressed archives.

When greedy fails

The same habit that makes greedy attractive, never looking back, makes it wrong on many problems. Making change is the classic example. With coins of 1, 5, 10 and 25 cents, taking the largest coin that fits always uses the fewest coins. With coins of 1, 3 and 4 units, making 6 greedily gives 4 + 1 + 1, three coins, while 3 + 3 uses two. Whether greedy works depends on the coin system, and for an arbitrary system the correct method is the dynamic program of lesson 4.

A more relevant failure for Northway is the 0/1 knapsack problem. Maren has 64 KB of spare flash for optional firmware features, each with a size and a value to customers. Taking features in order of value per kilobyte is the natural greedy rule, and it can be badly wrong when a large valuable feature does not fit after smaller ones have been taken. If features could be split into fractions, the fractional knapsack, the greedy rule would be optimal; because they cannot, lesson 4 solves the problem with dynamic programming.

Where greedy works and where it does not.
ProblemGreedy ruleOptimal?Reason
Interval schedulingEarliest finishYesStays ahead of any optimal solution
Minimize maximum latenessEarliest deadlineYesSwapping inversions never hurts
Prefix codesMerge two rarest (Huffman)YesRarest symbols can be siblings in an optimal tree
Minimum spanning treeLightest safe edge (Kruskal, Prim)YesCut property, see CMPUT-204
Change with 1, 3, 4Largest coinNo6 = 4 + 1 + 1 but 3 + 3 is better
0/1 knapsackBest value per unit sizeNoIndivisible items block better combinations
Weighted interval schedulingEarliest finishNoA heavy interval may be worth several light ones

A recipe for greedy design

A dependable process for greedy algorithms has four steps. Cast the problem as a sequence of choices. Propose a greedy rule and immediately hunt for a small counterexample, because a counterexample found in five minutes saves a week. If none appears, prove the rule safe with a stays-ahead or exchange argument. Finally, implement with the right data structure, usually a sort or a priority queue, and test against brute force. When the counterexample hunt succeeds, the structure of the counterexample often points to the correct paradigm: if the failure comes from an early choice that should have depended on later ones, think dynamic programming.

Common mistakes

  • MistakeAssuming a greedy rule is optimal because it works on the examples tried.

    InsteadSearch deliberately for a counterexample with two or three items, then prove the rule with an exchange or stays-ahead argument.

  • MistakeUsing earliest finish time for weighted interval scheduling.

    InsteadWeights break the greedy rule; one long valuable interval can beat several short cheap ones. Use the dynamic program in lesson 4.

  • MistakeBuilding Huffman codes by sorting once and merging in sorted order.

    InsteadMerged trees must re-enter the queue at their new weight. Use a priority queue so each merge takes the two lightest current trees.

  • MistakeConfusing the fractional and 0/1 knapsack problems.

    InsteadGreedy by value per unit is optimal only when items can be split. Indivisible items need dynamic programming.

Summary and key terms

  • A greedy algorithm makes the locally best choice at each step and never undoes it.
  • Earliest finish time solves interval scheduling; the proof shows greedy stays ahead of any optimal solution.
  • Earliest deadline first minimizes maximum lateness; swapping adjacent inversions never increases lateness.
  • Huffman's algorithm builds optimal prefix codes by repeatedly merging the two lightest trees.
  • Greedy fails for change-making with unusual coins, 0/1 knapsack and weighted interval scheduling.
  • Hunt for a small counterexample first; if none exists, prove the rule with an exchange argument.
Greedy algorithm
An algorithm that builds a solution by repeatedly making the locally best choice.
Interval scheduling
Choosing the largest set of non-overlapping intervals.
Stays-ahead argument
A proof that greedy is never behind an optimal solution at any step.
Inversion
A pair of jobs scheduled in the opposite order to their deadlines.
Prefix code
A code in which no codeword is the beginning of another, so it decodes without separators.
Huffman coding
A greedy algorithm that builds a prefix code of minimum average length from symbol frequencies.
Fractional knapsack
A knapsack problem where items may be split, solved optimally by taking the best value per unit first.

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. Give a counterexample to shortest-interval-first for interval scheduling.

    Show answer

    Intervals [0, 5), [4, 6), [5, 10). Shortest first picks [4, 6), which overlaps both others, giving one interval; the optimum takes [0, 5) and [5, 10), two intervals.

  2. Jobs: X takes 10 with deadline 25, Y takes 15 with deadline 20. Which order minimizes maximum lateness, and what is it?

    Show answer

    Earliest deadline: Y then X. Y finishes at 15 (on time) and X at 25 (on time), so the maximum lateness is 0. X first would make Y finish at 25, 5 late.

  3. Symbols with frequencies 0.5, 0.25, 0.125, 0.125. What are the Huffman code lengths and the average?

    Show answer

    Lengths 1, 2, 3, 3; average 0.5(1) + 0.25(2) + 0.125(3) + 0.125(3) = 1.75 bits.

  4. Why is greedy optimal for fractional knapsack but not 0/1 knapsack?

    Show answer

    With fractions, any capacity left after the best-ratio items can be filled exactly with a fraction of the next item, so nothing is wasted. With whole items, a high-ratio choice can leave unusable space that a different combination would fill.

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz