扫描线——学习笔记

扫描线是一种用来处理矩形相交的面积问题的算法
渐近时间复杂度约为 O ( n l o g n ) O(nlogn) O(nlogn)


Q1.

在坐标系给定n个矩形(以左下/右上角坐标给出)
这些矩形面积的并

例如下图
n=2
矩形1: (1,1) (3,3)
矩形2: (2,2) (4,4)
这里写图片描述

A1.

扫描线的过程大致可以描述为
整个面积并 以n个矩形的 2n条纵边为界 分割为2n-1个部分求解

即上图中我们作如下分割求解
这里写图片描述
我们要求的面积就是 a n s = S 1 + S 2 + S 3 ans=S1+S2+S3 ans=S1+S2+S3

再细致一点讲
我们记录每条纵边按x坐标升序排序
依次遍历每条纵边边,同时记录当前纵边覆盖的y轴总长度len
遍历到的纵边为一个矩形的左边界时,len增加,反之为右边界时,len减小
根据当前遍历到的纵边 i i i更新完len后
a n s + = ( x i + 1 − x i ) ∗ l e n ans+=(x_{i+1}-x_i)*len ans+=(xi+1xi)len x i + 1 x_{i+1} xi+1下一条扫描线的横坐标, x i x_i xi为当前扫描线横坐标)

最关键的问题来了,len是如何更新的呢
设一个矩形表示为 ( x 1 , y 1 ) ( x 2 , y 2 ) (x_1,y_1)(x_2,y_2) (x1,y1)(x2,y2)
我们记录这个矩形的扫描线两个四元组
( x 1 , y 1 , y 2 , k = 1 ) ( x 2 , y 1 , y 2 , k = − 1 ) (x_1,y_1,y_2,k=1)(x_2,y_1,y_2,k=-1) (x1,y1,y2,k=1)(x2,y1,y2,k=1)其中左边界记录的扫描线k=1,右边界则为-1
最后我们共记录了2n条扫描线

定义 c o v [ i ] = v cov[i]=v cov[i]=v表示 y y y轴上区间 [ i , i + 1 ] [i,i+1] [i,i+1]共被覆盖了 v v v
将这些扫描线按x升序排序
设当前遍历到的扫描线为 ( x i , y i , 1 , y i , 2 , k ) (x_i,y_{i,1},y_{i,2},k) (xi,yi,1,yi,2,k)
我们令 c o v [ y i , 1 ] cov[y_{i,1}] cov[yi,1]~ c o v [ y i , 2 − 1 ] cov[y_{i,2}-1] cov[yi,21]都加k,即更新此时的覆盖情况
那么此时有 l e n = ∑ c o v [ i ] > 0 1 len=\sum_{cov[i]>0}1 len=cov[i]>01,更新 a n s + = ( x i + 1 − x i ) ∗ l e n ans+=(x_{i+1}-x_i)*len ans+=(xi+1xi)len

对cov数组的更新显然可以用线段树优化
用len[p]表示结点p对应的区间内被覆盖的总长度

传统的延迟标记作用是查询带标记结点的子节点时下推更新
而注意到这里每次只查询len[1]的值(即根结点的值),所以不用延迟标记
对某个结点修改cov时直接自下向上更新,进行如下更新

void pushup(int p,int s,int t)
{
	if(cov[p]>0) len[p]=t+1-s;
	else if(s==t) len[p]=0;
	else len[p]=len[p<<1]+len[p<<1|1];
}

void update(int ll,int rr,int s,int t,int p,int w)//调用时应传入区间[y1,y2-1]
{
	if(ll<=s&&t<=rr){ cov[p]+=w; pushup(p,s,t); return;}
	int mid=s+t>>1;
	if(ll<=mid) update(ll,rr,s,mid,p<<1,w);
	if(rr>mid) update(ll,rr,mid+1,t,p<<1|1,w);
	pushup(p,s,t);
}

修改完后当前的总覆盖区间就是len[rt]


由于扫描线的题一般y都很大,且存在浮点数,所以要对y进行离散化

