Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Intervals & Greedy

Builds on Merge Intervals, which you haven’t finished yet.

P1MediumBuilt on Merge Intervals~4 min speedrun

Interval Scheduling (Activity Selection, Greedy by End Time)

Built on
Merge IntervalsSort by end instead of start, and count or drop intervals instead of merging them.
Grows into
End-Time Groups
Unlocks
Greedy with Regret
The problem

Non-overlapping Intervals (LC 435)(opens leetcode.com)

You have an array of intervals intervals, each entry [start_i, end_i]. Find the fewest intervals you'd need to delete so that whatever remains has no two intervals overlapping, and return that count.

Touching at a single boundary point does not count as overlapping -- [1, 2] and [2, 3] are considered non-overlapping.

Example 1
Input:intervals = [[1,2],[2,3],[3,4],[1,3]]
Output:1
Dropping [1,3] leaves the remaining intervals non-overlapping.

Interval Scheduling

intervals ➔ sort by end time ➔ greedily pick earliest finishing
Spot it when: You keep as many non-overlapping intervals as possible, or remove the fewest to make the rest non-overlapping .
Interval Scheduling
Python
1def erase_overlap_intervals(intervals: list[list[int]]) -> int:
2 """Greedy Non-overlapping Intervals: remove minimum intervals.
3 Strategy: Sort by END time; greedily keep earliest finishing.
4 Time: O(N log N) | Space: O(N)
5 """
6 if not intervals:
7 return 0
8 
9 # 1. Sort by End Time and Open the First Kept Interval:
10 # Keeping whichever interval finishes earliest leaves the most room for the intervals that
11 # follow, so sorting by end time and keeping the first one is the optimal first choice.
12 intervals.sort(key=lambda iv: iv[1])
13 removed_count = 0
14 prev_end = intervals[0][1] # prev_end is the end of the last interval kept so far
15 
16 for start, end in intervals[1:]:
17 # 2. Drop an Overlap or Keep and Advance:
18 # start < prev_end means this interval starts before the last kept one ends, so keeping both
19 # would overlap; dropping this one is always at least as good, because it ends no earlier
20 # than the one already kept.
21 if start < prev_end:
22 removed_count += 1 # Overlap: drop incoming interval
23 else:
24 prev_end = end # Keep incoming interval, advance boundary
25 
26 return removed_count
Invariant: prev_end is the end of the last kept interval, and the kept intervals are the most that can fit among those seen so far.

Sort by end time, then keep an interval whenever it starts after the last kept one ends. Finishing earliest leaves the most room for the rest, so this keeps the most intervals, and everything else is a removal. In the blueprint a kept interval moves prev_end to its end, and any interval with start < prev_end is counted in removed_count. The trap is sorting by start (key=lambda iv: iv[0]) and keeping whichever interval comes first: [1, 100] is kept and blocks [2, 3], [3, 4], ... that would fit one after another. (Sorting by start also works if, on an overlap, you keep the one that ends first; sorting by end makes that choice for you.)

See it run

Interval Scheduling, step by step

prev_end 3 · removed_count 0

prev_end 3 · removed_count 0

prev_end 3 · removed_count 1

prev_end 3 · removed_count 1

prev_end 4 · removed_count 1

prev_end 4 · removed_count 1

prev_end 4 · removed_count 2

prev_end 4 · removed_count 2 · answer 2

Each column is one time point. Each row of numbers is one interval, sorted by end. A dash marks the time points the interval covers. The word keep or drop replaces the dashes after the decision.

  • start the start time of the interval being checked
  • end the end time of the interval being checked
  • prev_end the end time of the last kept interval

Sort by end time. The first interval [2, 3] is kept, so prev_end = 3.

[1, 3]: start 1 is less than prev_end 3, so it overlaps the last kept interval.

Drop [1, 3]. removed_count becomes 1.

[3, 4]: start 3 is not less than prev_end 3. The two intervals only touch, so both stay.

Keep [3, 4]. prev_end moves from 3 to 4.

[1, 9]: start 1 is less than prev_end 4, so it overlaps the last kept interval.

Drop [1, 9]. removed_count becomes 2.

Done: 2 intervals are removed, so the answer is 2.

[2, 3]
0
1
2keep
3keep
4
5
6
7
8
9
[1, 3]
-
-
-
[3, 4]
-
-
[1, 9]
-
-
-
-
-
-
-
-
-
prev_end
3prev_end
1 / 8
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Sorting by start instead of by end

    Mistake
    intervals.sort(key=lambda iv: iv[0])
    Fix
    intervals.sort(key=lambda iv: iv[1])

    What happens: Sorted by start, a long interval such as [1, 100] can come first. The loop keeps it and sets prev_end = 100. Every short interval that starts before 100 is then removed. Sorted by end, the loop keeps the interval that finishes first and leaves the most room.

    Example: intervals = [[1, 100], [2, 3], [3, 4]]
    Mistake: returns 2Fix: returns 1
  2. 2

    Gotcha 2: Reading intervals[0][1] before the sort

    Mistake
    prev_end = intervals[0][1]
    intervals.sort(key=lambda iv: iv[1])
    removed_count = 0
    Fix
    intervals.sort(key=lambda iv: iv[1])
    removed_count = 0
    prev_end = intervals[0][1]

    What happens: Before the sort, intervals[0] is the first interval of the input. Its end can be large. The loop then compares every later interval with that large prev_end and removes too many.

    Example: intervals = [[1, 100], [1, 2], [2, 3]]
    Mistake: returns 2Fix: returns 1
  3. 3

    Gotcha 3: Writing start <= prev_end for intervals that only touch

    Mistake
    if start <= prev_end:
    Fix
    if start < prev_end:

    What happens: Non-overlapping Intervals (LC 435) says that [1, 2] and [2, 3] do not overlap. They only touch at 2. With <=, the start 2 counts as an overlap, and the loop removes [2, 3].

    Example: intervals = [[1, 2], [2, 3]]
    Mistake: returns 1Fix: returns 0
  4. 4

    Gotcha 4: Moving prev_end for a removed interval too

    Mistake
    if start < prev_end:
        removed_count += 1
    prev_end = end
    Fix
    if start < prev_end:
        removed_count += 1
    else:
        prev_end = end

    What happens: prev_end is the end of the last interval that was kept. A removed interval is not kept, so its end must not count. After the sort, a removed interval ends at or after prev_end. Moving prev_end to its end blocks later intervals that would fit.

    Example: intervals = [[1, 2], [1, 3], [2, 4]]
    Mistake: returns 2Fix: returns 1
Not this one if…
  • Every interval must stay, and overlapping ones are joined into one, as in Merge Intervals (LC 56): nothing is removed, so there is no count to keep as small as possible. See Merge Intervals
  • You must cover a whole range with as few intervals as possible, as in Video Stitching (LC 1024), instead of keeping as many non-overlapping intervals as possible: keeping the interval that ends earliest fails there. See Greedy

Deep Dive

Why the interval that ends first is the safe choice

This card builds on the Merge Intervals card. That card sorts by start and merges. This card sorts by end and keeps or drops each interval.

Look at the interval that ends first. Any set of non-overlapping intervals that does not use it can swap its first interval for this one. The new interval ends no later than the old one, so it overlaps nothing that the old one did not. The set keeps the same size. So some best answer always contains the interval that ends first.

After that choice, only intervals that start at or after its end can still be kept. The same argument applies to the remaining intervals. The loop does this with prev_end: it keeps an interval when start >= prev_end, then sets prev_end = end.

The most intervals that can be kept is n - removed_count, so the fewest removals is removed_count.

Example: in Non-overlapping Intervals (LC 435), intervals = [[1, 2], [2, 3], [3, 4], [1, 3]]. Sorted by end, the order is [1, 2], [2, 3], [1, 3], [3, 4]. [1, 2] is kept, so prev_end = 2. [2, 3] starts at 2, so it is kept and prev_end = 3. [1, 3] starts at 1, which is less than 3, so it is removed. [3, 4] starts at 3, so it is kept. The answer is 1.

One trace: touching intervals are kept

The board replays intervals = [[1, 9], [2, 3], [3, 4], [1, 3]]. The rows are in sorted order by end, so the first row is [2, 3].

[2, 3] is kept, so prev_end = 3. [1, 3] starts at 1, which is less than 3, so it is removed. [3, 4] starts at 3, and 3 < 3 is false. The two intervals only touch, so [3, 4] is kept and prev_end = 4. This is the step marked as the trap on the board.

If the test were start <= prev_end, [3, 4] would be removed. Then [1, 9] would also be removed, and the loop would return 3 instead of 2. In Non-overlapping Intervals (LC 435), intervals that share only an end point do not overlap.

[1, 9] ends last. It starts at 1, which is less than 4, so it is removed. The answer is 2. Only [2, 3] and [3, 4] remain.

The trap: sorting by start

Keep the same intervals and change only the sort key to iv[0]. The order becomes [1, 9], [1, 3], [2, 3], [3, 4]. The loop keeps [1, 9] first and sets prev_end = 9. Every other interval starts before 9, so all three are removed. The loop returns 3 instead of 2.

The reason is that the first interval by start does not leave room for the others. A long interval blocks many short ones. The interval that ends first blocks the fewest.

Sorting by start can work, but it needs an extra rule. On an overlap, keep the interval with the smaller end and set prev_end = min(prev_end, end). Sorting by end makes this choice automatically.

Also read intervals[0][1] only after the sort. Before the sort, intervals[0] is the first interval of the input, and its end can be large.

The cost, and which problems fit

The sort takes O(N log N) time and the loop takes O(N) time. The total is O(N log N). The loop keeps only prev_end and removed_count, so the extra space is O(1) besides the sort.

The same keep-the-earliest-end idea solves other counting questions. Minimum Number of Arrows to Burst Balloons (LC 452) sorts by end and shoots at the end of the first balloon. Every balloon that starts at or before that point is burst with it.

Example: for LC 435 with [[1, 2], [1, 2], [1, 2]], the sorted list is the same. The first is kept, and the other two start at 1, which is less than 2. The answer is 2.

If the question asks for the largest total weight and not the largest count, this greedy choice is no longer safe. Use dynamic programming over the sorted intervals.

60-Second Pitch

"I want to keep as many non-overlapping intervals as I can, so I sort by end time. The interval that ends first leaves the most room, and any best answer can swap its first interval for it. I keep the first interval and store its end as prev_end. For each next interval, if start < prev_end it overlaps, so I remove it and add 1 to removed_count. Otherwise I keep it and set prev_end = end. The sort costs O(N log N) and the loop costs O(N), with O(1) extra space. I check that intervals that only touch are kept, so the test is < and not <=."

Common Questions

Why sort by end time and not by start time?

The interval that ends first blocks the least time for the others. Any best answer can swap its first interval for the one that ends first, without losing a kept interval. A long interval that starts first can block many short ones, as [1, 9] does in [[1, 9], [2, 3], [3, 4], [1, 3]].

Why is the test start < prev_end and not <=?

In Non-overlapping Intervals (LC 435), intervals that share only an end point do not overlap. [2, 3] and [3, 4] can both be kept. With <=, the loop removes [3, 4] and returns too large a count.

What are the time and space complexity?

O(N log N) time, because of the sort. The loop is O(N). The extra space is O(1) besides the space the sort uses. The blueprint changes the caller's list by sorting it.

How do you return the kept intervals and not only the count?

Append each kept interval to a list kept in the loop, including the first one. The loop already decides keep or drop for each interval. The length of kept is n - removed_count.

What changes when each interval has a different value?

The greedy choice is no longer safe, because a short interval can have a small value and block a long one with a large value. Sort by end, then use dynamic programming: for each interval, take the best of skipping it or adding its value to the best total that ends at or before its start.

SWE Reasoning

Spot it

  • The question asks for the most intervals that do not overlap, or the fewest intervals to remove so that the rest do not overlap.
  • Every interval has the same value, so only the count matters.
  • Choosing one interval blocks the time it covers, and the other intervals compete for the time that is left.
  • The question is a scheduling one: rooms, talks, jobs, or balloons shot by one arrow.
  • The answer is a count, and the chosen set itself is not needed.

The invariant, step by step

  1. The list is empty, so return 0. Otherwise sort intervals by iv[1], which is the end time.
  2. Keep the first interval. Set prev_end = intervals[0][1] and removed_count = 0.
  3. For each start, end in the rest of the list, test start < prev_end.
  4. If it is true, the interval starts before the last kept interval ends. Drop it: removed_count += 1.
  5. Otherwise keep it: prev_end = end. It ends no earlier than the previous prev_end, so prev_end never moves left.
  6. prev_end is always the end of the last kept interval, and the kept set is the largest possible among the intervals seen so far.

Traps

  • Sorting by start. A long interval such as [1, 9] comes first and blocks the short ones. For [[1, 9], [2, 3], [3, 4], [1, 3]] the loop returns 3 instead of 2.
  • Using <= for the overlap test. Intervals that only touch do not overlap in Non-overlapping Intervals (LC 435). [3, 4] after prev_end = 3 must be kept.
  • Reading prev_end before the sort. intervals[0][1] must come from the sorted list. Before the sort it is the end of an arbitrary interval.
  • Counting the kept intervals and returning that. The question asks for the removals. Return removed_count, or n - kept.
  • Moving prev_end on a drop. A dropped interval must not change prev_end. Only a kept interval sets it.

Practice

Canonical LeetCode:Non-overlapping Intervals (LC 435)Maximum Length of Pair Chain (LC 646)Maximum Number of Events That Can Be Attended (LC 1353)Minimum Number of Arrows to Burst Balloons (LC 452)
Theory Context•Intervals & Greedy

Builds on Merge Intervals, which you haven’t finished yet.

P1MediumBuilt on Merge Intervals~4 min speedrun

Interval Scheduling (Activity Selection, Greedy by End Time)

Built on
Merge IntervalsSort by end instead of start, and count or drop intervals instead of merging them.
Grows into
End-Time Groups
Unlocks
Greedy with Regret
The problem

Non-overlapping Intervals (LC 435)(opens leetcode.com)

You have an array of intervals intervals, each entry [start_i, end_i]. Find the fewest intervals you'd need to delete so that whatever remains has no two intervals overlapping, and return that count.

Touching at a single boundary point does not count as overlapping -- [1, 2] and [2, 3] are considered non-overlapping.

Example 1
Input:intervals = [[1,2],[2,3],[3,4],[1,3]]
Output:1
Dropping [1,3] leaves the remaining intervals non-overlapping.

Interval Scheduling

intervals ➔ sort by end time ➔ greedily pick earliest finishing
Spot it when: You keep as many non-overlapping intervals as possible, or remove the fewest to make the rest non-overlapping .
Interval Scheduling
Python
1def erase_overlap_intervals(intervals: list[list[int]]) -> int:
2 """Greedy Non-overlapping Intervals: remove minimum intervals.
3 Strategy: Sort by END time; greedily keep earliest finishing.
4 Time: O(N log N) | Space: O(N)
5 """
6 if not intervals:
7 return 0
8 
9 # 1. Sort by End Time and Open the First Kept Interval:
10 # Keeping whichever interval finishes earliest leaves the most room for the intervals that
11 # follow, so sorting by end time and keeping the first one is the optimal first choice.
12 intervals.sort(key=lambda iv: iv[1])
13 removed_count = 0
14 prev_end = intervals[0][1] # prev_end is the end of the last interval kept so far
15 
16 for start, end in intervals[1:]:
17 # 2. Drop an Overlap or Keep and Advance:
18 # start < prev_end means this interval starts before the last kept one ends, so keeping both
19 # would overlap; dropping this one is always at least as good, because it ends no earlier
20 # than the one already kept.
21 if start < prev_end:
22 removed_count += 1 # Overlap: drop incoming interval
23 else:
24 prev_end = end # Keep incoming interval, advance boundary
25 
26 return removed_count
Invariant: prev_end is the end of the last kept interval, and the kept intervals are the most that can fit among those seen so far.

Sort by end time, then keep an interval whenever it starts after the last kept one ends. Finishing earliest leaves the most room for the rest, so this keeps the most intervals, and everything else is a removal. In the blueprint a kept interval moves prev_end to its end, and any interval with start < prev_end is counted in removed_count. The trap is sorting by start (key=lambda iv: iv[0]) and keeping whichever interval comes first: [1, 100] is kept and blocks [2, 3], [3, 4], ... that would fit one after another. (Sorting by start also works if, on an overlap, you keep the one that ends first; sorting by end makes that choice for you.)

See it run

Interval Scheduling, step by step

prev_end 3 · removed_count 0

prev_end 3 · removed_count 0

prev_end 3 · removed_count 1

prev_end 3 · removed_count 1

prev_end 4 · removed_count 1

prev_end 4 · removed_count 1

prev_end 4 · removed_count 2

prev_end 4 · removed_count 2 · answer 2

Each column is one time point. Each row of numbers is one interval, sorted by end. A dash marks the time points the interval covers. The word keep or drop replaces the dashes after the decision.

  • start the start time of the interval being checked
  • end the end time of the interval being checked
  • prev_end the end time of the last kept interval

Sort by end time. The first interval [2, 3] is kept, so prev_end = 3.

[1, 3]: start 1 is less than prev_end 3, so it overlaps the last kept interval.

Drop [1, 3]. removed_count becomes 1.

[3, 4]: start 3 is not less than prev_end 3. The two intervals only touch, so both stay.

Keep [3, 4]. prev_end moves from 3 to 4.

[1, 9]: start 1 is less than prev_end 4, so it overlaps the last kept interval.

Drop [1, 9]. removed_count becomes 2.

Done: 2 intervals are removed, so the answer is 2.

[2, 3]
0
1
2keep
3keep
4
5
6
7
8
9
[1, 3]
-
-
-
[3, 4]
-
-
[1, 9]
-
-
-
-
-
-
-
-
-
prev_end
3prev_end
1 / 8
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Sorting by start instead of by end

    Mistake
    intervals.sort(key=lambda iv: iv[0])
    Fix
    intervals.sort(key=lambda iv: iv[1])

    What happens: Sorted by start, a long interval such as [1, 100] can come first. The loop keeps it and sets prev_end = 100. Every short interval that starts before 100 is then removed. Sorted by end, the loop keeps the interval that finishes first and leaves the most room.

    Example: intervals = [[1, 100], [2, 3], [3, 4]]
    Mistake: returns 2Fix: returns 1
  2. 2

    Gotcha 2: Reading intervals[0][1] before the sort

    Mistake
    prev_end = intervals[0][1]
    intervals.sort(key=lambda iv: iv[1])
    removed_count = 0
    Fix
    intervals.sort(key=lambda iv: iv[1])
    removed_count = 0
    prev_end = intervals[0][1]

    What happens: Before the sort, intervals[0] is the first interval of the input. Its end can be large. The loop then compares every later interval with that large prev_end and removes too many.

    Example: intervals = [[1, 100], [1, 2], [2, 3]]
    Mistake: returns 2Fix: returns 1
  3. 3

    Gotcha 3: Writing start <= prev_end for intervals that only touch

    Mistake
    if start <= prev_end:
    Fix
    if start < prev_end:

    What happens: Non-overlapping Intervals (LC 435) says that [1, 2] and [2, 3] do not overlap. They only touch at 2. With <=, the start 2 counts as an overlap, and the loop removes [2, 3].

    Example: intervals = [[1, 2], [2, 3]]
    Mistake: returns 1Fix: returns 0
  4. 4

    Gotcha 4: Moving prev_end for a removed interval too

    Mistake
    if start < prev_end:
        removed_count += 1
    prev_end = end
    Fix
    if start < prev_end:
        removed_count += 1
    else:
        prev_end = end

    What happens: prev_end is the end of the last interval that was kept. A removed interval is not kept, so its end must not count. After the sort, a removed interval ends at or after prev_end. Moving prev_end to its end blocks later intervals that would fit.

    Example: intervals = [[1, 2], [1, 3], [2, 4]]
    Mistake: returns 2Fix: returns 1
Not this one if…
  • Every interval must stay, and overlapping ones are joined into one, as in Merge Intervals (LC 56): nothing is removed, so there is no count to keep as small as possible. See Merge Intervals
  • You must cover a whole range with as few intervals as possible, as in Video Stitching (LC 1024), instead of keeping as many non-overlapping intervals as possible: keeping the interval that ends earliest fails there. See Greedy

Deep Dive

Why the interval that ends first is the safe choice

This card builds on the Merge Intervals card. That card sorts by start and merges. This card sorts by end and keeps or drops each interval.

Look at the interval that ends first. Any set of non-overlapping intervals that does not use it can swap its first interval for this one. The new interval ends no later than the old one, so it overlaps nothing that the old one did not. The set keeps the same size. So some best answer always contains the interval that ends first.

After that choice, only intervals that start at or after its end can still be kept. The same argument applies to the remaining intervals. The loop does this with prev_end: it keeps an interval when start >= prev_end, then sets prev_end = end.

The most intervals that can be kept is n - removed_count, so the fewest removals is removed_count.

Example: in Non-overlapping Intervals (LC 435), intervals = [[1, 2], [2, 3], [3, 4], [1, 3]]. Sorted by end, the order is [1, 2], [2, 3], [1, 3], [3, 4]. [1, 2] is kept, so prev_end = 2. [2, 3] starts at 2, so it is kept and prev_end = 3. [1, 3] starts at 1, which is less than 3, so it is removed. [3, 4] starts at 3, so it is kept. The answer is 1.

One trace: touching intervals are kept

The board replays intervals = [[1, 9], [2, 3], [3, 4], [1, 3]]. The rows are in sorted order by end, so the first row is [2, 3].

[2, 3] is kept, so prev_end = 3. [1, 3] starts at 1, which is less than 3, so it is removed. [3, 4] starts at 3, and 3 < 3 is false. The two intervals only touch, so [3, 4] is kept and prev_end = 4. This is the step marked as the trap on the board.

If the test were start <= prev_end, [3, 4] would be removed. Then [1, 9] would also be removed, and the loop would return 3 instead of 2. In Non-overlapping Intervals (LC 435), intervals that share only an end point do not overlap.

[1, 9] ends last. It starts at 1, which is less than 4, so it is removed. The answer is 2. Only [2, 3] and [3, 4] remain.

The trap: sorting by start

Keep the same intervals and change only the sort key to iv[0]. The order becomes [1, 9], [1, 3], [2, 3], [3, 4]. The loop keeps [1, 9] first and sets prev_end = 9. Every other interval starts before 9, so all three are removed. The loop returns 3 instead of 2.

The reason is that the first interval by start does not leave room for the others. A long interval blocks many short ones. The interval that ends first blocks the fewest.

Sorting by start can work, but it needs an extra rule. On an overlap, keep the interval with the smaller end and set prev_end = min(prev_end, end). Sorting by end makes this choice automatically.

Also read intervals[0][1] only after the sort. Before the sort, intervals[0] is the first interval of the input, and its end can be large.

The cost, and which problems fit

The sort takes O(N log N) time and the loop takes O(N) time. The total is O(N log N). The loop keeps only prev_end and removed_count, so the extra space is O(1) besides the sort.

The same keep-the-earliest-end idea solves other counting questions. Minimum Number of Arrows to Burst Balloons (LC 452) sorts by end and shoots at the end of the first balloon. Every balloon that starts at or before that point is burst with it.

Example: for LC 435 with [[1, 2], [1, 2], [1, 2]], the sorted list is the same. The first is kept, and the other two start at 1, which is less than 2. The answer is 2.

If the question asks for the largest total weight and not the largest count, this greedy choice is no longer safe. Use dynamic programming over the sorted intervals.

60-Second Pitch

"I want to keep as many non-overlapping intervals as I can, so I sort by end time. The interval that ends first leaves the most room, and any best answer can swap its first interval for it. I keep the first interval and store its end as prev_end. For each next interval, if start < prev_end it overlaps, so I remove it and add 1 to removed_count. Otherwise I keep it and set prev_end = end. The sort costs O(N log N) and the loop costs O(N), with O(1) extra space. I check that intervals that only touch are kept, so the test is < and not <=."

Common Questions

Why sort by end time and not by start time?

The interval that ends first blocks the least time for the others. Any best answer can swap its first interval for the one that ends first, without losing a kept interval. A long interval that starts first can block many short ones, as [1, 9] does in [[1, 9], [2, 3], [3, 4], [1, 3]].

Why is the test start < prev_end and not <=?

In Non-overlapping Intervals (LC 435), intervals that share only an end point do not overlap. [2, 3] and [3, 4] can both be kept. With <=, the loop removes [3, 4] and returns too large a count.

What are the time and space complexity?

O(N log N) time, because of the sort. The loop is O(N). The extra space is O(1) besides the space the sort uses. The blueprint changes the caller's list by sorting it.

How do you return the kept intervals and not only the count?

Append each kept interval to a list kept in the loop, including the first one. The loop already decides keep or drop for each interval. The length of kept is n - removed_count.

What changes when each interval has a different value?

The greedy choice is no longer safe, because a short interval can have a small value and block a long one with a large value. Sort by end, then use dynamic programming: for each interval, take the best of skipping it or adding its value to the best total that ends at or before its start.

SWE Reasoning

Spot it

  • The question asks for the most intervals that do not overlap, or the fewest intervals to remove so that the rest do not overlap.
  • Every interval has the same value, so only the count matters.
  • Choosing one interval blocks the time it covers, and the other intervals compete for the time that is left.
  • The question is a scheduling one: rooms, talks, jobs, or balloons shot by one arrow.
  • The answer is a count, and the chosen set itself is not needed.

The invariant, step by step

  1. The list is empty, so return 0. Otherwise sort intervals by iv[1], which is the end time.
  2. Keep the first interval. Set prev_end = intervals[0][1] and removed_count = 0.
  3. For each start, end in the rest of the list, test start < prev_end.
  4. If it is true, the interval starts before the last kept interval ends. Drop it: removed_count += 1.
  5. Otherwise keep it: prev_end = end. It ends no earlier than the previous prev_end, so prev_end never moves left.
  6. prev_end is always the end of the last kept interval, and the kept set is the largest possible among the intervals seen so far.

Traps

  • Sorting by start. A long interval such as [1, 9] comes first and blocks the short ones. For [[1, 9], [2, 3], [3, 4], [1, 3]] the loop returns 3 instead of 2.
  • Using <= for the overlap test. Intervals that only touch do not overlap in Non-overlapping Intervals (LC 435). [3, 4] after prev_end = 3 must be kept.
  • Reading prev_end before the sort. intervals[0][1] must come from the sorted list. Before the sort it is the end of an arbitrary interval.
  • Counting the kept intervals and returning that. The question asks for the removals. Return removed_count, or n - kept.
  • Moving prev_end on a drop. A dropped interval must not change prev_end. Only a kept interval sets it.

Practice

Canonical LeetCode:Non-overlapping Intervals (LC 435)Maximum Length of Pair Chain (LC 646)Maximum Number of Events That Can Be Attended (LC 1353)Minimum Number of Arrows to Burst Balloons (LC 452)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•