HDU 1542 & POJ 1151 Atlantis【线段树扫描线】

本文介绍了一种使用扫描线算法来解决多个矩形区域重叠问题的方法,并提供了两种不同的实现思路及对应的代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

扫描线用于求若干个相交矩形的面积并,因为用几何方法在相交的情况复杂的时候难以计算。下面给2个矩形的情况做例子

给定2个矩形对角的点坐标,则下图的面积为图2的三种颜色面积和

图1

图2    

现在假设有一条竖着的线从左边往右边扫,扫到矩阵的边时,若是入边,将边这一个区间的cover属性+1,出边则-1

图中圈圈的数字是第几条边,花括号的数是cover值。如图扫描线经过②号边(入边)时最上面部分是1,下面还是1,中间是2,到出边就又减了


图3

则扫描到②号线时,计算一次面积,长为②号线的x减去①号线(前面一条线)的x,高为[Ymin,Ymax]中所有cover为正的高度和

(在这里即为①号线的Yup,Ydown差)。这样重复下去即求得面积和。

放线段树上,区间端点为各条边的上下2个y值,每个区间也都有个cover属性表示覆盖的次数,如上所提。

图4

假设点为[1,1],[2,2],[3,3],[4,4](按图1的情况),则有上图。①号线为[1,3],②为[2,4]...

看这图的第二行,前面一个区间是[1,2],后面是[2,4],而不是{3,4],是因为在矩形里,区间是连续的,[2,3]是存在的,如果为mid+1则[2,3]会被弄没。

第三行:[1,2]没有再继续分成[1,1]和[2,2],是因为这样的点区间没有意义,一条线当然要两个点

然而区间端点并不一定是这么好的整数,即可能是浮点数,还可能坐标很大,所以需要对这些区间的Y值离散化(这里不解释,可见),离散化后就跟上图的1,2,3,4一样了。


NEUQ扫描线-视频链接

HDU 1542 Atlantis 也是POJ 1151 Atlantis  不过POJ用G++交的话记得是%.2f而不是%.2lf

思路一:保存区间时区间端点都用double类型,避免离散化的麻烦。然后只对叶区间处理,如扫描图3里的第②条线时,

面积为②号线上cover为1、为2、为1的三段这和。

这种情况下求每一部分面积时:长为当前线的x减去前面一条线的x,代码里的tree[i].x保存上一条线的x值,可以这样做是因为每次求第i条线时,

上一条线已经扫过,在建树时设置x为-1表示没有上一条。

#include <cstdio>
#include <cmath>
#include <cstring>
#include <string>
#include <set>
#include <map>
#include <stack>
#include <queue>
#include <vector>
#include <iostream>
#include <iomanip>
#include <algorithm>
using namespace std;
#define ll __int64
#define INF 0x7FFFFFFF
#define INT_MIN -(1<<31)
#define eps 10^(-6)
#define Q_CIN ios::sync_with_stdio(false);
#define REP( i , n ) for ( int i = 0 ; i < n ; ++ i )
#define FOR( i , a , b ) for ( int i = a ; i <= b ; ++ i )
#define CLR( a , x ) memset ( a , x , sizeof (a) );
#define RE freopen("1.in","r",stdin);
#define WE freopen("1.out","w",stdout);
#define MOD 10009
#define debug(x) cout<<#x<<":"<<(x)<<endl;
//#define lson i<<1,l,m
//#define rson i<<1|1,m+1,r

const int maxn=105;
const int maxN=100010;
double y[maxn<<1];
struct Line
{
    double x;
    double y_up,y_down;
    int dir;        //1为入边,-1为出边
}L[maxn<<1];         //2*n的边数
struct Tree
{
    double y_up,y_down,x;       //x为上一条线段的x值
    int cover;
    bool leaf;          //是否为叶区间
}tree[maxN<<2];
bool cmp(Line a,Line b)
{
    return a.x<b.x;
}
void build(int i,int l,int r)
{
    tree[i].x=-1;tree[i].y_up=y[r];tree[i].y_down=y[l];tree[i].cover=0;tree[i].leaf=false;
    if(l+1==r)      //[1,2]这样的区间就不分下去了
    {
        tree[i].leaf=true;
        return;
    }
    int m=(l+r)>>1;
    build(i<<1,l,m);
    build(i<<1|1,m,r);      //区间是连续的,所以右区间的l要与左区间的r相同
}
double update(int i,double x,double y_down,double y_up,int dir)
{
    if(y_down>=tree[i].y_up||y_up<=tree[i].y_down)    //如在[1,2]里查询[3,4]直接返回0
        return 0.0;
    if(tree[i].leaf)        //非叶区间不管
    {
        if(tree[i].cover>0)     //有覆盖到,求面积
        {
            double xx=tree[i].x;
            double ch=(x-xx)*(tree[i].y_up-tree[i].y_down);
            tree[i].cover+=dir;     //边覆盖次数
            tree[i].x=x;
            return ch;
        }else
        {
            tree[i].x=x;            //设x为x
            tree[i].cover+=dir;
            return 0.0;
        }
    }
    double s1=update(i<<1,x,y_down,y_up,dir);
    double s2=update(i<<1|1,x,y_down,y_up,dir);
    return s1+s2;
}
int main()
{
    int n;
    double x1,x2,y1,y2;
    int te=1;
//    RE
    while(cin>>n,n)
    {
        int cnt=1;
        FOR(i,1,n)
        {
            cin>>x1>>y1>>x2>>y2;
            L[cnt].x=x1;L[cnt].y_down=y1;L[cnt].y_up=y2;L[cnt].dir=1;y[cnt]=y1;cnt++;
            L[cnt].x=x2;L[cnt].y_down=y1;L[cnt].y_up=y2;L[cnt].dir=-1;y[cnt]=y2;cnt++;
        }
//        sort(L+1,L+cnt,cmp);
//        sort(y+1,y+cnt);
//        build(1,1,cnt-1);
        sort(L+1,L+2*n+1,cmp);      //线段按x排序
        sort(y+1,y+2*n+1);          //y数组升序
        build(1,1,2*n);
        double ans=0.0;
        FOR(i,1,2*n)
            ans+=update(1,L[i].x,L[i].y_down,L[i].y_up,L[i].dir);
        cout<<"Test case #"<<te++<<endl;
        cout<<setiosflags(ios::fixed);
        cout<<"Total explored area: "<<setprecision(2)<<ans<<endl<<endl;
    }
    return 0;
}

