Hàm nhân tính và Sàng tuyến tính
Một hàm nhân tính (multiplicative function) là hàm số học thỏa và với mọi cặp . Các hàm như (Euler), (Möbius), (số ước), (tổng ước) đều là hàm nhân tính. Sàng tuyến tính (linear sieve) cho phép tính toàn bộ giá trị của một hàm nhân tính trên đoạn trong — nhanh hơn cả sàng Eratosthenes và nhanh hơn rất nhiều so với việc phân tích thừa số từng số ().
Các hàm nhân tính quan trọng
| Hàm | Ký hiệu | Định nghĩa | Ví dụ |
|---|---|---|---|
| Phi Euler | Số phần tử trong nguyên tố cùng nhau với | (là ) | |
| Möbius | nếu là tích SNT phân biệt, nếu có | , | |
| Số ước | Số ước dương của | ||
| Tổng ước | Tổng các ước dương của | ||
| Hàm đơn vị | (bằng khi , ngược lại ) | — | |
| Hàm hằng | với mọi | — |
Tính theo phân tích thừa số (vì nhân tính nên chỉ cần biết giá trị trên các lũy thừa nguyên tố rồi nhân lại):
- nếu mọi , ngược lại
Ý tưởng / Trực giác
Vì sao "nhân tính" lại quan trọng? Nếu biết trên các lũy thừa nguyên tố thì biết trên mọi số: tách thành tích các đôi một nguyên tố cùng nhau rồi nhân. Bài toán tính trên cả mảng quy về "làm sao gán đúng giá trị khi xây dần từng số".
Vì sao sàng tuyến tính chạy ? Mấu chốt: ta muốn mỗi hợp số bị "đánh dấu loại" đúng một lần duy nhất. Sàng Eratosthenes loại nhiều lần (qua , qua ). Sàng tuyến tính quy ước: số hợp chỉ được sinh ra bởi với là ước nguyên tố nhỏ nhất (smallest prime factor, lp) của . Vì mỗi có duy nhất một và một cách viết , mỗi chỉ bị sinh một lần tổng công việc là .
Vì sao cập nhật được ngay khi sinh? Khi sinh ( là SNT đang xét, duyệt tăng dần), có hai trường hợp:
- (): khi đó và nguyên tố cùng nhau, dùng tính nhân tính trực tiếp: , .
- : đã là ước của , nên chứa . Lúc này . Với phi: thêm một thừa số chỉ nhân thêm (không phải ), tức . Quan trọng: ta break ngay sau trường hợp này, vì chính là nên các tiếp theo sẽ không còn là của — để chúng cho vòng lặp của khác xử lý, đảm bảo "loại đúng một lần".
Quy tắc nhân tính tổng quát: nếu tính được nhanh từ , thì cùng khung sàng này tính được ,... (chỉ cần thêm mảng đếm số mũ của ).
Ví dụ chạy tay
Chạy sàng tuyến tính tính và tới . Ký hiệu: primes là danh sách SNT đã tìm, dòng đánh dấu >> là bước đang sinh hợp số .
i=2 : 2 là SNT -> primes={2} phi[2]=1 mu[2]=-1
>> i*2=4 : p=2, 2|2 -> phi[4]=phi[2]*2=2, mu[4]=0, BREAK
i=3 : 3 là SNT -> primes={2,3} phi[3]=2 mu[3]=-1
>> i*2=6 : p=2, 2∤3 -> phi[6]=phi[3]*(2-1)=2, mu[6]=-mu[3]=1
>> i*3=9 : p=3, 3|3 -> phi[9]=phi[3]*3=6, mu[9]=0, BREAK
i=4 : 4 hợp số (lp=2) phi[4]=2 mu[4]=0
>> i*2=8 : p=2, 2|4 -> phi[8]=phi[4]*2=4, mu[8]=0, BREAK
i=5 : 5 là SNT -> primes={2,3,5} phi[5]=4 mu[5]=-1
>> i*2=10 : p=2, 2∤5 -> phi[10]=phi[5]*(2-1)=4, mu[10]=-mu[5]=1
>> i*3=15 (>12, dừng vòng p vì 5*3=15>12)
i=6 : 6 hợp số (lp=2) phi[6]=2 mu[6]=1
>> i*2=12 : p=2, 2|6 -> phi[12]=phi[6]*2=4, mu[12]=0, BREAK
Kết quả tích lũy (ô đang điền được làm dấu):
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 1 | 2 | 2 | 4 | 2 | 6 | 4 | 6 | 4 | 10 | 4 | |
| 1 | -1 | -1 | 0 | -1 | 1 | -1 | 0 | 0 | 1 | -1 | 0 |
Kiểm chứng: ✓; có ✓.
Cài đặt
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e7 + 5;
int phi[MAXN], mu[MAXN], lp[MAXN]; // lp[i] = ước nguyên tố nhỏ nhất của i
vector<int> primes;
bool is_composite[MAXN];
void linear_sieve(int n) {
phi[1] = mu[1] = 1;
for (int i = 2; i <= n; i++) {
if (!is_composite[i]) { // i là số nguyên tố
primes.push_back(i);
lp[i] = i;
phi[i] = i - 1; // các số 1..i-1 đều coprime với SNT i
mu[i] = -1; // 1 thừa số nguyên tố, mũ 1
}
// sinh các hợp số i*p với p <= lp[i], dừng khi vượt n
for (int j = 0; j < (int)primes.size()
&& (long long)i * primes[j] <= n; j++) {
int p = primes[j];
is_composite[i * p] = true;
lp[i * p] = p; // p là ƯCNT nhỏ nhất của i*p
if (i % p == 0) {
// p | i => i*p chứa p^2
phi[i * p] = phi[i] * p; // thêm 1 thừa số p: nhân p
mu[i * p] = 0; // có p^2 => mu = 0
break; // dừng để mỗi hợp số bị sinh đúng 1 lần
} else {
// gcd(i, p) = 1 => dùng tính nhân tính
phi[i * p] = phi[i] * (p - 1);
mu[i * p] = -mu[i];
}
}
}
}
Khung sàng cho và — cần thêm mảng số mũ của :
int d[MAXN], cnt[MAXN]; // cnt[i] = số mũ của lp trong i
long long sigma[MAXN], pw[MAXN]; // pw[i] = lp^(cnt+1) hỗ trợ tính sigma
void sieve_d_sigma(int n) {
d[1] = sigma[1] = 1;
for (int i = 2; i <= n; i++) {
if (!is_composite[i]) {
primes.push_back(i);
cnt[i] = 1; d[i] = 2; // p^1 có 2 ước
pw[i] = (long long)i * i; // p^2
sigma[i] = 1 + i; // 1 + p
}
for (int j = 0; j < (int)primes.size()
&& (long long)i * primes[j] <= n; j++) {
int p = primes[j], x = i * p;
is_composite[x] = true;
if (i % p == 0) {
cnt[x] = cnt[i] + 1;
d[x] = d[i] / (cnt[i] + 1) * (cnt[x] + 1); // (a+2)/(a+1)
pw[x] = pw[i] * p;
sigma[x] = sigma[i] / (pw[i] - 1) * (pw[x] - 1);
break;
} else {
cnt[x] = 1; d[x] = d[i] * 2; // nhân (1+1) cho thừa số p mới
pw[x] = (long long)p * p;
sigma[x] = sigma[i] * (1 + p);
}
}
}
}
Độ phức tạp
- Thời gian: . Lý do: vòng ngoài chạy lần; tổng số lần thực thi vòng trong bằng số hợp số được sinh, mà mỗi hợp số bị sinh đúng một lần (qua phân tích duy nhất với điều kiện
break). Vậy tổng công việc là — không có thừa số như Eratosthenes. - Bộ nhớ: cho mỗi mảng (
phi,mu,lp,is_composite...). Với , mỗi mảngintchiếm MB; cân nhắc gộp/bỏ mảng không cần để khớp giới hạn bộ nhớ. Danh sáchprimescó phần tử.
⚠️ Lỗi thường gặp
- Tràn số khi nhân chỉ số: điều kiện
i * primes[j] <= nphải ép(long long), nếu không trànint(với , tích có thể ) gây sai biên hoặc vòng lặp vô tận. - Quên
breakkhii % p == 0: đây là lỗi phá vỡ tính tuyến tính. Bỏbreakkhiến hợp số bị sinh nhiều lần, giá trị bị ghi đè sai và độ phức tạp tụt về (hoặc tệ hơn). - Modulo âm với : nhận giá trị . Khi cộng dồn theo modulo, kết quả có thể âm — phải
((x % MOD) + MOD) % MODtrước khi in, nếu không ra số âm sai. - Nghịch đảo trong công thức dưới modulo: công thức và chia chỉ đúng với số nguyên thật. Nếu làm theo modulo , phép chia phải thay bằng nghịch đảo modular (Fermat), và chỉ hợp lệ khi mẫu nguyên tố cùng nhau với MOD — chia trực tiếp sẽ sai.
- Quên khởi tạo : mọi hàm nhân tính có . Bỏ sót làm hỏng mọi giá trị nhân lên từ .
- Cận trên sai khi : với vòng sàng không chạy; phải đảm bảo đã gán đúng trước khi dùng.
Tích Dirichlet và Nghịch đảo Möbius
Tích Dirichlet: . Tích của hai hàm nhân tính lại là hàm nhân tính.
Quan hệ then chốt (nghịch đảo Möbius):
Ví dụ kinh điển: , tức . Dạng đảo của nó dùng để "gỡ" tổng ước số — nền tảng của các bài đếm theo điều kiện .
// Tính g từ f bằng nghịch đảo Möbius: g(n) = sum_{d|n} mu(n/d) f(d)
vector<long long> mobius_inversion(vector<long long>& f, int n) {
vector<long long> g(n + 1, 0);
for (int i = 1; i <= n; i++)
for (int j = i; j <= n; j += i) // j chạy qua các bội của i
g[i] += mu[j / i] * f[j];
return g;
}
Ứng dụng: Đếm cặp nguyên tố cùng nhau
Đếm số cặp , , có . Dùng để khử điều kiện : số cặp có chia hết cho là , nghịch đảo Möbius cho:
// Đếm cặp (i,j), 1<=i<j<=n, gcd(i,j)=1
long long count_coprime_pairs(int n) {
long long ans = 0;
for (int d = 1; d <= n; d++) {
long long cnt = n / d; // số bội của d trong [1,n]
ans += (long long)mu[d] * cnt * (cnt - 1) / 2; // mu[d] * C(cnt,2)
}
return ans;
}
Khi nào dùng?
- Bài toán đếm với điều kiện — dùng nghịch đảo Möbius để khử ràng buộc.
- Tính một hàm nhân tính trên cả với lớn — dùng sàng tuyến tính .
- trong nghịch đảo modular và định lý Euler ( khi ).
- Đếm cấu hình bất biến qua phép xoay (vòng cổ, vòng tròn) — công thức Burnside dùng trên các ước của .
Bài tập luyện
- Đếm số lượng ước (cntdivs) — (Amateur) Sàng tính cho mọi một lần để trả lời nhiều truy vấn — bài nhập môn dùng khung sàng cho hàm nhân tính.
- Bội số nguyên tố (primemult) — (Intermediate) Đếm số trong chia hết cho ít nhất một SNT cho trước bằng bao hàm–loại trừ (dấu chính là tinh thần ).
- Phân tích ước số (divanal) — (Intermediate) Tính , và tích các ước trực tiếp từ phân tích thừa số nguyên tố — áp dụng đúng các công thức nhân tính (kèm nghịch đảo modular).
- Tổng các ước số (sumdivs) — (Advanced) Tính với bằng kỹ thuật tổng theo khối (divisor block sum) trên hàm nhân tính .
- Đếm cặp nguyên tố cùng nhau (cntcopair) — (Advanced) Đếm cặp trong mảng — áp dụng trực tiếp nghịch đảo Möbius như mục ứng dụng ở trên.
- Đếm vòng cổ (cntnecklace) — (Advanced) Đếm chuỗi hạt màu bất biến qua phép xoay — công thức dùng hàm Euler trên các ước.