Skip to main content

Line Sweeps

Trigger: how many are live at once, or the running value at each point. Turn each interval into a +1 at its start and a -1 at its end, sort the events, and read the running total. Which question is it asking?

Depth: counting overlaps at a point​

What depth means​

Define the depth at a time t as how many intervals contain t:

depth = lambda t: sum(1 for s, e in intervals if s <= t < e)

Minimum meeting rooms is exactly max(depth(t) for all t), because at the busiest instant every one of those meetings is mid-flight and each needs its own room, and at no instant does anything need more. So the whole problem is: find the maximum of depth over the timeline. The timeline is continuous, so you cannot try every t.

Why only 2n points matter​

depth is a step function. It can only change at a time where an interval starts or ends, because those are the only moments an interval enters or leaves the "contains t" set. Between two consecutive endpoints nothing changes, so the whole continuous timeline collapses to the 2n endpoints, and the maximum is guaranteed to be attained at one of them.

That is the sweep line: walk the endpoints left to right and maintain depth as you go, rather than recomputing it from scratch at each one.

Maintaining depth incrementally​

Recomputing depth(t) at each of the 2n endpoints would be O(n²). But crossing an endpoint changes depth by exactly one, and you already know the direction: a start adds an interval, an end removes one. So tag each endpoint with its delta - +1 for a start, -1 for an end - sort the tagged endpoints by time, and depth becomes a running total that costs O(1) per event.

def min_rooms(intervals):
events = []
for start, end in intervals:
events.append((start, 1)) # an interval opens here
events.append((end, -1)) # an interval closes here
events.sort(key=lambda e: (e[0], e[1])) # by time; at equal times, -1 before +1
 
concurrent = peak = 0
for time, delta in events:
concurrent += delta # concurrent == depth just after `time`
peak = max(peak, concurrent)
return peak

concurrent is the depth immediately after the current event, and peak is the running maximum, so the loop's invariant is exactly "the answer so far." Everything is O(n log n) for the sort plus O(n) for the sweep, and O(n) space for the event list.

GOTCHASort ties: why the key is (e[0], e[1]) and not just e[0]
0510152030[0, 30][5, 10][15, 20]+1+1-1+1-1-1012depthpeak = 2 rooms
Meetings [0,30], [5,10] and [15,20]. Depth rises at each start and falls at each end, and its maximum over the whole timeline is 2 - so two rooms. Note the maximum is reached twice, at t=5 and again at t=15.

Step it yourself - the sweep line moves one event at a time, bars light up while they are open, and the depth chart draws itself only as far as the sweep has reached:

visualization loads as you reach it
Python
def min_rooms(intervals):
events = []
for start, end in intervals:
events.append((start, 1))
events.append((end, -1))
events.sort(key=lambda e: (e[0], e[1]))
 
concurrent = peak = 0
for time, delta in events:
concurrent += delta
peak = max(peak, concurrent)
return peak

Two other shapes compute the exact same peak, and are worth recognizing since solutions online use all three interchangeably:

ApproachWhat it tracksWhen it reads clearer
Sweep line (events)a running +1/-1 total over sorted event timesthe answer is framed as "concurrency at a point", or you need every intermediate count, not just the peak
Min-heap of end timesthe soonest-freeing room, popped whenever it is free before the next meeting startsthe answer is framed as "rooms" or "resources" - it's the most direct translation of the real-world allocation
Two sorted pointers (starts / ends)a start-pointer and an end-pointer racing through two separately-sorted arraysyou only have the raw intervals and want to avoid building a heap or an explicit event list at all
import heapq
 
def min_rooms_heap(intervals):
intervals.sort(key=lambda x: x[0])
heap = [] # end times of rooms currently in use
for start, end in intervals:
if heap and heap[0] <= start:
heapq.heappop(heap) # the earliest-freeing room is free by `start` - reuse it
heapq.heappush(heap, end)
return len(heap) # rooms still occupied = rooms needed
 
def min_rooms_two_pointers(intervals):
starts = sorted(s for s, _ in intervals)
ends = sorted(e for _, e in intervals)
rooms = peak = 0
s = e = 0
while s < len(starts):
if starts[s] < ends[e]: # a meeting starts before the earliest one ends
rooms += 1
peak = max(peak, rooms)
s += 1
else: # the earliest meeting ends first - free a room
rooms -= 1
e += 1
return peak

