Một số chia 3 dư 2, chia 5 dư 3. Số đó là bao nhiêu? Thử vài số là ra 8, và cứ cộng thêm 15 thì vẫn đúng.
Định lý thặng dư Trung Hoa (CRT) làm việc đó cho mọi hệ : ghép chúng thành một điều kiện . Các mô-đun nguyên tố cùng nhau thì hệ luôn có nghiệm và là tích. Có ước chung thì hệ có thể vô nghiệm, còn có nghiệm thì là bội chung nhỏ nhất. Mọi thứ quy về ghép hai đồng dư bằng Euclid mở rộng, cẩn thận tràn số.
Nguyên tố cùng nhau
Tìm với và , . Mọi thỏa điều kiện đầu có dạng . Thay vào điều kiện sau:
Nghịch đảo tồn tại vì , lấy từ Euclid mở rộng . Với , nghiệm nằm trong , và mọi nghiệm cách nhau đúng : hai nghiệm có hiệu chia hết cho cả lẫn , nên chia hết cho .
Hình dưới ghép lần lượt: mỗi hàng là các số thỏa một đồng dư, hàng cuối là các số thử theo bước của mô-đun đã ghép.
Ví dụGhép Hai Số Dư
Đề. bộ với nguyên tố cùng nhau. In không âm nhỏ nhất thỏa , , và .
Lời giải. Mọi thỏa điều kiện đầu có dạng . Thay vào điều kiện thứ hai: . Vì , có nghịch đảo theo mô-đun , lấy từ Euclid mở rộng , nên . Lấy thì nằm trong và là nghiệm không âm nhỏ nhất.
Bước 1: Euclid mở rộng. Lời gọi con trên cho ; thay được , . Truyền y, x theo thứ tự đảo vào lời gọi con rồi sửa y là đúng hai đẳng thức đó.
ll exgcd(ll a, ll b, ll &x, ll &y) {
if (!b) {
x = 1, y = 0;
return a;
}
ll d = exgcd(b, a % b, y, x);
y -= a / b * x;
return d;
}
Bước 2: tính và . Hệ số p có thể âm, đưa về ; cũng đưa về không âm trước khi nhân. Cả hai thừa số dưới nên tích dưới ; a + n * t không quá .
exgcd(n, m, p, q);
p = (p % m + m) % m;
ll t = ((b - a) % m + m) % m * p % m;
cout << a + n * t << ' ' << n * m << '\n';
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll exgcd(ll a, ll b, ll &x, ll &y) {
if (!b) {
x = 1, y = 0;
return a;
}
ll d = exgcd(b, a % b, y, x);
y -= a / b * x;
return d;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
ll T;
cin >> T;
while (T--) {
ll a, n, b, m, p, q;
cin >> a >> n >> b >> m;
exgcd(n, m, p, q);
p = (p % m + m) % m;
ll t = ((b - a) % m + m) % m * p % m;
cout << a + n * t << ' ' << n * m << '\n';
}
}
2
2 3 3 5
1 4 3 7
8 15
17 28
Độ phức tạp: mỗi bộ .
Có ước chung
Đặt . Phương trình có nghiệm khi và chỉ khi : vế trái và cùng chia hết cho ở phần . Khi đó chia cả ba cho :
Euclid mở rộng cho , và chính là nghịch đảo của theo mô-đun . Nghiệm với , chu kỳ . Viết thay vì để khỏi tràn.
Hỏi nhanh
Hệ và có nghiệm không?
chia hết nên có nghiệm; . Chu kỳ là , không phải tích 24.
Ví dụHai Lịch Không Đồng Nguyên
Đề. Như bài trên nhưng có thể có ước chung. In no solution, hoặc cùng .
Lời giải. Như bài trên, cần , nhưng giờ có thể khác 1. Vế trái và mô-đun đều chia hết cho , nên có nghiệm khi và chỉ khi . Khi đó chia cả ba cho : , với và nguyên tố cùng nhau. Hệ số của chính là nghịch đảo của theo mô-đun . Chu kỳ là .
Bước 1: kiểm có nghiệm. g lấy luôn từ giá trị trả về của exgcd. Phép % với số âm cho kết quả âm hoặc 0, nhưng ở đây chỉ cần biết có bằng 0 hay không.
ll g = exgcd(n, m, p, q);
if ((b - a) % g) {
cout << "no solution\n";
continue;
}
Bước 2: giải theo mô-đun . Chu kỳ in bằng n / g * m: chia trước rồi mới nhân để khỏi tràn.
ll mg = m / g;
p = (p % mg + mg) % mg;
ll t = ((b - a) / g % mg + mg) % mg * p % mg;
cout << a + n * t << ' ' << n / g * m << '\n';
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll exgcd(ll a, ll b, ll &x, ll &y) {
if (!b) {
x = 1, y = 0;
return a;
}
ll d = exgcd(b, a % b, y, x);
y -= a / b * x;
return d;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
ll T;
cin >> T;
while (T--) {
ll a, n, b, m, p, q;
cin >> a >> n >> b >> m;
ll g = exgcd(n, m, p, q);
if ((b - a) % g) {
cout << "no solution\n";
continue;
}
ll mg = m / g;
p = (p % mg + mg) % mg;
ll t = ((b - a) / g % mg + mg) % mg * p % mg;
cout << a + n * t << ' ' << n / g * m << '\n';
}
}
2
2 4 6 8
1 2 0 4
6 8
no solution
Độ phức tạp: mỗi bộ .
Ghép nhiều điều kiện
Giữ một đồng dư gộp , bắt đầu từ , rồi ghép từng điều kiện vào. Sau mỗi bước là các mô-đun đã ghép và . Gặp một bước vô nghiệm thì cả hệ vô nghiệm.
Chỗ dễ sai là tràn số: khi và lớn, tích trước khi lấy dư có thể vượt . Nhân bằng __int128 (hoặc nhân Ấn Độ) ở đúng bước này. Còn thì vừa long long nếu đề bảo đảm . Nhớ đưa về trước khi ghép, nhất là khi đề cho số âm hoặc lớn hơn mô-đun.
Hai cách nhân modulo cho số lớn khác nhau rất xa về tốc độ:
__int128 nhanh hơn nhân Ấn Độ (nhân bằng cộng gấp đôi) khoảng 9 lần, vì chỉ là một phép nhân và một phép chia phần cứng. Quy tắc: ưu tiên __int128; nhân Ấn Độ chỉ khi trình biên dịch không có.
Ví dụHợp Nhất Nhiều Chu Kỳ
Đề. đồng dư , , bội chung nhỏ nhất các không quá , hệ luôn có nghiệm. In nghiệm không âm nhỏ nhất.
Lời giải. Giữ đồng dư gộp , bắt đầu từ , (mọi số nguyên đều thỏa). Ghép lần lượt từng điều kiện bằng đúng bước của bài trên, với đóng vai . Sau mỗi bước là bội chung nhỏ nhất các mô-đun đã ghép và . Đề bảo đảm có nghiệm nên không kiểm g | (b - R).
Bước 1: một bước ghép. b %= a đưa số dư về . p là nghịch đảo của theo mô-đun . Tích d * p giữa hai số tới thì vượt long long, nên chỉ bước đó nhân bằng __int128; kết quả lấy dư cho mg dưới , ép về long long được.
b %= a;
ll g = exgcd(M, a, p, q), mg = a / g;
p = (p % mg + mg) % mg;
ll d = ((b - R) / g % mg + mg) % mg;
ll t = (ll)((__int128)d * p % mg);
Bước 2: cập nhật. nên , chính là bội chung mới, không quá . Chia trước, nhân sau.
R += M * t;
M = M / g * a;
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll n, R, M;
ll exgcd(ll a, ll b, ll &x, ll &y) {
if (!b) {
x = 1, y = 0;
return a;
}
ll d = exgcd(b, a % b, y, x);
y -= a / b * x;
return d;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
R = 0, M = 1;
for (ll i = 1; i <= n; i++) {
ll a, b, p, q;
cin >> a >> b;
b %= a;
ll g = exgcd(M, a, p, q), mg = a / g;
p = (p % mg + mg) % mg;
ll d = ((b - R) / g % mg + mg) % mg;
ll t = (ll)((__int128)d * p % mg);
R += M * t;
M = M / g * a;
}
cout << R << '\n';
}
3
6 4
8 4
9 7
52
Độ phức tạp: .
Đếm trong đoạn
Ghép xong còn . Nghiệm đầu tiên không nhỏ hơn là , phép dư lấy về không âm. Số nghiệm trong là nếu , ngược lại 0.
Đề đòi số dương thì . Cấp số cộng với thì phải ít nhất là .
Hỏi nhanh
Sau khi ghép được . Có bao nhiêu số nguyên dương thỏa hệ?
Nghiệm dương nhỏ nhất là chứ không phải ; các nghiệm là , tức . Đếm từ 0 sẽ ra 6, thừa một.
Nhiều bài “hai chu kỳ trùng nhau” là CRT trá hình: lịch hai vòng, đèn nhấp nháy, tia sáng phản xạ khi trải gương ra. Khi trạng thái là một đoạn thời gian chứ không phải một thời điểm, duyệt các cặp độ lệch nhỏ và giải một CRT cho mỗi cặp.
Ví dụĐếm Nghiệm Trong Giới Hạn
Đề. test. Đếm số nguyên dương thỏa với điều kiện, .
Lời giải. Ghép các đồng dư thành như bài trên; gặp bước vô nghiệm thì đáp án là 0. Nghiệm dương nhỏ nhất là , hoặc nếu . Các nghiệm cách nhau , nên số nghiệm không quá là khi .
Bước 1: hàm ghép. Một bước ghép, trả về 0 khi vô nghiệm. Mô-đun tới 10 nên mọi tích đều nhỏ.
bool merge(ll r, ll m) {
ll p, q, g = exgcd(M, m, p, q);
if ((r - R) % g) return 0;
ll mg = m / g;
p = (p % mg + mg) % mg;
ll t = ((r - R) / g % mg + mg) % mg * p % mg;
R += M * t;
M = M / g * m;
return 1;
}
Bước 2: ghép và đếm. Đọc hết a[] rồi mới đọc b[], đúng thứ tự input. Vòng ghép dừng ngay khi một bước vô nghiệm.
R = 0, M = 1;
bool ok = 1;
for (ll i = 1; i <= k && ok; i++) ok = merge(b[i], a[i]);
ll first = R ? R : M;
if (!ok || first > N) cout << 0 << '\n';
else cout << (N - first) / M + 1 << '\n';
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll R, M, a[15], b[15];
ll exgcd(ll a, ll b, ll &x, ll &y) {
if (!b) {
x = 1, y = 0;
return a;
}
ll d = exgcd(b, a % b, y, x);
y -= a / b * x;
return d;
}
bool merge(ll r, ll m) {
ll p, q, g = exgcd(M, m, p, q);
if ((r - R) % g) return 0;
ll mg = m / g;
p = (p % mg + mg) % mg;
ll t = ((r - R) / g % mg + mg) % mg * p % mg;
R += M * t;
M = M / g * m;
return 1;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
ll T;
cin >> T;
while (T--) {
ll N, k;
cin >> N >> k;
for (ll i = 1; i <= k; i++) cin >> a[i];
for (ll i = 1; i <= k; i++) cin >> b[i];
R = 0, M = 1;
bool ok = 1;
for (ll i = 1; i <= k && ok; i++) ok = merge(b[i], a[i]);
ll first = R ? R : M;
if (!ok || first > N) cout << 0 << '\n';
else cout << (N - first) / M + 1 << '\n';
}
}
2
20 2
4 6
2 4
30 2
2 3
0 0
1
5
Độ phức tạp: mỗi test bước ghép.
Ví dụGiao Hai Cấp Số Cộng
Đề. Đếm số nguyên trong có dạng và cũng có dạng với . , các số còn lại trong .
Lời giải. với nghĩa là và ; tương tự cho dãy thứ hai. Vậy cần đếm thỏa hai đồng dư. Ghép hai đồng dư thành bằng hàm merge như bài trên, rồi đếm số phần tử của cấp số cộng đó trong đoạn.
Bước 1: ghép. có thể âm, đưa về trước. Trong merge, d * p nhân bằng __int128 vì cả hai tới .
if (!merge((b1 % a1 + a1) % a1, a1) || !merge((b2 % a2 + a2) % a2, a2)) {
cout << 0 << '\n';
return 0;
}
Bước 2: số đầu tiên trong đoạn. lo là cận dưới thật của . Số nhỏ nhất đồng dư với là , phép dư lấy về không âm. Bội chung tới , vẫn dưới giới hạn long long, nhưng cộng trừ quanh nó dễ tràn nên tính bằng __int128.
ll lo = max(L, max(b1, b2));
__int128 first = lo + (((__int128)R - lo) % M + M) % M;
if (first > Rg) cout << 0 << '\n';
else cout << (ll)((Rg - first) / M + 1) << '\n';
Cận phải đặt tên Rg để khỏi trùng với R của đồng dư.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll R, M;
ll exgcd(ll a, ll b, ll &x, ll &y) {
if (!b) {
x = 1, y = 0;
return a;
}
ll d = exgcd(b, a % b, y, x);
y -= a / b * x;
return d;
}
bool merge(ll r, ll m) {
ll p, q, g = exgcd(M, m, p, q);
if ((r - R) % g) return 0;
ll mg = m / g;
p = (p % mg + mg) % mg;
ll d = ((r - R) / g % mg + mg) % mg;
ll t = (ll)((__int128)d * p % mg);
R += M * t;
M = M / g * m;
return 1;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
ll a1, b1, a2, b2, L, Rg;
cin >> a1 >> b1 >> a2 >> b2 >> L >> Rg;
R = 0, M = 1;
if (!merge((b1 % a1 + a1) % a1, a1) || !merge((b2 % a2 + a2) % a2, a2)) {
cout << 0 << '\n';
return 0;
}
ll lo = max(L, max(b1, b2));
__int128 first = lo + (((__int128)R - lo) % M + M) % M;
if (first > Rg) cout << 0 << '\n';
else cout << (ll)((Rg - first) / M + 1) << '\n';
}
3 1 4 2 -10 30
2
Độ phức tạp: .
Khi đi thi
- Viết sẵn hàm
merge(r, m)ghép vào đồng dư gộp bằng Euclid mở rộng; dùng được cả khi có ước chung. - Đưa số dư về trước khi ghép; tính ; nhân bằng
__int128khi thừa số có thể vượt . - Nghiệm dương nhỏ nhất là khi ; đếm nghiệm trong đoạn bằng nghiệm đầu tiên rồi chia cho .
Tóm tắt
- , có nghiệm khi ; nghiệm duy nhất theo mô-đun .
- với , nghiệm .
- Nhiều đồng dư: ghép lần lượt, tăng thành .
- Bài chu kỳ trùng nhau, lịch, phản xạ: đặt đúng các đồng dư rồi ghép.
Bài tập
Đọc thêm
- Định lý thặng dư Trung HoaVNOI Wiki
- Chinese Remainder Theoremcp-algorithms
- 中国剩余定理OI Wiki
- Extended Euclidean AlgorithmUSACO Guide