Activity Selection Problem
Problem Statement
Given n activities with their start and finish times, select the maximum number of activities that can be performed by a single person, assuming that a person can only work on a single activity at a time.
Constraints:
- 1 ≤ n ≤ 10^5
- 0 ≤ start[i] < finish[i] ≤ 10^9
Example:
- Input:
start = [1, 3, 0, 5, 8, 5],finish = [2, 4, 6, 7, 9, 9] - Output:
4 - Explanation: Activities 0, 1, 3, and 4 can be selected (indices based on sorted order by finish time).
Example 2:
- Input:
start = [10, 12, 20],finish = [20, 25, 30] - Output:
1 - Explanation: Only one activity can be performed as all overlap.
Approach 1: Brute Force (Recursion)
Intuition
Try all possible subsets of activities and find the maximum subset where no two activities overlap.
Algorithm
- Generate all possible subsets of activities
- For each subset, check if activities are non-overlapping
- Track the maximum size of valid subsets
- Two activities are compatible if one finishes before the other starts
import java.util.*;
public class Solution {
private int maxCount = 0;
private boolean isCompatible(int[][] activities, List<Integer> subset) {
for (int i = 0; i < subset.size(); i++) {
for (int j = i + 1; j < subset.size(); j++) {
int a = subset.get(i), b = subset.get(j);
// Check if activities overlap
if (!(activities[a][1] <= activities[b][0] ||
activities[b][1] <= activities[a][0])) {
return false;
}
}
}
return true;
}
private void solve(int[][] activities, int idx, List<Integer> current) {
if (idx == activities.length) {
if (isCompatible(activities, current)) {
maxCount = Math.max(maxCount, current.size());
}
return;
}
// Include current activity
current.add(idx);
solve(activities, idx + 1, current);
current.remove(current.size() - 1);
// Exclude current activity
solve(activities, idx + 1, current);
}
public int activitySelection(int[] start, int[] finish) {
int n = start.length;
int[][] activities = new int[n][2];
for (int i = 0; i < n; i++) {
activities[i][0] = start[i];
activities[i][1] = finish[i];
}
maxCount = 0;
solve(activities, 0, new ArrayList<>());
return maxCount;
}
}Complexity Analysis
- Time Complexity: O(2^n) - We generate all possible subsets
- Space Complexity: O(n) - Recursion stack and current subset storage
Approach 2: Greedy (Sort by Finish Time) - Optimal
Intuition
The greedy choice is to always select the activity that finishes earliest and is compatible with previously selected activities. This leaves maximum room for remaining activities.
Key Insight: By selecting the activity with the earliest finish time, we maximize the remaining time for other activities.
Algorithm
- Sort activities by finish time
- Select the first activity (earliest finish time)
- For each subsequent activity, if its start time >= last selected finish time, select it
- Count selected activities
import java.util.*;
public class ActivitySelection {
public int maxActivities(int[] start, int[] finish) {
int n = start.length;
// Create activities array: [finish, start, index]
int[][] activities = new int[n][3];
for (int i = 0; i < n; i++) {
activities[i][0] = finish[i];
activities[i][1] = start[i];
activities[i][2] = i;
}
// Sort by finish time
Arrays.sort(activities, (a, b) -> a[0] - b[0]);
int count = 1; // First activity is always selected
int lastFinish = activities[0][0];
for (int i = 1; i < n; i++) {
int currentStart = activities[i][1];
int currentFinish = activities[i][0];
// If current activity starts after last selected finishes
if (currentStart >= lastFinish) {
count++;
lastFinish = currentFinish;
}
}
return count;
}
public List<Integer> getSelectedActivities(int[] start, int[] finish) {
int n = start.length;
int[][] activities = new int[n][3];
for (int i = 0; i < n; i++) {
activities[i][0] = finish[i];
activities[i][1] = start[i];
activities[i][2] = i;
}
Arrays.sort(activities, (a, b) -> a[0] - b[0]);
List<Integer> selected = new ArrayList<>();
selected.add(activities[0][2]);
int lastFinish = activities[0][0];
for (int i = 1; i < n; i++) {
if (activities[i][1] >= lastFinish) {
selected.add(activities[i][2]);
lastFinish = activities[i][0];
}
}
return selected;
}
public static void main(String[] args) {
ActivitySelection as = new ActivitySelection();
int[] start = {1, 3, 0, 5, 8, 5};
int[] finish = {2, 4, 6, 7, 9, 9};
System.out.println(as.maxActivities(start, finish)); // Output: 4
}
}Dry Run Example
Activities:
Index: 0 1 2 3 4 5
Start: 1 3 0 5 8 5
Finish:2 4 6 7 9 9
After sorting by finish time:
Activity: (1,2) (3,4) (0,6) (5,7) (8,9) (5,9)
Index: 0 1 2 3 4 5
Step 1: Select activity 0 (1,2). last_finish = 2, count = 1
Step 2: Activity 1 (3,4). start=3 >= 2. Select! last_finish = 4, count = 2
Step 3: Activity 2 (0,6). start=0 < 4. Skip.
Step 4: Activity 3 (5,7). start=5 >= 4. Select! last_finish = 7, count = 3
Step 5: Activity 4 (8,9). start=8 >= 7. Select! last_finish = 9, count = 4
Step 6: Activity 5 (5,9). start=5 < 9. Skip.
Result: 4 activities selected
Timeline:
[1-2] [3-4] [5-7] [8-9]
0 1 3 4
Complexity Analysis
- Time Complexity: O(n log n) - Dominated by sorting
- Space Complexity: O(n) for storing activities (can be O(1) if we sort in place)
Why Greedy Works
Proof by Exchange Argument:
- Let OPT be any optimal solution
- Let G be our greedy solution
- If the first activity in G differs from OPT, we can replace OPT's first activity with G's (which finishes earlier), without losing any activities
- This exchange doesn't reduce the number of activities
- By induction, greedy solution is optimal
Greedy Choice Property: The activity with earliest finish time is always part of some optimal solution.
Key Takeaways
- Greedy by Finish Time: Sort by finish time, not start time or duration
- Why Finish Time? Earliest finish leaves maximum room for other activities
- Compatibility Check: Activity is compatible if start >= last finish
- First is Free: First activity (earliest finish) is always selected
- Classic Interval Scheduling: This is the foundation for many scheduling problems
- Similar Problems: Meeting Rooms, Non-overlapping Intervals, Maximum Meetings
- Extension: Can be extended to weighted activities using DP