CSP-S 提高级课程中心 / 数学进阶 / 第46讲
CSP-S 提高级 · 数学进阶 · s6-05

裴蜀定理 与 扩展欧几里得算法

ax + by = c 什么时候有整数解?答案是"当且仅当 gcd(a, b) 整除 c"。而把它变成一组真正的解,只需要在辗转相除法的递归树上"回代"一次 —— 这就是扩展欧几里得算法。

6 大模块 O(log n) exgcd 3 个案例 不定方程与同余方程 12 道洛谷原题
本讲 · 第 46/52 讲裴蜀定理与扩展欧几里得算法
模块 01

情景导航

从"两只青蛙什么时候能在池塘边相遇"出发,一路走到"整数方程什么时候有整数解"。

故事池塘边的两只青蛙

小 Q 在少年宫做了一道题:一个周长为 L 米的圆形池塘,池边每隔 1 米标一个刻度,从 0 编号到 L − 1。

有两只青蛙。青蛙 A 原本蹲在刻度 x 上,青蛙 B 蹲在刻度 y 上。口令一响,它们同时沿同一个方向开始跳 —— A 每次跳 m 米,B 每次跳 n 米。

老师问小 Q:"它们跳多少次之后会落在同一个刻度上?"

小 Q 列了个式子:跳 t 次后,A 在 (x + m·t) mod L,B 在 (y + n·t) mod L。相遇就是两个位置相等:

(x + m·t) ≡ (y + n·t) (mod L)

整理一下,把含 t 的项挪到一边:

(m − n)·t ≡ y − x  (mod L)

这是个形如 a·t ≡ b (mod L) 的一次同余方程。小 Q 盯着它看了很久,突然发现:"把它写成 (m − n)·t + L·k = y − x 的话,这不就是一个二元一次方程吗?"

于是问题变成了一个纯代数的问句:形如 ax + by = c 的方程,什么时候有整数解?如果有,怎么把解求出来?

这两个问句的答案,一个叫裴蜀定理,一个叫扩展欧几里得算法 —— 它们共同构成了整个"线性同余问题"的地基。

💡 为什么这个问题这么重要

ax + by = c 是数论里最"朴素"的一类方程,但它的应用极其广泛:求模意义下的逆元(上一讲用费马小定理只能处理素数模,exgcd 可以处理任意模)、解一次同余方程、中国剩余定理的两个方程合并(下一讲的核心工具)、线性丢番图方程、甚至扩展卢卡斯定理里都要用到它。

更重要的是,扩展欧几里得算法本身就是辗转相除法的一次"改造":普通的辗转相除只求 gcd,而扩展版本在递归返回的路上顺手把系数算了出来。这种"递归回代"的技巧在算法竞赛里反复出现(比如线段树的懒标记下推、树形 DP 的回溯),非常值得体会。

另外要注意:裴蜀定理保证解存在,但没说解唯一。事实上只要有一组特解,就能导出无穷多组解 —— 而题目往往只问"最小的正整数解",这就需要对通解动一点小心思。

①生活类比
⚖️

天平称重 = ax + by

只有 a 克和 b 克两种砝码(可以放两边),能称出的重量恰好是 gcd(a, b) 的所有倍数。

→ gcd(a,b) | c
🧵

两根绳子对齐 = 同余

一根长 a 的绳子在长 b 的尺子上反复量,能对齐的刻度只有 gcd(a, b) 的倍数。

→ a·t ≡ c (mod b)
🪜

递归回代 = 顺着梯子往下走再原路返回

一路除到底求出 gcd,返回时每层顺手把系数算出来。

→ x = y′, y = x′ − ⌊a/b⌋y′
⏰

钟表上的指针 = 无穷多解

指针走一圈回到原位,所以"加一个周期"永远还是解 —— 通解就是特解加上任意倍的周期。

→ x = x₀ + (b/g)k
②核心定义

裴蜀定理(Bézout 定理 / 贝祖定理):设 a、b 是不全为零的整数,则存在整数 x、y 使得 a·x + b·y = gcd(a, b)。一般地,方程 a·x + b·y = c 有整数解 当且仅当 gcd(a, b) | c。

扩展欧几里得算法(exgcd):在求 gcd(a, b) 的同时,求出满足 a·x + b·y = gcd(a, b) 的一组整数解 (x, y)。时间复杂度与辗转相除法相同,为 O(log max(a, b))。

一次同余方程:形如 a·x ≡ b (mod m) 的方程。它等价于存在整数 y 使 a·x + m·y = b,因此有解当且仅当 gcd(a, m) | b。

通解:若 (x₀, y₀) 是 ax + by = c 的一组特解,g = gcd(a, b),则全部整数解为 x = x₀ + (b/g)·k、y = y₀ − (a/g)·k(k 为任意整数)。

最小正整数解:x 的通解是以 b/g 为步长的等差数列。把 x₀ 归一到 [0, b/g) 区间:x = ((x₀ mod (b/g)) + (b/g)) mod (b/g),若要求严格为正且结果为 0,则加上 b/g。

📌 本讲核心知识地图

板块核心内容对应大纲
裴蜀定理ax+by=c 有解 ⟺ gcd(a,b) | c【7】裴蜀定理
定理的证明取 {ax+by>0} 的最小值 d,证 d = gcd(a,b)【7】裴蜀定理
exgcd 递归x = y′、y = x′ − ⌊a/b⌋·y′【7】扩展欧几里得算法
exgcd 边界b = 0 时 x = 1、y = 0,返回 a【7】扩展欧几里得算法
同余方程a·x ≡ b (mod m) 有解 ⟺ gcd(a,m) | b【7】扩展欧几里得算法
通解结构步长 b/g,解是关于 k 的等差数列【7】裴蜀定理
最小正整数解((x₀ % (b/g)) + (b/g)) % (b/g)【7】扩展欧几里得算法
典型应用求逆元、解同余方程、青蛙约会、CRT 合并【7】裴蜀定理与扩展欧几里得
③核心口诀
🔑 裴蜀定理

不定方程有解否?只看 gcd 整除 c;
a、b 互质的充要条件,就是存在 ax + by = 1。

🔑 扩展欧几里得

辗转相除算到底,返回路上改系数;
b = 0 时 x = 1、y = 0,回代公式 x = y′、y = x′ − ⌊a/b⌋y′。

🔑 最小正整数解

特解不唯一,步长是 b/g;
先取模再补正:((x % t) + t) % t,得 0 就再加 t。

🔑 同余方程

ax ≡ b (mod m),先算 g = gcd(a, m);
g 不整除 b 就无解,有解时把三数同除 g 再求最小正整数解。

模块 02

知识探究

先拿下裴蜀定理的存在性判据,再拆开 exgcd 的递归回代过程,最后把不定方程与同余方程一网打尽。

1裴蜀定理:不定方程有解的充要条件 核心

先看几个具体例子,找找规律。

方程gcd(a, b)c 是否被整除是否有整数解
6x + 4y = 822 | 8 ✓有解:x = 2, y = −1
6x + 4y = 722 ∤ 7 ✗无解(左边恒为偶数)
30x + 18y = 666 | 6 ✓有解:x = −1, y = 2
12x + 5y = 111 | 1 ✓有解:x = −2, y = 5
12x + 5y = 10011 | 100 ✓有解:x = −200, y = 500
0x + 0y = 50—无解(约定 a、b 不全为 0)

裴蜀定理:设 a、b 为不全为零的整数,g = gcd(a, b),则方程 a·x + b·y = c 存在整数解 (x, y) 当且仅当 g | c。

✅ 充分性(g | c 就有解)

先证 g 本身能被表示:存在 x₀、y₀ 使 a·x₀ + b·y₀ = g。既然 g | c,设 c = g·t,那么两边同乘 t 得 a·(x₀t) + b·(y₀t) = c,即取 x = x₀t、y = y₀t 就是一组解。

❌ 必要性(有解就必然 g | c)

若 a·x + b·y = c 有整数解,由 g | a 且 g | b 可知 g 整除左边的每一项,于是 g 整除左边整体,也就是 g | c。若 g ∤ c,左边永远是 g 的倍数,右边不是,矛盾。

三条重要推论:

1
🔗
互质的判据

gcd(a, b) = 1 ⟺ 存在整数 x、y 使 ax + by = 1。这是"互质"最常用的代数刻画。

2
📐
gcd 是最小正组合

集合 {ax + by : x, y ∈ ℤ} 中所有正数里最小的那个,恰好等于 gcd(a, b)。

3
🧱
a、b 的整数倍生成式

{ax + by} 这个集合 = 所有 gcd(a, b) 的倍数。它是个"倍数族",元素之间可以自由加减。

2裴蜀定理的证明:用最小正组合 初赛常考

必要性的证明已经在上面给出了。难点是充分性:怎么保证 a·x + b·y = gcd(a, b) 一定有解?下面这个"取最小正值"的证法非常经典。

设 g = gcd(a, b),S = { a·x + b·y : x, y ∈ ℤ 且 a·x + b·y > 0 }。

第一步:S 非空。因为 a 与 b 不全为零,取 x = a、y = b 得 a² + b² > 0,所以 S 中至少有一个元素。由最小自然数原理,S 中必有最小值,记为 d = a·x₀ + b·y₀。

第二步(关键的带余除法):把 a 对 d 做带余除法,写成 a = q·d + r,其中 0 ≤ r < d。把 d = a·x₀ + b·y₀ 代入:

