ACM 逆序对(逆序数)总结

匿名 (未验证) 提交于 2019-12-03 00:43:02

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

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

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

如2431中,21,43,41,31是逆序,逆序数是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

不过最后还是听我讲解吧

全排列+逆序数

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

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

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

我们列举一下

n = 2

n = 3

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

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

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

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

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

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

其实这是一个等差数列。

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.

Input Format

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

Output Format

样例输入

 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; }

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!