數據結構——k-d樹
一直以為k-d樹是一種高級的數據結構,和LCT可以並列;不過實際上沒那麽厲害。
k-d樹解決的是k維空間裏的點的範圍查找問題。k維空間必須滿足兩點之間的距離是歐幾裏德距離,比如二維的話,A(x1,y1)和B(x2,y2)的距離就是√(x1-x2)2+(y1-y2)2。
k-d樹是一顆二叉搜索樹,只不過每個節點的鍵值不唯一了,都有k個鍵值。k-d樹必須解決如何在這種情況下查找的問題。方法比較簡單:對k-d樹的每一層預先分配一下依賴哪個鍵值。比如2-d樹維護平面直角坐標系的點,我們可以讓第一層依賴於橫坐標,那麽橫坐標小於根節點的橫坐標的點都在左子樹,大於根節點的橫坐標的點都在右子樹。
如果這一層分配好了依賴的鍵值,那麽在這一層查找時暫時不管其他鍵值了。同時,分配好的層不能改,因為那樣樹的結構就必須改變,那修正這棵樹就太麻煩了。
由於有多個鍵值,k-d樹不能一直只維護一個鍵值,那樣無法對其他鍵值搜索。一種方法是循環分配,第n層依賴於第n%k個鍵值。比如上面的2-d樹,可以讓奇數層依賴於x,偶數層依賴於y。如果點的分布已經知道不很平均了,那麽可以讓方差比較大的那些鍵值多分配一些層。比如我們已經知道給我們的點的橫坐標都一樣,那麽上面那棵2-d樹完全可以改成1-d樹了。不過不知道輸入的時候還是用循環分配比較好。
k-d樹可以實現二叉搜索樹的查找功能,還可以多實現一個範圍查找功能。
以1-d樹為例:假設要求維護一些數軸上的點,支持插入和範圍查找。範圍查找表示輸入一個數軸上的區間[l,r],要求輸出區間裏的點的個數。
1-d樹維護範圍查找是這樣的:首先對根節點判斷,假如根節點在整個區間的右邊且不在區間裏,表明區間裏的所有點一定都在根節點的左邊了,這時候直接搜索左子樹即可;另一種對稱的情況一樣,搜索右子樹即可。問題是處理根節點在區間裏的情況:假設根節點的坐標為x,那麽對左子樹搜索區間[l,x],同時對右子樹搜索區間[x,r],答案加起來就可以了。查找到NULL的時候直接返回0。這個處理是比較好理解的。
情況推廣到k-d樹是一樣的,只不過查找時也要判斷當前層依賴的鍵值,只對詢問的這一維的範圍做一維查找即可。
有時候k-d樹被要求做部分查找,比如對平面直角坐標系,查找橫坐標在[l,r]區間裏的點。這時候如果當前層的依賴鍵值不是橫坐標,直接對左右子樹暴力查找[l,r],否則還是做一維查找。
k-d樹的復雜度很復雜,可能會受到重復元素的幹擾。同時,如果部分查找太多也會影響復雜度。分析它的復雜度本身就很復雜,我照抄書上的結果:對於平衡的k-d樹,復雜度為O(M+kN1-1/k)。k為維度,N為節點數,M為範圍內的節點數。簡單一點的話可以略去M(k或N較大時)。可以看到,k越大,M越多,上界越接近O(N)
上面大家可能看到了平衡這兩個字:如何對k-d樹維護平衡?一般的平衡樹的操作不可行,因為它們都用到了旋轉,但是旋轉意味著整棵樹的節點高度都變了,那麽它們所在的層就變了,那麽就需要重新調整它們的兒子,這對效率來講明顯是災難性的。顯然我們不應該頻繁的改變樹的結構,尤其不應改變節點的高度。
替罪羊樹可以滿足這一點。替罪羊樹平時不會改變樹的結構,僅在重構時改變樹的結構。重構和本身的替罪羊不太一樣,還要將需要重構的點按照當前鍵值排序,然後再像以前一樣處理。也可以建一顆空樹,預先處理好每層的依賴鍵值,將點一個個插入,最後拼回去。由於本來範圍查找的復雜度就是O(log2n),因此這個操作還是可以維持在上界裏。不過1-d樹可以像本來那樣重構。
若用循環分配,在遞歸時記錄一下當前層數就可以O(1)處理當前層的依賴鍵值了。
這樣,k-d樹的刪除可以用懶惰刪除,即只給一個被刪除節點打上標記,還可以用它查找,但不計入答案裏,當重構時順便刪除,這樣可以最低限度維持樹的結構不改變。
k-d樹是一種比較方便的多維範圍查找的數據結構,復雜度也不是很差,所以是多維範圍查找的首選。
1-d樹代碼(不處理重復元的代碼)(之後可能補上2-d樹的):
#include<cstdio> #define nil 0 #define alpha 0.8f #define MXN 100000+1 class IO{ public: IO operator >> (int &a){scanf("%d",&a);return *this;} IO operator << (const char &a){printf("%c",a);return *this;} IO operator << (const char *a){printf(a);return *this;} IO operator << (const int &a){printf("%d",a);return *this;} }cin,cout; class Node{ public: int x,size,fa,left,right,del; }; class QueryData{ public: int xl,xr; void Set(int l,int r){xl=l,xr=r;return;} int Check(Node N){ if(xl<=N.x&&N.x<=xr) return 0; else if(xr<N.x) return 1; else if(N.x<xl) return 2; } }; class Node node[MXN]; int recycle[MXN],lst[MXN],ntop,rtop,ltop,root,node_sum,del_sum; void Init(){ ntop=ltop=root=0; rtop=-1; return; } int NewNode(int k){ int nw; if(rtop==-1) nw=++ntop; else nw=recycle[rtop--]; node[nw].x=k; node[nw].del=0; node[nw].size=1; node[nw].fa=node[nw].left=node[nw].right=0; return nw; } void Update(int now){ node[now].size=node[node[now].left].size+node[node[now].right].size+1; return; } bool CheckBalan(int now){ double T=(double)node[now].size; double L=(double)node[node[now].left].size; double R=(double)node[node[now].right].size; return L>T*alpha || R>T*alpha; } void Travel(int now){ if(now==nil) return; Travel(node[now].left); if(node[now].del==0) lst[ltop++]=now; else{ recycle[++rtop]=now; del_sum--; } Travel(node[now].right); return; } int Build(int l,int r){ if(l>=r) return nil; int nw=lst[(l+r)/2]; node[nw].left=Build(l,(l+r)/2); node[nw].right=Build((l+r)/2+1,r); node[node[nw].left].fa=nw; node[node[nw].right].fa=nw; Update(nw); return nw; } int Search(int now,int k){ if(now==nil) return nil; if(node[now].x==k){ if(node[now].del==0) return now; else return nil; } if(k<node[now].x) return Search(node[now].left,k); if(k>node[now].x) return Search(node[now].right,k); } void Insert(int now,int ins){ if(root==nil) root=ins; else{ node[now].size++; if(node[ins].x<node[now].x){ if(node[now].left==nil){ node[now].left=ins; node[ins].fa=now; } else Insert(node[now].left,ins); } else{ if(node[now].right==nil){ node[now].right=ins; node[ins].fa=now; } else Insert(node[now].right,ins); } } return; } void Insert(int k){ if(Search(root,k)!=nil) return; node_sum++; int ins=NewNode(k); Insert(root,ins); int t=nil,f=nil,flag=0,a=nil; for(int i=ins;i!=nil;i=node[i].fa){ if(CheckBalan(i)) t=i; } if(t!=nil){ f=node[t].fa; if(f==nil||node[f].left==t) flag=0; else flag=1; ltop=0; Travel(t); a=Build(0,ltop); if(f==nil) root=a; else if(flag==0) node[f].left=a; else if(flag==1) node[f].right=a; node[a].fa=f; Update(f); } return; } void Remove(int now,int k){ if(now==nil) return; if(k==node[now].x){ node[now].del=1; } else{ if(k<node[now].x) Remove(node[now].left,k); else Remove(node[now].right,k); } return; } void GrandOrder(int now){ if(now==nil) return; GrandOrder(node[now].left); if(node[now].del==0) cout<<node[now].x<<‘ ‘; GrandOrder(node[now].right); return; } int Query(int now,QueryData D){ if(now==nil) return 0; int ans=0,temp=D.Check(node[now]); QueryData T; if(temp==0){ if(node[now].del==0) ans=1; T.Set(D.xl,node[now].x); ans+=Query(node[now].left,T); T.Set(node[now].x,D.xr); ans+=Query(node[now].right,T); } else if(temp==1){ ans+=Query(node[now].left,D); } else if(temp==2){ ans+=Query(node[now].right,D); } return ans; } int main(){ freopen("test.txt","r",stdin); freopen("testans.txt","w",stdout); Init(); int p,x,y; while(1){ if(del_sum*2>=node_sum){ ltop=0; int a; Travel(root); a=Build(0,ltop); root=a; node[a].fa=nil; node_sum=node[a].size; del_sum=0; } cin>>p; if(p==0) break; if(p==1){ cin>>x; Insert(x); } if(p==2){ cin>>x; if(Search(root,x)!=nil){ Remove(root,x); del_sum++; } } if(p==3){ cin>>x>>y; QueryData D; D.Set(x,y); cout<<Query(root,D)<<‘\n‘; } if(p==4){ GrandOrder(root); cout<<‘\n‘; } } return 0; }1-d樹
數據結構——k-d樹