CMPUT-204 · Lesson 11 of 12 · 25 min

Greedy algorithms

A greedy algorithm builds a solution one piece at a time, always taking the choice that looks best right now and never reconsidering it. Dijkstra's, Kruskal's and Prim's algorithms are all greedy, and their correctness depended on careful arguments. This lesson studies the greedy method in general: when it works, how to prove that it works, and how to recognize problems where it fails. The examples, scheduling activities, assigning rooms, meeting deadlines, compressing data with Huffman codes and making change, are each solved by a short algorithm whose real content is the proof.

What makes a problem greedy-friendly

Two properties together make a greedy algorithm correct. The greedy-choice property says that some optimal solution begins with the greedy choice, so committing to it never rules out an optimum. Optimal substructure says that after making the choice, what remains is a smaller instance of the same problem, and an optimal solution to it combines with the choice to give an optimal solution overall. Many tempting greedy rules lack the first property, so the honest workflow is: propose a rule, look hard for a counterexample, and if none appears, prove the rule correct.

Two proof styles cover most greedy algorithms. In a greedy-stays-ahead proof, you show that after each step the greedy partial solution is at least as good as any other algorithm's partial solution after the same number of steps, measured in the right way. In an exchange argument, you take any optimal solution and transform it step by step into the greedy solution, showing that each exchange keeps it optimal. You saw an exchange argument in lesson 10's proof of the cut property.

Activity selection

Given \(n\) activities, each with a start time \(s_i\) and a finish time \(f_i\), select as many as possible so that no two overlap. Think of booking the most talks into a single lecture hall. Several greedy rules come to mind: choose the shortest activity, the one that starts earliest, the one with the fewest conflicts, or the one that finishes earliest. Only the last rule always works.

A timeline from 0 to 12 with eight activities drawn as horizontal bars. The four activities chosen by the earliest-finish rule are solid; the others are pale.
Earliest-finish-first selects a maximum set of compatible activities
Candidate greedy rules for activity selection
Greedy ruleCounterexample ideaCorrect?
Shortest activity firstA short activity overlaps two long, compatible onesNo
Earliest start firstOne long activity that starts first blocks many short onesNo
Fewest conflicts firstCarefully built instances defeat itNo
Earliest finish firstNone exists; provably optimalYes

Here is a greedy-stays-ahead proof for earliest finish first. Let the greedy choices, in order, finish at times \(g_1 \le g_2 \le \cdots \le g_k\), and let any optimal solution, sorted by finish time, finish at \(o_1 \le o_2 \le \cdots \le o_j\). Claim: \(g_r \le o_r\) for every \(r \le k\). For \(r = 1\) this holds because greedy picks the earliest finish of all. If \(g_r \le o_r\), then the optimal solution's \((r+1)\)-th activity starts after \(o_r \ge g_r\), so it was available to greedy, and greedy chose something finishing no later: \(g_{r+1} \le o_{r+1}\). If the optimum had more activities, \(j > k\), its activity \(k + 1\) would start after \(o_k \ge g_k\) and greedy would not have stopped. So \(k = j\), and greedy is optimal. Sorting by finish time costs \(\Theta(n \log n)\) and the selection is a single linear scan.

Testing three greedy rules against brute force on random instances
from itertools import combinations
import random

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

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

def best(acts):                              # brute force over subsets, small n only
    for r in range(len(acts), 0, -1):
        for sel in combinations(acts, r):
            if compatible(sel):
                return r
    return 0

rules = {'earliest finish': lambda a: a[1], 'earliest start': lambda a: a[0],
         'shortest': lambda a: a[1] - a[0]}
random.seed(11)
fails = {name: 0 for name in rules}
for trial in range(300):
    acts = []
    for _ in range(9):
        s = random.randint(0, 20); acts.append((s, s + random.randint(1, 8)))
    opt = best(acts)
    for name, key in rules.items():
        if len(greedy(acts, key)) < opt:
            fails[name] += 1
