Non-overlapping Intervals

Medium Greedy

Problem

Given a collection of intervals, return the minimum number of intervals you need to remove to make the rest of the intervals non-overlapping.

  • 1 ≤ intervals.length ≤ 10⁵
  • intervals[i].length == 2
  • −5 * 10⁴ ≤ start_i < end_i ≤ 5 * 10⁴

Example

Input: intervals = [[1,2],[2,3],[3,4],[1,3]]
Output: 1

A brute-force approach would check all subsets of intervals to find the largest non-overlapping set, which is exponential in time. Instead, the algorithm sorts intervals by start time and iterates through them, greedily deciding which intervals to keep or remove. For example, after sorting, intervals are [[1,2],[1,3],[2,3],[3,4]]. Starting with the first interval [1,2], the algorithm compares subsequent intervals. When it encounters [1,3], it overlaps with [1,2], so it removes one. It chooses to keep the interval with the earlier end time to maximize room for future intervals. This process continues, resulting in removing only one interval ([1,3]) to achieve a non-overlapping set.

Approach

Straightforward Solution

A brute-force approach tries all subsets of intervals to find the largest non-overlapping subset, which is O(2^n) and infeasible for large inputs.

Core Observation

The fundamental truth is that to minimize removals, one must maximize the number of non-overlapping intervals. Overlaps occur when the start of the current interval is less than the end of the previous chosen interval.

Path to Optimal

Sorting intervals by start time allows a linear scan to detect overlaps. When an overlap is found, removing the interval with the later end time preserves more space for future intervals, maximizing the count of non-overlapping intervals. This greedy choice is optimal because it leaves the earliest possible end time, increasing the chance that subsequent intervals fit without overlap.

Optimal Approach

Sort intervals by their start time. Initialize a variable to track the end of the last included interval. Iterate through intervals starting from the second one. If the current interval's start is at least the previous end, update the previous end to the current interval's end. Otherwise, increment the removal count and update the previous end to the minimum of the current and previous ends. Return the removal count after processing all intervals.

Time

O(n log n)

Sorting the intervals takes O(n log n). The subsequent single pass through the intervals is O(n), resulting in overall O(n log n) time.

Space

O(1)

The algorithm uses a constant amount of extra space for counters and pointers, not counting the input array.

Pattern Spotlight

Greedy Algorithms (Interval Scheduling)

When minimizing removals to avoid overlaps, always keep the interval with the earliest end time among overlapping intervals to maximize the remaining space for future intervals.

Solution

1class Solution:
2 def eraseOverlapIntervals(self, intervals: list[list[int]]) -> int:
3 intervals.sort()
4 res = 0
5 prev_end = intervals[0][1]
6
7 for start, end in intervals[1:]:
8 if start >= prev_end:
9 prev_end = end
10 else:
11 res += 1
12 prev_end = min(end, prev_end)
13
14 return res

Step-by-Step Solution

1

Sort Intervals by Start Time to Prepare for Greedy Selection

3intervals.sort()

Objective

To order intervals so that overlaps can be detected in a single pass by comparing adjacent intervals.

Key Insight

Sorting intervals by their start time arranges them in a sequence where any overlap must occur between consecutive intervals. This ordering is crucial because it allows the algorithm to detect and resolve overlaps efficiently without backtracking or complex data structures.

Interview Quick-Check

Core Logic

Sorting by start time ensures that any overlapping intervals appear consecutively, enabling a linear scan to detect overlaps.

Complexity

Sorting dominates the time complexity at O(n log n), which is acceptable for large inputs.

2

Iterate Through Intervals to Count and Resolve Overlaps

4res = 0
5prev_end = intervals[0][1]
7for start, end in intervals[1:]:
8 if start >= prev_end:
9 prev_end = end
10 else:
11 res += 1
12 prev_end = min(end, prev_end)

Objective

To scan intervals and increment removal count when overlaps are detected, updating the tracking end time to maintain the earliest possible end.

Key Insight

When the current interval overlaps with the previous one (start < prev_end), removing the interval with the later end time preserves the earliest end boundary, maximizing space for future intervals. This greedy choice is the key to minimizing removals. Updating prev_end to the minimum end time ensures the algorithm always tracks the tightest boundary.

Interview Quick-Check

Core Logic

When intervals overlap, increment removal count and update prev_end to the smaller end time to keep the interval that allows more future intervals.

State & Boundaries

The loop starts from the second interval because the first interval is always included initially.

Common Pitfalls & Bugs

Failing to update prev_end to the minimum end time when removing an interval can cause incorrect overlap detection later.

3

Return the Total Number of Intervals Removed

14return res

Objective

To output the minimum number of intervals that must be removed to eliminate all overlaps.

Key Insight

After processing all intervals, the removal count reflects the minimal removals needed because the algorithm greedily kept intervals with earliest end times, ensuring maximal non-overlapping intervals.

Interview Quick-Check

Core Logic

Returning the removal count completes the solution, representing the minimal removals required.

Line Analysis

This solution has 2 Critical lines interviewers watch for.

Line 12 Critical
prev_end = min(end, prev_end)

Update prev_end to the minimum of current and previous ends to keep earliest finishing interval.

Updating prev_end to the minimum end time is the core greedy step that guarantees optimality by always keeping the interval that frees up the most room for subsequent intervals.

Line 11 Critical
res += 1

Increment removal count due to overlap.

Incrementing the removal count here is critical because it directly accounts for the minimal number of intervals that must be removed to resolve detected overlaps.

Line 3 Core
intervals.sort()

Sort intervals by their start time in ascending order.

Sorting by start time arranges intervals so that any overlap can be detected by comparing consecutive intervals, enabling a linear scan to resolve conflicts efficiently.

Line 8 Core
if start >= prev_end:

Check if current interval starts after or at the previous end time (no overlap).

If the current interval does not overlap with the previous, update prev_end to the current interval's end to continue tracking the boundary.

Line 10 Core
else:

Handle the case where current interval overlaps with previous.

Detecting overlap triggers removal logic to maintain a non-overlapping set.

Line 5 Core
prev_end = intervals[0][1]

Set prev_end to the end time of the first interval.

Tracking the end of the last included interval allows the algorithm to detect overlaps with subsequent intervals by comparing start times.

Line 9 Standard
prev_end = end

Update prev_end to current interval's end when no overlap occurs.

Updating prev_end to the current interval's end maintains the correct boundary for future overlap checks.

Line 7 Standard
for start, end in intervals[1:]:

Iterate over intervals starting from the second interval.

The first interval is always included initially, so iteration begins at the second to check for overlaps.

Line 14 Standard
return res

Return the total count of removed intervals.

Returning the accumulated removal count provides the minimal number of intervals to remove to achieve a non-overlapping set.

Line 4 Basic
res = 0

Initialize removal count to zero.

This variable accumulates the number of intervals removed to achieve a non-overlapping set, forming the final output.

Test Your Understanding

Why does choosing to keep the interval with the earliest end time among overlapping intervals lead to an optimal solution?

Because keeping the interval that ends earliest leaves the maximum room for subsequent intervals to fit without overlapping, thereby maximizing the number of intervals kept and minimizing removals.

Related Problems

Greedy pattern

Don't just read it. Drill it.

Reconstruct Non-overlapping Intervals from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.

Unlock the Non-overlapping Intervals drill

or drill a free problem