K-D tree小结

介绍

是一个能维护多维空间中点集的数据结构。

各方面操作复杂度据说都比较玄学。

构造

假设现在要维护的是一个 k k k 维点的点集。

K-D tree会构造成一个类似平衡树的样子,但是一般的平衡树只有一个键值,而这里每个点有 k k k 维的坐标,看起来不是很好比较。

容易想到的方法是,每一个节点处只比较一维坐标,先选定一个点放在当前位置,对于剩下的点,这一维坐标比当前点的要小的话,就放在左子树里,否则放在右子树里。

那么每个节点处比较哪一维坐标呢?普遍的方法是轮流比较,如果我的父亲比较的是第 c c c 维,那我就比较第 ( c + 1 )   m o d   k (c+1)\bmod k (c+1)modk 维,即,深度为 d d d 的节点比较 d   m o d   k d\bmod k dmodk 维。

另一个不普遍的做法是最大方差法,是取当前方差最大的那一维来做比较,这两种做法的实践时间复杂度比较会在最后给出。

那么建树的代码如下(轮流比较法 自己瞎起的名字):

int C;bool cmp(point x,point y){
    
    return x.d[C]<y.d[C];}//d[C]表示第C维的坐标
void build_KDtr(KD_node *&now,int l,int r,int CO=0)
{
    
    
	if(l>=r)return;
	int mid=l+r>>1;C=CO;
	nth_element(q+l,q+mid,q+r,cmp);
	//取这一维坐标大小在中间的点作为当前点,函数的作用是,比他小的放左边,比他大的放右边
	now=new KD_node(q[mid],r-l);
	build_KDtr(now->zuo,l,mid,(CO+1)%K);
	build_KDtr(now->you,mid+1,r,(CO+1)%K);
	now->check();
}

然后这是一个节点内储存的信息:

struct KD_node{
    
    
	point x,ld,ru;
	//一棵子树对应一个点集,ld和ru记录的是这个点集构成的矩阵的左下角和右上角
	//ld即leftdown,ru即rightup,这两个信息会在大部分关于K-D tree的询问中用到
	KD_node *ch[2];
	int size;//子树大小,后面会讲用处
	
	KD_node(point &X,int sz):x(X),ld(X),ru(X),size(sz){
    
    zuo=you=null;}
	//这里的null不是NULL写错了,null是一个节点,它的用处是避免讨论一些边界情况
	KD_node(){
    
    }
	
	void check(){
    
    
		for(int i=0;i<K;i++){
    
    
			ld.d[i]=min(x.d[i],min(zuo->ld.d[i],you->ld.d[i]));
			ru.d[i]=max(x.d[i],max(zuo->ru.d[i],you->ru.d[i]));
		}
	}
};

顺便给一下null节点的构造:

void init(){
    
    
	null=new KD_node();null->size=0;
	for(int i=0;i<K;i++){
    
    
		null->ld.d[i]=inf;
		null->ru.d[i]=-inf;
	}
}

添加节点

像平衡树一样添加就好了。

void add_node(KD_node *&now,point x,int CO=0,bool v=false)
{
    
    
	if(now==null){
    
    now=new KD_node(x,1);return;}
	now->size++;
	C=CO;int to=cmp(x,now->x)^1;
	bool tf=false;
	if(now->size*alpha<=now->ch[to]->size+1)tf=true;
	add_node(now->ch[to],x,(CO+1)%K,v|tf);
	if(!v&&tf)rebuild(now);
	now->check();
}

里面用到了一个看起来很奇怪的东西,rebuild。

容易发现,如果你往一个小矩形范围内加很多点,那么K-D tree就会变得很深,做询问时复杂度会变得很大,于是此时我们需要重新建树,类似替罪羊树那样, t f tf tf v v v 的作用是找到最浅的需要重构的点,重构的标准是:左或右子树的大小占了总子树大小的 α \alpha α 以上。这里 α \alpha α 取了 0.75 0.75 0.75

rebuild函数的实现:

void erase(KD_node *&now){
    
    
	if(now==null)return;
	q[++n]=now->x;
	erase(now->zuo);erase(now->you);
	delete now;
}
void rebuild(KD_node *&now){
    
    n=0;erase(now);build_KDtr(now,1,n+1);}

例题

SJY摆棋子

题目大意: 给出 n n n 个黑点, m m m 次操作,每次新增一个黑点,或给出一个白点,问离这个白点最近的黑点到它的距离。

先将黑点造出K-D tree,新增黑点就是添加节点,询问的话就要用到上面维护的矩形。

做法比较玄学,对于当前询问的点,放到K-D tree里面去递归,每到达一个节点就用该节点上的点来更新答案。

然后再看左右儿子,看一下询问的点离哪个儿子对应的矩形近,就先去哪个,递归完之后,再看看询问点到另一个儿子的矩形的距离,假如小于答案,那就说明有可能更新答案,再递归进去跑。

所以本质就是个搜索加剪枝。

