• Keine Ergebnisse gefunden

Der Name der Hauptfunktion ist aus oben genannten Gründen LR_PLANAR().

Wie im Pseudocode dient die Hauptfunktion zum Aufrufen der Funktio-nen für die einzelFunktio-nen Phasen25, zum Richten der Kanten26 und zum Sor-tieren der Adjazenzlisten nach der Verschachtelungsordnung 27. Die Aufru-fe der Observerfunktionen werden in Abschnitt 5.2.1 behandelt. Wenn man LR_PLANAR() ohne einen Observer zu übergeben aufruft, werden die ent-sprechenden Aufrufe nicht ausgeführt, damit LR_PLANAR() wie die oben erwähnten anderen Planaritätstestimplementierungen von LEDA verwend-bar ist28.

Zuerst wird eine Instanz der der Struktur gblvars erstellt und initialisiert, wel-che die global benötigten Variablen enthält. Am Ende der Funktion werden mit free_gblvars(gblvars& gbl) die in der Struktur vorhandenen Knoten-und Kantenlisten geleert. Es ist wichtig, dass dies gemacht wird, solange der Graph, zu dem die Knoten und Kanten in den Listen gehören, noch existiert.

25jeweils für jede Wurzel, falls der Graph mehrere Zusammenhangskomponenten hat

26nach Phase 1

27jeweils nach Phase 1 und Phase 2

28Wenn Laufzeittests durchgeführt werden, sollten die Observeraufrufe auskommentiert oder die entsprechenden if-Abfragen durch Präprozessordirektiven ersetzt werden, damit zur Laufzeit nicht jedes Mal überprüft werden muss, ob es einen Observer gibt. In dieser Implementierung wird dies zur Laufzeit geprüft, damit die Funktion möglichst allgemein bleibt.

Versucht man eine Liste zu leeren, deren Knoten oder Kanten zu einem nicht mehr existenten Graphen gehören, führt das zu einem schwer aundbaren Laufzeitfehler.

Um Mehrfachkanten und Loops zu entfernen muss der Graph mit

G.make_undirected() ungerichtet gemacht werden, damit danach beim Auf-ruf von Make_Simple(G)29, falls zwei Kanten (v, w), (w, v) ∈ E zwischen zwei Knotenv, w∈V existieren, eine der beiden gelöscht wird. Danach wird der Graph mittels G.make_directed() wieder gerichtet gemacht, damit in Phase 1 eine Tiefensuchorientierung aufgebaut werden kann. Die beiden zu-sätzlichen Funktionsaufrufe vor und nach dem Aufruf der Einbettungsfunk-tion, werden im Abschnitt zur Einbettungsphase näher erläutert. Wie man im Proler-Ergebnis30 sieht, verbraucht die Sortierung der Adjazenzlisten re-lativ viel Laufzeit. Auch die Zeit für leda::node_struct::del_adj_edge (hauptsächlich) und für leda::graph::sort_edges gehören zu Bucketsort.

bucket_sort_edges fügt alle Kanten des Graphen einer einzigen Liste hin-zu. Diese wird dann mit Bucketsort sortiert. Dann werden die Adjazenzlisten aller Knoten gelöscht, weswegen auch auch die von del_adj_edge benötigte Zeit hauptsächlich bucket_sort_edges zuzuordnen ist. sort_edgesfügt da-nach die Kanten aus der sortierten Liste in die entsprechende Adjazenzliste ein. In dieser Implementierung wurde jedoch darauf verzichtet eine eigene Bucketsortmethode zu erstellen31, da erstens die Lösung alle Kanten in ei-ner Liste zu sortieren und danach wieder auf die einzelnen Adjazenzlisten aufzuteilen schneller ist, als jede Adjazenzliste einzeln zu sortieren. Zweitens wird Anzahl der Buckets durch LEDA auf maximal 2n+ 1, n= #V einge-schränkt, da das kleinste und gröÿte Element des übergebenen Kantenarrays (edge_array < int >) bestimmt wird und nur Buckets für die beiden Grenz-fälle und die dazwischenliegenden Ganzzahlen erstellt werden. Somit ist die Bucketgröÿe minimal, sodass an dieser Stelle nicht optimiert werden kann.

4.3 Phase 1 - Orientierung

Die Schleife, in der im Pseudocode alle noch nicht orientierten, von einem Knotenvausgehenden Kanten durchlaufen werden, wird in der Implementie-rung anders ausgedrückt. Das Iterationsmakro forall_inout_edges(vw,v) wird verwendet um über alle Kanten(v, w), die von einem Knoten v ausgehen zu iterieren. Innerhalb dieser Iterationsschleife muss getestet werden, ob die Kante bereits orientiert wurde. Algorithmus 6 veranschaulicht den Aufbau

29macht einen Graphen einfach

30s. Anhang)

31Für diesen Zweck; für die layout()-Funktion wurde eine eigene Bucketsortmethode geschrieben. Warum dies nötig war, wird weiter unten erklärt.

der ersten Tiefensuche der LEDA-Implementation. Die Höhe wurde in der Implementierung mit −1 initialisiert.

Algorithm 6: DFS1 - Struktur der LEDA-Implementierung Kantenliste U

dfs1_impl(Knotenv) begin Knotenw

forall_inout_edges(v, vw) begin w← opposite(v, vw);

if height[w]<0 then //Baumkante

if v ist nicht der Startknoten von vw then füge (vw) zuU hinzu (Orientierung) endElternkante, Lowpoints und Nestingorder

endelse if height[w]< height[v] and vw6=parentedge[v] then //Rückwärtskante

if v ist nicht der Startknoten von vw then füge (vw) zuU hinzu (Orientierung) endLowpoints und Nestingorder

end end end

Was bei der Orientierungstiefensuche beachtet werden muss und bereits in Algorithmus 6 angedeutet wurde ist, dass die zu orientierenden Kanten nicht direkt wie im Pseudocode orientiert werden, sondern einer Kantenliste mit umzuorientierenden Kanten hinzugefügt wird. Die Richtung, der sich in dieser Liste bendlichen Kanten, wird in der Hauptfunktion (LR_PLANAR()) nach der Orientierungstiefensuche umgekehrt. Dies ist deswegen nötig, weil das Ite-rationsmakro forall_inout_edges(vw,v) nicht erlaubt, dass sich während des Durchlaufs die Adjazenzlisten von v verändern. Durch die Richtungsän-derung einer Kante vw, die v enthält, würden sich die Adjazenzlisten von v insofern ändern, als dass vw entweder aus der Adjazenzliste der eingehen-den Kanten gelöscht wird und in die Adjazenzliste der ausgeheneingehen-den Kanten eingefügt wird, oder umgekehrt.

4.4 Phase 2 - Planaritätstest

Die Struktur der Blöcke des Pseudocodes unterscheidet sich auch in Phase 2 von der der Implementierung32; jedoch nur in einer Kleinigkeit. In der Imple-mentierung werden die Wurzelknoten getrennt behandelt um für jede Wurzel ein Vergleich zu sparen. Im Pseudocode werden sie wie alle anderen Knoten behandelt, damit der Code kompakter bleibt. Es wird für die Wurzelkno-ten deswegen zusätzlich geprüft, ob sie RückkehrkanWurzelkno-ten haben, was bei zur Wurzel adjazenten, ausgehenden Kanten nie der Fall sein kann. Da der Un-terschied nur bei Wurzeln auftritt ist der LaufzeitunUn-terschied nicht messbar.

Er wurde nur der Vollständigkeit halber hier aufgeführt.

Für den in dieser Phase verwendeten Stack wurde nicht der nahe liegende LEDA-Datentyp stack < E > verwendet sondern eine Liste. Dies wurde deshalb gemacht, weil für die Animation in jedem Schritt der ganze Stack angezeigt werden soll. Bei einem Stack wäre dies nur möglich, indem man ihn komplett abbaut, den Inhalt zwischenspeichert und ihn dann wieder aufbaut.

Es wäre möglich gewesen, es so auf diese Art und Weise zu lösen mit der Be-gründung, dass die Funktion LR_PLANAR() so schnell wie möglich laufen soll und vom Observer so wenig wie möglich beeinusst werden soll. In dieser Im-plementierung wurde die Möglichkeit mit der Liste gewählt, da sie bequemer ist und der Observer so oder so die Funktion LR_PLANAR() beeinusst und es deswegen eine Version ohne Observer gibt, die möglichst schnell laufen soll.

