You are on page 1of 22

Algoritmi e strutture dati 2

Introduzione (9-10 / 1 / 2008) ............................................................................................. 3 Programmazione dinamica (16 / 1 / 2008) ........................................................................ 8 Applicazioni concrete di programmazione dinamica (31 / 1 / 2008) ........................... 14 Grafi (31 / 1 / 2008) ............................................................................................................ 15

Indice:

Appendici collegate:
Nessuna appendice collegata

Dati riguardanti il quaderno virtuale:


Ultima sezione aggiunta: [aggiornato] Grafi il 12 / 3 / 2008

Quaderno virtuale di teoria

Introduzione (9-10 / 1 / 2008)


Per poter iniziare la nostra trattazione dobbiamo fare un ripasso abbastanza rapido di tutte le conoscenze che dovrebbero essere state acquisite dal corso di algoritmi e programmazione. Iniziamo il nostro ripasso con il ricapitolare notazione e metodo per quantificare il tempo di calcolo impiegato da un algoritmo con un certo input. Chiamiamo T la nostra funzione che rappresenta il tempo di calcolo, con n indichiamo la dimensione dellinput dellalgoritmo e con T(n) rappresentiamo il costo computazionale dellalgoritmo rispetto allinput di dimensione n. Per capire il comportamento di T(n) si effettua il confronto con funzioni base (polinomi, logaritmi,) usando le notazioni asintotiche. Sia g : N N una funzione, indichiamo con (g (n )) = { f : N N : c1 , c 2 , n0 : n n0 , c1 g (n ) f (n ) c 2 g (n )} il limite asintotico stretto.

Con T ( g (n )) individuo una classe di funzione che al limite hanno lo stesso comportamento. Con un abuso di notazione indicheremo T = ( g (n )) O(g (n )) = { f : N N : c, n0 : n n0 , f (n ) c g (n )} il limite asintotico superiore.

( g (n )) = { f : N N : c, n0 : n n0 , c g (n ) f (n )} il limite asintotico inferiore.

Algoritmi e strutture dati 2

Vediamo un po di esempi pratici: T ( n) = 4 n 2 T (n ) = n 2 T (n ) = O n 2 T (n ) = n 2 T (n ) = O n 5 T (n) = 4n 2 2n T (n ) = n 2 T (n ) = 5 T (n ) = (1)

( ( ( ( (

) ) ) ) )

Per comodit possiamo creare una scala di infiniti 1 3 1 log n L n 2 n 4 L n n log n L n 2 n 3 L 2 n e n 3n L n! L n n Vediamo, ora, come calcolare il costo (in termini di tempo) di un algoritmo. Per comodit utilizzeremo dello pseudocodice.
procedura iter(n) begin x:=1 for i=2 to n do for j=1 to i do x:=x+i+j end for end for end

c1 cf1 cf2 c2

c1 il costo dellassegnamento, cf1 il costo del test del ciclo for esterno, cf2 il costo del test del ciclo for interno e c2 il costo dellassegnamento allinterno del secondo ciclo for T(n) dato dalla somma dei costi per ogni istruzione che richieda test o assegnamenti I costi dei cicli for dipendono da quante volte quelle istruzioni vengono eseguite.

Quaderno virtuale di teoria


T (n ) = 1 + n + (i + 1) + i = 1 + n + i + 1 + i = n(n + 1) n(n + 1) = 1+ n + 1 + (n 1) + 1 2 2
i =2 i =2 i =2 i =2 i =2 n n n n n

T(n) un polinomio di grado 2 quindi posso concludere che T (n ) = n 2 . Passiamo, ora, agli algoritmi ricorsivi. Un algoritmo ricorsivo se la definizione della sua funzione ricorsiva
se n = 0 f (n ) = 1 se n > 0 f (n ) = n f (n 1)
procedura fatt(n) begin if n=0 then return 1 else return n*fatt(n-1) end

( )

f :N N

Per determinare la complessit di un algoritmo ricorsivo devo definire la complessit come funzione ricorsiva: T (0 ) = 1 T (n ) = 1 + T (n 1) quindi T (n ) = 1 + T (n 1) = 1 + 1 + T (n 1) = K = n + T (0 ) = n + 1
T (n ) = (n )

Proseguiamo con unaltra procedura ricorsiva


procedura ricorsiva (A, l, r) begin if l<r then m = (l+r)/2 ricorsiva(A, l, m) ricorsiva(A, m+1, r) end

Questo algoritmo di per se non fa nulla perch manca la fase di combinazione dei risultati del metodo divide et impera, ma ci utile per capire il calcolo della complessit. Identifichiamo come fatta T(n) T (1) = 1

n T (n ) = 1 + 2T 2 Nel caso di un input di lunghezza 1 ho solo il test dellif, mentre per un input di lunghezza qualsiasi ho il costo unitario del test dellif e del primo assegnamento, poi ho le due chiamate ricorsive su met dellinput iniziale. Schematizziamo cosa succede al variare dellinput:

Algoritmi e strutture dati 2

Descrivo brevemente questo albero: con input di lunghezza n ho il costo fisso che 1 e poi devo sommare i costi per con input di lunghezza la met e cos via. Lultima colonna indica il costo totale del livello dellalbero che ha un input con determinata lunghezza. La profondit (o altezza) dellalbero indicata con k che uguale al log 2 n Per calcolare il costo complessivo dellalgoritmo devo sommare tutti i valori riportati nella parte pi a destra dello schema: k 1 2 k +1 T (n ) = 2 i = = 1 + 2 k +1 = 1 + 2 log n +1 = 1 + 2 2 log n = 2n 1 = (n ) 1 2 i =0 Vediamo ora il metodo principale che un teorema che ci semplifica la vita per calcolare la tendenza asintotica: n T (n ) = a T + f (n ) per a 1 b > 1 b T (1) = k
log a log a Se f (n ) = n b T (n ) = n b log 2 n log a log a Se > 0 t.c. f (n ) = O n b T (n ) = n b log a + n Se > 0 t.c. f (n ) = n b e SE c < 1 t.c. a f c f (n ) T (n ) = ( f (n )) b Vediamo un esempio: n T (n ) = 2T + n 2 T (1) = k

a = 2 b = 2 f (n ) = n

log b a = log 2 2 = 1

Quaderno virtuale di teoria

Devo confrontare f(n) n = n1 s quindi T (n ) = (n log n )

( )

Non sempre detto che la versione ricorsiva di un algoritmo sia sempre pi veloce di quella iterativa. Nel caso degli algoritmi di ordinamento la versione ricorsiva effettivamente migliore di quella iterativa nel caso peggiore, ma questo non accade per lalgoritmo che mi restituisce la serie dei numeri di Fibonacci; per capire meglio questa situazione analizziamolo nel dettaglio. Partiamo dalla versione iterativa:
procedura fib_it(n) begin x=0 y=1 for i=2 to n z=x+y x=y y=z end for if n=0 return 0 else return y end

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

Versione ricorsiva:
procedura fib_r(n) begin if n=0 return 0 else if n=1 return 1 else return fib_r(n-1)+fib_r(n-2) end

T (0 ) = 1 T (1) = 1

T (n ) = 1 + T (n 1) + T (n 2 )

Questo calcolo un po pi complesso. T (n ) T (n 1) T (n 2 ) = 1 (quando utilizzer (**) mi riferir a questa equazione) Per prima cosa risolviamo T (n ) T (n 1) T (n 2 ) = 0 (la indicher con (*)) Mi chiedo se la soluzione di (*) potr essere sotto forma esponenziale, lo verifico provando a sostituire r n r (n1) r (n2 ) = 0 r (n2 ) r 2 r 1 = 0

r1 =

1+ 5 2

r2 =
n

1 5 2
n

1+ 5 1 5 T (n ) = T (n ) = 2 2 Sono soluzioni di (*) Osservazione:


n

1+ 5 1 5 anche c1 2 + c 2 2 soluzione di (*)

Algoritmi e strutture dati 2

Per risolvere (**), oltre a tutte le soluzioni di (*), mi occorre anche una qualunque soluzione di (**) Devo trovare un valore di T(n) in modo da ottenere come risultato 1: T(n) = -1 il valore che cerco
1+ 5 1 5 + c2 Tutte le soluzioni di (**) sono T (n ) = c1 2 2 1 Adesso devo trovare la soluzione specifica che si adatti con i miei casi base del calcolo del costo dellalgoritmo, quindi devo trovare una soluzione compatibile con T(0) = 1 e T(1) = 1, in sostanza devo trovare i valori c1 e c2 in modo che soddisfino T(0) = 1 e T(1) = 1. 1+ 5 c1 = 5
n n

5 1 2 n 1+ 5 T (n ) = 2 Il tempo impiegato dallalgoritmo esponenziale e quindi un pessimo algoritmo. Naturalmente lalgoritmo ricorsivo si pu ottimizzare facendo in modo di salvare i risultati intermedi della computazione perch se proviamo a fare lalbero delle chiamate ricorsive noteremo molte chiamate con gli stessi parametri disposte in diversi punti. c2 =

Programmazione dinamica (16 / 1 / 2008)


La programmazione dinamica una tecnica recente che consente di trovare soluzioni veloci a problemi i cui algoritmi avevano un tempo di calcolo esponenziale. Per introdurci a questa tecnica inizieremo con la soluzioni di problemi che hanno a che fare con sequenze. Iniziamo col dare alcune definizioni che risulteranno utili in seguito.
Definizione: Un alfabeto = {a, b, c, d ,..., z} un insieme finito di simboli Definizione: Dato un alfabeto , una sequenza X una concatenazione di simboli di e viene indicata come segue: X =< a1 , a 2 ,...., a n > oppure X = a1a 2 ...a n La lunghezza di X indica come |X| = n Definizione: Y sottosequenza di X =< a1 , a 2 ,...., a n > se e solo se esiste una sequenza di indici < i1 , i2 ,...., ik > tale che Y =< b1 , b2 ,...., bn > e ai1 = b1 , ai 2 = b2 ,..., aik = bk con i1 < i2 < .... < ik . Praticamente la sottosequenza di X una qualsiasi sequenza che si ottiene cancellando 0 o pi simboli di X Definizione: Una sottosequenza comune delle sequenze X e Y una sottosequenza che appartiene sia ad X che a Y. Esempio: X = < L,I,B,R,O > Y = < O,M,B,R,E,L,L,O > Z = < B,R,O > 8

Quaderno virtuale di teoria Definizione: La sequenza vuota <> anche indicata con .

A noi tra le sottosequenze comuni interessa una di quelle pi lunghe: Longest Common Subsequence (LCS). Va precisato che questa sottosequenza comune non detto che sia unica. Passiamo, ora, al problema di identificare una LCS Vediamo cosa deve fare il nostro algoritmo: input: 2 sequenze X e Y su un alfabeto output: una sottosequenza comune, Z, di X e Y obiettivo: la lunghezza di Z massima Quante sono le sottosequenze comuni di X e Y e come le trovo? Se X =< a1 , a 2 ,...., a n > devo cercare tutti i sottoinsiemi degli indici {1,2,3,.,n}, che sono 2n. Algoritmo (triviale): (1) trovo un sottoinsieme di indici I = {i1, i 2,..., ik } dellinsieme {1,2,3,.,n} ed estraggo < ai1 , ai 2 ,..., aik > (2) Verifico che Z =< ai1 , ai 2 ,..., aik > anche sottosequenza di Y (3) Se s Z sottosequenza comune di X e Y Lalgoritmo triviale prova tutte le sottosequenze di X, che sono 2n, dove |X| = n, |Y| = m con n m. Il tempo di calcolo 2 n m . Dobbiamo trovare un algoritmo migliore. Iniziamo la nostra ricerca dellalgoritmo da molto lontano: cerchiamo la definizione ricorsiva per trovare un lcs
Definizione: Prefisso di lunghezza i della sequenza X =< a1 , a 2 ,...., a n > la sequenza X i =< a1 ,...., ai >

Indico con lcs(X, Y) una sottosequenza comune pi lunga di X e Y. Propriet ricorsiva: X =< a1 , a 2 ,...., a n > X =n Y =< b1 , b2 ,...., bm > Y =m Z =k Z = lcs( X , Y ) =< c1 , c 2 ,...., c k >

Caso 1: X = GIOCATTOLO Y = BARATTOLO Se an = bm allora ck = an = bm. La definizione ricorsiva : lcs( X , Y ) = lcs( X n1 , Ym1 ) a n Caso 2: X = YELLOW Y = BELLO
9

Algoritmi e strutture dati 2

Per questo caso la definizione ricorsiva di lcs : lcs( X , Y ) = lcs( X n1 , Y ) Caso 3: X = MIXER Y = MISERO Per questo caso la definizione ricorsiva di lcs : lcs( X , Y ) = lcs( X , Ym1 ) Caso 4: X = BUONO Y = MOBILE Z = BI Questo caso gi contemplato nei casi 2 e 3. Conclusione: Se a n = bn lcs( X , Y ) = lcs( X n1 , Ym1 ) a n Se a n bn lcs( X , Y ) = lcs( X n 1 , Y ) oppure lcs( X , Y ) = lcs( X , Ym1 ) Calcoliamo la lunghezza di lcs(X, Y), che indico con il parametro c[n,m] dove |X| = n, |Y| = m. c[i, j ] = c[i 1, j 1] + 1 se a n = bn c[i, j ] = max{c[i 1, j ], c[i, j 1]} se a n bn

procedura calcola_lcs_lungh(i, j, X, Y) begin if i = 0 OR j = 0 then return 0 else if ai = bj then return calcola_lcs_lungh(i-1,j-1,X,Y)+1 else c1 = calcola_lcs_lungh(i-1,j,X,Y) c2 = calcola_lcs_lungh(i,j-1,X,Y) return max{c1, c2} end

da notare che questa procedura ha tempo esponenziale, noi vogliamo un algoritmo con complessit polinomiale. Il motivo per cui questo algoritmo esponenziale dato dal fatto che ho le stesse chiamate ricorsive con gli stessi valori in pi posizioni dellalbero delle chiamate ricorsive; dobbiamo realizzare una versione dellalgoritmo che sia in grado di memorizzare i valori precedenti senza doverli ogni volta ricalcolare. Iniziamo con capire quante sono le chiamate ricorsive distinte per il calcolo di c[n,m]: i e j possono assumere rispettivamente valori compresi tra 0 e n e tra 0 e m quindi ho (n + 1)(m + 1) chiamate ricorsive distinte. Questo numero di chiamate polinomiale. Applichiamo la tecnica della programmazione dinamica basandoci sullidea contemporaneamente al calcolo dei valori li memorizzo. Come? Uso la tecnica bottom-up, cio quella che utilizza literazione e come struttura dati usiamo una (o pi matrici). La matrice C conterr i valori della lunghezza della sequenza comune pi lunga per ogni coppia di indici i e j, useremo anche una matrice D che conterr le indicazioni su come ricomporre la lcs. Per scrivere questo algoritmo ci basiamo nuovamente sulla definizione ricorsiva del problema, da cui sappiamo che la prima riga e la prima colonna della matrice C conterranno solo 0.

10

Quaderno virtuale di teoria

procedura begin n = m = for

calcola_lungh_it(X, Y)

length(X); length(Y); i = 0 to n C[i,0] = End for for j = 0 to m C[0,j] = End for

/* riempio la prima colonna di 0 */ 0 /* riempio la prima riga di 0 */ 0

for i = 1 to n for j = 1 to m if ai = bj then C[i,j]=C[i-1,j-1]+1; D[i,j]= ; else if C[i-1,j] >= C[i,j-1] then C[i,j]=C[i-1,j]; D[i,j]=; else C[i,j]=C[i,j-1]; D[i,j]=; End for End for end

i simboli di freccia indicano le direzioni che dovr seguire a partire dallultimo elemento della matrice per poter ricreare una delle possibili lcs. Il costo dellalgoritmo (n m ) Vediamo, ora, di creare la procedura di stampa della lcs a partire dalle due stringhe X e Y e dalla matrice D (delle direzioni); essendo una procedura ricorsiva abbiamo bisogno come parametri anche gli indici massimi della matrice.
11

Algoritmi e strutture dati 2

procedura stampa_lcs(D, X, Y, i, j) begin if i = 0 OR j = 0 then return then else if D[i,j] = stampa_lcs(D, X, Y, i-1, j-1) print X[i]; else if D[i,j] = then stampa_lcs(D, X, Y, i-1, j) else stampa_lcs(D, X, Y, i, j-1) end

Questo algoritmo al massimo ha una complessit (n + m ) e quindi polinomiale La tecnica della programmazione dinamica stata introdotta da Belman nel 56 (in questo periodo fu usata prevalentemente per risolvere problemi sui grafi). Negli anni 70 questa tecnica ha avuto successo nella biologia computazionale (problemi su sequenze). Iniziamo una analisi pi approfondita di questa tecnica partendo da un esempio: vogliamo costruire un dizionario e il nostro obiettivo disporre di un algoritmo efficiente per studiare la somiglianza tra sequenze. Per farlo lidea fondamentale quella di cercare accoppiamenti tra le parole (matching); dobbiamo evitare laccoppiamento tra simboli che si incrociano.
Definizione: Allineamento di X e Y (X, Y sequenze) Individuare coppie formate da un simbolo di X e uno di Y in cui non ci siano incroci. Se ho la coppia (xi,yj) e (xt,yz) allora i < t j < z

Devo associare alle coppie di confronto e ai non accoppiamenti un costo. Indico con (maggiore di zero) il costo di un gap, cio quando ho un simbolo di una sequenza che non allineato a un simbolo dellaltra sequenza. Con xi y j indico il costo dellaccoppiamento dellelemento i della sequenza X e dellelemento j della sequenza Y. 1. Se xi = yj xi y j = 0 2. Se xi yj

xi y j > 0

Una volta che abbiamo definito il costo del gap e il costo delle coppie possiamo definire il costo di un allineamento M, che indichiamo con c(M). c(M) la somma dei costi delle colonne di M le quali corrispondo ai gap oppure alle coppie di simboli allineati in M. Ma come posso calcolare c(M) se non conosco M? 1. trovo la definizione ricorsiva dellottimo 2. la uso per ricostruire iterativamente lottimo

Definizione ricorsiva significa esprimere lottimo per X e Y in funzione dellottimo per i prefissi di X e Y. Costruisco un coefficiente che indica lottimo per sottoistanze: opt(i, j)
Analisi dei casi possibili per allineare X e Y: X = x[1]x[m] Y = y[1]y[n] 1 caso: esiste la coppia (x[m], y[n]) nellallineamento ottimo
12

Quaderno virtuale di teoria

opt(m,n) = opt(m-1, n-1) + x m y n 2 caso: uno tra x[m] e y[n] non si accoppia con niente. Poich non sono ammesse coppie che si incrociano in M si ha che o x[m] non si accoppia o y[n] non si accoppia. opt(m,n) = opt(m, n-1) + 3 caso: opt(m,n) = opt(m-1, n) + casi base: opt(0,n) = n opt(m,0) = m opt(i, j) = min{ opt(i-1, j-1) + xi y j , opt(i, j-1) + , opt(i-1, j) + } scelgo il minimo perch cerco il caso che produce il costo minimo. Vediamo un altro esempio di programmazione dinamica abbinato alle biotecnologie. Problema: Predizione della struttura secondario del RNA Input: stringa (sequenza) di RNA sullalfabeto {A,C,G,U} B =n B = b1b2 ...bn Output: struttura secondaria di B che ottimizza lenergia Esempio:

Vediamo le regole per definire la costruzione della struttura secondaria con ottima energia: 1. deve essere rispettata la complementazione delle basi: A U, U A, C G, G C 2. due basi che si accoppiano devono essere separate da almeno 4 basi 3. sono proibiti i crossing, cio due o pi ponti che collegano le basi non possono incrociarsi

Analizziamo meglio il problema e riscriviamo le condizioni da verificare Problema: predizione sequenza secondaria RNA Input: stringa di basi B = b1b2 ...bn Output: un insieme S = bi , b j tali che verificano (1), (2) e (3) } Obiettivo: massimizzare |S| (numero di coppie)

{(

Regole: 1. bi , b j S - se bi = A allora bj = U - se bi = U allora bj = A

13

Algoritmi e strutture dati 2

- se bi = C allora bj = G - se bi = G allora bj = C 2. Se bi , b j S allora i < j 4 3.

( ) Se (bi , b j ), (bk , bl ) S

con i < k allora non possiamo avere i < k < j < l

Risolviamo il problema con la programmazione dinamica tenendo conto che il costo il numero di ponti e lottimo il massimo numero di ponti. Poich lottimo per la stringa b1...bn richiede il calcolo dellottimo per sottoistanze date da sottostringhe bi bi +1...b j allora il parametro OPT che misura lottimo ha due indici Con OPT(i, j) indico lottimo per la stringa bi ...b j 1 caso (base): Se i >= j 4 allora OPT(i, j) = 0 2 caso: OPT(i, j) = max {OPT (i, t 1) + OPT (t + 1, j 1) + 1}
i t < j 4

3 caso: OPT(i, j) = OPT(i, j 1) Implementiamo, in pseudocodice, lalgoritmo


procedura RNA(B) begin n = length(B) for k = 5 to n-1 for i = 1 to n-k j = i + k calcolo M[i,j] usando le regole end for end for end return M[1, n] //che lottimo

Applicazioni concrete di programmazione dinamica (31 / 1 / 2008)


Quando si applica la programmazione dinamica (PD) ? In problemi di ottimizzazione in cui il numero di soluzione da determinarsi delle sottoistanze polinomiale nellinput. In ogni applicazione della programmazione dinamica si individuano Problema (es.: LCS, allinamento, ) Input Output ammissibile [o soluzione] (sottosequenza comune, ) Obiettivo: massimizzare o minimizzare Lottimo costruibile a partire da ottimi di sottoistanze del problema. Lottimo p calcolato mediante una relazione ricorsiva detta ricorrenza di programmazione dinamica. Abbiamo gi visto esempi di questa riccorrenza, tra cui LCS, allineamento, predizione RNA. Vediamo quante sottochiamate ricorsive distinte posso avere: 1 j m opt(i, j) con 1 i n nel caso peggiore il numero di chiamate n m che polinomiale Vediamo, ora, le fasi della programmazione dinamica:
14

Quaderno virtuale di teoria

Fase 1: a) Costruire la ricorrenza di PD per calcolare lottimo b) Calcolo lottimo mediante un procedimento iterativo (bottom up) riempiendo una matrice che memorizza le chiamate ricorsive distinte Fase 2: ricostruzione di una soluzione ottima a) Si tiene traccia di come si sono ottenuti gli ottimi per sottoistanze (ad esempio le frecce che abbiamo usato per LCS) b) Si disegna una procedura top-down (ricorsiva) per elencare gli elementi che formano una soluzione ottima

Grafi (31 / 1 / 2008)


La relazione binaria su un insieme finito E di elementi consiste in un insieme R di coppie in E, R EE. Esempio: E = {paolo, pieno, gira, chiara} R1 = {(paolo, chiara), (pieno, chiara)} R2 = {(pieno, gira), (paolo, chiara)} Il grafo serve a rappresentare relazioni binarie. Grafo diretto (o orientato) G = (A, V) Dove V lelenco degli elementi che costituiscono i vertici di V A sono gli archi e sono relazioni su V, cio un insieme di coppie in V V Disegnamo il grafo rappresentato dallesempio R1:

Un vertice v adiacente a un vertice u se i due vertici hanno un arco che li collega.


Grafo non orientato G = (A, V) dove A un insieme di coppie nor ordinate di vertici A = {(u , v ) : u V , v V }

Il grado di un nodo di un grafo non orientato indicato dagli archi collegati. Nel grafo orientato si indica con in-degree il numero di archi entranti nel nodo e con out-degree il numero di archi uscenti dal nodo. Un cammino una sequenza di vertici < v0 , v1 ,..., v n > tale che 0 i n (vi , vi +1 ) A . Se esiste un cammino da v0 a vk diciamo che vk raggiungibile da v0. La misura del cammino misurata con gli archi. Se tutti i vertici sono distinti si dice cammino semplice, mentre se il primo vertice coincide con lultimo sono in presenza di un ciclo. Un ciclo semplice se consiste di tutti i vertici distinti eccetto quello di inizio e quello di fine, mentre non semplice se ne cammino ho un vertice che si ripete due o pi volte e questo vertice non ne quello di inizio, ne quello di fine. La connettivit indica che per ogni coppia di vertici, questi vertici sono raggiungibili.
15

Algoritmi e strutture dati 2

In un grafo orientato e connesso non detto che si verifichi la propriet di raggiungibilit.

Grafo G non orientato: G connesso se e solo se per ogni coppia divertici (u,v) u raggiungibile da v (in questo caso vale anche il viceversa perch il grafo non orientato). La raggiungibilit una relazione binaria su coppie di vertici e gode delle propriet di riflessivit, transitivit e di simmetria. La relazione si dice di equivalenza quando tutte le propriet sono valide. Essendo la raggiungibilit una relazione di equivalenza esiste, quindi, una partizione in classi di equivalenza dove non presente ambiguit
Riprendiamo le propriet delle relazioni. Le relazioni possono essere: Riflessive a in relazione con a Transitive a in relazione con b e b in relazione con c implica che a in relazione con c Simmetriche a in relazione con b implica che b in relazione con a
Definizione di componente connessa: una classe di equivalenza rispetto alla relazione di raggiungibilit

Il grafo G non connesso ma ha 3 componenti connesse:

Grafo G orientato: G fortemente connesso se e solo se per ogni coppia di vertici (u, v) u raggiungibile da v e v raggiungibile da u. Questo grafo fortemente connesso Questo grafo non fortemente connesso

Sono in presenza di un grafo semplice (orientato o non orientato) quando tra una determinata coppia di vertici ho un arco solo (se il grafo non orientato) oppure ho due archi orientati in senso opposto che li collega; altrimenti sono in presenza di un multigrafo
16

Quaderno virtuale di teoria

I cappi sono ammessi solo su in grafi orientati.


Rappresentazione in memoria dei grafi Ci sono due modi standard di rappresentare un grafo: liste di adiacenza e matrici di adiacenza.

La lista di adiacenza costituita da una lista Adj[u] per ogni vertice u appartenente a V. Nel caso peggiore la lista ha lunghezza |V| per ogni vertice. In un grafo non orientato per ogni adiacenza c anche la sua opposta. La matrice di adiacenza utilizza gli indici della matrice come identificatori dei vertici. Ogni cella della matrice ha valore 1 se esiste larco che collega i due vertici rappresentati dagli indici, altrimenti ha valore 0. La dimensione della matrice sar sempre V V
Visita in ampiezza di un grafo (BFS Breadth First Search): Dato un grafo G = (V, E) ed un vertice s (sorgente) la ricerca in ampiezza visita sistematicamente, partendo dalla sorgente s, il grafo per scoprire tutti i vertici che sono raggiungibili da s. Nel contempo viene calcolata anche la distanza di ogni vertice dal nodo sorgente.

Vediamo come possibile realizzare questo algoritmo di ricerca: Per ogni vertice devono conoscere gli adiacenti (memorizzo il grafo con liste di adiacenza) Mi serve una coda (FIFO) per memorizzare lordine con cui visitare i vertici di G Ho bisogno di un sistema di colorazione che mi identifichi se un vertice gi stato visitato, deve essere visitato e se sono gi stati visitati i suoi adiacenti Struttura dellalgoritmo BFS(G, s) //G il grafo e s il nodo sorgente (1) Inizializzazione Inizializzo la colorazione dei vertici c[v] = white vertice non visitato c[v] = grey vertice visitato c[v] = black ho visitato tutti i vertici adiacenti al vertice v Inizializzo la distanza dalla sorgente per ogni vertice escluso la sorgente d[v] = infinito, mentre d[s] = 0 Inizializzo il parent di ogni vertice a nil p[v] = nil
For each v {s} V(G) do c[v] = white d[v] = infinity p[v] = nil End for c[s] = grey //la sorgente il primo nodo ad essere visitato d[s] = 0 p[s] = nil

(2) inserimento di s nella coda Q


Enqueue(Q, s)

(3) Fase iterativa Estraggo il nodo in testa alla coda Q Trovo i suoi adiacenti e li inserisco nella coda Q dopo aver aggiornato i parametri relativi ai vertici (solo per i vertici adiacenti non visitati)
17

Algoritmi e strutture dati 2

while not Empty(Q) do u = Head(Q) for each v Adj[u] do if c[v] = white then p[v] = u c[v] = grey d[v] = d[v] + 1 Enqueue(Q, v) End if End for Dequeue(Q, u) //tolgo u dalla coda c[u] = black end while

Analizziamo il tempo di calcolo dellalgoritmo. Linizializzazione del grafo ha costo O(|V|) Per la parte iterativa la colorazione ci garantisce che un vertice viene visitato il pi una volta. Il costo del ciclo while dipende dagli adiacenti di un vertice; per memorizzare il grafo abbiamo usato liste di adiacenza, una lista di adiacenza ha dimensione |V| + 2*|E| se il grafo non orientato e ha dimensione |V| + |E| se il grafo orientato; siccome il ciclo while scorre sugli adiacenti O(|V| + |E|). In totale lalgoritmo O(n + m) dove n rappresenta il numero di vertici e m il numero di archi.
Visita in profondit di un grafo (DFS Depth First Search): lalgoritmo DFS ha una sottofunzione ricorsiva e una iterativa. Chiamiamo la procedura iterativa DFS e la procedura ricorsiva DFS_visit

Struttura della procedura DFS(G) Inizializzazione del grafo G c[v] = bianco per ogni v appartenente a V(G) f(v) un contatore che indicher il tempo di visita, ma non lo utilizzeremo per il momento p[v] il padre di v Sostanzialmente linizializzazione la stessa dellalgoritmo BFS Iterazione: per ogni vertice bianco v effettuo la chiamata alla funzione DFS_visit Osservazione: ho tante chiamate della DFS_visit quante sono le componenti connesse di G (per un grafo non orientato) Struttura della procedura DFS_visit(G, v) Coloro v di grigio Per ogni u colorato di bianco appartenente ad Adj[v] ripeto la chiamata DFS_visit Coloro v di nero Pseudocodice:
sub DFS(G) for each v appartenente V do c[v] = white; p[v] = null; end for for each v appartenente V do if c[v] = white then DFS_visit(G, v) end for end sub

18

Quaderno virtuale di teoria


sub DFS_visit(G, v) c[v] = grey; for each u appartenente Adj[v] do if c[u] = white then p[u] = v; DFS_visit(G, u) End if end for c[v] = black; end sub

Il costo di DFS ancora O(n + m). Nellalgoritmo DFS ci sono anche dei parameti di tempo d[v] e f[v] per ogni vertice v che indicano rispettivamente quando il vertice stato colorato di grigio (cio quando v visitato) e quando il vertice viene colorato di nero (cio quando tutti i suoi adiacenti sono stati visitati). Questi parametri di tempo non sono altro che contatori. Lalgoritmo DFS con i contatori di tempo viene cos modificato:
sub DFS(G) for each v appartenente V do c[v] = white; p[v] = null; end for time = 0; //contatore di tempo for each v appartenente V do if c[v] = white then DFS_visit(G, v) end for end sub sub DFS_visit(G, v) c[v] = grey; time = time + 1; d[v] = time; for each u appartenente Adj[v] do if c[u] = white then p[u] = v; DFS_visit(G, u) End if end for time = time + 1; f[v] = time; c[v] = black; end sub

In generale lalgoritmo DFS su grafi non orientati calcola le componenti connesse, mentre se un grafo orientato non possiamo contare le componenti connesse.
Grafo pesato Nel grafo pesato ogni arco etichettato con un costo numerico dato dalla funzione peso che definita come: w : E R + Rappresentiamo G con la matrice di adiacenza. Per ogni coppia di vertici i e j indichiamo con wij il costo dellarco tra i e j come segue: w(i, j ) N {0} se (i, j ) E wij = 0 se i = j se (i, j ) E

Il problema si dice ben posto se nel grafo non figurano cicli la cui somma dei pesi risulta negativa.
19

Algoritmi e strutture dati 2

Chiamiamo W la matrice di adiacenza che rappresenta il grafo G. Indichiamo il cammino che va da i a j con ij =< vi1 , vi 2 ,..., vik > e con c( ij ) il costo del cammino

ij . Il costo del cammino la somma dei costi degli archi che compongono il cammino
c( ij ) = w(vil , vil +1 ) per un i fissato.
l =1 k 1

Vogliamo calcolare il costo del cammino minimo tra due vertici e voglio anche sapere quali sono i vertici coinvolti nel cammino. Questo il problema dellAll Pairs Shortest Path (APSP); voglio conoscere il cammino minimo tra tutte le coppie di vertici di un grafo G pesato. Input: G = (V, E, w) Questione: (i, j ) V V trova il cammino ij tale che c( ij ) minimo Output: siccome APSP un problema di ottimizzazione. Gli ottimi sono dati da una matrice dei costi, la soluzione ottima per ogni i, j un cammino, cio un elenco di vertici. La soluzione composta, quindi, da 2 parti. Per la soluzione possiamo sfruttare la propriet della sottostruttura ottima: dato un cammino minimo tra i vertici u e v (che indichiamo con Puv) e dato z, un vertice intermedio, lungo il cammino Puv si ha che il cammino da v a z e quello da z a u devono essere minimi in termini di costo. Sia D(i,j) il costo di un cammino minimo da i a j. Come esprimiamo D(i,j) in maniera ricorsiva? Lidea quella che per andare da i a j devo passare dal predecessore di j. Siccome non abbiamo la possibilit di sapere quali sono i predecessori di un vertice coinvolti nel cammino minimo dobbiamo trovare un sistema per inserire dei vertici e fare il controllo se possono o non possono appartenere al cammino minimo. Lidea dellalgoritmo quella di nominare i vertici con valori tra 1 e k. Indico con D (k ) (i, j ) il costo di un cammino minimo da i a j che utilizza come soli possibili vertici intermedi i vertici che hanno nome 1,2,,k Equazione di ricorsione di Floid: D (k ) (i, j ) = min D (k 1) (i, j ), D (k 1) (i, k ) + D (k 1) (k , j )
D
(0 )

