E ExamMaster

Data Structures & Algorithms · Data Structures & Algorithms

Greedy Algorithms

Making locally optimal choices to reach a global optimum for suitable problems.

Eight concepts on greedy algorithms — the greedy-choice property, greedy versus DP, activity selection, Huffman coding, when greedy fails, jump-game reachability, and a technique picker. Fast local choices win only when the problem proves they compose into a global optimum.

  • Data Structures & Algorithms
  • Medium level
  • 8 concepts
  • 5 practice questions

1Greedy: commit to the locally best choice

A greedy algorithm builds a solution by repeatedly taking the choice that looks best right now and never reconsidering it. That strategy yields a global optimum only when the problem has the greedy-choice property (some optimal solution includes the local pick) and optimal substructure (the rest is an optimal solution to a smaller instance).

Without those properties, a locally best step can lock you out of every optimal completion — coin change with arbitrary denominations is the classic warning. Speed alone does not certify correctness.

Figure. Greedy takes the local pick and never revisits it. That is optimal only when some optimum includes that pick and the rest is an optimum of the leftover instance. Speed alone does not prove either property.

What greedy needs

  1. Local ruleDefine what "best right now" means (earliest finish, highest ratio, farthest reach, …).
  2. CommitTake that choice permanently; do not branch on alternatives.
  3. JustifyArgue greedy-choice + optimal substructure (often via an exchange argument) — or refuse greedy.
Greedy yields a global optimum only when
  1. The problem has the greedy-choice property and optimal substructure
  2. The local rule runs in O(1) time per step
  3. You sort the input first — sorting alone makes any greedy rule correct

Correctness is about structure, not about step cost or the presence of a sort. Sorting often enables a greedy scan but does not prove the rule.

2Greedy vs dynamic programming

Dynamic programming explores (or tabulates) multiple choices at each stage and keeps optima for subproblems. Greedy keeps only one choice per stage. That makes greedy faster — often a sort plus a linear scan — but wrong when the discarded branches contained the only optimal answers.

Reach for greedy when you can prove a local rule is safe. Reach for DP when subproblems overlap and the safe move depends on trying more than one option (0/1 knapsack, edit distance, many path counts).

Figure. Greedy commits to a local choice; DP keeps optimal answers for every subproblem when the local choice is unsafe.

Greedy vs DP
LensGreedyDP
Choices keptOne local pick per stepAll relevant sub-answers
Typical timeSort + O(n) scanPolynomial in state space
Correct whenGreedy-choice holdsOptimal substructure + overlapping subproblems
Failure modeLocal trap (bad coin systems)Too large a state space
0/1 knapsack (take or skip each item wholly) is handled by
  1. DP — a greedy by value or ratio can miss the optimum
  2. Greedy by heaviest item first, always optimal
  3. Greedy by lightest item first, always optimal

0/1 knapsack needs subproblem optima over remaining capacity. Ratio greedy is optimal for fractional knapsack, not 0/1.

3Activity selection by earliest finish

To maximize the number of non-overlapping intervals, sort activities by finish time and scan once: always pick the next activity whose start is \geq the last chosen finish. Earliest finish leaves the most room for future picks — an exchange argument shows some optimal schedule includes the greedy choice.

Sorting costs O(n \log n); the scan is O(n). Sorting by start time instead can fail: a long early starter blocks many short later activities.

Figure. Intervals on a shared time scale (width proportional to duration, x to start). Greedy keeps (1,3) and (4,7); (2,5), (5,9), and (1,8) each overlap a pick.

Earliest-finish scan

  1. SortOrder activities by increasing finish time.
  2. Pick firstTake the earliest-finishing activity; set last_finish to its finish.
  3. ScanWhenever the next activity's start ≥ last_finish, pick it and update last_finish.

Activity Selection

