更新于 2026/5/7 23:33:20 作者

command_block

KDtree\rm KD-tree 是一种能对 kk 维空间中的进行快速检索的树形数据结构。

相对另一个传统方法《树套树》而言,有以下优势 :

  • 部分情况下代码实现更简单

  • 空间复杂度 O(n)O(n)

  • 可以搭配传统的整体标记系统。

KDT\rm KDT 搭配剪枝乱搞后会在某些问题中取得奇效,特别是在考场上收益率特别高。

KDtree\rm KD-tree 的构造

KDT\rm KDT 是一棵二叉排序树,每个节点有一个管辖(超)矩形。保证 uu 子树中的所有点都在 uu 的管辖矩形内。

左右儿子的管辖矩形是由父亲的管辖矩形水平划分而得。KDT\rm KDT 的核心思想就是对矩形的水平分割。

在满足上述要求的前提下,KDT\rm KDT 还要使得高度严格为 log2n+O(1)\log_2n+O(1)

这是一个 KDT\rm KDT 以及平面划分的例子。

  • 具体构造流程 :

挑选一个维度,使用与该维度正交的超平面来分割当前区域。

对于二维情况,即选择 x,yx,y 其中一维,使用水平直线 y=cy=c 或竖直直线 x=cx=c 来划分当前矩形。

不妨设我们在 xx 坐标上划分。

为了分割得尽量均匀,我们令 cc 为当前区域中,点的 xx 坐标的中位数,这样恰能将点集分成两半。

将当前区域以及点集划分成两部分后,继续分治。

如上图,第一次在 xx 坐标划分选中了 号点,划分线为图中红线。

关于分割维度的选择,有如下两种常见的方案 :

  • 轮换划分 : 每个维度轮着来。

  • 方差划分 : 优先划分方差较大的维度。

后者实现略复杂,但是传说有奇妙加速,具体原理不明。

寻找中位数时,使用 nth_element 指令可以做到线性。

这样,建树的总复杂度就是 T(n)=2T(n/2)+O(n)=O(nlogn)T(n)=2T(n/2)+O(n)=O(n\log n)

KDtree\rm KD-treerange query\rm range\ query

在不利用差分的前提下,支持将一个矩形区域内的点,划分成 O(n)O(\sqrt{n}) 个点负责的区域。

做法非常暴力,若查询范围包含当前范围,则取当前范围。

  • 复杂度分析

假设我们查询矩形 RR ,将树上的节点按照管辖矩形分类 :

  1. RR 无交集。

  2. 完全包含在 RR 内。

  3. 部分包含在 RR 内。

观察查询算法的行为,其一路经过 33 类点,且一旦遇到 1,21,2 类点就停下。

而且,1,21,2 类点的子树中显然无三类点,这样,查询的复杂度和 33 类点的个数直接相关。

对于矩形的每条边,分别计算其“穿过”的节点个数。

我们以轮换划分来分析。在相邻的两轮中,分别对 x,yx,y 进行了划分。

能够发现,在划分出的四个区域中,每个区域的大小都是原来的 1/41/4 ,任意的一条直线只能穿过其中两个。

这样,设 T(n)T(n) 为在大小为 nnKDT\rm KDT 中一条直线所经过的区域数,那么有

T(n)=2T(n/4)+O(1)T(n)=2T(n/4)+O(1)T(n)=O(n)T(n)=O(\sqrt{n})

所以,一条直线经过的区域个数为 O(n)O(\sqrt{n})

: 有一点细节,我们的划分出的矩形区域并非严格不交,而在边界上是相交的。

如果一条直线恰好于边界重合,其可能经过多于 22 个区域。

但是,如果出现这种情况,递归向儿子时,直线总是在矩形的一侧,则不会再次经过多于 22 个区域,复杂度仍是正确的。

如果想避免这种情况,将坐标乘以 22 ,并将矩形略微放大,使得没有点恰在矩形边上即可。

扩展① : 若在 kk 维空间中,连续的 kk 轮划分会剖分出 2k2^k 个部分。

一个用于划分的超平面最多穿过其中 2k12^{k-1} 个部分(可以归纳证明)。

这样,复杂度递推式就为 T(h)=2k1T(hk)+O(1)T(h)=2^{k-1}T(h-k)+O(1) ,即 T(h)=2h(h/k)T(h)=2^{h-(h/k)}

所以,超平面经过的区域个数为 O(n1(1/k))O(n^{1-(1/k)})

扩展② : 有时候,两个维度的操作个数不同,我们可以改变 KDT\rm KDT 的结构来改进复杂度。

若在相邻的两轮划分中, xx 上分成 aa 段,yy 上分成 bb 段,则有 abab 份,每一份大小为 n/abn/ab

对于垂直于 xx 轴的一条直线,其经过区域数为 T1(n)=bT1(n/ab)+O(1)T_1(n)=bT_1(n/ab)+O(1)

对于垂直于 yy 轴的一条直线,其经过区域数为 T2(n)=aT2(n/ab)+O(1)T_2(n)=aT_2(n/ab)+O(1)

将两者相乘,可得 T1(n)T2(n)=abT1(n/ab)T2(n/ab)T_1(n)T_2(n)=abT_1(n/ab)T_2(n/ab) (和 O(1)O(1) 相关的部分可不计)

这样,不难看出 T1(n)T2(n)=O(n)T_1(n)T_2(n)=O(n) ,即两个维度操作复杂度乘积为 O(n)O(n)

