Anteprima della struttura

Algoritmi sui grafi: BFS, Dijkstra e Kruskal dimostrati passo passo

Tre domande diverse, tre algoritmi: come arrivare con meno passaggi, come spendere meno lungo un percorso e come collegare tutti i punti con il costo totale minimo. Usiamo lo stesso grafo per distinguere con precisione questi obiettivi.

Articoli /algoritmi-grafi-bfs-dijkstra-kruskal
Algoritmi sui grafi: BFS, Dijkstra e Kruskal dimostrati passo passo

25 min

Tre domande diverse, tre algoritmi: come arrivare con meno passaggi, come spendere meno lungo un percorso e come collegare tutti i punti con il costo totale minimo. Usiamo lo stesso grafo per distinguere con precisione questi obiettivi.

Un grafo è formato da vertici V e archi E. Il nostro è semplice, non orientato e connesso: un collegamento si percorre in entrambi i sensi. Ha 6 vertici e 9 archi. I numeri sugli archi sono costi, non lunghezze del disegno. Esaminiamo i vicini in ordine alfabetico; a parità di peso ordiniamo gli archi per nome. Un cammino collega due vertici; un ciclo torna al punto iniziale senza ripetere gli altri vertici. Un albero è connesso e privo di cicli.

A–B: 4   A–C: 2   B–C: 1   B–D: 5   C–D: 8   C–E: 10   D–E: 2   D–F: 6   E–F: 3

Linee verdi continue: archi scelti o predecessori correnti; linee grigie tratteggiate: altri archi. In BFS i pesi sono omessi. In Dijkstra i predecessori intermedi possono cambiare. ∞ indica una distanza non ancora trovata.

1. BFS: il minor numero di passaggi

La visita in ampiezza ignora i pesi. Tutti gli archi valgono un passaggio. Partiamo da A e cerchiamo la distanza, misurata in numero di archi, verso ogni vertice. La coda FIFO estrae per primo chi è entrato per primo.

Assegna d(A)=0 e inserisci A nella coda. Estrai il primo vertice u; per ogni vicino v non ancora scoperto, assegna d(v)=d(u)+1, registra u come predecessore e inserisci v in coda. Marca v già quando lo inserisci, non quando lo estrai, per evitare duplicati. Ripeti finché la coda è vuota.

PassoVertice / arcoCoda / insieme definitivo / costoDistanze in ordine A, B, C, D, E, F
0[A]0, ∞, ∞, ∞, ∞, ∞
1A[B, C]0, 1, 1, ∞, ∞, ∞
2B[C, D]0, 1, 1, 2, ∞, ∞
3C[D, E]0, 1, 1, 2, 2, ∞
4D[E, F]0, 1, 1, 2, 2, 3
5E[F]0, 1, 1, 2, 2, 3
6F[]0, 1, 1, 2, 2, 3

Dopo A la coda è [B,C]. B scopre D; C scopre E, ma non riscrive D. D scopre F. Risultato: A–B–D–F ha 3 archi. Anche A–C–E–F ne ha 3: la soluzione può non essere unica. Il primo cammino costa 15, quindi meno passaggi non significa minor costo.

BFS: Stato iniziale (Passo 0). —
Stato iniziale (Passo 0). —
BFS: Stato intermedio (Passo 3). AB, AC, BD, CE
Stato intermedio (Passo 3). AB, AC, BD, CE
BFS: Stato finale (Passo 6). AB, AC, BD, CE, DF
Stato finale (Passo 6). AB, AC, BD, CE, DF

Perché funziona

  1. Invariante: la coda contiene vertici in ordine di distanza non decrescente; mentre si tratta il livello k, i nuovi vertici entrano in fondo al livello k+1.
  2. Base: A è al livello 0, corretto. Passo induttivo: se tutti i vertici fino al livello k hanno distanza corretta, ogni nuovo vicino di un vertice di livello k è raggiungibile in k+1 archi.
  3. Non può avere distanza minore: un suo predecessore su un cammino più corto sarebbe stato elaborato a un livello precedente e lo avrebbe già scoperto. Perciò la prima distanza assegnata è minima. I predecessori ricostruiscono un cammino minimo.

Con liste di adiacenza ogni vertice entra una volta e ogni arco viene esaminato due volte: tempo O(|V|+|E|), memoria ausiliaria O(|V|), oltre al grafo. In un grafo non connesso, da A si visita solo la sua componente.

Princeton · Algorithms, 4th Edition — BFS

2. Dijkstra: il cammino di costo minimo

Ora i pesi contano. Sono tutti non negativi: è l’ipotesi decisiva. d(v) è il costo del miglior cammino finora trovato, inizialmente infinito tranne d(A)=0. L’insieme S raccoglie i vertici con distanza definitiva.

Scegli fuori da S il vertice u con d(u) minimo. Inseriscilo in S. Per ogni vicino v ancora fuori da S, prova il rilassamento: d(v) ← min(d(v), d(u)+w(u,v)). Se migliora, aggiorna il predecessore di v. Continua; se il minimo è infinito, i vertici restanti sono irraggiungibili.

PassoVertice / arcoCoda / insieme definitivo / costoDistanze in ordine A, B, C, D, E, F
0{}0, ∞, ∞, ∞, ∞, ∞
1A{A}0, 4, 2, ∞, ∞, ∞
2C{A, C}0, 3, 2, 10, 12, ∞
3B{A, C, B}0, 3, 2, 8, 12, ∞
4D{A, C, B, D}0, 3, 2, 8, 10, 14
5E{A, C, B, D, E}0, 3, 2, 8, 10, 13
6F{A, C, B, D, E, F}0, 3, 2, 8, 10, 13

