扫描线
1 概念
扫描线一般运用在图形上面,其含义就是拿一条线在图形上扫,然后维护信息得到答案。
通常情况下,扫描线被用于求面积、周长问题。
2 矩形面积并
2.1 思路
先给出例题:【模板】扫描线
考虑我们所学的求不规则图形面积的方式,无外乎割补。然而补似乎太难实现,因此考虑割。
如下图:
那么我们考虑怎么割。显然如果遇到一条横向的线段(纵向同理),那么就意味着有一块矩形。那么我们让这根扫描线从下往上扫,碰到横向线段停下来维护信息。如下:
考虑我们此时要记录什么,就是当前扫到的线段长度。因此我们要现将所有线段的左右端点存下来,然后排序:
现在整个
先考虑如何处理,我们可以将一个矩形看做有上边和下边。显然在到达上边时线段会出现一次,而离开后就会减掉一次。那么我们可以给线段附上权值,即下边为
那么现在回顾我们上面要维护的东西。我们知道,现在要维护中间的区间段,要在碰到线段时给对应的区间段增减出现次数,最后求出总和。
看不懂啊,我们改写一下:维护一个区间,支持区间修改,区间查询。这一下就看懂了,使用线段树。
如下图所示:
在线段树上,我们维护两个信息:当前节点所代表的区间段出现次数,当前节点所代表的区间段总共出现的区间段长度,记为
同时,我们其实并不需要求出所有区间的和,只需要知道整个区间的和,因此无需懒标记以及下放。
然而又有一个问题,我们的线段树实际上维护的是中间的区间段,因此对于区间段
最后的最后,我们每种
剩下的看代码吧。
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 矩形周长并
其实有了上面的学习,周长并就不难了。
首先显然考虑分成两部分:从下往上和从左往右。接下来考虑贡献,我们依然维护线段总长,然后考虑分类:
- 如果是增线段,那么由它而增加的部分是新增的周长
- 如果是减线段,那么由它而减去的部分是新增的周长
因此,无论是增线段还是减线段,对于周长做出的贡献就是对于线段总长的改变(注意这个改变指的是非负的)。
按照这个维护一下即可,代码如下:
#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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律