← 目录 第 37 / 60 节 全部 60 节

37 · 最大公约数、同余与快速幂

数论应用的三件套:辗转相除求 gcd 与 lcm、扩展欧几里得解同余方程与逆元、二进制拆分求 a^b mod m。每一件都附一条防溢出规矩:lcm 先除后乘、快速幂每步取模、乘积超范围上 __int128。
普及/提高-预计 65 分钟gcd扩展欧几里得快速幂
本节出现的新函数 / 新语法(先看懂再用)

上一节把数拆成质数,这一节处理“两个数的公共结构”和“大指数”。三件事:两个数的最大公约数与最小公倍数、同余方程与逆元、ab mod m。第三件是第二件的工具,第二件是第一件的推广——三者用同一套代码互相支撑。

gcd(a, b) = gcd(b, a mod b),递归到 b = 0 就得到答案;ab mod m 只要把 b 拆成二进制,31 轮乘法就能算完。
要做的事数据规模用什么复杂度
两个数的 gcda, b ≤ 10^18辗转相除O(log min(a,b)),≤ 95 步
两个数的 lcm结果 ≤ 9.2×10^18a / gcd * b,先除后乘同上
a·x ≡ 1 (mod m) 的逆元m ≤ 10^9扩展欧几里得 / 费马小定理O(log m)
a^b mod mb ≤ 10^18快速幂O(log b)
两个 10^18 的数相乘再取模mod ≤ 10^18__int128O(1)

先把这几个工具拆开看清。它们都很短,但每一个都有一处“不这么写就错”的细节,这些细节正是这一节真正的考点。

新函数拆解 · std::gcd(C++17)
int g = std::gcd(a, b); long long g = std::gcd(x, y);
组成含义 / 格式要求
头文件<numeric>。<bits/stdc++.h> 已经包含,不用额外写。
两个参数类型必须相同gcd(3, 4LL) 编译不过(模板推导冲突)。竞赛里统一用 long long 最省事。
返回值非负。gcd(−12, 18) = 6,它内部按绝对值算。
gcd(x, 0) = |x|辗转相除的终止条件正好是 b = 0,所以 0 是合法参数,不用特判。
C++17 之前只能手写辗转相除,或用 GNU 扩展 __gcd(a, b)(非标准,MSVC 没有)。
手写版四行:while (b) { t = a % b; a = b; b = t; } return a; 逻辑与递归版完全一样。
最小可运行示例
示例
#include <bits/stdc++.h>
using namespace std;

int main() {
    cout << gcd(12, 18) << "\n";        // 6
    cout << gcd(0, 7)   << "\n";        // 7:gcd(0, x) = |x|
    cout << gcd(-12, 18)<< "\n";        // 6:取绝对值

    long long a = 1000000000000LL, b = 8;
    cout << gcd(a, b) << "\n";          // 4:两个都是 long long

    // 手写辗转相除(不依赖 C++17)
    long long x = a, y = b;
    while (y) { long long t = x % y; x = y; y = t; }
    cout << x << "\n";                  // 4
    return 0;
}
注意

gcd(3, 4LL) 这类“一个 int 一个 long long”的写法直接编译失败(no matching function)。出错时先看参数的声明类型。

手写版若 a、b 可能为负数,先各自取绝对值:C++ 的 % 对负数返回负余数,不处理会死循环。

新函数拆解 · lcm:先除后乘
long long lcm(long long a, long long b) { return a / gcd(a, b) * b; }
组成含义 / 格式要求
a * b / gcd错。a = b = 4×10^9 时 a·b = 1.6×10^19,超出 long long 上限 9.22×10^18,先溢出再除,结果毫无意义。
a / gcd * b对。gcd 一定整除 a,先除不丢精度;中间值最大就是最终答案,不会比 lcm 更大。
为什么顺序有效公因子被提前消掉了。两个数相乘只做一次,谁先谁后不影响结果,只影响中间值的大小。
std::lcm 也存在C++17 提供,同样先除后乘;但竞赛里更常手写——模意义下的 lcm 要乘逆元,标准库帮不上。
返回值lcm(a, b) = a / gcd(a, b) * b 对正整数恒成立;带负数时先取绝对值。
装不下的情况lcm 本身超过 9.2×10^18 时,任何 long long 写法都没救,要么题目保证范围,要么上 __int128。
最小可运行示例
示例
long long lcm(long long a, long long b) {
    return a / gcd(a, b) * b;      // 先除后乘
}

