求排列的逆序數(分治)
阿新 • • 發佈:2022-02-06
題目描述:考慮1,2,…,n (n <= 100000)的排列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的一個排列,求它的逆序數。
其實笨辦法很簡單就是涉及一個雙重迴圈。遍歷一個數之後再往後遍歷找有沒有比這個數小的數,有的話就輸出相應的數。雙重迴圈很明顯複雜度是O(n^2)。
現在使用分治的思維,就是左邊來找逆序數,右邊再找逆序數,任何再找右邊一個數左邊一個數的逆序數(要求O(n)實現)。找左右兩邊都有的逆序數關鍵:
一開始讓i指向開頭的元素,讓j指向右半邊開頭的元素。將i與j比較,如果右半邊的數比左半邊大的話就把j往後移動知道j移到5停止。這樣後面的所有數就與10構成逆序數。任何i++就好了j也直接往後走。
/* 歸併排序是將兩個(或兩個以上)有序表合併成一個新的有序表,即把待排序序列分為 * 若干個子序列,每個子序列是有序的,然後再把有序的子序列合併為整體有序序列 * 歸併排序是分治演算法的一個典型的應用,而且是穩定的一種排序,這題利用歸併排序 * 的過程中,計算每個小區間的逆序數,進而得到大區間的逆序數。那麼,問題就解決了。 歸併排序是將數列a[l,h]分成兩半a[l,mid]和a[mid+1,h]分別進行歸併排序,然後再將這兩半合併起來。 在合併的過程中(設l<=i<=mid,mid+1<=j<=h),當a[i]<=a[j]時,並不產生逆序數;當a[i]>a[j]時,在 前半部分中比a[i]大的數都比a[j]大,將a[j]放在a[i]前面的話,逆序數要加上mid+1-i。因此,可以在歸併 排序中的合併過程中計算逆序數.*/ #include <iostream> #include <string.h> #include <stdio.h> using namespace std; #define N 1000002 long long a[N],tmp[N]; long long ans; //歸併排序的合併部分 void Merge(int l,int m,int r) { int i = l; int j = m + 1; int k = l; while(i <= m && j <= r) {if(a[i] > a[j]) { tmp[k++] = a[j++]; ans += m - i + 1; } else { tmp[k++] = a[i++]; } } while(i <= m) tmp[k++] = a[i++]; while(j <= r) tmp[k++] = a[j++]; for(int i=l;i<=r;i++) a[i] = tmp[i]; } //l左端點,r右端點 //歸併排序 void Merge_sort(int l,int r) { if(l < r) { int m = (l + r) >> 1; Merge_sort(l,m);//將前半部分排序 Merge_sort(m+1,r);//將後半部分排序 Merge(l,m,r);//合併前後兩個部分 } } int main() { int n,T; scanf("%d",&T); while(T--) { scanf("%d",&n); for(int i=0;i<n;i++) scanf("%d",&a[i]); ans = 0; //0左端點,n-1右端點 Merge_sort(0,n-1); printf("%lld\n",ans); } return 0; }
認真想一想,這個程式碼和歸併排序的程式碼的區別,就是在歸併排序的同時進行答案的計算,把兩邊按照從小到大來排序。