Greedy Interval Scheduling
Given a pile of intervals, keep as many as possible with no two overlapping. The obvious greedy choices are wrong in an instructive way:
- Earliest start? One interval starting at time 0 and running all day blocks everything.
- Shortest duration? A short interval planted in the middle can break two long compatible ones.
The choice that is optimal: always keep the interval that finishes earliest. Finishing early leaves the most room for everything after it, and that intuition can be turned into a proof.
This page is as much about the exchange argument as the code, because “sort by end” is three lines and “why is that optimal?” is the actual interview question.
What you’ll learn
Section titled “What you’ll learn”- The sort-by-end template, and the one comparison it hinges on.
- The exchange argument — how to prove a greedy choice is safe.
- Why the same code answers “erase the fewest”, “shoot the fewest arrows”, and “longest chain”.
- The
>vs>=decision, which is the only thing distinguishing several of these problems. - Three real LeetCode problems solved in the browser: 435, 452, 646.
The cue
Section titled “The cue”Visual intuition
Section titled “Visual intuition”Sorted by end time, not start. That choice is the whole insight:
Sorting by start time is the intuitive move and it is wrong: one long interval starting early can block several short ones. The exchange argument for end-time sorting is what makes this greedy provably optimal rather than merely plausible.
The template
Section titled “The template”def max_non_overlapping(intervals):
if not intervals:
return 0
intervals.sort(key=lambda x: x[1]) # sort by END time
kept = 1
last_end = intervals[0][1]
for start, end in intervals[1:]:
if start >= last_end: # compatible: starts after we finished
kept += 1
last_end = end # commit to this one
# else: overlaps -- skip it, keep the earlier-finishing one
return kept
print(max_non_overlapping([[1, 2], [2, 3], [3, 4], [1, 3]])) # 3for the sort, for the scan, extra space.
That is the entire pattern. Everything below is either a proof of it or a rewording of it.
Why earliest-finish is optimal — the exchange argument
Section titled “Why earliest-finish is optimal — the exchange argument”This is the reasoning interviewers are fishing for. Learn the shape; it transfers to every greedy proof.
Let g be the interval finishing earliest overall, and let OPT be any
optimal solution.
- If
OPTalready containsg, there is nothing to show. - Otherwise, let
fbe the interval inOPTthat finishes earliest. By definition ofg, we knowend(g) <= end(f). - Swap
fforginOPT. Is the result still valid? Every other interval inOPTstarts at or afterend(f), andend(g) <= end(f), so they all start at or afterend(g)too. No new overlap is created. - The swap keeps the size identical, so the modified solution is also
optimal — and it contains
g.
So there is always an optimal solution containing the earliest-finishing
interval. Take g, discard everything that overlaps it, and recurse on the
rest — each step is safe by the same argument.
Dry run
Section titled “Dry run”intervals = [[1,2], [2,3], [3,4], [1,3]]. Sorting by end time reorders them to
[1,2] · [2,3] · [1,3] · [3,4] — note [1,3] lands third, after [2,3], because ties on
the end value keep their relative order and 3 = 3.
| interval | start >= last_end? | decision | last_end | kept |
|---|---|---|---|---|
[1,2] | — (seed) | keep | 2 | 1 |
[2,3] | 2 ≥ 2 ✓ | keep | 3 | 2 |
[1,3] | 1 ≥ 3 ✗ | skip — overlaps what we already committed to | 3 | 2 |
[3,4] | 3 ≥ 3 ✓ | keep | 4 | 3 |
Answer 3, and therefore LC 435’s answer — removals — is 4 − 3 = 1.
- The scan never reconsiders. Once
[2,3]is committed,[1,3]is rejected without comparing it against anything else. That is what makes this greedy rather than a DP, and the exchange argument above is what licenses it. start >= last_end, not>. Intervals are half-open here:[2,3]may begin exactly where[1,2]ends. If the problem’s intervals are inclusive ([2,3]and[1,2]both occupy point 2, as in some scheduling variants) the test becomesstart > last_end. One character, and it only shows up on the touching-intervals test case.[1,3]is the interval that punishes sorting by start. Sorted by start it would come second, be kept, and block[2,3]— giving 2 instead of 3. Sorted by end, the earliest-finishing option always wins, which is exactly the property the proof needs.
Complexity
Section titled “Complexity”| Step | Cost |
|---|---|
| Sort by end time | — dominates |
| Single scan | |
| Extra space | beyond the sort (Python’s sort is auxiliary) |
| Total | time |
The is a hard floor for the comparison-based version, and worth saying why: this problem is at least as hard as detecting whether any two intervals overlap, which is element-distinctness-like and in the comparison model. If the input is already sorted by end time, the whole thing is — worth asking about, because several LeetCode variants hand you sorted input (LC 57) and the follow-up is often “what if it were already sorted?”
Contrast the alternatives on the same problem:
| Approach | Time | Correct? |
|---|---|---|
| Sort by end, greedy scan | ✅ | |
| Sort by start, greedy scan | ❌ — fails [[1,100],[2,3],[4,5]] | |
| Sort by duration, greedy scan | ❌ — fails [[1,5],[4,6],[5,9]] | |
| DP over intervals (weighted case) | with binary search | ✅ — and required once intervals carry weights |
That last row is the boundary: greedy counts how many, but as soon as each interval has a value and you want maximum total value, earliest-finish is no longer optimal and you need the DP (LC 1235).
The variant map
Section titled “The variant map”All four of these are the same scan. Only the comparison and the returned quantity change.
| Problem | Sort by | Keep when | Return |
|---|---|---|---|
| 435 Erase overlaps | end | start >= last_end | len - kept |
| 452 Burst balloons | end | start > last_end | kept (arrows) |
| 646 Longest chain | end | start > last_end | kept |
| 1024 Video stitching | start | (different — interval covering) | jumps |
Practice — real LeetCode problems
Section titled “Practice — real LeetCode problems”LC 435 — Non-overlapping Intervals · Medium
Section titled “LC 435 — Non-overlapping Intervals · Medium”Problem. Given an array of intervals, return the minimum number you must remove so that the rest are non-overlapping.
Constraints. 1 <= len(intervals) <= 10^5,
-5 * 10^4 <= start < end <= 5 * 10^4.
Examples. [[1,2],[2,3],[3,4],[1,3]] gives 1 (remove [1,3]) ·
[[1,2],[1,2],[1,2]] gives 2 · [[1,2],[2,3]] gives 0
Editorial — approach, complexity, follow-ups
The reframe is the insight: minimising removals is maximising retentions.
Once stated that way it is the standard interval-scheduling problem, and the
answer is len(intervals) - max_kept.
Time . Space beyond the sort.
[[1,100],[2,3],[4,5]] returning 1 is the case that punishes sorting by
start: you must drop the long [1,100] and keep the two short ones. A
sort-by-start solution keeps [1,100] and reports 2 removals.
[[1,2],[2,3]] returning 0 confirms the >= — half-open intervals that
touch are compatible.
There is also an DP solution (longest non-overlapping subsequence by end time) which is worth naming to show you know greedy is not the only route — but it TLEs at , and the exchange argument is what justifies the faster answer.
Follow-ups you should expect: “Prove the greedy is optimal” — the exchange argument above; this is the most likely follow-up. “What if intervals have weights and you want maximum total weight?” — greedy fails; it becomes weighted interval scheduling, solved by DP with binary search in . That is a genuinely important boundary to know. “Return which intervals to remove?” — record the skipped ones during the scan.
LC 452 — Minimum Number of Arrows to Burst Balloons · Medium
Section titled “LC 452 — Minimum Number of Arrows to Burst Balloons · Medium”Problem. Balloons are given as [start, end] on the x-axis. An arrow
shot straight up at x bursts every balloon with start <= x <= end. Return
the minimum number of arrows needed to burst them all.
Constraints. 1 <= len(points) <= 10^5,
-2^31 <= start <= end <= 2^31 - 1.
Examples. [[10,16],[2,8],[1,6],[7,12]] gives 2 ·
[[1,2],[3,4],[5,6],[7,8]] gives 4 · [[1,2],[2,3],[3,4],[4,5]] gives 2
Editorial — approach, complexity, follow-ups
Sort by end. Shoot the first arrow at points[0][1] — the earliest end.
Any balloon overlapping that position is burst for free, so the arrow is
never wasted, and placing it any further right could miss the first balloon.
Then skip forward past everything already burst and repeat.
Time . Space .
[[1,2],[2,3],[3,4],[4,5]] giving 2 is the inclusive-endpoint test. Sorted
by end, the first arrow goes at 2, bursting [1,2] and [2,3]
(because 2 is inside [2,3]). The next unburst balloon is [3,4], so the
second arrow at 4 bursts [3,4] and [4,5]. Two arrows.
Using >= instead of > would demand a new arrow for [2,3] and return
4 — a wrong answer produced by code that otherwise looks identical to
LC 435. This is the clearest example on the site of why endpoint conventions
deserve an explicit question.
Follow-ups you should expect: “Why shoot at the end and not the middle?”
— the end is the rightmost position still guaranteed to hit the current
balloon, so it maximises how many later balloons are also caught. “Return the
arrow positions?” — collect last each time you increment. “What if arrows
had a width?” — becomes an interval-covering problem, closer to LC 1024.
LC 646 — Maximum Length of Pair Chain · Medium
Section titled “LC 646 — Maximum Length of Pair Chain · Medium”Problem. Given pairs where pairs[i] = [left, right] and
left < right, a pair [c, d] can follow [a, b] if b < c. Return the
length of the longest chain you can form, choosing and ordering pairs freely.
Constraints. 1 <= len(pairs) <= 1000,
-1000 <= left < right <= 1000.
Examples. [[1,2],[2,3],[3,4]] gives 2 ([1,2] -> [3,4]) ·
[[1,2],[7,8],[4,5]] gives 3
Editorial — approach, complexity, follow-ups
Exactly LC 435’s algorithm with a strict comparison. Because pairs may be reordered freely, “longest chain” is precisely “largest set of mutually non-overlapping pairs” — any such set can be laid out in end order to form a chain.
Time . Space .
Initialising count = 0 and last = float("-inf") lets the loop handle the
first pair with no special case, which is tidier than seeding from
pairs[0]. It also handles the negative coordinates in the third test case
correctly — seeding last = 0 would break there, and that case exists
specifically to catch it.
[[1,2],[2,3],[3,4]] giving 2 is the strictness check: [1,2] cannot be
followed by [2,3] because 2 < 2 is false. Contrast LC 435, where those
two are compatible. The same input, a different answer, one character of
difference in the code.
There is a well-known DP for this (sort by first element, then longest-increasing-subsequence style), and LeetCode’s own editorial presents it. Worth mentioning to show range, but greedy is and provably optimal.
Follow-ups you should expect: “Return the chain itself?” — collect pairs
as you accept them. “What if the rule were b <= c?” — switch to >=, and
it becomes LC 435. “What if each pair had a value and you wanted maximum
total?” — greedy fails; weighted interval scheduling via DP + binary search.
LeetCode problem set
Section titled “LeetCode problem set”Generated from the problem database, so each entry carries its sheet membership and reported companies. Tick them off as you go — progress is saved in this browser, and the Export button writes it to a file you can keep.
Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.
- 252Meeting Roomspremiumeasy
- 621Task Schedulermedium
- 435Non-overlapping IntervalsmediumMaximise kept, then subtract; `>=` for half-open
- 452Minimum Number of Arrows to Burst BalloonsmediumInclusive endpoints, so the comparison is strict `>`
- 646Maximum Length of Pair ChainmediumStrict chain rule; seed `last` at $-\infty$ for negatives
- 763Partition Labelsmedium
- 846Hand of Straightsmedium
- 1024Video Stitchingmedium**Covering**, not selecting -- sort by start and jump-extend
Interview follow-ups
Section titled “Interview follow-ups”| They ask | What they’re checking | The answer |
|---|---|---|
| “Why sort by end?” | Whether you can prove it | The exchange argument: any optimal solution can swap its earliest-finishing interval for the globally earliest-finishing one without breaking validity or changing size |
| “Why not by start, or by duration?” | Whether you tested it | [[1,100],[2,3],[4,5]] breaks sort-by-start; [[1,5],[4,6],[5,9]] breaks sort-by-duration |
“> or >=?” | Attention to conventions | Half-open intervals use >= (touching is fine); inclusive endpoints use > |
| “What if intervals have weights?” | Knowing where greedy dies | Greedy fails; weighted interval scheduling needs DP with binary search, |
| “Can you do it without sorting?” | Lower bounds | Not in general — by reduction from sorting, unless coordinates are bounded and you can bucket |
| “Which intervals did you keep?” | Bookkeeping | Collect them during the scan instead of only counting |
| “Covering instead of selecting?” | Distinguishing greedies | Sort by start and extend furthest — LC 1024, a different pattern |
Edge-case checklist
Section titled “Edge-case checklist”- Single interval — answer
1kept /0removed /1arrow. - All identical —
[[1,2],[1,2],[1,2]]; only one survives. - Touching endpoints —
[[1,2],[2,3]]; the>=vs>test, and it differs by problem. - One interval swallowing all others —
[[1,100],[2,3],[4,5]]; breaks sort-by-start. - Negative coordinates — seed
lastatfloat("-inf"), never0. - Already sorted input — no special handling, but a plausible test.
- Empty input — return
0; guard beforeintervals[0]. - Fully nested intervals — sorting by end naturally prefers the inner one, which is correct.
Self-check
Section titled “Self-check”-
Why sort by END time rather than by start time?
The exchange argument makes it precise: any optimal solution can be rewritten to start with the earliest-finishing interval without getting worse, so the greedy choice is always safe.
pch.quizShowAnswer
B — Because committing to the earliest-finishing interval leaves the most room for everything after it — sorting by start fails on [[1,100],[2,3],[4,5]], keeping 1 interval where the answer is 2 — The exchange argument makes it precise: any optimal solution can be rewritten to start with the earliest-finishing interval without getting worse, so the greedy choice is always safe.
-
Is sorting by shortest duration a valid alternative?
Shortest-first is the most plausible-sounding wrong answer, so it is worth carrying the counterexample. Only earliest-finish is safe.
pch.quizShowAnswer
B — No — on [[1,5],[4,6],[5,9]] the shortest is [4,6], which blocks both [1,5] and [5,9] for a total of 1, where the answer is 2 — Shortest-first is the most plausible-sounding wrong answer, so it is worth carrying the counterexample. Only earliest-finish is safe.
-
The compatibility test is `start >= last_end`. When would it be `start > last_end`?
One character, and it only shows up on the touching-intervals test case — the same half-open-versus-inclusive decision that drives the sweep-line tie-break.
pch.quizShowAnswer
B — When intervals are inclusive on both ends — if [1,2] and [2,3] both occupy point 2 they conflict, whereas half-open intervals do not — One character, and it only shows up on the touching-intervals test case — the same half-open-versus-inclusive decision that drives the sweep-line tie-break.
-
LC 435 asks for the minimum number of intervals to ERASE. How does that relate to this template?
In the dry run, 4 intervals with 3 kept means 1 erasure. Recognising the complement is often the whole insight a problem is testing.
pch.quizShowAnswer
B — It is the same computation: erasures = n − (maximum number of non-overlapping intervals kept) — In the dry run, 4 intervals with 3 kept means 1 erasure. Recognising the complement is often the whole insight a problem is testing.
-
Each interval now carries a value, and you want the maximum total value of a non-overlapping subset. Does the greedy still work?
This is the boundary of the pattern. A single high-value long interval can beat many cheap short ones, which no ordering-based greedy can see.
pch.quizShowAnswer
B — No — earliest-finish is no longer optimal once intervals are weighted; that is weighted interval scheduling, solved by DP with binary search in O(n log n) (LC 1235) — This is the boundary of the pattern. A single high-value long interval can beat many cheap short ones, which no ordering-based greedy can see.
-
What is the complexity, and what would make it faster?
The scan itself is one pass with O(1) state. Asking whether the input is pre-sorted is a cheap question that occasionally changes the answer you can offer.
pch.quizShowAnswer
B — O(n log n), dominated by the sort — and O(n) if the input arrives already sorted by end time, which some variants guarantee — The scan itself is one pass with O(1) state. Asking whether the input is pre-sorted is a cheap question that occasionally changes the answer you can offer.
Recall card
Section titled “Recall card”- Cue — “maximum number of non-overlapping intervals”, “minimum removals”, “can one resource serve all of these”; unweighted.
- Do — sort by end time, then scan keeping any interval whose
start >= last_end, updatinglast_endon each keep. - Why end time — earliest finish leaves the most room; the exchange argument turns that intuition into a proof.
- Wrong sort keys — by start fails
[[1,100],[2,3],[4,5]]; by duration fails[[1,5],[4,6],[5,9]]. Carry both counterexamples. >=vs>— half-open intervals may touch; inclusive ones may not.- Erasures =
n − kept(LC 435). - Cost — from the sort, scan, state. if pre-sorted.
- Boundary — weighted intervals break the greedy entirely: that is DP + binary search (LC 1235). Counting overlaps rather than selecting is the sweep line.
- Sort by end time and greedily keep every compatible interval. That is the whole algorithm: , space.
- Finishing earliest leaves the most room for what follows, and the exchange argument turns that intuition into a proof. Be ready to give it — it is the likeliest follow-up.
- “Remove the fewest” is “keep the most” in disguise: answer
len - kept. - 435, 452 and 646 are the same scan. The differences are
>=versus>(endpoint conventions) and what you return. - Greedy dies as soon as intervals carry weights — that is DP territory.
- Selecting a non-overlapping subset (this page) is not the same as covering a range (LC 1024). Sorting by end is right for the first and wrong for the second.
Next: Greedy Reachability and Jumps — the covering-style greedy, where you track how far you can get rather than which items you keep.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading