Graphdurchmusterung, Breiten- und Tiefensuche 1

Werbung
Prof. Thomas Richter
Institut für Analysis und Numerik
Otto-von-Guericke-Universität Magdeburg
[email protected]
18. Mai 2017
Material zur Vorlesung Algorithmische Mathematik II am 18.05.2017
Graphdurchmusterung, Breiten- und Tiefensuche
1 Zusammenang
Folie 1
Wir haben einen Graphen G = (V, E) zusammenhängend genannt, wenn es zu je
zwei Knoten x, y ∈ V einen Weg in G gibt, der x und y verbindet.
Definition 1 (Zusammenhangskomponente). Es sei G = (V, E) ein ungerichteter
Graph. Die maximalen zusammenhängenden Teilgraphen von G sind die Zusammenhangskomponenten von G.
1
4
2
5
3
Ein Graph mit zwei Zusammenhangskomponenten G1 und G2
G1 = ({1, 2, 3}, {(1, 2), (2, 3)}),
G2 = ({4, 5}, {(4, 5)}).
Im Folgenden untersuchen wir Algorithmen zum Bestimmen von Zusammenhangskomponenten. Der Grundalgorithmus ist die Durchmusterung von Graphen. Ausgehend
von einem Knoten x ∈ V suchen wir alle Knoten y ∈ V, die von x aus erreichbar sind.
1
Folie 2
Wir benötigen einige weitere Begriffe.
Definition 2 (Baum und Wald). Ein ungerichteter Graph heißt Wald, falls er keinen
Kreis enthält. Ein Baum ist ein zusammenhängender Wald. Knoten vom Grad 1 in einem
Baum heißten Blatt.
Die Zusammenhangskomponenten eines Waldes sind Bäume.
Bäume eignen sich, um die Zusammenhangskomponenten eines Graphen darzustellen.
2
Folie 3
Algorithmus 1: Graphdurchmusterung
Gegeben Graph G = (V, E) sowie Knoten x ∈ V. Ausgabe Teilgraph G 0 = (R, F)
1
2
3
4
5
6
7
8
9
10
11
R = {x}
Q = {x}
F=∅
Solange Q 6= ∅
Wä hle y ∈ Q
F a l l s e = (y, z) ∈ E mit z ∈ V \ R
R = R ∪ {z}
Q = Q ∪ {z}
F = F ∪ {(y, z)}
Sonst
Q = Q \ {y}
Stück für Stück wird ein Graph (R, F) aufgebaut. Ausgehend von einer Menge Q werden
alle Knoten z hinzugefügt, die von Elementen aus Q erreichbar sind. Knoten y ∈ Q, die
keine Nachbarn ausserhalb von R haben werden aus Q entfernt. Neue Nachbarn werden
Q hinzugefügt, da diese gegebenenfalls weitere Nachbarn haben.
Folie 4
Satz 1 (Graphdurchmusterung). Der Algorithmus zur Graphdurchmusterung liefert eine maximale Zusammenhangskomponente (R, F) mit x ∈ R. Er kann in linearer Laufzeit
O(|V| + |F|)
implementiert werden.
Beweis. (i) Wir zeigen zunächst, dass der Graph G 0 = (R, F) zu jedem Zeitpunkt im
Verfahren ein Baum ist. Zu Beginn des Verfahrens ist
G 0 = ({x}, ∅),
also zusammenhängend und kreisfrei und somit ein Baum. Der Graph G 0 wird nur in
Zeilen 7+9 durch hinzufügen eines Knotens z ∈ V \ R und einer Kante e ∈ E geändert. Der neue (ungerichtete) Graph ist weiterhin zusammenhängend und wegen z 6∈ R
weiterhin kreisfrei, also ein Baum.
(ii) Angenommen, nach Abschluss des Verfahrens gebe es einen Knoten r ∈ V \ R, der
aber vom Knoten x ∈ R erreichbar wäre. Dann gibt es einen Weg P in G, der x und r
verbindet. Es sei (a, b) ∈ E eine Kante auf diesem Weg mit a ∈ R und b ∈ V \ R. Da
3
a ∈ R muss es einen Durchlauf des Algorithmus gegeben haben mit a ∈ Q. Dieser
Knoten a wird jedoch nur dann aus Q entfernt, wenn es keine solche Kante (a, b) gibt.
Dies ist ein Widerspruch, also kann es keine erreichbaren Knoten r ∈ V \ R geben.
(iii) Wir merken uns zunächst in einer Liste der Länge |V| zu jedem Knoten die Anzahl
der Kanten innerhalb der Adjazenzliste, die bereits untersucht wurden. (Linear in |V|).
Untersucht werden nur die Kanten, die von Knoten in R aus erreichbar sind. Auf diese Weise wird jede Kante (von jedem Endknoten aus) maximal einmal betrachtet. Der
Algorithmus besteht somit aus maximal 2|F| Schritten. In jedem dieser Schritte werden
der Menge Q Elemente hinzugefügt bzw. entfernt. Verwenden wir Datenstrukturen, die
dies in konstanter Laufzeit O(1) ermöglichen, so erhalten wir insgesamt lineare Laufzeit
O(|V| + |E|).
Folie 5
Satz 2 (Zusammenhangskomponenten). Alle Zusammenhangskomponenten eines ungerichteten Graphen können in linearer Laufzeit
O(|V| + |E|)
bestimmt werden.
Beweis. Man startet das Verfahren mit einem beliebigen Knoten x ∈ V. Als Ergebnis
erhält man in der Laufzeit
O(|V| + |F1 |)
eine Zusammenhangskomponente G1 = (V1 , F1 ). Wir führen das Verfahren weiter mit
einem Knoten x ∈ V \ V1 (falls vorhanden). Die entsprechende Zusammenhangskomponente G2 = (V2 , F2 ) können wir in Laufzeit
O(|F2 |)
bestimmen, da die anfängliche Initialisierung der Liste der besuchten Kanten nur einmal erfolgen muss. Nach m Schritten erhalten wir die Laufzeit
O(|V| + |F1 | + |F2 | + · · · + |Fm |) = O(|V| + |E|),
da
|F1 | + · · · + |Fm | 6 |E|.
Der Algorithmus zur Graphdurchmusterung ist noch nicht eindeutig festgelegt, da in Zeile 5 die Wahl eines Elements y ∈ Q spezifiziert werden muss.
4
Folie 6
Definition 3 (Breitensuche und Tiefensuche). Wählt man bei der Graphdurchmusterung jeweils den Knoten y ∈ Q, der zuletzt zu Q hinzugefügt wurde, so nennt man das
Verfahren Tiefensuche. Wählt man den Knoten y ∈ Q, der als erstes zu Q hinzugefügt
wurde, so nennt man das Verfahren Breitensuche.
Die Grundideen der beiden Varianten beschreiben zwei verschiedene Datenstrukturen,
last in first out sowie first in first out. Die Erste Datenstruktur wird mit einem Stack realisiert. Wie auf einen Bücherstapel werden Elemente oben abgelegt und auch wieder von
oben entfernt. Die zweite Variante, first in first out beschreibt eine Warteschlange. Das Element welches zuerst in die Warteschlange zugefügt wurde, wir auch als erstes wieder
entfernt. Stellen wir uns beide Datenstrukturen als Listen vor, so erfolgt bei einem Stack
das Hinzufügen und Entfernen auf der gleichen Seite, bei einer Warteschlange erfolgen
Hinzufügen und Entfernen auf unterschiedlichen Seiten.
2 Datentypen und Realisierung
Folie 7
Zur Verwaltung eines Stacks eignet sich der bereits bekannte Datentyp vector. Die
relevanten Befehle sind:
• clear () : Löscht alle Einträge und setzt die Länge auf Null
• size () : Liefert die Länge des Vektors zurück
• push_back(x): Hängt das neue Element x in konstanter Laufzeit O(1) hinten an
den Vektor an
• back(): Gibt das letzte Element in konstanter Laufzeit O(1) zurück
• pop_back(): Entfernt das letzte Element in konstanter Laufzeit O(1)
Mit einem vector kann die Tiefensuche implementiert werden.
5
Folie 8
Algorithmus 2: Tiefensuche
1
2
3
4
void t i e f e n s u c h e ( i n t x , Graph& OUT, c o n s t Graph& IN )
{
i n t N = IN .N( ) ;
OUT. i n i t (N) ;
/ / I n i t i a l i s i e r u n g von Ausgabe
5
6
v e c t o r < i n t > E_besucht (N, −1) ;
/ / B e s u c h t e Kanten
v e c t o r <bool > inOUT (N, f a l s e ) ;
inOUT [ x ]= t r u e ;
/ / H i n z u g e f u e g t e Knoten
v e c t o r < i n t > Q;
Q. push_back ( x ) ;
/ / Stack
7
8
9
10
11
12
13
while (Q. s i z e ( ) >0)
{
i n t y = Q. back ( ) ;
14
15
16
/ / L e t z t e s E l e m e n t im S t a c k
17
i n t n i = E_besucht [ y ] + 1 ;
while ( ni <IN . a d j [ y ] . s i z e ( ) )
{
i n t n = IN . a d j [ y ] [ n i ] ;
++E_besucht [ y ] ;
18
19
20
21
22
/ / Erste , n i c h t b e s u c h t e Kante
/ / D u r c h l a u f e Nachbarn
/ / B e s u c h t e K a n t e merken
23
24
i f ( ! inOUT [ n ] )
/ / Neuer Knoten g e f u n d e n
{
OUT. neue_kante ( y , n ) ; / / K a n t e h i n z u
inOUT [ n]= t r u e ;
/ / Knoten a l s b e s u c h t m a r k i e r e n
25
26
27
28
29
Q. push_back ( n ) ;
break ;
30
31
}
++ n i ;
32
33
}
i f ( n i ==IN . a d j [ y ] . s i z e ( ) )
Q. pop_back ( ) ;
34
35
36
/ / K e i n e Nachbarn ?
}
37
38
/ / Knoten a u f den S t a c k l e g e n
/ / Abbruch
}
6
Zur Realisierung einer Warteschlage gibt es in C++ den Datentyp queue. Wie vector ist
queue ein template, es können also Warteschlagen von verschiedenen Typen verwaltet
werden, z.B. queue<int>.
Folie 9
Zur Verwaltung einer Warteschlange eignet sich der Datentyp queue. Die relevanten
Befehle sind:
• empty(): Liefert true zurück, wenn die Warteschlange leer ist
• push(x): Hängt das neue Element x in konstanter Laufzeit O(1) hinten an
• front () : Gibt das erste Element der Warteschlange in konstanter Laufzeit O(1)
zurück.
• pop(): Entfernt das erste Element in konstanter Laufzeit O(1)
Die Tiefensuche kann durch Verwendung einer Warteschlange anstelle eines Stacks einfach
zu einer Breitensuche modifiziert werden.
7
Folie 10
Algorithmus 3: Breitensuche
1
2
3
4
void b r e i t e n s u c h e ( i n t x , Graph& OUT, c o n s t Graph& IN )
{
i n t N = IN .N( ) ;
OUT. i n i t (N) ;
/ / I n i t i a l i s i e r u n g von Ausgabe
5
6
v e c t o r < i n t > E_besucht (N, −1) ;
/ / B e s u c h t e Kanten
v e c t o r <bool > inOUT (N, f a l s e ) ;
inOUT [ x ]= t r u e ;
/ / H i n z u g e f u e g t e Knoten
queue< i n t > Q;
Q. push ( x ) ;
/ / Warteschlange
7
8
9
10
11
12
13
while (Q. s i z e ( ) >0)
{
i n t y = Q. f r o n t ( ) ;
14
15
16
/ / E r s t e s E l e m e n t im S t a c k
17
i n t n i = E_besucht [ y ] + 1 ;
while ( ni <IN . a d j [ y ] . s i z e ( ) )
{
i n t n = IN . a d j [ y ] [ n i ] ;
++E_besucht [ y ] ;
18
19
20
21
22
/ / Erste , n i c h t b e s u c h t e Kante
/ / D u r c h l a u f e Nachbarn
/ / B e s u c h t e K a n t e merken
23
24
i f ( ! inOUT [ n ] )
/ / Neuer Knoten g e f u n d e n
{
OUT. neue_kante ( y , n ) ; / / K a n t e h i n z u
inOUT [ n]= t r u e ;
/ / Knoten a l s b e s u c h t m a r k i e r e n
25
26
27
28
29
Q. push ( n ) ;
}
++ n i ;
30
31
32
}
i f ( n i ==IN . a d j [ y ] . s i z e ( ) )
Q. pop ( ) ;
33
34
35
/ / K e i n e Nachbarn ?
}
36
37
/ / Knoten a n h a e n g e n
}
8
Die Breitensuche hat eine zur Suche von kürzesten Wegen wichtige Eigenschaft.
Folie 11
Satz 3 (Breitensuche). Der durch Breitensuche ausgehend von x ∈ V erzeugte Baum
B = (V 0 , E 0 ) enthält einen kürzesten Weg von Knoten x ∈ V 0 zu allen anderen, von x
aus erreichbaren Knoten y ∈ V 0 . Die Länge des Weges dist(x, y) kann in linearer Laufzeit
bestimmt werden.
Beweis. (i) Wir estellen von x aus den Baum mit Hilfe der Breitensuche. Dabei speichern
wir zusätzlich den Vektor vector<int> dist (N);, zu Beginn dist [ i]=−1; für alle Knoten i und
dist [x]=0; für den Startknoten.
Beim Hinzufügen einer neuen Kante in Zeile 27 wird der Abstand zum neuen Knoten
n definiert dist [n]=dist[y]+1;.
(ii) Wir zeigen nun
distG (x, y) = distB (x, y),
dass es also keinen kürzeren Weg im Graphen G = (V, E) als im Baum B = (V 0 , E 0 ) gibt.
Die Relation 6 ist klar, da B ⊂ G ein Teilgraph ist. Wir gehen davon aus, dass es einen
Knoten v ∈ V gibt mit
distG (x, v) < distB (x, v).
(1)
Dabei sei v so gewählt, dass distG (x, v) minimal ist. Mit P bezeichnen wir den kürzesten
Weg von x nach v in G. Dann sei (u, v) die letzte Kante in diesem Weg P. Nach Voraussetzung gilt
distG (x, v) = distG (x, u) + 1 = distB (x, u) + 1,
(2)
da ja v der derjenige Knoten mit kleinstem Abstand in G ist, der die Ungleichung (1)
erfüllt.
Zu jedem Zeitpunkt im Algorithmus gilt
distB (x, y) 6 distB (x, z) + 1 ∀y ∈ R, z ∈ Q
sowie
distB (x, y) 6 distB (x, z) + 1
∀y, z ∈ Q falls y zuerst zugefügt wurde.
Der Knoten v wird erst dann hinzugefügt, nachdem u aus Q entfernt wurde. Damit
muss
distB (x, v) 6 distB (x, u) + 1
gelten, ein Widerspruch zu (1) und (2).
9
Herunterladen