26
Struktury danych w C# Część 5. Od drzew do grafów Scott Mitchell 4GuysFromRolla.com marzec 2004 Streszczenie: Graf — podobnie jak drzewo — jest zbiorem węzłów i krawędzi. Jednak w przypadku grafów nie ma żadnych zasad dotyczących sposobu połączenia poszczególnych węzłów za pomocą krawędzi. W tym artykule — piątym już z serii poświęconej strukturom danych — zaprezentowano grafy, należące do najbardziej wszechstronnych i uniwersalnych struktur danych. Długość dokumentu — około 25 stron drukowanych. Pobierz kod przykładów — Graphs.msi . Spis treści Wprowadzenie Analiza różnych typów krawędzi Tworzenie klasy grafu w C# Często stosowane algorytmy grafowe Podsumowanie Bibliografia Wprowadzenie Pierwszy i drugi artykuł tej serii poświęciliśmy liniowym strukturom danych — tablicy, klasie ArrayList, kolejce, stosowi i tablicy z haszowaniem. W trzecim artykule rozpoczęliśmy analizę struktur drzewiastych. Jak pamiętamy, drzewa składają się ze zbioru węzłów i wszystkie węzły są ze sobą połączone. Połączenia pomiędzy węzłami nazywane są krawędziami. Połączenia w drzewach muszą spełniać wiele różnych warunków. Na przykład wszystkie węzły w drzewie (poza korzeniem) muszą posiadać dokładnie jednego rodzica, a każdy węzeł może posiadać dowolną liczbę dzieci. Dzięki tym prostym regułom, dla każdego drzewa prawdziwe są stwierdzenia wypisane poniżej: 1. Rozpoczynając chodzenie po drzewie od dowolnego węzła, można dotrzeć do każdego innego węzła w drzewie. Oznacza to, że nie istnieje węzeł, do którego nie prowadzi żadna ścieżka. 2. W drzewie nie ma cykli. Cykl istnieje wtedy, gdy zaczynamy chodzenie po drzewie od pewnego węzła v i idąc ścieżką prowadzącą przez zbiór węzłów v 1 , v 2 , ..., v k z powrotem trafiamy do węzła v. 3. Liczba krawędzi w drzewie jest mniejsza o jeden od liczby węzłów w drzewie.

5_Struktury danych - od drzew do grafów

  • Upload
    bodekwr

  • View
    949

  • Download
    2

Embed Size (px)

Citation preview

Page 1: 5_Struktury danych - od drzew do grafów

Struktury danych w C#

Część 5. Od drzew do grafów

Scott Mitchell

4GuysFromRolla.com

marzec 2004

Streszczenie:

Graf — podobnie jak drzewo — jest zbiorem węzłów i krawędzi. Jednak w przypadku grafów nie ma żadnych

zasad dotyczących sposobu połączenia poszczególnych węzłów za pomocą krawędzi. W tym artykule — piątym

już z serii poświęconej strukturom danych — zaprezentowano grafy, należące do najbardziej wszechstronnych

i uniwersalnych struktur danych.

Długość dokumentu — około 25 stron drukowanych.

Pobierz kod przykładów — Graphs.msi.

Spis treści

Wprowadzenie

Analiza różnych typów krawędzi

Tworzenie klasy grafu w   C#

Często stosowane algorytmy grafowe

Podsumowanie

Bibliografia

Wprowadzenie

Pierwszy i drugi artykuł tej serii poświęciliśmy liniowym strukturom danych — tablicy, klasie ArrayList, kolejce,

stosowi i tablicy z haszowaniem. W trzecim artykule rozpoczęliśmy analizę struktur drzewiastych. Jak

pamiętamy, drzewa składają się ze zbioru węzłów i wszystkie węzły są ze sobą połączone. Połączenia pomiędzy

węzłami nazywane są krawędziami. Połączenia w drzewach muszą spełniać wiele różnych warunków. Na

przykład wszystkie węzły w drzewie (poza korzeniem) muszą posiadać dokładnie jednego rodzica, a każdy węzeł

może posiadać dowolną liczbę dzieci. Dzięki tym prostym regułom, dla każdego drzewa prawdziwe są

stwierdzenia wypisane poniżej:

1. Rozpoczynając chodzenie po drzewie od dowolnego węzła, można dotrzeć do każdego innego węzła w drzewie. Oznacza to, że nie istnieje węzeł, do którego nie prowadzi żadna ścieżka.

2. W drzewie nie ma cykli. Cykl istnieje wtedy, gdy zaczynamy chodzenie po drzewie od pewnego węzła v i idąc ścieżką prowadzącą przez zbiór węzłów v1, v2, ..., vk z powrotem trafiamy do węzła v.

3. Liczba krawędzi w drzewie jest mniejsza o jeden od liczby węzłów w drzewie.

W dalszej części trzeciego artykułu omówione zostały drzewa binarne, które są specjalnym rodzajem drzew.

W drzewach binarnych węzeł może mieć najwyżej dwoje dzieci.

Page 2: 5_Struktury danych - od drzew do grafów

W tym artykule zajmiemy się grafami. Grafy — tak jak drzewa — składają się z węzłów (nazywanych też

wierzchołkami) i krawędzi, ale — w przeciwieństwie do drzew — węzły mogą być połączone w dowolny sposób.

W przypadku grafów nie istnieje pojęcie węzła-korzenia ani pojęcie węzłów-rodziców. Graf można raczej opisać

jako zbiór połączonych węzłów.

Uwaga   Wszystkie drzewa są oczywiście grafami. Drzewo to specjalny przypadek grafu, w którym wszystkie

węzły są dostępne z węzła wyjściowego i w którym nie ma cykli.

Na ilustracji 1. przedstawiono trzy przykłady grafów. Warto zwrócić uwagę, że w przeciwieństwie do drzew,

w grafach mogą istnieć zbiory węzłów, które nie są powiązane z pozostałymi zbiorami węzłów. Na przykład

w grafie (a) istnieją dwa odrębne zbiory węzłów. W grafach mogą także występować cykle — w grafie (b) istnieje

nawet kilka cykli. Jeden cykl to ścieżka idąca od węzła v1 przez v2 do v4 i z powrotem do węzła v1. Kolejny cykl to

ścieżka łącząca węzły v1, v2, v3, v5, v4 i idąca z powrotem do v1. Cykle istnieją także w grafie (a). W grafie (c) nie

ma żadnych cykli, liczba krawędzi w tym grafie jest o jeden mniejsza od liczby węzłów i wszystkie węzły są

osiągalne. Dlatego graf (c) jest drzewem.

