ACM 逆序对(逆序数)总结
阅读原文时间:2021年04月20日阅读:1

最近做题遇到几次逆序数了,今天总结一下,以后遇到了再也不怕了。

首先说明一下什么是逆序数,下面是百度的定义

在一个排列中,如果一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么它们就称为一个逆序。一个排列中逆序的总数就称为这个排列的逆序数

如2431中,21,43,41,31是逆序,逆序数是4。

①如何求逆序数

方法如下:

考察每一位,判断从这一位往后有多少小于该位的,结果累加,得到最后结果。

例如,2,4,3,1     先判断2,之后有1是小于2的,结果+1,再判断4,之后3,1都小于4,结果+2,判断3时,结果再+1,最后得到结果4.

至于为什么,我也不知道,反正我感觉挺好理解的。

②如何用算法实现

当然,对于上述简单的过程,我们可以用两个for循环直接暴力,但是,这种方法一般都太low了,复杂度太高。

高效的方法有 : 归并排序、线段树、树状数组。

个人建议使用线段树求解,因为线段树在很多地方都有应用。

对于归并排序和树状数组,我就不解释了,因为归并排序一搞就忘记了而且用处不大,想了解的这里给出一篇博客

https://blog.csdn.net/acm_jl/article/details/51147010

例题是 HDU 1394,然后做完这个可以再写一下POJ 2299

HDU 1394的解析如下

https://www.cnblogs.com/qlky/p/5693747.html

然后我写的是POJ 2299

下面开始解析,当然,大家一定要有一定线段树的基础

算法的思想还是最基础的思想,考察每个点,判断它以后有多少个小于它的,然后累加。

与之前不同的地方在于,求小于某个数的个数时用到了线段树,所以非常快

这里的线段树表示某个范围内的值个数query 1--3,就表示值在1--3的个数

然后对于1--n个数,我们每个数都进行两步操作

查询1--a[i],单点更新a[i],值+1

例如对于 2 4 3 1 序列,我们从右向左判断,首先查询1-1,没有,值为0,然后1点值更新

再查询1--3,值为1,(实质表示的意思是比3小的有1个),然后更新3,

再查询4,更新4

查询2,更新2,得到最后的结果

下面是POJ 2299 的代码,要注意的是要离散化一下,因为值比较大

#include <iostream>
#include <cstdio>
#include<algorithm>
#include <cstring>
#include <stack>
#define fori(l,r) for( int i = l ; i <= r ; i++ )
#define forj(l,r) for( int j = 1 ; j <= r ; j++ )
#define lef rt<<1
#define rig rt<<1|1
#define mid (l+r)>>1
#define mem(a,val) memset(a,val,sizeof a)
typedef long long ll;
using namespace std;

struct spe
{
    int val,pos;
};
const int maxn = 5e5+5;
spe p[maxn];
int n;
int pos[maxn];
int tree[maxn<<2];
int L,R;
bool cmp( spe a,spe b )
{
    return a.val < b.val;
}
void change( int rt )
{
    tree[rt] = tree[lef]+tree[rig];
}
void update( int l,int r,int rt,int pos )       //在pos位置值加1
{
    if( l == r )
    {
        tree[rt]++;
        return;
    }
    int m = mid;
    if( m >= pos )
        update(l,m,lef,pos);
    else update(m+1,r,rig,pos);
    change(rt);
}
ll query( int l,int r,int rt )
{
    if( l >= L && r <= R )
        return tree[rt];
    int m = mid;
    ll ans = 0;
    if( m >= L )
        ans += query(l,m,lef);
    if( m < R )
        ans += query(m+1,r,rig);
    return ans;
}
int main()
{
    while( scanf("%d",&n) == 1 && n )
    {
        mem(tree,0);
        fori(1,n)
        {
            scanf("%d",&p[i].val);
            p[i].pos = i;
        }
        sort(p+1,p+1+n,cmp);
        int cnt = 1;
        fori(1,n)       //进行离散化
        {
            p[i].val = cnt++;
            pos[ p[i].pos ] = i;
        }
        ll ans = 0;
        for( int i = n ; i >= 1 ; i-- )
        {
            L = 1;
            R = p[ pos[i] ].val;
            ans += query(1,n,1);
            update(1,n,1,p[ pos[i] ].val );
        }
        printf("%lld\n",ans);
    }
    return 0;
}
/*
3
(([()])))

*/

其实上面都只是基础,真正的比赛不会出上面的题

如果想更深入的了解,可以看看这篇文章,虽然我是看的是有点晕

https://www.cnblogs.com/saltless/archive/2011/06/01/2065619.html

