2024年第十五届蓝桥杯C/C++大学B组题 B逆序对期望

前言:“语文是读懂题目的前提,数学是做对题目的基础。”当两者都不兼备的时候,那就把这题让了吧。逆序对是什么?期望又是什么?这是博主当时在做这道题目的真实想法。现在想想,无论是逆序对还是期望,二者任意一个概念不清楚都不太可能做出来,所以本道题目的复盘工作把时间都花在了收集资料上(绝对不是玩了一个五一假期的缘故

 一、题目分析

       逆序对,简单来讲是一组值与排序不相符的数字,例如(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(可能性)都为\frac{1}{n}

        最后需要将结果四舍五入保留两位小数,那我们这里就使用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

三、经验总结

       虽然本道题是博主没见过的题型,但对其深入的了解和反复推敲是非常值得的,这样不仅能复习巩固旧的知识,还能去了解新的内容,对个人的能力提升是个非常不错的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值