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 nm. 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 nm. 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 nm. 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é limnnlog(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 sS (tale che s≤x per ogni xS). 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-1N, e poiché s-1<s, si ha s-1S, 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 nn0”, la tesi diventa “P(n) è vero per ogni naturale nn0”. 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-1N, e poiché 1,2,…,s-1<s, si ha 1,2,…,s-1S. 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,r0 (q=quoziente, r=resto) tali che a=bq+r, con r<b. Dimostrazione. Esistenza di q,r: Sia S={x0 / x=a-by, con y intero 0}. Poiché aS, tale S è sottoinsieme non vuoto di N{0}, e sia s il minimo in S. Si ha s0, 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 sb, si avrebbe s-b0, 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 rr1 (per es. r>r1). Allora si avrebbe r-r1>0, r-r1r<b, r-r1=b(q1-q), quindi anche q1-q>0, cioè q1-q1, 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 ab (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 bbn-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 bb0 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 0r<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: 0bn-i<2b . (**) Infatti per i=1 si ha 0bn-1=1<2b (perché b>0). Supponendo vera la (**) per i, dimostriamola per i+1: seguendo l’algoritmo, se bbn-i allora si ha bn-i-1=2(bn-i-b)+an-i-10 (essendo (bn-i-b),an-i-10), e inoltre bn-i-1=2(bn-i-b)+an-i-12(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-10 (essendo bn-i,an-i-10) 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: 0b0<2b Da ciò possiamo dedurre che 0r<b: infatti nell’istruzione 3) vi sono 2 casi e in ambedue verificheremo la tesi: - se bb0 allora si pone r=bo-b, dunque banalmente r0, ma anche r<b perché b0<2b - se b>b0 allora si pone r=b0 , dunque r<b ed inoltre r0 perché b00.