The heap version never grows past the number of rooms currently in use, so it's the tightest of the three on space; the two-pointer version needs no heap or event tuples at all, just two sorted arrays - both compute the same peak as the event sweep above. The heap version is also the first step toward problems that ask which interval to serve next, not just how many are live - see admit, expire, serve.

The peak​

The sweep above, applied: the running total is how many intervals are active at that instant, and the maximum it ever reaches is the answer whenever the question is "how many at once".

253, 1094 and 2406 also have a min-heap solution. It computes the same peak but also knows which intervals are open and which frees up first, so it extends to deciding which one to serve: admit / expire / serve.

253. Meeting Rooms II​

253

Meeting Rooms II

LeetCode
Medium

Line Sweep

FIG. 253 MEETING ROOMS II● INTERACTIVE
visualization loads as you reach it
class Solution:
def minMeetingRooms(self, intervals: List[List[int]]) -> int:
def streamEvents(intervals):
events = []
for start, end in intervals:
events.extend([(start, 1), (end, -1)])
return sorted(events)
 
active = maxi = 0
for point, curr in streamEvents(intervals):
active += curr
maxi = max(maxi, active)
return maxi
top ↑

1094. Car Pooling​

1094

Car Pooling

LeetCode
Medium

Line Sweep

FIG. 1094 CAR POOLING SWEEP● INTERACTIVE
visualization loads as you reach it
class Solution:
def carPooling(self, trips: List[List[int]], capacity: int) -> bool:
def streamEvents(intervals):
events = []
for num, start, end in intervals:
events.extend([(start, num), (end, -num)])
return sorted(events)
 
active = 0
for point, curr in streamEvents(trips):
active += curr
if active > capacity:
return False
return True
top ↑

2406. Divide Intervals Into Minimum Number of Groups​

2406

Divide Intervals Into Minimum Number of Groups

LeetCode
Medium

Line Sweep

FIG. 2406 DIVIDE INTERVALS SWEEP● INTERACTIVE
visualization loads as you reach it
class Solution:
def minGroups(self, intervals: List[List[int]]) -> int:
def streamEvents(intervals):
line = []
for a, b in intervals:
line.extend([[a, 1], [b + 1, -1]])
return sorted(line)
 
active = maxi = 0
for point, curr in streamEvents(intervals):
active += curr
maxi = max(maxi, active)
return maxi
top ↑

1419. Minimum Number of Frogs Croaking​

1419

Minimum Number of Frogs Croaking

LeetCode
Medium

Line Sweep + Counting Sort

FIG. 1419 MIN FROGS BUCKETS● INTERACTIVE
visualization loads as you reach it
class Solution:
def minNumberOfFrogs(self, croakOfFrogs: str) -> int:
buckets = [0] * (len(croakOfFrogs))
counter = collections.defaultdict(int)
croak = "croak"
maxi = active = 0
for idx, char in enumerate(croakOfFrogs):
if char == "c":
buckets[idx] += 1
elif char == "k":
buckets[idx] -= 1
active += buckets[idx]
counter[char] += 1
maxi = max(maxi, active)
if not (counter["c"] >= counter["r"] >= counter["o"] >= counter["a"] >= counter["k"]):
return -1
return maxi if counter["c"] == counter["k"] else -1
top ↑

1854. Maximum Population Year​

1854

Maximum Population Year

LeetCode
Easy

Line Sweep

FIG. 1854 MAXIMUM POPULATION YEAR SWEEP● INTERACTIVE
visualization loads as you reach it
class Solution:
def maximumPopulation(self, logs: List[List[int]]) -> int:
def streamEvents(intervals):
line = []
for start, end in intervals:
line.extend([[start, 1], [end, -1]])
return sorted(line, key=lambda i: i)
 
maxi = [-1, 1930]
active = 0
for point, curr in streamEvents(logs):
active += curr
# print(point, curr)
if maxi[0] < active:
maxi = (active, point)
return maxi[1]
top ↑

2779. Maximum Beauty of an Array After Applying Operation​

2779

Maximum Beauty of an Array After Applying Operation

LeetCode
Medium

Line Sweep

FIG. MAXIMUM BEAUTY OF AN ARRAY AFTER APPLYING OPERATION● INTERACTIVE
visualization loads as you reach it
class Solution:
def maximumBeauty(self, nums: List[int], k: int) -> int:
def streamEvents(interval):
line = []
for i in interval:
line.extend([[i - k, 1], [i + k + 1, -1]])
return sorted(line)
 