// 反例:a = b = 4e9
//   a * b / gcd = 1.6e19 / 4e9   → 1.6e19 已超出 long long,溢出成负数
//   a / gcd * b = 1e9 * 4e9      → 4e18,正确
注意

写乘法前先估一次中间值。这是数论题里最常见、也最难查的一类 WA:答案小的时候全对,大测试点突然变成负数。

a ≥ b 时先算 a / gcd 得到的商更小,习惯上把大的放前面。

新函数拆解 · __int128(128 位整数)
__int128 x = (__int128)a * b % m;
组成含义 / 格式要求
范围约 ±1.7×10^38。两个 10^18 的数相乘得 10^36,装进去还剩两位数量级的余量。
不是标准类型GCC / Clang 支持(洛谷、Codeforces、NOI 系列评测机都是 GCC),MSVC 不支持。别名 __int128_t。
不能直接读入输出cin、cout、scanf、printf 全不认识它。读入先读进 long long 再赋值;输出要手写按位打印。
转换要写在乘法之前(__int128)a * b 正确:a 先被提升,b 自动跟着提升。(__int128)(a * b) 错误:乘法已经在 long long 里溢出,再转也救不回来。
速度加减与 long long 接近,乘除取模慢 3~10 倍。10^8 次循环里慎用。
典型用法把一次可能溢出的乘法“抬”到 128 位算完,取模后降回 long long。
最小可运行示例
示例
// ① 防溢出的乘法取模:a、b < m ≤ 10^18
long long mulMod(long long a, long long b, long long m) {
    return (long long)((__int128)a * b % m);   // 中间 10^36,long long 装不下
}

// ② 打印 __int128:cout 不认识它,只能自己按位输出
void print128(__int128 x) {
    if (x < 0) { putchar('-'); x = -x; }
    if (x > 9) print128(x / 10);
    putchar('0' + (int)(x % 10));
}
注意

(long long)(...) 这一步不能省:表达式类型是 __int128,赋给 long long 变量时虽会自动截断,但显式写出来更清楚,也避免 cout 直接输出它。

本节题目 k ≤ 2^31,(2^31)² = 4.6×10^18 还在 long long 内,其实不用 128 位;但模板写成 __int128 版本可以无脑复用,不必每次重新估范围。

新函数拆解 · 快速幂(二进制拆分指数)
long long qpow(long long a, long long b, long long mod);
组成含义 / 格式要求
a底数。进门先 a %= mod,否则第一次乘法就可能用到大数。
b指数。必须用 long long:2^31 = 2147483648 已经超过 int 上限 2147483647。
mod模数。mod = 1 时任何数都同余于 0,结果必须是 0。
r = 1 % mod累乘器初值。直接写 1 在 mod = 1 且 b = 0 时会返回 1,答案应为 0;写 1 % mod 一劳永逸。
b & 1取 b 的最低位。为 1 表示当前这份 a^(2^k) 要乘进答案。
a = a * a % moda 依次变成 a2、a4、a8……每轮平方一次。
b >>= 1右移一位,处理下一个二进制位。循环次数 = b 的二进制位数 ≤ 63。
最小可运行示例
示例
long long qpow(long long a, long long b, long long mod) {
    long long r = 1 % mod;              // 处理 mod = 1 的退化情况
    a %= mod;
    while (b) {
        if (b & 1) r = r * a % mod;     // 这一位是 1:把 a^(2^k) 乘进来
        a = a * a % mod;                // a 平方,准备下一位
        b >>= 1;
    }
    return r;
}

