简单入门CDQ分治(很有意思的算法)

最近因为牛客暑期多校的一道题涉及到了CDQ分治,于是便去学习了一下CDQ分治。

CDQ分治是以曾经的IOI选手陈丹琦命名的一种强大的算法,主要用于解决偏序问题,通过对一维进行排序(在这里说的总维度为二维),再对其它维度进行分治。

再来讲讲CDQ分治的特点,普通分治是将原问题划分为若干个子问题,每个子问题相互独立且与原问题形式相同,递归求解子问题,最后合并子问题的解得到原问题的解。而CDQ分治中,对于你每一次划分出来的两个子问题,前一个子问题用来解决后一个子问题,而不是其本身。

CDQ分治是一个非常不错的算法,不过当我们想要使用它时需要注意,题目里修改操作对询问的贡献独立,修改操作互不影响,题目允许使用离线算法

好了,CDQ分治的基本介绍就到这为止,现在我们来开始基本的CDQ分治的学习。

1.归并排序

学习CDQ分治,归并排序是不可或缺的。

直接上代码吧。

#include<bits/stdc++.h>
using namespace std;
int b[100];
void merge_sort(int l,int r,int *a){
	if(l==r)return;
	int m=(l+r)>>1;
	merge_sort(l,m,a);
	merge_sort(m+1,r,a);
	int t1=l,t2=m+1;
	for(int i=l;i<=r;i++){
		if((a[t1]<a[t2]&&t1<=m)||t2>r){
			b[i]=a[t1++];
		}
		else{
			b[i]=a[t2++];
		}
	}
	
	for(int i=l;i<=r;i++)a[i]=b[i];
}
int main(){
	int c[10]={5,6,87,8,45,7,2,1,14,3};
	merge_sort(0,9,c);
	for(int i=0;i<10;i++)cout<<c[i]<<' ';
}

也许有些人不是很理解这段代码什么逻辑,那么我便为这些人专门讲解一下吧。上述代码的总体思想是维护你每次递归划分的左区间和右区间的有序性,最后使得整个区间有序,至于怎么维护,你们仔细看代码,我们将原区间划分到不可再分即l==r(只有一个元素)时,我们需要返回到上一次划分的时候,即区间里只有两个元素,这时左区间和右区间必定有序(因为左区间和右区间都只有一个元素),然后我们对这段区间进行排序,维护该段区间的整体有序性,后续的操作也是类似的道理,至于更多的细节还请大家看代码自己领悟。

 

2.逆序对问题

在了解了归并排序后,我们再来讲一下CDQ分治最简单的应用——逆序对问题。

题目:给一列数a1,a2,…,an,求它的逆序对数,即有多少个有序对(i,j),使得i<j但ai>aj。1<=n<=1e6;

我们很容易找到一种特殊情况,即这一列数的左区间和右区间都是有序的,这时我们便可以通过很简单的比较求出有多少个逆序对并且不会超时,既然左区间和右区间有序,不妨设左区间为[1,k],右区间为[k+1,n],那么若是a[i]>a[j]a[i]为左区间里的一个数,a[j]为右区间的一个数),那么ans+=k-i+1,我们对左区间里的每一个数进行这样一个比较,便能求得整个区间的逆序对数。

看到这里详细很多人已经明白了,既然这样的话,那么我用归并排序维护每个递归划分的区间的有序性,同时统计这些区间的答案不就行了吗?没错,这就是CDQ分治了,通过归并排序维护有序性,再通过左区间对右区间求解。

具体怎么做,我们还是看代码吧。

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+9;
int a[maxn],b[maxn],ans=0;
void CDQ(int l,int r,int *a){
	if(l==r)return;
	int m=(l+r)>>1;
	CDQ(l,m,a);
	CDQ(m+1,r,a);
	int t1=l,t2=m+1;
	for(int i=l;i<=r;i++){
		if(a[t1]>a[t2]&&t2<=r||t1>m){
			b[i]=a[t2++];
			ans+=m-t1+1;
		}
		else{
			b[i]=a[t1++];
		}
	}
	for(int i=l;i<=r;i++)a[i]=b[i];
}
int main(){
	int i,j,k,n;
	cin>>n;
	for(i=1;i<=n;i++){
		cin>>a[i];
	}
	CDQ(1,n,a);
	cout<<ans<<endl;
}

