扫描线算法学习-完全解析 HDU 1542 Atlantis

前言

看了好久的扫描线,终于看懂了,想用自己的语言阐述一下,加深理解。也供各位读者学习。

注:学习本文之前首先得会线段树,离散化等操作,这里给出两篇 b l o g blog blog​​供参考:程序设计之离散化线段树入门

例题引入

  • 题目链接

    HDU-1542 Atlantis

  • 题目大意

    给你 n n n​​​个矩形,求它们的矩形面积并,即覆盖在平面上的面积为多少。

    image-20210808165806773

  • 思考

    针对求每个单独矩形的面积,实际上我们是很好解决的。那么对于它们相交的面积,我们不仅很难处理而且非常容易出错,特别是当 n n n​很大的时候无从下手。这就需要用到我们本文所学的扫描线算法了, 其可以使用线段树来实现,一般可以用来解决一些图形的周长,体积和面积问题。

扫描线算法

介绍及演示

顾名思义,就是我们可以用一根假想的扫描线来扫描这些矩形,而每次扫描到边的时候,我们就可以进行答案的计算(即通过和上一个扫描线扫描到边的位置之间的差和所覆盖的宽度成绩来获取分块矩形的面积)。我们用上面的矩形来演示:

image-20210808171403379

扫完全部的矩形之后,我们就可以获取矩形面积并了。

如何实现

注:本文是将扫描线从下往上移的,当然从左往右移也行,只是看法不一样。代码会给出两份,即从下往上,从左往右

可以看出,我们需要知道我们用扫描线分割的矩形的宽度和高度。可以看出,高度其实特别简单计算,就是当前扫描线和扫描线上次扫到边的高度差,那么宽度如何计算呢?这也是扫描线最难理解的一点。这里引入入边和出边的概念。

入边和出边

我们可以将 x x x​​轴上看成一段区间,即 [ x l , x r ] [x_l,x_r] [xl,xr]​(其中 x l x_l xl​为所有矩形中最左端的 x x x​, x r x_r xr​为所有矩形最右端的 x x x​),那么易知,所有矩形都在这个区间范围内,同时,这个区间的分隔点为每个矩形边的 x x x​​​坐标。

那么我们用扫描线扫描的时候实际上就会覆盖一段区间,而当遇到==入边(底边)的时候,就是加入了一个矩形,覆盖区间有可能会增大(因为扫描线进入了该矩形);而当遇到出边(顶边)==时,说明减少了一个矩形,即覆盖区间有可能会减小(因为扫描线离开了该矩形)。那么我们只要遇到边时计算出区间长度即可算出分割的矩形面积了。而这求区间长度问题正好是利用线段树来实现的。

步骤

  1. 将矩形的入边和出边转化为扫描线,则需要保存扫描线的左端点和右端点,即 x l , x r x_l,x_r xl,xr,这样我们才知道区间范围。还需要保存边的性质,即是出边还是入边,由于入边 + + +操作,出边 − - 操作,所以我们可以令 1 1 1为入边, − 1 -1 1​为出边;然后需要保存的就是边的高度了,即两根扫描线的高度即是分割矩形的高度。
  2. 对扫描线按高度进行排序,由于我们要从下往上遍历,所以自然是按高度来的。
  3. 建立线段树维护覆盖区间长度,那么这个时候我们还需要用 c o v e r cover cover​​数组来统计区间覆盖次数,用一个 l e n g t h length length数组统计结点的区间长度。

区间表示注意事项

  1. 由于我们结点保存的是区间,而由于区间左右端点可以是浮点型的,如果我们这样处理会产生误差,且由于区间长度本质分布不均,更不符合我们线段树的设想,所以我们需要将区间离散化,即我们可以先保存所有的区间端点,然后排序通过 u n i q u e unique unique​函数去重,这样每个端点可以用它们的序号来表示,那么查询它们所在区间可以通过值来二分查找序号。这样节省空间且易于操作。

  2. 由于我们线段树维护的是线段的长度而不是点的值,我们划分左右孩子的时候需要注意左孩子的区间右端点等于右孩子左端点。这样才能是区间分割不出现缝隙。

