Thuật toán Euclid mở rộng
Thuật toán Euclid tìm ước chung lớn nhất chỉ trong phép chia, thay vì thử mọi ước trong . Bản mở rộng của nó còn trả về hai số nguyên thỏa . Từ một cặp đó ta giải được cả một họ bài toán: nghịch đảo modulo khi modulo không nguyên tố, phương trình nghiệm nguyên (đếm nghiệm, tìm nghiệm không âm nhỏ nhất), phương trình đồng dư và định lý số dư Trung Hoa (CRT) cho hai modulo bất kỳ.
Ý tưởng / Trực giác
Euclid: vì sao
Viết với . Số nào chia hết cả và thì chia hết ; ngược lại chia hết và thì chia hết . Vậy hai cặp và có cùng tập ước chung, nên cùng ước chung lớn nhất. Lặp lại đến khi số thứ hai bằng : .
Vì sao chỉ cần bước
Xét cặp với . Sau hai bước, số lớn bị thay bằng , và số này luôn nhỏ hơn :
- nếu thì ;
- nếu thì .
Cứ hai bước số lớn giảm ít nhất một nửa, nên số bước không quá khoảng . Chặn chính xác hơn là định lý Lamé: trường hợp xấu nhất là hai số Fibonacci liên tiếp (mỗi bước thương đều bằng ). Vì , với mọi cặp số long long thuật toán dừng sau không quá khoảng phép chia.
Euclid mở rộng: truy ngược nghiệm
Giả sử đã có nghiệm cho cặp nhỏ hơn: . Thay :
Đáy đệ quy là với nghiệm hiển nhiên . Mỗi tầng chỉ tốn , nên toàn bộ vẫn . Ngoài ra, với , các hệ số trả về luôn nhỏ: và , không bao giờ tràn long long.
Bản lặp dựa trên một bất biến: luôn giữ hai "dòng" thỏa , bắt đầu từ và . Mỗi bước lấy dòng trên trừ lần dòng dưới (đúng phép chia của Euclid trên cột ); vì phép trừ tuyến tính nên bất biến vẫn đúng. Khi cột về , dòng còn lại cho cùng cần tìm.
Định lý Bézout
Với không đồng thời bằng , tập giá trị đúng bằng tập mọi bội của .
Một chiều hiển nhiên: và nên . Chiều còn lại: Euclid mở rộng tạo ra nên nhân với là tạo được . Hệ quả dùng liên tục:
- có nghiệm nguyên khi và chỉ khi .
- có nghịch đảo modulo khi và chỉ khi : khi đó cho , tức .
- Với hai bước nhảy và , các vị trí đạt được (nếu không bị giới hạn biên) là đúng các bội của .
Họ nghiệm tổng quát của
Nếu là một nghiệm thì mọi nghiệm có dạng
Lý do: từ , chia hai vế cho được . Vì và nguyên tố cùng nhau nên . Nói cách khác: các nghiệm nằm cách đều nhau, nhảy bước còn nhảy ngược bước . Mọi câu hỏi "nghiệm trong khoảng", "nghiệm không âm nhỏ nhất" đều quy về chọn .
Phương trình đồng dư và CRT
- tương đương . Có nghiệm khi , và khi đó có đúng nghiệm trong , cách đều nhau .
- Hệ , : đặt , điều kiện thứ hai thành , chính là một phương trình đồng dư. Có nghiệm khi , và nghiệm duy nhất theo modulo (không phải khi hai modulo không nguyên tố cùng nhau).
Ví dụ chạy tay
Euclid mở rộng với ,
Chiều đi xuống là Euclid thường; chiều đi lên dùng công thức , (với của chính dòng đó):
đi xuống (Euclid) đi lên (truy ngược, đọc từ dưới lên)
a b q a mod b x y kiểm tra a*x + b*y
---- ---- ---- ------- ---- ---- -------------------
99 78 1 21 -11 14 99*(-11) + 78*14 = 3
78 21 3 15 3 -11 78*3 + 21*(-11) = 3
21 15 1 6 -2 3 21*(-2)+ 15*3 = 3
15 6 2 3 1 -2 15*1 + 6*(-2) = 3
6 3 2 0 0 1 6*0 + 3*1 = 3
3 0 - - 1 0 3*1 + 0*0 = 3 <- đáy
Ví dụ dòng với : dòng dưới cho , nên , . Kết quả: .
Phương trình với
và nên có nghiệm. Nhân nghiệm trên với được , . Bước nhảy: tăng , giảm . Rút gọn về : , kéo theo . Họ nghiệm trên trục :
k : -1 0 1 2
x : -4 22 48 74 x = 22 + 26k
y : 82 49 16 -17 y = 49 - 33k
[----------]
x>=0 và y>=0
Có đúng 2 nghiệm không âm: và . Nghiệm có không âm nhỏ nhất là ; nghiệm có nhỏ nhất là , vì mỗi bước tăng thì thay đổi .
CRT với modulo không nguyên tố cùng nhau
Giải , :
gcd(6, 10) = 2, 6*2 + 10*(-1) = 2 -> p = 2
r2 - r1 = 6 chia hết cho 2 -> có nghiệm
cần 6t ≡ 6 (mod 10) <=> 3t ≡ 3 (mod 5)
t = ((r2-r1)/g) * p mod (m2/g) = (6/2)*2 mod 5 = 1
x = 2 + 6*1 = 8, lcm(6,10) = 30 -> x ≡ 8 (mod 30)
Kiểm tra: và . Còn hệ , thì vô nghiệm vì (vế đầu buộc lẻ, vế sau buộc chẵn).
Cài đặt
C++
Toàn bộ thư viện dưới đây đã được kiểm thử ngẫu nhiên với bản vét cạn. __int128 dùng ở những chỗ tích hai số cỡ có thể tràn.
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef __int128 lll;
// Classic Euclid
ll gcd_iter(ll a, ll b) {
while (b != 0) {
ll r = a % b;
a = b;
b = r;
}
return a;
}
// Recursive extended Euclid: returns g = gcd(a, b) and sets a*x + b*y = g
ll ext_gcd(ll a, ll b, ll &x, ll &y) {
if (b == 0) { x = 1; y = 0; return a; }
ll x1, y1;
ll g = ext_gcd(b, a % b, x1, y1);
// b*x1 + (a % b)*y1 = g and a % b = a - (a / b) * b
x = y1;
y = x1 - (a / b) * y1;
return g;
}
// Iterative version, invariants: a0*x + b0*y = a and a0*x1 + b0*y1 = b
ll ext_gcd_iter(ll a, ll b, ll &x, ll &y) {
ll x1 = 0, y1 = 1;
x = 1; y = 0;
while (b != 0) {
ll q = a / b;
tie(x, x1) = make_tuple(x1, x - q * x1);
tie(y, y1) = make_tuple(y1, y - q * y1);
tie(a, b) = make_tuple(b, a - q * b);
}
return a;
}
// Inverse of a modulo m (m need not be prime); -1 if gcd(a, m) != 1
ll inv_mod(ll a, ll m) {
ll x, y;
ll g = ext_gcd(((a % m) + m) % m, m, x, y);
if (g != 1) return -1;
return ((x % m) + m) % m;
}
// One solution of a*x + b*y = c (a, b != 0) with 0 <= x < |b|/g; false if none
bool find_any_solution(ll a, ll b, ll c, ll &x, ll &y, ll &g) {
ll x1, y1;
g = ext_gcd(llabs(a), llabs(b), x1, y1);
if (c % g != 0) return false;
if (a < 0) x1 = -x1;
ll step = llabs(b) / g;
// reduce before multiplying: x1 * (c / g) alone may overflow
lll t = (lll)(x1 % step) * ((c / g) % step) % step;
x = (ll)((t + step) % step);
y = (ll)(((lll)c - (lll)a * x) / b); // exact division
return true;
}
// Floor / ceil division that are correct for negative numbers
ll floor_div(ll p, ll q) { return p / q - ((p % q != 0) && ((p < 0) != (q < 0))); }
ll ceil_div(ll p, ll q) { return p / q + ((p % q != 0) && ((p < 0) == (q < 0))); }
// Number of solutions of a*x + b*y = c (a, b != 0) in the given box
ll count_solutions(ll a, ll b, ll c, ll minx, ll maxx, ll miny, ll maxy) {
ll x0, y0, g;
if (!find_any_solution(a, b, c, x0, y0, g)) return 0;
ll dx = b / g, dy = a / g; // x = x0 + k*dx, y = y0 - k*dy
ll kl1, kr1, kl2, kr2;
if (dx > 0) { kl1 = ceil_div(minx - x0, dx); kr1 = floor_div(maxx - x0, dx); }
else { kl1 = ceil_div(maxx - x0, dx); kr1 = floor_div(minx - x0, dx); }
if (dy > 0) { kl2 = ceil_div(y0 - maxy, dy); kr2 = floor_div(y0 - miny, dy); }
else { kl2 = ceil_div(y0 - miny, dy); kr2 = floor_div(y0 - maxy, dy); }
ll lo = max(kl1, kl2), hi = min(kr1, kr2);
return hi >= lo ? hi - lo + 1 : 0;
}
// a*x = c (mod m): returns g = number of solutions in [0, m) (0 if none);
// solutions are x0, x0 + step, ..., x0 + (g-1)*step
ll solve_congruence(ll a, ll c, ll m, ll &x0, ll &step) {
a = ((a % m) + m) % m; c = ((c % m) + m) % m;
ll x, y;
ll g = ext_gcd(a, m, x, y);
if (c % g != 0) return 0;
step = m / g;
x0 = (ll)((lll)(((x % step) + step) % step) * (c / g) % step);
return g;
}
// x = r1 (mod m1), x = r2 (mod m2), moduli need not be coprime.
// Returns {r, lcm(m1, m2)} or {-1, -1} if there is no solution.
pair<ll, ll> crt(ll r1, ll m1, ll r2, ll m2) {
ll p, q;
ll g = ext_gcd(m1, m2, p, q); // m1*p + m2*q = g
if ((r2 - r1) % g != 0) return {-1, -1};
ll m2g = m2 / g;
ll t = (ll)((lll)((r2 - r1) / g % m2g) * (p % m2g) % m2g);
if (t < 0) t += m2g;
ll lcm = m1 * m2g; // assumed to fit in long long
ll r = (ll)(((lll)r1 + (lll)m1 * t) % lcm);
if (r < 0) r += lcm;
return {r, lcm};
}
int main() {
ll x, y, g;
g = ext_gcd(99, 78, x, y);
printf("gcd=%lld x=%lld y=%lld\n", g, x, y); // 3 -11 14
printf("inv(7, 30)=%lld\n", inv_mod(7, 30)); // 13
printf("inv(6, 30)=%lld\n", inv_mod(6, 30)); // -1
find_any_solution(99, 78, 6000, x, y, g);
printf("99x+78y=6000: x=%lld y=%lld\n", x, y); // 22 49
printf("non-negative: %lld\n",
count_solutions(99, 78, 6000, 0, 6000, 0, 6000)); // 2
ll x0, st;
printf("6x=4 (mod 10): %lld solutions", solve_congruence(6, 4, 10, x0, st));
printf(", x0=%lld step=%lld\n", x0, st); // 2, 4, 5
auto [r, l] = crt(2, 6, 8, 10);
printf("crt: x = %lld (mod %lld)\n", r, l); // 8 30
}
Một số ghi chú khi dùng:
- Nghiệm không âm nhỏ nhất của (với ):
find_any_solutionđã trả ; chỉ cần kiểm tra thêm , tức . Nếu thì không có nghiệm không âm nào, vì tăng chỉ làm nhỏ đi. - Nghiệm không âm có nhỏ nhất: biến thiên tuyến tính theo , nên tối ưu nằm ở một đầu của khoảng hợp lệ. Với , dồn càng nhiều vào biến đi với hệ số lớn () càng tốt, tức lấy nhỏ nhất.
- CRT nhiều phương trình: gộp dần từng cặp
(r, m) = crt(r, m, ri, mi), bắt đầu từ(0, 1); dừng ngay khi gặp-1.
Python
Python có số nguyên không giới hạn nên không lo tràn. Từ Python 3.8, pow(a, -1, m) trả nghịch đảo modulo với bất kỳ (ném ValueError nếu ).
def ext_gcd(a, b):
"""Return (g, x, y) with a*x + b*y = g = gcd(a, b)."""
x0, y0, x1, y1 = 1, 0, 0, 1
while b:
q = a // b
a, b = b, a - q * b
x0, x1 = x1, x0 - q * x1
y0, y1 = y1, y0 - q * y1
return a, x0, y0
def crt(r1, m1, r2, m2):
"""Solve x = r1 (mod m1), x = r2 (mod m2). Return (r, lcm) or None."""
g, p, _ = ext_gcd(m1, m2)
if (r2 - r1) % g:
return None
lcm = m1 // g * m2
t = (r2 - r1) // g * p % (m2 // g)
return (r1 + m1 * t) % lcm, lcm
print(ext_gcd(99, 78)) # (3, -11, 14)
print(crt(2, 6, 8, 10)) # (8, 30)
print(pow(7, -1, 30)) # 13
Độ phức tạp
| Thao tác | Thời gian | Bộ nhớ | Vì sao |
|---|---|---|---|
gcd_iter, ext_gcd_iter |
cứ hai bước số lớn giảm ít nhất một nửa | ||
ext_gcd đệ quy |
cùng số bước, mỗi bước một khung đệ quy (tối đa khoảng ) | ||
inv_mod, find_any_solution |
một lần Euclid mở rộng cộng phép tính | ||
count_solutions |
tìm một nghiệm rồi giao hai khoảng của , không duyệt nghiệm | ||
solve_congruence |
một lần Euclid mở rộng; liệt kê hết nghiệm thì thêm | ||
crt hai phương trình |
một lần Euclid mở rộng | ||
crt gộp phương trình |
lần gộp, là modulo lớn nhất |
Cột bộ nhớ của các hàm dùng ext_gcd là vì đó là độ sâu đệ quy (tối đa khoảng khung, không đáng kể); thay ext_gcd bằng ext_gcd_iter thì mọi hàm đều chỉ tốn bộ nhớ.
Điểm mạnh nằm ở count_solutions: số nghiệm trong khoảng có thể tới nhưng ta chỉ tính hai đầu mút của nên vẫn là .
⚠️ Lỗi thường gặp
- Tràn số khi nhân nghiệm với . có thể cỡ , nhân tiếp là tràn cả
long long. Triệu chứng: sai ở test lớn dù test nhỏ đúng. Cách tránh: rút gọn modulo trước khi nhân, và nhân bằng__int128(nhưfind_any_solution). Trong CRT cũng có hai chỗ như vậy: tích((r2-r1)/g % m2g) * (p % m2g)có hai thừa số cỡ , nên có thể tới ; và phép cộng có thể vượt dù mỗi số hạng đều vừa (vì nên riêng luôn vừalong longkhi lcm vừa). Hàmcrtở trên tính cả hai bằng__int128. - Modulo âm trong C++.
x % mmang dấu củax, nên-5 % 3 == -2. Nghiệm từ Euclid mở rộng rất hay âm; luôn chuẩn hóa((x % m) + m) % mtrước khi dùng làm đáp án hay chỉ số mảng. - Quên chia cho . Bước nhảy nghiệm là , không phải ; modulo sau khi giải là , không phải ; CRT cho nghiệm theo , không phải . Dùng nhầm sẽ bỏ sót nghiệm hoặc đếm sai.
- Dùng nghịch đảo Fermat khi không nguyên tố. Công thức đó chỉ đúng với nguyên tố. Với hợp số (ví dụ , ) phải dùng
inv_modvà kiểm tra ; nếu không thì nghịch đảo không tồn tại, không được trả bừa một số. - Chia làm tròn sai với số âm. Khi tìm khoảng , phép
/của C++ làm tròn về , không phải làm tròn xuống. Triệu chứng: đếm lệch nghiệm khi âm. Cách tránh: dùngfloor_div/ceil_divriêng, và nhớ đảo chiều bất đẳng thức khi bước nhảy âm. - Áp CRT "nguyên tố cùng nhau" cho modulo bất kỳ. Công thức cần các modulo đôi một nguyên tố cùng nhau; với , thì không tồn tại. Phải kiểm tra rồi gộp theo như hàm
crtở trên. - vượt quá khi gộp nhiều phương trình. Tích các modulo tăng rất nhanh. Nếu đề chỉ hỏi nghiệm nhỏ nhất một giới hạn , có thể dừng sớm khi ; nếu cần giá trị chính xác thì dùng số lớn hoặc Python.
- Euclid bằng phép trừ. Viết
while (a != b) if (a > b) a -= b; else b -= a;trông giống Euclid nhưng là : chạy vòng. Luôn dùng phép chia lấy dư. - Hệ số âm hoặc bằng . Gọi
ext_gcdvới số âm có thể trả âm; gọi với thì và mọi phép chia cho đều lỗi. Hãy đưa về rồi sửa dấu (nhưfind_any_solution), và xử lý riêng trường hợp một hệ số bằng .
Biến thể / Mở rộng
- Nghịch đảo của cả dãy modulo nguyên tố tính được trong bằng truy hồi ; bản thân truy hồi này là một bước của Euclid.
- CRT cho modulo hợp số trong tổ hợp: tính theo từng lũy thừa nguyên tố rồi ghép bằng CRT, xem Định lý Lucas.
- Logarithm rời rạc khi : bước rút gọn trước khi chạy Baby-step Giant-step cần giải đồng dư tuyến tính và nghịch đảo modulo hợp số.
- Bài toán đồng xu (Frobenius) hai loại: với , số lớn nhất không biểu diễn được dạng với là , và có đúng số như vậy.
- Tổng quan các công cụ số học khác (sàng, lũy thừa nhanh, phân tích thừa số) ở trang Toán học.
Bài tập luyện
Pool hiện chỉ có một bài gắn tag extended_euclid (bài VOI bên dưới), nên phần luyện kỹ thuật chọn các bài mà lời giải thực sự dựa vào Euclid, Bézout, phương trình nghiệm nguyên hoặc CRT.
Luyện kỹ thuật
- Phep Tinh GCD (gcdspell) (Học việc) khởi động: cài Euclid cho , chú ý kiểu
long longvà dùng phép chia lấy dư thay cho phép trừ. - Hàng rào vô hạn (inffence) (Nâng cao) chia cho để hai số nguyên tố cùng nhau, rồi dùng Bézout để thấy tấm đỏ có thể nằm cách tấm xanh đúng đơn vị (theo độ dài đã rút gọn); từ đó tính số tấm cùng màu liên tiếp nhiều nhất.
- Trò chơi đồng dư (remgm42) (Nâng cao) theo CRT, biết mọi tương đương biết và không hơn; câu hỏi quy về kiểm tra mà không để lcm tràn số.
- Ngày may mắn (luckydays) (Kỳ cựu) theo Bézout, độ lệch giữa đầu hai đoạn may mắn chỉ có thể thay đổi theo bội của ; thử hai độ lệch gần nhất rồi lấy giao đoạn.
- Phân số (fractions) (Kỳ cựu) tách với hai thừa số nguyên tố cùng nhau, rồi giải phương trình nghiệm nguyên với ràng buộc , .
Đề VOI
- Sản xuất gỗ (voi24_wpro) (Kỳ cựu) VOI 2024: sau khi đưa về quy hoạch động trên các mối nối ban đầu, bài toán con là tìm nghiệm không âm của sao cho nhỏ nhất; để ý số lần phải giải bài toán con này trước khi chọn cách cài đặt.
Bình luận