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 , và 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 bằng cách thay bởi . Đi ngược các bước đó còn cho thêm hai số nguyên với , 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ư , phương trình cùng cả họ nghiệm của nó.
Hệ số Bézout
Giả sử đã có với , trong đó , . Thay vào: , tức . Vậy , . Điểm dừng là : .
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, . Các hệ số trả về thỏa và , nên không tràn khi vừa ll. Mọi cặp hệ số khác có dạng với nguyên: hiệu của hai nghiệm phải thỏa , và nguyên tố cùng nhau.
Hỏi nhanh
Đã biết . Cặp nào dưới đây cũng là hệ số Bézout của ?
Mọi nghiệm là vì và . Với được : . cho , gấp đôi .
Ví dụHệ số Bézout tối ưu
Đề. Cho . In hệ số Bézout của với nhỏ nhất; hòa thì ưu tiên , rồi nhỏ hơn.
Lời giải. Euclid mở rộng cho một nghiệm ; mọi nghiệm khác là . Nghiệm của Euclid đã có và , nên nghiệm tối ưu nằm trong vài nghiệm lân cận, từ tới .
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 , hòa thì so cặp (x > y, x): ưu tiên trước, nhỏ sau.
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;
}
#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
1 -2
.
Nghịch đảo modulo
Nghịch đảo của modulo là với , tức có nghiệm: tồn tại khi và chỉ khi , và khi đó Euclid mở rộng cho ngay. Đưa về bằng (x % m + m) % m. Khác với lũy thừa (định lý Fermat), cách này đúng với mọi modulo, kể cả hợp số, và chỉ tốn .
Khi cần nghịch đảo của cả dãy theo modulo nguyên tố , có cách nhanh hơn cả hai. Viết , lấy theo modulo rồi nhân hai vế với :
mà đã có nghịch đảo từ trước, nên mỗi số chỉ tốn một phép nhân:
inv[1] = 1;
for (ll i = 2; i <= n; i++) inv[i] = (MOD - MOD / i) * inv[MOD % i] % MOD;
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ư là phương trình . Gọi : vế trái luôn chia hết cho , nên vô nghiệm nếu . Ngược lại chia cả ba số cho : với , nghiệm duy nhất theo modulo là . Theo modulo ban đầu có đúng nghiệm, cách nhau . Phép nhân có thể tới , nên lấy dư trước khi nhân hoặc dùng __int128.
Hỏi nhanh
Đồng dư có bao nhiêu nghiệm trong ?
chia hết 2, nên chia cả ba cho 2: , nghiệm . Trong có hai nghiệm, và . Không có nghịch đảo của 4 modulo 6 chỉ có nghĩa là không chia thẳng được, không có nghĩa là vô nghiệm.
Ví dụĐồng dư tuyến tính
Đề. bộ . Với mỗi bộ in một nghiệm của , hoặc impossible.
Lời giải. Với : thì vô nghiệm. Ngược lại chia cả ba cho , được với nguyên tố cùng nhau với , nên . Nghiệm này nằm trong , nhỏ nhất; các nghiệm khác hơn nó một bội của .
Bước 1: nghịch đảo. ext(a, m, x, y) cho ; chia cho thấy chính là nghịch đảo của theo mô-đun .
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 (tới ), tích tới , sát giới hạn long long, nên ép sang __int128 cho chắc.
ll mm = m / g;
ll r = (__int128)(x % mm + mm) % mm * (b / g % mm) % mm;
#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
2
impossible
2
Mỗi bộ .
Ví dụĐường đến ngai vàng
Đề. Vòng tròn vị trí, đang ở , mỗi bước tiến . In số bước nhỏ nhất (kể cả 0) để tới vị trí 0, hoặc .
Lời giải. Sau bước đang ở . Cần , tức : đồng dư tuyến tính của ví dụ trước với . 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 .
Bước 1: giải đồng dư. Ở đây , tích hai số dưới vừa long long, không cần __int128.
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';
cho , chia hết cho và , nên in 0 đúng như mong muốn.
#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
2
-1
0
Mỗi bộ .
Họ nghiệm
Phương trình có nghiệm nguyên khi và chỉ khi chia hết . Khi đó nhân hệ số Bézout với được một nghiệm , và mọi nghiệm là
Khi , tăng thì giảm. Muốn nghiệm có dương nhỏ nhất: lấy modulo rồi đưa về , tính từ phương trình. Nếu khi đó vẫn dương thì có nghiệm dương, và số nghiệm dương là số bước còn trừ được từ 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.
Ví dụĐiểm nguyên trên đường thẳng
Đề. Tìm một điểm nguyên trên đường với , hoặc in .
Lời giải. Cần . Giải với giá trị tuyệt đối để ext chạy trên số không âm: . Nếu thì vô nghiệm.
Bước 1: nhân và đổi dấu. Nhân hai hệ số với được một nghiệm của ; đổi dấu khi , khi để về đúng phương trình gốc. , nhân với , tích dưới , vừa long long.
x *= -C / g;
y *= -C / g;
if (A < 0) x = -x;
if (B < 0) y = -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
-1 1
.
Ví dụMẫu Phương Trình Nghiệm Nguyên
Đề. phương trình với . Không có nghiệm nguyên thì in ; có nghiệm nhưng không có nghiệm dương thì in dương nhỏ nhất và 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, nhỏ nhất, nhỏ nhất, lớn nhất, lớn nhất.
Lời giải. Đặt , : mỗi bước tăng thêm thì giảm . Lấy nghiệm có dương nhỏ nhất; ứng với nó lớn nhất. Từ đó đếm được còn bao nhiêu bước mà vẫn dương.
Bước 1: dương nhỏ nhất. Nhân với theo mô-đun , rồi đưa về . Tích tới , vẫn vừa long long.
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ì thuộc họ nghiệm.
Bước 2: có nghiệm dương. thì số nghiệm dương là số lần còn giảm đi mà vẫn , cộng chính nghiệm hiện tại: . Nghiệm cuối cho nhỏ nhất và lớn nhất.
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; dương nhỏ nhất (của một nghiệm khác) là đưa về .
ll ym = (y % q + q) % q;
if (!ym) ym = q;
cout << x << ' ' << ym << '\n';
#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
1 3 2 3 2
-1
1 2 2 2 2
1 1 3 1 3
Mỗi phương trình .
Khi đi thi
- Nghịch đảo modulo hợp số, đồng dư , phương trình : Euclid mở rộng. Luôn kiểm trước.
- Nghiệm “nhỏ nhất không âm” là lấy dư theo bước của họ nghiệm, không theo ; viết
(x % p + p) % pvì phép%của C++ giữ dấu số bị chia. - Tích của hai số tới đã cần
ll; tới hay thì nhân bằng__int128hoặc lấy dư trước mỗi phép nhân.
Tóm tắt
- , ; điểm dừng khi .
- Họ hệ số Bézout là .
- Nghịch đảo của modulo tồn tại khi và là hệ số của .
- có nghiệm khi ; nghiệm duy nhất theo modulo .
- : một nghiệm từ Bézout nhân , đếm nghiệm dương bằng cách đưa về nhỏ nhất rồi đếm số bước còn trừ được từ .
Bài tập
Đọc thêm
- Thuật toán EuclidVNOI Wiki
- Nghịch đảo moduloVNOI Wiki
- Extended Euclidean Algorithmcp-algorithms
- Linear Diophantine Equationscp-algorithms