2 Durchsuchen von Graphen

Werbung
Durchsuchen von Graphen
2
- 19 -
Durchsuchen von Graphen
Das vollständige Durchsuchen eines Graphen entlang seiner Kanten und
Knoten, wobei alle Knoten erreicht werden müssen, ist eine grundlegende
Aufgabe, die in vielen Graphalgorithmen als Teilaufgabe auftritt. Zwei besonders häufig verwendete Standardverfahren hierfür sind die Tiefensuche
und die Breitensuche.
Reihenfolgebeziehungen zwischen Objekten können mit Hilfe eines gerichteten azyklischen Graphen modelliert werden. Der Algorithmus des topologischen Sortierens ermöglicht es, eine lineare Anordnung der Objekte zu
berechnen, so dass die definierten Reihenfolgebeziehungen nicht verletzt
werden.
Nach dem Durcharbeiten dieses Kapitels sollen Sie
•
die Funktionsweise der Suchverfahren verstanden haben und nachvollziehen können,
•
Beispiele von Problemen kennen, die mit Hilfe dieser Standardverfahren
gelöst werden können,
•
für gegebene Problemstellungen einschätzen können, welches Suchverfahren angemessen ist und
•
auf Basis dieses Verfahrens einen Algorithmus konstruieren können.
2.1
Tiefensuche
Angenommen, wir wollen ermitteln, ob ein Graph zusammenhängend ist.
Für “kleine” Graphen könnten wir die Antwort auf diese Frage leicht einem Diagramm des Graphen entnehmen. Haben wir aber einen Graphen
mit mehreren Tausend Knoten, dann lässt sich diese Frage nicht mehr
so einfach beantworten. Vielleicht steht uns auch gar kein Diagramm zur
Verfügung, sondern der Graph ist nur im Computer repräsentiert. Für diesen
Fall benötigen wir einen Algorithmus, der berechnet, ob ein Graph zusammenhängend ist.
Laut Definition 1.10 ist ein Graph zusammenhängend, wenn je zwei Knoten über einen Weg verbunden sind. Für eine Berechnung können wir es
uns etwas einfacher machen. Offensichtlich genügt es, ausgehend von einem
Startknoten zu jedem Knoten einen Weg zu finden.
Abbildung 11 verdeutlicht solch einen Suchalgorithmus für Bäume. Die gestrichelten Pfeile geben an, in welcher Reihenfolge die Knoten besucht wer-
Lernziele
- 20 -
Graphentheorie
den. Wir beginnen die Suche im Knoten a und laufen von dort zu Knoten
b, dem ersten Nachbarn von Startknoten a. Von b aus gehen wir zu dessem
ersten Nachbarn c, kommen dort aber nicht weiter. Also gehen wir zu b
zurück und fahren mit d, dem zweiten Nachbarn von b, fort. Auch in d kommen wir nicht weiter, und weil b keinen weiteren Nachbarn hat, gehen wir
zurück bis a. Knoten a hat den unbesuchten Nachbarn e und von e gehen
wir wiederum zurück zu a, wo die Suche endet.
a
b
c
e
d
Abbildung 11: Tiefensuche in einem Baum
Tiefensuche für
Bäume
Das allgemeine Prinzip für das Durchlaufen des Baumes lautet also: Wenn
ein Knoten v betreten wurde, dann arbeite rekursiv alle Nachbarn von v
ab und kehre anschließend zu dem Knoten zurück, von dem aus v betreten
wurde. In Abbildung 11 bedeutet dies, dass wir zuerst in die Tiefe gehen,
bevor wir einen weiteren Nachbarn eines Knotens besuchen.
Während es in einem Baum genau einen Weg zwischen zwei Knoten gibt,
können in einem allgemeinen Graphen mehrere Wege zwischen zwei Knoten existieren. Daher müssen wir darauf achten, Teile des Graphen nicht
mehrfach zu durchlaufen. Wir erreichen dies, indem wir eine simple Technik einsetzen: Immer wenn wir einen Knoten betreten, markieren wir diesen
und so markierte Knoten betreten wir nicht ein weiteres Mal.
Markiert werden die Knoten v mit einer Tiefensuchnummer t(v). Ausgehend
von t(v) = 1 für den Startknoten gibt die Tiefensuchnummer die Reihenfolge
an, in der die Knoten besucht wurden. Für noch nicht besuchte Knoten gilt
t(v) = ∞.
Tiefensuchalgorithmus
Algorithmus 2.1 (Tiefensuche) Es sei G = (V, E) ein Graph. Für einen
Knoten v ∈ V ist die rekursive Prozedur DFSEARCH(v) wie folgt definiert:
i := i + 1; t(v) := i; N(v) := {w|{v, w} ∈ E};
while ∃w ∈ N(v) mit t(w) = ∞ do
N(v) := N(v) \ {w}; B := B ∪ {{v, w}};
DFSEARCH(w);
end
Durchsuchen von Graphen
- 21 -
Der Algorithmus für die Tiefensuche lautet:
B := ∅; i := 0;
for all v ∈ V do t(v) := ∞;
while ∃v ∈ V mit t(v) = ∞ do
DFSEARCH(v);
Die Kantenmenge B liefert für jede Zusammenhangskomponente von G
einen Baum.
Da in der Tiefensuche nicht festgelegt ist, in welcher Reihenfolge die Prozedur DFSEARCH(w) für die Knoten w ∈ N(v) aufgerufen wird, hängen t(v)
und B von der Repräsentation des Graphen ab.
Beispiel 2.1 Abbildung 12 zeigt im oberen Teil einen Graph mit seiner
Adjazenzlistendarstellung. Wenn die Knoten in der durch die Adjazenzlisten definierten Reihenfolge besucht werden, dann stellt der untere Teil der
Abbildung das Ergebnis der Tiefensuche mit den Kanten der Menge B und
den Tiefensuchnummern t(v) dar. Eine andere Reihenfolge der Knoten in
den Adjazenzlisten könnte zu einem anderen Ergebnis führen.
c
b
a
b
c
d
e
f
g
h
d
g
e
a
h
f
b
a
b
b
a
g
f
f
e
c
d
c
d
h
h
g
d
e
3
2
4
7
1
5
6
8
Abbildung 12: Tiefensuche
Die while-Schleife der Tiefensuche wird für jede Zusammenhangskomponente genau einmal aufgerufen. Daher genügt es, einen Zähler in diese
while-Schleife einzubauen, um die Anzahl der Zusammenhangskomponenten zu berechnen.
Anwendungen
der Tiefensuche
- 22 -
Graphentheorie
nZHK := 0;
while ∃v ∈ V mit t(v) = ∞ do
nZHK := nZHK + 1;
DFSEARCH(v);
end
In den Übungen betrachten wir weitere Anwendungen für die Tiefensuche.
Zeitaufwand der
Tiefensuche
Abschließend wollen wir noch untersuchen, wie groß der Zeitaufwand für die
Tiefensuche ist. In der Prozedur DFSEARCH wird jede Kante {v, w} zweimal
betrachtet, einmal beim Aufruf von DFSEARCH(v) und ein weiteres Mal bei
dem von DFSEARCH(w). Die Gesamtanzahl der Iterationen für diese whileSchleife beträgt somit höchstens 2 · |E|. Alle Operationen innerhalb der
while-Schleife können in konstanter Zeit ausgeführt werden. Darüber hinaus wird die Prozedur DFSEARCH genau |V |-mal aufgerufen. Somit erhalten
wir O(|V | + |E|) als Gesamtaufwand für die Tiefensuche.
Satz 2.1 Der Zeitaufwand für Algorithmus 2.1 beträgt O(|V | + |E|).
✍
Übungsaufgaben
2.1 Geben Sie für den folgenden Graphen die Nummern t(v) sowie die
Menge B der Baumkanten an, die sich bei der Tiefensuche ergeben.
Starten Sie die Suche im Knoten a. Die Adjazenzlisten seien alphabetisch sortiert.
c
j
f
d
b
h
a
Erkennung von
Artikulationspunkten
e
g
k
l
i
2.2 Für einen Graphen G = (V, E) heißt ein Knoten a ∈ V Artikulationspunkt, wenn die Anzahl der Zusammenhangskomponenten von
G(V \ {a}) größer ist als die von G. Anschaulich: Durch die Wegnahme von a und den dazu inzidenten Kanten zerfällt der Graph in
weitere Teile.
Wie kann man mit Tiefensuche Artikulationspunkte erkennen? Konstruieren Sie einen entsprechenden Algorithmus (ohne Beweis der
Korrektheit).
Durchsuchen von Graphen
2.2
- 23 -
Breitensuche
Die Tiefensuche, die wir im letzten Abschnitt kennen gelernt haben, ist
natürlich nicht die einzig mögliche Vorgehensweise, um einen Graphen systematisch zu durchsuchen. Wenn wir uns bei der Tiefensuche am Knoten v
befinden, wählen wir einen noch nicht besuchten Nachbarn von v aus, um
von dort aus die Suche fortzusetzen. Bei der Breitensuche dagegen tragen
wir alle Nachbarn von v in eine Warteschlange W ein, die diejenigen Knoten enthält, von denen aus der Graph weiter durchsucht werden muss. In
jeder Iteration der Breitensuche nehmen wir den ersten Knoten w aus der
Warteschlange W heraus, ermitteln alle Nachbarn von w, die noch nicht
erreicht worden sind und fügen diese Nachbarn in die Warteschlange ein.
Damit durchläuft die Breitensuche einen Baum wie in Abbildung 13 Ebene
für Ebene.
a
b
c
e
d
Abbildung 13: Breitensuche in einem Baum
Analog zur Tiefensuche werden die Knoten mit einer Breitensuchnummer
b(v) markiert. Zusätzlich wird ein Knoten v als “erreicht” markiert, wenn
er in die Warteschlange W aufgenommen wird. Hierzu dient die Markierung
r(v).
Algorithmus 2.2 (Breitensuche) Es sei G = (V, E) ein Graph. Für einen
Knoten v ∈ V ist die Prozedur BFSEARCH(v) wie folgt definiert:
i := i + 1; b(v) := i; N(v) := {w|{v, w} ∈ E}
for all w ∈ N(v) mit r(w) = false do
B := B ∪ {{v, w}};
füge w an W an;
r(w) := true;
end
Der Algorithmus für die Breitensuche lautet:
Breitensuchalgorithmus
- 24 -
Graphentheorie
B := ∅; i := 0; W := ();
for all v ∈ V do b(v) := ∞; r(v) := false; end
while W = () and ∃v ∈ V : b(v) = ∞ do
W := (v);
r(v) := true;
while W 6= () do
wähle ersten Knoten w aus W ;
entferne w aus W ;
BFSEARCH(w);
end
end
Die Prozedur BFSEARCH(v) wird jeweils für den ersten Knoten der Warteschlange W ausgeführt. Die Kantenmenge B liefert wiederum ein Gerüst
für jede Zusammenhangskomponente des Graphen. Der Algorithmus terminiert, wenn die Warteschlange leer ist und kein Knoten v mit b(v) = ∞ mehr
existiert. Die äußere while-Schleife der Breitensuche wird für jede Zusammenhangskomponente genau einmal durchlaufen, die innere while-Schleife
genau einmal für jeden Knoten.
Beispiel 2.2 Abbildung 14 zeigt das Ergebnis der Breitensuche für den
Graph von Abbildung 12.
4
2
5
7
1
3
6
8
Abbildung 14: Breitensuche
Zeitaufwand der
Breitensuche
Berechnung von
Knotenabständen
Satz 2.2 Der Zeitaufwand für Algorithmus 2.2 beträgt O(|V | + |E|).
Beweis: Wir argumentieren analog zu Satz 2.1: In BFSEARCH wird jede
Kante genau zweimal betrachtet, die weiteren Anweisungen von BFSEARCH
sind mit konstantem Aufwand verbunden und im Hauptalgorithmus wird
jeder Knoten genau einmal selektiert und aus W gelöscht.
2
Welche Anwendungen gibt es für die Breitensuche? Wenn wir einen Zähler
in die äußere while-Schleife einbauen, können wir mit der Breitensuche
ebenfalls die Anzahl der Zusammenhangskomponenten berechnen.
Eine andere Anwendung für die Breitensuche ist die Bestimmung der
Abstände da (v) zwischen einem ausgezeichneten Knoten a und allen anderen Knoten v ∈ V . Die Zahl da (v) sei definiert als die Länge eines kürzesten
Durchsuchen von Graphen
- 25 -
Weges zwischen a und v. Da die Breitensuche Ebene für Ebene verläuft,
wird implizit solch ein kürzester Weg ermittelt. Wir müssen uns im Verlauf der Breitensuche also nur noch die Abstände für die schon erreichten
Knoten merken. Hierzu erweitern wir die for all-Schleife in der Prozedur
BFSEARCH(v) um die Anweisung:
da (w) := da (v) + 1
und setzen da (a) := 0 zu Beginn der Breitensuche. Abbildung 15 zeigt
ein Beispiel für solch eine Abstandsberechnung. Das Gerüst in der unteren
Abbildung entspricht der Menge B, die Zahlen an den Knoten geben die
Abstände zum Knoten a an. Wir gehen dabei davon aus, dass die Adjazenzlisten alphabetisch sortiert sind.
c
d
f
e
g
b
a
2
2
1
0
3
2
1
Abbildung 15: Abstandsberechnung mit Hilfe der Breitensuche
✍
Übungsaufgaben
2.3 Geben Sie für den Graphen aus Aufgabe 2.1 die Breitensuchnummern
b(v) und die Menge B an.
- 26 -
Schichtengraph
Graphentheorie
2.4 Die Knoten des folgenden Graphen können wir in fünf Schichten (die
Bereiche zwischen den gestrichelten Linien) aufteilen: 0 : {1}, 1 :
{2, 3}, 2 : {4}, 3 : {5, 6}, 4 : {7}.
b
e
d
g
a
c
0
1
f
2
3
4
Jede gerichtete Kante verbindet einen Knoten aus Schicht i mit einem
Knoten aus Schicht i + 1. Für die Knoten einer Schicht entspricht die
Schichtennummer der Entfernung vom Knoten a.
Betrachten Sie nun allgemein einen gerichteten kreisfreien Graphen
G = (V, A), der genau einen Knoten v mit indeg(v) = 0 hat. Wie
kann man mit Breitensuche erkennen, ob solch ein Graph in Schichten
eingeteilt werden kann?
2.3
Topologisches Sortieren
Gerichtete kreisfreie Graphen eignen sich unter anderem zur Modellierung
von Reihenfolgebeziehungen, z.B. innerhalb von Projekten. Ein Projekt besteht aus auszuführenden Tätigkeiten t1 , . . . , tn , die voneinander abhängen.
Wenn Tätigkeit ti vor Tätigkeit tj ausgeführt werden muss, dann repräsentieren wir dies im Graphen durch die gerichtete Kante (ti , tj ). Für die Planung des Projekts benötigen wir eine Anordnung der einzelnen Tätigkeiten,
so dass alle Reihenfolgebeziehungen berücksichtigt sind. Solch eine Anordnung nennen wir topologische Sortierung.
Topologische
Sortierung
Definition 2.1 Es sei G = (V, A) ein gerichteter kreisfreier Graph mit
V = {v1 , . . . , vn }. Eine Knotenreihenfolge L = (vi1 , . . . , vin ) aller Knoten
aus V heißt topologische Sortierung von G, wenn die folgende Bedingung
erfüllt ist:
für alle (vij , vik ) ∈ A gilt ij < ik
Das heißt, für jede Kante (v, w) ∈ A ist v vor w in L.
Durchsuchen von Graphen
- 27 -
Beispiel 2.3 Jede topologische Sortierung der Projekttätigkeiten repräsentiert eine erlaubte Ausführungsreihenfolge. Abbildung 16 zeigt einen gerichteten kreisfreien Graph mit einer zugehörigen topologischen Sortierung.
a
c
b
d
e
f
1
2
3
4
5
6
c
e
a
d
b
f
Abbildung 16: Gerichteter kreisfreier Graph mit topologischer Sortierung
Unser Ziel ist es natürlich, einen Algorithmus zu konstruieren, der eine topologische Sortierung für einen gerichteten kreisfreien Graphen G berechnet.
Es bietet sich an, die topologische Sortierung von links nach rechts zu konstruieren, d.h. im i-ten Schritt entscheiden wir, welcher Knoten Position i
in der Sortierung einnimmt.
Welche Tätigkeiten könnten als erste ausgeführt werden? Dies sind offensichtlich diejenigen Tätigkeiten, die nicht von anderen Tätigkeiten
abhängen. In G sind das die Knoten, die nicht Endknoten einer gerichteten
Kante sind, d.h. alle Knoten v mit indeg(v) = 0.
Wir wählen einen Knoten v mit indeg(v) = 0 aus, der den ersten Rang in
der topologischen Sortierung einnimmt. Alle Reihenfolgebeziehungen, die v
betreffen, sind damit erfüllt, so dass wir v zusammen mit seinen inzidenten Kanten aus G entfernen können. Der so entstandene Graph ist wieder
kreisfrei und enthält genau die Reihenfolgebeziehungen, die noch zu berücksichtigen sind. Damit haben wir die gleiche Situation wie zu Beginn. Eine
iterative Anwendung der erläuterten Vorgehensweise liefert uns somit die
gewünschte topologische Sortierung.
Algorithmus 2.3 Gegeben sei ein gerichteter kreisfreier Graph G = (V, A).
Berechnet wird eine topologische Sortierung, die durch die Nummerierung
rang(v) gegeben ist.
Q := {v ∈ V |indeg(v) = 0}
k := 0;
while Q 6= ∅ do
Algorithmus
zum
topologischen
Sortieren
- 28 -
Graphentheorie
wähle ein v aus Q;
k := k + 1;
rang(v) := k;
Q := Q \ {v};
for all (v, w) ∈ A do
indeg(w) := indeg(w) − 1;
if indeg(w) = 0 then Q := Q ∪ {w};
end
end
Was passiert, wenn wir Algorithmus 2.3 auf einen Graphen G anwenden, der
einen gerichteten Kreis enthält? Zwischen den Knoten eines Kreises existieren zyklische Abhängigkeiten, so dass sie nie in die Menge Q aufgenommen
werden können. Daher wird der Algorithmus terminieren, ohne dass den
Kreisknoten v ein rang(v) zugewiesen wurde.
Zeitaufwand des
topologischen
Sortierens
Satz 2.3 Der Zeitaufwand für Algorithmus 2.3 beträgt O(|V | + |A|).
Beweis: Die while-Schleife wird genau |V |-mal durchlaufen, denn jeder
Knoten wird genau einmal in Q aufgenommen bzw. aus Q gelöscht. Jede
Kante e ∈ A wird in der for all-Schleife genau einmal berücksichtigt, d.h.
der Gesamtaufwand summiert über alle Iterationen der for all-Schleife
beträgt O(|A|).
2
✍
Übungsaufgaben
2.5 Bestimmen Sie eine topologische Sortierung für den gerichteten Graphen G = (V, A) mit
V = {a, b, c, d, e, f, g, h, i, j, k, l}
A = {(a, c), (b, d), (c, d), (e, c), (e, d), (e, f ), (e, g), (f, c), (f, h),
(g, f ), (g, h), (g, i), (h, j), (i, h), (i, j), (i, k), (l, k)}
Zusammenfassung
Tiefensuche und Breitensuche sind Algorithmen, mit denen wir in linearer Zeit Graphen systematisch untersuchen können. Eine topologische
Sortierung stellt eine Reihenfolge der Knoten dar, bei der die durch die
Kanten definierten Abhängigkeiten erfüllt sind.
Herunterladen