要移动元素的次数实质就是逆序对数。
这个在入门经典上有详细解释,归并排序一改就ok了。
这里我们首先应该明确一个问题,前后两段内部的元素位置并不影响两个分别来自前后两段元素的逆序性,简单点说,a来自前段,b来自后端,如果(a,b)是个逆序对,那无论a,b在其段内怎么移动,(a,b)仍是逆序对。所以这个问题的解法就是用前段的逆序对数加上后段的逆序对数,再加上合并之后的的逆序对数,很明显这是一个递归过程。
比如说序列 6 3 2 0 4 7 1 0.
使用分治法求逆序对数。
第一次划分:6 3 2 0 | 4 7 1 0
第二次划分:6 3 | 2 0 | 4 7 1 0
第三次划分:6 | 3 | 2 0 | 4 7 1 0
此时第三次划分产生的左右两段的6和3都已经到达边界,开始回升。
第三次划分的回升:左6,右3,很明显有一个逆序对,排序后为 3 6.
第四次划分:3 6 | 2 |0 | 4 7 1 0
此时第四次划分产生的左右两段的2和0都已经到达边界,开始回升。
第四次划分的回升:左2,右0,很明显有一个逆序对,排序后为 0 2.
第二次划分产生的左右两段问题都已解决,可以回升。
第二次划分的回升:左:3 6,右0 2,很明显有四个逆序对,排序后为0,2,3,6
到这里第一次划分产生的两段,左段已经求得逆序对数并排序,下面解决右段。
第五次划分:0 2 3 6 | 4 7| 1 0
第六次划分:0 2 3 6| 4| 7| 1 0
第六次划分产生的左右两段都已达边界,开始回升。
第六次划分的回升:左:4,右:7,没有逆序对,排序后是4 7
第五次划分产生的左段问题已经解决,再处理右段。
第七次划分:2 3 6| 4 7|1 |0
第七次划分产生的左右两段都已达边界,开始回升。
第七次划分的回升:左:1 ,右: 0,有一个逆序对,排序后为0 1.
这时候再合并第五次的划分。
第五次划分的回升:左4 7,右 0,1 ,有四个逆序对,排序后为 0,1,4,7
这时候第一次划分产生的右段也都处理完了。
第一次划分的回升:左 :0 2 3 6,右 0 1 4 7,有七个逆序对,排序后为0 0 1 2 3 4 6 7
最后可以得到逆序对数为18.
在回升过程中,在左右两个数组中最小的元素比较,如果左最小元素大于右最小元素,即说明此时左边所有元素都大于右最小元素,此时可以统计一下左边元素个数为右该元素的逆序对数。
其实排序会影响逆序对数的,但是在排序之前小规模的问题已经被解决,所以会方便解决更大规模的问题而不会影响结果。
下面的归并排序是算法导论上的思路,是闭区间。
#include<iostream> #include<cstdio> #include<cstring> #include<cstdlib> #define _MAX 2147483647 using namespace std; int A[500005]={0},L[250005]={0},R[250005]={0}; long long cnt=0; void merge(int p,int q,int r) { int n1=q-p+1,n2=r-q; int i,j; // memset(L,0,sizeof(L)); // memset(R,0,sizeof(R)); for(i=1;i<=n1;++i) L[i]=A[p+i-1]; for(j=1;j<=n2;++j) R[j]=A[q+j]; L[n1+1]=_MAX; R[n2+1]=_MAX; i=1;j=1; for(int k=p;k<=r;++k) if(L[i]<=R[j]) A[k]=L[i++]; else {A[k]=R[j++];cnt+=n1+1-i;} } void merge_sort(int p,int r) { if(p<r) { int q=((p+r)/2); merge_sort(p,q); merge_sort(q+1,r); merge(p,q,r); } } int main() { //freopen("in.txt","r",stdin); int n; while(scanf("%d",&n)&&n) { for(int i=0;i<n;++i)scanf("%d",&A[i]); cnt=0; merge_sort(0,n-1); printf("%lld\n",cnt); } return 0; }
#include <cstdio> #include <cstring> #include <cmath> #include <algorithm> #define N 500050 using namespace std; int n; int A[N],T[N]; long long int ans; void msort(int x,int y) { if(y > x+1) { int m = x+(y-x)/2; int q = x,p = m,i = x; msort(x,m); msort(m,y); while(q < m || p < y) { if(p >= y || (q < m && A[q] <= A[p])) T[i++] = A[q++]; else { ans += m-q; T[i++] = A[p++]; } } for(i = x; i < y; i++) A[i] = T[i]; } } int main() { //freopen("in.txt","r",stdin); //freopen("out.txt","w",stdout); while(scanf("%d",&n) == 1&& n) { memset(A,0,sizeof(A)); for(int i = 0; i < n; i++) scanf("%d",&A[i]); ans = 0; msort(0,n); printf("%lld\n",ans); } return 0; }
几个月以后学了树状数组重写了这道题。
先按从大到小排序,依次取最大的放入树状数组中,它放入位置为它在排序之前的位置,然后统计从1到该位置的和并累计,最后将该位置的值更新为1。如此直到全部放入。
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <cmath> using namespace std; struct Num { int val,cur; }; int n; int a[500005]; Num x[500005]; bool cmp(Num a,Num b) { return a.val>b.val; } int lowbit(int x) { return (-x)&x; } int Sum(int p) { int sum=0; while(p>0) { sum+=a[p]; p=p-lowbit(p); } return sum; } void Add(int p,int val) { while(p<=n) { a[p]+=val; p=p+lowbit(p); } } int main() { while(scanf("%d",&n)&&n) { for(int i=1; i<=n; ++i) { scanf("%d",&x[i].val); x[i].cur=i; } sort(x+1,x+n+1,cmp); memset(a,0,sizeof(a)); long long ans=0; for(int i=1; i<=n; ++i) { ans+=Sum(x[i].cur); Add(x[i].cur,1); } printf("%lld\n",ans); } return 0; }