K-D Tree簡單介紹
K-D Tree
簡介
K-D Tree全稱 K-Dimensional Tree,也就是 \(K\) 維樹,是一種高效的樹形結構
K-D Tree與平衡樹(平衡二叉查詢樹)比較類似,不同在於平衡樹每個節點僅僅維護一個值,而K-D Tree所維護的資訊可能是 \(2\) 維甚至更高的,那麼怎麼樣才能保證均攤複雜度呢?下面就來說一說
實現
儲存與維護
K-D Tree一般需要儲存多維點資訊和樹上節點資訊,下面是儲存多維點的例子
struct point { int d[k]; const bool operator < (point p)const { return d[wd]<p.d[wd]; } }
樹上節點一般維護子樹中每一維資訊的最大值和最小值,以及左右兒子編號還有自己對應的一個節點的資訊,下面是K-D Tree樹上節點的例子
struct Tree
{
int ch[2];
point cur,mn,mx;
};
維護方法就是與線段樹類似的pushup,可操作性很強,從左右兒子更新自己的資訊,有點區別的就是需要注意更新時要加上自己節點所對應的資訊,下面是維護節點資訊(子樹多維資訊最大值最小值)的pushup例子
void pushup(Tree p) { for(int i=0;i<2;++i)//列舉左右兒子 if(p.ch[i])//如果子節點存在 for(int j=0;j<2;++j)//就更新資訊 p.mn.d[j]=min(p.mn.d[j],kd[p.ch[i]].mn.d[j]),//更新第j維最小值 p.mx.d[j]=max(p.mx.d[j],kd[p.ch[i]].mx.d[j]);//更新第j維最大值 }
建樹
K-D Tree主要通過建樹來保證樹的形態較為平衡,對於樹上的每一層,我們都選擇一個維度進行劃分,以選擇的維度為分辨器來分割節點,主要有 輪轉法,隨機法和最大方差法,選擇之後只要使用函式 nth_element() 來選擇中位數,以中位數為劃分點,小於中位數的為左兒子,大於中位數的為右兒子,遞迴建樹即可,下面來說說維度劃分
輪轉法 利用取模運算,每一層一次選擇不同的維度進行劃分,例如一共有 \(k\) 個維度,那麼,第一層用第 \(1\) 維,第 \(2\) 層用第 \(2\) 維……第 \(i\) 層用第 \(i\%k\) 維
隨機法 顧名思義利用隨機數隨機選擇一個維度進行劃分
最大方差法
建樹的複雜度為 \(\Theta(n\log n)\)
下面是一個例子,二維的資料以輪轉法劃分
void build(int &pos,int l,int r,int dep)
{
pos=0;//先賦值為0讓父節點的子節點編號為0
if(l>r)return;//如果為空直接返回
int mid=(l+r)>>1;//找到最中間的位置
pos=mid;//以最中間位置的下標為當前節點的下標
wd=dep&1;//維度劃分
nth_element(node+l,node+mid,node+r+1);//找到中位數
build(kdt[pos].ch[0],l,mid-1,dep+1);//建左兒子
build(kdt[pos].ch[1],mid+1,r,dep+1);//建右兒子
pushup(pos);//更新維護的資料,根據具體題目調整
}
查詢
K-D Tree的查詢操作也很多樣,我認為我講的不夠清晰,推薦大家看 Luogu 上@光與影的彼岸的一篇部落格,講的非常詳細清晰 淺談偏序問題與K-D Tree - 光與影的彼岸 - 洛谷部落格
我就只簡單說一下求平面最近點和最遠點的方法
首先考慮樸素演算法,將每個節點和所有節點比較,求出所有點對的距離,時間複雜度 \(\Theta(n^2)\)
怎麼優化呢?我們可以像A*演算法一樣設計一個估價函式,根據K-D Tree的劃分性質和節點維護的資訊,每個節點對應的子樹最大值最小值可以看做一個一個的矩形,估價函式則是估計當前點到矩形的最短距離(注意這裡估價函式同樣需要滿足任何估計代價必須小於實際代價),如果當前找到的最短距離都比估價要小,則可以放棄繼續查詢這棵子樹,節省了大量的時間
查詢的期望複雜度為 \(\Theta(n\sqrt n)\)
這也是為什麼大多數K-D Tree都是作為騙分的演算法,但是由於建樹方法多種多樣,所以卡K-D Tree也並不是那麼輕鬆
下面給出求平面最近點的例子
inline int sqr(int x){return x*x;}//平方
inline int dis2(point x,point y){return sqr(x.d[0]-y.d[0])+sqr(x.d[1]-y.d[1]);}//歐幾里得距離平方
inline int fmin(point x,Tree y)//估價函式
{
int f=0;
for(int i=0;i<2;++i)
f+=sqr(max(y.mn.d[i]-x.d[i],0.0))+sqr(max(x.d[i]-y.mx.d[i],0.0));
return f;
}
int maxans=-1;
inline void querymin(Tree x,point y)
{
if(x.cur!=y) minans=min(minans,dis2(x.cur,y));//最近點要注意不能判斷到自己,否則最近點就都是自身了
int fl=INF,fr=INF;//估價初始化為正無窮,保證在沒有兒子的時候不會繼續往下進入死迴圈
if(x.ch[0]) fl=fmin(y,kd[x.ch[0]]);//左兒子估價
if(x.ch[1]) fr=fmin(y,kd[x.ch[1]]);//右兒子估價
if(fl>=fr)//優先走估價更小的子節點
{
if(fr<minans)querymin(kd[x.ch[1]],y);//估價滿足就走右兒子
if(fl<minans)querymin(kd[x.ch[0]],y);//估價滿足就走左兒子
}
else
{
if(fl<minans)querymin(kd[x.ch[0]],y);
if(fr<minans)querymin(kd[x.ch[1]],y);
}
}
插入刪除
插入操作從根節點開始一直往被劃分的方向向下走,直到找到一個相同的點或者找到的節點沒有對應子節點,如果找到一個相同的點就維護一個 \(cnt\) 累加即可,若找到的節點沒有子節點就申請一個新節點,並且把新的節點編號作為找到節點的左兒子或者右兒子
刪除操作和替罪羊樹的刪除操作比較像,用和插入操作相同的方法找到對應節點之後,為節點打上被刪除標記,上傳資訊的時候特判不要加入自己的資訊即可
插入和刪除操作結束之後都需要上傳標記
需要注意的是,如果只是單純插入刪除點,在操作次數多了之後K-D Tree可能退化為一條鏈,導致效率變低,為了避免這種問題,我們需要考慮使用類似平衡樹的方法來維持樹的平衡,但是Splay,有旋Treap之類的旋轉操作顯然會破壞K-D Tree的性質,我們這時就需要替罪羊樹的重構操作,將樹直接拍扁然後重新建子樹就能儘可能維護樹的平衡
小結
K-D Tree能非常方便地處理多維資料,而且可擴充套件性較強,可以方便地支援各種操作,碼量也並不算很大,只是複雜度較高,不過在OI考場上絕對是一種優秀的騙分方法,在大多數情況下都能獲得很高的分數,下面給出幾道例題,可以嘗試一下
Luogu P1429 平面最近點對(加強版) 列舉每一個點,K-D Tree找最近點,並記錄最小值即可,複雜度 \(\Theta(n^{\frac32})\)
Luogu P6247 [SDOI2012]最近最遠點對 同上題,只是多了一個最遠點
Luogu P4357 [CQOI2016]K 遠點對 注意到 \(k\) 很小,直接暴力把所有點的最遠的 \(k\) 個點放入大小為 \(k\) 小根堆,最後堆頂就是答案
Luogu P4169 天使玩偶/SJY擺棋子 帶插入K-D Tree板子,對於每個給定點找最近點曼哈頓距離就行
該文為本人原創,轉載請註明出處