print('instances where the rule is not optimal (out of 300):', fails)
Output
instances where the rule is not optimal (out of 300): {'earliest finish': 0, 'earliest start': 118, 'shortest': 282}

Random testing finds failures of the earliest-start and shortest-first rules quickly and never finds one for earliest finish, which matches the proof. Testing like this is a cheap way to kill bad ideas; it cannot prove a good one, which is why the argument above matters.

Interval partitioning

A related problem asks for the minimum number of rooms needed to schedule all the activities, with no overlaps within a room. The depth of a set of intervals, the largest number that overlap at any single time, is an obvious lower bound: those activities need different rooms. A greedy algorithm meets the bound. Process activities in order of start time, and put each one in any room that has become free, opening a new room only when every existing room is busy. A min-heap of room finish times finds a free room in \(O(\log n)\), so the algorithm runs in \(O(n \log n)\). Whenever it opens room \(d\), the \(d - 1\) other rooms are all busy at that start time, so at least \(d\) activities overlap; the number of rooms never exceeds the depth.

Interval partitioning with a heap of room finish times
import heapq

def rooms_needed(acts):
    ends = []                               # min-heap of finish times, one per room
    for s, f in sorted(acts):               # by start time
        if ends and ends[0] <= s:
            heapq.heapreplace(ends, f)      # reuse the room that frees up earliest
        else:
            heapq.heappush(ends, f)         # open a new room
    return len(ends)

def depth(acts):
    events = sorted([(s, 1) for s, f in acts] + [(f, -1) for s, f in acts])
    cur = best = 0
    for t, d in events:                     # finishes (-1) sort before starts at equal times
        cur += d; best = max(best, cur)
    return best

talks = [(9, 10.5), (9, 12.5), (9, 10.5), (11, 12.5), (11, 14), (13, 14.5), (13, 14.5), (14, 16.5), (15, 16.5), (15, 16.5)]
print('rooms used by greedy:', rooms_needed(talks), ' depth:', depth(talks))
Output
rooms used by greedy: 3  depth: 3

Scheduling to minimize maximum lateness

Now suppose one machine must process \(n\) jobs, each with a processing time \(t_i\) and a deadline \(d_i\). If job \(i\) finishes at time \(C_i\), its lateness is \(\max(0, C_i - d_i)\), and we want to minimize the largest lateness. The greedy rule is earliest deadline first: sort the jobs by deadline and run them back to back. The proof is an exchange argument. Call a pair of jobs an inversion if the one with the later deadline is scheduled first. Any schedule with an inversion has an adjacent inversion, and swapping that adjacent pair never increases the maximum lateness: the job that moves later now finishes when the other used to, but its deadline is later, so its lateness is no worse than the other job's was. Repeatedly swapping removes all inversions without increasing the maximum lateness, turning any optimal schedule into the greedy one.

Worked problem

Earliest deadline first

Jobs (time, deadline): A (3, 6), B (2, 8), C (1, 9), D (4, 9), E (3, 14), F (2, 15). Schedule by earliest deadline and find the maximum lateness.

Show solution
  1. Order by deadline: A, B, then C and D (both 9), E, F.
  2. Finish times: A 3, B 5, C 6, D 10, E 13, F 15.
  3. Lateness: A 0, B 0, C 0, D 10 − 9 = 1, E 0, F 0.
  4. Maximum lateness is 1, and no schedule can do better.

Answer: Maximum lateness 1, achieved by the order A, B, C, D, E, F.

Huffman coding

Text is usually stored with a fixed number of bits per character, but if some characters are much more common than others, a variable-length code saves space by giving common characters short codewords. To decode unambiguously, the code must be a prefix code: no codeword is a prefix of another. A prefix code corresponds to a binary tree whose leaves are the characters, with each left branch read as 0 and each right branch as 1. The cost of the code is the average codeword length, \(\sum_c p(c) \cdot \text{depth}(c)\), where \(p(c)\) is the frequency of character \(c\).