// qpow(2, 10, 9)   = 1024 % 9 = 7
// qpow(2, 0, 1000) = 1
// qpow(7, 5, 1)    = 0
注意

每一步都要取模。写成 r = r * a 最后统一取模,b ≥ 40 就溢出,症状是中等数据突然错、大数据变成负数。

mod ≤ 2^31 时 r·a 不超过 4.7×1018,long long 够用;mod 到 10^18 就必须把两次乘法换成 __int128。

例题 · 【模板】快速幂洛谷 P1226普及/提高-去洛谷做这题 ↗

输入三个整数 b、p、k(1 ≤ b, p, k ≤ 231),求 bp mod k 的值。

输出格式严格为 b^p mod k=s,其中 s 是结果,等号两边没有空格。

注意:p 最大 231,用 int 读入会失败。

输入: 2 10 9 输出: 2^10 mod 9=7
例题怎么想:把指数拆成二进制

先算朴素做法要多久:循环 p 次做一次乘法,p ≤ 231 = 21 亿,就算每纳秒一次乘法也要 2 秒以上,必然 TLE。所以必须减少乘法次数,而不是优化每一步。

关键观察:p 的二进制最多 31 位,p 就是若干个 2 的幂之和。于是 b^p 等于对应的那些 b^(2^k) 相乘——只要手里有 b^1、b^2、b^4、…、b^(2^30) 这 31 个数,按 p 的二进制位挑出来相乘即可。

这 31 个数不用分别算:后一个是前一个的平方。(b^2)^2 = b^4,(b^4)^2 = b^8,一路平方下去,正好 31 次。

取模可以随时插入:因为 (x·y) mod m = ((x mod m)·(y mod m)) mod m。每一次乘法后立刻取模,中间值就永远不超过 m² ≤ 4.6×1018,long long 装得下。

三步循环
r = 1, a = b % k
当 p > 0:
    若 p 的最低位是 1:r = r * a % k     ← 这一位对应的幂要乘进来
    a = a * a % k                        ← 幂次翻倍:a, a², a⁴, ...
    p >>= 1                              ← 右移,看下一位
输出 r

两处细节必须写在代码里,否则有测试点过不了:p 用 long long 读(231 超 int);输出用原始的 b、p、k(不是在函数里取过模的副本)。

完整代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

// 计算 a^b mod m
ll qpow(ll a, ll b, ll m) {
    ll r = 1 % m;                          // m = 1 时答案恒为 0,1 % 1 = 0
    a %= m;                                // 底数先进模,后面的乘法才安全
    while (b) {
        if (b & 1) r = (__int128)r * a % m; // 这一位是 1,把当前的 a^(2^k) 乘进答案
        a = (__int128)a * a % m;            // 平方,得到下一份的幂
        b >>= 1;                            // 处理下一个二进制位
    }
    return r;
}

int main() {
    ll b, p, k;
    cin >> b >> p >> k;                     // p 可达 2^31,必须 long long
    cout << b << "^" << p << " mod " << k << "=" << qpow(b, p, k) << "\n";
    return 0;
}
逐行拆解
代码片段含义与理由
ll r = 1 % m;累乘器初值。写 1 的话,m = 1 且 b = 0 时返回 1,而正确答案是 0。
a %= m;底数先取模。否则 b 的最低位就是 1 时,第一轮 r * a 直接用到大数,乘完再取模已经溢出。
while (b)b 每轮右移一位,循环次数等于 b 的二进制位数,p ≤ 2^31 时最多 31 轮。
if (b & 1)取最低位。这一位是 1 表示 a^(2^k) 属于乘积,要乘进 r;是 0 就跳过。
(__int128)r * a % m抬起一轮再取模。m 到 10^18 时 r·a 会溢出 long long,这样写一劳永逸。
a = (__int128)a * a % m;平方。注意这里用的是取模后的 a,与 r 无关,两行互不干扰。
b >>= 1;右移一位。写成 b /= 2 结果一样,移位更贴合“二进制位”的语义。
cin >> b >> p >> k;三个变量都是 long long:p 最大 2^31 = 2147483648,int 上限只有 2147483647,用它读会失败。
cout << b << "^" ...输出的是原始 b、p、k,不是函数里取过模的副本。格式里 ^ 和 = 两边都没有空格。

