KD树小结

Posted Fenghr

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了KD树小结相关的知识,希望对你有一定的参考价值。

很久之前我就想过怎么快速在二维平面上查找一个区域的信息,思考许久无果,只能想到几种优秀一点的暴力。

Kd树就是干上面那件事的。

别的不多说,赶紧把自己的理解写下来,免得凉了。

 

KD树的组成

以维护k维空间(x,y,……)内的KD树为例,主要由一下三部分组成:

  1. p[k],代表树上这个结点所储存的点(在题目中给出的/你自己加上的点集中的一个点)。
  2. ch[2],表示它的子结点(没错,KD树是一棵二叉树)
  3. mi[k]与mx[k],mi/mx[i]代表KD树这个结点统辖的所有点的第i-1范围。比如说mi[1]=2,mx[1]=4,就代表这棵树统辖的点的y坐标都在[2,4]内。

 

不看mi和mx,长得就和splay/trie树一样,一个p维护当前节点,一个ch[2]记录左右儿子。

不看p[k],长得就和线段树一样,有左右儿子和区间信息。

没错,KD树形功能如线段树,结点维护区域信息;形态如splay/trie树,每个结点有实际的值和意义。

 

KD树的构建

一般题目都是二维平面。下面就以二维平面KD树的构建为例。

读入把点存进结构体数组a中,坐标分别为a[x].p[i]。

inline void build(int &x,int l,int r,int type){
  x=(l+r)>>1;now=type;
  nth_element(a+l,a+x,a+r+1,cmp);
  nd=a[x];newnode(x);
  if(l<x)build(ch[x][0],l,x-1,type^1);else ch[x][0]=0;
  if(x<r)build(ch[x][1],x+1,r,type^1);else ch[x][1]=0;
  pushup(x);
}

build(kd.root,1,n,0);

非常优美……对type、now作用不明的同学请继续阅读……你要现在就明白就奇怪了

系统函数nth_element(a+l,a+x,a+r+1),头文件algorithm,需定义<或cmp函数。

作用:把排序后第x大的放到第x位,比它小的放进左边,比它大的放进右边(两边无序)。

注意区间开闭:左闭右开,中间也是闭合的。

复杂度:平均,期望是O(n)?可以接受。

下面给出cmp、newnode、pushup代码。

struct Node{int p[2],mi[2],mx[2];}a[N];
inline bool cmp(const Node &a,const Node &b){return a.p[now]<b.p[now];}
inline void Min(int &x,int y){x=x<y?x:y;}
inline void Max(int &x,int y){x=x>y?x:y;}
inline void pushup(int x){
  int ls=ch[x][0],rs=ch[x][1];
  if(ls){
    Min(T[x].mi[0],T[ls].mi[0]);Max(T[x].mx[0],T[ls].mx[0]);
    Min(T[x].mi[1],T[ls].mi[1]);Max(T[x].mx[1],T[ls].mx[1]);
  }
  if(rs){
    Min(T[x].mi[0],T[rs].mi[0]);Max(T[x].mx[0],T[rs].mx[0]);
    Min(T[x].mi[1],T[rs].mi[1]);Max(T[x].mx[1],T[rs].mx[1]);
  }
}

inline void newnode(int x){
  T[x].p[0]=T[x].mi[0]=T[x].mx[0]=nd.p[0];
  T[x].p[1]=T[x].mi[1]=T[x].mx[1]=nd.p[1];
}

不要问我为什么辣么长,为了减常冲榜,把循环展开了……

聪明的读者已经发现KD树的构建巧妙之处。它不是纯粹按照x维,或者某一维排序,而是先竖着分一下,再横着分,再竖着分……

这样分割的区域更加整齐划一更加均匀,不像上面的划分,到最会变成一条条长条,KD树划分到底还是很好看的。

这样分割有什么好处呢?等你真正领悟了KD树的精髓之后你就会发现……嘿嘿嘿……

 

KD树的操作

1.往KD树上插点

插点可以分为插新点和插老点。如果有老点,特判一句,把信息覆盖即可。

inline void insert(int &x,int type){
  if(!x){x=++cnt,newnode(cnt);return;}
  if(nd.p[0]==T[x].p[0] && nd.p[1]==T[x].p[1]){
    ……(自行维护);return;
  }
  if(nd.p[type]<T[x].p[type])insert(ch[x][0],type^1);
  else insert(ch[x][1],type^1);
  pushup(x);
}