看完代码之后是不是感觉和归并排序特别像呢,没错,这段代码其实就是对归并排序进行了一些些微的改动而已,考虑了前面区间对后面区间的影响,统计了答案。

 

3.二维偏序问题

其实之前的逆序对问题也可是称之为二维偏序问题,只不过逆序对问题中其中一维(下标)默认是有序的,于是我们便忽略了这一维,只考虑了值这一维度。在二维偏序问题中我们经常要对其中一个维度进行排序,再对另一个维度进行CDQ分治,来得出问题的答案。

在这里给出一个比较经典的问题吧。

给定一个N个元素的序列a,对这个序列进行M次以下两种操作中的一个 
操作1:格式1 x k,把位置x的元素加上k 
操作2:格式为2 x y,求出区间[x,y]内所有元素的和

有些同学在学习树状数组时可能见过这个问题,没错,这个问题确实可以用树状数组来解决,而且还比用CDQ分治要轻松,但是,那又如何,今天我就要用CDQ分治来解决这个问题。

我们注意到这个问题有两个维度,第一个维度,操作的时间(可以理解为数组下标),第二个维度,操作的位置。时间这个维度因为我们是按照顺序加入的,所以不用管,我们只需要对位置维度进行CDQ分治就行了。

注意:我们不要忘记CDQ分治使用的前提,这道题因为有多次询问,我们需要使用离线算法

piu,直接上代码。

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+9;
struct node{
	int type,set,val;
	bool operator <(const node &a){//用于排序 
		if(set!=a.set)return set<a.set;
		else return type<a.type;
	}
}a[3*maxn],b[3*maxn];
int ans[maxn];
void CDQ(int l,int r,node *a){
	if(l==r)return;
	int m=(l+r)>>1;
	CDQ(l,m,a);
	CDQ(m+1,r,a);
	int t1=l,t2=m+1,sum=0;
	for(int i=l;i<=r;i++){
		if(a[t1]<a[t2]&&t1<=m||t2>r){
			if(a[t1].type==1){
				sum+=a[t1].val;	//类似前缀合 
			}
			b[i]=a[t1++];
		}
		else{
			if(a[t2].type==2)ans[a[t2].val]-=sum; 
			else if(a[t2].type==3)ans[a[t2].val]+=sum;
			b[i]=a[t2++];
		}
	}	
	for(int i=l;i<=r;i++)a[i]=b[i];
}
int main(){
	int i,j,k,n,m,cnt;
	cin>>n>>m;
	for(i=1;i<=n;i++){
		cin>>a[i].val;
		a[i].type=1;a[i].set=i;
	}
	cnt=n+1;
	int tot=1;
	for(i=1;i<=m;i++){
		int t,x,y;
		cin>>t>>x>>y;
		if(t==1){
			a[cnt].val=y;
			a[cnt].set=x;
			a[cnt++].type=1;
		}
		else{//离线 
			a[cnt].val=tot;a[cnt].type=2;a[cnt++].set=x-1;
			a[cnt].val=tot++;a[cnt].type=3;a[cnt++].set=y;
		}
	}
	CDQ(1,cnt-1,a);
	for(i=1;i<tot;i++)cout<<ans[i]<<endl;
}

没错,这道题还用到了一点前缀和的知识,哈哈哈哈。反正这段代码认真看吧,如果前面两个弄懂了,这个应该也不难弄懂。

 

4.三维偏序问题

解决了二维偏序问题,我们来看一看三维偏序问题。对于三维偏序问题,我就讲一讲大概的思路,因为我也不是很熟练。

首先,我们对一维进行排序,然后我们就可以忽略这一维度的影响了,之后我们再对另外一维进行CDQ分治。

ok,前面两个维度已经解决了,那么我们要如何解决最后一个维度呢,其中一种办法是通过树状数组解决。至于具体怎么解决我就不多说了,感兴趣的自己去百度吧。

 

 

转载于:https://www.cnblogs.com/zookkk/p/10398131.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值