Sweep Line and Event Counting
Merge Intervals works by comparing intervals to each other. That is the right tool when you need to combine them. But when the question is “how many overlap at the busiest moment?”, comparing pairs is the wrong frame — and .
The sweep line reframes it. Stop thinking about intervals as objects and think about the moments they change something:
An interval
[start, end)[start, end)is really two events:+1+1atstartstartand-1-1atendend.
Sort all the events by time, walk through them keeping a running total, and that total is the number of active intervals at every instant. The maximum it reaches is your answer.
What you’ll learn
- The
+1 / -1+1 / -1event transform, and why sorting events beats comparing intervals. - The tie-breaking rule at a shared timestamp — which decides whether touching intervals count as overlapping.
- Inclusive vs. half-open endpoints, and the
end + 1end + 1fix. - When a min-heap of end times is the better shape than events.
- Three real LeetCode problems solved in the browser: 1094, 2406, 253.
The cue
The template
def max_overlap(intervals):
"""Peak number of simultaneously active [start, end) intervals."""
events = []
for start, end in intervals:
events.append((start, 1)) # one more active from here
events.append((end, -1)) # one fewer from here
events.sort() # by time; -1 before +1 at equal times
active = best = 0
for _time, delta in events:
active += delta
best = max(best, active)
return best
print(max_overlap([[0, 30], [5, 10], [15, 20]])) # 2
print(max_overlap([[7, 10], [2, 4]])) # 1def max_overlap(intervals):
"""Peak number of simultaneously active [start, end) intervals."""
events = []
for start, end in intervals:
events.append((start, 1)) # one more active from here
events.append((end, -1)) # one fewer from here
events.sort() # by time; -1 before +1 at equal times
active = best = 0
for _time, delta in events:
active += delta
best = max(best, active)
return best
print(max_overlap([[0, 30], [5, 10], [15, 20]])) # 2
print(max_overlap([[7, 10], [2, 4]])) # 1for the sort, then a single pass. Space for the events.
The heap alternative
For “minimum rooms” there is a second formulation that some find more intuitive: sort intervals by start, and keep a min-heap of end times of the currently-occupied rooms.
import heapq
def min_rooms(intervals):
if not intervals:
return 0
intervals.sort() # by start time
ends = [] # min-heap of end times in use
for start, end in intervals:
if ends and ends[0] <= start: # earliest-finishing room is free
heapq.heappop(ends) # reuse it
heapq.heappush(ends, end)
return len(ends) # rooms ever needed at once
print(min_rooms([[0, 30], [5, 10], [15, 20]])) # 2import heapq
def min_rooms(intervals):
if not intervals:
return 0
intervals.sort() # by start time
ends = [] # min-heap of end times in use
for start, end in intervals:
if ends and ends[0] <= start: # earliest-finishing room is free
heapq.heappop(ends) # reuse it
heapq.heappush(ends, end)
return len(ends) # rooms ever needed at once
print(min_rooms([[0, 30], [5, 10], [15, 20]])) # 2| Time | Space | Best when | |
|---|---|---|---|
| Events | You want the count, or the whole timeline profile | ||
| Min-heap of ends | You need to know which room, or to track per-room state |
Same complexity. The event version generalises better (it handles weights, as in LC 1094, and produces the full profile); the heap version tells you which resource is being reused, which matters if rooms have identities.
The variant map
| Variant | The event transform | Canonical problem |
|---|---|---|
| Peak overlap / min rooms | +1+1 at start, -1-1 at end | 253 Meeting Rooms II |
| Weighted capacity | +n+n and -n-n instead of ±1±1 | 1094 Car Pooling |
| Inclusive endpoints | Release at end + 1end + 1 | 2406 Divide Intervals |
| Booking with a limit | Sweep on each query, or keep a delta map | 731 · 732 |
| Height profile | Sweep x-coordinates with a max-heap of heights | 218 Skyline |
| Free time across schedules | Sweep for gaps where active == 0active == 0 | 759 (Premium) |
Practice — real LeetCode problems
LC 1094 — Car Pooling · Medium
Problem. A car with capacitycapacity empty seats drives east only. Given
trips[i] = [numPassengers, from, to]trips[i] = [numPassengers, from, to], return TrueTrue if it is possible to
pick up and drop off all passengers without ever exceeding capacity.
Constraints. 1 <= len(trips) <= 10001 <= len(trips) <= 1000,
1 <= numPassengers <= 1001 <= numPassengers <= 100, 0 <= from < to <= 10000 <= from < to <= 1000,
1 <= capacity <= 10^51 <= capacity <= 10^5.
Examples. trips = [[2,1,5],[3,3,7]], capacity = 4trips = [[2,1,5],[3,3,7]], capacity = 4 gives FalseFalse ·
same trips with capacity = 5capacity = 5 gives TrueTrue
Editorial — approach, complexity, follow-ups
The only change from the basic template is that events carry a weight
(numPassengersnumPassengers) rather than ±1±1. The rolling sum is then occupancy rather
than a count.
Time . Space .
The third test case, [[2,1,5],[3,5,7]][[2,1,5],[3,5,7]] with capacity 33, is the one that
matters: trip 1 carries 2 passengers to location 5, and trip 2 picks up 3 at
location 5. If the pickup were processed first, occupancy would momentarily
hit 5 and wrongly return FalseFalse. Because (5, -2)(5, -2) sorts before (5, 3)(5, 3),
the drop-off happens first and the answer is correctly TrueTrue.
Since locations are bounded by 10001000, there is an even simpler
answer worth mentioning — a difference array over positions:
delta = [0] * 1001
for num, start, end in trips:
delta[start] += num
delta[end] -= num
onboard = 0
for d in delta:
onboard += d
if onboard > capacity:
return False
return Truedelta = [0] * 1001
for num, start, end in trips:
delta[start] += num
delta[end] -= num
onboard = 0
for d in delta:
onboard += d
if onboard > capacity:
return False
return TrueNo sorting at all. This is the same relationship as
prefix sums and difference arrays
— when the coordinate space is small and bounded, bucket it directly instead
of sorting events. Recognising that from the to <= 1000to <= 1000 constraint is the
kind of thing that reads well.
Follow-ups you should expect: “What if the car could go both ways?” —
the timeline no longer orders the trips, so the sweep breaks. “Coordinates up
to ?” — the difference array dies, the event sweep survives. “Return
the location of peak occupancy?” — record the timestamp when bestbest updates.
LC 2406 — Divide Intervals Into Minimum Number of Groups · Medium
Problem. Given intervals[i] = [left, right]intervals[i] = [left, right] (both endpoints
inclusive), split them into the minimum number of groups such that no
two intervals in the same group intersect. Return that minimum.
Constraints. 1 <= len(intervals) <= 10^51 <= len(intervals) <= 10^5,
1 <= left <= right <= 10^61 <= left <= right <= 10^6.
Examples. [[5,10],[6,8],[1,5],[2,3],[1,10]][[5,10],[6,8],[1,5],[2,3],[1,10]] gives 33 ·
[[1,3],[5,6],[8,10],[11,13]][[1,3],[5,6],[8,10],[11,13]] gives 11
Editorial — approach, complexity, follow-ups
Two intervals can share a group exactly when they do not intersect, so the minimum number of groups is the peak overlap — the argument in the note above.
Time . Space .
The whole problem is the endpoint convention. These intervals are
inclusive: [1,5][1,5] occupies position 5, so it conflicts with [5,10][5,10].
Emitting (end, -1)(end, -1) would let the release sort before the arrival and count
them as compatible — returning 22 instead of 33 on the first example.
Releasing at end + 1end + 1 keeps the interval active through endend.
[[1,2],[2,3]][[1,2],[2,3]] returning 22 is the minimal test of this: with half-open
handling it would wrongly give 11.
Follow-ups you should expect: “Which intervals go in which group?” — use the min-heap formulation; the heap slot you reuse identifies the group. “Coordinates up to — use a difference array?” — yes, , a reasonable trade at this size and the same insight as LC 1094. “Are the groups unique?” — no, only the count is; many valid partitions exist.
LC 253 — Meeting Rooms II · Medium · Premium
Problem. Given meeting time intervals [start, end)[start, end), return the minimum
number of conference rooms required.
Examples. [[0,30],[5,10],[15,20]][[0,30],[5,10],[15,20]] gives 22 ·
[[7,10],[2,4]][[7,10],[2,4]] gives 11
Editorial — approach, complexity, follow-ups
The answer is the peak overlap, for the lower-bound/upper-bound reason given
earlier: kk simultaneous meetings need kk rooms, and kk rooms always
suffice.
Time . Space .
[[1,5],[5,10],[10,15]][[1,5],[5,10],[10,15]] returning 11 is the half-open test: consecutive
back-to-back meetings share one room. Contrast with LC 2406, where the same
shape of input gives 22 because endpoints are inclusive. Same pattern,
opposite convention — which is exactly why “are the endpoints inclusive?”
is a genuine clarifying question rather than pedantry.
The empty-input case returning 00 is worth a guard in the heap version
(intervals[0]intervals[0] would raise); the event version handles it naturally since
the loop simply never runs.
Follow-ups you should expect: “Which room does each meeting get?” — the
heap version: pair each end time with a room id, and reuse the popped id.
“Meeting Rooms I (LC 252)?” — just whether any two overlap: sort by start
and check adjacent pairs, or ask whether the peak exceeds 1. “What if rooms
have capacities or features?” — no longer a pure sweep; becomes a matching
or flow problem. “Return the busiest time window?” — track the timestamps
where activeactive hits bestbest.
LeetCode problem set
| # | Problem | Difficulty | The twist |
|---|---|---|---|
| 1094 | Car Pooling | Medium | Weighted events; bounded coordinates allow a difference array |
| 2406 | Divide Intervals Into Minimum Number of Groups | Medium | Inclusive endpoints — release at end + 1end + 1 |
| 253 | Meeting Rooms II | Medium · Premium | The canonical statement; half-open endpoints |
| 731 | My Calendar II | Medium | Allow double booking, reject triple — sweep a delta map per query |
| 732 | My Calendar III | Hard | Return the running peak after each booking; a SortedDictSortedDict of deltas |
| 218 | The Skyline Problem | Hard | Sweep x-coordinates with a max-heap of active heights |
| 759 | Employee Free Time | Hard · Premium | Sweep for the gaps where active == 0active == 0 |
Interview follow-ups
| They ask | What they’re checking | The answer |
|---|---|---|
| “Are the endpoints inclusive?” | Whether you ask it | It flips the tie-break: half-open means touching is fine; inclusive means release at end + 1end + 1 |
| “Why does minimum rooms equal peak overlap?” | Rigour | kk simultaneous meetings force kk rooms (lower bound), and kk rooms always suffice by assigning greedily (upper bound) |
| “Sweep or heap?” | Judgement | Same ; events generalise to weights and profiles, the heap tells you which resource is reused |
| “Coordinates are small and bounded” | Recognising the shortcut | A difference array over positions gives with no sorting |
| “Coordinates up to ?” | The reverse | The difference array dies; the event sweep is unaffected |
| “Why do drop-offs sort before pickups?” | Attention to detail | Tuple comparison: (t, -1) < (t, 1)(t, -1) < (t, 1) because -1 < 1-1 < 1 — the correct behaviour falls out for free |
| “Can you stream the intervals?” | Limits | A sweep needs all events sorted up front; for online queries keep a sorted delta map (LC 731/732) |
Edge-case checklist
- Empty input — must return
00; the event version handles it, the heap version needs a guard. - Single interval — answer
11. - Touching intervals —
[[1,5],[5,10]][[1,5],[5,10]]; the case that exposes the endpoint convention. Half-open gives11, inclusive gives22. - Fully nested intervals —
[[1,10],[2,9],[3,8]][[1,10],[2,9],[3,8]]gives33; overlap is not just about crossing edges. - Identical intervals —
nncopies overlapnntimes. - Zero-length interval —
[[1,1]][[1,1]]; sane under inclusive endpoints, degenerate under half-open. - All disjoint — answer
11, verifying you take a maximum rather than a total. - Unsorted input — always sort; never assume the input is ordered.
Recap
- Turn each interval into two events:
+1+1at start,-1-1at end. Sort, scan, keep a running total — that total is the live overlap. - Minimum rooms/groups = peak overlap. Be ready to prove both bounds.
- The tie-break is the pattern’s one real subtlety. Half-open intervals
work for free because
(t, -1)(t, -1)sorts before(t, 1)(t, 1). Inclusive endpoints needend + 1end + 1. - Events carry weights just as easily as
±1±1(LC 1094). - When coordinates are small and bounded, a difference array skips the sort entirely — .
- A min-heap of end times is the equivalent formulation, and is better when you need to know which resource is reused.
- Merging intervals outputs intervals; sweeping outputs a count. Choose by what the answer looks like.
Next: Greedy Interval Scheduling — why sorting by end time is the right move when you want to keep as many intervals as possible.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
