區間(Intervals)
範圍 — 先排序再合併的區間題型 — 合併、插入、數重疊、最少刪除數。 另見:scanning_line.md — 需要維護一個即時計數時的事件掃描;difference_array.md — 大量區間更新、最後只讀一次;array_overlap_explaination.md — 重疊條件本身;heap.md — 用優先佇列做區間排程。
LeetCode 題目清單
總覽
區間類題目處理的是數值範圍,通常表示成 [start, end] 配對,要做的操作像是合併重疊範圍、找交集,或安排互不重疊的事件。
關鍵性質
- 時間複雜度:排序 O(n log n) + 處理 O(n) = 整體 O(n log n)
- 空間複雜度:O(1) 到 O(n),看輸出需求而定
- 核心想法:依起點排序,然後線性掃過去處理重疊
- 什麼時候用:牽涉到範圍、排程、行事曆管理、資源配置的題目
核心演算法步驟
- 依起點排序區間(貪婪題偶爾改成依終點)
- 依序處理,判斷哪些重疊、哪些不重疊
- 依題目需求套用合併/移除策略
- 處理邊界情況,像是空區間或只有一個區間
什麼時候用區間演算法
- 合併重疊範圍:行事曆衝突、記憶體配置
- 排程最佳化:會議室、任務分派
- 範圍查詢:時間序列資料、基因序列
- 資源管理:頻寬配置、CPU 排程
參考資料
1) 題型分類
模式 1:區間合併 — LC 56 Priority 5 of 5 — Must know — expect it in almost every loop
- 說明:把重疊的區間併成一個
- 例題:LC 56(Merge Intervals)、LC 57(Insert Interval)
- 辨認關鍵字:「合併」、「combine」、「重疊區間」
- 排序:依起點(遞增)
模式 2:區間排程(貪婪) — LC 435 Priority 4 of 5 — High value — a gap here costs you rounds
- 說明:求最多的互不重疊區間,或最少要移除幾個區間
- 例題:LC 435(Non-overlapping Intervals)、LC 452(Minimum Arrows)
- 辨認關鍵字:「最多」、「最少」、「不重疊」、「移除」
- 排序:貪婪解法依終點(遞增)
模式 3:區間交集 — LC 986 Priority 3 of 5 — Worth knowing — usually a variant of a must-know pattern
- 說明:找兩份區間清單的共同時段或重疊區域
- 例題:LC 986(Interval List Intersections)、LC 1288(Remove Covered Intervals)
- 辨認關鍵字:「交集」、「重疊」、「共同」、「被覆蓋」
- 排序:依起點排序,用雙指標處理
模式 4:用點覆蓋區間 — LC 452 Priority 3 of 5 — Worth knowing — usually a variant of a must-know pattern
- 說明:找出能同時覆蓋多個區間的點,或找出空隙
- 例題:LC 452(Minimum Arrows)、LC 1024(Video Stitching)
- 辨認關鍵字:「覆蓋」、「點」、「箭」、「最少覆蓋數」
- 排序:依策略決定用起點或終點
模式 5:會議室排程 — LC 253 Priority 5 of 5 — Must know — expect it in almost every loop
- 說明:算出需要幾間會議室,或檢查排程有沒有衝突
- 例題:LC 252(Meeting Rooms)、LC 253(Meeting Rooms II)
- 辨認關鍵字:「會議」、「會議室」、「排程衝突」
- 排序:依起點排序,用優先佇列管理房間
模式 6:行事曆與訂位 — LC 729
- 說明:處理行事曆訂位,偵測並解決衝突
- 例題:LC 729(My Calendar I)、LC 731(My Calendar II)、LC 732(My Calendar III)
- 辨認關鍵字:「行事曆」、「訂位」、「重複訂位」、「k 重訂位」
- 排序:維持區間有序,用二分搜尋找插入位置
2) 模板與演算法
模板比較表
| 模板類型 | 適用情境 | 排序策略 | 什麼時候用 |
|---|---|---|---|
| 合併模板 | 把重疊區間併起來 | 依起點排序 | LC 56、57,合併類題目 |
| 貪婪模板 | 最多互不重疊 | 依終點排序 | LC 435、452,排程最佳化 |
| 雙指標模板 | 交集/比對 | 兩份清單都依起點排序 | LC 986,比對兩份區間清單 |
| 優先佇列模板 | 資源管理 | 依起點排序,堆積依終點 | LC 253,會議室類題目 |
| 二分搜尋模板 | 行事曆/訂位 | 維持有序 | LC 729-732,動態插入區間 |
通用區間模板 Priority 4 of 5 — High value — a gap here costs you rounds
def solve_interval_problem(intervals):
"""
Universal template for interval problems
"""
# Step 1: Handle edge cases
if not intervals or len(intervals) <= 1:
return intervals
# Step 2: Sort intervals (by start time or end time based on problem)
intervals.sort(key=lambda x: x[0]) # Sort by start time
# intervals.sort(key=lambda x: x[1]) # Sort by end time for greedy problems
# Step 3: Initialize result
result = []
# Step 4: Process intervals sequentially
for current in intervals:
# Step 5: Check overlap condition with last processed interval
if not result or no_overlap_condition(result[-1], current):
result.append(current)
else:
# Step 6: Handle overlap (merge, count, or remove)
handle_overlap(result, current)
return result
def no_overlap_condition(prev, curr):
"""Check if two intervals don't overlap"""
return prev[1] < curr[0] # prev ends before curr starts
def handle_overlap(result, current):
"""Handle overlapping intervals based on problem type"""
# For merging: extend the last interval
result[-1][1] = max(result[-1][1], current[1])
# For counting: increment counter
# For removal: choose which interval to keep
各別模板
模板 1:區間合併(LC 56、57)
def merge_intervals(intervals):
"""
Merge overlapping intervals
Time: O(n log n), Space: O(n)
"""
if not intervals:
return []
# Sort by start time
intervals.sort(key=lambda x: x[0])
merged = [intervals[0]]
for current in intervals[1:]:
last = merged[-1]
# No overlap: add current interval
if last[1] < current[0]:
merged.append(current)
# Overlap: merge intervals
else:
last[1] = max(last[1], current[1])
return merged
// Java version
public int[][] merge(int[][] intervals) {
if (intervals.length <= 1) return intervals;
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
List<int[]> merged = new ArrayList<>();
for (int[] current : intervals) {
if (merged.isEmpty() || merged.get(merged.size() - 1)[1] < current[0]) {
merged.add(current);
} else {
merged.get(merged.size() - 1)[1] = Math.max(
merged.get(merged.size() - 1)[1], current[1]
);
}
}
return merged.toArray(new int[merged.size()][]);
}
模板 2:貪婪排程(LC 435、452)
def min_intervals_to_remove(intervals):
"""
Find minimum intervals to remove for non-overlapping set
Time: O(n log n), Space: O(1)
"""
if not intervals:
return 0
# Sort by end time (greedy strategy)
intervals.sort(key=lambda x: x[1])
count = 0
prev_end = intervals[0][1]
for i in range(1, len(intervals)):
# Overlap detected
if intervals[i][0] < prev_end:
count += 1 # Remove current interval
else:
prev_end = intervals[i][1] # Update end time
return count
// Java version
public int eraseOverlapIntervals(int[][] intervals) {
if (intervals.length <= 1) return 0;
Arrays.sort(intervals, (a, b) -> Integer.compare(a[1], b[1]));
int count = 0;
int prevEnd = intervals[0][1];
for (int i = 1; i < intervals.length; i++) {
if (intervals[i][0] < prevEnd) {
count++;
} else {
prevEnd = intervals[i][1];
}
}
return count;
}
模板 3:雙指標求交集(LC 986)
def interval_intersection(firstList, secondList):
"""
Find intersection of two interval lists
Time: O(m + n), Space: O(min(m, n))
"""
result = []
i = j = 0
while i < len(firstList) and j < len(secondList):
# Find intersection
start = max(firstList[i][0], secondList[j][0])
end = min(firstList[i][1], secondList[j][1])
# Valid intersection
if start <= end:
result.append([start, end])
# Move pointer of interval that ends first
if firstList[i][1] < secondList[j][1]:
i += 1
else:
j += 1
return result
模板 4:用優先佇列做會議室(LC 253)
import heapq
def min_meeting_rooms(intervals):
"""
Find minimum meeting rooms required
Time: O(n log n), Space: O(n)
"""
if not intervals:
return 0
# Sort by start time
intervals.sort(key=lambda x: x[0])
# Min heap to track end times
heap = []
for start, end in intervals:
# If earliest meeting ends before current starts
if heap and heap[0] <= start:
heapq.heappop(heap)
# Add current meeting's end time
heapq.heappush(heap, end)
return len(heap)
// Java version
public int minMeetingRooms(int[][] intervals) {
if (intervals.length == 0) return 0;
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
PriorityQueue<Integer> heap = new PriorityQueue<>();
for (int[] interval : intervals) {
if (!heap.isEmpty() && heap.peek() <= interval[0]) {
heap.poll();
}
heap.offer(interval[1]);
}
return heap.size();
}
模板 5:行事曆訂位(LC 729)
class MyCalendar:
"""
Calendar with overlap detection using binary search
Time: O(log n) per booking, Space: O(n)
"""
def __init__(self):
self.bookings = []
def book(self, start, end):
# Binary search for insertion position
left, right = 0, len(self.bookings)
while left < right:
mid = (left + right) // 2
if self.bookings[mid][1] <= start:
left = mid + 1
else:
right = mid
# Check overlap with neighbors
if left > 0 and self.bookings[left - 1][1] > start:
return False
if left < len(self.bookings) and self.bookings[left][0] < end:
return False
# No overlap, insert booking
self.bookings.insert(left, [start, end])
return True
3) 依模式分類的題目
合併模式題目
| 題目 | LC # | 關鍵技巧 | 難度 | 模板 |
|---|---|---|---|---|
| Merge Intervals | 56 | 依起點排序,合併重疊 | Medium | 合併模板 |
| Insert Interval | 57 | 插入後合併 | Medium | 合併模板 |
| Summary Ranges | 228 | 連續數字的範圍 | Easy | 合併模板 |
| Data Stream as Disjoint Intervals | 352 | TreeMap/SortedDict | Hard | 合併模板 |
| Merge Similar Items | 2363 | 依權重合併 | Easy | 合併模板 |
貪婪排程題目
| 題目 | LC # | 關鍵技巧 | 難度 | 模板 |
|---|---|---|---|---|
| Non-overlapping Intervals | 435 | 依終點排序,貪婪移除 | Medium | 貪婪模板 |
| Minimum Arrows to Burst Balloons | 452 | 依終點排序,數箭數 | Medium | 貪婪模板 |
| Maximum Length of Pair Chain | 646 | 依第二個元素排序 | Medium | 貪婪模板 |
| Activity Selection Problem | - | 經典貪婪演算法 | Medium | 貪婪模板 |
| Car Pooling | 1094 | 時間軸 + 容量 | Medium | 貪婪模板 |
| Partition Labels | 763 | 最後出現位置構成的區間 + 單趟合併 | Medium | 合併模板 |
| Jump Game II | 45 | 隱含區間 + 貪婪覆蓋 | Medium | 貪婪模板 |
| Jump Game | 55 | 掃描最遠可達位置 | Medium | 貪婪模板 |
Intersection & Coverage Problems
| Problem | LC # | Key Technique | Difficulty | Template |
|---|---|---|---|---|
| Interval List Intersections | 986 | Two pointers | Medium | Two Pointer Template |
| Remove Covered Intervals | 1288 | Sort and filter | Medium | Merge Template |
| Find Right Interval | 436 | Binary search | Medium | Binary Search |
| Employee Free Time | 759 | Merge + find gaps | Hard | Merge Template |
| Video Stitching | 1024 | Greedy coverage | Medium | Greedy Template |
| Maximum Area of a Piece of Cake After Horizontal and Vertical Cuts | 1465 | Max gap between sorted cuts | Medium | Gap Scan Template |
| Missing Ranges | 163 | Gap scan over sorted unique values | Medium | Gap Scan Template |
| Find All Numbers Disappeared in an Array II | 4031 | Clamp + dedupe + sort, then gap scan | Medium | Gap Scan Template |
會議室與排程題目
| 題目 | LC # | 關鍵技巧 | 難度 | 模板 |
|---|---|---|---|---|
| Meeting Rooms | 252 | 排序後檢查衝突 | Easy | 基本模板 |
| Meeting Rooms II | 253 | 優先佇列 | Medium | 優先佇列模板 |
| Meeting Scheduler | 1229 | 雙指標 + 時長 | Medium | 雙指標模板 |
| Minimum Time to Make Rope Colorful | 1578 | 連續區間 | Medium | 貪婪模板 |
| Course Schedule III | 630 | 優先佇列 + 貪婪 | Hard | 優先佇列模板 |
行事曆與訂位題目
| 題目 | LC # | 關鍵技巧 | 難度 | 模板 |
|---|---|---|---|---|
| My Calendar I | 729 | 有序清單 + 二分搜尋 | Medium | 行事曆模板 |
| My Calendar II | 731 | 偵測重複訂位 | Medium | 行事曆模板 |
| My Calendar III | 732 | 用時間軸做 k 重訂位 | Hard | 行事曆模板 |
| Exam Room | 855 | 維護最大間隔 | Medium | 二分搜尋 |
| Range Module | 715 | 線段樹/區間 | Hard | 進階模板 |
進階區間題目
| 題目 | LC # | 關鍵技巧 | 難度 | 模板 |
|---|---|---|---|---|
| Falling Squares | 699 | 座標壓縮 | Hard | 進階模板 |
| The Skyline Problem | 218 | 掃描線 + 優先佇列 | Hard | 進階模板 |
| Rectangle Area II | 850 | 座標壓縮 | Hard | 進階模板 |
| Perfect Rectangle | 391 | 面積計算 + 驗證 | Hard | 進階模板 |
| Count Integers in Intervals | 2276 | 動態區間 | Hard | 進階模板 |
4) 模式選擇策略
決策流程圖
Problem Analysis for Interval Problems:
1. Are you merging overlapping intervals?
├── YES → Use Merge Template (LC 56, 57)
│ ├── Single interval insertion? → Insert Interval Template
│ └── Multiple overlaps? → Standard Merge Template
└── NO → Continue to 2
2. Are you finding maximum non-overlapping intervals?
├── YES → Use Greedy Template (LC 435, 452)
│ ├── Sort by end time
│ └── Greedy selection strategy
└── NO → Continue to 3
3. Are you finding intersections between interval lists?
├── YES → Use Two Pointer Template (LC 986)
│ ├── Two sorted lists? → Standard Two Pointer
│ └── Multiple lists? → Merge then process
└── NO → Continue to 4
4. Are you managing meeting rooms or resources?
├── YES → Use Priority Queue Template (LC 253)
│ ├── Count resources needed? → Min heap approach
│ └── Check availability? → Sort + scan
└── NO → Continue to 5
5. Are you handling dynamic bookings/calendar?
├── YES → Use Calendar Template (LC 729-732)
│ ├── Single booking? → Binary search insertion
│ ├── Double booking allowed? → Two lists approach
│ └── K-booking? → Timeline/sweep line
└── NO → Consider Advanced Templates
6. Advanced cases (Skyline, Rectangles, etc.)
├── Coordinate compression needed?
├── Sweep line algorithm required?
└── Segment tree for range operations?
模板選擇指南
快速決策樹:
- 重疊判斷:
prev[1] >= curr[0](假設已依起點排序) - 合併策略:延伸
prev[1] = max(prev[1], curr[1]) - 貪婪策略:依終點排序,留下最早結束的
- 資源管理:對終點用最小堆
- 動態插入:用二分搜尋維持有序
5) 關鍵模式與重疊判斷
重疊判斷方法
方法 1:已依起點排序之後
def has_overlap(interval1, interval2):
"""Check if two intervals overlap (sorted by start)"""
return interval1[1] > interval2[0]
方法 2:一般情況(任意順序)
def has_overlap(interval1, interval2):
"""Check if two intervals overlap (any order)"""
start1, end1 = interval1
start2, end2 = interval2
return start1 < end2 and start2 < end1
重疊示意圖
Case 1 - No Overlap:
|----| interval1
|----| interval2
Case 2 - Overlap:
|-------|
|-------|
Case 3 - Complete Overlap:
|-----------|
|-----|
常見區間操作
def merge_two_intervals(a, b):
"""Merge two overlapping intervals"""
return [min(a[0], b[0]), max(a[1], b[1])]
def interval_length(interval):
"""Calculate interval length"""
return interval[1] - interval[0]
def intervals_intersection(a, b):
"""Find intersection of two intervals"""
start = max(a[0], b[0])
end = min(a[1], b[1])
return [start, end] if start <= end else None
def point_in_interval(point, interval):
"""Check if point is in interval"""
return interval[0] <= point <= interval[1]
6) 總結與速查
複雜度速查
| 操作 | 時間 | 空間 | 備註 |
|---|---|---|---|
| 排序區間 | O(n log n) | O(1) | 一定要做的第一步 |
| 合併重疊 | O(n) | O(n) | 排序之後 |
| 求交集 | O(m + n) | O(min(m,n)) | 雙指標解法 |
| 會議室 | O(n log n) | O(n) | 對終點用優先佇列 |
| 行事曆訂位 | O(log n) | O(n) | 每次插入做一次二分搜尋 |
| 貪婪排程 | O(n log n) | O(1) | 依終點排序 |
模板速查
| 模板 | 模式 | 關鍵程式碼 |
|---|---|---|
| 合併 | 重疊區間 | if last[1] < curr[0]: append else: merge |
| 貪婪 | 最多互不重疊 | sort(key=end); if curr[0] >= prev[1]: count++ |
| 雙指標 | 兩份清單求交集 | start=max(starts), end=min(ends) |
| 優先佇列 | 資源管理 | heappush(end_time); if heap[0] <= start: heappop |
| 二分搜尋 | 動態插入 | bisect.insort 或自己寫二分搜尋 |
常見模式與技巧
模式 1:合併重疊
# Standard merging after sorting
intervals.sort()
merged = [intervals[0]]
for curr in intervals[1:]:
if merged[-1][1] < curr[0]:
merged.append(curr)
else:
merged[-1][1] = max(merged[-1][1], curr[1])
模式 2:貪婪挑選
# Sort by end time for optimal selection
intervals.sort(key=lambda x: x[1])
count = 1
prev_end = intervals[0][1]
for start, end in intervals[1:]:
if start >= prev_end:
count += 1
prev_end = end
模式 3:時間軸事件
# Convert intervals to events for sweep line
events = []
for start, end in intervals:
events.append((start, 1)) # start event
events.append((end, -1)) # end event
events.sort()
解題步驟
- 辨認模式:是合併、排程、交集,還是資源管理?
- 選排序策略:依起點(合併)還是依終點(貪婪)
- 選模板:用上面對應的模板
- 處理邊界情況:空陣列、只有一個區間、完全相同的區間
- 最佳化:資料量大時考慮空間最佳化
常見錯誤與提示
🚫 常見錯誤:
- 排序順序搞錯:該依終點排序(貪婪題)卻依起點排序
- 差一錯誤:重疊條件裡
<=和<用混 - 沒處理邊界情況:沒檢查空陣列或只有一個區間
- 合併邏輯出錯:合併時忘了同時更新起點和終點
- 對貪婪策略一知半解:不理解為什麼依終點排序會對
- 空間複雜度:建了不必要的中間資料結構
✅ 最佳實務:
- 一律先排序:多數區間題都需要有序輸入
- 把重疊定義講清楚:動手寫程式前先明確定義重疊條件
- 用對模板:讓模板對上題目的模式
- 測邊界情況:空輸入、單一區間、完全相同的區間
- 把例子畫出來:把區間畫出來才看得懂重疊的樣態
- 選對排序 key:合併用起點,貪婪用終點
面試提示
- 先從例子開始:在紙上把區間畫出來
- 釐清邊界情況:空區間怎麼辦?長度為零的點區間呢?
- 解釋排序的選擇:為什麼依起點/終點排序?
- 走一遍演算法:一步一步展示合併/貪婪的邏輯
- 逐步最佳化:先寫出能跑的解,再最佳化
- 練熟常見模式:把上面五個主要模板練到精
- 分析時間複雜度:一定要說明是排序 O(n log n) + 處理 O(n)
資料結構轉換技巧
List 轉陣列(Java)
List<int[]> result = new ArrayList<>();
// ... populate result
return result.toArray(new int[result.size()][]);
Python 中的高效合併
# Using list comprehension for functional style
def merge_intervals(intervals):
intervals.sort()
result = [intervals[0]]
[result.append(curr) if result[-1][1] < curr[0]
else result[-1].__setitem__(1, max(result[-1][1], curr[1]))
for curr in intervals[1:]]
return result
相關主題
- 貪婪演算法:區間排程最佳化
- 二分搜尋:行事曆訂位與插入類題目
- 優先佇列:會議室與資源管理
- 雙指標:交集與比對類題目
- 掃描線:天際線、矩形這類進階題
- 線段樹:區間上的範圍更新與查詢
LC 範例
2-1) Merge Intervals (LC 56) — 排序 + 合併
依起點排序;跟最後一個已合併的區間比對,把重疊的併起來。
// LC 56 - Merge Intervals
// IDEA: Sort by start, merge when current.start <= last.end
// time = O(N log N), space = O(N)
public int[][] merge(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
List<int[]> merged = new ArrayList<>();
for (int[] interval : intervals) {
if (merged.isEmpty() || merged.get(merged.size()-1)[1] < interval[0]) {
merged.add(interval);
} else {
merged.get(merged.size()-1)[1] = Math.max(merged.get(merged.size()-1)[1], interval[1]);
}
}
return merged.toArray(new int[merged.size()][]);
}
2-2) Non-overlapping Intervals (LC 435) — 貪婪區間排程
依終點排序;貪婪地留下最早結束的區間,讓移除數最少。
// LC 435 - Non-overlapping Intervals
// IDEA: Greedy — sort by end, count overlapping intervals to remove
// time = O(N log N), space = O(1)
public int eraseOverlapIntervals(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> a[1] - b[1]);
int removals = 0, prevEnd = Integer.MIN_VALUE;
for (int[] interval : intervals) {
if (interval[0] < prevEnd) {
removals++; // overlap: remove current (keep the one ending earlier)
} else {
prevEnd = interval[1];
}
}
return removals;
}
2-3) Insert Interval (LC 57) — 線性掃描 + 合併
插入新區間,並在一趟掃描中把所有重疊的區間合併掉。
// LC 57 - Insert Interval
// IDEA: Three phases — add non-overlapping left, merge overlapping, add right
// time = O(N), space = O(N)
public int[][] insert(int[][] intervals, int[] newInterval) {
List<int[]> result = new ArrayList<>();
int i = 0, n = intervals.length;
// Phase 1: add all intervals that end before newInterval starts
while (i < n && intervals[i][1] < newInterval[0]) result.add(intervals[i++]);
// Phase 2: merge overlapping intervals
while (i < n && intervals[i][0] <= newInterval[1]) {
newInterval[0] = Math.min(newInterval[0], intervals[i][0]);
newInterval[1] = Math.max(newInterval[1], intervals[i][1]);
i++;
}
result.add(newInterval);
// Phase 3: add remaining intervals
while (i < n) result.add(intervals[i++]);
return result.toArray(new int[result.size()][]);
}
2-4) Meeting Rooms II (LC 253) — 對終點用最小堆
依起點排序;堆積追蹤最早結束的那間房 — 如果它在下一場會議開始前就結束,就重複使用。
// LC 253 - Meeting Rooms II
// IDEA: Sort by start; min-heap of end times — reuse room if heap.peek() <= start
// time = O(N log N), space = O(N)
public int minMeetingRooms(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
PriorityQueue<Integer> heap = new PriorityQueue<>();
for (int[] iv : intervals) {
if (!heap.isEmpty() && heap.peek() <= iv[0]) heap.poll();
heap.offer(iv[1]);
}
return heap.size();
}
2-5) Minimum Number of Arrows to Burst Balloons (LC 452) — 貪婪
依終點排序;在區間終點射一箭就能戳破所有重疊的;出現空隙時才往前推進。
// LC 452 - Minimum Number of Arrows to Burst Balloons
// IDEA: Greedy — sort by end; new arrow only when next start > current end
// time = O(N log N), space = O(1)
public int findMinArrowShots(int[][] points) {
Arrays.sort(points, (a, b) -> Integer.compare(a[1], b[1]));
int arrows = 1, end = points[0][1];
for (int i = 1; i < points.length; i++)
if (points[i][0] > end) { arrows++; end = points[i][1]; }
return arrows;
}
2-6) Interval List Intersections (LC 986) — 雙指標
推進終點較早的那個指標;範圍相交時記錄下重疊部分。
// LC 986 - Interval List Intersections
// IDEA: Two pointers — compute intersection, advance pointer with smaller end
// time = O(M+N), space = O(M+N)
public int[][] intervalIntersection(int[][] A, int[][] B) {
List<int[]> res = new ArrayList<>();
int i = 0, j = 0;
while (i < A.length && j < B.length) {
int lo = Math.max(A[i][0], B[j][0]);
int hi = Math.min(A[i][1], B[j][1]);
if (lo <= hi) res.add(new int[]{lo, hi});
if (A[i][1] < B[j][1]) i++;
else j++;
}
return res.toArray(new int[res.size()][]);
}
2-7) Remove Covered Intervals (LC 1288) — 排序 + 貪婪
依起點遞增、終點遞減排序;若某區間的終點 ≤ 目前的最大終點,它就是被覆蓋的。
// LC 1288 - Remove Covered Intervals
// IDEA: Sort start ASC, end DESC; count intervals not covered by running maxEnd
// time = O(N log N), space = O(1)
public int removeCoveredIntervals(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> a[0] != b[0] ? a[0] - b[0] : b[1] - a[1]);
int count = 0, maxEnd = 0;
for (int[] iv : intervals)
if (iv[1] > maxEnd) { count++; maxEnd = iv[1]; }
return count;
}
2-8) Video Stitching (LC 1024) — 貪婪區間覆蓋
依起點排序;在每個邊界上挑那個能把覆蓋範圍推得最遠的片段。
// LC 1024 - Video Stitching
// IDEA: Greedy — at current end, pick clip reaching farthest next position
// time = O(N log N), space = O(1)
public int videoStitching(int[][] clips, int time) {
Arrays.sort(clips, (a, b) -> a[0] - b[0]);
int count = 0, curEnd = 0, farthest = 0, i = 0;
while (i < clips.length && curEnd < time) {
while (i < clips.length && clips[i][0] <= curEnd)
farthest = Math.max(farthest, clips[i++][1]);
if (farthest == curEnd) return -1;
curEnd = farthest;
count++;
}
return curEnd >= time ? count : -1;
}
2-9) Maximum Profit in Job Scheduling (LC 1235) — DP + 二分搜尋
工作依終點排序;dp[i] = 用前 i 個工作能得到的最大利潤;用二分搜尋找最後一個不衝突的工作。
// LC 1235 - Maximum Profit in Job Scheduling
// IDEA: Sort by end; DP + binary search for latest non-overlapping job
// time = O(N log N), space = O(N)
public int jobScheduling(int[] startTime, int[] endTime, int[] profit) {
int n = startTime.length;
int[][] jobs = new int[n][3];
for (int i = 0; i < n; i++) jobs[i] = new int[]{endTime[i], startTime[i], profit[i]};
Arrays.sort(jobs, (a, b) -> a[0] - b[0]);
int[] dp = new int[n + 1];
for (int i = 0; i < n; i++) {
int lo = 0, hi = i;
while (lo < hi) {
int mid = (lo + hi + 1) / 2;
if (jobs[mid-1][0] <= jobs[i][1]) lo = mid;
else hi = mid - 1;
}
dp[i+1] = Math.max(dp[i], dp[lo] + jobs[i][2]);
}
return dp[n];
}
2-10) My Calendar I (LC 729) — TreeMap 重疊檢查
TreeMap 的 floor/ceiling 讓每次訂位的重疊偵測是 O(log N)。
// LC 729 - My Calendar I
// IDEA: TreeMap — O(log N) overlap check with floorKey / ceilingKey
// time = O(log N) per booking, space = O(N)
class MyCalendar {
TreeMap<Integer, Integer> cal = new TreeMap<>();
public boolean book(int start, int end) {
Integer prev = cal.floorKey(start), next = cal.ceilingKey(start);
if ((prev == null || cal.get(prev) <= start) && (next == null || next >= end)) {
cal.put(start, end);
return true;
}
return false;
}
}
2-11) Meeting Rooms I (LC 252) — 排序 + 相鄰檢查
依起點排序;只要有任何一場會議在前一場結束前開始,就存在重疊。
// LC 252 - Meeting Rooms
// IDEA: Sort by start; adjacent overlap check
// time = O(N log N), space = O(1)
public boolean canAttendMeetings(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
for (int i = 1; i < intervals.length; i++)
if (intervals[i][0] < intervals[i-1][1]) return false;
return true;
}
2-12) Partition Labels (LC 763) — 從資料造出區間,再合併 Priority 5 of 5 — Must know — expect it in almost every loop
關鍵想法:區間不是給你的 — 是你造出來的。每個字元
c擁有區間[first(c), last(c)];一個合法的分段就是一個合併後的區間。因為我們是從左掃到右,這些區間抵達時就已經依起點排好序,所以不需要sort():維護一個滾動的end = max(end, last[c]),i == end的那一刻就切一刀。模式:
輸入 → 推導出區間 → 合併是面試中最常見的區間偽裝(LC 56 的引擎也是它,只是輸入變成隱含的)。
為什麼 i == end 是正確的切點:end 是目前為止看過的每個字元其最後出現位置的最大值。當掃描索引追上它時,[start, i] 裡的任何字元都不會在後面再出現 ⇒ 這個區塊就封閉了,不可能再跟右邊的東西合併。
s = a b a b c b a c a d e f e g d e h i j h k l i j
i: 0 1 2 3 4 5 6 7 8 9 ...
last[a]=8, last[b]=5, last[c]=7 -> end grows 0,5,5,5,7,7,8,8,8 -> cut at i=8 (len 9)
next block starts at 9 ...
// java
// LC 763 - Partition Labels
// IDEA: last occurrence of each char = that char's interval end; extend & cut in one pass (no sort)
// time = O(N), space = O(1) (26 letters)
public List<Integer> partitionLabels(String s) {
int[] last = new int[26];
for (int i = 0; i < s.length(); i++) last[s.charAt(i) - 'a'] = i;
List<Integer> res = new ArrayList<>();
int start = 0, end = 0;
for (int i = 0; i < s.length(); i++) {
end = Math.max(end, last[s.charAt(i) - 'a']); // extend current interval
if (i == end) { // interval closed -> cut here
res.add(end - start + 1);
start = i + 1;
}
}
return res;
}
# python
# LC 763 - Partition Labels
# IDEA: last[c] = that char's interval end; extend running end, cut when i == end
# time = O(N), space = O(1) (26 letters)
def partitionLabels(s):
last = {c: i for i, c in enumerate(s)} # dict comprehension keeps the LAST index
res, start, end = [], 0, 0
for i, c in enumerate(s):
end = max(end, last[c]) # extend current interval
if i == end: # interval closed -> cut here
res.append(end - start + 1)
start = i + 1
return res
跟 LC 56(2-1)的對照:LC 56 非排序不可,因為區間是以任意順序給的;這裡掃描順序就是起點順序,所以成本降到 O(N)。
2-13) Jump Game II (LC 45) — 對隱含區間做貪婪覆蓋
2-8(Video Stitching)的變形:這裡的轉折是根本沒給你區間陣列 — 索引
i隱含地覆蓋[i, i + nums[i]],而且這些區間本來就依起點排好了,所以 O(N log N) 的排序消失,貪婪覆蓋只要 O(N)。模式:跟 LC 1024 一樣的雙邊界貪婪 —
curEnd= 目前為止這些跳躍買到的覆蓋邊界,farthest= 所有起點落在其中的區間裡最遠能到哪。碰到i == curEnd代表這一層用完了 ⇒ 再花一次跳躍。
// java
// LC 45 - Jump Game II
// IDEA: index i = interval [i, i+nums[i]]; greedy cover, +1 jump when current coverage is exhausted
// time = O(N), space = O(1)
public int jump(int[] nums) {
int jumps = 0, curEnd = 0, farthest = 0;
for (int i = 0; i < nums.length - 1; i++) { // stop at n-1: no jump needed once we can reach it
farthest = Math.max(farthest, i + nums[i]);
if (i == curEnd) { // exhausted current layer -> must jump
jumps++;
curEnd = farthest;
}
}
return jumps;
}
# python
# LC 45 - Jump Game II
# IDEA: index i = interval [i, i+nums[i]]; greedy cover, +1 jump when current coverage is exhausted
# time = O(N), space = O(1)
def jump(nums):
jumps = cur_end = farthest = 0
for i in range(len(nums) - 1): # stop before last index
farthest = max(farthest, i + nums[i])
if i == cur_end: # exhausted current layer -> must jump
jumps += 1
cur_end = farthest
return jumps
變形 — LC 55 Jump Game:只問能不能到,把跳躍計數器拿掉
一樣的掃描;不再數層數,而是在掃描索引越過最遠覆蓋位置的那一刻就宣告失敗(覆蓋出現了空隙)。
// java
// LC 55 - Jump Game
// IDEA: same coverage scan; unreachable once i > farthest (gap in the cover)
// time = O(N), space = O(1)
public boolean canJump(int[] nums) {
int farthest = 0;
for (int i = 0; i < nums.length; i++) {
if (i > farthest) return false; // gap: cover breaks here
farthest = Math.max(farthest, i + nums[i]);
}
return true;
}
# python
# LC 55 - Jump Game
# IDEA: same coverage scan; unreachable once i > farthest (gap in the cover)
# time = O(N), space = O(1)
def canJump(nums):
farthest = 0
for i, n in enumerate(nums):
if i > farthest:
return False # gap: cover breaks here
farthest = max(farthest, i + n)
return True
| 題目 | 有給區間嗎? | 需要排序嗎? | 問的問題 |
|---|---|---|---|
| LC 1024 Video Stitching | 明確給了 clips[i] = [s, e] |
要 — O(N log N) | 覆蓋 [0, time] 最少要幾段 |
| LC 45 Jump Game II | 隱含的 [i, i+nums[i]] |
不用 — 本來就依起點排好 | 到 n-1 最少要幾個區間 |
| LC 55 Jump Game | 隱含的 [i, i+nums[i]] |
不用 | [0, n-1] 到底能不能被覆蓋 |
2-14) Maximum Area of a Piece of Cake (LC 1465) — 間隔掃描(區間的補集)
關鍵想法:一組切點的補集就是一組區間。把邊界排序之後,每一塊就只是相鄰的差值 — 再加上兩個邊界間隔(
0 → 第一刀和最後一刀 → 邊界),幾乎所有錯誤答案都是漏了這兩個。模式:同一套「排序後邊界之間的間隔」掃描,也能解各種找空檔的題目(例如 LC 759 Employee Free Time 合併後的空隙、LC 228 Summary Ranges)。這題兩個軸是獨立的,所以
maxArea = maxGap(h) * maxGap(w)。
// java
// LC 1465 - Maximum Area of a Piece of Cake After Horizontal and Vertical Cuts
// IDEA: complement of sorted cuts = piece intervals; max gap per axis, multiply (axes independent)
// time = O(H log H + V log V), space = O(1)
public int maxArea(int h, int w, int[] horizontalCuts, int[] verticalCuts) {
Arrays.sort(horizontalCuts);
Arrays.sort(verticalCuts);
long maxH = maxGap(horizontalCuts, h);
long maxV = maxGap(verticalCuts, w);
return (int) ((maxH * maxV) % 1_000_000_007L); // multiply as long: 1e9 * 1e9 overflows int
}
private long maxGap(int[] cuts, int border) {
long best = cuts[0]; // border gap: 0 -> first cut
for (int i = 1; i < cuts.length; i++)
best = Math.max(best, cuts[i] - cuts[i-1]); // inner gaps: cut -> cut
return Math.max(best, border - cuts[cuts.length - 1]); // border gap: last cut -> border
}
# python
# LC 1465 - Maximum Area of a Piece of Cake After Horizontal and Vertical Cuts
# IDEA: complement of sorted cuts = piece intervals; max gap per axis, multiply (axes independent)
# time = O(H log H + V log V), space = O(1)
def maxArea(h, w, horizontalCuts, verticalCuts):
def max_gap(cuts, border):
cuts = sorted(cuts)
best = max(cuts[0], border - cuts[-1]) # the two border gaps
for a, b in zip(cuts, cuts[1:]):
best = max(best, b - a) # inner gaps
return best
return (max_gap(horizontalCuts, h) * max_gap(verticalCuts, w)) % (10 ** 9 + 7)
2-15) Find All Numbers Disappeared in an Array II (LC 4031) — Gap Scan With a Sentinel Priority 4 of 5 — High value — a gap here costs you rounds
Key Idea: same interval complement scan as 2-14, but the answer is the gaps themselves: sort the values, and every jump from
prevtoxlarger than 1 exposes the missing block[prev+1, x-1].Two things make this harder than LC 163 (Missing Ranges), which hands you a sorted, duplicate-free array already inside
[lower, upper]. Herenumsis unsorted, may hold duplicates, and may hold values outside the range — all three break the scan, and all three are fixed by onesorted(set(...))over the clamped values before the loop.The sentinel
prev = lower - 1is what removes the leading special case: the first real value is then compared againstlowerexactly like every inner value is compared against its predecessor. The trailing gap has no such trick — nothing follows the last value, so it needs an explicit check after the loop. Forgetting it is the standard wrong answer.
nums = [3,9,7], lower = 1, upper = 12
clamp + dedupe + sort -> [3, 7, 9]
prev=0 x=3 -> 3 > 0+1 -> emit [1, 2] prev=3
x=7 -> 7 > 3+1 -> emit [4, 6] prev=7
x=9 -> 9 > 7+1 -> emit [8, 8] prev=9 (single missing number = width-1 range)
after loop: prev=9 < 12 -> emit [10, 12] (trailing gap: NOT covered by the loop)
res = [[1,2], [4,6], [8,8], [10,12]]
# python
# LC 4031 - Find All Numbers Disappeared in an Array II
# IDEA: clamp to [lower, upper] + dedupe + sort, then emit each gap between consecutive values
# time = O(N log N), space = O(N)
def findDisappearedNumbers(nums, lower, upper):
nums = sorted({x for x in nums if lower <= x <= upper})
res = []
prev = lower - 1 # sentinel: makes the leading gap an ordinary gap
for x in nums:
if x > prev + 1: # a hole between prev and x
res.append([prev + 1, x - 1])
prev = x
if prev < upper: # trailing gap: the loop can never emit it
res.append([prev + 1, upper])
return res
// java
// LC 4031 - Find All Numbers Disappeared in an Array II
// IDEA: TreeSet clamps + dedupes + sorts in one pass; then the same gap scan
// time = O(N log N), space = O(N)
public List<List<Integer>> findDisappearedNumbers(int[] nums, int lower, int upper) {
TreeSet<Integer> seen = new TreeSet<>();
for (int x : nums)
if (x >= lower && x <= upper) seen.add(x);
List<List<Integer>> res = new ArrayList<>();
int prev = lower - 1; // sentinel
for (int x : seen) {
if (x > prev + 1) res.add(Arrays.asList(prev + 1, x - 1));
prev = x;
}
if (prev < upper) res.add(Arrays.asList(prev + 1, upper)); // trailing gap
return res;
}
🚫 Traps: (1) dropping the trailing gap, or the leading one if you start from nums[0] instead of the sentinel; (2) not filtering values outside [lower, upper] — one stray x < lower makes prev run backwards and emits garbage; (3) not deduping — a repeated value gives x == prev, so x > prev + 1 is false and nothing breaks here, but the same array in the LC 163 two-pointer form emits an inverted range [prev+1, x-1]; (4) [8,8] is a legal answer — a single missing number is a range whose ends coincide, not a value to be skipped.
| Problem | Input guarantees | Extra work before the scan |
|---|---|---|
| LC 163 Missing Ranges | sorted, unique, all inside [lower, upper] |
none — scan directly, O(N) |
| LC 228 Summary Ranges | sorted, unique | none — emits the present runs, the complement of this |
| LC 4031 Disappeared Numbers II | none of the above | clamp + dedupe + sort, O(N log N) |