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