不想学数学,所以只能连续不断地写数据结构()
K-D tree 是一种用来处理多维空间问题的数据结构,处理 k k k 维空间信息时时间复杂度最坏为 n 1 − 1 k n^{1-\frac{1}{k}} n1−k1。通常用的是 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);
}
传送门
这就是调了 2h 本题。
第 k k k 远问题就只要开个堆动态维护就好了,其它都一样。
为什么失忆了忘了 priority_queue 重载要把符号反过来是值得反思的。
传送门
两个限制:距离和在子树内。距离是对深度的区间限制,在子树内是对 d f n dfn dfn 序的区间限制,这便是个二维问题。使用 KDT 区间修改单点查询即可。
这个限制的转化思想还是很巧妙的pap。