David Huffman's 1952 algorithm, devised as a student, builds an optimal tree greedily. Start with each character as a one-node tree weighted by its frequency. Repeatedly remove the two lightest trees from a priority queue, join them under a new root whose weight is their sum, and insert the result. When one tree remains, it is an optimal prefix code. The exchange argument shows that some optimal tree has the two least frequent characters as sibling leaves at the deepest level; merging them reduces the problem to a smaller one with the same structure. With a heap, Huffman's algorithm takes \(O(k \log k)\) for an alphabet of \(k\) characters.

A Huffman tree for the letters a, b, c, d, e and f with frequencies 45, 13, 12, 16, 9 and 5. Leaf a hangs from the root with code 0; the others sit deeper, with codes such as 100 for c and 1100 for f.
A Huffman tree; frequent letters get short codes
Building a Huffman code with a heap
import heapq
from itertools import count

def huffman(freq):
    tick = count()                           # tie-breaker keeps heap entries comparable
    heap = [(f, next(tick), ch) for ch, f in sorted(freq.items())]
    heapq.heapify(heap)
    while len(heap) > 1:
        f1, _, a = heapq.heappop(heap)
        f2, _, b = heapq.heappop(heap)
        heapq.heappush(heap, (f1 + f2, next(tick), (a, b)))
    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], '')
    return codes

freq = {'a': 45, 'b': 13, 'c': 12, 'd': 16, 'e': 9, 'f': 5}      # per 100 characters
codes = huffman(freq)
for ch in sorted(codes):
    print(ch, freq[ch], codes[ch])
avg = sum(freq[c] * len(codes[c]) for c in freq) / sum(freq.values())
print('average bits per character:', avg, ' fixed-length code needs 3')
Output
a 45 0
b 13 101
c 12 100
d 16 111
e 9 1101
f 5 1100
average bits per character: 2.24  fixed-length code needs 3

With these frequencies, the Huffman code averages 2.24 bits per character instead of 3, a 25 percent saving. Huffman coding is a stage in DEFLATE, the method inside ZIP files and PNG images, and in JPEG and MP3 compression, usually combined with other techniques.

When greedy fails

Greedy algorithms fail whenever an early commitment can block a better overall solution. Two classic examples make the point. In the fractional knapsack problem, a thief may take fractions of items, each with a weight and a value, up to a weight limit; taking items in decreasing order of value per kilogram is optimal. In the 0/1 knapsack problem, items must be taken whole, and the same rule can fail badly. With capacity 50 and items of weight and value (10, 60), (20, 100) and (30, 120), the ratio rule takes the first two for a value of 160, but the last two give 220. The 0/1 knapsack problem needs dynamic programming, in CMPUT-304.

Making change is the other standard example. With Canadian or US coins of 1, 5, 10 and 25 cents, giving the largest coin that fits at each step always uses the fewest coins. But with coins of 1, 3 and 4, the greedy method makes 6 as 4 + 1 + 1, three coins, while 3 + 3 uses two. Whether greedy works depends on the coin system, and coin systems where it works are called canonical. A greedy algorithm is never correct just because it seems natural.

Greedy change-making is optimal for some coin systems and not others
from functools import lru_cache

def greedy_change(amount, coins):
    used = []
    for c in sorted(coins, reverse=True):
        while amount >= c:
            amount -= c; used.append(c)
    return used

def optimal_count(amount, coins):
    @lru_cache(maxsize=None)
    def best(a):
        if a == 0:
            return 0
        return 1 + min(best(a - c) for c in coins if c <= a)
    return best(amount)

for coins in ((1, 5, 10, 25), (1, 3, 4)):
    bad = [a for a in range(1, 100) if len(greedy_change(a, coins)) > optimal_count(a, coins)]
    print(coins, 'amounts below 100 where greedy is not optimal:', len(bad), bad[:5])
print('greedy for 6 with (1, 3, 4):', greedy_change(6, (1, 3, 4)))
Output
(1, 5, 10, 25) amounts below 100 where greedy is not optimal: 0 []
(1, 3, 4) amounts below 100 where greedy is not optimal: 24 [6, 10, 14, 18, 22]
greedy for 6 with (1, 3, 4): [4, 1, 1]