依然非常的美妙……等等有什么不对?

我们能估计出一棵刚建好的KD树深度是O(log)的。

但你这么随便乱插……有道题叫HNOI2017 spaly 插入不旋转的单选splay见过?T成苟。

这都不是问题!知不知道有一种数据结构叫做替罪羊树哇?

知道替罪羊树怎么保证复杂度的吗?

重构!大力重构!自信重构!不爽就重构!

为了省事大概没插入10000次就重构一次好了……

if(kd.cnt==sz){
  for(int i=1;i<=sz;++i)a[i]=kd.T[i];
  kd.rebuild(kd.root,1,sz,0);sz+=10000;
}

 

2.在KD树上查询

  • 如果是单点(给定点)查询:
    • 太简单啦!
  • 如果是查询距离一个点(x‘,y‘)最近的点(曼哈顿距离,|x-x‘|+|y-y‘|):
    • 首先我们看暴力的剪枝:按某一维排序,如果该维的差过大就不管了。
    • 而令我们期待的KD树呢?呃不好意思,它也是这么做的……
    • 我们维护过两个叫做mi[]和mx[]的东西吧……这个时候就是它派上用场了。
    • 具体还请看代码吧:
      //查询的点(x‘,y‘)储存在nd中。
      //这里的l,r就是mi,mx的意思。
      inline int dis(Node p,int x,int ans=0){
        for(int i=0;i<2;++i)
          ans+=max(0,t[x].l[i]-p.p[i])+max(0,p.p[i]-t[x].r[i]);
        return ans;
      }
      
      inline void query(int x){
        Ans=min(Ans,abs(t[x].p[0]-nd.p[0])+abs(t[x].p[1]-nd.p[1]));
        int dl=ch[x][0]?dis(nd,ch[x][0]):Inf;
        int dr=ch[x][1]?dis(nd,ch[x][1]):Inf;
        if(dl<dr){
          if(dl<Ans)query(ch[x][0]);
          if(dr<Ans)query(ch[x][1]);
        }
        else{
          if(dr<Ans)query(ch[x][1]);
          if(dl<Ans)query(ch[x][0]);
        }
      }
    • dis():如果当前点在这个区间内就是0,否则就是最极的点到它的距离。
    • 聪明绝顶的你已经发现了……这TM就是个暴力。
    • 当暴力有了时间复杂度证明……还叫暴力么?读书人的事,能叫偷么?
    • 这么暴力有几个好处:不用枚举所有点;剪枝有效及时。
    • 复杂度有保障,大概在O(√n)级别。
  • 如果是区间查询,以区间查询点权和为例(之前就有维护好):
    • inline bool in(int l,int r,int xl,int xr){return l<=xl && xr<=r;}
      inline bool out(int l,int r,int xl,int xr){return xr<l || r<xl;}
      
      inline int query(int x,int x1,int y1,int x2,int y2){
        int ans=0;if(!x)return ans;
        if(in(x1,x2,T[x].mi[0],T[x].mx[0]))
          if(in(y1,y2,T[x].mi[1],T[x].mx[1]))
            return T[x].sum;
        if(out(x1,x2,T[x].mi[0],T[x].mx[0]))return 0;
        if(out(y1,y2,T[x].mi[1],T[x].mx[1]))return 0;
        if(in(x1,x2,T[x].p[0],T[x].p[0]))
          if(in(y1,y2,T[x].p[1],T[x].p[1]))
            ans+=T[x].val;
        return ans+query(ch[x][0],x1,y1,x2,y2)+query(ch[x][1],x1,y1,x2,y2);
      }
    • 别看代码长又看起来复杂,写起来跟线段树似的,还是一样的暴力搞。

KD树的基本姿势大概就是这个样子……例题有"SJY摆棋子"、"简单题"等。

以上是关于KD树小结的主要内容,如果未能解决你的问题,请参考以下文章

初学kd树

python kd树 搜索

Kd 树迭代实现(C++)

BZOJ 4066 简单题(KD树)

KD树是什么? 为什么要用KD树? KD树怎么用? KD树和KNN的关联是什么?

HDU 5992 Finding Hotels(KD树)题解

(c)2006-2024 SYSTEM All Rights Reserved IT常识