Ch 24 / 30 Greedy Algorithms and When They Lie 0/0 exercises Exercises ↓

AmouAI Hub/Courses/Data Structures & Algorithms/Chapter 24

Part 6 · Design Paradigms · Chapter 24

Greedy Algorithms and When They Lie

Take the best-looking option right now and never look back. Sometimes provably optimal, sometimes disastrously wrong — and the skill is telling the two apart.

Reading
CLRS Ch. 16
Focus
Proving, or breaking, greed
Cost
usually O(n log n)
Needs
Sorting, heaps

By the end of this chapter you can

  1. State the greedy-choice property and distinguish it from optimal substructure
  2. Run an exchange argument to prove a greedy algorithm correct
  3. Solve interval scheduling and interval partitioning, and explain why the sort key differs
  4. Produce a counterexample that kills a plausible-looking greedy rule
  5. Build a Huffman code and explain why its greedy merge is optimal
  6. Decide, for a new problem, whether to reach for greedy, DP, or brute force

1What greedy actually means

Make the choice that looks best right now, commit to it, and never revisit it. No search, no backtracking, no table.

Climbing in fog

You want the highest point on the moor, and visibility is ten metres. The greedy strategy is: always step in the steepest upward direction. Never step down, never retrace.

On a single smooth hill this reaches the summit and does so quickly, which is the appeal. On a moor with two hills it reaches the top of whichever hill you happened to start on, declares victory, and has no way to discover the taller one behind you. It never learns it was wrong, because it never looks.

That is the whole trade. Greedy algorithms are fast and short because they refuse to consider alternatives — which is also precisely how they fail.

Formally, a greedy algorithm builds a solution one decision at a time, and each decision is made by a fixed local rule and never undone. Compare that with the other two paradigms in this part of the course:

ParadigmHow many options does it consider?Cost
Brute force / backtrackingAll of them, with pruningExponential
Dynamic programmingAll of them, but each subproblem oncePolynomial
GreedyExactly one, chosen by a ruleUsually one sort: O(n log n)

Greedy is by far the cheapest, and its algorithms are usually four or five lines. The catch is that correctness is not automatic. For DP, if your recurrence is right the answer is right. For greedy, you also need a proof that the local rule never destroys a global optimum — and without that proof you have a heuristic, not an algorithm.

The trap that gets everybody

Greedy algorithms are seductive because a plausible rule usually works on the examples you happen to try. It passes your three test cases, it looks elegant, and it is wrong on an input you have not thought of.

The professional habit is the opposite of the intuitive one: before you write a greedy algorithm, spend two minutes actively trying to break it. If you cannot break it, then try to prove it. Only then implement it.

2Two properties, and the exchange argument

A greedy algorithm is correct when two specific things are true. There is a standard proof technique for the harder one.

Greedy needs the same optimal substructure that dynamic programming needs: an optimal solution contains optimal solutions to its subproblems. But it needs one more thing that DP does not.

The greedy-choice property

There is an optimal solution that contains the greedy first choice.

Note how strong that is. DP asks only that you can combine subproblem answers. Greedy asks that you can commit to the first move immediately, before solving anything, and still be able to reach an optimum.

If both properties hold, induction finishes the job: the greedy choice is safe, what remains is a smaller instance of the same problem, and by induction greed solves that too.

The greedy-choice property is almost always proved by an exchange argument, which follows the same four steps every time:

exchange.pymemorise this shape
# The exchange argument, as a template
#
# 1. Let OPT be any optimal solution.
# 2. Suppose OPT does not make the greedy choice g. It makes some other choice o.
# 3. SWAP: replace o with g inside OPT.
#      - show the result is still VALID (does not break a constraint)
#      - show the result is NO WORSE (its value did not decrease)
# 4. So there is an optimal solution containing g. Recurse on what is left.
#
# If step 3 fails -- the swap breaks validity, or makes the answer worse --
# that failure usually IS your counterexample. Look at it closely.
Trading places in a queue

Imagine the optimal solution is a queue of choices, and greedy insists on putting a particular person first. Take the optimal queue and move that person to the front, sliding whoever was there back one place.

