Chương 4·Bài 47

Extended Euclidean Algorithm

Có hai bình 35 lít và 15 lít, đổ đầy hay đổ hết bao nhiêu lần tùy ý. Lượng nước nhỏ nhất đong được là 5 lít, đúng gcd⁡(35,15)\gcd(35, 15), và 35⋅1−15⋅2=535 \cdot 1 - 15 \cdot 2 = 5 cho biết cách đong: đổ đầy bình lớn một lần, rót bỏ hai bình nhỏ.

Thuật toán Euclid (bài 08) tìm g=gcd⁡(a,b)g = \gcd(a, b) bằng cách thay (a,b)(a, b) bởi (b,a mod b)(b, a \bmod b). Đi ngược các bước đó còn cho thêm hai số nguyên x,yx, y với ax+by=gax + by = g, gọi là hệ số Bézout. Từ một cặp hệ số này giải được mọi bài tuyến tính trên số nguyên: nghịch đảo modulo khi modulo không nguyên tố, đồng dư ax≡b(modm)ax \equiv b \pmod m, phương trình ax+by=cax + by = c cùng cả họ nghiệm của nó.

Hệ số Bézout

Giả sử đã có x1,y1x_1, y_1 với bx1+ry1=gb x_1 + r y_1 = g, trong đó r=a mod b=a−qbr = a \bmod b = a - qb, q=⌊a/b⌋q = \lfloor a/b \rfloor. Thay rr vào: bx1+(a−qb)y1=gb x_1 + (a - qb) y_1 = g, tức a⋅y1+b⋅(x1−qy1)=ga \cdot y_1 + b \cdot (x_1 - q y_1) = g. Vậy x=y1x = y_1, y=x1−qy1y = x_1 - q y_1. Điểm dừng là b=0b = 0: a⋅1+0⋅0=a=ga \cdot 1 + 0 \cdot 0 = a = g.

Đi xuống bằng phép chia lấy dư, rồi truy ngược từng hàng: x = y₁, y = x₁ − q·y₁.
C++
ll ext(ll a, ll b, ll &x, ll &y) {
    if (!b) {
        x = 1, y = 0;
        return a;
    }
    ll x1, y1, g = ext(b, a % b, x1, y1);
    x = y1;
    y = x1 - a / b * y1;
    return g;
}

Độ sâu đệ quy như Euclid, O(log⁡min⁡(a,b))O(\log \min(a, b)). Các hệ số trả về thỏa ∣x∣≤b/g|x| \le b/g và ∣y∣≤a/g|y| \le a/g, nên không tràn khi a,ba, b vừa ll. Mọi cặp hệ số khác có dạng (x+kbg, y−kag)(x + k \frac{b}{g},\ y - k \frac{a}{g}) với kk nguyên: hiệu của hai nghiệm phải thỏa aΔx=−bΔya \Delta x = -b \Delta y, và ag,bg\frac{a}{g}, \frac{b}{g} nguyên tố cùng nhau.

Hỏi nhanh

Đã biết 35⋅1+15⋅(−2)=535 \cdot 1 + 15 \cdot (-2) = 5. Cặp nào dưới đây cũng là hệ số Bézout của (35,15)(35, 15)?

Ví dụHệ số Bézout tối ưu

Đề. Cho a,b≤109a, b \le 10^9. In hệ số Bézout x,yx, y của gcd⁡(a,b)\gcd(a, b) với ∣x∣+∣y∣|x| + |y| nhỏ nhất; hòa thì ưu tiên x≤yx \le y, rồi xx nhỏ hơn.

Lời giải. Euclid mở rộng cho một nghiệm (x,y)(x, y); mọi nghiệm khác là (x+kbg, y−kag)(x + k \frac{b}{g},\ y - k \frac{a}{g}). Nghiệm của Euclid đã có ∣x∣≤b/g|x| \le b/g và ∣y∣≤a/g|y| \le a/g, nên nghiệm tối ưu nằm trong vài nghiệm lân cận, kk từ −2-2 tới 22.

Thử các nghiệm lân cận. Hàm ext như trên. da, db là bước dời của họ nghiệm. So theo ∣x∣+∣y∣|x| + |y|, hòa thì so cặp (x > y, x): ưu tiên x≤yx \le y trước, xx nhỏ sau.

C++
for (ll k = -2; k <= 2; k++) {
    ll cx = x + k * da, cy = y - k * db;
    ll s1 = llabs(cx) + llabs(cy), s2 = llabs(bx) + llabs(by);
    if (s1 < s2 || (s1 == s2 && make_pair(cx > cy, cx) < make_pair(bx > by, bx))) bx = cx, by = cy;
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

ll ext(ll a, ll b, ll &x, ll &y) {
    if (!b) {
        x = 1, y = 0;
        return a;
    }
    ll x1, y1, g = ext(b, a % b, x1, y1);
    x = y1;
    y = x1 - a / b * y1;
    return g;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    ll a, b, x, y;
    cin >> a >> b;
    ll g = ext(a, b, x, y), da = b / g, db = a / g;
    ll bx = x, by = y;
    for (ll k = -2; k <= 2; k++) {
        ll cx = x + k * da, cy = y - k * db;
        ll s1 = llabs(cx) + llabs(cy), s2 = llabs(bx) + llabs(by);
        if (s1 < s2 || (s1 == s2 && make_pair(cx > cy, cx) < make_pair(bx > by, bx))) bx = cx, by = cy;
    }
    cout << bx << ' ' << by << '\n';
}
35 15

O(log⁡min⁡(a,b))O(\log \min(a, b)).

Nghịch đảo modulo

Nghịch đảo của aa modulo mm là xx với ax≡1(modm)ax \equiv 1 \pmod m, tức ax+my=1ax + my = 1 có nghiệm: tồn tại khi và chỉ khi gcd⁡(a,m)=1\gcd(a, m) = 1, và khi đó Euclid mở rộng cho xx ngay. Đưa về [0,m)[0, m) bằng (x % m + m) % m. Khác với lũy thừa am−2a^{m-2} (định lý Fermat), cách này đúng với mọi modulo, kể cả hợp số, và chỉ tốn O(log⁡m)O(\log m).

Khi cần nghịch đảo của cả dãy 1,2,…,n1, 2, \dots, n theo modulo nguyên tố pp, có cách nhanh hơn cả hai. Viết p=⌊p/i⌋⋅i+(p mod i)p = \lfloor p/i \rfloor \cdot i + (p \bmod i), lấy theo modulo pp rồi nhân hai vế với i−1(p mod i)−1i^{-1} (p \bmod i)^{-1}:

i−1≡−⌊pi⌋⋅(p mod i)−1(modp),i^{-1} \equiv -\left\lfloor \frac{p}{i} \right\rfloor \cdot (p \bmod i)^{-1} \pmod p,

mà p mod i<ip \bmod i < i đã có nghịch đảo từ trước, nên mỗi số chỉ tốn một phép nhân:

C++
inv[1] = 1;
for (ll i = 2; i <= n; i++) inv[i] = (MOD - MOD / i) * inv[MOD % i] % MOD;
Nghịch đảo của mười triệu số 1, 2, …, 10⁷ theo modulo 10⁹ + 7, trung vị 5 lần chạy, i7-10850H, g++ -O2.lũy thừa p − 2 từng số868 ms · gấp 2,1 lầnEuclid mở rộng từng số1716 ms · gấp 4,2 lầnbảng truy hồi tuyến tính406 ms
Nghịch đảo của 1, 2, …, 10⁷ theo modulo 10⁹ + 7 bằng ba cách, đo trên i7-10850H, g++ -O2.

Bảng truy hồi nhanh nhất. Lạ hơn là Euclid mở rộng chậm gấp đôi lũy thừa: nó chia cho số thay đổi ở mỗi bước, còn lũy thừa chỉ chia cho hằng MOD (bài 08). Modulo nguyên tố thì cứ dùng lũy thừa; Euclid mở rộng để dành cho modulo hợp số.

Đồng dư tuyến tính

Đồng dư ax≡b(modm)ax \equiv b \pmod m là phương trình ax−my=bax - my = b. Gọi g=gcd⁡(a,m)g = \gcd(a, m): vế trái luôn chia hết cho gg, nên vô nghiệm nếu g∤bg \nmid b. Ngược lại chia cả ba số cho gg: a′x≡b′(modm′)a' x \equiv b' \pmod{m'} với gcd⁡(a′,m′)=1\gcd(a', m') = 1, nghiệm duy nhất theo modulo m′m' là x≡b′⋅(a′)−1x \equiv b' \cdot (a')^{-1}. Theo modulo mm ban đầu có đúng gg nghiệm, cách nhau m′m'. Phép nhân b′⋅(a′)−1b' \cdot (a')^{-1} có thể tới 4⋅10184 \cdot 10^{18}, nên lấy dư trước khi nhân hoặc dùng __int128.

Hỏi nhanh

Đồng dư 4x≡2(mod6)4x \equiv 2 \pmod 6 có bao nhiêu nghiệm trong {0,1,…,5}\{0, 1, \dots, 5\}?

Ví dụĐồng dư tuyến tính

Đề. T≤105T \le 10^5 bộ a,b,m≤2⋅109a, b, m \le 2 \cdot 10^9. Với mỗi bộ in một nghiệm của ax≡b(modm)ax \equiv b \pmod m, hoặc impossible.

Lời giải. Với g=gcd⁡(a,m)g = \gcd(a, m): g∤bg \nmid b thì vô nghiệm. Ngược lại chia cả ba cho gg, được a′x≡b′(modm′)a' x \equiv b' \pmod{m'} với a′a' nguyên tố cùng nhau với m′m', nên x=a′−1b′ mod m′x = a'^{-1} b' \bmod m'. Nghiệm này nằm trong [0,m′)[0, m'), nhỏ nhất; các nghiệm khác hơn nó một bội của m′m'.

Bước 1: nghịch đảo. ext(a, m, x, y) cho ax+my=gax + my = g; chia cho gg thấy xx chính là nghịch đảo của a/ga/g theo mô-đun m/gm/g.

C++
ll g = ext(a, m, x, y);
if (b % g) {
    cout << "impossible\n";
    continue;
}

Bước 2: nhân. Hai thừa số dưới m′m' (tới 2⋅1092 \cdot 10^9), tích tới 4⋅10184 \cdot 10^{18}, sát giới hạn long long, nên ép sang __int128 cho chắc.

C++
ll mm = m / g;
ll r = (__int128)(x % mm + mm) % mm * (b / g % mm) % mm;
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

ll ext(ll a, ll b, ll &x, ll &y) {
    if (!b) {
        x = 1, y = 0;
        return a;
    }
    ll x1, y1, g = ext(b, a % b, x1, y1);
    x = y1;
    y = x1 - a / b * y1;
    return g;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    ll T;
    cin >> T;
    while (T--) {
        ll a, b, m, x, y;
        cin >> a >> b >> m;
        ll g = ext(a, m, x, y);
        if (b % g) {
            cout << "impossible\n";
            continue;
        }
        ll mm = m / g;
        ll r = (__int128)(x % mm + mm) % mm * (b / g % mm) % mm;
        cout << r << '\n';
    }
}
3
4 2 6
4 3 6
7 5 9

Mỗi bộ O(log⁡m)O(\log m).

Ví dụĐường đến ngai vàng

Đề. Vòng tròn N≤109N \le 10^9 vị trí, đang ở SS, mỗi bước tiến KK. In số bước nhỏ nhất (kể cả 0) để tới vị trí 0, hoặc −1-1.

Lời giải. Sau tt bước đang ở (S+tK) mod N(S + tK) \bmod N. Cần S+tK≡0(modN)S + tK \equiv 0 \pmod N, tức Kt≡N−S(modN)Kt \equiv N - S \pmod N: đồng dư tuyến tính của ví dụ trước với c=N−Sc = N - S. Nghiệm nhỏ nhất không âm là đáp án, vì mọi nghiệm khác hơn nó một bội của N/gN/g.

Bước 1: giải đồng dư. Ở đây m′=N/g≤109m' = N/g \le 10^9, tích hai số dưới 10910^9 vừa long long, không cần __int128.

C++
ll g = ext(k, n, x, y), c = n - s;
if (c % g) {
    cout << -1 << '\n';
    continue;
}
ll mm = n / g;
cout << (x % mm + mm) % mm * (c / g % mm) % mm << '\n';

S=0S = 0 cho c=Nc = N, chia hết cho gg và c/g mod m′=0c/g \bmod m' = 0, nên in 0 đúng như mong muốn.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

ll ext(ll a, ll b, ll &x, ll &y) {
    if (!b) {
        x = 1, y = 0;
        return a;
    }
    ll x1, y1, g = ext(b, a % b, x1, y1);
    x = y1;
    y = x1 - a / b * y1;
    return g;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    ll T;
    cin >> T;
    while (T--) {
        ll n, s, k, x, y;
        cin >> n >> s >> k;
        ll g = ext(k, n, x, y), c = n - s;
        if (c % g) {
            cout << -1 << '\n';
            continue;
        }
        ll mm = n / g;
        cout << (x % mm + mm) % mm * (c / g % mm) % mm << '\n';
    }
}
3
10 4 3
6 1 4
7 0 3

Mỗi bộ O(log⁡N)O(\log N).

Họ nghiệm

Phương trình có nghiệm nguyên khi và chỉ khi g=gcd⁡(a,b)g = \gcd(a, b) chia hết cc. Khi đó nhân hệ số Bézout với c/gc/g được một nghiệm (x0,y0)(x_0, y_0), và mọi nghiệm là

x=x0+k⋅bg,y=y0−k⋅ag,k∈Z.x = x_0 + k \cdot \frac{b}{g}, \qquad y = y_0 - k \cdot \frac{a}{g}, \qquad k \in \mathbb{Z}.

Khi a,b>0a, b > 0, xx tăng thì yy giảm. Muốn nghiệm có xx dương nhỏ nhất: lấy x0x_0 modulo b/gb/g rồi đưa về [1,b/g][1, b/g], tính yy từ phương trình. Nếu yy khi đó vẫn dương thì có nghiệm dương, và số nghiệm dương là số bước a/ga/g còn trừ được từ yy mà vẫn dương. Với hệ số âm, đổi dấu ẩn tương ứng rồi giải với giá trị tuyệt đối.

Đường thẳng ax + by = c; chấm là các nghiệm nguyên, chấm đỏ là nghiệm dương. Đổi c sang số không chia hết cho gcd để thấy vô nghiệm.

Ví dụĐiểm nguyên trên đường thẳng

Đề. Tìm một điểm nguyên trên đường Ax+By+C=0Ax + By + C = 0 với ∣A∣,∣B∣,∣C∣≤2⋅109|A|, |B|, |C| \le 2 \cdot 10^9, hoặc in −1-1.

Lời giải. Cần Ax+By=−CAx + By = -C. Giải với giá trị tuyệt đối để ext chạy trên số không âm: ∣A∣x′+∣B∣y′=g|A| x' + |B| y' = g. Nếu g∤Cg \nmid C thì vô nghiệm.

Bước 1: nhân và đổi dấu. Nhân hai hệ số với −C/g-C/g được một nghiệm của ∣A∣x+∣B∣y=−C|A| x + |B| y = -C; đổi dấu xx khi A<0A < 0, yy khi B<0B < 0 để về đúng phương trình gốc. ∣x′∣≤∣B∣/g|x'| \le |B|/g, nhân với ∣C∣/g≤2⋅109|C|/g \le 2 \cdot 10^9, tích dưới 4⋅10184 \cdot 10^{18}, vừa long long.

C++
x *= -C / g;
y *= -C / g;
if (A < 0) x = -x;
if (B < 0) y = -y;
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

ll ext(ll a, ll b, ll &x, ll &y) {
    if (!b) {
        x = 1, y = 0;
        return a;
    }
    ll x1, y1, g = ext(b, a % b, x1, y1);
    x = y1;
    y = x1 - a / b * y1;
    return g;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    ll A, B, C, x, y;
    cin >> A >> B >> C;
    ll g = ext(llabs(A), llabs(B), x, y);
    if (C % g) {
        cout << -1 << '\n';
        return 0;
    }
    x *= -C / g;
    y *= -C / g;
    if (A < 0) x = -x;
    if (B < 0) y = -y;
    cout << x << ' ' << y << '\n';
}
6 9 -3

O(log⁡min⁡(∣A∣,∣B∣))O(\log \min(|A|, |B|)).

Ví dụMẫu Phương Trình Nghiệm Nguyên

Đề. T≤2⋅105T \le 2 \cdot 10^5 phương trình ax+by=cax + by = c với a,b,c≤109a, b, c \le 10^9. Không có nghiệm nguyên thì in −1-1; có nghiệm nhưng không có nghiệm dương thì in xx dương nhỏ nhất và yy dương nhỏ nhất (có thể từ hai nghiệm khác nhau); có nghiệm dương thì in số nghiệm dương, xx nhỏ nhất, yy nhỏ nhất, xx lớn nhất, yy lớn nhất.

Lời giải. Đặt p=b/gp = b/g, q=a/gq = a/g: mỗi bước tăng xx thêm pp thì yy giảm qq. Lấy nghiệm có xx dương nhỏ nhất; ứng với nó yy lớn nhất. Từ đó đếm được còn bao nhiêu bước mà yy vẫn dương.

Bước 1: xx dương nhỏ nhất. Nhân x0x_0 với c/gc/g theo mô-đun pp, rồi đưa về [1,p][1, p]. Tích tới 101810^{18}, vẫn vừa long long.

C++
ll p = b / g, q = a / g;
ll x = (x0 % p) * (c / g % p) % p;
x = (x % p + p) % p;
if (!x) x = p;
ll y = (c - a * x) / b;

(c - a * x) / b chia hết vì (x,y)(x, y) thuộc họ nghiệm.

Bước 2: có nghiệm dương. y≥1y \ge 1 thì số nghiệm dương là số lần còn giảm yy đi qq mà vẫn ≥1\ge 1, cộng chính nghiệm hiện tại: ⌊(y−1)/q⌋+1\lfloor (y - 1)/q \rfloor + 1. Nghiệm cuối cho yy nhỏ nhất và xx lớn nhất.

C++
ll cnt = (y - 1) / q + 1;
cout << cnt << ' ' << x << ' ' << y - (cnt - 1) * q << ' ' << x + (cnt - 1) * p << ' ' << y << '\n';

Bước 3: không có nghiệm dương. Không nghiệm nào cả hai cùng dương; yy dương nhỏ nhất (của một nghiệm khác) là yy đưa về [1,q][1, q].

C++
ll ym = (y % q + q) % q;
if (!ym) ym = q;
cout << x << ' ' << ym << '\n';
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

ll ext(ll a, ll b, ll &x, ll &y) {
    if (!b) {
        x = 1, y = 0;
        return a;
    }
    ll x1, y1, g = ext(b, a % b, x1, y1);
    x = y1;
    y = x1 - a / b * y1;
    return g;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    ll T;
    cin >> T;
    while (T--) {
        ll a, b, c, x0, y0;
        cin >> a >> b >> c;
        ll g = ext(a, b, x0, y0);
        if (c % g) {
            cout << -1 << '\n';
            continue;
        }
        ll p = b / g, q = a / g;
        ll x = (x0 % p) * (c / g % p) % p;
        x = (x % p + p) % p;
        if (!x) x = p;
        ll y = (c - a * x) / b;
        if (y >= 1) {
            ll cnt = (y - 1) / q + 1;
            cout << cnt << ' ' << x << ' ' << y - (cnt - 1) * q << ' ' << x + (cnt - 1) * p << ' ' << y << '\n';
        } else {
            ll ym = (y % q + q) % q;
            if (!ym) ym = q;
            cout << x << ' ' << ym << '\n';
        }
    }
}
4
2 3 12
4 6 5
4 6 20
3 5 18

Mỗi phương trình O(log⁡min⁡(a,b))O(\log \min(a, b)).

Khi đi thi

  • Nghịch đảo modulo hợp số, đồng dư ax≡b(modm)ax \equiv b \pmod m, phương trình ax+by=cax + by = c: Euclid mở rộng. Luôn kiểm g∣cg \mid c trước.
  • Nghiệm “nhỏ nhất không âm” là lấy dư theo bước b/gb/g của họ nghiệm, không theo bb; viết (x % p + p) % p vì phép % của C++ giữ dấu số bị chia.
  • Tích của hai số tới 10910^9 đã cần ll; tới 101210^{12} hay 101810^{18} thì nhân bằng __int128 hoặc lấy dư trước mỗi phép nhân.

Tóm tắt

  • x=y1x = y_1, y=x1−⌊a/b⌋y1y = x_1 - \lfloor a/b \rfloor y_1; điểm dừng (1,0)(1, 0) khi b=0b = 0.
  • Họ hệ số Bézout là (x+kb/g,y−ka/g)(x + k b/g, y - k a/g).
  • Nghịch đảo của aa modulo mm tồn tại khi gcd⁡(a,m)=1\gcd(a, m) = 1 và là hệ số xx của ax+my=1ax + my = 1.
  • ax≡b(modm)ax \equiv b \pmod m có nghiệm khi g∣bg \mid b; nghiệm duy nhất theo modulo m/gm/g.
  • ax+by=cax + by = c: một nghiệm từ Bézout nhân c/gc/g, đếm nghiệm dương bằng cách đưa xx về nhỏ nhất rồi đếm số bước a/ga/g còn trừ được từ yy.

Bài tập

Đọc thêm