是一个能维护多维空间中点集的数据结构。
各方面操作复杂度据说都比较玄学。
假设现在要维护的是一个 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
#include
#include
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远点对 题解