写在前面:
本系列博客仅作为本人十一假期过于无聊的产物,对小学期的程序设计作业进行一个总结式的回顾,如果将来有BIT的学弟学妹们在百度搜思路时翻到了这一条博客,也希望它能对你产生一点帮助(当然,依经验来看,每年的题目也会有些许的不同,所以不能保证每一题都覆盖到,还请见谅)。
不过本人由于学艺不精,代码定有许多不足之处,欢迎各位一同来探讨。
同时请未来浏览这条博客的学弟学妹们注意,对于我给出完整代码的这些题,仅作帮助大家理解思路所用(当然,因为懒,所以大部分题我都只给一个伪代码)。Anyway,请勿直接复制黏贴代码,小学期的作业也是要查重的,一旦被查到代码重复会严厉扣分,最好的方法是浏览一遍代码并且掌握相关的要领后自己手打一遍,同时也要做好总结和回顾的工作,这样才能高效地提升自己的代码水平。
加油!
指路类似题:力扣O51
成绩 | 10 | 开启时间 | 2021年08月31日 星期二 12:30 |
折扣 | 0.8 | 折扣时间 | 2021年09月5日 星期日 23:00 |
允许迟交 | 否 | 关闭时间 | 2021年10月10日 星期日 23:00 |
Description
小张在暑假时间来到工地搬砖挣钱。包工头交给他一项艰巨的任务,将一排砖头按照从低到高的顺序排好。可是小张的力量有限,每次只能交换相邻的两块砖头,请问他最少交换几次能够完成任务?
Input
第一行一个整数,表示砖头数量。
第二行个整数,表示砖头的高度。
Output
一个整数,表示最少交换几次能够完成任务。
测试用例 1 | 以文本方式显示
| 以文本方式显示
| 1秒 | 64M |
题意分析:
想用冒泡排序是吧?想得美!哪有这么简单的题!
这题当然,最容易想到的方法就是冒泡排序,因为整个流程和冒泡排序的流程完全一致,并且贪心的来看,冒牌排序的总的步骤数一定最少(同样,这里的贪心策略请读者试着自己证明一下,我会放在这一段的后方)。然而3e5的数据规模,实在是支撑不起我们这个的算法。
这时候我们需要透过现象看本质,冒泡排序的背后究竟蕴含着什么?如果你认真证明了一遍贪心策略,你会发现,每一个元素要想“跨过山河大海”回到它们的家,它们就需要跨过那些在它们右侧且比他们小的元素,而那些比它们大的元素,肯定在它回家之前就已经回到家了,自然不会发生交换。而它的右侧比它小的每一个元素都与它形成了一个逆序数对,所以,真正的本质就是——逆序数对。我们总共的操作数,就是整个序列的逆序数对的数量。
那么,逆序数对又要如何来求?冒泡排序已经被我们毙了,剩下一个和逆序数对有点关联的就是归并排序,为什么这么说呢?我们就要细看归并排序的过程。
归并排序的最主要操作,就是把一个数组分成前后两截,先分别让前后两截已序,再利用双指针将前后两截合并为一个新的已序数组?至于前后两截怎么已序?那这就是归并排序用到的递归思想了,递归重点很显然就是该数组只有一个元素的时候。说回正题,这到底和逆序数对有什么关联呢?我们研究归并排序的主体部分,假设已经有前后各半段已序数组:
1 | 5 | 7 | 9 | 13 | 2 | 4 | 8 | 9 | 11 |
我们用双指针法,将两截归并的过程如下:
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的,换句话说,我们就能知道原始的逆序数对究竟有多少。
好消息是,归并排序的时间复杂度是,且十分稳定,这在我们这题的数据规模之下实在是再好不过了,就他了!
贪心策略的证明:
我们在冒泡排序的过程中,是做轮,第轮从第1个开始遍历到第个,但凡遍历到的元素比后面的元素大,二者交换位置,这样第i轮就能确定整个序列中第大的元素。如果不按这个策略,会有更少步骤的做法吗?我们如果随意地交换中间两个数,假如左侧的元素小于右侧的元素,那这一步操作就显得非常糟糕——一定至少还要再换一次,来让重新回到的左边,因为最终的结果一定是在的左边;假如 与 相等,那这个操作就完全没有意义,也非常糟糕; 假如 >,那么这样并不会比冒泡排序更好——因为在冒泡排序的过程中,肯定也会有这么一次交换,当所有比大的数都到最右边之后,这时候必然会发生与的交换,所以这件事情是早晚会发生的,我冒泡排序只是让它发生得更有条理一点。综上所述,随机的交换总是不会比冒泡排序更好。说人话,就是冒泡排序没有任何的“无用操作”乃至“负作用操作”。
贴代码:
因为归并排序的思路大家应该很熟悉了,所以不过多讲解了,就是在中间额外追踪一个变量而已(记得开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;
}