Westfälische Wilhelms-Universität Münster Ausarbeitung Graph-Algorithmen im Rahmen des Seminars Parallele Programmierung Bernd Kruthoff Themensteller: Prof. Dr. Herbert Kuchen Institut für Wirtschaftsinformatik Praktische Informatik in der Wirtschaft Inhaltsverzeichnis Inhaltsverzeichnis 1 Einleitung............................................................................................................1 2 Graph-Algorithmen .............................................................................................2 2.1 2.2 2.3 Verbundene Komponenten................................................................................2 2.1.1 Überblick.............................................................................................2 2.1.2 Auf dem Parallelrechnermodell CREW PRAM...................................2 2.1.3 Auf weiteren Parallelrechnungsmodellen ............................................5 Kürzeste Pfade von einem Ausgangsknoten ausgehend ....................................5 2.2.1 Überblick.............................................................................................5 2.2.2 Sequentieller Algorithmus ...................................................................6 2.2.3 Parallelisierung des Algorithmus .........................................................8 Minimal spannender Baum .............................................................................12 2.3.1 Überblick...........................................................................................12 2.3.2 Sollins Algorithmus...........................................................................12 2.3.2.1 In sequentieller Ausführung.........................................................12 2.3.2.2 Parallelisierungsmöglichkeiten ....................................................15 2.3.2.3 Laufzeitverhalten .........................................................................15 2.3.3 Kruskals Algorithmus........................................................................16 2.3.3.1 In sequentieller Ausführung.........................................................16 2.3.3.2 Parallelisierungsmöglichkeiten ....................................................17 2.3.3.3 Laufzeitverhalten .........................................................................18 3 Zusammenfassung.............................................................................................18 4 Literaturverzeichnis..............................................................................................I II Kapitel 1: Einleitung 1 Einleitung Viele Probleme lassen sich in verständlicher Form unter Benutzung von Objekten und Verbindungen zwischen diesen beschreiben. Mathematisch können diese durch einen Graphen abgebildet werden. Die so dargestellten Problemstellungen lassen sich mit Hilfe von Graph-Algorithmen auf Computern lösen. Um eine Steigerung der Rechengeschwindigkeit für eine schnellere Bearbeitung der Algorithmen zu erreichen oder deren Berechenbarkeit gar erst zu ermöglichen, ist es oft nötig, die sequentielle Von-Neumann-Architektur zu verlassen und Parallelrechner mit mehreren Prozessoren zu nutzen. Dies wirkt sich auf die bisher zur Problemlösung genutzten Algorithmen aus, die nun parallel bearbeitet werden sollen. In dieser Ausarbeitung sollen in diesem Zusammenhang einige Graph-Algorithmen vorgestellt werden, die auch in Algorithmen für Parallelrechnermodelle umgewandelt werden können. Zu Beginn wird die Suche nach verbundenen Komponenten auf Parallelrechnermodellen geschildert. Im Anschluss erfolgt die Beschreibung eines sequentiellen Algorithmus zum Auffinden von kürzesten Wegen von einem Ausgangsknoten zu allen anderen Knoten und dessen Parallelisierungsmöglichkeiten. Abschließend werden zwei Algorithmen zur Suche des minimal spannenden Baums eines Graphen in sequentieller und paralleler Form behandelt. Das Basiswissen für diese Ausarbeitung sollte aus den Grundstudiumsveranstaltungen Quantitative Methoden I bzw. Informatik I oder Informatik II bekannt sein. Eine ausführlichere Einführung in die Grundbegriffe findet man in [OW96] oder auch in [SE02]. Für Erklärungen zu den an dieser Stelle verwendeten Architekturen von Parallelrechnermodelle wird auf die entsprechende Ausarbeitung im Rahmen dieses Seminars oder auf [RR98] verwiesen. Als Grundlage für diese Ausarbeitung dient [QU94]. 1 Kapitel 2: Graph-Algorithmen 2 Graph-Algorithmen 2.1 Verbundene Komponenten 2.1.1 Überblick Oftmals ist es bei Graphen von Interesse, ob und welche Knoten man von einem beliebigen Knoten erreichen kann. Es existieren drei weit verbreitete Verfahren, um verbundene Komponenten eines ungerichteten Graphen zu finden: 1. über Suchformen wie z. B. Tiefen- und Breitensuche. 2. über die Transitive Hülle, die über parallele Matrizenmultiplikation der Adjazenzmatrix gefunden werden kann. Sequentiell kann diese mittels eines Durchlaufs durch die Adjazenzmatrix über den bekannten Algorithmus von S. Warshall ermittelt werden (siehe z. B. [SE02]). 3. über das Zusammenfügen von Knoten zu Komponenten, bis keine weiteren Zusammenschlüsse durchgeführt werden können. 2.1.2 Auf dem Parallelrechnermodell CREW PRAM Für das dritte Verfahren hat Hirschberg einen Algorithmus entwickelt, der auf der Parallelisierung durch das sogenannte CREW PRAM Modell basiert und bei dem zu Beginn alle Knoten eigenständige Komponenten sind. Jede Iteration läuft dabei in 3 Schritten ab: 1. Finden des Wurzelknotens einer Nachbarkomponente, der die geringste Nummer aufweist. Wurzelknoten sind dabei die Knoten einer Komponente, die wiederum die geringste Nummer haben. 2. Verbinden des Wurzelknotens der Komponente mit dem gefundenen Wurzelknoten. 3. Bildung einer übergeordneten Komponente, so dass alle zugehörigen Knoten auf den Wurzelknoten zeigen und diesen als Wert zugeordnet bekommen. 2 Kapitel 2: Graph-Algorithmen Der Algorithmus endet, wenn keine weiteren Nachbarkomponenten gefunden werden können. Dies soll anhand eines Beispiels verdeutlicht werden: 5 8 4 3 1 2 7 6 Abbildung 1: Beispielgraph für Hirschbergs Algorithmus Knoten Komponente 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 Tabelle 1: Komponentenzugehörigkeit vor der ersten Iteration Die hervorgehobenen Kanten sind die in den jeweiligen Schritten interessierenden Kanten. Es sind lediglich 2 Iterationen notwendig: 1. Iteration: 5 8 Schritt 1: 3 1 4 2 7 6 5 Schritt 2: 8 3 1 4 2 7 6 5 8 Schritt 3: 1 3 4 7 2 6 Abbildung 2: Schritte 1 bis 3 der ersten Iteration 3 Kapitel 2: Graph-Algorithmen Knoten Komponente 1 1 2 2 3 1 4 2 5 5 6 2 7 1 8 5 Tabelle 2: Komponentenzugehörigkeit nach der ersten Iteration 2. Iteration: 5 8 Schritt 1: 3 1 4 2 7 6 5 Schritt 2: 8 3 1 4 2 7 6 5 Schritt 3: 8 3 1 4 7 2 6 Abbildung 3: Schritte 1 bis 3 der zweiten Iteration Knoten Komponente 1 1 2 1 3 1 4 1 5 5 6 1 7 1 8 5 Tabelle 3: Komponentenzugehörigkeit nach der zweiten Iteration Da sich in jedem Schritt die Anzahl der Komponenten mindestens um den Faktor 2 verringert, können alle n Knoten in log n Iterationen den entsprechenden übergeordneten Komponenten zugewiesen werden. Die einzelnen Schritte je Iteration 4 Kapitel 2: Graph-Algorithmen können dabei auf die Minimumbestimmung zurückgeführt werden, die eine Zeitkomplexität von (log n) besitzt (s. [HI76]), woraus sich ergibt, dass dieser Algo(log 2 n) Zeit be- rithmus zum Auffinden aller verbundenen Komponenten insgesamt nötigt. Dazu werden in Hirschbergs Version n2 Prozessoren benötigt, um eine Laufzeit von (log n) je Einzelschritt zu gewährleisten. Allerdings lässt sich anhand des sogenannten Theorems von Brent feststellen, dass be reits ( n n / log n ) Prozessoren für den Algorithmus ausreichen, da n / log n Pro zessoren die Minimumsuche und die Zuweisung zu den Komponenten für n Knoten weiterhin ohne Zeitverlust in (log n) bewältigen können. Eine weitere Verbesserung stellten Chin et al. vor. Diese kommt mit ( n n / log 2 n ) Prozessoren aus, da gemäß Beschreibung je Iteration nur ein repräsentativer Knoten der zusammengelegten Komponenten benötigt wird und isolierte Komponenten von weiteren Betrachtungen ausgeschlossen werden können. 2.1.3 Auf weiteren Parallelrechnungsmodellen Nassimi und Sahni änderten Hirschbergs Algorithmus zur Nutzung auf einem 2dimensionalen mesh SIMD Modell ab (vgl. [NS80]). Sie stellten fest, dass bei n Prozessoren, n = 2k Knoten und maximalem Vertexgrad d eine Zeitkomplexität von ( dn log n) erreicht werden kann. Unter Vertexgrad eines Knoten ist die Anzahl d der von einem Knoten v abgehenden Kanten zu verstehen. Ebenfalls auf Hirschbergs Algorithmus basierend entwickelten Miller und Stout einen Algorithmus für eine Pyramidentopologie (s. [MS87]). Sie errechneten, dass das Auffinden der verbundenen Komponenten in ( n ) möglich ist, wenn die Adjazenzmatrix eines ungerichteten Graphen mit n Knoten in der Grundfläche eines SIMD-P Rechner mit n2 Prozessoren gespeichert wird. 2.2 Kürzeste Pfade von einem Ausgangsknoten ausgehend 2.2.1 Überblick Der von Moore 1959 entwickelte Algorithmus bestimmt die kürzesten Pfade von einem gewählten Ausgangsknoten zu allen anderen Knoten eines gewichteten und gerichteten 5 Kapitel 2: Graph-Algorithmen Graphen. Er wird hier zunächst in seiner sequentiellen Form vorgestellt, danach erfolgen Beschreibungen zur parallelen Abarbeitung des Algorithmus. 2.2.2 Sequentieller Algorithmus Zur Betrachtung des Algorithmus wird nachstehender Graph genutzt: B 4 A D 1 2 1 3 C E Abbildung 4: Beispielgraph Der Algorithmus benötigt ein Distanzarray, in dem die kürzesten Pfade zu den Knoten geführt werden. Zu Beginn enthält dieses für jeden Pfad außer zum Ausgangsknoten das Gewicht . Der kürzeste Pfad vom Ausgangsknoten zu sich selbst ist immer 0. Des weiteren wird eine Queue der abzuarbeitenden Knoten geführt, in der ursprünglich nur der Ausgangsknoten enthalten ist. Die Ausgangssituation sieht somit wie folgt aus: Distanzarray A 0 B C D E Queue A (a) (b) Tabelle 4: Distanzarray (a) und Queue (b) in der Ausgangssituation Je Iteration geschieht nun Folgendes: • Zunächst wird der erste Knoten aus der Queue gelöscht. • Dann wird geprüft, ob die bisherigen Distanzen zu den Nachbarknoten des gelöschten Knoten aktualisiert werden müssen. Dies ist der Fall, wenn der bisherige Wert größer ist als die Distanz des gelöschten Knoten zuzüglich der verbindenden Kante der beiden Knoten. 6 Kapitel 2: Graph-Algorithmen • Zuletzt werden alle vom gelöschten Knoten aus erreichbaren Knoten an das Ende der Queue eingefügt, falls sie noch nicht in der Queue stehen und sich ihr Wert im Distanzarray verringert hat. Durch die Forderung, dass nur Knoten in die Queue aufgenommen werden, für die der Wert im Distanzarray geringer wird, ist das Entstehen einer Endlosschleife aufgrund eines Zyklus im Graphen nicht möglich, da sich bei der zweiten Betrachtung eines Knoten des Zyklus die Distanz zu diesem durch die Nutzung des Zykluspfades nicht verringert. Der Algorithmus soll am Beispiel der ersten drei Iterationen verdeutlicht werden: 1. Iteration: 4 A 3 B 1 2 1 D C E (a) Distanzarray A 0 B 4 C 1 D E Queue B C (b) (c) Abbildung 5: Untersuchte Kanten (a), entstandenes Distanzarray (b) und Queue (c) 2. Iteration: 4 A 3 B 1 2 1 D C E (a) Distanzarray A 0 B 4 C 1 D 7 E Queue C D (b) (c) Abbildung 6: Untersuchte Kante (a), entstandenes Distanzarray (b) und Queue (c) 3. Iteration: B 4 A D 2 1 1 C E (a) Distanzarray A 0 B 3 C 1 D 7 E Queue D B (b) (c) Abbildung 7: Untersuchte Kante (a), entstandenes Distanzarray (b) und Queue (c) 7 Kapitel 2: Graph-Algorithmen Vor der letzten Iteration ergibt sich für das Distanzarray und die Queue folgendes Bild: Distanzarray A 0 B 3 C 1 D 6 E 7 Queue E (a) (b) Tabelle 5: Distanzarray (a) und Queue (b) vor der letzten Iteration Als nächstes wird der Knoten E aus der Queue gelöscht. Da vom Knoten E kein weiterer Knoten erreicht werden kann, ist keine Distanzüberprüfung nötig und es wird kein Knoten in die Queue neu eingefügt. Nun ist die Queue leer und der Algorithmus bricht ab. 2.2.3 Parallelisierung des Algorithmus Es gibt zwei Möglichkeiten, eine parallele Bearbeitung dieses Algorithmus durch mehrere Prozessoren zu erreichen. Zum einen kann die Untersuchung der abgehenden Kanten des gelöschten Knotens und die evtl. nötige Aktualisierung des Distanzarray parallelisiert werden. Alternativ kann die Bearbeitung der in der Queue befindlichen Knoten parallel von mehreren asynchronen Prozessoren durchgeführt werden. Die zweite Methode ist vorzuziehen, da die entstehenden Aufgaben für die einzelnen Prozessoren größer sind und Möglichkeiten zur Parallelisierung bei der ersten Methode aufgrund der begrenzten Anzahl der abgehenden Kanten eingeschränkt sind. Der zugehörige Pseudocode des gewählten parallelen Ansatzes sieht wie folgt aus: SHORTEST PATH Global distance n halt p s weight {Element i enthält die Entfernung von s nach i} {Anzahl Knoten im Graphen} {Variable, die anzeigt, wann die Prozesse halten können} {Anzahl der Prozesse} {Ausgangsknoten} {Enthält Gewicht jeder Kante} 1. begin 2. for all Pi, 1 i p do { for j = i to n step p do { 3. 4. INITIALIZE(j) 5. } 8 Kapitel 2: Graph-Algorithmen 6. } 7. enqueue s 8. halt = false 9. for all i, 1 i p do { 10. repeat SEARCH(i) until halt = true 11. } 12. end SEARCH (i) parameter i local new_distance u v {Prozessnummer} {Entfernung nach v, wenn u passiert wird} {Untersuchter Knoten, von dem Kanten abgehen} {mit u über Kante verbundener Knoten} 13. begin 14. lock the queue 15. if the queue is empty { 16. waiting(i) = true 17. if i = 1 { 18. halt = waiting(2) and … and waiting (p) 19. } 20. unlock the queue 21. } 22. else { 23. dequeue u 24. waiting(i) = false 25. unlock the queue 26. for every edge {u,w} in the graph do { 27. new_distance = distance(u) + weight ({u,w}) 28. lock (distance (v)) 29. if new_distance < distance(v) { 30. distance(v) = new_distance 31. unlock (distance(v)) 32. if v is not in the queue { 33. lock the queue; enqueue v; unlock the queue; 34. } 35. } 36. else { 37. unlock (distance(v)) 38. } 39. } 40. } 41. end Durch die Nutzung mehrerer asynchroner Prozessoren entstehen Konflikte beim zeitgleichen Zugriff auf die Queue, der gleichzeitigen Aktualisierung des Distanzarrays und beim Terminieren von Prozessen. Im Folgenden werden einige Erläuterungen zum Pseudocode gegeben und es sollen Lösungsmöglichkeiten für diese Probleme dargestellt 9 Kapitel 2: Graph-Algorithmen werden. Dabei wird davon ausgegangen, dass in der Regel ein Prozess auf einem Prozessor läuft. Der vorzuziehende parallele Algorithmus initialisiert in der Funktion INITIALIZE (j) zunächst parallel das Distanzarray mittels der gestarteten Prozessoren (Zeile 2 bis 6). Außerdem wird für jeden Prozessor die für ihn vorgesehene Variable waiting auf false gesetzt. Sie wird für das Abbruchkriterium des Algorithmus benötigt. Ein Prozessor sucht nun durch die Funktion SEARCH(i) in der Queue nach einem Knoten, den er bearbeiten kann. Da die Möglichkeit besteht, dass gleichzeitig mehrere Prozessoren für ihren laufenden Prozess auf die Queue zugreifen wollen, muss ein Sperrmechanismus eingebaut werden, so dass exklusiver Zugriff erreicht wird (Zeile 14). Sobald der jeweilige Prozessor keinen weiteren abzuarbeitenden Knoten in der Queue findet, setzt er seine Variable waiting auf true und gibt die Queue wieder frei (Zeilen 15, 16 und 20). Wenn Prozessor 1 warten muss, kontrolliert er vor der Freigabe noch, ob die zusätzlich benötigte Variable halt auf true gesetzt werden muss und somit das Abbruchkriterium erreicht wird (Zeile 17 bis 19). Dies ist der Fall, wenn die Variable waiting aller anderen Prozessoren ebenfalls den Wert true hat. Erst dann können alle Prozessoren stoppen. Durch dieses Vorgehen kann verhindert werden, dass ein Prozessor sofort terminiert, wenn er erkennt, dass die Queue (momentan) leer ist. Stattdessen überprüft er die Queue solange, bis die Variable halt auf true gesetzt ist. Findet der jeweilige Prozessor aber einen Knoten in der Queue, muss dieser aus der Queue gelöscht werden und die Variable waiting des Prozessors auf false gesetzt werden, da der Prozessor nun mit der Bearbeitung dieses Knotens startet, und die Sperre der Queue aufgehoben werden (Zeile 23 bis 25). Bei der Aktualisierung des Distanzarray muss gewährleistet werden, dass der entsprechende Prozessor momentan alleinigen Zugriff auf das neu zu belegende Feld des Distanzarrays besitzt, denn sonst kann es zu einer Art Rennen zwischen den Prozessoren kommen. Dies wird wieder über eine exklusive Sperrung für diesen Prozessor erreicht, indem die Variable distance(v) für die Dauer der Überprüfung und Aktualisierung gesperrt wird (Zeilen 28 ff.). Falls der zu erreichende Knoten in die Queue eingefügt werden muss, ist wieder ein exklusiver Zugriff auf diese durch den jeweiligen Prozessor nötig (Zeile 32 bis 34). Insbesondere das Sperren der Queue kann allerdings die Geschwindigkeitsbeschleunigung des parallelen Algorithmus erheblich beeinträchtigen. Um diesen Konflikt zwischen den Prozessoren zu umgehen, eignet sich die Datenstruktur des Linked Array. 10 Kapitel 2: Graph-Algorithmen Zur parallelen Bearbeitung sollten dazu zwei Arrays genutzt werden: Ein Array für die in der laufenden Iteration zu untersuchende Queue und ein Array, in dem die Knoten aufgeführt werden, die während dieser Iteration an die Queue angehangen werden, und das somit die Queue für die nächste Iteration bildet. Dies kann auch mit nur einem Array durch Aufteilung abgebildet werden. Der Grundgedanke des Linked Array ist dabei einerseits, dass die p Prozessoren jeweils den p-ten Knoten des einen Arrays untersuchen und löschen. Andererseits trägt jeder Prozessor zu untersuchende Knoten, die er je Iteration findet, nur in einen für ihn abgegrenzten Bereich des anderen Arrays ein. An diese Einträge werden dann nach jeder Iteration jeweils p Verweise auf den nächsten Bereich angehängt. So können in der nächsten Iteration einfach die Rollen zwischen den Arrays getauscht werden. Die Prozessoren können dann das zu untersuchende Array mit Hilfe der Verweise leicht durchsuchen. Wenn ein Prozessor einen Verweis findet, der außerhalb des zu bearbeitenden Arrays liegt, gibt es keine weiteren abzuarbeitenden Knoten für ihn. Im Folgenden sind beispielhaft für p=4 Prozessoren die beiden Arrays dargestellt. Xij bezeichnet dabei den j-ten eingefügten Knoten des Prozessors i: P1 P2 P3 P4 P1 X11 X12 X13 X14 X15 P2 P3 P4 P1 ... X21 X22 X23 ... Abbildung 8: Array der Queue mit den zu bearbeitenden Knoten und Verweisen. Links vom Trennstrich hat Prozessor 1 in der vorherigen Iteration Knoten und Verweise eingefügt hat. X11 ... Bereich für P1 X21 ... Bereich für P2 X31 ... Bereich für P3 X41 ... Bereich für P4 Abbildung 9: Array mit Bereichen für jeden Prozessor zum Einfügen in die Queue Durch dieses Vorgehen werden Zugriffskonflikte auf die Arrays unter den Prozessoren vermieden, da es je ein Array für die zu bearbeitenden (und somit zu löschenden) Knoten und für die einzufügenden Knoten gibt. Außerdem wird eine gleichmäßige Lastverteilung zwischen den Prozessoren erreicht, was sich durch einen nahezu gleichen Anteil von zu bearbeitenden Knoten für jeden Prozessor je Iteration ausdrückt. 11 Kapitel 2: Graph-Algorithmen Mit Hilfe dieser Datenstruktur lässt sich die erreichbare Beschleunigung der Parallelisierung optimieren, obwohl Zeit für das Nachvollziehen der Verweise und die Synchronisation der Prozessoren am Ende jeder Iteration verloren geht. Falls es nicht nur einen Prozessor gibt, der die Einfügeoperationen vornimmt, müssen die Arrays erheblich größer dimensioniert werden als hätte man einen Prozessor für diese Aufgabe, da Knoten dann von verschiedenen Prozessoren mehrmals in das Array eingefügt werden können. Dadurch kann außerdem wieder das Problem des gleichzeitigen Zugriff auf das Distanzarray in Form der Variable distance(v) auftreten, was wieder einen Sperrmechanismus nötig macht. Die Laufzeit hängt bei Moores Algorithmus vom Aufbau des Graphen und der Knotenfolge in der Queue und der damit verbundenen Abarbeitungsreihenfolge der Knoten ab. Die Anzahl der nutzbaren Prozessoren ist durch die Anzahl der Knoten des Graphen beschränkt, da sich maximal n – 1 Knoten in der Queue befinden können. 2.3 Minimal spannender Baum 2.3.1 Überblick Ein minimal spannender Baum ist ein Baum eines verbundenen Graphen, der alle seine Knoten mit minimaler Gesamtlänge/minimalem Gesamtgewicht der Kanten verbindet. Die Algorithmen zum Auffinden dieser Bäume finden häufig Anwendung, z. B. bei der Suche nach minimalen Daten- oder Telekommunikationsnetzen. Im folgenden werden zwei bekannte, sequentielle Algorithmen für gewichtete und ungerichtete Graphen und die Möglichkeiten zu deren paralleler Bearbeitung vorgestellt. 2.3.2 Sollins Algorithmus 2.3.2.1 In sequentieller Ausführung Der Algorithmus von Sollin hat die gleiche Vorgehensweise wie der zuvor beschriebene Algorithmus von Hirschberg (Kap. 3.1.2). Er startet ebenfalls mit einem Wald von Teilbäumen, die jeweils nur einen einzelnen Knoten umfassen. Anstatt der Komponente mit dem geringsten Wert wird in jeder Iteration die Kante mit geringsten Gewicht zu einem anderen Teilbaum gesucht und die beiden Teilbäume über diese Kante verbunden, wenn kein Zyklus entsteht. Ein minimal spannender Baum muss zyklenfrei sein, denn sonst ist eine Kante überflüssig, um alle Knoten erreichen zu können. 12 Kapitel 2: Graph-Algorithmen Anhand des folgenden Beispielgraphen soll die Vorgehensweise verdeutlicht werden: 4 A 5 2 B C 6 G 1 1 5 3 H D 1 2 I 3 7 E F 4 Abbildung 10: Beispielgraph für Sollins Algorithmus Die Wahl der Kanten für die einzelnen Knoten in der ersten Iteration zeigt die nächste Abbildung: 4 A 5 2 B C 6 3 1 5 G 1 H D 1 2 I 3 7 E F 4 Abbildung 11: Kantenwahl (hervorgehoben) in der 1. Iteration Wenn der Algorithmus für einen Teilbaum zwei abgehende Kanten mit dem gleichen Gewicht findet, ist die Auswahl der Kante laufzeitabhängig und wird auch durch die Datenstruktur der Kanten beeinflusst. Daraus resultiert, dass der Algorithmus nicht deterministisch ist. Im Beispielgraph ist diese Situation für den Knoten G gegeben. Da kein Zyklus entsteht, wenn die gefundenen Kanten in die Menge T der benötigen Kanten eingefügt werden, können die Teilbäume zu jeweils einem Teilbaum über die gefundenen Kanten verbunden werden. Es existieren am Ende der ersten Iteration somit 4 Teilbäume und die Menge T enthält 5 Kanten. In der zweiten Iteration wird die Menge T um die Kanten {A,G} und entweder {C,H} bzw. {D,I} erweitert. Die beiden übriggebliebenen Teilbäume werden in der dritten und letzten Iteration über die Kante {B,C} oder {C,G} verbunden. 13 Kapitel 2: Graph-Algorithmen Daraus kann sich folgender minimal spannender Baum ergeben: 4 A B C G 1 1 5 3 H D 1 2 I 3 F E Abbildung 12: Minimal spannender Baum des Beispielgraphen nach Sollin Der zugehörige parallele Pseudocode unterscheidet sich von der sequentiellen Version lediglich durch zwei zusätzliche Zeilen. Diese sind nötig, damit zwei Prozessoren nicht gleichzeitig die gleichen Teilbäume verbinden. Sie sind in rot hervorgehoben: MINIMUM SPANNING TREE 1. begin 2. n {Anzahl Knoten im Graphen} 3. T {Menge der für den minimal aufspannenden Baum nötigen Kanten} 4. for i = 1 to n do { 5. Knoten i gehört zunächst zum Teilbaum i 6. } 7. T = ∅ 8. while |T| < n – 1 do { // Abbruchkriterium for every tree i do { 9. 10. closest(i) = 11. } 12. for every edge {v,w} do { 13. if FIND (v) != FIND (w) { 14. if weight ({v,w}) < closest(FIND(v)){ 15. closest (FIND (v)) = weight ({v,w}) 16. edge (FIND (v)) = {v,w} 17. } 18. } 19. } 20. for every tree i do { 21. {v,w} = edge i 22. lock v, lock u 23. if FIND (v) != FIND (w){ 24. T = T ∪ {v,w} 25. UNION (v,w) 26. } 27. unlock v, unlock u 28. } 29. } 14 Kapitel 2: Graph-Algorithmen 30. end UNION und FIND sind effiziente Funktionen, um zum einen zwei Mengen zu einer zusammenzufassen bzw. den Namen der Menge wiederzugeben, in der sich ein Knoten befindet. Näheres zu deren Ablauf und Laufzeitverhalten kann u.a. in [AH74] und in [HU73] nachgelesen werden. 2.3.2.2 Parallelisierungsmöglichkeiten Die Iterationen, die im Pseudocode durch die gesamte while-Schleife abgebildet werden, können nicht parallelisiert werden, da jede zunächst abgeschlossen sein muss, bevor die nächste starten kann. Bei den beiden for-Schleifen zur vorbereitenden Initialisierung der kürzesten Kanten mit dem Gewicht (Zeile 9 bis 11) und zur Suche der Kanten mit dem geringsten Gewicht (Zeile 12 bis 19) kann eine Parallelisierung durch Aufteilen der zu bearbeitenden Knoten auf die Prozessoren erfolgen. Für das Verbinden der Teilbäume (Zeile 20 bis 28) muss der parallele Algorithmus wie schon erwähnt erweitert werden. Der Grund dafür ist, dass zwei Prozessoren nicht gleichzeitig zwei Teilbäume verbinden dürfen, denn dann würde eine überflüssige Kante in die Menge T der benötigten Kanten aufgenommen. Deshalb wird ein Sperrmechanismus eingebaut (Zeile 22 und 27), der für exklusiven Zugriff auf zwei Teilbäume sorgt, die ein Prozessor zusammenfügen möchte. In [OW96] ist eine parallele Version für Sollins Algorithmus angegeben, bei der für jeden der n Knoten ein Prozessor zur Verfügung steht. Im ersten Schritt suchen die Prozessoren für die Ihnen zugeteilten Knoten und nicht für Teilbäume die Kante mit dem geringsten Gewicht. Dann wird für jeden Teilbaum die Kante mit dem geringsten Gewicht ausgewählt, in dem die Prozessoren der Knoten, die bereits zu einem Teilbaum gehören, ihre jeweils gewählte Kante vergleichen. Die übriggebliebenen Prozessoren enthalten dann die Kanten über die nun neue Teilbäume gebildet werden müssen. 2.3.2.3 Laufzeitverhalten Die Zeitkomplexität dieses Algorithmus hängt bei Parallelisierung der Schleifen von der Anzahl der Knoten n ab, da diese in jeder Iteration betrachtet werden, und von der Anzahl der zur Verfügung stehenden Prozessoren p. In jedem Durchlauf verringert sich die Anzahl der Teilbäume um mindestens den Faktor 2, da jeder Teilbaum mit mindestens einem weiteren Teilbaum verbunden wird. 15 Kapitel 2: Graph-Algorithmen Dadurch sind höchstens log n Durchläufe notwendig. Dies muss mit der Komplexität einer Iteration multipliziert werden. Die in den Iterationen genutzten UNION und FIND-Operationen können mit einer sehr langsam wachsenden Zeitkomplexität von (log* n) vereinfachend als Konstanten angesehen werden. Die Initialisierung in den Zeilen 9 bis 11 hat eine Komplexität von n / p , da die Teilbäume auf die p Prozessoren vorverteilt werden können. Dazu muss für die Suche der Kante mit dem geringsten Gewicht bei allen Knoten in der zweiten for-Schleife n2 / p addiert werden, weil von n Knoten n–1 Kanten abgehen können und somit n2 Untersuchungen auf p Prozessoren aufgeteilt werden müssen. Dazu kommt der Wert n / p * p für die letzte for-Schleife, da die n Knoten/Teilbäume zum Verbinden zwar auf p Prozessoren vorverteilt werden können, aber im worst case ein Prozessor auf alle anderen Prozessoren warten, bis er einen Teilbaum sperren kann. Da die Datenstruktur der Knoten bei der Auftragsvergabe auf die Prozessoren gesperrt werden muss, hat jede von den drei for-Schleifen noch einen zusätzlichen Zeitaufwand von p. Daraus ergibt sich folgende Zeitkomplexität: ( log n ) * ( (n / p + p) + = ( log n ) * ( n2 / p + p ) + ( n2 / p + n / p + n + 3 p ) = ( n / p * p + p)) ( log n ( n2 / p + n / p + n + p)) Eine gute Beschleunigung kann erreicht werden, wenn p < < n. Bei dem in [OW96] aufgezeigten Algorithmus sind ebenfalls log n Iterationen not wendig, allerdings können die Teilschritte jeweils in eine Zeitkomplexität von ( n) durchgeführt werden, so dass (n log n) erreicht wird, unter der Voraussetzung, dass p = n Prozessoren zur Verfügung stehen. 2.3.3 Kruskals Algorithmus 2.3.3.1 In sequentieller Ausführung Dieser Algorithmus ist seit spätestens 1956 bekannt und wird allgemein J.B. Kruskal zugesprochen, der ihn in [KR56] erstmals publiziert. Zunächst sind wie bei Sollins Algorithmus alle Knoten für sich Teilbäume eines Waldes. Es wird zu Beginn die Kante mit dem geringsten Gewicht aufgenommen und 16 Kapitel 2: Graph-Algorithmen die entsprechenden Teilbäume verbunden. Dann folgt schrittweise die Kante, die von den übriggebliebenen Kanten wiederum das geringste Gewicht besitzt und die keinen Zyklus entstehen lässt, bis alle Knoten des Graphen in einem Baum enthalten sind. Diese Verfahrensweise wird nun tabellarisch anhand des Beispielgraphen aus Abb. 7 aufgezeigt: Kante Gewicht Entsteht 1 1 1 2 2 3 3 4 4 5 Zyklus? Nein Nein Nein Nein Ja Nein Nein Nein Ja Nein {A,F} {C,D} {H,I} {C,H} {D,I} {A,G} {G,E} {A,B} {F,E} {B,C} Aufnehmen? Alle Knoten in einem Baum? Ja Nein Ja Nein Ja Nein Ja Nein Nein Nein Ja Nein Ja Nein Ja Nein Nein Nein Ja Ja Tabelle 6: Ermittlung der nötigen Kanten Nach Aufnahme der Kante {B,C} ist folgender minimal spannende Baum entstanden: 5 4 A B 2 C H 1 1 3 G 1 D I 3 F E Abbildung 13: Minimal spannender Baum des Beispielgraphen nach Kruskal Die Sortierung der Kanten bei gleichem Gewicht hängt wieder von der Datenstruktur ab, so dass dieser Algorithmus auch nicht deterministisch ist und ebenfalls der minimal spannende Baum nach Sollin hätte entstehen können. 2.3.3.2 Parallelisierungsmöglichkeiten Zur Parallelisierung dieses Algorithmus wird ein Heap eingesetzt, der die Kanten in der gewünschten aufsteigenden Reihenfolge in die Wurzel schiebt, wo sie entnommen 17 Kapitel 2: Graph-Algorithmen werden. Der Aufbau eines Heap wird hier als bekannt vorgesetzt, da er Bestandteil von Grundstudiumsveranstaltungen ist. Die einzelnen Ebenen des Heap werden jeweils von einem Prozessor bearbeitet. Ein weiterer Prozessor wird benötigt, um die Kante, die sich aktuell in der Wurzel befindet, zu entnehmen, zu prüfen, ob sie für den minimal spannenden Baum notwendig ist, und die entsprechenden Teilbäume gegebenenfalls zu verbinden. Entsteht auf einer Ebene durch die Entnahme eines Knoten ein freier Platz, wird dieser mit dem passenden Nachfolger der darunter liegenden Ebene gefüllt und der Prozessor für diese Ebene muss seinerseits nun aktiv werden. Wenn auf der unteren Ebene leere Blattknoten entstehen, werden diese mit dem Wert ∞ gefüllt. Sobald die Wurzel diesen Wert annimmt, endet der parallele Algorithmus und ein minimal spannender Baum ist gefunden. 2.3.3.3 Laufzeitverhalten Bei der Parallelisierung von Kruskals Algorithmus hängt die Zeitkomplexität von der Anzahl der Kanten m ab, die der Heap enthält da diese in jeder Iteration entnommen werden. Der parallele Neuaufbau des Heap eines verbundenen und gerichteten Graphen und die damit verbundene erneute Bereitstellung einer entnehmbaren Kante kann in konstanter Zeit von einem UMA Multiprozessor mit log m Prozessoren durchgeführt werden. Dabei kann jeder Prozessor, der sich um eine Ebene kümmert, diese ebenfalls als Heap verwalten. Unter Zuhilfenahme der Funktionen UNION und FIND kann auch die Entnahme der Wurzel, deren Überprüfung und die Verbindung der Teilbäume von dem zuständigen Prozessor in (nahezu) konstanter Zeit durchgeführt werden. Daraus resultiert eine Zeitkomplexität von ( m) bei log m Prozessoren für den parallelisierten Algorithmus von Kruskal. 3 Zusammenfassung Aus dem weiten Feld von Graphen-Algorithmen wurde in dieser Arbeit zunächst der Algorithmus zur Erkennung der verbundenen Komponenten auf dem CREW PRAM Modell in der Version von Hirschberg und dessen Verbesserungsmöglichkeiten vorgestellt. Zur Lösung dieses Problems nutzt er das Verfahren des Zusammenfügen von Knoten zu Komponenten. Entsprechende Algorithmen für weitere Parallelrechnermodelle basieren auf diesem. 18 Kapitel 3: Zusammenfassung Als nächstes wurde der Algorithmus von Moore zur Suche nach den kürzesten Pfaden von einem Ausgangsknoten zu allen anderen Knoten aufgezeigt. Dieser nutzt ein Distanzarray und eine Queue. Weiterhin wurde die Möglichkeit der Parallelisierung in Bezug auf die Schleifen des Algorithmus dargestellt. Diese ist auf die Nutzung von Sperrmechanismen angewiesen. Eine effizientere parallele Bearbeitung bietet hier die Nutzung der Datenstruktur Linked Array. Abschließend wurden noch zwei bekannte Algorithmen zum Auffinden eines minimal spannenden Baumes und deren Parallelisierung erläutert. Dabei handelt es sich einerseits um den Algorithmus von Sollin, der ein ähnliches Vorgehen wie Hirschbergs Algorithmus anwendet. Eine Parallelisierung ist hier ebenfalls innerhalb der Schleifen unter einfachem Hinzufügen eines Sperrmechanismus möglich. Eine gute Beschleunigung wird dabei erreicht, wenn die Anzahl der Prozessoren erheblich kleiner ist als die Anzahl der Knoten. Eine etwas abgewandelte Version für den Fall, dass die Anzahl der Prozessoren mit der Anzahl der Knoten übereinstimmt, wurde angesprochen. Außerdem wurde der Algorithmus von Kruskal beschrieben. Er sucht sich nacheinander die jeweils kürzeste Kante und fügt sie in die Menge der benötigten Kante ein, wenn dadurch kein Zyklus im Graphen entsteht. Dessen parallele Abarbeitung erfolgt durch die Nutzung eines Heap und ergibt eine von der Anzahl m der Kanten abhängige Zeitkomplexität von ( m) . Für die weitere Entwicklung ist eine Untersuchung der vorgestellten Algorithmen für speziellere bzw. andere Parallelrechnermodelle denkbar. 19 Literaturverzeichnis 4 Literaturverzeichnis [AH74] A.V. Aho, J.E. Hopcraft, J.D. Ullman: The Design and Analysis of Computer Algorithms, Addison-Wesley, 1974. [AK89] S. G. Akl: The Design and Analysis of Parallel Algorithms, Prentice Hall, 1989. [CL81] F.Y. Chin, J. Lam, I.N. Chen: Optimal parallel algorithms for the connected component problem, Proceedings of the 1981 International Conference on Parallel Processing, IEEE, New York, Aug., S. 170-175, 1981. [CL82] F.Y. Chin, J. Lam, I.N. Chen: Efficient parallel algorithms for some graph problems, Communications ot the ACM, vol. 25, no. 9, Sept., S. 659–665, 1982. [GW88] A. Gibbsons, W. Rytter: Efficient Parallel Algorithm, Cambridge University Press, 1988. [HI76] D.S. Hirschberg: Parallel algorithms for the transitive closure and the connected component problem, Proceedings of the 8th Annual ACM Symposium on the Theory of Computing, ACM, New York, May, S. 5557, 1976. [HU73] J.E. Hopcraft, J.D. Ullman: Set-merging algorithms, SIAM Journal on Computing, vol. 2, S. 294-303, 1973. [KR56] J.B. Kruskal: On the shortest subtree of a graph and the traveling salesman problem, Proceedings of the American Mathematical Society, vol.7, Feb., S. 48-50, 1956. [MS87] R. Miller, Q.F. Stout: Data movement techniques for the pyramid computer, SIAM Journal on Computing, vol. 16, no. 1, Feb., S. 38-60, 1987. [NS80] D. Nassimi, S. Sahni: Finding connected components and connected ones an a mesh-connected parallel computer, SIAM Journal on Computing, vol. 9, no. 4, Nov., S. 744-757, 1980. 1 Literaturverzeichnis [OW96] T. Ottmann, P. Widmayer: Algorithmen und Datenstrukturen, 3. Aufl., Spektrum Lehrbuch, 1996. [QU94] M. J. Quinn: Parallel Computing Theory and Practise, 2nd ed., Kapitel 12.2, 12.4-12.6, McGraw-Hill, 1994. [RR98] Rauber, T. und G. Rünger: Parallele und verteilte Programierung., Kapitel 2.3, 2.4, 2.7, 2.8. Springer 1998. [SE02] R. Sedgewick: Algorithmen, 2. Aufl., Pearson Studium, 2002. II