COMPUTER SCIENCE FUNDAMENTALS SERIES

Greedy
Algorithms

Greedy choice property · Activity selection · Huffman coding ·
Fractional knapsack · Exchange arguments · Matroid theory
Mid-level software engineer track  ·  20 slides
02

The Greedy Paradigm

What is a greedy algorithm?

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.

  • At each decision point, pick the choice that looks best right now
  • Never reconsider previous choices — no backtracking
  • Hope that local optimality leads to global optimality
  • Much simpler and faster than exhaustive search or DP

The greedy template

GREEDY-SOLVE(problem):
    solution = empty
    while problem is not solved:
        candidate = SELECT best remaining option
        if candidate is FEASIBLE:
            solution = solution + candidate
    return solution

When does it work?

A greedy strategy is optimal only when the problem has:

  • Greedy choice property — a locally optimal choice is part of some globally optimal solution
  • Optimal substructure — optimal solution contains optimal solutions to sub-problems

Not every optimisation problem has these properties. Proving greedy correctness is the hard part — the algorithm itself is usually simple.

03

Greedy Choice Property & Optimal Substructure

Greedy choice property

At every step, we can make a choice that is locally best without considering future sub-problems, and still reach a global optimum.

  • There exists an optimal solution that includes the greedy choice
  • We do not need to solve all sub-problems first (unlike DP)
  • Proof: assume OPT does not include the greedy choice, then show we can swap it in without worsening the result

Optimal substructure

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.

  • Shared with DP — both require this property
  • Difference: greedy commits to one sub-problem; DP considers all

Proof strategy (exchange argument)

StepDescription
1. DefineFormalise the greedy choice
2. AssumeSuppose OPT exists that does not include the greedy choice
3. ExchangeModify OPT by swapping in the greedy choice — show no worse
4. ConcludeAn 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.

04

Activity Selection / Interval Scheduling

Problem

Given n activities with start and finish times (s_i, f_i), select the maximum number of non-overlapping activities.

ActivityStartFinish
A06
B14
C35
D57
E39
F59
G610
H811

Greedy strategy

Sort by finish time (earliest first), then greedily select each activity whose start time >= the finish time of the last selected.

Why earliest finish time?

  • Earliest start? Fails — a long activity starting early blocks many short ones
  • Shortest duration? Fails — a short activity in the middle can block two others
  • Fewest conflicts? Works but slower to compute
  • Earliest finish leaves the most room for subsequent activities

Earliest finish time is the canonical greedy choice for interval scheduling.

05

Activity Selection — Proof & Implementation

Correctness proof sketch

  1. Sort activities by finish time: f_1 <= f_2 <= ... <= f_n
  2. Activity 1 (earliest finish) is in some OPT — if not, swap the first activity in OPT with activity 1; it finishes no later, so no new conflicts
  3. After selecting activity 1, the remaining problem is the same type
  4. By induction, the greedy algorithm is optimal

Complexity

  • Time: O(n log n) — dominated by sorting
  • Space: O(1) extra (if sorted in place)

The weighted variant (each activity has a profit) requires dynamic programming — greedy no longer works.

Implementation

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)]
06

Huffman Coding — Idea

Problem

Given characters with known frequencies, assign variable-length binary codes to minimise total encoded length.

  • Frequent characters get short codes
  • Rare characters get long codes
  • Must be a prefix-free code — no code is a prefix of another — for unambiguous decoding

Huffman coding is the foundation of data compression. Used in DEFLATE (gzip, PNG), JPEG, and many other formats.

Example

CharFreqFixed (3-bit)Huffman
a450000
b13001101
c12010100
d16011111
e91001101
f51011100
  • Fixed-length: 100 × 3 = 300 bits
  • Huffman: 45×1 + 13×3 + 12×3 + 16×3 + 9×4 + 5×4 = 224 bits
  • 25% smaller
07

Huffman Coding — Algorithm

Algorithm

  1. Create a leaf node for each character with its frequency
  2. Insert all nodes into a min-priority queue
  3. While the queue has more than one node:
    • Extract the two nodes with lowest frequency
    • Create a new internal node with these as children; frequency = sum
    • Insert the new node back into the queue
  4. The remaining node is the root of the Huffman tree

Greedy choice

Always merge the two lowest-frequency nodes. This ensures the rarest characters end up deepest in the tree (longest codes).

Complexity

  • Time: O(n log n) — each heap op is O(log n), performed O(n) times
  • Space: O(n) for the tree

Implementation

import 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)
08

Fractional Knapsack

Problem

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.

Greedy strategy

  1. Compute value-to-weight ratio v_i / w_i
  2. Sort items by ratio in decreasing order
  3. Take whole items if they fit; otherwise take the fraction that fills the remaining capacity

Why greedy works

The 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.

Complexity

  • Time: O(n log n) for sorting
  • Space: O(1) extra

