Salta al contenuto
Note per Studenti Cammini minimi e algoritmo di Dijkstra

Cammini minimi e algoritmo di Dijkstra

In questa pagina 5

Sia G=(V,E,w)G = (V, E, w) un grafo non diretto e pesato, con w:E→Rw : E \to \mathbb{R} (esempio: rete di aeroporti con distanze in km). La lunghezza di un cammino u1,…,uku_1, \dots, u_k è ∑i=1k−1w(ui,ui+1)\sum_{i=1}^{k-1} w(u_i, u_{i+1}); se w(e)=1w(e) = 1 per ogni arco è il numero di archi. La distanza d(u,v)d(u, v) è la minima lunghezza di un cammino da uu a vv e il cammino che la realizza è un cammino minimo.

Proposizione (sottostruttura ottima). Se u1,…,uku_1, \dots, u_k è un cammino minimo da u1u_1 a uku_k, allora ui,…,uju_i, \dots, u_j è un cammino minimo da uiu_i a uju_j per ogni 1≤i<j≤k1 \le i < j \le k. Dimostrazione: se esistesse un cammino più corto da uiu_i a uju_j, sostituendolo nel cammino originale si otterrebbe un cammino da u1u_1 a uku_k più corto, contro l'ipotesi.

SSSP (single-source shortest paths): dato s∈Vs \in V, determinare le distanze da ss a tutti gli altri vertici e i relativi cammini minimi (rappresentati dai campi parent). Si assume che GG non abbia cicli di peso negativo (altrimenti la lunghezza potrebbe diventare arbitrariamente piccola). Se tutti i pesi sono 11 basta la BFS in Θ(n+m)\Theta(n + m) (vedi Visite di grafi - BFS e DFSVisite in ampiezza (BFS) e in profondità (DFS) come design pattern; etichette discovery, cross e back edge; BFS tree e distanze; complessità Theta(n+m) con liste di adiacenza; applicazioni (connettività, componenti, spanning tree, cammini minimi non pesati, cicli, vertici a distanza al più d); esempio svolto su un grafo di 7 vertici.Visite di grafi - BFS e DFS →).

Algoritmo di Dijkstra

Sviluppato nel 1956, richiede pesi non negativi. Generalizza la BFS: a partire da ss fa crescere una cloud di vertici di cui si conoscono già distanza da ss e cammino minimo.

Algoritmo ShortestPaths(G, s)
Input: grafo non diretto G=(V,E,w), pesi >= 0, sorgente s
Output: distanze e cammini minimi da s, nei campi v.D e v.parent
s.D <- 0; s.parent <- null
forall v in V - {s} do v.D <- +infinito; v.parent <- null
Q <- coda con priorità con tutti i vertici (chiave v.D)
while Q non è vuota do
    u <- Q.removeMin()
    forall (u, v) in E con v in Q do
        if u.D + w(u,v) < v.D then
            v.D <- u.D + w(u,v); v.parent <- u
            aggiorna Q per la nuova chiave di v

Esempio svolto

Vertici A..FA..F e archi (peso): AB (7)AB\,(7), AC (9)AC\,(9), AF (14)AF\,(14), BC (10)BC\,(10), BD (15)BD\,(15), CD (11)CD\,(11), CF (2)CF\,(2), DE (6)DE\,(6), EF (9)EF\,(9). Sorgente AA.

Iterazione Entra in cloud Distanze correnti (B,C,D,E,F)(B, C, D, E, F) dopo il rilassamento
inizio AA (00) (7,9,∞,∞,14)(7, 9, \infty, \infty, 14)
1 BB (77) (7,9,22,∞,14)(7, 9, 22, \infty, 14) (BCBC: 17>917 > 9; BDBD: 2222)
2 CC (99) (7,9,20,∞,11)(7, 9, 20, \infty, 11) (CDCD: 20<2220 < 22; CFCF: 11<1411 < 14)
3 FF (1111) (7,9,20,20,11)(7, 9, 20, 20, 11) (FEFE: 11+9=2011 + 9 = 20)
4 DD (2020) invariato (DEDE: 26>2026 > 20)
5 EE (2020) —

