树状数组(BIT) 知识点讲解

信息学竞赛(CSP-J/S、NOIP、GESP)高频考点精讲:从基础规则到推演与易错点,配真题例题。学完可一键去 OI伴学在线刷题。

去 OI伴学在线练习 →

本讲学习目标

概念

想象你在玩一个“数数游戏”。桌上有一排编号从 1 到 $n$ 的盒子,每个盒子里放了一些糖果。现在有两个请求:

1. 往第 $x$ 号盒子里增加 $v$ 颗糖果;

2. 问前 $y$ 号盒子($1 \sim y$)里一共有多少颗糖果。

如果 $n, m$ 都只有 100,你可以每次把前 $y$ 个盒子一个个数一遍,总复杂度 $O(n \cdot m)$,勉强能过。但如果 $n, m$ 涨到 $10^5$,每次数一遍就要扫 $10^5$ 个数,总共要扫 $10^{10}$ 次,直接超时。

有没有一种数据结构,能让“单点加”和“前缀和”都变成对数级 $O(\log n)$?有,就是树状数组(Fenwick Tree,简称 BIT)。它代码极短,只靠几个简单的循环就能实现这两个操作,是线段树之前最值得先吃透的 $O(\log n)$ 结构。

触发清单(看到这些信号第一时间想到本知识点)

题干信号优先考虑什么对应思路
数组 + 「单点修改」 + 「区间求和 / 前缀和」树状数组模板 1add(pos, val) + query(x)
「区间加一个数,问某个位置的值」树状数组模板 2(差分)差分转单点,再套模板 1
「逆序对」 / 「有多少对满足 i<j 且 a[i]>a[j]」边插边查前缀和数值当下标,query(a[i]-1)
「偏序统计」 / 「第 k 小」离散化 + 值域 BIT + 二分排序消一维,对另一维建 BIT

定义

术语翻译: - 前缀和:数组前 $x$ 个数的累加值,即 $a[1]+a[2]+\dots+a[x]$。 - 差分数组:令 $d[i]=a[i]-a[i-1]$(约定 $a[0]=0$),则 $a[x]$ 等于差分数组的前缀和。 - lowbit(x):取出 $x$ 二进制表示中最低位的 1及其后面的 0 组成的数值。例如 $12=1100_2$,lowbit(12)=4=100_2。

核心:lowbit 为什么能取出最低位的 1

设 $x$ 最低位的 1 在第 $k$ 位(从右数 0 起),则 $x$ 的低 $k$ 位全为 0、第 $k$ 位为 1。

举例 $x=12=1100_2$:~x=0011,-x=0100,1100 & 0100 = 0100_2 = 4。

大纲还给出另一种等价写法(用异或):

lowbit(i) = i & (i ^ (i - 1)) (与 i & -i 完全等价)

CCF 大纲的正式定义(提高级考纲原文)

对数组 $a$ 构造数组 $c$,其中

c[i] = a[i - 2^k + 1] + … + a[i]($k$ 为 $i$ 在二进制下末尾 0 的个数),即从 $a[i - 2^k + 1]$ 到 $a[i]$ 的累加和;数组 $c$ 称为 $a$ 的树状数组。

等价的写法是 c[i] = a[i - lowbit(i) + 1] + … + a[i],其中 lowbit(i) 是 $i$ 二进制最低位的 1 及其后面的 0 组成的数值。换句话说,c[i] 管的区间长度恰好是 lowbit(i)、左端点是 i - lowbit(i) + 1——这正是上面「每个 c[i] 管自己一段」说法的官方表述,也是 query/add 不断 ±lowbit 能不重不漏拼出前缀和的根本原因。

两个操作(全课就这俩函数)

① 单点加 add(pos, val) —— 从 pos 起每次 pos += lowbit(pos) 往「父节点」走,把路径上的 c 都加上 val:

void add(int pos, int val) {
 for (; pos <= n; pos += lowbit(pos)) c[pos] += val;
}

② 前缀和 query(x) —— 从 x 起每次 x -= lowbit(x) 往「左兄弟」走,把路径上的 c 累加:

int query(int x) {
 int s = 0;
 for (; x > 0; x -= lowbit(x)) s += c[x];
 return s;
}

区间和 [l, r] = query(r) - query(l - 1)。

c[i] 管理的区间:长度 = lowbit(i),左端点 = i - lowbit(i) + 1,右端点 = i。两个操作每次去掉/加上一个二进制位的 1,步数 = x 二进制中 1 的个数,故都是 $O(\log n)$。

为什么这些写法是对的(推演)

推演 0 · lowbit 为什么能取出最低位的 1(补码逐位走查):见上文。

推演 1 · 单点改 + 区间和为什么是 O(log n):

c[i] 管长度 lowbit(i)、以 i 结尾的一段。a[k]+=v 时,从 i=k 起 i += lowbit(i) 更新所有覆盖 k 的节点;sum(R) 时从 i=R 起 i -= lowbit(i) 累加。两者每次砍掉/补上一个二进制位的 1,步数 = 二进制中 1 的个数 ≤ ⌊log₂n⌋+1,故 $O(\log n)$。下标必须从 1 起(lowbit(0)=0 会让 x -= lowbit(x) 卡在 0 死循环)。

推演 2 · 区间加 + 单点查为什么是「差分数组的 BIT」:

区间 [l,r] += v 等价于差分 d[l]+=v, d[r+1]-=v;单点 a[x] = 差分数组前缀和 = query(x)。于是「区间加」变成两次 add,「单点查」变成一次 query——完全套模板 1,只是维护的对象换成差分数组。

推演 3 · 逆序对为什么用 BIT:

从左到右把 a[i] 的值当下标 +1,每次查「已插入中 > a[i] 的有几个」= query(MAX) - query(a[i]),即 a[i] 贡献的逆序对;累计即总数,$O(n \log n)$。本质是"边插入边查前缀和",BIT 正是干这个的。

技巧速查表(背下来)

想做的事写法说明
单点加 a[pos]+=vadd(pos, v)从 pos 向上 +lowbit 累加
前缀和 sum(1..x)query(x)从 x 向下 -lowbit 累加
区间和 [l,r]query(r) - query(l-1)经典容斥
区间加 [l,r]+=v(模板 2)add(l, v); add(r+1, -v)差分转单点
单点查 a[x](模板 2)query(x)差分数组前缀和
逆序对ans += query(rk[i]-1); add(rk[i], 1)先查后插,数值当下标
第 k 小值域 BIT 上二分 midquery(mid)>=k ? r=mid : l=mid+1
二维前缀和二维数组 c[x][y] 套同样逻辑先 x 后 y 或反之

原讲义配图

树状数组结构示意图

add 与 query 路径

例题

例题 1:单点修改 + 区间求和(自构造)

① 题目描述

给定一个长度为 $n$ 的数组 $a$(下标从 1 开始)。接下来有 $m$ 次操作,每次操作给出三个整数 $op, x, y$:

数据范围:$n, m \le 5 \times 10^5$,$a_i, y$ 为整数。请输出每次询问的结果。

② 审题分析

③ 解题思路

1. 读入 $n, m$。

2. 读入初始数组 $a$,用 $n$ 次 add(i, a[i]) 建树。

3. 循环处理 $m$ 次操作:

- $op=1$:调用 add(x, y)。

- $op=2$:输出 query(y) - query(x - 1)。

④ C++ 完整参考代码(逐行注释)

#include <cstdio>
int n, m, c[500005]; // c 就是树状数组本身:c[i] 管「以 i 结尾、长 lowbit(i)」的一段

int lowbit(int x) { return x & -x; } // 取出 x 二进制最低位的 1:12(1100₂) → 4(100₂)

