【DS】CDQ 分治学习笔记

都什么年代了还在做传统分治(×

0x01:引入

CDQ 分治是一类离线算法,一般用来计算形如点对 \((i,j)\) 对答案的贡献,其中 \((i,j)\) 满足某种大小限制条件(又称偏序关系)。显然这样的点对共有 \(O(n^2)\) 个,CDQ 分治则能够在(一般是) \(O(n\log^{k-1} n)\)\(k\) 为限制个数) 的时间中解决。

0x02:流程

P3810 【模板】三维偏序(陌上花开) 为例。

我们要计算点对 \((i,j)\) 的数量,满足 \(a_j \leq a_i,\ b_j \leq b_i ,\ c_j \leq c_i ,\ j \ne i\)

假设所有元素都互不相同,先以 \(a\) 为第一关键字做三关键字排序。则对于所有 \(i\),满足条件的 \(j\) 一定在其左侧。

对整个序列进行关于第二维 \(b\) 的双关键字归并排序,把所有可贡献的 \((i,j)\) 分成三类:

  1. \(i,j\in [l,mid]\)
  2. \(i,j\in [mid+1,r]\)
  3. \(j\in[l,mid],i\in[mid+1,r]\)

前两类可以在子分治归并中计算,考虑计算第三类情况:显然,子分治归并后,对 \(i\) 有贡献的 \(j\)\([l,mid]\) 的一个前缀,且随着 \(i\) 的移动单调(因为对左右两区间的 \(b\) 单调),于是可以维护双指针,对于 \(c\) 用树状数组维护并查询即可。

注意为什么上面假设所有元素互不相同,因为 CDQ 一般解决不了有重复元素的问题,除非重复元素之间不算贡献,所以要去重。

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
template <typename T>
inline void read(T& r) {
   r=0;bool w=0; 
   char ch=getchar();
   while(ch<'0'||ch>'9') w=ch=='-'?1:0,ch=getchar();
   while(ch>='0'&&ch<='9') r=r*10+(ch-'0'), ch=getchar();
   r=w?-r:r;
}

const int N=1e5+10;
#define lb x&-x

int n,k,f[N];
struct node{
   int a,b,c;
   int cnt,ans;
}a[N],b[N];
int tre[N<<1];

inline bool cmp1(const node& x,const node& y){
   return x.a==y.a?(x.b==y.b?x.c<y.c:x.b<y.b):x.a<y.a;
}
inline bool cmp2(const node& x,const node& y){
   return x.b==y.b?x.c<y.c:x.b<y.b;
}

void add(int x,int v){
   for(;x<=k;x+=lb)tre[x]+=v;
}
int query(int x){
   int res=0;
   for(;x;x-=lb)res+=tre[x];
   return res;
}

void solve(int l,int r){
   if(l==r)return;
   int mid=l+r>>1;
   solve(l,mid),solve(mid+1,r);
   sort(a+l,a+mid+1,cmp2),sort(a+mid+1,a+r+1,cmp2);//偷懒换来大常数
   int j=l-1;
   for(int i=mid+1;i<=r;++i){
       while(a[j+1].b<=a[i].b&&j+1<=mid)++j,add(a[j].c,a[j].cnt);
       a[i].ans+=query(a[i].c);
   }
   for(int i=l;i<=j;++i)add(a[i].c,-a[i].cnt);
}


int main(){
   read(n),read(k);
   for(int i=1;i<=n;++i)read(b[i].a),read(b[i].b),read(b[i].c);
   sort(b+1,b+n+1,cmp1);
   int tot=0,cnt=0;
   for(int i=1;i<=n;++i){
       ++cnt;
       if(b[i].a!=b[i+1].a||b[i].b!=b[i+1].b||b[i].c!=b[i+1].c){
           a[++tot]=b[i];
           a[tot].cnt=cnt;cnt=0;
       }
   }
   solve(1,tot);
   for(int i=1;i<=tot;++i)f[a[i].ans+a[i].cnt-1]+=a[i].cnt;
   for(int i=0;i<n;++i)printf("%d\n",f[i]);
   return 0;
}

总的来说,CDQ 分治应该是一个通过排序不断降维的过程,将限制条件变简单后加上数据结构维护。

CDQ 分治大多数用来解决三维偏序问题,三维以上很少见;二维其实就是对第一维排序转化成逆序对问题;一维?排个序不就行了。

于是一般问题的解决思路就是,转化成三维偏序问题,然后 CDQ 计算贡献。

一些注意的点:

  • 是离线!!!
  • 注意是否要离散化。
  • 上面已经讲过了,有重复元素且能相互贡献的话要去重。
  • 注意排序时要多关键字,才能保证满足条件的 \(j\) 一定在 \(i\) 左边。
  • 有待补充

0x03:例题

\(\circ\) P3755 [CQOI2017]老C的任务

题意:给你二维坐标系上的点,每个点有权值,每次询问一个矩形内的点权和。

思路:KDT?我不会,考虑怎么转化成 CDQ 分治。

显然可以二维前缀和,一个查询转化成了多个子问题:求一个点左下方矩形的权值和,这太好做了。

对于一个点 \((x_i,y_i)\),它左下方的权值和就是 \(\sum w_j[x_j\le x_i][y_j\le y_i]\)

当然,询问的点和原来的点要区分开啊,再多加一维 \(z_i\in\{0,1\}\)\(0\) 表示原来的点, \(1\) 表示查询点,则对于一个查询点 \((x_i,y_i,z_i=1)\) 它左下方的权值和为 \(\sum w_j[x_j\le x_i][y_j\le y_i][z_j<z_i]\)。这就成了三维偏序问题,\(CDQ\) 解决。

注意到 \(z_i\) 只有两种取值,故不用数据结构维护,简单归并一下就是 \(O(n\log n)\)

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;

const int N=5e5+10;

int n,m,tot=0;
struct Data{int x,y,z,w,id,op;ll sum=0;}q[N],tmp[N];
ll ans[N];

inline bool cmp1(Data a,Data b){
    return a.x==b.x?(a.y==b.y?a.z<b.z:a.y<b.y):a.x<b.x;
}

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,k=1;ll sum=0;
    while(i<=mid&&j<=r){
        if(q[i].y<=q[j].y)sum+=!q[i].z*q[i].w,tmp[k++]=q[i++];
        else ans[q[j].id]+=sum*q[j].op,tmp[k++]=q[j++];
    }
    while(i<=mid)tmp[k++]=q[i++];
    while(j<=r)ans[q[j].id]+=sum*q[j].op,tmp[k++]=q[j++];
    for(i=l,j=1;i<=r;++i,++j)q[i]=tmp[j];
}

int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i){
       int x,y,w;
        scanf("%d%d%d",&x,&y,&w);
        q[++tot]=(Data){x,y,0,w};
    }
    for(int i=1;i<=m;++i){
        int x1,x2,y1,y2;
        scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
        q[++tot]=(Data){x2,y2,1,0,i,1};
        q[++tot]=(Data){x2,y1-1,1,0,i,-1};
        q[++tot]=(Data){x1-1,y2,1,0,i,-1};
        q[++tot]=(Data){x1-1,y1-1,1,0,i,1};
    }
    sort(q+1,q+tot+1,cmp1);
    cdq(1,tot);
    for(int i=1;i<=m;++i)printf("%lld\n",ans[i]);
    return 0;
}
posted @ 2022-10-10 09:35  RuntimeErr  阅读(25)  评论(0编辑  收藏  举报