$CDQ$ 分治总结

\(CDQ\) 分治是一种特殊的分治方法,基本思想就是前一半的结果辅助后一半答案解答。

一、归并排序

提到 \(CDQ\) 分治,就不得不提到归并排序。
作为一种 似乎只有在瑞士轮里才有用的算法,归并排序有着优秀的时间复杂度,短小精悍的代码,十分的可爱。
首先,我们将问题转换成这样(\(l,r\) 代表目前处理的区间,\(a\) 为原序列):

  1. 假如 \(l=r\),直接返回;
  2. 将序列分成前后两部分,分别递归处理;
  3. 维护两个指针 \(lt=l,rt=mid+1\)
  4. \(a_{lt}<a_{rt}\),先放 \(a_{lt}\)\(lt++\),否则放 \(a_{rt}\)\(rt++\)

正确性显然,考虑时间复杂度:

  1. \(i\) 层序列长度为 \(n\times 2^{1-i}\),最多分成 \(\log_2n\) 层;
  2. 每层每个量只过一遍,相当于一层时间复杂度 \(O(n)\)

所以时间复杂度 \(O(n\log_2n)\)
代码有注释,可供参考:

#include<bits/stdc++.h>
using namespace std;
int n,a[100005],b[100005];
void gb_sort(int l,int r){
	if(l==r) return;//l,r相同,直接退出
	int mid=(l+r)/2,lt=l,rt=mid+1;
	gb_sort(l,mid);gb_sort(mid+1,r);//递归处理
	for(int i=l;i<=r;i++){
		if(a[lt]<a[rt]&&lt<=mid||rt>r)
			b[i]=a[lt++];else b[i]=a[rt++];
	}//排序,[l,r]的有序序列保存在b[]
	for(int i=l;i<=r;i++) a[i]=b[i];//复制过来
}signed main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	gb_sort(1,n);
	for(int i=1;i<=n;i++)
		printf("%d ",a[i]);
	return 0;
}

这是 \(CDQ\) 分治的一个基本运用。

二、二维偏序(逆序对)

我们考虑这个问题:

  • 给出长度为 \(n\) 的序列 \(a\),问有多少个数对 \((i,j)\),使得 \(i<j\)\(a_i>a_j\)\(n\le 5\times 10^5\)

很明显,此题也可以用树状数组,并且很方便(记住这个叫树状数组的物体,很快我们会再次遇到他,并且了解他的弱项)。
不过,我们可不可以通过改动归并排序代码,解决这个问题呢?
当然可以!我们发现,可以将原序列分为三个部分统计答案:前半、后半、整体。
前半和后半可以递归处理,主要考虑 通过前半部分的数据统计整体答案!!!
\(k\)\([mid+1,r]\) 中,则 \(k\) 可以和在归并排序中后于他进入的位置 \(l\) 形成逆序对,其中 \(l\)\([l,mid]\) 中。
那么我们只需做出以下修改:

#include<bits/stdc++.h>
using namespace std;
int n,a[500005],b[500005];
long long ans;
void gb_sort(int l,int r){
	if(l==r) return;
	int mid=(l+r)/2,lt=l,rt=mid+1;
	gb_sort(l,mid);gb_sort(mid+1,r);
	for(int i=l;i<=r;i++){
		if((a[lt]>a[rt]&&rt<=r)||lt>mid)
		    b[i]=a[rt++],ans+=mid+1-lt;
		else b[i]=a[lt++];
		//还有mid+1-lt个属于[l,mid]中的l没有加入
	}for(int i=l;i<=r;i++) a[i]=b[i];
}signed main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	gb_sort(1,n);printf("%lld",ans);
	return 0;
}

那么,假如将问题改一下呢?

  • 给出长度为 \(n\) 的两个序列 \(a,b\),问有多少个数对 \((i,j)\),使得 \(a_i<a_j\)\(b_i<b_j\)\(n\le 5\times 10^5\)

看起来来者不善,但假如我们按照 \(a\) 序列先行排序呢?
我们就会发现,本题需要考虑的就只剩下 \(b\) 序列了,相当于求顺序对。
代码就不写了,思路也十分简单。

三、陌上花开 可缓缓归矣——三维偏序

看如下这个问题(本题名为:陌上花开):

  • 给出长度为 \(n\) 的三个序列 \(a,b,c\),问有多少个数对 \((i,j)\),使得 \(a_i<a_j\)\(b_i<b_j\)\(c_i<c_j\)\(n\le 10^5\)

我相信大家的第一反应一定不是直接排序第一维后跑二维树状数组……
面对这种题,我们该怎么办呢?
\(CDQ\) 分治就可以很好的解决这个问题。
考虑先按照第一维排序,然后考虑第二维。
假如要把问题分成前半、后半、整体的话,前半和后半当然还是递归。
整体,主要考虑前半对后半的贡献。
发现,当前半部分的 \(b,c\) 都小于后半部分时,能供提供一个答案。
那么,我们只需要按照 \(b\) 值前后分别进行排序,用树状数组维护已经进队的前半部分 \(c\) 值,后半部分每次查询树状数组即可。
具体可以看代码:

