拓展欧几里得算法讲解

深入浅出扩展欧几里得算法

1. 从欧几里得到扩展欧几里得

在数论的世界里,我们很早就会遇到欧几里得算法(辗转相除法),它像一把精准的尺子,能够高效地丈量出两个整数的最大公约数 $ \gcd(a, b) $。

然而,生活不止一面,数学也是如此。欧几里得算法只告诉了我们“最大公约数是多少”,却没有告诉我们更多关于这两个数之间关系的秘密。扩展欧几里得算法(Extended Euclidean Algorithm,简称 exgcd)就是在欧几里得算法的基础上,向前迈进的一步——它不仅求出 $ \gcd(a, b) $,还试图找到整数 $ x $ 和 $ y $,使得它们满足裴蜀等式(Bézout‘s identity):

\[a x + b y = \gcd(a, b) \]

这个等式在数论中有着基石般的地位,它告诉我们,$ \gcd(a, b) $ 一定可以表示为 $ a $ 和 $ b $ 的线性组合。而扩展欧几里得算法,就是打开这扇门的钥匙。

2. 核心思想与数学推导

学习扩展欧几里得,最好的方式不是死记硬背代码,而是顺着递归的思路,亲手推导一遍。

想象一下我们正在处理两个数 $ a $ 和 $ b $。我们想要找到 $ (x, y) $ 使得:

\[a x + b y = \gcd(a, b) \]

根据欧几里得算法的原理,$ \gcd(a, b) = \gcd(b, a \bmod b) $。那么,对于下一层递归的系数 $ b $ 和 $ a \bmod b $,我们假设已经求出了一组解 $ (x', y') $,使得:

\[b x' + (a \bmod b) y' = \gcd(b, a \bmod b) = \gcd(a, b) \]

现在,关键的一步来了。我们需要找到当前层的 $ (x, y) $ 与下一层的 $ (x', y') $ 之间的关系。

我们知道,取模运算可以转化为除法:$ a \bmod b = a - \lfloor a/b \rfloor \cdot b $。将其代入上一层的等式:

\[b x' + (a - \lfloor a/b \rfloor \cdot b) y' = \gcd(a, b) \]

将式子展开并重新组合 $ a $ 和 $ b $ 的系数:

\[a y' + b (x' - \lfloor a/b \rfloor \cdot y') = \gcd(a, b) \]

看!我们得到了一个形式为 $ a \cdot (\text{something}) + b \cdot (\text{something else}) = \gcd(a, b) $ 的等式。对比我们最初的目标 $ a x + b y = \gcd(a, b) $,可以发现一组非常优美的对应关系:

\[\begin{cases} x = y' \\ y = x' - \lfloor a/b \rfloor \cdot y' \end{cases} \]

这就是扩展欧几里得算法的核心递归关系。

那么,递归的终点在哪里?回想欧几里得算法的终点:当 $ b = 0 $ 时,$ \gcd(a, 0) = a $。此时,我们需要找到一组 $ (x, y) $ 使得:

\[a x + 0 \cdot y = a \]

很显然,$ 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) 这一行是精髓。我们故意将 yx 的位置交换传入,这样在递归返回时,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) $,其通解形式如下:

\[\begin{cases} x = x_0 + \dfrac{b}{\gcd(a, b)} \cdot t \\ y = y_0 - \dfrac{a}{\gcd(a, b)} \cdot t \end{cases} \quad (t \in \mathbb{Z}) \]

其中 $ (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 $,即:

\[a x + b y = 1 \]

题目保证有解,根据裴蜀定理,这意味着 $ \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 $。相遇条件为:

\[x + m t \equiv y + n t \pmod{L} \]

移项得:

\[(x - y) + (m - n) t \equiv 0 \pmod{L} \]

这意味着存在一个整数 $ k $ 使得:

\[(x - y) + (m - n) t = k L \]

整理成标准的不定方程形式:

\[(m - n) t - L k = y - x \]

为了使用 exgcd,我们通常将方程写成 $ A x + B y = C $ 的形式。令 \(A = m - n\)\(B = -L\)\(C = y - x\),则方程为:

\[A t + B k = C \]

我们的目标是求最小的正整数 $ t $。

解题步骤

  1. 处理负数:为了确保 exgcd 过程的稳定,我们通常将 $ A $ 转为正数。如果 $ A < 0 $,则取反 $ A $ 和 $ C $(这相当于将方程两边乘以 -1)。
  2. 判断无解:令 $ g = \gcd(|A|, |B|) $。根据裴蜀定理,方程有解的充要条件是 $ g \mid C $。如果不满足,输出 "Impossible"
  3. 求解特解:利用 exgcd 求 $ A x + B y = g $ 的一组特解 $ (x_0, y_0) $。则原方程 $ A t + B k = C $ 的一组特解为 $ t_0 = x_0 \cdot \frac{C}{g} $。
  4. 调整最小解:通解中 $ 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 X + b Y = a (x_0 - 1) + b (-y_0 - 1) = a x_0 - a - b y_0 - b = (a x_0 + b y_0) - a - b = 1 - a - b \]

这是一个负数。我们可以在这个等式两边加上 $ a b $ 来调整,得到:

\[a (X + b) + b Y = a b - a - b + 1 \]

但这样并不是最大。最终推导出的最大不可表示数正是 $ 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美化

posted @ 2026-02-27 13:47  Co_led  阅读(6)  评论(0)    收藏  举报