Skip to content

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 O(n2)O(n^2).

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+1 at startstart and -1-1 at endend.

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 / -1 event 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 + 1 fix.
  • 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

sweep_line_template.py
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]]))              # 1
sweep_line_template.py
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]]))              # 1

O(nlogn)O(n \log n) for the sort, then a single O(n)O(n) pass. Space O(n)O(n) 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.

min_rooms_heap.py
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]]))   # 2
min_rooms_heap.py
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]]))   # 2
TimeSpaceBest when
EventsO(nlogn)O(n \log n)O(n)O(n)You want the count, or the whole timeline profile
Min-heap of endsO(nlogn)O(n \log n)O(n)O(n)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

VariantThe event transformCanonical problem
Peak overlap / min rooms+1+1 at start, -1-1 at end253 Meeting Rooms II
Weighted capacity+n+n and -n-n instead of ±1±11094 Car Pooling
Inclusive endpointsRelease at end + 1end + 12406 Divide Intervals
Booking with a limitSweep on each query, or keep a delta map731 · 732
Height profileSweep x-coordinates with a max-heap of heights218 Skyline
Free time across schedulesSweep for gaps where active == 0active == 0759 (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 O(nlogn)O(n \log n). Space O(n)O(n).

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 O(n+1000)O(n + 1000) answer worth mentioning — a difference array over positions:

python
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 True
python
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 True

No 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 10910^9?” — 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 O(nlogn)O(n \log n). Space O(n)O(n).

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 10610^6 — use a difference array?” — yes, O(n+106)O(n + 10^6), 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 O(nlogn)O(n \log n). Space O(n)O(n).

[[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

#ProblemDifficultyThe twist
1094Car PoolingMediumWeighted events; bounded coordinates allow a difference array
2406Divide Intervals Into Minimum Number of GroupsMediumInclusive endpoints — release at end + 1end + 1
253Meeting Rooms IIMedium · PremiumThe canonical statement; half-open endpoints
731My Calendar IIMediumAllow double booking, reject triple — sweep a delta map per query
732My Calendar IIIHardReturn the running peak after each booking; a SortedDictSortedDict of deltas
218The Skyline ProblemHardSweep x-coordinates with a max-heap of active heights
759Employee Free TimeHard · PremiumSweep for the gaps where active == 0active == 0

Interview follow-ups

They askWhat they’re checkingThe answer
“Are the endpoints inclusive?”Whether you ask itIt flips the tie-break: half-open means touching is fine; inclusive means release at end + 1end + 1
“Why does minimum rooms equal peak overlap?”Rigourkk simultaneous meetings force kk rooms (lower bound), and kk rooms always suffice by assigning greedily (upper bound)
“Sweep or heap?”JudgementSame O(nlogn)O(n \log n); events generalise to weights and profiles, the heap tells you which resource is reused
“Coordinates are small and bounded”Recognising the shortcutA difference array over positions gives O(n+range)O(n + \text{range}) with no sorting
“Coordinates up to 10910^9?”The reverseThe difference array dies; the event sweep is unaffected
“Why do drop-offs sort before pickups?”Attention to detailTuple 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?”LimitsA 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 gives 11, inclusive gives 22.
  • Fully nested intervals[[1,10],[2,9],[3,8]][[1,10],[2,9],[3,8]] gives 33; overlap is not just about crossing edges.
  • Identical intervalsnn copies overlap nn times.
  • 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+1 at start, -1-1 at 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 need end + 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 — O(n+range)O(n + \text{range}).
  • 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 coffee

Was this page helpful?

Let us know how we did