阶段 7 · 数据结构 · 第 38 章提高组 S

树状数组

★ 关键一步是 lowbit —— c[i] 只攒 lowbit(i) 那么长的一段,于是它成了「可以修改的前缀和」,正好补上第 6 章前缀和的死穴:一改就作废。

需要先学:第 6 章 前缀和与差分、第 11 章 分治例题:单点修改 + 区间求和建议用时:130 分钟
第 6 章欠下的那笔账,这一章来还

第 6 章章末那张选择表,最后一行写着:

场景 用什么
只查不改 前缀和 O(1) 查
只改不查(最后才输出) 差分 O(1) 改
又改又查 树状数组 / 线段树(第 38、39 章)

那一章还说了一句更要紧的:「到那时你会发现,它们干的事情本质上就是『可以修改的前缀和』。」

★ 这一章就是把那句话变成代码。而真正的转折只有一句:

前缀和的死穴不是「慢」,是「攒得太多」 —— 每个 s[i] 都从 1 一路攒到 i, 所以随便改一个数,后面一大片全部作废。 树状数组把「攒多少」这件事按下标的二进制位调了一下:c[i] 只攒 lowbit(i) 那么长的一段。 攒得少,所以改得起;而攒得刚刚好,所以查也不慢。

1一句话问题

给定长度 n 的数组 a[1..n](n ≤ 2×10⁵,|a_i| ≤ 10⁹)。接下来 m 次操作(m ≤ 2×10⁵):

  • 1 p x:把 a[p] 加上 x(|x| ≤ 10⁹);
  • 2 l r:输出 a[l] + a[l+1] + … + a[r]。

★ 全部操作做完之后,再输出一行:整个数组的和 a[1] + … + a[n]。

★ 最后那一行是故意加的,第 12 步会拿数字说话

这道题的原型是洛谷 P3374,那道题只有前两条。这里多要了一行「整个数组的和」。

它看起来像凑数,其实是专门为一个 bug 准备的一条腿: 后面会证明,sum(r) 那条路只有在 r = n 时才碰得到 c[n] —— 而随机造一个 l ≤ r 出来,r 正好等于 n 的概率是 (1 + 1/2 + … + 1/n) / n ≈ ln n / n (n = 8 时还有 34%,n = 2×10⁵ 时只剩 0.006%)。 这一行是唯一保证 r = n 的那次查询。

⚠ 这是第 35、36、37 章那条「题面多问一句,对拍就多一条腿」的第四次现场。 第 37 章给它划过一条边界(它是给数据不够狠的时候兜底的,不是万能药), 这一章的账正好复刻了那条边界,而且这次能说清它兜的到底是什么:

顺手写的生成器(档位 0) 调狠之后(最终档 8)
只比前面那几行 269 / 98 / 245 / 169 / 0 / 0 300 / 263 / 300 / 300 / 300 / 179
★ 加上最后那一行 300 / 187 / 276 / 169 / 0 / 0 300 / 280 / 300 / 300 / 300 / 207

★★ 弱数据上它把那个 bug 从 98 抬到 187(接近翻倍), 数据够狠之后只值 263 → 280。 ★ 而它兜的正是「随机数据碰不到的那个边界」(r = n)—— 操作一多,随机查询里迟早会撞上 r = n,它的边际价值自然就掉下来了。

2手算一遍:默认那 8 格 + 7 步

★ 这 7 步里埋了三件事,第 11 步会挨个用到
8 7
3 1 4 1 5 9 2 6

2 1 8      → 31        整个数组的和
1 3 5      →           a[3] = 4+5 = 9   ★ 这次修改要跑三格:c[3] → c[4] → c[8]
2 3 6      → 24        9+1+5+9
2 7 7      → 2         ★ sum(7) 要走三格(7 的二进制 111,三个 1)—— n=8 时最长的一条查询路
1 1 -3     →           a[1] = 0         ★ 这次修改要跑四格:c[1] → c[2] → c[4] → c[8](最长)
1 8 -6     →           a[8] = 0         ★ 这次只跑一格(c[8] 就到头了)—— 最短
2 2 5      → 16        1+9+1+5

答案:31 24 2 16,最后一行 27。

  • 第 4 条操作 2 7 7,是「查询路径最长」的现场(popcount(7) = 3);
  • 第 5、6 条那两次修改,一次跑四格一次跑一格 —— ★ 同一个 n,同一种操作,开销差四倍, 这件事第 9 步会变成一整张表;
  • 而最后那个 27,是「add 循环写成 i < n」唯一的现场 —— ★ 那个 bug 的前四行输出一个字都不错。

3两个暴力,而且它们是一对反面

第一个暴力最老实:数组原样放着,改就直接改,查就从 l 加到 r。

brute.cpp标准答案:什么都不攒 —— 改 O(1)、查 O(区间长)
// 标准答案 —— 什么都不预处理:数组原样放着,改就直接改,查就整段扫一遍
//
// 为什么它存在:正解(树状数组)想的是「把一部分区间和攒起来,改的时候只修那几段」。
// 要是标准答案也去攒和,两份代码就是同一个思路写了两遍 ——
// 只能验出打字错误,验不出想法错误(第 9、15、34、35、36、37 章那条)。
//
// 所以这一份**一点和都不攒**:`1 p x` 就是 `a[p] += x`,`2 l r` 就是从 l 老实加到 r。
//
// ★ 扫的时候**一次 break 都没有**(第 31、35 章那条:会 break 的暴力会假装自己不慢)——
// 不过这道题的求和本来也没什么可 break 的,这里要小心的是另一件事:
// ⚠ **不许顺手维护一个「当前总和」来加速最后那一行**,那就已经在攒和了。
// 最后一行也老老实实从 1 加到 n。
//
// 复杂度:改 O(1)、查 O(区间长度),最坏 O(nm)。
//
// 输入:第一行 n m;第二行 a[1..n];接下来 m 行,每行是
// "1 p x"(a[p] += x)或 "2 l r"(输出 a[l]+…+a[r])。
// 输出:每个 "2" 一行;★ 最后再输出一行 —— 整个数组的和 a[1]+…+a[n]。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
if (!(cin >> n >> m)) return 0;
vector<long long> a(n + 1, 0); // ★ 1 基下标(区间类题目那条约定)
for (int i = 1; i <= n; i++) cin >> a[i];
string out;
for (int q = 0; q < m; q++) {
int op;
cin >> op;
if (op == 1) {
int p; long long x;
cin >> p >> x;
a[p] += x; // 改:O(1),这是它唯一的优点
} else {
int l, r;
cin >> l >> r;
long long s = 0;
for (int i = l; i <= r; i++) s += a[i]; // 查:老实扫,不 break
out += to_string(s);
out += '\n';
}
}
long long tot = 0; // ★ 最后那一行:也是老实从头加到尾
for (int i = 1; i <= n; i++) tot += a[i];
out += to_string(tot);
out += '\n';
cout << out;
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

第二个暴力是第 6 章那份前缀和原封不动搬过来,加上「改完整段重算」:

prefix.cpp第 6 章的前缀和 + 改完重算 —— 改 O(n)、查 O(1)
// 第二个暴力 —— 第 6 章那份前缀和,原封不动搬过来,加上「改完整段重算」
//
// 为什么它存在(这一章的整个动机就在它身上):
// 第 6 章章末那张表写着「只查不改用前缀和」,并且明说了它的死穴:
//
// 一旦有人改了 a[p],s[p] 到 s[n] 全部作废,重算要 O(n)。
//
// 这一份就是把那句话**跑出来**。它和 brute.cpp 正好是一对反面:
//
// | 做法 | 改一次 | 查一次 |
// | brute(原数组)| O(1) | O(区间长)|
// | prefix(前缀和)| O(n) | O(1) |
//
// ★ 两份代码的输出**逐字节相同**(它们解的是同一道题),
// 所以对拍在这上面一个字都看不见 —— 第 36 章那个「第四个盲区」在这一章的第一次现场。
// 要看见差别只能换尺子:数「碰了几个格子」(见 count.cpp)。
//
// ★★ 而且这一章比第 37 章更进一步:**这两个暴力谁更烂,取决于操作配比** ——
// 全是查询时 prefix 完胜,全是修改时 brute 完胜,混着来两个一起爆。
// 所以那张耗时表**必须给出配比这个旋钮的两端**,只报一列无论报哪列都是骗人的
// (第 35 章「必须同时给出两种形状」的同款)。
//
// 复杂度:改 O(n)、查 O(1)。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
if (!(cin >> n >> m)) return 0;
vector<long long> a(n + 1, 0), s(n + 1, 0);
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i]; // 第 6 章那两行
string out;
for (int q = 0; q < m; q++) {
int op;
cin >> op;
if (op == 1) {
int p; long long x;
cin >> p >> x;
a[p] += x;
for (int i = p; i <= n; i++) s[i] += x; // ★ 死穴:后面整段全要动
} else {
int l, r;
cin >> l >> r;
out += to_string(s[r] - s[l - 1]); // ★ 这一行是 O(1),第 6 章的全部内容
out += '\n';
}
}
out += to_string(s[n]); // 最后那一行:白送
out += '\n';
cout << out;
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★ 这两份不是「一个快一个慢」,是「一个的天堂是另一个的坟墓」
做法 改一次 查一次
brute(原数组) O(1) O(区间长)
prefix(前缀和) O(n) O(1)

⚠ 所以「这两个暴力哪个更慢」这个问题本身没有答案 —— 除非你先说清楚操作配比是多少。 这件事下一步就要量出来,而它决定了这一章的耗时表必须有两端。

4实测慢:而且「谁更慢」取决于配比

genBig.cpp 的旋钮不是规模,是操作配比:

genBig.cpp(四个配比档位)档位 0 全查询 / 1 一半一半 / 2 全修改 / 3 最坏下标

本机实测(n = m = 2×10⁵,命令写在下面,读者可以自己复现):

g++ -O2 -o genBig genBig.cpp && g++ -O2 -o brute brute.cpp && g++ -O2 -o prefix prefix.cpp && g++ -O2 -o fast fast.cpp
./genBig 200000 200000 1 > big1.txt      # 档位:0 全查询 / 1 一半一半 / 2 全修改 / 3 最坏下标
time ./fast < big1.txt > /dev/null
操作配比 brute 原数组 prefix 前缀和 ✓ fast 树状数组 只读入不算
全是查询(档位 0) 3.54 秒 ✓ 0.04 秒 0.04 秒 0.03 秒
一半一半(档位 1) 1.78 秒 3.90 秒 0.04 秒 0.03 秒
全是修改(档位 2) ✓ 0.03 秒 7.79 秒 0.04 秒 0.03 秒
最坏下标(档位 3) 4.59 秒 7.85 秒 0.03 秒 0.03 秒
⚠ 读这张表之前,先看最后那一列

「只读入」那一列是 ./count io(读完输入就退出,什么都不算)。 它一直是 0.03 秒 —— 也就是说 fast 那一列全程都在噪声里: n = 2×10⁵ 时树状数组做的活,和「把这堆数读进来」一样便宜。

