11087 统计逆序对(递归(分治),交错)

11087 统计逆序对

时间限制:1000MS  内存限制:65535K
提交次数:0 通过次数:0

题型: 编程题   语言: C++;C;VC;JAVA

Description

设a[0…n-1]是一个包含n个数的数组,若在i<j的情况下,有a[i]>a[j],则称(i, j)为a数组的一个逆序对(inversion)。
比如 <2,3,8,6,1> 有5个逆序对。请采用类似“合并排序算法”的分治思路以O(nlogn)的效率来实现逆序对的统计。

一个n个元素序列的逆序对个数由三部分构成:
(1)它的左半部分逆序对的个数,(2)加上右半部分逆序对的个数,(3)再加上左半部分元素大于右半部分元素的数量。
其中前两部分(1)和(2)由递归来实现。要保证算法最后效率O(nlogn),第三部分(3)应该如何实现?

此题请勿采用O(n^2)的简单枚举算法来实现。

并思考如下问题:
(1)怎样的数组含有最多的逆序对?最多的又是多少个呢?
(2)插入排序的运行时间和数组中逆序对的个数有关系吗?什么关系?



输入格式

第一行:n,表示接下来要输入n个元素,n不超过10000。
第二行:n个元素序列。


输出格式

逆序对的个数。


输入样例

5
2 3 8 6 1


输出样例

5


提示

此题看着简单啊,但陷入此坑的同学不少……。题目初看简单,但仔细分析有点意思!


一、算法整体思路和框架

确定n个元素的逆序对数目在最坏情况O(nlogn)的算法,可以考虑仿归并排序中的分治算法:一个序列的逆序对个数由三部分构成:
(1)它的左半部分逆序对的个数,(2)加上右半部分逆序对的个数,(3)再加上左半部分元素大于右半部分元素的数量。
其中(1)和(2)由递归来实现,(3)计算后加入。

伪代码如下:
//count_inversion():计算逆序对数量
int count_inversion(int array[], int low, int high)
{
    int count = 0, middle;
    if(low < high)
    {
        middle = low + (high - low) / 2;
        count += count_inversion(array, low, middle);   //加入:对左段的逆序对的递归计数
        count += count_inversion(array, middle + 1, high);    //加入:对右段的逆序对的递归计数
        count += merge_inversion(array, low, middle, high);   //加入上面提到的第三种情形的计数
    }

    return count;
}


二、分治算法的效率分析,是否能达到题目所要求的O(nlogn)?

分析一下算法效率,题目要求算法效率为O(nlogn),假设T(n)为n个元素逆序对统计的分治算法时间。
则T(1)=1, T(n) = 2T(n/2) + O(?),要使得算法整个效率T(n)=O(nlogn),则上述第(3)步只能是O(n)。

那又怎么能在O(n)时间内算出“左半部分元素大于右半部分元素的数量”?
一般方法,左段任何一个元素都要和右段任何一个元素比较,然后得到第(3)步的逆序对个数,这需要O(n^2)啊,O(n)做不到的啊。但是,若左右段
元素都各自段内有序,那可以做到O(n),只需要逐个比较左右段段首元素即可。
这个过程和“合并排序算法”的归并的过程很类似,只是在归并的时候加入了计数的操作。

即左右段“队首元素”进行比较,
*  如果来自于左段的队首大于右段的队首,则记数加上左段元素个数(为什么是加上左段元素个数呢?因为左段段首是左段中最小的了,最小的
都比右段段首大,那左段所有元素都大于右段段首元素的,因此计数应该是加上左段中所有的元素个数),且同时记录下小的这个数(即右段段首);
*  否则记数不变,但依然记下小的这个数(即左段段首)。

这第(3)步做完,同时左段和右段也合并有序了,并将合并后有序数组覆盖原数组。也就是说第(3)步会改变数的顺序,在统计的同时也进行排序了。


三、疑问?

有人问,数的顺序都改变了,那原数序列和合并排序后的序列,去求逆序对个数还一样的吗?这样求解是对的吗?
 —————— 回答: 是一样的!
1, 首先你问的这个问题表述就不准确,其实并不是“合并排序后的序列去求逆序对个数”,而是在合并之前先让左右段成序,再统计第(3)步,而后再
合并排序成一个有序序列的。若全都排好了再统计逆序对个数,那当然是不一样的了。
2, 其次左右序列成序并不影响第(3)步(即统计左段元素比右段元素大的数量)。而在整个序列排序之前,第(3)步又已做完,序列再怎么改变此时
已经不影响逆序对统计数了,因为已经统计完了。
3, 第(3)步要O(n)完成,还就得左右段都成序了才行,若左右段不成序,就做不到O(n)完成第(3)步,那得两重循环O(n^2)才能做第(3)步了,进一步,
若第(3)步完成需O(n^2),那整个算法也无法做到O(nlogn)了。

