树状数组
树状数组(Fenwick Tree / Binary Indexed Tree, BIT)是一种维护前缀和的数据结构,支持在 O(log n) 时间内进行单点更新和前缀查询。凭借其代码量小、常数极小的优势,它是竞赛和实际开发中的利器。以下是树状数组的常见用法及对应的C++实现。
0. 基础模板
核心操作:
add(i, delta): 将位置 i 的元素增加 delta,同时维护所有受影响的树状数组节点。sum(i): 查询前缀和 [1, i]。range_sum(l, r): 查询区间和 [l, r] = sum(r) - sum(l-1)。
注意: 树状数组的下标通常从 1 开始,因此需要将输入数据映射到 1-indexed。
template<typename T>
struct BIT {
int n;
vector<T> tree;
BIT(int n) : n(n), tree(n + 1, 0) {}
void add(int i, T delta) {
while (i <= n) {
tree[i] += delta;
i += i & -i;
}
}
T sum(int i) {
T res = 0;
while (i > 0) {
res += tree[i];
i -= i & -i;
}
return res;
}
T range_sum(int l, int r) { return sum(r) - sum(l - 1); }
};
1. 单点修改,区间查询
这是树状数组最经典的应用场景。
操作:
add(pos, val)将原数组a[pos]的值增加val。query(l, r)返回a[l..r]的元素和。
BIT<int> bit(n);
bit.add(x, v); // a[x] += v
int ans = bit.range_sum(l, r);
2. 区间修改,单点查询
利用差分思想,将区间修改转化为单点修改。
设差分数组 d[i] = a[i] - a[i-1],则 a[pos] = Σ d[1..pos]。
对区间 [l, r] 加上 val,只需 d[l] += val; d[r+1] -= val。
BIT<int> bit(n);
// 区间 [l, r] 全部加 val
bit.add(l, val);
bit.add(r + 1, -val);
// 查询 a[pos] 的当前值
int cur = bit.sum(pos);
3. 区间修改,区间查询
同样是基于差分,但需要维护两个树状数组。
推导:
Σa[1..p] = Σ(i=1..p) Σ(j=1..i) d[j] = (p+1) * Σd[1..p] - Σ(i*d[i])。
因此需要两个 BIT,一个维护 d[i],另一个维护 i * d[i]。
区间修改操作:对 d[l] += val, d[r+1] -= val;
同时 i*d[i] 的 BIT 也要做相应加法。
区间查询 [l, r] = prefix(r) - prefix(l-1)。
struct RangeBIT {
int n;
BIT<long long> bit1, bit2; // bit1: d[i], bit2: i*d[i]
RangeBIT(int n) : n(n), bit1(n), bit2(n) {}
void range_add(int l, int r, long long val) {
bit1.add(l, val);
bit1.add(r + 1, -val);
bit2.add(l, l * val);
bit2.add(r + 1, -(r + 1) * val);
}
long long prefix_sum(int p) {
return (p + 1) * bit1.sum(p) - bit2.sum(p);
}
long long range_sum(int l, int r) {
return prefix_sum(r) - prefix_sum(l - 1);
}
};
4. 求逆序对
树状数组可以 O(n log maxVal) 求逆序对数。
基本思路: 将数组元素离散化(保证值域 1~n),从左到右扫描,每遇到一个数 x,查询树状数组中大于 x 的已出现元素个数(即 query(n) - query(x)),然后将 x 加入树状数组。
int count_inversions(vector<int>& a) {
int n = a.size();
// 离散化
vector<int> sorted = a;
sort(sorted.begin(), sorted.end());
sorted.erase(unique(sorted.begin(), sorted.end()), sorted.end());
for (int& v : a) v = lower_bound(sorted.begin(), sorted.end(), v) - sorted.begin() + 1;
BIT<int> bit(n);
long long ans = 0;
for (int i = 0; i < n; ++i) {
ans += bit.range_sum(a[i] + 1, n); // 比 a[i] 大的个数
bit.add(a[i], 1);
}
return ans;
}
5. 维护权值树状数组(第 k 小)
如果我们要动态查询集合中第 k 小的数(k 从 1 开始),可以使用权值树状数组二分查找。
利用树状数组的节点累加特性,通过倍增快速定位到第 k 小的位置。
复杂度: O(log N)。
int find_kth(BIT<int>& bit, int k) {
int n = bit.n, idx = 0, bitmask = 1 << (int)log2(n);
while (bitmask) {
int t = idx + bitmask;
if (t <= n && bit.tree[t] < k) {
idx = t;
k -= bit.tree[t];
}
bitmask >>= 1;
}
return idx + 1; // 1‑indexed
}
// 用法:bit 记录每个值出现的次数,动态 insert/remove。
6. 二维树状数组
支持在二维平面上进行单点更新和子矩阵求和。
操作: add(x, y, delta) 更新 (x,y) 的权值,query(x, y) 查询从 (1,1) 到 (x,y) 的子矩阵和。
通过容斥可得任意子矩阵和。
struct BIT2D {
int n, m;
vector<vector<int>> tree;
BIT2D(int n, int m) : n(n), m(m), tree(n + 1, vector<int>(m + 1, 0)) {}
void add(int x, int y, int delta) {
for (int i = x; i <= n; i += i & -i)
for (int j = y; j <= m; j += j & -j)
tree[i][j] += delta;
}
int sum(int x, int y) {
int res = 0;
for (int i = x; i > 0; i -= i & -i)
for (int j = y; j > 0; j -= j & -j)
res += tree[i][j];
return res;
}
int query(int x1, int y1, int x2, int y2) {
return sum(x2, y2) - sum(x1 - 1, y2) - sum(x2, y1 - 1) + sum(x1 - 1, y1 - 1);
}
};
7. 树上问题(DFS 序 + 树状数组)
将树按 DFS 序展开,子树对应一个连续区间,用树状数组维护区间和。
适用场景: 子树加法、子树求和,或链上操作(需配合树链剖分)。
实现: 第一次 DFS 记录每个节点的 in[x](进入时间戳)和 out[x](离开时间戳),子树节点区间为 [in[x], out[x]]。
vector<vector<int>> adj;
vector<int> in, out;
int timer = 0;
void dfs(int u, int fa) {
in[u] = ++timer;
for (int v : adj[u])
if (v != fa) dfs(v, u);
out[u] = timer;
}
// 对子树 x 全体加 val
bit.range_add(in[x], out[x], val); // 需配合区间修改BIT
// 查询节点 x 的当前值
int val = bit.sum(in[x]); // 单点查询
9. 其他常见扩展用法
- 离线处理区间不同值个数:将询问按右端点排序,树状数组维护每种颜色最后一次出现的位置(经典HH项链问题)。
- 差分数组的 K 次多项式前缀和:涉及组合数的高阶前缀和需结合公式推导,常用多个 BIT 实现。
- 三维偏序(CDQ分治):树状数组常作为内层查询结构。
- 维护集合中大于/小于某个值的元素和(权值BIT):与第 k 小类似,可直接查询。
💡 总结
树状数组的核心是 单点更新和前缀查询,所有扩展都基于这两点通过数学变换得出。它的优点是常数极小、代码短,但功能不如线段树灵活。在面对需要动态维护前缀信息的场景时,BIT 往往是首选方案。
所有函数均封装在结构体中,使用时注意 下标从 1 开始,并根据需要将 int 改为 long long。

浙公网安备 33010602011771号