CDQ 分治
分治,分而治之,一般采取递归的形式,先将要处理的部分分开分别处理,再合并计算。
而 CDQ 分治正是基于分治思想的离线算法。
具体地,CDQ 分治对询问进行分治,对于一个询问区间 \([l,r]\),CDQ 分治进行以下操作:
- 处理 \([l,mid]\)。
- 处理 \([mid+1,r]\)。
- 计算 \([l,mid]\) 中的修改对 \([mid+1,r]\) 的贡献。
CDQ 分治常用于解决点对相关问题。
对于偏序问题,CDQ 分治可以在较优的时间复杂度内求出,如逆序对等二维偏序。归并排序就是 CDQ 分治思想的一种体现。
接下来进入一道例题。
P3810 【模板】三维偏序(陌上花开)
有 \(n\) 个元素,第 \(i\) 个元素有 \(a_i,b_i,c_i\) 三个属性,设 \(f(i)\) 表示满足 \(a_j \le a_i\) 且 \(b_j \le b_i\) 且 \(c_j \le c_i\) 且 \(j \ne i\) 的 \(j\) 的数量。
对于 \(d \in [0, n)\),求 \(f(i)=d\) 的数量。
\(1 \le n \le 10^5\),\(1 \le a_i,b_i,c_i \le 2 \times 10^5\)。
经典题。
显然这道题是不能 \(O(n^2)\) 暴力枚举的。与点对有关,于是 CDQ 分治。
首先将点按 \(a_i\) 排序,这样就保证只有前面的点对后面的点产生影响。
分治。对于区间 \([l,r]\),假设我们已经处理好了 \([l,mid]\) 和 \([mid+1,r]\),先对这两个区间分别按 \(b_i\) 排序,由于我们在最开始就对整个序列按 \(a_i\) 排序,\([l,mid]\) 中的每个点的 \(a_i\) 一定都是大于 \([mid+1,r]\) 中的每个点的 \(a_i\) 的,所以我们只需要处理 \([l,mid]\) 中 \(b_i\) 和 \(c_i\) 的影响。
将两个区间中的点按 \(b_i\) 从小到大排序(也可用归并实现,文章中使用 sort
),然后双指针,\(i\) 代表 \([mid+1,r]\) 从 \(mid+1\) 开始的指针,\(j\) 代表 \([l,mid]\) 从 \(l\) 开始的指针。对于 \(c_i\) 我们可以使用树状数组维护。
对于一个 \(i\),我们将所有 \(b_j \le b_i\) 的点加入树状数组,此时所有加入了树状数组的点 \(x\) 都满足 \(a_x \le a_i,b_x \le b_i\),因此在树状数组上查询 \(c_x \le c_i\) 的点,即 query(c[i])
即可。
注意清空树状数组。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5,M=2e5+5;
struct node{int a,b,c;int ans,cnt;
}a[N],A[N];
int n,m,len;
int ans[N];
namespace BIT{int tree[M];void add(int x,int k){for(;x<=m;x+=x&-x)tree[x]+=k;}int query(int x){int res=0;for(;x;x-=x&-x)res+=tree[x];return res;}
}
using BIT::add;
using BIT::query;
bool cmpa(node a,node b){if(a.a!=b.a)return a.a<b.a;if(a.b!=b.b)return a.b<b.b;return a.c<b.c;
}
bool cmpb(node a,node b){if(a.b!=b.b)return a.b<b.b;return a.c<b.c;
}
void cdq(int l,int r){if(l>=r)return;int mid=l+r>>1;cdq(l,mid);cdq(mid+1,r);sort(a+l,a+mid+1,cmpb);//按 b 排序sort(a+mid+1,a+r+1,cmpb);int i,j;for(i=mid+1,j=l;i<=r;i++){while(j<=mid&&a[j].b<=a[i].b){add(a[j].c,a[j].cnt);//树状数组维护 cj++;}a[i].ans+=query(a[i].c);}for(i=l;i<j;i++)add(a[i].c,-a[i].cnt);
}
int main(){cin>>n>>m;for(int i=1;i<=n;i++)cin>>A[i].a>>A[i].b>>A[i].c;sort(A+1,A+1+n,cmpa);//按 a 排序,保证分开的区间前后 a 相对有序int tot=1;for(int i=2;i<=n;i++){//将点去重简化计算if(A[i].a!=A[i-1].a||A[i].b!=A[i-1].b||A[i].c!=A[i-1].c){a[++len]={A[i-1].a,A[i-1].b,A[i-1].c,0,tot};tot=1;}else{tot++;}}a[++len]={A[n].a,A[n].b,A[n].c,0,tot};cdq(1,len);for(int i=1;i<=len;i++)ans[a[i].ans+a[i].cnt-1]+=a[i].cnt;for(int i=0;i<n;i++)cout<<ans[i]<<'\n';return 0;
}
P3374 【模板】树状数组 1
单点加,区间查。
其实 CDQ 分治也能维护这个东西。
把对 \([l,r]\) 的查询转成对 \([1,l-1]\) 和 \([1,r]\) 的查询,对于查询 \([1,x]\),由于只有位置 \(y \le x\) ,且时间早于这个查询的修改能对这个查询造成影响,所以这就是一个二维偏序问题!
二维偏序果断想到 CDQ。我们按时间顺序保存每个操作,对于区间 \([l,mid]\) 和 \([mid+1,r]\),双指针 \(i,j\) 分别表示 \([l,mid]\) 和 \([mid+1,r]\)。由于我们先按时间顺序保存了每个操作,所以 \([l,mid]\) 与 \([mid+1,r]\) 之间的相对时间大小不变。归并操作区间,按操作位置排序,处理 \([l,mid]\) 中的操作时,如果是修改则累加贡献;处理 \([mid+1,r]\) 中的操作时,如果是查询则将贡献保存到答案数组中。然后输出答案数组就好了。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
struct node{int op,pos,val,id;//op=1修改,=2查询[1,r],=3查询[1,l-1]//pos:操作的位置,val:修改的值/第val个答案//id:时间顺序bool operator <(const node &n)const{if(pos!=n.pos)return pos<n.pos;return id<n.id;}
}q[N*3],tmp[N*3];//tmp:归并数组
int n,m,cnt;
int p,ans[N];
void cdq(int l,int r){if(l==r)return;int mid=l+r>>1;cdq(l,mid);cdq(mid+1,r);int i,j,k,sum=0;for(i=l,j=mid+1,k=l;i<=mid&&j<=r;){//累加贡献if(q[i]<q[j]){if(q[i].op==1)sum+=q[i].val;tmp[k++]=q[i++];}else{//将贡献累加到答案数组if(q[j].op==2)ans[q[j].val]-=sum;if(q[j].op==3)ans[q[j].val]+=sum;tmp[k++]=q[j++];}}for(;i<=mid;){if(q[i].op==1)sum+=q[i].val;tmp[k++]=q[i++];}for(;j<=r;){if(q[j].op==2)ans[q[j].val]-=sum;if(q[j].op==3)ans[q[j].val]+=sum;tmp[k++]=q[j++];}for(i=l;i<=r;i++)q[i]=tmp[i];
}
int main(){cin>>n>>m;for(int i=1;i<=n;i++){cnt++;q[cnt].op=1;q[cnt].pos=i;cin>>q[cnt].val;}for(int i=1;i<=m;i++){int op;cin>>op;if(op==1){cnt++;q[cnt].op=1;q[cnt].id=i;cin>>q[cnt].pos>>q[cnt].val;}else{//拆分询问p++;cnt++;q[cnt].op=2;q[cnt].val=p;q[cnt].id=i;cin>>q[cnt].pos;q[cnt].pos--;cnt++;q[cnt].op=3;q[cnt].val=p;q[cnt].id=i;cin>>q[cnt].pos;}}cdq(1,cnt);for(int i=1;i<=p;i++)cout<<ans[i]<<'\n';return 0;
}