Algorytmy i struktury danych/Przeszukiwanie grafów: Różnice pomiędzy wersjami
m |
|||
Linia 1: | Linia 1: | ||
− | |||
Z poprzedniego wykładu wiemy, że gdy celem jest ułożenie algorytmu o złożoności liniowej ze względu na rozmiar grafu (czyli sumę liczb wierzchołków i krawędzi), to graf powinien być reprezentowany przez listy sąsiedztw. Na tym wykładzie przyjmiemy taką właśnie reprezentację grafu wejściowego. Rozwiązanie każdego problemu grafowego, które zależy od poznania struktury połączeń w całym grafie, wymaga przeszukania jego wierzchołków i krawędzi. Takie przeszukanie polega na ''odwiedzeniu'' każdego wierzchołka i zbadaniu krawędzi opuszczających już odwiedzone wierzchołki. Jeśli okazuje się, że drugi koniec badanej krawędzi nie był jeszcze odwiedzony, dołącza się go do zbioru wierzchołków odwiedzonych. Przeszukiwania dokonujemy z wykorzystaniem dynamicznego zbioru <math>S</math>, w którym przechowujemy odwiedzane wierzchołki i których sąsiedztwa nie są jeszcze do końca zbadane. Zakładamy, że na zbiorze <math>S</math> wykonywane są następujące operacje: | Z poprzedniego wykładu wiemy, że gdy celem jest ułożenie algorytmu o złożoności liniowej ze względu na rozmiar grafu (czyli sumę liczb wierzchołków i krawędzi), to graf powinien być reprezentowany przez listy sąsiedztw. Na tym wykładzie przyjmiemy taką właśnie reprezentację grafu wejściowego. Rozwiązanie każdego problemu grafowego, które zależy od poznania struktury połączeń w całym grafie, wymaga przeszukania jego wierzchołków i krawędzi. Takie przeszukanie polega na ''odwiedzeniu'' każdego wierzchołka i zbadaniu krawędzi opuszczających już odwiedzone wierzchołki. Jeśli okazuje się, że drugi koniec badanej krawędzi nie był jeszcze odwiedzony, dołącza się go do zbioru wierzchołków odwiedzonych. Przeszukiwania dokonujemy z wykorzystaniem dynamicznego zbioru <math>S</math>, w którym przechowujemy odwiedzane wierzchołki i których sąsiedztwa nie są jeszcze do końca zbadane. Zakładamy, że na zbiorze <math>S</math> wykonywane są następujące operacje: | ||
− | <math>Insert(S,v)</math>: wstaw do zbioru <math>S</math> wierzchołek <math>v</math> | + | <math>Insert(S,v)</math>: wstaw do zbioru <math>S</math> wierzchołek <math>v</math>; |
− | <math>Get(S)</math>: funkcja, której wynikiem jest (dowolny) wierzchołek z <math>S</math>, | + | <math>Get(S)</math>: funkcja, której wynikiem jest (dowolny) wierzchołek z <math>S</math>, wywoływana tylko wtedy, gdy <math>S \ne \emptyset</math>; |
− | <math>Delete(S,v)</math>: usuń z <math>S</math> wierzchołek <math>v</math> | + | <math>Delete(S,v)</math>: usuń z <math>S</math> wierzchołek <math>v</math> (zazwyczaj będzie to wierzchołek <math>Get(S)</math>. |
− | Informacje o tym, które wierzchołki zostały już odwiedzone będziemy przechowywać w tablicy logicznej <math>visited[1..n]</math>. Wartością <math>visited[v]</math> | + | Informacje o tym, które wierzchołki zostały już odwiedzone, będziemy przechowywać w tablicy logicznej <math>visited[1..n]</math>. Wartością <math>visited[v]</math> jest PRAWDA (TRUE) wtedy i tylko wtedy, gdy wierzchołek <math>v</math> został już odwiedzony. Do przechodzenia list sąsiedztw posłuży nam tablica <math>current[1..n]</math>. Wartością <math>current[v]</math> jest pierwszy wierzchołek na liście <math>L[v]</math> - sąsiad <math>v</math> - który nie był jeszcze oglądany od strony <math>v</math>, co oznacza że krawędź opuszczająca <math>v</math> w kierunku tego sąsiada nie została jeszcze zbadana. |
Inicjacja każdego przeszukiwania wygląda następująco: | Inicjacja każdego przeszukiwania wygląda następująco: | ||
Linia 53: | Linia 52: | ||
'''Wynik''' | '''Wynik''' | ||
− | Tablica <math>c[1..n]</math> o wartościach w zbiorze wierzchołków <math>V</math> taka, że <math>c[u]=c[v]</math> wtedy i tylko wtedy, gdy <math>u</math> i <math>v</math> należą do tej samej spójnej składowej | + | Tablica <math>c[1..n]</math> o wartościach w zbiorze wierzchołków <math>V</math> taka, że <math>c[u]=c[v]</math> wtedy i tylko wtedy, gdy <math>u</math> i <math>v</math> należą do tej samej spójnej składowej. |
Zauważmy, że problem spójności bardzo łatwo rozwiązać przy użyciu procedury <math>Search(s)</math>. Jeśli przeszukiwanie rozpoczynamy od nieodwiedzonego wierzchołka <math>s</math>, to w wyniku wykonania <math>Search</math> zostaną odwiedzone wszystkie wierzchołki w grafie <math>G</math> osiągalne z <math>s</math>. Innymi słowy odwiedzona zostanie cała spójna składowa zawierająca <math>s</math>. Jeśli dla każdego nowo odwiedzanego wierzchołka <math>v</math> wykonamy przypisanie <math>c[v] := s</math>, to po zakończeniu przeszukiwania spójnej składowej zawierającej <math>s</math>, wartość <math>c</math> każdego wierzchołka w tej składowej będzie właśnie równa <math>s</math>. Jedyne miejsce, w którym musimy zmienić procedurę <math>Search</math> jest wiersz 16. Nowy wiersz 16 powinien mieć postać: | Zauważmy, że problem spójności bardzo łatwo rozwiązać przy użyciu procedury <math>Search(s)</math>. Jeśli przeszukiwanie rozpoczynamy od nieodwiedzonego wierzchołka <math>s</math>, to w wyniku wykonania <math>Search</math> zostaną odwiedzone wszystkie wierzchołki w grafie <math>G</math> osiągalne z <math>s</math>. Innymi słowy odwiedzona zostanie cała spójna składowa zawierająca <math>s</math>. Jeśli dla każdego nowo odwiedzanego wierzchołka <math>v</math> wykonamy przypisanie <math>c[v] := s</math>, to po zakończeniu przeszukiwania spójnej składowej zawierającej <math>s</math>, wartość <math>c</math> każdego wierzchołka w tej składowej będzie właśnie równa <math>s</math>. Jedyne miejsce, w którym musimy zmienić procedurę <math>Search</math> jest wiersz 16. Nowy wiersz 16 powinien mieć postać: | ||
Linia 68: | Linia 67: | ||
5 <math>SearchCC(v)</math> | 5 <math>SearchCC(v)</math> | ||
− | Dokonamy teraz analizy złożoności algorytmu obliczania spójnych składowych. Zakładamy przy tym, że każda z operacji na pomocniczym zbiorze <math>S</math> jest wykonywana w czasie stałym. Zauważmy, że każdy wierzchołek jest dokładnie raz wstawiany do zbioru <math>S</math> - w momencie odkrycia go jako | + | Dokonamy teraz analizy złożoności algorytmu obliczania spójnych składowych. Zakładamy przy tym, że każda z operacji na pomocniczym zbiorze <math>S</math> jest wykonywana w czasie stałym. Zauważmy, że każdy wierzchołek jest dokładnie raz wstawiany do zbioru <math>S</math> - w momencie odkrycia go jako nieodwiedzonego - i dokładnie raz jest usuwany ze zbioru <math>S</math> - po zbadaniu całego jego sąsiedztwa. Sąsiadów wierzchołka przeglądamy przechodząc po jego liście sąsiedztwa, a dla każdego elementu listy obliczenia z nim związane zajmują stały czas – jeśli wierzchołek był już odwiedzony, to nic nie robimy, natomiast, jeśli nie był odwiedzony, to markujemy go jako odwiedzonego i wstawiamy do zbioru <math>S</math>. Tak więc obliczenia związane z przeglądaniem sąsiedztw wierzchołków zajmują czas proporcjonalny do sumy długości list sąsiedztw, która to suma wynosi <math>2m</math>. Z naszej analizy wynika, że problem spójnych składowych można rozwiązać w czasie <math>O(n+m)</math>, czyli liniowym ze względu na rozmiar grafu. |
− | W dalszym ciągu tego wykładu będziemy rozważali tylko grafy spójne. Niech <math>G=(V,E)</math> będzie grafem spójnym, a <math>s</math> wyróżnionym wierzchołkiem w <math>G</math>. Zanalizujmy raz jeszcze wykonanie procedury <math>Search(s)</math> dla grafu <math>G</math>. Dla każdego wierzchołka <math>v \ne s</math> niech <math>p[v]</math> będzie wierzchołkiem z którego wierzchołek <math>v</math> zostaje odwiedzony, tzn. <math>p[v]</math> zostaje zamarkowany jako odwiedzony w wierszu 16 gdy zostaje odkryty na liście sąsiedztwa <math>v</math>. Nietrudno zauważyć, że graf <math>(V,\{v-p[v]: v \in V-\{s\}\})</math> jest drzewem rozpinającym grafu <math>G</math>. Jeśli każdą krawędź tego drzewa zorientujemy od <math>p[v]</math> do <math>v</math>, to otrzymamy drzewo | + | W dalszym ciągu tego wykładu będziemy rozważali tylko grafy spójne. Niech <math>G=(V,E)</math> będzie grafem spójnym, a <math>s</math> wyróżnionym wierzchołkiem w <math>G</math>. Zanalizujmy raz jeszcze wykonanie procedury <math>Search(s)</math> dla grafu <math>G</math>. Dla każdego wierzchołka <math>v \ne s</math> niech <math>p[v]</math> będzie wierzchołkiem z którego wierzchołek <math>v</math> zostaje odwiedzony, tzn. <math>p[v]</math> zostaje zamarkowany jako odwiedzony w wierszu 16, gdy zostaje odkryty na liście sąsiedztwa <math>v</math>. Nietrudno zauważyć, że graf <math>(V,\{v-p[v]: v \in V-\{s\}\})</math> jest drzewem rozpinającym grafu <math>G</math>. Jeśli każdą krawędź tego drzewa zorientujemy od <math>p[v]</math> do <math>v</math>, to otrzymamy drzewo o korzeniu <math>s</math>, w którym krawędzie są skierowane od korzenia w kierunku liści. Takie drzewo będziemy nazywali drzewem przeszukiwania grafu. Zauważmy, że wskaźniki <math>p</math> wyznaczają dla każdego wierzchołka <math>v</math> jedyną ścieżkę w drzewie łączącą <math>v</math> z korzeniem <math>s</math>. |
Dotychczas nic nie mówiliśmy o implementacji zbioru <math>S</math>. Rozważymy dwie naturalne implementacje. W pierwszej z nich zbiór <math>S</math> jest kolejką typu FIFO. W tej implementacji wynikiem funkcji <math>Get(S)</math> jest ten wierzchołek ze zbioru <math>S</math>, który przebywa w nim najdłużej. W drugiej implementacji zbiór <math>S</math> jest stosem, a wynikiem <math>Get(S)</math> jest wierzchołek przebywający w <math>S</math> najkrócej, czyli ostatnio wstawiony. Zarówno kolejkę, jak i stos łatwo zaimplementować w taki sposób, żeby każda z wymaganych przez nas operacji na zbiorze <math>S</math> była wykonywana w czasie stałym. Do tego celu można użyć struktury listowej. | Dotychczas nic nie mówiliśmy o implementacji zbioru <math>S</math>. Rozważymy dwie naturalne implementacje. W pierwszej z nich zbiór <math>S</math> jest kolejką typu FIFO. W tej implementacji wynikiem funkcji <math>Get(S)</math> jest ten wierzchołek ze zbioru <math>S</math>, który przebywa w nim najdłużej. W drugiej implementacji zbiór <math>S</math> jest stosem, a wynikiem <math>Get(S)</math> jest wierzchołek przebywający w <math>S</math> najkrócej, czyli ostatnio wstawiony. Zarówno kolejkę, jak i stos łatwo zaimplementować w taki sposób, żeby każda z wymaganych przez nas operacji na zbiorze <math>S</math> była wykonywana w czasie stałym. Do tego celu można użyć struktury listowej. | ||
Linia 76: | Linia 75: | ||
==Przeszukiwanie wszerz== | ==Przeszukiwanie wszerz== | ||
− | Czym jest drzewo przeszukiwania, gdy do implementacji zbioru <math>S</math> użyjemy kolejki? Zauważmy, że do zbioru <math>S</math> wierzchołki są wstawiane w następującej kolejności. Najpierw pojawia się w <math>S</math> wyróżniony wierzchołek <math>s</math>. Wierzchołek <math>s</math> zostanie usunięty (z początku) kolejki dopiero po przejrzeniu jego całej listy sąsiedztwa i wrzuceniu każdego napotkanego na niej wierzchołka na koniec kolejki. Następnie dla każdego sąsiada <math>s</math> przeglądana jest jego lista sąsiedztwa i każdy wierzchołek dotychczas jeszcze nieodwiedzony zostaje zamarkowany jako odwiedzony i umieszczony na końcu kolejki. W ten sposób po sąsiadach wierzchołka <math>s</math> w kolejce pojawią się wszyscy sąsiedzi sąsiadów <math>s</math>, którzy nie sąsiadują bezpośrednio z <math>s</math>. W dalszej kolejności w zbiorze <math>S</math> pojawią się sąsiedzi sąsiadów sąsiadów <math>s</math>, sąsiedzi sąsiadów sąsiadów sąsiadów <math>s</math>, itd. Podzielmy zbiór wierzchołków grafu na warstwy <math>W_0, W_1, W_2, \ldots</math>. Warstwa <math>W_0</math> składa się tylko z wierzchołka <math>s</math>. Warstwa <math>W_1</math> to sąsiedzi <math>s</math>. Warstwa <math>W_2</math> to te wierzchołki grafu, które sąsiadują z co najmniej jednym wierzchołkiem z warstwy <math>W_1</math> i nie należą do żadnej z warstw poprzednich, czyli <math>W_0</math> i <math>W_1</math>. Do warstwy <math>W_3</math> należą wierzchołki sąsiadujące z co najmniej jednym wierzchołkiem z warstwy poprzedniej (<math>W_2</math>) i nie należą do warstw o numerach mniejszych od 3, itd. Nietrudno zauważyć, że <math>i</math>-ta warstwa składa się dokładnie z tych wierzchołków, których odległość (długość najkrótszej ścieżki) od <math>s</math> w grafie <math>G</math> wynosi dokładnie <math>i</math>. Dla każdego wierzchołka <math>v</math> wskaźniki <math>p[v], p[p[v]],\ldots </math> wyznaczają najkrótszą ścieżkę łączącą <math>v</math> z <math>s</math>. Kolejność w jakiej przeszukiwane są wierzchołki grafu w tym przypadku usprawiedliwia nazwę tego sposobu przeszukiwania jako przeszukiwania wszerz (ang. ''Breadth First Search'', w skrócie BFS). Przemieszczamy się po grafie całą jego szerokością, warstwa po warstwie. Z naszych rozważań wynika, | + | Czym jest drzewo przeszukiwania, gdy do implementacji zbioru <math>S</math> użyjemy kolejki? Zauważmy, że do zbioru <math>S</math> wierzchołki są wstawiane w następującej kolejności. Najpierw pojawia się w <math>S</math> wyróżniony wierzchołek <math>s</math>. Wierzchołek <math>s</math> zostanie usunięty (z początku) kolejki dopiero po przejrzeniu jego całej listy sąsiedztwa i wrzuceniu każdego napotkanego na niej wierzchołka na koniec kolejki. Następnie dla każdego sąsiada <math>s</math> przeglądana jest jego lista sąsiedztwa i każdy wierzchołek dotychczas jeszcze nieodwiedzony zostaje zamarkowany jako odwiedzony i umieszczony na końcu kolejki. W ten sposób po sąsiadach wierzchołka <math>s</math> w kolejce pojawią się wszyscy sąsiedzi sąsiadów <math>s</math>, którzy nie sąsiadują bezpośrednio z <math>s</math>. W dalszej kolejności w zbiorze <math>S</math> pojawią się sąsiedzi sąsiadów sąsiadów <math>s</math>, sąsiedzi sąsiadów sąsiadów sąsiadów <math>s</math>, itd. Podzielmy zbiór wierzchołków grafu na warstwy <math>W_0, W_1, W_2, \ldots</math>. Warstwa <math>W_0</math> składa się tylko z wierzchołka <math>s</math>. Warstwa <math>W_1</math> to sąsiedzi <math>s</math>. Warstwa <math>W_2</math> to te wierzchołki grafu, które sąsiadują z co najmniej jednym wierzchołkiem z warstwy <math>W_1</math> i nie należą do żadnej z warstw poprzednich, czyli <math>W_0</math> i <math>W_1</math>. Do warstwy <math>W_3</math> należą wierzchołki sąsiadujące z co najmniej jednym wierzchołkiem z warstwy poprzedniej (<math>W_2</math>) i nie należą do warstw o numerach mniejszych od 3, itd. Nietrudno zauważyć, że <math>i</math>-ta warstwa składa się dokładnie z tych wierzchołków, których odległość (długość najkrótszej ścieżki) od <math>s</math> w grafie <math>G</math> wynosi dokładnie <math>i</math>. Dla każdego wierzchołka <math>v</math> wskaźniki <math>p[v], p[p[v]],\ldots </math> wyznaczają najkrótszą ścieżkę łączącą <math>v</math> z <math>s</math>. Kolejność w jakiej przeszukiwane są wierzchołki grafu w tym przypadku usprawiedliwia nazwę tego sposobu przeszukiwania jako przeszukiwania wszerz (ang. ''Breadth First Search'', w skrócie BFS). Przemieszczamy się po grafie całą jego szerokością, warstwa po warstwie. Z naszych rozważań wynika, że drzewo przeszukiwania wszerz jest drzewem najkrótszych ścieżek łączących wierzchołki grafu z wyróżnionym wierzchołkiem <math>s</math>. Wynika stąd, że najkrótsze ścieżki łączące wszystkie wierzchołki grafu z jednym wyróżnionym wierzchołkiem można wyznaczyć w czasie liniowym, o ile tylko wagi krawędzi są jednostkowe. |
==Przeszukiwanie w głąb== | ==Przeszukiwanie w głąb== | ||
Linia 109: | Linia 108: | ||
Wierzchołek <math>u</math> jest przodkiem wierzchołka <math>v</math> w drzewie przeszukiwania w głąb wtedy i tylko wtedy, gdy <math>nr[u] < nr[v] < nr[u] + d[u]</math>, gdzie <math>d[u]</math> jest liczbą wierzchołków w poddrzewie o korzeniu w <math>u</math>. | Wierzchołek <math>u</math> jest przodkiem wierzchołka <math>v</math> w drzewie przeszukiwania w głąb wtedy i tylko wtedy, gdy <math>nr[u] < nr[v] < nr[u] + d[u]</math>, gdzie <math>d[u]</math> jest liczbą wierzchołków w poddrzewie o korzeniu w <math>u</math>. | ||
− | Pokażemy teraz, w jaki sposób zastosować przeszukiwanie w głąb do wyznaczanie wszystkich mostów grafie. Przypomnijmy, że mostem w spójnym grafie <math>G</math> nazywamy każdą krawędź, której usunięcie rozspójnia ten graf. Zauważmy, że bardzo łatwo wyznaczyć wszystkie mosty w czasie <math>O(m(n+m))</math>. Wystarczy dla każdej krawędzi sprawdzić w czasie liniowym, czy usunięcie tej krawędzi zwiększa liczbę spójnych składowych w grafie. Przeszukiwanie w głąb pozwala rozwiązać ten problem w czasie liniowym. Zanim przedstawimy stosowny algorytm spróbujmy scharakteryzować mosty z wykorzystaniem drzewa przeszukiwania w głąb i numeracji w głąb. | + | Pokażemy teraz, w jaki sposób zastosować przeszukiwanie w głąb do wyznaczanie wszystkich mostów grafie. Przypomnijmy, że mostem w spójnym grafie <math>G</math> nazywamy każdą krawędź, której usunięcie rozspójnia ten graf. Zauważmy, że bardzo łatwo wyznaczyć wszystkie mosty w czasie <math>O(m(n+m))</math>. Wystarczy dla każdej krawędzi sprawdzić w czasie liniowym, czy usunięcie tej krawędzi zwiększa liczbę spójnych składowych w grafie. Przeszukiwanie w głąb pozwala rozwiązać ten problem w czasie liniowym. Zanim przedstawimy stosowny algorytm, spróbujmy scharakteryzować mosty z wykorzystaniem drzewa przeszukiwania w głąb i numeracji w głąb. |
'''Spostrzeżenie 1''' | '''Spostrzeżenie 1''' |
Wersja z 13:41, 25 wrz 2006
Z poprzedniego wykładu wiemy, że gdy celem jest ułożenie algorytmu o złożoności liniowej ze względu na rozmiar grafu (czyli sumę liczb wierzchołków i krawędzi), to graf powinien być reprezentowany przez listy sąsiedztw. Na tym wykładzie przyjmiemy taką właśnie reprezentację grafu wejściowego. Rozwiązanie każdego problemu grafowego, które zależy od poznania struktury połączeń w całym grafie, wymaga przeszukania jego wierzchołków i krawędzi. Takie przeszukanie polega na odwiedzeniu każdego wierzchołka i zbadaniu krawędzi opuszczających już odwiedzone wierzchołki. Jeśli okazuje się, że drugi koniec badanej krawędzi nie był jeszcze odwiedzony, dołącza się go do zbioru wierzchołków odwiedzonych. Przeszukiwania dokonujemy z wykorzystaniem dynamicznego zbioru
, w którym przechowujemy odwiedzane wierzchołki i których sąsiedztwa nie są jeszcze do końca zbadane. Zakładamy, że na zbiorze wykonywane są następujące operacje:: wstaw do zbioru wierzchołek ;
: funkcja, której wynikiem jest (dowolny) wierzchołek z , wywoływana tylko wtedy, gdy ;
: usuń z wierzchołek (zazwyczaj będzie to wierzchołek .
Informacje o tym, które wierzchołki zostały już odwiedzone, będziemy przechowywać w tablicy logicznej
. Wartością jest PRAWDA (TRUE) wtedy i tylko wtedy, gdy wierzchołek został już odwiedzony. Do przechodzenia list sąsiedztw posłuży nam tablica . Wartością jest pierwszy wierzchołek na liście - sąsiad - który nie był jeszcze oglądany od strony , co oznacza że krawędź opuszczająca w kierunku tego sąsiada nie została jeszcze zbadana.Inicjacja każdego przeszukiwania wygląda następująco:
1 for eachdo 2 begin 3 := pierwszy wierzchołek na liście sąsiedztwa ; 4 FALSE; //markujemy, że wierzchołek nie był jeszcze odwiedzony 5 end;
Załóżmy, że przeszukiwanie grafu rozpoczynamy z wybranego wierzchołka
. Wówczas schemat przeszukiwania z można zapisać następująco.Algorytm Przeszukiwanie grafu - algorytm generyczny
1 Search(s); 2 begin 3; 4 TRUE; //markujemy jako odwiedzony 5 while do 6 begin 7 ; 8 if NIL then 9 //jeśli nie wyczerpaliśmy całej listy sąsiadów wierzchołka 10 begin 11 //pobierz kolejny wierzchołek z listy sąsiadów i przejdź do następnego 12 // wierzchołka na liście 13 ; ; 14 if NOT then //jeśli nie był jeszcze odwiedzony 15 begin 16 TRUE;// markujemy jako odwiedzony 17 //wstaw do zbioru 18 end 19 end 20 else //wszystkie krawędzie opuszczające zostały już zbadane 21 22 end 23 end
Powyższy schemat może być użyty do rozwiązania jednego z najbardziej podstawowych problemów grafowych - problemu spójności. Problem ten formułuje się następująco.
Dane
Graf G=(V,E) (zadany przez listy sąsiedztw)
Wynik
Tablica
o wartościach w zbiorze wierzchołków taka, że wtedy i tylko wtedy, gdy i należą do tej samej spójnej składowej.Zauważmy, że problem spójności bardzo łatwo rozwiązać przy użyciu procedury
. Jeśli przeszukiwanie rozpoczynamy od nieodwiedzonego wierzchołka , to w wyniku wykonania zostaną odwiedzone wszystkie wierzchołki w grafie osiągalne z . Innymi słowy odwiedzona zostanie cała spójna składowa zawierająca . Jeśli dla każdego nowo odwiedzanego wierzchołka wykonamy przypisanie , to po zakończeniu przeszukiwania spójnej składowej zawierającej , wartość każdego wierzchołka w tej składowej będzie właśnie równa . Jedyne miejsce, w którym musimy zmienić procedurę jest wiersz 16. Nowy wiersz 16 powinien mieć postać:16TRUE; ;
Tak zmodyfikowaną procedurę
nazwiemy z angielskiego Search Connected Components (czyli wyszukaj spójne składowe). Oto algorytm obliczania spójnych składowych z wykorzystaniem procedury .1 Inicjacja przeszukiwania grafu. 2 for eachdo 3 if NOT then 4 //odkryta została nowa spójna składowa zawierająca wierzchołek 5
Dokonamy teraz analizy złożoności algorytmu obliczania spójnych składowych. Zakładamy przy tym, że każda z operacji na pomocniczym zbiorze
jest wykonywana w czasie stałym. Zauważmy, że każdy wierzchołek jest dokładnie raz wstawiany do zbioru - w momencie odkrycia go jako nieodwiedzonego - i dokładnie raz jest usuwany ze zbioru - po zbadaniu całego jego sąsiedztwa. Sąsiadów wierzchołka przeglądamy przechodząc po jego liście sąsiedztwa, a dla każdego elementu listy obliczenia z nim związane zajmują stały czas – jeśli wierzchołek był już odwiedzony, to nic nie robimy, natomiast, jeśli nie był odwiedzony, to markujemy go jako odwiedzonego i wstawiamy do zbioru . Tak więc obliczenia związane z przeglądaniem sąsiedztw wierzchołków zajmują czas proporcjonalny do sumy długości list sąsiedztw, która to suma wynosi . Z naszej analizy wynika, że problem spójnych składowych można rozwiązać w czasie , czyli liniowym ze względu na rozmiar grafu.W dalszym ciągu tego wykładu będziemy rozważali tylko grafy spójne. Niech
będzie grafem spójnym, a wyróżnionym wierzchołkiem w . Zanalizujmy raz jeszcze wykonanie procedury dla grafu . Dla każdego wierzchołka niech będzie wierzchołkiem z którego wierzchołek zostaje odwiedzony, tzn. zostaje zamarkowany jako odwiedzony w wierszu 16, gdy zostaje odkryty na liście sąsiedztwa . Nietrudno zauważyć, że graf jest drzewem rozpinającym grafu . Jeśli każdą krawędź tego drzewa zorientujemy od do , to otrzymamy drzewo o korzeniu , w którym krawędzie są skierowane od korzenia w kierunku liści. Takie drzewo będziemy nazywali drzewem przeszukiwania grafu. Zauważmy, że wskaźniki wyznaczają dla każdego wierzchołka jedyną ścieżkę w drzewie łączącą z korzeniem .Dotychczas nic nie mówiliśmy o implementacji zbioru
. Rozważymy dwie naturalne implementacje. W pierwszej z nich zbiór jest kolejką typu FIFO. W tej implementacji wynikiem funkcji jest ten wierzchołek ze zbioru , który przebywa w nim najdłużej. W drugiej implementacji zbiór jest stosem, a wynikiem jest wierzchołek przebywający w najkrócej, czyli ostatnio wstawiony. Zarówno kolejkę, jak i stos łatwo zaimplementować w taki sposób, żeby każda z wymaganych przez nas operacji na zbiorze była wykonywana w czasie stałym. Do tego celu można użyć struktury listowej.Przeszukiwanie wszerz
Czym jest drzewo przeszukiwania, gdy do implementacji zbioru
użyjemy kolejki? Zauważmy, że do zbioru wierzchołki są wstawiane w następującej kolejności. Najpierw pojawia się w wyróżniony wierzchołek . Wierzchołek zostanie usunięty (z początku) kolejki dopiero po przejrzeniu jego całej listy sąsiedztwa i wrzuceniu każdego napotkanego na niej wierzchołka na koniec kolejki. Następnie dla każdego sąsiada przeglądana jest jego lista sąsiedztwa i każdy wierzchołek dotychczas jeszcze nieodwiedzony zostaje zamarkowany jako odwiedzony i umieszczony na końcu kolejki. W ten sposób po sąsiadach wierzchołka w kolejce pojawią się wszyscy sąsiedzi sąsiadów , którzy nie sąsiadują bezpośrednio z . W dalszej kolejności w zbiorze pojawią się sąsiedzi sąsiadów sąsiadów , sąsiedzi sąsiadów sąsiadów sąsiadów , itd. Podzielmy zbiór wierzchołków grafu na warstwy . Warstwa składa się tylko z wierzchołka . Warstwa to sąsiedzi . Warstwa to te wierzchołki grafu, które sąsiadują z co najmniej jednym wierzchołkiem z warstwy i nie należą do żadnej z warstw poprzednich, czyli i . Do warstwy należą wierzchołki sąsiadujące z co najmniej jednym wierzchołkiem z warstwy poprzedniej ( ) i nie należą do warstw o numerach mniejszych od 3, itd. Nietrudno zauważyć, że -ta warstwa składa się dokładnie z tych wierzchołków, których odległość (długość najkrótszej ścieżki) od w grafie wynosi dokładnie . Dla każdego wierzchołka wskaźniki wyznaczają najkrótszą ścieżkę łączącą z . Kolejność w jakiej przeszukiwane są wierzchołki grafu w tym przypadku usprawiedliwia nazwę tego sposobu przeszukiwania jako przeszukiwania wszerz (ang. Breadth First Search, w skrócie BFS). Przemieszczamy się po grafie całą jego szerokością, warstwa po warstwie. Z naszych rozważań wynika, że drzewo przeszukiwania wszerz jest drzewem najkrótszych ścieżek łączących wierzchołki grafu z wyróżnionym wierzchołkiem . Wynika stąd, że najkrótsze ścieżki łączące wszystkie wierzchołki grafu z jednym wyróżnionym wierzchołkiem można wyznaczyć w czasie liniowym, o ile tylko wagi krawędzi są jednostkowe.Przeszukiwanie w głąb
Z przeszukiwaniem w głąb (ang. Depth First Search, w skrócie DFS) mamy do czynienia, gdy do implementacji zbioru
używamy stosu. Określenie przeszukiwanie w głąb bierze się stąd, że zawsze próbujemy kontynuować przeszukiwanie z najpóźniej odkrytego wierzchołka, czyli z tego, który znajduje się na szczycie stosu. Okazuje się, że przeszukując graf w głąb możemy zebrać niezmiernie przydatne informacje o strukturze grafu, które mogą być wykorzystane w konstruowaniu efektywnych algorytmów dla bardzo wielu problemów grafowych. Przeszukiwanie w głąb dużo wygodniej opisać rekurencyjnie. Rozważmy poniższą procedurę , w której dodatkowo numerujemy wierzchołki w kolejności odwiedzania. W tym celu użyjemy globalnej zmiennej ’’ost_nr’’, której wartością jest ostatnio nadany numer. Zmienną ’’ost_nr’’ inicjujemy na 0. Otrzymaną numerację będziemy nazywali numeracją w głąb.Algorytm Przeszukiwanie w głąb
1; 2 // jest nowo odkrytym wierzchołkiem 3 begin 4 //markujemy jako odwiedzony 5 TRUE; 6 //wierzchołkowi nadajemy kolejny numer 7 ; ; 8 //przeglądamy listę sąsiedztwa i dla każdego nowo odkrytego 9 //wierzchołka wywołujemy procedurę 10 for each do 11 if NOT then 12 13 end
Jeśli chcemy przeszukać graf poczynając od wierzchołka
, to wystarczy wywołać , inicjując wcześniej tablice i , oraz zmienną . Zauważmy, że krawędź zaliczamy do drzewa przeszukiwania (w głąb), jeśli po wejściu do wierzchołka odkrywamy, że wierzchołek na liście sąsiedztwa nie został jeszcze odwiedzony. Krawędzie, które nie należą do drzewa przeszukiwania mają jedną bardzo ważną własność.Krawędź niedrzewowa łączy zawsze potomka z przodkiem w drzewie przeszukiwania w głąb.
Dowód powyższej własności jest niezwykle prosty. Załóżmy nie wprost, że istnieje niedrzewowa krawędź
taka, że wierzchołki , nie są w relacji potomek-przodek. Bez straty ogólności możemy przyjąć, ze zostaje odwiedzony wcześniej niż . Załóżmy, że zostaje odwiedzony w wyniku wywołania przeszukiwania podczas przeglądania listy sąsiedztwa . Wówczas jednak znajdzie się w poddrzewie przeszukiwania o korzeniu w . Ponieważ jest na liście , to do takiego wywołania dojść musi – sprzeczność z założeniem, że , nie są w relacji potomek-przodek.Numeracja w głąb pozwala łatwo sprawdzić, czy dwa różne wierzchołki
, są w relacji przodek-potomek w drzewie przeszukiwania w głąb. Załóżmy, że .Wierzchołek
jest przodkiem wierzchołka w drzewie przeszukiwania w głąb wtedy i tylko wtedy, gdy , gdzie jest liczbą wierzchołków w poddrzewie o korzeniu w .Pokażemy teraz, w jaki sposób zastosować przeszukiwanie w głąb do wyznaczanie wszystkich mostów grafie. Przypomnijmy, że mostem w spójnym grafie
nazywamy każdą krawędź, której usunięcie rozspójnia ten graf. Zauważmy, że bardzo łatwo wyznaczyć wszystkie mosty w czasie . Wystarczy dla każdej krawędzi sprawdzić w czasie liniowym, czy usunięcie tej krawędzi zwiększa liczbę spójnych składowych w grafie. Przeszukiwanie w głąb pozwala rozwiązać ten problem w czasie liniowym. Zanim przedstawimy stosowny algorytm, spróbujmy scharakteryzować mosty z wykorzystaniem drzewa przeszukiwania w głąb i numeracji w głąb.Spostrzeżenie 1
Jeśli krawędź jest mostem w grafie, to jest krawędzią każdego drzewa rozpinającego tego grafu.
Rozważmy drzewo przeszukiwania w głąb i niech
będzie krawędzią w tym drzewie. Załóżmy także, że jest ojcem .Spostrzeżenie 2
Krawędź
jest mostem wtedy i tylko wtedy, gdy żadna krawędź niedrzewowa nie łączy wierzchołka z poddrzewa o korzeniu w z właściwym przodkiem . Innymi słowy wtedy i tylko wtedy, gdy oba końce każdej krawędzi niedrzewowej leżą w poddrzewie o korzeniu w , jeśli tylko jeden z tych końców jest w tym poddrzewie.Spróbujemy warunek ze spostrzeżenia 2 wyrazić trochę inaczej. Dla każdego wierzchołka
niech będzie najmniejszym numerem w głąb wierzchołka, który można osiągnąć z ścieżką składającą z krawędzi drzewowych z poddrzewa o korzeniu w i zakończoną co najwyżej jedną krawędzią niedrzewową prowadzącą poza to poddrzewo. Funkcję można rekurencyjnie zdefiniować w następujący sposób:
Spostrzeżenie 3
Niech
będzie krawędzią drzewa przeszukiwania w głąb taką, że jest ojcem w tym drzewie. Wówczas krawędź jest mostem wtedy i tylko wtedy, gdy .
Powyższe spostrzeżenia pozwalają już na zaproponowanie liniowego algorytmu wyznaczania mostów w spójnym grafie . Algorytm ten zapiszemy za pomocą rekurencyjnej procedury .
Algorytm Mosty
1; 2 // jest nowo odkrytym wierzchołkiem 3 begin 4 //markujemy jako odwiedzony 5 TRUE; 6 //wierzchołkowi nadajemy kolejny numer 7 ; ; 8 //inicjacja 9 ; 10 //przeglądamy listę sąsiedztwa i dla każdego nowo odkrytego 11 //wierzchołka wywołujemy procedurę ; dokonujemy też 12 //aktualizacji 13 for each do 14 if NOT then 15 begin 16 ; 17 ; 18 if then 19 krawędź jest mostem; 20 end 21 else 22 23 end
Żeby wyznaczyć wszystkie mosty wystarczy wywołać
dla dowolnego wierzchołka . Złożoność wyznaczania mostów jest asymptotycznie taka sama jak zwykłego przeszukiwania grafu, czyli .