Definition 6.5.1
Sei ein Digraph und . Wir schreiben
, wenn es einen Pfad von nach gibt;
, wenn und , wenn es also Pfade in
beide Richtungen gibt;
, wenn aber nicht.
Wenn es klar ist, welcher Digraph gemeint ist, dann lassen wir den Subskript auch gerne
weg.
Für eine Auflistung der Knotenmenge (Auflistung heißt hier, dass jeder
Knoten genau einmal in vorkommt)
schreiben wir (bzw. ), wenn vor (bzw. nach )
in vorkommt.
Mit dieser Notation können wir die Definition der topologischen Sortierung sehr kompakt
schreiben: ist
eine topologische Sortierung von wenn
gilt.
Definition 6.5.2
Ein Digraph heißt stark zusammenhängend, wenn
für alle gilt. Wenn es also von jedem Knoten
zu jedem anderen einen Pfad (und auch wieder zurück) gibt.
Für einen (nicht unbedingt stark zusammenhängenden) Digraph ist die starke
Zusammenhangskomponente von
(Englisch strongly connected component) die Knotenmenge
Zwei Mengen und sich nur überschneiden können, wenn sie
identisch sind. Sie bilden also eine Partition von :
ist die Menge der starken Zusammenhangskomponenten von .
Übungsaufgabe 6.5.1
Finden Sie die starken Zusammenhangskomponenten in
Unser Hauptziel in diesem Abschnitt ist es, einen linearen Algorithmus zu
entwickeln, der die linearen Zusammenhangskomponenten findet.
Wenn Ihnen Effizienz egal ist, dann können Sie zum Beispiel
folgendes machen:
Ineffizienter Algorithmus für starke Zusammenhangskomponenten
color := ein Array der Größe ; jede Zelle auf undefined
initialisiert
for u in V:
if color[u] != undefined: continue (weil u schon besucht wurde)
reachable_from_u :=
can_reach_u :=
gibt allen Knoten in der Schnittmenge reachable_from_ucan_reach_u die
Farbe
Nun ist color[v] für jeden Knoten definiert und ist einfach der Name des
kleinsten
Knoten,
der in der gleichen Zusammenhangskomponente liegt.
Was ist die Laufzeit des Algorithmus? Die for-Schleife macht Durchläufe.
Jeder Durchlauf ermittelt reachable_from_u und can_reach_u, zum Beispiel
mit Tiefensuche und benötigt daher Zeit. Wir erhalten also eine Laufzeit
von . Dies ist nicht linear.
Ist unsere Analyse übermäßig pessimistisch? Wenn zum Beispiel in Zeile 2.i bereits
eine Farbe hat, dann überspringen wir ihn ja.
Übungsaufgabe 6.5.2
Zeigen Sie, dass die obige Laufzeitanalyse optimal ist. Genauer gesagt: finden Sie einen
Digraphen mit
Knoten und Kanten, sodass die Laufzeit ist.
Hinweis. Es ist kein besonders kompliziertes Beispiel.
Der
Algorithmus von Kosaraju und Sharir
Der erste Schritt hin zu einem linearen Algorithmus ist Tiefensuche. Erinnern Sie sich
an den Algorithmus topSortDFS im
letzten Abschnitt.
Wenn ein DAG ist, liefert er uns eine topologische Sortierung. Falls kein
DAG ist, kann uns die Ergebnisliste , wenn sie schon keine topologische Sortierung
ist, uns dennoch nützliche Information liefern? Probieren wir es aus und
lassen topSortDFS auf dem Digraph der letzen Übungsaufgabe laufen.
Statt einer Animation zeige ich Ihnen einfach direkt die Bäume.
Die DFS-Bäume, wenn wir topSortDFS ausführen und die Knoten in der Reihenfolge
durchgehen:
Übungsaufgabe 6.5.3
Lassen Sie den Algorithmus topSortDFS nochmal auf dem gleichen Graphen laufen, gehen
aber die Knoten in Zeile 5 von
topSortDFS in einer
anderen, von Ihnen selbst gewählen Reihenfolge durch.
Welche Reihenfolge gibt Ihnen die wenigsten DFS-Bäume? Welche die meisten?
Wir zeigen nun die DFS-Bäume, die Liste und die stark zusammenhängenden Komponenten
von in einer Abildung:
Als erstes fällt auf dass jede starke Zusammenhangskomponente in einem DFS-Baum ist,
also
nicht über mehrere verteilt, und innerhalb dieses DFS-Baumes auch zusammenhängend ist.
Übungsaufgabe 6.5.4
Beweisen Sie, dass dies kein Zufall ist. Beweisen Sie, dass eine starke Zusammenhangskomponente
in einem DFS-Baum enthalten ist: .
Zeigen Sie des weiteren, dass innerhalb von nicht zerstückelt ist,
dass also in zusammenhängend ist.
Die Liste stellt zwar keine topologische Sortierung dar, wie soll sie auch, wenn gar
kein
DAG ist, sortiert aber so gut es geht:
Theorem 6.5.3
Sei :=topSortDFS(). Für alle gilt
. In Worten: wenn es einen Pfad von nach
gibt,
aber keinen zurück, dann muss in weiter rechts stehen.
Beweis. To be done.
Jetzt können wir einen linearen Algorithmus beschreiben, der die starken
Zusammenhangskomponenten findet.
Wir gehen dies Liste von rechts nach links durch und führen eine DFS entgegen der
Kantenrichtung durch.
Sei der Knoten, der gerade "dran" ist. Alle Knoten zu seiner rechten in sind bereits
abgearbeitet und markiert worden.
Das Theorem garantiert uns, dass für alle Knoten links von entweder
gilt (sie also in der gleichen
starken Zusammenhangskomponente wie liegen) oder gilt (es also keinen
Pfad von nach gibt).
Alle Knoten, die Tiefensuche "in Rückwärtsrichtung" von ausgehend findet, sind
also mit in einer Zusammenhangskomponente. Findet sie auch alle? Nun ja, überzeugen sie
sich:
wenn sie einen Knoten "übersähe", dann nur, weil der Weg dorthin durch einen bereits
früher markierten Aufruf
von Tiefensuche markiert und blockiert worden wäre; aber dieser Aufruf hätte ja dann bereits die
ganze starke
Zusammenhangskomponente markiert.
Algorithmus: Starke Zusammenhangskomponenten finden
def findSCC(G): L = topSortDFS(G) colorArray = [undefined, ..., undefined] for u in L from right to left: if colorArray[u] == undefined: colorByDfs(u, color=u) def colorByReverseDfs(u, color): if (colorArray[u] != undefined: return else: colorArray[u] = color for v in inNeighbors(u): colorByReverseDfs(v, color) return colorArray