题解:P5656 【模板】二元一次不定方程 (exgcd)

· · 题解

扩展欧几里得算法

前置知识:欧几里得算法

扩展欧几里得算法(Extended Euclidean algorithm, EXGCD)用于求形如

ax + by = c

二元一次不定方程的所有整数解。

(为了方便,我们认为 a, b, c \ge 0。对于负数的情况可能需要多一点讨论。)

有解性判定——裴蜀定理

裴蜀定理(Bézout's lemma)给出了二元一次不定方程有整数解的充要条件:方程 ax + by = c 有解当且仅当

\gcd(a, b) \mid c

证明

\gcd(a, b) = g

必要性 由于等式左边一定是 g 的倍数,因此等式右边也必须是 g 的倍数。

充分性 为了方便,不妨设 c = g。如果在这种情况下存在某组解,那么当 cg 的倍数时,把这组解扩大若干整数倍,就可以得到原方程的解。所以研究有解性时只需考虑这种情况。

S = \{xa + yb | x, y \in \mathbb{Z}\}。下面证明 S 中的最小正元素为 g

首先,S 中至少有一个正元素(ab,说“一个”是因为二者可能相同),设最小的正元素为 d_0 = x_0a + y_0b。另取 S 中任一正元素 p = x_1a + y_1b,考虑它对 d 的带余除法 p = qd_0 + r,那么

\begin{aligned} r &= p - qd_0 \\ &= (x_1a + y_1b) - q(x_0a - y_0b) \\ &= (x_1 - qx_0)a + (y_1 + qy_0)b \end{aligned}

因此 r \in S。由于 rp 除以 d_0 的余数,所以 0 \le r < d_0。如果 r 为正数,就与 d_0 的最小性矛盾。因此 r = 0。那么可以推出:S 中任意正元素都是 d_0 的倍数。特别地,有 d_0 \mid ad_0 \mid b,因此 d_0ab 的公约数。

另一方面,由于 ab 都是 g 的倍数,而 d_0ab 的线性组合,因此 d_0 也是 g 的倍数(在 I 章中我们已经用过了这个结论)。结合两点可知 d_0 = g。至此,我们就证明了当 c = gax + by = c 一定有解。对于更一般的情况,如果 c = c'g,把 ax + by = g 的每个解乘 c' 倍就得到了 ax + by = c 的解,所以两个方程的有解性相同。\Box

解的形式及个数

我们可以进一步说明,只要有解,就有无数组解。设 (x_0, y_0) 是任意一组解,a = a'gb = b'g,则方程的解集为

A = \left\{ \left( x_0 + kb', y_0 - ka' \right) | k \in \mathbb{Z} \right \}

换句话说,把 x 加上若干个 b',同时把 y 减去若干个 a' 不改变 ax + by 的值,因为 a \cdot \dfrac{kb}{g} = b \cdot \dfrac{ka}{g}。另外,这两者显然都是整数,所以变换后得到的还是整数解。

另一方面,对于任意的解 (x', y') 满足 ax' + by' = c,把此等式与 ax_0 + by_0 = c 相减,有

a(x' - x_0) + b(y' - y_0) = 0

由于 a = a'gb = b'g,所以

a'(x' - x_0) = -b'(y' - y_0)

左边是 a' 的倍数,因此右边也是 a' 的倍数。又因为 a'b' 互质,所以 y' - y_0a' 的倍数。设 y' - y_0 = a'k,则 y' = y_0 + a'k,代入得 x' = x_0 - b'k,这就把 (x', y') 转化成了 A 中的形式。所以,A 确实包含了方程的所有整数解。

过程

现在我们要求出 ax + by = c任意一组解。先根据裴蜀定理判断是否有解,如果有解,为了方便仍然假设 c = g。求出一组解以后我们可以根据通解的公式求出其它的解。

我们使用和欧几里得算法类似的迭代方法:每次迭代都会使 ab 减小,最终 b 减小到 0 时就可以得到显然的解,然后反推原方程的解。

具体地,如果 b = 0,那么 \gcd(a, b) = a,方程可以写为 ax + 0y = a。此时显然 x = 1, y = 0 是一组解。当然,y 可以取任何整数,但取 0 比较方便,并且这保证了最终得到的原方程的解不会太大,

否则,我们把原方程迭代为一个新的方程:a'x + b'y = \gcd(a', b'),其中 a' = bb' = a \bmod b。这样做的意义和欧几里得算法是相同的:迭代后方程的系数会减小,最终到达边界情况时就能得到解。那么,假设我们求出了这个方程的一组解 (x_1, y_1),要如何反推回原方程的一组解 (x_0, y_0) 呢?

由于 \gcd(a, b) = \gcd(b, a \bmod b),所以 \gcd(a, b) = \gcd(a', b'),因此

