CDQ分治是干什么用的:顶一层数据结构。
基本思想:我们要解决一系列问题,这问题一般包含修改和查询操作,可以把这些问题按发生时间排成一个序列,用一个区间[L,R]表示。
1.分:对于一个[l,r]区间,我们把它分成[l,mid],[mid+1,r]两区间处理
2.合:对于一个[l,r]区间,统计[l,mid]的修改对[mid+1,r]查询的贡献
CDQ分治与普通分治的区别:普通分治的[l,mid]区间不会对[mid+1,r]区间造成影响。
算法过程大致如下:对所有操作按时间分治,递归处理区间[l,r](也就是第l次到第r次操作),求出中点mid,计算[l,mid]中的修改操作对[mid+1,r]中的查询的影响,接着继续递归[l,mid]和[mid+1,r]。
其实每个人基本都有打过CDQ分治:求逆序对。
我们来个更高级的,求顺序对
二维偏序问题
给定N个有序对(a,b),求对于每个(a,b),满足a2<a且b2<b的有序对(a2,b2)有多少个。
首先,我们把这些有序对对a[i]排序,这样就满足了第一个条件a2<a
然后,对于此时的b[i],考虑分治。
如何合并?
对于区间[l,r],考虑[l,mid]中的b[i](l<=i<=mid) 小于 [mid+1,r]中的b[j](mid+1<=j<=r)的个数(其中[l,mid]和[mid+1,r]中的b[i]<b[j]已经通过之前的合并处理掉了(即[l,(l+mid)/2]与[(l+mid)/2+1,r]的合并处理掉了))
即对于任意一个j(mid+1<=j<=r),我们要统计b[i]<b[j](l<=i<=mid)的元素对数。
记bi[]为b[i](l<=i<=mid),bj[]为b[j](mid+1<=j<=r)(即bi[]为前一半,bj[]为后一半)
如果我们的bi[]已经排好序了,那我们把每个bj[j](mid+1<=j<=r)在bi[]中二分一下不就知道了?
但是,这样复杂度多个log。
我们先考虑优化二分的那个log。如果我们的bj[]也排好序,那么维护两个指针x,y,表示此时b[i]遍历到x,b[j]遍历到y。
如果此时bj[y]<=bi[x],ans+=x-1(因为bj[y]在bi[]数组中满足bj[y]<bi[i]的元素个数就只有x-1个,统计答案),y++(准备统计下一个在bj[]数组里的数)。
如果此时bj[y]>bi[x],x++(因为bj[y]在bi[]数组中满足bj[y]<bi[i]的元素个数还可以更多)
那么排序怎么办呢?
有没有发现我们之前的排序很像归并排序!在我们之前进行y++时,把bj[y]丢到一个新的数组里,进行x++时,把bi[x]丢进去。这样这个新数组的元素就有序了。
其实上面过程就基本是归并排序求逆序对的过程嘛。
但是,有没有发现上面的算答案过程有一点问题?(上面下划线的一句话)排序并不能使a2<a,只能使a2<=a。
怎么办呢?待会再讲。
先来点难一点的。
[9018_1954]【模板】树状数组
给定一个N个元素的序列a,初始值全部为0,对这个序列进行以下两种操作:
操作1:格式为1 x k,把位置x的元素加上k(位置从1标号到N)。
操作2:格式为2 x y,求出区间[x,y]内所有元素的和。
什么嘛,这不是随便一个树状数组就搞过去了吗。
考虑用CDQ分治做。
每个操作记录4个变量:
type:操作类型(操作1:0,操作2:我们可以看成答案为[1,y]-[1,x-1],对于[1,y],记type=1,对于[1,x-1]记type=-1),至于为什么要这样记,待会就知道了
a:操作1的k(若是操作2,则a=0)
wz:改变(查询)的元素位置(操作1:x,操作2:x-1和y)
id:这个询问是对应哪个query的(若是操作1,则id=0)
考虑按开头说的算法过程做:那么如何统计计算[l,mid]中的修改操作对[mid+1,r]中的查询呢?
由于我们已经把每个询问拆成两个询问了,那么我们考虑如何计算[1,i]的答案。
我们考虑像之前的二维偏序问题一样用归并排序,那么排序什么呢?显然是位置(wz)啊,因为只有修改操作的位置在查询操作的位置之前才会对答案有影响,维护一个变量sum表示当前查询操作到目前为止的a的和。
如果此时查询操作的位置在修改操作的后面或者一样,那么这个修改操作肯定对查询操作有影响啊,把记录此时数组的前一半遍历到的位置的指针往后推,sum+=q[i].a。但是我们发现,如果这个操作是询问操作,要不要特判呢?显然不要,因为询问操作的a值为0。
否则,就意味着前半区间里的对这个查询操作有影响的操作全部结束,统计答案,这时,我们的type就派上用场了,直接用ans[q[i].id]+=type*sum就好了(也不用对修改操作特判(type==0),也不用对这个查询操作的符号判断(type=±1)),把记录此时数组的后一半遍历到的位置的指针往后推就好了。
还有一些问题:题目有给出一个原始数组啊。把它看做几个修改操作就好了。
但是,为什么这样求得的ans数组就是答案呢?
我们分析任意一个询问是如何被计算的。即如图的黄色部分的询问是由红色部分的左半边的修改+绿色部分的左半边的修改得到的(由于它是在蓝色部分的左半边,所以忽略蓝色部分)
归并的时候4个j打成i了(统计答案时那里(一个else一个while后的各两个j))
#include<iostream> #include<cstdio> using namespace std; int ans[500100]; struct xxx{ int type,wz,id,a; }q[1501000],tmp[1501000]; void cdq(int l,int r) { if(l==r)return; int mid=(l+r)/2; cdq(l,mid);cdq(mid+1,r); int i=l,j=mid+1,tot=0,sum=0; while(i<=mid&&j<=r) { if(q[i].wz<=q[j].wz){sum+=q[i].a;tmp[++tot]=q[i];i++;} else {ans[q[j].id]+=q[j].type*sum;tmp[++tot]=q[j];j++;} } while(i<=mid)tmp[++tot]=q[i++]; while(j<=r){ans[q[j].id]+=q[j].type*sum;tmp[++tot]=q[j++];} for(int i=l;i<=r;i++)q[i]=tmp[i-l+1]; } int main() { int n,m;scanf("%d%d",&n,&m);int tot=0; for(int i=1;i<=n;i++){tot++;scanf("%d",&q[tot].a);q[tot].type=0;q[tot].wz=i;q[tot].id=0;} int qtot=0; for(int i=1;i<=m;i++) { int a,b,c;scanf("%d%d%d",&a,&b,&c); tot++; if(a==1){q[tot].type=0;q[tot].wz=b;q[tot].id=0;q[tot].a=c;} if(a==2) { qtot++;q[tot].type=-1;q[tot].wz=b-1;q[tot].id=qtot;q[tot].a=0; tot++;q[tot].type=1;q[tot].wz=c;q[tot].id=qtot;q[tot].a=0; } } cdq(1,tot); for(int i=1;i<=qtot;i++)printf("%d ",ans[i]); return 0; }
那么我们回到第一个问题。显然,还是得按照a[i]排序。
如果我们把每个(a,b)拆成两个操作:添加一个数b,查询在它之前比b小的元素个数。然后对于相同的a,把所有查询的顺序放到所有添加的前面,这样,对于这个查询,是不是只会查到a比它小的询问呢?
记一个type,=0表示询问,=1表示添加。然后差不多就是上面两道题的做法结合一下就行了嘛。
简单说一下吧,就是当你区间[l,mid]与区间[mid+1,r]合并时,考虑区间[l,mid]的添加操作对[mid+1,r]的查询操作的影响,即归并排序b,在归并过程中统计答案。代码参见20171129校内训练
至此,我们CDQ分治的基本操作就都讲完了,后面的更难的部分如果这部分理解好了就也不会太难。