• Keine Ergebnisse gefunden

Isomorphietest f¨ ur Rooted OOLDG s ([3], Proposition 2.9) . 52

5.3 Algorithms.java

5.3.2 Isomorphietest f¨ ur Rooted OOLDG s ([3], Proposition 2.9) . 52

MittelscheckIsomorphismkann f¨ur zwei LDGs Algorithmus 4 ausgef¨uhrt werden, der in polynomieller Zeit die Isomorphie zweier Graphen dieser Klasse ¨uberpr¨uft.

Die entsprechende Funktion heißtAlgorithms.CheckIsomorphismund liefert bei Erfolg den Isomorphismus π in Form einer Map zur¨uck bzw. bricht mit einer

Exception ab, falls ein Fehler auftritt.

Abbildung 20: Isomorphietest: Phase 1 und 2

Wie bekannt, f¨uhrt der Einfachheit halber jede Graph-Instanz als Attribut die Anzahl seiner Kanten mit (CountEdges). In Phase drei, vier und f¨unf werden die Datenstrukturen f¨ur die Kanten angelegt (vgl. Algorithmus 4, Schritt (1), (2) und (3)), Initialisierungen vorgenommen und versucht, die Wurzeln aufeinander abzubilden:

23 m a p p i n g . put ( r o o t 1 . g e t N a m e () , r o o t 2 . g e t N a m e () ) ;

24 s t a c k . p u s h ( r o o t 1 . g e t N a m e () ) ;

25 }

Abbildung 21: Isomorphietest: Phase 3, 4 und 5

Die FunktiongenerateMap() erzeugt f¨ur eineGraph-Instanz gerade die ben¨otigte Datenstruktur. mapping entspricht der Repr¨asentation des Isomorphismus π und ist anfangs uninitialisiert (null). In Step 5 k¨onnen die Wurzeln problemlos mit der gew¨ahlten Art und Weise ermittelt werden, weil die Graphen an dieser Stelle den Test auf die Rooted OOLDG-Eigenschaft bestanden haben.

Nun ist die Hauptschleife des Algorithmus zu diskutieren, in der der Graph sys-tematisch traversiert und das Mapping aufgebaut wird:

1 // S t e p 6: Use s t a c k to t r a v e r s e the g r a p h

28 if(! m a p p i n g . get ( f o l l o w e d n o d e ) . e q u a l s ( w2 ) )

Abbildung 22: Isomorphietest: Phase 6 (Hauptschleife)

Die Nachfolger des gerade vom Stack entnommenen Knotens werden nacheinander besucht, wobei die Abbildung π entsprechend erweitert wird (Zeile 31), wenn alle notwendigen Bedingungen hierzu eingehalten werden. Wird eine solche Bedingung verletzt, dann wird per Exception abgebrochen und die genaue Fehlermeldung ausgegeben (ist im obigen Code aus Gr¨unden der ¨Ubersicht entfernt). Verwendet werden prim¨ar wieder Maps zur Ermittlung der Nachfolger eines Knotens, welche mit einem Iterator durchlaufen werden.

Es ist bereits bekannt, dass in O(nlogn) Zeit mit n := |G1|+|G2| entschieden werden kann, ob zwei Rooted Outgoing-Ordered LDGs isomorph sind.

5.3.3 Berechnung des Outgoing-Ordered Subgraphs

Uber den Befehl¨ calculateOOSubgraphl¨asst sich die Berechnung des OO-Subgra-phen starten, die sich an Algorithmus 5 orientiert und mit dessen Ergebnis sich an-schließend die eigentlich GI-vollst¨andige Frage nach der Isomorphie zweier LDGs kl¨aren l¨asst, falls die Ergebnisgraphen Rooted sind (vgl. Behauptung 3.3 aus Ab-schnitt 3.3).

Die Funktion Algorithms.CalculateOOSubgraph(Graph graph) erh¨alt eine In-stanz G der Klasse Graph und erzeugt daraus eine neue InstanzG0. Die Menge der Beschriftungen L und auch der Knoten V bleibt jeweils gleich:

1 p u b l i c G r a p h C a l c u l a t e O O S u b g r a p h ( G r a p h g r a p h ) {

2 G r a p h s u b g r a p h = new G r a p h () ;

3

4 // S t e p 1: C o p y all l a b e l s f r o m old g r a p h to s u b g r a p h

5 for(S t r i n g l a b e l : g r a p h . L a b e l s ) {

6 try {

Abbildung 23: OO-Subgraph-Berechnung: Kopieren von L und V Um Referenzierungsfehler zu vermeiden werden beide Mengen

”per Hand“ durch-laufen und danach in G0 neu angelegt. Anschließend m¨ussen die Kanten aus G quantifiziert und nur solche inG0 ¨ubernommen werden, die nicht ¨ofter als ein mal vorkommen:

Abbildung 24: OO-Subgraph-Berechnung: Quantifizieren der Kanten

An dieser Stelle bietet es sich erneut an, eineHashMapzu verwenden und f¨ur jede Kante (v1, v2, l)∈E die Kombination ausv1 und l als Schl¨ussel zu speichern und die entsprechende Anzahl der Vorkommen dieser Kombination als Wert abzulegen.

In Zeile 7 wird zun¨achst ¨uberpr¨uft, ob beim Auftreten einer solchen Kombination selbige bereits in der Map vorhanden ist (dann inkrementieren) oder nicht (dann

anlegen und als Wert 1 mitgeben). In G0 werden im letzten Schritt lediglich die Kanten ¨ubernommen, deren Wert eins ist:

1 // S t e p 4: Add o n l y e d g e s w h o s e c o u n t is s m a l l e r t h a n 2

2 for(Map. Entry < G r a p h . Node , List< G r a p h . Edge > > e n t r y : g r a p h . A d j a c e n c y L i s t . e n t r y S e t () ) {

3 for( G r a p h . E d g e e d g e : e n t r y . g e t V a l u e () ) {

4 int c o u n t = c o u n t e r . get ( e d g e . g e t N a m e F r o m () + " ### " + e d g e . g e t L a b e l () ) ;

5 if( c o u n t < 2) {

6 try {

7 s u b g r a p h . a d d E d g e ( e d g e . g e t N a m e F r o m () , e d g e . g e t N a m e T o () , e d g e . g e t L a b e l () ) ;

8 } c a t c h ( E x c e p t i o n s ex ) {

9 S y s t e m . out . p r i n t l n ( ex . g e t M e s s a g e () ) ;

10 }

11 }

12 }

13 }

14

15 r e t u r n s u b g r a p h ;

16 }

Abbildung 25: OO-Subgraph-Berechnung: ¨Ubernahme von geeigneten Kanten Vor allem werden keine

”falschen“ Kanten eingef¨ugt, weil durch die ¨Uberpr¨ufung in Zeile 5 alle ungeeigneten wegfallen und daher nur die Kanten ausE in Betracht gezogen werden.

Unterstellt man derget- undput-Methode der HashMap eine konstante Laufzeit, dann kann der OO-Subgraph von G in O(|L|+|V|+|E|) berechnet werden.

Die vorgestellten Algorithmen sollen im Anschluss ausf¨uhrlich getestet werden, wobei das Hauptaugenmerk hierbei nat¨urlich auf Algorithmus 4 liegt, bei dem die schnelle Laufzeit von besonderer Relevanz ist. F¨ur die ¨Uberpr¨ufung der Roo-ted Outgoing-Ordered-Eigenschaft bzw. der Berechnung des OO-Subgraphs sind ebenfalls Testf¨alle abgetragen, obwohl ihr Effizienzverhalten bei Ver¨anderung der Graphstruktur eher nebens¨achlich ist.

6 Tests

Um letztlich interessante Testf¨alle durchf¨uhren zu k¨onnen, m¨ussen vorab große Graphinstanzen erzeugt werden, auf denen die Algorithmen dann operieren. Da das selbstverst¨andlich nicht von Hand geschehen kann, wurde daspackage CheckI-somorphism um eine KlasseGenerateGraph erweitert, mit denen sich unterschied-liche Graphklassen individuell erzeugen lassen k¨onnen.

Besonders attraktiv sind nicht nur die Testf¨alle, in denen schlichtweg die Knoten-zahl|V|bzw. die Kantenzahl|E|variiert wird, sondern auch solche, bei denen sich die Graphstruktur ¨andert. Entsprechend sind in GenerateGraph.java vier Metho-den implementiert, die jeweils Instanzen auf verschieMetho-dene Art und Weise aufbauen.

1. Vollst¨andige Graphen

Zur Erinnerung sei auf Definition 2.9 verwiesen.

2. Zuf¨allige Graphen mit variabler Knotenzahl

Die entsprechende Methode erh¨alt eine vorgegebene Knotenzahl n und er-zeugt f¨ur jeden Knoten eine perMath.random()pseudozuf¨allig ausgew¨urfelte Kantenzahl.

3. Zuf¨allige Graphen mit variabler Knoten- und Kantenzahl

Hier wird neben der Knotenzahl n auch eine Kantenzahl m ubergeben. Die¨ Kanten werden zuf¨allig eingef¨ugt (d.h. m steht f¨ur |E| und nicht f¨ur die Anzahl der Kanten pro Knoten), so dass es sein kann, dass ein Knoten deutlich mehr Kanten hat als andere.

4. Gradbeschr¨ankte Graphen mit variabler Knoten- und Gradzahl F¨ur diese Graphinstanzen wird der Grad jedes Knotens durch Eingabe an die Methode fixiert.

Jede erzeugte Graphinstanz wird in einer.graph-Datei abgespeichert und im Ord-ner /graphs/generated/ abgelegt, wobei im Dateinamen zus¨atzlich die

” Graph-klasse“ und das Erstelldatum vermerkt ist. Außerdem werden stets nur so viele Beschriftungen hinzugef¨ugt wie auch n¨otig sind. Diese sind – ebenso wie die Kno-ten – mit nat¨urlichen Zahlen, beginnend bei 1 benannt.

Beachtet werden muss nat¨urlich, dass die vorgestellten Strukturen nicht kom-plett vollst¨andig erf¨ullt werden k¨onnen, weil die Rooted- und auch die Outgoing-Ordered-Eigenschaft stets eingehalten werden muss. Um das zu gew¨ahrleisten, wird stets der Knoten mit Name 1 als Wurzel r ausgew¨ahlt, das heißt vonr gibt es immer genau |V| − 1 Kanten, damit die Wurzel initial ist. Zus¨atzlich muss sichergestellt werden, dass es genau eine solche Wurzel gibt: Der Einfachheit hal-ber erh¨alt deshalb jeder andere Knoten v ∈ V\{1} eine Eigenkante, so dass r

der einzige Knoten mit Eingrad null ist. Die Knotenbeschriftungen selbst werden zuf¨allig ausgew¨urfelt, weil diese f¨ur die Outgoing-Ordered-Eigenschaft nicht von Relevanz sind. F¨ur die Kanten muss die Beschriftung sorgf¨altiger gew¨ahlt werden:

Alle Kanten (1, v, l)∈E sind mit l=v beschriftet. Die Schleifen (v, v, l)∈E mit v ∈ {2, . . . , n}tragen die Beschriftungl= 1. F¨ur alle anderen Kanten (v, u, l)∈E mit v ∈ {2, . . . , n} gilt l=uund es gibt niemals zwei Kanten von Knotenv zuu.

Das ver¨andert die Testergebnisse nicht und erleichtert die Erf¨ullung der Outgoing-Ordered-Eigenschaft. Insgesamt kommen also alle erzeugten Graphinstanzen den Anforderungen nach, damit die Algorithmen auf ihnen operieren k¨onnen.

Um Algorithmus 4 zum Isomorphietest aufrufen zu k¨onnen, sind logischerwei-se zwei Graphinstanzen von N¨oten. Die Klasse GenerateGraph enth¨alt deswegen zwei weitere MethodenCopyGraph() und MisshapeGraph(). Erstgenannte kopiert die ¨ubergebene Graphinstanz eins zu eins in eine Neue. Hierbei wird darauf ge-achtet, explizit keine Referenzen zu verwenden, damit der Anwender im Anschluss nach Belieben den Graph ver¨andern kann, ohne dass dabei auch der Originalgraph Anderungen mit sich tr¨¨ agt. Damit l¨asst sich im Anschluss die Isomorphie testen.

Im Kopiervorgang wurde darauf verzichtet, die Knotennamen zu modifizieren, weil dies lediglich die Komplexit¨at in der Erhaltung der Kantenstruktur erh¨oht h¨atte, aber keinen laufzeittechnischen Einfluss auf Algorithmus 4 selbst hat, weil sich dieser ausschließlich mit der Kantenstruktur und nicht mit den Knotennamen besch¨aftigt.

Die Methode MisshapeGraph() hingegen kann daf¨ur benutzt werden, um aus ei-nem gegebenen Graph G einen modifizierten GraphG0 mit gleicher Knoten- und Kantenzahl zu erzeugen, der in seiner Struktur allerdings ver¨andert ist. Dadurch lassen sich (große) Graphen erzeugen, f¨ur die der Isomorphietest negativ ausf¨allt und damit nat¨urlich vorzeitig abbricht. Da hier allerdings eher die Worst-Case-Laufzeit interessiert, wurde auf Testf¨alle dieser Art verzichtet.

Damit der Anwender diese generierten Graphen auch erzeugen und anschließend mit ihnen arbeiten kann, wurde der Interpreter um die BefehlegenerateComplete Graph, generateRandomNGraph, generateRandomNMGraph, generateDegreeGra ph, importGeneratedGraph, copyGraph und misshapeGraph erweitert. Die ers-ten vier nehmen jeweils die Knoers-tenzahl (und entweder bei RandomNM-Graphen die Kantenzahl bzw. bei Degree-Graphen die Gradzahl) entgegen und erzeu-gen nach dem beschriebenen Schema den entsprechenden Graphen. Die Funk-tion importGeneratedGraph erh¨alt den Dateinamen der .graph-Datei und eine Variable g1|g2|.. |g10, mit der der Graph anschließend aufrufbar ist. Die bei-den letztgenannten Befehle erhalten zwei dieser Variablen, wobei die erste immer den Originalgraph darstellt. F¨ur weitere Nutzungsinformationen kann erneut

Ab-schnitt 4.2 in Betracht gezogen werden. An dieser Stelle ist zu beachten, dass die Algorithmen zur Generierung von großen Testgraphen nicht robust bez¨uglich der Benutzereingaben sind, weil diese lediglich zu Laufzeittests der Algorithmen aus Kapitel 3 dienen.

6.1 Vorbemerkungen

Die Testdaten wurden auf einem Rechner mit einemIntel Core i7-4770K-Prozessor mit vier Kernen und 3.5 GHz Taktfrequenz pro Kern generiert. Die Gr¨oße des Hauptspeichers betr¨agt 16 GB und das Betriebssystem ist Windows 7 in der 64 Bit-Version.

Bei der Kompilierung des Projekts ¨uber das Kommando javac wurden keine be-sonderen Optionen verwendet.

Weiterhin wurden Laufzeitschwankungen, die durch den Java GarbageCollector hervorgerufen werden k¨onnen, abgefangen. Der GarbageCollector ist ein automa-tisiertes und nicht steuerbares Programm, welches zur Laufzeit eines realen Pro-gramms versucht, nicht l¨anger ben¨otigte Speicherbereiche zu identifizieren und anschließend wieder freizugeben. Trotz der Notwendigkeit dieser T¨atigkeit wird damit leider ein nichtdeterministisches Verhalten herbeigef¨uhrt, weil unbekannt ist, wann genau die Collection durchgef¨uhrt wird. Um den so entstehenden Ver-waltungsoverhead, der logischerweise zu einer Erh¨ohung der Laufzeit f¨uhrt, nicht fehlerhaft in den Testdaten mit zu erfassen, ist eine M¨oglichkeit, das Programm mehrmals auszuf¨uhren und im Anschluss den Mittelwert der berechneten Zeiten zu bilden.

Die im Folgenden abgebildeten Tests wurden jeweils zwanzig mal wiederholt, um m¨oglichst genau an der tats¨achlichen Laufzeit zu sein.

Dazu wurde ebenfalls eine weitere Klasse TestSuite angelegt, die genau das be-werkstelligen soll. Die Klasse enth¨alt eine MethodeTest(), die den auszuf¨uhrenden Algorithmus, den Dateinamen der .graph-Datei und die Anzahl der Wiederho-lungen als Eingaben erh¨alt und den berechneten Mittelwert wieder ausgibt.

F¨ur die Laufzeitmessung selbst wurde schlichtweg die Differenz der aktuellen Zeit vor Ausf¨uhrung des Algorithmus und der Zeit danach berechnet. ¨Uber die Funk-tion System.nanoTime() erh¨alt man den Wert der Systemzeit in Nanosekunden, wodurch die Messung sehr genau wird. Die Zeit, die ben¨otigt wird, um die .graph-Datei einzulesen bzw. um die Instanz f¨ur Algorithmus 4 zu kopieren, ist dabei selbstverst¨andlich nicht mit inbegriffen, sondern lediglich die Dauer der realen Ausf¨uhrung des entsprechenden Algorithmus.