第 3.10 章:树状数组(BIT)
📝 前置条件: 了解前缀和(第 3.2 章)和位运算。本章与线段树(第 3.9 章)互补——BIT 代码更短,常数更小,但支持的操作更少。
树状数组(又名二进制索引树 / BIT)是竞赛编程中最常用的数据结构之一:不到 15 行代码,却能在 O(log N) 时间内支持单点更新和前缀查询。
3.10.1 核心思想:什么是 lowbit?
lowbit 的位运算原理
对任意正整数 x,lowbit(x) = x & (-x) 返回 x 的二进制表示中最低位 1 所代表的值。
x = 6 → 二进制:0110
-x = -6 → 补码:1010(按位取反 + 1)
x & (-x) = 0010 = 2 ← 最低位 1 对应 2^1 = 2
示例:
| x | 二进制 | -x(补码) | x & (-x) | 含义 |
|---|---|---|---|---|
| 1 | 0001 | 1111 | 0001 = 1 | 管理 1 个元素 |
| 2 | 0010 | 1110 | 0010 = 2 | 管理 2 个元素 |
| 3 | 0011 | 1101 | 0001 = 1 | 管理 1 个元素 |
| 4 | 0100 | 1100 | 0100 = 4 | 管理 4 个元素 |
| 6 | 0110 | 1010 | 0010 = 2 | 管理 2 个元素 |
| 8 | 1000 | 1000 | 1000 = 8 | 管理 8 个元素 |
BIT 树结构直觉
BIT 的精妙之处:tree[i] 不存储单个元素,而是存储一段区间的和,长度恰好是 lowbit(i)。
BIT 结构(n=8):每个 tree[i] 覆盖恰好 lowbit(i) 个以下标 i 结尾的元素。
查询 prefix(7) 的跳转路径(i -= lowbit(i) 向下跳):
💡 跳转规律: 查询时
i -= lowbit(i)(向下跳),更新时i += lowbit(i)(向上跳)。每次跳转消除最低位的 1,最多 log N 步。
下标 i: 1 2 3 4 5 6 7 8
tree[i] 管理的范围:
tree[1] = A[1] (长度 lowbit(1)=1)
tree[2] = A[1]+A[2] (长度 lowbit(2)=2)
tree[3] = A[3] (长度 lowbit(3)=1)
tree[4] = A[1]+...+A[4] (长度 lowbit(4)=4)
tree[5] = A[5] (长度 lowbit(5)=1)
tree[6] = A[5]+A[6] (长度 lowbit(6)=2)
tree[7] = A[7] (长度 lowbit(7)=1)
tree[8] = A[1]+...+A[8] (长度 lowbit(8)=8)
更新位置 3 的跳转路径(i += lowbit(i) 向上跳):
查询 prefix(7) 时,通过 i -= lowbit(i) 向下跳:
i=7:加tree[7](管理 A[7]),然后7 - lowbit(7) = 7 - 1 = 6i=6:加tree[6](管理 A[5..6]),然后6 - lowbit(6) = 6 - 2 = 4i=4:加tree[4](管理 A[1..4]),然后4 - lowbit(4) = 4 - 4 = 0,停止
共 3 步 = O(log 7) ≈ 3 步。
更新位置 3 时,通过 i += lowbit(i) 向上跳:
i=3:更新tree[3],然后3 + lowbit(3) = 3 + 1 = 4i=4:更新tree[4],然后4 + lowbit(4) = 4 + 4 = 8i=8:更新tree[8],8 > n,停止
3.10.2 单点更新 + 前缀查询——完整代码
📄 查看代码:3.10.2 单点更新 + 前缀查询——完整代码
// ══════════════════════════════════════════════════════════════
// 树状数组(BIT)—— 经典实现
// 支持:单点更新 O(log N),前缀和查询 O(log N)
// 数组必须使用 1-indexed(关键!)
// ══════════════════════════════════════════════════════════════
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 300005;
int n;
long long tree[MAXN]; // BIT 数组,1-indexed
// ── lowbit:返回最低位 1 的值 ──
inline int lowbit(int x) {
return x & (-x);
}
// ── 更新:给位置 i 加 val ──
// 向上遍历:i += lowbit(i)
// 覆盖位置 i 的每个祖先节点都被更新
void update(int i, long long val) {
for (; i <= n; i += lowbit(i))
tree[i] += val;
// 时间:O(log N) — 最多 log2(N) 次迭代
}
// ── 查询:返回前缀和 A[1..i] ──
// 向下遍历:i -= lowbit(i)
// 将 [1..i] 分解为 O(log N) 个不重叠的区间
long long query(int i) {
long long sum = 0;
for (; i > 0; i -= lowbit(i))
sum += tree[i];
return sum;
// 时间:O(log N) — 最多 log2(N) 次迭代
}
// ── 构建:从已有数组 A[1..n] 初始化 BIT ──
// 方法一:N 次单独更新 — O(N log N)
void build_slow(long long A[]) {
fill(tree + 1, tree + n + 1, 0LL);
for (int i = 1; i <= n; i++)
update(i, A[i]);
}
// 方法二:O(N) 构建(利用「直接父节点」关系)
void build_fast(long long A[]) {
for (int i = 1; i <= n; i++) {
tree[i] += A[i];
int parent = i + lowbit(i); // BIT 中的直接父节点
if (parent <= n)
tree[parent] += tree[i];
}
}
// 方法三:O(N) 构建(利用前缀和)
// 原理:tree[i] = sum(A[i-lowbit(i)+1 .. i])
// = prefix[i] - prefix[i - lowbit(i)]
void build_prefix(long long A[], long long prefix[]) {
// 先求前缀和
for (int i = 1; i <= n; i++) prefix[i] = prefix[i-1] + A[i];
// 利用前缀和直接计算每个节点
for (int i = 1; i <= n; i++)
tree[i] = prefix[i] - prefix[i - lowbit(i)];
}
// ── 完整示例 ──
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int q;
cin >> n >> q;
long long A[MAXN] = {};
for (int i = 1; i <= n; i++) cin >> A[i];
build_fast(A); // O(N) 初始化
while (q--) {
int type;
cin >> type;
if (type == 1) {
// 单点更新:A[i] += val
int i; long long val;
cin >> i >> val;
update(i, val);
} else {
// 前缀查询:A[1..r] 的和
int r;
cin >> r;
cout << query(r) << "\n";
}
}
return 0;
}
3.10.3 区间查询 = prefix(r) - prefix(l-1)
区间查询 sum(l, r) 与前缀和技术完全一致:
📄 区间查询 `sum(l, r)` 与前缀和技术完全一致:
// 区间求和:A[l..r] 的和
// 时间:O(log N) — 两次前缀查询
long long range_query(int l, int r) {
return query(r) - query(l - 1);
// query(r) = A[1] + A[2] + ... + A[r]
// query(l-1) = A[1] + A[2] + ... + A[l-1]
// 差值 = A[l] + A[l+1] + ... + A[r]
}
// 示例:
// A = [3, 1, 4, 1, 5, 9, 2, 6] (1-indexed)
// range_query(3, 6) = query(6) - query(2)
// = (3+1+4+1+5+9) - (3+1)
// = 23 - 4 = 19
// 验证:A[3]+A[4]+A[5]+A[6] = 4+1+5+9 = 19 ✓
3.10.4 对比:前缀和 vs BIT vs 线段树
| 操作 | 前缀和数组 | 树状数组(BIT) | 线段树 |
|---|---|---|---|
| 构建 | O(N) | O(N) 或 O(N log N) | O(N) |
| 前缀查询 | O(1) | O(log N) | O(log N) |
| 区间查询 | O(1) | O(log N) | O(log N) |
| 单点更新 | O(N) 重建 | O(log N) ✓ | O(log N) ✓ |
| 区间更新 | O(N) | O(log N)(差分 BIT) | O(log N)(懒惰标记) |
| 区间最小/最大 | O(1)(稀疏表) | ❌ 不支持 | ✓ 支持 |
| 代码复杂度 | 极简 | 简单(10 行) | 复杂(50+ 行) |
| 常数因子 | 最小 | 非常小 | 较大 |
| 空间 | O(N) | O(N) | O(4N) |
什么时候选 BIT?
- ✅ 只需前缀/区间和 + 单点更新
- ✅ 需要极简代码(竞赛中减少 bug)
- ✅ 逆序对计数、归并排序计数问题
- ❌ 需要区间最小/最大 → 用线段树
- ❌ 需要复杂区间操作(区间乘法等)→ 用线段树
3.10.5 交互式可视化:BIT 更新过程
3.10.6 区间更新 + 单点查询(差分 BIT)
标准 BIT 支持「单点更新 + 前缀查询」。利用差分数组技术,可以改为支持「区间更新 + 单点查询」。
原理
设差分数组 D[i] = A[i] - A[i-1](D[1] = A[1]),则:
A[i] = D[1] + D[2] + ... + D[i](即 A[i] 是 D 的前缀和)- 给 A[l..r] 全部加 val 等价于:
D[l] += val; D[r+1] -= val
📄 C++ 完整代码
// ══════════════════════════════════════════════════════════════
// 差分 BIT:区间更新 + 单点查询
// ══════════════════════════════════════════════════════════════
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 300005;
int n;
long long diff_bit[MAXN]; // 差分数组 D[] 上的 BIT
inline int lowbit(int x) { return x & (-x); }
// 更新差分 BIT 的位置 i:D[i] += val
void diff_update(int i, long long val) {
for (; i <= n; i += lowbit(i))
diff_bit[i] += val;
}
// 查询 A[i] = D[1..i] 的和 = 差分 BIT 的前缀查询
long long diff_query(int i) {
long long s = 0;
for (; i > 0; i -= lowbit(i))
s += diff_bit[i];
return s;
}
// 区间更新:给 A[l..r] 全部加 val
// 等价于:D[l] += val,D[r+1] -= val
void range_update(int l, int r, long long val) {
diff_update(l, val); // D[l] += val
diff_update(r + 1, -val); // D[r+1] -= val
}
// 单点查询:返回 A[i] 的当前值
// A[i] = D[1] + D[2] + ... + D[i] = prefix_sum(D, i)
long long point_query(int i) {
return diff_query(i);
}
进阶:区间更新 + 区间查询(双 BIT)
同时支持区间更新和区间查询,使用两个 BIT:
📄 同时支持区间更新和区间查询,使用两个 BIT:
// ══════════════════════════════════════════════════════════════
// 双 BIT:区间更新 + 区间查询
// 公式:sum(1..r) = B1[r] * r - B2[r]
// 其中 B1 是 D[] 上的 BIT,B2 是 (i-1)*D[i] 上的 BIT
// ══════════════════════════════════════════════════════════════
long long B1[MAXN], B2[MAXN];
inline int lowbit(int x) { return x & (-x); }
void add(long long* b, int i, long long v) {
for (; i <= n; i += lowbit(i)) b[i] += v;
}
long long sum(long long* b, int i) {
long long s = 0;
for (; i > 0; i -= lowbit(i)) s += b[i];
return s;
}
// 区间更新:给 A[l..r] 加 val
void range_add(int l, int r, long long val) {
add(B1, l, val);
add(B1, r + 1, -val);
add(B2, l, val * (l - 1)); // 补偿前缀公式
add(B2, r + 1, -val * r);
}
// 前缀和 A[1..r]
long long prefix_sum(int r) {
return sum(B1, r) * r - sum(B2, r);
}
// 区间和 A[l..r]
long long range_sum(int l, int r) {
return prefix_sum(r) - prefix_sum(l - 1);
}
3.10.7 USACO 风格题:用 BIT 统计逆序对
题目描述
统计逆序对(O(N log N))
给定长度为 N 的整数数组 A(元素不同,范围 1..N),统计逆序对的数量。
逆序对:一对下标 (i, j),满足 i < j 但 A[i] > A[j]。
样例输入:
5
3 1 4 2 5
样例输出:
3
解释: 逆序对是 (3,1)、(3,2)、(4,2),共 3 对。
解法:BIT 逆序对计数
📄 查看代码:解法:BIT 逆序对计数
// ══════════════════════════════════════════════════════════════
// 用树状数组统计逆序对 — O(N log N)
//
// 核心思路:
// 从左到右处理 A[i]。
// 对每个 A[i],以 A[i] 为右端点的逆序对数
// = 已处理过的值中大于 A[i] 的数量
// = (目前处理的元素数) - (已处理的 <= A[i] 的元素数)
// = i-1 - prefix_query(A[i])
// 对所有 i 求和即为总逆序对数。
//
// BIT 的作用:追踪已见过的值的频率。
// 见到值 v 后:update(v, +1)
// 查询 <= x 的值的数量:query(x)
// ══════════════════════════════════════════════════════════════
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 300005;
int n;
int bit[MAXN]; // 频率计数 BIT
inline int lowbit(int x) { return x & (-x); }
// 在位置 v 加 1(见到了值 v)
void update(int v) {
for (; v <= n; v += lowbit(v))
bit[v]++;
}
// 统计已见过的 [1..v] 中的值的数量
int query(int v) {
int cnt = 0;
for (; v > 0; v -= lowbit(v))
cnt += bit[v];
return cnt;
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
cin >> n;
ll inversions = 0;
for (int i = 1; i <= n; i++) {
int a;
cin >> a;
// 统计以 a 为右端点的逆序对:
// 已见过的值中大于 a 的数量
// = (目前已见 i-1 个元素) - (已见的 <= a 的数量)
int less_or_equal = query(a); // [1..a] 中已见的数量
int greater = (i - 1) - less_or_equal; // [a+1..n] 中已见的数量
inversions += greater;
// 标记我们已见过值 a
update(a);
}
cout << inversions << "\n";
return 0;
}
/*
对 A = [3, 1, 4, 2, 5] 的追踪:
i=1, a=3:已见=[],query(3)=0,greater=0-0=0。逆序对=0。update(3)。
i=2, a=1:已见=[3],query(1)=0,greater=1-0=1。逆序对=1。update(1)。
(3 > 1:1 个逆序对:(3,1) ✓)
i=3, a=4:已见=[3,1],query(4)=2,greater=2-2=0。逆序对=1。update(4)。
(没有已见的元素 > 4)
i=4, a=2:已见=[3,1,4],query(2)=1,greater=3-1=2。逆序对=3。update(2)。
(3>2 且 4>2:2 个逆序对:(3,2),(4,2) ✓)
i=5, a=5:已见=[3,1,4,2],query(5)=4,greater=4-4=0。逆序对=3。update(5)。
最终:3 ✓
*/
复杂度分析:
- 时间:O(N log N) —— N 次迭代,每次 O(log N) 的更新 + 查询
- 空间:O(N)(BIT)
扩展: 若数组元素不在 1..N 范围内,先做坐标压缩再使用 BIT:
📄 C++ 完整代码
// 任意值的坐标压缩
vector<int> A(n);
for (int i = 0; i < n; i++) cin >> A[i];
// 步骤一:排序去重
vector<int> sorted_A = A;
sort(sorted_A.begin(), sorted_A.end());
sorted_A.erase(unique(sorted_A.begin(), sorted_A.end()), sorted_A.end());
// 步骤二:将每个值替换为它的排名(1-indexed)
for (int i = 0; i < n; i++) {
A[i] = lower_bound(sorted_A.begin(), sorted_A.end(), A[i]) - sorted_A.begin() + 1;
// A[i] 现在在 [1..M],M = sorted_A.size()
}
// 现在用 n = sorted_A.size() 使用 BIT
3.10.8 常见错误
❌ 错误一:lowbit 实现有误
// ❌ 错误 — 常见笔误
int lowbit(int x) { return x & (x - 1); } // 这会清除最低位,而非返回它!
// x=6 (0110):x&(x-1) = 0110&0101 = 0100 = 4(错误,应为 2)
// ✅ 正确
int lowbit(int x) { return x & (-x); }
// x=6:-6 = ...11111010(补码)
// 0110 & 11111010 = 0010 = 2 ✓
记忆口诀: x & (-x) 读作「x 与负 x 相与」。-x 是按位取反加 1,保留最低位的 1,清除其下所有位,反转其上所有位,相与只保留最低位。
❌ 错误二:0-indexed 数组(0-indexed 陷阱)
BIT 必须使用 1-indexed 数组。0-indexed 会导致死循环!
// ❌ 错误 — 0-indexed 导致死循环!
// 如果 i = 0:query 循环:i -= lowbit(0) = 0 - 0 = 0 → 死循环!
// ✅ 正确 — 转换为 1-indexed
for (int i = 0; i < n; i++) {
update(i + 1, arr[i]); // 将 0-indexed 的 i 转换为 1-indexed 的 i+1
}
// 注意:对 0-indexed 范围 [l, r] 的查询用 query(r+1) - query(l)
❌ 错误三:大和的整数溢出
// ❌ 错误 — tree[] 对大和应该用 long long
int tree[MAXN]; // 和超过 2^31 时溢出
// ✅ 正确
long long tree[MAXN];
// 还有:统计逆序对时,逆序对数最多 N*(N-1)/2 ≈ 4.5×10^10(N=3×10^5)
// 结果计数器始终用 long long!
long long inversions = 0; // ✅ 不是 int!
❌ 错误四:多组测试数据间忘记清空 BIT
📄 查看代码:❌ 错误四:多组测试数据间忘记清空 BIT
// ❌ 错误 — 多组测试数据时
int T; cin >> T;
while (T--) {
// 忘记清空 tree[]!
// 上一组测试数据的旧数据污染结果
solve();
}
// ✅ 正确 — 每组测试数据前重置
int T; cin >> T;
while (T--) {
fill(tree + 1, tree + n + 1, 0LL); // 清空 BIT
solve();
}
3.10.9 本章总结
📋 公式速查
| 操作 | 代码 | 描述 |
|---|---|---|
| lowbit | x & (-x) | x 的最低位 1 的值 |
| 单点更新 | for(;i<=n;i+=lowbit(i)) t[i]+=v | 向上传播 |
| 前缀查询 | for(;i>0;i-=lowbit(i)) s+=t[i] | 向下分解 |
| 区间查询 | query(r) - query(l-1) | 差值公式 |
| 区间更新(差分 BIT) | upd(l,+v); upd(r+1,-v) | 差分数组 |
| 逆序对计数 | (i-1) - query(a[i]) | 处理每个元素时计数 |
| 数组必须 | 1-indexed | 0-indexed → 死循环 |
❓ 常见问题
Q1:BIT 和线段树都支持前缀和 + 单点更新,该选哪个?
A:尽可能用 BIT。BIT 只有 10 行代码,常数更小(实测快 2-3 倍),出错概率更低。只有需要区间最小/最大(RMQ)、区间赋色或更复杂区间操作时才选线段树。竞赛中,BIT 是「默认武器」,线段树是「重型火炮」。
Q2:BIT 能支持区间最小查询(RMQ)吗?
A:标准 BIT 不能支持 RMQ,因为最小值运算没有「逆运算」(无法像减法那样「撤销」一次最小合并)。区间最小/最大需要用线段树或稀疏表。有一种「静态 BIT RMQ」技术,但只在无更新情况下有效,实际用处有限。
Q3:BIT 能做二维(2D BIT)吗?
A:可以!二维 BIT 解决二维前缀和 + 单点更新问题,复杂度 O(log N × log M)。代码结构使用两层嵌套循环:
// 二维 BIT 更新 void update2D(int x, int y, long long v) { for (int i = x; i <= N; i += lowbit(i)) for (int j = y; j <= M; j += lowbit(j)) bit[i][j] += v; }USACO 中不常见,但偶尔会在二维坐标计数题中用到。
3.10.10 练习题
🟢 简单一:区间求和(单点更新) 给定长度为 N 的数组,支持两种操作:
1 i x:A[i] 加 x2 l r:查询 A[l] + A[l+1] + ... + A[r]
提示: BIT 的直接应用。用 update(i, x) 和 query(r) - query(l-1)。
🟢 简单二:小于 K 的元素个数 给定 N 次操作,每次要么插入一个整数(范围 1..10^6),要么查询「当前已插入的整数中有多少个 ≤ K?」
提示: BIT 维护值域上的频率数组。update(v, 1) 插入值 v,query(K) 是答案。
🟡 中等一:区间加法,单点查询 给定长度为 N 的数组(初始全零),支持两种操作:
1 l r x:给 A[l..r] 的每个元素加 x2 i:查询 A[i] 的当前值
提示: 使用差分 BIT(第 3.10.6 节)。
🟡 中等二:逆序对计数(含坐标压缩) 给定长度为 N 的数组,元素范围 1..10^9(可能有重复),统计逆序对数量。
提示: 先坐标压缩,再用 BIT 计数(第 3.10.7 节的变体)。注意相等元素:(i,j) 满足 i<j 且 A[i]>A[j](严格大于)才算逆序对。
🔴 困难:区间加法,区间求和(双 BIT) 给定长度为 N 的数组,支持两种操作:
1 l r x:给 A[l..r] 的每个元素加 x2 l r:查询 A[l] + ... + A[r]
提示: 用双 BIT。公式:prefix_sum(r) = B1[r] * r - B2[r],其中 B1 维护差分数组,B2 维护加权差分数组。
✅ 全部 BIT 练习题完整题解
🟢 简单一:区间求和
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
int n, q;
long long tree[MAXN];
int lowbit(int x) { return x & (-x); }
void update(int i, long long val) { for (; i <= n; i += lowbit(i)) tree[i] += val; }
long long query(int i) { long long s=0; for (; i > 0; i -= lowbit(i)) s += tree[i]; return s; }
int main() {
ios_base::sync_with_stdio(false); cin.tie(NULL);
cin >> n >> q;
while (q--) {
int t; cin >> t;
if (t == 1) { int i; long long x; cin >> i >> x; update(i, x); }
else { int l, r; cin >> l >> r; cout << query(r) - query(l-1) << "\n"; }
}
}
🟡 中等一:区间加法,单点查询(差分 BIT)
核心思路:在 BIT 中维护差分数组。range_add(l,r,x) = update(l,x) + update(r+1,-x)。单点查询 = query(i)。
void range_add(int l, int r, long long x) { update(l, x); update(r+1, -x); }
long long point_query(int i) { return query(i); }
🟡 中等二:逆序对计数
// 先坐标压缩,然后对每个元素 x:
// 逆序对 += (已插入的元素数) - query(压缩后的 x)
// 然后插入 x:update(压缩后的 x, 1)
🔴 困难:区间加法,区间求和(双 BIT)
// prefix_sum(r) = (r+1)*sum(D[1..r]) - sum(i*D[i], i=1..r)
// = (r+1)*B1.query(r) - B2.query(r)
// 其中 B1 存 D[i],B2 存 i*D[i]
struct DoubleBIT {
long long B1[MAXN], B2[MAXN];
int n;
DoubleBIT(int n) : n(n) { memset(B1,0,sizeof(B1)); memset(B2,0,sizeof(B2)); }
void add(int i, long long v) {
for (int x=i; x<=n; x+=x&-x) { B1[x]+=v; B2[x]+=v*i; }
}
void range_add(int l, int r, long long v) { add(l,v); add(r+1,-v); }
long long prefix(int i) {
long long s=0; for(int x=i;x>0;x-=x&-x) s+=(i+1)*B1[x]-B2[x]; return s;
}
long long range_query(int l, int r) { return prefix(r)-prefix(l-1); }
};
3.10.11 权值树状数组:全局第 k 小
权值 BIT 维护的是值域频率数组:bit[v] 表示值 v 在序列中出现了多少次。可以高效查询「序列中第 k 小的元素」。
朴素做法:二分 + 前缀查询,O(log² N)
📄 查看代码:朴素做法:二分 + 前缀查询,O(log² N)
// 在值域 [1..MAXV] 上的 BIT 中,找第 k 小的值
int kth_binary_search(int k) {
int lo = 1, hi = MAXV;
while (lo < hi) {
int mid = (lo + hi) / 2;
if (query(mid) >= k)
hi = mid;
else
lo = mid + 1;
}
return lo;
}
倍增优化:O(log N)
借助 BIT 的树形结构,倍增法可以在 O(log N) 内完成第 k 小查询:
📄 借助 BIT 的树形结构,倍增法可以在 O(log N) 内完成第 k 小查询:
// 全局第 k 小(倍增法)— O(log N)
// 前提:BIT 维护值域频率,bit[v] = v 的出现次数
int kth(int k) {
int sum = 0, x = 0;
// 从最高位开始,逐位确定答案
for (int i = (int)log2(MAXV); i >= 0; --i) {
int nx = x + (1 << i);
if (nx <= MAXV && sum + bit[nx] < k) {
x = nx; // 这一段全选,继续向右扩展
sum += bit[nx];
}
// 否则答案在 [x+1, x + 2^(i-1)] 范围内,不扩展
}
return x + 1; // x 是最后一个 sum < k 的位置,答案是 x+1
}
// 完整示例:动态维护序列,支持插入和第 k 小查询
// 插入值 v:update(v, 1)
// 删除值 v:update(v, -1)
// 查询第 k 小:kth(k)
💡 原理解析: BIT 的树形态使得
bit[x]正好是以 x 为根的子树之和(x 的二进制最低位之前的区间)。倍增时,每次尝试将 x 的某一位设为 1:若该位为 1 时的前缀和仍 < k,说明答案在右侧,就扩展;否则缩小在左侧查找。共 O(log V) 步。
💡 章节联系: BIT 和线段树是 USACO 中最常配合使用的两个数据结构。BIT 用 1/5 的代码量处理 80% 的场景。掌握 BIT 后,回到第 3.9 章学习线段树懒惰传播——那是 BIT 无法触达的领域。