论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!

相关推荐
洛水水16 分钟前
【力扣100题】81.寻找两个正序数组的中位数
数据结构·算法·leetcode
H__Rick27 分钟前
C51学习-DAY4
嵌入式硬件·学习·51单片机·硬件工程
happymaker06261 小时前
LeetCodeHot100——155.最小栈
算法
洛水水1 小时前
【力扣100题】85.每日温度
算法·leetcode·职场和发展
red_redemption1 小时前
自由学习记录(201)
学习
一条泥憨鱼1 小时前
Java开发效率神器:Lombok从入门到精通!
java·后端·学习·开发·lombok
Coder-magician1 小时前
《代码随想录》刷题打卡day15:二叉树part05
数据结构·c++·算法
Kurisu_红莉栖1 小时前
力扣56合并区间
算法·leetcode
Irissgwe1 小时前
算法的时间复杂度和空间复杂度
数据结构·c++·算法·c·时间复杂度·空间复杂度
随意起个昵称1 小时前
区间dp-基础题目3(永别)
c++·算法