Example

ItemValueWeightRatio
A60106.0
B100205.0
C120304.0

Capacity W = 50:

A: 10 + B: 20 + C: 20/30

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

0/1 Knapsack

No fractions allowed — does NOT have the greedy choice property. Requires dynamic programming.

09

Job Sequencing with Deadlines

Problem

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.

Greedy strategy

  1. Sort jobs by profit in decreasing order
  2. For each job, schedule it in the latest available slot before its deadline
  3. If no slot is available, skip the job

Complexity

  • Time: O(n log n) for sorting + O(n × d) for scheduling
  • Can be O(n log n) with Union-Find
  • Space: O(d) for the slot array

Example

JobDeadlineProfit
J12100
J2119
J3227
J4125
J5315

Sorted by profit: J1, J3, J4, J2, J5

Slot 1: J3Slot 2: J1Slot 3: J5

J4, J2 skipped — no available slots before deadline

Total profit: 100 + 27 + 15 = 142

10

Minimum Number of Coins

Greedy strategy

Always pick the largest denomination that does not exceed the remaining amount.

Example — standard denominations

Coins: {1, 5, 10, 25}   Target: 41

25 10 5 1

Result: 4 coins (optimal)

When greedy fails

Coins: {1, 3, 4}   Target: 6

  • Greedy: 4 + 1 + 1 = 3 coins
  • Optimal: 3 + 3 = 2 coins

The denomination set is not canonical — greedy does not always work.

Canonical coin systems

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.

11

Minimum Number of Platforms

Problem

Given arrival and departure times of n trains, find the minimum number of platforms required so that no train waits.

Greedy strategy

  1. Sort all arrival and departure times separately
  2. Sweep through events chronologically (two-pointer)
  3. Arrival increments platform count; departure decrements
  4. Track the maximum concurrent count

Example

TrainArrivalDeparture
T109:0009:10
T209:4012:00
T309:5011:20
T411:0011:30
T515:0019:00
T618:0020:00

Peak overlap: T2, T3, T4 → 3 platforms

Implementation

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

Complexity

  • Time: O(n log n) — dominated by sorting
  • Space: O(1) extra
12

Greedy Graph Algorithms — Kruskal's MST

Minimum Spanning Tree (MST)

Given a connected, weighted, undirected graph, find a spanning tree with minimum total edge weight.

Kruskal's algorithm

  1. Sort all edges by weight (ascending)
  2. For each edge: if adding it does not create a cycle, include it in the MST
  3. Stop when V - 1 edges are selected

Greedy choice

Always 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.

Complexity

  • Time: O(E log E) — dominated by sorting edges
  • Space: O(V) for Union-Find

Best for sparse graphs where E ≈ V.

Pseudocode (Union-Find)

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

Trace example

A B C D 1 2 3 5 4

Green edges = MST (weight 1+2+3=6). Dashed = rejected.

13

Greedy Graph Algorithms — Prim's MST

Prim's algorithm

  1. Start from any vertex; add it to the MST set
  2. Repeatedly add the minimum-weight edge connecting an MST vertex to a non-MST vertex
  3. Stop when all vertices are in the MST

Greedy choice

Always pick the cheapest edge that expands the current tree. The cut property again guarantees optimality.

Pseudocode (binary heap)

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

Complexity comparison

ImplementationTime
Adjacency matrixO(V²)
Binary heap + adj listO((V+E) log V)
Fibonacci heap + adj listO(E + V log V)

Prim's vs Kruskal's

AspectKruskal'sPrim's
ApproachEdge-centricVertex-centric
Best forSparse graphsDense graphs
Data structureUnion-FindPriority queue
Starting pointAny orderFixed source
14

Dijkstra's Shortest Path

Problem

Find the shortest path from a single source to all other vertices in a weighted graph with non-negative edge weights.

Algorithm

  1. Set dist[source] = 0, dist[v] = ∞ for all others
  2. Use a min-priority queue keyed by dist
  3. Extract vertex u with minimum dist, then relax all neighbours
  4. Repeat until the queue is empty

Greedy choice

Always process the unvisited vertex with the smallest known distance. Once extracted, its distance is final.

Why non-negative weights?

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.

Complexity

ImplementationTime
Adjacency matrixO(V²)
Binary heap + adj listO((V+E) log V)
Fibonacci heapO(E + V log V)

Relaxation step

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.

15

Greedy vs Dynamic Programming

Similarities

  • Both require optimal substructure
  • Both solve optimisation problems
  • Both decompose the problem into sub-problems

Key differences

AspectGreedyDP
Choice timingBefore solving sub-problemsAfter solving sub-problems
Sub-problemsOne remainsMultiple overlapping
BacktrackingNeverConsiders all options
ProofGreedy choice propertyBellman equation
ComplexityO(n log n)O(n²) or O(nW)
ImplementationSimple loopTable / memoisation

