1. 程式人生 > 實用技巧 >歸併排序求逆序對

歸併排序求逆序對

歸併排序求逆序對

逆序對:對於數列的第 \(i\) 個和第 \(j\) 個元素,如果滿足 \(i < j\)\(a[i] > a[j]\),則其為一個逆序對。

暴力求法就是兩遍for迴圈,\(O(n^2)\)超時

用歸併可以順便求出來,\(O(nlogn)\)

在一次歸併排序中,因為歸併要求將一個完整區間分為兩塊,所以逆序對出現了三種情況。

  1. 逆序對在mid的左邊
  2. 逆序對在mid的右邊
  3. 逆序對的兩個元素一個在mid左邊,一個在mid右邊

之前提過歸併可以順便求出來,這裡設歸併排序有大小為該區間逆序對個數的返回值,即\(l-r的逆序對個數=merge\_sort(l,r)\)

因為是遞迴處理嘛,所以先將左右兩邊的歸併排序。重點是第三部分,如何處理這種情況。

首先,通過前兩次歸併排序,從\(L-mid,mid+1-R\)兩個部分都是有序的,在合併這兩個有序序列時,可以求出第三種情況的逆序對個數。

重新想一下逆序對的定義,是在前面的數比後面的數大。在區間上來看,就是\(L-mid\)的這一部分中有元素比\(mid+1-R\)中的元素大。

每次移動右半邊的指標時,從中間到左半邊指標所包含的所有數字 都是大於右半邊指標所指向的數字的,這些都可以組成逆序對,直接加上即可。

最後呢,其實1和2的情況沒有討論,因為歸併排序最後還是要分到只有兩個數進行排序的。一個數不能構成逆序對,所以只有兩個數字的情況下,只存在第三種情況,接著往下遞迴就不需要考慮12了。

程式碼:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int maxn = 1e5 + 10;
int n, q[maxn], tmp[maxn];

ll solve(int l, int r)
{
    if (l >= r)
        return 0;
    int mid = l + r >> 1;
    ll ans = solve(l, mid) + solve(mid + 1, r);
    int i = l, j = mid + 1, k = l;
    while (i <= mid && j <= r) {
        if (q[i] <= q[j])//特別要注意這個,雖然加不加等號對於排序而言並沒有影響,但是求逆序數時,加不加等於號是兩種情況。只有在嚴格大於的時候才是逆序數。
            tmp[k++] = q[i++];
        else {
            tmp[k++] = q[j++];
            ans += (mid - i + 1);
        }
    }
    while (i <= mid)
        tmp[k++] = q[i++];
    while (j <= r)
        tmp[k++] = q[j++];
    for (int i = l; i <= r; i++)
        q[i] = tmp[i];
    return ans;
}

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; ++i)
        scanf("%d", &q[i]);
    ll res = solve(0, n - 1);
    cout << res;
}