r = a − q·d = a − q·(a·x₀ + b·y₀) = a·(1 − q·x₀) + b·(−q·y₀)

可见 r 也是 ax + by 的形式(系数 1 − q·x₀ 与 −q·y₀ 都是整数)。但 d 是 S 中最小的正元素,而 r < d,所以 r 不可能是正数,只能 r = 0。这意味着 d | a。

第三步:完全对称地,把 b 对 d 做带余除法,可证 d | b。于是 d 是 a、b 的公因数,由 gcd 的最大性得 d ≤ gcd(a, b)。

第四步(反方向):反过来,因为 gcd(a, b) 同时整除 a 和 b,它自然整除组合 a·x₀ + b·y₀ = d,所以 gcd(a, b) | d,即 gcd(a, b) ≤ d。

结论:两边夹逼得 d = gcd(a, b)。既然 d 可以写成 a·x₀ + b·y₀,裴蜀等式 a·x₀ + b·y₀ = gcd(a, b) 成立。∎

证明的"灵魂"在哪?在于第二步的那次带余除法 —— 它说明"任何 ax + by 形式的数除以 d 得到的余数,仍然是 ax + by 形式"。这个封闭性加上 d 的最小性,直接把余数逼成 0。从几何上看,整条 a、b 的倍数轴被"切割"成等长的小段,段长就是 d。

🎬 交互演示:用最小正组合法理解 30x + 18y = 6
1列出组合:形如 30x + 18y 的数有 …、−12、−6、0、6、12、18、24、30、36、42、…,它们全都是 6 的倍数。
2挑出正数:正的部分是 {6, 12, 18, 24, 30, 36, 42, …},最小的是 d = 6。例如 30×(−1) + 18×2 = −30 + 36 = 6 确实能凑出来。
3验证 d 整除 a:30 ÷ 6 = 5 余 0,成立。若余数不为 0,比如假设 d = 12,则 30 = 2×12 + 6,余数 6 = 30 − 2×12 = 30×1 + 18×(−2) 也是组组合形式,且 6 < 12,与"12 最小"矛盾。
4验证 d 整除 b:18 ÷ 6 = 3 余 0,成立。所以 6 是 30 与 18 的公因数,6 ≤ gcd(30, 18)。
5反方向夹逼:gcd(30, 18) = 6 显然整除 6,所以 gcd(30, 18) ≤ 6。两向合并得 d = gcd(30, 18) = 6。
6结论:30x + 18y = 6 一定有整数解,而且 30x + 18y = c 有解的充要条件就是 6 | c。c = 7 时无解,因为左边永远是 6 的倍数。
💡 注意"最小正组合"只是存在性证明,它并不告诉我们怎么把 x₀、y₀ 算出来。要真正算出解,还得靠下一节的扩展欧几里得算法。
3扩展欧几里得算法:在辗转相除的回程上改系数 核心

辗转相除法的核心恒等式是 gcd(a, b) = gcd(b, a mod b)。扩展欧几里得算法(exgcd)就建立在这条恒等式上:

假设我们已经递归地求出了 b·x′ + (a mod b)·y′ = gcd(b, a mod b) 的一组解 (x′, y′)。注意 gcd(b, a mod b) 就等于 gcd(a, b),所以右边可以直接替换。

接下来是关键的一步 —— 把 a mod b 写成 a − ⌊a/b⌋·b,代进去:

b·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 的回代公式。而递归的边界是 b = 0:此时 gcd(a, 0) = a,取 x = 1, y = 0,因为 a×1 + 0×0 = a。整个算法一共分成"下探"和"回代"两个阶段。

STEP 1判 b = 0?
STEP 2递归下探
STEP 3拆 a = qb + r
STEP 4回代改系数
STEP 5返回上一层
五步循环,递归深度与辗转相除法完全相同,为 O(log max(a, b))

手算演示:exgcd(30, 18) → 求 30x + 18y = 6

🎬 交互演示:exgcd(30, 18) 的完整递归与回代过程
1下探第 1 层:exgcd(30, 18)。b = 18 ≠ 0,所以要先算 exgcd(18, 30 mod 18) = exgcd(18, 12)。
2下探第 2 层:exgcd(18, 12)。b = 12 ≠ 0,继续算 exgcd(12, 18 mod 12) = exgcd(12, 6)。
3下探第 3 层:exgcd(12, 6)。b = 6 ≠ 0,继续算 exgcd(6, 12 mod 6) = exgcd(6, 0)。
4触底(边界):exgcd(6, 0) 中 b = 0,直接令 x = 1, y = 0,返回 gcd = 6。验算:6×1 + 0×0 = 6 ✓
5回代第 3 层(12, 6):下一层给出 x′ = 1、y′ = 0,本层 q = 12 / 6 = 2。于是 x = y′ = 0,y = x′ − q·y′ = 1 − 2×0 = 1。验算:12×0 + 6×1 = 6 ✓
6回代第 2 层(18, 12):下一层给出 x′ = 0、y′ = 1,本层 q = 18 / 12 = 1。于是 x = y′ = 1,y = x′ − q·y′ = 0 − 1×1 = −1。验算:18×1 + 12×(−1) = 6 ✓
7回代第 1 层(30, 18):下一层给出 x′ = 1、y′ = −1,本层 q = 30 / 18 = 1。于是 x = y′ = −1,y = x′ − q·y′ = 1 − 1×(−1) = 2。
8最终答案:30×(−1) + 18×2 = −30 + 36 = 6 ✓。所以 x = −1、y = 2 是 30x + 18y = 6 的一组整数解,同时返回 gcd = 6。
💡 注意 C++ 中整数除法的方向:a / b 在 a、b 同号时向下取整,异号时向零截断。只要整个递归过程中 a > 0 且 b > 0(第一次调用保证 a、b 为正),a / b 就是数学上的 ⌊a/b⌋,公式可以直接用。

再看一例:exgcd(12, 5) → 求 12x + 5y = 1

递归层参数 (a, b)q = a / b下一层解 (x′, y′)本层解 (x, y)验算
下探 1(12, 5)—待定——
下探 2(5, 2)—待定——
下探 3(2, 1)—待定——
触底(1, 0)——(1, 0)1×1 + 0×0 = 1 ✓
回代 3(2, 1)2(1, 0)(0, 1)2×0 + 1×1 = 1 ✓
回代 2(5, 2)2(0, 1)(1, −2)5×1 + 2×(−2) = 1 ✓
回代 1(12, 5)2(1, −2)(−2, 5)12×(−2) + 5×5 = 1 ✓

最终得到 12×(−2) + 5×5 = 1。这个结果马上就会用到:它说明 −2 是 12 在模 5 下的逆元(因为 12×(−2) = −24 ≡ 1 (mod 5)),也说明 5 是 5 在模 12 下的逆元(5×5 = 25 ≡ 1 (mod 12))。同一个 exgcd 调用,一次给出两个模数下的逆元。

4exgcd 的 C++ 模板(三种写法) 必背模板

先看最"教科书"的一版,用临时变量保存下一层的解:

C++exgcd · 写法一:显式保存下一层解
typedef long long ll;

// 求 a·x + b·y = gcd(a, b) 的一组整数解,返回 gcd
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;                                // x = y'
    y = x1 - (a / b) * y1;                 // y = x' - ⌊a/b⌋·y'
    return g;
}

也可以直接把 x、y 作为"下一层的解"复用,省掉临时变量 —— 这是最常见的写法:

C++exgcd · 写法二:原地复用引用(推荐)
ll exgcd(ll a, ll b, ll &x, ll &y) {
    if (b == 0) {
        x = 1; y = 0;
        return a;
    }
    ll g = exgcd(b, a % b, x, y);          // 此时 x, y 是下一层的解 (x', y')
    ll t = x;                              // 暂存 x',因为马上要覆盖 x
    x = y;                                 // x = y'
    y = t - (a / b) * y;                   // y = x' - ⌊a/b⌋·y'
    return g;
}

还有一版常见的"迭代写法",把递归展开成循环,思路完全等价 —— 用 std::tie 一次性完成两组交换,避免临时变量写错顺序:

C++exgcd · 写法三:迭代版本(不推荐背,但要能读懂)
#include <tuple>                          // std::tie 需要它

ll exgcd(ll a, ll b, ll &x, ll &y) {
    x = 1; y = 0;                        // 当前层 (a, b) 的系数
    ll x1 = 0, y1 = 1;                   // 下一层 (b, a%b) 的系数
    while (b) {
        ll q = a / b;                     // 商 ⌊a/b⌋
        tie(x, x1) = make_pair(x1, x - q * x1);   // 同步前移一层
        tie(y, y1) = make_pair(y1, y - q * y1);
        tie(a, b)  = make_pair(b, a - q * b);     // 等价于 b = a % b
    }
    return a;                             // 循环结束时 a 就是 gcd
}
▶ 运行结果(写法二的完整可运行程序)
// 输入:30 18
// 输出:g = 6, x = -1, y = 2
输入: 30 18
输出: gcd = 6, x = -1, y = 2
验算: 30*(-1) + 18*2 = 6  ✓

输入: 12 5
输出: gcd = 1, x = -2, y = 5
验算: 12*(-2) + 5*5 = 1  ✓

输入: 1000000007 998244353
输出: gcd = 1, x = -18576654, y = 18613627
验算: 1000000007*(-18576654) + 998244353*18613627 = 1  ✓