Ilustracja 1. Trzy przykłady grafów

Za pomocą grafów można zaprezentować wiele rzeczywistych problemów. Na przykład wyszukiwarki

internetowe takie jak Google modelują Internet w postaci grafu — strony internetowe są węzłami grafu,

a łączące strony odnośniki są krawędziami grafu. Programy typu Microsoft MapPoint, generujące trasy przejazdu

z miasta do miasta, także wykorzystują grafy — miasta odpowiadają węzłom grafu, a drogi łączące miasta są

krawędziami grafu.

Analiza różnych typów krawędzi

Zbiór węzłów i krawędzi to najprostsza definicja grafu. Grafy mogą jednak mieć krawędzie kilku typów:

krawędzie skierowane i krawędzie nieskierowane,

krawędzie z wagami i krawędzie bez wag.

Opisując sposób wykorzystania grafu do przedstawienia jakiegoś problemu trzeba wskazać typ zastosowanego

grafu — czy jest to graf skierowany ważony, czy też jest to graf nieskierowany, ale również z wagami.

W następnej sekcji omówimy, czym różnią się krawędzie skierowane i nieskierowane oraz krawędzie z wagami

i bez wag.

Page 3: 5_Struktury danych - od drzew do grafów

Krawędzie skierowane i nieskierowane

Krawędzie grafu są połączeniami pomiędzy węzłami. Domyślnie krawędź jest dwukierunkowa. Oznacza to, że

jeśli pomiędzy węzłami v i u istnieje krawędź, to można nią przejść zarówno od węzła v do u, jak i od węzła u do

v. Grafy z krawędziami dwukierunkowymi nazywane są grafami nieskierowanymi, ponieważ sposób przejścia po

krawędzi nie jest ograniczony tylko do jednego wyraźnego kierunku.

W niektórych przypadkach w grafie mogą występować jednokierunkowe połączenia pomiędzy węzłami. Na

przykład przy tworzeniu modelu Internetu w postaci grafu, odnośnik ze strony internetowej v do strony u

stanowiłby jednokierunkową krawędź od węzła v do węzła u. Oznacza to, że można przejść z v do u, ale nie

można przejść w drugą stronę — z u do v. Graf, w którym krawędzie są jednokierunkowe, nazywany jest grafem

skierowanym.

Gdy rysujemy graf, krawędzie dwukierunkowe kreślone są jako zwykłe linie (tak jak na ilustracji 1.). Krawędzie

jednokierunkowe rysowane są jako strzałki, których grot wskazuje kierunek krawędzi. Na ilustracji 2.

przedstawiono graf skierowany, w którym strony internetowe witryny przedstawiono jako węzły i poprowadzono

pomiędzy nimi skierowane krawędzie. Krawędź skierowana od u do v oznacza, że na stronie internetowej u

znajduje się odnośnik do strony internetowej v. Jeśli i strona u odwołuje się do strony v, i strona v odwołuje się

do strony u, to na ilustracji umieszczane są dwie strzałki — jedna od v do u, a druga od u do v.

Ilustracja 2. Model stron składających się na witrynę

Krawędzie z wagami i bez wag

Zazwyczaj grafy służą do przedstawienia zbioru elementów oraz relacji pomiędzy tymi elementami. Na przykład

graf z ilustracji 2. przedstawia zbiór stron składających się na witrynę oraz odnośniki umożliwiające poruszanie

się pomiędzy tymi stronami. Czasami jednak ważne jest, by połączeniu dwóch węzłów przypisać jakiś koszt.

Mapę można w bardzo prosty sposób przedstawić jako graf — miasta to węzły, a drogi łączące miasta to

krawędzie. Jeśli chcemy ustalić najkrótszą trasę przejazdu z jednego miasta do drugiego, to do poszczególnych

Page 4: 5_Struktury danych - od drzew do grafów

krawędzi musimy przyporządkować koszt przejazdu z miasta do miasta. Logicznym rozwiązaniem jest

przypisanie do każdej krawędzi wagi, która może być na przykład równa odległości dzielącej dwa miasta.

Na ilustracji 3. widoczny jest graf przedstawiający kilka miast południowej Kalifornii. Koszt związany z trasą

prowadzącą od jednego miasta do drugiego jest sumą kosztów przypisanych do krawędzi składających się na

daną trasę. Najkrótsza trasa to taka, z którą związany jest najmniejszy koszt. Korzystając z przykładowej

ilustracji, możemy wyznaczyć długość trasy z San Diego do Santa Barbara. Może ona wynosić 210 mil, jeśli

zamierzamy przejechać przez Riverside i Barstow. Jednak najkrótsza trasa prowadzi przez Los Angeles i ma tylko

130 mil.

Ilustracja 3. Graf, w którym węzły są miastami stanu Kalifornia, a waga krawędzi równa jest liczbie

mil

Kierunek i waga krawędzi nie zależą od siebie, dlatego graf może mieć krawędzie jednego z czterech

następujących typów:

skierowane z wagami,

skierowane bez wag,

nieskierowane z wagami,

nieskierowane bez wag.

Grafy na ilustracji 1. to grafy nieskierowane bez wag. Na ilustracji 2. przedstawiono grafy skierowane bez wag,

a na ilustracji 3. przedstawiono ważony graf nieskierowany.

Grafy rzadkie i grafy gęste

Graf może nie mieć wcale krawędzi lub może mieć ich wiele, jednak typowy graf ma więcej krawędzi niż węzłów.

Jaka jest maksymalna liczba wszystkich krawędzi w grafie o n węzłach? Zależy to od tego, czy graf jest

skierowany. Jeśli graf jest skierowany, to każdy węzeł może być połączony krawędzią z każdym innym węzłem.

Page 5: 5_Struktury danych - od drzew do grafów

Oznacza to, że każdy z n węzłów posiada n – 1 krawędzi, co w sumie daje n * (n – 1) krawędzi (czyli wartość

bliską wartości n2).

Uwaga   W artykule tym przyjmuję, że węzeł nie może posiadać krawędzi prowadzących do samego siebie. Jednak w teorii grafów dopuszcza się istnienie krawędzi prowadzących od węzła v do węzła v (tzw. pętli). Jeśli w grafie dopuszcza się istnienie pętli, to maksymalna liczba wszystkich krawędzi w grafie skierowanym wynosi n2.

Jeśli graf nie jest skierowany, to jeden węzeł — nazwijmy go v1 — może mieć krawędzie do wszystkich

pozostałych węzłów, czyli możne wychodzić z niego n – 1 krawędzi. Kolejny węzeł — nazwijmy go v2 — może

