Problem overview
The function non_overlapping_intervals receives a vector of intervals, each a two-element vector [start, end], and returns the largest number of intervals you can keep so that no two of them overlap. Two intervals whose borders only touch, such as [1, 2] and [2, 3], don't overlap. Each interval's end is larger than its start.
For [[1, 2], [2, 3], [3, 4], [1, 3]] the answer is 3: the intervals [1, 2], [2, 3] and [3, 4] fit together, and [1, 3] overlaps two of them.
Approach: always take the interval that ends first
Trying every subset of intervals is exponential. A greedy rule is much faster: sort the intervals by their end point and walk through them, keeping an interval whenever it starts at or after the end of the last one you kept.
The rule works because the interval that ends earliest leaves the most room for everything after it. Suppose some best answer does not contain the earliest-ending interval, A. Its first interval, B, ends no earlier than A does, so replacing B with A cannot cause a new overlap with the rest, and the count stays the same. So some best answer always includes A. After taking it, the same argument applies to the intervals that start at or after A ends.
The statement sets O(n log n) time, which the sort provides. The walk afterwards is a single pass.
On the example, sorting by end gives [1, 2], [2, 3] and [1, 3] (in either order for the two intervals ending at 3), then [3, 4]. Take [1, 2]. Next, [2, 3] starts at 2, which is not before 2, so take it. [1, 3] starts at 1, before 3, so skip it. [3, 4] starts at 3, so take it. The count is 3.
Step by step
- Start from the sorted vector. The starter code already calls
sort_by_keywith the end point as the key. The first interval after sorting ends earliest, so taking it is always safe. - Count it. Declare
resultas a mutablei32starting at 1, for that first interval. - Remember the last kept interval. Declare
last_takenas a mutableusize, starting at 0, because it will index the vector. - Walk the rest. Loop
iover1..intervals.len(). Index 0 is already taken. - Test for overlap. The current interval's start,
intervals[i][0], must be greater than or equal to the end of the last kept one,intervals[last_taken][1]. Use>=because touching borders are allowed. - Keep it. Add 1 to
resultand setlast_takentoi. - Return
resultafter the loop.
The solution in Rust
impl Solution {
fn non_overlapping_intervals(mut intervals: Vec<Vec<i32>>) -> i32 {
intervals.sort_by_key(|a| a[1]);
let mut result = 1i32;
let mut last_taken = 0usize;
for i in 1..intervals.len() {
if intervals[i][0] >= intervals[last_taken][1] {
result += 1;
last_taken = i;
}
}
result
}
}
The Rust details worth knowing:
mutin the parameter.mut intervalsmakes the owned vector mutable inside the function, whichsort_by_keyrequires since it reorders the elements in place. The caller's variable was moved into the function, so nothing outside changes.- The closure.
|a| a[1]receives a reference to each inner vector and returns the key to sort by, here the end point. The key is ani32, which implementsOrd. The sort compares keys and leaves the intervals themselves intact. - Stable sorting.
sort_by_keykeeps intervals with equal keys in their original order. The greedy walk doesn't depend on that, since intervals with the same end are interchangeable for the count. - Typed literals.
1i32fixes the result type to match the return type, and0usizemakeslast_takenusable as an index. Vectors accept onlyusizeindexes. - No
++. Rust has no increment operator, so the count goes up with+= 1. - Tail expression.
resultwithout a semicolon is the function's value.
Complexity
Time: O(n log n). The sort costs O(n log n), and the pass afterwards is O(n).
Space: close to O(1) beyond the input, with a caveat. The walk uses only two variables, but sort_by_key is a stable sort, and stable sorting in Rust may allocate a temporary buffer that grows with the input. If you need a strict constant-space sort, sort_unstable_by_key sorts in place and gives the same answer here, because the order of intervals with equal ends doesn't affect the count.
Edge cases and common mistakes
- Touching intervals.
[[1, 2], [2, 3]]gives 2. With>instead of>=the answer would be 1. - Identical intervals.
[[1, 2], [1, 2], [1, 2]]gives 1, since only one can be kept. - One long interval around short ones.
[[1, 10], [2, 3], [4, 5]]gives 2. The long interval ends last, so it is considered last and skipped. - Sorting by start instead of end. The long interval
[1, 10]would be taken first and block everything else, giving 1. - A single interval. The loop is empty and the function returns 1.
- An empty vector. The function returns 1, because
resultstarts at 1 and never checks. The lesson's inputs always hold at least one interval, but a general-purpose function would return 0 early for an empty input. - Comparing with the wrong interval. The start must be compared with the end of the last kept interval, not the one just before it in the vector.
Testing
[[1,2],[2,3],[3,4],[1,3]]gives3.[[1,2],[1,2],[1,2]]gives1.[[1,10],[2,3],[4,5]]gives2.[[1,2]]gives1.
