Greedy Algorithms

A greedy algorithm makes the locally optimal choice at every single step, and never looks back to reconsider it — hoping that a sequence of locally-best choices adds up to a globally optimal answer. It's tempting because it's simple and fast, usually O(n log n) or better. The catch: it only produces the correct answer for problems with the greedy choice property — where a locally optimal choice is provably never wrong. This lesson shows one problem where that property holds (so greedy is safe and simple), and one where it doesn't (so greedy quietly gives a wrong answer) — the skill worth building is telling the two apart.

Where Greedy Works: Activity Selection / Interval Scheduling

Problem: given a list of activities, each with a start and end time, select the maximum number of activities that don't overlap (you can only do one at a time).

The greedy rule: sort all activities by end time, ascending. Then walk through them in that order, greedily picking every activity whose start time is at or after the end time of the last activity you picked.

Why this greedy choice is safe: among all activities, the one that finishes earliest can never hurt you to pick first — picking it leaves the maximum possible remaining time for everything else, and any valid solution can be rearranged to include it without reducing the count. That exchange argument is what makes this a real "greedy choice property," not just a fast heuristic that usually works.

# activities is a list of (start, end) tuples
def max_activities(activities):
    activities = sorted(activities, key=lambda a: a[1])  # sort by end time, ascending

    count = 1  # always take the first activity after sorting
    last_end = activities[0][1]

    for start, end in activities[1:]:
        if start >= last_end:  # doesn't overlap the last one picked
            count += 1
            last_end = end

    return count

Time complexity: O(n log n) — dominated by the sort; the single pass afterward is O(n). Space complexity: O(n) for the sorted copy the sort produces (ignoring Timsort's own internal working space) — just one running variable besides that, no table or memo needed, unlike the DP problems from the last two lessons.

Why sort by end time, not start time or duration? Sorting by start time can lock you into a long activity early, blocking several shorter ones that would've fit instead. Sorting by shortest duration also fails — a short activity in the middle of the timeline can still block more total activities than a longer one at the very start. End time is the only one of the three with a provable exchange argument.

Where Naive Greedy Fails: 0/1 Knapsack

Recall 0/1 Knapsack from the previous lesson: choose a subset of items (whole or not at all) to maximize value within a weight capacity. A tempting greedy rule: sort items by value density (value ÷ weight) descending, and keep taking items in that order as long as they still fit.

# Greedy by value density — looks reasonable, but is NOT guaranteed optimal for 0/1 items.
def knapsack_greedy_by_density(weights, values, capacity):
    n = len(weights)
    # sort item indices by value/weight ratio, descending
    indices = sorted(range(n), key=lambda i: values[i] / weights[i], reverse=True)

    total_value = 0
    remaining = capacity
    for i in indices:
        if weights[i] <= remaining:
            total_value += values[i]
            remaining -= weights[i]

    return total_value

Counter-example that breaks it: capacity = 10. Item A: weight 6, value 12 (density 2.0). Item B: weight 5, value 9 (density 1.8). Item C: weight 5, value 9 (density 1.8). Greedy picks A first (highest density) using 6 of the 10 capacity, leaving only 4 — not enough for B or C — for a total value of 12. But picking B and C together costs weight 10 (exactly the capacity) for a total value of 18 — the true optimum, which greedy never even considers because it committed to A and can't undo that choice.

Time complexity: O(n log n) — the sort dominates; the pass afterward is O(n). Space complexity: O(n) for the sorted index list. But the complexity is irrelevant here since the answer can be wrong — this is precisely why runtime alone never justifies an algorithm; correctness comes first.

Contrast with the DP solution: the 0/1 Knapsack DP from the previous lesson considers, for every item, both "skip it" and "take it," and lets dp[i-1][w] vs. value + dp[i-1][w-weight] settle which is better for that specific remaining capacity — it never commits early the way greedy does. That's the fundamental reason DP gets 18 (correct) here, and greedy gets 12 (wrong): DP keeps every relevant option open until it has enough information to compare them; greedy discards options permanently at each step.

Recognizing Which Applies

SignalLikely approach
Choosing an item excludes/limits future choices in a way that can be proven never regretted (exchange argument holds)Greedy is safe — usually faster and simpler than DP
A locally-best choice can lock you out of a better global combination (as with 0/1 knapsack)DP — needs to compare all relevant combinations, not commit early
Interval/scheduling problems (activity selection, minimum meeting rooms)Greedy, typically after sorting by end time
"Fractional" version of a resource-allocation problem (can take part of an item)Often greedy works even when the 0/1 version needs DP — Fractional Knapsack is the classic example
Next: You've now seen every major data structure and pattern this track covers. Before moving into interview-framework lessons, consolidate everything into one reference page: Big-O Cheat Sheet.