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) is really two events: +1 at start and -1 at end.

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.

  • The +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 + 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 sweep never asks “does A overlap B”. It only counts:

intervalSplit each interval into a +1 and a -1, then just add them upLC 253 · O(n log n)
0–305–1015–206–120561012152030
rooms in use0peak0
events8
setupForget the intervals as objects. Split each into two **events** — a +1 when it starts and a −1 when it ends — then sort all the events by time. The answer is the maximum the running total ever reaches, and no interval is ever compared with another.
1/10

Watch the tie-break at equal times: ends sort before starts, because a room vacated at t is available at t. Getting that backwards inflates the answer by one and is the single most common bug in this pattern.

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.

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

intervals = [[0,30], [5,10], [15,20]] — LC 253’s example. Six events, sorted by time:

timedeltaactivebestwhat just happened
0+111[0,30] starts
5+122[5,10] starts while [0,30] is still running
10−112[5,10] ends
15+122[15,20] starts — ties the peak, does not beat it
20−112[15,20] ends
30−102[0,30] ends; active returns to 0

Answer 2 rooms.

  • The interval identities are gone by the time the loop runs. After the events are built, nothing knows that [5,10] and [15,20] were different meetings — only that the timeline gains and loses occupants. That erasure is the whole trick: an interval problem becomes a running sum.
  • active returns to exactly 0, which is a free correctness check. If it does not, the events are unbalanced — usually a missing -1 push, or a duplicate.
  • best is not the last value, it is the maximum along the way. The peak at t=5 has passed by the time the scan finishes. Returning active instead of best returns 0 on every balanced input — a bug that looks like “my answer is always zero”.
  • The touching case is where the tie-break earns its keep. On [[1,5],[5,9]] the events at t=5 are (5, -1) and (5, +1); Python sorts tuples lexicographically and 1 < 1, so the departure is processed first and the answer is 1. Push the release to end + 1 instead — the inclusive convention — and the same input answers 2. Both are right; which one the problem wants is a reading-comprehension question, not an algorithmic one.
StepCost
Build 2n2n eventsO(n)O(n)
Sort themO(nlogn)O(n \log n) — dominates
Single passO(n)O(n)
SpaceO(n)O(n) for the event list
TotalO(nlogn)O(n \log n) time, O(n)O(n) space
FormulationTimeSpaceWhen it wins
Two sorted arrays (starts, ends, two pointers)O(nlogn)O(n \log n)O(n)O(n)same bound, avoids building tuples; the classic LC 253 answer
Event list (this page)O(nlogn)O(n \log n)O(n)O(n)generalises to weighted deltas and to more than two event kinds
Min-heap of end timesO(nlogn)O(n \log n)O(k)O(k) for k roomswhen you must also know which room, or track per-room state
Difference array / bucket countsO(n+T)O(n + T)O(T)O(T)when times are small bounded integers (TT range) — beats sorting outright
Pairwise overlap checkO(n2)O(n^2)O(1)O(1)never, beyond n < 100

The difference-array row is worth remembering: if timestamps are small integers — hours in a day, years in a range, coordinates up to 10510^5 — you can skip the sort entirely. Add +1 at start and −1 at end in an array of that size and prefix-sum it, which is O(n+T)O(n + T) and often the intended solution for the “car pooling” and “corporate flight bookings” family (LC 1094, LC 1109). That is the same difference-array idea, and a sweep line is its unbounded-coordinates cousin.

VariantThe event transformCanonical problem
Peak overlap / min rooms+1 at start, -1 at end253 Meeting Rooms II
Weighted capacity+n and -n instead of ±11094 Car Pooling
Inclusive endpointsRelease at end + 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 == 0759 (Premium)

Problem. A car with capacity empty seats drives east only. Given trips[i] = [numPassengers, from, to], return True if it is possible to pick up and drop off all passengers without ever exceeding capacity.

Constraints. 1 <= len(trips) <= 1000, 1 <= numPassengers <= 100, 0 <= from < to <= 1000, 1 <= capacity <= 10^5.

Examples. trips = [[2,1,5],[3,3,7]], capacity = 4 gives False · same trips with capacity = 5 gives True

Editorial — approach, complexity, follow-ups

The only change from the basic template is that events carry a weight (numPassengers) rather than ±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]] with capacity 3, 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 False. Because (5, -2) sorts before (5, 3), the drop-off happens first and the answer is correctly True.

Since locations are bounded by 1000, 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

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 <= 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 best updates.

LC 2406 — Divide Intervals Into Minimum Number of Groups · Medium

Section titled “LC 2406 — Divide Intervals Into Minimum Number of Groups · Medium”

Problem. Given 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^5, 1 <= left <= right <= 10^6.

Examples. [[5,10],[6,8],[1,5],[2,3],[1,10]] gives 3 · [[1,3],[5,6],[8,10],[11,13]] gives 1

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] occupies position 5, so it conflicts with [5,10]. Emitting (end, -1) would let the release sort before the arrival and count them as compatible — returning 2 instead of 3 on the first example. Releasing at end + 1 keeps the interval active through end.

[[1,2],[2,3]] returning 2 is the minimal test of this: with half-open handling it would wrongly give 1.

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