Dopo A: B=4 e C=2. C migliora B a 3, propone D=10 ed E=12. B migliora D a 8. D migliora E a 10 e propone F=14. E migliora F a 13. Il cammino ottimo A–C–B–D–E–F costa 2+1+5+2+3=13, ma usa 5 archi: più del cammino BFS.

Dijkstra: Stato iniziale (Passo 0). —
Stato iniziale (Passo 0). —
Dijkstra: Stato intermedio (Passo 3). BC, AC, BD, CE
Stato intermedio (Passo 3). BC, AC, BD, CE
Dijkstra: Stato finale (Passo 6). BC, AC, BD, DE, EF
Stato finale (Passo 6). BC, AC, BD, DE, EF

Perché funziona

  1. Ogni distanza finita è il costo di un cammino realmente trovato: non sottostima quindi il costo ottimo. Supponiamo corrette le distanze già rese definitive in S.
  2. Se il vertice minimo u avesse un cammino più economico, prendiamo il primo vertice y di quel cammino fuori da S e il suo predecessore x in S. Quando x è stato elaborato, il rilassamento ha assegnato a y un costo non superiore a quello del prefisso del cammino.
  3. Poiché i pesi restanti sono non negativi, quel prefisso costa al massimo l’intero cammino verso u. Quindi d(y) sarebbe minore di d(u), contro la scelta del minimo. La distanza di u è definitiva; per induzione lo sono tutte quelle estratte.

Con liste di adiacenza e heap binario con decrease-key: O((|V|+|E|) log |V|), memoria ausiliaria O(|V|). Una versione che cerca il minimo scandendo i vertici costa O(|V|²+|E|). Con pesi negativi la dimostrazione non vale: nel grafo orientato A→B=2, A→C=5, C→B=−10, B verrebbe fissato a 2 ma il vero minimo è −5.

Princeton · Algorithms, 4th Edition — Dijkstra

3. Kruskal: collegare tutto al minor costo

Non cerchiamo un percorso da A a F: vogliamo una rete che colleghi tutti i vertici minimizzando la somma dei pesi degli archi scelti. È un albero ricoprente minimo (MST). Un albero su 6 vertici ha esattamente 5 archi.

Ordina gli archi per peso crescente. Parti con ogni vertice in una componente separata. Accetta un arco solo se unisce due componenti diverse; altrimenti formerebbe un ciclo. Una struttura Union-Find controlla e fonde le componenti. Fermati dopo |V|−1 archi accettati.

PassoVertice / arcoCoda / insieme definitivo / costoKruskal
00
1BC (1)1Scelto
2AC (2)3Scelto
3DE (2)5Scelto
4EF (3)8Scelto
5AB (4)8Scartato: ciclo
6BD (5)13Scelto

Ordine iniziale: BC(1), AC(2), DE(2), EF(3), AB(4), BD(5). AB viene scartato perché A e B sono già collegati attraverso C: chiuderebbe il triangolo A–C–B–A. BD collega infine {A,B,C} a {D,E,F}. Costo totale 1+2+2+3+5=13. Gli archi DF(6), CD(8), CE(10) non servono più.

Kruskal: Stato iniziale (Passo 0). —
Stato iniziale (Passo 0). —
Kruskal: Stato intermedio (Passo 3). BC, AC, DE
Stato intermedio (Passo 3). BC, AC, DE
Kruskal: Stato finale (Passo 6). BC, AC, DE, EF, BD
Stato finale (Passo 6). BC, AC, DE, EF, BD

Perché funziona: argomento di scambio

  1. Invariante: esiste un albero ricoprente minimo T che contiene tutti gli archi finora scelti F. All’inizio F è vuoto, quindi è vero. Sia e il prossimo arco accettato, tra due componenti diverse di F.
  2. Se e è già in T, non cambia nulla. Altrimenti aggiungendolo a T nasce un unico ciclo. Su quel ciclo c’è un arco f che esce dalla componente di uno degli estremi di e; f non appartiene a F. Inoltre w(f)≥w(e): un arco più leggero che attraversasse quella componente sarebbe stato considerato prima e avrebbe unito le componenti.
  3. Sostituiamo f con e: la rete resta un albero e il costo non aumenta. Per minimalità di T, anche il nuovo albero è minimo e contiene F insieme a e. L’invariante continua. Dopo |V|−1 scelte il nostro albero coincide con un MST.

Ordinamento O(|E| log |E|); Union-Find con compressione dei cammini e unione per rango richiede complessivamente O(|E| α(|V|)), quasi lineare. Memoria O(|V|+|E|) includendo gli archi ordinati. Kruskal ammette anche pesi negativi. Se il grafo è sconnesso produce una foresta ricoprente minima, non un unico albero.

Princeton · Algorithms, 4th Edition — Kruskal

Attenzione: in questo esempio Dijkstra e Kruskal danno entrambi 13 e lo stesso insieme di archi, ma è una coincidenza. Nel triangolo AB=2, AC=2, BC=1, l’albero dei cammini minimi da A usa AB e AC (totale 4); un MST usa BC e uno dei due archi di peso 2 (totale 3). Nel secondo, uno dei cammini da A costa 3 invece di 2. Obiettivo diverso, risposta diversa.