【HDU 1542】Atlantis 线段树扫描线(附代码的详细解释)

本文详细介绍了一种经典的扫描线算法,用于解决多个矩形区域的总面积计算问题。通过将图形分解成若干水平线段,逐步向上扫描,计算每段水平线所覆盖的区域长度,最终累加得出总面积。

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

Atlantis

Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)
Total Submission(s): 17354    Accepted Submission(s): 7051



Problem Description
There are several ancient Greek texts that contain descriptions of the fabled island Atlantis. Some of these texts even include maps of parts of the island. But unfortunately, these maps describe different regions of Atlantis. Your friend Bill has to know the total area for which maps exist. You (unwisely) volunteered to write a program that calculates this quantity.
 
Input
The input file consists of several test cases. Each test case starts with a line containing a single integer n (1<=n<=100) of available maps. The n following lines describe one map each. Each of these lines contains four numbers x1;y1;x2;y2 (0<=x1<x2<=100000;0<=y1<y2<=100000), not necessarily integers. The values (x1; y1) and (x2;y2) are the coordinates of the top-left resp. bottom-right corner of the mapped area.

The input file is terminated by a line containing a single 0. Don’t process it.

Output
For each test case, your program should output one section. The first line of each section must be “Test case #k”, where k is the number of the test case (starting with 1). The second one must be “Total explored area: a”, where a is the total explored area (i.e. the area of the union of all rectangles in this test case), printed exact to two digits to the right of the decimal point.

Output a blank line after each test case.

Sample Input
210 10 20 2015 15 25 25.50
Sample Output
Test case #1Total explored area: 180.00


 题目大意:给出N个矩形的左下角坐标和右上角坐标,求出所有矩形的总覆盖面积。