Diese Version liegt ebenfalls auf der CD bei. Der Stack ist etwas schneller, weil er durch eine einfach verkettete Liste repräsentiert wird.list < E >wird durch eine zweifach verkettete Liste repräsentiert. So spart man sich durch die Verwendung von stack < E > zusätzliche Pointer.

Für die auf dem Stack liegenden Intervallpaare wurden zwei eigene Datenty-pen s_interval und pair eingeführt. s_interval wurde als Struct, pair als Klasse realisiert. Wegen der kürzeren Schreibweise wurde der Typ interval deniert, welcher gleichbedeutend mit struct s_interval ist. s_interval hat zwei Elemente low und high vom Typ edge. Ferner besitzt es einen Copyconstructor und eine Methode empty(). Sie gibt true zurück, falls low leer ist. Das spart einen Vergleich, da ein Intervall mit einem Element kei-nen Sinn macht33. Die Klasse pair hat zwei Member L und R vom Typ interval. Sie besitzt einen Copyconstructor und einen Initialisierungskon-struktor, welchem die Kanten, die in den Intervallen gespeichert werden sollen, übergeben werden. Ferner eine Methode empty() und eine

Metho-32Im oben erklärten vereinfachten Pseudocode (Algorithmus 2) wurde die gleiche Struk-tur wie in der Implementierung gewählt.

33In diesem Kontext gibt es nur endliche Intervalle.

de reset(), welche dafür gebraucht wird, die Hilfsintervallpaare Q und P in add_constraints() zurücksetzen zu können. Diese beiden Datenstruk-turen sind ein Kompromiss aus lesbarem Code und Geschwindigkeit. Ferner sollten sie für diesen Zweck nur das nötigste enthalten. Es sollen noch zwei weitere Möglichkeiten aufgezeigt und mit der verwendeten verglichen wer-den, von denen eine ohne eigene Datentypen auskommt und die andere nur einen eigenen Datentyp benötigt. Die Methode ohne eigene Datentypen ver-wendet den LEDA Datentyp two_tuple < A, B > verschachtelt. Das heiÿt, ein Intervall wird als two_tuple < edge, edge > dargestellt, ein Paar als two_tuple < two_tuple < edge, edge >, two_tuple < edge, edge > >. Der Vorteil wäre, keine eigene Struktur implementieren zu müssen. Die unschö-ne, pseudocodeferne Schreibweise mit den verschachtelten Templates könnte man mittels typedef umgehen:

typedef two_tuple<edge, edge> interval;

typedef two_tuple<interval, interval> pair;

Es gibt jedoch zwei entscheidende Nachteile. Der Zugri auf die Elemente im Tupel geschieht über die Getter-Methodentwo_tuple < A, B >::f irst()und two_tuple < A, B >::second(). Das entfernt das Codebild weiter vom Pseu-docode. Um auf die untere Kante des rechten Intervalls eines Intervallpaares P zugreifen zu können, müsste man P.second().second()34 aufrufen. Bei Verwendung der oben vorgestellten eigenen Datenstruktur wäre der Aufruf P.R.low. Damit entspräche er dem Pseudocode und wäre leichter zu lesen.

Der zweite groÿe Nachteil ist, dass dem LEDA-Tupel eine swap() Methode fehlt. Diese wird aber gebraucht um in add_constraints() die Intervalle ei-nes Paares tauschen zu können. Lösen könnte man das Problem, indem man für jedes Tupel ein bool Wert für die Seite mitführen würde. Diese Metho-de wäre sehr unelegant. Weiter könnte man einen einen neuen Datentyp von two_tupel < A, B >erben lassen, der die fehlende Funktionalität implemen-tiert. Dies wäre aber fast der gleiche Aufwand wie einen einfachen eigenen Typen zu erstellen. Mit einem eigenen Typen hat man auch den oben be-schriebenen Nachteil nicht. Aus diesen Gründen wurde von der Verwendung des LEDA-Typstwo_tupel < A, B >abgesehen. Die zweite Möglichkeit, die ebenfalls mit der gewählten verglichen werden soll, ist die Verwendung eines Arrays mit fünf Elementen, in dem die ersten beiden Elemente den Kanten des einen Intervalls entsprechen, die zweiten beiden Elemente den Kanten des anderen Intervalls. Das fünfte Element enthält ein Side-Flag. Der Vorteil wäre, dass man swap extrem schnell realisieren könnte. Es müsste nur das

34Man könnte das Intervallpaar auch anders auf die Tupel aufteilen. Hier soll nur die un-schöne Schreibweise gezeigt werden. Auf String-Ersetzung mittels Präprozessordirektiven wurde verzichtet, weil dies sehr schlechter Stil wäre. Es könnte auch an anderen Stellen im Code noch second oder first vorkommen.

Flag verändert werden. Da man in C++ eigentlich keine Arrays mit verschie-denen Typen in einer Dimension machen kann, könnte man das Flag auch durch den Typ edge repräsentieren. Ein Nullpointer würde z.B. heiÿen, dass die ersten beiden Elemente das linke Intervall repräsentieren, eine beliebige Kante das Gegenteil. Der oensichtliche Nachteil dieser Methode ist die un-schöne und nicht abstrakte Zugrisschreibweise. Ferner bräuchte man eine Wrapperklasse, welche die oben erwähnten Bedingungen erfüllt, damit ein so dargestelltes Paar als Teil einer LEDA-Liste verwendet werden kann. Deswe-gen wurde auch von dieser Repräsentation des Intervallpaares abgesehen.

Eine weitere Methode, welche wahrscheinlich später in einer noch mehr auf Geschwindigkeit optimierten Version verwendet werden wird, wäre eine Kom-bination aus der Arraymethode und der verwendeten Klasse Pair. Man könn-te in der Klasse einen Member bool side halkönn-ten, welcher bestimmt welches der beiden Memberintervalle das rechte bzw. das linke ist. Ferner könnten die Methoden Pair::getL() bzw. Pair::getR() den Zugri abstrahieren.

Damit wäre swap() schnell und die Zugrisschreibweise akzeptabel.

Für beide eigenen Datentypen wurde das Makro LEDA_MEMORY(T) verwen-det. Es deniert für einen eigenen TypenT new und delete um und schaltet für ihn das LEDA-Speichermanagement ein. Der LEDA-Speichermanager re-serviert 255 Byte groÿe Speicherblöcke in Listen. Für jeden Aufruf von new, teilt er einen vorreservierten Speicherbereich zu, für jeden Aufruf von delete gibt er ihn wieder frei. Somit spart man bei vielen kleinen Objekten den Aufwand für das Reservieren und Freigeben kleiner Speicherbereiche, was Rechenzeit spart. P air verbraucht 32 Bytes, interval 16 Bytes. Für die-se kleinen Objekte, welche oft gelöscht und erzeugt werden, lohnt sich der LEDA-Speichermanager.

4.5 Phase 3 - Einbettung

Das Problem bei der Implementierung der Einbettungsphase ist, dass - wie oben beschrieben - die Knoten getrennte Adjazenzlisten für ein- bzw. aus-gehende Kanten haben. In der Einbettungsphase soll aber für jeden Knoten eine Ordnung auf allen, zu einem Knoten adjazenten Kanten bestimmt wer-den. Aus diesem Grund muss der Graph in der Hauptfunktion LR_PLANAR() zunächst bidirektional gemacht werden. Das heiÿt, dass für jede Kante e = (v, w); v, w∈V eine Umkehrkante e0 = (w, v) eingefügt wird. Danach muss jeder Kante die Umkehrinformation zugewiesen werden, was bedeutet, dass jede Kante eihre Umkehrkantee0 kennen muss. Wenn diese Umkehrinforma-tion bei einem bidirekUmkehrinforma-tionalen Graphen gesetzt ist, wird er Map genannt. In LEDA wird aus einem GraphenGdurch die Methode G.make_bidirected()

ein bidirektionaler Graph. G.make_map() erzeugt aus einem bidirektionalen Graphen eine Map. In dieser Implementierung wurde die überladene Me-thode G.make_map(list<edge>& R) verwendet, welche beide Schritte direkt ausführt und die hinzugefügten Kanten in der Liste R speichert. Diese Liste ist für die Animation wichtig, damit man nach der Einbettungsphase die Um-kehrkanten für die Anzeige wieder löschen kann. Dafür gibt es keine spezielle Methode.