mieć najwyżej n – 2 krawędzie, ponieważ istnieje już krawędź łącząca ten węzeł z węzłem v1. Trzeci węzeł — v3

— może mieć co najwyżej n – 3 krawędzie i tak dalej. Dlatego dla n węzłów w grafie nieskierowanym może być

co najwyżej (n – 1) + (n – 2) + ... + 1 krawędzi. Po zsumowaniu tego wyrażenia otrzymamy wynik

[n * (n - 1)] / 2. Czyli w grafie skierowanym może być co najwyżej dwa razy więcej krawędzi niż w grafie

nieskierowanym.

Graf rzadki to graf, w którym jest znacznie mniej niż n2 krawędzi. Na przykład graf o n węzłach i n krawędziach

lub nawet 2n krawędziach to graf rzadki. Graf, w którym liczba krawędzi jest bliska maksymalnej liczbie

krawędzi, nazywany jest grafem gęstym.

Planując wykorzystanie grafu w algorytmie dobrze jest znać stosunek liczby węzłów do liczby krawędzi. Jak

dowiemy się z dalszej części tego artykułu, asymptotyczna złożoność obliczeniowa operacji przeprowadzanych

na grafie zależy w dużym stopniu od liczby krawędzi i liczby węzłów w grafie.

Tworzenie klasy grafu w C#

Grafy są strukturą danych powszechnie stosowaną w rozwiązaniach bardzo wielu różnych problemów, jednak

nie ma takiej struktury w środowisku .NET Framework. Przyczyną tego jest między innymi to, że efektywna

implementacja klasy grafów zależy od wielu czynników związanych z rozwiązywanym problemem. Grafy są

zwykle modelowane poprzez:

listę sąsiedztwa,

macierz sąsiedztwa.

Te dwie metody różnią się sposobem wewnętrznej reprezentacji węzłów i krawędzi grafu w klasie grafu.

Przyjrzyjmy się obydwu metodom i poznajmy ich zalety oraz wady.

Przedstawienie grafu w postaci list sąsiedztwa

W trzecim artykule opisałem tworzenie w języku C# klasy drzew binarnych o nazwie BinaryTree. Każdy węzeł

w drzewie binarnym był instancją klasy Node. Klasa Node zawierała trzy właściwości:

Value — zmienna typu object, w której przechowywana jest wartość węzła,

Left — referencja do lewego dziecka węzła,

Right — referencja do prawego dziecka węzła.

Page 6: 5_Struktury danych - od drzew do grafów

Klasy Node oraz BinaryTree nie są wystarczająco rozbudowane, by móc służyć za implementację grafu. Po

pierwsze, klasa Node drzewa binarnego dopuszcza tylko dwie krawędzie wychodzące z danego węzła — do

lewego i do prawego dziecka. Po drugie, w klasie BinaryTree można ustawić referencję tylko do jednego węzła

— korzenia drzewa. Niestety w przypadku grafu jest to niewystarczające — w klasie grafu musi istnieć

możliwość dodania referencji do wszystkich węzłów grafu.

Jednym z rozwiązań jest utworzenie klasy Node zawierającej tablicę obiektów typu Node, w której będą

zapisywani sąsiedzi danego węzła. Klasa Graph także musiałaby zawierać tablicę instancji klasy Node, w której

zapisane byłyby wszystkie węzły grafu. Rozwiązanie takie nazywane jest listami sąsiedztwa, ponieważ każdy

węzeł zawiera listę sąsiednich węzłów (z którymi jest bezpośrednio połączony). Na ilustracji 4. przedstawiono

listy sąsiedztwa w postaci graficznej.

Page 7: 5_Struktury danych - od drzew do grafów

Ilustracja 4. Reprezentacja grafu w postaci list sąsiedztwa

W przypadku grafu nieskierowanego, na listach sąsiedztwa znajdują się zduplikowane dane o krawędziach. Na

przykład w reprezentacji grafu (b) z ilustracji 4., w liście sąsiedztwa węzła a jest wpisany węzeł b, a w liście

sąsiedztwa węzła b znajduje się węzeł a.

W liście sąsiedztwa każdego węzła znajduje się dokładnie tyle węzłów, z iloma węzłami dany węzeł jest

powiązany. Dlatego lista sąsiedztwa to bardzo wydajna pod względem pamięciowym forma przedstawienia grafu

— przechowywane są w niej tylko potrzebne dane. W szczególności dla grafu z V wierzchołkami i E krawędziami

potrzebnych jest V + E instancji klasy Node w przypadku grafu skierowanego, a w przypadku grafu

nieskierowanego V + 2E instancji klasy Node.

Chociaż nie wynika to z ilustracji czwartej, listę sąsiedztwa można także wykorzystać do przedstawienia grafu

z wagami. Jedyną różnicą jest to, że w liście sąsiedztwa dla każdego węzła n każda instancja klasy Node musi

także zawierać informację o koszcie związanym z przejściem krawędzi z węzła n.

Wadą listy sąsiedztwa jest to, że chcąc sprawdzić, czy istnieje krawędź z węzła u do węzła v, trzeba przeszukać

listę sąsiedztwa węzła u. W przypadku gęstych grafów lista sąsiedztwa węzła u będzie długa — ustalenie, czy

istnieje krawędź pomiędzy dwoma węzłami, ma liniową złożoność obliczeniową. Na szczęście przy korzystaniu

z grafów rzadko istnieje potrzeba sprawdzenia, czy istnieje krawędź pomiędzy dwoma konkretnymi węzłami.

Częściej konieczne będzie raczej wypisanie wszystkich krawędzi wychodzących z danego węzła.

Page 8: 5_Struktury danych - od drzew do grafów

Przedstawienie grafu jako macierzy sąsiedztwa

Innym sposobem przedstawienia grafu jest zastosowanie macierzy sąsiedztwa. W przypadku grafu z n węzłami,

macierz sąsiedztwa jest dwuwymiarową tablicą o rozmiarze n × n. Jeśli macierz ma reprezentować graf ważony,

to element macierzy o współrzędnych (u, v) ma wartość wagi krawędzi od u do v (lub na przykład -1, jeśli nie

istnieje krawędź z u do v). W macierzy sąsiedztwa dla grafu bez wag, macierz może zawierać wartości

boolowskie — wartość True na pozycji (u, v) oznacza, że istnieje krawędź z u do v, a wartość False oznacza,

że taka krawędź nie istnieje.

Na ilustracji 5. przedstawiono reprezentację grafu w postaci macierzy sąsiedztwa.

Ilustracja 5. Reprezentacja grafu w postaci macierzy sąsiedztwa

W przypadku grafów nieskierowanych macierz sąsiedztwa jest symetryczna względem głównej przekątnej.

Oznacza to, że jeśli w grafie nieskierowanym istnieje krawędź pomiędzy węzłami u i v, to w tablicy macierzy

sąsiedztwa znajdą się dwa odpowiadające sobie wpisy na pozycjach (u, v) oraz (v, u).

Ponieważ ustalenie, czy istnieje krawędź pomiędzy danymi dwoma węzłami, polega na zwykłym sprawdzeniu

wartości w tablicy, operacja ta przeprowadzana jest w stałym czasie. Wadą macierzy sąsiedztwa jest

zajmowanie dużej ilości pamięci. Macierz sąsiedztwa jest zapisywana w postaci tablicy zawierającej

n2 elementów, a więc w przypadku rzadkich grafów wiele pozycji w tablicy będzie pustych. W przypadku grafów

nieskierowanych połowa danych to informacje powtórzone.

Page 9: 5_Struktury danych - od drzew do grafów

Chociaż w tworzonej klasie Graph moglibyśmy zastosować dowolną z form reprezentacji grafu (macierz

sąsiedztwa lub listy sąsiedztwa), zdecydowałem się na zastosowanie modelu list sąsiedztwa. Wybrałem to

rozwiązanie, ponieważ jest ono logicznym rozszerzeniem klas Node i BinaryTree, utworzonych w poprzednich

artykułach tej serii.

Tworzenie klasy Node

Klasa Node reprezentuje pojedynczy węzeł grafu. W grafach węzły zazwyczaj reprezentują jakiś obiekt. Dlatego

klasa Node zawiera właściwość Data o typie danych Object. We właściwości tej mogą być zapisane dowolne

dane skojarzone z węzłem. Trzeba także umożliwić prostą identyfikację poszczególnych węzłów, dlatego do

klasy węzła dodamy właściwość typu string o nazwie Key, która będzie unikalnym identyfikatorem każdego

węzła.

Zdecydowaliśmy się na reprezentację grafu poprzez listy sąsiedztwa, dlatego każdy egzemplarz klasy Node

musi mieć listę swoich sąsiadów. Jeśli przedstawiamy graf ważony, lista sąsiedztwa musi także zawierać

informacje o wadze poszczególnych krawędzi. Aby umożliwić stosowanie i zarządzanie listami sąsiedztwa,

utworzymy klasę AdjacencyList.

Klasy AdjacencyList oraz EdgeToNeighbor

Węzeł Node zawiera egzemplarz klasy AdjacencyList, który przechowuje informacje o krawędziach

prowadzących do sąsiadów danego węzła. Skoro klasa AdjacencyList przechowuje zbiór krawędzi, to najpierw

musimy utworzyć klasę reprezentującą krawędź. Klasa ta będzie reprezentować krawędź do węzła-sąsiada,

nazwijmy ją więc EdgeToNeighbor. Do każdej krawędzi możemy chcieć przypisać jakąś wagę, dlatego klasa

EdgeToNeighbor powinna zawierać dwie właściwości:

Cost — liczba całkowita będąca wartością wagi danej krawędzi,

Neighbor — referencja do węzła-sąsiada.

Klasa AdjacencyList dziedziczy po klasie System.Collections.CollectionBase i jest silnie typowanym

zbiorem instancji klasy EdgeToNeighbor. Kod klas EdgeToNeighbor i AdjacencyList jest następujący:

public class EdgeToNeighbor{ // prywatne zmienne składowe private int cost; private Node neighbor;

public EdgeToNeighbor(Node neighbor) : this(neighbor, 0) {}

public EdgeToNeighbor(Node neighbor, int cost) { this.cost = cost; this.neighbor = neighbor; }

public virtual int Cost { get { return cost; } }

Page 10: 5_Struktury danych - od drzew do grafów

public virtual Node Neighbor { get { return neighbor; } }}

public class AdjacencyList : CollectionBase{ protected internal virtual void Add(EdgeToNeighbor e) { base.InnerList.Add(e); }

public virtual EdgeToNeighbor this[int index] { get { return (EdgeToNeighbor) base.InnerList[index]; } set { base.InnerList[index] = value; } }}

Właściwość Neighbors klasy Node umożliwia dostęp do wewnętrznej składowej AdjacencyList. Metoda

Add() klasy AdjacencyList jest oznaczona jako internal, co oznacza, że krawędzie do listy sąsiedztwa

węzła mogą być dopisywane wyłącznie przez klasy znajdujące się w tym samym podzespole. Klasy zostały

opracowane tak, że programista korzystający z klasy Graph może modyfikować strukturę grafu wyłącznie za

pośrednictwem metod składowych klasy Graph, a nie poprzez właściwość Neighbors węzła.

Dodawanie krawędzi do węzła

Oprócz właściwości Key, Data i Neighbors klasa Node musi zawierać także metodę, umożliwiającą

programiście zajmującemu się klasą Graph dodanie krawędzi prowadzącej od danego węzła do sąsiada. Jak

pamiętamy z opisu podejścia wykorzystującego listy sąsiedztwa, jeśli istnieje nieskierowana krawędź pomiędzy

węzłami u i v, to u w swojej liście sąsiedztwa będzie miało referencję do v, natomiast v będzie miało w swojej

liście sąsiedztwa referencję do u. Każdy egzemplarz klasy Node powinien być odpowiedzialny za

przechowywanie wyłącznie listy sąsiedztwa reprezentowanego przez siebie węzła, a nie list sąsiedztwa innych

węzłów w grafie. Jak za chwilę zobaczymy, klasa Graph zawiera metody umożliwiające dodanie albo

skierowanej, albo nieskierowanej krawędzi pomiędzy dwoma węzłami.

Aby ułatwić implementację metody klasy Graph, służącej do wstawiania krawędzi pomiędzy dwa węzły, klasa

Node zawiera metodę dodającą skierowaną krawędź, prowadzącą od danego węzła do określonego sąsiada.

Metoda AddDirected(), przyjmuje jako argument instancję klasy Node oraz opcjonalny parametr wagi,

następnie tworzy instancję klasy EdgeToNeighbor i dodaje ją do listy sąsiedztwa danego węzła. Opisanemu

powyżej procesowi odpowiada następujący kod:

protected internal virtual void AddDirected(Node n){ AddDirected(new EdgeToNeighbor(n));}

protected internal virtual void AddDirected(Node n, int cost){ AddDirected(new EdgeToNeighbor(n, cost));}

