【学习笔记】扫描线

这篇具有很好参考价值的文章主要介绍了【学习笔记】扫描线。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

扫描线可以用来求解周长并和面积并。

面积并

扫描线

给定 \(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;
}

周长并

扫描线

求解周长并同样可以使用扫描线。周长并与面积并类似,不过要分成横边和纵边来计算:

先来考虑比较简单的纵边,而横边则结合着接下来的线段树优化讲。每次扫描,由于两线之间的距离是固定的,所以当前纵边的贡献就是 \(\text{和上条线的距离}\times\text{当前共有几条边}\)

线段树优化

和面积并一样,周长并也可以使用线段树了,不过会复杂一些(维护的信息更多)。先画出线段树:

然后考虑需要维护哪些信息和如何转移。首先肯定要维护的信息是 \(sum\),也就是整个区间被覆盖了几次,理由同面积并。其余还是分横边和纵边考虑。

纵边:要维护当前共有几条边,其实就是当前不相交的线段数 \(\times 2\)(也就是把相交的线段“拼”在一起后统计),那么记 \(c\) 为当前区间线段条数,考虑如何转移。

\(sum\) 不为 \(0\) 时,其为当前区间长度;否则要从左右儿子转移而来,直接相加肯定不对,因为有一些左右儿子的线段会相交,例如:

左右儿子的 \(c\) 均为 \(1\),但当前的 \(c\) 也是 \(1\)

那什么情况下两边线段会相交?观察左右儿子的交界处(左儿子右端点、右儿子左端点),当且仅当它们均被线段覆盖时会相交,\(c\) 需要减 \(1\),所以再维护一个区间左右端点是否被覆盖 \(lc,rc\) 即可。它们的转移也很简单,\(sum\)\(0\) 时分别为左儿子 \(lc\)、右儿子 \(rc\),否则均为 \(1\)

横边:这个和求面积并是长方形的长有点像,要维护当前区间被覆盖的长度 \(len\)。为什么?其实看看答案是什么就一目了然了。观察以下两张图:

其中左图为实际周长的贡献,右图为线段树中的 \(len\),可以发现,实际周长其实就是两两 \(len\) 之差!这就是为什么要维护 \(len\) 的原因。注意,在统计答案时,还得加上最后的那条横边。

自此,考虑完了以上信息并得出了统计答案的方法,pushup 也很好写了:

struct node1 {
	int sum, len, c;
	bool lc, rc;
}t[N << 3];
void pushup(int k, int l, int r) {
	if (t[k].sum) t[k] = {t[k].sum, X[r + 1] - X[l], 1, 1, 1};
	else {
		node1 ls = t[k << 1], rs = t[k << 1 | 1];
		t[k] = {0, ls.len + rs.len, ls.c + rs.c, ls.lc, rs.rc};
		if (ls.rc && rs.lc) t[k].c--;
	}
}

在放上最终代码前,总结一下:

然后就是代码中的细节要注意一下:文章来源地址https://www.toymoban.com/news/detail-618063.html

//洛谷 P1856 
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 5;;
struct node {
	int l, r, h, v;
}Y[N];
struct node1 {
	int sum, len, c;
	bool lc, rc;
}t[N << 3];
int X[N];
bool cmp(node x, node y) {
    //如 P5490 第一篇题解所说,由于是求周长,同一高度的边也要按先下边后上边排序
	if (x.h != y.h) return x.h < y.h;
	else return x.v > y.v;
}
void pushup(int k, int l, int r) {
	if (t[k].sum) t[k] = {t[k].sum, X[r + 1] - X[l], 1, 1, 1};
	else {
		node1 ls = t[k << 1], rs = t[k << 1 | 1];
		t[k] = {0, ls.len + rs.len, ls.c + rs.c, ls.lc, rs.rc};
		if (ls.rc && rs.lc) t[k].c--;
	}
}
void updata(int k, int l, int r, int L, int R, int v) {
	if (L <= X[l] && X[r + 1] <= R) {
		t[k].sum += v;
		return pushup(k, l, r);
	}
	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);
}
int main() {
	int n;
	cin >> n;
	for (int i = 1, x1, x2, y1, y2; i <= n; i++) {
		cin >> x1 >> y1 >> x2 >> y2; 
		Y[2 * i - 1] = {x1, x2, y1, 1}, Y[2 * i] = {x1, x2, y2, -1}; 
		X[2 * i - 1] = x1, X[2 * i] = x2;
	}
	n = n << 1;
	sort(Y + 1, Y + 1 + n, cmp);
	sort(X + 1, X + 1 + n);
	int len = unique(X + 1, X + 1 + n) - X - 1, ans = 0, lst = 0;
	for (int i = 1; i < n; i++) {
		updata(1, 1, len - 1, Y[i].l, Y[i].r, Y[i].v);
		ans += 2 * t[1].c * (Y[i + 1].h - Y[i].h) + abs(t[1].len - lst);//记得加绝对值
		lst = t[1].len;
	}
	cout << ans + Y[n].r - Y[n].l;
	return 0;
} 

