30
Complessit` a computazionale: O-grande Mauro Bianco Tratto da fondamenti di informatica di A. V. Aho e J. D. Ullman. Indice 1 Introduzione 2 2 Analisi asintotica 3 2.1 esempio ................................ 4 3 Confrontare i tempi di esecuzione 5 4 Notazione O grande 6 5 Esempio 7 6 Alcune propriet` a 7 6.1 Legge transitiva ............................ 8 6.2 Notazioni precise ........................... 9 6.3 Regola della somma ......................... 10 6.4 Regola del prodotto ......................... 11 7 Esempio 11 8 Analisi dei programmi 13 8.1 Istruzioni semplici .......................... 13 8.2 Analisi dei blocchi IF-THEN-ELSE ................. 14 8.3 Analisi dei cicli ............................ 14 8.4 Analisi di programmi con procedure ................ 15 9 Esempio 16 9.1 Bubble-sort .............................. 16 9.2 Insertion-sort ............................. 16 10 Analisi di funzioni e subroutine ricorsive 17 1

Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

Embed Size (px)

Citation preview

Page 1: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

Complessita computazionale: O-grande

Mauro Bianco

Tratto da fondamenti di informatica di A. V. Aho e J. D. Ullman.

Indice

1 Introduzione 2

2 Analisi asintotica 32.1 esempio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4

3 Confrontare i tempi di esecuzione 5

4 Notazione O grande 6

5 Esempio 7

6 Alcune proprieta 76.1 Legge transitiva . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86.2 Notazioni precise . . . . . . . . . . . . . . . . . . . . . . . . . . . 96.3 Regola della somma . . . . . . . . . . . . . . . . . . . . . . . . . 106.4 Regola del prodotto . . . . . . . . . . . . . . . . . . . . . . . . . 11

7 Esempio 11

8 Analisi dei programmi 138.1 Istruzioni semplici . . . . . . . . . . . . . . . . . . . . . . . . . . 138.2 Analisi dei blocchi IF-THEN-ELSE . . . . . . . . . . . . . . . . . 148.3 Analisi dei cicli . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148.4 Analisi di programmi con procedure . . . . . . . . . . . . . . . . 15

9 Esempio 169.1 Bubble-sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169.2 Insertion-sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

10 Analisi di funzioni e subroutine ricorsive 17

1

Page 2: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

11 Esempi 2111.1 Ricerca di un valore in un array . . . . . . . . . . . . . . . . . . . 21

11.1.1 Versione 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . 2111.1.2 Versione 2: array ordinato . . . . . . . . . . . . . . . . . . 2111.1.3 Versione 3: array ordinato: ricerca binaria . . . . . . . . . 2211.1.4 Versione 4: array ordinato: ricerca binaria non ricorsiva . 23

11.2 Merge sort: ordinamento per fusione . . . . . . . . . . . . . . . . 2411.2.1 Merge di due array ordinati . . . . . . . . . . . . . . . . . 2411.2.2 Merge sort . . . . . . . . . . . . . . . . . . . . . . . . . . 2611.2.3 Divide et Impera . . . . . . . . . . . . . . . . . . . . . . . 28

Nota : Le parti delimitate da “***” sono da considerarsi facoltative.

1 Introduzione

Se dobbiamo scrivere un programma che dovra essere usato una sola volta suuna piccola quantita di dati e poi scaricato, sceglieremo l’algoritmo piu facile darealizzare che conosciamo, scriveremo il programma, lo proveremo e passeremopoi ad altro. Se, al contrario, abbiamo bisogno di scrivere un programma chedebba essere usato, corretto e modificato da molte persone per un lungo perio-do di tempo, si presentano questioni di tipo diverso. Una e la comprensibilita,o semplicita, dell’algoritmo: gli algoritmi semplici sono migliori per molte ra-gioni, di cui la piu importante e forse che un algoritmo semplice e piu facile darealizzare in modo corretto di uno complicato. E anche meno probabile che ilprogramma che ne risulta contenga errori elusivi che si manifestano solo quandoil programma, dopo essere stato usato per un lungo periodo di tempo, incontraun dato di ingresso inaspettato.

I programmi dovrebbero essere scritti in modo chiaro e documentati con cura,cosı da poter essere corretti e modificati da diverse persone. Se un algoritmoe semplice e comprensibile, e anche piu facile da descrivere. Con l’aiuto diuna buona documentazione, le modifiche al programma originale possono esserecompiute con facilita sia dal suo autore dopo qualche tempo, sia da qualcunaltro.

Quando un programma deve essere eseguito ripetutamente, la sua efficien-za e quella dell’algoritmo su cui e basato diventano questioni importanti. Ingenere, associamo l’efficienza al tempo necessario per l’esecuzione del program-ma, sebbene ci siano anche altre risorse che un programma deve cercare di usarecon parsimonia, quali:

• la quantita di memoria occupata dalle sue variabili;

• la quantita di traffico generata su una rete di calcolatori;

• la quantita di dati che devono essere trasferiti su, o da, un disco.

2

Page 3: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

Per problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma puo essere effettivamente usato o no. Per noi,dunque, l’efficienza di un programma coincidera con il tempo che impiega,misurato in funzione delle dimensioni dei suoi dati di ingresso. Spesso,la comprensibilita e l’efficienza sono obiettivi in conflitto tra loro.

2 Analisi asintotica

Una volta d’accordo sul fatto che si possa valutare un programma misurandoil suo tempo di esecuzione, ci troviamo di fronte al problema di determinare incosa questo consista. I due approcci principali, in tal senso, sono:

1. il benchmarking (o banco di prova);

2. l’analisi.

Del benchmarking diremo solo che quando si confrontano due programmiprogettati per svolgere la stessa serie di compiti, di solito si sviluppa un pic-colo insieme di dati di ingresso tipici, che possa servire come banco di prova(benchmark). In altre parole, conveniamo che i dati del banco di prova sianorappresentativi di tutti quelli su cui il programma sara utilizzato: un program-ma che si comporti bene sul banco di prova presumibilmente si comportera benesu tutti i dati. Per esempio, un banco di prova per la valutazione di programmidi ordinamento potra contenere: un piccolo insieme di numeri, quali le prime20 cifre di π, un insieme di medie dimensioni, quale l’insieme dei codici di avvi-amento postale, infine un insieme di grandi dimensioni, quale l’insieme di tuttii numeri di telefono della Lombardia. Potremmo anche voler controllare che ilprogramma funzioni in modo efficiente (e corretto) su un insieme vuoto di ele-menti da ordinare, su un insieme con un solo elemento, su una lista che sia giaordinata, etc. Questi ultimi vengono detti i casi limite e sono molto importantispecialmente per la correttezza.

Quando si analizza un programma, si inizia col raggruppare i dati di ingres-so secondo la loro dimensione (taglia). Sebbene si possa definire rigorosamentecosa si intenda per dimensione dei dati, cio che noi chiamiamo dimensione deidati puo variare da programma a programma. Per un programma di ordinamen-to, una buona misura della dimensione e il numero degli elementi da ordinare,anche se ogni elemento ha una lunghezza in termini di cifre (o bit) che lo rap-presentano. Per un programma che risolve n equazioni lineari in n incognite,e normale scegliere n come dimensione del problema, anche se i coefficienti delsistema sono n2.

Un caso particolarmente importante e il caso di un algoritmo che valuti seun numero naturale n e primo. Utilizzando l’algoritmo che cerca i divisori delnumero possiamo dire che il numero di divisioni che eseguiamo e proporzionale alnumero n. Il problema e che il numero n e rappresentato da una sequenza di cifrelunga circa logb(n) simboli, dove b e il valore della base usata per rappresentare il

3

Page 4: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

numero n. Il numero di divisioni da eseguire, in questo caso, andrebbe espressocome blog

b(n), ovvero un numero esponenziale rispetto alla lunghezza dell’input.

Per rappresentare il numero di unita di tempo impiegate da un programma,o da un algoritmo, su un generico input di dimensione n conviene utilizzare unafunzione T (n), che chiameremo tempo di esecuzione del programma. Peresempio, un programma puo avere un tempo di esecuzione T (n) = cn, dovec e una costante. Detto altrimenti, il tempo di esecuzione di tale programmae linearmente proporzionale alla dimensione dei dati cui viene applicato. Unprogramma, o algoritmo, di questo tipo viene detto lineare in tempo,o anche,piu semplicemente, lineare. Possiamo pensare al tempo di esecuzione T (n)come al numero di istruzioni Fortran eseguite dal programma, o come al temponecessario a eseguire il programma su un calcolatore fissato, anche se il piu dellevolte non specificheremo l’unita di misura per T (n). In realta, come vedremo nelprossimo paragrafo, ha senso parlare del tempo di esecuzione di un programmasolo come di una costante (ignota) moltiplicata per T (n).

Abbastanza spesso il tempo di esecuzione di un programma dipende da datiparticolari e non solo dalle loro dimensioni, come nel caso dell’algoritmo di con-trollo della primalita di un numero nel caso in cui il numero sia pari rispetto alcaso in cui esso sia primo. In questi casi, definiamo T (n) come il tempo di ese-cuzione nel caso pessimo, (worst case), cioe il massimo tempo di esecuzionesu un qualsiasi input di taglia n.

*** Un’altra comune misura delle prestazioni e Tmed(n),il tempo di ese-cuzione medio del programma sui dati di di taglia n. Il tempo di esecuzionemedio e talvolta una misura piu realistica delle prestazioni che si osservano nel-la pratica, ma e spesso assai piu difficile da calcolare di quello nel caso pessimo.La nozione stessa di tempo esecuzione medio implica, inoltre, che tutti gli inputdi taglia n siano ugualmente probabili, un’assunzione che puo non essere verain una data situazione. Si potrebbe allora specificare meglio le caratteristichestatistiche degli input e valutare con maggiore precisione il tempo di esecuzionedel programma nei casi realistici. ***

2.1 esempio

Stimiamo il tempo di esecuzione del seguente frammento di programma checerca il minimo in un array, partendo dalla posizione i

1. pos = i;

2. do j = i+l, n

3. if (A(j) < A(pos)) then

4. pos=j

5. end if

6. end do

Abbiamo bisogno, per prima cosa, di sviluppare una semplice nozione diunita di tempo. Tratteremo la questione in dettaglio nel seguito, mentre peril momento sara sufficiente adottare il seguente, semplice, schema. Conteremo

4

Page 5: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

un’unita di tempo per ogni volta che viene eseguito un comando di assegna-mento. Nella seconda linea, conteremo un’unita per l’assegnamento a j del suovalore all’inizio del ciclo DO, un’unita per l’incremento di j e un’unita per con-trollare se j > n ogni volta che passiamo per il ciclo. Infine, conteremo un’unitaper il controllo nella terza linea nella istruzione IF.

Prendiamo in considerazione, per prima cosa, il corpo del ciclo interno, cioele linee 3. e 4. . Il controllo della linea 3. viene sempre eseguito, mentrel’assegnamento della linea 4. e eseguito solo se il controllo precedente ha avutosuccesso. Il corpo richiede dunque 1 o 2 unita di tempo, a seconda dei dati delvettore A. Se vogliamo considerare il caso pessimo, possiamo dunque assumereche il corpo richieda 2 unita. Il ciclo viene eseguito n − i volte, ogni voltaeseguendo il corpo (2 unita), incrementando j e controllando se j > n (altre 2unita).

Dunque, il numero di unita di tempo spese per eseguire il ciclo e 4(n − i).A questo valore dobbiamo aggiungere 1 per l’assegnamento del valore iniziale apos della linea 1., 1 per l’assegnamento a j della linea 2. all’inizio del ciclo e1 per il primo controllo j > n della linea 2., che non e collegato con la fine diun’iterazione del ciclo. Il tempo totale utilizzato dal frammento di programma edunque 4(n−i)+3. E naturale considerare n−i+1 quale �dimensione� dei dati,poiche si tratta della lunghezza del vettore A(i:n) , su cui opera il programma.Ponendo m = n − i + 1, il tempo di esecuzione 4(n − i) + 3 puo essere scrittocome 4(n − i + 1) − 1, che e uguale a 4m − 1. Il tempo di esecuzione T (m) edunque 4m− 1.

3 Confrontare i tempi di esecuzione

Supponiamo, per un dato problema, di avere a disposizione un programmalineare A con tempo di esecuzione TA(n) = 100n e un programma B con tempodi esecuzione quadratico TB(n) = 2n2. Supponiamo, ancora, che entrambi itempi siano espressi in millisecondi, quando misurati su un certo calcolatore sudati di dimensione n.

Possiamo vedere che per dati di dimensione inferiore a 50 il programma Be piu veloce del programma A. Quando i dati divengono piu grandi di 50, e ilprogramma A a diventare piu veloce e, da questo punto in poi, piu grossi sonoi dati, maggiore e il vantaggio di A su B. Per dati di dimensione 100, A e duevolte piu veloce di B e quando la dimensione e 1000, A e 20 volte piu veloce.

La forma della funzione del tempo di esecuzione di un programma determi-na, in ultima analisi, le dimensioni massime dei problemi che quel programmapuo risolvere. Al crescere della velocita dei calcolatori, poi, la dimensione deiproblemi che possiamo risolvere con un programma il cui tempo di esecuzionecresce lentamente aumenta di piu di quella dei problemi risolubili con un pro-gramma il cui tempo di esecuzione cresce rapidamente. Nella seguente tabellasono mostrate le massime taglie di problemi risolubili con A e con B in undeterminato tempo.

5

Page 6: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

Secondi Con A Con B1 10 2210 100 70100 1000 2231000 10000 707

4 Notazione O grande

Supponiamo di aver scritto e verificato un programma Fortran e di averselezionato dei dati sui quali eseguirlo. Il tempo di esecuzione del programmasu questi dati dipende ancora da due fattori.

1. La macchina sulla quale il programma viene eseguito. Alcuni calcolatorieseguono le istruzioni piu rapidamente di altri;

2. Il particolare compilatore Fortran usato per generare le istruzioni macchi-na per questo calcolatore. Sulle macchine convenzionali ci sono moltimodi di realizzare uno stesso comando Fortran e il numero delle istruzionimacchina usate per eseguire un certo comando sara diverso da compilatorea compilatore.

Come risultato, non possiamo prendere un programma Fortran con i suoi datie dire: ”Questo compito richiede 3.21 secondi”, a meno che non si conoscano(e molto bene) la macchina e il compilatore che verranno usati. In ogni caso,anche se conoscessimo il programma, i dati, la macchina e il compilatore, la pre-visione esatta del numero di istruzioni che saranno eseguite rimane un compitotroppo complesso. Per questo motivo, il tempo di esecuzione di un programmaviene generalmente indicato con la notazione O-grande, che e concepita perconsentirci di nascondere fattori costanti quali:

1. il numero medio delle istruzioni macchina generate da un particolare com-pilatore;

2. il numero medio delle istruzioni macchina che un particolare calcolatoreesegue in un secondo.

Per esempio, invece di dire, come abbiamo fatto nell’esempio in sezione 2.1,che il frammento di programma un tempo 4m−1 su un vettore di lunghezza m,diremo che impiega un tempo O(m), che si legge O-grande di m, o semplice-mente o di m (o anche ordine di m) e che, in modo informale, significa “unaqualche costante moltiplicata per m”.

La definizione di “una qualche costante moltiplicata per m” ci consente nonsolo di ignorare le costanti sconosciute associate al compilatore e alla macchina,ma anche di formulare alcune ipotesi semplificative. Nell’esempio abbiamo ipo-tizzato che tutti i comandi di assegnamento usino la stessa quantita di tempoe che questa sia necessaria per la verifica della condizione di terminazione delciclo DO, per l’incremento di j a ogni iterazione, per l’assegnamento del valoreiniziale e cosı via. Nessuna di queste ipotesi e vera: ne segue che le costanti

6

Page 7: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

4 e -1, nella formula del tempo di esecuzione T (m) = 4m − 1, rappresentanosolo delle approssimazioni, tanto che sarebbe piu appropriato dire “una qualchecostante moltiplicata per m, piu o meno un’altra costante”.

Sia T (n) il tempo di esecuzione di un programma, misurato come funzionedella taglia n dei dati. Dovendo questa funzione esprimere il tempo di esecuzionedi un programma, assumeremo che:

1. l’argomento n sia un intero non negativo;

2. T (n) sia non negativo per ogni argomento n.

Sia adesso f(n) una funzione definita sugli interi non negativi n. Diciamoche “T (n) e O(f(n))” se esiste un numero naturale n0 e una costantec > 0 tale che per ogni n > n0 si ha T (n) ≤ c · f(n).

5 Esempio

Supponiamo di avere un programma con tempo di esecuzione T (0) = 1,T (1) = 4, T (2) = 9, e, in generale, T (n) = (n + 1)2. Possiamo affermare cheT (n) e O(n2), o che T (n) e quadratico, poiche, nella definizione precedente,possiamo porre c = 4 e n0 = 1. Osserviamo poi che (n + 1)2 ≤ 4n2 per n ≥ 1.Per dimostrarlo, sviluppiamo (n + 1)2 come n2 + 2n + 1. Se n ≥ 1, sappiamoche n ≤ n2 e 1 ≤ n2,e dunque n2 + 2n + 1 ≤ n2 + 2n2 + n2 = 4n2.

Altrimenti, possiamo prendere c = 2 e n0 = 3, poiche, come si puo facilmenteverificare, (n + 1)2 ≤ 2n2 per ogni n ≥ 3. Tuttavia, non possiamo sceglieren0 = 0 per alcun c, poiche con n = 0 dovremmo dimostrare che (0 + 1)2 ≤ c02,cioe , che 1 e minore o uguale a c volte 0. Se scegliamo n0 = 0,non abbiamo vied’uscita. Questo fatto non deve pero preoccuparci, poiche, per dimostrare che(n + 1)2 e O(n2), dobbiamo solo determinare un particolare valore per c e pern0

6 Alcune proprieta

Puo sembrare strano che, nonostante (n+1)2 sia piu grande di n2, possiamoaffermare lo stesso che (n+1)2 e O(n2). In realta, possiamo anche dire che (n+1)2 e O-grande di ogni frazione di n2, per esempio O(n2/100). Per vedere perchecio sia possibile, siano n0 = 1 e c = 400. Allora, se n ≥ 1, sappiamo che (n +1)2 ≤ 400(n2/lOO) = 4n2 per lo stesso ragionamento dell’esempio precedente.Le regole generali che giustificano queste osservazioni sono le seguenti:

1. I fattori costanti non interessano. Per ogni costante positiva d e ognifunzione T (n), T (n) e O(dT (n)), indipendentemente dal fatto che d siaun numero grande o una frazione molto piccola, purche d > O. ***Perdimostrarlo, siano n0 = O e c = l/d. Avremo T (n) ≤ cdT (n), poicheed = 1. Allo stesso modo, se sappiamo che T (n) e O(f(n)), sappiamoanche che T (n) e O(df(n)) per ogni d > O,anche per un d molto piccolo.

7

Page 8: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

Cio avviene perche sappiamo che T (n) ≤ c1f(n) per una costante c1 eper tutti gli n ≥ no. Se scegliamo c = c1/d, possiamo vedere che T (n) ≤c(df(n)) per n ≥ no.***

2. I termini di grado inferiore non interessano. Supponiamo che T (n) siaun polinomio della forma aknk + ak−1n

k−1 + . . . + a2n2 + a1n + a0 con

il coefficiente principale, ak, positivo. Possiamo allora trascurare tutti itermini eccetto il primo (il termine con l’esponente piu alto, k) e, appli-cando la regola (1), ignorare la costante ak, sostituendola con 1. Possiamocosı concludere che T (n) e O(nk). ***A riprova, scegliamo n0 = 1 e siac la somma di tutti i coefficienti ai positivi, per 0 ≤ i ≤ k. Se un coef-ficiente aj e negativo, sicuramente sara ajn

j ≤ 0; se aj e positivo, alloraajn

j ≤ ajnk, per tutti i j < k, purche n ≥ 1. Dunque, T (n) non e piu

grande di nk volte la somma dei coefficienti positivi, ovvero di cnk.***

La definizione di �O-grande� puo trarre in inganno, poiche richiede, dopoaver esaminato T (n) e f(n), di scegliere c e n0 una volta per tutte, mostrandopoi che T (n) ≤ cf(n) per tutti gli n ≥ n0. Non e possibile scegliere c e/o n0 inmodo diverso per ogni valore di n. Per esempio,si incontra di tanto in tanto laseguente “dimostrazione” errata che n2 e O(n): Si scelga n0 = 0 e, per ogni n,sia c = n. Si avra n2 ≤ cn. Si tratta di un ragionamento sbagliato, poiche sideve determinare c una volta per tutte, senza conoscere n.

In realta, possiamo dimostrare che n2 non e O(n) nel modo seguente, percontraddizione: supponiamo che lo sia. Dovrebbero esistere delle costanti c > Oe n0 tali che n2 ≤ cn per ogni n ≥ n0. Dividendo entrambi i membri per n,concludiamo che n ≤ c per ogni n ≥ n0. Se pero scegliamo n1, uguale al piugrande tra 2c e n0, la disuguaglianza n1 > c deve valere (poiche n1 ≥ n0 e n ≤ ce stato supposto valere per ogni n ≥ n0). Concludiamo che n1, che e almeno 2c,deve essere anche minore di c. Dal momento che c deve essere positivo, questoe impossibile. Dunque, le costanti c e n0, che avrebbero mostrato che n2 eraO(n), non esistono, e n2 non e O(n).

Come abbiamo visto nel paragrafo precedente, e possibile semplificare leespressioni O-grande eliminando i fattori costanti e i termini di grado inferiore.L’importanza di tale semplificazione sara chiara quando passeremo all’analisidi programmi. In genere, il tempo di esecuzione dipende da molti comandi oframmenti di programma, ma e normale che solo pochi di questi determininola sua parte principale. Spesso e possibile semplificare di molto le espressionidel tempo di esecuzione, eliminando i termini di grado inferiore e combinandotermini uguali o pressappoco uguali.

6.1 Legge transitiva

Tratteremo, per cominciare, una regola utile per riflettere sulle espressioniO-grande. Una relazione come ≤ e detta transitiva, poiche obbedisce alla legge“se A ≤ B e B ≤ C, allora A ≤ C”. Per esempio, se 2 ≤ 4 e 4 ≤ 6, possiamosicuramente affermare che 2 ≤ 6. La relazione “e O-grande di” e un altro

8

Page 9: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

esempio di relazione transitiva, cioe, se f(n) e O(g(n)) e g(n) e O(h(n)), abbiamoanche che f(n) e O(h(n)). Se f(n) e O(g(n)). Ci sono allora delle costanti n1

e c1 tali che f(n) ≤ c1g(n) per ogni n ≥ n1. Allo stesso modo, se g(n) eO(h(n)), ci sono delle costanti n2 e c2 tali che g(n) ≤ c2h(n) per ogni n ≥ n2.Sia n0 = max(n1, n2), e sia c = c1c2. Quindi, purche n ≥ n0, ne segue chef(n) ≤ clc2h(n), dimostrando che f(n) e O(h(n)).

Sappiamo cheT (n) = 3n5 + lOn4 − 4n3 + n + 1e O(n5). Sappiamo che “i fattori costanti non interessano”, che n5 e O(.0ln5).

Per la legge transitiva di O-grande, sappiamo che T (n) e O(.01n5).Solo fattori costanti e termini minori del termine dominante possono essere

semplificati.

• Se p(n) e q(n) sono polinomi e il grado di q(n) e maggiore o uguale algrado di p(n), allora p(n) e O(q(n)).

• Se il grado di q(n) e minore del grado di p(n), allora p(n) non e O(q(n)).

• Per ogni polinomio p(n) e ogni esponenziale an con a > 1, possiamodimostrare che p(n) e O(an)

• Per ogni polinomio p(n) e ogni esponenziale an con a > 1, possiamodimostrare che an non e O(p(n))

Quindi, se T (n) = 30n2 + 123, allora T (n) e O(n2), (O(n3)), (O(n4)),(O(2n)), etc. T (n), pero, non e O(n).

6.2 Notazioni precise

In genere, ci piacerebbe conoscere il limite superiore O-grande piu “preciso”che si possa ottenere; ovvero, se T (n) e O(n2), vogliamo questo asserto e nonquello, tecnicamente vero ma piu debole che T (n) e O(n3)). D’altra parte, talescelta nasconde delle ambiguita, poiche se preferiamo O(n2) come espressionedel tempo di esecuzione, dovremmo a maggior ragione preferire O(0.5n2), inquanto piu preciso, e ancor piu O(.01n2) e cosı via. Dal momento, tuttavia, chei fattori costanti delle espressioni O-grande non interessano, non c’e in realtaalcuna ragione per cercare di determinare il tempo di esecuzione “piu preciso”diminuendo il fattore costante. Quando e possibile, cercheremo dunque di usareespressioni O-grande con fattore costante 1.

La seguente tabella elenca alcuni dei piu comuni tempi di esecuzione con iloro nomi informali. Notiamo, in particolare, che O(1) e un’abbreviazione per“una costante”, e useremo spesso O(1) con questo significato.

9

Page 10: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

O-Grande Nome informaleO(1) Costante

O(log n) LogaritmicoO(n) Lineare

O(n log n) n log nO(n2) QuadraticoO(n3) CubicoO(2n) Esponenziale

Notiamo che quando parliamo di logaritmi all’interno di una espressioneO-grande non e necessario specificarne la base. Se infatti a e b sono duebasi, loga n = (logb n)(loga b); siccome loga b e una costante, loga n e logb ndifferiscono soltanto per una costante.

Diremo che f(n) e un limite O-grande preciso di T (n) se

1. T (n) e O(f(n))

2. Se T (n) e O(g(n)) allora f(n) e O(g(n))

Il punto due afferma che non puo esistere una funzione che cresca (nel sensodi O-grande) piu di T (n) e meno di f(n).

6.3 Regola della somma

Esiste una regola generale per combinare due espressioni O-grande. Sup-poniamo che un programma sia composto da due parti, la prima con tempoO(n2) e la seconda con tempo O(n3). Possiamo “sommare” questi due limitiO-grande per ottenere il tempo richiesto dall’intero programma. In molti casiinfatti, e questo e uno di quelli, si possono “sommare” delle espressioni O-grandecon la regola della somma.

Supponiamo che T1(n) sia O(f1(n)) e che T2(n) sia O(f2(n)); supponiamoaltresı che f2 cresca meno velocemente di f1 (cioe che f2(n) sia O(f1(n))).Possiamo allora concludere che T1(n) + T2(n) e O(f1(n)).

Sappiamo, infatti, che ci sono delle costanti n1, n2, n3, c1, c2 e c3 tali che:

1. se n ≥ n1, allora T1(n) ≤ c1f1(n);

2. se n ≥ n2, allora T2(n) ≤ c2f2(n);

3. se n ≥ n3, allora f2(n) ≤ c3f1(n).

Sia n0 il piu grande tra nl, n2 e n3, in modo che (1), (2) e (3) valgano quandon ≥ n0. Abbiamo allora, per n ≥ n0

T1(n) + T2(n) ≤ c1f1(n) + c2f2(n).

Se usiamo la (3) come limite superiore di f2(n), possiamo sbarazzarci deltutto di f2(n) e concludere che

T1(n) + T2(n) ≤ c1f1(n) + c2c3f1(n)

10

Page 11: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

.Di conseguenza abbiamo, per ogni n ≥ n0

T1(n) + T2(n) ≤ cf1(n)

dove c = c1 + c2c3. Quindi T1(n) + T2(n) e O(f1(n)).

6.4 Regola del prodotto

Se T (n) e O(f(n)) e S(n) e O(g(n)), allora T (n)S(n) e O(f(n)g(n)). Ladimostrazione e molto semplice: siano n1 ≥ 0 e c1 > 0 tali che T (n) ≤ c1f(n)per ogni n > n1. Siano poi n2 ≥ 0 e c2 > 0 tali che S(n) ≤ c2g(n) per ognin > n2. Scegliendo n0 = max{n1, n2} e c = c1c2 si ottiene che, per ogni n ≥ n0,

T (n)S(n) ≤ c1f(n)c2g(n) = cf(n)g(n).

7 Esempio

Consideriamo il seguente frammento di programma che inizializza un arraybidimensionale A ad essere una matrice identita n× n.

( ) integer, parameter :: nmax=1000

( ) integer :: i,j,n

( ) real, dimension(nmax,nmax) :: A

(1) read (*,*) n

(2) do i=1, n

(3) do j=1, n

(4) A(i,j)=0.0

( ) end do

( ) end do

(5) do i=1

(6) A(i,i) = 1.0

( ) end do

La linea (1), che legge n, richiede tempo O(1), cioe costante, indipendente-mente dal valore di n. Anche il comando di assegnamento della linea (6) richiedetempo O(1), e il ciclo costituito dalle linee (5) e (6) viene eseguito esattamenten volte, per un tempo totale O(n). Analogamente, il comando della linea (4)richiede tempo 0(1). Il ciclo costituito dalle linee (3) e (4) viene eseguito n volte,per un tempo totale O(n). Il ciclo piu esterno, le linee (2), (3) e (4), viene ese-guito n volte, richiedendo tempo O(n) per ogni iterazione, per un tempo totaleO(n2).

Dunque, il tempo totale richiesto dal programma e O(1) + O(n2) + O(n),rispettivamente per il comando (1), per il ciclo delle linee (2), (3) e (4) e per ilciclo delle linee (5) e (6). Visto che qualsiasi funzione O(1) e anche O(n2) e chequalsiasi e funzione O(n) e anche O(n2) il tempo complessivo e n2.

11

Page 12: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

Il programma trascorre quasi tutto il tempo nelle rige (2), (3) e (4). Lerighe non numerate non devono essere conteggiate, in quanto esse servono soloal compilatore per tradurre correttamente il programma in linguaggio macchinae non vengono di fatto eseguite. Esse sono tuttavia sempre un numero costantee potrebbero essere incluse nella costante della riga (1).