void add(int pos, int val) { // 单点加:把 a[pos] 加 val
 for (; pos <= n; pos += lowbit(pos)) // 沿「父节点」一路向上,直到跳出数组
 c[pos] += val; // 路径上每个 c[pos] 都管着这个位置,全都要 +val
}

int query(int x) { // 前缀和:返回 a[1] + a[2] + … + a[x]
 int s = 0;
 for (; x > 0; x -= lowbit(x)) // 每次剥掉 x 最低位的 1,直到 x = 0
 s += c[x]; // 拼上的这些 c[x] 段刚好不重不漏盖住 a[1..x]
 return s;
}

int main() {
 scanf("%d%d", &n, &m);
 for (int i = 1; i <= n; i++) { // 下标必须从 1 开始!lowbit(0)=0 会让 query 死循环
 int v; scanf("%d", &v);
 add(i, v); // 建树 = 逐个单点加,n 次 add 就位
 }
 while (m--) {
 int op, x, y; scanf("%d%d%d", &op, &x, &y);
 if (op == 1) add(x, y); // 操作 1:a[x] += y
 else printf("%d\n", query(y) - query(x - 1)); // 操作 2:区间和 = 两个前缀相减
 }
 return 0;
}

⑤ 代码解读(含真实运行输出)

设输入如下($n=8, m=4$):

8 4
1 2 3 4 5 6 7 8
1 3 5
2 4 7
2 1 8
1 8 -2

真实运行结果(按顺序输出):

22
41

(注:题目只要求输出询问结果,因此只有两行。)

例题 2:区间修改 + 单点查询(自构造)

① 题目描述

给定长度为 $n$ 的数组 $a$(下标从 1 开始)。接下来有 $m$ 次操作,每次操作给出三个整数 $op, x, y$:

数据范围:$n, m \le 2 \times 10^5$,$a_i, y$ 为整数。请输出每次询问的结果。

② 审题分析

③ 解题思路

1. 读入 $n, m$。

2. 读入初始数组 $a$,用差分建树:对每个 $i$ 执行 add(i, a[i] - a[i-1])(约定 $a[0]=0$)。

3. 循环处理 $m$ 次操作:

- $op=1$:add(x, y); add(y+1, -y);

- $op=2$:输出 query(x)。

④ C++ 完整参考代码(逐行注释)

#include <cstdio>
int n, m, c[200005]; // c 是差分数组的树状数组,开 n+2 防越界

int lowbit(int x) { return x & -x; }

void add(int pos, int val) {
 for (; pos <= n + 1; pos += lowbit(pos)) c[pos] += val;
}

int query(int x) {
 int s = 0;
 for (; x > 0; x -= lowbit(x)) s += c[x];
 return s;
}

int main() {
 scanf("%d%d", &n, &m);
 int prev = 0;
 for (int i = 1; i <= n; i++) {
 int v; scanf("%d", &v);
 add(i, v - prev); // 差分建树:d[i] = a[i] - a[i-1]
 prev = v;
 }
 while (m--) {
 int op, x, y; scanf("%d%d%d", &op, &x, &y);
 if (op == 1) {
 add(x, y); // d[x] += y
 add(y + 1, -y); // d[y+1] -= y
 } else {
 printf("%d\n", query(x)); // a[x] = 差分数组前缀和
 }
 }
 return 0;
}

⑤ 代码解读(含真实运行输出)

设输入如下($n=5, m=3$):

5 3
1 2 3 4 5
1 2 10
2 3
2 5

真实运行结果:

13
5

例题复盘总结

两道题的共同点与差异:

- 例题 1:单点改 + 区间查 → 直接套模板 1 的 add/query。

- 例题 2:区间改 + 单点查 → 差分转单点,再套模板 1。

1. 识别操作类型(单点/区间、修改/查询)。

2. 判断是否需要差分(区间改 + 单点查)。

3. 确认下标从 1 开始,r+1 可能越界需开 n+2。

