k-d tree學習總結


本文轉載自http://blog.csdn.net/zhjchengfeng5/article/details/7855241# 

  首先來一個問題:

    給定平面上一個點集 E ,還有一個定點 V ,怎么在一群點中找出一個點 U,使得 V 與 U 的距離最近(歐幾里得距離)?

當然,我們能夠想到一種做法:枚舉 E 中所有的點,找出它們中距離V 最近的點 U。

但是,假設現在有兩個點集 E1 與 E2 ,對於 E2 中每一個點 Vi ,找出一個在E1 中的一個點 Ui,使得 Vi 到 Ui 的距離最短,這怎么做?還是枚舉?

既然枚舉的復雜度很高 ( O(n) 的復雜度 ),那有沒有辦法把復雜度降下來呢?答案是肯定的,引入一種數據結構K-D tree

一、何為 K-D tree?

        二叉樹(有左兒子,右兒子的那種樹形結構)

二、能解決哪些問題?

        K-D tree 可以在 log(n) ( 最壞是 sqrt(n) )的時間復雜度內求出一個點集 E 中,距離一個定點 V 最近的點(最近鄰查詢),稍稍處理一下,我們還可以求出點集 E 中距離距離 V 最近的 k 個點(k鄰近查詢)

三、怎么利用 K-D tree 解決上面的問題?

       將點集 E中的點按照某種規則建成一棵二叉樹,查詢的時候就在這顆建好的二叉樹上面用 log(n) (最壞是 sqrt(n))的時間復雜度查詢出距離最近的點

