【学习笔记】扫描线

扫描线是用来求解图形面积并的一个算法。

问题引入

给定 \(n\) 个长方形,求它们的面积并。下面以两个长方形为例:

对于这个问题,可以有容斥等做法,但是还有个更简单的方法——扫描线。

扫描线

扫描线,顾名思义,就是,拿一条“线”取扫(这里是从下往上扫,其实其它的扫的方式也是可以的):

如图所示,这几个图形被四条线分成了 \(3\) 个部分,我们可以对每一个部分的面积计算并累加起来。由于这些部分都是长方形,而这些长方形的宽很好计算,就是线扫过的距离(图中橙色箭头所标),而要求的就是长方形的长。

把每个矩阵的下面的边的值记作 \(1\),上面的边值记作 \(-1\),每次经过值为 \(1\) 条边就把对应边加进来,否则删去,那么现在长方形的长就是已加的边组成的边的长度(注意不是所有已加边的长度总和)。

暴力显然是不行的,考虑用数据结构来优化。

线段树优化扫描线

观察到,根据矩形的“横边”,可以把整个图形分为 \(3\) 个部分:

可以建一棵线段树来维护这三个部分。

具体的,线段树的每个节点都对应其的一部分,比如节点 \(2\) 对应的就是第一、二部分,节点 \(6\) 对应的就是第三部分。每个节点又维护了两个信息:第一是当前节点被覆盖的次数,第二是当前节点所对应的长方形的长。而长方形的长就是根节点所维护的第二个信息。

为什么要这么维护呢?第二个原因显然,而第一个如果对于每个 \(+1-1\) 的修改,都访问到叶子节点,那么复杂度就爆了,所以在把目标区间拆分成一个个小区间时,就修改区间对应的节点即可(其实有些类似懒标记)。

对应的,线段树要求能从子节点推到父节点,也就是 pushup 操作如下:

int X[N];//X[i] 表示从左往右第 i 条竖边
int tsum[N << 3], tlen[N << 3];//tsum 表示信息一,tlen 表示信息二
void pushup(int k, int l, int r) {//tsum 是由 +1-1 决定的,所以只更新 tlen
	if (tsum[k]) tlen[k] = X[r + 1] - X[l];//若整个区间都被访问过了则 tlen 为对应的区间长度
	else tlen[k] = tlen[k << 1] + tlen[k << 1 | 1];//否则为左右节点的长方形长之和
}

注意,在 pushup 中,包括之后的所有操作,节点 \(k\) 在线段树中维护的区间为 \([l,r]\),实际的区间为 \([X_l,X_{r+1}]\)(其实这个操作就是离散化),\(r\) 要加一是因为否则叶子节点维护的就是一个点了。

接下来是 updata。其实就是对每一条线段对应的区间进行更新,不过要注意边界的问题(其实可以这么想,区间对应的 \(l,r\) 是线段左端点的范围):

void updata(int k, int l, int r, int L, int R, int v) {
    //注意区分这里的 L, R 指的是实际的区间而不是 X 的下标
    //而 l, r 是指在 X 中的下标范围
	if (L <= X[l] && X[r + 1] <= R) {//在区间内就直接更新 tsum
		tsum[k] += v;
		return pushup(k, l, r);//注意这里也要更新 tlen 因为 tlen 也是受 tsum 影响的
	}
	int m = l + ((r - l) >> 1);
	if (L < X[m + 1]) updata(k << 1, l, m, L, R, v);
	//这里和一般的线段树不一样,比如要更新 [1,3],并不用更新 [3,4] 这个区间,所以不取等号
	//还有一点要时刻注意区间 [l,m] 对应 X 的范围是 [l,m+1]
	if (R > X[m + 1]) updata(k << 1 | 1, m + 1, r, L, R, v); 
	pushup(k, l, r);
}

具体的过程还是以上面的例子为例(其中蓝色表示当前更新的边,粉色表示当前长方形的宽,标在节点中的两个数表示 \(tsum\)\(tlen\)):

然后是删边的两个操作:

PS:其实在实际过程中可以不用更新最后一条边因为更新了也不会被统计到了。

最后是完整的代码(洛谷 P5490 【模板】扫描线),还是要注意边界的问题:

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5 + 5;//两条边所以开两倍
struct node {
	int l, r, h, v;
}Y[N];
int X[N];
bool cmp(node x, node y) {//从下到上排序
	return x.h < y.h;
}
int tsum[N << 3], tlen[N << 3]; //这里有个问题就是在 pushup 访问时可能会越界就多开一点
void pushup(int k, int l, int r) {
	if (tsum[k]) tlen[k] = X[r + 1] - X[l];
	else tlen[k] = tlen[k << 1] + tlen[k << 1 | 1];
}
void updata(int k, int l, int r, int L, int R, int v) {
	if (L <= X[l] && X[r + 1] <= R) {
		tsum[k] += v;
		pushup(k, l, r);
		return;
	}
	int m = l + ((r - l) >> 1);
	if (L < X[m + 1]) updata(k << 1, l, m, L, R, v);
	if (R > X[m + 1]) updata(k << 1 | 1, m + 1, r, L, R, v); 
	pushup(k, l, r);
}
signed main() {
	int n;
	cin >> n;
	for (int i = 1; i <= n; i++) {
		int x1, y1, x2, y2;
		cin >> x1 >> y1 >> x2 >> y2;//注意输入的是先纵坐标后横坐标
		X[2 * i - 1] = x1, X[2 * i] = x2;
		Y[2 * i - 1] = {x1, x2, y1, 1}, Y[2 * i] = {x1, x2, y2, -1};
	}
	sort(Y + 1, Y + 1 + 2 * n, cmp);
	sort(X + 1, X + 1 + 2 * n);
	int len = unique(X + 1, X + 1 + 2 * n) - X - 1, ans = 0;//去重
	for (int i = 1; i < 2 * n; i++) {
		updata(1, 1, len - 1, Y[i].l, Y[i].r, Y[i].v);//还是注意是 len-1 因为对应的是“左端点”
		ans += tlen[1] * (Y[i + 1].h - Y[i].h);//宽度是下一条边高度减当前边高度
	}
	cout << ans;
	return 0;
}

热门相关:倾心之恋:总裁的妻子   宫合2020   闺范   买妻种田:山野夫君,强势宠!   不科学御兽