快速幂解决“指数很大怎么算”。数论的另一半问题是“除法怎么取模”。加减乘都可以边算边取模,只有除法不行:(a / b) mod m 既不等于 (a mod m) / (b mod m),很多时候压根不是整数。要让“除以 b”变成“乘以 b 的逆元”,就得解同余方程 b·x ≡ 1 (mod m)。

运算能否直接取模正确写法
(a + b) mod m能(a mod m + b mod m) mod m
(a − b) mod m能((a mod m − b mod m) + m) mod m,先加 m 防负
(a × b) mod m能快速幂每步取模的依据;中间乘积超范围就上 __int128
(a ÷ b) mod m不能乘以 b 的逆元 b−1,前提是 gcd(b, m) = 1

求逆元要先会解 a·x + b·y = gcd(a, b)。裴蜀定理保证这个方程一定有整数解,而把辗转相除的每一步“回代”就能把解构造出来——这就是扩展欧几里得。

扩展欧几里得(递归回代版)
// 求 a*x + b*y = gcd(a,b) 的一组整数解,返回 gcd(a,b)
// x、y 用引用传递,返回值通过参数带出(第 11 节学过)
long long exgcd(long long a, long long b, long long &x, long long &y) {
    if (b == 0) {                    // 出口:a*1 + 0*0 = a
        x = 1; y = 0;
        return a;
    }
    long long x1, y1;
    long long g = exgcd(b, a % b, x1, y1);   // 先让规模变小,再回代
    x = y1;                          // a 的系数
    y = x1 - (a / b) * y1;           // b 的系数
    return g;
}
回代是怎么来的
代码片段含义与理由
if (b == 0) { x = 1; y = 0; return a; }出口。此时 a 就是 gcd(a, b),取 x = 1、y = 0 满足 a·1 + 0·0 = a。
exgcd(b, a % b, x1, y1)规模从 (a, b) 变成 (b, a mod b),和辗转相除走的是同一条路,所以递归深度也同阶。
b·x1 + (a mod b)·y1 = g递归返回时我们手里有这一条等式。
a mod b = a − (a / b)·b把上式里的 a mod b 展开:b·x1 + (a − (a/b)·b)·y1 = a·y1 + b·(x1 − (a/b)·y1)。
x = y1;对照展开后的式子,a 的系数是 y1,所以新的 x 就是 y1。
y = x1 - (a / b) * y1;b 的系数是 x1 − (a/b)·y1,这就是新的 y。
返回 g顺手拿到 gcd,省得再调一次,也是判断“方程有没有解”的依据。

拿到 exgcd 之后,逆元就是一行的事:a·x + m·y = 1 里的 x 就是 a 模 m 的逆元。另一种求法是把上一节的快速幂接上来——当 m 是质数时,费马小定理直接给答案。

两种求逆元
// ① 扩展欧几里得:任意模数,只要 gcd(a, m) == 1 就有逆元
long long invExgcd(long long a, long long m) {
    long long x, y;
    long long g = exgcd(a, m, x, y);     // a*x + m*y = gcd(a, m)
    if (g != 1) return -1;               // 不互质,逆元不存在
    return (x % m + m) % m;              // 归一化到 [0, m)
}

// ② 费马小定理:只在 m 是质数时可用(1e9+7、998244353 这类)
long long invFermat(long long a, long long m) {
    return qpow(a, m - 2, m);            // a^(m-2) ≡ a^(-1) (mod m)
}
逆元的三处关键
代码片段含义与理由
a·x + m·y = gcd(a, m)裴蜀定理:a·x + m·y 能取到的最小正数恰是 gcd(a, m)。所以方程的右边必须能被 gcd 整除。
if (g != 1) return -1;要让右边等于 1,只有 gcd = 1。这就是“逆元存在 ⟺ 互质”的来源。
(x % m + m) % mC++ 的 % 结果符号跟着被除数走,负数取模仍是负数。先 %m 把它拉进 (−m, m),加 m 变正,再 %m 收进 [0, m)。
qpow(a, m - 2, m)费马小定理:m 为质数且 m ∤ a 时 a^(m−1) ≡ 1,两边同除 a 得 a^(m−2) ≡ a^(−1)。
两种做法的取舍费马版是 log m 次乘法,常数小、代码短;exgcd 版任意模数都能用。m 是 1e9+7 时竞赛里普遍用费马版。
这一节的 8 种典型错法(含症状)

WA,答案恒为 0。—— 快速幂的累乘器写成 long long r = 0;。任何数乘 0 都是 0,样例就能看出来。

WA,小数据全对、大数据错误或为负。—— 每步忘了取模,中间值 40 次乘法后溢出。凡是“取模题答案忽正忽负”,先查这一步。

TLE 或输出随机值。—— 指数 p 用 int 读 2^31。cin 解析失败后 p 保持未初始化值,循环次数完全随机,表现为 TLE 或答案离谱。指数、模数一律 long long。

WA,输出负数。—— 逆元直接写 x % m。样例 3 10 恰好得到正数 7 看不出来,换 a = 2、b = 3 会输出 −1(正确答案 2)。必须 (x % m + m) % m。

WA,答案比正确值小且是负数。—— lcm 写成 a * b / gcd(a, b),a、b 到 4×10^9 时乘积溢出。

RE(除零)或无限递归。—— 手写 gcd 的出口写成 if (b == 1) return 1;。该停的时候 b 已经变成 0,下一轮还要算 a % b,除零 RE。必须是 if (b == 0)。

CE(编译失败)。—— gcd(3, 4LL) 类型不一致;或者用 cin >> x 读 __int128。前者加一个 LL 后缀统一类型,后者只能先读 long long 再赋值。

WA,本应输出“无解”却输出了一个数。—— 同余方程 a·x ≡ b (mod m) 在 gcd(a, m) ∤ b 时无解,但 exgcd 照样会返回 a·x + m·y = g 的一组解,直接拿来用就是错的。先判 b % g != 0 再谈求解。

自己动手复现

关掉页面,从空文件把 P1226 敲一遍并提交到洛谷。

改一改 ①:把 while 循环换成 for (ll i = 0; i < p; ++i) r = r * b % k;,输入 2 1000000000 998244353,用 time 计时——亲眼看一次 10 亿次乘法要多久。

改一改 ②:把 if (b & 1) 改成 if (b & 2),输入 2 10 1000,看答案从 24 变成多少。想清楚为什么只“少乘了某些位”。

改一改 ③:把 lcm 写成 a * b / gcd(a, b),输入 a = b = 4000000000,打印中间值 a*b——观察它变成负数,再换回 a / gcd * b 对比。

改一改 ④:用 exgcd 版求逆元,把 (x % m + m) % m 改成 x % m,输入 a = 2、m = 3,输出会变成 −1。这就是“最小正整数解”必须归一化的原因。

数学补给 · 同余、裴蜀与逆元

同余的定义只有一句:a ≡ b (mod m) 表示 m 整除 a − b。它把整数按“除以 m 的余数”分成 m 类,而所有运算都在这 m 个类上进行——这就是“模 m 剩余系”。

为什么加减乘可以随便取模:若 a ≡ a′、b ≡ b′ (mod m),则 a ± b ≡ a′ ± b′、a·b ≡ a′·b′。所以每一步乘完立刻取模,结果不变。这也解释了快速幂为什么能边乘边模。

除法为什么不行:2 ≡ 8 (mod 6),两边同除 2 得 1 与 4,而 1 ≢ 4 (mod 6)。取模把“差 m 的倍数”这个信息扔掉了,而除法恰好要用到它。补救办法是乘逆元。

裴蜀定理:对任意整数 a、b(不全为 0),存在整数 x、y 使 a·x + b·y = gcd(a, b),而且 gcd(a, b) 是所有形如 a·x + b·y 的正数中最小的那个。

通解结构:若 (x0, y0) 是 a·x + b·y = g 的一组解,则全部解为 x = x0 + k·(b/g)、y = y0 − k·(a/g),k 取遍整数。所以对 b 取模后逆元唯一——这就是 (x % m + m) % m 一定对的理由。

费马小定理:p 为质数且 p ∤ a,则 ap−1 ≡ 1 (mod p),于是 a−1 ≡ ap−2。模 109+7 求逆元时 p − 2 = 1000000005,一次快速幂 30 轮乘法搞定。(更一般的是欧拉定理 aφ(m) ≡ 1,条件只有 gcd(a, m) = 1。)

量级感受:gcd 的递归次数不超过 5·log10(min(a, b))(拉梅定理),a、b ≤ 2×109 时不超过 50 层,绝无爆栈风险;快速幂的轮数等于指数的二进制位数,b ≤ 1018 时 60 轮。两个算法在 1018 的量级上都只要几十步——这就是“对数级”的含义。

举一反三 · 想清楚这些再往下走
  1. 如果要算 ab mod m,但 b 是一个 1000 位的十进制字符串(远超 long long),该怎么办?(提示:指数能不能也“取模”?取模的对象是什么?)
  2. gcd(a, b) = gcd(b, a mod b) 为什么成立?请用“a、b 的公因子集合 = b、a mod b 的公因子集合”这个角度写出证明。
  3. gcd(a, b) · lcm(a, b) = a · b 对两个数成立。三个数的时候还成立吗?lcm(a, b, c) 该怎么算才不会溢出?
  4. 扩展欧几里得求出的一组解不唯一。如果要求 x 为最小正数,怎么在 O(1) 内调整(不能暴力试 k)?
  5. 若 gcd(a, m) = d > 1,方程 a·x ≡ b (mod m) 有解的条件是什么?有解时不同的解有几个?怎么把它化归成互质的情形?
  6. 快速幂的模板能不能直接用来算矩阵的幂?需要满足什么条件(结合律?单位元?)才能照抄?
  7. 为什么 r 和 a 每一步都要取模,而 b 不需要?如果 b 也取模会发生什么?
自测:能全部回答再进下一节
挑战题 · 同余方程
洛谷 P1082普及/提高-去洛谷做这题 ↗

求关于 x 的同余方程 a·x ≡ 1 (mod b) 的最小正整数解。

输入:一行两个正整数 a、b(2 ≤ a, b ≤ 2×109),用一个空格隔开。

输出:一行一个正整数 x0,即最小正整数解。输入数据保证一定有解。

引导性提示(先只看这里)

把“a·x 除以 b 余 1”翻译成等式:a·x − 1 是 b 的倍数,也就是存在整数 y 使 a·x + b·y = 1。

求 a·x + b·y = gcd(a, b) 的一组解是扩展欧几里得的标准任务。先想清楚:本题里 gcd(a, b) 必须等于几,方程才有解?

解出来的 x 可能为负、也可能大于 b。怎么把它挪进 1 到 b−1 之间?注意 C++ 里负数取模的结果。

想看思路了?点开完整思路