If you can show the queue is still legal and still just as good, then greedy's demand cost nothing — there was always an optimal answer that agreed with it. Repeat the argument for the second position, the third, and so on, and greedy's whole queue is optimal.

The proof never constructs the optimum. It only shows the optimum can always be nudged toward greed without getting worse, which is a much easier thing to demonstrate.

3Interval scheduling: greed done right

One room, many requests, each with a start and an end. Accept as many as possible. The right sort key is not the obvious one.

Three plausible rules, two of which are wrong:

RuleVerdictCounterexample
Earliest start timeWrongOne 9am–6pm booking blocks eight one-hour ones
Shortest durationWrongA short meeting in the middle can block two long ones either side
Fewest conflictsWrongHarder to break, but breakable — and slow
Earliest finish timeCorrect

Earliest finish wins because finishing early is exactly the thing that helps the future: it frees the room sooner and constrains nothing else.

activities.pyO(n log n), and the sort is all of it
def max_activities(intervals):
    """intervals: list of (start, end). Returns the largest non-overlapping subset."""
    chosen = []
    end_of_last = float("-inf")

    for s, e in sorted(intervals, key=lambda iv: iv[1]):   # by END time
        if s >= end_of_last:                               # no clash
            chosen.append((s, e))
            end_of_last = e

    return chosen
The proof, in three lines

Let g be the activity finishing earliest, and let OPT be an optimal schedule whose first activity is o ≠ g.

Swap o for g. Valid? Yes: g finishes no later than o (it finishes earliest of all), so it cannot clash with anything that followed o. Same size? Yes: one activity out, one in.

So an optimal schedule starting with g exists. Delete g and everything overlapping it, and the remainder is the same problem on fewer activities. Induction closes it.

Now change the question slightly and the sort key changes with it. Interval partitioning: schedule every request, using as few rooms as possible.

rooms.py
import heapq

def min_rooms(intervals):
    """Fewest rooms needed to hold every interval."""
    rooms = []                                   # heap of end times, one per room in use

    for s, e in sorted(intervals):               # by START time this time
        if rooms and rooms[0] <= s:              # the earliest-freeing room is free
            heapq.heapreplace(rooms, e)          # reuse it
        else:
            heapq.heappush(rooms, e)             # otherwise open a new room

    return len(rooms)
Why the answer cannot be beaten

Whenever this algorithm opens the k-th room, it is because k − 1 rooms are all busy at that exact moment — so k intervals genuinely overlap at that instant, and no schedule could use fewer than k rooms.

The greedy answer therefore equals a lower bound that holds for every possible answer, which is the other standard way to prove greed correct: match a lower bound, rather than exchange your way toward the optimum.

Note the two problems sort differently — by end time, by start time — even though both are “intervals, greedily”. The sort key is the algorithm. Getting it from the proof rather than from intuition is the entire discipline.

4Where greed lies

Three problems where the greedy rule is obvious, natural, and wrong.

Coin change. Make an amount with the fewest coins. Greedy takes the largest coin that fits, repeatedly. On British, American or Euro coins this is optimal — those systems were designed to make it so. Change the denominations and it collapses.

coins.pythe classic counterexample
# Coins {1, 3, 4}, target 6
#
# Greedy: take 4  ->  2 left
#         take 1  ->  1 left
#         take 1  ->  done.   THREE coins.
#
# Optimal:  3 + 3.            TWO coins.
#
# The 4 was locally the best move and globally the wrong one. Nothing in the
# greedy rule can ever discover that, because it never reconsiders the 4.

0/1 knapsack. Items have a value and a weight, the bag has a capacity, and each item is taken whole or not at all. The natural rule — take the best value-per-kilo first — is wrong:

