← 目录 第 46 / 60 节 全部 60 节

46 · 树状数组

用 lowbit 把前缀和切成 log n 段,让“改一个点”和“查一段和”都变成 O(log n)。一份 12 行的代码,换三种下标含义,就能做单点改区间和、区间改单点查、逆序对,再顺手学会在树状数组上二分。
提高-预计 65 分钟树状数组逆序对区间修改
本节出现的新函数 / 新语法(先看懂再用)

第 18 节的前缀和有个硬伤:改一个数,后面所有 s[i] 全部作废,重建一次 O(n)。而比赛中“单点修改 + 区间询问”的组合出现得极频繁——数据在变,问题却还是那一个。这一节解决的就是这个矛盾:让改一个点和查一段和都只要 O(log n),代价是 12 行代码。

树状数组的全部秘密在一句话里:t[i] 管辖的区间长度恰好是 lowbit(i)。看不懂这句话,就只是在背模板。
操作朴素数组前缀和树状数组
单点修改 a[i] += vO(1)O(n) 重建O(log n)
前缀和 a[1..i]O(n)O(1)O(log n)
区间和 a[l..r]O(n)O(1)O(log n)
区间加 + 单点查O(n)O(n)O(log n)(差分)
区间最大值O(n)不支持不支持
代码量3 行5 行12 行

前缀和的思路是“把 a[1..i] 压成一个数”,一旦中间某个值变了,这个压缩就废了。树状数组换了一种压法:按 i 的二进制位来压,每个二进制位对应一段。于是改一个点只会动到 O(log n) 段,求一个前缀和也只需要拼 O(log n) 段。以 n = 8 为例:t[1]=a[1],t[2]=a[1..2],t[3]=a[3],t[4]=a[1..4],t[6]=a[5..6],t[8]=a[1..8]。那么 sum(6) = t[6] + t[4] = a[5..6] + a[1..4],两段拼完,一步没多。

先看清两块骨头:lowbit(怎么算管辖长度)和三个函数的骨架(怎么跳下标)。这两块想通了,后面四种用法只是换一下“下标代表什么”。

新函数拆解 · lowbit(取最低位的 1)
int lowbit(int x) { return x & -x; }
组成含义 / 格式要求
x & -x把 x 与它的相反数按位与。结果是 x 的二进制里最低位那个 1所代表的值。
-x 从哪来补码:-x = ~x + 1,先把 x 每一位取反,再 +1,于是最低位的 1 被完整保留下来。
三个例子lowbit(6) = lowbit(110₂) = 10₂ = 2;lowbit(8) = 8;lowbit(7) = 1。
x 必须 ≥ 1x = 0 时 x & -x = 0,i += lowbit(i) 就变成原地踏步 → 死循环 TLE。
在树状数组里的两个用途① t[i] 的管辖长度 = lowbit(i);② i += lowbit(i) 跳到“下一个管着我的节点”,i -= lowbit(i) 跳到“左边还没累加的那一段的右端”。
最小可运行示例
示例
#include <bits/stdc++.h>
using namespace std;

int main() {
    for (int x = 1; x <= 10; ++x)
        cout << x << " -> " << (x & -x) << "\n";
    // 1->1  2->2  3->1  4->4  5->1  6->2  7->1  8->8  9->1  10->2
    return 0;
}
注意

规律:2 的幂返回自己,奇数返回 1,其余返回“最低位 1 的权值”。

别自己造写法(比如 x - (x & (x - 1)) 虽然也对),标准写法就这一个,读别人代码也认这个。

新函数拆解 · 树状数组(add / sum 两个函数)
void add(int i, int v); int sum(int i);
组成含义 / 格式要求
int t[N]树状数组本体,放全局(自动清零、不占栈)。t[i] 存 a[i−lowbit(i)+1 .. i] 的和。
add(i, v)执行 a[i] += v。从 i 出发,每次 i += lowbit(i),把 v 加到每一个“管着 i”的节点上。
sum(i)返回 a[1]+...+a[i]。从 i 出发,每次 i -= lowbit(i),把沿途的 t[i] 全部累加。
区间和sum(r) − sum(l−1),和前缀和一模一样:两个前缀相减。
下标从 1 开始lowbit(0) = 0 会让循环停不下来。题目是 1-based 就直接用;0-based 要整体 +1。
复杂度两个函数都是 O(log n):下标每走一步,二进制里至少少一个 1 或多进一位。
最小可运行示例
示例
#include <bits/stdc++.h>
using namespace std;

int n = 8, t[10];                                 // t[i] 管 lowbit(i) 个位置
int a[9] = {0, 3, 1, 4, 1, 5, 9, 2, 6};

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

void add(int i, int v) {
    for (; i <= n; i += lowbit(i)) t[i] += v;      // 所有管着 i 的节点都要改
}

int sum(int i) {
    int s = 0;
    for (; i > 0; i -= lowbit(i)) s += t[i];       // 一路往左上跳,把大段拼起来
    return s;
}

int main() {
    for (int i = 1; i <= n; ++i) add(i, a[i]);     // 建树:n 次单点加
    cout << sum(5) << "\n";                        // 3+1+4+1+5 = 14
    cout << sum(7) - sum(2) << "\n";               // a[3..7] = 4+1+5+9+2 = 21
    add(3, 10);                                    // a[3] 从 4 变成 14
    cout << sum(5) << "\n";                        // 24
    return 0;
}
注意

t 的意义是“和”,所以 add 里是 +=;如果维护的是“出现次数”,add(i, 1) 就够。

建树也可以 O(n):先 t[i] = a[i],再让 i 从 1 到 n 执行 t[i + lowbit(i)] += t[i]。n 大时省一个 log,但 n 次 add 在 5×10⁵ 下也只要 10⁷ 次加法。

add 和 sum 的循环条件完全不同:add 往上(i 变大),sum 往左上(i 变小)。写反了必然错。

先拿洛谷的模板题把它跑起来。这道题把“单点修改”和“区间求和”放在一起,正好对上树状数组的两个函数。

例题 · 【模板】树状数组 1洛谷 P3374普及/提高-去洛谷做这题 ↗

给定一个长度为 n 的数列,需要进行 m 次操作,操作有两种:

1 x k:把第 x 个数加上 k;2 x y:输出区间 [x, y] 内每个数的和。

数据范围:1 ≤ n, m ≤ 5×10⁵,数列中的数与 k 的绝对值不超过 10⁵。

输入格式:第一行两个整数 n、m;第二行 n 个整数;接下来 m 行,每行三个整数,表示一次操作。
输出格式:对每个操作 2,输出一行一个整数。

输入: 5 5 1 5 4 2 3 1 1 3 2 2 5 1 3 -1 1 4 2 2 1 4 输出: 14 16
先算清楚暴力为什么会死

最朴素的写法:数组存原值。修改 O(1),查询把 [x,y] 扫一遍 O(n)。最坏情况 m 次全是查询,n = m = 5×10⁵ 时是 2.5×10¹¹ 次加法,几十秒起步,直接 TLE。

换成前缀和:查询 O(1),但每次修改都要把 s[x..n] 重算一遍,还是 O(n),一样死。两种做法的毛病是同一个——它们把“一段”压成了“一个数”,改一点就得整体重来。

树状数组的做法:不再压成一个数,而是压成 log n 个数。sum(5) 只拼 t[5] + t[4](因为 5 = 101₂,去掉最低位 1 得 4,再去掉得 0);add(5, v) 只改 t[5]、t[6]、t[8]、t[16]…(5 → 6 → 8 → 16,每次进位)。两头都只碰 log n 个位置,总复杂度 O(m log n) ≈ 5×10⁵ × 19 ≈ 10⁷,稳过。

