扫描线(一)——求矩形面积并

前言

扫描线应该是一个很有用的算法。

它有许多用途,比较经典的应该就是用来求矩形面积并


什么是矩形面积并

或许你会问,什么是矩形面积并?

在一个平面上,有若干个矩形,它们覆盖的总面积就是矩形面积并(重叠部分只算一次)。

要求矩形面积并,我们就可以用扫描线。


离散化

首先,我们要将这张图离散化预处理一下。

离散化的过程应该比较简单,将每个节点的坐标全部存在一个数组中,排序+去重之后,就形成了一个离散化之后的数组,把它们加入一个 m a p map 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 x x坐标发生了变化,我们就要更新 a n s ans ans了,需将 a n s ans ans加上 x x x轴的变化量×当前被覆盖的长度

  • 对于当前操作的边,我们又分两种讨论:

    • 对于一条开始的边( f l a g = 1 flag=1 flag=1),我们就将这条边所覆盖的节点被覆盖次数加 1 1 1
    • 对于一条结束的边( f l a g = − 1 flag=-1 flag=1),我们就将这条边所覆盖的节点被覆盖次数减 1 1 1

    综上所述,我们只需将这条边所覆盖的节点被覆盖次数加上 f l a g flag flag即可。

不难发现,在最坏情况下时间复杂度是 O ( n 2 ) O(n^2) O(n2)的。

因此,就需要优化。


线段树优化

其实这题的优化也很简单,直接用线段树维护即可。

对于每个节点,需要维护这个节点所代表的区间内被覆盖的长度 S u m [ i ] Sum[i] Sum[i]以及这个区间被覆盖的次数 E x i s t [ i ] Exist[i] Exist[i]

不难发现,对于某一时刻,被覆盖的总长度应为 S u m [ 1 ] Sum[1] Sum[1](因为 1 1 1号节点代表的区间为 [ 1... n ] [1...n] [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 ( n l o g n ) O(nlogn) O(nlogn)了。


代码

#include
#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;
}

例题

【HDU1542】Atlantis

Link

【HDU1542】Atlantis 的题解详见博客【HDU1542】Atlantis (扫描线的经典运用)

你可能感兴趣的:(扫描线)