Given activities with (start, finish) = [(1,3), (2,5), (4,7), (1,8), (5,9)], select the maximum number of non-overlapping activities.

  • Sort by finish: (1,3), (2,5), (4,7), (1,8), (5,9)order fixed
  • Pick (1,3); last_finish = 31 activity
  • Skip (2,5) — start 2 < 3; pick (4,7); last_finish = 72 activities
  • Skip (1,8) and (5,9) — both start before 7maximum count = 2: (1,3) and (4,7)

Pro tip. Sorting by earliest finish time provably maximizes the count of non-overlapping intervals — a classic exchange-argument result. (Recomputed on this instance: two activities, not three — nothing after (4,7) is compatible.)

Coding lab. Pick the earliest-finish pair runs in the app, with checks on your output.

Activity selection maximizes count by sorting on
  1. Earliest finish time
  2. Earliest start time
  3. Longest duration first

Earliest finish is the safe greedy key. Earliest start or longest duration can block denser schedules.

4Huffman coding with a min-heap

Huffman coding builds an optimal prefix code by greedily merging the two lowest-frequency nodes until one tree remains. Each merge creates a parent whose frequency is the sum of its children; a min-heap (priority queue) supplies the two lightest nodes in O(\log n) per extract.

Total time O(n \log n) for n symbols. The greedy step is safe: an exchange argument shows some optimal code tree combines the two rarest symbols at the deepest level.

Figure. Parents 2, 4, 8 from the ledger. Rarest leaves (1, 1) sit deepest under the first merge.

Merge lightest pair

  1. Heap of leavesInsert each symbol's frequency into a min-heap.
  2. MergeExtract two minima; create a parent with frequency sum; push the parent back.
  3. RepeatUntil one node remains — that is the code tree; depth of a leaf is its code length.

Huffman merge loop

import heapq
def huffman_cost(freqs):
    h = list(freqs)
    heapq.heapify(h)
    total = 0
    while len(h) > 1:
        a = heapq.heappop(h)
        b = heapq.heappop(h)
        parent = a + b
        total += parent
        heapq.heappush(h, parent)
    return total

Four-symbol Huffman merges

Frequencies [1, 1, 2, 4]. Run Huffman merges with a min-heap. What parent frequencies are created, in order, and what is the sum of those parent frequencies (a standard weighted-external-path accumulator)?

  • Heap [1, 1, 2, 4]; merge 1+1parent 2; heap [2, 2, 4]
  • Merge 2+2parent 4; heap [4, 4]
  • Merge 4+4parent 8; heap [8]
  • Sum of parents created2 + 4 + 8 = 14

Pro tip. Huffman coding is O(n \log n) using a priority queue — each of n-1 merges does two extracts and one insert.

Each Huffman step merges
  1. The two lowest-frequency nodes currently in the heap
  2. The two highest-frequency nodes, to finish rare symbols last
  3. A random pair — any binary tree is an optimal prefix code

The greedy rule always combines the lightest pair. Heaviest-first or random merges lose the optimality proof.

5When greedy fails: prove or find a counterexample

Greedy needs an exchange argument or an explicit greedy-choice proof. If you cannot prove it, hunt a counterexample. Coin change with denominations [1, 3, 4] and amount 6 is enough: the "largest coin first" rule takes 4+1+1 (three coins) while 3+3 uses two.

Fractional knapsack is safe for ratio greedy; 0/1 knapsack is not. Interval scheduling by earliest finish is safe; by longest interval is not. The pattern is: local density does not always equal global density.

Figure. Largest-first greedy spends three coins; two coins of 3 are enough. A counterexample kills the rule.

Stress-test a greedy rule

  1. State the ruleName the local key (largest coin, longest interval, …).
  2. Seek a small counterexampleFind an instance where the rule's answer is worse than a known better solution.
  3. Or proveExchange argument: transform any optimal solution into the greedy one without loss.

Coin change trap

Denominations [1, 3, 4], amount 6. Compare "largest coin first" greedy to the true minimum number of coins.

  • Greedy: take 4, remain 2 → 1+13 coins: 4+1+1
  • Optimal: 3+32 coins
  • Gapgreedy uses one extra coin — rule fails on this system

