1. 程式人生 > 實用技巧 >KD-Tree 學習筆記

KD-Tree 學習筆記

定義

\(kd-tree\)\(k-dimensional\)樹的簡稱),是一種對\(k\)維空間中的例項點進行儲存以便對其進行快速檢索的樹形資料結構。

主要應用於多維空間關鍵資料的搜尋(如:範圍搜尋和最近鄰搜尋)。\(K-D\)樹是二進位制空間分割樹的特殊的情況。

在計算機科學裡,\(k-d\)樹( \(k-\)維樹的縮寫)是在\(k\)維歐幾里德空間組織點的資料結構。

\(k-d\) 樹可以使用在多種應用場合,如多維鍵值搜尋(例:範圍搜尋及最鄰近搜尋)。k\(-d\)樹是空間二分樹\((Binary space partitioning )\)的一種特殊情況。

設問題維度是 \(K\),其單次查詢的複雜度大概是 \(O(n^{\frac{K−1}{K}})\)

大多數情況下,\(kd-tree\) 處理的都是二維空間上的問題

樹的建立

採用遞迴的方式實現,類似於線段樹

我們設定一個變數 \(pl\) 表示當前以哪一維作為基準排序

遞迴到某一層時,取當前這一維的中位數,把比中位數小的分到左子樹,比中位數大的分到右子樹

這樣查詢的時候會更快,類似於切面包

通常的寫法是在幾個維度之間交替選取 \(pl\),還有的寫法是選擇幾個維度中方差最小的,甚至還可以 \(rand\) 一個維度

程式碼

struct trr{
	int d[2],mx[2],mn[2],lc,rc;
	friend bool operator < (trr aa,trr bb){
		return aa.d[orz]<bb.d[orz];
	}
}tr[maxn],jl[maxn];
void push_up(rg int da){
	rg int lc=tr[da].lc,rc=tr[da].rc;
	for(rg int i=0;i<2;i++){
		tr[da].mx[i]=tr[da].mn[i]=tr[da].d[i];
		if(lc){
			tr[da].mx[i]=std::max(tr[da].mx[i],tr[lc].mx[i]);
			tr[da].mn[i]=std::min(tr[da].mn[i],tr[lc].mn[i]);
		}
		if(rc){
			tr[da].mx[i]=std::max(tr[da].mx[i],tr[rc].mx[i]);
			tr[da].mn[i]=std::min(tr[da].mn[i],tr[rc].mn[i]);
		}
	}
}
int build(rg int l,rg int r,rg int pl){
	orz=pl;
	rg int mids=(l+r)>>1;
	std::nth_element(jl+l,jl+mids,jl+r+1);
	tr[mids]=jl[mids];
	if(mids>l) tr[mids].lc=build(l,mids-1,!pl);
	if(r>mids) tr[mids].rc=build(mids+1,r,!pl);
	push_up(mids);
	return mids;
}

查詢

類似於 \(dfs\) 的剪枝,如果當前的子樹全在要查詢的範圍之內,直接返回整棵子樹的答案

如果當前的子樹全在要查詢的範圍之外,直接剪掉這棵子樹

否則繼續在左右子樹之間遞迴

下面放幾個比較常用的操作

查詢最近點對

寫法一

int mindis(rg int da,rg int xx,rg int yy){
	return Max(0,xx-tr[da].mx[0])+Max(0,tr[da].mn[0]-xx)+Max(0,yy-tr[da].mx[1])+Max(0,tr[da].mn[1]-yy);
}
void cxmin(rg int da,rg int xx,rg int yy){
	rg int haha=dis(xx,yy,tr[da].d[0],tr[da].d[1]);
	if(haha!=0) nans=Min(nans,haha);
	rg int lc=tr[da].lc,rc=tr[da].rc;
	if(lc){
		if(mindis(lc,xx,yy)<nans) cxmin(lc,xx,yy);
	}
	if(rc){
		if(mindis(rc,xx,yy)<nans) cxmin(rc,xx,yy);
	}
}

寫法二

int mindis(rg int da,rg int xx,rg int yy){
	return Max(0,xx-tr[da].mx[0])+Max(0,tr[da].mn[0]-xx)+Max(0,yy-tr[da].mx[1])+Max(0,tr[da].mn[1]-yy);
}
void cxmin(rg int da,rg int xx,rg int yy){
	if(!da) return;
	rg int haha=dis(xx,yy,tr[da].d[0],tr[da].d[1]);
	if(haha!=0) nans=Min(nans,haha);
	rg int lc=tr[da].lc,rc=tr[da].rc;
	rg int jl1=INF,jl2=INF;
	if(lc) jl1=mindis(lc,xx,yy);
	if(rc) jl2=mindis(rc,xx,yy);
	if(jl1<jl2){
		if(jl1<nans) cxmin(lc,xx,yy);
		if(jl2<nans) cxmin(rc,xx,yy);
	} else {
		if(jl2<nans) cxmin(rc,xx,yy);
		if(jl1<nans) cxmin(lc,xx,yy);
	}
}

