【ETOJ P1017】求逆序对个数 题解(分治算法+归并排序)

130 阅读2分钟

题目描述

给定一个长度为 nn 的数组 aa,求 aa 的逆序对个数。

逆序对的定义是一个二元组 (i,j)(i, j),满足:i<ji<jai>aja_i > a_j

输入格式

一个整数 nn,满足条件:1n2×1051 ≤ n ≤ 2 × 10^5

接下来一行 nn 个整数表示数组 aa,满足条件:1ai1091 ≤ a_i ≤ 10^9

输出格式

一行输出一个结果。

样例

输入样例1

5
2 3 1 1 6

输出样例1

4

样例解释

样例1中的逆序对为 (2,1)(2,1), (2,1)(2,1), (3,1)(3,1), (3,1)(3,1),共 44 个。

提示

使用归并排序或树状数组。


思路

逆序对是指在一个数组中,对于下标iijj,如果i<ji < j但是a[i]>a[j]a[i] > a[j],那么我们就说(a[i],a[j])(a[i], a[j])是一个逆序对。

归并排序是一种典型的分治算法,它将一个大问题分解为两个或更多的相同或相似的子问题,然后将子问题的解合并,这样就可以得到原问题的解。

在这段代码中,mergeSort函数就是实现了这个过程。首先,它会判断当前的区间是否只有一个或两个元素,如果是的话,就可以直接求解出逆序对的个数,然后返回。如果区间内的元素数量多于两个,那么就需要将区间分为两部分,然后分别对它们进行排序和求解逆序对的个数。

在合并两个已排序的子区间时,如果左边区间的当前元素大于右边区间的当前元素,那么就存在逆序对,逆序对的个数就是左边区间的当前元素到左边区间的末尾的元素的个数。这是因为左边区间的所有元素都大于右边区间的当前元素。

最后,将排序后的结果复制回原数组,然后返回逆序对的个数。在主函数中,首先读入数组的元素,然后调用mergeSort函数进行排序和求解逆序对的个数,最后输出逆序对的个数。


AC代码

#include <algorithm>
#include <iostream>
#define ll long long
#define AUTHOR "HEX9CF"
using namespace std;

const int N = 1e6 + 7;

int n;
ll a[N], b[N];

ll mergeSort(int l, int r) {
    ll ret = 0;
    // 递归出口
    if(r - l <= 1) {
        if(a[l] > a[r]) {
            swap(a[l], a[r]);
            ret++;
        }
        return ret;
    }
	// 合并区间
	int mid = (l + r) >> 1;
	ret = mergeSort(l, mid) + mergeSort(mid + 1, r);
	int p1 = l;
	int p2 = mid + 1;
	for (int i = l; i <= r; i++) {
		if (p1 == mid + 1) {
			b[i] = a[p2++];
		} else if (p2 == r + 1) {
			b[i] = a[p1++];
		} else if (a[p1] <= a[p2]) {
			b[i] = a[p1++];
		} else {
            // 逆序
			b[i] = a[p2++];
			ret += 1ll * mid - p1 + 1;
		}
	}
	// 合并完成,拷贝回原数组
	for (int i = l; i <= r; i++) {
		a[i] = b[i];
	}
	return ret;
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);

	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	cout << mergeSort(1, n) << endl;
	return 0;
}