Scheduling, Intervals & the Sweep
Interval and scheduling problems are where heaps stop being a data-structure exercise and start being the obvious tool. The recurring shape: events happen over time, resources are occupied and then released, and you must always know which resource frees up first. “Frees up first” is a minimum query over a changing set — a min-heap, every time.
Meeting Rooms II: The Archetype
Given intervals [start, end), find the minimum number of rooms needed so that no two overlapping meetings share a room.
The heap formulation is almost a restatement of the problem. Sort meetings by start time and keep a min-heap of the end times of currently occupied rooms. The root is the room that frees soonest — the only room worth checking:
#include <queue>
#include <vector>
#include <algorithm>
int minMeetingRooms(std::vector<std::vector<int>>& meetings) {
if (meetings.empty()) return 0;
std::sort(meetings.begin(), meetings.end()); // by start time
std::priority_queue<int, std::vector<int>, std::greater<int>> endTimes;
for (const auto& m : meetings) {
if (!endTimes.empty() && endTimes.top() <= m[0])
endTimes.pop(); // a room freed in time: reuse it
endTimes.push(m[1]); // occupy a room until m[1]
}
return (int)endTimes.size(); // rooms held simultaneously
}
Why checking only the root suffices: if the earliest-ending meeting has not finished, no other room is free either. One comparison decides between reuse and allocation.
And why the answer is the final heap size: the heap only ever grows when no room could be reused, so its maximum size equals the peak concurrency — and because we pop at most one per iteration, the size never shrinks below that peak. O(n log n), dominated by the sort.
<= when a meeting ending at time t lets another start at t, and < when it does not. This one character is the difference between accepting and failing on the boundary test, and problem statements are often vague about it. Decide explicitly rather than guessing.
The Alternative: Sweep Line
The same problem without a heap. Split each interval into two events, sort them all, and sweep:
int minMeetingRooms(std::vector<std::vector<int>>& meetings) {
std::vector<std::pair<int,int>> events; // (time, +1 start / -1 end)
for (const auto& m : meetings) {
events.push_back({m[0], +1});
events.push_back({m[1], -1});
}
std::sort(events.begin(), events.end()); // ends sort before starts at equal time
int cur = 0, best = 0;
for (const auto& e : events) { cur += e.second; best = std::max(best, cur); }
return best;
}
Sorting (time, delta) pairs puts -1 before +1 at identical times, which encodes the half-open convention for free. This version is simpler and often faster.
So when is the heap worth it? When you need to know which resource, not merely how many. The sweep gives a count; the heap gives you the actual room, so you can assign an identifier, attach state to it, or report the schedule. Problems that ask you to output an assignment need the heap.
Task Scheduler with Cooldown
Identical tasks must be separated by at least n intervals of cooldown; minimise total time. This one needs the two-container arrangement from Part 9: a max-heap of what is runnable and a queue of what is cooling down.
#include <queue>
#include <unordered_map>
int leastInterval(std::vector<char>& tasks, int n) {
std::unordered_map<char, int> count;
for (char c : tasks) ++count[c];
std::priority_queue<int> ready; // max-heap of remaining counts
for (const auto& kv : count) ready.push(kv.second);
std::queue<std::pair<int,int>> cooling; // (remaining count, time it becomes ready)
int time = 0;
while (!ready.empty() || !cooling.empty()) {
++time;
if (!cooling.empty() && cooling.front().second == time) {
ready.push(cooling.front().first); // cooldown expired
cooling.pop();
}
if (!ready.empty()) {
int remaining = ready.top() - 1;
ready.pop();
if (remaining > 0) cooling.push({remaining, time + n + 1});
}
// else: idle tick, nothing runnable
}
return time;
}
Greedily running the most frequent remaining task is what makes this optimal — the bottleneck is always the task with the highest count, so every tick spent on anything else while it is available wastes an opportunity to space it out. The max-heap enforces that choice, and the FIFO queue works because cooldowns all have the same duration and therefore expire in insertion order.
Single-Threaded CPU
Tasks have an enqueue time and a duration. The CPU always picks the shortest available task, breaking ties by index. Two structures again: sort by availability, and hold the available set in a heap keyed by (duration, index).
std::vector<int> getOrder(std::vector<std::vector<int>>& tasks) {
int n = (int)tasks.size();
std::vector<int> idx(n);
for (int i = 0; i < n; ++i) idx[i] = i;
std::sort(idx.begin(), idx.end(),
[&](int a, int b) { return tasks[a][0] < tasks[b][0]; });
using Job = std::pair<int,int>; // (duration, original index)
std::priority_queue<Job, std::vector<Job>, std::greater<Job>> ready;
std::vector<int> order;
long long now = 0;
int i = 0;
while ((int)order.size() < n) {
while (i < n && tasks[idx[i]][0] <= now) { // admit everything that has arrived
ready.push({tasks[idx[i]][1], idx[i]});
++i;
}
if (ready.empty()) { // CPU idle: jump to the next arrival
now = tasks[idx[i]][0];
continue;
}
auto [dur, id] = ready.top();
ready.pop();
now += dur;
order.push_back(id);
}
return order;
}
Two things to steal. The time jump when the ready set is empty — advancing one tick at a time would be O(max_time) and time values are often up to 109. And now as long long, since n durations each up to 109 overflow int comfortably.
The Template
Nearly every scheduling problem in this family is:
- Sort by the time things become available (start time, enqueue time, arrival).
- Keep a heap of active or available items, keyed by whatever the selection rule is (end time, duration, profit, count).
- Advance a clock. At each step, admit newly available items into the heap, then take the heap's root.
- Jump, do not tick. When nothing is available, skip straight to the next event time.
The two-sorted-orders structure — one for availability, one for priority — is the signature. That is exactly why one sort plus one heap keeps appearing: you need two different orderings of the same data at the same time.
Practice
- Meeting Rooms II medium — the archetype; write both the heap and the sweep-line version.
- Task Scheduler medium — heap plus cooldown queue; there is also an O(n) formula solution worth deriving.
- Single-Threaded CPU medium — note the time jump and the overflow.
- Process Tasks Using Servers medium — two heaps, free servers and busy servers.
- Car Pooling medium — capacity as a sweep; compare with the heap version.
- Maximum Number of Events That Can Be Attended medium — greedy with a heap of deadlines.