(注意,若 a,ba,b 较大,复杂度也会退化)

对于某个问题,若 xx 维度上有 O(n)O(n) 个修改, yy 维度上有 O(nlog2n)O(n\log^2n) 个查询。

则可以让 xx 上复杂度为 O(nlogn)O(\sqrt{n}\log n)yy 上复杂度 O(n/logn)O(\sqrt{n}/\log n) ,可得总复杂度为 O(nnlogn)O(n\sqrt{n}\log n)

具体操作举例 : 相邻的三次划分中, xx 占两次, yy 占一次,则有 :

x:T1(n)=2T(n/8)+O(1)=O(n1/3)x:T_1(n)=2T(n/8)+O(1)=O(n^{1/3})

y:T1(n)=4T(n/8)+O(1)=O(n2/3)y:T_1(n)=4T(n/8)+O(1)=O(n^{2/3})

  • 例题CF44G Shooting Gallery

    题意 : 给出一些矩形,每个矩形都有一个高度,保证高度互不相同。

    有若干次从下到上的射击,会击中并摧毁射击点上最低的那个矩形。

    求出每次射击击中的矩形,或指出未射中。

考虑使用矩形来匹配射击点。

可以把矩形按高度排序后,依次寻找范围内的编号最小的射击点,并将其删除,正确性显然。

先对射击点建立 KDT\rm KDT ,维护子树内未被删除的最小射击点。range query 之后单点修改即可。

剪枝 : 若子树内编号最小点仍然大于当前答案,则不进入。优先进入最小点编号更小的子树。(大幅优化)

由于我个人比较偏好 Leafy\rm Leafy 数据结构,下面是“线段树式”的 KDT\rm KDT 实现。

#include<algorithm>
#include<cstdio>
#define MaxN 100500
using namespace std;
struct Node
{int xl,yl,xr,yr,p;}a[MaxN<<2];
struct Point{int x,y,p;}p[MaxN];
bool cmpX(const Point &A,const Point &B)
{return A.x<B.x;}
bool cmpY(const Point &A,const Point &B)
{return A.y<B.y;}
void build(int l,int r,int u,bool kd)
{
  if (l==r){
    a[u].xl=a[u].xr=p[l].x;
    a[u].yl=a[u].yr=p[l].y;
    a[u].p=p[l].p;
    return ;
  }int mid=(l+r)>>1;
  nth_element(p+l,p+mid,p+r+1,kd ? cmpX : cmpY);
  build(l,mid,u<<1,!kd);
  build(mid+1,r,u<<1|1,!kd);
  int ls=u<<1,rs=u<<1|1;
  a[u].p=min(a[ls].p,a[rs].p);
  a[u].xl=min(a[ls].xl,a[rs].xl);
  a[u].yl=min(a[ls].yl,a[rs].yl);
  a[u].xr=max(a[ls].xr,a[rs].xr);
  a[u].yr=max(a[ls].yr,a[rs].yr);
}
int to,n;
void upd(int l=1,int r=n,int u=1)
{
  if (l==r){a[u].p=n+1;return ;}
  int mid=(l+r)>>1;
  if (to<=mid)upd(l,mid,u<<1);
  else upd(mid+1,r,u<<1|1);
  a[u].p=min(a[u<<1].p,a[u<<1|1].p);
}
Node wf;
void qry(int u=1)
{
  if (a[u].p>=wf.p||wf.xr<a[u].xl||a[u].xr<wf.xl||wf.yr<a[u].yl|a[u].yr<wf.yl)return ;
  if (wf.xl<=a[u].xl&&a[u].xr<=wf.xr&&wf.yl<=a[u].yl&&a[u].yr<=wf.yr)
    {wf.p=min(wf.p,a[u].p);return ;}
  if (a[u<<1].p<a[u<<1|1].p)
    {qry(u<<1);qry(u<<1|1);}
  else {qry(u<<1|1);qry(u<<1);}
}
struct Squ{int xl,yl,xr,yr,p,h;}s[MaxN];
bool cmpS(const Squ &A,const Squ &B)
{return A.h<B.h;}
int m,tp[MaxN],ans[MaxN];
int main()
{
  scanf("%d",&m);
  for (int i=1;i<=m;i++){
    scanf("%d%d%d%d%d",&s[i].xl,&s[i].xr,&s[i].yl,&s[i].yr,&s[i].h);
    s[i].p=i;
  }
  sort(s+1,s+m+1,cmpS);
  scanf("%d",&n);
  for (int i=1;i<=n;i++){
    scanf("%d%d",&p[i].x,&p[i].y);
    p[i].p=i;
  }
  build(1,n,1,0);
  for (int i=1;i<=n;i++)tp[p[i].p]=i;
  for (int i=1;i<=m;i++){
    wf=(Node){s[i].xl,s[i].yl,s[i].xr,s[i].yr,n+1};
    qry();
    if (wf.p==n+1)continue;
    ans[wf.p]=s[i].p;
    to=tp[wf.p];upd();
  }
  for (int i=1;i<=n;i++)
    printf("%d\n",ans[i]);
  return 0;
}

KDtree\rm KD-tree 配合标记

KDT\rm KDT 和树套树有本质的不同,其能把 kk 维空间内的一个区域映射成 O(n1(1/k))O(n^{1-(1/k)}) 个区间的并。(类似于树剖?)

所以,传统的(线段树)标记体系可以应用于 KDT\rm KDT 上。

