zoukankan      html  css  js  c++  java
  • 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
    ",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分治的基本操作就都讲完了,后面的更难的部分如果这部分理解好了就也不会太难。

  • 相关阅读:
    帧同步与状态同步
    撸个服务端出来系列(五)
    撸个服务端出来系列(四)
    撸个服务端出来系列(三)
    撸个服务端出来系列(二)
    新博客手机适配的问题
    撸个服务端出来系列(一)
    高性能事件分发器,lua 版
    cocos2dx lua UI栈
    cocos2dx3.x+cocostudio多屏幕分辨率适配解决方案(干货)
  • 原文地址:https://www.cnblogs.com/lher/p/7906305.html
Copyright © 2011-2022 走看看