CDQ分治(初步入门)

CDQ分治

CDQ分治,传说中是一个神犇创造的算法。

在了解这种算法之前,我们有必要了解一下一种基本的思想:分治。

分治介绍

分而治之,将原问题不断划分成若干个子问题,直到子问题规模小到足以直接解决

子问题间互相独立且原问题形式相同,递归求解这些子问题,然后将各子问题的解合并得到原问题的解

一般步骤

划分 Divide

将原问题划分成若干子问题,子问题间互相独立且与原问题形式相同

解决 Conquer

递归解决子问题(递归是彰显分治优势的工具,仅仅进行一次分治策略也许看不出优势,但递归划分到子问题规模足够小,子问题的解可用常数时间解决)

合并 Merge

将各子问题的解合并得到原问题的解

时间复杂度

直观估计

分治由以上三部分构成,整体时间复杂度则由这三部分的时间复杂度之和构成

由于递归,最终的子问题变得极为简单,以至于其时间复杂度在整个分治策略上的比重微乎其微。

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<cstdio>
#include<cstring>
#include<iostream>
#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 id<a.id;
        else return type<a.type;
    }
};
node A[N],B[N];
ll ans[N];

void 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]<A[t2])||t2>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)为右上角坐标

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

posted @ 2022-02-11 21:19  PassName  阅读(640)  评论(0编辑  收藏  举报