扩展欧几里得算法(EXGCD)

Published on7/29/2025
Created on 10/26/2021

by Chesium 2021-10-26

前置知识

扩展欧几里得算法

用于计算形如 ax+by=max+by=m 的丢番图方程的整数解。 由裴蜀定理可知,该方程有整数解当且仅当 gcd(a,b)m\gcd(a,b)\mid m,我们先求 ax+by=gcd(a,b)ax+by=\gcd(a,b) 的解,将求出的 xxyy 乘上 mgcd(a,b)\frac{m}{\gcd(a,b)} 就是原方程的解了。 既然是“扩展欧几里得算法”,那肯定是从求最大公因数的欧几里得算法变化而来的:

int gcd(int a,int b){
 return b?gcd(b,a%b):a;
}

我们通过修改该算法,对每次递归的 aka_kbkb_k 逐次解 akx+bky=gcd(a,b)a_kx+b_ky=\gcd(a,b) 即可最终解出 ax+by=gcd(a,b)ax+by=\gcd(a,b),先考虑递归边界,此时有 b=0b'=0a=gcd(a,b)a'=\gcd(a,b) ,很显然有 1×a+0×b=gcd(a,b)1\times a'+0\times b'=\gcd(a,b)。即此时方程的解为 x=1x=1y=0y=0。 接着,假设我们解出了 bx+(a%b)y=gcd(a,b)bx+(a\%b)y=\gcd(a,b) 的一个解为 x=xx=x'y=yy=y' ,我们有:

bx+(a%b)y=gcd(a,b)bx+(aabb)y=gcd(a,b)bx+ayabby=gcd(a,b)ay+b(xaby)=gcd(a,b)\begin{align} bx'+(a\%b)y'&=\gcd(a,b)\\ bx'+(a-\lfloor\frac{a}{b}\rfloor b)y'&=\gcd(a,b)\\ bx'+ay'-\lfloor\frac{a}{b}\rfloor by'&=\gcd(a,b)\\ ay'+b(x'-\lfloor\frac{a}{b}\rfloor y')&=\gcd(a,b) \end{align}

我们令 x=yx=y'y=xabyy=x'-\lfloor\frac{a}{b}\rfloor y' , 则就有 ax+by=gcd(a,b)ax+by=\gcd(a,b) 。这就是扩展欧几里得算法:

  1. 求出 bx+(a%b)y=gcd(a,b)bx+(a\%b)y=\gcd(a,b) 的一组特解 xx'yy'
  2. 则有 x0=yx_0=y'y0=xabyy_0=x'-\lfloor\frac{a}{b}\rfloor y'
  3. 递归边界: 递归到形如 kx+0y=gcd(a,b)kx+0y=\gcd(a,b) 时,其的一组特解是 x=1x=1y=0y=0(此时 gcd(a,b)=k\gcd(a,b)=k
  4. 最后要把 x0x_0y0y_0 乘上 cc

求出特解 x0x_0y0y_0 后,怎么求出通解呢? 设另一组解为 x1=x0+Δxx_1=x_0+\Delta xy1=y0Δyy_1=y_0-\Delta y ,有:

a(x0+Δx)+b(y0Δy)=ax0+by0=gcd(a,b)aΔx=bΔyΔxΔy=ba=b/gcd(a,b)a/gcd(a,b)\begin{align} a(x_0+\Delta x)+b(y_0-\Delta y)&=ax_0+by_0=\gcd(a,b)\\ a\Delta x&=b\Delta y\\ \frac{\Delta x}{\Delta y}&=\frac{b}{a} =\frac{b/\gcd(a,b)}{a/\gcd(a,b)} \end{align}

即, Δx\Delta xΔy\Delta y 的最小取值分别是 dx=bgcd(a,b)d_x=\frac{b}{\gcd(a,b)}dy=agcd(a,b)d_y=\frac{a}{\gcd(a,b)}xxyy 以其作为周期。 接下来,我们想求 xxyy 的最小非负整数值,这就是:

xmin=(x0 % dx+dx) % dxymin=(y0 % dy+dy) % dy\begin{align} x_{min}&=(x_0\ \%\ d_x+d_x)\ \%\ d_x\\ y_{min}&=(y_0\ \%\ d_y+d_y)\ \%\ d_y \end{align}

取模完一次后还要加上一个区间,因为可能 x0x_0y0y_0 是负数。最后又要取模一次,是为了处理 x0x_0y0y_0 是正数时重复加上区间,要回到原来最小的状态。 如果要求最小正整数解,那则要特判一下 xminx_{min}yminy_{min} 为零的情况,此时最小正整数解就应为其区间。 同时,相应的,当 xx 取到最小值时 yy 取到最大值,反之亦然。 至于正整数解的个数,我们可以列出:

{x0+kdx>0y0kdy>0(kZ)\begin{cases} x_0+kd_x>0\\ y_0-kd_y>0\\ \end{cases} \quad(k\in\mathbb{Z})

可以解得:

x0dx<k<y0dy-\frac{x_0}{d_x}<k< \frac{y_0}{d_y}

每一个 kk 的取值就对应着一个正整数解(加上等号就为非负整数解,修改不等式组右侧的值即可求出类似 “ x>px>py>qy>q ” 这种解的个数)

要解形如

axb (mod m)ax\equiv b\ (\mathrm{mod}\ m)

的一元线性同余方程,我们可以将方程变一下形:

axb0 (mod m)axb=km(kZ)ax+km=b(kZ)\begin{align} ax-b&\equiv0\ (\mathrm{mod}\ m)\\ ax-b&=-km&(k\in\mathbb{Z})\\ ax+km&=b&(k\in\mathbb{Z}) \end{align}

这不就是要求一个关于 xxkk 的丢番图方程的整数解吗?刚刚就讲了这个。 求乘法逆元是同理的。

总结:扩展欧几里得算法可以做以下的事:

通过额外的一些处理,其可以:

参考代码:(P5656 【模板】二元一次不定方程 (exgcd) - 洛谷)

#include <cctype>
#include <cstdio>
#include <cstring>
#include <iostream>
//---//
#include <algorithm>
#include <cmath>
#include <vector>
using namespace std;

typedef unsigned int u;
typedef long long ll;
typedef unsigned long long llu;

#define rep(i, a, b) for (ll i = a; i < b; i++)
#define REP(i, a, b) for (ll i = a; i <= b; i++)
#define per(i, b, a) for (ll i = b; i >= a; i--)

ll exgcd(ll a, ll b, ll &x, ll &y) {
  if (b) {
    ll g = exgcd(b, a % b, x, y);
    swap(x, y);
    y -= a / b * x;
    return g;
  } else {
    x = 1;
    y = 0;
    return a;
  }
}

signed main() {
  ll T, a, b, c, g, x0, y0, dx, dy, minx, miny, l, r;
  scanf("%lld", &T);
  while (T--) {
    scanf("%lld%lld%lld", &a, &b, &c);
    if (c % (g = exgcd(a, b, x0, y0)) == 0) {
      x0 *= c / g;
      y0 *= c / g;
      dx = b / g;
      dy = a / g;
      l = x0 % dx == 0 ? -x0 / dx + 1 : ceil(-(double)x0 / dx);
      r = y0 % dy == 0 ? y0 / dy - 1 : floor((double)y0 / dy);
      if (l <= r)
        printf("%lld %lld %lld %lld %lld\n", r - l + 1, x0 + l * dx,
               y0 - r * dy, x0 + r * dx, y0 - l * dy);
      else {
        minx = (x0 % dx + dx) % dx;
        if (minx == 0) minx = dx;
        miny = (y0 % dy + dy) % dy;
        if (miny == 0) miny = dy;
        printf("%lld %lld\n", minx, miny);
      }
    } else
      printf("-1\n");
  }
}

// https://www.luogu.com.cn/record/60967484

参考代码:(P2613 【模板】有理数取余 - 洛谷)

#include <cctype>
#include <cstdio>
#include <cstring>
#include <iostream>
//---//
#include <algorithm>
#include <cmath>
#include <vector>
using namespace std;

typedef unsigned int u;
typedef long long ll;
typedef unsigned long long llu;

#define rep(i, a, b) for (ll i = a; i < b; i++)
#define REP(i, a, b) for (ll i = a; i <= b; i++)
#define per(i, b, a) for (ll i = b; i >= a; i--)

const ll mod = 19260817;

ll read() {
  ll r = 0;
  char c = getchar();
  while (c < '0' || c > '9') c = getchar();
  while (c >= '0' && c <= '9') {
    r = (r * 10 + c - '0') % mod;
    c = getchar();
  }
  return r;
}

void exgcd(ll a, ll b, ll& x, ll& y) {
  if (b) {
    exgcd(b, a % b, x, y);
    swap(x, y);
    y -= a / b * x;
  } else {
    x = 1;
    y = 0;
  }
}

signed main() {
  ll a = read(), b = read(), x, y;
  if (b == 0) return 1 & printf("Angry!");
  exgcd(b, mod, x, y);
  printf("%lld", (x % mod + mod) % mod * a % mod);
}

// https://www.luogu.com.cn/record/60970838