( v a l [ y i ] val[y_i] val[yi]表示 y i y_i yi离散化后的数值, p o s [ i ] pos[i] pos[i]表示 i i i对应的原数值)
离散化后对于四元组 ( x i , y i , 1 , y i , 2 , k ) (x_i,y_{i,1},y_{i,2},k) (xi,yi,1,yi,2,k)
我们的更新区间为 [ v a l [ y i , 1 ] , v a l [ y i , 2 ] − 1 ] [val[y_{i,1}],val[y_{i,2}]-1] [val[yi,1],val[yi,2]1]

而对应的上推变为

void pushup(int p,int s,int t)
{
	if(cov[p]>0) len[p]=pos[t+1]-pos[s];
	else if(s==t) len[p]=0;
	else len[p]=len[p<<1]+len[p<<1|1];
}

扫描线应用

POJ - 1151 Atlantis【扫描线】
#include<iostream>
#include<cmath>
#include<algorithm>
#include<queue>
#include<cstring>
#include<cstdio>
using namespace std;
typedef double dd;

int read()
{
    int x=0,f=1;
    char ss=getchar();
    while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
    while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
    return x*f;
}

const int maxn=2010;
int n,cs;
int tot,cnt;
struct Line{dd x,y1,y2;int k;}line[maxn];
bool cmp(Line a,Line b){return a.x<b.x;}
dd a[maxn],pos[maxn];
dd len[maxn<<2],ans;
int cov[maxn<<2];

void init()
{
    cnt=tot=0; ans=0;
    memset(len,0,sizeof(len));
    memset(cov,0,sizeof(cov));
}

void pushup(int p,int s,int t)
{
	if(cov[p]>0) len[p]=pos[t+1]-pos[s];
	else if(s==t) len[p]=0;
	else len[p]=len[p<<1]+len[p<<1|1];
}

void update(int ll,int rr,int s,int t,int p,int v)
{
	if(ll<=s&&t<=rr){ cov[p]+=v; pushup(p,s,t); return;}
	int mid=s+t>>1;
	if(ll<=mid) update(ll,rr,s,mid,p<<1,v);
	if(rr>mid) update(ll,rr,mid+1,t,p<<1|1,v);
	pushup(p,s,t);
}

int main()
{
    while(scanf("%d",&n)!=EOF)
    {
        if(n==0) break; init();
        for(int i=1;i<=n;i++)
        {
            dd x1,y1,x2,y2;
            scanf("%lf%lf%lf%lf",&x1,&y1,&x2,&y2);

            line[++tot].x=x1; line[tot].k=1;
			line[tot].y1=y1;  line[tot].y2=y2; 
			
			a[tot]=y1;
			
			line[++tot].x=x2; line[tot].k=-1;
			line[tot].y1=y1;  line[tot].y2=y2; 
			
			a[tot]=y2;
        }
        
        sort(a+1,a+1+tot);
        for(int i=1;i<=tot;++i)
		if(i==1||a[i]!=a[i-1])
		pos[++cnt]=a[i];
        
        sort(line+1,line+1+tot,cmp); 
        for(int i=1;i<tot;i++)
        {
            int ll=lower_bound(pos+1,pos+1+cnt,line[i].y1)-pos;
            int rr=lower_bound(pos+1,pos+1+cnt,line[i].y2)-pos;
            
			update(ll,rr-1,1,cnt,1,line[i].k);
            ans+=len[1]*(line[i+1].x-line[i].x);
        }
        
        printf("Test case #%d\n",++cs);
		printf("Total explored area: %.2f\n\n",ans);
    }
    return 0;
}



HDU - 1255 覆盖的面积【扫描线】

多个矩形的面积交,考察对扫描线的理解

l e n [ p ] [ 0 ] len[p][0] len[p][0]记录 结点p对应区间内至少被覆盖一次的长度
l e n [ p ] [ 1 ] len[p][1] len[p][1]记录 结点p对应区间内至少被覆盖两次的长度

求面积并的上推代码为

void pushup(int p,int s,int t)
{
	if(cov[p]>0) len[p]=pos[t+1]-pos[s];
	else if(s==t) len[p]=0;
	else len[p]=len[p<<1]+len[p<<1|1];
}

