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 的补码做按位与)
🌟 为什么重要?
• 逆序对计数:树状数组的经典应用,统计比当前数大的已出现的数
• 区间修改+单点查询:差分数组 + 树状数组,可以高效处理
• 代码极短:核心只有10行左右,竞赛中省时省力
• 很多问题的基础:二维树状数组、离散化+树状数组等进阶技巧
• GESP 8级必考:是理解线段树的前置知识
📋 前置知识(学这个之前你需要知道)
1. 前缀和(GESP 2-3级):理解"前缀和可以快速求区间和"的概念
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] 的和
📐 两个核心操作
前缀和查询 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 停止)
💻 完整代码(带详细注释)
#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个元素的和!
⏱ 复杂度分析
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),在频繁修改+查询时优势明显
⚠️ 易错点
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)。
🎯 练习建议
入门练习:
• 洛谷 P3374 【模板】树状数组 1(直接套模板,单点修改+区间查询)
• 洛谷 P3372 【模板】线段树 1(先用树状数组试,发现区间修改不行→引出线段树)

进阶练习(树状数组经典应用):
• 洛谷 P1908 逆序对(树状数组求逆序对,经典!)
• 洛谷 P1972 [SDOI2009]HH的项链(离散化 + 树状数组)

学习方法:
① 先在纸上画出 tree 数组的结构,理解 lowbit 如何划分区间
② 手动模拟 query(7) 和 update(3,5) 的过程
③ 背下代码模板(核心只有10行)
④ 理解后对比线段树——树状数组是线段树的"精简版"