为什么这样切一定能覆盖完整?因为二进制分解是唯一的:i 减去 lowbit(i) 相当于删掉最低位的 1,反复做就得到 i 的二进制各位,而每一位恰好对应一段连续区间,这些区间首尾相接、正好拼成 [1, i],既不漏也不重。

建树别忘:代码里用 n 次 add 建树,是 O(n log n);n = 5×10⁵ 时约 10⁷ 次加法,可以接受。要更快就线性建树(见上一张函数卡的注意)。

数值:本题答案在 int 内,但把 t 和 sum 写成 long long 也只占 4 MB(比 int 多 2 MB),以后遇到大数值不用再回来改一遍。

完整可编译代码(C++17)
#include <bits/stdc++.h>
using namespace std;

const int N = 500005;
int n, m;
long long t[N];                       // t[i] 管 a[i-lowbit(i)+1 .. i] 这一段

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

void add(int i, long long v) {
    for (; i <= n; i += lowbit(i))    // 往上跳:把所有"管着 i"的节点都改掉
        t[i] += v;
}

long long sum(int i) {
    long long s = 0;
    for (; i > 0; i -= lowbit(i))     // 往左上跳:把几个大段拼成前缀
        s += t[i];
    return s;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m;
    for (int i = 1; i <= n; ++i) {    // 建树:读一个数就当成一次单点加
        int x;
        cin >> x;
        add(i, x);
    }

    while (m--) {
        int op, x, y;
        cin >> op >> x >> y;
        if (op == 1) add(x, y);                    // 第 x 个数加 y
        else cout << sum(y) - sum(x - 1) << "\n";  // 区间和 = 两个前缀之差
    }
    return 0;
}
逐行拆解
代码片段含义与理由
long long t[N]全局数组自动清零。开 5×10⁵ 个 long long 是 4 MB,任何题都不会超内存。
for (; i <= n; i += lowbit(i))add 的循环。i 从待改位置往上爬,爬到的每一个 i 都有一段包含原位置,所以都要改。
i += lowbit(i) 走一遍 55 → 6 → 8 → 16 → … 每次把最低位的 1 往上进一位,最多 log₂n 步就超过 n。
for (; i > 0; i -= lowbit(i))sum 的循环。i 每次删掉最低位的 1,把 t[i] 这一段拿走,剩下的交给更小的 i。
i -= lowbit(i) 走一遍 66 → 4 → 0。对应 a[5..6] + a[1..4],正好是 a[1..6]。
sum(y) - sum(x - 1)区间和转成两个前缀和相减。为什么是 x−1 不是 x:sum(x) 包含 a[x],而 a[x] 在答案里。
add(i, x) 建树读入 a[i] 时直接在 i 位置做一次单点加,树就建好了,不需要额外的 build 函数。
while (m--)循环 m 次,m 归零后自动结束。注意 m 被用掉了,后面不能再当数据范围的界。

第二种用法只改一件事:让树状数组去管差分数组。于是“区间加”变成两次单点加,“单点查”变成一个前缀和——正好是 add 和 sum。

例题 · 【模板】树状数组 2洛谷 P3368普及/提高-去洛谷做这题 ↗

给定一个长度为 n 的数列,需要进行 m 次操作,操作有两种:

1 x y k:把区间 [x, y] 内每个数加上 k;2 x:输出第 x 个数的值。

数据范围:1 ≤ n, m ≤ 5×10⁵,所有输入与答案都在 64 位整数范围内。

输入格式:第一行两个整数 n、m;第二行 n 个整数,表示初始数列;接下来 m 行,每行为上述两种操作之一。
输出格式:对每个操作 2,输出一行一个整数。

输入: 5 5 1 5 4 2 3 1 2 4 2 2 3 1 1 5 -1 1 3 5 7 2 4 输出: 6 10
把“区间加”翻译成“两次单点加”

差分数组的定义:d[1] = a[1],d[i] = a[i] − a[i−1]。它的好处是 a[x] = d[1] + d[2] + … + d[x]——原数组的值就是差分数组的前缀和。

于是区间加 [l, r] 全部加 k,在差分里只动两个位置:d[l] += k(从此往后都多了 k),d[r+1] −= k(从 r+1 起把多出来的还回去)。中间那些 d 值全不变。

这样一来,两个操作正好对上树状数组的两个函数:区间加 = 两次 add,单点查 = 一次 sum。都是 O(log n)。

边界:r = n 时 d[n+1] 根本不会被任何前缀和用到,而 add(n+1, −k) 的循环条件 i <= n 立刻不成立,什么也不做。所以那句越界判断可以不写——但你要知道它为什么安全。

建树:把 d[i] = a[i] − a[i−1] 逐个 add 进去,就完成了初值。

数值:差分值带负号,k 也可能很大,t、add 的参数、sum 的返回值统一用 long long。

完整可编译代码(C++17)
#include <bits/stdc++.h>
using namespace std;

const int N = 500005;
int n, m;
long long a[N], t[N];                 // t 维护的是差分数组 d

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

void add(int i, long long v) {
    for (; i <= n; i += lowbit(i)) t[i] += v;
}

long long sum(int i) {
    long long s = 0;
    for (; i > 0; i -= lowbit(i)) s += t[i];
    return s;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m;
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
        add(i, a[i] - a[i - 1]);      // 差分 d[i] 直接进树状数组
    }

    while (m--) {
        int op;
        cin >> op;
        if (op == 1) {
            int l, r;
            long long k;
            cin >> l >> r >> k;
            add(l, k);                // 从 l 起,后面都多 k
            add(r + 1, -k);           // 从 r+1 起还回去(r=n 时这行自动空转)
        } else {
            int x;
            cin >> x;
            cout << sum(x) << "\n";   // 原数组第 x 个值 = 差分的前缀和
        }
    }
    return 0;
}
逐行拆解
代码片段含义与理由
add(i, a[i] - a[i - 1])读入时就地差分。a[0] 默认为 0,所以 a[1] − a[0] = a[1],正好是 d[1] 的定义。
add(l, k)差分数组第 l 位加 k,含义是“a[l..n] 都加 k”。
add(r + 1, -k)第 r+1 位减 k,把上面多加的那部分收回去,于是实际生效的只有 [l, r]。
r = n 时为什么安全add 的循环条件是 i <= n,传入 n+1 时条件一开始就不成立,函数直接返回,不会碰 t[n+1]。
sum(x)d[1..x] 的和 = a[x]。这就是“单点查询”,一次前缀和搞定。
long long a[N]a[i] 在修改后可能很大,虽然本题只用来算初值,统一用 long long 免掉类型转换的坑。

第三种用法最反直觉:让树状数组的下标不是位置,而是数值本身。这时 t 存的是“某个值出现了几次”,前缀和 sum(v) 就是“值 ≤ v 的元素有多少个”。一旦有了这个能力,逆序对就变成了一行加法。

逆序对:从左往右扫,问“前面有多少个比我大”

逆序对是满足 i < j 且 a[i] > a[j] 的有序对。换个说法:把每个数当成主角,数一数它前面有多少个数比它大,全部加起来,就是答案。

扫到第 i 个数时,前面有 i−1 个数。其中 ≤ a[i] 的有 sum(a[i]) 个(因为 sum(v) 数的是“值 ≤ v 的出现次数”),剩下的自然全都严格大于 a[i],就是这一位的贡献:(i − 1) − sum(a[i])。

所以每读一个数,做两件事:先查 sum(a[i]) 算贡献,再改 add(a[i], 1) 把“这个值出现过一次”记进去。查询和修改各 O(log n),总共 O(n log n)。

顺序不能反:先 add 再 sum,会把自己算进“前面”,答案偏大。

