cdq分治
cdq分治
cdq分治是一种特殊的分治方式,可以有效处理偏序问题,可以一定程度上替代一些复杂的数据结构。cdq分治的缺点在于,必须保证能够将数据离线处理。
核心思想
和分治类似,对于待处理的区间,将其分为左区间 \([l,mid]\) 和右区间 \([mid+1,r]\) 并对区间内的子问题进行分治处理。与普通分治不同的是,cdq分治允许左区间对右区间造成贡献,也就是说,每一层分治,我们把所有待求的子问题分成三个部分:只包含在左区间的,只包含在右区间的,跨越左右两个区间的。对于前两者,继续递归分治;然后对于后者,在递归回溯时进行处理,统计贡献。
三维偏序
三维偏序是最经典的cdq分治的应用。
偏序问题
偏序关系,指集合中一系列具有自反性、反对称性、传递性的关系,例如 \(\le\) 具有以下性质:
自反性:\(x \le x\)
反对称性:\(x \le y,y \le x \Rightarrow x = y\)
传递性:\(x \le y,y \le z \Rightarrow x \le z\)
所以 \(\le\) 是一种偏序关系。
而所谓n维偏序,就代表当前给出的集合中,存在n组互不相同的偏序关系。
例如,对于三维偏序的一种常见表述方式为:给定若干个三元组 \((a_{i}, b_{i}, c_{i})\),求满足\(a_{j} \le a_{i}, b_{j} \le b_{i}, c_{j} \le c_{i}\)的j的数量。
求解三维偏序问题
我们由低向高,从二维偏序问题(逆序对问题)推导三维题偏序问题。我们都知道,在树状数组求解逆序对的过程中,我们先把某一维作为关键字排序,从而消除这一维对答案的影响,然后就只需要统计另一个维度的答案就行了。
类比如下思路,我们可以把我们获得的n个三元组按照第一维a升序排序,然后就消除了第一维a对于答案的影响,只用考虑b和c两个维度即可。
但是我们需要考虑一个问题:偏序问题具有自反性,也就是说右区间可能对左区间造成贡献。但按照分治思想,我们只能统计左区间向右区间的贡献。对于这个问题,我们的解决办法就是:去重。具体来说,就是把完全相同的三元组全部合并到它第一次出现的位置,并记录它的出现次数作为这个三元组的值,统计答案时直接统计这个值。
然后我们开始统计答案。
这时第一维a已经有序了,但第二维b第三维c都仍然处于无序状态。我们都知道,求解二维的逆序对问题有两种主流思路:树状数组维护和朴素分治。而cdq分治则创造性地将两者结合起来。
具体来说,我们按照分治思想拆分区间,对于左右区间分别排序,合并时里利用树状数组统计左区间对右区间的贡献。
具体实现流程大致如下:
- 将区间对半拆分为 \([l,mid]\) 和 \([mid+1,r]\),直接继续向下递归拆分,直到分无可分时开始回溯
- 回溯过程中,用一组双指针扫描左右区间,动态地对第二维b进行排序。当出现 \(b_{i} \le b_{j}\) 这种满足偏序关系的情况时,用树状数组记录当前位置的c造成的贡献。否则,统计当前位置的贡献并加入答案。
- 双指针扫完以后要进行扫尾,处理由于左右区间长度不均匀没被扫到的区域;每进行一对左右区间的统计,都需要清空树状数组,等待下一对区间统计。
由于题目要求我们给出一个比较特殊的答案统计,按照题目要求重新统计一遍答案并输出就行了。答案统计方式如下:
\(mem[i].ans\) 统计了满足偏序关系但不包含完全相同这种情况的j的对数,而先前记录的 \(mem[i].val\) 恰好记录了完全相同的j的对数,两者之和就是去重后第i个点的答案。
代码和模板题地址:
P3810 【模板】三维偏序(陌上花开)
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 500010
#define lowbit(x) x&(-x)
int n,m,res[N];
struct memb{
int a,b,c,val,ans;
}mem[N],tmp[N];
int Bit[N],cnt=1;
/*排序*/
bool cmp(memb x,memb y){
return x.a!=y.a?x.a<y.a:(x.b!=y.b?x.b<y.b:x.c<y.c);
}
/*树状数组*/
void add(int x,int k){
while(x<=m){Bit[x]+=k; x+=lowbit(x);}
return;
}
//树状数组前缀和
int ask(int x){
int res=0;
while(x!=0){res+=Bit[x]; x-=lowbit(x);}
return res;
}
/*cdq分治*/
void Cdqmerge(int l,int r){
if(l==r) return;
int mid=(l+r)>>1;
//继续分治子问题
Cdqmerge(l,mid);
Cdqmerge(mid+1,r);
int i=l,j=mid+1,k=l;
while(i<=mid&&j<=r){
if(mem[i].b<=mem[j].b){
add(mem[i].c,mem[i].val);
tmp[k++]=mem[i++];
}
else{
mem[j].ans+=ask(mem[j].c);//统计答案
tmp[k++]=mem[j++];
}
}
//扫尾,同时统计答案
while(i<=mid){add(mem[i].c,mem[i].val);tmp[k++]=mem[i++];}
while(j<=r){mem[j].ans+=ask(mem[j].c);tmp[k++]=mem[j++]; }
//清空树状数组,等待下一次查询
for(int p=l;p<=mid;p++) add(mem[p].c,-mem[p].val);
//把tmp中临时储存的数据放回原数组
for(int p=l;p<=r;p++) mem[p]=tmp[p];
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>mem[i].a>>mem[i].b>>mem[i].c;
mem[i].ans=0;mem[i].val=1;
}
//先把a这一维排序
sort(mem+1,mem+n+1,cmp);
//去重,并记录重复三元组出现的次数
cnt=1;//第一个数保留,从第2个数开始修改
for(int i=2;i<=n;i++){
if(mem[i].a==mem[cnt].a&&mem[i].b==mem[cnt].b&&mem[i].c==mem[cnt].c)
mem[cnt].val++;
else mem[++cnt]=mem[i];
}
Cdqmerge(1,cnt);
//统计所有答案
for(int i=1;i<=cnt;i++){
res[mem[i].ans+mem[i].val-1]+=mem[i].val;
}
for(int i=0;i<n;i++) cout<<res[i]<<"\n";
return 0;
}

浙公网安备 33010602011771号