浅谈扫描线

浅谈扫描线

前置知识

  • 离散化
  • 线段树
  • 平面几何。?

前言

发现网上大部分题解貌似对于本质没有讲得很明白?

反正自己学时是走了挺多弯路的,在这里总结下,也算为 OI 事业做贡献。?

攒攒 RP。

情景

给定一个坐标系,上面有若干矩形,需要维护它们之间的信息。

扫描线在网络中有从左往右和从下往上扫两种阵营,本文属于后者。

若无特殊说明,均为以 y y y 坐标从小到大扫描。

一般是将矩形的上下边拆出来,按照 y y y 坐标排序,将上下边赋予不同的权值,再用线段树维护 x x x 轴上元素的信息。

元素可能是线段(区间)也可能是点。

求面积并

例题:P5490 【模板】扫描线 & 矩形面积并

这题貌似题解满街都是,图也可以去 oi-wiki 看,这里就不放了。

考虑将一个以 ( x 1 , y 1 ) (x_1,y_1) (x1,y1) 为左端点, ( x 2 , y 2 ) (x_2,y_2) (x2,y2) 为右端点的矩形拆出两条边,分别是 ( x 1 , x 2 , y 1 ) (x_1,x_2,y_1) (x1,x2,y1) 和 ( x 1 , x 2 , y 2 ) (x_1,x_2,y_2) (x1,x2,y2)。其中 x 1 < x 2 , y 1 < y 2 x_1<x_2,y_1<y_2 x1<x2,y1<y2。

下文称 ( x 1 , x 2 , y 1 ) (x_1,x_2,y_1) (x1,x2,y1) 为矩形的下边 , ( x 1 , x 2 , y 2 ) (x_1,x_2,y_2) (x1,x2,y2) 为矩形的上边。

我们先对每一个矩形都进行此操作,并对边按照 y y y 坐标从小到大排序。

首先发现矩形面积并可以拆分为若干小矩形的面积之和。

而小矩形面积又等于长乘高。高是容易的,只需要求出刚处理的相邻两条线段的 y y y 坐标之差即可。

那么长度呢?

注意,这里可能有若干个矩形不挨在一起,所以只统计一个区间 l , r l,r l,r 是否有矩形是不可以的。

注意到一段区间 l , r l,r l,r 能被算进小矩形面积当且仅当其被至少一个矩形包含。

而区间被矩形包含的开始就是矩形的下边。

考虑维护 x x x 轴的每个区间 是否被包含,等价于维护是否存在至少一条在该区间下方的下边。

而如果有两条下边重合了,重合的那段区间需要计算两次。

所以可以考虑用线段树维护每个 x x x 轴的区间被包含了多少次,遇到下边就将区间 x 1 , x 2 x_1,x_2 x1,x2 整体加一,上边就减一。

然后所有被包含的区间长度总和就是线段树中所有值不为 0 0 0 的元素个数

这里动态开点貌似也可以,但是离散化一下会更好。

然后如何维护就是个人认为网上讲得不是很详细的一点了。

我们令 x x x 代表 x , x + 1 x,x+1 x,x+1 这段区间。

显然一段长度为一的区间左端点是 l l l,右端点是 l + 1 l+1 l+1,而区间加的 p , q p,q p,q 是不包含以 q q q 为左端点的区间的。

这才有了所谓的"端点偏移"。

剩下的就是代码实现了,不想写注释,不懂可以参考模板题的题解。

