Lab 06 — Intervals: Meeting Rooms II (Heap Of Ends + Sweep-Line Alternate)
Goal
Master the two canonical interval algorithms — min-heap of end times and event-based sweep line — applied to the same problem (LC 253). Deliverable solves it both ways, in O(N log N) time, O(N) space, and you can articulate the tie-breaking rule, why one approach is more intuitive while the other generalizes more cleanly to “max concurrent X” problems.
Background Concepts
Sorting by start time as the canonical interval-prep; min-heap of end times tracking active intervals; sweep line as event stream (time, ±1) with stable tie-breaking; the “end before start” tie-break for closed-on-start, open-on-end intervals. Review pattern Intervals and Heap.
Interview Context
Interval problems appear at Meta, Google, and Amazon — and Meeting Rooms II in particular is a top-15 most-asked Medium. The interview signal is whether you can recognize that “min number of rooms” = “max concurrent meetings”, and then compute concurrency either via heap-of-ends or sweep. Strong candidates do one approach correctly; elite candidates do both, articulate the trade-off, and handle the open/closed interval tie-break correctly without prompting.
Problem Statement
Given an array of meeting time intervals intervals[i] = [start_i, end_i], return the minimum number of meeting rooms required.
Constraints
1 ≤ N ≤ 10^40 ≤ start_i < end_i ≤ 10^6- Each interval is half-open
[start, end): a meeting ending at timetand one starting at timetcan share a room.
Clarifying Questions
- Are intervals half-open or closed? (Crucial:
[1,3]and[3,5]— same room or not? Per LC 253, half-open: same room. This dictates the tie-break.) - Can two meetings start at the same time? (Yes — they need separate rooms.)
- Are intervals sorted? (No assumption.)
- Is
start < endstrict? (Per constraints, yes; no zero-duration meetings.) - Are room IDs significant, or just the count? (Just the count.)
Examples
| Input | Output |
|---|---|
[[0,30],[5,10],[15,20]] | 2 |
[[7,10],[2,4]] | 1 |
[[1,5],[5,10],[10,15]] | 1 (chained, half-open) |
[[1,5],[2,5],[5,10]] | 2 (overlap at [2,5]; the [5,10] reuses) |
[[1,2]] | 1 |
Initial Brute Force
For each pair (i, j), count overlaps; max over all time points.
def min_rooms_brute(intervals):
times = sorted(set(t for s, e in intervals for t in (s, e)))
best = 0
for t in times:
count = sum(1 for s, e in intervals if s <= t < e) # half-open
best = max(best, count)
return best
Brute Force Complexity
Time O(N²). Space O(N). At N=10⁴, ~10⁸ ops — borderline; passes in C++ but TLEs in Python.
Optimization Path A — Heap of End Times
Sort intervals by start. Maintain a min-heap of end times for currently active meetings. For each new interval (start, end):
- If the heap’s smallest end ≤ start, that room frees up — pop it.
- Push the new end.
The number of rooms needed = max heap size ever, which equals the final heap size if we don’t pop more than we push (and we don’t, by the invariant). Actually, simpler: rooms = heap size at end, since we only pop when reusing, never net.
import heapq
def min_rooms_heap(intervals):
intervals.sort(key=lambda x: x[0])
heap = []
for s, e in intervals:
if heap and heap[0] <= s:
heapq.heappop(heap) # reuse a room
heapq.heappush(heap, e)
return len(heap)
Optimization Path B — Sweep Line
Convert intervals to events (start, +1), (end, -1). Sort: by time, with end events before start events on ties (because [1,5] and [5,10] share a room). Sweep, tracking max concurrent.
def min_rooms_sweep(intervals):
events = []
for s, e in intervals:
events.append((s, +1))
events.append((e, -1))
events.sort() # (time, +1)>(time, -1): -1 sorts first since -1 < 1
cur = best = 0
for _, delta in events:
cur += delta
best = max(best, cur)
return best
The tie-break is automatic: (t, -1) < (t, +1) because -1 < 1 lexicographically. End events fire before start events at the same t, so a freed room is reused.
Final Expected Approach
Either A or B is acceptable; mention both in the interview.
Data Structures Used
- Heap approach: the input array (sorted) + a min-heap of end times.
- Sweep approach: an events array of size 2N + a single integer counter.
Correctness Argument (Heap)
Invariant: after processing intervals 0..i-1 in sorted-by-start order, heap contains the end times of all rooms that are still in use at time intervals[i-1].start. Equivalently, heap is the multiset of end times of meetings that haven’t ended yet by the time we’d schedule the next one.
When processing (s, e):
- Any heap top
≤ scorresponds to a room whose meeting has ended; it’s reusable. Pop one. - Push
efor the new meeting.
The final len(heap) is the cumulative maximum number of concurrent meetings, since we only pop when a room frees up (so the heap size strictly decreases only when an old room is reused for a new meeting; otherwise, it grows). Equivalently, max-active-at-any-point = max-rooms-ever-needed.
(Note: we only pop one room even if multiple have ended, but that’s fine — each subsequent meeting will pop its own.)
Correctness Argument (Sweep)
A sweep at time t maintains cur = number of intervals whose start ≤ t < end, half-open. The max of cur over all t is the max concurrency, which is the min rooms needed. The tie-break “end before start at the same time” implements the half-open convention: an end at t decrements cur before a start at t increments it, so cur correctly reflects “intervals active at exactly t” with the half-open semantics.
Complexity
Both: time O(N log N) (sort dominates; heap and sweep each O(N log N) and O(N) respectively after the sort). Space O(N).
Implementation Requirements
- Sort by start for the heap approach. By start ascending; tie-break doesn’t matter for the heap (because we always pop if
heap[0] ≤ s, which is correct for either tie-break). - For the sweep approach, the natural tuple sort
(time, delta)withdelta ∈ {-1, +1}already gives the right tie-break; don’t reverse the comparator. - Use a min-heap (Python
heapq, JavaPriorityQueuedefault, C++priority_queue<int, vector<int>, greater<int>>). - Don’t sort start-times and end-times separately into two arrays — that’s a third valid approach (the “two pointers” approach, equivalent to sweep) but make sure you understand it’s distinct from the heap approach.
Tests
- Smoke: the five examples above.
- Unit: all-disjoint intervals → 1; all-identical intervals → N; chain
[1,2],[2,3],[3,4]→ 1. - Edge: N = 1 → 1; intervals all start at 0 → N (all overlap).
- Tie-break:
[[5,10],[10,15]]→ 1 (half-open). If your test returns 2, your sort comparator is wrong. - Adversarial: tournament-bracket —
[[1,4],[2,5],[7,9],[1,5]](mix); validate against brute on small inputs. - Large: N = 10⁴, random intervals; both heap and sweep should run sub-50ms.
Follow-up Questions
- “Return the actual schedule (which interval goes in which room)?” → augment heap entries to
(end, room_id); reuse the poppedroom_idfor the new interval. - “Real-time scheduling: intervals arrive in order, decide room on the fly?” → still works; no offline assumption needed for the heap version (sweep needs all events upfront).
- “Closed intervals
[s, e]instead of half-open?” → flip the tie-break: start before end at the same time, i.e., sort(t, +1)before(t, -1). Or in heap version, changeheap[0] <= stoheap[0] < s. - “Maximum number of overlapping intervals (LC 1851)?” → identical pattern.
- “Insert / delete intervals dynamically?” → balanced BST keyed on start; O(log N) per op (Phase 3).
- “Generalize to weighted intervals?” → DP on intervals (interval scheduling maximization, LC 1235), Phase 3.
Product Extension
Resource allocation in CI/CD: minimum number of build agents required to handle a queue of (start, duration) jobs without delay, given they’re all queued ahead of time. Or: minimum servers needed to handle a known load profile (each request has a known service window). The same heap-of-ends pattern, run on a stream, computes peak concurrency in real time.
Language/Runtime Follow-ups
- Python:
heapqis min-only; for max-heap, negate.heapq.heappop(h)andheapq.heappush(h, x)are O(log N). For sweep,events.sort()on a list of tuples works directly thanks to lexicographic tuple ordering. - Java:
PriorityQueue<Integer>defaults to min-heap. Boxing tax on primitives — for hot loops, useIntPriorityQueuefrom a primitive collections lib. Sort intervals viaArrays.sort(intervals, (a, b) -> a[0] - b[0])— beware overflow (useInteger.compare(a[0], b[0])). - Go:
container/heaprequires implementing theheap.Interface(5 methods). Verbose but flexible. - C++:
std::priority_queue<int, std::vector<int>, std::greater<int>>for min-heap (default is max). Sort withstd::sort. - JS/TS: no built-in heap. Implement one (~30 lines) or use a library. For sweep,
events.sort((a, b) => a[0] - b[0] || a[1] - b[1]). Beware:Array.sortis not stable in all engines for older versions of V8 (it’s stable since ES2019 — so it’s fine on modern Node, but mention it). - Edge: Java’s
PriorityQueue.peek()is O(1);poll()is O(log N). C++’stop()is O(1);pop()is O(log N) but doesn’t return the value — calltop()first.
Common Bugs
- Wrong tie-break in sweep. For half-open intervals, end events must fire before start events at the same time. The natural
(time, ±1)tuple sort gives this for free (since -1 < +1); reversing the comparator breaks it. - Heap comparator on max-heap default (Java’s
PriorityQueuehas a min-heap default; C++’spriority_queuehas a max-heap default — easy to forget which is which). - Sorting by end instead of start in the heap approach — gives wrong room counts.
- Popping heap on
heap[0] < sinstead of<=— for half-open[s, e),<=is correct (a meeting ending atshas ended, the room is free for one starting ats). - Forgetting to push the new end after popping — heap loses an entry, undercount.
- Comparator overflow in Java:
a[0] - b[0]overflows when values are huge; useInteger.compare. - Sweeping events without separating same-time events — fragile; always tie-break explicitly even if the data “happens” to not have ties.
Debugging Strategy
- Trace
[[0,30],[5,10],[15,20]]with the heap approach: sort doesn’t change order. Process[0,30]: heap =[30]. Process[5,10]: 30 > 5, no pop; push 10; heap =[10, 30]. Process[15,20]: 10 ≤ 15, pop; push 20; heap =[20, 30]. Final size = 2. ✓ - For half-open tie-break: trace
[[5,10],[10,15]]. Heap:[10]; second interval,10 <= 10→ pop, push 15; heap =[15]. Final size 1. ✓ If you used<, you’d get 2. - For sweep: events
[(5,+1),(10,-1),(10,+1),(15,-1)]sorted:[(5,+1),(10,-1),(10,+1),(15,-1)]. Sweep:curbecomes 1, 0, 1, 0; max 1. ✓ - Validate against brute on 30 random inputs of size ≤ 50.
Mastery Criteria
- Recognized “min rooms” / “max concurrent intervals” within 30 seconds.
- Wrote both heap and sweep solutions within 10 minutes total.
- Articulated the half-open tie-break before coding.
- Handled the language-specific heap default (min vs max) without bugs.
- Identified the connection to LC 1851 (max overlapping intervals) and LC 1094 (Car Pooling).
- Solved LC 56 (Merge Intervals) and LC 57 (Insert Interval) within a week, observing the same sort-by-start skeleton.