Aplicació: Màxim comú divisor
En aquest lliçó es mostren diverses solucions per calcular el màxim comú divisor de dos nombres. Totes elles usen iteracions i requereixen l'ús de condicions una mica més elaborades que les utilitzades fins ara.
Definició
Donats dos nombres naturals
Com a cas especial, i per definició, el màxim comú divisor de
Una aplicació del màxim comú divisor és la reducció de fraccions: Per exemple, com que el màxim comú divisor de
Primera solució
Com fer un programa que trobi el màxim comú divisor de dos nombres x
$ > 0$ i y
$ > 0$ qualssevol? Fixem-nos que aquest nombre no pot ser més gran que x
. Per tant, un possible mètode consisteix a provar si x
divideix també a y
; si és així, ja tenim el resultat: és x
. Altrament, provem si x
x
com a y
; si és així, x
$ - 1$ és el resultat. Altrament, provem si x
x
com a y
; si és així, x
$ - 2$ és el resultat. Etcètera:
int main() {
int x, y;
cin >> x >> y;
int d = x;
while (not (x%d == 0 and y%d == 0)) {
d = d - 1;
}
cout << d << endl;
}
Estudiem aquest programa amb deteniment. La línia
int d = x;
Copia el valor de x
a una variable d
, la qual al final de l'execució acabarà contenint el màxim comú divisor de x
i y
. Després, repetidament, comprovarem si el valor actual de d
és un divisor de x
i també un divisor de y
. Si no és el cas, restarem d
, i tornarem a provar. Així doncs, quina condició ha de complir la d
que busquem? Aquesta:
(x%d == 0 and y%d == 0)
L'operador %
retorna el residu (allò que sobra) de la divisió entera. Per exemple, 42%10
val x%d == 0
ens diu que si dividim x
entre d
, no sobra res. En altres paraules, ens diu que la divisió és exacta i que, per tant, d
divideix a x
.
Però ens cal comprovar que d
divideix a x
i (and
, en anglès) també que d
divideix a y
. L'operador and
es compleix només quan les dues condicions es compleixen. Per exemple, la condició
(a%2 == 0 and a >= 100)
indica si una variable a
conté un nombre parell que sigui almenys
Continuem. Quan ha de parar el while
? Quan la condició sobre d
es compleixi. A la inversa, el while
ha de continuar mentre la condició d'aturada no es compleixi. Per aquest motiu, en el while
usem la condició negada:
while (not (x%d == 0 and y%d == 0)) ...
L'operador not
només es compleix quan la condició sobre la que s'aplica no es compleix. Per exemple,
(not a >= b)
i
(a < b)
són equivalents (tot i que la segona és més concisa i, per tant, millor).
Lògicament, la línia
d = d - 1;
decrementa en d
, de forma simetrica a com i = i + 1;
incrementa el valor de i
en
Finalment, ara ja podem entendre com funciona tot el programa. Suposem que es llegeix un x
i un y
. Si en simulem l'execució, la d
comencarà valent while
, s'escriurà un
Una versió millorada
El codi anterior es pot millorar en alguns aspectes. Per exemple, de la mateixa manera que existeix l'operador ++
per incrementar, també existeix l'operador --
per decrementar. Així que podem substituir d = d - 1;
per --d;
.
D'altra banda, dintre del while
només hi ha una instrucció, per la qual cosa podem evitar usar les claus.
Finalment, la condició del while
(not (x%d == 0 and y%d == 0))
es pot simplificar: Siguin c1
i c2
dues condicions lògiques. Una llei fonamental de la lògica, deguda al matemàtic De Morgan not (c1 and c2)
és equivalent a (not c1) or (not c2)
, on l'operador or
es compleix quan es compleix almenys una de les seves dues condicions. En el nostre cas, tenim que c1
és x%d == 0
i c2
és y%d == 0
, i per tant tenim que not c1
és x%d != 0
i not c2
és y%d != 0
.
Tot plegat, el codi queda així:
int main() {
int x, y;
cin >> x >> y;
int d = x;
while (x%d != 0 or y%d != 0) --d;
cout << d << endl;
}
Aquest programa funciona correctament, perquè el bucle s'atura quan troba la primera d
(és a dir, la d
més gran) que compleix la condició demanada. I sempre en trobarà alguna, perquè en un cas límit arribarà a d
x
i y
.
Finalment, fixem-nos que aquest codi no funcionaria si x
pogués valer d
valdria inicialment %
amb segon paràmetre
Segona solució: L'algorisme d'Euclides
Algorisme
A continuació veurem com calcular el màxim comú dividor de dos nombre utilitzant l'algorisme d'Euclides, descobert pels grecs clàssics i descrit per Euclides
Resteu al més gran dels dos nombres el més petit, fins que siguin iguals; aquesta és la solució.
Provem aquest algorisme per calcular el màxim comú divisor de
78 66
---------
12 66
12 54
12 42
12 30
12 18
12 6
6 6
---------
6
Correctesa
La correctesa de l'algorisme d'Euclides es basa en dues propietats. La primera, trivial, diu que
Propietat. Si
Demostració. Qualsevol enter que divideixi
Implementació
Aquesta és una implementació de l'algorisme d'Euclides en C++:
int main() {
int x, y;
cin >> x >> y;
while (x != y) { // Fins que siguin iguals (⟺ mentre siguin diferents)
if (x < y) y = y - x; // Restem el més petit dels dos nombres al més gran
else x = x - y;
}
cout << x << endl; // Escrivim la solució
}
Després de llegir les entrades x
i y
, tenim un bucle que s'atura quan x == y
. A dins del bucle, on els nombres no poden ser iguals, restem el nombre més petit del més gran, fent la comparació x < y
per saber quin és quin. Després del bucle, quan els dos nombres són iguals, n'escribim un d'ells (x
, per exemple).
Cal dir que aquest programa tampoc funciona quan un dels nombres és
Comparació
Respecte a la primera solució, la principal virtud de l'algorisme d'Euclides és que en general fa menys iteracions, perquè a cada pas avança més. Per exemple, per a
Malgrat això, l'algorisme d'Euclides encara pot ser lent per a algunes entrades. Per exemple, quin és el màxim comú divisor de
Tercera solució: L'algorisme d'Euclides amb mòduls
Considerem aquesta part de la taula d'execució de l'algorisme d'Euclides:
12 66
12 54
12 42
12 30
12 18
12 6
Quantes vegades restem
int main() {
int x, y;
cin >> x >> y;
while (x != y) { // Fins que siguin iguals (⟺ mentre siguin diferents)
if (x < y) y = y%x; // Usem % per estalviar passos
else x = x%y;
}
cout << x << endl; // Escrivim la solució
}
☠️ Compte amb aquest programa! Provem-lo amb
78 66
---------
12 66
12 6
0 6
error, %0
Aquest és un fenomen molt habitual: Intentant millorar un programa per fer-lo més eficient, l'hem espatllat. En efecte, el nou programa arriba (ràpidament) a una situació on una de les variables és %
amb
Així doncs, ens hem de conformar amb el programa lent però correcte? No: el que hem de fer és arreglar el programa ràpid. Aquesta és una possible solució:
int main() {
int x, y;
cin >> x >> y;
while (y != 0) {
int r = x%y;
x = y;
y = r;
}
cout << x << endl;
}
Provem-lo amb
78 66
---------
66 12
12 6
6 0
---------
6
Per compendre com funciona, primer suposem que x
és el més gran dels dos nombres. Perquè això segueixi sent cert a cada pas del programa, usem una variable auxiliar r
que guardi l'actual residu x%y
. Fixem-nos que $0 \le $ r
$ < $ y
. Després, posem l'antiga y
a la nova x
, i posem el residu r
com a nova y
. Quan arribem a una situació on y
és x
actual.
I què passa si la x
inicial és més petita que y
? Res greu. Comprovem-ho amb
66 78
---------
78 66
66 12
12 6
6 0
---------
6
El programa només fa una iteració més, en la qual posa els dos nombres en ordre. La resta de l'execució és idèntica a quan els nombres venen donats en "l'ordre bo".
I si algun dels dos nombres és inicialment
Acabem aquesta lliçó amb alguns comentaris sobre l'eficiència d'aquest últim programa. Es pot demostrar que el nombre d'iteracions del bucle està afitat per cinc vegades el nombre de dígits del mínim entre x
i y
. Per exemple, si el nombre més petit dels dos és
Lliçons.jutge.org
Jordi Petit, Salvador Roura
© Universitat Politècnica de Catalunya, 2024