KD-Tree 学习笔记
定义
\(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\)也可以作为骗分的利器