先理解:tree[i] 到底维护哪一段?
lowbit(i) 表示二进制下最低位的 1 对应的值。它决定了 tree[i] 负责的区间长度。
例如 i=6,二进制是 110,所以 lowbit(6)=2,于是 tree[6] 维护 [5,6]。
int lowbit(int x) { return x & -x; }
可视化:tree[i] 维护的区间
选择一个 tree[i],看它覆盖原数组中的哪一段。
6 的二进制是 110,最低位的 1 代表 2;tree[6] = a[5] + ... + a[6]。
当前高亮:tree[6] 覆盖 a[5] 到 a[6]。
单点加:为什么是 i += lowbit(i)?
当 a[x] 增加 k 时,所有“包含 x 的 tree 区间”都要更新。沿着 x, x+lowbit(x), ... 往上跳,刚好会经过这些负责区间。
void add(int x, int k) {
for (int i = x; i <= n; i += lowbit(i)) {
tree[i] += k;
}
}
可视化:单点加更新哪些区间
每次访问的 tree[i] 都是一段包含 a[x] 的区间,所以这些节点都要加上 k。
tree[6] 维护 [5,6],tree[8] 维护 [1,8],它们都包含 a[6]。
更新路径:tree[6] -> tree[8]。
前缀查询:为什么是 i -= lowbit(i)?
查询 query(x) 时,我们把 [1,x] 拆成若干个 tree 已经维护好的区间。从 x 开始,每次拿走 tree[i] 负责的一段,然后跳到左边剩余部分。
int query(int x) {
int ans = 0;
for (int i = x; i; i -= lowbit(i)) {
ans += tree[i];
}
return ans;
}
可视化:query 拆成哪些区间
查询前缀和时,访问到的每个 tree[i] 正好贡献一个不重不漏的区间。
从 i=6 开始,每次加入 tree[i],再令 i -= lowbit(i)。
query(6) 会拆成 [5,6] + [1,4]。
区间加 + 单点查询:维护差分数组
如果树状数组里维护的是差分数组 d[i] = a[i] - a[i-1],那么 a[x] 就等于 d[1] + ... + d[x]。
于是给区间 [l,r] 整体加 k,只需要让 d[l] += k,再让 d[r+1] -= k。查询单点值时,直接求差分前缀和。
add(i, a[i] - a[i - 1]);
void range_add(int l, int r, int k) {
add(l, k);
add(r + 1, -k);
}
int point_query(int x) {
return query(x);
}
可视化:区间加如何变成两个单点加
下面演示 [l,r] += k 如何只修改差分数组的两个位置,以及单点查询如何通过前缀和还原。
原数组 a[i]
差分数组 d[i]
树状数组 tree[i] 维护 d[]
区间 [3,6] += 2,会变成 add(3,2) 和 add(7,-2)。
差分含义:a[x] = d[1] + ... + d[x],所以 point_query(x) = query(x)。