一、从刚落幕的国际信息学奥林匹克说起
2026 年 8 月 9 日至 15 日,第 38 届国际信息学奥林匹克竞赛在乌兹别克斯坦首都塔什干举行,来自全球 90 多个国家和地区的近 400 名选手同场竞技。中国队四名选手斩获 3 金 1 银、团体总分第一,其中一位选手以 498.27 分成为本届最高分。截至本届,中国队累计金牌已突破 100 枚。
值得注意的是官方对赛题范围的描述:算法设计、数据结构与程序设计实现。"数据结构"被单独拎出来,不是偶然------在真实赛场上,同一道题用暴力和用合适的数据结构,往往就是 0 分和满分的差别。
今天要讲的 树状数组(Binary Indexed Tree,也叫 Fenwick Tree) 就是这样一件"性价比极高"的兵器:代码只有十来行,却能把很多 O(n\^2) 的统计问题压到 O(n \\log n)。我们用一道原创题把它彻底讲透。

二、原创练习题:科技节机器人列队
题目描述
学校科技节要举办机器人巡游表演。n 台机器人排成一列,从左到右第 i 台的高度为 a_i。导演希望它们按高度从矮到高(非递减)排列,这样队形看起来最整齐。
由于场地狭窄,机器人只能与相邻的那一台交换位置,每交换一次记为一步。
请回答三个问题:
- 问题 A:有多少组"高度倒置"的机器人?即满足 i \< j 且 a_i \> a_j 的下标对 (i, j) 有多少个。
- 问题 B:最少需要多少步相邻交换,才能让整列机器人变成非递减排列?
- 问题 C :对每一台机器人,输出它左边比它高的机器人数量。
输入格式
第一行一个整数 n。
第二行 n 个整数 a_1, a_2, \\dots, a_n。
输出格式
第一行输出问题 A 的答案(问题 B 的答案与它相同,原因见后文)。
第二行输出 n 个整数,为问题 C 的答案。
数据范围
0 \\le n \\le 5 \\times 10\^5,-10\^9 \\le a_i \\le 10\^9,高度可能重复。
样例输入
8
5 3 9 3 7 1 9 2
样例输出
15
0 1 0 2 1 5 0 6
样例解释
以第 8 台(高度 2)为例,它左边有 5、3、9、3、7、9 共 6 台比它高,所以第 8 个数是 6。把 8 个位置的贡献相加:0+1+0+2+1+5+0+6 = 15,恰好等于逆序对总数 15。这个"分解求和"的关系是本题的核心线索。
三、核心考点拆解
这道题看似只是数数,实际串起了五个高频考点。
考点 1:逆序对的定义与"相邻交换"的等价性
问题 A 求的就是标准的逆序对数量。问题 B 为什么答案完全一样?
关键观察:交换相邻两个元素,逆序对数量最多变化 1。
- 若 a_i \\le a_{i+1},交换后它们变成一个新的逆序对,总数 +1;
- 若 a_i \> a_{i+1},交换后这一对被消掉,总数 -1;
- 而对于其他任意一对元素 (x, y),交换相邻位置并不改变它们的前后相对次序,所以贡献不变。
排好序的数组逆序对为 0。每一步最多消掉 1 个逆序对,所以至少需要"逆序对个数"步;而冒泡排序恰好每次交换都消掉 1 个,能达到这个下界。结论:最少相邻交换次数 = 逆序对个数,这是一个必须记住的经典结论。
顺带一提:如果允许任意两个位置交换,答案就完全不同了(见进阶三),别混为一谈。
考点 2:为什么必须离散化
树状数组是以"值"为下标的桶。本题 a_i 可以到 10\^9,开这么大的数组直接爆内存。
离散化就是把值域压缩到 1 \\dots m(m \\le n 为不同值的个数),只保留大小关系:
- 复制一份数组,排序;
- 去重;
- 用二分查找(
lower_bound/bisect_left)把每个原值换成它在去重数组里的排名 +1(转成 1-based)。
排名保持了原有的大小顺序,因此所有比较结果不变。这一步同时顺手解决了负数和小数(先转排名即可)的问题。
考点 3:树状数组的两个动作
树状数组只做两件事,都靠 lowbit(i) = i & (-i) 驱动:
add(i, v) : 把位置 i 的值加上 v ------ 沿 i += lowbit(i) 往上跳
query(i) : 返回前缀和 [1..i] ------ 沿 i -= lowbit(i) 往下跳
tree[i] 管理的是区间 (i - \\text{lowbit}(i),\\ i\]。两个操作跳的次数都不超过 \\log n,所以单次都是 O(\\log n)。
在本题里,我们用它维护一个计数桶 :add(r, 1) 表示"排名 r 的高度出现了一次",query(r) 表示"目前已插入的元素中,排名 \\le r 的有多少个"。
考点 4:扫描方向决定统计口径
同一个树状数组,扫描方向不同,含义完全不同。这是最容易写错的地方。
| 目标 | 扫描方向 | 查询写法 | 含义 |
|---|---|---|---|
| 问题 A / B:逆序对总数 | 从右往左 | query(r - 1) |
右侧严格小于 a_i 的个数 |
| 问题 C:左边比它高的个数 | 从左往右 | i - query(r) |
已插入 i 个,减去 \\le a_i 的个数 |
注意问题 A 用 query(r - 1) 而不是 query(r):逆序对要求严格大于,相等的高度不算倒置,必须把等于 a_i 的那一档排除掉。这个"差一"是本题最高频的错误来源。
考点 5:答案的量级
n = 5 \\times 10\^5 且完全逆序时,逆序对数为 \\frac{n(n-1)}{2} \\approx 1.25 \\times 10\^{11},早已超过 32 位 int 上限 2147483647。必须用 long long(Python 无此顾虑)。这是本题最经典的"隐形丢分点"。

