Problem overview
The method nonOverlappingIntervals receives an int[][] named intervals, where each row {start, end} is one interval. It returns the largest number of intervals that can be chosen so that no two chosen intervals overlap. Intervals that only touch, such as [1, 2] and [2, 3], do not overlap. Each end point is greater than its start point.
For [[1, 2], [2, 3], [3, 4], [1, 3]] the answer is 3: the intervals [1, 2], [2, 3] and [3, 4] fit one after another, and [1, 3] overlaps two of them. No choice of four works, since [1, 3] overlaps [1, 2] and [2, 3].
This is the classic activity selection problem: think of each interval as a meeting that needs a room, and ask for the most meetings that fit in one room. A closely related version asks for the fewest intervals to remove so that the rest do not overlap. That number is intervals.length minus the answer computed here.
Approach: always take the interval that ends first
Trying every subset of intervals would take O(2^n) time, and a dynamic programming solution that compares each interval with all earlier ones takes O(n^2). The task asks for O(n log n), which a greedy strategy reaches. A greedy algorithm builds the answer by making the best local choice at each step and never reconsidering it. Here the right local choice is the question: which interval should I take first?
The one that ends earliest. An early end leaves the largest possible stretch of time for everything that comes after. Suppose an optimal answer starts with some other interval. Swap that interval for the one with the earliest end. The earliest-ending interval finishes no later than the one it replaces, so it cannot overlap anything that the original choice left room for, and the count stays the same. So some optimal answer always contains the earliest-ending interval, and the same argument applies to what remains after it.
Two other greedy ideas look attractive but fail. Taking the earliest start picks [1, 10] from [[1, 10], [2, 3], [4, 5], [6, 7]] and then nothing else fits, giving 1 where the answer is 3. Taking the shortest interval first can also go wrong, since one short interval may sit in the middle of two long ones that would otherwise both fit.
In the algorithm, sort the intervals by end point, take the first one, and then scan from left to right. Take an interval whenever it starts at or after the end of the last one taken. Skip it otherwise.
On the example, sorting by end gives [1, 2], [2, 3], [1, 3], [3, 4]. Take [1, 2]. Next [2, 3] starts at 2, which is at least the last end of 2, so take it. [1, 3] starts at 1, before the last end of 3, so skip it. [3, 4] starts at 3, which is at least 3, so take it. The result is 3.
Step by step
- Sort by end point.
Arrays.sortneeds a comparator, because anint[]has no natural order. The lambda receives two intervals,firstandsecond, and returnsfirst[1] - second[1], which is negative whenfirstends earlier. - Take the first interval. After sorting it ends the earliest, so taking it is always safe. Start
resultat 1 and remember its index inlastTaken, which starts at 0. Storing the index lets the next comparison read the end point from the array. - Loop over the rest. The index
igoes from 1 up tointervals.length - 1, since index 0 is already taken. - Test for a gap. If
intervals[i][0] >= intervals[lastTaken][1], the current interval starts when or after the last one ends, so they do not overlap. The>=is what lets touching intervals count as separate. - Take it. Increment
resultand setlastTakentoi. - Return
resultafter the loop.
The solution in Java
class Solution {
public int nonOverlappingIntervals(int[][] intervals) {
Arrays.sort(intervals, (first, second) -> first[1] - second[1]);
int result = 1, lastTaken = 0;
for (int i = 1; i < intervals.length; i++) {
if (intervals[i][0] >= intervals[lastTaken][1]) {
result++;
lastTaken = i;
}
}
return result;
}
}
Java details to know:
- The import.
Arrayslives injava.util, so a file of your own needsimport java.util.Arrays;. - Sorting an array of arrays.
intervalsis an array ofint[]objects, soArrays.sortaccepts a comparator, which it would not for a plainint[]. The sort moves the row references and does not copy the numbers. - The input is modified. The sort is in place, so the caller's array ends up reordered. Work on a copy if the original order matters.
- The comparator and overflow. The subtraction
first[1] - second[1]overflowsintwhen the end points are far apart, such as 2,000,000,000 and -2,000,000,000. The wrapped result has the wrong sign and breaks the sort. For example, with{{1, 2000000000}, {-2147483648, -2000000000}}the code above returns 1 instead of 2.Integer.compare(first[1], second[1])never overflows and is the safer way to write the same comparison.
Complexity
Sorting takes O(n log n) comparisons and the single pass afterwards takes O(n), so the total is O(n log n). The pass itself uses only three int variables. The sort of object arrays in Java is a merge-based sort that can use extra memory, so the strict extra space is O(n) in the worst case, even though the scan is O(1).
Edge cases and common mistakes
- Using
>instead of>=. Then touching intervals are treated as overlapping and the example returns 2. - Sorting by start point. The scan then compares with the wrong interval, and the result can be too small, as the
[1, 10]example above shows. - Identical intervals.
[[1, 2], [1, 2], [1, 2]]returns 1. The copies overlap each other completely. - Nested intervals. In
[[1, 10], [2, 3], [4, 5], [6, 7]]the short ones that end first win, and the answer is 3. - Empty input. The method starts
resultat 1, so an empty array returns 1 instead of 0. The task implies at least one interval; add a check forintervals.length == 0if empty input is possible.
Testing
Try [[1, 2], [2, 3], [3, 4], [1, 3]] (3), [[1, 2], [1, 2], [1, 2]] (1), [[1, 10], [2, 3], [4, 5], [6, 7]] (3) and [[1, 2], [1, 3]] (1).
