Given an array of intervals intervals where intervals[i] = [start_i, end_i], return the minimum number of intervals you need to remove to make the rest of the intervals non-overlapping.
Note: Intervals are non-overlapping even if they have a common point. For example, [1, 3] and [2, 4] are overlapping, but [1, 2] and [2, 3] are non-overlapping.
Example 1:
Input: intervals = [[1,2],[2,4],[1,4]]
Output: 1Explanation: After [1,4] is removed, the rest of the intervals are non-overlapping.
Example 2:
Input: intervals = [[1,2],[2,4]]
Output: 0Constraints:
1 <= intervals.length <= 1000intervals[i].length == 2-50000 <= starti < endi <= 50000
You should aim for a solution with O(nlogn) time and O(n) space, where n is the size of the input array.
If two intervals are sorted in ascending order by their start values, they overlap if the start value of the second interval is less than the end value of the first interval. And these are called overlapping intervals.
A brute force approach would be to sort the given intervals in ascending order based on their start values and recursively explore all possibilities. This would be an exponential approach. Can you think of a better way? Maybe a greedy approach works here.
We first sort the given intervals based on their start values to efficiently check for overlaps by comparing adjacent intervals. We then iterate through the sorted intervals from left to right, keeping track of the previous interval’s end value as prevEnd, initially set to the end value of the first interval.
We then iterate from the second interval. If the current interval doesn't overlap, we update prevEnd to the current interval's end and continue. Otherwise, we set prevEnd to the minimum of prevEnd and the current interval’s end, greedily removing the interval that ends last to retain as many intervals as possible.
We want to remove the minimum number of intervals so that the remaining intervals do not overlap.
A helpful way to think about this is:
minimum removals = total intervals - maximum keptTo make decisions, we sort the intervals by start time and use recursion to explore two choices at each interval:
The recursive function represents:
"What is the maximum number of non-overlapping intervals we can keep starting from index i, given that the last chosen interval is prev?"
dfs(i, prev):i is the current interval indexprev is the index of the last chosen interval (-1 if none chosen yet)i reaches the end of the list, return 0 (no more intervals to take)res = dfs(i + 1, prev)prev == -1 or intervals[prev][1] <= intervals[i][0]:res = max(res, 1 + dfs(i + 1, i))res, the maximum number of intervals we can keep from this state.kept = dfs(0, -1)len(intervals) - kept as the minimum number of intervals to remove.class Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
intervals.sort()
def dfs(i, prev):
if i == len(intervals):
return 0
res = dfs(i + 1, prev)
if prev == -1 or intervals[prev][1] <= intervals[i][0]:
res = max(res, 1 + dfs(i + 1, i))
return res
return len(intervals) - dfs(0, -1)We want to remove the minimum number of intervals so that the remaining intervals do not overlap.
A common trick is to flip the problem:
minimum removals = total intervals - maximum keptThis solution sorts intervals by end time. After that, for any interval i, the next interval we choose must start at or after intervals[i][1].
We define a DP state that answers:
"If we choose interval i as part of our set, what is the maximum number of non-overlapping intervals we can take starting from i?"
The result for an index depends on future indices, and many states repeat, so we use memoization.
n be the number of intervals.memo to store results for each index i.dfs(i):i includedi is already in memo, return the stored value.res = 1 because we are taking interval i.j where j > i:intervals[i][1] <= intervals[j][0]res = max(res, 1 + dfs(j))res in memo[i] and return it.dfs(0).n - dfs(0) as the minimum number of intervals to remove.class Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
intervals.sort(key = lambda x: x[1])
n = len(intervals)
memo = {}
def dfs(i):
if i in memo:
return memo[i]
res = 1
for j in range(i + 1, n):
if intervals[i][1] <= intervals[j][0]:
res = max(res, 1 + dfs(j))
memo[i] = res
return res
return n - dfs(0)We want to remove the minimum number of intervals so that the remaining intervals do not overlap.
A useful trick is to instead find the maximum number of non-overlapping intervals we can keep.
Once we know that:
minimum removals = total intervals - maximum keptAfter sorting intervals by end time, we can build a DP array where:
dp[i] = the maximum number of non-overlapping intervals we can keep ending at interval i (meaning interval i is included)To compute dp[i], we look at all earlier intervals j < i:
j ends before interval i starts, they can both be kept1 + dp[j]n be the number of intervals.dp of size n.i from 0 to n - 1:dp[i] = 1 (we can always keep interval i alone)j from 0 to i - 1:intervals[j][1] <= intervals[i][0]:dp[i] = max(dp[i], 1 + dp[j])dp, the maximum number of non-overlapping intervals we can keep is:max_non_overlapping = max(dp)n - max_non_overlappingclass Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
intervals.sort(key=lambda x: x[1])
n = len(intervals)
dp = [0] * n
for i in range(n):
dp[i] = 1
for j in range(i):
if intervals[j][1] <= intervals[i][0]:
dp[i] = max(dp[i], 1 + dp[j])
max_non_overlapping = max(dp)
return n - max_non_overlappingWe want to remove the minimum number of intervals so that the remaining intervals do not overlap.
A common trick is to flip the goal:
minimum removals = total intervals - maximum keptAfter sorting intervals by end time, consider interval i:
i → keep the best answer up to i - 1i → then we must take it after the last interval that endsintervals[i][0]The slow part is finding that "previous compatible interval".
Because the intervals are sorted by end time, we can find it using binary search.
We maintain:
dp[i] = maximum number of non-overlapping intervals we can keep using intervals 0..idp of size n:dp[i] stores the maximum number of non-overlapping intervals we can keepi + 1 intervalsdp[0] = 1 since we can always keep the first interval.i from 1 to n - 1:idx:[0, i) where intervals[pos][1] > intervals[i][0]idx - 1 is the last interval that does not overlap with interval idp[i]:idx == 0):i alone, so compare with skipping:dp[i] = dp[i - 1]i → dp[i - 1]i → 1 + dp[idx - 1]dp[i] = max(dp[i - 1], 1 + dp[idx - 1])dp[n - 1].n - dp[n - 1] as the minimum number of intervals to remove.class Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
intervals.sort(key=lambda x: x[1])
n = len(intervals)
dp = [0] * n
dp[0] = 1
def bs(r, target):
l = 0
while l < r:
m = (l + r) >> 1
if intervals[m][1] <= target:
l = m + 1
else:
r = m
return l
for i in range(1, n):
idx = bs(i, intervals[i][0])
if idx == 0:
dp[i] = dp[i - 1]
else:
dp[i] = max(dp[i - 1], 1 + dp[idx - 1])
return n - dp[n - 1]We want to remove the minimum number of intervals so that the remaining intervals do not overlap.
A greedy strategy works well here. After sorting intervals by their start time, we process them from left to right and always keep the interval that ends earlier when an overlap occurs.
Why this works:
So instead of choosing which interval to keep globally, we make a local greedy decision whenever an overlap happens.
prevEnd as the end of the first intervalres = 0 to count how many intervals we remove(start, end):start >= prevEnd:prevEnd = endresprevEnd = min(end, prevEnd)resWe want to remove the minimum number of intervals so that the remaining intervals do not overlap.
A very clean greedy idea is to always keep the interval that ends earliest.
Why? Because an interval that ends earlier leaves more room for future intervals, reducing the chance of overlap later.
So instead of deciding which intervals to remove directly, we:
Whenever we see an overlap:
This greedy choice is optimal and ensures the maximum number of intervals are kept.
prevEnd as the end of the first intervalres = 0 to count how many intervals we remove(start, end):start < prevEnd:resprevEnd = endresclass Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
intervals.sort(key = lambda pair: pair[1])
prevEnd = intervals[0][1]
res = 0
for i in range(1, len(intervals)):
if prevEnd > intervals[i][0]:
res += 1
else:
prevEnd = intervals[i][1]
return resSorting by start time instead of end time (or vice versa without proper handling) leads to suboptimal solutions. While both can work, sorting by end time gives a cleaner greedy solution because the interval that ends earliest always leaves the most room for future intervals. Sorting by start time requires additional logic to handle overlaps correctly.
Using < instead of <= (or vice versa) when checking for overlaps causes off-by-one errors. Intervals [1, 2] and [2, 3] do NOT overlap because one ends exactly where the other begins. The condition should be intervals[i][0] >= prevEnd for non-overlapping, not intervals[i][0] > prevEnd.
Confusing what to count leads to returning the wrong answer. The problem asks for the minimum number of intervals to REMOVE, not the maximum to keep. If you count intervals kept, remember to return n - kept as the final answer. This subtle difference causes many incorrect submissions.