CDQ 分治
定义
CDQ 分治作为一种思想,主要在解决形如 \([l,r]\) 区间中找符合要求的点对 \((i,j)\) 的问题时应用。具体的思想是:
- 先递归解决 \([l,mid]\) 和 \([mid+1,r]\) 中的点对;
- 再计算 \(l\le i\le mid<mid+1\le j\le r\) 的点对 \((i,j)\) 数量。
例题
例 1:P3810 【模板】三维偏序(陌上花开)
题意:给定长为 \(n\) 的数组 \(a,b,c\),令 \(f(i)\) 表示 \(a_j\le a_i,b_j\le b_i,c_j\le c_i\) 的 \(j\) 的数量,对于每个 \(d\in[0,n)\),求出满足 \(f(i)=d\) 的 \(i\) 的个数。
问题实质上可以转化为求出所有的 \(f(i)\)。对于 \(f(i)\),我们一维一维处理:先按照 \(a\) 升序排序,这样我们就要找出所有满足 \(b_j\le b_i,c_j\le c_i\) 的有序数对 \((j,i)\)(\(j<i\))的个数。对于剩下两维,我们使用 CDQ 分治。假设函数 cdq(l,r)
能够处理 \([l,r]\) 之间的问题,那么,算法流程如下:
- 先调用
cdq(l,mid)
和cdq(mid+1,r)
; - 计算 \(l\le i\le mid<mid+1\le j\le r\) 的点对 \((i,j)\) 数量。
现在考虑设计算法解决后一个问题。我们可以枚举每一个 \(j\in[mid+1,r]\),计算符合条件的 \(i\)。先在 \([l,mid]\) 和 \([mid+1,r]\) 中分别按 \(b\) 排序,这样我们从小到大枚举 \(j\) 时,\(i\) 相对应的是 \([l,x]\) 的一段,且 \(x\) 随 \(j\) 增大而不降,这一点可以用双指针解决。然后我们要统计 \([l,x]\) 中 \(c_i<c_j\) 的个数,可以使用树状数组解决。
时间复杂度满足 \(T(n) = 2T(\frac{n}{2})+\mathcal{O}(n\log n)\),于是时间复杂度 \(T(n)=\mathcal{O}(n\log^2 n)\)。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define maxn 100005
#define maxk 200005
using namespace std;
int n,k,ans[maxn],cnt[maxn],num[maxn]; struct node{int a,b,c,cnt,id;}a[maxn]; bool cmp2(node aa,node bb){return aa.b<bb.b;}
bool cmp1(node aa,node bb){return aa.a==bb.a?(aa.b==bb.b?aa.c<bb.c:aa.b<bb.b):aa.a<bb.a;}
int c[maxk]; int lowbit(int x){return x&(-x);} void modify(int p,int ad){for(;p<=k;p+=lowbit(p)) c[p]+=ad;}
int query(int p){int res=0; for(;p>=1;p-=lowbit(p)) res+=c[p]; return res;}
void cdq(int l,int r){
if(l==r) return; int mid=(l+r)>>1; cdq(l,mid); cdq(mid+1,r);
sort(a+l,a+mid+1,cmp2); sort(a+mid+1,a+r+1,cmp2); int i=l,j=mid+1;
for(;j<=r;j++){while(i<=mid&&a[i].b<=a[j].b) modify(a[i].c,a[i].cnt),i++; ans[a[j].id]+=query(a[j].c);}
for(i--;i>=l;i--) modify(a[i].c,-a[i].cnt);
}
int main(){
scanf("%d%d",&n,&k); for(int i=1;i<=n;i++) scanf("%d%d%d",&a[i].a,&a[i].b,&a[i].c),a[i].id=i; sort(a+1,a+1+n,cmp1);
int pos=1; for(int i=2;i<=n;i++){
if(a[i].a==a[i-1].a&&a[i].b==a[i-1].b&&a[i].c==a[i-1].c) a[pos].cnt++;
else{a[pos]=(node){a[i-1].a,a[i-1].b,a[i-1].c,++a[pos].cnt,pos}; num[pos]=a[pos].cnt; pos++;}
if(i==n){a[pos]=(node){a[n].a,a[n].b,a[n].c,++a[pos].cnt,pos}; num[pos]=a[pos].cnt;}
} cdq(1,pos); for(int i=1;i<=pos;i++) cnt[ans[i]+num[i]-1]+=num[i]; for(int i=0;i<n;i++) printf("%d\n",cnt[i]); return 0;
}
例 2:P3157 [CQOI2011] 动态逆序对
题意:给定一个 \(1\sim n\) 的排列 \(p\),有 \(m\) 次操作,每次删去其中的一个数,求每次操作前 \(p\) 中的逆序对对数。
我们发现每一次操作前都要求逆序对个数不好求,于是可以把问题转化为求每一次删数减少的逆序对数,这样求前缀和就能求出操作前的逆序对数(对于不删的数,随便钦定一个删除顺序,不影响答案)。逆序对需要满足 \(i<j,p_i>p_j\) 或 \(i>j,p_i<p_j\),现在假设在统计删除 \(i\) 减少的逆序对数量,则还要满足 \(time_i<time_j\)(\(time\) 是删除时间)。此时问题转化为了两个三维偏序,直接 CDQ 分治解决即可。
#include<iostream>
#include<cstdio>
#include<algorithm>
#define maxn 100005
#define ll long long
using namespace std;
int n,m,inv[maxn],x,llen=0; ll ans[maxn]; struct node{int a,b,c;}a[maxn]; bool vis[maxn];
bool cmp1(node aa,node bb){return aa.a==bb.a?(aa.b==bb.b?aa.c<bb.c:aa.b<bb.b):aa.a<bb.a;}
bool cmp2(node aa,node bb){return aa.b<bb.b;}
int c[maxn]; int lowbit(int x){return x&(-x);} void modify(int p,int ad){for(;p<=n;p+=lowbit(p)) c[p]+=ad;}
int query(int p){int res=0; for(;p>=1;p-=lowbit(p)) res+=c[p]; return res;}
void cdq(int l,int r){
if(l==r) return; int mid=(l+r)>>1; cdq(l,mid); cdq(mid+1,r);
sort(a+l,a+mid+1,cmp2); sort(a+mid+1,a+r+1,cmp2); int i=l,j=mid+1;
for(;i<=mid;i++){while(j<=r&&a[j].b<a[i].b) modify(a[j].c,1),j++; ans[a[i].a]+=query(n)-query(a[i].c);}
for(j--;j>=mid+1;j--) modify(a[j].c,-1); i=mid; j=r;
for(;i>=l;i--){while(j>=mid+1&&a[j].b>a[i].b) modify(a[j].c,1),j--; ans[a[i].a]+=query(a[i].c);}
for(j++;j<=r;j++) modify(a[j].c,-1);
}
int main(){
scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&x),inv[x]=i;
for(int i=1;i<=m;i++){scanf("%d",&x); a[++llen]=(node){i,x,inv[x]}; vis[x]=1;}
for(int i=1;i<=n;i++) if(!vis[i]) a[++llen]=(node){llen,i,inv[i]}; sort(a+1,a+1+n,cmp1);
cdq(1,n); for(int i=n-1;i>=1;i--) ans[i]+=ans[i+1]; for(int i=1;i<=m;i++) printf("%lld\n",ans[i]); return 0;
}
例 3:CF762E Radio stations
题意:有 \(n\) 个雷达站,第 \(i\) 个由位置 \(x_i\)、覆盖半径 \(r_i\) 以及频率 \(f_i\) 描述。求满足相互覆盖且频率相差不超过 \(k\) 的点对 \((i,j)\) 的对数。
符合条件的 \((i,j)\) 需要满足 \(|x_i-x_j|\le \min(r_i,r_j)\),\(|f_i-f_j|\le k\)。发现前者的 \(\min\) 不好维护,于是把 \(\min\) 拆掉,条件变成 \(\begin{cases}r_i\le r_j\\|f_i-f_j|\le k\\|x_i-x_j|\le r_i\end{cases}\)。按照 \(r\) 排序,采用 CDQ 分治,第二个条件用双指针维护,第三个条件用树状数组维护。
#include<iostream>
#include<cstdio>
#include<algorithm>
#define maxn 100005
#define ll long long
using namespace std;
int n,k,m,uni[maxn*3]; ll ans=0; struct node{int a,b,c,l,r;}a[maxn]; bool cmp2(node aa,node bb){return aa.b<bb.b;}
bool cmp1(node aa,node bb){return aa.a==bb.a?(aa.b==bb.b?aa.c<bb.c:aa.b<bb.b):aa.a<bb.a;}
int c[maxn*3]; int lowbit(int x){return x&(-x);} void modify(int p,int ad){for(;p<=m;p+=lowbit(p)) c[p]+=ad;}
int query(int p){int res=0; for(;p>=1;p-=lowbit(p)) res+=c[p]; return res;}
void cdq(int l,int r){
if(l==r) return; int mid=(l+r)>>1; cdq(l,mid); cdq(mid+1,r);
sort(a+l,a+1+mid,cmp2); sort(a+mid+1,a+r+1,cmp2); int i=l,le=mid+1,ri=mid+1; for(;i<=mid;i++){
while(ri<=r&&a[ri].b-a[i].b<=k) modify(a[ri].c,1),ri++;
while(le<ri&&a[i].b-a[le].b>k) modify(a[le].c,-1),le++;
ans+=query(a[i].r)-query(a[i].l-1);
} for(ri--;ri>=le;ri--) modify(a[ri].c,-1);
}
int main(){
scanf("%d%d",&n,&k); for(int i=1;i<=n;i++){
scanf("%d%d%d",&a[i].c,&a[i].a,&a[i].b); uni[++m]=a[i].c;
uni[++m]=a[i].l=max(0,a[i].c-a[i].a); uni[++m]=a[i].r=a[i].c+a[i].a;
} sort(uni+1,uni+1+m); m=unique(uni+1,uni+1+m)-uni-1; for(int i=1;i<=n;i++){
a[i].c=lower_bound(uni+1,uni+1+m,a[i].c)-uni;
a[i].l=lower_bound(uni+1,uni+1+m,a[i].l)-uni; a[i].r=lower_bound(uni+1,uni+1+m,a[i].r)-uni;
} sort(a+1,a+1+n,cmp1); cdq(1,n); printf("%lld",ans); return 0;
}