一直以为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树的复杂度还是比较高的,并且重复元越多越影响实际效率。。
上面大家可能看到了平衡这两个字:如何对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; }