introduzione
Dato un grafo diretto e pesato in cui i pesi degli archi possono essere anche negativi e fissato un suo nodo , si vuole determinare il costo minimo dei cammini che conducono da a tutti gli altri nodi del grafo.
- se non esiste un cammino, il costo sarà considerato infinito.
ciclo negativo
Un ciclo negativo è un ciclo diretto in un grafo la cui somma dei pesi degli archi è negativa.
se in un cammino tra i nodi e è presente un nodo che appartiene ad un ciclo negativo, allora non esiste un cammino di costo minimo tra e .
Infatti, ripassando più volte per il ciclo, si abbasserebbe arbitrariamente il costo del cammino.
proprietà
Se il grafo non contiene cicli negativi, allora per ogni nodo raggiungibile dalla sorgente esiste un cammino di costo minimo che attraversa al più archi.
- infatti, se un cammino avesse più di archi, almeno un nodo verrebbe ripetuto formando un ciclo (e, visto che il grafo non ha cicli negativi, rimuovere cicli dal cammino non aumenterà il suo costo complessivo, quindi esisterà sempre un cammino ottimale di lunghezza )
L’algoritmo di Bellman-Ford risolve il problema dei cammini di costo minimo in .
Esso definisce la seguente tabella di dimensione :
- costo di un cammino minimo da al nodo di lunghezza al più
Si costruisce la soluzione al problema determinando i valori delle righe della tabella.
- la soluzione sarà data dagli valori che si troveranno nell’ultima riga:
(il costo minimo per andare da al generico nodo sarà )
distanza di da se stesso
I valori della prima riga della tabella saranno tutti tranne , che varrà .
- si avrà anche che (chiaramente il costo di un cammino da a sarà sempre )
Per definire la regola che permette di calcolare i valori delle celle con della riga , bisogna distinguere due casi:
- il cammino di lunghezza al più da ha lunghezza esattamente
- il cammino di lunghezza al più da ha lunghezza inferiore a
- ci troviamo nel caso in cui esiste un cammino minimo di lunghezza al più ad un nodo e un arco
- si prende, tra tutti gli archi che portano a , quello che costa di meno, e si somma il suo costo al percorso che portava a (che si trova nella riga precedente)
La formula che include entrambi i casi è:
formula per tutte le righe
implementazione
implementazione più efficiente: grafo trasposto
poiché nel calcolo della formula è necessario più volte conoscere gli archi entranti nel generico nodo , conviene usare il grafo trasposto di .
def Trasposto(G):
n = len(G)
GT = [ [] for _ in G]
for i in range(n):
for j, costo in G[i]:
GT[j].append((i, costo))
return GT
def CostoCammini(G, s):
n = len(G)
inf = float('inf')
T = [ [inf]*n for _ in range(n)]
T[0][s] = 0
GT = Trasposto(G)
for i in range(1, n): # righe della matrice
for j in range(n): # nodi per ogni riga
T[i][j] = T[i-1][j]
for x, costo in GT[j]:
T[i][j] = min(T[i][j], T[i-1][x] + costo)
return T[n-1]
- l’inizializzazione della tabella costa
- la costruzione di costa
- i tre
for
annidati non hanno limite superiore non (come potrebbe sembrare), perché:- i due
for
più interni hanno costo totale (scorrono tutte le liste di adiacenza in - infatti, se nel primofor
si itera su tutti i nodi, non si entrerà nel secondo perché non ci saranno archi entranti)
- i due
La complessità totale è quindi
ottimizzazioni
- Se la riga della tabella è uguale alla riga , anche le righe successive non varieranno: in questo caso, conviene terminare l’algoritmo senza calcolare le righe restanti.
- questo accorgimento non migliora il costo asintotico dell’algoritmo ma può fare differenza nella pratica
- Non serve memorizzare l’intera tabella : bastano le ultime due righe. L’algoritmo può essere facilmente modificato per utilizzare memoria e non
trovare cicli negativi
Una piccola modifica all’algoritmo permette di scoprire se il grafo contiene cicli negativi raggiungibili da .
- basta infatti calcolare una riga in più della tabella (quindi la riga ) con il costo dei cammini minimi di lunghezza al più
- se le righe e risultano diverse, allora nel grafo è presente un ciclo negativo
trovare i cammini
Oltre al costo dei cammini, è possibile usare Bellman-Ford per costruire l’albero dei cammini minimi.
- basta mantenere, per ogni nodo , il suo predecessore: il nodo che lo precede nel cammino
- il valore di verrà aggiornato ogni volta che il valore cambia (ovvero diminuisce) perché è stato trovato un cammino migliore
def costo_cammini1(G, s):
T = [[float('inf')]*len(G) for _ in range(len(G))]
P = [-1]*len(G)
GT = trasposto(G)
T[0][s] = 0
P[s] = s
for i in range(1,n)
for j in range(n):
if j==s:
T[k][j] = 0
else:
for x,costo in GT[j]:
if T[k-1][x]+costo < T[k][j]:
T[k][j] = T[k-1][x]+costo
P[j] = x
# ^ l'attuale cammino minimo
# che arriva a j lo fa tramite x
return T[len(G)-1], P
Al termine dell’algoritmo:
- ⇒ è raggiungibile da
- conterrà il nodo che precede nel cammino minimo da a
- ⇒ non è raggiungibile da
- conterrà -1