active = 0
maxi = 0
for point, curr in streamEvents(nums):
active += curr
maxi = max(maxi, active)
return maxi
top ↑

Every point​

Same +1/-1 events, but the peak is not enough: the running value is needed at every position, at every stretch between event points, or at a set of query points. When the coordinates are small indices this is a difference array; when they are sparse, sort the event points (and the queries) and read the running value as the sweep passes each one.

370. Range Addition​

370

Range Addition

LeetCode
Medium

Line Sweep

FIG. 370 RANGE ADDITION SWEEP● INTERACTIVE
visualization loads as you reach it
class Solution:
def getModifiedArray(self, length: int, updates: List[List[int]]) -> List[int]:
def streamEvents(intervals):
line = []
for start, end, weight in intervals:
line.extend([[start, weight], [end + 1, -weight]])
return sorted(line)
 
res = [0] * (length + 1)
prev = None
active = 0
for point, curr in streamEvents(updates):
start = point if prev is None else prev
for i in range(start, point + 1):
res[i] = active
active += curr
res[point] = active
prev = point
return res[:length]
top ↑

2381. Shifting Letters II​

2381

Shifting Letters II

LeetCode
Medium

Line Sweep + Counting Sort

FIG. 2381 SHIFTING LETTERS II● INTERACTIVE
visualization loads as you reach it
class Solution:
def shiftingLetters(self, s: str, shifts: List[List[int]]) -> str:
buckets = [0] * (len(s) + 1)
for start, end, direction in shifts:
direction = -1 if direction == 0 else 1
buckets[start] += direction
buckets[end + 1] -= direction
ans = []
alphabets = "abcdefghijklmnopqrstuvwxyz"
alphabet_map = {j: i for i, j in enumerate(alphabets)}
active = 0
for point, curr in enumerate(buckets[:-1]):
active += curr
char = alphabets[(alphabet_map[s[point]] + active) % 26]
ans.append(char)
return "".join(ans)
top ↑

2237. Count Positions on Street With Required Brightness​

2237

Count Positions on Street With Required Brightness

LeetCode
Medium

Line Sweep + Counting Sort

FIG. 2237 COUNT POSITIONS ON STREET WITH REQUIRED BRIGHTNESS● INTERACTIVE
visualization loads as you reach it
class Solution:
def meetRequirement(self, n: int, lights: List[List[int]], requirement: List[int]) -> int:
buckets = [0] * (n + 2)
for center, rang in lights:
start = max(0, center - rang)
end = min(n - 1, center + rang)
buckets[start] += 1
buckets[end + 1] -= 1
active = 0
count = 0
for point, curr in enumerate(buckets[:-2]):
active += curr
if active >= requirement[point]:
count += 1
return count
top ↑

1943. Describe the Painting​

1943

Describe the Painting

LeetCode
Medium

Line Sweep

FIG. DESCRIBE THE PAINTING● INTERACTIVE
visualization loads as you reach it
class Solution:
def splitPainting(self, segments: List[List[int]]) -> List[List[int]]:
def streamEvents(intervals):
line = []
for start, end, weight in intervals:
line.extend([[start, weight], [end, -weight]])
return sorted(line)
 
ans = []
active = 0
prev = None
for point, curr in streamEvents(segments):
if prev and point != prev and active:
ans.append([prev, point, active])
prev = point
active += curr
return ans
top ↑

2251. Number of Flowers in Full Bloom​

2251

Number of Flowers in Full Bloom

LeetCode
Hard

Line Sweep + Sorted Queries

FIG. 2251 NUMBER OF FLOWERS IN FULL BLOOM● INTERACTIVE
visualization loads as you reach it
class Solution:
def fullBloomFlowers(self, flowers: List[List[int]], people: List[int]) -> List[int]:
def streamEvents(interval):
events = []
for start, end in flowers:
events.extend([[start, 1], [end + 1, -1]])
return sorted(events)
 
people_sorted = sorted(people)
active = 0
prev = 0
ans = {}
finder = 0
for point, curr in streamEvents(flowers):
while finder < len(people_sorted) and prev <= people_sorted[finder] < point:
ans[people_sorted[finder]] = active
finder += 1
active += curr
prev = point
return [ans.get(i, 0) for i in people]
top ↑