Greedy Algorithms
By the end of this lesson you will be able to trace the activity-selection problem from its dynamic-programming roots through recursiveActivitySelector and greedyActivitySelector; state and use the greedy-choice property and optimal substructure to tell when a greedy algorithm is even allowed; build a Huffman tree by hand from a min-priority queue and read off its codewords and bit cost; recognise a matroid and run the generic matroidGreedy algorithm on it (spanning forests, task scheduling); and — just as importantly — construct and explain the classic cases where greedy FAILS (0/1 knapsack, coin changing with denominations {1, 5, 8}).
1. The activity-selection problem & optimal substructure
n people each want it for an interval [si, fi) — starting at si, finishing at fi. Two requests are compatible if their intervals don't overlap. You want the MAXIMUM number of requests you can satisfy. This is the running example for this whole lesson — assume the activities are already sorted so f1 ≤ f2 ≤ ... ≤ fn.An 11-activity example (activities numbered 0 to 10), already sorted by finish time — you'll see this exact set animated below:
Optimal substructure: define fits(p, q) = the activities that start after activity p finishes and finish before activity q starts. If some activity k in fits(p, q) belongs to an optimal solution, that solution splits into an optimal solution for fits(p, k), the activity k itself, and an optimal solution for fits(k, q); a cut-and-paste argument (swap in a better sub-answer and the whole thing improves, a contradiction) shows each part must be optimal for its own subproblem. This gives the naive DP recurrence:
The same recurrence as Dart (two sentinel positions frame the real activities: one that finishes at time 0 before everything, one that starts at infinity after everything), counting its inner-loop steps:
Input size → what’s feasible: this table needs C(n+2, 3) ≈ n3/6 inner steps: n = 80 → 88,560 (instant), n = 1,000 → ≈ 1.7·108 (about a second), n = 105 → 1.7·1014 (never). Hence the next section's shortcut.
(i,j). What makes activity selection special is the earliest-finish rule in Section 2: one specific choice is ALWAYS safe, which collapses the two-dimensional DP down to zero tables at all.2. recursiveActivitySelector & the greedy-choice property
This means only ONE subproblem ever remains after the greedy choice — not the many subproblems a generic DP recurrence would suggest. recursiveActivitySelector(s, f, k, n) makes this concrete (0-indexed like Dart; k is the last activity chosen, and the first call uses k = −1 to mean “nothing chosen yet”, with finish time 0):
aj ≠ am (where am is the truly earliest-finishing activity available). Since am finishes at or before aj, swapping aj out for am cannot break compatibility with anything else in the solution, and the solution stays the same SIZE — so it's still optimal, and now contains am. This is the same "exchange argument" pattern used throughout this lesson (Huffman's leaf-swap lemma, the matroid greedy-choice lemma).The earliest-finish rule as code: enumerate EVERY compatible subset of the 11 activities, take each maximum one, apply the swap, and check it stays compatible, keeps its size and now contains activity 0 (the earliest finisher):
Edge case: what happens when several activities are fully overlapping, so only ONE of them can ever be chosen?
Try your own set of activities on the recursive version itself (format start-finish, comma-separated — sorted by finish time automatically):
Θ(n) time — each activity is examined at most once across the whole recursion (the pointer m only ever moves forward), even though the recursion looks like it could revisit work.Dart implementation (a tiny finishOf(k) helper treats k < 0 as “finish time 0”, so the first call can say “nothing chosen yet” with k = −1; the loop bound is m < n because the last valid index is n − 1):
Input size → what’s feasible: n ≤ ~104 sorted activities → this recursive version is fine (the recursion depth equals the number of activities picked, so at most n frames); for n beyond a few 104, or when you cannot risk a stack overflow, use the loop in the next section.
3. greedyActivitySelector (iterative)
The recursive version is “tail recursive” — the recursive call is the very last thing that happens — so it converts mechanically into a simple loop, greedyActivitySelector(s, f):
Edge case: every activity finishes at the SAME time (all fully overlap) — only one can ever be picked, no matter how many there are:
Try your own set of activities (format start-finish, comma-separated — they'll be sorted by finish time automatically, exactly as greedyActivitySelector requires):
Θ(n) time given pre-sorted input (or Θ(n lg n) if you must sort first), O(1) extra space, always returns a MAXIMUM-size compatible subset (not necessarily the only one — the example above has other equally-sized answers such as {a1, a4, a9, a10}).Dart implementation (the first activity, index 0, is always chosen, so A = [0] and k = 0; the loop then scans m = 1 … n − 1):
Input size → what’s feasible: n = 106 activities already sorted by finish time → one pass = n − 1 ≈ 106 comparisons (milliseconds); if they arrive unsorted, sort first: n lg n ≈ 2·107 steps, still well under a second.
The Θ(n) claim and the unsorted-input pitfall, both as running code (greedyComparisons counts one comparison per activity; the pitfall input is (0,10), (1,2), (4,5) in START order):
4. Elements of the greedy strategy
- Cast the problem as "make one choice, then solve one remaining subproblem" — not many subproblems.
- Prove the greedy-choice property: show SOME optimal solution contains your proposed first choice (an exchange argument, as in the earliest-finish proof).
- Show optimal substructure: greedy choice + an optimal solution to what's left ⇒ an optimal solution overall.
Greedy versus the DP of Section 1, on the same inputs: the DP table work grows like n3/6, the greedy loop like n.
5. Fractional vs. 0/1 knapsack — when greedy fails
W. There are n items, each with a weight wi and a value vi. In the fractional version the thief can cut items into pieces (think gold dust, olive oil); in the 0/1 version each item is indivisible — take the whole thing or leave it (think a painting, a stereo).The natural greedy idea for both: sort items by value-per-pound vi/wi, descending, then grab as much as you can starting from the best ratio.
The exchange argument above as arithmetic (eps kilograms move from the lower-ratio item to the higher-ratio one):
Try your own items and capacity (format name:weight:value, comma-separated):
Dart implementation of fractional knapsack (the shape mirrors greedyActivitySelector: sort, then walk once):
Input size → what’s feasible: n = 105 items → the sort costs n lg n ≈ 1.7·106 steps (fine); a selection-based method removes the sort for an O(n) running time (see the Expert questions).
Now run the EXACT SAME greedy-by-ratio idea on the 0/1 version of the identical instance — items can only be taken whole:
O(nW) DP table solves it exactly, shown in the Interview questions section below).All three algorithms as Dart, on this lesson’s instance (weights 8, 15, 22; values 48, 75, 88; W = 30). Ratios are compared by cross-multiplying (v[b]*w[a] vs v[a]*w[b]) so no floating-point division is needed:
O(n lg n), PROVABLY optimal. 0/1 knapsack: the same greedy heuristic can be arbitrarily far from optimal — you need dynamic programming instead. Same-looking problem, one word ("fractional") away from a totally different algorithmic answer.6. Huffman codes
Cost of a tree T: B(T) = ∑ c.freq × dT(c) — frequency times codeword length (depth), summed over every character c. buildHuffmanTree builds the optimal tree bottom-up using a min-priority queue Q keyed on frequency:
The cost formula B(T) = ∑ c.freq · dT(c) as code, together with the second way to count it (each merge adds its combined frequency once):
z, an optimal tree for the smaller alphabet C - {x,y} + {z}, with z's leaf expanded back into an internal node with children x,y, is optimal for the original alphabet C. Together these prove Huffman's algorithm is correct — the exact same greedy-choice-plus-optimal-substructure pattern as activity selection, just with a priority queue standing in for "always pick the finish-time minimum."The exchange step of the greedy-choice lemma as code: swapping two leaves changes B(T) by exactly (fx − fy)(dx − dy), so moving the frequent letter up and the rare letter down can only help:
Edge cases: what does the tree look like with only 2 characters, or with several characters at EXACTLY EQUAL frequency?
Try your own alphabet (format char:freq, comma-separated, 2–7 distinct characters):
O(n lg n) time with a binary min-heap (n-1 merges × O(lg n) per removeMin/add). Produces a full binary tree (every internal node has exactly 2 children — a node with one child could be spliced out to shorten codewords). All optimal trees for the same frequencies share the SAME total cost B(T), even if individual codewords differ.Dart implementation (a sorted List<HuffNode> stands in for a binary min-heap: a moving head index plays removeMin and insert plays add):
Input size → what’s feasible: alphabet of n = 256 byte values → trivial; n = 105 distinct words → this sorted-list version costs O(n) per merge = 1010 (too slow), so use a real heap or the sort-once two-queue method (O(n lg n), interview question “Build a full Huffman code”).
7. Matroids & the GREEDY algorithm
The three matroid conditions as running code, on the graphic matroid of a triangle (edges 0, 1, 2) plus a pendant edge 3. Subsets are bit masks, independence = “no cycle”. It checks the empty set, heredity, the exchange property, and the equal-size property (every maximal independent set has |V| − 1 = 3 edges):
The graphic matroid MG = (SG, ℑG) for an undirected graph G=(V,E): the ground set is the edges, and a set of edges is "independent" exactly when it's ACYCLIC (forms a forest). A short argument (see the Expert questions) proves this really is a matroid, and it follows that every MAXIMAL independent set (here: every spanning tree) has the same size, |V|-1.
The generic algorithm, specialised to a WEIGHTED matroid (build the maximum-weight independent set):
Edge case: a graph that is a single TRIANGLE, with all three edges TIED at the same weight — must GREEDY still reject one, no matter how the tie is broken?
Try your own weighted edge list (format u-v-w, comma-separated, plus the number of vertices):
x is the first element (by decreasing weight) with {x} ∈ ℑ, some optimal (maximum-weight independent) set contains x — another exchange argument, using the matroid EXCHANGE PROPERTY itself to swap x in for some no-heavier element. A contraction lemma then shows the rest of the problem reduces to the SAME kind of problem on a smaller, "contracted" matroid — so matroidGreedy is always correct on any matroid, with no problem-specific proof needed.The generic matroidGreedy itself, written once against an independence oracle (any matroid plugs in), shown with the graphic matroid on the page’s 5-vertex example:
O(n lg n) sort + n independence checks. For the graphic matroid this specialises to exactly Kruskal's minimum/maximum-spanning-tree algorithm — a union-find independence test in place of a generic ℑ membership test.Dart implementation (vertices numbered 0..|V|-1, so a plain List<int> parent array works for the union-find independence test; sorting is DESCENDING here because we're building a MAXIMUM-weight forest, the mirror image of the minimum-spanning-tree version):
Input size → what’s feasible: V = 105 vertices, E = 2·105 edges → sort E lg E ≈ 3.6·106 steps plus E near-constant union-find calls (fast); the generic oracle version above re-checks the whole set each time (O(n) per test), so it is for understanding, not for n = 105.
8. Task scheduling as a matroid
n unit-time tasks (each takes exactly 1 time slot), each with a deadline di (an integer, 1 ≤ di ≤ n) and a penalty wi you must pay if the task finishes LATE. You want to schedule tasks (one at a time, one slot each) to MINIMIZE the total penalty of the late ones — equivalently, to MAXIMIZE the total penalty saved by the ON-TIME ("early") tasks.The on-time counting rule: a set of tasks A can ALL be scheduled on time if and only if, for every t = 0..n, Nt(A) ≤ t, where Nt(A) counts tasks in A with deadline ≤ t. This "independence" relation is a matroid (see the Expert questions), so the matroid greedy-choice property from Section 7 applies directly: run GREEDY, ordering tasks by DECREASING penalty.
The counting rule as code (the independence test Nt(A) ≤ t), plugged into the generic matroidGreedy from Section 7 on this lesson’s 7-task example:
Edge case: what happens when EVERY task shares the exact same deadline?
Try your own tasks (format deadline:penalty pairs, comma-separated):
O(n2) implementation: process tasks by decreasing penalty, and place each one in the LATEST still-free slot at or before its deadline (rejecting it — "late" — only if every slot up to its deadline is already full). A disjoint-set forest speeds this up to near-linear by finding the latest free slot in close to O(1) amortized time (see the Expert questions).Dart implementation (a deadline is a time between 1 and n, meaning “finish by the end of minute d”; the slot list is 0-indexed, so slot[s] is the task running during minute s + 1, and math.min(n, deadline) - 1 is the last slot index a task may use):
Input size → what’s feasible: n = 1,000 tasks → the O(n2) latest-free-slot scan is 106 steps (fine); n = 105 → 1010 (too slow), so use the disjoint-set version (Expert question).
9. Coin changing — another counterexample
A small counterexample: denominations {1, 5, 8}, amount = 10.
Try your own denominations and target amount — the animation always runs the safe DP alongside greedy and tells you whether they agree:
O(n · amount) time, ALWAYS exact and optimal, for ANY denomination set (even ones without a coin of value 1, where some amounts become simply impossible). Greedy is O(n) but only correct for canonical systems — and you often can't easily tell in advance whether your denominations are canonical.Greedy and DP coin changing as code, plus a tester that finds the first amount where greedy is not optimal ({1,5,8} fails at 10, {1,6,10} at 12, US coins never up to 500):
Input size → what’s feasible: amount ≤ 105 with at most 12 denominations → DP = 1.2·106 steps (instant); greedy is O(n) but only valid for canonical sets, and proving a set canonical in general is not something to do at run time.
Quiz
Interview questions
Cheat sheet
| Algorithm | Time | Space | Optimal? | When to use |
|---|---|---|---|---|
| recursiveActivitySelector / greedyActivitySelector | Θ(n) after sort, Θ(n lg n) total | O(1) extra (iterative) | Yes (earliest-finish rule) | Max # of non-overlapping intervals; input MUST be sorted by finish time |
| Weighted activity selection | O(n²) (O(n lg n) with binary search for p(i)) | O(n) | Yes (DP, not plain greedy) | Same as above but maximizing total VALUE, not count |
| Fractional knapsack | O(n lg n) | O(n) | Yes (exchange argument) | Items can be split (money, weight of a commodity) |
| 0/1 knapsack — greedy by ratio | O(n lg n) | O(1) | No — counterexample in §5 | Never use for 0/1; use the O(nW) DP instead |
| 0/1 knapsack — DP | O(nW) | O(nW) (O(W) rolling) | Yes | Items indivisible, small integer capacity W |
| buildHuffmanTree | O(n lg n) with a binary heap | O(n) | Yes (greedy choice + substructure) | Build an optimal prefix code from character frequencies |
| matroidGreedy on a matroid | O(n lg n + n·f(n)), f(n)=cost of one independence check | O(n) | Yes, but only ON a matroid | Max/min spanning forest, task scheduling, anything provably matroid-shaped |
| Task scheduling (latest-free-slot greedy) | O(n²) simple, ~O(n α(n)) with disjoint sets | O(n) | Yes (it is a matroid) | Unit-time tasks, deadlines, minimize total penalty of late ones |
| Coin changing — greedy | O(n) per amount | O(1) | Only for CANONICAL denominations | US-style coins; never trust blindly on arbitrary denominations |
| Coin changing — DP | O(n · amount) | O(amount) | Always | Any denomination set, safe default |