扫描线模板

前言

其实之前是学过扫描线的,但在此进行一个系统梳理(目前仅涉及到最基本的东西)

扫描线

扫描线是什么

拿出最经典的图
扫描线
发现就是扫描矩形的边(同一方向的),把矩形的一边当入边,另一边当出边,从下往上扫(从入边方向扫),将图形分成若干矩形(为区分暂且叫做矩块)。
扫到一个入/出边就查询当前矩块的长(平行于扫描线的边),宽用两个边之间的距离来算,并将该入/出边进行维护。
矩块的长的维护可以用树状数组或者线段树来维护,这里先叙述线段树的做法:

  • 扫到入边,对区间进行增加
  • 扫到出边,对区间进行删除
  • 与传统线段树不同点在于,该线段树的叶子节点不是“点”,而是单位区间,因此算区间长度时应该注意,对右端点/左端点进行+1/-1
  • 区间加法也略有不同,应该维护覆盖次数(完全覆盖该区间)

上问提到的单位区间是指:

  • 你离散化了,那就是各个相邻点(相邻边的横/纵坐标)的距离
  • 你没有离散化,用动态开点,那就是单位1

示例代码(有注释解释)

#include<iostream>//离散化+线段树 
#include<string.h>
#include<string>
#include<algorithm>
#include<vector>
#include<deque>
#include<queue>
#define lson v[rt].ls
#define rson v[rt].rs
using namespace std;
#define int long long
int read(){int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9') {if(ch=='-') f=-1;ch=getchar();}while(ch>='0'&&ch<='9') {x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}return x*f;}
const int maxn=2e5+5;
int n,root,segcnt,y[maxn],val[maxn],sum,ans;//离散化要用 
struct mcx
{
	int x,y1,y2,in;
}e[maxn];//存矩形的边 
bool cmp(mcx v1,mcx v2)
{
	return v1.x<v2.x;
}//这样排序 
struct nd
{
	int ls,rs,cnt,len;
}v[20*maxn];//线段树 
int lower(int x)
{
	int l=1,r=sum,ret=0;
	while(l<=r)
	{
		int mid=(l+r)>>1;
		if(val[mid]<=x)
		{
			ret=mid;
			l=mid+1;
		}
		else r=mid-1;
	}
	return ret;
}//离散化后查询对应位置(lower_bound) 
void pushup(int rt,int l,int r)
{
	if(v[rt].cnt) v[rt].len=val[r+1]-val[l];//完全覆盖,注意这里r+1 
	else v[rt].len=v[lson].len+v[rson].len;//未完全覆盖 
}
void update(int &rt,int l,int r,int L,int R,int k)
{
	if(!rt) rt=++segcnt;
	if(l>=L&&r<=R)
	{
		v[rt].cnt+=k;
		pushup(rt,l,r);//这里也要pushup 
		return;
	}
	int mid=(l+r)>>1;
	if(L<=mid) update(lson,l,mid,L,R,k);
	if(R>mid) update(rson,mid+1,r,L,R,k);
	pushup(rt,l,r);
}
signed main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		int x1,y1,x2,y2;
		cin>>x1>>y1>>x2>>y2;
		e[i]={x1,y1,y2,1};
		e[i+n]={x2,y1,y2,0};
		y[i]=y1,y[i+n]=y2;//离散化准备 
	}
	sort(y+1,y+1+2*n);
	sort(e+1,e+1+2*n,cmp);
	for(int i=1;i<=2*n;i++) if(y[i]!=y[i-1]||i==0) val[++sum]=y[i];//手写离散化(unique) 
	for(int i=1;i<=2*n;i++)
	{
		ans+=v[1].len*(e[i].x-e[i-1].x);//计算前一个矩块的贡献 
		if(e[i].in) update(root,1,sum,lower(e[i].y1),lower(e[i].y2)-1,1);
		else update(root,1,sum,lower(e[i].y1),lower(e[i].y2)-1,-1);
	}
	cout<<ans;
	return 0;
}

又一个扫描线

既然矩形的面积并完了,那么周长并也就好说了。
同样从左往右扫,还要从上往下扫(好像有扫一边的做法,这里说扫两遍的做法)。
同样用线段树维护。
同样在扫到入/出边进行贡献计算,但是不用算矩块的宽了,只需要算出矩块长的差值就行(初始长为0)。
不同在于扫两遍,记录答案在更新出/入边之后.
结合上文与代码理解就行。

