树状数组求逆序对

问题定义

逆序对(Inversion)指满足 i < j 且 a[i] > a[j] 的数对 (a[i], a[j])。暴力两两比较是 O(n²),而”离散化 + 树状数组“可以在 O(n log n) 内完成统计,思路与归并排序并列的两大经典解法之一。

算法思路

把”统计逆序对”转化为”动态统计比当前元素小的个数”:

  1. 离散化:若数值范围大(如 10⁹)或含负数,先排序去重,把每个值映射为 1..n 的排名,保持大小关系不变;
  2. 从右向左遍历:每遇到 a[i],先查询树状数组中小于 a[i] 的元素个数(这些元素都在 i 的右侧),累加到答案;
  3. 插入update(a[i], 1),把当前元素放入树状数组,供左侧元素统计。

关键点:从右向左保证查询时树状数组里只包含右侧已扫描的元素;query(a[i]-1) 统计的是严格小于 a[i] 的个数(用 query(a[i]) 会把相等元素也算进去,重复元素时出错)。

C++ 实现


#include <vector>
#include <algorithm>
using namespace std;

class FenwickTree {
private:
    vector<int> tree;
    int lowbit(int x) { return x & -x; }
public:
    FenwickTree(int n) : tree(n + 1, 0) {}
    void update(int x, int delta) {
        while (x < (int)tree.size()) {
            tree[x] += delta;
            x += lowbit(x);
        }
    }
    int query(int x) {
        int res = 0;
        while (x > 0) {
            res += tree[x];
            x -= lowbit(x);
        }
        return res;
    }
};

long long countInversions(vector<int>& nums) {
    // 1. 离散化
    vector<int> sorted = nums;
    sort(sorted.begin(), sorted.end());
    sorted.erase(unique(sorted.begin(), sorted.end()), sorted.end());
    for (int& num : nums)
        num = lower_bound(sorted.begin(), sorted.end(), num) - sorted.begin() + 1;

    // 2. 从右向左统计
    FenwickTree ft(nums.size());
    long long res = 0;                     // 用 long long!n 可达 1e5,答案可能爆 int
    for (int i = nums.size() - 1; i >= 0; --i) {
        res += ft.query(nums[i] - 1);      // 右侧比 nums[i] 小的个数
        ft.update(nums[i], 1);             // 插入当前元素
    }
    return res;
}

示例:[3, 1, 4, 2]

离散化后 [2, 1, 3, 4],从右向左:

  • i=3(值 4):query(3) = 0,累计 0,插入 4;
  • i=2(值 2):query(1) = 1(只有 1 比 2 小),累计 1,插入 2;
  • i=1(值 1):query(0) = 0,插入 1;
  • i=0(值 3):query(2) = 1(1 比 3 小),累计 2。

总逆序对 = 2,对应 (3,1)(4,2),正确。

复杂度

  • 离散化:O(n log n)(排序 + 去重);
  • 树状数组操作:每次 update/query O(log n),共 O(n log n);
  • 总复杂度 O(n log n),空间 O(n)。

扩展

  • 求每个元素的逆序数:遍历时把每次 query(a[i]-1) 的结果存入数组即可;
  • 重复元素:离散化保留重复值,用 query(a[i]-1) 仍然只统计严格小于的个数,正确性不受影响;
  • 与归并排序对比:归并排序离线 O(n log n)、无需离散化;BIT 方案支持在线动态插入(如排行榜实时统计)。看场景选。

实战体会

两种遍历方向本质等价。从右向左是”统计右边比自己小的”,从左向右则是”统计左边比自己大的”——query(a[i]-1)i - query(a[i]) 是同一类查询的对称写法。刷题时固定一种即可,省得每次现推。

离散化顺带解决了负数。BIT 下标要求非负,离散化映射到 1..n 后负数也自然可用。实际工作里给排行榜算”前面有多少人分数比我高”用的就是这个套路,支持在线插入,这是归并排序做不到的。

重复元素是翻车高发点。求”严格小于”必须用 query(a[i]-1) 而非 query(a[i]),否则相等元素被多算。面试里这个细节常用来区分”背过模板”和”真正理解”。

别忘了 long long。n=10⁵ 时逆序对数最多约 5×10⁹,int 会溢出——这是我第一次提交 WA 的原因。

滚动至顶部