浅谈 CDQ 分治
本文章同步发表在洛谷博客。
CDQ 好强,拜谢 CDQ /bx
CDQ 是我教练的学姐喵!
什么是 CDQ 分治?
CDQ 分治一般用于求解偏序问题,二维偏序问题一般可以不使用 CDQ 分治而用普通分治或树状数组轻松解决,三维偏序问题 CDQ 分治是最佳选择,而四维偏序问题就需要 CDQ 套 CDQ 了。
CDQ 分治求解三维偏序问题时类似于是把用来求解二维偏序的两种方案(归并排序和树状数组)结合在一起使用,外层是一个归并排序的分治,内层处理时再套上树状数组,就能解决三维偏序问题了。
CDQ 分治模板题
模板,三维偏序。
那我就借这道题来详解一下 CDQ 分治吧。
首先执行普通的读入,然后排序并离散化。
离散化完成之后,需保证其按照 \(a\) 升序排序。
接下来步入 CDQ 分治的核心环节。
首先递归处理左右两边。
重点来了!
这里是一个类似于求逆序对的时候所运用的双指针,用 \(p1\) 和 \(p2\) 两个指针分别代表左右两个子区间。
接下来开始判断。
如果 \(p1\) 的 \(b\) 小于等于 \(p2\) 的 \(b\),那么这个时候可不是像二维偏序那样直接开始统计答案,而是把 \(p1\) 的 \(c\) 值存进树状数组!因为到时候还需要判断最后一维 \(c\),因此不能在 \(b\) 满足条件时就草草记录。
否则的话,也就是说 \(p2\) 的 \(b\) 比 \(p1\) 的 \(b\) 要大,那么借助树状数组当前存储的东西,将 \(p2\) 所对应的答案加上当前树状数组中存储的小于等于 \(p2\) 的 \(c\) 的个数。
执行完这一块操作,CDQ 分治的精华就已经结束了。它不同于普通的二维偏序问题,在满足条件的情况下就直接开始记录,而是把剩下那一维塞进树状数组,需要的时候再取出来并统计。
然后清空一下树状数组并把原数组的这个区间以 \(b\) 为第一关键字排好序即可。
最后根据题目要求计算一下答案就结束啦。
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define pii pair<int,int>
#define fr first
#define se second
#define pb push_back
using namespace std;
const int N = 1e5+5 , K = 2e5+5;
struct node{
int x,y,z,t,ans;
}s[N],a[N],tmp[N];
int m,n,ppp,c[K],cnt[N];
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
bool cmp1(node d1,node d2){
if(d1.x!=d2.x)return d1.x<d2.x;
return (d1.y<d2.y||(d1.y==d2.y&&d1.z<d2.z));
}
bool cmp2(node d1,node d2){
return (d1.y<d2.y||(d1.y==d2.y&&d1.z<d2.z));
}
void add(int x,int k){while(x<=ppp)c[x]+=k,x+=x&-x;return;}
int ask(int x){int sum=0;while(x)sum+=c[x],x-=x&-x;return sum;}
void CDQsolve(int l,int r){
if(l==r)return;
int mid=(l+r)>>1;
CDQsolve(l,mid),CDQsolve(mid+1,r);
int p1=l,p2=mid+1;
for(int i=l;i<=r;i++)
if(p2>r||(p1<=mid&&a[p1].y<=a[p2].y))
tmp[i]=a[p1],add(a[p1].z,a[p1].t),++p1;
else tmp[i]=a[p2],tmp[i].ans+=ask(a[p2].z),++p2;
for(int i=l;i<=mid;i++)add(a[i].z,-a[i].t);
for(int i=l;i<=r;i++)a[i]=tmp[i],tmp[i]={0,0,0,0,0};return;
}
int main(){
m=read(),ppp=read();
for(int i=1;i<=m;i++)s[i].x=read(),s[i].y=read(),s[i].z=read();
sort(s+1,s+m+1,cmp1);
for(int i=1;i<=m;a[n].t++,i++)
if(s[i].x!=s[i-1].x||s[i].y!=s[i-1].y||s[i].z!=s[i-1].z)a[++n]=s[i];
CDQsolve(1,n);
for(int i=1;i<=n;i++)cnt[a[i].ans+a[i].t-1]+=a[i].t;
for(int i=0;i<m;i++)cout<<cnt[i]<<"\n";
return 0;
}
说白了,其本质就是——分治套上树状数组。
CDQ 分治优化 DP
这个东西不是最板的 CDQ 分治了,它明显是一个求最长上升子序列一样的东西,但是,显而易见的,这个规则很奇怪,所以不能直接用最简单的求最长上升子序列的方法。
这个时候咱们的 CDQ 分治就又出场啦——CDQ 分治是可以用来优化 DP 的哦!
其本质非常简单,就是在模板 CDQ 分治统计答案的时候,把它改造成一个 DP 的转移式就行了。
换句话说,就是把 s[i].ans+=ask(a[p2].z) 这句话修改为 s[p2].dp=max(s[p2].dp,ask(s[p2].d)+1),就成功实现了 CDQ 优化 DP 的一个过程。
当然,这题不是最无聊的偏序问题,所以一开始要提炼一下乱七八糟的题面,将其弄成数学式的形式,然后整个题目就特别显然易懂了。
最开始还需要离散化一下。
注意细节。千万不要像我一样树状数组上界给了个 \(0\) 自己还没发现!
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define pii pair<int,int>
#define fr first
#define se second
#define pb push_back
using namespace std;
const int N = 1e5+5;
struct node{int a,b,c,d,dp;}s[N];
int ppp,m,n,r[3*N],ans;map<int,int> mp;
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
bool cmp1(node d1,node d2){return d1.a<d2.a;}
bool cmp2(node d1,node d2){return d1.d<d2.d;}
bool cmp3(node d1,node d2){return d1.b<d2.b;}
void clr(int x){while(x<=ppp)r[x]=0,x+=x&-x;return;}
void add(int x,int k){while(x<=ppp)r[x]=max(r[x],k),x+=x&-x;return;}
int ask(int x){int sum=0;while(x)sum=max(sum,r[x]),x-=x&-x;return sum;}
void CDQsolve(int l,int r){
if(l==r)return;
sort(s+l,s+r+1,cmp1);
int mid=(l+r)>>1;CDQsolve(l,mid);
sort(s+l,s+mid+1,cmp2),sort(s+mid+1,s+r+1,cmp3);
int p1=l,p2=mid+1;
for(int i=l;i<=r;i++)
if(p2>r||(p1<=mid&&s[p1].d<=s[p2].b))add(s[p1].c,s[p1].dp),p1++;
else s[p2].dp=max(s[p2].dp,ask(s[p2].d)+1),p2++;
for(int i=l;i<=mid;i++)clr(s[i].c);
CDQsolve(mid+1,r);return;
}
int main(){
n=read();ppp=3*n;
for(int i=1;i<=n;i++)
s[i].a=read(),s[i].b=read(),s[i].c=read(),s[i].d=read();
for(int i=1;i<=n;i++)
mp[s[i].b]=1,mp[s[i].c]=1,mp[s[i].d]=1;
int now=0;for(auto &it:mp)it.se=(++now);
for(int i=1;i<=n;i++)
s[i].b=mp[s[i].b],s[i].c=mp[s[i].c],s[i].d=mp[s[i].d];
for(int i=1;i<=n;i++)s[i].dp=1;
CDQsolve(1,n);
for(int i=1;i<=n;i++)ans=max(ans,s[i].dp);
cout<<ans<<"\n";
return 0;
}
CDQ 分治求解四维偏序问题
正如我开头所提到的,CDQ 分治也可以用来求解四维偏序问题,但是需要用到 CDQ 分治套 CDQ 分治的写法。
这里就用一道例题简单说一下。同样的,也是一个 CDQ 分治优化 DP 哦。
大概的一个思想就是,首先外面有一层 CDQ 分治去处理掉四维偏序中的一个维度,然后再在里面塞一个 CDQ 分治去弄剩下的三维。
不过由于外层 CDQ 处理其中一维的时候已经标定了顺序,所以内层 CDQ 分治在塞进树状数组以及查询树状数组的时候需要判断一下,只有在指定的某一边的时候才能够进行统计,不然外面那一圈是乱的。
然后整个就结束了,注意细节。
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define pii pair<int,int>
#define fr first
#define se second
#define pb push_back
using namespace std;
const int N = 5e4+5;
struct node{int a,b,c,d,e,id,t;}p[N],h[N],s[N];
int m,n,ppp,ans,r[N],dp[N];map<int,int> mp;
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
bool operator != (const node &A , const node &B){
return (A.a!=B.a||A.b!=B.b||A.c!=B.c||A.d!=B.d);
}
bool cmp1(node d1,node d2){
if(d1.a!=d2.a)return d1.a<d2.a;
if(d1.b!=d2.b)return d1.b<d2.b;
return (d1.c<d2.c||(d1.c==d2.c&&d1.d<d2.d));
}
bool cmp2(node d1,node d2){
if(d1.b!=d2.b)return d1.b<d2.b;
return (d1.c<d2.c||(d1.c==d2.c&&d1.d<d2.d));
}
bool cmp3(node d1,node d2){
return (d1.c<d2.c||(d1.c==d2.c&&d1.d<d2.d));
}
void clr(int x){while(x<=ppp)r[x]=0,x+=x&-x;return;}
void add(int x,int k){while(x<=ppp)r[x]=max(r[x],k),x+=x&-x;return;}
int ask(int x){int sum=0;while(x)sum=max(sum,r[x]),x-=x&-x;return sum;}
void CDQ2(int l,int r){
if(l==r)return;
int mid=(l+r)>>1;CDQ2(l,mid);
stable_sort(h+l,h+mid+1,cmp3);
stable_sort(h+mid+1,h+r+1,cmp3);
int p1=l,p2=mid+1;
for(int i=l;i<=r;i++)
if((p2>r)||(p1<=mid&&h[p1].c<=h[p2].c))
{if(!h[p1].e)add(h[p1].d,dp[h[p1].id]);p1++;}
else{if(h[p2].e)dp[h[p2].id]=max(dp[h[p2].id],ask(h[p2].d)+h[p2].t);p2++;}
for(int i=l;i<=mid;i++)if(!h[i].e)clr(h[i].d);
stable_sort(h+mid+1,h+r+1,cmp2);CDQ2(mid+1,r);return;
}
void CDQ1(int l,int r){
if(l==r)return;
int mid=(l+r)>>1;CDQ1(l,mid);
for(int i=l;i<=r;i++)h[i]=s[i],h[i].e=(i>mid);
stable_sort(h+l,h+r+1,cmp2);
CDQ2(l,r);CDQ1(mid+1,r);return;
}
int main(){
cin>>m;
for(int i=1;i<=m;i++)
cin>>p[i].a>>p[i].b>>p[i].c>>p[i].d;
for(int i=1;i<=m;i++)mp[p[i].d]=1;
ppp=0;for(auto &it:mp)it.se=(++ppp);
for(int i=1;i<=m;i++)p[i].d=mp[p[i].d];
stable_sort(p+1,p+m+1,cmp1);
for(int i=1;i<=m;s[n].t++,i++)
if(p[i]!=p[i-1])s[++n]=p[i],s[n].id=n;
for(int i=1;i<=n;i++)dp[i]=s[i].t;
stable_sort(s+1,s+n+1,cmp1);CDQ1(1,n);
for(int i=1;i<=n;i++)ans=max(ans,dp[i]);
cout<<ans<<"\n";
return 0;
}
CDQ 分治练习题
这是一道 CDQ 分治的简单运用题。
可以发现这道题目只需求出数组一开始拥有的逆序对数量,之后依次计算减少的逆序对数量即可。
而一个数被删除的时间在计算逆序对时非常重要,因此逆序对就多了一个名为“删除时间”的变量,其也会影响到逆序对的计算。
然后它就成了一个比较显然的 CDQ 分治了,按照通常的处理方式计算即可,注意在分治函数内部需要执行两次 while 以精准统计,并且还需要清空树状数组。
细节有点繁琐。
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define pii pair<int,int>
#define fr first
#define se second
#define pb push_back
using namespace std;
const int N = 2e5+5;
struct node{LL x,del,sum;}a[N];
LL n,m,Pid[N],Res,e[N];
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
bool cmp1(node d1,node d2){return d1.x<d2.x;}
bool cmp2(node d1,node d2){return d1.del<d2.del;}
void add(LL x,LL k){while(x<=n+1)e[x]+=k,x+=x&-x;return;}
LL ask(LL x){LL sum=0;while(x)sum+=e[x],x-=x&-x;return sum;}
void CDQsolve(int l,int r){
if(l==r)return;int mid=(l+r)/2;
CDQsolve(l,mid),CDQsolve(mid+1,r);
int p1=l,p2=mid+1;
while(p1<=mid){
while(a[p1].x>a[p2].x&&p2<=r)add(a[p2].del,1),p2++;
a[p1].sum+=ask(m+1)-ask(a[p1].del),p1++;
}
p1=l,p2=mid+1;while(p1<=mid)
{while(a[p1].x>a[p2].x&&p2<=r)add(a[p2].del,-1),p2++;p1++;}
p1=mid,p2=r;
while(p2>mid){
while(a[p2].x<a[p1].x&&p1>=l)add(a[p1].del,1),p1--;
a[p2].sum+=ask(m+1)-ask(a[p2].del),p2--;
}
p1=mid,p2=r;while(p2>mid)
{while(a[p2].x<a[p1].x&&p1>=l)add(a[p1].del,-1),p1--;p2--;}
sort(a+l,a+r+1,cmp1);return;
}
int main(){
n=read(),m=read();
for(int i=1;i<=n;i++)a[i].x=read(),Pid[a[i].x]=i;
for(int i=1;i<=n;i++)a[i].del=m+1;
for(int i=1;i<=m;i++){int x=read();a[Pid[x]].del=i;}
for(int i=1;i<=n;i++)Res+=ask(n+1)-ask(a[i].x),add(a[i].x,1);
for(int i=0;i<=n+1;i++)e[i]=0;CDQsolve(1,n);
sort(a+1,a+n+1,cmp2);
for(int i=1;i<=m;i++){cout<<Res<<"\n";Res-=a[i].sum;}
return 0;
}
简单总结 CDQ 分治
CDQ 分治,是一个用来处理偏序问题的算法,其本质就是将归并排序的思想(即分治)与树状数组相结合,成功在 \(O(n \log ^2 n)\) 的时间复杂度下完美解决三维偏序问题。其也可以通过自己套自己的方式解决四维偏序问题,用处很广。当然,它经常用于优化 DP,最长上升子序列,或者类似于题目重载了一个比较符的形式的 DP 都可能会运用到它。
码这么多字也不容易,还麻烦你留个赞支持一下,真是太感谢啦!

浙公网安备 33010602011771号