扫描线

\(\text{luogu-5490}\)

\(n\) 个四边平行于坐标轴的矩形的面积并。

\(1 \le n \le {10}^5\)\(0 \le x_1 < x_2 \le {10}^9\)\(0 \le y_1 < y_2 \le {10}^9\)


扫描线可以用于快速计算多个矩形的面积并。扫描线的运行过程和名称类似,即一根线沿一个方向扫描所有图形,如下图。

可以发现,在扫描时我们将一个不那么规则的区域,切割成了互不相交的若干个矩形。这若干个矩形的面积之和就是所求的答案。

如何计算切割出的单个矩形面积呢?我们需要知道矩形的底和高。矩形的高很好求,即为排序后相邻两横线间的距离。那么如何求矩形的高呢?

想要维护矩形的高需要从扫描线的运行方式入手。每次扫到一根横线,我们或将这根横线加入,或将这根横线删除。

可以将一根横坐标在 \(l\sim r\) 范围内的的横线拆成 \(l\sim l+1,l+2\sim l+3,\dots,r-1\sim r\)\(r-l\) 根长度为 \(1\) 的横线。容易想到,可以对所有长度为 \(1\) 的横线都开一个桶来储存这根横线的数量。储存值大于 \(0\) 的桶的数量即为当前矩形的高。

这样,将横线加入,就是把这根横线范围内所有桶的储存值 \(+1\),将横线删除,就是把这根横线范围内所有桶的储存值 \(-1\)

无论是加入横线还是删除横线,每次的操作都是对一片区域内的桶 \(+1\)\(-1\),最终查询的是储存值大于 \(0\) 的桶的数量。发现这种区间操作可以利用线段树来快速维护。

建树的过程和一般的线段树是一样的,这里不过多讲述。

线段树的每个节点,都维护一段区间内的桶,所以每个节点需要记录的信息有:

  1. l,r:维护的区间,即左端点的右端点。
  2. w:这个区间被完整覆盖几次。
  3. x:这个区间内被覆盖过的桶的数量,即这个区间内被覆盖的长度。

所以在向下递归节点时,如果当前节点被操作区间完全覆盖,则直接对 cnt 进行修改;否则,如果操作区间和左儿子维护区间相交,则对左儿子递归,如果操作区间和右儿子维护区间相交,则对右儿子递归。

这些处理完之后,需要更新这个节点区间内被覆盖的长度 len

void upd(ll p) {
	if(t[p].w) t[p].x = h[t[p].r + 1] - h[t[p].l];
	else if(t[p].l == t[p].r) t[p].x = 0;
	else t[p].x = t[p << 1].x + t[p << 1 | 1].x;
	return;
}

void update(ll p, ll l, ll r, ll x) {
	if(l <= t[p].l && t[p].r <= r) { t[p].w += x, upd(p); return; }
	if(l <= t[p << 1].r) update(p << 1, l, r, x);
	if(t[p << 1 | 1].l <= r) update(p << 1 | 1, l, r, x);
	upd(p); return; 
}

和一般的线段树不同,我们不需要下传标记。因为加入一根横线,在之后必定会删除一根完全相同的横线。删除时会和加入时递归到同一个节点,并直接对这个节点修改。

由于根节点维护的是整个区间,所以查询根节点上储存的 len 即可知道当前被覆盖的桶的数量。

横坐标的范围较大,但可能出现的横坐标值的数量很少,我们可以对横坐标进行离散化处理。即每个桶记录的是一段区间,而不一定是一个长度为 \(1\) 的横线。

由于每个矩形会产生两个横坐标值,所以离散化后横坐标区间为 \(0\sim 2n\)。所以要开 \(2n\) 个桶,即线段树有 \(2n\) 个叶子节点。计算得出线段树需要不超过 \(4n\) 个节点,储存线段树的数组开到 \(4n\) 就够了,但需要注意在更新叶子节点时不要去查找它的子节点。

和一般的线段树类似,在递归时线段树的每一层最多只有 \(3\) 个节点被递归到,而线段树的层数约为 \(\log n\),故单次操作时间复杂度为 \(O(\log n)\)。共有 \(n\) 个矩形,每个矩形有 \(2n\) 个横线,故需要在线段树上操作 \(2n\) 次。忽略常数,总时间复杂度为 \(O(n\log n)\)

#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
#define ll long long
#define MAXN 200005

