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\)也可以作為騙分的利器