★ 第 29、34、36、37 章那条「量之前先确认你量的就是它」的第五次现场。 而这次它的形状是第 36、37 章那个:秒表在正解身上直接失灵。 所以下一步得换尺子。

⚠ 顺带一件很容易忽略的事:档位 0 和档位 2 各有一个暴力跑得和正解一样快。 它们不是「不行的做法」,是「只在一端行的做法」。

5慢在哪:前缀和攒得太多了

★ 换尺子:数「碰了几个格子」

秒表失灵,那就数次数(第 21 章 stairsCount.cpp 以来的老规矩:次数可复现,秒数不可复现)。

口径很简单:读或写数组里的一个元素,算碰一格。

count.cpp三种做法并排跑,数碰的格子(附「只读入」开关)
// ★ 这一章的尺子:**数一数每种做法碰了多少个数组格子**
//
// 为什么它存在(两件不同的事):
//
// ① brute(原数组)/ prefix(前缀和)/ fenwick(树状数组)是**三个完全不同的做法**,
// 可它们的输出**逐字节相同** —— 对拍在它们身上一个字都看不见。
// 这是第 36 章那个「第四个盲区」在这一章的现场(第 37 章已经证明它比想象的宽:
// linear / sorted / 堆同样一模一样)。★ 次数可复现,秒数不可复现。
// ② ★★ 而这一章比第 37 章多一层:**那两个暴力谁更烂,取决于操作配比。**
// 全是查询时 prefix 完胜、全是修改时 brute 完胜,混着来两个一起爆 ——
// 所以只报一列(无论报哪列)都是骗人的。这张表要配着 genBig.cpp 的配比旋钮一起看。
//
// ============ 口径(正文里也要写清楚,否则这张表没法读)============
//
// 「碰一个格子」= 读或写数组里的一个元素一次。三种做法各自碰的是:
//
// · brute 改:a[p] 一格;查:a[l..r] 整段;末行:a[1..n] 整段
// · prefix 改:a[p] 一格 **+ s[p..n] 一整段**(这就是第 6 章那个死穴);
// 查:s[r] 和 s[l−1] 两格;末行:s[n] 一格
// · fenwick 改:add 那条路走过的格子;查:sum(r) 和 sum(l−1) 两条路
// —— ★ 而 sum(r) 的长度**正好等于 r 的二进制里 1 的个数**(popcount)
//
// ⚠ 建树也是实打实的开销,单独列一张表(第 29、34、36 章那条:
// 量之前先确认你量的就是它,别把开销偷偷藏起来)。
//
// 用法:./count < 数据 打印那两张表
// ./count csv < 数据 同一批数字,一行一种做法,给脚本和 check:viz 读
// ./count io < 数据 ★ 只把输入读完就退出,什么都不算(量耗时时要减掉它)
#include <bits/stdc++.h>
using namespace std;
// 按显示宽度补空格:ASCII 算 1 格,汉字算 2 格(setw 数的是字节,对不齐)
static string padDisp(const string& s, int width) {
int disp = 0;
for (unsigned char c : s) {
if ((c & 0xC0) == 0x80) continue;
disp += (c < 0x80) ? 1 : 2;
}
return s + string(max(0, width - disp), ' ');
}
static inline int lowbit(int i) { return i & -i; }
int n, m;
int main(int argc, char** argv) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
const string mode = (argc > 1) ? argv[1] : "";
if (!(cin >> n >> m)) return 0;
vector<long long> a0(n + 1, 0);
for (int i = 1; i <= n; i++) cin >> a0[i];
vector<array<long long, 3>> ops(m); // {op, x, y}
for (int q = 0; q < m; q++) {
long long op, x, y;
cin >> op >> x >> y;
ops[q] = {op, x, y};
}
if (mode == "io") { printf("读入完毕:n = %d,m = %d\n", n, m); return 0; }
// ---------- 建树的开销 ----------
long long bldFast = 0, bldSlow = 0, bldPrefix = n;
for (int i = 1; i <= n; i++) {
bldFast++; // c[i] += a[i]
if (i + lowbit(i) <= n) bldFast++; // 整个交给爸爸
for (int j = i; j <= n; j += lowbit(j)) bldSlow++;
}
// ---------- 三种做法跑同一串操作 ----------
struct Row { const char* name; const char* note; long long upd, qry, tail; string out; };
Row rows[3] = {
{"brute", "原数组:改 O(1)、查 O(区间长)", 0, 0, 0, ""},
{"prefix", "前缀和:改 O(n)、查 O(1)", 0, 0, 0, ""},
{"fenwick", "树状数组:两边都 O(log n)", 0, 0, 0, ""},
};
{ // brute
vector<long long> a = a0;
string out;
for (auto& o : ops) {
if (o[0] == 1) { a[(int)o[1]] += o[2]; rows[0].upd += 1; }
else {
int l = (int)o[1], r = (int)o[2];
long long s = 0;
for (int i = l; i <= r; i++) s += a[i];
rows[0].qry += r - l + 1;
out += to_string(s); out += '\n';
}
}
long long t = 0;
for (int i = 1; i <= n; i++) t += a[i];
rows[0].tail = n;
out += to_string(t); out += '\n';
rows[0].out = out;
}
{ // prefix
vector<long long> a = a0, s(n + 1, 0);
for (int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i];
string out;
for (auto& o : ops) {
if (o[0] == 1) {
int p = (int)o[1];
a[p] += o[2];
for (int i = p; i <= n; i++) s[i] += o[2];
rows[1].upd += 1 + (n - p + 1);
} else {
int l = (int)o[1], r = (int)o[2];
rows[1].qry += 2;
out += to_string(s[r] - s[l - 1]); out += '\n';
}
}
rows[1].tail = 1;
out += to_string(s[n]); out += '\n';
rows[1].out = out;
}
{ // fenwick
vector<long long> c(n + 2, 0);
for (int i = 1; i <= n; i++) {
c[i] += a0[i];
if (i + lowbit(i) <= n) c[i + lowbit(i)] += c[i];
}
auto add = [&](int p, long long x) {
for (int i = p; i <= n; i += lowbit(i)) { c[i] += x; rows[2].upd++; }
};
auto sum = [&](int r, long long& cnt) {
long long s = 0;
for (int i = r; i > 0; i -= lowbit(i)) { s += c[i]; cnt++; }
return s;
};
string out;
for (auto& o : ops) {
if (o[0] == 1) add((int)o[1], o[2]);
else {
int l = (int)o[1], r = (int)o[2];
long long s = sum(r, rows[2].qry) - sum(l - 1, rows[2].qry);
out += to_string(s); out += '\n';
}
}
out += to_string(sum(n, rows[2].tail)); out += '\n';
rows[2].out = out;
}
if (mode == "csv") {
for (auto& r : rows)
printf("%s,%lld,%lld,%lld,%lld,%s\n", r.name, r.upd, r.qry, r.tail,
r.upd + r.qry + r.tail, r.out == rows[2].out ? "同正解" : "不同");
printf("build,%lld,%lld,%lld,%lld,-\n", bldPrefix, bldFast, bldSlow, (long long)n);
return 0;
}
int nUpd = 0, nQry = 0;
for (auto& o : ops) (o[0] == 1 ? nUpd : nQry)++;
printf("n = %d,m = %d(修改 %d 次,查询 %d 次)\n\n", n, m, nUpd, nQry);
printf("%s %s %s %s %s %s %s\n",
padDisp("做法", 9).c_str(), padDisp("说明", 30).c_str(),
padDisp("改碰的格子", 12).c_str(), padDisp("查碰的格子", 12).c_str(),
padDisp("末行", 8).c_str(), padDisp("合计", 12).c_str(), padDisp("答案", 8).c_str());
for (auto& r : rows)
printf("%s %s %s %s %s %s %s\n",
padDisp(r.name, 9).c_str(), padDisp(r.note, 30).c_str(),
padDisp(to_string(r.upd), 12).c_str(), padDisp(to_string(r.qry), 12).c_str(),
padDisp(to_string(r.tail), 8).c_str(),
padDisp(to_string(r.upd + r.qry + r.tail), 12).c_str(),
padDisp(r.out == rows[2].out ? "同正解" : "✗ 不同", 8).c_str());
bool same = true;
for (auto& r : rows) same &= (r.out == rows[2].out);
printf("\n★ 三种做法的答案%s —— 所以这张表里的差别,对拍**一个字都看不见**。\n",
same ? "**完全一样**" : "有不一样的");
printf("\n建树的开销(碰的格子数)\n");
printf(" prefix(前缀和扫一遍) %lld\n", bldPrefix);
printf(" fenwick O(n) 建树 %lld ← 每格最多被交给爸爸一次,所以 < 2n\n", bldFast);
printf(" fenwick 做 n 次 add(慢) %lld ← buildSlow.cpp,答案完全一样\n", bldSlow);
printf("★ n = %d 时 O(n) 建树是做 n 次 add 的 %.2f 倍快。\n",
n, bldFast ? (double)bldSlow / (double)bldFast : 0.0);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

实测(./genBig 20000 20000 档位,n = m = 2×10⁴,全部写进了 check:viz):

操作配比 brute 原数组 prefix 前缀和 ✓ fast 树状数组
全是查询 100 702 922 ✓ 40 001 285 551
一半一半 49 909 536 101 220 994 ✓ 219 440
全是修改 ✓ 40 000 200 708 713 153 496
最坏下标 165 121 414 198 490 078 ✓ 289 928

★★ 这张表比秒表诚实得多,而且它说的话有点反直觉:

树状数组在任何一端都不是最优的。 全查询时前缀和比它少七倍(4 万 vs 28.6 万),全修改时原数组比它少四倍(4 万 vs 15.3 万)。 ★ 它赢的地方是「两端都不烂」 —— 一半一半的时候,两个暴力一个 4991 万、一个 1.01 亿, 而它 21.9 万,少了两个数量级。

(第 37 章那句「堆赢在两列同时是 O(log n)」的加强版:那一章堆在两列上都不输, 这一章树状数组在每一端都输一点点,可它是唯一一个中间不崩的。)

⚠ 两张表的规模不一样,这是故意的,得说明白: 碰格数可复现,所以选了跑得起的规模并写成断言(n = m = 2×10⁴); 耗时和机器有关,断言不了,所以要拧到足够大才看得出差别(n = m = 2×10⁵)。

于是问题变得很具体了:

prefix 每改一个数要碰 n − p + 1 格,是因为每个 s[i] 都从 1 攒到 i。 那么 —— 能不能让每个格子少攒一点?

6★ 关键一步:lowbit 决定「攒多长」

★★ 一句话:c[i] 只攒 lowbit(i) 那么长的一段

lowbit(i) = i & -i —— i 的二进制里最低位的那个 1(连同它后面的 0)。

lowbit(6) = lowbit(0b110) = 0b10 = 2
lowbit(7) = lowbit(0b111) = 0b01 = 1
lowbit(8) = lowbit(0b1000) = 8

⚠ i & -i 凭什么等于「最低位的那个 1」,这一章不证 —— 它靠的是补码, 而补码全书只有第 46 章第 9 步讲。这一章先把它当成一个成立的事实用; 上面那三行就是它的全部行为,C++ 速查那一组可以点着跑一遍。

★ 定义:c[i] = a[i−lowbit(i)+1] + … + a[i],也就是以 i 结尾、长度正好 lowbit(i) 的那一段。

对照一下就明白它到底改了什么:

第 i 格攒的区间 长度
第 6 章的前缀和 s[i] [1, i] i(一路涨)
本章的 c[i] (i − lowbit(i), i] lowbit(i)(时长时短)

攒得少,所以改一个数只会影响少数几格。

cover.cpp 把这张表打出来,而且每一行都当场验算一遍(不是「打印一下」就完了):

cover.cpp每格管哪一段 + 两条路的长度,全部硬验证
// ★ 关键一步的「看得见」版本 —— 把每个 c[i] 管辖的那一段打出来,并且**当场验证**它
//
// 为什么它存在:`lowbit` 这一章最容易变成「背两个循环」。
// 这份代码不讲循环,只讲一句话:**c[i] = a[i − lowbit(i) + 1] + … + a[i]**。
//
// ⚠ 而且它不是「打印一下」就完了(第 24 章 split.cpp 立的规矩):
// 每一行都拿**定义**(老老实实把那一段 a 加起来)和**建树的结果**对一遍,
// 只要有一格对不上就报错退出。所以这张表既是插图,也是一次硬验证。
//
// 用法:./cover [n] 默认 n = 8(正文和动画用的就是它)
// ./cover 16 看 16 格的样子
//
// ⚠ 输入的 a[1..n] 从 stdin 读;不给就用 1..n(这样 c[i] 的值一眼能验算)。
#include <bits/stdc++.h>
using namespace std;
static inline int lowbit(int i) { return i & -i; }
int main(int argc, char** argv) {
int n = (argc > 1) ? atoi(argv[1]) : 8;
if (n < 1) n = 1;
if (n > 64) n = 64; // 这是给人看的表,不是压力测试
vector<long long> a(n + 1);
for (int i = 1; i <= n; i++) if (!(cin >> a[i])) a[i] = i; // 没给数据就用 1..n
// O(n) 建树(和 fast.cpp 那三行一模一样)
vector<long long> c(n + 2, 0);
for (int i = 1; i <= n; i++) {
c[i] += a[i];
if (i + lowbit(i) <= n) c[i + lowbit(i)] += c[i];
}
printf("n = %d\n\n", n);
printf(" i 二进制 lowbit 管辖的区间 长度 c[i] 验算\n");
bool ok = true;
for (int i = 1; i <= n; i++) {
int lb = lowbit(i);
int lo = i - lb + 1;
long long s = 0; // ★ 按定义老实加一遍
for (int j = lo; j <= i; j++) s += a[j];
if (s != c[i]) ok = false;
string bin;
for (int b = 5; b >= 0; b--) bin += char('0' + ((i >> b) & 1));
char seg[32];
snprintf(seg, sizeof(seg), "[%d, %d]", lo, i);
printf("%3d %s %6d %-14s %4d %-8lld %s\n",
i, bin.c_str(), lb, seg, lb, c[i], s == c[i] ? "✓" : "✗ 对不上");
}
printf("\n★ 长度那一列就是 lowbit(i) —— c[i] 攒的东西**正好只有 lowbit(i) 那么长**。\n");
printf(" 对照第 6 章的前缀和:那里每个 s[i] 都从 1 攒到 i(长度 i),所以改一个数要动一大片。\n");
// 顺带把「查前缀和」那条路也打出来:它的长度正好是二进制里 1 的个数
printf("\n查前缀和 sum(r) 走过的格子(★ 长度 = r 的二进制里 1 的个数)\n");
for (int r = 1; r <= n; r++) {
printf(" sum(%2d) = ", r);
int cnt = 0;
for (int i = r; i > 0; i -= lowbit(i)) { printf("%sc[%d]", cnt ? " + " : "", i); cnt++; }
printf("%*s(%d 格,popcount(%d) = %d)\n", max(0, 30 - cnt * 7), "", cnt, r, __builtin_popcount(r));
if (cnt != __builtin_popcount(r)) ok = false;
}
// 改 a[p] 那条路
printf("\n改 a[p] 要跟着动的格子(★ 每一步最低位的 1 都严格往高位挪)\n");
for (int p = 1; p <= n; p++) {
printf(" add(%2d): ", p);
int cnt = 0, last = 0;
for (int i = p; i <= n; i += lowbit(i)) {
printf("%sc[%d]", cnt ? " → " : "", i);
if (cnt && lowbit(i) <= last) ok = false; // 硬验证:lowbit 必须严格变大
last = lowbit(i); cnt++;
}
printf("%*s(%d 格)\n", max(0, 30 - cnt * 7), "", cnt);
}
printf("\n%s\n", ok ? "✓ 全部验算通过(定义 / popcount / lowbit 严格递增,三件都对上了)"
: "✗ 有对不上的,上面标出来了");
return ok ? 0 : 1;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
  i  二进制    lowbit   管辖的区间      长度   c[i]
  1  000001       1   [1, 1]             1   3
  2  000010       2   [1, 2]             2   4
  3  000011       1   [3, 3]             1   4
  4  000100       4   [1, 4]             4   9
  5  000101       1   [5, 5]             1   5
  6  000110       2   [5, 6]             2   14
  7  000111       1   [7, 7]             1   2
  8  001000       8   [1, 8]             8   31
★ 两条路,以及它们各自的长度(都能证)

① 查前缀和 sum(r) = a[1] + … + a[r]

c[r] 已经管住了 (r−lowbit(r), r],剩下的是 (0, r−lowbit(r)] —— 同一个问题,规模变小了(第 1、2 章那句「把大问题切成同形状的小问题」)。于是:

for (int i = r; i > 0; i -= lowbit(i)) s += c[i];

★ i -= lowbit(i) 干的事是「抹掉二进制里最低位的那个 1」, 所以循环转的圈数恰好等于 r 的二进制里 1 的个数(popcount)—— ⚠ 注意这不是「不超过」,是等号。它 ≤ ⌊log₂r⌋ + 1,于是 O(log n)。

sum(7) = c[7] + c[6] + c[4]      3 格,popcount(7) = 3   ← n=8 时最长
sum(8) = c[8]                    1 格,popcount(8) = 1   ← 一步到头

② 改 a[p] += x:所有管到 p 的格子都要跟着加 x。

for (int i = p; i <= n; i += lowbit(i)) c[i] += x;

★ i += lowbit(i) 干的事是「进位」:设 lowbit(i) = 2^k,那么 i 的第 k 位是 1, 加上 2^k 必然往上进位,第 k 位(连同它上面那串连续的 1)全变成 0 —— 所以新的 lowbit 严格比旧的大。最低位的 1 一路往高位挪,最多挪 ⌊log₂n⌋+1 次就超过 n 了。

add(1): c[1] → c[2] → c[4] → c[8]      4 格   ← 最长
add(8): c[8]                           1 格   ← 最短

⚠ 为什么「管到 p 的格子」正好是那条路? 两句话:

  • 每一步都合法:i' = i + lowbit(i),因为 lowbit(i') > lowbit(i) 且 i' − lowbit(i') < i − lowbit(i) < p ≤ i < i',所以 c[i'] 也管着 p;
  • 一个都不漏:设 j 管着 p 且 j > i,那 j 的区间完整地盖住 i 所在的位置, 于是 j ≥ i' —— 因为这些区间要么互相套住、要么完全不相交,永远不会「半重叠」。 ★ 这条性质是整个 lowbit 结构的地基。

⇒ 改 O(log n)、查 O(log n)。两边同时是 log —— 这就是它比那两个暴力强的全部原因。

7正解

fast.cpp正解:树状数组(含 O(n) 建树那三行)
// 正解 —— 树状数组(Fenwick Tree):**可以修改的前缀和**
//
// ============ ★ 关键一步:c[i] 到底管哪一段 ============
//
// lowbit(i) = i & -i —— i 的二进制里**最低位的那个 1**(连同它后面的 0)。
// 例如 lowbit(6) = lowbit(0b110) = 0b10 = 2,lowbit(8) = 8,lowbit(7) = 1。
//
// ★ 定义:**c[i] = a[i − lowbit(i) + 1] + … + a[i]**,也就是以 i 结尾、长 lowbit(i) 的那一段。
// 写成区间就是 **(i − lowbit(i), i]**。
//
// i : 1 2 3 4 5 6 7 8
// 管: [1] [1,2] [3] [1,4] [5] [5,6] [7] [1,8]
// 长: 1 2 1 4 1 2 1 8 ← 正好是 lowbit(i)
//
// 第 6 章的前缀和是「每个 s[i] 都从头攒到 i」,所以改一个数要动后面**所有**格子。
// 树状数组只攒 lowbit(i) 那么长的一段 —— **攒得少,所以改得起**。
// ⚠ 而这正是第 6 章章末那张表许下的那句话:又改又查的时候,
// 要的不是「更快的前缀和」,是**能改的**前缀和。
//
// ============ 两条路:为什么一个 += 一个 −= ============
//
// ① 查前缀和 sum(r) = a[1]+…+a[r]:
// c[r] 已经管住了 (r − lowbit(r), r],剩下的是 (0, r − lowbit(r)] ——
// **同一个问题,规模变小了**(第 1、2 章那句「把大问题切成同形状的小问题」)。
// 于是 `for (i = r; i > 0; i -= lowbit(i)) s += c[i];`
//
// ★ 这一句 `i -= lowbit(i)` 干的事是「**抹掉二进制里最低位的那个 1**」,
// 所以循环转的圈数 **恰好等于 r 的二进制里 1 的个数**(popcount(r))——
// ⚠ 注意这不是「不超过」,是**等号**。它 ≤ ⌊log₂ r⌋ + 1,于是 O(log n)。
//
// ② 改 a[p] += x:所有**管到 p** 的 c[i] 都要跟着加 x。
// 哪些 i 管到 p?答案是 p, p+lowbit(p), 那个再 +lowbit, …(证明见下面 ⚠)
// 于是 `for (i = p; i <= n; i += lowbit(i)) c[i] += x;`
//
// ★ 这一句 `i += lowbit(i)` 干的事是「**进位**」:设 lowbit(i) = 2^k,
// 那么 i 的第 k 位是 1,加上 2^k 必然往上进位,第 k 位(和它上面那串连续的 1)全变成 0 ——
// 所以**新的 lowbit 严格比旧的大**。最低位的 1 一路往高位挪,
// 最多挪 ⌊log₂ n⌋ + 1 次就超过 n 了,于是也是 O(log n)。
//
// ⚠ 为什么「管到 p 的 c[i]」正好是那条路?两句话:
// · 每一步都合法:c[i] 管 (i−lowbit(i), i],而 i' = i + lowbit(i) 管 (i'−lowbit(i'), i'],
// 因为 lowbit(i') > lowbit(i) 且 i' − lowbit(i') < i − lowbit(i) < p ≤ i < i',所以 i' 也管着 p;
// · 一个都不漏:设 j 管着 p 且 j > i,那 j 的区间 (j−lowbit(j), j] 完整地盖住 i 所在的位置,
// 于是 j ≥ i'(区间要么套住、要么不相交,不会「半重叠」—— 这是 lowbit 结构最关键的性质)。
//
// ⇒ **改 O(log n)、查 O(log n)。两边同时是 log —— 这就是它比那两个暴力强的全部原因。**
// (第 37 章那句「堆赢在两列同时是 O(log n)」的同款:它只攒「刚好够用的那点和」。)
//
// ============ ★ 建树只要 O(n),不用做 n 次 add ============
//
// c[i] 管的那一段 = a[i] 加上「它下面挂着的几个子段」。
// 顺着 i 从小到大扫,先把 a[i] 放进 c[i],再把 c[i] 整个交给它爸爸 i + lowbit(i) ——
// 一遍扫完就建好了(见 main 里那三行)。
// ★ 这和「做 n 次 add」的结果**完全一样**(buildSlow.cpp 300 轮逐字节验过),
// 只是后者要 O(n log n)。⚠ 对拍在这上面一个字都看不见 —— 又一个「只影响复杂度」的写法。
//
// 复杂度 O((n + m) log n)。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
long long c[MAXN]; // ★ c[1..n],c[0] 空着不用(1 基下标)
int n, m;
inline int lowbit(int i) { return i & -i; }
/** a[p] += x:沿着「进位」那条路往上,每一格都加 x */
void add(int p, long long x) {
for (int i = p; i <= n; i += lowbit(i)) c[i] += x; // ⚠ 是 i <= n,不是 i < n
}
/** 前缀和 a[1]+…+a[r]:沿着「抹掉最低位的 1」那条路往下,转 popcount(r) 圈 */
long long sum(int r) {
long long s = 0;
for (int i = r; i > 0; i -= lowbit(i)) s += c[i]; // ⚠ 是 i > 0,不是 i > 1
return s;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n >> m)) return 0;
// ★ O(n) 建树:a[i] 先落进 c[i],再整个交给爸爸
for (int i = 1; i <= n; i++) {
long long x;
cin >> x;
c[i] += x;
int f = i + lowbit(i);
if (f <= n) c[f] += c[i];
}
string out;
for (int q = 0; q < m; q++) {
int op;
cin >> op;
if (op == 1) {
int p; long long x;
cin >> p >> x;
add(p, x);
} else {
int l, r;
cin >> l >> r;
out += to_string(sum(r) - sum(l - 1)); // ⚠ 是 l-1,不是 l(区间和 = 两个前缀和相减)
out += '\n';
}
}
out += to_string(sum(n)); // ★ 最后那一行:整个数组的和
out += '\n';
cout << out;
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★ 建树只要 O(n),而且那个数是能算准的