据说时间复杂度是 O ( log ⁡ n O(\log n O(logn ~ n ) \sqrt n) n ) 的?

不是很懂qwq,代码如下:

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 500010
#define inf 999999999
#define zuo ch[0]
#define you ch[1]
#define alpha 0.75

int n,m;
const int K=2;
struct point{
    
    int d[K];}q[maxn];
int dis(point x,point y){
    
    
	int re=0;
	for(int i=0;i<K;i++)re+=abs(x.d[i]-y.d[i]);
	return re;
}
int C;bool cmp(point x,point y){
    
    return x.d[C]<y.d[C];}
struct KD_node *root=NULL,*null=NULL;
struct KD_node{
    
    
	point x,ld,ru;
	KD_node *ch[2];
	int size;
	
	KD_node(point &X,int sz):x(X),ld(X),ru(X),size(sz){
    
    zuo=you=null;}
	KD_node(){
    
    }
	
	void check(){
    
    
		for(int i=0;i<K;i++){
    
    
			ld.d[i]=min(x.d[i],min(zuo->ld.d[i],you->ld.d[i]));
			ru.d[i]=max(x.d[i],max(zuo->ru.d[i],you->ru.d[i]));
		}
	}
	int dis_min(point &z){
    
    
		int re=0;
		for(int i=0;i<K;i++)
		if(z.d[i]<ld.d[i])re+=ld.d[i]-z.d[i];
		else if(z.d[i]>ru.d[i])re+=z.d[i]-ru.d[i];
		return re;
	}
};
void init(){
    
    
	null=new KD_node();null->size=0;
	for(int i=0;i<K;i++){
    
    
		null->ld.d[i]=inf;
		null->ru.d[i]=-inf;
	}
}
void build_KDtr(KD_node *&now,int l,int r,int CO=0)
{
    
    
	if(l>=r)return;
	int mid=l+r>>1;C=CO;
	nth_element(q+l,q+mid,q+r,cmp);
	now=new KD_node(q[mid],r-l);
	build_KDtr(now->zuo,l,mid,(CO+1)%K);
	build_KDtr(now->you,mid+1,r,(CO+1)%K);
	now->check();
}
void erase(KD_node *&now){
    
    
	if(now==null)return;
	q[++n]=now->x;
	erase(now->zuo);erase(now->you);
	delete now;
}
void rebuild(KD_node *&now){
    
    n=0;erase(now);build_KDtr(now,1,n+1);}
void add_node(KD_node *&now,point x,int CO=0,bool v=false)
{
    
    
	if(now==null){
    
    now=new KD_node(x,1);return;}
	now->size++;
	C=CO;int to=cmp(x,now->x)^1;
	bool tf=false;
	if(now->size*alpha<=now->ch[to]->size+1)tf=true;
	add_node(now->ch[to],x,(CO+1)%K,v|tf);
	if(!v&&tf)rebuild(now);
	now->check();
}
int ans;
void ask_min(KD_node *&now,point x)
{
    
    
	if(now==null)return;
	ans=min(ans,dis(now->x,x));
	int dis[2]={
    
    now->zuo->dis_min(x),now->you->dis_min(x)},to=(dis[0]<dis[1])^1;
	if(dis[to]<ans)ask_min(now->ch[to],x);
	if(dis[to^1]<ans)ask_min(now->ch[to^1],x);
}
inline char cn()
{
    
    
	static char buf[1000010],*p1=buf,*p2=buf;
	return p1==p2&&(p2=(p1=buf)+fread(buf,1,1000000,stdin),p1==p2)?EOF:*p1++;
}
#define cn getchar
void read(int &x)
{
    
    
	x=0;int f1=1;char ch=cn();
	while(ch<'0'||ch>'9'){
    
    if(ch=='-')f1=-1;ch=cn();}
	while(ch>='0'&&ch<='9')x=x*10+(ch-'0'),ch=cn(); x*=f1;
}

int main()
{
    
    
	read(n);read(m);
	for(int i=1;i<=n;i++)
	for(int j=0;j<K;j++)read(q[i].d[j]);
	init();build_KDtr(root,1,n+1);
	for(int i=1;i<=m;i++){
    
    
		int id;point x;
		read(id);
		for(int j=0;j<K;j++)read(x.d[j]);
		switch(id){
    
    
			case 1:add_node(root,x);break;
			case 2:ans=2*inf;ask_min(root,x);printf("%d\n",ans);break;
		}
	}
}

上面这个是轮流比较法,最大方差法类似,只需要改build_KDtr函数就可以了,下面是两者时间比较:
在这里插入图片描述
上面的是最大方差法,下面的是轮流比较法。

大概是蒟蒻实现的不好,最大方差法跑的居然会慢些……但具体看每一个测试点的话,还是各有千秋的。

题表

蒟蒻刚学K-D tree,以后还会更新的qwq

[BJWC2014]数据   题解
[CQOI2016]K远点对   题解

猜你喜欢

转载自blog.csdn.net/a_forever_dream/article/details/108023849