(i, j ) = w[i, j ] = wij

Per riempire ogni matrice D al passo corrente in base al vertice considerato occorre un tempo O(n2); siccome i vertici vanno da 1 a n ho n matrici D quindi il costo dellalgoritmo O(n3). Lobiettivo dellalgoritmo anche quello di fornire come output anche i vertici che compongono il cammino minimo. Per sapere i vertici che compongono il cammino minimo dobbiamo tener traccia dei predecessori di un vertice in una matrice. (k ) (i, j ) il predecessore di j in un cammino ottimo da i a j passante per 1,2,,k se D (k ) (i, j ) = D (k 1) (i, k ) + D (k 1) (k , j ) (k 1) (k , j ) La procedura che stampa lelenco dei vertici ricorsiva:
sub PrintPath( , inizio, fine) //inizio e fine indicano i vertici di inizio e fine if (inizio = fine) then PrintPath( print fine;
(n ) (n )

(k ) (i, j ) =

(k ) ( k 1) ( k 1) (i, j ) se D (i, j ) = D (i, j )

, inizio,

(n ) (inizio, fine ) )

20

Quaderno virtuale di teoria


end if end sub

Algoritmo di Dijkstra Problema: trovare un cammino minimo da s (sorgente) a v (qualsiasi vertice) in un grafo pesato rappresentato con liste di adiacenza. La soluzione di tipo Greedy.

Idea: STEP 1: assegno ad ogni vertice v V {s} il parametro d[v] che la stima della distanza da s a v p[v] il predecessore di v lungo un cammino minimo da s a v questi parametri vengono inizializzati come segue: per ogni vertice v d[v] = infinito p[v] = nil d[s] = 0 STEP 2: estraggo (trovo) il vertice v V S che ha valore d[v] minimo (allinizio questo vertice la sorgente). Dobbiamo, anche, gestire un insieme S che conterr i vertici di cui conosco la distanza minima dalla sorgente. Inserisco il vertice v in S. Aggiorno la distanza d[u] per ogni vertice u che adiacente a v. Itero lo step 2 finch S = V, cio fino a che conosco le distanze minime dalla sorgente di tutti i vertici del grafo. Pseudocodice:
Procedura Dijkstra(G as AdjacenceList = (V, E, w), s) Begin For each v appartenente a V p(v) = nil; d(v) = infinity; End for d(s) = 0; S = {}; Q = V //coda di priorit su d[v] per la gestione dei vertici da estrarre while not isEmpty(Q) do u = ExtractMin(Q); S = S + {u}; for each v appartenente a Adj[u] Relax(u, v, w); //vertice da cui passo, vertice di cui devo //aggiornare la distanza, funzione costo end for End while End Procedura Relax(u, v, w) Begin if(d[v] > d[u] + w(u,v)) then d[v] = d[u] + w(u,v); p[v] = u; End if End

21

Algoritmi e strutture dati 2

Siccome una tecnica greedy devo dimostrarne la correttezza. PR1: Devo dimostrare che quando u S d [u ] = (s, u ) , con (s, u ) indico il costo minimo di un cammino da s a u. Prima che u S d [u ] (s, u ) La dimostrazione si basa sulle seguenti ipotesi: il primo vertice non la sorgente S al momento in cui aggiungo u non vuoto Deve esistere un cammino da s e u Assumiamo per assurdo che esiste un vertice u S tale che d [u ] > (s, u ) . Sia u il primo vertice che aggiungo ad S per il quale non vale PR1. Se d (u ) > (s, u ) poich non vale PR1 allora esiste p tale che p = (s, u ) . Dato p (percorso minimo da s a u) esistono due vertici x e y tali che 1. x S 2. y S 3. (x, y) un arco in p Poich x S , d (x ) = (s, x ) , inoltre poich (x, y ) p : d ( y ) = (s, x ) + w(x, y ) = (s, y ) d (u ) > (s, u ) (s , y ) (s , u ) Allora d ( y ) = (s, y ) (s, u ) < d (u ) , ne consegue che d ( y ) < d (u ) , ma poich lalgoritmo sceglie il vertice che sta fuori S che ha d[v] minimo ottengo una contraddizione. Lalgoritmo, in questo caso, avrebbe dovuto scegliere y e non u da aggiungere ad S Il costo di questo algoritmo O(V + E log V Shortest Path).

) che meglio dellalgortimo di Floyd (All Pairs

22