hdu1542 矩形面积并(线段树+离散化+扫描线)
题意:
给你n个矩形,输入每个矩形的左上角坐标和右下角坐标。
然后求矩形的总面积。(矩形可能相交)。
题解:
前言:
先说说做这道题的感受:
刚看到这道题顿时就懵逼了,几何 烂的渣渣。后来从网上搜题解。才知道用到线段树+离散化+扫描线。不过这是我第一次接触扫描线,根本不知道什么鬼啊。后来各种博客和论文看了一天才真正理解。
不过一想到网上的博客和论文,就来气。都什么啊,代码注释少的很而且说不明白什么意思,比如线段树怎么存每个节点的数据?为什么这么存?每个节点的数据变量都什么意思?更新的时候怎么更新?意思是啥?都TM没写明白,还得老子自己慢慢悟才想明白到底咋回事。
所以为了让更多的同学明白这道题怎么做,线段树+离散化+扫描线到底是个啥,特此写下这篇博客!
正文:
1、从理论角度如何解这道题
扫描线的用法:
由于画图太麻烦了,直接用别人的图吧...
分割线里面的内容转载自http://hzwer.com/879.html
-------------------------------------------------分割线-------------------------------------------------------------------------------------------------------
顾名思义,扫描法就是用一根想象中的线扫过所有矩形,在写代码的过程中,这根线很重要。方向的话,可以左右扫,也可以上下扫。方法是一样的,这里我用的是由下向上的扫描法。
如上图所示,坐标系内有两个矩形。位置分别由左下角和右上角顶点的坐标来给出。上下扫描法是对x轴建立线段树,矩形与y平行的两条边是没有用的,在这里直接去掉。如下图。
现想象有一条线从最下面的边开始依次向上扫描。线段树用来维护当前覆盖在x轴上的线段的总长度,初始时总长度为0。用ret来保存矩形面积总和,初始时为0。
由下往上扫描,扫描到矩形的底边时将它插入线段树,扫描到矩形的顶边时将底边从线段树中删除。而在代码中实现的方法就是,每条边都有一个flag变量,底边为1,顶边为-1。
用cover变量(通过线段树维护)来表示某x轴坐标区间内是否有边覆盖,初始时全部为0。插入或删除操作直接让cover += flag。当cover > 0 时,该区间一定有边覆盖。
开始扫描到第一条线,将它压入线段树,此时覆盖在x轴上的线段的总长度L为10。计算一下它与下一条将被扫描到的边的距离S(即两条线段的纵坐标之差,该例子里此时为3)。
则 ret += L * S. (例子里增量为10*3=30)
结果如下图
橙色区域表示已经计算出的面积。
扫描到第二条边,将它压入线段树,计算出此时覆盖在x轴上的边的总长度。
例子里此时L=15。与下一条将被扫描到的边的距离S=2。 ret += 30。 如下图所示。
绿色区域为第二次面积的增量。
接下来扫描到了下方矩形的顶边,从线段树中删除该矩形的底边,并计算接下来面积的增量。如下图。
蓝色区域为面积的增量。
此时矩形覆盖的总面积已经计算完成。 可以看到,当共有n条底边和顶边时,只需要从下往上扫描n-1条边即可计算出总面积。
此题因为横坐标包含浮点数,因此先离散化。另外,因为用线段树维护的是覆盖在x轴上的边,而边是连续的,并非是一个个断点,因此线段树的每一个叶子结点实际存储的是该点与下一点之间的距离。
--------------------------------------------------分割线-------------------------------------------------------------------------------------------------------------------------------
我的理解:
线段树存的是在x轴上线段区间的覆盖情况。
把矩形的上下边拆分成一条一条的线,每个左右边看成是x轴方向一个一个的点。把这些上下边和点都按升序排序。然后用传说中的扫描线从下往上挨个扫描每条上下边。
当遇到一条边时,在线段树中把这条边覆盖的每个线段打上标记,cover+=flag。(线结构体里的flag变量标识该条线段是上边还是下边,为1是下边,-1是上边),当每个节点的cover==0时,说明这个节点所掌控的线段范围没有被覆盖或没有完全被覆盖,cover>0时,被覆盖。
更新完之后,然后用(即将扫描到的下一条边的高度-当前边的高度)*x轴上线段区间被覆盖的长度,就是当前所计算的面积。然后把这些面积加在一起就是总面积。
由于横坐标范围太大,要离散化之后再在线段树中存储。(离散化自己百度,不难)。
那么线段树怎么存线段呢?比如1~10长度的线段,怎么存,每个节点都有什么意义?看下图:
存1~10每个区间的长度。
为什么这么存呢?看下图:
这样每个编号对应一段单位线段。所以1~3的长度是编号1,2所代表的线段的长度。
这只是一个简单的例子。
明白了这些,来看下代码:
AC代码:
#include <iostream> #include <algorithm> #include <cstring> #include <cstdio> using namespace std; const int MAX=5000; typedef struct node //树节点结构体 { node(){l=r=len=flag=0;} int l,r; double len; //如果该区间下有被覆盖的线段,len表示被覆盖的区域的长度 int flag; //标识是否被覆盖 }TreeNode; typedef struct line //边 结构体,f=1为下边,f=-1为上边 { line(){a=b=h=f=0;} double a,b; //a,b分别表示这条线的左右横坐标 double h; //h表示这条线段所在的高度 int f; //标识是上边还是下边 }Line; double a[MAX]; int n; int line_len; //有line_len条边 int a_len; //有a_len个横坐标 TreeNode tNode[MAX<<4]; Line line[MAX]; void buildTree(int p,int l,int r) { tNode[p].l=l; tNode[p].r=r; tNode[p].flag=0; //标识是否被覆盖 tNode[p].len=0; //如果该区间下有被覆盖的线段,len表示被覆盖的区域的长度 if(l==r) return; int mid=(l+r)>>1; buildTree(p<<1,l,mid); buildTree(p<<1|1,mid+1,r); return; } void getLen(int p) //计算当前节点被覆盖的长度 { if(tNode[p].flag) //如果当前节点被完全覆盖 { tNode[p].len=a[tNode[p].r+1]-a[tNode[p].l]; //每个节点代表的是当前节点到下一个节点的长度 } else if(tNode[p].l==tNode[p].r) //叶子节点,单位线段,如果当前节点没有被覆盖或者没有被完全覆盖,那么就是没有给覆盖 { tNode[p].len=0; } else tNode[p].len=tNode[p<<1].len+tNode[p<<1|1].len; //不是叶子节点,不知道覆盖情况,从孩子节点获取信息 } void update(int p,int l,int r,int val) //更新线段树 val为-1表示覆盖一层,否则删除一层 { if(tNode[p].l>=l&&tNode[p].r<=r) //不必更新到底,因为每条上下边都是对应的,添加删除不影响其他节点,而且所需要的信息是第一个节点的信息。 { tNode[p].flag+=val; getLen(p); return; } if(tNode[p].l>r||tNode[p].r<l) { return; } update(p<<1,l,r,val); update(p<<1|1,l,r,val); getLen(p); return; } int binary(double key,int low,int high) //二分查找每个横坐标离散化之后所对应的编号。建树的时候要用这些编号建树,不是用实际的坐标 { while(low<=high) { int mid=(low+high)>>1; if(a[mid]==key) return mid; if(key<a[mid]) { high=mid-1; } if(key>a[mid]) { low=mid+1; } } return -1; } bool compare(Line a,Line b) { return a.h<b.h; } int main() { int n; int Case=1; while(cin>>n&&n) { line_len=0; a_len=0; memset(a,0,sizeof(a)); double x1,y1,x2,y2; for(int i=0;i<n;i++) { cin>>x1>>y1>>x2>>y2; //横坐标的每个点赋值 a[a_len++]=x1; a[a_len++]=x2; //每条线赋值 line[line_len].a=x1; line[line_len].b=x2; line[line_len].h=y1; line[line_len].f=-1; line[line_len+1].a=x1; line[line_len+1].b=x2; line[line_len+1].h=y2; line[line_len+1].f=1; line_len+=2; } sort(a,a+a_len); //把所有横坐标排序 int num=1; for(int i=1;i<a_len;i++) //排除重复的 { if(a[i]!=a[i-1]) a[num++]=a[i]; } buildTree(1,0,num-1); //排除重复的之后由num个横坐标,所以建树范围是0~num-1,也可以是1~num。随便 sort(line,line+line_len,compare); //所有的边所处高度排序 double sum=0; for(int i=0;i<line_len-1;i++) //遍历排序完的边 { update(1,binary(line[i].a,0,num-1),binary(line[i].b,0,num-1)-1,line[i].f); //先更新覆盖情况 sum+=(line[i+1].h-line[i].h)*tNode[1].len; //计算面积 } printf("Test case #%d\n",Case); printf("Total explored area: %.2lf\n\n",sum); Case++; } return 0; }
先要把这个理解,再看代码,不然看也看不懂,慢慢理解,不能急。