到了这里,关于【学习笔记】扫描线的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【计算机图形学 】扫描线多边形填充算法 | OpenGL+鼠标交互

    传送门 实现多边形扫描线填充算法,并和鼠标进行交互。 具体原理略过,会贴上完整代码,可直接运行。 环境: vs2019,OpenGL的库(可以搜索如何用vs使用OpenGL的库,可以使用vs自带的插件或者其他方法,很方便) 要点: 1.NET和AET的创建,改动 2.改变鼠标点击和鼠标拖拽的响应

    2023年04月08日
    浏览(39)
  • 计算机图形学实验——利用MFC对话框实现多边形绘制与填充(扫描线填充算法)附源码

    内容概括: 利用基于对话框的MFC项目 实现鼠标点击绘制多边形 实现扫描线算法填充多边形 源码见Yushan-Ji/ComputerGraphics: ECNU2023秋 计算机图形学课程实验代码 (github.com) 通过鼠标交互输入多边形 对各种多边形进行填充,包括边界自交的情况 利用 OnLButtonDown 和 OnRButtonDown 函数,

    2024年02月04日
    浏览(45)
  • 设计模式学习笔记 - 面向对象 - 2.封装、抽象、继承、多态分别用来解决哪些问题?

    封装 也叫作信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类提供的方法(或者叫作函数)来访问内部信息或数据。 下面这段代码是一个简化版的虚拟钱包的代码实现。在金融系统中,我们会给每个用户创建一个虚拟钱包,用来记录用户在我们

    2024年02月21日
    浏览(39)
  • 数字藏品可以用来干什么?

    一、作为数字收藏艺术品,满足收藏者的爱好。绘画、文物等艺术品是数字收藏品是最基础的应用,也是目前最受欢迎的种类,它与现实生活中的其他艺术品具有相似性,一样通过网上购买的方式获得。 数字藏品,虽然“摸不着”,但与传统艺术品相比较,又具有一定优势,

    2024年02月09日
    浏览(30)
  • C#是什么?可以用来做什么?

            C#(读作“C Sharp”)是一种容易使用不复杂新型的编程语言,不仅是面向对象,它的类型还安全。C# 源于 C 语言系列,C、C++、Java 和 JavaScript 程序员很快就可以上手使用。C# 是一个现代的、通用的、面向对象的编程语言,它是由微软(Microsoft)开发的,由 Ecma 和

    2024年02月21日
    浏览(29)
  • 文心一言可以用来论文降重吗

    大家好,今天来聊聊文心一言可以用来论文降重吗,希望能给大家提供一点参考。 以下是针对论文重复率高的情况,提供一些修改建议和技巧,可以借助此类工具: 文心一言可以用来论文降重吗?🔥🔥 对于许多写论文的朋友来说,降重是一项令人头疼的任务。而现在,随

    2024年03月14日
    浏览(52)
  • AIGC系列:1.chatgpt可以用来做哪些事情?

    上图的意思:神器轩辕剑 那么,在现在AI盛行的信息时代, 你是否知道如何获得和利用ChatGPT这一把轩辕剑来提升你的攻击力和生存能力呢? 程序员小张: 刚毕业,参加工作1年左右,日常工作是CRUD 架构师老李: 多个大型项目经验,精通各种开发架构屠龙宝术; 在未来的世

    2024年02月09日
    浏览(27)
  • LaWGPT:一款可以用来维权的AI大模型

    大家好,大模型 fine-tune,在各个领域百花齐放。 上两天发过一篇文章,介绍了一个基于 LLaMA 训练得到的 AI 医生咨询助手。 看不少小伙伴都感兴趣,咱今天再介绍一个法律领域的 LaWGPT。 昨天,都在传,杭州互联网大厂裁员,消息铺天盖地,搞得人心惶惶。 合法裁员,给个

    2024年02月11日
    浏览(31)
  • 文心一言可以用来论文降重吗 papergpt

    大家好,今天来聊聊文心一言可以用来论文降重吗,希望能给大家提供一点参考。 以下是针对论文重复率高的情况,提供一些修改建议和技巧,可以借助此类工具: 文心一言可以用来论文降重吗 一、引言 随着人工智能技术的不断发展,越来越多的工具被应用于各种领域。其

    2024年02月03日
    浏览(32)
  • python cv2是什么,可以用来干什么

    OpenCV (Open Source Computer Vision Library) 是一个流行的开源计算机视觉库,提供了丰富的图像和视频处理功能。通过使用 OpenCV 的 Python 绑定库 cv2,可以实现以下一些功能: 图像读取和显示:使用 cv2.imread() 读取图像文件,使用 cv2.imshow() 显示图像窗口。 图像处理:包括图像滤波、

    2024年02月14日
    浏览(34)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包