代码实现

  • 从下往上扫
/**
  *@filename:new_矩形面积并
  *@author: pursuit
  *@created: 2021-08-08 14:58
**/
#include <bits/stdc++.h>
#define debug(a) cout << "debug : " << (#a)<< " = " << a << endl;
#define x first
#define y second
using namespace std;

typedef pair<double,double> pdd;
typedef long long ll;
const int N = 4e5 + 10;
const int P = 1e9 + 7;
const int INF = 0x3f3f3f3f;

struct Line{
    double xl,xr;//扫描线的左端和右端。
    double yy;//扫描线的高度。
    int d;//代表是入边还是出边,其中规定底边为入边,顶边为出边。
    bool operator < (const Line &A){
        return yy < A.yy;
    }
    Line(){}
    Line(double _xl,double _xr,double _yy,int _d):xl(_xl),xr(_xr),yy(_yy),d(_d){}
}scan[N];
int cover[N];//区间i的覆盖情况。
double length[N];//端点i的长度。
double xx[N];//存放离散化后的x值。
int n,k,tot;//n个矩形。tot根扫描线。
void pushup(int rt,int l,int r){
    //判断是否完全覆盖该区间。
    if(cover[rt]){
        //更新区间长度。
        length[rt] = xx[r] - xx[l];
    }
    else if(l + 1 == r){
        //由于cover[0] = 0,且为叶子结点。到了叶子结点不可能由子节点贡献覆盖了。
        length[rt] = 0;
    }
    else{
        length[rt] = length[rt << 1] + length[rt << 1 | 1];
    }
}
void update(int rt,int l,int r,int xl,int xr,int d){
    //判断是否完全覆盖了当前结点的区间
    if(xl <= l && xr >= r){
        //就更新这个区间。
        cover[rt] += d;//更新这个区间的覆盖情况。
        pushup(rt,l,r);
        return;
    }
    if(l + 1 == r)return;
    int mid = (l + r) >> 1;
    if(xl <= mid){
        update(rt << 1,l,mid,xl,xr,d);
    }
    if(xr > mid){
        update(rt << 1 | 1,mid,r,xl,xr,d);
    }
    pushup(rt,l,r);
}
void solve(){
    //对扫描线和x排序。
    sort(xx + 1,xx + tot + 1);
    sort(scan + 1,scan + tot + 1);
    //获取去重后的长度。同时也离散化了xx。
    int len = unique(xx + 1,xx + tot + 1) - (xx + 1);
    //初始化。
    fill(cover,cover + N,0);
    fill(length,length + N,0);
    int xl,xr;//获取离散化后的xl和xr。
    double ans = 0;
    for(int i = 1; i <= tot; ++ i){
        //相邻两根扫描线之间的高度差即是高。而length[1]代表的是根节点管辖的区间覆盖宽度,正是宽。
        ans += length[1] * (scan[i].yy - scan[i - 1].yy);
        //将这根扫描线加入到线段树中。
        //查找第一个大于等于查找值的元素,而由于去重了,且一定存在,所以即是其本身下标。
        xl = lower_bound(xx + 1,xx + len + 1,scan[i].xl) - xx;
        xr = lower_bound(xx + 1,xx + len + 1,scan[i].xr) - xx;
        update(1,1,len,xl,xr,scan[i].d);
    }
    printf("Test case #%d\nTotal explored area: %.2f\n\n",k,ans);
}
int main(){	
    k = 0;
    pdd a,b;
    while(~scanf("%d", &n) && n){
        k ++,tot = 0;
        for(int i = 1; i <= n; ++ i){
            scanf("%lf%lf%lf%lf", &a.x, &a.y, &b.x, &b.y);
            scan[++tot] = Line(a.x,b.x,a.y,1);
            xx[tot] = a.x;
            scan[++tot] = Line(a.x,b.x,b.y,-1);
            xx[tot] = b.x;
        }
        solve();
    }
    return 0;
}
  • 从左往右扫
