線段樹求矩形面積並
經典題目,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; }