Grafy
Graf Graf (ang. graph) to zbiór wierzchołków (ang. vertices), które mogą być połączone krawędziami (ang. edges) w taki sposób, że każda krawędź kończy się i zaczyna w którymś z wierzchołków. Graf zapisujemy w postaci uporządkowanej pary G = (V, E), gdzie V = {v1, v2, …, vn} to zbiór n ponumerowanych wierzchołków a E = {e1, e2, …, em} to zbiór m krawędzi. Każda krawędź jest parą wierzchołków grafu (u,v) połączonych tą krawędzią, przy czym u,vV. Oznaczenia: |V| = n |E| = m
Przykłady grafów Graf prosty, to graf bez pętli i bez krawędzi wielokrotnych.
Historia grafów Za najstarszy przykład zastosowania grafów w rozwiązaniu zadanego problemu uznaje się zagadnienie mostów królewieckich, opis którego opublikował w 1736 roku Leonhard Euler.
Sąsiedztwo Dwa wierzchołki u i v sąsiadują ze sobą (są sąsiednie), gdy istnieje krawędź {u,v}E. Dwie krawędzie są incydentne, gdy mają wspólny koniec, czyli {u,v} i {v,w}. Krawędź e jest incydentna z wierzchołkiem v, gdy v jest jednym z końców krawędzi e. Stopień wierzchołka v to liczba wszystkich incydentnych z nim krawędzi w grafie, co oznaczamy deg(v).
Podgrafy Podgraf to fragment grafu pierwotnego. Graf G’=(V’,E’) jest podgrafem grafu G=(V,E), co zapisujemy G’G, gdy G’ powstał z G przez usunięcie części wierzchołków wraz z incydentnymi krawędziami oraz części krawędzi, czyli V’V oraz E’E.
Graf gęsty Graf gęsty to graf, w którym jest „stosunkowo dużo” krawędzi, czyli mO(n2-), dla pewnego [0,1). Graf gęsty najczęściej zapisujemy w postaci macierzy sąsiedztwa. Macierz sąsiedztwa dla grafu prostego jest symetryczna względem głównej przekątnej – można więc zredukować macierz kwadratową do macierzy trójkątnej dolnej. Pamięć potrzebna do zaprezentowania grafu w postaci macierzy sąsiedztwa wynosi O(n2).
Graf rzadki Graf rzadki to graf, w którym jest „stosunkowo mało” krawędzi, czyli mO(n1+), dla pewnego [0,1). Graf rzadki najczęściej zapisujemy w postaci listy sąsiadów. Każdej krawędzi odpowiadają 2 węzły na różnych liściach sąsiadów. Pamięć potrzebna do zaprezentowania grafu w postaci listy sąsiadów wynosi O(n+m).
Grafy szczególne Graf pusty – |E|=0 Graf pełny – |E|=(n2-n)/2 Graf cykliczny Graf liniowy Graf kołowy Graf dwudzielny – zbiór wierzchołków możemy podzielić na dwa rozłączne podzbiory w taki sposób, że każda krawędź ma końce należące do różnych podzbiorów, czyli V=AB, gdzie AB=, oraz każda krawędź e=(u,v)E jest postaci uA i vB.
Ścieżki i cykle Ścieżka w grafie G o długości k, to ciąg k+1 wierzchołków (vi0,vi1,vi2,…,vik) taki, że każde dwa sąsiednie wierzchołki tym ciągu są krawędzią w G, czyli (vij-1,vij)E dla j=1…k. Ścieżka prosta w grafie G o długości k, to ścieżka, w której żadne dwa wierzchołki nie pojawiają się dwukrotnie. Cykl w grafie G to ścieżka, w której pierwszy i ostatni wierzchołek są takie same. Cykl prosty grafie G to cykl, w którym żaden wierzchołek, za wyjątkiem pierwszego i ostatniego, nie powtarzają się.
Spójność Graf jest spójny, gdy pomiędzy każdą parą wierzchołków istnieje jakaś ścieżka prosta, która je łączy. Każdy graf można przedstawić w postaci sumy podgrafów spójnych, które nazywają się spójnymi składowymi grafu. Wierzchołek, którego stopień wynosi 0 nazywamy wierzchołkiem izolowanym.
Spójność Minimalna liczba krawędzi w grafie spójnym wynosi n-1. Taki graf nazywa się drzewem. W drzewie nie ma cylki. Liczba krawędzi w grafie złożonym z k składowych spójności wynosi n–k ≤ m ≤ (n-k)(n-k-1)/2. Graf, który ma n wierzchołków i m > (n- 1)(n-2)/2 krawędzi jest spójny
Grafy eulerowskie Graf eulerowski to graf posiadający cykl Eulera. Cykl Eulera to cykl, który przechodzi przez wszystkie krawędzie dokładnie jeden raz. Twierdzenie: Każdy graf, w którym stopnie wszystkich wierzchołków są parzyste posiada cykl Eulera.
Grafy hamiltonowskie Graf hamiltonowski to graf posiadający cykl Hamiltona. Cykl Hamiltona to cykl, który przechodzi przez wszystkie wierzchołki dokładnie jeden raz. Twierdzenie: Jeśli w grafie stopień każdego wierzchołka jest ≥n/2, gdzie n to liczba wierzchołków w grafie, to w grafie tym istnieje cykl Hamiltona.
Przeglądanie grafu wszerz Zaznaczamy wszystkie wierzchołki jako nieodwiedzone. Wyznaczamy wierzchołek startowy s i wrzucamy go do kolejki planowanych odwiedzin. Powtarzamy następująca procedurę: wyciągamy wierzchołek z kolejki, odwiedzamy go, do kolejki odwiedzin wrzucamy wszystkich jego sąsiadów, którzy jeszcze nie byli odwiedzeni.
Przeglądanie grafu wszerz Wszerz(G, s) { stwórz pustą kolejkę Q; Q := {s}; while ( Q != {} ) do x wyciągnij z kolejki Q przetwarzamy x; zaznacz x jako odwiedzony; for u: (x,u) E do if (u nieodwiedzony i nie ma go w kolejce) Q := Q+u; }
Przeglądanie grafu w głąb Zaznaczamy wszystkie wierzchołki jako nieodwiedzone. Wyznaczamy wierzchołek startowy s i wrzucamy go na stos planowanych odwiedzin. Powtarzamy następująca procedurę: ściągamy wierzchołek ze stosu, odwiedzamy go, na stos odwiedzin wrzucamy wszystkich jego sąsiadów, którzy jeszcze nie byli odwiedzeni.
Przeglądanie grafu w głąb W_glab(G, s) { stwórz pusty stos S; S := {s}; while ( S != {} ) do ściągnij x ze stosu S; przetwarzamy x; zaznacz x jako odwiedzony; for u: (x,u) E do if (u nieodwiedzony i nie ma go na stosie) S := S+u; }
Przeglądanie grafu Złożoność czasowa: O(n+m) bo odwiedzamy wszystkie wierzchołki i przetwarzając wierzchołek sprawdzamy wszystkich jego sąsiadów. Złożoność pamięciowa: O(n). Przeglądanie grafu w głąb można zaprogramować rekurencyjnie.
Grafy skierowane Graf skierowany (digraf) to graf, w którym krawędzie mają kierunek (jeden wierzchołek jest początkowy a drugi końcowy). Stopień wejściowy wierzchołka to liczba krawędzi wchodzących do tego wierzchołka; stopień wyjściowy wierzchołka to liczba krawędzi wchodzących z tego wierzchołka. Silnie spójna składowa grafu skierowanego G, to taki maksymalny podgraf H (a jednocześnie jego spójna składowa), że pomiędzy każdą parą wierzchołków w H istnieją ścieżki, które je łączą.
Grafy ważone Graf ważony to graf, w którym krawędziom są przypisane pewne wagi (najczęściej nieujemne): G(V, E, w), gdzie w: E R.
Zadanie 1 Zdefiniuj graf w postaci macierzy sąsiedztwa. W grafie tym będziemy dopuszczali następujące operacje modyfikujące: dokładanie i usuwanie krawędzi. Liczba wierzchołków ma być niezmienna. Przetestuj swoją strukturę.
Zadanie 2 Napisz funkcję sprawdzającą czy zadany graf jest spójny. Wykorzystaj przeglądanie grafu wszerz.
Zadanie 3 Napisz funkcję liczącą z ilu składowych spójności składa się zadany graf. Wykorzystaj przeglądanie grafu w głąb.
Zadanie 4 Zdefiniuj graf w postaci listy sąsiadów. W grafie tym będziemy dopuszczali następujące operacje modyfikujące: dokładanie i usuwanie krawędzi. Liczba wierzchołków ma być niezmienna. Uzupełnij graf o metodę udostępniającą sąsiadów zadanego wierzchołka. Przetestuj swoją strukturę.
Algorytmy grafowe
Minimalne drzewo rozpinające Minimalne drzewo rozpinające (ang. minimum spaning tree) danego grafu ważonego G to taki podgraf T, który jest drzewem oraz w którym suma wag jest najmniejsza z możliwych.
Minimalne drzewo rozpinające Algorytm Kruskala (1956 r.): Utwórz las L z wierzchołków oryginalnego grafu – każdy wierzchołek jest na początku osobnym drzewem. Utwórz zbiór S zawierający wszystkie krawędzie oryginalnego grafu. Dopóki S nie jest pusty oraz L nie jest jeszcze drzewem rozpinającym: Wybierz i usuń z S jedną z krawędzi o minimalnej wadze. Jeśli krawędź ta łączyła dwa różne drzewa, to dodaj ją do lasu L, tak aby połączyła dwa odpowiadające drzewa w jedno. W przeciwnym wypadku odrzuć ją.
Minimalne drzewo rozpinające Implementacja algorytm Kruskala: Las L z wierzchołków oryginalnego grafu przechowujemy w strukturze dla zbiorów rozłącznych (na przykład w drzewiastej strukturze dla zbiorów rozłącznych). Zbiór S zawierający wszystkie krawędzie oryginalnego grafu przechowujemy w kolejce priorytetowej (na przykład w kopcu). Drzewo rozpinające będziemy pamiętali w postaci list sąsiadów. Złożoność czasowa: O(E log(E)) Złożoność pamięciowa: O(E + V)
Najkrótsze ścieżki w grafie Najkrótsza ścieżka (ang. shortest path) w grafie ważonym to ścieżka, która łączy zadaną parę wierzchołków oraz suma wag krawędzi należących do tej ścieżki jest najmniejsza z możliwych. Problem polega na znalezieniu w grafie ważonym najkrótszego połączenia pomiędzy danymi wierzchołkami. Szczególnymi przypadkami tego problemu są problem najkrótszej ścieżki od jednego wierzchołka do wszystkich innych oraz problem najkrótszej ścieżki pomiędzy wszystkimi parami wierzchołków.
Najkrótsze ścieżki w grafie pomiędzy wybranym wierzchołkiem a wszystkimi pozostałymi Algorytm Dijkstry służy do znajdowania najkrótszej ścieżki z pojedynczego źródła w grafie o nieujemnych wagach krawędzi: Przez s oznaczamy wierzchołek źródłowy. Stwórz tablicę D odległości od źródła dla wszystkich wierzchołków grafu. Na początku D[s]:=0, zaś dla wszystkich pozostałych wierzchołków D[v]:=. Utwórz kolejkę priorytetową Q wszystkich wierzchołków grafu. Priorytetem kolejki jest aktualnie wyliczona odległość od wierzchołka źródłowego s. Wstaw s do kolejki Q. Dopóki kolejka nie jest pusta: Usuń z kolejki wierzchołek u o najniższym priorytecie (wierzchołek najbliższy źródła, który nie został jeszcze rozważony) Dla każdego sąsiada v wierzchołka u dokonaj relaksacji poprzez u: D[v] := min(D[u]+w(u, v), D[v]).
Implementacja algorytm Dijkstry: Najkrótsze ścieżki w grafie pomiędzy wybranym wierzchołkiem a wszystkimi pozostałymi Implementacja algorytm Dijkstry: Kolejkę priorytetową Q wszystkich wierzchołków grafu przechowujemy w kolejce priorytetowej (na przykład w kopcu). Złożoność czasowa: O(E log(V)) Złożoność pamięciowa: O(V)
Najkrótsze ścieżki w grafie pomiędzy zadaną parą wierzchołków Problem znalezienia w grafie ważonym najkrótszego połączenia pomiędzy wierzchołkiem początkowym s a wierzchołkiem docelowym t. Problem ten można rozwiązać za pomocą algorytmu Dijkstry, przerywając do w momencie rozpatrzenia wierzchołka t.
Najkrótsze ścieżki w grafie pomiędzy wszystkimi parami wierzchołków Algorytm Floyda-Warshalla służy do znajdowania najkrótszych ścieżek pomiędzy wszystkimi parami wierzchołków w grafie skierowanym ważonym o nieujemnych wagach krawędzi (wystarczy aby nie było w nim ujemnych cykli). Algorytm Floyda-Warshalla opiera się na następującym spostrzeżeniu: niech dij(k) oznacza długość najkrótszej spośród ścieżek vi do vj o wierzchołkach pośrednich w zbiorze {v1,...,vk}; stąd dij(0) = Aij (połączenie przez jedną krawędź) dij(k+1) = min{dij(k), di k+1(k)+dk+1 j(k)}
Najkrótsze ścieżki w grafie pomiędzy wszystkimi parami wierzchołków skopiuj wartości macierzy sąsiedztwa T do tablicy D inicjalizuj tablicę P zerami for (każdy węzeł k grafu spośród węzłów (1,..,n) ) for (każdy węzeł i grafu spośród węzłów (1,..,n) ) for (każdy węzeł j grafu spośród węzłów (1,..,n) ) if (D[i,k]+D[k,j] < D[i,j]) { D[i,j] = D[i,k] + D[k,j] //najkrótsza ścieżka prowadzi teraz przez węzeł k P[i,j] = k }
Najkrótsze ścieżki w grafie pomiędzy wszystkimi parami wierzchołków Implementacja algorytm Dijkstry: Graf pamiętamy w macierzy. Obliczenia rozpoczynamy od macierzy odległości odwzorowującej graf pierwotny: Złożoność czasowa: O(V3) Złożoność pamięciowa: O(V2)
Zadanie 5 Zaprogramuj algorytm Dijkstry Znajdowania najkrótszej ścieżki w grafie z nieujemnymi wagami na krawędziach. Przetestuj swój algorytm.