knapsack_fail.py
# Capacity 10
#   item A: value 60, weight 10   -> 6.0 per kg
#   item B: value 55, weight  6   -> 9.2 per kg   <- best density
#   item C: value 20, weight  4   -> 5.0 per kg
#
# Greedy by density: B (55, 6kg), then C fits (20, 4kg) -> 75
# Optimal:           A alone                            -> 60?  no, 75 wins here.
#
# Now shrink C to value 5:
#   Greedy by density: B + C = 60.   Optimal: A alone = 60.  Still tied.
# And with capacity 10, item A (60, 10kg), item B (55, 6kg), item C (5, 5kg):
#   Greedy: B (6kg used), C does not fit in 4kg -> 55
#   Optimal: A -> 60.  Greedy loses.
The one-word fix that makes it work

Allow items to be split — the fractional knapsack — and greedy by density becomes provably optimal. You fill the bag with the densest material until it is full, and the last item is simply cut to fit.

The exchange argument works because any solution not using the densest available material can swap a kilo of something worse for a kilo of it, and improve. That swap is only possible because fractions are allowed. Indivisibility is what breaks greed, and it is what forces 0/1 knapsack into dynamic programming in Chapter 26.

Longest path. Repeatedly walk to the heaviest unvisited neighbour. This is wrong so immediately that it barely needs a counterexample — one fat edge leads into a dead end while a hundred thin ones would have led onward. Unlike the others, longest path has no efficient correct algorithm at all: it is NP-hard on a general graph. (On a DAG it is linear — Chapter 21.)

How to hunt a counterexample

When you suspect a greedy rule, try these in order. Most broken rules die to one of them:

  1. Make the greedy choice barely better. If greedy picks 5 over 4, it should also pick 5 over 4.999 — so build a case where that razor-thin gain costs you a large opportunity.
  2. Make it block two things. One item that consumes the room / weight / time that two better items would have shared.
  3. Try ties. Equal values often expose a rule that quietly depended on uniqueness.
  4. Go tiny. Three items is usually enough. If you need ten to break it, you are probably not breaking it.

5Huffman coding

The greedy algorithm you have used today without knowing it: it is inside zip, JPEG, MP3 and every HTTP response you receive.

Short names for frequent people

You are inventing nicknames for your colleagues, and the rule is that no nickname may be the start of another — otherwise “Al” and “Alex” are ambiguous when spoken quickly.

Obviously the person you mention fifty times a day should get the shortest nickname, and the one you mention twice a year can afford a long one. Huffman coding is that idea made exact: assign short bit-strings to frequent symbols, long ones to rare symbols, and guarantee that no code is a prefix of another.

The greedy step is beautifully counter-intuitive: instead of deciding the frequent symbols first, Huffman repeatedly grabs the two rarest symbols and merges them into one combined symbol.

huffman.pyO(n log n)
import heapq

def huffman_lengths(freq):
    """freq: {symbol: count}. Returns total bits of the optimal prefix code."""
    if len(freq) <= 1:
        return sum(freq.values())         # one symbol still needs one bit each

    heap = list(freq.values())
    heapq.heapify(heap)
    total = 0

    while len(heap) > 1:
        a = heapq.heappop(heap)           # the two RAREST
        b = heapq.heappop(heap)
        merged = a + b
        total += merged                   # every merge deepens both subtrees by one bit
        heapq.heappush(heap, merged)

    return total
Why merging the two rarest is safe

In an optimal prefix code, the two deepest leaves must be siblings — if a deepest leaf had no sibling you could promote it one level and save bits, so it was not optimal.

And the two rarest symbols can always be placed at that deepest pair: swapping a rarer symbol into a deeper slot and a commoner one into a shallower slot never increases the total, because you have moved fewer occurrences down and more occurrences up.

Merging them turns the problem into the same problem with one fewer symbol, so induction finishes. The exchange argument again, in different clothes.

The running total is worth understanding, because it is not obvious that summing the merges gives the encoded length. Each merge adds one bit to every symbol underneath it, and there are exactly a + b occurrences underneath. So total += a + b is precisely “one more bit for each of those occurrences”, and the sum over all merges is the weighted depth of the tree — which is the encoded size.

What Huffman does not do

Huffman is optimal among codes that assign a whole number of bits to each symbol. That restriction costs real space when one symbol dominates: a symbol with probability 0.9 deserves about 0.15 bits and Huffman must give it 1.

