1. 程式人生 > >KD樹小結

KD樹小結

abs 剪枝 定義 系統 font n) 註意 二叉 曼哈頓距離

很久之前我就想過怎麽快速在二維平面上查找一個區域的信息,思考許久無果,只能想到幾種優秀一點的暴力。

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樹小結