四、归并排序(逆序对)
(一)、归并排序
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide
and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;
即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二
路归并。
例如有8 个数据需要排序:10 4 6 3 8 2 5 7
归并排序主要分两大步:分解、合并。
合并过程为:比较a[i]和a[j]的大小,若a[i]≤a[j],则将第一个有序表中的元
素a[i]复制到r[k]中,并令i 和k 分别加上1;否则将第二个有序表中的元素a[j]复制
到r[k]中,并令j 和k 分别加上1,如此循环下去,直到其中一个有序表取完,然后再将
另一个有序表中剩余的元素复制到r 中从下标k 到下标t 的单元。归并排序的算法我们通
常用递归实现,先把待排序区间[s,t]以中点二分,接着把左边子区间排序,再把右边子区
间排序,最后把左区间和右区间用一次归并操作合并成有序的区间[s,t]。
#include //归并排序
#include
#include
#include
using namespace std;// 示例:7 5 8 15 8 45 2 5
int m[1001],r[1001];
void compare(int a,int b){
if(a==b) return;
int mid = (a+b) / 2;
compare(a,mid);
compare(mid+1,b);//这两行是我一直没有想通的,一百行for循环写的很嗨皮。。。
int qian=a,hou=mid+1,tot=a;
while(qian<=mid&&hou<=b){
if(m[qian]<=m[hou]){
r[tot] = m[qian];
tot++;
qian++;
}
else{
r[tot] = m[hou];
tot++;
hou++;
}
}
while(qian<=mid){
r[tot]=m[qian];
tot++;
qian++;
}
while(hou<=b){
r[tot]=m[hou];
tot++;
hou++;
}//这两个while不用再写if了,也比我的思路好。
for(int i=a;i<=b;i++){
m[i]=r[i];
}
}
int main(){
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&m[i]);
}
compare(1,n);
for(int i=1;i<=n;i++){
printf("%d ",m[i]);
}
}
(二)、逆序对
上述提到归并排序是稳定的排序,相等的元素的顺序不会改变,进而用其可以解决逆序
对的问题。首先我们了解一下什么是逆序对。
逆序对:设A 为一个有n 个数字的有序集(n>1),其中所有数字各不相同。如果存
在正整数i, j 使得1 ≤ i < j ≤ n 而且A[i] > A[j],则<A[i], A[j]> 这个
有序对称为A 的一个逆序对,也称作逆序数。
例如,数组(3,1,4,5,2)的逆序对有(3,1),(3,2),(4,2),(5,2),共4 个。
所谓逆序对的问题,即对给定的数组序列,求其逆序对的数量。
从逆序对定义上分析,逆序对就是数列中任意两个数满足大的在前,小的在后的组合。
如果将这些逆序对都调整成顺序(小的在前,大的在后),那么整个数列就变的有序,即排
序。因而,容易想到冒泡排序的机制正好是利用消除逆序来实现排序的,也就是说,交换相
邻两个逆序数,最终实现整个序列有序,那么交换的次数即为逆序对的数量。
冒泡排序可以解决逆序对问题,但是由于冒泡排序本身效率不高,时间复杂度为
O(n^2),对于n 比较大的情况就没用武之地了。我们可以这样认为,冒泡排序求逆序对效
率之所以低,是因为其在统计逆序对数量的时候是一对一对统计的,而对于范围为n 的序
列,逆序对数量最大可以是(n+1)*n/2,因此其效率太低。那怎样可以一下子统计多个,
而不是一个一个累加呢?这个时候,归并排序就可以帮我们来解决这个问题。
在合并操作中,我们假设左右两个区间元素为:
左边:{3 4 7 9} 右边:{1 5 8 10}
那么合并操作的第一步就是比较3 和1,然后将1 取出来,放到辅助数组中,这个时候
我们发现,右边的区间如果是当前比较的较小值,那么其会与左边剩余的数字产生逆序关系,
也就是说1 和3、4、7、9 都产生了逆序关系,我们可以一下子统计出有4 对逆序对。接
下来3,4 取下来放到辅助数组后,5 与左边剩下的7、9 产生了逆序关系,我们可以统计
出2 对。依此类推,8 与9 产生1 对,那么总共有4+2+1 对。这样统计的效率就会大大提
高,便可较好的解决逆序对问题。
而在算法的实现中,我们只需略微修改原有归并排序,当右边序列的元素为较小值时,
就统计其产生的逆序对数量,即可完成逆序对的统计。
#include //基本完全copy上一题
#include
#include
#include
using namespace std;// 示例:7 5 8 15 8 45 2 5
int m[1001],r[1001],number=0;
void compare(int a,int b){
if(a==b) return;
int mid = (a+b) / 2;
compare(a,mid);
compare(mid+1,b);//这两行是我一直没有想通的,一百行for循环写的很嗨皮。。。
int qian=a,hou=mid+1,tot=a;
while(qian<=mid&&hou<=b){
if(m[qian]<=m[hou]){
r[tot] = m[qian];
tot++;
qian++;
}
else{
r[tot] = m[hou];
tot++;
hou++;
number += mid-qian+1;//较上一题就加了这一行,一开始还直接无脑写成了++。。。
}
}
while(qian<=mid){
r[tot]=m[qian];
tot++;
qian++;
}
while(hou<=b){
r[tot]=m[hou];
tot++;
hou++;
}//这两个while不用再写if了,也比我的思路好。
for(int i=a;i<=b;i++){
m[i]=r[i];
}
}
int main(){
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&m[i]);
}
compare(1,n);
for(int i=1;i<=n;i++){
printf("%d “,m[i]);
}
printf(”\n%d",number);
}
走楼梯
时间限制: 1 Sec 内存限制: 128 MB
题目描述
楼梯有N 级台阶,上楼可以一步上一阶,也可以一步上二阶,编一个递推程序,计算从第
一阶走到第N 阶共有多少种不同的方法。
输入
输入一个数N(1<=N<=10)
输出
输出共有多少种方法
样例输入
4
样例输出
5
解析:
这题是简单的递推题,首先,到第1 个台阶我们有1 种方法;到第2 个台阶,我们可
以在第0 个台阶上两阶,也可以在第1 个台阶上一阶到达,即有2 种方法;对于之后第i
个台阶来说,同样的方式,可以由第i-2 个台阶上两阶,也可以由第i-1 个台阶上一阶到
达。令f[i]表示到第i 台阶的方法数,由上述分析可以得出f[i]=f[i-1]+f[i-2],初
始f[1]=1,f[2]=2,即可推得到达第n 个台阶的方法数了,复杂度O(n)。
//两种方法差不多,有手就行
#include // 走楼梯(递归)
#include
#include
#include
using namespace std;
int jisuan(int n){
if(n1) return 1;
if (n2) return 2;
return jisuan(n-1)+jisuan(n-2);
}
int main(){
int n;
scanf("%d",&n);
printf("%d",jisuan(n));
}
#include // 走楼梯(递推)
#include
#include
#include
using namespace std;
int main(){
int n,m[101];
scanf("%d",&n);
m[1] = 1;
m[2] = 2;
for(int i=3;i<=n;i++){
m[i] = m[i-1]+m[i-2];
}
printf("%d",m[n]);
}
过河卒【NOIP2002】
时间限制: 1 Sec 内存限制: 128 MB
题目描述
棋盘上的A 点有一个过河卒,需要走到目标B 点。卒行走的规则是:可以向下、或者向右。
同时在棋盘上的任一点有一个对方的马(如C 点),对方的马所在的点和所有跳跃一步可
达的点称为对方马的控制点(如图中的C 点和P1,P2,…,P8)。卒不能通过对方马的控制
点。棋盘用坐标表示,A 点(0,0),B 点(n,m)(n,m 为不超过20 的整数),同样马的
位置坐标是需要给出的,C≠A 且C≠B。现在输入B 点和C 点的坐标,要你计算出过河卒
从A 点能够到达B 点的路径的条数。
图21 过河卒
输入
一行,4 个空格隔开的整数,表示(n,m)和C 点的坐标。
输出
过河卒从A 点能够到达B 点的路径的条数。
样例输入
4 8 2 4
样例输出
0
解析:
这题的递推思想和走楼梯的思路一样,在坐标(i,j)位置时,它可以由(i-1,j)和
(i,j-1) 点过来, 令f[i][j] 表示到达(i,j) 位置时的方案数, 那么
f[i][j]=f[i-1][j]+f[i][j-1]。当然此题稍微增加了难点,也就是有障碍点,这些
点不能达到,所以他们的方案数特殊处理为0,我们可以在一开始预处理掉。
#include // 过河卒 bug不少。。。
#include
#include
#include
using namespace std;
int main(){
int a[23][23],n,m,I,J,i,j;
scanf("%d%d%d%d",&n,&m,&I,&J);
for(i=0;i<=n;i++){
for(j=0;j<=m;j++){
a[i][j]=1;
printf("%d “,a[i][j]);
}
printf(”\n");
}
printf("\n");
if(I-1>=0&&J-2>=0) a[I-1][J-2]=0;
if(I-2>=0&&J-1>=0) a[I-2][J-1]=0;
if(I-2>=0&&J+1>=0) a[I-2][J+1]=0;
if(I-1>=0&&J+2>=0) a[I-1][J+2]=0;
if(I+1>=0&&J+2>=0) a[I+1][J+2]=0;
if(I+2>=0&&J+1>=0) a[I+2][J+1]=0;
if(I+1>=0&&J-2>=0) a[I+1][J-2]=0;
if(I+2>=0&&J-1>=0) a[I+2][J-1]=0;
a[I][J]=0;
for(i=1;i<=n;i++) if(a[i][0]) a[i][0]=a[i-1][0];
for(i=1;i<=m;i++) if(a[0][i]) a[0][i]=a[0][i-1];
for(i=0;i<=n;i++){
for(j=0;j<=m;j++){
printf("%d “,a[i][j]);
}
printf(”\n");
}
printf("\n");
for(i=1;i<=n;i++){
for(j=1;j<=m;j++){
if(a[i][j]){
a[i][j]=a[i-1][j]+a[i][j-1];
}
printf("%d “,a[i][j]);
}
printf(”\n");
}
printf("%d",a[n][m]);
}