归并排序(MERGE SORT)是又一类不同的排序方法,合并的含义就是将两个或两个以上的有序数据序列合并成一个新的有序数据序列,因此它又叫归并算法。它的基本思想就是假设数组A有N个元素,那么可以看成数组A是又N个有序的子序列组成,每个子序列的长度为1,然后再两两合并,得到了一个 N/2 个长度为2或1的有序子序列,再两两合并,如此重复,值得得到一个长度为N的有序数据序列为止,这种排序方法称为2路合并排序。
例如数组A有7个数据,分别是: 49 38 65 97 76 13 27,那么采用归并排序算法的操作过程如图所示:
初始值 [49] [38] [65] [97] [76] [13] [27]
看成由长度为1的7个子序列组成
第一次合并之后 [38 49] [65 97] [13 76] [27]
看成由长度为1或2的4个子序列组成
第二次合并之后 [38 49 65 97] [13 27 76]
看成由长度为4或3的2个子序列组成
第三次合并之后 [13 27 38 49 65 76 97]
归并排序算法过程图
合并算法的核心操作就是将一维数组中前后相邻的两个两个有序序列合并成一个有序序列。合并算法也可以采用递归算法来实现,形式上较为简单,但实用性很差。
序列逆序数:
定义1 由1,2,…,n 组成的一个有序数组称为一个n级排列。在一个排列中如果一对数的前后位置与大小顺序相反,即大数排在小数的前面,则称它们为一个逆序。一个排列中所有逆序的总和称为该排列的逆序数。
那么,如何求解一个序列的逆序数呢?暴力法是问题解的最简单的方法,下面给出核心代码:
int Inversion Number(int *a,int n)
counter=0
for(i from 1 to n)
for(j from i+1 to n)
if(a[i]>a[j]) counter=counter+1
return counter;
直接根据定义暴力求解固然简单,但是往往效率不高,O(n^2)的复杂度在数据规模较大的时候,是不很乐观的。
《算法导论》(第二版P24)有这样一个问题:给出一个算法,它能在O(n*lgn)的最坏情况运行时间,确定n个
元素的任何排列中逆序对的数目。(提示:修改归并排序)
归并求逆序简单原理:
归并排序是分治的思想,具体原理自己去看书吧。利用归并求逆序是指在对子序列 s1和s2在归并时,若s1[i]>s2[j](逆序状况),则逆序数加上s1.length-i,因为s1中i后面的数字对于s2[j]都是逆序的。
void merge(long a[],int p,int q,int r)
{
int n1=q-p+1;
int n2=r-q;
int i,j,k;
long left[n1+1];
long right[n2+1];
for(i=1;i<=n1;i++)
left[i]=a[p+i-1];
for(j=1;j<=n2;j++)
right[j]=a[q+j];
left[n1+1]=1e9; //设置一个哨兵
right[n2+1]=1e9; //同上
i=1;
j=1;
for(k=p;k<=r;k++)
if(left[i]<right[j])
a[k]=left[i++];
else
{
a[k]=right[j++];
ans+=n1-i+1; //计数;
}
}
void mergesort(long a[],int p,int r)
{
if(p<r)
{
int q=(p+r)/2;
mergesort(a,p,q);
mergesort(a,q+1,r);
merge(a,p,q,r); //合并步骤 p<=left<=q q<right<=r;
}
}
PKU 1007:
求逆序数,然后排序输出就行了。
PKU 1804, PKU 2299:
是最简单的关于逆序对的题目,题目大意是给出一个序列,求最少移动多少步可能使它顺序,规定只能相邻移动。
相邻移动的话,假设a b 相邻,若a<b 交换会增加逆序数,所以最好不要做此交换;若a==b 交换无意思,也不要进行此交换;a>b时,交换会减少逆序,使序列更顺序,所以做交换。
由上可知,所谓的移动只有一种情况,即a>b,且一次移动的结果是逆序减1。假设初始逆序是n,每次移动减1,那么就需要n次移动时序列变为顺序。所以题目转化为直接求序列的逆序便可以了。
ZJU 1481:
这题和本次预选赛的F略有相似,不过要简单得多。题意是给定序列s,然后依次将序列首项移至序列尾,这样共有n-1次操作便回到了原序列(操作类似于循环左移)。问这n-1次操作和原序列,他们的逆序数最小的一次是多少?
有模板在手,直观地可以想到是,对于这n次都求逆序数,然后输出最小的一次就可以了,但这样做的复杂度有O(n*nlogn),太过复杂。
如果只求初始序列的逆序数的话,只要后面的n-1次操作的逆序数能够在O(1)的算法下求得,就能保证总体O(nlogn)的复杂度了。事实上,对于每次操作确实可以用O(1)的算法求得逆序数。将序列中ai移到aj的后面,就是ai做j-i次与右邻的交换,而每次交换有三个结果:逆序+1、逆序-1、逆序不变。由于题目中说明序列中无相同项,所以逆序不变可以忽略。逆序的加减是看ai与aj间(包括aj)的数字大小关系,所以求出ai与aj间大于ai的数字个数和小于ai的数字个数然后取差,就是ai移动到aj后面所导致的逆序值变化了。
依据上面的道理,因为题目有要求ai是移动到最后一个数,而ai又必定是头项,所以只要计算大于ai的个数和小于ai的个数之差就行了。然后每次对于前一次的逆序数加上这个差,就是经过这次操作后的逆序数值了。
PKU 2086:
这题不是求逆序对,而是知道逆序数k来制造一个序列。要求序列最小,两个序列比较大小是自左向右依次比较项,拥有较大项的序列大。
其实造序列并不难,由1804可知,只要对相邻数做调整就能做到某个逆序数了。难点是在求最小的序列。举例 1 2 3 4 5,要求逆序1的最小序列是交换4 5,如果交换其他任意相邻数都无法保证最小。由此可以想到,要保证序列最小,前部分序列可以不动(因为他们已经是最小的了),只改动后半部分。而我们知道n个数的最大逆序数是n*(n-1)/2,所以可以求一个最小的p,使得 k<p*(p-1)/2。得到前半部分是1到n-p,所有的逆序都是由后半部分p个数完成的。
考虑k=7,n=6的情况,求得p=5,即前部分1不动,后面5个数字调整。4个数的最大逆序是5 4 3 2,逆序数是6,5个数是6 5 4 3 2,逆序数是10。可以猜想到,保证5中4个数的逆序不动,调整另一个数的位置就可以增加或减少逆序数,这样就能调整出6-10间的任意逆序。为了保证最小,我们可以取尽量小的数前移到最左的位置就行了。2前移后逆序调整4,3前移后调整了3,4调整2,5调整1,不动是调整0,可以通过这样调整得到出6-10,所以规律就是找到需要调整的数,剩下的部分就逆序输出。需要调整的数可以通过总逆序k-(p-1)*(p-2)/2+(n-p)求得。
PKU 1455:
这是一道比较难的关于逆序数推理的题目,题目要求是n人组成一个环,求做相邻交换的操作最少多少次可以使每个人左右的邻居互换,即原先左边的到右边去,原右边的去左边。容易想到的是给n个人编号,从1..n,那么初始态是1..n然后n右边是1,目标态是n..1,n左边是1。
初步看上去好象结果就是求下逆序(n*(n-1)/2 ?),但是难点是此题的序列是一个环。在环的情况下,可以减少许多次移动。先从非环的情况思考,原1-n的序列要转化成n-1的序列,就是做n(n-1)/2次操作。因为是环,所以(k)..1,n..k+1也可以算是目标态。例如:1 2 3 4 5 6的目标可以是 6 5 4 3 2 1,也可以是 4 3 2 1 6 5。所以,问题可以转化为求形如(k)..1,n..k+1的目标态中k取何值时,逆序数最小。
经过上面的步骤,问题已经和ZJU1481类似的。但其实,还是有规律可循的。对于某k,他的逆序数是左边的逆序数+右边的逆序数,也就是(k*(k-1)/2)+((n-k)*(n-k-1)/2) (k>=1 && k<=n)。展开一下,可以求得k等于n/2时逆序数最小为((n*n-n)/2),现在把k代入进去就可以得到解了。
要注意的是k是整数,n/2不一定是整数,所以公式还有修改的余地,可以通用地改为(n/2)*(n-1)/2。
PKU 2893:
用到了求逆序数的思想,但针对题目还有优化,可见M*N PUZZLE的优化。
PKU 1077:
比较经典的搜索题,但在判断无解的情况下,逆序数帮了大忙,可见八数码实验报告。