给个代码:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ljl;
#define y1 Atserckcn_CSPS2026_yidengjiang
const int N=1e5+5;
int n,tot,cx;
ljl ans,X[N<<1];
struct Line{
	ljl l,r,y,op;
	bool operator < (const Line a)const{
		return y<a.y;}
}l[N<<1];
struct NODE{
	ljl l,r,cnt,cv;
}node[N*2*4];
#define lc (p<<1)
#define rc (p<<1|1)
void pushup(int p)
{
	if(node[p].cv){node[p].cnt=X[node[p].r+1]-X[node[p].l];return;}
	if(node[p].l==node[p].r){node[p].cnt=0;return;}
	node[p].cnt=node[lc].cnt+node[rc].cnt;
	return;
}
void bld(int l,int r,int p)
{
	node[p].l=l;node[p].r=r;
	if(l==r)return;
	int mid=(l+r)>>1;
	bld(l,mid,lc);bld(mid+1,r,rc);
	pushup(p);
	return;
}
void addlr(int l,int r,ljl val,int p)
{
	if(l>r)return;
	if(l<=node[p].l&&node[p].r<=r)
	{
		node[p].cv+=val;
		if(node[p].cv>=1)node[p].cnt=X[node[p].r+1]-X[node[p].l];
		else pushup(p);
		return;
	}
	int mid=(node[p].l+node[p].r)>>1;
	if(l<=mid)addlr(l,r,val,lc);
	if(mid<r)addlr(l,r,val,rc);
	pushup(p);
	return;
}
int main(){
	ios::sync_with_stdio(0);
	cin>>n;
	for(int i=1,x1,x2,y1,y2;i<=n;++i)
	{
		cin>>x1>>y1>>x2>>y2;
		l[++tot].l=x1;l[tot].r=x2;l[tot].y=y1;l[tot].op=1;
		l[++tot].l=x1;l[tot].r=x2;l[tot].y=y2;l[tot].op=-1;
		X[++cx]=x1;X[++cx]=x2;
	}
	sort(X,X+cx+1);
	cx=unique(X+1,X+cx+1)-X-1;
	bld(1,cx-1,1);
	sort(l+1,l+tot+1);
	for(int i=1;i<=tot;++i)
	{
		int L=lower_bound(X+1,X+cx+1,l[i].l)-X;
		int R=lower_bound(X+1,X+cx+1,l[i].r)-X;
		if(i>1)ans+=(node[1].cnt*(l[i].y-l[i-1].y));
		addlr(L,R-1,l[i].op,1);
	}cout<<ans<<'\n';
	return 0;
}

求矩形周长

题目:P1856 IOI 1998 USACO5.5 矩形周长 Picture

我去竟然是 IOI 真题吗。

这题我并没有写,只是学校数学课摸鱼时口胡了几种做法。

第一种。注意到 x x x 轴和 y y y 轴谁是谁其实不重要,所以可以先从 y y y 轴从下往上扫一遍,按上题一样统计有多少区间是在外面的,这要的是两次查找的长度之差的绝对值 ,再按 x x x 轴扫一遍,统计竖着的区间长度。这应该是对的,就是好麻烦。

为什么是差?因为差的是新增的。

对矩形周长差贡献的证明

发现洛谷上某篇题解是这么说的:

侵权自删。

那么怎么证明呢?

