Greedy · 1 of 35
Activity Selection Problem
Fit as many play dates as you can into one afternoon by always picking the one that ends soonest.
- Simple way
- time O(2^n * n^2)
room O(n) - Clever way
- time O(n log n)
room O(n)
The problem, simply
You get n activities. Each one has a start time and a finish time. One person can do only one activity at a time, and cannot leave an activity halfway.
Give back the largest number of activities that person can do. Two activities fit together when the second one starts at or after the moment the first one finishes.
A story to remember it
Picture it
Six activities on a time ruler, in the order they were written down. Some bars overlap, so we cannot keep them all.
Examples
A normal case
- In
start = [5, 1, 3, 0, 5, 8], finish = [9, 2, 4, 6, 7, 9]- Out
4
B (1 to 2), C (3 to 4), E (5 to 7) and F (8 to 9) fit one after another. No way to fit five.
The classic textbook set
- In
start = [1, 3, 0, 5, 8, 5], finish = [2, 4, 6, 7, 9, 9]- Out
4
Same activities, written down in another order. The order of the input never changes the answer.
Ends that touch
- In
start = [10, 12, 20], finish = [20, 25, 30]- Out
2
10 to 20 ends exactly when 20 to 30 starts, so both fit. 12 to 25 clashes with both. If your version says touching is not allowed, the answer is 1.
One long activity against many short ones
- In
start = [0, 1, 3, 5], finish = [10, 2, 4, 6]- Out
3
The long one from 0 to 10 blocks everything. Picking by earliest finish skips it and keeps the three short ones.
One activity
- In
start = [4], finish = [7]- Out
1
One activity always fits.
Way 1: the simple way
Try every possible group of activities. For each group, check that no two of them clash. Keep the biggest group that has no clash.
- For each activity, choose "take it" or "leave it". That makes
2^ngroups. - For a finished group, compare every pair. Two activities clash if one starts before the other ends, both ways.
- Remember the biggest group with no clash.
class Solution {
private int best = 0;
public int activitySelection(int[] start, int[] finish) {
best = 0;
tryAll(start, finish, 0, new ArrayList<>());
return best;
}
private void tryAll(int[] s, int[] f, int i, List<Integer> chosen) {
if (i == s.length) {
if (noClash(s, f, chosen)) best = Math.max(best, chosen.size());
return;
}
chosen.add(i); // take activity i
tryAll(s, f, i + 1, chosen);
chosen.remove(chosen.size() - 1);
tryAll(s, f, i + 1, chosen); // leave activity i
}
private boolean noClash(int[] s, int[] f, List<Integer> chosen) {
for (int x = 0; x < chosen.size(); x++) {
for (int y = x + 1; y < chosen.size(); y++) {
int a = chosen.get(x), b = chosen.get(y);
boolean apart = f[a] <= s[b] || f[b] <= s[a];
if (!apart) return false;
}
}
return true;
}
}How long it takes: 2^n groups, and each check compares up to n^2 pairs, so O(2^n * n^2). Extra room: the chosen list and the recursion, O(n). Fine for 15 activities, hopeless for 50.
Way 2: the clever way
Do what the swing helper does. Sort the activities by finish time. Walk down the sorted list and keep a note of when the last kept activity ends.
- Sort by finish time, earliest first.
- Set
lastEndto minus infinity. Nothing is kept yet. - For each activity in that order: if it starts at or after
lastEnd, keep it and setlastEndto its finish. Otherwise skip it. - The number kept is the answer.
Why is this safe? Take any best plan. Its first activity ends at some time. The activity that ends earliest of all ends no later, so you can swap it in and nothing after it breaks. Repeat the same swap for the second pick, the third, and so on. This is called an exchange argument.
Keep the activity that frees you up soonest
01 / 08
Sort the activities by when they finish. The one that ends first goes on top. Nothing is picked yet.
- last end
- none
- picked
- 0
- done
- answer
- rejected
- ignored
class Solution {
public int activitySelection(int[] start, int[] finish) {
int n = start.length;
int[][] acts = new int[n][2];
for (int i = 0; i < n; i++) {
acts[i][0] = start[i];
acts[i][1] = finish[i];
}
// whoever finishes first goes first
Arrays.sort(acts, (a, b) -> Integer.compare(a[1], b[1]));
int count = 0;
long lastEnd = Long.MIN_VALUE; // nothing kept yet
for (int[] a : acts) {
if (a[0] >= lastEnd) { // starts after the last one ended
count++;
lastEnd = a[1];
}
}
return count;
}
}How long it takes: sorting is O(n log n) and the walk is O(n), so O(n log n) in total. Extra room: the pairs array, O(n). If the input already comes sorted by finish time, the walk alone is O(n) with O(1) room.
Watch out for
- Touching rule. Check whether an activity that starts exactly when the last one ends is allowed. This page says yes (
>=). Some judges, like the meetings version, say no (>). - Subtracting in the comparator,
a[1] - b[1], can overflow for big times. UseInteger.compare. - Starting
lastEndat 0 breaks when an activity starts at a negative time or at 0 with the strict rule. Start it below every possible time. - If you need the chosen activities and not only the count, keep their original numbers in the pairs before sorting.
What interviewers ask next
- Prove it is optimal. Exchange argument: the earliest finishing activity can replace the first pick of any best plan without breaking it, and the rest of the problem is the same problem on a smaller set.
- What if each activity has a value and you want the most value? Greedy breaks. Sort by finish, then use dynamic programming:
best[i] = max(best[i-1], value[i] + best[p]), wherepis the last activity that ends beforeistarts, found by binary search.O(n log n). - How many rooms do you need to run all activities? Sort by start and keep a min heap of end times. The biggest size the heap reaches is the answer.
- Fewest activities to remove so the rest do not overlap? It is
nminus this answer (LeetCode 435). - Fewest arrows to burst balloons on a line? Same greedy: sort by end, shoot at the end of the first balloon, skip everything that arrow hits.
Remember this
Grown-up words: the swing turns are intervals, "ends soonest first" is the earliest finish time greedy rule, the swap proof is an exchange argument, and this whole problem is called interval scheduling.