A greedy algorithm builds a solution piece by piece, always choosing the locally optimal option at each step, hoping that local choices lead to a globally optimal solution.
GREEDY-SOLVE(problem):
solution = empty
while problem is not solved:
candidate = SELECT best remaining option
if candidate is FEASIBLE:
solution = solution + candidate
return solution
A greedy strategy is optimal only when the problem has:
Not every optimisation problem has these properties. Proving greedy correctness is the hard part — the algorithm itself is usually simple.
At every step, we can make a choice that is locally best without considering future sub-problems, and still reach a global optimum.
After making the greedy choice, the remaining problem is a smaller instance of the same type, and its optimal solution combined with the greedy choice yields the overall optimum.
| Step | Description |
|---|---|
| 1. Define | Formalise the greedy choice |
| 2. Assume | Suppose OPT exists that does not include the greedy choice |
| 3. Exchange | Modify OPT by swapping in the greedy choice — show no worse |
| 4. Conclude | An optimal solution including the greedy choice exists |
This "exchange argument" is the workhorse proof technique for greedy correctness. If you cannot construct a valid exchange, the greedy choice property likely does not hold.
Given n activities with start and finish times (s_i, f_i), select the maximum number of non-overlapping activities.
| Activity | Start | Finish |
|---|---|---|
| A | 0 | 6 |
| B | 1 | 4 |
| C | 3 | 5 |
| D | 5 | 7 |
| E | 3 | 9 |
| F | 5 | 9 |
| G | 6 | 10 |
| H | 8 | 11 |
Sort by finish time (earliest first), then greedily select each activity whose start time >= the finish time of the last selected.
Earliest finish time is the canonical greedy choice for interval scheduling.
f_1 <= f_2 <= ... <= f_nO(n log n) — dominated by sortingO(1) extra (if sorted in place)The weighted variant (each activity has a profit) requires dynamic programming — greedy no longer works.
def activity_selection(activities):
# Sort by finish time
activities.sort(key=lambda x: x[1])
selected = [activities[0]]
last_finish = activities[0][1]
for start, finish in activities[1:]:
if start >= last_finish:
selected.append((start, finish))
last_finish = finish
return selected
# Example
acts = [(0,6),(1,4),(3,5),(5,7),
(3,9),(5,9),(6,10),(8,11)]
print(activity_selection(acts))
# [(1,4), (5,7), (8,11)]
Given characters with known frequencies, assign variable-length binary codes to minimise total encoded length.
Huffman coding is the foundation of data compression. Used in DEFLATE (gzip, PNG), JPEG, and many other formats.
| Char | Freq | Fixed (3-bit) | Huffman |
|---|---|---|---|
| a | 45 | 000 | 0 |
| b | 13 | 001 | 101 |
| c | 12 | 010 | 100 |
| d | 16 | 011 | 111 |
| e | 9 | 100 | 1101 |
| f | 5 | 101 | 1100 |
100 × 3 = 300 bits45×1 + 13×3 + 12×3 + 16×3 + 9×4 + 5×4 = 224 bitsAlways merge the two lowest-frequency nodes. This ensures the rarest characters end up deepest in the tree (longest codes).
O(n log n) — each heap op is O(log n), performed O(n) timesO(n) for the treeimport heapq
def huffman(freq):
heap = [(f, i, char)
for i, (char, f)
in enumerate(freq.items())]
heapq.heapify(heap)
counter = len(heap)
while len(heap) > 1:
f1, _, left = heapq.heappop(heap)
f2, _, right = heapq.heappop(heap)
merged = (left, right)
heapq.heappush(
heap,
(f1 + f2, counter, merged)
)
counter += 1
return heap[0][2] # root of tree
freq = {'a':45, 'b':13, 'c':12,
'd':16, 'e':9, 'f':5}
tree = huffman(freq)
Given n items with weight w_i and value v_i, and a knapsack of capacity W, maximise total value. Fractions of items are allowed.
v_i / w_iThe item with the highest ratio should be included as much as possible. If it were not, we could swap in more of it for less of a lower-ratio item, improving the total.
O(n log n) for sortingO(1) extra| Item | Value | Weight | Ratio |
|---|---|---|---|
| A | 60 | 10 | 6.0 |
| B | 100 | 20 | 5.0 |
| C | 120 | 30 | 4.0 |
Capacity W = 50:
Total: 60 + 100 + (20/30)×120 = 240
No fractions allowed — does NOT have the greedy choice property. Requires dynamic programming.
Given n jobs with deadline d_i and profit p_i, each taking one unit of time, schedule on a single machine to maximise total profit. A job earns its profit only if completed by its deadline.
O(n log n) for sorting + O(n × d) for schedulingO(n log n) with Union-FindO(d) for the slot array| Job | Deadline | Profit |
|---|---|---|
| J1 | 2 | 100 |
| J2 | 1 | 19 |
| J3 | 2 | 27 |
| J4 | 1 | 25 |
| J5 | 3 | 15 |
Sorted by profit: J1, J3, J4, J2, J5
J4, J2 skipped — no available slots before deadline
Total profit: 100 + 27 + 15 = 142
Always pick the largest denomination that does not exceed the remaining amount.
Coins: {1, 5, 10, 25} Target: 41
Result: 4 coins (optimal)
Coins: {1, 3, 4} Target: 6
The denomination set is not canonical — greedy does not always work.
US/UK/Euro denominations are canonical — greedy works. For arbitrary denominations, use DP. Testing whether a denomination set is canonical is itself a non-trivial problem.
Given arrival and departure times of n trains, find the minimum number of platforms required so that no train waits.
| Train | Arrival | Departure |
|---|---|---|
| T1 | 09:00 | 09:10 |
| T2 | 09:40 | 12:00 |
| T3 | 09:50 | 11:20 |
| T4 | 11:00 | 11:30 |
| T5 | 15:00 | 19:00 |
| T6 | 18:00 | 20:00 |
Peak overlap: T2, T3, T4 → 3 platforms
def min_platforms(arrivals, departures):
arrivals.sort()
departures.sort()
plat_needed = 0
max_plat = 0
i = j = 0
while i < len(arrivals):
if arrivals[i] <= departures[j]:
plat_needed += 1
max_plat = max(max_plat,
plat_needed)
i += 1
else:
plat_needed -= 1
j += 1
return max_plat
O(n log n) — dominated by sortingO(1) extraGiven a connected, weighted, undirected graph, find a spanning tree with minimum total edge weight.
V - 1 edges are selectedAlways pick the lightest edge that does not form a cycle. The cut property guarantees this is safe: the minimum-weight edge crossing any cut must be in some MST.
O(E log E) — dominated by sorting edgesO(V) for Union-FindBest for sparse graphs where E ≈ V.
KRUSKAL(G):
sort edges by weight
UF = UnionFind(V)
mst = []
for (u, v, w) in sorted_edges:
if UF.find(u) != UF.find(v):
mst.append((u, v, w))
UF.union(u, v)
return mst
Green edges = MST (weight 1+2+3=6). Dashed = rejected.
Always pick the cheapest edge that expands the current tree. The cut property again guarantees optimality.
PRIM(G, start):
key[v] = INF for all v
key[start] = 0
parent[v] = NIL for all v
Q = min-heap of all vertices
while Q not empty:
u = EXTRACT-MIN(Q)
for each neighbour v of u:
if v in Q and w(u,v) < key[v]:
parent[v] = u
key[v] = w(u,v)
DECREASE-KEY(Q, v)
return parent
| Implementation | Time |
|---|---|
| Adjacency matrix | O(V²) |
| Binary heap + adj list | O((V+E) log V) |
| Fibonacci heap + adj list | O(E + V log V) |
| Aspect | Kruskal's | Prim's |
|---|---|---|
| Approach | Edge-centric | Vertex-centric |
| Best for | Sparse graphs | Dense graphs |
| Data structure | Union-Find | Priority queue |
| Starting point | Any order | Fixed source |
Find the shortest path from a single source to all other vertices in a weighted graph with non-negative edge weights.
dist[source] = 0, dist[v] = ∞ for all othersdistu with minimum dist, then relax all neighboursAlways process the unvisited vertex with the smallest known distance. Once extracted, its distance is final.
If edge weights can be negative, a vertex extracted from the queue might later be reachable via a cheaper path. This breaks the greedy invariant. Use Bellman-Ford for negative weights.
| Implementation | Time |
|---|---|
| Adjacency matrix | O(V²) |
| Binary heap + adj list | O((V+E) log V) |
| Fibonacci heap | O(E + V log V) |
def dijkstra(graph, source):
dist = {v: float('inf')
for v in graph}
dist[source] = 0
pq = [(0, source)]
while pq:
d, u = heapq.heappop(pq)
if d > dist[u]:
continue
for v, w in graph[u]:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
heapq.heappush(pq,
(dist[v], v))
return dist
Most widely used shortest-path algorithm — GPS routing, OSPF, game AI pathfinding.
| Aspect | Greedy | DP |
|---|---|---|
| Choice timing | Before solving sub-problems | After solving sub-problems |
| Sub-problems | One remains | Multiple overlapping |
| Backtracking | Never | Considers all options |
| Proof | Greedy choice property | Bellman equation |
| Complexity | O(n log n) | O(n²) or O(nW) |
| Implementation | Simple loop | Table / memoisation |
→ Greedy if you can prove the greedy choice property
→ DP if you need to consider multiple choices per step
→ Greedy if there is a natural exchange argument
→ DP if the problem has overlapping sub-problems
When in doubt, try greedy first. If you can prove it, you get a simpler, faster algorithm. If you cannot, switch to DP.
Items {(60,10), (100,20), (120,30)}, capacity 50.
Denominations {1, 3, 4}, target 6.
Even when not optimal, greedy often provides a good approximation with a provable bound:
| Problem | Approximation |
|---|---|
| Set cover | O(ln n) factor |
| TSP (metric) | Within factor 2 |
| Makespan (LPT) | Within factor 4/3 |
An exchange argument proves greedy correctness by showing that any optimal solution can be transformed into the greedy solution without loss of quality.
OPT be an arbitrary optimal solutionG be the greedy solutionOPT and G differOPT at that point to match G — show still feasible and no worseOPT is transformed into GG is optimalOPT starts with activity a_k (not the earliest-finishing a_1)a_k with a_1: since f_1 ≤ f_k, activity a_1 finishes no later → no new conflictsA matroid is a pair (S, I) where S is a finite ground set and I is a family of "independent" subsets satisfying:
A ∈ I and B ⊆ A, then B ∈ IA, B ∈ I and |A| < |B|, then ∃ x ∈ B \ A such that A ∪ {x} ∈ IRado-Edmonds theorem: a greedy algorithm produces an optimal solution for maximising a weighted sum over independent sets of a matroid — and only for matroids.
If your problem can be cast as optimising over a matroid, greedy is guaranteed to work.
| Matroid | Ground set | Independent sets |
|---|---|---|
| Graphic | Edges of a graph | Acyclic edge subsets (forests) |
| Uniform | Any set | Subsets of size ≤ k |
| Partition | Partitioned elements | At most one per partition |
| Linear | Vectors | Linearly independent subsets |
Kruskal's MST works because forests form the independent sets of a graphic matroid. The greedy algorithm on this matroid finds a maximum-weight basis (= MST).
Activity selection, fractional knapsack, job sequencing
Huffman coding, Dijkstra, Prim
Minimum platforms, interval scheduling
Proving any greedy correct
| Source | Description |
|---|---|
| CLRS | Introduction to Algorithms — Ch. 15-16: greedy algorithms and matroid theory |
| Kleinberg & Tardos | Algorithm Design — Ch. 4: greedy algorithms, exchange arguments |
| Erickson | Algorithms — jeffe.cs.illinois.edu — free textbook |
| MIT 6.046J | Design and Analysis of Algorithms — greedy lectures on MIT OCW |
| Oxley | Matroid Theory, 2nd ed. — the definitive matroid reference |