Algorytmy i Struktury Danych Wykład 5 Grafy. Przechodzenie grafu wszerz, w głąb. Minimalne drzewo rozpinające: algorytm Kruskala i algorytm Prima.
Podstawowe definicje Grafem to struktura oznaczona jako G=(V,E) składająca się ze skończonego i niepustego zbioru wierzchołków V i zbioru krawędzi E. Krawędź od wierzchołków u do v oznaczamy jako (vi,vj). Drogą od wierzchołka v1 do vn nazywamy sekwencję krawędzi: (v1,v2), (v2,v3),…, (vn-1,vn) Dwa wierzchołki vi i vj nazywamy sąsiędnimi, jeżeli w zbiorze D istnieje krawędź (vi,vj). Taką krawędź nazywamy incydentną do wierzchołków vi, vj. Stopniem wierzchołka v deg(v) – nazywamy liczbę krawędzi do niego incydentnych. Jeśli deg(v)=0 to jest to wierzchołek izolowany. Graf rzadki = to graf, dla którego |E| jest dużo mniejsze od |V|2. Graf gęsty – to graf, dla którego |E| jest bliskie |V|2.
Oznaczenia dla złożoności Czas działania dla grafu G=(V,E) najczęściej się mierzy w zależności od liczby wierzchołków |V| i liczby krawędzi |E|. Przyjmijmy, że zapis O(|V||E|) będziemy oznaczać jako O(V E).
Rodzaje grafów: Grafy proste graf pełny – każdy wierzchołek jest połączony krawędzią z każdym wierzchołkiem
Rodzaje grafów: Grafy złożone Multigraf – dwa wierzchołki mogą być połączone kilkoma krawędziami Pseudograf – pojawiają się pętle czyli krawędzie wychodzące z i wchodzące do tego samego wierzchołka
Rodzaje grafów: Grafy złożone Graf skierowany – krawędzie, zwane też łukami, spełniają warunek (u,v)≠(v,u) 5 2 4 6 1 Graf ważony – gdy krawędziom zostają przyporządkowane liczby – wagi
Reprezentacja grafów:
Lista sąsiedztwa 1 2 3 4 5 2 3 4 1 4 1 3
Lista sąsiedztwa Niech będzie dany graf G=(V,E). Jego reprezentacja w postaci listy sąsiedztwa będzie zapisana w tablicy Adj zawierającej |V| list, gdzie każda lista odpowiadać będzie. odpowiedniemu wierzchołkowi z V. Dla każdego uV elementami listy sąsiedztwa Adj[u] są wszystkie wierzchołki v takie że krawędź (u,v) należy do zbioru E. To oznacza, że Adj[u] zawiera wszystkie wierzchołki (lub wskaźniki do nich) sąsiadujące z u w grafie G. Formę listową preferuję się dla grafów rzadkich. Formę macierzową dla gęstych lub w przypadku Jeżeli graf G będzie miał skierowane krawędzie, to suma długości wszystkich list będzie wynosić |E|, związane jest to z tym, że krawędź postaci (u,v) to wystąpienie v na liście Adj[u]. Jeżeli zaś graf G będzie miał nieskierowane krawędzie to suma długości wszystkich list wierzchołków sąsiednich będzie dana wzorem 2|E|, gdyż dla nieskierowanej krawędzi (u,v), u znajdzie się na liście sąsiadów v a v na liście sąsiadów wierzchołka u. Rozmiar wymaganej pamięci przez reprezentację listową, niezależnie czy graf jest skierowany czy nie, wynosi (V+E). Listę sąsiedztwa można wykorzystać do reprezentacji grafów ważonych. Wówczas posługujemy się funkcją wagową w:ER . Dla danego grafu ważonego G=(V,E) funkcję wagową dla krawędzi (u,v) zapiszemy jako w(u,v). W praktyce oznacza to, że wagę pamiętamy obok wierzchołka sąsiedniego.
Macierz sąsiedztwa 1 2 3 4 5 1 2 3 4 5 0 1 1 1 0 1 0 0 0 0 1 0 0 1 0 1 0 1 0 0 0 0 0 0 0
Macierz sąsiedztwa Niech w grafie G=(V,E) wierzchołki będą ponumerowane od 1,2, …, |V| w pewien dowolny sposób. Jeżeli mamy etykiety wierzchołków zapisane innymi znakami należy im przyporządkować numery od 1,2, …, |V|. Wówczas macierz sąsiedztwa dla grafu G będzie zapisana w postaci macierzy A=(aij) wymiaru |V|x|V| w sposób następujący: 𝑎 𝑖𝑗 = 1 𝑗𝑒ś𝑙𝑖 (𝑖,𝑗)∈𝐸 0 𝑤 𝑝𝑟𝑧𝑒𝑐𝑖𝑤𝑛𝑦𝑚 𝑝𝑟𝑧𝑦𝑝𝑎𝑑𝑘𝑢 Dla grafów nieskierowanych macierz sąsiedztwa jest symetryczna. Niech graf G=(V,E) będzie grafem ważonym z funkcją wagową w. Wówczas waga w(u,v) krawędzi (u,v)E będzie zapisana w wierszu u i kolumnie v macierzy sąsiedztwa. Jeśli dana krawędź nie istnieje to wpisujemy, NIL, 0 albo ∞, w zależności od implementacji analizowanego problemu.
Macierz incydencji (1,2) (1,3) (1,4)(3,4) 1 2 3 4 5 1 1 1 0 1 0 0 0 (1,2) (1,3) (1,4)(3,4) 1 2 3 4 5 1 1 1 0 1 0 0 0 0 1 0 1 0 0 1 1 0 0 0 0 1 2 3 4 5 1 2 3 4 5 0 -1 -1 1 0 1 0 0 0 0 1 0 0 -1-1 -1 0 1 0 0 0 0 1 0 0
Macierz incydencji Niech będzie dany graf G=(V,E). Jeżeli graf G będzie grafem skierowanym wówczas macierz incydencji zdefiniujemy jako macierz B=(bij) o wymiarze |V|x|E| taką, że 𝑏 𝑖𝑗 = −1, 𝑗𝑒ś𝑙𝑖 𝑘𝑟𝑎𝑤ę𝑑ź 𝑗 𝑤𝑦𝑐ℎ𝑜𝑑𝑧𝑖 𝑧 𝑤𝑖𝑒𝑟𝑧𝑐ℎ𝑜ł𝑘𝑎 𝑖, 1, 𝑗𝑒ś𝑙𝑖 𝑘𝑟𝑎𝑤ę𝑑ź 𝑗 𝑤𝑐ℎ𝑜𝑑𝑧𝑖 𝑑𝑜 𝑤𝑖𝑒𝑟𝑧𝑐ℎ𝑜ł𝑘𝑎 𝑖, 0 𝑤 𝑝𝑟𝑧𝑒𝑐𝑖𝑤𝑛𝑦𝑚 𝑝𝑟𝑧𝑦𝑝𝑎𝑑𝑘𝑢. Jeżeli graf G będzie grafem nieskierowanym wówczas macierz incydencji zdefiniujemy jako macierz C=(cij) o wymiarze |V|x|E| taką, że 𝑐 𝑖𝑗 = 1 𝑗𝑒ż𝑒𝑙𝑖 𝑘𝑟𝑎𝑤ę𝑑ź 𝑖,𝑗 𝑗𝑒𝑠𝑡 𝑖𝑛𝑐𝑦𝑑𝑒𝑛𝑡𝑛𝑎 𝑑𝑜 𝑤𝑖𝑒𝑟𝑧𝑐ℎ𝑜ł𝑘ó𝑤 𝑖,𝑗 0 𝑤 𝑝𝑟𝑧𝑒𝑐𝑖𝑤𝑛𝑦𝑚 𝑝𝑟𝑧𝑦𝑝𝑎𝑑𝑘𝑢.
Algorytm przechodzenia grafu Niech G(V,E), n- liczba elementów zbioru V, m – liczba elementow zbioru m, pV. Wybieramy wierzchołek startowy pV, odwiedzamy go i zaznaczamy jako odwiedzony. Odkładamy wierzchołek w wybranej strukturze danych (stos – przechodzenie w głąb, kolejka –przechodzenie wszerz). Wykonujemy dopóki struktura danych wybrana do przechowywania wierzchołków nie jest pusta odczytujemy numer wierzchołka ze struktury, Jeśli odczytany wierzchołek ma sąsiada to: odwiedzamy najbliższego jego „sąsiada” i zaznaczamy go jako odwiedzony, jeśli „sąsiad” ma wierzchołki do odwiedzenia to odkładamy go do wybranej struktury, jeśli odczytany wierzchołek nie ma sąsiada to usuwamy go ze struktury.
Przechodzenie grafu w głąb Inna nazwa dfs z ang. Depth First Search Założenia: Graf jest reprezentowany w postaci listy sąsiedztwa lstw, gdzie lstw[i][0] – liczba sąsiadów dla i-tego wierzchołka, lstw[i][k] – gdzie k=1,2,…,maxs przyjmują wartości kolejnych wierzchołków „sąsiadów”, visited[|V|] – tablica przechowująca informacje, czy wierzchołek był odwiedzony – 1 czy nie -0, current[|V|] – tablica przechowująca wskaźniki do najbliższego sąsiada danego wierzchołka. Wierzchołki grafu są przechowywane na stosie,
DFS void dfs(int n){ int startowy=1; short int visited[maxw]; if ((current[p]) <= (lstw[p][0])) { stos[0]=0; push(stos,p); while (stos[0] > 0) { v=front(stos); w=lstw[v][current[v]]; current[v]++; if (current[v]>lstw[v][0])pop(stos); if (visited[w]!=1){ wizyta(w); visited[w]=1; if ((current[w]) <= (lstw[w][0])) push(stos,w); } }} } void dfs(int n){ int startowy=1; short int visited[maxw]; int current[maxw]; int v,p,w; for (v=1;v<=n;v++) { visited[v]=0; current[v]=startowy; } p=startowy; wizyta(p); // odwiedzamy wierzchołek p visited[p]=1;// zaznaczmy p jako odwiedzony
Przechodzenie wszerz Inna nazwa bfs - Breadth-first search Założenia: Graf jest reprezentowany w postaci listy sąsiedztwa lstw, gdzie lstw[i][0] – liczba sąsiadów dla i-tego wierzchołka, lstw[i][k] – gdzie k=1,2,…,maxs przyjmują wartości kolejnych wierzchołków „sąsiadów”, visited[|V|] – tablica przechowująca informacje, czy wierzchołek był odwiedzony – 1 czy nie -0, current[|V|] – tablica przechowująca wskaźniki do najbliższego sąsiada danego wierzchołka. Wierzchołki grafu są przechowywane w kolejce,
BFS void dfs(int n){ int startowy=1; short int visited[maxw]; int current[maxw]; int v,p,w; for (v=1;v<=n;v++) { visited[v]=0; current[v]=startowy; } p=startowy; wizyta(p); // odwiedzamy wierzchołek p visited[p]=1;// zaznaczmy p jako odwiedzony if ((current[p]) <= (lstw[p][0])) { enqueue(p); while (kolejka jest nie pusta) { v=front(kolejka); w=lstw[v][current[v]]; current[v]++; if (current[v]>lstw[v][0])dequeue(); if (visited[w]!=1){ wizyta(w); visited[w]=1; if ((current[w]) <= (lstw[w][0])) enqueue(w); } }} }
Przechodzenie w głąb-Przykład
Przechodzenie wszerz - przykład
Minimalne drzewa ropinające MST- minimum spanning tree Niech graf G=(V,E) będzie spójnym grafem nieskierowanym zaś z każdą krawędzią (u,v) E jest związana waga w(u,v). Wówczas minimalnym drzewem rozpinającym będziemy nazywać drzewo T będące acyklicznym podzbiorem TE, łączącym wszystkie wierzchołki i którego łączna waga określona wzorem: 𝑤 𝑇 = (𝑢,𝑣)∈𝑇 𝑤(𝑢,𝑣) jest najmniejsza.
Rozrastanie się minimalnego drzewa rozpinającego Załóżmy, że mamy spójny graf nieskierowany G=(V,E) z funkcją wagową w:ER. Aby znaleźć minimalne drzewo rozpinające możemy użyć algorytmu zachłannego polegającego na dołączaniu pojedynczych krawędzi w poszczególnych krokach. Oznaczmy przez Z – zbiór krawędzi. Na początku każdej iteracji zbiór Z stanowi podzbiór pewnego minimalnego drzewa rozpinającego. Jest to niezmiennik pętli. W każdym kroku szukamy krawędzi (u,v) którą możemy dodać do Z tak aby Z{(u,v)} był podzbiorem minimalnego drzewa rozpinającego. Jest to krawędź bezpieczna dla Z, gdyż można ją dodać bezpiecznie do Z bez naruszenia niezmiennika.
MST - algorytm generyczny void generic_mst(graf G, waga w) { Z= while Z nie tworzy minimalnego drzewa rozpinającego znajdź krawędź (u,v), będącą bezpieczną dla Z Z=Z{(u,v)} return Z }
Podstawowe definicje Przekrój (S,V-S) grafu nieskierowanego G=(V,E) nazywamy podział V na zbiory S i V-S. Jeżeli jeden z krańców krawędzi (u,v)E należy do S, a drugi do V-S to mówimy wówczas, że krawędź ta krzyżuje się z przekrojem (S,V-S). Przekrój uwzględnia zbiór krawędzi Z, jeżeli żadna z krawędzi z Z nie krzyżuje się z tym przekrojem.
Podstawowe definicje Jeżeli krawędź krzyżuje się z przekrojem to nazywa się ją krawędzią lekką, pod warunkiem że jej waga jest najmniejsza spośród wszystkich wag krawędzi krzyżujących się z tym przekrojem. Uogólniając, krawędź będziemy nazywać krawędzią lekką o danej własności, jeżeli jej waga będzie najmniejsza spośród wag wszystkich krawędzi o danej własności.
Twierdzenie Niech będzie dany spójny graf nieskierowany G=(V,E) z funkcją wagową w:ER, Z określone jako podzbiór E zawarty w pewny minimalnym drzewie rozpinającym grafu G oraz niech (S, V-S) będzie dowolnym przekrojem G uwzględniającym Z i niech (u,v) będzie krawędzią lekką krzyżującą się z (S,V-S). Wówczas krawędź (u,v) jest bezpieczna dla Z. Wniosek Niech G=(V,E) będzie spójnym grafem nieskierowanym z funkcją wagową w o wartościach rzeczywistych, określoną na E. Niech Z będzie podzbiorem E zawartym w pewnym minimalnym drzewie rozpinającym grafu G i niech D=(VD,ED) będzie spójną składową (drzewem) w lesie Gz=(V,Z). jeśli (u,v) jest krawędzią lekką łączącą D z pewną inną składową w Gz, to krawędź (u,v) jest bezpieczna dla Z.
Algorytm Kruskala void mst_kruskal(graf G, wagi w) { A= for każdy wierzchołek vG.V MAKE_SET(v) posortuj krawędzie z G.E rosnąco względem wag w for każda krawędź (u,v)G.E, w kolejności rosnących wag {if FIND_SET(u)FIND_SET(v) Z=Z{(u,v)} UNION(u,v) } return Z
Znaczenie operacji z pseudokodu MAKE_SET(x) – tworzy nowy zbiór, którego jedynym elementem jest x i x nie może być elementem innego zbioru (warunek rozłączności zbiorów), UNION(y, z) łączy dwa zbiory dynamiczne zawierające odpowiednio y i z w nowy zbiór będący ich sumą. Zakładamy, że przed wykonaniem operacji te dwa zbiory były rozłączne. FIND_SET(x) – zwraca wskaźnik do reprezentanta (jedynego) zbioru zawierającego x.
Algorytm Kruskala - przykład
Algorytm Prima . Krawędzie ze zbioru Z tworzą tu zawsze pojedyncze drzewo. Najpierw drzewo zawiera dowolnie wybrany wierzchołek-korzeń r. Następnie w każdym kroku dodaje się do niego krawędź lekką łączącą wierzchołek z drzewa Z z izolowanym wierzchołkiem Gz=(V,Z). Wszystkie wierzchołki znajdujące się poza drzewem, ustawione są w kolejce priorytetowej Q typu min. Dla każdego wierzchołka v kluczem v.key , który ustala pozycję w kolejce, jest minimalna waga spośród wag krawędzi łączących v z wierzchołkami drzewa. Jeśli krawędzi nie ma to przyjmujemy, że v.key=∞. Natomiast atrybut v. oznacza ojca wierzchołka v w obliczanym drzewie. Podczas wykonywania algorytmu zbiór Z z procedury generic_mst jest pamiętany niejawnie jako Z={(v,v.):vV-{r}-Q} Kiedy algorytm kończy zadanie, kolejka priorytetowa Q jest pusta a minimalnym drzewem rozpinającym Z w grafie G jest: Z={(v,v.):vV-{r}}.
Algorytm Prima - pseudokod void mst_prim(graf G, wagi w, korzeń r) { for każdy uG.V u.key=∞ u.=NIL } r.key=0 Q=G.V while Q u=EXTRACT-MIN(Q) for każdy vG.adj[u] if vQ I w(u,v)<v.key v.=u v.key=w(u,v) }}}
Algorytm Prima - przykład
Bibliografia A. Drozdek, „C++. Algorytmy i struktury danych”, Helion, Gliwice 2004; L. Banachowski, K. Diks, W. Rytter, „Algorytmy i struktury danych”, WNT, Warszawa1996; Cormen Thomas; Leiserson Charles; Rivest Ronald; Stein Clifford, „Wprowadzenie do Algorytmów”, Wydawnictwo Naukowe PWN, Warszawa 2012.