acm-扫描线学习笔记

引言

简单的扫描线可以用于解决矩形求面积并,矩形求周长并的问题,再次基础上我们能够解决更加复杂的三维问题,维护更加复杂的连续量,核心思想是降维+离散

问题引入

我们以洛谷P5490 【模板】扫描线为例题来讲解扫描线的基础运用。
先给出题面描述:
题面描述
扫描线的精髓在于用一根垂直于坐标轴的线去扫描平面上的对象,并在此过程中维护关于平行于该扫描线的方向上的一维的变量。
具体就本题而言,我们可以设置一根平行于x轴而垂直于y轴的线条然后沿着y轴向上扫描矩形。当然,这根线上我们会始终维护一些量,本题中我们维护的是当前扫描线上与所有矩形的交集,显然是一根一根的线段,由于是求面积,我们维护的是这些交集线段的总长度。
下面说下如何维护扫描线上与当前所有矩形交集的总长度。我们考虑扫描线在沿着y坐标向上移动的过程中,当某个矩形与扫描线存在交集的第一刻一定是扫描线与矩形下边相交的时刻,同样地,当他们突然又不存在交集的时候一定是在扫描线与矩阵上边分离的时刻。故我们可以考虑处理出所有矩形的上边和下边,然后按照扫描线与它们相遇的顺序排序,即按照y坐标从小到大排序,对于y坐标相同的可以任意排序,后面就会知道对本题没有任何影响。这样处理完后我们按顺序考虑每个边(它可能是上边也可能是下边),如果遇到了下边,那么意味着扫描线某个矩形相遇,假设这条下边的横坐标范围是 [ l , r ] \mathbf{[l,r]} [l,r],然后我们在扫描线上的 [ l , r ] \mathbf{[l,r]} [l,r]范围上的所有点的权值+1,代表这个块区域与一个矩形相交了,扫描到上边的时候我们就给权值-1即可,代表这块区域与一个矩形相离了。当扫描线上某块区域权值为0意味着这块区域没有与任何矩形相交,否则就与若干个矩形相交(但我们不关心这个,本题只关心面积,不关心某个区域被几个矩形覆盖)。

那么怎么求面积呢?想象一个扫描线在一个图形上扫,那么扫过这个图形的面积自然等于这根扫描线与这个图形无数个相交的线段的积分和。由于本题中扫描线上线段的长度是离散地变化的,我们不需要精细地模拟扫描线一点点向上扫的过程,因此我们是跳着扫描,由于我们将所有的矩形边按照y坐标从小到大排序了,因此我们发现当扫描线从一个矩形边扫到下一个矩形边之间的时刻扫描线上维护的量(即与所有矩形相交长度)是不会发生改变的,故我们在从一个矩形边扫到下一个矩形边的时候,我们就用着两个矩形边的y坐标之差去乘以当前扫描线上的与所有矩形相交长度,这样得到的就是扫描线在这个过程中扫过的面积。这样子扫完所有的矩形边后我们就通过累加得到了整个图形的总面积。

然后代码怎么写呢?我们考虑用线段树维护扫描线上的量,因为扫描线在扫的过程中只有一种操作,即给线上某个区间加一个权值,直接维护点的话太多了,也是离散化,并且我们让线段树上每个点代表一个区间,我们用 [ l , r ] \mathbf{[l,r]} [l,r]代表 [ X [ l ] , X [ r + 1 ] ] \mathbf{[X[l],X[r+1]]} [X[l],X[r+1]]区间(l,r是离散化后的点标号,X[l],X[r]是l,r编号对应的实际坐标),这样子就能实现给区间加权的操作了。然后扫描线在从当前矩形边扫到下一个矩形边的时候,得先算出当前矩形边到下个矩形边将扫过的面积,然后再去下一个矩形边计算它对扫描线上维护的量产生的影响。

具体代码还是很好理解的,给出一份参考代码。

int X[maxn],tot,mark[maxn<<2],t[maxn<<2];//mark[u]代表线段树上的节点u对应的区间被几个矩阵所完整覆盖,t[u]代表线段树上节点u对应的区间中被覆盖区域长度
struct Line{//扫描线
	int l,r,h,v;
	bool operator<(const Line a)const{
		return h<a.h;
	}
}line[maxn];
void pushup(int rt,int l,int r){
	if(mark[rt])t[rt]=X[r+1]-X[l];//当前区间被完整覆盖,故覆盖长度为该节点管理的区间总长
	else if(l<r)t[rt]=t[rt<<1]+t[rt<<1|1];//由于当前区间没有被完整覆盖,故考虑它子区间的情况
	else t[rt]=0;//由于当前节点只负责最小的离散化区间,并且它没被覆盖,故它的覆盖区间长度只能为0
}
void add(int rt,int l,int r,int ql,int qr,int v){
	if(ql<=l && qr>=r){
		mark[rt]+=v;
		pushup(rt,l,r);
		return;
	}
	int mid=l+r>>1;
	if(ql<=mid)add(rt<<1,l,mid,ql,qr,v);
	if(qr>=mid+1)add(rt<<1|1,mid+1,r,ql,qr,v);
	pushup(rt,l,r);
}
int id(int x){//获取离散化编号
	return lower_bound(X+1,X+1+tot,x)-X;
}
int main(){
	int n;
	rd(&n);
	FOR(i,1,n+1){
		int x1,y1,x2,y2;
		rd(&x1,&y1,&x2,&y2);
		X[++tot]=x1,X[++tot]=x2;
		line[2*i-1]=Line{x1,x2,y1,1},line[i*2]=Line{x1,x2,y2,-1};
	}
	sort(X+1,X+1+tot);
	tot=unique(X+1,X+1+tot)-X-1;//离散化
	sort(line+1,line+1+2*n);//按照y坐标排序
	ll ans=0;
	FOR(i,1,2*n){
		int l=id(line[i].l),r=id(line[i].r);
		add(1,1,tot-1,l,r-1,line[i].v);
		ans+=1ll*(line[i+1].h-line[i].h)*t[1];
	}
	wrn(ans);
}

习题

例题一
题目来源:POJPicture
题面:
例题一题面
题解:本题是一个比较简单的变式,考虑在扫描线上维护当前与矩形相交的区域有多少个不连续的部分,假设是x个,那么我们往上扫的过程中假设扫y的距离,产生的周长贡献是2xy,当然这只是计算了与扫描线垂直的周长,因此我们横着扫一遍后竖着再来一遍即可,与模板题的写法大同小异。
代码:

struct Line{
	int l,r,h,v;
	bool operator <(const Line a)const{
		return h<a.h;
	}
}line[maxn];
struct Node{
	int x1,y1,x2,y2;
}a[maxn];
int n,X[maxn],tot,mark[maxn<<2],ml[maxn<<2],mr[maxn<<2],t[maxn<<2];
int id(int x){
	return lower_bound(X+1,X+1+tot,x)-X;
}
void pushup(int rt){
	if(mark[rt])t[rt]=1,ml[rt]=mr[rt]=1;
	else t[rt]=t[rt<<1]+t[rt<<1|1]-(mr[rt<<1] && ml[rt<<1|1]),ml[rt]=ml[rt<<1],mr[rt]=mr[rt<<1|1];
}
void add(int rt,int l,int r,int ql,int qr,int v){
	if(ql<=l && qr>=r){
		mark[rt]+=v;
		pushup(rt);
		return;
	}
	int mid=l+r>>1;
	if(ql<=mid)add(rt<<1,l,mid,ql,qr,v);
	if(qr>=mid+1)add(rt<<1|1,mid+1,r,ql,qr,v);
	pushup(rt);
}
ll solve(){
	ll ans=0;
	tot=0;
	FOR(i,1,n+1){
		X[++tot]=a[i].x1,X[++tot]=a[i].x2;
		line[i*2-1]=Line{a[i].x1,a[i].x2,a[i].y1,1};
		line[i*2]=Line{a[i].x1,a[i].x2,a[i].y2,-1};
	}
	sort(X+1,X+1+tot);
	tot=unique(X+1,X+1+tot)-X-1;
	memset(mark,0,sizeof(int)*(4*tot+1));//记得清空线段树 
	memset(ml,0,sizeof(int)*(4*tot+1));
	memset(mr,0,sizeof(int)*(4*tot+1));
	memset(t,0,sizeof(int)*(4*tot+1));
	sort(line+1,line+1+2*n);
	FOR(i,1,2*n){
		int l=id(line[i].l),r=id(line[i].r);
		add(1,1,tot-1,l,r-1,line[i].v);
		ans+=2ll*t[1]*(line[i+1].h-line[i].h);
	}
	return ans;
}
int main(){
	rd(&n);
	FOR(i,1,n+1)rd(&a[i].x1,&a[i].y1,&a[i].x2,&a[i].y2);
	ll ans1=solve();//竖着扫一下 
	FOR(i,1,n+1)swap(a[i].y1,a[i].x1),swap(a[i].y2,a[i].x2);
	ll ans2=solve();//横着扫一下 
	wrn(ans1+ans2);
}

例题二
题目来源:LuoGuP1502 窗口的星星
题面:
例题二题面
题解:本题需要一些转化才行,我们以每个星星为矩形的左下角,矩形的宽为w-1,高为h-1,那么显然只有存在交点的矩形才能将它们各自代表的星星放在同一个窗口里,然后再考虑某个区域(或某个点,某个线段或某个面积)被多个矩形同时占据,那么这些区域所代表的星星都可以被放在同一个窗口中,原因也很简单,我们以该区域中的任意一个点为矩形的右上角,做一个宽为w-1高为h-1的矩形,那么这些星星都一定在这个矩形汇总,考虑反证法,若这些星星有些不在这个矩形中,那么这些星星的代表矩阵一定不会与该区域有交集。
然后这道题就转化为矩形相交问题了,只不过我们发现我们研究的对象不再只是矩形的面积,还包括矩形之间相交的点和线段。不过这很容易改进,我们建线段树的时候考虑对离散的每个横坐标(而不是每个区间)建立线段树,由于面积相交一定有线段相交,线段相交一定有横坐标相交,那么维护这些离散横坐标上的占据矩形即可,另外根据本题题意权值设置为星星的亮度之和即可,线段树要写成求最大值的形式。
最后还有个细节容易出错,那就是对扫描线排序的时候不仅要按y坐标从小到大,还要在y坐标相同的时候按权值从大到小排列,这是因为当一个矩形的上边和另一个矩形的下边相交的时候,显然这时候这条线段的占据矩阵是两个,但如果你先扫描的上边就会先减权值,就无法得到正确的最大权值。
代码:

int n,tot,X[maxn];
ll t[maxn<<2],lz[maxn<<2];
struct Line{
	int l,r,h;
	ll v;
	bool operator<(const Line a)const{
		return h<a.h || h==a.h && v>a.v;
	}
}line[maxn];

int id(int x){
	return lower_bound(X+1,X+1+tot,x)-X;
}
void pushup(int rt){
	t[rt]=max(t[rt<<1],t[rt<<1|1]);
} 
void pushdown(int rt){
	if(lz[rt]){
		lz[rt<<1]+=lz[rt];
		lz[rt<<1|1]+=lz[rt];
		t[rt<<1]+=lz[rt];
		t[rt<<1|1]+=lz[rt];
		lz[rt]=0; 
	}
}
void add(int rt,int l,int r,int ql,int qr,int v){
	if(l>=ql && r<=qr){
		t[rt]+=v;
		lz[rt]+=v;
		return;
	}
	int mid=l+r>>1;
	pushdown(rt);
	if(ql<=mid)add(rt<<1,l,mid,ql,qr,v);
	if(qr>=mid+1)add(rt<<1|1,mid+1,r,ql,qr,v);
	pushup(rt);
}
void clear(int rt,int l,int r){
	t[rt]=lz[rt]=0;
	if(l==r)return;
	int mid=l+r>>1;
	clear(rt<<1,l,mid);
	clear(rt<<1|1,mid+1,r);
	t[rt]=lz[rt]=0; 
}
ll solve(){
	ll ans=0;
	sort(X+1,X+1+tot);
	tot=unique(X+1,X+1+tot)-X-1;
	sort(line+1,line+1+2*n);
	FOR(i,1,2*n){
		int l=id(line[i].l),r=id(line[i].r);
		add(1,1,tot,l,r,line[i].v);
		ans=max(ans,t[1]);
	}
	clear(1,1,tot);
	return ans; 
}
int main(){
	int t;
	rd(&t);
	while(t--){
		int w,h;
		rd(&n,&w,&h);
		tot=0;
		FOR(i,1,n+1){
			int x,y,l;
			rd(&x,&y,&l);
			X[++tot]=x,X[++tot]=x+w-1;
			line[i*2-1]=Line{x,x+w-1,y,l};
			line[i*2]=Line{x,x+w-1,y+h-1,-l};
		}
		wrn(solve());
	}
}

例题三
题目来源:The 2019 ICPC Vietnam National ContestK.Kingdom of Ants
题面:例题三题面1
例题三题面2

题解:本题难度稍微大一点,需要维护偶数个并且非0矩形覆盖的区域面积,我们考虑维护扫描线上的两个量,一个是奇数个矩形覆盖的区域长度,另一个是非0矩形覆盖的区域长度,显然有 偶 数 非 0 矩 形 覆 盖 长 度 = 非 0 矩 形 覆 盖 长 度 − 奇 数 矩 形 覆 盖 长 度 \mathbf{偶数非0矩形覆盖长度=非0矩形覆盖长度-奇数矩形覆盖长度} 0=0成立,注意到奇数矩形覆盖面积一定是非0的。由于这两个量都好维护,于是本题就得到了解决。然后本题有个坑点就是不保证 x 1 < x 2 , y 1 < y 2 \mathbf{x_1<x_2,y_1<y_2} x1<x2,y1<y2,需要swap一下。

维护的两个量中第一个量是非0矩形覆盖长度,这个量跟模板题维护的量是一模一样,而第二个量是奇数矩形覆盖长度,我们就需要打懒标记,每次线段树对 [ l , r ] \mathbf{[l,r]} [l,r]加或减权值的时候,由于权值都是减1或加1,故一定会发生奇偶反转,即原来是奇数覆盖的区间变成偶数覆盖,原来是偶数覆盖的区间变成奇数覆盖,故打上翻转懒标记即可,是常规的线段树操作。

两个量都维护好后,询问答案的时候减一下即可。

代码:

int n,X[maxn],tot,t[maxn<<2],sum[maxn<<2],mark[maxn<<2],rev[maxn<<2];//t维护奇数覆盖区间长度,sum维护非0覆盖区间长度 
struct Line{//数非0覆盖区间长度=非0覆盖区间总长度-奇数覆盖区间总长度=sum[1]-t[1] 
	int l,r,h,v;
	bool operator<(const Line a)const{
		return h<a.h;
	}
}line[maxn];

int id(int x){
	return lower_bound(X+1,X+1+tot,x)-X;
}
void pushup(int rt,int l,int r){
	if(mark[rt])sum[rt]=X[r+1]-X[l];
	else if(l<r)sum[rt]=sum[rt<<1]+sum[rt<<1|1];
	else sum[rt]=0;
}
void pushdown(int rt,int l,int r){
	int mid=l+r>>1;
	int len1=X[mid+1]-X[l],len2=X[r+1]-X[mid+1];
	if(rev[rt]){
		rev[rt<<1]^=1;
		rev[rt<<1|1]^=1;
		t[rt<<1]=len1-t[rt<<1];
		t[rt<<1|1]=len2-t[rt<<1|1];
		rev[rt]=0;
	}
}
void add(int rt,int l,int r,int ql,int qr,int v){
	if(l>=ql && r<=qr){
		mark[rt]+=v;
		t[rt]=X[r+1]-X[l]-t[rt];
		rev[rt]^=1;
		pushup(rt,l,r);
		return;
	}
	int mid=l+r>>1;
	pushdown(rt,l,r);
	if(ql<=mid)add(rt<<1,l,mid,ql,qr,v);
	if(qr>=mid+1)add(rt<<1|1,mid+1,r,ql,qr,v);
	pushup(rt,l,r);
	t[rt]=t[rt<<1]+t[rt<<1|1];
}

int main(){
	rd(&n);
	tot=0;
	FOR(i,1,n+1){
		int x1,y1,x2,y2;
		rd(&x1,&y1,&x2,&y2);
		if(x1>x2)swap(x1,x2);//坑点,记得让x1<x2,y1<y2 
		if(y1>y2)swap(y1,y2);
		line[i*2-1]=Line{x1,x2,y1,1};
		line[i*2]=Line{x1,x2,y2,-1};
		X[++tot]=x1,X[++tot]=x2;
	}
	sort(X+1,X+1+tot);
	tot=unique(X+1,X+1+tot)-X-1;
	sort(line+1,line+1+2*n);
	ll ans=0;
	FOR(i,1,2*n){
		int l=id(line[i].l),r=id(line[i].r);
		add(1,1,tot-1,l,r-1,line[i].v);
		ans+=1ll*(sum[1]-t[1])*(line[i+1].h-line[i].h);
	} 
	wrn(ans);
} 

例题四
题目来源:HDUGet The Treasury

题面:
例题四题面
题解:这道题考察扫描线在三维中的运用,我们注意到n很小,考虑将z离散化后从小到大排序,这样我们就得到一层一层的二维扫描面,注意到二维扫描面在前后两个离散化的z之间截取图形的面积是不会发生变化的,因此我们考虑将所有在这两个相邻的离散化z之间的所有立方体的截面提取出来,我们的目标就是获取这个截面上覆盖大于等于3的面积,于是这又转化为矩形相交求面积并问题了,也就是说对于每两个z之间我们都找到所有符合条件的立方体做一次扫描线求面积并即可,总面积乘以z的变化量就是体积量,这样这道题就做出来了。

代码:

int n;
int a[maxn][6],Z[maxn],X[maxn],ztot,tot,t1[maxn<<2],t2[maxn<<2],t3[maxn<<2],
	mark[maxn<<2];
struct Line{
	int l,r,h,v;
	bool operator<(const Line a)const{
		return h<a.h;
	}
}line[maxn];
int id(int x){
	return lower_bound(X+1,X+1+tot,x)-X;
}

