论CDQ分治

论CDQ分治

前言

需要的前置知识点:归并排序,树状数组(或线段树),偏序问题的定义。

归并排序求逆序对(极其重要,会了这个其实你就可以不用看了因为这个就是cdq分治,当然不会也可以看因为没有用到这个)

二维偏序:二维数点问题

问题是,坐标系第一象限有若干个点,求每个点左下角的点数。

解法:排序排一维,然后第二维上数据结构,比如树状数组或线段树,因为排序保证了 x x x坐标的顺序,我们在树状数组里查 y y y小于当前点的就可以了。

复杂度 n l o g n nlogn nlogn

可以发现排序很好使,直接解决了一维,那能不能解决两维呢?看似不能,因为 x , y x,y x,y很难同时升序,但是实际上是可以的,方法就是cdq分治

问题转化

假设我们有两个区间,都是有序的,对于区间二的每一个数 x x x,求出区间一中有多少个 y y y比它小,这个怎么做?双指针。

那么回归刚才的问题,我们也想用这个解法,为什么这个很好呢?考虑将原序列划分为两个区间,保证区间一的 x x x更小,区间内按 y y y排序,然后双指针,就可以做了。

但是问题是,区间一对区间二的贡献计算全了,那区间二内部的呢?

当你问出这个的时候,你发现这个等价于原问题 了,递归下去,做完了,这就是cdq分治。

算法实现

当然细节还是要说一说的,我们发现左右区间中 y y y局部有序,而且 x x x满足左区间任取都小于等于右区间,如果我们把 x x x看成下标,你发现这个性质完美地符合归并排序。

你还记得归并排序求逆序对吗,那个的本质就是cdq分治

因为逆序对可以归约到二维偏序,然后下标不用排天然有序,数值使用归并排一下。

实现方式就是按归并排序递归,然后合并的时候,如果右侧值小,就把那个点的答案计算一下,然后接着做就都一样了。

时间复杂度 n l o g n nlogn nlogn,同归并排序

从二维偏序到三维偏序

我就是喜欢数据结构如果学了cdq还能用到吗?

能的,这两个可以一起用。

对于偏序问题,有一个基本思路,就是计算维度。

排序 = 1维

cdq = 2维(就是加强版的排序)

树状数组/线段树 = 1维

树套树 = 2维

计算一下,cdq+树状数组 = 2+1 = 3

那么我们把问题换成三维数点

先按 z z z排序,这个时候在递归的结构里 z z z天然有序,你只需解决两个区间之间的二维偏序即可。

区间内按 x x x排序, x x x那维同样解决,现在你发现, y y y的限制不一定满足,原本是左区间将数插入新序列就要计算答案,但现在不一定了,怎么办?

直接扔进树状数组,然后右区间插入时按 y y y查询即可。

时间复杂度怎样呢?我们不直接清空树状数组,而是一个一个点删除,这样每个点进来一次就会被删除一次, n n n个点,每个点在 l o g n logn logn层中出现,即遍历 l o g n logn logn次,每次遍历到都要花 l o g n logn logn的时间插入或删除。

总计 n l o g 2 n nlog^2n nlog2n

