Serwis Edukacyjny w I-LO w Tarnowie Materiały dla uczniów liceum |
Wyjście Spis treści Wstecz Dalej Autor artykułu: mgr Jerzy Wałaszek |
©2024 mgr Jerzy Wałaszek |
ProblemW spójnym grafie ważonym należy znaleźć najkrótsze ścieżki od wybranego wierzchołka startowego do wszystkich pozostałych wierzchołków. |
Jeśli ścieżki grafu posiadają nieujemne wagi, to najlepszym rozwiązaniem tego problemu jest przedstawiony w poprzednim rozdziale algorytm Dijkstry. W niektórych zastosowaniach ścieżki mogą posiadać wagi ujemne. W takim przypadku musimy użyć nieco mniej efektywnego, lecz bardziej wszechstronnego algorytmu Bellmana-Forda. Algorytm tworzy poprawny wynik tylko wtedy, gdy graf nie zawiera ujemnego cyklu (ang. negative cycle), czyli cyklu, w którym suma wag krawędzi jest ujemna. Jeśli taki cykl istnieje w grafie, to każdą ścieżkę można "skrócić" przechodząc wielokrotnie przez cykl ujemny. W takim przypadku algorytm Bellmana-Forda zgłasza błąd.
Opisany tutaj algorytm będzie tworzył dwie n-elementowe tablice danych (n oznacza liczbę wierzchołków w grafie):
Na początku algorytmu ustawiamy wszystkie komórki tablicy d na największą możliwą wartość (oryginalnie na nieskończoność) za wyjątkiem komórki odwzorowującej wierzchołek startowy, w której umieszczamy 0. Natomiast we wszystkich komórkach tablicy p umieszczamy -1 (w grafie nie ma wierzchołka o numerze -1, oznacza to zatem brak poprzednika).
Następnie wykonujemy n-1 obiegów pętli, w której dokonujemy relaksacji krawędzi (każdy obieg ustala koszt dojścia do przynajmniej jednego wierzchołka grafu, ponieważ wierzchołek startowy ma koszt 0, to pozostaje nam ustalenie kosztu jeszcze dla n-1 wierzchołków, stąd wymagane jest co najwyżej n-1 obiegów pętli). Polega ona na tym, iż przeglądamy po kolei wszystkie krawędzie grafu. Jeśli natrafimy na krawędź u – v o wadze w, dla której koszt dojścia d [v] jest większy od kosztu dojścia d [u]+w (czyli dojście do wierzchołka v od wierzchołka u tą krawędzią jest tańsze od poprzednio znalezionych dojść), to ustawiamy koszt d [v] na d [u]+w i w tablicy poprzedników dla p [v] umieszczamy numer wierzchołka u. Gdy pętla wykona n-1 obiegów, w tablicy d będziemy mieli koszty dojść do poszczególnych wierzchołków grafu po najkrótszych ścieżkach, a w tablicy p dla każdego wierzchołka znajdziemy jego poprzednik na najkrótszej ścieżce od wierzchołka startowego.
Należy jeszcze sprawdzić, czy w grafie nie występuje cykl ujemny. W tym celu jeszcze raz przeglądamy zbiór krawędzi i jeśli natrafimy na krawędź u – v o wadze w dla której dalej koszt dojścia d [v] jest większy od d [u] = w, to mamy do czynienia z cyklem ujemnym ( normalnie sytuacja taka nie może wystąpić, ponieważ relaksacja powinna ustawić d [v] na d [u]+w. Tylko w przypadku istnienia cyklu ujemnego relaksacja sobie z tym nie radzi). W takim przypadku algorytm powinien zgłosić błąd.
Prześledźmy na przykładzie sposób pracy algorytmu Bellmana-Forda:
Oto nasz graf ważony z ujemnymi wagami krawędzi. | ||||||||||||||||||||||||||||
|
Wybieramy na wierzchołek startowy wierzchołek o numerze zero.
Policzymy koszty dojść do pozostałych wierzchołków po najkrótszych
ścieżkach. Tworzymy tablice d i p, wypełniając je odpowiednio |
|||||||||||||||||||||||||||
|
W pierwszym obiegu relaksujemy krawędź 0 – 1, dla której mamy: d [0] = 0 d [1] = ∞ d [1] > d [0] = 5 Ustawiamy: d [1] ← 0+5 = 5 p [1] ← 0 (do wierzchołka 1 przychodzimy z wierzchołka 0). |
|||||||||||||||||||||||||||
|
W drugim obiegu relaksujemy krawędzie 1 – 3 i 1 – 4.
|
|||||||||||||||||||||||||||
|
W trzecim obiegu relaksujemy krawędzie 4 – 2, 4 – 5 i 3 – 4
(zakładamy najbardziej niekorzystną kolejność relaksacji).
|
|||||||||||||||||||||||||||
|
W czwartym obiegu relaksujemy krawędzie 4 – 2 i 4 – 5
|
|||||||||||||||||||||||||||
|
Obieg piąty nie wprowadzi już żadnych zmian, ponieważ warunek
relaksacji nie będzie spełniony dla żadnej z krawędzi
(pętlę relaksacji można przerwać, jeśli w danym obiegu algorytm nie
wprowadził żadnych dalszych zmian do tablic d i p).
Tablica d zawiera zatem minimalne koszty dojścia od
wierzchołka 0 do poszczególnych wierzchołków w grafie. Tablica p
zawiera informacje o najkrótszych ścieżkach wiodących od wierzchołka
0 do pozostałych wierzchołków. Graf nie posiada ujemnych cykli. |
n | : | liczba wierzchołków w grafie, n ∈ C. |
graf | : | zadany w dowolnie wybrany sposób, algorytm tego nie precyzuje. Graf musi być spójny. |
v | : | numer wierzchołka startowego, v ∈ C. |
d | : | n elementowa tablica z kosztami dojścia. Element d [i] zawiera koszt najkrótszej ścieżki od wierzchołka v do i. |
p | : | n elementowa tablica poprzedników. Element p [i] zawiera numer wierzchołka, który jest poprzednikiem wierzchołka i-tego na najkrótszej ścieżce od wierzchołka v do i. Element p [v] = -1, ponieważ wierzchołek startowy nie posiada poprzednika.. |
false:
graf zawiera ujemny cykl, który uniemożliwia wyznaczenie najkrótszych ścieżek (każda ścieżka może być najkrótszą, jeśli przepuścimy ją odpowiednią liczbę razy przez cykl ujemny).
x, y | : | numery wierzchołków w grafie, x, y ∈ C. |
i | : | licznik pętli, i ∈ C. |
test | : | logiczna zmienna decyzyjna. |
K01: | Utwórz n elementową tablicę
d i wypełnij ją największą liczbą |
|
K02: | Utwórz n elementową tablicę
p i wypełnij ją liczbami -1 |
|
K03: | d [v] ← 0 | koszt dojścia do wierzchołka startowego |
K04: | Dla i = 2, 3, …,
n
: wykonuj kroki K05…K12 |
pętlę główną wykonujemy co najwyżej n-1 razy |
K05: | test ← true | zmienna przechowuje informację o zmianach |
K06: | Dla x
= 0, 1, …, n-1: wykonuj kroki K07…K11 |
|
K07: |
Dla każdego sąsiada y wierzchołka x : wykonuj kroki K08…K11 |
|
K08: |
Jeśli d [y] ≤
d [x]+waga krawędzi x-y, to następny obieg pętli K07 |
sprawdzamy warunek relaksacji krawędzi |
K09: | test ← false | zapamiętujemy zmianę |
K10: | d [y] ← d [x]+waga krawędzi x-y | dokonujemy relaksacji krawędzi |
K11: | p [y] ← x | ustawiamy poprzednik wierzchołka y na x |
K12: | Jeśli test
= true, to zakończ z wynikiem true |
wynik w d i p |
K13: | Dla x = 0, 1, …,
n-1: wykonuj krok K14 |
sprawdzamy istnienie ujemnego cyklu |
K14: | Dla każdego
sąsiada y wierzchołka x : wykonuj: Jeśli d [y ] > d [x]+waga krawędzi x-y, to zakończ z wynikiem false |
ujemny cykl!!! |
K15: | Zakończ z wynikiem true |
Uwaga:
Zanim uruchomisz program, przeczytaj wstęp do tego artykułu, w którym wyjaśniamy funkcje tych programów oraz sposób korzystania z nich. |
Poniższy wyznacza ścieżki o
najniższym koszcie dojścia do poszczególnych wierzchołków grafu za
pomocą algorytmu Bellmana-Forda. Definicja danych wejściowych jest
następująca: W pierwszym wierszu program odczytuje kolejno numer wierzchołka startowego v, liczbę wierzchołków n oraz liczbę krawędzi m. W m kolejnych wierszach znajdują się definicje krawędzi. Definicja składa się z trzech liczb x y w. Pierwsza i druga liczba określa wierzchołki grafu, które są ze sobą połączone tą krawędzią – krawędź biegnie od wierzchołka x do wierzchołka y. Trzecia liczba jest wagą krawędzi w. |
Dane przykładowe (przekopiuj do
schowka i wklej do okna konsoli):
|
Pascal// Algorytm Bellmana-Forda // Data : 13.04.2014 // (C)2014 mgr Jerzy Wałaszek //--------------------------- program bellman_ford; // Typy danych type PSLel = ^SLel; SLel = record next : PSLel; v, w : integer; end; // Zmienne globalne var m, n : integer; // Liczba krawędzi i wierzchołków w grafie A : array of PSLel; // Tablica dynamiczna list sąsiedztwa d : array of int64; // Tablica kosztów dojścia p : array of integer; // Tablica poprzedników // Funkcja wyznacza najkrótsze ścieżki // v-wierzchołek startowy // Wyjście: // true -wyniki w d i p // false-graf zawiera ujemny cykl //------------------------------------ function BF (v : integer) : boolean; var i, x : integer; test : boolean; pv : PSLel; begin d [v] := 0; // Zerujemy koszt dojścia do v for i := 2 to n do // Pętla relaksacji begin test := true; // Oznacza, że algorytm nie wprowadził zmian do d i p for x := 0 to n-1 do // Przechodzimy przez kolejne wierzchołki grafu begin pv := A [x]; // Przeglądamy listę sąsiadów wierzchołka x while pv <> nil do begin if d [pv^.v] > d [x] = pv^.w then // Sprawdzamy warunek relaksacji begin test := false; // Jest zmiana w d i p d [pv^.v] := d [x]+pv^.w; // Relaksujemy krawędź z x do jego sąsiada p [pv^.v] := x; // Poprzednikiem sąsiada będzie x end; pv := pv^.next; // Następny sąsiad end; if test then Exit (true); // Jeśli nie było zmian, to kończymy end; end; // Sprawdzamy istnienie ujemnego cyklu for x := 0 to n-1 do begin pv := A [x]; while pv <> nil do begin if d [pv^.v] > d [x]+pv^.w then Exit (false); // ujemny cykl!! pv := pv^.next; end; end; BF := true; end; // ********************** // *** PROGRAM GŁÓWNY *** // ********************** var S : array of integer; // Stos i, v, x, y, w, sptr : integer; rv, pv : PSLel; begin read (v, n, m); // Wierzchołek startowy, liczba wierzchołków i krawędzi SetLength (A, n); // Tworzymy tablicę list sąsiedztwa SetLength (d, n); // Tworzymy tablicę kosztów dojścia SetLength (p, n); // Tworzymy tablice poprzedników for i := 0 to n-1 do // Inicjujemy struktury danych begin d [i] := MAXINT; p [i] := -1; A [i] := nil; end; for i := 1 to m do begin read (x, y, w); // Czytamy wierzchołki krawędzi oraz jej wagę new (pv); // Tworzymy element listy pv^.v := y; // Inicjujemy go pv^.w := w; pv^.next := A [x]; // Dodajemy go na początek listy sąsiadów wierzchołka x A [x] := pv; end; writeln; // Wyznaczamy najkrótsze ścieżki algorytmem Bellmana-Forda if BF (v) then begin SetLength (S, n); // Tworzymy prosty stos sptr := 0; for i := 0 to n-1 do begin write (i, ': '); x := i; // Wierzchołki ścieżki umieszczamy na stosie repeat // w kolejności od ostatniego do pierwszego S [sptr] := x; inc (sptr); x := p [x]; until x = -1; while sptr > 0 do // Wierzchołki ze stosu drukujemy begin // w kolejności od pierwszego do ostatniego dec (sptr); write (S [sptr], ' '); end; writeln('$', d [i]); // Na końcu wyświetlamy koszt end; SetLength (S, 0); // Usuwamy stos end else writeln('Negative Cycle found!'); // Usuwamy struktury dynamiczne for i := 0 to n-1 do begin pv := A [i]; while pv <> nil do begin rv := pv; pv := pv^.next; dispose (rv); end; end; SetLength (A, 0); SetLength (d, 0); SetLength (p, 0); end. |
// Algorytm Bellmana-Forda // Data : 13.04.2014 // (C)2014 mgr Jerzy Wałaszek //--------------------------- #include <iostream> using namespace std; const int MAXINT = 2147483647; // Największa liczba całkowita // Typy danych struct SLel { SLel * next; int v, w; }; // Zmienne globalne int m, n; // Liczba krawędzi i wierzchołków w grafie SLel ** A; // Tablica dynamiczna list sąsiedztwa long long * d; // Tablica kosztów dojścia int * p; // Tablica poprzedników // Funkcja wyznacza najkrótsze ścieżki // v-wierzchołek startowy // Wyjście: // true -wyniki w d i p // false-graf zawiera ujemny cykl //------------------------------------ bool BF (int v) { int i, x; bool test; SLel * pv; d [v] = 0; // Zerujemy koszt dojścia do v for(i = 1; i < n; i++) // Pętla relaksacji { test = true; // Oznacza, że algorytm nie wprowadził zmian do d i p for(x = 0; x < n; x++) // Przechodzimy przez kolejne wierzchołki grafu for(pv = A [x]; pv; pv = pv->next) // Przeglądamy listę sąsiadów wierzchołka x if(d [pv->v] > d [x]+pv->w) // Sprawdzamy warunek relaksacji { test = false; // Jest zmiana w d i p d [pv->v] = d [x]+pv->w; // Relaksujemy krawędź z x do jego sąsiada p [pv->v] = x; // Poprzednikiem sąsiada będzie x } if(test) return true; // Jeśli nie było zmian, to kończymy } // Sprawdzamy istnienie ujemnego cyklu for(x = 0; x < n; x++) for(pv = A [x];pv;pv = pv->next) if(d [pv->v] > d [x]+pv->w) return false; // ujemny cykl!! return true; } // ********************** // *** PROGRAM GŁÓWNY *** // ********************** int main() { int i, v, x, y, w, sptr, *S; SLel *rv, *pv; cin >> v >> n >> m; // Wierzchołek startowy, liczba wierzchołków i krawędzi A = new SLel * [n]; // Tworzymy tablicę list sąsiedztwa d = new long long [n]; // Tworzymy tablicę kosztów dojścia p = new int [n]; // Tworzymy tablice poprzedników for(i = 0; i < n; i++) // Inicjujemy struktury danych { d [i] = MAXINT; p [i] = -1; A [i] = NULL; } for(i = 0; i < m; i++) { cin >> x >> y >> w; // Czytamy wierzchołki krawędzi oraz jej wagę pv = new SLel; // Tworzymy element listy pv->v = y; // Inicjujemy go pv->w = w; pv->next = A [x]; // Dodajemy go na początek listy sąsiadów wierzchołka x A [x] = pv; } cout << endl; // Wyznaczamy najkrótsze ścieżki algorytmem Bellmana-Forda if(BF (v)) { S = new int [n]; // Tworzymy prosty stos sptr = 0; for(i = 0; i < n; i++) { cout << i << ": "; for(x = i;x != -1;x = p [x]) // Wierzchołki ścieżki umieszczamy na stosie S [sptr++] = x; // w kolejności od ostatniego do pierwszego while(sptr) // Wierzchołki ze stosu drukujemy cout << S [--sptr] << " "; // w kolejności od pierwszego do ostatniego cout << "$" << d [i] << endl; // Na końcu wyświetlamy koszt } delete [] S; // Usuwamy stos } else cout << "Negative Cycle found!" << endl; // Usuwamy struktury dynamiczne for(i = 0; i < n; i++) { pv = A [i]; while(pv) { rv = pv; pv = pv->next; delete rv; } } delete [] A; delete [] d; delete [] p; return 0;} |
Basic' Algorytm Bellmana-Forda ' Data : 13.04.2014 ' (C)2014 mgr Jerzy Wałaszek '--------------------------- const MAXINT = 2147483647 ' Największa liczba całkowita ' Typy danych Type SLel Next As SLel Ptr v As Integer w As Integer End Type ' Zmienne globalne Dim Shared As Integer m, n ' Liczba krawędzi i wierzchołków w grafie Dim Shared As SLel Ptr Ptr A ' Tablica dynamiczna list sąsiedztwa Dim Shared As LongInt Ptr d ' Tablica kosztów dojścia Dim Shared As Integer Ptr p ' Tablica poprzedników ' Funkcja wyznacza najkrótsze ścieżki ' v-wierzchołek startowy ' Wyjście: ' true -wyniki w d i p ' false-graf zawiera ujemny cykl '------------------------------------ Function BF (ByVal v As integer) As Integer Dim As Integer i, x, test Dim As SLel Ptr pv d [v] = 0 ' Zerujemy koszt dojścia do v For i = 2 To n ' Pętla relaksacji test = 1 ' Oznacza, że algorytm nie wprowadził zmian do d i p For x = 0 To n-1 ' Przechodzimy przez kolejne wierzchołki grafu pv = A [x] While pv ' Przeglądamy listę sąsiadów wierzchołka x If d [pv->v] > d [x]+pv->w Then ' Sprawdzamy warunek relaksacji test = 0 ' Jest zmiana w d i p d [pv->v] = d [x]+pv->w ' Relaksujemy krawędź z x do jego sąsiada p [pv->v] = x ' Poprzednikiem sąsiada będzie x End If pv = pv->Next ' Następny sąsiad Wend Next If test = 1 Then Return 1 ' Jeśli nie było zmian, to kończymy Next ' Sprawdzamy istnienie ujemnego cyklu For x = 0 To n-1 pv = A [x] While pv If d [pv->v] > d [x]+pv->w Then Return 0 ' ujemny cykl!! pv = pv->Next Wend Next return 1 End Function ' ********************** ' *** PROGRAM GŁÓWNY *** ' ********************** Dim As Integer i, v, x, y, w, sptr Dim As Integer Ptr S Dim As SLel Ptr rv, pv Open Cons For Input As #1 Input #1, v, n, m ' Wierzchołek startowy, liczba wierzchołków i krawędzi A = New SLel Ptr [n] ' Tworzymy tablicę list sąsiedztwa d = New LongInt [n] ' Tworzymy tablicę kosztów dojścia p = New integer [n] ' Tworzymy tablice poprzedników For i = 0 To n-1 ' Inicjujemy struktury danych d [i] = MAXINT p [i] = -1 A [i] = 0 Next For i = 0 To m-1 Input #1, x, y, w ' Czytamy wierzchołki krawędzi oraz jej wagę pv = New SLel ' Tworzymy element listy pv->v = y ' Inicjujemy go pv->w = w pv->next = A [x] ' Dodajemy go na początek listy sąsiadów wierzchołka x A [x] = pv Next Close #1 Print ' Wyznaczamy najkrótsze ścieżki algorytmem Bellmana-Forda If BF (v) = 1 Then S = New Integer [n] ' Tworzymy prosty stos sptr = 0 For i = 0 To n-1 Print Using "&: ";i; x = i While x <> -1 ' Wierzchołki ścieżki umieszczamy na stosie S [sptr] = x ' w kolejności od ostatniego do pierwszego sptr += 1 x = p [x] Wend While sptr > 0 ' Wierzchołki ze stosu drukujemy sptr -= 1 Print Using "& ";S [sptr]; ' w kolejności od pierwszego do ostatniego Wend Print using "$&";d [i] ' Na końcu wyświetlamy koszt Next Delete [] S ' Usuwamy stos Else Print "Negative Cycle found!" EndIf Print ' Usuwamy struktury dynamiczne For i = 0 To n-1 pv = A [i] While pv rv = pv pv = pv->Next Delete rv Wend Next Delete [] A Delete [] d Delete [] p End |
Wynik: | |
0 6 11 0 1 5 1 3 3 1 4 9 2 0 3 2 1 -4 3 4 3 3 5 2 4 2 -1 4 5 -5 5 0 9 5 2 8 0: 0 $0 1: 0 1 $5 2: 0 1 3 4 2 $10 3: 0 1 3 $8 4: 0 1 3 4 $11 5: 0 1 3 4 5 $6 |
Algorytm Bellmana-Forda zawodzi, gdy graf zawiera cykl ujemny. Z drugiej strony możemy wykorzystać ten algorytm właśnie do wykrywania istnienia cyklu ujemnego w grafie.
Zespół Przedmiotowy Chemii-Fizyki-Informatyki w I Liceum Ogólnokształcącym im. Kazimierza Brodzińskiego w Tarnowie ul. Piłsudskiego 4 ©2024 mgr Jerzy Wałaszek |
Materiały tylko do użytku dydaktycznego. Ich kopiowanie i powielanie jest dozwolone pod warunkiem podania źródła oraz niepobierania za to pieniędzy.
Pytania proszę przesyłać na adres email:
Serwis wykorzystuje pliki cookies. Jeśli nie chcesz ich otrzymywać, zablokuj je w swojej przeglądarce.
Informacje dodatkowe.