Common mistakes

  • MistakeTrusting a greedy rule because it works on a few examples.

    InsteadSearch for counterexamples, then prove the rule with a stays-ahead or exchange argument.

  • MistakeSelecting activities by shortest duration or earliest start.

    InsteadBoth fail on simple instances; earliest finish time is the provably optimal rule.

  • MistakeAssuming greedy change-making works for every coin system.

    InsteadIt works for canonical systems such as 1, 5, 10, 25 but fails for 1, 3, 4.

  • MistakeUsing the value-per-weight rule for 0/1 knapsack.

    InsteadIt is optimal only for the fractional version; 0/1 knapsack needs dynamic programming.

  • MistakeBuilding a variable-length code that is not prefix-free.

    InsteadWithout the prefix property, a bit string can decode in more than one way; use a code tree.

Summary and key terms

  • A greedy algorithm makes the locally best choice at each step and never revisits it.
  • Greedy is correct when the greedy-choice property and optimal substructure both hold.
  • Greedy-stays-ahead and exchange arguments are the two main ways to prove a greedy algorithm optimal.
  • Earliest finish first maximizes the number of compatible activities, in Θ(n log n).
  • Assigning intervals to rooms in start order uses exactly as many rooms as the maximum overlap.
  • Earliest deadline first minimizes maximum lateness, by an exchange argument on inversions.
  • Huffman coding repeatedly merges the two least frequent trees to build an optimal prefix code.
Greedy algorithm
An algorithm that builds a solution by repeatedly making the best immediate choice.
Greedy-choice property
Some optimal solution contains the first greedy choice.
Greedy stays ahead
A proof that the greedy partial solution is never worse than any other after each step.
Exchange argument
A proof that transforms an optimal solution into the greedy one without making it worse.
Activity selection
Choosing the maximum number of non-overlapping intervals.
Depth
The maximum number of intervals overlapping at any single time.
Lateness
How far a job finishes after its deadline, or zero if it is on time.
Prefix code
A code in which no codeword is a prefix of another.
Huffman code
An optimal prefix code built by merging the two least frequent trees repeatedly.
Canonical coin system
A coin system for which greedy change-making is always optimal.

Practice problems

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

  1. Activities (1, 4), (3, 5), (0, 6), (5, 7), (8, 9), (5, 9). Which does earliest finish first select?

    Show answer

    (1, 4), (5, 7), (8, 9): three activities.

  2. Give a counterexample to shortest-first activity selection.

    Show answer

    (0, 5), (4, 6), (5, 10): shortest-first picks (4, 6) alone, but (0, 5) and (5, 10) are compatible.

  3. Intervals (0, 3), (1, 4), (2, 5), (4, 6). What is the depth, and how many rooms are needed?

    Show answer

    Depth 3 at time 2.5; 3 rooms.

  4. Why does swapping an adjacent inversion never increase maximum lateness?

    Show answer

    The job moved later now finishes where the earlier-deadline job did, but its own deadline is later, so its lateness is at most the other job's old lateness.

  5. Is {0, 01, 11} a prefix code?

    Show answer

    No: 0 is a prefix of 01.

  6. Huffman merges frequencies 5, 9, 12, 13. What are the first two merges?

    Show answer

    5 + 9 = 14, then 12 + 13 = 25.

  7. Fractional knapsack, capacity 50, items (10, 60), (20, 100), (30, 120). Optimal value?

    Show answer

    60 + 100 + (20/30) × 120 = 240.

  8. Coins 1, 6, 10: does greedy make 12 optimally?

    Show answer

    No: greedy gives 10 + 1 + 1 (three coins), but 6 + 6 uses two.

  9. What is the running time of activity selection including sorting?

    Show answer

    Θ(n log n).

  10. Name a greedy algorithm from earlier lessons and the property its proof used.

    Show answer

    Kruskal or Prim (cut property) or Dijkstra (the closest unfinished vertex is exact with non-negative weights).

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz