K-D tree 学习笔记

不想学数学,所以只能连续不断地写数据结构()

简述

K-D tree 是一种用来处理多维空间问题的数据结构,处理 k k k 维空间信息时时间复杂度最坏为 n 1 − 1 k n^{1-\frac{1}{k}} n1k1。通常用的是 2-D tree,在一般情况下能跑到 O ( log ⁡ n ) O(\log n) O(logn),但可以被卡到 O ( n n ) O(n\sqrt n) O(nn )

KDT 是二叉搜索树状的,思想就是用若干超平面尽量平分空间里的所有点,常见的是每次交替选择维度,取中位数来划分左右子树,使得建出的树高是 O ( log ⁡ n ) O(\log n) O(logn) 的,保证时间复杂度。

KDT 所能解决的问题大概是和二元组相关,要满足某些偏序关系的在线问题,功能不错,就是写起来有点费手。

一些基本操作

KDT 里必须要维护的是左右子树 l s ls ls r s rs rs,当前点代表的信息 v a l val val 和当前点子树内所有结点在每一维上坐标的最大最小值 m n mn mn m x mx mx,其它的根据需要加就行。

建树

取中位数的操作可以用 nth_element() 函数 O ( n ) O(n) O(n) 实现,然后就按部就班递归就好了。

为什么 pushup 不用循环写得这么长呢,因为不这么干后面有的题会 TLE 而调试 2h。

注意 nth_element 会破坏原先数组的顺序,所以建树之后不要没事乱调用原数组。

inline bool cmp0(qaq a,qaq b){
	if(a.x[0]!=b.x[0]) return a.x[0]<b.x[0];
	return a.x[1]<b.x[1];
}
inline bool cmp1(qaq a,qaq b){
	if(a.x[1]!=b.x[1]) return a.x[1]<b.x[1];
	return a.x[0]<b.x[0];
}
inline void pushup(int k){
	if(ls){
		ckmax(t[k].mx[0],t[ls].mx[0]),ckmin(t[k].mn[0],t[ls].mn[0]);
		ckmax(t[k].mx[1],t[ls].mx[1]),ckmin(t[k].mn[1],t[ls].mn[1]);
	}
	if(rs){
		ckmax(t[k].mx[0],t[rs].mx[0]),ckmin(t[k].mn[0],t[rs].mn[0]);
		ckmax(t[k].mx[1],t[rs].mx[1]),ckmin(t[k].mn[1],t[rs].mn[1]);	
	}
}
void build(int &k,int l,int r,int flag){
	if(l>r) return;
	k=l+r>>1,ls=rs=0;
	nth_element(a+l,a+k,a+r+1,flag?cmp0:cmp1); 
	t[k].val=a[k];
	t[k].mn[0]=t[k].mx[0]=a[k].x[0];
	t[k].mn[1]=t[k].mx[1]=a[k].x[1];
	build(ls,l,k-1,flag^1),build(rs,k+1,r,flag^1);
	pushup(k);
}

估价函数

估价函数是用于求最近/最远点对一类问题时的剪枝,即根据记录的 m n mn mn m x mx mx 信息算出进入子树所可能得到的最优解,若最优解不优于当前解,则这一棵子树都不用搜索。

其实虽然有了估价函数,KDT 单次找离一个点最近/最远的点还是可以被卡到 O ( n ) O(n) O(n)所以可以去分治。

最近点对(以下均为曼哈顿距离):

inline int getmn(int k){
	if(!k) return 2e9;
	int ans=0;
	ff(i,0,1){
		if(now.x[i]<t[k].mn[i]) ans+=t[k].mn[i]-now.x[i];
		else if(now.x[i]>t[k].mx[i]) ans+=now.x[i]-t[k].mx[i];
	}
	return ans;
}

最远点对:

inline int getmx(int k){
	if(!k) return 0;
	int ans=0;
	ff(i,0,1){
		ans+=max(t[k].mx[i]-now.x[i],now.x[i]-t[k].mn[i]);
	}
	return ans;
}

估价函数带来的另一个优化是当左右子树都合法时,优先搜索估价值更优的子树,增加另一个子树不被搜到的概率。

比如查询最近点对就是这样的:

void qmin(int k){
	if(!k) return;
	mnn=min(mnn,dis(a[k],now));
	int ml=getmn(ls),mr=getmn(rs);
	if(ml<mr){
		if(ml<mnn) qmin(ls);
		if(mr<mnn) qmin(rs);
	}
	else{
		if(mr<mnn) qmin(rs);
		if(ml<mnn) qmin(ls);
	}
}

修改查询

单点/区间修改,单点/区间查询都类似线段树,但是要注意特判当前结点要不要修改/计入答案

插入删除

以插入为例,大量节点的插入会破坏 O ( log ⁡ n ) O(\log n) O(logn) 的树高,这时候我们考虑类似替罪羊树的思想,设置一个常数 l i m lim lim(一般为 0.75 0.75 0.75),当某个点的某个儿子的子树的大小超过这个点子树大小的 l i m lim lim 倍时,把树暴力拍扁重构成一棵平衡的树。

为了省空间可以用一个 v e c t o r vector vector 存储废弃的节点来重新利用,但是 build 传地址党用之前不用忘了清空 l s ls ls r s rs rs不然就又要调试 2h 了。

多出来的一堆函数:

vector<int> rub;
inline int neww(){
	int ans;
	if(!rub.empty()) ans=rub.back(),rub.pop_back();
	else ans=++tot;
	return ans;
}
void del(int k,int pos){
	if(ls) del(ls,pos);
	a[pos+t[ls].siz+1]=t[k].val,rub.push_back(k);
	if(rs) del(rs,pos+t[ls].siz+1);
}
inline void check(int &k,int flag){
	if(max(t[ls].siz,t[rs].siz)>t[k].siz*lim) 
		del(k,0),build(k,1,t[k].siz,flag);
}
void ins(int &k,qaq tmp,int flag){
	if(!k){
		k=neww(),t[k].siz=1,t[k].val=tmp;
		t[k].mn[0]=t[k].mx[0]=tmp.x[0];
		t[k].mn[1]=t[k].mx[1]=tmp.x[1];
		ls=rs=0;
		return;
	}
	if(tmp.x[flag]<=t[ls].val.x[flag]) ins(ls,tmp,flag^1);
	else ins(rs,tmp,flag^1);
	pushup(k),check(k,flag);
}

一些例题

[国家集训队]JZPFAR

传送门

这就是调了 2h 本题。

k k k 远问题就只要开个堆动态维护就好了,其它都一样。

为什么失忆了忘了 priority_queue 重载要把符号反过来是值得反思的。

[YBTOJ]染颜色

传送门

两个限制:距离和在子树内。距离是对深度的区间限制,在子树内是对 d f n dfn dfn 序的区间限制,这便是个二维问题。使用 KDT 区间修改单点查询即可。

这个限制的转化思想还是很巧妙的pap。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值