TL;DR
Greedy: sort by end time, keep every interval that starts after the last kept end β O(n log n) time, O(1) extra space.
Approach 1 β Brute force: try keeping or removing each interval
For every interval, branch on βremove itβ (cost 1) versus βkeep itβ (cost 0, but it constrains what can follow). Sorting by start first lets each branch carry just one piece of state: the end of the last kept interval.
from typing import List
class Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
intervals.sort(key=lambda it: it[0])
n = len(intervals)
def best(i: int, last_end: int) -> int:
if i == n:
return 0
# Option A: remove interval i.
removed = 1 + best(i + 1, last_end)
# Option B: keep it, if it fits.
if intervals[i][0] >= last_end:
kept = best(i + 1, intervals[i][1])
return min(removed, kept)
return removed
return best(0, float("-inf"))
Complexity: O(2^n) time, O(n) recursion depth. last_end is not a small state (it can be any coordinate), so plain memoization doesnβt tame it directly β and with n = 10^5, exponential is dead on arrival.
Approach 2 β DP: longest non-overlapping subsequence
The insight: removals are minimized exactly when the kept set is maximized, and βmaximum set of mutually non-overlapping intervalsβ has optimal substructure just like Longest Increasing Subsequence. Sort by start; let dp[i] be the size of the largest non-overlapping set whose last interval is i; extend from any earlier j that ends by the time i starts.
from typing import List
class Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
intervals.sort(key=lambda it: it[0])
n = len(intervals)
dp = [1] * n
for i in range(n):
for j in range(i):
if intervals[j][1] <= intervals[i][0]:
dp[i] = max(dp[i], dp[j] + 1)
return n - max(dp)
Walkthrough on [[1,2],[2,3],[3,4],[1,3]]: sorted by start β [[1,2],[1,3],[2,3],[3,4]].
dp[0] = 1 for [1,2].
[1,3]: no earlier interval ends by 1 β dp[1] = 1.
[2,3]: [1,2] ends at 2 <= 2 β dp[2] = 2.
[3,4]: [1,3] and [2,3] both end by 3 β dp[3] = dp[2] + 1 = 3.
- Keep
max(dp) = 3 intervals, so remove 4 - 3 = 1. Matches the example.
Complexity: O(n^2) time, O(n) space. Correct, but n = 10^5 means ~10^10 pair checks β the constraints kill this one too. Itβs worth knowing because it generalizes (e.g., to weighted interval scheduling, where greedy fails).
Approach 3 β Greedy by earliest end (activity selection)
The insight: among all intervals you could keep next, the one that ends earliest is always a safe choice β swapping it in place of any other candidate can only leave more room for later intervals (a classic exchange argument). So sort by end and greedily keep everything compatible.
from typing import List
class Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
intervals.sort(key=lambda it: it[1])
removed = 0
last_end = float("-inf")
for start, end in intervals:
if start >= last_end:
last_end = end # keep it
else:
removed += 1 # overlaps the kept set
return removed
Walkthrough on [[1,2],[2,3],[3,4],[1,3]]: sorted by end β [[1,2],[2,3],[1,3],[3,4]].
[1,2]: 1 >= -inf β keep, last_end = 2.
[2,3]: 2 >= 2 β keep (touching is fine), last_end = 3.
[1,3]: 1 >= 3 is false β remove, removed = 1.
[3,4]: 3 >= 3 β keep, last_end = 4.
- Answer
1. Matches the example β and note the greedy removed exactly the interval the explanation named.
Complexity: O(n log n) time for the sort, O(1) extra space.
Common pitfalls
- Sorting by start and greedily keeping the first compatible interval β a long early interval like
[1,100] then blocks everything; the greedy is only correct sorted by end. (A start-sorted greedy can be repaired by always discarding the larger end on conflict, but end-sorted is the cleaner statement.)
- Using
start > last_end instead of >= β that counts touching intervals like [1,2] and [2,3] as conflicts and over-removes.
- Returning the size of the kept set instead of
n - kept (or vice versa) β read what the problem asks for.
- Reaching for the
O(n^2) DP under these constraints β it times out at n = 10^5; save it for the weighted variant.
Pattern takeaway
When the question is βkeep the most / remove the fewest intervals,β sort by end time and greedily commit to the earliest-ending compatible interval β finishing early can never hurt the future. Contrast with merge/overlap questions, which sort by start. Which endpoint you sort on is the first decision of every intervals problem, and this problem is the canonical reason end-sorting exists.