• 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维,或者某一维排序,而是先按x维,再按y维,再按z维,再……最后又回到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 插入不旋转的单旋spaly见过?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树的基本姿势大概就是这个样子……好写不好写错,基本上都是个板子。

    附上学长的一(三)句话,从各方面进行了深度总结:

    能不写最好还是不要写吧,轻松被卡→_→,也许可以出奇制胜?如果要写,重新构树是个不错的选择。发现大数据跑不过,多半是剪枝挂了。

    还是给个链接……MashiroSky大爷

    upd:以当前坐标差最大的来做type应该比轮换type更优秀……

    例题有"SJY摆棋子"、"简单题"等,在此就不做赘述了。

    比较有意思的应用就是【bzoj3489】 A simple rmq problem,正如上面所言,KD树解决传统数据结构题出奇制胜。

    附上"BZOJ4066简单题"代码一份,操作是单点修改+矩形求和在线。

     

    #include    <iostream>
    #include    <cstdio>
    #include    <cstdlib>
    #include    <algorithm>
    #include    <vector>
    #include    <cstring>
    #include    <queue>
    #include    <complex>
    #include    <stack>
    #define LL long long int
    #define dob double
    #define FILE "bzoj_4066"
    //#define FILE "简单题"
    using namespace std;
    
    const int N = 200010;
    int n,lst,now,sz=10000;
    
    inline int gi(){
      int x=0,res=1;char ch=getchar();
      while(ch>'9'||ch<'0'){if(ch=='-')res*=-1;ch=getchar();}
      while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
      return x*res;
    }
    
    inline void Min(int &x,int y){x=x<y?x:y;}
    inline void Max(int &x,int y){x=x>y?x:y;}
    struct Node{int p[2],mi[2],mx[2],val,sum;}a[N];
    inline bool cmp(const Node &a,const Node &b){return a.p[now]<b.p[now];}
    struct KD_Tree{
      int ch[N][2],root,cnt;
      Node T[N],nd;
    
      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]);
        }
        T[x].sum=T[x].val;
        if(ls)T[x].sum+=T[ls].sum;
        if(rs)T[x].sum+=T[rs].sum;
      }
    
      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];
        T[x].sum=T[x].val=nd.val;
      }
      
      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]){
          T[x].val+=nd.val;T[x].sum+=nd.val;
          return;
        }
        if(nd.p[type]<T[x].p[type])insert(ch[x][0],type^1);
        else insert(ch[x][1],type^1);
        pushup(x);
      }
      
      inline void rebuild(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)rebuild(ch[x][0],l,x-1,type^1);else ch[x][0]=0;
        if(x<r)rebuild(ch[x][1],x+1,r,type^1);else ch[x][1]=0;
        pushup(x);
      }
      
      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;
    
    int main()
    {
      freopen(FILE".in","r",stdin);
      freopen(FILE".out","w",stdout);
      n=gi();
      while(1){
        int type=gi();if(type==3)break;
        int x1=gi()^lst,y1=gi()^lst;
        if(type==1){
          int A=gi()^lst;
          kd.nd.p[0]=x1;kd.nd.p[1]=y1;
          kd.nd.sum=kd.nd.val=A;
          kd.insert(kd.root,0);
          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;
          }
        }
        if(type==2){
          int x2=gi()^lst,y2=gi()^lst;
          lst=kd.query(kd.root,x1,y1,x2,y2);
          printf("%d
    ",lst);
        }
      }
      fclose(stdin);fclose(stdout);
      return 0;
    }
    简单题

     

     

  • 相关阅读:
    【转+补充】在OpenCV for Android 2.4.5中使用SURF(nonfree module)
    Delphi StarOffice Framework Beta 1.0 发布
    Angular ngIf相关问题
    angularjs文档下载
    公众号微信支付开发
    公众号第三方平台开发 教程六 代公众号使用JS SDK说明
    公众号第三方平台开发 教程五 代公众号处理消息和事件
    公众号第三方平台开发 教程四 代公众号发起网页授权说明
    公众号第三方平台开发 教程三 微信公众号授权第三方平台
    公众号第三方平台开发 教程二 component_verify_ticket和accessToken的获取
  • 原文地址:https://www.cnblogs.com/fenghaoran/p/8176236.html
Copyright © 2020-2023  润新知