【BIT2021程设】12.排队——归并排序求逆序数对

写在前面:

本系列博客仅作为本人十一假期过于无聊的产物,对小学期的程序设计作业进行一个总结式的回顾,如果将来有BIT的学弟学妹们在百度搜思路时翻到了这一条博客,也希望它能对你产生一点帮助(当然,依经验来看,每年的题目也会有些许的不同,所以不能保证每一题都覆盖到,还请见谅)。

不过本人由于学艺不精,代码定有许多不足之处,欢迎各位一同来探讨。

同时请未来浏览这条博客的学弟学妹们注意,对于我给出完整代码的这些题,仅作帮助大家理解思路所用(当然,因为懒,所以大部分题我都只给一个伪代码)。Anyway,请勿直接复制黏贴代码,小学期的作业也是要查重的,一旦被查到代码重复会严厉扣分,最好的方法是浏览一遍代码并且掌握相关的要领后自己手打一遍,同时也要做好总结和回顾的工作,这样才能高效地提升自己的代码水平。

加油!


指路类似题:力扣O51

成绩10开启时间2021年08月31日 星期二 12:30
折扣0.8折扣时间2021年09月5日 星期日 23:00
允许迟交关闭时间2021年10月10日 星期日 23:00

Description

小张在暑假时间来到工地搬砖挣钱。包工头交给他一项艰巨的任务,将一排砖头按照从低到高的顺序排好。可是小张的力量有限,每次只能交换相邻的两块砖头,请问他最少交换几次能够完成任务?

Input

第一行一个整数n(1 \leq n \leq 300000 ),表示砖头数量。
第二行n个整数a_i(-1000000000 \leq a_i \leq 1000000000 ),表示砖头的高度。

Output

一个整数,表示最少交换几次能够完成任务。

测试用例 1以文本方式显示
  1. 5↵
  2. 4 1 3 2 5↵
以文本方式显示
  1. 4↵
1秒64M


题意分析:

        想用冒泡排序是吧?想得美!哪有这么简单的题!

        这题当然,最容易想到的方法就是冒泡排序,因为整个流程和冒泡排序的流程完全一致,并且贪心的来看,冒牌排序的总的步骤数一定最少(同样,这里的贪心策略请读者试着自己证明一下,我会放在这一段的后方)。然而3e5的数据规模,实在是支撑不起我们这个O(n^2)的算法。

        这时候我们需要透过现象看本质,冒泡排序的背后究竟蕴含着什么?如果你认真证明了一遍贪心策略,你会发现,每一个元素要想“跨过山河大海”回到它们的家,它们就需要跨过那些在它们右侧且比他们小的元素,而那些比它们大的元素,肯定在它回家之前就已经回到家了,自然不会发生交换。而它的右侧比它小的每一个元素都与它形成了一个逆序数对,所以,真正的本质就是——逆序数对。我们总共的操作数,就是整个序列的逆序数对的数量。

        那么,逆序数对又要如何来求?冒泡排序已经被我们毙了,剩下一个和逆序数对有点关联的就是归并排序,为什么这么说呢?我们就要细看归并排序的过程。

        归并排序的最主要操作,就是把一个数组分成前后两截,先分别让前后两截已序,再利用双指针将前后两截合并为一个新的已序数组?至于前后两截怎么已序?那这就是归并排序用到的递归思想了,递归重点很显然就是该数组只有一个元素的时候。说回正题,这到底和逆序数对有什么关联呢?我们研究归并排序的主体部分,假设已经有前后各半段已序数组:

157913248911

        我们用双指针法,将两截归并的过程如下:

        1,5,7,9,13,2,4,8,9,11

        1,2,5,7,9,13,4,8,9,11 ——此时2从归并前的第6位到了归并后的第2位,并且它所跨越的所有元素都比它大,比它小的所有元素都在它最终位置的左侧,换句话说,这一次“跨越”总共消去了6-2=4对逆序数对;

        1,2,4,5,7,9,13,8,9,11——此时4从归并前的第7位到了归并后的第3位,并且它所跨越的所有元素都比它大(此时2已经到了第2位并且中间所有元素都右移了一位,所以并没有跨越2),比它小的所有元素(1和2)都在它最终位置的左侧,换句话说,这一次“跨越”总共消去了7-3=4对逆序数对;

        1,2,4,5,7,9,13,8,9,11——2和4跨过来后,5已经被挤到了第四位,这里正是它应在的位置   

        ……

        我们可以发现,每次归并排序只有两种可能,要么左半边的元素被归入,此时它的位置一定保持不变,要么右半边的元素被归入,此时它一定跨过且仅跨过若干个比它大的元素——这一步过后,整个数组的逆序数对消去了若干个,值等于总的跨越步长。同时,我们知道已序的数组的逆序数对一定为0,那么我们只需要追踪归并排序的过程,就可以知道这个逆序数对是由多少消为0的,换句话说,我们就能知道原始的逆序数对究竟有多少。

        好消息是,归并排序的时间复杂度是O(nlogn),且十分稳定,这在我们这题的数据规模之下实在是再好不过了,就他了!


贪心策略的证明:

        我们在冒泡排序的过程中,是做n轮,第i轮从第1个开始遍历到第n-i个,但凡遍历到的元素比后面的元素大,二者交换位置,这样第i轮就能确定整个序列中第i大的元素。如果不按这个策略,会有更少步骤的做法吗?我们如果随意地交换中间两个数,假如左侧的元素a小于右侧的元素b,那这一步操作就显得非常糟糕——一定至少还要再换一次,来让a重新回到b的左边,因为最终的结果一定是ab的左边;假如ab相等,那这个操作就完全没有意义,也非常糟糕; 假如a >b,那么这样并不会比冒泡排序更好——因为在冒泡排序的过程中,肯定也会有这么一次交换,当所有比a大的数都到最右边之后,这时候必然会发生ab的交换,所以这件事情是早晚会发生的,我冒泡排序只是让它发生得更有条理一点。综上所述,随机的交换总是不会比冒泡排序更好。说人话,就是冒泡排序没有任何的“无用操作”乃至“负作用操作”。


贴代码:

因为归并排序的思路大家应该很熟悉了,所以不过多讲解了,就是在中间额外追踪一个变量而已(记得开long long!!)

    #include <bits/stdc++.h>
    using namespace std;  
    typedef long long ll;  
    const int INF = 0x3f3f3f;  
      
    int arr[300010];  
    int tmp[300010];  
      
    void copyArr(int start, int end){  
        for(int i = start; i <= end; i++){  
            arr[i] = tmp[i];  
        }  
    }  
      
    ll mergeSort(int a[300010], int start, int end){  //返回归并排序过程中消去的逆序数对数
        int mid = (start + end) / 2;  
        if(start >= end)  return 0; //递归的终止条件  
        ll count = mergeSort(a, start, mid) + mergeSort(a, mid + 1, end);  
        //递归地排序两个子数组,并且统计两个子数组排序过程中消去的逆序数对数
      
        int lpt = start;  
        int rpt = mid + 1;  
        int tempIndex = start;  
        while(lpt <= mid and rpt <= end){  
            if(a[lpt] <= a[rpt]){  
                tmp[tempIndex++] = a[lpt];  
                count += rpt - mid - 1;  
                lpt++;  
            } else{  
                tmp[tempIndex++] = a[rpt];  
                rpt++;  
            }  
        }  
        for(int i = lpt; i <= mid; i++){  
            tmp[tempIndex++] = a[i];  
            count += rpt - mid - 1;  
        }  
        for(int i = rpt; i <= end; i++){  
            tmp[tempIndex++] = a[i];  
        }  
        copyArr(start, end);  
        return count;  
    }  
      
    int main(){  
        //ifstream infile("input.txt", ios::in);  
        //ofstream outfile("output.txt", ios::out);  
      
        int n;  
        cin >> n;  
      
      
        for(int i = 0; i < n; i++){  
            cin >> arr[i];  
        }  
      
        cout << mergeSort(arr, 0, n - 1) << endl;  
        //for(int i = 0; i < n; i++)  
        //    cout << arr[i] << ' ';  
      
        return 0;  
    }  

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

千里之码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值