CDQ分治

原文链接

另外两个不错的博客点击打开链接  点击打开链接

基本思想

  CDQ分治的基本思想十分简单。如下:

  1. 我们要解决一系列问题,这些问题一般包含修改和查询操作,可以把这些问题排成一个序列,用一个区间[L,R]表示。
  2. 分。递归处理左边区间[L,M]和右边区间[M+1,R]的问题。
  3. 治。合并两个子问题,同时考虑到[L,M]内的修改对[M+1,R]内的查询产生的影响。即,用左边的子问题帮助解决右边的子问题。

  这就是CDQ分治的基本思想。和普通分治不同的地方在于,普通分治在合并两个子问题的过程中,[L,M]内的问题不会对[M+1,R]内的问题产生影响。

具体实现和用途

  二维偏序问题

  给定N个有序对(a,b),求对于每个(a,b),满足a2<a且b2<b的有序对(a2,b2)有多少个。

  我们从归并排序求逆序对来引入二维偏序问题。

  回忆一下归并排序求逆序对的过程,我们在合并两个子区间的时候,要考虑到左边区间的对右边区间的影响。即,我们每次从右边区间的有序序列中取出一个元素的时候,要把“以这个元素结尾的逆序对的个数”加上“左边区间有多少个元素比他大”。这是一个典型的CDQ分治的过程。

  现在我们把这个问题拓展到二维偏序问题。在归并排序求逆序对的过程中,每个元素可以用一个有序对(a,b)表示,其中a表示数组中的位置,b表示该位置对应的值。我们求的就是“对于每个有序对(a,b),有多少个有序对(a2,b2)满足a2<a且b2>b”,这就是一个二维偏序问题。

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

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

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

  二维偏序问题的拓展

  给定一个N个元素的序列a,初始值全部为0,对这个序列进行以下两种操作:

  操作1:格式为1 x k,把位置x的元素加上k(位置从1标号到N)。

  操作2:格式为2 x y,求出区间[x,y]内所有元素的和。

  这是一个经典的树状数组问题,可以毫无压力地秒掉,现在,我们用CDQ分治解决它——带修改和查询的问题。

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

  问题来了:如何表示修改与查询?

  具体细节请参见代码,这里对代码做一些解释,请配合代码来看。我们定义结构体Query包含3个元素:type,idx,val,其中idx表示操作的位置,type为1表示修改,val表示“加上的值”。而对于查询,我们用前缀和的思想把他分解成两个操作:sum[1,y]-sum[1,x-1],即分解成两次前缀和的查询。在合并的过程中,type为2表示遇到了一个查询的左端点x-1,需要把该查询的结果减去当前“加上的值的前缀和”,type为3表示遇到了一个查询的右端点y,需要把查询的结果加上当前“加上的值的前缀和”,val表示“是第几个查询”。这样,我们就把每个操作转换成了带有附加信息的有序对(时间,位置),然后对整个序列进行CDQ分治。

  有几点需要注意:

  1. 对于位置相同的操作,要先修改后查询。
  2. 代码中为了方便,使用左闭右开区间。
  3. 合并问题的时候统计“加上的值的前缀和”,只能统计左边区间内的修改操作,改动查询结果的时候,只能修改右边区间内的查询结果。因为只有左边区间内的修改值对右边区间内的查询结果的影响还没有统计。
  4. 代码中,给定的数组是有初始值的,可以把每个初始值变为一个修改操作。

  代码如下:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cmath>

using namespace std;
typedef long long ll;
const int MAXN = 500001; // 原数组大小
const int MAXM = 500001; // 操作数量
const int MAXQ = (MAXM<<1)+MAXN;

int n,m;

struct Query {
    int type, idx; ll val;
    bool operator<( const Query &rhs ) const { // 按照位置从小到大排序,修改优先于查询
        return idx == rhs.idx ? type < rhs.type : idx < rhs.idx;
    }
}query[MAXQ];
int qidx = 0;

ll ans[MAXQ]; int aidx = 0; // 答案数组

//整一个CDQ分治其实就类似与归并排序,只是现在将一堆"无序"(其实已经按时间有序)的数据,用归并排序对下标进行排序
//并且这里只需要考虑修改操作对查询操作的影响,所以其实就是在归并排序每一次左右区间排序时,将左区间内的修改操作的值的,都加到右区间内查询操作中就行了
//这里的话,左区间的查询操作就需要比他更左的区间的修改操作来赋值(因为那样时间顺序才是对的),右区间的修改操作其实已经在更深层次的排序中,算过他的影响了
//那么在下一层的排序中,如果我们之前排的是左区间q(相对于这一层来说),那么就只需要将之前排序的区间q内的修改操作对这一层的右区间P的所有查询操作的ans进行修改即可。
//如果我们之前排的是右区间p,那么我们只需要将左区间q内的修改操作对p的查询的ans进行修改

Query tmp[MAXQ]; // 归并用临时数组,用来记录在一次操作中各个操作按下标排序的位置
void cdq( int L, int R ) {
    if( R-L <= 1 ) return;
    int M = (L+R)>>1; cdq(L,M); cdq(M,R);  //归并左右区间修改
    ll sum = 0;
    int p = L, q = M, o = 0;
    while( p < M && q < R ) {  //p只在左区间移动[L,M),q只在右区间移动[M,R),并且一定保证左(右)区间是已经按下标排成有序的了
        if( query[p] < query[q] ) { // 只统计左边区间内的修改值
            if( query[p].type == 1 ) sum += query[p].val;
            tmp[o++] = query[p++];  //记录下标排序的结果
        }
        else { // 只修改右边区间内的查询结果,sum是左区间在下标小于q的修改操作的和,因为右区间也是严格按下标有序的,所以sum一定对右区间后面大于q的查询操作有影响,因为左区间的时间顺序都是在右区间之前的
            if( query[q].type == 2 ) ans[query[q].val] -= sum;
            else if( query[q].type == 3 ) ans[query[q].val] += sum;
            tmp[o++] = query[q++];
        }
    }
    while( p < M ) tmp[o++] = query[p++];
    while( q < R ) {
        if( query[q].type == 2 ) ans[query[q].val] -= sum;
        else if( query[q].type == 3 ) ans[query[q].val] += sum;
        tmp[o++] = query[q++];
    }
    for( int i = 0; i < o; ++i ) query[i+L] = tmp[i];  //将区间整个按小标排序的结果重新排序
}

int main() {
    scanf( "%d%d", &n, &m );
    for( int i = 1; i <= n; ++i ) { // 把初始元素变为修改操作
        query[qidx].idx = i; query[qidx].type = 1;
        scanf( "%lld", &query[qidx].val ); ++qidx;
    }
    for( int i = 0; i < m; ++i ) {
        int type; scanf( "%d", &type );
        query[qidx].type = type;
        if( type == 1 ) scanf( "%d%lld", &query[qidx].idx, &query[qidx].val );
        else { // 把查询操作分为两部分
            int l,r; scanf( "%d%d", &l, &r );
            query[qidx].idx = l-1; query[qidx].val = aidx; ++qidx;  //aidx为这个查询区间的答案在ans中的下标
            query[qidx].type = 3; query[qidx].idx = r; query[qidx].val = aidx; ++aidx;
        }
        ++qidx;
    }
    cdq(0,qidx);
    for( int i = 0; i < aidx; ++i ) printf( "%lld\n", ans[i] );
    return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值