Activity Selection
Given activities with start and finish times, select the maximum number of non-overlapping activities. The classic greedy solution sorts by earliest finish time and greedily picks compatible activities. This lesson covers the algorithm, proof sketch, LeetCode 435 variant, and implementations in C, Java, and Python with trace tables.
Overview
Each activity occupies a half-open interval [start, finish) on a timeline. Two activities overlap if one starts before the other finishes. The goal is to pack as many activities as possible into the schedule.
Timeline (example):
0 1 2 3 4 5 6 7 8 9
|----A----|
|--B--|
|--------C--------|
|--D--|
|-E-|
Greedy (sort by finish): pick A, D, E → 3 activities
Problem Statement
Input: n activities, each with start[i] and finish[i] where start[i] < finish[i].
Output: Maximum count of activities that can be performed by a single person (no two selected activities overlap).
Overlap rule: Activities i and j are compatible if start[j] >= finish[i] (assuming we process in order and the previous activity finished first).
Complexity target: O(n log n) time for sorting + O(n) scan; O(n) extra space if storing selected activities.
Greedy Algorithm
- Sort activities by increasing finish time.
- Select the first activity (earliest finish).
- For each remaining activity, if
start >= last_selected_finish, select it and updatelast_selected_finish.
Why Greedy Works
Exchange argument (sketch): Let g be the first activity in the greedy solution (minimum finish time). Let O be any optimal solution. If O does not include g, replace its first activity with g — since g finishes no later, nothing else is blocked. Repeat on the remaining subproblem. Hence greedy is optimal.
Code (C, Java, Python)
Python
def activity_selection(activities):
"""Max non-overlapping activities — sort by finish time. O(n log n)."""
activities = sorted(activities, key=lambda x: x[1])
count = 0
last_end = float('-inf')
selected = []
for start, finish in activities:
if start >= last_end:
count += 1
last_end = finish
selected.append((start, finish))
return count, selected
if __name__ == "__main__":
acts = [(1, 4), (3, 5), (0, 6), (5, 7), (8, 9)]
c, sel = activity_selection(acts)
print(f"count = {c}, selected = {sel}") # 3, [(1,4), (5,7), (8,9)]
Java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
public class ActivitySelection {
/** Sort by finish time; greedily pick compatible activities. */
public static int maxActivities(int[][] activities) {
Arrays.sort(activities, Comparator.comparingInt(a -> a[1]));
int count = 0;
int lastEnd = Integer.MIN_VALUE;
for (int[] act : activities) {
int start = act[0], finish = act[1];
if (start >= lastEnd) {
count++;
lastEnd = finish;
}
}
return count;
}
public static void main(String[] args) {
int[][] acts = {{1, 4}, {3, 5}, {0, 6}, {5, 7}, {8, 9}};
System.out.println(maxActivities(acts)); // 3
}
}
C
#include <stdio.h>
#include <stdlib.h>
typedef struct { int start, finish; } Activity;
int cmp_finish(const void *a, const void *b) {
return ((Activity *)a)->finish - ((Activity *)b)->finish;
}
int activity_selection(Activity acts[], int n) {
qsort(acts, n, sizeof(Activity), cmp_finish);
int count = 0, i, last_end = -1000000;
for (i = 0; i < n; i++) {
if (acts[i].start >= last_end) {
count++;
last_end = acts[i].finish;
}
}
return count;
}
int main(void) {
Activity acts[] = {{1, 4}, {3, 5}, {0, 6}, {5, 7}, {8, 9}};
printf("%d\n", activity_selection(acts, 5)); /* 3 */
return 0;
}
Output & Trace
Program output
| Input activities | Max count | Selected (one optimal set) |
|---|---|---|
| [(1,4), (3,5), (0,6), (5,7), (8,9)] | 3 | (1,4), (5,7), (8,9) |
| [(1,2), (2,3), (3,4)] | 3 | All three (touching ends OK) |
| [(1,5), (2,3), (4,6)] | 2 | (2,3), (4,6) or (1,5) + compatible |
Greedy trace — [(1,4), (3,5), (0,6), (5,7), (8,9)]
| Step | Activity (start, finish) | start >= last_end? | Action | last_end | count |
|---|---|---|---|---|---|
| After sort | — | — | Order: (1,4), (3,5), (0,6), (5,7), (8,9) | — | 0 |
| 1 | (1, 4) | yes | Select | 4 | 1 |
| 2 | (3, 5) | no (3 < 4) | Skip | 4 | 1 |
| 3 | (0, 6) | no | Skip | 4 | 1 |
| 4 | (5, 7) | yes | Select | 7 | 2 |
| 5 | (8, 9) | yes | Select | 9 | 3 ← answer |
LeetCode 435 — Non-overlapping Intervals
Problem: Given intervals, find the minimum number of intervals to remove so the rest are non-overlapping.
Reduction: removals = n - max_non_overlapping. Use the same greedy (sort by end, pick compatible).
def erase_overlap_intervals(intervals):
"""LeetCode 435 — min removals for non-overlapping set."""
if not intervals:
return 0
intervals.sort(key=lambda x: x[1])
kept = 1
last_end = intervals[0][1]
for start, finish in intervals[1:]:
if start >= last_end:
kept += 1
last_end = finish
return len(intervals) - kept
Example: [[1,2],[2,3],[3,4],[1,3]] → keep 3, remove 1.
Variants & Related Problems
| Problem | Twist | Greedy key |
|---|---|---|
| Activity selection | Max count | Sort by finish time |
| Non-overlapping intervals (435) | Min removals | Same greedy; answer = n − kept |
| Meeting rooms II (253) | Min rooms needed | Sort starts/ends; sweep line |
| Interval partitioning | Min classrooms | Sort by start; min-heap of end times |
| Weighted activity selection | Max total weight | DP (not pure greedy) |
Approach Comparison
| Approach | Time | Space | Notes |
|---|---|---|---|
| Greedy (sort by finish) | O(n log n) | O(1) or O(n) | Optimal — use this |
| Brute force subsets | O(2ⁿ) | O(n) | Interview baseline only |
| DP (weighted version) | O(n log n) or O(n²) | O(n) | When activities have profits |
Real-Life Applications
| Domain | Scenario | Mapping |
|---|---|---|
| Office scheduling | Book max meetings in one conference room | Non-overlapping intervals |
| Broadcasting | Schedule max ads in a TV slot | Activity selection on time windows |
| CPU scheduling | Run max non-preemptive jobs on one core | Finish-time greedy (simplified) |
| Resource allocation | Assign max tasks to one machine | Compatible time ranges |
| Calendar apps | Suggest how many events fit without conflict | LeetCode 435 style removals |
| Sports venues | Max games on one field per day | Sort by end time, pack schedule |
Interview Tips
- State upfront: sort by finish time, then greedy scan.
- Clarify overlap: is
[1,2]and[2,3]overlapping? Usually no if condition isstart >= last_end. - For LeetCode 435, say
answer = n - max_keptusing the same loop. - Mention exchange argument in one sentence if asked for proof.
- If weights are given, switch to DP — greedy by finish alone fails.
- Connect to greedy introduction and job sequencing (deadline-based variant).
Key Takeaway
Activity selection = sort by finish time + greedily pick the next compatible activity. Same core loop solves max count and min removals (435). Weighted versions need DP.
Next up: Fractional knapsack · Job sequencing · Greedy introduction