Graphalgorithmen Seminar parallele Programmierung SS 2003 Bernd Kruthoff und Jochen Olejnik 1 Gliederung 1. Motivation 2. Verbundene Komponenten - Hirschbergs Algorithmus 3. Minimal Spannender Baum - Kruskals Algorithmus - Sollins Algorithmus 4. Kürzeste Pfade von einem Knoten ausgehend - Moores Algorithmus 5. Zusammenfassung 2 1. Motivation • • • • Probleme der Praxis werden komplexer Lassen sich häufig durch Graphen darstellen Herkömmliche Algorithmen stoßen an Grenzen Lösung: Parallelisierung bekannter Algorithmen 3 Verbundene Komponenten • Finden aller verbundenen Komponenten in einem ungerichteten Graphen 3 mögliche Ansätze: A. Suchalgorithmen • durch Breiten- bzw. Tiefensuche durch den kompletten Graphen 4 Verbundene Komponenten B. Transitive Hülle • Grundlage: Adjazenzmatrix • Bestimmen der transitiven Hülle durch log n Plus-MinMultiplikationen • Plus-Min-Multiplikation ist Matrixmultiplikation bei der Skalamultiplikationen durch Additionen und Additionen durch Minimumoperationen ersetzt werden • => Strukturanalogie zur Matrixmultiplikation: • => Laufzeit: log n für eine Plus-Min-Multiplikation • => log 2 n für log n Plus-Min-Multiplikationen 5 C. Hirschbergs Algorithmus • Grundidee: Knoten zu Knotengruppen zusammenfassen bis kein weiteres Zusammenfassen mehr möglich ist • Jeder Knoten gehört zu genau einer Knotengruppe • Knotengruppen werden durch Wurzel (hier: kleinstes Element) identifiziert Der Algorithmus: 1. Schritt: Zu jedem Knoten wird die angrenzenden Knotengruppe mit der kleinsten Wurzel gesucht 2. Schritt: Verbinden der Wurzeln der in Schritt 1 gefundenen Knotengruppen 3. Schritt: Die in Schritt 2 gefundenen Knotengruppen werden zu einer Knotengruppe zusammengefasst • Endet, wenn es in Schritt 1 keine angrenzende Knotengruppe mehr gibt 6 Ein Beispiel: Die Ausgangssituation: 1 4 9 2 7 8 3 6 Knoten Knotengruppe 10 11 5 1 2 3 4 5 6 7 8 9 10 11 1 2 3 4 5 6 7 8 9 10 11 7 Beispiel: 1. Iteration 1 4 9 2 7 8 3 6 10 11 5 1. Schritt: Zu jedem Knoten wird die angrenzende Knotengruppe mit der kleinsten Wurzel gesucht 1 4 9 2 7 8 3 6 10 11 5 8 Beispiel: 1. Iteration 2. Schritt: Verbinden der Wurzeln der in Schritt 1 gefundenen Knotengruppen 1 4 9 2 7 8 3 6 10 11 5 9 Beispiel: 1. Iteration 1 4 9 2 7 8 3 6 10 11 5 3. Schritt: Die in Schritt 2 gefundenen Knotengruppen werden zu einer Knotengruppe zusammengefasst 1 4 9 2 7 8 3 6 10 11 5 10 Beispiel: 1. Iteration Ergebnis 1 4 9 2 7 8 3 6 10 11 5 Knoten 1 2 3 4 5 6 7 8 9 10 11 Knotengruppe 1 2 2 1 5 2 1 2 2 5 5 11 Beispiel: 2. Iteration 1 4 9 2 7 8 3 6 10 11 Letzte Iteration: 1 4 9 2 7 8 3 6 5 10 11 Startgraph: 5 1. Schritt: Zu jedem Knoten wird die angrenzenden Knotengruppe mit kleinster Wurzel gesucht 1 4 9 2 7 8 3 6 10 11 5 12 Beispiel: 2. Iteration 1 4 9 2 7 8 3 6 10 11 5 2. Schritt: Verbinden der Wurzeln der in Schritt 1 gefundenen Knotengruppen 1 4 9 2 7 8 3 6 10 11 5 13 Beispiel: 2. Iteration 1 4 9 2 7 8 3 6 10 11 5 3. Schritt: Die in Schritt 2 gefundenen Knotengruppen werden zu einer Knotengruppe zusammengefasst 1 4 9 2 7 8 3 6 10 11 5 14 Beispiel: 2. Iteration Ergebnis 1 4 9 2 7 8 3 6 Knoten 1 2 3 4 5 6 7 8 9 10 11 Knotengruppe 1 1 1 1 5 1 1 1 1 5 5 10 11 5 15 Komplexität • Der Algorithmus benötigt log n Iterationen weil sich die Anzahl der Knotengruppen mit jeder Iteration mindestens halbiert • Es werden n2 Prozessoren benötigt, weil maximal n benachbarte Knotengruppen pro Knoten verglichen werden müssen. • => Gesamtkomplexität log 2 n 16 Verbesserungen • Betrachten von Brents Theorem: • Es reichen log n Prozessoren aus um n Elemente anzusprechen und deren Minimum in log nZeit zu finden. • Jeder Prozessor kann log n Elemente anstatt nur einem ansprechen oder das Minimum aus log n Elementen berechnen, anstatt nur das Minimum aus zwei Elementen ohne die Zeitkomplexität zu verändern. • Der Algorithmus benötigt also log 2 n Zeit bei n logn n Prozessoren. 17 Eine Implementierung • Implementierung von Hirschbergs Algorithmus auf das 2D Mesh SIMD Rechnermodell (Nassimi, Sahni) • 3 neue Befehle: • random.read(a,[b]c) liest den Wert der Variablen c im Bereich von Prozess b in die Variable a ein • random.write(a,[b]c) schreibt den Wert der Variablen a in die Variable c im Bereich von Prozess b • Laufzeit dieser Operationen ist nur vom Rechner abhängig => konstant in Bezug auf die Anzahl der Elemente • bits(i,j,k) gibt den Wert der Stellen j bis k der Zahl i zurück • z. B. bits (9,3,2) : 9 ist binär 0101 zweite und dritte Stelle: 01 also wird 1 zurückgegeben • Ist j<k wird 0 zurückgegeben 18 public VERBUNDENE KOMPONENTEN(): Parameter: d //maximaler Knotengrad n //Anzahl Knoten = Anzahl Prozessoren Globale Variablen:k //betrachtete Kante iteration //Nummer der Iteration Lokale Variablen: kandidat //Wurzel des Nachbarknotens nachbar[1...d] //Nachbarknoten der aktuellen Knotengruppe w //Wurzel 1. begin 2. for (alle Pi where 1 i n ){ 3. w = i; jeder Knoten wird Wurzel 4. } 5. for (iteration = 0 tolog n 1 ){ 6. for (alle Pi where 1 i n ){ es gibt keine bekannten Nachbarn 7. kandidat=; =>Entfernung zu Nachbarn wird auf 8. } gesetzt 9. for (k=1 to d){ 10. for (alle Pi 1 i n where ){ Suchen des Nach11. HOLEN.UND.VERGLEICHEN(nachbar[k], w, kandidat); barknotens mit 12. } der kleinsten 13. } Wurzel 14. for (alle Pi where 1 i n ){ 15. AKTUALISIERE.WURZEL(kandidat, w); 16. } 17. ZUSAMMENFASSEN(w, n); //Zusammenfügen der Knotengruppen 18. } 19. end 19 private HOLEN.UND.VERGLEICHEN(v, w, kandidat); //entspricht 1. Schritt übergebene Werte: v //Nachbarknoten w, kandidat lokale Variablen: tmp //Wurzel des Nachbarknotens 20. begin 21. random.read(tmp, [v]w); 22. if (tmp==w){ 23. tmp=; Prüfen, ob v schon kandidat als Wurzel 24. } hat 25. kandidat=min(kandidat, tmp); 26. end private AKTUALISIERE.WURZEL(kandidat, w); //entspricht 2. Schritt übergebene Werte: kandidat, w 27. begin 28. random.write(kandidat[w]w); 29. if (w==){ hat diese Knotengruppe überhaupt 30. w=i; Nachbarn? 31. } 32. if (w>i){ 33. random.read(w, [w]w); gibt es einen Zyklus? 34. } 35. end 20 private ZUSAMMENFASSEN(w, n); //entspricht 3. Schritt übergebene Werte: w, n 36. begin 37. for (b=1 to log(n)){ 38. for (alle Pi){ 39. if (bits(w, log(n)-1, b)==bits(i, log(n), b)){ 40. random.read(w, [w]w); 41. } 42. } 43. } 44. end 21 Laufzeit dieser Implementierung 22 1. begin 2. for (alle Pi where 1 i n ){ 3. w = i; jeder Knoten wird Wurzel 4. } 5. for (iteration = 0 tolog n 1 ){ 6. for (alle Pi where 1 i n ){ es gibt keine bekannten Nachbarn 7. kandidat=; =>Entfernung zu Nachbarn wird auf 8. } gesetzt 9. for (k=1 to d){ 10. for (alle Pi 1 i n where ){ Suchen des Nach11. HOLEN.UND.VERGLEICHEN(nachbar[k], w, kandidat); barknotens mit 12. } der kleinsten 13. } Wurzel 14. for (alle Pi where 1 i n ){ 15. AKTUALISIERE.WURZEL(kandidat, w); 16. } 17. ZUSAMMENFASSEN(w, n); //Zusammenfügen der Knotengruppen 18. } 19. end • Loop (Zeilen 5-18) wird log n mal durchlaufen • In innerem Loop (Zeilen 9-13) wird d mal die Minimumoperation angewendet, welche O(n) Zeit verbraucht 23 Laufzeit dieser Implementierung • Loop (Zeilen 5-18) wird log n mal durchlaufen • In innerem Loop (Zeilen 9-13) wird d mal die Minimumoperation angewendet, welche O(n) Zeit verbraucht Auf dem 2D mesh-SIMD-Rechner mit n Prozessoren und n=2k Knoten und Maximalwert d hat dieser Algorithmus eine Komplexität von O(dn log n). 24 Minimal Spannender Baum • Finden des Minimal Spannenden Baumes in einem ungerichteten verbundenen gewichteten Graphen • Kruskals Algorithmus • Idee: Alle Knoten sind anfangs Bäume • Pro Iteration wird nun die kleinste noch nicht im Minimal Spannenden Baum vorhandene Kante eingefügt, falls dadurch kein Zyklus entstünde 25 Beispiel Kruskals Algorithmus A 4 2 7 A 4 D 4 2 B C 3 B 4 D 2 6 5 G 5 4 2 B 7 6 2 G B 4 D 4 4 D E 6 8 1 F C 3 7 2 C 3 A E 8 1 F A E 8 1 F C 3 7 2 2 5 E 6 8 1 F G 5 G 26 Beispiel Kruskals Algorithmus A 4 2 B 3 7 A 4 C 3 7 F 4 D 6 5 2 G 4 2 B C 3 7 4 D 2 E 6 8 1 F 5 G E 6 8 1 F A E 8 1 D 4 2 B C 2 5 G 27 Der Heap als Datenstruktur • Parallelisierung durch geschickte Wahl der Datenstruktur • Hier bietet sich der Heap an! => Gute Laufzeiteigenschaften • Hier wird der Heap mit -wertigen Knoten zu einem vollständigen Binärbaum ausgebaut • Lemma: Man kann mit einer UMA-Multiprozessormaschine mit log nProzessoren ein Element aus einer n-elementigen Menge in konstanter Zeit aus dem Heap auslesen. 28 Der Heap als Datenstruktur • Jede Ebene hat Flag mit Wert => voll wenn alle Knoten belegt => leer wenn es leere Knoten in Ebene gibt • Wert empty_node zeigt pro Ebene Knoten, in dem Wert fehlt • Jeder Prozessor ist für eine Ebene zuständig. Wird ein Knoten leer so füllt ihn der zuständige Prozessor aus dem kleinsten Element der Kinder dieses Knotens wieder auf • Wird ein Blatt leer, so wird der Wert eingetragen • Heapaufbau kann durch Prescheduling parallelisiert werden 29 Ein Beispielheap E Heap 1 1 5 2 2 5 4 3 4 3 7 6 7 21 8 9 10 25 18 11 12 12 30 14 27 15 30 F EN Voll kein Leer 2 Leer 6 Voll kein 30 Der Gesamtalgorithmus • Aufbauen des Heap • Prozess entnimmt fortlaufend Knoten aus dem Heap und überprüft, ob sie zum Minimal Spannenden Baum gehören • Die restlichen Prozesse stellen währenddessen den Heap wieder her • Terminiert, wenn ausgelesen wird • Laufzeit: Wiederaufbau des Heap und Überprüfung auf Zyklus (Find und Union) geschieht in konstanter Zeit • Entnahme der Knoten in konstanter Zeit möglich • =>Gesamtalgorithmus benötigt O(n) Zeit 31 Wechsel.... 32