Algorytm Dijkstry
Po tej lekcji będziesz umieć:
- zaimplementować algorytm Dijkstry z kolejką priorytetową,
- uzasadnić, dlaczego wagi muszą być nieujemne,
- rozpoznać zadania, w których wierzchołkiem jest stan, a nie miasto.
1. Problem
Dany jest graf skierowany lub nieskierowany o \(n\) wierzchołkach i \(m\) krawędziach z wagami nieujemnymi oraz wierzchołek startowy \(s\). Chcemy wyznaczyć \(d(v)\), czyli długość najkrótszej ścieżki z \(s\) do każdego \(v\). Jest to problem SSSP (ang. single source shortest paths).
Formalnie, dla ścieżki \(P = (v_0, v_1, \dots, v_k)\) jej koszt to \[ w(P) \;=\; \sum_{i=0}^{k-1} w(v_i, v_{i+1}), \] a szukamy \[ d(v) \;=\; \min_{P:\, s \rightsquigarrow v} w(P). \] Jeśli \(v\) jest nieosiągalny, przyjmujemy \(d(v) = \infty\).
2. Intuicja
Utrzymujemy zbiór \(S\) wierzchołków domkniętych, dla których znamy już ostateczną odległość. Na starcie \(S = \{s\}\) oraz \(dist[s] = 0\).
W każdym kroku bierzemy wierzchołek \(u \notin S\) o najmniejszym \(dist[u]\), ogłaszamy jego odległość ostateczną i relaksujemy wszystkie krawędzie wychodzące z \(u\): \[ dist[v] \;\leftarrow\; \min\bigl(dist[v],\; dist[u] + w(u,v)\bigr). \]
To algorytm zachłanny: raz domknięty wierzchołek nigdy nie zmienia swojej odległości.
3. Pseudokod
dijkstra(G, s):
dist[v] = INF dla kazdego v
dist[s] = 0
Q = kolejka priorytetowa z para (0, s)
dopoki Q niepusta:
(d, u) = Q.pop_min()
jesli d > dist[u]: pomin # wpis przeterminowany
dla (v, w) w sasiedzi(u):
jesli d + w < dist[v]:
dist[v] = d + w
Q.push((dist[v], v))
zwroc dist
4. Implementacja w C++
#include
using namespace std;
using ll = long long;
const ll INF = (ll)1e18;
// g[u] = lista par (sasiad, waga)
vector dijkstra(int n, const vector>>& g, int src) {
vector dist(n, INF);
priority_queue, vector>, greater<>> pq;
dist[src] = 0;
pq.push({0, src});
while (!pq.empty()) {
auto [d, u] = pq.top();
pq.pop();
if (d > dist[u]) continue; // leniwe usuwanie
for (auto [v, w] : g[u]) {
if (d + w < dist[v]) {
dist[v] = d + w;
pq.push({dist[v], v});
}
}
}
return dist;
}
greater<> jest potrzebne, bo priority_queue jest domyślnie kopcem maksimum.
Para \((dist, v)\) porównuje się leksykograficznie, więc porządek jest po odległości.
4.1. Odtwarzanie ścieżki
if (d + w < dist[v]) {
dist[v] = d + w;
parent[v] = u;
pq.push({dist[v], v});
}
Następnie idziemy od \(t\) po tablicy parent aż do \(s\) i odwracamy wynik.
Jeśli \(dist[t] = \infty\), ścieżki nie ma.
5. Złożoność
| Wariant | Struktura | Złożoność |
|---|---|---|
| Klasyczny, skan tablicy | tablica | \(O(n^2 + m)\) |
| Z kopcem binarnym | priority_queue / set | \(O(m \log n)\) |
| Z kopcem Fibonacciego | kopiec Fibonacciego | \(O(m + n \log n)\) |
W praktyce na zawodach używa się wersji z priority_queue.
Wersja \(O(n^2)\) bywa lepsza dla grafów bardzo gęstych, gdzie \(m = \Theta(n^2)\),
na przykład \(n = 2000\) i graf pełny.
Wersja z leniwym usuwaniem wrzuca do kolejki \(O(m)\) wpisów, ale nie wymaga operacji
decrease-key, dlatego jest prostsza i w praktyce szybsza od wariantu z set.
6. Dlaczego wagi muszą być nieujemne
Kontrprzykład:
- \(s \to a\) o wadze \(1\),
- \(s \to b\) o wadze \(2\),
- \(b \to a\) o wadze \(-2\).
Dijkstra domyka \(a\) z wartością \(1\), bo w tym momencie jest to minimum w kolejce. Prawdziwa odległość to \(d(a) = 2 + (-2) = 0\). Zachłanny niezmiennik opiera się na tym, że dalsza wędrówka po krawędziach nie może niczego skrócić. Ujemna krawędź to łamie.
Dla wag ujemnych: Bellman-Ford w \(O(nm)\), a dla wszystkich par Floyd-Warshall w \(O(n^3)\) albo algorytm Johnsona.
7. Szkic dowodu poprawności
Niezmiennik. W momencie zdjęcia wierzchołka \(u\) z kolejki zachodzi \(dist[u] = d(u)\).
Dowód nie wprost. Niech \(u\) będzie pierwszym zdjętym wierzchołkiem, dla którego \(dist[u] > d(u)\). Rozważmy najkrótszą ścieżkę \(s \rightsquigarrow u\) i pierwszy wierzchołek \(y\) na niej, który nie został jeszcze domknięty. Jego poprzednik \(x\) jest domknięty, więc krawędź \((x,y)\) była już zrelaksowana, czyli \(dist[y] = d(y)\). Ponieważ wagi są nieujemne, a \(y\) leży na najkrótszej ścieżce do \(u\): \[ dist[y] \;=\; d(y) \;\le\; d(u) \;<\; dist[u]. \] Zatem kolejka priorytetowa zdjęłaby \(y\) przed \(u\). Sprzeczność.
8. Typowe błędy
-
Przepełnienie zakresu. Przy \(m = 2 \cdot 10^5\) krawędziach o wagach do \(10^9\)
sumy sięgają \(10^{14}\), więc potrzebny jest
long long. -
Zbyt duże INF. Dla
INF = LLONG_MAXwyrażeniedist[u] + wsię przepełnia. Bezpieczna wartość to \(10^{18}\). W powyższym kodzie problem nie występuje, bo do kolejki trafiają tylko wierzchołki osiągalne. -
Brak leniwego usuwania. Pominięcie linii
if (d > dist[u]) continue;nie psuje wyniku, ale powoduje wielokrotne przetwarzanie wierzchołków i może dać TLE. - Dijkstra przy ujemnych wagach. Częsty błąd, gdy wagi są liczone jako różnice.
- Graf nieskierowany. Trzeba dodać obie krawędzie.
9. Warianty i rozszerzenia
9.1. BFS 0-1
Gdy wagi to tylko \(0\) i \(1\), zamiast kolejki priorytetowej wystarczy deque:
krawędź o wadze \(0\) wrzucamy na przód, o wadze \(1\) na koniec. Złożoność \(O(n + m)\).
9.2. Dijkstra na grafie stanów
Wierzchołkiem nie musi być pojedyncze miasto. Może to być para \((v, k)\), gdzie \(k\) to liczba zużytych zniżek, albo \((v, p)\) z parzystością liczby przebytych krawędzi. Budujemy graf warstwowy o \(O(nK)\) wierzchołkach i puszczamy zwykłą Dijkstrę. To najczęstsze zastosowanie olimpijskie.
9.3. Wiele źródeł
Wrzucamy na start wszystkie źródła z odległością \(0\). Dostajemy \(d(v) = \min_{s \in S} d(s, v)\), czyli odległość do najbliższego źródła.
9.4. Liczenie najkrótszych ścieżek
Dodajemy tablicę cnt. Przy \(dist[u] + w < dist[v]\) ustawiamy \(cnt[v] = cnt[u]\),
a przy \(dist[u] + w = dist[v]\) robimy \(cnt[v] \mathrel{+}= cnt[u]\), modulo \(10^9 + 7\).
9.5. Najszersza ścieżka
Zamiast minimalizować sumę, maksymalizujemy najwęższe gardło: \[ \max_{P:\, s \rightsquigarrow v} \; \min_{e \in P} w(e). \] Struktura algorytmu jest ta sama, zmienia się tylko operacja relaksacji.
10. Zadania
-
Maraton
podstawowy
Czysta Dijkstra na grafie ważonym. Dobre pierwsze zadanie na sam algorytm.
-
Bajtockie Koleje Państwowe
rozgrzewka grafowa
Wejście w modelowanie grafowe, zanim dojdą wagi.
-
Codeforces 20C, Dijkstra?
podstawowy
Ta sama Dijkstra, ale trzeba wypisać samą ścieżkę, więc wymusza tablicę
parent. -
Ciężarówki 2
średni
Wariant gardłowy. Dijkstra nie jest tu rozwiązaniem wzorcowym, ale daje sensowny wynik częściowy.
-
Juror
trudny, nieklasyczny
Trzeba skonstruować anty-test, na którym wersja z leniwym usuwaniem działa wolno. Świetne domknięcie tematu: sprawdza, czy naprawdę rozumiesz, skąd bierze się \(O(m \log n)\).
Nadal pracujemy nad treścią w tym poziomie. Będzie dostępna wkrótce!