Conceptual Foundation & Intuitive Analogy
Intervals and greedy choices share one idea: sort the input first, then make one pass that keeps a single running value. Interval sweeps merge, insert and schedule 1D spans. Greedy passes pick the locally best option and never go back.
🧭 Conceptual Foundation & Pattern Intuition
Many algorithmic problems ask questions about overlapping spans:
- "Which calendar meetings conflict?" (Interval Merging)
- "How many conference rooms are needed at the same time?" (Sweep-Line Concurrency)
- "What is the most intervals I can keep without any overlap?" (Earliest Finish Time)
- "Can I reach the last index?" (Greedy Furthest Reach) In their raw form, these problems appear to need pairwise comparisons. Sorting removes that cost:
- Sorting by start time places every interval that can overlap the open interval directly after it. One left-to-right pass is enough, and the pass keeps one running interval.
- Sorting by end time lets a greedy pass keep the interval that ends first, which leaves the most room for the rest.
- A greedy pass without a sort keeps one number, such as the furthest index reached, in place of every possible path.
🛫 The Analogy: The Airport Runway Schedule
Think of a runway schedule. If the flight take-off windows are sorted by start time, air traffic control only has to check whether the next flight starts before the current block ends. If it does, the two windows join into one block. If it does not, the current block is finished and a new block starts.
🔑 Why It Works
After sorting, each element is compared with one running value, so each element is inspected a constant number of times. The sort costs and the pass costs .
After sorting by start, anything that overlaps the interval I am building is the very next one I will see. I never need to look backwards.
Sort first. Then one running value (the open interval, the last kept end, the furthest reach) is enough to decide each next item.
Merge: if curr.start <= prev.end: extend prev.end. Schedule: if curr.start >= prev_end: keep curr.
The 2 Mental Models of Intervals & Greedy
Interval merging, scheduling, sweep lines and one-way greedy choices: name the model before coding.
1Merge Intervals
intervals ➔ sort by start time ➔ merge or extend prev.endMerge Intervals
| 1 | def merge_intervals(intervals: list[list[int]]) -> list[list[int]]: |
| 2 | """Interval Merging via chronological start-time sorting. |
| 3 | Time: O(N log N) | Space: O(N) |
| 4 | """ |
| 5 | if not intervals: |
| 6 | return [] |
| 7 | |
| 8 | # 1. Sort by Start Time: |
| 9 | # Sorting puts every interval that can overlap the current one directly after it, so one |
| 10 | # left-to-right pass is enough: no interval later in the list can start earlier than this one. |
| 11 | intervals.sort(key=lambda iv: iv[0]) |
| 12 | merged: list[list[int]] = [] # merged holds finished, disjoint intervals, in order |
| 13 | accumulated = intervals[0] # accumulated is the one interval still open for absorbing overlaps |
| 14 | |
| 15 | for incoming in intervals[1:]: |
| 16 | # 2. Absorb or Close the Accumulator: |
| 17 | # incoming overlaps accumulated exactly when incoming starts at or before accumulated's end; |
| 18 | # touching at one shared point still counts as overlap. |
| 19 | if incoming[0] <= accumulated[1]: |
| 20 | accumulated[1] = max(accumulated[1], incoming[1]) # Keep the later end: a nested interval must not shrink it |
| 21 | else: |
| 22 | merged.append(accumulated) # No later interval starts before accumulated ends, so it is finished |
| 23 | accumulated = incoming # Start a new accumulator from incoming |
| 24 | |
| 25 | # 3. Flush the Last Accumulator: |
| 26 | # The loop only saves accumulated when a later interval closes it, so the final open interval is saved here. |
| 27 | merged.append(accumulated) |
| 28 | return merged |
Sort by start, then keep one accumulated interval open: if incoming[0] <= accumulated[1] the two overlap, so stretch accumulated[1]; otherwise save accumulated into merged and start again from incoming. After sorting, every later interval starts at or after incoming, so once one starts past the current end, nothing later can reach back into accumulated: it is finished. The trap is the end update: use max(accumulated[1], incoming[1]), because an interval nested inside another would otherwise shrink the merged end.
Gotcha 1 of 4
- The trap
Gotcha 1: Setting the end to
incoming[1]withoutmaxMistakeaccumulated[1] = incoming[1]Fixaccumulated[1] = max(accumulated[1], incoming[1])What happens:
incomingcan lie completely insideaccumulated. Thenincoming[1]is smaller thanaccumulated[1]. The plain assignment moves the merged end back toincoming[1]. A later interval that overlaps the old end is then saved as a separate interval.Example: intervals = [[1, 5], [2, 3], [4, 6]]Mistake: returns [[1, 3], [4, 6]]Fix: returns [[1, 6]]
2Greedy
choices in order ➔ locally best option (exchange argument) ➔ never undoneGreedy (Exchange Argument, Furthest Reach)
| 1 | def can_jump(nums: list[int]) -> bool: |
| 2 | """Jump Game: greedy furthest reach. |
| 3 | Time: O(N) | Space: O(1) |
| 4 | """ |
| 5 | reach = 0 # Furthest index some run of jumps can land on so far |
| 6 | |
| 7 | for i in range(len(nums)): |
| 8 | # 1. Stop When Index i Is Unreachable, Otherwise Extend reach: |
| 9 | # An index past reach has no jump landing on it, so it must never extend reach with its own jump; |
| 10 | # check that first. Otherwise take the larger of reach and i + nums[i], since a longer reach always |
| 11 | # contains every index a shorter one does. |
| 12 | if i > reach: |
| 13 | return False |
| 14 | reach = max(reach, i + nums[i]) |
| 15 | |
| 16 | return True # The loop stood on every index, the last one included |
You never need the path, only how far any path can get: keep reach, the furthest index a run of jumps can land on so far, and at every index you can stand on extend it with reach = max(reach, i + nums[i]). A jump may be shorter than nums[i], so every index up to reach can be landed on and the reachable indices always form one block 0..reach; taking the larger reach can never cost an option, because a longer block contains every index of a shorter one. That is the greedy argument in general: the locally best choice keeps everything any other choice would keep, so it never has to be undone. The trap is the order of the two lines: check if i > reach: return False before reading nums[i], because an index past reach can't be stood on, and letting its jump extend reach makes [3,2,1,0,4] return True.
Gotcha 1 of 3
- The trap
Gotcha 1: Updating
reachbefore checkingi > reachMistakereach = max(reach, i + nums[i]) if i > reach: return FalseFixif i > reach: return False reach = max(reach, i + nums[i])What happens: An index past
reachcannot be stood on. So its jump must never count. Updating first lets that index extendreachwith its own jump. Theni > reachis False, and the loop goes on as if indexicould be reached.Example: nums = [3, 2, 1, 0, 4]Mistake: returns TrueFix: returns False
Built on the mental models above
- Interval SchedulingInterval Scheduling (Activity Selection, Greedy by End Time)
Sort by end instead of start, and count or drop intervals instead of merging them.
- End-Time GroupsInterval Grouping (Minimum Arrows to Burst Balloons)
Count the groups of intervals that share a point instead of the intervals to remove: shoot at the end of the first interval not yet hit.
- Sweep LineSweep Line (Line Sweep)
Break each interval into a start event and an end event, sort the events, and track how many are open.
The Intervals & Greedy Decision Rule
When facing an unseen interval or greedy problem, ask in this order:
Compare curr.start <= prev.end and extend prev.end = max(prev.end, curr.end).
Sort by end time and greedily retain the earliest ending interval that starts at or after the last kept end.
Create +1 (start) and -1 (end) events, sorting end events first at equal timestamps.
Prove with an exchange argument that the greedy pick keeps every option another pick would, then take it at every step.
Sort by end and shoot at the end of the first interval not yet hit; an interval that starts at or before that spot is already hit.
Pattern Architecture & Invariant Design Masterclass
One idea runs through every card here: sort first, then keep one running value, and an O(n²) comparison problem becomes O(n).
Interval sweeps. An interval is a start and an end — a meeting from 9 to 10, a booking from the 3rd to the 6th. Asked "which of these overlap?", the instinct is to compare every pair: O(n²). But sort by start time and the problem changes shape: an interval that overlaps anything already merged must overlap the block being built, merged[-1], so a single left-to-right pass comparing each interval only to the one it is currently accumulating is enough. The sort costs O(n log n), the sweep O(n), and the sort dominates.
Greedy choices. A greedy problem offers a choice at each step. The greedy rule picks the locally best option and never goes back. It is correct only when you can argue that no other option could have done better. Jump Game keeps one number, reach, in place of every possible path.
The invariants to say out loud:
- Greedy: "Everything before index
iis decided, and one number summarizes what it allows."
How this plays out in actual problems:
🟢 Scenario A: Interval Merging & Absorption (Sort by Start Time)
- In interval merging (like Merge Intervals LC 56), we consolidate overlapping blocks into unified continuous spans.
- The Rule: Sort intervals by start time. Iterate left-to-right. If
incoming[0] <= merged[-1][1], expand the current interval usingmerged[-1][1] = max(merged[-1][1], incoming[1]). Otherwise, appendincomingas a new disjoint block. - The Code:
if incoming[0] <= merged[-1][1]: merged[-1][1] = max(merged[-1][1], incoming[1])
🔵 Scenario B: Earliest Finish Time Greedy Scheduling (Sort by End Time)
- In non-overlapping interval scheduling (like LC 435 / LC 452), we want to maximize the count of mutually compatible intervals.
- The Rule: Sort intervals by end time. Greedily select the interval that finishes earliest. Any subsequent interval whose
start < current_endmust be removed/skipped. - The Code:
if start >= prev_end: prev_end = end; count += 1
💡 The Ultimate Takeaway Decision Rule:
When analyzing an interval or sequence query problem, ask: "Are we merging overlapping spans, scheduling non-overlapping intervals, counting overlaps, or making a one-way choice?"
- Consolidating Overlapping Ranges ➔ Sort by start time
x[0], sequential absorption ().- Maximizing Non-Overlapping / Min Removals ➔ Sort by end time
x[1], greedy earliest finish selection ().- Concurrent Resource Peak (Meeting Rooms II) ➔ Chronological sweep-line boundary events
(t, +1)&(t, -1)().- One-way choice with a proof ➔ Greedy with a single running value, such as
reach().
📦 1. Interval Merge & Absorption (LC 56 & 57)
- Mental Model: Sort intervals by start time. After sorting, an interval that overlaps anything already merged must overlap the block being built,
merged[-1], so earlier blocks are never checked again. - Code: see the Merge Intervals card.
⏱️ 2. Earliest Finish Time Greedy Scheduling (LC 435 & 452)
- Mental Model: To leave maximal room for subsequent intervals, always pick the interval that finishes earliest. Sorting by end time guarantees optimal greedy choice.
- LC 435 vs. LC 452: In Non-overlapping Intervals (LC 435), intervals that only touch do not overlap, so the blueprint keeps an interval when
start >= prev_end. In Minimum Number of Arrows to Burst Balloons (LC 452), balloons that touch are burst by the same arrow, so a new arrow is needed only whenstart > prev_end. - Code: see the Interval Scheduling card.
📈 3. Sweep-Line Concurrency & Meeting Rooms II (LC 253)
- Mental Model: Decompose intervals into discrete point events:
+1room at start time,-1room at end time. Sort events chronologically. - Code: see the Sweep Line card.
Understanding Invariants: Interval Sweeps & Greedy Running Values
After sorting by start, anything that overlaps the interval I am building is the very next one I will see.
Interval sweeps sort chronologically to turn quadratic pairwise checks into single O(N) passes, and greedy passes keep one running value that summarizes everything already decided.
Because intervals are sorted by start, overlapping intervals must be adjacent and can be absorbed into the running block.
Keeping the interval that ends earliest leaves the most room for every interval that comes after it.
Common Questions
"Do [1, 3] and [3, 5] overlap? Which character in my code decides that?"
It depends on the problem, and you must ask before you write code. If intervals that share an endpoint count as overlapping (Merge Intervals), the test is
incoming[0] <= merged[-1][1]. The<=merges them. If touching intervals do not count as overlapping (Non-overlapping Intervals), the test isstart >= prev_endto keep one. The only difference is one character,<or<=, and it changes the answer.
Coach discussion points
- 1.
"Do
[1,3]and[3,5]overlap? Which character in your code decides that, and what happens if I want the other behaviour?" — Tests whether the convention was a deliberate decision or an accident. Candidates who ask this before coding score highest. - 2.
"Your merge loop has an
emitinside it. Run me through[[1,4]]— a single interval — line by line." — Discovers the missing final flush by tracing rather than by being told. The cheapest possible test, and the one most solutions fail. - 3.
"Same list of meetings, but now I ask how many rooms I need instead of which blocks are busy. What changes about your approach, and why isn't merging enough?" — Separates "the intervals touch" from "how many are simultaneously live", which is the single most common conflation in this pattern.
Complexity analysis
Merge Intervals — Time O(n log n), Space O(n).
The sort dominates completely; the sweep after it is a single O(n) pass with O(1) work per interval. If the input arrives already sorted, the whole thing is O(n) — say that, because it is the follow-up question. Space is O(n) for the output, plus O(log n) for the sort's own stack; if you may modify the input in place, the auxiliary space is O(log n).
Meeting Rooms II — Time O(n log n), Space O(n).
Sort O(n log n), plus n heap pushes and up to n pops at O(log n) each — also O(n log n). Neither dominates asymptotically. The heap holds at most n end times.
A calendar service holds a room's bookings and must display them as consolidated blocks — three back-to-back meetings from 9–10, 9:30–11, and 10:45–12 should render as one busy block 9–12, not three overlapping bars. It must also answer the harder operational question: how many rooms does this schedule actually need? Both are interval sweeps. The same computation consolidates CPU-usage windows in monitoring dashboards, merges IP address ranges in firewall rules, and de-duplicates overlapping time ranges in billing.
💡 Don't worry if interval sorting or greedy proofs feel unintuitive right now! Sorting turns quadratic comparisons into one linear pass. As you work through the cards, the same three questions repeat: what is the sort key, what single value do I keep, and when do I save it.
Failing to sort intervals by start time: Interval merging fundamentally requires intervals to be sorted by their start coordinates (). Unsorted inputs cannot be merged in a single linear pass.
Overwriting the interval end instead of taking the maximum: When merging
currandnext, the new end time ismax(curr.end, next.end). Writingcurr.end = next.endcorrupts intervals wherenextis fully contained withincurr(e.g.[1, 5]and[2, 3]).Missing the final merged interval: Because intervals are accumulated dynamically, forgetting to commit the last interval to
mergeddrops the final span.
Staff+ Systems Architecture & Hardware Considerations
Triggers: "merge overlapping intervals", "fewest intervals to remove", "how many meetings overlap at once", "can you reach the last index". Each one is solved by sorting first and then making one pass.
Up to to intervals or positions. Sorting costs and the pass costs . Comparing every pair costs and is too slow.
Sorting by the wrong key (start for merging, end for choosing the most intervals); < instead of <= when intervals that touch count as overlapping; forgetting to save the last open interval after the loop; replacing the end instead of taking the larger end.
Conceptual Foundation & Intuitive Analogy
Intervals and greedy choices share one idea: sort the input first, then make one pass that keeps a single running value. Interval sweeps merge, insert and schedule 1D spans. Greedy passes pick the locally best option and never go back.
🧭 Conceptual Foundation & Pattern Intuition
Many algorithmic problems ask questions about overlapping spans:
- "Which calendar meetings conflict?" (Interval Merging)
- "How many conference rooms are needed at the same time?" (Sweep-Line Concurrency)
- "What is the most intervals I can keep without any overlap?" (Earliest Finish Time)
- "Can I reach the last index?" (Greedy Furthest Reach) In their raw form, these problems appear to need pairwise comparisons. Sorting removes that cost:
- Sorting by start time places every interval that can overlap the open interval directly after it. One left-to-right pass is enough, and the pass keeps one running interval.
- Sorting by end time lets a greedy pass keep the interval that ends first, which leaves the most room for the rest.
- A greedy pass without a sort keeps one number, such as the furthest index reached, in place of every possible path.
🛫 The Analogy: The Airport Runway Schedule
Think of a runway schedule. If the flight take-off windows are sorted by start time, air traffic control only has to check whether the next flight starts before the current block ends. If it does, the two windows join into one block. If it does not, the current block is finished and a new block starts.
🔑 Why It Works
After sorting, each element is compared with one running value, so each element is inspected a constant number of times. The sort costs and the pass costs .
After sorting by start, anything that overlaps the interval I am building is the very next one I will see. I never need to look backwards.
Sort first. Then one running value (the open interval, the last kept end, the furthest reach) is enough to decide each next item.
Merge: if curr.start <= prev.end: extend prev.end. Schedule: if curr.start >= prev_end: keep curr.
The 2 Mental Models of Intervals & Greedy
Interval merging, scheduling, sweep lines and one-way greedy choices: name the model before coding.
1Merge Intervals
intervals ➔ sort by start time ➔ merge or extend prev.endMerge Intervals
| 1 | def merge_intervals(intervals: list[list[int]]) -> list[list[int]]: |
| 2 | """Interval Merging via chronological start-time sorting. |
| 3 | Time: O(N log N) | Space: O(N) |
| 4 | """ |
| 5 | if not intervals: |
| 6 | return [] |
| 7 | |
| 8 | # 1. Sort by Start Time: |
| 9 | # Sorting puts every interval that can overlap the current one directly after it, so one |
| 10 | # left-to-right pass is enough: no interval later in the list can start earlier than this one. |
| 11 | intervals.sort(key=lambda iv: iv[0]) |
| 12 | merged: list[list[int]] = [] # merged holds finished, disjoint intervals, in order |
| 13 | accumulated = intervals[0] # accumulated is the one interval still open for absorbing overlaps |
| 14 | |
| 15 | for incoming in intervals[1:]: |
| 16 | # 2. Absorb or Close the Accumulator: |
| 17 | # incoming overlaps accumulated exactly when incoming starts at or before accumulated's end; |
| 18 | # touching at one shared point still counts as overlap. |
| 19 | if incoming[0] <= accumulated[1]: |
| 20 | accumulated[1] = max(accumulated[1], incoming[1]) # Keep the later end: a nested interval must not shrink it |
| 21 | else: |
| 22 | merged.append(accumulated) # No later interval starts before accumulated ends, so it is finished |
| 23 | accumulated = incoming # Start a new accumulator from incoming |
| 24 | |
| 25 | # 3. Flush the Last Accumulator: |
| 26 | # The loop only saves accumulated when a later interval closes it, so the final open interval is saved here. |
| 27 | merged.append(accumulated) |
| 28 | return merged |
Sort by start, then keep one accumulated interval open: if incoming[0] <= accumulated[1] the two overlap, so stretch accumulated[1]; otherwise save accumulated into merged and start again from incoming. After sorting, every later interval starts at or after incoming, so once one starts past the current end, nothing later can reach back into accumulated: it is finished. The trap is the end update: use max(accumulated[1], incoming[1]), because an interval nested inside another would otherwise shrink the merged end.
Gotcha 1 of 4
- The trap
Gotcha 1: Setting the end to
incoming[1]withoutmaxMistakeaccumulated[1] = incoming[1]Fixaccumulated[1] = max(accumulated[1], incoming[1])What happens:
incomingcan lie completely insideaccumulated. Thenincoming[1]is smaller thanaccumulated[1]. The plain assignment moves the merged end back toincoming[1]. A later interval that overlaps the old end is then saved as a separate interval.Example: intervals = [[1, 5], [2, 3], [4, 6]]Mistake: returns [[1, 3], [4, 6]]Fix: returns [[1, 6]]
2Greedy
choices in order ➔ locally best option (exchange argument) ➔ never undoneGreedy (Exchange Argument, Furthest Reach)
| 1 | def can_jump(nums: list[int]) -> bool: |
| 2 | """Jump Game: greedy furthest reach. |
| 3 | Time: O(N) | Space: O(1) |
| 4 | """ |
| 5 | reach = 0 # Furthest index some run of jumps can land on so far |
| 6 | |
| 7 | for i in range(len(nums)): |
| 8 | # 1. Stop When Index i Is Unreachable, Otherwise Extend reach: |
| 9 | # An index past reach has no jump landing on it, so it must never extend reach with its own jump; |
| 10 | # check that first. Otherwise take the larger of reach and i + nums[i], since a longer reach always |
| 11 | # contains every index a shorter one does. |
| 12 | if i > reach: |
| 13 | return False |
| 14 | reach = max(reach, i + nums[i]) |
| 15 | |
| 16 | return True # The loop stood on every index, the last one included |
You never need the path, only how far any path can get: keep reach, the furthest index a run of jumps can land on so far, and at every index you can stand on extend it with reach = max(reach, i + nums[i]). A jump may be shorter than nums[i], so every index up to reach can be landed on and the reachable indices always form one block 0..reach; taking the larger reach can never cost an option, because a longer block contains every index of a shorter one. That is the greedy argument in general: the locally best choice keeps everything any other choice would keep, so it never has to be undone. The trap is the order of the two lines: check if i > reach: return False before reading nums[i], because an index past reach can't be stood on, and letting its jump extend reach makes [3,2,1,0,4] return True.
Gotcha 1 of 3
- The trap
Gotcha 1: Updating
reachbefore checkingi > reachMistakereach = max(reach, i + nums[i]) if i > reach: return FalseFixif i > reach: return False reach = max(reach, i + nums[i])What happens: An index past
reachcannot be stood on. So its jump must never count. Updating first lets that index extendreachwith its own jump. Theni > reachis False, and the loop goes on as if indexicould be reached.Example: nums = [3, 2, 1, 0, 4]Mistake: returns TrueFix: returns False
Built on the mental models above
- Interval SchedulingInterval Scheduling (Activity Selection, Greedy by End Time)
Sort by end instead of start, and count or drop intervals instead of merging them.
- End-Time GroupsInterval Grouping (Minimum Arrows to Burst Balloons)
Count the groups of intervals that share a point instead of the intervals to remove: shoot at the end of the first interval not yet hit.
- Sweep LineSweep Line (Line Sweep)
Break each interval into a start event and an end event, sort the events, and track how many are open.
The Intervals & Greedy Decision Rule
When facing an unseen interval or greedy problem, ask in this order:
Compare curr.start <= prev.end and extend prev.end = max(prev.end, curr.end).
Sort by end time and greedily retain the earliest ending interval that starts at or after the last kept end.
Create +1 (start) and -1 (end) events, sorting end events first at equal timestamps.
Prove with an exchange argument that the greedy pick keeps every option another pick would, then take it at every step.
Sort by end and shoot at the end of the first interval not yet hit; an interval that starts at or before that spot is already hit.
Pattern Architecture & Invariant Design Masterclass
One idea runs through every card here: sort first, then keep one running value, and an O(n²) comparison problem becomes O(n).
Interval sweeps. An interval is a start and an end — a meeting from 9 to 10, a booking from the 3rd to the 6th. Asked "which of these overlap?", the instinct is to compare every pair: O(n²). But sort by start time and the problem changes shape: an interval that overlaps anything already merged must overlap the block being built, merged[-1], so a single left-to-right pass comparing each interval only to the one it is currently accumulating is enough. The sort costs O(n log n), the sweep O(n), and the sort dominates.
Greedy choices. A greedy problem offers a choice at each step. The greedy rule picks the locally best option and never goes back. It is correct only when you can argue that no other option could have done better. Jump Game keeps one number, reach, in place of every possible path.
The invariants to say out loud:
- Greedy: "Everything before index
iis decided, and one number summarizes what it allows."
How this plays out in actual problems:
🟢 Scenario A: Interval Merging & Absorption (Sort by Start Time)
- In interval merging (like Merge Intervals LC 56), we consolidate overlapping blocks into unified continuous spans.
- The Rule: Sort intervals by start time. Iterate left-to-right. If
incoming[0] <= merged[-1][1], expand the current interval usingmerged[-1][1] = max(merged[-1][1], incoming[1]). Otherwise, appendincomingas a new disjoint block. - The Code:
if incoming[0] <= merged[-1][1]: merged[-1][1] = max(merged[-1][1], incoming[1])
🔵 Scenario B: Earliest Finish Time Greedy Scheduling (Sort by End Time)
- In non-overlapping interval scheduling (like LC 435 / LC 452), we want to maximize the count of mutually compatible intervals.
- The Rule: Sort intervals by end time. Greedily select the interval that finishes earliest. Any subsequent interval whose
start < current_endmust be removed/skipped. - The Code:
if start >= prev_end: prev_end = end; count += 1
💡 The Ultimate Takeaway Decision Rule:
When analyzing an interval or sequence query problem, ask: "Are we merging overlapping spans, scheduling non-overlapping intervals, counting overlaps, or making a one-way choice?"
- Consolidating Overlapping Ranges ➔ Sort by start time
x[0], sequential absorption ().- Maximizing Non-Overlapping / Min Removals ➔ Sort by end time
x[1], greedy earliest finish selection ().- Concurrent Resource Peak (Meeting Rooms II) ➔ Chronological sweep-line boundary events
(t, +1)&(t, -1)().- One-way choice with a proof ➔ Greedy with a single running value, such as
reach().
📦 1. Interval Merge & Absorption (LC 56 & 57)
- Mental Model: Sort intervals by start time. After sorting, an interval that overlaps anything already merged must overlap the block being built,
merged[-1], so earlier blocks are never checked again. - Code: see the Merge Intervals card.
⏱️ 2. Earliest Finish Time Greedy Scheduling (LC 435 & 452)
- Mental Model: To leave maximal room for subsequent intervals, always pick the interval that finishes earliest. Sorting by end time guarantees optimal greedy choice.
- LC 435 vs. LC 452: In Non-overlapping Intervals (LC 435), intervals that only touch do not overlap, so the blueprint keeps an interval when
start >= prev_end. In Minimum Number of Arrows to Burst Balloons (LC 452), balloons that touch are burst by the same arrow, so a new arrow is needed only whenstart > prev_end. - Code: see the Interval Scheduling card.
📈 3. Sweep-Line Concurrency & Meeting Rooms II (LC 253)
- Mental Model: Decompose intervals into discrete point events:
+1room at start time,-1room at end time. Sort events chronologically. - Code: see the Sweep Line card.
Understanding Invariants: Interval Sweeps & Greedy Running Values
After sorting by start, anything that overlaps the interval I am building is the very next one I will see.
Interval sweeps sort chronologically to turn quadratic pairwise checks into single O(N) passes, and greedy passes keep one running value that summarizes everything already decided.
Because intervals are sorted by start, overlapping intervals must be adjacent and can be absorbed into the running block.
Keeping the interval that ends earliest leaves the most room for every interval that comes after it.
Common Questions
"Do [1, 3] and [3, 5] overlap? Which character in my code decides that?"
It depends on the problem, and you must ask before you write code. If intervals that share an endpoint count as overlapping (Merge Intervals), the test is
incoming[0] <= merged[-1][1]. The<=merges them. If touching intervals do not count as overlapping (Non-overlapping Intervals), the test isstart >= prev_endto keep one. The only difference is one character,<or<=, and it changes the answer.
Coach discussion points
- 1.
"Do
[1,3]and[3,5]overlap? Which character in your code decides that, and what happens if I want the other behaviour?" — Tests whether the convention was a deliberate decision or an accident. Candidates who ask this before coding score highest. - 2.
"Your merge loop has an
emitinside it. Run me through[[1,4]]— a single interval — line by line." — Discovers the missing final flush by tracing rather than by being told. The cheapest possible test, and the one most solutions fail. - 3.
"Same list of meetings, but now I ask how many rooms I need instead of which blocks are busy. What changes about your approach, and why isn't merging enough?" — Separates "the intervals touch" from "how many are simultaneously live", which is the single most common conflation in this pattern.
Complexity analysis
Merge Intervals — Time O(n log n), Space O(n).
The sort dominates completely; the sweep after it is a single O(n) pass with O(1) work per interval. If the input arrives already sorted, the whole thing is O(n) — say that, because it is the follow-up question. Space is O(n) for the output, plus O(log n) for the sort's own stack; if you may modify the input in place, the auxiliary space is O(log n).
Meeting Rooms II — Time O(n log n), Space O(n).
Sort O(n log n), plus n heap pushes and up to n pops at O(log n) each — also O(n log n). Neither dominates asymptotically. The heap holds at most n end times.
A calendar service holds a room's bookings and must display them as consolidated blocks — three back-to-back meetings from 9–10, 9:30–11, and 10:45–12 should render as one busy block 9–12, not three overlapping bars. It must also answer the harder operational question: how many rooms does this schedule actually need? Both are interval sweeps. The same computation consolidates CPU-usage windows in monitoring dashboards, merges IP address ranges in firewall rules, and de-duplicates overlapping time ranges in billing.
💡 Don't worry if interval sorting or greedy proofs feel unintuitive right now! Sorting turns quadratic comparisons into one linear pass. As you work through the cards, the same three questions repeat: what is the sort key, what single value do I keep, and when do I save it.
Failing to sort intervals by start time: Interval merging fundamentally requires intervals to be sorted by their start coordinates (). Unsorted inputs cannot be merged in a single linear pass.
Overwriting the interval end instead of taking the maximum: When merging
currandnext, the new end time ismax(curr.end, next.end). Writingcurr.end = next.endcorrupts intervals wherenextis fully contained withincurr(e.g.[1, 5]and[2, 3]).Missing the final merged interval: Because intervals are accumulated dynamically, forgetting to commit the last interval to
mergeddrops the final span.
Staff+ Systems Architecture & Hardware Considerations
Triggers: "merge overlapping intervals", "fewest intervals to remove", "how many meetings overlap at once", "can you reach the last index". Each one is solved by sorting first and then making one pass.
Up to to intervals or positions. Sorting costs and the pass costs . Comparing every pair costs and is too slow.
Sorting by the wrong key (start for merging, end for choosing the most intervals); < instead of <= when intervals that touch count as overlapping; forgetting to save the last open interval after the loop; replacing the end instead of taking the larger end.