四、既然是二叉樹,怎么建樹?

       這是最關鍵的地方,因為不管是 划分樹 , 線段樹 , 字典樹 ,甚至是其他的數據結構或者算法(例如 KMP 之類的) ,之所以能夠高效的處理問題,主要就是預處理的好。 K-D tree 之所以高效,就是因為建樹很高明,高明之處體現在 “將點集 E中的點按照某種規則建成一棵二叉樹” 的這種規則上

       在講這種規則之前,我們先來看看 K-D tree 這種數據結構為什么叫做 K-D tree 

               K:K鄰近查詢中的k

               D:空間是D維空間(Demension)

                tree:你可以理解為是二叉樹,也可以單純的看做是一顆 tree

        好了, K 我們已經用到了,tree 我們也已經用到了,但是 D 呢?貌似這篇文章到現在為止還沒有提到過 D 吧?

       這種規則,就是針對空間的“維”的

       既然要建樹,那么樹上的節點肯定要定義一些狀態:

       節點的狀態:

                分裂點(split_point)

                分裂方式(split_method)

                左兒子(left_son)

                右兒子(right_son)

        我們建樹的規則就是節點的狀態中的:分裂方式(split_method)

        想必讀者已經看見上面的關鍵字了:分裂點 分裂方式,為什么反復的出現分裂這兩個字呢?難道建一顆 K-D tree 還要分裂什么,分裂空間?

        對,K-D tree的建立就是分裂空間的過程!

        怎么建樹呢?

        建樹依據:

                先計算當前區間 [ L , R ] 中(這里的區間是點的序號區間,而不是我們實際上的坐標區間),每個點的坐標的每一維度上的方差,取方差最大的那一維,設為 d,作為我們的分裂方式(split_method ),把區間中的點按照在 d 上的大小,從小到大排序,取中間的點 sorted_mid 作為當前節點記錄的分裂點,然后,再以 [ L , sorted_mid-1 ] 為左子樹建樹 , 以 [sorted_mid+1 , R ] 為右子樹建樹,這樣,當前節點的所有狀態我們便確定下來了:

                split_point= sorted_mid

                split_method= d

                left_son    =  [ L , sorted_mid-1 ]

                right_son =  [ sorted_mid+1 , R ]

        為了便於理解,我先舉個例子:

        假設現在我們有平面上的點集 E ,其中有 5 個二維平面上的點 : (1,4)(5,8) (4,2) (7,9) (10,11)

        它們在平面上的分布如圖:

        首先,我們對區間 [ 1 , 5 ] 建樹:

        先計算區間中所有點在第一維(也就是 x 坐標)上的方差:

                平均值 : ave_1 =5.4

                方差 : varance_1 =9.04

        再計算區間中所有點在第二維(也就是 y 坐標)上的方差:

                平均值:ave_2 =6.8

                方差:varance_2 =10.96

        明顯看見,varance_2 > varance_1 ,那么我們在本次建樹中,分裂方式 :split_method =2 , 再將所有的點按照 第 2 維 的大小從小到大排序,得到了新的點的一個排列:

                (4,2) (1,4)5,8) (7,9) (10,11)

        取中間的點作為分裂點 sorted_mid =5,8)作為根節點,再把區間 [ 1 , 2] 建成左子樹 , [ 4 , 5] 建成右子樹,此時,直線 : y = 8 將平面分裂成了兩半,前面一半給左兒子,后面一半給了右兒子,如圖:

             

        建左子樹 [1 , 3 ] 的時候可以發現,這時候是 第一維 的方差大 ,分裂方式就是1 ,把區間 [ 1, 2 ] 中的點按照 第一維 的大小,從小到大排序 ,取中間點1,4) 根節點,再以區間 [ 2, 2] 建立右子樹 得到節點 (4,2)

           

         建右子樹 [4 , 5 ] 的時候可以發現,這時還是 第一維 的方差大, 於是,我們便得到了這樣的一顆二叉樹 也就是 K-D tree,它把平面分成了如下的小平面,使得每個小平面中最多有一個點:

                                                                       

        可以看見,我們實際上在建樹的過程中,把整個平面分成了 4 個部分

        樹是建了,那么查詢呢?

        查詢過程:

                查詢,其實相當於我們要將一個點“添加”到已經建好的 K-D tree 中,但並不是真的添加進去,只是找到他應該處於的子空間即可,所以查詢就顯得簡單的毒攻了

                每次在一個區間中查詢的時候,先看這個區間的分裂方式是什么,也就是說,先看這個區間是按照哪一維來分裂的,這樣如果這個點對應的那一維上面的值比根節點的小,就在根節點的左子樹上進行查詢操作,如果是大的話,就在右子樹上進查詢操作

                每次回溯到了根節點(也就是說,對他的一個子樹的查找已經完成了)的時候,判斷一下,以該點為圓心,目前找到的最小距離為半徑,看是否和分裂區間的那一維所構成的平面相交,要是相交的話,最近點可能還在另一個子樹上,所以還要再查詢另一個子樹,同時,還要看能否用根節點到該點的距離來更新我們的最近距離。為什么是這樣的,我們可以用一幅圖來說明:

         

         在查詢到左兒子的時候,我們發現,現在最小的距離是 r = 10 ,當回溯到父親節點的時候,我們發現,以目標點(10,1)為圓心,現在的最小距離 r = 10 為半徑做圓,與分割平面 y = 8 相交,這時候,如果我們不在父親節點的右兒子進行一次查找的話,就會漏掉 (10,9) 這個點,實際上,這個點才是距離目標點 (10,1) 最近的點

由於每次查詢的時候可能會把左右兩邊的子樹都查詢完,所以,查詢並不是簡單的 log(n) 的,最壞的時候能夠達到 sqrt(n)


        好了,到此,K-D tree 就差不多了,寫法上與很多值得優化的地方,至於怎么把最鄰近查詢變換到 K 鄰近查詢,我們用一個數組記錄一個點是否可以用來更新最近距離即可,下面貼上 K-D tree 一個模板

#include <iostream>  
#include <cstdio>  
#include <cstring>  
#include <cmath>  
#include <algorithm>  
#include <vector>  
#include <string>  
#include <queue>  
#include <stack>  
      
#define INT_INF 0x3fffffff  
#define LL_INF 0x3fffffffffffffff  
#define EPS 1e-12  
#define MOD 1000000007  
#define PI 3.141592653579798  
#define N 60000  
      