c[i] 管的那一段 = a[i] 加上「它下面挂着的几个子段」。 顺着 i 从小到大扫,先把 a[i] 放进 c[i],再把 c[i] 整个交给它爸爸 i + lowbit(i):

for (int i = 1; i <= n; i++) {
    c[i] += a[i];
    int f = i + lowbit(i);
    if (f <= n) c[f] += c[i];
}

一遍扫完就建好了。碰的格子数是 n(放进去)+ 有爸爸的格子数,而:

★★ 没有爸爸的格子(i + lowbit(i) > n),恰好就是 sum(n) 那条路上的格子。 所以建树碰的格子数 = 2n − popcount(n),一个数都不多。

n = 2×10⁴ 时 popcount = 5,实测正好 39 995 = 40 000 − 5(check:viz 钉着这条)。

⚠ 对照一下「老老实实做 n 次 add」的写法:同样是 n = 2×10⁴,它要碰 153 456 格, 是 O(n) 建树的 3.84 倍。

buildSlow.cpp⚠ 不是 bug:它的输出和 c[] 数组都和正解完全一样,只是建树慢了一个 log
// 「不带优化的实现」 —— 建树时老老实实做 n 次 add,O(n log n)
//
// ⚠ 它**不是 bug**:输出和 fast.cpp 逐字节相同(300 轮验过),造出来的 c[] 数组也**完全一样**。
// 差别只有一个:建树从 O(n) 变成了 O(n log n)。
//
// ★ 所以对拍在它身上一辈子抓不到 —— 第 36 章那个「第四个盲区」在这一章的第二次现场
// (第一次是 prefix.cpp / brute.cpp / fast.cpp 三个**完全不同的算法**答案相同)。
// 要看见差别只能换尺子:数「碰了几个 c[] 格子」(见 count.cpp 的 build 那一列)。
//
// ⚠ 而且这一份还提醒一件事:**「慢」和「错」是两码事,但两者都得有人管。**
// n = 2×10⁵ 时这一份也就多花几毫秒,真正的价值在于说清 O(n) 建树那三行凭什么成立。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
long long c[MAXN];
int n, m;
inline int lowbit(int i) { return i & -i; }
void add(int p, long long x) {
for (int i = p; i <= n; i += lowbit(i)) c[i] += x;
}
long long sum(int r) {
long long s = 0;
for (int i = r; i > 0; i -= lowbit(i)) s += c[i];
return s;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n >> m)) return 0;
for (int i = 1; i <= n; i++) {
long long x;
cin >> x;
add(i, x); // ⚠ n 次 add:对,但慢了一个 log
}
string out;
for (int q = 0; q < m; q++) {
int op;
cin >> op;
if (op == 1) {
int p; long long x;
cin >> p >> x;
add(p, x);
} else {
int l, r;
cin >> l >> r;
out += to_string(sum(r) - sum(l - 1));
out += '\n';
}
}
out += to_string(sum(n));
out += '\n';
cout << out;
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

★ 注意 buildSlow.cpp 不是错误版本 —— 它 300 轮逐字节和正解相同。 对拍在它身上一个字都看不见,这一章后面还会碰到同样的事。

8动画一:那条高亮的路,就是 O(log n) 里的 log

阶梯图:第 i 行那根横条就是 c[i] 管的那一段,长度 = lowbit(i)
全程碰格 21(改 8 / 查 12 / 末行 1)
第 1 / 9 步
这一步碰了 0 格★ 累计碰格 0n = 8,log₂n ≈ 3.00
1
2
3
4
5
6
7
8
c[1]
3
lowbit = 1
c[2]
4
lowbit = 2
c[3]
4
lowbit = 1
c[4]
9
lowbit = 4
c[5]
5
lowbit = 1
c[6]
14
lowbit = 2
c[7]
2
lowbit = 1
c[8]
31
lowbit = 8
a[i]
3
1
4
1
5
9
2
6
高亮 = 这一步走过的格子(查询有两条路:sum(r) 和 sum(l−1)) ★ 结论:每格最多被交给爸爸一次,所以建树是 O(n),不是 O(n log n)。
建树完毕:c[i] 攒的是 a[i−lowbit(i)+1] … a[i] 这一段,长度正好 lowbit(i)。
怎么看这个动画
  • 阶梯图的第 i 行,就是 c[i] 管的那一段,长度正好 lowbit(i)。 对照第 6 章的前缀和 —— 那里第 i 根横条一律从 1 拉到 i,所以改一个数要动一大片。
  • 改的时候高亮从 p 往上跳,查的时候从端点往下跳; ⚠ 查询是两条路(sum(r) 和 sum(l−1)),画面上用两种颜色分开。
  • 切到「★ 最坏的两个下标」那一档:改一律打 p = 1、查一律用 r = 15(二进制全是 1)—— 那是这条 log 上界唯一被顶紧的时候。再切到「⚠ 最省事的两个下标」对比一下, 同一个 n、同样的操作条数,碰的格子数差三倍多。

⚠ trace.cpp 就是这个动画的文字版,check:viz 拿它和动画逐步比 (两条路径 / 碰格 / 累计 / 整个 c 数组 / 结论),不只比最终答案。

trace.cpp动画照着它画:每一步的两条路、碰格数、c 数组
// 逐步打印树状数组的样子 —— 动画就是照着这一份画的
//
// 每一步打四行:
// · 「路径」= 这一步走过的 c[] 下标。
// ★ 改 a[p] 是 p → p+lowbit(p) → …(往上,最低位的 1 一路往高位挪);
// ★ 查 [l,r] 是**两条**路:sum(r) 和 sum(l−1),两条都从端点出发一次抹掉一个最低位的 1。
// 所以「查」那一行的路径写成 "r 的路 | l−1 的路"。
// · 「碰格」= 这一步碰了几个 c[] 格子,以及累计碰了多少(口径和 count.cpp 一字不差);
// · 「c」= 这一步做完之后 c[1..n] 的样子;
// · 「结论」= 这一步对外发生了什么(改了谁 / 查出来是几)。
//
// ⚠ check:viz 会拿这四行和动画**逐步**比(不只比最后的答案 —— 第 21 章以来那条规矩:
// 「纯逻辑对了、但画面画的是另一回事」是要靠这个防的)。
#include <bits/stdc++.h>
using namespace std;
static inline int lowbit(int i) { return i & -i; }
int n, m;
vector<long long> c;
long long touchTot = 0;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n >> m)) return 0;
c.assign(n + 2, 0);
vector<long long> a(n + 1);
for (int i = 1; i <= n; i++) cin >> a[i];
// 建树(O(n))—— 动画的第 0 帧就是它建完之后的样子
for (int i = 1; i <= n; i++) {
c[i] += a[i];
if (i + lowbit(i) <= n) c[i + lowbit(i)] += c[i];
}
auto cStr = [&]() {
string s;
for (int i = 1; i <= n; i++) { if (i > 1) s += ' '; s += to_string(c[i]); }
return s;
};
auto pathStr = [](const vector<int>& v) {
string s;
for (size_t i = 0; i < v.size(); i++) { if (i) s += ' '; s += to_string(v[i]); }
return s;
};
printf("建树 c [%s]\n", cStr().c_str());
for (int t = 1; t <= m; t++) {
int op;
cin >> op;
vector<int> pa, pb; // 两条路(改只有一条)
long long step = 0;
string note;
if (op == 1) {
int p; long long x;
cin >> p >> x;
for (int i = p; i <= n; i += lowbit(i)) { c[i] += x; pa.push_back(i); step++; }
note = "a[" + to_string(p) + "] += " + to_string(x);
} else {
int l, r;
cin >> l >> r;
long long sr = 0, sl = 0;
for (int i = r; i > 0; i -= lowbit(i)) { sr += c[i]; pa.push_back(i); step++; }
for (int i = l - 1; i > 0; i -= lowbit(i)) { sl += c[i]; pb.push_back(i); step++; }
note = "查 [" + to_string(l) + "," + to_string(r) + "] → " + to_string(sr - sl);
}
touchTot += step;
printf("步 %d op=%d\n", t, op);
printf(" 路径 [%s] | [%s] 碰格 %lld 累计碰格 %lld\n",
pathStr(pa).c_str(), pathStr(pb).c_str(), step, touchTot);
printf(" c [%s]\n", cStr().c_str());
printf(" 结论 %s\n", note.c_str());
}
// ★ 最后那一行:整个数组的和,也就是 sum(n) —— 它照样是一条路,照样计进累计
vector<int> pa;
long long tot = 0;
for (int i = n; i > 0; i -= lowbit(i)) { tot += c[i]; pa.push_back(i); }
touchTot += (long long)pa.size();
printf("收尾 路径 [%s] 碰格 %d 累计碰格 %lld 总和 %lld\n",
pathStr(pa).c_str(), (int)pa.size(), touchTot, tot);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

