K-D tree詳解

K-D tree大意:建立在暴力平衡樹替罪羊樹上的一種暴力數據分割方式

而且在某些問題上擁有玄學的時間複雜度,感覺很廢
也可以去看看ball tree,比這個優秀(但是麻煩


前置芝士:替罪羊樹

自己找資料學吧,懶得寫


時間複雜度:

不好說啊,視題目而定吧
對於k維數據,像查詢一些區間內的點和第幾遠就是O(nlogn)O(nn11k)O(nlogn)到O(n\cdot n^{1-\frac {1}{k}})的(不會證明
但是對於找近鄰感覺完全沒法分析(玄學
聽說BBF可以優化?


算法:

給你一堆點,每個點有k個座標(k維直角座標系)
已經說了是暴力數據分割了,你應該知道怎麼做吧

存儲信息:

每個點存一下自己的左右兒子,子樹大小(重建需要),子樹中每一位座標最大和最小(判斷區域)即可
有時要順應題目要求存一些點權和或最大、小值之類

插入:

總體來講就是像平衡樹設置一個根,然後一個一個元素插入,每次只增加一個節點;
但我們判斷新建點進左子樹還是進右子樹的策略改變,因爲我們有多個判斷指標,是多維而不是普通平衡樹的一維,所以我們要採取特殊的分割方式:

第一,很容易想到的是輪換分割
每個座標輪流着來,衆生平等,讓每個座標都儘量平均。
example:這一層用x分割,下一層用y分割,再下一層用x分割

第二,有點神奇的方差分割
可以參考:方差
但是這玩意很少用,也沒什麼人專門卡K-D tree
而且用起來需要大量的加法和乘法,而它們都是很慢的,常數大,有時還沒有輪換快

我們這裏所說的分割,實際上是通過當前點x的座標與新建點y的座標相比來確定進入哪一棵子樹
如果現在的座標判斷是wd,d存儲座標,則:

  • 若y.d[wd]<=x.d[wd],進入左子樹
  • 若y.d[wd]>x.d[wd],進入右子樹

當我們無路可走,即當前點標號爲0(即沒有建過)時,就可以新建點在這裏安家了

查詢:

就是暴力,每到一個點就用當前點信息更新答案,注意是當前點而不是區間。

然後就是儘量剪枝
感覺每個題剪枝都不太一樣啊
但是很明確的有幾種

  1. 查詢某塊區域的點,直接通過判斷座標來得出區域相交信息來剪枝
    如果查詢的區間包括當前點表示的區間,直接加上當前區間的總答案
    如果有交集但不包括,就左右子樹都進入查詢(可見其暴力
    如果沒有交集,直接回溯

  2. 查詢k遠離,維護一個小根堆,每次比較一下當前的區間中的最遠距離和堆頂
    如果最遠距大於堆頂,就左右子樹爆搜
    如果不,直接回溯

  3. 查詢k近鄰,感覺就是假的,每次比較左右子樹的最小值,同時像上面比較最近距離和堆頂
    左子樹小就先走左子樹,右兒子小就先走右子樹,什麼玩意


技巧:

先給你n個點的時候可以不一個一個插入(很慢)
直接一發重建函數把樹建出來:

rt=build(1,n,0);

代碼:

代碼中可以看到一個函數:nth_element
其實沒什麼高級的,就是將l~r的指定位置mid搞一搞,前面的數小於a[mid],後面的數大於a[mid]
因爲有多維所以要寫一個重載運算符或一個cmp函數

#include<bits/stdc++.h>
using namespace std;
const double alpha=0.7;
const int k=2;
struct pt{
	int d[k],w;
}a[200010];
struct data{
	int mx[k],mn[k],sum,ls,rs,sz;
	pt p;
}tree[200010];
int stk[200010];
int n,ans,tot,word,top,rt;
int operator <(pt u,pt v){
	return u.d[word]<v.d[word];
}
int newnode(){//記得寫垃圾回收,不然一拍平重建就會變成兩倍空間
	if(top) return stk[top--];
	return ++tot;
}
void up(int x){//更新
	int l=tree[x].ls;
	int r=tree[x].rs;
	for(int i=0;i<2;i++){
		tree[x].mx[i]=tree[x].mn[i]=tree[x].p.d[i];
		if(l) tree[x].mx[i]=max(tree[x].mx[i],tree[l].mx[i]);
		if(r) tree[x].mx[i]=max(tree[x].mx[i],tree[r].mx[i]);
		if(l) tree[x].mn[i]=min(tree[x].mn[i],tree[l].mn[i]);
		if(r) tree[x].mn[i]=min(tree[x].mn[i],tree[r].mn[i]);
	}
	tree[x].sum=tree[l].sum+tree[r].sum+tree[x].p.w;
	tree[x].sz=tree[l].sz+tree[r].sz+1;
}
int build(int l,int r,int wd){//重建
	if(l>r) return 0;
	int mid=(l+r)>>1;
	int x=newnode();
	word=wd,nth_element(a+l,a+mid,a+r+1);//就是找個當前維度的中值啦
	tree[x].p=a[mid];
	tree[x].ls=build(l,mid-1,wd^1);
	tree[x].rs=build(mid+1,r,wd^1);
	up(x); return x;
}
void flt(int x,int num){//拍平	
	int l=tree[x].ls;
	int r=tree[x].rs;
	if(l) flt(l,num);
	stk[++top]=x;
	a[tree[l].sz+num+1]=tree[x].p;
	if(r) flt(r,num+tree[l].sz+1);
}
void check(int &x,int wd){
	int l=tree[x].ls,r=tree[x].rs;
	if(tree[x].sz*alpha<tree[l].sz||tree[x].sz*alpha<tree[r].sz)
	flt(x,0),x=build(1,tree[x].sz,wd);
}
void ins(int &x,pt y,int wd){
	if(x==0){
		x=newnode();
		tree[x].p=y;
		tree[x].ls=0;
		tree[x].rs=0;
		up(x);
		return;
	}
	if(y.d[wd]<=tree[x].p.d[wd]) ins(tree[x].ls,y,wd^1); //wd^1是針對2維,多維請寫一個nxt數組輪換
	else ins(tree[x].rs,y,wd^1);
	up(x),check(x,wd);
}
bool in(){
	//判斷滿足條件
}
bool out(){
	//判斷不滿足條件
}
int query(int x,int x1,int y1,int x2,int y2){
	if(x==0) return 0;
	int ans=0,l=tree[x].ls,r=tree[x].rs;
	if(in()) return tree[x].sum; //總體包括則直接返回整個區間答案
	if(out()) return 0; //整個不包括則返回0
	if(in()) ans+=tree[x].p.w; //當前點是否滿足				
	ans+=query(l,x1,y1,x2,y2)+query(r,x1,y1,x2,y2);
	return ans;
}
int main(){
	scanf("%d",&n);
}

例題:

1941: [Sdoi2010]Hide and Seek:入門
2648: SJY擺棋子:入門
2716: [Violet 3]天使玩偶:入門
以上三道都是找最近、最遠點對的入門題。
4520: [Cqoi2016]K遠點對:第k遠,要用堆(優先隊列)維護
4066: 簡單題:求平面上某矩形和,帶插入,插入的話就一直往下走,直到能插入爲止
3489: A simple rmq problem:不那麼裸的kd-tree,把pre,next,i看做三個維度就可以搞了
2850: 巧克力王國:求某條直線下的點權和,比簡單題還簡單
3053: The Closest M Points:用堆維護,注意多組數據,每次要清空tr

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章