扫描线(一)——求矩形面积并
前言
扫描线应该是一个很有用的算法。
它有许多用途,比较经典的应该就是用来求矩形面积并。
什么是矩形面积并
或许你会问,什么是矩形面积并?
在一个平面上,有若干个矩形,它们覆盖的总面积就是矩形面积并(重叠部分只算一次)。
要求矩形面积并,我们就可以用扫描线。
离散化
首先,我们要将这张图离散化预处理一下。
离散化的过程应该比较简单,将每个节点的坐标全部存在一个数组中,排序+去重之后,就形成了一个离散化之后的数组,把它们加入一个\(map\)中即可(或直接二分)。
代码如下:
for(i=1;i<=n;++i) cin>>x1>>y1>>x2>>y2,xy[(i<<2)-3]=x1,xy[(i<<2)-2]=y1,xy[(i<<2)-1]=y2,xy[i<<2]=x2;//将每一个点的坐标全部存在一个数组中
sort(xy+1,xy+(n<<2)+1);//排序
for(i=1;i<=n<<2;++i)//枚举每一个值
if(!p[xy[i]]) f[p[xy[i]]=++cnt]=xy[i];//p存储离散化后的值,f存储原来的值,需去重
如何存储一个矩形
接下来,我们要考虑如何存储一个读入的矩形。
不难发现,其实我们只需要记录每个矩形的左右两条边即可,其中一条标记为正,一条标记为负,表示开始与结束。
代码如下:
struct Square//一个结构体
{
int flag,nx,ny1,ny2;//flag记录这条边的类型(1或-1,分别表示开始与结束),nx,ny1,ny2分别存储x,y1,y2离散化后的值
double x,y1,y2;//x记录这条边x轴上的坐标,y1,y2分别记录这条边在y轴上的起点与终点
}a[2*N+5];
for(i=1;i<=n;++i) cin>>x1>>y1>>x2>>y2,a[(i<<1)-1]=(Square){1,0,0,0,x1,y1,y2},a[i<<1]=(Square){-1,0,0,0,x2,y1,y2};//读入并存储每一个矩形
//离散化的过程(见上)
for(i=1;i<=n<<1;++i)//将每条边的坐标更新为离散化后的坐标
a[i].nx=p[a[i].x],a[i].ny1=p[a[i].y1],a[i].ny2=p[a[i].y2];
扫描线的核心过程
做完了以上的一系列处理,我们就可以开始用扫描线来扫描了。
-
首先,我们按照从左往右的顺序枚举每一条边。
-
每当\(x\)坐标发生了变化,我们就要更新\(ans\)了,需将\(ans\)加上 \(x\)轴的变化量×当前被覆盖的长度。
-
对于当前操作的边,我们又分两种讨论:
- 对于一条开始的边(\(flag=1\)),我们就将这条边所覆盖的节点被覆盖次数加\(1\)
- 对于一条结束的边(\(flag=-1\)),我们就将这条边所覆盖的节点被覆盖次数减\(1\)
综上所述,我们只需将这条边所覆盖的节点被覆盖次数加上\(flag\)即可。
不难发现,在最坏情况下时间复杂度是\(O(n^2)\)的。
因此,就需要优化。
线段树优化
其实这题的优化也很简单,直接用线段树维护即可。
对于每个节点,需要维护这个节点所代表的区间内被覆盖的长度\(Sum[i]\)以及这个区间被覆盖的次数\(Exist[i]\)。
不难发现,对于某一时刻,被覆盖的总长度应为\(Sum[1]\)(因为\(1\)号节点代表的区间为\([1...n]\)),因此我们只需写区间修改操作即可。
代码如下:
inline void PushUp(int l,int r,int rt)//从子节点上传信息
{
if(Exist[rt]) Sum[rt]=f[r+1]-f[l];//如果当前节点本身就被覆盖了,那么这个区间被覆盖的总长度就是f[r+1]-f[l]
else if(l==r) Sum[rt]=0;//不然,如果这个区间只有一个节点,那么被覆盖的总长度为0
else Sum[rt]=Sum[rt<<1]+Sum[rt<<1|1];//否则,被覆盖的总长度就等同于左右子节点被覆盖的总长度之和
}
inline void Update(int l,int r,int rt,int L,int R,int v)//线段树的区间修改
{
if(L>R) return;//如果修改区间的左边界大于右边界,就退出修改
if(L<=l&&r<=R) return (void)(Exist[rt]+=v,PushUp(l,r,rt));//如果当前区间被修改区间包含,就更新当前节点信息
register int mid=l+r>>1;
if(L<=mid) Update(l,mid,rt<<1,L,R,v);//修改左儿子
if(R>mid) Update(mid+1,r,rt<<1|1,L,R,v);//修改右儿子
PushUp(l,r,rt);//从子节点上传信息
}
这样,就可以将代码时间复杂度降为\(O(nlogn)\)了。
代码
#include<bits/stdc++.h>
#define N 100
using namespace std;
int n,cnt,Exist[N<<4];
double Sum[N<<4],xy[(N<<2)+5];
struct Square//一个结构体
{
int flag,nx,ny1,ny2;//flag记录这条边的类型(1或-1,分别表示开始与结束),nx,ny1,ny2分别存储x,y1,y2离散化后的值
double x,y1,y2;//x记录这条边x轴上的坐标,y1,y2分别记录这条边在y轴上的起点与终点
}a[2*N+5];
map<double,int> p;map<int,double> f;//p存储离散化后的值,f存储原来的值
//线段树模板--------------------------------------------------------------------
inline void PushUp(int l,int r,int rt)//从子节点上传信息
{
if(Exist[rt]) Sum[rt]=f[r+1]-f[l];//如果当前节点本身就被覆盖了,那么这个区间被覆盖的总长度就是f[r+1]-f[l]
else if(l==r) Sum[rt]=0;//不然,如果这个区间只有一个节点,那么被覆盖的总长度为0
else Sum[rt]=Sum[rt<<1]+Sum[rt<<1|1];//否则,被覆盖的总长度就等同于左右子节点被覆盖的总长度之和
}
inline void Update(int l,int r,int rt,int L,int R,int v)//线段树的区间修改
{
if(L>R) return;//如果修改区间的左边界大于右边界,就退出修改
if(L<=l&&r<=R) return (void)(Exist[rt]+=v,PushUp(l,r,rt));//如果当前区间被修改区间包含,就更新当前节点信息
register int mid=l+r>>1;
if(L<=mid) Update(l,mid,rt<<1,L,R,v);//修改左儿子
if(R>mid) Update(mid+1,r,rt<<1|1,L,R,v);//修改右儿子
PushUp(l,r,rt);//从子节点上传信息
}
//----------------------------------------------------------------------------
inline bool cmp(Square x,Square y)//比较两条边
{
return x.nx<y.nx;//返回x轴坐标较小的
}
int main()
{
register int i;register double x1,x2,y1,y2;
for(i=1;i<=n;++i) cin>>x1>>y1>>x2>>y2,a[(i<<1)-1]=(Square){1,0,0,0,xy[(i<<2)-3]=x1,xy[(i<<2)-2]=y1,xy[(i<<2)-1]=y2},a[i<<1]=(Square){-1,0,0,0,xy[i<<2]=x2,y1,y2};
//一个离散化的过程--------------------------------------------------------------------
sort(xy+1,xy+(n<<2)+1);//排序
for(i=1;i<=n<<2;++i)//枚举每一个值
if(!p[xy[i]]) f[p[xy[i]]=++cnt]=xy[i];
for(i=1;i<=n<<1;++i)//将每条边的坐标更新为离散化后的坐标
a[i].nx=p[a[i].x],a[i].ny1=p[a[i].y1],a[i].ny2=p[a[i].y2];
//--------------------------------------------------------------------------------
sort(a+1,a+(n<<1)+1,cmp),memset(Exist,0,sizeof(Exist)),memset(Sum,0,sizeof(Sum));
int Now=1;double ans=0.0;//Now表示当前扫描到的边的编号,ans记录面积
for(i=1;i<=cnt;++i)//枚举x坐标
{
ans+=(f[i]-f[i-1])*Sum[1];//更新ans
if(a[Now].nx^i) continue;//如果当前边不在扫描到的这一列上,就跳过
while(a[Now].nx==i&&Now<=n<<1)
Update(1,cnt,1,a[Now].ny1,a[Now].ny2-1,a[Now].flag),++Now;//修改,操作下一条边
}
return printf("%.2lf",ans),0;
}
例题
待到再迷茫时回头望,所有脚印会发出光芒