protected internal virtual void AddDirected(EdgeToNeighbor e){

Page 11: 5_Struktury danych - od drzew do grafów

neighbors.Add(e);}

Tworzenie klasy Graph

Jak pamiętamy, metoda list sąsiedztwa wymaga, by w klasie grafu była zapisana lista wszystkich węzłów tego

grafu. Z kolei każdy węzeł przechowuje listę węzłów sąsiednich. A więc w klasie Graph musimy umieścić listę

wszystkich węzłów. Moglibyśmy je zapisać w tablicy ArrayList, jednak lepszym rozwiązaniem będzie tablica

z haszowaniem. Argumentem za zastosowaniem tablicy z haszowaniem jest to, że metody klasy Graph,

wykorzystywane do dodawania krawędzi, muszą sprawdzić, czy w grafie na pewno znajdują się węzły, które

mają zostać połączone krawędzią. Jeśli zapisywalibyśmy węzły w tablicy ArrayList, to musielibyśmy przeszukać

całą tablicę (złożoność liniowa!). W przypadku tablicy z haszowaniem sprawdzenie, czy dane węzły istnieją,

wykonywane jest w stałym czasie. Więcej informacji na temat tablicy z haszowaniem i charakteryzującej ją

złożoności obliczeniowej poszczególnych operacji można znaleźć w drugim artykule tej serii.

Pokazana poniżej klasa NodeList zawiera silnie typowane metody Add() i Remove(), służące do dodawania

i usuwania węzłów z grafu. Klasa ta zawiera także metodę ContainsKey(), która umożliwia sprawdzenie, czy

w grafie istnieje węzeł o określonej wartości klucza.

