Activity Selection Problem — Why Start-Time Sorting Fails
Sorting by start time instead of finish time caused a scheduler to miss 31% of viable slots.
- Activity Selection maximizes non-overlapping tasks in one resource
- Sort by finish time, not start time or duration — that's the key
- Greedy picks earliest-finishing compatible activity each step
- Complexity is O(n log n) — dominated by the sort
- Breaks if you have weighted values — need DP then
- The proof is exchange argument: any optimal solution can be transformed to greedy
Imagine you have one classroom and 10 student clubs all want to book it on Saturday — Chess Club from 9–11, Drama Club from 10–12, Robotics from 11–2, and so on. You can only run one club at a time, and you want to fit in as many clubs as possible. The Activity Selection Problem is exactly that: given a list of activities with start and end times, pick the maximum number of non-overlapping activities you can schedule. The trick — and this is the beautiful part — is that a single dead-simple rule gets you the perfect answer every time.
Schedulers are everywhere. Your phone's calendar, a hospital operating theatre booking system, a broadcast station filling airtime — they all face the same core constraint: one resource, many competing demands, and the need to squeeze in as many valid slots as possible. The Activity Selection Problem is the textbook formalisation of that constraint, and mastering it unlocks a pattern you'll reuse across dozens of real problems.
The naive approach tries every combination. For 20 activities that's over a million. For 40 it's a trillion. That blows up fast. The greedy algorithm solves it in O(n log n) — dominated entirely by sorting — and produces a provably optimal answer. Most developers don't trust greedy algorithms. They want to see all options first. Activity Selection builds that trust because its proof is short, intuitive, and genuinely satisfying.
You'll learn why sorting by finish time (not start time, not duration) is the key, how to prove it's optimal without heavy math, and what interviewers really check on your whiteboard.
What is Activity Selection Problem?
You have a single room and 50 meeting requests. Each meeting has a start and end time. Pick the maximum number that don't overlap. That's it.
Greedy works because the earliest-finishing meeting leaves the most room for the rest. No backtracking, no DP, just sort and pick.
In production this shows up as job schedulers, bandwidth allocation, or any place where a resource can only do one thing at a time.
Proof of Optimality: The Exchange Argument
Why does sorting by finish time always give the maximum count? The exchange argument proves it.
Assume an optimal solution O exists. If the first activity in O is not the earliest-finishing one, we can swap it with the earliest-finishing activity without reducing the count. The earliest-finishing activity finishes no later than O's first, so it leaves at least as much room for the rest. By induction, the greedy choice is always safe.
In production, you don't need to prove this every time — but understanding it lets you recognise when greedy fails (like with weights or pre-emption).
Implementation: Production-Grade Java Code
Here's a complete, testable implementation with proper package naming and boundary handling.
- Use Comparator.comparingInt for clean sort by end time
- Use
>=to allow back-to-back activities - Return empty list for null/empty input
- Include a helper to get the theoretical max (relaxation for testing)
The code lives in io.thecodeforge.scheduling.
>, skipped adjacent meetings.>= fixed it — back-to-back meetings now allowed.Visual Timeline: Gantt Chart of Selected Activities
To truly understand how the greedy algorithm builds its optimal schedule, visualising the selected activities on a timeline is incredibly helpful. The Gantt chart below shows a typical input set of 12 activities and the 4 that the algorithm selects.
The chart uses time on the x-axis and each selected activity as a bar. Notice how the algorithm always picks the earliest-finishing activity that does not overlap with the last selected one. This step-by-step construction is what makes the proof intuitive: at every choice, the remaining timeline is maximised.
In the example, activities (1,4), (5,7), (8,11), and (12,14) are selected. You can see there are no gaps that can be filled because any candidate would overlap. The chart also shows why sorting by start time fails: the early-starting long activity (0,6) would block three shorter ones.
- Each bar represents one activity with its start and end times.
- Bars are arranged in the order they are selected.
- The gaps after the last activity indicate unused time that cannot accommodate any remaining activity because they all start before the gap ends.
This visual aid is particularly useful when explaining the algorithm to non-technical stakeholders or during code reviews to verify correctness.
C++ Implementation of Activity Selection
The C++ implementation mirrors the Java version exactly in logic, but uses C++ idioms: vector, struct, lambda sort, and range-based loops. The critical part is the custom comparator that sorts by end time.
One common pitfall in C++ is using std::sort with a comparator that does not define a strict weak ordering. Make sure your comparator returns true if a.end < b.end. For ties, you may add a secondary sort by start time to ensure deterministic behaviour.
Here is the complete, production-ready C++ code. It includes a helper struct and a function that returns the selected activities. The time complexity is O(n log n) for sorting, O(n) for selection.
[]) and uses return a.end < b.end;. Using <= can violate strict weak ordering and cause undefined behaviour.C# Implementation of Activity Selection
The C# implementation leverages LINQ for concise sorting and List for dynamic collection. The algorithm is identical: sort by end time, iterate, and select activities that start after or at the last selected end time.
One important detail in C#: the OrderBy method returns a new sorted collection. To avoid copying, you can use List.Sort with a Comparison delegate. However, OrderBy is more readable and acceptable in production for moderate-sized lists.
The code below uses a record struct for immutability and clarity. The >= operator ensures back-to-back activities are allowed.
List.Sort instead of OrderBy to avoid memory allocation overhead. Example: activities.Sort((a, b) => a.End.CompareTo(b.End));OrderBy with Array.Sort to handle 500k events. The algorithm remained O(n log n) but the memory footprint dropped by 40%.>= for overlap check.Weighted Activity Selection — When Greedy Breaks
If each activity has a profit, you want to maximise sum of profits, not count. Greedy fails because a low-profit early-ending activity might block a high-profit one that starts slightly later.
Solution: Dynamic Programming. Sort by end time, then for each activity find the last non-overlapping one with binary search. DP recurrence: dp[i] = max(dp[i-1], profit[i] + dp[p[i]]).
Trade-off: O(n log n) isn't worse than greedy's sort, but O(n) space costs memory. In production, detect weights at runtime and switch algorithms.
- Greedy picks a $10 activity because it ends early, but blocks a $1000 one starting right after
- DP considers both choices: include or skip
- Binary search makes p[i] efficient, keeping overall O(n log n)
Extensions: Multiple Resources and Meeting Rooms II
What if you have k identical rooms instead of one? The problem becomes 'maximise number of activities across k rooms' — also solvable greedily.
Approach: Sort activities by start time (not end!). Use a min-heap of room end times. For each activity, if the earliest-free room finishes ≤ current start, reassign that room; otherwise allocate a new room.
Complexity: O(n log n + n log k). This is the classic 'Meeting Rooms II' problem.
In production, this models multi-threaded job schedulers, multiple operating theatres, or allocating cloud VMs.
Practice Problems on Interval Scheduling
To solidify your understanding of the Activity Selection problem and its variants, work through these 6 curated problems. They range from direct applications to weighted and multi-resource extensions.
- Weighted Job Scheduling (LeetCode 1235) — The direct DP extension. Each job has a profit. Maximize total profit. Requires binary search for previous non-overlapping job.
- Meeting Rooms II (LeetCode 253) — Minimum number of conference rooms required to host all meetings. Uses start-time sort and min-heap.
- Non-overlapping Intervals (LeetCode 435) — Given intervals, find the minimum number to remove to make the rest non-overlapping. Inverts the selection problem.
- Minimum Number of Arrows to Burst Balloons (LeetCode 452) — A twist: find minimum arrows to burst all balloons when arrows burst overlapping intervals at any point. Solvable with similar greedy.
- Maximum Number of Events That Can Be Attended (LeetCode 1353) — Events have start and end days; you can attend at most one per day. Uses a priority queue variant.
- Car Pooling (LeetCode 1094) — Trips with start and end locations and passengers. Determine if you can pick up and drop off all passengers with a given capacity. Uses sweep line / difference array.
Each problem reinforces a different aspect of interval scheduling. Start with the classic (1) if you want to practice the weighted DP, or (2) if you want to extend to multiple resources. All are available on LeetCode with active discussion forums.
Greedy Sort Bug Caused Scheduler to Miss 31% of Viable Slots
- Sort by finish time, not start time — this is the only correct greedy key for activity selection
- Unit tests that only check 'some output exists' don't catch suboptimal greedy bugs
- Add optimality ratio metrics to scheduler output — not just correctness but quality
- Code review greedy algorithms by asking: what does this sort key maximise?
Collections.emptyList(). Add unit test: assertEquals(0, select(Collections.emptyList()).size()).Key takeaways
Common mistakes to avoid
5 patternsSorting by start time instead of finish time
Sorting by duration (shortest job first)
Using >= instead of > for overlap check
Not handling the weighted variant with greedy
Forgetting to handle empty input or single activity edge cases
Collections.emptyList(). Add unit tests for empty input, single activity, and all-overlapping inputs.Interview Questions on This Topic
Given a set of activities with start and end times, how would you select the maximum number of non-overlapping activities? Walk me through your approach.
java
List<Activity> selectActivities(List<Activity> activities) {
activities.sort(Comparator.comparingInt(a -> a.end));
List<Activity> selected = new ArrayList<>();
Activity lastSelected = activities.get(0);
selected.add(lastSelected);
for (int i = 1; i < activities.size(); i++) {
if (activities.get(i).start >= lastSelected.end) {
selected.add(activities.get(i));
lastSelected = activities.get(i);
}
}
return selected;
}
``Frequently Asked Questions
That's Greedy & Backtracking. Mark it forged?
6 min read · try the examples if you haven't