学校化学课摸鱼想到的(逃。

首先可以类比括号序列,下边是左括号,上边是右括号。则全部矩形构成了合法括号序列。

那么一条边能对答案有贡献当且仅当满足下列一个:

  • 下面没边
  • 上面没边

注意到这是不可能全部满足的。但这不重要。

那么类似括号序列处理,统计在能匹配的都匹配了后,还剩余的左括号(下边)的数量。

如果是 0 0 0,那么说明该边是下边(想想为什么),要计入答案。并区间加。

否则对答案就是没有贡献的。

那么上方没有边的上边该怎么计算?

注意到上边是区间减法操作,而减法后,可以计入答案当且仅当有区间因为这次操作 而从 1 → 0 1\rightarrow 0 1→0。

综上,不管是上边还是下边能对答案做出贡献的情况都能用 a n s c u r − a n s l s t ans_{cur}-ans_{lst} anscur−anslst 表示出来。

第二种。发现扫描 y y y 轴的时候太浪费了,什么都没做。

发现每步扫描对答案的贡献是 A + c × Δ y A+c\times \Delta y A+c×Δy。其中 Δ y \Delta y Δy 是相邻两边的 y y y 轴坐标之差。 A A A 是两次查找的长度差, c c c 是有多少个点没被包含且值不为 0 0 0。

画个图理解下。

然后维护一下就做完了。

代码去题解区看看吧。

求区间最值

题:P12119 NordicOI 2025 垃圾收集 Garbage Collection

首先维护矩形是困难的,考虑化矩形为点。

显然可以通过固定一个角的方式确定一个矩形。

本文以 ( x , y ) (x,y) (x,y) 表示以 ( x , y ) (x,y) (x,y) 为左下角, ( x + W − 1 , y + H − 1 ) (x+W-1,y+H-1) (x+W−1,y+H−1) 为右上角的矩形。称 ( x , y ) (x,y) (x,y) 为代表点。

那么一个垃圾 ( x , y ) (x,y) (x,y) 想要捕获它,代表点 ( a , b ) (a,b) (a,b) 就必须满足 x − W + 1 ≤ a ≤ x , y − H + 1 ≤ b ≤ y x-W+1\le a\le x,y-H+1\le b\le y x−W+1≤a≤x,y−H+1≤b≤y。

不懂的自行画图。

所以可以考虑将垃圾 ( x , y ) (x,y) (x,y) 转化为矩形加法,将以 ( X − W + 1 , y − H + 1 ) (X-W+1,y-H+1) (X−W+1,y−H+1) 为左下角, ( x , y ) (x,y) (x,y) 为右上角的矩形中所有整点都加上权值。

那么现在是求一个点 ( x , y ) (x,y) (x,y) 使得以其权值最大。

注意:上文我们讨论的扫描线都是维护线段(区间),但这里是点,所以不需要端点偏移。

考虑将转化后的矩形的下边权值赋为 w w w,上边赋为 − w -w −w。

这样按照 y y y 坐标排序,每次操作完对所有点取 max ⁡ \max max 就好了。

吗?

并不是!

注意到我们是可以取到 y y y 坐标和上边一样高度的点的,如果只看 y y y 进行排序很有可能会优先减掉贡献,就无法算出正确答案。

那么怎么修改?

这里我搞出了两种。

  • 以 y y y 为第一关键字从小到大排, w w w 为第二关键字从大到小排。

这样因为上边的权值是负数,一定没有下边的权值大,所以每次都会优先加入下边。

  • 将上边的 y y y 坐标加一,再比较完 y y y 后按照 w w w 从小到大排。

这里的上边可以表示"不可以取到这个高度"的意思,所以要优先限制,再计算。

给出代码。

第一种的排序:

cpp 复制代码
struct Lines{
	ljl l,r,y,w;
	bool operator < (const Lines a)const{
		if(y!=a.y)
			return y<a.y;
		return w>a.w;
	}
}lns[N*2];

建立线段:

cpp 复制代码
lns[++tot].l=x-W+1;lns[tot].r=x;lns[tot].y=y-H+1;lns[tot].w=w;
lns[++tot].l=x-W+1;lns[tot].r=x;lns[tot].y=y;lns[tot].w=-w;	

第二种排序:

cpp 复制代码
struct Lines{
	ljl l,r,y,w;
	bool operator < (const Lines a)const{
		if(y!=a.y)
			return y<a.y;
		return w<a.w;
	}
}lns[N*2];

建立线段:

cpp 复制代码
lns[++tot].l=x-W+1;lns[tot].r=x;lns[tot].y=y-H+1;lns[tot].w=w;
lns[++tot].l=x-W+1;lns[tot].r=x;lns[tot].y=y+1;lns[tot].w=-w;	

代码:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ljl;
const int N=1e5+5;
int n,tot;
ljl W,H,X[N*2],cx,ans;
struct Lines{
	ljl l,r,y,w;
	bool operator < (const Lines a)const{
		if(y!=a.y)
			return y<a.y;
		return w<a.w;
	}
}lns[N*2];
struct NODE{
	int l,r;ljl lz,maxn;
}node[N*2*4];
#define lc (p<<1)
#define rc (p<<1|1)
void pushup(int p)
{
	if(node[p].l==node[p].r)return;
	node[p].maxn=max(node[lc].maxn,node[rc].maxn);
	return;
}
void bld(int l,int r,int p)
{
	node[p].l=l;node[p].r=r;
	if(l==r)return;
	int mid=(l+r)>>1;
	bld(l,mid,lc);bld(mid+1,r,rc);
	pushup(p);
	return;
}
void pushdown(int p)
{
	if(!node[p].lz)return;
	if(node[p].l==node[p].r)return;
	node[lc].lz+=node[p].lz;
	node[rc].lz+=node[p].lz;
	node[lc].maxn+=node[p].lz;
	node[rc].maxn+=node[p].lz;
	node[p].lz=0;
	return;
}
void addlr(int l,int r,ljl val,int p)
{
	if(l<=node[p].l&&node[p].r<=r)
	{
		node[p].maxn+=val;node[p].lz+=val;
		return;
	}
	pushdown(p);
	int mid=(node[p].l+node[p].r)/2;
	if(l<=mid)addlr(l,r,val,lc);
	if(mid<r)addlr(l,r,val,rc);
	pushup(p);
	return;
}
int main(){
	ios::sync_with_stdio(0);
	cin>>n>>W>>H;
	for(ljl i=1,x,y,w;i<=n;++i)
	{
		cin>>x>>y>>w;
		lns[++tot].l=x-W+1;lns[tot].r=x;lns[tot].y=y-H+1;lns[tot].w=w;
		lns[++tot].l=x-W+1;lns[tot].r=x;lns[tot].y=y+1;lns[tot].w=-w;		
		X[++cx]=x-W+1;X[++cx]=x;
	}
	sort(lns+1,lns+tot+1);
//	cout<<"---------\n";
//	for(int i=1;i<=tot;++i)cout<<lns[i].l<<' '<<lns[i].r<<' '<<lns[i].y<<'\n';
//	cout<<"---------\n";
	sort(X+1,X+cx+1);
	cx=unique(X+1,X+cx+1)-X-1;
//	for(int i=1;i<=cx;++i)cout<<X[i]<<' ';
//	cout<<'\n';
	bld(1,cx,1);
	for(int i=1;i<=tot;++i)
	{
		int l=lower_bound(X+1,X+cx+1,lns[i].l)-X;
		int r=lower_bound(X+1,X+cx+1,lns[i].r)-X;
		addlr(l,r,lns[i].w,1);
		ans=max(ans,node[1].maxn);
//		cout<<"["<<l<<','<<r<<"],add "<<lns[i].w<<'\n';
	}cout<<ans<<'\n';
	return 0;
}

后记

因为作者很弱,扫描线还有很多用法都没讲到,以后有机会补一补。

感谢 Tiger_Rory 学长把他在【数据删除】学校给【数据删除】班讲的扫描线课件分享给我!/bx

大家不要学我上课摸鱼!

相关推荐
a187927218311 小时前
【算法】双指针与滑动窗口(一):框架总纲——三类问题、一个原理与判决书
算法·leetcode·区间·双指针·滑动窗口·原理·算法讲解
shehuiyuelaiyuehao1 小时前
算法42,模拟算法,模拟z字形变换
算法
朝朝辞暮i1 小时前
C++第一课
开发语言·c++·算法
老赵的博客1 小时前
c++ tcp的三次握手
c++
橘子汽水1681 小时前
Leetcode 208,207实现Trie前缀树,课程表
java·数据结构·算法·leetcode
hanlin031 小时前
刷题笔记:力扣第169题-多数元素
笔记·算法·leetcode
张小姐的猫1 小时前
【AI大模型接入SDK】 —— Ollama本地接入Deepseek
java·linux·开发语言·网络·c++·人工智能
hfywmsj1 小时前
广州餐饮铺位招租数据建模:基于多维评估体系的选址算法设计与实现
人工智能·算法·广州餐饮铺位招租
wabs6662 小时前
关于二叉树【力扣116.填充每个节点的下一个右侧节点指针的思考】
数据结构·c++·算法·leetcode·二叉树·层序遍历