AcWing 788. 逆序对的数量

题目传送门

一、理解感悟

1、什么是逆序对?

大的在前,小的在后,这样有一个算一个。

逆序对包括:\(\{5,2\},\{5,2\},\{5,1\},\{5,4\},\{2,1\},\{2,1\}\),共\(6\)个。

2、暴力大法

暴力的计算代码是这样滴:

#include <bits/stdc++.h>

using namespace std;
typedef long long LL;
int n;
const int N = 100010;
int a[N];
LL ans;
//暴力大法
/*测试数据
 5
 5 2 2 1 4
 */
int main() {
    cin >> n;
    for (int i = 1; i <= n; i++)cin >> a[i];
    //暴力大法
    for (int i = 1; i <= n; i++)
        for (int j = i + 1; j <= n; j++)
            if (a[i] > a[j]) ans++;
    cout << ans << endl;
    return 0;
}

2、答案的数据类型

最终的\(ans\)定义为什么类型?就是说给定范围内的数据,产生的逆序对最多是多少个呢?本题中\(n<=100000\),就是小于\(10^5\)。那么什么情况下逆序对最多呢?就是完全倒序的情况下逆序对最多,那么在有\(n\)个数据的情况下,有多少个逆序对呢?
对于第\(1\)个而言,有\(n-1\)个逆序对,第\(2\)个有\(n-2\)个逆序对,....,第\(n\)个有\(0\)个逆序对。
\(ans=(n-1)+(n-2)+(n-3)+...+1=\frac{n*(n-1)}{2}\)
本题\(n\)最大是\(10^5\),所以\(ans\)最大值是\(ans=100000*(100000-1)/2 \approx 1e{10} \div 2=5*10^9\)
\(int\)的极限是\(2147483647\),比\(2e9\)大一点点,就是说\(int\)装不下,需要用\(long \ long\):

十年\(OI\)一场空,不开\(long\ long\) 见祖宗

毫无悬念,\(tle\)妥妥的。

二、利用归并求逆序对

\(Q\):为什么归并排序可以求逆序对呢?

答:在归并两个子数组时,有三种情况:

(1)、逆序对在左边(红色)

这在调用左侧子数组进行递归时,已经完成了统计,自己不管。(内部问题内部解决)

(2)、逆序对在右边(绿色)
这在调用右侧子数组进行递归时,已经完成了统计,自己不管。(内部问题内部解决)

(3)、逆序对在左侧和右侧(黄色)
这个需要自己来处理,对于右侧的黄色圆,那么在左侧的黄色圆及左侧黄色圆后面,一直到中线的所有数,都是比右侧黄色圆大的,就是有\(mid-i+1\)个逆序对。

可以结合代码来理解原理,其实就是在归并的时刻,只处理合并数组中大小反着的两个数字,一旦发现前面的数字比后面的数字大,就是发现了一个逆序对,同时,由于归并排序的特点,两个要归并的数组是内部有序的,所以,意味着左侧当前数字及左侧当前数字一直到左侧集合结束的所有数字,都比右侧当前数字大!个数就是\(mid-i+1\)个,其中\(l<=i<=mid\)

三、代码实现

#include <bits/stdc++.h>

using namespace std;
typedef long long LL;

const int N = 100010;
int n;          //n个数据
int q[N];       //原数组
int tmp[N];     //临时数组
LL ans;         //结果

/**
 * 功能:利用归并排序求数组中逆序对的数量
 * @param q 原数组
 * @param l 左侧开始索引
 * @param r 右侧结束索引
 */
void merge_sort(int q[], int l, int r) {
    //数组为空,则是递归出口
    if (l >= r) return;
    //中位值
    int mid = (l + r) >> 1;
    //递
    //处理左侧子数组
    merge_sort(q, l, mid);
    //处理右侧子数组
    merge_sort(q, mid + 1, r);

    //归
    int i = l;       //左侧出发点
    int j = mid + 1; //右侧出发点
    int k = 0;       //下标索引
    //两个数组每个数据打擂台,谁小谁进入临时数组
    while (i <= mid && j <= r)
        if (q[i] <= q[j]) tmp[k++] = q[i++];
        else {
            tmp[k++] = q[j++];
            //(1)这里进来一次,就表示出现了一回逆序的数字,
            /*(2)因为两组PK的数组,本身自己内部是有序的,由小到大的。所以,当发现一个逆序时,
             此数字后面的其它数字必须也是逆序。此数字与它后面的数字的总个数=mid-i+1*/
            ans += mid - i + 1;//j长大一次,结果就增大mid-i+1个。
        }
    //如果有剩余的,就全部扫进临时数组
    while (i <= mid) tmp[k++] = q[i++];
    while (j <= r) tmp[k++] = q[j++];
    //将tmp数组数据复制回q数组
    for (i = l, j = 0; i <= r; i++, j++) q[i] = tmp[j];
}

int main() {
    //优化输入
    ios::sync_with_stdio(false);
    //输入
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> q[i];
    //归并排序
    merge_sort(q, 1, n);
    //输出
    cout << ans << endl;
    return 0;
}
原文地址:https://www.cnblogs.com/littlehb/p/15223919.html