/**
  *@filename:矩形面积并
  *@author: pursuit
  *@created: 2021-08-07 20:33
**/
#include <bits/stdc++.h>
#define debug(a) cout << "debug : " << (#a)<< " = " << a << endl;
#define x first 
#define y second
using namespace std;

typedef pair<double,double> pdd;
typedef long long ll;
const int N = 4e5 + 10;
const int P = 1e9 + 7;
const int INF = 0x3f3f3f3f;

int n,k;
struct Line{
    double yd,yu,xx;//边的y上坐标y下坐标,横坐标。
    int d;//区分入边和出边。入边为1,出边为-1.
    bool operator < (const Line &A){
        return xx < A.xx;
    }
    Line(){}
    Line(double _yd,double _yu,double _xx,int _d):
    yd(_yd),yu(_yu),xx(_xx),d(_d){}
}scan[N];//扫描线。
int tot;//扫描线数量。
int cover[N];//存放i结点对应覆盖情况的值。
double length[N];//存放区间i下的总长度。
double yy[N];//存放离散后的y的值,下标我们用lower_bound进行查找。
void pushup(int rt,int l,int r){
    if(cover[rt]){
        length[rt] = yy[r] - yy[l];
    }
    else if(l + 1 == r){
        length[rt] = 0;//到了叶子结点。
    }
    else{
        length[rt] = length[rt << 1] + length[rt << 1 | 1];
    }
}
void update(int rt,int l,int r,int yl,int yr,int d){
    if(yl <= l && yr >= r){
        //说明区间全部包含在里面。
        cover[rt] += d;//根据出边入边加上相应值。
        pushup(rt,l,r);
        return;
    }
    if(l + 1 == r)return;//到了子节点。
    int mid = (l + r) >> 1;
    if(yl <= mid){
        update(rt << 1,l,mid,yl,yr,d);
    }
    if(yr > mid){
        update(rt << 1 | 1,mid,r,yl,yr,d);
    }
    pushup(rt,l,r);
} 
void solve(){
    sort(yy + 1,yy + tot + 1);//排序。
    sort(scan + 1,scan + 1 + tot);
    //获取去重后的数组。
    int len = unique(yy + 1,yy + tot + 1) - (yy + 1);
    fill(length,length + N,0);
    fill(cover,cover + N,0);
    int yl,yr;
    double ans = 0;//累加矩形面积。
    for(int i = 1; i <= tot; ++ i){
        ans += length[1] * (scan[i].xx - scan[i - 1].xx);
        yl = lower_bound(yy + 1,yy + 1 + len,scan[i].yd) - yy;
        yr = lower_bound(yy + 1,yy + 1 + len,scan[i].yu) - yy; 
        update(1,1,len,yl,yr,scan[i].d);
    }
    printf("Test case #%d\nTotal explored area: %.2f\n\n",k,ans);
}
int main(){	
    pdd a,b;
    while(~scanf("%d", &n) && n){
        k ++;
        tot = 0;
        for(int i = 0; i < n; ++ i){
            //矩形的左下角和右上角坐标。
            scanf("%lf%lf%lf%lf", &a.x, &a.y, &b.x, &b.y);
            //下底扫描线和上底扫描线。
            //即给入边和出边赋值。
            scan[++tot] = Line(a.y,b.y,a.x,1);
            yy[tot] = a.y;
            scan[++tot] = Line(a.y,b.y,b.x,-1);
            yy[tot] = b.y;
        }
        solve();
    }
    return 0;
}

参考资料

线段树学习笔记 - 扫描线算法

一文读懂扫描线算法

posted @   unique_pursuit  阅读(225)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示