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).