www.mathematik-netz.de © Copyright, Page 1 of 8 Einführung in die Graphen-Theorie 1.0 Grundlegende Definitionen Die Theorie der Graphen, das in faszinierender Weise Anwendungen und Theorie, Anschaulichkeit und trickreiche Methoden miteinander verbindet ist nicht nur in der Mathematik sondern auch in der Informatik von großer Bedeutung. Wer kennt nicht das „Das Haus vom Nikolaus“ und wollte schon immer wissen, warum sich ein Erfolg nur dann einstellen kann, wenn man an vorher genau bestimmbaren Ecken mit seiner Zeichnung startet. Ein Graph stellt eine Menge von Objekten zusammen mit einer Beziehung (Relation) auf diesen Objekten dar. Wir betrachten einige Beispiele: (a) (b) (c) (d) Objekte: Personen; Beziehung: Person A kennt Person B. Spieler eines Tennisturniers; A spielt gegen B. Städte; es gibt eine Autobahn zwischen A und B. Stellungen im Schachspiel; Stellung A lässt sich durch einen Zug in Stellung B überführen. Die Bezeichnung Graph stammt von der üblichen „graphischen“ Visualisierung: Objekte werden als Knoten Beziehungen (Relationen) als Kanten dargestellt. Diese Kanten können dabei gerichtet sein (einen einfachen bzw. doppelten Pfeil) ungerichtet sein ( einen Strich ). Entsprechend unterscheidet man auch zwischen einem gerichteten und einem ungerichtetem Graphen. Die Platzierung von Ecken (oft auch Knoten genannt) bzw. Kanten in einer graphischen Darstellung spielt keine Rolle, d.h. es gibt für dieselbe Relation verschiedene Darstellungsmöglichkeiten. Definition: (Digraph= directed graph) Ein gerichteter Graph ist ein Paar G1=(V, E), wobei gilt: V ist eine endliche, nichtleere Menge (die Elemente werden Knoten genannt) ist. E ist eine endliche Menge von Kanten wobei gilt, E ⊆ V × V. Eine Kante ist also ein Paar von Knoten; ist e=<v, w> ∈ E, dann heißt v der Anfangsknoten von e und w der Endknoten von e. Kanten der Form <v,v> werden auch Schleifen genannt. , Definition: Ein ungerichteter Graph ist ein Paar G2=(V, E), wobei gilt: V ist eine endliche, nichtleere Menge (die Elemente werden Knoten genannt) ist. E ist eine ungeordnete endliche Menge von Kanten {v,w} mit v, w ∈ V und v ≠ w. Im ungerichteten Fall können die Kanten auch als zweielementige Knotenmenge aufgefasst werden. Üblich ist jedoch die Tupelschreibweise (v, w) anstelle von {v, w}. Für ungerichtete Graphen gilt offensichtlich (v, w)=(w, v). , Definition: (Grad) Der Grad eines Knotens ist die Gesamtzahl der eingehenden und ausgehenden Kanten; entsprechend spricht man vom Eingangsgrad bzw. vom Ausgangsgrad. , www.mathematik-netz.de © Copyright, Page 2 of 8 Definition: (Pfad) Ein Pfad ist eine Folge von Knoten p:=v1, …, vn, so dass für 1 ≤ i ≤ n-1 gilt: (vi, vi+1) ∈ E. Die Länge L des Pfades p ist die Anzahl der Kanten auf dem Pfad; also L(p)=n-1. Ein einzelner Knoten v1 stellt einen Pfad der Länge 0 dar. Ein Pfad p heißt einfach, wenn alle Knoten auf dem Pfad paarweise verschieden sind. Dabei ist als Ausnahme v1=vn erlaubt. Ein Knoten w heißt von Knoten v erreichbar (in G), wenn es einen Pfad q:=v0, v1, . . . , vr in G gibt, so daß v0 = v und vr = w. , Wir bezeichnen Nachfolgermengen bzw. Vorgängermengen künftig wie folgt: Post(v) = {w ∈ V: (v,w) ∈ E} bezeichnet die Menge aller (direkten) Nachfolger von v, Pre(v) = {w ∈ V: (w, v) ∈ E} = {w ∈ V : v ∈ Post(w)} die Menge aller (direkten) Vorgänger von v, Post* (v) = {w ∈ V: w ist von v in G erreichbar} dieMenge aller von v erreichbaren Knoten, Pre*(v) = {w ∈ V: v ∈ Post*(w)} die Menge aller Knoten, die v erreichen können. In ungerichteten Graphen werden zwei Knoten v, w, die über eine Kante miteinander verbunden sind (d.h. (v,w) ∈ E), auch benachbart genannt. , Definition: (Zyklus) Ein einfacher Pfad p:=v1, …, vn in einem gerichteten Graphen, dessen Länge mind. 1 (d.h. n>0) ist und in dem der erste und der letzte Knoten identisch sind (v1=vn) heißt einfacher Zyklus. Ein einfacher Pfad p:=v1, …, vn in einem ungerichteten Graphen, dessen Länge mind. 3 (d.h. n>2) ist und in dem der erste und der letzte Knoten identisch sind (v1=vn) heißt einfacher Zyklus. Ein Zyklus ist ein Pfad p:=v1, v2, …, vm der sich aus einfachen Zyklen zusammensetzt. Jeder einfache Zyklus ist also ein Zyklus. Ein Graph G heißt azyklisch (oder zyklenfrei oder kreisfrei), falls es keine Zyklen in G gibt. Man verwendet auch oft die Abkürzung DAG für „directed acyclic graphs“ (also zyklenfreie Digraphen). Häufig spricht man auch von der Zyklenfreiheit einer Kantenmenge E’ ⊆ E und meint damit die Zyklenfreiheit des Teilgraphen G’=(V, E’). , Definition: (Stark verbunden) Zwei Knoten v und w eines gerichteten Graphen G=(V, E) heißen stark verbunden (strongly connected), wenn es einen Pfad von v nach w und einen Pfad von w nach v gibt. Eine stark verbundene Komponente (oder starke Komponente) ist ein Teilgraph mit maximaler Knotenzahl, in dem alle Knoten paarweise verschieden sind. Für eine starke Komponente G1’=(V’, E’) gilt also: Es gibt keinen Teilgraphen G1’’=(V’’, E’’) mit V ⊇ V’’ ⊇ V’ oder E ⊇ E’’ ⊇ E’, der ebenfalls eine starke Komponente ist. Ein gerichteter Graph G, der nur aus einer einzigen starken Komponente G’ besteht, heißt stark verbundener Graph. In der Literatur wird auch anstatt der Begriffe „stark verbunden“ und „stark verbundene Komponente“ synonym „zusammenhängend“ und „Zusammenhangskomponente“ verwendet. , www.mathematik-netz.de © Copyright, Page 3 of 8 Nebenstehend sind jeweils einfache Beispiele zu einem gerichteten und einem ungerichtetem Graph skizziert. Dabei deutet man die Ausrichtung durch Pfeile von einem Anfangsknoten zum Endknoten an. Der gerichtete Graph weist nur einen (einfachen) Zyklus auf, nämlich den Pfad (x, y). Die Knoten x und y im gerichteten Graph sind stark verbunden, da (x, y), (y, x) ∈ E. Der gerichtete Graph ist jedoch nicht stark verbunden. Im oben stehenden gerichteten Beispielgraphen ist Post(v)={w, x}, Pre(v)= ∅ sowie Post*(v)={v, w, x, y}, Pre*(v)= ∅ . Stellt man weitere Forderungen an stark verbundene Graphen so kommt man zu dem Begriff Clique; sei also G=(V,E) ein ungerichteter Graph ohne Mehrfachkanten und U eine Teilmenge von V. Man bezeichnet U als Clique von G, wenn für je zwei beliebige verschiedene Knoten v und w aus U gilt, dass sie durch eine Kante miteinander verbunden sind. Nebenstehend ist eine mögliche Form einer Clique mit 4 Knoten skizziert. Man kann leicht erkennen, dass jeder einzelne Knoten mit den übrigen drei Knoten eine direkte Verbindung besitzt. Cliquen spielen eine bedeutende Rolle im Kontext der Auffindung einer knotenüberdeckenden Menge oder aber auch die Komplexitätstheorie. 2.0 Graphendurchlauf - Traversierungsmethoden Ein erstes Problem, auf dessen Lösung weitere Graph-Algorithmen aufbauen, besteht darin, systematisch alle Knoten eines Graphen aufzusuchen. Traversierung [spätlat. transvertere = umwenden] ist ein Sammelbegriff für Verfahren, die eine Route bestimmen, bei der jeder Knoten und jede Kante eines Graphen genau einmal besucht werden. Es gibt zwei wesentliche Arten, Graphen zu durchlaufen, nämlich Tiefendurchlauf (depth-first-search), Breitendurchlauf (breadth-first-search). Beide Verfahren verwenden zwei Mengen von Knoten: Eine Menge Visited, welche alle bereits besuchten Knoten enthält. Eine Teilmenge W von Visited, welche den „Rand“ (oder auch Hülle) der bereits besuchten Knoten verwaltet. Ein Knoten w ∈ Visited gehört genau dann zur Randmenge, wenn für diesen noch nicht alle ausgehenden Kanten benutzt wurden. Ein Knoten aus der Grundmenge aller Knoten gilt als besucht, sobald dieser in die Menge Visited eingefügt wird. Eine Kante gilt als benutzt, sobald diese als Teilweg eines Pfades zu einem (zu besuchenden) Knoten dient. Ein Knoten v heißt vollständig expandiert, wenn alle von v ausgehenden Kanten benutzt wurden. www.mathematik-netz.de © Copyright, Page 4 of 8 Die Tiefensuche wie auch die Breitensuche verwenden beide das folgende Grundgerüst als Algorithmus: algorithm Travers(v) /* Travers besucht alle direkten Nachbarn von v, welche bisher noch nicht besucht wurden */ W:={v} // W verwaltet die noch nicht vollständig expandierten Knoten, W ist die Randmenge , WHILE W != ∅ DO Wähle einen Knoten w aus W; IF es gibt keine von w ausgehende, unbenutzte Kante THEN W:=W\{w} ELSE Wähle eine unbenutzte Kante (w, u) und markiere diese als benutzt; IF u ∈ / Visited THEN Füge u in Visited und W ein; FI FI OD Beipsiel: In diesem Beispiel werden wir den eben vorgestellten Algorithmus Travers(v) näher untersuchen. Wir nehmen vereinfachend an, alle Knoten des Graphen seien von einer Wurzel r aus erreichbar. Einen solchen Graphen nennt man Wurzelgraphen. Untenstehender Graph ist ein solcher Wurzelgraph, wobei A die Wurzel ist. Von A aus müssen alle Knoten des Graphen erreichbar sein. Wir überprüfen das. A B A→B A→C A→D A→E : : : : Pfad Pfad Pfad Pfad (A, (A, (A, (A, B). C). C, B, D). C, E). C D E Als initiale Mengen werden Visited:= ∅ und W:= ∅ . Sodann rufen wir Travers(A) auf und der erste Befehlt setzt W:={A}; da W also nichtleer ist wird die WHILE-Schleife ausgeführt. Wir wählen (evtl. nach einer konkret vorgegebenen Reihenfolge) einen Knoten w aus W – dies kann nur A sein und überprüfen, ob es keine von A ausgehende, unbenutzte Kante gibt. Dies ist nicht der Fall, daher greift der ELSE-Zweig: wir wählen eine unbenutzte Kante, z.B. (A, C), und fügen C der Menge Visited und W hinzu. Es ergeben sich also die aktualisierten Mengen Visited:={A, C} und W={A, C}. Im nächsten Schleifendurchlauf wird nun die noch einzig unbenutzte Kante (A, B) gewählt und damit B der Menge Visited und W hinzugefügt. Im darauffolgenden Schleifendurchlauf greift nun jedoch die bedingte Anweisung, denn es gibt nun in der Tat keine unbenutzte Kante von A aus. Deshalb wird A aus der Randmenge W entfernt. Analog werden die übrigen noch in W enthaltenen Knoten abgearbeitet, bis schließlich alle von A aus erreichbaren Knoten und Kanten abgearbeitet sind. www.mathematik-netz.de © Copyright, Page 5 of 8 , Durch den Algorithmus Travers(A) gestartet mit einer Besuchsmenge Visited, welche die Eigenschaft hat Visited ∩ Post(A)={v} besitzt, werden genau die von v erreichbaren Knoten besucht. Da der Graph in obigem Beispiel ein Wurzelgraph ist, werden also alle Knoten besucht und alle Kanten benutzt. Ist der zu untersuchende Graph jedoch kein Wurzelgraph, hat dieser also mehrere Zusammenhangskomponenten, so müssen alle Knoten v des Graphen als Startknoten aufgerufen werden, nur dann ist sichergestellt, dass der Graph vollständig durchlaufen wird. In den folgenden Algorithmen wird dies mit der Anweisung FOR ALL Knoten v DO bewerkstelligt. Sinnvoller, als sich jede bereits benutzte Kante zu merken, ist es, sich eine konkret abzulaufende Reihenfolge der Kanten vorzugeben. Dies kann man z.B. durch eine einfache Adjazenzliste realisieren. Der Tiefen- und Breitendurchlauf unterscheiden sich nun lediglich in der Art der Datenstruktur, die für die Randmenge W verwendet wird: Für einen Breitendurchlauf wird eine Queue, für einen Tiefendurchlauf ein Stack (Keller) verwendet. algorithm BreitenSuche(Graph G, Startknoten v) Visited:= ∅ ; Initialisiere W als leere Warteschlange; FOR ALL Knoten v DO IF v ∈ / Visited THEN füge v in Visited ein; ADD(W, v); WHILE W != ∅ DO w := FRONT(W); REMOVE(W); /* Durchlaufe die Adjazenzliste von w und */ /* füge alle noch nicht besuchten Söhne von w inW ein */ FOR ALL u ∈ Post(w) DO IF u ∈ / Visited THEN füge u in Visited ein; ADD(W,u) FI OD OD FI , OD Beispiel: Gegeben seien der abgebildete Graph und die Adjazenzliste. Wir durchlaufen den Graphen mit Hilfe des Algorithmus BreitenSuche(). Als Startknoten verwenden wir v1. Zu Beginn des Algorithmus sind Visited= ∅ und W= ∅ . Da vom Startknoten aus jeder Knoten erreichbar ist, können wir die äußere FOR ALL-Anweisung ignorieren. www.mathematik-netz.de © Copyright, Page 6 of 8 Wir notieren die Priorität innerhalb der Queue (FIFO=first in first out) dadurch, dass das Element mit höchster Priorität an erster Stelle von links aus steht. So ist also Front(W’)=a für W’={a, b, c}. Da der Startknoten noch nicht als besucht markiert ist, wird dieser zunächst der Menge Visited und der Menge W hinzugefügt. Daraufhin durchlaufen wir die WHILE-Schleife zum ersten Mal. Als erstes besuchtes Element wird daher v1 ausgegeben und aus der Queue W entfernt. Aus der Adjazenzliste ersehen wir, dass der Knoten v1 die Nachbarn v3, v6 und v2 besitzt, diese werden der Queue in dieser Reihenfolge hinzugefügt. Darüber hinaus werden sie als besucht markiert, d.h. V hinzugefügt. Nach diesem Schleifendurchlauf erhalten wir also die Mengen Visited={v1, v3, v6, v2} und W={v3, v6, v2}. Als nächstes verfahren wir analog mit dem Element v3, da dies das vorderste Element innerhalb der Queue W ist. Es wird also v3 ausgegeben und aus der Warteschlange W entfernt. Sodann betrachten wir alle Nachbarn von v3, dies ist lediglich der Knoten v6. Allerdings wird v6 weder Visited noch W hinzugefügt, da dieser Knoten bereits markiert ist. Nach diesem Schleifendurchlauf erhalten wir also die Mengen Visited={v1, v3, v6, v2} und W={v6, v2}. Deshalb fahren wir mit v6 analog fort, usw.! Als Ausgabe erhält man schließlich v1, v3, v6, v2 v7, v4 und v5. , Als nächstes untersuchen wir die Tiefensuche, doch viel Neues können wir dabei nicht erwarten. Schließlich ändert sich nur die Datenstruktur für die Randmenge W und damit einige Operationen. algorithm TiefenSuche(Graph G, Startknoten v) Visited:= ∅ ; Initialisiere W als leere Keller; FOR ALL Knoten v DO IF v ∈ / Visited THEN füge v in Visited ein; Push(W, v); WHILE W != ∅ DO w := TOP(W); IF das Ende der Adjazenzliste von w ist erreicht THEN POP(W); ELSE u:= nächster Knoten in der Adjazenzliste von w; IF u ∈ / Visited THEN füge u in Visited ein; PUSH(W,u) FI FI OD FI , OD Der Algorithmus kann auch in einfacher und natürlicher Weise in einen rekursiven Algorithmus umgeschrieben werden. Wir erinnern uns, dass ein rekursiv formulierter Algorithmus letztlich den internen Stack des Rechners verwendet. Deshalb ist es auch nur sinnfällig, dass der Algorithmus TiefenSucheRek keine Datenstruktur für die Randmenge benötigt. , algorithm TiefenSucheRek(Graph G, Startknoten v) Visited:= ∅ ; FOR ALL Knoten v DO IF v ∈ / Visited THEN TiefenSucheRek(Graph G, v); FI OD www.mathematik-netz.de © Copyright, Page 7 of 8 Der Algorithmus TiefenSucheRek entspricht einem Backtracking-Algorithmus, wobei v ∈ / Visited der Randbedingung entspricht. Wir laufen also entsprechend der Adjazenzliste solange durch den Graphen, bis wir auf ein bereits besuchtes Element stoßen; sodann springen wir eine rekursive Stufe zurück und fahren mit dem zugehörigen nächsten Element aus der Adjazenzliste weiter. Beispiel: Gegeben seien wieder der abgebildete Graph und die Adjazenzliste aus dem letzten Beispiel. Diesmal durchlaufen wir den Graphen mit Hilfe des Algorithmus TiefenSucheRek(). Als Startknoten wählen wir diesmal den Knoten v2. Der erste Nachbar von v2 ist v3; deshalb wechseln wir in den Listenkopf von v3, also in die dritte Zeile von oben. Natürlich markieren wir den Knoten v3 als besucht. Der einzige Nachbar von v3 ist v6, wir markieren v6 als besucht und wechseln in die sechste Zeile von oben. Entsprechend erreichen wir v7 und v5. Im Anschluss daran enthält die Menge Visited = {v2, v3, v6, v7, v5}. Der einzige Nachbar von v5 ist v6, doch v6 ist bereits als besucht markiert. Hier also wurde die Randbedingung erreicht, deshalb springen wir zur letzten bearbeiteten Zeile zurück, also zur Zeile sieben. Doch auch hier sind bereits alle Nachbarn als besucht markiert. Letztlich springen wir zurück bis zur zweiten Zeile, in welcher v2 (der Startknoten) den Listenkopf darstellt. Der Knoten v4 wurde bisher noch nicht besucht, deshalb springen wir in die vierte Zeile. Unmittelbar stellen wir fest, dass bereits alle Nachbarn von v4 abgearbeitet sind. Letztlich ergibt sich also die Ausgabereihenfolge v2, v3, v6, v7, v5 und v4, für die erste Zusammenhangskomponente. Will man alle Knoten besuchen, so müsste man den Algorithmus erneut mit v1 als Startknoten initiieren. , Da rekursive Strukturen in natürlicher Weise auf Bäumen operieren, ist es naheliegend dies auch konsequent für die Tiefensuche zu tun. Dazu benötigen wir noch eine Definition: Die Expansion X(v) eines Graphen G in einem Knoten v ist ein Baum, der wie folgt definiert ist: (i) (ii) Falls v keinen Nachfolger hat, so ist X(v) der Baum, der nur aus dem Knoten v besteht. Falls v1, …, vk die Nachfolger von v sind, so ist X(v) der Baum [v, X(v1), …, X(vk)]. Dabei kann die Reihefolge der Nachbarn bspw. wieder durch eine Adjazenzliste angegeben werden. , Beispiel: 1 1 2 2 1 4 3 4 2 4 # # Expansion X von G1 5 Graph G1 4 5 # 5 3 # www.mathematik-netz.de © Copyright, Page 8 of 8 Für den Graphen G1 ist die Expansion im Knoten 1 ein Baum mit der Wurzel 1 und den Teilbäumen X(v1), …, X(vk), wobei vi ∈ G1 sind. Dieser Baum stellt also die Menge aller vom Knoten 1 ausgehenden Pfade in G dar. In G1 ist ein Zyklus z(1 → 2, 2 → 1, 1 → 2, …) enthalten, d.h. die Expansion X1 von G1 ist unendlich. , Es ist evident, dass der durch die Expansion erzeugte Baum unendlich ist, falls G Zyklen enthält. Ein Tiefendurchlauf eines Graphen G entspricht nun gerade einem Preorder-Durchlauf der Expansion von G, der jeweils in einem bereits besuchten Knoten von G abgebrochen wird. Zur Erinnerung, ein PreorderDurchlauf beschreibt ganz grob formuliert folgenden Pfad: Wurzel, linker Teilbaum, rechter Teilbaum. Für den Graphen G1 ergibt sich dann folgendes Bild. 1 2 4 1 2 4 # # 4 5 5 3 Der Durchlauf startet in der Wurzel. Anschließend wird der linke Teilbaum durchlaufen, solange bis wir auf einen bereits besuchten Knoten stößt. Die ist hier beim Knoten mit der Beschriftung 1 der Fall. Der rekursive Aufruf bricht ab und der rechte Teilbaum wird durchlaufen. 3 3 5 , Definition: Ein Spannbaum ist eine Menge von Kanten, die alle Knoten verbindet. , Definition: Der minimale Spannbaum hat zudem die Eigenschaft, das die Summe der Kantengewichte 'kleiner gleich' jeder beliebigen anderen Menge von Kanten ist, die alle Knoten verbindet. Hierbei hat eine Kante ein Gewicht, was der Länge der Strecke oder der Fahrzeit entspricht. , Dabei wollen wir es für den Anfang auch belassen. Ich wünsche viel Spaß auf http://mathematik-netz.de ! Anmerkung: Diese Ausarbeitung basiert zu Teilen auf dem Skript aus der Vorlesung Informatik IV, Prof. Dr. Chr. Baier, der Uni Bon.