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