扫描线(Sweep Line)是一种将二维问题降为一维的经典思想:用一条线沿某方向扫过平面,在扫描过程中用数据结构维护当前状态,高效处理区间覆盖、面积并等问题。
一、核心思想
Mermaid · 渲染中(下方为源码)
graph TD A[事件排序] --> B[扫描线] B --> C[数据结构维护当前状态] C --> D[区间覆盖/面积并]
- 将所有事件(开始/结束)按坐标排序
- 用一条"线"从左到右(或从下到上)扫描
- 在扫描过程中维护活跃集合(通常用线段树/优先队列)
- 在每个事件点更新答案
二、矩形面积并(LeetCode 850)
思路
- 将每个矩形拆成两条竖边事件:(x, y1, y2, +1) 和 (x, y1, y2, -1)
- 按 x 排序
- 扫描时用线段树维护 y 方向的覆盖长度
- 面积 += 覆盖长度 × Δx
int rectangleArea(int[][] rectangles) {
int MOD = 1_000_000_007;
List<int[]> events = new ArrayList<>(); // {x, y1, y2, type}
Set<Integer> ySet = new TreeSet<>();
for (int[] r : rectangles) {
events.add(new int[]{r[0], r[1], r[3], 1}); // 左边 +1
events.add(new int[]{r[2], r[1], r[3], -1}); // 右边 -1
ySet.add(r[1]); ySet.add(r[3]);
}
events.sort((a, b) -> a[0] - b[0]);
int[] ys = ySet.stream().mapToInt(Integer::intValue).toArray();
int m = ys.length - 1;
int[] count = new int[m]; // 每段被覆盖次数
long area = 0;
int prevX = events.get(0)[0];
for (int[] event : events) {
int x = event[0], y1 = event[1], y2 = event[2], type = event[3];
// 累加面积
int coveredLen = 0;
for (int i = 0; i < m; i++) {
if (count[i] > 0) coveredLen += ys[i+1] - ys[i];
}
area = (area + (long)coveredLen * (x - prevX)) % MOD;
prevX = x;
// 更新覆盖
int lo = Arrays.binarySearch(ys, y1);
int hi = Arrays.binarySearch(ys, y2);
for (int i = lo; i < hi; i++) count[i] += type;
}
return (int) area;
}三、天际线问题(LeetCode 218)
思路
- 建筑拆成 (left, -height) 和 (right, height) 事件
- 按 x 排序(同 x 时高度特殊处理)
- 用 TreeMap(或优先队列)维护当前最大高度
- 最大高度变化时产生关键点
List<List<Integer>> getSkyline(int[][] buildings) {
List<int[]> events = new ArrayList<>();
for (int[] b : buildings) {
events.add(new int[]{b[0], -b[2]}); // 进入(负高度)
events.add(new int[]{b[1], b[2]}); // 离开
}
events.sort((a, b) -> a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]);
TreeMap<Integer, Integer> heights = new TreeMap<>();
heights.put(0, 1);
List<List<Integer>> result = new ArrayList<>();
int prevMax = 0;
for (int[] e : events) {
int h = Math.abs(e[1]);
if (e[1] < 0) { // 进入
heights.merge(h, 1, Integer::sum);
} else { // 离开
if (heights.get(h) == 1) heights.remove(h);
else heights.merge(h, -1, Integer::sum);
}
int curMax = heights.lastKey();
if (curMax != prevMax) {
result.add(List.of(e[0], curMax));
prevMax = curMax;
}
}
return result;
}四、其他应用
| 问题 | 扫描方向 | 数据结构 |
|---|---|---|
| 矩形面积并 | x 方向 | 线段树(y 覆盖) |
| 天际线 | x 方向 | TreeMap / 堆 |
| 线段相交 | x 方向 | 平衡 BST |
| 最近点对 | x 方向 | 有序集合 |
| 区间调度 | 时间 | 优先队列 |
五、复杂度
| 问题 | 时间 |
|---|---|
| 矩形面积并(暴力) | O(n²) |
| 矩形面积并(线段树) | O(n log n) |
| 天际线 | O(n log n) |
六、面试要点
- 事件拆分:每个对象拆成进入/离开两个事件
- 排序:按扫描坐标排序
- 维护活跃集:堆/TreeMap/线段树
- 面积累加:覆盖长度 × Δx
- LeetCode:218(天际线)、850(矩形面积 II)、253(会议室 II)