Pro tip. Canonical coin systems (like US coins) often make largest-first optimal — arbitrary denomination sets do not. Prove or look up; do not assume.

On denominations [1, 3, 4] and amount 6, largest-first greedy
  1. Uses 3 coins, while 3+3 is better with 2
  2. Is optimal with 2 coins
  3. Cannot run because 4 does not divide 6

Greedy takes 4 then two 1s. Divisibility is irrelevant — remainders can use smaller coins; optimality is the failure.

6Jump game: farthest reach

Given nums[i] as the maximum jump length from index i, decide whether you can reach the last index. Track farthest: the rightmost index reachable so far. Scan i from left to right while i ≤ farthest; update farthest = max(farthest, i + nums[i]). If farthest ever reaches the end, return true.

The scan is O(n) and avoids an O(n^2) DP that tries every jump length from every index. The greedy claim: only the farthest frontier matters for reachability.

Figure. Cells are jump lengths. Index 0 reaches through 2; index 1 extends farthest to the last cell.

Farthest-reach scan

  1. Initializefarthest = 0.
  2. Scan reachable iFor i from 0 while i ≤ farthest, set farthest = max(farthest, i + nums[i]).
  3. DecideIf farthest ≥ n−1 at any time, success; if the loop ends with farthest < n−1, failure.

Jump game reachability

def can_jump(nums):
    farthest = 0
    for i, step in enumerate(nums):
        if i > farthest:
            return False
        farthest = max(farthest, i + step)
        if farthest >= len(nums) - 1:
            return True
    return True

Jump Game (Reachability)

Given nums = [2, 3, 1, 1, 4] where each value is the max jump length, determine if you can reach the last index.

  • i = 0 ≤ farthest 0: farthest = max(0, 0+2) = 2can reach index 2
  • i = 1 ≤ 2: farthest = max(2, 1+3) = 4farthest = 4
  • 4 ≥ last index 4reachable → true

Pro tip. The greedy "farthest reach" scan is O(n) and avoids the O(n^2) DP of checking every jump combination.

On nums = [2, 3, 1, 1, 4], after processing index 1, farthest is
  1. 4, so the last index is already reachable
  2. 2, unchanged from index 0
  3. 3, only i + 1

From index 1 you may jump 3 steps to index 4. max(2, 4) = 4 covers the end.

7Safe greedy classics: ratios and intervals

Fractional knapsack — you may take a fraction of an item — is solved by sorting on value/weight ratio and filling the bag in that order in O(n \log n). 0/1 knapsack forbids fractions and needs DP. Interval scheduling (activity selection) and Huffman join the "proven greedy" set; Dijkstra is greedy on non-negative weights with a priority queue in O((V+E)\log V).

Memorizing the names is less useful than remembering which constraint makes the proof work: fractions allowed, earliest finish, non-negative edges, lightest-pair merge.

Figure. Classic greedy sorts: density for fractional knapsack, earliest end for intervals, frequency for Huffman.

Proven greedy templates
ProblemGreedy keyTime
Activity / interval schedulingEarliest finishO(n \log n)
Fractional knapsackValue/weight ratioO(n \log n)
Huffman codingMerge two lightestO(n \log n)
Dijkstra (non-negative weights)Extract nearest vertexO((V+E)\log V) with a heap
Fractional knapsack sorts items by
  1. Value/weight ratio, and may take a fraction of the next item
  2. Weight alone, lightest first, never taking fractions
  3. Value alone, and must take whole items only

Ratio order plus fractional fill is the optimal greedy. Whole items only is 0/1 (DP). Lightest-first ignores value.

8When to reach for greedy

Try greedy when the prompt smells like "maximum number of non-overlapping…", "minimum number of… under a matroid-like choice", farthest reach, or Huffman-style repeated merge — and you can cite a known proof or write an exchange argument. Prefer DP when choices interact through a capacity or edit budget you must tabulate.

If a peer proposes a greedy sort key, either prove it or kill it with a three-line counterexample before you ship it in an interview.