using namespace std;  
      
typedef long long LL;  
typedef unsigned long long ULL;  
typedef double DB;  
      
struct data  
{  
    LL pos[10];  
    int id;  
} T[N] , op , point;  
int split[N],now,n,demension;  
      
bool use[N];  
LL ans,id;  
DB var[10];  
      
bool cmp(data a,data b)  
{  
    return a.pos[split[now]]<b.pos[split[now]];  
}  
      
void build(int L,int R)  
{  
    if(L>R) return;  
      
    int mid=(L+R)>>1;  
          
    //求出 每一維 上面的方差  
    for(int pos=0;pos<demension;pos++)  
    {  
        DB ave=var[pos]=0.0;  
        for(int i=L;i<=R;i++)  
            ave+=T[i].pos[pos];  
        ave/=(R-L+1);  
        for(int i=L;i<=R;i++)  
            var[pos]+=(T[i].pos[pos]-ave)*(T[i].pos[pos]-ave);  
        var[pos]/=(R-L+1);  
    }  
          
        //找到方差最大的那一維,用它來作為當前區間的 split_method ,split_method保存在split[mid]中 
    split[now=mid]=0;  
    for(int i=1;i<demension;i++)  
        if(var[split[mid]]<var[i]) split[mid]=i;  
          
        //對區間排排序,找到中間點  
    nth_element(T+L,T+mid,T+R+1,cmp);  
          
    build(L,mid-1);  
    build(mid+1,R);  
}  
//bulid過后split{i]表示以i結點為中心點的分裂方式 
void query(int L,int R)  
{  
    if(L>R) return;  
    int mid=(L+R)>>1;  
          
    //求出目標點 op 到現在的根節點的距離  
    LL dis=0;  
    for(int i=0;i<demension;i++)  
        dis+=(op.pos[i]-T[mid].pos[i])*(op.pos[i]-T[mid].pos[i]);  
          
    //如果當前區間的根節點能夠用來更新最近距離,並且 dis 小於已經求得的 ans  
    if(!use[T[mid].id] && dis<ans)  
    {  
        ans=dis;  //更新最近距離  
        point=T[mid];  //更新取得最近距離下的點  
        id=T[mid].id;  //更新取得最近距離的點的 id  
    }  
          
    //計算 op 到分裂平面的距離  
    LL radius=(op.pos[split[mid]]-T[mid].pos[split[mid]])*(op.pos[split[mid]]-T[mid].pos[split[mid]]);  
          
    //對子區間進行查詢  
    if(op.pos[split[mid]]<T[mid].pos[split[mid]])  
    {  
        query(L,mid-1);  
        if(radius<=ans) query(mid+1,R);  
    }  
    else  
    {  
        query(mid+1,R);  
        if(radius<=ans) query(L,mid-1);  
    }  
}  
      
int main()  
{  
    while(scanf("%d%d",&n,&demension)!=EOF)  
    {  
        //讀入 n 個點  
        for(int i=1;i<=n;i++)  
        {  
               for(int j=0;j<demension;j++)  
               scanf("%I64d",&T[i].pos[j]);  
               T[i].id=i;  
        }  
              
        build(1,n);  //建樹  
      
        int m,q; scanf("%d",&q);  // q 個詢問  
        while(q--)  
        {  
            memset(use,0,sizeof(use));  
                  
            for(int i=0;i<demension;i++)  
                scanf("%I64d",&op.pos[i]);  
            scanf("%d",&m);  
            printf("the closest %d points are:\n",m);  
                while(m--)  
            {  
                ans=(((LL)INT_INF)*INT_INF);  
                query(1,n);  
                for(int i=0;i<demension;i++)  
                {  
                    printf("%I64d",point.pos[i]);  
                    if(i==demension-1) printf("\n");  
                    else printf(" ");  
                }  
                use[id]=1;  
            }  
        }  
    }  
    return 0;  
}  

 



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM