树状数组求逆序对
极难2什么是逆序对?——从排队的故事讲起
同学们有没有遇到过这样的情况:老师让大家按身高从低到高排队,结果有些同学站错了位置。比如,排好的队伍本来是:小明(170cm)、小红(155cm)、小刚(165cm)、小丽(180cm)。小红的个子比小明矮,但她却站在小明后面——这就形成了一个“逆序对”:高的在前,矮的在后。在数学上,逆序对就是数组中满足 i < j 且 a[i] > a[j] 的一对元素。
逆序对的个数能帮我们了解一个数组的“有序程度”。比如,在体育课上,老师想看看队伍有多乱;在排序算法中,逆序对数量就是移动元素的最小次数;在推荐系统中,逆序对可以用来比较两个排序结果的好坏。
通常,我们可以用归并排序来统计逆序对,但今天要学一个更酷的方法——树状数组,它不仅代码短、速度也很快(O(n log n)),而且思路非常巧妙。
核心思想:一边“插队”一边数数
想象你有全班同学的名单,你想知道每个同学前面有多少个比他高的人。一个朴素的笨办法是:每次看一个新同学,就回头看前面所有人,比一比身高——但这样太慢了(O(n²))。树状数组的思路是:边插入边统计,就像在排队时,每来一个新同学,我们就问:“现在队列里有多少人比你高?” 然后把他自己也加入队列。
因为我们要频繁地“问人数”和“加人”,所以需要一个能快速完成这两个操作的数据结构。树状数组正好胜任!它可以做到:
- add(位置, 1):在某个数值的位置上增加1个人(插入)
- sum(位置):快速算出所有数值小于等于某个值的人数(前缀和)
这样,我们就能用 已经插入的总人数 - 小于等于当前身高的人数 得出前面比他高的人数。
准备工作:离散化——把大数字变小
如果直接拿身高(比如 170、155)当树状数组的下标,那下标范围会很大(比如身高从50到250),数组要开很大,浪费空间。更糟的是,数值可能很大(比如 10^9),数组根本开不了。怎么办?
离散化就是把数值映射成1、2、3…这样小的整数。比如小明(170)变成3,小红(155)变成1,小刚(165)变成2……映射时只保留大小关系,不关心具体数值。
具体做法:
- 把原数组复制一份,排序并去重(如果有重复值,相同的值映射到同一个数字)。
- 用二分查找找出每个元素在排序数组中的位置(从1开始编号),这个编号就是离散化后的值。
例如数组 [3, 1, 2] 排序去重后是 [1,2,3],那么:
- 3 映射成 3
- 1 映射成 1
- 2 映射成 2
这样树状数组只需要开 n 的大小,而不是很大的数值范围。
树状数组是怎么工作的?(简单回顾)
树状数组(Fenwick Tree)是一种支持单点更新和前缀和查询的数据结构。它有一个神奇的 lowbit 操作:lowbit(x) = x & -x,它提取出 x 的最低位的1所对应的值(比如 x=6(110) -> lowbit=2(10))。
- add(idx, delta):从 idx 开始,不断
idx += lowbit(idx),把沿途所有节点都加上 delta。 - sum(idx):从 idx 开始,不断
idx -= lowbit(idx),累加所有节点的值。
如果我们把每个数值的出现次数放在树状数组里,那么 sum(x) 就是所有 <= x 的数值的个数。
(如果你还不熟悉树状数组,可以把它想象成一种“范围统计”的魔法盒子——插入一个数,很快就能知道“有多少个数小于等于它”。)
一步步推导逆序对个数
拿数组 [3, 1, 2] 举例,离散化后还是 [3, 1, 2]。树状数组初始全0,下标1..3。
i=0, 处理数值x=3
sum(3) = 0 (小于等于3的已有0个)
已插入总数 i = 0 (因为还没插)
前面比3大的个数 = i - sum(3) = 0 - 0 = 0
答案 += 0
add(3, 1) // 插入一个3
i=1, 处理数值x=1
sum(1) = 0 (小于等于1的已有0个)
已插入总数 i = 1 (前面已经插了一个3)
前面比1大的个数 = 1 - 0 = 1 (就是那个3)
答案 += 1 → 答案=1
add(1, 1) // 插入一个1
i=2, 处理数值x=2
sum(2) = ? // 已经插入了3和1,所以小于等于2的有1(一个1),所以 sum(2)=1
已插入总数 i = 2
前面比2大的个数 = 2 - 1 = 1 (是那个3)
答案 += 1 → 答案=2
add(2, 1) // 插入一个2
最终答案2,对应逆序对(3,1)和(3,2)。验证一下:数组 [3, 1, 2] 中,3>1,3>2,确实只有两对。
ASCII图示意(更详细版本)
我们一步一步画一下树状数组的变化(树状数组下标1..3,初始全0):
初始: tree[1]=0, tree[2]=0, tree[3]=0 (下标1..3)
第1个元素 x=3:
sum(3) = tree[3] = 0
前面比3大的 = 0 - 0 = 0
答案=0
add(3,1):
idx=3 → tree[3] +=1 → tree[3]=1
idx=3+lowbit(3)=3+1=4 > n 结束
现在树状数组: [0,0,0,1] (下标0不用,1..3: tree[1]=0, tree[2]=0, tree[3]=1)
第2个元素 x=1:
sum(1) = tree[1] = 0
已插入数 i=1
前面比1大的 = 1 - 0 = 1
答案=1
add(1,1):
idx=1 → tree[1] +=1 → tree[1]=1
idx=1+lowbit(1)=1+1=2 → tree[2] +=1 → tree[2]=1
idx=2+lowbit(2)=2+2=4>n 结束
现在树状数组: tree[1]=1, tree[2]=1, tree[3]=1
第3个元素 x=2:
sum(2) = tree[2] = 1 (因为tree[2]代表区间[1..2]的和)
已插入数 i=2
前面比2大的 = 2 - 1 = 1
答案=2
add(2,1):
idx=2 → tree[2] +=1 → tree[2]=2
idx=2+lowbit(2)=2+2=4>n 结束
最终树状数组: tree[1]=1, tree[2]=2, tree[3]=1
完整代码实现(带中文注释)
C++ 版本
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Fenwick {
private:
int n; // 树状数组的大小
vector<int> tree; // 存储树状数组
int lowbit(int x) { return x & -x; }
public:
Fenwick(int n) : n(n), tree(n + 1, 0) {}
// 在位置 idx 上增加 delta
void add(int idx, int delta) {
while (idx <= n) {
tree[idx] += delta;
idx += lowbit(idx);
}
}
// 查询前缀和 [1..idx]
int sum(int idx) {
int res = 0;
while (idx > 0) {
res += tree[idx];
idx -= lowbit(idx);
}
return res;
}
};
// 主函数:计算逆序对数目
long long count_inversions(vector<int>& a) {
int n = a.size();
// 1. 离散化
vector<int> sorted = a; // 复制原数组
sort(sorted.begin(), sorted.end()); // 排序
// 去重(如果数值可能重复)
sorted.erase(unique(sorted.begin(), sorted.end()), sorted.end());
// 将每个原数值映射为从1开始的排名
vector<int> rank(n);
for (int i = 0; i < n; ++i) {
// lower_bound 找到第一个 >= a[i] 的位置,下标从0开始,+1后从1开始
rank[i] = lower_bound(sorted.begin(), sorted.end(), a[i]) - sorted.begin() + 1;
}
// 2. 初始化树状数组,大小为不同数值的个数
Fenwick ft(sorted.size());
long long ans = 0; // 逆序对总数
// 3. 从左到右扫描
for (int i = 0; i < n; ++i) {
int x = rank[i]; // 当前元素的离散化值
int less_or_equal = ft.sum(x); // 前面已有元素中 <= x 的个数
int greater = i - less_or_equal; // 前面比 x 大的个数(i 是已插入的总数)
ans += greater; // 累加逆序对
ft.add(x, 1); // 将当前元素插入树状数组
}
return ans;
}
int main() {
vector<int> arr1 = {3, 1, 2};
cout << "逆序对个数: " << count_inversions(arr1) << endl; // 2
vector<int> arr2 = {5, 2, 6, 1};
// 5,2,6,1 逆序对: (5,2),(5,1),(2,1),(6,1) 共4个
cout << "逆序对个数: " << count_inversions(arr2) << endl; // 4
vector<int> arr3 = {1, 2, 3, 4, 5};
cout << "逆序对个数: " << count_inversions(arr3) << endl; // 0
return 0;
}
Python 版本
class Fenwick:
def __init__(self, n):
self.n = n
self.tree = [0] * (n + 1) # 下标从1开始
def lowbit(self, x):
return x & -x
def add(self, idx, delta):
while idx <= self.n:
self.tree[idx] += delta
idx += self.lowbit(idx)
def sum(self, idx):
res = 0
while idx > 0:
res += self.tree[idx]
idx -= self.lowbit(idx)
return res
def count_inversions(arr):
# 1. 离散化
sorted_vals = sorted(set(arr)) # 排序并去重
# 建立原值到排名的映射(从1开始)
val_to_rank = {val: i+1 for i, val in enumerate(sorted_vals)}
n = len(arr)
ft = Fenwick(len(sorted_vals))
ans = 0
# 2. 从左到右扫描
for i, val in enumerate(arr):
x = val_to_rank[val]
less_or_equal = ft.sum(x) # 前面 <= x 的元素个数
greater = i - less_or_equal # 前面大于 x 的元素个数(i是已插入的总数)
ans += greater
ft.add(x, 1) # 插入当前元素
return ans
# 测试
if __name__ == "__main__":
print(count_inversions([3, 1, 2])) # 2
print(count_inversions([5, 2, 6, 1])) # 4
print(count_inversions([1, 2, 3, 4, 5])) # 0
常见错误与调试
-
忘记离散化:如果直接使用原数值作为树状数组下标,数值范围可能太大,导致数组越界或浪费大量空间。一定要先离散化。
-
离散化时下标从0开始:树状数组通常习惯下标从1开始,因为
sum(0)等于0,减少边界判断。注意映射时 +1。 -
重复数值处理:如果数组有重复数值,比如
[2, 2, 1],题目中i<j且a[i] > a[j]才构成逆序对,相等不算。离散化时相同的值应映射到同一个排名,这样sum(x)会算上所有等于x的已出现元素,greater = i - sum(x)自然排除了等于的情况。这是正确的。 -
树状数组的
sum和add实现错误:sum的循环条件是idx > 0,add的循环条件是idx <= n。lowbit实现:return x & -x。 -
数据类型溢出:逆序对数量可能很大(最多 n*(n-1)/2,当n=10^5时约5e9),要用 long long(C++)或 Python 的 int(自动大整数)。
-
测试边界:
- 空数组:返回0。
- 单个元素:返回0。
- 已排序递增数组:返回0。
- 完全逆序数组:比如
[5,4,3,2,1],逆序对数应该是 10。
生活中的更多例子
- 考试排名:老师想看看成绩排名和上次相比,有多少对同学名次发生了对调(即逆序)。比如上次成绩排序是[A,B,C],这次变成了[C,B,A],逆序对有 (C,B)、(C,A)、(B,A) 三对。
- 游戏排行榜:在游戏中,玩家按积分排序后,两个玩家交换了名次,就产生一个逆序对。逆序对总数可以衡量排行榜的“混乱程度”。
- 数组的相似度:两个排列(比如1~5的两种排序)之间的逆序对数可以用来度量它们的相似度,逆序对越少越相似(归并排序中也用逆序对来比较两个数组)。
延伸学习
- 归并排序求逆序对:另一种 O(n log n) 方法,也值得掌握,可以对比两种思路。
- 用树状数组求“顺序对”:即
i<j且a[i] < a[j],只需把代码中的greater改成less_or_equal并注意重复值的处理即可。 - 求每个元素前面的逆序个数:不只统计总数,还可以把每个
greater存入一个数组,得到每个元素前面有多少个比它大的元素。 - 二维偏序问题:逆序对是一维偏序的简单情况,二维偏序(如求满足
i<j且a[i] < a[j]且b[i] < b[j]的对数)可以用树状数组+离散化等更高级技巧解决。
试试自己手算一下 [2, 3, 1] 的逆序对个数吧(答案是2: (2,1) 和 (3,1))!用我们讲的方法,一步步模拟,看看你能不能得出同样的结果。
例题精讲
使用树状数组求逆序对时,如果数组元素的范围很大(例如1e9),通常需要先进行什么预处理?
使用树状数组求逆序对的时间复杂度是?
使用树状数组求逆序对时,通常的做法是:从右向左遍历数组,对于每个元素,先查询树状数组中小于该元素值的个数并累加到答案,然后将该元素插入树状数组。
若数组中含有重复元素,离散化时应当保持原顺序(即先出现的元素映射为较小的数值),这样在求逆序对时不会将相等元素误算为逆序对。
以下函数使用树状数组求逆序对,假设数组已离散化且下标从1开始,树状数组有query(pos)和update(pos,delta)两个操作。请填补空白处。
long long countInversions(vector<int>& a) {
int n = a.size();
vector<int> bit(n + 1, 0);
long long ans = 0;
for (int i = n - 1; i >= 0; i--) {
ans += ___;
update(a[i], 1);
}
return ans;
}