KD樹小結
很久之前我就想過怎麽快速在二維平面上查找一個區域的信息,思考許久無果,只能想到幾種優秀一點的暴力。
Kd樹就是幹上面那件事的。
別的不多說,趕緊把自己的理解寫下來,免得涼了。
KD樹的組成
以維護k維空間(x,y,……)內的KD樹為例,主要由一下三部分組成:
- p[k],代表樹上這個結點所儲存的點(在題目中給出的/你自己加上的點集中的一個點)。
- ch[2],表示它的子結點(沒錯,KD樹是一棵二叉樹)
- 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樹小結