为了减少细节,可以把 KDT\rm KDT 写成 Leafy\rm Leafy 的。

  • 例题① : BZOJ4154 Generating Synergy

    题意 : 给定一棵有根树,支持下列操作 :

    • 将距离节点 uu 不超过 ll 的子节点染成颜色 cc

    • 或询问点 uu 的颜色。

不难发现,子树的限制是 dfsdfs 序上的一个区间,而深度的限制是 depdep 上的一个前缀。

所以,问题相当于矩形覆盖,单点查询,使用 KDT\rm KDT 即可。


区间 [L,R][L,R] 与区间 [l,r][l,r] 有交集的充要条件是 [lR][Lr][l\leq R][L\leq r] ,是二维偏序。

考虑离线,将每个询问看作二位平面上的点 (l,r)(l,r) ,建立 KDT\rm KDT

f[i][j]f[i][j] 为 : 第 jj 个询问区间,从第 ii 个序列区间开始向前,能连续有交的区间个数。

每加入一个序列区间 L,RL,R ,则满足 [lR][Lr][l\leq R][L\leq r] 的询问区间 ff 值加一,其余清零。

f[?][j]f[?][j] 的(历史)最大值即为询问 jj 的答案。

KDtree\rm KD-tree 的插入

  • 例题①P4148 简单题

  • 局部重构

    一种广为流传的做法是 : 首先暴力插入,然后模仿替罪羊树,当左右子树不平衡时重构部分子树。

    大部分人把 α\alpha 设为 0.750.75 左右,并分析出此时重构的复杂度是 O(nlog2n)O(n\log^2n)

    这时不能想当然地认为查询复杂度仍然为 O(n)O(\sqrt{n})

    树的高度并非 log2n+O(1)\log_2 n+O(1) ,而是 log4/3n+O(1)\log_{4/3}n+O(1) ,为原来的两倍还多。

    h>2log2nh>2\log_2 n\Rightarrow 查询复杂度 O(2h/2)>O(2log2n)=O(n)O(2^{h/2})>O(2^{\log_2n})=O(n) ,似乎基本上没保证了。

    实际上还是有更紧的界。不妨假设整颗树都是最不平衡的情况,即每个点的大儿子大小都是总大小的 α\alpha 倍。

    KDT\rm KDT 在向下的两层产生的四个分支中,只会选择两个分支。

    四个分支和原树的大小比例如图 :

    这样,最坏情况下只能选出大小为 α2,α(1α)\alpha^2,\alpha(1-\alpha) 的两个分支。

    T(n)T(n) 为大小为 nn 的树的查询复杂度,有递推式 T(n)=T(α2n)+T(α(1α)n)+O(1)T(n)=T(\alpha^2n)+T(\alpha(1-\alpha)n)+O(1)

    α=0.75\alpha=0.75 时, T(n)=T(9n/16)+T(3n/16)+O(1)T(n)=T(9n/16)+T(3n/16)+O(1)

    T(n)=O(nc)T(n)=O(n^c) ,则有 nc=(9n/16)c+(3n/16)cn^c=(9n/16)^c+(3n/16)^c ,即 (9/16)c+(3/16)c=1(9/16)^c+(3/16)^c=1 解得 c0.68c≈0.68

    也就是说,实际上是大约 O(n0.68)O(n^{0.68}) 的复杂度,而且很难卡满。

    或者 ,我们取更小的 α=3/5\alpha=3/5 ,此时有 T(n)=T(9n/25)+T(6n/25)+O(1)T(n)=T(9n/25)+T(6n/25)+O(1) ,解得 T(n)O(n0.57)T(n)≈O(n^{0.57})

    这个复杂度仍然很难卡满,已经能够令人满意了。

    重构的时间消耗则约为 O(13/56/51nlognlog5/3n)O(\frac{1-3/5}{6/5-1}n\log n\log_{5/3}n) 大概是 242\sim 4 倍常数的样子。

    附 : 替罪羊树复杂度简略分析

    当然,有条件的话,根据实际情况调 α\alpha 当然是最好的。

下面是 α=3/5\alpha=3/5 的代码。评测记录