下标要求:树状数组的下标必须是 1..n 的整数,所以这个写法只在 1 ≤ a[i] ≤ n 时能直接用。值域到 10⁹ 时必须先离散化(把数值换成名次),节末的挑战题就是这件事。

答案规模:最坏情况下所有数两两构成逆序对,共 n(n−1)/2 个。n = 5×10⁵ 时约 1.25×10¹¹,int 装不下,必须是 long long。

逆序对(值域 ≤ n 的版本)
#include <bits/stdc++.h>
using namespace std;

const int N = 500005;
int n, a[N], t[N];                    // t 的下标是"值",不是"位置"

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

void add(int i, int v) {
    for (; i <= n; i += lowbit(i)) t[i] += v;
}

int sum(int i) {
    int s = 0;
    for (; i > 0; i -= lowbit(i)) s += t[i];
    return s;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 1; i <= n; ++i) cin >> a[i];   // 保证 1 <= a[i] <= n

    long long ans = 0;
    for (int i = 1; i <= n; ++i) {
        ans += (i - 1) - sum(a[i]);   // 前面 i-1 个里,≤ a[i] 的有 sum(a[i]) 个
        add(a[i], 1);                 // 再把 a[i] 记进去:这个值多出现了一次
    }
    cout << ans << "\n";
    return 0;
}
逐行拆解
代码片段含义与理由
int n, a[N], t[N]这里的 n 同时是“元素个数”和“值域上界”,所以 t 的下标范围刚好够。
ans += (i - 1) - sum(a[i])i−1 是前面已有的元素个数,sum(a[i]) 是其中 ≤ a[i] 的个数,相减就是严格大于的个数。
为什么用 sum(a[i]) 不是 sum(a[i] - 1)sum(a[i]) 把“等于 a[i]”的也扣掉了,剩下来的才是严格大于,正好符合逆序对的定义。
add(a[i], 1)把“值 a[i] 出现了一次”写进树状数组。维护的是计数,所以加 1。
先 sum 后 add自己此刻还没进树,不会被 sum 数到。顺序颠倒会让每个数至少多算一次。
long long ans答案上限 n(n−1)/2 ≈ 1.25×10¹¹,远超 int 的 2.1×10⁹。

第四种用法把树状数组当成一棵“二叉搜索树”:在 O(log n) 内找到最小的 p 使 sum(p) ≥ k。答案如果靠二分,就是 O(log²n),而树状数组自己就能二分。

原理:树状数组的节点正好构成一棵“按二进制分段”的树。从 p = 0 出发,从大到小试 step = 2²⁰, 2¹⁹, …, 1:如果 t[p + step] < k,说明前 p+step 个数的和还不够 k,答案在右边,就跳过去,并把 k 减去这一段的和;否则停在原地。走完 log n 步,p 就是“最后一个前缀和 < k 的位置”,答案是 p+1。

树状数组上二分(前提:所有数非负,前缀和单调)
#include <bits/stdc++.h>
using namespace std;

int n = 8, t[10];                       // 用 t 记"每个位置出现了几次"

int lowbit(int x) { return x & -x; }
void add(int i, int v) { for (; i <= n; i += lowbit(i)) t[i] += v; }

int kth(int k) {                        // 最小的 p,使 sum(p) >= k
    int p = 0;
    for (int step = 1 << 20; step; step >>= 1)      // 起点取一个不小于 n 的 2 的幂
        if (p + step <= n && t[p + step] < k) {     // 这一段还不够,就整段吃掉
            p += step;
            k -= t[p];                              // t[p] 正是刚吃掉那一段的和
        }
    return p + 1;
}

