14 KiB
Pytanie 2: Algorytmy najkrótszej ścieżki
Pytanie
"Omówić i porównać algorytmy najkrótszej ścieżki wskazując ich kluczowe właściwości i logikę budowy: Dijkstry, Belmana-Forda, A."*
Przedmiot: AISDI (Algorytmy i Struktury Danych)
📚 Odpowiedź główna
Wprowadzenie - problem najkrótszej ścieżki
Problem: Dany jest graf G = (V, E) z funkcją wag w: E → ℝ. Znajdź ścieżkę z wierzchołka źródłowego s do wierzchołka docelowego t o minimalnej sumie wag krawędzi.
Warianty problemu:
- Single-Source Shortest Path (SSSP) - z jednego źródła do wszystkich wierzchołków
- Single-Pair Shortest Path - z s do konkretnego t
- All-Pairs Shortest Path (APSP) - między wszystkimi parami (Floyd-Warshall)
1. Algorytm Dijkstry
Charakterystyka
- Autor: Edsger Dijkstra (1956, opublikowany 1959)
- Typ: Zachłanny (greedy)
- Problem: SSSP - najkrótsze ścieżki z jednego źródła do wszystkich wierzchołków
- Ograniczenie: ⚠️ Tylko nieujemne wagi krawędzi (w(e) ≥ 0)
Idea algorytmu (logika budowy)
- Relaksacja: Stopniowe ulepszanie oszacowań odległości
- Zachłanność: W każdym kroku wybieramy wierzchołek o najmniejszej znanej odległości
- Optymalna podstruktura: Najkrótsza ścieżka składa się z najkrótszych podścieżek
Pseudokod
DIJKSTRA(G, w, s):
// Inicjalizacja
for each v ∈ V:
d[v] ← ∞
π[v] ← NIL
d[s] ← 0
Q ← priority_queue(V) // min-heap według d[v]
S ← ∅ // zbiór przetworzonych
while Q ≠ ∅:
u ← EXTRACT-MIN(Q)
S ← S ∪ {u}
for each v ∈ Adj[u]: // relaksacja
if d[u] + w(u,v) < d[v]:
d[v] ← d[u] + w(u,v)
π[v] ← u
DECREASE-KEY(Q, v, d[v])
return d, π
Złożoność czasowa
| Implementacja kolejki | EXTRACT-MIN | DECREASE-KEY | Całkowita |
|---|---|---|---|
| Lista/tablica | O(V) | O(1) | O(V²) |
| Kopiec binarny | O(log V) | O(log V) | O((V + E) log V) |
| Kopiec Fibonacciego | O(log V)* | O(1)* | O(V log V + E) |
*amortyzowane
Dlaczego nie działa dla ujemnych wag?
A ---(-5)--- B
| |
(1) (1)
| |
S -----------C
(2)
Dijkstra przetwarza wierzchołki w kolejności rosnącej odległości i oznacza je jako "zakończone". Jeśli waga może być ujemna, późniejszy wierzchołek może "poprawić" już zakończony.
Przykład: S→C = 2 (Dijkstra ustala jako finalne), ale S→A→B→C = 1 + (-5) + 1 = -3 < 2
2. Algorytm Bellmana-Forda
Charakterystyka
- Autorzy: Richard Bellman, Lester Ford Jr. (1958)
- Typ: Programowanie dynamiczne
- Problem: SSSP z wykrywaniem cykli ujemnych
- Zaleta: ✅ Działa dla ujemnych wag
- Ograniczenie: ⚠️ Graf nie może mieć cyklu o ujemnej sumie wag (ale algorytm go wykrywa!)
Idea algorytmu (logika budowy)
- Indukcja po liczbie krawędzi: d^(k)[v] = najkrótsza ścieżka do v używająca ≤ k krawędzi
- |V|-1 iteracji: Najkrótsza ścieżka bez cykli ma co najwyżej |V|-1 krawędzi
- Relaksacja wszystkich krawędzi: W każdej iteracji relaksujemy każdą krawędź
Pseudokod
BELLMAN-FORD(G, w, s):
// Inicjalizacja
for each v ∈ V:
d[v] ← ∞
π[v] ← NIL
d[s] ← 0
// Główna pętla: |V|-1 iteracji
for i ← 1 to |V| - 1:
for each edge (u, v) ∈ E:
if d[u] + w(u,v) < d[v]: // relaksacja
d[v] ← d[u] + w(u,v)
π[v] ← u
// Wykrywanie cyklu ujemnego
for each edge (u, v) ∈ E:
if d[u] + w(u,v) < d[v]:
return "CYKL UJEMNY"
return d, π
Złożoność czasowa
O(V · E) - zawsze, niezależnie od implementacji
Dla grafów gęstych (E ≈ V²): O(V³) - wolniejszy niż Dijkstra O(V²)
Wykrywanie cyklu ujemnego
Po |V|-1 iteracjach, wszystkie najkrótsze ścieżki (bez cykli) są znalezione. Jeśli w iteracji |V| nadal można zrelaksować krawędź → istnieje cykl ujemny.
Optymalizacja: wczesne zakończenie
for i ← 1 to |V| - 1:
changed ← false
for each edge (u, v) ∈ E:
if d[u] + w(u,v) < d[v]:
d[v] ← d[u] + w(u,v)
π[v] ← u
changed ← true
if not changed:
break // Brak zmian = gotowe wcześniej
3. Algorytm A* (A-star)
Charakterystyka
- Autorzy: Peter Hart, Nils Nilsson, Bertram Raphael (1968)
- Typ: Heurystyczny (informed search)
- Problem: Single-Pair - najkrótsza ścieżka z s do konkretnego t
- Zaleta: ✅ Znacznie szybszy niż Dijkstra dla problemu s→t
- Wymóg: Heurystyka dopuszczalna (admissible)
Idea algorytmu (logika budowy)
- Rozszerzenie Dijkstry: Dodajemy funkcję heurystyczną
- f(n) = g(n) + h(n):
- g(n) = koszt dotarcia do n (znany)
- h(n) = heurystyczne oszacowanie kosztu n → cel (szacowany)
- f(n) = całkowite oszacowanie kosztu ścieżki przez n
- Wybieramy wierzchołek o minimalnym f(n) zamiast minimalnym g(n)
Pseudokod
A-STAR(G, w, s, t, h):
// Inicjalizacja
for each v ∈ V:
g[v] ← ∞
f[v] ← ∞
π[v] ← NIL
g[s] ← 0
f[s] ← h(s)
OPEN ← priority_queue({s}) // min-heap według f[v]
CLOSED ← ∅
while OPEN ≠ ∅:
u ← EXTRACT-MIN(OPEN)
if u = t:
return RECONSTRUCT-PATH(π, t)
CLOSED ← CLOSED ∪ {u}
for each v ∈ Adj[u]:
if v ∈ CLOSED:
continue
tentative_g ← g[u] + w(u, v)
if v ∉ OPEN:
OPEN ← OPEN ∪ {v}
else if tentative_g ≥ g[v]:
continue
π[v] ← u
g[v] ← tentative_g
f[v] ← g[v] + h(v)
return "BRAK ŚCIEŻKI"
Heurystyka - kluczowy element
Wymagane właściwości:
-
Dopuszczalność (Admissibility): h(n) ≤ h*(n) dla każdego n
gdzie h*(n) = rzeczywisty koszt n → cel
→ Gwarantuje optymalność rozwiązania
-
Spójność/Monotoniczność (Consistency): h(n) ≤ w(n, m) + h(m) dla każdej krawędzi (n, m)
→ Gwarantuje, że węzeł nie musi być ponownie otwarty → Spójność implikuje dopuszczalność
Popularne heurystyki (dla siatek 2D):
| Heurystyka | Wzór | Ruch |
|---|---|---|
| Manhattan | |x₁-x₂| + |y₁-y₂| | 4 kierunki |
| Euklidesowa | √((x₁-x₂)² + (y₁-y₂)²) | dowolny kąt |
| Czebyszewa | max(|x₁-x₂|, |y₁-y₂|) | 8 kierunków |
| Oktylowa | max(Δx, Δy) + (√2-1)·min(Δx, Δy) | 8 kier. + przekątne |
Złożoność czasowa
- Najgorszy przypadek: O((V + E) log V) - jak Dijkstra
- Praktycznie: Znacznie lepiej dzięki heurystyce - przeszukuje mniej wierzchołków
- Zależy od jakości h: Im lepsza heurystyka, tym mniej eksploracji
Przypadki specjalne:
- h(n) = 0: A* = Dijkstra
- h(n) = h(n):* A* idzie prosto do celu (idealna heurystyka)
- h(n) > h(n):* Może nie znaleźć optymalnej ścieżki!
📊 Tabela porównawcza
| Cecha | Dijkstra | Bellman-Ford | A* |
|---|---|---|---|
| Typ | Zachłanny | Prog. dynamiczne | Heurystyczny |
| Problem | SSSP | SSSP | Single-pair (s→t) |
| Ujemne wagi | ❌ NIE | ✅ TAK | ❌ NIE |
| Cykle ujemne | Błędny wynik | Wykrywa | Błędny wynik |
| Złożoność | O(V log V + E) | O(V·E) | O((V+E) log V)* |
| Pamięć | O(V) | O(V) | O(V) |
| Optymalizacja | Kolejka priorytetowa | Wczesne zakończenie | Heurystyka |
| Zastosowanie | Grafy nieujemne | Grafy z ujemnymi | Pathfinding w grach |
*praktycznie często znacznie mniej
🎮 Zastosowania praktyczne
Dijkstra
- Nawigacja GPS (drogi nie mają ujemnych odległości)
- Routing w sieciach (OSPF protocol)
- Mapy Google/Apple (dla małych obszarów)
Bellman-Ford
- Routing w sieciach (RIP protocol - prostszy)
- Arbitraż walutowy (szukanie cykli ujemnych = zysk!)
- Systemy z "karami" (ujemne wagi = bonusy)
A*
- Gry komputerowe - pathfinding NPC, RTS
- Robotyka - planowanie ruchu
- Puzzle - 8-puzzle, 15-puzzle
- Nawigacja - gdy znamy pozycję celu
🔄 Relaksacja - wspólny element
Wszystkie trzy algorytmy używają relaksacji krawędzi:
RELAX(u, v, w):
if d[u] + w(u,v) < d[v]:
d[v] ← d[u] + w(u,v)
π[v] ← u
Różnica w kolejności relaksacji:
- Dijkstra: Relaksuje krawędzie wychodzące z wierzchołka o minimalnym d[v]
- Bellman-Ford: Relaksuje wszystkie krawędzie w każdej z |V|-1 iteracji
- A:* Relaksuje krawędzie wychodzące z wierzchołka o minimalnym f[v] = g[v] + h[v]
🧠 Mnemoniki
"DBF - Dijkstra Bellman Ford"
- Dijkstra = Dodatnie wagi tylko
- Bellman-Ford = Bez ograniczeń (ujemne OK)
- Find cycles = wykrywa cykle ujemne
"A* = A sterowana"
- Algorytm Asterowany heurystyką
- Ale heurystyka musi być Admissible
"GREP" dla Dijkstry:
- Greedy (zachłanny)
- Relaksacja krawędzi
- Extract-min z kolejki
- Priority queue kluczowa
"VE" dla Bellman-Ford:
- V-1 iteracji
- E krawędzi relaksowanych w każdej
"HIG" dla A*:
- Heurystyka kieruje
- Informed search
- Goal-oriented (zorientowany na cel)
Złożoność - "Dijkstra lubi VlogV, Bellman lubi VE":
- Dijkstra: O(V log V + E) z kopcem Fibonacciego
- Bellman-Ford: O(V · E)
❓ Możliwe pytania dodatkowe (follow-up)
Q1: "Pokaż działanie algorytmu Dijkstry na przykładzie"
Odpowiedź:
Graf:
A ---(1)--- B
| |
(4) (2)
| |
S ---(2)--- C ---(1)--- D
| Krok | Przetwarzany | d[S] | d[A] | d[B] | d[C] | d[D] |
|---|---|---|---|---|---|---|
| Init | - | 0 | ∞ | ∞ | ∞ | ∞ |
| 1 | S | 0 | 4 | ∞ | 2 | ∞ |
| 2 | C | 0 | 4 | 4 | 2 | 3 |
| 3 | D | 0 | 4 | 4 | 2 | 3 |
| 4 | A | 0 | 4 | 4 | 2 | 3 |
| 5 | B | 0 | 4 | 4 | 2 | 3 |
Najkrótsza ścieżka S→D: S → C → D (koszt 3)
Q2: "Jak wykryć ujemny cykl algorytmem Bellman-Forda?"
Odpowiedź:
Po |V|-1 iteracjach, wykonujemy jeszcze jedną iterację po wszystkich krawędziach:
# Po głównej pętli
for (u, v) in edges:
if d[u] + w[u][v] < d[v]:
return "UJEMNY CYKL ISTNIEJE"
Dlaczego to działa?
- Najkrótsza ścieżka prosta (bez cykli) ma co najwyżej |V|-1 krawędzi
- Po |V|-1 iteracjach wszystkie takie ścieżki są znalezione
- Jeśli |V|-ta iteracja poprawia cokolwiek → ścieżka przez cykl jest krótsza → cykl ujemny
Q3: "Dlaczego heurystyka musi być dopuszczalna w A*?"
Odpowiedź:
Dopuszczalność: h(n) ≤ h*(n) - nigdy nie przeszacowujemy kosztu
Dowód optymalności:
- Załóżmy, że A* zwraca ścieżkę P o koszcie g(P)
- Niech P* będzie optymalną ścieżką o koszcie g(P*)
- Jeśli g(P) > g(P*), to w momencie zwrócenia P:
- Jakiś węzeł n na P* był w OPEN
- f(n) = g(n) + h(n) ≤ g(n) + h*(n) = g(P*) < g(P) = f(goal)
- Ale A* wybrał goal zamiast n → sprzeczność!
Przykład niedopuszczalnej heurystyki:
S ---(1)--- A ---(1)--- T
\ /
\---(10)--------/
Jeśli h(A) = 5 (a h*(A) = 1), A* może wybrać ścieżkę S→T (koszt 10) zamiast S→A→T (koszt 2).
Q4: "Porównaj A* z Dijkstrą - kiedy użyć którego?"
Odpowiedź:
| Sytuacja | Lepszy algorytm |
|---|---|
| Szukam ścieżki do WSZYSTKICH wierzchołków | Dijkstra |
| Szukam ścieżki do JEDNEGO celu | A* |
| Nie mam dobrej heurystyki | Dijkstra |
| Graf ma strukturę geometryczną (mapa 2D) | A* |
| Graf abstrakcyjny (np. stanów) | Zależy od heurystyki |
| Potrzebuję gwarancji optymalności | Oba (A* z dopuszczalną h) |
Praktycznie:
- A* z h(n)=0 to Dijkstra
- Dobra heurystyka może zredukować eksplorację o rzędy wielkości
- W grach A* jest standardem (Unity NavMesh używa wariantu A*)
Q5: "Co to jest algorytm Floyd-Warshalla i jak się ma do omawianych?"
Odpowiedź:
Floyd-Warshall rozwiązuje All-Pairs Shortest Path (APSP):
- Znajduje najkrótsze ścieżki między WSZYSTKIMI parami wierzchołków
- Złożoność: O(V³)
- Działa z ujemnymi wagami (wykrywa cykle ujemne)
- Programowanie dynamiczne
for k ← 1 to V:
for i ← 1 to V:
for j ← 1 to V:
d[i][j] ← min(d[i][j], d[i][k] + d[k][j])
Porównanie:
- SSSP z dodatnimi wagami: Dijkstra O(V log V + E)
- SSSP z ujemnymi wagami: Bellman-Ford O(VE)
- APSP: Floyd-Warshall O(V³) lub V × Dijkstra O(V² log V + VE)
Q6: "Jakie są warianty algorytmu Dijkstry?"
Odpowiedź:
-
Bidirectional Dijkstra:
- Równoczesne przeszukiwanie z s i z t
- Spotykają się w środku
- ~2× szybszy w praktyce
-
Dial's Algorithm:
- Dla małych, całkowitych wag [0, C]
- O(V + E + C) zamiast O((V+E) log V)
- Używa "bucket queue"
-
Johnson's Algorithm:
- APSP dla grafów rzadkich
- Używa Bellman-Ford + V × Dijkstra
- O(VE + V² log V) - lepszy niż Floyd-Warshall dla rzadkich grafów
🎯 Kluczowe punkty do zapamiętania
- Dijkstra = zachłanny, tylko nieujemne wagi, najszybszy dla SSSP
- Bellman-Ford = prog. dynamiczne, ujemne wagi OK, wykrywa cykle ujemne
- A* = Dijkstra + heurystyka, szybki dla single-pair, wymaga h admissible
- Relaksacja = wspólna operacja, różnica w kolejności
- Złożoność: Dijkstra O(V log V + E), BF O(VE), A* zależy od h
📖 Źródła do pogłębienia
- Cormen, Leiserson, Rivest, Stein - "Introduction to Algorithms" (CLRS)
- Sedgewick, Wayne - "Algorithms"
- Hart, Nilsson, Raphael - "A Formal Basis for the Heuristic Determination of Minimum Cost Paths" (1968)
- Red Blob Games - "Introduction to A*" (online, interaktywny)