Problem overview
The method NonOverlappingIntervals(int[,] intervals) receives intervals as rows of a two-dimensional array: column 0 holds the start and column 1 the end. It returns the largest number of intervals you can select so that no two of them overlap. Intervals that only touch at an end point, like [1, 2] and [2, 3], do not overlap. Each end is larger than its start.
For the intervals [1, 2], [2, 3], [3, 4] and [1, 3] the answer is 3: take the first three and skip [1, 3], which overlaps both [1, 2] and [2, 3].
Approach: always take the interval that finishes first
Checking every subset of intervals is exponential. A greedy rule is enough: sort the intervals by end point, then walk through them and take each one that starts no earlier than the end of the last one taken.
Why does finishing first matter? The interval that ends earliest leaves the most room for the intervals after it. Suppose some best selection does not contain that interval. Its first interval ends no earlier, so swapping the earliest-ending interval in for it cannot create an overlap and keeps the count the same. Repeating the argument shows the greedy choices are never worse than any other plan.
On the example, the sorted order by end is [1, 2], then the two intervals ending at 3, then [3, 4]. The scan takes [1, 2]. A candidate ending at 3 that starts at 2 is taken, since 2 is at least 2, and then [1, 3] is skipped because it starts at 1, before the last end of 3. Finally [3, 4] starts at 3 and is taken. The total is 3, whichever order the two tied intervals come in.
Step by step
- Sort row indexes, not rows. C# cannot reorder the rows of an
int[,]directly. Instead, storeintervals.GetLength(0)inrows, create anint[] orderholding0, 1, ..., rows - 1, and sort it withArray.Sortand a comparison lambda that compares the end points of the two rows:intervals[first, 1] - intervals[second, 1]. - Take the first interval. Set
resultto 1 andlastTakentoorder[0], the row that ends first. The conditionalrows > 0 ? order[0] : 0avoids readingorder[0]from an empty array. - Loop over the rest of
orderfrom position 1. - Compare. The current interval is row
order[i]. If its start,intervals[order[i], 0], is at least the end of the last taken one,intervals[lastTaken, 1], there is no overlap. The comparison uses>=because touching ends are allowed. - Take it. Increment
resultand setlastTakentoorder[i]. Store the row number, not the position inorder, since the next comparison readsintervals[lastTaken, 1]. - Return
resultafter the loop.
The solution in C#
class Solution {
public int NonOverlappingIntervals(int[,] intervals) {
int rows = intervals.GetLength(0);
int[] order = new int[rows];
for (int i = 0; i < rows; i++) order[i] = i;
Array.Sort(order, (first, second) => intervals[first, 1] - intervals[second, 1]);
int result = 1, lastTaken = rows > 0 ? order[0] : 0;
for (int i = 1; i < rows; i++) {
if (intervals[order[i], 0] >= intervals[lastTaken, 1]) {
result++;
lastTaken = order[i];
}
}
return result;
}
}
C# details:
- Sorting indirectly. A rectangular array is a single block, so you cannot swap its rows as objects. Sorting an array of row numbers gets the same effect, and
intervals[order[i], 0]reads through that order. - The comparison lambda.
Array.Sort(array, comparison)accepts aComparison<int>. Returning a negative number means the first argument goes before the second, zero means a tie, and a positive number means it goes after. Subtraction is a compact way to produce that. - Subtraction can overflow. For ordinary values it is fine. If end points could be near
int.MinValueandint.MaxValue, the difference would wrap and flip the sign.intervals[first, 1].CompareTo(intervals[second, 1])avoids that. - Two variables, one declaration.
int result = 1, lastTaken = ...;declares both asint. - Ties.
Array.Sortis not a stable sort, so equal end points may come in either order. That does not change the count here.
Complexity
Time: O(n log n). The sort dominates. The scan afterwards is a single O(n) pass.
Space: O(n) extra in this C# version, for the order array of row numbers. That array is the price of sorting rows of an int[,] without copying the data.
Edge cases and common mistakes
- Touching intervals.
[1, 2]and[2, 3]are compatible. Using>instead of>=would reject them and return 1 for that pair. - Sorting by start instead of end. For
[1, 5],[2, 3]and[4, 6], taking the earliest start picks[1, 5]and blocks both of the others, giving 1. Sorting by end gives 2. - Nested intervals. A long interval containing short ones is skipped automatically, because short ones end sooner and are taken first.
- Storing the wrong index. Setting
lastTaken = istores a position inorder, but the code reads it as a row ofintervals. It would compare with the wrong interval. - Empty input. The guard stops the code from reading
order[0], butresultstarts at 1, so zero intervals would still return 1. The expected answer is 0 for that case, so a program that must handle empty input needs an earlyreturn 0. - Thinking in terms of
.Length. On a 2D array it counts every cell, so for 4 intervals it is 8. UseGetLength(0).
Testing
[[1, 2], [2, 3], [3, 4], [1, 3]]gives 3.[[1, 5], [2, 3], [4, 6]]gives 2.[[1, 2], [2, 3]]gives 2.[[1, 10], [2, 3], [3, 4], [4, 5]]gives 3.