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分治则创造性地将两者结合起来。
具体来说,我们按照分治思想拆分区间,对于左右区间分别排序,合并时里利用树状数组统计左区间对右区间的贡献
具体实现流程大致如下:

  1. 将区间对半拆分为 \([l,mid]\)\([mid+1,r]\),直接继续向下递归拆分,直到分无可分时开始回溯
  2. 回溯过程中,用一组双指针扫描左右区间,动态地对第二维b进行排序。当出现 \(b_{i} \le b_{j}\) 这种满足偏序关系的情况时,用树状数组记录当前位置的c造成的贡献。否则,统计当前位置的贡献并加入答案。
  3. 双指针扫完以后要进行扫尾,处理由于左右区间长度不均匀没被扫到的区域;每进行一对左右区间的统计,都需要清空树状数组,等待下一对区间统计
    由于题目要求我们给出一个比较特殊的答案统计,按照题目要求重新统计一遍答案并输出就行了。答案统计方式如下:
    \(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;
}

posted @ 2025-05-28 17:19  Yun_Mo_s5_013  阅读(28)  评论(0)    收藏  举报