1.问题重述
桥的长度最多能容纳两节车厢,可以把相邻两节车厢的位置交换,用这种方法可以重新排列车厢的顺序。用这座桥将进站的车厢按车厢号从小到大排列。编一个程序,输入初始的车厢顺序,计算最少用多少步就能将车厢排序。
2.分解
从1到n的某一个全排列的一个数列 →输入
使用给定操作使数列升序的最少次数→输出
只能使用特定操作对序列进行排序→约束
最少次数→最优解
题目输入是从1到n的某一个全排列的一个数列 ,那么可以知道输入的数ai范围在[1,n]内,且每个数字最多出现一次。
题目约束是使用特定操作(每次交换相邻数字)对序列进行排序,然后求该操作的最少次数。因此我们需要思考并找出排序过程中触发该操作的必要条件,以此保证次数最少。
3.模式识别
量化描述:
已知从1到n的某一个全排列,你可以多次将排列中第 i 项和第 i+1 项 (1≤i<n) 互换。至少需要多少次互换可以使该数列变为升序?
模式特征:
【n 个整数的表示与存储】→【数组/链表/字符串】
【特定方法排序】→【模拟操作/抽象成数学问题】
4.抽象
将n个整数进行表示与存储,可以采用数组、链表等数据结构;由于数组具有随机存储的特性,相较链表迭代更优,若无特殊情况可以优先考虑使用数组进行存储。
题目中给定操作为每次可以互换两个数的位置,需要用最少次数使数组为升序。那么我们按照题意操作,首先比较相邻的元素,如果第一个比第二个大,就交换它们两个;接下来对每一对相邻数字做同样的工作,从开始第一对到结尾的最后一对,这样之后,最后的数字应该是最大的。然后我们对其它数字重复上述操作,直到数列全部有序为止。这种方法就是冒泡排序算法。
由上述可知,该问题可以直接用冒泡排序算法对给定数列进行排序,然后统计排序中进行交换的次序。
另外我们发现冒泡排序的必要交换次数等于该数列的总逆序对数,因此也可以将该问题抽象成求逆序数的问题。
(定义:如果有i < j,且>,则称为数组A中的一个逆序对。关于两者为什么相等,简单解释为:每次需要相邻交换的一对数字必定是一个逆序对,因此交换完成后数组中的逆序对的数量会减少一个。思路更完整的证明可以参考[李均成. 冒泡排序的对换次数与排列逆序数相等的证明[J]. 数学学习与研究, 2019(06):140-140])
5.算法设计
可以预估大部分计算机的单秒运算量在次到次之间。本题输入数据规模达到,因此能够支持不高于的运行次数,勉强能够支持的运行次数。
如果使用模拟方式实现,则使用冒泡排序算法即可,其的时间复杂度足以通过本题。
由于只询问需要的交换次数而不要求交换结果,因此本题实际上不需要真正的冒泡排序,可以在其基础上省去实际交换操作,仅统计能够触发交换操作的次数。
如果将其转化为求逆序对问题,仍然可以使用冒泡排序进行求解,也可以采用归并排序算法、树状数组或线段树+离散化辅助统计算法等进行求解。
6.算法实现
实现一:
该解决方案采用朴素模拟思想实现。
#include<cstdio>
int main(){
static int a[10005],n,i,j;
scanf("%d",&n);
for(i=1;i<=n;i++) scanf("%d",&a[i]);
int cnt=0,t;
for(i=1;i<=n;i++){
for(j=i+1;j<=n;j++){
if(a[i]>a[j]){//满足交换条件
t=a[i];
a[i]=a[j];
a[j]=t;
cnt++;
}
}
}
printf("%d",cnt);
return 0;
}
实现二:
该解决方案采用树状数组实现,由于题目数据的特殊性,故没有使用离散化。
#include <array>
#include <cstdio>
#include <climits>
#include <iostream>
#include <algorithm>
const int N_MAX = 10005;
int n,m;
template<typename _Tp,size_t Maxn>
class binary_indexed_tree {
typedef _Tp value_type;
private:
value_type a[Maxn];
value_type b[Maxn];
int lowbit(int x) {
return x & -x;
}
void add(int x, value_type val);
value_type sum(int x);
public:
void update(int l, int r, value_type val) {
add(l, val);
add(r + 1, -val);
}
value_type query(int l, int r) {
return sum(r) - sum(l - 1);
}
};
template<typename _Tp,size_t Maxn>
void binary_indexed_tree<_Tp,Maxn>::add(int x, value_type val) {
for (register int i = x; i <= Maxn; i += lowbit(i)) a[i] += val, b[i] += val * x;
}
template<typename _Tp,size_t Maxn>
_Tp binary_indexed_tree<_Tp,Maxn>::sum(int x) {
value_type ans = 0;
for (register int i = x; i; i -= lowbit(i)) ans += (x + 1)*a[i] - b[i];
return ans;
}
binary_indexed_tree<int,N_MAX>tree;//树状数组用来维护每个数字出现的次数
int a[N_MAX];
int main()
{
std::cin>>n;
for(int i=1;i<=n;i++){
std::cin>>a[i];
}
long long ans=0;
for(int i=1;i<=n;i++){//动态维护[1,i-1]区间内每个数的出现次数
tree.update(a[i],a[i],1);//将区间拓展成[1,i]
ans+=tree.query(a[i]+1,n);
/*查询[1,i]区间内
a[i]+1到n的数字的出现次数,也就是查询比a[i]
大的数字的出现次数*/
}
std::cout<<ans;
return 0;
}
7.推广应用
关于求解逆序对:如果数据量进一步增大,具有时间复杂度的冒泡排序将无法在短时间内计算出逆序对数,此时应该考虑使用具有时间复杂度的归并排序或者树状数组等算法来求解逆序对。下面列出常见的求解算法及其时空复杂度。
算法/数据结构 | 时间复杂度 | 空间复杂度 |
冒泡排序 | ||
归并排序 | ||
树状数组 | ||
树状数组(离散化) | ||
自平衡二叉搜索树(AVL) | ||
字典树(二进制形式存储) |
参考题目:P1908 逆序对 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
关于逆序对其它性质:逆序对数不仅是排序过程中必需的最少交换次数,也是每个元素在已排序位置之上的距离总和,以及为排好序列而从序列中可以删除元素的最小数量。它常用于量度排列或者序列的已排序程度。