#include<algorithm>
#include<cstdio>
#define MaxN 200500
using namespace std;
int read(){}
struct Point{int x,y,v;}p[MaxN];
int tot;
bool cmpX(const Point &A,const Point &B)
{return A.x<B.x;}
bool cmpY(const Point &A,const Point &B)
{return A.y<B.y;}
struct Node{
  int xl,yl,xr,yr,l,r,s,c;
  void leaf(Point &P){
    xl=xr=P.x;yl=yr=P.y;
    s=P.v;c=1;
  }
}a[MaxN<<1];
int tn,rt,rub[MaxN<<1],tb;
int cre(){
  if (!tb)return ++tn;
  int u=rub[tb--];
  a[u]=(Node){0,0,0,0,0,0};
  return u;
}
void pia(int u)
{
  if (a[u].l==a[u].r){
    p[++tot]=(Point){a[u].xl,a[u].yl,a[u].s};
    rub[++tb]=u;return ;
  }pia(a[u].l);pia(a[u].r);
  rub[++tb]=u;
}
bool chk(int u)
{return a[u].c>3&&max(a[a[u].l].c,a[a[u].r].c)*5>a[u].c*3;}
inline void up(int u)
{
  int l=a[u].l,r=a[u].r;
  a[u].s=a[l].s+a[r].s;
  a[u].c=a[l].c+a[r].c;
  a[u].xl=min(a[l].xl,a[r].xl);
  a[u].yl=min(a[l].yl,a[r].yl);
  a[u].xr=max(a[l].xr,a[r].xr);
  a[u].yr=max(a[l].yr,a[r].yr);
}
void build(int l,int r,int &u,bool kd)
{
  u=cre();
  if (l==r){a[u].leaf(p[l]);return ;}
  int mid=(l+r)>>1;
  nth_element(p+l,p+mid,p+r+1,kd ? cmpX : cmpY);
  build(l,mid,a[u].l,!kd);
  build(mid+1,r,a[u].r,!kd);
  up(u);
}
Point wfp,reb;
void ins(int u,bool kd)
{
  if (a[u].l==a[u].r){
    int v0=cre(),v1=cre();
    a[v0]=a[u];a[v1].leaf(wfp);
    a[u].l=v0;a[u].r=v1;up(u);
    return ;
  }int ls=a[u].l;
  if (a[ls].xl<=wfp.x&&wfp.x<=a[ls].xr
    &&a[ls].yl<=wfp.y&&wfp.y<=a[ls].yr)
    ins(ls,!kd);
  else ins(a[u].r,!kd);
  up(u);
  if (chk(u)){reb.x=u;reb.y=kd;}
}
void insert()
{
  if (!tn){a[tn=1].leaf(wfp);return ;}
  reb.x=0;ins(1,0);
  if (reb.x){
    tot=0;pia(reb.x);
    int tmp;build(1,tot,tmp,reb.y);
  }
}
Node wf;
void qry(int u=1)
{
  if (wf.xr<a[u].xl||a[u].xr<wf.xl||wf.yr<a[u].yl|a[u].yr<wf.yl)return ;
  if (wf.xl<=a[u].xl&&a[u].xr<=wf.xr&&wf.yl<=a[u].yl&&a[u].yr<=wf.yr)
    {wf.s+=a[u].s;return ;}
  qry(a[u].l);qry(a[u].r);
}
int n,las;
int main()
{
  while(1){
    int op=read();
    if (op==1){
      wfp.x=read()^las;
      wfp.y=read()^las;
      wfp.v=read()^las;
      insert();
    }if (op==2){
      wf.xl=read()^las;wf.yl=read()^las;
      wf.xr=read()^las;wf.yr=read()^las;
      wf.s=0;qry();
      printf("%d\n",las=wf.s);
    }if (op==3)break;
  }return 0;
}
  • 根号分治

    设一个阈值 BB ,当积攒了 BB 个插入后,重构整棵树。

    重构复杂度为 O(n2logn/B)O(n^2\log n/B) 查询复杂度为 O(B+n)O(B+\sqrt{n}) ,取 B=nlognB=\sqrt{n\log n} 可得复杂度为 O(nnlogn)O(n\sqrt{n\log n})

    如果不想带 log\log,可以开两棵 KDT\rm KDT

    插入的点先暂存,积攒了 n\sqrt{n} 个插入后,和第一棵树合并。

    积攒了 BB 个插入后,和第二棵树合并。

    查询需要在大小 KDT\rm KDT 中查询,以及查看散点,复杂度为 O(n)O(\sqrt{n})

    合并的复杂度为 O(Bnlogn+n2logn/B)O(B\sqrt{n}\log n+n^2\log n/B) ,取 b=n3/4b=n^{3/4} 可得复杂度为 O(n5/4logn)O(n^{5/4}\log n)

    这样一来就得到了一个均摊 O(nn)O(n\sqrt{n}) 的算法。

常数较小,跑得比局部重构要快得多(似乎也更好写)。

O(nnlogn)O(n\sqrt{n\log n}) 评测记录

#include<algorithm>
#include<cstdio>
#define MaxN 100500
using namespace std;
int read(){}
struct Node
{int xl,yl,xr,yr,s;}a[MaxN<<2];
struct Point{int x,y,v;}p[MaxN];
bool cmpX(const Point &A,const Point &B)
{return A.x<B.x;}
bool cmpY(const Point &A,const Point &B)
{return A.y<B.y;}
void build(int l,int r,int u,bool kd)
{
  if (l==r){
    a[u].xl=a[u].xr=p[l].x;
    a[u].yl=a[u].yr=p[l].y;
    a[u].s=p[l].v;
    return ;
  }int mid=(l+r)>>1;
  nth_element(p+l,p+mid,p+r+1,kd ? cmpX : cmpY);
  build(l,mid,u<<1,!kd);
  build(mid+1,r,u<<1|1,!kd);
  int ls=u<<1,rs=u<<1|1;
  a[u].s=a[ls].s+a[rs].s;
  a[u].xl=min(a[ls].xl,a[rs].xl);
  a[u].yl=min(a[ls].yl,a[rs].yl);
  a[u].xr=max(a[ls].xr,a[rs].xr);
  a[u].yr=max(a[ls].yr,a[rs].yr);
}
Node wf;
void qry(int u=1)
{
  if (wf.xr<a[u].xl||a[u].xr<wf.xl||wf.yr<a[u].yl||a[u].yr<wf.yl)return ;
  if (wf.xl<=a[u].xl&&a[u].xr<=wf.xr&&wf.yl<=a[u].yl&&a[u].yr<=wf.yr)
    {wf.s+=a[u].s;return ;}
  qry(u<<1|1);qry(u<<1);
}
int main()
{
  int pl=0,pr=0,n=read(),las=0;
  while(1){
    int op=read();
    if (op==1){
      pr++;
      p[pr].x=read()^las;
      p[pr].y=read()^las;
      p[pr].v=read()^las;
      if ((pr-pl)*(pr-pl)>pr*40)build(1,pl=pr,1,0);
    }if (op==2){
      wf.xl=read()^las;wf.yl=read()^las;
      wf.xr=read()^las;wf.yr=read()^las;
      wf.s=0;qry();
      for (int i=pl+1;i<=pr;i++)
        if (wf.xl<=p[i].x&&p[i].x<=wf.xr&&wf.yl<=p[i].y&&p[i].y<=wf.yr)
          wf.s+=p[i].v;
      printf("%d\n",las=wf.s);
    }if (op==3)break;
  }return 0;
}

