Find the minimum number of intervals to remove so the rest are non-overlapping. The greedy trick is sorting by end time — not start — so you always keep the interval that finishes earliest and leaves the most room for future ones.
Given an array of intervals [start, end], return the minimum number of intervals you must remove so that no two remaining intervals overlap. Example: [[1,2],[2,3],[3,4],[1,3]] → remove [1,3] (1 removal). Example: [[1,2],[1,2],[1,2]] → remove 2, keep one. Two intervals that only share an endpoint ([1,2] and [2,3]) do not overlap.
n − (max non-overlapping kept).0.intervals.sort((a,b) => a[1]-b[1]). This ordering is the entire insight — the first interval in the sorted list is always safe to keep.lastEnd = intervals[0][1], removed = 0.intervals[i][0] >= lastEnd: no overlap → keep, set lastEnd = intervals[i][1]. Otherwise: overlap → increment removed (the current interval ends later and is strictly worse to keep).removed.[[1,10],[2,3],[3,5]]. Sorted by start: [1,10] comes first and you keep it, forcing removal of two more. Sorted by end: [2,3]comes first, you keep all three. Start-sorting maximises the "hole" in the future; end-sorting minimises it. Always sort by end.After sorting the single scan is O(n). Total: O(n log n) time, O(1) space (or O(n) if the sort is not in-place, e.g. in JS environments). There is no sub-quadratic DP approach that beats the greedy here — the exchange-argument proof shows the greedy is optimal.
4 intervals by end time. Sorting by end (not start) ensures we always commit to the interval that ends soonest — leaving maximum room for the future.function eraseOverlapIntervals(intervals: number[][]): number {
if (intervals.length === 0) return 0;
// Sort by END time — greedy optimality: commit to the interval
// that ends soonest, leaving the most room for future intervals.
intervals.sort((a, b) => a[1] - b[1]);
let removed = 0;
let lastEnd = intervals[0][1]; // end of the last kept interval
for (let i = 1; i < intervals.length; i++) {
if (intervals[i][0] >= lastEnd) {
// No overlap: keep this interval, advance the boundary
lastEnd = intervals[i][1];
} else {
// Overlap: remove this interval (it ends later — keeping it
// would only block more future intervals)
removed++;
}
}
return removed;
}0 immediately.a[1] - b[1] puts the earliest-ending interval first. Every correctness guarantee flows from this ordering.lastEnd starts at its end value. removed starts at 0.lastEnd there is no overlap — keep it and advance lastEnd. Otherwise it overlaps the last kept interval; because we sorted by end the current interval ends at least as late, so removing it (not the previous one) is always the correct greedy choice.removed is the minimum number of intervals that must be deleted.[1,100]) would be kept first, blocking everything behind it. Sorting by end guarantees we always keep the interval with the smallest "footprint" on the future timeline.start >= lastEnd already handles touching endpoints correctly — they are not considered overlapping per the problem statement.| "minimum removals" over intervals | sort by end, greedy scan |
| maximize non-overlapping intervals kept | end-sort + keep if start ≥ lastEnd |
| interval scheduling / activity selection | greedy by earliest finish time |
| arrows to burst balloons (LC 452) | same end-sort, count groups |
// Sort by END (not start!)
intervals.sort((a, b) => a[1] - b[1]);
let removed = 0;
let lastEnd = intervals[0][1];
for (let i = 1; i < intervals.length; i++) {
if (intervals[i][0] >= lastEnd) {
lastEnd = intervals[i][1]; // keep, advance boundary
} else {
removed++; // overlap, remove
}
}
return removed;[[1,2],[2,3],[3,4],[1,3]]. How many intervals are removed?[1,2] and [2,3], are considered:[[1,100],[11,22],[1,11],[2,12]] — how many removals are needed?