题意:
给出n个数(0~n-1,每个数仅出现一次),问它长为n的循环序列中逆序对最少的数量。
多种解法:暴力+树状数组+分治+规律推导公式
题目:
The inversion number of a given number sequence a1, a2, …, an is the number of pairs (ai, aj) that satisfy i < j and ai > aj.
For a given sequence of numbers a1, a2, …, an, if we move the first m >= 0 numbers to the end of the seqence, we will obtain another sequence. There are totally n such sequences as the following:
a1, a2, …, an-1, an (where m = 0 - the initial seqence)
a2, a3, …, an, a1 (where m = 1)
a3, a4, …, an, a1, a2 (where m = 2)
…
an, a1, a2, …, an-1 (where m = n-1)
You are asked to write a program to find the minimum inversion number out of the above sequences.
Input
The input consists of a number of test cases. Each case consists of two lines: the first line contains a positive integer n (n <= 5000); the next line contains a permutation of the n integers from 0 to n-1.
Output
For each case, output the minimum inversion number on a single line.
Sample Input
10
1 3 6 9 0 8 5 7 4 2
Sample Output
16
分析:
第一种解法:暴力
1.为了实现环,将数组扩到两倍a[i+n]=a[i];
2.求出初始串的状态,即最小逆序对;
3.由(1),直接往后遍历,每次选取连续的n个数,即效果等同于每次把最后的元素放置最前即可实现环。
AC代码
/**暴力*/
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
const int M=1e4+10;
int a[M],b[M];
int n,ans,num,cnt;
int main()
{while(~scanf("%d",&n)){memset(b,0,sizeof(b));for(int i=0;i<n;i++){scanf("%d",&a[i]);a[i+n]=a[i];}ans=0;for(int i=0;i<n;i++){for(int j=i+1;j<n;j++)if(a[i]>a[j])b[i]++;ans+=b[i];}for(int i=1;i<=n-1;i++){cnt=0;for(int j=i;j<i+n-1;j++){if(a[j]>a[i-1])b[j]++;cnt+=b[j];}if(ans>cnt)ans=cnt;}printf("%d\n",ans);}return 0;
}
第二种解法:树状数组求逆序数。
1.由题意:n个数(0~n-1,每个数仅出现一次),用树状数组来维护[0,n)的区间。
2.每次读入一个数,将该数存入树状数组b[],用树状数组来维护[0,n)的区间和。
3.每次先求出区间[ai+1,n)的和,即为ai之前比ai大的数字的个数。
4.由前面,求出初始串的状态,即最小逆序对
5.如序列a1,a2,a3,a4,a5,它的逆序对数量s=sum(num(ak>ai,k<i));序列变为a2,a3,a4,a5,a1,和上一个序列相比,变化分为两步。
(1)拿走a1,逆序对减少a1( 整个序列是0~n-1且每个数字仅出现一次)个,
(2)将a1 放入序列尾部;加入序列尾部,逆序对增加n-1-a1个,这样就可以递推求解各序列的逆序对个数。
#include<stdio.h>/**树状数组求逆序数*/
#include<string.h>
#include<algorithm>
using namespace std;
const int M=5e3+10;
int n,ans,num,mi;
int a[M],b[M];
int lowbit(int x)
{return x&(-x);
}
int query(int x)///每次先求出区间[ai+1,n)的和,即为ai之前比ai大的数字的个数
{int num=0;x+=1;while(x<=n){num+=b[x];x+=lowbit(x);}return num;
}
void update(int pos,int val)///用树状数组来维护[0,n)的区间和
{while(pos>0){b[pos]+=val;pos-=lowbit(pos);}
}
int main()
{while(~scanf("%d",&n)){ans=0;memset(b,0,sizeof(b));for(int i=0; i<n; i++){scanf("%d",&a[i]);update(a[i]+1,1);///每次读入一个数,将该数存入树状数组b[]ans+=query(a[i]+1);///查询前面是否有比该数大的数,即为逆序数}mi=ans;for(int i=0; i<n; i++){ans=ans+(n-1)-2*a[i];/**如序列a1,a2,a3,a4,a5,它的逆序对数量s=sum(num(ak>ai,k<i));
序列变为a2,a3,a4,a5,a1,和上一个序列相比,变化分为两步,拿走a1和将a1 放入序列尾部;拿走a1,逆序
对减少a1( 整个序列是0~n-1且每个数字仅出现一次)个,加入序列尾部,逆序对增加n-1-a1个,这样就可
以递推求解各序列的逆序对个数。*/if(ans<mi)mi=ans;}printf("%d\n",mi);}return 0;
}
第三种解法:分治求逆序数
1.用分治求出初始串的状态,每次把最后的元素放置最前即可实现环。
2.每次移动时:(与前面树状树状的后面解法一样)
(1)所有data[i]后面比他小的元素逆序数 -1(0~k-1,k个);
(2)所有data[i]后面比他大的元素逆序数 +1(n-1-k个);
(3)逆序数改变总数是 n - 2*k - 1(k = data[i]);
枚举不同的首元素,输出最小的逆序数即可。
#include <stdio.h>
#include <stdlib.h>
const int M=5e3+10;
int a[M];
int b[M];
int c[M];
int dfs(int aa,int bb)
{if (aa<bb){int L=dfs(aa,(aa+bb)/2);int R=dfs((aa+bb)/2+1,bb);int num=L+R,tot=aa;int x=aa,y=(aa+bb)/2;int u=(aa+bb)/2+1,v=bb;while(x<=y||u<=v)if(u<=v&&(x==y+1||a[x]>a[u])){b[tot++]=a[u++];num+=y-x+1;//计算A B 中的逆序数}elseb[tot++]=a[x++];for(int i=aa;i<=bb;++i)a[i]=b[i];return num;}elsereturn 0;
}int main()
{int n;while (~scanf("%d",&n)){for( int i = 1 ; i <= n ; ++ i )scanf("%d",&a[ i ]);for ( int i = 1 ; i <= n ; ++ i )c[ i ] = a[ i ];int ans= dfs( 1, n );int mi =ans;for ( int i = 1 ; i < n ; ++ i ){ans+= n - 2*c[ i ] - 1;if ( ans< mi )mi=ans;}printf("%d\n",mi);}return 0;
}