Decision guide

→ 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.

16

When Greedy Fails

Classic counter-examples

0/1 Knapsack

Items {(60,10), (100,20), (120,30)}, capacity 50.

  • Greedy by ratio: items 1+2 → value 160
  • Optimal: items 2+3 → value 220

Coin change (non-canonical)

Denominations {1, 3, 4}, target 6.

  • Greedy: 4+1+1 = 3 coins
  • Optimal: 3+3 = 2 coins

More failures

  • Longest path in a DAG — greedy by heaviest next edge fails
  • TSP — nearest-neighbour heuristic produces far-from-optimal tours

Why greedy fails

  • The locally best choice locks out a combination of future choices that would have been globally better
  • There is no "exchange" that can fix the solution
  • The problem lacks the greedy choice property, even though it may have optimal substructure

Greedy as approximation

Even when not optimal, greedy often provides a good approximation with a provable bound:

ProblemApproximation
Set coverO(ln n) factor
TSP (metric)Within factor 2
Makespan (LPT)Within factor 4/3
17

Exchange Arguments for Proofs

The technique

An exchange argument proves greedy correctness by showing that any optimal solution can be transformed into the greedy solution without loss of quality.

Template

  1. Let OPT be an arbitrary optimal solution
  2. Let G be the greedy solution
  3. Find the first point where OPT and G differ
  4. Modify OPT at that point to match G — show still feasible and no worse
  5. Repeat until OPT is transformed into G
  6. Conclude: G is optimal

Example — activity selection

  • OPT starts with activity a_k (not the earliest-finishing a_1)
  • Replace a_k with a_1: since f_1 ≤ f_k, activity a_1 finishes no later → no new conflicts
  • Modified solution has the same size and is still valid
  • By induction on remaining activities, greedy is optimal

Tips

  • The exchange must preserve feasibility — this is where most proofs break down
  • Usually argues about the first difference between OPT and G
  • Induction handles the remaining sub-problem
  • If you cannot construct a valid exchange, the greedy choice property likely does not hold
18

Matroid Theory

What is a matroid?

A matroid is a pair (S, I) where S is a finite ground set and I is a family of "independent" subsets satisfying:

Axioms

  1. Hereditary: if A ∈ I and B ⊆ A, then B ∈ I
  2. Exchange: if A, B ∈ I and |A| < |B|, then ∃ x ∈ B \ A such that A ∪ {x} ∈ I

Why matroids matter

Rado-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.

Examples of matroids

MatroidGround setIndependent sets
GraphicEdges of a graphAcyclic edge subsets (forests)
UniformAny setSubsets of size ≤ k
PartitionPartitioned elementsAt most one per partition
LinearVectorsLinearly independent subsets

Kruskal's and matroids

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).

19

Common Greedy Patterns & Pitfalls

Patterns

Sort then scan

Activity selection, fractional knapsack, job sequencing

Priority queue

Huffman coding, Dijkstra, Prim

Sweep line

Minimum platforms, interval scheduling

Exchange argument

Proving any greedy correct

Common pitfalls

  • Assuming greedy works without proof — the most dangerous mistake; always verify the greedy choice property
  • Wrong sorting criterion — activity selection by start time or duration fails; only finish time works
  • Confusing fractional and 0/1 — fractional knapsack is greedy; 0/1 is DP
  • Ignoring edge cases — empty input, ties in sorting, all items identical
  • Negative weights — Dijkstra fails with negative edges; Kruskal/Prim handle them fine

Interview strategy

  1. Identify if the problem has the greedy choice property
  2. State the greedy criterion explicitly
  3. Argue correctness (exchange argument or matroid)
  4. Code the sort + scan / priority queue
  5. State time and space complexity
20

Summary & Further Reading

Key takeaways

  • Greedy algorithms make locally optimal choices and never backtrack — simple and fast when correct
  • Correctness requires the greedy choice property and optimal substructure — prove it or switch to DP
  • Activity selection, Huffman coding, and fractional knapsack are canonical greedy problems
  • Kruskal's, Prim's, and Dijkstra's are greedy graph algorithms justified by the cut property
  • Exchange arguments are the standard proof technique
  • Matroid theory gives a complete characterisation of when greedy works
  • When greedy fails (0/1 knapsack, non-canonical coins), it often still provides a useful approximation

Recommended reading

SourceDescription
CLRSIntroduction to Algorithms — Ch. 15-16: greedy algorithms and matroid theory
Kleinberg & TardosAlgorithm Design — Ch. 4: greedy algorithms, exchange arguments
EricksonAlgorithmsjeffe.cs.illinois.edu — free textbook
MIT 6.046JDesign and Analysis of Algorithms — greedy lectures on MIT OCW
OxleyMatroid Theory, 2nd ed. — the definitive matroid reference