\(CDQ\) 分治是一种特殊的分治方法,基本思想就是前一半的结果辅助后一半答案解答。
一、归并排序
提到 \(CDQ\) 分治,就不得不提到归并排序。
作为一种 似乎只有在瑞士轮里才有用的算法,归并排序有着优秀的时间复杂度,短小精悍的代码,十分的可爱。
首先,我们将问题转换成这样(\(l,r\) 代表目前处理的区间,\(a\) 为原序列):
- 假如 \(l=r\),直接返回;
- 将序列分成前后两部分,分别递归处理;
- 维护两个指针 \(lt=l,rt=mid+1\);
- 若 \(a_{lt}<a_{rt}\),先放 \(a_{lt}\) 且 \(lt++\),否则放 \(a_{rt}\) 且 \(rt++\)。
正确性显然,考虑时间复杂度:
- 第 \(i\) 层序列长度为 \(n\times 2^{1-i}\),最多分成 \(\log_2n\) 层;
- 每层每个量只过一遍,相当于一层时间复杂度 \(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]&<<=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&<<=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;
}
分析时间复杂度:
- 仍然 \(\log n\) 层,每层处理 \(n\) 个值;
- 但是,由于有树状数组,所以时间复杂度还要多加一个 \(\log n\)。
所以总时间复杂度为 \(O(n\log^2n)\),十分的优秀(当然不排除神犇使用 \(KD-Tree\) 等其他算法的可能)。
四、时间维度
那么,假如问题如此修改呢?
- 有一个序列 \(a\),我们会对它进行 \(q\) 次操作,有两种操作:
- 修改一个值
- 询问区间和
很明显,这是一个树状数组模板题,可以用它轻松解决。
但是,我们能不能用 \(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\))
祝大家好运!