32 4. Algoritmi Numerici Fondamentali Consideriamo adesso un algoritmo classico, per il calcolo del Massimo Comun Divisore (MCD) tra due numeri interi, n ed m: l'Algoritmo di Euclide. Senza perdita di generalità, assumiamo n ≥ m > 0 e chiamiamo R il MCD. Possiamo descrivere l'Algoritmo di Euclide in un linguaggio pseudo-naturale: Euclide(n,m) x ← n y ← m while x ≠ y do if x > y then x ← x - y else y ← y - x endif end R←y All'algoritmo descritto sopra corrisponde il diagramma di flusso della Figura 16. Questo algoritmo ha un ciclo di natura diversa da quelli visti in precedenza, perchè non si sa quante volte il ciclo stesso venga eseguito. Consideriamo il ciclo dell'algoritmo e scriviamolo esplicitando un contatore k del numero di cicli eseguiti: xk-1 , yk-1 k xk , yk Otteniamo le seguenti condizioni: Asserzione iniziale: n ≥ m > 0 Condizione iniziale: x0 = n, y0 = m Condizione di terminazione: xt = yt per qualche t Invariante: MCD(xk , yk) = MCD(n,m) Asserzione finale: R = MCD(xt , yt) = MCD(n,m) In questo caso, non si sa a priori quante volte venga eseguito il ciclo, perchè non si sa quando la condizione che lo controlla diventi vera. Nella Tabella A riportiamo l'esecuzione dell'algoritmo per n = 40 ed m = 16. Tabella A k 0 1 x (n =) 40 24 y (m = ) 16 16 2 3 8 8 16 8 La correttezza dell'algoritmo è dovuta alla seguente proprietà del MCD: Supponendo, senza restrizione, che n ≥ m, se R = MCD(n,m), allora: 33 (a) n = h1 R, m = h2 R (b) Non esiste un numero R' > R tale che per esso valgano le (a). n, m n≥m>0 x <- n y <- m MCD(n,m) = MCD(x,y) x=y x=y x=y x≠y SI x MCD(n,m) = n NO NO x>y SI x>y x<y y <- (y-x) y-x>0 x <- (x-y) x-y>0 Figura 16 – Algoritmo di Euclide basato sulle differenze successive Consideriamo la differenza n - m = R (h1 - h2). Il numero (h 1 - h2) è un numero intero e quindi R è anche un divisore della differenza (n - m). Inoltre, non esisterà un numero R' > R che goda dlla stessa proprietà, perchè non esiste né per n né per m. Quindi, il MCD di due numeri è anche il MCD di uno qualsiasi dei due e della loro differenza. L'Algoritmo di Euclide calcola il MCD tra due numeri riducendo iterativamente il calcolo a quello del MCD tra il minore dei due e la loro differenza. Consideriamo adesso la terminazione e la complessità dell'algoritmo. Abbiamo che: x0 = n, y0 = m Il ciclo viene ripetuto fino a che si raggiunge un t tale per cui xt = yt. Quindi, finchè è k < t , abbiamo: xk = if (xk-1 > yk-1) then (xk-1 - yk-1) else xk-1 yk = if (xk-1 < yk-1) then (yk-1 - xk-1) else yk-1 (9) Le (9) implicano che, ad ogni iterazione, la maggiore tra le due variabili yk e xk diminuisce. Quindi, essendo yk e xk sempre > 0, la loro differenza tende 34 necessariamente a 0, implicando l'esistenza di un t per cui xt = yt. Quindi il programma termina. Il ciclo viene quindi eseguito t volte e la complessità dell'algoritmo è data da: C(n, m) = 2 + 4 t + 1 + 1 = 4 (t + 1) = O(t) Quando m = 1, il ciclo di sottrazioni successive viene fatto tante volte quante sono le unità di n, perché ad ogni ciclo viene tolta 1 unità da x. Quindi, t ≤ n: C(n, m) = O(Max(n, m)) (10) L'Algoritmo di Euclide può essere modificato in modo da avere una complessità minore di quella lineare data dalla (10). Occorre introdurre l'operazione binaria mod : n mod m = Resto della divisione intera tra n e m (11) Per esempio, (12 mod 5) = 2, (23 mod 10) = 3. Osservando che il resto della divisione è il numero che rimane dopo aver tolto il divisore dal dividendo un numero di volte pari al quoziente intero, il ciclo dell'algoritmo di Euclide può essere trasformato sostituendo il mod alla sottrazione; di nuovo, senza restrizione, si può pensare che n ≥ m > 0. Euclide(n,m) while m ≠ 0 do z←n n← m m ← n mod m end R←n Analizzando l'algoritmo in modo analogo al precedente, abbiamo: nk-1 , mk-1 Asserzione iniziale: n ≥ m > 0 Condizione iniziale: n0 = n, m0 = m Condizione di terminazione: mt = 0 k Invariante: MCD(nk , mk) = MCD(n,m) nk , mk Asserzione finale: R = MCD(n,m) Durante il ciclo, abbiamo le seguenti assegnazioni: nk = mk-1 mk = nk-1 mod mk-1 = mk-2 mod mk-1 Usiamo adesso il seguente lemma: (12) Lemma 1 – Per ogni coppia di numeri interi (x, y), si ha: x mod y < x/2. Dimostrazione : Sappiamo che, per definizione, è x mod y < y. Consideriamo adesso due casi: (a) Se y ≤ x/2, allora, per la proprietà transitiva: x mod y < y ≤ x/2 e il lemma è vero. (b) Se x/2 < y < x, allora il quoziente di n/m sarà uguale a 1 e il resto non è altro che la differenza (x - y). Ma, se y > x/2, sarà (x - y) = x mod y < x/2. Quindi, in ogni caso possibile il lemma è vero. ■ Usando il risultato del Lemma 1, dalla (12) abbiamo che: mk = nk-1 mod mk-1 = mk-2 mod mk-1 < mk-2 /2 (13) La (13) ci dice che il valore di m si dimezza (almeno) ogni due passi. Quindi, quando m2t vale 1, si ha al più un ciclo solo da eseguire ancora. Otteniamo: t k = 0 : m0 = m, k = 2 : m2 < m/21, k = 4 : m4 < m/22, …, k = 2 t : m2t < m/2 35 La condizione di trminazione dice che deve essere: t t m/2 = 1 à 2 = m à t = lg2m Quindi, la complessità dell'Algoritmo di Euclide modificato è data da: C(n, m) = O(lg2min(n, m)) (14) La terminazione e la correttezza si dimostrano allo stesso modo che per l'algoritmo basato sulle differenze successive. Analizziamo ora un altro algoritmo classico, denominato Setacci di Eratostene, che genera tutti i numeri primi in sequenza crescente. L'algoritmo associa ad ogni numero primo p j (j ≥ 1) un "setaccio" σj, attraverso le cui maglie "cadono" i numeri che sono multipli di pj : p1 = 2 p2 = 3 p3 = 5 ………… pj ………. pk Viene esaminata la sequenza dei numeri interi dispari (quelli pari non sono primi) e dalla sequenza vengono eliminati tutti i numeri che non sono primi. L'idea dell'algoritmo è quella di far passare i numeri sopra tutti i setacci generati fino a quel momento; se il numero non cade in alcuno dei setacci, allora è il prossimo primo. Nella Figura 17 è riportato il diagramma di flusso. n=3 p1 = 2, p2 = 3, k = 2 p1 Sqrt(3) j=1 pj pk Sqrt(n) primo(n) Sqrt(n) primo(n) j=j+1 pj ≤ Sqrt(n) and (n mod pj ≠ 0) SI n=n+2 NO pj > Sqrt(n) ¬ primo(n) primo(n ) SI k=k+1 pk = n pk = n n mod pj ≠ 0 NO ¬ primo(n) Figura 17– Algoritmo dei Setacci di Eratostene, per la generazione dei numeri primi. 36 L'algoritmo presenta due cicli annidati, il più esterno dei quali non termina, perchè deve trovare tutti i primi. Quello più interno, invece, deve terminare per ogni valore di n. Nell'algoritmo, k denota il numero di primi già trovati al momento in cui si esamina n. Quindi, i primi presenti saranno quelli da p1 a pk. Osserviamo che il ciclo interno si può fermare per due motivi: (a) Il numero n cade in un setaccio ( n è multiplo di un pj) (b) Si sono esaminati tutti i primi utili. Questi primi sono quelli che vanno da p1 a un primo p t tale che pt ≤ [ ], ma pt+1 > [ ]. In altre parole basta esaminare quei primi che non superano . La ragione di questo risiede nel fatto che il numero n, se non è primo, cade nel setaccio che corrisponde al più piccolo dei suoi divisori. Sia p il più piccolo divisore di n; allora esisterà un altro intero divisore r, tale che, per definizione di divisibilità, soddisfa la relazione: n=p r (15) Ma p è il più piccolo divisore di n, quindi: r ≥ p. Sostituendo nella (15), otteniamo: n = p r ≥ p p = p2 -> p≤ Essendo p intero, sarà alla fine: p ≤ [ ]. Consideriamo lo schema dei due cicli, per calcolare gli invarianti. ni-1, ki-1 i pi-1 j≥2 pj ni, ki Ciclo esterno n0 = 3, k0 = 1 Il ciclo non termina ni = n1-1 + 2 k i = if (ni-1 è primo) then (ki-1 + 1) else ki-1 Ciclo interno p1 = 2, k1 = 1 Il ciclo termina per un t tale che pt ≤ [ ] e pt+1 > [ ] pj = next-prime(pj-1) Terminazione del ciclo interno – Siccome ad ogni iterazione pj aumenta, perchè la sequenza dei primi è strettamente crescente, e siccome n1-1 è costante nel ciclo, esiste sicuramente un numero t tale che la condizione di terminazione diventa vera. Correttezza – Dobbiamo dimostrare che l'algoritmo trova tutti e soli i primi esistenti. Osserviamo subito che l'algoritmo non può perdere un numero primo. Infatti, sia p il numero primo perso: in questo caso, p deve essere stato trovato non primo e quindi eliminato dalla sequenza dei numeri dispari, essendo caduto in un setaccio. Questo è impossibile, perchè, non esistendo alcun divisore di p, p non può cadere in alcun setaccio. D'altra parte, potrebbe accadere che un numero n non primo sia dichiarato primo. Questo errore potrebbe verificarsi se, al momento di esaminare n, non fossero stati trovati tutti i setacci occorrenti, e cioè se pk < p t ≤ [ ]. Facciamo vedere che questo non è possibile usando il principio di induzione (Ricordiamo che pk è l'ultimo primo trovato). 37 Passo base: B0 -> Quando si esamina n0 = 3, è k = 1, p1 = 2 > Ipotesi induttiva: Hp : Per tutti i valori dell'indice del ciclo fino a (i-1), vale la relazione pk(i-1)+1 > [ ] , essendo pk(i-1) l'ultimo primo trovato quando si esamina ni-1. Tesi da dimostrare: Ts : Si ha pk(i)+1 > [ ] . Si ha la relazione ni = ni-1 + 2. Quindi, [ ] ≤ [ ] + 1. D'altra parte, per quanto riguarda i primi trovati, essi non cambiano se n i-1 non è primo. In questo caso, sarà ancora pk(i)+1 = pk(i-1)+1 ≥ [ ] , perchè n i non può richiedere un ulteriore primo per la sua analisi, non essendoci due primi che differiscono di meno di due unità (tranne che per 2 e 3, caso che viene esaminato direttamente). Se ni-1 è primo, si ha k(i) = k(i-1) e quindi pk(i)+1 ≥ pk(i-1)+1 + 2 ≥ [ ] . Quindi, l'algoritmo è corretto. Complessità – Per quanto riguarda la complessità, consideriamo la Figura 16. L'algoritmo ha una complessità sostanzialmente dipendente dai due cicli annidati. Dato che il ciclo esterno non si ferma, per valutare la complessità, assumiamo che si vogliano trovare i primi non superiori a un numero N (dispari). Quindi, il ciclo esterno è ripetuto (N-1)/2 volte. Per ogni ripetizione, cui è associato il valore n, si fanno un massimo di t confronti tra n e i setacci, con pt+1 > [ ]. Questi confronti si fanno tutti quando n è primo, ma se ne possono fare di meno quando n non è primo. Siccome cerchiamo la massima complessità (quella del caso peggiore), possiamo dire che vengono fatti sempre t(n) confronti. Ma t(n) è il numero di primi non superiori a [ ]. Siccome vale la relazione: (Numero di primi ≤ M) ~ per ogni M (16) avremo che: t(n) ~ La complessità sarà dunque: C(N) = ≤ = O(N3/2/lnN) Nell'Esempio 25 avevamo introdotto un algoritmo per il calcolo della potenza y = x n, che era lineare con n. Vogliamo adesso vedere se c'è un algoritmo più efficiente di quello. Se la potenza n = 2p e’ un numero pari, possiamo scrivere: xn = x2p = xp * xp (17) Per calcolare xn occorre fare n = 2p moltiplicazioni, ma usando la (17) e memorizzando xp, basta farne (p +1 ). Se p è ancora un numero pari, possiamo ripetere il procedimento. Quindi se n è una potenza di 2, cioè n = 2m posso scrivere l'algoritmo di Figura 18. Analizziamo l'algoritmo. Sia k la variabile che denota il numero di ripetizioni del ciclo. Avremo: Asserzione inziale: base0 = x, esp0 = n Asserzione finale: espt = 1 per un certo t basek = basek-1* basek-1; espk = espk-1/2 38 x, n (x (n = 2m) 0) (m 0) esp = n base = x (xn = baseesp) esp = 1 (esp = 2q) 0) base SI (esp = 2q) (q (q > 0) NO base = base * base (xn = baseesp/2) esp = esp/2 Figura 18 – Calcolo della potenza xn. Avremo quindi: base0 = x base1 = base0 * base0 = x2 base2 = base1 * base1 = (x2)2 = x4 esp0 = n esp1 = esp0/ 2 = n/2 esp2 = esp1/ 2 = n/4 ………………. ………………. 2k basek = basek-1* basek-1 = (x2)2 = x espk = espk-1/ 2 = n/2k Quando ci si ferma, si deve avere: espt = n/2t = 1 à t = lg2n Quindi, il programma termina, perchè esp diminuisce ad ogni ciclo, pur restando positivo. Quando k = t = lg2n, il programma fornisce l'output: t base t = x2 = x2 lg2n = xn Quindi l'algoritmo è corretto. Per quanto riguarda la complessità, essa dipende dal numero di volte che il ciclo viene eseguito: C (n) = O(t) = O(lg2n). Qundo n non è una potenza di 2, in particolare se n è dispari, si può scrivere n = 2p +1 e possiamo scrivere xn = x(2p+1) = x * x (2p) e quindi applicare il procedimento visto per gli n pari al passo successivo, ottenendo l'algoritmo di Figura 19. Analizziamo il ciclo e dimostriamo che la formula xn = (ris × baseesp) ∧ (esp ≥ 0) è l’invariante. basek-1, espk-1, risk-1 Condizione iniziale: base0 = x, esp0 = n, ris0 = 1 Condizione finale: espt = 0 per un certo t k basek-1, espk-1, risk-1 39 Inoltre: basek = if (espk-1 è pari) then( basek-1)2 else basek-1 espk = if (espk-1 è pari) then espk-1/2 else espk-1 risk = if (espk-1 è pari) then risk-1 else risk-1* basek-1 La prima volta che si entra nel ciclo, e cioè quando ris = 1, esp = n e base = x, l’invariante è certamente vero. Supponiamo che lo sia dopo un certo numero di iterazioni e dimostriamo che lo è dopo una ulteriore iterazione, usando il principio di induzione matematica. Da esp ≠ 0 ed esp ≥ 0 deriviamo esp > 0. Nel caso in cui esp sia pari : ris ↔ (baseesp = ris) ↔ (base ↔ baseesp/2) (18) Dopo l’assegnamento base←base*base è vera l’asserzione: (xn = ris) ↔ baseesp/2 e quindi dopo l'assegnazione esp ← esp/2 ritrovo l’invariante. Notiamo che nel caso in cui esp sia dispari, l’equazione (18) non è vera. In questo caso sappiamo che: ris ↔ (baseesp = ris) ↔ base ↔ baseesp-1 Quindi, dopo l’assegnamento ris ← ris * base ottengo l’asserzione: (xn = ris) ↔ baseesp-1 e dopo l’assegnamento esp ← esp-1 ottengo di nuovo l'invariante. Inoltre, poichè n ≥ 0, il programma termina. Quindi all’uscita del ciclo (cioè quando esp = 0) l’invariante diventa: (xn = ris) ↔ (base0 = ris) ↔ (1 = ris) che è esattamente l’asserzione finale. Calcoliamo ora la complessità dell’algoritmo. La complessità è dell’ordine del numero dei cicli effettuati. Per prima cosa vediamo un esempio. Si consideri x13. Con l’algoritmo lineare si effettuano 13 cicli, mentre con questo algoritmo si effettuano cicli con esp = 13, esp = 12, esp = 6, esp = 3, esp = 2, esp = 1 (poi esp = 0 e si esce). Quindi si effettuano 6 cicli. Consideriamo l’algoritmo in generale. Abbiamo visto che il caso migliore si ha m quando l’esponente n è una potenza di 2, diciamo 2 . In questo caso si effettuano m cicli, e cioè un numero di cicli pari a log2n. Il caso peggiore si ha quando n è dispari, e tutte le volte che si sottrae 1 o si divide per 2 il numero risultante è ancora dispari. m In questo caso n = 2 - 1, per cui effettueremo (2 log2n) volte il ciclo. Quindi la complessità è O(log2n).