Activity Selection (Interval Scheduling)
You are given n activities, each with a start time and a finish time. A single person can work on only one activity at a time, so two activities conflict if their time ranges overlap. Return the maximum number of activities the person can complete. Two activities where one ends exactly when the other begins do not conflict and may both be chosen.
- 1 ≤ n ≤ 10⁵
- For each activity, start < finish
- One activity at a time; ranges that overlap in their interior conflict
- Touching at an endpoint (one finishes as the next starts) is allowed
start = [1,3,0,5,8,5], finish = [2,4,6,7,9,9]4start = [1,2,3], finish = [2,3,4]3start = [1,1,1], finish = [3,3,3]1start = [0], finish = [10]1start = [1,2,4], finish = [3,5,6]2You have a pile of activities, each occupying a time range, and you can only do one at a time. You want to do as many as possible. Note carefully what is being maximised: the count of activities, not the total time occupied, not the value of any single activity. A person who does five short talks beats a person who does one all-day workshop.
When several activities overlap and you can keep only one, which do you keep? The instinct is to reach for a rule, so let us test the tempting ones and watch them break — this is the discipline from the coin problem, applied again.
Both failures share a cause: the thing that matters is not when an activity starts or how long it is, but when it frees you up again.
Here is the choice that works: among all activities, do the one that finishes earliest. Then throw away everything that conflicts with it, and repeat on what remains.
The intuition is that finishing early is the most generous thing an activity can do for you — it hands back the most future time for everything else. But intuition is not proof, and greedy demands proof, so here is the actual argument.
f be the activity that finishes earliest of all. Claim: there is an optimal schedule that includes f. Take any optimal schedule, and look at its earliest-finishing activity, call it g. Since f finishes no later than g (it finishes earliest of everything), we can swap g out and f in. Does that break anything? Every other activity in the optimal schedule started after g finished, and f finishes no later than g, so all of them still start after f finishes — no new conflict is created. The swap keeps the same number of activities and now includes f. So an optimal schedule containing f exists, which means picking f is safe. Having picked it, the remaining problem — schedule everything that starts at or after f's finish — is the exact same problem on a smaller set, and the argument repeats.That is the whole proof, and it is worth internalising, because it is the archetype for every interval greedy you will meet. The pattern: identify the choice, then show any optimal solution can be bent to include it without loss.
The proof hands us the algorithm directly. Sort by finish time; walk through, taking each activity whose start is not before the last taken activity's finish.
def max_activities(activities):
# activities = [(start, finish), ...]
activities.sort(key=lambda a: a[1]) # sort by FINISH time
count = 0
last_finish = float('-inf')
for start, finish in activities:
if start >= last_finish: # no conflict with the last one kept
count += 1
last_finish = finish # this activity now bounds the future
return countSorting is O(N log N) and dominates; the scan is O(N). We only ever compare against last_finish, because once activities are sorted by finish time, the last one kept is guaranteed to be the earliest-freeing constraint on everything ahead.
(1,2), (3,4), (0,6), (5,7), (8,9), (5,9).(1,2): start 1 ≥ −∞. Take it. Count 1, last_finish = 2.(3,4): start 3 ≥ 2. Take it. Count 2, last_finish = 4.(0,6): start 0 < 4. Conflict, skip. (It finishes late anyway — exactly the kind of greedy trap avoided.)(5,7): start 5 ≥ 4. Take it. Count 3, last_finish = 7.(8,9): start 8 ≥ 7. Take it. Count 4, last_finish = 9.(5,9): start 5 < 9. Conflict, skip.Answer 4, the schedule (1,2), (3,4), (5,7), (8,9). Notice (0,6) was rejected despite starting first — the early start was a lure, and finishing-first was the safe guide.
This is the foundational greedy on intervals, and its shape recurs constantly. The reusable recipe:
Recognition signals for this exact pattern in an unseen problem: you are given intervals or (start, end) jobs; you want the maximum number that mutually avoid overlap, or equivalently the minimum number to delete so the rest are disjoint. Those two phrasings are the same problem — keeping the most is the mirror of removing the fewest — and "Non-overlapping Intervals" later in this section is precisely that mirror, solved with this identical sort-by-finish greedy.
A close cousin swaps the goal: if instead you wanted to cover a timeline with the fewest rooms or catch every interval with the fewest points, the sort key and the tracked boundary change, but the discipline — pick the safe choice, prove it by exchange — does not.
Finish Line Greedy
CORE LOGIC
Always prioritize the interval that ends first. This leaves the most remaining time for other activities.
REMOVAL RULE
If an interval starts before the previous finish line, it MUST be removed.