第 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]。
这道题的原型是洛谷 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 步
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。
// 标准答案 —— 什么都不预处理:数组原样放着,改就直接改,查就整段扫一遍//// 为什么它存在:正解(树状数组)想的是「把一部分区间和攒起来,改的时候只修那几段」。// 要是标准答案也去攒和,两份代码就是同一个思路写了两遍 ——// 只能验出打字错误,验不出想法错误(第 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;}点「运行 ▶」看结果
第二个暴力是第 6 章那份前缀和原封不动搬过来,加上「改完整段重算」:
// 第二个暴力 —— 第 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;}点「运行 ▶」看结果
| 做法 | 改一次 | 查一次 |
|---|---|---|
brute(原数组) |
O(1) | O(区间长) |
prefix(前缀和) |
O(n) | O(1) |
⚠ 所以「这两个暴力哪个更慢」这个问题本身没有答案 —— 除非你先说清楚操作配比是多少。 这件事下一步就要量出来,而它决定了这一章的耗时表必须有两端。
4实测慢:而且「谁更慢」取决于配比
genBig.cpp 的旋钮不是规模,是操作配比:
本机实测(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 以来的老规矩:次数可复现,秒数不可复现)。
口径很简单:读或写数组里的一个元素,算碰一格。
// ★ 这一章的尺子:**数一数每种做法碰了多少个数组格子**//// 为什么它存在(两件不同的事)://// ① 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;}点「运行 ▶」看结果
实测(./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 决定「攒多长」
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 把这张表打出来,而且每一行都当场验算一遍(不是「打印一下」就完了):
// ★ 关键一步的「看得见」版本 —— 把每个 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;}点「运行 ▶」看结果
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正解
// 正解 —— 树状数组(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;}点「运行 ▶」看结果
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 倍。
// 「不带优化的实现」 —— 建树时老老实实做 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;}点「运行 ▶」看结果
★ 注意 buildSlow.cpp 不是错误版本 —— 它 300 轮逐字节和正解相同。
对拍在它身上一个字都看不见,这一章后面还会碰到同样的事。
8动画一:那条高亮的路,就是 O(log n) 里的 log
- 阶梯图的第 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 数组 / 结论),不只比最终答案。
// 逐步打印树状数组的样子 —— 动画就是照着这一份画的//// 每一步打四行:// · 「路径」= 这一步走过的 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;}点「运行 ▶」看结果
9★ 上界证出来了,随机数据却永远碰不到它
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。
// ★ 「上界证出来了」和「随机数据碰得到它」是两回事 —— 这一章的第三次现场// (第 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动画二:三种做法并排跑 —— 而它们的答案逐字节相同
⚠ 计数器比画面多两格:
s[i] 那一行改数时还要改 a[p] 本身, 查 [1,r] 时读的是 s[0] —— 这两格画不进来,但 count.cpp 数着它们。- 现场一(这个动画):
brute/prefix/fast是三个完全不同的做法, 可它们的输出逐字节相同。上面那张碰格表里两个数量级的差距,对拍一个字都看不见。 - 现场二:
buildSlow.cpp(n 次add建树)和正解连c[]数组都一模一样。
★ 这是第 36 章立的「随机对拍第四个盲区」,第 37 章证明了它比想象的宽 (
linear/sorted/ 堆三个不同算法答案相同),这一章是第三次 —— 只要它们解的是同一道题,对拍就一个字都看不见。 唯一的出路还是那一条:换尺子,数次数。
⚠ 把动画切到「全是查询」和「全是修改」两档看一眼: 两个暴力各有一档几乎不动,另一档满屏点亮 —— 而树状数组两档都是稀稀拉拉几格。 这就是「两端都不烂」在画面上的样子。
11★ 对拍:六个错误版本,各自靠什么现形
这是第 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 章从没有过的旋钮:下标的二进制位形状。 以前的旋钮是值域 / 规模 / 结构角色 / 操作序列,说的都是「数据长什么样」; 这一次说的是「下标长什么样」—— 它和数值一点关系都没有。
// 错误版本① —— 两条路的方向记反了: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;}点「运行 ▶」看结果
// 错误版本② —— 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;}点「运行 ▶」看结果
// 错误版本③ —— 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;}点「运行 ▶」看结果
// 错误版本④ —— 区间和写成 `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;}点「运行 ▶」看结果
// 错误版本⑤ —— 建树时直接把 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;}点「运行 ▶」看结果
// 错误版本⑥ —— 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 longint 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;}点「运行 ▶」看结果
| 前四行 | 最后那一行 | |
|---|---|---|
| 正解 | 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。
// 正解 —— 树状数组(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 章那条)。
把同一批数据只比前面那几行(把最后那行总和去掉):
| 档位 | 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,重复元素多得很,这个错当场就会被打出来。
// ★ 跨章节:用树状数组重做一遍第 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;}点「运行 ▶」看结果
check:viz里 300 轮三方对拍:第 11 章的 O(n²) 暴力 / 第 11 章的归并版 / 这一份, 答案完全一致 —— 而且生成器也是第 11 章那份,一个字没改。 ⚠ 顺带钉了一条:这 300 组里有 169 组答案非 0, 免得「全是升序数据、答案全是 0」这种废数据蒙混过关(第 24 章那条:生成器造完先看一眼答案像不像话)。
14自测
- 洛谷 P3374 【模板】树状数组 1解析 → —— 本章那道题的原题(单点加 + 区间和)。先合上页面默写 lowbit / add / sum 三段再交,别翻回第 7 步
- 洛谷 P3368 【模板】树状数组 2解析 → —— ★ 反过来:区间加 + 单点查。做法是在「差分数组」上建树状数组 —— 第 6 章那对逆运算在这儿又用上了一次
- 洛谷 P2367 语文成绩解析 → —— 第 6 章用纯差分做过(那时数组不再变,最后统一还原一次)。这道题边改边问,才非上树状数组不可 —— 两份代码摆在一起,多出来的正是「可修改」这三个字
- 洛谷 P1908 逆序对解析 → —— ★ 第 11 章用归并做过,本章第 13 步用树状数组重做了一遍。交一份树状数组版,然后和你第 11 章那份对拍 —— 生成器都不用换
- 洛谷 P1972 [SDOI2009] HH 的项链解析 → —— ★★ 难度上一个台阶:询问离线、按右端点排序,每种颜色只在「最后一次出现的位置」记 1。想明白「为什么排完序就能一遍扫完」,树状数组才算真的会用
c[i]只攒lowbit(i)那么长的一段 —— 攒得少,所以改得起;攒得刚好,所以查也不慢。 前缀和不是「慢」,是「攒得太多」。- 两条路都是 O(log n),可随机数据只跑在界的一半上。
sum(r)的步数恰好等于 popcount(r) —— 要让界现形,得自己造r = 2^k − 1。 - 生成器这一章多了一类旋钮:下标的二进制形状。 而它的两头分别是两个 bug 的天堂和坟墓 —— 三种都得造。
⚠ 下一章(线段树)会把这道题再做一遍,而且能做树状数组做不了的事(区间改、区间最值)。 到那时值得回头对比一次:树状数组代码短、常数小,线段树能干的事多。