void pu(int rt,int len,int i,int j){
	int rec;
	if(j)rec=(j==1)?(t1[rt<<1]+t1[rt<<1|1]):(j==2?(t2[rt<<1]+t2[rt<<1|1]):(t3[rt<<1]+t3[rt<<1|1]));
	else rec=len-t1[rt<<1]-t1[rt<<1|1]-t2[rt<<1]-t2[rt<<1|1]-t3[rt<<1]-t3[rt<<1|1];
	if(i==1)t1[rt]+=rec;
	else if(i==2)t2[rt]+=rec;
	else t3[rt]+=rec;
}
void pushup(int rt,int l,int r){ 
	int len=X[r+1]-X[l];
	t1[rt]=t2[rt]=t3[rt]=0;
	if(l==r){
		if(mark[rt]==1)t1[rt]=len;
		else if(mark[rt]==2)t2[rt]=len;
		else if(mark[rt]>=3)t3[rt]=len;
		return;
	}
	if(!mark[rt])pu(rt,len,1,1),pu(rt,len,2,2),pu(rt,len,3,3);
	else if(mark[rt]==1)pu(rt,len,1,0),pu(rt,len,2,1),pu(rt,len,3,2),pu(rt,len,3,3);
	else if(mark[rt]==2)t1[rt]=0,pu(rt,len,2,0),pu(rt,len,3,1),pu(rt,len,3,2),pu(rt,len,3,3);
	else if(mark[rt]>=3)t1[rt]=t2[rt]=0,pu(rt,len,3,0),pu(rt,len,3,1),pu(rt,len,3,2),pu(rt,len,3,3);
}
void add(int rt,int l,int r,int ql,int qr,int v){
	if(l>=ql && r<=qr){
		mark[rt]+=v;
		pushup(rt,l,r);
		return;
	}
	int mid=l+r>>1;
	if(ql<=mid)add(rt<<1,l,mid,ql,qr,v);
	if(qr>=mid+1)add(rt<<1|1,mid+1,r,ql,qr,v);
	pushup(rt,l,r); 
}
void clear(int rt,int l,int r){
	t1[rt]=t2[rt]=t3[rt]=mark[rt]=0;
	if(l==r)return;
	int mid=l+r>>1;
	clear(rt<<1,l,mid);
	clear(rt<<1|1,mid+1,r);
}
ll solve(){
	ll ans=0;
	FOR(i,1,ztot){//现在考虑Z[i]与Z[i+1]之间的所有立方体截面总面积 
		int ltot=0;tot=0;
		FOR(j,1,n+1){//枚举所有立方体,注意到有Z是离散化的,故立方体与Z[i],Z[i+1]之间的区域只有完全包含和不包含的关系 
			if(a[j][2]<=Z[i] && a[j][5]>=Z[i+1]){//看是否包含Z[i]与Z[i+1]的范围 
				line[++ltot]=Line{a[j][0],a[j][3],a[j][1],1};//提取出界面的矩形进行常规扫描线套路 
				line[++ltot]=Line{a[j][0],a[j][3],a[j][4],-1};
				X[++tot]=a[j][0],X[++tot]=a[j][3];
			}
		}
		//以下代码都是针对该截面而言求面积并的常规化套路 
		sort(line+1,line+1+ltot);
		sort(X+1,X+1+tot);
		tot=unique(X+1,X+1+tot)-X-1;
		ll area=0;
		FOR(j,1,ltot){
			int l=id(line[j].l),r=id(line[j].r);
			add(1,1,tot-1,l,r-1,line[j].v);
			area+=1ll*t3[1]*(line[j+1].h-line[j].h);
		}
		ans+=area*(Z[i+1]-Z[i]);//将界面面积与z变化量相乘得体积 
		clear(1,1,tot-1);
	}
	return ans;
}
int main(){
	int t,kase=0;
	rd(&t);
	while(t--){
		rd(&n);
		ztot=tot=0;
		FOR(i,1,n+1){
			rd(a[i],6);
			Z[++ztot]=a[i][2],Z[++ztot]=a[i][5];
		}
		sort(Z+1,Z+1+ztot);//离散化z坐标 
		ztot=unique(Z+1,Z+1+ztot)-Z-1;
		printf("Case %d: %lld\n",++kase,solve());
	}
} -){
		rd(&n);
		ztot=tot=0;
		FOR(i,1,n+1){
			rd(a[i],6);
			Z[++ztot]=a[i][2],Z[++ztot]=a[i][5];
		}
		sort(Z+1,Z+1+ztot);
		ztot=unique(Z+1,Z+1+ztot)-Z-1;
		printf("Case %d: %lld\n",++kase,solve());
	}
} 

总结

扫描线主要用于求二维或三维上一些量的和或并或交,这些量可以是长度,可以是面积也可以是体积,还可以某些东西的个数、权值等等。求解这些量的关键是降低维度,对于二维而言我们直接降低成一维的线段树,对于三维而言我们降低成二维的平面,然后这个子问题是一个常规的扫描线问题,我们就能用常规的套路去解决。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值