← 目录 / 算法文档 · 模块十九 拓展专题 / 19.3 数论进阶

19.3 数论进阶

模运算下没有现成的除法——扩展欧几里得算法能求出"模意义下的倒数",把除法问题转换成乘法问题。

本页目录
① 为什么需要扩展欧几里得

1 模块讲过的欧几里得算法(辗转相除法)能求出 gcd(a,b),但只告诉我们"最大公约数是多少",没有告诉我们怎么用 ab 凑出这个最大公约数扩展欧几里得算法(Extended Euclidean Algorithm,简称 ExGCD)在求 gcd(a,b) 的同时,还能求出一组整数解 x, y,满足:

裴蜀定理(贝祖定理)

a×x + b×y = gcd(a, b)

这样的整数解 x, y 一定存在(这是数论里的裴蜀定理),扩展欧几里得算法能把它们具体求出来。这个结果是后面求逆元(本节 ④)、解线性同余方程等一系列数论问题的基础工具。

② 核心思想:从 gcd(b, a%b) 反推 gcd(a, b)

回忆欧几里得算法的递归关系:gcd(a,b) = gcd(b, a mod b),递归到 b=0 时,gcd(a,0)=a。扩展欧几里得在这个递归的基础上,多带一步"回溯":假设已经递归算出了 b×x₁ + (a mod b)×y₁ = gcd(b, a mod b),可以推出一组满足 a×x + b×y = gcd(a,b)x, y——递归返回的过程中,把子问题的解"翻译"成当前问题的解。

推导步骤内容
已知b×x₁ + (a mod b)×y₁ = gcd(b, a mod b) = gcd(a, b)
代入 a mod b = a - (a/b)×bb×x₁ + (a - (a/b)×b)×y₁ = gcd(a, b)
展开、按 a、b 重新归类a×y₁ + b×(x₁ - (a/b)×y₁) = gcd(a, b)
对照 a×x+b×y=gcd(a,b)x = y₁,y = x₁ - (a/b)×y₁
③ 图解:手算 ExGCD(30, 18)

递归求解 ExGCD(30, 18),先一路递归到底(和普通欧几里得算法的过程一样),再逐层回溯计算 x, y

递归 + 回溯的完整过程
调用a mod bgcd回溯算出的 x, y
ExGCD(6, 0)—(b=0,递归到底)6x=1, y=0
ExGCD(12, 6)12 mod 6 = 06x=y₁=0,y=x₁-(12/6)×y₁=1-2×0=1
ExGCD(18, 12)18 mod 12 = 66x=y₁=1,y=x₁-(18/12)×y₁=0-1×1=-1
ExGCD(30, 18)30 mod 18 = 126x=y₁=-1,y=x₁-(30/18)×y₁=1-1×(-1)=2
最终解出 x=-1y=2:验证一下 30×(-1) + 18×2 = -30+36 = 6,正好等于 gcd(30,18)=6。整个过程只在最底层(b=0)直接给出解 (x=1,y=0),之后每一层回溯都只是套用②推导出的公式,不需要重新计算。
④ 逆元:模运算下的"倒数"

在普通实数运算里,a 除以 b 等于 a 乘以 b 的倒数 1/b。但在模运算的世界里没有"分数"这回事——如果需要在模 m 的意义下"除以 a",就要找到 a逆元 a⁻¹,满足:

逆元的定义

a × a⁻¹ ≡ 1 (mod m)

求出 a⁻¹ 之后,"除以 a"就可以换成"乘以 a⁻¹"来处理——这在很多需要"对大数取模"的组合数学、概率类问题里非常常用(比如算出一个很大的分数结果,要求对 10⁹+7 取模)。逆元存在的条件是 gcd(a, m) = 1am 互质),否则逆元不存在。

求逆元的方法:把 a×a⁻¹ ≡ 1 (mod m) 改写成 a×x + m×y = 1(这里 gcd(a,m)=1),用 ExGCD 求出的 x 就是答案(如果 x 是负数,加上 m 调整到 [0, m) 范围内)。以求 3 关于模 7 的逆元为例:ExGCD(3, 7) 算出 x=-2y=1(验证:3×(-2)+7×1=-6+7=1),调整负数:-2 mod 7 = 5。验证 3×5=15=14+1≡1 (mod 7)3 关于模 7 的逆元是 5

⑤ 完整代码
C++ · 扩展欧几里得与逆元
1// 求 a、b 的最大公约数,同时算出满足 a*x+b*y=gcd(a,b) 的 x, y(引用传参带回结果)
2int ExGCD(int a, int b, int& x, int& y)
3{
4 if (b == 0) { x = 1; y = 0; return a; } // ★ 递归到底:gcd(a,0)=a,此时 x=1,y=0
5 int x1, y1;
6 int g = ExGCD(b, a % b, x1, y1); // 先递归求子问题
7 x = y1; // ★ 回溯:套用②推导出的公式
8 y = x1 - (a / b) * y1;
9 return g;
10}
11
12// 求 a 关于模 m 的逆元;返回 -1 表示逆元不存在(a、m 不互质)
13int Inverse(int a, int m)
14{
15 int x, y;
16 int g = ExGCD(a, m, x, y);
17 if (g != 1) return -1; // ★ 不互质,逆元不存在
18 return ((x % m) + m) % m; // ★ 调整到 [0, m) 范围内
19}
💡
还有一种更简单的求逆元方法——费马小定理:如果模数 m 恰好是质数,可以证明 a^(m-2) mod m 就是 a 的逆元,用 1.7 节学过的快速幂就能求出来,代码比 ExGCD 更短。但这个方法要求 m 必须是质数;如果 m 不是质数(只要求 gcd(a,m)=1),就必须用本节的 ExGCD 方法,适用范围更广。
⑥ 常见陷阱
忘记检查 gcd(a,m) 是否等于 1 就直接用 ExGCD 求逆元:如果 am 不互质,逆元根本不存在——ExGCD 依然会返回一组 x,y,但满足的是 a×x+m×y=gcd(a,m)(不是 1),直接拿这个 x 当逆元用会得到错误结果。必须先判断第 17 行的 g==1,再决定逆元是否存在。
ExGCD 算出的 x 可能是负数,忘记调整:直接返回的 x(比如本节例子里的 -2)在数学上是正确的解,但作为"模 m 意义下的逆元",通常要求落在 [0, m) 这个范围内。第 18 行 ((x % m) + m) % m 这个写法,和 17.1 节字符串哈希处理负数取模的技巧是同一个道理——先加一个 m 再取模,确保结果非负。
把"求逆元"和"求普通除法"混淆:逆元只在模运算的语境下有意义——如果题目根本不涉及取模,就不需要求逆元,普通的整数除法或浮点数除法就够用了。逆元通常出现在"结果需要对某个数取模"(常见于组合数学、概率 DP 等场景)又同时涉及除法运算的题目里。
🏆
全书回顾:从最开始的变量、循环、数组,到本节的扩展欧几里得——这趟旅程走过了 C++ 语法的方方面面,也走过了从枚举、排序、贪心这些基础算法,到搜索、动态规划、图论、字符串、数据结构,一路到树上倍增和博弈论这些进阶专题。不同章节之间反复出现的"状态定义、转移方程"、"贪心选择"、"分治与递归"这些思路,比任何一道具体的题目都更值得留在脑子里——遇到新问题时,先想想它更像本书里学过的哪一类问题,往往就已经找到了解题的方向。