9★ 上界证出来了,随机数据却永远碰不到它

★★ 这是第三次现场了(第 33 章、第 37 章各一次)

sum(r) 的步数恰好等于 popcount(r)。那么随机取一个 r,它平均是多少?

二进制里 1 的个数,期望正好是位数的一半。 也就是说:

随机查询平均只走 log₂n ÷ 2 步 —— 只有 r = 2^k − 1(二进制全是 1)才顶到 log₂n。

add 那一边也一样,而且 n = 2^k 时同样有精确形式: add(p) 的步数 = popcount(n − p) + 1(p 每加一次 lowbit,n − p 就少掉一个 1), 所以平均是 log₂n ÷ 2 + 1,最坏(p = 1)是 log₂n + 1。

steps.cpp★ 这张表不带随机:1..n 全枚举,逐位可复现
// ★ 「上界证出来了」和「随机数据碰得到它」是两回事 —— 这一章的第三次现场
// (第 33 章 Bellman-Ford 的 n−1 轮第一次、第 37 章堆的 push 第二次)
//
// 这一章的界是 O(log n),而且证得死死的。可**随机数据上它离界差着一半**:
//
// · 查前缀和:sum(r) 走的格子数 **恰好等于 popcount(r)**(不是「不超过」,是等号)。
// 于是随机 r 的平均步数 = 二进制里 1 的个数的平均 ≈ **log₂n ÷ 2** ——
// ★ 只有 r = 2^k − 1(**二进制全是 1**)才顶到 log₂n。
// · 改一个数:add(p) 每一步最低位的 1 严格往高位挪,所以 ≤ ⌊log₂n⌋+1;
// ★ 而 n = 2^k 时它也有个精确形式:**add(p) 的步数 = popcount(n − p) + 1**
// (p 每加上一次 lowbit,离 n 还差的那个数 n−p 就少掉一个 1)——
// 所以平均是 **log₂n ÷ 2 + 1**,最坏(p = 1)是 log₂n + 1。
//
// ★★ 于是这张表最漂亮的地方是:**两列的平均都正好是各自最坏的一半**,
// 而且这个「一半」是能证的(popcount 的期望 = 位数的一半),不是量出来碰巧。
// ⚠ 我动手前对「改」那一列的预判是错的(以为它平均只走两三步)——
// **先跑再写**(第 16 章立的规矩)在这一章又兑现了一次。
//
// ⚠ 这一章有个第 33、37 章都没有的便利:**这两条平均值不用随机,可以精确算**
// (把 1..n 全枚举一遍就是了)—— 所以这张表**逐位可复现**,不带任何种子。
//
// ★ 顺带把「自己造最坏情况」那件事做出来:最后两列就是专门挑出来的那个下标。
//
// 用法:./steps [最大的 k] 默认 k = 20(n 一路取到 2^20)
#include <bits/stdc++.h>
using namespace std;
static inline int lowbit(int i) { return i & -i; }
// 按显示宽度补空格(右对齐):ASCII 算 1 格,汉字算 2 格
// ⚠ printf 的 %Ns 数的是**字节**,一个汉字 3 字节却只占 2 格宽 —— 第 25、26 章那条。
static string padR(const string& s, int width) {
int disp = 0;
for (unsigned char ch : s) {
if ((ch & 0xC0) == 0x80) continue;
disp += (ch < 0x80) ? 1 : 2;
}
return string(max(0, width - disp), ' ') + s;
}
int main(int argc, char** argv) {
int maxk = (argc > 1) ? atoi(argv[1]) : 20;
if (maxk < 2) maxk = 2;
if (maxk > 24) maxk = 24;
printf("%s %s | %s %s | %s %s\n",
padR("n", 8).c_str(), padR("log2 n", 7).c_str(),
padR("查:平均步数", 16).c_str(), padR("查:最坏", 10).c_str(),
padR("改:平均步数", 16).c_str(), padR("改:最坏", 10).c_str());
printf("%s %s | %s %s | %s %s\n",
padR("", 8).c_str(), padR("", 7).c_str(),
padR("(1..n 全枚举)", 16).c_str(), padR("(r=2^k-1)", 10).c_str(),
padR("(1..n 全枚举)", 16).c_str(), padR("(p=1)", 10).c_str());
for (int k = 10; k <= maxk; k += 2) {
int n = 1 << k;
long long qTot = 0, uTot = 0;
for (int r = 1; r <= n; r++) qTot += __builtin_popcount((unsigned)r);
for (int p = 1; p <= n; p++) {
int c = 0;
for (int i = p; i <= n; i += lowbit(i)) c++;
uTot += c;
}
int worstQ = __builtin_popcount((unsigned)(n - 1)); // r = 2^k − 1,全是 1
int worstU = 0;
for (int i = 1; i <= n; i += lowbit(i)) worstU++; // p = 1
printf("%8d %7d | %16.2f %10d | %16.2f %10d\n",
n, k, (double)qTot / n, worstQ, (double)uTot / n, worstU);
}
printf("\n★★ 两列的**平均都正好是各自最坏的一半**:查是 log₂n / 2 对 log₂n,\n");
printf(" 改是 log₂n / 2 + 1 对 log₂n + 1 —— 而这个「一半」是能证的\n");
printf(" (popcount 的期望 = 位数的一半),不是量出来碰巧。\n");
printf("⚠ 所以随机数据永远只跑在界的一半上。要让 O(log n) 这条界现形,\n");
printf(" **得自己造那个下标**:查要 r = 2^k − 1(二进制全是 1),改要 p = 1。\n");
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
n log₂n 查:平均(全枚举) 查:最坏(r=2^k−1) 改:平均(全枚举) 改:最坏(p=1)
1 024 10 5.00 10 6.00 11
4 096 12 6.00 12 7.00 13
16 384 14 7.00 14 8.00 15
65 536 16 8.00 16 9.00 17
262 144 18 9.00 18 10.00 19
1 048 576 20 10.00 20 11.00 21

★★ 两列的平均,都正好是各自最坏的一半 —— 而且这个「一半」是能证的,不是量出来碰巧。

⚠ 所以要让 O(log n) 这条界现形,得自己造那个下标:查要 r = 2^k − 1,改要 p = 1 (genBig.cpp 的档位 3 就是干这个的)。随机数据答不了「这条界紧不紧」。

★ 这一章比第 33、37 章的现场更彻底:那两次是「随机数据碰不到最坏情况」, 这一次连平均值是多少都能精确算出来 —— 而它恰好只有上界的一半。

⚠ 我在这一步的预判是错的,值得记一笔

动笔前我以为 add 那一列「平均只走两三步」(想当然:一半的 p 是偶数,开局就跳过好几层)。 跑完才发现它是干干净净的 log₂n / 2 + 1,而且和「查」那一列一模一样的形状。

先跑再写(第 16 章立的规矩)。这一章又兑现了一次 —— 而且这次的收获不是「数字不一样」,是那个错误的直觉让我差点漏掉「两列同形状」这个结论。

10动画二:三种做法并排跑 —— 而它们的答案逐字节相同

同一串操作,三种做法各碰了多少格 —— ★ 而它们的答案逐字节相同
第 1 / 9 步
原数组:改 O(1)、查 O(区间长)
累计碰格 0 这一步 +0
前缀和:改 O(n)、查 O(1)
累计碰格 0 这一步 +0
✓ 树状数组:两边都 O(log n)
累计碰格 0 这一步 +0
1
2
3
4
5
6
7
8
a[i]
s[i]
c[i]
点亮 = 这一步读或写过的格子。★ 三行的答案完全一样,差的只是点亮了几格 —— 所以这三行的差距,随机对拍一个字都看不见。
⚠ 计数器比画面多两格:s[i] 那一行改数时还要改 a[p] 本身, 查 [1,r] 时读的是 s[0] —— 这两格画不进来,但 count.cpp 数着它们。
同一串操作,三种做法并排跑。盯住右边那三个「累计碰过的格子」。
★★ 对拍看不见「慢」—— 这一章有两个现场
  • 现场一(这个动画):brute / prefix / fast 是三个完全不同的做法, 可它们的输出逐字节相同。上面那张碰格表里两个数量级的差距,对拍一个字都看不见。
  • 现场二:buildSlow.cpp(n 次 add 建树)和正解连 c[] 数组都一模一样。

★ 这是第 36 章立的「随机对拍第四个盲区」,第 37 章证明了它比想象的宽 (linear / sorted / 堆三个不同算法答案相同),这一章是第三次 —— 只要它们解的是同一道题,对拍就一个字都看不见。 唯一的出路还是那一条:换尺子,数次数。

⚠ 把动画切到「全是查询」和「全是修改」两档看一眼: 两个暴力各有一档几乎不动,另一档满屏点亮 —— 而树状数组两档都是稀稀拉拉几格。 这就是「两端都不烂」在画面上的样子。

11★ 对拍:六个错误版本,各自靠什么现形

★ 动笔调生成器之前,先把「每个 bug 靠什么现形」一条条写下来

这是第 37 章立的规矩(写在 gen.cpp 开头)。这一章的清单是:

错误版本 靠什么现形
wrongDir 两条路方向记反 什么数据都行 —— ★ 它是基线,抓得住它什么都不能说明
wrongEnd add 写成 i < n ★ n 的二进制形状(n = 2^k 时 add 必经 c[n])+ 一次 r = n 的查询
wrongVar i -= lowbit(r) ★ r 不是 2 的幂(r = 2^k 时它其实是对的)—— ⚠ 和上一条正好相反
wrongRange sum(r) − sum(l) a[l] ≠ 0
wrongBuild c[i] = a[i] 初始数组不全是 0
wrongInt c[] 用 int 部分和超过 2³¹,别的一概不管

★★ 这一章冒出来一类前 37 章从没有过的旋钮:下标的二进制位形状。 以前的旋钮是值域 / 规模 / 结构角色 / 操作序列,说的都是「数据长什么样」; 这一次说的是「下标长什么样」—— 它和数值一点关系都没有。

wrongDir.cpp✗ 两条路的方向记反了(基线)
// 错误版本① —— 两条路的方向记反了:add 往下走、query 往上走
//
// 只改了两个符号(`+=` ↔ `-=`),是初学树状数组最常见的手滑。
// ⚠ 它甚至不会崩、不会死循环,边界条件照抄过来还挺像回事。
//
// ★ 它靠什么现形:几乎什么数据都行 —— 这一份是这一章的**基线**,
// 用来说明「抓得住它」根本不能证明生成器够狠(第 31 章那份 wrongIdentity 的同款用途)。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
long long c[MAXN];
int n, m;
inline int lowbit(int i) { return i & -i; }
void add(int p, long long x) {
for (int i = p; i > 0; i -= lowbit(i)) c[i] += x; // ✗ 方向反了
}
long long sum(int r) {
long long s = 0;
// ✗ 方向反了。⚠ 这里多写了个 `i > 0`:因为 lowbit(0) = 0,
// 区间查询里 sum(l-1) 在 l = 1 时会传进来一个 0,光写 `i <= n` 会**原地死循环**。
// (对拍里挂一个死循环的程序,整张表就再也跑不完了 —— 错误版本也得能跑完。)
for (int i = r; i > 0 && i <= n; i += lowbit(i)) s += c[i];
return s;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n >> m)) return 0;
for (int i = 1; i <= n; i++) {
long long x;
cin >> x;
c[i] += x;
int f = i + lowbit(i);
if (f <= n) c[f] += c[i];
}
string out;
for (int q = 0; q < m; q++) {
int op;
cin >> op;
if (op == 1) {
int p; long long x;
cin >> p >> x;
add(p, x);
} else {
int l, r;
cin >> l >> r;
out += to_string(sum(r) - sum(l - 1));
out += '\n';
}
}
out += to_string(sum(n));
out += '\n';
cout << out;
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
wrongEnd.cpp✗ add 写成 i < n —— 前四行一个字都不错
// 错误版本② —— add 的循环条件写成 `i < n`(差一,漏掉最后那一格 c[n])
//
// ★★ 它靠什么现形:**n 的二进制形状** —— 这是这一章全新的一类旋钮,
// 前 37 章的旋钮都是「值域 / 规模 / 结构角色 / 操作序列」,没有一个是**下标的二进制位**。
//
// · add(p) 那条路会不会经过 n?c[n] 管的是 (n − lowbit(n), n],
// 所以**当且仅当 p > n − lowbit(n)**。于是:
// n = 2^k(比如 8、16)→ lowbit(n) = n → **每一个 p 都经过 n**,必漏;
// n 是奇数(lowbit = 1)→ 只有 p = n 才经过,概率 1/n。
// · 漏了之后还得有人去读 c[n] 才看得出来:sum(r) 的路从 r 出发,
// **只有 r = n 时才碰得到 c[n]**。
//
// ⇒ ★ 所以题面最后那一行「整个数组的和」(= sum(n))**正好是唯一保证 r = n 的那次查询** ——
// 它是专门为这个 bug 准备的一条腿(第 35、36、37 章那条「题面多问一句」的第四次)。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
long long c[MAXN];
int n, m;
inline int lowbit(int i) { return i & -i; }
void add(int p, long long x) {
for (int i = p; i < n; i += lowbit(i)) c[i] += x; // ✗ 应该是 i <= n
}
long long sum(int r) {
long long s = 0;
for (int i = r; i > 0; i -= lowbit(i)) s += c[i];
return s;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n >> m)) return 0;
for (int i = 1; i <= n; i++) {
long long x;
cin >> x;
c[i] += x;
int f = i + lowbit(i);
if (f <= n) c[f] += c[i];
}
string out;
for (int q = 0; q < m; q++) {
int op;
cin >> op;
if (op == 1) {
int p; long long x;
cin >> p >> x;
add(p, x);
} else {
int l, r;
cin >> l >> r;
out += to_string(sum(r) - sum(l - 1));
out += '\n';
}
}
out += to_string(sum(n));
out += '\n';
cout << out;
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
wrongVar.cpp✗ 循环里 lowbit 的参数没跟着 i 变
// 错误版本③ —— query 里 lowbit 的参数写成了 r,没跟着 i 变
//
// for (int i = r; i > 0; i -= lowbit(r)) ✗ // 应该是 lowbit(i)
//
// 复制粘贴 / 手快时最容易漏掉的一个字母,编译器一声不吭。
//
// ★★ 它靠什么现形:**r 的二进制形状** —— 而且条件干净得可以直接写出来:
// 这份代码每次都减掉同一个数 lowbit(r),于是它加的是 c[r] + c[r−lowbit(r)] + c[r−2·lowbit(r)] + …
// · **r 是 2 的幂**时 lowbit(r) = r,一步就减到 0 —— 它加的正好只有 c[r],**完全正确**;
// · r 不是 2 的幂时它会多加一堆本不该加的格子(r = 7 时把 c[1..7] 全加了一遍)。
//
// ⇒ ★★ **它和 wrongEnd 想要的东西正好相反**:
// wrongEnd 要 n = 2^k(add 那条路才必经 c[n]),wrongVar 要 r ≠ 2^k(不然它是对的)。
// ⚠ 所以 n **不能只造 2 的幂,也不能一个 2 的幂都不造** ——
// 生成器里 n 从 {2^k, 2^k−1, 2^k+1} 三选一,就是为了这一对拉扯
// (第 31 章那条「两头都要看:某一支永远走不到是坑,某一支占得太多也是坑」)。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
long long c[MAXN];
int n, m;
inline int lowbit(int i) { return i & -i; }
void add(int p, long long x) {
for (int i = p; i <= n; i += lowbit(i)) c[i] += x;
}
long long sum(int r) {
long long s = 0;
// ⚠ r = 0 时 lowbit(r) = 0,`i > 0` 那句在 i = 0 时就已经不成立,所以不会死循环。
for (int i = r; i > 0; i -= lowbit(r)) s += c[i]; // ✗ 应该是 lowbit(i)
return s;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n >> m)) return 0;
for (int i = 1; i <= n; i++) {
long long x;
cin >> x;
c[i] += x;
int f = i + lowbit(i);
if (f <= n) c[f] += c[i];
}
string out;
for (int q = 0; q < m; q++) {
int op;
cin >> op;
if (op == 1) {
int p; long long x;
cin >> p >> x;
add(p, x);
} else {
int l, r;
cin >> l >> r;
out += to_string(sum(r) - sum(l - 1));
out += '\n';
}
}
out += to_string(sum(n));
out += '\n';
cout << out;
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
wrongRange.cpp✗ 区间和写成 sum(r) − sum(l)
// 错误版本④ —— 区间和写成 `sum(r) − sum(l)`(少减了一格,把 a[l] 也扣掉了)
//
// 这是第 6 章那条「区间和 = s[r] − s[l−1]」的老账,换个壳又来一次。
//
// ★ 它靠什么现形:**a[l] ≠ 0**。
// ⚠ 而「a[l] 是不是 0」几乎完全由生成器决定 ——
// 顺手让初始数组全 0、又只做几次修改的话,随便挑一个 l 大概率就是 0,
// 这个 bug 当场隐身。(这一章「顺手写法」踩的坑之一,见 gen.cpp 档位 1。)
// · 另外 l = 1 时它算的是 sum(r) − sum(1),同样错 —— 所以边界那一端不用特别造。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
long long c[MAXN];
int n, m;
inline int lowbit(int i) { return i & -i; }
void add(int p, long long x) {
for (int i = p; i <= n; i += lowbit(i)) c[i] += x;
}
long long sum(int r) {
long long s = 0;
for (int i = r; i > 0; i -= lowbit(i)) s += c[i];
return s;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n >> m)) return 0;
for (int i = 1; i <= n; i++) {
long long x;
cin >> x;
c[i] += x;
int f = i + lowbit(i);
if (f <= n) c[f] += c[i];
}
string out;
for (int q = 0; q < m; q++) {
int op;
cin >> op;
if (op == 1) {
int p; long long x;
cin >> p >> x;
add(p, x);
} else {
int l, r;
cin >> l >> r;
out += to_string(sum(r) - sum(l)); // ✗ 应该是 sum(l - 1)
out += '\n';
}
}
out += to_string(sum(n));
out += '\n';
cout << out;
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
wrongBuild.cpp✗ 建树时直接把 a[i] 塞进 c[i]
// 错误版本⑤ —— 建树时直接把 a[i] 塞进 c[i](把树状数组当成了原数组)
//
// 这是「以为 c[] 就是 a[] 换了个名字」的典型症状:`add` / `sum` 两个函数都抄对了,
// 唯独初始化那一行想当然。⚠ 而它跑起来一点异常都没有 —— 只是所有答案都偏小。
//
// ★ 它靠什么现形:**初始数组不能全是 0**。
// ⚠⚠ 而这正是这一章「顺手写法」的现场(第 27–34、37 章连着九次的那条):
// 随手写生成器时最自然的做法就是「数组从全 0 开始,反正要什么值都能加出来」——
// 题面明明写着 |a_i| ≤ 10⁹(0 只是其中一个值),生成器却替题目做了主。
// 实测:顺手那一档它是 **0 / 300**(第十次)。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
long long c[MAXN];
int n, m;
inline int lowbit(int i) { return i & -i; }
void add(int p, long long x) {
for (int i = p; i <= n; i += lowbit(i)) c[i] += x;
}
long long sum(int r) {
long long s = 0;
for (int i = r; i > 0; i -= lowbit(i)) s += c[i];
return s;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n >> m)) return 0;
for (int i = 1; i <= n; i++) {
long long x;
cin >> x;
c[i] = x; // ✗ c[i] 不是 a[i],它管的是一整段
}
string out;
for (int q = 0; q < m; q++) {
int op;
cin >> op;
if (op == 1) {
int p; long long x;
cin >> p >> x;
add(p, x);
} else {
int l, r;
cin >> l >> r;
out += to_string(sum(r) - sum(l - 1));
out += '\n';
}
}
out += to_string(sum(n));
out += '\n';
cout << out;
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
wrongInt.cpp✗ c[] 忘了开 long long —— 在这组数据上一个字都不错
// 错误版本⑥ —— c[] 忘了开 long long
//
// ★★ 它靠什么现形:**没有任何生成器能靠随机对拍抓住它**(第 35 章那条的第二次现场)。
// 小数据上它 300 轮逐字节全对;只有当**部分和真的超过 2^31** 时才会翻车,
// 而这个条件和「对拍要造小数据」是天生矛盾的。
//
// ⚠ 而且和第 35 章一模一样:溢出之后的那个数**不是负数,是个「看着挺正常」的数** ——
// n = 2×10⁵、每个都是 10⁹ 时正解给 2×10¹⁴,它给一个几位数的东西,
// 不看正确答案根本不会起疑。
//
// **这只能靠脑子,不能靠对拍**:写之前先估一估「和最大能到多少」。
// 这道题:2×10⁵ × 10⁹ = 2×10¹⁴ —— int 装不下(上限约 2.1×10⁹),必须 long long。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
int c[MAXN]; // ✗ 应该是 long long
int n, m;
inline int lowbit(int i) { return i & -i; }
void add(int p, int x) {
for (int i = p; i <= n; i += lowbit(i)) c[i] += x;
}
int sum(int r) {
int s = 0;
for (int i = r; i > 0; i -= lowbit(i)) s += c[i];
return s;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n >> m)) return 0;
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
c[i] += x;
int f = i + lowbit(i);
if (f <= n) c[f] += c[i];
}
string out;
for (int q = 0; q < m; q++) {
int op;
cin >> op;
if (op == 1) {
int p, x;
cin >> p >> x;
add(p, x);
} else {
int l, r;
cin >> l >> r;
out += to_string(sum(r) - sum(l - 1));
out += '\n';
}
}
out += to_string(sum(n));
out += '\n';
cout << out;
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★ 默认那组数据上,wrongEnd 和 wrongVar 的错法正好互补
前四行 最后那一行
正解 31 24 2 16 27
wrongEnd ✓ 31 24 2 16 ✗ 31
wrongVar ✗ 31 28 19 26 ✓ 27
  • wrongEnd 只错最后那一行:因为 n = 8,每次 add 都要经过 c[8](它漏掉了), 而只有 sum(8) 才会去读 c[8] —— 也就是题面多问的那一行。
  • wrongVar 只有最后那一行对:它每次减掉的是同一个 lowbit(r), 而 r = 8 是 2 的幂,一步就减到 0,加的正好只有 c[8] —— 完全正确。