ax_0 + by_0 = bx_1 + (a \bmod b) y_1

a \bmod b 改写为 a - b \cdot \left\lfloor \dfrac{a}{b} \right\rfloor,那么

ax_0 + by_0 = bx_1 + \left( a - b \cdot \left\lfloor \dfrac{a}{b} \right\rfloor \right)y_1

整理等式右边,得到

ax_0 + by_0 = ay_1 + b \left(x_1 - \left\lfloor \dfrac{a}{b} \right\rfloor y_1 \right)

对比两边得到

\begin{cases} x_0 = y_1 \\ y_0 = \left(x_1 - \left\lfloor \dfrac{a}{b} \right\rfloor y_1 \right) \end{cases}

这就得到了两个方程解的关系。

此算法时间复杂度的分析和欧几里得算法完全相同,二者的时间复杂度都是 O(\log w),其中 wab 的值域,这里不再赘述。

以下是一份 exgcd 的示例代码,它返回一个二元组(pair),表示方程的解 x, y

auto exgcd(int a, int b) {
    if(b == 0) {
        return make_pair(1, 0);
    }
    auto [x, y] = exgcd(b, a % b);
    return make_pair(y, x - (a / b) * y);
}

这种写法比较美观,因为函数的返回值恰好就是方程的解。但我不知道使用 pair 会不会导致常数偏大。大多数人使用的是一种传引用的写法:

void exgcd(int a, int b, int &x, int &y) {
    if(b == 0) {
        x = 1, y = 0;
        return;
    }
    int x0, y0;
    exgcd(b, a % b, x0, y0);
    x = y0, y = x0 - (a / b) * y0;
}

值域分析

需要注意的是,我们只是说明了 $ax + by = g$ 的解的值域范围,但对于更一般的 $ax + by = c$ 的情况,把 $x$ 和 $y$ 同乘上 $c / g$ 以后,值域是没有限制的。 ### 本题的求解 这道题不仅要求出解,还要回答有关解的一些其它问题。我们逐个来研究。 1. **是否有解?** 我们用裴蜀定理判断是否有解。现在假设有解,并且已经用扩欧求出了一组解 $(x_0, y_0)$。 2. **是否有正整数解?** 我们让 $x_0$ 对 $b'$ 取模,就得到了 $x$ 为正整数的解中最小的 $x$。(为了保证 $x$ 为正,当 $x_0 \bmod b' = 0$ 时应取 $x = b'$ 而不是 $0$。)求出对应的 $y$,由于 $x$ 和 $y$ 是此消彼长的关系,现在已经尽可能让 $x$ 在为正的情况下尽可能小了,如果存在正整数解,那么 $y$ 此时一定为正,据此可以判断是否有正整数解。 如果有正整数解,那么显然现在求出的 $x$ 和 $y$ 分别是正整数解中最小的 $x$ 和最大的 $y$,记为 $x_{\min}$ 和 $y_{\max}$。同理还可以求出正整数解中最大的 $x$ 和最小的 $y$,记为 $x_{\max}$ 和 $y_{\min}$。而如果没有正整数解,我们实际上也已经求出了 $x$ 和 $y$ 的最小正整数值。 3. **如果有正整数解,有多少个?** 只需求出 $x_{\max}$ 和 $x_{\min}$ 中有多少个可以作为解的 $x$ 即可,也就是 $$ \dfrac{x_{\max} - x_{\min}}{b'} + 1 $$ 综上所述,我们就解决了本题。 代码: ```cpp #include<bits/stdc++.h> using namespace std; typedef long long i64; i64 mod(i64 x, i64 y) { i64 t = (x % y + y) % y; return t ? t : y; // 取正值 } auto exgcd(int a, int b) { if(b == 0) { return make_pair(1, 0); } auto [x, y] = exgcd(b, a % b); return make_pair(y, x - (a / b) * y); } void solve() { int a, b, c; cin >> a >> b >> c; int g = __gcd(a, b), a0 = a / g, b0 = b / g, c0 = c / g; if(c % g != 0) { cout << "-1\n"; // 无解 return; } i64 x, y; tie(x, y) = exgcd(a, b); x *= c0, y *= c0; i64 xmin = mod(x, b0), ymax = (c - a * xmin) / b; i64 ymin = mod(y, a0), xmax = (c - b * ymin) / a; if(ymax > 0) { i64 cnt = (xmax - xmin) / b0 + 1; cout << cnt << ' ' << xmin << ' ' << ymin << ' ' << xmax << ' ' << ymax << '\n'; } else { cout << xmin << ' ' << ymin << '\n'; } } int main() { cin.tie(nullptr) -> sync_with_stdio(false); int t; cin >> t; while(t--) { solve(); } return 0; } ```