![]() |
![]() ![]() ![]() Prezentowane materiały są przeznaczone dla uczniów szkół ponadgimnazjalnych. Autor artykułu: mgr Jerzy Wałaszek, wersja1.0 |
©2008 mgr
Jerzy Wałaszek |
Graf (ang. graph) jest strukturą danych zbudowaną z wierzchołków (ang. vertex):
oraz z łączących te wierzchołki krawędzi (ang. edge):
W celu identyfikacji wierzchołki grafu zwykle numerujemy od 1 do n, gdzie n oznacza liczbę wierzchołków. Sposób numeracji jest dowolny, lecz czasem ma znaczenie:
Umawiamy się, iż w strukturze grafu przechodzimy od wierzchołka do wierzchołka tylko wzdłuż łączących te wierzchołki krawędzi. Na przykład w naszym grafie możemy bezpośrednio przejść z wierzchołka nr 1 tylko do wierzchołka nr 3 i nr 5. Do pozostałych wierzchołków możemy dojść, ale nie bezpośrednio. Wierzchołki, które łączą się krawędzią z danym wierzchołkiem nazywamy jego sąsiadami lub wierzchołkami sąsiednimi.
Ze względu na własności krawędzi rozróżniamy kilka rodzajów grafów:
Graf zerowy (ang. null graph) - graf zawiera jedynie wierzchołki, ale nie zawiera żadnej krawędzi.
Graf nieskierowany (ang. undirected graph) - po każdej krawędzi można przejść w obu kierunkach.
Graf skierowany (ang. directed graph lub digraph) - krawędzie są skierowane i można je przejść tylko zgodnie z ich kierunkami - ruch "pod prąd" jest zabroniony. Krawędź skierowaną oznaczamy strzałką na rysunku grafu:
Krawędzie grafu mogą posiadać skojarzone ze sobą wartości zwane wagami (np. jeśli graf reprezentuje sieć dróg, to wagi mogą oznaczać odległości pomiędzy wierzchołkami, zużycie paliwa przy ruchu wzdłuż danej krawędzi, czas przebycia krawędzi, przepustowość drogi itp.). Graf taki nazywamy grafem z wagami lub grafem ważonym (ang. weighted graph). Graf ważony może być zarówno grafem nieskierowanym jak i skierowanym.
Krawędź łącząca dwa wybrane wierzchołki grafu może się wielokrotnie powtarzać. Również mogą istnieć krawędzie, które łączą dany wierzchołek ze sobą samym (krawędź łącząca z obu stron ten sam wierzchołek nazywa się pętlą - ang. loop). Taki graf nosi nazwę multigrafu lub pseudografu (ang. multigraph):
W matematyce graf przedstawiany jest jako para zbiorów:
G = (V,E)
V - zbiór wierzchołków
E - zbiór krawędzi
Zbiór wierzchołków V może składać się z numerów wierzchołków:
![]() |
V = {1,2,3,4,5,6,7} |
Zbiór krawędzi E to zbiór par wierzchołków ze zbioru V, które połączone są w grafie krawędzią:
![]() |
E = {(1,3),(1,5),(2,3),(2,5),(2,6),(3,4),(3,6),(4,5),(4,7),(6,7)} |
Jeśli graf jest grafem nieskierowanym, to pary w zbiorze krawędzi E mogą zawierać wierzchołki w dowolnym porządku. W grafie skierowanym wierzchołki w parze są uporządkowane zgodnie z kierunkiem krawędzi. Pierwszy wierzchołek jest wierzchołkiem wyjściowym, a drugi jest wierzchołkiem docelowym:
![]() |
E = {(1,3),(2,3),(3,4),(3,6),(4,5),(4,7),(5,1),(5,2),(6,2),(7,6)} |
Jeśli mamy do czynienia z grafem ważonym, to zbiór krawędzi E tworzą trójki. W każdej trójce dwa pierwsze elementy określają wierzchołki połączone krawędzią, a trzeci element określa wagę tej krawędzi:
![]() |
E = {(1,3,3),(2,3,5),(3,4,1),(3,6,3),(4,5,3),(4,7,4),(5,1,2),(5,2,1),(6,2,5),(7,6,9)} |
Multigraf reprezentujemy zbiorem krawędzi E, w którym pary wierzchołków mogą się powtarzać oraz para może być złożona z tego samego wierzchołka:
![]() |
E = {(1,3),(1,3),(1,5),(2,3),(2,3),(2,5),(2,5),(2,6),(3,4),(3,6),(3,6),(4,4),(4,5),(4,7),(6,6),(6,7),(6,7),(6,7)} |
Wymyślono wiele sposobów reprezentowania grafów w pamięci komputera. My opiszemy dwa - macierz sąsiedztwa (ang. adjacency matrix) oraz listy sąsiedztwa (ang. adjacency list).
Tworzymy kwadratową macierz o wymiarze n × n, gdzie n oznacza liczbę wierzchołków w grafie. Numery wierszy oraz kolumn tej macierzy odpowiadają numerom wierzchołków w grafie. Dane dwa wierzchołki są połączone krawędzią, jeśli na przecięciu wiersza o numerze pierwszego wierzchołka i kolumny o numerze drugiego wierzchołka komórka macierzy przechowuje wartość 1. Dwa wierzchołki nie są połączone krawędzią, jeśli na przecięciu ich wiersza i kolumny mamy 0.
Dla grafu skierowanego macierz sąsiedztwa wygląda następująco:
![]() |
|
Wiersze traktujemy jako wierzchołki wyjściowe. Kolumny traktujemy jako wierzchołki docelowe. Wiersz nr 4 odnosi się do wierzchołka 4. Widzimy, iż w kolumnach 5 i 7 znajduje się wartość 1. Oznacza to, iż wierzchołek 4 łączą dwie krawędzie z wierzchołkami 5 i 7 (sprawdź na rysunku). Kolumna nr 4 odnosi się również do wierzchołka nr 4, lecz teraz traktowanego jako wierzchołek docelowy krawędzi. W kolumnie tej 1 występuje tylko w wierszu nr 3. Oznacza to, iż wierzchołek nr 4 jest końcem krawędzi rozpoczynającej się w wierzchołku nr 3. |
Graf nieskierowany posiada krawędzie, które można przebywać w obu kierunkach. Możemy go potraktować jako graf skierowany, w którym mamy po dwie krawędzie skierowane w odwrotnych kierunkach:
![]() |
= | ![]() |
Dzięki takiemu podejściu wykorzystujemy macierz sąsiedztwa do reprezentacji grafu nieskierowanego - dla danych dwóch wierzchołków v1 i v2, które w grafie nieskierowanym łączy krawędź, umieszczamy 1 w macierzy sąsiedztwa na przecięciu wiersza v1 i kolumny v2 (krawędź z v1 do v2) oraz na przecięciu wiersza v2 i kolumny v1 (krawędź "odwrotna" z v2 do v1). W ten sposób w grafie istnieje droga w obu kierunkach pomiędzy tymi dwoma wierzchołkami.
![]() |
|
Zauważ, iż dla grafu nieskierowanego macierz sąsiedztwa jest symetryczna względem głównej przekątnej. |
Graf ważony możemy przedstawić dwoma macierzami - macierzą sąsiedztwa, w której umieszczamy informacje o ścieżkach - oraz macierzą wag ścieżek o takiej samej budowie jak macierz sąsiedztwa. Jeśli mamy pewność, iż wagi ścieżek nigdy nie są zerowe, to zamiast dwóch macierzy możemy użyć tylko jednej macierzy sąsiedztwa, w której umieszczamy zamiast jedynek wagi ścieżek.
![]() |
|
Multigraf przedstawiamy macierzą sąsiedztwa, w której komórkach umieszczamy informację o liczbie ścieżek łączących wierzchołki.
![]() |
|
Zwróć uwagę, iż w przypadku multigrafu główna przekątna macierzy może zawierać elementy niezerowe - są to krawędzie-pętle, które łączą dany wierzchołek z nim samym. W naszym przypadku tak jest dla wierzchołków nr 4 i nr 6. |
Definicję grafu przekazuje się do komputera najczęściej podając liczbę krawędzi, po której następują pary wierzchołków połączonych krawędzią w grafie. Dla przykładowego grafu wygląda to tak:
![]() |
10 1 3 1 5 2 3 2 5 2 6 3 4 3 6 4 5 4 7 6 7 |
Poniższy program odczytuje z konsoli znakowej definicję grafu nieskierowanego definicję grafu podaną za pomocą krawędzi jak wyżej. Liczba wierzchołków jest ograniczona do 32. Następnie dla każdego wierzchołka grafu program wypisuje wszystkich jego sąsiadów.
Uwaga: Wszystkie programy są tworzone w środowisku Dev-C++.
// Kurs teorii grafów dla początkujących //(C)2009 I LO w Tarnowie //-------------------------------------- #include <iostream> using namespace std; const int VMAX = 32; // maksymalna liczba wierzchołków // Macierz sąsiedztwa tworzymy o wymiarach o 1 większych, // aby zapewnić numerację wierszy i kolumn od 1 do VMAX. // Jeśli chcemy oszczędzać pamięć, to należy stosować // numerację wierzchołków od 0 do VMAX-1. //------------------------------------------------------- bool AM[VMAX+1][VMAX+1]; // macierz sąsiedztwa int n; // faktyczna liczba wierzchołków w grafie int m; // liczba krawędzi // Funkcja zeruje macierz sąsiedztwa //---------------------------------- void zero_AM() { for(int i = 1; i <= VMAX; i++) for(int j = 1; j <= VMAX; j++) AM[i][j] = false; } // Funkcja odczytuje definicję grafu // ze standardowego wejścia //---------------------------------- void read_AM() { n = 0; cin >> m; for(int i = 0; i < m; i++) { int v1,v2; cin >> v1 >> v2; if(v1 > n) n = v1; if(v2 > n) n = v2; AM[v1][v2] = AM[v2][v1] = true; } } // Funkcja wypisuje sąsiadów każdego wierzchołka //---------------------------------------------- void show_neighbours() { for(int i = 1; i <= n; i++) { cout << i << ": "; for(int j = 1; j <= n; j++) if(AM[i][j]) cout << j << " "; cout << endl; } } // Program główny //--------------- main() { zero_AM(); // zerujemy macierz sąsiedztwa read_AM(); // wczytujemy definicję grafu cout << endl; show_neighbours(); // wyświetlamy sąsiadów cout << endl << endl; system("PAUSE"); } |
10 1 3 1 5 2 3 2 5 2 6 3 4 3 6 4 5 4 7 6 7 1: 3 5 2: 3 5 6 3: 1 2 4 6 4: 3 5 7 5: 1 2 4 6: 2 3 7 7: 4 6 |
Zadanie 1
Napisz analogiczne programy dla grafu skierowanego i multigrafu.
Zadanie 2
Napisz program, który wczyta definicję grafu skierowanego, a następnie dla każdego wierzchołka wyświetli wszystkie wierzchołki, dla których ten wierzchołek jest wierzchołkiem docelowym łączącej je krawędzi.
Zadanie 3
Ważony graf skierowany definiujemy następująco:
Pierwsza liczba określa ilość krawędzi w grafie.
Następnie występuje tyle trójek liczb ile wynosiła pierwsza liczba. W każdej trójce dwie pierwsze liczby określają kolejno numer wierzchołka wyjściowego i numer wierzchołka docelowego krawędzi. Trzecia liczba jest wagą tej krawędzi.
Graf może posiadać maksymalnie 32 wierzchołki.
Przykładowe dane są następujące:
![]() |
8 1 2 1 1 5 3 2 3 2 2 5 2 3 4 2 4 1 4 4 5 5 5 3 6 |
Napisz program, który dla każdego wierzchołka grafu wyliczy różnicę sumy wag krawędzi wchodzących do wierzchołka i sumy wag krawędzi wychodzących z wierzchołka. Wyniki nanieś na rysunek grafu.
Reprezentacja grafu macierzą sąsiedztwa jest bardzo prosta, lecz pamięciożerna. Macierz wymaga pamięci rzędu O(n2), gdzie n oznacza liczbę wierzchołków w grafie. Macierz w czasie O(1) daje odpowiedź na pytanie, czy wierzchołek v łączy się z wierzchołkiem u - wystarczy sprawdzić komórkę w wierszu v i kolumnie u. Jednakże wyszukiwanie wszystkich sąsiadów wierzchołka v wymaga przeglądnięcia całego wiersza, zatem ma złożoność O(n).
Przy przetwarzaniu grafów stosuje się często inną reprezentację. Tworzy się tablicę list - czyli tablicę, której elementami są listy. Tablica posiada tyle elementów ile w grafie jest wierzchołków. Zatem poszczególne elementy tablicy odpowiadają poszczególnym wierzchołkom grafu. Na liście wierzchołka umieszcza się numery wierzchołków, które są połączone krawędzią z tym wierzchołkiem. Tablice list nazywamy listami sąsiedztwa (ang. Adjancency Lists).
Graf skierowany:
![]() |
|
Graf nieskierowany - każdą krawędź traktujemy jako dwie krawędzie skierowane przeciwnie:
![]() |
|
Multigraf - w reprezentacji listami sąsiedztwa krawędzie mogą się w naturalny sposób powtarzać - na liście po prostu umieszczamy wierzchołek docelowy tyle razy, ile jest do niego krawędzi z danego wierzchołka wyjściowego. Nie występuje także żaden problem dla pętli.
![]() |
|
Graf ważony - elementy list sąsiedztwa są dwuskładnikowe. Pierwszy składnik jest numerem wierzchołka docelowego krawędzi, a drugi składnik jest wagą tej krawędzi.
![]() |
|
W porównaniu z macierzami sąsiedztwa listy sąsiedztwa są bardziej efektywne pamięciowo - złożoność klasy O(m), gdzie m oznacza liczbę krawędzi grafu. Listy sąsiedztwa nadają się doskonale do zapamiętywania dużych grafów. Szybciej uzyskujemy dostęp do sąsiadów danego wierzchołka - mamy ich przecież bezpośrednio na liście. Jednakże sprawdzenie, czy dane dwa wierzchołki u i v łączy krawędź wymaga przejrzenia list sąsiedztwa wierzchołka v i u, nie jest to zatem operacja wykonywana w czasie stałym.
Poniższy program odczytuje z konsoli znakowej definicję grafu nieskierowanego definicję grafu podaną za pomocą krawędzi jak wyżej. Liczba wierzchołków jest ograniczona do 32. Następnie dla każdego wierzchołka grafu program wypisuje wszystkich jego sąsiadów.
Uwaga: Wszystkie programy są tworzone w środowisku Dev-C++.
// Kurs teorii grafów dla początkujących //(C)2009 I LO w Tarnowie //-------------------------------------- #include <iostream> #include <list> using namespace std; const int VMAX = 32; // maksymalna liczba wierzchołków // Tablicę list sąsiedztwa tworzymy o wymiarze o 1 większym od VMAX, // aby zachować numerację wierzchołków od 1 do VMAX list<int> AL[VMAX+1]; // tablica list sąsiedztwa int n; // faktyczna liczba wierzchołków w grafie int m; // liczba krawędzi // Funkcja odczytuje definicję grafu // ze standardowego wejścia //---------------------------------- void read_AL() { n = 0; cin >> m; for(int i = 0; i < m; i++) { int v1,v2; cin >> v1 >> v2; if(v1 > n) n = v1; if(v2 > n) n = v2; AL[v1].push_back(v2); // v2 umieszczamy na liście wierzchołka v1 AL[v2].push_back(v1); // v1 umieszczamy na liście wierzchołka v2 } } // Funkcja wypisuje sąsiadów każdego wierzchołka //---------------------------------------------- void show_neighbours() { for(int i = 1; i <= n; i++) { cout << i << ": "; for(list<int>::iterator j = AL[i].begin(); j != AL[i].end(); j++) cout << *j << " "; cout << endl; } } // Program główny //--------------- main() { read_AL(); // wczytujemy definicję grafu cout << endl; show_neighbours(); // wyświetlamy sąsiadów cout << endl << endl; system("PAUSE"); } |
10 1 3 1 5 2 3 2 5 2 6 3 4 3 6 4 5 4 7 6 7 1: 3 5 2: 3 5 6 3: 1 2 4 6 4: 3 5 7 5: 1 2 4 6: 2 3 7 7: 4 6 |
Postaraj się wykonać dla reprezentacji listami sąsiedztwa te same zadania co dla reprezentacji macierzą sąsiedztwa.
![]() | I Liceum Ogólnokształcące |
Pytania proszę przesyłać na adres email: i-lo@eduinf.waw.pl
W artykułach serwisu są używane cookies. Jeśli nie chcesz ich otrzymywać,
zablokuj je w swojej przeglądarce.
Informacje dodatkowe