扫描线

用一条竖线从左扫到右,把二维的矩形面积问题压成一维的线段树维护

一、要解决的问题

平面上 n 个矩形(边与坐标轴平行,坐标可达 109),求它们并集的面积——重叠部分只算一次。直接离散成格子做标记,坐标范围太大存不下;扫描线把它降到 O(n log n)。

生活类比:量一堵贴满海报的墙的总覆盖面积。你不用逐格检查——拿一把直尺从左往右匀速移动,只需记录"当前直尺上被海报盖住的总长度",再乘上直尺每次移动的距离,逐段累加就是总面积。海报的左边缘让覆盖"开始",右边缘让覆盖"结束"。

二、核心框架

  1. 拆事件:每个矩形拆成两条竖边:左边缘(y₁→y₂,+1 进入事件)、右边缘(y₁→y₂,−1 离开事件)。
  2. 按 x 排序:所有事件按横坐标从小到大排序。
  3. 扫描:相邻事件 xi、xi+1 之间,"当前被覆盖的 y 总长度"不变,这段贡献 = 长度 × (xi+1 − xi)。先用旧长度算贡献,再处理事件更新。
  4. 维护覆盖长度:y 轴上用线段树维护"被至少一个矩形覆盖的总长度"。
矩形 A 矩形 B 扫描线 x = x₃(B 的左边缘,+1 事件) 扫描方向 → 事件序列(按 x): x₁:A 进入 [Ay₁, Ay₂) +1 x₃:B 进入 [By₁, By₂) +1 x₂:A 离开      −1 x₄:B 离开      −1 每段面积 = 当前覆盖长 × Δx 重叠区域只被"覆盖长度"统计一次
两个矩形的扫描:4 条竖边产生 4 个事件,覆盖长度在事件之间保持不变

三、配套技术①:离散化

y 坐标可达 109,但不同的 y 值最多 2n 个。把所有出现的 y 收集起来、排序、去重,用排名代替原值:

vector<long long> ys;                    // 收集所有 y₁、y₂
sort(ys.begin(), ys.end());
ys.erase(unique(ys.begin(), ys.end()), ys.end());
// 原值 y → 下标:lower_bound(ys.begin(), ys.end(), y) - ys.begin()

关键认识:离散化后的相邻下标 i、i+1 对应一段真实长度 ys[i+1] − ys[i] 的区间。线段树的叶子不再是"一个点",而是"一个段"——这是扫描线线段树与普通线段树最大的不同。

四、配套技术②:线段树维护覆盖(不下传)

零基础提示 · 线段树是什么:线段树把一个大区间递归对半分成一棵二叉树,每个节点负责一段区间并记录这段的汇总信息(这里是"被覆盖长度")。修改和查询都只需访问 O(log n) 个节点。把 y 轴离散化成若干"段"后,线段树就建在这些段上。

每个线段树节点维护两个值:

cnt > 0 → len = 整段真实长度(ys[r+1] − ys[l])
cnt = 0 且非叶子 → len = 左儿子 len + 右儿子 len

因为所有更新都是整段覆盖、且只查询全局总长(根节点的 len),不需要懒标记下传——这是扫描线线段树最精妙的地方:不存在的"单点查询"让它免于维护下传逻辑。

void update(int u, int l, int r, int ql, int qr, int v) {   // 对[ql,qr]整体+v
    if (ql <= l && r <= qr) cnt[u] += v;
    else { /* 递归左右儿子(注意右边界对应段的下标换算) */ }
    if (cnt[u] > 0) len[u] = ys[r+1] - ys[l];
    else if (l == r) len[u] = 0;
    else len[u] = len[u<<1] + len[u<<1|1];
}

五、完整流程走查

步骤动作复杂度
1读入 n 个矩形,拆成 2n 个事件,收集所有 yO(n)
2y 离散化;事件按 x 排序O(n log n)
3依次处理事件:贡献 += 根 len × (xi − xi−1),再 update 该事件的 y 段 ±1O(n log n)
4输出累计面积—

数字走查:矩形 A(x∈[0,3),y∈[0,2))与 B(x∈[2,5),y∈[1,3))

事件 x事件内容本段贡献(旧覆盖长 × Δx)处理后覆盖段(长度)
0A 进入 y[0,2)—(起点)[0,2)(2)
2B 进入 y[1,3)2 × (2−0) = 4[0,3)(3)
3A 离开3 × (3−2) = 3[1,3)(2)
5B 离开2 × (5−3) = 4无(0)
合计4 + 3 + 4 = 11

验证:A 面积 6 + B 面积 6 − 重叠 1 = 11 ✓。注意 x=2 处的贡献用的是 B 进入之前的覆盖长 2——"先算旧状态、再更新"。

顺序要点:先算贡献、再处理当前事件。因为"贡献"用的是上一条线到这一条线之间的覆盖状态,属于旧长度。

六、变体:矩形周长并(P1856)

同样扫描,但线段树要额外维护:覆盖的段数、左右端点是否被覆盖。竖边贡献 = 段数 × 2 × Δx,横边贡献 = 相邻两次扫描线覆盖长度的差的绝对值。思路一致,细节更多,是面积并过关后的标准加练。

七、易错点清单

八、练习

考情提示:扫描线是 NOI 大纲 2025 修订版对提高级新增的 7 级知识点,且可与线段树、离散化组合出题,值得按"模板默写级"准备。

配套小测(5 题,即时判分)