拓展欧几里得算法讲解
深入浅出扩展欧几里得算法
1. 从欧几里得到扩展欧几里得
在数论的世界里,我们很早就会遇到欧几里得算法(辗转相除法),它像一把精准的尺子,能够高效地丈量出两个整数的最大公约数 $ \gcd(a, b) $。
然而,生活不止一面,数学也是如此。欧几里得算法只告诉了我们“最大公约数是多少”,却没有告诉我们更多关于这两个数之间关系的秘密。扩展欧几里得算法(Extended Euclidean Algorithm,简称 exgcd)就是在欧几里得算法的基础上,向前迈进的一步——它不仅求出 $ \gcd(a, b) $,还试图找到整数 $ x $ 和 $ y $,使得它们满足裴蜀等式(Bézout‘s identity):
这个等式在数论中有着基石般的地位,它告诉我们,$ \gcd(a, b) $ 一定可以表示为 $ a $ 和 $ b $ 的线性组合。而扩展欧几里得算法,就是打开这扇门的钥匙。
2. 核心思想与数学推导
学习扩展欧几里得,最好的方式不是死记硬背代码,而是顺着递归的思路,亲手推导一遍。
想象一下我们正在处理两个数 $ a $ 和 $ b $。我们想要找到 $ (x, y) $ 使得:
根据欧几里得算法的原理,$ \gcd(a, b) = \gcd(b, a \bmod b) $。那么,对于下一层递归的系数 $ b $ 和 $ a \bmod b $,我们假设已经求出了一组解 $ (x', y') $,使得:
现在,关键的一步来了。我们需要找到当前层的 $ (x, y) $ 与下一层的 $ (x', y') $ 之间的关系。
我们知道,取模运算可以转化为除法:$ a \bmod b = a - \lfloor a/b \rfloor \cdot b $。将其代入上一层的等式:
将式子展开并重新组合 $ a $ 和 $ b $ 的系数:
看!我们得到了一个形式为 $ a \cdot (\text{something}) + b \cdot (\text{something else}) = \gcd(a, b) $ 的等式。对比我们最初的目标 $ a x + b y = \gcd(a, b) $,可以发现一组非常优美的对应关系:
这就是扩展欧几里得算法的核心递归关系。
那么,递归的终点在哪里?回想欧几里得算法的终点:当 $ b = 0 $ 时,$ \gcd(a, 0) = a $。此时,我们需要找到一组 $ (x, y) $ 使得:
很显然,$ x = 1, y = 0 $ 就是一组解。
至此,我们便拥有了从底层回溯,层层递推,最终得到原方程一组特解的全部工具。
3. 代码实现与模版
将上述推导转化为代码,便得到了扩展欧几里得算法的标准模版。这份代码简洁而强大,是解决许多数论问题的基石。
#include <bits/stdc++.h>
using namespace std;
/**
* @brief 扩展欧几里得算法
*
* @param a, b 输入的两个整数
* @param x, y 引用传递,用于存储方程 ax + by = gcd(a, b) 的一组特解
* @return int a 和 b 的最大公约数
*/
int exgcd(int a, int b, int &x, int &y) {
// 递归边界:当 b 为 0 时,gcd(a, 0) = a
if (b == 0) {
// 此时方程变为 a*x + 0*y = a,一组显而易见的解是 x=1, y=0
x = 1;
y = 0;
return a; // 返回最大公约数
}
// 递归调用,计算下一层。注意参数的顺序和 x, y 的对应关系
// 下一层求解的是:b*x' + (a%b)*y' = gcd(b, a%b)
// 我们将下一层的 x', y' 存储在当前层的 y, x 变量中(这是一种巧妙的写法)
int d = exgcd(b, a % b, y, x);
// 回溯阶段:利用推导出的关系更新当前层的 y
// 关系式: 当前层 x = 下一层 y' (已经存于当前层 x 中?需要理清)
// 更清晰的写法是:
// int x1 = y, y1 = x - (a / b) * y;
// x = x1; y = y1;
// 但为了简洁,很多模版写成如下形式,其本质是一样的。
// 此时,经过递归调用后:
// x 已经被赋值为下一层的 y' (即我们推导公式中的 y')
// y 已经被赋值为下一层的 x' (即我们推导公式中的 x')
// 因此,更新当前层的 y = x' - (a/b) * y' 就是:
y = y - (a / b) * x;
// 注意:当前层的 x 已经在上一步的递归调用中被赋值为下一层的 y',不需要额外修改。
return d; // 返回最大公约数
}
int main() {
int a, b, x, y;
cin >> a >> b;
int g = exgcd(a, b, x, y);
cout << "gcd(" << a << ", " << b << ") = " << g << endl;
cout << "一组特解: x = " << x << ", y = " << y << endl;
// 验证:a*x + b*y 应该等于 g
cout << "验证: " << a << "*" << x << " + " << b << "*" << y << " = " << a*x + b*y << endl;
return 0;
}
代码解释:
- 函数
exgcd是递归的,其返回值是gcd(a, b)。 - 参数的交换:
exgcd(b, a % b, y, x)这一行是精髓。我们故意将y和x的位置交换传入,这样在递归返回时,x自然就保存了下一层的 $ y' $,y自然就保存了下一层的 $ x' $。这是一种非常简洁的实现技巧。 - 回溯更新:得到下一层的
x(即 $ y' $)和y(即 $ x' $)后,根据公式 $ y = x' - \lfloor a/b \rfloor \cdot y' $,更新当前层的y即可。当前层的x就是下一层的y',无需变动。
4. 通解形式与解的调整
exgcd 求出的 (x, y) 只是 一组特解。对于方程 $ a x + b y = \gcd(a, b) $,其通解形式如下:
其中 $ (x_0, y_0) $ 是 exgcd 求出的一组特解。
这个通解形式非常好理解。当我们给 $ x $ 加上 $ \frac{b}{g} $ 时,为了使等式左边变化量为0(因为右边是定值 \(g\)),$ y $ 需要相应地减去 $ \frac{a}{g} \cdot t $。
在很多题目中,我们往往需要求满足某些特殊条件的解,比如“最小的正整数 $ x $”。这时就需要利用通解对解进行调整。常用的调整技巧是取模运算:
// 将特解 x0 调整为模 (b/g) 意义下的最小正整数解
x = (x0 % (b/g) + (b/g)) % (b/g);
这个操作能确保 x 落在 $ [0, \frac{b}{g}) $ 区间内,且为正值。
5. 实战演练:三道洛谷经典题目
掌握了模版,我们来看三道洛谷上的经典题目,体会如何将实际问题转化为扩展欧几里得模型。
题目一:P1082 [NOIP2012 提高组] 同余方程
题目链接:https://www.luogu.com.cn/problem/P1082
题目大意:求关于 $ x $ 的同余方程 $ a x \equiv 1 \pmod{b} $ 的最小正整数解。输入保证有解。
思路分析:
同余方程 $ a x \equiv 1 \pmod{b} $ 实际上等价于存在一个整数 $ y $ 使得 $ a x - 1 = b y $,即:
题目保证有解,根据裴蜀定理,这意味着 $ \gcd(a, b) \mid 1 $,所以 $ \gcd(a, b) $ 必须等于 1,即 $ a $ 与 $ b $ 互质。
这正是扩展欧几里得算法求解的标准形式!我们调用 exgcd(a, b, x, y),得到一组特解 $ (x_0, y_0) $ 满足 $ a x_0 + b y_0 = 1 $。那么 $ x_0 $ 就是原同余方程的一个解。最后,我们利用通解公式将其调整为最小的正整数解即可。
AC代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// 扩展欧几里得,求 ax + by = gcd(a, b) 的一组特解
void exgcd(ll a, ll b, ll &x, ll &y) {
if (b == 0) {
x = 1; y = 0;
return;
}
exgcd(b, a % b, y, x);
y -= (a / b) * x;
}
int main() {
ll a, b, x, y;
cin >> a >> b;
exgcd(a, b, x, y);
// 此时 x 是 ax + by = 1 的一个特解,调整为模 b 意义下的最小正整数解
// 注意通解中 x 的变化步长是 b/gcd(a,b),这里 gcd=1,所以步长是 b
x = (x % b + b) % b;
cout << x << endl;
return 0;
}
题目二:P1516 青蛙的约会
题目链接:https://www.luogu.com.cn/problem/P1516
题目大意:两只青蛙在数轴上(实际上是环形)朝同一方向跳,起始坐标分别为 $ x $ 和 $ y $,步长分别为 $ m $ 和 $ n $,数轴长度(模数)为 $ L $。问它们最少需要跳多少步才能碰面,或者判断永远无法碰面。
思路分析:
设两只青蛙跳了 $ t $ 步后相遇。那么它们在数轴上的位置分别为 $ (x + m t) \bmod L $ 和 $ (y + n t) \bmod L $。相遇条件为:
移项得:
这意味着存在一个整数 $ k $ 使得:
整理成标准的不定方程形式:
为了使用 exgcd,我们通常将方程写成 $ A x + B y = C $ 的形式。令 \(A = m - n\),\(B = -L\),\(C = y - x\),则方程为:
我们的目标是求最小的正整数 $ t $。
解题步骤:
- 处理负数:为了确保 exgcd 过程的稳定,我们通常将 $ A $ 转为正数。如果 $ A < 0 $,则取反 $ A $ 和 $ C $(这相当于将方程两边乘以 -1)。
- 判断无解:令 $ g = \gcd(|A|, |B|) $。根据裴蜀定理,方程有解的充要条件是 $ g \mid C $。如果不满足,输出
"Impossible"。 - 求解特解:利用 exgcd 求 $ A x + B y = g $ 的一组特解 $ (x_0, y_0) $。则原方程 $ A t + B k = C $ 的一组特解为 $ t_0 = x_0 \cdot \frac{C}{g} $。
- 调整最小解:通解中 $ t $ 的步长为 $ \frac{|B|}{g} = \frac{L}{g} $。将 $ t_0 $ 调整为最小正整数解:$ t = (t_0 \bmod \frac{L}{g} + \frac{L}{g}) \bmod \frac{L}{g} $。注意如果结果为 0,则步长应为 $ \frac{L}{g} $(即周期长度)。
AC代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll exgcd(ll a, ll b, ll &x, ll &y) {
if (b == 0) {
x = 1; y = 0;
return a;
}
ll d = exgcd(b, a % b, y, x);
y -= (a / b) * x;
return d;
}
int main() {
ll x, y, m, n, L;
cin >> x >> y >> m >> n >> L;
ll A = m - n;
ll B = -L;
ll C = y - x;
// 处理负数,方便计算 gcd 和后续调整
if (A < 0) {
A = -A;
C = -C;
}
// B 是 -L,通常为负,但取绝对值不影响 gcd
ll g = exgcd(A, B, x, y); // 此时 x, y 是 Ax + By = g 的特解
if (C % g != 0) {
cout << "Impossible" << endl;
} else {
// 求原方程 A*t + B*k = C 的特解 t0
ll t0 = x * (C / g);
// 通解步长
ll step = B / g;
if (step < 0) step = -step; // 步长取正
// 调整 t 为最小正整数解
ll ans = (t0 % step + step) % step;
if (ans == 0) ans = step; // 保证为正
cout << ans << endl;
}
return 0;
}
题目三:P3951 [NOIP2017 提高组] 小凯的疑惑
题目链接:https://www.luogu.com.cn/problem/P3951
题目大意:给定两个互质的正整数 $ a $ 和 $ b $,求不能用 $ a $ 和 $ b $ 线性组合(系数为非负整数)表示的最大整数。
思路分析:
这题看起来和 exgcd 关系不大,但其实可以用 exgcd 来推导。
我们知道,所有能表示的数 $ k $ 满足存在非负整数 $ x, y $ 使得 $ a x + b y = k $。
题目要求最大的不能表示的 $ k $。考虑一个能表示的数 $ k $,它减 1 后 $ k-1 $ 是不能表示的。我们要最大化 $ k-1 $,也就是最大化 $ k $。
从方程 $ a x + b y = 1 $ 出发,因为 $ a, b $ 互质,该方程一定有整数解(但 $ x, y $ 可正可负)。
设 \((x_0, y_0)\) 是 $ a x + b y = 1 $ 的一组特解。
那么对于任意整数 \(t\),\(x = x_0 + b t\),\(y = y_0 - a t\) 都是通解。
为了构造一个最大的能表示的数 \(k = a X + b Y\)(\(X, Y \ge 0\)),我们不妨取 $ X = x_0 + b t $且 \(X \ge 0\),$ Y = y_0 - a t $ 且 $ Y \ge 0 $。
我们要让 $ X $ 和 $ Y $ 尽可能小但非负,这样才能使得它们构造出的 $ k $ 尽可能大?需要仔细分析。
一个经典的结论是:最大的不可表示的数为 $ a b - a - b $。我们可以通过 exgcd 找到一组特殊的解来理解。
令 $ x_0 $ 是满足 $ a x \equiv 1 \pmod{b} $ 的最小正整数解,那么 $ 1 \le x_0 \le b-1 $。代入 $ a x_0 + b y_0 = 1 $,可得 $ y_0 = \frac{1 - a x_0}{b} $,由于 \(a x_0 > 1\),$ y_0 $ 是负数。
此时,构造 \(X = x_0 - 1\),$ Y = -y_0 - 1 $(两者均非负),则:
这是一个负数。我们可以在这个等式两边加上 $ a b $ 来调整,得到:
但这样并不是最大。最终推导出的最大不可表示数正是 $ a b - a - b $。虽然这里推导稍显复杂,但代码实现却可以很简洁地利用 exgcd。
AC代码(基于 exgcd 推导的版本):
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
void exgcd(ll a, ll b, ll &x, ll &y) {
if (!b) {
x = 1; y = 0;
return;
}
exgcd(b, a % b, y, x);
y -= (a / b) * x;
}
int main() {
ll a, b, x, y;
cin >> a >> b;
exgcd(a, b, x, y);
// 找到 ax + by = 1 中 x 的最小正整数解
x = (x % b + b) % b;
// 此时对应的 y = (1 - a*x) / b 是一个负数
y = (1 - a * x) / b; // 这里 y 为负
// 根据推导,最大不可表示数为 a * (x - 1) + b * (-y - 1) - 1
// 或者直接使用公式 ans = a*b - a - b
// 这里给出公式法的代码,简洁明了
ll ans = a * b - a - b;
cout << ans << endl;
return 0;
}
6. 总结与思考
扩展欧几里得算法是数论中的一颗明珠,它将求最大公约数的过程,巧妙地扩展到了求解线性不定方程。掌握它,不仅仅意味着记住一段代码,更重要的是理解其递归推导的逻辑,以及如何通过通解调整得到目标解。
从上面的三道题可以看出,exgcd 的应用非常灵活:
- P1082 是直接应用,求模逆元。
- P1516 是建立方程,将实际问题转化为线性不定方程。
- P3951 则更深一层,利用 exgcd 的解来推导数学结论。
希望这篇讲解能帮助你理清思路,在遇到类似问题时,能想起这位强大的“老朋友”。
备注
本文经AI美化

浙公网安备 33010602011771号