不过最后还是听我讲解吧

我觉得ACM中遇到逆序数最多的就是  全排列+逆序数

就是给你一个数,问它的全排列有多少个逆序数

例题忘记是在哪了,大家可以自己搜一下

由于它是一个全排列,所以就很有规律性

我们列举一下

n = 2

全排列:  1   2,  2   1             逆序数是0+1 = 1

n = 3

全排列: 1  2  3,   1   3   2,  2   1   3,  2  3  1 ,   3  1  2 ,  3  2  1     逆序数是0+1+1+2+2+3

当然,最好的是能够自己去找规律,这样记忆最深刻。

对于n = 3时,其中的每一种排列情况都是 n = 2中的一个变种

也就是说,求出了n,我们就可以求n+1,这是一个递推的过程

例如,1 2 3, 2  1  3,  3  1  2, 它们其实就是n=2 中1 2的变种,之所以把他们归为一类,是因为,如果把首位忽略,然后求后两位的逆序数,发现是相等的,然后他们只是分别在前面插入1  , 2 , 3 。

所以1 2 3中的 2  3,前面插入1,结果是不变的,  2  1   3中的1  3,插入2,结果+1,  3   1   2中的 1  2,插入3,结果+2;

请大家再仔细琢磨下,就能发现递推规律。

下面是我自己推的一个公式,如果大家有通项公式,可以互相交流下

其中q = !( i-1 ),q为i-1的阶乘

其实这是一个等差数列。

例如n = 2是,答案是1,n = 3时,答案就是1+3+5 = 9

n = 4时,答案就是 9+15+21+27=72

每一个dp[i]都是等差数列的和,有i项,首项是dp[i-1],公差为i-1的阶乘。

代码实现:

#include <iostream>
typedef long long ll;
using namespace std;
const ll mod = 1e9+7;
ll dp[104];
ll factorial[105];      //储存阶乘
const int maxn = 105;

ll fastpow( ll base,int x )    //快速幂
{
    ll ans = 1;
    while( x )
    {
        if( x&1 )
            ans = (ans*base)%mod;
        base = ( base*base )%mod;
        x = x>>1;
    }
    return ans;
}
void init()
{
    ll total = 1;
    for( int i = 1 ; i <= maxn ; i++ )
    {
        total = (total*i)%mod;
        factorial[i] = total;
    }
    dp[1] = 0;
    dp[2] = 1;
    ll up;
    ll ans = 0;
    for( int i = 3 ; i <= maxn ; i++ )
    {
        ll q = factorial[i-1];
        up = ( dp[i-1]*2+( ( i-1 )*q )%mod )%mod;
        up = (up*i)%mod;
        ans = ( up*fastpow(2,mod-2) )%mod;
        dp[i] = ans;
    }

}
int main()
{
    init();
    int x;
    while( cin>>x )
    {
        cout<<dp[x]<<endl;
    }
    return 0;
}
/*


*/

下面是一道更难一点的题,是计蒜客上面的

JSZKC is the captain of the lala team.

There are NN girls in the lala team. And their height is [1,N][1,N] and distinct. So it means there are no two girls with a same height.

JSZKC has to arrange them as an array from left to right and let h_ihi​ be the height of the i^{th}ith girl counting from the left. After that, he can calculate the sum of the inversion pairs. A inversion pair counts if h_i>h_jhi​>hj​ with i<ji<j.

Now JSZKC wants to know how many different arranging plans with the sum of the inversion pairs equaling KK. Two plans are considered different if and only if there exists an ii with h_ihi​ different in these two plans.

As the answer may be very large, you should output the answer mod 10000000071000000007.

Input Format

The input file contains several test cases, each of them as described below.

  • The first line of the input contains two integers NN and KK (1 \le N \le 5000,0 \le K \le 5000)(1≤N≤5000,0≤K≤5000), giving the number of girls and the pairs that JSZKC asked.

There are no more than 50005000 test cases.

Output Format

An integer in one line for each test case, which is the number of the plans mod 10000000071000000007.

样例输入

3 2
3 3

样例输出

2
1

题目来源

The 2018 ACM-ICPC China JiangSu Provincial Programming Contest

这个题很坑,不能开二维数组,只能先把答案储存下来,然后用滚动数组(当然我不太会用,用两个数组复制的)

很重要的规律就是递推,有一个递推公式

下面的是官方的递推公式

递推公式:用N(n,k)表示1~n排列中逆序数为k者的个数,则有
N(n,k+1)=N(n,k)+N(n-1,k+1)-N(n-1,k+1-n)

我推出来的有些不一样

