Docente: Giuseppe Scollo
Università di Catania, sede di Comiso (RG)
Facoltà di Scienze Matematiche, Fisiche e Naturali
Corso di Studi in Informatica applicata, AA 2006-7
molti algoritmi, progettati seconda la regola
divide et impera
,
risolvono il problema decomponendolo in istanze più piccole
dello stesso problema
più precisamente, la soluzione per un valore N del parametro primario (dimensione dell'input o di una struttura dati fondamentale dell'algoritmo) è espressa in termini della stessa soluzione per un valore minore di tale parametro
l'analisi delle prestazioni di tali algoritmi si basa sulla soluzione di relazioni di ricorrenza, nelle quali il costo computazionale CN è definito induttivamente, a partire da un costo di base C1, per il tramite di un'equazione ricorsiva
mostriamo nel seguito una semplice tecnica di soluzione di ricorrenze attraverso sostituzioni successive per alcune classi fondamentali di algoritmi
classe degli algoritmi: si esamina tutto l'input e se ne elimina un elemento, reiterando quindi l'algoritmo sul resto dell'input
ricorrenza: CN = CN-1 + N, per N > 1 , con C1 = 1
soluzione: CN
≈
N2/2
classe degli algoritmi: si dimezza l'input ad ogni passo, di costo costante, reiterando quindi l'algoritmo sull'input dimezzato
ricorrenza: CN = CN/2 + 1, per N > 1 , con C1 = 1
soluzione: per N = 2n si ha: C2n
≈
n
e più in generale
CN
≈
lg N
se si interpreta il dimezzamento come quoziente intero,
N/2 = ⌊
N/2⌋
, poiché in tal caso il numero di iterazioni è
uguale al numero di bit necessari alla rappresentazione binaria naturale
di N, cioè
⌈
lg N⌉
classe degli algoritmi: ad ogni passo si esamina tutto l'input e lo si dimezza, reiterando quindi l'algoritmo sull'input dimezzato
ricorrenza: CN = CN/2 + N, per N > 1 , con C1 = 0
soluzione:
se si adopera il quoziente intero,
N/2k =
⌊
N/2k⌋
,
si ha:
CN
≈
2N
dove il fattore 2 è il limite della serie geometrica di ragione 1/2,
e l'errore di tale approssimazione per eccesso tende a 0 per
N
→
∞
classe degli algoritmi: ad ogni passo si esamina tutto l'input e lo si divide in due metà, reiterando quindi l'algoritmo su entrambe le metà
ricorrenza: CN = 2CN/2 + N, per N > 1 , con C1 = 0
soluzione: nell'ipotesi che N = 2n, si ha: C2n = 2C2n-1 + 2n
quindi, dividendo entrambi i membri per 2n: C2n/2n
dunque
CN =
N lg N nell'ipotesi posta, e più in generale
CN
≈
N lg N
classe degli algoritmi: ad ogni passo, di costo costante, si divide l'input in due metà, reiterando quindi l'algoritmo su entrambe le metà
ricorrenza: CN = 2CN/2 + 1, per N > 1 , con C1 = 1
soluzione: nell'ipotesi che N = 2n, si ha: C2n = 2C2n-1 + 1
quindi, dividendo entrambi i membri per 2n: C2n/2n
approssimando per eccesso con il limite della serie geometrica di ragione 1/2
e quindi, più generalmente per qualsiasi N :
CN
≈
2N
a titolo di esempio, vediamo l'uso di relazioni di ricorrenza nel confronto delle prestazioni di algoritmi diversi per uno stesso problema:
un semplice algoritmo per il problema in esame effettua la scansione sequenziale degli elementi nell'array, confrontandoli con l'elemento dato fino a quando
ecco una codifica di questo algoritmo come funzione C++,
dove i parametri
l, r
sono indici che delimitano lo spazio di ricerca del valore
v
nell'array
a
,
dunque N =
r-l+1
,
e si ha il valore di ritorno -1 quando la ricerca ha esito negativo:
int search(int a[], int v, int l, int r) { for (int i = l; i <= r; i++) if (v == a[i]) return i; return -1; }
∝
N lg N, irrisorio quando
M >> N
perché sostenuto una tantum),
si può dimezzare il costo medio della ricerca sequenziale
ad esito negativo, arrestandola quando il valore dell'elemento
dell'array è maggiore di quello cercato (se l'array è ordinato per
valori crescenti)
l'ordinamento preventivo dell'array, che produce un modesto guadagno prestazionale nella ricerca sequenziale, è sfruttato molto meglio nell'algoritmo di ricerca binaria, che ad ogni passo con esito negativo dimezza la dimensione dello spazio di ricerca nel passo successivo
ecco una codifica dell'algoritmo come funzione C++:
int search(int a[], int v, int l, int r) { while (r >= l) { int m = (l+r)/2; if (v == a[m]) return m; if (v < a[m]) r = m-1; else l=m+1; } return -1; }
dallo studio già effettuato della ricorrenza CN = CN/2 + 1 possiamo concludere che il costo della ricerca binaria è proporzionale a lg N (anche nel caso peggiore)
il guadagno di efficienza rispetto alla ricerca sequenziale è esponenziale!
finora si sono considerate stime e analisi delle prestazioni di algoritmi per un dato problema, o classe di problemi, e per essi risulta utile distinguere fra
per complessità computazionale di un problema si intende:
il costo computazionale nel caso peggiore di un algoritmo per un dato problema costituisce un limite superiore alla complessità computazionale del problema:
è anche utile determinare limiti inferiori alla complessità computazionale di problemi, analizzandone caratteristiche inerenti, ma questo compito è di solito più difficile
≈
lg N
anche con l'approssimazione intera per eccesso del dimezzamento:
N/2 =
⌈
N/2⌉
≈
2N
anche con l'approssimazione intera per eccesso del quoziente:
N/2k =
⌈
N/2k⌉
a
sia ordinato per valori crescenti, in modo che la ricerca termini
con esito negativo quando il valore dell'elemento dell'array è
maggiore di quello cercato