P5490 【模板】扫描线 & 矩形面积并
提供一个扫描线模板代码,后面附带和人类结晶ds的扫描线答疑过程
#include<bits/stdc++.h> #define ll long long #define lc rt << 1 #define rc rt << 1 | 1 #define lson lc, l, mid #define rson rc, mid + 1, r using namespace std; const int N = 2e5 + 10; struct ScanLine { ll x, y1, y2; int flag; // 1表示入边,-1表示出边 } line[N << 1]; // 每个矩形产生2条边 struct Node { ll len; // 当前区间被覆盖的总长度 int cnt; // 当前区间被完全覆盖的次数 } t[N << 3]; // 线段树开8倍空间 ll ys[N << 1]; // 静态数组存储离散化后的y坐标 int n, m, cnt_y; // cnt_y记录离散化后的y坐标数量 void pushup(int rt, int l, int r) { if (t[rt].cnt) { t[rt].len = ys[r + 1] - ys[l]; } else if (l != r) { t[rt].len = t[lc].len + t[rc].len; } else { t[rt].len = 0; } } void build(int rt, int l, int r) { t[rt] = {0, 0}; if (l == r) return; int mid = (l + r) >> 1; build(lson); build(rson); } void update(int rt, int l, int r, int L, int R, int val) { if (R < l || L > r) return; if (L <= l && r <= R) { t[rt].cnt += val; pushup(rt, l, r); return; } int mid = (l + r) >> 1; update(lson, L, R, val); update(rson, L, R, val); pushup(rt, l, r); } bool cmp(const ScanLine &a, const ScanLine &b) { return a.x < b.x; } int main() { scanf("%d", &n); cnt_y = 0; for (int i = 1; i <= n; i++) { ll x1, y1, x2, y2; scanf("%lld%lld%lld%lld", &x1, &y1, &x2, &y2); line[2 * i - 1] = {x1, y1, y2, 1}; // 入边 line[2 * i] = {x2, y1, y2, -1}; // 出边 ys[++cnt_y] = y1; ys[++cnt_y] = y2; } // 离散化y坐标 sort(ys + 1, ys + cnt_y + 1); cnt_y = unique(ys + 1, ys + cnt_y + 1) - ys - 1; m = cnt_y - 1; // 线段树管理的区间是[1, m] // 按照x坐标排序扫描线 sort(line + 1, line + 2 * n + 1, cmp); build(1, 1, m); // 线段树区间从1开始 ll res = 0; for (int i = 1; i <= 2 * n; i++) { if (i > 1) { res += t[1].len * (line[i].x - line[i - 1].x); } // 找到离散化后的y坐标(下标从1开始) int y1 = lower_bound(ys + 1, ys + cnt_y + 1, line[i].y1) - ys; int y2 = lower_bound(ys + 1, ys + cnt_y + 1, line[i].y2) - ys - 1; update(1, 1, m, y1, y2, line[i].flag); } printf("%lld\n", res); return 0; }
在扫描线算法中,1 和 -1 用于标记矩形的 入边(左边界) 和 出边(右边界),这是为了高效维护当前 y轴区间被覆盖的次数。下面我会详细解释它们的意义和作用原理:
1. 为什么用 1 和 -1?
-
1(入边):
当扫描线遇到矩形的 左边界(x=x₁) 时,表示从这个 x 坐标开始,该矩形的 y 区间[y₁, y₂]开始被覆盖。此时需要 +1 表示新增覆盖。 -
-1(出边):
当扫描线遇到矩形的 右边界(x=x₂) 时,表示从这个 x 坐标开始,该矩形的 y 区间[y₁, y₂]不再被覆盖。此时需要 -1 表示移除覆盖。
2. 覆盖次数的维护
线段树的每个节点维护两个关键信息:
-
cnt:当前 y 区间 被完全覆盖的次数(直接由1和-1更新)。 -
len:当前 y 区间 实际被覆盖的总长度(通过cnt计算)。
操作逻辑:
-
遇到入边(+1):
-
cnt += 1→ 如果cnt > 0,说明该区间被至少一个矩形覆盖,len直接取区间长度。
-
-
遇到出边(-1):
-
cnt -= 1→ 如果cnt == 0,说明该区间不再被覆盖,len由子区间的覆盖情况决定。
-
3. 具体例子
假设有一个矩形 (x₁=2, y₁=3, x₂=5, y₂=6),扫描线会生成两条边:
line[1] = {2, 3, 6, 1}; // 左边界(入边) line[2] = {5, 3, 6, -1}; // 右边界(出边)
扫描过程:
-
扫描到
x=2(入边):-
更新 y 区间
[3, 6]的cnt += 1 -
此时
cnt=1→len = 6-3 = 3(区间被覆盖)
-
-
扫描到
x=5(出边):-
更新 y 区间
[3, 6]的cnt -= 1 -
此时
cnt=0→len = 0(区间不再被覆盖)
-
4. 为什么这样设计?
-
高效维护覆盖状态:
通过+1和-1的差分操作,可以 动态维护当前 x 位置下 y 轴各区间的覆盖次数,无需遍历所有矩形。 -
支持重叠区域:
如果多个矩形的 y 区间重叠,cnt会累加。例如:-
两个矩形覆盖同一 y 区间 →
cnt=2 -
退出一个矩形后 →
cnt=1(仍被覆盖,不影响len计算)
-
-
面积计算的正确性:
相邻扫描线之间的面积 =当前总覆盖长度 (t[1].len) × (x₂ - x₁),1和-1保证了len的准确更新。
5. 对比其他标记方法
| 标记值 | 含义 | 作用 |
|---|---|---|
1 |
入边(开始覆盖) | 增加 y 区间的覆盖次数 |
-1 |
出边(结束覆盖) | 减少 y 区间的覆盖次数 |
0 |
无效 | 不用于扫描线算法(会丢失覆盖信息) |
总结
-
1和-1是差分思想的体现,高效维护区间覆盖状态。 -
入边 (+1):开始覆盖某个 y 区间。
-
出边 (-1):结束覆盖某个 y 区间。
-
线段树通过
cnt和len的配合,最终计算出 所有矩形的并集面积。
这些表达式中的 -ys 和 -ys - 1 是 指针/迭代器算术运算 的关键部分,用于将 迭代器位置转换为数组下标。下面我会详细解释它们的意义和计算逻辑:
1. unique(ys + 1, ys + cnt_y + 1) - ys - 1
作用:
计算 去重后 ys 数组的实际有效长度(即离散化后的不同 y 坐标数量)。
分步解析:
-
unique(ys + 1, ys + cnt_y + 1)-
unique函数将ys[1..cnt_y]范围内的 相邻重复元素移到末尾,并返回 指向去重后末尾的下一个位置的指针。 -
例如,假设
ys = [0, 100, 100, 150, 200, 200, 200](cnt_y=6),去重后可能变为[0, 100, 150, 200, x, x, x],unique返回指向第一个x的指针。
-
-
- ys-
用
unique返回的指针 减去数组首地址ys,得到的是 去重后的元素个数(包含无效部分)。 -
例如,若
unique返回ys + 4,则ys + 4 - ys = 4(表示前4个元素有效)。
-
-
- 1-
因为
ys的下标从1开始,所以需要 减去1 来得到 实际存储的有效坐标数量。 -
例如,
unique返回ys + 4→4 - 1 = 3,表示ys[1..3]是去重后的坐标。
-
最终结果:
-
cnt_y = unique(...) - ys - 1的值是 去重后ys数组的有效长度(即离散化后的不同 y 坐标数量)。
2. lower_bound(ys + 1, ys + cnt_y + 1, val) - ys
作用:
在离散化后的 ys 数组中 二分查找 val 的下标(从 1 开始)。
分步解析:
-
lower_bound(ys + 1, ys + cnt_y + 1, val)-
在
ys[1..cnt_y]范围内查找 第一个大于等于val的元素的指针。 -
例如,
ys = [0, 100, 150, 200],val=150时返回指向ys[3]的指针。
-
-
- ys-
用
lower_bound返回的指针 减去数组首地址ys,得到的是val在ys中的下标(1-based)。 -
例如,返回
ys + 3→ys + 3 - ys = 3(表示val是ys[3])。
-
用于 y1 的查找:
-
y1 = lower_bound(...) - ys直接得到line[i].y1在ys中的下标(从1开始)。
3. lower_bound(ys + 1, ys + cnt_y + 1, val) - ys - 1
作用:
在离散化后的 ys 数组中 二分查找 val 的前一个位置的下标,用于 线段树区间右端点。
为什么需要 -1?
-
线段树的每个叶子节点表示的是一个 区间
[ys[i], ys[i+1]],而不是单个点。 -
对于矩形的
y2,我们需要找到 覆盖的区间右边界,即ys[i]对应的线段树区间是i-1。 -
例如:
-
ys = [0, 100, 150, 200](下标1..3) -
val = 150时:-
lower_bound返回ys + 2(值150) -
- ys得到2 -
-1后得到1,对应线段树管理的区间[ys[1], ys[2]] = [100, 150]
-
-
用于 y2 的查找:
-
y2 = lower_bound(...) - ys - 1得到line[i].y2对应的 线段树区间右端点下标。
总结
| 表达式 | 作用 | 示例(假设 ys = [0, 100, 150, 200]) |
|---|---|---|
unique(...) - ys - 1 |
计算去重后的有效长度 | unique 返回 ys+4 → 4-1=3(有效 ys[1..3]) |
lower_bound(...) - ys |
查找值对应的下标(1-based) | val=150 → 3 |
lower_bound(...) - ys - 1 |
查找线段树区间右端点 | val=150 → 2(对应区间 [ys[2], ys[3]]) |
关键点
-
-ys:将指针转换为数组下标(1-based)。 -
-1:-
在
unique中:调整下标从1开始的计数。 -
在
lower_bound中:将点坐标转换为线段树管理的区间下标。
-
-
线段树区间:每个叶子节点管理的是
[ys[i], ys[i+1]],所以y2需要-1。
这样设计保证了离散化后的坐标能正确映射到线段树的区间上,从而高效计算覆盖面积。

浙公网安备 33010602011771号