Pattern 16 of 20
Intervals
Sort by start or end, then sweep: merge, insert and count overlaps.
When to use it
- Inputs are ranges, meetings or bookings
- Merging overlaps or finding free time
- Counting how many ranges are active at once
The idea
Once intervals are sorted, overlaps only happen between neighbours, so one pass decides everything. Sorting by end time instead of start time is the trick behind maximising non-overlapping ranges.
Typical cost O(n log n) time for the sort, O(n) space for the output.
How it works
Interval problems become easy the moment the intervals are sorted. Sorted by start, overlapping intervals sit next to each other, so a single pass can merge them, count gaps, or insert a new one. Without sorting, every interval must be compared with every other.
Two intervals [a, b] and [c, d] overlap exactly when a <= d and c <= b (or with strict inequalities if touching endpoints do not count). Decide the touching rule up front; it is the most common source of wrong answers.
For 'remove the fewest intervals to make the rest disjoint' or 'schedule the most meetings', sort by end time instead. Taking the interval that finishes first leaves the most room for everything after it.
For 'how many overlap at once' (meeting rooms, maximum concurrent events), sweep over sorted events: +1 at each start and -1 at each end, or keep a min-heap of end times. The peak of the running count is the answer.
How to attack it
- 1Fix the overlap rule, including whether touching endpoints overlap.
- 2Sort by start for merging and inserting, by end for selecting the most disjoint.
- 3Keep a 'current' interval and extend or close it in one pass.
- 4Use a sweep or a heap of end times for maximum concurrency.
- 5Check empty input and a single interval.
Template
A skeleton to adapt, not a solution to paste. Write it from memory a few times.
import heapq
def merge(intervals):
intervals.sort(key=lambda iv: iv[0]) # sort by start
merged = []
for start, end in intervals:
if merged and start <= merged[-1][1]: # overlaps the last one
merged[-1][1] = max(merged[-1][1], end)
else:
merged.append([start, end])
return merged
def max_disjoint(intervals):
intervals.sort(key=lambda iv: iv[1]) # sort by end
count, last_end = 0, float("-inf")
for start, end in intervals:
if start >= last_end: # fits after the last chosen
count += 1
last_end = end
return count
def rooms_needed(intervals):
intervals.sort(key=lambda iv: iv[0])
ends = [] # min-heap of end times in use
for start, end in intervals:
if ends and ends[0] <= start:
heapq.heappop(ends) # reuse the room that freed up
heapq.heappush(ends, end)
return len(ends)Variants
- Merge overlapping
- Sort by start and extend the last merged interval while the next one overlaps it.
- Insert into a sorted list
- Copy intervals that end before the new one, merge those that overlap, then copy the rest; O(n) with no sort.
- Most non-overlapping
- Sort by end and keep each interval that starts after the last kept end; removals are n minus that count.
- Maximum concurrency
- Sweep over start and end events, or keep a min-heap of end times; the peak count is the number of rooms.
- Intersections of two lists
- Two pointers over two sorted lists; advance the one that ends first.
Pitfalls
- Not deciding whether [1, 2] and [2, 3] overlap, then mixing < and <= across the code.
- Forgetting to sort, or sorting by the wrong endpoint for the question.
- Replacing the end with the new end instead of the maximum of the two when merging.
- Mutating the caller's input when sorting in place if the problem forbids it.
- In a sweep, processing starts before ends at the same time, which overcounts rooms.
What it costs
| Operation | Cost |
|---|---|
| Sort | O(n log n) |
| Merge or select in one pass | O(n) after sorting |
| Insert into an already sorted list | O(n) |
| Rooms with a heap or a sweep | O(n log n) |
25 Intervals problems
Easiest first. Problems marked Free can be solved without Pro.
- 1Clash-Free CalendarFreeEasy
- 2Total Alarm TimeFreeEasy
- 3Days Off the CalendarFreeEasy
- 4Group Consecutive BadgesEasy
- 5Painted Fence LengthEasy
- 6Longest Free GapEasy
- 7Shift Coverage CountsEasy
- 8Consolidate RangesMedium
- 9Slot a New Range InMedium
- 10Fewest Meeting RoomsMedium
- 11Cancel the FewestMedium
- 12Fewest Inspection PointsMedium
- 13Carve Out a WindowMedium
- 14Count Essential RangesMedium
- 15Peak Server LoadMedium
- 16Free for EveryoneMedium
- 17Next Available SlotMedium
- 18Self-Contained ChunksMedium
- 19Cover the Whole ShiftMedium
- 20One Event Per DayMedium
- 21Most Profitable GigsHard
- 22Busiest StudioHard
- 23Tightest Covering RangeHard
- 24Stage OutlineHard
- 25Two Checkpoints EachHard
Solve them in the browser, in Python or JavaScript.