Skip to content

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.

  • 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.

Sorted by end time, not start. That choice is the whole insight:

intervalFinishing earliest leaves the most room for whatever comes nextLC 435 · O(n log n)
2–31–43–65–78–9123456789
kept0last end−∞
sortedSorted by **end** time, and that choice is the entire insight. Sorting by start time is the intuitive move and it is wrong: a long interval starting early can block several short ones. Finishing earliest leaves the most room for whatever comes next.
1/12

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.

max_non_overlapping.py
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]]))   # 3

O(nlogn)O(n \log n) for the sort, O(n)O(n) for the scan, O(1)O(1) 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.

  1. If OPT already contains g, there is nothing to show.
  2. Otherwise, let f be the interval in OPT that finishes earliest. By definition of g, we know end(g) <= end(f).
  3. Swap f for g in OPT. Is the result still valid? Every other interval in OPT starts at or after end(f), and end(g) <= end(f), so they all start at or after end(g) too. No new overlap is created.
  4. 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.

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.

intervalstart >= last_end?decisionlast_endkept
[1,2]— (seed)keep21
[2,3]2 ≥ 2 ✓keep32
[1,3]1 ≥ 3 ✗skip — overlaps what we already committed to32
[3,4]3 ≥ 3 ✓keep43

Answer 3, and therefore LC 435’s answer — removals — is 43 = 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 becomes start > 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.
StepCost
Sort by end timeO(nlogn)O(n \log n) — dominates
Single scanO(n)O(n)
Extra spaceO(1)O(1) beyond the sort (Python’s sort is O(n)O(n) auxiliary)
TotalO(nlogn)O(n \log n) time

The O(nlogn)O(n \log n) 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 Ω(nlogn)\Omega(n \log n) in the comparison model. If the input is already sorted by end time, the whole thing is O(n)O(n) — 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:

ApproachTimeCorrect?
Sort by end, greedy scanO(nlogn)O(n \log n)
Sort by start, greedy scanO(nlogn)O(n \log n)❌ — fails [[1,100],[2,3],[4,5]]
Sort by duration, greedy scanO(nlogn)O(n \log n)❌ — fails [[1,5],[4,6],[5,9]]
DP over intervals (weighted case)O(nlogn)O(n \log n) 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).

All four of these are the same scan. Only the comparison and the returned quantity change.

ProblemSort byKeep whenReturn
435 Erase overlapsendstart >= last_endlen - kept
452 Burst balloonsendstart > last_endkept (arrows)
646 Longest chainendstart > last_endkept
1024 Video stitchingstart(different — interval covering)jumps

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 O(nlogn)O(n \log n). Space O(1)O(1) 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 O(n2)O(n^2) 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 n=105n = 10^5, 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 O(nlogn)O(n \log n). 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 O(nlogn)O(n \log n). Space O(1)O(1).

[[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 O(nlogn)O(n \log n). Space O(1)O(1).

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 O(n2)O(n^2) 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 O(nlogn)O(n \log n) 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.

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.

8 problems
1 easy7 medium0 hard

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.

They askWhat they’re checkingThe answer
“Why sort by end?”Whether you can prove itThe 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 conventionsHalf-open intervals use >= (touching is fine); inclusive endpoints use >
“What if intervals have weights?”Knowing where greedy diesGreedy fails; weighted interval scheduling needs DP with binary search, O(nlogn)O(n \log n)
“Can you do it without sorting?”Lower boundsNot in general — Ω(nlogn)\Omega(n \log n) by reduction from sorting, unless coordinates are bounded and you can bucket
“Which intervals did you keep?”BookkeepingCollect them during the scan instead of only counting
“Covering instead of selecting?”Distinguishing greediesSort by start and extend furthest — LC 1024, a different pattern
  • Single interval — answer 1 kept / 0 removed / 1 arrow.
  • 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 last at float("-inf"), never 0.
  • Already sorted input — no special handling, but a plausible test.
  • Empty input — return 0; guard before intervals[0].
  • Fully nested intervals — sorting by end naturally prefers the inner one, which is correct.
pch.quizTag Greedy interval scheduling — self-check
  1. Why sort by END time rather than by start time?

    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.

  2. Is sorting by shortest duration a valid alternative?

    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.

  3. The compatibility test is `start >= last_end`. When would it be `start > last_end`?

    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.

  4. LC 435 asks for the minimum number of intervals to ERASE. How does that relate to this template?

    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.

  5. Each interval now carries a value, and you want the maximum total value of a non-overlapping subset. Does the greedy still work?

    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.

  6. What is the complexity, and what would make it faster?

    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.

  • 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, updating last_end on 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).
  • CostO(nlogn)O(n \log n) from the sort, O(n)O(n) scan, O(1)O(1) state. O(n)O(n) 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: O(nlogn)O(n \log n), O(1)O(1) 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.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading