Hypergraphengenerator färbungsrelevanter Isomorphieklassenrepräsentanten Stephan Lukits 2. Oktober 2013 Zusammenfassung Es wird ein Algorithmus entwickelt, der zu jeder Isomorphieklasse färbungsrelevanter Hypergraphen genau einen Repräsentanten erzeugt. Mathematischen Grundlagen zu Hypergraphen und deren Färbung finden sich in [Vol02] und [Vol09]. Inhaltsverzeichnis 1 Praeliminarien 2 2 Hypergraphengenerator 3 2.1 Kandidatenreduktion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 2.2 Isomorphietest vs kanonischer Repräsentant . . . . . . . . . . . . . . . . . . . . . . 8 2.3 Grundlagen der Implementierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2.4 Kantenerzeugung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 2.5 Hypergraphenerzeugung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 C Generator Code 18 C.1 Klassische Methode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 C.1.1 Kantenerzeugung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 C.1.2 Hypergraphenerzeugung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 C.2 Klassische Methode mit Korollar 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 C.3 Klassische Methode mit Ordnung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 C.3.1 Kantenerzeugung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 C.3.2 Hypergraphenerzeugung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 C.4 Orderly-Methode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 1 1 Praeliminarien Zunächst sollen die allgemeinen zentralen Begriffe und Bezeichnungen kurz zusammengetragen werden, die in dieser Arbeit Verwendung finden. Die Begriffe Menge, Relation, Funktion und Folge wie injektiv, surjektiv und bijektiv werden in ihrer üblichen Bedeutung verwendet. Sei f eine Funktion, dann bezeichnen dom(f ), ran(f ) und img(f ) die Definitionsmenge, den Wertebereich und das Bild von f . Wie üblich bezeichnet f ◦ g die Verkettung bzw. Verknüpfung zweier Funktionen f und g mit img(g) ⊂ dom(f ), wobei dom(f ◦g) = dom(g), ran(f ◦g) = ran(f ) und f ◦g(x) = f (g(x)). Die Natürlichen Zahlen beginnen mit 1 und wir bezeichnen die ersten n natürlichen Zahlen mit Nn := {1, 2, 3, . . . , n}. Sei M eine endliche Menge. Dann heißt eine Bijektion von M auf M Permutation von M . In einem algorithmischen Kontext ist es von Nutzen eine Permutation von M in ein Paar Bijektionen zu zerlegen, von denen eine als |M |-Tupel aufgefasst werden kann. Sei M eine endliche Menge und p : M → M eine Permutation von M . Weiter sei (f : M → N|M | , g : N|M | → M ) ein Paar Bijektionen mit p = g ◦ f . Sicher existiert solch ein Paar aber es ist selbstredend nicht eindeutig bestimmt. Um nun z.B. alle Permutationen zu erzeugen, fixieren wir f auf eine beliebige Bijektion von M auf N|M | . Weiter können wir ein g, definiert durch 1 7→ g(1), . . . , |M | 7→ g(|M |), eindeutig als |M |-Tupel (m1 , . . . , m|M | ) interpretieren. Umgekehrt können wir jedes |M |-Tupel (m1 , . . . , m|M | ) mit paarweise verschiedenen Elementen aus M eindeutig als eine Funktion g mit 1 7→ m1 , . . . , |M | 7→ m|M | auffassen. Listen wir nun all diese |M |-Tupel auf, erhalten wir alle Bijektionen von N|M | auf M und verknüpft mit f alle Permutationen von M . Da wir in der Bibliothek der hier verwendeten Programmiersprache schon einen Permutationsalgorithmus für indizierte Datentypen vorfinden, können wir so mit geringem Programmieraufwand Aufwand alle Permutationen eines |M |-Tupels und damit auch von M selbst durchlaufen. Ein Paar H := (V, E) endlicher Mengen wollen wir genau dann einen Hypergraph nennen, wenn E ⊂ pot(V ). V heißt Knotenmenge, ein v ∈ V Knoten und |V | heißt Ordnung von H. Die Knoten eines Hypergraphen H = (V, E) werden im Weiteren mit den ersten |V | natürlichen Zahlen bezeichnet, d.h. V = N|V | . E heißt Kantenmenge und ein e ∈ E Kante. |E| heißt die Größe des Hypergraphen. Eine Kante e ∈ E ist in einer Kante e0 ∈ E enthalten, wenn e ⊂ e0 . Ein Hypergraph heißt einfach, wenn keine seiner Kanten in einer anderen enthalten ist. Weiterhin bezeichne H, auch mit Indizierung versehen oder dekoriert wie z.B. in H̃, H̄, H 0 , stets einen einfachen Hypergraphen, V (H) dessen Knotenmenge und E(H) dessen Kantenmenge. Ein Hypergraph mit leerer Kantenmenge heißt leerer Hypergraph. Bezüglich einer festgelegten Knotenmenge ist der leere Hypergraph eindeutig bestimmt. Sei e ⊂ V (H). Dann bezeichnet H + e den Hypergraphen (V (H), E(H) ∪ {e}). Analog H − e := (V (H), E(H) \ {e}). Weiter führen wir die Formalismen e ⊂ E(H) und E(H) ⊂ e ein. Ersteres ist eine Abkürzung von: „e ist in einer Kante aus E(H) enthalten“; letzteres ist eine Abkürzung von „Es gibt eine Kante in E(H), die in e enthalten ist“. Anstelle von E(H) schreiben wir in diesem Zusammenhang auch nur H. Die Anzahl der Kanten, in denen ein Knoten v ∈ V (H) vorkommt, heißt Grad des Knoten in H und wird mit grad(v) bezeichnet. Sei e ∈ E(H). Dann heißt |e| Grad von e in H. Der größte Grad aller Kanten eines Hypergraphen heißt Rang des Hypergraphen. Haben alle Knoten den gleichen Grad k, dann sprechen wir von einem k-regulären Hypergraph. Haben alle Kanten den gleichen Grad r, dann sprechen wir von einem r-uniformen Hypergraph. Wir wollen zwei (einfache) Hypergraphen H und H 0 genau dann isomorph nennen, wenn es eine Bijektion f von V (H) auf V (H 0 ) gibt, sodass mit k ∈ N|V (H)| für alle Kanten gilt {v1 , . . . , vk } ∈ E(H) ⇐⇒ {f (v1 ), . . . , f (vk )} ∈ E(H 0 ). 2 dom(f ), ran(f ), img(f ) Nn Permutation Knoten, Ordnung Kante, Größe einfach V (H), E(H) leerer Hypergraph H ±e e ⊂ E(H), E(H) ⊂ e Grad, grad(v) Rang k-regulären r-uniformen isomorph Offenbar induziert jede Permutation von Nn einen Isomorphismus eines Hypergraphen der Ordnung n und umgekehrt. Im Weiteren bezeichne Aut(H) die Menge der isomorphen Hypergraphen von H, die durch alle Permutationen von Nn induziert werden. Aut(H) heißt die Automorphismengruppe von H. Definition 1. Eine Funktion f : V (H) → Nk wollen wir genau dann k-Färbung von H nennen, wenn f surjektiv ist und es für jede Kante e ∈ E(H) zwei Knoten v, w ∈ e gibt mit f (v) 6= f (w), also wenn e nicht monochromatisch ist. H heißt k-färbbar, wenn zu H eine k-Färbung existiert. Eine surjektive Funktion f : W → Nk mit W ⊂ V (H) heißt k-Teilfärbung, wenn sie das Färbungskriterium erfüllt. Eine k-Teilfärbung eines Hypergraphen nennen wir maximal, wenn kein Knoten mehr gefärbt werden kann, ohne eine monochromatische Kante in dem Hypergraph zu erzeugen. _ Automorphismengruppe k-Färbung k-färbbar k-Teilfärbung maximale k-Teilfärbung Trivialerweise sind alle k-Färbungen eines Hypergraphen auch maximale k-Teilfärbungen. Um zu betonen, dass eine k-Teilfärbung eines Hypergraph keine Färbung von diesem ist, sprechen wir wie üblich von einer echten k-Teilfärbung des Hypergraph. Offenbar kann eine Kante mit weniger als zwei Knoten nicht gefärbt werden. Des weiteren gilt: ist eine Kante e ∈ E(H) korrekt gefärbt, sind auch alle Kanten e0 ∈ E(H) korrekt gefärbt, in denen e enthalten ist. Definition 2 (Färbungsrelevanz). Deshalb wollen wir einen Hypergraph genau dann färbungsrelevant nennen, wenn er einfach ist und jede seiner Kante wenigstens den Grad 2 hat. Entsprechend bezeichne En alle potentiellen Kanten, färbungsrelevante Hypergraphen mit n Knoten. Eine Kante e ∈ En heißt färbungsrelevant bezüglich eines Hypergraphen H, wenn H + e ein färbungsrelevanter Hypergraph ist. _ Im weiteren spielen nur noch färbungsrelevante Hypergraphen eine Rolle. Der leere Hypergraph (bezügliche einer Knotenmenge) ist trivialerweise färbungsrelevant, und kommt uns mitunter als Induktionsanfang gelegen. 2 Hypergraphengenerator Der Hypergraphengenerator soll eine Auflistung färbungsrelevanter Hypergraphen der Ordnung n konstruieren, die für jede Isomorphieklasse der färbungsrelevanten Hypergraphen der Ordnung n genau einen Repräsentanten enthält. Zunächst überlegen wir uns ein möglichst einfaches rekursives Verfahren, auch die klassische Methode genannt, um diese Repräsentanten zu konstruieren. Dieses Verfahren werden wir dann schrittweise verbessern. 1. Erzeuge En 2. C0 auf den leeren Graph (mit n Knoten) und erzeuge daraus L0 die Menge der Repräsentanten mit 0 Kanten. Setze L auf L0 . 3. Seien Ci die färbungsrelevanten Hypergraphen mit i Kanten. Dann erzeuge zu jedem Hypergraph hk ∈ {h1 , . . . , h|Ci | } = Ci die Menge Chk = {hk + e | e ∈ En und e ist färbungsrelevant zu hk }. 3 färbungsrelevant En 4. Erzeuge die Menge [ Ci+1 = Chk . 1≤k≤|Ci | und daraus dann die Menge Li+1 der Repräsentanten mit i + 1 Kanten. 5. Setze L = L ∪ Li+1 und gehe zurück zu 2., so langen Ci+1 nicht leer ist. 6. L enthält nun die Menge der gesuchten Repräsentanten. Wir erzeugen also (1.) zunächst die Menge der potentiellen Kanten En eines Hypergraphen der Ordnung n. Dann erzeugen wir als Rekursionsanfang (2.) die Färbungsrelevanten Hypergraphen mit 0 Kanten C0 und erzeugen aus dieser Menge die Repräsentanten mit 0 Kanten L0 . Im Rekursionsschritt (3. und 4.) erzeugen wir dann aus den färbungsrelevanten Hypergraphen mit i Kanten die färbungsrelevanten Hypergraphen mit (i + 1)-Kanten Ci+1 . Aus dieser Menge filtern wir dann die Repräsentanten mit (i + 1)-Kanten Li+1 . Offensichtlich erhalten wir irgend wann ein leeres Ci+1 . In diesem Fall enthält L durch die sukzessive Ergänzung der gefundenen Repräsentanten alle gesuchten Repräsentanten. Erstens können wir z.B. umsetzen indem wir die Knoten mit den selben Indizes indizieren, mit denen wir die Stellen aller Binärvektoren der Länge n indizieren. Dann können wir einen solchen Binärvektor als Kante auffassen, die genau die Knoten mit den Indizes enthält, die genau die 1-Stellen des Vektors indizieren. Wollen wir nun alle Kanten erzeugen beginnen wir mit dem Nullvektor der Länge n und addieren so lange binär 1, bis wir den 1-Vektor erhalten. Alle dabei erzeugten Vektoren mit wenigstens zwei 1-Stellen induzieren Kanten aus En . In den Punkten 2. – 4. ist die einzige Schwierigkeit aus einem Ci+1 die Repräsentanten Li+1 zu gewinnen. Dazu durchlaufen wir die färbungsrelevanten Hypergraphen aus Ci . Für einen Hypergraph h dieses Durchlaufes bilden wir die Automorphismengruppe Aut(h). Wir fügen h genau dann in Li ein, wenn es in Li keinen Hypergraph gibt, der in Aut(h) enthalten ist. Die Automorphismengruppe eines Hypergraph erhalten wir, indem wir über die indizierten Knoten permutieren. Da es zu n Knoten schon 2n − (n + 1) Kanten in En gibt, wächst die Anzahl der Hypergraphen in den Ci exponentiell in Abhängigkeit der Knoten. Des weiteren ist die Erzeugung einer Automorphismengruppe eines Hypergraphen von fakultativer Komplexität. Es ist also für das Laufzeitverhalten hilfreich, wenn wir die auf Isomorphie zu untersuchenden Hypergraphen der Ci so gering wie möglich halten. Wir werden die folgenden Sätze verwenden, um die Anzahl der Kandidaten zu reduzieren, die als Repräsentanten in Frage kommen. 2.1 Kandidatenreduktion Zunächst stellen wir fest, dass wir aus einer vollständigen Menge von Repräsentanten mit i Kanten wieder eine vollständige Menge von Repräsentanten mit i + 1 Kanten erhalten. Satz 1 (Isomorphieerhalt). Seien H und H 0 zwei färbungsrelevante isomorphe Hypergraphen mit n Knoten. Dann existiert zu jeder färbungsrelevanten Kante e ∈ En zu H eine färbungsrelevante Kante e0 ∈ En zu H 0 , sodass H + e ∼ = H 0 + e0 . Beweis. Seien H und H 0 zwei färbungsrelevante isomorphe Hypergraphen mit n Knoten und e = {v1 , . . . , vk } ∈ En eine färbungsrelevante Kante zu H. Dann existiert eine strukturerhaltende 4 Bijektion f von V (H) auf V (H 0 ) und ein e0 = {f (v1 ), . . . , f (vk )} ∈ En . Per Konstruktion gilt H+e ∼ = H 0 + e0 . Angenommen e0 wäre nicht zu H 0 färbungsrelevant, dann gäbe es ein ē0 = {v̄1 , . . . v̄j } ∈ E(H 0 ) mit ē0 ⊂ e0 . Dann ist aber ē = {f −1 (v̄1 ), . . . , f −1 (v̄j )} ∈ E(H) und ē ⊂ e, was der Färbungsrelevanz von e zu H widerspricht. Korollar 1. Sei Li eine Repräsentantenmenge färbungsrelevanter Hypergraphen mit i Kanten. Dann enthält Ci+1 = {h + e | h ∈ Li , e ∈ En und e ist färbungsrelevant zu h} von jeder Isomorphiklasse färbungsrelevanter Hypergraphen mit i + 1 Kanten wenigstens einen Repräsentanten. Das heißt für unseren Algorithmus, dass wir im 3. Punkt Ci durch Li ersetzen können. Wir können noch weiter gehen und müssen gar nicht alle färbungsrelevanten Kanten durchprobieren, wenn wir auf den Kanten eine Ordnung einführen. Wählen wir im weiteren eine Kante zu einem Hypergraph aus, der um diese ergänzt werden soll, setzen wir stillschweigend voraus, dass es sich um eine färbungsrelevante Kante zu diesem Hypergraph handelt. Definition 3 (Kantenordnung). Seien e, e0 ∈ En Kanten, die durch Tupel ihrer natürlich geordneten Knoten repräsentiert werden. Dann heißt e größer als e0 , wenn e länger ist als e0 , oder wenn sie gleich lang sind, bis zu einer Stelle i komponentenweise übereinstimmen und der Knoten von e an der Stelle i größer als der von e0 ist. Kantenordnung Diese Ordnung lässt sich analog auf Hypergraphen fortsetzen, indem wir einen Hypergraphen als entsprechend geordnetes Kantentupel auffassen. Ausserdem setzen wir für ein e ∈ En H<e H < e :⇔ alle Kanten e0 ∈ E(H) sind kleiner als e. beziehungsweise e<H e < H :⇔ es gibt eine Kante e0 ∈ E(H) die größer ist als e. In diesem Sinne wollen wir anstelle von der Größe eines Graphen oder dem Grad einer Kante auch von der Länge eines Graphen oder der Länge einer Kante sprechen. Die Indizierung geordneter Objekte wird nun doppeldeutig verwendet. Je nach Zusammenhang bezeichnet der Index wie üblich eine aufzählende Nummerierung; er kann aber auch die Länge des Objekts bezeichnen, also bei einem Graphen z.B. die Anzahl der Kanten. Zählen wir die Elemente eines geordneten Objekts auf, gehen wir immer davon aus, dass die Elemente geordnet aufgezählt werden. Bezeichnet Hk einen Hypergraph der Länge k, dann bezeichnet Hk−1 den Hypergraphen Hk − ek , wobei ek , wie gerade erwähnt, die größte Kante von Hk ist. H0 bezeichnet in diesem Zusammenhang den leeren Hypergraph. _ Korollar 2. Seien Hi und Hi0 Hypergraphen mit i Kanten und e, e0 ∈ En mit Hi < e sowie Hi0 < e0 . Dann gilt: Hi < Hi0 =⇒ Hi + e < Hi0 + e0 . Nach der trivialen Konstruktion von L0 erhalten wir, falls wir C1 aufsteigend geordneter durchlaufen, das folgende L1 : L1 = {{1, 2}}, {{1, 2, 3}}, {{1, 2, 3, 4}}, . . . , {{1, 2, 3, . . . , n}} 5 Länge In L0 sind folglich die kleinsten Hypergraphen mit 0 Kanten und indem wir Graphen und Kanten geordnet durchlaufen, gewinnen wir aus L0 die kleinsten Repräsentanten mit genau einer Kante. Es stellt sich also die Frage, ob wir, durchlaufen wir die aus Li gewonnenen Hypergraphen Ci+1 aufsteigend geordnet, ein Li+1 mit den minimalen (i+1)-Repräsentanten erhalten? Dies ist offenbar der Fall, wenn diese in Ci+1 enthalten sind. Was wiederum genau dann der Fall ist, wenn zu jedem minimalen Repräsentanten H mit i + 1 Kanten ein minimaler Repräsentant H 0 ∈ Li mit einer Kante e ∈ En existiert, sodass H 0 + e = H. Für L1 gilt dies trivialer weise. Nehmen wir an, es gäbe einen minimalen Repräsentanten Hi+1 mit i + 1 Kanten zu dem es keinen Hypergraphen H 0 ∈ Li mit einer Kante e ∈ En gibt, sodass H 0 + e = Hi+1 . Also ist insbesondere Hi 6∈ Li . Dann existiert mit Korollar 1 und voraussetzungsgemäß ein minimaler Hi0 ∈ Li mit Hi0 ∼ = Hi und Hi0 < Hi zu dem mit Satz 1 eine Kante e0 ∈ En existiert, sodass Hi0 +e0 ∼ = Hi +ei+1 = Hi+1 . Voraussetzungsgemäß ist Hi0 + e0 > Hi+1 . D.h. mit Korollar 2 ist e0 < Hi0 . Es existiert also ein maximales k ∈ {0, . . . , i − 1} mit Hk0 < e0 . Ist H̄i = {e01 , . . . , e0k , e0 , e0k+2 , . . . , e0i−1 } ∈ Li , dann ist H̄i < Hi0 und H̄i + e0i ∼ = Hi+1 . Dies widerspricht unserer minimalen Wahl von Hi0 . Ist H̄i 6∈ Li , dann existiert per Induktionsvoraussetzung ein H̃i ∈ Li mit H̃i ∼ = H̄i und H̃i < H̄i < Hi0 . Da es 0 0 ∼ mit ei eine Kante gibt, sodass H̄i + ei = Hi+1 , gibt es wieder mit Satz 1 eine färbungsrelevante Kante ẽ ∈ En , sodass H̃i + ẽ ∼ = Hi+1 . Also erhalten wir auch in diesem Fall einen Widerspruch zur minimalen Wahl von Hi0 und können festhalten: Satz 2 (Minimumfortsetzung). Sei Li die Menge der kleinsten Repräsentanten der Isomorphieklassen färbungsrelevanter Hypergraphen mit i Kanten. Dann gibt es zu jedem kleinsten derartigen Repräsentanten H mit i + 1 Kanten einen Repräsentanten H 0 ∈ Li mit einem e ∈ En , sodass H 0 + e = H. Weiter folgt wie besprochen für unseren Algorithmus: Korollar 3. Seien Li die kleinsten Repräsentanten der Isomorphieklassen färbungsrelevanter Hypergraphen mit i Kanten und Ci+1 die gemäß unseres Algorithmus aus Li resultierenden Hypergraphen mit i + 1 Kanten. Verarbeiten wir die Hypergraphen aus Ci+1 aufsteigend in eben dieser Ordnung um Li+1 zu gewinnen, dann enthält Li+1 die minimalen Repräsentanten mit i + 1 Kanten. Nehmen wir nun an, wir bekämen schon alle minimalen Repräsentanten für Ci+1 , wenn wir die Hypergraphen aus Li nur mit Kanten ergänzten, die größer sind als die jeweiligen Hypergraphen, also für ein hk ∈ Li : Chk = {hk + e | e ∈ En , hk < e}. Dann könnten wir uns durch ein aufsteigend geordnetes Durchlaufen der Hypergraphen von Li zur Konstruktion der Kandidaten für Li+1 nicht nur die Sortierung von Ci+1 sparen, wir könnten uns den Umweg über Ci+1 ganz sparen. Denn jeder so erzeugte neue Hypergraph mit i + 1 Kanten ist wegen dem aufsteigend geordnetem Probieren der Kanten und Korollar 2 größer als die zuvor erzeugten. D.h. wir könnten sofort nach dem oben vorgeschlagenen Verfahren entscheiden, ob ein so erzeugter Kandidat in Li+1 aufgenommen wird, oder nicht. Vermuten wir also, dass es zu jedem färbungsrelevanten H + e mit H ∈ Li und e < H einen H 0 ∈ Li mit einem e0 ∈ En derart gibt, dass H 0 < H, H 0 < e0 und H 0 + e0 ∼ = H + e. Da es in L0 nur den Hypergraph H0 gibt und zu diesem keine kleinere Kante existiert, ergibt sich für diesen Fall die Bestätigung unserer Vermutung trivialer weise. Gelte nun unsere Vermutung bis zu einem beliebigen Index i und sei weiter H = {e1 , . . . , ei } aus Li und e ∈ En , sodass e < H und H + e ein Kandidat für Li+1 ist. Dann gibt es einen größten 6 Index j ∈ {0, . . . , i − 1} mit Hj < e. Sei H ∗ = {e1 , . . . , ej , e, . . . , ei−1 }. Ist H ∗ ∈ Li , dann gilt offenbar H ∗ < H, H ∗ < ei sowie H ∗ + ei ∼ = H + e und wir sind fertig. Ist H ∗ 6∈ Li , dann existiert ein H̃ ∗ ∈ Li mit H̃ ∗ ∼ = H ∗ und H̃ ∗ < H ∗ < H. Da es mit ei zu H ∗ eine ∗ ∼ Kante gibt, sodass H + ei = H + e, gibt es mit Satz 1 auch ein ẽ ∈ En derart, dass H̃ ∗ + ẽ ∼ = H + e. ∗ 0 0 0 0 ∼ Also ist mit H̃ und ẽ die Existenz eines Hypergraphen H und einer Kante e mit H + e = H + e und H 0 < H gesichert. Sein nun H 0 = {e01 , . . . , e0i } ∈ Li der kleinste Hypergraph, der vorstehende Bedingung erfüllt und e0 ∈ En eine Kante, sodass H 0 + e0 ∼ = H + e. Angenommen e0 < H 0 . Dann gibt es einen weiteren größten Index j ∈ {0, . . . , i − 1} mit Hj0 < e0 . Sei H ∗ = {e01 , . . . , e0j , e0 , e0j+2 , . . . , e0i }. Da H ∗ < H 0 widerspräche H ∗ ∈ Li der minimalen Wahl von H 0 , also H ∗ 6∈ Li . Dann aber gibt es ein H̃ ∗ ∈ Li mit H̃ ∗ ∼ = H ∗ und H̃ ∗ < H ∗ < H 0 wegen der Induktionsvoraussetzung. Da es mit e0i eine Kante gibt, sodass H ∗ + e0i = H 0 + e0 ∼ = H + e, existiert mit Satz 1 zu H̃ ∗ eine Kante ẽ ∈ En derart, dass ∗ ∗ 0 H̃ + ẽ ∼ = H + e. Da H̃ < H , erhalten wir wieder einen Widerspruch zur minimalen Wahl von 0 H . Es folgt H 0 < e0 . Unsere Vermutung bestätigt sich also und wir können einen entsprechenden Satz notieren. Satz 3 (Kantenergänzung). Sei Li die Menge der kleinsten Repräsentant der Isomorphieklassen färbungsrelevanter Hypergraphen mit i Kanten bezüglich einer lexikographischen Ordnung, die aus einer lexikographischen Kantenordnung fortgesetzt wurde. Sei H ∈ Li , e ∈ En , e < H und H +e ein färbungsrelevanter Hypergraph. Dann existiert ein kleinerer Hypergraph H 0 ∈ Li und eine Kante e0 ∈ En mit H 0 < e0 , sodass H 0 + e0 ∼ = H + e. Korollar 4. Sei En aufsteigend lexikalisch geordnet. Sei Li die Menge der kleinsten Repräsentanten mit i Kanten bezüglich der Fortsetzung der Kantenordnung auf Hypergraphen. Die Hypergraphen aus Li werden aufsteigend durchlaufen. Sei h ein Hypergraph dieses Durchlaufes. h wird aufsteigend um Kanten ergänzt die größer sind als h. Für jede solche Kante e > h gilt, dass h + e genau dann in Li+1 aufgenommen wird, falls keiner der Hypergraphen Hi+1 in Li+1 Element von Aut(h + e) ist. Dann Enthält Li+1 von jeder Isomorphieklasse Hypergraphen mit i + 1 Kanten genau einen Repräsentanten. Dieser Repräsentant ist minimal. Wir erhalten damit auch noch eine Vereinfachung der Berechnung der Färbungsrelevanz einer Kante e ∈ En bezüglich eines Hypergraphen. Denn aus Definition 3 folgt: Korollar 5. Sei Hi ein Hypergraph mit i Kanten. Hi < e =⇒ e 6⊂ Hi (e ∈ En ) Damit müssen wir für die Färbungsrelevanz einer Kante e bezüglich eines Hypergraph H mit H < e nur noch prüfen, ob H 6⊂ e. Die Korollare 1 – 4 spiegeln die Modifikationen unseres Algorithmus wieder, um die Kandidaten zu reduzieren. Als nächstes wollen wir das Entscheidungsverfahren, nach dem ein Kandidat als Repräsentant in Li+1 aufgenommen wird, genauer untersuchen. 7 2.2 Isomorphietest vs kanonischer Repräsentant Gemäß des im letzten Abschnitt vorgestellten Algorithmus, müssen wir für einen Kandidaten H der (i + 1)-Repräsentantenliste Li+1 bezüglich aller schon in Li+1 befindlichen Repräsentanten auf Isomorphie prüfen, um nach der klassischen Methode zu entscheiden, ob H in Li+1 aufgenommen wird oder nicht. Ein weiterer Ansatz, der die Isomorphietests umgeht, ist die sogenannte Orderly Method. Unter der Voraussetzung, dass sich die zu erzeugende kombinatorische Struktur entsprechend ordnen lässt und der Konstruktionsalgorithmus gewisse Bedingungen erfüllt, schlug Read in [Rea78] vor, dass die Repräsentanten der Isomorphieklassen sogenannte kanonische Repräsentanten der jeweiligen Isomorphieklasse sein sollen. Wählen wir dann unter geeigneten Voraussetzungen zum Beispiel das größte Objekt einer Isomorphieklasse, wie Read es tat, als kanonischen Repräsentanten, dann müssen wir einen Kandidaten H für Li+1 „nur“ in seinen kanonischen Repräsentanten H c umformen und prüfen, ob dieser größer ist als der Letzte in Li+1 . Wobei Li+1 die geordnete Liste der kanonischen (i + 1)-Repräsentanten bezeichnet. Ist H c größer, dann fügen wir H c in Li+1 ein, anderenfalls wird H c verworfen. Im Wesentlichen funktioniert ein Orderly Algorithmus wie folgt: 1. Jede Repräsentanten-Liste Li besteht ausschließlich aus kanonischen Repräsentanten, die entsprechend der definierten Ordnung aufsteigend geordnet sind. 2. Ein durch einen Konstruktionsalgorithmus KA konstruierter (i + 1)-Kandidat wird genau dann Li+1 hinzugefügt, wenn dies unter Einhaltung der Kriterien in 1. geschehen kann. Damit sich die orderly Methode erfolgreich umsetzen lässt, muss der Konstruktionsalgorithmus zusammen mit der Wahl des kanonischen Repräsentanten und der Ordnung gewisse Bedingungen erfüllen. Bezeichne KA einen Konstruktionsalgorithmus, der aus einer Konfiguration H ∈ Li Kandidaten für Li+1 erzeugt, während KA(H) die Menge der erzeugten Kandidaten aus H bezeichnet. Read nannte dann die folgenden Bedingungen: 1. Für jeden kanonischen Repräsentanten Hi+1 ∈ Li+1 existiert ein Hi ∈ Li , sodass Hi+1 ∈ KA(Hi ). 2. Sei f : Li+1 −→ Li ein Abbildung, die einer Konfiguration Hi+1 ∈ Li+1 den ersten iRepräsentanten Hi zuordnet, für den gilt Hi+1 ∈ KA(Hi ). Dann ist f schwach monoton X < Y =⇒ f (X) ≤ f (Y ) (X, Y ∈ Li+1 ) 3. Die Ausgabe von KA(Hi ) ist bezüglich unserer Ordnung aufsteigend sortiert. Das ersteres der Fall sein muss, ist offensichtlich, da sonst die Vollständigkeit unserer Aufzählung nicht gewährleistet ist. Wäre 2. nicht erfüllt, dann könnte ein kanonischer Repräsentant Hi+1 0 0 vor einem kleineren kanonischen Repräsentanten Hi+1 konstruiert werden, sodass Hi+1 der Liste Li+1 gemäß der Orderly Method nicht mehr hinzugefügt würde. Entstehen schließlich zwei (i + 1)Repräsentanten aus dem selben i-Repräsentanten, dann muss noch sicher gestellt werden, dass der kleinere von den Beiden zuerst konstruiert wird. Dies erledigt die 3. Bedingung. Read formuliert dann den Satz: Erfüllt die Wahl eins Konstruktionsalgorithmus, des kanonischen Repräsentanten und einer Ordnung auf den Repräsentanten die Bedingungen 1. – 3., dann ist der Algorithmus effektiv. Denn 1. sichert uns die Vollständigkeit des Ergebnisses sowie 2. und 3. zusammen die Korrektheit. 8 Orderly Method Gehen wir zu unseren färbungsrelevanten Hypergraphen zurück, stell wir fest, dass wir im letzten Abschnitt schon alle Bedingungen bewiesen haben, die hier gefordert werden. Tatsächlich folgt aus Korollar 4, dass wir immer den kleinsten Hypergraphen einer Isomorphieklasse als Repräsentanten wählen und die Hypergraphen gemäß der Ordnung aus Definition 3 konstruiert werden. Also wählen wir für unsere kombinatorische Struktur sinnvollerweise den kleinsten Hypergraphen als kanonischen Repräsentanten. Definition 4 (kanonischer Repräsentant). Sie H ein Hypergraph und Aut(H) dessen Automorphismengruppe. H heißt genau dann kanonischer Repräsentant (einer Isomorphieklasse), wenn er bezüglich der Ordnung aus Definition 3 kleiner als alle übrigen Automorphismen in Aut(H) ist. _ Damit können wir uns bei einem potentiellen Kandidaten Hi+1 ∈ KA(Hi ) einer Isomorphieklasse für Li+1 auf die Frage beschränken: Ist Hi+1 kanonischer Repräsentant? Oder anders formuliert, gibt es in der Automorphismengruppe von Hi+1 keinen kleineren Automorphismus? Falls nicht nehmen wir Hi+1 in Li+1 auf, falls schon verwerfen wir ihn. Offensichtlich müssen wir dann nicht mehr die ganze Liste durchlaufen und Isomorphietests vollziehen, was zu einer bemerkenswerten Speicherersparnis führt. Dies war 1978, als Read sein Paper publizierte, natürlich noch von anderer Wichtigkeit als heute. Außerdem konnten wir durch Korollar 4 das Vorgehen von Read noch ein wenig verbessern, weil wir nun völlig unabhängig von Li+1 prüfen können, ob ein Hi+1 ein geeigneter Kandidat für Li+1 ist. Dies hat den praktischen Vorteil, dass wir diese so adaptierte Methode leichter parallelisieren können als das Readsche Vorgehen. Was den Aufwand für eine nahe liegende Umformung zum kanonischen Repräsentanten anbelangt, ist diese auch von fakultativer Komplexität, sodass für die Komplexitätsbetrachtungen aus dieser Vorgehensweise vermutlich kaum ein nennenswerter Vorteil erwachsen wird. Betrachten wir eine nahe liegende Methode, um von einem Hypergraphen H zu überprüfen, ob er kanonischer Repräsentant ist: • Bilde alle Knotenpermutationen bezüglich der Knotenordnung. • Bilde die Automorphismengruppe von H. • Prüfe, ob in dieser ein kleinerer Graph als H enthalten ist. Für die Isomorphietest-Variante müssten wir stattdessen prüfen, ob einer der schon in Li+1 enthaltenen Hypergraphen Element der Automorphismengruppe ist. Verwenden wir in Python den Typ set um die Automorphismengruppe zu speichern, dann können wir im Durchschnitt in konstanter Zeit auf Enthaltensein prüfen. Sei k die Anzahl der Hypergraphen, die schon in Li+1 sind. Dann stehen k Tests auf Enthaltensein n! Vergleichen gegenüber, wobei n die Anzahl der Knoten ist. Ein Zusammenhang zwischen n, k und i könnte für differenziertere Komplexitätsbetrachtungen von Interesse sein. (Dabei ist i die Anzahl der Kanten der untersuchten Hypergraphen.) Wir wollen hier nicht weiter darauf eingehen und uns für die Orderly Method entscheiden, da diese speichereffizienter und leichter zu parallelisieren ist. Last but not least legen Publikation wie [McK81], [HG03] und [BL83] nahe, dass es noch effizientere Möglichkeiten gibt einen Hypergraphen in seinen kanonischen Repräsentanten umzuformen, als dessen vollständige Automorphismengruppe zu erzeugen. 9 kanonischer Repräsentant 2.3 Grundlagen der Implementierung An dieser Stelle soll kurz in die Besonderheiten der Sprache Python eingeführt werden und die Ergebnisse von fünf verschiedenen Implementierungsvarianten zusammengefasst werden, bevor wir die effizienteste Implementierung im Detail betrachten. Es wurde zum einen der am Anfang von Abschnitt 2 vorgestellte klassische Algorithmus C implementiert und eine Variante C1 davon, die Korollar 1 berücksichtigt, da dieser eine erste signifikante Effizienzsteigerung einbringt. Dann wurde eine Variante C5 des klassischen Algorithmus implementiert, die auch die übrigen Ergebnisse bis Korollar 5 berücksichtigt und schließlich wurde C5 so zu Co modifiziert, dass die OrderlyMethode umgesetzt wurde. Co konnte dann durch einen Implementierungstrick Cot noch einmal beschleunigt werden. Betrachten wir die durchschnittliche Länge von 10 Läufen für die Erzeugung der Repräsentanten mit 5 bzw. 6 Knoten, dann erhalten wir die Geschwindigkeitsentwicklung aus Abbildung 1 bzw. 2. sec. 6 ◦ 20.83 ◦ 2, 16 ◦ 0, 97 0, 59 0, 19 ◦ ◦ - C1 C C5 Co Cot Algorithmus Abbildung 1: Effizienz der Repräsentantenerzeugung für n = 5 min. 6 67, 86 ◦ ◦ 10, 82 ◦ 8, 39 ◦ 2, 23 - C1 C5 Co Cot Abbildung 2: Effizienz der Repräsentantenerzeugung für n = 6 10 Algorithmus Wir haben also eine signifikante Verbesserung der klassischen Methode durch die Kandidatenreduktion und die orderly Methode erreicht. Für n = 5 konnte die Effizienz in etwa verhundertfacht werden während n = 6 (Abb. 2) überhaupt erst durch diese Verbesserungen zugänglich wurde. In Abbildung 2 können wir ebenfalls sehen, dass sich der Algorithmus C5 und Co für den Fall n = 6 annähern. Während für n = 5 noch eine Performancegewinn von gut 33 Prozent möglich war, ist für n = 6 nur noch ein Gewinn von gut 20 Prozent festzustellen. Betrachten wir nun die Umsetzung der effizientesten Implementierung. In den ersten beiden Zeilen Code eines Python Moduls wird der Interpreter des Codes und dessen Codierung festgelegt. 1 2 #! / u s r / b i n / p y t h o n # −∗− c o d i n g : u t f −8 −∗− Als erstes erzeugen wir eine aufsteigend geordnete Aufzählung der potentiellen färbungsrelevanten Kanten. Im Kontext dieser geordneten Aufzählung erhält jede Knotenmenge, die eine Kante repräsentiert, einen eindeutigen Index, der die Ordnung widerspiegelt. Dann erzeugen wir eine Zuordnung, die einer (färbungsrelevanten) Kontenmenge in konstanter Zeit ihren Index in der geordneten Kantenaufzählung zuordnet. Schließlich erzeugen wir noch die Knotenpermutationen, um später die Automorphismengruppe durchlaufen zu können. 3 4 5 6 d e f g e n e r a t e _ h y p e r g r a p h s ( numberOfNodes ) : e d g e s = c r e a t e _ e d g e s ( numberOfNodes ) mapToIndex = c r e a t e _ e d g e s _ i n d e x _ m a p p i n g ( e d g e s ) n o d e P e r m u t a t i o n s = c r e a t e _ n o d e _ p e r m u t a t i o n s ( numberOfNodes ) Um von einem Hypergraphen H festzustellen, ob er ein kanonischer Repräsentant ist, ob also in der Automorphismengruppe von H kein kleinerer Hypergraph als H ist, müssen wir die Automorphismengruppe von H durchlaufen. Dazu durchlaufen wir die Knotenpermutationen, die in Zeile 6 erzeugt werden, und bilden die Knoten der Kanten von H unter diesen ab. Eine so gefundene Bildkanten ist aber schon in den erzeugten Kanten edges aus Zeile 4 enthalten. Um nun diese Knotenmenge in konstanter Zeit der entsprechenden bereits erzeugten Kante zuordnen zu können, berechnen wir in Zeile 5 die Zuordnung mapToIndex, die einer Knotenmenge den entsprechenden Kantenindex aus edges zuordnet. So erzeugen wir alle Kanten der Hypergraphen der Automorphismengruppe eines Kandidaten nicht wieder neu, sondern können die schon berechneten Kanten für diese Hypergraphen recyceln. Dann berechnen wir aus praktischen Gründen als Rekursionsanfang die Repräsentanten mit einer Kante anstelle von L0 . Denn im Weiteren wollen wir einen Repräsentanten Ri+1 mit i + 1 Kanten aus einem entsprechenden Repräsentanten Ri mit i Kanten berechnen, indem wir Ri mit den färbungsrelevanten Kanten kombinieren, die größer als die aus Ri sind. Wir müssen also die größte Kante von Ri ermitteln. Fassen wir einen Hypergraphen als aufsteigend geordnetes Tupel von Kantenindizes auf, wobei diese Indizes die Kantenordnung widerspiegeln, dann repräsentiert der letzte Kantenindex im Tupel, die größte Kante des Hypergraphen. Nähmen wir nun als Rekursionsanfang an, dass L_i nur den leeren Hypergraphen enthält, dann würde ein Zugriff auf den größten Index zu einer Ausnahme führen aufgrund einer Bereichsüberschreitung des leeren Tupels. Um nun nicht während der gesamten Hypergraphenerzeugung jeden Repräsentanten auf den leeren Hypergraph prüfen zu müssen, berechnen wir als Rekursionsanfang die Hypergraphen mit einer Kante und stellen damit sicher, dass immer eine größte Kante in einem Repräsentanten existiert. 11 L_i = [ ] f o r hg i n g e n e r a t e _ L 1 ( e d g e s ) : L_i . append ( hg ) y i e l d hg 7 8 9 10 In Zeile 7 initialisieren wir die Variable L_i mit der leeren Liste [] . Eine Liste ist ein vordefinierter indizierter Datentyp Pythons. Das i-te Element wird mit der [ i ] -Operation angesprochen, d.h. L_i[ i ] gibt das i-te Element der Liste L_i zurück, wobei i eine ganze Zahl ist. Ist i eine positive ganze Zahl, dann gibt L_i[−i] das i-letzte Element der Liste L_i zurück. L_i[−1] ist z.B. dementsprechend das letzte Element von L_i. Die Indizierung einer Liste startet mit 0. Im Unterschied zu einem typischen Array ist die Größe einer Liste nicht festgelegt und die Listenelemente sind nicht typisiert. Zugriff, Wertzuweisung und Ergänzung eines Listenelemnts sind, wie für einen Array-Typ üblich, in konstanter Zeit implementiert. Mit for element in indexedVar: werden die Elemente einer Variable eines indizierter Datentyps durchlaufen, also insbesondere auch die einer Listenvariable (Zeile 8). Eine Liste wird mit der Methode append(element) um ein neues Element ergänzt. Also wird in Zeile 9 die Liste L_i um ein Element der Aufzählung generate_L1(edges) ergänzt. Das Schlüsselwort yield in Zeile 10 macht eine Funktion zu einer sogenannten Generator-Funktion. generate_L1(edges) aus Zeile 8 ist ebenfalls eine Generator-Funktion. Dabei hat yield zum einen die Funktion eines returnAusdrucks und liefert den Wert des nachfolgenden Ausdrucks zurück. Im Falle von Zeile 10 wird also der Hypergraph hg zurückgegeben. Zum anderen wird der Zustand der Funktion konserviert und beim nächsten Aufruf der Funktion wird direkt nach dem zurück gegebenen Ausdruck wieder eingesprungen, und die Funktion mit der selben Variablenbelegung wie beim Verlassen fortgesetzt. w h i l e L_i : L_i_plus_1 = [ ] f o r hg i n g e n e r a t e _ L _ i _ p l u s _ 1 ( L_i , n o d e P e r m u t a t i o n s , mapToIndex , e d g e s ) : L_i_plus_1 . append ( hg ) y i e l d hg L_i = L_i_plus_1 11 12 13 14 15 16 17 Ist die for -Schleife aus Zeile 8 beendet enthält L_i die kleinsten Isomorphieklassenrepräsentanten der färbungsrelevanten Hypergraphen mit einer Kante. So lange L_i nicht leer ist (Zeile 11) werden aus den Hypergraphen von L_i die Repräsentanten mit i + 1 Kanten gewonnen (Zeile 12–15). Diese werden, nachdem sie jeweils im Sinne einer Generator-Funktion zurück gegeben wurden (Zeile 16), für den nächsten Durchlauf der while-Schleife L_i zugewiesen (Zeile 17). In den nächsten beiden Teilabschnitten besprechen wir die Kanten- und Hypergraphenerzeugung im Detail. 2.4 Kantenerzeugung Wir überlegten uns im Zuge der Umsetzung des ersten Algorithmus schon ein Vorgehen zur Kantenerzeugung. Da wir die Kanten nun aber geordnet wollen, können wir auch gleich ein Verfahren wählen, das uns die Kanten entsprechend geordnet liefert. Dazu setzen wir die Knoten {1, . . . , n} natürlich geordnet voraus, sodass wir jeden Knotenindex in eineindeutiger Weise als Index eines binären n-Tupels t auffassen können. Dann können wir ein t als die Kante interpretieren, die genau die Knoten enthält, deren Indizes 1-Stellen von t indizieren. Es gibt dann für jede färbungsrelevante Kante genau ein solches n-Tupel und für jedes Tupel mit wenigstens zwei 1-Stellen genau eine färbungsrelevante Kante. Betrachten wir zunächst — quasi als Induktionsanfang — die Kanten mit zwei Knoten, d.h. die binären n-Tupel mit genau zwei 1-Stellen. Bezeichne 11 die erste und 12 12 die zweite 1-Stelle. Wir beginnen mit der kleinsten 2-Kante: (110 , 121 , 02 , . . . , 0n−1 ) = {1, 2}. Sei i der Index von 11 und j der von 12 . Dann funktioniert der Algorithmus für 2-Kanten wie folgt: j < n − 1: Setze die Stelle j (des n-Tupels) auf 0 und die Stelle j + 1 auf 12 , d.h. (. . . , 11i , . . . , 12j , . . . , 0n−1 ) geht über in (. . . , 11i , . . . , 0j , 12j+1 , . . .). Setze (den Index) j auf j + 1. j = n − 1: Berechne i. i < n − 2: Setze die Stellen i und j auf 0 sowie die Stelle i + 1 auf 11 und i + 2 auf 12 , d.h. (. . . , 11i , . . . , 12n−1 ) geht über in (00 , . . . , 0i , 11i+1 , 12i+2 , . . . , 0n−1 ). Setze j auf i + 2. i = n − 2: Wir sind bei (00 , . . . , 0n−3 , 11n−2 , 12n−1 ) angelangt und fertig. Die beiden Konstruktionsregeln für ein neues Tupel haben gemein, dass 11 in dem konstruierten Tupel auf einer kleineren Stelle ist als 12 , also ist 11 immer auf einer kleineren Stelle als 12 . Ebenso offensichtlich durchläuft 11 alle Stellen von 0 bis einschl. n−2 und für jede dieser Stellen i durchläuft 12 die Stellen i + 1 bis einschl. n − 1, d.h. es werden alle 2-Kanten gebildet. Schließlich sehen wir, dass beide Konstruktionsregeln zu größeren Kanten führen als die Ausgangskante (gemäß unserer Ordnung). Damit sind die erhaltenen Kanten durch die Reihenfolge ihrer Konstruktion aufsteigend geordnet. Verallgemeinern wir nun 2 auf m, d.h. wir konstruieren alle m-Kanten aus n Knoten, dann ist m unsere Ausgangskanten (110 , . . . 1m und für den Fall, dass m−1 , 0m , . . . , 0n−1 ). j ist die Stelle von 1 j = n − 1, bezeichnet k die Anzahl der 1-Stellen am Ende des Tupels. Nehmen wir z.B. m = 3 und n = 5, dann gilt im Falle (110 , 01 , 02 , 123 , 134 ), dass k = 2. i bezeichne jetzt die größte Stelle mit einer 1, der eine 0 folgt und die kleiner als j ist. Wir haben nun wieder zwei Konstruktionsregeln und eine Abbruchbedingung, wobei i nur in der Konstruktionsregel mit der Voraussetzung j = n − 1, k < m erscheint. Es ist klar, dass unter dieser Vorraussetzung i existiert. j < n − 1: Setze die Stelle j auf 0 und die Stelle j + 1 auf 1m . Setze j auf j + 1. j = n − 1 Berechne k. k < m: Berechne i. Es gilt nun also (. . . , 1m−k , 0i+1 , . . . , 0n−(k+1) , 1m−k+1 , . . . , 1m n−1 ). i n−k | {z } falls k=1 Setze die Stellen n − k bis einschließlich n − 1 sowie i auf 0 und setze die Stellen i + 1 bis einschl. i + k + 1 auf 1m−k , . . . , 1m . Setze j auf i + k + 1. k = m: Wir sind bei (00 , . . . , 0n−(m+1) , 11n−m , . . . , 1m n−1 ) angelangt und fertig. Die zweite Transformation bedeutet also in etwa: „Wenn 1m am Ende des Tupels ist und es noch wenigstens eine 1pi gibt, die nicht an den Einsen am Ende des Tupels hängt, dann ersetze die k Einsen am Ende des Tupels durch 0, schiebe 1pi um eine Stelle nach rechts und hänge die k Einsen vom Ende des Tupels an 1pi+1 an. 13 Auch im allgemeinen Fall sehen wir leicht, dass das erstmalige Anwenden der beiden Konstruktionsregeln zu Tupeln führt, in denen die 11 bis einschl. 1m ihre Reihenfolge behalten und das Resultat einer Konstruktion stets größer ist als das vorhergehende Tupel. Nehmen wir uns zwei beliebige n-Tupel mit m Einsen, wobei auf das eine der erste und das andere der zweite Fall zutrifft. Bezeichnen wir die Einsen von links nach rechts mit 11 bis 1m , und wenden die jeweilige Konstruktionsregel an, dann können wir auch sehe, dass die resultierenden Tupel größere Tupel sind und das die Einsen in ihrer Reihenfolge bleiben. Sodass wir mit Struktureller Induktion sagen können, dass die Einsen immer in der Ausgangsreihenfolge bleiben und, dass die induzierten Kanten aufsteigend geordnet erzeugt werden. Nun müssen wir noch die Frage klären, ob tatsächlich alle m-Kanten erzeugt werden. Setzen wir m = 2 und k = 1, dann erhalten wir den schon untersuchten Fall der 2-Kanten für ein beliebiges n. Nehmen wir nun an, dass unsere Behauptung bis zu (m − 1)-Kanten für beliebiges n gilt. Dann können wir in einem beliebigen n Tupel mit m Einsen diese von links nach rechts mit 11 , . . . , 1m bezeichnen. Ausserdem gibt es Indizes i1 bis einschl. im von 11 bis einschl. 1m . Beginnen wir mit unsrem Ausgangstupel: (110 , . . . , 1m m−1 , 0m , . . . , 0n−1 ), dann gilt voraussetzungsgemäß, dass die m− 2 m 1 Einsen 1 , . . . , 1 in den Stellen 1 bis n − 1 jede mögliche Position einnehmen, also insbesondere auch die Positionen n − (m − 1) bis n − 1. Wenden wir nun unsere zweite Transformationsregel an, dann geht 1 m (110 , 01 , . . . , 0n−m , 12n−(m−1) , . . . 1m n−1 ) in (01 , 11 , . . . , 1m , 0m+1 , . . . 0n−1 ) über. Dies können wir wiederholen bis der Index von 11 der Index n − m ist. Voraussetzungsgemäß folgt sofort, dass 12 bis einschl. 1m die nachfolgenden Stellen geordnet einnehmen werden, also werden alle m-Kanten von n Knoten aufsteigend erzeugt. Nun müssen wir nur noch die 2- bis einschl. n-Kanten aus n Knoten aufsteigend Zusammenfassen, um En aufsteigend geordnet zu erhalten. Zur Umsetzung in Python definieren wir die Funktion 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 d e f c r e a t e _ e d g e s ( numberOfNodes ) : d e f m_edges (m, n ) : edge , j , k , e d g e s = ( [ 1 ] ∗m + [ 0 ] ∗ ( n−m) , m−1, 0 , [ ] ) w h i l e not ( k == m) : # b r e a k c o n d i t i o n e d g e s . append ( b i n a r y _ v e c t o r _ t o _ e d g e ( e d g e ) ) i f j < n −1: # f i r s t c o n s t r u c t i o n r u l e edge [ j ] = 0 j += 1 edge [ j ] = 1 e l s e : # j == n−1 k = 1 # c o u n t t h e o n e s a t t h e end o f t u p e l try : w h i l e e d g e [ n−k − 1 ] : k += 1 e x c e p t I n d e x E r r o r : # m == n return edges i = n − k − 1 # calculate i try : w h i l e not e d g e [ i ] : i −= 1 e x c e p t I n d e x E r r o r : # k == m pass e l s e : # non e x c e p t i o n b r an c h , s e c o n d c o n s t r u c t i o n r u l e 14 41 42 43 44 45 46 47 48 edge [ i ] = 0 e d g e [ n−k : n ] = [ 0 ] ∗ k e d g e [ i +1: i+k +2] = [ 1 ] ∗ ( k+1) j = i+k+1 return edges def binary_vector_to_edge ( binVector ) : return f r o z e n s e t ( pos f o r pos i n range ( l e n ( b i n V e c t o r ) ) i f b i n V e c t o r [ pos ] ) 49 50 51 52 53 edges = [ ] f o r m i n r a n g e ( 2 , numberOfNodes +1): e d g e s += m_edges (m, numberOfNodes ) return edges Die innere Funktion m_edges(m, n) (Zeile 18) erzeugt die m-Kanten von n Knoten und binary_vector_to_edge(binVector) (Zeile 46) konvertiert einen binären Vektor zur Menge der Indizes der 1-Komponenten des Vektors. Die zurück gegebene Kantenmenge wird in Zeile 50 mit der leeren liste initialisiert. Die Funktion range(m, n) liefert eine Aufzählung von einschließlich m bis ausschließlich n. In Zeile 51 werden also alle möglichen färbungsrelevanten Kantengrade eines Hypergraph mit n Knoten durchlaufen, um dann in der darauffolgenden Zeile die Kanten eines solchen Grads zu erzeugen und an die schon erzeugten Kanten anzuhängen. Der +-Operator verknüpft also im Kontext zweier Listen diese Listen. Schließlich werden die erzeugten geordneten Kanten edges zurück gegeben. Eine Kante ist vom Datentyp frozenset . Eine Objekt dieses Typs ist nach dessen Definition im Vergleich zum Typ set nicht mehr änderbar. Deshalb können Objekte dieses Typs als „Schlüssel“ für Schlüssel 7→ Wert Zuordnungen dienen. In den Zeilen 45 – 48 wird eine Binärvektor-Repräsentation einer Kante in eine frozenset -Repräsentation konvertiert. Dabei liefert ein Ausdruck der Form „ f (elem) for elem in iteration if g(elem)“ eine Aufzählung der Elemente elem unter der Funktion f aus der Aufzählung iteration , welche die Bedingung g(elem) erfüllen. In den Zeilen 47 – 48 werden also die positionen eines Binär-Vektors aufgezählt deren Wert nicht 0 ist. (f ist in diesem Beispiel offenbar die Identität.) Diese Aufzählung wird dann dem Konstruktor frozenset (...) übergeben. Die eigentliche Kantenerzeugung, wie wir sie gerade oben besprochen haben, findet dann in der inneren Funktion m_edges(m, n) statt. Das Idiom [m]∗n erzeugt eine Liste der Länge n deren n Elemente alle mit m initialisiert sind. Also werden in dem Ausdruck [1]∗ m + [0]∗(n−m) zwei Listen verknüpft. Erstere dieser beiden Listen hat die Länge m und ihre Elemente sind mit 1 initialisiert, während letztere von der Länge (n-m) ist und ihre Elemente mit 0 initialisiert sind. Offenbar kann das Resultat als Binärvektor der länge n aufgefasst werden, dessen ersten m Stellen 1 sind während die übrigen verschwinden. Wir initialisieren also edge mit der kleinsten Kanten mit m Knoten und j mit der größten Stelle, die mit einer 1 besetzt ist. Da am Ende des Vektors (falls m < n) noch keine 1 ist, wird k mit 0 initialisiert und die zurückzugebende Kantenliste wird mit der leeren Liste initialisiert. Der Index von 1m ist nun dementsprechend m − 1 und der von 11 ist 0. So lange nicht 11 bis einschl. 1m am Ende des Tupels sind, d.h. k == m, führen wir je nach dem eine der beiden oben beschriebenen Konstruktionen aus. Für die Zeilen 42 und 43 sei noch angemerkt, dass myList[m:n] die Teilliste vom einschl. m-ten bis zum ausschließlich n-ten Element bezeichnet. Dementsprechend setzt z.B. edge[n−k:n] = [0]∗k die letzten k Stellen des binärvektors edge auf 0. 15 Nun erzeugen wir noch die Zuordnung, die einem Kanten-Objekt dessen Index in der Kantenaufzählung zuordnet. 55 56 57 58 59 def create_edges_index_mapping ( edges ) : mapToIndex = d i c t ( ) for idx in range ( len ( edges ) ) : mapToIndex [ e d g e s [ i d x ] ] = i d x r e t u r n mapToIndex Wir durchlaufen in 57 alle Kantenindizes und ordnen mit mapToIndex[edges[idx]] = idx in der darauffolgenden Zeile einer Kante ihren Index zu. 2.5 Hypergraphenerzeugung Da die Kanten geordnet vorliegen, ist es zur Erzeugung der kleinsten Repräsentanten mit einer Kante ausreichend alle Kanten zu durchlaufen und bei jedem Gradwechsel einen entsprechenden Hypergraphen mit der kleinsten Kante eines bestimmten Grads zu erzeugen. 60 61 62 63 64 65 def generate_L1 ( edges ) : lastLength = 0 for edgeIdx in range ( len ( edges ) ) : i f len ( edges [ edgeIdx ] ) > lastLength : lastLength = len ( edges [ edgeIdx ] ) y i e l d tuple ( [ edgeIdx ] ) Am Rückgabewert in Zeile 65 wird deutlich, dass ein Hypergraph durch ein (geordnetes) Tupel von Kantenindizes repräsentiert wird. Für die Erzeugung von Li+1 benötigen wir zunächst noch die Knotenpermutationen. Dazu bedienen wir uns eines permutations-Objekts, das bei Python mitgeliefert wird. 66 67 68 69 70 71 72 from i t e r t o o l s import p e r m u t a t i o n s d e f c r e a t e _ n o d e _ p e r m u t a t i o n s ( numberOfNodes ) : nodePermutations = [ ] f o r p e r m u t a t i o n i n p e r m u t a t i o n s ( r a n g e ( numberOfNodes ) ) : n o d e P e r m u t a t i o n s . append ( t u p l e ( [ permutation , d i c t ( ) ] ) ) return nodePermutations permutations(range(numberOfNodes)) liefert in Zeile 69 einen Generator für alle n-Tupel der Zahlen {0, . . . , n−1} deren Stellen paarweise verschieden sind. Eine Besonderheit unsrer Implementierung ist, dass wir eine Permutation aus nodePermutations nicht direkt verwenden sondern ein Paar definieren, dessen erste Komponente das Permutationstupel ist und dessen zweite Komponente eine leere Zuordnung ist (Zeile 71). Da bei der Erzeugung der Automorphismengruppe eines Kandidaten immer wieder die selben Kanten abgebildet werden, wird eine einmal berechnete Zuordnungen von einer Kante zu deren Bild unter einer Permutation in der zweiten Komponente der entsprechenden Permutation gespeichert. Dadurch steigt zwar der Speicherbedarf wieder spürbar an. Er bleibt aber in einem praktikablem Rahmen und der Effizienzgewinn ist für n = 5 z.B. eine Verdreifachung wie an dem Unterschied zwischen den Implementierungen Co und Cot in Abbildung 1 zu sehen ist. Abschließend ist noch die Generator-Funktion zur Erzeugung der Repräsentanten mit i + 1 Kanten aus den Repräsentanten mit i Kanten zu implementieren. 16 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 d e f g e n e r a t e _ L _ i _ p l u s _ 1 ( L_i , n o d e P e r m u t a t i o n s , mapToIndex , e d g e s ) : def i s _ c a n o n i c a l ( c a n d i d a t e ) : d e f i s _ s m a l l e r _ t h e n ( agHg , c a n d i d a t e ) : f o r i d x i n r a n g e ( l e n ( agHg ) ) : i f agHg [ i d x ] > c a n d i d a t e [ i d x ] : r e t u r n F a l s e i f agHg [ i d x ] < c a n d i d a t e [ i d x ] : r e t u r n True return False # i d e n t i c a l f o r np i n n o d e P e r m u t a t i o n s : agHg = t u p l e ( ) # new h y p e r g r a p h o f Aut ( h y p e r g r a p h ) for edgeIdx in candidate : t r y : agHg += np [ 1 ] [ e d g e I d x ] except : np [ 1 ] [ e d g e I d x ] = t u p l e ( [ # we r e c y c l e e d g e s mapToIndex [ f r o z e n s e t ( np [ 0 ] [ n i ] f o r n i i n e d g e s [ e d g e I d x ] ) ] ] ) agHg += np [ 1 ] [ e d g e I d x ] i f i s _ s m a l l e r _ t h e n ( s o r t e d ( agHg ) , c a n d i d a t e ) : return False r e t u r n True d e f i s _ c o l o r i n g _ r e l e v a n t _ t o ( edge , h y p e r g r a p h ) : for edgeIdx in hypergraph : i f edges [ e d g e I d x ] . i s s u b s e t ( edge ) : retu rn F a l s e r e t u r n True 96 97 98 99 100 101 102 f o r h y p e r g r a p h i n L_i : f o r e d g e I d x i n r a n g e ( ( h y p e r g r a p h [ −1]+1) , l e n ( e d g e s ) ) : i f i s _ c o l o r i n g _ r e l e v a n t _ t o ( edges [ edgeIdx ] , hypergraph ) : candidate = hypergraph + tuple ( [ edgeIdx ] ) i f is_canonical ( candidate ): y i e l d ( candidate ) Die inner Funktion is_canonical (candidate) (Zeile 74) berechnet, ob der übergebene Kandidate kanonisch ist. Die inner Funktion is_coloring_relevant_to (edge, hypergraph) prüft, ob die Kante edge zum Hypergraph hypergraph aus Li färbungsrelevant ist. Wir durchlaufen also alle Hypergraphen H aus Li (Zeile 97). Wir prüfen dann zu jedem H jede Kanten e, die größer als die größte Kante in H ist, ob sie färbungsrelevant zu H ist (Zeilen 98f). Für diesen Fall (Zeile 100) haben wir einen neuen Kandidaten H + e. Von diesem berechnen wir, ob er kanonisch ist und falls ja, geben wir ihn zurück. Anderenfalls machen wir mit der nächsten Kante bzw. mit dem nächsten Hypergraph weiter. Zur Berechnung von is_coloring_relevant_to (edge, hypergraph) prüfen wir entsprechend Korollar 5 nur, ob der aktuelle Hypergraph H aus Li eine Kante enthält (Zeile 93), die eine Teilmenge der übergebenen Kante e ist (Zeile 94). Im letzteren Fall wird entsprechend Definition 2 und der mangelnden Einfachheit von H + e False zurück gegeben; stellt sich H + e als einfach heraus, wird True zurück gegeben. Wurde so ein neuer Kandidate H + e gefunden, dann werden in den Zeilen 80–88 alle Automorphismen durchlaufen und in den Zeilen 75–79 wird von jedem Automorphismus getestet, ob er kleiner ist als der aktuelle Kandidat. Findet sich kein Automorphismus, der kleiner ist als der Kandidat, dann ist der Kandidat offenbar kanonisch. 17 Anhang C Generator Code Jede der folgenden Implementierungen beginnt mit den Zeilen 103 104 #! / u s r / b i n / p y t h o n # −∗− c o d i n g : u t f −8 −∗− 105 106 107 108 109 d e f g e n e r a t e _ h y p e r g r a p h s ( numberOfNodes ) : e d g e s = c r e a t e _ e d g e s ( numberOfNodes ) mapToIndex = c r e a t e _ e d g e s _ i n d e x _ m a p p i n g ( e d g e s ) n o d e P e r m u t a t i o n s = c r e a t e _ n o d e _ p e r m u t a t i o n s ( numberOfNodes ) In Zeile 5 wird eine Liste von Kanten erzeugt und in der darauffolgenden Zeile eine Zuordnung, die einer Kante ihren Index in der zuvor berechneten Liste zuordnet. Denn wenn die Automorphismengruppe eines Hypergraphen, der als Kantenmenge implementiert ist, erzeugt wird, werden die Knoten einer Kante durch eine Knoten-Permutation auf eine andere Kante abgebildet. Um nun eine Kante nicht vielfach im Speicher zu halten, finden wir durch diese Zuordnung den Index dieser Kante in unserer Kantenliste und verwenden dann die Kante aus der edges-Liste. In Zeile 7 erzeugen wir dann noch eine Liste der Kontenpermutationen, die wir für jede Erzeugung einer Automorphismengruppe benötigen. Ausserdem finden wir in einer generate_hypergraphs-Funktion wenigstens eine yield hg-Anweisung (wobei hg einen Hypergraph enthält). Das Schlüsselwort yield hat neben der Funktionsweise des return-Schlüsselworts noch die Eigenschaften, dass der Zustand (des Rahmens) der Funktion auch nach der Rückgabe eines Wertes durch yield im Speicher gehalten wird und dass ein darauf folgender Aufruf der Funktion direkt nach dem Rückgabewert der yield -Anweisung fortgesetzt wird. Das Schlüsselwort yield macht eine Funktion in Python zu einer sogenannten GeneratorFunktion. Diese haben z.B. den Vorteil, dass aufwendig rekursiv erzeugte Objekte einzeln zurück gegeben werden können, ohne gleich alle Objekte berechnen zu müssen. So können wir z.B. mit for hg in generate_hypergraphs(5): ... Hypergraph für Hypergraph Isomorphieklassenrepräsentanten der Hypergraphen mit 5 Knoten berechnen. C.1 1 2 Klassische Methode #! / u s r / b i n / p y t h o n # −∗− c o d i n g : u t f −8 −∗− 3 4 5 6 7 8 9 10 11 12 d e f g e n e r a t e _ h y p e r g r a p h s ( numberOfNodes ) : e d g e s = c r e a t e _ e d g e s ( numberOfNodes ) mapToIndex = c r e a t e _ e d g e s _ i n d e x _ m a p p i n g ( e d g e s ) n o d e P e r m u t a t i o n s = c r e a t e _ n o d e _ p e r m u t a t i o n s ( numberOfNodes ) C_i = s e t ( [ f r o z e n s e t ( ) ] ) # i n i t i a l i z e C_i w i t h empty h y p e r g r a p h w h i l e C_i : f o r hg i n g e n e r a t e _ L _ i ( C_i , n o d e P e r m u t a t i o n s , mapToIndex , e d g e s ) : y i e l d hg C_i = c r e a t e _ C _ i _ p l u s _ 1 ( C_i , e d g e s ) 18 Nachdem die Zeilen 5–7 klar sind, können wir in 8 Ci mit dem leeren Hypergraph initialisieren und mit der Erzeugung der Hypergraphen beginnen. Dies geschieht in dem wir mittels des Generators generate_L_i in Zeile 10 die Isomophieklassen-Repräsentanten aus Ci heraus filtern. In der darauffolgenden Zeile wird jeder so gefundene Repräsentant mit dem Schlüsselwort yield zurück gegeben. Schließlich erzeugen wir mit der Funktion create_C_i_plus_1 in 12 aus Ci die nächsten potentiellen Repräsentanten mit i + 1 Kanten. So lange diese Funktion potentielle Repräsentanten zurück liefert fahren wir fort, aus diesen Repräsentanten herauszufiltern. Ci wird mittels des Datentyps set implementiert während Hypergraphen und Kanten durch den Datentyp frozenset umgesetzt werden. Objekte letzteren Typs sind unveränderbar. Deshalb können sie einen hash-Wert haben, der Voraussetzung ist, um Element einer Menge (Typ: set ) oder Schlüssel einer Zuordnung (Typ: dict ) sein zu können. Da Hypergraphen als Elemente der Ci Menge Elemente einer Menge sind und Kanten in der Zuordnung mapToIndex als Schlüssel dienen, werden beide als Objekte vom Typ frozenset implementiert. C.1.1 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 Kantenerzeugung d e f c r e a t e _ e d g e s ( numberOfNodes ) : def add_binary_one_to_binVector ( ) : for p o s i t i o n in range ( len ( binVector ) ) : i f not b i n V e c t o r [ p o s i t i o n ] : binVector [ position ] = 1 break else : binVector [ position ] = 0 def hase_binVector_two_nodes ( ) : oneNode = F a l s e for p o s i t i o n in range ( len ( binVector ) ) : i f binVector [ position ] : i f oneNode : r e t u r n True e l s e : oneNode = True def add_binVector_to_edges ( ) : e d g e s . append ( f r o z e n s e t ( p o s i t i o n for p o s i t i o n in range ( len ( binVector )) i f binVector [ position ] ) ) 31 32 33 34 35 36 37 38 39 b i n V e c t o r = [ 0 ] ∗ numberOfNodes e n d V e c t o r = [ 1 ] ∗ numberOfNodes edges = [ ] w h i l e b i n V e c t o r != e n d V e c t o r : add_binary_one_to_binVector () i f hase_binVector_two_nodes ( ) : add_binVector_to_edges ( ) return edges Um den Algorithmus möglichst kompakt formulieren zu können, werden in den Zeilen 21–37 Funktionen definiert, die auf der Funktionsvariable binVector arbeiten, die in Zeile 39 mit dem (binären) 0-Vektor initialisiert wird. Der in der darauf folgenden Zeile definierte 1-Vektor dient zwei Zeilen weiter als Abbruchbedingung. Die Variable edges wird mit der leeren Liste initialisiert und wird im folgenden um die erzeugten Kanten ergänz und schließlich als Ergebnis der Berechnung zurück gegeben. 19 In den Zeilen 42-45 werden dann die Kanten erzeugt, so lange bis binVector mit endVector identisch ist. Die Abbruchbedingung wird erreicht, da in Zeile 43 zu binVector 1 binär hinzugezählt wird. Dann wird geprüft, ob der so entstandene Binärvektor wenigstens zwei 1 enthält. Ist dies der Fall wird er in eine Kante konvertiert und der Kantenliste hinzugefügt. Noch ein paar Bemerkungen zum Verständnis des Codes. Die Anweisung range(n) erzeugt die Aufzählung (0, 1, . . . , n − 1), sodass for position in range(len (binVector )) alle Indizes der Liste binVector durchläuft. Die 0 wird in einem Konditionalen Zusammenhang als falsch ( False ) interpretiert und alle Zahlen ungleich 0 als wahr (True), sodass if not binVector [ position ]: genau dann wahr ist, wenn binVector an der Stelle position eine 0 stehen hat. Das Idiom [ f (item) for item in enumeration if g(item)] erzeugt ein Aufzählung der f (item) für die g(item) wahr ist, sodass in den Zeilen 36 und 37 eine Aufzählung der Stellen des Binärvektors erzeugt werden, die ungleich 0 sind. Diese Aufzählung wird dann dem Konstruktor der forzenset -Klasse übergeben. Die so erzeugte Kante wird dann mit der Methode append in 35 der Kantenliste hinzugefügt. Schließlich erzeugt das Idiom [m]∗n eine Liste der Länge n deren Elemente alle mit m initialisiert werden, also erzeugt [0]∗ numberOfNodes eine Liste der Länge numberOfNodes deren Elemente alle mit 0 initialisiert sind. In diesem Zusammenhang können wir auch noch die Erzeugung der Kanten-Index-Zuordnung betrachten: 40 41 42 43 44 def create_edges_index_mapping ( edges ) : mapToIndex = d i c t ( ) for idx in range ( len ( edges ) ) : mapToIndex [ e d g e s [ i d x ] ] = i d x r e t u r n mapToIndex Der Datentyp dict liefert eine Zuordnung. In Zeile 49 werden alle Kantenindizes der zuvor erzeugten Liste durchlaufen und in der darauf folgenden Zeile wird einer Kante ihr Index zugeordnet. C.1.2 Hypergraphenerzeugung Bevor wir die Kandidaten für die Isomorphieklassenrepräsentanten erzeugen betrachten wir kurz die Erzeugung der Knotenpermutationen. Diese ist denkbar einfach, da wir uns des bei Python mitgelieferten permutations-Modul bedienen können: 45 46 47 48 49 50 from i t e r t o o l s import p e r m u t a t i o n s d e f c r e a t e _ n o d e _ p e r m u t a t i o n s ( numberOfNodes ) : nodePermutations = [ ] f o r p e r m u t a t i o n i n p e r m u t a t i o n s ( r a n g e ( numberOfNodes ) ) : n o d e P e r m u t a t i o n s . append ( p e r m u t a t i o n ) return nodePermutations der permutations generator erzeugt zu einem n-Tupel (0, 1, . . . , n − 1) alle dessen Permutationen. 51 52 53 54 55 56 d e f c r e a t e _ C _ i _ p l u s _ 1 ( C_i , e d g e s ) : d e f i s _ c o l o r i n g _ r e l e v a n t _ t o ( edge , hg ) : f o r e i n hg : i f edge . i s s u b s e t ( e ) or e . i s s u b s e t ( edge ) : return False r e t u r n True 57 58 C_i_plus_1 = s e t ( ) 20 59 60 61 62 63 f o r hg i n C_i : f o r edge i n edges : i f i s _ c o l o r i n g _ r e l e v a n t _ t o ( edge , hg ) : C_i_plus_1 . add ( hg | f r o z e n s e t ( [ e d g e ] ) ) r e t u r n C_i_plus_1 Die Erzeugung der Menge Ci+1 in Abhängigkeit von Ci (und den Kanten) geschieht indem zu jedem Hypergraph hg aus Ci (Zeile 66) bezüglich einer jeden Kante edge (Zeile 67) geprüft wird, ob sie zu hg Färbungsrelevant ist (Zeile 68). Ist edge färbungsrelevant zu hg, dann wird der Hypergraph hg + edge der Menge Ci+1 hinzugefügt (Zeile 69). Sind m1 und m2 beide vom Typ frozenset , dann liefert m1 | m2 ein frozenset , das die Vereinigung von m1 und m2 ist. Der Ausdruck frozenset ([ edge]) erzeugt ein Objekt vom Typ frozenset , das als einziges Element edge enthält. 64 65 66 67 68 69 70 71 72 73 74 d e f g e n e r a t e _ L _ i ( C_i , n o d e P e r m u t a t i o n s , mapToIndex , e d g e s ) : d e f Aut ( h y p e r g r a p h ) : automorphismGroup = s e t ( ) f o r np i n n o d e P e r m u t a t i o n s : agHg = f r o z e n s e t ( ) # new h y p e r g r a p h f o r Aut ( h y p e r g r a p h ) f o r edge i n hypergraph : agHg = agHg | f r o z e n s e t ( [ # we r e c y c l e e d g e s e d g e s [ mapToIndex [ f r o z e n s e t ( np [ node ] f o r node i n e d g e ) ] ] ] ) automorphismGroup . add ( agHg ) r e t u r n automorphismGroup 75 76 77 78 79 80 81 82 83 84 85 86 87 L_i = s e t ( ) f o r c a n d i d a t e i n C_i : A u t C a n d i d a t e = Aut ( c a n d i d a t e ) c a n d i d a t e S u c c e e d e d = True f o r h y p e r g r a p h _ f r o m _ L _ i i n L_i : i f hypergraph_from_L_i i n AutCandidate : candidateSucceeded = False break i f candidateSucceeded : L_i . add ( c a n d i d a t e ) y i e l d candidate return Zur Erzeugung der Repräsentanten mit i Kanten müssen wir uns die schon gefundenen Repräsentanten merken, um von einem weiteren Kandidaten entscheiden zu können, ob er aufgenommen wird. Deshalb initialisieren wir in Zeile 83 die Variable L_i mit der leeren Menge und verwenden sie, um uns die schon erzeugten Repräsentanten zu merken (Zeile 92). Um nun die Repräsentanten zu gewinnen, durchlaufen wir die Kandidaten mit i Kanten (Zeile 84). Zu jedem Kandidaten erzeugen wir dessen Automorphismengruppe (Zeile 85) und prüfen, ob sich einer der Hypergraphen aus L_i in dieser Automorphismengruppe befindet (Zeilen 87–90). Ist dies nicht der Fall (Zeile 91), dann ergänze L_i um diesen Kandidaten und gib ihn zurück. Für die Erzeugung der Automorphismengruppe eines Kandidaten durchlaufen wir alle Knotenpermutationen (Zeile 74). Für jede dieser Permutationen bilden wir alle Kanten des Hypergraphen (Zeile 76) unter der Permutation ab (Zeile 79) und fassen diese Bildkanten zu einem Hypergraphen der Automorphismengruppe zusammen (Zeile 77). 21 C.2 Klassische Methode mit Korollar 1 Hier ändert sich auf der Code-Ebene kaum etwas, aber die Geschwindigkeit für die Berechnung der Isomorphieklassenrepräsentanten der Hypergraphen mit 5 Knoten verzehnfacht sich. 1 2 #! / u s r / b i n / p y t h o n # −∗− c o d i n g : u t f −8 −∗− 3 4 5 6 7 8 9 10 11 12 13 14 d e f g e n e r a t e _ h y p e r g r a p h s ( numberOfNodes ) : e d g e s = c r e a t e _ e d g e s ( numberOfNodes ) mapToIndex = c r e a t e _ e d g e s _ i n d e x _ m a p p i n g ( e d g e s ) n o d e P e r m u t a t i o n s = c r e a t e _ n o d e _ p e r m u t a t i o n s ( numberOfNodes ) C_i = s e t ( [ f r o z e n s e t ( ) ] ) # i n i t i a l i z e C_i w i t h empty h y p e r g r a p h w h i l e C_i : L_i = s e t ( ) f o r hg i n g e n e r a t e _ L _ i ( C_i , n o d e P e r m u t a t i o n s , mapToIndex , e d g e s ) : L_i . add ( hg ) y i e l d hg C_i = c r e a t e _ C _ i _ p l u s _ 1 ( L_i , e d g e s ) Wir benutzen jetzt in Zeile 14 zur der Berechnung der Kandidaten mit i + 1 Kanten nicht mehr Ci als Ausgangsmenge sondern die Repräsentanten mit i Kanten, also Li . Aus diesem Grund müssen wir uns die Repräsentanten mit i Kanten merken, was zu den zusätzlichen Zeilen 10 und 12 führt. C.3 Klassische Methode mit Ordnung Unter Ausnutzung von Definition 3 und der übrigen Korollare von Korollar 1 bis Korollar 5 erhalten wir den folgenden Code. #! / u s r / b i n / p y t h o n 2 # −∗− c o d i n g : u t f −8 −∗− 1 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 d e f g e n e r a t e _ h y p e r g r a p h s ( numberOfNodes ) : e d g e s = c r e a t e _ e d g e s ( numberOfNodes ) mapToIndex = c r e a t e _ e d g e s _ i n d e x _ m a p p i n g ( e d g e s ) n o d e P e r m u t a t i o n s = c r e a t e _ n o d e _ p e r m u t a t i o n s ( numberOfNodes ) L_i = [ ] f o r hg i n g e n e r a t e _ L 1 ( e d g e s ) : L_i . append ( hg ) y i e l d hg w h i l e L_i : L_i_plus_1 = [ ] f o r hg i n g e n e r a t e _ L _ i _ p l u s _ 1 ( L_i , n o d e P e r m u t a t i o n s , mapToIndex , e d g e s ) : L_i_plus_1 . append ( hg ) y i e l d hg L_i = L_i_plus_1 Hier ändert sich nun einiges. Li wird nun nicht mehr als Menge sondern als Liste implementiert, damit die Ordnung garantiert werden kann. Wir erzeugen nun den Rekursionsanfang — die Repräsentanten mit einer Kante — separat. Der Grund dafür ist, dass der Code zur Erzeugung der 22 Repräsentanten mit i + 1 Kanten auf größte Kante eines Repräsentanten mit i Kanten zugreift. Dies ist natürlich beim leeren Hypergraph nicht möglich. Um nun nicht ständig auf den leeren Hypergraph prüfen zu müssen erzeugen wir L1 separat und haben dann für alle übrigen Li garantiert, dass die Graphen aus Li−1 wenigstens eine Kante, also insbesondere eine größte Kanten, enthalten. Nachdem in den Zeilen 9-11 L1 erzeugt wurde, werden die übrigen Hypergraphen erzeugt, so lange es zu einem Hypergraph aus Li noch wenigstens eine färbungsrelevante Kante existiert. Ci müssen wir in dieser Variante nicht mehr erzeugen, wir erzeugen Li+1 direkt aus Li (Zeile 14). Wurde Li+1 erzeugt, dann setzen wir Li auf Li+1 und setzen Li+1 für die nächste Runde wieder auf die leere Liste (Zeilen 18 und 13). C.3.1 Kantenerzeugung Die Kantenerzeugung gerät nun etwas aufwändiger und wird schon in Abschnitt 2.4 ausführlich besprochen. Da wir die Kanten geordnet erzeugen, können wir das Größenverhältnis zweier Kanten durch ihre Indizes in der geordneten Kantenauflistung feststellen. Deshalb können wir eine Kante weiterhin als ungeordnete Knotenmenge mit dem Typ frozenset speichern (Zeile 21). 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 d e f c r e a t e _ e d g e s ( numberOfNodes ) : def binary_vector_to_edge ( binVector ) : return f r o z e n s e t ( pos f o r pos i n range ( l e n ( b i n V e c t o r ) ) i f b i n V e c t o r [ pos ] ) d e f m_edges (m, n ) : edge , j , k , e d g e s = ( [ 1 ] ∗m + [ 0 ] ∗ ( n−m) , m−1, 0 , [ ] ) w h i l e not ( k == m) : e d g e s . append ( b i n a r y _ v e c t o r _ t o _ e d g e ( e d g e ) ) i f j < n −1: edge [ j ] = 0 j += 1 edge [ j ] = 1 else : k = 1 # c o u n t t h e o n e s a t t h e end o f t u p e l try : w h i l e e d g e [ n−k − 1 ] : k += 1 e x c e p t I n d e x E r r o r : # k == 0 : m == n return edges i = n − k − 1 # f i n d b i g g e s t i d x o f one n o t a t end try : w h i l e not e d g e [ i ] : i −= 1 e x c e p t I n d e x E r r o r : # k == m pass else : edge [ i ] = 0 e d g e [ n−k : n ] = [ 0 ] ∗ k e d g e [ i +1: i+k +2] = [ 1 ] ∗ ( k+1) j = i+k+1 return edges 50 51 edges = [ ] 23 52 53 54 f o r m i n r a n g e ( 2 , numberOfNodes +1): e d g e s += m_edges (m, numberOfNodes ) return edges Die Zuordnung der Kanten zu ihren Indizes erzeugen wir wie gehabt. 55 56 57 58 59 def create_edges_index_mapping ( edges ) : mapToIndex = d i c t ( ) for idx in range ( len ( edges ) ) : mapToIndex [ e d g e s [ i d x ] ] = i d x r e t u r n mapToIndex C.3.2 Hypergraphenerzeugung An der Verfahrensweise zur Erzeugung der Permutationen ändert sich auch nichts. 60 61 62 63 64 65 from i t e r t o o l s import p e r m u t a t i o n s d e f c r e a t e _ n o d e _ p e r m u t a t i o n s ( numberOfNodes ) : nodePermutations = [ ] f o r p e r m u t a t i o n i n p e r m u t a t i o n s ( r a n g e ( numberOfNodes ) ) : n o d e P e r m u t a t i o n s . append ( p e r m u t a t i o n ) return nodePermutations Die Erzeugung von L1 setzen wir um, indem wir die Kanten geordnet durchlaufen und jedes mal, wenn sich die Länge einer Kante ändert erzeugen wir einen neuen Repräsentanten mit der kleinsten Kanten ihrer Länge. 66 67 68 69 70 71 def generate_L1 ( edges ) : lastLength = 0 for edgeIdx in range ( len ( edges ) ) : i f len ( edges [ edgeIdx ] ) > lastLength : lastLength = len ( edges [ edgeIdx ] ) y i e l d tuple ( [ edgeIdx ] ) Hier zeigt sich schon, dass wir Hypergraphen weiterhin als Tuple, also als geordnete Mengen von Kanten, auffassen (Zeile 71). Wir benötigen zwar noch keinen Größenvergleich zwischen zwei Hypergraphen, aber zur Erzeugung neuer Kandidaten aus einem schon gewonnenen Repräsentanten benötigen wir dessen größte Kante. Diese ist einfach die letzte Kante des Repräsentanten (Zeile 90), wenn wir Hypergraphen entsprechend ihrer Konstruktion als geordnete Kantenmengen auffassen. 72 73 74 75 76 77 78 79 80 81 82 83 d e f g e n e r a t e _ L _ i _ p l u s _ 1 ( L_i , n o d e P e r m u t a t i o n s , e i d i c t , e d g e s ) : d e f Aut ( h y p e r g r a p h ) : automorphismGroup = s e t ( ) f o r np i n n o d e P e r m u t a t i o n s : agHg = t u p l e ( ) # new h y p e r g r a p h f o r Aut ( h y p e r g r a p h ) for edgeIdx in hypergraph : agHg = agHg + t u p l e ( [ # we r e c y c l e e d g e s eidict [ frozenset ( np [ e i ] f o r e i i n e d g e s [ e d g e I d x ] ) ] ] ) automorphismGroup . add ( t u p l e ( s o r t e d ( agHg ) ) ) r e t u r n automorphismGroup d e f i s _ c o l o r i n g _ r e l e v a n t _ t o ( edge , h y p e r g r a p h ) : 24 for edgeIdx in hypergraph : i f edges [ e d g e I d x ] . i s s u b s e t ( edge ) : ret urn F a l s e r e t u r n True 84 85 86 87 L_i_plus_1 = s e t ( ) f o r h y p e r g r a p h i n L_i : f o r e d g e I d x i n r a n g e ( ( h y p e r g r a p h [ −1]+1) , l e n ( e d g e s ) ) : i f i s _ c o l o r i n g _ r e l e v a n t _ t o ( edges [ edgeIdx ] , hypergraph ) : candidate = hypergraph + tuple ( [ edgeIdx ] ) A u t C a n d i d a t e = Aut ( c a n d i d a t e ) candidateFailed = False f o r h y p e r g r a p h _ f r o m _ L _ i _ p l u s _ 1 i n L_i_plus_1 : i f hypergraph_from_L_i_plus_1 i n AutCandidate : c a n d i d a t e F a i l e d = True break i f not c a n d i d a t e F a i l e d : L_i_plus_1 . add ( c a n d i d a t e ) y i e l d candidate 88 89 90 91 92 93 94 95 96 97 98 99 100 101 C.4 Orderly-Methode Hier ändert sich zum vorhergehenden Abschnitt nur noch die Funktion generate_L_i_plus_1. 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 d e f g e n e r a t e _ L _ i _ p l u s _ 1 ( L_i , n o d e P e r m u t a t i o n s , mapToIndex , e d g e s ) : def i s _ c a n d i d a t e _ c a n o n i c a l ( ) : d e f i s _ s m a l l e r _ t h e n _ c a n d i d a t e ( agHg ) : idx = 0 f o r e d g e I d x i n agHg : i f edgeIdx > candidate [ idx ] : return False i f edgeIdx < candidate [ idx ] : r e t u r n True i d x += 1 return False f o r np i n n o d e P e r m u t a t i o n s : agHg = t u p l e ( ) # new h y p e r g r a p h f o r Aut ( h y p e r g r a p h ) for edgeIdx in candidate : agHg = agHg + t u p l e ( [ # we r e c y c l e e d g e s mapToIndex [ f r o z e n s e t ( np [ node ] f o r node i n e d g e s [ e d g e I d x ] ) ] ] ) i f i s _ s m a l l e r _ t h e n _ c a n d i d a t e ( s o r t e d ( agHg ) ) : return False r e t u r n True def i s _ c o l o r i n g _ r e l e v a n t _ t o _ h y p e r g r a p h ( edge ) : for edgeIdx in hypergraph : i f edges [ e d g e I d x ] . i s s u b s e t ( edge ) : retu rn F a l s e r e t u r n True 96 97 98 99 f o r h y p e r g r a p h i n L_i : f o r e d g e I d x i n r a n g e ( ( h y p e r g r a p h [ −1]+1) , l e n ( e d g e s ) ) : i f is_coloring_relevant_to_hypergraph ( edges [ edgeIdx ] ) : 25 100 101 102 candidate = hypergraph + tuple ( [ edgeIdx ] ) i f is_candidate_canonical (): y i e l d ( candidate ) Hier fällt sofort ins Auge, dass im Vergleich zur klassischen Methode die Menge L_i_plus_1 nicht mehr benötigt wird und der damit verbundene Test, ob schon ein anderer Repräsentant erzeugt wurde, fällt ebenfalls weg. Dafür muss nun die Eigenschaft is_canonical berechnet werden. Setzen wir schließlich etwas des durch die Orderly-Methode gewonnenen Speichers an der richtigen Stelle wieder ein, dann erhalten wir noch eine weitere spürbare Effizienzsteigerung. Dazu wird neben der generate_L_i_plus_1-Funktion auch noch die Erzeugung der permutationen angepasst. Ein Element der Permutationen-Liste ist nun ein Paar bestehend aus der Permutation und einer leeren Zuordnung. In dieser Zuordnung wird im weiteren Verlauf ein Kantenindex dem Kantenindex seines Bildes unter der Permutation zugeordnet. Dies geschieht natürlich nicht für alle Kanten im Vorfeld sondern immer nur für die Kanten die auch tatsächlich unter einer Permutation schon einmal zugeordnet wurden, sodass die sich wiederholenden Berechnungen der Bildkanten für die Berechnung der Automorphismengruppe weg fallen. 60 61 62 63 64 65 66 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 from i t e r t o o l s import p e r m u t a t i o n s d e f c r e a t e _ n o d e _ p e r m u t a t i o n s ( numberOfNodes ) : nodePermutations = [ ] f o r p e r m u t a t i o n i n p e r m u t a t i o n s ( r a n g e ( numberOfNodes ) ) : n o d e P e r m u t a t i o n s . append ( t u p l e ( [ permutation , d i c t ( ) ] ) ) return nodePermutations d e f g e n e r a t e _ L _ i _ p l u s _ 1 ( L_i , n o d e P e r m u t a t i o n s , mapToIndex , e d g e s ) : d e f i s _ s m a l l e r _ t h e n ( agHg , c a n d i d a t e ) : f o r i d x i n r a n g e ( l e n ( agHg ) ) : i f agHg [ i d x ] > c a n d i d a t e [ i d x ] : r e t u r n F a l s e i f agHg [ i d x ] < c a n d i d a t e [ i d x ] : r e t u r n True return False # i d e n t i c a l f o r np i n n o d e P e r m u t a t i o n s : agHg = t u p l e ( ) # new h y p e r g r a p h o f Aut ( h y p e r g r a p h ) for edgeIdx in candidate : t r y : agHg += np [ 1 ] [ e d g e I d x ] except : np [ 1 ] [ e d g e I d x ] = t u p l e ( [ # we r e c y c l e e d g e s mapToIndex [ f r o z e n s e t ( np [ 0 ] [ n i ] f o r n i i n e d g e s [ e d g e I d x ] ) ] ] ) agHg += np [ 1 ] [ e d g e I d x ] i f i s _ s m a l l e r _ t h e n ( s o r t e d ( agHg ) , c a n d i d a t e ) : return False r e t u r n True d e f i s _ c o l o r i n g _ r e l e v a n t _ t o ( edge , h y p e r g r a p h ) : for edgeIdx in hypergraph : i f edges [ e d g e I d x ] . i s s u b s e t ( edge ) : retu rn F a l s e r e t u r n True 94 95 96 97 f o r h y p e r g r a p h i n L_i : f o r e d g e I d x i n r a n g e ( ( h y p e r g r a p h [ −1]+1) , l e n ( e d g e s ) ) : i f i s _ c o l o r i n g _ r e l e v a n t _ t o ( edges [ edgeIdx ] , hypergraph ) : 26 98 99 100 candidate = hypergraph + tuple ( [ edgeIdx ] ) i f is_canonical ( candidate ): y i e l d ( candidate ) Nun versuchen wir in Zeile 81 einem Kantenindex direkt dessen Bild bezüglich einer Permutation zuzuordnen. Gelingt dies nicht, wird eine Ausnahme ausgelöst und wir Speichern die im Ausnahmeblock berechnete Zuordnung (Zeilen 83–86). 27 Literatur [BL83] László Babai and Eugene M. Luks. Canonical labeling of graphs. In 15th ACM Symp. on Theory of Computing, pages 171 – 183. ACM, 1983. [HG03] Thomas F. Hain and Geof Goldbogen. Determination of graph isomorphism by the greatest characteristic string invariant. In 41st Annual ACM Southeast Conference, Georgia, USA, March 2003. [McK81] B. D. McKay. Practical graph isomorphism. In 10th. Manitoba Conference on Numerical Mathematics and Computing, volume 30 of Congressus Numerantium, pages 45 – 87, 1981. [Rea78] Ronald C. Read. Every one a winner or how to avoid isomorphism search when cataloguing combinatorial configurations. In P. Hell B. Alspach and D.J. Miller, editors, Algorithmic Aspects of Combinatorics, volume 2 of Annals of Discrete Mathematics, pages 107 – 120. Elsevier, 1978. [Vol02] Vitaly I. Voloshin. Color mixed hypergraphs: theory, algorithms, and applications. American Mathematical Society, 2002. [Vol09] Vitaly I. Voloshin. Introduction to Graph and Hypergraph Theory. Nova Science Publishers Inc, 2009. 28 Index E(H), 4 E(H) ⊂ e, 5 H + e, 5 H < e, 11 Nn , 3 V (H), 4 dom(f ), 3 En , 7 img(f ), 3 ran(f ), 3 e < H, 12 e ⊂ E(H), 5 Automorphismengruppe, 6 einfach, Hypergraph, 4, 7 färbbar, 6 Färbung, 6 färbungsrelevant, 7 Größe, Hypergraph, 4 Grad, Kante, 5 Grad, Knoten, 5 isomorph, 5 kanonischer Repräsentant, 21 Kante, 4 Kantenmenge, 4 Kantenordnung, 11 Knoten, 4 Knotenmenge, 4 Länge, 12 leerer Hypergraph, 4 maximal, Teilfärbung, 6 Orderly Method, 19 Ordnung, 4 Permutation, 3 Rang, 5 regulär, 5 Teilfärbung, 6 uniform, 5 29