[CP] 学习记录——树状数组求逆序对个数

NOTE1: 本文并不包含树状数组的原理及实现。
NOTE2: 主要参考 树状数组 - OI Wiki 写成。

逆序对

对于给定数组 \(a\),记下标为 \(i\) 的元素为 \(a_i\),定义逆序对 \((i,j)\) 为满足:

\[a_i>a_j,\ i<j \]

的数对。

使用暴力方法求逆序对个数

根据上述定义,可以很快写出暴力方法:

int cnt = 0;
for (int i = 0; i < n; i++) {
    for (int j = i + 1; j < n; j++) {
        if (a[i] > a[j])
            ++cnt;
    }
}

时间复杂度 \(O(n^2)\)\(n\) 较大时就会超时。

使用树状数组求逆序对个数

更快的方法是使用归并排序或树状数组,这里介绍后者。

权值数组

本方法的关键在于权值数组(如果之前学过桶排序应该很好理解)。对于数组 \(a\),定义权值数组 \(b\),使得 \(b_j\) 等于 \(j\)\(a\) 中出现的次数。比如对于 \(a=[3,2,5,1,4,2]\),可以构造权值数组 \(b=[0,1,2,1,1,1]\)。可见,权值数组掩盖了原数组中元素之间的大小关系,这与逆序对定义中蕴含的大小关系是相悖的。

然而,如果我们一个个地向权值数组中添加元素呢?

回想先前的暴力做法,对于原数组的每个元素 \(a_i\),我们遍历 \(i<j<n\) 的数组部分,统计所有满足 \(a_i>a_j\) 的数对个数……

注意到,逆序对定义中的两个条件并没有先后之分,所以我们反过来思考,可以找到所有满足 \(a_i>a_j\) 的数对,统计 \(i<j<n\) 的个数。

如果用权值数组来实现,因为权值数组的下标严格递增,故 “所有满足 \(a_i>a_j\) 的数对个数” 就等于 \(\sum\limits_{k=0}^{a_i-1}b_k\),前提是这些位于权值数组中的元素的下标都大于 \(i\)

那么,如何保证这个前提?只需要从原数组的最后一个元素开始,一个个地将其添加到权值数组中,直到第一个元素,同时在每次添加元素时将 \(\sum\limits_{k=0}^{a_i-1}b_k\) 累加到最终答案里面。

因为修改权值数组会影响到 \(\sum\limits_{k=0}^{a_i-1}b_k\) 的查询,为了更高效地完成上述操作,使用树状数组来实现权值数组的功能,因此就有 “权值树状数组” 的叫法。

离散化

当输入数组 \(a\) 的元素值域较宽时,因为权值数组的下标对应的是 \(a\) 的元素值,因此可能会有较多的空间浪费,特别地,当元素值非常大时(如 \(10^{13}\)),则根本无法开辟这样的权值数组。

这一问题可以用离散化来解决。因为我们当前关注的是元素的相对大小关系,而不关心元素的值究竟是多少,所以可以计算出每个元素的相对索引。以数组 \(a=[10000000,1,100,10]\) 为例,可以构造出如下的数组 \(a'=[4,1,3,2]\),表示 “将原数组排序后,每个元素所处的位置”(为了方便,索引一般从 1 开始)。这样一来,\(a\) 的每个元素都可以通过 \(a'\) 对应到权值数组的某个元素上了。

例题

F. Greetings

posted @ 2024-03-01 12:00  ZXPrism  阅读(56)  评论(0)    收藏  举报