From 5f843047c9042ee967e1245351fa4a92a206f37b Mon Sep 17 00:00:00 2001 From: Grzegorz Kwacz Date: Sat, 19 Oct 2024 21:19:14 +0200 Subject: [PATCH] E2: DFS i sortowanie topologiczne --- docs/E-grafy/E2-dfs.md | 484 +++++++++++++++++++++ docs/E-grafy/E2-sortowanie-topologiczne.md | 196 +++++++++ mkdocs.yml | 5 + 3 files changed, 685 insertions(+) create mode 100644 docs/E-grafy/E2-dfs.md create mode 100644 docs/E-grafy/E2-sortowanie-topologiczne.md diff --git a/docs/E-grafy/E2-dfs.md b/docs/E-grafy/E2-dfs.md new file mode 100644 index 0000000..ca10ecf --- /dev/null +++ b/docs/E-grafy/E2-dfs.md @@ -0,0 +1,484 @@ +# Przeszukiwanie w głąb + +Poznaliśmy już algorytm BFS, a teraz poznamy alternatywny sposób przeszukiwania +grafu, który ze względu na swoją prostotę implementacji jest używany dużo +częściej. Jest to metoda przeszukiwania w głąb (Depth First Search). + +## Kolejka a stos + +Przpomnijmy sobie jak wygląda przeszukiwanie wszerz: + +```cpp +vector odkryty(n); +queue Q; +odkryty[zrodlo] = true; +Q.push(zrodlo); +while (!Q.empty()) { + v = Q.front(); + Q.pop(); + + for (int u : G[v]) { + if (!odkryty[u]) { + odkryty[u] = true; + Q.push(u); + } + } +} +``` + +Mamy zatem trzy rodzaje wierzchołków: + +- Nieodkryte. +- Znajdujące się na kolejce. +- Odwiedzone. + +W każdej iteracji algorytmu bierzemy wierzchołek z kolejki i odwiedzamy go, +wrzucając na kolejkę wszystkich jego nieodkrytych sąsiadów. + +Co, gdybyśmy jednak zamiast kolejki użyli innego kontenera, na przykład stosu? +Czy nadal odwiedzilibyśmy te same wierzchołki? Okazuje się, że tak. Nadal mamy +własność, że jeżeli odwiedzimy wierzchołek $v$, to wszystkich jego sąsiadów w +końcu też odwiedzimy. Czyli jeżeli ze źródła istnieje ścieżka $s \rightarrow v_1 +\rightarrow v_2 \rightarrow \ldots \rightarrow v_k \rightarrow v$, to skoro +odwiedzamy źródło, to $v_1$ też, tak samo $v_2$, i tak dalej, aż w końcu $v$. + +Popatrzmy jak algorytm używający stosu przebiegałby na poniższym grafie: + +
+```dot +graph G { + 1 -- 2 + 2 -- 5 + 1 -- 3 + 3 -- 4 + 4 -- 5 +} +``` +
+| Stos: || +|:----:|| +
+
+
+```dot +graph G { + 1 [color=blue] + 1 -- 2 + 2 -- 5 + 1 -- 3 + 3 -- 4 + 4 -- 5 +} +``` +
+| Stos: || 1 | +|:----:||:-:| +
+
+
+```dot +graph G { + 1 [color=green] + 1 -- 2 + 2 -- 5 + 1 -- 3 + 3 -- 4 + 4 -- 5 +} +``` +
+| Stos: || 1 | +|:-----:||:--------:| +
+
+
+```dot +graph G { + 1 [color=green] + 1 -- 2 + 2 -- 5 + 1 -- 3 + 3 -- 4 + 4 -- 5 +} +``` +
+| Stos: || +|:-----:|| +
+
+
+```dot +graph G { + 1 [color=green] + 2 [color=blue] + 1 -- 2 + 2 -- 5 + 1 -- 3 + 3 -- 4 + 4 -- 5 +} +``` +
+| Stos: || 2 | +|:-----:||:-:| +
+
+
+```dot +graph G { + 1 [color=green] + 2 [color=blue] + 3 [color=blue] + 1 -- 2 + 2 -- 5 + 1 -- 3 + 3 -- 4 + 4 -- 5 +} +``` +
+| Stos: || 2 | 3 | +|:-----:||:-:|:-:| +
+
+
+```dot +graph G { + 1 [color=red] + 2 [color=blue] + 3 [color=blue] + 1 -- 2 + 2 -- 5 + 1 -- 3 + 3 -- 4 + 4 -- 5 +} +``` +
+| Stos: || 2 | 3 | +|:-----:||:-:|:-:| +
+
+
+```dot +graph G { + 1 [color=red] + 2 [color=blue] + 3 [color=green] + 1 -- 2 + 2 -- 5 + 1 -- 3 + 3 -- 4 + 4 -- 5 +} +``` +
+| Stos: || 2 | 3 | +|:-----:||:-:|:--------:| +
+
+
+```dot +graph G { + 1 [color=red] + 2 [color=blue] + 3 [color=green] + 1 -- 2 + 2 -- 5 + 1 -- 3 + 3 -- 4 + 4 -- 5 +} +``` +
+| Stos: || 2 | +|:-----:||:-:| +
+
+
+```dot +graph G { + 1 [color=red] + 2 [color=blue] + 3 [color=green] + 4 [color=blue] + 1 -- 2 + 2 -- 5 + 1 -- 3 + 3 -- 4 + 4 -- 5 +} +``` +
+| Stos: || 2 | 4 | +|:----:||:-:|:-:| +
+
+
+```dot +graph G { + 1 [color=red] + 2 [color=blue] + 3 [color=red] + 4 [color=blue] + 1 -- 2 + 2 -- 5 + 1 -- 3 + 3 -- 4 + 4 -- 5 +} +``` +
+| Stos: || 2 | 4 | +|:-----:||:-:|:-:| +
+
+
+```dot +graph G { + 1 [color=red] + 2 [color=blue] + 3 [color=red] + 4 [color=green] + 1 -- 2 + 2 -- 5 + 1 -- 3 + 3 -- 4 + 4 -- 5 +} +``` +
+| Stos: || 2 | 4 | +|:-----:||:-:|:--------:| +
+
+
+```dot +graph G { + 1 [color=red] + 2 [color=blue] + 3 [color=red] + 4 [color=green] + 1 -- 2 + 2 -- 5 + 1 -- 3 + 3 -- 4 + 4 -- 5 +} +``` +
+| Stos: || 2 | +|:-----:||:-:| +
+
+
+```dot +graph G { + 1 [color=red] + 2 [color=blue] + 3 [color=red] + 4 [color=green] + 5 [color=blue] + 1 -- 2 + 2 -- 5 + 1 -- 3 + 3 -- 4 + 4 -- 5 +} +``` +
+| Stos: || 2 | 5 | +|:-----:||:-:|:-:| +
+
+
+```dot +graph G { + 1 [color=red] + 2 [color=blue] + 3 [color=red] + 4 [color=red] + 5 [color=blue] + 1 -- 2 + 2 -- 5 + 1 -- 3 + 3 -- 4 + 4 -- 5 +} +``` +
+| Stos: || 2 | 5 | +|:-----:||:-:|:-:| +
+
+
+```dot +graph G { + 1 [color=red] + 2 [color=blue] + 3 [color=red] + 4 [color=red] + 5 [color=green] + 1 -- 2 + 2 -- 5 + 1 -- 3 + 3 -- 4 + 4 -- 5 +} +``` +
+| Stos: || 2 | 5 | +|:-----:||:-:|:--------:| +
+
+
+```dot +graph G { + 1 [color=red] + 2 [color=blue] + 3 [color=red] + 4 [color=red] + 5 [color=green] + 1 -- 2 + 2 -- 5 + 1 -- 3 + 3 -- 4 + 4 -- 5 +} +``` +
+| Stos: || 2 | +|:-----:||:-:| +
+
+
+```dot +graph G { + 1 [color=red] + 2 [color=blue] + 3 [color=red] + 4 [color=red] + 5 [color=red] + 1 -- 2 + 2 -- 5 + 1 -- 3 + 3 -- 4 + 4 -- 5 +} +``` +
+| Stos: || 2 | +|:-----:||:-:| +
+
+
+```dot +graph G { + 1 [color=red] + 2 [color=green] + 3 [color=red] + 4 [color=red] + 5 [color=red] + 1 -- 2 + 2 -- 5 + 1 -- 3 + 3 -- 4 + 4 -- 5 +} +``` +
+| Stos: || 2 | +|:-----:||:--------:| +
+
+
+```dot +graph G { + 1 [color=red] + 2 [color=green] + 3 [color=red] + 4 [color=red] + 5 [color=red] + 1 -- 2 + 2 -- 5 + 1 -- 3 + 3 -- 4 + 4 -- 5 +} +``` +
+| Stos: || +|:-----:|| +
+
+
+```dot +graph G { + 1 [color=red] + 2 [color=red] + 3 [color=red] + 4 [color=red] + 5 [color=red] + 1 -- 2 + 2 -- 5 + 1 -- 3 + 3 -- 4 + 4 -- 5 +} +``` + +
+| Stos: || +|:-----:|| +
+
+ + +## Co wybrać, BFS czy DFS? + +Zauważmy, że jeżeli próbowalibyśmy przypisywać wierzchołkom powyższego grafu +odległości tak samo, jak robimy to w algorytmie BFS, to odległość wierzchołka +$5$ od wierzchołka $1$ wyszłaby nam $3$ zamiast $2$. Nie jest to żaden błąd, ale +znak, że algorytm DFS po prostu nie nadaje się do tego zadania. + +Można się zatem zastanawiać, po co używać DFS, skoro BFS robi to samo, tylko +lepiej (bo dodatkowo potrafi policzyć odległości). Implementacja, którą na razie +zobaczyliśmy nie motywuje nas do tego. Spójrzmy jednak na poniższą rekurencyjną +implementację: + +```cpp +vector odwiedzony; +vector> sasiedzi; + +void dfs(int v) { + odwiedzony[v] = true; + for (int u : sasiedzi[v]) + if (!odwiedzony[u]) + dfs(u); +} + +int main() { + // ... + dfs(1); + // ... +} +``` + +Jak widać, jest ona dużo prostsza i krótsza od poprzedniej. Nie ma tu żadnego +stosu, za niego służy nam *stos wywołań rekurencyjnych*. Ze względu na to, +algorytm DFS jest dużo częściej używany w praktyce (i praktycznie zawsze +implementowany w sposób rekurencyjny). diff --git a/docs/E-grafy/E2-sortowanie-topologiczne.md b/docs/E-grafy/E2-sortowanie-topologiczne.md new file mode 100644 index 0000000..96d797b --- /dev/null +++ b/docs/E-grafy/E2-sortowanie-topologiczne.md @@ -0,0 +1,196 @@ +# Sortowanie topologiczne + +## Kolejność w skierowanym grafie acyklicznym + +Grafy skierowane, w których nie ma żadnych cykli (czyli ścieżek z pewnego +wierzchołka do niego samego) nazywa się *acyklicznymi* (ang. Directed Acyclic +Graph). + +W takich grafach możemy przypisać wierzchołkom numery tak, aby krawędzie +przechodziły tylko od wierzchołka o niższym numerze do wierzchołka o +wyższym. Taką kolejność nazywamy *posortowaniem topologicznym* wierzchołków. +To, że zawsze jest to możliwe, najłatwiej pokazać konstruując algorytm, który +będzie je znajdować. Zaraz to zrobimy, ale najpierw zróbmy pewne spostrzeżenie. + +Fakt: +*W każdym acyklicznym grafie skierowanym istnieje wierzchołek o stopniu +wejściowym zero.* +Dowód: +Załóżmy przeciwnie, czyli że każdy wierzchołek ma jakiegoś poprzednika. Weźmy +dowolny wierzchołek $v_0$. Ma on pewnego poprzednika $v_1$, który z kolei ma +swojego poprzednika $v_2$, i tak dalej, aż do $v_n$ (a nawet w nieskończoność, +ale nam wystarczy tyle). Mamy zatem ścieżkę +$v_n \rightarrow v_{n-1} \rightarrow \ldots \rightarrow v_1 \rightarrow v_0$ +złożoną z $n+1$ wierzchołków, ale w naszym grafie jest tylko $n$ różnych +wierzchołków! Oznacza to, że któryś z nich się powtarza na tej ścieżce, czyli +wyszło nam, że mamy cykl w grafie, ale nasz graf wcale nie ma żadnych +cykli! Zatem to nasze założenie, że każdy wierzchołek ma pewnego +poprzednika, było błędne. Prawdą natomiast jest, że musi istnieć co najmniej jeden +wierzchołek o stopniu wejściowym zero. + +## Algorytm iteracyjny + +Algorytm iteracyjny będzie budować listę wierzchołków w kolejności topologicznej +i opierać się na następującej obserwacji: + +- Jeżeli któryś wierzchołek nie ma poprzedników, to może znajdować się na +początku listy (czyli być pierwszym w kolejności topologicznej) + +Oprócz tego zauważmy, że taki wierzchołek nie będzie miał wpływu na kolejność +pozostałych wierzchołków między sobą. W takim razie po umieszczeniu go na +początku listy możemy go usunąć z grafu. Zostajemy więc z nowym grafem, na +którym możemy powtórzyć nasze podejście (zauważmy jednak, że "początek" dla +następnego wierzchołka będzie *po* aktualnie umieszczonym wierzchołku). + +### Implementacja + +Jak szybki może być taki algorytm? Wykona on $n$ iteracji, w każdej musi wziąć +dowolny wierzchołek o stopniu wejściowym zero, nazwijmy go `v`, a następnie +usunąć go z grafu. Stopnie wejściowe wszystkich wierzchołków możemy pamiętać w +tablicy, więc naiwne szukanie zajęłoby nam czas $O(n)$. + +Jak zaimplementować usuwanie z grafu? Tablicą `nastepnicy[v]` nie musimy się +przejmować, bo liczymy na to, że nigdy już nie potraktujemy `v` jako istniejący +wierzchołek, więc nie powinniśmy się do niej odwoływać. Drugą rzeczą, na którą +`v` ma wpływ, są stopnie wejściowe jego następników, a więc trzeba je wszystkie +pomniejszyć o 1. + +Takie podejście mogłoby wyglądać następująco: + +```cpp +vector lista; +for (int i = 0; i < n; i++) { + // Szukamy wierzchołka do usunięcia + int v; + for (int u = 0; u < n; u++) { + if (stopien_wejściowy[u] == 0 && !usuniety[u]) { + v = u; + break; + } + } + + // Dodajemy v do listy za wierzchołkami, które już zostały dodane, ale przed + // wierzchołkami, które zostaną dodane w późniejszych iteracjach + lista.push_back(v); + + usuniety[v] = true; + // Zmniejszamy stopnie następników + for (int u : nastepnicy[v]) + stopien_wejsciowy[u]--; +} +``` + +Szukanie wierzchołków zajmie nam sumarycznie $O(n^2)$ operacji. Zmniejszanie +stopni wykona dokładnie jedną operację dla każdej krawędzi w naszym grafie, więc +sumarycznie zajmie czas $O(m)$. Niestety ten liniowy czynnik jest dominowany +przez pierwszy krok, więc finalna złożoność wynosi $O(n^2)$. + +### Optymalizacja szukania wierzchołka do usunięcia + +Jak zauważyliśmy, szukanie wierzchołka o stopniu wejściowym zero jest wąskim +gardłem w algorytmie. Czy możemy przyspieszyć ten krok? + +Wyobraźmy sobie, że w kolejnych krokach algorytmu mamy worek, w którym znajdują +się wszystkie wierzchołki o stopniu zero. Wtedy jedna iteracja algorytmu +sprowadzałaby się do: + +- Wyjmij dowolny wierzchołek `v` z worka i usuń go z grafu. +- Zaktualizuj worek. + +Aktualizacja worka będzie polegać na dodaniu do niego wierzchołków, których +stopień wejściowy spadł do zera podczas usuwania `v` z grafu. Zatem wystarczy +sprawdzić jedynie następników `v`, jako że stopień pozostałych wierzchołków się +nie zmienił! + +Daje nam to istotne przyspieszenie, jako że sumarycznie podczas aktualizacji +worka we wszystkich iteracjach rozpatrzymy każdą krawędź grafu dokładnie raz, +więc zajmie to czas $O(m)$. Włączając jeszcze koszt początkowej budowy worka i +iteracji algorytmu, dostaniemy czas $O(n+m)$. + +Naszym „workiem” może być jakakolwiek kontener C++, który udostępnia szybkie +dodawanie i usuwanie elementów, na przykład kolejka, stos albo wektor. + +Pełna implementacja tego algorytmu może wyglądać następująco: + +```cpp +vector nastepnicy[n]; +int stopien_wejsciowy[n]; + +vector sortuj() { + // worek będzie trzymał wszystkie wierzchołki o stopniu wejściowym 0 + stack worek; + + for (int v = 0; v < n; v++) + if (stopien_wejsciowy[v] == 0) + worek.push(v); + + vector lista; + + while (!worek.empty()) { + // Bierzemy dowolny wierzchołek stopnia 0 + int v = worek.top(); + worek.pop(); + + // Dodajemy v do listy za wierzchołkami, które już zostały dodane, ale przed + // wierzchołkami, które zostaną dodane w późniejszych iteracjach + lista.push_back(v); + + // Usuwamy v z grafu + for (int u : nastepnicy[v]) { + stopien_wejsciowy[u]--; + // Jeżeli stopień spadł do 0, to dodajemy do worka + if (stopien_wejsciowy[u] == 0) + worek.push(u); + } + } + + return lista; +} +``` + +Zniknęła nam tablica `usuniety`, ponieważ w worku mamy jedynie nieusunięte +wierzchołki. + +## Algorytm rekurencyjny + +Będziemy budować listę posortowanych wierzchołków rekurencyjnie. Przypomnijmy, +że aby kolejność wierzchołków spełniała warunek posortowania, wszyscy następnicy +każdego wierzchołka muszą być po nim na liście. My jednak, aby ułatwić +implementację, zrobimy odwrotnie i wszyscy następnicy wierzchołka będą *przed* +nim. Aby otrzymać wynik zgodny z definicją podaną wcześniej, wystarczy taką +listę odwrócić (choć nie zawsze jest to konieczne i czasami wygodniej pracować w +takiej formie). + +Zaimplementujmy więc funkcję `dfs(v)`, której wywołanie będzie gwarantować, że +na naszej liście znajdzie się wierzchołek `v` i wszystkie wierzchołki +topologicznie od niego większe (i będą one w dobrej kolejności). + +```cpp +vector nastepnicy[n]; +bool odwiedzony[n]; +vector lista; + +// Po wywołaniu dfs(v), v będzie się znajdować na liście +int dfs(int v) { + if (odwiedzony[v]) + // v już jest na liście, nic nie trzeba robić + return; + + odwiedzony[v] = true; + + // Upewniamy się, że wszyscy następnicy są na liście + for (int u : nastepnicy[v]) + dfs(u); + + // Teraz możemy dodać na koniec v + lista.push_back(v); +} +``` + +Aby nasza lista była kompletna, możemy użyć założenia o funkcję `dfs` i +zagwarantować, że każdy wierzchołek się na niej znajdzie: + +```cpp +for (int v = 0; v < n; v++) + dfs(v); +``` diff --git a/mkdocs.yml b/mkdocs.yml index 6e4d71e..701e117 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -15,12 +15,14 @@ markdown_extensions: - pymdownx.arithmatex: generic: true - md_in_html + - mkdocs_graphviz extra_javascript: - javascripts/mathjax.js - javascripts/preview-popup.js - https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js + - https://cdn.jsdelivr.net/gh/rod2ik/cdn@main/mkdocs/javascripts/mkdocs-graphviz.js nav: - Start: 'index.md' @@ -43,6 +45,9 @@ nav: - 'Rekurencja, szybkie potęgowanie i algorytm Euklidesa': './B-proste-algorytmy/B5-rekurencja.md' - 'Proste sortowanie': './B-proste-algorytmy/B6-sortowanie.md' - 'Sumy w tablicach': './B-proste-algorytmy/B7-sumy-gasienica.md' + - 'Grafy': + - 'DFS': './E-grafy/E2-dfs.md' + - 'Sortowanie topologiczne': './E-grafy/E2-sortowanie-topologiczne.md' extra_css: