欧几里得算法的推导
为什么 gcd(a,b) = gcd(b, a%b) 成立?令 a = qb + r,若 d | a 且 d | b,则 d | r = a - qb;反之若 d | b 且 d | r,则 d | a。所以 a 和 b 的公因子集合 = b 和 r 的公因子集合,GCD 相等。
package main
import "fmt"
// 递归版(经典)
func gcd(a, b int) int {
if b == 0 { return a }
return gcd(b, a%b)
}
// 迭代版(避免栈溢出)
func gcdIter(a, b int) int {
for b != 0 {
a, b = b, a%b
}
return a
}
// 最小公倍数:lcm(a,b) = a*b / gcd(a,b)
// 先除后乘,防止溢出
func lcm(a, b int) int {
return a / gcd(a, b) * b
}
func main() {
// 分数化简:48/36 = (48/gcd) / (36/gcd) = 4/3
a, b := 48, 36
g := gcd(a, b)
fmt.Printf("%d/%d 化简为 %d/%d\n", a, b, a/g, b/g)
// 任务调度:A 每 12s 触发,B 每 8s 触发,多久同时触发?
fmt.Println("最小公倍数:", lcm(12, 8)) // 24
// 验证欧几里得是对的
fmt.Println(gcd(252, 105)) // 21
fmt.Println(gcd(0, 5)) // 5(边界)
}
扩展欧几里得与贝祖定理
贝祖定理:存在整数 x, y 使得 ax + by = gcd(a, b)。扩展欧几里得算法不只返回 GCD,还返回这组系数 (x, y)。当 gcd(a, m) = 1 时,ax ≡ 1 (mod m),x 就是 a 的模逆元。
package main
import "fmt"
// 扩展欧几里得:返回 (gcd, x, y),满足 a*x + b*y = gcd
func extGCD(a, b int) (int, int, int) {
if b == 0 {
return a, 1, 0
}
g, x1, y1 := extGCD(b, a%b)
// 回溯:a*y1 + b*(x1 - (a/b)*y1) = g
return g, y1, x1 - (a/b)*y1
}
func main() {
g, x, y := extGCD(252, 105)
fmt.Printf("gcd=%d, x=%d, y=%d\n", g, x, y) // gcd=21
fmt.Println(252*x + 105*y) // 21 ✓
// 用 extGCD 求模逆元:3 * x ≡ 1 (mod 11)
_, inv, _ := extGCD(3, 11)
inv = (inv%11 + 11) % 11
fmt.Println(inv, 3*inv%11) // 4 1 ✓
}
应用:最简分数与音乐节拍
| 应用场景 | 用 GCD/LCM | 具体做法 |
|---|---|---|
| 分数化简 | GCD | 分子分母同除 gcd(a,b) |
| 多任务同步触发 | LCM | 求所有周期的最小公倍数 |
| 音乐节拍对齐 | LCM | 不同拍号的公共周期 |
| 模逆元(密码学) | 扩展 GCD | gcd(a,m)=1 时求贝祖系数 |
| 公平分配 | GCD | n 个数的 GCD 是最大公平分量 |
口诀GCD 是公约,LCM 是公倍,扩展 GCD 是密码学的钥匙。
✓推荐做法
- lcm 先除后乘:
a/gcd(a,b)*b,避免 a*b 溢出 - 多个数的 GCD 用 reduce:
gcd(gcd(a,b), c) - Go 1.21+ 可用
math/gcd标准库
✗不推荐
- 用辗转相除以外的方法求 GCD——没有更快的通用方法
- 忘记 gcd(0, n) = n 这个边界
⚠常见误区
- 扩展 GCD 返回的 x 可能是负数,取模时要
(x%m+m)%m
判断标准:能用一句话证明欧几里得递推式的正确性 → 掌握本章。
欧几里得算法是人类历史上第一个有记录的算法,它比零的发明还早 600 年。
— 算法导论