我用pre记录前缀和,dp[i][j]记录i的全排列时,逆序数为j的个数

dp[i][j] = pre[i-1][j-1]+dp[i-1][j]

当然,由于不能开二维数组,代码看起来比较难懂。

AC代码:

#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<list>
#include<map>
#define fori(l,r) for( int i = l ; i <= r ; i++ )
#define forj(l,r) for( int j = 1 ; j <= r ; j++ )
#define mem(a,val) memset(a,val,sizeof a)
#define inf 0x3f3f3f3f
using namespace std;
int n,m;
typedef long long ll;
const int maxn = 5004;
const ll mod =  1000000007;
ll dp[maxn];
ll pre[maxn],cur[maxn];    //两个前缀和数组
ll ans[maxn];
struct spe
{
    int first,second;
};
int cnt = 1;
spe query[maxn];
void init()
{
    mem(dp,0);
    mem(pre,0);
    mem(cur,0);

    fori(2,5000)
    {
        int goal = i*(i-1)>>1;
        int nextgoal = i*(i+1)>>1;
        goal = min(goal,maxn);
        nextgoal = min(nextgoal,maxn);

        pre[0] = dp[0] = cur[0] = 1;
        forj(1,goal)
        {
            dp[j] = ( pre[j-1]+dp[j] )%mod;
            if( j >= i )
                dp[j] = ( dp[j]-pre[j-i]+mod )%mod;
            cur[j] = ( cur[j-1]+dp[j] )%mod;      //前缀和累加
        }
        forj(1,cnt-1)       //如果更新的当前行有包含答案
            if( query[j].first == i )
            {
                if( query[j].second <= goal )
                    ans[j] = dp[ query[j].second ];
                else ans[j] = 0;
            }
        forj( 1,nextgoal )
        {
            if( j > goal )
                pre[j] = cur[goal];
            else pre[j] = cur[j];
        }
    }
}
int main()
{
    mem(ans,0);
    while( scanf("%d %d",&query[cnt].first,&query[cnt].second) == 2 && query[cnt].first )
    {
        if( query[cnt].first == 1 )
        {
            if( query[cnt].second == 0 )
                ans[cnt] = 1;
            else ans[cnt] = 0;
        }
        cnt++;
    }
    init();
    fori(1,cnt-1)
        printf("%lld\n",ans[i]);
    return 0;
}
/*

1 1
1 2
2 1
2 2
3 1
3 2
3 3
3 4
3 5
3 6
4 1
4 2
4 3
4 4
6 5
6 6

*/

下面是标程代码:

#include <map>
#include <cmath>
#include <cstdio>
#include <ctime>
#include <string>
#include <vector>
#include <cstring>
#include <cstdlib>
#include <utility>
#include <iostream>
#include <algorithm>
#define LL long long
#define pi 3.1415926535897932384626433
#define sqr(a) ((a)*(a))

using namespace std;

typedef pair<int,int> PII;
typedef pair<PII,int> PIII;
const int N=5002,P=1000000007;
int f[2][N];
int ans[N];
vector<PIII> d;

void data_maker()
{
    srand(time(0));
    freopen("B.in", "w", stdout);
    printf("1 0\n1 1\n1 5000\n5000 5000\n");
    for (int Case=1;Case<=4996;Case++)
    {
        printf("%d %d\n",rand()%5000+1,rand()%5001);
    }
    fclose(stdout);
}
int main()
{
    //data_maker();
    //freopen("B.in", "r", stdin);
    //freopen("B.out", "w", stdout);

    int i,j,k,x,y,Case=0,cur=0;
    while (scanf("%d%d",&x,&y)!=EOF) d.push_back({{x,y},++Case});
    sort(d.begin(),d.end());
    f[1][0]=1;f[1][1]=-1;k=1;
    for (i=1;i<=5000;i++,k^=1)
    {
        for (j=0;j<=5000;j++)
        {
            if (j>0) f[k][j]=(f[k][j]+f[k][j-1])%P;
            f[k^1][j]=(f[k^1][j]+f[k][j])%P;
            if (i+j+1<=5000)
                f[k^1][i+j+1]=((f[k^1][i+j+1]-f[k][j])%P+P)%P;
        }
        while (cur!=Case&&i==d[cur].first.first)
            ans[d[cur].second]=f[k][d[cur].first.second],++cur;
        memset(f[k],0,sizeof(f[k]));
    }

    for (i=1;i<=Case;i++) printf("%d\n",ans[i]);

    return 0;
}

最后给自己打个广告吧,自己做了一个网站,大家可以访问访问

 https://www.bowenyang666.com