Lucas Theorem và Số học Modular Nâng cao
Khi cần tính mà rất lớn (tới ) còn là số nguyên tố nhỏ, ta không thể tính trực tiếp giai thừa vì vượt xa mọi kiểu số. Định lý Lucas đưa bài toán về việc nhân các tổ hợp nhỏ theo từng chữ số trong cơ số , đạt thay vì . Trang này còn bàn các công cụ số học modular nâng cao thường đi kèm: nghịch đảo modular, CRT (gộp kết quả khi không nguyên tố), Cipolla (căn bậc hai modulo ) và căn nguyên thủy.
Ý tưởng / Trực giác
Vì sao Lucas đúng
Mấu chốt nằm ở một đẳng thức đa thức trên trường . Với nguyên tố, tam giác Pascal "sụp" theo modulo :
Lý do: khai triển nhị thức cho với , mà chứa thừa số ở tử nhưng mẫu không chia hết cho (vì mọi thừa số đều ), nên . Chỉ còn hai số hạng đầu và cuối.
Bây giờ viết trong cơ số : . Khi đó
Hệ số của ở vế trái chính là . Ở vế phải, vì mỗi nhân tử chỉ sinh ra các lũy thừa , nên muốn ghép lại thành ta buộc phải lấy đúng chữ số (trong cơ số của ) từ nhân tử thứ . Mỗi cách góp cho hệ số . Nhân lại:
Hệ quả quan trọng: nếu bất kỳ chữ số nào thỏa thì , kéo theo cả tích bằng . Đây cũng là định lý Kummer ở dạng gọn: chia hết cho khi và chỉ khi có "nhớ" lúc cộng trong cơ số .
Vì sao cần nghịch đảo modular
Để tính với , ta không chia được trực tiếp trong số học modular. Thay vào đó nhân với nghịch đảo . Vì nguyên tố và , định lý Fermat nhỏ cho . Đây là lý do mọi cài đặt đều có hàm power (lũy thừa nhanh).
Ví dụ chạy tay
Tính . Giá trị thật là , nên đáp án phải là . Xem Lucas tái tạo điều đó.
Đổi và sang cơ số :
n = 13 = (1 1 1)_3 vì 13 = 1*9 + 1*3 + 1
k = 4 = (0 1 1)_3 vì 4 = 0*9 + 1*3 + 1
^ ^ ^
p^2 p^1 p^0
Ghép theo từng cột chữ số (mỗi cột một tổ hợp nhỏ ):
vị trí i : 2 1 0
n_i : 1 1 1
k_i : 0 1 1
------------------------------
C(n_i,k_i): C(1,0) C(1,1) C(1,1)
= 1 * 1 * 1 = 1 (mod 3)
Vậy — khớp.
Một ví dụ cho thấy kết quả : . Ta có , :
i : 1 0
n_i : 1 0
k_i : 0 4
C(1,0) C(0,4)
= 1 * 0 = 0 (mod 5)
Tại cột , nên , kéo cả tích về . Thật vậy , chia hết cho .
Cài đặt
Lucas cơ bản (một vài truy vấn, nhỏ)
// Lũy thừa nhanh: a^e mod p — O(log e)
long long power(long long a, long long e, long long p) {
long long r = 1; a %= p;
while (e > 0) {
if (e & 1) r = r * a % p;
a = a * a % p;
e >>= 1;
}
return r;
}
// C(n,k) mod p với n,k < p, tính trực tiếp tử/mẫu rồi nhân nghịch đảo
long long C_small(long long n, long long k, long long p) {
if (k > n) return 0;
long long num = 1, den = 1;
for (long long i = 0; i < k; i++) {
num = num * ((n - i) % p) % p; // tử: n*(n-1)*...*(n-k+1)
den = den * ((i + 1) % p) % p; // mẫu: k!
}
return num * power(den, p - 2, p) % p; // chia = nhân nghịch đảo (Fermat)
}
// Lucas: tách theo từng chữ số cơ số p, đệ quy
long long lucas(long long n, long long k, long long p) {
if (k == 0) return 1; // chặn đáy: C(n,0)=1
if (k > n) return 0; // chữ số nào k_i>n_i -> kết quả 0
// C(n,k) = C(n%p, k%p) * C(n/p, k/p)
return C_small(n % p, k % p, p) * lucas(n / p, k / p, p) % p;
}
Lucas tối ưu cho nhiều truy vấn — precompute giai thừa mod
Khi cố định và có nhiều truy vấn, tiền xử lý fact[] và inv_fact[] để mỗi tính trong :
const int MAXP = 1e6 + 5;
long long fact[MAXP], inv_fact[MAXP], P;
void precompute_lucas(long long p) {
P = p;
fact[0] = 1;
for (int i = 1; i < p; i++) fact[i] = fact[i-1] * i % p;
inv_fact[p-1] = power(fact[p-1], p-2, p); // nghịch đảo của (p-1)!
for (int i = p-2; i >= 0; i--) // truy hồi ngược: rẻ hơn
inv_fact[i] = inv_fact[i+1] * (i+1) % p;
}
long long C_lucas(long long n, long long k) {
if (k > n || k < 0) return 0;
long long ans = 1;
while (n > 0 || k > 0) { // duyệt từng chữ số cơ số P
long long ni = n % P, ki = k % P;
if (ki > ni) return 0;
ans = ans * fact[ni] % P * inv_fact[ki] % P * inv_fact[ni-ki] % P;
n /= P; k /= P;
}
return ans;
}
Khi modulo KHÔNG nguyên tố: Lucas + CRT
Nếu không nguyên tố, ta tính (bằng định lý Andrew Granville / Lucas mở rộng) rồi gộp bằng CRT. Phần lõi CRT:
// Nghịch đảo modular tổng quát qua Euclid mở rộng (mod bất kỳ, gcd phải = 1)
long long ext_gcd(long long a, long long b, long long &x, long long &y) {
if (b == 0) { x = 1; y = 0; return a; }
long long x1, y1, g = ext_gcd(b, a % b, x1, y1);
x = y1; y = x1 - (a / b) * y1;
return g;
}
long long inv_mod(long long a, long long m) {
long long x, y;
if (ext_gcd((a % m + m) % m, m, x, y) != 1) return -1; // không tồn tại
return (x % m + m) % m;
}
// Gộp hệ x ≡ r1 (mod m1), x ≡ r2 (mod m2) với gcd(m1,m2)=1
long long crt(long long r1, long long m1, long long r2, long long m2) {
long long inv = inv_mod(m1 % m2, m2);
long long t = ((r2 - r1) % m2 + m2) % m2 * inv % m2;
return r1 + t * m1; // nghiệm theo mod m1*m2
}
Cipolla — căn bậc hai modulo số nguyên tố lẻ
Tìm với . Điều kiện tồn tại (ký hiệu Legendre ): .
// Trả về một nghiệm x của x^2 ≡ n (mod p), hoặc -1 nếu vô nghiệm
long long cipolla(long long n, long long p) {
n %= p;
if (n == 0) return 0;
if (power(n, (p-1)/2, p) != 1) return -1; // không phải thặng dư bậc 2
if (p % 4 == 3) return power(n, (p+1)/4, p); // công thức tắt
long long a = 0; // tìm a: a^2-n là phi-thặng-dư
while (power(((a*a - n) % p + p) % p, (p-1)/2, p) != p - 1) a++;
long long w2 = ((a*a - n) % p + p) % p; // "i^2" trong GF(p^2)
// số phức (a + sqrt(w2)); lũy thừa (p+1)/2 trong GF(p^2)
using P2 = pair<long long,long long>;
auto mul = [&](P2 X, P2 Y) -> P2 {
return { (X.first*Y.first + X.second%p*Y.second%p*w2) % p,
(X.first*Y.second + X.second*Y.first) % p };
};
P2 res = {1, 0}, base = {a, 1};
for (long long e = (p+1)/2; e > 0; e >>= 1) {
if (e & 1) res = mul(res, base);
base = mul(base, base);
}
return min(res.first, p - res.first); // res.second luôn = 0
}
Căn nguyên thủy (primitive root)
là căn nguyên thủy mod nếu cấp của đúng bằng (tức là hoán vị của ). Cần cho logarithm rời rạc, NTT...
bool is_primitive_root(long long g, long long p, vector<long long>& qs) {
for (long long q : qs) // qs = các ước nguyên tố của p-1
if (power(g, (p-1)/q, p) == 1) return false;
return true;
}
long long find_primitive_root(long long p, vector<long long> qs) {
for (long long g = 2; ; g++)
if (is_primitive_root(g, p, qs)) return g;
}
Độ phức tạp
| Thao tác | Thời gian | Bộ nhớ | Vì sao |
|---|---|---|---|
| Lucas cơ bản / truy vấn | có chữ số, mỗi chữ số gọi C_small tốn vòng lặp |
||
| Lucas precompute / truy vấn | tiền xử lý fact/inv_fact một lần ; mỗi chữ số tra cứu |
||
power (lũy thừa nhanh) |
bình phương lặp, mỗi bước giảm nửa số mũ | ||
| Cipolla | kỳ vọng | dò trung bình vài bước (nửa số là phi-thặng-dư), rồi lũy thừa GF tốn | |
| CRT gộp 2 đồng dư | chi phí một nghịch đảo modular |
Lý do cốt lõi Lucas nhanh: số chữ số của trong cơ số là . Với và thì chỉ khoảng chữ số — gần như tức thời.
⚠️ Lỗi thường gặp
- Tràn số khi nhân. Tích
fact[ni] * inv_fact[ki] * inv_fact[ni-ki]với cỡ vẫn an toàn tronglong longnếu lấy% Psau mỗi phép nhân. Nếu gộp ba thừa số rồi mới mod, hoặc dùngint, sẽ tràn. Luôna = a * b % ptừng bước, và biến trung gian làlong long. - Modulo âm. Trong Cipolla và CRT có hiệu
a*a - nhayr2 - r1có thể âm; phải chuẩn hóa((x % p) + p) % p. Quên+ pcho kết quả sai hoặc chỉ số mảng âm. - Dùng nghịch đảo Fermat khi không nguyên tố. chỉ là nghịch đảo khi nguyên tố. Với modulo hợp số phải dùng
inv_mod(Euclid mở rộng) và kiểm tra — nếu không nguyên tố cùng nhau thì nghịch đảo không tồn tại, không được trả bừa. - Áp Lucas khi KHÔNG nguyên tố. Lucas chỉ đúng cho nguyên tố. Với modulo như (không nguyên tố) hay phải tách thừa số nguyên tố + CRT, không được gọi
lucas(n, k, m)thẳng. - Quên trường hợp ở từng chữ số. Nhiều người chỉ kiểm tra ở đầu mà quên rằng ngay cả khi tổng thể, vẫn có thể tại một cột — lúc đó kết quả phải là . Vòng lặp
C_lucasphảireturn 0ngay khi gặpki > ni. - Precompute thiếu phần tử / sai biên.
fact[]vàinv_fact[]cần đủ chỉ số tới (không phải tới ). Cấp phát mảng nhỏ hơn gây truy cập ngoài biên; ngược lại đừng cố tạo mảng tới . - Cipolla với hoặc . Phải xử lý riêng (nghiệm ) và tránh dùng công thức cho chẵn; thuật toán chỉ đúng cho nguyên tố lẻ.
Biến thể / Mở rộng
- Định lý Kummer & lũy thừa của trong : số mũ của trong bằng số lần "nhớ" khi cộng trong cơ số . Hữu ích khi cần biết có chia hết cho không.
- Lucas mở rộng (Andrew Granville) cho , kết hợp CRT để xử lý modulo hợp số bất kỳ.
- Xem thêm các kỹ thuật nền tảng tại Toán học: số học modular, nghịch đảo, lũy thừa nhanh.
Bài tập luyện
- Mã Số Bò (cowids) — (Intermediate) dùng để đếm số nhị phân có đúng bit và "đi tới" số thứ ; luyện ý nghĩa tổ hợp của hệ số nhị thức.
- Tự giúp mình (helpself) — (Advanced) tính tổng trên tất cả tập con modulo ; luyện đếm tổ hợp và rút gọn lũy thừa .
- Số học mô-đun (modarith) — (Advanced) đếm hàm thỏa phương trình với nguyên tố lẻ; luyện cấp nhân (multiplicative order), căn nguyên thủy và lũy thừa modular.
- Năng Lực Bò Gold (cowmpetg) — (Veteran) đếm dãy hợp lệ với modulo ; cần hệ số nhị thức mod ở quy mô lớn — đúng đất diễn của Lucas/nghịch đảo modular.
- HILO (hilo21) — (Master) tổng trên hoán vị modulo ; bài tổ hợp modular nặng, tổng hợp giai thừa, nghịch đảo và đếm có dấu.