★★ 同一个事实(n = 8 是 2 的幂)同时是一个 bug 的死刑和另一个 bug 的赦免。 这就是下一步整张档位表的主线。

⚠ 还有一件事要单独说:wrongInt 在这组数据上一个字都不错。 它要的是「部分和超过 2³¹」,而这里最大的和才 31。

对拍器
★ 这个生成器有六个旋钮,其中「n 的二进制形状」有两个方向完全相反的极端 —— 而且我第一次把它加进去时,量出来比不加还差。下面三张表把每一处的账都摆出来。
// 正解 —— 树状数组(Fenwick Tree):**可以修改的前缀和**
//
// ============ ★ 关键一步:c[i] 到底管哪一段 ============
//
// lowbit(i) = i & -i —— i 的二进制里**最低位的那个 1**(连同它后面的 0)。
// 例如 lowbit(6) = lowbit(0b110) = 0b10 = 2,lowbit(8) = 8,lowbit(7) = 1。
//
// ★ 定义:**c[i] = a[i − lowbit(i) + 1] + … + a[i]**,也就是以 i 结尾、长 lowbit(i) 的那一段。
// 写成区间就是 **(i − lowbit(i), i]**。
//
// i : 1 2 3 4 5 6 7 8
// 管: [1] [1,2] [3] [1,4] [5] [5,6] [7] [1,8]
// 长: 1 2 1 4 1 2 1 8 ← 正好是 lowbit(i)
//
// 第 6 章的前缀和是「每个 s[i] 都从头攒到 i」,所以改一个数要动后面**所有**格子。
// 树状数组只攒 lowbit(i) 那么长的一段 —— **攒得少,所以改得起**。
// ⚠ 而这正是第 6 章章末那张表许下的那句话:又改又查的时候,
// 要的不是「更快的前缀和」,是**能改的**前缀和。
//
// ============ 两条路:为什么一个 += 一个 −= ============
//
// ① 查前缀和 sum(r) = a[1]+…+a[r]:
// c[r] 已经管住了 (r − lowbit(r), r],剩下的是 (0, r − lowbit(r)] ——
// **同一个问题,规模变小了**(第 1、2 章那句「把大问题切成同形状的小问题」)。
// 于是 `for (i = r; i > 0; i -= lowbit(i)) s += c[i];`
//
// ★ 这一句 `i -= lowbit(i)` 干的事是「**抹掉二进制里最低位的那个 1**」,
// 所以循环转的圈数 **恰好等于 r 的二进制里 1 的个数**(popcount(r))——
// ⚠ 注意这不是「不超过」,是**等号**。它 ≤ ⌊log₂ r⌋ + 1,于是 O(log n)。
//
// ② 改 a[p] += x:所有**管到 p** 的 c[i] 都要跟着加 x。
// 哪些 i 管到 p?答案是 p, p+lowbit(p), 那个再 +lowbit, …(证明见下面 ⚠)
// 于是 `for (i = p; i <= n; i += lowbit(i)) c[i] += x;`
//
// ★ 这一句 `i += lowbit(i)` 干的事是「**进位**」:设 lowbit(i) = 2^k,
// 那么 i 的第 k 位是 1,加上 2^k 必然往上进位,第 k 位(和它上面那串连续的 1)全变成 0 ——
// 所以**新的 lowbit 严格比旧的大**。最低位的 1 一路往高位挪,
// 最多挪 ⌊log₂ n⌋ + 1 次就超过 n 了,于是也是 O(log n)。
//
// ⚠ 为什么「管到 p 的 c[i]」正好是那条路?两句话:
// · 每一步都合法:c[i] 管 (i−lowbit(i), i],而 i' = i + lowbit(i) 管 (i'−lowbit(i'), i'],
// 因为 lowbit(i') > lowbit(i) 且 i' − lowbit(i') < i − lowbit(i) < p ≤ i < i',所以 i' 也管着 p;
// · 一个都不漏:设 j 管着 p 且 j > i,那 j 的区间 (j−lowbit(j), j] 完整地盖住 i 所在的位置,
// 于是 j ≥ i'(区间要么套住、要么不相交,不会「半重叠」—— 这是 lowbit 结构最关键的性质)。
//
// ⇒ **改 O(log n)、查 O(log n)。两边同时是 log —— 这就是它比那两个暴力强的全部原因。**
// (第 37 章那句「堆赢在两列同时是 O(log n)」的同款:它只攒「刚好够用的那点和」。)
//
// ============ ★ 建树只要 O(n),不用做 n 次 add ============
//
// c[i] 管的那一段 = a[i] 加上「它下面挂着的几个子段」。
// 顺着 i 从小到大扫,先把 a[i] 放进 c[i],再把 c[i] 整个交给它爸爸 i + lowbit(i) ——
// 一遍扫完就建好了(见 main 里那三行)。
// ★ 这和「做 n 次 add」的结果**完全一样**(buildSlow.cpp 300 轮逐字节验过),
// 只是后者要 O(n log n)。⚠ 对拍在这上面一个字都看不见 —— 又一个「只影响复杂度」的写法。
//
// 复杂度 O((n + m) log n)。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
long long c[MAXN]; // ★ c[1..n],c[0] 空着不用(1 基下标)
int n, m;
inline int lowbit(int i) { return i & -i; }
/** a[p] += x:沿着「进位」那条路往上,每一格都加 x */
void add(int p, long long x) {
for (int i = p; i <= n; i += lowbit(i)) c[i] += x; // ⚠ 是 i <= n,不是 i < n
}
/** 前缀和 a[1]+…+a[r]:沿着「抹掉最低位的 1」那条路往下,转 popcount(r) 圈 */
long long sum(int r) {
long long s = 0;
for (int i = r; i > 0; i -= lowbit(i)) s += c[i]; // ⚠ 是 i > 0,不是 i > 1
return s;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n >> m)) return 0;
// ★ O(n) 建树:a[i] 先落进 c[i],再整个交给爸爸
for (int i = 1; i <= n; i++) {
long long x;
cin >> x;
c[i] += x;
int f = i + lowbit(i);
if (f <= n) c[f] += c[i];
}
string out;
for (int q = 0; q < m; q++) {
int op;
cin >> op;
if (op == 1) {
int p; long long x;
cin >> p >> x;
add(p, x);
} else {
int l, r;
cin >> l >> r;
out += to_string(sum(r) - sum(l - 1)); // ⚠ 是 l-1,不是 l(区间和 = 两个前缀和相减)
out += '\n';
}
}
out += to_string(sum(n)); // ★ 最后那一行:整个数组的和
out += '\n';
cout << out;
return 0;
}
点一下即可编辑

