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