Introducing
Your new presentation assistant.
Refine, enhance, and tailor your content, source relevant images, and edit visuals quicker than ever before.
Trending searches
Michał Ochenduszkiewicz
W dzisiejszych czasach bardzo często korzystamy z nawigacji samochodowej, np. wbudowaną w mapy Google. Jedną z jej funkcji jest obliczanie najkrótszej trasy pomiędzy dwoma wybranymi przez nas punktami. Program uwzględnia przy tym długość dróg, dopuszczalną prędkość, a ostatnio także ich stan (to, czy określone odcinki są zamknięte), korki oraz wypadki. W jaki sposób obliczana jest najbardziej optymalna droga?
Potraktujmy sieć dróg jako ważony graf skierowany. Niech każde skrzyżowanie będzie jakimś jego wierzchołkiem, a droga - krawędzią. Wagą tych krawędzi będzie wartość funkcji wyznaczającej oczekiwany czas przejazdu tą drogą, czyli dla przykładu iloraz długości drogi oraz dopuszczalnej prędkości na niej.
Na początek rozważmy problem najkrótszej ścieżki dla:
W przypadku drzewa wystarczy puścić dowol-ny algorytm przeszukiwania grafu (DFS lub BFS) z początkowego wierzchołka. Z uwagi na fakt, iż w drzewie istnieje dokładnie jedna ścieżka pomiędzy dwoma wierzchołkami, ta jedna ścieżka będzie tą najkrótszą. Odległość między nimi równa jest sumie odległości między kolejnymi wierzchołkami, które będziemy po drodze odwiedzać.
Dla nieważonego grafu użyjemy BFS. Zauważmy, że jeśli określony wierzchołek X jest odległy o n od początkowego wierzchołka A, wszystkie nieodwiedzone wierzchołki, do których można dojść bezpośrednio z X będą oddalone od A o n+1. W ten sposób otrzymamy grupy punktów oddalonych od A o ustaloną odległość.
Przejdźmy do sedna sprawy. Problem najkrótszej ścieżki pojawia się również na Olimpiadzie Informatycznej. Problemy algorytmiczne na OI różnią się jednak od rzeczywistych sytuacji, gdzie można sobie pozwolić na bardziej heurystyczne algorytmy. Tak czy inaczej, podstawowym algorytmem znajdowania najkrótszej ścieżki na OI jest algorytm Dijkstry. Jak on działa?
Będziemy chcieli stworzyć tablicę d[V] odległości od wyjściowego punktu A, które początkowo będą wynosić nieskończoność (poza d[A]=0). Wrzućmy wszystkie wierzchołki na kolejkę priorytetową, której priorytetem jest aktualnie wyliczona odległość od A. Będziemy z tej kolejki zdejmować najbliższy A nierozważony jeszcze wierzchołek. Dla każdego wierzchołka x, do którego można dojść z A rozważamy, czy d[A] + u(A,x) < d[x], gdzie u(A,x) to waga krawędzi między A a x. Jeśli tak, to d[x]:=d[A] + u(A,x).
Algorytm Dijkstry ma jedną wadę: nie działa, gdy w grafie występują krawędzie o ujemnej wadze. Tu z pomocą przychodzi nam algorytm Bellmana-Forda. Kosztem większej złożoności obliczeniowej obliczy nam on odpowiedni wynik lub poinformuje o ujemnym cyklu w grafie (wtedy nie ma sensu obliczanie najkrótszej ścieżki, ponieważ ciągle chodząc po ujemnym cyklu będziemy coraz bardziej zmniejszać jej wartość, aż stanie się ona nieskończenie mała).
Tutaj również będziemy chcieli początkowo przypisać każdemu wierzchołkowi poza A nieskończone d[x]. Różnica jest taka, że zamiast kolejki priorytetowej będziemy wszystkie wierzchołki rozważać po kolei. Dla każdego wierzchołka B rozważymy wszystkich jego sąsiadów x. Jeśli d[x]>d[B]+u(B,x), d[x]:=d[B]+u(B,x).
Powyższe algorytmy służyły do obliczania odległości od pojedynczego wierzchołka. Jeśli mielibyśmy rozważyć odległości między wszystkimi parami wierzchołków, zastosowanie V razy (V - liczba wierzchołków) tych algorytmów często nie jest najlepszym rozwiązaniem. Wówczas stosujemy algorytmy Floyda-Warshalla lub Johnsona.
Algorytm Floyda-Warshalla znajduje odległości między każdą parą wierzchołków lub stwierdza istnienie ujemnego cyklu. Jego zasada działania jest dziecinnie prosta. Utwórzmy dwuwymiarową tablicę d[V][V]. Jeśli istnieje krawędź między A i B, d[A][B] jest równe jej wadze. d[A][A] jest równe 0 dla każdego wierzchołka. Dalszą część algorytmu ilustruje następujący pseu-dokod:
dla każdego wierzchołka k od 1 do n:
dla każdego wierzchołka i od 1 do n:
dla każdego wierzchołka j od 1 do n:
jeśli d[i][j]>d[i][k]+d[k][j]:
d[i][j]:=d[i][k]+d[k][j]
//sprawdzenie, czy jest ujemny cykl
dla każdego wierzchołka k od 1 do n:
jeśli d[k][k]<0: istnieje ujemny cykl
Algorytm Johnsona to de facto złożenie 2 algorytmów. Najpierw do naszego grafu dodamy wierzchołek Q połączony z każdym wierzchołkiem krawędzią o wadze 0. Puśćmy z niego algorytm Bellmana-Forda, który albo wykryje ujemny cykl, albo zwróci tablicę h[V} odległości poszczególnych wierzchołków od Q. Każdej krawędzi (u,v) przypiszemy nową wagę w(u,v):=w(u,v)+h[u]-h[v]. Usuwamy wierzchołek Q i po tak zmodyfikowanym grafie puszczamy z każdego wierzchołka algorytm Dijkstry. Jeśli wiemy, że graf nie zawiera ujemnych krawędzi, możemy po prostu wywołać algorytm Dijkstry z każdego wierzchołka. Algorytm Johnsona jest szybszy niż algorytm Floyda-Warshalla dla grafów rzadkich, czyli o względnie małym stosunku liczby krawędzi do liczby wag.
Powyższe algorytmy mają spore zastosowanie nie tylko na Olimpiadzie Informatycznej, ale i w prawdziwym życiu. W przypadku wyznaczania najkrótszej drogi na mapie są jednak za wolne. Nie możemy pozwolić sobie na przetworzenie wszystkich skrzyżowań. Musimy zastosować coś szybszego, być może jakąś heurystykę lub ich szereg. Z pomocą przychodzi nam dużo szybszy, ale heurystyczny (co skreśla go na OI) algorytm A* i jego odmiany. Z uwagi na mnogość jego odmian i możliwych heurystyk pozwolę sobie go nie omawiać. Przykładowe jego wersje znaleźć można na Wikipedii.
Dziękuję za uwagę.