300 轮实测(种子 1..300,最终档 8):

故意写错的地方 被抓 第几轮
wrongDir(方向记反) 300 / 300 第 1 轮
wrongVar(lowbit(r)) 300 / 300 第 1 轮
wrongRange(sum(l)) 300 / 300 第 1 轮
wrongBuild(c[i] = a[i]) 300 / 300 第 1 轮
wrongEnd(i < n) 280 / 300 第 2 轮
wrongInt(忘开 long long) 207 / 300 第 1 轮
buildSlow(只是建树慢) ★ 0 / 300 —
★★ 等一下 —— 对拍抓到溢出了?这是要给一条老规矩划边界

从第 6、11 章起这本书就一路写着「对拍查不出溢出 —— 这只能靠脑子」, 第 35 章还有过一次干干净净的 0 / 300 现场(wrongInt.cpp 用 int 存面积)。 可这一章它是 207 / 300。

两次都没错,差别在一个具体的量上:

★★ 「对拍抓不到溢出」不是定律,是一个条件 —— 条件是「溢出所需要的数据规模,超出了对拍小数据的规模」。

  • 第 35 章那道题:面积 = 高 × 宽,要 2×10⁵ 根柱子才溢出 —— 小数据永远碰不到。
  • 这一章:三个 10⁹ 加起来就溢出了 —— 只要生成器的值域拧到 10⁹,当场就抓得到。

⚠ 但别把这句话记反了:真正该做的动作没变 —— 先估一估「多大的数据才会溢出」,再决定能不能指望对拍。 这道题 2×10⁵ × 10⁹ = 2×10¹⁴,int 上限约 2.1×10⁹ —— 估这一下只要三秒钟, 而它比任何对拍都可靠。(而且下一张表会看到:值域拧不拧满,是一个和别的 bug 完全无关的旋钮 —— 现实里你多半是为了别的 bug 在调生成器,顺手就把它漏掉了。)

12★★ 生成器:十一个档位,一次只改一处

★★ 第一张表:六个旋钮,全部从「顺手写法」出发

gen.cpp 带了十一个档位(./gen 种子 档位),种子固定 1..300:

档位 相对档位 0 改了什么 Dir End Var Rng Bld Int
0(顺手写法) n, m ∈ [6,12]、★ 初始数组全 0、值域 [1,100] 全正、改查各半 300 187 276 169 ★ 0 ★ 0
1 ★ 放开那个顺手:初始数组随机非零 300 172 299 299 300 0
2 ★ n 只取 2 的幂(4 / 8) 299 ★ 300 ★ 170 224 0 0
3 ★ n 一个 2 的幂都不取(3/5/7/9) 297 ★ 177 ★ 279 216 0 0
4 ★ 三种形状混着取(2^k / 2^k−1 / 2^k+1) 298 211 245 221 0 0
5 操作序列拉长 m ∈ [20,40] 300 272 300 300 0 0
6 值域拉到 [−10⁹, 10⁹] 300 187 276 169 0 28

★ 档位 0 那两个 0 是这一章的起点。「初始数组全 0」是我随手就写下来的 (「反正要什么值都能用操作加出来」)—— 可题面写的是 |a_i| ≤ 10⁹,0 只是其中一个值。

★ 第 27~34、37 章那条「生成器里那个不假思索的顺手写法,会悄悄给数据加一条题目里没有的性质」, 这一章第十次。这次加的那条性质是:「数组从空的开始」。 它一口气打掉了 wrongBuild(0 / 300),还顺手压住了 wrongRange(169,放开后 299)。

