Graphen
Graphen
Graphen
Erfolgreiche ePaper selbst erstellen
Machen Sie aus Ihren PDF Publikationen ein blätterbares Flipbook mit unserer einzigartigen Google optimierten e-Paper Software.
Algorithmen und Datenstrukturen 2<br />
Prof. Dr. C. Stamm, B. Scheuner christoph.stamm@fhnw.ch Tel.: 056 462 47 44<br />
1 Einführung in <strong>Graphen</strong><br />
Ein Graph ist eine Menge von Knoten, welche durch Kanten verbunden sind. Bis jetzt haben Sie eine<br />
spezielle Teilmenge der <strong>Graphen</strong> die Bäume kennen gelernt. Es gibt jedoch noch viele andere Arten<br />
von <strong>Graphen</strong>. <strong>Graphen</strong> dienen ganz allgemein der Veranschaulichung von Zusammenhängen zwischen<br />
verschiedenen Objekten. Dabei werden die Objekte meistens als Knoten und die Zusammenhänge<br />
als Kanten dargestellt.<br />
Ein ganz offensichtliches Beispiel ist das Internet. Es<br />
besteht aus Endgeräten und Routern (Knoten) und aus<br />
Netzwerk-Verbindungen (Kanten) dazwischen. Ein Problem<br />
im Internet ist das Auffinden einer möglichst<br />
schnellen Verbindung zwischen zwei Endgeräten. Üblicherweise<br />
gibt es zwischen zwei Knoten immer mehr als<br />
nur eine Verbindung. Aber welche dieser Verbindungen<br />
?<br />
ist nun die kürzeste oder die schnellste<br />
Das nebenstehende Liniennetz eines Verkehrsbetriebes<br />
ist auch ein Graph. An jeder Haltestelle können Sie sich<br />
ein Bild dieses <strong>Graphen</strong> ansehen. Hier fragen sich die<br />
Benutzer, welches der schnellste oder billigste Weg von<br />
einer Haltestelle zur nächsten ist, oder auf welcher<br />
ssen.<br />
Route sie am wenigsten umsteigen mü<br />
Man kann aber noch ganz andere Probleme als <strong>Graphen</strong><br />
darstellen. Z.B. kann das Finden einer besten (z.B.<br />
kürzesten, einfachsten, schnellsten) Besichtigungstour<br />
einer Stadt mit vielen Sehenswürdigkeiten als <strong>Graphen</strong>problem<br />
interpretiert werden. Jede Sehenswürdigkeit<br />
wird dabei als Knoten dargestellt. Zwei Knoten sind<br />
genau dann mit einer Kante verbunden, wenn die beiden<br />
Sehenswürdigkeiten über einen direkten Weg verbunden<br />
sind. Nun möchten Sie z.B. eine Tour finden, bei der Sie<br />
alle Sehenswürdigkeiten besichtigen können und dabei<br />
so wenig wie möglich Kilometer zurücklegen müssen.<br />
Sie sehen, dass <strong>Graphen</strong> in unserer modernen Welt sehr<br />
weit verbreitet sind. Sehr viele Situationen lassen sich in einem Graph veranschaulichen; meist mit<br />
dem Ziel, Probleme zu lösen. Das Lösen dieser Probleme ist Gegenstand der <strong>Graphen</strong>theorie. Als<br />
erstes werden wir nun definieren, was ein Graph ist, und welche Eigenschaften er hat. Danach werden<br />
wir einige bekannte Algorithmen betrachten.<br />
In Wikipedia 1 finden Sie ein Glossar der <strong>Graphen</strong>theorie mit ca. 250 Begriffen. Ein Teil dieser Begriffe<br />
kennen Sie schon von den Bäumen und aus dem Fach Diskrete Mathematik.<br />
1.1 Definitionen<br />
Graph: Ein Graph besteht aus einer Knotenmenge V (vertices) und einer Kantenmenge E (edges).<br />
Dabei gilt:<br />
(i) |V| = n > 0<br />
(ii) E = {(u, v) | u, v ∈ V} wobei u und v nicht verschieden sein müssen. Eine Kante ist also ein<br />
Paar von Knoten. Falls u = v gilt, dann sprechen wir von einer Schleife oder Schlinge, also<br />
einer Kante, welche einen Knoten mit sich selber verbindet.<br />
|E| = m ≥ 0<br />
Notation: V = {1, 2, ..., n} und E = {(1,2), (3,2), ..., (2,4)}<br />
Knoten: Der Grad eines Knoten in einem <strong>Graphen</strong> entspricht der Menge der Kanten, welche den<br />
Knoten als (einen) Anfangs- bzw. Endpunkt haben. Der Grad wird angegeben als deg(v). Eine<br />
1 http://de.wikipedia.org/wiki/Glossar_<strong>Graphen</strong>theorie<br />
1
Kante ist zu ihren Anfangs- und Endknoten inzident. Alle Knoten, mit denen ein Knoten durch eine<br />
Kante verbunden ist, heissen adjazente Knoten. Diese adjazenten Knoten werden meist auch als<br />
Nachbarn bezeichnet.<br />
Kanten: Eine Kante verbindet zwei Knoten miteinander. Dabei kann sie eine Richtung haben (gerichtet),<br />
oder aber auch ungerichtet sein. Wenn sie eine Richtung hat, wird sie als Pfeil gezeichnet<br />
und als Paar von Knoten (u, v) symbolisiert, wobei u der Startknoten und v der Zielknoten ist. Eine<br />
ungerichtete Kante wird als Verbindungslinie gezeichnet und als Menge {u, v} symbolisiert.<br />
Neben der Richtung kann eine Kante auch noch ein Gewicht haben. Solche Kanten heissen gewichtete<br />
Kanten. Gewichte stellen zum Beispiel die Entfernung zweier Knoten oder die Bandbreite<br />
einer Internetleitung dar.<br />
Sind zwei Knoten durch mehrere direkte Kanten verbunden, wird diese Gruppe als Mehrfachkante<br />
zusammengefasst.<br />
Ist ein Knoten mit sich selber durch eine Kante verbunden, wird diese Kante als Schlinge bezeichnet.<br />
(Für den Grad eines Knoten wird diese Kante zweimal gezählt.)<br />
Zusammenhängende <strong>Graphen</strong>: Ein Graph ist zusammenhängend, wenn von jedem Knoten jeder<br />
andere Knoten über Kanten erreicht werden kann. Ist ein Graph nicht zusammenhängend, besteht<br />
er aus mehreren Komponenten. Das Tramnetz einer Stadt ist üblicherweise ein zusammenhängender<br />
Graph. Alle Tramnetze der Schweiz gemeinsam betrachtet, sind jedoch ein nicht zusammenhängender<br />
Graph.<br />
2<br />
Mehrfachkante<br />
3<br />
0<br />
3<br />
Schlinge<br />
1 2<br />
3<br />
Baum: Ein Baum ist ein spezieller zusammenhängender Graph. Er zeichnet sich dadurch aus, dass<br />
er keine Kreise und somit auch keine Schlingen enthält (siehe Unterkapitel Kreise).<br />
1.2 Einfache <strong>Graphen</strong><br />
Ein einfacher Graph ist ein ungerichteter Graph ohne Mehrfachkanten und ohne Schlingen. In einfachen<br />
<strong>Graphen</strong> und auch in allen anderen ungerichteten <strong>Graphen</strong> können folgende Aussagen gemacht<br />
werden:<br />
• Die Summe aller Knotengrade ist gerade, sie entspricht dem Zweifachen der Anzahl Kanten m.<br />
∑ v in V deg(v) = 2⋅m<br />
• Die Anzahl der Knoten mit ungeradem Knotengrad ist gerade.<br />
Beweis: Die Summe aller Knotengrade ist gerade. Die Summe aller geraden Knotengrade ist gerade.<br />
Eine gerade Zahl minus eine gerade Zahl muss wieder gerade sein. Somit ist die Summe aller<br />
ungeraden Grade wieder gerade. Diese Summe kann nun nur gerade sein, wenn es eine gerade<br />
Anzahl von ungeraden Graden gibt.<br />
Formal: ∑ v in V deg(v) = ∑ v in V deg gerade (v) + ∑ v in V deg ungerade (v) = 2 m<br />
∑ v in V deg gerade (v) ist gerade, weil die Summe von geraden Zahlen immer gerade ist.<br />
∑ v in V deg ungerade (v) muss auch gerade sein, weil die Summe der beiden gerade ist.<br />
Nehmen wir nun an, dass wir eine ungerade Anzahl ungerader Grade haben. Dann können<br />
wir immer Paare (mit gerader Summe) bilden, bis auf einen Grad. Dass diese Summe nie gerade<br />
sein kann ist einleuchtend.<br />
1.2.1 Vollständige <strong>Graphen</strong> (K n )<br />
K 2<br />
K 3 K 4<br />
K 5<br />
Ein vollständiger Graph ist ein einfacher Graph. Er hat aber die zusätzliche Eigenschaft, dass von<br />
jedem Knoten aus alle andern Knoten direkt mit einer Kante verbunden sind. Solche <strong>Graphen</strong> können<br />
für die Darstellung eines Tourniers verwendet werden, bei dem jedes Team gegen jedes andere Team<br />
spielen muss.<br />
2
1.2.2 Kreise (C n )<br />
Ein Kreis ist ein einfacher Graph, bei dem alle Knoten den<br />
Grad 2 haben. Von jedem Knoten kommt man auf zwei Wegen<br />
zu jedem anderen Knoten.<br />
1.2.3 Bipartite <strong>Graphen</strong><br />
Ein einfacher Graph heisst bipartit, wenn die Knotenmenge in zwei disjunkte Teilmengen aufgeteilt<br />
werden kann und wenn alle Kanten nur Verbindungen zwischen den beiden Teilmengen herstellen. Es<br />
gibt also keine Kanten zwischen zwei Knoten aus der gleichen Teilmenge.<br />
Ein bipartiter Graph heisst vollständig, wenn jeder Knoten aus der einen Teilmenge mit allen Knoten<br />
aus der andern Teilmenge verbunden ist. Wir bezeichnen einen vollständigen bipartiten Graph mit<br />
(K a,b ).<br />
Solche <strong>Graphen</strong> können zum Beispiel bei einer Partnerschaftsvermittlung gezeichnet werden. Jeder<br />
Mann wird mit einer potentiellen Partnerin verbunden und umgekehrt.<br />
K 3,4 K 4,1<br />
1.2.4 Hyperwürfel<br />
Eine etwas spezielle Art eines <strong>Graphen</strong> ist der Hyperwürfel. Dieser Graph ist wiederum ein einfacher<br />
Graph. Seine Knoten haben jedoch ganz bestimmte Namen. Ein Hyperwürfel der Dimension d hat die<br />
Knotenmenge, welche aus allen binären Folgen der Länge d besteht. Formal heisst dies: V= { {0,1} d }.<br />
Eine Kante verbindet immer genau dann zwei Knoten, wenn sich deren Binärfolge in nur einer Stelle<br />
unterscheiden. Z.B. werden die zwei Knoten mit den Binärfolgen 1011 und 1001 miteinander verbunden.<br />
Solche Hyperwürfel werden zum Beispiel in der Codierungstheorie verwendet.<br />
1.2.5 Planare <strong>Graphen</strong><br />
Planare <strong>Graphen</strong> sind einfache <strong>Graphen</strong>, welche so gezeichnet werden können, dass sich keine zwei<br />
Kanten überschneiden. Über planare <strong>Graphen</strong> lassen sich sehr viele Aussagen machen. Zwei davon<br />
werden hier erläutert. Um diese Aussagen jedoch zu verstehen, muss erst noch ein neuer Begriff<br />
eingeführt werden:<br />
Gebiet oder Fläche: Ein Gebiet ist eine zusammenhängende Fläche, welche von mindestens drei<br />
Kanten begrenzt wird.<br />
1. Bäume sind planar.<br />
Idee: Durch ihre spezielle Struktur können alle Bäume so gezeichnet werden, dass sich keine zwei<br />
Kanten überschneiden.<br />
2. Eulersche Polyederformel<br />
Sei G ein planarer, zusammenhängender, einfacher Graph mit n Knoten, m Kanten und f Gebieten.<br />
Dann gilt: n – m + f = 2.<br />
Beweis: Aus 1 ist bekannt, dass Bäume planar sind. In jedem <strong>Graphen</strong> kann ein Baum (Spannbaum)<br />
aufgespannt werden, der alle Knoten beinhaltet.<br />
Ein Baum mit n Knoten hat m = n – 1 Kanten. Es existiert nur ein Gebiet, f = 1. Daraus folgt:<br />
n – (n – 1) + 1 = 2.<br />
Nun werden die restlichen Kanten des <strong>Graphen</strong> sukzessive hinzugefügt. Dabei fällt auf, dass bei<br />
jeder Kante die hinzugefügt wird, ein neues Gebiet erzeugt wird.<br />
Die beiden Bilder zeigen die zwei Möglichkeiten.<br />
Entweder wird vom umgebenden Gebiet<br />
ein Teilgebiet abgetrennt oder ein inneres<br />
Gebiet wird geteilt. Auf diese Weise werden die<br />
fehlenden k Kanten hinzugefügt.<br />
n – (n – 1 + k) + 1 + k = n – n +1 – k +1 + k = 2<br />
3
3. Das Verhältnis von Gebieten zu Kanten: 3f ≤ 2m<br />
Beweis: Ein Gebiet wird von mindestens drei Kanten begrenzt. Eine Kante begrenzt immer zwei<br />
Gebiete. 3f bezeichnet alle Randkanten. Dabei ist aber jede Kante doppelt gezählt.<br />
4. Kanten und Regionen in planaren <strong>Graphen</strong><br />
Für jeden einfachen planaren <strong>Graphen</strong> G = (V,E) mit |V| ≥ 3 gilt:<br />
• m ≤ 3n – 6<br />
• f ≤ 2n – 4<br />
Beweis: n – m + f = 2 und 3f ≤ 2m sind gegeben. Aus der Polyederformel folgt: f = 2 – n + m. Setzen<br />
wir diesen Wert für f in (3f ≤ 2m) ein erhalten wir: 3(2 – n + m) ≤ 2m. Daraus folgt:<br />
6 – 3n + 3m ≤ 2m ⇒ 6 – 3n + m ≤ 0 ⇒ m ≤ 3n – 6.<br />
Die zweite Aussage lässt sich analog dazu beweisen (Hausaufgabe).<br />
5. K 5 und K 3,3 sind nicht planar.<br />
K 5 oder K 3,3 sind die beiden kleinsten nicht planaren <strong>Graphen</strong>, was direkt aus dem Satz von Kuratowski<br />
2 folgt. Der Satz von Kuratowski sagt: „Ein endlicher Graph ist genau dann planar, wenn er<br />
keinen Teilgraphen enthält, der durch Unterteilung von K 5 oder K 3,3 entstanden ist. Unterteilung bedeutet<br />
hier das beliebig oft wiederholbare (auch nullmalige) Einfügen von neuen Knoten auf Kanten.<br />
Mit Teilgraph ist hier ein Graph gemeint, der aus dem ursprünglichen <strong>Graphen</strong> durch Entfernen von<br />
Knoten bzw. Kanten entsteht.“ Somit erlaubt der Satz von Kuratowski zu entscheiden, ob ein Graph<br />
planar ist oder nicht.<br />
Ein anderer Beweis, dass K 5 nicht planar ist, folgt aus den Forderungen aus Punkt 4.<br />
Beweis für K 5 : Dieser Graph besitzt n = 5 Knoten und 10 Kanten. In (m ≤ 3n – 6) ergibt dies<br />
10 ≤ 15 – 6 = 9 (stimmt also nicht)<br />
1.3 Gerichtete <strong>Graphen</strong> (Digraphen)<br />
Im Unterschied zu den allgemeinen <strong>Graphen</strong> besitzen die Kanten gerichteter <strong>Graphen</strong> eine Richtung.<br />
Das bedeutet, dass eine Kante zwischen zwei Knoten nur in einer Richtung durchlaufen werden darf.<br />
Gerichtete <strong>Graphen</strong> finden ihre Anwendung in ganz verschiedenen Gebieten. In einem Projektplan<br />
werden die Aktivitäten mit einer gerichteten Kante verbunden um anzuzeigen, wie der zeitliche Ablauf<br />
sein muss. In einer Strassenkarte werden Einbahnstrassen mit einer Richtung versehen.<br />
Eingangsgrad: Der Eingangsgrad (Indegree) eines Knoten entspricht der Anzahl gerichteter<br />
Kanten, die in den Knoten hineinführen: indeg(v)<br />
Ausgangsgrad: Der Ausgangsgrad (Outdegree) eines Knoten entspricht der Anzahl gerichteten<br />
Kanten, die aus dem Knoten hinausführen: outdeg(v)<br />
Die Summe der Eingangsgrade aller Knoten ist gleich der Summe der Ausgangsgrade aller Knoten.<br />
∑ indeg(v) = ∑ outdeg(v)<br />
2 z.B. http://de.wikipedia.org/wiki/Satz_von_Kuratowski<br />
4
1.4 Lernkontrolle<br />
1.4.1 <strong>Graphen</strong>eigenschaften<br />
Hier sind einige <strong>Graphen</strong> abgebildet. Geben Sie für jeden <strong>Graphen</strong> die Grade der Knoten an. Versuchen<br />
Sie dann so viele Eigenschaften wie möglich festzuhalten. Mit Eigenschaften sind hier gemeint:<br />
• Zu welcher Gruppe von <strong>Graphen</strong> gehört der Graph? Z.B. Bäume, Kreise, vollständig bipartite<br />
<strong>Graphen</strong>, einfacher Graph, etc.<br />
• Ist der Graph zusammenhängend?<br />
• Etc.<br />
a) b) c)<br />
d)<br />
e)<br />
f)<br />
1.4.2 Summe der Knotengrade<br />
Im Kapitel über einfache <strong>Graphen</strong> wurde gesagt, dass es immer eine gerade Anzahl von Knoten mit<br />
ungeradem Grad geben muss. Wie sieht dies für die Anzahl der Knoten mit geradem Grad aus?<br />
1.4.3 Planare <strong>Graphen</strong><br />
Lassen sich die die untenstehenden <strong>Graphen</strong> planar zeichnen? Be- oder widerlegen Sie die Planarität<br />
mit den Formeln aus dem Kapitel über planare <strong>Graphen</strong>. Zeichnen Sie die planaren <strong>Graphen</strong> planar.<br />
Schreiben Sie auch dazu, was die <strong>Graphen</strong> darstellen.<br />
a) b)<br />
c) d)<br />
1.4.4 Kurzaufgaben<br />
a) Wie viele Kanten hat der K 5 ?<br />
b) Wie viele Kanten hat der K n ?<br />
c) Wie viele Kanten hat der K 3,3 ?<br />
d) Wie viele Kanten hat der K a,b ?<br />
e) Wie viele Kanten hat der C 4 ?<br />
f) Wie viele Kanten hat der C n ?<br />
g) Zeichnen Sie für die folgende Situation einen <strong>Graphen</strong>, der die Situation widergibt: Sie laden 5<br />
Freunde ein. Jeder Gast kennt zwei andere Gäste. Zeichnen Sie einen zusammenhängenden<br />
<strong>Graphen</strong>.<br />
h) Kann ein Graph mit 11 Knoten und 30 Kanten planar sein?<br />
i) Kann ein Graph mit 5 Knoten und 6 Gebieten planar sein?<br />
j) Kann ein Graph mit 5 Knoten, 10 Kanten und 8 Gebieten planar sein?<br />
5
2 Speicherung von <strong>Graphen</strong><br />
Nach der Einführung kennen Sie die grundlegende Struktur von <strong>Graphen</strong>. In vielen Computerprogrammen<br />
wird mit <strong>Graphen</strong> gearbeitet, z.B. Navigation, Routenplanung im Abfuhrwesen, Internet<br />
Routing Protokoll. Alle diese Programme speichern in irgendeiner Form einen oder mehrere verschiedene<br />
<strong>Graphen</strong>. Oft werden diese <strong>Graphen</strong> nicht nur im Hauptspeicher sondern auch auf dem Externspeicher<br />
gehalten.<br />
Die Art der Speicherung der <strong>Graphen</strong> kann auf die Effizienz der auszuführenden Algorithmen einen<br />
Einfluss haben. Das ist eigentlich nichts Neues, wie Sie schon längst bei anderen Datenstrukturen<br />
gesehen haben. Aus diesem Grund ist es wichtig, <strong>Graphen</strong> speicher- und laufzeiteffizient abzuspeichern.<br />
Im Folgenden lernen Sie vier unterschiedliche Arten der Speicherung kennen.<br />
2.1 Adjazenzmatrix<br />
Der Beispielgraph ist der Graph G=(V,E) mit V={1,2,3,4,5} und E= {(1,3), (1,3), (3,4), (2,3), (2,5), (5,5)}<br />
1<br />
3<br />
4<br />
2<br />
5<br />
1 2 3 4 5<br />
1 0 0 2 0 0<br />
2 0 0 1 0 1<br />
3 0 0 0 1 0<br />
4 0 0 0 0 0<br />
5 0 0 0 0 1<br />
In einer Adjazenzmatrix werden adjazente Knotenpaare gespeichert. Es wird vermerkt, welche Knoten<br />
durch eine Kante miteinander verbunden sind.<br />
Die Adjazenzmatrix ist eine Matrix der Grösse n × n, wobei n die Anzahl der Knoten bezeichnet. Sind<br />
zwei Knoten (u und v) mit einer gerichteten Kante verbunden wird in der Adjazenzmatrix in der u-ten<br />
Zeile und der v-ten Spalte eine 1 eingetragen. Sind die beiden Knoten mit mehr als einer Kante verbunden,<br />
wird die Anzahl der Kanten eingetragen. Für u und v gilt 1 ≤ u ≤ n, 1 ≤ v ≤ n.<br />
Im Beispiel ist ein Digraph mit seiner Adjazenzmatrix dargestellt. Es ist erkennbar, dass Mehrfachkanten<br />
zusammengezählt werden. Treten Schlingen auf, sind diese in der Diagonalen der Matrix ersichtlich.<br />
Besteht die Diagonale aus lauter Nullen, ist der Graph schlingenfrei.<br />
In ungerichteten <strong>Graphen</strong> ist die Matrix symmetrisch, weil sowohl (u, v) = 1 als auch (v, u) = 1 eingetragen<br />
wird. Selbstverständlich lässt sich in diesem Fall die Speicherung leicht optimieren, indem nur<br />
eine Dreiecksmatrix gespeichert wird.<br />
Wenn der Graph gewichtet ist, dann wird das Gewicht der Kante in die Zelle eingetragen. Bei gewichteten<br />
<strong>Graphen</strong> ist es unüblich, dass Mehrfachkanten auftreten (je nach Problem müsste dieser Umstand<br />
anders behandelt werden). Man kann in einer Adjazenzmatrix nicht unterscheiden, ob die Kanten<br />
gewichtet sind oder ob Mehrfachkanten aufgetreten sind.<br />
Bemerkung: Bei dieser Art der Speicherung mit einem herkömmlichen Array werden die Knoten nur<br />
implizit in Form des Array-Index gespeichert. Alle weiteren Eigenschaften von Knoten müssen separat<br />
gespeichert werden.<br />
2.2 Inzidenzmatrix<br />
Der Beispielgraph ist der Graph G = (V, E) mit V = {1, 2, 3, 4, 5} und E = {{1,3}, {1,3}, {3,4}, {2,3}, {2,5},<br />
{5}}<br />
2<br />
1<br />
3<br />
1<br />
3<br />
4<br />
4<br />
2<br />
5<br />
6<br />
5<br />
1 2 3 4 5 6<br />
1 1 1 0 0 0 0<br />
2 0 0 0 1 1 0<br />
3 1 1 1 1 0 0<br />
4 0 0 1 0 0 0<br />
5 0 0 0 0 1 1<br />
In der Inzidenzmatrix wird eingetragen, welche Kanten zu welchen Knoten inzident sind.<br />
Die Inzidenzmatrix ist eine (n × m)-Matrix. Dabei werden nicht nur die Knoten nummeriert, sondern<br />
auch die Kanten. Gehört eine Kante e mit Index j zu einem Knoten v, wird in der v-ten Zeile und der j-<br />
6
ten Spalte eine 1 eingetragen. Ansonsten wird eine 0 eingetragen. Für v und j gelten 1 ≤ v ≤ n, 1 ≤ j ≤<br />
m.<br />
Im Beispiel ist eine Inzidenzmatrix für einen ungerichteten <strong>Graphen</strong> gezeigt. Für gerichtete <strong>Graphen</strong><br />
kann die Darstellung nicht einfach übernommen werden. Mann muss definieren können, ob ein inzidenter<br />
Knoten der Ursprung oder das Ziel ist. Man könnte z.B. für Ursprung eine 1 eintragen und für<br />
Ziel eine -1.<br />
Gewichte der Kanten können in der Inzidenzmatrix berücksichtigt werden indem der Wert des Gewichts<br />
in der Matrix eingetragen wird.<br />
Bemerkung: Bei dieser Art der Speicherung werden Knoten und Kanten nur implizit in Form ihrer<br />
Indizes erfasst. Zusätzliche Informationen zu Kanten und Knoten müssen deshalb in einer separaten<br />
Struktur gespeichert werden.<br />
2.3 Adjazenzlisten<br />
Der Beispielgraph G = (V, E) ist gerichtet aber ungewichtet.<br />
Es sei: G = (V, E) mit V = {1, 2, ..., 9} und E = {(1,2),<br />
(1,3), (1,7), (4,6), (5,4), (6,1), (6,5), (6,6), (7,5), (9,8)}<br />
Wie bei der Adjazenzmatrix werden bei dieser Speicherung<br />
die Verbindungen zwischen zwei Knoten in den<br />
Vordergrund gestellt. Es wird also gespeichert, welcher<br />
Knoten mit welchem anderen Knoten benachbart ist.<br />
Die Grundlage der Speicherung ist ein Knoten-Array.<br />
Dieses Array hat so viele Felder, wie es Knoten gibt.<br />
Jedem Knoten wird eine Position im Array zugeteilt. Im<br />
Knoten-Array werden mindestens die Anfangszeiger auf<br />
verkettete Listen gespeichert. In diesen Listen sind die<br />
Knoten gespeichert, zu welchen eine direkte Verbindung<br />
durch eine Kante besteht. Die Kante (u, v) aus dem Graph<br />
führt zu einem Eintrag in der Liste an der Stelle u im<br />
Array.<br />
Diese Speicherung benötigt Ө(n + m) Speicherplatz. Adjazenzlisten unterstützen viele Operationen,<br />
z.B. das Verfolgen von gerichteten Kanten in <strong>Graphen</strong>. Andere Operationen dagegen werden nur<br />
schlecht unterstützt, insbesondere das Hinzufügen und Entfernen von Knoten.<br />
2.4 Speicherung mit doppelt verketteten Listen<br />
Der Beispielgraph ist derselbe<br />
wie vorhin.<br />
Bei dieser Speicherung werden<br />
die Knoten und die Kanten als<br />
Objekte behandelt. Es wird<br />
ihnen also zugestanden, mehr<br />
Information als nur den Namen<br />
zu enthalten.<br />
Die Basis bildet eine doppelt<br />
verkettete Liste, welche die<br />
Knotenobjekte enthält. Jedes<br />
Knotenobjekt speichert dann<br />
eine Liste mit den von ihm<br />
ausgehenden Kanten-Objekten.<br />
Auch diese Liste ist doppelt<br />
verkettet.<br />
Ein Kantenobjekt enthält drei<br />
Referenzen. Zwei Referenzen<br />
werden für die doppelt verkettete<br />
Kantenliste benötigt. Die<br />
dritte Referenz zeigt auf den<br />
Knoten, auf den die gerichtete Kante zeigt. Zusätzlich können im Kantenobjekt noch weitere Informationen<br />
wie z.B. das Gewicht der Kante gespeichert werden.<br />
7
2.5 Lernkontrolle<br />
2<br />
1<br />
2<br />
3<br />
1<br />
4<br />
3<br />
4<br />
5<br />
5<br />
2.5.1 Adjazenzmatrix<br />
a) Geben Sie die Adjazenzmatrix für die beiden <strong>Graphen</strong> an.<br />
b) Bei welchem <strong>Graphen</strong> ist der Speicherplatz besser ausgenutzt?<br />
c) Denken Sie dass der Speicherplatz generell gut ausgenützt ist?<br />
d) Gibt es <strong>Graphen</strong>, für die diese Speicherung gut, bzw. sehr schlecht ist?<br />
2.5.2 Inzidenzmatrix<br />
a) Geben Sie die Inzidenzmatrix für die beiden <strong>Graphen</strong> an.<br />
b) Bei welchem <strong>Graphen</strong> ist der Speicherplatz besser ausgenutzt?<br />
c) Denken Sie dass der Speicherplatz generell gut ausgenützt ist?<br />
d) Gibt es <strong>Graphen</strong>, für die diese Speicherung gut, bzw. sehr schlecht ist?<br />
2.5.3 Adjazenzlisten<br />
Speichern Sie den oben rechts stehenden <strong>Graphen</strong> mit Hilfe von Adjazenzlisten ab.<br />
8
3 <strong>Graphen</strong>algorithmen<br />
Es gibt sehr viele Probleme, welche sich im Zusammenhang mit <strong>Graphen</strong> stellen. Sie werden hier nur<br />
einen ganz kleinen Teil dieser Probleme und deren Lösungsansätze kennen lernen.<br />
3.1 Topologisches Sortieren<br />
Eine topologische Sortierung basiert auf einer vergleichenden Relation. In einem gerichteten <strong>Graphen</strong><br />
besteht durch die Kantenrichtung eine solche vergleichende Relation zwischen je zwei benachbarten<br />
Knoten. Die Kantenrichtung kann dabei gelesen werden wie zum Beispiel: „grösser als“, „folgt auf“<br />
oder „ist Nachfolger von“. In ungerichteten <strong>Graphen</strong> besteht diese vergleichende Relation nicht und<br />
daher kann keine topologische Sortierung erreicht werden.<br />
Beispiel: Wir müssen einen Projektplan erstellen. Als erstes stellen wir fest, welche Aktivitäten zu<br />
erledigen sind: „Analyse“, „Implementierung“, „Einkauf der Hardware“, „Test“, „Installation der Hardware“,<br />
„Installation der Software“. Es ist klar, dass diese Aktivitäten eine zeitliche Abhängigkeit haben.<br />
Wir müssen beispielsweise zuerst die Hardware kaufen, bevor wir sie installieren können.<br />
Folgender Projektplan könnte nun erstellt werden:<br />
Einkauf<br />
Hardware<br />
Installation<br />
Hardware<br />
Analyse<br />
Implementierung<br />
Test<br />
Installation<br />
Software<br />
Wir sehen hier die zeitliche Abhängigkeit (Relation „kommt nach“). Wenn wir dieses Projekt nun<br />
alleine durchführen müssen, können wir nicht so parallel arbeiten, wie das hier dargestellt ist. Wir<br />
müssen eine sequenzielle (topologisch sortierte) Abfolge haben. Davon gibt es mehrere. Hier drei<br />
Beispiele:<br />
Analyse → Einkauf Hardware → Installation Hardware → Implementierung → Test → Installation Software<br />
Analyse → Einkauf Hardware → Implementierung → Installation Hardware → Test → Installation Software<br />
Analyse → Implementierung → Test → Einkauf Hardware → Installation Hardware → Installation Software<br />
Eine topologische Sortierung eines Digraphen ist also eine vollständige Ordnung der Knoten, die mit<br />
der durch die gerichteten Kanten ausgedrückten partiellen Ordnung verträglich ist.<br />
3.1.1 Zyklenfreiheit<br />
Ein Zyklus oder Kreis ist eine Folge von Kanten bei der Anfangs- und Endpunkt dieselben sind. Eine<br />
Schlinge ist der kleinstmögliche Zyklus mit genau einem Knoten.<br />
Nicht für alle Digraphen kann eine topologische Sortierung erzeugt werden. Die zentrale Eigenschaft<br />
von topologisch sortierbaren Digraphen ist, dass sie keine Zyklen enthalten, also zyklenfrei sind.<br />
In manchen Problemstellungen ist es sehr wichtig, dass die <strong>Graphen</strong> zyklenfrei sind. Beim Projektplan<br />
darf es nicht sein, dass zwei Vorgänge je voneinander abhängen. Auch in Computerprogrammen ist<br />
es oft nicht erlaubt, dass zyklische Abhängigkeiten bestehen: Wenn Klasse C1 von C2 abhängt, C2<br />
von C3 abhängt und C3 von C1 abhängt, dann besteht ein zyklische Abhängigkeit.<br />
Satz: Jeder zyklenfreie Graph hat eine topologische Sortierung.<br />
3.1.2 Algorithmus<br />
Die Idee des Algorithmus ist es, mit Knoten zu arbeiten, welche keine eingehenden Kanten haben,<br />
d.h. die indeg(v) = 0 besitzen. Diese Knoten können nicht zu einem Zyklus gehören, da sie von keinem<br />
anderen Knoten erreicht werden können. Es ist sofort einleuchtend, dass es in einem topologisch<br />
sortierbaren <strong>Graphen</strong> mindestens einen Knoten v mit indeg(v) = 0 geben muss, weil es sonst keinen<br />
ersten Knoten in der topologischen Sortierung geben würde. Dieser Knoten wird markiert und bildet<br />
den Anfang der topologischen Sortierung. Alle markierten Knoten werden konzeptionell der Reihe<br />
nach aus dem <strong>Graphen</strong> entfernt. Dadurch erniedrigt sich der Eingangsgrad anderer Knoten und es<br />
gibt wieder einen oder mehrere neue Knoten mit indeg(v) = 0.<br />
9
Input: Ein gerichteter Graph G. Alle Knoten kennen ihren Eingangsgrad.<br />
Output: Eine topologische Sortierung von G oder die Information, dass es keine solche Sortierung<br />
gibt.<br />
L := { v ∈ V | indeg(v) == 0 }.<br />
for (i := 1..n) do<br />
if (L == {}) then<br />
es gibt keine Sortierung Exit<br />
endif<br />
entferne den ersten Knoten u aus L und gebe ihn mit i aus<br />
forall (Kanten e inzident zu u) do<br />
v := Zielknoten von e<br />
indeg(v) := indeg(v) – 1<br />
if (indeg(v) == 0) then<br />
füge v in L ein<br />
endif<br />
entferne e<br />
endforall<br />
entferne u<br />
endfor<br />
Dieser Pseudocode liefert eine topologische Sortierung eines <strong>Graphen</strong> wenn dieser zyklenfrei ist. Die<br />
Laufzeit dieses Algorithmus beträgt O(n + m). Die äussere for-Schleife wird für jeden Knoten einmal<br />
durchlaufen, also n mal. Die innere forall-Schleife geht für den Knoten u all seine ausgehenden,<br />
inzidenten Kanten durch. Im Ganzen werden in allen forall-Schleifen alle Kanten einmal besucht.<br />
Beispiel<br />
1 2<br />
3<br />
2<br />
3<br />
3<br />
4<br />
5 6<br />
4<br />
5 6 4 5 6<br />
3<br />
4 6 4<br />
6<br />
6<br />
Die markierten Knoten sind die Knoten mit indeg(v) = 0. In jedem Schritt wird ein Knoten dieser Art<br />
aus dem <strong>Graphen</strong> entfernt. Die resultierende topologische Sortierung ist:<br />
1 → 2 → 5 → 3 → 4 → 6<br />
3.2 Durchlaufen von <strong>Graphen</strong><br />
Wie schon bei den Bäumen besteht auch hier der Bedarf, alle Knoten in einer geordneten Weise zu<br />
besuchen und eventuell auszugeben. Gerade im Zusammenhang mit der Speicherung eines <strong>Graphen</strong><br />
auf dem Externspeicher (Serialisierung) ist es wichtig, alle Knoten zu besuchen und die Knotenobjekte<br />
und inzidenten Kantenobjekte abzuspeichern.<br />
Wiederum gibt es zwei Strategien mit denen alle Knoten besucht werden können: Die eine Strategie<br />
ist die Tiefensuche (DFS = depth first search), die andere ist die Breitensuche (BFS = breadth first<br />
search). Beide Strategien durchlaufen alle Knoten und alle Kanten, und haben sogar dieselbe Laufzeit.<br />
10
3.2.1 Tiefensuche (DFS)<br />
Bei der DFS-Strategie wird der Graph zunächst „in die Tiefe gehend durchsucht“. Die DFS-Strategie<br />
ist eine Verallgemeinerung des Preorder-Durchlaufprinzips bei Binärbäumen. Das Prinzip ist das<br />
folgende:<br />
• Kanten werden ausgehend von dem zuletzt entdeckten Knoten<br />
v, der mit noch unerforschten Kanten inzident ist, erforscht. w<br />
• Erreicht man von v aus einen noch nicht erforschten Knoten w,<br />
so verfährt man mit w genauso wie mit v.<br />
• Wenn alle mit w inzidenten Kanten erforscht sind, erfolgt ein<br />
Backtracking zu v.<br />
Es gibt viele verschiedene Arten, wie die DFS-Strategie implementiert werden kann. Die folgende ist<br />
also nur eine von vielen.<br />
v<br />
DFS mit drei Farben<br />
Bei dieser Methode gibt es drei Arten von Knoten (weiss, grau, schwarz). Am Anfang ist jeder Knoten<br />
weiss. Das bedeutet, dass er noch nicht entdeckt worden ist. Sobald ein Knoten entdeckt worden ist,<br />
wird er grau. Wenn ein Knoten komplett abgearbeitet ist, wird er schwarz gefärbt. Dies ist der Fall,<br />
wenn er das Backtracking zum Vorgängerknoten einleitet.<br />
Des Weiteren wird für jeden Knoten noch abgespeichert, wer sein Vorgänger ist. D.h., von welchem<br />
Knoten aus er entdeckt worden ist. Der Vorgänger wird in einem Array α gespeichert. Selbstverständlich<br />
wäre es auch möglich, den Vorgänger direkt im Knoten zu speichern.<br />
DFS (Graph G)<br />
forall (v in V) do<br />
farbe[v]:= weiss<br />
α[v]:= null<br />
endforall<br />
forall (v in V) do<br />
\\ so werden alle Zusammenhangskomponenten gefunden<br />
if (farbe[v] == weiss) then<br />
DFS-Visit(v)<br />
endif<br />
endforall<br />
DFS-Visit(v)<br />
farbe[v]:= grau<br />
forall (w adjazent zu v) do<br />
if (farbe[w] == weiss) then<br />
α[w]:= v<br />
DFS-Visit(w)<br />
endif<br />
endforall<br />
farbe[v]:= schwarz<br />
Durch dieses Verfahren werden alle Zusammenhangskomponenten gefunden. Jeder Knoten und jede<br />
Kante werden einmal besucht. Am Ende enthält α die Informationen eines Spannbaums für den <strong>Graphen</strong>,<br />
falls dieser zusammenhängend ist.<br />
Laufzeitanalyse: Die Methode DFS(G) benötigt offensichtlich O(n) Schleifendurchläufe. Die Methode<br />
DSF-Visit betrachtet für den Knoten v alle inzidenten, ausgehenden Kanten. Also dauern alle for-<br />
Schleifen von DFS-Visit zusammen ∑ v∈V outdeg(v) = m. Somit ergibt sich eine Laufzeit von O(n + m)<br />
11
Beispiel Tiefensuche<br />
1 2<br />
3 1 2 3<br />
1<br />
2 3<br />
4 5 6<br />
4 5 6<br />
4<br />
5 6<br />
1 2<br />
3 1 2<br />
3 1 2 3<br />
4 5 6<br />
4 5 6<br />
4<br />
5 6<br />
1 2<br />
3 1 2 3<br />
1<br />
2 3<br />
4 5 6<br />
4 5 6<br />
4 5 6<br />
1 2<br />
3 1 2 3<br />
1 2 3<br />
4 5 6<br />
4 5 6<br />
4<br />
5 6<br />
Als erstes wird in diesem Beispiel die Tiefensuche von Knoten 1 aus bis zum Knoten 6 gemacht.<br />
Danach sind alle Knoten ausser Knoten 4 abgearbeitet. Als letztes Knoten wird noch Knoten 4 gefunden<br />
und abgearbeitet.<br />
3.2.2 Topologische Sortierung mittels DFS<br />
Aus dem Algorithmus für die Tiefensuche kann ein zweiter Algorithmus für die topologische Sortierung<br />
von zyklenfreien <strong>Graphen</strong> gewonnen werden:<br />
DFS-Topological-Sort<br />
Counter := n<br />
rufe DFS(G) auf {<br />
sobald ein Knoten schwarz gefärbt wird, setze nummer[v] := counter;<br />
counter--<br />
}<br />
3.2.3 Breitensuche (BFS)<br />
Der Algorithmus für die Breitensuche lautet wie folgt:<br />
1. Für einen Knoten werden die noch nicht besuchten Nachbarknoten gesucht.<br />
2. Jeder Nachbarknoten wird ans Ende einer Warteschlange eingefügt.<br />
3. Solange die Warteschlange nicht leer ist, wähle den nächsten Knoten und beginne wieder bei 1.<br />
Nachfolgend sehen Sie den Algorithmus in Pseudocode. Der Knoten s bezeichnet dabei den Startknoten<br />
der Breitensuche. Q ist die Warteschlange (Queue).<br />
BFS(Graph G, Knoten s)<br />
forall (v in V) do<br />
v.gefunden := false<br />
// alle Knoten unentdeckt<br />
endforall<br />
s.gefunden := true<br />
Q = {s}<br />
while (Q nicht leer) do<br />
entferne das vorderste Element u aus Q<br />
forall (v in der Nachbarschaft von u) do // Alle Knoten die von u aus mit einer<br />
// Kante erreicht werden können<br />
12
if (v.gefunden == false) then<br />
v.gefunden := true<br />
füge v in Q ein<br />
endif<br />
endforall<br />
endwhile<br />
Dieser Algorithmus benötigt O(n + m) Zeit (wie DFS). Die while-Schleife wird so lange ausgeführt,<br />
bis es keine unentdeckten Knoten mehr gibt (also n mal). Für jeden Knoten wird die Nachbarschaft<br />
betrachtet. Dies sind so viele adjazente Knoten, wie der Knoten inzidente Kanten besitzt. Im Ganzen<br />
wird die forall-Schleife also m-mal ausgeführt.<br />
3.3 Kürzeste Pfade<br />
Problem 1: Finde den kürzesten Pfad zwischen zwei Knoten.<br />
Beispiel: Im Liniennetz der SBB möchten Sie den kürzesten Weg zwischen Ihrem Wohnort und<br />
Brugg-Windisch finden, damit Sie nicht zu viel Zeit für Ihren Schulweg verlieren.<br />
Problem 2: Finde alle kürzesten Pfade zwischen einem Knoten s und allen anderen Knoten des<br />
<strong>Graphen</strong>.<br />
Beispiel: Sie wollen herausfinden, wer aus Ihrer Klasse den kürzesten Schulweg hat.<br />
Problem 3: Finde für jedes Knotenpaar des <strong>Graphen</strong> den kürzesten Pfad.<br />
Beispiel: Sie erhalten einen Plan mit allen Standorten der FHNW. Nun dürfen Sie sich Ihr Schulgebäude<br />
selber aussuchen. Sie suchen dasjenige mit der geringsten Entfernung zu Ihrem<br />
Wohnort.<br />
Die beiden Algorithmen, die in diesem Kapitel vorgestellt werden, lösen beide Problem 2. Die Algorithmen<br />
sind nach ihren Erfindern benannt. Der berühmteste Algorithmus wurde 1959 von E.W.<br />
Dijkstra vorgeschlagen. Vor ihm entwickelten R. Bellman(1956) und L. Ford(1958) unabhängig voneinander<br />
denselben Algorithmus. Sowohl der Algorithmus von Dijkstra als auch der von Bellman und<br />
Ford beschäftigen sich mit kürzesten Pfaden zwischen einem Knoten und allen anderen Knoten im<br />
<strong>Graphen</strong>, dieses Problem wird auch als single source shortest path-Problem bezeichnet. Ausgehend<br />
von einer Quelle (source) werden die kürzesten Pfade zu allen anderen Knoten im <strong>Graphen</strong> berechnet.<br />
Was aber unterscheidet diese beiden Algorithmen, wenn sie doch dasselbe Ziel haben?<br />
Beide Algorithmen funktionieren für gewichtete <strong>Graphen</strong> (gerichtet und ungerichtet) mit positiven<br />
Kantenwerten. Der Algorithmus von Bellman und Ford kann zusätzlich noch mit gerichteten <strong>Graphen</strong><br />
arbeiten, deren Kantengewichte negativ sein dürfen.<br />
3.3.1 Einleitung, Begriffe<br />
Die hier definierten Begriffe sind recht intuitiv. Trotzdem müssen sie definiert werden, um Missverständnissen<br />
vorzubeugen.<br />
Pfad:<br />
Ein Pfad ist eine endliche Folge P = (v 0 , e 1 , v 1 , ... , e k , v k ) mit k ≥ 0 mit v i aus<br />
V und e i = (v i-1 , v i ) aus E. In einem Graph ohne Mehrfachkanten ist ein Pfad<br />
alleine durch die Folge von benachbarten Knoten definiert.<br />
Gewichtsfunktion: Die Gewichtsfunktion weist jeder Kante ein Gewicht zu. Diese Funktion wird<br />
meist mit w (weight) bezeichnet. w(e) = Gewicht der Kante e.<br />
Länge eines Pfades: Die Länge eines Pfades in einem gewichteten <strong>Graphen</strong> G = (V, E) mit Gewichtsfunktion<br />
w ist bestimmt durch:<br />
wP ( ) = ∑ we (<br />
i<br />
)<br />
k<br />
i=<br />
1<br />
Das ist die Summe der Kantengewichte der verwendeten<br />
Kanten.<br />
Distanz, Abstand: Der Abstand zweier Knoten u, v ist der kürzeste Pfad zwischen den beiden<br />
Knoten. Bezeichnet wird dieser Abstand mit dist(u, v). Kann v von u aus<br />
nicht erreicht werden ist der Abstand unendlich. Ein Knoten hat zu sich selber<br />
den Abstand 0.<br />
Weitere Voraussetzung: Der Graph, auf dem die Berechnung kürzester Pfade durchgeführt wird, ist<br />
ein einfacher Graph, er enthält also keine Schlingen oder Mehrfachkanten.<br />
13
Lemma: Sei s ein ausgezeichneter Knoten. Dann gilt: dist(s, v) ≤ dist(s, u) + w(u, v) für alle u, v in V.<br />
Beweis: dist(s, v) ist der Wert des kürzesten Pfades zwischen s und v. Wird dabei die Kante {u, v} für<br />
die Berechnung benutzt, so ist dist(s, u) + w(u, v) = dist(s, v). Wenn sie nicht benutzt wird, kann<br />
dist(s, v) nicht grösser sein als dist(s, u) + w(u, v), weil ansonsten nicht der kürzeste Pfad gefunden<br />
worden ist.<br />
3.3.2 Allgemeine Methoden<br />
Die beiden Algorithmen zur Berechnung der kürzesten Pfade verwenden die gleiche Initialisierung<br />
init(Graph G, Knoten s) und dieselbe Methode test(…). Der Graph G kann gerichtet oder<br />
ungerichtet sein. Der Knoten s ist der Startknoten von dem aus die kürzesten Pfade berechnet werden.<br />
Die Werte d[v] enthalten den Wert des momentan kürzesten Pfades von s nach v. Diese Werte<br />
sind dabei obere Schranken für dist(s, v). Am Ende der Algorithmen enthält d[v] den Wert des kürzesten<br />
Pfades von s nach v.<br />
init(Graph G, Knoten s)<br />
forall (v in V) do<br />
d[v] := ∞<br />
// α[v]:= null //enthält den Nachbarknoten über den der kürzeste Pfad geht<br />
endforall<br />
d[s]:= 0 // s hat zu sich selber Abstand 0<br />
Die Methode test(u, v) testet, ob eine gegebene Kante (u, v) den momentanen kürzesten Pfad<br />
von s nach v abkürzen kann. Es wird dabei getestet, ob die Summe aus der Distanz der Kante (u, v)<br />
und der Länge des Pfades von s nach u kleiner ist als die bisherige Länge des Pfades von s nach v.<br />
boolean test(u,v)<br />
if (d[v] > d[u] + w(u, v)) then<br />
d[v] := d[u] + w(u, v);<br />
// α[v] := u //enthält den Nachbarknoten über den der kürzeste Pfad geht<br />
endif<br />
Die Verweise auf den Knoten, von dem aus der eine Knoten erreicht worden ist, werden in dieser<br />
Situation noch nicht gebraucht.<br />
3.4 Der Algorithmus von Dijkstra<br />
Dieser Algorithmus ist ein Greedy-Algorithmus. Greedy-Algorithmen (gierige Algorithmen oder Raffkealgorithmen)<br />
bilden in der Informatik eine spezielle Klasse von Algorithmen. Sie zeichnen sich dadurch<br />
aus, dass sie immer denjenigen Folgezustand auswählen, der zum Zeitpunkt der Wahl den grössten<br />
Gewinn bzw. das beste Ergebnis verspricht. Bei Greedy-Algorithmen gibt es kein Backtracking. Eine<br />
Lösung, die einmal als fix gekennzeichnet worden ist, wird im weiteren Verlauf nicht mehr verändert.<br />
Dieser Algorithmus ist sehr ähnlich zum BFS-Algorithmus. Der BFS liefert für den Fall, in dem alle<br />
Gewichte dieselben sind, eine Lösung für dieses Problem.<br />
3.4.1 Prinzip und Pseudocode<br />
Der Algorithmus von Dijkstra 3 arbeitet mit einer „Wellenfront“-Strategie. Für alle Knoten hinter der<br />
Front ist der endgültige Distanzwert bereits ermittelt worden. Wir nennen diese Knoten permanent und<br />
speichern sie in PERM ab. Für diese Knoten v gilt also: dist(s, v) = d[v]. Die Knoten vor der Front sind<br />
noch nicht bearbeitet worden. Für alle Knoten auf der Front ist die Berechnung in Gange. Diese Knoten<br />
werden in einer Prioritätswarteschlange PQ gespeichert. Die Prioritäten entsprechen den momentanen<br />
Abständen zum Startknoten.<br />
Anfangszustand: Am Anfang ist die Menge der permanenten Knoten leer. Die Menge der Knoten auf<br />
der Front enthält nur den Startknoten s.<br />
3 http://de.wikipedia.org/wiki/Algorithmus_von_Dijkstra<br />
14
• Nimm einen Knoten u aus PQ, welcher den kleinsten Abstand zu s hat.<br />
• Für alle Nachbarknoten v von u überprüfe mittels test(u, v) die Distanz. Wenn nötig wird diese<br />
Distanz aktualisiert. Ist der Nachbarknoten v schon in PQ, wird seine Priorität dort aktualisiert. Ist<br />
er noch nicht in dieser Menge, wird er mit dem aktuellen Wert der Distanz in diese Menge eingefügt.<br />
• Nimm den Knoten u in die Menge der permanenten Knoten auf.<br />
Pseudocode<br />
Gegeben ist ein gewichteter Graph G mit nicht negativen Knotengewichten w und ein Startknoten s.<br />
Dijkstra (G =(V, E), w, s)<br />
init(G, s)<br />
PERM := {}<br />
// Menge der permanent markierten Knoten<br />
PQ := Prioritätswarteschlange // z.B. Min-Heap<br />
PQ.insert(s, d[s]) // füge s mit Prio d[s] = 0 in PQ ein<br />
while(!PQ.isEmpty()) do // es wurden noch nicht alle Distanzen definitiv gesetzt<br />
u := PQ.removeMin() // u ist ein Knoten mit der kleinsten Distanz<br />
forall(v benachbart zu u) do // Alle Knoten die von u aus mit einer Kante<br />
// erreicht werden können<br />
if (d[v] == ∞) then<br />
PQ.insert(v, d[v])<br />
endif<br />
if (test(u, v)) then // Teste ob dieser Pfad kürzer ist.<br />
// Wenn ja, wird in Test(u,v) d[v] heruntergestzt.<br />
PQ.updatePrio(v, d[v]) // setze die Priorität von v in PQ auf d[v]<br />
endif<br />
endforall<br />
PERM.add(u)<br />
// füge u in die Menge PERM ein<br />
endwhile<br />
Bemerkung<br />
Die Distanzwerte können auch in den Knoten selber gespeichert werden. Um zu markieren, ob ein<br />
Knoten schon als permanent markiert worden ist, kann man dies auch im Knoten selber speichern.<br />
3.4.2 Laufzeitanalyse<br />
In jedem Durchlauf der while-Schleife wird genau ein Knoten in die Menge PERM eingefügt. Das<br />
Einfügen in eine ungeordnete Menge kann in O(1) gemacht werden. Da n Knoten vorhanden sind,<br />
resultiert für das Einfügen die Zeitkomplexität O(n).<br />
Das Minimum in der Prioritätswarteschlange muss n mal gesucht werden, da jeder Knoten einmal das<br />
Minimum ist. Wie schnell das Minimum aus der Prioritätswarteschlange geholt und entfernt werden<br />
kann, hängt von der Realisierung der Prioritätswarteschlange ab. Auch die Zeit, die für das Einfügen<br />
und das Aktualisieren des Schlüssels benötigt wird, hängt von der Prioritätswarteschlange ab. Jeder<br />
Knoten wird einmal in die Prioritätswarteschlange eingefügt. Der Wert muss höchstens m mal verändert<br />
werden. Generell sieht die Laufzeit also so aus:<br />
T = O(n) + n·T min aus PQ extrahieren + n·T einfügen in PQ + m·T aktualisieren in PQ<br />
Nehmen wir nun an, dass die Prioritätswarteschlange durch einen binären Min-Heap realisiert worden<br />
ist. Dann wird die gesamte Laufzeit zu:<br />
T = O(n) + n·O (log n) + n·O(log n) + m·O(log n)) = O(n log n + m log n)<br />
T = O ((n + m) log n)<br />
3.4.3 Problem bei negativen Gewichten<br />
Wie schon an früherer Stelle angedeutet, setzt der Algorithmus von Dijkstra nicht negative Kantengewichte<br />
voraus. Die folgenden Bilder zeigen ein Beispiel, bei dem mit dem Algorithmus von Dijkstra<br />
Probleme auftreten. Nachdem die Distanz zum oberen Knoten auf 1 gesetzt und dieser Knoten in die<br />
Menge PERM aufgenommen wurde, wird er als Nachbar vom Knoten rechts unten noch einmal in der<br />
Prioritätswarteschlange PQ erwartet, obwohl er in PQ gar nicht mehr enthalten ist.<br />
15
1<br />
∞<br />
1<br />
1<br />
1<br />
1<br />
0 - 4 0 - 4 0 -4<br />
2<br />
2<br />
2<br />
∞<br />
2<br />
2<br />
3.5 Der Algorithmus von Bellman und Ford<br />
Der Algorithmus von Bellman und Ford, der hier erklärt wird, kann auch mit negativen Gewichten<br />
umgehen (wenn der Graph gerichtet ist). Wenn negative Gewichte zugelassen sind, tritt ein grundsätzliches<br />
Problem auf: es kann ein Kreis negativer Länge von s aus erreichbar sein und damit<br />
dist(s, v)= –∞ für alle Knoten v gelten, die von s aus erreichbar sind. Denn dieser Kreis kann unendlich<br />
viele Male durchlaufen werden und vermindert den Wert des Pfades bei jedem<br />
Durchlaufen.<br />
Ein ungerichteter Graph enthält immer Zyklen, ausser es handelt sich um einen Baum.<br />
Wenn nun eine Kante negatives Gewicht hat, ist schon ein Kreis negativer Länge<br />
vorhanden.<br />
Dieser Algorithmus ist kein Greedy-Algorithmus. Die Werte der Abstände können sich<br />
bis zum Schluss verändern. Natürlich werden aber auch hier Zwischenwerte gespeichert und aktualisiert.<br />
3.5.1 Prinzip und Pseudocode<br />
• Es gibt n Phasen, wobei in der ersten Phase initialisiert wird.<br />
• In jeder weiteren Phase wird jede Kante mit test(u, v) überprüft und d[v] aktualisiert.<br />
• Am Ende enthält das Array d[v] für alle Knoten v die Distanz zwischen s und v.<br />
Bellman-Ford(G = (V, E), w, s)<br />
init(G,s)<br />
for (k := 1..n-1) do<br />
forall ((u,v) in E) do<br />
test(u, v)<br />
endforall<br />
endfor<br />
-3<br />
-3<br />
-3<br />
Die genaue Vorgehensweise wollen wir anhand des oben stehenden <strong>Graphen</strong> illustrieren. Dabei ist es<br />
zentral, dass wir eine Kantenreihenfolge festlegen, in der alle Kanten des <strong>Graphen</strong> besucht werden.<br />
Obwohl der Algorithmus für alle möglichen Kantenreihenfolgen funktioniert, hat die Wahl Einfluss<br />
darauf, wie früh die kürzesten Distanzen entdeckt werden. Mit gewissen Reihenfolgen werden die<br />
korrekten Distanzen erst in der letzten Iteration gefunden, während mit anderen Kantenreihenfolgen<br />
die kürzesten Distanzen bereits in der ersten Iteration gefunden werden.<br />
Für unser Beispiel wählen wir die folgende Kantenreihenfolge: (A,C), (B,C), (B,D), (s,A), (C,D), (A,B).<br />
Phasen k d[s] d[C] d[C] d[D] d[A] d[D] d[B]<br />
0 (init) 0 ∞ ∞ ∞ ∞ ∞ ∞<br />
1 3 3 – 1 = 2<br />
2 3 + 2 = 5 2 + 2 = 4 2 + 8 = 10 4 + 3 = 7<br />
3, 4, 5 0 4 4 7 3 7 2<br />
3.5.2 Laufzeitanalyse<br />
Die Laufzeit dieses Algorithmus ist, im Gegensatz zum Algorithmus von Dijkstra, von keiner Datenstruktur<br />
abhängig. Die for-Schleife wird (n – 1)-mal durchlaufen. Bei jedem Durchlauf werden alle m<br />
Kanten geprüft. Also ist die Laufzeit O(n·m).<br />
Falls ein Kreis negativer Länge auftritt, ist die Laufzeit für die Berechnung unendlich gross. Dass die<br />
Schleife nur n mal ausgeführt wird, verhindert eine endlose Laufzeit beim Algorithmus.<br />
16
3.6 Lernkontrolle<br />
3.6.1 Topologische Sortierung<br />
a) Kann ein ungerichteter Graph topologisch sortiert werden?<br />
b) Geben Sie für den folgenden <strong>Graphen</strong> eine topologische Sortierung an.<br />
1<br />
4<br />
6<br />
2<br />
3 5 7<br />
c) Zeichnen Sie noch zwei Kanten ein, welche die topologische Sortierung nicht beeinträchtigen.<br />
3.6.2 Durchlaufen von <strong>Graphen</strong><br />
Zeichnen Sie im folgenden <strong>Graphen</strong> die verschiedenen Schritte einer a) Tiefensuche und b) Breitensuche<br />
ein. Startknoten ist der Knoten 1. Geben Sie auch an, in welcher Reihenfolge die Knoten gefunden<br />
werden.<br />
1<br />
4<br />
6<br />
2<br />
3<br />
5 7<br />
3.6.3 Kürzeste Pfade<br />
a) Sie haben gesehen, dass die Laufzeit des Dijkstra-Algorithmus O((n + m) log n) ist. Rechnen Sie<br />
nun aus, wie die Laufzeit wäre, wenn anstatt der Prioritätswarteschlange eine sortierte Liste verwendet<br />
würde.<br />
b) Sie erhalten einen <strong>Graphen</strong> und sollen nun herausfinden, ob er eine Kante mit negativem Gewicht<br />
enthält. Schreiben Sie in Pseudocode eine entsprechende Testmethode. Geben Sie zudem die<br />
Laufzeit Ihres Algorithmus an.<br />
c) Bestimmen Sie eine möglichst scharfe untere Grenze der Laufzeit für einen Algorithmus zur<br />
Überprüfung von Kanten mit negativem Gewicht.<br />
17
4 Spannbäume<br />
Spannbäume sind Bäume die aus Kanten eines <strong>Graphen</strong> bestehen. Sie wissen, dass ein Baum mit n<br />
Knoten genau n-1 Kanten enthält. Für einen Spannbaum werden also n – 1 Kanten aus der Menge<br />
der Kanten ausgesucht, so dass ein zusammenhängender Graph entsteht. In diesem <strong>Graphen</strong> gibt es<br />
zwischen zwei Knoten genau einen Weg gibt.<br />
Wie ein Spannbaum berechnet wird, wissen Sie im Prinzip schon. Die Suchwege bei Breiten- und<br />
Tiefensuche bilden einen Spannbaum. Im Code von BFS und DFS ist das Berechnen eines Spannbaumes<br />
schon „eingebaut“. Speichert jeder Knoten den adjazenten Knoten, von dem er entdeckt<br />
wurde, dann entspricht dies dem Speichern des Vaters im Spannbaum.<br />
Das Beispiel zeigt einen Spannbaum, der bei der Breitensuche vom grauen Knoten aus berechnet<br />
wurde.<br />
Ein Spannbaum kann auch als Gerüst eines <strong>Graphen</strong> gesehen werden. Spannbäume gibt es nur in<br />
zusammenhängenden <strong>Graphen</strong>. (Damit ein Graph zusammenhängend ist, muss er deshalb mindestens<br />
n-1 Kanten haben.)<br />
Übungsaufgaben<br />
Geben Sie für den folgenden <strong>Graphen</strong> einen möglichen Spannbaum an.<br />
Versuchen Sie im folgenden <strong>Graphen</strong> einen Spannbaum zu finden, dessen Kanten möglichst kurz<br />
sind.<br />
4.1 Minimale Spannbäume<br />
In der Praxis braucht man meist nicht irgendeinen Spannbaum, sondern ist auf der Suche nach einem<br />
minimalen Spannbaum. Minimale Spannbäume werden an den verschiedensten Orten eingesetzt:<br />
Telekommunikationsfirmen versuchen ein möglichst kurzes Hauptnetz zu haben. Das spart Materialund<br />
Unterhaltskosten. Es muss dabei sichergestellt werden, dass alle Ortschaften am Netz angeschlossen<br />
sind. Vertriebsfirmen möchten ihre Verteillager so legen, dass die Wege zwischen den<br />
einzelnen Lagern möglichst kurz sind.<br />
18
4.2 Formale Definitionen<br />
Spannbaum:<br />
Ein Spannbaum ST (= Spanning Tree) eines <strong>Graphen</strong> G = (V, E) besteht<br />
aus n – 1 Kanten der Menge E. Diese Kanten bilden einen zusammenhängenden<br />
<strong>Graphen</strong>, der alle Knoten aus V enthält.<br />
Minimaler Spannbaum: Ein minimaler Spannbaum MST (= Minimum Spanning Tree) ist ein Spanbaum,<br />
bei dem die Summe der Kantengewichte minimal ist.<br />
(Es kann in einem <strong>Graphen</strong> mehr als einen minimalen Spannbaum geben.)<br />
4.3 Algorithmus von Prim<br />
Der Algorithmus von Prim, auch Prim-Dijkstra-Algorithmus genannt, funktioniert analog zum Dijkstra-<br />
Algorithmus. Der einzige Unterschied liegt darin, dass der Abstand zum Spannbaum und nicht der<br />
Abstand zum Startknoten als Priorität (für die Prioritätswarteschlange) verwendet wird.<br />
Der Abstand eines Knoten zum Spannbaum ist gleich dem kleinsten Gewicht einer Kante, die den<br />
Knoten mit einem Knoten des Spannbaums verbindet.<br />
4<br />
7 1<br />
8<br />
Im Beispiel hat der graue Knoten vier inzidente Kanten, die ihn mit Knoten aus dem bisherigen<br />
Spannbaum verbinden. Die Kante mit dem geringsten Gewicht hat das Gewicht w(e) = 1. Somit hat<br />
der graue Knoten den Abstand 1 zum Spannbaum.<br />
Abstand von v zum minimalen Spannbaum MST:<br />
dist(v, MST) = min { w(e) | e = { v, u } ∈ E und v, u ∈ V}<br />
Ist der Knoten v zu keinem Knoten des Spannbaums adjazent, ist die Distanz unendlich.<br />
4.3.1 Prinzip<br />
Vor der Implementierung des Prim-Algorithmus wird hier das Prinzip vorgestellt. Als erstes wird die<br />
Startphase beschrieben. Danach wird gesagt, wie der Spannbaum in jedem Schritt um eine Kante<br />
wächst. Als letztes wird noch die Endbedingung genannt.<br />
Start: Als Ausgangspunkt wird ein beliebiger Knoten genommen. Dieser ist zu Beginn der einzige<br />
Knoten im Spannbaum. Der Spannbaum enthält noch keine Kante.<br />
Wachsen: Es wird der Knoten gesucht, der den kleinsten Abstand vom Spannbaum hat. Dieser<br />
Knoten darf aber nicht schon im Spannbaum enthalten sein.<br />
Der Knoten wird dann über die minimale Kante mit dem Spannbaum verbunden und in die<br />
Menge der Knoten des Spannbaums aufgenommen.<br />
Ende: Wenn alle Knoten im Spannbaum enthalten sind, dann ist die Berechnung zu Ende.<br />
4.3.2 Beispiel<br />
Gegeben ist ein Graph mit nebenstehender Adjazenzmatrix.<br />
Die Werte in den Feldern geben die<br />
Kantengewichte an.<br />
s 1 2 3 4 5 6<br />
s 0 2 0 5 7 0 0<br />
1 2 0 8 6 0 3 0<br />
2 0 8 0 0 0 2 4<br />
3 5 6 0 0 1 0 5<br />
4 7 0 0 1 0 0 1<br />
5 0 3 2 0 0 0 2<br />
6 0 0 4 5 1 2 0<br />
19
In diesem Beispiel wird ein Spannbaum ausgehend vom Knoten s berechnet. Für jeden Schritt werden<br />
die folgenden Informationen angegeben. Ein Bild zeigt den momentanen minimalen Spannbaum (linke<br />
Spalte). Der Knoten, der als nächstes in den Spannbaum aufgenommen wird, die Queue und die<br />
Menge der Knoten, die im Spannbaum sind, werden in der mittleren Spalte angegeben. In der Spalte<br />
ganz rechts werden die Abstände der Knoten zum aktuellen Spannbaum angegeben.<br />
Queue: {s}<br />
s 1 2 3 4 5 6<br />
MST: { }<br />
0 ∞ ∞ ∞ ∞ ∞ ∞<br />
s<br />
1 2<br />
3<br />
5<br />
Knoten: s<br />
Queue: {1,3,4}<br />
MST: { }<br />
s 1 2 3 4 5 6<br />
0 2 ∞ 5 7 ∞ ∞<br />
4<br />
6<br />
s<br />
1 2<br />
3<br />
5<br />
Knoten: 1<br />
Queue: {5, 3, 4, 2}<br />
MST: {s}<br />
s 1 2 3 4 5 6<br />
0 0 8 5 7 3 ∞<br />
4<br />
6<br />
s<br />
1 2<br />
3<br />
5<br />
Knoten: 5<br />
Queue: {2, 6, 3, 4}<br />
MST: {s, 1} s 1 2 3 4 5 6<br />
0 0 2 5 7 0 2<br />
4<br />
6<br />
s<br />
1 2<br />
3<br />
5<br />
Knoten: 2<br />
Queue: {6, 3, 4}<br />
MST: {s, 1, 5} s 1 2 3 4 5 6<br />
0 0 0 5 7 0 2<br />
4<br />
6<br />
s<br />
1 2<br />
3<br />
5<br />
Knoten: 6<br />
Queue: {4, 3}<br />
MST: {s, 1, 5, 2} s 1 2 3 4 5 6<br />
0 0 0 5 1 0 0<br />
4<br />
6<br />
s<br />
1 2<br />
3<br />
5<br />
Knoten: 4<br />
Queue: {4}<br />
MST: {s, 1, 5, 2, 6 } s 1 2 3 4 5 6<br />
0 0 0 1 0 0 0<br />
4<br />
6<br />
s<br />
1 2<br />
3<br />
5<br />
Knoten: 3<br />
Queue: { }<br />
MST: {s, 1, 5, 2, 6, 4} s 1 2 3 4 5 6<br />
0 0 0 0 0 0 0<br />
4<br />
6<br />
20
Übungsaufgaben<br />
a) Zeichnen Sie den <strong>Graphen</strong>, der zu der nachfolgend gegebenen Adjazenzmatrix (links) gehört.<br />
Erzeugen Sie dann den minimalen Spannbaum für diesen <strong>Graphen</strong>. Füllen Sie dabei die rechte<br />
Tabelle der Gewichte aus (rechts). Geben Sie in dieser Tabelle in der Spalte ganz links an, welches<br />
der aktuelle Knoten ist (analog zu den vorherigen Beispielen).<br />
b) Geben Sie die Menge der Kanten an, die im minimalen Spannbaum enthalten sind. Z.B. {s, 3}<br />
c) Welches Gewicht hat der hier erzeugte minimale Spannbaum.<br />
d) Hätte in einem Schritt auch eine andere Kante (bzw. ein anderer Knoten) in den MST aufgenommen<br />
werden können?<br />
s 1 2 3 4 5 6<br />
s 0 1 0 10 0 0 0<br />
1 1 0 2 0 0 7 0<br />
2 0 2 0 0 8 5 0<br />
3 10 0 0 0 2 0 0<br />
4 0 0 8 2 0 3 0<br />
5 0 7 5 0 3 0 4<br />
6 0 0 0 0 0 4 0<br />
s<br />
s 1 2 3 4 5 6<br />
0 ∞ ∞ ∞ ∞ ∞ ∞<br />
4.3.3 Implementierung<br />
Für die Implementierung des Algorithmus von Prim können Methoden analog zu denen des Dijkstra-<br />
Algorithmus verwendet werden. Diese müssen jedoch etwas abgeändert werden. Zuerst werden die<br />
abgeänderten Hilfsmethoden danach die Hauptmethode beschrieben.<br />
Hilfsmethoden<br />
Die beiden Hilfsmethoden init(…) und test(…) müssen nur wenig angepasst werden. Das zentrale<br />
Element, welches ändert, ist d[v]. Dieser Wert entspricht nun dem Abstand vom Spannbaum.<br />
init(Graph G, Knoten s)<br />
forall (v in V) do<br />
d[v] := ∞ // minimaler Abstand zum Spannbaum<br />
α[v] := null // enthält den Nachbarknoten, über den der kürzeste Weg geht<br />
endforall<br />
d[s] := 0<br />
// s ist der erste Knoten des Spannbaums und hat deshalb den<br />
// Abstand 0 vom Spannbaum<br />
Die Methode init(…) initialisiert für jeden Knoten den Abstand vom Spannbaum. Am Anfang haben<br />
alle Knoten bis auf den Startknoten den Abstand unendlich.<br />
Die Methode test(…) wird aufgerufen, wenn eine neue Kante (und somit auch ein neuer Knoten u) in<br />
den MST aufgenommen wird. Die Methode wird dabei verwendet, um zu testen, ob die Nachbarn v<br />
des neuen Knoten u über die inzidenten Kanten von u schneller erreicht werden können als über<br />
Kanten inzident zu den anderen Knoten des MST.<br />
boolean test(e := {u,v})<br />
if (w(e) < d[v]) then // Wenn die aktuelle Kante e geringeres Gewicht hat,<br />
// als das aktuelle Minimum<br />
d[v] := w(e); // wird der Wert des Minimus auf das Gewicht der Kante e gesetzt<br />
α[v] := u;<br />
endif<br />
In dieser Variante wird bei einem erfolgreichen Test für den Knoten v der Knoten u als derjenige<br />
Knoten gespeichert, über den v am schnellsten erreicht wird. Man könnte aber auch die Kante speichern,<br />
über die der Knoten v mit dem geringsten Gewicht erreicht wird. Das Ziel beider Methoden ist<br />
es, dass am Ende des Algorithmus nachvollziehbar ist, welche Kanten den MST bilden.<br />
21
Hauptmethode<br />
Die Hauptmethode benötigt die Hilfsmengen MST und queue. Wie schon erwähnt, ist die Priorität der<br />
Knoten in der Prioritätswarteschlange gleich dem Abstand vom Spannbaum und nicht gleich dem<br />
Abstand vom Startknoten s.<br />
Vorarbeiten: Mit der Methode init(…) wird der Anfangszustand der Abstände initialisiert. Alle<br />
Knoten haben einen unendlichen Abstand von s. Nur s hat Abstand 0.<br />
Eine Prioritätswarteschlange PQ und eine Menge zur Speicherung der Spannbaumknoten<br />
MST wird erzeugt.<br />
Schleife: Solange noch nicht alle Koten im Spannbaum aufgenommen sind, wird die Schleife<br />
ausgeführt.<br />
1. Entnehme den Knoten u mit der geringsten Distanz zum Spannbaum aus der<br />
Prioritätsschlange PQ.<br />
2. Füge den Knoten in die Menge der Knoten des Spannbaums ein (MST).<br />
3. Überprüfe mit test(…), ob Nachbarknoten v von u über u eine geringere Distanz<br />
zum Spannbaum haben.<br />
4. Verändere die Prioritäten der Nachbarn entsprechend dem Ergebnis.<br />
prim (G :=(V,E), Gewichtsfunktion w, Startknoten s)<br />
init(G, s)<br />
MST := {}<br />
PQ := Prioritärsschlange<br />
PQ.insert(s, d[s])<br />
while (|MST| < n) do<br />
u := PQ.removeMin()<br />
MST.insert(u)<br />
forall (Nachbarn v von u) do<br />
if (d[v] == ∞) then<br />
PQ.insert(v, d[v])<br />
endif<br />
if (test({u,v}) then<br />
PQ.updatePrio(v, d[v])<br />
endif<br />
endforall<br />
endwhile<br />
22