思路二(主流一点):给每个区间增加一个len属性,表示该区间能与下一条线段计算用的高度,然后顺势每次插入线段就用整个大区间(树)与下一条能算的高度求出面积。

(区间存端点有点离散化的思想,把区间存的都变成了常规的整数l,r)如插入②时,因为有y数组排序,插入的区间就是【2,4】,则【2,4】能与下一条计算的是y[4]-y[2],然后通过pushup把【1,2】能计算的也加进来,就可以求②和③中间的面积了。因为区间的l,r是int,所以插入的线段的区间需要用二分在y数组里找出下标。

#include <cstdio>
#include <cmath>
#include <cstring>
#include <string>
#include <set>
#include <map>
#include <stack>
#include <queue>
#include <vector>
#include <iostream>
#include <iomanip>
#include <algorithm>
using namespace std;
#define ll __int64
#define INF 0x7FFFFFFF
#define INT_MIN -(1<<31)
#define eps 10^(-6)
#define Q_CIN ios::sync_with_stdio(false);
#define REP( i , n ) for ( int i = 0 ; i < n ; ++ i )
#define FOR( i , a , b ) for ( int i = a ; i <= b ; ++ i )
#define CLR( a , x ) memset ( a , x , sizeof (a) );
#define RE freopen("1.in","r",stdin);
#define WE freopen("1.out","w",stdout);
#define MOD 10009
#define debug(x) cout<<#x<<":"<<(x)<<endl;
//#define lson i<<1,l,m
//#define rson i<<1|1,m+1,r

const int maxn=105;
const int maxN=100010;
double y[maxn<<1];
int ulen;
struct Line
{
    double x;
    double y_up,y_down;
    int dir;        //1为入边,-1为出边
}L[maxn<<1];         //2*n的边数
struct Tree
{
    int l,r;
    int cover;
    double len;
}tree[maxN<<2];
bool cmp(Line a,Line b)
{
    return a.x<b.x;
}
void build(int i,int l,int r)
{
    tree[i].l=l;
    tree[i].r=r;
    tree[i].cover=0;
    tree[i].len=0.0;
    if(l+1==r)
        return;
    int m=(l+r)>>1;
    build(i<<1,l,m);
    build(i<<1|1,m,r);
}
int find(double x)
{
    int l=1,r=ulen;
    while(l<=r)
    {
        int mid=(l+r)/2;
        if(fabs(y[mid]-x)<1e-6) //        if(y[mid]==x)
            return mid;
        if(y[mid]>x)
            r=mid-1;
        else
            l=mid+1;
    }
    return l;
}
void fun(int i)     //包含pushup和完全覆盖的情况下更新
{
    if(tree[i].cover>0)     //有覆盖则整一段可以做为下一条线段计算用
        tree[i].len=(y[tree[i].r]-y[tree[i].l]);
    else if(tree[i].l+1==tree[i].r)     //叶子
        tree[i].len=0;
    else tree[i].len=tree[i<<1].len+tree[i<<1|1].len;
}
void update(int i,int l,int r,int dir)
{
    if(tree[i].l>r||tree[i].r<l)        //完全不搭
        return;
    if(l<=tree[i].l&&tree[i].r<=r)  //完全包含
    {
        tree[i].cover+=dir;
        fun(i);
        return;
    }
    update(i<<1,l,r,dir);
    update(i<<1|1,l,r,dir);
    fun(i);
}
int main()
{
    int n;
    double x1,x2,y1,y2;
    int te=1;
    RE
    while(cin>>n,n)
    {
        int cnt=1;
        FOR(i,1,n)
        {
            cin>>x1>>y1>>x2>>y2;
            L[cnt].x=x1;L[cnt].y_down=y1;L[cnt].y_up=y2;L[cnt].dir=1;y[cnt]=y1;cnt++;
            L[cnt].x=x2;L[cnt].y_down=y1;L[cnt].y_up=y2;L[cnt].dir=-1;y[cnt]=y2;cnt++;
        }       //cnt==2*n+1
        sort(L+1,L+cnt,cmp);      //线段按x排序
        sort(y+1,y+cnt);          //y数组保存y值,升序,才与线段树区间对应
        ulen=1;
        y[0]=-100005.0;
        for(int i=1;i<cnt;i++)      //y数组去重,默认y[0]与y[1]不同
        {
            if(y[i]!=y[i-1])
            {
                y[ulen++]=y[i];
            }
        }
        ulen--;         //  !!!!!
        build(1,1,ulen);
        double ans=0.0;
        FOR(i,1,cnt-1)
        {                       //在新线的y区间插入新cover(dir)
            int a=find(L[i].y_down);    //在y数组里找y_down的下标
            int b=find(L[i].y_up);
            update(1,a,b,L[i].dir);             //每插入一条就把这条与下一条能算的算起来
            ans+=tree[1].len*(L[i+1].x-L[i].x); //tree[1].len为整个树能与i+1条线计算的,因为有pushup操作
        }
        cout<<"Test case #"<<te++<<endl;
        cout<<setiosflags(ios::fixed);
        cout<<"Total explored area: "<<setprecision(2)<<ans<<endl<<endl;
    }
    return 0;
}
既然是整型的l,r,那就按常规的线段树飘逸去吧,把tree结构体省了,添加参数代替。

#include <cstdio>
#include <cmath>
#include <cstring>
#include <string>
#include <set>
#include <map>
#include <stack>
#include <queue>
#include <vector>
#include <iostream>
#include <iomanip>
#include <algorithm>
using namespace std;
#define ll __int64
#define INF 0x7FFFFFFF
#define INT_MIN -(1<<31)
#define eps 10^(-6)
#define Q_CIN ios::sync_with_stdio(false);
#define REP( i , n ) for ( int i = 0 ; i < n ; ++ i )
#define FOR( i , a , b ) for ( int i = a ; i <= b ; ++ i )
#define CLR( a , x ) memset ( a , x , sizeof (a) );
#define RE freopen("1.in","r",stdin);
#define WE freopen("1.out","w",stdout);
#define MOD 10009
#define debug(x) cout<<#x<<":"<<(x)<<endl;

#define lson i<<1,l,m
#define rson i<<1|1,m,r

const int maxn=105;
const int maxN=100010;
double y[maxn<<1];
int ulen;
struct Line
{
    double x;
    double y_up,y_down;
    int dir;        //1为入边,-1为出边
    bool operator <(const struct Line &another)const{
        return x<another.x;
    }
}L[maxn<<1];
int cover[maxN<<2];
double len[maxN<<2];

void build(int i,int l,int r)
{
    cover[i]=0;len[i]=0.0;
    if(l+1==r)
        return;
    int m=(l+r)>>1;
    build(lson);
    build(rson);
}
void fun(int i,int l,int r)     //包含pushup和完全覆盖的情况下更新
{
    if(cover[i]>0)      len[i]=(y[r]-y[l]);
    else if(l+1==r) len[i]=0;
    else len[i]=len[i<<1]+len[i<<1|1];
}
void update(int i,int l,int r,int L,int R,int dir)
{
    if(L<=l&&r<=R)
    {
        cover[i]+=dir;
        fun(i,l,r);
        return;
    }
    int m=(l+r)>>1;
    if(L<m)         //注意这里没有等号,如[1,4]里插入[2,4],不能去左子区间[1,2]找
        update(lson,L,R,dir);
    if(R>m)
        update(rson,L,R,dir);
    fun(i,l,r);
}
int main()
{
    int n;
    double x1,x2,y1,y2;
    int te=1;
//    RE
    while(cin>>n,n)
    {
        int cnt=1;
        FOR(i,1,n)
        {
            cin>>x1>>y1>>x2>>y2;
            L[cnt].x=x1;L[cnt].y_down=y1;L[cnt].y_up=y2;L[cnt].dir=1;y[cnt]=y1;cnt++;
            L[cnt].x=x2;L[cnt].y_down=y1;L[cnt].y_up=y2;L[cnt].dir=-1;y[cnt]=y2;cnt++;
        }       //cnt==2*n+1
        sort(L+1,L+cnt);
        sort(y+1,y+cnt);
        ulen=1;
        y[0]=-100005.0;
        for(int i=1;i<cnt;i++)
            if(y[i]!=y[i-1])
                y[ulen++]=y[i];
        ulen--;
        build(1,1,ulen);
        double ans=0.0;

        FOR(i,1,cnt-1)
        {
            int a=lower_bound(y+1,y+ulen,L[i].y_down)-y;
            int b=lower_bound(y+1,y+ulen,L[i].y_up)-y;//因为知道元素一定存在
            update(1,1,ulen,a,b,L[i].dir);
            ans+=len[1]*(L[i+1].x-L[i].x);
        }
        printf("Test case #%d\nTotal explored area: %.2lf\n\n",te++,ans);
    }
    return 0;
}



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值