int main() {
    add(2, 1); add(5, 2); add(7, 1);                // 位置 2 一个、位置 5 两个、位置 7 一个
    for (int k = 1; k <= 4; ++k) cout << kth(k) << " ";   // 2 5 5 7
    cout << "\n";
    return 0;
}
逐行拆解
代码片段含义与理由
step 从 2²⁰ 开始必须从“不小于 n 的 2 的幂”开始:整段跳跃靠的就是二进制拼位,起点换成 n(n 未必是 2 的幂)就拼不回去了。
p + step <= n越界保护。step 一开始远大于 n,这个判断会把它挡掉。
t[p + step] < kt[p+step] 存的是 (p+step−lowbit, p+step] 这一段的和。它 < k 意味着答案不在这段左边。
k -= t[p]把刚跨过的这一段和从 k 里扣掉,问题变成“在剩下的后缀里找第 k 个”。
return p + 1循环结束时 p 是最后一个 sum(p) < k 的位置,所以第一个够 k 的位置是 p+1。
所有数必须非负前缀和单调才有“第一个 ≥ k 的位置”这种说法。有负数时这个二分直接失效,不是慢,是错。
四种用法,其实是同一份代码

树状数组唯一不变的是骨架:lowbit、add 往上跳、sum 往左上跳。变的是下标代表什么,以及t 里存什么。四种用法对照如下。

用法下标代表t 存什么查询是什么
单点改 + 区间和位置一段位置的和前缀和相减
区间加 + 单点查位置差分数组的一段和前缀和 = 原值
逆序对 / 计数数值(名次)该值出现的次数前缀和 = 值 ≤ v 的个数
上二分位置一段位置的和不用 sum,直接跳

什么时候别用树状数组。它只能维护可差分的运算:加法(有相反数)、异或(自己是自己的逆)、模意义下的乘法(有逆元)。区间最值不行——max(1..r) 没法“减掉” max(1..l−1) 还原出 max(l..r),因为求最大值没有逆运算。要区间最值就得上线段树,下一节。

逆序对还有一个等价身份:冒泡排序的交换次数、插入排序的元素移动次数、把序列排成升序所需的最少相邻交换次数,三者都等于逆序对数。看到“相邻交换”四个字要想得起这一行。

反过来扫也一样。求“每个数右边比它小的个数”,从右往左扫:每步贡献是 已插入个数 − sum(a[i] − 1),再把 a[i] 记进树里。

这一节的 6 个坑,按症状记

下标从 0 开始:lowbit(0) = 0,add 里 i += 0 永远不动 → TLE,表现为程序卡死、一个字符都不输出。原题 1-based 就直接用,0-based 记得整体 +1。

差分区间加漏了 add(r + 1, -k),或者把 +k 和 −k 写反 → WA。症状很典型:前几个数对,从 l 之后全部偏大(或偏小),而且差值恰好是 k 的倍数。

逆序对的 ans 用 int:n = 5×10⁵ 时答案可达 1.25×10¹¹,溢出后变成负数 → WA,而且小数据完全正常,只有大测试点挂。

把 ans += (i-1) - sum(a[i]) 写成 ans += i - sum(a[i]):多算了自己 → WA,答案恰比如正确答案大 n。

用树状数组求区间最大值:sum(r) - sum(l-1) 那套逻辑对 max 不成立,代码能编译能跑,样例甚至可能过 → WA。这类“逻辑不成立”的错最难查,因为不报错。

多组数据忘记清空 t:第 2 组开始答案全错 → WA。只要 t 是全局数组,多测就在每组开头 memset(t, 0, sizeof t)。

自己动手复现

关掉页面,把 P3374 的 30 行从零敲一遍(不看书),提交确认样例输出 14 和 16。

改一改①:把 add 的循环改成 for (; i > 0; i -= lowbit(i)),先预测再跑。你会发现 add(1, x) 只改了很少几个位置,sum(5) 立刻不对——上行下行的方向不能反。

改一改②:在 add 里加一句 cout << i << ' ';,调用 add(5, 1),看到 5 6 8 16,这就是“进位”的形状。

改一改③:在 P3368 里删掉 add(r + 1, -k),用样例跑一遍,观察第 2 个输出的变化——一半的区间被多加了,这正是“忘记减回去”。

