第五届蓝桥杯省赛 小朋友排队 知识点:逆序对 树状数组

45 阅读2分钟

1.小朋友排队 - 蓝桥云课 (lanqiao.cn)

首先看看这道题什么意思:

输入

3
3 2 1

输出

9

其实就是冒泡排序,排成正序。 image.png

冒泡排序实际上就是在找逆序对。

即在满足 i<j(下标)的对情况下,有多少个ai>aj。如果大于就交换。

对于求逆序对,我们有一个结论:

我们如果确定一个数x,如果这个数前面有k1个数比它大,后面有k2个数比它小,那么就需要交换k1+k2次。

证明

前面有k1个数比它大,那么至少把k1个数全部交换到它后面,后面有k2个数比它小,那么至少就要这些小的数全部交换到x前面。所以就需要交换k1+k2次。

验证

比如4前面有1个数比4大,4后面有2个数比4大,那么总共就需要交换3次。

image.png

交换过程:

image.png

总结

那么这道题就演变为了求: 有多少个比x大的数在x前面 + 有多少个比x小的数在x后面。

这道题用树状数组

code

这道题可能会爆int,因为n最大为10510^5,那么假如要交换10510^5次,那么不高兴程度和就为: 1+2+3++1051+2+3+……+10^5 ,用高斯求和:105(105+1)2=10(10)2=5e9 \frac{10^5*(10^5+1)}{2} = \frac{10^(10)}{2}=5e9,而int的最大范围是2e92e9 ,因此我们需要开个long long。

代码部分我们分为两步求,第一步求 每一个数前面有多少个数比它大,比如我们要求有多少个数比hi大,那么是不是应该是 hi+1,hi+2,……10^6,我们可以用求和公式:s[n]-s[hi]求出。

这里树状数组不是求区间和,而是求落在区间内的个数

/*
举例:1+2+3+4+5+6+7+8+9+10=55
推导出:1-n之间的加法公式:n*(n-1)
*/
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1000010;
int n;
int h[N], tr[N];
int sum[N];

int lowbit(int x)
{
    return x & -x;
}

void add(int x, int v)
{
    for (int i = x; i < N; i += lowbit(i))  tr[i] += v;
}

int query(int x) //O(logN)查找逆序对儿的数量
{
    int res = 0;
    for (int i = x; i > 0; i -= lowbit(i))   res += tr[i];
    return res;
}
int main() {
    cin >> n;
    for (int i = 1; i <= n; i++)
    {
        cin >> h[i];
        h[i]++;
    }
    
    //每个数前面有多少个数比它大
    for (int i = 1; i <= n; i++) {  //正序处理
        sum[i] = query(N - 1) - query(h[i]);
        add(h[i], 1);
    }

   //树状数组清空
    memset(tr, 0, sizeof tr);
    
    //每个数后面有多少个数比它大
    for (int i = n; i > 0; i--) {
        sum[i] += query(h[i] - 1);
        add(h[i], 1);
    }
    
    long long res = 0;
    for (int i = 1; i <= n; i++)
        res += (long long)sum[i] * (sum[i] + 1) / 2;
    cout << res << endl;
}