给个代码吧(luogu P3810)

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N = 200010;
int n,k,cnt = 0;
int ans[N];
inline int lowbit(int x){
	return x&-x;
}
struct BITtree{
	int num[N];
	void update(int x,int y){
		for(int i = x;i<=k;i+=lowbit(i)){
			num[i]+=y;
		}
	}
	int query(int x){
		int res = 0;
		for(int i = x;i>0;i-=lowbit(i)){
			res+=num[i];
		}
		return res;
	}
}tr;
struct node{
	int x,y,z;
	int num,ans;
}a[N],b[N];
bool cmp1(node a,node b){
	if(a.z!=b.z)return a.z<b.z;
	else if(a.x!=b.x)return a.x<b.x;
	else return a.y<b.y;
}
queue<pair<int,int>> del;
void cdq(int l,int r){
	if(l==r)return;
	int mid = (l+r)>>1;
	cdq(l,mid);
	cdq(mid+1,r);
	for(int p = 1,i = l,j = mid+1;p<=r-l+1;p++){
		if(j>r||(i<=mid&&a[i].x<=a[j].x)){
			b[p] = a[i];
			tr.update(b[p].y,b[p].num);
			del.push({b[p].y,b[p].num});
			i++;
		}else{
			b[p] = a[j];
			b[p].ans+=tr.query(b[p].y);
			j++;
		}
	}
	while(!del.empty()){
		tr.update(del.front().first,-del.front().second);
		del.pop();
	}
	for(int i = l,j = 1;i<=r;i++,j++){
		a[i] = b[j];
	}
}
int main(){
	cin>>n>>k;
	for(int i = 1;i<=n;i++){
		cin>>b[i].x>>b[i].y>>b[i].z;
		b[i].ans = b[i].num = 0;
	}
	sort(b+1,b+n+1,cmp1);
	for(int i = 1;i<=n;i++){
		if(cnt==0||b[i].x!=b[i-1].x||b[i].y!=b[i-1].y||b[i].z!=b[i-1].z){
			cnt++;
			a[cnt] = b[i];
		}
		a[cnt].num++;
	}
	cdq(1,cnt);
	for(int i = 1;i<=cnt;i++){
		ans[a[i].ans+a[i].num-1]+=a[i].num;
	}
	for(int i = 0;i<n;i++){
		cout<<ans[i]<<"\n";
	}
	return 0;
}

其他应用

你要是刚学就不要看这里了,先把二维偏序和三维偏序的板子写了,理解理解,要不看这个有点困难

问题就是在静态数组中求区间mex,多组询问(luogu P4137)

我会主席树!

恭喜你秒了,但是我们不用数据结构。

首先我们发现,值域很大,但是屁用没有,一定会有一个 x ≤ n x \le n x≤n,满足 x x x不存在,那么大于 x x x的数没用了,直接扔。

然后我们发现,如果原序列是排列(即在上述情况基础上,没有重复的),区间mex等于补集min ,你会这个之后2026联合省选D2T1就秒了,但是这还不够,重复是难免的,补集里有,区间里可能还有。

这个时候不好做,但是我们可以发现一个性质,对于一个数 x x x,原序列中有若干个区间没有 x x x,只要我们的区间是这个区间的子区间,那么它就没有 x x x。

好了,我们现在有思路了,对于每个 x x x,假设它有 k k k个,那你就划分出 k + 1 k+1 k+1个区间,设这个区间左右端点分别为 p p p和 q q q,你要求所有 p p p小于等于 l l l且 q q q大于等于 r r r的区间中, x x x的最小值,显然,二维偏序,上cdq,先排 l l l和 p p p,然后在递归中局部排 q q q和 r r r,马上就做完了。

没啥用,感觉不如主席树。

但是cdq的题就是考验问题转化,你如果能早点做这题,省选D2T1不就秒了吗(虽然各位神犇本来也能秒)

后记

cdq分治没有数据结构好想,还不怎么考,但是你学了它一定不会后悔的。

二维的点阵,照理来说无法排序,但是cdq分治就可以一会排 x x x一会排 y y y,利用局部的有序解决问题。

总之就是cdq分治牛逼!

本文作者是蒟蒻,如有错误请各位神犇指点
森林古猿出品,必属精品,请认准CSDN森林古猿1!

相关推荐
weixin_423533998 小时前
c++类的继承学习-去中心化交易所(DEX)的“流动性池初始化与交易指令”设计
c++·学习·去中心化
气泡音人声分离8 小时前
技术解析|均衡器(EQ)工作原理与实操指南:从频率拆分到听感优化
算法·均衡器·音频剪辑
远离UE48 小时前
UE5 各类型灯光学习
学习·ue5
New农民工8 小时前
射频芯片学习-dBm概念
学习·射频学习
weixin_413063218 小时前
复现 MatchED 边缘检测模型(单张图片重复8次,训练200 epoch)
python·算法·计算机视觉·边缘检测模型
2601_962440848 小时前
计算机毕业设计之jsp教室管理系统
java·开发语言·笔记·分布式·算法·课程设计·推荐算法
AI视频剪辑官8 小时前
播客切片工具选型核心评价维度
网络·人工智能·算法
复杂网络11 小时前
AI 不睡觉,但它比你更会做实验
算法
贵慜_Derek11 小时前
MAI-04|干净数据在工程上意味着什么:MAI 预训练数据治理
人工智能·算法·llm