Use the When to reach for greedy table: max non-overlapping intervals or farthest-reach jumps → known greedy; 0/1 knapsack or arbitrary coin systems → DP; otherwise prove or counterexample.

Prompt → approach
Prompt shapeFirst tool to try
Max non-overlapping intervalsEarliest-finish greedy
Reach last index with jump lengthsFarthest-reach greedy
0/1 knapsack / overlapping subproblemsDP
Arbitrary coin denominations, min coinsDP (not largest-first)
Minimum coins for amount V with arbitrary denominations is usually
  1. DP — largest-first greedy can fail
  2. Largest-first greedy, always optimal for every denomination set
  3. Activity selection by earliest finish

Unbounded knapsack-style DP is the safe general tool. Largest-first needs special denomination systems; activity selection is an intervals problem.

Notes

  • Greedy Principle: Make the choice that looks best right now and never reconsider; this yields a global optimum only when the problem has the greedy-choice property and optimal substructure.
  • Greedy vs DP: Greedy commits to one locally optimal choice per step, while DP explores all choices - greedy is faster but only correct for specific problems.
  • Activity Selection: To maximize non-overlapping intervals, sort by earliest finish time and greedily pick each compatible activity.
  • Huffman Coding: Repeatedly merge the two lowest-frequency nodes to build an optimal prefix code, using a min-heap.
  • Proving Correctness: Greedy needs an exchange argument or the greedy-choice property; otherwise it can give a suboptimal answer (e.g., coin change with arbitrary denominations).

Formulas

  • Activity selection (sorted by finish time): O(n \log n) for the sort, O(n) for selection.
  • Huffman coding: O(n \log n) using a priority queue.
  • Fractional knapsack (greedy by value/weight ratio): O(n \log n).
  • Dijkstra (greedy shortest path) with a heap: O((V + E) \log V).
  • Interval scheduling: sort then single pass, total O(n \log n).

Exam traps & shortcuts

  • Before choosing greedy, confirm the greedy-choice property; if a counterexample exists, switch to DP.
  • For interval/scheduling problems, sorting by finish time (not start time) is the key greedy insight.
  • Fractional knapsack is greedy (ratio-based); 0/1 knapsack is NOT greedy and requires DP.
  • Standard coin systems (like US coins) work greedily, but arbitrary denominations may not - verify or use DP.

Reference tables

Restated from the concepts — scan sheet only.

Complexity cheatsheet
AlgorithmTimeNotes
Activity selectionO(n \log n)Sort by finish + scan
Huffman codingO(n \log n)Min-heap merges
Fractional knapsackO(n \log n)Ratio sort
Jump game (reachability)O(n)Farthest frontier
Dijkstra with heapO((V+E)\log V)Non-negative weights

Rules the ledgers and proofs keep using.

Formula anchors
IdentityForm
Activity pick teststart ≥ last_finish
Jump updatefarthest = max(farthest, i + nums[i])
Huffman parentfreq(parent) = freq(a) + freq(b)
Fractional knapsack keysort by value/weight

Recap

Night-before greedy pegs.

Permit
Greedy needs greedy-choice + optimal substructure — prove or counterexample.
vs DP
Greedy keeps one choice; DP keeps sub-answers. 0/1 knapsack → DP.
Intervals
Sort by earliest finish; scan with last_finish.
Huffman
Always merge two lightest; O(n \log n) with a heap.
Coins
[1,3,4] amount 6: greedy 4+1+1 loses to 3+3.
Jumps
Track farthest; O(n) reachability.
Safe set
Fractional knapsack, activity selection, Huffman, Dijkstra (≥0 weights).

Practise Greedy Algorithms

Reading is free and needs no account. Practice, mocks and progress live in the app.

  • 5 exam-style questions on this topic, with explanations
  • A 6-question practice set that ends the chapter
  • Timed mocks scored with the real marking scheme
  • Readiness tracked per topic, kept on your device
Continue with Google — freeNo card, no trial. Works offline once installed.