O(nn+n5/4logn)O(n\sqrt{n}+n^{5/4}\log n) 评测记录

#include<algorithm>
#include<cstdio>
#include<cmath>
#define MaxN 100500
using namespace std;
int read(){
  int X=0;char ch=0,w=0;
  while(ch<48||ch>57)ch=getchar(),w|=(ch=='-');
  while(ch>=48&&ch<=57)X=X*10+(ch^48),ch=getchar();
  return w?-X:X;
}
struct Point{int x,y,v;}p[MaxN];
bool cmpX(const Point &A,const Point &B)
{return A.x<B.x;}
bool cmpY(const Point &A,const Point &B)
{return A.y<B.y;}
struct Node{int xl,yl,xr,yr,s;};
Node wf;
struct KDT
{
  Node a[MaxN<<2];
  void build(int l,int r,int u,bool kd)
  {
    if (l==r){
      a[u].xl=a[u].xr=p[l].x;
      a[u].yl=a[u].yr=p[l].y;
      a[u].s=p[l].v;
      return ;
    }int mid=(l+r)>>1;
    nth_element(p+l,p+mid,p+r+1,kd ? cmpX : cmpY);
    build(l,mid,u<<1,!kd);
    build(mid+1,r,u<<1|1,!kd);
    int ls=u<<1,rs=u<<1|1;
    a[u].s=a[ls].s+a[rs].s;
    a[u].xl=min(a[ls].xl,a[rs].xl);
    a[u].yl=min(a[ls].yl,a[rs].yl);
    a[u].xr=max(a[ls].xr,a[rs].xr);
    a[u].yr=max(a[ls].yr,a[rs].yr);
  }
  void qry(int u=1)
  {
    if (wf.xr<a[u].xl||a[u].xr<wf.xl||wf.yr<a[u].yl||a[u].yr<wf.yl)return ;
    if (wf.xl<=a[u].xl&&a[u].xr<=wf.xr&&wf.yl<=a[u].yl&&a[u].yr<=wf.yr)
      {wf.s+=a[u].s;return ;}
    qry(u<<1|1);qry(u<<1);
  }
}T0,T1;
int main()
{
  int p0=0,p1=0,p2=0,n=read(),las=0;
  while(1){
    int op=read();
    if (op==1){
      p2++;
      p[p2].x=read()^las;
      p[p2].y=read()^las;
      p[p2].v=read()^las;
      if ((p2-p1)*(p2-p1)>p2*1.5)
        T1.build(p0+1,(p1=p2),1,0);
      else if (p1-p0>1.2*pow(p2,0.75)){
        T0.build(1,p0=p1,1,0);
        T1.build(p0+1,(p1=p2),1,0);
      }
    }if (op==2){
      wf.xl=read()^las;wf.yl=read()^las;
      wf.xr=read()^las;wf.yr=read()^las;
      wf.s=0;T0.qry();T1.qry();
      for (int i=p1+1;i<=p2;i++)
        if (wf.xl<=p[i].x&&p[i].x<=wf.xr&&wf.yl<=p[i].y&&p[i].y<=wf.yr)
          wf.s+=p[i].v;
      printf("%d\n",las=wf.s);
    }if (op==3)break;
  }return 0;
}

  • 例题②P4848 崂山白花蛇草水

  • 做法1 :

    使用 KDT\rm KDT 套权值线段树来维护,查询时多树二分。

    建树的复杂度可以用线段树合并做到 O(qlogw)O(q\log w),查询的复杂度是 O(qlogw)O(\sqrt{q}\log w) ,且常数较大。

    使用上文的根号分治方法,总时间复杂度为 O(qqlogw)O(q\sqrt{q}\log w),空间复杂度为 O(qlogw)O(q\log w)

实现较为繁琐,且不能通过本题。

  • 做法2 :

    可以改用权值线段树套 KDT\rm KDT ,这样时间复杂度仍然是 O(qqlogw)O(q\sqrt{q}\log w)

    注意到,在线段树上二分时,我们可以选择查询左儿子的 KDT\rm KDT 还是右儿子的 KDT\rm KDT。(具体做法见代码)

    我们选择较小的那一侧来查询,这样时间开销较低。

    最坏情况下,我们每次都会进入点较多的子树。

    这相当于把点集分成 O(logw)O(\log w) 个部分,然后分别查询,查询复杂度降为 O(logwq/logw)=O(qlogw)O(\log w*\sqrt{q/\log w})=O(\sqrt{q\log w})

    重构的复杂度是 O(q5/4logqlogw)O(q^{5/4}\log q\log w)

