数论

01 / 共享的机制

很多题目底下其实藏着一层数论结构。一旦看出输入里真正存在的整除性 / 模 / 乘性结构, O(N) 的循环就坍缩成 O(log N)O(N log log N)。 三个经典子模式:欧几里得求 GCD、按指数二进制位做快速幂、 用素数筛一次性标记所有合数。

01 / 一句话本质
gcd(a, b) = gcd(b, a % b) —— 同时整除 ab 的数也一定整除余数,所以可以不断用更小的余数替换原值而不丢答案。每一步 pair 都按几何级数下降,log(min(a, b)) 次迭代就结束。LCM 顺势而出:a / gcd(a, b) · b
题目求 gcd(a, b) 与 lcm(a, b)a24b36
当前 (a, b)24a36b历史(24, 36)
a = 24b = 36 开始。反复使用 gcd(a, b) = gcd(b, a % b),直到 b = 0
0 / 5
a
24
b
36
gcd
lcm
0 / 5

02 / 模式骨架

# 欧几里得 GCD —— 在右操作数为 0 时终止def gcd(a, b):while b 0:a, b b, a % b # 缩小return a# LCM —— 先除再乘,避免溢出def lcm(a, b):return a / gcd(a, b) · b

03 / 什么时候用这个模式

"最大公约数 / 公因数"
最经典的用法。LC 1071 —— 「能同时整除两个字符串的最大字符串」化归为对两个长度求 GCD。 只要问题里出现「公因子」「公约数」或「能同时容纳两者的最大单位」,就找欧几里得。
"最小公倍数 / 周期"
需要找同时被 ab 整除的最小数时用 —— 同步周期、重复信号、 「两个事件最早一起发生在何时」。先求 gcd,再算 a / gcd · b —— 先除后乘可以避免 a · b 溢出。
"分数是否最简 / 是否互素"
两个数互素当且仅当 gcd(a, b) = 1。把 p/q 化为最简分数,只要一次 GCD 加两次除法。有理数键的哈希不变量、Stern–Brocot 树导航、Farey 邻居 —— 都立足于此。
"对整个数组求 GCD"
LC 1979。{a₁, …, aₙ} 的 gcd 等于 gcd(…gcd(gcd(a₁, a₂), a₃)…, aₙ) —— 两两折叠。中间值只可能不增, 一旦降到 1 就可以早停;总工作量仍被各次归约的开销之和所界定。

04 / 常见坑

i.
a · b / gcd(a, b) 算 LCM 而没有先除。
先乘后除在 32 位整型上会溢出 —— 即便 64 位,若 ab 接近 2³¹ 也可能炸。永远先除:a / gcd(a, b) · b。 这次除法是精确的(gcd 整除 a),顺序安全, 且中间值不会超过最终答案。
ii.
忘记 b == 0 的终止条件。
没有这层保护,要么死循环(a % 0 未定义或抛异常), 要么更糟 —— 第一步就因为 b == 0 触发除零崩溃。按定义 gcd(a, 0) = a,让这种情形从循环里自然落出。
iii.
负数输入时结果的符号。
约定俗成 gcd 取非负值;某些语言的内建 %(如 C) 的符号跟随被除数,递归中可能落到负的 a 上。 入口处统一取 abs(a)abs(b),或者用语言里数学风格的模运算。 Python 的 % 跟随除数符号,可直接使用;C/Java 不行。

05 / 去 LeetCode 上练习

四道题。前三题是欧几里得归约的直接应用;最后一题借助 Bézout 恒等式 ——gcd(a, b) 是能写成 a·x + b·y 的最小正整数 —— 判定可行性。

— / 什么时候用哪个

GCD / LCM
问题核心是整除性时用 —— 公因数、是否互素、最简分数、由 a · b / gcd(a, b) 求 LCM。欧几里得恒等式 gcd(a, b) = gcd(b, a % b) 每一步都呈几何级数下降。
欧几里得 · LC 1071 · LC 1979
模幂
要计算 aⁿ mod M(n 极大)或质数模下的乘法逆元时用。 每步把指数减半;n 的二进制位决定哪些平方项会被乘进答案。O(log n) 替代 O(n)。
指数减半 · LC 50 · LC 372
素数筛
需要所有不超过 N 的素数(或所有最小素因子)时用。对每个素数 p, 把它的所有倍数标记为合数;每个合数被标记的次数摊销下来是 O(log log N)。
埃氏筛 · LC 204 · LC 952