Algorytmy grafowe Wykład 2 Przeszukiwanie grafów Tomasz Tyksiński CDV
Rozkład materiału 1. Podstawowe pojęcia teorii grafów, reprezentacje komputerowe grafów 2. Przeszukiwanie grafów 3. Spójność grafu, lasy, drzewa, drzewa rozpinające 4. Najkrótsze ścieżki w grafie, algorytm Dijkstry 5. Algorytm Kruskala 6. Cykle Hamiltona w grafach 7. Kolorowanie grafów 8. Skojarzenia w grafach
Przeszukiwanie grafów Pierwszym przedstawionym algorytmem działającym na strukturze grafu będzie algorytm przeglądający wszystkie elementy tej struktury Algorytm ten w różnych odmianach ma wiele zastosowań, od wypisywania wszystkich elementów grafu, przez poszukiwanie czy dany element znajduje się w grafie, po poszukiwanie najkrótszej drogi w grafie lub znajdowanie drzew rozpinających graf
Przeszukiwanie grafów Algorytm zawsze zaczyna się w jednym z wierzchołków v Następnie przeszukujemy wszystkie krawędzie incydentne do tego wierzchołka i poruszamy się do pewnego wierzchołka przyległego w W nowym wierzchołku w przeszukujemy wszystkie krawędzie incydentne do w i przechodzimy do kolejnego wierzchołka Powtarzamy powyższe kroki, aż przeszukamy wszystkie wierzchołki w grafie Ten sposób przeszukiwania wierzchołków grafu nazywamy przeszukiwaniem wszerz (ang. breadth-first search, BFS)
Przykład
Przykład 1 2 3 4 5 6 7
Przeszukiwanie grafów Inną metodą przeszukiwania jest sposób w którym, zamiast przeszukiwania każdej krawędzi incydentnej do wierzchołka v, będziemy poruszać się do pewnego wierzchołka przyległego w (wcześniej jeszcze nie odwiedzonego) W tym wypadku być może zostawiamy wierzchołek v z jakimiś niezbadanymi krawędziami incydentnymi Przeglądamy zatem w grafie ścieżkę (czyli ciąg wierzchołków połączonych krawędziami) przechodząc do nowego wierzchołka, gdy tylko to jest możliwe Taka metoda przeszukiwania grafu, nazywa się przeszukiwaniem w głąb (ang. depth-first search, DFS) lub metodą powrotu po tej samej ścieżce na grafie
Przykład 1 2 3 6 7 4 5
Algorytm BFS Zapiszmy teraz precyzyjniej algorytm przeszukiwania grafu wszerz (BFS) Oznaczmy przez G = (V, E) - graf nieskierowany zapisany w postaci listy wierzchołków sąsiednich x - ustalony wierzchołek od którego rozpoczynamy przeszukiwanie grafu I v - tablica zawierająca wierzchołki incydentne z v NI v - liczba elementów w tablicy I v
Algorytm BFS Precyzyjniejszy opis algorytmu 1. Ustaw: Numer(x) = 1, Drzewo =, Pozostałe = ; dopisz x do Kolejki 2. Jeżeli Kolejka jest pusta to STOP 3. Pobierz element z Kolejki i zapisz go jako v 4. Dla każdego wierzchołka w incydentnego do v wykonaj: 4.1 Jeżeli Numer(w) == 0, tzn. wierzchołek w odwiedzamy po raz pierwszy, to nadaj wierzchołkowi w kolejny numer, dopisz w do Kolejki, a krawędź {v,w} dodaj do Drzewo 4.2 Jeżeli Numer(w)!= 0, tzn. wierzchołek w już był odwiedzany, to jeżeli krawędź {v,w} nie należy już do Drzewa, to dodaj ją do zbioru Pozostałe 5. Wróć do kroku 2.
Algorytm BFS Zapis algorytmu w pseudokodzie BFS(G, x) // zmienne globalne Numer, Drzewo, Pozostałe Numer[x] = 1; Ponumerowano = 1; NKolejka = 1; Kolejka[NKolejka] = x; while (NKolejka > 0) { v = Kolejka[1]; NKolejka = NKolejka 1; for (i=1; i<=nkolejka; i++) Kolejka[i] = Kolejka[i+1]; for (i=1; i<=niv; i++) { w = Iv[i]; if (Numer[w] == 0){ Ponumerowano = Ponumerowano + 1; Numer[w] = Ponumerowano; Drzewo = Drzewo + {v,w}; NKolejka = Nkolejka + 1; Kolejka[NKolejka] = w; } else if ({v,w} not in Drzewo) Pozostałe = Pozostałe + {v,w}; } } return Numer, Drzewo, Pozostałe
Algorytm BFS Przykład działania algorytmu Inicjowanie parametrów Numer[a] = 1; Ponumerowano = 1; NKolejka = 1; Kolejka = a;
Algorytm BFS Przykład działania algorytmu Kroki algorytmu dla a v = a; NKolejka = 0; Kolejka = NULL; w = b; Ponumerowano = 2; Numer[b] = 2; Drzewo = {ab}; NKolejka = 1; Kolejka = [b]; w = c; Ponumerowano = 3; Numer[c] = 3; Drzewo = {ab, ac}; NKolejka = 2; Kolejka = [b, c];
Algorytm BFS Przykład działania algorytmu Kroki algorytmu dla b v = b; NKolejka = 1; Kolejka = [c]; w = a; w = c; w = d; Ponumerowano = 4; Numer[d] = 4; Drzewo = {ab, ac, bd}; NKolejka = 2; Kolejka = [c, d]; w = e; Ponumerowano = 5; Numer[e] = 5; Drzewo = {ab, ac, bd, be}; NKolejka = 3; Kolejka = [c, d, e]; w = f; Ponumerowano = 6; Numer[f] = 6; Drzewo = {ab, ac, bd, be, bf} NKolejka = 4; Kolejka = [c, d, e, f];
Algorytm BFS Przykład działania algorytmu Kroki algorytmu dla c v = c; NKolejka = 3; Kolejka = [d, e, f]; w = a; w = b; w = f; w = g; Ponumerowano = 7; Numer[g] = 7; Drzewo = {ab, ac, bd, be, bf, cg} NKolejka = 4; Kolejka = [d, e, f, g];
Algorytm BFS Przykład działania algorytmu Kroki algorytmu dla d v = d; NKolejka = 3; Kolejka = [e, f, g]; w = b; w = e;
Algorytm BFS Przykład działania algorytmu Kroki algorytmu dla e v = e; NKolejka = 2; Kolejka = [f, g]; w = b; w = d; w = f;
Algorytm BFS Przykład działania algorytmu Kroki algorytmu dla f v = f; NKolejka = 1; Kolejka = [g]; w = b; w = c; w = g;
Algorytm BFS Przykład działania algorytmu Kroki algorytmu dla g v = g; NKolejka = 0; Kolejka = NULL; w = c; w = f;
Algorytm BFS W przypadku grafów skierowanych możemy oczywiście przesuwać się wyłącznie wzdłuż krawędzi wychodzących z odwiedzanego wierzchołka Być może zostaną wówczas na końcu nie odwiedzone wierzchołki Należy wtedy powtórzyć algorytm od kolejnego nie odwiedzonego jeszcze wierzchołka kontynuując numerację i budowę drzewa, które teraz przyjmie formę lasu
Algorytm DFS Opiszemy teraz dokładniej algorytm przeszukiwania grafu w głąb (DFS) Oznaczmy przez G = (V, E) - graf nieskierowany zapisany w postaci listy wierzchołków sąsiednich x - ustalony wierzchołek od którego rozpoczynamy przeszukiwanie grafu I v - tablica zawierająca wierzchołki incydentne z v NI v - liczba elementów w tablicy I v Stos - tablica przechowująca ciąg wierzchołków umożliwiająca powroty algorytmu, NStos - liczba wierzchołków w tablicy Stos
Algorytm DFS Opis słowny algorytmu 1. Ustaw v = x, i = 0, Drzewo =, Pozostałe = 2. Ustaw i = i + 1 oraz Numer(v) = i 3. Poszukaj krawędzi incydentnej do wierzchołka v, która nie została jeszcze odwiedzona. Jeżeli nie ma takiej krawędzi (tzn. po każdej krawędzi incydentnej do v już przeszliśmy), to przejdź do kroku 5. Wybierz pierwszą krawędź incydentną do wierzchołka v, która nie została jeszcze odwiedzona, przykładowo {v,w} i przejdź po niej 4. Jesteśmy w wierzchołku w Jeżeli w jest wierzchołkiem, w którym jeszcze nie byliśmy podczas tego szukania (tzn. Numer(w) jest nieokreślony), to dodaj krawędź {v,w} do zbioru Drzewo. Ustaw v = w i przejdź do kroku 2. Jeżeli w jest wierzchołkiem, w którym już wcześniej byliśmy (tzn. Numer(w) < Numer(v)), to dodaj krawędź {v,w} do zbioru Pozostałe. Przejdź do kroku 3. Jesteśmy więc z powrotem w wierzchołku v 5. Sprawdź, czy istnieje jakaś odwiedzona krawędź {u,v} w zbiorze Drzewo z wartością Numer(u) < Numer(v) 6. Jeżeli jest taka krawędź, to wróć do wierzchołka u. (Zauważmy, że u jest wierzchołkiem, z którego osiągnięto v po raz pierwszy). Ustaw v = u i przejdź do kroku 3. 7. Jeżeli nie ma takiej krawędzi, to zatrzymaj algorytm (jesteśmy z powrotem w korzeniu x po przejściu każdej krawędzi i odwiedzeniu każdego wierzchołka połączonego z x).
Algorytm DFS Zapis algorytmu w pseudokodzie DFS(G, x) // zmienne globalne Numer, Drzewo, Pozostałe Numer[x] = 1; Ponumerowano = 1; NStos = 1; Stos[NStos] = x; while (NStos > 0) { v = Stos[NStos] if (NIv == 0) NStos = NStos 1; else { w = Iv[1]; NIv = NIv 1; for (i = 1; i<=niv; i++) Iv[i] = Iv[i+1]; if (Numer[w] == 0) { Ponumerowano = Ponumerowano + 1; Numer[w] = Ponumerowano; Drzewo = Drzewo + {vw}; NStos = NStos + 1; Stos[NStos] = w; } else if (numer[w]<numer[v]) Pozostałe = Pozostałe + {vw}; } } return Numer, Drzewo, Pozostałe
Algorytm DFS Przykład działania algorytmu Inicjowanie parametrów Numer[a] = 1; Ponumerowano = 1; NStos = 1; Stos[1] = [a];
Algorytm DFS Przykład działania algorytmu Kroki algorytmu dla a v = a; w = b; Ponumerowano = 2; Numer[b] = 2; Drzewo = {ab}; NStos = 2; Stos = [a, b];
Algorytm DFS Przykład działania algorytmu Kroki algorytmu dla b v = b; w = a; w = c; Ponumerowano = 3; Numer[c] = 3; Drzewo = {ab, bc}; NStos = 3; Stos = [a, b, c];
Algorytm DFS Przykład działania algorytmu Kroki algorytmu dla c v = c; w = a; w = b; w = f; Ponumerowano = 4; Numer[f] = 4; Drzewo = {ab, bc, cf}; NStos = 4; Stos = [a, b, c, f];
Algorytm DFS Przykład działania algorytmu Kroki algorytmu dla f v = f; w = b; w = c; w = g; Ponumerowano = 5; Numer[g] = 5; Drzewo = {ab, bc, cf, fg}; NStos = 5; Stos = [a, b, c, f, g] Wycofanie z wierzchołka g v = g; w = c; w = f; NStos = 4; Stos = [a, b, c, f]
Algorytm DFS Przykład działania algorytmu Wycofanie z wierzchołka f v = f; w = b; w = c; w = g; NStos = 3; Stos = [a, b, c] Wycofanie z wierzchołka c v = c; w = a; w = b; w = f; w = g; NStos = 2; Stos = [a, b]
Algorytm DFS Przykład działania algorytmu Kroki algorytmu dla b v = b; w = a; w = d; Ponumerowano = 6; Numer[d] = 6; Drzewo = {ab, bc, cf, fg, bd}; NStos = 3; Stos = [a, b, d]
Algorytm DFS Przykład działania algorytmu Kroki algorytmu dla d v = d; w = b; w = e; Ponumerowano = 7; Numer[e] = 7; Drzewo = {ab, bc, cf, fg, bd, de}; NStos = 4; Stos = [a, b, d, e]; Wycofanie z wierzchołka e v = e; w = b; w = d; NStos = 3; Stos = [a, b, d];
Algorytm DFS Przykład działania algorytmu Wycofanie z wierzchołka d v = d; w = b; w = e; NStos = 2; Stos = [a, b]; Wycofanie z wierzchołka b v = b; w = a; w=c; w=d; w=e; w=f; NStos = 1; Stos = [a]
Algorytm DFS Przykład działania algorytmu Wycofanie z wierzchołka a v = a; w = b; w = c; NStos = 0; Stos = NULL;
Algorytm DFS Algorytm DFS można zapisać w prostszej rekurencyjnej wersji, zamiast stosowania struktury stosu DFS(G, x) // zmienne globalne Numer, Drzewo, Pozostałe v = x; Numer[v] = 1; Ponumerowano = 1; for each w in N(v) { // N(v) wierzchołki incydentne z v if (Numer[w] == 0) { Ponumerowano = Ponumerowano + 1; Numer[w] = Ponumerowano; Drzewo = Drzewo + {vw}; DFS(G,w); } else if (Numer[w] < Numer[v]) Pozostałe = Pozostałe + {vw}; } return Numer, Drzewo, Pozostałe // REKURENCJA
Algorytm DFS Rozpatrując listową reprezentację grafu (tablica wierzchołków z dołączonymi listami sąsiadów) można zapisać powyższy algorytm rekurencyjny w jeszcze prostszy sposób Przez odwiedzony[v] oznaczmy tablicę flag oznaczających czy dany wierzchołek v był już odwiedzony, na początku zawiera ona wartości false Przez aktualny[v] oznaczmy tablicę wskaźników na pierwsze elementy list wierzchołków L[v] DFS(G, v) odwiedzone[v] = true; while (aktualny[v]!= NULL){ w = wierzchołek na liście L[v] wskazywany przez aktualny[v]; if (odwiedzony[w] == false) DFS(G, w); aktualny[v] = wskaźnik do następnego wierzchołka na liście L[v]; }
Algorytm DFS Złożoność algorytmu DFS oraz wielu jego pochodnych, wykorzystujących DFS, jest rzędu O( V + E ), czyli O(m + n)
Zastosowanie Algorytm DFS można wykorzystać do napisania wielu innych algorytmów Przykładowo do algorytmu numerującego wszystkie wierzchołki grafu NumerujWierzchołki(G) for each v in V Numer[v] = 0; Drzewo = ; Pozostałe = ; for each v in V if (Numer[v] == 0) DFS(G,v);
Zastosowanie Za pomocą odpowiedniej klasyfikacji krawędzi podczas działania algorytmu DFS możemy wykonać sortowanie topologiczne skierowanego grafu acyklicznego Polega ono na ułożeniu wierzchołków grafu na linii prostej w takiej kolejności, że wszystkie łuki prowadzą w prawą stronę Zapis taki może ilustrować kolejność wykonywania różnych zależnych od siebie procesów
Zastosowanie Przykładowo jeżeli wiemy jakie części garderoby mamy zakładać po sobie, to jak ustawić je w kolejności ubierania
Zastosowanie W wyniku sortowania topologicznego powyższego grafu otrzymujemy następującą strukturę Opisuje ona przykładową bezkolizyjną kolejność zakładania części garderoby: 1. skarpetki, 2. slipki, 3. spodnie, 4. buty, 5. zegarek, 6. koszula, 7. pasek, 8. krawat, 9. marynarka
Podwójne numerowanie Zanim opiszemy algorytm sortowania topologicznego zauważmy pewne własności poszukiwania DFS Po wykonaniu algorytmu dostajemy las przeszukiwań w głąb lub w szczególności drzewo gdy graf jest spójny Możemy dodatkowo oznaczyć wierzchołek odwiedzony jako odwiedzony po raz pierwszy podczas przeszukiwania wierzchołek przetworzony, czyli taki którego lista sąsiedztwa została już całkowicie przeanalizowana
Podwójne numerowanie Wprowadzamy etykiety czasowe dla każdego wierzchołka, jako dwie liczby całkowite z przedziału 1, 2, 3, 2 V jedna opisująca czas odwiedzin (wierzchołek odwiedzony) druga czas przetworzenia (wierzchołek przetworzony) Zapis etykiet można wprowadzić poprzez prostą modyfikację algorytmu DFS, dodającego numer czasu przetworzenia dla wierzchołka v gdy odwiedzimy wszystkich jego sąsiadów (za pętlą for each)
Przykład 1 14 2 13 3 8 9 12 10 11 4 7 5 6
Podwójne numerowanie W przypadku grafów skierowanych, jak w sortowaniu topologicznym, oznaczając graf skierowany spójny nie musimy otrzymać struktury drzewa Kierunki łuków mogą spowodować, że od pewnego wierzchołka nie będzie można kontynuować poszukiwań, należy wówczas wybrać kolejny nie odwiedzony wierzchołek Otrzymamy wówczas zamiast drzewa strukturę lasu (nawet gdy graf jest spójny)
Przykład 1 10 2 7 8 9 3 6 4 5 11 14 12 13
Algorytm sortowania TopologicalSort(G) 1. Wykonaj DFS(G) obliczając czasy przetworzenia wszystkich wierzchołków 2. Wstaw wierzchołek v na początek listy, kiedy tylko zostanie on przetworzony 3. return lista wierzchołków
Przykład Posortujmy graf opisujący ubieranie strojów 11 16 17 18 12 15 13 14 6 7 1 8 2 5 3 4 9 10 Skarpetki Slipki Spodnie Buty Zegarek Koszula Pasek Krawat Marynarka
Zadanie Przeprowadź obliczenia Przeszukiwania wszerz (BFS) Przeszukiwania w głąb (DFS) Sortowania topologicznego dla powyższego grafu skierowanego Wypisz kolejność odwiedzania wierzchołków w obu przeszukiwaniach, startując od wybranego przez siebie miejsca w grafie
Następny wykład Spójność grafu Drzewa rozpinające