Questo esempio costituisce un’applicazione della regola per la quale i terminidi grado inferiore non interessano, perche abbiamo eliminato i termini 1 e n,che sono polinomi di grado inferiore a n2. La regola della somma ci consente,tuttavia, di fare di piu. Se abbiamo un numero costante di termini che, a meno diO-grande, coincidono (per esempio una sequenza di 10 comandi di assegnamentoognuno dei quali richieda tempo O(1), possiamo allora �sommare� i dieci O(1)per ottenere O(1). In modo meno formale, la somma di 10 costanti e ancorauna costante.

Dobbiamo, pero, stare molto attenti a non confondere �un numero costante� ditermini O(1) con un numero di termini che varia al variare della dimensione deidati. Si potrebbe essere indotti a pensare, per esempio, che il tempo necessarioa eseguire una volta il ciclo delle linee (5) e (6) dell’esempio e O(1). Poicheil numero di volte che eseguiamo il corpo del ciclo e n, il tempo di esecuzionetotale delle linee (5) e (6) e O(1) + O(1) + O(1) + ... + O(1) (n volte). La regoladella somma ci dice che la somma di due O(1) e O(1) e per induzione si puo di-mostrare che la somma di un numero costante di O(1) e O(1). Nel programma,tuttavia, n non e una costante, poiche varia al variare della dimensione dei dati.Nessuna applicazione ripetuta della regola della somma ci permette di assegnareun valore costante a n volte O(1). Se riflettiamo sulla questione ci renderemoconto che se sommiamo n termini, ciascuno dei quali e una costante c, il risulta-to sara cn, una funzione che e O(n), che costituisce il tempo di esecuzione realedelle linee (5) e (6).

*** Sarebbe bello se due qualsiasi funzioni f(n) e g(n) potessero essereconfrontate mediante un O-grande, cioe se potessimo sempre affermare che f(n)e O(g(n)), oppure che g(n) e O(f(n)) (o entrambe le cose) Sfortunatamenteci sono coppie di funzioni incommensurabili, nessuna delle quali e O-grandedell’altra.

Consideriamo ad esempio la funzione f(n) che e n per n dispari e n2 pern pari (cioe f(1) = 1, f(2) = 4, f(3) = 3, f(4) = 16, f(5) = 5 e cosı via).Analogamente, poniamo g(n) uguale a n2 se n e dispari e g(n) uguale a n sen e pari. In questo caso, f(n) non puo essere O(g(n)), a causa degli n pari;come abbiamo osservato precedentemente infatti, n2 e definitivamente diversoda O(n). Analogamente, g(n) non puo essere O(f(n)), a causa degli n dispari,perche i valori di g superano i valori corrispondenti di f . ***

12

Page 13: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

8 Analisi dei programmi

8.1 Istruzioni semplici

Ogni istruzione di tipo assegnazione, espressione, I/O, che non contenganochiamate a funzioni o subroutine, prendono tempo costante O(1). In fortran sideve prestare molta attenzione al fatto che alcune istruzioni scritte su una solariga prendono in realta tempo non costante.

Si pensi ad esempio alla stampa di un array con la seguente rigawrite (*,*) (A(i), i=1,n,2)

Questa riga contiene un do implicito e produce la stampa di circa n/2 valori.Questa riga prende quindi tempo O(n). Lo stesso ragionamento vale per leinizializzazioni di array. In generale sono proprio gli array a causare la maggioredifficolta di analisi nei programmi fortran. Ad esempio, se A e B sono due arraydi lunghezza n, le seguenti righe richiedono tutte tempo lineare in n:

1. A(1:n:2)=A(1:n:2)+A(2:n:2)-B(1:n:2)

2. A=B

Le seguenti invece prendono tempo costante:

1. A(i)=A(i+1)+A(i-1)-B(i)

2. A(1:10:2)=A(1:10:2)+A(2:10:2)-B(1:10:2)

3. A(1:5)=B(1:5)

La riga nel punto 2 e O(1) se il numero 10 non ha alcuna relazione con n.Ovvero se vale sempre che 10 e in relazione con n (ad esempio se e sempre veroche n = 10, o che n = 20, o che n = 210) allora non possiamo piu affermare cheil tempo di esecuzione e O(1). Ad esempio se n = 210, sempre e in ogni caso,allora la riga prendera tempo log(n). Se il 10 non ha relazione con n, ovvero sen e una variabile il cui valore deve solo essere maggiore di 10, e puo assumerequalsiasi valore, allora il tempo di quella riga e O(1). Lo stesso vale per la rigaal punto 3.

Una sequenza di lunghezza indipendente da n di istruzioni che prendonotempo costante prende tempo costante!

Il motivo di questa maggiore difficolta, di fatto non presente in altri lin-guaggi di programmazione meno versatili, e che il calcolatore su cui eseguiamoi programmi e una macchina di von Neumann. Il modello di von Neumannprevede che ogni operazione viene eseguita in una unita di tempo e che ques-ta operazione sia eseguita su una quantita di dati costante (O(1)). Possiamocioe eseguire la somma di due numeri o copiare un numero da una locazione dimemoria ad un’altra, ma non possiamo agire direttamente su sequenze di daticon una sola operazione. Per eseguire operazioni su sequenze di dati si devonoeseguire piu operazioni, tipicamente organizzate in loop.

13

Page 14: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

8.2 Analisi dei blocchi IF-THEN-ELSE

Un comando condizionale della forma

if (<condizione>) then

<parte-H>

else

<parte-else>

end if

e formato da:

1. una condizione da verificare;

2. una parte then, che viene eseguita solo se la condizione e vera;

3. una parte else (facoltativa), che viene eseguita solo se la condizione efalsa.

Per valutare la condizione e necessario un tempo O(1), a meno che non cisiano chiamate a funzione. Indipendentemente dalla sua complessita, se non cisono chiamate a funzione, la valutazione della condizione richiede alla macchinadi eseguire soltanto un numero costante di operazioni aritmetiche, di accessi astrutture dati, di confronti tra valori e di operazioni logiche.

Supponiamo che nella condizione non compaiano chiamate a funzione e chele parti then ed else richiedano (come limite superiore) tempo, rispettivamente,f(n) e g(n). Una tra f(n) e g(n) (o entrambe) potrebbero essere O; Se f(n) =O(g(n)), possiamo assumere che O(g(n)) sia un limite superiore del tempo diesecuzione del comando condizionale. La ragione sta nel fatto che:

1. possiamo trascurare lo O(1) per la condizione;

2. se la parte else viene eseguita, sappiamo che g(n) e un limite del tempodi esecuzione;

3. se viene eseguita la parte then invece della parte else, il tempo di ese-cuzione sara O(g(n)) poiche f(n) e O(g(n)).

Analogo e il caso in cui g(n) e O(f(n)). Il problema si presenta quando f eg non siano l’una O-grande dell’altra. Sappiamo che sara eseguita la parte then

o la parte else, ma non entrambe, e cosı un sicuro limite superiore del tempodi esecuzione sara il piu grande tra f(n) e g(n). Dobbiamo quindi scrivere iltempo di esecuzione del comando condizionale come (max(f(n), g(n))).

8.3 Analisi dei cicli

Il tempo di esecuzione di un ciclo e O(1) per l’inizializzazione del ciclo, piuO(1) per ogni iterazione per il controllo del ciclo, piu la somma del tempo diesecuzione di ogni esecuzione del corpo del ciclo. Chiaramente i due O(1)possono essere ignorati se il corpo del ciclo e presente.

14

Page 15: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

Risulta necessario stabilire con la dovuta precisione il numero di iterazionidel ciclo in funzione della taglia dell’ingresso. Per i cicli DO iterativi questo puonon essere banale, ma e tipicamente piu complesso per i cicli WHILE.

Se il corpo del ciclo richiede lo stesso tempo indipendentemente dall’indicedell’iterazione allora basta moltiplicare tale tempo per il numero di iterazioni(si pensi ad esempio al caso di bubble-sort). In altri casi il tempo puo esseredipendente dall’indice dell’iterazione, come nel caso di insertion-sort.

8.4 Analisi di programmi con procedure

Il caso di analisi di programmi con chiamate a subroutine e funzioni, ladifficolta non sembra insuperabile. Il problema risiede nel fatto che il tempodi esecuzione di una subroutine o di una funzione, dipende dalla taglia del suoinput (che sono i suoi argomenti), e non e l’input del programma stesso.

Se poi una subroutine P chiama la subroutine Q, dobbiamo mettere in re-lazione la misura della dimensione degli argomenti di Q con quella usata per P.Non esiste in questo caso una regola generale utile, ma di caso in caso si possonogeneralmente ottenere delle stime del tempo di esecuzione di una subroutine.

Supponiamo di aver determinato che un buon limite superiore per il tempo diesecuzione di una subroutine P e O(f(n)), dove n e una misura della dimensionedegli argomenti di P. Nelle procedure che chiamano P possiamo allora impiegarela stime O(f(n)) per quei comandi che esprimono una chiamata a P. Possiamodunque includere tale tempo in un blocco di istruzioni che contiene la chiamata,nel corpo di un ciclo che la contiene o in una delle due parti (then o else) diun comando condizionale che la contiene.

Il caso delle funzioni e analogo, ma bisogna tener conto del fatto che questepossono comparire all’interno di assegnamenti, o di condizioni, e che, in ununico assegnamento o condizione, possono essere presenti piu chiamate. Perun assegnamento o comando di scrittura che contenga una o piu chiamate difunzione prendiamo come limitazione del tempo di esecuzione la somma dellelimitazioni di ogni chiamata. Quando una chiamata di funzione con limitazionesuperiore O(f(n)) compare in una condizione, nell’inizializzazione o nel limitefinale di un ciclo DO, il tempo di quella chiamata di funzione viene consideratosecondo lo schema che segue.

1. Se la chiamata di funzione compare nella condizione di un ciclo while, sisomma f(n) alla limitazione del tempo di ciascuna iterazione.

2. Se la chiamata di funzione compare nell’inizializzazione o nel limite finaledi un ciclo DO, si somma f(n) al costo totale del ciclo, perche essa vienevalutata solo una volta.

3. Se la chiamata di funzione compare nella condizione di un comando con-dizionale, si somma f(n) al costo del comando.

15

Page 16: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

9 Esempio

9.1 Bubble-sort

Analizziamo la seguente subroutine che realizza il bubble sort:

( ) subroutine bubblesort(dati, n) ! Bubble sort

( ) implicit none

( ) integer, intent(in) :: n

( ) integer, intent(inout),dimension(n) :: dati

( ) integer :: i, j, tmp

(1) do i=1,n-1 ! Esegue n-1 scansioni

(2) do j=1,n-1 ! Esegue una scansione

(3) if (dati(j)>dati(j+1)) then

(4) tmp=dati(j+1) ! SWAP

(5) dati(j+1)=dati(j)

(6) dati(j)=tmp

( ) end if

( ) end do

( ) end do

Per analizzare l’algoritmo di bubble-sort partiamo dall’interno, ovvero dallerighe (3). . . (6). La riga (3) contiene un confronto senza chiamate a funzione,quindi richiede tempo costante. La parte then esegue uno swap, che richiede treassegnamenti e quindi un tempo costante. La parte else non e presente e quindirichiede tempo nullo. Nel caso peggiore il tempo da considerare e il tempo dellaparte then. Complessivamente il corpo del ciclo interno prende tempo O(1).

Il ciclo a riga (2) viene eseguito n−1 volte, mentre il corpo del ciclo richiedetempo O(1). n− 1 e O(n). Il tempo complessivo e dunque O(n) (O(1) ·O(n)).

Il ciclo in riga (1) viene eseguito n − 1 volte. Il corpo di quel ciclo prendetempo O(n) e quindi il tempo totale di esecuzione e O(n2).

Altro modo per calcolare questo limite e contare quante volte viene eseguitoil corpo del ciclo piu interno ((3). . . (6)), che prende tempo O(1). Esso vieneeseguito (n− 1)2 volte, quindi O(n2).

9.2 Insertion-sort

Analizziamo la seguente subroutine che realizza l’insertion sort:

( ) subroutine sort(dati, n) ! Insertion sort

( ) implicit none

( ) integer, intent(in) :: n

( ) integer, intent(inout), dimension(n) :: dati

( ) integer :: i, j, tmp, min, pos

(1) do i=1,n-1

16

Page 17: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

(2) pos=i

(3) do j=i+1,n ! Cerco il minimo

(4) if (dati(j)<dati(pos)) then

(5) pos=j ! Aggiorno la posizione

( ) end if

( ) end do

(6) tmp=dati(i) ! Algoritmo di SWAP: scambio di dati

(7) dati(i)=dati(pos)

(8) dati(pos)=tmp

( ) end do

( ) end subroutine sort

Anche in questo caso partiamo dall’interno. Le righe (4) e (5) prendonotempo O(1), mentre il ciclo a riga (3) viene eseguito n− i volte, dove, per ora inon e quantificato e quindi non possiamo fare ipotesi. Ad esempio, per quantone sappiamo a questo punto dell’analisi, i potrebbe essere sempre maggioredi n e il ciclo potrebbe non essere mai eseguito. Quindi per ora scriviamoO(n− i)O(1) = O(n− i).

Le istruzioni da (2) a (8) sono due blocchi di istruzioni elementari e un ciclo.Quindi il tempo di esecuzione di queste istruzioni e O(1) + O(n− i).

Il ciclo piu esterno alla riga (1), viene eseguito n−1 volte. Si devono sommarequindi i tempi di esecuzione di ogni iterazione, dato che questo dipende da i, lavariabile di indice del ciclo piu esterno. Sara quindi:

(O(1) + O(n− 1)) + (O(1) + O(n− 2)) + . . . + (O(1) + O(2)) + (O(1) + O(1)) =

= (O(1) + O(1) + . . . + O(1)) + (O(1) + O(2) + . . . + O(n− 1) =

= O(n) + c

n−1∑

i=1

i =

= O(n) + c(n− 1)n

2=

= O(n) + O(n2) =

= O(n2)

Gli algoritmi di insertion sort e quello di bubble sort hanno la stessa com-plessita asintotica. Questo vuol dire che se ci sono differenze tra i tempi diesecuzione dell’uno e dell’altro, questi non si amplificano sensibilmente all’au-mentare della taglia del problema. Non e poi detto che su tutte le piattaforme(calcolatore+compilatore) e con tutti gli input possibili uno dei superi semprel’altro in termini di prestazioni. Sono quindi confrontabili.

10 Analisi di funzioni e subroutine ricorsive

Per determinare il tempo di esecuzione di una subroutine o una funzioneche chiama se stessa in modo ricorsivo e necessario un lavoro maggiore di quello

17

Page 18: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

necessario per una procedura non ricorsiva. Per l’analisi di una subroutinericorsiva, infatti, si deve associare a ogni subroutine P di un programma untempo di esecuzione, incognito, TP (n), che definisca il tempo di esecuzione di Pin funzione di n, la taglia degli argomenti di P. Stabiliamo, poi, una definizioneinduttiva, detta relazione di ricorrenza per TP (n), che metta in relazioneTP (n) con una funzione della forma TQ(k), corrispondente alle altre procedureQ del programma e alle relative dimensioni, k, dei loro argomenti. Se P edirettamente ricorsiva, uno o piu di questi Q coincidera con P.

Il valore di TP (n) e normalmente stabilito per induzione su n, la dimensionedell’argomento. E dunque necessario scegliere una nozione di dimensione del-l’argomento per garantire che, al procedere della ricorsione, le procedure sianochiamate con argomenti progressivamente piu piccoli. Possiamo ora consideraredue casi.

1. La dimensione dell’argomento e abbastanza piccola perche P non effet-tui alcuna chiamata ricorsiva. Questo caso corrisponde alla base delladefinizione induttiva di TP (n).

2. La dimensione dell’argomento e abbastanza grande da causare chiamatericorsive. Assumiamo, tuttavia, che qualsiasi chiamata ricorsiva fatta da P,sia a se stessa sia a qualche altra procedura Q, avvenga con argomenti piupiccoli. Questo caso corrisponde al passo induttivo della definizionedi TP (n).

La relazione di ricorrenza che definisce TP (n) deriva dall’esame del codicedella procedura P nel modo seguente.

1. Per ogni chiamata di procedura Q, o uso di funzione Q all’interno diun’espressione (si osservi che Q puo coincidere con P), usiamo TQ(k) qualetempo di esecuzione della chiamata, dove k e la misura della dimensionedegli argomenti nella chiamata.

2. Determiniamo il tempo di esecuzione del corpo della procedura P con lastessa tecnica usata nei paragrafi precedenti, ma lasciando i termini dellaforma TQ(k) come funzioni incognite e non come funzioni concrete qualin2, n, etc. Tali termini, in genere, non possono essere combinati confunzioni concrete mediante tecniche di semplificazione come la regola perla sommatoria. Dobbiamo analizzare P due volte: la prima nell’ipotesiche la dimensione n dell’argomento di P sia abbastanza piccola da nongenerare alcuna chiamata ricorsiva, la seconda nell’ipotesi contraria chen non sia cosı piccolo. In questo modo otteniamo due espressioni per iltempo di esecuzione di P: una che costituisce la base della relazione diricorrenza per TP (n) e una che costitusce la parte induttiva.

3. Nelle espressioni per il tempo di esecuzione di P cosı ottenute, rimpiazz-iamo i termini O-grande, quali O(f(n)), con una specifica costantemoltiplicata per la funzione in questione: per esempio, cf(n).

18

Page 19: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

4. Se a e un valore di base per la dimensione di ingresso, poniamo TP (a)uguale all’espressione che risulta dal passo (3) nell’ipotesi che non ci sianochiamate ricorsive. Poniamo inoltre TP (n) uguale all’espressione ottenutain (3) nel caso in cui n non sia un valore di base.

Il tempo di esecuzione dell’intera procedura viene determinato risolvendoquesta relazione di ricorrenza.

Vediamo un esempio: consideriamo la seguente funzione ricorsiva:

( ) integer recursive function fattoriale(n) result(f)

( ) ! Calcola il fattoriale di n ricorsivamente

( ) integer, intent(in) :: n

( )

(1) if (n<=1) then

(2) f=1

( ) else

(3) f=n*fattoriale(n-1)

( ) end if

( )

( ) end function fattoriale

Vediamo cosa succede quando n > 1. In questo caso, la condizione della linea(1) e falsa e solo le linee (1) e (3) verranno eseguite. La linea (1) richiede tempoO(1), mentre la linea (3) richiede O(1) per la moltiplicazione e l’assegnamentoe T (n − 1) per la chiamata ricorsiva a fattoriale. Per n > 1, cioe, il tempodi esecuzione di fattotiale e O(1) + T (n− 1). Possiamo dunque definire T (n)mediante la seguente relazione di ricorrenza.

BASE. T (1) = 0(1).INDUZIONE. T (n) = 0(1) + T (n− 1), per n > 1.Inventiamo adesso dei nuovi simboli di costante da mettere al posto delle

costanti nascoste all’interno delle diverse espressioni O-grande, cosı come sug-gerisce la regola 3. In questo caso, sostituiamo O(1) nella base con la costantea e O(1) nell’induzione con la costante b, ottenendo la seguente relazione diricorrenza.

BASE. T (1) = a.INDUZIONE. T (n) = b + T (n− 1), per n > 1.Dobbiamo adesso risolvere questa relazione di ricorrenza per T(n). Il calcolo

dei primi valori e facile, poiche T (1) = a per la base; per la regola induttiva,abbiamo

T (2) = b + T (1) = a + b.

Applicando ancora tale regola, otteniamo

T (3) = b + T (2) = b + (a + b) = a + 2b

e quindi

19

Page 20: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

T (4) = b + T (3) = b + (a + 2b) = a + 3b.

A questo punto, non sara sorprendente ipotizzare T (n) = a + (n − l)b, perogni n ≥ 1. E infatti un metodo comune per trattare le relazioni di ricorrenzae quello di provare a calcolare alcuni valori, ipotizzare una soluzione e quindidimostrare che l’ipotesi e corretta mediante una dimostrazione per induzione.

Qui, tuttavia, possiamo ricavare la soluzione in modo diretto applicandoripetutamente una sostituzione. Dapprima sostituiamo, nell’equazione ricorsivala variabile n con m, ottenendo

T (m) = b + T (m− 1), per m > 1

Ora sviluppiamo la ricorrenza come segue:

T (m) = b + T (m− 1) =

= b + (b + T (m− 2)) = 2b + T (m− 2) =

= b + (b + (b + T (m− 3))) = 3b + T (m− 3)

(1)

Continuiamo cosı, sostituendo ogni volta T (n − i) con b + T (n − i − 1), fino aottenere T (1). A questo punto, abbiamo l’equazione T (n) = (n − 1)b + T (1).Possiamo ora usare la base per sostituire T (1) con a e concludere che T (n) =a + (n− 1)b.

Per essere davvero rigorosi dobbiamo dimostrare per induzione l’intuizioneche abbiamo avuto su cosa accade quando sostituiamo ripetutamente T (n− i).

Possiamo generalizzare la relazione di ricorrenza della precedente sezioneammettendo la somma di una qualche funzione g(n) al posto della costante bdella regola induttiva. Possiamo scrivere tale forma come

BASE. T (1) = a.INDUZIONE. T (n) = T (n− 1) + g(n), per n > 1.Si tratta di una forma che si presenta ogni qual volta si abbia una procedura

ricorsiva che impiega tempo g(n) e che richiama poi se stessa su un argomentopiu piccolo di 1 dell’argomento su cui era stata chiamata la procedura.

Risolviamo la relazione di ricorrenza per sostituzioni successive. Scriviamoper prima cosa la regola induttiva con argomento m, cioe T (m) = T (m− 1) +g(m), e cominciamo poi a mettere questa espressione al posto di T nel membrodestro della regola induttiva originale, ottenendo la successione di espressioni

T (n) = T (n− 1) + g(n)

= T (n− 2) + g(n− l) + g(n)

= T (n− 3) + g(n− 2) + g(n− 1) + g(n)

= T (n− i) + g(n− i + 1) + g(n− i + 2) + . . . + g(n− 1) + g(n).

(2)

Possiamo alla fine dimostrare per induzione su i, per i = 1, 2, . . . , n− 1, che

20

Page 21: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

T (n) = T (n− i) +

i−1∑

j=0

g(n− j).

Dal momento che vogliamo scegliere un valore di i in modo tale che T (n− i)sia coperto dalla base, prendiamo i = n − 1 Siccome T (1) = a, otteniamo

T (n) = a+∑n−1

j=0 g(n− j). Detto altrimenti, T (n) e la costante a piu la sommadi tutti i valori di g da 2 a n, ovvero a+g(2)+ ...+g(n). A meno che tutti i g(j)siano nulli, il termine a sara trascurabile quando tradurremo questa espressionein una in notazione O-grande, e dunque, in generale, abbiamo bisogno solo dellasomma dei g(i).

11 Esempi

11.1 Ricerca di un valore in un array

11.1.1 Versione 1

( ) subroutine cerca(val, dat, len, ind)

( ) implicit none

( ) integer, intent(in):: val

( ) integer, intent(in), dimension(len):: dat

( ) integer, intent(in) :: len

( ) integer, intent(out) :: ind

( )

(1) do ind=1,len

(2) if (dat(ind)==val) return

( ) end do

( )

(3) ind=0

( )

( ) end subroutine cerca

La riga (2) che prende tempo costante, viene eseguita len volte se val nonviene trovato, altrimenti un numero di volte uguale all’indice della prima occor-renza di val nell’array dat. Nel caso peggiore, quindi, il ciclo ciene eseguito len

volte. L’algoritmo e dunque O(n), dove n e il numero di elementi dell’array.

11.1.2 Versione 2: array ordinato

( ) subroutine cerca(val, dat, len, ind)

( ) implicit none

( ) integer, intent(in):: val

( ) integer, intent(in), dimension(len):: dat

( ) integer, intent(in) :: len

( ) integer, intent(out) :: ind

21

Page 22: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

( )

(1) do ind=1,len

(2) if (dat(ind)==val) return

(3) if (dat(ind)>val) exit

( ) end do

( )

(4) ind = 0

( )

( ) end subroutine cerca

Il corpo del ciclo (righe (2) e (3)) richiede tempo costante e viene eseguito alpiu len volte. Anche in questo caso la complessita e O(n). Nel caso medio, incui ogni sequenza di input e possibile e il valore cercato ha la stessa distribuzionestatistica degli elementi dell’array, il tempo medio e la meta del caso precedente.Notate che questo comunque non cambia la complessita asintotica.

11.1.3 Versione 3: array ordinato: ricerca binaria

( ) recursive subroutine cerca(val, dat, len, ind)

( ) implicit none

( ) integer, intent(in):: val

( ) integer, intent(in), dimension(len):: dat

( ) integer, intent(in) :: len

( ) integer, intent(out) :: ind

( ) integer :: i

( )

(1) if (len>=1) then

(2) i=(1+len)/2

(3) if (dat(i)>val) then

(4) call cerca(val, dat(1:i-1), i-1, ind)

(5) else if (dat(i)<val) then

(6) call cerca(val, dat(i+1:len), len-i, ind)

(7) if (ind>0) ind=ind+i

( ) else

(8) ind=i

( ) end if

( ) else

(9) ind=0

( ) end if

( )

( ) end subroutine cerca

Questa versione, chiamata ricerca binaria e ricorsiva e la possiamo anal-izzare con le tecniche illustrate precedentemente. Se l’array ha lunghezza 1, iltempo della ricerca e O(1). Altrimenti, se il valore cercato non e quello chesta nella meta dell’array, allora il tempo della ricerca e il O(1) piu il tempo dicercare in meta array. Se il valore e quello che sta nella meta dell’array, allora

22

Page 23: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

la subroutine esce. Il caso peggiore quindi e quello in cui il valore cercato none presente nell’array. Il tempo, nel caso peggiore puo quindi essere scritto comesegue:

BASE. T (1) = O(1)INDUZIONE. T (n) = T (n/2) + O(1)Ripetendo i passi fatti nella sezione precedente poniamoBASE. T (1) = aINDUZIONE. T (n) = T (n/2) + bOra possiamo espandere la ricorrenza

T (m) = b + T (m/2) =

= b + (b + T (m

4)) = 2b + T (

m

4) =

= b + (b + (b + T (m

8))) = 3b + T (

m

8) =

= . . . = ib + T (m

2i)

(3)

Il procedimento si ferma quando m/2i e minore o uguale a 1, ovvero quando

2i ≥ m

il che avviene quandoi ≥ log m

(In informatica, se la base del logaritmo e omessa si intende essere 2). Nel casopeggiore, quindi, quando m/2i vale 1 si ottiene:

T (m) = b log m + a = O(log m)

Il tempo di esecuzione e O(log n), asintoticamente enormemente piu basso chenel caso precedente.

11.1.4 Versione 4: array ordinato: ricerca binaria non ricorsiva

( ) subroutine cerca(val, dat, len, ind)

( ) implicit none

( ) integer, intent(in):: val

( ) integer, intent(in), dimension(len):: dat

( ) integer, intent(in) :: len

( ) integer, intent(out) :: ind

( ) integer :: b,e

( )

(1 ) b=1

(2 ) e=len

(3 ) do while (e-b>=0)

(4 ) ind=(b+e)/2

23

Page 24: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

(5 ) if (val<dat(ind)) then

(6 ) e=ind-1

(7 ) else if (val>dat(ind)) then

(8 ) b=ind+1

( ) else

(9 ) exit

( ) end if

( ) end do

( )

( )

(10) if (e-b<0) then

(11) ind = 0

( ) end if

( ) end subroutine cerca

L’analisi in questo caso si basa sul fatto che ind e la media degli indicidi inizio e fine dell’area di ricerca e che, se il valore non viene trovato (casopeggiore), l’indice di inizio o di fine diventa circa uguale a ind, dimezzando inogni iterazione l’area di ricerca. Quando inizio e fine coincidono, allora usciamodal ciclo. Un’analisi come nel caso ricorsivo in questo vaso ci porta a dire che iltempo e ancora logaritmico.

L’analisi potrebbe risultare particolarmente difficile se non sapessimo che ilprogramma scimmiotta l’algoritmo ricorsivo visto precedentemente. Richiederebbecioe di capire cosa il programma fa senza sapere il motivo per cui e stato scritto(reverse engineering).

11.2 Merge sort: ordinamento per fusione

L’algoritmo di merge sort e un esempio di algoritmo ricorsivo per ordinareun array di elementi, esattamente come insertion sort e bubble sort. Primadi illustrarlo vediamo pero una procedura per creare una sequenza ordinatapartendo da due sequenze ordinate.

11.2.1 Merge di due array ordinati

Il problema da risolvere e il seguente: dati sue array A e B, lunghi rispet-tivamente na e nb, di valori ordinati in senso crescente, produrre un array Cordinato in senso crescente che includa tutti e soli i valori degli array in ingresso.Ad esempio, se A e B sono rispettivamente:

A = 1, 3, 5, 6, 9

B = 2, 3, 4, 7, 8

allora C deve essereC = 1, 2, 3, 3, 4, 5, 6, 7, 8, 9

24

Page 25: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

Come si puo realizzare l’algoritmo di merging? Possiamo avere tre indici,ia che scorre A, ib che scorre B e ic che scorre C. All’inizio si inizializzano gliindici a 1 e l’algoritmo potrebbe essere il seguente:

1. prendi il minimo tra A(ia) e B(ib) e mettilo in C(ic) e incrementa ic di 1

2. se il minimo era A(ia) allora ia ← ia+1, altrimenti ib ← ib+1 e incrementaic di 1

3. se valgono contemporaneamente ia ≤ na e ib ≤ nb, allora torna al passo 1

4. se ia ≤ na allora

(a) C(ic)← A(ia)

(b) ia ← ia + 1 e ic ← ic + 1

5. se ib ≤ nb allora

(a) C(ic)← B(ib)

(b) ib ← ib + 1 e ic ← ic + 1

I passi da 1 a 3 effettuano il merge vero e proprio. Si ha infatti che ognivolta che si aggiunge un elemento a C, questo elemento e minore o uguale aivalori di indici maggiori di ia in A e di ib in B. In oltre il valore aggiunto nonpuo essere minore dei valori precedentemente inseriti in C in quanto gli arrayA e B sono ordinati in senso crescente.

All’inizio di ogni iterazione, quindi, so che andro a inserire in C un valoremaggiore o uguale a quelli che ho gia inserito e minore o uguale a quelli chesaranno inseriti successivamente.

Quando uno dei due array A o B sono stati scanditi completamente, sap-piamo che gli elementi non ancora scanditi di B e A, rispettivamente, sonomaggiori di tutti i valori fino a quel momento inseriti in C. Si tratta quindi (neipassi 4 e 5) di “appendere” a C i valori non ancora scanditi dell’array rimasto.

La descrizione data dell’algoritmo consente gia di farne l’analisi del tempo diesecuzione su di una macchina di von Neumann. Infatti, i passi 1 e 2 prendonotempo costante, mentre il ciclo 3 viene eseguito fino all’esaurimento di uno deidue array A o B. Supponiamo di aver esaurito l’array A e che in B rimanganom elementi da inserire in c. Il ciclo 3 viene quindi eseguito na + nb −m volte,perche ogni volta che viene eseguito il ciclo 3 viene inserito un valore in C. Aquesto punto il ciclo nelle istruzioni 5.a e 5.b viene eseguito m volte, per untotale di na + nb −m + m = na + nb.

Il ragionamento che si segue sull’esaurimento dell’array B e analogo e portaa dire che il tempo di esecuzione dell’algoritmo di merge e O(na + nb).

Il codice Fortran di una subroutine che realizza il merge tra due vettori e ilseguente:

( ) subroutine merge(A, na, B, nb,C)

( ) implicit none

25

Page 26: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

( ) integer, intent(in) :: na, nb ! taglie di A e B risp.

( ) integer, intent(in), dimension(na) :: A

( ) integer, intent(in), dimension(nb) :: B

( ) integer, intent(out), dimension(na+nb) :: C

( ) integer :: ia ! indice per scorrere A

( ) integer :: ib ! indice per scorrere B

( ) integer :: ic ! scorro e conto C

( )

( ) ! merge vero e proprio

( 1) ic=0

( 2) ia=1

( 3) ib=1

( 4) do while ((ia<=na) .and. (ib<=nb))

( 5) if (A(ia)<B(ib)) then

( 6) ic=ic+1

( 7) C(ic)=A(ia)

( 8) ia=ia+1

( ) else

( 9) ic=ic+1

(10) C(ic)=B(ib)

(11) ib=ib+1

( ) end if

( ) end do

( )

( ) ! Adesso o A o B sono finiti, ma non entrambi

( ) ! Finisco di copiare il pezzo rimanente si A o B

(12) if (ia<=na) then

(13) do while (ia<=na)

(14) ic=ic+1

(15) C(nc)=A(ia)

(16) ia=ia+1

( ) end do

( ) else

(17) do while (ib<=nb)

(18) ic=ic+1

(19) C(nc)=B(ib)

(20) ib=ib+1

( ) end do

( ) end if

( )

( ) end subroutine merge

Provate a farne l’analisi come negli esempi precedenti.

11.2.2 Merge sort

L’algoritmo di merge sort si basa sulla seguente osservazione ricorsiva:

26

Page 27: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

BASE: un array di lunghezza uguale a 1 e gia ordinato.RICORSIONE: per ordinare un array di lunghezza n si possono ordinare le

due meta di lunghezza rispettivamente bn/2c e n−bn/2c e poi eseguire il mergetra queste due meta.

Questo e sufficiente a descrivere l’algoritmo e a calcolarne il tempo di ese-cuzione O-grande. Riuscire a ottenere il tempo di esecuzione di un algoritmoprima di scriverne il programma e uno degli aspetti fondamentali della notazioneO-grande, in quanto consente di poter scegliere un algoritmo prima ancora direalizzarlo, risparmiando il notevole tempo di sviluppo.

Analizziamo l’algoritmo: il tempo di esecuzione puo essere scritto in formaricorsiva nel modo seguente:

BASE: T (1) = 0 (array gia ordinato)RICORSIONE:

T (n) = T (⌊n

2

) + T (n−⌊n

2

) + O(⌊n

2

+ n−⌊n

2

) ' 2T (n/2) + O(n)

Per risolvere questa relazione di ricorrenza usiamo lo stesso metodo usatoprecedentemente. Sviluppiamo dunque la relazione e cerchiamo di capire qualeespressione ha il passo i-esimo. Ipotiziamo, per semplicita che n = 2k per un kintero, in modo da poter sempre dividere per due l’argomento di T (·), inoltredecidiamo che il tempo per eseguire il merge sia bn. Si ha quindi:

T (n) = 2T(n

2

)

+ bn =

= 2(

2T(n

4

)

+ bn

2

)

+ bn =

= 4T(n

4

)

+ 2bn

= 4(

2T(n

8

)

+ bn

4

)

+ 2bn =

= 8T(n

8

)

+ 3bn

Dall’ultimo passaggio possiamo affermare (anche se andrebbe dimostrato for-malmente) che al passo i-esimo

T (n) = 2iT( n

2i

)

+ ibn

La ricorsione termina quando n/2i vale 1. In questo caso siamo alla basedella ricorsione e possiamo sostituire il valore 0 a T (1). n/2i = 1 implica chei = log n (come sempre la base del logaritmo e 2). Alla fine si ottiene:

T (n) = 2log nT (1) + bn log n = bn log n

Il tempo di esecuzione di merge-sort e quindi O(n log n). Gli algoritmi insertion-sort e bubble-sort erano entrambi O(n2). Possiamo dunque concludere che l’al-goritmo di merge-sort, sebbene richieda una subroutine ricorsiva e quindi unacostante moltiplicativa maggiore rispetto agli altri due algoritmi, diventa sempre

27

Page 28: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

piu veloce rispetto agli altri due via via che la taglia dell’array da ordinare cresce.Si puo dimostrare che, se per ordinare i dati si utilizzano i confronti tra elementi(come nel caso di tutti gli algoritmi di ordinamento che abbiamo visto), alloranon possiamo fare meglio (nel senso di O-grande, o asintoticamente) di n log n.Esistono, tuttavia, algoritmi che si basano su altri principi e che ordinano intempo lineare, se gli elementi da ordinare hanno certe caratteristiche.

Bisogna aggiungere che l’algoritmo di merge-sort non puo essere eseguitoin-place. Insertion sort e bubble sort agiscono direttamente sull’array da or-dinare, mentre l’algoritmo di merge deve utilizzare un array di appoggio su cuimemorizzare il risultato del merge. Merge-sort quindi richiede piu memoria.

Di seguito il programma che esegue il merge-sort e che utilizza la subroutinemerge illustrata nella sezione precedente. Notate che, a differenza di insertion-sort e bubble-sort, merge-sort richiede come parametro in ingresso un arraydella stessa lunghezza di quello da ordinare che serve per contenere l’uscitadella routine di merge.

( ) recursive subroutine mergesort(dati, n, workspace)

( ) implicit none

( ) integer, intent(in) :: n

( ) integer, intent(inout),dimension(n) :: dati

( ) integer, intent(inout),dimension(n) :: workspace

( ) integer :: middle

( )

( ) ! la sequenza lunga 1 e‘ gia‘ ordinata

(1) if (n>1) then

(2) middle=n/2

(3) call mergesort(dati(1:middle),middle, workspace)

(4) call mergesort(dati(middle+1:n), n-middle, workspace)

(5) call merge(dati(1:middle), middle, dati(middle+1:n), &

n-middle, workspace)

(6) dati=workspace

( ) end if

( )

( ) end subroutine mergesort

11.2.3 Divide et Impera

L’algoritmo di merge-sort ha proprieta molto interessanti. Oltre a esserericorsivo (ma ricordiamo che tutti gli algoritmi possono essere espressi in modoricorsivo, e per questo che le funzioni computabili vengono dette anche funzioniricorsive), esso divide il problema principale in due sottoproblemi di taglia piupiccola e poi combina le soluzioni di questi due sottoproblemi per ottenere lasoluzione al problema dato.

Questa e una applicazione di un metodo di progetto di algoritmi dicarattere piu generale chiamato divide et impera, o, all’inglese, divide and

28

Page 29: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

conquer. In generale se un problema e scomponibile in k sottoproblemi, og-nuno di taglia h(n), e se la composizione di questi sottoproblemi per calcolarela soluzione al problema originale e una funzione f(n), allora otteniamo unalgoritmo con il seguente tempo di esecuzione:

BASE: T (n) = g(n) per n ≤ n0, dove n0 e la base della ricorsione.RICORSIONE: T (n) = kT (h(n)) + f(n) se n ≥ n0

Divide et impera e un motto latino che sta a indicare che per sconfiggere inemici li si deve dividere e poi combatterli separatamente. L’idea e che la sommadella forza dei singoli nemici dovrebbe essere minore della forza dei nemici unitiassieme e coalizzati contro di noi.

Se il tempo per risolvere un problema di taglia n e piu che lineare (ad esem-pio O(n2), O(n5), O(2n)) allora la somma dei tempi necessari a risolvere dueproblemi di taglia rispettivamente n1 e n2, maggiori di un valore n0, e minoredel tempo necessario a risolvere un solo problema di taglia n1 + n2. Questofenomeno e noto come disuguaglianza di Jensen. Data una funzione piu chelineare (detta anche superlineare) f , allora, se n1 e n2 sono maggiori di un valoren0, si ha

f(n1 + n2) ≥ f(n1) + f(n2)

Per fare un esempio, supponiamo che f(n) = n2 + an + b. Allora vediamoche

f(n1 + n2) = n21 + n2

2 + 2n1n2 + an1 + an2 + b =

= f(n1) + f(n2) + 2n1n2 − b ≥ f(n1) + f(n2) (4)

L’ultima disuguaglianza vale se 2n1n2 ≥ b, il che e sempre vero se n1, n2 ≥⌈

b/2⌉

= n0

Supponiamo di non applicare la strategia ricorsiva ma di avere a disposizioneun algoritmo A che impiega t(n) per risolvere un problema di taglia n. Sup-poniamo che la funzione h(n) sia uguale a n/k, ovvero di dividere il problemain k problemi di taglia n/k. Ci limitiamo alle seguenti due possibilita:

1. risolvere il problema di taglia n in una volta usando A impiegando t(n);

2. risolvere il problema in k sottoproblemi usando A per risolvere ognuno diessi e impiegando tempo kt(n/k), per poi ricomporre la soluzione in tempof(n).

Applichiamo cioe il metodo ricorsivo solo al primo livello di ricorsione, senzarisolvere i sottoproblemi dividendo i sottoproblemi stessi. Possiamo dire chese t(n) < kt(n/k) + f(n) allora siamo certi che un algoritmo ricorsivo R ditipo “divite et impera” non ci dara un algoritmo piu veloce di risoluzione. Se,viceversa, t(n) > kt(n/k) + f(n) possiamo dire che l’algoritmo ricorsivo R epiu veloce dell’algoritmo A. Questo non implica, comunque, che non esistaun algoritmo B che superi l’algoritmo ricorsivo R. Il fatto di essere partiti daun algoritmo estremamente poco efficiente ci potrebbe aver messo sulla cattivastrada. Quello che si deve vedere quindi e se il vantaggio dovuto a Jensen

29

Page 30: Complessita computazionale: O-grande - Aula Didattica … problemi di grosse dimensioni, tuttavia, e il tempo di esecuzione a de-terminare se un dato programma pu o essere e ettivamente

compensa il tempo che dobbiamo spendere per la composizione della soluzionea partire dai sottoproblemi.

Il metodo di “divide et impera” rappresenta una delle tecniche piu proficueper il progetto di algoritmi e e di sicuro la piu elegante. Ha reso possibili,negli anni, oltre che l’ordinamento in tempo ottimo (O(n log n)), anche il calcolodella trasformata di Fourier in tempo O(n log n), rendendo possibile la soluzioneuna quantita enorme di problemi tecnologici e scientifici precedentemente nonapprocciabili con il calcolatore. La tecnica e servita per il prodotto di matrici ealtre importanti applicazioni ancora.

30