Arithmetic and range coding lift the whole-bit restriction and beat Huffman on skewed data, which is why modern formats use them. Huffman survives because it is fast, simple, and within a fraction of a bit per symbol of the entropy bound in ordinary cases.

6Deciding what to reach for

A short procedure for a problem you have not seen before.

The honest order of operations, in an interview or at a desk:

  1. Write the brute force first, even if only in your head. It defines what “correct” means and gives you something to test against.
  2. Guess a greedy rule and try to break it. Two minutes with the checklist in §4. If you break it, you have learned exactly what the difficulty is, which usually points at the DP state.
  3. If you cannot break it, sketch the exchange argument. If the swap is obviously valid and obviously not worse, implement greedy and move on.
  4. If the exchange fails, go to DP. The reason it failed is normally the extra dimension your DP state needs — “how much capacity is left”, “how many stops have I used”.
Signal in the problemSuggests
“Maximise the number of non-overlapping…”Greedy, sort by end
“Minimise the number of groups/rooms/boats…”Greedy, sort by start + heap
Items are divisibleGreedy by ratio
Items are indivisible and capacity is boundedDP (knapsack)
A choice now changes what is available later, unpredictablyDP or backtracking
“Merge / combine until one remains”Greedy with a heap (Huffman-shaped)
“Fewest coins / steps” with arbitrary denominationsDP — greedy is a trap
Answer needs the actual choices, not just the countEither, but keep back-pointers
Greedy algorithms you have already met
  • Dijkstra (Ch. 22) — greedily lock in the closest unfinished vertex. The proof is an exchange argument, and it needs non-negative weights exactly because the swap must not improve things.
  • Kruskal and Prim (Ch. 23) — greedily take the cheapest safe edge. The cut property is the exchange argument.
  • Huffman — greedily merge the two rarest.

All three are famous precisely because greed is provably right there. That is rarer than it looks, and the three of them get cited so often that people over-generalise. Most greedy rules you invent for a new problem will be wrong.

The cashier's algorithm has a licence

Nobody worries that shop assistants hand out change greedily. It works because the coin system was designed so that it works — a system where greedy is always optimal is called canonical, and every currency in circulation is one.

That is the general shape of successful greed: not that the strategy is clever, but that the problem has a structure which happens to reward it. Your job is to check the structure is there, not to admire the strategy.

What to carry forward

  • A greedy algorithm makes one locally best choice at each step and never revisits it. Fast and short; correct only under conditions.
  • It needs optimal substructure (as DP does) and the greedy-choice property: some optimal solution contains the greedy first choice.
  • The standard proof is an exchange argument: take an optimum, swap in the greedy choice, show it stays valid and no worse.
  • The other standard proof is to match a lower bound — interval partitioning opens a room only when that many intervals genuinely overlap.
  • Interval scheduling sorts by end time; interval partitioning sorts by start time and uses a heap. The sort key is the algorithm.
  • Greedy fails on coin change with arbitrary denominations ({1,3,4} and amount 6), on 0/1 knapsack, and on longest path.
  • Make the knapsack fractional and greedy by density becomes optimal. Indivisibility is what breaks greed.
  • Huffman merges the two rarest symbols repeatedly; the sum of the merge weights is the encoded size.
  • Before writing a greedy algorithm, spend two minutes trying to break it: make its gain razor-thin, make its choice block two others, add ties, keep the input tiny.
  • Dijkstra, Kruskal and Prim are all greedy with proofs. Their fame makes greed look more reliable than it is.

>_Playground

The three wrong interval rules, greedy vs optimal coin change on every denomination system, and Huffman on real text.

scratch.pypython not loaded
Real Python, running in your browser. Nothing is installed or uploaded.
Output appears here. The first run takes a few seconds while Python loads.

Exercises

Checked automatically the moment you submit. Work top to bottom — each one assumes the last. Your answers are saved in this browser.

All Warm-up Core Challenge Reset chapter

Chapter 25 — Dynamic Programming I: The Grammar

DP is not a trick, it is a sentence with four blanks: state, transition, base case, order. Fill those four in and the code writes itself.

Continue →