[分治] 求排列的逆序数

【题目描述】

在Internet上的搜索引擎经常需要对信息进行比较,比如可以通过某个人对一些事物的排名来估计他(或她)对各种不同信息的兴趣,从而实现个性化的服务。

对于不同的排名结果可以用逆序来评价它们之间的差异。考虑1,2,…,n的排列i1,i2,…,in,如果其中存在j,k,满足 j < k 且 ij > ik, 那么就称(ij,ik)是这个排列的一个逆序。

一个排列含有逆序的个数称为这个排列的逆序数。例如排列 263451 含有8个逆序(2,1),(6,3),(6,4),(6,5),(6,1),(3,1),(4,1),(5,1),因此该排列的逆序数就是8。显然,由1,2,…,n 构成的所有n!个排列中,最小的逆序数是0,对应的排列就是1,2,…,n;最大的逆序数是n(n-1)/2,对应

的排列就是n,(n-1),…,2,1。逆序数越大的排列与原始排列的差异度就越大。

现给定1,2,…,n的一个排列,求它的逆序数。

输入:第一行是一个整数n,表示该排列有n个数(n <= 100000)。第二行是n个不同的正整数,之间以空格隔开,表示该排列。

输出:该排列的逆序数。

【题目提示】

1. 利用二分归并排序算法(分治);
2. 注意结果可能超过int的范围,需要用long long存储。



【题目分析】

很多人要问了,到底啥是逆序?

简单来说:

现有一列数未经过排序:263451。我们可以观察到:2比1要大,可是1却在队尾。所以(2,1)就是一个逆序对,那我们就可以得出如题目所示的(2,1),(6,3),(6,4),(6,5),(6,1),(3,1),(4,1),(5,1)这些逆序对。共8个,就输出了8。

我一开始的思路:暴力出奇迹。

#include<iostream>
using namespace std;
int a[100001];
int main()
{
    ios::sync_with_stdio(false);
    int i,n,j,ans=0;
    cin>>n;
    for(i=1;i<=n;++i)
     cin>>a[i];
    for(i=1;i<=n;i++)
    {
        for(j=i+1;j<=n;j++)
        {
            if(a[i]>a[j]&&i<j)
             ans++;
        }
    }
    cout<<ans;
}

直接写了如上代码,可是Time Limit Exceeded(6分)。我的妈呀这红红的几个单词直接就打破了我的自尊(开玩笑)

回头看了一眼提示,要用归并排序...都没听过这种排序...(微笑中透露着***)

经过老师的讲解,我倒是明白了归并是啥东西:

归并排序算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并
归并过程为:比较a[i]和b[j]的大小,若a[i]≤b[j],则将第一个有序表中的元素a[i]复制到r[k]中,并令i和k分别加上1;否则将第二个有序表中的元素b[j]复制到r[k]中,并令j和k分别加上1,如此循环下去,直到其中一个有序表取完,然后再将另一个有序表中剩余的元素复制到r中从下标k到下标t的单元。归并排序的算法我们通常用递归实现,先把待排序区间[s,t]以中点二分,接着把左边子区间排序,再把右边子区间排序,最后把左区间和右区间用一次归并操作合并成有序的区间[s,t]。

(这**不就是二分升级为了二分排序吗...)

(附上归并模板)

//归并排序 
inline void merge(int left,int mid,int right)
{
    int i=left,j=mid+1,k=left;
    int b[n];
    while(i<=mid&&j<=right)
    {
        if(a[i]<=a[j])b[k++]=a[i++];
        else b[k++]=a[j++];
    }
    while(i<=mid)b[k++]=a[i++];
    while(j<=right)b[k++]=a[j++];
    for(i=1;i<=n;i++)
     a[i]=b[i];
}
void mergesort(int a[],int left,int right)
{
    if(left>right)return;
    int mid=(left+right)/2;
    mergesort(a,left,mid);//分治 
    mergesort(a,mid+1,right);
    merge(a,left,mid,right);
}

为什么这种方法就比暴力快呢?

在你将两个有序队列归并为一个有序队列的进行过程中,你就可以进行答案的统计了。

(AC代码)

#include<stdio.h>
long long ans,a[100001],b[100001],n;
void merge(int left,int mid,int right)
{
    int i=left,j=mid+1,k=left;
    while(i<=mid&&j<=right)
    {
        if(a[i]<a[j])b[k++]=a[i++];
        else if(a[i]>a[j])
        {
            ans+=j-k;
            b[k++]=a[j++];
        }
        else b[k++]=a[i++];
    }
    while(i<=mid)b[k++]=a[i++];
    while(j<=right)b[k++]=a[j++];
    for(i=left;i<=right;i++)
     a[i]=b[i];
}
void mergesort(int left,int right)
{
    if(left>right)return;
    int mid=(left+right)/2;
    if(left<right)
    {
        mergesort(left,mid);//分治 
        mergesort(mid+1,right);
        merge(left,mid,right);
    }
    return ;
}
int main()
{
    scanf("%lld",&n);
    for(int i=1;i<=n;i++)
     scanf("%lld",&a[i]);
    mergesort(1,n);
    printf("%lld",ans);
    return 0;
}

通过如上代码,不难看出,边归并边排序的过程才是最节约时间的。

超时代码的时间复杂度为O(n²),而AC代码的时间复杂度为O(nlogn)

 

来一张图看看:

设有一队列456 113 342 9679 123 31 315

我要用归并排序来排序这个队列。

首先将此队列用二分法分成两个队列,再排序,得到两个有序队列:

1.   113 342 456

2.   31 123 315 9679(这四个数在之前无序队列里的排序肯定比上一个队列的排序大)

模拟一下:首先创建一个空队列,用来存储排好序的队列。31和113比,31小,先入队,ans不加(不满足逆序条件)。123和113比,113入队,但在这之前,31已经先入队,且31的原序数比113大,所以ans++。123和342比,123先入队,ans不加(不满足逆序条件)。315和342比,315先入队,ans不加(不满足逆序条件)。342和9679比,342先入队,但在342入队之前,123 315 31均已先入队,且在原无序队列里的序数比342大,数字比342小,故ans+=3,456同理。

故求出这个无序队列的逆序数:7。

再看这道题,是不是更清晰明了了呢?

posted @ 2017-08-07 01:22  jiandanset  阅读(916)  评论(0编辑  收藏  举报