Algorithmen auf Graphen

Werbung
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
Herunterladen