K-D tree学习笔记

news/2025/1/16 16:05:21/文章来源:https://www.cnblogs.com/YYYmoon/p/18655807

翻译过来就是维护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);
}

dalao

插入/删除

如果我们维护的点集会发生变动,此时静态建树的 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上有专门的介绍,算法也是分治。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/870168.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

本次小论文minor revision中的知识积累

可以发邮件向编辑申请延期返修截止日期 https://cn.service.elsevier.com/app/answers/detail/a_id/29653/c/10595/supporthub/publishing/role/作者/ https://zhuanlan.zhihu.com/p/577324425申请邮件模板:如何在Editorial Manager系统中提交修改稿?【爱思唯尔Editorial Man…

多通道传感器接入集中控制频率温度 传感器集线器带来更多方便

多通道传感器接入集中控制频率温度 传感器集线器带来更多方便现场传感器太多,编号容易混乱?传感器集线器可以将多路传感器轮流切换到单一接口,为现场提供更多方便。操作简便直观,使用一个百位拨动开关和两个旋转开关(十位和个位)自由切换到任意传感器。 传感器通道多,最…

实战指南:优化采购流程,实现高效采购管理

优化采购流程需要从多个方面入手,包括明确采购需求、加强供商管理、优化采购谈判与合同签订、加强采购执行与跟踪、提高质量控制与验收水平、进行成本分析与优化、加强人员培训与发展以及注重合规与风险管理。通过这些措施的实施,可以显著提高采购效率和质量,降低采购成本,…

Lableview 标签软件 | LABELVIEW 条形码标签软件

Lableview 标签软件 | LABELVIEW 条形码标签软件Lableview 专业顾问 手机|微信:13928851814值得您信赖的条形码标签软件稳健可靠的条形码标签创建与集成简单的数据库连接易于使用的界面和提供便利的向导100 多种条形码符号体系可自定义的打印界面变量选用表增加了灵活性LABE…

原生JS实现一个日期选择器(DatePicker)组件

这是通过原生HTML/CSS/JavaScript完成一个日期选择器(datepicker)组件,一个纯手搓的组件的开发。主要包括datepicker静态结构的编写、日历数据的计划获取、组件的渲染以及组件事件的处理。 根据调用时的时间格式参数,可以控制短日期格式或长日期格式。实现效果(短日期格式…

LabelMatrix 标签软件 | LABEL MATRIX 条形码标签软件

LabelMatrix 标签软件 | LABEL MATRIX 条形码标签软件LabelMatrix 专业顾问 手机|微信:13928851814LABEL MATRIX 条形码标签软件 借助功能丰富的条形码标签软件,为将来的发展奠定坚实的基础适用于简单标签需求的条形码标签软件提供帮助的内置向导熟悉的 Windows 用户界面10…

《操作系统真相还原》实验记录2.5——线程实现

本文章实现内容如下: 1. 实现单线程的创建功能 2. 实现多线程调度的基本功能,包含:时钟中断处理函数;任务调度器;任务切换函数;零、项目说明本项目仓库现已公开,地址:GitHub:-HC-OS-操作系统设计项目 本项目当前进度:已完成多线程调度基础功能;一、前置知识点 1.1 …

360SafeBrowsergetpass:红队360浏览器密码抓取工具

免责声明 仅限用于技术研究和获得正式授权的攻防项目,请使用者遵守《中华人民共和国网络安全法》,切勿用于任何非法活动,若将工具做其他用途,由使用者承担全部法律及连带责任,作者及发布者不承担任何法律连带责任项目介绍 红队一键辅助抓取360安全浏览器密码的CobaltStrik…

连续番茄时钟和长休息

原始时钟只支持手动25min+休息,用js增加连续自动番茄去支持Tempermonkey的浏览器的Add-ons安装 代码 https://pomodoro.pomodorotechnique.com/ 打开后刷新一次// ==UserScript== // @name Automated Pomodoro with Long Break // @namespace http://tampermonkey.…

Kernel Memory: 强大的AI驱动记忆系统

Kernel Memory简介 Kernel Memory(简称KM)是由微软开发的一个强大的多模态AI服务,专门用于高效索引和处理大规模数据集。它支持检索增强生成(RAG)、合成记忆、提示工程和自定义语义记忆处理等先进功能,为构建智能应用提供了强大的基础设施。 KM可以作为Web服务、Docker容器、C…

亚矩阵云手机:服务于未来新型电商矩阵的助力者

亚矩阵云手机是基于端云一体虚拟化技术 通过云网、安全、AI等数字化能力,弹性适配用户个性化需求,释放手机本身硬件资源,随需加载海量云上应用的手机形态 简单来说,云手机=云服务器+Android OS,用户可以远程实时控制云手机,实现安卓APP的云端运行;也可以基于云手机的基础算力,高…

0.LED基础控制

典中典之发光二极管,我从小学到大长脚是正级,断脚是负极 里面大块的是负级,小块的是正极 电阻标注:若标注102 -> 代表着为1K电阻(10^2) 若标注473 -> 代表着为47K电阻(47 * 10^3) 以此类推 其他器件标注也为类似模式RP7 RP9模块为限流电阻 此单片机使用TTL规范(高…