Hướng dẫn giải của Tính hệ số nhị thức
Nộp một lời giải chính thức trước khi tự giải là một hành động có thể bị ban.
Lời giải này đang bị ẩn cho đến khi bạn chọn mở ra.
Chúng tôi khuyên bạn nên tự thử giải bài trước. Việc mở lời giải có thể làm lộ mất ý tưởng chính trước khi bạn có cơ hội tự giải.
Bạn phải đăng nhập để mở lời giải này.
Đăng nhậpTác giả: , , ,
Hiểu bài toán
Bài toán yêu cầu tính giá trị của tổ hợp chập k lấy n, ký hiệu C(n, k), sau đó lấy số dư khi chia cho m. Dữ liệu input có giới hạn: k ≤ 10^6, n ≤ 10^9, m ≤ 10^9. Điều này có nghĩa là k khá nhỏ so với n, nhưng n và m lại rất lớn. Ta không thể tính trực tiếp C(n, k) vì giá trị quá lớn. Ta cần một phương pháp tính modulo trực tiếp và xử lý trường hợp m không nguyên tố.
Các cách tiếp cận
Cách Sử dụng Lucas Theorem (chỉ hiệu quả khi m là số nguyên tố)
void solve_lucas() {
// Chỉ hoạt động nếu m là số nguyên tố
// Công thức: C(n, k) mod m = product of C(n_i, k_i) mod m
// Trong đó n_i, k_i là các chữ số trong cơ sở m
if (is_prime(m)) {
ll res = 1;
while (n > 0 || k > 0) {
ll ni = n % m;
ll ki = k % m;
if (ki > ni) { cout << 0; return; }
res = (res * C_mod_prime(ni, ki, m)) % m;
n /= m;
k /= m;
}
cout << res;
}
}
- Time Complexity: O(log_m n) * O(m) hoặc O(log m) nếu dùng precompute
- Space Complexity: O(m) hoặc O(1)
Lucas Theorem chỉ áp dụng được khi m là số nguyên tố. Nó chia n và k thành các chữ số trong cơ sở m, sau đó tính tích các tổ hợp nhỏ hơn. Tuy nhiên, đề bài không đảm bảo m nguyên tố, nên cách này không đủ để giải quyết tất cả các trường hợp (mặc dù có thể là một phần của giải pháp tổng quát).
Cách Phân tích thừa số nguyên tố và Công thức lũy thừa (Prime Factorization + Power Formula)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// Phân tích m thành thừa số nguyên tố
vector<pair<ll, int>> prime_factors(ll m) {
vector<pair<ll, int>> factors;
for (ll i = 2; i * i <= m; ++i) {
if (m % i == 0) {
int cnt = 0;
while (m % i == 0) {
m /= i;
cnt++;
}
factors.push_back({i, cnt});
}
}
if (m > 1) factors.push_back({m, 1});
return factors;
}
// Tính số mũ của p trong n! (Legendre's formula)
ll get_power(ll n, ll p) {
ll res = 0;
while (n) {
res += n / p;
n /= p;
}
return res;
}
// Tính n! / p^a mod p^b (loại bỏ hết thừa số p)
ll factorial_mod_prime_power(ll n, ll p, ll pe) {
if (n == 0) return 1;
ll res = 1;
// Tính tích các số không chia hết cho p
for (ll i = 1; i < pe; ++i) {
if (i % p != 0) res = (__int128)res * i % pe;
}
res = pow_mod(res, n / pe, pe); // Lặp lại pe! ^ (n/pe)
// Xử lý phần còn lại
for (ll i = 1; i <= n % pe; ++i) {
if (i % p != 0) res = (__int128)res * i % pe;
}
return (__int128)res * factorial_mod_prime_power(n / p, p, pe) % pe;
}
// Hàm tính C(n, k) mod m
ll solve(ll n, ll k, ll m) {
if (k < 0 || k > n) return 0;
if (k == 0 || k == n) return 1 % m;
auto factors = prime_factors(m);
vector<ll> remainders;
for (auto [p, a] : factors) {
ll pe = 1;
for (int i = 0; i < a; ++i) pe *= p;
ll exponent = get_power(n, p) - get_power(k, p) - get_power(n - k, p);
if (exponent >= a) {
remainders.push_back(0);
continue;
}
ll num = factorial_mod_prime_power(n, p, pe);
ll den_k = factorial_mod_prime_power(k, p, pe);
ll den_nk = factorial_mod_prime_power(n - k, p, pe);
ll den = (__int128)den_k * den_nk % pe;
ll inv_den = 1;
// Tính nghịch đảo modular, den va pe co gcd = 1
// Su dung Extended Euclid hay Fermat de tinh inv
// O day su dung Extended Euclid
ll x, y;
extgcd(den, pe, x, y);
inv_den = (x % pe + pe) % pe;
ll val = (__int128)num * inv_den % pe;
val = (__int128)val * pow_mod(p, exponent, pe) % pe;
remainders.push_back(val);
}
// Dung CRT de ket hop cac ket qua
ll res = 0, mod = 1;
for (int i = 0; i < factors.size(); ++i) {
ll pe = 1;
for (int j = 0; j < factors[i].second; ++j) pe *= factors[i].first;
// Cong thuc CRT
// ... (code CRT)
}
return res;
}
- Time Complexity: O(sqrt(m) + k * log(m)) (thường là khoảng 10^6 operation)
- Space Complexity: O(log m)
Đây là phương pháp tổng quát nhất để giải quyết bài toán C(n, k) mod m khi m không nguyên tố.
- Phân tích m thành các thừa số nguyên tố dạng p^a.
- Với mỗi p^a, tính C(n, k) mod p^a bằng cách:
- Sử dụng Legendre's formula để kiểm tra xem kết quả có bằng 0 không (nếu số mũ của p trong C(n, k) >= a).
- Tính phần tử không chứa p trong C(n, k) (tức là n! / p^{vp(n!)}).
- Tính p^{vp(C(n, k))} mod p^a.
- Kết hợp và dùng định lý số học Euler (hoặc Extended Euclid) để chia modulo.
- Sử dụng China Remainder Theorem (CRT) để gộp các kết quả từ các mô-đun p^a lại thành kết quả modulo m.
Cách Tối ưu hóa bằng cách chia nhỏ k và tính trực tiếp
// Cach 1: Neu m nho, co the tinh binh thuong
// Cach 2: Su dung cong thuc C(n, k) = (n-k+1)/1 * (n-k+2)/2 * ... * n/k
// Voi m nguyen to, ta co the chia binh thuong.
// Voi m khong nguyen to, phai su dung Extended Euclid cho tung buoc.
ll solve_optimized(ll n, ll k, ll m) {
if (k > n - k) k = n - k;
ll res = 1;
for (ll i = 1; i <= k; ++i) {
ll mul = n - k + i;
// Cong thuc: res = res * mul / i
// res = (res * mul) / i
// De lay modulo, ta can tinh res * mul * inv(i) mod m
// Tuy nhien, inv(i) chi ton tai neu gcd(i, m) = 1.
// Neu m co n phan tu, i co the chia het m.
//
// Cach khac: Giai pt tuyen tinh: res * mul = ans * i (mod m)
// Hay su dung thuat toan fragmentary remainder sequence
}
return res % m;
}
- Time Complexity: O(k log m)
- Space Complexity: O(1)
Phương pháp này dựa trên công thức truy hồi C(n, k) = C(n, k-1) * (n-k+1) / k. Ta có thể tính tuần tự từ C(n, 1) đến C(n, k). Vấn đề lớn nhất là phép chia trong modular arithmetic. Nếu m không nguyên tố, ta không thể đơn giản nhân với nghịch đảo của i. Ta phải dùng Extended Euclid để giải phương trình tuyến tính tại mỗi bước hoặc dùng phương pháp phân tích thừa số nguyên tố cho từng bước tính toán, nhưng tổng quát hơn vẫn là phương pháp 2 (Prime Factorization).
Phân tích độ phức tạp
| Cách tiếp cận | Time | Space | Tên |
|---|---|---|---|
| 1 | O(log_m n) * O(m) hoặc O(log m) nếu dùng precompute | O(m) hoặc O(1) | Sử dụng Lucas Theorem (chỉ hiệu quả khi m là số nguyên tố) |
| 2 | O(sqrt(m) + k * log(m)) (thường là khoảng 10^6 operation) | O(log m) | Phân tích thừa số nguyên tố và Công thức lũy thừa (Prime Factorization + Power Formula) |
| 3 | O(k log m) | O(1) | Tối ưu hóa bằng cách chia nhỏ k và tính trực tiếp |
Bài học kinh nghiệm
- Do k nhỏ (≤ 10^6) nhưng n rất lớn (≤ 10^9), ta không thể dùng mảng để tính tổ hợp trực tiếp.
- Vì m có thể không nguyên tố, ta không thể dựa dẫm hoàn toàn vào Lucas Theorem hay nghịch đảo modular đơn giản.
- Phương pháp hiệu quả nhất là kết hợp Phân tích thừa số nguyên tố (Prime Factorization) cho m và China Remainder Theorem (CRT).
Lỗi thường gặp
- Tràn số (Overflow) khi tính n! hoặc các tích lớn. Cần sử dụng __int128 hoặc các hàm nhân mod đặc biệt.
- Sai lầm khi cho rằng nghịch đảo modular luôn tồn tại. Nó chỉ tồn tại khi số đó và m nguyên tố cùng nhau.
- Quên kiểm tra trường hợp C(n, k) = 0 mod m (khi số mũ của các thừa số nguyên tố trong C(n, k) lớn hơn hoặc bằng số mũ trong m).
Bình luận