給定長度 $n$($n \le 10^6$)的整數序列,求逆序對數量:$i < j$ 且 $a_i > a_j$ 的對數。答案可能超過 32 位整數,需用 64 位(long long)。
歸並過程中,將數組分為左右兩半,遞歸排序並統計左右內部逆序對。合併時,若左半元素 $L[i] > R[j]$,則 $L[i..mid]$ 所有元素均大於 $R[j]$(因左右半已有序),一次性新增 $mid - i + 1$ 個逆序對。
void merge(int l, int mid, int r) {
int i = l, j = mid + 1, k = l;
while (i <= mid && j <= r) {
if (a[i] <= a[j]) {
tmp[k++] = a[i++];
} else {
tmp[k++] = a[j++];
ans += (long long)(mid - i + 1); // 關鍵:a[i..mid] 都 > a[j]
}
}
while (i <= mid) tmp[k++] = a[i++];
while (j <= r) tmp[k++] = a[j++];
copy(tmp + l, tmp + r + 1, a + l);
}
#include
using namespace std;
const int MAXN = 1000005;
int a[MAXN], tmp[MAXN];
long long ans;
void mergeSort(int l, int r) {
if (l >= r) return;
int mid = (l + r) >> 1;
mergeSort(l, mid);
mergeSort(mid + 1, r);
// 合併
int i = l, j = mid + 1, k = l;
while (i <= mid && j <= r) {
if (a[i] <= a[j]) tmp[k++] = a[i++];
else {
tmp[k++] = a[j++];
ans += mid - i + 1;
}
}
while (i <= mid) tmp[k++] = a[i++];
while (j <= r) tmp[k++] = a[j++];
for (int i = l; i <= r; ++i) a[i] = tmp[i];
}
int main() {
ios::sync_with_stdio(false); cin.tie(nullptr);
int T; cin >> T;
while (T--) {
int n; cin >> n;
for (int i = 0; i < n; ++i) cin >> a[i];
ans = 0;
mergeSort(0, n - 1);
cout << ans << '\n';
}
return 0;
}
tmp。遠優於暴力 $O(n^2)$,可處理 $n=10^6$。同樣原理可用於樹狀數組($O(n \log C)$,$C$ 為值域)或線段樹,但歸並排序最簡潔、常數最小。
| 細節 | 說明 |
|---|---|
| 長整型 | 最大逆序對 $n(n-1)/2 \approx 5 \times 10^{11}$,必須用 long long。 |
| 穩定性 | 合併時 a[i] <= a[j] 取左,保證相等元素相對序不變,算法穩定。 |
| 臨時數組 | 全局靜態分配 tmp[MAXN] 避免遞歸中重複分配棧內存。 |
| 拷貝回寫 | 合併後必須 copy 回 a,保證上層合併見到有序數組。 |
輸入:
2
2
1 1
3
1 3 2
輸出:
0
1
第二組:(3,2) 唯一逆序對。
歸並排序統計逆序數是「分治 + 有序性利用」的教科書級範例。掌握其合併階段的計數邏輯(mid - i + 1),即可舉一反三解決一系列「有序數組配對計數」問題。代碼簡短、常數極小、無值域限制,是競賽與面試首選方案。