GESP 8级
树状数组
8级 · 高级数据结构与DP
树状数组(Binary Indexed Tree / Fenwick Tree)是一种支持单点修改和前缀和查询的数据结构,两种操作都只要 O(log n)。它比普通数组的前缀和 O(n) 快得多,比线段树代码短得多,是竞赛中的"瑞士军刀"。
💡 这是什么?
问题回顾:你有一个数组 a[1..n],需要频繁做两种操作:
• 修改:把 a[i] 的值加上 delta
• 查询:求 a[1]+a[2]+...+a[x](前缀和)
如果用普通数组,修改 O(1) 但前缀和要 O(n);如果用前缀和数组,前缀和 O(1) 但修改要 O(n)。树状数组两种操作都是 O(log n)!
核心秘诀:lowbit
lowbit(x) = x 的二进制表示中,最低位的 1 及其后面的 0 构成的值
例如:lowbit(6) = lowbit(110₂) = 2
计算方法:lowbit(x) = x & (-x)(x 和 x 的补码做按位与)
• 修改:把 a[i] 的值加上 delta
• 查询:求 a[1]+a[2]+...+a[x](前缀和)
如果用普通数组,修改 O(1) 但前缀和要 O(n);如果用前缀和数组,前缀和 O(1) 但修改要 O(n)。树状数组两种操作都是 O(log n)!
核心秘诀:lowbit
lowbit(x) = x 的二进制表示中,最低位的 1 及其后面的 0 构成的值
例如:lowbit(6) = lowbit(110₂) = 2
计算方法:lowbit(x) = x & (-x)(x 和 x 的补码做按位与)
🌟 为什么重要?
• 逆序对计数:树状数组的经典应用,统计比当前数大的已出现的数
• 区间修改+单点查询:差分数组 + 树状数组,可以高效处理
• 代码极短:核心只有10行左右,竞赛中省时省力
• 很多问题的基础:二维树状数组、离散化+树状数组等进阶技巧
• GESP 8级必考:是理解线段树的前置知识
• 区间修改+单点查询:差分数组 + 树状数组,可以高效处理
• 代码极短:核心只有10行左右,竞赛中省时省力
• 很多问题的基础:二维树状数组、离散化+树状数组等进阶技巧
• GESP 8级必考:是理解线段树的前置知识
📋 前置知识(学这个之前你需要知道)
1. 前缀和(GESP 2-3级):理解"前缀和可以快速求区间和"的概念
2. 二进制基础(GESP 2级):位运算 &, |, <<, >>,特别是 x & (-x) 的含义
3. 数组基础:1-indexed 数组(树状数组通常从1开始编号)
注意:树状数组不需要学树的知识!它虽然叫"树状",但本质是利用二进制的巧妙数组。
2. 二进制基础(GESP 2级):位运算 &, |, <<, >>,特别是 x & (-x) 的含义
3. 数组基础:1-indexed 数组(树状数组通常从1开始编号)
注意:树状数组不需要学树的知识!它虽然叫"树状",但本质是利用二进制的巧妙数组。
📐 lowbit 的含义(二进制视角)
lowbit(x) = x & (-x)
例子:
x=8 = 1000₂ → lowbit(8) = 8
x=6 = 0110₂ → lowbit(6) = 2
x=12 = 1100₂ → lowbit(12) = 4
x=7 = 0111₂ → lowbit(7) = 1
x=1 = 0001₂ → lowbit(1) = 1
tree[x] 管辖的区间长度 = lowbit(x)
tree[x] 存的是 a[x-lowbit(x)+1] 到 a[x] 的和
例子:
x=8 = 1000₂ → lowbit(8) = 8
x=6 = 0110₂ → lowbit(6) = 2
x=12 = 1100₂ → lowbit(12) = 4
x=7 = 0111₂ → lowbit(7) = 1
x=1 = 0001₂ → lowbit(1) = 1
tree[x] 管辖的区间长度 = lowbit(x)
tree[x] 存的是 a[x-lowbit(x)+1] 到 a[x] 的和
📐 两个核心操作
前缀和查询 query(x):从 x 开始,每次 x -= lowbit(x),累加 tree[x]
query(7) = tree[7] + tree[6] + tree[4]
(7→6→4→0 停止)
单点修改 update(x, val):从 x 开始,每次 x += lowbit(x),tree[x] += val
update(3, 5) → tree[3]+=5, tree[4]+=5, tree[8]+=5
(3→4→8 停止)
query(7) = tree[7] + tree[6] + tree[4]
(7→6→4→0 停止)
单点修改 update(x, val):从 x 开始,每次 x += lowbit(x),tree[x] += val
update(3, 5) → tree[3]+=5, tree[4]+=5, tree[8]+=5
(3→4→8 停止)
💻 完整代码(带详细注释)
#include <iostream>
using namespace std;
const int N = 500010;
int tree[N]; // 树状数组,tree[i] 管辖某个区间的和
int n; // 数组大小
// lowbit(x):x 的二进制中最低位的1及后面的0构成的值
// 例如:lowbit(6) = lowbit(110₂) = 2
int lowbit(int x) {
return x & (-x);
// x & (-x) 的原理:
// -x 在二进制中等于 x 取反再加1
// 与 x 做按位与后,只有最低位的1保留
}
// 单点修改:把原数组第 x 个元素加上 val
// 同时更新所有管辖范围包含 x 的 tree 节点
void update(int x, int val) {
for (; x <= n; x += lowbit(x)) {
// 每次跳到"父节点":加上 lowbit(x)
tree[x] += val;
}
// 为什么这样就能更新?tree[x] 管辖 [x-lowbit(x)+1, x]
// x += lowbit(x) 跳到管辖更大区间的祖先节点
}
// 前缀和查询:求 a[1]+a[2]+...+a[x]
int query(int x) {
int sum = 0;
for (; x > 0; x -= lowbit(x)) {
// 每次跳到"不重叠的前一段"
sum += tree[x];
}
// 为什么这样就能求前缀和?
// tree[x] 管辖 [x-lowbit(x)+1, x]
// x -= lowbit(x) 跳到不重叠的前一段
return sum;
}
// 区间求和:求 a[l]+a[l+1]+...+a[r]
// 利用前缀和的差:sum(l,r) = query(r) - query(l-1)
int rangeQuery(int l, int r) {
return query(r) - query(l - 1);
}
int main() {
int q; // 操作次数
cin >> n >> q;
// 读入初始数组,逐个插入树状数组
for (int i = 1; i <= n; i++) {
int val;
cin >> val;
update(i, val); // 把第 i 个位置加上 val
}
// 处理操作
while (q--) {
int op;
cin >> op;
if (op == 1) {
// 修改操作:把第 x 个元素加上 y
int x, y;
cin >> x >> y;
update(x, y);
} else {
// 查询操作:求第 l 到第 r 个元素的和
int l, r;
cin >> l >> r;
cout << rangeQuery(l, r) << endl;
}
}
return 0;
}
🔍 query 操作过程举例
求 query(13),即 a[1]+a[2]+...+a[13]:
13 = 1101₂,lowbit(13) = 1 → tree[13] 管辖 a[13]
13-1 = 12 = 1100₂,lowbit(12) = 4 → tree[12] 管辖 a[9..12]
12-4 = 8 = 1000₂,lowbit(8) = 8 → tree[8] 管辖 a[1..8]
8-8 = 0 → 停止
结果:query(13) = tree[13] + tree[12] + tree[8]
三步就求出了前13个元素的和!
13 = 1101₂,lowbit(13) = 1 → tree[13] 管辖 a[13]
13-1 = 12 = 1100₂,lowbit(12) = 4 → tree[12] 管辖 a[9..12]
12-4 = 8 = 1000₂,lowbit(8) = 8 → tree[8] 管辖 a[1..8]
8-8 = 0 → 停止
结果:query(13) = tree[13] + tree[12] + tree[8]
三步就求出了前13个元素的和!
⏱ 复杂度分析
update:O(log n),每次 x += lowbit(x) 最多执行 log₂(n) 次
query:O(log n),每次 x -= lowbit(x) 最多执行 log₂(n) 次
建树:O(n log n)(逐个 update)或 O(n)(利用前缀和技巧)
对比普通前缀和:修改 O(1)但查询 O(n),或修改 O(n)但查询 O(1)
树状数组:两种都是 O(log n),在频繁修改+查询时优势明显
query:O(log n),每次 x -= lowbit(x) 最多执行 log₂(n) 次
建树:O(n log n)(逐个 update)或 O(n)(利用前缀和技巧)
对比普通前缀和:修改 O(1)但查询 O(n),或修改 O(n)但查询 O(1)
树状数组:两种都是 O(log n),在频繁修改+查询时优势明显
⚠️ 易错点
1. 必须从1开始编号! → 树状数组下标从1开始,query(0) 会直接返回0。不要用0-based下标。
2. lowbit 用位运算 → 不能写成 x % 2 或者 x & 1,那样只得到最低位是0还是1,不是 lowbit 的值。
3. update 和 query 的循环方向 → update 是 x += lowbit(x)(向上跳),query 是 x -= lowbit(x)(向下跳),别搞反。
4. 数组开多大 → tree 数组至少开到 n,保险起见开到 n+5。
5. 区间查询公式 → rangeQuery(l,r) = query(r) - query(l-1),如果 l=1 就是 query(r)。
2. lowbit 用位运算 → 不能写成 x % 2 或者 x & 1,那样只得到最低位是0还是1,不是 lowbit 的值。
3. update 和 query 的循环方向 → update 是 x += lowbit(x)(向上跳),query 是 x -= lowbit(x)(向下跳),别搞反。
4. 数组开多大 → tree 数组至少开到 n,保险起见开到 n+5。
5. 区间查询公式 → rangeQuery(l,r) = query(r) - query(l-1),如果 l=1 就是 query(r)。
🎯 练习建议
入门练习:
• 洛谷 P3374 【模板】树状数组 1(直接套模板,单点修改+区间查询)
• 洛谷 P3372 【模板】线段树 1(先用树状数组试,发现区间修改不行→引出线段树)
进阶练习(树状数组经典应用):
• 洛谷 P1908 逆序对(树状数组求逆序对,经典!)
• 洛谷 P1972 [SDOI2009]HH的项链(离散化 + 树状数组)
学习方法:
① 先在纸上画出 tree 数组的结构,理解 lowbit 如何划分区间
② 手动模拟 query(7) 和 update(3,5) 的过程
③ 背下代码模板(核心只有10行)
④ 理解后对比线段树——树状数组是线段树的"精简版"
• 洛谷 P3374 【模板】树状数组 1(直接套模板,单点修改+区间查询)
• 洛谷 P3372 【模板】线段树 1(先用树状数组试,发现区间修改不行→引出线段树)
进阶练习(树状数组经典应用):
• 洛谷 P1908 逆序对(树状数组求逆序对,经典!)
• 洛谷 P1972 [SDOI2009]HH的项链(离散化 + 树状数组)
学习方法:
① 先在纸上画出 tree 数组的结构,理解 lowbit 如何划分区间
② 手动模拟 query(7) 和 update(3,5) 的过程
③ 背下代码模板(核心只有10行)
④ 理解后对比线段树——树状数组是线段树的"精简版"