⋔ Grafi e ottimizzazione

Floyd-Warshall: le distanze minime tra tutte le coppie

Floyd-Warshall calcola le distanze minime tra ogni coppia di vertici con tre cicli annidati: idea, codice, ricostruzione dei percorsi e alternative.

Dijkstra e Bellman-Ford rispondono alla domanda «quanto dista ogni vertice da questa sorgente?». A volte, però, serve la tabella completa: la distanza tra ogni coppia di vertici. È il caso della classica tabella delle distanze chilometriche stampata sulle vecchie carte stradali, di una matrice dei tempi di viaggio tra i magazzini di un'azienda di logistica, o dell'analisi di una rete aziendale in cui si vuole sapere quanti salti separano ogni coppia di macchine.

L'algoritmo di Floyd-Warshall risolve questo problema con un codice di poche righe, che si scrive a memoria e difficilmente contiene errori.

L'idea: vertici intermedi ammessi, uno alla volta

Numeriamo i vertici da 0 a n − 1. Chiamiamo D_k(i, j) la lunghezza del cammino più corto da i a j che usa come vertici intermedi solo quelli con indice minore di k.

  • Con k = 0 non sono ammessi intermedi: D_0(i, j) è il peso dell'arco diretto, oppure infinito se manca.
  • Passando da k a k + 1 si ammette anche il vertice k. Il cammino migliore o non lo usa (e resta D_k(i, j)), oppure ci passa una volta sola: da i a k e poi da k a j.

Quindi:

D_{k+1}(i, j) = min( D_k(i, j), D_k(i, k) + D_k(k, j) )

Dopo aver ammesso tutti i vertici si ottengono le distanze minime vere. È un esempio limpido di programmazione dinamica: un problema grande risolto combinando sottoproblemi più piccoli, in un ordine che garantisce che ciò che serve sia già stato calcolato.

Il codice

Una sola matrice basta, aggiornata sul posto:

def floyd_warshall(D):
    n = len(D)
    for k in range(n):
        for i in range(n):
            dik = D[i][k]
            if dik == float("inf"):
                continue
            for j in range(n):
                if dik + D[k][j] < D[i][j]:
                    D[i][j] = dik + D[k][j]
    return D

L'ordine dei cicli è fondamentale: k deve essere il ciclo più esterno. Scambiarlo con i o j è un errore classico che produce risultati sbagliati senza nessun messaggio d'errore.

La matrice iniziale ha 0 sulla diagonale, il peso dell'arco dove esiste e infinito altrove. Se tra due vertici ci sono più archi, si tiene il più leggero.

Ricostruire i percorsi

Le distanze da sole spesso non bastano: si vuole sapere anche per dove passare. Si mantiene una seconda matrice succ, dove succ[i][j] è il primo vertice da visitare dopo i per andare a j. All'inizio succ[i][j] = j per ogni arco esistente; ogni volta che il passaggio per k migliora la coppia (i, j), si pone succ[i][j] = succ[i][k]. Per ricostruire il percorso si parte da i e si segue succ fino ad arrivare a j.

Pesi negativi e cicli negativi

Floyd-Warshall accetta archi di peso negativo. Se esiste un ciclo negativo, alla fine almeno un elemento della diagonale risulta minore di zero: un vertice «dista da sé stesso» meno di zero. Basta quindi controllare la diagonale per sapere se i risultati sono affidabili.

Costo e limiti

Tre cicli annidati su n vertici: il tempo è proporzionale a n³ e la memoria a n². Qualche ordine di grandezza:

Vertici Celle della matrice Iterazioni del ciclo interno
100 10 mila 1 milione
1.000 1 milione 1 miliardo
10.000 100 milioni 1.000 miliardi

Fino a qualche centinaio di vertici è istantaneo anche in Python; a qualche migliaio diventa pesante senza librerie numeriche; oltre, conviene cambiare approccio. Per grafi grandi e sparsi è molto meglio lanciare Dijkstra da ogni vertice, con un costo complessivo di circa n · (n + m) log n, o l'algoritmo di Johnson se ci sono pesi negativi.

Una variante utile: la chiusura transitiva

Sostituendo la somma con l'«e» logico e il minimo con l'«o» logico, lo stesso schema risponde alla domanda «da i si può raggiungere j?». Si ottiene la chiusura transitiva del grafo, nota anche come algoritmo di Warshall.

Esempi pratici:

  • in un sistema di permessi con ruoli che ereditano da altri ruoli, la chiusura dice quali permessi ha effettivamente ogni ruolo;
  • in una rete con regole di firewall, dice quali segmenti possono comunicare, anche passando per altri segmenti;
  • in un organigramma, dice chi è superiore di chi, a qualunque livello.

In sintesi

  • Floyd-Warshall calcola le distanze minime tra tutte le coppie di vertici.
  • Ammette i vertici intermedi uno alla volta: è programmazione dinamica.
  • Il ciclo su k deve essere il più esterno; il costo è n³ in tempo e n² in memoria.
  • Accetta pesi negativi; un valore negativo sulla diagonale segnala un ciclo negativo.
  • Con operazioni logiche al posto di somma e minimo calcola la chiusura transitiva.
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