定义

\(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\)也可以作为骗分的利器

版权声明:本文为liuchanglc原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/liuchanglc/p/14177897.html