这是一道很经典和简单的扫描线题目,没有什么坑,数据也不大,最初是看大佬的博客学习的解题思想(大佬的:https://blog.youkuaiyun.com/stay_accept/article/details/50503332),然后自己理解了一下AC掉了。

在这里写出来的是比较容易理解的写法,我个人以及身边同学学习的时候碰到的一些问题附在注释和最后。

首先说一下扫描线这一算法的基本思路。


想象有一条平行于x轴的直线,自y=0的位置开始,缓缓向上运动。它经过这个图形的哪一部分,就累加哪一部分的面积。最后,当所有被包在矩形中的空间都被扫描结束,得出的数字自然就是所有矩形的面积总和了。

那么这条线是如何操作的?

它从y=0一直向上,直到碰到第一个矩形的下边。这个时候开始计算图案第一部分的面积(红色阴影部分):先将这个矩形的底边向x轴投影,然后再乘以高度。

继续向上,到达第二条横线处,再将这段长度向下投影,并与上一次的投影合并。再用下一条横线的y轴坐标减去现在这条横线的y轴坐标,得出高度。底*高得出面积,然后继续累加。

下一段,同上。

要计算第4段的面积了,但是这次情况有了一点略微的不同,最下面的矩形已经被计算完了,我们对图形进行扫描的这条直线已经碰到它的上边了。这时我们需要在投影中抹除属于这个矩形的部分。但是为啥只抹除了这么一小块呢?因为其他两个矩形的投影还在呢。抹除了不需要的投影之后,就可以计算阴影部分的面积了。

最后一部分,左侧的矩形也计算完了,将它投影到x轴的部分抹除后,计算阴影部分的面积。得出答案。

这就是整个算法所完成的操作。

接下来落实到程序上。由于每一个部分的计算都是以新的一条横线开始,那么我们要做的第一件事就是先把所有的横线存下来,也就是每一个矩形的两条横线,然后将它们按照从y轴坐标小到大的顺序排序。

除此之外,还要存储所有横线的左右端点,以及左右端点的位置。因为在计算投影长度的时候,需要这些数据。

还有建树的问题。既然所有矩形都要投影到x轴,那么自然就是以x轴的坐标为基础建树。保存了每一条横线的左右端点的位置后,我们就有了2*N个x轴上的点,将这些点排序并去除重复点,得到m个点,分别编号为1-m。

每当碰到一条横线的时候,就按照它的左右端点,进行区间更新,找到这条横线的左右端点是保存下来的m个点中的第几个,然后对区间进行相应的更新,并pushup将这一段的总长度上传到最顶部,这样,树的最顶部就是所有投影的总长度了。

最后一个问题,如何在更新时计算出一段区间内的总长度?这里我采用的方式是:每一个节点都代表的是它到它右侧一个节点的长度。比如,tree[root].left=1,tree[root].right=1,那么这一个节点代表的就是[1,2)的长度,而如果tree[root].left=1,tree[root].right=5,那么这个节点代表的就是[1,6)的长度。这一点很重要,整个程序在涉及区间的问题上,都是用此方式表示的。


#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<iostream>
#include<algorithm>
using namespace std;
struct node
{
    double l;//横线的左端点
    double r;//横线的右端点
    double pos;//横线的y轴坐标
    int s;//标记横线是下边还是上边,下边的s=1,上边的s=-1
} k[210];
double x[210];
int qwe(node x,node y)//将横线按照y轴坐标从小到大的顺序排序
{
    return x.pos<y.pos;
}
struct node1
{
    int left;//区间左端点
    int right;//区间右端点
    int s;//该区间是否向下投影的标记
    double sum;//区间的投影总长度
} tree[810];
void build_tree(int root,int left,int right)//在x轴上建树
{
    tree[root].left=left;
    tree[root].right=right;
    tree[root].s=0;
    tree[root].sum=0;
    if(left==right)return ;
    int mid=(left+right)/2;
    build_tree(root*2,left,mid);
    build_tree(root*2+1,mid+1,right);
}
void push_up(int root)//将投影长度上传
{
    if(tree[root].s)
    {
        tree[root].sum=(x[tree[root].right+1]-x[tree[root].left]);
    }
    /*
        tree[root].s>0时,代表该区间是需要向x轴投影的。并且s也不可能为负数,
        因为扫描是从下向上的,在碰到一条上边之前,一定会碰到与之对应的下边,
        s为0的时候是不可能直接就碰到上边的。
        由于建树时是以“区间+右侧区域”的方式构建的,所以计算区间实际上的长度时,
        要将右侧节点+1再减去左侧节点
    */
    else if(tree[root].left==tree[root].right)
    {
        tree[root].sum=0;
    }
    /*
        如果该区间无需向下投影,并且更新到了树的叶子节点,
        使区间长度=0并结束函数
    */
    else//如果不是叶子节点,就让这个节点的长度等于它两个子节点的长度和
    {
        tree[root].sum=tree[root*2].sum+tree[root*2+1].sum;
    }
}
void update_tree(int root,int left,int right,int s)//对树进行更新
{
    if(tree[root].left==left&&tree[root].right==right)
    {//找到需要的区间后,更改此区间的s标记,并上传
        tree[root].s+=s;
        push_up(root);
        return ;
    }
    int mid=(tree[root].left+tree[root].right)/2;
    //然后就是普通的区间更新了
    if(right<=mid)update_tree(root*2,left,right,s);
    else if(left>=mid+1)update_tree(root*2+1,left,right,s);
    else
    {
        update_tree(root*2,left,mid,s);
        update_tree(root*2+1,mid+1,right,s);
    }
    push_up(root);
}
int main()
{
    int cas=0,n;
    double x1,y1,x2,y2;
    while(~scanf("%d",&n)&&n)
    {
        int cntk=1;
        int cntx=1;
        for(int i=0; i<n; i++)
        {
            scanf("%lf%lf%lf%lf",&x1,&y1,&x2,&y2);
            k[cntk].l=x1;
            k[cntk].r=x2;
            k[cntk].pos=y1;
            k[cntk++].s=1;
            x[cntx++]=x1;
            k[cntk].l=x1;
            k[cntk].r=x2;
            k[cntk].pos=y2;
            k[cntk++].s=-1;
            x[cntx++]=x2;
        }
        sort(k+1,k+cntk,qwe);
        sort(x+1,x+cntx);
        int num=2;
        for(int i=2; i<cntx; i++) 
        {//为存下来的所有x轴上的点去重
            if(x[i]!=x[i-1])x[num++]=x[i];
        }
        cntx=num;
        build_tree(1,1,cntx-2);
        //横线总共有cntx-1条,建树时用不到最后一条
        double sum=0;
        for(int i=1; i<=cntk-2; i++) 
        {//更新时也用不到最后一条
            int l=lower_bound(x+1,x+cntx,k[i].l)-x;
            //在保存下的x轴点中寻找横线的左端点,因为已经去重完了,
            //所以返回值肯定就是要找的那个点
            int r=lower_bound(x+1,x+cntx,k[i].r)-x-1;
            //寻找右端点,然后-1,也是为了“每个节点存储的都是它及
            //它右边的长度”这一要求。
            update_tree(1,l,r,k[i].s);
            sum+=(tree[1].sum*(k[i+1].pos-k[i].pos));
            //计算这一部分选中的矩形面积并累加
        }
        printf("Test case #%d\n",++cas);
        printf("Total explored area: %.2lf\n\n",sum);
    }
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值