Lezione 4
Problemi trattabili e soluzioni sempre più efficienti
Gianluca Rossi
Trattabile o intrattabile?
Consideriamo ora il problema, ben noto a tutti gli studenti a partire dalla
scuola media, di calcolare il massimo comun divisore (MCD) di due interi. Il
metodo insegnato a scuola presuppone la scomposizione in fattori primi dei
due interi.
Una prima soluzione Il metodo più semplice consiste nel verificare se il
numero intero n in questione è divisibile per 2, 3, . . . , n − 1. Se ad esempio n
è divisibile per 2, allora si ha che esistono s, q > 0 tali che n = 2s × q, con q
non divisibile per 2. Si pone allora 2 tra i fattori primi di n (con esponente
s) ed al posto di n si considera, nel seguito, il valore q. Si passa quindi a
considerare allo stesso modo la divisibilità per 3, e cosı̀ via. Il metodo può
essere riassunto come:
r=n
f o r i in 1 . . . n − 1 :
if i divide r :
r = is × q
p r e n d i i con p o t e n z a s
r=q
Evidentemente, il metodo verifica la divisibilità per al più n − 1 valori
(perché?). Questo non fornisce però un algoritmo polinomiale, in quanto
si assume che la relazione polinomiale debba valere tra il numero di passi
eseguiti (nel caso peggiore) e la descrizione dei dati di input: dato che la
descrizione dell’intero n richiede d = log10 n cifre in notazione decimale (o
b = log2 n cifre in binario), abbiamo che il numero di verifiche è esponenziale
in tale descrizione, in quanto n = 10d o n = 2b .
In generale, non è noto nessun algoritmo polinomiale per fattorizzare un
intero (su questa difficoltà poggia buona parte della moderna crittografia e dei
1
relativi protocolli su Internet). Tuttavia, questo non significa che il problema
del calcolo del massimo comun divisore sia intrattabile in quanto esistono
algoritmi che non richiedono la fattorizzazione e che effettuano un numero
di passi polinomiale rispetto alla lunghezza della descrizione dei due interi.
Uno di questi algoritmi è quello di Euclide che ora andremo a descrivere.
Algoritmo di Euclide L’algoritmo di Euclide prende il nome dal suo scopritore il matematico greco vissuto nel terzo secolo prima di Cristo. Si basa
sull’osservazione che se d è il massimo comun divisore tra n ed m allora d è
anche il massimo comun divisore tra n − m ed m (d’ora in poi assumeremo
sempre che n sia più grande di m). Dando per vero questo fatto ed osservando che MCD(n, 0) = n possiamo ridurre il problema di calcolare il massimo
comun divisore tra n ed m al problema di calcolarlo tra n − m ed m. Questo
semplifica le cose perché prima o poi, ripetendo il procedimento, uno dei due
interi diventa nullo. Per esempio se n = 128 e m = 72 allora
MCD(128, 72) = MCD(56, 72) = MCD(56, 16) = MCD(40, 16)
= MCD(24, 16) = MCD(8, 16) = MCD(8, 8)
= MCD(8, 0) = 8.
Si osservi che per calcolare il MCD(56, 16) abbiamo sottratto 16 da 56 per un
numero di volte tale da consentire al risultato della sottrazione di divenire
minore di 16 (in questo caso 3 volte). Dopo la terza sottrazione abbiamo
ottenuto come risultato 8 e 56 = 3 · 16 + 8 ovvero, dopo 3 passi ci siamo
ridotti a calcolare il massimo comun divisore tra 16 ed il resto della divisione
di 56 per 16 (che indicheremo 56 mod 16). In verità questo fatto è vero in
tutti i passaggi infatti 128 = 1 · 72 + 56 e 16 = 2 · 8 + 0. Questa osservazione ci
permette di introdurre una regola alternativa per passare da un passo a quello
successivo nella procedura: il massimo comun divisore tra n ed m (n ≥ m)
è il massimo comun divisore tra m ed il resto della divisione di n per m
(ovvero n mod m). Usando questa regola il numero di passi necessario per
ottenere il massimo comun divisore tra due numeri si riduce sensibilmente.
Per esempio
MCD(128, 72) = MCD(56, 72) = MCD(56, 16) = MCD(8, 16)
= MCD(0, 8) = 8.
Da quanto detto possiamo descrivere formalmente l’algoritmo di Euclide ricorsivamente. Per rendere l’algoritmo più compatto assumiamo che il primo
intero in input è maggiore o uguale del secondo.
2
def e u c l i d e ( n , m) :
i f m == 0 :
return n
return e u c l i d e (m, n mod m)
Per convincerci che l’algoritmo è corretto manca da dimostrare che il
massimo comun divisore di n ed m è anche il massimo comun divisore di m e
n − m. È quello che ci accingeremo a fare ora. La dimostrazione si articolerà
in due passi:
1. Se d = MCD(n, m) allora d è un divisore anche di n − m;
2. Non esiste un divisore di m ed n − m più grande di d.
Se d = MCD(n, m) allora n = d · a e m = d · b per due interi a e b; quindi
n − m = d(a − b) ovvero d divide n − m (e, ovviamente m) e
d ≤ MCD(m, n − m).
(1)
Questo dimostra il primo punto, per dimostrare il secondo assumeremo che
MCD(m, n − m) = q > d e faremo vedere che la cosa non è possibile quindi,
per forza di cose, deve essere
d ≥ MCD(m, n − m).
(2)
Poiché q divide m si ha m = q · x dove x è un intero opportuno. Ma q
divide anche n − m allora deve esistere un intero y tale che n − q · x = q · y.
Quindi n = q · (x + y) ovvero q divide n ma sappiamo anche che q divide m
e che q è più grande di d, questo non è possibile in quanto, per ipotesi, d è il
massimo comun divisore tra n ed m. Riassumendo abbiamo dimostrato che
le l’Equazione 1 e l’Equazione 2 valgono contemporaneamente quindi vale il
seguente risultato.
Teorema.
MCD(n, m) = MCD(m, n − m) = MCD(m, n mod m).
Ora dimostriamo che l’algoritmo di Euclide è esponenzialmente più efficiente di quello basato su fattorizzazione. Guardando l’algoritmo di Euclide
dovrebbe essere evidente che il numero di passi eseguito dall’algoritmo è all’incirca il numero di volte che viene calcolato il resto di una divisione tra
due numeri. Infatti sia T (n, m) il numero di passi eseguito dall’algoritmo su
3
input n ed m (con n ≥ m) allora T (n, 0) = 1 altrimenti T (n, m) = 1+T (m, n
mod m). Assumendo che l’algoritmo calcoli k resti (r1 , r2 , . . . , rk ) allora
T (n, m) = 1 + T (m, r1 ) = 2 + T (r1 , r2 ) = . . . = k + T (rk−1 , rk ) = k + 1.
Adesso occorre trovare un modo per legare k ad n ed m. In particolare qual
è la coppia di numeri n ed m per il quali k è massimo? O, equivalentemente,
fissato k, qual è la coppia n ed m con n minimo che richiede il calcolo di k
resti? La risposta a questa domanda è fortemente legata ad una successione
numerica introdotta dal matematico pisano Leonardo Fibonacci vissuto a
cavallo del dodicesimo e tredicesimo secolo.
I primi due numeri della successione di Fibonacci sono 0 ed 1, ogni numero
che segue è dato dalla somma dei due numeri che lo precedono quindi
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, . . .
Più formalmente, sia fi l’i-esimo numero della successione di Fibonacci (o
numero di Fibonacci) allora f0 = 0, f1 = 1 e per ogni altro i, fi = fi−1 + fi−2 .
Guardando la sequenza si ha la sensazione che i numeri della successione
di Fibonacci crescano molto velocemente. A quanto pare Fibonacci arrivò a
questa successione cercando una legge che descrivesse la crescita di una popolazione di animali parecchio prolifici, i conigli. Questa intuizione è supportata
dalla matematica infatti si può dimostrare che
fi =
ϕi − (1 − ϕ)i
√
5
√
dove ϕ = (1 + 5)/2 ≈ 1.62 è detta sezione aurea. Quindi per i grande
(1 − ϕ)i è un numero molto vicino allo zero per questo si assume fi ≈ ϕi .
In che modo i numeri di Fibonacci sono legati al nostro problema? Dimostreremo che la coppia di numeri minima che nell’algoritmo di Euclide
richiede il calcolo di k resti è data da fk+1 e fk .
La tecnica di dimostrazione utilizza il cosı̀ detto principio di induzione
che si basa su un’idea piuttosto intuitiva. Vogliamo dimostrare che la proposizione P , che dipende da un intero k, sia vera per tutti i k. Se si riuscisse a
dimostrasse che P (k) è vera se è vera P (k−1), poiché k è un qualsiasi numero
da 0 in poi, si avrebbe che P (k − 1) è vera se lo è P (k − 2) e cosı̀ via. Se il
risultato fosse vero per 0 allora la catena terminerebbe positivamente e tutte
le proposizioni fin qui costruite sarebbero vere; in altre parole la proposizione
P (k) per ogni k. Quindi una dimostrazione basata sul principio di induzione
prevede due fasi: nella prima fase si dimostra che P (0) è vera; nella seconda
si assume che P (k − 1) è vero (ipotesi induttiva) e si dimostra che lo è P (k).
4
Applichiamo il principio di induzione per dimostrare la nostra proposizione P (k) che afferma “la coppia di numeri minima che nell’algoritmo di
Euclide richiede il calcolo di k resti è data da fk+1 e fk ”.
Se k = 0 allora m deve essere 0 e n il più piccolo intero maggiore di 0
ovvero m = 0 = f0 e n = 1 = f1 . Ora assumiamo che P (k − 1) sia vero.
Dall’algoritmo si ha euclide(n, m) = euclide(m, r) dove r = n mod m. Se
l’esecuzione di euclide(n, m) richiede il calcolo di k resti allora l’esecuzione
di euclide(m, r) ne richiede k − 1. Quindi, se m ed r è la coppia più piccola
che richiede il calcolo di k − 1 resti allora m = fk−1+1 = fk e r = fk−1 .
Inoltre n = q · m + r = q · fk + fk−1 per qualche q, poiché vogliamo che questo
sia minimo allora q deve essere 1 (non può essere zero, perché?) ovvero
n = fk + fk−1 = fk+1 .
Riassumendo, se T (n, m) = k + 1 allora n ≥ fk+1 ≈ ϕk+1 , quindi
log10 n ≥ (k + 1) log10 ϕ ≈ 0.21(k + 1) = 0.21T (n, m).
che dimostra il teorema che segue.
Teorema. Siano n ed m due interi con n ≥ m, allora il numero di passi che
esegue l’algoritmo di Euclide nel caso peggiore è proporzionale a log10 n.
5