CDQ分治
如果现在有一些操作,有些操作会产生贡献,同时里面的情况会依次发生更改,要求我们去维护发生更改后的总贡献。
这个问题会使得我们初感很棘手,主要原因在于这是一个动态的问题,当其中一个操作发生变化后会对很多的操作产生影响,导致寻常的数据结构难以维护。
而现在引入的CDQ分治可以将一个动态问题分成几个静态问题,把操作的更改产生的贡献变得可以离线处理。
经过上述的叙述,我相信一定会使得对CDQ分治本就不多的理解雪上加霜,实际上,整个CDQ分治的过程我看来都是很抽象的,所以,下面将会引入一个例题,我们可以跟着例题进行探索。
例题:P3810 【模板】三维偏序(陌上花开)
题意:
给定几组数,每组包含了三个数\(a,b,c\),要求对于每一组数\(x\),要求找到一共有多少组数\(y\)满足\(a_x<=a_y,b_x<=b_y,c_x<=c_y\)。数据规模到达了\(5e5\)组数。
分析:
现在我们发现有三组关系,接下来我们可以考虑如何将三组关系一一解决。
首先,明显的我们可以直接以\(a\)为关键字从小到大排序,那么此时后面的数相对于前面的数一定满足第一个关系。
然后,我们可以开始进行最关键的一环:分治。我们设定一个子问题\(solve(l,r)\)表示将区间\(l,r\)之内的情况处理。显然,我们类比归并排序,将\((l,r)\)划分为\((l,mid)\),\((mid+1,r)\),先处理\(solve(l,mid)\),\(solve(mid+1,r)\),随后再处理区间\((l,mid)\)对区间\((mid+1,r)\)的影响。
我们可以接下来像归并排序一样,先将左右区间分别以\(b\)为第一关键字排序,然后我们就会发现如果我们在右区间从左到右递推,我们会发现按照单调性,我们可以解决掉第二个关系,左对右区间的影响也可以叠加。最后我们在套上一个树状数组就可以解决掉第三个关系。
CDQ分治的大致过程即是如此。下面将每个部分分别附上代码实现。
实现:
第一个关系的处理:排序
bool cmp1(node x,node y) {
if(x.a!=y.a) return x.a<y.a;
if(x.b!=y.b) return x.b<y.b;
return x.c<y.c;
}
sort(A+1,A+n+1,cmp1); // main()内
CDQ分治:
bool cmp2(node x,node y) {
if(x.b!=y.b) return x.b<y.b;
return x.c<y.c;
}//按$b$为第一关键字排序
int T[maxn];
void modify(int x,int val) {for(;x<=k;x+=x&-x) T[x]+=val;}
int query(int x) {int ans=0; for(;x;x-=x&-x) ans+=T[x]; return ans;}
// 树状数组实现
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,cmp2); sort(A+mid+1,A+r+1,cmp2);
int j=l;
for(int i=mid+1;i<=r;i++) {
while(A[i].b>=A[j].b && j<=mid) {modify(A[j].c,A[j].cnt); j++;}//单调性递推
A[i].ans+=query(A[i].c);//计算贡献
}
for(int i=l;i<j;i++) modify(A[i].c,-A[i].cnt);//消除影响
}
AC代码:
#include<algorithm>
#include<stdio.h>
#include<queue>
#define maxn 200005
using namespace std;
int n,k,n1;
struct node {
int a,b,c,cnt,ans;
} A[maxn];
bool cmp1(node x,node y) {
if(x.a!=y.a) return x.a<y.a;
if(x.b!=y.b) return x.b<y.b;
return x.c<y.c;
}
void unique() {
for(int i=1;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[++n1]=A[i];
A[n1].cnt++;
}
n=n1;
}
bool cmp2(node x,node y) {
if(x.b!=y.b) return x.b<y.b;
return x.c<y.c;
}
int T[maxn];
void modify(int x,int val) {for(;x<=k;x+=x&-x) T[x]+=val;}
int query(int x) {int ans=0; for(;x;x-=x&-x) ans+=T[x]; return ans;}
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,cmp2); sort(A+mid+1,A+r+1,cmp2);
int j=l;
for(int i=mid+1;i<=r;i++) {
while(A[i].b>=A[j].b && j<=mid) {modify(A[j].c,A[j].cnt); j++;}
A[i].ans+=query(A[i].c);
}
for(int i=l;i<j;i++) modify(A[i].c,-A[i].cnt);
}
int ans[maxn];
int main() {
freopen("P3810.in","r",stdin);
freopen("P3810.out","w",stdout);
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++) {
scanf("%d%d%d",&A[i].a,&A[i].b,&A[i].c);
}
sort(A+1,A+n+1,cmp1); unique();
cdq(1,n);
for(int i=1;i<=n;i++) {
ans[A[i].ans+A[i].cnt-1]+=A[i].cnt;
}
for(int i=0;i<n;i++) {
printf("%d\n",ans[i]);
}
return 0;
}
用途
CDQ分治是一种基于分治的思想,有时候对于一些在线问题我们会很难直接进行处理,这时我们就可以使用CDQ分治。
我们可以给操作加上一个数值,表示操作进行的时间刻,这使得我们可以判断操作进行的先后了,方便将修改问题转换为偏序问题,更有利于我们进行离线处理。
例题:P3157 [CQOI2011] 动态逆序对
对于本题的删除操作,我们可以考虑给排列中的每个数构造一组数:(位置,大小,修改时间)。
这样一来,我们会发现对于一个数,将其删去产生的影响为:位置在它前面,大小比它大,修改时间比它后的数个数,以及位置在它后面,大小比它小,修改时间比它后的数个数。
我们可以分成两次三维偏序操作,对于每次操作通过CDQ分治维护,最后将每个数产生的影响一次叠加即可。
#include<algorithm>
#include<stdio.h>
#include<queue>
#define maxn 100005
#define ll long long
using namespace std;
int n,m;
struct node {
int pos,num,tim,ans;
} A[maxn];
int p[maxn],c[maxn];
bool cmp1(node x,node y) {return x.tim>y.tim;}
bool cmp2(node x,node y) {return x.pos>y.pos;}
int T[maxn];
void modify(int x,int val) {for(;x<=n;x+=x&-x) T[x]+=val;}
int query(int x) {int ans=0; for(;x;x-=x&-x) ans+=T[x]; return ans;}
void cdq1(int l,int r) {
if(l==r) return;
int mid=(l+r)>>1;
cdq1(l,mid),cdq1(mid+1,r);
sort(A+l,A+mid+1,cmp1),sort(A+mid+1,A+r+1,cmp1);
int j=l;
for(int i=mid+1;i<=r;i++) {
while(A[j].tim>=A[i].tim && j<=mid) {modify(A[j].num,1),j++;}
A[i].ans+=query(n)-query(A[i].num);
}
for(int i=l;i<j;i++) modify(A[i].num,-1);
}
void cdq2(int l,int r) {
if(l==r) return;
int mid=(l+r)>>1;
cdq2(l,mid);
cdq2(mid+1,r);
sort(A+l,A+mid+1,cmp1),sort(A+mid+1,A+r+1,cmp1);
int j=l;
for(int i=mid+1;i<=r;i++) {
while(A[j].tim>=A[i].tim && j<=mid) modify(A[j].num,1),j++;
A[i].ans+=query(A[i].num);
}
for(int i=l;i<j;i++) modify(A[i].num,-1);
}
ll sum;
void cal() {
for(int i=1;i<=n;i++) {
sum+=query(n)-query(A[i].num);
modify(A[i].num,1);
}
for(int i=1;i<=n;i++) modify(A[i].num,-1);
}
int main() {
// freopen("P3157.in","r",stdin);
// freopen("P3157.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) {
scanf("%d",&A[i].num);
A[i].pos=i,A[i].tim=n+1;c[A[i].num]=i;
}
for(int i=1;i<=m;i++) {
scanf("%d",&p[i]);
A[c[p[i]]].tim=i;
}
cal();
cdq1(1,n);
sort(A+1,A+n+1,cmp2);
cdq2(1,n);
sort(A+1,A+n+1,cmp1);
for(int i=n;i>0;i--) {
if(A[i].tim>n) continue;
printf("%lld\n",sum);
sum-=A[i].ans;
}
return 0;
}