core idea
Interval problems are array problems wearing a calendar costume: each element is a [start, end]
pair, and before you write a single comparison, one question decides the whole solution — what
do you sort by. Sort by start and every overlap becomes adjacent, so a single left-to-right
sweep can fold ranges together. Sort by end and the earliest-finishing option becomes the
provably safe greedy pick, so the same kind of sweep can choose among conflicting ranges instead.
Almost everything else in this topic is one of those two sweeps, occasionally with a heap bolted
on to track how many ranges are open at once.
| sub-shape | sort by | goal | typical trigger |
|---|---|---|---|
| merge | start |
fold overlapping ranges into fewer, wider ones | “merge”, “combine”, “insert a meeting” |
| select | end |
keep the largest non-conflicting subset (or count what you’d drop) | “how many can you keep/attend”, “minimum removals” |
when to reach for it
- The input is a list of
[start, end]pairs — bookings, log windows, free/busy blocks, “meetings,” version ranges. - You need to combine overlapping ranges into fewer, wider ones → sort by start, sweep once.
- You need to pick the largest non-conflicting subset, or count the minimum you’d remove → sort by end, greedy sweep.
- You need to know how many ranges are alive at the same instant (rooms, connections, workers) → sort by start, track live count with a heap of end times.
- Brute force compares every pair of intervals —
O(n^2). One sort turns that into a single linear sweep.
universal templates
Merge — sort by start, extend the open run or close it and start a new one:
public int[][] MergeTemplate(int[][] intervals)
{
Array.Sort(intervals, (a, b) => a[0].CompareTo(b[0])); // sort by START — merging only cares what runs into what
var result = new List<int[]> { intervals[0] };
for (int i = 1; i < intervals.Length; i++)
{
var last = result[^1];
if (intervals[i][0] <= last[1]) // current starts before (or exactly when) last ends
last[1] = Math.Max(last[1], intervals[i][1]); // widen the open run to cover both
else
result.Add(intervals[i]); // no overlap — close the run, start a new one
}
return [.. result];
}
Select — sort by end, keep whatever doesn’t conflict with the last thing you kept:
public int SelectTemplate(int[][] intervals)
{
Array.Sort(intervals, (a, b) => a[1].CompareTo(b[1])); // sort by END — greedy needs the earliest finisher first
int kept = 0;
int lastEnd = int.MinValue;
foreach (var iv in intervals)
{
if (iv[0] >= lastEnd) // doesn't conflict with the last interval you kept
{
kept++;
lastEnd = iv[1]; // this is now the interval to beat
}
}
return kept; // "minimum removals" = intervals.Length - kept
}
Every problem below is one of these two skeletons — sometimes with a min-heap layered on top of the merge template’s start-sort to track how many ranges are open at once, instead of merging them.
the one question to ask
Before you write a comparison, ask: “do I need to combine ranges, or choose among them?” Combine → sort by start. Choose → sort by end. Get that backwards and you’ll write a correct-looking sweep that silently returns the wrong number — the proof behind the select half lives in greedy, and the sort itself is sorting’s bread and butter.
problems
Four problems, and every one of them is the merge template, the select template, or the merge template with a heap standing in for the single running interval:
Sort by start; extend the current merged interval or start a new one.
Three phases: all-before, merge-overlapping, all-after.
Sort by END — keep the interval that finishes earliest.
Min-heap of end times = rooms in use; peak heap size is the answer.
cheat sheet — intervals
recognize it
- input is a list of
[start, end](or[start, end)) pairs — meetings, bookings, log windows, version ranges - "merge overlapping" / "combine ranges" → sort by start
- "max non-conflicting" / "minimum removals" → sort by end, greedy select
- "how many rooms/resources needed at once" → sort by start, heap of end times (or the start/end event sweep)
key tricks
- the whole topic is one sort plus one linear sweep — the sort key (start vs end) IS the design decision, make it first
- merge check:
current.Start <= last.End; select check:current.Start >= lastEnd— note the flipped-feeling inequality - touching endpoints (
current.Start == last.End) count as overlap when merging, but do NOT conflict when selecting — the two problems disagree with each other on purpose, check the statement - Meeting Rooms II = the select template's start-sort, with a min-heap of end times standing in for a single
lastEnd - the chronological event-sweep (
starts[]andends[]sorted independently, walked with two pointers) reconstructs the same answer as the heap without payingO(log n)per operation
common bugs
- sorting by the wrong key — merge needs start, select needs end; getting it backwards still compiles and still "runs," just returns a plausible-looking wrong answer
<vs<=at the overlap/conflict check — flips whether touching intervals count, and it's easy to copy the wrong convention from a sibling problem in this same topic- advancing the "kept" tracker (
lastEnd) when you actually dropped an interval — the survivor has to stay the earlier-finishing one, or the greedy guarantee breaks - mutating an interval
int[]in place (last[1] = ...) without noticing it's a live reference back into the caller's own input array