4. 写出 add/query 框架,填入具体逻辑。

- 例题 2 中忘记开 n+2,导致 add(y+1, -y) 越界段错误。

- 例题 2 中差分建树时写成 add(i, a[i]) 而非 add(i, a[i]-a[i-1]),导致所有查询结果偏大。

- 例题 1 中区间和写成 query(r)-query(l) 少减 1,漏掉左端点。

- 例题 2 中区间加时只对 d[l] 加 v 而忘记对 d[r+1] 减 v,导致修改效果只影响左半部分。

写法总表

技法分层解决什么典型写法
模板 1:单点改 + 区间查单点加、前缀和add(pos,val) / query(x) / query(r)-query(l-1)
模板 2:区间改 + 单点查区间加、单点值差分 d[l]+=v, d[r+1]-=v,然后套用模板 1
逆序对 / 偏序统计边插边查前缀和数值当下标,ans += query(rk[i]-1); add(rk[i],1)
第 k 小 / 值域统计值域上的二分值域 BIT 上二分 mid,query(mid)>=k ? r=mid : l=mid+1
二维前缀和二维矩形和二维数组 c[x][y],同样 add/query 逻辑

分工说明:触发清单告诉你“什么时候想到本知识点”,速查表告诉你“具体怎么写”,总表则按难度从细到粗列出所有技法层次。

常见错误

1. 下标从 0 开始导致死循环:query(0) 时 x -= lowbit(x) 永远是 0,循环条件 x>0 不成立直接返回 0,但 add(0, v) 会让 pos += lowbit(pos) 卡在 0 无限循环。所有下标必须从 1 计数,输入为 0 时要 +1 或特判。

2. 区间和写成 query(r) - query(l):少减了 1,左端点被漏算。正确写法是 query(r) - query(l - 1)。

3. 模板 2 忘记用差分:直接在区间 [l,r] 上循环 add 会退化成 $O(n)$ 单次操作,$m$ 次操作直接超时。区间改必须转 d[l]+=k, d[r+1]-=k 再查前缀和。

4. 数值当下标时没离散化:a[i] 可能是 $10^9$ 或负数,c[10^9] 数组开不下。先把数值排序去重映射成 $1..m$。

5. 数组 c 开太小 / 没清零:模板 1 开 n+1,模板 2 开 n+2;多组数据之间要 memset 或重新 add。

自测

1. 写出 lowbit(x) 的两种写法(用 &,以及用异或的等价写法 x & (x ^ (x - 1)))。并口头说清 x & -x 为什么能留下最低位的 1(用补码推一遍)。

2. 模板 1 里 add 和 query 的循环方向分别是什么?为什么是 $O(\log n)$?

3. 区间 [l, r] 求和为什么是 query(r) - query(l-1)?如果写成 query(r) - query(l) 会少算什么?

4. 「区间加 k,单点查 a[x]」为什么要用差分数组转成模板 1?直接在 [l,r] 上 add 会怎样?

5. 用树状数组求逆序对时,查询语句应该写在插入之前还是之后?为什么要离散化?

举一反三 · 审题流水线

拿到一道数组题,按这条流水线走一遍:

练手题

题 A:给定长度为 $n$ 的数组 $a$,接下来 $m$ 次操作:

请输出所有 $3$ 次操作的结果。数据范围 $n,m \le 1.5 \times 10^5$。

题 B:有 $n$ 个位置,每个位置有一个初始值 $a_i$。接下来 $m$ 次操作:

请输出所有 $2$ 次操作的结果。数据范围 $n,m \le 2 \times 10^5$。

学完这个考点,用历年真题检验一下

OI伴学 把「树状数组(BIT)」在 CCF-GESP / CSP / NOIP 真题里出现过的题目汇总到一页,可按考点横向刷;每题都带答案、解题思维和常见错误。

看 树状数组(BIT) 历年真题汇总 → 去平台刷题 →