CDQ分治【分治(真得头疼)

前言:
说实话我对于这种没有固定板子,变化多端的算法实在是非常头疼的
但是不学不行,这也是一种很重要的思伟方式
所以趁着这几天的心情比较好(快要放大周),赶紧学一波~

鸣谢:tham,stdcall

CDQ分治,传说中是一个♀神犇创造的算法
在了解这种算法之前,我们有必要了解一下一种基本的思想:分治

知识储备:分治

  • 分治介绍
    分而治之,将原问题不断划分成若干个子问题,直到子问题规模小到足以直接解决
    子问题间互相独立且原问题形式相同,递归求解这些子问题,然后将各子问题的解合并得到原问题的解
  • 一般步骤
    • 划分 Divide
      将原问题划分成若干子问题,子问题间互相独立且与原问题形式相同
    • 解决 Conquer
      递归解决子问题(递归是彰显分治优势的工具,仅仅进行一次分治策略也许看不出优势,但递归划分到子问题规模足够小,子问题的解可用常数时间解决)
    • 合并 Merge
      将各子问题的解合并得到原问题的解
  • 时间复杂度
    • 直观估计
      • 分治由以上三部分构成,整体时间复杂度则由这三部分的时间复杂度之和构成
      • 由于递归,最终的子问题变得极为简单,以至于其时间复杂度在整个分治策略上的比重微乎其微

分治的一个经典例子就是归并求逆序对
简单叙述一下算法:
利用归并把序列对半分,在一般的归并过程中,我们在前后两部分各设两个指针,按照大小顺序合并成一个序列
我们要做的就是在这个过程中记录逆序对个数
什么情况下会有逆序对呢?
无非是在前面的数比在后面的数大(翻译过来:在前半部分的数比在后半部分的数大)
设前半部分的指针为t1,后半部分的指针为t2
如果a[t1] > a[t2],那么t1~mid的元素一定都比t2大,一定都可以与t2形成逆序对

ans+=(mid-t1+1)+1

CDQ分治【分治(真得头疼)_第1张图片

#include
#include
#include

using namespace std;

const int N=100010;
int a[N],b[N],n,ans=0; 

void merge(int l,int r)
{
    if (l==r) return;
    int mid=(l+r)>>1;
    merge(l,mid);
    merge(mid+1,r);
    int t1=l; 
    int t2=mid+1;
    for (int i=l;i<=r;i++)
    {
        if ((t1<=mid&&a[t1]<=a[t2])||t2>r) {
            b[i]=a[t1];
            t1++;
        }
        else
        {
            b[i]=a[t2]; 
            ans+=(mid-t1+1);
            t2++;
        } 
    }
    for (int i=l;i<=r;i++) a[i]=b[i];
}

int main()
{
    scanf("%d",&n);
    for (int i=1;i<=n;i++) scanf("%d",&a[i]);
    merge(1,n);
    printf("%d",ans);
    return 0;
} 

我为什么要介绍这个呢?
因为归并就是一个最简单的分治问题
我们在合并两个子区间的时候,要考虑到左边区间的对右边区间的影响
即,我们每次从右边区间的有序序列中取出一个元素的时候,要把“以这个元素结尾的逆序对的个数”(左边区间有多少个元素比他大)
这是一个典型的CDQ分治的过程

CDQ分治

CDQ分治是我们处理各类问题的重要武器
它的优势在于可以顶替复杂的高级数据结构,而且常数比较小
缺点在于必须离线操作

二维偏序问题

上面介绍了归并求逆序对的经典问题,我们由此引入二维偏序问题:
给定N个有序对(a,b),求对于每个(a,b),满足a0 < a且b0 < b的有序对(a0,b0)有多少个

在归并求逆序对的时候,实际上每个元素是用一个有序对(a,b)表示的,
其中a表示数组中的位置,b表示该位置对应的值
我们求的就是“对于每个有序对(a,b),有多少个有序对(a0,b0)满足a0 < a且b0 > b”,这就是一个二维偏序问题

注意到在求逆序对的问题中,a元素是默认有序的,即我们拿到元素的时候,数组中的元素是默认从第一个到最后一个按顺序排列的,所以我们才能在合并子问题的时候忽略a元素带来的影响
因为我们在合并两个子问题的过程中,左边区间的元素一定出现在右边区间的元素之前,即左边区间的元素的a都小于右边区间元素的a

那么对于二维偏序问题,我们在拿到所有有序对(a,b)的时候,先把a元素从小到大排序
这时候问题就变成了“求顺序对”,因为a元素已经有序,可以忽略a元素带来的影响,和“求逆序对”的问题是一样的。

考虑二维偏序问题的另一种解法,用树状数组代替CDQ分治,即常用的用树状数组求顺序对
在按照a元素排序之后,我们对于整个序列从左到右扫描,每次扫描到一个有序对,求出“扫描过的有序对中,有多少个有序对的b值小于当前b值”
然而当b的值非常大的时候,空间和时间上就会吃不消,便可以用CDQ分治代替,就是我们所说的“顶替复杂的高级数据结构”

二维偏序问题的拓展

给定一个N个元素的序列a,初始值全部为0,对这个序列进行以下两种操作
操作1:格式1 x k,把位置x的元素加上k
操作2:格式为2 x y,求出区间[x,y]内所有元素的和

这是一个经典的树状数组问题
但是我们就是要没事找事,我们用CDQ分治解决它——带修改和询问的问题

我们把ta转化成一个二维偏序问题,每个操作用一个有序对(a,b)表示,其中a表示操作的时间,b表示操作的位置,时间是默认有序的,所以我们在合并子问题的过程中,就按照b从小到大的顺序合并。

首先我们把原数列和1操作都看作是修改操作
询问操作[l,r]我们拆成两个:l-1,r
因为我们询问的是一个区间和,一般的思路就是前缀和相减(我们需要具备这样的思维)
实际上我们这道题也可以这样,我们按照时间顺序进行修改
记录前缀和,当遇到l-1的标记时,我们减去sum(l-1)
遇到r标记时,询问的处理就完成了

具体流程:

  • 按照id(插入位置)归并排序
  • 进行左区间的修改
  • 统计右区间的询问

需要注意的是:

  • 在合并的时候,我们只处理左区间的修改,只统计右区间的查询
    因为左区间的修改一定可以影响右区间的查询
    这就体现出了CDQ分治的基本思想了
  • 我们把所有操作都记录到了一个数组中,所以数组的大小至少要开到500000*3
#include
#include
#include
#define ll long long

using namespace std;

const int N=5000010;
int n,m,totx=0,tot=0;     //totx是操作的个数,tot询问的编号 

struct node{
    int type,id;
    ll val;
    bool operator < (const node &a) const   //重载运算符,优先时间排序 
    {
        if (id!=a.id) return idelse return typevoid CDQ(int L,int R)
{
    if (L==R) return;
    int M=(L+R)>>1;
    CDQ(L,M);
    CDQ(M+1,R);
    int t1=L,t2=M+1;
    ll sum=0; 
    for (int i=L;i<=R;i++)
    {
        if ((t1<=M&&A[t1]R) //只修改左边区间内的修改值
        {
            if (A[t1].type==1) sum+=A[t1].val;   //sum是修改的总值
            B[i]=A[t1++]; 
        }
        else                         //只统计右边区间内的查询结果
        {
            if (A[t2].type==3) ans[A[t2].val]+=sum;
            else if (A[t2].type==2) ans[A[t2].val]-=sum;
            B[i]=A[t2++];
        }
    }
    for (int i=L;i<=R;i++) A[i]=B[i];
}

int main()
{
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;i++)
    {
        tot++;
        A[tot].type=1; A[tot].id=i;            //修改操作 
        scanf("%lld",&A[tot].val);
    }
    for (int i=1;i<=m;i++)
    {
        int t;
        scanf("%d",&t);
        tot++;
        A[tot].type=t; 
        if (t==1)
            scanf("%d%lld",&A[tot].id,&A[tot].val);
        else
        {
            int l,r;
            scanf("%d%d",&l,&r);
            totx++; 
            A[tot].val=totx; A[tot].id=l-1;    //询问的前一个位置 
            tot++; A[tot].type=3; A[tot].val=totx; A[tot].id=r;  //询问的后端点 
        }
    }
    CDQ(1,tot);
    for (int i=1;i<=totx;i++) printf("%lld\n",ans[i]);
    return 0;
}

三维偏序问题

给定N个有序三元组(a,b,c),求对于每个三元组(a,b,c),有多少个三元组(a0,b0,c0)满足a0 < a且b0 < b且c0 < c
不用CDQ的算法,我们就不说了(太麻烦了)
类似二维偏序问题,先按照a元素从小到大排序,这样我们就可以忽略a元素的影响
然后CDQ分治,按照b元素从小到大进行归并排序
哪c元素我们要怎么处理呢?
这时候比较好的方案就是借助权值树状数组,
每次从左边取出三元组(a,b,c),根据c值在树状数组中进行修改
从右边的序列中取出三元组(a,b,c)时,在树状数组中查询c值小于(a,b,c)的三元组的个数
注意,每次使用完树状数组要把树状数组清零

三维偏序问题的拓展

平面上有N个点,每个点的横纵坐标在[0,1e7]之间,有M个询问,每个询问为查询在指定矩形之内有多少个点,矩形用(x1,y1,x2,y2)的方式给出,其中(x1,y1)为左下角坐标,(x2,y2)为右上角坐标

把每个点的位置变成一个修改操作,用三元组(时间,横坐标,纵坐标)来表示,把每个查询变成二维前缀和的查询
这样对于只有位于询问的左下角的修改,才对询问有影响
操作的时间是默认有序的,分治过程中按照横坐标从小到大排序,用树状数组维护纵坐标的信息

你可能感兴趣的:(CDQ分治,知识储备)