Wie in Abschnitt 3.3.3 bereits erwähnt, wurden die von einem Knoten ausge-henden Kanten mittels der Verschachtelungsreihenfolge bereits in die richtige Ordnung gebracht. Innerhalb der Einbettungstiefensuche sollen die eingehen-den Kanten in diese Ordnung integriert wereingehen-den. Deshalb wereingehen-den nun die Adjazenzlisten, welche die ausgehenden Kanten halten, verwendet. In ihnen sind nun die Umkehrkanten der eingehenden Kanten35. Wenn man diese Um-kehrkanten der eingehenden Kanten richtig in die Ordnung der ausgehenden Kanten einfügt, bendet sich die für die planare Einbettung gebraucht Infor-mation in der Map. Man erhält eine planare Map.

Wegen der getrennten Adjazenzlisten muss man auch innerhalb der Einbettungs-tiefensuche-Funktion Dinge beachten, die nicht im Pseudocode stehen. Lis-ting 1 zeigt die Implementierung dieser Funktion. Die folgenden Zeilennum-mern sind auf dieses Listing bezogen.

Listing 1: Phase3 Implementierung 11 embedding_dfs (w, gbl ) ;

12 }

35In den Adjazenzlisten, welche die eingehenden Kanten enthalten, sind nun auch die Umkehrkanten der ausgehenden Kanten. Diese werden zwar nicht gebraucht, es ist aber nicht möglich Umkehrkanten nur für eine Art von Kanten zu erstellen.

In Zeile 6 wird zunächst für jede Kante, die von Knoten v ausgeht, die Um-kehrkante in f gespeichert. In Zeile 7 wird geprüft, ob es sich bei der Kante ei um eine wirkliche Baumkante handelt36. Ist dies der Fall, wird in Zeile 8 die Umkehrkantef, welche in der Adjazenzliste der ausgehenden Kanten von w die eingehende Elternkante repräsentiert, vor alle wirklichen ausgehenden Kanten geschoben. Zeile 13 prüft, ob es sich beiei um eine echte Rückwärts-kante handelt. Ist dies der Fall, wird die UmkehrRückwärts-kantef repräsentativ für die Kanteei in der Adjazenzliste der ausgehenden Kanten vonwan der richtigen Stelle eingefügt.

5 Das Animationsprogramm

5.1 Funktionsumfang des Animationsprogramms

Das Animationsprogramm ermöglicht es, die Testphase des oben erklärten Algorithmus schrittweise auszuführen. Wie das Hauptfenster, welches ein LEDA-GraphWin ist, bedient wird, kann in [6] und [7] nachgelesen werden.

Hier soll veranschaulicht werden, was mit dem Animationsprogramm mög-lich ist. Nachdem der Benutzer einen Graphen ausgewählt hat, wird nach der Bestätigung des Benutzers die Orientierungsphase ausgeführt. Danach wird der Graph gelayoutet. Dieses Layout soll dem Layout der oben zur Erklärung verwendeten Beispielgraphen nahekommen. Der Tiefensuchbaum ist deutlich zu erkennen. Die Rückwärtskanten sind kurvenförmig gezeichnet und liegen zu Beginn alle auf der rechten Seite37. Dies ist der Ausgangszustand vor der zweiten Phase. Die zweite Phase wird in drei Ebenen eingeteilt. In jeder die-ser Ebenen gibt es gewisse Ereignisse. In der ersten Ebene benden sich die Ereignisse, die die Elternkante edes Knotenv, mit dem die Test-Tiefensuche aufgerufen wurde, betreen. In der zweiten Ebene benden sich diejenigen Ereignisse, die die vom Zielknoten der Kante e ausgehenden Kanten ei be-treen und in Ebene 3 benden sich die in add_constraints() auftretenden Ereignisse. Folgende Übersicht zeigt die einzelnen Ereignisse38.

Ebene 1:

• Der Unterbaum der Kante e wurde abgeschlossen.

36ei könnte auch eine der Umkehrkanten sein, welche aber nicht beachtet werden sollen.

