Lezione del 7 marzo 2007
Complessità computazionale di un algoritmo
Tornando alla nostra definizione di TimeA(n), valore che indica il numero di operazioni elementari
eseguite nel caso peggiore dall’algoritmo A su un input di dimensione n, possiamo considerare
TimeA(n) come funzione della dimensione n e stimare la sua velocità di crescita trovando
opportune funzioni g(n) tali che TimeA(n)=O(g(n)): diremo in tal caso che l’algoritmo A ha
complessità computazionale O(g).
In particolare diremo che l’algoritmo A ha complessità computazionale polinomiale se la
funzione TimeA(n) è di ordine polinomiale, e che l’algoritmo A ha complessità computazionale
esponenziale se la funzione TimeA(n) è di ordine esponenziale: nel primo caso f=O(nk) per un
opportuno k intero 0, nel secondo f=O(an) per un’opportuna costante reale a>1.
Gli algoritmi di complessità polinomiale sono considerati più efficienti di quelli di complessità
esponenziale, perché, al crescere della lunghezza n dell’input, il tempo di esecuzione nei primi
cresce molto meno velocemente che nei secondi.
Tuttavia, per un fissato valore della lunghezza n dell’input, un algoritmo di complessità polinomiale
può avere un tempo di esecuzione più alto di un algoritmo di complessità esponenziale.
Esempio: Siano dati l’algoritmo A con TimeA(n)=n7 di complessità polinomiale e l’algoritmo B
con TimeA(n)=2n di complessità esponenziale. Per un input di lunghezza n=32 (quindi un input con
32 cifre binarie) l’algoritmo A esegue un massimo di 327=235 operazioni, mentre l’algoritmo B
esegue un massimo di 232 (quindi un numero inferiore): se ogni operazione è eseguita in 1
milionesimo di secondo, l’algoritmo A impiega un massimo di circa 9 ore e mezza, l’algoritmo B
un massimo di circa 1 ora e 12 minuti. Ma per un input di lunghezza doppia n=64 l’algoritmo A
esegue un massimo di 647 operazioni (circa 52 giorni), ma l’algoritmo B un massimo di 264
operazioni (circa 585.00 anni !!!).
E’ anche vero che un algoritmo di complessità polinomiale può essere egualmente “intrattabile”
perché ha tempi di esecuzione molto alti: se per esempio Time(n)=Cnk allora Time(n)=O(nk) ha
complessità polinomiale, ma se la costante C è per esempio C=1010000 oppure l’esponente k è
k=10000, il numero di operazioni elementari eseguite dall’algoritmo è “astronomico”.
Complessità di alcuni algoritmi aritmetici.
a) Costruiamo un algoritmo A=Somma(x,y) per calcolare la somma x+y di due numeri naturali x,y
dati in input: supponiamo che, se n=L(x),m=L(y) sono le lunghezze binarie degli addendi, si abbia
nm.
L’algoritmo di somma si può eseguire con gli stessi metodi che si usano per sommare numeri
naturali rappresentati in base 10, utilizzando le operazioni elementari di somma sui singoli bits:
- si incolonna la rappresentazione binaria di y sotto quella di x, aggiungendo (se n>m) n-m bits=0
alla sinistra dei bits di y
- si inizializza il valore del riporto c=0
- procedendo da destra verso si sinistra si somma ogni bit di x con il bit dello stesso posto di y (con
l’operazione elementare BitSum di somma di bits), ottenendo ad ogni passo una delle cifre binarie
di x+y e un nuovo valore del riporto c
- se l’ultimo riporto è c=1 si aggiunge a sinistra un ulteriore bit=1 nel risultato x+y
- si esce con output x+y
Per esempio se x=(110011)2, y=(1101) allora:
110011+
001101
1000000
dunque il risultato della somma è x+y=(1000000)2.
Schematizzando l’algoritmo:
1) input x=(an-1an-2…..a1a0)2 , y=(bm-1bm-2…..b1b0)2 con n=L(x) m=L(y)
2) se n>m si pone bm=bm+1=…=bn-1=0
3) c=0
4) per i=0,1…,n-1 si calcola BitSum(ai,bi,c)=(t,c1) e si pone zi=t, c=c1
5) se c=1 si pone zn=1
6) si esce con output x+y=(znzn-1…..z1z0)2
In tale algoritmo si eseguono n operazioni elementari, dunque Time A(n)=O(n), e l’algoritmo ha
complessità polinomiale (lineare).
b) Costruiamo un algoritmo A=Diff(x,y) per calcolare la differenza x-y di due numeri naturali x,y
(con x>y) dati in input: se n=L(x),m=L(y) sono rispettivamente le lunghezze binarie, si avrà
certamente nm.
Si può ricondurre il caso a quello dell’algoritmo Somma(x,y) con il seguente ragionamento:
consideriamo il numero binario y1 ottenuto da y sostituendo ogni bit 0 con 1 e viceversa e poi
aggiungendo a sinistra (n-m) bits =1 (quindi y+y1 è un numero binario con n bits tutti=1 cioè
y+y1=2n-1).
Si ha dunque y+(y1+1)=2n (y1+1 è il cosiddetto “complemento a 2 di y”) da cui x-y=x+(y1+1)-2n.
Quindi per ottenere x-y basta sommare x con il complemento a 2 di y e poi sottrarre 2n al risultato
(il che equivale ad elidere l’ultimo bit =1 a sinistra): poiché nel calcolo di x+(y1+1) (addendi
lunghezza ≤n ) si esegue (vedere algoritmo Somma(x,y)) un numero n di operazioni elementari, si
conclude che TimeA(n)=O(n), e anche questo algoritmo ha complessità polinomiale (lineare).
c) Costruiamo un algoritmo A=Prod(x,y) per calcolare il prodotto x+y di due numeri naturali x,y
dati in input: supponiamo che, se n=L(x),m=L(y) sono le lunghezze binarie dei fattori, si abbia
nm.
L’algoritmo di prodotto si può eseguire con gli stessi metodi che si usano per moltiplicare numeri
naturali rappresentati in base 10, utilizzando le operazioni elementari di somma sui singoli bits.
Si costruiscono (al più) m righe (una riga in meno per ogni bit =0 in y) ognuna consistente nella
copia del numero binario y shiftato opportunamente verso sinistra di un certo numero di posti (il che
equivale ad aggiungere a destra dei bits =0) e si sommano tutte le righe (pensate come numeri
binari): per contare il numero di operazioni elementari, supponiamo di sommare le righe 2 alla volta
(la prima con la seconda, la terza con il risultato della somma della prima con la seconda etc.)
eseguendo in totale un numero ≤m-1 di somme fra numeri di lunghezza ≤n+m, e poiché ognuna di
tali somme corrisponde ad un numero ≤n+m di operazioni elementari di somma di bits, in totale il
numero di operazioni elementari nell’algoritmo è ≤(n+m)(m-1)<n2 ossia TimeA(n)=O(n2), e anche
questo algoritmo ha complessità polinomiale (quadratica).
Per esempio se x=(11011)2, y=(1011) allora:
11011x
1011
11011
110110
11011000
100101001
(si suppone di sommare la prima riga 11011 con la seconda 110110 e poi
sommare il risultato con la terza riga 11011000)
dunque il risultato del prodotto è xy=(100101001)2.
Tale algoritmo di prodotto non è il più efficiente: esiste un algoritmo più sofisticato che ha
complessità O(nlog(n)log(log(n))) “minore” di O(n2) (perché limnnlog(n)log(log(n))/n2=0).
Spesso, per i nostri scopi, ci limiteremo a costruire un algoritmo che risolva un problema senza
indagare se sia il più efficiente in termini di complessità.
Aritmetica dei numeri naturali.
Nell’insieme N dei numeri naturali supporremo note le proprietà delle operazioni aritmetiche
(somma, differenza, prodotto) e dell’ordinamento.
Ricordiamo fra le proprietà dell’insieme N dei numeri naturali il cosiddetto Assioma del buon
ordinamento (o del minimo): in un qualunque sottoinsieme S non vuoto di N esiste sempre un
minimo sS (tale che s≤x per ogni xS).
E’ ovvio che la stessa proprietà vale anche per ogni sottoinsieme non vuoto S di N{0}, in quanto
se S contiene 0, allora 0 è il minimo in S.
Principio di induzione (Ia forma):
Sia P(n) una proprietà relativa ad un generico numero naturale n (si dice anche che P(n) è un
predicato nella variabile n, con n che assume valori in N).
Se:
1) P(1) è vero
2) P(n) vero  P(n+1) vero
allora P(n) è vero per ogni n in N.
Dimostrazione.
Per assurdo sia non vuoto l’insieme S di tutti i naturali n tali che P(n) è falso, e sia s il minimo in S.
Allora s>1, per l’ipotesi 1), dunque s-1N, e poiché s-1<s, si ha s-1S, dunque P(s-1) è vero, ossia
P(s) è vero, per l’ipotesi 2), contraddizione.
Il numero 1 è detto anche base dell’induzione.
Esistono altre versioni dello stesso principio in cui la base dell’induzione può essere un fissato
numero naturale n0: in tale caso l’ipotesi 1) diventa “P(n0) è vero”, l’ipotesi 2) diventa “P(n) vero 
P(n+1) vero per ogni nn0”, la tesi diventa “P(n) è vero per ogni naturale nn0”.
Esiste anche una versione del principio di induzione riferita ad un predicato P(n) in cui la variabile
n non assume tutti i valori naturali, ma solo i valori consecutivi da 1 a un naturale fissato n0, e in
questo caso l’ipotesi 2) diventa “P(n) vero  P(n+1) vero per ogni n=1,….,n0-1”, e la tesi diventa
“P(n) è vero per ogni n=1,2,….,n0” .
Principio di induzione (IIa forma):
Sia P(n) una proprietà relativa ad un generico numero naturale n.
1) P(1) è vero
2) P(k) vero per tutti i k=1,2,…,n-1  P(n) vero
allora P(n) è vero per ogni n in N.
Dimostrazione.
Per assurdo sia non vuoto l’insieme S di tutti i naturali n tali che P(n) è falso, e sia s il minimo in S.
Allora s>1, per l’ipotesi 1), dunque 1,2,…s-1N, e poiché 1,2,…,s-1<s, si ha 1,2,…,s-1S. Dunque
P(1), P(2), ……, P(s-1) sono veri, ossia P(s) è vero, per l’ipotesi 2), contraddizione.
Anche per questa forma del principio esistono altre versioni con base dell’induzione qualunque, o
valide per un numero finito di valori di n.
Teorema dell’algoritmo della divisione per i numeri naturali.
Dati comunque i naturali a,b (a=dividendo, b=divisore), esistono e sono unici i numeri interi q,r0
(q=quoziente, r=resto) tali che a=bq+r, con r<b.
Dimostrazione.
Esistenza di q,r: Sia S={x0 / x=a-by, con y intero 0}. Poiché aS, tale S è sottoinsieme non
vuoto di N{0}, e sia s il minimo in S. Si ha s0, s=a-by, con y intero 0, a=by+s. Se poniamo
q=y, r=s, avremo la tesi se dimostreremo che s<b: se per assurdo fosse sb, si avrebbe s-b0, s-b=ab(y+1)S, s-b<s, contraddizione.
Unicità di q,r: Dimostriamo prima l’unicità di r, supponendo per assurdo che sia a=bq+r=bq1+r1
con q,q1,r,r1 interi 0, r,r1<b, ed rr1 (per es. r>r1). Allora si avrebbe r-r1>0, r-r1r<b, r-r1=b(q1-q),
quindi anche q1-q>0, cioè q1-q1, b(q1-q)b, contraddizione. Quindi è vero che r=r1. Da ciò si
deduce anche 0=r-r1=b(q1-q), ossia q=q1 .
Dimostreremo ora che è possibile trovare quoziente e resto della divisione fra due numeri naturali,
con un algoritmo di complessità polinomiale.
Se a=dividendo, b=divisore , possiamo assumere che sia ab (se a<b la divisione si effettua in modo
banale con quoziente q=0, resto r=a).
Si ha allora certamente n=L(a)m=L(b).
La divisione di a per b si può effettuare con l’usuale algoritmo “scolastico”: il vantaggio di
utilizzare cifre binarie 0,1 consiste nel fatto che la moltiplicazione di un numero per 1 dà come
risultato il numero stesso, mentre per 0 dà come risultato 0.
Facciamo un esempio: siano a=(1101101)2, b=(101)2.
Prima “stacchiamo” (da sinistra) nel divisore a un “segmento” binario (di lunghezza minima) che
non sia inferiore al divisore b (in questo caso è il segmento 110 di lunghezza 3) e sottraiamo da tale
segmento il valore del divisore b, dando nello stesso tempo il valore 1 al primo bit del quoziente q:
1101101 101
101
1
1
In seguito “abbassiamo” il prossimo bit del dividendo, ma poiché otteniamo un numero 11 inferiore
al divisore, diamo il valore 0 al secondo bit del quoziente q e “abbassiamo” un ulteriore bit
ottenendo il numero 111 non inferiore al dividendo: diamo allora il valore 1 al terzo bit del
quoziente e sottraiamo dal numero 111 così ottenuto il valore del divisore:
1101101 101
101
101
111
101
10
Di nuovo “abbassiamo” il prossimo bit del dividendo, ma poiché otteniamo un numero 100
inferiore al divisore, diamo il valore 0 al quarto bit del quoziente q e “abbassiamo” un ulteriore bit
ottenendo il numero 1001 non inferiore al dividendo: diamo allora il valore 1 al quinto bit del
quoziente e sottraiamo dal numero 1001 così ottenuto il valore del divisore:
1101101 101
101
10101
111
101
1001
101
100
Avendo ottenuto un valore 100 che è inferiore al divisore, l’algoritmo si ferma con output quoziente
q=(10101)2 , e resto r=(100)2.
In generale se n=L(a)m=L(b), l’algoritmo esegue un numero di sottrazioni ≤n fra coppie di numeri
di lunghezza ≤n , e poiché ognuna di tali sottrazioni ha (come visto nell’algoritmo Diff(x,y))
complessità lineare O(n), l’algoritmo della divisione ha complessità O(n 2) quadratica, e dunque
polinomiale.
L’algoritmo si può formalizzare nel modo seguente:
Sia n la dimensione di a, cioè il numero di cifre (binarie) di a:
a=an-12n-1+an-22n-2+…..+a121+a020 dove ogni ai=0,1, an-1=1 .
In output avremo le cifre binarie del quoziente q= qn-12n-1+qn-22n-2+…..+q121+q020 e il resto r<b
(non è detto che il quoziente abbia esattamente dimensione n, perché alcune delle prime cifre qi
potranno essere =0).
L’algoritmo esegue i seguenti passi:
1) Si pone bn-1=1
2) Si esegue un ciclo di n-1 iterazioni, per i=1,….,n-1:
se bbn-i allora si pone qn-i=1, bn-i-1=2(bn-i-b)+an-i-1; in caso contrario si pone qn-i=0, bn-i-1=2bn-i+an-i-1
(tale ciclo definisce le cifre qn-1,….,q1, e costruisce i numeri bn-1,…,b0 ; notare che in ogni caso si ha
bn-i-1=2(bn-i-bqn-i)+an-i-1 ad ogni iterazione)
3) Se bb0 allora si pone q0=1, r=bo-b; in caso contrario si pone q0=0, r=bo
(questo passo costruisce l’ultima cifra q0 del quoziente e il resto r; notare che in ogni caso si ha
r=b0-bq0).
Posto q= qn-12n-1+qn-22n-2+…..+q121+q020 , dobbiamo verificare che 0r<b, e che a=bq+r.
Si verifica per induzione che per ogni i>0 si ha:
bn-i=(an-12i-1+an-22i-2+…+an-i+121+an-i20)-b(qn-12i-1+qn-22i-2+…+qn-i+121). (*)
Infatti per i=1 si ha bn-1=1=an-1.
Supponendo vera la (*) per i, dimostriamola per i+1:
bn-(i+1)=bn-i-1=2(bn-i-bqn-i)+an-i-1=(an-12i+an-22i-1+…+an-i21+an-i-120)-b(qn-12i+qn-22i-1+…+qn-i21)
In particolare per i=n si ha:
b0=(an-12n-1+an-22n-2+…..+a121+a020)-b(qn-12n-1+qn-22n-2+…+q121)
da cui:
r=b0-bq0=(an-12n-1+an-22n-2+…..+a121+a020)-b(qn-12n-1+qn-22n-2+…+q121+q020)=a-bq .
Inoltre si verifica per induzione che per ogni i>0 si ha:
0bn-i<2b . (**)
Infatti per i=1 si ha 0bn-1=1<2b (perché b>0).
Supponendo vera la (**) per i, dimostriamola per i+1:
seguendo l’algoritmo, se bbn-i allora si ha bn-i-1=2(bn-i-b)+an-i-10 (essendo (bn-i-b),an-i-10), e inoltre
bn-i-1=2(bn-i-b)+an-i-12(2b-1-b)+an-i-1=2b+(an-i-1-2)<2b (essendo an-i-1=0,1) ;
se invece b>bn-i allora si ha bn-i-1=2bn-i+an-i-10 (essendo bn-i,an-i-10) e inoltre bn-i-1=2bn-i+an-i-1
2(b-1)+ an-i-1=2b+(an-i-1-2)<2b (come sopra).
La (**) in particolare per i=n fornisce:
0b0<2b
Da ciò possiamo dedurre che 0r<b: infatti nell’istruzione 3) vi sono 2 casi e in ambedue
verificheremo la tesi:
- se bb0 allora si pone r=bo-b, dunque banalmente r0, ma anche r<b perché b0<2b
- se b>b0 allora si pone r=b0 , dunque r<b ed inoltre r0 perché b00.