前言:“语文是读懂题目的前提,数学是做对题目的基础。”当两者都不兼备的时候,
那就把这题让了吧。逆序对是什么?期望又是什么?这是博主当时在做这道题目的真实想法。现在想想,无论是逆序对还是期望,二者任意一个概念不清楚都不太可能做出来,所以本道题目的复盘工作把时间都花在了收集资料上(绝对不是玩了一个五一假期的缘故)
一、题目分析
逆序对,简单来讲是一组值与排序不相符的数字,例如(3,2)在顺序(自然数排序)排序里就是一对逆序对,因为3排在了2之前,但是3的值要比2大,不符合排序的标准,换成数组来理解就比较清晰了。假设有一个顺序数组 a[10],里面存放了1,2,3……10等数字,但是其中有一对数字的位置互换了,遍历整个数组,就会出现 i < j,而a[ i ] > a[ j ]的情况,这个时候我们就称 i 和 j 为一组逆序对。那么我们也会明锐的觉察到,如果一个顺序数列的开头和结尾的位置互换了,那么从头开始往后数的每一个数字都能和首个数字组成一组逆序对,这种情况下出现的逆序对的数量是最多的。因此,不同位置的交换会影响逆序对数量,我们需要一一例举出每种交换位置的情况。
求逆序数,可以通过基本的双重循环遍历去寻找,但是程序的时间复杂度比较大,再嵌套进必要的循环里,运行时间就会非常长了,本人也是不大喜欢dfs类的解法,所以在这里我们使用归并算法进行求逆序对的这个操作。
本道题的核心算法——归并算法,原理是分治思想,将一个主序列拆分成许多个子序列,而每一个子序列都拥有相同的特点,因此对每个子序列排列后再合并至有序时,与正常的排序出来的结果是一样的。
左右两边比较,小的就放在前面,大的放后面,两者之间的位置差即逆序对数量。(以2 3 4和1 5 6的比较为例,1<2,因为2 3 4子序列在之前的排序中已经有序,则之后的数3 4都比2大,所以也理所当然地比1大,因此从逆序数对开始出现的两个位置相减就能获得逆序对的数量)比较完一轮就合并,将每轮的数量相加,最后出来的结果就是逆序对的个数。
归并算法包含了拆和并两个过程(实际上是三个,还有排序),由上图的步骤分析可以看出拆完是第一步骤,合并是第二步骤,且顺序为先拆完再合并,如果是顺序代码是不大可能出现这样的效果的,所以这里用到了递归,就能保证在合并前先完成其对序列的拆分排序。
用归并算法求逆序数的代码如下:
#include<iostream>
#include<cstdio>
using namespace std;
int n,a[10000],i,c[10000];
long long ans;
void x(int l,int r)
{
int mid=(l+r)/2,i,j,tmp;
if(r>l)
{
x(l,mid);
x(mid+1,r);
tmp=l;
for(i=l,j=mid+1;i<=mid&&j<=r;)
{
if(a[i]>a[j]) //当出现这种情况时表明ij为逆序数
{
c[tmp++]=a[j++];//把j并入数组中,运算顺序为先赋值,后分别自增
ans+=mid-i+1; //因为j是较小的一个,那么从i之后到j之前的每个数都应该与j构成逆序对
}
else c[tmp++]=a[i++]; //i和j是顺序,继续执行
}
if(i<=mid) for(;i<=mid;) c[tmp++]=a[i++]; //该情况为j>r时出现,则说明本轮r侧已经有序
if(j<=r) for(;j<=r;) c[tmp++]=a[j++]; //该情况为i>mid时出现,则说明本轮l侧已经有序
for(i=l;i<=r;i++) a[i]=c[i]; //将数据覆盖到原数组中
}
}
int main()
{
cin>>n; //输入元素个数
for(i=1;i<=n;i++) scanf("%d",&a[i]);
x(1,n);
cout<<ans;
}
第一大问题就解决了,接下来是第二个问题,如何求期望。根据题目意思,一次随即交换是指均匀随机选取两个位置交换,也就是说每一对出现的数字都有可能,从(1,2),(1,3)……到(50,51),这里就要用到双重循环遍历,两次随机操作则需要四重循环完成,其中有可能出现操作位在同一对数身上的情况,这样一来代码就比较简单了,不用考虑四个数字相互重复的可能,只需保持一次操作中比较的两个位置不同就行了。在这里,逆序对的个数期望是由每一个可能出现的结果除以操作总数再相加,原因是每一次交换的可能性都是一样的(均匀随机),则只需要考虑其相对于操作总数所占的比,数学期望E(X)的公式如下所示
这里的每一个p(可能性)都为
最后需要将结果四舍五入保留两位小数,那我们这里就使用round函数对其进位保留。
二、代码实现
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
int n,a[10000],i,c[10000];
long long ans;
void x(int l,int r)
{
int mid=(l+r)/2,i,j,tmp;
if(r>l) //递归算法的“出口”
{
x(l,mid); //这里在递归
x(mid+1,r);
tmp=l;
for(i=l,j=mid+1;i<=mid&&j<=r;)
{
if(a[i]>a[j]) //当出现这种情况时表明ij为逆序数
{
c[tmp++]=a[j++];//把j并入数组中,运算顺序为先赋值,后分别自增
ans+=mid-i+1; //因为j是较小的一个,那么从i之后到j之前的每个数都应该与j构成逆序对
}
else c[tmp++]=a[i++]; //i和j是顺序,继续执行
}
if(i<=mid) for(;i<=mid;) c[tmp++]=a[i++]; //该情况为j>r时出现,则说明本轮r侧已经有序
if(j<=r) for(;j<=r;) c[tmp++]=a[j++]; //该情况为i>mid时出现,则说明本轮l侧已经有序
for(i=l;i<=r;i++) a[i]=c[i]; //将数据覆盖到原数组中
}
} //求逆序数的代码无需变动
int main()
{
n=51;
int num = 0;
for (int t1 = 1; t1 <= n; ++ t1 )//第一次的i
for (int t2 = t1 + 1; t2 <= n; ++ t2 )//第一次的j,j = i+1就可以保证交换的位置不同,还可以提高算法速度,因为12交换和21交换都会计入num操作总数中,这里相当于把2/4变成1/2
for (int t3 = 1; t3 <= n; ++ t3 )//第二次的i
for (int t4 = t3 + 1; t4 <= n; ++ t4 )//第二次的j
{
num ++;//操作次数+1
for (int k = 1; k <= n; ++ k )//生成1~51的顺序数组
a[k] = k;
swap(a[t1], a[t2]);//第一次随机交换
swap(a[t3], a[t4]);//第二次随机交换
x(1,n);//求逆序数个数
}
printf("%.2lf\n", round((double)ans/num* 100) / 100);//输出最终结果
return 0;
}
输出结果
65.33
三、经验总结
虽然本道题是博主没见过的题型,但对其深入的了解和反复推敲是非常值得的,这样不仅能复习巩固旧的知识,还能去了解新的内容,对个人的能力提升是个非常不错的。