CDQ分治学习笔记

首先看看最简单的归并排序,来初步理解一下分治的思想

对于一个\([l,r]\)的区间,我们要进行归并排序,首先我们先递归区间\([l,mid]\)和区间\([mid+1,r]\),使得这两个区间有序。我们假设这两个区间已经是有序状态,然后,就是要将两个有序数列合并成一个有序新数列,我们不妨设两个数列分别是\(a\)数组和\(b\)数组,每次取\(a\)数组和\(b\)数组中较小的首元素取来压入到新数列中去。那么我们就可以在\(O(n)\)的复杂度内合并两个有序数列。

请读者仔细思考,这样操作的正确性,思考为什么是首元素,以及为什么要在两个子序列已经有序的情况下进行。这是\(CDQ\)分治大门打开的第一步,必须深刻理解其思想。

接下来就是要分析归并排序的复杂度,对于排序长为\(n\)的序列,复杂度为\(T(n)\)。因为要分治\([l,mid]\)\([mid+1,r]\),又二者长度为\(n/2\),故\(T(n)\)肯定包含\(2T(n/2)\)这一项。又要合并两个有序数列,故还包含\(O(n)\),故\(T(n)=2T(n/2)+O(n)\)。因为主定理过于困难,有兴趣的同学可以自主查阅,这里给出一种比较暴力的计算\(T(n)\)的方法。

\(T(n)=2*T(n/2)+O(n)\\=4*T(n/4)+O(n)+2*O(n/2)=4*T(n/4)+2*O(n)\\=......\\={2^k}*T(\dfrac{n}{2^k})+k*O(n)\)

为了使\({2^k}*T(\dfrac{n}{2^k})\)

没有时间复杂度贡献,故让\(\dfrac{n}{2^k}<1\),于是有\(k=log_{2}{n}\),所以\(T(n)=k*O(n)=log_{2}{n}*O(n)\)

综上,归并排序时间复杂度为\(O(nlgn)\)十分优秀。\(\\\)(本文中\(2\)为底的对数简写为\(lgn\),数学中\(lgn\)是以\(10\)为底的)

接下来一定要学习标程的实现方法,实际并不是\(a\)\(b\)两个数组,而是直接在原数组中操作的。

以下是代码实现(升序排序):

void merge(int l,int r)
{
	if(l==r) return;
	int mid=(l+r)>>1;
	merge(l,mid);merge(mid+1,r);
	//递归左子区间和右子区间
	int t1=l,t2=mid+1;
	for(int i=l;i<=r;i++)
	{
		if((a[t1]>=a[t2]&&t1<=mid)||t2>r) b[i]=a[t1++];
		//取较小首元素
		//a=b[c++];相当于a=b[c],c++;
		//a=b[++c];相当于c++,a=b[c];
		else b[i]=a[t2++];
	}
	for(int i=l;i<=r;i++) a[i]=b[i];
	//已有序,压回原数列中
}

接下来稍微升级一下,考虑如何计算一个没有重复元素的数列的逆序对数,这里就要考虑到一个重要的思想——贡献。

我们每次考虑左子区间每个数对右子区间每个数的贡献。首先,显然的是,左子区间的数的在原数列的位置都小于右子区间的数的原位置。如果左子区间已经有\(sum\)个数未压入,现在取较小首元素取到的是右子区间的数\(x\)

故知道已压入的数都小于\(x\),未压入的数都大于\(x\),所以一共有\(sum\)个数大于\(x\)且在\(x\)位置之前。那么这些数都与\(x\)产生一个逆序对,贡献了\(sum\)个逆序对。

仔细思考,不难发现,\(x\)之前的所有数都在归并排序全过程中与\(x\)进行了贡献,可以用倍增每次的结果取断点,把\(x\)位置前的区间分成几段来考虑。

于是总逆序对数就结了,时间复杂度还是\(O(nlgn)\),十分优秀。

以下是标程,与归并标程类似:

void merge(int l,int r)
{
	if(l==r) return;
	int mid=(l+r)>>1;
	merge(l,mid);merge(mid+1,r);
	int t1=l,t2=mid+1;
	for(int i=l;i<=r;i++)
	{
		if((a[t1]>=a[t2]&&t1<=mid)||t2>r) b[i]=a[t1++];
		else
		{
			ans+=mid-t1+1;
			//mid-t1+1刚好是左子区间没加进去的数的个数
			b[i]=a[t2++];
		}
	}
	for(int i=l;i<=r;i++) a[i]=b[i];
}
posted @ 2022-05-29 16:47  chenguoyi  阅读(35)  评论(0编辑  收藏  举报