示例

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=10010;
#define lson v2[rt].ls
#define rson v2[rt].rs
#define lson1 v4[rt].ls
#define rson1 v4[rt].rs
int n,ans;
int tot,sum,root,y[maxn],val[maxn];
struct mcx
{
	int x,y1,y2;
	bool in;
}v1[maxn];
bool cmp(mcx x,mcx y)
{
	return x.x<y.x;
}
struct nd
{
	int ls,rs,cnt,len;
}v2[14*maxn];
void pushup(int rt,int l,int r)
{
	if(v2[rt].cnt) v2[rt].len=val[r+1]-val[l];
	else v2[rt].len=v2[lson].len+v2[rson].len;
}
void update(int &rt,int l,int r,int L,int R,int k)
{
	if(!rt) rt=++tot;
	if(l>=L&&r<=R)
	{
		v2[rt].cnt+=k;
		pushup(rt,l,r);
		return;
	}
	int mid=(l+r)>>1;
	if(L<=mid) update(lson,l,mid,L,R,k);
	if(R>mid) update(rson,mid+1,r,L,R,k);
	pushup(rt,l,r);
}
int query(int x)
{
	int l=1,r=sum,ans1=0;
	while(l<=r)
	{
		int mid=(l+r)>>1;
		if(x<=val[mid])
		{
			ans1=mid;
			r=mid-1;
		}
		else l=mid+1;
	}
	return ans1;
}
//第一遍扫用到的 


int tot1,sum1,root1,x[maxn],val1[maxn];
struct mcx1
{
	int y,x1,x2;
	bool in;
}v3[maxn];
bool cmp1(mcx1 x,mcx1 y)
{
	return x.y<y.y;
}
struct nd1
{
	int ls,rs,cnt,len;
}v4[14*maxn];
void pushup1(int rt,int l,int r)
{
	if(v4[rt].cnt) v4[rt].len=val1[r+1]-val1[l];
	else v4[rt].len=v4[lson1].len+v4[rson1].len;
}
void update1(int &rt,int l,int r,int L,int R,int k)
{
	if(!rt) rt=++tot1;
	if(l>=L&&r<=R)
	{
		v4[rt].cnt+=k;
		pushup1(rt,l,r);
		return;
	}
	int mid=(l+r)>>1;
	if(L<=mid) update1(lson1,l,mid,L,R,k);
	if(R>mid) update1(rson1,mid+1,r,L,R,k);
	pushup1(rt,l,r);
}
int query1(int x)
{
	int l=1,r=sum1,ans1=0;
	while(l<=r)
	{
		int mid=(l+r)>>1;
		if(x<=val1[mid])
		{
			ans1=mid;
			r=mid-1;
		}
		else l=mid+1;
	}
	return ans1;
}
//第二遍扫用到的 

int main () 
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		int x1,y1,x2,y2;
		cin>>x1>>y1>>x2>>y2;
		v1[i].x=x1;v1[i].y1=y1;v1[i].y2=y2;v1[i].in=true;
		v1[i+n].x=x2;v1[i+n].y1=y1;v1[i+n].y2=y2;v1[i+n].in=false;
		y[i]=y1;y[i+n]=y2;
		v3[i].y=y1;v3[i].x1=x1;v3[i].x2=x2;v3[i].in=true;
		v3[i+n].y=y2;v3[i+n].x1=x1;v3[i+n].x2=x2;v3[i+n].in=false;
		x[i]=x1;x[i+n]=x2;
	}
	sort(y+1,y+1+2*n);
	sort(v1+1,v1+1+2*n,cmp);
	sort(x+1,x+1+2*n);
	sort(v3+1,v3+1+2*n,cmp1);//离散化 
	
	for(int i=1;i<=2*n;i++) if(y[i]!=y[i-1]||i==1) val[++sum]=y[i];
	int Len=0;
	for(int i=1;i<=2*n;i++)
	{
		if(v1[i].in) update(root,1,sum,query(v1[i].y1),query(v1[i].y2)-1,1);
		else update(root,1,sum,query(v1[i].y1),query(v1[i].y2)-1,-1);
		ans+=abs(v2[1].len-Len);//维护后更新答案 
		Len=v2[1].len;
	}//第一遍扫 
	for(int i=1;i<=2*n;i++) if(x[i]!=x[i-1]||i==1) val1[++sum1]=x[i];
	Len=0;
	for(int i=1;i<=2*n;i++)
	{
		if(v3[i].in) update1(root1,1,sum1,query1(v3[i].x1),query1(v3[i].x2)-1,1);
		else update1(root1,1,sum1,query1(v3[i].x1),query1(v3[i].x2)-1,-1);
		ans+=abs(v4[1].len-Len);
		Len=v4[1].len;
	}
	cout<<ans;
    return 0;
}

