Skip to content
DSAPrep

    ↑↓ to move · Enter to open · Esc to close

    MediumIntervals

    Non Overlapping Intervals

    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 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 do not need to remove any of the intervals since they are already non-overlapping.

    Constraints

    • 1 <= intervals.length <= 10^5
    • intervals[i].length == 2
    • -5 * 10^4 <= start_i < end_i <= 5 * 10^4
    View original on LeetCode ↗

    Brute Force (Longest Non-Overlapping Chain via DP)

    Time O(n²)Space O(n)

    Removing the fewest intervals to leave a non-overlapping set is the same as keeping the largest possible non-overlapping subset. Sort by start, then for every interval compute the longest chain of non-overlapping intervals that can follow it (classic “longest increasing subsequence”-style DP): dp[i] = 1 + max(dp[j]) over every j that starts at or after intervals[i] ends. The answer is n minus the best chain length.

    class Solution:
    def eraseOverlapIntervals(self, intervals: list[list[int]]) -> int:
    intervals.sort(key=lambda iv: iv[0])
    n = len(intervals)
    dp = [1] * n
    for i in range(n - 1, -1, -1):
    for j in range(i + 1, n):
    if intervals[j][0] >= intervals[i][1]:
    dp[i] = max(dp[i], 1 + dp[j])
    return n - max(dp)

    The nested loop compares every pair of intervals — O(n²) time, O(n) space for the dp array. Correct, but it recomputes overlap relationships that a single greedy pass can settle in one look-ahead.

    Greedy: Sort by End, Keep Earliest Finisher

    OptimalTime O(n log n)Space O(1)

    Sort intervals by their end time. Greedily keep an interval whenever it starts at or after the end of the last interval kept — keeping the interval that finishes earliest always leaves the most room for future intervals, so this greedy choice never costs an optimal solution. Every interval that cannot be kept must be removed.

    class Solution:
    def eraseOverlapIntervals(self, intervals: list[list[int]]) -> int:
    intervals.sort(key=lambda iv: iv[1])
    removals = 0
    prev_end = float('-inf')
    for start, end in intervals:
    if start >= prev_end:
    prev_end = end
    else:
    removals += 1
    return removals

    Watch the dashed prev_end gate sweep right: an interval is kept only when its bar reaches the line, and every removal is a bar that starts behind it.

    prev_end-infremovals0kept0

    Sorted by end time

    [1, 2]
    [2, 3]
    [1, 3]
    [3, 4]
    1234
    1 / 10
    currentresultdiscarded

    Sorted by end time: [1,2], [2,3], [1,3], [3,4]. Nothing is kept yet, so prev_end sits below every interval and the first one always fits.

    Why sorting by end (not start) matters: the interval that ends earliest always leaves the most room for everything after it, so it is always safe to keep. Complexity: dominated by the sort → O(n log n) time; only a running counter and prev_end are tracked → O(1) extra space.

    More Intervals practice