public class NodeList : IEnumerable{ // prywatne zmienne składowe private Hashtable data = new Hashtable();

// metody public virtual void Add(Node n) { data.Add(n.Key, n); }

public virtual void Remove(Node n) { data.Remove(n.Key); }

public virtual bool ContainsKey(string key) { return data.ContainsKey(key); }

public virtual void Clear() { data.Clear(); }

// właściwości... public virtual Node this[string key] { get { return (Node) data[key]; } } // ... niektóre metody i właściwości pominięto // dla zachowania czytelności kodu}

Klasa Graph zawiera publiczną właściwość Nodes typu NodeList. Co więcej, klasa Graph zawiera kilka

metod służących do dodawania krawędzi skierowanych lub nieskierowanych, z wagami lub bez wag pomiędzy

Page 12: 5_Struktury danych - od drzew do grafów

dwa istniejące w grafie węzły. Metoda AddDirectedEdge() przyjmuje jako parametry wejściowe dwa obiekty

typu Node oraz opcjonalny parametr wagi, a następnie tworzy krawędź skierowaną od pierwszego węzła do

drugiego. Podobnie metoda AddUndirectedEdge() przyjmuje jako parametry wejściowe dwa obiekty typu

Node oraz opcjonalny parametr wagi, a następnie dodaje krawędź skierowaną od pierwszego węzła do

drugiego, a także krawędź skierowaną od drugiego węzła do pierwszego.

Oprócz metod służących do dodawania krawędzi, klasa Graph zawiera także metodę Contains(), która zwraca

wartość boolowską wskazującą, czy dany węzeł istnieje w grafie. Poniżej zostały zamieszczone najważniejsze

fragmenty kodu klasy Graph:

public class Graph{ // prywatne zmienne składowe private NodeList nodes;

public Graph() { this.nodes = new NodeList(); }

public virtual Node AddNode(string key, object data) { // upewniamy się, że klucz jest unikalny if (!nodes.ContainsKey(key)) { Node n = new Node(key, data); nodes.Add(n); return n; } else throw new ArgumentException("W grafie istnieje już węzeł o kluczu " + key); }

public virtual void AddNode(Node n) { // upewniamy się, że węzeł jest unikalny if (!nodes.ContainsKey(n.Key)) nodes.Add(n); else throw new ArgumentException("W grafie istnieje już węzeł o kluczu " + n.Key); }

public virtual void AddDirectedEdge(string uKey, string vKey) { AddDirectedEdge(uKey, vKey, 0); }

public virtual void AddDirectedEdge(string uKey, string vKey, int cost) { // sprawdzamy referencje do węzłów o kluczach uKey i vKey if (nodes.ContainsKey(uKey) && nodes.ContainsKey(vKey)) AddDirectedEdge(nodes[uKey], nodes[vKey], cost); else throw new ArgumentException("Co najmniej jeden z podanych węzłów nie znajduje się w grafie."); }

public virtual void AddDirectedEdge(Node u, Node v) { AddDirectedEdge(u, v, 0); }

public virtual void AddDirectedEdge(Node u, Node v, int cost)

Page 13: 5_Struktury danych - od drzew do grafów

{ // Sprawdzamy, czy u i v należą do grafu if (nodes.ContainsKey(u.Key) && nodes.ContainsKey(v.Key)) // dodajemy krawędź u -> v u.AddDirected(v, cost); else // co najmniej jeden z węzłów nie został znaleziony w grafie throw new ArgumentException("Co najmniej jeden z podanych węzłów nie znajduje się w grafie."); }

public virtual void AddUndirectedEdge(string uKey, string vKey) { AddUndirectedEdge(uKey, vKey, 0); }

public virtual void AddUndirectedEdge(string uKey, string vKey, int cost) { // sprawdzamy referencje do węzłów o kluczach uKey i vKey if (nodes.ContainsKey(uKey) && nodes.ContainsKey(vKey)) AddUndirectedEdge(nodes[uKey], nodes[vKey], cost); else throw new ArgumentException("Co najmniej jeden z podanych węzłów nie znajduje się w grafie."); }

public virtual void AddUndirectedEdge(Node u, Node v) { AddUndirectedEdge(u, v, 0); }

public virtual void AddUndirectedEdge(Node u, Node v, int cost) { // Sprawdzamy, czy u i v należą do grafu if (nodes.ContainsKey(u.Key) && nodes.ContainsKey(v.Key)) { // Dodajemy krawędź u -> v oraz v -> u u.AddDirected(v, cost); v.AddDirected(u, cost); } else // co najmniej jeden z węzłów nie został znaleziony w grafie throw new ArgumentException("Co najmniej jeden z podanych węzłów nie znajduje się w grafie."); }

public virtual bool Contains(Node n) { return Contains(n.Key); }

public virtual bool Contains(string key) { return nodes.ContainsKey(key); }

public virtual NodeList Nodes { get { return this.nodes; } }}

Zarówno metoda AddDirectedEdge(), jak i metoda AddUndirectedEdge(), sprawdzają, czy wskazane węzły

istnieją w grafie. Jeśli węzły te nie istnieją w grafie, zgłaszany jest wyjątek ArgumentException. Każda

z tych metod jest także przeciążona. Krawędź można dodać przekazując referencje do dwóch węzłów lub

podając klucze węzłów, pomiędzy którymi ma zostać ta krawędź wstawiona.

Page 14: 5_Struktury danych - od drzew do grafów

Stosowanie klasy Graph

Utworzyliśmy wszystkie klasy potrzebne dla naszej struktury danych grafu. Za chwilę zajmiemy się powszechnie

stosowanymi algorytmami grafowymi, takimi jak tworzenie minimalnego drzewa rozpinającego i znajdowanie

najkrótszej ścieżki z jednego węzła do innych. Ale zanim przejdziemy do tych zagadnień, przyjrzymy się

sposobowi stosowania utworzonej klasy Graph w prostej aplikacji C#.

Najpierw należy utworzyć egzemplarz klasy Graph. Następnie należy dodać do grafu węzły. Wiąże się to

z wywołaniem metody AddNode() klasy Graph dla każdego dodawanego do grafu węzła. Odtwórzmy graf

z ilustracji 2. Musimy dodać do grafu sześć węzłów. Niech kluczem Key każdego z tych węzłów będzie nazwa

pliku strony internetowej. Właściwości Data każdego z węzłów zamiast danych przypiszemy pustą referencję,

chociaż moglibyśmy podać zawartość pliku lub zbiór słów kluczowych opisujących zawartość danej strony.

Graph web = new Graph();web.AddNode("Privacy.htm", null);web.AddNode("People.aspx", null);web.AddNode("About.htm", null);web.AddNode("Index.htm", null);web.AddNode("Products.aspx", null);web.AddNode("Contact.aspx", null);

Następnie należy dodać krawędzie. Tworzymy graf skierowany bez wag, dlatego dodając krawędź z u do v

będziemy stosować metodę AddDirectedEdge(u, v) klasy Graph.

web.AddDirectedEdge("People.aspx", "Privacy.htm"); // People -> Privacy

web.AddDirectedEdge("Privacy.htm", "Index.htm"); // Privacy -> Indexweb.AddDirectedEdge("Privacy.htm", "About.htm"); // Privacy -> About

web.AddDirectedEdge("About.htm", "Privacy.htm"); // About -> Privacyweb.AddDirectedEdge("About.htm", "People.aspx"); // About -> Peopleweb.AddDirectedEdge("About.htm", "Contact.aspx"); // About -> Contact

web.AddDirectedEdge("Index.htm", "About.htm"); // Index -> Aboutweb.AddDirectedEdge("Index.htm", "Contact.aspx"); // Index -> Contactsweb.AddDirectedEdge("Index.htm", "Products.aspx"); // Index -> Products

web.AddDirectedEdge("Products.aspx", "Index.htm"); // Products -> Indexweb.AddDirectedEdge("Products.aspx", "People.aspx");// Products -> People

Wynikiem wykonania powyższych poleceń jest obiekt web, reprezentujący graf przedstawiony na ilustracji 2.

Mamy już graf, możemy więc spróbować udzielić odpowiedzi na kilka pytań. W przypadku grafu z naszego

przykładu może nas na przykład interesować odpowiedź na pytanie, jaką najmniejszą liczbę odnośników musi

kliknąć użytkownik, aby ze strony głównej (Index.htm) dostać się do jakiejś innej strony. Udzielenie

odpowiedzi na takie pytanie wymaga skorzystania z algorytmów grafowych. W następnej sekcji poznamy dwa

algorytmy, często stosowane do analizy grafów ważonych:

algorytm konstruowania minimalnego drzewa rozpinającego,

algorytm wyszukiwania najkrótszej ścieżki pomiędzy dwoma węzłami.

Często stosowane algorytmy grafowe

Grafy to struktury danych, za pomocą których można odzwierciedlić wiele rzeczywistych problemów — dlatego

istnieje tak wiele algorytmów grafowych stosowanych do rozwiązywania często spotykanych problemów. Aby

poszerzyć naszą wiedzę o grafach, przyjrzyjmy się dwóm najważniejszym ich zastosowaniom.

Page 15: 5_Struktury danych - od drzew do grafów

Problem minimalnego drzewa rozpinającego

Wyobraźmy sobie, że pracujemy w firmie telefonicznej i mamy poprowadzić linie telefoniczne w wiosce, w której

jest dziesięć domów (przyporządkowano im oznaczenia od H1 do H10). Zadnie to wymaga poprowadzenia kabla,

łączącego wszystkie domy. Kabel musi dotrzeć do domu H1, H2 i tak dalej aż po dom H10. Ze względu na

przeszkody geograficzne, takie jak wzgórza, drzewa, rzeki i inne, nie można poprowadzić kabla bezpośrednio od

domu do domu.

Na ilustracji 6. przedstawiono model tego zadania w postaci grafu. Każdy węzeł to dom, a krawędzie

odpowiadają możliwym połączeniom pomiędzy poszczególnymi budynkami. Wagi przypisane poszczególnym

krawędziom to odległości dzielące domy. Naszym zadaniem jest połączenie wszystkich domów przy jak

najmniejszym zużyciu kabla telefonicznego.

Ilustracja 6. Graficzne przedstawienie zadania połączenia kablem telefonicznym 10 budynków

W przypadku spójnego, nieskierowanego grafu istnieje pewien podzbiór krawędzi, które łączą wszystkie węzły

i nie tworzą cyklu. Taki podzbiór krawędzi tworzy drzewo — liczba zawartych w nim krawędzi jest o jeden

mniejsza od liczby wierzchołków, a skonstruowany z tych krawędzi graf jest acykliczny. Drzewo takie nazywane

jest drzewem rozpinającym. Dla jednego grafu może istnieć wiele drzew rozpinających. Na ilustracji 7.

przedstawiono dwa poprawne drzewa rozpinające dla grafu z ilustracji 6. (krawędzie tworzące drzewo

rozpinające są pogrubione).

Page 16: 5_Struktury danych - od drzew do grafów

Ilustracja 7. Drzewa rozpinające grafu z ilustracji szóstej

W przypadku grafów ważonych z różnymi drzewami rozpinającymi związane są różne koszty. Koszt drzewa

rozpinającego to suma wag krawędzi składających się na to drzewo. Minimalne drzewo rozpinające to takie

drzewo rozpinające, które charakteryzuje się najmniejszym kosztem.

Istnieją dwa podstawowe sposoby rozwiązania problemu minimalnego drzewa rozpinającego. Pierwszy sposób

polega na budowaniu drzewa rozpinającego poprzez wybieranie krawędzi o minimalnej wadze w taki sposób, by

w budowanym drzewie nie powstały cykle. Rozwiązanie to przedstawiono na ilustracji 8.

Page 17: 5_Struktury danych - od drzew do grafów
Page 18: 5_Struktury danych - od drzew do grafów

Ilustracja 8. Minimalne drzewo rozpinające wykorzystujące krawędzie o najmniejszej wadze

Inny sposób wyznaczania minimalnego drzewa rozpinającego polega na podzieleniu węzłów grafu na dwa zbiory

rozłączne — zbiór węzłów znajdujących się już w drzewie i zbiór węzłów, które nie zostały jeszcze dołączone do

drzewa. W każdej iteracji do drzewa rozpinającego jest dodawana krawędź o najmniejszej wadze, łącząca węzeł

należący już do drzewa z węzłem, który nie należy jeszcze do drzewa. Pierwszy krok algorytmu polega na

losowym wybraniu pierwszego węzła. Ten sposób rozwiązania przedstawiono na ilustracji dziewiątej, gdzie jako

węzeł początkowy wybrano węzeł H1. Węzły, które zostały już dodane do zbioru węzłów należących do drzewa

rozpinającego, są zaznaczone kolorem żółtym.

Page 19: 5_Struktury danych - od drzew do grafów
Page 20: 5_Struktury danych - od drzew do grafów

Ilustracja 9. Wyszukiwanie minimalnego drzewa rozpinającego metodą Prima

Techniki przedstawione na ilustracjach 8. i 9. doprowadziły do znalezienia takiego samego minimalnego drzewa

rozpinającego. Jeśli w grafie istnieje tylko jedno minimalne drzewo rozpinające, to te dwa algorytmy dają takie

samo rozwiązanie. Jeśli jednak w grafie jest więcej minimalnych drzew rozpinających, to te dwie metody mogą

dać różne wyniki (oczywiście obydwa wyniki będą poprawne).

Uwaga   Pierwszy z przedstawionych sposobów rozwiązania został opracowany przez Josepha Kruskala w 1956 roku w laboratoriach Bella. Drugi sposób rozwiązania został opracowany w 1957 roku przez Roberta Prima — innego naukowca z laboratoriów Bella. W Internecie można znaleźć mnóstwo informacji na temat tych algorytmów, także aplety Java demonstrujące działanie algorytmów w sposób graficzny (na przykład algorytm Kruskala i algorytm Prima). Dostępne są również kody źródłowe w różnych językach programowania.

Wyznaczanie najkrótszej ścieżki z jednym źródłem

Gdy planujemy podróż samolotem, to jednym z trapiących nas problemów jest znalezienie trasy o najmniejszej

liczbie przesiadek. Raczej nikt nie lubi lecieć z Nowego Jorku do Los Angeles z przesiadkami w Chicago i Denver.

Większość osób wybrałaby samolot bezpośredni, lecący prosto z Nowego Jorku do Los Angeles — bez żadnych

przesiadek po drodze.

Wyobraźmy sobie jednak, że bardziej cenimy pieniądze niż swój czas i jesteśmy zainteresowani znalezieniem

najtańszej trasy przelotu bez względu na liczbę przesiadek. Może to oznaczać lot z Nowego Jorku do Miami,

gdzie przesiądziemy się do samolotu lecącego do Dallas, skąd z kolei polecimy do Phoenix, następnie

przesiądziemy się na samolot do San Diego, skąd w końcu polecimy do Los Angeles.

Problem ten można rozwiązać przedstawiając dostępne loty i ich ceny w postaci grafu skierowanego z wagami.

Taki graf przedstawiono na ilustracji 10.

Ilustracja 10. Graf przedstawiający dostępne loty i związane z nimi koszty

Interesuje nas wyszukanie „najkrótszej” ścieżki z Nowego Jorku do Los Angeles. Patrząc na ilustrację szybko

możemy ustalić, że najkrótsze (czyli najtańsze) połączenie prowadzi przez Chicago i San Francisco. Aby jednak

zadanie takie mogło być rozwiązane przez komputer, musimy sformułować odpowiedni algorytm.

Page 21: 5_Struktury danych - od drzew do grafów

Edgar Dijkstra — jeden z najbardziej uznanych autorytetów w dziedzinie informatyki — opracował najczęściej

wykorzystywany algorytm wyszukiwania najkrótszej ścieżki z węzła źródłowego do wszystkich innych węzłów

w skierowanym grafie z wagami. Algorytm ten — zwany algorytmem Dijkstry — działa z wykorzystaniem dwóch

tablic. W każdej tablicy istnieje rekord dla każdego węzła grafu. Te dwie tablice to:

tablica kosztu — zapisane są w niej aktualne informacje o najniższym koszcie (najkrótszej ścieżce)

pokonania trasy od źródła do każdego innego węzła w grafie,

tablica tras — dla każdego węzła n wskazuje, przez który węzeł prowadzi najkrótsza ścieżka do węzła n.

Początkowo w tablicy kosztu na wszystkich pozycjach — oprócz pozycji węzła startowego z wpisaną wartością 0

— wpisane są bardzo duże wartości (na przykład nieskończoność). Na wszystkich pozycjach w tablicy tras

wpisana jest wartość null. Algorytm pamięta także zbiór Q, zawierający węzły, które należy jeszcze sprawdzić.

Początkowo do zbioru Q należą wszystkie węzły grafu.

Algorytm wybiera (i usuwa) ze zbioru Q węzeł, dla którego w tablicy kosztu wpisana jest najmniejsza wartość.

Wybrany węzeł oznaczmy literą n, a literą d oznaczmy wartość w tablicy odległości dla węzła n. Dla każdej

krawędzi węzła n sprawdzane jest, czy suma d i kosztu przejścia z n do sąsiada jest mniejsza niż wartość

wpisana w tablicy kosztu dla tego sąsiada. Jeśli wartość ta jest mniejsza, to znaleziona została lepsza trasa do

danego węzła, więc tablice kosztu i trasy są odpowiednio aktualizowane.

Aby lepiej wyjaśnić działanie tego algorytmu, zastosujmy go do grafu z ilustracji 10. Chcemy poznać najtańsze

połączenie z Nowego Jorku do Los Angeles, więc jako węzeł źródłowy wybieramy Nowy Jork. Tablica kosztu na

początku działania algorytmu na pozycji Nowy Jork ma wpisaną wartość 0, a na wszystkich pozostałych

pozycjach ma wpisaną nieskończoność. Tablica tras na wszystkich pozycjach ma wpisane puste referencje,

a zbiór Q zawiera wszystkie węzły grafu (sytuacja została przedstawiona na ilustracji 11.).

Page 22: 5_Struktury danych - od drzew do grafów

Ilustracja 11. Tablice kosztu i trasy, wykorzystywane do wyznaczenia najtańszego połączenia

Ze zbioru Q wybieramy miasto, do którego w tablicy kosztu przypisana jest najmniejsza wartość — w naszym

przykładzie jest to Nowy Jork. Następnie sprawdzamy, z jakimi miastami Nowy Jork posiada bezpośrednie

połączenie lotnicze i czy koszt przelotu z Nowego Jorku do tych miast jest niższy niż koszt wpisany dla tych

miast w tablicy odległości. Następnie ze zbioru Q usuwamy Nowy Jork i uaktualniamy dane w tablicach kosztu

i trasy dla miast Chicago, Denver, Miami i Dallas.

Page 23: 5_Struktury danych - od drzew do grafów

Ilustracja 12. Etap drugi algorytmu ustalania najtańszego połączenia

W następnej iteracji miastem ze zbioru Q z wpisem o najmniejszej wartości w tablicy odległości jest Chicago.

Sprawdzamy, czy istnieje tańsze połączenie do sąsiadów Chicago. Mniejsze wartości otrzymujemy dla San

Francisco i Denver. Kosz przelotu do San Francisco przez Chicago wynosi 75 USD +25 USD, co daje wartość

mniejszą niż nieskończoność, więc uaktualniamy wpisy dla San Francisco. Także lot przez Chicago do Denver

jest tańszy niż bezpośredni przelot z Nowego Jorku do Denver (75 USD + 20 USD < 100 USD), więc

uaktualniamy wpisy dla Denver. Na ilustracji 13. przedstawiono wartości wpisów w tablicach i zawartość zbioru

Q po sprawdzeniu lotów z Chicago.

Ilustracja 13. Stan tablic po trzecim etapie procesu

Proces ten jest wykonywany tak długo, jak długo w zbiorze Q istnieją jakieś węzły. Na ilustracji 14.

przedstawiono zawartość tablic po opróżnieniu zbioru Q.

Page 24: 5_Struktury danych - od drzew do grafów

Ilustracja 14. Ostateczne wyniki algorytmu wyszukiwania najtańszego połączenia

Po wyczerpaniu elementów zbioru Q tablice zawierają informacje o najtańszych połączeniach lotniczych

z Nowego Jorku do pozostałych miast. Aby ustalić trasę przelotu do Los Angeles, należy sprawdzić wpis dla L.A.

w tablicy tras i cofać się przez kolejne miasta aż do osiągnięcia Nowego Jorku. A więc, jeśli w tablicy tras na

pozycji L.A. wpisane jest San Francisco, to ostatnia przesiadka miała miejsce w San Francisco. Z wpisu w tablicy

tras dla San Francisco wynika, że najtańszy lot do San Francisco odbywa się przez Chicago. W tablicy tras dla

Chicago widnieje Nowy Jork. Jeśli złożymy te wszystkie informacje razem, okaże się, że najtaniej z Nowego Jorku

do Los Angeles można lecieć przez Chicago i San Francisco.

Uwaga   Implementację algorytmu Dijkstry w języku C# można znaleźć w pliku z przykładami dla tego artykułu. Plik zawiera testową aplikację dla klasy Graph, która ustala najkrótszą trasę z jednego miasta do drugiego z zastosowaniem algorytmu Dijkstry.

Podsumowanie

Grafy są często stosowaną strukturą danych, ponieważ można za ich pomocą przedstawić wiele rzeczywistych

problemów. Graf składa się ze zbioru węzłów i dowolnej liczby połączeń pomiędzy tymi węzłami, nazywanych

krawędziami. Krawędzie mogą być skierowane lub nieskierowane i mogą (ale nie muszą) mieć przypisane wagi.

W tym artykule przedstawione zostały podstawowe informacje o grafach. Utworzyliśmy także klasę Graph.

Klasa ta jest podobna do utworzonej w trzecim artykule klasy BinaryTree. Różnica polega na tym, że węzły

w klasie Graph mogą mieć dowolną liczbę krawędzi, a węzły drzew binarnych mogą mieć maksymalnie dwie

krawędzie. Podobieństwo to nie powinno dziwić, ponieważ drzewa są specjalnym przypadkiem grafów.

Po utworzeniu klasy Graph przyjrzeliśmy się dwóm popularnym algorytmom grafowym — algorytmowi

znajdowania minimalnego drzewa rozpinającego i algorytmowi wyznaczania najkrótszej ścieżki w ważonym

grafie skierowanym. W artykule nie przedstawiłem kodu źródłowego z implementacją tych algorytmów, ale

w Internecie znajduje się wiele takich przykładów. Również plik, dostępny do pobrania z tym artykułem, zawiera

aplikację testową dla klasy Graph, która wykorzystuje algorytm Dijkstry do wyznaczenia najkrótszej trasy

pomiędzy dwoma miastami.

Page 25: 5_Struktury danych - od drzew do grafów

W następnym artykule — szóstej i ostatniej części tej serii — zajmiemy się problemem efektywnej reprezentacji

zbiorów rozłącznych. Zbiory rozłączne to kolekcja co najmniej dwóch zbiorów, nie posiadających żadnych

wspólnych elementów. Na przykład w algorytmie Prima wyznaczania minimalnego drzewa rozpinającego, węzły

są rozdzielane na dwa zbiory rozłączne — zbiór węzłów, które znajdują się już w drzewie rozpinającym oraz zbiór

węzłów, które jeszcze nie zostały dołączone do drzewa rozpinającego.

Bibliografia

„Wprowadzenie do algorytmów”, Thomas H. Cormen, Charles E. Leiserson i Ronald L. Rivest,

Wydawnictwa Naukowo-Techniczne

Scott Mitchell — autor 5 książek i założyciel witryny 4GuysFromRolla.com. Od 5 lat zajmuje się w Microsoft technologiami internetowymi. Scott pracuje jako niezależny konsultant, szkoleniowiec i autor artykułów, a niedawno zdobył dyplom z informatyki na Uniwersytecie Kalifornijskim w San Diego. Jego adres e-mail to [email protected]. Blog Scotta dostępny jest pod adresem http://ScottOnWriting.NET.