Section titled “LC 253 — Meeting Rooms II · Medium · Premium”

Problem. Given meeting time intervals [start, end), return the minimum number of conference rooms required.

Examples. [[0,30],[5,10],[15,20]] gives 2 · [[7,10],[2,4]] gives 1

Editorial — approach, complexity, follow-ups

The answer is the peak overlap, for the lower-bound/upper-bound reason given earlier: k simultaneous meetings need k rooms, and k rooms always suffice.

Time O(nlogn)O(n \log n). Space O(n)O(n).

[[1,5],[5,10],[10,15]] returning 1 is the half-open test: consecutive back-to-back meetings share one room. Contrast with LC 2406, where the same shape of input gives 2 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 0 is worth a guard in the heap version (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 active hits best.

Generated from the problem database, so each entry carries its sheet membership and reported companies. Tick them off as you go — progress is saved in this browser, and the Export button writes it to a file you can keep.

7 problems
0 easy4 medium3 hard

Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.

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 + 1
“Why does minimum rooms equal peak overlap?”Rigourk simultaneous meetings force k rooms (lower bound), and k 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) because -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)
  • Empty input — must return 0; the event version handles it, the heap version needs a guard.
  • Single interval — answer 1.
  • Touching intervals[[1,5],[5,10]]; the case that exposes the endpoint convention. Half-open gives 1, inclusive gives 2.
  • Fully nested intervals[[1,10],[2,9],[3,8]] gives 3; overlap is not just about crossing edges.
  • Identical intervalsn copies overlap n times.
  • Zero-length interval[[1,1]]; sane under inclusive endpoints, degenerate under half-open.
  • All disjoint — answer 1, verifying you take a maximum rather than a total.
  • Unsorted input — always sort; never assume the input is ordered.
pch.quizTag Sweep line — self-check
  1. What does converting intervals into (time, ±1) events actually buy you?

    pch.quizShowAnswer

    B — It discards interval identity and turns the problem into a running sum over a timeline — so 'how many overlap' becomes a prefix maximum rather than a pairwise comparison — After the events are built, nothing knows which meeting was which. That erasure is exactly what collapses an O(n²) pairwise question into one O(n) pass.

  2. Two intervals touch: one ends at 5, another starts at 5. Do they overlap?

    pch.quizShowAnswer

    B — It depends on the convention — half-open [start, end) means no (the template's (5,−1) sorts before (5,+1), so the room frees first), while inclusive [start, end] means yes, which requires pushing the release to end + 1 — On [[1,5],[5,9]] the half-open reading answers 1 and the inclusive reading answers 2. Both are correct algorithms; which the problem wants is a reading question, and getting it backwards is the classic sweep-line bug.

  3. Why does the template return `best` rather than `active`?

    pch.quizShowAnswer

    B — Because `active` is 0 at the end of any balanced input — the peak occurred earlier in the scan, so it must be tracked as a running maximum — Returning `active` gives 0 on every valid input, which presents as 'my answer is always zero'. That `active` lands back on 0 is also a free correctness check for unbalanced events.

  4. Timestamps are guaranteed to be integers from 0 to 1000. What is the better solution?

    pch.quizShowAnswer

    B — A difference array of size 1001: +1 at start, −1 at end, then prefix-sum. O(n + T) with no sort at all — This is the intended solution for the car-pooling and flight-booking family (LC 1094, LC 1109). A sweep line is the version you fall back to when coordinates are unbounded.

  5. When is the min-heap-of-end-times formulation preferable to the event list?

    pch.quizShowAnswer

    B — When you need more than the count — which room a meeting got, or per-room state — since the heap keeps the rooms themselves rather than just a total — Both are O(n log n). The event list generalises better to weighted deltas and extra event types; the heap generalises better when you must identify the resource.

  6. The problem asks for the total time covered by at least one interval, not the peak count. What changes?

    pch.quizShowAnswer

    B — Track the previous event time and accumulate (t − prev) whenever `active > 0` before applying the delta — the sweep is the same, the accumulator changes — Same events, different thing measured between them. Recognising that the sweep is a framework — count, coverage, weighted load — rather than one recipe is the transferable part.

  • Cue — “how many overlap at once”, “minimum rooms/platforms/servers”, “peak load”, or anything asking about a quantity over time rather than which intervals to pick.
  • Do — emit (start, +1) and (end, -1) for every interval, sort, then scan a running active, tracking best = max(best, active).
  • Return best, not activeactive ends at 0 on balanced input.
  • The tie-break is the bug — half-open [s, e): (t,-1) sorts before (t,+1) for free, so touching intervals do not conflict. Inclusive [s, e]: push the release to end + 1.
  • active back to 0 at the end is a free sanity check.
  • CostO(nlogn)O(n \log n) time from the sort, O(n)O(n) space.
  • Small integer timestamps → skip the sort: a difference array plus prefix sum is O(n+T)O(n + T) (LC 1094, LC 1109).
  • Heap variant when you need which resource, not just how many.
  • Not the same as greedy selection — that picks a subset, this measures the timeline.
  • Turn each interval into two events: +1 at start, -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) sorts before (t, 1). Inclusive endpoints need end + 1.
  • Events carry weights just as easily as ±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.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading