hdu 1542 Atlantis
线段树求矩形面积并
经典题目,poj 1151 是相同的题目。终于学了求矩形面积并,详细说一下。
首先是看小hh的线段树专题,因为找不到什么论文来看所以只好啃他的代码,啃了一个晚上,有感觉,但是不确定,只能轻轻体会到扫描线的意义。后来啃不下去了,就自己想,给想了出来,但是想出来居然是跟原始的方法不同的。所以下面说的是原始的方法(或者说是小hh代码中的方法),以及我自己想出来的一种方法,两种虽然不同,但是个人感觉本质还是差不多的,不过从效率上看,小hh的那种代码应该效率更高。另外下面给出的代码都是用线段树来模拟扫描法,其实还有更好的方法就是用DP的思想去优化,据说效率提高不是一点两点而是很多,但是还没学,学完会继续更新
分析:
1.矩形比较多,坐标也很大,所以横坐标需要离散化(纵坐标不需要),熟悉离散化后这个步骤不难,所以这里不详细讲解了,不明白的还请百度
2.重点:扫描线法:假想有一条扫描线,从左往右(从右往左),或者从下往上(从上往下)扫描过整个多边形(或者说畸形。。多个矩形叠加后的那个图形)。如果是竖直方向上扫描,则是离散化横坐标,如果是水平方向上扫描,则是离散化纵坐标。下面的分析都是离散化横坐标的,并且从下往上扫描的。
扫描之前还需要做一个工作,就是保存好所有矩形的上下边,并且按照它们所处的高度进行排序,另外如果是上边我们给他一个值-1,下边给他一个值1,我们用一个结构体来保存所有的上下边
struct segment
{
double l,r,h; //l,r表示这条上下边的左右坐标,h是这条边所处的高度
int f; //所赋的值,1或-1
}
接着扫描线从下往上扫描,每遇到一条上下边就停下来,将这条线段投影到总区间上(总区间就是整个多边形横跨的长度),这个投影对应的其实是个插入和删除线段操作。还记得给他们赋的值1或-1吗,下边是1,扫描到下边的话相当于往总区间插入一条线段,上边-1,扫描到上边相当于在总区间删除一条线段(如果说插入删除比较抽象,那么就直白说,扫描到下边,投影到总区间,对应的那一段的值都要增1,扫描到上边对应的那一段的值都要减1,如果总区间某一段的值为0,说明其实没有线段覆盖到它,为正数则有,那会不会为负数呢?是不可能的,可以自己思考一下)。
每扫描到一条上下边后并投影到总区间后,就判断总区间现在被覆盖的总长度,然后用下一条边的高度减去当前这条边的高度,乘上总区间被覆盖的长度,就能得到一块面积,并依此做下去,就能得到最后的面积
(这个过程其实一点都不难,只是看文字较难体会,建议纸上画图,一画即可明白,下面献上一图希望有帮组)
从这个图,也可以感受到,就好比一个畸形的容器,往里面倒水,从最下面往上面涨,被水淹过的部分其实就是我们要求的面积
下面给出代码
/* 1.保存矩形的上下边界,并且重要的,记录他们是属于上还是下,然后按高度升序排序 2.保存竖线坐标,并且去重,是为了离散化 3.以保存的上下边界数组去更新 */ #include <cstdio> #include <cstring> #include <algorithm> using namespace std; #define INF 0x3f3f3f3 #define MAX 110 #define LCH(i) ((i)<<1) #define RCH(i) ((i)<<1 | 1) struct segment //保存矩形上下边界 { double l,r,h; //左右横坐标,纵坐标 int f; //-1为下边界,1为上边界 }ss[2*MAX]; struct node //线段树节点 { int l,r; int cnt; //该节点被覆盖的情况 double len; //该区间被覆盖的总长度 int mid() { return (l+r)>>1; } }tt[2*MAX*4]; double pos[2*MAX]; int nums; int cmp(struct segment a ,struct segment b) { return a.h<b.h; } void build(int a, int b ,int rt) { tt[rt].l=a; tt[rt].r=b; tt[rt].cnt=0; tt[rt].len=0; if(a==b) return ; int mid=tt[rt].mid(); build(a,mid,LCH(rt)); build(mid+1,b,RCH(rt)); } int binary(double key ,int low, int high) { while(low<=high) { int mid=(low+high)>>1; if(pos[mid] == key) return mid; else if(key < pos[mid]) high=mid-1; else low=mid+1; } return -1; } void get_len(int rt) { if(tt[rt].cnt) //非0,已经被整段覆盖 tt[rt].len = pos[tt[rt].r+1] - pos[tt[rt].l]; else if(tt[rt].l == tt[rt].r) //已经不是一条线段 tt[rt].len = 0; else //是一条线段但是又没有整段覆盖,那么只能从左右孩子的信息中获取 tt[rt].len = tt[LCH(rt)].len + tt[RCH(rt)].len ; } void updata(int a, int b ,int val ,int rt) { if(tt[rt].l==a && tt[rt].r==b) //目标区间 { tt[rt].cnt += val; //更新这个区间被覆盖的情况 get_len(rt); //更新这个区间被覆盖的总长度 return ; } int mid=tt[rt].mid(); if(b<=mid) //只访问左孩子 updata(a,b,val,LCH(rt)); else if(a>mid) //只访问有孩子 updata(a,b,val,RCH(rt)); else //左右都要访问 { updata(a,mid,val,LCH(rt)); updata(mid+1,b,val,RCH(rt)); } get_len(rt); //计算该区间被覆盖的总长度 } int main() { int Case=0; int n; while(scanf("%d",&n)!=EOF && n) { nums=0; for(int i=0; i<n; i++) { double x1,y1,x2,y2; scanf("%lf%lf%lf%lf",&x1,&y1,&x2,&y2); ss[nums].l=x1; ss[nums].r=x2; ss[nums].h=y1; ss[nums].f=1; //记录上边界的信息 ss[nums+1].l=x1; ss[nums+1].r=x2; ss[nums+1].h=y2; ss[nums+1].f=-1; //记录下边界的信息 pos[nums]=x1; pos[nums+1]=x2; //记录横坐标 nums += 2; } sort(ss,ss+nums,cmp); //横线按纵坐标升序排序 sort(pos,pos+nums); //横坐标升序排序 //for(int i=0; i<nums; i++) printf("%.2lf %.2lf %.2lf\n",ss[i].l,ss[i].r,ss[i].h); int m=1; for(int i=1; i<nums; i++) if(pos[i]!=pos[i-1]) //去重 pos[m++]=pos[i]; build(0,m-1,1); //离散化后的区间就是[0,m-1],以此建树 double ans=0; for(int i=0; i<nums; i++) //拿出每条横线并且更新 { int l=binary(ss[i].l,0,m-1); int r=binary(ss[i].r,0,m-1)-1; updata(l,r,ss[i].f,1); //用这条线段去更新 ans += (ss[i+1].h-ss[i].h)*tt[1].len; //printf("%.2lf\n",ans); } printf("Test case #%d\n",++Case); printf("Total explored area: %.2f\n\n",ans); } return 0; }
————————————————————————————————————————————————————————————————————————————
下面说一下我自己理解出来的一个方法,当时是还没有明白上面的代码及其思想的时候想出来的
1.离散化横坐标,从下往上扫描上下边,一样要排序,一样给下边赋值1,上边赋值-1
2.没扫描到一条上下边,把它投影到总区间,但不是算总区间被覆盖的总长度。而是这条边界投影后,看这条边界对应的区间内,哪些部分对应的值变为了0(那个1和-1叠加后会变回0),变为0的部分就可以乘上高度差得到一小块的面积
这种方法还要记录一个值,就是总区间上每一段对应的最低高度,当某一段没有被线段覆盖时,它的最低高度是0,如果一旦被一个边界覆盖了,它的最低高度就是这条边界的高度(而且可以知道这个边界一定是下边界,不会是上边界首先覆盖的,这个道理和上面的一样),而已经被覆盖的线段,如果再给其他边界覆盖,无论是增加还是消除,其最低高度都不变。除非是完全消掉,那么它的最低高度又变回0
这个过程其实也不难理解的,但是文字真心难理解,建议自己画图,很容易明白,下面再献上一图,希望有帮助
下面给出两个代码,都是实现上面的思想的,第一种用了LAZY思想,效率比第二个高,第二个是不加思索地深入到每一片叶子再求面积。但是在oj都跑出了0ms,只能说数据水了。。。
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; #define INF 0x3f3f3f3f #define MAX 110 #define LCH(i) ((i)<<1) #define RCH(i) ((i)<<1|1) struct segment { double l,r,h; int f; }ss[2*MAX]; struct node { int l,r; double h; int cnt; int mid() { return (l+r)>>1; } }tt[2*MAX*4]; double pos[2*MAX]; double ans; int nums; int cmp(struct segment a ,struct segment b) { return a.h<b.h; } void build(int a ,int b ,int rt) { tt[rt].l=a; tt[rt].r=b; tt[rt].h=0; tt[rt].cnt=INF; if(a==b) return ; int mid=tt[rt].mid(); build(a,mid,LCH(rt)); build(mid+1,b,RCH(rt)); } int binary(double key ,int low, int high) { while(low<=high) { int mid=(low+high)>>1; if(pos[mid] == key) return mid; else if(key < pos[mid]) high=mid-1; else low=mid+1; } return -1; } /* 一个线段进来,是将该线段对应的党员的值都增加val,而不是变为val 所以并不是找到目标区间就停止了,而在找到了目标区间的基础上,还要保证该区间各单元的值都相等 那么才可以成段更新,因此找到了目标区间还要继续深入(而且可知深入进去都必定是目标区间的子区间) 在最后找到了可改变值的区间时,就进去求面积函数 */ void cal(int val ,int rt ,int n) { if(tt[rt].cnt + val == 0) //可以计算面积 { ans += (pos[tt[rt].r+1]-pos[tt[rt].l])*(ss[n].h-tt[rt].h); tt[rt].cnt=0; } else if(tt[rt].cnt == 0 && val==-1) //加入底线 { tt[rt].h=ss[n].h; tt[rt].cnt=-1; } else if(tt[rt].cnt==INF) { tt[rt].cnt = val; tt[rt].h=ss[n].h; } else tt[rt].cnt+=val; return ; } void updata(int a ,int b ,int val ,int rt , int n) { int mid; if(tt[rt].l==a && tt[rt].r==b) //找到了目标区间但是还不能改变区间值 { if(tt[rt].cnt!=INF || tt[rt].l==tt[rt].r) //整段的值都是一样的,可以更新了 { cal(val,rt,n); //先进入求面积函数 } else //整段的值不同,那么还要继续深入 { mid=tt[rt].mid(); updata(a,mid,val,LCH(rt),n); updata(mid+1,b,val,RCH(rt),n); } return ; } mid=tt[rt].mid(); if(tt[rt].cnt!=INF) //当前区间的数值是统一的,要传递给左右孩子 { tt[LCH(rt)].cnt=tt[RCH(rt)].cnt=tt[rt].cnt; tt[rt].cnt=INF; } if(b<=mid) //左孩子 updata(a,b,val,LCH(rt),n); else if(a>mid) updata(a,b,val,RCH(rt),n); else { updata(a,mid,val,LCH(rt),n); updata(mid+1,b,val,RCH(rt),n); } } int main() { int Case=0; int n; while(scanf("%d",&n)!=EOF && n) { double x1,x2,y1,y2; nums=0; for(int i=0; i<n; i++) { scanf("%lf%lf%lf%lf",&x1,&y1,&x2,&y2); ss[nums].l=x1; ss[nums].r=x2; ss[nums].h=y1; ss[nums].f=-1; ss[nums+1].l=x1; ss[nums+1].r=x2; ss[nums+1].h=y2; ss[nums+1].f=1; pos[nums]=x1; pos[nums+1]=x2; nums += 2; } sort(ss,ss+nums,cmp); sort(pos,pos+nums); int m=1; for(int i=1; i<nums; i++) if(pos[i]!=pos[i-1]) pos[m++]=pos[i]; build(0,m-1,1); ans=0; for(int i=0; i<nums; i++) //拿出每条横线并且更新 { int l=binary(ss[i].l,0,m-1); int r=binary(ss[i].r,0,m-1)-1; updata(l,r,ss[i].f,1,i); //用这条线段去更新 //printf("%.2lf\n",ans); } printf("Test case #%d\n",++Case); printf("Total explored area: %.2lf\n\n",ans); } return 0; }
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; #define INF 0x3f3f3f3f #define MAX 110 int LCH(int n) { return n<<1; } int RCH(int n) { return n<<1|1; } struct segment { double l,r,h; int f; }ss[2*MAX]; struct node { int l,r; double h; int cnt; int mid() { return (l+r)>>1; } }tt[2*MAX*4]; double pos[2*MAX]; double ans; int nums; int cmp(struct segment a ,struct segment b) { return a.h<b.h; } void build(int a ,int b ,int rt) { tt[rt].l=a; tt[rt].r=b; tt[rt].h=0; tt[rt].cnt=0; if(a==b) return ; int mid=tt[rt].mid(); build(a,mid,LCH(rt)); build(mid+1,b,RCH(rt)); } int binary(double key ,int low, int high) { while(low<=high) { int mid=(low+high)>>1; if(pos[mid] == key) return mid; else if(key < pos[mid]) high=mid-1; else low=mid+1; } return -1; } void cal(int val ,int rt ,int n) { if(tt[rt].cnt==0 && val==-1) { tt[rt].cnt = val; tt[rt].h=ss[n].h; return ; } if(tt[rt].cnt + val == 0) { ans += (pos[tt[rt].r+1]-pos[tt[rt].l])*(ss[n].h-tt[rt].h); tt[rt].cnt=0; tt[rt].h=0; return ; } tt[rt].cnt += val; return ; } void updata(int a ,int b ,int val ,int rt ,int n) //一直更新到叶子 { if(tt[rt].l == tt[rt].r) //到达叶子 { cal(val,rt,n); return ; } int mid=tt[rt].mid(); if(b<=mid) //左孩子 updata(a,b,val,LCH(rt),n); else if(a>mid) //右孩子 updata(a,b,val,RCH(rt),n); else { updata(a,mid,val,LCH(rt),n); updata(mid+1,b,val,RCH(rt),n); } } int main() { int Case=0; int n; while(scanf("%d",&n)!=EOF && n) { double x1,x2,y1,y2; nums=0; for(int i=0; i<n; i++) { scanf("%lf%lf%lf%lf",&x1,&y1,&x2,&y2); ss[nums].l=x1; ss[nums].r=x2; ss[nums].h=y1; ss[nums].f=-1; ss[nums+1].l=x1; ss[nums+1].r=x2; ss[nums+1].h=y2; ss[nums+1].f=1; pos[nums]=x1; pos[nums+1]=x2; nums += 2; } sort(ss,ss+nums,cmp); sort(pos,pos+nums); int m=1; for(int i=1; i<nums; i++) if(pos[i]!=pos[i-1]) pos[m++]=pos[i]; build(0,m-1,1); ans=0; for(int i=0; i<nums; i++) //拿出每条横线并且更新 { int l=binary(ss[i].l,0,m-1); int r=binary(ss[i].r,0,m-1)-1; updata(l,r,ss[i].f,1,i); //用这条线段去更新 //printf("%.2lf\n",ans); } printf("Test case #%d\n",++Case); printf("Total explored area: %.2f\n\n",ans); } return 0; }