第 35~39 章那一块(数据结构)的难点在结构:写错一句下推,答案就悄悄错了。 数学这一块的算法短得多 —— 这一章的正解只有 5 行 —— 可风险原封不动地转移到了别处:
取模、溢出、负数、以及 0 和 1 这两个边界。
★ 所以这一章的六个错误版本里,有三个是溢出,而且它们要的数据一个比一个苛刻。 ⚠ 而最值得记的一件事在生成器上:这一章的「顺手写法」不是给数据加了一条题目没有的性质, 是随机数据自己把题目做没了 —— 第 11 步会拿一个算得出来的常数说清这件事。
1 一句话问题
给定 n 个正整数
a[1..n](n ≤ 10⁵,1 ≤ a_i ≤ 10⁹):
- 第一行输出
gcd(a_1, …, a_n);- 第二行输出
lcm(a_1, …, a_n);★ 如果它超过 10¹⁸ 就输出-1;- ★ 第三行输出前缀 gcd:
g_1 g_2 … g_n,其中g_i = gcd(a_1, …, a_i)。
① 第二问那个 -1: 一道纯粹的「求 lcm」在 n = 10⁵ 时答案会大到没法写。
加了上限之后,「什么时候该判溢出、判在哪一句」就成了题目的一部分 ——
而这正是数学这一块最容易翻车的地方。
⚠ 第 10 步会看到:三个和溢出有关的错误版本,要求的数据规模一个比一个高。
② 第三问那个前缀 gcd: 它是「题面多问一句」的第六次(第 35~39 章连着五次)。 这一次多问的东西很朴素 —— 它逼你把中间过程也算对,而不是只交一个最终答案。 第 11 步会看到,它正好卡住一个只错在中间过程上的 bug。
⚠ 而它还有一个副作用,是写完之后才发现的:这一行让「随机数据把题目做没了」这件事变得看得见 ——
随机数据上这一行几乎恒等于 a₁ 1 1 1 …。
2 手算一遍:默认那 8 个数
8
12 18 30 5 60 7 1000000007 907696939
第一行 gcd = 1 (12,18 → 6;6,30 → 6;6,5 → 1,之后就一直是 1)
第二行 lcm = -1 (lcm 到 1260 × 1000000007 = 1.26×10¹², 再乘 9 亿 → 超 10¹⁸)
第三行 前缀 gcd = 12 6 6 1 1 1 1 1三处特意安排:
- 前三个数带公因子(12、18、30 → 6),第四个数 5 一来 gcd 就掉到 1 —— ★ 这样第三行才有内容可看,而不是从第二项开始就一路 1;
gcd(30,5) = 5而gcd(12,18,30,5) = 1——「前缀 gcd」和「相邻两个的 gcd」在这里分道扬镳;- 最后两个是九位数,而且和前面互质 —— lcm 一步跨过 10¹⁸。
⚠ 前六个数完全可以手算;后两个是给溢出准备的,得靠代码。这一点得说明白, 别假装那两个数也是手算出来的。
3 三个暴力:而它们慢的原因**各不相同**
第一个是标准答案:按定义算 —— 把每个数分解质因数,gcd 取指数的 min,lcm 取 max。
点「运行 ▶」看结果
每个质因子的 min + max = 两个指数之和,于是
gcd(a,b) × lcm(a,b) = a × b
—— 这就是代码里那句 lcm = a / gcd(a,b) * b 的来历。
⚠ 而为什么要先除后乘,第 7 步再说,那是这一章一半的坑。
第二个暴力最老实:从 min(a,b) 往下试,第一个同时整除两个数的就是答案。
点「运行 ▶」看结果
第三个是更相减损术(《九章算术》里那个):gcd(a,b) = gcd(a−b, b)。
点「运行 ▶」看结果
check:viz 里 300 轮钉着这件事。也就是说 ——
这一章接下来要讲的所有差距,对拍原理上一个字都看不见。 (第 36 章立的「随机对拍第四个盲区」,第 37、38、39 章各一次现场,这是第五次。)
★ 而这一章还把这个盲区推宽了一点:前四次至少有两份是「同一个算法的不同写法」, 这一次四份代码的想法两两不同 —— 分解质因数 / 取模 / 相减 / 枚举约数。
4 实测慢:三种慢法,各有各的绝境
genBig.cpp 的旋钮不是规模,是这两个数长什么样:
本机实测(n = 2,命令写在下面,读者可以自己复现):
g++ -O2 -o genBig genBig.cpp && g++ -O2 -o fast fast.cpp && g++ -O2 -o sub sub.cpp && g++ -O2 -o slow slow.cpp
./genBig 2 3 > big3.txt # 0 随机 / 1 一大一小 / 2 斐波那契 / 3 两个大质数 / 4 两个一样
time ./slow < big3.txt
| 这两个数 | ✓ fast 取模 | brute 分解质因数 | sub 相减 | slow 枚举约数 |
|---|---|---|---|---|
| 随机两个大数 | 0.00 秒 | 0.00 秒 | 0.00 秒 | 0.60 秒 |
| ★ 一大一小(10⁹ 和 1) | 0.00 秒 | 0.00 秒 | 1.68 秒 | 0.00 秒 |
| ★ 相邻的两个斐波那契数 | 0.00 秒 | 0.00 秒 | 0.00 秒 | 7.40 秒 |
| ★ 两个大质数 | 0.00 秒 | 0.00 秒 | 0.02 秒 | 10.13 秒 |
| ⚠ 两个一样的数 | 0.00 秒 | 0.00 秒 | 0.00 秒 | 0.00 秒 |
每一档的输家都不一样,而且换一档就换一个人。
- 「一大一小」是相减法的绝境:第一步就要减十亿次;
- 「两个大质数」是枚举约数的绝境:一路试到 min(a,b) 才发现 gcd 是 1;
- 「两个一样的数」谁都不慢 —— 三种做法同时一步到头。
★ 只报一列,无论报哪一列,都会把某个做法冤枉成「其实也还行」。 这是第 35 章那条(数据的形状不对,暴力会假装自己不慢)在这一章的样子。
⚠ 而 fast 和 brute 那两列全程都是 0.00 —— 秒表在它们身上完全失灵。
(第 36~39 章连着四次,这是第五次。)所以下一步还得换尺子。
5 慢在哪:换尺子,数「转了多少圈」
秒表失灵就数次数(第 21 章以来的老规矩)。可这一章有个新麻烦:
相减法在 gcd(10⁹, 1) 上要减十亿次,真跑一次要按秒算,三百轮就按分钟算。
好在这两个数都有闭式,一行就能算出来:
| 做法 | 转的圈数 | 怎么算出来的 |
|---|---|---|
fast 取模 | 取模次数 | 跑一遍就知道 |
sub 相减 | 每一步商的和 | a = q·b + r 那一步,相减法要减 q 次 |
slow 枚举 | min(a,b) − gcd(a,b) + 1 | 就是那个 for 循环的长度 |
brute 试除 | 约 √a | 分解一个数要试到 √a |
★ 这是这本书第一次用「算」的方式给出计数器,理由很实在:跑不动。 而它和「跑出来」是同一个数 —— 前两行是定义,后两行是循环边界。
点「运行 ▶」看结果
实测(./genBig 2 档位,全部写进了 check:viz):
| 这两个数 | ✓ fast 取模 | sub 相减 | slow 枚举 | brute 试除 |
|---|---|---|---|---|
| 随机两个大数 | 17 | 86 | 35 380 259 | 10 830 |
| ★ 一大一小 | 1 | 999 999 937 | 1 | 31 621 |
| ★ 相邻斐波那契 | 43 ← 最坏 | 44 | 701 408 733 | 527 |
| ★ 两个大质数 | 7 | 22 727 280 | 999 999 893 | 63 242 |
| ⚠ 两个一样的数 | 1 | 1 | 1 | 63 242 ← 只有它还慢 |
★★ 这张表里藏着这一章最漂亮的一句话,在斐波那契那一行:
取模 43 步,相减 44 步 —— 几乎一样。 因为斐波那契那一档每一步的商都是 1,而「取模」本来就是「一口气减 q 次」的缩写。 ⇒ 斐波那契之所以是取模法的最坏情况,恰恰因为那时候取模退化成了相减。
⚠ 最后一行也值得单看:brute(分解质因数)只看这个数本身有多大,不看两个数的关系 ——
所以别人都一步到头的时候,它还得老老实实试 63 242 次。
于是问题变得很具体了:
枚举约数要试
min(a,b)次,相减法要减「商的和」那么多次。 那么 —— 有没有办法一步就把 a 缩到很小?
6 ★ 关键一步:gcd(a, b) = gcd(b, a mod b)
设 a = k·b + r(r = a mod b)。
- 若
d | b且d | r,那么d | (k·b + r) = a⇒ d 是 (a,b) 的公约数; - 若
d | a且d | b,那么d | (a − k·b) = r⇒ d 是 (b,r) 的公约数。
⇒ 两对数的公约数集合完全相同,那么其中最大的那个当然也相同。∎
★ 这一步值钱的地方在于它换了个对象去证: 直接去比「两个最大值谁大谁小」是证不动的, 把问题换成集合相等,一行就完事了。
第 19 章的交换论证(别盯着最优解,去看「把它变成我的解」的过程)、 第 34 章的切割性质(别盯着最小生成树,去看一条切割边)—— 都是同一种手法:证不动的时候,换一个更大的对象去证。
设 a ≥ b,看 a mod b:
- 若
b ≤ a/2,那么a mod b < b ≤ a/2; - 若
b > a/2,那么a mod b = a − b < a/2。
两种情况都得到 a mod b < a/2 ⇒ 两步之内第一个数至少减半 ⇒ O(log a)。
⚠ 但这只是个上界。真正的最坏情况长什么样、随机数据能不能碰到它,第 9 步专门量。
7 正解,以及这一章一半的坑
点「运行 ▶」看结果
① 循环条件是 while (b),不是 while (b > 1)。
gcd(a, 0) = a(0 被所有数整除),所以到头的标志是 b 变成 0。
写成 b > 1 会在「这一对互质」的时候返回一个错的数 —— 而两个随机数互质的概率是 60.79%。
② lcm 必须先除后乘:a / gcd(a,b) * b。
写成 a * b / gcd(a,b),中间那一乘就已经爆了 long long。
★ a 和 b 都到 10⁹ 时 a*b 是 10¹⁸ —— 还没爆;
可 a 是已经累出来的 lcm(能到 10¹⁸ 附近),再乘一个 10⁹ 就是 10²⁷。
③ 溢出判断要写在乘之前**:**
long long t = l / gcd2(l, a[i]);
if (t > LIM / a[i]) over = true; // ✓ 乘之前先判
else l = t * a[i];写成「先乘完再判 l > LIM」是站错了位置 —— 那时候 l 早就绕回去了。
⚠ 这个 bug 最阴的地方是:逻辑本身完全正确,只是晚了一句。
8 动画一:每一帧就是一句 a = q × b + r
- 上面那根长条是 a,下面按 q 段切开的是「q 个 b」,尾巴上剩下的那一小截就是余数 r。 一眼就能看出「取模 = 一口气减掉 q 个 b」。
- ★★ 右边两个计数器要一起看:取模次数每步 +1,相减次数每步 +q。
- 切到「★ 一大一小」那一档:第一步的 q 就是一亿 —— 相减法当场崩掉。
- 切到「★ 相邻的两个斐波那契数」:每一步的 q 都是 1,两个计数器齐头并进 —— ★ 这就是上一步那句话的画面版。
⚠ trace.cpp 是这个动画的文字版,check:viz 拿它和动画逐步比
(a / 商 / b / 余数 / 两个累计计数),不只比最终答案。
点「运行 ▶」看结果
9 ★ 上界证出来了,随机数据却永远碰不到它
steps.cpp 不带随机:1 ≤ a, b ≤ n 全枚举,逐位可复现。
点「运行 ▶」看结果
| n | 平均步数(全枚举) | 0.8428 · ln n | 最坏步数 | ★ 最坏那一对 |
|---|---|---|---|---|
| 100 | 3.983 | 3.881 | 10 | (55, 89) |
| 300 | 4.888 | 4.807 | 12 | (144, 233) |
| 900 | 5.804 | 5.733 | 14 | (377, 610) |
| 2 700 | 6.727 | 6.659 | 17 | (1597, 2584) |
★★ 两件事都值得停下来看:
① 最坏那一对,永远是相邻的两个斐波那契数(55/89、144/233、377/610、1597/2584)——
这就是 Lamé 定理。道理也直白:要让步数多,就要每一步「减得尽量少」,
也就是 a = 1·b + r 且 r 尽量大;倒着推回去,(b, a) → (a+b, b),长出来的正是斐波那契数列。
于是 gcd(F(k+1), F(k)) 正好要转 k−1 步(表里每一行都钉了这条)。
② 平均步数是能算出来的(Heilbronn / Dixon):(12 ln2 / π²)·ln n ≈ 0.8428 · ln n —— 而实测那一列和它一路贴着走,四行差都不到 0.11。
★ 「证出来的常数」和「实测出来的常数」是同一个数 —— 第 37、38、39 章那条规矩,这是第四次。
⚠ 而最要紧的是第三张表:同样大小的两个数,随机取一对 vs 斐波那契那一对:
| 数的大小 | 随机 1000 对的平均步数 | ★ 斐波那契那一对 |
|---|---|---|
| 89 | 3.904 | 9 |
| 10 946 | 7.948 | 19 |
| 1 346 269 | 11.901 | 29 |
| 165 580 141 | 15.801 | 39 |
★★ 随机数据只跑在最坏的 40% 上,而且它永远碰不到那个最坏。 要让 O(log) 这条界现形,得自己造那个输入。
10 ★ 对拍:六个错误版本,各自靠什么现形
| 错误版本 | 靠什么现形 |
|---|---|
甲 wrongOne while (b > 1) | 某一对数互质(随机数据 60.8% 就撞上)—— ★ 基线 |
甲 wrongBreak g==1 就 break 整个循环 | 全体 gcd 中途变 1,而且后面还有数 |
乙 wrongPrefix 前缀 gcd 写成相邻 gcd | ★ 要 gcd(a_{i−1},a_i) ≠ gcd(a_1..a_i) ⇒ 数据得带公因子 |
丙 wrongNoLim 根本不判溢出 | 答案超过 10¹⁸ 就行 —— ★ 三个溢出 bug 里门槛最低的 |
丙 wrongLate 判断写在乘完之后 | ★★ 要撞破 long long(≈9.2×10¹⁸),而且绕回去之后要落在 10¹⁸ 以内 |
丙 wrongLcm 先乘后除 | 同上 |
★ 「值域拧满」在这一章不是开关,是一把有三个刻度的旋钮。 ⚠ 而甲组要「gcd 会变成 1」、乙组要「有公因子」—— 这两组是打架的,第 11 步会看到代价。
点「运行 ▶」看结果
点「运行 ▶」看结果
点「运行 ▶」看结果
点「运行 ▶」看结果
点「运行 ▶」看结果
点「运行 ▶」看结果
| 第一行 | 第二行 | 第三行 | |
|---|---|---|---|
| 正解 | 1 | -1 | 12 6 6 1 1 1 1 1 |
wrongOne | ✗ 2 | -1 | ✗ 12 6 6 **5 5 2 2 2** |
wrongBreak | 1 | ✗ 180 | 12 6 6 1 1 1 1 1 |
wrongPrefix | 1 | -1 | ✗ 12 6 6 **5 5** 1 1 1 |
wrongNoLim | 1 | ✗ 18575894801788 | 12 6 6 1 1 1 1 1 |
wrongLate | 1 | ✗ 18575894801788 | 12 6 6 1 1 1 1 1 |
wrongLcm | 1 | ✗ 18575894801788 | 12 6 6 1 1 1 1 1 |
★ 后三个一模一样 —— 光看这一组数据,你分不清是哪一个 bug。
⚠ 这件事本身要写进正文:「对拍抓到了」和「我知道错在哪」是两回事。 三个 bug 都会让那一乘绕回去,绕回去之后的那个数当然一样。
★ 而 wrongBreak 的那个 180 是最好玩的:if (g == 1) break; 这句话对第一问完全正确
(gcd 一旦是 1 就再也变不回去),它只是顺手把后面那些数也跳过了 ——
于是 lcm 只算了前四个数。
★ 一个只对「其中一问」成立的剪枝,被顺手用在了整个循环上。
300 轮实测(种子 1..300,最终档 9):
| 故意写错的地方 | 被抓 | 第几轮 |
|---|---|---|
wrongNoLim(不判溢出) | 300 / 300 | 第 1 轮 |
wrongOne(while (b > 1)) | 268 / 300 | 第 1 轮 |
wrongBreak(提前 break) | 184 / 300 | 第 2 轮 |
wrongLcm(先乘后除) | 142 / 300 | 第 2 轮 |
wrongPrefix(相邻 gcd) | 131 / 300 | 第 1 轮 |
wrongLate(判断晚了一句) | 117 / 300 | 第 2 轮 |
sub / slow / brute(只是快慢不同) | ★ 0 / 300 | — |
11 ★★ 生成器:随机数据把这道题自己做没了,而那个概率算得出来
生成器里最顺手的那句话是 a_i = rnd(1, 10⁹)。它有一个后果 —— 而这个后果是能算出来的:
点「运行 ▶」看结果
| n 个数 | 实测 gcd = 1 的比例 | 1/ζ(n)(算出来的) |
|---|---|---|
| 2 | 60.63% | 60.79% ← 这就是 6/π² |
| 3 | 83.12% | 83.19% |
| 4 | 92.42% | 92.39% |
| 5 | 96.48% | 96.44% |
| 8 | 99.58% | 99.59% |
推导只有一句:两个数同时被质数 p 整除的概率是 1/p²,各个质数互相独立 ⇒
互质的概率 = Π(1 − 1/p²) = 1/ζ(2) = 6/π²;n 个数就把 2 换成 n。
★★★ 于是「输出这 n 个数的 gcd」这一问,在随机数据上几乎恒等于「输出 1」—— 题目自己退化了。 第 24 章问过「这个量取到极端时,题目会退化成哪道更简单的题」, 这一章的答案是:不用取极端,随机就够了。
⚠ 这是「顺手写法」的第十二次,可它的形状是新的: 前十一次都是「我替题目做了一个它没规定的主」(1 号点当根、边权全写 1、初始数组全 0……), 这一次我什么都没多做 —— 是随机本身把题目做没了。
| 档位 | 相对档位 0 改了什么 | One | Break | Prefix | NoLim | Late | Lcm | 最弱支 |
|---|---|---|---|---|---|---|---|---|
| 0(顺手写法) | n ∈ [2,8]、a_i 在 [1,10⁹] 里独立均匀 | 263 | 147 | 158 | 244 | 56 | 84 | 56 |
| 1 | ⚠ 全部乘一个公因子 g ∈ [1,30] | ★ 12 | ★ 4 | 155 | 244 | 37 | 124 | 4 |
| 2 | 一半的数乘公因子 | 221 | 89 | 195 | 244 | 43 | 83 | 43 |
| 3 | 值域压到 [1,50] | 265 | 204 | 157 | ★ 0 | ★ 0 | ★ 0 | 0 |
| 4 | n 拉长到 [10,20] | 300 | 174 | 297 | 300 | ★ 0 | 5 | 0 |
| 5 | 混入 1 | 285 | 187 | 116 | 221 | 50 | 84 | 50 |
| 6 | 值域贴着上限取 | 266 | 148 | 161 | 244 | 50 | 82 | 50 |
| 7 | 相邻成倍数关系 | 240 | 131 | 200 | 203 | 36 | 74 | 36 |
| 8 | ★★ n 压短到 [3,5] | 263 | 174 | 128 | 300 | ★ 116 | 154 | ★ 116 |
① ⚠ 档位 1 是这一章的反面教材。
我按上面那条「随机数据把题目做没了」去补公因子,一口气全加上 ——
结果 wrongOne 从 263 掉到 12、wrongBreak 从 147 掉到 4。
道理很直白:甲组那两个 bug 要的正是「gcd 会变成 1」,我把这条路堵死了。
★ 第 31 章那条「某一支永远走不到是坑,某一支占得太多也是坑」的又一次现场 —— 而这次是我照着上一条教训做,做过了头。 ⇒ 改成「只给一半的数乘公因子」(档位 2),Prefix 涨到 195,甲组也活了下来。
② ⚠⚠ 而全场最大的功臣,是一个和直觉相反的旋钮:把 n 压短。
最弱支 56 → 116,翻了一倍还多,而且 wrongNoLim 直接满分。
为什么?wrongLate 只在溢出那一步的乘积撞破 long long、而且绕回去之后落在 10¹⁸ 以内时才现形;
n 一大,它绕回去之后后面还有好几次乘法,迟早会有一次让 l > 10¹⁸ 成立 —— 于是它蒙对了 -1。
★★ 数据越多越容易抓 bug,这句话在这里是反的。
③ ⚠ 对照着看档位 4(n 拉长)就更清楚了:它把 wrongOne 顶到 300、wrongPrefix 顶到 297,
看着是全表最漂亮的一档 —— 可它同时把两个溢出 bug 打成 0 和 5。
★★ 同一个旋钮的两头,正好是两组 bug 的天堂和坟墓。 (第 38 章「n 是不是 2 的幂」那次的第二次现场,这次的旋钮是 n 本身。)
| 档位 | 内容 | One | Break | Prefix | NoLim | Late | Lcm | 最弱支 |
|---|---|---|---|---|---|---|---|---|
| 9(最终档) | 8 + 6(n 压短 + 值域贴上限) | 268 | 184 | 131 | 300 | 117 | 142 | ✓ 117 |
| 10(对照) | 9 + 一半的数乘公因子 | 206 | 105 | 162 | 300 | 95 | 179 | 95 |
| 11(对照) | 9 + 混入 1 | 289 | 202 | 89 | 222 | 96 | 118 | 89 |
| 12(诊断) | 9,但公因子改成全部都乘 | ★ 13 | ★ 10 | 146 | 300 | 110 | 226 | 10 |
① 「混入 1」是负分,撤回(117 → 89)。第 32、34、35、37、39 章那条调优不可加的又一次。
② ⚠ 而档位 10 那笔账要老实写:它是有用的,可我没留。
加上「一半的数乘公因子」之后:wrongPrefix 131 → 162,
而且 300 轮里 gcd ≠ 1 的从 32 轮涨到 94 轮(边界覆盖实打实变好了)。
代价是最弱支从 117 掉到 95。
★★ 这和第 38 章那次判据相同、结论相反,值得并排记一次: · 第 38 章为了边界覆盖留下了一个抓获率略差的旋钮(最弱支 222 → 207,掉 7%); · 这一章同样是为了边界覆盖,可代价是 117 → 95(掉 19%)—— 太贵,不留。 ⇒ 判据从来只有一条(让最弱的那一支尽量强),变的是代价有多大。 ⚠ 但那一支不能没有,所以它作为可重跑的档位 10 留在生成器里 —— 读者随时能跑到「数据带公因子」的那一支。
12 动画二:四种做法的圈数 —— 而它们的答案完全相同
count.cpp 一字不差。- 一大一小 → 相减法崩(十亿次);
- 两个大质数 → 枚举约数崩(要试到 min);
- 两个一样的数 → 前三种同时一步到头,只有分解质因数还得跑 √a。
⚠ 而这四根条对应的四份代码,答案逐字节相同(check:viz 里 300 轮钉着)。
★ 这就是「对拍看不见慢」的第五次现场,而且这次四份代码的想法两两不同。
13 选讲:扩展欧几里得(第 42 章会用到)
递归回来时已经有 b·x' + (a mod b)·y' = g,而 a mod b = a − ⌊a/b⌋·b,代进去:
b·x' + (a − ⌊a/b⌋·b)·y' = a·y' + b·(x' − ⌊a/b⌋·y') = g⇒ x = y’,y = x’ − ⌊a/b⌋ · y’。边界:b = 0 时 (x, y) = (1, 0)。∎
★ 它顺带证明了裴蜀定理:a·x + b·y 能取到的最小正整数正好是 gcd(a,b) ——
于是「a·x + b·y = c 有整数解 ⟺ gcd(a,b) | c」。第 42 章求逆元时会直接用它。
点「运行 ▶」看结果
⚠ 两个细节:
- 验算必须用
__int128:a、b到 10⁹ 时a·x会超 long long(x本身能到 10⁹ 量级)。 - 看那一行
gcd(121393, 75025) = 1,x = 28657,y = −46368—— ★ 全是斐波那契数。 最坏输入的系数也长在同一个数列上,不是巧合。
14 自测
gcd(a,b) = gcd(b, a mod b)—— 证的是两对数的公约数集合相同,不是最大值相同。 证不动的时候换一个更大的对象去证(第 19、34 章同款)。- 上界是 O(log),最坏是相邻的斐波那契数,而随机数据只跑在最坏的 40% 上。 ★ 而斐波那契之所以最坏,恰恰因为那时候每一步的商都是 1,取模退化成了相减。
- 随机数据会把这道题自己做没了(n 个数 gcd = 1 的概率是 1/ζ(n))—— ⚠ 可照着这条去补公因子,补过头就把另外两个 bug 打崩了(263 → 12、147 → 4)。 ★ 而全场最大的功臣是个反直觉的旋钮:把 n 压短(最弱支 56 → 116)。
⚠ 下一章(质数)会把「试除」这件事从这里接过去: 这一章分解一个数要试到 √a,下一章要问的是一次把 1..n 全部筛出来要多少步。