Calcoleremo il costo degli algoritmi utilizzando il criterio della misura di costo uniforme. Il costo computazionale lo possiamo vedere come una funzione monotona non decrescente che rappresenta il tempo di esecuzione in base alla dimensione dell’input. Come prima cosa dobbiamo quindi definire la dimensione del nostro input che può essere il numero di elementi di una lista da ordinare, i nodi di un albero o il numero di elementi da cercare.
Il costo computazionale è ritenuto valido solo asintoticamente, ovvero attraverso la notazione asintotica.
Pseudocodice
Per valutare il costo computazionale non si scrive il codice in un linguaggio specifico ma lo pseudocodice ovvero un linguaggio di programmazione informale:
- si usano i costrutti di base come for, while, if
- si può usare la nostra lingua naturale per specificare passaggi
- si possono omettere molti dettagli al fine di rappresentare soltanto la soluzione vera e propria
Regole per valutare la complessità di un algoritmo
- Le istruzioni elementari hanno costo (aritmetiche, lettura, assegnazione, if, stampa)
- Le istruzioni iterative hanno un costo pari alla somma dei costi massimi di ciascuna delle iterazioni, da notare che viene valutata anche la condizione (es. while) e che questa viene valutata una volta in più rispetto al numero delle iterazioni, ovvero quando l’iterazione termina.
Dipendenza del costo dall’input
Un algoritmo potrebbe avere tempi di esecuzione diversi a seconda dell’input, in questo caso dobbiamo individuare prima di tutto i casi migliore e peggiore, capire quindi quale input è più vantaggioso e quale no. É molto importante, a prescindere dall’input, trovare il tempo di esecuzione del caso peggiore in termini di o se non possibile arrotondarlo a oppure .
Esempio: Troviamo il massimo in una lista di n valori
m = A[0] #Costo 1
for i in range(A): #Costo Θ(lunghezzaA)
if i > m: #Costo 1
m = i #Costo 1
print("Il valore massimo è: " + m) #Costo 1
All’interno dell’iterazione le operazione hanno tutte lo stesso costo quindi possiamo svolgere il prodotto
In realtà la complessità sarebbe ma le costanti non le consideriamo nel calcolo dei limiti asintotici.
Variazione dei tempi di esecuzione di un algoritmo in funzione del suo costo computazionale
Ipotizziamo di avere a disposizione una macchina in grado di svolgere un’operazione in un nanosecondo ( operazioni al secondo) e supponiamo che la dimensione del problema sia :
- Tempo di computazione 1 millesimo di secondo
- Tempo di computazione 20 millesimi di secondo
- Tempo di computazione 1000 secondi = 16 minuti
E se il costo cresce esponenzialmente ? Anche se abbassassimo con la stessa macchina vista prima avremo un costo pari a ovvero circa secoli.
Un algoritmo con una complessità esponenziale non serve a molto
Anche con un avanzamento tecnologico un algoritmo del genere non è molto conveniente, ad esempio prendendo una macchina estremamente veloce in grado di risolvere un problema simile con e prendendone un’altra 1000 volte più veloce, nello stesso periodo T riusciremo a risolvere soltanto un problema con , non abbiamo quindi fatto grandi progressi.
Algoritmi Ricorsivi
Una funzione si dice ricorsiva quando chiama se stessa.
- Deve avere un caso base
- E un meccanismo di calcolo ricorsivo, ovvero come posso calcolare il valore successivo conoscendo quello che ho
In ambito informatico di solito con la ricorsione risolviamo tanti piccoli sottoproblemi e combinando le soluzioni otteniamo la soluzione al problema originale.
Le chiamate a sottoproblemi devono sempre convergere ad un caso base per permettere la terminazione del programma
Ogni funzione, ricorsiva o non, richiede una certa quantità di memoria per:
- Caricare il codice
- Passare i parametri
- Memorizzare il valore delle variabili
In generale le funzioni ricorsive hanno maggiori esigenze in termini di memoria delle funzioni non ricorsive
Qualsiasi problema risolvibile con un algoritmo ricorsivo può essere risolto anche con un algoritmo iterativo Quando conviene quindi utilizzare un algoritmo ricorsivo?
- Quando permette di scrivere una soluzione in modo più chiaro e simile alla natura del problema
- Non conviene quando esiste una soluzione iterativa semplice e chiara
- Non conviene utilizzare la ricorsione quando è importante avere una buona efficienza
Abbiamo un costo elevato di spazio perchè ogni volta che andiamo in ricorsione creiamo una copia della lista con un elemento in meno, possiamo ottimizzare il codice passando un indice invece che delle copie della lista:
Costo di fibonacci iterativo e ricorsivo
Il calcolo di Fibonacci ricorsivo ha costo in memoria molto elevato e vedremo più avanti un costo in tempo di mentre la sua versione iterativi ha spazio e in tempo Questo perchè nella versione ricorsiva eseguiamo tantissimi calcoli che abbiamo già svolto precedentemente
Equazione di ricorrenza
Calcolare la complessità di un algoritmo ricorsivo è più complesso ad uno iterativo infatti questo da luogo ad una funzione di costo anch’essa ricorsiva. Trovarla è abbastanza semplice ma questa va riformulata in modo da non renderla ricorsiva altrimenti ci sarà impossibile quantificare il costo asintotico. Per riformulare la funzione di costo si imposta una equazione di ricorrenza costituita dalla formulazione ricorsiva e dal caso base.
L’equazione deve essere sempre costituita dalla somma di almeno due addendi, uno contiene la parte ricorsiva mentre l’altro il costo di tutto quello che avviene al di fuori della chiamata ricorsiva. DEVE ESSERE SEMPRE PRESENTE UN CASO BASE
Per risolvere le equazioni di ricorrenza abbiamo a disposizione 4 metodi:
- metodo iterativo
- metodo dell’albero
- metodo di sostituzione
- metodo principale
Metodo Iterativo
In questo metodo sviluppiamo l’equazione in modo da esprimerla come somma di termini dipendenti da e dal caso base. Contro: Maggiore quantità di calcoli algebrici rispetto agli altri metodi.
Esempi:
Calcolo di Fibonacci
Non siamo quindi riusciti a trovare la crescita asintotica precisa ma abbiamo comunque potuto approssimarla e notare che il calcolo di un numero di Fibonacci in modo ricorsivo ha costo esponenziale.
Metodo dell’albero
In questo metodo rappresentiamo graficamente lo sviluppo ricorsivo e vediamo il costo passo per passo.
Esempio: e come caso base
Quindi ogni nodo ha costo e sommando tutti i valori che sono equivalenti a otteniamo
Master Theorem
Questo metodo è molto meccanico ma non sempre applicabile, infatti possiamo utilizzarlo solo quando nella funzione di ricorrenza andiamo in ricorsione su un .
Più nello specifico definiamo questa forma:
- Se = per qualche costante allora
- Se allora
- Se per qualche costante e se per qualche costante e per sufficientemente grande allora .
Metodo di sostituzione
Con questo metodo non andiamo a trovare una soluzione ma ne dovremmo ipotizzare una e dimostrare che questa sia vera.
Esempio:
Per utilizzare questo metodo è necessario eliminare la notazione asintotica dall’equazione di ricorrenza, ottenendo quindi:
Per due costanti e fissate.
Ipotizziamo la soluzione , ossia dove è una costante che dobbiamo ancora determinare.
Caso Base: che sarà vera per
Induzione Sostituendo nella formulazione ricorsiva otteniamo
Ottenendo quindi che la disequazione è vera quando
Abbiamo quindi dimostrato che è effettivamente un poiché è possibile trovare un valore per il quale si ha infatti basta prendere .
Troviamo adesso la notazione asintotica omega
Adesso dobbiamo ipotizzare una funzione che viene dominata da quindi troviamo una costante tale che
Come fatto prima sostituiamo nel caso base
che viene soddisfatta quando
Sostituiamo nell’equazione di ricorrenza usando l’ipotesi induttiva
Che risulta vera nel caso in cui scegliamo
Abbiamo quindi dimostrato che è un dato che possiamo trovare un valore tale che ovvero basta prendere
Esempio più complesso