KD-Tree 学习笔记

发布于 2019-01-31  428 次阅读


K-D Tree,全名k-dimensional Tree,是一种分割k维数据空间的数据结构。主要应用于多维空间关键数据的搜索(如:范围搜索和最近邻搜索)。K-D Tree是二进制空间分割树的特殊的情况,以下是一棵二维空间上的 k-d tree:

建树

K-D Tree 的建树过程类似于平衡树:对于已知点集,求出其在某一维度上排序后的中间点,作为这个空间的分割点,然后把空间一分为二,再对每个部分递归建树,返回子部分分割点的编号,作为当前部分分割点的左右儿子。关于每次按照哪个维度排序,最好的方案是按照方差大小,但是为了简便我们就遍历每一维来建立。

inline int build(int l,int r,int dir){
    if(l > r) return 0;
    int x = New(),mid = (l + r) >> 1;
    nthdir = dir;
    std::nth_element(data+l,data+mid,data+r+1);
    p[x] = data[mid];lc = build(l,mid-1,dir^1);
    rc = build(mid+1,r,dir^1);
    pushup(x);return x;
}

其中 pushup 函数维护了 max 和 min 两个数组,用来存储每一个分割空间的极值。

inline void pushup(int x){
    FOR(i,0,1){
        min[x][i] = max[x][i] = p[x][i];
        if(lc){
            min[x][i] = std::min(min[x][i],min[lc][i]);
            max[x][i] = std::max(max[x][i],max[lc][i]);
        }
        if(rc){
            min[x][i] = std::min(min[x][i],min[rc][i]);
            max[x][i] = std::max(max[x][i],max[rc][i]);
        }
    }
    size[x] = size[lc] + size[rc] + 1;
}

插入

插入的时候就是从根开始,判断这个点在分割点的那边,如果有的话就递归插入子节点,否则直接创建新的子节点(也和平衡树很像)
删除和插入差不多......

inline void insert(int x,Point P,int dir){
    if(P[dir] < p[x][dir]){
        if(lc) insert(lc,P,dir^1);
        else{
            lc = ++cnt;p[cnt] = P;
            min[cnt][0] = max[cnt][0] = P[0];
            min[cnt][1] = max[cnt][1] = P[1];
        }
    }
    else{
        if(rc) insert(rc,P,dir^1);
        else{
            rc = ++cnt;p[cnt] = P;
            min[cnt][0] = max[cnt][0] = P[0];
            min[cnt][1] = max[cnt][1] = P[1];
        }
    }
    pushup(x);
}

查询

查询时对于每一个被分割的点集,先利用到分割点的距离更新答案,然后判断边界是否能用于更新答案,如果能的话,就递归进入这个子区域更新答案。为了提高效率,我们贪心的选择答案更优的地方优先递归查询。

inline void query(int x,Point P){
    ans = std::min(ans,P-p[x]);
    int L = INT_MAX,R = INT_MAX;
    if(lc) L = calc(lc,P);
    if(rc) R = calc(rc,P);
    if(L < R){
        if(L < ans) query(lc,P);
        if(R < ans) query(rc,P);
    }
    else{
        if(R < ans) query(rc,P);
        if(L < ans) query(lc,P);
    }
}

例题:BZOJ2648 SJY摆棋子
这个题目就是kdtree裸题呀。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
 
#define fi first
#define lc (ch[x][0])
#define se second
#define U unsigned
#define rc (ch[x][1])
#define Re register
#define LL long long
#define MP std::make_pair
#define CLR(i,a) memset(i,a,sizeof(i))
#define FOR(i,a,b) for(Re int i = a;i <= b;++i)
#define ROF(i,a,b) for(Re int i = a;i >= b;--i)
#define SFOR(i,a,b,c) for(Re int i = a;i <= b;i+=c)
#define SROF(i,a,b,c) for(Re int i = a;i >= b;i-=c)
#define DEBUG(x) std::cerr << #x << '=' << x << std::endl
 