还有一个板子应用——逆序对

从后往前遍历数组,每遍历到一个数加入线段树(或树状数组),求一下小于这个数的数量,所有数量之和即为所求。
代码如下

示例

#include<iostream>
#include<string.h>
#include<string>
#include<algorithm>
#include<vector>
#include<deque>
#include<queue>
using namespace std;
#define int long long
const int maxn=5e5+5;
int read(){int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9') {if(ch=='-') f=-1;ch=getchar();}while(ch>='0'&&ch<='9') {x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}return x*f;}
struct bit
{
	int size=0;
	int topbit=1;
	vector<int> tree;
	int lowbit(int x){return x&(-x);}
	void init(int x)
	{
		size=x;
		topbit=1;
		while((topbit<<1)<=size) topbit<<=1;
		tree.assign(size+2,0);
	}
	void clear(){fill(tree.begin(),tree.end(),0);}
	void add(int x,int val)
	{
		while(x<=size)
		{
			tree[x]+=val;
			x+=lowbit(x);
		}
	}
	int danquery(int x)
	{
		if(x<0) return 0;
		int ret=0;
		while(x)
		{
			ret+=tree[x];
			x-=lowbit(x);
		}
		return ret;
	}
	int rangquery(int l,int r) {return l>r?0:danquery(r)-danquery(l-1);}
	int kth(int rank)
	{
		int pos=0;
		for(int i=topbit;i;i>>=1)
		{
			int op=pos+i;
			if(op<=size&&tree[op]<rank)
			{
				pos=op;
				rank-=tree[op];
			}
		}
		return pos+1;
	}
}tree;
int n;
int a[maxn],m,b[maxn];
signed main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		b[i]=a[i];
	}
	sort(b+1,b+1+n);
	m=unique(b+1,b+1+n)-b;
	int ret=0;
	tree.init(m+3);
	for(int i=n;i>=1;i--)
	{
		int op=lower_bound(b+1,b+1+m,a[i])-b;
		tree.add(op,1);
		ret+=tree.danquery(op-1);
	}
	cout<<ret;
	return 0;
}

再来一道

HH的项链
可以先离线询问,把左端点当入边,右端点当出边,从前往后扫,扫到右端点求一下左右端点之间的贡献.

#include<bits/stdc++.h>
using namespace std;
const int maxn1=1e6+10;
typedef long long ll;
int n=0,m=0;
int a[maxn1];
int c[maxn1];
int d[maxn1];
int ans[maxn1];
struct nd
{
	int l,r,loca;
}v[maxn1];
bool cmp(nd j,nd g)
{
	return j.r<g.r;
}
int lowbit(int x){return x&(-x);}
void add(int i,int x)
{
	while(i<=n)
	{
		c[i]+=x;
		i+=lowbit(i);
	}
}
int sum(int x)
{
	int ans=0;
	while(x)
	{
		ans+=c[x];
		x-=lowbit(x);
	}
	return ans;
}
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	cin>>m;
	for(int i=1;i<=m;i++)
	{
		cin>>v[i].l>>v[i].r;
		v[i].loca=i;
	}
	sort(v+1,v+1+m,cmp);
	int next=1;
	for(int i=1;i<=m;i++)
	{
		for(int j=next;j<=v[i].r;j++)
		{
			if(d[a[j]]) add(d[a[j]],-1);
			add(j,1);
			d[a[j]]=j;
		}
		next=v[i].r+1;
		ans[v[i].loca]=sum(v[i].r)-sum(v[i].l-1);
	}
	for(int i=1;i<=m;i++) cout<<ans[i]<<endl;
	return 0;
}
posted @ 2026-07-16 20:08  wang7979  阅读(5)  评论(0)    收藏  举报