扫描线
算法模型
用于求矩阵面积并或者周长并。
假设给定平面上若干个可能相交的矩阵,求它们的面积并(面积之和减去相交部分)或者周长并(外轮廓的长度)。我们可以虚拟出一条按顺序扫描整个平面的线段,通过对平行或垂直于 \(x\) 轴的线段进行处理得到答案。
时间复杂度是 \(O(nlogn)\)
算法思想
面积并
求 \(n\) 个矩阵的面积并。
按照容斥的思路,我们需要先求出所有矩阵的面积之和,再减去两两相交的部分,再加上被重复减去的部分……很难用代码实现。因此,我们不需要计算出每一个矩阵的面积,而是考虑将给出的矩阵转化,使其更容易处理。
假设我们现在有 \(n\) 个相交的矩阵,如下图(图源网络,感谢 @Gu_Pigeon 的图片解析,侵权自删):
我们可以把这 \(n\) 个矩阵切割成如图的若干个不相交长方形,然后统计长方形的面积。这样处理就不必再考虑相交的面积了。但是求出平面上的 \(n\) 个长方形面积之和也不是很好处理。
假设在这个平面上有一条线段,它平行于 \(x\) 轴,且所有的 \(n\) 个矩阵的左右端点均在这条扫描线的两端点之间。这条扫描线从 \(y\) 坐标最小的矩阵的底边开始自底向上扫描所有的矩阵,并在遇到矩阵边的时候进行处理。扫描过一遍矩阵之后,我们就可以求出面积并。
我们考虑最简单的情况:两个矩阵相交,如下图。假设扫描线遇到横边的时候会停下来,那么我们可以根据矩阵的横边,把 \(n\) 个矩阵的面积分割成若干部分(图源网络,感谢 @NCC79601 的图片解析,侵权自删):
上图中 \(4\) 条横边将 \(2\) 个矩阵的面积分割成了 \(3\) 个部分。我们可以将每条横边的左右端点和高度存储下来,设第 \(i\) 条横边的高度为 \(h_i\),则从下往上数第 \(i\) 个图形的宽即为 \(h_{i + 1} - h_i\) 。于是我们只需要求出每一条扫描线上所有被其覆盖的横边的长度之和再乘以两条横边之间的距离,就可以求出这部分的面积并了。
尝试用一个桶来维护扫描线上所有的 \(x\) 坐标是否被横边覆盖。这样做有很多问题:
-
\(x\) 坐标不一定是非负整数,需要 离散化
-
空间复杂度太高,解决办法是只存储横边左右端点的 \(x\) 坐标离散化后在数组中的下标,需要时直接计算即可
-
时间复杂度太高,需要 线段树 优化
可以把所有垂直于 \(x\) 轴的竖边提取出来,将它们的延长线与 \(x\) 轴的垂足看作是一个点,权值为竖边的 \(x\) 坐标。如上图,我们可以用区间 \([1, 2]\) 来表示第一个被切割出的长方形的横边长度,\([2, 3], [3, 4]\) 同理。由此,平面上的问题就被转化成了区间问题。对于区间 \([l, r]\),我们需要维护:
-
区间的左右端点 \(l, r\)
-
区间是否被整体覆盖
-
区间被横边覆盖的总长度
考虑修改。
当我们遇到一条横边时,它对应的区间已经被它整体覆盖了,其被覆盖的次数加 \(1\)。由于该区间被整体覆盖,所以它被覆盖的长度等于区间的长度。最后我们统计线段树的根结点的长度标记,乘以两条横边之间的高度差即为一部分的总面积。
每次更新信息的时候分类讨论:
-
当前结点被完全覆盖,长度为它的右端点减去左端点
-
否则长度为两个子结点被覆盖的长度之和。
这样做还有问题。每当我们遇到一条横边,我们就需要新建一棵线段树或者将原本的线段树初始化,这样做的时间复杂度太高,所以我们还需要优化,使得我们可以在同一颗棵线段树上统计。
假如我们不清空线段树的信息,那么第 \(1\) 条扫描线到第 \(i - 1\) 条扫描线的信息都会被保存下来。扫描线可能已经遇到了若干条上边,此时应该把它们对应的下边的影响删除。我们发现,因为题目给出的图形是矩阵,所以我们可以给对应的下边和上边分别赋上相反的权值。
因为扫描线是从下往上扫描的,所以我们可以给下边赋上权值 \(1\),给上边赋上权值 \(-1\) 。当我们遇到下边时,说明这个矩阵还会在上面若干个部分出现,所以它对应的区间被覆盖次数加 \(1\) ,之后统计它的影响。当我们遇到一条上边时,之前一定已经统计完了对应下边的影响,这时应该把对应区间的被覆盖次数 \(-1\) ,代表这个图形的面积被统计完毕。
然而最后还有一个带问题:线段树中存在对应长度为 \(1\) 的结点。这样的结点对应的是一个平面上的 \(x\) 坐标。显然一条线段的两个端点如果重合,它就等价于一个单点。这种叶结点会维护错误。
解决办法是令线段树中原本代表区间 \([l, r]\) 的结点代表区间 \([l, r + 1]\) 。这样最小规模的结点也至少保存了原本的一条切割后的横边。
于是我们可以推出扫描线算法的整体流程:
-
用结构体数组保存横边的左右端点、高度和上下边对应的不同权值,并将它们按高度从小到大排序。这样每次就可以先遍历到下边,保证了过程中产生的答案非负
-
将每条横边的左右端点对应的 \(x\) 坐标保存下来并离散化
-
扫描到第 \(i\) 条横边时就在线段树上维护第 \(i\) 条横边带来的影响:假设这条横边的对应的区间为 \([l, r]\),线段树上对应区间 \([l, r]\) 的结点被覆盖次数加上横边的权值,长度为区间本身的长度
-
每次询问扫描线覆盖的横边长度之和,答案加上其乘以两条横边之间的高度差的乘积
最后,扫描线算法还有一些需要注意的地方。当更新区间 \([l, r]\) 时,如果当前结点的左端点 \(\geq r\) 或者右端点 \(\leq l\),说明操作不合法。以及线段树维护的时候并不需要 push_down
。详见代码。
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 1e5 + 5;
struct node {
int l, r, cnt;
long long len;
} tree[4 * maxn];
struct Line {
int flag;
long long l, r, h;
Line(): l(), r(), h(), flag() {}
Line(long long _l, long long _r, long long _h, int _flag): l(_l), r(_r), h(_h), flag(_flag) {}
bool operator < (const Line& rhs) const {
return h < rhs.h;
}
} line[2 * maxn];
int n, m;
long long x1, y1, x2, y2;
long long x[2 * maxn];
inline long long read() {
long long res = 0, flag = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') {
flag = -1;
}
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
res = res * 10 + ch - '0';
ch = getchar();
}
return res * flag;
}
void push_up(int k) {
if (tree[k].cnt) {
tree[k].len = (x[tree[k].r + 1] - x[tree[k].l]);
} else {
tree[k].len = tree[2 * k].len + tree[2 * k + 1].len;
}
}
void build(int k, int l, int r) {
tree[k].l = l;
tree[k].r = r;
if (l == r) {
tree[k].len = tree[k].cnt = 0;
return;
}
int mid = (l + r) / 2;
build(2 * k, l, mid);
build(2 * k + 1, mid + 1, r);
push_up(k);
}
void update(int k, int l, int r, int w) {
if (x[tree[k].r + 1] <= l || x[tree[k].l] >= r) {
return;
}
if (x[tree[k].l] >= l && x[tree[k].r + 1] <= r) {
tree[k].cnt += w;
push_up(k);
return;
}
int mid = (tree[k].l + tree[k].r) / 2;
update(2 * k, l, r, w);
update(2 * k + 1, l, r, w);
push_up(k);
}
int main() {
long long ans = 0;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
x1 = read(), y1 = read();
x2 = read(), y2 = read();
x[2 * i - 1] = x1;
x[2 * i] = x2;
line[2 * i - 1] = Line(x1, x2, y1, 1);
line[2 * i] = Line(x1, x2, y2, -1);
}
n <<= 1;
sort(line + 1, line + n + 1);
sort(x + 1, x + n + 1);
m = unique(x + 1, x + n + 1) - x - 1;
build(1, 1, m - 1);
for (int i = 1; i < n; i++) {
update(1, line[i].l, line[i].r, line[i].flag);
ans += (tree[1].len * (line[i + 1].h - line[i].h));
}
printf("%lld\n", ans);
return 0;
}
周长并
求 \(n\) 个矩阵的周长并。
可以模仿面积并的做法,得到第一种做法:
先将所有的横边按 \(y\) 坐标从小到大排序,设在第 \(i - 1\) 条横边加入线段树后的横边总长为 \(p\) 。
加入第 \(i\) 条边后:
-
如果总长不变,说明当前横边已经被其他横边覆盖,故不计入答案。
-
如果总长改变且当前横边为下边,说明当前横边部分或全部未被覆盖,答案增加两次总长的差
-
若总长改变且当前横边为上边,说明当前横边在抵消了其他下边影响的同时还多出了一部分,加上两次总长的差的绝对值。对于纵边同理。
这样做码量巨大,考虑在线段树上多维护一些。我们考虑在线段树上维护以下信息:
-
区间的左右端点
-
区间的左右端点是否被横边覆盖
-
覆盖此区间的横边数量,若两条横边相连,视为一条横边,例如 \([1, 2]\) 和 \([3, 4]\) ,看作是横边 \([1, 4]\)
-
区间被横边覆盖的长度
-
区间被完整覆盖的次数
对于横边的处理方式和第一种方法一样。
对于纵边,我们可以发现一条横边的左右端点应该各连接着一条纵边,因为如果一条纵边与横边的垂足在横边中间,这条纵边应该不计入周长并。所以我们需要统计整条扫描线上完整横边的数量,也就是覆盖区间 \([1, n]\) 的横边数量。
把矩阵分成若干份,则这些横边连出的纵边在这部分内的长度应该为两条横边的高度之差。所以纵边的长度之和应该等于 当前区间 \([1, n]\) 被横边覆盖的次数 \(\times\) 两条横边的高度之差。
因为在维护线段树时没有处理最后一条横边,所以我们最后还需要给答案加上最后一条横边的长度。
最后,因为如果有两条高度相同且部分重合的上边和下边,如果先计算上边会导致这条线段会被重复计算。因此如果两条线段高度相同,按照下边大于上边的优先级计算。
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 1e5 + 5;
struct node {
int l, r;
int len, cov, cnt;
bool lc, rc;
} tree[maxn << 2];
struct Line {
int l, r, h, flag;
Line(): l(), r(), h(), flag() {}
Line(int _l, int _r, int _h, int _flag): l(_l), r(_r), h(_h), flag(_flag) {}
bool operator < (const Line& rhs) const {
if (h == rhs.h) {
return flag > rhs.flag;
}
return h < rhs.h;
}
} line[maxn << 1];
int n;
int x[maxn << 1];
inline int read() {
int res = 0, flag = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') {
flag = -1;
}
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
res = res * 10 + ch - '0';
ch = getchar();
}
return res * flag;
}
int calc(int x, int y) {
return max(x, y) - min(x, y);
}
void push_up(int k) {
if (tree[k].cov) {
tree[k].len = x[tree[k].r + 1] - x[tree[k].l];
tree[k].lc = tree[k].rc = true;
tree[k].cnt = 1;
} else {
tree[k].len = tree[2 * k].len + tree[2 * k + 1].len;
tree[k].lc = tree[2 * k].lc;
tree[k].rc = tree[2 * k + 1].rc;
tree[k].cnt = tree[2 * k].cnt + tree[2 * k + 1].cnt;
if (tree[2 * k].rc && tree[2 * k + 1].lc) {
tree[k].cnt--;
}
}
}
void build(int k, int l, int r) {
tree[k].l = l;
tree[k].r = r;
if (l == r) {
tree[k].cnt = tree[k].cov = tree[k].len = 0;
tree[k].lc = tree[k].rc = 0;
return;
}
int mid = (l + r) / 2;
build(2 * k, l, mid);
build(2 * k + 1, mid + 1, r);
push_up(k);
}
void update(int k, int l, int r, int w) {
if (x[tree[k].r + 1] <= l || x[tree[k].l] >= r) {
return;
}
if (x[tree[k].l] >= l && x[tree[k].r + 1] <= r) {
tree[k].cov += w;
push_up(k);
return;
}
update(2 * k, l, r, w);
update(2 * k + 1, l, r, w);
push_up(k);
}
int main() {
int x1, y1, x2, y2;
int ans = 0, p = 0;
n = read();
for (int i = 1; i <= n; i++) {
x1 = read(), y1 = read();
x2 = read(), y2 = read();
x[2 * i - 1] = x1;
x[2 * i] = x2;
line[2 * i - 1] = Line(x1, x2, y1, 1);
line[2 * i] = Line(x1, x2, y2, -1);
}
n <<= 1;
sort(line + 1, line + n + 1);
sort(x + 1, x + n + 1);
int m = unique(x + 1, x + n + 1) - x - 1;
build(1, 1, m - 1);
for (int i = 1; i < n; i++) {
update(1, line[i].l, line[i].r, line[i].flag);
ans += (calc(tree[1].len, p));
p = tree[1].len;
ans += 2 * (tree[1].cnt * (line[i + 1].h - line[i].h));
}
ans += (line[n].r - line[n].l);
printf("%d\n", ans);
return 0;
}