寫法二的查詢函式和寫法一有所不同

雖然是一個小小的優化,但是剪枝效果超群

查詢最遠點對

int maxdis(rg int da,rg int xx,rg int yy){
	return Max(Max(dis(xx,yy,tr[da].mn[0],tr[da].mn[1]),dis(xx,yy,tr[da].mx[0],tr[da].mn[1])),Max(dis(xx,yy,tr[da].mn[0],tr[da].mx[1]),dis(xx,yy,tr[da].mx[0],tr[da].mx[1])));
}
void cxmax(rg int da,rg int xx,rg int yy){
	if(!da) return;
	nans=Max(nans,dis(xx,yy,tr[da].d[0],tr[da].d[1]));
	rg int lc=tr[da].lc,rc=tr[da].rc;
	rg int jl1=-INF,jl2=-INF;
	if(lc) jl1=maxdis(lc,xx,yy);
	if(rc) jl2=maxdis(rc,xx,yy);
	if(jl1>jl2){
		if(jl1>nans) cxmax(lc,xx,yy);
		if(jl2>nans) cxmax(rc,xx,yy);
	} else {
		if(jl2>nans) cxmax(rc,xx,yy);
		if(jl1>nans) cxmax(lc,xx,yy);
	}
}

查詢 \(k\) 遠點對

維護一個有\(k\)個元素的小根堆,每次和堆頂元素比較

long long maxdis(rg int da,rg int xx,rg int yy){
	return std::max(std::max(dis(xx,yy,tr[da].mn[0],tr[da].mn[1]),dis(xx,yy,tr[da].mx[0],tr[da].mn[1])),std::max(dis(xx,yy,tr[da].mn[0],tr[da].mx[1]),dis(xx,yy,tr[da].mx[0],tr[da].mx[1])));
}
void cxmax(rg int da,rg int xx,rg int yy){
	if(!da) return;
	rg long long now=dis(xx,yy,tr[da].d[0],tr[da].d[1]);
	if(now>q.top()){
		q.pop();
		q.push(now);
	}
	rg int lc=tr[da].lc,rc=tr[da].rc;
	rg long long jl1=-0x3f3f3f3f3f3f3f3f,jl2=-0x3f3f3f3f3f3f3f3f;
	if(lc) jl1=maxdis(lc,xx,yy);
	if(rc) jl2=maxdis(rc,xx,yy);
	if(jl1>jl2){
		if(jl1>q.top()) cxmax(lc,xx,yy);
		if(jl2>q.top()) cxmax(rc,xx,yy);
	} else {
		if(jl2>q.top()) cxmax(rc,xx,yy);
		if(jl1>q.top()) cxmax(lc,xx,yy);
	}
}

維護資訊

\(kd-tree\) 也可以維護各種各樣的資訊

和線段樹基本類似,但是\(kd-tree\)的每一個節點都是真是存在的點

修改的時候也要注意 \(push\_up\)\(push\_down\)

加點操作

在左右子樹之間遞迴即可

int ad(rg int da,rg int aa,rg int bb,rg int pl){
	if(!da){
		da=++n;
		tr[da].d[0]=aa;
		tr[da].d[1]=bb;
		push_up(da);
		return da;
	}
	if(pl==0){
		if(tr[da].d[pl]<aa) tr[da].lc=ad(tr[da].lc,aa,bb,!pl);
		else tr[da].rc=ad(tr[da].rc,aa,bb,!pl);
	} else {
		if(tr[da].d[pl]<bb) tr[da].lc=ad(tr[da].lc,aa,bb,!pl);
		else tr[da].rc=ad(tr[da].rc,aa,bb,!pl);
	}
	push_up(da);
	return da;
}

但是某些毒瘤出題人會精心構造資料把你的樹卡成一條鏈

所以我們需要根據替罪羊的思想排扁重構

當某個子樹的大小乘上一個平衡因子大於整棵樹的大小就要重構

平衡因子一般設為 \(0.75\)

重構時,我們只需要把所有需要重構的點拿出來再建一遍樹即可

