文章目录
⭐感谢您能点击进来,这是对我莫大的鼓励⭐
1. 题目
2. 分析思路
2.1 审题和分析
看完题目,我们可以总结出以下重要的几点:
- 左移实际上是循环左移
- 序列有n个元素,而这n个元素恰好是 0到 n − 1 n-1 n−1的排列
- 最关键的是求解一个序列P的逆序数对个数
2.2 什么是逆序对个数
逆序对个数即逆序数。在一个排列中,如果一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么它们就称为一个逆序对。一个排列中逆序对的总数就称为这个排列的逆序数。
说白了,逆序数就是跟标准序列(从小到大)相反序数的总和。如2431中,21,43,41,31是逆序,逆序数就是4。
2.2 思路
逆序数求法1——一个一个数
中学的时候我们计算逆序数,最简单也是最容易想到的方法就是,对于数列中的每一个数 a [ i ] a[i] a[i],遍历数列中的数 a [ j ] a[j] a[j] (其中 j<i),若 a [ i ] < a [ j ] a[i]<a[j] a[i]<a[j],则逆序数加1,对于每一个 a [ i ] a[i] a[i],看它的前面有几个数是比它大的,以此统计出来逆序数。可以看出这种求法有待优化,时间复杂度比较高
逆序数求法2——归并思想
使用归并排序求逆序数
归并排序是将数组 A[left , right] 分成两半 A[left , mid] 和 A[mid+1 , right]分别进行归并排序,然后再将这两半合并起来。
在合并的过程中(设left<=i<=mid,mid+1<=j<=right),当a[i]<=a[j]时,并不产生逆序数;当a[i]>a[j]时,在前半部分中比a[i]大的数都比a[j]大,将a[j]放在a[i]前面的话,逆序数要加上 mid + 1 - i 。因此,可以在归并排序中的合并过程中计算逆序数
归并排序时,每一次后方元素移到前面来的移动距离就是本次操作/本次递归函数的逆序数。这样累加每次递归过程的逆序数,在完成整个递归过程之后,最后的累加和就是逆序的总数
//===========归并排序=============
#include<iostream>
using namespace std;
int count=0;//计算逆序数
void Merge(int* A,int left,int mid,int right,int* tmp)
{ //Merge可以将两个有序的数组排好序,时间复杂度:o(n)
int i=left;//左边部分索引
int j=mid+1;//右边部分索引
int k=left;//tmp数组索引
while(i <= mid && j <= right)
{
if(A[i] <= A[j])tmp[k++]=A[i++];//左边比右边小
else{//右边比左边小
tmp[k++] = A[j++];
count += mid-i+1;//只加这一句就能求逆序数了
}
}
while(i <= mid)tmp[k++] = A[i++];//处理剩下的
while(j <= right)tmp[k++] = A[j++];//处理剩下的
//tmp[]已经有序,将tmp[]中数据复制回原数组A[]
for(int i=left;i <= right;++i)A[i] = tmp[i];
}
void MergeSort(int* A,int left,int right,int* tmp)//假定MergeSort能将一个乱序数组A排好序.
{
if(left < right)//数组被分割到只剩一个元素时,不会进入这个循环,函数直接返回
{
int mid=(left+right)/2;
MergeSort(A,left,mid,tmp);//排好一个左边的数组1
MergeSort(A,mid+1,right,tmp);//排好一个右边的数组2
Merge(A,left,mid,right,tmp); //合并两个有序的数组
}
}
void main()
{
int A[]={2,1,3,6,4,0,11,3,5};
int len=sizeof(A)/sizeof(A[0]);
int *tmp=new int[len];
MergeSort(A,0,len-1,tmp);
cout<<"排序完的数组A:";
for(int i=0;i<len;++i)cout<<A[i]<<' ';
cout<<endl;
cout<<"逆序数:"<<count<<endl;
delete[] tmp;
}
上面的代码实现了归并排序且求逆序数,把其应用到题目中来。
循环左移对逆序数的改变
左移第一次,把 a [ 0 ] a[0] a[0]放到末尾,其余部分往前一个位置,得到新的序列b。再次提醒一下题目中很重要的一点:序列有n个元素,而这n个元素恰好是 0到 n − 1 n-1 n−1的排列,所以其实序列的数字本身就隐含着自己该往前移动的距离
举个例子:a = [2 , 3 , 1 , 0],左移一次得到的新序列b = [3 , 1 , 0 , 2],绿色部分之间的相对位置不改变,所以a,b序列逆序数的变化我们只需要考虑这个被左移的2。
- 本来有两个比2小的数(0和1)该被移到2前面,但现在不用了,它们只需要移动紧靠2后面的位置,整体少移了2个距离;而在最后面的2又需要往前移动一定距离,由于是0到 n − 1 n-1 n−1的排列,所以整体多移了 ( n − 1 ) − 2 (n-1)-2 (n−1)−2的距离,所以从序列a到序列b,逆序数的变化就是 [ ( n − 1 ) − 2 ] − 2 = − 1 [(n-1)-2]-2 = -1 [(n−1)−2]−2=−1 ,左移一次使上面的序列a的逆序数少了1。
- 左移一次的逆序数 = 左移前的逆序数 + ( 整体多移的距离 ) − ( 整体少移的距离 ) +(整体多移的距离)-(整体少移的距离) +(整体多移的距离)−(整体少移的距离)
for (i = 0; i < n - 1; i++)
{
ans += (n - 1 - arrcp[i]) - arrcp[i];
cout << ans << endl;
}
3. 代码实现
3.1 C++代码实现
#include<iostream>
using namespace std;
long mergesort(int left, int right, int* arr, int* temp) {
if (left >= right)return 0;
int mid = (left + right) / 2;
long ans = mergesort(left, mid, arr, temp) + mergesort(mid + 1, right, arr, temp);
int i = left, j = mid + 1;//左边部分索引,右边部分索引
int k = 0;//tmp数组索引
while (i <= mid && j <= right) {
if (arr[i] <= arr[j])temp[k++] = arr[i++];//不产生逆序数
else {//产生逆序数,是arr[j]放到arr[i]前面所需要移动的距离
ans += mid - i + 1;
temp[k++] = arr[j++];
}
}
while (i <= mid)temp[k++] = arr[i++];
while (j <= right)temp[k++] = arr[j++];
//temp[]已经有序,将temp[]中数据复制回原数组arr[]
for (i = left, j = 0; i <= right; i++, j++) {
arr[i] = temp[j];
}
return ans;
}
int main()
{
int n, i, j;
long ans;
cin >> n;
int* arr = new int[n], * arrcp = new int[n], * temp = new int[n];
for (i = 0; i < n; i++) {
cin >> arr[i];
arrcp[i] = arr[i];
}
ans = mergesort(0, n - 1, arr, temp);
//此时arr已经排好序,arrcp是排序前的
cout << ans << endl;
for (i = 0; i < n - 1; i++) {
ans += (n - 1 - arrcp[i]) - arrcp[i];
cout << ans << endl;
}
delete[] temp;
delete[] arr;
delete[] arrcp;
return 0;
}
3.2 总结
这道题的关键点在于
- 利用归并排序求解逆序数,只需要在归并排序的基础上再加一句话
- 一定注意所给的序列的特殊性,它是一个0到n-1的排列,隐含着元素该移动的距离
⭐感谢您能看到这里,我会好好努力继续分享好的文章的!⭐