扫描线

1 概念

扫描线一般运用在图形上面,其含义就是拿一条线在图形上扫,然后维护信息得到答案。

通常情况下,扫描线被用于求面积、周长问题。

2 矩形面积并

2.1 思路

先给出例题:【模板】扫描线

考虑我们所学的求不规则图形面积的方式,无外乎割补。然而补似乎太难实现,因此考虑割。

如下图:

那么我们考虑怎么割。显然如果遇到一条横向的线段(纵向同理),那么就意味着有一块矩形。那么我们让这根扫描线从下往上扫,碰到横向线段停下来维护信息。如下:

考虑我们此时要记录什么,就是当前扫到的线段长度。因此我们要现将所有线段的左右端点存下来,然后排序:

现在整个 x 轴就被分为了五部分,考虑中间的三部分。我们发现扫描线的关键就在于维护中间三段出现了多少段,将他乘上与下一条线段的高度差就可以得到当前矩形的面积。

先考虑如何处理,我们可以将一个矩形看做有上边和下边。显然在到达上边时线段会出现一次,而离开后就会减掉一次。那么我们可以给线段附上权值,即下边为 1,上边为 1

那么现在回顾我们上面要维护的东西。我们知道,现在要维护中间的区间段,要在碰到线段时给对应的区间段增减出现次数,最后求出总和。

看不懂啊,我们改写一下:维护一个区间,支持区间修改,区间查询。这一下就看懂了,使用线段树。

如下图所示:

在线段树上,我们维护两个信息:当前节点所代表的区间段出现次数,当前节点所代表的区间段总共出现的区间段长度,记为 flg,cnt。那么当 flg0 时,cnt 就是当前区间段的长度,否则 cnt 就是左右子树的 cnt 之和。由于我们总是先增后减,因此 flg 一定非负。

同时,我们其实并不需要求出所有区间的和,只需要知道整个区间的和,因此无需懒标记以及下放。

然而又有一个问题,我们的线段树实际上维护的是中间的区间段,因此对于区间段 [l,r],它的端点应该是 [xl,xr+1]

最后的最后,我们每种 x 我们只需要一个,因此要去重。

剩下的看代码吧。

2.2 代码

#include <bits/stdc++.h>
#define int long long

using namespace std;

typedef long long LL;
const int Maxn = 1e6 + 5e5 + 5;

int n;

struct segment {//定义线段
	int l, r, h;//左右端点和 y 轴上的值
	int flg;//权值(±1)
	bool operator < (const segment &b) const {//按 y 坐标排序
		if(h == b.h) {//注意细节:
			/*
			当两条线段重合的时候,应当先增后减,这样才能让线段树上的值非负。
			*/
			return flg > b.flg;
		}
		return h < b.h;
	}
}seg[Maxn];

int x[Maxn];

struct seg_tree {//普通线段树
	struct node {
		int l, r, flg;
		int len;
	}t[Maxn];
	#define lp (p << 1) 
	#define rp (p << 1 | 1)
	void build(int p, int l, int r) {//建树
		t[p] = {l, r, 0, 0};
		if(l == r) {
			return ;
		}
		int mid = (l + r) >> 1;
		build(lp, l, mid);
		build(rp, mid + 1, r);
	}
	void pushup(int p) {
		if(t[p].flg) {//这条区间段出现过
			t[p].len = x[t[p].r + 1] - x[t[p].l];//就是对应的长度
		}
		else {
			t[p].len = t[lp].len + t[rp].len;//左右子树相加
		}
	}
	void mdf(int p, int l, int r, int val) {//查询
		if(x[t[p].r + 1] <= l || r <= x[t[p].l]) {//超出范围
			/*
			用 <= 而非 < 的原因就是 l, r, x 实际上都是区间的端点,端点重合是不可能查询到的
			*/
			return ;
		}
		if(l <= x[t[p].l] && x[t[p].r + 1] <= r) {//区间包含
			t[p].flg += val;
			pushup(p);
			return ;
		}
		mdf(lp, l, r, val);
		mdf(rp, l, r, val);
		pushup(p);
	}
}SEG;

