首页 > 其他分享 >K-D tree学习笔记

K-D tree学习笔记

时间:2025-01-16 16:13:12浏览次数:1  
标签:lc int 复杂度 tree 笔记 学习 rc mx

翻译过来就是维护k维信息的树,是一种可以高效处理k维空间信息的数据结构。

一般在算法竞赛中,k=2的情况较多。

考虑对于一维数组,我们想要找到一个y,使得对于给定的x,有|x-y|最小。那么不妨考虑二叉搜索树(就是二分法),取数组的中位数为根,构造一棵树,使得每个点的左儿子小于它,右儿子大于它。那么当x比当前节点小时,走左子树;大,走右子树。

而 K-D tree 就具有二叉搜索树的形态。

建树

如果我们已知k维空间中若干不同点的坐标,要建一棵 K-D tree 。就选择一个维度和一个切割点,使在当前维度上比当前点值小的都归到左子树,大的归到右子树。

为了使树的形态平衡,保证复杂度,我们轮流选择k个维度,且每次在选择切割点时选择该维度上的中位数。这样树高是logn+O(1)的。

时间瓶颈是寻找k维上的中位数,如果每次sort,会 \(O(nlog^2n)\) 。实际上我们只需要考虑我们需要的东西:另中位数在中间,左边的比它小,右边的比它大,单次可以直接调用nth_element优化到 \(O(n)\),总体 \(O(nlogn)\) .

int tot;
struct kd_tree{
	int lc,rc,v[2],mn[2],mx[2];
}t[maxn];
void pushup(int x){
	for(int i=0;i<2;i++){//取k=2的情况 
		t[x].mn[i]=t[x].mx[i]=t[x].v[i];
		if(t[x].lc){
			t[x].mn[i]=min(t[x].mn[i],t[t[x].lc].mn[i]);
			t[x].mx[i]=max(t[x].mx[i],t[t[x].lc].mx[i]);
		}
		if(t[x].rc){
			t[x].mn[i]=min(t[x].mn[i],t[t[x].rc].mn[i]);
			t[x].mx[i]=max(t[x].mx[i],t[t[x].rc].mx[i]);
		}
	}
} 
void build(int &x,int l,int r,int typ){
	x=++tot;
    int mid=(l+r)>>1;
	nth_element(a+l,a+mid,a+r+1,[typ](node p,node q){return p.v[typ]<q.v[typ];});
	//这里,由于要调用typ,把排序函数写这里比较好 
	t[x].v[0]=a[mid].v[0],t[x].v[1]=a[mid].v[1];
	if(l<mid) build(t[x].lc,l,mid-1,!typ);
	if(mid<r) build(t[x].rc,mid+1,r,!typ);
	pushup(x);
}

插入/删除

如果我们维护的点集会发生变动,此时静态建树的 K-D tree 的复杂度就无法得到保证。而常见的平衡树维护平衡的两个操作,旋转和随机优先级,都不能用到 K-D tree 上。所以我们常用以下方式。

1.根号重构

一种方法是用替罪羊树的重构套路,设置一个平衡因子对 K-D tree 重构。但实际上这样只能保证高度是O(logn),不是严格的logn+O(1),所以查询复杂度可能退化。(一般能过)

正确的方法是设定一个阈值B,(每次插入时直接从根节点开始和每个节点比较并向下递归?)当插入次数达到B时暴力重构整棵树。删除时仍用(惰性删除?),当树内删除数量达到B时暴力重构。

因此,当我们取到 \(B=\sqrt{nlogn}\) 时复杂度最优,单次均摊 \(O(\sqrt{nlogn})\)

2.二进制分组

如果无删除操作,这种做法是更优的。维护若干棵大小为 \(2^i\) 的 K-D tree ,满足这些树的大小之和为 n。

插入时,新增一棵大小为 \(2^0\) 的 K-D tree ,不断将相同大小的 K-D tree 合并。实际操作时,可以先将能一起合并的所有树拍扁,只需要重构一次。

总复杂度 \(O(nlog^2n)\)。对于每一位 \(2^i\) ,每次操作到时贡献的节点数为 \(2^i\) ,但又只有 \(\frac{n}{2^i}\) 次能产生贡献。相当于每一位都总共贡献过n个节点,加上建树还有一个log,故总复杂度得证。

int tot=0,top,pol[maxn];
void del(int &x){
	pol[++top]=x;
	t[x]={0,0,0,0,0,0,0,0};
	x=0;
}
int newnode(){
	return top?pol[top--]:++tot;//回收节点省空间
} 
void redo(int &x){
	if(!x) return ;
	a[++cnt]={t[x].v[0],t[x].v[1]};
	redo(t[x].lc),redo(t[x].rc);
	del(x);
}

int main(){
	for(int i=1;i<=n;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		a[cnt=1]={x,y};
		for(int j=0;j<20;j++){
			if(!rt[j]){//没有当前大小的树了,建树 
				build(rt[j],1,cnt,0);
				break;
			}
			else redo(rt[j]);
		}
	}
	return 0;
}

查询

1.矩阵查询

递归。

若当前子树对应的矩形和目标矩形无交点,则不继续搜索;
全部被目标矩形包含,返回整个子树信息;
否则,先判断当前节点合法性,继续递归。

这样复杂度单次 \(O(n^{1-\frac{1}{k}})\) ,证明看oi-wiki

2.邻域查询

即求出平面上一个点的最近/远点。

以求最近点为例。暴搜,对每个子树对应的矩阵设计一个估价函数(如,查询点到这个矩阵的最短距离),启发式搜索,先搜索估价函数更优的子树的答案。如果当前节点估价函数都大于当前答案,直接退出。

这个部分时间复杂度的瓶颈就在于,我们用于判断的估价函数是查询点到这个矩阵的一个计算值。

以估价函数为查询点到这个矩阵的最短距离为例。然而当前点x到子树矩形边界的最短距离mindis——我们理想情况下想去把ans更新成的最优情况,不一定有一个具体存在的点y能使Dis(x,y)=mindis,自然就不能把ans更新到那么优。

所以假设先遍历理想状态下mindis的左子树,遍历后答案也可能比右子树中点的答案劣。还需要看x到右子树矩形边界的最短距离,若小于ans,还得遍历右子树更新答案。

(所以轻轻松松被卡到O(n)......

但随机数据下 K-D tree 求解这个问题为均摊 \(O(logn)\)。(骗分的曙光

[SDOI2010] 捉迷藏
//用扫描线会很好维护
//这里使用kd tree,相当于对每个点求出最近/远邻 
#include<bits/stdc++.h>
using namespace std;
const int maxn=5e5+5;
int n,tot,rt,ans=2e9,mx,mn,b[maxn];
struct node{
	int v[2];
}a[maxn];
struct kd_tree{
	int lc,rc,v[2],mn[2],mx[2];
}t[maxn];
void pushup(int x){
	for(int i=0;i<2;i++){
		t[x].mn[i]=t[x].mx[i]=t[x].v[i];
		if(t[x].lc){
			t[x].mn[i]=min(t[x].mn[i],t[t[x].lc].mn[i]);//!!查了俩小时 
			t[x].mx[i]=max(t[x].mx[i],t[t[x].lc].mx[i]);
		}
		if(t[x].rc){
			t[x].mn[i]=min(t[x].mn[i],t[t[x].rc].mn[i]);
			t[x].mx[i]=max(t[x].mx[i],t[t[x].rc].mx[i]);
		}
	}
}
void build(int &x,int l,int r,int typ){
	x=++tot; 
	int mid=(l+r)>>1;
	nth_element(a+l,a+mid,a+r+1,[typ](node p,node q){
		return p.v[typ]<q.v[typ];
	});
	t[x].v[0]=a[mid].v[0],t[x].v[1]=a[mid].v[1];
	if(l<mid) build(t[x].lc,l,mid-1,!typ);
	if(mid<r) build(t[x].rc,mid+1,r,!typ);
	pushup(x);
}
int sol(int x){
	return x==0?2e9:x;
}
int dis(int x,int y){
	return abs(t[x].v[0]-t[y].v[0])+abs(t[x].v[1]-t[y].v[1]);
}
int mxdis(int x,int y){
	int t1=abs(t[x].v[0]-t[y].mn[0])+max(abs(t[x].v[1]-t[y].mn[1]),abs(t[x].v[1]-t[y].mx[1]));
	int t2=abs(t[x].v[0]-t[y].mx[0])+max(abs(t[x].v[1]-t[y].mn[1]),abs(t[x].v[1]-t[y].mx[1]));
	return max(t1,t2);
}
int mndis(int x,int y){
	int t1=(t[x].v[0]<t[y].mn[0])?t[y].mn[0]-t[x].v[0]:((t[x].v[0]>t[y].mx[0])?t[x].v[0]-t[y].mx[0]:0);
	int t2=(t[x].v[1]<t[y].mn[1])?t[y].mn[1]-t[x].v[1]:((t[x].v[1]>t[y].mx[1])?t[x].v[1]-t[y].mx[1]:0);
	return t1+t2;
}
void mxque(int x,int p){
	mx=max(mx,dis(p,x));
	if(!t[x].lc){
		if(t[x].rc) mxque(t[x].rc,p);
	}
	else if(!t[x].rc) mxque(t[x].lc,p);
	else{
		int mx1=mxdis(p,t[x].lc),mx2=mxdis(p,t[x].rc);
		if(mx>=mx1&&mx>=mx2) return ;
		if(mx1>=mx2){
			mxque(t[x].lc,p);
			if(mx<mx2) mxque(t[x].rc,p);
		} else{
			mxque(t[x].rc,p);
			if(mx<mx1) mxque(t[x].lc,p);
		}
	}
}
void mnque(int x,int p){
	mn=min(mn,sol(dis(p,x)));//细节 
	if(!t[x].lc){
		if(t[x].rc) mnque(t[x].rc,p);
	}
	else if(!t[x].rc) mnque(t[x].lc,p);
	else{
		int mn1=mndis(p,t[x].lc),mn2=mndis(p,t[x].rc);
		if(mn<=mn1&&mn<=mn2) return ;
		if(mn1<=mn2){
			mnque(t[x].lc,p);
			if(mn>mn2) mnque(t[x].rc,p);
		} else{
			mnque(t[x].rc,p);
			if(mn>mn1) mnque(t[x].lc,p); 
		}
	}
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d%d",&a[i].v[0],&a[i].v[1]);
	}
	for(int i=1;i<=n;i++) b[i]=i;
	build(rt,1,n,0);
	for(int i=1;i<=n;i++){
		mx=0,mn=2e9;
		mxque(rt,i);
		mnque(rt,i);
		ans=min(ans,mx-mn);
	}
	printf("%d",ans);
	return 0;
} 

ps.

1.调 K-D tree 小技巧:阅读程序,重点关注下标和数组

2.实际上如果真要考 K-D tree 的邻域查询,那么考欧氏距离最远邻居多。(但其实这正解好像是旋转卡壳,K-D tree顶多是优秀的骗分手段)

原因如下。

若限制为曼哈顿距离:二维平面最近/远邻,可以直接用cdq分治(不带修改——二维偏序,带修改——三维偏序)解决,\(O(nlog^2n)\) 并不会因为数据影响正确性。例如[Violet] 天使玩偶/SJY摆棋子。

若限制为欧氏距离:二维平面最近邻,即平面最近点对。oiwiki上有专门的介绍,算法也是分治。

标签:lc,int,复杂度,tree,笔记,学习,rc,mx
From: https://www.cnblogs.com/YYYmoon/p/18655807

相关文章

  • 人工智能之深度学习-[1]-了解深度学习
    深度学习深度学习(DeepLearning)是机器学习(MachineLearning)的一种方法,它通过模拟人脑的神经网络结构来进行学习和推理。深度学习使用多层神经网络来分析和建模数据,尤其擅长处理大量数据和复杂模式的识别,如图像、语音、文本等。深度学习的“深度”指的是神经网络中的层数,一......
  • 又是做题笔记
    说在前面都是一些OIBingo上的题,可能还会有部分ABC的题。P5929[POI1999]地图题目大意将一个长度为\(N\)的序列染成\(M\)种颜色。对于一个颜色\(k\),要求染\(k\)颜色的点权与\(A(k)\)的差的绝对值尽量地小。其中\(A(k)\)定义如下:在染颜色\(k\)的点中至少......
  • 入门网络安全工程师要学习哪些内容_网络安全工程师需要学什么考什么证
    大家都知道网络安全行业很火,这个行业因为国家政策趋势正在大力发展,大有可为!但很多人对网络安全工程师还是不了解,不知道网络安全工程师需要学什么?知了堂小编总结出以下要点。网络安全工程师是一个概称,学习的东西很多,具体学什么看自己以后的职业定位。如果你以后想成为安......
  • 大数据学习记录,Java基础(4)
    多态多态的形式和体现1.对象的多态性对象的多态性:父类的引用指向子类的对象格式:(父类类型:指子类继承的父类类型,或者实现的接口类型)父类类型变量名=子类对象;例:Personp=newStudent();Objecto=newPerson();//Object类型的变量o,指向Person类型的对象o=newStuden......
  • 大数据学习记录,Java基础(3)
    面向对象面向对象的特征:封装随着系统越来越复杂,类会越来越多,那么类之间的访问边界必须把握好,面向对象的开发原则要遵循“高内聚、低耦合”,而“高内聚,低耦合”的体现之一:高内聚:类的内部数据操作细节自己完成,不允许外部干涉;低耦合:仅暴露少量的方法给外部使用,尽量方便外部调用......
  • (未完工)「学习笔记」二维数点问题
    0.0前言看了一个晚上,加上同桌的讲解,大致了解了二维数点问题的基本思路。0.1前置知识可持久化线段树树状数组1.0概述二维数点问题的一般形式是形如“给定平面上\(n\)个点,每次询问给定一个矩形,求该位于矩形内的点的个数”一类问题,模板题为P2163[SHOI2007]园丁的......
  • Windows系统下NoteFlow的下载:提供直观、易用的界面,使用户能够轻松创建和连接笔记节点
    NoteFlow(适用于python3.9及以上):功能:节点笔记软件,有助于更好地组织和管理笔记内容。特点:提供直观、易用的界面,使用户能够轻松创建和连接笔记节点。一.从github上获取创作者的代码跳伞到github下载文件压缩包二.Windows只按照pip就行使用pip安装(适用于所有平台)打开命令行......
  • 移除clock tree的don‘t touch属性
    我正在「拾陆楼」和朋友们讨论有趣的话题,你⼀起来吧?拾陆楼知识星球入口 为了让clocktree不被绕线或优化影响,我们会使用mark_clock_tree-dont_touch-freeze_routing,但是route阶段可能会产生全局范围内的绕线问题,集中出现在clocknet与signalnet相关绕线上。这里可以......
  • Datawhale组队学习打卡-Fun-transformer-Task1引言
    文章目录写在前面Embedding:词汇到向量空间的映射**引入Embedding的意义****1.Embedding的定义****2.高维稀疏表示的特点****3.区别****1.什么是Embedding****2.Embedding的作用****3.一些常见的Embedding方法****4.代码示例****5.一些拓展**Seq2SeqSeq2Seq......
  • C++基础学习01
    C++基础学习012025-01-1611:29:04星期四首先是函数的结构,具体包括四个部分:返回值类型,函数名,形参列表和函数体接下来是输出输出流和C++标准库。我们要实现一个简单的A+B的输入输出,这里需要用到C++的iostream库,于是我们就需要使用cin和cout分别表示输入输出。然后是关于......