37Das jetzige Layout eignet sich nur für kleinere Graphen. Genaueres zum Layout ndet sich in Abschnitt 5.2.3

38In einer zukünftigen Version könnte man noch mehr Ereignisse denieren.

• Die zum Quellknoten von zu e zurückkehrenden Kanten wurden ge-trimmt.

Ebene 2:

• Für die Kante ei wurde ein neues Intervallpaar auf den Stack gelegt.

• Die Kante ei wurde samt Unterbaum in e1, ..., ei−1 integriert.

Ebene 3:

• Das oberste, zum Unterbaum vonei gehörige Intervallpaar wurde vom Stack geholt und in Q gespeichert.

• Q.L und Q.R wurden vertauscht, weil Q.Lnicht leer war.

• Die rechte Seite von Q wurde in die rechte Seite von P integriert.

• Der Lowpoint von Q.R.low war gleich dem Lowpoint von e 39.

• Der Unterbaum von ei wurde abgearbeitet. In den Intervallpaaren auf dem Stack gibt es keine Kanten mehr, welche zum Unterbaum von ei gehören.

• Das oberste, zum Unterbaum vone1, ..., ei−1gehörige Intervallpaar wur-de vom Stack geholt und in Qgespeichert.

• Q.L und Q.R wurden vertauscht, weil Q.R mit ei im Konikt stand.

• Die rechte Seite von Q wurde an die rechte Seite von P angehängt.

• Die linke Seite von Q wurde in die linke Seite vonP integriert.

• P wurde auf den Stack gelegt.

Jedes mal, wenn nun eine Bestätigung des Benutzers erfolgt, wird der nächste Schritt ausgeführt, was heiÿt, dass das Programm bis zum nächsten Auftre-ten eines dieser Ereignisse läuft und auf erneute Bestätigung wartet. Bei Ereignissen aus der dritten Ebene hält das Programm nur an, wenn es mit dem Parameter -l3 gestartet wurde. In jedem Schritt wird eine Meldung im Messagewindow gezeigt, welche beschreibt, was gerade geschehen ist. Im Stackwindow wird der aktuelle Zustand des Stacks angezeigt und die gerade aktuellen Kanteneundei40. Falls das Programm mit -l3 gestartet wurde, wer-den dort auch die von add_constraints() benötigten Hilfskantenlistenpaare

39wurde kanonisiert

40ei wird nur angezeigt, wenn es im gezeigten Schritt eine Kanteei gab.

angezeigt. Die Kanten werden im Stackfenster in der Form (<Quellknoten -T ief ensuchnummer>, <Zielknoten-T ief ensuchnummer>) benannt. Die angezeigten Tiefensuchnummern entsprechen bereits den Tiefensuchnummern der 2. Phase. Warum diese Tiefensuchnummern für die Animation bereits vor der 2. Phase berechnet wurden, wird in Abschnitt 5.2.3 erklärt. Das Haupt-fenster zeigt den Graphen, in dem die von dem gerade durchgeführten Schritt betroenen Kanten eingefärbt sind. Hier wurde Colorlinking eingesetzt. Das heiÿt, die Kante e und die Kanten in P und Q haben im Stackwindow die gleiche Farbe wie im Graphen. Die einzelnen Farben bedeuten folgendes:

• braun : Die aktuelle Kante e

• schwarz: Eine im aktuellen Schritt neu hinzugefügte Rückwärtskanteei, eine Kante ei welche kanonisiert wurde. Falls die gerade abgearbeitete Kante ei eine Baumkante ist, wird sie ebenfalls schwarz gezeichnet.

• rot : Eine Kante welche im aktuellen Schritt auf die rechte Seite gelegt wurde.

• blau: Eine Kante welche im aktuellen Schritt auf die linke Seite gelegt wurde.

• grün: Eine Kante welche im aktuellen Schritt getrimmt wurde.

• grau: Kanten, welche im aktuellen Schritt nicht wichtig waren. Kanten die grau gestrichelt sind, sind Kanten, die nie mehr angefasst werden,

• grau: Kanten, welche im aktuellen Schritt nicht wichtig waren. Kanten die grau gestrichelt sind, sind Kanten, die nie mehr angefasst werden,