#include<bits/stdc++.h>
#define ll long long
#define N 100005
using namespace std;
inline int read(){
    int x=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){
        if(ch=='-') f=-1;
        ch=getchar();
    }while(ch>='0'&&ch<='9')
        x=x*10+ch-'0',ch=getchar();
    return x*f;
}int n,m,k,re[N],c[2*N];struct laz{int a,b,c,sz,ans;}lyh[N],zjy[N];
//判个相等,没啥别的意思
int ch(laz x,laz y){return (x.a==y.a&&x.b==y.b&&x.c==y.c);}
//树状数组
void add(int x,int cc){for(;x<=k;x+=x&-x) c[x]+=cc;}
int sum(int x){int re=0;for(;x;x-=x&-x) re+=c[x];return re;}
//按第一维排序
int cmp(laz x,laz y){
    return x.a!=y.a?x.a<y.a:(x.b!=y.b?x.b<y.b:x.c<y.c);
}//cdq主要部分!!!
void cdq(int l,int r){
    if(l==r) return;int mid=(l+r)/2;cdq(l,mid);cdq(mid+1,r);//递归处理
    int lt=l,rt=mid+1;for(int i=1;i<=k;i++) c[i]=0;//清空树状数组
    for(int i=l;i<=r;i++){
        if((zjy[lt].b<=zjy[rt].b&&lt<=mid)||rt>r)
            add(zjy[lt].c,zjy[lt].sz),lyh[i]=zjy[lt++];//前半部分,贡献加入树状数组
        else lyh[i]=zjy[rt++],lyh[i].ans+=sum(lyh[i].c);//后半部分,查询树状数组,修改答案
    }for(int i=l;i<=r;i++) zjy[i]=lyh[i];
    //实际上,这里并不一定需要在cdq中用归并方法排序,也可以在前面直接排序
}signed main(){
    n=read();k=read();for(int i=1;i<=n;i++)
        lyh[i].a=read(),lyh[i].b=read(),lyh[i].c=read();
    sort(lyh+1,lyh+n+1,cmp);
    for(int i=1;i<=n;){
        zjy[++m]=lyh[i];
        while(ch(zjy[m],lyh[i]))
            zjy[m].sz++,i++;
    }//注意去重
    cdq(1,m);for(int i=1;i<=m;i++)
        re[zjy[i].ans+zjy[i].sz-1]+=zjy[i].sz;
    for(int i=0;i<n;i++) printf("%d\n",re[i]);
    return 0;
}

分析时间复杂度:

  1. 仍然 \(\log n\) 层,每层处理 \(n\) 个值;
  2. 但是,由于有树状数组,所以时间复杂度还要多加一个 \(\log n\)

所以总时间复杂度为 \(O(n\log^2n)\),十分的优秀(当然不排除神犇使用 \(KD-Tree\) 等其他算法的可能)。

四、时间维度

那么,假如问题如此修改呢?

  • 有一个序列 \(a\),我们会对它进行 \(q\) 次操作,有两种操作:
  1. 修改一个值
  2. 询问区间和

很明显,这是一个树状数组模板题,可以用它轻松解决。
但是,我们能不能用 \(CDQ\) 分治来解决这个问题呢?
假如我们将时间也看作一维,那么这道题就仍可以看作类似二维偏序问题,就转化为了 \(CDQ\) 分治。
再看下面这个问题(\([bzoj1176][Balkan2007]Mokia\) / \([bzoj2683]\) 简单题):

  • 维护一个 \(W\times W\) 的矩阵,每次操作可以增加某格子的权值,或询问某子矩阵的总权值。修改操作数 \(M<=160000\) ,询问数 \(Q<=10000,W<=2000000\)

同样,将时间维理解成第一维,而且已经排序好了。
详细见代码:

#include<bits/stdc++.h>
#define N 2000005
#define M 800005
#define K 200005
using namespace std;
int n,tot,cnt,c[N],ans[K];
struct yh{int x,y,v,p,id,o;}a[M],tp[M];//o为问题种类
int cmp(yh a,yh b){
    return a.x!=b.x?a.x<b.x:a.y!=b.y?a.y<b.y:a.o<b.o;
}void ad(int x,int y,int z,int w){
    tot++;a[tot]={x,y,z,0,tot,w};
    if(w) a[tot].p=cnt;
}void add(int x,int k){for(;x<=n;x+=x&-x) c[x]+=k;}
int sum(int x){int re=0;for(;x;x-=x&-x) re+=c[x];return re;}
void cdq(int l,int r){
    if(l==r) return;int mid=(l+r)/2,lt=l,rt=mid+1;
    //处理答案
    for(int i=l;i<=r;i++){
        if(a[i].id<=mid&&!a[i].o) add(a[i].y,a[i].v);
        if(a[i].id>mid&&a[i].o) ans[a[i].p]+=a[i].v*sum(a[i].y);
    }//排序
    for(int i=l;i<=r;i++)
        if(a[i].id<=mid&&!a[i].o) add(a[i].y,-a[i].v);
    for(int i=l;i<=r;i++){
        if(a[i].id<=mid) tp[lt++]=a[i];
        else tp[rt++]=a[i];
    }for(int i=l;i<=r;i++) a[i]=tp[i];
    cdq(l,mid);cdq(mid+1,r);
    //这样写似乎更清晰?
}signed main(){
    cin>>n;while(1){
        int k,b,c,d,e;cin>>k;if(k>2) break;
        if(k==1) cin>>b>>c>>d,ad(b,c,d,0);
        else{
            cin>>b>>c>>d>>e;cnt++;
            ad(d,e,1,1);ad(b-1,c-1,1,1);
            ad(d,c-1,-1,1);ad(b-1,e,-1,1);
        }
    }sort(a+1,a+tot+1,cmp);cdq(1,tot);
    for(int i=1;i<=cnt;i++) cout<<ans[i]<<"\n";
    return 0;
}

五、练习

[Violet 3]天使玩偶(最好用树状数组维护最大值)
[Tjoi2016&Heoi2016]序列(\(CDQ+dp\)
祝大家好运!

posted @ 2024-02-03 08:40  长安一片月_22  阅读(9)  评论(0编辑  收藏  举报