数学补给 · lowbit 与 log n 到底从哪来

把 n = 8 时每个节点的管辖范围列出来,规律一眼可见:

i二进制lowbit(i)t[i] 管辖下一个管 i 的节点 i+lowbit(i)
111a[1..1]2
2102a[1..2]4
3111a[3..3]4
41004a[1..4]8
51011a[5..5]6
61102a[5..6]8
71111a[7..7]8
810008a[1..8]16

为什么 x & -x 能取出最低位的 1。补码下 -x = ~x + 1:取反把最低位 1 变成 0、把它右边的 0 全变成 1,再 +1 让右边的 1 全部进位回 0,只有最低位那个 1 被完整地还原。所以 x & -x 只留下它,其余全 0。

sum 为什么是 O(log n)。每步 i -= lowbit(i) 至少消掉一个二进制 1。2¹⁸ = 262144 ≤ n < 2¹⁹ = 524288 时,i 最多 19 位,所以最多 19 步。n = 5×10⁵ 落在这一档,一次 sum 最多 19 次加法。

add 为什么也是 O(log n)。i += lowbit(i) 是把最低位的 1 往前推一位,下标最多涨到 2 倍,19 步之内必定超过 n。一次 add 也是最多 19 次加法。

两边合起来:一次操作 ≤ 19 次加法,5×10⁵ 次操作 ≈ 10⁷ 次,实际运行 0.1 秒量级。这就是为什么树状数组的常数比线段树小得多——它连一次递归都没有。

不能做最值的根源是“没有逆运算”。sum 能相减是因为加法有相反数;max 若要“相减”,等于要从 max(l..r) 和 max(1..l−1) 反推出 max(1..r),信息不足。所以区间最值必须让每个节点存“自己那一段的最值”,查询时把 O(log n) 段的答案合并——这正是线段树。

举一反三 · 想清楚这些再往下走
  1. 把维护的信息从“和”换成“异或”,add 和 sum 各改哪几处?为什么异或可以而 max 不行?(提示:异或的“逆运算”是谁?)
  2. 逆序对那一题,如果要输出“每个位置 i 左边比它大的个数”而不是总和,代码要改哪一行?空间要不要多加一个数组?
  3. 二维版:n × m 的网格,支持单点加和子矩阵求和。t 该开成几维?add 和 sum 各有几层循环?复杂度是多少?
  4. 树状数组上二分的 step 为什么必须从“≥ n 的 2 的幂”开始?如果从 n 开始,第一个被尝试的位置是 n,会漏掉或重复哪一段?
  5. 区间加 并且 区间求和,能不能只用树状数组做?需要几个?(提示:把 sum(l..r) 用差分的前缀和展开成两个可维护的量)
  6. 如果一道题有 5×10⁵ 次单点修改,但只有 10 次区间查询,你还会用树状数组吗?用什么更划算?反过来呢?
自测:能全部回答再进下一节
挑战题 · 逆序对
洛谷 P1908普及/提高-去洛谷做这题 ↗

给定一个长度为 n 的正整数序列。若 i < j 且 aᵢ > aⱼ,则称 (i, j) 是一个逆序对。求序列中逆序对的个数。

输入格式:第一行一个整数 n;第二行 n 个整数,表示序列。
输出格式:一行一个整数,表示逆序对个数。

数据范围:1 ≤ n ≤ 5×10⁵,1 ≤ aᵢ ≤ 10⁹。答案最大约 n(n−1)/2 ≈ 1.25×10¹¹,需用 64 位整数。

引导性提示(先只看这里)

值到 10⁹,下标放不下;但 n 只有 5×10⁵——能当下标的只有“排名”,不是数值本身。

从左往右扫。读到第 i 个数时,只需要回答一句:前面已经出现过的数里,有多少个严格比它大?

把“某个排名出现了几次”存进树状数组,前缀和就是“不超过某排名的元素个数”。