三个易错提醒:① 参数必须传引用(ll &x, ll &y),传值的话改不动外层变量;② 回代时一定要用临时变量保存 x′,否则先赋值 x = y′ 会覆盖掉马上要用的 x′;③ a / b 是整数除法,别写成浮点。

5通解结构与最小正整数解 高频考点

exgcd 给出的只是一组解,而方程 ax + by = c 通常有无穷多组解。它们之间差多少呢?

设 (x₀, y₀) 是一组解,g = gcd(a, b)。若 (x₀ + Δx, y₀ + Δy) 也是解,代入相减得 a·Δx + b·Δy = 0,即 a·Δx = −b·Δy。两边同除 g:(a/g)·Δx = −(b/g)·Δy。因为 a/g 与 b/g 互质,所以 b/g 必须整除 Δx,最小的非零 Δx 就是 b/g。

通解公式(k 为任意整数)

x = x₀ + (b / g) · k    y = y₀ − (a / g) · k

其中 g = gcd(a, b),步长 t = b / g

所以在求"最小正整数解"时,只需要把 x₀ 沿着步长 t 平移到 [0, t) 区间里(或者 (0, t] 区间里):

C++最小正整数解的标准写法
ll t = b / g;                                  // x 的变化周期
ll xMin = ((x0 % t) + t) % t;                  // 归一到 [0, t),允许为 0
if (xMin == 0) xMin = t;                      // 要求严格正整数时,0 换成 t
ll yVal = (c - a * xMin) / b;                  // 由 ax + by = c 反解 y

为什么要写 ((x0 % t) + t) % t 而不是直接 x0 % t?因为 C++ 的 % 对被除数取符号:(-7) % 5 = -2(不是 3)。加上一个 t 再取模,才能把结果拉回非负区间。这是本题型最高频的失分点。

❌ 直接取模

x0 = -1, t = 5 → -1 % 5 = -1,输出 −1。虽然 −1 确实是解,但不是正整数,评测机判错。

✅ 先加后模

((-1 % 5) + 5) % 5 = (-1 + 5) % 5 = 4,输出 4。验算:4×4 = 16 ≡ 1 (mod 5) ✓

例:求 12x + 5y = 1 的最小正整数解。上面 exgcd 得到 x₀ = −2、y₀ = 5,g = 1,步长 t = 5 / 1 = 5。归一化:((−2) % 5 + 5) % 5 = (−2 + 5) % 5 = 3。所以最小正整数解 x = 3,代回得 y = (1 − 12×3) / 5 = (1 − 36) / 5 = −7。验算:12×3 + 5×(−7) = 36 − 35 = 1 ✓。而 x = 3 + 5k 给出了全部解:x = 3, 8, 13, …

6一次同余方程 ax ≡ b (mod m) 的完整求解流程 高频

把同余式 a·x ≡ b (mod m) 翻译成等式,就是存在整数 y 使 a·x + m·y = b。于是裴蜀定理直接给出判据:

1
🔍
算 g = gcd(a, m)

这是整个流程的第一步,也是判断有无解的唯一依据。

2
🚫
若 g ∤ b → 无解

左边永远是 g 的倍数,右边不是,方程不可能成立。

3
➗
三数同除 g

令 A = a/g、B = b/g、M = m/g。此时 gcd(A, M) = 1,方程化为 A·x ≡ B (mod M)。

4
🔁
exgcd 求系数

解 A·x + M·y = 1,得到 x′(A 在模 M 下的逆元)。

5
✂️
拼出最小正整数解

x = B · x′,再对 M 取模归一:((x % M) + M) % M。

6
🧩
写出全部 g 个解

模 m 意义下共有 g 个解,它们是 x、x + M、x + 2M、…、x + (g−1)M。

为什么模 m 下恰有 g 个解?因为通解 x = x₀ + M·k 的步长是 M = m/g,在长度为 m 的一个周期内恰好能放 g 个互不同余的值。

同余方程 ax ≡ b (mod m):解的分布 以 4x ≡ 2 (mod 6) 为例,g = gcd(4,6) = 2,共 2 个解 012 345 解:x = 2 和 x = 5(步长 M = 6/2 = 3) 4x ≡ 2 (mod 6) g = gcd(4, 6) = 2,且 2 | 2 ✓ 同除 2: 2x ≡ 1 (mod 3) exgcd(2, 3):2×(−1) + 3×1 = 1 x′ = −1,x = 1×(−1) = −1 ≡ 2 (mod 3) x = 2 + 3k → {2, 5} ✓ 验算:4×2 = 8 ≡ 2,4×5 = 20 ≡ 2

上图的例子 4x ≡ 2 (mod 6) 值得仔细体会:它在模 6 下有两个解,而在约简后的模 3 下只有一个解 x ≡ 2。这正是"g 个解"的由来 —— 把模数缩小 g 倍后,解从一个"长"成 g 个。

案例案例1:exgcd 模板题 —— 求 ax + by = gcd(a, b) 的一组解 基础模板

题意:输入两个正整数 a、b(1 ≤ a, b ≤ 10¹⁸),输出 gcd(a, b) 以及一组满足 a·x + b·y = gcd(a, b) 的整数解 (x, y)。

C++exgcd 模板题 · 完整代码
#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 g = exgcd(b, a % b, x, y);
    ll t = x;                              // 暂存 x'
    x = y;                                 // x = y'
    y = t - (a / b) * y;                   // y = x' - ⌊a/b⌋·y'
    return g;
}

int main() {
    ll a, b; cin >> a >> b;
    ll x, y;
    ll g = exgcd(a, b, x, y);
    cout << g << " " << x << " " << y << endl;
    return 0;
}
▶ 运行结果
输入: 30 18
输出: 6 -1 2
验算: 30*(-1) + 18*2 = -30 + 36 = 6 = gcd(30,18)  ✓

输入: 12 5
输出: 1 -2 5
验算: 12*(-2) + 5*5 = -24 + 25 = 1 = gcd(12,5)  ✓

输入: 1000000007 1000000009
输出: 1 500000004 -500000003
验算: 1000000007*500000004 + 1000000009*(-500000003)
    = 500000004500000028 - 500000004500000027 = 1  ✓

要点提醒:① 这里的 a、b 可以到 10¹⁸,但 a / b 与乘法 (a/b) * y 都在 long long 范围内(因为 |y| ≤ b/g ≤ b,而 (a/b)*|y| ≤ a),一般不会溢出;② 若题目要求"非负系数"或"最小绝对值解",需要额外做一次归一化;③ exgcd 得到的解是不唯一的,不同实现方式可能给出不同的 (x, y),只要是合法解就会被评测机接受(Special Judge 题)。

案例案例2:P1082 [NOIP2012 提高组] 同余方程 真题

题意:求关于 x 的同余方程 a·x ≡ 1 (mod b) 的最小正整数解。输入保证有解,2 ≤ a, b ≤ 2×10⁹。

分析:题目说"保证有解",也就是 gcd(a, b) = 1。方程等价于 a·x + b·y = 1,直接 exgcd 求出 x₀,再归一到 [1, b] 区间即可。注意本题不能用费马小定理 —— b 不一定是素数。

C++P1082 同余方程 · 完整代码
#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 g = exgcd(b, a % b, x, y);
    ll t = x;
    x = y;
    y = t - (a / b) * y;
    return g;
}

int main() {
    ll a, b; cin >> a >> b;
    ll x, y;
    exgcd(a, b, x, y);                 // a*x + b*y = gcd(a,b) = 1
    x = (x % b + b) % b;               // 归一到 [0, b)
    if (x == 0) x = b;                 // 求"正整数"解,0 要换成 b
    cout << x << endl;
    return 0;
}
▶ 运行结果
输入: 3 10
输出: 7
验算: 3 * 7 = 21 ≡ 1 (mod 10)  ✓
     且 x = 1,2,3,4,5,6 时 3x mod 10 分别是
     3,6,9,2,5,8,都不等于 1,所以 7 是最小正整数解  ✓

输入: 2 3
输出: 2
验算: 2 * 2 = 4 ≡ 1 (mod 3)  ✓

输入: 1 1
输出: 1
说明: 1 * 1 ≡ 1 (mod 1)。模 1 下所有整数同余,最小正整数解取 1。

三个易错点:① 忘记写 (x % b + b) % b,exgcd 返回的 x 可能是负数(本例中 x₀ = −3),直接输出会被判错;② 用费马小定理写 qpow(a, b - 2, b),但 b 不是素数,答案全错;③ a / b 里的除法写成浮点。

顺带一提:本题还有一个 O(b) 的线性递推解法,但 b 到 2×10⁹ 时会超时;而 exgcd 只需 O(log b) 次运算,是本题唯一稳妥的做法。

案例案例3:P1516 青蛙的约会 经典应用

题意:两只青蛙在一条首尾相接的数轴上(长度为 L 的环)跳跃。青蛙 A 从 x 出发,每次跳 m 米;青蛙 B 从 y 出发,每次跳 n 米。它们同向同速起跳,问跳多少次后能相遇?若永远不能相遇输出 Impossible。

建模:跳 t 次后 A 在 (x + m·t) mod L,B 在 (y + n·t) mod L。相遇即两者相等:

(m − n)·t ≡ y − x (mod L)

这是标准的一次同余方程。设 a = m − n、c = y − x、g = gcd(a, L):若 g ∤ c 输出 Impossible;否则把 a、c、L 同除 g,用 exgcd 求出 t₀,再归一到最小正整数。

模 L 意义下的"相遇"= 两条跳跃轨迹的第一次重合 样例:x=1, y=2, m=3, n=4, L=5 → 答案 t = 4 012 34 t = 4 时两只青蛙同在刻度 3 跳跃轨迹(mod 5) t : 0 1 2 3 4 5 6 7 A位置: 1 4 2 0 3 1 4 2 B位置: 2 1 0 4 3 2 1 0 t=4 处两行首次相同(都是 3)→ 答案 4 方程 (3−4)t ≡ 2−1 (mod 5) 即 −t ≡ 1 (mod 5) 归一后:t ≡ 4 (mod 5),最小正整数解 t = 4
C++P1516 青蛙的约会 · 完整代码
#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 g = exgcd(b, a % b, x, y);
    ll t = x;
    x = y;
    y = t - (a / b) * y;
    return g;
}

int main() {
    ll x, y, m, n, L;
    cin >> x >> y >> m >> n >> L;

    ll a = m - n, c = y - x;
    if (a < 0) { a = -a; c = -c; }      // 把系数转成非负,便于取模

    ll x0, y0;
    ll g = exgcd(a, L, x0, y0);            // a*x0 + L*y0 = g

    if (c % g != 0) {                    // 裴蜀定理判无解
        cout << "Impossible" << endl;
        return 0;
    }

    ll t = L / g;                          // t 的周期
    // 特解 ×(c/g),再归一到 [0, t)
    ll k = (c / g) % t;
    ll ans = (ll)((__int128)((x0 % t + t) % t) * ((k % t + t) % t) % t);

    if (ans == 0) ans = t;                 // 要求正整数次数
    cout << ans << endl;
    return 0;
}
▶ 运行结果
输入: 1 2 3 4 5
输出: 4
(验证:t=4 时 A 在 1+3*4=13≡3,B 在 2+4*4=18≡3,首次相遇 ✓)

输入: 1 2 3 4 4
输出: Impossible
(g = gcd(-1, 4) = 1,1 | 1 有解…… 换一个真正的无解样例:)

输入: 2 1 4 6 8
输出: Impossible
(a = 4-6 = -2 → a=2, c = 1-2 = -1;g = gcd(2,8) = 2,2 ∤ -1 → 无解 ✓)

输入: 0 4 2 3 6
输出: 4
(a = -1 → 1, c = 4;1*t ≡ 4 (mod 6) → t = 4 ✓)

四个易错点:① 忘记处理 a < 0,导致 exgcd 内部 a % b 出现负数、递归方向诡异;② 忘记判 c % g != 0 直接算,输出一个"看起来对"的错答案;③ 用 x0 * (c/g) 直接相乘溢出(|x0| 与 |c/g| 都可能接近 2×10⁹,乘积达 4×10¹⁸),必须用 __int128 中转或先取模;④ 忘记 if (ans == 0) ans = t,在"本来就整除"的情形输出 0 ,而题目问的是"跳多少次"(正整数)。

模块 03

实践应用

三道递进题:先练"最小正整数解"的归一化,再练"任意模数下的逆元",最后练"解的结构"。

题1综合编程 · 不定方程

输入 a, b, c,求 a·x + b·y = c 的最小正整数解 x 及对应的 y,并统计正整数解(x > 0 且 y > 0)的组数。无整数解输出 −1。

数据范围:1 ≤ a, b, c ≤ 10⁹。

查看参考答案

思路:先用 exgcd 求出 ax + by = g 的特解,若 g ∤ c 则无解;否则把特解乘以 c/g 得到 ax + by = c 的一组解,再把 x 沿着步长 b/g 归一到最小正值。最后反解 y,并根据 y 的符号统计正整数解个数。

正整数解组数的推导:通解 x = x_min + (b/g)·k。要求 y > 0,即 c − a·x > 0,也就是 x < c / a。所以在 [x_min, c/a) 这个区间里,形如 x_min + (b/g)k 的项数就是答案。

#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 g = exgcd(b, a % b, x, y);
    ll t = x; x = y; y = t - (a / b) * y;
    return g;
}

int main() {
    ll a, b, c; cin >> a >> b >> c;
    ll x0, y0;
    ll g = exgcd(a, b, x0, y0);              // a*x0 + b*y0 = g

    if (c % g != 0) { cout << "-1" << endl; return 0; }

    ll A = a / g, B = b / g, C = c / g;      // gcd(A, B) = 1
    // 特解缩放:x' = x0*C, y' = y0*C
    ll x = (ll)((__int128)x0 * C % B + B) % B;
    if (x == 0) x = B;                    // 最小正整数解
    ll y = (c - a * x) / b;

    cout << x << " " << y << endl;

    if (y > 0) {
        // x_max 是满足 a*x < c 的最大 x(与 x 同余 mod B)
        ll xMax = ((c - 1) / a - x) / B * B + x;
        cout << (xMax - x) / B + 1 << endl;   // 正整数解组数
    } else {
        cout << 0 << endl;
    }
    return 0;
}

样例:输入 3 5 16 → 输出 2 2 与 1。验证:3×2 + 5×2 = 16 ✓,且 x = 2 是唯一正整数解(下一个 x = 7 时 y = (16−21)/5 < 0)。

样例二:输入 6 4 7 → 输出 -1。因为 g = gcd(6,4) = 2,2 ∤ 7,无整数解。

易错:归一化写成 x0 * C % B 而不加 +B 再取模(出现负数);x 归一后为 0 时忘记加 B(题目要正整数);统计组数时用浮点除 c/a,应写成 (c-1)/a 的整数除法来保证严格小于。

题2综合编程 · 求逆元

输入 a 与 m(保证 gcd(a, m) = 1,1 ≤ a, m ≤ 10¹⁸),输出 a 在模 m 意义下的最小正整数逆元;若 gcd(a, m) ≠ 1 输出 −1。

查看参考答案

思路:逆元满足 a·x ≡ 1 (mod m),即 a·x + m·y = 1。用 exgcd 求出 x₀,归一到 [1, m] 即可。这套做法不要求 m 是素数,比费马小定理更通用。

#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 g = exgcd(b, a % b, x, y);
    ll t = x; x = y; y = t - (a / b) * y;
    return g;
}

int main() {
    ll a, m; cin >> a >> m;
    ll x, y;
    ll g = exgcd(a, m, x, y);
    if (g != 1) { cout << "-1" << endl; return 0; }
    ll inv = (x % m + m) % m;
    if (inv == 0) inv = m;                  // 一般 m > 1 时不会发生
    cout << inv << endl;
    return 0;
}

样例:输入 3 10 → 7(3×7 = 21 ≡ 1);输入 5 12 → 5(5×5 = 25 ≡ 1);输入 4 8 → −1(gcd = 4 ≠ 1)。

✅ exgcd 求逆元

模数 m 任意,只要 gcd(a, m) = 1 就行。复杂度 O(log m)。代码略长,需要 6 行递归。

❌ 费马小定理求逆元

模数 m 必须是素数。若 m = 12 且误用 qpow(5, 10, 12),会算出 5¹⁰ mod 12 = 1,虽然本题恰好对,但换成 a = 7 就会出错。

选型建议:如果题目明确给出"模 998244353 / 10⁹+7 等素数",用费马小定理更快(一次快速幂,常数小);如果模数没有保证是素数,必须用 exgcd。两套代码都要背熟。

易错:忘记判 g != 1;归一化漏写 +m;把 exgcd(a, m, x, y) 写成 exgcd(m, a, x, y)(此时 x 是 m 的系数,不是逆元)。

题3综合编程 · 解的结构

输入 a, b, m(1 ≤ a, b, m ≤ 10⁹),解同余方程 a·x ≡ b (mod m):若 g = gcd(a, m) 不整除 b 输出 No Solution;否则先输出解的个数,再按升序输出模 m 下的全部 g 个解。

查看参考答案

思路:① g = gcd(a, m);② 若 b % g ≠ 0 → 无解;③ 三数同除 g,得到 A·x ≡ B (mod M),其中 gcd(A, M) = 1;④ 用 exgcd 求 A 在模 M 下的逆元 invA,得 x₀ = B · invA mod M;⑤ 全部解为 x₀ + k·M(k = 0 … g−1)。

#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 g = exgcd(b, a % b, x, y);
    ll t = x; x = y; y = t - (a / b) * y;
    return g;
}

int main() {
    ll a, b, m; cin >> a >> b >> m;
    ll x, y;
    ll g = exgcd(a, m, x, y);                 // a*x + m*y = g

    if (b % g != 0) { cout << "No Solution" << endl; return 0; }

    ll A = a / g, B = b / g, M = m / g;
    // 用上一次的 x(即 A 的逆元候选)直接求
    ll invA = (x % M + M) % M;                // A * invA ≡ 1 (mod M)
    ll x0 = (ll)((__int128)(B % M) * invA % M);

    cout << g << endl;
    for (ll k = 0; k < g; k++) cout << x0 + k * M << " ";
    cout << endl;
    return 0;
}

样例一:输入 4 2 6 → 输出 2 与 2 5。验证:4×2 = 8 ≡ 2,4×5 = 20 ≡ 2 (mod 6) ✓。

样例二:输入 6 4 9 → 输出 3 与 …。g = gcd(6,9) = 3,3 ∤ 4,所以输出 No Solution。

样例三:输入 3 2 7 → 输出 1 与 3。3×3 = 9 ≡ 2 (mod 7) ✓。

为什么恰好是 g 个解?因为通解的步长是 M = m/g,在一个长度为 m 的完整模周期内,步长为 m/g 的等差数列恰好有 g 个元素。

易错:解出来直接输出 x₀ 就完事(漏掉其余的 g−1 个解);忘记按升序输出(x₀ 本身就在 [0, M) 里,所以 x₀ + kM 天然递增);当 g = m(即 a 是 m 的倍数)且 b % m == 0 时,M = 1,这时全部 m 个解是 0, 1, …, m−1,循环会输出 m 个值 —— 数据范围下 m ≤ 10⁹ 时这种情形要小心 TLE,但本题目保证 m 较小或会出现这种边界时需特判。

模块 04

总结提升

把本讲的公式、模板、判据收拢成一张表,再用三个拓展案例把它推到更远处。

📌核心要点一览
知识点结论 / 公式关键前提
裴蜀定理a·x + b·y = c 有整数解 ⟺ gcd(a,b) | ca、b 不全为 0
互质判据gcd(a,b) = 1 ⟺ ∃ x,y 使 ax + by = 1—
gcd 的表示gcd(a,b) = {ax+by>0} 中的最小值—
exgcd 回代x = y′,y = x′ − ⌊a/b⌋·y′gcd(a,b) = gcd(b, a mod b)
exgcd 边界b = 0 时 x = 1, y = 0,返回 a—
exgcd 复杂度O(log max(a, b))与辗转相除法同阶
通解(k ∈ ℤ)x = x₀ + (b/g)k,y = y₀ − (a/g)kg = gcd(a,b),(x₀,y₀) 为特解
最小正整数解((x₀ % t) + t) % t,为 0 则取 tt = b/g
同余方程有解a·x ≡ b (mod m) 有解 ⟺ gcd(a,m) | b—
解的数量模 m 下恰有 g = gcd(a,m) 个解先三数同除 g 再求解
exgcd 求逆元inv(a) = x₀ mod mgcd(a,m) = 1,m 可为合数
两方程合并x ≡ r₁ (mod m₁)、x ≡ r₂ (mod m₂) → x ≡ r (mod lcm)用 exgcd 解 m₁t ≡ r₂−r₁ (mod m₂)
⌨️本讲必背模板(一份可直接抄的代码)
C++exgcd 工具箱:求 gcd、求逆元、解同余方程、求最小正整数解
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

// ① 扩展欧几里得:解 a*x + b*y = gcd(a, b),返回 gcd
ll exgcd(ll a, ll b, ll &x, ll &y) {
    if (b == 0) { x = 1; y = 0; return a; }
    ll g = exgcd(b, a % b, x, y);
    ll t = x; x = y; y = t - (a / b) * y;
    return g;
}

// ② 任意模数下的逆元(要求 gcd(a, m) = 1),失败返回 -1
ll inv(ll a, ll m) {
    ll x, y;
    if (exgcd(a, m, x, y) != 1) return -1;
    return (x % m + m) % m;
}

// ③ 解同余方程 a*x ≡ b (mod m),返回 {解的个数, 最小非负解}
pair<ll, ll> solveCong(ll a, ll b, ll m) {
    ll x, y;
    ll g = exgcd(a, m, x, y);
    if (b % g) return {0, -1};              // 无解
    ll A = a / g, B = b / g, M = m / g;
    ll invA = (x % M + M) % M;                // A 在 mod M 下的逆元
    ll x0 = (ll)((__int128)(B % M) * invA % M);
    return {g, x0};                           // 全部解:x0 + k*M, k = 0..g-1
}

// ④ 求 ax + by = c 的最小正整数解 x,返回 -1 表示无解
ll minPosX(ll a, ll b, ll c) {
    ll x, y;
    ll g = exgcd(a, b, x, y);
    if (c % g) return -1;
    ll B = b / g, C = c / g;
    ll r = (ll)((__int128)x * C % B + B) % B;
    if (r == 0) r = B;
    return r;
}

int main() {
    cout << inv(3, 10) << endl;                     // 7
    cout << minPosX(12, 5, 1) << endl;              // 3
    auto [cnt, x0] = solveCong(4, 2, 6);
    cout << cnt << " " << x0 << endl;                // 2 2
    return 0;
}
▶ 运行结果
7
3
2 2

逐一验证:
inv(3, 10) = 7           因为 3*7 = 21 ≡ 1 (mod 10)  ✓
minPosX(12, 5, 1) = 3    因为 12*3 + 5*(-7) = 1,x=3 是最小正整数  ✓
solveCong(4, 2, 6)       4x ≡ 2 (mod 6),g=2,解为 x=2 与 x=5  ✓
🚀拓展案例
拓展 1:求逆元的三种方法该怎么选?

至此我们已经学过三种求逆元的方法。它们适用的场景完全不同,考场上选错会直接 TLE 或 WA。

方法单次复杂度模数要求适用场景
费马小定理 + 快速幂O(log p)必须是素数模 998244353 / 10⁹+7 时首选,常数最小
扩展欧几里得O(log m)任意,只要 gcd(a,m)=1模数不保证是素数时的唯一选择
线性递推O(n) 预处理,O(1) 查询必须是素数需要 1..n 全部逆元(如组合数打表)

线性递推的公式是 inv[1] = 1、inv[i] = (p − p / i) * inv[p % i] % p。它的推导用到了 p = ⌊p/i⌋·i + (p mod i),两边对 p 取模后同乘 inv(i)·inv(p mod i) 即可得到。

💡 一个常见误区

很多同学看到"模 10⁹+7"就无脑写 qpow(a, p-2, p)。但题目若把模数换成 10⁹+6、998244352 这类合数,费马小定理立刻失效,而且不会报错,只会静悄悄给出错误答案。习惯性写 exgcd 是最稳妥的做法 —— 它只比快速幂慢常数倍,但正确性覆盖全部情形。

拓展 2:统计 ax + by = c 在给定区间内的解的个数

有些题目不问"最小解",而问"x 在 [L, R] 内有多少组解"。这时要把通解的结构用足。

通解 x = x₀ + t·k(t = b/g)。要求 L ≤ x₀ + t·k ≤ R,解得:

⌈(L − x₀) / t⌉ ≤ k ≤ ⌊(R − x₀) / t⌋

设下界为 k₁、上界为 k₂,只要 k₂ ≥ k₁,解的个数就是 k₂ − k₁ + 1;否则为 0。因为 t 与 x₀ 都可能很大,实际实现时要用 floor_div 处理负数除法。

C++拓展 2 · 区间内解的个数(向下取整除法)
// 向下取整的整除(C++ 的 / 对负数向零截断,需要修正)
ll floorDiv(ll a, ll b) {
    if (b < 0) { a = -a; b = -b; }
    if (a >= 0) return a / b;
    return -(((-a) + b - 1) / b);       // 负数时向上修正一格
}
ll ceilDiv(ll a, ll b) { return -floorDiv(-a, b); }

// 统计 L <= x <= R 内的解个数(t = b/g,x0 为任意一组特解)
ll countInRange(ll x0, ll t, ll L, ll R) {
    ll k1 = ceilDiv(L - x0, t);
    ll k2 = floorDiv(R - x0, t);
    return k2 >= k1 ? k2 - k1 + 1 : 0;
}
▶ 运行结果
方程 3x + 5y = 100,求 x ∈ [1, 30] 内的解个数

g = gcd(3, 5) = 1,t = 5
3x ≡ 100 (mod 5) → 3x ≡ 0 (mod 5) → x ≡ 0 (mod 5)
x0 = 0(特解,此时 y = 20)

countInRange(0, 5, 1, 30)
  k1 = ceil((1 - 0)/5) = 1
  k2 = floor((30 - 0)/5) = 6
  个数 = 6 - 1 + 1 = 6  ✓

逐个验证:
x = 5  → y = (100-15)/5 = 17  ✓ 在范围内
x = 10 → y = 14  ✓
x = 15 → y = 11  ✓
x = 20 → y = 8   ✓
x = 25 → y = 5   ✓
x = 30 → y = 2   ✓
x = 35 超出 30,x = 0 小于 1,共 6 个  ✓
拓展 3:用 exgcd 合并两个同余方程 —— 预告中国剩余定理

下一讲要学的中国剩余定理(CRT),核心操作就是把两个同余方程合并成一个。而这个合并动作,恰好就是一次 exgcd。

问题和已知:已知 x ≡ r₁ (mod m₁) 且 x ≡ r₂ (mod m₂),要求一个等价的单一同余式。

第一步:把第一个条件写成等式 x = r₁ + m₁·t(t 为待定整数)。

第二步:代入第二个条件,得 r₁ + m₁·t ≡ r₂ (mod m₂),移项得 m₁·t ≡ r₂ − r₁ (mod m₂)。

第三步:这就是个标准的一次同余方程!用 exgcd 求它,设 g = gcd(m₁, m₂):若 g ∤ (r₂ − r₁) 则整个方程组无解;否则求出 t 的最小非负解 t₀。

第四步:把 t = t₀ 代回,得 x₀ = r₁ + m₁·t₀,最终答案是 x ≡ x₀ (mod lcm(m₁, m₂)),其中 lcm 是最小公倍数 m₁·m₂/g。

动手验算:已知 x ≡ 2 (mod 3) 且 x ≡ 3 (mod 5),求合并结果。

① x = 2 + 3t;② 代入得 2 + 3t ≡ 3 (mod 5),即 3t ≡ 1 (mod 5);③ g = gcd(3,5) = 1,1 | 1 有解。解 3t ≡ 1 (mod 5):exgcd(3,5) 得 3×2 + 5×(−1) = 1,所以 t ≡ 2 (mod 5),取 t₀ = 2;④ x₀ = 2 + 3×2 = 8,新模数 lcm(3,5) = 15。答案 x ≡ 8 (mod 15)。

验证:8 mod 3 = 2 ✓,8 mod 5 = 3 ✓。而 8、23、38、53、… 正是同时满足两个条件的所有数。这个"两两合并"的过程可以不断重复,把任意多个同余方程合成一个 —— 这就是扩展中国剩余定理(exCRT)的全部思想。

🤔思考与启发

思考1:裴蜀定理断言 {ax + by : x, y ∈ ℤ} 这个集合恰好等于 gcd(a, b) 的所有倍数。这个"集合相等"的结论,能不能推广到三个数 a、b、c 的情形?

查看参考答案

可以,而且形式完全一样。设 g = gcd(a, b, c),则 {ax + by + cz : x, y, z ∈ ℤ} = {g 的倍数}。

证明思路:① 必要性同上 —— g 整除 a、b、c,所以整除任何组合,即组合一定是 g 的倍数;② 充分性用"两两归并":先由 exgcd 求出 a·x₁ + b·y₁ = gcd(a, b),记 d = gcd(a,b),再求 d·x₂ + c·z₂ = gcd(d, c) = g。把第一式乘 x₂ 得 a·(x₁x₂) + b·(y₁x₂) = d·x₂,加上第二式即得 a·(x₁x₂) + b·(y₁x₂) + c·z₂ = g。

更一般地:对任意 n 个整数,{Σ aᵢxᵢ} 就等于 gcd(a₁, …, aₙ) 的倍数族。

代码上的体现:求多个数的 gcd 就是 g = gcd(a₁, gcd(a₂, gcd(a₃, …))),每次只处理两个数 —— 和上面的归并证明一模一样。

易错:以为三个数时要"同时"处理,其实拆成两组两个数的运算就够了。

思考2:exgcd 的递归深度是多少?为什么说它"几乎不会爆栈"?如果 a、b 取到 long long 的上界,最深能递归多少层?

查看参考答案

结论:递归深度与辗转相除法的步数相同,是 O(log(min(a, b))) 级别。

为什么只有对数级:每次递归参数变成 (b, a mod b),而 a mod b < b;更关键的是可以证明 a mod b < a / 2。因为若 b ≤ a/2,则 a mod b < b ≤ a/2;若 b > a/2,则 a mod b = a − b < a/2。所以每两层参数至少减半,深度是 O(log a)。

最坏情形:当 a、b 是相邻斐波那契数时步数最多。F(45) ≈ 1.13×10⁹,F(46) 已超过 long long 能容纳的一半量级附近;取到 9.2×10¹⁸ 时,对应的斐波那契下标约在 90 左右。所以 深度不会超过约 90 层。

实践意义:90 层的递归远在默认栈空间(通常 1~8 MB)承受范围内,可以放心用递归写,不需要改迭代。相比之下,朴素递归求斐波那契要递归 10⁹ 层,那才会爆栈。

易错:误以为"参数每步减半"(实际是每两步减半);或者以为数据大就要改成迭代。

思考3:已知 a·x + b·y = c 有一组解 (x₀, y₀),那么"|x| + |y| 的最小值"该怎么求?这个最小值有一个简洁的公式吗?

查看参考答案

思路:把通解 x(k) = x₀ + (b/g)k、y(k) = y₀ − (a/g)k 代入,得到关于 k 的分段线性函数 f(k) = |x(k)| + |y(k)|。

关键观察:x(k)、y(k) 都是 k 的线性函数,x 随 k 单调递增(因 b > 0),y 随 k 单调递减。所以 f(k) 是"两段折线":在 x 变号处和 y 变号处各有一个转折点。f 的最小值一定出现在这两个转折点或其相邻整数上。

算法:令 k₁ = ⌈−x₀ / (b/g)⌉(x 过零点的位置),k₂ = ⌊y₀ / (a/g)⌋(y 过零点的位置)。只需在 {k₁−1, k₁, k₁+1, k₂−1, k₂, k₂+1} 这 6 个候选点上逐个算 f(k) 取最小即可,复杂度 O(1)。

简化结论(常见情形):当方程有正整数解时,最小值通常就是"把 x 归一到 [0, b/g),把 y 归一到 [0, a/g)"这两组解里较好的那一组。很多题目(如"最小绝对值系数")的标答就是在这两个候选里取 min。

易错:以为 f(k) 是凸函数就一定能三分(其实是两个绝对值之和,确实是凸的,三分可行但常数大);或者只算 k₁ 忘了 k₂。

模块 05

学习检测

先找错,再背口诀 —— 把易错点消灭在考试之前。

易错1

exgcd 回代时忘记保存 x′

❌ 错误
exgcd(b, a%b, x, y); 之后直接写 x = y; y = x - (a/b)*y;。第二行的 x 已经被第一行覆盖成 y′ 了,算出来的 y 完全不对。
✅ 正确
ll t = x; x = y; y = t - (a / b) * y; —— 必须先用临时变量 t 存下 x′,再覆盖 x,最后用 t 算 y。
口诀:先存 x′ 再覆盖,回代顺序不能乱!
易错2

求最小正整数解时忘记归一化

❌ 错误
exgcd(3, 10) 得到 x = −3,直接 cout << x; 输出 −3。或者写 x = x % b;,C++ 里 (-3) % 10 = -3,还是负数。
✅ 正确
x = (x % b + b) % b; 得到 7(先加一个模数再取模,把负数拉回 [0, b))。要求严格正整数时,若结果为 0 再 x = b。
口诀:C++ 取模会带符号,先加模再取模最保险!
易错3

不判有无解就直接算

❌ 错误
对 6x + 4y = 7 直接 exgcd 输出一组 (x, y)。程序算出的数代回去是 6x + 4y = 2,根本不是 7,但代码不报错。
✅ 正确
先算 g = exgcd(a, b, x0, y0),再判 if (c % g != 0) 就无解。注意特解要缩放:x = x0 * (c / g),不能直接用 x0。
口诀:先判 g 能不能整除 c,再动手解方程!
易错4

中间乘法溢出

❌ 错误
青蛙约会中写 ans = x0 * (c / g) % t;。x0 与 c/g 都可能接近 2×10⁹,乘积可达 4×10¹⁸ —— 虽然勉强在 long long 内,但若题目的模数再大一点(如 10¹⁸)就会溢出成负数。
✅ 正确
用 (__int128) 中转:ans = (ll)((__int128)a * b % mod);。或者先把两个因子各自对模数取模再乘(注意别丢负号)。
口诀:两个大数相乘,先上 __int128!
易错5

参数没用引用,解传不回来

❌ 错误
写成 ll exgcd(ll a, ll b, ll x, ll y)(少了 &)。函数内部算得再对,外层的 x、y 依然是调用前的初值(通常是 0 或随机值)。
✅ 正确
签名写成 ll exgcd(ll a, ll b, ll &x, ll &y)。这样递归的每一层都在同一组变量上操作,回代结果自然写回外层。
口诀:要带回结果,引用符号不能丢!
📋速查表
结论 / 公式内容前提条件
裴蜀定理ax + by = c 有整数解 ⟺ gcd(a,b) | ca、b 不全为 0
裴蜀等式存在 x,y 使 ax + by = gcd(a,b)—
互质刻画gcd(a,b)=1 ⟺ ∃x,y: ax+by=1—
gcd 最小值刻画gcd(a,b) = min{ax+by > 0}x,y 取遍整数
exgcd 回代x = y′,y = x′ − ⌊a/b⌋·y′需保存 x′
exgcd 边界b = 0:x = 1, y = 0,return a—
通解x = x₀ + (b/g)k,y = y₀ − (a/g)kg = gcd(a,b),k ∈ ℤ
最小正整数解((x₀ % t) + t) % t,0 则取 tt = b/g
同余方程判据a·x ≡ b (mod m) 有解 ⟺ gcd(a,m) | b—
解个数恰有 g = gcd(a,m) 个模 m 不同的解解为 x₀ + (m/g)k
exgcd 求逆元inv(a) ≡ x₀ (mod m)gcd(a,m)=1,m 任意
复杂度O(log max(a,b)),最坏约 90 层递归相邻斐波那契数最慢
📌笔记卡
必记概念
  • 有解 ⟺ gcd(a,b) | c
  • 互质 ⟺ 存在 ax + by = 1
  • 回代公式 x = y′、y = x′ − ⌊a/b⌋y′
  • 通解步长 t = b / g
  • 同余方程模 m 下有 g 个解
必背模板
  • exgcd:边界 + 递归 + 临时变量回代
  • inv(a,m):exgcd 后归一到 [0,m)
  • minPosX:乘 C 后对 B 取模,0 换 B
  • solveCong:三数同除 g 再求逆元
  • 合并两方程:m₁t ≡ r₂−r₁ (mod m₂)
