Algoritmi greedy Gli algoritmi greedy in genere non sono esatti, cioè determinano soluzioni non necessariamente ottime Per il problema dell’albero ricoprente sono esatti • Algoritmo di Prim – conserva connessione e aciclicità – tende alla ricopertura • Algoritmo di Kruskal – conserva aciclicità e ricopertura – tende verso la connessione Algoritmo di Prim Prim(G,c) U = {v1}; X = While U V do e* = (u,v) = arg minu U, v V\U cuv X = X {e*} U = U {v} • Parte con un solo vertice (arbitrario) e nessun arco (sottografo connesso e aciclico, ma non ricoprente) • Aggiunge ogni volta l’arco minimo del taglio individuato dall’albero corrente conserva connessione e aciclicità Esempio di esecuzione dell’algoritmo di Prim 1 v1 v2 4 2 2 3 6 4 U = {v1} X =Ø 4 2 6 4 2 v5 4 2 5 1 v1 4 3 v3 U = {v1, v2} X = {[v1,v2]} v3 5 2 6 v5 3 6 v2 2 2 2 v4 1 v1 v2 4 v3 5 v4 v4 2 v5 1 v1 4 v4 v2 2 2 v5 5 3 v3 U = {v1, v2, v5} X = {[v1,v2], [v1,v5]} Esempio di esecuzione dell’algoritmo di Prim 1 v1 4 2 6 4 v4 v5 5 v2 2 4 3 2 6 2 1 v1 v3 U = {v1, v2 , v3, v5} X = {[v1,v2], [v1,v5], [v3,v5]} 4 v4 v2 2 2 v5 3 v3 5 U = {v1, v2 , v3 , v4, v5} = V X = {[v1,v2], [v1,v5], [v3,v5], [v4,v5]} costo: 9 Esempio di esecuzione dell’algoritmo di Prim Altre soluzioni ammissibili (di costo non inferiore) 1 v1 4 2 6 4 v4 v2 2 4 v3 4 5 1 v1 4 2 6 4 v4 2 6 2 v5 2 2 v5 5 v4 v2 1 v1 costo: 16 3 costo: 11 v2 2 2 v5 3 v3 5 3 v3 costo: 9 Altra soluzione ottima, ottenibile cambiando lato al passo 2 Complessità dell’algoritmo di Prim La complessità dipende da come si determina e* Idea 1: scandire tutti i lati: O(mn) Ogni ciclo While richiede O(m) operazioni elementari (se T(U,X) è una lista di lati più un vettore di incidenza 4m + 4 operazioni) • 2m per l’appartenenza al taglio [v,w] (S), [v,w] A • 4 per aggiornare X e U (incremento contatore, aggiunta estremi nella lista, cambio di bit nel vettore) • 2m per confrontare il costo minimo con cuv ed eventualmente aggiornarlo Una versione O(n2) N.B.: Ad ogni passo nel taglio cambiano solo gli archi incidenti nell’ultimo vertice aggiunto a U Idea 2: per ogni vertice v fuori da U conservare il lato minimo incidente in v del taglio indotto da U (e il suo costo) Pred[v] = arg min uU cuv Cmin[v] = min uU cuv(cuv = + se [i,j] E) 4 v1 U v3 v5 vU 2 6 Pred[v] = v3 Cmin[v] = 2 Una versione O(n2) Prim2(G,c) For v = 1 to n do Uinc[v] = 0 Cmin[v] = + Pred[v] = 0 { U = {v1}; X = } Uinc[1] = 1 Cmin[1] = 0 For v Adj(1) do Cmin[v] = c1v Pred[v] = 1 m=0 … … While m < n-1 do {U V} vMin = 0 min = + For v = 2 to n do If (Uinc[v] = 0) and (Cmin[v] < min) min = Cmin[v] vMin = v Uinc[vMin] = 1 {U = U {v}} For v Adj(vMin) do If (Uinc[v] = 0) and (cvvMin<Cmin[v]) Cmin[v] = cvvMin Pred[v] = vMin Esecuzione della versione O(n2) Prim2(G,c) For v = 1 to n do Uinc[v] = 0 Cmin[v] = + Pred[v] = 0 { U = {v1}; X = } Uinc[1] = 1 Cmin[1] = 0 For v Adj(1) do Cmin[v] = c1v Pred[v] = 1 1 v1 4 2 2 3 6 4 v3 5 v4 U = {v1} X= Ø Uinc = [1 0 0 0 0] Cmin = [0 1 4 6 2] Pred = [1 1 1 1 1] 2 v5 1 v1 4 2 v2 2 3 6 4 m=0 … v2 v4 2 v5 5 v3 Esecuzione della versione O(n2) UU=={v{v 1,v 1}2} X =X{[v = 1Ø,v2]} … While m < n-1 do {U V} vMin = 0 min = + For v = 2 to n do If (Uinc[v] = 0) and (Cmin[v] < min) min = Cmin[v] vMin = v Uinc[vMin] = 1 {U = U {v}} For v Adj(vMin) do If (Uinc[v] = 0) and (cuv < Cmin[v]) Cmin[v] = cuv Pred[v] = vMin 1 v1 4 2 v2 2 3 6 4 v4 2 v5 v3 5 Uinc = [1 01 0 0 0] Cmin = [0 1 3 6 2] Pred = [1 1 2 1 1] Esecuzione della versione O(n2) … While m < n-1 do {U V} vMin = 0 min = + For v = 2 to n do If (Uinc[v] = 0) and (Cmin[v] < min) min = Cmin[v] vMin = v Uinc[vMin] = 1 {U = U {v}} For v Adj(vMin) do If (Uinc[v] = 0) and (cuv < Cmin[v]) Cmin[v] = cuv Pred[v] = vMin U = {v1, v2, v5} X = {[v1,v2], [v1,v5]} 1 v1 4 2 6 4 v4 v2 2 2 v5 3 v3 5 Uinc = [1 1 0 0 1] Cmin = [0 1 2 4 2] Pred = [1 1 5 5 1] Esecuzione della versione O(n2) … While m < n-1 do {U V} vMin = 0 min = + For v = 2 to n do If (Uinc[v] = 0) and (Cmin[v] < min) min = Cmin[v] vMin = v Uinc[vMin] = 1 U = {v1, v2 , v3, v5} X = {[v1,v2], [v1,v5], [v3,v5]} v1 4 2 6 4 {U = U {v}} v4 For v Adj(vMin) do If (Uinc[v] = 0) and (cuv < Cmin[v]) Cmin[v] = cuv Pred[v] = vMin 1 v5 v2 2 2 3 v3 5 Uinc = [1 1 1 0 1] Cmin = [0 1 2 4 2] Pred = [1 1 5 5 1] Esecuzione della versione O(n2) … While m < n-1 do {U V} vMin = 0 min = + For v = 2 to n do If (Uinc[v] = 0) and (Cmin[v] < min) min = Cmin[v] vMin = v Uinc[vMin] = 1 {U = U {v}} For v Adj(vMin) do If (Uinc[v] = 0) and (cuv < Cmin[v]) Cmin[v] = cuv Pred[v] = vMin U = {v1, v2 , v3 , v4, v5} X = {[v1,v2], [v1,v5], [v3,v5], [v4,v5]} 1 v1 4 2 6 4 v4 v2 2 2 v5 3 v3 5 Uinc = [1 1 1 1 1] Cmin = [0 1 2 4 2] Pred = [1 1 5 5 1] Complessità – Prim versione O(n2) 1 4 2 3 For v = 1 to n do … For v Adj(1) do … 1 L’inizializzazione richiede O(n) 2 Il primo ciclo For interno richiede O(n) While m < n-1 do … For v = 2 to n do … For v Adj(vMin) do … 3 Il secondo ciclo For interno scorre la stella uscente da vMin L’analisi aggregata dà O(m) per l’insieme delle sue esecuzioni 4 I due cicli For interni sono eseguiti n - 1 volte O(n2) Una versione O(m log n) L’operazione più costosa è la ricerca del lato di costo minimo Idea 3: conserviamo gli O(n) lati candidati in uno heap • creiamo lo heap dei lati candidati (BuildHeap): O(n) in più • leggiamo direttamente l’elemento minimo: O(n) anziché O(n2) • Aggiorniamo lo heap quando l’albero cresce di un vertice v; ogni lato incidente in v potrebbe entrare nello heap che andrebbe quindi aggiornato (Heapify): O(log n) anziché O(1) Complessivamente, esaminiamo O(m) archi Correttezza dell’algoritmo di Prim Dato un albero ricoprente T, lato di diminuzione è un lato e T che, aggiunto a T, vi induce un ciclo C T {e} contenente un lato f C \ {e} di costo superiore (ce < cf ) f ce < cf C e T c(T {e}\{f}) < c(T) Se esiste un lato e di diminuzione rispetto a un albero T ricoprente, si può ridurre il costo di T scambiando il lato e con almeno un lato f di C Correttezza dell’algoritmo di Prim Per assurdo: l’albero T(V,X) restituito dall’algoritmo di Prim non sia ottimo, e lo sia invece T*(V,X*) Seguiamo l’esecuzione di Prim, arrestandoci al primo arco e X, ma X* Aggiungendo e a X* ciclo C U Sia f (U) C v h Sicuramente ce cf e f T* Se ce = cf allora T* {e} / {f} è ottimo perché costa come T* Se ce < cf, e è un lato di diminuzione quindi T* non è ottimo!