想看思路了?点开完整思路

离散化。把 a 拷贝一份排序去重,再用 lower_bound 把每个 a[i] 换成它在去重数组里的名次 1..tot。大小关系不变,而值域被压到 n 以内,可以当下标。O(n log n)。

树状数组的含义换成计数。下标是名次,t 里存“这个名次出现了几次”。于是 sum(rk) = 已经出现过的、名次 ≤ rk 的元素个数。

逐位统计。扫到第 i 个数时,前面已经有 i−1 个;其中 ≤ a[i] 的有 sum(rk) 个,剩下 (i−1) − sum(rk) 个都严格大于它,每一个都构成逆序对,累加。

然后把它记进去。add(rk, 1)。先查后改,不能颠倒。

复杂度与数值。离散化 O(n log n),统计 O(n log n),n = 5×10⁵ 时约 2×10⁷ 次操作。ans 用 long long:最坏 1.25×10¹¹。相同元素不会互相构成逆序对,所以查的是 sum(rk)(含等号)。

还是不会?点开题解代码与拆解
参考代码
#include <bits/stdc++.h>
using namespace std;

const int N = 500005;
int n, a[N], b[N], t[N];              // t 的下标是"名次"

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

void add(int i) {                     // 名次 i 的出现次数 +1
    for (; i <= n; i += lowbit(i)) ++t[i];
}

int sum(int i) {                      // 名次 <= i 的元素出现了几个
    int s = 0;
    for (; i > 0; i -= lowbit(i)) s += t[i];
    return s;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 1; i <= n; ++i) { cin >> a[i]; b[i] = a[i]; }

    sort(b + 1, b + n + 1);                          // 离散化:排序
    int tot = unique(b + 1, b + n + 1) - b - 1;      // 去重后值域缩到 1..tot

    long long ans = 0;
    for (int i = 1; i <= n; ++i) {
        int rk = lower_bound(b + 1, b + tot + 1, a[i]) - b;   // a[i] 的名次
        ans += (i - 1) - sum(rk);    // 前面 ≤ 它的有 sum(rk) 个,其余都严格大于它
        add(rk);                     // 再把它的名次记进去
    }
    cout << ans << "\n";
    return 0;
}
关键片段拆解
片段作用
b[i] = a[i]; sort(b + 1, b + n + 1)拷贝一份排序,原数组 a 不能动,后面还要按原顺序扫。
unique(b + 1, b + n + 1) - b - 1去重后返回到新末尾的迭代器,减去首地址 b 再 −1 就是不同值的个数 tot。重复值共用一个名次,正好符合逆序对“严格大于”的语义。
lower_bound(b + 1, b + tot + 1, a[i]) - b找 a[i] 在去重数组里的位置,得到 1..tot 的名次。注意右端是 b + tot + 1,区间左闭右开。
ans += (i - 1) - sum(rk)把“前面有多少个比我大”算出来。sum(rk) 数的是名次 ≤ rk 的,含相等元素。
add(rk)计数型树状数组:只 +1,不传 v。先查后改,自己不会被算进 sum。
i <= n 的循环上界名次只到 tot ≤ n,用 n 当上界完全安全,t 不会越界。
这题最容易错的地方
  • 离散化时忘记去重,直接用 lower_bound 在未去重的数组里查:名次不唯一,重复元素的计数会错 → WA。
  • ans 用 int:答案到 10¹¹,溢出成负数 → WA,小数据全对,大数据全挂。
  • 把 sum(rk) 改成 sum(rk - 1):相等的元素被算成“比它大”,有重复值时答案偏大。
  • 先 add(rk) 再统计:自己也被数进去,每个数贡献少 1 或不稳定 → WA。
  • 数组只开到 10⁵(看 n 是 5×10⁵ 却没改):越界 → RE 或随机 WA。三个数组都要 5×10⁵ 以上。
延伸阅读 / 参考