考场提醒
  • 传引用别丢 &
  • 先判无解再求解
  • 归一化必须 (x%t+t)%t
  • 大数乘法上 __int128
  • 模数非素数必须用 exgcd 求逆元
课后练习

分层练习作业

A基础 / B提高 / C挑战,难度递增。C组后附 12 道洛谷原题。

🟢 A组 · 基础(4题)

A1计算题

手算 exgcd(35, 12),求出满足 35x + 12y = 1 的一组整数解。

查看参考答案

第一步(下探):35 = 2×12 + 11,所以 gcd(35,12) = gcd(12, 11)。

12 = 1×11 + 1,所以 gcd(12, 11) = gcd(11, 1)。

11 = 11×1 + 0,所以 gcd(11, 1) = gcd(1, 0) = 1。

第二步(回代):1×1 + 0×0 = 1 → 边界层 (x, y) = (1, 0)。

层 (11, 1):q = 11,x = 0、y = 1 − 11×0 = 1 → 11×0 + 1×1 = 1 ✓

层 (12, 11):q = 1,x = 1、y = 0 − 1×1 = −1 → 12×1 + 11×(−1) = 1 ✓

层 (35, 12):q = 2,x = −1、y = 1 − 2×(−1) = 3 → 35×(−1) + 12×3 = −35 + 36 = 1 ✓

答案:x = −1、y = 3。通解:x = −1 + 12k、y = 3 − 35k。

易错:回代时把 q 写成 ⌊b/a⌋;忘记用临时变量保存 x′。

A2编程题

实现 exgcd,输入 a、b,输出 gcd(a, b) 与一组满足 ax + by = gcd(a, b) 的整数解。

查看参考答案

思路:直接套模板,注意引用参数与临时变量。

#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 g = exgcd(b, a % b, x, y);
    ll t = x; x = y; y = t - (a / b) * y;
    return g;
}
int main() {
    ll a, b, x, y; cin >> a >> b;
    ll g = exgcd(a, b, x, y);
    cout << g << " " << x << " " << y << endl;
    return 0;
}

样例:输入 35 12 → 输出 1 -1 3。验算:35×(−1) + 12×3 = 1 ✓

易错:参数传值而非引用;递归调用写成 exgcd(a % b, b, x, y)(参数顺序反了)。

A3编程题

用 exgcd 求 a 在模 m 下的逆元(1 ≤ a, m ≤ 10⁹,保证 gcd(a, m) = 1)。

查看参考答案

思路:逆元满足 ax + my = 1,exgcd 求出的 x 归一到 [0, m) 就是答案。

#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 g = exgcd(b, a % b, x, y);
    ll t = x; x = y; y = t - (a / b) * y;
    return g;
}
int main() {
    ll a, m, x, y; cin >> a >> m;
    exgcd(a, m, x, y);
    cout << (x % m + m) % m << endl;
    return 0;
}

样例:输入 3 10 → 7;输入 5 12 → 5;输入 7 12 → 7(7×7 = 49 ≡ 1)。

易错:忘记归一化导致输出负数;未归一化时 x 可能大于 m,也需要取模。

A4证明题

证明:a、b 互质当且仅当存在整数 x、y 使得 ax + by = 1。

查看参考答案

必要性(gcd = 1 → 存在解):由裴蜀定理,存在整数 x₀、y₀ 使 a·x₀ + b·y₀ = gcd(a, b)。既然 gcd(a, b) = 1,这一组 (x₀, y₀) 就是零等于 1 的解。∎

充分性(存在解 → gcd = 1):设存在整数 x、y 使 ax + by = 1。令 d = gcd(a, b),则 d | a 且 d | b,所以 d | (ax + by) = 1。由 d 是正整数得 d = 1,即 a、b 互质。∎

验证:a = 35、b = 12 时 35×(−1) + 12×3 = 1,所以 gcd(35, 12) = 1 ✓

应用:这条等价刻画是"只用整数系数判断互质"的标准套路,也是欧拉定理、中国剩余定理证明中的常用工具。

易错:充分性方向里忘记说明"d 是正整数"(若允许 d = −1 会推出 d = ±1);或者用"a、b 都能被 d 整除"就直接断定 d = 1,缺少"d | 1"这一步。

🟡 B组 · 提高(4题)

B1综合编程

输入 a、b、c(均为 int 范围内正整数),判断 ax + by = c 是否有整数解,若有输出最小正整数 x 及对应的 y。

查看参考答案

思路:g = exgcd(a, b) → 判 c % g;否则把特解乘 c/g,再把 x 对 b/g 取模归一。

#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 g = exgcd(b, a % b, x, y);
    ll t = x; x = y; y = t - (a / b) * y;
    return g;
}
int main() {
    ll a, b, c, x, y; cin >> a >> b >> c;
    ll g = exgcd(a, b, x, y);
    if (c % g) { cout << "No Solution" << endl; return 0; }
    ll B = b / g, C = c / g;
    ll xm = (ll)((__int128)x * C % B + B) % B;
    if (xm == 0) xm = B;
    ll ym = (c - a * xm) / b;
    cout << xm << " " << ym << endl;
    return 0;
}

样例:输入 12 5 1 → 输出 3 -7(12×3 + 5×(−7) = 1 ✓);输入 6 4 7 → No Solution。

易错:忘记把特解乘 C;归一化时漏掉 +B;x * C 溢出。

B2综合编程

【P1082 同余方程】输入 a、b(2 ≤ a, b ≤ 2×10⁹,保证有解),输出 ax ≡ 1 (mod b) 的最小正整数解。

查看参考答案

思路:标准 exgcd 求逆元。因为题目保证有解,所以 gcd(a, b) = 1,直接 exgcd 后归一化。

#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 g = exgcd(b, a % b, x, y);
    ll t = x; x = y; y = t - (a / b) * y;
    return g;
}
int main() {
    ll a, b, x, y; cin >> a >> b;
    exgcd(a, b, x, y);
    cout << (x % b + b) % b << endl;
    return 0;
}

样例:输入 3 10 → 7;输入 2 3 → 2。

考点:exgcd 求逆元 + 负数归一化。

易错:用费马小定理(b 不是素数,直接 WA);输出未归一化的负数。

B3分析题

为什么 exgcd 的递归深度是对数级的?请给出严格证明,并说明最坏情况出现在什么输入上。

查看参考答案

核心引理:对任意正整数 a ≥ b,有 a mod b < a / 2。

证明:分两种情形。① 若 b ≤ a/2,则 a mod b < b ≤ a/2。② 若 b > a/2,则 a = 1·b + (a − b),其中 a − b < a − a/2 = a/2,所以 a mod b = a − b < a/2。两种情形都成立。∎

推论:一次递归调用 exgcd(a, b) 变成 exgcd(b, a mod b)。看两次递归:a → b → (a mod b)。由引理,a mod b ≤ a/2,所以每两层第一个参数至少减半。因此总层数是 O(log a)。

最坏输入:相邻的两个斐波那契数。设 a = F(n+1)、b = F(n),则 a mod b = F(n−1),每层只缩小到前一个斐波那契数,缩减比例约为黄金分割比 0.618,是所有输入中下降最慢的。

具体数值:F(88) ≈ 1.1×10¹⁸,F(89) ≈ 1.8×10¹⁸,都在 long long 范围内。所以取 a = F(89)、b = F(88) 时递归深度约 88 层 —— 这就是 long long 数据下的最坏情形。

实践结论:90 层的递归远低于栈溢出阈值,用递归完全安全;同时 O(log n) 的复杂度意味着即使数据到 10¹⁸,运算次数也只有几十次。

易错:误说"每层减半"(实际是每两层减半);或者以为最坏情况是 a、b 差很小。

B4综合编程

给定 n(n ≤ 10⁶),求 1..n 每个数在模素数 p 下的逆元之和,输出对 p 取模的结果。要求与"逐个用 exgcd 求"对比复杂度。

查看参考答案

思路:用线性递推公式 inv[1] = 1、inv[i] = (p − p/i) * inv[p % i] % p,O(n) 一次求出所有逆元。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1000010;
ll inv[N];
int main() {
    ll n, p; cin >> n >> p;
    inv[1] = 1;
    ll sum = 1 % p;
    for (ll i = 2; i <= n; i++) {
        inv[i] = (p - p / i) % p * inv[p % i] % p;   // 线性递推
        sum = (sum + inv[i]) % p;
    }
    cout << sum << endl;
    return 0;
}

样例:输入 10 1000000007 → 输出 728968262。逐项验证前几个:inv[2] = 500000004(2×500000004 = 1000000008 ≡ 1 ✓)、inv[3] = 333333336(3×333333336 = 1000000008 ≡ 1 ✓)、inv[5] = 400000003(5×400000003 ≡ 1 ✓)。

复杂度对比:线性递推 O(n) 预处理后可以 O(1) 查询,n = 10⁶ 时约 10⁶ 次运算;若对每个 i 单独 exgcd,则是 O(n log p),约 3×10⁷ 次运算,慢了约 30 倍。

公式推导:由 p = ⌊p/i⌋·i + (p mod i),两边对 p 取模得 0 ≡ ⌊p/i⌋·i + (p mod i),即 ⌊p/i⌋·i ≡ −(p mod i)。两边同乘 inv(i)·inv(p mod i),整理得 inv(i) ≡ −⌊p/i⌋·inv(p mod i),写成非负形式就是代码里的 (p − p/i)。

