树状数组 · 信息学学习手册1-求和3个模板

树状数组 · 信息学学习手册

适用对象:已掌握循环、数组、排序、前缀和,准备系统学习树状数组(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(log⁡n)O(\log n)O(logn) O(log⁡n)O(\log n)O(logn) O(log⁡n)O(\log n)O(logn) nnn 可减信息、量大改动频
线段树 O(n)O(n)O(n) O(log⁡n)O(\log n)O(logn) O(log⁡n)O(\log n)O(logn) O(log⁡n)O(\log n)O(logn) 4n4n4n 不可减信息、要 lazy

前缀和改一个 aia_iai 要重算 si..sns_i..s_nsi..sn;差分能改不能查。BIT 把两者同时压到 O(log⁡n)O(\log n)O(logn),代价是只管前缀。

一、BIT 是把前缀和做成可修改的。 它不存每个 sxs_xsx,而把 sxs_xsx 拆成 O(log⁡n)O(\log n)O(logn) 个块和:改一个元素只更新含它的 O(log⁡n)O(\log n)O(logn) 块,查一次只拼 O(log⁡n)O(\log n)O(logn) 块。

二、BIT 管的是前缀信息。 它回答的永远是 ∑i≤xai\sum_{i\le x}a_i∑i≤xai、max⁡i≤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 不可减------知道 max⁡1,r\max1,rmax1,r 与 max⁡1,l−1\max1,l-1max1,l−1 推不出 max⁡l,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(log⁡n)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)≤⌊log⁡2n⌋+1=O(log⁡n)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 的个数严格增加 ,最多 ⌊log⁡2n⌋\lfloor\log_2 n\rfloor⌊log2n⌋ 个,次数同样 ≤⌊log⁡2n⌋+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(nlog⁡n)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. 内部 1-index :c[1]..c[n] 有效,c[0] 永不用,读入也从 1 开始;二维同理从 (1,1)(1,1)(1,1) 起,题目给 0-index 坐标时读入后统一加 1。
  2. #define lowbit(x) ((x) & -(x)),宏要带括号。
  3. c[]、返回值、答案一律 long long。
  4. 区间答案 === 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(nlog⁡n)O(n\log n)O(nlogn)(按 2.4 的做法可做到 O(n)O(n)O(n)),单次操作 O(log⁡n)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),两条事实:

  1. 区间加 === 两次单点改 :l,rl,rl,r 全体加 vvv   ⟺  \iff⟺ dl+=vd_l\mathrel{+}=vdl+=v、dr+1−=vd_{r+1}\mathrel{-}=vdr+1−=v,因为区间内部相邻差不变。
  2. 原值 === 差分前缀和 :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(nlog⁡n)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[]。

相关推荐
ComputerInBook1 小时前
泛函分析概观
人工智能·算法·机器学习·泛函分析
lanxiaoqing021 小时前
演讲训练的第一步:先敢说,再会说
人工智能·学习·学习方法·口才
Nuanyt2 小时前
LeetCode Hot100 刷题笔记 · 普通数组专题
java·笔记·算法·leetcode
q27551300422 小时前
AS8132 DP转HDMI 4K60(420)被动式 国产性价比AS方案
linux·算法·硬件架构·dash
ShineWinsu2 小时前
对于Qt:信号与槽的解析
c++·qt·gui·lambda·信号·信号与槽·槽函数
安全指北针2 小时前
密态计算:数据 “可用不可见” 的底层技术解析
人工智能·算法
Jmyd01232 小时前
3D 扫描仪选型技术拆解:精度、点云与软件链路
人工智能·算法·3d·三维数字化
WL学习笔记2 小时前
使用string类
c++·后端·string类
xxxiugou1232 小时前
链表算法题精讲:从两数相加到K组翻转
数据结构·c++·算法·链表