★★ 档位 2 和 3 是这一章真正的新东西,请对着看:

n 只取 2 的幂:wrongEnd 冲到 300 / 300(每次 add 都必经 c[n]), 可 wrongVar 掉到 170(r = 2^k 时它是一份正确的程序)。 n 一个 2 的幂都不取:正好倒过来 —— wrongEnd 掉到 177,wrongVar 涨到 279。

★★ 同一个旋钮的两头,两个 bug 正好交换位置。 这是第 31 章那条「某一支永远走不到是坑,某一支占得太多也是坑」在这一章的样子, 而且形状比第 31 章那次干净得多:这里不是「多了挤掉别人」, 是一端直接把另一个 bug 变成了正确的代码。 ⇒ 所以档位 4 那样三种都造,才是唯一说得过去的选择。

★★ 第二张表:合起来,以及一处「量出来是负分」的改动
档位 内容 Dir End Var Rng Bld Int
7 1 + 4 + 5(非零 + 混形状 + 拉长) 300 280 300 300 300 0
8(最终档) 7 + 6(值域也拉开) 300 280 300 300 300 ★ 207
9(对照) 8 减去「二进制形状」(n 退回随机 [6,12]) 300 272 300 300 300 222
10(诊断) 8,但 n 的基数放大到 16 300 ★ 251 300 300 300 224

① 值域拉开:单独加只值 28,放进最终环境是 0 → 207。 档位 6 单独加时 wrongInt 只有 28 / 300;档位 7 → 8 是 0 → 207。

★ 第 32、34、35、36、37 章那条「调优不可加」的第六次 —— 这次是正面的那一种:序列一长,部分和才真的堆得起来,值域拧满才有意义。

② ⚠ 而「n 的二进制形状」这一处,我差点做错。 第一版的混合档基数取到 16(也就是档位 10),在最终环境里量出来 wrongEnd = 251 —— 比什么都不改的对照档(272)还差。按第 34、35、37 章那条规矩,看着就该撤回。

可它同时改了两件事:形状(我想要的)和规模(顺手带上的,n 最大到 17)。 把规模那一半收回去(基数只到 8),同一处改动立刻变成 280 —— 反超对照档。

★★ 「量到主语」那条规矩(第 22、29、34、36 章)第一次用在生成器上: 一处改动量出来是负分时,先问它是不是偷偷夹带了第二个旋钮。

③ 最后定档:为什么留着「二进制形状」。 最终档(8)和对照档(9)的账很接近,而且一列涨一列跌: wrongEnd 280 vs 272(涨),wrongInt 207 vs 222(跌)。按「让最弱的那一支尽量强」的标准, 对照档反而略胜(222 > 207)。它还是被留下了,理由不是抓获率,是边界覆盖:

n = 2^k 时 c[n] 一格盖住整个数组,是这道题结构上最特殊的一支, 而顺手随机 [6,12] 里它只占 1/7。最终档 300 轮里有 91 轮 n 是 2 的幂。 ⚠ 这笔账要明写,不许粉饰成「改了就是更好」(第 27、35 章那条)。

gen.cpp(十一个档位)六处改动全部可重跑,包括那次「差点撤回」的诊断档
★ 题面多问那一行值多少 —— 两笔账都要摆

把同一批数据只比前面那几行(把最后那行总和去掉):

档位 Dir End Var Rng Bld Int
0(顺手) 只比前几行 269 ★ 98 245 169 0 0
0 ★ 加上最后那一行 300 ★ 187 276 169 0 0
8(最终档) 只比前几行 300 263 300 300 300 179
8 ★ 加上最后那一行 300 280 300 300 300 207

★★ 弱数据上它接近翻倍(98 → 187),数据够狠之后只值 263 → 280。 和第 37 章那次(5~11 倍 vs 283 → 290)形状完全一致,这是第二次现场。

★ 而这次能说清它兜的到底是什么:那一行是 sum(n), 是唯一保证 r = n 的那次查询 —— 而 r = n 正是随机查询碰不到的那个边界。 操作一多,随机查询里迟早会撞上 r = n,它的边际价值就掉了。

★★ 「题面多问一句」兜的是「随机数据碰不到的那个边界」。 所以它在弱数据上格外值钱,而现实里你的生成器多半就是档位 0 那个样子。

13★ 跨章节:用树状数组重做第 11 章的逆序对

★ 同一道题,第三种完全不同的思路

第 11 章是「归并排序顺手数出来」(分治)。这一份换个角度:

从左往右扫,扫到 a[j] 时问一句:前面已经放进去的数里,有几个比它大? 那些数下标更小、值更大 —— 每一个都是一个逆序对。

「前面放进去的数里有几个 ≤ 某个值」正好是一句前缀和, 「放进去一个数」正好是一次单点 +1 —— 又改又查,正是这一章的题。

前面比 a[j] 大的个数 = (已经放进去的个数) − (已经放进去的 ≤ a[j] 的个数)
                     = (j − 1) − sum(rank[j])

★ 「相等不算逆序对」这件事就藏在那个 ≤ 里:sum 数的是 ≤ a[j], 减掉之后剩下的才是严格大于。写成 < 就错了 —— 这是第 22 章 lower_bound / upper_bound 一个字母之差的同款。 ⚠ 而第 11 章的生成器一半的数取自 0..5,重复元素多得很,这个错当场就会被打出来。

inv.cpp★ 树状数组 + 离散化:输入输出和第 11 章一字不差
// ★ 跨章节:用树状数组重做一遍第 11 章的逆序对
//
// 第 11 章是「归并排序顺手数出来」(分治)。这一份是**完全不同的思路**:
//
// 从左往右扫,扫到 a[j] 时问一句:**前面已经放进去的数里,有几个比它大?**
// 那些数的下标都比 j 小、值都比 a[j] 大 —— 每一个都是一个逆序对。
//
// 「前面放进去的数里有几个 ≤ 某个值」正好是一句**前缀和**,
// 而「放进去一个数」正好是一次**单点 +1** —— 又改又查,正是这一章的题。
//
// 前面比 a[j] 大的个数 = (已经放进去的个数) − (已经放进去的 ≤ a[j] 的个数)
// = (j − 1) − sum(rank[j])
//
// ★ 「相等不算逆序对」这件事就藏在 sum 的那个 ≤ 里(第 22 章 lower/upper_bound
// 一个字母之差的同款):sum 数的是 **≤ a[j]**,减掉之后剩下的才是**严格大于**。
// ⚠ 把它写成「< a[j]」的话,相等的那些会被算成逆序对 —— 而第 11 章的生成器
// 一半的数取自 0..5,重复元素多得很,这个错当场就会被打出来。
//
// ============ 为什么要离散化 ============
//
// 树状数组的下标是「位置」,这里我们要拿**值**当下标。可值可以是负数、可以很大 ——
// 所以先把所有值排序去重,用「它排第几」(rank,从 1 开始)代替原值。
// ★ 只比较大小的题,换成 rank 不影响任何答案。
//
// 输入 / 输出**和第 11 章一字不差**(第一行 n,第二行 n 个整数;输出逆序对个数),
// 所以它和第 11 章的 brute.cpp / fast.cpp 可以直接三方对拍,**生成器也用第 11 章那份**。
//
// 复杂度 O(n log n) —— 和归并那份同级,但常数和思路都不一样。
#include <bits/stdc++.h>
using namespace std;
int n;
vector<int> c;
static inline int lowbit(int i) { return i & -i; }
void add(int p, int x) {
for (int i = p; i <= n; i += lowbit(i)) c[i] += x;
}
int sum(int r) {
int s = 0;
for (int i = r; i > 0; i -= lowbit(i)) s += c[i];
return s;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int len;
if (!(cin >> len)) return 0;
vector<long long> a(len);
for (int i = 0; i < len; i++) cin >> a[i];
// 离散化:值 → 它在去重排序后的位置(1 基)
vector<long long> srt = a;
sort(srt.begin(), srt.end());
srt.erase(unique(srt.begin(), srt.end()), srt.end());
n = (int)srt.size();
c.assign(n + 2, 0);
long long cnt = 0; // ⚠ 最多 n(n−1)/2,一律 long long
for (int j = 0; j < len; j++) {
int rk = (int)(lower_bound(srt.begin(), srt.end(), a[j]) - srt.begin()) + 1;
cnt += (long long)j - sum(rk); // 前面比 a[j] 严格大的个数
add(rk, 1);
}
cout << cnt << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

check:viz 里 300 轮三方对拍:第 11 章的 O(n²) 暴力 / 第 11 章的归并版 / 这一份, 答案完全一致 —— 而且生成器也是第 11 章那份,一个字没改。 ⚠ 顺带钉了一条:这 300 组里有 169 组答案非 0, 免得「全是升序数据、答案全是 0」这种废数据蒙混过关(第 24 章那条:生成器造完先看一眼答案像不像话)。

14自测

自测清单0 / 11
配套练习
  • 洛谷 P3374 【模板】树状数组 1解析 → —— 本章那道题的原题(单点加 + 区间和)。先合上页面默写 lowbit / add / sum 三段再交,别翻回第 7 步
  • 洛谷 P3368 【模板】树状数组 2解析 → —— ★ 反过来:区间加 + 单点查。做法是在「差分数组」上建树状数组 —— 第 6 章那对逆运算在这儿又用上了一次
  • 洛谷 P2367 语文成绩解析 → —— 第 6 章用纯差分做过(那时数组不再变,最后统一还原一次)。这道题边改边问,才非上树状数组不可 —— 两份代码摆在一起,多出来的正是「可修改」这三个字
  • 洛谷 P1908 逆序对解析 → —— ★ 第 11 章用归并做过,本章第 13 步用树状数组重做了一遍。交一份树状数组版,然后和你第 11 章那份对拍 —— 生成器都不用换
  • 洛谷 P1972 [SDOI2009] HH 的项链解析 → —— ★★ 难度上一个台阶:询问离线、按右端点排序,每种颜色只在「最后一次出现的位置」记 1。想明白「为什么排完序就能一遍扫完」,树状数组才算真的会用
这一章我卡在哪(过一个月回来看,这几行比整章正文都值钱)
这一章记住三句话
  1. c[i] 只攒 lowbit(i) 那么长的一段 —— 攒得少,所以改得起;攒得刚好,所以查也不慢。 前缀和不是「慢」,是「攒得太多」。
  2. 两条路都是 O(log n),可随机数据只跑在界的一半上。 sum(r) 的步数恰好等于 popcount(r) —— 要让界现形,得自己造 r = 2^k − 1。
  3. 生成器这一章多了一类旋钮:下标的二进制形状。 而它的两头分别是两个 bug 的天堂和坟墓 —— 三种都得造。

⚠ 下一章(线段树)会把这道题再做一遍,而且能做树状数组做不了的事(区间改、区间最值)。 到那时值得回头对比一次:树状数组代码短、常数小,线段树能干的事多。