a·x ≡ 1 (mod b) 等价于 b | (a·x − 1),即存在整数 y 使 a·x − 1 = b·y,也就是 a·x + b·(−y) = 1。令 y′ = −y,问题变成解 a·x + b·y′ = 1。

由裴蜀定理,a·x + b·y 能取到的最小正数恰是 gcd(a, b)。题目保证有解,所以 gcd(a, b) 只能是 1。

调用 exgcd(a, b, x, y),返回时 x 满足 a·x + b·y = gcd(a, b) = 1,它就是一组解。

通解是 x + k·b:因为 a·(x + k·b) = a·x + k·a·b ≡ a·x (mod b)。所以把所有解对 b 取模,落进 [0, b) 的那个就是唯一答案。

C++ 的 % 对负数返回负余数,必须写成 (x % b + b) % b。又因为 b ≥ 2 且 a·x ≡ 1 ≠ 0,所以结果不会等于 0,落在 1..b−1,正是最小正整数解。

复杂度:与辗转相除同阶,b ≤ 2×109 时递归不超过 50 层,O(log b)。

还是不会?点开题解代码与拆解
参考代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

// 求 a*x + b*y = gcd(a,b) 的一组整数解,返回 gcd(a,b)
ll exgcd(ll a, ll b, ll &x, ll &y) {
    if (b == 0) {                       // 出口:a*1 + 0*0 = a
        x = 1; y = 0;
        return a;
    }
    ll x1, y1;
    ll g = exgcd(b, a % b, x1, y1);     // 规模先变小
    x = y1;                             // 回代:a 的系数
    y = x1 - (a / b) * y1;              // 回代:b 的系数
    return g;
}

int main() {
    ll a, b, x, y;
    cin >> a >> b;

    exgcd(a, b, x, y);                  // 得到 a*x + b*y = gcd(a,b) = 1
    x = (x % b + b) % b;                // 归一化到 [0, b):先 % 再 +b 再 %
    cout << x << "\n";
    return 0;
}
关键片段拆解
片段作用
ll exgcd(ll a, ll b, ll &x, ll &y)x、y 用引用传递,答案靠它们带出;返回值是 gcd。a、b 都是 long long,因为上限 2×10^9 已经贴着 int 的边缘。
if (b == 0) { x = 1; y = 0; return a; }递归出口,也就是辗转相除的最后一步:此时 gcd = a,取 x = 1、y = 0 使 a·1 + 0·0 = a 成立。
exgcd(b, a % b, x1, y1)把 (a, b) 换成 (b, a mod b)。这是整个算法唯一“变小”的地方,也是它必然终止的原因。
x = y1; y = x1 - (a / b) * y1;回代。由 b·x1 + (a − (a/b)·b)·y1 = g 直接对照系数得到,不能写反。
x = (x % b + b) % b;★ 归一化。exgcd 给的解可能为负(例如 a = 2、b = 3 时 x = −1),负余数仍是负数,必须加 b 再取模才能落进 [0, b)。
cout << xb ≥ 2 且 a·x ≡ 1 (mod b),所以 x 不会是 0;归一化后的值就是最小正整数解。
这题最容易错的地方
  • 输出 x 而不归一化:样例 3 10 恰好是正数 7,看不出问题;换 a = 2、b = 3 会输出 −1(正确答案 2),WA。
  • 把 x % b 当成“取最小正数”:C++ 的余数跟随被除数符号,负数取模还是负数。
  • 回代公式写错:把 x = y1; y = x1 - (a/b)*y1; 里的 x1、y1 对调,样例 3 10 立刻输出 1 而不是 7。这两行必须从 a mod b 的展开式自己推一遍。
  • 把递归参数换成 exgcd(b, a % b, y, x)(另一种紧凑写法)却照抄上面两行回代公式——两种写法必须成套使用,混用会得到错误的符号。
  • 参数用 int:a、b 到 2×109,中间 a / b、乘法都贴着 int 上限,统一 long long 更安全。
延伸阅读 / 参考