易错:把递推公式写成 inv[i] = (p - p/i) * inv[i % p](下标错了,应是 p % i);忘记 1 % p 处理 p = 1 的边界;数组用 int 存但在中间乘法时溢出。

🔴 C组 · 挑战(4题)

C1初赛选择题

下列说法正确的有几个?
① 方程 6x + 9y = 8 有整数解
② gcd(a, b) 一定可以表示成 ax + by 的形式(x, y 为整数)
③ 同余方程 ax ≡ b (mod m) 在模 m 下若有解,则解的个数等于 gcd(a, m)
④ 若 a·x + b·y = c 有一组整数解,则它有无穷多组整数解(a、b 均非零)

A. 1    B. 2    C. 3    D. 4

查看参考答案

答案:C(3 个)

①错误:gcd(6, 9) = 3,而 3 ∤ 8,由裴蜀定理无整数解。

②正确:这正是裴蜀定理的直接结论。

③正确:设 g = gcd(a, m),有解时通解为 x = x₀ + (m/g)·k,在模 m 的一个周期内恰有 g 个不同的值。

④正确:通解 x = x₀ + (b/g)k、y = y₀ − (a/g)k,k 取遍所有整数时 (x, y) 互不相同(因为 b/g ≠ 0 且 a/g ≠ 0),所以有无穷多组。

考点:裴蜀定理判据、gcd 的可表示性、同余方程解的个数。

易错:把 ④ 判断成"错"(以为解有限);或者对 ① 只用"两边都是整数"就认为一定有解。

C2构造与证明

用数学归纳法(对辗转相除的步数归纳)证明扩展欧几里得算法的正确性:算法一定能在有限步内返回一组满足 ax + by = gcd(a, b) 的整数解。

查看参考答案

归纳的对象:递归深度,也就是辗转相除的步数。

基础情形(步数为 0,即 b = 0):算法直接返回 x = 1、y = 0 与 a。此时 a×1 + 0×0 = a = gcd(a, 0),等式成立。✓

归纳假设:假设对任意输入 (a′, b′)(其中 b′ = a mod b 且递归步数更少的情形),算法都能返回 (x′, y′) 使 b·x′ + (a mod b)·y′ = gcd(b, a mod b)。

归纳步骤:设 gcd(a, b) = g。由辗转相除的恒等式 g = gcd(b, a mod b),归纳假设给出 b·x′ + (a mod b)·y′ = g。

把 a mod b = a − ⌊a/b⌋·b 代入左边:

b·x′ + (a − ⌊a/b⌋·b)·y′ = a·y′ + b·(x′ − ⌊a/b⌋·y′)

与目标形式 a·x + b·y = g 对照,取 x = y′、y = x′ − ⌊a/b⌋·y′ 即得 a·x + b·y = g。而这两个赋值恰好就是算法中回代那两行。

终止性:每次递归第二参数从 b 变成 a mod b < b,严格递减的非负整数序列必然在有限步内到 0,所以算法必终止。∎

证明的关键点:① 归纳参数选"步数"而不是"数值大小";② 必须用辗转相除恒等式把 gcd(b, a mod b) 换成 gcd(a, b);③ 回代公式不是"猜"出来的,而是把 a mod b 展开后整理出来的。

易错:归纳假设里漏掉"gcd(b, a mod b) = gcd(a, b)"这一步,导致等号两边对不上;或者只证终止性不证正确性。

C3综合编程

【P1516 青蛙的约会】两只青蛙在长度为 L 的环形数轴上跳跃,起点 x、y,每步跳 m、n 米,同向起跳。求最少跳多少次能相遇,无解输出 Impossible。

查看参考答案

建模:(x + m·t) ≡ (y + n·t) (mod L) → (m − n)·t ≡ y − x (mod L)。

#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 g = exgcd(b, a % b, x, y);
    ll t = x; x = y; y = t - (a / b) * y;
    return g;
}
int main() {
    ll x, y, m, n, L; cin >> x >> y >> m >> n >> L;
    ll a = m - n, c = y - x;
    if (a < 0) { a = -a; c = -c; }
    ll x0, y0;
    ll g = exgcd(a, L, x0, y0);
    if (c % g) { cout << "Impossible" << endl; return 0; }
    ll t = L / g;
    ll ans = (ll)((__int128)((x0 % t + t) % t) * ((c / g) % t + t) % t);
    if (ans == 0) ans = t;
    cout << ans << endl;
    return 0;
}

样例一:输入 1 2 3 4 5 → 输出 4。验证:t=4 时 A 在 1+12=13≡3,B 在 2+16=18≡3。前 4 次(t=0,1,2,3)位置分别为 (1,2)、(4,1)、(2,0)、(0,4),均不相等。

样例二:输入 2 1 4 6 8 → 输出 Impossible。a = −2 → 2、c = −1;g = gcd(2, 8) = 2,2 ∤ −1,无解。

易错:忘记把负系数转正;忘记判无解;乘积溢出;结果 0 未替换为 t。

C4算法设计

设计算法:给定 a、b、c(1 ≤ a, b, c ≤ 10⁹),求 ax + by = c 的所有解中,使 |x| + |y| 最小的那一组的 |x| + |y| 值。要求 O(log) 复杂度,并说明为什么只需检查常数个候选点。

查看参考答案

第一步:用 exgcd 判无解 —— 若 gcd(a,b) ∤ c 输出 −1。

第二步:得到一组特解 (x₀, y₀)(把 exgcd 的解乘以 c/g),并令步长 dx = b/g、dy = −a/g。通解为 x(k) = x₀ + dx·k、y(k) = y₀ + dy·k。

第三步(为什么只需常数个候选点):f(k) = |x(k)| + |y(k)| 是两个绝对值函数之和。因为 dx > 0、dy < 0,所以 x(k) 单调递增、y(k) 单调递减,各自只在一个零点处改变符号。于是 f(k) 是一条"两段折线",共有两个转折点:

k₁ = ⌈−x₀ / dx⌉(x 变号处)   k₂ = ⌊y₀ / (−dy)⌋ = ⌊y₀ / (a/g)⌋(y 变号处)

折线函数的最小值必在转折点处取到(也可能是相邻整数点,因为 k 必须为整数)。所以只需检查 {k₁−1, k₁, k₁+1, k₂−1, k₂, k₂+1} 这 6 个点,取 f 的最小值即可。

#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 g = exgcd(b, a % b, x, y);
    ll t = x; x = y; y = t - (a / b) * y;
    return g;
}
ll f(ll x, ll y) { return llabs(x) + llabs(y); }
int main() {
    ll a, b, c, x, y; cin >> a >> b >> c;
    ll g = exgcd(a, b, x, y);
    if (c % g) { cout << -1 << endl; return 0; }
    ll C = c / g;
    ll x0 = (ll)((__int128)x * C), y0 = (ll)((__int128)y * C);
    ll dx = b / g, dy = a / g;              // dy 实际上带负号
    // 两个转折点的候选 k(用向下取整除法求)
    auto floordiv = [](ll p, ll q) -> ll {
        if (q < 0) { p = -p; q = -q; }
        if (p >= 0) return p / q;
        return -(((-p) + q - 1) / q);
    };
    ll k1 = -floordiv(-x0, dx);              // ceil(-x0 / dx) = -floor(x0/dx)
    ll k2 = floordiv(y0, dy);
    ll ans = LLONG_MAX;
    for (ll k : {k1 - 1, k1, k1 + 1, k2 - 1, k2, k2 + 1}) {
        ans = min(ans, f(x0 + dx * k, y0 - dy * k));
    }
    cout << ans << endl;
    return 0;
}

样例:输入 3 5 16 → 输出 4。验证:解为 (2,2)、(7,−1)、(−3,5)、(−8,8)、…,|x|+|y| 分别为 4、8、8、16,最小是 4 ✓

样例二:输入 6 4 7 → 输出 -1。

为什么不能直接三分:f(k) 确实是凸的,三分可以但需要处理整数边界且常数大。转折点法只需 6 次求值,更简单也更稳。

易错:只算 k₁ 忘了 k₂(比如 y 先过零点的情形);用 C++ 的 / 直接求 ceil(负数时会错);x * C 溢出;候选点忘了 ±1 修正。

🏆 洛谷原题精选(12道)

以下 12 道题围绕"扩展欧几里得 / 不定方程 / 同余方程 / 求逆元"展开,按难度从普及- 到提高排列。

1
P1372
又是毕业季I普及-
2
P3811
【模板】乘法逆元普及+
3
P2613
【模板】有理数取余普及+
4
P1082
同余方程【NOIP2012提高组】提高
5
P1072
[NOIP2009] Hankson 的趣味题提高
6
P1516
青蛙的约会提高
7
P5656
二元一次不定方程提高
8
P3951
[NOIP2017] 小凯的疑惑提高
9
P1290
欧几里德的游戏提高
10
P2421
[NOI2002] 荒岛野人提高
11
P2054
[AHOI2005] 洗牌提高
12
P2152
[SDOI2009] SuperGCD提高

提示:第 12 题是"高精度 + gcd"的综合题,虽然不用 exgcd 但要自己实现大整数的取模与辗转相除(可以用二进制 gcd 加速);第 8 题(小凯的疑惑)的答案是 a·b − a − b,其证明要用到"a、b 互质时最大不能表示的数",正好是裴蜀定理的一个漂亮推论。