四、解法一:树状数组 + 离散化(主解)
思路一句话:从右往左扫,每遇到一个元素,先问"右边已经放进去了多少个比我矮的",再把自己放进桶里。
C++ 实现
cpp
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
int m; // 离散化后的值域大小
vector<int> tree_; // 树状数组,有效下标 1..m
void add(int i, int v) { // 单点加
for (; i <= m; i += i & (-i)) tree_[i] += v;
}
int query(int i) { // 前缀和 [1..i]
int s = 0;
for (; i > 0; i -= i & (-i)) s += tree_[i];
return s;
}
int main() {
int n;
if (scanf("%d", &n) != 1) return 0;
vector<int> a(n);
for (int i = 0; i < n; ++i) scanf("%d", &a[i]);
// ---- 离散化:把任意大小的高度压到 1..m ----
vector<int> s(a);
sort(s.begin(), s.end());
s.erase(unique(s.begin(), s.end()), s.end());
m = (int)s.size();
tree_.assign(m + 1, 0);
// ---- 问题 A / B:从右往左统计逆序对 ----
long long ans = 0; // 必须 long long
for (int i = n - 1; i >= 0; --i) {
int r = int(lower_bound(s.begin(), s.end(), a[i]) - s.begin()) + 1;
ans += query(r - 1); // 右侧严格小于 a[i] 的个数
add(r, 1);
}
printf("%lld\n", ans);
// ---- 问题 C:清空后从左往右 ----
tree_.assign(m + 1, 0);
for (int i = 0; i < n; ++i) {
int r = int(lower_bound(s.begin(), s.end(), a[i]) - s.begin()) + 1;
printf("%d%c", i - query(r), i + 1 == n ? '\n' : ' ');
add(r, 1);
}
return 0;
}
Python 实现
python
import sys
from bisect import bisect_left
def main():
data = sys.stdin.read().split()
if not data:
return
n = int(data[0])
a = list(map(int, data[1:1 + n]))
# ---- 离散化 ----
s = sorted(set(a))
m = len(s)
tree = [0] * (m + 1)
def add(i):
while i <= m:
tree[i] += 1
i += i & (-i)
def query(i):
t = 0
while i > 0:
t += tree[i]
i -= i & (-i)
return t
# ---- 问题 A / B:从右往左 ----
ans = 0
for x in reversed(a):
r = bisect_left(s, x) + 1
ans += query(r - 1) # 严格小于,所以是 r-1
add(r)
print(ans)
# ---- 问题 C:清空后从左往右 ----
tree = [0] * (m + 1)
res = []
for idx, x in enumerate(a):
r = bisect_left(s, x) + 1
res.append(idx - query(r)) # 已插入 idx 个,减去 <= x 的
add(r)
print(' '.join(map(str, res)))
main()
两份代码对样例均输出:
15
0 1 0 2 1 5 0 6
五、解法二:归并排序求逆序对(对拍神器)
逆序对还有一条完全不同的路:在归并排序合并两个有序段时顺手计数。
当左段 L[i] > R[j],说明 R[j] 比左段剩下的 L[i..] 全部 都小,一次性累加 len(L) - i 个逆序对。
python
def count_inversions(a):
def rec(arr):
if len(arr) <= 1:
return arr, 0
mid = len(arr) // 2
L, c1 = rec(arr[:mid])
R, c2 = rec(arr[mid:])
out, i, j, c = [], 0, 0, c1 + c2
while i < len(L) and j < len(R):
if L[i] <= R[j]: # 注意是 <=,相等不算逆序对
out.append(L[i]); i += 1
else:
out.append(R[j]); j += 1
c += len(L) - i # 关键一行
out.extend(L[i:]); out.extend(R[j:])
return out, c
return rec(list(a))[1]
两种解法的取舍:
| 树状数组 | 归并排序 | |
|---|---|---|
| 代码量 | 稍多(含离散化) | 稍少 |
| 是否需要离散化 | 需要 | 不需要 |
| 能否顺带回答问题 C | 能(换扫描方向) | 不方便 |
| 可扩展性 | 强(区间查询、第 k 小、二维偏序) | 弱(专用于逆序对) |
| 常数 | 小 | 递归开销略大 |
建议:赛场上写树状数组做主解,写归并排序做对拍验证。 两条思路互相独立,一旦答案不一致必有一方写错,比盯着代码看要高效得多。
六、复杂度分析
树状数组解法
- 时间:离散化排序 O(n \\log n) + n 次
add/query各 O(\\log n),共 O(n \\log n)。 - 空间:原数组 + 离散化数组 + 树状数组,共 O(n)。
归并排序解法:时间 O(n \\log n),空间 O(n)(递归栈 O(\\log n),合并临时数组 O(n))。
暴力枚举:O(n\^2)。在 n = 5 \\times 10\^5 时约 1.25 \\times 10\^{11} 次比较,必然超时;但它是最好的对拍基准,n \\le 2000 时可以放心用。
七、易错点清单
写这道题最容易踩的七个坑,逐条对照检查:
- 答案用了
int。1.25 \\times 10\^{11} 远超int上限,必须long long。这是本题第一大丢分点。 - 把
query(r - 1)误写成query(r)。逆序对要求严格大于,相等的高度不能算,必须用r - 1把等于 a_i 的那一档排除掉;写成r会在有重复元素时多算一批。 - 树状数组下标从 0 开始 。
lowbit(0) = 0,add(0, 1)会陷入死循环。离散化后的排名必须+1变成 1-based。 - 两次统计之间忘记清空树状数组 。问题 A 和问题 C 共用一棵树,中间必须
assign(m + 1, 0)重置,否则问题 C 的结果会掺进问题 A 的残留计数。 - 离散化忘记去重 。不去重会让 m 变成 n,虽然一般不至于出错,但相等元素的排名不唯一,
lower_bound的语义会变得混乱,容易连带写错第 2 条。 - 归并排序里写成
L[i] < R[j]。必须是<=,否则相等元素会被误算成逆序对。 - n = 0 未处理 。空输入时循环不执行、输出空行即可,但要保证
tree_大小至少为 1,sort/unique对空 vector 也要安全。
八、四个进阶方向
进阶一:树状数组倍增求第 k 小
树状数组不只能算前缀和,还能在 O(\\log n) 内直接定位"第 k 小的值",比"二分套 query"的 O(\\log\^2 n) 更快。
原理是从最高位往低位试着往右跳,能跳就跳、并把已经越过的计数从 k 里扣掉:
cpp
int LOG; // 满足 (1<<LOG) <= m 的最大值
int kth(int k) { // 返回第 k 小的排名下标(1..m)
int pos = 0;
for (int j = LOG; j >= 0; --j) {
int nxt = pos + (1 << j);
if (nxt <= m && tree_[nxt] < k) {
pos = nxt;
k -= tree_[nxt];
}
}
return pos + 1;
}
配合 add(r, 1) / add(r, -1) 就能维护一个支持插入、删除、查询第 k 小的动态排名集合------这是"权值树状数组"的标准玩法,很多"动态中位数""滑动窗口第 k 大"的题都靠它。
进阶二:逆序对奇偶性与数字华容道
逆序对的奇偶性是一个排列的不变量:任意一次相邻交换都会让它 \\pm 1,也就是奇偶性翻转一次。
这条性质能直接判定经典的 15 数码(数字华容道)是否可解:把空格看作最大数,计算整个排列的逆序对奇偶性,再结合空格所在行号,就能在不搜索的情况下断定目标状态可达与否。判定只需 O(n\^2) 或 O(n \\log n),比盲目跑 BFS 省下天量时间。
进阶三:换成"任意交换",答案完全不同
如果放开限制,允许交换任意两个位置(元素互不相同),最少交换次数变成:
\\text{答案} = n - (\\text{置换环的个数})
做法是把"当前位置 → 排序后该元素应在的位置"看成一个置换,找出所有环,每个长度为 L 的环需要 L - 1 次交换。
对比一下差别有多大:数组 [3, 2, 1] 的逆序对是 3 (相邻交换要 3 步),但任意交换只需 1 步(直接换 3 和 1)。读题时务必看清是"相邻交换"还是"任意交换",这是同类题最常见的陷阱设置。
进阶四:从单点修改升级到区间修改区间查询
原始树状数组是"单点改 + 前缀查"。用两棵树状数组 B_1、B_2 配合差分,就能支持"区间加 + 区间求和":
- 区间 \[l, r\] 加 v:在 B_1 上做
add(l, v)、add(r+1, -v);在 B_2 上做add(l, v*(l-1))、add(r+1, -v*r)。 - 前缀和 \[1, i\] 查询:i \\times \\text{query}*{B_1}(i) - \\text{query}*{B_2}(i)。
这样一来,很多"必须上线段树"的题目就能用两个十行函数解决,代码短、常数小、调试快。如果还要处理"区间内逆序对个数"这类问题,再往上就是莫队算法或树套树,可以作为下一阶段的目标。
九、小结与互动
回顾一下这道题给出的完整链条:
- 问题转化:把"最少相邻交换次数"识别为"逆序对计数"(考点 1 的等价性证明);
- 离散化:把 10\^9 的值域压到 O(n),让"以值为下标"成为可能;
- 树状数组 :用
lowbit两个方向的跳跃,O(\\log n) 完成单点加与前缀和; - 扫描方向:右→左统计逆序对,左→右统计前缀贡献,同一份数据结构两种口径;
- 对拍验证:归并排序 + 暴力枚举双重兜底,避免"差一"和溢出这类沉默错误。
本文所有代码在发布前都做过验证:Python 的树状数组、归并排序、暴力三套解法在 3000 组随机数据(含空数组、全相等、完全逆序、含负数、大值域等边界)上答案完全一致;C++ 版与 Python 版对样例输出逐字符相同;进阶一的倍增求第 k 小在 2000 组随机计数分布上与暴力扫描一致;进阶三的置换环公式与小规模 BFS 最短交换次数一致。
留几个问题给你动手:
- 如果题目改成"求满足 i \< j 且 a_i \> 2 \\times a_j 的对数",树状数组的写法要怎么改?(提示:查询边界不再是 r-1,需要另做一次二分)
- 问题 C 若改成"右边比它矮的个数",扫描方向和查询式子各是什么?
- 试着把进阶四的两棵树状数组写出来,用随机数据和暴力前缀和对拍,看能不能一次通过。
如果这篇讲解帮你理清了树状数组的思路,欢迎点赞收藏;对哪个考点还有疑问,评论区留言,下一篇挑最多人问的展开细讲。
下一篇预告:倍增思想的另一大应用------树上最近公共祖先(LCA),以及它如何把树上路径查询降到 O(\\log n)。
📚 免费少儿编程资料(夸克网盘领取)
以下资料来自夸克网盘分享,点击链接可直接保存;若需在 App 内打开,也可复制下方明文链接:
- 全国青少年信息素养大赛复赛集训题目Python&C++.docx
https://pan.quark.cn/s/93995d3cb150 - 2024信息素养-智能算法应用挑战赛-复赛初中组题目7月7日.pdf
https://pan.quark.cn/s/da97b5dbf75d - Python背记手册.pdf
https://pan.quark.cn/s/7568ae9ca92b - Python课程
https://pan.quark.cn/s/a94bf02d00c6 - 2024信息素养大赛图形化复赛集训题答案3-9
https://pan.quark.cn/s/6ccab7ec3cbc - 2025年03月份电子学会考级真题
https://pan.quark.cn/s/4403c4228912 - 2025全国青少年信息素养大赛赛项说明
https://pan.quark.cn/s/d9d0df4a9f29 - 青少儿信息素养大赛编程资料
https://pan.quark.cn/s/4ab6bd83be8a
资料持续更新,关注获取最新分享。