Algoritmul lui Euclid în C++. CMMDC și CMMMC
În acest articol voi prezenta conceptele de CMMDC (cel mai mare divizor comun) și CMMMC (cel mai mic multiplu comun), precum și Algoritmul lui Euclid, atât prin scăderi repetate, cât și prin împărțiri repetate. La final, vom discuta despre câteva aplicații la CMMDC și CMMMC.
Cel mai mare divizor comun (CMMDC)
Cel mai mare divizor comun a două numere naturale și este efectiv cel mai mare număr natural care îl divide atât pe cât și pe Acesta se notează (de la greatest common divisor) sau pur și simplu
De exemplu, și Există un singur caz în care valoarea nu e definită, și anume atunci când Asta pentru că este divizibil cu toate numerele naturale nenule. Nu putem spune că
Să luăm un exemplu mai mare: și Dacă descompunem în factori primi numerele și obținem:
De aici observăm că putem defini CMMDC-ul a două numere drept produsul dintre factorii lor comuni luați la cea mai mică putere. Este cât se poate de logic: Dacă am scădea puterea unui factor din descompunerea lui am obține un CMMDC mai mic, iar dacă am mări-o, cel puțin unul dintre cele două numere nu va mai fi divizibil cu noul CMMDC.
Cel mai mic multiplu comun (CMMMC)
Cel mai mic multiplu comun a două numere naturale nenule este cel mai mic număr natural nenul care este divizibil atât cu cât și cu Acesta se notează (de la least common multiple) sau De exemplu, și
Din nou, vom lua un exemplu mai complex ca să analizăm descompunerile în factori primi ale numerelor și Fie și
De aici, putem observa că CMMMC-ul a două numere este egal cu produsul dintre factorii lor comuni și necomuni luați la cea mai mare putere.
Algoritmul lui Euclid prin scăderi repetate
Algoritmul lui Euclid este o metodă foarte simplă și eficientă de a calcula CMMDC-ul a două numere. Există două variante ale Algoritmului lui Euclid: una prin scăderi repetate și cealaltă prin împărțiri repetate. Prima nu este nici pe departe la fel de eficientă precum a doua, dar voi începe cu ea pentru că ne ajută să înțelegem mai ușor varianta prin împărțiri.
Algoritmul lui Euclid prin scăderi repetate presupune să scădem la fiecare pas numărul mai mic din numărul mai mare, până când unul dintre ele devine La final, valoarea numărului care a rămas nenul va fi egală cu
while (a && b) if (a > b) a -= b; else b -= a;cout << a + b << '\n';
(Afișăm pentru că această valoare este egală cu numărul care nu a devenit
Se observă că algoritmul acesta modifică valorile inițiale ale variabilelor și Deci, dacă avem nevoie de ele mai târziu, putem să le facem copii la început, și să folosim copiile în cadrul algoritmului. Sau, mai simplu, definim o funcție gcd
, care primește ca parametri numerele și și returnează
// ...
int gcd(int a, int b) { while (a && b) if (a > b) a -= b; else b -= a; return a + b;}
// ...
int main() { int a, b; cin >> a >> b; cout << gcd(a, b) << '\n'; return 0;}
Explicație
Să vedem de ce algoritmul furnizează întotdeaua rezultatul corect. Practic, el se bazează pe următoarea relație de recurență:
Al doilea caz este trivial, iar primul se justifică astfel:
Notăm Fie un divizor oarecare al lui
- Dacă atunci, știind că rezultă că adică
- Dacă atunci, știind că rezultă că adică
Mai pe scurt, Cu alte cuvinte, perechile și au aceeași mulțime de divizori comuni. Așadar, cel mai mare divizor comun al fiecărei perechi trebuie și el să fie același. Adică,
Exemple
Iată o animație care ilustrează modul în care funcționează Algoritmul lui Euclid prin scăderi repetate pe trei perechi de numere: și
Complexitate
După cum am spus mai sus, această variantă a algoritmului nu este eficientă. De exemplu, dacă și se vor efectua un miliard de operații, ceea ce este enorm. Putem spune deci că Algoritmul lui Euclid prin scăderi repetate are complexitatea
Algoritmul lui Euclid prin împărțiri repetate
Să ne uităm cum evoluează perechea de numere în cazul algoritmului precedent:
L-am scăzut pe din până când a devenit mai mic decât Practic, am simulat procesul de împărțire a lui la prin scăderi repetate. Astfel, la final a devenit adică restul împărțirii lui la Așadar, putem optimiza algoritmul inițial împărțind la fiecare pas numărul mai mare la cel mai mic, și reținând în acesta restul împărțirii. În cazul de mai sus economisim doi pași, însă în cazul economisim un miliard de pași.
while (a && b) if (a > b) a %= b; else b %= a;cout << a + b << '\n';
Iată și o implementare ceva mai elegantă:
while (b) { int r = a % b; a = b; b = r;}cout << a << '\n';
La fiecare pas, îl copiem pe în și pe (restul împărțirii) în menținând astfel invariantul Numai la primul pas este posibil ca acesta să nu fie îndeplinit. Dacă inițial algoritmul se termină într-un pas, iar dacă numerele se inversează după primul pas, devenind mai mare decât
Complexitate
Să luăm din nou exemplul acela mare de la începutul articolului. Avem:
Numărul de pași efectuați este foarte mic în comparație cu numerele inițiale, ceea ce ne face să intuim că algoritmul are o complexitate de ordinul Se întâmplă ca intuiția să fie corectă, iar mai jos aveți demonstrația. E doar pentru cei interesați; în niciun caz nu aveți nevoie de ea la școală. Pentru clasa a noua, e suficient să știți că Algoritmul lui Euclid este… extrem de rapid.
Vom analiza trei iterații consecutive ale algoritmului. Perechea devine unde iar devine unde Considerăm că de unde și și
Știm că adică pentru un natural. Dar așa că Prin urmare,
Dar deci mai avem că
Adunând cele două relații, obținem că
Așadar, la fiecare două iterații, suma valorilor și devine de cel puțin două ori mai mică. Asta înseamnă că numărul maxim de iterați din while
este aproximativ În concluzie, complexitatea algoritmului este de ordinul În caz că vă întrebați, cel mai nefavorabil caz este cel în care numerele date sunt termeni consecutivi din Șirul lui Fibonacci, dar nu mai intru în detalii.
Formula pentru CMMMC
CMMMC-ul poate fi calculat foarte ușor folosind următoarea formulă:
De aici îl scoatem pe drept Înmulțirea și împărțirea se efectuează în timp constant, motiv pentru care complexitatea calculării CMMMC-ului este egală cu cea a calculării CMMDC-ului, adică
int lcm(int a, int b) { return a * b / gcd(a, b);}
Să demonstrăm formula. Reamintesc că CMMDC-ul este produsul factorilor comuni la cea mai mică putere, iar CMMMC-ul este produsul factorilor comuni și necomuni la cea mai mare putere. Ei bine, putem reformula prima definiție. Putem considera că CMMDC-ul este produsul factorilor comuni și necomuni la cea mai mică putere, dacă prin asta înțelegem că un factor care nu apare în ambele numere se va lua la puterea Deci, dacă un factor apare la puterea în și la în știm că în va apărea la puterea iar în la Când înmulțim cu exponentul lui va deveni care este tot una cu Așadar,
CMMDC și CMMMC pentru mai multe numere
Cum calculăm CMMDC-ul și CMMMC-ul pentru mai mult de două numere? În cazul CMMDC-ului e simplu. Luăm primele două numere, le calculăm CMMDC-ul, apoi calculăm CMMDC-ul dintre acest CMMDC și numărul următor și așa mai departe.
În cazul CMMMC-ului putem fi tentați să generalizăm formula cu produsul, însă ea nu funcționează pentru mai mult de două numere. Dacă ați înțeles demonstrația de mai sus, ar trebui să fie clar de ce. Ne rămâne să procedăm la fel, adică să luăm câte două numere și să le calculăm CMMMC-ul.
Aplicații
În continuare, voi prezenta cinci probleme legate de CMMDC și CMMMC.
Problema 1.
Se dau două numere naturale nenule și Să se determine dacă cele două numere sunt prime între ele. Două numere se numesc prime între ele, coprime sau relativ prime dacă singurul lor divizor comun este
Testăm pur și simplu dacă By the way, două numere consecutive sunt întotdeauna prime între ele. Ca să aibă un divizor comun trebuie ca diferența lor să fie divizibilă cu Cum diferența dintre și este singura valoare posibilă a lui este
bool relativePrimes(int a, int b) { return gcd(a, b) == 1;}
Problema 2.
Se dau două fracții și Știind că și sunt numere naturale, iar și sunt nenule, să se afișeze suma celor două fracții sub forma unei fracții ireductibile
O fracție este ireductibilă dacă Deci, ca să aducem o fracție la forma sa ireductibilă, trebuie să o simplificăm prin
int e = a * d + c * b;int f = c * d;int g = gcd(e, f);cout << e / g << " / " << f / g << '\n';
În caz că numerele date sunt mari, ar trebui să evităm pe cât posibil calculele ale căror rezultat ar putea depăși valoarea maximă a tipului int
. În acest sens, putem aduce din start fracțiile date la forma lor ireductibilă. În plus, când le aducem la un numitor comun, ar fi bine să alegem ca acesta să fie
int gcdAB = gcd(a, b); a /= gcdAB; b /= gcdAB;int gcdCD = gcd(c, d); c /= gcdCD; d /= gcdCD;int lcmBD = lcm(b, d);a *= lcmBD / b;c *= lcmBD / d;int e = a + c;int f = lcmBD;int gcdEF = gcd(e, f);e /= gcdEF;f /= gcdEF;cout << e << " / " << f << '\n';
Problema 3.
O instalație de Crăciun consistă din becuri de diferite culori. Știind că inițial toate becurile sunt aprinse și că fiecare bec (cu se aprinde din în secunde, să se determine după cât timp toate becurile vor fi aprinse simultan din nou.
Răspunsul trebuie să fie divizibil cu fiecare dintre timpii dați. Cum ne interesează prima dată când toate becurile vor fi aprinse din nou, vom calcula
int ans = 1;for (int i = 1; i <= n; i++) ans = lcm(ans, t[i]);cout << ans << '\n';
Problema 4.
Se consideră un dreptunghi de dimensiuni Să se determine numărul minim de pătrate congruente în care poate fi împărțit dreptunghiul dat, precum și lungimea laturii acestora.
De exemplu:
Se observă că lungimea laturii alese trebuie să fie divizibilă atât cu cât și cu Cum noi vrem ca și să fie cât mai mici posibil, trebuie să fie cât mai mare. Deci, nu poate fi decât Numărul pătratelor va fi
int l = gcd(a, b);cout << a * b / l / l << ' ' << l << '\n';
Problema 5.
Se dau două puncte în plan și ambele de coordonate întregi. Să se determine numărul punctelor laticeale de pe segmentul Prin punct laticeal înțelegem punct de coordonate întregi.
De exemplu:
Dacă înșiruim punctele laticeale de pe segmentul în funcție de cât de apropiate sunt de punctul vom observa că fiecare două puncte consecutive determină câte un dreptunghi. Cum dreptunghiurile determinate sunt congruente, lățimea lor trebuie să dividă lățimea dreptunghiului mare (determinat de și iar lungimea lor trebuie să dividă lungimea dreptunghiului mare. Asta înseamnă că numărul dreptunghiurilor mici trebuie să dividă unde iar
Totuși, numărul nu are de ce să fie mai mic decât pentru că dimensiunile dreptunghiurilor n-ar mai fi prime între ele, așa că fiecare dreptunghi ar putea fi împărțit în două sau mai multe dreptunghiuri cu colțuri în puncte laticeale. Deci, numărul de dreptunghiuri este ceea ce înseamnă că numărul punctelor căutate este
int latticePoints(int xA, int yA, int xB, int yB) { return gcd(abs(xA - xB), abs(yA - yB)) + 1;}
Sfârșit! Urmează în curând un articol despre descompunerea unui număr în factori primi și aplicațiile acesteia. Dacă aveți vreo întrebare despre CMMDC, CMMMC sau Algoritmul lui Euclid, nu ezitați să o adresați mai jos, într-un comentariu