signed main() {
	ios::sync_with_stdio(0);
	cin >> n;
	for(int i = 1; i <= n; i++) {
		int a, b, c, d;
		cin >> a >> b >> c >> d;
		x[2 * i - 1] = a;
		x[2 * i] = c;//存储所有 x 值
		seg[2 * i - 1] = {a, c, b, 1};
		seg[2 * i] = {a, c, d, -1};//赋权值
	}
	n <<= 1;
	sort(seg + 1, seg + n + 1);//按 y 坐标排序,满足扫描线概念
	sort(x + 1, x + n + 1);
	int cnt = unique(x + 1, x + n + 1) - x - 1;//去重
	SEG.build(1, 1, cnt - 1);//建树
	/*
	使用 cnt - 1 的原因就是 [l,r] 的右端点是 x[t[p].r + 1]
	*/
	int ans = 0;
	for(int i = 1; i <= n; i++) {
		SEG.mdf(1, seg[i].l, seg[i].r, seg[i].flg);//区间修改
		ans += SEG.t[1].len * (seg[i + 1].h - seg[i].h);//线段总长度乘上高度
	}
	cout << ans;
	return 0;
}

3 矩形周长并

例题:[IOI1998] Picture

其实有了上面的学习,周长并就不难了。

首先显然考虑分成两部分:从下往上和从左往右。接下来考虑贡献,我们依然维护线段总长,然后考虑分类:

  • 如果是增线段,那么由它而增加的部分是新增的周长
  • 如果是减线段,那么由它而减去的部分是新增的周长

因此,无论是增线段还是减线段,对于周长做出的贡献就是对于线段总长的改变(注意这个改变指的是非负的)。

按照这个维护一下即可,代码如下:

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
const int Maxn = 5e5 + 5;

int n;

int x[Maxn], y[Maxn];

struct segment {
	int l, r, h, flg;
	bool operator < (const segment &b) const {
		if(h == b.h) {
			return flg > b.flg;
		}
		return h < b.h;
	}
}segx[Maxn], segy[Maxn];

struct segtree {
	struct node {
		int l, r, flg, len;
	}t[Maxn];
	#define lp (p << 1)
	#define rp (p << 1 | 1)
	void build(int p, int l, int r) {
		t[p] = {l, r, 0, 0};
		if(l == r) {
			return ;
		}
		int mid = (l + r) >> 1;
		build(lp, l, mid);
		build(rp, mid + 1, r);
	}
	void pushup(int p, int typ) {
		if(t[p].flg) {
			if(typ == 0) t[p].len = x[t[p].r + 1] - x[t[p].l];
			if(typ == 1) t[p].len = y[t[p].r + 1] - y[t[p].l];
		}
		else {
			t[p].len = t[lp].len + t[rp].len;
		}
	}
	void mdf(int p, int l, int r, int val, int typ) {
		if(typ == 0) {
			if(x[t[p].r + 1] <= l || r <= x[t[p].l]) return ;
			if(l <= x[t[p].l] && x[t[p].r + 1] <= r) {
				t[p].flg += val;
				pushup(p, typ);
				return ;
			}
		} 
		else {
			if(y[t[p].r + 1] <= l || r <= y[t[p].l]) return ;
			if(l <= y[t[p].l] && y[t[p].r + 1] <= r) {
				t[p].flg += val;
				pushup(p, typ);
				return ;
			}
		}
		mdf(lp, l, r, val, typ);
		mdf(rp, l, r, val, typ);
		pushup(p, typ);
	}
}SEGX, SEGY;

int main() {
	ios::sync_with_stdio(0);
	cin >> n;
	for(int i = 1; i <= n; i++) {
		int a, b, c, d;
		cin >> a >> b >> c >> d;
		x[2 * i - 1] = a, x[2 * i] = c;
		y[2 * i - 1] = b, y[2 * i] = d;
		segx[2 * i - 1] = {a, c, b, 1}, segx[2 * i] = {a, c, d, -1};
		segy[2 * i - 1] = {b, d, a, 1}, segy[2 * i] = {b, d, c, -1};
	}
	n <<= 1;
	sort(x + 1, x + n + 1), sort(segx + 1, segx + n + 1);
	sort(y + 1, y + n + 1), sort(segy + 1, segy + n + 1);
	int cnt1 = unique(x + 1, x + n + 1) - x - 1;
	int cnt2 = unique(y + 1, y + n + 1) - y - 1;
	SEGX.build(1, 1, cnt1 - 1), SEGY.build(1, 1, cnt2 - 1);
	int ans = 0, lasx = 0, lasy = 0;
	for(int i = 1; i <= n; i++) {
		SEGX.mdf(1, segx[i].l, segx[i].r, segx[i].flg, 0);
		SEGY.mdf(1, segy[i].l, segy[i].r, segy[i].flg, 1);
		ans += abs(SEGX.t[1].len - lasx) + abs(SEGY.t[1].len - lasy);
		lasx = SEGX.t[1].len, lasy = SEGY.t[1].len;
	}
	cout << ans << '\n';
	return 0;
}
posted @   UKE_Automation  阅读(32)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示