kD-Tree
k-D Tree
k-D Tree(KDT , k-Dimension Tree) 是一种可以 高效处理\(k\)维空间信息 的数据结构。
在算法竞赛的题目中,一般有\(k=2\) 。
作者因水平有限,在此文章中只讨论\(k=2\) 的情况。
感性理解
(白嫖自\(oi-wiki\))
其实就是对平面的不断划分。图中每个小矩形在\(kD-Tree\) 中都对应一个叶子节点,然后我们就可以像二叉搜索树那样维护子树信息以高效地回答某些询问。
建树
步骤如下:
- 若当前平面中只有一个点,返回当前点。
- 选择一个维度(\(k=2\) 时就是横或纵),在当前层按照这个维度进行划分。
- 选择一个点作为该平面所对应子树的根节点,将所有在选择的维度上小于根节点的点置于左子树中,其他的置于右子树中,递归地建树。
选择维度要么交错地选择(比如第一次纵向划分,第二次横向划分,第三次再纵向划分...),要么选择方差最大的维度进行划分。(不得不说前者的确比后者好写多了,不过据说后者在某些情况下有奇效??)
至于选点,一般都选择该维度上的中位数,这样可以是左右两边分得尽量平均,从而保证了树高在\(\mathcal O(\log_2n)\) 级别。
实现时可以采用函数\(nth\_element\) ,它的复杂度是线性的,可以将中位数排在正确位置上,同时也可以保证左边的数都比它小,右边的数都比它大,这样也便于我们后面递归地建立左右子树。
于是不难得知如此建树复杂度为\(\mathcal O(n\log_2n)\)
插入/删除
先来说说插入
显然直接按照每一层的维度和根节点判断往左子树走还是往右子树走是很$trivial $ 的,意即插入的正确性是很好保证的。
现在我们关心的是如果某个位置插入次数过多可能会导致\(kD-Tree\) 极不平衡,从而是复杂度退化。
在这里我们采用替罪羊树的思想,即设一个阈值\(\alpha\) ,当某个子树大小和整个树大小的比值超过\(\alpha\) 时,我们直接将其拍扁,然后按照上面所讲的方式重构。
再来看看删除
仍然类似替罪羊树,采用惰性删除,即对于一个删除节点将它打上删除标记,然后当未删除的节点个数在整棵子树中占比小于\(\alpha\) 时重构,重构时直接不考虑这些节点即可。
维护子树信息
基本的包括当前节点对应平面上的矩形位置,还有些题目需要维护点的权值和之类的,当然都是因题而异啦
题目
K 远点对
给定若干个点,求第\(K\) 远点对距离的平方。
sol.
建出\(kD-Tree\) ,套路化地维护一个小根堆。
从每个节点出发进行考虑。
如果当前点到询问点的距离大于堆顶,将堆顶弹出,然后再将这个距离插入其中。最后堆顶就是答案。只是这样复杂度难以承受,需要剪枝。
我们可以对\(kD-Tree\) 上的节点维护对应平面的矩形位置。如果当前点到矩形的最远距离都小于当前堆的堆顶,显然这棵子树不用再考虑。
此外,假设某个节点的两个子树都可能包含答案,我们可以优先查询离当前点远的矩形,回来后再根据堆顶判断是否需要再进入另一棵子树。
注意题目要求是无序点对,因此需要将\(K\) 乘\(2\)
于是我们就用暴力算法过掉了此题(因为虽然有种种剪枝,但是实际复杂度仍为\(\mathcal O(n^2)\))
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef double db;
const int N=1e5+5;
priority_queue<ll,vector<ll>,greater<ll> >q;
int n,k;
struct pt{int x,y;}p[N];
inline bool cmpx(const pt&x,const pt&y){return x.x<y.x;}
inline bool cmpy(const pt&x,const pt&y){return x.y<y.y;}
inline ll dis(int a,int b,int c,int d){return 1ll*(a-c)*(a-c)+1ll*(b-d)*(b-d);}
namespace kd_tree
{
int lx[N],rx[N],ly[N],ry[N],d[N],lc[N],rc[N];
inline void up(int x)
{
lx[x]=rx[x]=p[x].x;
ly[x]=ry[x]=p[x].y;
if(lc[x])
lx[x]=min(lx[x],lx[lc[x]]),rx[x]=max(rx[x],rx[lc[x]]),
ly[x]=min(ly[x],ly[lc[x]]),ry[x]=max(ry[x],ry[lc[x]]);
if(rc[x])
lx[x]=min(lx[x],lx[rc[x]]),rx[x]=max(rx[x],rx[rc[x]]),
ly[x]=min(ly[x],ly[rc[x]]),ry[x]=max(ry[x],ry[rc[x]]);
}
int build(int l,int r)
{
if(l>r)return 0;
db avx=0,avy=0,vx=0,vy=0;
for(int i=l;i<=r;++i)avx+=p[i].x,avy+=p[i].y;
avx/=r-l+1,avy/=r-l+1;
for(int i=l;i<=r;++i)
vx+=(p[i].x-avx)*(p[i].x-avx),
vy+=(p[i].y-avy)*(p[i].y-avy);
int mid=l+r>>1;
if(vx>vy)d[mid]=1,nth_element(p+l,p+mid,p+r+1,cmpx);
else d[mid]=2,nth_element(p+l,p+mid,p+r+1,cmpy);
lc[mid]=build(l,mid-1),rc[mid]=build(mid+1,r);
up(mid);return mid;
}
inline ll f(int pos,int t)
{
ll ans=0;
ans=max(ans,dis(p[pos].x,p[pos].y,lx[t],ly[t]));
ans=max(ans,dis(p[pos].x,p[pos].y,lx[t],ry[t]));
ans=max(ans,dis(p[pos].x,p[pos].y,rx[t],ly[t]));
ans=max(ans,dis(p[pos].x,p[pos].y,rx[t],ry[t]));
return ans;
}
inline void upd(ll d)
{
if(q.top()<d)q.pop(),q.push(d);
}
void query(int l,int r,int x)
{
if(l>r)return;
int mid=l+r>>1;
upd(dis(p[x].x,p[x].y,p[mid].x,p[mid].y));
if(l==r)return;
ll dl=f(x,lc[mid]),dr=f(x,rc[mid]),u=q.top();
if(dl>u&&dr>u)
{
if(dl>dr)
{
query(l,mid-1,x);
if(q.top()<dr)query(mid+1,r,x);
}
else
{
query(mid+1,r,x);
if(q.top()<dl)query(l,mid-1,x);
}
}
else
{
if(dl>u)query(l,mid-1,x);
if(dr>u)query(mid+1,r,x);
}
}
}
using namespace kd_tree;
int main()
{
scanf("%d%d",&n,&k);k<<=1;
while(q.size()<k)q.push(0ll);
for(int i=1;i<=n;++i)scanf("%d%d",&p[i].x,&p[i].y);
build(1,n);
for(int i=1;i<=n;++i)query(1,n,i);
printf("%lld\n",q.top());
return 0;
}
简单题
平面上插入点(点有点权),询问某个矩形范围内所有点的权值和。卡空间,强制在线。
sol.
此题简直就是给\(kD-Tree\) 量身定做的题目!
此题中需要维护节点对应平面的矩形位置以及其内的点的权值和。插入按照之前讲的来做。
询问时如果当前节点的矩形被询问矩形完全包含,直接返回;如果完全不包含,返回\(0\) ;否则考虑是否包含根节点,累加答案,然后再进入左右子树来查询即可。这样做单次查询最少\(\mathcal O(\log_2n)\) 最多\(\mathcal O(\sqrt n)\) (我也不会证明...)
#include<bits/stdc++.h>
using namespace std;
typedef double db;
const int N=2e5+5;
const db alpha=0.725;
int xl,xr,yl,yr;
struct pt{int x,y,v;}p[N];
inline bool cmpx(const int&x,const int&y){return p[x].x<p[y].x;}
inline bool cmpy(const int&x,const int&y){return p[x].y<p[y].y;}
namespace kd_tree
{
int rt,sz[N],lc[N],rc[N],lx[N],rx[N],ly[N],ry[N],sum[N],g[N],t;
void go(int x)
{
if(lc[x])go(lc[x]);
g[++t]=x;
if(rc[x])go(rc[x]);
}
inline void up(int rt)
{
sz[rt]=1,sum[rt]=p[rt].v;
lx[rt]=rx[rt]=p[rt].x,ly[rt]=ry[rt]=p[rt].y;
if(lc[rt])
lx[rt]=min(lx[rt],lx[lc[rt]]),rx[rt]=max(rx[rt],rx[lc[rt]]),
ly[rt]=min(ly[rt],ly[lc[rt]]),ry[rt]=max(ry[rt],ry[lc[rt]]),
sz[rt]+=sz[lc[rt]],sum[rt]+=sum[lc[rt]];
if(rc[rt])
lx[rt]=min(lx[rt],lx[rc[rt]]),rx[rt]=max(rx[rt],rx[rc[rt]]),
ly[rt]=min(ly[rt],ly[rc[rt]]),ry[rt]=max(ry[rt],ry[rc[rt]]),
sz[rt]+=sz[rc[rt]],sum[rt]+=sum[rc[rt]];
}
int build(int l,int r,bool o)
{
if(l>r)return 0;
int mid=l+r>>1;
if(o)nth_element(g+l,g+mid,g+r+1,cmpx);
else nth_element(g+l,g+mid,g+r+1,cmpy);
lc[g[mid]]=build(l,mid-1,o^1),rc[g[mid]]=build(mid+1,r,o^1);
up(g[mid]);return g[mid];
}
inline void rebuild(int&x,bool o)
{
t=0;go(x);
x=build(1,t,o);
}
inline bool pd(int x){return alpha*sz[x]<=1.0*max(sz[lc[x]],sz[rc[x]]);}
void ins(int&x,int v,bool o)
{
if(!x){x=v;up(x);return;}
if(o)p[v].x<=p[x].x?ins(lc[x],v,o^1):ins(rc[x],v,o^1);
else p[v].y<=p[x].y?ins(lc[x],v,o^1):ins(rc[x],v,o^1);
up(x);if(pd(x))rebuild(x,o);
}
int query(int rt)
{
if(xl>rx[rt]||xr<lx[rt]||yl>ry[rt]||yr<ly[rt])return 0;
if(xl<=lx[rt]&&xr>=rx[rt]&&yl<=ly[rt]&&yr>=ry[rt])return sum[rt];
int ans=0;
if(xl<=p[rt].x&&p[rt].x<=xr&&yl<=p[rt].y&&p[rt].y<=yr)ans+=p[rt].v;
ans+=query(lc[rt])+query(rc[rt]);
return ans;
}
}
using namespace kd_tree;
int n;
int main()
{
scanf("%d",&n);
int lans=0,opt,a,b,c;
for(int i=1;;)
{
scanf("%d",&opt);
if(opt==1)
{
scanf("%d%d%d",&a,&b,&c);
p[i].x=a^lans,p[i].y=b^lans,p[i].v=c^lans;
ins(rt,i,0);++i;
}
else if(opt==2)
{
scanf("%d%d%d%d",&xl,&yl,&xr,&yr);
xl^=lans,xr^=lans,yl^=lans,yr^=lans;
printf("%d\n",lans=query(rt));
}
else break;
}
return 0;
}