Funcția Möbius
Noțiuni introductive
În teoria numerelor, o funcție aritmetică este o funcție . O funcție aritmetică exprimă proprietăți aritmetice pentru .
Pentru numere prime între ele (adică ), avem două feluri de funcții aritmetice:
- funcții aditive, unde ;
- funcții multiplicative, unde = .
Pentru simplitate vom defini următoarele aspecte:
- dacă este o propoziție adevărată sau 0 în caz contrar.
- = partea întreagă a lui .
Cât și următoarele proprietăți celebre:
- .
- Șirul , cu , are valori distincte.
Pentru , număr prim, și , definim următoarele funcții multiplicative:
- funcția identică ;
- funcția putere , unde este constantă (nu confundăm cu funcția exponențială );
- funcția unitate ;
- funcția divizorilor = numărul de divizori ai lui ;
- indicatorul lui Euler , câte numere , cu și există
- funcția Möbius .
Definiție
Două funcții multiplicative, și , sunt identice dacă pentru oricare număr prim și oricare , .
Precalcularea funcțiilor multiplicative
În contextul nostru, vom lucra cel mai des cu funcții multiplicative, iar de cele mai multe ori avem nevoie să știm valorile unei funcții pentru un set mai larg de elemente. Și se dovedește că Ciurul învățat în clasa a 6-a este bun nu numai la aflarea numerelor prime.
Ciurul lui Eratostene
Acest algoritm este poate cel mai popular printre elevii de liceu și gimnaziu pentru a afla numerele prime într-un interval.
vector<int> ciur(N + 1);
ciur[0] = ciur[1] = 1;
for (int i = 2; i <= N; i++) {
if (ciur[i] == 0) { // numarul i este prim
for (int j = 2 * i; j <= N; j += i) {
ciur[j] = 1; // j se scrie ca i * p
}
}
}La finalul programului, va fi număr prim doar dacă .
Complexitatea de timp este .
Ciur liniar
Observăm că fiecare număr compus este parcurs de către cel de-al doilea for de mai multe ori. Dacă am putea să iterăm prin fiecare număr compus exact o singură dată am ajunge la complexitatea de . Reținem într-un vector auxiliar numerele prime, și pentru un fixat vom parcurge numerele prime până când un număr prim divide .
vector<int> prime;
vector<int> is_composite(N + 1);
for (int i = 2; i <= n; i++) {
if (!is_composite[i]) {
prime.push_back(i);
}
for (int j = 0; j < prime.size() && i * prime[j] <= n; j++) {
is_composite[i * prime[j]] = 1;
if (i % prime[j]) {
break;
}
}
}Demonstrație
Ca să demonstrăm faptul că ciurul de mai sus iterează prin fiecare număr compus exact odată avem nevoie de cel mai mic factor prim al acestuia, . Să presupunem că . Pentru oricare , este divizor a lui , presupunem ca este prim. Cum , atunci , însă este cel mai mic număr prim care divide , deci nu există un astfel . Deci odată luată în considerare perechea ,, va fi calculat doar o singură dată, transformând complexitatea finală în .
Precalcularea indicatorului lui Euler folosind Ciurul Liniar
Pentru a calcula trebuie să luam în considerare 3 cazuri:
- este prim, deci
- și , deci . (prin înțelegem: "a nu divide pe b").
- și . Acest caz este uneori greu de tratat, dar din fericire știm sigur că .
vector<int> prime;
vector<int> phi(N), compus(N);
phi[1] = 1;
for (int i = 2; i <= N; i++) {
if (!compus[i]) {
prime.push_back(i);
phi[i] = i - 1;
}
for (int j = 0; j < prime.size() && i * prime[j] <= N; j++) {
compus[i * prime[j]] = 1;
if (i % prime[j]) {
phi[i * prime[j]] = phi[i] * phi[prime[j]];
} else {
phi[i * prime[j]] = prime[j] * phi[i];
}
}
}Generalizare a ciurului liniar
Totuși, putem să generalizăm algoritmul prezentat mai sus pentru a funcționa pentru oricare funcție multiplicativă. Fie puterea celui mai mic factor din descompunerea în factori primi a lui . Pentru oricare și , cel mai mic număr prim care divide , putem scrie .
vector<int> prime, phi(N + 1), compus(N + 1), sml(N + 1);
phi[1] = 1;
for (int i = 2; i <= N; i++) {
if (!compus[i]) {
prime.push_back(i);
phi[i] = i - 1;
sml[i] = 1;
}
for (int j = 0; j < prime.size() && i * prime[j] <= N; j++) {
compus[i * prime[j]] = 1;
if (i % prime[j]) {
phi[i * prime[j]] = phi[i] * phi[prime[j]];
sml[i * prime[j]] = 1;
} else {
phi[i * prime[j]] =
(phi[i] / (pow(prime[j], sml[i]) - pow(prime[j], sml[i] - 1)));
phi[i * prime[j]] *=
(pow(prime[j], sml[i] + 1) - pow(prime[j], sml[i]));
sml[i * prime[j]] = sml[i] + 1;
}
}
}Atenție
Funcția pow din cod este o funcție scrisă de mână. Nu recomandăm folosirea funcției pow din <cmath>, din cauza erorilor de precizie.
Gândim similar pentru funcția Möbius:
- prim
- , , deci
- , , deci .
Observație
În cazul în care fracția de mai sus nu este definită (numitorul este 0), putem spune din start că .
vector<int> prime;
vector<int> sml(N), mobius(N), composite(N);
mobius[1] = 1;
for (int i = 2; i < N; i++) {
if (!composite[i]) {
prime.push_back(i);
mobius[i] = -1;
sml[i] = 1;
}
for (int j = 0; j < prime.size() && i * prime[j] < N; j++) {
composite[i * prime[j]] = 1;
if (i % prime[j]) {
mobius[i * prime[j]] = mobius[i] * mobius[prime[j]];
sml[i * prime[j]] = 1;
} else {
int cltr = (sml[i] == 0) - (sml[i] == 1);
int pl = (sml[i] + 1 == 0) - (sml[i] + 1 == 1);
if (cltr == 0) {
mobius[i] = 0;
} else {
mobius[i * prime[j]] = (mobius[i] / cltr) * pl;
}
sml[i * prime[j]] = sml[i] + 1;
}
}
}Implementare mai populară
Rareori avem nevoie de ciur liniar, și dacă nu intră în timp pentru precalculare, de ce ar intra ?
vector<int> phi(N), mobius(N);
phi[1] = mobius[1] = 1;
for (int i = 2; i < N; i++) {
phi[i] = i - 1;
}
for (int i = 1; i < N; i++) {
for (int j = 2 * i; j < N; j += i) {
mobius[j] -= mobius[i];
if (i > 1) {
phi[j] -= phi[i];
}
}
}Inversiunea lui Möbius
Ultimele din cele 3 funcții prezentate la începutul articolului sunt mai cunoscute ca restul, însă noi ne vom folosi cel mai mult de ultimele 2, anume indicatorul lui Euler și funcția Möbius.
Fie = . Inversiunea lui Möbius ne spune:
Cu toate astea, o proprietate mai importantă este . Ceea ce sugerează expresia este că pentru oricare număr natural suma va da 1 doar dacă . Pare nesemnificativă proprietatea, însă este foarte utilă în rezolvarea multor probleme de informatică.
Exercițiu 1: Calculează câte perechi () există cu proprietatea că .
Rezolvare: Noi trebuie să calculăm . Ne putem folosi de proprietatea de mai sus și să scriem relația astfel:
Iterăm prin toate numerele în loc de divizorii lui și obținem
Rearanjăm termenii și obținem
Observăm că
deci relația finală devine , care poate fi calculată în .
Exercițiu 2: Calculează câte perechi exisă, astfel încât și = .
Rezolvare:
Observăm că e identic cu exercițiul precedent, rezultatul fiind .\
Exercițiul 3: Calculează , unde cel mai mic multiplu comun al numerelor și .
Rezolvare: Știm totuși că , astfel problema ne cere să calculăm suma:
Pentru a ne ușura calculul, putem defini:
Observăm deci că dacă știm suma produselor , cu , fie această sumă , atunci rezultatul devine:
Pentru a calcula ne putem folosi de funcția mobius astfel:
Observăm că:
Deci:
Revenim la problema noastră inițială:
Iar răspunsul final este , care este calculabil în .
Probleme propuse spre rezolvare
Problema sumgcd
Pentru și date la tastatură, trebuie să calculați , unde reprezintă un -tuplu. Un -tuplu reprezintă o mulțime de elemente nu neapărat distincte cu valori cuprinse între 1 și . Formal, noi trebuie să calculam .
Dacă pentru un fixat aflăm câte M-tupluri există cu cmmdc-ul egal cu , atunci putem rezolva foarte ușor problema. Fie numărul de tupluri pentru care :
Rezultatul problemei este dat de . Complexitatea de timp pentru a calcula este , astfel complexitatea finală este
Altă soluție este următoarea:
Vom pune pe cele poziții doar multiplii de , astfel se formează șiruri posibile, dintre care scădem .
Complexitatea devine:
Putem precalcula puterile lui , obținem astfel . Ambele iau 100 puncte.
Problema cntgcd
Se dau două numere naturale și . Calculați câte perechi de numere și mai mici ca există, astfel încât . Perechea este la fel ca perechea .
Putem să luăm rezultatul de la primul exercițiu, pentru că probleme sunt echivalente. Singura restricție este faptul că perechea , dar putem efectiv să împărțim rezultatul la 2.
Soluția ia undeva la 45 puncte, datorită faptului că .
Fie = numărul de perechi , unde . Noi trebuie să calculăm practic .
Pentru putem calcula suma brut. Pentru putem elimina perechile care au cmmdc-ul 2, 3 etc.
Datorită faptului că șirul are elemente diferite, putem doar să calculăm câte numere există, astfel încât și să adunăm la rezultat .
Observație
Fie cel mai mic număr astfel încât . Atunci cel mai mare număr care îndeplinește aceeași proprietate este .
long long f(long long n) {
// cout << n << '\n';
if (n <= 1000000) {
return sum_phi[n]; // phi(1) + phi(2) + ... + phi(n)
}
if (dp[n]) {
return dp[n];
// am calculat deja rezultatul pt n
}
long long ans = 1LL * (1LL * n * (n + 1)) / 2;
for (int i = 2, dr; i <= n; i = dr + 1) {
dr = (n / (n / i));
if (dr > n) {
break;
}
ans -= (dr - i + 1) * f(n / i);
}
dp[n] = ans;
return ans;
}Complexitatea algoritmului de mai sus este foarte interesantă, ea fiind .
Problema tupleco
Se dau două numere și . Să se afle , numărul de tupluri formate din elemente cu proprietatea că:
- .
- .
Soluție de (sau chiar 100) de puncte
Ne vom folosi de funcția Möbius pentru a calcula rezultatul. Dacă facem abstracție de prima proprietate, răspunsul nostru devine:
Ce înseamnă însă ? Reprezintă numărul de șiruri de lungime , unde este multiplu de . Ca să numărăm doar numărul de șiruri care sunt sortate, ne vom folosi de Stars and Bars, astfel numărul de șiruri cu este egal cu .
Rezultatul nostru devine:
Soluția rulează în cu sau precalcularea.
#include <bits/stdc++.h>
using namespace std;
const int N = 1e7 + 1, mod = 3000017;
int n, k;
ifstream fin("tupleco.in");
ofstream fout("tupleco.out");
#define cin fin
#define cout fout
long long C(int n, int k, vector<long long> &f, vector<long long> &invf) {
return (1ULL * f[n] * (1ULL * invf[k] * invf[n - k] % mod) % mod) % mod;
}
int main() {
cin.tie(0)->sync_with_stdio(0);
cin >> k >> n;
vector<long long> f(n + k + 1), inv(n + k + 1), invf(n + k + 1);
vector<short> mobius(n + 1);
f[0] = f[1] = inv[0] = inv[1] = invf[0] = invf[1] = 1;
for (int i = 2; i <= n + k; i++) {
f[i] = (1ULL * f[i - 1] * i) % mod;
inv[i] = (1ULL * inv[mod % i] * (mod - mod / i)) % mod;
invf[i] = (1ULL * invf[i - 1] * inv[i]) % mod;
}
mobius[1] = 1;
for (int i = 1; i <= n; i++) {
if (mobius[i]) {
for (int j = i + i; j <= n; j += i) {
mobius[j] -= mobius[i];
}
}
}
long long ans = 0;
for (int d = 1; d <= n; d++) {
int lt = n / d;
long long plt = C(lt + k - 1, k, f, invf);
if (mobius[d] == -1) {
ans = (1ULL * ans + mod - plt) % mod;
} else if (mobius[d] == 1) {
ans = (1ULL * ans + plt) % mod;
}
}
cout << ans;
}Ok, dar putem mai bine?
Ne folosim de ideea prezentată la problema anterioară.
Observație
Deducem cu puternicele noastre simțuri că modulul () în problema asta este mult mai mic decât , astfel putem să calculăm combinările mult mai rapid:
- , deci putem precalcula combinările în .
- , deci
#include <bits/stdc++.h>
using namespace std;
const int mod = 3e6 + 17, N = 1e6 + 2;
ifstream fin("tupleco.in");
ofstream fout("tupleco.out");
#define cin fin
#define cout fout
struct Mint {
int val;
Mint(int x = 0) { val = x % mod; }
Mint(long long x) { val = x % mod; }
Mint operator+(Mint oth) { return val + oth.val; }
Mint operator*(Mint oth) { return 1LL * val * oth.val; }
Mint operator-(Mint oth) { return val - oth.val + mod; }
Mint fp(Mint a, long long n) {
Mint p = 1;
while (n) {
if (n & 1) {
p = p * a;
}
a = a * a;
n /= 2;
}
return p;
}
Mint operator/(Mint oth) {
Mint invers = fp(oth, mod - 2);
return 1LL * val * invers.val;
}
friend ostream &operator<<(ostream &os, const Mint &lol) {
os << lol.val;
return os;
}
};
vector<Mint> f(mod), invf(mod), inv(mod);
Mint C(int n, int k) {
if (n < 0 || k < 0 || n < k) {
return 0;
}
if (n >= mod) {
return C(n / mod, k / mod) * C(n % mod, k % mod);
}
return f[n] * invf[n - k] * invf[k];
}
int n, k;
unordered_map<int, Mint> mp;
Mint fr(int n) {
if (mp[n].val) {
return mp[n];
}
int dr = 2;
Mint total = C(n + k - 1, k);
while (dr <= n) {
int ptr = n / (n / dr);
int lt = n / dr;
total = total - (fr(lt) * (ptr - dr + 1));
dr = ptr + 1;
}
mp[n] = total;
return total;
}
int main() {
f[0] = f[1] = inv[0] = inv[1] = invf[1] = invf[0] = 1;
for (int i = 2; i < mod; i++) {
f[i] = f[i - 1] * i;
inv[i] = inv[mod % i] * (mod - mod / i);
invf[i] = invf[i - 1] * inv[i];
}
cin >> k >> n;
cout << fr(n);
}Probleme suplimentare
- ONI 2021 Baraj Seniori Pastile
- Lot 2023 Juniori countall
- RoAlgo Contest #8 Gya-chan and the gcd operation
- USACO Gold Cowpability
- Listă de probleme cu Mobius
- Sum of gcd of Tuples (Hard)