⋔ Grafi e ottimizzazione

Bellman-Ford: cammini minimi con pesi negativi e cicli

Bellman-Ford spiegato: perché ripetere n−1 passate, come scoprire i cicli negativi e dove servono davvero i pesi negativi, dai cambi all'instradamento.

L'algoritmo di Dijkstra è veloce ed elegante, ma ha un limite netto: tutti gli archi devono avere peso non negativo. Quando questa condizione non vale, serve un approccio più paziente. L'algoritmo di Bellman-Ford è più lento, ma accetta pesi negativi e, soprattutto, sa riconoscere la situazione in cui il problema stesso non ha senso: la presenza di un ciclo negativo.

Quando compaiono pesi negativi

Una lunghezza stradale non è mai negativa, ma molti problemi modellati come cammini minimi sì:

  • guadagni e costi: in una sequenza di operazioni alcune costano e altre fanno guadagnare; un guadagno si rappresenta come peso negativo;
  • vincoli di differenza: sistemi di vincoli del tipo «l'evento B deve avvenire al massimo 3 ore dopo A» si traducono in un grafo dove alcuni archi hanno peso negativo, e le soluzioni sono date proprio dalle distanze minime;
  • tassi di cambio: passando per logaritmi, come vedremo, la ricerca di opportunità di arbitraggio diventa una ricerca di cicli negativi.

L'idea: rilassare tutto, più volte

Bellman-Ford non sceglie un ordine furbo in cui elaborare i vertici. Rilassa tutti gli archi, poi lo fa di nuovo, e ancora, per n − 1 volte in tutto.

def bellman_ford(n, archi, s):
    INF = float("inf")
    dist = [INF] * n
    dist[s] = 0
    for _ in range(n - 1):
        cambiato = False
        for u, v, w in archi:
            if dist[u] != INF and dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                cambiato = True
        if not cambiato:
            break
    for u, v, w in archi:
        if dist[u] != INF and dist[u] + w < dist[v]:
            raise ValueError("ciclo negativo raggiungibile dalla sorgente")
    return dist

Perché proprio n − 1 passate? Se non ci sono cicli negativi, un cammino minimo non ripete vertici, quindi usa al massimo n − 1 archi. Dopo la prima passata sono corrette tutte le distanze dei vertici raggiungibili con un arco lungo un cammino minimo; dopo la seconda quelle con due archi; dopo la k-esima quelle con k archi. Dopo n − 1 passate, tutte.

La variabile cambiato è un'ottimizzazione pratica importante: se in una passata nessuna stima migliora, le successive non cambierebbero nulla e si può uscire subito. Su molti grafi reali l'algoritmo termina dopo poche passate.

Il controllo finale: cicli negativi

Un ciclo negativo è un ciclo la cui somma dei pesi è minore di zero. Se è raggiungibile dalla sorgente, percorrerlo una volta in più abbassa sempre il costo, quindi la «distanza minima» non esiste: si può scendere all'infinito.

Bellman-Ford lo scopre con una passata supplementare. Se dopo n − 1 passate qualche arco si può ancora rilassare, c'è per forza un ciclo negativo. Seguendo all'indietro i puntatori al predecessore dal vertice migliorato, per n passi, si finisce sicuramente dentro il ciclo, e da lì lo si può ricostruire per mostrarlo.

Un esempio: l'arbitraggio sui cambi

Supponiamo di conoscere i tassi di cambio tra alcune valute. Un arbitraggio è una sequenza di conversioni che, partendo da una valuta, ritorna alla stessa con più denaro di prima: il prodotto dei tassi lungo il ciclo è maggiore di 1.

Il trucco è trasformare i prodotti in somme con i logaritmi. Si dà a ogni arco da X a Y il peso −log(tasso X→Y). Il prodotto dei tassi su un ciclo è maggiore di 1 esattamente quando la somma dei logaritmi è positiva, cioè quando la somma dei pesi è negativa. Cercare un arbitraggio significa quindi cercare un ciclo negativo, ed è un compito su misura per Bellman-Ford.

Nella realtà commissioni, spread e latenza riducono queste opportunità, ma il modello resta un esempio limpido di come una trasformazione dei pesi riconduca un problema nuovo a un algoritmo noto.

Costo e confronto con Dijkstra

Ogni passata esamina tutti gli m archi e le passate sono al massimo n − 1, quindi il costo nel caso peggiore è proporzionale a n · m. È molto più di Dijkstra, che costa circa (n + m) log n.

Aspetto Dijkstra Bellman-Ford
Pesi negativi no sì
Rileva cicli negativi no sì
Costo tipico (n + m) log n n · m
Struttura dati coda di priorità lista di archi

La regola pratica è semplice: se i pesi sono tutti non negativi si usa Dijkstra; altrimenti Bellman-Ford, o una sua variante.

Varianti e usi

  • Coda dei vertici modificati: invece di rilassare tutti gli archi a ogni passata, si rielaborano solo i vertici la cui stima è cambiata. Nei casi medi accelera molto, anche se il caso peggiore resta lo stesso.
  • Algoritmo di Johnson: per calcolare i cammini minimi tra tutte le coppie in grafi sparsi con pesi negativi, si usa una volta Bellman-Ford per «ripesare» gli archi rendendoli non negativi, poi si lancia Dijkstra da ogni vertice.
  • Instradamento distance-vector: alcuni protocolli di rete storici, come RIP, si basano su una versione distribuita della stessa idea: ogni router comunica ai vicini le proprie stime e le aggiorna in base a quelle ricevute.

In sintesi

  • Bellman-Ford calcola cammini minimi da una sorgente anche con pesi negativi.
  • Rilassa tutti gli archi per n − 1 passate, fermandosi prima se nulla cambia.
  • Una passata in più rivela i cicli negativi, in cui la distanza minima non esiste.
  • Con pesi −log(tasso), la ricerca di arbitraggi diventa ricerca di cicli negativi.
  • Costa n · m: va usato quando i pesi negativi ci sono davvero.
MM
Marco Marzaduri

Marco Marzaduri si occupa di cybersecurity, informatica forense e sviluppo web. Laureato in informatica, unisce il rigore dell'analisi tecnica all'attenzione per le persone che usano la tecnologia.

Leggi anche