Non-overlapping Intervals
MedPrompt
Given an array of intervals intervals where intervals[i] = [starti, endi], return the minimum number of intervals you need to remove to make the rest of the intervals non-overlapping.
Note that intervals which only touch at a point are non-overlapping. For example, [1, 2] and [2, 3] are non-overlapping.
Example 1:
Input: intervals = [[1,2],[2,3],[3,4],[1,3]]
Output: 1
Explanation: [1,3] can be removed and the rest of the intervals are non-overlapping.Example 2:
Input: intervals = [[1,2],[1,2],[1,2]]
Output: 2
Explanation: You need to remove two [1,2] to make the rest of the intervals non-overlapping.Example 3:
Input: intervals = [[1,2],[2,3]]
Output: 0
Explanation: You don't need to remove any of the intervals since they're already non-overlapping.
Constraints:
1 <= intervals.length <= 105intervals[i].length == 2-5 * 104 <= starti < endi <= 5 * 104
Approaches
3 approaches with complexity analysis and trade-offs.
This approach uses dynamic programming, which is a common technique for optimization problems. The idea is to build up a solution by finding the optimal solution for smaller subproblems. We sort the intervals by their start times and then, for each interval, we determine the maximum number of non-overlapping intervals that can be formed ending with that interval. This is analogous to solving the Longest Increasing Subsequence problem.
Algorithm
- Sort the
intervalsarray based on the start times of the intervals. - Create a dynamic programming array
dpof sizen, wherenis the number of intervals. dp[i]will store the maximum number of non-overlapping intervals in a sequence that ends withintervals[i].- Initialize all elements of
dpto 1, as any single interval is a valid non-overlapping set. - Iterate from the second interval (
i = 1) to the end. For eachintervals[i], iterate through all preceding intervalsj(from0toi-1). - If
intervals[j]does not overlap withintervals[i](i.e.,intervals[j][1] <= intervals[i][0]), it meansintervals[i]can extend the non-overlapping sequence ending atintervals[j]. Updatedp[i]accordingly:dp[i] = max(dp[i], 1 + dp[j]). - After the loops complete, find the maximum value in the
dparray. This value,maxKept, is the size of the largest set of non-overlapping intervals. - The minimum number of intervals to remove is
n - maxKept.
Walkthrough
First, we sort the intervals based on their start times. This ordering is crucial as it allows us to build our solution sequentially. We define dp[i] as the maximum number of compatible (non-overlapping) intervals we can select from the first i intervals, with the condition that the i-th interval must be included in our selection.
We initialize dp[i] = 1 for all i, because a single interval by itself is always a valid non-overlapping set. Then, we iterate through the intervals from i = 1 to n-1. For each i, we look back at all previous intervals j < i. If intervals[j] and intervals[i] are non-overlapping (i.e., intervals[j][1] <= intervals[i][0]), we can potentially form a longer sequence of non-overlapping intervals by appending intervals[i] to the sequence ending at intervals[j]. We update dp[i] to be the maximum of its current value and 1 + dp[j].
After computing all dp values, the maximum value in the dp array gives the size of the largest possible non-overlapping set of intervals. The minimum number of intervals to remove is then the total number of intervals minus this maximum size.
import java.util.Arrays;import java.util.Comparator; class Solution { public int eraseOverlapIntervals(int[][] intervals) { if (intervals.length == 0) { return 0; } Arrays.sort(intervals, Comparator.comparingInt(a -> a[0])); int n = intervals.length; int[] dp = new int[n]; Arrays.fill(dp, 1); int maxKept = 1; for (int i = 1; i < n; i++) { for (int j = 0; j < i; j++) { if (intervals[j][1] <= intervals[i][0]) { dp[i] = Math.max(dp[i], 1 + dp[j]); } } maxKept = Math.max(maxKept, dp[i]); } return n - maxKept; }}Complexity
Time
O(N^2), where N is the number of intervals. The dominant part is the nested loop used to fill the `dp` array. The initial sort takes O(N log N).
Space
O(N), where N is the number of intervals. This is for the `dp` array used to store intermediate results.
Trade-offs
Pros
It is a systematic approach that guarantees finding the optimal solution.
The logic is a standard application of dynamic programming.
Cons
The O(N^2) time complexity makes it too slow for the given constraints, leading to a 'Time Limit Exceeded' error on large inputs.
A more efficient method is a greedy approach. The problem of minimizing removals is equivalent to maximizing the number of non-overlapping intervals we keep. This approach involves sorting the intervals by their start times and then iterating through them, making a locally optimal choice at each step. When an overlap is detected, we greedily decide which interval to discard to best accommodate future intervals.
Algorithm
- Sort the
intervalsarray based on the start times of the intervals. - If there are 1 or fewer intervals, no removals are needed, so return 0.
- Initialize a
removalscounter to 0. - Keep track of the end point of the last interval that was kept. Initialize this
lastEndwith the end point of the first interval (intervals[0][1]). - Iterate through the sorted intervals starting from the second one (
i = 1). - For each
intervals[i], check if it overlaps with the interval corresponding tolastEnd. An overlap occurs ifintervals[i][0] < lastEnd. - If there is an overlap:
- Increment the
removalscounter. - To make a greedy choice, we must discard one of the two overlapping intervals. We should keep the one that finishes earlier to leave more room for future intervals. So, we update
lastEnd = min(lastEnd, intervals[i][1]).
- Increment the
- If there is no overlap (
intervals[i][0] >= lastEnd):- We keep the current interval. Update
lastEndto the end of the current interval:lastEnd = intervals[i][1].
- We keep the current interval. Update
- Return the final
removalscount.
Walkthrough
The strategy is to process intervals in the order they begin. We sort the intervals by their start times. We then iterate through the sorted list, maintaining the end time of the last interval we decided to keep. Let's call this lastEnd.
We initialize lastEnd with the end time of the very first interval. Then, for each subsequent interval, we check if its start time is less than lastEnd.
If it is, we have an overlap. We must remove one interval. The greedy choice is to remove the interval that finishes later. This is because keeping the interval that finishes earlier will free up the timeline sooner, maximizing the potential to fit more intervals later on. So, we increment our removal count and update lastEnd to the minimum of its current value and the end time of the current interval (effectively keeping the one that ends sooner).
If the current interval's start time is not less than lastEnd, there's no overlap. We can keep this interval, so we simply update lastEnd to the end time of this new interval.
After checking all intervals, the total count of removals is our answer.
import java.util.Arrays;import java.util.Comparator; class Solution { public int eraseOverlapIntervals(int[][] intervals) { if (intervals.length <= 1) { return 0; } Arrays.sort(intervals, Comparator.comparingInt(a -> a[0])); int removals = 0; int lastEnd = intervals[0][1]; for (int i = 1; i < intervals.length; i++) { // Overlap case if (intervals[i][0] < lastEnd) { removals++; // Greedily keep the one that ends earlier lastEnd = Math.min(lastEnd, intervals[i][1]); } else { // No overlap case, update the end to the current interval's end lastEnd = intervals[i][1]; } } return removals; }}Complexity
Time
O(N log N), dominated by the initial sorting of the intervals. The subsequent linear scan takes O(N) time.
Space
O(log N) or O(N), depending on the space used by the sorting algorithm. In Java, `Arrays.sort` for primitives uses a dual-pivot quicksort which takes O(log N) space on average, while for objects it uses Timsort which takes O(N) space in the worst case.
Trade-offs
Pros
The O(N log N) time complexity is very efficient and passes the given constraints.
It correctly solves the problem by making a locally optimal greedy choice.
Cons
The logic for updating the
lastEndduring an overlap can be slightly less direct to reason about compared to the end-time sorting strategy.
This is the most efficient and canonical greedy solution for this problem. The core insight is that by sorting the intervals by their end times, we can make a simple and powerful greedy choice. At each step, we consider keeping the interval that finishes the earliest. This strategy maximizes the available time for subsequent intervals, leading to a globally optimal solution.
Algorithm
- If there are 1 or fewer intervals, no removals are needed, so return 0.
- Sort the
intervalsarray based on the end times of the intervals in ascending order. - Initialize a counter for the number of non-overlapping intervals we can keep,
count = 1. We start with 1 because we will always keep the first interval in the sorted list. - Initialize a variable
endto hold the end time of the last kept interval. Set it to the end time of the first interval:end = intervals[0][1]. - Iterate through the sorted intervals starting from the second one (
i = 1). - For each
intervals[i], check if it is compatible with the last kept interval. This is true ifintervals[i][0] >= end. - If they are compatible, it means we can keep this interval. Increment
countand updateendto the end time of the current interval:end = intervals[i][1]. - If they are not compatible (i.e., they overlap), we do nothing. We are effectively discarding the current interval because it conflicts with our previous choice, and our previous choice (which ends earlier) is the better greedy pick.
- After the loop,
countholds the maximum number of non-overlapping intervals. The number to remove isn - count.
Walkthrough
This approach reframes the problem from 'minimum intervals to remove' to 'maximum intervals to keep'. The key idea is to always pick the interval that finishes first. Why? Because this interval frees up the resource (the timeline) as early as possible, thus maximizing the opportunity to schedule more intervals later.
First, we sort the intervals based on their end times. We then initialize a count of non-overlapping intervals to 1, and set a variable end to the end time of the first interval in the sorted list. This signifies that we are keeping the first interval.
Next, we iterate through the rest of the sorted intervals. For each interval, we check if its start time is greater than or equal to end. If it is, the interval doesn't overlap with the last one we kept. This makes it a valid candidate to add to our non-overlapping set. We increment count and update end to the end time of this new interval. If an interval does overlap, we simply ignore it and move on. By ignoring it, we are effectively 'removing' it because our previous choice (the one with the earlier end time) is preserved.
Finally, the number of intervals to remove is the total number of intervals minus the count of intervals we were able to keep.
import java.util.Arrays;import java.util.Comparator; class Solution { public int eraseOverlapIntervals(int[][] intervals) { if (intervals.length <= 1) { return 0; } // Sort by end times Arrays.sort(intervals, Comparator.comparingInt(a -> a[1])); int count = 1; // Count of non-overlapping intervals int end = intervals[0][1]; for (int i = 1; i < intervals.length; i++) { // If the current interval does not overlap with the previous one we kept if (intervals[i][0] >= end) { count++; end = intervals[i][1]; } } // The number of intervals to remove is total - max non-overlapping return intervals.length - count; }}Complexity
Time
O(N log N), where N is the number of intervals. The cost is dominated by the sorting step.
Space
O(log N) or O(N), for the space required by the sorting algorithm, similar to the other greedy approach.
Trade-offs
Pros
Optimal time complexity of O(N log N).
The logic is very clean and straightforward once the greedy strategy is understood.
It is a classic and widely applicable algorithm for interval scheduling problems.
Cons
The correctness relies on the greedy choice proof, which might not be immediately obvious without prior exposure to activity selection problems.
Solutions
Solution
class Solution {public int eraseOverlapIntervals(int[][] intervals) { Arrays.sort(intervals, Comparator.comparingInt(a->a[1])); int t = intervals[0][1], ans = 0; for (int i = 1; i < intervals.length; ++i) { if (intervals[i][0] >= t) { t = intervals[i][1]; } else { ++ans; } } return ans; }}class Solution {public: int eraseOverlapIntervals(vector<vector<int>> &intervals) { sort(intervals.begin(), intervals.end(), [](const auto &a, const auto &b) { return a[1] < b[1]; }); int ans = 0, t = intervals[0][1]; for (int i = 1; i < intervals.size(); ++i) { if (t <= intervals[i][0]) t = intervals[i][1]; else ++ans; } return ans; }};class Solution: def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int: intervals . sort(key=lambda x: x[1]) ans, t = 0, intervals[0][1] for s, e in intervals[1:]: if s >= t: t = e else: ans += 1 return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.