Interval scheduling
Greedy interval problems: activity selection, merging, minimum meeting rooms, interval covering, minimum arrows and the minimum number of intervals to remove.
Learning goals
- Pick the right sort key (end, start or both) for each interval problem
- Solve activity selection, covering and arrow problems in O(n log n)
- Merge overlapping intervals and count simultaneous meetings
- Reduce 'remove the fewest intervals' to activity selection
Prerequisites
Calendars, ranges of IDs, booking systems and schedulers all reduce to intervals, and almost every interval problem is solved by one sort followed by one pass, once you know which sort.
Activity selection
Sort by **end time** and take every interval whose start is at or after the last chosen end. Run the “Earliest finish (optimal)” preset: of 8 intervals, the algorithm chooses 3, and the log shows each rejected interval overlapping the last chosen one. The “Touching ends are compatible” preset shows the boundary rule: an interval that starts exactly when the previous one ends is allowed, so use >=.
To list the chosen intervals instead of counting them, add each to a list. If you also need the chosen intervals in their original order, remember their indices before sorting.
1static List<int[]> activitySelection(int[][] intervals) {2int[][] a = intervals.clone();3Arrays.sort(a, Comparator.comparingInt(x -> x[1]));4List<int[]> chosen = new ArrayList<>();5int lastEnd = Integer.MIN_VALUE;6for (int[] iv : a) {7if (iv[0] >= lastEnd) { chosen.add(iv); lastEnd = iv[1]; }8}9return chosen;10}
Best
O(n log n)
Average
O(n log n)
Worst
O(n log n)
Space
O(n) for the result and sorted copy
Activity selection: which greedy rule works?
Interactive · edit the inputsorted by earliest finish time
Sort the 8 intervals by earliest finish time (optimal). Then scan once and take every interval that starts at or after the end of the last chosen one.
1static int select(int[][] iv, int mode) { // mode 0: earliest finish (optimal), 1: earliest start, 2: shortest2Comparator<int[]> rule = mode == 0 ? Comparator.comparingInt(a -> a[1])3: mode == 1 ? Comparator.comparingInt(a -> a[0])4: Comparator.comparingInt(a -> a[1] - a[0]);5Arrays.sort(iv, rule);6int count = 0, lastEnd = Integer.MIN_VALUE;7for (int[] cur : iv) {8if (cur[0] >= lastEnd) { // compatible with everything chosen so far (half-open intervals)9count++; lastEnd = cur[1]; // choose it10} // otherwise skip it11}12return count;13}
Live state
intervals
8
rule
earliest finish time (optimal)
Execution trace (dry run table)Show 13 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L5 | intervals=8 rule=earliest finish time (optimal) | Sort the 8 intervals by earliest finish time (optimal). Then scan once and take every interval that starts at or after the end of the last chosen one. |
| 2 | L8 | start=1 end=4 last end=none | [1, 4) does not overlap the last choice: take it. |
| 3 | L9 | chosen=1 last end=4 | Chosen A = [1, 4). The last end is now 4. |
| 4 | L8 | start=3 end=5 last end=4 | [3, 5) starts before 4: it overlaps a chosen interval, skip it. |
| 5 | L8 | start=0 end=6 last end=4 | [0, 6) starts before 4: it overlaps a chosen interval, skip it. |
| 6 | L8 | start=5 end=7 last end=4 | [5, 7) does not overlap the last choice: take it. |
| 7 | L9 | chosen=2 last end=7 | Chosen D = [5, 7). The last end is now 7. |
| 8 | L8 | start=3 end=9 last end=7 | [3, 9) starts before 7: it overlaps a chosen interval, skip it. |
| 9 | L8 | start=5 end=9 last end=7 | [5, 9) starts before 7: it overlaps a chosen interval, skip it. |
| 10 | L8 | start=6 end=10 last end=7 | [6, 10) starts before 7: it overlaps a chosen interval, skip it. |
| 11 | L8 | start=8 end=11 last end=7 | [8, 11) does not overlap the last choice: take it. |
| 12 | L9 | chosen=3 last end=11 | Chosen H = [8, 11). The last end is now 11. |
| 13 | L12 | chosen=3 removed=5 optimum=3 return=3 | Chosen 3 intervals, which is optimal (5 must be removed to leave a non-overlapping set). |
Interval merging
To merge overlapping intervals, sort by **start** and sweep: if the next interval starts at or before the current merged interval's end, extend the end; otherwise close the current interval and start a new one. In the “Classic” preset 5 intervals become 3, and “Touching ends” shows [1,4] and [4,5] becoming [1,5].
The sort key is different from activity selection because the question is different: merging needs neighbours in start order so that anything that can overlap the current block is processed next.
1static List<int[]> mergeIntervals(int[][] intervals) {2int[][] a = intervals.clone();3Arrays.sort(a, Comparator.comparingInt(x -> x[0])); // by start4List<int[]> merged = new ArrayList<>();5for (int[] iv : a) {6if (merged.isEmpty() || merged.get(merged.size() - 1)[1] < iv[0]) {7merged.add(new int[]{iv[0], iv[1]}); // gap: start a new block8} else {9int[] last = merged.get(merged.size() - 1);10last[1] = Math.max(last[1], iv[1]); // overlap or touch: extend (nested keeps the larger end)11}12}13return merged;14}
Best
O(n log n)
Average
O(n log n)
Worst
O(n log n)
Space
O(n)
Merge overlapping intervals (sort by start, then scan)
Interactive · edit the inputinput
In input order, overlaps can be far apart. Sorting by start puts every overlap next to its neighbour.
1public static List<int[]> merge(int[][] intervals) {2Arrays.sort(intervals, Comparator.comparingInt(a -> a[0])); // sort by start3List<int[]> merged = new ArrayList<>();4for (int[] cur : intervals) {5int[] last = merged.isEmpty() ? null : merged.get(merged.size() - 1);6if (last != null && cur[0] <= last[1]) {7last[1] = Math.max(last[1], cur[1]); // overlap: extend8} else {9merged.add(cur); // gap: start a new interval10}11}12return merged;13}
Live state
intervals
5
Execution trace (dry run table)Show 13 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | intervals=5 | In input order, overlaps can be far apart. Sorting by start puts every overlap next to its neighbour. |
| 2 | L2 | intervals=5 | Sorted by start: O(n log n). Now one left-to-right scan is enough. |
| 3 | L6 | start=1 end=3 last end=none | Nothing merged yet: this interval starts the list. |
| 4 | L9 | merged=1 | Append [1, 3] as a new merged interval. |
| 5 | L6 | start=2 end=6 last end=3 | [2, 6] starts before the last merged interval ends, so they overlap. |
| 6 | L7 | last end=6 | Extend the last interval: end = max(3, 6) = 6. |
| 7 | L6 | start=8 end=10 last end=6 | [8, 10] starts after 6: there is a gap. |
| 8 | L9 | merged=2 | Append [8, 10] as a new merged interval. |
| 9 | L6 | start=9 end=12 last end=10 | [9, 12] starts before the last merged interval ends, so they overlap. |
| 10 | L7 | last end=12 | Extend the last interval: end = max(10, 12) = 12. |
| 11 | L6 | start=15 end=18 last end=12 | [15, 18] starts after 12: there is a gap. |
| 12 | L9 | merged=3 | Append [15, 18] as a new merged interval. |
| 13 | L12 | return=[1,6] [8,12] [15,18] | 5 intervals became 3. O(n log n) for the sort plus O(n) for the scan. |
Minimum meeting rooms
How many rooms are needed so no two overlapping meetings share one? Sort meetings by start and keep a **min-heap of end times**, one entry per room in use. For each meeting, if the earliest-ending room is free (its end is at or before this start) reuse it by removing that end, then add this meeting's end. The heap size at the end is the number of rooms.
The “Classic” preset needs 3 rooms: at time 15 the meetings [0,30], [10,25] and [15,20] all overlap. “Back to back” needs only 1 because a meeting may start exactly when another ends. The answer equals the maximum number of meetings overlapping at any instant, which Chapter 22 computes with an event sweep.
1static int minMeetingRooms(int[][] meetings) {2int[][] a = meetings.clone();3Arrays.sort(a, Comparator.comparingInt(x -> x[0]));4PriorityQueue<Integer> endTimes = new PriorityQueue<>();5for (int[] m : a) {6if (!endTimes.isEmpty() && endTimes.peek() <= m[0]) endTimes.poll(); // reuse the room that frees first7endTimes.add(m[1]);8}9return endTimes.size();10}
Best
O(n log n)
Average
O(n log n)
Worst
O(n log n)
Space
O(n) for the heap
Minimum meeting rooms (sweep sorted events)
Interactive · edit the inputmeetings
starts (sorted)
ends (sorted)
Only the times matter, not which meeting is which. Sort starts and ends separately and sweep left to right.
1public static int minRooms(int[][] meetings) { // [start, end), end is exclusive2int n = meetings.length;3int[] starts = new int[n], ends = new int[n];4for (int i = 0; i < n; i++) { starts[i] = meetings[i][0]; ends[i] = meetings[i][1]; }5Arrays.sort(starts);6Arrays.sort(ends);7int rooms = 0, best = 0, e = 0;8for (int s = 0; s < n; s++) {9while (ends[e] <= starts[s]) { e++; rooms--; } // meetings that ended free a room10rooms++;11best = Math.max(best, rooms);12}13return best;14}
Live state
rooms
0
best
0
Execution trace (dry run table)Show 9 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L5 | rooms=0 best=0 | Only the times matter, not which meeting is which. Sort starts and ends separately and sweep left to right. |
| 2 | L11 | time=0 rooms=1 best=1 | A meeting starts at 0: 1 room in use. Peak so far 1. |
| 3 | L11 | time=5 rooms=2 best=2 | A meeting starts at 5: 2 rooms in use. Peak so far 2. |
| 4 | L9 | time=10 rooms=1 best=2 | A meeting ended at 10 ≤ 10: its room is free again. |
| 5 | L11 | time=10 rooms=2 best=2 | A meeting starts at 10: 2 rooms in use. Peak so far 2. |
| 6 | L11 | time=15 rooms=3 best=3 | A meeting starts at 15: 3 rooms in use. Peak so far 3. |
| 7 | L9 | time=22 rooms=2 best=3 | A meeting ended at 20 ≤ 22: its room is free again. |
| 8 | L11 | time=22 rooms=3 best=3 | A meeting starts at 22: 3 rooms in use. Peak so far 3. |
| 9 | L13 | return=3 | 3 rooms needed: the maximum number of meetings overlapping at any moment. O(n log n). |
Interval covering
Given intervals and a target range [lo, hi], choose the fewest intervals whose union covers the range. Sort by start. Keep reach, the right end of the covered part (initially lo). At each round look at **all** intervals starting at or before reach and take the one with the largest end. If none extends the reach, the range has a gap and the answer is impossible.
The “Needs four” preset covers [0, 12] with 4 intervals, “One interval is enough” shows an early wide interval swallowing the rest, and “Gap: impossible” returns -1 because nothing starts at or before the reach 3 and extends it past the hole.
1static int minCover(int[][] intervals, int lo, int hi) {2int[][] a = intervals.clone();3Arrays.sort(a, Comparator.comparingInt(x -> x[0]));4int reach = lo, used = 0, i = 0;5while (reach < hi) {6int best = reach;7while (i < a.length && a[i][0] <= reach) { best = Math.max(best, a[i][1]); i++; }8if (best == reach) return -1; // nothing extends the covered range9reach = best;10used++;11}12return used;13}
Best
O(n log n)
Average
O(n log n)
Worst
O(n log n)
Space
O(1) beyond the sorted copy
Cover a range with the fewest intervals
Interactive · edit the inputsorted by start
Cover [0, 12] using as few intervals as possible. Repeatedly look at every interval that starts within the covered part and take the one that reaches farthest.
1static int minCover(int[][] iv, int from, int to) {2Arrays.sort(iv, Comparator.comparingInt(a -> a[0])); // by start3int count = 0, reach = from, i = 0;4while (reach < to) {5int farthest = reach;6while (i < iv.length && iv[i][0] <= reach) farthest = Math.max(farthest, iv[i++][1]); // all intervals that start in time7if (farthest == reach) return -1; // nothing extends the covered part: a gap8count++; reach = farthest; // take the one that reaches farthest9}10return count;11}
Live state
from
0
to
12
reach
0
Execution trace (dry run table)Show 10 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | from=0 to=12 reach=0 | Cover [0, 12] using as few intervals as possible. Repeatedly look at every interval that starts within the covered part and take the one that reaches farthest. |
| 2 | L6 | reach=0 candidates=1 farthest=3 | Intervals A start at or before 0; the farthest end among them is 3. |
| 3 | L8 | count=1 reach=3 | Take A: the covered part now reaches 3. |
| 4 | L6 | reach=3 candidates=2 farthest=6 | Intervals B, C start at or before 3; the farthest end among them is 6. |
| 5 | L8 | count=2 reach=6 | Take C: the covered part now reaches 6. |
| 6 | L6 | reach=6 candidates=1 farthest=9 | Intervals D start at or before 6; the farthest end among them is 9. |
| 7 | L8 | count=3 reach=9 | Take D: the covered part now reaches 9. |
| 8 | L6 | reach=9 candidates=2 farthest=12 | Intervals E, F start at or before 9; the farthest end among them is 12. |
| 9 | L8 | count=4 reach=12 | Take F: the covered part now reaches 12. |
| 10 | L10 | return=4 | Covered [0, 12] with 4 intervals. |
Minimum arrows
Balloons are intervals on a line and an arrow at x bursts every balloon with start <= x <= end. The fewest arrows: sort by **end**, shoot at the first balloon's end, and skip every balloon that starts at or before that point; the next balloon that starts later needs a new arrow. This is activity selection in disguise, because a set of balloons that need different arrows is a set of pairwise disjoint intervals.
In the “Touching balloons share an arrow” preset, [1,2], [2,3], [3,4], [4,5] take 2 arrows at 2 and 4, and “All overlap” takes a single arrow.
1static int minArrows(int[][] balloons) {2int[][] a = balloons.clone();3Arrays.sort(a, Comparator.comparingInt(x -> x[1])); // compare ends without subtraction: no overflow4int arrows = 0;5long shot = Long.MIN_VALUE;6for (int[] b : a) {7if (b[0] > shot) { arrows++; shot = b[1]; } // not burst by the last arrow8}9return arrows;10}
Best
O(n log n)
Average
O(n log n)
Worst
O(n log n)
Space
O(1) beyond the sorted copy
Minimum arrows to burst all balloons
Interactive · edit the inputsorted by right end
Sort by right end. An arrow shot at the right end of the first balloon bursts every balloon that starts at or before that point; the next unburst balloon needs a new arrow.
1static int minArrows(int[][] balloons) {2Arrays.sort(balloons, Comparator.comparingInt(a -> a[1])); // by right end (compare, never subtract)3int arrows = 0; long shot = Long.MIN_VALUE;4for (int[] b : balloons) {5if (b[0] > shot) { // not burst by the previous arrow6arrows++; shot = b[1]; // shoot at its right end7}8}9return arrows;10}
Live state
balloons
4
Execution trace (dry run table)Show 8 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | balloons=4 | Sort by right end. An arrow shot at the right end of the first balloon bursts every balloon that starts at or before that point; the next unburst balloon needs a new arrow. |
| 2 | L5 | start=1 end=6 last arrow=none | Balloon C starts after the last arrow: it was not burst, so a new arrow is needed. |
| 3 | L6 | arrows=1 last arrow=6 | Shoot an arrow at 6, the right end of C: it also bursts every later balloon that starts by 6. |
| 4 | L5 | start=2 end=8 last arrow=6 | Balloon B contains the last arrow's position 6: already burst. |
| 5 | L5 | start=7 end=12 last arrow=6 | Balloon D starts after the last arrow: it was not burst, so a new arrow is needed. |
| 6 | L6 | arrows=2 last arrow=12 | Shoot an arrow at 12, the right end of D: it also bursts every later balloon that starts by 12. |
| 7 | L5 | start=10 end=16 last arrow=12 | Balloon A contains the last arrow's position 12: already burst. |
| 8 | L9 | return=2 | 2 arrows at [6, 12]. |
Non-overlapping intervals
'Remove the fewest intervals so the rest do not overlap' is the complement of activity selection: keep the largest non-overlapping set, and the answer is n - kept. No new algorithm is needed.
The animation reports exactly that quantity at the end of each run (for example, the Earliest finish preset keeps 3 of 8, so 5 must be removed). When duplicates or touching intervals appear, the boundary rule start >= lastEnd decides whether touching intervals count as overlapping.
1static int eraseOverlapIntervals(int[][] intervals) {2if (intervals.length == 0) return 0;3int[][] a = intervals.clone();4Arrays.sort(a, Comparator.comparingInt(x -> x[1]));5int kept = 1, lastEnd = a[0][1];6for (int i = 1; i < a.length; i++) {7if (a[i][0] >= lastEnd) { kept++; lastEnd = a[i][1]; }8}9return a.length - kept;10}
Best
O(n log n)
Average
O(n log n)
Worst
O(n log n)
Space
O(1) beyond the sorted copy
Activity selection: which greedy rule works?
Interactive · edit the inputsorted by earliest finish time
Sort the 8 intervals by earliest finish time (optimal). Then scan once and take every interval that starts at or after the end of the last chosen one.
1static int select(int[][] iv, int mode) { // mode 0: earliest finish (optimal), 1: earliest start, 2: shortest2Comparator<int[]> rule = mode == 0 ? Comparator.comparingInt(a -> a[1])3: mode == 1 ? Comparator.comparingInt(a -> a[0])4: Comparator.comparingInt(a -> a[1] - a[0]);5Arrays.sort(iv, rule);6int count = 0, lastEnd = Integer.MIN_VALUE;7for (int[] cur : iv) {8if (cur[0] >= lastEnd) { // compatible with everything chosen so far (half-open intervals)9count++; lastEnd = cur[1]; // choose it10} // otherwise skip it11}12return count;13}
Live state
intervals
8
rule
earliest finish time (optimal)
Execution trace (dry run table)Show 13 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L5 | intervals=8 rule=earliest finish time (optimal) | Sort the 8 intervals by earliest finish time (optimal). Then scan once and take every interval that starts at or after the end of the last chosen one. |
| 2 | L8 | start=1 end=4 last end=none | [1, 4) does not overlap the last choice: take it. |
| 3 | L9 | chosen=1 last end=4 | Chosen A = [1, 4). The last end is now 4. |
| 4 | L8 | start=3 end=5 last end=4 | [3, 5) starts before 4: it overlaps a chosen interval, skip it. |
| 5 | L8 | start=0 end=6 last end=4 | [0, 6) starts before 4: it overlaps a chosen interval, skip it. |
| 6 | L8 | start=5 end=7 last end=4 | [5, 7) does not overlap the last choice: take it. |
| 7 | L9 | chosen=2 last end=7 | Chosen D = [5, 7). The last end is now 7. |
| 8 | L8 | start=3 end=9 last end=7 | [3, 9) starts before 7: it overlaps a chosen interval, skip it. |
| 9 | L8 | start=5 end=9 last end=7 | [5, 9) starts before 7: it overlaps a chosen interval, skip it. |
| 10 | L8 | start=6 end=10 last end=7 | [6, 10) starts before 7: it overlaps a chosen interval, skip it. |
| 11 | L8 | start=8 end=11 last end=7 | [8, 11) does not overlap the last choice: take it. |
| 12 | L9 | chosen=3 last end=11 | Chosen H = [8, 11). The last end is now 11. |
| 13 | L12 | chosen=3 removed=5 optimum=3 return=3 | Chosen 3 intervals, which is optimal (5 must be removed to leave a non-overlapping set). |
Practice ladder
Now try it yourself
Easy
- Meeting Rooms
- Merge Intervals (small inputs by hand)
Medium
- Non-overlapping Intervals
- Minimum Number of Arrows to Burst Balloons
- Meeting Rooms II
- Video Stitching
Advanced
- Minimum Number of Taps to Open to Water a Garden
- Maximum Profit in Job Scheduling
Key takeaways
- Activity selection and the problems that reduce to it sort by end time; merging sorts by start time.
- Meeting rooms needs a min-heap of end times, or equivalently an event sweep for the maximum overlap.
- Covering keeps the reach and takes the farthest-reaching interval that starts within it.
- Minimum arrows and minimum removals are activity selection with a different final answer.
- Decide up front whether touching endpoints overlap; it is the most common off-by-one in this topic.