DSA Greedy Algorithms

Explore greedy algorithms — strategies that make the locally optimal choice at each step — and learn when that approach is guaranteed to work and when it silently fails.

What Is a Greedy Algorithm?

A greedy algorithm builds a solution step by step, at each step choosing whichever option looks best right now, without reconsidering earlier choices or looking ahead. Greedy algorithms are simple and fast, but they only produce a truly optimal answer when the problem has a special structure called the greedy-choice property: a locally optimal choice at each step is guaranteed to lead to a globally optimal solution.

  • Greedy-choice property - a locally optimal choice at each step leads to a globally optimal solution
  • Optimal substructure - an optimal solution to the problem contains optimal solutions to its subproblems
  • No backtracking - once a choice is made, a greedy algorithm never revisits or undoes it
  • Not always optimal - without the greedy-choice property, a greedy strategy can produce a suboptimal or even wrong answer

When Greedy Works: Coin Change and Activity Selection

Two classic problems where the greedy-choice property holds are coin change with canonical denominations and activity selection. In coin change, repeatedly taking the largest coin that does not exceed the remaining amount never blocks a better solution. In activity selection — choosing the maximum number of non-overlapping intervals from a schedule — always picking the activity that finishes earliest leaves the most room for future choices.

Example

def greedy_coin_change(amount, coins):
    coins = sorted(coins, reverse=True)
    result = []
    for coin in coins:
        while amount >= coin:
            amount -= coin
            result.append(coin)
    return result

print(greedy_coin_change(63, [25, 10, 5, 1]))  # [25, 25, 10, 1, 1, 1]

Example

def select_activities(activities):
    # Each activity is a (start, end) tuple.
    activities = sorted(activities, key=lambda a: a[1])
    selected = [activities[0]]
    last_end = activities[0][1]
    for start, end in activities[1:]:
        if start >= last_end:
            selected.append((start, end))
            last_end = end
    return selected

schedule = [(1, 4), (3, 5), (0, 6), (5, 7), (8, 9), (5, 9)]
print(select_activities(schedule))  # [(1, 4), (5, 7), (8, 9)]

When Greedy Fails: Non-Canonical Coins

Greedy coin change breaks down for arbitrary denominations. With coins [1, 3, 4] and a target of 6, greedy takes the largest coin first (4), then is forced into two 1-coins, using 3 coins total — but the optimal answer is 3 + 3, using only 2 coins. Greedy can't see that taking a smaller coin now would have led to a better outcome later.

Example

def greedy_coin_change(amount, coins):
    coins = sorted(coins, reverse=True)
    result = []
    for coin in coins:
        while amount >= coin:
            amount -= coin
            result.append(coin)
    return result

# Greedy picks 4 + 1 + 1 (3 coins), but 3 + 3 (2 coins) is optimal.
print(greedy_coin_change(6, [1, 3, 4]))  # [4, 1, 1] -- not optimal!
ProblemGreedy Works?Why
Coin change (canonical coins)YesEach coin choice never blocks a better later choice
Coin change (arbitrary coins)NoTaking the largest coin can leave a remainder no combination covers optimally
Activity selectionYesPicking earliest finish time keeps the most room for future activities
0/1 KnapsackNoValue-vs-weight tradeoffs require comparing combinations, which DP handles
Note: Before trusting a greedy solution, try to prove the greedy-choice property or test it against a brute-force or DP solution on small inputs. If they ever disagree, greedy is not safe for that problem.

Exercise: DSA Greedy Algorithms

What is the core strategy of a greedy algorithm?