扫描线模板
前言
其实之前是学过扫描线的,但在此进行一个系统梳理(目前仅涉及到最基本的东西)
扫描线
扫描线是什么
拿出最经典的图
发现就是扫描矩形的边(同一方向的),把矩形的一边当入边,另一边当出边,从下往上扫(从入边方向扫),将图形分成若干矩形(为区分暂且叫做矩块)。
扫到一个入/出边就查询当前矩块的长(平行于扫描线的边),宽用两个边之间的距离来算,并将该入/出边进行维护。
矩块的长的维护可以用树状数组或者线段树来维护,这里先叙述线段树的做法:
- 扫到入边,对区间进行增加
- 扫到出边,对区间进行删除
- 与传统线段树不同点在于,该线段树的叶子节点不是“点”,而是单位区间,因此算区间长度时应该注意,对右端点/左端点进行+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;
}

浙公网安备 33010602011771号