程式碼(SYZ擺棋子)

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<vector>
#define rg register
inline int read(){
	rg int x=0,fh=1;
	rg char ch=getchar();
	while(ch<'0' || ch>'9'){
		if(ch=='-') fh=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9'){
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return x*fh;
}
const int maxn=1e6+5;
const double alpha=0.75;
int n,m,ans,orz,rt,sta[maxn],tp,cnt;
std::vector<int> xx,yy;
struct trr{
	int d[2],mx[2],mn[2],lc,rc,siz;
	friend bool operator < (trr aa,trr bb){
		return aa.d[orz]<bb.d[orz];
	}
}tr[maxn],jl[maxn];
void push_up(rg int da){
	rg int lc=tr[da].lc,rc=tr[da].rc;
	for(rg int i=0;i<2;i++){
		tr[da].mx[i]=tr[da].mn[i]=tr[da].d[i];
		if(lc){
			tr[da].mx[i]=std::max(tr[da].mx[i],tr[lc].mx[i]);
			tr[da].mn[i]=std::min(tr[da].mn[i],tr[lc].mn[i]);
		}
		if(rc){
			tr[da].mx[i]=std::max(tr[da].mx[i],tr[rc].mx[i]);
			tr[da].mn[i]=std::min(tr[da].mn[i],tr[rc].mn[i]);
		}
	}
	tr[da].siz=tr[lc].siz+tr[rc].siz+1;
}
int newnode(){
	if(tp) return sta[tp--];
	else return ++cnt; 
}
int build(rg int l,rg int r,rg int pl){
	orz=pl;
	rg int mids=(l+r)>>1,da=newnode();
	std::nth_element(jl+l,jl+mids,jl+r+1);
	tr[da]=jl[mids];
	if(mids>l) tr[da].lc=build(l,mids-1,!pl);
	if(r>mids) tr[da].rc=build(mids+1,r,!pl);
	push_up(da);
	return da;
}
int Max(rg int aa,rg int bb){
	return aa>bb?aa:bb;
}
int Min(rg int aa,rg int bb){
	return aa<bb?aa:bb;
}
int Abs(rg int aa){
	return aa<0?-aa:aa;
}
int mindis(rg int da,rg int aa,rg int bb){
	rg int nans=0;
	nans+=Max(0,aa-tr[da].mx[0]);
	nans+=Max(0,tr[da].mn[0]-aa);
	nans+=Max(0,bb-tr[da].mx[1]);
	nans+=Max(0,tr[da].mn[1]-bb);
	return nans;
}
int getdis(rg int da,rg int aa,rg int bb){
	return Abs(aa-tr[da].d[0])+Abs(bb-tr[da].d[1]);
}
void cx(rg int da,rg int aa,rg int bb){
	rg int lc=tr[da].lc,rc=tr[da].rc;
	ans=Min(ans,getdis(da,aa,bb));
	rg int ac1=0x3f3f3f3f,ac2=0x3f3f3f3f;
	if(lc) ac1=mindis(lc,aa,bb);
	if(rc) ac2=mindis(rc,aa,bb);
	if(ac1<ac2){
		if(ac1<ans) cx(lc,aa,bb);
		if(ac2<ans) cx(rc,aa,bb);
	} else {
		if(ac2<ans) cx(rc,aa,bb);
		if(ac1<ans) cx(lc,aa,bb);
	}
}
void init(rg int now){
	sta[++tp]=now;
	jl[tp].d[0]=tr[now].d[0];
	jl[tp].d[1]=tr[now].d[1];
	if(tr[now].lc) init(tr[now].lc);
	if(tr[now].rc) init(tr[now].rc);
}
void check(rg int &da,rg int pl){
	if(alpha*tr[da].siz<tr[tr[da].lc].siz || alpha*tr[da].siz<tr[tr[da].rc].siz){
		tp=0;
		init(da);
		da=build(1,tr[da].siz,pl);
	}
}
int ad(rg int da,rg int aa,rg int bb,rg int pl){
	if(!da){
		da=newnode();
		tr[da].d[0]=aa;
		tr[da].d[1]=bb;
		push_up(da);
		return da;
	}
	if(pl==0){
		if(tr[da].d[pl]<aa) tr[da].lc=ad(tr[da].lc,aa,bb,!pl);
		else tr[da].rc=ad(tr[da].rc,aa,bb,!pl);
	} else {
		if(tr[da].d[pl]<bb) tr[da].lc=ad(tr[da].lc,aa,bb,!pl);
		else tr[da].rc=ad(tr[da].rc,aa,bb,!pl);
	}
	push_up(da);
	check(da,pl);
	return da;
}
int main(){
	n=read(),m=read();
	for(rg int i=1;i<=n;i++){
		jl[i].d[0]=read(),jl[i].d[1]=read();
	}
	rt=build(1,n,0);
	rg int aa,bb,cc;
	for(rg int i=1;i<=m;i++){
		aa=read(),bb=read(),cc=read();
		if(aa==1){
			ad(rt,bb,cc,0);
		} else {
			ans=0x3f3f3f3f;
			cx(rt,bb,cc);
			printf("%d\n",ans);
		}
	}
	return 0;
}

習題

一般來說看到區間最遠、最近、\(k\)遠點對、平面上矩陣的問題都可以往 \(kd-tree\) 的方面想一想

當然 \(kd-tree\) 還是有一些比較妙的操作的

比如 [NOI2019]彈跳利用了\(kd-tree\) 優化建圖

Generating Synergy則把樹上的操作轉變為深度和 \(dfn\) 序兩維限制,從而可以使用 \(kd-tree\)

當不會正解的時候,\(kd-tree\)也可以作為騙分的利器