由于本题数据随机,很多代码都将复杂度向查询倾斜。实际上,这在极限数据下表现并不好。

同时,由于本题时限较紧( log3\log^310510^5 ),需利用数据特性,将重构次数减少才能通过。

#include<algorithm>
#include<cstdio>
#include<vector>
#include<cmath>
#define pb push_back
#define MaxN 100500
using namespace std;
int read(){}
struct Point{int x,y;}sp[MaxN];
bool cmpX(const Point &A,const Point &B)
{return A.x<B.x;}
bool cmpY(const Point &A,const Point &B)
{return A.y<B.y;}
struct Node{int xl,yl,xr,yr,c,l,r;}a[MaxN*80];
int tn,rub[MaxN*80],tb;
int cre(){
  if (!tb)return ++tn;
  int u=rub[tb--];
  a[u]=(Node){0,0,0,0,0,0};
  return u;
}
Node wf;
struct KDT
{
  int rt;
  void del(int u){
    if (!u)return ;
    rub[++tb]=u;
    del(a[u].l);del(a[u].r);
  }
  void build(int l,int r,int &u,bool kd)
  {
    a[u=cre()].c=r-l+1;
    if (l==r){
      a[u].xl=a[u].xr=sp[l].x;
      a[u].yl=a[u].yr=sp[l].y;
      return ;
    }int mid=(l+r)>>1;
    nth_element(sp+l,sp+mid,sp+r+1,kd ? cmpX : cmpY);
    build(l,mid,a[u].l,!kd);
    build(mid+1,r,a[u].r,!kd);
    int ls=a[u].l,rs=a[u].r;
    a[u].xl=min(a[ls].xl,a[rs].xl);
    a[u].yl=min(a[ls].yl,a[rs].yl);
    a[u].xr=max(a[ls].xr,a[rs].xr);
    a[u].yr=max(a[ls].yr,a[rs].yr);
  }
  void qry(int u)
  {
    if (wf.xr<a[u].xl||a[u].xr<wf.xl||wf.yr<a[u].yl||a[u].yr<wf.yl)return ;
    if (wf.xl<=a[u].xl&&a[u].xr<=wf.xr&&wf.yl<=a[u].yl&&a[u].yr<=wf.yr)
      {wf.c+=a[u].c;return ;}
    qry(a[u].l);qry(a[u].r);
  }
};
struct SquDS
{
  vector<Point> p;
  KDT T0,T1;int p0,p1;
  void insert(Point tp)
  {
    p.pb(tp);
    if ((p.size()-p1)*(p.size()-p1)>p.size()*48){
      p1=p.size();
      for (int i=p0;i<p1;i++)sp[i]=p[i];
      T1.del(T1.rt);T1.build(p0,p1-1,T1.rt,0);
    }else if (p1-p0>pow(p.size()*7,0.75)){
      for (int i=0;i<p.size();i++)sp[i]=p[i];
      T0.del(T0.rt);T0.build(0,(p0=p1)-1,T0.rt,0);
      T1.del(T1.rt);T1.build(p0,(p1=p.size())-1,T1.rt,0);
    }
  }
  int qry()
  {
    wf.c=0;T0.qry(T0.rt);T1.qry(T1.rt);
    for (int i=p1;i<p.size();i++)
      wf.c+=(wf.xl<=p[i].x&&p[i].x<=wf.xr&&wf.yl<=p[i].y&&p[i].y<=wf.yr);
    return wf.c;
  }
};
struct SGT_Node
{int l,r;SquDS s;}t[MaxN<<5];
int tot,to;Point wfp;
#define Lim 1000000000
void add(int l,int r,int &u)
{
  if (!u)u=++tot;
  t[u].s.insert(wfp);
  if (l==r)return ;
  int mid=(l+r)>>1;
  if (to<=mid)add(l,mid,t[u].l);
  else add(mid+1,r,t[u].r);
}
int qry(int l,int r,int u,int c,int k)
{
  if (l==r)return l;
  int mid=(l+r)>>1,ls=t[u].l,rs=t[u].r,lc,rc;
  if (t[ls].s.p.size()<t[rs].s.p.size())
    {lc=t[ls].s.qry();rc=c-lc;}
  else {rc=t[rs].s.qry();lc=c-rc;}
  if (k>lc)
    return qry(mid+1,r,rs,rc,k-lc);
  return qry(l,mid,ls,lc,k);
}
int n,q,las,SGTrt;
int main()
{
  n=read();q=read();
  for (int i=1,op;i<=q;i++){
    op=read();
    if (op==1){
      wfp.x=read()^las;wfp.y=read()^las;
      to=read()^las;add(1,Lim,SGTrt);
    }else {
      wf.xl=read()^las;wf.yl=read()^las;
      wf.xr=read()^las;wf.yr=read()^las;
      int c=t[SGTrt].s.qry(),k=read()^las;
      if (k>c){
        puts("NAIVE!ORZzyz.");
        las=0;continue;
      }printf("%d\n",las=qry(1,Lim,SGTrt,c,c-k+1));
    }
  }return 0;
}
  • 做法3 :

    若我们的线段树恰好在每个叶子有一个点(分布均匀),那么我们一次询问所遇到的 KDT\rm KDT 点数分别是 q,q/2,q/4...q,q/2,q/4...

    此时查询复杂度 i=0O(q/2i)=O(q)\sum\limits_{i=0}O(\sqrt{q/2^i})=O(\sqrt{q}),插入亦然。

    当然,实际数据可能往邻近值域塞入较多点,这样我们每次都会遇到很大棵的 KDT\rm KDT

    外层可以模仿替罪羊树的重构,这样保证了深度加深时划分的较为平均,但是需要支付 O(qlog3q)O(q\log^3q) 的重构代价,以及带来巨大常数。(所以本做法并不实用)

.

  • 做法4 :

    考虑不定叉的权值线段树,给定常数 kk ,第零层分 212^1 叉,第一层分 2k12^{k-1} 叉,第二层分 2k2k2^{k^2-k} 叉……

    以此类推,第 tt 层分 2ktkt12^{k^t-k^{t-1}} 叉。

    若共有 nn 个叶子,对于第 mm 层的点 uu :

    • 兄弟数量为 $2^{\sum\limits_{i=0}^{m-1}k^i-k^{i-1}}=O(2^{k^{m-1}})$

    • 2kmkm12^{k^{m}-k^{m-1}} 个分支

    • 一个分支的大小为 n/2kmn/2^{k^m} 级别。

    这样,在第 mm 层消耗的查询的复杂度为 $O\Big(2^{k^{m}-k^{m-1}}*\sqrt{n/2^{k^m}}\Big)=O(\sqrt{n}*2^{k^{m-1}(k/2-1)})$。

    总复杂度也就是 $O\Big(\sqrt{n}*\sum\limits_{i=0}^{2^{k^m}\leq n}2^{k^{i-1}(k/2-1)}\Big)$

    若取 k=2k=2 ,则有 k/21=0k/2-1=0 ,后半部分是 i=022mn20=O(loglogn)\sum\limits_{i=0}^{2^{2^m}\leq n}2^0=O(\log\log n)

    这颗树一共有 O(loglogn)O(\log\log n) 层,所以查询复杂度为 O(nloglogn)O(\sqrt{n}\log\log n) ,重构复杂度为 O(nlognloglogn)O(n\log n\log\log n)

    使用根号分治,以 BB 为阈值,查询的复杂度为 O(qloglogn+B)O(\sqrt{q}\log\log n+B) ,重构的复杂度为 O(q2logqloglogq/B)O(q^2\log q\log\log q/B)

    B=qlogqloglogqB=\sqrt{q\log q\log\log q} 可以做到 O(qqlogqloglogq)O(q\sqrt{q\log q\log\log q}) 的复杂度。空间低至 O(qloglogq)O(q\log\log q)

    本做法只需要写静态 KDT\rm KDT ,而且瓶颈在散点处理,常数较小。

    若取 1<k<21<k<2 ,可能可以获得更好的理论复杂度。

#include<algorithm>
#include<cstdio>
#include<vector>
#include<cmath>
#define pb push_back
#define MaxN 100500
using namespace std;
int read(){}
struct Point{int x,y,v;}p[MaxN],sp[MaxN];
bool cmpX(const Point &A,const Point &B){return A.x<B.x;}
bool cmpY(const Point &A,const Point &B){return A.y<B.y;}
bool cmpV(const Point &A,const Point &B){return A.v<B.v;}
struct Node{int xl,yl,xr,yr,c;}a[MaxN*4*4];
Node *tn,wf;
struct KDT
{
  Node *a;
  void build(int l,int r,int u,bool kd)
  {
    a[u].c=r-l+1;
    if (l==r){
      a[u].xl=a[u].xr=sp[l].x;
      a[u].yl=a[u].yr=sp[l].y;
      return ;
    }int mid=(l+r)>>1;
    nth_element(sp+l,sp+mid,sp+r+1,kd ? cmpX : cmpY);
    build(l,mid,u<<1,!kd);
    build(mid+1,r,u<<1|1,!kd);
    int ls=u<<1,rs=u<<1|1;
    a[u].xl=min(a[ls].xl,a[rs].xl);
    a[u].yl=min(a[ls].yl,a[rs].yl);
    a[u].xr=max(a[ls].xr,a[rs].xr);
    a[u].yr=max(a[ls].yr,a[rs].yr);
  }
  int qry(int u=1)
  {
    if (wf.xr<a[u].xl||a[u].xr<wf.xl||wf.yr<a[u].yl||a[u].yr<wf.yl)return 0;
    if (wf.xl<=a[u].xl&&a[u].xr<=wf.xr&&wf.yl<=a[u].yl&&a[u].yr<=wf.yr)
      return a[u].c;
    return qry(u<<1)+qry(u<<1|1);
  }
};
struct SGT_Node{int mid[12];KDT s;}t[205];
const int d[6]={0,2,4,10,1251};
void build(int l,int r,int u,int dep)
{
  if (l>r)return ;
  int k=d[dep];
  t[u].s.a=tn;tn+=(r-l+1)<<2;
  for (int i=l;i<=r;i++)sp[i]=p[i];
  t[u].s.build(l,r,1,0);
  if (k>r-l+1)return ;
  for (int i=0;i<=k;i++)
    t[u].mid[i]=l+(r-l+1)*i/k;
  for (int i=0;i<k;i++)
    build(t[u].mid[i],t[u].mid[i+1]-1,u*k+i,dep+1);
}
inline bool in(const Point &p,const Node &r)
{return r.xl<=p.x&&p.x<=r.xr&&r.yl<=p.y&&p.y<=r.yr;}
int tp,p0,p1,wfk,ret;
int ltg(int lim){
  int ret=0;
  while(p[tp].v<=lim&&tp<=p1)
    ret+=in(p[tp++],wf);
  return ret;
}
int ltk()
{
  while(tp<=p1){
    int c=in(p[tp],wf);
    if (wfk>c)wfk-=c;
    else return p[tp].v;
    tp++;
  }return -1;
}
int qry(int l,int r,int u,int dep)
{
  if (l>r)return -1;
  int k=d[dep];
  if (k>r-l+1){
    ret=l;
    for (int i=l;i<=r;i++){
      int stp=tp,pc=ltg(p[i].v),tc=in(p[i],wf)+pc;
      if (wfk>tc)wfk-=tc;
      else {
        if (wfk>pc)return p[i].v;
        else {tp=stp;break;}
      }
    }return -1;
  }
  for (int i=0;i<k;i++){
    int stp=tp,
        tc=t[u*k+i].s.qry()+ltg(p[t[u].mid[i+1]-1].v);
    if (wfk>tc)wfk-=tc;
    else {
      tp=stp;
      return qry(t[u].mid[i],t[u].mid[i+1]-1,u*k+i,dep+1);
    }
  }return -1;
}
int n,q,las;
int main()
{
  n=read();q=read();
  for (int i=1;i<=200;i++)t[i].s.a=a;
  for (int i=1,op;i<=q;i++){
    op=read();
    if (op==1){
      Point sav;
      sav.x=read()^las;
      sav.y=read()^las;
      sav.v=1000000000-(read()^las);
      bool fl=0;
      for (int i=p1;i>p0;i--)
        if (sav.v<=p[i].v)p[i+1]=p[i];
        else {p[i+1]=sav;fl=1;break;}
      if (!fl)p[p0+1]=sav;
      p1++;
      if ((p1-p0)*(p1-p0)>=p1*120){
        sort(p+1,p+(p0=p1)+1,cmpV);
        tn=a+1;build(1,p1,1,1);
      }
    }else {
      wf.xl=read()^las;wf.yl=read()^las;
      wf.xr=read()^las;wf.yr=read()^las;
      wfk=read()^las;
      tp=p0+1;
      las=qry(1,p0,1,1);
      if (las==-1)las=ltk();
      if (las==-1){
        puts("NAIVE!ORZzyz.");
        las=0;continue;
      }printf("%d\n",las=(1000000000-las));
    }
  }return 0;
}

KDT\rm KDT 乱搞

人,要有梦想(?)

这部分的代码实现先咕了。

  • 邻近点查询 : HDU2966 In case of failure

    题意 :给平面图上若干点(无重点),对每个点,找到欧几里德距离最近点。

一个非常直觉的做法 : 在树上暴力搜索,若已经得到答案 dd ,以询问点为圆心, dd 为半径画一个圆,与该圆无交的区域不去搜索。

如果把点安排在圆周上并微扰,询问圆心的复杂度可以卡到 O(n)O(n) 级别。

但是,该算法在随机数据下表现良好。

在本题中,由于询问点同时也是贡献点,难以构造出 Hack\rm Hack 数据。

  • P4475 巧克力王国

    题意 : 给出若干个点 (x,y)(x,y) ,带有点权。

    每次给出 a,b,ca,b,c ,询问满足 ax+bycax+by\leq c 的点的点权和。

不难发现等价于半平面数点,正经的做法可见 EI\rm EI 的讨论

数据随机的情况下,当然是 KDT\rm KDT 大法好。

直觉做法 : 若半平面包含当前矩形,直接返回,否则继续递归。

数据随机的情况下复杂度可以估计为 O(n1/2+eps)O(n^{1/2+eps})O(nlog23)O(n3/4)O(n^{\log_2\sqrt{3}})≈O(n^{3/4})

记下一个区域内最大的半径,如果区域内不可能有圆和当前圆有交,则不搜索。

这样可以直接过掉 luogu\rm luogu 的数据,Loj\rm Loj 上随机旋转后也可通过。

KDT\rm KDT 分治

类似线段树分治。

  • 移动迷宫

    题意 : 给出一张无向图,每条边有两个权值 a,ba,b,支持如下操作。

    • 修改某条边的权值。

    • 给出常数 kk ,查询从某个点出发,经过权值满足 k[a,b]k\in[a,b] 的边,能到达的点数。

    允许离线。

我们把询问按照 ((时间,k),k) 看作平面上的点。

一条边有权值 a,ba,b ,以及存在区间 l,rl,r ,能够贡献的询问是一个矩形。

对询问建立 KDT\rm KDT ,将边挂上去,然后模仿线段树分治即可。

时间复杂度为 O(nnlogn)O(n\sqrt{n}\log n) 空间为 O(nn)O(n\sqrt{n})

空间复杂度可以做到更低,我们不使用提前 O(n)O(\sqrt{n}) 挂一个操作的方式,而把所有操作一起移动(这才是真正的分治)

每次到达一个点时,将当前节点上滞留的操作 :

  • 若完全包含当前区域,则执行该操作。

  • 否则,分到下面相应的孩子中。

最后,将该节点上滞留标记占用的空间回收。(需要手写链表)

考虑我们在 KDT\rm KDTdfsdfs 的过程,若处理到点 uu ,只有 uu 到根路径的侧链共 O(logn)O(\log n) 个节点可能挂着操作。

这样,一个操作最多同时存在于 O(logn)O(\log n) 个节点中,空间复杂度也就是 O(nlogn)O(n\log n) 了。