- jockwitz.de

Werbung
Selbstständigkeitserklärung
Hiermit versichere ich, dass ich die vorliegende Bachelorarbeit selbstständig und nur unter
Zuhilfenahme der angegebenen Quellen erstellt habe.
Andreas Groll
Freiberg, den 28.11.2005
Einverständniserklärung
Hiermit erkläre ich mein Einverständnis für die öffentliche Ausstellung der Bachelorarbeit
in der Universitätsbibliothek ”Georgius Agricola” der TU Bergakademie Freiberg.
Andreas Groll
Freiberg, den 28.11.2005
Selbstständigkeitserklärung
Hiermit versichere ich, dass ich die vorliegende Bachelorarbeit selbstständig und nur unter
Zuhilfenahme der angegebenen Quellen erstellt habe.
Robert Jockwitz
Freiberg, den 28.11.2005
Einverständniserklärung
Hiermit erkläre ich mein Einverständnis für die öffentliche Ausstellung der Bachelorarbeit
in der Universitätsbibliothek ”Georgius Agricola” der TU Bergakademie Freiberg.
Robert Jockwitz
Freiberg, den 28.11.2005
BACHELORARBEIT
Zur Erlangung des akademischen Titels
Bachelor of Science
V ISUALISIERUNG GRUNDLEGENDER
G RAPHENALGORITHMEN
Andreas Groll & Robert Jockwitz
Abgabedatum: 28.11.2005
1. Gutachter: Prof. Dr. Martin Sonntag
2. Gutachter: Dipl. Math. Anja Kohl
Institut für Diskrete Mathematik und Algebra
Fakultät für Mathematik und Informatik
Technische Universität Bergakademie Freiberg
Danksagung
An dieser Stelle möchten wir uns bei unserem Betreuer
Prof. Dr. rer. nat. habil. Martin Sonntag
für die umfangreiche und intensive Betreuung
während der Erstellung unserer Bachelorarbeit recht herzlich bedanken.
I NHALTSVERZEICHNIS
Inhaltsverzeichnis
1. Vorwort
1
I.
2
Einführung Graphentheorie
2. Begriffsdefinition
2
II. Algorithmen
6
3. Minimalgerüste
3.1. Algorithmus von Kruskal . . . . . . .
3.1.1. Mathematischer Algorithmus .
3.1.2. Implementation in Java . . . .
3.2. Algorithmus von Prim . . . . . . . .
3.2.1. Mathematischer Algorithmus .
3.2.2. Implementation in Java . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
6
7
7
9
12
12
13
4. Kürzeste Wege
15
4.1. Algorithmus von Dantzig/Dijkstra . . . . . . . . . . . . . . . . . . . . . 16
4.2. Implementation in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
5. Das Chinesische Briefträgerproblem
21
5.1. Algorithmus von Hierholzer . . . . . . . . . . . . . . . . . . . . . . . . 23
5.2. Implementation in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
6. Das Traveling Salesman Problem
26
6.1. Christofides-Heuristik . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
6.2. Implementation in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
7. Maximalstromproblem
31
7.1. Algorithmus von Ford-Fulkerson . . . . . . . . . . . . . . . . . . . . . . 32
7.1.1. Mathematische Betrachtung . . . . . . . . . . . . . . . . . . . . 33
7.1.2. Implementation in Java . . . . . . . . . . . . . . . . . . . . . . . 35
8. Matchingprobleme
38
8.1. Ungarische Methode . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
8.1.1. Mathematische Betrachtung . . . . . . . . . . . . . . . . . . . . 38
8.1.2. Implementation in Java . . . . . . . . . . . . . . . . . . . . . . . 40
i
I NHALTSVERZEICHNIS
III. Das Programm GraphCalc
44
9. Klassenaufbau
44
10. Komponenten
10.1. GraphHQ . . . . . . . . . . .
10.2. GraphPanel . . . . . . . . . .
10.3. Graphenformat . . . . . . . .
10.4. GraphParser und GraphWriter
10.5. Convert-Tools . . . . . . . . .
10.6. Console . . . . . . . . . . . .
10.7. GraphMenu . . . . . . . . . .
10.8. GraphPrint . . . . . . . . . . .
10.9. Weitere Klassen . . . . . . . .
45
45
47
50
52
53
55
56
56
57
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
11. GraphCalc Anwendungsfälle
59
11.1. Installation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
11.2. Durchführen eines Algorithmus . . . . . . . . . . . . . . . . . . . . . . 60
11.3. Konvertieren von Graphen . . . . . . . . . . . . . . . . . . . . . . . . . 61
IV. Anhang
62
A. Datenträger
62
B. Abbildungsverzeichnis
63
C. Listings
64
D. Literatur
65
ii
1
VORWORT
1. Vorwort
Der Inhalt dieser Bachelorarbeit ist mit dem Titel „Visualisierung Graphentheoretischer
Algorithmen“ noch sehr grob umschrieben. Die Aufgabenstellung war, eine Software zu
entwickeln, die die fundamentalen graphentheoretischen Algorithmen enthält, die in der
Vorlesung „Algorithmen und Graphentheorie“ an der TU Bergakademie Freiberg von
Prof. Dr. rer. nat. habil. Sonntag gelehrt werden. Das so entstandene Programm stellt
dem Endbenutzer eine Oberfläche zur Verfügung, mit der er Graphen eingeben, editieren und die bekannten Algorithmen auf eben diese Graphen anwenden kann. Dabei ist es
dem Benutzer möglich, auch komplexe Graphen mit den Algorithmen zu bearbeiten und
sich dabei die wichtigen Teilschritte anzeigen zu lassen. Bei der Entwicklung des Programmes wurde viel Wert darauf gelegt, neben einer einfachen Bedienbarkeit für normale
Anwender, dem Lehrenden ein effizientes Programm zur Verfügung zu stellen, um dieses
auch während einer Lehrveranstaltung zur Präsentation der Algorithmen zu nutzen. Die
im Programm enthaltenen Algorithmen sind: Kruskal, Prim, Dantzig/Dijkstra, Hierholzer, Christofides, Ford-Fulkerson und die Ungarische Methode.
Der Umfang dieser sieben Algorithmen machte es notwendig, die Bachelorarbeit von
zwei Personen bearbeiten zu lassen. Schon allein der Quellcode des gesamten Programmes umfasst mehr als 19000 Zeilen. Mit der Teilung der Arbeit musste eine klare Trennung in zwei Teile erfolgen, mit annähernd gleichem Umfang jeweils im mathematischen
und programmiertechnischen Teil. Darauf basierend erfolgt nun eine Aufschlüsselung der
Bereiche dieser Bachelorarbeit nach dem Autor.
Andreas Groll ist für folgende Bereiche verantwortlich: Vorwort, Minimalgerüste, Maximalstromproblem, Matchingprobleme, Klassenaufbau, GraphHQ und GraphPanel. Diesen Abschnitten entsprechend erfolgte auch die Programmierung.
Die bearbeiteten Abschnitte von Robert Jockwitz sind: Begriffsdefinition, Kürzeste Wege,
Das chinesische Briefträgerproblem, Das Traveling Salesman Problem, Graphenformat,
Graphparser und GraphWriter, Convert-Tools, Console, GraphMenu, GraphPrint, Weitere
Klassen und GraphCalc Anwendungsfälle.
In einigen Fällen war es nicht möglich, eine klare Trennung durchzuführen. So ist die
Planungsphase ein Beispiel dafür, dass ein umfangreiches Softwareprojekt mit mehreren Programmierern nur mit ausreichender Zusammenarbeit und gut durchdachter Versionsverwaltung realisierbar ist. Die einzelnen Aufgabenbereiche dieser Bachelorarbeit
konnten deshalb nur entstehen, weil in grundlegenden Prozessen der Softwareentwicklung zusammen gearbeitet wurde.
1
2
BEGRIFFSDEFINITION
Teil I.
Einführung Graphentheorie
2. Begriffsdefinition
In diesem Kapitel werden einige Grundbegriffe der Graphentheorie erklärt, welche im
weiteren Verlauf der Bachelorarbeit Verwendung finden. Die Definition der Grundbegriffe erfolgt analog zu [Vol91], [Tit03], [Cla94] und [Son04]. Im Nachfolgenden werden,
wenn nicht anders vereinbart, ausschließlich ungerichtete Graphen betrachtet. Für den
gerichteten Fall können die Definitionen meist analog übertragen werden. Ausnahmen
bilden lediglich Begriffe, die keine solche Analogie zulassen.
Graph
Ein ungerichteter Graph ist ein Tripel G = (V, E, ϕ ) (Kurzschreibweise: G =
(V, E)), bestehend aus einer nichtleeren Knotenmenge V , einer davon disjunkten Kantenmenge E und einer Inzidenzfunktion ϕ . Die Inzidenzfunktion
ϕ ordnet jedem e ∈ E eine nichtleere Menge {x, y} ⊆ V zu. Die Knoten x und
y heißen dann Endknoten von e.
Ist jedem Element von E ein geordnetes Paar (x, y) ∈ V ×V von Knoten zugeordnet, so spricht man von einem gerichteten Graphen. Die Kanten eines
gerichteteten Graphen heißen auch Bögen.
Man bezeichnet mit |V | = n(G) = n die Anzahl der Knoten und mit |E| =
m(G) = m die Anzahl der Kanten.
Das Einfügen einer Kante k 6∈ E in den Graphen G = (V, E) bezeichnet man
mit G + {k} = G (V, E ∪ {k}), wobei die Inzidenzfunktion ϕ natürlich auch
der neuen Kante k Knoten x, y ∈ V zuordnen muss.
Schlinge - Mehrfachkante
Eine Schlinge ist eine Kante e mit ϕ (e) = {v1 , v2 } bzw. ϕ (e) = (v1 , v2 ) und
v1 = v2 .
Von Mehrfachkanten oder parallelen Kanten bzw. Bögen spricht man, wenn
ϕ (e1 ) = ϕ (e2 ) für e1 6= e2 ist.
schlichter Graph
Einen Graphen G, der weder Schlingen noch parallele Kanten enthält, bezeichnet man als schlichten Graphen. Für schlichte Graphen identifiziert man
oft eine Kante e ∈ E mit ihren Endknoten: e = {v1 , v2 }.
2
2
BEGRIFFSDEFINITION
vollständiger Graph
Ein Graph G = (V, E) heißt vollständig, wenn er schlicht ist und jedes Paar
unterschiedlicher Knoten durch eine Kante verbunden ist.
Abbildung 1: Die vollständigen Graphen mit bis zu fünf Knoten
inzident - adjazent
Sei G = (V, E) und v1 , v1 ∈ V, e1 , e2 ∈ E.
Die Kante e1 des Graphen G heißt mit dem Knoten v1 inzident, wenn v1 ein
Endknoten von e1 ist. Man sagt auch, dass v1 mit e1 inzident ist. Zwei Kanten
e1 und e2 , die mit einem gemeinsamen Knoten v inzidieren, heißen adjazent.
Zwei verschiedene Knoten v1 und v2 , die durch eine Kante e1 verbunden sind,
heißen adjazent.
Grad (Valenz) eines Knotens
Sei G = (V, E) und v ∈ V .
Der Grad d (v) bzw. dG (v) von v entspricht der Anzahl der mit v inzidenten
Kanten, wobei jede Schlinge doppelt gezählt wird.
Der Knoten v des Graphen heißt gerade oder ungerade, abhängig davon, ob
sein Grad gerade oder ungerade ist.
Kantenfolge - Kantenzug - Weg - Kreis
Sei G = (V, E) und e1 , e2 , ..., ek ∈ E mit ei = {vi−1 , vi } i = 1, ..., k.
Man bezeichnet f = (v0 , e1 , v1 , e2 , v2 , ..., vk−1 , ek , vk ) als eine Kantenfolge von
v0 nach vk mit der Länge l( f ) = k. Dabei ist v0 der Anfangsknoten und vk der
Endknoten von f .
Ein Kantenzug ist eine Kantenfolge, deren Kanten paarweise verschieden
sind.
Ein Weg ist eine Kantenfolge, deren Knoten paarweise verschieden sind.
Ein Kreis ist eine Kantenfolge, deren Knoten v0 , v1 , ..., vk−1 paarweise verschieden sind und deren Anfangsknoten v0 gleich dem Endknoten vk ist.
3
2
Abbildung 2: Weg mit 6 Knoten
BEGRIFFSDEFINITION
Abbildung 3: Kreis mit 6 Knoten
zusammenhängender Graph
Ein Graph G = (V, E) heißt zusammenhängend, wenn zwischen je zwei Knoten v1 , v2 ∈ V ein Weg von v1 nach v2 existiert.
Ist v ∈ V ein Knoten, so bildet die Menge aller mit v durch Wege verbundenen
Knoten (einschließlich der mit diesen Knoten inzidenten Kanten von G) eine
Komponente Ki (G) von G, also einen zusammenhängenden Teilgraphen von
G. Die Anzahl der Komponenten von G bezeichnet man als c(G). Es gilt:
c(G)
[
Ki (G) = G.
i=1
zyklomatische Zahl
Ist G ein Graph, so heißt die Größe µ (G) = m(G) − n(G) + c(G)
zyklomatische Zahl von G.
bipartiter Graph
Ein Graph G heißt bipartit, wenn sich seine Knoten V so in zwei disjunkte Teilmengen aufteilen lassen, dass es zwischen den Knoten innerhalb der
beiden Knotenmengen keine Kante gibt (äquivalent ist: alle Kreise von G
besitzen eine gerade Länge).
Abbildung 4: Ein bipartiter Graph
bewerteter Graph
Ein bewerteter bzw. gewichteter Graph G = (V, E, g) ist ein Graph, bei dem
jeder Kante e eine reelle Zahl g(e) zugeordnet wird. Unter der Länge eines
Teilgraphen bzw. Untergraphen versteht man die Summe der Bewertungen
der zum Teilgraphen gehörenden Kanten.
4
2
BEGRIFFSDEFINITION
EULERscher Kantenzug - EULERscher Graph
Ein Kantenzug in G heißt EULERscher Kantenzug, wenn er jede Kante von
G enthält.
Eine EULERsche Tour von G ist ein geschlossener EULERscher Kantenzug.
Ein EULERscher Graph ist ein zusammenhängender Graph G, in dem eine
EULER-Tour existiert.
HAMILTONscher Graph
Existiert in einem Graphen G ein Kreis C, der alle Knoten des Graphen enthält, so heißt C HAMILTONscher Kreis und G HAMILTONscher Graph.
Baum - Wald - Gerüst
Als Wald bezeichnet man einen kreisfreien Graphen G. Ist zudem der Graph
zusammenhängend, so spricht man von einem Baum.
Ein Gerüst bzw. Spannbaum oder aufspannender Baum ist ein Teilgraph von
G eines ungerichteten Graphen G, welcher ein Baum ist und alle Knoten von
G enthält.
Abbildung 5: Bäume mit 4 Knoten
Matching
Eine Menge M ⊆ E paarweise nichtadjazenter Kanten von G heißt Matching.
Ein Matching M1 heißt gesättigt, wenn es kein weiteres Matching von G gibt,
welches M1 als echte Teilmenge enthält.
Ein Matching M2 heißt maximal, wenn es ein Matching von G mit maximaler
Kantenzahl ist.
Ein Matching M3 heißt perfekt, wenn alle Knoten des Graphen G in M3 enthalten sind.
Abbildung 7: Maximales/perfektes
Matching
Abbildung 6: Gesättigtes Matching
5
3
MINIMALGERÜSTE
Teil II.
Algorithmen
Dieser Teil enthält alle Algorithmen, die im Programm „GraphCalc“ implementiert sind.
Die mathematische Betrachtung der Algorithmen orientiert sich (streng) an den Arbeiten
von Sonntag [Son04], Volkmann [Vol91] und Cormen [Cor03].
3. Minimalgerüste
Wie bei vielen mathematischen Problemen, so gibt es auch bei dem Problem der minimalen Spannbäume in der Praxis viele Anwendungsfälle, für die man effiziente Algorithmen
braucht bzw. diese anwendet. Das nachfolgende Beispiel soll deshalb eine kurze Einleitung zum Problem der Minimalgerüste bieten.
Telekommunikationsanbieter stehen oft vor dem Problem, verschiedene Telefonnetzwerkknoten miteinander zu verbinden. Um dabei alle n Netzwerkknoten miteinander so zu
verknüpfen, dass kein Kommunikationsknoten mehrfach an das gesamte Netz angeschlossen ist, sind genau n − 1 Leitungen notwendig. Aus ökonomischen Gründen wird dabei
jede Firma meist die Leitungen bevorzugen, die die geringsten Kosten verursachen.
Dieses Vernetzungsproblem kann durch einen zusammenhängenden Graphen G = (V, E, ϕ ),
mit der Menge der Netzwerkknoten V und der Menge E aller möglichen Verbindungen zwischen zwei der Netzwerkknoten, dargestellt werden. Jede Kante {u, v} ∈ E hat
dabei ein Gewicht g(u, v), welches die Kosten repräsentiert, um die Netzwerkknoten u
und v miteinander zu verbinden. Im aktuellen Kapitel wird generell davon ausgegangen, dass das Gewicht g(u, v) eine nichtnegative, reelle Zahl ist. Es wird nun ein Baum
T = (V, E(T )) mit einer Kantenmenge E(T ) ⊆ E gesucht, die alle Knoten verknüpft und
deren Gesamtgewicht
g(T ) =
∑ g(u, v)
{u,v}∈E(T )
minimal ist. Der gesuchte Baum T ist offenbar ein Gerüst von G und man spricht davon,
dass T den Graphen G aufspannt. Die Abbildung 8 auf der nächsten Seite stellt ein
solches Gerüst dar. Ist das Gesamtgewicht des Gerüsts T minimal, so bezeichnet man
dieses als Minimalgerüst.
Dieses Kapitel der Bachelorarbeit wird zwei Algorithmen beschreiben, die zur Aufgabe
haben, ein Minimalgerüst eines Graphen G zu bestimmen: Der Algorithmus von Kruskal
und der Algorithmus von Prim. Beide Algorithmen gehören zu der Klasse der GreedyAlogrithmen. Ein Greedy-Algorithmus entscheidet sich anhand einer Bewertungsfunktion
immer für die im aktuellen Schritt günstigste Möglichkeit. Sowohl beim Algorithmus von
6
3
35
H
MINIMALGERÜSTE
B
37
8
30
N
39
80
40
35
L
63
F
12
D
Abbildung 8: Ein zusammenhängender Graph. Rote Kanten bilden ein Minimalgerüst
mit Gewicht 125.
Kruskal als auch von Prim findet dieses Vorgehen Anwendung, indem in jedem Schritt
eine kürzest mögliche Kante zum aktuellen Teilgraphen hinzugenommen wird, wobei
darauf geachtet wird, dass keine Kreise entstehen.
3.1. Algorithmus von Kruskal
Der wohl bekannteste Algorithmus zur Suche eines Minimalgerüsts in einem schlichten, kantenbewerteten Graphen ist der Algorithmus von Joseph Bernard Kruskal [Kru65],
welcher im Jahr 1956 veröffentlicht wurde. Dieser Greedy-Algorithmus wählt in jedem
Schritt eine geeignete Kante minimalen Gewichts, die dem immer größer werdenden Wald
hinzugefügt wird, bis ein Gerüst entstanden ist.
3.1.1. Mathematischer Algorithmus
Sei G = (V, E) ein schlichter, kantenbewerteter Graph mit n(G) ≥ 2.
1. Sei T = (V, 0).
/
2. Wenn E = 0/ −→ Stopp.
Wenn E 6= 0/ −→ wähle e ∈ E minimaler Länge, E := E − {e}.
3. Wenn T + {e} einen Kreis besitzt −→ gehe zu 2.
T := T + {e}.
Wenn m(T ) < n(T ) − 1 −→ gehe zu 2.
4. Stopp.
7
3
MINIMALGERÜSTE
Bricht der Algorithmus in Schritt 2 ab, so ist der Ausgangsgraph G nicht zusammenhängend. Der Beweis ergibt sich aus der Bedingung m(T ) = n(T ) − c(T ) = n(T ) − 1
für Bäume, also auch Gerüste zusammenhängender Graphen. Die Anzahl der Kanten
im resultierenden Graphen T ergibt sich aus der Differenz der Anzahl der Knoten in T
und der Anzahl der Komponenten von T . Durch den Abbruch in Schritt 2 ergibt sich
m(T ) = n(T ) − c(T ) < n(T ) − 1, was nur der Fall sein kann, wenn die Anzahl der Komponenten c(T ) > 1 ist. Der Graph ist also nicht zusammenhängend.
Für zusammenhängende Graphen erhält man beim vollständigen Durchlaufen des Algorithmus ein Minimalgerüst, mit den Kanten E(T ) = {e1 , . . . , en−1 }, mit
g(e1 ) ≤ . . . ≤ g(en−1 ), wenn die Kanten von T in der Reihenfolge ihrer Hinzunahme im
Algorithmus nummeriert werden. Es bleibt zu zeigen, dass T ein Minimalgerüst ist. Dazu
nimmt man an, dass T kein Minimalgerüst ist. Wählt man unter allen Minimalgerüsten
eines aus, welches eine maximale Menge Kanten mit T gemeinsam hat, und bezeichnet
es mit H, so gilt E(H) 6= E(T ). Sei nun i der kleinste Index mit ei ∈ E(T ) und ei ∈
/ E(H).
Aufgrund der Annahme, dass H ein Gerüst von G ist, besitzt H keinen Kreis, das heißt die
zyklomatische Zahl µ (H) ist 0. Daraus folgt µ (H + {ei }) = 1, der Graph H + {ei } besitzt
somit genau einen Kreis. Es muss also eine Kante l auf diesem Kreis existieren, die nicht
zum Baum T gehört. Der Graph H ′ = (H + {ei }) − l, der durch Löschen der Kante l entsteht ist demnach ein zusammenhängender Graph mit n(G) Knoten und n(G) − 1 Kanten.
Das wiederum bedeutet, dass H ′ ein Gerüst von G sein muss, mit dem Gesamtgewicht
g(H ′ ) = g(H) + g(ei ) − g(l). Da H ein Minimalgerüst ist, muss g(H) ≤ g(H ′ ) gelten,
woraus sich g(l) ≤ g(ei ) ergibt. Offenbar besitzt nun G({e1 , . . . , ei−1 , l}) ohne ei keinen
Kreis mehr. Der Algorithmus von Kruskal gibt die Ungleichung g(ei ) ≤ g(l) vor, somit
ist g(ei ) = g(l) und H ′ ist auch ein Minimalgerüst. Allerdings hat H ′ eine Kante mehr mit
T gemeinsam als H. Dies ist ein Widerspruch zur Wahl von H und somit ist bewiesen,
dass T ein Minimalgerüst von G ist. (vgl. [Vol91], S. 48)
8
3
MINIMALGERÜSTE
3.1.2. Implementation in Java
Die Umsetzung des Algorithmus von Kruskal orientiert sich sehr stark an den mathematischen Vorgaben. Es ist mit relativ einfachen Mitteln möglich, die einzelnen algorithmischen Schritte umzusetzen. Eine grobe Struktur soll folgender Pseudo-Code vermitteln:
1. V = 0,
/ E = 0,
/ T = (V, E).
2. Wenn n(T ) < n(G) ∧ m(T ) < n(G) − 1 und
Suche durch findEdge() nach geeigneter Kante erfolgreich −→ gehe zu 3.
Stopp.
3. Wenn Hinzunahme der geeigneten Kante e = {v1 , v2 } keinen Kreis ergibt
−→ T = (V ∪ {v1 , v2 }, E ∪ {e}).
Gehe zu 2.
Die Java-Klasse Kruskal stellt dafür die in Listing 1 aufgeführten Methoden bereit. Der
Konstruktor Kruskal(Graph) erzeugt ein Objekt der Klasse Kruskal, welches einen neuen,
zunächst „leeren Graphen“ im Speicher erzeugt, der später das Minimalgerüst enthalten
soll. Das übergebene Graph-Objekt bildet dabei die Grundlage für alle Schritte im Verlauf
des Algorithmus. Ebenso wird die Methode init() aufgerufen, die die Startbedingungen
für den Algorithmus überprüft. Zum einen wird sichergestellt, dass der Ausgangsgraph
zusammenhängend ist und keine Kante ein negatives Gewicht besitzt, zum anderen wird
überprüft, ob es sich auch um einen schlichten Graphen handelt. Sollte eine der Bedingungen nicht erfüllt sein, so ist ein Starten des Algorithmus nicht möglich.
Gestartet wird der Algorithmus durch den Aufruf der Methode getTree(), die ein GraphObjekt mit dem Minimalgerüst an die aufrufende Instanz zurückgibt. Die Methode besteht
im Wesentlichen nur aus einer Schleife, die beendet wird, wenn entweder das Abbruchkriterium der Methode testEnd() erfüllt ist oder aber die findEdge()-Methode keine weitere
geeignete Kante findet. Liefert die Methode findEdge() eine geeignete Kante, so wird diese aus dem Ausgangsgraphen entfernt und in den Ergebnisgraphen eingefügt, wodurch im
Laufe aller Schritte aus einem Wald ein Gerüst, genauer ein Minimalgerüst, wird.
Die Methode testEnd() führt, wie schon erwähnt, eine Überprüfung auf das Abbruchkriterium durch. Der Ergebnisgraph T ist genau dann ein Gerüst des Ausgangsgraphen
1
2
3
4
5
6
7
8
public cl as s Kruskal
{
p u b l i c K r u s k a l ( Graph ) throws E x c e p t i o n { }
p r i v a t e Edge f i n d E d g e ( ) { }
p u b l i c Graph g e t T r e e ( ) { }
p r i v a t e v o i d i n i t ( ) throws E x c e p t i o n { }
p r i v a t e b o o lea n t e s t E n d ( ) { }
}
Listing 1: Hauptmethoden der Klasse Kruskal
9
3
MINIMALGERÜSTE
G, wenn n(T ) = n(G) und m(T ) = n(G) − 1. Es mag verwirrend erscheinen, wieso nicht
einfach nur die Bedingung m(T ) = n(T ) −1 geprüft wird. Dies lässt sich aber dadurch begründen, dass der Ergebnisgraph zur Laufzeit nur immer die Knoten enthält, die inzident
zu einer hinzugefügten Kante sind. Somit steigt die Anzahl der Knoten im Ergebnisgraphen im Verlauf des Algorithmus an.
Die wichtigste Methode der Klasse Kruskal ist die findEdge()-Methode. In einer Schleife wird im Ausgangsgraphen nach der Kante mit minimalem Gewicht gesucht. Eine
solche Kante erfüllt noch nicht notwendig alle Bedingungen, um sie in den Ergebnisgraphen aufzunehmen. Zuvor muss sichergestellt werden, dass die Kante keinen Kreis
im Ergebnisgraphen bildet. Die Klasse GraphUtils stellt dafür die statische Methode
hasCircle(Graph,Edge) zur Verfügung. Sollte diese Kante nun zu einem Kreis führen,
so wird die Kante aus dem Ausgangsgraphen gelöscht und es wird nach der nächsten
Kante gesucht, die das Minimalitätskriterium erfüllt. Die erste Kante, die keinen Kreis
bildet, wird dann an die aufrufende Methode zurückgeliefert.
Abbildung 9 auf der nächsten Seite veranschaulicht den Ablauf des Algorithmus von
Kruskal. In jedem einzelnen Schritt findet der Algorithmus eine minimale Kante, die nur
zum Ergebnisgraphen hinzugefügt wird, wenn sie auch keinen Kreis erzeugt.
10
3
a
11
7
19
d
23
20
b
13
c
35
f
d
(a) Ausgangsgraph
a
11
7
19
d
23
13
c
a
35
d
f
11
7
19
d
23
20
b
13
e
23
63
35
8
e
c
f
11
20
b
13
23
63
35
8
e
c
f
(d) minimale Kante
(c) minimale Kante
a
13
7
19
63
8
e
20
b
(b) minimale Kante
20
b
11
7
19
63
8
e
a
MINIMALGERÜSTE
c
a
35
8
f
d
13
23
(e) minimale Kante
20
b
7
19
63
11
e
c
63
35
8
f
(f) ergibt Kreis
a
11
7
19
d
23
20
b
13
e
c
63
35
8
f
(g) minimale Kante
Abbildung 9: Ablauf des Kruskal-Algorithmus. Bei jedem Schritt wird eine minimale
Kante gesucht, welche durch einen Pfeil hervorgehoben wird. Ergibt diese keinen Kreis,
so wird sie rot gefärbt, bis das Gerüst gefunden wurde.
11
3
MINIMALGERÜSTE
3.2. Algorithmus von Prim
Der zweite, weit verbreitete Algorithmus zur Bestimmung eines Minimalgerüsts in einem
zusammenhängenden, kantenbewerteten Graphen ist der Algorithmus von Prim. Auch
dieser Algorithmus ist nach seinem Erfinder, dem Informatiker Robert Prim benannt, der
diesen Algorithmus erstmals 1957 publizierte [Pri57]. Allerdings entwarf Prim diesen Algorithmus ohne das Wissen, dass bereits der Tscheche Vojtech Jarnik im Jahre 1930 in
einer Publikation [Jar30] den selben Algorithmus beschrieb. Weiterhin fand der Algorithmus von Prim seine Verwendung im Algorithmus von Dantzig/Dijkstra, der im Kapitel 4
auf Seite 15 beschrieben wird. Diesen Tatsachen ist es zu verdanken, dass der Algorithmus von Prim ebenfalls, wenn auch selten, als DJP-Algorithmus oder Jarnik-Algorithmus
bezeichnet wird.
3.2.1. Mathematischer Algorithmus
Sei G = (V, E) ein schlichter, kantenbewerteter Graph mit v0 ∈ G.
1. T1 := ({v0 }, 0).
/
2. Für k = 1, . . . , n − 1 sei e = {u, v} eine Kante minimaler Länge mit beliebigen
u ∈ V (Tk ) ∧ v ∈
/ V (Tk ).
Tk+1 := (V (Tk ) ∪ {v}, E(Tk ) ∪ {e}).
Stopp, sobald keine Kante e mehr existiert, die die Bedingung erfüllt.
Wird der Algorithmus vollständig durchlaufen, so muss n(T ) = n(G) = n sein. Aus diesem Zusammenhang ergibt sich, dass im letzten Schritt der Graph Tn ein Gerüst von G ist.
Der Nachweis, dass es sich bei Tn um ein Minimalgerüst handelt, erfolgt analog zu dem
Beweis für den Algorithmus von Kruskal auf Seite 8.
Sollte der Algorithmus vorher abbrechen, so ist der Graph G nicht zusammenhängend
und es existiert kein Minimalgerüst.
12
3
MINIMALGERÜSTE
3.2.2. Implementation in Java
Der strukturelle Ablauf des Algorithmus von Prim unterscheidet sich nur unwesentlich
vom Algorithmus von Kruskal. Nachfolgender Pseudo-Code weist deshalb Ähnlichkeiten
zu dem auf Seite 9 auf:
1. V = {v0 }, E = 0,
/ T = (V, E), v0 ∈ G.
2. Wenn n(T ) < n(G) und
findEdge() eine minimale Kante e = {u, v} mit u ∈ V ∧ v ∈
/ V findet −→ gehe zu 3.
Stopp.
3. T = (V ∪ {v}, E ∪ {e}).
Gehe zu 2.
Die Implementation des Algorithmus von Prim erfolgt in der Java-Klasse Prim. Alle dafür
notwendigen Methoden sind im Listing 2 aufgeführt. Zu Beginn ist wie üblich der Konstruktor Prim(Graph, int) für die Klasse Prim dargestellt. Wird ein Objekt dieser Klasse
erzeugt, so wird ein Graph-Objekt und ein Integer-Wert übergeben. Das Graph-Objekt
enthält dabei den Ausgangsgraphen, dessen Minimalgerüst gefunden werden soll. Der
Integer-Wert macht den entscheidenden Unterschied zur Klasse Kruskal aus. Hier wird
ein Startknoten übergeben, welcher der Ausgangspunkt für die Berechnung des Minimalgerüsts ist. Dieser Knoten wird im ersten Schritt des Algorithmus in einen zuvor leer
initialisierten Ergebnisgraphen eingefügt. Weiterhin wird im Konstruktor die Methode
init() aufgerufen, die als erstes verschiedene Startbedingungen überprüft. Primär findet in
der Methode eine Überprüfung statt, ob der Ausgangsgraph zusammenhängend ist. Aus
algorithmischer Sicht ist dies zwar nicht notwendig, da ein nicht zusammenhängender
Graph kein Gerüst liefern würde, aber auf Grund der Vereinheitlichung aller implementierten Algorithmen wird jeder Graph vor dem Starten des Algorithmus auf diese Eigenschaft untersucht. Weiterhin wird sichergestellt, dass der Ausgangsgraph keine negativen
Wichtungen besitzt. Ebenso wie die Klasse Kruskal besitzt diese Klasse eine öffentliche
Methode getTree(), welche als Rückgabewert das Minimalgerüst enthält. Auch hier wird
in einer Schleife permanent geprüft, ob einerseits die Endbedingung n(T ) = n(G) der Methode testEnd() erfüllt ist und ob andererseits eine neue minimale Kante in der Methode
1
2
3
4
5
6
7
8
9
p u b l i c c l a s s Prim
{
p u b l i c Prim ( Graph , i n t ) throws E x c e p t i o n { }
p r i v a t e Edge f i n d E d g e ( ) { }
p u b l i c Graph g e t T r e e ( ) { }
p r i v a t e v o i d i n i t ( ) throws E x c e p t i o n { }
p r i v a t e V e c t o r removeEdges ( V e c t o r ) { }
p r i v a t e b o o lea n t e s t E n d ( ) { }
}
Listing 2: Hauptmethoden der Klasse Prim
13
3
MINIMALGERÜSTE
findEdge() gefunden werden kann. Die findEdge()-Methode sucht dabei alle Knoten im
Ergebnisgraphen und die dazu inzidenten Kanten. Dort werden dann mit Hilfe der Methode removeEdges(Vector) alle Kanten gelöscht, deren Start- und Endknoten schon im
Baum des Ergebnisgraphen liegen und somit einen Kreis bilden würden. Der Rückgabewert der Methode getTree() ist demnach eine geeignete Kante, die den Baum erweitert.
Abschließend soll die Abbildung 10 noch einmal den Algorithmus von Prim an einem
einfachen Beispiel verdeutlichen.
11
a
7
19
13
23
d
20
b
c
8
e
11
7
19
13
23
d
20
b
c
8
e
11
f
7
19
d
23
20
b
13
e
f
20
b
13
23
d
c
8
e
63
35
f
(d) minimale, inzidente Kante
c
a
f
d
(e) minimale, inzidente Kante
11
23
20
b
7
19
63
35
8
63
35
8
e
7
19
(c) minimale, inzidente Kante
a
11
a
63
35
c
(b) minimale, inzidente Kante
(a) Ausgangsgraph mit Startknoten e
a
13
23
d
f
20
b
7
19
63
35
11
a
13
e
c
63
35
8
f
(f) Minimalgerüst
Abbildung 10: Ablauf des Prim-Algorithmus. Bei jedem Schritt wird eine minimale
Kante gesucht, die inzident zu genau einem der Knoten im Baum ist. Diese wird durch
einen Pfeil gekennzeichnet. Der Baum ist rot gefärbt und markiert zuletzt das
Minimalgerüst.
14
4
KÜRZESTE WEGE
4. Kürzeste Wege
Kürzeste-Wege-Probleme spielen heutzutage nicht nur in der Graphentheorie eine sehr
große Rolle. Eine mögliche Anwendung ist die Planung und Analyse von Verkehrs- und
Kommunikationsnetzen. Bei der Untersuchung von Graphen in diesem Zusammenhang
interessiert man sich für möglichst günstige Wege von einem Startknoten zu einem Zielknoten. In den meisten Fällen reicht es aus, die Wege bezüglich eines bestimmten Merkmals (Zeitaufwand, Weglänge, entstandene Kosten, ...) zu bewerten.
Das Problem des kürzesten Weges
Gegeben sei ein schlichter, bewerteter Graph G = (V, E, g) mit der Abbildung g : E → R.
Gesucht sind kürzeste Wege von einem festen Knoten u zu allen anderen Knoten des Graphen G. Ein Weg von u nach v mit u, v ∈ V (G) ist ein kürzester Weg, wenn das Gewicht
des Weges minimal ist.
A
10
1
D
4
31
B
C
17
E
7
11
F
Abbildung 11: Der kürzeste Weg von A nach C
Kürzeste-Wege-Probleme lassen sich unter anderem in folgende Klassen unterteilen [Bra94]:
• Gesucht ist ein kürzester Weg zwischen zwei vorgegebenen Knoten u und v mit
u, v ∈ V (G).
• Gesucht sind alle kürzesten Wege zwischen zwei vorgegebenen Knoten u und v mit
u, v ∈ V (G).
• Gesucht ist je ein kürzester Weg von einem Startknoten u zu allen anderen Knoten
v im Graphen G mit u, v ∈ V (G).
• Gesucht ist je ein kürzester Weg zwischen allen Paaren (u, v) von Knoten u und v
des Graphen G.
Es lassen sich sicherlich weitere ähnliche Problemstellungen für kürzeste Wege finden,
die in bestimmten Situationen von Interesse sein können. Gleichwohl wird deutlich, dass
trotz der unterschiedlichen Zielstellungen alle diese Probleme eine gewisse Ähnlichkeit
aufweisen.
15
4
KÜRZESTE WEGE
Der im Nachfolgenden betrachtete Algorithmus von George Bernard Dantzig und Edsger Wybe Dijkstra aus dem Jahre 1959 dient zur Berechnung je eines kürzesten Weges
von einem vorgegebenen Startknoten u zu allen anderen Knoten.
4.1. Algorithmus von Dantzig/Dijkstra
Der Algorithmus gehört wieder zur Klasse der Greedy-Alogrithmen. Dabei wird sukzessive der nächstbeste Knoten, der Knoten mit dem geringsten Gewicht, in eine Knotenergebnismenge aufgenommen und aus der noch zu bearbeitenden Knotenmenge entfernt.
Sei G = (V, E, g) schlicht, bewertet mit ∀e ∈ E : g(e) > 0 und y0 ∈ V .
1. Man setze
0 , y = y0
∞ , y ∈ V − {y0 },
A0 := {y0 },
m := 0.
t(y) :=
2. Man wähle ein {x∗ , y∗ } ∈ E mit x∗ ∈ Am ∧ y∗ ∈ Ām und t(x∗ ) + g(x∗ , y∗ ) minimal.
3. Man setze t(y∗) := t(x∗) + g(x∗ , y∗ ) und Am+1 := Am ∪ {y∗ }, m := m + 1.
4. Stopp, wenn keine Kante {x, y} ∈ E mit x ∈ Am ∧ y ∈ Ām mehr existiert, sonst gehe
zu 2.
Nach der Initialisierung der Startbedingungen erfolgt eine Iteration in n − 1 Schritten.
Im ersten Schritt wird, ausgehend vom Startknoten y0 , eine Kante mit dem geringsten
Gewicht zu einem nächsten Nachbarknoten bestimmt. In den Folgeschritten wählt man
immer den am kostengünstigsten zu erreichenden, bislang noch nicht besuchten Knoten,
ausgehend von allen bereits besuchten Knoten. Nach Beendigung des Algorithmus erhält
man die Wege, ausgehend vom Startknoten y0 zu allen anderen Knoten, mit den geringsten Kosten.
Um die Korrektheit des Algorithmus zu beweisen, muss gezeigt werden, dass die Länge
t(ym ) des im Algorithmus konstruierten Weges F vom Ausgangsknoten y0 zu einem beliebigen Knoten ym aus der Menge Am gleich der Länge eines kürzesten Weges von y0 nach
ym ist. Es darf also keinen anderen Weg von y0 nach ym geben, dessen Länge kleiner t(ym )
ist. Im ersten Schritt des Algorithmus wird der Knoten y0 zu der Menge A0 = 0/ hinzugefügt und die Länge des kürzesten Weges t(y0) mit 0 initialisiert. Sei für alle Knoten, die
bis zum Schritt k − 1 zur Menge Ak−1 hinzugefügt wurden, die Behauptung richtig. Folglich muss nur noch die Hinzunahme eines weiteren Knotens y∗ zur Knotenmenge Ak−1
diskutiert werden. Im k-ten Schritt des Algorithmus soll der Knoten y∗ der Knotenmenge
Ak−1 hinzugefügt werden, also Ak = Ak−1 ∪ {y∗ }. Dabei kann es, auf Grund der Wahl des
neuen Knotens y∗ (im zweiten Schritt des Algorithmus), keinen kürzeren Weg von y0 zu
y∗ geben.
16
4
KÜRZESTE WEGE
Ein wesentlicher Nachteil des Algorithmus ist die Voraussetzung, dass die Gewichtsfunktion ausschließlich positive Werte annehmen muss. Die Korrektheit der Lösung des Algorithmus ist bei negativen Kosten nicht mehr gewährleistet. (vgl. [Sac70])
Abbildung 12 repräsentiert einen Graphen mit negativen Kantengewichten. Der Algorithmus würde einen Weg F vom Ausgangsknoten b1 über den Knoten b2 zum Knoten b3
konstruieren. Tatsächlich aber wäre ein Weg über den Knoten b4 zu b3 kürzer.
b3
-6
3
b4
b2
8
9
b1
Abbildung 12: Keine Korrektheit des Algorithmus von Dantzig/Dijkstra bei negativen
Kantengewichten
Die Komplexität des Algorithmus beträgt O(n2 ). Im i-ten Schritt sind maximal n − i − 1
Summen und zweimal n − i − 1 Vergleiche zur Bestimmung des Minimums nötig. Also
müssen 3 ∑n−1
i=0 n − i − 1 = 3 (n(n − 1)/2) Schritte durchgeführt werden. Verwendet man
hingegen den Algorithmus, um je einen kürzesten Weg zwischen allen Paaren (u, v) von
Knoten u und v des Graphen G zu bestimmen, so muss der Algorithmus n − 1 mal wiederholt werden. Damit steigt die Komplexität auf O(n3 ). Eine effizientere Möglichkeit
zur Abspeicherung der Listen ist z.B. die Datenstruktur Fibonacci-Heap. Dabei sinkt die
Komplexität auf O(m + n · log(n)) (vgl. [Cor03]).
Eine Veranschaulichung des Algorithmus soll anhand des Beispielgraphen (Abbildung
11) erfolgen. Zur besseren Darstellung werden die Knotenbezeichnungen in den einzelnen
Schritten durch das Gewicht der Knoten bzw. das ”potentielle Gewicht” der im jeweiligen
Schritt zu untersuchenden Knoten y∗ ∈ Ām ersetzt.
17
4
Knoten y
A
B
C
D
E
F
m=0
t(y)
A0
0
x
∞
∞
∞
∞
∞
m=1
t(y)
A1
0
x
10
x
∞
∞
∞
∞
m=2
t(y)
A2
0
x
10
x
∞
11
x
∞
∞
m=3
t(y)
A3
0
x
10
x
∞
11
x
15
x
∞
m=4
t(y)
A4
0
x
10
x
∞
11
x
15 26
x
x
m=5
t(y)
A5
0
x
10 33 11
x x x
15 26
x
x
KÜRZESTE WEGE
Nach Beendigung des Algorithmus geben die Werte t(y) die Längen der kürzesten Wege vom Ausgangsknoten zu jedem Knoten an. Demzufolge sind die Längen der Wege
von A nach B,C, D, E, F gleich 10, 33, 11, 15 und 26. In nachfolgender Tabelle wird der
Algorithmus nochmals zusammengefasst.
18
4
A
0
schrittweise
t(y)
Werte
B
∞
10
Knoten
C D E F
∞ ∞ ∞ ∞
∞ ∞ ∞ ∞
∞ 11 ∞ ∞
∞
15 ∞
∞
26
33
KÜRZESTE WEGE
Ām
{A, B,C, D, E, F}
{B,C, D, E, F}
{C, D, E, F}
{C, E, F}
{C, F}
{C}
Für den Algorithmus von Dijkstra gibt es einen Weg vom Startknoten A zum Zielknoten F
der Länge t(F) genau dann, wenn der Zielknoten F in irgendeiner Stufe des Algorithmus
einen endlichen Wert t(F) besitzt.
4.2. Implementation in Java
In der Vorbetrachtung zur Implementierung des Algorithmus wurden ausschließlich ungerichtete Graphen berücksichtigt. Das Programm GraphCalc hingegen ermöglicht die
Durchführung des Algorithmus für gerichtete und ungerichtete Graphen.
Bei der Implementierung des Dantzig/Dijkstra-Algorithmus wurde die folgende JavaKlasse mit den wichtigsten dargestellten Methoden realisiert.
1
2
3
4
5
6
7
8
public cl as s Dantzig
{
p u b l i c D a n t z i g ( Graph , i n t )
p u b l i c b o o lea n s t a r t ( ) throws E x c e p t i o n
p r i v a t e b o o lea n i n i t ( ) throws E x c e p t i o n
p riv a t e Vector getNeighbours ( int , Hashtable , Hashtable )
p r i v a t e V e c t o r g etN ex tN o d e ( V e c t o r ) throws E x c e p t i o n
}
throws E x c e p t i o n
Listing 3: Hauptmethoden der Klasse Dantzig
Beim Erzeugen eines neuen Objektes vom Typ Dantzig muss der Konstruktor
Dantzig(Graph, int) aufgerufen werden. Dabei wird ein Graph-Objekt (der Graph, auf den
der Algorithmus angewandt werden soll) und ein Integer-Objekt (die ID des Startknotens)
übergeben. Mit dem Erzeugen eines neuen Objektes werden alle nötigen Initialisierungen
für den Algorithmus vorgenommen. Mit dem Aufruf der Methode start() wird, bevor der
eigentliche Algorithmus startet, eine Überprüfung der Vorbedingungen durchgeführt:
• der Graph besitzt ausschließlich positive Wichtungen und
• der Startknoten ist im Graphen enthalten.
Sind diese Bedingungen erfüllt, wird der eigentliche Algorithmus ausgeführt. Zur effektiveren Bearbeitung werden bereits besuchte Knoten in einer Hashtabelle, mit der Reihenfolge der Hinzunahme der Knoten zu den bereits besuchten Knoten, gespeichert. Damit
19
4
KÜRZESTE WEGE
erübrigte sich das Problem der Initialisierung der Wegstrecken zu den einzelnen Knoten, ausgenommen dem Startknoten, mit ∞. Nach Aufnahme des Startknotens in diese
Hashtabelle für besuchte Knoten und dem Entfernen des Startknotens aus der Hashtabelle für noch nicht besuchte Knoten wird in einer while-Schleife sukzessive der jeweils
nächste, von allen bereits besuchten Knoten aus am besten erreichbare Knoten gesucht.
Nachdem ein solcher Knoten gefunden wurde, wird dieser ebenfalls entfernt bzw. in die
jeweils dafür vorgesehene Hashtabelle eingefügt. Sollte es keinen weiteren möglichen
Knoten geben, wird die while-Schleife verlassen und der Algorithmus ist beendet. Es war
zu beachten, dass die Liste mit den noch nicht besuchten Knoten nicht zwingend leer sein
muss, da der Algorithmus auch für nicht zusammenhängende Graphen korrekt arbeitet.
Besonders erwähnen möchte ich die Methode getNeighbours(int, Hashtable, Hashtable).
Nachdem die Methode mit einer Knoten-ID, der Hashtabelle mit den noch nicht besuchten
Knoten und einer Hashtabelle mit den bereits besuchten Knoten aufgerufen wurde, werden nach dem Algorithmus alle möglichen Nachbarn zu den bereits besuchten Knoten
ermittelt und diese in einem doppelt geschachtelten Vektor zurückgegeben. Dabei besitzt
der Vektor die folgende Struktur:
[x00 , x01 , x02 , x03 , x04 ] , [x10 , x11 , x12 , x13 , x14 ] , . . . , [xn0 , xn1 , xn2 , xn3 , xn4 ] .
Jeder innere Vektor entspricht einem Nachbarn und damit einem möglichen nächsten
Knoten. Die n + 1 inneren Vektoren bestehen aus fünf Einträgen: dem Ausgangsknoten,
dem Zielknoten, der Kanten-ID zum Erreichen des Zielknotens, den Kosten der Kante
zum Erreichen des Zielknotens und den Gesamtkosten zum Erreichen des nächstbesten
Knotens, ausgehend vom Startknoten. Diese Art der Speicherung ermöglicht es relativ
effizient, trotz größerem Speicherbedarf und Zeitaufwand, den nächstbesten Knoten zu
ermitteln und Informationen über kürzeste Wege abzufragen.
Problematisch bei der Realisierung des Algorithmus war, dass der Graph zum einen gerichtet und zum anderen ungerichtet sein kann. Dies erforderte für manche Methoden eine
”doppelte” Implementierung. Eine andere Möglichkeit zur Problemlösung wäre eine Modifizierung des Ausgangsgraphen gewesen. Der Graph hätte dafür von einem gerichteten
in einen ungerichteten Graphen transformiert werden müssen.
Zur besseren Darstellung des Ergebnisgraphen im Outputfenster wurden Veränderungen
bezüglich der Knotennamen vorgenommmen. Diese bestehen nach Durchlauf des Algorithmus aus dem Namen des Knotens und der Nummer des Schrittes, in welchem der
Knoten dem Ergebnisgraphen hinzugefügt wurde.
20
5
DAS CHINESISCHE BRIEFTRÄGERPROBLEM
5. Das Chinesische Briefträgerproblem
Die Stadt Königsberg (heute Kaliningrad) wurde im 18. Jahrhundert durch den Fluss
Pregel in mehrere Stadtteile geteilt, die untereinander durch sieben Brücken verbunden
waren. Man stellte sich die Frage, ob es möglich ist, von Insel A (Abbildung 13) einen
Rundgang durch die Stadt zu unternehmen, dabei jede Brücke genau einmal zu überqueren und am Ende zur Insel A zurückzukehren.
B
D
A
C
Abbildung 13: Das Königsberger Brückenproblem
Im Jahre 1736 wurde der Mathematiker Leonhard Euler mit der Lösung des Problems beauftragt. Euler abstrahierte die Karte graphentheoretisch, indem er jedem Landteil einen
Knoten und jeder Brücke eine Kante zuwies. In seiner Arbeit ”Solutio Problematis ad
Geometriam Situs Pertinentis” erklärte er kurz darauf, dass das vorliegende Problem nicht
lösbar ist. Er formulierte eine allgemeine notwendige und hinreichende Bedingung: ”Ein
Bild ist in einem Zug zeichenbar, wenn es entweder zwei oder keinen Knotenpunkt ungeraden Grades besitzt.” [Eul36].
Wird eine EULER-Tour in einem zusammenhängenden Graphen G durchlaufen, so muss
jeder Knoten über eine Kante betreten und über eine andere verlassen werden. Damit liefert jeder Knotendurchgang den Beitrag 2 zum Grad des Knotens. Euler zeigte, dass es
für das Königsberger Brückenproblem keinen Rundgang geben kann, da der Graph (Abbildung 13) Knoten ungeraden Grades besitzt und somit nicht EULERsch ist. Den Beweis
und einen Algorithmus zur Lösung des Problemes, welcher ausnutzte, dass EULERsche
Graphen aus kantendisjunkten Kreisen zusammengesetzt werden können (Abbildung 14),
veröffentlichte Carl Fridolin Bernhard Hierholzer im Jahre 1873.
Ein zusammenhängender Graph besitzt einen (offenen) EULERschen Kantenzug, wenn
er genau zwei Knoten ungeraden Grades besitzt. Angenommen, der Graph G besitzt einen
EULERschen Kantenzug: Ist v ein vom Anfangs- und Endknoten des Kantenzuges verschiedener Knoten, so ist der Grad von v gerade. Somit ergeben sich für die einzig möglichen Knoten ungeraden Grades die Anfangs- und Endknoten des Kantenzuges. Sind der
Anfangs- und der Endknoten ein und derselbe, so ist jeder Knotengrad gerade und man
erhält eine EULERsche Tour. Nehmen wir an, dass der Graph G zusammenhängend ist
21
5
DAS CHINESISCHE BRIEFTRÄGERPROBLEM
und höchstens zwei Knoten ungeraden Grades besitzt. Enthält G keinen Knoten mit ungeradem Grad, so ist G EULERsch und besitzt einen EULERschen Kantenzug. Folglich
bleibt nur noch der Fall zu betrachten, wenn der Graph G zwei ungerade Knoten u und
v besitzt. Sei G + e ein Graph, der aus G durch Hinzufügen einer neuen Kante e, die u
und v verbindet, besteht. Der Grad der Knoten u und v wird um den Grad 1 erhöht. Daraus folgt, dass der Graph G + e ausschließlich gerade Knoten und eine EULERsche-Tour
C = (v0 , e1 , v1 , e2 , ..., en, vn ) besitzt. Ist v0 = vn = u und e1 = e, so führt das Löschen von e
aus der Tour zu einem EULERschen Kantenzug (v1 , e2 , ..., en, vn ) von v nach u in G. (vgl.
[Cla94])
Abbildung 14: Ein EULERscher Graph, dargestellt mit kantendisjunkten Kreisen
Aufbauend auf Hierholzers Arbeit, beschäftigte sich 1962 der chinesische Mathematiker
Mei Ko Kwan mit dem folgendem Problem:
In seinem Zuständigkeitsbereich muss ein Briefträger die Post an alle Haushalte verteilen.
Beginnend beim Postamt, durchläuft er alle Straßen seines Gebietes und kehrt anschließend zum Ausgangspunkt zurück. Gesucht ist ein Rundgang minimaler Länge, bei der alle
Straßen mindestens einmal durchlaufen werden.
Das Chinesische Briefträgerproblem
Gegeben sei ein zusammenhängender und bewerteter Graph G = (V, E, g) mit g (e) ≥ 0 für
alle e ∈ E. Gesucht ist eine geschlossene Kantenfolge Z minimaler Länge mit E (Z) = E.
Eine solche Kantenfolge Z heißt dann optimal.
22
5
DAS CHINESISCHE BRIEFTRÄGERPROBLEM
5.1. Algorithmus von Hierholzer
Die Lösung des Problems lässt sich in zwei Fälle aufteilen. Im ersten Fall, dem eigentlichen Algorithmus von Hierholzer, besitzt der Graph eine EULER-Tour. Damit ist die
minimale Gesamtlänge gleich der Länge einer EULER-Tour im Graphen. Im zweiten
Fall, einer Erweiterung des Algorithmus von Hierolzer, ist der Graph nicht EULERsch
und gewisse Kanten müssen mehrfach durchlaufen werden.
1. Fall: Der Graph G ist EULERsch.
Man wähle einen beliebigen Knoten v1 ∈ V aus dem Graphen G und konstruiere von diesem ausgehend einen nicht fortsetzbaren Kantenzug Z1 von G. Da alle Knotengrade in G
gerade sind, muss der Endknoten von Z1 gleich v1 sein. Ist Z1 eine EULER-Tour, so ist
der Algorithmus beendet.
Wenn Z1 keine EULER-Tour ist, so lösche man alle schon besuchten Kanten im Graphen:
G1 = G − E (Z1 ). Man wähle einen Knoten v2 mit v2 ∈ V (Z1 ) inzident mit einer Kante
von G1 . Da der Graph G zusammenhängend ist, existiert v2 . Ausgehend von v2 , konstruiere man in G1 einen nicht fortsetzbaren geschlossenen Kantenzug Z2 (Abbildung 15).
Anschließend setze man die beiden Kantenzüge Z1 und Z2 zu einem Kantenzug von G
zusammen. Hierbei beginnt man in v1 und läuft entlang von Z1 bis zum Knoten v2 . Von
v2 durchläuft man Z2 und dann die verbliebenen Kanten von Z1 . Befinden sich noch nicht
alle Kanten von G in diesem Kantenzug, so setze man den Algorithmus fort.
v1
Z1
v2
Z2
Abbildung 15: Hierholzer, Konstruieren eines geschlossenen Kantenzuges
Die Komplexität des Algorithmus von Hierholzer beträgt O(n2 ). Bei geeigneter Datenstruktur ist eine lineare Laufzeit von O(m + n) möglich. (vgl. [Bra94])
2. Fall: Der Graph G ist nicht EULERsch.
Ist der Graph G nicht EULERsch, so müssen Kanten des Graphen mehrfach durchlaufen
werden. Sind v1 , .., v2p die Knoten ungeraden Grades in G und dG vi , v j für (1 ≤ i < j ≤ 2p)
p
Sp
ihre Abstände, so sei L = min ∑i=1 dG (ui , u′i ) | i=1 {ui , u′i } = v1 , ..., v2p . Fügt man
die dem Minimum L entsprechenden p Wege zu G hinzu, so entsteht ein bewerteter
23
5
DAS CHINESISCHE BRIEFTRÄGERPROBLEM
EULERscher Graph, in dem jede EULER-Tour eine optimale Kantenfolge in G liefert.
Eine solche EULER-Tour kann mit dem ersten Fall (Algorithmus von Hierholzer) bestimmt werden.
5.2. Implementation in Java
Die Klassenstruktur der Klasse Hierholzer mit den wichtigsten Methoden ist im Listing 4
dargestellt.
1
2
3
4
5
6
7
8
9
10
11
12
public cl as s Hierholzer
{
p u b l i c H i e r h o l z e r ( Graph , i n t ) throws E x c e p t i o n
p u b l i c b o o lea n i n i t ( ) throws E x c e p t i o n
p u b l i c b o o lea n s t a r t ( ) throws E x c e p t i o n
p r i v a t e v o i d g e t M a t c h i n g ( V ecto r , V e c t o r ) throws E x c e p t i o n
p r i v a t e V e c t o r g e t S h o r t P a t h A n d W e i g h t ( V e c t o r ) throws E x c e p t i o n
p r i v a t e b o o lea n c r e a t e E u l e r G r a p h ( ) throws E x c e p t i o n
p r i v a t e b o o lea n f i n d C i r c l e ( i n t ) throws E x c e p t i o n
p r i v a t e i n t f i n d N e w C i r c l e S t a r t I D ( ) throws E x c e p t i o n
p r i v a t e b o o lea n c r e a t e O n e C i r c l e ( V e c t o r ) throws E x c e p t i o n
}
Listing 4: Hauptmethoden der Klasse Hierholzer
Mittels des Konstruktors Hierholzer(Graph, int) wird ein neues Objekt erzeugt. Dabei
müssen der Graph, auf dem der Algorithmus von Hierholzer durchgeführt werden soll,
und die ID des Startknotens übergeben werden. Mit dem Erzeugen eines neuen Objektes werden alle nötigen Variablen, welche zur Lösung des Algorithmus nötig sind, initialisiert. Gestartet und durchgeführt wird der Algorithmus mit dem Aufruf der publicMethode start() und der Überprüfung der Voraussetzungen:
• der Graph besitzt ausschließlich positive Wichtungen,
• der Startknoten ist im Graphen enthalten,
• der Graph ist ungerichtet und
• der Graph ist zusammenhängend.
Nachdem eine erfolgreiche Überprüfung der Bedingungen für das Ausführen des Algorithmus durchgeführt wurde, schließt sich ein Test auf Überprüfung nach Knoten ungeraden Grades an. Besitzt der Graph Knoten ungeraden Grades, so wird in der Methode
createEulerGraph() ein EULERscher Graph erzeugt. Dafür werden alle Knoten ungeraden Grades ermittelt und in einem Vektor gespeichert. Zum Finden der kürzesten Wege
zwischen Paaren von Knoten wurde der Algorithmus von Dantzig/Dijkstra zur Hilfe genommen. Nachdem die Kanten mit dem geringsten Gewicht dem Graphen hinzugefügt
wurden, besitzt dieser ausschließlich Knoten geraden Grades und ist somit ein EULERscher Graph. Ausgehend vom Startknoten, sucht der Algorithmus iterativ kantendisjunkte
Kreise im Graphen (Abbildung 15). Aus einer Liste mit noch nicht besuchten Knoten
24
5
DAS CHINESISCHE BRIEFTRÄGERPROBLEM
wählt der Algorithmus einen nächsten, zum aktuell betrachteten Knoten adjazenten Knoten, speichert diesen in einem circleVector und entfernt ihn aus der Liste. Damit wird
sichergestellt, dass der Algorithmus alle Knoten im Graphen durchläuft. Existiert zu einem Knoten kein weiterer adjazenter Knoten und ist die Liste mit den noch nicht besuchten Knoten nicht leer, wurde ein Kreis gefunden und der Algorithmus bestimmt nach Wahl eines beliebigen, zum gefundenen Kreis adjazenten, jedoch noch nicht besuchten Knoten - einen weiteren Kreis. Diese beiden Kreise werden in der Methode
createOneCircle(Vector) zu einer geschlossenen Kantenfolge zusammengefügt. Anschließend wird ein weiterer geeigneter Kreis bestimmt und erneut die Methode
createOneCircle(Vector) aufgerufen. Dies wird solange wiederholt, bis keine weiteren
unbesuchten Knoten mehr existieren.
1
p r i v a t e b o o lea n c r e a t e O n e C i r c l e ( V e c t o r c i r c l e ) throws E x c e p t i o n {
2
/ / E r m i t t e l n d e r e r s t e n Knoten −ID d e s z w e i t e n K r e i s e s
i n t f ir s tN ew N o d e I D = I n t e g e r . p a r s e I n t ( c i r c l e . g e t ( 0 ) . t o S t r i n g ( ) ) ;
/ / V e k t o r i n d e x f u e r d a s Zusammenfuegen d e r b e i d e n K r e i s e
i n t i n p u t C i r c l e A t = −1;
/ / H i l f s v e k t o r zum t e m p o r a e r e n Z w i s c h e n s p e i c h e r n d e s n eu en K r e i s e s
V e c t o r tm p V ecto r = new V e c t o r ( ) ;
3
4
5
6
7
8
9
f o r ( i n t i = 0 ; i < m _ c i r c l e N o d e I D s . s i z e ( ) ; i ++) {
/∗
∗ H i l f s v e k t o r m i t den Knoten −IDs d e s e r s t e n K r e i s e s ,
∗ b i s zum e r s t e n d e s z w e i t e n K r e i s e s f u e l l e n
∗/
}
10
11
12
13
14
15
16
f o r ( i n t i = 0 ; i < c i r c l e . s i z e ( ) − 1 ; i ++) {
/∗
∗ E i n f u e g e n d e s z w e i t e n K r e i s e s i n den H i l f s v e k t o r
∗/
}
17
18
19
20
21
22
f o r ( i n t i = i n p u t C i r c l e A t ; i < m _ c i r c l e N o d e I D s . s i z e ( ) ; i ++ ) {
/∗
∗ E i n f u e g e n d e s z w e i t e n T e i l s d e s z w e i t e n K r e i s e s i n den H i l f s v e k t o r
∗/
}
23
24
25
26
27
28
/ / M e m b e r v a r i a b l e a l s V e k t o r m i t dem n eu en K r e i s neu e r z e u g e n
m _ c i r c l e N o d e I D s = new V e c t o r ( tm p V ecto r ) ;
29
30
31
return true ;
32
33
}
Listing 5: Auszug aus der Methode createOneCircle(Vector) aus der Klasse Hierholzer
Das Zusammenfügen der beiden Kreise aus dem übergebenen Vektor circle und der Membervariable m_circleNodeIDs erfolgt, wie im Listing 5 dargestellt, in drei Schritten. Dabei
wird der erste Kreis bis zur ID des Knotens des zweiten Kreises durchlaufen. An dieser
Stelle wird der zweite Kreis eingefügt. Das Ergebnis des Algorithmus ist eine optimale,
geschlossene Kantenfolge im Graphen G.
25
6
DAS TRAVELING SALESMAN PROBLEM
6. Das Traveling Salesman Problem
Das Traveling Salesman Problem (kurz: TSP) besteht darin, eine Rundreise möglichst
effizient durch n gegebene Städte zu unternehmen und anschließend zum Ausgangsort zurückzukehren. Die Entfernungen zwischen den n Städten sind bekannt.
Im Gegensatz zum EULERschen Kantenzug besteht das Problem beim TSP darin, einen
Kreis zu finden, der alle Knoten V eines Graphen G = (V, E) genau einmal durchläuft. Ein
solcher Kreis wird, nach dem vom englischen Mathematiker Sir William Rowan Hamilton
erfundenen Spiel, bei dem es um das Auffinden einer Rundreise auf einem Isokaeder geht,
als HAMILTON-Kreis bezeichnet. Ein HAMILTON-Kreis repräsentiert gleichzeitig eine
kürzeste Rundreise durch den Graphen G. Der in Abbildung 16 dargestellte Graph hat
12367159999937622562 verschiedene HAMILTON-Kreise.
Abbildung 16: Ein Graph mit einem Hamiltonkreis [Tit03]
Geht man beim TSP von einem vollständigen Graphen mit n Knoten aus, so existieren
(n − 1)! verschiedene Rundreisen. Damit wächst die Laufzeit bei vollständiger Enumeration exponentiell an das TSP und gehört somit zur Klasse der NP-schweren Probleme.
Ein Spezialfall des TSP ist das metrische TSP. Dabei genügt die Gewichtsfunktion g der
Dreiecksungleichung, d.h. es gilt für je drei Knoten x, y, z ∈ V : g(x, y) ≤ g(x, z) + g(z, y).
Traveling Salesman Problem
Gegeben sei ein vollständiger Graph G = (G,V, g) mit V = {v1 , ..., vn}. Gesucht ist ei
ne Permutation π : {1, ..., n} → {1, ..., n} so, dass ∑n−1
g(v
,
v
)
π (i) π (i+1) + g(vπ (n) , vπ (1) )
i=1
minimal ist. Auf Grund der Komplexität des Problems wird im nachfolgenden Algorithmus nur ein Spezialfall des TSP, das metrische TSP, betrachtet.
26
6
DAS TRAVELING SALESMAN PROBLEM
6.1. Christofides-Heuristik
Gegeben sei ein vollständiger, kantenbewerteter Graph G = (V, E, g), für den die Dreiecksungleichung gilt. Die Idee der Christofides-Heuristik ist es, ein minimales, gewichtetes Matching zwischen allen Knoten ungeraden Grades eines Minimalgerüsts T zu finden
und die Kanten des Matchings zu T hinzuzufügen. Dabei müssen gegebenenfalls Kanten
von G verdoppelt werden. So erhält man einen Graphen, der eine EULER-Tour besitzt,
aus der man dann einen HAMILTON-Kreis konstruiert.
1. Man bestimme ein Minimalgerüst T = (V, ET ) von G.
2. Es sei U der von den Knoten ungeraden Grades in T aufgespannte Untergraph von
G.
3. Bestimme in dem Graphen U ein perfektes Matching M mit minimalem Gewicht.
4. Man bestimme in T ∪M := (V, ET ∪ M) eine EULER-Tour f = vi0 , ..., vim−1 , vim = vi0
und streiche in f alle vi j , für die ein l < j existiert, mit vil = vi j , und erhalte einen
HAMILTON-Kreis C = vπ (1) , vπ (2) , ..., vπ (n) , vπ (1) .
Für das metrische TSP gehört die Christofides-Heuristik mit einer Komplexität von O(n3 )
zu den besten (bekannten) approximativen Algorithmen, und zwar mit einer Approximationsgüte von 3/2. Dies überlegt man sich folgendermaßen: Jede optimale Rundreise C0 ist
ein HAMILTON-Kreis im Graph. Durch Weglassen einer Kante in der Rundreise erhält
man ein Gerüst. Für das Minimalgerüst T gilt: g(T ) ≤ g(C0 ). Außerdem durchläuft C0
alle Knoten aus dem Graphen G, damit auch die ungeraden Knoten vπ (i1 ) , vπ (i2 ) , ..., vπ (il )
von T , wobei i1 < i2 < ... < il sei. Dann sind M ′ := {{vπ (i1 ) , vπ (i2 ) }, {vπ (i3 ) , vπ (i4 ) }, ...,
{vπ (il−1 ) , vπ (il ) }} und M ′′ := {{vπ (i2 ) , vπ (i3 ) }, {vπ (i4 ) , vπ (i5 ) }, ..., {vπ (il ) , vπ (i1 ) }} perfekte
Matchings von U . Offenbar sind die Kosten g(M) des minimalen perfekten Matchings
M kleiner oder gleich den Kosten von M ′ bzw. M ′′ , Demzufolge ist g(M) ≤ 21 g(C0 ). Daraus folgt: g(C) ≤ g(T ) + g(M) ≤ 23 g(C0 ). (vgl. [Son04])
27
6
DAS TRAVELING SALESMAN PROBLEM
Beispiel zur Anwendung des Christofides-Algorithmus [Koh04]
6 Computerprogramme P1 , ..., P6 sollen der Reihe nach auf einem Großrechner abgearbeitet werden (und dann wieder von vorn beginnen). Jedes Programm benötigt seine eigenen
Ressourcen, wie z.B. einen Teil des Hauptspeichers, einen Compiler und Laufwerke, und
der Wechsel von einer Ressourcenmenge zur anderen kostet Übertragungszeit. Die folgende Matrix C = (ci j ) enthält die Zeiten ci j , die für den Wechsel der Ressourcenmenge
der Hilfsmittel für das Programm Pi zu der für das Programm Pj benötigt werden.





C=



0
18
17
23
12
19
18
0
26
31
20
30
17
26
0
16
11
9
23
31
16
0
17
19
12
20
11
17
0
14
19
30
9
19
14
0









Man ermittle eine Reihenfolge der Programme, die eine möglichst geringe Gesamtübertragungszeit benötigt.
Zur Lösung des Problems muss als erstes ein Minimalgerüst T im Graphen bestimmt
werden. Die Bestimmung eines solchen Minimalgerüsts erfolgt mit dem Algorithmus von
Kruskal, da dieser, anders als der Algorithmus von Prim, keinen Startknoten benötigt und
der Algorithmus autonomer ablaufen kann. Anschließend werden im Minimalgerüst die
Knoten ungeraden Grades bestimmt (Abbildung 17).
18
P1
12
P2
P3
16
P2
11
31
9
26
30
P3
16
9
P5
P4
P6
P4
Abbildung 17: Christofides,
Minimalgerüst mit Knoten
ungeraden Grades
19
P6
Abbildung 18: Christofides,
perfektes Matching minimalen
Gewichts
Mittels der Knoten ungeraden Grades wird ein perfektes Matching M minimalen Gewichts bestimmt (Abbildung 18). Der Algorithmus von Hierholzer im Graphen
T ∪ M := (V, ET ∪ M) (Abbildung 19)
bestimmt die EULER-Tour
f = (P6 , P3, P4 , P2, P1 , P5 , P3, P6 ). Durch das Streichen der doppelten Knoten erhält man
den HAMILTON-Kreis C = (P6 , P3, P4 , P2 , P1, P5 , P6) (Abbildung 20) und damit die Reihenfolge der Programme, welche eine geringe (hier sogar optimale) Gesamtübertragungszeit von 100 Zeiteinheiten benötigen.
28
6
18
18
P1
12 P3
P2
16
31
P4
DAS TRAVELING SALESMAN PROBLEM
11
9
P1
12 P3
P2
16
31
P5
11
9
P4
P6
P6
Abbildung 19: Christofides,
Minimalgerüst und die Kanten des
perfekten Matchings minimalen
Gewichts
P5
14
Abbildung 20: Christofides,
HAMILTON-Kreis
6.2. Implementation in Java
Die Klassenstruktur der Klasse Christofides besteht aus folgenden Methoden:
1
2
3
4
5
6
7
8
9
10
public cl as s C h r i s t o f i d e s
{
p u b l i c C h r i s t o f i d e s ( Graph ) throws E x c e p t i o n
p u b l i c C h r i s t o f i d e s ( Graph , i n t )
p u b l i c b o o lea n s t a r t ( ) throws E x c e p t i o n
p r i v a t e b o o lea n i n i t ( ) throws E x c e p t i o n
p r i v a t e V e c t o r g e t S h o r t P a t h A n d W e i g h t ( V e c t o r ) throws E x c e p t i o n
p r i v a t e v o i d g e t M a t c h i n g ( V ecto r , V e c t o r ) throws E x c e p t i o n
p r i v a t e b o o lea n
i s S a t i s f y T r i a n g l e I n E q u a t i o n ( ) throws E x c e p t i o n
}
Listing 6: Hauptmethoden der Klasse Christofides
Die Christofides-Klasse stellt zwei Konstruktoren zur Verfügung. Zum einen den Konstruktor Christofides(Graph) und zum anderen den Konstruktor Christofides(Graph,int).
Nachdem die Vorbedingungen
• der Graph besitzt ausschließlich positive Wichtungen,
• der Startknoten ist im Graphen enthalten (gegebenfalls erfolgt eine Überprüfung
der ID des Knotens zur Bestimmung des Minimalgerüsts nach Prim),
• der Graph ist schlicht und vollständig,
• der Graph ist ungerichtet,
• der Graph ist zusammenhängend und
• der Graph erfüllt die Dreiecksungleichung
überprüft wurden, wird beim Aufruf des ersten Konstruktors ein Minimalgerüst nach
Kruskal und im zweiten Fall ein Minimalgerüst nach Prim bestimmt. Anschließend werden völlig analog zum Algorithmus die Knoten ungeraden Grades und ein perfektes
29
6
DAS TRAVELING SALESMAN PROBLEM
Matching minimalen Gewichts zwischen den Knoten ungeraden Grades bestimmt. Mittels des Hierholzer-Algorithmus wird eine EULER-Tour bestimmt. Dafür wird ein neues
Objekt vom Typ Hierholzer erzeugt, welches einen Vektor mit den Knoten-IDs zurückgibt. Nach Entfernen der doppelt besuchten Knoten aus diesem Vektor erhält man einen
HAMILTON-Kreis. Als besonders schwierig erwies sich die Überprüfung der Dreiecksungleichung. Hierfür wurde, wie in Listing 7 dargestellt, eine Distanzmatrix erzeugt.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
f o r ( i n t i = 0 ; i < n o d eC o u n t ; i ++) {
f o r ( i n t i i = 0 ; i i < n o d eC o u n t ; i i ++) {
E n u m e r a t i o n enums2
= m_edges . k e y s ( ) ;
String
count ;
w h i l e ( enums2 . h as M o r eE lem e n t s ( ) ) {
c o u n t = ( S t r i n g ) enums2 . n e x t E l e m e n t ( ) ;
int
i d 1 = ( ( Edge ) m_edges . g e t ( c o u n t ) ) . get_FromNode ( ) ;
int
i d 2 = ( ( Edge ) m_edges . g e t ( c o u n t ) ) . get_ToNode ( ) ;
i f ( ( i d 1 == I n t e g e r . p a r s e I n t ( ( S t r i n g ) nodeName . e l e m e n t A t ( i ) )
&& i d 2 == I n t e g e r . p a r s e I n t ( ( S t r i n g ) nodeName . e l e m e n t A t ( i i ) ) )
| | ( i d 2 == I n t e g e r . p a r s e I n t ( ( S t r i n g ) nodeName . e l e m e n t A t ( i ) )
&& i d 1 == I n t e g e r . p a r s e I n t ( ( S t r i n g ) nodeName . e l e m e n t A t ( i i ) ) ) ) {
m _ d i s t a n c e M a t r i x [ nodeCount −i i −1][ nodeCount −i −1] =
( ( Edge ) m_edges . g e t ( c o u n t ) ) . g et_ E d g eW ei g h t ( ) ;
}
}
}
}
Listing 7: Christofides, Erzeugen der Distanzmatrix zur Überprüfung der
Dreiecksungleichung
Anschließend wird, wie in Listing 8 dargestellt, die Dreiecksungleichung anhand der Distanzmatrix überprüft.
1
2
3
4
5
6
f o r ( i n t i 1 = 0 ; i1 < nodeCount −1; i 1 ++)
f o r ( i n t i 3 = i 1 + 1 ; i3 <nodeCount −1; i 3 ++)
f o r ( i n t i 2 = i 3 + 1 ; i2 < n o d eC o u n t ; i 2 ++)
i f ( m_distanceMatrix [ i1 ] [ i3 ] +
m_distanceMatrix [ i1 ] [ i2 ] < m_distanceMatrix [ i1 +1][ i2 ] )
return f a l s e ;
Listing 8: Christofides, Überprüfen der Dreiecksungleichung anhand der Distanzmatrix
Sollte die Dreiecksungleichung an einer Stelle nicht erfüllt sein, so liefert die Methode
isSatisfyTriangleInEquation () ein false zurück.
30
7
MAXIMALSTROMPROBLEM
7. Maximalstromproblem
Das folgende Kapitel beschäftigt sich mit Flüssen, auch Ströme genannt, in gerichteten
Graphen. Ebenfalls wird dieses Thema mit einem kurzen, praxisnahen Beispiel eingeleitet, wobei ein Wasserrohrsystem das Zutreffendste ist, um das Maximalstromproblem zu
veranschaulichen.
In einem Wasserrohrsystem mit mehreren Leitungen unterschiedlichen Querschnitts („Kapazität“) soll Wasser von einer Pumpstation zu einem Auffangbecken gepumpt werden,
wobei das Wasser in den Rohren nur in eine vorgegebene Richtung fließen kann, nicht
aber in die entgegengesetzte Richtung. Es ist die durch das Rohrsystem pro Zeiteinheit
maximal beförderbare Wassermenge (einschließlich der Flussmenge jedes einzelnen Rohres) zu bestimmen.
Spezielle kantenbewertete, gerichtete Graphen werden im Zusammenhang mit dem Maximalstromproblem als Netzwerke oder auch Transportnetze bezeichnet. Bei einem
Netzwerk N = (V, E, q, s, c) handelt es sich um einen schlingenfreien, zusammenhängenden Digraphen D = (V, E) mit der Knotenmenge V und der Kantenmenge E. Die so genannte Quelle ist q, wobei q ∈ V mit d − (q) = 0. Als Senke bezeichnet man s mit s ∈ V ,
d + (s) = 0. q und s sind dabei zwei ausgewählte Knoten der Menge V , die jeweils die
Eigenschaften der Quelle bzw. der Senke besitzen. Die Kapazitätsfunktion c ist eine Abbildung c : E → R+ ∪{0} und ordnet somit jeder Kante aus E deren so genannte Kapazität
c(e) zu. Die Funktion f heißt zulässiger Fluss oder Strom im Netzwerk N, wenn sie für
alle e ∈ E und für alle a ∈ V −{q, s} die Bedingungen 0 ≤ f (e) ≤ c(e) und f + (a) = f − (a)
erfüllt. Mit f + (a) bezeichnet man den Fluss aller von Knoten a ausgehenden Kanten und
mit f − (a) den Fluss aller im Knoten a endenden Kanten. Weiterhin bezeichnet man den
(zulässigen) Strom f0 mit f0 (E) = ∑e∈E f0 (e) = 0 als den Nullstrom. Ein Bogen e ∈ E
wird bezüglich des Stromes f als ungesättigter Bogen bezeichnet, wenn seine Restkapazität cr (e) größer als Null ist, also cr (e) = c(e) − f (e) > 0 gilt. Die Abbildung 21 auf der
nächsten Seite zeigt ein solches Netzwerk, wobei die Kantenbeschriftung die Kapazität
der entsprechenden Kante darstellt. Ziel ist es, unter Beachtung der Kantenkapazitäten
einen Maximalstrom zu bestimmen, der von der Quelle q ausgehend die Senke s erreicht.
Im Folgenden wird nun der Algorithmus von Ford und Fulkerson beschrieben, der eine
Lösung für dieses Problem liefert.
31
7
MAXIMALSTROMPROBLEM
v1
5
q
5
2
2
4
v2
3
1
v4
v3
4
v5
3
s
6
Abbildung 21: Ein Netzwerk mit der Quelle q, der Senke s, gerichteten Kanten und ihren
Kapazitäten c
7.1. Algorithmus von Ford-Fulkerson
Der Algorithmus entstand durch die Zusammenarbeit von Lester Randolph Ford Jr. und
Delbert Ray Fulkerson. Seit der Veröffentlichung [FFJ56] im Jahre 1956 hat der Algorithmus zur Lösung von Maximalstromproblemen nicht an Bedeutung verloren. Im Wesentlichen wird in jedem Schritt nach einem Weg von der Quelle q zur Senke s gesucht,
mit dessen Hilfe der aktuelle Strom vergrößert werden kann. Existiert kein solcher Weg
mehr, so ist ein Maximalstrom im Netzwerk gefunden.
Für die graphische Darstellung des Ford-Fulkerson-Algorithmus werden für die Bachelorarbeit einige Vereinbarungen getroffen. Für eine zweispaltige Darstellung ergeben sich
für die abgebildeten Graphen folgende Unterschiede: Befinden sich in der Abbildung des
Transportnetzwerkes an den Bögen Angaben mit Klammern, so stellt der Wert vor der
Klammer die Kapazität eines Bogens dar, der Wert innerhalb der Klammern ist der aktuelle Stromwert. Fehlt bei der Beschriftung ein Klammerausdruck, so handelt es sich um die
Angabe der Restkapazitäten und bei der gesamten Abbildung um das Residualnetzwerk.
Das Residualnetzerk ist ein Netzwerk, das alle Bögen des ursprünglichen Netzwerkes mit
um den jeweiligen Flusswert verminderten Kantenkapazitäten enthält. Zusätzlich enthält
das Residualnetzwerk für jede Kante e mit f (e) > 0 eine zu e entgegengesetzt gerichtete
Kante e′ mit der (Rest-)Kapazität cr (e′ ) = f (e).
32
7
MAXIMALSTROMPROBLEM
7.1.1. Mathematische Betrachtung
Sei N = (V, E, q, s, c) ein Netzwerk mit ganzzahligen Kapazitäten. Im Folgenden bezeichnet f stets einen (zulässigen) Strom in N. Noch nicht definierte Ausdrücke werden im
nachfolgenden Text spezifiziert.
1. Setze f gleich dem Nullstrom f0 , also f := f0 .
2. T := ({q}, 0).
/
3. A := V (T ).
4. Wenn ein ungesättigter Bogen e = (i, j) ∈ (A, Ā) oder
ein Bogen e = ( j, i) ∈ (Ā, A) mit f (e) > 0 existiert −→ gehe zu 6.
5. Stopp.
6. T ∗ := (V (T ) ∪ { j}, E(T ) ∪ {e}).
7. Wenn s ∈
/ V (T ∗ ) −→ setze T := T ∗ und gehe zu 3.
8. Sei w der eindeutig bestimmte (q, s)-Weg in T ∗ .
Bestimme die Restkapazität cr (w).
Bestimme den Strom f ′ und dessen Stromstärke I( f ′ ),
wobei I( f ′ ) = I( f ) + cr (w) ≥ I( f ) + 1.
9. f := f ′ .
Gehe zu 2.
Am Anfang des Algorithmus wird der Gesamtstrom f des Netzwerks mit dem Nullstrom
initialisiert, also für jeden Bogen auf 0 gesetzt. Da im Verlauf der einzelnen Schritte ein
Weg w von der Quelle q zur Senke s aufgebaut werden soll, wird der Baum T zu Hilfe
genommen, der zu Beginn nur den Knoten q enthält. Weiterhin ordnet man alle Knoten
aus T einer Knotenmenge A zu. Im folgenden Schritt 4 wird ein Bogen e gesucht, dessen
Startknoten in A und dessen Zielknoten in Ā liegt - es gilt also e = (i, j) ∈ (A, Ā), e heißt
dann Vorwärtsbogen - und der weiterhin noch über freie Kapazitäten verfügt, das heißt
c(e) − f (e) > 0. Alternativ kann dies auch ein so genannter Rückwärtsbogen sein mit
e = ( j, i) ∈ (Ā, A), für den aber f (e) > 0 sein muss. Ist die Suche nach einem solchen
Bogen erfolgreich verlaufen, wird im Schritt 6 der Baum T um den entsprechenden Bogen
e und den Knoten j ∈ Ā erweitert. Sollte nach diesem Schritt noch nicht die Senke s in T
enthalten sein, so muss in Schritt 3 der Baum T erweitert werden, bis s ∈ V (T ) ist oder der
Algorithmus in Schritt 5 stoppt. Im Fall s ∈ V (T ) sucht man in Schritt 8 (bei s beginnend)
den (eindeutig bestimmten) (q, s)-Weg w und bestimmt seine Restkapazität
cr (w) := min cr (e),
e∈E(w)
33
7
MAXIMALSTROMPROBLEM
d.h. die minimale, freie Kapazität aller Kanten e, die den Weg w bilden. Allerdings ist zu
beachten, dass eine Unterscheidung für Vorwärts- sowie Rückwärtsbögen gemacht werden muss. Ist e ∈ E(w) ein Vorwärtsbogen, so lässt sich die Restkapazität des Bogens
wie gewohnt mit cr (e) = c(e) − f (e) ermitteln. Handelt es sich um einen Rückwärtsbogen, so ergibt sich cr (e) = f (e). Die Restkapazität des Weges w ist wegen Schritt
4 größer 0, somit handelt es sich um einen so genannten zunehmenden Weg, der laut
I( f ′) = I( f ) + cr (w) ≥ I( f ) + 1 den Gesamtstrom um cr (w) erhöht. Zum Schluss wird
der Gesamtstrom f auf den neuen Wert f ′ gesetzt und die Suche nach einem neuen zunehmenden Weg in Schritt 2 fortgesetzt.
Der Algorithmus von Ford-Fulkerson bricht nach endlich vielen Schritten mit einem Maximalstrom ab, lediglich bei irrationalen Kapazitäten ist es möglich, dass er nicht abbricht und zusätzlich gegen einen falschen Wert konvergiert. Die Bücher von Ford und
Fulkerson [FFJ62] oder auch von Lovász und Plummer [Lov86] geben dazu Beispiele
an. Ebenso kann unter ungünstigen Umständen die Laufzeit des Algorithmus nicht polynomial in n und m sein. Sie ist dann abhängig von der Kapazitätsfunktion c. Abbildung 22 stellt ein Netzwerk dar, welches im ungünstigsten Fall 2p Durchläufe bei dem
Ford-Fulkerson-Algorithmus benötigt. Dies tritt nur ein, wenn abwechselnd die Wege
(q, a, b, s) und (q, b, a, s) genommen werden und somit die Stromstärke in jedem Schritt
nur um 1 erhöht wird. Der Algorithmus wird polynomial, wenn in jedem Schritt ein kürzester zunehmender Weg gewählt wird; das zeigten Edmonds und Karp im Jahr 1972
[Edm72].
a
p
q
p
s
1
p
p
b
Abbildung 22: Ein Netzwerk, das bei ungünstiger Wahl zunehmender Wege 2p
Durchläufe des Ford-Fulkerson-Algorithmus benötigt
34
7
MAXIMALSTROMPROBLEM
7.1.2. Implementation in Java
Die Programmierung des Algorithmus von Ford und Fulkerson macht es notwendig, neben dem eigentlichen Netzwerk, also dem gerichteten, kantenbewerteten Graphen, noch
ein weiteres Graph-Objekt für den Algorithmus zur Verfügung zu stellen. Der eigentliche Graph, der den aktuellen und später den maximalen Strom enthält, ist in der Variable m_flowGraph gespeichert. Die Variable m_residualGraph speichert den Graphen mit
den verbleibenden Kapazitäten und weiteren notwendigen Kanten. Den Aufbau der JavaKlasse kann man dem Listing 9 entnehmen. Zur näheren Erläuterung aller Methoden wird
nun einleitend eine grobe Struktur des Ablaufs des Algorithmus gegeben:
1. Setze Strom aller Kanten in m_flowGraph auf 0.
2. Findet findPath() keinen zunehmenden Weg w von q nach s, gehe zu 4.
3. Ermittle cr (w) und erhöhe den Strom mit addFlow() entlang von w.
Gehe zurück zu 2.
4. Stopp.
Der Konstruktor der Klasse FordFulkerson initialisiert diese mit einem gerichteten Ausgangsnetzwerk und der Knoten-ID der Quelle q und Senke s. Weiterhin wird hier der
Strom aller Kanten mit Hilfe der Methode resetWeight() auf 0 gesetzt. Mit dem Aufruf
der Methode init() wird überprüft, ob das Ausgangsnetzwerk die Bedingungen gerichtet,
zusammenhängend und schlicht erfüllt, ob die Kapazitäten der Kanten nicht negativ sind
und ob die Quelle und Senke ordnungsgemäß definiert wurden.
1
2
3
4
public cl as s FordFulkerson
{
p r i v a t e Graph m_flowGraph ;
p r i v a t e Graph m _ r e s i d u a l G r a p h ;
5
p u b l i c F o r d F u l k e r s o n ( Graph , i n t , i n t ) throws E x c e p t i o n { }
p r i v a t e v o i d addFlow ( Graph )
{ }
p r i v a t e Graph a d j u s t F l o w G r a p h ( ) { }
p r i v a t e Graph f i n d P a t h ( Edge ) { }
p r i v a t e i n t getMaxFlow ( ) { }
p r i v a t e i n t g etM in W eig h t ( Graph ) { }
p r i v a t e v o i d i n i t ( ) throws E x c e p t i o n { }
p r i v a t e v o i d r e s e t W e i g h t ( Graph g r a p h ) { }
p r i v a t e b o o lea n s e a r c h D i r e c t e d P a t h ( Graph , i n t , i n t , i n t ) { }
public void s t a r t ( ) { }
6
7
8
9
10
11
12
13
14
15
16
}
Listing 9: Hauptmethoden der Klasse FordFulkerson
35
7
MAXIMALSTROMPROBLEM
Der Algorithmus selbst wird mit der öffentlichen Methode start() begonnen. Hier wird zunächst eine Startkante für den Algorithmus gesucht, deren Startknoten q ist. Die Methode
findPath() sucht rekursiv einen Weg zur Senke, wobei natürlich Kreise im Weg nicht zulässig sind. Grundlage für die Suche nach einer Verbindung von q nach s ist der Residualgraph m_residualGraph, der sowohl Vorwärts- als auch Rückwärtsbögen als Bögen mit
den aktuellen (Rest-) Kapazitäten enthält. Auf Basis der Abbildung 21 auf Seite 32 wird
der in Abbildung 23(a) auf der nächsten Seite rot dargestellte Weg w als zunehmender
Weg ausgewählt. Eine Erhöhung des Stroms findet durch die Methode addFlow() statt, die
Methode getMinWeight() liefert dazu als Stromerhöhung entlang w den Wert cr (w) = 2.
Nun wird der gesamte Strom in m_flowGraph entlang dieses Weges um 2 erhöht und es
ergibt sich der Strom in Abbildung 23(b). Weiterhin muss die Methode addFlow() den Residualgraph aktualisieren, d.h. es muss der Strom der Menge 2 von der Restkapazität aller
Vorwärtskanten des Weges w abgezogen werden, damit die Restkapazität ordnungsgemäß repräsentiert wird. Wird dabei die Restkapazität einer Kante 0, wird diese gelöscht.
Da der Algorithmus von Ford-Fulkerson die Möglichkeit bietet, auch Rückwärtskanten
auszuwählen, müssen entlang des Weges w die Restkapazitäten der Rückwärtskanten um
den Betrag 2 erhöht werden. Gibt es noch keine Rückwärtskante, da die Vorwärtskante
noch unbenutzt war, so wird eine neue Rückwärtskante mit der entsprechenden Restkapazität eingefügt. Abbildung 23(c) zeigt den so entstandenen Residualgraphen mit den
korrigierten Restkapazitäten der Vorwärtskanten und entsprechenden Rückwärtskanten.
Darin wird im nächsten Schritt wieder ein Weg von der Quelle zur Senke gesucht. Ein
möglicher solcher Weg ist erneut durch eine rote Markierung hervorgehoben. Wieder ergibt sich daraus ein neuer Strom, wie in Abbildung 23(d) zu sehen ist. Die letzten beiden
Abbildungen stellen nochmals den sich ergebenden Residualgraphen (Abbildung 23(e))
und den neuen Strom (Abbildung 23(f)) dar. Natürlich ist der Algorithmus damit noch
nicht beendet, allerdings sollte die Funktionsweise klar ersichtlich sein.
Findet der Algorithmus keinen zunehmenden Weg mehr von q nach s, so kann die maximale Stromstärke mit Hilfe der Methode getMaxFlow() berechnet werden und alle Schritte sind somit abgeschlossen.
Zu erwähnen ist noch, dass diese Implementation des Ford-Fulkerson-Algorithmus schon
in einem entscheidenden Punkt optimiert ist. Das in 7.1.1 auf Seite 34 angesprochene
Problem einer möglicherweise nicht polynomialen Laufzeit des Algorithmus wurde umgangen, da der Algorithmus in Schritt 2 nicht zufällig eine benachbarte Kante der Quelle
nimmt, sondern eine Nachbarkante so lange als Ausgangskante für die Wegsuche benutzt,
wie, von ihr ausgehend, weitere Wege zur Flusserhöhung gefunden werden können. Erst
dann wird sich einer anderen zu q inzidenten Kante zugewandt.
36
7
MAXIMALSTROMPROBLEM
v1
v1
5
q
5(0)
2
4
v2
5
v3
3
1
2
3
4(0)
q
s
6
v1
5(0)
2
4
1
2
2
v3
2
2
1
1
6
4
1
2
v4
v3
3
2
2
s
6(0)
4(2)
(d) aktueller Strom
q
s
3
1
v1
5(0)
2
1
3(3)
v5
v4
5
v2
3(2)
1(0)
2(2)
v1
1
v3
v2
5(1)
2
(c) Residualgraph
q
2(0)
4(1)
q
s
v5
v4
6(0)
4(2)
(b) aktueller Strom
5
5
s
v5
v4
v1
v2
3(2)
3(2)
1(0)
2(2)
4
(a) Residualgraph
q
v3
v2
5(0)
v5
v4
2(0)
6
2(0)
4(3)
5(3)
1(0)
2(2)
v5
v3
v2
v4
3(0)
3(3)
s
6(2)
v5
4(2)
(f) aktueller Strom
2
(e) Residualgraph
Abbildung 23: Die linke Seite zeigt das Residualnetzwerk mit dem aktuellen Weg, die
rechte Seite den sich daraus ergebenden Strom. Die rot gefärbten Kanten stellen
zunehmende Wege von q nach s dar.
37
8
MATCHINGPROBLEME
8. Matchingprobleme
Matchingprobleme treten in der Praxis wie im nachfolgenden Beispiel meist als Zuordnungsprobleme auf.
In einer Schule besitzen die Lehrer a1 , . . . , an in der Regel unterschiedliche Fachqualifikationen. So kann unter anderem Lehrer a1 nur Französisch und Englisch oder Lehrer
a4 nur Mathematik und Physik unterrichten. Jeder Lehrer kann somit Unterricht in einem
oder mehreren der Fächer b1 , . . ., bn geben. Nun stellt sich die Frage, ob es zu einem festen Zeitpunkt möglich ist, dass jeder Lehrer eines seiner Fächer unterrichten kann und
dabei jedes Fach von genau einem Lehrer gelehrt wird.
Dieses einfache klassische Beispiel für ein Zuordnungsproblem verfügt über genau die
graphentheoretischen Voraussetzungen, die für den folgenden Algorithmus notwendig
sind. Der Graph G ist ein bipartiter Graph mit der Bipartition A = {a1 , . . ., an } und
B = {b1 , . . . , bn }, also mit den Lehrern A und den Fächern B. Ein Knoten aus A wird
dementsprechend mit mindestens einem Knoten aus B durch eine Kante verbunden. Da
der Graph bipartit sein muss, ist kein Knoten aus A oder B mit einem Knoten aus der
selben Partition verbunden. Anhand des Beispiels ist zu erkennen, dass eine Bipartition
gegeben ist, da eine Verbindung zwischen zwei Lehrern oder zwischen zwei Fächern einfach sinnlos wäre.
Eine, in gewissem Sinne optimale Lösung für das Zuordnungsproblem existiert nur, wenn
ein perfektes Matching in dem Graphen G existiert, also wenn jeder Lehrer genau ein
Fach unterrichtet. Sollte bei einem solchen Problem kein perfektes Matching existieren,
so ist meist ein maximales Matching von großem Interesse.
8.1. Ungarische Methode
Eine Lösung für das eben genannte Problem liefert die so genannte Ungarische Methode,
erstmals veröffentlicht von Kuhn [Kuh55] im Jahre 1955. Weitere Beiträge dazu stammen
von Munkres 1957 [Mun57] und Edmonds 1965 [Edm65].
8.1.1. Mathematische Betrachtung
Vor dem eigentlichen Algorithmus sind noch einige Begriffe einzuführen. Sei M Matching in G = (V, E), a ∈ V − V (M). Ein M-alternierender Weg bezeichnet einen Weg,
dessen Kanten abwechselnd zu M und nicht zu M gehören. Der Baum T , ein Teilgraph
von G, heißt M-alternierender Wurzelbaum mit der Wurzel a, wenn a ∈ V (T ) und für
jedes v ∈ V (T ) ein M-alternierender (a, v)-Weg wa,v in T existiert. T ist genau dann gesättigt, wenn es keine Kante in G gibt, die T vergrößern kann.
Ein M-alternierender Weg in G heißt M-erweiternd, wenn die Endpunkte des Weges mit
keiner Kante aus M inzident sind.
38
8
MATCHINGPROBLEME
Ist w ein M-erweiternder Weg in T mit dem Startknoten a in G, welcher durch keine Kante aus M verlängerbar ist, so ist w auch M-erweiternd in G.
Wenn T gesättigt ist und kein M-erweiternder Weg in T von a aus existiert, so besitzt
auch G keinen solchen Weg. (vgl. [Vol91], S. 95)
Mit Hilfe dieser Überlegungen ergibt sich nach [Son04] folgender Algorithmus der Ungarischen Methode:
Sei G = (A ∪ B, E) paar und |A| ≤ |B|.
1. Sei M ein gesättigtes Matching.
2. Wenn V (M) ∩ A = A −→ Stopp.
3. Wähle a ∈ A −V (M).
Setze S := {a},
I := 0/ und
T := ({a}, 0).
/
4. Wenn I = N(S) −→ setze A := A − {a} und gehe zu 2.
5. Wähle y ∈ N(S) − I und e = {x, y} mit x ∈ V (T ).
6. Wenn y ∈ V (M) −→ wähle e′ = {y, z} ∈ M mit z 6∈ V (T ),
S := S ∪ {z},
I := I ∪ {y},
T := (V (T ) ∪ {y, z}, E(T ) ∪ {e, e′ }) und
gehe zu 4.
7. Wenn y ∈
/ V (M), so sei wa,x der (a, x)-Weg in T ,
w := (wa,x , e, y),
M := M △ E(w) und
gehe zu 2.
Der Algorithmus startet mit Hilfe eines schon existierenden, gesättigten Matchings. Dieses kann leicht konstruiert werden. Mit dieser Ausgangslage wird in Schritt 2 überprüft,
ob die Partition A schon vollständig durch das Matching M abgedeckt ist. Sind alle Knoten aus A schon im Matching enthalten, so ist ein maximales Matching gefunden, der
Algorithmus ist beendet. Ist das Abbruchkriterium nicht erfüllt, so gibt es also noch Knoten der Partition A, die noch nicht gematcht sind. Aus eben dieser Knotenmenge wird ein
Startknoten a ausgewählt. Es wird eine Knotenmenge S mit dem Knoten a initialisiert und
eine andere I mit der leeren Menge. Der Baum T enthält mit Beginn dieser Schleife auch
nur den Knoten a. Im darauf folgenden Schritt 4 wird ermittelt, ob die Knotenmenge I
alle Nachbarn der Knoten in S enthält. Tritt dieser Fall ein, bedeutet dies, dass über den
gewählten Startknoten a kein M-erweiternder Weg gefunden werden kann. Dieser Knoten wird verworfen und mit Schritt 2 ein neuer Startknoten gewählt. Mit Schritt 5 wird
39
8
MATCHINGPROBLEME
ein Knoten y ausgewählt, der einerseits zu einem Knoten in S, im ersten Durchlauf also
zu a, benachbart ist, andererseits aber noch nicht in der schon besuchten Knotenmenge
I liegt. Mit dieser Bedingung wird gewährleistet, dass vom Knoten a der Partition A nur
ein Knoten y aus der Partition B aufgesucht wird. Ist der gewählte Knoten y schon Teil
des Matchings M, so wird über eine schon gematchte Kante wieder ein noch nicht besuchter Knoten z der Partition A ausgewählt. Die Knotenmenge wird um diesen Knoten z
aus A vergrößert, ebenso I um den Knoten y aus B. Der Baum T wird dem entsprechend
um die zwei gewählten Kanten ergänzt. Mit dieser neuen Ausgangssituation wird wieder
ab Schritt 4 nach einem noch nicht besuchten Nachbarn der neuen Knotenmenge S gesucht. Ist allerdings der in Schritt 5 gewählte Nachbarknoten aus B noch nicht gematcht,
so wurde ein M-erweiternder Weg von einem Knoten der Partition A zu einem aus B
gefunden. Nun wird ein neues Matching durch die symmetrische Differenz (allgemein:
A △ B = (A − B) ∪ (B − A)) des alten Matchings und der Kanten des erweiternden Weges
gebildet. Dieser Vorgang setzt sich dann in Schritt 2 wieder fort, bis ein maximales oder
auch perfektes Matching gefunden wurde.
8.1.2. Implementation in Java
Die Umsetzung des Algorithmus basiert zu Teilen auf den Überlegungen von Hopcroft
und Karp im Jahre 1973 [Hop73], was für die Ungarische Methode eine Optimierung der
Laufzeit bedeutet.
Die wichtigsten Methoden und Variablen der Java-Klasse Ungar sind in Listing 10 als
Überblick aufgelistet. Initialisiert wird die Klasse mit dem Ausgangsgraphen G, der alle
möglichen Verbindungen der Knoten enthält. Weiterhin werden noch alle Knoten übergeben, die Element der ersten Partition des bipartiten Graphen sind. Im Anschluss daran
wird eine Differenzmenge dieser Knoten mit allen Knoten des Ausgangsgraphen gebildet
und es ergeben sich die Knoten der zweiten Partition. Die Methode init() überprüft dann,
ob der Ausgangsgraph schlicht und bipartit ist. Sollten alle Voraussetzungen erfüllt sein,
so kann der Algorithmus gestartet werden. Anhand des Listings ist zu erkennen, dass der
Algorithmus im Wesentlichen auf einer Breiten- und Tiefensuche beruht. Bezugnehmend
1
2
3
4
p u b l i c c l a s s Ungar
{
p r i v a t e H a s h t a b l e m_nodeLevel ;
p r i v a t e V e c t o r m_nodeQueue ;
5
p u b l i c Ungar ( Graph , V e c t o r ) throws E x c e p t i o n { }
p r i v a t e b o o lea n B r e i t e n s u c h e ( ) { }
p r i v a t e v o i d i n i t ( ) throws E x c e p t i o n { }
p rivat e void r e s e t L e v e l s ( ) { }
public void s t a r t ( ) { }
p riv a t e Vector Tiefensuche ( i n t ) { }
6
7
8
9
10
11
12
}
Listing 10: Hauptmethoden der Klasse Ungar
40
8
MATCHINGPROBLEME
darauf ergibt sich für den Ablauf der Ungarischen Methode folgender Algorithmus:
1. Starte mit einem leeren Matching M := 0.
/
2. Wenn simultane Breitensuche mit Breitensuche() keinen erweiternden Weg findet
−→ Stopp.
3. Gibt es einen erweiternden Weg, so starte Tiefensuche mit Tiefensuche() und finde
einen kürzesten alternierenden Weg von allen Startknoten.
4. Gehe zurück zu 2.
Die simultane Breitensuche entscheidet zum einen, ob es in dem Graphen noch einen
M-erweiternden Weg gibt und zum anderen assoziiert sie mit jedem Knoten Levelwerte
(m_nodeLevel in Listing 10). Mit dem Wissen, dass alle erweiternden Wege eine ungerade
Länge besitzen und von einem freien, also noch nicht gematchten Knoten a der Partition A
ausgehend, bei einem freien Knoten b der Partition B enden, erhält man durch das Setzen
eines Levelwertes level[u] bei allen Knoten u am Ende der Breitensuche die Länge eines
kürzesten alternierenden Weges, die dem Levelwert des letzten Knotens entspricht. Damit
zwischen bereits besuchten und noch nicht besuchten Knoten unterschieden werden kann,
wird zu Beginn das Level jedes Knotens des Graphen mit Hilfe der Methode resetLevels()
auf -1 gesetzt.
Weiterhin existiert eine Knotenwarteschlange m_nodeQueue, die alle Knoten enthält, die
bei der simultanen Breitensuche abgearbeitet werden. Daraus erschließt sich die Bedeutung der Bezeichnung „simultane Breitensuche“. Zu Beginn wird die Warteschlange nicht
nur mit einem freien Knoten aus der Partition A initialisiert, sondern mit allen freien Knoten von A.
Beim Ablauf des Algorithmus unterscheidet man zwei Phasen. In Phase 1 befinden sich
zu Beginn nur freie Knoten der Partition A in der Warteschlange. Dies wäre zu Beginn
des Algorithmus der Fall. Es wird nun für jeden Knoten u entlang seiner ungematchten
Nachbarkanten e = {u, v} ein Nachbar aus der Partition B gesucht. Wurde der Knoten v
noch nicht besucht, ist also sein Levelwert bei Besuch des Knotens noch -1, ergibt sich ein
neuer Levelwert level[v] = level[u] + 1. Der Knoten v wird dann an das Ende der Warteschlange angehängt. Sind alle Knoten der Partition A aus der Warteschlange abgearbeitet,
befinden sich nur noch Knoten aus B darin. Diese bilden die Grundlage für die zweite
Phase.
In Phase 2 besteht die Warteschlange am Anfang nur aus Knoten der Partition B. Nun erfolgt für jeden Knoten u der Warteschlange die Suche eines Nachbarknoten aus A entlang
einer gematchten Nachbarkante e = {u, v}. Auch hier wird ein bisher noch nicht besuchter Knoten v mit einem neuen Levelwert level[v] = level[u] + 1 versehen und dieser dann
an das Ende der Warteschlange gestellt. Die Phase ist abgeschlossen, wenn keine Knoten
aus B mehr in der Warteschlange sind.
Die Breitensuche endet genau dann, wenn in Phase 1 ein freier Knoten der Partition B
41
8
1
2
3
4
a
b
c
d
MATCHINGPROBLEME
Abbildung 24: Ein bipartiter Graph mit gesättigtem Matching. Gematchte Kanten sind
rot gefärbt.
gefunden wird. Der M-erweiternde Weg hat dann die Länge level[v] und endet mit v. Mit
dieser Gewissheit, dass ein erweiternder Weg existiert, wird die Tiefensuche gestartet.
Ebenso kann die Breitensuche enden, wenn kein Knoten mehr in der Warteschlange ist
und somit das erarbeitete Matching maximal ist.
Der in Abbildung 24 dargestellte Graph mit einem gesättigten Matching soll ein sehr
einfaches Beispiel darstellen, auf dessen Grundlage die Breitensuche die Levelwerte vergibt. Da dieses Matching offensichtlich nicht maximal ist, wird die Breitensuche mit den
freien Knoten 1 und 3 initialisiert. In den darauf folgenden Phasen ergeben sich die in
Abbildung 25 auf der nächsten Seite dargestellen Levelwerte. Wie zu erkennen ist, ist ein
kürzester M-erweiternder Weg ein Weg der Länge 3. In diesem Beispiel werden sogar
zwei erweiternde Wege gleicher Länge gefunden. Mit diesem Ergebnis kann nun die Tiefensuche gestartet werden.
Da die Tiefensuche nur dann aufgerufen wird, wenn die Breitensuche erfolgreich beendet
wurde, kann der erweiternde Weg aus der Breitensuche direkt übernommen werden. Mit
dem Ziel, möglichst viele solcher Wege zu finden, wird die Tiefensuche mit allen freien
Knoten der Partition A initialisiert und es werden nur Kanten e betrachtet, die eine der
beiden folgenden Bedingungen erfüllen:
• Ist der aktuelle Knoten u ∈ A, so ist e = {u, v} ∈ E \ M und level[v] = level[u] + 1.
• Ist der aktuelle Knoten u ∈ B, so ist e = {u, v} ∈ M und level[v] = level[u] + 1.
Unter ausschließlicher Verwendung solcher Kanten e erreicht man von einem Knoten
u ∈ A nach level[u] Schritten einen Knoten v ∈ B und dieser Weg ist ein kürzester Merweiternder Weg. Dieser muss nun umgematcht („invertiert“) werden, das heißt, Kanten
dieses Weges, die in M waren, werden aus M gelöscht und Kanten des Weges, die noch
nicht in M waren, werden eingefügt. Diese Operation entspricht der symmetrischen Differenz, wie sie schon bei der mathematischen Betrachtung in 8.1.1 auf Seite 40 erwähnt
wurde. Damit verbunden ist auch ein Sperren der Knoten des Weges für die aktuelle Tiefensuche von einem anderen Startknoten aus. Es werden die Levelwerte der Knoten auf
dem Weg auf -1 zurückgesetzt.
42
8
MATCHINGPROBLEME
Führt man das Beispiel aus Abbildung 24 auf der vorherigen Seite weiter fort, so erhält
man mit der Tiefensuche die Levelwerte aus Abbildung 25. Die Tiefensuche findet nun
aufgrund der aufsteigenden Knotenlevel den erweiternden Weg (1, a, 2, b). Dieser wird
umgematcht und für die laufende Tiefensuche gesperrt, so dass nur noch ein zunehmender Weg (3, c, 4, d) gefunden und umgematcht wird. Natürlich müssen die Knoten wieder
gesperrt werden, aber eine weitere Tiefensuche ist erfolglos, da es keinen freien Startknoten aus A mehr gibt. Das Ergebnis dieses Ummatchens ist der in Abbildung 26 dargestellte Graph, der nun ein Matching maximaler Kardinalität enthält. Der Algorithmus war
erfolgreich.
Level 0:
1
3
Level 1:
a
c
Level 2:
2
4
Level 3:
b
d
Abbildung 25: Aufgespannter Levelwert-Baum. Jedem Knoten wird anhand seines
Vorgängers ein Levelwert zugeordnet.
1
2
3
4
a
b
c
d
Abbildung 26: Das Ergebnis des Algorithmus: ein Matching maximaler Kardinalität.
43
9
KLASSENAUFBAU
Teil III.
Das Programm GraphCalc
9. Klassenaufbau
Das Java-Programm GraphCalc besteht Java-typisch aus verschiedenen Paketen, um eine Trennung von Programmfunktionen zu erreichen. Die Klasse GraphCalc dient dabei als Einstiegspunkt für den Java-Interpreter, um das Programm zu starten. Die Klasse
GraphHQ, die danach instanziert wird, beinhaltet alle Funktionen, die mit der Darstellung
der Anwendung zu tun haben. Diese Klasse wird auf Seite 45 näher erläutert und gehört
zum Package GraphVis, dessen Aufbau im Klassendiagramm in Abbildung 27 dargestellt
ist. Ein weiteres Paket Algorithm enthält die im Programm enthaltenen Algorithmen. Jeder Algorithmus ist in einer separaten Klasse implementiert, was es ermöglicht, das Programm um weitere Algorithmen modular zu erweitern. Das Package Utils enthält wichtige
Klassen, die klassenübergreifend Verwendung finden, unter anderem eine Klasse Graph,
die einen gespeicherten Graphen als Datentyp implementiert, ebenso eine statische Klasse
GraphUtils, die Methoden enthält, die von mehreren Algorithmen benötigt werden und
somit nur einmal implementiert werden müssen. Im Paket Graph sind Beispielgraphen
enthalten, die beim ersten Start des Programmes installiert werden. Mit diesen Graphen
können alle Algorithmen getestet werden. Eine Übersicht über die Paketstruktur des Programmes GraphCalc gibt das Klassendiagramm in Abbildung 28 auf der nächsten Seite.
GraphOutputFrame
GraphNaviPanel
GraphHQ
GraphPanel
GraphEditGui
GraphTreePanel
GraphPopupMenu
GraphMenu
GraphPrint
Abbildung 27: Klassendiagramm des Packages GraphVis
44
10
KOMPONENTEN
Utils
GraphVis
Algorithm
GraphCalc
Graph
Abbildung 28: Klassendiagramm des gesamten Programmes GraphCalc
10. Komponenten
Dem Nutzer von Computersoftware erschließen sich meist nur die visuellen Komponenten einer Anwendung. Die Interaktion mit dem Programm GraphCalc kann man in drei
Bereiche unterteilen, wobei die ersten beiden nahe beieinander liegen. Einerseits ist das
das Gerüst der Anwendung, welches die Menüs enthält und die grundsätzliche Kommunikation der einzelnen Komponenten des Programmes verwaltet. Diese Funktionalität stellt
die Java-Klasse GraphHQ zur Verfügung, welche nachfolgend näher beschrieben ist. Der
zweite, auf Seite 47 beschriebene Bereich wird durch die Klasse GraphPanel implementiert und stellt die gesamte Zeichenfunktionalität zur Verfügung. Eine weitere Komponente ist das Konvertierungswerkzeug, welches mit der eigentlichen Implementierung der
Algorithmen wenig zu tun hat. Es dient vielmehr dazu, Graphen in verschiedene Dateiformate umzuwandeln. Eine nähere Erläuterung dazu befindet sich auf Seite 53.
10.1. GraphHQ
Die Java-Klasse GraphHQ ist das äußere Gerüst des Programmes GraphCalc; hier laufen
alle Informationen, von Nutzereingaben bis zu den Ergebnissen der Algorithmen, zusammen und es werden daraufhin weitere Aktionen ausgeführt. Das Listing 11 auf Seite 47
listet
die
wichtigsten
Methoden
der
Klasse
auf.
Die
Klasse
GraphHQ ist von der Basisklasse JFrame abgeleitet, die alle wichtigen Funktionen eines
Anwendungsfensters implementiert. Zusätzlich werden noch die Interfaces
DragGestureListener und DropTargetListener implementiert, die es ermöglichen, dass
eine Datei mit der Dateiendung .graph einfach per Drag’n’Drop in das Applikationsfenster gezogen werden kann und damit gleich der entsprechende Graph geladen wird.
Die Klasse GraphHQ speichert alle komponentenübergreifenden Zustände in entspre-
45
10
KOMPONENTEN
chenden Membervariablen, unter anderem wird ein Graph-Objekt gespeichert, welches
den aktuell geladenen Graphen enthält, ebenso wird die aktuell selektierte Kante oder der
ausgewählte Algorithmus gespeichert.
Der Konstruktor GraphHQ() initialisiert zuerst das Fenster mit seinen wichtigsten Eigenschaften, danach werden allen eben erwähnten Membervariablen mit der Methode
createElements() Anfangswerte zugewiesen. Daraufhin erzeugt die Methode
createPanels(int) die Struktur des Hauptfensters. Durch den übergebenen Wert wird unterschieden, ob es sich bei dem Anzeigemodus um den Vollbild- oder Fenstermodus handelt. Wird das Programm als Vollbild dargestellt, so werden einzelne Fensterinhalte aus
der Fensteransicht aus Platzgründen nicht dargestellt. Der Vollbildmodus ist daher vorrangig für die Präsentation von Algorithmen gedacht, wo zum Beispiel die Baumansicht
überflüssig ist und bei Bedarf das Ausgabefenster ausgeblendet werden kann. Im Fall des
Fenstermodus
ruft
die
Methode
createPanels(int)
die
Methode
createCenterPanel() auf. Diese Methode erzeugt nun alle Fensterbereiche: den eigentlichen Zeichenbereich, die Baumanzeige des Graphen, den Navigationsbereich und den
Ausgabebereich.
Wie schon erwähnt, dient die Klasse GraphHQ als eine Art Vermittler zwischen den einzelnen Klassen. Wenn eine Änderung in einer Komponente der Anwendung auch eine
andere Komponente betrifft, wird in der Klasse GraphHQ eine Methode aufgerufen, die
alle anderen betroffenen Komponenten benachrichtigt. Dadurch muss nicht eine Komponente selbst wissen, wer noch von einer Änderung betroffen ist, sondern nur die Klasse GraphHQ besitzt dieses Wissen und führt alle notwendigen Schritte aus. Wird beispielsweise eine Kante durch einen Mausklick im Graphen selektiert, wird die Methode
updateTree() von der Klasse GraphPanel aufgerufen. Diese Methode benachrichtigt nun
die Klasse GraphTree, denn diese ist dafür verantwortlich, alle Kanten aufzulisten und eine selektierte Kante grafisch hervorzuheben. Des Weiteren können alle Klassen im Package GraphVis auf nahezu sämtliche wichtigen Membervariablen der Klasse GraphHQ zugreifen, die Auswirkungen werden sofort in allen Klassen sichtbar. Welche Variablen dies
genau betrifft, kann im Quelltext nachgelesen werden.
Eine weitere wichtige Methode ist switchView(int). Diese wird von der Methode
switchFullscreen()aufgerufen, wenn eine Umschaltung zwischen Vollbild- und Fensterdarstellung erfolgen soll. Die hier verwendete Variante zur Vollbilddarstellung ist ein
wenig unkonventionell, ließ sich aber auf Grund der Anforderungen nicht vermeiden.
Java bietet eine direkte Overlay-Vollbild-Unterstützung, allerdings ist es dafür zwingend
notwendig, die Anzeige über allen anderen Fenstern zu platzieren. Da allerdings das Ausgabefenster im Vollbild ein separates Fenster ist, würde dieses immer verdeckt werden.
Aus diesem Grund wird beim Umschalten in den Vollbildmodus das bisher komplett erzeugte Fenster der Anwendung wieder auseinander genommen. Das Fenster wird in eine
rand- und titelleistenlose Darstellung versetzt, jedes Anzeigeelement wird dann neu platziert, bis das gewünschte Aussehen erreicht ist. Wird in den Fenstermodus geschaltet, so
durchläuft das Programm den umgekehrten Weg. Das Fenster wird wieder aufgelöst und
46
10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
KOMPONENTEN
p u b l i c c l a s s GraphHQ e x t e n d s JFrame implements D r a g G e s t u r e L i s t e n e r , D r o p T a r g e t L i s t e n e r
{
p u b l i c GraphHQ ( ) throws E x c e p t i o n { }
p r i v a t e v o i d c r e a t e E l e m e n t s ( ) throws E x c e p t i o n { }
p r i v a t e v o i d c r e a t e C e n t e r P a n e l ( ) throws E x c e p t i o n { }
p r i v a t e v o i d c r e a t e P a n e l s ( i n t ) throws E x c e p t i o n { }
p rivat e void i n s t a l l A p p ( ) { }
void deleteAllFromGraph ( ) { }
void dis ableE ditG r aph ( ) { }
void enableEditGraph ( ) { }
void r e s e t S e l e c t i o n s ( ) { }
void s e t S e l e c t e d A l g o r i t h m ( i n t ) { }
void s e t S t a r t S i t u a t i o n ( ) { }
void s w i t c h F u l l s c r e e n ( ) { }
v o i d s w itch V iew ( i n t ) { }
v o i d u p d a t e T r e e ( ) throws E x c e p t i o n { }
}
Listing 11: Hauptmethoden der Klasse GraphHQ
zum Schluss wie beim ersten Programmstart zusammengesetzt.
Eine Methode, die direkt beim Programmstart aufgerufen wird, ist installApp(). Sie stellt
so etwas wie eine Installationsroutine dar. Es wird in dem Heimatverzeichnis des Nutzers
nach einem Ordner .GraphCalc gesucht. Existiert dieses nicht, wird angenommen, dass
es der erste Start des Programmes ist und der Nutzer wird gefragt, ob er es installieren
will. Bei dieser Installation werden in besagtes Verzeichnis Beispielgraphen für alle Algorithmen installiert. Dieses Verzeichnis dient auch als Standardpfad zum Speichern von
Graphen.
In der Klasse GraphHQ gibt es noch weitere Methoden, die hier nicht näher beschrieben
werden sollen. Dazu gehören auch die Methoden, die die Algorithmen initialisieren und
starten. An dieser Stelle sei auf den Quelltext der Klasse GraphHQ verwiesen.
10.2. GraphPanel
Der Anzeigebereich des Anwendungsfensters, in dem der Graph gezeichnet und angezeigt
werden kann, wird durch die Java-Klasse GraphPanel zur Verfügung gestellt. Einen Überblick über die Klasse soll das Listing 13 auf Seite 50 schaffen. Natürlich enthält die Klasse einen Konstruktor; in diesem Fall GraphPanel(GraphHQ, String). Im vorhergehenden
Abschnitt wurde schon erwähnt, dass alle wichtigen Komponenten der Anwendung ein
Objekt der Klasse GraphHQ besitzen, über das auf alle wichtigen globalen Eigenschaften
zugegriffen werden kann. Der übergebene String identifiziert die Komponente näher.
Der wichtigste Punkt der Klasse GraphPanel ist die Umsetzung der Zeichenfunktionen.
Die gesamte GraphPanel-Komponente - vereinfacht ausgedrückt: der Rahmen des Zeichenbereichs - wird nahezu immer ein Querformat sein, da die Bildschirme meist ein Seitenverhältnis von 4 : 3 (in verhältnismäßig wenigen Fällen ein breiteres) besitzen, außer
der Anwender verkleinert das Anwendungsfenster manuell. Dieser Ausnahmefall ist aber
eher unwahrscheinlich, da das für die Bedienung des Programmes generell ungeeignet
47
10
KOMPONENTEN
ist. Daher wird für die Berechnung des Anzeigebereichs die kleinste Kante als Referenz
genommen, in dem Fall die Höhe. Anhand der Höhe wird auf den Zeichenbereich ein für
den Anwender unsichtbares Gitternetz gelegt. Jedes Quadrat des Gitters stellt eine mögliche Position eines Knotens dar. Bei der Erzeugung des Objekts der Klasse GraphPanel
wird im Konstruktor ein Gittermaß von 29 festgelegt. Bei den Tests mit dem Programm
stellte es sich als eine optimale Größe heraus, sowohl beim normalen Arbeiten, als auch
bei der Präsentation im Vollbildmodus. Wird das Fenster das erste Mal angezeigt oder
in der Größe verändert, führt das zu einer Neuberechnung des Gitters. Die Anzahl der
Höhenpixel wird dabei durch das Gittermaß geteilt, um die maximale Höhe eines Feldes
zu erhalten. Ist der so berechnete Wert n gerade, ergibt sich die Feldhöhe aus n = n − 1.
Das ist notwendig, damit eine optimale Anzeige der Knoten gewährleistet ist. Da es sich
um Kreise handelt und der Mittelpunkt für das Zeichnen von Kanten zwischen zwei Knoten sehr wichtig ist, muss die Feldhöhe immer ungerade sein, um ein exaktes Pixel als
Mittelpunkt ermitteln zu können. Diese Höhe wird auch als Feldbreite benutzt, da es sich
um quadratische Felder, also um „runde Kreise“ handeln soll. Nimmt man die maximale Höhe als Grundlage zur Berechnung des Gitternetzes, führt das bei querformatigen
Bildschirmen dazu, dass horizontal mehr Felder möglich sind. Bei der Berechnung der
Knotenpositionen wird diese Möglichkeit natürlich berücksichtigt.
Führt der Anwender einen Klick mit der linken Maustaste in den Zeichenbereich aus, wird
die Methode mouseClick() aufgerufen. Damit beginnt eine Kette von Entscheidungsprozessen, da das Verhalten dieser Methode von verschiedenen Zuständen des Hauptprogrammes abhängt. Im Folgenden wird nur auf die wichtigsten Entscheidungen eingegangen.
Zunächst erfolgt eine Zuordnung des Mausklicks zu den entsprechenden Gitterkoordinaten. Ist an der ermittelten Stelle ein leeres Feld und kein weiterer Knoten markiert, wird
als erstes mit der Methode lineSelected(Line2D, int, int) überprüft, ob sich eine Kante in
unmittelbarer Nähe befindet. Ist dem nicht so, kann nun ein neuer Knoten platziert werden. Bei der Erzeugung eines neuen Knotens wird eine relative Platzierung des Knotens
im Zeichenbereich gespeichert. Das bedeutet, dass die ermittelte Feldkoordinate in Bezug
zu der maximalen Anzahl der Felder gesetzt wird. Damit wird erreicht, dass die Breite
des Anzeigebereichs flexibel bleiben kann. Bei Veränderung der Fenstergröße verschieben sich die gesetzten Knoten so, dass die relative Position im Fenster nahezu konstant
bleibt.
Wird in dem ermittelten Feld ein bereits existierender Knoten ermittelt, überprüft das Programm, ob zuvor schon ein Knoten markiert wurde. Ist dies geschehen, so wird zwischen
diesen Knoten eine Kante gezogen, wenn nicht schon zwei Kanten zwischen beiden existieren und wenn nicht schon in der selben Richtung eine Kante existiert.
Existiert zwischen beiden Knoten noch keine Kante, wird zwischen beiden Mittelpunkten eine neue Kante gezogen. Die Pfeilspitze wird, falls aktiviert, so berechnet, dass sie
um den Radius eines Knotens versetzt gezeichnet wird, damit sie genau mit dem Knoten
abschließt. Sollte schon eine Kante (im Falle von Digraphen) in entgegengesetzter Rich-
48
10
KOMPONENTEN
tung vorhanden sein, kann die Kante natürlich nicht darüber gelegt werden. Es müssen
nun beide Kanten in entgegengesetzter Richtung verschoben werden. Am Ende liegen
beide Kanten parallel nebeneinander. Auch hier wird die Position von Pfeilspitzen genau
berechnet, allerdings kann hier nicht der Radius der Knoten als Berechnungsgrundlage
dienen, es muss der Abstand mit Winkelfunktionen angepasst werden.
Die Berechnung der Kanten erfolgt in der Methode calcEdge(String, int), die Berechnung
der Pfeilspitzen in der Methode calcArrowHead(Line2D).
Begleitend zu der Berechnung der Kanten und Knoten findet auch die Berechnung der
Beschriftungen statt. In der Methode calcNodeNames() wird eine bestmögliche Position
für die Beschriftung eines Knotens gesucht. Dafür wird zuerst der notwendige Platzbedarf für den Beschriftungstext ermittelt. Daraufhin werden acht Möglichkeiten getestet,
den Text anzuordnen, beginnend mit der Position über dem Knoten, oben rechts über
dem Knoten, rechts neben dem Knoten und so weiter im Uhrzeigersinn. Bei diesem Test
wird überprüft, ob wenigstens eine Textposition nicht mit irgendeiner existierenden Kante konkurriert und eine Überschneidung auftritt. Findet das Programm keine freie Stelle
für die Beschriftung, wird der Abstand zum Knotenmittelpunkt um fünf Pixel erhöht und
alle acht neuen Positionen werden überprüft. Diese Schleife sucht so lange, bis eine freie
Stelle gefunden wurde. Dort wird dann die Knotenbeschriftung gespeichert.
Die Methode calcEdgeNames(int) arbeitet genauso, nur wird hier vom Mittelpunkt einer Kante aus in immer größer werdenden Abständen im Uhrzeigersinn nach einer freien
Stelle für die Kantenbeschriftung gesucht.
Das Zeichnen aller Knoten und Kanten sowie das Färben dieser findet in den Methoden
statt, die mit draw beginnen. Eine wichtiger Punkt bei allen Zeichenoperationen war es,
das Anti-Aliasing von Java durch den in Listing 12 dargestellten Aufruf einzuschalten.
Dadurch konnte die Grafikqualität erheblich verbessert werden.
Ein wichtiger Bestandteil der Klasse GraphPanel sind die Methoden zum Zeichnen des so
1
2
3
G r ap h ics 2 D g2 = ( G r ap h ics 2 D ) g ;
g2 . s e t R e n d e r i n g H i n t
( R e n d e r i n g H i n t s . KEY_ANTIALIASING, R e n d e r i n g H i n t s . VALUE_ANTIALIAS_ON) ;
Listing 12: Anti–Aliasing Funktionsaufruf
genannten Ghostgraphen. Dieser stellt bei einzelnen Algorithmen den Ursprungsgraphen
farblich abgeschwächt dar, unter anderem bei den Algorithmen von Kruskal und Prim.
Der Graph dient dem Betrachter als Unterstützung, um einen Überblick über die nächste mögliche Kante zu geben. Dafür zuständige Methoden sind ebenfalls in die Klasse
GraphPanel implementiert.
49
10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
KOMPONENTEN
p u b l i c c l a s s G r a p h P a n e l e x t e n d s J P a n e l implements P r i n t a b l e
{
p u b l i c G r a p h P a n e l ( GraphHQ , S t r i n g ) { }
p r i v a t e G e n e r a l P a t h calcA r r o w H ead ( Line2D ) { }
p r i v a t e Line2D c a l c E d g e ( S t r i n g , i n t ) { }
p r i v a t e v o i d calcEdgeNames ( i n t ) { }
p rivat e void calcG r id ( ) { }
p r i v a t e v o i d calcNodeNames ( ) { }
p r i v a t e v o i d drawEdges ( G r ap h ics 2 D ) { }
p r i v a t e v o i d d r aw G h o s tE d g es ( G r ap h ics 2 D ) { }
p r i v a t e v o i d drawGhostNod es ( G r ap h ics 2 D ) { }
p r i v a t e v o i d drawNodes ( G r ap h ics 2 D ) { }
p r i v a t e i n t ed g eC o u n tB etw een ( i n t , i n t ) { }
v o i d e n a b l e G h o s t G r a p h ( Graph ) { }
p r i v a t e b o o lea n g r i d C h a n c h e d ( ) { }
p r i v a t e b o o lea n l i n e S e l e c t e d ( Line2D , i n t , i n t ) { }
void load ( ) { }
p r i v a t e v o i d m o u s eC lick ( ) { }
p r i v a t e Line2D moveEdge ( i n t , S t r i n g , i n t ) { }
p r i v a t e v o i d moveNodes ( ) { }
public void paintComponent ( Graphics ) { }
p riv a t e void placeNodes ( ) { }
p u b l i c i n t p r i n t ( G r a p h i c s , PageFormat , i n t ) throws P r i n t e r E x c e p t i o n { }
B u f f e r e d I m a g e makeImage ( ) { }
p rivat e void r e c a l c A l l ( i n t ) { }
p rivat e void r ecalcE dges ( i n t ) { }
p rivat e void s e t G r i d S i z e ( i n t ) { }
void updatePanel ( ) { }
}
Listing 13: Hauptmethoden der Klasse GraphPanel
10.3. Graphenformat
Zur Speicherung der Graphen wurde die Auszeichnungssprache eXtensible Markup Language (kurz XML) verwendet. Markup-Sprachen dienen zur Beschreibung der Form des
Dokumentes. Es wird also festgelegt, wie der Inhalt des Dokumentes interpretiert werden
soll. Des Weiteren wird neben einer einfachen Verarbeitung eine bessere Strukturierung
der Daten erreicht. Anhand des Listings 14, welches einen vollständigen und schlichten
Graphen mit zwei Knoten repräsentiert, soll das XML-Dokument erklärt werden. XMLDokumente bestehen aus XML-Elementen. Ähnlich wie in anderen Auszeichnungssprachen besteht ein XML-Element aus einem Start-Tag, wie z.B. <graph>, gefolgt von dem
Inhalt und/oder weiteren Elementen und einem End-Tag, wie z.B. </graph>. End- und
Start-Tag bestehen dabei aus dem selben Namen mit dem Unterschied, dass vor dem
Namen des End-Tag ein ”/” gesetzt wird. In der ersten Zeile des Listings befindet sich
die XML-Verarbeitungsanweisung (engl.: Processing Instruction), die angibt, dass die
XML-Version 1.0 verwendet wird. In der zweiten Zeile wird eine Dokumententypdefinition (kurz DTD) zu der XML-Datei zugeordnet. Für alle erzeugten Graphen existiert
eine DTD mit dem Namen graph.dtd. Die DTD legt die korrekte Syntax des Dokumentes
fest. Sie besteht aus Entitäten und Attributen. Nähere Erläuterungen zu DTDs sind unter
[DTD] zu finden. Es gibt neben den Verarbeitungsanweisungen noch weitere Regeln für
XML-Dokumente. So muss z.B. das gesamte Dokument in ein Element, das so genann-
50
10
1
2
3
4
5
6
7
8
9
10
11
12
KOMPONENTEN
<? xml v e r s i o n = " 1 . 0 " ?>
<!DOCTYPE g r a p h SYSTEM " g r a p h . d t d " >
<graph d i r e c t e d ="0 ">
< d e s c r i p t i o n > cu s to m g r ap h < / d e s c r i p t i o n >
<nodes >
<node x= " 50 " y= " 86 " i d = " 1 " >P6 < / node >
<node x= " 81 " y= " 65 " i d = " 2 " >P5 < / node >
</ nodes >
< ed g es >
< ed g e from = " 1 " t o = " 2 " i d = " 9 " name= " 1−2" >30 </ edge >
</ ed g es >
</ g r ap h >
Listing 14: Graphenformat–eXtensible Markup Language
te Root-Element, eingeschlossen sein. Weitere Regeln für XML-Dokumente sind unter
[XML] nachzulesen. Das eigentliche XML-Element graph, das Root-Element, beginnt
mit dem Start-Tag in Zeile 3 und endet mit dem End-Tag in der Zeile 12. Innerhalb dieser beiden Tags müssen drei weitere Tags, das description-Tag, das nodes-Tag und das
edges-Tag, vorhanden sein. Im description-Tag können Informationen zu dem Graphen
abgespeichert werden. Die beiden Tags nodes bzw. edges beinhalten alle Knoten bzw.
Kanten des Graphen, die wiederum die Information in node- bzw. edge-Tags speichern.
Zu den Start-Tags eines jeden XML-Elements ist es möglich, Attribute (zusätzliche Informationen) zuzuordnen. Dafür wird innerhalb des Start-Tags der Attributname, gefolgt von
einem Gleichheitszeichen und der Information in Anführungszeichen, eingefügt. Der Tag
graph z.B. besitzt das Attribut directed und als mögliche Information 0 oder 1. Wie später noch gezeigt wird, wird anhand dieses Attributes unterschieden, ob es sich um einen
gerichteten oder ungerichteten Graphen handelt. Die Syntax der Kanten ist so spezifiziert,
dass es, egal ob der Graph gerichtet oder ungerichtet ist, immer einen Start- und Zielknoten für jede Kante gibt. Je nachdem, welchen Wert das Attribut directed annimmt, wird
der Graph unterschiedlich betrachtet. Ein node-Tag hingegen besteht aus drei Attributen:
der x- und der y-Koordinate zum Zeichnen des Knotens und einer id, der Knoten-ID zur
eindeutigen Identifizierung des Knotens. Der Inhalt des node-Tags gibt Auskunft über den
Namen des Knotens. Ein edge-Tag besteht aus vier Attributen: from (ID des Startknoten),
to (ID des Zielknoten), id (ID der Kante) und dem name-Attribut (Name der Kante). Inhalt
des XML-Elements ist das Gewicht der Kante.
51
10
KOMPONENTEN
10.4. GraphParser und GraphWriter
Zur Verarbeitung der Graphen im XML-Format wurde ein Parser in der Java-Klasse
GraphParser implementiert. Die wichtigsten Methoden der Klasse sind im Listing 15
dargestellt.
1
2
3
4
5
6
7
8
9
10
11
public c l a s s GraphParser extends D ef au ltH an d ler {
{
p u b l i c G r a p h P a r s e r ( ) throws E x c e p t i o n { }
p u b l i c G r a p h P a r s e r ( S t r i n g ) throws E x c e p t i o n { }
p r i v a t e v o i d p a r s e ( ) throws E x c e p t i o n { }
p u b l i c v o i d c h a r a c t e r s ( char [ ] , i n t , i n ) { }
p u b l i c v o i d endDocument ( ) throws SAXException { }
p u b l i c v o i d s t a r t E l e m e n t ( S t r i n g , S t r i n g , S t r i n g , o r g . xml . s a x . A t t r i b u t e s ) { }
p u b l i c b o o lea n i s G r a p h D i r e c t e d ( ) { }
p u b l i c Graph g e t _ P a r s e d G r a p h ( ) { }
}
Listing 15: Hauptmethoden der Klasse GraphParser
Zur Verarbeitung der Graphen wird der Konstruktor GraphParser(String) mit dem Namen der zu parsenden Datei aufgerufen und damit ein neues Objekt der Klasse erzeugt.
Zur einfacheren Verarbeitung wurde die Simple API for XML (kurz SAX) verwendet. Der
Parser bietet einen ereignisbasierten Zugriff auf XML-Dokumente. Dazu wird das gesamte Dokument sequentiell durchlaufen und für jedes erkannte XML-Konstrukt, wie z.B. ein
Element oder eine Processing Instruction, eine Callback-Methode aufgerufen. Damit war
es möglich, die einzelnen Ereignisse der XML-Elemente abzufangen und individuell zu
verarbeiten. Der Parser löst beim Lesen die folgenden Ereignisse aus:
• öffnendes Tag,
• Inhalt eines XML-Elements und
• schließendes Tag.
Tritt ein Ereignis auf, so wird der entsprechende Eventhandler aufgerufen. In der Klasse
GraphParser ist der wichtigste Eventhandler die Methode void startElement(...). Mit der
Behandlung der Ereignisse werden die gewünschten Informationen zu dem Graphen aus
der XML-Datei extrahiert und ein neues Graph-Objekt erzeugt.
Da die SAX-Schnittstelle keine Möglichkeit zur Speicherung von XML-Dokumenten ermöglicht, wurde die Klasse GraphWriter, dargestellt im Listing 16, implementiert. Nach
Aufruf des Konstruktors GraphWriter(Graph, String, String, boolean) mit den Parameter Graph, dem Dateinamen, dem Verzeichnis und der Aufforderung zum Überspeichern
einer Datei, falls diese existiert, wird eine neue Datei erzeugt und der Graph im XMLFormat in dieser Datei gespeichert. Zusätzlich wird überprüft, ob sich in dem Verzeichnis, wo der Graph gespeichert wird, die Dokumententypdefinition graph.dtd befindet. Ist
in diesem Verzeichnis diese Datei nicht vorhanden, wird zusätzlich zur Datei mit dem
Graphen die Dokumententypdefinition in dem Verzeichnis abgelegt.
52
10
1
2
3
4
5
6
7
8
9
10
11
KOMPONENTEN
public c l a s s GraphWriter
{
p u b l i c G r a p h W r i t e r ( Graph , S t r i n g , S t r i n g , b o o lea n ) { }
p r i v a t e v o i d i n i t ( S t r i n g , S t r i n g ) throws E x c e p t i o n { }
p r i v a t e v o i d s t a r t D o c u m e n t ( ) throws E x c e p t i o n { }
p r i v a t e v o i d p r i n t D e s c r i p t i o n ( ) throws E x c e p t i o n { }
p r i v a t e v o i d p r i n t N o d e s ( ) throws E x c e p t i o n { }
p r i v a t e v o i d p r i n t E d g e s ( ) throws E x c e p t i o n { }
p r i v a t e v o i d endDocument ( ) throws E x c e p t i o n { }
p r i v a t e v o i d writeDTD ( S t r i n g d i r e c t o r y ) throws E x c e p t i o n { }
}
Listing 16: Hauptmethoden der Klasse GraphWriter
10.5. Convert-Tools
Zur weiteren Verarbeitung der Graphen bzw. zum Importieren der Graphen aus anderen
Formaten besitzt das Programm GraphCalc eine Benutzeroberfläche zum Konvertieren
der einzelnen Formate in das Graph-Format. Nachdem ein Graph ausgewählt wurde, muss
dem Programm mitgeteilt werden, in welchem Format sich dieser befindet. Dabei stehen
die folgenden Ausgangsformate zur Verfügung:
• edge-set (Kantenmenge): In der ersten Zeile des Graphen befindet sich eine 0 für
einen ungerichteten Graphen, oder eine 1 für einen gerichteten Graphen. In der
zweiten Zeile befindet sich die Anzahl der Knoten. In den weiteren Zeilen folgt
jeweils eine Kante. Dabei besteht jede Kante aus zwei Knoten und dem Gewicht der
Kante. Bei gerichteten Graphen ist der erste Knoten der Startknoten und der zweite
der Zielknoten einer Kante. Knoten und Gewicht werden jeweils durch Komma
getrennt.
• neighborhood-lists (Nachbarschaftsliste): Das Format der Nachbarschaftsliste ist
ähnlich dem der Kantenmenge. In der ersten Zeile wird ebenfalls definiert, ob es
sich um einen gerichteten oder ungerichteten Graphen handelt, und in der zweiten Zeile wird die Anzahl der Knoten angegeben. In den folgenden Zeilen wird
jeweils ein Knoten angegeben, gefolgt von allen Nachbarknoten. Auf das Gewicht
der Kanten wird bei Nachbarschaftslisten verzichtet; es wird automatisch auf den
Wert 0 gesetzt.
• start-end-weight: Anders als oben wird auf die Information, ob der Graph gerichtet
oder ungerichtet ist, und die Anzahl der Knoten verzichtet. Es können nur ungerichtete Graphen dargestellt werden. Jede Zeile der einzulesenden Datei repräsentiert
eine Kante mit dem Start- und Endknoten sowie dem Gewicht.
• XML-style: Der Graph wird im Graph-Format eingelesen und in diesem abgespeichert.
53
10
KOMPONENTEN
Listing 17 stellt die wichtigsten Methoden der Klasse CreateGraphFromFile dar.
1
2
3
4
5
6
7
8
9
10
11
12
13
public cl as s CreateGraphFromFile
{
public CreateGraphFromFile ( String , String , in t ) { }
public CreateGraphFromFile ( String , String , String , String
p rivat e void i n i t ( S tr ing , S tr ing , i n t ) { }
p r i v a t e v o i d g r ap h I n p u tF o r m atO N E ( S t r i n g , S t r i n g ) { }
//
p r i v a t e v o i d graphInputFormatTWO ( S t r i n g , S t r i n g ) { }
//
p r i v a t e v o i d graphInputFor matTH REE ( S t r i n g , S t r i n g ) { } / /
p r i v a t e v o i d g r ap h I n p u tF o r m atF O UR ( S t r i n g , S t r i n g ) { } / /
p r i v a t e v o i d s av eG r ap h ( S t r i n g , S t r i n g , b o o lea n ) { }
p u b l i c Graph g e t G r a p h ( ) { }
p r i v a t e v o i d s av eG r ap h ( S t r i n g , S t r i n g , b o o lea n ) { }
}
, int ) { }
edge−s e t
neighborhood− l i s t s
s t a r t −end−w e i g h t
XML− s t y l e
Listing 17: Hauptmethoden der Klasse CreateGraphFromFile
Zum Erzeugen eines neuen Objektes vom Typ CreateGraphFromFile muss einer der beiden Konstruktoren aufgerufen werden. Dabei wird entweder die Quelldatei, das Verzeichnis der Quelldatei und das Eingabeformat oder die Quelldatei, das Verzeichnis der Quelldatei, die Outputdatei, das Verzeichnis der Outputdatei und das Eingabeformat übergeben. In der init()-Methode wird die Wahl des Inputformates ausgewertet und je nachdem,
welches Format gewählt wurde, die entsprechende Methode zur Verarbeitung aufgerufen. Dabei werden die Quelldatei und das Quellverzeichnis übergeben. Die angegebenen
Kommentare in den Zeilen 6 bis 9 geben an, welche Methode für welches Inputformat aufgerufen werden muss. Nach erfolgreichem Umwandeln des Graphen wird dieser mittels
der Methode saveGraph() gespeichert, dabei wird ein neues Objekt vom Typ GraphWriter
erzeugt und diesem die nötigen Parameter übergeben.
54
10
KOMPONENTEN
10.6. Console
Für die Verarbeitung von Graphen ist nicht immer die grafische Darstellung nötig. Genügt
es, einen Algorithmus auf einen Graphen ohne visuelle Auswertung durchzuführen, so
kann die in GraphCalc implementierte Console verwendet werden.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
p u b l i c c l a s s C o n s o l e e x t e n d s JFrame implements
ActionListener , DragGestureListener , DropTargetListener
{
public Console ( )
p rivat e void p a r s e S t r i n g ( S t r i n g ) { }
p rivat e void help ( ) { }
p riv a t e void p r in tG r ap h ( Vector ) { }
p r i v a t e Graph openGraph ( S t r i n g ) { }
p riv a t e void d an tzig ( Vector ) { }
p r i v a t e v o i d p r im ( V e c t o r ) { }
p riv a t e void h i e r h o l z e r ( Vector ) { }
p riv a t e void k r u s k al ( Vector ) { }
p riv a t e void c h r i s t o f i d e s ( Vector ) { }
p riv a t e void f o r d f u l k e r s o n ( Vector ) { }
p riv a t e void ungar ( Vector ) { }
p r i v a t e i n t getNodeID ( Graph , S t r i n g ) { }
p r i v a t e v o i d a d d T e x t T o T e x t A r e a ( S t r i n g , S t r i n g , b o o lea n ) { }
public void dragGestureRecognized ( DragGestureEvent ) { }
public void actionP er f or m ed ( ActionEvent ) { }
}
Listing 18: Hauptmethoden der Klasse Console
Der im Listing 18 dargestellte Konstruktor erzeugt ein grafisches Fenster. Dieses Fenster besteht aus einer JTextArea für die Ausgabe von Informationen und einem JTextFiled
für die Eingabe von Ausführungsanweisungen. Diese beiden Elemente werden auf einem
JPanel mittels dem BorderLayout von Java angeordnet. Nach der Eingabe von Anweisungen in das Textfeld und dem Drücken der Enter-Taste wird automatisch die Methode
actionPerformed() aufgerufen. Diese wiederum ruft die Methode parseString() auf und
übergibt den zu bearbeitenden String, welcher aus dem Textfeld ausgelesen wurde. Es ist
sehr wichtig, dass für jeden Algorithmus das Eingabeformat beachtet wird, da es nur so zu
einem erfolgreichen Parsen des Strings kommt. Nachdem der String zerlegt wurde, wird
der jeweilige Algorithmus aufgerufen und die nötigen Parameter übergeben. Eine Liste
aller unterstützten Funktionen erhält man nach der Eingabe von help in dem Textfeld.
55
10
KOMPONENTEN
10.7. GraphMenu
Das Menü des Programmes GraphCalc wird in der Klasse GraphMenu (Listing 19) erzeugt.
1
2
3
4
5
6
7
8
9
10
11
p u b l i c c l a s s GraphMenu e x t e n d s JMenuBar implements A c t i o n L i s t e n e r
{
p u b l i c GraphMenu ( GraphHQ ) { }
public void actionP er f or m ed ( ActionEvent ) { }
p riv a t e void createMenu ( ) { }
p r i v a t e v o i d s av eG r ap h ( ) { }
p riv a t e void setAlgorthmSpeed ( i n t ) { }
p riv a t e void openConvertTool ( ) { }
b o o lea n o p e n G r a p h F r o m F i l e ( S t r i n g ) { }
v o i d s e t E d i t G r a p h ( b o o lea n ) { }
}
Listing 19: Hauptmethoden der Klasse GraphMenu
Mittels Aufruf des Konstruktors und mit der Übergabe eines Zeigers auf das
GraphHQ-Objekt wird die Methode createMenu() aufgerufen und das Menü erzeugt.
Es wird ein JMenu-Objekt erzeugt, auf dem die einzelnen Menüpunkte angeordnet werden. Den Menüpunkten werden entweder Items oder weitere Menüpunkte zugeordnet.
Nach der Auswahl eines Menüpunktes durch den Nutzer wird automatisch die Methode
actionPerformed() aufgerufen und damit die Auswertung des Ereignisses und die weitere
Verarbeitung vorgenommen.
10.8. GraphPrint
Eine weitere nützliche Funktion von GraphCalc ist die Druckfunktion. Die wichtigsten
Methoden der Klasse GraphPrint sind im Listing 20 dargestellt.
1
2
3
4
5
6
7
8
9
10
11
p u b l i c c l a s s G r a p h P r i n t implements A c t i o n L i s t e n e r
{
p u b l i c G r a p h P r i n t ( GraphHQ ) { }
p rivat e void createFrame ( ) { }
p rivat e void pr intG r aph ( P r i n t e r J o b pj ) { }
p r i v a t e v o i d p r i n t ( Image , boolean , boolean , boolean , b o o lea n ) { }
p rivat e void pr intG r aph ( . . . ) { }
p r i v a t e v o i d printGraphXML ( . . . ) { }
p rivat e void pr intN eighbour hoodL is t ( . . . ) { }
p rivat e void pr intCover ( . . . ) { }
}
Listing 20: Hauptmethoden der Klasse GraphPrint
Nachdem ein neues GraphPrint-Objekt mittels Aufruf des Konstruktors erzeugt wurde,
wird automatisch die Methode createFrame aufgerufen. Diese Methode dient zum Erzeugen des Fensters zum Anwählen der gewünschten Druckoption. Auf einem JDialogObjekt werden die Checkbuttons und Buttons platziert. Es besteht die Möglichkeit, zwischen folgenden Elementen zu wählen und diese beliebig zu kombinieren:
56
10
KOMPONENTEN
• cover sheet: Ermöglicht das Drucken eines Deckblattes.
• print graph: Ermöglicht das Drucken des Graphen. Dabei ist zu beachten, dass der
Graph nur im Hochformat gedruckt werden kann.
• print Graph as xml: Ermöglicht das Drucken des Graphen im XML-Format.
• print Graph as neighbours: Ermöglicht das Drucken des Graphen als Nachbarschaftsliste.
Durch Bestätigen des Druckes mittels des Buttons print wird ermittelt, welches die gewünschten Druckoptionen sind und diese in den entsprechenden Methoden verarbeitet.
Die abstrakte Klasse PrinterJob, welche als Parameter übergeben wird, repräsentiert einen
Druckauftrag und ist Ausgangspunkt für alle Druckaktivitäten. Sie besitzt die statische
Methode getPrinterJob, mit der Instanzen eines Druckauftrages erzeugt und globale Eigenschaften des Druckauftrages kontrolliert werden können. Neben den eigentlichen
Druckdaten besitzt eine Seite Eigenschaften wie Papiergröße und Randeinstellungen. Für
die Darstellung der Druckdaten auf den Seiten war es nötig, die entsprechenden Methoden mittels des Graphics-Objektes aufzurufen. Es werden also alle nötigen Informationen
in einem Image gespeichert und an den Drucker geschickt.
10.9. Weitere Klassen
Für GraphCalc existieren eine Vielzahl weiterer Klassen. Im Nachfolgenden ist eine Auflistung ausgewählter Klassen mit dazugehöriger kurzer Beschreibung.
• GraphTreePanel: Die Klasse GraphTreePanel erzeugt ein JTree-Objekt im Programm. Innerhalb dieses ”Baumes” können alle Informationen zum Graphen abgerufen werden. Mittels Doppelklick auf Einträge im dargestellten Baum werden
diese in einem gesonderten JFrame-Objekt dargestellt und können modifiziert werden.
• GraphNaviPanel: Das GraphNaviPanel-Objekt erzeugt alle nötigen Elemente zur
Ablaufsteuerung des Algorithmus. Die Elemente der Ablaufsteuerung sind: Starten
und Anhalten des Algorithmus, einen Schritt nach vorn bzw. zurück und an den
Anfang bzw. das Ende des Algorithmus springen. Sollten einzelne Navigationselemente nicht anwählbar sein, so werden diese deaktiviert.
• GraphEditGUI: Die Klasse GraphEditGUI repräsentiert eine Benutzeroberfläche
zur Bearbeitung eines Graphen. Es werden alle Elemente des Graphen dargestellt
und es besteht die Möglichkeit zur Modifizierung des Graphen.
57
10
KOMPONENTEN
• GraphPopupMenu: Die Klasse GraphPopupMenu repräsentiert ein Kontextmenü
im GraphTreePanel. Wird innerhalb des ”Baumes” ein Rechtsklick auf einen Knoten oder eine Kante ausgeführt, so kann das jeweilige Element aus dem Graphen
entfernt werden.
• ErrorGUI: Die Klasse ErrorGUI erzeugt ein JFrame-Objekt mit der Möglichkeit
zur Darstellung von Fehlermeldungen.
• Edge: Ein Edge-Objekt repräsentiert eine Kante in einem Graphen. Es werden die
folgenden Informationen gespeichert: Kanten-ID, Kantengewicht, Kantenname,
Startknoten und Zielknoten. Weiterhin werden Methoden zum Abfragen bzw. Bearbeiten der Informationen bereitgestellt.
• Node: Die Implementierung der Klasse Node erfolgt analog zur Klasse Edge.
58
11
GRAPHCALC ANWENDUNGSFÄLLE
11. GraphCalc Anwendungsfälle
Die nachfolgenden Abschnitte repräsentieren ausgewählte Anwendungsfälle für das Programm GraphCalc.
Zur einfacheren Darstellung der einzelnen Prozesse wird die UML-Notation, insbesondere Anwendungsfalldiagramme (engl.: use cases), verwendet. Dabei werden Beziehungen und Prozesse zwischen Anwendungsfällen untereinander, zu beteiligten Personen
und/oder Prozessen beschrieben.
11.1. Installation
Abbildung 29: Anwendungsfalldiagramm Installation
Bevor das Programm GraphCalc auf dem Rechner läuft, muss es installiert werden. Herkömmliche (Windows-) Programme werden häufig mit einem speziellen Installationsprogramm, z.B. dem InstallShield ausgeliefert. Etwas anders sieht das bei Java-Programmen
aus. Die Plattformunabhängigkeit von Java-Programmen und deren Installation erfordert
die so genannte Java-Laufzeitumgebung. Die Applikation, in diesem Fall das Programm
GraphCalc, läuft innerhalb der Virtuellen-Maschine (VM). Das Programm wird mittels
Webstart installiert und gestartet. Die Webstart-Technologie wurde auf der JavaOne 2000
erstmals präsentiert. Die Technologie umfasst die Bereiche Installation, Start und Update
der Software, geregelt durch das Java Network Launcher Protocol (JNLP). Beim ersten
Start der Datei GraphCalc.jnlp, dargestellt im Listing 21, wird überprüft, ob die installierte Laufzeitumgebung den erforderlichen Bedingungen des Programmes genügt (es ist
59
11
GRAPHCALC ANWENDUNGSFÄLLE
eine Java-Version ab 1.5 nötig) und ein Homeverzeichnis mit Beispielgraphen, sowie Verknüpfungen zum Starten des Programmes wird angelegt.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<? xml v e r s i o n = " 1 . 0 " e n c o d i n g = " u t f −8" ?>
< j n l p s p e c = " 1 . 0 + " c o d e b a s e = " . . . " h r e f = " G r ap h C alc . j n l p " >
<information >
< t i t l e > GraphCalc < / t i t l e >
< v en d o r > R o b e r t J o c k w i t z and A n d r eas G r o l l < / v en d o r >
<homepage h r e f = " i n d e x . h t m l " / >
< d e s c r i p t i o n > s i m u l a t e s s e v e r a l graph t h e o r e t i c a l alg o r ith m s </ d e s c r i p t i o n >
< o f f l i n e −a l l o w e d / >
< s h o r t c u t online =" f a l s e ">
<desktop / >
<menu submenu= " G r ap h C alc " / >
</ s h o r t c u t >
</ i n f o r m a t i o n >
<resources >
< j 2 s e v e r s i o n =" 1 . 5 1.4∗+ " / >
< j a r h r e f = " G r ap h C alc . j a r " main = " t r u e " / >
</ r e s o u r c e s >
< a p p l i c a t i o n −d e s c main−c l a s s = " G r ap h C alc " / >
<security >
< a l l −p e r m i s s i o n s / >
</ s e c u r i t y >
</ j n l p >
Listing 21: Java–Webstart Ressource
Das Starten des Programmes GraphCalc erfolgt über die erstellten Verknüpfungen.
11.2. Durchführen eines Algorithmus
Der Nutzer des Programmes GraphCalc kann durch Betätigen des start-Button (dargestellt mit einem roten Punkt) auf dem NaviPanel die Durchführung eines Algorithmus,
auf dem im Anzeigebereich dargestellten Graphen, starten. Hierfür öffnet sich ein modaler Dialog mit allen zur Verfügung stehenden Algorithmen. Abhängig vom jeweiligen
Algorithmus wird der Nutzer aufgefordert, weitere Informationen für die Durchführung
des Algorithmus einzugeben. Nach der Eingabe aller erforderlichen Informationen wird,
für den Nutzer nicht erkennbar, der Algorithmus für den dargestellten Graphen abgearbeitet. Für jeden Schritt wird ein neues Objekt vom Typ Graph erzeugt, an das GraphPanel
geschickt und dargestellt. Über das Navipanel hat der Nutzer die Möglichkeit, die visuelle Darstellung zu beeinflussen und die schrittweise Anzeige des Algorithmus zu beenden.
Die Anzeigegeschwindigkeit, die Beschriftung der Kanten und Knoten, sowie der Wechsel in den Fullscreen-Modus erfolgt über die jeweiligen Menüpunkte.
60
11
GRAPHCALC ANWENDUNGSFÄLLE
11.3. Konvertieren von Graphen
Die Konvertierung von Graphen in das XML-Format erfolgt über ein im Programm
GraphCalc implementiertes eigenständiges Tool, welches über den Menüpunkt
Abbildung 30: Anwendungsfalldiagramm Konvertierung von Graphen
Graph - convert Graph gestartet wird. Der Nutzer muss nun die zu konvertierende Datei öffnen, das Ausgangsformat festlegen und den Graphen speichern.
61
A
DATENTRÄGER
Teil IV.
Anhang
A. Datenträger
Der beiliegende Datenträger enthält:
• Bachelorarbeit im PDF-Format (VisGraphAlg.pdf)
• Bachelorarbeit im PDF-Format inklusive des Programmes „GraphCalc“ als
Dateianhang (VisGraphAlg-Dist.pdf)
• Das Programm „GraphCalc“ (GraphCalc.jar)
• Die Programmquellen
(GraphCalc-source.zip)
zum
Programm
„GraphCalc“
als
ZIP-Datei
• Das CVS-Repository als ZIP-Datei - enthält alle Änderungen im Programm seit
Beginn der Entwicklung (GraphCalc-CVS.zip)
• Die TeX-Quellen dieses Dokuments als ZIP-Datei (TeX-Source.zip)
Haftungsausschluss
Die Nutzung der bestehenden Software erfolgt ausdrücklich auf eigene Gefahr und ohne
Garantie durch die Autoren. Die Autoren können in keinem Fall für Schäden, die durch
die Nutzung dieser Software entstehen könnten, verantwortlich gemacht werden.
62
A BBILDUNGSVERZEICHNIS
Abbildungsverzeichnis
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
Die vollständigen Graphen mit bis zu fünf Knoten . . . . . . . . . . . . .
Weg mit 6 Knoten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Kreis mit 6 Knoten . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Ein bipartiter Graph . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Bäume mit 4 Knoten . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Gesättigtes Matching . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Maximales/perfektes Matching . . . . . . . . . . . . . . . . . . . . . . .
Ein zusammenhängender Graph . . . . . . . . . . . . . . . . . . . . . .
Ablauf des Kruskal-Algorithmus . . . . . . . . . . . . . . . . . . . . . .
Ablauf des Prim-Algorithmus . . . . . . . . . . . . . . . . . . . . . . . .
Der kürzeste Weg von A nach C . . . . . . . . . . . . . . . . . . . . . .
Keine Korrektheit des Algorithmus von Dantzig/Dijkstra bei negativen
Kantengewichten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Das Königsberger Brückenproblem . . . . . . . . . . . . . . . . . . . . .
Ein EULERscher Graph, dargestellt mit kantendisjunkten Kreisen . . . .
Hierholzer, Konstruieren eines geschlossenen Kantenzuges . . . . . . . .
Ein Graph mit einem Hamiltonkreis [Tit03] . . . . . . . . . . . . . . . .
Christofides, Minimalgerüst mit Knoten ungeraden Grades . . . . . . . .
Christofides, perfektes Matching minimalen Gewichts . . . . . . . . . . .
Christofides, Minimalgerüst und die Kanten des perfekten Matchings minimalen Gewichts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Christofides, HAMILTON-Kreis . . . . . . . . . . . . . . . . . . . . . .
Beispiel Maximalstromproblem . . . . . . . . . . . . . . . . . . . . . .
Beispiel nichtpolynomieller Laufzeit . . . . . . . . . . . . . . . . . . . .
Beispiel Ford-Fulkerson-Algorithmus . . . . . . . . . . . . . . . . . . .
Beispiel 1 Ungarische Methode . . . . . . . . . . . . . . . . . . . . . . .
Beispiel 2 Ungarische Methode . . . . . . . . . . . . . . . . . . . . . . .
Beispiel 3 Ungarische Methode . . . . . . . . . . . . . . . . . . . . . . .
Klassendiagramm des Packages GraphVis . . . . . . . . . . . . . . . . .
Klassendiagramm des gesamten Programmes GraphCalc . . . . . . . . .
Anwendungsfalldiagramm Installation . . . . . . . . . . . . . . . . . . .
Anwendungsfalldiagramm Konvertierung von Graphen . . . . . . . . . .
63
3
4
4
4
5
5
5
7
11
14
15
17
21
22
23
26
28
28
29
29
32
34
37
42
43
43
44
45
59
61
L ISTINGS
Listings
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
Hauptmethoden der Klasse Kruskal . . . . . . . . . . . . . . . . . . . .
Hauptmethoden der Klasse Prim . . . . . . . . . . . . . . . . . . . . . .
Hauptmethoden der Klasse Dantzig . . . . . . . . . . . . . . . . . . . .
Hauptmethoden der Klasse Hierholzer . . . . . . . . . . . . . . . . . . .
Auszug aus der Methode createOneCircle(Vector) aus der Klasse Hierholzer
Hauptmethoden der Klasse Christofides . . . . . . . . . . . . . . . . . .
Christofides, Erzeugen der Distanzmatrix zur Überprüfung der Dreiecksungleichung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Christofides, Überprüfen der Dreiecksungleichung anhand der Distanzmatrix . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Hauptmethoden der Klasse FordFulkerson . . . . . . . . . . . . . . . . .
Hauptmethoden der Klasse Ungar . . . . . . . . . . . . . . . . . . . . .
Hauptmethoden der Klasse GraphHQ . . . . . . . . . . . . . . . . . . .
Anti–Aliasing Funktionsaufruf . . . . . . . . . . . . . . . . . . . . . . .
Hauptmethoden der Klasse GraphPanel . . . . . . . . . . . . . . . . . .
Graphenformat–eXtensible Markup Language . . . . . . . . . . . . . . .
Hauptmethoden der Klasse GraphParser . . . . . . . . . . . . . . . . . .
Hauptmethoden der Klasse GraphWriter . . . . . . . . . . . . . . . . . .
Hauptmethoden der Klasse CreateGraphFromFile . . . . . . . . . . . . .
Hauptmethoden der Klasse Console . . . . . . . . . . . . . . . . . . . .
Hauptmethoden der Klasse GraphMenu . . . . . . . . . . . . . . . . . .
Hauptmethoden der Klasse GraphPrint . . . . . . . . . . . . . . . . . . .
Java–Webstart Ressource . . . . . . . . . . . . . . . . . . . . . . . . . .
64
9
13
19
24
25
29
30
30
35
40
47
49
50
51
52
53
54
55
56
56
60
L ITERATUR
Literatur
[Boe04] Ferdinand Börner, Graphen, Algorithmen und Anwendungen beim Schaltkreisentwurf, vorlesungbegleitendes Material, Universität Potsdam (Institut für Informatik), 2004
[Boo00] Barry Boone, William Stanek, Java 2 - all in one, McGraw-Hill Companies,
2000
[Bra94] Andreas Brandstädt, Graphen und Algorithmen, Teubner-Verlag Stuttgart, 1994
[Cla94] John Clark, Graphentheorie - Grundlagen und Anwendungen, Akademischer
Verlag Spektrum, 1994
[Cor03] Thomas H. Cormen, Introduction to algorithms, MIT Press, 2003
[DTD] http://www.payer.de/xml/xml03.htm
[Edm65] J. Edmonds, Paths, trees, and flowers, Canad. J. Math. 17, S. 449-467, 1965
[Edm72] J. Edmonds and R. M. Karp, Theoretical improvements in algorithmic efficiency for network flow problems, Journal of the Association for Computing Machinery, 19 (1972), 248-264
[Eul36] Solutio problematis ad geometriam situs pertinentis, Commentarii Academiae
Scientiarum Imperialis Petropolitanae 8, 1736
[Fels]
S. Felsner, http://page.inf.fu-berlin.de/~felsner/Paper/vdm99.ps.gz
[FFJ56] L. Ford and D. Fulkerson, Maximal Flow Through a Network, Canadian Journal
of Mathematics 8 (1956), S. 399
[FFJ62] L. Ford and D. Fulkerson, Flows in Networks, Princeton University Press., 1962
[Groe] Martin Grötschel, www.zib.de/groetschel/teaching/skript1-9.ps
[Gue99] R. H. Güting, Kurs Datenstrukturen (01663), Fernuniversität Hagen, 1999
[Hop73] J. E. Hopcroft and R. M. Karp, A n5/2 Algorithm for Maximum Matchings in
Bipartite Graphs, SIAM Journal on Computing, 2 (1973), S. 225-231
[Jar30] V. Jarník, O jistem problemu minimalnim, Praca Moravske Prirodovedecke Spolecnosti, 6 (1930), S. 57-63
[Kemn] A. Kemnitz, http://www.mathematik.tu-bs.de/dm/mitarb/gta4.ps
[Koh04] Anja Kohl, Übung: Algorithmische Graphentheorie, TU Bergakademie Freiberg, 2004
65
L ITERATUR
[Kru65] J. B. Kruskal, On the shortest spanning subtree and the traveling salesman problem, Proceedings of the American Mathematical Society, 7 (1956), S. 48-50
[Kuh55] H. W. Kuhn, The Hungarian Method for the Assignment Problem, Naval Research Logistics Quarterly, 2 (1955), S. 83-97
[Lov86] L. Lovász and M. Plummer, Matching Theory, Annals of Discrete Mathematics,
North-Holland, 29 (1986), S. 47
[Mun57] J. Munkres, Algorithms for the assignment and transportation problems, J. Soc.
Indust. Appl. Math., 5 (1957), S. 32-38
[Nie04] Elke Niedermair, Michael Niedermair, LATEX Praxisbuch, Franzis Verlag
GmbH, 2004
[Pri57] R. C. Prim, Shortest connection networks and some generalisations, Bell System
Technical Journal, 36 (1957), S. 1389-1401
[Proe]
H. J. Prömel, http://www.informatik.hu-berlin.de/Institut/struktur/algorithmen/ga/
[Sac70] Horst Sachs, Einführung in die Graphentheorie der endlichen Graphen, BSB
B.G. Teubner Verlagsgesellschaft, 1970
[Sch00] Brit Schröter, Kompaktreferenz Java 2, DATA BECKER GmbH & Co. KG, 2000
[Sch01] Herbert Schildt, Java 2 ENT-PACKT, MITP-Verlag GmbH, 2001
[Schw] D. Schweigert, http://kluedo.ub.uni-kl.de/Mathematik/Quellen/
[Son04] Martin Sonntag, Vorlesung: Algorithmische Graphentheorie, TU Bergakademie
Freiberg, 2004
[Tit03] Peter Tittmann, Graphentheorie - Eine anwendungsorientierte Einführung, Fachbuchverlag Leipzig, 2003
[Vol91] Lutz Volkmann, Graphen und Digraphen: eine Einführung in die Graphentheorie, Wien; New York: Springer, 1991
[Vorn]
O. Vornberger, http://www-lehre.informatik.uni-osnabrueck.de/~graph/skript/
[Wal87] Hansjoachim Walther, Günter Nägler, Graphen Algorithmen Programme, VEB
Fachbuchverlag Leipzig, 1987
[WP_1] Wikipedia, http://en.wikipedia.org/wiki/Prim%27s_algorithm
[WP_2] Wikipedia, http://de.wikipedia.org/wiki/Graphentheorie
[WP_3] Wikipedia, http://de.wikipedia.org/wiki/Briefträgerproblem
66
L ITERATUR
[WP_4] Wikipedia, http://de.wikipedia.org/wiki/Eulerkreisproblem
[WP_5] Wikipedia, http://de.wikipedia.org/wiki/Hamiltonkreis-Problem
[XML] http://de.selfhtml.org/xml/intro.htm
[Zil03] Thorsten Zilm, Das Einsteigerbuch - Latex, Verlag Moderne Industrie Buch AG
& Co. KG Landsberg, 2003
67
Herunterladen