Blind Plus 100 Visualized

#65 ยท Intervals

Non-overlapping Intervals

Given a list of intervals, return the minimum number of intervals to remove so that the remaining ones do not overlap. Intervals that only touch (one ends where the next starts) do not overlap.

MediumLeetCode
  • Being examined
  • New / inserted
  • Kept / merged / answer
  • Removed / conflict
  • Untouched
step 1 / 1

Pointers

Variables

Hash map / table

Stack / queue

Foundations & data-structure selection

Activity selection (greedy by earliest end)

Removing the fewest intervals is the same as keeping the most. Among compatible choices, the interval that ends first leaves the most room, so always keep it. After sorting by end time, one pass keeps an interval whenever it starts no earlier than the last kept end.

Memory layout

A sorted array and two numbers (last end, count).

OperationCost
timeO(n log n)
spaceO(1) beyond the sort

Longest chain DP

dp[i] = the most intervals that can be kept if interval i is the last one, found by checking every earlier compatible interval. It is the interval version of the longest increasing subsequence and is O(nยฒ).

Memory layout

One integer per interval.

OperationCost
time / spaceO(nยฒ) / O(n)

Why it fits

  • Keeping the maximum number of mutually compatible intervals is the classic interval scheduling problem, and its greedy solution (earliest finish first) is provably optimal.
  • An exchange argument shows it: any optimal solution can swap its first interval for the one that ends earliest without creating a conflict.
  • Sorting by start and dropping the later-ending interval on a conflict is the same greedy in disguise: it keeps the smaller prevEnd.

Why alternatives degrade

Greedy by earliest start:
A long interval that starts first can block many short ones: [1,100] would be kept while [2,3], [4,5], [6,7] are all removed.
Greedy by shortest length:
A short interval in the middle can overlap two long ones that would otherwise both be kept, giving a worse answer.
Trying all subsets:
There are 2โฟ subsets, and checking each is O(n).

Four approaches, from naive to production

1 ยท beginner

Recursion: keep it or skip it

time O(2โฟ)space O(n)

Sort by start. For each interval, either skip it or keep it if it does not overlap the last kept one, and take the better outcome. The answer is the total count minus the most that can be kept.

int eraseOverlapIntervals(vector<vector<int>>& intervals) {
    vector<vector<int>> sorted = intervals; sort(sorted.begin(), sorted.end());
    function<int(int, int)> keep = [&](int i, int lastEnd) -> int {
        if (i == (int)sorted.size()) return 0;
        int best = keep(i + 1, lastEnd);
        if (sorted[i][0] >= lastEnd) best = max(best, 1 + keep(i + 1, sorted[i][1]));
        return best;
    };
    return (int)sorted.size() - keep(0, -1000000000);
}

Flaws & bottlenecks

  • Two branches per interval gives an exponential number of calls.
  • The same (position, last end) state can be reached through different choices and is recomputed.
  • Recursion depth is n.

Interviewer cross-questions

What is the state of the recursion?

Say: The position i and the end of the last kept interval. With memoisation there are at most nยฒ such states, which leads to the DP version.

Why is skipping always allowed?

Say: Removing an interval can never create an overlap, so the "skip" branch is always valid.

Why count removals as n minus the kept count?

Say: Every interval is either kept or removed, so minimising removals is maximising kept intervals.

2 ยท intermediate

DP: longest chain of compatible intervals

time O(nยฒ)space O(n)

Sort by start. dp[i] is the longest chain of compatible intervals that ends with interval i: one more than the best dp[j] over earlier intervals j that end no later than i starts.

int eraseOverlapIntervals(vector<vector<int>>& intervals) {
    vector<vector<int>> sorted = intervals; sort(sorted.begin(), sorted.end());
    int n = sorted.size();
    vector<int> dp(n, 1);
    int best = 1;
    for (int i = 1; i < n; i++) {
        for (int j = 0; j < i; j++) {
            if (sorted[j][1] <= sorted[i][0]) dp[i] = max(dp[i], dp[j] + 1);
        }
        best = max(best, dp[i]);
    }
    return n - best;
}

Flaws & bottlenecks

  • Quadratic: too slow for 100,000 intervals.
  • Two nested loops over mostly irrelevant pairs.
  • Needs the sort by start for the dependency order.

