Chương 5·Bài 54

Chinese Remainder Theorem

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ệ x≡ri(modmi)x \equiv r_i \pmod{m_i}: ghép chúng thành một điều kiện x≡R(modM)x \equiv R \pmod M. Các mô-đun nguyên tố cùng nhau thì hệ luôn có nghiệm và MM là tích. Có ước chung thì hệ có thể vô nghiệm, còn có nghiệm thì MM 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 xx với x≡a(modn)x \equiv a \pmod n và x≡b(modm)x \equiv b \pmod m, gcd⁡(n,m)=1\gcd(n, m) = 1. Mọi xx thỏa điều kiện đầu có dạng x=a+ntx = a + n t. Thay vào điều kiện sau:

a+nt≡b(modm)  ⟺  t≡(b−a)⋅n−1(modm).a + n t \equiv b \pmod m \iff t \equiv (b - a) \cdot n^{-1} \pmod m.

Nghịch đảo n−1 mod mn^{-1} \bmod m tồn tại vì gcd⁡(n,m)=1\gcd(n, m) = 1, lấy từ Euclid mở rộng np+mq=1n p + m q = 1. Với t∈[0,m)t \in [0, m), nghiệm x=a+ntx = a + n t nằm trong [0,nm)[0, nm), và mọi nghiệm cách nhau đúng nmnm: hai nghiệm có hiệu chia hết cho cả nn lẫn mm, nên chia hết cho nmnm.

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.

Ghép từng đồng dư vào đồng dư gộp.

Ví dụGhép Hai Số Dư

Đề. T≤1000T \le 1000 bộ a,n,b,ma, n, b, m với n,m≤109n, m \le 10^9 nguyên tố cùng nhau. In xx không âm nhỏ nhất thỏa x mod n=ax \bmod n = a, x mod m=bx \bmod m = b, và K=nmK = nm.

Lời giải. Mọi xx thỏa điều kiện đầu có dạng x=a+ntx = a + n t. Thay vào điều kiện thứ hai: nt≡b−a(modm)n t \equiv b - a \pmod m. Vì gcd⁡(n,m)=1\gcd(n, m) = 1, nn có nghịch đảo pp theo mô-đun mm, lấy từ Euclid mở rộng np+mq=1n p + m q = 1, nên t≡(b−a)⋅p(modm)t \equiv (b - a) \cdot p \pmod m. Lấy t∈[0,m)t \in [0, m) thì x=a+ntx = a + n t nằm trong [0,nm)[0, nm) và là nghiệm không âm nhỏ nhất.

Bước 1: Euclid mở rộng. Lời gọi con trên (b,a mod b)(b, a \bmod b) cho bx′+(a mod b)y′=db x' + (a \bmod b) y' = d; thay a mod b=a−⌊a/b⌋ba \bmod b = a - \lfloor a/b \rfloor b được x=y′x = y', y=x′−⌊a/b⌋y′y = x' - \lfloor a/b \rfloor y'. 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 đó.

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 tt và xx. Hệ số p có thể âm, đưa về [0,m)[0, m); b−ab - a cũng đưa về không âm trước khi nhân. Cả hai thừa số dưới 10910^9 nên tích dưới 101810^{18}; a + n * t không quá (n−1)+n(m−1)<nm(n - 1) + n(m - 1) < nm.

C++
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';
Code đầy đủ
#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

Độ phức tạp: mỗi bộ O(log⁡m)O(\log m).

Có ước chung

Đặt g=gcd⁡(n,m)g = \gcd(n, m). Phương trình a+nt≡b(modm)a + n t \equiv b \pmod m có nghiệm khi và chỉ khi g∣(b−a)g \mid (b - a): vế trái và mm cùng chia hết cho gg ở phần ntn t. Khi đó chia cả ba cho gg:

t≡b−ag⋅(ng)−1(modmg).t \equiv \frac{b - a}{g} \cdot \left(\frac{n}{g}\right)^{-1} \pmod{\frac{m}{g}}.

Euclid mở rộng cho np+mq=gn p + m q = g, và pp chính là nghịch đảo của n/gn/g theo mô-đun m/gm/g. Nghiệm x=a+ntx = a + n t với t∈[0,m/g)t \in [0, m/g), chu kỳ K=ng⋅m=lcm⁡(n,m)K = \frac{n}{g} \cdot m = \operatorname{lcm}(n, m). Viết n/g⋅mn / g \cdot m thay vì n⋅m/gn \cdot m / g để khỏi tràn.

Hỏi nhanh

Hệ x≡1(mod4)x \equiv 1 \pmod 4 và x≡3(mod6)x \equiv 3 \pmod 6 có nghiệm không?

Ví dụHai Lịch Không Đồng Nguyên

Đề. Như bài trên nhưng n,m≤109n, m \le 10^9 có thể có ước chung. In no solution, hoặc x∈[0,K)x \in [0, K) cùng K=lcm⁡(n,m)K = \operatorname{lcm}(n, m).

Lời giải. Như bài trên, cần nt≡b−a(modm)n t \equiv b - a \pmod m, nhưng giờ g=gcd⁡(n,m)g = \gcd(n, m) có thể khác 1. Vế trái và mô-đun đều chia hết cho gg, nên có nghiệm khi và chỉ khi g∣(b−a)g \mid (b - a). Khi đó chia cả ba cho gg: ngt≡b−ag(modmg)\frac{n}{g} t \equiv \frac{b - a}{g} \pmod{\frac{m}{g}}, với ng\frac{n}{g} và mg\frac{m}{g} nguyên tố cùng nhau. Hệ số pp của np+mq=gn p + m q = g chính là nghịch đảo của ng\frac{n}{g} theo mô-đun mg\frac{m}{g}. Chu kỳ là lcm⁡(n,m)=ng⋅m\operatorname{lcm}(n, m) = \frac{n}{g} \cdot m.

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.

C++
ll g = exgcd(n, m, p, q);
if ((b - a) % g) {
    cout << "no solution\n";
    continue;
}

Bước 2: giải theo mô-đun m/gm/g. Chu kỳ in bằng n / g * m: chia trước rồi mới nhân để khỏi tràn.

C++
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';
Code đầy đủ
#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

Độ phức tạp: mỗi bộ O(log⁡m)O(\log m).

Ghép nhiều điều kiện

Giữ một đồng dư gộp x≡R(modM)x \equiv R \pmod M, bắt đầu từ R=0,M=1R = 0, M = 1, rồi ghép từng điều kiện vào. Sau mỗi bước MM là lcm⁡\operatorname{lcm} các mô-đun đã ghép và 0≤R<M0 \le R < M. Gặp một bước vô nghiệm thì cả hệ vô nghiệm.

Chỗ dễ sai là tràn số: khi MM và mim_i lớn, tích b−Rg⋅p\frac{b - R}{g} \cdot p trước khi lấy dư có thể vượt 101810^{18}. Nhân bằng __int128 (hoặc nhân Ấn Độ) ở đúng bước này. Còn R+Mt<lcm⁡R + M t < \operatorname{lcm} thì vừa long long nếu đề bảo đảm lcm⁡≤1018\operatorname{lcm} \le 10^{18}. Nhớ đưa rir_i về [0,mi)[0, m_i) 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 độ:

Một triệu phép nhân modulo với mô-đun gần một tỉ tỉ, trung vị 5 lần chạy, i7-10850H, g++ -O2.__int12825 ms · gấp 5,7 lầnlong double4,4 msnhân Ấn Độ227 ms · gấp 52 lần
Một triệu phép nhân modulo với mô-đun gần một tỉ tỉ, ba cách làm, đo trên i7-10850H, g++ -O2.

__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ỳ

Đề. n≤105n \le 10^5 đồng dư x≡bi(modai)x \equiv b_i \pmod{a_i}, ai,bi≤1012a_i, b_i \le 10^{12}, bội chung nhỏ nhất các aia_i không quá 101810^{18}, hệ luôn có nghiệm. In nghiệm không âm nhỏ nhất.