Distanze da AA: B=7B = 7, C=9C = 9, F=11F = 11, D=20D = 20, E=20E = 20. Cammini (da parent): B←AB \leftarrow A; C←AC \leftarrow A; F←CF \leftarrow C; D←CD \leftarrow C; E←FE \leftarrow F. Quindi il cammino minimo A→EA \to E è A,C,F,EA, C, F, E di lunghezza 9+2+9=209 + 2 + 9 = 20. (Verificato eseguendo un'implementazione con heap; a parità DD e EE entrano in cloud in uno dei due ordini.)

Correttezza

Segue da due lemmi (la dimostrazione del Lemma 2 non è richiesta nel dettaglio). Alla fine di ogni iterazione del while, per ogni v∈Vv \in V:

  • Lemma 1. Se v.D =+∞= +\infty allora v.parent == null; se v.D <+∞< +\infty, il cammino ottenuto risalendo da vv con parent è un cammino da ss a vv di lunghezza v.D.
  • Lemma 2. Quando uu viene estratto da QQ, u.D =d(s,u)= d(s, u).

Idea per il Lemma 2. Se per assurdo u.D >d(s,u)> d(s,u), si considera un cammino minimo da ss a uu e il primo vertice zz fuori dalla cloud su di esso. Il predecessore di zz è in cloud, quindi l'arco verso zz è stato rilassato e z.D ≤d(s,z)≤d(s,u)<\le d(s,z) \le d(s,u) < u.D (qui servono i pesi ≥0\ge 0: d(s,z)≤d(s,u)d(s,z) \le d(s,u)). Allora zz sarebbe stato estratto prima di uu, contraddizione.

Perché servono pesi non negativi. In un grafo diretto con archi s→as \to a di peso 22, s→bs \to b di peso 33 e b→ab \to a di peso −2-2 (nessun ciclo negativo) Dijkstra estrae aa con D=2D = 2, ma il cammino s,b,as, b, a vale 11. (In un grafo non diretto un solo arco negativo è già un ciclo negativo: si percorre avanti e indietro.)

Complessità

Dipende dall'implementazione della coda QQ. Ogni vertice è estratto una volta (nn removeMin), si fanno al più mm aggiornamenti di chiave (un rilassamento per arco), e ogni vertice mantiene un puntatore alla propria entry in QQ.

Operazione su QQ Heap Lista doppiamente concatenata non ordinata
costruzione iniziale Θ(n)\Theta(n) Θ(n)\Theta(n)
removeMin Θ(log⁡n)\Theta(\log n) Θ(n)\Theta(n)
aggiornamento di una chiave Θ(log⁡n)\Theta(\log n) Θ(1)\Theta(1)

Nello heap l'aggiornamento di una chiave diminuita richiede un up-heap bubbling in O(log⁡n)O(\log n) (vedi HeapAlbero binario completo e sua altezza floor(log2 n); heap = albero completo con heap-order property; proprietà (radice minima, cammini non decrescenti); rappresentazione su array con level numbering; insert con up-heap bubbling, removeMin con down-heap bubbling, rimozione di una entry qualsiasi; invarianti e costi Theta(log n); esempio svolto.Heap →). Totale:

  • heap: n⋅Θ(log⁡n)+m⋅Θ(log⁡n)+Θ(n+m)=O((n+m)log⁡n)n \cdot \Theta(\log n) + m \cdot \Theta(\log n) + \Theta(n + m) = O((n + m) \log n);
  • lista non ordinata: n⋅Θ(n)+m⋅Θ(1)=O(n2+m)=O(n2)n \cdot \Theta(n) + m \cdot \Theta(1) = O(n^2 + m) = O(n^2) (perché m≤n2m \le n^2).

Teorema. La complessità di ShortestPaths(G, s) è O(min⁡{n2,(n+m)log⁡n})O(\min\{n^2, (n + m) \log n\}): con la scelta migliore dell'implementazione. Per grafi sparsi (m=O(n)m = O(n)) conviene lo heap (O(nlog⁡n)O(n \log n)); per grafi densi (m=Θ(n2)m = \Theta(n^2)) la lista (Θ(n2)\Theta(n^2) contro Θ(n2log⁡n)\Theta(n^2 \log n)).

Errori comuni

  • Usare Dijkstra con pesi negativi.
  • Dimenticare l'aggiornamento di parent quando si migliora D (si ottengono le distanze ma non i cammini).
  • Rilassare anche archi verso vertici già nella cloud (inutile, non cambia la distanza finale).
  • Dire che costa sempre O(mlog⁡n)O(m \log n): dipende dall'implementazione di QQ; O(nlog⁡n)O(n \log n) non tiene conto degli aggiornamenti di chiave su un grafo denso.

Versione ripasso

Teoria collegata