Interviewer cross-questions

How does this relate to the longest increasing subsequence?

Say: It is the same shape: dp[i] depends on earlier items that are "compatible" with i. With an end-sorted order, the binary-search trick would give O(n log n), but the greedy argument gives it more simply.

Why sort by start?

Say: Then every compatible predecessor of i has a smaller index, so dp can be filled left to right.

What is the answer from dp?

Say: n minus the largest dp value: all other intervals are removed.

3 ยท advanced

Greedy: earliest end first

time O(n log n)space O(1) beyond the sort

Sort by end time. Keep the first interval, then keep each later interval whose start is no earlier than the last kept end. The number removed is n minus the number kept.

int eraseOverlapIntervals(vector<vector<int>>& intervals) {
    vector<vector<int>> sorted = intervals;
    sort(sorted.begin(), sorted.end(), [](const vector<int>& a, const vector<int>& b) { return a[1] < b[1]; });
    int kept = 1;
    int end = sorted[0][1];
    for (int i = 1; i < (int)sorted.size(); i++) {
        if (sorted[i][0] >= end) {
            kept++;
            end = sorted[i][1];
        }
    }
    return (int)sorted.size() - kept;
}

Flaws & bottlenecks

  • The correctness proof (exchange argument) is less obvious than the DP.
  • Must sort by END, not start (a common mistake).
  • Ties in end time need no special handling, but touching intervals must be allowed (>= not >).

Interviewer cross-questions

Why does keeping the earliest-ending interval never hurt?

Say: Take any optimal solution. Replace its first interval by the earliest-ending one: it ends no later, so it cannot overlap anything the original did not. The count stays the same, so keeping it is safe.

Why >= and not >?

Say: The problem treats intervals that only touch ([1,2] and [2,3]) as non-overlapping, so a start equal to the last end is allowed.

What if intervals have weights?

Say: Then greedy fails and you need weighted interval scheduling: sort by end and use DP with binary search for the latest compatible interval.

4 ยท expert

Greedy by start, keep the earlier end

time O(n log n)space O(1) beyond the sort

Sort by start and remember only the end of the interval currently kept. When the next interval overlaps it, count one removal and keep whichever ends earlier (prevEnd = min); otherwise it becomes the new kept interval.

int eraseOverlapIntervals(vector<vector<int>>& intervals) {
    vector<vector<int>> sorted = intervals; sort(sorted.begin(), sorted.end());
    int prevEnd = sorted[0][1];
    int removed = 0;
    for (int i = 1; i < (int)sorted.size(); i++) {
        if (sorted[i][0] < prevEnd) {
            removed++;
            prevEnd = min(prevEnd, sorted[i][1]);
        } else {
            prevEnd = sorted[i][1];
        }
    }
    return removed;
}

Flaws & bottlenecks

  • It never records which intervals were removed, only how many.
  • The "keep the earlier end" step is subtle: the removed interval is not necessarily the one just read.
  • Sorting by start with ties in arbitrary order still works but is harder to argue about.

Interviewer cross-questions

Is this really the same as the end-sorted greedy?

Say: Yes. On a conflict you drop the interval that ends later, which is exactly what the earliest-end rule would have preferred. It lets you sort by start, the same order other interval problems (merging) use.

Why use min on the conflict?

Say: Keeping the interval with the smaller end leaves the most room for the intervals still to come.

How would you also return the removed intervals?

Say: On a conflict, record the one with the larger end as removed. That needs the intervals themselves, not just the end value.

Synchronized code walkthrough

Showing: Recursion: keep it or skip it โ€” shares the player above; click any step to jump.

int eraseOverlapIntervals(vector<vector<int>>& intervals) {
    vector<vector<int>> sorted = intervals; sort(sorted.begin(), sorted.end());
    function<int(int, int)> keep = [&](int i, int lastEnd) -> int {
        if (i == (int)sorted.size()) return 0;
        int best = keep(i + 1, lastEnd);
        if (sorted[i][0] >= lastEnd) best = max(best, 1 + keep(i + 1, sorted[i][1]));
        return best;
    };
    return (int)sorted.size() - keep(0, -1000000000);
}