Descompunerea în factori primi. Probleme de divizibilitate în C++
În articolul precedent am prezentat Algoritmul lui Euclid și am rezolvat în C++ câteva probleme legate de CMMDC și CMMMC. Astăzi vom discuta despre descompunerea unui număr în factori primi și vom rezolva niște probleme clasice de divizibilitate, majoritatea legate de factorizarea întregilor.
Problema 1.
Se dă un număr natural nenul Să se afișeze toți divizorii numărului
Cea mai simplă idee este să luăm pe rând fiecare număr natural de la la și să vedem dacă este divizibil cu acesta, adică dacă restul împărțirii lui la este
for (int d = 1; d <= n; d++) if (n % d == 0) cout << d << ' ';cout << '\n';
Totuși, această soluție este ineficientă. De exemplu, pentru execuția programului durează două-trei secunde. Observația ce ne permite să optimizăm algoritmul este că, de fiecare dată când găsim un divizor al lui putem afirma că și este un divizor al lui Astfel, dacă la fiecare nou divizor îl afișăm atât pe el, cât și pe complementul lui, putem să ne oprim atunci când devine mai mare decât
Ei bine, valoarea la care ne putem opri din a căuta divizori este Dacă îl împărțim pe la un număr mai mic decât sigur obținem un număr mai mare decât La fel și invers.
Noul algoritm are complexitatea care este mult mai bună decât precedentul Pentru se vor efectua doar de pași, în loc de un miliard. Iată așadar codul:
for (int d = 1; d * d <= n; d++) if (n % d == 0) { cout << d << ' '; if (n / d != d) cout << n / d << ' '; }cout << '\n';
Iar în caz că vrem să afișăm divizorii în ordine crescătoare, putem ca pe cei mai mici sau egali cu să-i reținem într-un vector de lungime iar pe cei mai mari decât într-un vector de lungime La final, afișăm elementele lui în ordine crescătoare, iar pe cele ale lui în ordine descrescătoare.
for (int d = 1; d * d <= n; d++) if (n % d == 0) { l[p++] = d; if (n / d != d) r[q++] = n / d; }for (int i = 0; i < p; i++) cout << l[i] << ' ';for (int i = q - 1; i >= 0; i--) cout << r[i] << ' ';cout << '\n';
Problema 2. [Testul de primalitate]
Se dă un număr natural nenul Să se verifice dacă este un număr prim sau nu.
Mai întâi niște teorie. Divizorii și se numesc divizorii improprii ai lui pe când ceilalți divizori se numesc divizorii proprii. Un număr natural mai mare decât se numește prim dacă nu are niciun divizor propriu. Adică dacă are exact doi divizori (pozitivi). Adică dacă singurul său divizor diferit de este el însuși. Un număr natural mai mare decât se numește compus dacă nu este prim. Adică dacă poate fi scris drept produsul a mai multe numere prime (nu neapărat distincte). Numerele și nu sunt considerate nici prime, nici compuse.
Revenind la problema noastră, prima metodă de a verifica primalitatea unui număr este să-i numărăm toți divizorii și să testăm dacă numărul acestora este exact
int cnt = 0;for (int d = 1; d <= n; d++) if (n % d == 0) cnt++;cout << (cnt == 2 ? "DA\n" : "NU\n");
Putem optimiza puțin codul căutând doar divizori proprii și dând break
când l-am găsit pe primul. Însă, dacă procedăm așa, va trebui să tratăm separat cazul
bool ok = n > 1;for (int d = 2; d < n; d++) if (n % d == 0) { ok = false; break; }cout << (ok ? "DA\n" : "NU\n");
În continuare, putem reduce timpul de execuție la aplicând observația de la problema precedentă. Știm că dacă nu am găsit niciun divizor până la sigur nu vom găsi nici după. Dacă am găsi, acesta ar trebui să aibă un complement mai mic sau egal cu
bool ok = n > 1;for (int d = 2; d * d <= n; d++) if (n % d == 0) { ok = false; break; }cout << (ok ? "DA\n" : "NO\n");
Problema 3. [Descompunerea în factori primi]
Se dă un număr natural nenul Să se afișeze descompunerea sa în factori primi.
Descompunerea în factori primi (numită și factorizare) a unui număr natural nenul se referă la scrierea lui sub forma unde numerele sunt numere prime distincte. Algoritmul clasic presupune să luăm pe rând câte un număr prim și să testăm dacă este divizibil cu Dacă da, îl împărțim pe la cât timp calculând pe parcurs exponentul la care apare în descompunerea lui Ne oprim când
for (int d = 2; n > 1; d++) if (n % d == 0) { int e = 0; while (n % d == 0) { e++; n /= d; } cout << d << ' ' << e << '\n'; }
Poate ați observat că în if
n-am mai testat dacă este prim. Asta pentru că nici nu poate fi compus, căci a fost deja împărțit la toți divizorii primi ai lui așa că nu mai poate divide valoarea actuală a lui
În ceea ce privește complexitatea, algoritmul nu e grozav. Dacă este prim, va trebui să ajungă la valoarea ca să putem ieși din for
. Și ăsta nu e singurul caz în care algoritmul rulează în timp liniar. Un alt caz nefavorabil este cel în care este produsul dintre un număr prim foarte mic și unul foarte mare. De exemplu, Algoritmul va efectua pași, adică care e echivalent cu
Pentru a reduce complexitatea worst-case la trebuie să ne oprim atunci când depășește radicalul lui Atunci vom ști sigur că ce a mai rămas din este un număr prim, pe care îl vom putea afișa când ieșim din for
.
for (int d = 2; d * d <= n; d++) if (n % d == 0) { int e = 0; while (n % d == 0) { e++; n /= d; } cout << d << ' ' << e << '\n'; }if (n > 1) cout << n << " 1\n";
Problema 4.
Se dau două numere naturale nenule și Să se calculeze cel mai mare divizor comun al lor.
Deja am văzut în articolul despre Algoritmul lui Euclid cum putem calcula CMMDC-ul în timp logaritmic. Însă acum vreau să vă arăt soluția ce folosește descompunerea numerelor în factori primi, care evident că are complexitatea bătând ul de la Algoritmul lui Euclid prin scăderi repetate.
Soluția constă în a calcula descompunerile numerelor și reținând în vectorii și fiecare factor prim al lui și respectiv exponentul acestuia, și procedând similar cu și După aceea, intersectăm cele două perechi de vectori folosind interclasare. Pentru fiecare divizor comun găsit, înmulțim răspunsul cu
for (int d = 2; d * d <= a; d++) if (a % d == 0) { divA[x] = d; while (a % d == 0) { expA[x]++; a /= d; } x++; }if (a > 1) { divA[x] = a; expA[x++] = 1;}
for (int d = 2; d * d <= b; d++) if (b % d == 0) { divB[y] = d; while (b % d == 0) { expB[y]++; b /= d; } y++; }if (b > 1) { divB[y] = b; expB[y++] = 1;}
int gcd = 1;divA[x] = divB[y] = 1e9;for (int i = 0, j = 0; i < x || j < y; ) if (divA[i] < divB[j]) i++; else if (divA[i] > divB[j]) j++; else { for (int k = 0; k < min(expA[i], expB[j]); k++) gcd *= divA[i]; i++; j++; }cout << gcd << '\n';
Problema 5.
Se dau două numere naturale nenule și Să se verifice dacă descompunerile numerelor și conțin aceeași factori primi.
Descompunem în factori primi numerele și urmând să verificăm dacă factorii primi găsiți în fiecare descompunere sunt aceiași. Am putea să reținem factorii primi ai fiecărui număr într-un vector, dar mai simplu este să calculăm produsul lor, la final rămânându-ne să verificăm doar dacă cele două produse sunt egale.
int descA = 1;for (int d = 2; d * d <= a; d++) if (a % d == 0) { descA *= d; while (a % d == 0) a /= d; }if (a > 1) descA *= a;
int descB = 1;for (int d = 2; d * d <= b; d++) if (b % d == 0) { descB *= d; while (b % d == 0) b /= d; }if (b > 1) descB *= b;cout << (descA == descB ? "DA\n" : "NU\n");
Problema 6.
Se dă un număr natural nenul Să se verifice dacă este un număr aproape prim. Un număr se numește aproape prim dacă poate fi scris sub forma unui produs de două numere prime distincte.
Parcurgem numerele naturale de la la (exclusiv). Dacă e divizibil cu și atât cât și sunt prime, atunci afișăm "DA"
și ne oprim. Am definit o funcție isPrime(int n)
care testează dacă este prim; era cam enervant să testăm primalitatea a două numere într-un for
fără să folosim funcții.
#include <bits/stdc++.h>using namespace std;
bool isPrime(int n) { for (int d = 2; d * d <= n; d++) if (n % d == 0) return false; return true;}
int main() { int n; cin >> n; for (int d = 2; d * d < n; d++) if (n % d == 0 && isPrime(d) && isPrime(n / d)) { cout << "DA\n"; return 0; } cout << "NU\n"; return 0;}
Complexitatea acestei soluții este Primul radical vine de la for
, al doilea de la testul de primalitate, iar radicalul din interiorul celui din urmă vine de la faptul că numărul maxim pentru care se testează primalitatea este
Totuși, există și o soluție în care se bazează pe ideea de la problema precedentă. Putem să-l descompunem în factori primi pe și să calculăm pe parcurs numărul factorilor primi găsiți, precum și produsul acestora. Dacă numărul lor e și produsul este egal cu atunci este aproape prim.
int cnt = 0, prod = 1;for (int d = 2; d * d <= n; d++) if (n % d == 0) { cnt++; prod *= d; while (n % d == 0) n /= d; }if (n > 1) { cnt++; prod *= n;}cout << (cnt == 2 && prod == n ? "DA\n" : "NU\n");
Problema 7.
Se dau două numere naturale nenule și Să se afișeze divizorii comuni ai acestora.
Dacă un număr îl divide atât pe cât și pe atunci trebuie să-l dividă pe Deci, mai întâi calculăm CMMDC-ul numerelor și iar apoi afișăm divizorii acestuia.
while (b) { int r = a % b; a = b; b = r;}for (int d = 1; d * d <= a; d++) if (a % d == 0) { cout << d << ' '; if (a / d != d) cout << a / d << ' '; }cout << '\n';
Problema 8.
Se dă un număr natural nenul Să se determine câte perechi ordonate de numere naturale nenule există, cu proprietatea că
Să scriem descompunerile în factori primi ale celor trei numere:
Pentru ca să fie egal cu trebuie ca:
Fiecare pereche de exponenți poate lua valori, și anume:
Prin urmare, răspunsul problemei este Putem calcula această expresie în timp ce descompunem numărul în factori primi.
int ans = 1;for (int d = 2; d * d <= n; d++) { int e = 0; while (n % d == 0) { e++; n /= d; } ans *= 2 * e + 1;}cout << ans << '\n';
Problema 9.
Se dă un număr natural Să se determine în câte zerouri se termină numărul
Numărul de zerouri de la finalul unui număr este egal cu puterea maximă a lui cu care este divizibil (de fapt exponentul ei). Cum valoarea căutată este dată de unde este exponentul la care apare factorul în descompunerea lui Așadar, putem lua fiecare număr natural de la la și să calculăm valorile și pe care să le adunăm apoi la și La final, afișăm
int e2 = 0, e5 = 0;for (int i = 1; i <= n; i++) { int x = i; while (x % 2 == 0) { e2++; x /= 2; } while (x % 5 == 0) { e5++; x /= 5; }}cout << min(e2, e5) << '\n';
Dacă ne uităm mai atent, vom observa că nu este nevoie să calculăm și și pentru că va fi întotdeauna mai mic decât Acum că am făcut observația asta, putem parcurge numerele din în pentru că avem nevoie doar de multiplii lui
int ans = 0;for (int i = 5; i <= n; i += 5) { int x = i; while (x % 5 == 0) { ans++; x /= 5; }}cout << ans << '\n';
Până acum avem complexitatea însă putem deduce o formulă ce ne va permite să calculăm răspunsul în timp logaritmic. Ideea este similară cu cea de la Ciurul lui Eratostene: La primul pas parcurgem din în numerele mai mici sau egale cu incrementând de fiecare dată rezultatul. Însă, în cazul multiplilor de am procesat un singur factor de din descompunerea lor, și știm că mai avem cel puțin unul. Deci, la al doilea pas parcurgem numerele din în incrementând de fiecare dată rezultatul. Apoi parcurgem numerele din în și tot așa.
În loc să parcurgem efectiv numerele din în adunând câte un la rezultat, putem să adunăm din start valoarea pentru că acesta e numărul multiplilor lui mai mici sau egali cu Așadar, formula finală este:
Numărul de pași efectuați este pentru că cea mai mare putere a lui mai mică sau egală cu este Din acest motiv, complexitatea algoritmului este
int ans = 0;for (int p = 5; p <= n; p *= 5) ans += n / p;cout << ans << '\n';
Această formulă este un caz particular al formulei lui Legendre, care ne spune că exponentul lui din descompunerea în factori primi a lui este:
Problema 10.
Se dă un număr natural Să se determine ultima cifră nenulă a numărului
Extragem din fiecare număr natural de la la toți factorii de și de posibili, calculând exponenții lor în și Între timp, calculăm modulo produsul dintre numerele rămase după extragerea factorilor respectivi. Apoi, înmulțim rezultatul cu pentru că asta e puterea lui din care nu contribuie la formarea de zerouri la finalul numărului. Evident că și această operație va fi efectuată modulo
int e2 = 0, e5 = 0, ans = 1;for (int i = 1; i <= n; i++) { int x = i; while (x % 2 == 0) { e2++; x /= 2; } while (x % 5 == 0) { e5++; x /= 5; } ans = ans * x % 10;}for (int i = 0; i < e2 - e5; i++) ans = ans * 2 % 10;cout << ans << '\n';
Cam astea sunt cele mai importante probleme legate de descompunerea în factori primi. Puteți rezolva în continuare probleme de divizibilitate pe PbInfo. Urmează în curând un articol cu trei aplicații ceva mai avansate ale descompunerii în factori primi: Numărul divizorilor, suma divizorilor și indicatorul lui Euler. Dacă aveți vreo problemă de divizibilitate care vă dă bătăi de cap, nu ezitați o să lăsați mai jos, într-un comentariu, pentru a vă ajuta