前言
上一篇我们学了线段树——它能处理区间查询和区间修改,是”万能选手”。但如果你的问题只需要单点修改 + 前缀查询呢?线段树 100 多行的代码是不是有点杀鸡用牛刀?
树状数组(Binary Indexed Tree, BIT / Fenwick Tree)就是为这个场景量身定制的。它只有不到 20 行的核心代码,却能在 时间内完成单点修改和前缀求和。
而且,很多问题可以巧妙地转化为”前缀查询”,所以 BIT 的适用范围远比你想象的广。逆序对计数就是经典案例。
问题的本质
前缀和的升级版
普通前缀和数组 查询,但修改一个元素要 更新所有后续的前缀和。
BIT 的巧妙之处:把前缀和拆成 段。每段覆盖一个”二进制区间”,修改时只需要更新 段。查询时也只需要合并 段。
lowbit:二进制的魔法
一个数 的 lowbit 是它二进制表示中最低位的 1 所代表的值。例如:
- ,lowbit =
- ,lowbit =
计算方法:lowbit(x) = x & (-x)。
BIT 中,位置 管辖的区间长度恰好是 。也就是说,tree[i] 负责区间 的和。
位置: 1 2 3 4 5 6 7 8
lowbit: 1 2 1 4 1 2 1 8
管辖: [1] [1,2] [3] [1,4] [5] [5,6] [7] [1,8]
注意:位置 4 管辖 (长度 4),位置 8 管辖 (长度 8)。
理论 + 代码
核心操作
前缀查询 sum(i):求 。
从 开始,不断把 tree[i] 累加,然后 i -= lowbit(i) 跳到上一段。每步减去 lowbit,所以最多 步。
int sum(int i) {
int s = 0;
for (; i > 0; i -= i & -i) // ① 每步减去 lowbit
s += tree[i];
return s;
}
单点修改 add(i, v):把 加上 。
从 开始,更新所有覆盖 的 tree 节点。每次 i += lowbit(i) 跳到下一个覆盖者。
void add(int i, int v) {
for (; i <= n; i += i & -i) // ② 每步加上 lowbit
tree[i] += v;
}
模拟走一遍
初始 tree 全 0,对长度为 8 的数组执行 add(3, 5), add(6, 2):
| 操作 | tree 变化 | 说明 |
|---|---|---|
| add(3,5) | tree[3]+=5, tree[4]+=5, tree[8]+=5 | 3→4→8 |
| add(6,2) | tree[6]+=2, tree[8]+=2 | 6→8 |
| sum(5) | tree[5]+tree[4] = 0+5 = 5 | 5→4→0 |
| sum(7) | tree[7]+tree[6]+tree[4] = 0+2+5 = 7 | 7→6→4→0 |
逆序对
逆序对:满足 且 的对数。
用 BIT 的做法:从右往左扫,对每个 ,查询”右边已经出现了多少个比 小的数”= sum(A_i - 1)。然后把 加入 BIT:add(A_i, 1)。
例题
例题 1:TB B59 — Number of Inversions
题目:给定 到 的排列 ,求逆序对数(满足 且 的对数)。
数据范围:
分析: 是 到 的排列,值域恰好 ,不需要离散化。从右往左扫,用 BIT 查”右边有多少个比 小的”。
代码:
#include <cstdio>
using namespace std;
const int MAXN = 150006;
int tree[MAXN], n;
void add(int i, int v) { for (; i <= n; i += i & -i) tree[i] += v; }
int sum(int i) { int s = 0; for (; i > 0; i -= i & -i) s += tree[i]; return s; }
int main() {
scanf("%d", &n);
int A[MAXN];
for (int i = 0; i < n; i++) scanf("%d", &A[i]);
long long ans = 0;
for (int i = n - 1; i >= 0; i--) {
ans += sum(A[i] - 1); // ① 查右边有几个比 A[i] 小的
add(A[i], 1); // ② 把 A[i] 加入 BIT
}
printf("%lld\n", ans);
return 0;
}
逐行解析:
- ①
sum(A[i]-1)查的是 BIT 中当前有多少个值 。因为我们从右往左扫,BIT 中存的都是 右边的元素。所以这查的就是”右边有多少个比 小的”。 - ② 每处理完一个元素就把它加入 BIT。
- 因为 是排列,值域 ,BIT 大小 就够了。
验证:。从右往左:
- i=3, A[3]=3: sum(2)=0, add(3,1). ans=0
- i=2, A[2]=1: sum(0)=0, add(1,1). ans=0
- i=1, A[1]=4: sum(3)=2, add(4,1). ans=2
- i=0, A[0]=2: sum(1)=1, add(2,1). ans=3 ✓
例题 2:ACL Practice B — Fenwick Tree
题目:长度为 的数组 。 个查询:
0 p x:1 l r:输出数据范围:,0-indexed
分析:BIT 的模板题。注意 0-indexed,需要统一转成 1-indexed。区间和 = sum(r) - sum(l)。
代码:
#include <cstdio>
using namespace std;
const int MAXN = 500006;
long long tree[MAXN];
int n;
void add(int i, long long v) { for (; i <= n; i += i & -i) tree[i] += v; }
long long sum(int i) { long long s = 0; for (; i > 0; i -= i & -i) s += tree[i]; return s; }
int main() {
int Q;
scanf("%d%d", &n, &Q);
for (int i = 0; i < n; i++) {
long long a;
scanf("%lld", &a);
add(i + 1, a); // ① 0-indexed 转 1-indexed
}
while (Q--) {
int t;
scanf("%d", &t);
if (t == 0) {
int p; long long x;
scanf("%d%lld", &p, &x);
add(p + 1, x); // ② 单点加
} else {
int l, r;
scanf("%d%d", &l, &r);
printf("%lld\n", sum(r) - sum(l)); // ③ 区间 [l, r) 的和
}
}
return 0;
}
逐行解析:
- ① BIT 是 1-indexed 的,而题目是 0-indexed,所以要
+1。 - ③
sum(r) - sum(l)= 的和 的和 = 的和。
例题 3:TB A59 — RSQ (Range Sum Queries)
题目:长度 的数组,初始全 0。 个查询:
1 pos x:(赋值,不是加)2 l r:输出数据范围:
分析:BIT 只支持”加”操作。赋值怎么办?维护当前值,add(pos, x - current[pos])。
代码:
#include <cstdio>
using namespace std;
const int MAXN = 100006;
long long tree[MAXN];
int cur[MAXN]; // 当前值
int n;
void add(int i, long long v) { for (; i <= n; i += i & -i) tree[i] += v; }
long long sum(int i) { long long s = 0; for (; i > 0; i -= i & -i) s += tree[i]; return s; }
int main() {
int Q;
scanf("%d%d", &n, &Q);
while (Q--) {
int t;
scanf("%d", &t);
if (t == 1) {
int pos; long long x;
scanf("%d%lld", &pos, &x);
add(pos, x - cur[pos]); // ① 差值更新
cur[pos] = x;
} else {
int l, r;
scanf("%d%d", &l, &r);
printf("%lld\n", sum(r - 1) - sum(l - 1)); // ② [l, r-1]
}
}
return 0;
}
逐行解析:
- ① BIT 只支持”加”操作,但题目要求”赋值”。所以维护当前值数组
cur,每次add(pos, x - cur[pos])来补差值。 - ② 区间 的和 =
sum(r-1) - sum(l-1)。注意题目查询2 l r查的是 到 。
例题 4(练习):T90 017 — Crossing Segments(★7)
题目:圆周上有 个点, 条线段(连接点 和 )。求有多少对线段在端点以外相交。
数据范围:
思路提示:这是一道 BIT 的高级应用(★7 难题)。圆上两条线段相交 ⟺ 它们的端点交错。关键转化:把线段 看成区间,两条线段相交当且仅当一个区间的端点恰好在另一个区间内(即 )。利用排序 + BIT 计数可以 解决。
参考文献
教材讲解 — 競技プログラミングの鉄則
系统练习 — 競技プログラミングの鉄則
实战练习 — 競プロ典型 90 問
模板练习 — ACL Practice Contest
系列索引
第零章 基础工具
第一章 搜索技术
第二章 数学基础
第三章 数据结构
- 03-01 栈、队列与单调栈
- 03-02 堆与优先队列
- 03-03 并查集
- 03-04 树状数组
- 03-05 线段树
- 03-06 懒标记线段树
- 03-07 Sparse Table 与倍增
- 03-08 字符串哈希
第四章 图论
- 04-01 图的遍历
- 04-02 最短路—Dijkstra 与 01-BFS
- 04-03 最短路—Bellman-Ford 与 Floyd
- 04-04 拓扑排序
- 04-05 最小生成树
- 04-06 强连通分量与 2-SAT
- 04-07 二分图与网络流
- 04-08 树上问题
第五章 动态规划
- 05-01 DP入门—状态与转移
- 05-02 背包问题族
- 05-03 LIS、LCS与编辑距离
- 05-04 区间DP
- 05-05 状态压缩DP
- 05-06 树形DP与数位DP
- 05-07 矩阵快速幂与线性递推
第六章 贪心
第七章 字符串
第八章 进阶