ll read() {
	ll x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

struct tree { ll l, r, w, x; } t[MAXN << 2];
struct node { ll l, r, y, tg; }; 
vector<node> lin; 
vector<ll> h;
ll n;

bool cmp(node pl, node pr) { return pl.y < pr.y; }

void build(ll p, ll l, ll r) {
	t[p] = {l, r, 0, 0};
	if(l == r) return;
	ll mid = (l + r) >> 1;
	build(p << 1, l, mid);
	build(p << 1 | 1, mid + 1, r);
	return;
}

void upd(ll p) {
	if(t[p].w) t[p].x = h[t[p].r + 1] - h[t[p].l];
	else if(t[p].l == t[p].r) t[p].x = 0;
	else t[p].x = t[p << 1].x + t[p << 1 | 1].x;
	return;
}

void update(ll p, ll l, ll r, ll x) {
	if(l <= t[p].l && t[p].r <= r) { t[p].w += x, upd(p); return; }
	if(l <= t[p << 1].r) update(p << 1, l, r, x);
	if(t[p << 1 | 1].l <= r) update(p << 1 | 1, l, r, x);
	upd(p); return; 
}

int main() {
	n = read();
	for(int i = 1; i <= n; i ++) {
		ll xl = read(), yl = read(), xr = read(), yr = read();
		h.push_back(xl), h.push_back(xr);
		lin.push_back({xl, xr, yl, 1});
		lin.push_back({xl, xr, yr, -1});
	}
	sort(h.begin(), h.end());
	sort(lin.begin(), lin.end(), cmp);
	h.erase(unique(h.begin(), h.end()), h.end());
	build(1, 0, h.size() - 2); ll res = 0;
	for(int i = 0; i + 1 < lin.size(); i ++) {
		ll pl = lower_bound(h.begin(), h.end(), lin[i].l) - h.begin();
		ll pr = lower_bound(h.begin(), h.end(), lin[i].r) - h.begin();
		update(1, pl, pr - 1, lin[i].tg);
		res += t[1].x * (lin[i + 1].y - lin[i].y);
	}
	cout << res << "\n";
	return 0;
}

\(\text{hdu-1542}\)

古希腊的几篇古籍里都提到了传说中的亚特兰蒂斯岛。有些书里甚至附带了岛上部分区域的地图。但遗憾的是,这些地图描绘的都是岛上不同的区域。你的朋友比尔想知道这些地图覆盖的总面积。你(不幸地)自告奋勇写个程序来算算这个总面积。

\(1 \le n \le 100\)\(0 \le x_1 < x_2 \le 10^5\)\(0 \le y_1 < y_2 \le 10^5\)


和 luogu-5490 一摸一样,只不过有小数和多测。

#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
#define ll long long
#define MAXN 200005

ll read() {
	ll x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

struct tree { ll l, r, w; double x; } t[MAXN << 2];
struct node { double l, r, y; ll tg; }; 
vector<node> lin; 
vector<double> h;
ll n, cnt;

bool cmp(node pl, node pr) { return pl.y < pr.y; }

void build(ll p, ll l, ll r) {
	t[p] = {l, r, 0, 0};
	if(l == r) return;
	ll mid = (l + r) >> 1;
	build(p << 1, l, mid);
	build(p << 1 | 1, mid + 1, r);
	return;
}

void upd(ll p) {
	if(t[p].w) t[p].x = h[t[p].r + 1] - h[t[p].l];
	else if(t[p].l == t[p].r) t[p].x = 0;
	else t[p].x = t[p << 1].x + t[p << 1 | 1].x;
	return;
}

void update(ll p, ll l, ll r, ll x) {
	if(l <= t[p].l && t[p].r <= r) { t[p].w += x, upd(p); return; }
	if(l <= t[p << 1].r) update(p << 1, l, r, x);
	if(t[p << 1 | 1].l <= r) update(p << 1 | 1, l, r, x);
	upd(p); return; 
}

int main() {
	while(cin >> n) {
		if(!n) break; lin.clear(), h.clear();
		for(int i = 1; i <= n; i ++) {
			double xl, xr, yl, yr;
			cin >> xl >> yl >> xr >> yr;
			h.push_back(xl), h.push_back(xr);
			lin.push_back({xl, xr, yl, 1});
			lin.push_back({xl, xr, yr, -1});
		}
		sort(h.begin(), h.end());
		sort(lin.begin(), lin.end(), cmp);
		h.erase(unique(h.begin(), h.end()), h.end());
		build(1, 0, h.size() - 2); double res = 0;
		for(int i = 0; i + 1 < lin.size(); i ++) {
			ll pl = lower_bound(h.begin(), h.end(), lin[i].l) - h.begin();
			ll pr = lower_bound(h.begin(), h.end(), lin[i].r) - h.begin();
			update(1, pl, pr - 1, lin[i].tg);
			res += t[1].x * (lin[i + 1].y - lin[i].y);
		}
		printf("Test case #%lld\nTotal explored area: %.2lf\n\n", (++ cnt), res);
	}
	return 0;
}
posted @ 2026-03-15 20:00  So_noSlack  阅读(4)  评论(0)    收藏  举报