• Keine Ergebnisse gefunden

Berechnung des Outgoing-Ordered Subgraphs

Der Outgoing-Ordered Subgraph (kurz: OO-Subgraph) eines LDGs G ist der Graph, der entsteht, wenn man aus G alle mehrdeutigen Kantenbeschriftungen entfernt (vgl. Definition 2.21). Doch wozu ist er in Bezug auf den Isomorphietest n¨utzlich? Die folgende Behauptung kl¨art diese Frage:

Behauptung 3.3.

Seien G1 = (V1, E1), G2 = (V2, E2) zwei LDGs, so dass ihre OO-Subgraphen G0i :=OO(Gi) mit i= 1,2 die Rooted Eigenschaft besitzen.

Dann kann die Frage nach einem Isomorphismus zwischenG1 und G2 mittels Al-gorithmus 4 in ZeitO(nlogn) mitn=|G1|+|G2|beantwortet werden. Außerdem gibt es h¨ochstens einen solchen Isomorphismus.

Beweis. Eine bijektive Abbildung π:V1 →V2 ist genau dann ein Isomorphismus von G1 nachG2, wennπ ein Isomorphismus vonG01 nachG02 ist, da in G0i,i= 1,2 keine Knoten entfernt wurden.

Außerdem haben die Graphen G0i per Konstruktion die Outgoing-Ordered Eigen-schaft, so dass Algorithmus 4 mit den Eingaben G01 und G02 aufrufbar ist. Es ist bereits bekannt, dass die berechnete Abbildung πeindeutig ist, falls der Algorith-mus G01 ∼=G02 erfolgreich feststellt.

Damit ist es realisierbar, die eigentlich GI-vollst¨andige Frage nach der Isomor-phie zweier LDGs zu ¨uberpr¨ufen, welche weder Rooted sind noch die Outgoing-Ordered Eigenschaft besitzen. Die einzige Voraussetzung hierf¨ur ist, dass ihre OO-Subgraphen Rooted sind.

Wie man effizient ¨uberpr¨uft, ob ein eingegebener Graph diese Eigenschaft besitzt, wurde ausf¨uhrlich in Abschnitt 3.1 beschrieben. Zentral in diesem Teil der Arbeit soll der Algorithmus zur Berechnung des OO-Subgraphs sein.

Prinzipiell besteht die Hauptaufgabe dabei lediglich darin, die Menge der Kanten E zu durchlaufen und all diejenigen Kanten zu entfernen, die von demselben Kno-ten mit derselben Beschriftung ausgehen. Im nachfolgenden Pseudo-Code wird die-se Idee realisiert, indem zuerst die Kanten gez¨ahlt und anschließend nur diejenigen in den Subgraphen aufgenommen werden, die nicht ¨ofter als einmal vorkommen:

Algorithmus 5 Berechnung des OO-Subgraph eines gegebenen LDGs.

1: function CalculateOOSubgraph(G)

2: Graph oosubgraph = new Graph();

3: Dictionary counting = new Dictionary();

4:

5: oosubgraph.Labels = G.Labels; . copy all labels

6: oosubgraph.Nodes = G.Nodes; . copy all nodes

7:

8: for all (v1, v2, label)∈E do

9: if counting.contains((v1, label))then

10: counting.add((v1, label), counting.get((v1, label))+1);

11: else

12: counting.add((v1, label), 1);

13: end if

14: end for

15:

16: for all (v1, v2, label)∈E do

17: if counting.get((v1, label)) <2then

18: oosubgraph.addEdge(v1, v2, label); . add edge to subgraph

19: end if

20: end for

21:

22: return oosubgraph;

23: end function

Es ist direkt ablesbar, dass in O(|L|+|V|+ 2|E|) =O(|L|+|V|+|E|) Zeit der OO-Subgraph eines Graphen G berechenbar ist. Wie eingangs beschrieben, l¨asst sich mit diesem anschließend – sofern er die Rooted Eigenschaft besitzt – die Iso-morphie zweier LDGs testen.

In Kapitel 5 sind diese Algorithmen implementiert und vorgestellt. Außerdem fin-den sich im Anschluss Tests zur Laufzeit und Effizienz dieser.

Da die fundamentalen Algorithmen nun bekannt sind, kann sich den Rahmengege-benheiten f¨ur die zu entwickelnde Implementierung gewidmet werden. Haupts¨ ach-lich werden hierbei im folgenden Kapitel die, im Sinne der Laufzeit nicht unerheb-liche, Datenstruktur f¨ur die Graphen und weitere Komponenten des Programms vorgestellt.

4 Design und Verwendung

Um die pr¨asentierten Algorithmen letztlich implementieren zu k¨onnen, ist es zun¨achst notwendig, sich f¨ur eine Programmiersprache zu entscheiden. Im Rah-men dieser Bachelorarbeit wird mit der, dem imperativen Programmierparadigma folgenden, SpracheJavagearbeitet, welche in ihrer Entwicklung stark vonC bzw.

C++ gepr¨agt wurde, aber dennoch weniger low-level Funktionalit¨aten als diese besitzen.

Java ist insbesondere deshalb so praktikabel, weil es, begr¨undet durch die soge-nannte Java Virtual Machine (JVM), unabh¨angig von dem jeweiligen Betriebs-system ist, auf dem ein Java-Programm ausgef¨uhrt werden soll. Das Programm wird, anders als bei anderen Programmiersprachen, zun¨achst in denJava Byteco-de ¨ubersetzt, welcher erst innerhalb der besagten JVM zu plattformspezifischem Maschinencode kompiliert wird. Bei

”herk¨ommlichen“ Programmiersprachen f¨allt dieser Zwischenschritt weg, weil das Programm direkt in den Maschinencode trans-formiert wird.

Trotzdem wird, wie in [2] beschrieben, Java-Programmen nachgesagt, dass sie langsamer sind und mehr Speicherplatz als C++ -Programme ben¨otigen. Seit der Einf¨uhrung des Just-In-Time Java Compilers von Symantec (vgl. [5]) ist die Ausf¨uhrungsgeschwindigkeit von Java Programmen jedoch rapide angestiegen, weil zahlreiche Optimierungen der JVM vorgenommen und zus¨atzliche Features f¨ur die Programmiersprache eingef¨uhrt wurden, womit ihr Code besser und vor allem schneller analysiert werden kann.

Grunds¨atzlich istJavaobjekt-orientiert, wird aber nicht als

”reine objekt-orientier-te Sprache“ angesehen, weil ihreprimitiven Datentypen selbst keine Objekte sind.

Im Gegensatz zu anderen Sprachen haben sich die Java-Designer daf¨ur entschie-den, die Werte von Variablen von primitiven Datentypen in Feldern oder auf dem Stack zu speichern anstatt, wie eigentlich ¨ublich, im Heap. Die Entscheidung hier-zu beruht auf Performanzgr¨unden.

Dieses Kapitel soll prim¨ar einen ¨Uberblick ¨uber die benutzte Datenstruktur f¨ur ge-richtete (un-)beschriftete Graphen geben und den Interpreter vorstellen, mit dem der Anwender flexibel Graphen erstellen und die Algorithmen auf ihnen ausf¨uhren kann.

4.1 Die Datenstruktur f¨ ur Graphen

Wie erw¨ahnt wird mit einer objekt-orientierten Programmiersprache gearbeitet, so dass es f¨ur die Implementierung naheliegend ist, auf eine Klassenstruktur zur¨ uck-zugreifen. Zur Visualisierung der Idee der Klassenkonstruktionen wird zun¨achst

eine Darstellung in derUnified Modeling Language (UML) angegeben, welche un-ter anderem Klassendiagramme zur Verf¨ugung stellt und damit Klassenstrukturen und ihre Beziehungen untereinander anschaulich abbilden kann.

Graph

Abbildung 8: UML-Klassendiagramm f¨ur Graphen

Die HauptklasseGraphbesitzt zwei sogenannteInner Classes,NodeundEdge, welche durch Java unterst¨utzt und direkt im Code der Graph-Klasse definiert werden. Im obigen UML-Diagramm wurde diese Beziehung durch eine

Komposi-tion dargestellt (hat-Beziehung). Die Multiplizit¨aten geben hierbei an, dass eine Instanz der Klasse Graph beliebig viele (0. . .∗) Instanzen der Klasse Node bzw. Edge besitzen kann, wobei umgekehrt jede Instanz der Klasse Node bzw.

Edge zugenau einer (1) Instanz der KlasseGraph geh¨ort.

Es wird nun genauer auf die einzelnen Klassen eingegangen und ihre Funktions-weisen erl¨autert.

4.1.1 Die Klasse Node

In dieser Klasse werden die Knoten des Graphen verwaltet. Dabei hat jeder Kno-ten einen Namen name vom Typ String und eine Beschriftung label, eben-falls vom Typ String. Weiterhin werden der Eingangsgrad in degree und auch der Ausgangsgrad out degree, jeweils Integerzahlen, zu jedem Knoten gespei-chert und aktuell gehalten. Dies dient haupts¨achlich der Identifizierung alsWurzel (in degree = 0) bzw. als normaler Knoten (in degree 6= 0). Hierauf wird im sp¨ateren Verlauf dieser Arbeit erneut Bezug genommen.

Erw¨ahnenswert ist, dass der Name eines Knotens zur eindeutigen Identifizierung verwendet wird – soll heißen: Es darf keine zwei Knoten mit demselben Namen geben.

Die genannten Attribute der Klasse haben allesamt den Sichtbarkeitsmodifikator

”-“, sind alsoprivateund somit nur von der eigenen Klasse einseh-/modifizierbar.

Um die Attribute zu manipulieren bzw. auszulesen, sind aus diesem Grund Get-ter- und Setter-Methoden vorgesehen, die wiederum public (+) sind, das heißt auf die von außen zugegriffen werden kann.

Mittels getLabel(), getName(), getInDegree() und getOutDegree() k¨onnen die At-tribute ausgelesen werden, wobei lediglich der Wert zur¨uckgegeben wird und an-sonsten keine weiteren Befehle ausgef¨uhrt werden. Ferner ist es durch die Methode setLabel() m¨oglich, die Beschriftung des Knotens explizit zu setzen.

Bei der Instanziierung Node() der Objekte wird zwingend ein Knotenname ben¨ o-tigt, aber nicht unbedingt eine Beschriftung. F¨ur unbeschriftete Graphen wird dann immer der einzige Eintrag l ∈L als Standardbeschriftung ausgew¨ahlt. Au-ßerdem ist die Absicht der eindeutigen Identifizierung ¨uber den Knotennamen der Grund daf¨ur, dass es keinesetName()-Methode in der Klasse Nodegibt. M¨ochte man einen Knoten umbenennen, so muss man ihn zun¨achst mit Hilfe von remo-veNode() entfernen und anschließend mit neuem Namen wieder hinzuf¨ugen.

Zur Anpassung des Ein- und Ausgrad-Wertes gibt es die MethodenincInDegree() und incOutDegree() bzw.decInDegree() und decOutDegree(), mit denen man den entsprechenden Wert entweder um eins erh¨ohen oder um eins vermindern kann.

Die Klasse Node braucht zudem Zugriff auf die Menge der Beschriftungen aus der Graph-Instanz, falls ein beschrifteter Graph angelegt wird und demnach alle

Knoten und Kanten gleich beschriftet sind. Aufgrund der Tatsache, dass Node eine Inner Class von Graph ist, wird dies allerdings direkt gew¨ahrleistet.

4.1.2 Die Klasse Edge

Die KlasseEdge soll die Menge der KantenE eines Graphen repr¨asentieren und funktioniert prinzipiell ¨ahnlich wie die eben vorgestellte KlasseNode. Sie besitzt drei Attribute: fromund to sind jeweils Objekte vom Typ Nodeund beschrei-ben den Knoten, von dem die Kante ausgeht (from) bzw. den Knoten, in den die Kante eingeht (to). Außerdem hat, per Definition eines LDGs, jede Kante eine Beschriftung, so dass auch hier ein Attributlabelvom Typ String ben¨otigt wird. Ebenso sind die Attribute aus den genannten Gr¨unden wieder private (-) und k¨onnen nur mittels Getter- & Setter-Methoden ausgelesen bzw. ver¨andert werden, die selbst wiederum public (+) sind.

Zur eindeutigen Identifizierung einer Kante wird jetzt die Kombination aus allen drei Attributen gew¨ahlt. Das ist notwendig, weil es mehrere Kanten von Kno-ten v zu Knotenu geben kann, falls die jeweiligen Beschriftungen unterschiedlich gew¨ahlt wurden.

Zum Lesen deslabel-Wertes gibt es die MethodegetLabel(), die den entsprechen-den String zur¨uckliefert. Um die Beschriftung zu setzen, istsetLabel() vorgesehen.

Die from- und to-Werte selbst k¨onnen direkt nicht ausgelesen werden, sondern nur der Name bzw. die Beschriftung des entsprechenden Knotens. Daf¨ur sind die Methoden getNameFrom(), getNameTo(), getLabelFrom() und getLabelTo() vor-gesehen, die ebenfalls direkt den zugeh¨origen String zur¨ucksenden. Es wird also nur indirekt mit den eigentlichen Instanzen der Klasse Node gearbeitet.

Weiterhin ist beim Anlegen einer Kante mittels des Konstruktors Edge() der Ein-Grad vom Knoten to und der Aus-Grad vom Knoten from zu inkrementieren.

Insbesondere sollte anschließend ¨uberpr¨uft werden, ob dadurch die beiden Kno-ten die Wurzel-Eigenschaft verlieren oder erhalten. Als Destruktor ist finalize() vorgesehen, der den Ein- bzw. Aus-Grad dekrementiert und erneut die Wurzel-Uberpr¨¨ ufungen aufrufen soll.

4.1.3 Die Klasse Graph

Wie bereits erw¨ahnt, besteht die Klasse Graph aus Instanzen von Node und Edge, die in einer Datenstruktur AdjacencyListvom Typ Map als Adjazenz-liste verwaltet werden. Diese Map bildet hierbei einen Knoten (Schl¨ussel) auf eine Liste von Kanten (Wert) ab. Ihre Gr¨oße gibt damit die Anzahl der Knoten im Graphen an. Um die Anzahl der Kanten abzufragen (was haupts¨achlich im Hin-blick auf Algorithmus 4 relevant ist), m¨usste man jede einzelne Kantenliste pro Knoten durchgehen und einen Z¨ahler inkrementieren. Aus Laufzeitgr¨unden soll

deshalb ein Attribut CountEdges stets die aktuelle Zahl der Kanten im Gra-phen speichern und direkt beim Erzeugen bzw. L¨oschen von Kanten modifiziert werden.

Weiterhin dient das Attribut NodeMapping dazu, den Name eines Knotens auf seine zugeh¨orige Node-Instanz abzubilden, um auch hier Laufzeit einzusparen und die jeweilige Instanz m¨oglichst schnell greifbar zu haben.

F¨ur die Menge der Beschriftungen L des Graphen ist eine Liste Labels von Strings vorgesehen. Weiterhin soll ebenfalls beim Erzeugen bzw. Entfernen von Knoten bzw. Kanten eine Liste Roots aktuell gehalten werden, in der alle Kno-ten eingespeichert sind, die die Wurzel-Eigenschaft besitzen. Zudem ist, ebenso im Hinblick auf Algorithmus 4, eine Zuordnung NodeLabels von Knotennamen auf Knotenbeschriftung vorgesehen, um diese schnell abfragen zu k¨onnen.

Alle genannten Attribute sind protected (#), um dem gesamtem package und somit auch den Inner Classes Node und Edge Zugriff auf sie zu gew¨ahren. Von außerhalb sind sie allerdings nicht aufruf-/modifizierbar. Die Methoden der Klasse sind trivialerweise hingegen public(+).

Um Beschriftungen, Knoten und Kanten anzulegen, sind die Methoden addLa-bel(), addNode() und addEdge() vorgesehen. Zum Entfernen soll removeLabel(), removeNode() und removeEdge() benutzt werden. Wichtig ist an einigen Stellen, dass die interne CheckRoot()-Methode aufgerufen wird, um nach Ver¨anderungen zu ¨uberpr¨ufen, ob ein Knoten die Wurzel-Eigenschaft hat oder nicht. Hierzu wird das Attribut in degree der Klasse Node verwendet.

Weiterhin ist es per changeNodeLabel() bzw. per changeEdgeLabel() m¨oglich, die Beschriftung eines Knotens bzw. einer Kante zu modifizieren. Hier (aber auch beim Entfernen von Beschriftungen) ist es beispielsweise anschließend notwendig, redundante Kanten zu entfernen, falls durch die Modifizierung denn solche entste-hen. Erw¨ahnenswert ist außerdem, dass beim Anlegen eines Knotens bzw. einer Kante keine Beschriftung explizit angegeben wird, sondern immer automatisch die erste Beschriftungbder MengeLausgew¨ahlt wird. Das Gleiche passiert, wenn eine Beschriftung l entfernt wird – alle Knoten/Kanten, die diese Beschriftung l getragen haben, werden nun auf b gesetzt.

F¨ur die Visualisierung eines Graphen bzw. dessen Komponenten sind die Metho-den printLabels(),printNodes(), printEdges() und printGraph() gedacht. Die drei Erstgenannten geben lediglich zeilenweise ihre Eintr¨age aus, w¨ahrend die Letzt-genannte den Graph als Adjazenzliste darstellt.

Ansonsten erzeugtgenerateMap() die in Schritt (1) von Algorithmus 4 beschriebe-ne Datenstruktur und getSuccessors() gibt explizit die Nachfolger eines bestimm-ten Knobestimm-ten zur¨uck.

Abschließend sei noch angef¨uhrt, dass mittelsexportGraph()eine Graph-Instanz in eine Datei exportiert werden kann, um sie sp¨ater wieder flexibel mittels des

Interpreters, welcher im Folgenden nun ausf¨uhrlich beschrieben und vorgestellt werden soll, in das Programm zu laden.