Lời giải. Giữ đồng dư gộp x≡R(modM)x \equiv R \pmod M, bắt đầu từ R=0R = 0, M=1M = 1 (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 (R,M)(R, M) đóng vai (a,n)(a, n). Sau mỗi bước MM là bội chung nhỏ nhất các mô-đun đã ghép và 0≤R<M0 \le R < M. Đề 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ề [0,a)[0, a). p là nghịch đảo của M/gM/g theo mô-đun a/ga/g. Tích d * p giữa hai số tới 101210^{12} 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 101210^{12}, ép về long long được.

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. t<a/gt < a/g nên R+Mt<M⋅a/gR + M t < M \cdot a / g, chính là bội chung mới, không quá 101810^{18}. Chia trước, nhân sau.

C++
R += M * t;
M = M / g * a;
Code đầy đủ
#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

Độ phức tạp: O(nlog⁡a)O(n \log a).

Đếm trong đoạn

Ghép xong còn x≡R(modM)x \equiv R \pmod M. Nghiệm đầu tiên không nhỏ hơn lolo là f=lo+((R−lo) mod M)f = lo + ((R - lo) \bmod M), phép dư lấy về không âm. Số nghiệm trong [lo,hi][lo, hi] là ⌊(hi−f)/M⌋+1\lfloor (hi - f)/M \rfloor + 1 nếu f≤hif \le hi, ngược lại 0.

Đề đòi số dương thì lo=1lo = 1. Cấp số cộng at+ba t + b với t≥0t \ge 0 thì lolo phải ít nhất là bb.

Hỏi nhanh

Sau khi ghép được x≡0(mod6)x \equiv 0 \pmod 6. Có bao nhiêu số nguyên dương x≤30x \le 30 thỏa hệ?

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

Đề. T≤100T \le 100 test. Đếm số nguyên dương X≤N≤109X \le N \le 10^9 thỏa X mod ai=biX \bmod a_i = b_i với M≤10M \le 10 điều kiện, ai≤10a_i \le 10.

Lời giải. Ghép các đồng dư thành X≡R(modM)X \equiv R \pmod M 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à RR, hoặc MM nếu R=0R = 0. Các nghiệm cách nhau MM, nên số nghiệm không quá NN là ⌊(N−first)/M⌋+1\lfloor (N - \text{first}) / M \rfloor + 1 khi first≤N\text{first} \le N.

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ỏ.

C++
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.

C++
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';
Code đầy đủ
#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

Độ phức tạp: mỗi test O(M)O(M) bước ghép.

Ví dụGiao Hai Cấp Số Cộng

Đề. Đếm số nguyên trong [L,R][L, R] có dạng a1t+b1a_1 t + b_1 và cũng có dạng a2u+b2a_2 u + b_2 với t,u≥0t, u \ge 0. ai≤2⋅109a_i \le 2 \cdot 10^9, các số còn lại trong [−2⋅109,2⋅109][-2 \cdot 10^9, 2 \cdot 10^9].

Lời giải. x=a1t+b1x = a_1 t + b_1 với t≥0t \ge 0 nghĩa là x≡b1(moda1)x \equiv b_1 \pmod{a_1} và x≥b1x \ge b_1; tương tự cho dãy thứ hai. Vậy cần đếm x∈[max⁡(L,b1,b2),R]x \in [\max(L, b_1, b_2), R] thỏa hai đồng dư. Ghép hai đồng dư thành x≡R0(modM)x \equiv R_0 \pmod M 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. bib_i có thể âm, đưa về [0,ai)[0, a_i) trước. Trong merge, d * p nhân bằng __int128 vì cả hai tới 2⋅1092 \cdot 10^9.

C++
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 xx. Số nhỏ nhất ≥lo\ge lo đồng dư với RR là lo+((R−lo) mod M)lo + ((R - lo) \bmod M), phép dư lấy về không âm. Bội chung tới 4⋅10184 \cdot 10^{18}, 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.

C++
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ư.

Code đầy đủ
#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

Độ phức tạp: O(log⁡a)O(\log a).

Khi đi thi

  • Viết sẵn hàm merge(r, m) ghép vào đồng dư gộp (R,M)(R, M) bằng Euclid mở rộng; dùng được cả khi có ước chung.
  • Đưa số dư về [0,m)[0, m) trước khi ghép; tính M=M/g⋅mM = M / g \cdot m; nhân bằng __int128 khi thừa số có thể vượt 10910^9.
  • Nghiệm dương nhỏ nhất là MM khi R=0R = 0; đếm nghiệm trong đoạn bằng nghiệm đầu tiên rồi chia cho MM.

Tóm tắt

  • x≡a(modn)x \equiv a \pmod n, x≡b(modm)x \equiv b \pmod m có nghiệm khi gcd⁡(n,m)∣(b−a)\gcd(n, m) \mid (b - a); nghiệm duy nhất theo mô-đun lcm⁡(n,m)\operatorname{lcm}(n, m).
  • t=b−ag⋅p mod mgt = \frac{b - a}{g} \cdot p \bmod \frac{m}{g} với np+mq=gn p + m q = g, nghiệm x=a+ntx = a + n t.
  • Nhiều đồng dư: ghép lần lượt, MM tăng thành lcm⁡\operatorname{lcm}.
  • 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