Algorithmen auf Graphen Gruppe F - Übungsblatt 3 - Aufgabe 3 1 Kurze Einführung In der Vorlesung wurde bereits ein Algorithmus vorgestellt, mit dem man die starken Zusammenhangskomponenten eines gerichteten Graphen findet. Im wesentlichen läuft dieser Algorithmus in drei Stufen ab: 1. Durchlaufe den Graphen mittels Depth-First-Search, sobald ein Knoten fertig bearbeitet wurde, vergebe eine (aufsteigende) DFS-Id. 2. Drehe die Orientierung aller Kanten im Graphen um. 3. Ausgehend von der höchsten DFS-Id durchlaufe den Graphen wieder mittels DFS: die Zusammenhangskomponenten dieses DFS-Durchlaufs sind die starken Zusammenhangskomponenten des Graphen. Der Algorithmus funktioniert ohne Probleme und ist auch von der Laufzeit her linear, es gibt jedoch einige Schwächen: • DFS muss zwei Mal auf den gegebenen Graphen aufgerufen werden. • Das Umdrehen der Orientierung der Kanten benötigt ebenfalls linearen Aufwand. Im Folgenden wird nun ein Algorithmus besprochen, der einen gegebenen Graphen nur einmal mittels DFS durchlaufen muss. Um jedoch nachvollziehen zu können, welche der Knoten zu derselben Zusammenhangskomponente gehören, wird dafür ein Stack verwendet (mit den wie üblich definierten Operationen push und pop). Die Grundidee des in Kapitel 2 vorgestellten Algorithmus ist, für jeden Knoten seinen so g e n a n n t e n r o o t -Kn o t e n z u s p e i c h e r n u n d ü b e r d i e s e n r o o t -Kn o t e n d i e Zusammenhangskomponenten des Graphen nachzuvollziehen. 2 Algorithmus Der eigentliche Algorithmus, der die starken Zusammenhangskomponenten eines gerichteten Graphen findet, ist in Algorithm1 und Algorithm2 dargestellt. Damit der Algorithmus funktioniert, sind - wie im Pseudocode auf der nächsten Seite oben zu sehen - zwei zusätzliche Felder pro Knoten zu speichern: • root: speichert den Vorgänger des aktuellen Knotens • InComponent: wird in der aktuellen Implementierung auf true gesetzt, sobald die jeweilige SZK ausgegeben wird Bauer, Ebner, Moser, Schauer 1 21. Januar 2004 Algorithm l PowerDFS(v) 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: root[v]:=v; InComponent[v]:=false; push(v,stack); for all node w with (v,w)0 E do if w is not visited then PowerDFS(w); end if; if not InComponent[w] then root[v]:= MIN(root[v],root[w]); end if; end for; if root[v]==v then repeat w:=pop(stack); InComponent[w]:=true; until w==v; end if; Algorithm 2 main(v) 1: 2: 3: 4: 5: 6: stack:=nil; for all node v 0 V do if v not already visited then PowerDFS(v); end if end for Bauer, Ebner, Moser, Schauer 2 21. Januar 2004 Die wichtigsten Abschnitte von PowerDFS sind dabei die folgenden: Zeile 7-9 Abhängig davon, ob der Knoten w bereits einmal besucht wurde, wird die Variable root[v] entsprechend gesetzt: wurde w noch nicht als Knoten einer SZK identifiziert, ist root[v] derjenige der beiden Knoten v und w, welcher als erster in einem PowerDFS-Durchlauf erreicht wurde (die Funktion MIN(v,w) liefert v zurück, falls v zuerst durchlaufen wurde, und w sonst). Zeile 12-15 Gilt root[v]==v, so ist v entweder ein einzelner Knoten (d.h. bildet alleine eine SZK), oder es wurde ein Kreis (oder mehrere Kreise) entdeckt, die zusammen eine SZK bilden. Alle Knoten, die sich bis zur Wurzel auf dem Stack befinden, bilden diese SZK und werden in Zeile 13 ausgegeben. Die Laufzeit des gesamten Algorithmus ist ebenfalls wieder linear. In main(v) werden alle Knoten des Graphen einmal aufgerufen. Es wird aber nur für noch nicht besuchte Knoten PowerDFS(v) aufgerufen. In PowerDFS(v) wird über alle Kanten (v,w), die von v weggehen, iteriert. Wobei für noch nicht besuchte Knoten w PowerDFS(w) erneut aufgerufen wird. Daher werden diese Knoten aber in main(v) ignoriert. Zuletzt werden alle Knoten noch aus dem Stack gelöscht, was wiederum linearen Aufwand benötigt. Die Korrektheit des Algorithmus kann man durch folgende Überlegung zeigen. Der erste Knoten einer SZK wird von main(v) aus aufgerufen und repräsentiert die Wurzel der gesamten SZK (root[v]: = v). Von v aus werden alle Kanten und damit Knoten innerhalb der SZK rekursiv besucht, wobei v immer als eindeutige Wurzel der SZK identifiziert werden kann. Daher wird jeder Knoten in der SZK stets derselben Wurzel zugewiesen und auf den Stack gepusht. Nachdem die gesamte SZK aufgefunden wurde, werden alle - zur SZK gehörigen - Knoten vom Stack entfernt. Ein erneutes besuchen eines Knotens ist nicht mehr möglich, da nur nicht besuchte Knoten im Algorithmus berücksichtigt werden. Daher können alle Knoten immer nur ihrer eigenen SZK zugewiesen werden. 3 Beispiel Die Funktionsweise des Algorithmus soll an dieser Stelle noch durch ein Beispiel verdeutlicht werden und wird anhand des Graphen in Abbildung 1 vorgeführt. In der nachstehenden Tabelle werden die einzelnen Rekursionsschritte nachvollzogen und der entsprechende Inhalt der Variablen angegeben. Bauer, Ebner, Moser, Schauer 3 21. Januar 2004 Abbildung 1 PowerDFS root stack PowerDFS(a) root[a] = a a PowerDFS(c) root[c] = c ca PowerDFS(d) root[d] = d dca PowerDFS(b) root[b] = a bdca b kann auf nichts Neues verzweigen PowerDFS(f) root[f] = f fbdca f ist eine SZK und wird ausgegeben PowerDFS(d) root[d] = a bdca d kann auf nichts Neues verzweigen PowerDFS(c) root[c] = a bdca c kann auf nichts Neues verzweigen PowerDFS(e) root[e] = a ebdca e kann auf nichts Neues verzweigen PowerDFS(a) root[a] = a ebdca a ist Wurzel, SZK wird ausgegeben PowerDFS(g) root[g] = g g PowerDFS(i) root[i] = i ig PowerDFS(h) root[h] = g hig h kann auf nichts Neues verzweigen PowerDFS(i) root[i] = g hig i kann auf nichts Neues verzweigen PowerDFS(g) root[g] = g hig g ist Wurzel, SZK wird ausgegeben Bauer, Ebner, Moser, Schauer Bemerkung 4 21. Januar 2004