我们在此基础上扩展
加入 c o v [ p ] ≥ 2 cov[p]\geq 2 cov[p]2的情况
并在原来三种情况的基础上加入 l e n [ ] [ 1 ] len[][1] len[][1]的更新

void pushup(int p,int s,int t)
{
	if(cov[p]>=2) len[p][0]=len[p][1]=pos[t+1]-pos[s];//加入cov[p]>=2的情况
	else if(cov[p]==1)
	{
		len[p][0]=pos[t+1]-pos[s];
		if(s==t) len[p][1]=0;//加入len[1][]的更新
		else len[p][1]=len[p<<1][0]+len[p<<1|1][0];
	}
	else if(s==t) len[p][0]=len[p][1]=0;//原来只有len[0][p]=0;
	else{
		len[p][0]=len[p<<1][0]+len[p<<1|1][0];//cov[p]==0也加入len[1][]的更新
		len[p][1]=len[p<<1][1]+len[p<<1|1][1];
	}
}

完整代码

#include<iostream>
#include<cmath>
#include<algorithm>
#include<queue>
#include<cstring>
#include<cstdio>
using namespace std;
typedef double dd;

int read()
{
    int x=0,f=1;
    char ss=getchar();
    while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
    while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
    return x*f;
}

const int maxn=2010;
int T,n;
int tot,cnt;
struct Line{dd x,y1,y2;int k;}line[maxn];
bool cmp(Line a,Line b){return a.x<b.x;}
dd a[maxn],pos[maxn];
dd len[maxn<<2][2],ans;
int cov[maxn<<2];

void init()
{
    cnt=tot=0; ans=0;
    memset(len,0,sizeof(len));
    memset(cov,0,sizeof(cov));
}

void pushup(int p,int s,int t)
{
	if(cov[p]>=2) len[p][0]=len[p][1]=pos[t+1]-pos[s];
	else if(cov[p]==1)
	{
		len[p][0]=pos[t+1]-pos[s];
		if(s==t) len[p][1]=0;
		else len[p][1]=len[p<<1][0]+len[p<<1|1][0];
	}
	else if(s==t) len[p][0]=len[p][1]=0;
	else{
		len[p][0]=len[p<<1][0]+len[p<<1|1][0];
		len[p][1]=len[p<<1][1]+len[p<<1|1][1];
	}
}

void update(int ll,int rr,int s,int t,int p,int v)
{
	if(ll<=s&&t<=rr){ cov[p]+=v; pushup(p,s,t); return;}
	int mid=s+t>>1;
	if(ll<=mid) update(ll,rr,s,mid,p<<1,v);
	if(rr>mid) update(ll,rr,mid+1,t,p<<1|1,v);
	pushup(p,s,t);
}

int main()
{
    T=read();
	while(T--)
    {
        n=read(); init();
        for(int i=1;i<=n;i++)
        {
            dd x1,y1,x2,y2;
            scanf("%lf%lf%lf%lf",&x1,&y1,&x2,&y2);

            line[++tot].x=x1; line[tot].k=1;
			line[tot].y1=y1;  line[tot].y2=y2; 
			
			a[tot]=y1;
			
			line[++tot].x=x2; line[tot].k=-1;
			line[tot].y1=y1;  line[tot].y2=y2; 
			
			a[tot]=y2;
        }
        
        sort(a+1,a+1+tot);
        for(int i=1;i<=tot;++i)
		if(i==1||a[i]!=a[i-1])
		pos[++cnt]=a[i];
        
        sort(line+1,line+1+tot,cmp); 
        for(int i=1;i<tot;i++)
        {
            int ll=lower_bound(pos+1,pos+1+cnt,line[i].y1)-pos;
            int rr=lower_bound(pos+1,pos+1+cnt,line[i].y2)-pos;
            
			update(ll,rr-1,1,cnt,1,line[i].k);
            ans+=len[1][1]*(line[i+1].x-line[i].x);
        }
		printf("%.2lf\n",ans);
    }
    return 0;
}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值