【逆序对问题】车厢重组问题求解及拓展

1.问题重述

桥的长度最多能容纳两节车厢,可以把相邻两节车厢的位置交换,用这种方法可以重新排列车厢的顺序。用这座桥将进站的车厢按车厢号从小到大排列。编一个程序,输入初始的车厢顺序,计算最少用多少步就能将车厢排序

2.分解

1n的某一个全排列的一个数列 \left(n\le10000\right)→输入

使用给定操作使数列升序的最少次数→输出

只能使用特定操作对序列进行排序→约束

最少次数→最优解

题目输入是从1到n的某一个全排列的一个数列 \left(n\le10000\right),那么可以知道输入的数ai范围在[1,n]内,且每个数字最多出现一次。

题目约束是使用特定操作(每次交换相邻数字)对序列进行排序,然后求该操作的最少次数。因此我们需要思考并找出排序过程中触发该操作的必要条件,以此保证次数最少。

3.模式识别

量化描述:

已知从1n的某一个全排列,你可以多次将排列中第 i 项和第 i+1 项 (1≤i<n) 互换。至少需要多少次互换可以使该数列变为升序?

模式特征:

n 个整数的表示与存储】→【数组/链表/字符串】

【特定方法排序】→【模拟操作/抽象成数学问题】

4.抽象

将n个整数进行表示与存储,可以采用数组、链表等数据结构;由于数组具有随机存储的特性,相较链表迭代更优,若无特殊情况可以优先考虑使用数组进行存储。

题目中给定操作为每次可以互换两个数的位置,需要用最少次数使数组为升序。那么我们按照题意操作,首先比较相邻的元素,如果第一个比第二个大,就交换它们两个;接下来对每一对相邻数字做同样的工作,从开始第一对到结尾的最后一对,这样之后,最后的数字应该是最大的。然后我们对其它数字重复上述操作,直到数列全部有序为止。这种方法就是冒泡排序算法。

由上述可知,该问题可以直接用冒泡排序算法对给定数列进行排序,然后统计排序中进行交换的次序。

另外我们发现冒泡排序的必要交换次数等于该数列的总逆序对数,因此也可以将该问题抽象成求逆序数的问题。

(定义:如果有i < j,且a_i>a_j,则称<a_i,a_j>为数组A中的一个逆序对。关于两者为什么相等,简单解释为:每次需要相邻交换的一对数字必定是一个逆序对,因此交换完成后数组中的逆序对的数量会减少一个。思路更完整的证明可以参考[李均成. 冒泡排序的对换次数与排列逆序数相等的证明[J]. 数学学习与研究, 2019(06):140-140])

5.算法设计

可以预估大部分计算机的单秒运算量在10^7次到10^9次之间。本题输入数据规模达到10^4,因此能够支持不高于T(n^2/2)的运行次数,勉强能够支持O(n^2)的运行次数。

如果使用模拟方式实现,则使用冒泡排序算法即可,其O(n^2)的时间复杂度足以通过本题。 

由于只询问需要的交换次数而不要求交换结果,因此本题实际上不需要真正的冒泡排序,可以在其基础上省去实际交换操作,仅统计能够触发交换操作的次数。

如果将其转化为求逆序对问题,仍然可以使用冒泡排序进行求解,也可以采用归并排序算法、树状数组或线段树+离散化辅助统计算法等进行求解。

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.推广应用

关于求解逆序对:如果数据量进一步增大(n\le5\times{10}^5),具有O(n^2)时间复杂度的冒泡排序将无法在短时间内计算出逆序对数,此时应该考虑使用具有O(nlogn)时间复杂度的归并排序或者树状数组等算法来求解逆序对。下面列出常见的求解算法及其时空复杂度。

算法/数据结构

时间复杂度

空间复杂度

冒泡排序

O(n^2)

O(1)

归并排序

O(nlogn)

O(n)

树状数组

O(nlogn)

O(maxElement)

树状数组(离散化)

O(nlogn)

O(n)

自平衡二叉搜索树(AVL)

O(nlogn)

O(n)

字典树(二进制形式存储)

O(nlogn)

O(nlogn)

参考题目:P1908 逆序对 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

关于逆序对其它性质:逆序对数不仅是排序过程中必需的最少交换次数,也是每个元素在已排序位置之上的距离总和,以及为排好序列而从序列中可以删除元素的最小数量。它常用于量度排列或者序列的已排序程度。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值