CDQ分治

Posted lh

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CDQ分治相关的知识,希望对你有一定的参考价值。

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\\n",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分治的基本操作就都讲完了,后面的更难的部分如果这部分理解好了就也不会太难。

以上是关于CDQ分治的主要内容,如果未能解决你的问题,请参考以下文章

模板:CDQ分治

初窥CDQ分治

HDU 3507 Print Article(CDQ分治+分治DP)

bzoj 2683 简单题 cdq分治

bzoj4237: 稻草人 cdq分治 单调栈

cdq分治浅谈