数据结构——k-d树

Posted halifuda

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构——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树的复杂度还是比较高的,并且重复元越多越影响实际效率。。

上面大家可能看到了平衡这两个字:如何对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树的主要内容,如果未能解决你的问题,请参考以下文章

数据结构——k-d树

空间划分的数据结构(四叉树/八叉树/BVH树/BSP树/k-d树)

k-d tree

bzoj 2716 天使玩偶 —— K-D树

最近邻居 - k-d 树 - ***证明

K-D Tree原理和应用