const int MAXN = 1000000+5;
int min[MAXN][2],max[MAXN][2],ch[MAXN][2];
int nthdir;
 
inline void upmin(int &a,int b){
    if(a > b) a = b;
}
 
inline void upmax(int &a,int b){
    if(a < b) a = b;
}
 
struct Point{
    int pos[2];
    int& operator [](int x){
        return pos[x];
    }
 
    bool operator < (const Point &t) const {
        return pos[nthdir] < t.pos[nthdir];
    }
 
    int operator - (const Point &t) const {
        return std::abs(pos[0]-t.pos[0]) + std::abs(pos[1]-t.pos[1]);
    }
 
    Point(int x,int y){
        pos[0] = x;pos[1] = y;
    }
    Point(){}
}p[MAXN];
 
inline void pushup(int x){
    if(lc){
        upmin(min[x][0],min[lc][0]);
        upmin(min[x][1],min[lc][1]);
        upmax(max[x][0],max[lc][0]);
        upmax(max[x][1],max[lc][1]);
    }
    if(rc){
        upmin(min[x][0],min[rc][0]);
        upmin(min[x][1],min[rc][1]);
        upmax(max[x][0],max[rc][0]);
        upmax(max[x][1],max[rc][1]);
    }
}
 
inline int build(int l,int r,int dir){
    nthdir = dir;int x = (l + r) >> 1;
    std::nth_element(p+l,p+x,p+r+1);
    min[x][0] = max[x][0] = p[x][0];
    min[x][1] = max[x][1] = p[x][1];
    if(l < x) lc = build(l,x-1,dir^1);
    if(r > x) rc = build(x+1,r,dir^1);
    pushup(x);return x;
}
int cnt;
inline void insert(int x,Point P,int dir){
    if(P[dir] < p[x][dir]){
        if(lc) insert(lc,P,dir^1);
        else{
            lc = ++cnt;p[cnt] = P;
            min[cnt][0] = max[cnt][0] = P[0];
            min[cnt][1] = max[cnt][1] = P[1];
        }
    }
    else{
        if(rc) insert(rc,P,dir^1);
        else{
            rc = ++cnt;p[cnt] = P;
            min[cnt][0] = max[cnt][0] = P[0];
            min[cnt][1] = max[cnt][1] = P[1];
        }
    }
    pushup(x);
}
 
inline int calc(int x,Point P){
    return std::max(0,min[x][0]-P[0]) + std::max(0,min[x][1]-P[1]) + std::max(0,P[0]-max[x][0]) + std::max(0,P[1]-max[x][1]);
} // 点 P 到分割集 x 的距离
 
int ans = INT_MAX;
 
inline void query(int x,Point P){
    ans = std::min(ans,P-p[x]);
    int L = INT_MAX,R = INT_MAX;
    if(lc) L = calc(lc,P);
    if(rc) R = calc(rc,P);
    if(L < R){
        if(L < ans) query(lc,P);
        if(R < ans) query(rc,P);
    }
    else{
        if(R < ans) query(rc,P);
        if(L < ans) query(lc,P);
    }
}
 
int N,M;
 
int main(){
    //freopen("1.in","r",stdin);
    scanf("%d%d",&N,&M);cnt = N;
    FOR(i,1,N) scanf("%d%d",&p[i][0],&p[i][1]);
    int root = build(1,N,0);
    while(M--){
        int opt,x,y;scanf("%d%d%d",&opt,&x,&y);
        if(opt & 1) insert(root,Point(x,y),0);
        else{
            ans = INT_MAX;query(root,Point(x,y));
            printf("%d\n",ans);
        }
    }
    return 0;
}

结语

其实 K-D Tree 更像是一种优化暴力的方法,他常数比较大,有时候为了防止被出题人构造数据卡,需要使用类似于替罪羊树的思想来不断调整树的平衡。


一个OIer。