KD树小结
很久之前我就想過(guò)怎么快速在二維平面上查找一個(gè)區(qū)域的信息,思考許久無(wú)果,只能想到幾種優(yōu)秀一點(diǎn)的暴力。
KD樹(shù)就是干上面那件事的。
別的不多說(shuō),趕緊把自己的理解寫(xiě)下來(lái),免得涼了。
?
KD樹(shù)的組成
以維護(hù)k維空間(x,y,……)內(nèi)的KD樹(shù)為例,主要由一下三部分組成:
?
不看mi和mx,長(zhǎng)得就和splay/trie樹(shù)一樣,一個(gè)p維護(hù)當(dāng)前節(jié)點(diǎn),一個(gè)ch[2]記錄左右兒子。
不看p[k],長(zhǎng)得就和線段樹(shù)一樣,有左右兒子和區(qū)間信息。
沒(méi)錯(cuò),KD樹(shù)功能如線段樹(shù),結(jié)點(diǎn)維護(hù)區(qū)域信息;形態(tài)如splay/trie樹(shù),每個(gè)結(jié)點(diǎn)有實(shí)際的值和意義。
?
KD樹(shù)的構(gòu)建
一般題目都是二維平面。下面就以二維平面KD樹(shù)的構(gòu)建為例。
讀入把點(diǎn)存進(jìn)結(jié)構(gòu)體數(shù)組a中,坐標(biāo)分別為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);非常優(yōu)美……對(duì)type、now作用不明的同學(xué)請(qǐng)繼續(xù)閱讀……你要現(xiàn)在就明白就奇怪了
系統(tǒng)函數(shù)nth_element(a+l,a+x,a+r+1),頭文件algorithm,需定義<或cmp函數(shù)。
作用:把排序后第x大的放到第x位,比它小的放進(jìn)左邊,比它大的放進(jìn)右邊(兩邊無(wú)序)。
注意區(qū)間開(kāi)閉:左閉右開(kāi),中間也是閉合的。
復(fù)雜度:平均,期望是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]; }不要問(wèn)我為什么辣么長(zhǎng),為了減常沖榜,把循環(huán)展開(kāi)了……
聰明的讀者已經(jīng)發(fā)現(xiàn)KD樹(shù)的構(gòu)建巧妙之處。它不是純粹按照x維,或者某一維排序,而是先按x維,再按y維,再按z維,再……最后又回到x維……
這樣分割的區(qū)域更加整齊劃一更加均勻緊縮,不像上面的按照某一維劃分,到最后變成一條條長(zhǎng)條,KD樹(shù)劃分到底的圖案還是很好看的。
這樣分割有什么好處呢?等你真正領(lǐng)悟了KD樹(shù)的精髓之后你就會(huì)發(fā)現(xiàn)……嘿嘿嘿……
(就是為了把這個(gè)暴力數(shù)據(jù)結(jié)構(gòu)剪枝剪更多跑更快)
?
KD樹(shù)的操作
1.往KD樹(shù)上插點(diǎn)
插點(diǎn)可以分為插新點(diǎn)和插老點(diǎn)。如果有老點(diǎn),特判一句,把信息覆蓋即可。
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]){……(自行維護(hù));return;}if(nd.p[type]<T[x].p[type])insert(ch[x][0],type^1);else insert(ch[x][1],type^1);pushup(x); }依然非常的美妙……等等有什么不對(duì)?
我們能估計(jì)出一棵剛建好的KD樹(shù)深度是O(log)的。
但你這么隨便亂插……有道題叫HNOI2017 spaly 插入不旋轉(zhuǎn)的單旋spaly見(jiàn)過(guò)?T成茍。
這都不是問(wèn)題!知不知道有一種數(shù)據(jù)結(jié)構(gòu)叫做替罪羊樹(shù)哇?
知道替罪羊樹(shù)怎么保證復(fù)雜度的嗎?
重構(gòu)!大力重構(gòu)!自信重構(gòu)!不爽就重構(gòu)!
為了省事大概每插入10000次就重構(gòu)一次好了……
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樹(shù)上查詢
- 如果是單點(diǎn)(給定點(diǎn))查詢:
- 太簡(jiǎn)單啦!把插入改改就闊以辣!
- 如果是查詢距離一個(gè)點(diǎn)(x',y')最近的點(diǎn)(曼哈頓距離,|x-x'|+|y-y'|):
- 首先我們看暴力的剪枝:按某一維排序,如果該維的差過(guò)大就不管了。
- 而令我們期待的KD樹(shù)呢?呃不好意思,它也是這么做的……
- 我們維護(hù)過(guò)兩個(gè)叫做mi[]和mx[]的東西吧……這個(gè)時(shí)候就是它派上用場(chǎng)了。
- 具體還請(qǐng)看代碼吧: //查詢的點(diǎn)(x',y')儲(chǔ)存在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():如果當(dāng)前點(diǎn)在這個(gè)區(qū)間內(nèi)就是0,否則就是最極的點(diǎn)到它的距離。
- 聰明絕頂?shù)哪阋呀?jīng)發(fā)現(xiàn)了……這TM就是個(gè)暴力。
- 其實(shí)這個(gè)數(shù)據(jù)結(jié)構(gòu)就是一個(gè)暴力……
- 當(dāng)暴力有了時(shí)間復(fù)雜度證明……還叫暴力么?讀書(shū)人的事,能叫偷么?
- 這么暴力有幾個(gè)好處:不用枚舉所有點(diǎn);剪枝有效及時(shí)。
- 復(fù)雜度有保障,大概在O(√n)級(jí)別下,主要看數(shù)據(jù)。
- 如果是區(qū)間查詢,以區(qū)間查詢點(diǎn)權(quán)和為例(之前就有維護(hù)好):
- 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); }
- 別看代碼長(zhǎng)又看起來(lái)復(fù)雜,寫(xiě)起來(lái)跟線段樹(shù)似的,還是一樣的暴力搞。
?
KD樹(shù)的基本姿勢(shì)大概就是這個(gè)樣子……好寫(xiě)不好寫(xiě)錯(cuò),基本上都是個(gè)板子。
附上學(xué)長(zhǎng)的一(三)句話,從各方面進(jìn)行了深度總結(jié):
“能不寫(xiě)最好還是不要寫(xiě)吧,輕松被卡→_→,也許可以出奇制勝?如果要寫(xiě),重新構(gòu)樹(shù)是個(gè)不錯(cuò)的選擇。發(fā)現(xiàn)大數(shù)據(jù)跑不過(guò),多半是剪枝掛了。”
還是給個(gè)鏈接……MashiroSky大爺。
upd:以當(dāng)前坐標(biāo)差最大的來(lái)做type應(yīng)該比輪換type更優(yōu)秀……
例題有"SJY擺棋子"、"簡(jiǎn)單題"等,在此就不做贅述了。
比較有意思的應(yīng)用就是【bzoj3489】 A simple rmq problem,正如上面所言,KD樹(shù)解決傳統(tǒng)數(shù)據(jù)結(jié)構(gòu)題出奇制勝。
附上"BZOJ4066簡(jiǎn)單題"代碼一份,操作是單點(diǎn)修改+矩形求和在線。
?
#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 "簡(jiǎn)單題" 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\n",lst);}}fclose(stdin);fclose(stdout);return 0; } 簡(jiǎn)單題?
?
?
轉(zhuǎn)載于:https://www.cnblogs.com/fenghaoran/p/8176236.html
總結(jié)
- 上一篇: JPA 系列教程3-单向多对一
- 下一篇: entity-model-first