多维空间数据结构复习

多维空间数据结构复习

CDQ分治

应用

离线解决三维偏序问题,一般做法是先按第一位排序,然后分治排序第二维,排序的过程中用树状数组维护第三维。在排完第一维后,其实问题就相当于求一个二维偏序了。

CDQ分治的分治思想:合并两个子问题,同时考虑左半段内的修改对右半段内的查询产生的影响,就是用左半段的子问题帮助解决右半段的子问题。和不同分治不同的地方在于,普通分治在合并两个子问题的过程中,左半段的问题不会对右半段的问题产生影响。

主要考虑如何把问题转化维三维偏序,这里的偏序是一种二元关系

扩展到更高维的偏序问题时,可以CDQ套CDQ,但更高维的时候一般考虑使用KD-Tree

例题

1.陌上花开

题意:有 \(n\) 个元素,第 \(i\) 个元素有 \(a_i,b_i,c_i\) 三个属性,设 \(f(i)\) 表示满足 \(a_j \leq a_i\)\(b_j \leq b_i\)\(c_j \leq c_i\) 且$ j \ne i$ 的数量。

对于$ d \in [0, n)$,求 \(f(i) = d\)的数量。

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10,M=2e5+10;
int n,k;
struct Data
{
    int a,b,c,cnt,res;
    bool operator < (const Data &t) const
    {
        if(a!=t.a) return a<t.a;
        else if(b!=t.b) return b<t.b;
        else return c<t.c;
    }
    bool operator == (const Data&t) const
    {
        return a==t.a&&b==t.b&&c==t.c;
    }
};
Data q[N],w[N];
int tr[M],ans[M];
void add(int p,int x)
{
    for(;p<M;p+=p&-p)tr[p]+=x;
}
int query(int p)
{
    int res=0;
    for(;p;p-=p&-p) res+=tr[p];
    return res;
}

void CDQ(int l,int r)
{
    if(l>=r) return;
    int mid=l+r>>1;
    CDQ(l,mid),CDQ(mid+1,r);
    int i=l,j=mid+1,m=0;
    //由于分治,左区间和右区间在自身区间都排好序了,那么统计逆序对时
    //如果存在i属于[l,mid],j属于[mid+1,r],并且a[i]>a[j]
    //而a[i+1]到a[mid]都大于a[i],那么a[i+1]到a[mid]也大于a[j],而mid<j,所以这时候产生的偏序个数就是mid-i+1
    while(i<=mid &&j<=r)
    if(q[i].b<=q[j].b) add(q[i].c,q[i].cnt),w[++m]=q[i++];//w按照b排序
    else q[j].res+=query(q[j].c),w[++m]=q[j++];

    while(i<=mid) add(q[i].c,q[i].cnt),w[++m]=q[i++];
    while(j<=r) q[j].res+=query(q[j].c),w[++m]=q[j++];
    
    for(i=l;i<=mid;i++) add(q[i].c,-q[i].cnt);
    for(i=l,j=1;j<=m;i++,j++) q[i]=w[j];
        
      
}
int main()
{
     ios::sync_with_stdio(false);
     cin.tie(nullptr);
     cin>>n>>k;
     for(int i=1;i<=n;i++)
     cin>>q[i].a>>q[i].b>>q[i].c,q[i].cnt=1;
     sort(q+1,q+1+n);
     int m=2;
     for(int i=2;i<=n;i++) //离散化
        if(q[i]==q[m-1])q[m-1].cnt++;
        else q[m++]=q[i];
     CDQ(1,m-1);

     for(int i=1;i<m;i++)
        ans[q[i].res+q[i].cnt-1]+=q[i].cnt;
     for(int i=0;i<n;i++) cout<<ans[i]<<'\n';
        return 0;
}

KD-Tree

应用

KD-Tree是一种可以高效处理k维空间信息的数据结构

建树

k-D Tree 具有二叉搜索树的形态,二叉搜索树上的每个结点都对应\(k\) 维空间内的一个点。其每个子树中的点都在一个 维的超长方体内,这个超长方体内的所有点也都在这个子树中。

假设我们已经知道了\(k\) 维空间内的 \(n\)个不同的点的坐标,要将其构建成一棵 K-D Tree,步骤如下:

  • 若当前超长方体中只有一个点,返回这个点。

  • 选择一个维度,将当前超长方体按照这个维度分成两个超长方体。

  • 选择切割点:在选择的维度上选择一个点,这一维度上的值小于这个点的归入一个超长方体(左子树),其余的归入另一个超长方体(右子树)。

  • 将选择的点作为这棵子树的根节点,递归对分出的两个超长方体构建左右子树,维护子树的信息。

维度和切割点的选择方式:

  • 选择的维度要满足其内部点的分布的差异度最大,即每次选择的切割维度是方差最大的维度。
  • 每次在维度上选择切割点时选择该维度上的 中位数,这样可以保证每次分成的左右子树大小尽量相等。

插入/删除

如果维护的这个 维点集是可变的,即可能会插入或删除一些点,此时 k-D Tree 的平衡性无法保证。

我们引入一个重构常数\(\alpha\) ,对于 K-D Tree 上的一个结点 ,若其有一个子树的结点数在以 \(x\)为根的子树的结点数中的占比大于\(\alpha\) ,则认为以 为根的子树是不平衡的,需要重构。重构时,先遍历子树求出一个序列,然后用以上描述的方法建出一棵 K-D Tree,代替原来不平衡的子树。

在插入一个\(k\) 维点时,先根据记录的分割维度和分割点判断应该继续插入到左子树还是右子树,如果到达了空结点,新建一个结点代替这个空结点。成功插入结点后回溯插入的过程,维护结点的信息,如果发现当前的子树不平衡,则重构当前子树。

如果还有删除操作,则使用 惰性删除,即删除一个结点时打上删除标记,而保留其在 K-D Tree 上的位置。如果这样写,当未删除的结点数在以\(x\) 为根的子树中的占比小于\(\alpha\) 时,同样认为这个子树是不平衡的,需要重构。

例题

\(1.\)平面最近点对:给定平面上 \(n\) 个点,找出其中的一对点的距离,使得在这 \(n\) 个点的所有点对中,该距离为所有点对中最小的(不带重构)\(O(nlogn)\)

#include <bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int n,d[N],ls[N],rs[N];
double ans=2e18;
struct node{ 
    double x,y;//维数可以增加
}s[N];
double L[N],R[N],D[N],U[N];
double dist(int a,int b){return (s[a].x-s[b].x)*(s[a].x-s[b].x)+(s[a].y-s[b].y)*(s[a].y-s[b].y);}
bool cmp1(node i,node j){return i.x<j.x;}
bool cmp2(node i,node j){ return i.y<j.y;}
void maintain(int rt){
    L[rt]=R[rt]=s[rt].x;
    D[rt]=U[rt]=s[rt].y;
    if(ls[rt])
        L[rt]=min(L[rt],L[ls[rt]]),R[rt]=max(R[rt],R[ls[rt]]),
        D[rt]=min(D[rt],D[ls[rt]]),U[rt]=max(U[rt],U[ls[rt]]);
    if(rs[rt])
        L[rt]=min(L[rt],L[rs[rt]]),R[rt]=max(R[rt],R[rs[rt]]),
        D[rt]=min(D[rt],D[rs[rt]]),U[rt]=max(U[rt],U[rs[rt]]);
}
int build(int l,int r)
{
    if(l>=r)return 0;
    int mid=l+r>>1;
    double avx=0,avy=0,dvx=0,dvy=0;//平均数,方差
    for(int i=l;i<=r;i++)
        avx+=s[i].x,avy+=s[i].y;
    avx/=(double)(r-l+1),avy/=(double)(r-l+1);
    for(int i=l;i<=r;i++)
        dvx+=(s[i].x-avx)*(s[i].x-avx),dvy+=(s[i].y-avy)*(s[i].y-avy);
    //nth_element(s-start,nth,s-end,cmp)
    //把数组s[start...end]中第nth大的元素插到第nth的位置,使得它前面没有比它大的,后面没有比它小的
    //这里是来快速求中位数
    if(dvx>=dvy) d[mid]=1,nth_element(s+l,s+mid,s+r+1,cmp1);
    else d[mid]=2,nth_element(s+l,s+mid,s+r+1,cmp2);
    ls[mid]=build(l,mid-1),rs[mid]=build(mid+1,r);
    maintain(mid);
    return mid;
}
double f(int a,int b){
	double res=0;
	if(L[b]>s[a].x) res+=(L[b]-s[a].x)*(L[b]-s[a].x);
	if(R[b]<s[a].x) res+=(R[b]-s[a].x)*(R[b]-s[a].x);
	if(D[b]>s[a].y) res+=(D[b]-s[a].y)*(D[b]-s[a].y);
	if(U[b]<s[a].y) res+=(U[b]-s[a].y)*(U[b]-s[a].y);
	return res;
}
void query(int l,int r,int x){
      if(l>r) return;
      int mid=l+r>>1;
      if(mid!=x) ans=min(ans,dist(x,mid));
      if(l==r) return;
      double res1=f(x,ls[mid]),res2=f(x,rs[mid]);
      if(res1<ans&&res2<ans)
      {
      	 if(res1<res2)
      	 {
      	 	query(l,mid-1,x);
      	 	if(res2<ans)query(mid+1,r,x);
      	 }else
      	 {
      	 	query(mid+1,r,x);
      	 	if(res1<ans) query(l,mid-1,x);
      	 }
      }else
      {
      	if(res1<ans)query(l,mid-1,x);
      	if(res2<ans) query(mid+1,r,x);
      }
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++) cin>>s[i].x>>s[i].y;
    build(1,n);
    for(int i=1;i<=n;i++) query(1,n,i);
    printf("%.4lf\n",sqrt(ans));
    return 0;
}

\(2.\)Luogu P4148 简单题(需要重构)

题意:给定一个\(N\times N\)的棋盘,每个格子有一个整数,一开始全为\(0\),现在维护两种操作:

  • 1 x y A\(1\le x,y\le N\)\(A\)是正整数。将格子x y里的数字加上\(A\)
  • 2 x1 y1 x2 y2\(1\le x_1\le x_2\le N,1\le y_1\le y_2\le N\)。输出矩形\(x_1,y_1,x_2,y_2\)内的数字和
  • 3 终止程序

本题强制在线
待更
参考:OIWiKi

posted @ 2022-03-22 19:55  Arashimu  阅读(52)  评论(0编辑  收藏  举报