Atlantis
Total Submission(s): 17354 Accepted Submission(s): 7051
The input file is terminated by a line containing a single 0. Don’t process it.
Output a blank line after each test case.
这是一道很经典和简单的扫描线题目,没有什么坑,数据也不大,最初是看大佬的博客学习的解题思想(大佬的: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);
}
}