【模板】扫描线
【模板】扫描线
题目描述
求 个四边平行于坐标轴的矩形的面积并。
输入格式
第一行一个正整数 。
接下来 行每行四个非负整数 ,表示一个矩形的四个端点坐标为 。
输出格式
一行一个正整数,表示 个矩形的并集覆盖的总面积。
样例 #1
样例输入 #1
2
100 100 200 200
150 150 250 255
样例输出 #1
18000
提示
对于 的数据,。
对于 的数据,,,。
解题思路
扫描线经典问题,需要用线段树来进行优化。记录这篇题解主要是介绍常规的懒标记线段树写法,大部分题解给出的都是不下传懒标记的做法,然而一年多过去了我还是理解不了这样做为什么是正确的,所以总是忘掉然后回来复习结果还是看不懂。然后昨天又碰到了扫描线相关的题目,复习的时候看到了这篇题解,给出线段树的另外一种写法,现在是终于知道怎么用线段树求矩形并集的面积了。
还是先介绍一下求矩形并集的面积的问题,就是二维平面上有若干个四边平行于坐标轴的矩形,他们之间可能会有重叠,问这些矩形的并集的面积是多少。
看上去好像是容斥原理的问题,不过这样做的话会相当麻烦。扫描线的思想就是用平行于竖轴的水平线,根据矩形的竖边将整个不规则图形划分为若干个区域,如下图:
通过观察可以知道,两条竖线 和 之间有若干个规则的矩形,这些矩形的长都是 ,宽的总和记作 。要计算 个矩形并集的面积,等价于分别计算 条竖线间的矩形的面积再求和,即 。
问题是如何得到每个 ?我们用一个数组来记录竖轴上每个位置(线段)被覆盖的次数(当然由于题目中 的值域很大,需要离散化),那么有被覆盖的位置的总长度就是 了。
假设矩形的位置用左下角 和右上角 来表示,那么用四元组 和 来表示矩形的两条竖边。以第一个参数 为关键字对四元组从小到大排序,那么就会得到上图中从左到右的 条竖线。依次遍历每条竖线,如果第四个参数是 则在数组中把 的位置覆盖一次,否则是 则把 的位置删除一次覆盖。这样就可以维护出每个 ,不过在考虑对纵坐标离散化后这种暴力做法的时间复杂度是 。
由于涉及到区间加的问题,因此可以考虑用线段树来维护。这里就不给出大部分题解用到的做法了,下面介绍用常规懒标记线段树的写法,并不复杂而且更容易理解。
在这个问题中,线段树本质上是维护至少被覆盖一次的线段的数量(总长度),修改的操作只有区间加,并且保证任意修改后不会出现负数。因此线段树节点只需维护如下的信息:
struct Node {
int l, r, mn, s, add;
};
其中 是 这个区间中所有线段被覆盖的次数的最小值。 是被覆盖次数为 的所有线段的长度。 是区间加的懒标记。
对于某个区间中的线段,
- 如果 ,意味着该区间中所有的线段都被覆盖,那么被覆盖的线段总长度就是区间的长度。
- 否则 ,意味着存在某些线段没有被覆盖,那么被覆盖的线段的总长度就是区间的长度减去 。
在修改(维护的区间被修改的区间完全覆盖)和懒标记下传的操作中,如果区间加的值是 ,只需让 ,,而不用改变 ,这是因为对区间的所有线段都覆盖一次后,覆盖次数最小的线段还是之前的那些。
AC 代码如下,时间复杂度为 :
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5 + 10;
struct Seg {
int x, y1, y2, c;
}p[N];
int xs[N], sz;
struct Node {
int l, r, mn, s, add;
}tr[N * 4];
void build(int u, int l, int r) {
tr[u] = {l, r, 0, 0, 0};
if (l == r) {
tr[u].s = xs[r + 1] - xs[l];
}
else {
int mid = l + r >> 1;
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
tr[u].s = tr[u << 1].s + tr[u << 1 | 1].s;
}
}
void pushdown(int u) {
if (tr[u].add) {
tr[u << 1].mn += tr[u].add;
tr[u << 1].add += tr[u].add;
tr[u << 1 | 1].mn += tr[u].add;
tr[u << 1 | 1].add += tr[u].add;
tr[u].add = 0;
}
}
void modify(int u, int l, int r, int c) {
if (tr[u].l >= l && tr[u].r <= r) {
tr[u].mn += c;
tr[u].add += c;
}
else {
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
if (l <= mid) modify(u << 1, l, r, c);
if (r >= mid + 1) modify(u << 1 | 1, l, r, c);
tr[u].mn = min(tr[u << 1].mn, tr[u << 1 | 1].mn);
tr[u].s = 0;
if (tr[u << 1].mn == tr[u].mn) tr[u].s += tr[u << 1].s;
if (tr[u << 1 | 1].mn == tr[u].mn) tr[u].s += tr[u << 1 | 1].s;
}
}
int find(int x) {
int l = 1, r = sz;
while (l < r) {
int mid = l + r >> 1;
if (xs[mid] >= x) r = mid;
else l = mid + 1;
}
return l;
}
int main() {
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
int x1, y1, x2, y2;
scanf("%d %d %d %d", &x1, &y1, &x2, &y2);
p[i << 1] = {x1, y1, y2, 1};
p[i << 1 | 1] = {x2, y1, y2, -1};
xs[++sz] = y1, xs[++sz] = y2;
}
sort(xs + 1, xs + sz + 1);
sz = unique(xs + 1, xs + sz + 1) - xs - 1;
sort(p, p + 2 * n, [&](Seg &a, Seg &b) {
return a.x < b.x;
});
build(1, 1, sz - 1);
LL ret = 0;
for (int i = 0; i < 2 * n - 1; i++) {
modify(1, find(p[i].y1), find(p[i].y2) - 1, p[i].c);
LL len = xs[sz] - xs[1] - !tr[1].mn * tr[1].s;
ret += len * (p[i + 1].x - p[i].x);
}
printf("%lld", ret);
return 0;
}
参考资料
题解 P5490 【【模板】扫描线】:https://www.luogu.com.cn/blog/xuxuxuxuxu/solution-p5490
AcWing 247. 亚特兰蒂斯(算法提高课):https://www.acwing.com/video/653/
本文来自博客园,作者:onlyblues,转载请注明原文链接:https://www.cnblogs.com/onlyblues/p/17836031.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
2022-11-16 E. Cactus Wall