Algorytmy i struktury danych/Przeszukiwanie grafów: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Walen (dyskusja | edycje)
Nie podano opisu zmian
m Zastępowanie tekstu – „ </math>” na „</math>”
 
(Nie pokazano 2 wersji utworzonych przez 2 użytkowników)
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), 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>;
Linia 14: Linia 14:
  2 '''begin'''
  2 '''begin'''
  3  <math>current[v]</math> := pierwszy wierzchołek na liście sąsiedztwa <math>L[v]</math>;
  3  <math>current[v]</math> := pierwszy wierzchołek na liście sąsiedztwa <math>L[v]</math>;
  4  <math>visited[v] := </math> FALSE; //markujemy, że wierzchołek <math>v</math> nie był jeszcze odwiedzony
  4  <math>visited[v] :=</math> FALSE; //markujemy, że wierzchołek <math>v</math> nie był jeszcze odwiedzony
  5 '''end''';
  5 '''end''';


Linia 22: Linia 22:
  2  '''begin'''
  2  '''begin'''
  3    <math>S := \{s\}</math>;
  3    <math>S := \{s\}</math>;
  4    <math>visited[s] := </math>TRUE; //markujemy <math>s</math> jako odwiedzony
  4    <math>visited[s] :=</math>TRUE; //markujemy <math>s</math> jako odwiedzony
  5    '''while''' <math>S \ne \emptyset</math> '''do'''
  5    '''while''' <math>S \ne \emptyset</math> '''do'''
  6    '''begin'''
  6    '''begin'''
Linia 34: Linia 34:
  14      '''if''' NOT <math>visisted[v]</math> '''then''' //jeśli <math>v</math> nie był jeszcze odwiedzony
  14      '''if''' NOT <math>visisted[v]</math> '''then''' //jeśli <math>v</math> nie był jeszcze odwiedzony
  15  '''begin'''
  15  '''begin'''
  16    <math>visited[v] := </math> TRUE;// markujemy <math>v</math> jako odwiedzony
  16    <math>visited[v] :=</math> TRUE;// markujemy <math>v</math> jako odwiedzony
  17    <math>Insert(S,v)</math> //wstaw <math>v</math> do zbioru <math>S</math>
  17    <math>Insert(S,v)</math> //wstaw <math>v</math> do zbioru <math>S</math>
  18      '''end'''
  18      '''end'''
Linia 54: Linia 54:
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 zmieniać 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>, 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 zmieniać procedurę <math>Search</math> jest wiersz 16. Nowy wiersz 16 powinien mieć postać:
   
   
  16 <math>visited[v] := </math>TRUE; <math>c[v] := s</math>;
  16 <math>visited[v] :=</math>TRUE; <math>c[v] := s</math>;


Tak zmodyfikowaną procedurę <math>Search</math> nazwiemy <math>SearchCC</math> z angielskiego ''Search Connected Components'' (czyli ''wyszukaj spójne składowe'').
Tak zmodyfikowaną procedurę <math>Search</math> nazwiemy <math>SearchCC</math> z angielskiego ''Search Connected Components'' (czyli ''wyszukaj spójne składowe'').
Linia 67: 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 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.
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, nic nie robimy, natomiast jeśli nie był odwiedzony, markujemy go jako odwiedzonego i wstawiamy do zbioru <math>S</math>. Obliczenia związane z przeglądaniem sąsiedztw wierzchołków zajmują więc 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 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>.
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>, 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. Kolejkę 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. W tym celu można użyć struktury listowej.


==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ę ze wszystkich 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 długości (wagi) krawędzi są jednostkowe.
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ę ze wszystkich 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 długości (wagi) krawędzi są jednostkowe.


==Przeszukiwanie w głąb==       
==Przeszukiwanie w głąb==       


Z przeszukiwaniem w głąb (ang. ''Depth First Search'', w skrócie DFS) mamy do czynienia, gdy do implementacji zbioru <math>S</math> 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ę <math>DFS(v)</math>, w której dodatkowo numerujemy wierzchołki w kolejności odwiedzania. W tym celu użyjemy globalnej zmiennej <math>ost\-{nr}</math>, której wartością jest ostatnio nadany numer. Zmienną <math>ost\-{nr}</math> inicjujemy na 0. Otrzymaną numerację będziemy nazywali ''numeracją w głąb''.  
Z przeszukiwaniem w głąb (ang. ''Depth First Search'', w skrócie DFS) mamy do czynienia, gdy do implementacji zbioru <math>S</math> 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ę <math>DFS(v)</math>, w której dodatkowo numerujemy wierzchołki w kolejności odwiedzania. W tym celu użyjemy globalnej zmiennej <math>ost{nr}</math>, której wartością jest ostatnio nadany numer. Zmienną <math>ost{nr}</math> inicjujemy na 0. Otrzymaną numerację będziemy nazywali ''numeracją w głąb''.  


{{algorytm|Przeszukiwanie w głąb||
{{algorytm|Przeszukiwanie w głąb||
Linia 86: Linia 86:
  3  '''begin'''
  3  '''begin'''
  4    //markujemy <math>v</math> jako odwiedzony
  4    //markujemy <math>v</math> jako odwiedzony
  5    <math>visited[v] := </math>TRUE;
  5    <math>visited[v] :=</math>TRUE;
  6    //wierzchołkowi <math>v</math> nadajemy kolejny numer
  6    //wierzchołkowi <math>v</math> nadajemy kolejny numer
  7    <math>ost\-{nr} := ost\-{nr} + 1</math>; <math>nr[v] := ost\-{nr}</math>;  
  7    <math>ost{nr} := ost{nr} + 1</math>; <math>nr[v] := ost{nr}</math>;  
  8    //przeglądamy listę sąsiedztwa <math>v</math> i dla każdego nowo odkrytego  
  8    //przeglądamy listę sąsiedztwa <math>v</math> i dla każdego nowo odkrytego  
  9    //wierzchołka wywołujemy procedurę <math>DFS</math>
  9    //wierzchołka wywołujemy procedurę <math>DFS</math>
Linia 97: Linia 97:
}}
}}


Jeśli chcemy przeszukać graf poczynając od wierzchołka <math>s</math>, to wystarczy wywołać <math>DFS(s)</math>, inicjując wcześniej tablice <math>visited</math> i <math>current</math>, oraz zmienną <math>ost\-{nr}</math>.
Jeśli chcemy przeszukać graf poczynając od wierzchołka <math>s</math>, wystarczy wywołać <math>DFS(s)</math>, inicjując wcześniej tablice <math>visited</math> i <math>current</math> oraz zmienną <math>ost{nr}</math>.
Zauważmy, że krawędź <math>v--u</math> zaliczamy do drzewa przeszukiwania (w głąb), jeśli po wejściu do wierzchołka <math>v</math> odkrywamy, że wierzchołek <math>u</math> na liście sąsiedztwa <math>v</math> nie został jeszcze odwiedzony. Krawędzie, które nie należą do drzewa przeszukiwania mają jedną bardzo ważną własność.
Zauważmy, że krawędź <math>v--u</math> zaliczamy do drzewa przeszukiwania (w głąb), jeśli po wejściu do wierzchołka <math>v</math> odkrywamy, że wierzchołek <math>u</math> na liście sąsiedztwa <math>v</math> 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.
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ź <math>v-u</math> taka, że wierzchołki <math>v</math>, <math>u</math> nie są w relacji potomek-przodek. Bez straty ogólności możemy przyjąć, ze <math>v</math> zostaje odwiedzony wcześniej niż <math>u</math>. Załóżmy, że <math>u</math> zostaje odwiedzony w wyniku wywołania przeszukiwania <math>DFS</math> podczas przeglądania listy sąsiedztwa <math>v</math>. Wówczas jednak <math>u</math> znajdzie się w poddrzewie przeszukiwania o korzeniu w <math>v</math>. Ponieważ <math>u</math> jest na liście <math>v</math>, to do takiego wywołania dojść musi – sprzeczność z założeniem, że <math>u</math>, <math>v</math> nie są w relacji potomek-przodek.  
Dowód powyższej własności jest niezwykle prosty. Załóżmy nie wprost, że istnieje niedrzewowa krawędź <math>v-u</math> taka, że wierzchołki <math>v</math>, <math>u</math> nie są w relacji potomek-przodek. Bez straty ogólności możemy przyjąć, że <math>v</math> zostaje odwiedzony wcześniej niż <math>u</math>. Załóżmy, że <math>u</math> zostaje odwiedzony w wyniku wywołania przeszukiwania <math>DFS</math> podczas przeglądania listy sąsiedztwa <math>v</math>. Wówczas jednak <math>u</math> znajdzie się w poddrzewie przeszukiwania o korzeniu w <math>v</math>. Ponieważ <math>u</math> jest na liście <math>v</math>, do takiego wywołania dojść musi – sprzeczność z założeniem, że <math>u</math>, <math>v</math> nie są w relacji potomek-przodek.  


Numeracja w głąb pozwala łatwo sprawdzić, czy dwa różne wierzchołki <math>u</math>, <math>v</math> są w relacji przodek-potomek w drzewie przeszukiwania w głąb. Załóżmy, że <math>nr[u] < nr[v]</math>.
Numeracja w głąb pozwala łatwo sprawdzić, czy dwa różne wierzchołki <math>u</math>, <math>v</math> są w relacji przodek-potomek w drzewie przeszukiwania w głąb. Załóżmy, że <math>nr[u] < nr[v]</math>.
Linia 108: 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] \le 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] \le 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 wyznaczenia 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'''


Jeśli krawędź jest mostem w grafie, to jest krawędzią każdego drzewa rozpinającego tego grafu.
Jeśli krawędź jest mostem w grafie, jest krawędzią każdego drzewa rozpinającego tego grafu.


Rozważmy drzewo przeszukiwania w głąb i niech <math>u-v</math> będzie krawędzią w tym drzewie. Załóżmy także, że <math>u</math> jest ojcem <math>v</math>.
Rozważmy drzewo przeszukiwania w głąb i niech <math>u-v</math> będzie krawędzią w tym drzewie. Załóżmy także, że <math>u</math> jest ojcem <math>v</math>.
Linia 137: Linia 137:
  3  '''begin'''
  3  '''begin'''
  4    //markujemy <math>v</math> jako odwiedzony
  4    //markujemy <math>v</math> jako odwiedzony
  5    <math>visited[v] := </math>TRUE;
  5    <math>visited[v] :=</math>TRUE;
  6    //wierzchołkowi <math>v</math> nadajemy kolejny numer
  6    //wierzchołkowi <math>v</math> nadajemy kolejny numer
  7    <math>ost\-nr := ost\-nr + 1</math>; <math>nr[v] := ost\-nr</math>;
  7    <math>ostnr := ostnr + 1</math>; <math>nr[v] := ostnr</math>;
  8    //inicjacja <math>low[v]</math>  
  8    //inicjacja <math>low[v]</math>  
  9    <math>low[v] := ost\-nr</math>;
  9    <math>low[v] := ostnr</math>;
  10  //przeglądamy listę sąsiedztwa <math>v</math> i dla każdego nowo odkrytego  
  10  //przeglądamy listę sąsiedztwa <math>v</math> i dla każdego nowo odkrytego  
  11  //wierzchołka wywołujemy procedurę <math>DFS</math>; dokonujemy też  
  11  //wierzchołka wywołujemy procedurę <math>DFS</math>; dokonujemy też  

Aktualna wersja na dzień 10:02, 5 wrz 2023

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), 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 S, w którym przechowujemy odwiedzane wierzchołki i których sąsiedztwa nie są jeszcze do końca zbadane. Zakładamy, że na zbiorze S wykonywane są następujące operacje:

Insert(S,v): wstaw do zbioru S wierzchołek v;

Get(S): funkcja, której wynikiem jest (dowolny) wierzchołek z S, wywoływana tylko wtedy, gdy S;

Delete(S,v): usuń z S wierzchołek v (zazwyczaj będzie to wierzchołek Get(S).

Informacje o tym, które wierzchołki zostały już odwiedzone, będziemy przechowywać w tablicy logicznej visited[1..n]. Wartością visited[v] jest PRAWDA (TRUE) wtedy i tylko wtedy, gdy wierzchołek v został już odwiedzony. Do przechodzenia list sąsiedztw posłuży nam tablica current[1..n]. Wartością current[v] jest pierwszy wierzchołek na liście L[v] - sąsiad v - który nie był jeszcze oglądany od strony v, co oznacza że krawędź opuszczająca v w kierunku tego sąsiada nie została jeszcze zbadana.

Inicjacja każdego przeszukiwania wygląda następująco:

1 for each vV do
2 begin
3   current[v] := pierwszy wierzchołek na liście sąsiedztwa L[v];
4   visited[v]:= FALSE; //markujemy, że wierzchołek v nie był jeszcze odwiedzony
5 end;

Załóżmy, że przeszukiwanie grafu rozpoczynamy z wybranego wierzchołka s. Wówczas schemat przeszukiwania z s można zapisać następująco.

Algorytm Przeszukiwanie grafu - algorytm generyczny


1  procedure Search(s);
2  begin
3    S:={s};
4    visited[s]:=TRUE; //markujemy s jako odwiedzony
5    while S do
6    begin
7      u:=Get(S);
8      if current[u] NIL then 
9      //jeśli nie wyczerpaliśmy całej listy sąsiadów wierzchołka u
10     begin
11     //pobierz kolejny wierzchołek z listy sąsiadów i przejdź do następnego
12     // wierzchołka na liście
13       v:=current[u]; current[u]:=next(current[u]);
14       if NOT visisted[v] then //jeśli v nie był jeszcze odwiedzony
15   begin
16     visited[v]:= TRUE;// markujemy v jako odwiedzony
17     Insert(S,v) //wstaw v do zbioru S
18       end
19     end
20     else //wszystkie krawędzie opuszczające u zostały już zbadane
21       Delete(S,u)     
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 c[1..n] o wartościach w zbiorze wierzchołków V taka, że c[u]=c[v] wtedy i tylko wtedy, gdy u i v należą do tej samej spójnej składowej.

Zauważmy, że problem spójności bardzo łatwo rozwiązać przy użyciu procedury Search(s). Jeśli przeszukiwanie rozpoczynamy od nieodwiedzonego wierzchołka s, to w wyniku wykonania Search zostaną odwiedzone wszystkie wierzchołki w grafie G osiągalne z s. Innymi słowy odwiedzona zostanie cała spójna składowa zawierająca s. Jeśli dla każdego nowo odwiedzanego wierzchołka v wykonamy przypisanie c[v]:=s, po zakończeniu przeszukiwania spójnej składowej zawierającej s, wartość c każdego wierzchołka w tej składowej będzie właśnie równa s. Jedyne miejsce, w którym musimy zmieniać procedurę Search jest wiersz 16. Nowy wiersz 16 powinien mieć postać:

16 visited[v]:=TRUE; c[v]:=s;

Tak zmodyfikowaną procedurę Search nazwiemy SearchCC z angielskiego Search Connected Components (czyli wyszukaj spójne składowe). Oto algorytm obliczania spójnych składowych z wykorzystaniem procedury SearchCC.

1 Inicjacja przeszukiwania grafu.
2 for each vV do
3   if NOT visited[v] then
4     //odkryta została nowa spójna składowa zawierająca wierzchołek v
5     SearchCC(v);
   

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 S jest wykonywana w czasie stałym. Zauważmy, że każdy wierzchołek jest dokładnie raz wstawiany do zbioru S - w momencie odkrycia go jako nieodwiedzonego - i dokładnie raz jest usuwany ze zbioru S - 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, nic nie robimy, natomiast jeśli nie był odwiedzony, markujemy go jako odwiedzonego i wstawiamy do zbioru S. Obliczenia związane z przeglądaniem sąsiedztw wierzchołków zajmują więc czas proporcjonalny do sumy długości list sąsiedztw, która to suma wynosi 2m. Z naszej analizy wynika, że problem spójnych składowych można rozwiązać w czasie O(n+m), czyli liniowym ze względu na rozmiar grafu.

W dalszym ciągu tego wykładu będziemy rozważali tylko grafy spójne. Niech G=(V,E) będzie grafem spójnym, a s wyróżnionym wierzchołkiem w G. Zanalizujmy raz jeszcze wykonanie procedury Search(s) dla grafu G. Dla każdego wierzchołka vs niech p[v] będzie wierzchołkiem z którego wierzchołek v zostaje odwiedzony, tzn. p[v] zostaje zamarkowany jako odwiedzony w wierszu 16, gdy zostaje odkryty na liście sąsiedztwa v. Nietrudno zauważyć, że graf (V,{vp[v]:vV{s}}) jest drzewem rozpinającym grafu G. Jeśli każdą krawędź tego drzewa zorientujemy od p[v] do v, otrzymamy drzewo o korzeniu s, 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 p wyznaczają dla każdego wierzchołka v jedyną ścieżkę w drzewie łączącą v z korzeniem s.

Dotychczas nic nie mówiliśmy o implementacji zbioru S. Rozważymy dwie naturalne implementacje. W pierwszej z nich zbiór S jest kolejką typu FIFO. W tej implementacji wynikiem funkcji Get(S) jest ten wierzchołek ze zbioru S, który przebywa w nim najdłużej. W drugiej implementacji zbiór S jest stosem, a wynikiem Get(S) jest wierzchołek przebywający w S najkrócej, czyli ostatnio wstawiony. Kolejkę i stos łatwo zaimplementować w taki sposób, żeby każda z wymaganych przez nas operacji na zbiorze S była wykonywana w czasie stałym. W tym celu można użyć struktury listowej.

Przeszukiwanie wszerz

Czym jest drzewo przeszukiwania, gdy do implementacji zbioru S użyjemy kolejki? Zauważmy, że do zbioru S wierzchołki są wstawiane w następującej kolejności. Najpierw pojawia się w S wyróżniony wierzchołek s. Wierzchołek s 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 s 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 s w kolejce pojawią się wszyscy sąsiedzi sąsiadów s, którzy nie sąsiadują bezpośrednio z s. W dalszej kolejności w zbiorze S pojawią się sąsiedzi sąsiadów sąsiadów s, sąsiedzi sąsiadów sąsiadów sąsiadów s, itd. Podzielmy zbiór wierzchołków grafu na warstwy W0,W1,W2,. Warstwa W0 składa się tylko z wierzchołka s. Warstwa W1 to sąsiedzi s. Warstwa W2 to te wierzchołki grafu, które sąsiadują z co najmniej jednym wierzchołkiem z warstwy W1 i nie należą do żadnej z warstw poprzednich, czyli W0 i W1. Do warstwy W3 należą wierzchołki sąsiadujące z co najmniej jednym wierzchołkiem z warstwy poprzedniej (W2) i nie należą do warstw o numerach mniejszych od 3, itd. Nietrudno zauważyć, że i-ta warstwa składa się ze wszystkich tych wierzchołków, których odległość (długość najkrótszej ścieżki) od s w grafie G wynosi dokładnie i. Dla każdego wierzchołka v wskaźniki p[v],p[p[v]], wyznaczają najkrótszą ścieżkę łączącą v z s. 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 s. 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 długości (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 S 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ę DFS(v), w której dodatkowo numerujemy wierzchołki w kolejności odwiedzania. W tym celu użyjemy globalnej zmiennej ostnr, której wartością jest ostatnio nadany numer. Zmienną ostnr inicjujemy na 0. Otrzymaną numerację będziemy nazywali numeracją w głąb.

Algorytm Przeszukiwanie w głąb


1  procedure DFS(v);
2  //v jest nowo odkrytym wierzchołkiem
3  begin
4    //markujemy v jako odwiedzony
5    visited[v]:=TRUE;
6    //wierzchołkowi v nadajemy kolejny numer
7    ostnr:=ostnr+1; nr[v]:=ostnr; 
8    //przeglądamy listę sąsiedztwa v i dla każdego nowo odkrytego 
9    //wierzchołka wywołujemy procedurę DFS
10   for each uL[v] do
11     if NOT visited[u] then
12       DFS(u)
13 end;

Jeśli chcemy przeszukać graf poczynając od wierzchołka s, wystarczy wywołać DFS(s), inicjując wcześniej tablice visited i current oraz zmienną ostnr. Zauważmy, że krawędź vu zaliczamy do drzewa przeszukiwania (w głąb), jeśli po wejściu do wierzchołka v odkrywamy, że wierzchołek u na liście sąsiedztwa v 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ź vu taka, że wierzchołki v, u nie są w relacji potomek-przodek. Bez straty ogólności możemy przyjąć, że v zostaje odwiedzony wcześniej niż u. Załóżmy, że u zostaje odwiedzony w wyniku wywołania przeszukiwania DFS podczas przeglądania listy sąsiedztwa v. Wówczas jednak u znajdzie się w poddrzewie przeszukiwania o korzeniu w v. Ponieważ u jest na liście v, do takiego wywołania dojść musi – sprzeczność z założeniem, że u, v nie są w relacji potomek-przodek.

Numeracja w głąb pozwala łatwo sprawdzić, czy dwa różne wierzchołki u, v są w relacji przodek-potomek w drzewie przeszukiwania w głąb. Załóżmy, że nr[u]<nr[v].

Wierzchołek u jest przodkiem wierzchołka v w drzewie przeszukiwania w głąb wtedy i tylko wtedy, gdy nr[u]<nr[v]nr[u]+d[u], gdzie d[u] jest liczbą wierzchołków w poddrzewie o korzeniu w u.

Pokażemy teraz, w jaki sposób zastosować przeszukiwanie w głąb do wyznaczenia wszystkich mostów grafie. Przypomnijmy, że mostem w spójnym grafie G nazywamy każdą krawędź, której usunięcie rozspójnia ten graf. Zauważmy, że bardzo łatwo wyznaczyć wszystkie mosty w czasie O(m(n+m)). 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, jest krawędzią każdego drzewa rozpinającego tego grafu.

Rozważmy drzewo przeszukiwania w głąb i niech uv będzie krawędzią w tym drzewie. Załóżmy także, że u jest ojcem v.

Spostrzeżenie 2

Krawędź uv jest mostem wtedy i tylko wtedy, gdy żadna krawędź niedrzewowa nie łączy wierzchołka z poddrzewa o korzeniu w v z właściwym przodkiem v. Innymi słowy wtedy i tylko wtedy, gdy oba końce każdej krawędzi niedrzewowej leżą w poddrzewie o korzeniu w v, 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 v niech low[v] będzie najmniejszym numerem w głąb wierzchołka, który można osiągnąć z v ścieżką składającą z krawędzi drzewowych z poddrzewa o korzeniu w v i zakończoną co najwyżej jedną krawędzią niedrzewową prowadzącą poza to poddrzewo. Funkcję low można rekurencyjnie zdefiniować w następujący sposób:

low[v]=min({nr[v]}{nr[u]:uv jest krawędzią niedrzewową }{low[u]:u jest synem v w drzewie przeszukiwania w głąb})

Spostrzeżenie 3

Niech vu będzie krawędzią drzewa przeszukiwania w głąb taką, że v jest ojcem u w tym drzewie. Wówczas krawędź vu jest mostem wtedy i tylko wtedy, gdy low[u]>nr[v].


Powyższe spostrzeżenia pozwalają już na zaproponowanie liniowego algorytmu wyznaczania mostów w spójnym grafie G. Algorytm ten zapiszemy za pomocą rekurencyjnej procedury DFSBridges(v).

Algorytm Mosty


1  procedure DFSBridges(v);
2  //v jest nowo odkrytym wierzchołkiem
3  begin
4    //markujemy v jako odwiedzony
5    visited[v]:=TRUE;
6    //wierzchołkowi v nadajemy kolejny numer
7    ostnr:=ostnr+1; nr[v]:=ostnr;
8    //inicjacja low[v] 
9    low[v]:=ostnr;
10   //przeglądamy listę sąsiedztwa v i dla każdego nowo odkrytego 
11   //wierzchołka wywołujemy procedurę DFS; dokonujemy też 
12   //aktualizacji low[v]
13   for each uL[v] do
14     if NOT visited[u] then
15     begin  
16       DFSBridges(u);
17       low[v]:=min(low[v],low[u]);
18       if low[u]>nr[v] then
19         krawędź vu jest mostem; 
20     end
21     else
22       low[v]:=min(low[v],nr[u])
23 end;   

Żeby wyznaczyć wszystkie mosty wystarczy wywołać DFSBridges(s) dla dowolnego wierzchołka s. Złożoność wyznaczania mostów jest asymptotycznie taka sama jak zwykłego przeszukiwania grafu, czyli O(n+m).