树状数组 · 信息学学习手册
适用对象:已掌握循环、数组、排序、前缀和,准备系统学习树状数组(Fenwick 树)的选手。
从"抄模板"到"看到题就知道该不该用",共 8 个文件、7 篇、15 道配套例题。
配套题目(15 道,由易到难)
| 编号 | 题目 | 来源 | 主考 | 所在篇 |
|---|---|---|---|---|
| A | 【模板】树状数组 1 | 洛谷 P3374 | 单点修改 + 区间查询 | 第 1 篇 |
| B | 【模板】树状数组 2 | 洛谷 P3368 | 区间修改 + 单点查询(差分) | 第 1 篇 |
| C | 【模板】线段树 1 | 洛谷 P3372 | 区间修改 + 区间查询(双 BIT) | 第 2 篇 |
| D | 借教室 | 洛谷 P1083 | 二分答案 + 差分判定 | 第 2 篇 |
| E | 逆序对 | 洛谷 P1908 | 离散化 + 权值 BIT | 第 3 篇 |
| F | Balanced Photo G | 洛谷 P3608(USACO) | 左右双向扫描计数 | 第 3 篇 |
| G | 导弹拦截 | 洛谷 P1020 | LIS + Dilworth 定理 | 第 4 篇 |
| H | 友好城市 | 洛谷 P2782 | 排序 + LIS | 第 4 篇 |
| I | Why Did the Cow Cross the Road II P | 洛谷 P3657(USACO) | 候选边 + LIS | 第 4 篇 |
| J | 楼兰图腾 | 洛谷 P10589 | 三元组计数 | 第 3 篇 |
| K | Narrow Passage | AtCoder ABC479 F | 逆序对 + 交换次数 | 第 5 篇 |
| L | Promotion Counting P | 洛谷 P3605(USACO) | DFS 序 + 权值 BIT | 第 5 篇 |
| N | Multiset | Codeforces 1354D | 权值 BIT 上二分:第 kkk 小 | 第 6 篇(上) |
| O | Letters Removing | Codeforces 899F | 上二分求"当前第几个"的原下标 | 第 6 篇(上) |
| P | 冰火战士 | 洛谷 P6619 / 省选联考 2020 | 双 BIT + 两段上二分 | 第 6 篇(上) |
写作说明(请先读)
- 模板是通用的,例题是校准用的。第 1 篇的三个模板、第 7 篇的合集,必须练到不看笔记能默写;例题只用来校准"什么时候用"。
- 每道例题的数据范围都按题面写出,复杂度直接对着范围估;不确定的地方写"以题面为准",不编造。
全册结构
| 篇 | 文件 | 内容 | 例题 |
|---|---|---|---|
| 1 | 树状数组手册-01-原理与模板.md |
lowbit 原理、管辖范围、三个模板、O(n)O(n)O(n) 建树 | A、B |
| 2 | 树状数组手册-02-差分与二分.md |
差分、双 BIT、二分 + 差分判定 | C、D |
| 3 | 树状数组手册-03-离散化与逆序对.md |
离散化、逆序对、双向计数、三元组计数 | E、F、J |
| 4 | 树状数组手册-04-LIS与序列结构.md |
前缀 max、LIS、Dilworth、匹配建模 | G、H、I |
| 5 | 树状数组手册-05-树上与离线.md |
DFS 序、子树区间、交换与逆序 | L、K |
| 6(上) | 树状数组手册-06上-树状数组上二分.md |
按位试填、正确性、前置条件 | N、O、P |
| 6(下) | 树状数组手册-06下-二维离线与卡常数.md |
二维 BIT、离线处理、卡常 | --- |
| 7 | 树状数组手册-07-识别与训练.md |
识别手册、反例、模板合集、易错点、两周计划 | --- |
三条使用路径
路径一:基础内容
第 1 篇(原理 + 模板)→ 第 7 篇第十五章(背诵版模板)→ 挑 E、G、L 各做一道。够应付"模板题 + 逆序对 + 子树统计"这三类。
路径二:系统学习
按第 1 → 2 → 3 → 4 → 5 → 6 → 7 的顺序走,每篇做完对应例题再看下一篇。第 7 篇第十八章给了按天排的计划。
路径三:查漏补缺(已会 BIT)
直接看第 7 篇第十三章(识别手册)+ 第十六章(易错点 Top 15),然后做 C、I、K 三道"建模型"题检验。
第 1 篇 · 原理与模板
本篇例题(2 道,模板题):
编号 题目 来源 主考 A 【模板】树状数组 1 洛谷 P3374 单点修改 + 区间查询 B 【模板】树状数组 2 洛谷 P3368 区间修改 + 单点查询(差分)
写作说明 :所有
cpp代码块均用g++ -std=c++17 -O2编译通过;例题 A、B 各跑过一组自造小样例,题面样例以原题为准。数据范围只写题面明确的 1≤n,m≤5×1051\le n,m\le5\times10^51≤n,m≤5×105。
第一章 总论:树状数组到底在解决什么问题
| 工具 | 预处理 | 单点修改 | 区间查询 | 区间修改 | 空间 | 典型场景 |
|---|---|---|---|---|---|---|
| 前缀和 | O(n)O(n)O(n) | O(n)O(n)O(n) | O(1)O(1)O(1) | O(n)O(n)O(n) | nnn | 数组只读不改 |
| 差分 | O(n)O(n)O(n) | O(1)O(1)O(1) | 不能直接查 | O(1)O(1)O(1) | nnn | 只改不查 |
| 树状数组(BIT) | O(n)O(n)O(n) | O(logn)O(\log n)O(logn) | O(logn)O(\log n)O(logn) | O(logn)O(\log n)O(logn) | nnn | 可减信息、量大改动频 |
| 线段树 | O(n)O(n)O(n) | O(logn)O(\log n)O(logn) | O(logn)O(\log n)O(logn) | O(logn)O(\log n)O(logn) | 4n4n4n | 不可减信息、要 lazy |
前缀和改一个 aia_iai 要重算 si..sns_i..s_nsi..sn;差分能改不能查。BIT 把两者同时压到 O(logn)O(\log n)O(logn),代价是只管前缀。
一、BIT 是把前缀和做成可修改的。 它不存每个 sxs_xsx,而把 sxs_xsx 拆成 O(logn)O(\log n)O(logn) 个块和:改一个元素只更新含它的 O(logn)O(\log n)O(logn) 块,查一次只拼 O(logn)O(\log n)O(logn) 块。
二、BIT 管的是前缀信息。 它回答的永远是 ∑i≤xai\sum_{i\le x}a_i∑i≤xai、maxi≤xai\max_{i\le x}a_imaxi≤xai、≤x\le x≤x 的数有几个,区间一律用两个前缀相减:
∑i=lrai=sum(r)−sum(l−1)\sum_{i=l}^{r}a_i=\operatorname{sum}(r)-\operatorname{sum}(l-1)i=l∑rai=sum(r)−sum(l−1)
三、BIT 只擅长可减(可逆)信息。 求和、计数可减,区间查询就是两次前缀;max\maxmax、min\minmin、区间 gcd\gcdgcd 不可减------知道 max1,r\max1,rmax1,r 与 max1,l−1\max1,l-1max1,l−1 推不出 maxl,r\maxl,rmaxl,r,此时只能查前缀、只能单向改(第 4 篇)。判据一句:这信息能不能反过来减掉?
第二章 原理
2.1 lowbit 与管辖范围
lowbit(i)=i & (−i)\mathrm{lowbit}(i)=i\ \&\ (-i)lowbit(i)=i & (−i)
即 iii 二进制里最低位那个 111 所代表的值:12=(1100)212=(1100)_212=(1100)2 得 444,8=(1000)28=(1000)_28=(1000)2 得 888。补码下取反加一会把"最低位的 1 及其右边"原样保留、左边全取反,故与运算只留下最低位那个 1 和它右边的 0。记法:x & -x 剥出最低位的 1,且 lowbit(0)=0。
管辖范围 :cic_ici 不存 aia_iai,而存一段区间的和
ci=∑j=i−lowbit(i)+1iaj即ci 管辖 (i−lowbit(i), i]c_i=\sum_{j=i-\mathrm{lowbit}(i)+1}^{i}a_j\quad\text{即}\quad c_i\ \text{管辖}\ \bigl(i-\mathrm{lowbit}(i),\ i\bigr]ci=j=i−lowbit(i)+1∑iaj即ci 管辖 (i−lowbit(i), i]
n=8n=8n=8 时逐格列出:
| iii | 二进制 | lowbit(i)\mathrm{lowbit}(i)lowbit(i) | 管辖区间 | cic_ici 是 |
|---|---|---|---|---|
| 1 | 001 | 1 | (0,1](0,1](0,1] | a1a_1a1 |
| 2 | 010 | 2 | (0,2](0,2](0,2] | a1+a2a_1+a_2a1+a2 |
| 3 | 011 | 1 | (2,3](2,3](2,3] | a3a_3a3 |
| 4 | 100 | 4 | (0,4](0,4](0,4] | a1+⋯+a4a_1+\dots+a_4a1+⋯+a4 |
| 5 | 101 | 1 | (4,5](4,5](4,5] | a5a_5a5 |
| 6 | 110 | 2 | (4,6](4,6](4,6] | a5+a6a_5+a_6a5+a6 |
| 7 | 111 | 1 | (6,7](6,7](6,7] | a7a_7a7 |
| 8 | 1000 | 8 | (0,8](0,8](0,8] | a1+⋯+a8a_1+\dots+a_8a1+⋯+a8 |
管辖长度恰是 lowbit(i)\mathrm{lowbit}(i)lowbit(i),区间以 iii 结尾向左延伸,末尾 0 越多管得越宽。父亲是 i+lowbit(i)i+\mathrm{lowbit}(i)i+lowbit(i),于是成树:
text
c[8] = a[1..8]
├─ c[4] = a[1..4]
│ ├─ c[2] = a[1..2]
│ │ └─ c[1] = a[1]
│ └─ c[3] = a[3]
├─ c[6] = a[5..6]
│ └─ c[5] = a[5]
└─ c[7] = a[7]
2.2 两个操作:add 上跳、sum 下跳
add(x, v) :含 axa_xax 的块正是 xxx 自己加它的全部祖先,故沿父亲上跳 ,x←x+lowbit(x)x\leftarrow x+\mathrm{lowbit}(x)x←x+lowbit(x) 直到 x>nx>nx>n:
void add(int x, ll v){ for(; x<=n; x+=lowbit(x)) c[x]+=v; }
sum(x) :每次吃掉整块 cxc_xcx(覆盖 (x−lowbit(x),x](x-\mathrm{lowbit}(x),x](x−lowbit(x),x]),再令 x←x−lowbit(x)x\leftarrow x-\mathrm{lowbit}(x)x←x−lowbit(x) 往左继续吃,直到 x=0x=0x=0,这是下跳:
ll sum(int x){ ll s=0; for(; x>0; x-=lowbit(x)) s+=c[x]; return s; }
下跳为什么恰好拼出前缀 :x=7x=7x=7 走 7→6→4→07\to6\to4\to07→6→4→0,吃掉 c7,c6,c4c_7,c_6,c_4c7,c6,c4,不重不漏正是 1,71,71,7。一般地 cxc_xcx 覆盖 L,xL,xL,x(L=x−lowbit(x)+1L=x-\mathrm{lowbit}(x)+1L=x−lowbit(x)+1),剩下 1,L−11,L-11,L−1 正是 sum(L−1)\operatorname{sum}(L-1)sum(L−1),故
sum(x)=cx+sum(x−lowbit(x))\operatorname{sum}(x)=c_x+\operatorname{sum}\bigl(x-\mathrm{lowbit}(x)\bigr)sum(x)=cx+sum(x−lowbit(x))
2.3 为什么是 O(logn)O(\log n)O(logn)
下跳 :x←x−lowbit(x)x\leftarrow x-\mathrm{lowbit}(x)x←x−lowbit(x) 把 xxx 最右边的 111 变成 000(低位全 0,不借位),故次数恰好等于 xxx 的 popcount:
Tsum(x)=popcount(x)≤⌊log2n⌋+1=O(logn)T_{\text{sum}}(x)=\operatorname{popcount}(x)\le\lfloor\log_2 n\rfloor+1=O(\log n)Tsum(x)=popcount(x)≤⌊log2n⌋+1=O(logn)
上跳 :设 lowbit(x)=2k\mathrm{lowbit}(x)=2^klowbit(x)=2k,则第 kkk 位是 1、0..k−10..k-10..k−1 位全 0;加 2k2^k2k 后该位变 0 并向 k+1k+1k+1 位进位,故 xxx 末尾连续 0 的个数严格增加 ,最多 ⌊log2n⌋\lfloor\log_2 n\rfloor⌊log2n⌋ 个,次数同样 ≤⌊log2n⌋+1\le\lfloor\log_2 n\rfloor+1≤⌊log2n⌋+1。n≤106n\le10^6n≤106 时单次不到 21 步,无递归、无懒标记。
2.4 O(n)O(n)O(n) 建树
逐个 add 是 O(nlogn)O(n\log n)O(nlogn)。更快做法:iii 从小到大,先把 aia_iai 算进 cic_ici,再把整个 cic_ici 交给父亲:
for (int i = 1; i <= n; i++) { c[i] += a[i]; int j = i + lowbit(i); if (j <= n) c[j] += c[i]; }
为什么对 :处理到 iii 时,所有满足 j+lowbit(j)=ij+\mathrm{lowbit}(j)=ij+lowbit(j)=i 的 j<ij<ij<i 都已完工并把完整 块和加进了 cic_ici,此前 cic_ici 只加过 aia_iai;这些子块加 aia_iai 恰好无重叠铺满 (i−lowbit(i),i](i-\mathrm{lowbit}(i),i](i−lowbit(i),i],故 cic_ici 此刻正确。n=8n=8n=8 检验:i=8i=8i=8 收到 c4,c6,c7c_4,c_6,c_7c4,c6,c7(因 4+44+44+4、6+26+26+2、7+17+17+1 都等于 888),正是 1,81,81,8 ✔。要求 ccc 事先全 0,多组数据要清空。
2.5 与线段树的取舍
| 维度 | 树状数组 | 线段树 |
|---|---|---|
| 代码量 / 常数 | 约 20 行、单次十几步 | 80 行以上、递归 + 下传 |
| 内存 | nnn | 4n4n4n |
| 区间修改 | 转差分 / 双 BIT | lazy 直接做(赋值、乘加混合) |
口诀:信息可减 + 操作是加/减/计数 ⇒\Rightarrow⇒ BIT;否则线段树。
第三章 三个必背模板
3.1 公共约定
- 内部 1-index :
c[1]..c[n]有效,c[0]永不用,读入也从 1 开始;二维同理从 (1,1)(1,1)(1,1) 起,题目给 0-index 坐标时读入后统一加 1。 #define lowbit(x) ((x) & -(x)),宏要带括号。c[]、返回值、答案一律long long。- 区间答案 ===
f(r) - f(l-1);l=1时sum(0)返回 000。
3.2 模板 1:单点修改 + 区间查询
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 500005;
int n, m;
ll c[N];
#define lowbit(x) ((x) & -(x))
void add(int x, ll v) { for (; x <= n; x += lowbit(x)) c[x] += v; }
ll sum(int x) { ll s = 0; for (; x > 0; x -= lowbit(x)) s += c[x]; return s; }
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) { ll v; cin >> v; add(i, v); }
while (m--) {
int op, x; cin >> op >> x;
if (op == 1) { ll v; cin >> v; add(x, v); }
else { int y; cin >> y; cout << sum(y) - sum(x - 1) << '\n'; }
}
}
建树 O(nlogn)O(n\log n)O(nlogn)(按 2.4 的做法可做到 O(n)O(n)O(n)),单次操作 O(logn)O(\log n)O(logn),空间 O(n)O(n)O(n)。
3.3 模板 2:区间修改 + 单点查询
设 di=ai−ai−1d_i=a_i-a_{i-1}di=ai−ai−1(a0=0a_0=0a0=0),两条事实:
- 区间加 === 两次单点改 :l,rl,rl,r 全体加 vvv ⟺ \iff⟺ dl+=vd_l\mathrel{+}=vdl+=v、dr+1−=vd_{r+1}\mathrel{-}=vdr+1−=v,因为区间内部相邻差不变。
- 原值 === 差分前缀和 :ax=∑i≤xdia_x=\sum_{i\le x}d_iax=∑i≤xdi。
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 500005;
int n, m;
ll c[N], a[N];
#define lowbit(x) ((x) & -(x))
void add(int x, ll v) { for (; x <= n; x += lowbit(x)) c[x] += v; }
ll sum(int x) { ll s = 0; for (; x > 0; x -= lowbit(x)) s += c[x]; return s; }
void rangeAdd(int l, int r, ll v) {
add(l, v);
if (r + 1 <= n) add(r + 1, -v);
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) add(i, a[i] - a[i - 1]);
while (m--) {
int op; cin >> op;
if (op == 1) { int l, r; ll v; cin >> l >> r >> v; rangeAdd(l, r, v); }
else { int x; cin >> x; cout << sum(x) << '\n'; }
}
}
这里的 sum(x) 求的是 axa_xax。"区间改 + 区间查"必须用模板 3。
3.4 模板 3:区间修改 + 区间查询(双 BIT)
设 di=ai−ai−1d_i=a_i-a_{i-1}di=ai−ai−1,则 ai=∑j≤idja_i=\sum_{j\le i}d_jai=∑j≤idj,前缀和
S(x)=∑i=1xai=∑i=1x∑j=1idjS(x)=\sum_{i=1}^{x}a_i=\sum_{i=1}^{x}\sum_{j=1}^{i}d_jS(x)=i=1∑xai=i=1∑xj=1∑idj
第一步,交换求和顺序 :djd_jdj 在 i=j,...,xi=j,\dots,xi=j,...,x 时各被加一次,共 (x−j+1)(x-j+1)(x−j+1) 次:
S(x)=∑j=1x(x−j+1) djS(x)=\sum_{j=1}^{x}(x-j+1)\,d_jS(x)=j=1∑x(x−j+1)dj
第二步,拆括号:
S(x)=∑j=1x(x+1)−jdj=(x+1)∑j=1xdj−∑j=1xj djS(x)=\sum_{j=1}^{x}\bigl(x+1)-j\\bigrd_j=(x+1)\sum_{j=1}^{x}d_j-\sum_{j=1}^{x}j\,d_jS(x)=j=1∑x(x+1)−jdj=(x+1)j=1∑xdj−j=1∑xjdj
第三步,看出要两个前缀和 :∑j≤xdj\sum_{j\le x}d_j∑j≤xdj 与加权和 ∑j≤xj dj\sum_{j\le x}j\,d_j∑j≤xjdj。开两个 BIT,t1 存 djd_jdj,t2 存 j⋅djj\cdot d_jj⋅dj。区间加时(dl+=vd_l\mathrel{+}=vdl+=v、dr+1−=vd_{r+1}\mathrel{-}=vdr+1−=v)同步更新:
t1: add(l,v), add(r+1,−v);t2: add(l,v l), add(r+1,−v(r+1))\texttt{t1: }\mathrm{add}(l,v),\ \mathrm{add}(r{+}1,-v);\qquad \texttt{t2: }\mathrm{add}(l,v\,l),\ \mathrm{add}(r{+}1,-v(r{+}1))t1: add(l,v), add(r+1,−v);t2: add(l,vl), add(r+1,−v(r+1))
第二处权是 r+1r+1r+1 而非 lll------权永远是它自己的下标。于是
∑i=lrai=S(r)−S(l−1)=(r+1)U(r)−V(r)−l U(l−1)+V(l−1)\sum_{i=l}^{r}a_i=S(r)-S(l-1)=(r{+}1)U(r)-V(r)-l\,U(l-1)+V(l-1)i=l∑rai=S(r)−S(l−1)=(r+1)U(r)−V(r)−lU(l−1)+V(l−1)
其中 U(x)=∑j≤xdjU(x)=\sum_{j\le x}d_jU(x)=∑j≤xdj,V(x)=∑j≤xj djV(x)=\sum_{j\le x}j\,d_jV(x)=∑j≤xjdj。
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 500005;
int n, m;
ll t1[N], t2[N], a[N];
#define lowbit(x) ((x) & -(x))
void add(ll *t, int x, ll v) { for (; x <= n; x += lowbit(x)) t[x] += v; }
ll sum(ll *t, int x) { ll s = 0; for (; x > 0; x -= lowbit(x)) s += t[x]; return s; }
void rangeAdd(int l, int r, ll v) {
add(t1, l, v); add(t1, r + 1, -v);
add(t2, l, v * l); add(t2, r + 1, -v * (r + 1));
}
ll prefix(int x) { return (ll)(x + 1) * sum(t1, x) - sum(t2, x); }
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) {
ll d = a[i] - a[i - 1];
add(t1, i, d);
add(t2, i, d * i);
}
while (m--) {
int op; cin >> op;
if (op == 1) { int l, r; ll v; cin >> l >> r >> v; rangeAdd(l, r, v); }
else { int l, r; cin >> l >> r; cout << prefix(r) - prefix(l - 1) << '\n'; }
}
}
手算核对 :a={1,3,2}a=\{1,3,2\}a={1,3,2},d={1,2,−1}d=\{1,2,-1\}d={1,2,−1},U(3)=2U(3)=2U(3)=2、V(3)=1+4−3=2V(3)=1+4-3=2V(3)=1+4−3=2,S(3)=4×2−2=6S(3)=4\times2-2=6S(3)=4×2−2=6 ✔ 对 2,32,32,3 加 555 后 d={1,7,−1}d=\{1,7,-1\}d={1,7,−1},S(1)=1S(1)=1S(1)=1,S(3)=4×7−12=16S(3)=4\times7-12=16S(3)=4×7−12=16,故 2,32,32,3 和 =15=8+7=15=8+7=15=8+7 ✔
两条硬要求 :t2 存 j⋅djj\cdot d_jj⋅dj 而非 djd_jdj(漏乘下标最常见);两个 BIT 必须同时更新。
例题 A 【模板】树状数组 1(洛谷 P3374)
例题 B 【模板】树状数组 2(洛谷 P3368)
本篇易错点
1. 下标 0 进入 add。 lowbit(0)=0,for(; x<=n; x+=0) 是死循环。任何 add(x) 都要保证 x≥1x\ge1x≥1,读入写 for(int i=1;i<=n;i++)。
2. 把 x >>= 1 当成 x -= lowbit(x)。 两者都让 xxx 变小,语义却不同:前者"除以 2"会跳过整块,后者才是"吃掉最低位的 1"。sum 里写成 >>1 结果必错,而且小样例有时还能过 。上跳只有 x += lowbit(x)。
3. int 溢出。 三处必须 long long:c[](含 t1[]、t2[])、所有 sum/prefix 的返回值、双 BIT 里的 (x+1)*sum(t1,x) 与 d*i。n=5×105n=5\times10^5n=5×105、∣ai∣≤105|a_i|\le10^5∣ai∣≤105 时总和约 5×10105\times10^{10}5×1010,远超 int 上限。
4. n 与 N 写混。 N 是容量、n 是实际长度:循环边界用 n,声明用 N。常见错法是 for(; x<=N; ...) 越界读,或写变长数组 ll c[n] 导致 RE。
5. 区间查询忘了 l-1。 sum(r)-sum(l) 得到 (l,r](l,r](l,r],少了 ala_lal。
6. 卡常时还在 O(nlogn)O(n\log n)O(nlogn) 建树。 n=106n=10^6n=106 时逐个 add 约 2×1072\times10^72×107 次加法;改用 2.4 的 O(n)O(n)O(n) 建树法,或把读入值直接写进 c[] 再跑一遍建树,省掉 a[]。