到此,分析结束,你觉得这个分析是不是比较有意思?也就是说这个问题的求解是嵌在典型的合并排序算法之中的,不是嵌套而是交错进行。


四、具体实例分析

好吧,如果还不清楚,我们以题上的数据实例来分析吧。
序列:  2 3 8 6 1
分为: 2 3 8 | 6 1
对左段:(2 3 8) 计数count将增加0(此处省略若干递归的过程和文字);
对右段:(6 1) 计数count将增加(6)(1)的左段个数,即增加1,并且将右段调整变为:1 6
对左右段,此时序列为(2 3 8)(1 6),
    *  左段段首2大于右段段首1,所以左段所有元素都大于右段段首1,计数count增加左段段长,即增3,且同时记录下小的段首元素1,此时序列
为(2 3 8)(6);
    *  记下2,但计数值不增加,此时序列为(3 8)(6);
    *  记下3,但计数值不增加,此时序列为(8)(6);
    *  记下6,计数值count增加左段元素个数,这里即为1;
    *  记下8,但计数值不增加,此时序列为空了。
    *  记下的数形成新的序列:1 2 3 6 8,也就是排序之后的形式了。

返回计数值count:1+3+1,即返回5。


----------------------------------------------------------------------------------------------------
五、回答题目的问题

最后,回答本题提出的需要思考的问题:
(1)一个逆序的序列含有最多逆序对,最多为:1+2+...+(n-1)=n(n-1)/2。
   顺序的序列含有最少逆序对,数量为0。

(2)插入排序的运行时间和数组中逆序对个数有关,
   最好的情况,原序列已经成顺序,则插入排序的代价是O(n);
   最坏的情况,原序列为逆序,插入排序的代价为O(n^2);
   平均情况下也是O(n^2)的。
   每一个逆序对都将引起插入过程中的一次比较及后续数的挪位,因此说数组中逆序对个数越少,插入排序算法性能就越好。



回头一看,此题评论真的写的很长很长啊……,想做此题的,你只有耐着性子认真读吧,不理解多讨论讨论。


作者

zhengchan


我的实现代码

#include <iostream>
#include <cmath>
#include <string.h>

using namespace std;

int* arr;//结果有序数组
int* temp;//中间数组

// 有序合并为temp & 统计逆序对 & temp赋值回arr
int merge_inversion(int low, int middle, int high)
{
    int i = low, j = middle + 1, k = low;
    
    while ((i <= middle) && (j <= high)) {
        if (arr[i] <= arr[j]) {
            temp[k++] = arr[i++];
        }else{
            temp[k++] = arr[j++];
        }
    }
    
    int q = 0;
    if (i > middle) {
        for (q = j; q <= high; q++) {
            temp[k++] = arr[q];
        }
    }else{
        for (q = i; q <= high; q++) {
            temp[k++] = arr[q];
        }
    }
    //以上有序合并为temp数组
    
    
    //观察low,middle,high间的关系
    //cout << low << "+" << middle << "+" << high << "         " << arr[low] << "---" << arr[high] << endl;
  
    int count = 0;
    
    int left = low;// left,左段(还)需要比较的数的下标
    int right = middle + 1;// right,右段(还)需要比较的数的下标
    
    while (left <= middle && right <= high) {
        if (arr[left] > arr[right]) {// 左段首位 > 右段首位,即左段的最小都大于右段首位,则左段全都大于右段首位
            
            count += middle + 1 - left;// middle + 1 为右段首位
            //如2 3 8,1 6,其中1为首位
            
            right++;
        }else{
            
            left++;
        }
    }//统计逆序对
    
    
    // temp赋值回arr
    for (int t = low; t <= high; t++) {//别漏了=号
        
        arr[t] = temp[t];
    }
    
    return count;
}

// 计算逆序对数量
int count_inversion(int low, int high)
{
    int count = 0, middle;
    if(low < high)
    {
        middle = low + (high - low) / 2;
        count += count_inversion(low, middle);         //对左段的逆序对的递归计数
        count += count_inversion(middle + 1, high);    //对右段的逆序对的递归计数
        count += merge_inversion(low, middle, high);   //左段 > 右段的逆序对计数
    }
    
    if (count > 0) {//全部有序时会出现 count < 0 的情况
                //也可在count += middle + 1 - left;处判断
        return count;
    }
    
    return 0;
}
/*

5
2 3 8 6 1

5
1 2 3 4 5

10
5 1 4 7 3 2 8 9 0 6

*/
int main()
{
    int n;
    cin >> n;
    arr = new int[n];
    temp = new int[n];
    
    for (int i = 0 ; i < n; i++) {
        cin >> arr[i];
    }
    
    cout << count_inversion(0, n-1);
    
    cout << endl;
    return 0;
}


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值