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 uU cuv
Cmin[v] = min uU cuv(cuv = + se [i,j] E)
4
v1
U
v3
v5
vU
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!