Olejnik

Werbung
Westfälische Wilhelms-Universität Münster
Ausarbeitung
Graphalgorithmen
Im Rahmen des Seminars Parallele Programmierung im SS 2003
Jochen Olejnik
Themensteller: Prof. Dr. Herbert Kuchen
Betreuer: Prof. Dr. Herbert Kuchen
Institut für Wirtschaftsinformatik
Praktische Informatik in der Wirtschaft
Inhaltsverzeichnis
1
Einleitung................................................................................................................... 1
2
Verbundene Komponenten ........................................................................................ 1
3
2.1
Suchalgorithmen ............................................................................................... 1
2.2
Transitive Hülle ................................................................................................ 2
2.3
Hirschbergs Algorithmus.................................................................................. 2
2.3.1
Die Idee......................................................................................................... 2
2.3.2
Parallelisierung ............................................................................................. 2
2.3.3
Komplexität .................................................................................................. 3
Single Source Shortest Path....................................................................................... 6
3.1
4
Moores Algorithmus ......................................................................................... 6
3.1.1
Die Idee......................................................................................................... 6
3.1.2
Parallelisierung ............................................................................................. 8
3.1.3
Terminierungsüberlegungen ....................................................................... 10
3.1.4
Probleme ..................................................................................................... 10
Minimum Cost Spanning Tree................................................................................. 12
4.1
Sollins Algorithmus ........................................................................................ 12
4.1.1
Die Idee....................................................................................................... 12
4.1.2
Parallelisierung ........................................................................................... 13
4.1.3
Komplexität ................................................................................................ 14
4.2
Kruskal............................................................................................................ 15
4.2.1
Die Idee....................................................................................................... 15
4.2.2
Der Heap als Datenstruktur......................................................................... 15
4.2.3
Heapaufbau ................................................................................................. 16
4.2.4
Der parallele Algorithmus .......................................................................... 17
5
Schlusswort.............................................................................................................. 17
6
Literaturverzeichnis ................................................................................................. 18
II
Kapitel 1: Einleitung
1 Einleitung
Viele Probleme der Praxis lassen sich durch Graphen beschreiben. Als Beispiele seien
hier die optimale Anordnung von Chips auf einer Rechnerplatine, das TravelingSalesman-Problem
sowie
diverse
andere
Wegfindungsprobleme
und
das
kostengünstigste Verlegen von Versorgungsleitungen genannt.
Diese Probleme werden im Alltag immer komplexer, wodurch herkömmliche Algorithmen an ihre Grenzen stoßen. Es wurde versucht, bereits bekannte Algorithmen für
die wichtigsten Problemgruppen auf Parallelrechner anzupassen, um dem steigenden
Rechenbedarf entgegenzutreten.
In den einzelnen Kapiteln dieser Ausarbeitung werden nun beispielhaft parallele Lösungen für die wichtigsten Graphenprobleme vorgestellt. Für das Verbundene-Komponenten-Problem wird Hirschbergs Algorithmus näher betrachtet. Danach wird dann ein Lösungsansatz des Single-Source-Shortest-Path Problems anhand des Algorithmus von
Moore präsentiert. Das 4. Kapitel beschäftigt sich schließlich mit den Algorithmen von
Sollin und Kruskal, welche dem Minimum-Cost-Spanning-Tree Problem zuzuordnen
sind.
Zunächst wird in den Kapiteln auf die Funktionsweise der einzelnen Algorithmen eingegangen. Danach werden deren Laufzeit und einige Implementierungsbeispiele genauer unter die Lupe genommen. Es wird auch auf Probleme eingegangen, die durch die
Parallelisierung von ursprünglich sequentiellen Algorithmen entstehen können.
2 Verbundene Komponenten
Beim Verbundene Komponenten Problem geht es darum, alle verbundenen Knoten eines Graphen zu finden. Für verbundene ungerichtete Graphen gibt es dafür 3 Lösungsansätze.
2.1 Suchalgorithmen
Wenn man mittels Tiefen- oder Breitensuche den kompletten Graphen durchsucht, dann
löst man dies Problem automatisch.
1
Kapitel 2: Single Source Shorest Path
2.2 Transitive Hülle
Hat man die Adjazenzmatrix eines Graphen, so beschreibt die transitive Hülle alle verbundenen Komponenten. Man berechnet die transitive Hülle indem man auf die Adjazenzmatrix log n  Plus-Min-Multiplikationen anwendet. Eine Plus-Min-Multiplikation
ist eine Matrixmultiplikation bei der Skalarmultiplikationen durch Skalaradditionen und
Additionen durch die Minimumoperation ersetzt werden. Durch diese Strukturanalogie
(
)
hat dieser Lösungsansatz die Komplexität Θ n log 2 n , da n Plus-Min-Multiplikationen
(
)
durchgeführt werden und eine solche Multiplikation die Komplexität Θ log 2 n hat.
[Dekel et al.].
2.3 Hirschbergs Algorithmus
2.3.1 Die Idee
Die Grundidee hinter Hirschbergs Algorithmus ist, dass Knoten zu Knotengruppen zusammengefasst werden, bis alle verbundenen Knoten gefunden sind. Dabei gehört jeder
Knoten zu genau einer Knotengruppe, welche am Anfang nur aus ihm selbst besteht.
Eine Knotengruppe wird durch ihre Wurzel (hier: ihr kleinstes Element) identifiziert.
Die zugrundeliegende Datenstruktur ist die Adjazenzmatrix.
2.3.2 Parallelisierung
Der parallele Algorithmus durchläuft in jeder Iteration folgende 3 Schritte:
1. Schritt: Zu jedem Knoten wird die angrenzende Knotengruppe mit der kleinsten
Wurzel gesucht.
2. Schritt: Die Wurzeln der Knotengruppen, die sich in Schritt 1 gefunden haben, werden verbunden.
3. Schritt: Alle so verbundenen Knotengruppen werden zu jeweils einer Gruppe zusammengefasst.
2
Kapitel 2: Single Source Shorest Path
Die Startsituation:
1
4
9
2
10
11
Knoten 1 2 3 4 5 6 7 8 9 10 11
7
8
3
6
5
Knoten1 2 3 4 5 6 7 8 9 10 11
gruppe
1. Iteration
2. Iteration
Schritt 1:
Schritt 1:
1
4
9
2
7
8
3
6
10
11
1
4
9
2
5
7
8
3
6
10
11
5
Schritt 2:
Schritt 2:
1
4
9
2
7
8
3
6
10
11
1
4
9
2
5
7
8
3
6
10
11
5
Schritt 3:
Schritt 3:
1
4
9
2
7
8
3
6
10
11
1
4
9
2
5
7
8
3
6
10
11
5
Ergebnis:
Ergebnis:
Knoten 1 2 3 4 5 6 7 8 9 10 11
Knoten1 2 2 1 5 2 1 2 2 5 5
gruppe
Knoten 1 2 3 4 5 6 7 8 9 10 11
Knoten1 1 1 1 5 1 1 1 1
gruppe
5
5
Abbildung 1: Hirschbergs Algorithmus, ein Beispiel
2.3.3 Komplexität
Der Algorithmus benötigt log n  Iterationen, weil sich die Anzahl der Knotengruppen
pro Iteration mindestens halbiert. Dazu werden n2 Prozessoren benötigt, da maximal n
benachbarte Knotengruppen pro Knoten verglichen werden müssen. Die Anzahl der
(
)
2
Prozessoren sowie die Laufzeit von Θ log n lassen sich mit ein paar Zusatzüberle-
gungen verbessern:
3
Kapitel 2: Single Source Shorest Path
•
Nutzt man Brents Theorem aus, kann man mit
 n 
 log n 


Prozessoren n Variablen an-
sprechen und deren Minimum finden, weil jeder Prozessor im 1. Schritt log n Elemente auf einmal ansprechen kann und nicht nur eines. Des Weiteren kann jeder Prozessor
im 2. Schritt das Minimum aus log n Elementen auf einmal finden, ohne die Komplexität des Algorithmus zu erhöhen. Es reichen also n  n  Prozessoren aus um dieses
 log n 
2
Problem in Θ log n Zeit zu lösen. [Chin, Lam, Chen]
(
•
)
Wenn man mit einbezieht, dass in jeder Iteration die Betrachtung der Wurzeln der
Knotengruppen ausreicht und isolierte Bäume nicht mehr weiter betrachtet werden
( )
müssen, so verringert sich die benötigte Anzahl der Prozessoren auf Θ logn2 n bei
(
)
Θ log2 n Zeit. [Chin, Lam, Chen]
•
Nassimi und Sahni haben Hirschbergs Algorithmus auf dem q-dimensional-mesh
connected-SIMD-Modell implementiert. Für den Fall des 2-dimensionalen Gitters wird
nun auf eine Beispielimplementierung eingegangen. Zunächst müssen drei neue Befehle eingeführt werden. Der Befehl random.read(a,[b]c) liest den Wert der Variablen c
im Bereich von Prozess b in die Variable a ein. Der Befehl random.write(a,[b]c)
schreibt den Wert der Variablen a in die Variable c im Bereich von Prozess b. Die
Komplexität dieser beiden Operationen ist auf dem betrachteten Modell O(q2p), wobei
q die Dimension des Prozessornetzes und p die Anzahl der Prozessoren in einer Dimension beschreibt [DaBr]. Da sowohl q als auch p nur vom verwendeten System abhängig und somit extern gegeben sind, kann deren Laufzeit bei der Laufzeitbetrachtung als konstant angesehen werden. Diese Implementierung benutzt zusätzlich noch
die Funktion bits(i,j,k). Dabei werden die Stellen von j bis k von der Binärdarstellung
der Zahl i betrachtet und deren Wert zurückgegeben. Z. B. gibt Bits(10,3,2) =2 zurück.
Die Binärdarstellung von 10 lautet 1010. Nimmt man nun die Bits, zwischen Stelle j
und k (man fängt bei 0 an zu zählen), so erhält man hier binär 10, also die Dezimale 2.
Ist j<k, so wird 0 zurückgegeben.
4
Kapitel 2: Single Source Shorest Path
public VERBUNDENE KOMPONENTEN():
Parameter:
d
//maximaler Knotengrad
n
//Anzahl Knoten = Anzahl Prozessoren
Globale Variablen: k
//betrachtete Kante
interation
//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 to log 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 where 1 ≤ i ≤ n ){
 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
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
5
Kapitel 2: Single Source Shorest Path
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
Abbildung 2: Hirschbergs Algorithmus in Pseudocode
•
Der Algorithmus hat bei obiger Implementierung auf einem 2D-mesh-SIMDRechner mit n Prozessoren und n=2k Knoten mit Maximalwert von d eine Komplexität
von O(dn log n) [Nassimi, Sahni]. Der äußere Loop (Zeilen 5-18) wird log n mal
durchlaufen. Im inneren Loop (Zeilen 9-13) wird d mal die Minimumoperation angewendet, welche O(n) Zeit verbraucht.
3 Single Source Shortest Path
Beim Single Source Shortest Path Problem besteht die Aufgabe darin, zu einem Knoten
den jeweils kürzesten Pfad zu jedem anderen Knoten in einem gewichteten, gerichteten
Graphen zu finden.
3.1 Moores Algorithmus
3.1.1 Die Idee
Zur Vorbereitung wird die Entfernung vom Startknoten S zu den jeweils anderen Knoten auf ∞ und zu sich selbst auf 0 gesetzt.
Es gibt eine Queue, in der die Knoten aufgeführt sind, die als nächstes betrachtet werden sollen. Anfangs befindet sich hier nur der Startknoten .
Nun wird das oberste Element der Queue entfernt. Für alle Verbindungen vom Startknoten zu anderen Knoten wird überprüft, ob es kürzere Wege über den zu überprüfenden
Knoten und dessen ausgehende Kanten gibt. Ist dies der Fall, so wird die entsprechende
Verbindung aktualisiert und deren Zielknoten an das Ende der Queue eingefügt, wenn
er nicht bereits in der Queue enthalten ist.
6
Kapitel 3: Minimum Cost Spanning Tree
Der Algorithmus läuft nun so lange weiter, bis die Queue leer ist. Hier ein Beispiel:
Entf.
Q Schritt 2
A 0 A
2
2
F
E
F
E
B ∞
C ∞
D
D
12
12
2 1
2 1
3
3
D ∞
2
2
E ∞
A
B
C
A
B
C
6
6
F ∞
Entf.
Q Schritt 4
Schritt 3
A 0 C
2
2
F
E
F
E
B 2 D
C 8 F
D
D
12
12
2 1
2 1 D 5
3
3
2
2
E ∞
A
B
C
A
B
C
6
6
F 14
Entf.
Q Schritt 6
Schritt 5
A 0 F
2
2
F
E
F
E
B 2 E
C 7 C
D
D
12
12
2
1
2
1
3
3
D 5
2
2
E 9
A
B
C
A
B
C
6
6
F 14
Entf.
Q Schritt 8
Schritt 7
A 0 C
2
2
F
E
F
E
B 2 F
C 7
D
D
12
12
2 1 D 5
2 1
3
3
2
2
E 9
A
B
C
A
B
C
6
6
F 11
Entf.
Q Schritt 10
Schritt 9
A 0 E
2
2
F
E
F
E
B 2
C 7
D
D
12
12
2 1
2 1
3
3
D 5
2
2
E 8
A
B
C
A
B
C
6
6
F 11
Entf.
Q Hinweis:
Schritt 11
A 0
2
F
E
Entf. = Entfernungstabelle
B 2
C 7
D
12
2
1
3
Q = Queue
D 5
2
E 8
A
B
C
6
F 10
Abbildung 3: Der sequentielle Algorithmus von Moore
Schritt 1
Entf.
A 0
B 2
C ∞
D ∞
E ∞
F ∞
Entf.
A 0
B 2
C 8
D 5
E 9
F 14
Entf.
A 0
B 2
C 7
D 5
E 9
F 14
Entf.
A 0
B 2
C 7
D 5
E 8
F 11
Entf.
A 0
B 2
C 7
D 5
E 8
F 10
Q
B
Q
D
F
E
Q
E
C
Q
F
E
Q
F
7
Kapitel 3: Minimum Cost Spanning Tree
public SHORTEST PATH():
Globale Variablen: entfernung [I]
//Entfernung von S zu I
S
//Startknoten
wert[U,V]
//Wert der Kante zwischen U und V
Parameter:
n
//Anzahl Knoten im Graph
1. begin
2.
for (i=1 to n) {
3.
initialisiere(i);
4.
}
5.
enqueue(S);
6.
while (queue ist nicht leer){
7.
suche();
8.
}
9. end
private SUCHE():
Lokal Variablen:
neue.entfernung
U
V
//Entfernung zu V über U
//Startknoten der betrachteten Kante
//Endknoten der betrachteten Kante
10. begin
11.
dequeue(U);
12.
for (jede Kante {U,V} im Graphen){
13.
neue.entfernung = entfernung[U] + wert[U,V];
14.
if (neue.entfernung < entfernung[V]){
15.
entfernung[V] = neue.entfernung;
16.
if (V ist nicht in der queue){
17.
enqueue(V);
18.
}
19.
}
20.
}




Überprüfen ob es
durch neue Kante
kürzere Entfernungen gibt
end
Abbildung 4: Moores sequentieller Algorithmus in Pseudocode
3.1.2 Parallelisierung
Es gibt zwei mögliche Ansätze diesen Algorithmus zu parallelisieren. Der Erste sieht
die parallele Bearbeitung aller ausgehenden Kanten eines zu überprüfenden Knotens vor
(Zeilen 12-20), während der Zweite mehrere Knoten der Queue auf einmal überprüft
(Zeilen 6-8). Unabhängig von der Wahl des Ansatzes kann die Initialisierung (Zeilen 24) durch Prescheduling parallelisiert werden.
Der zweite Ansatz ist vorzuziehen, da das Problem hierdurch in größere Einheiten aufgespaltet wird und die Performanz des 1. Ansatzes sehr stark von der Anzahl der ausgehenden Knoten des Graphen abhängt.
Nachdem die Queue mit dem Startknoten initialisiert wurde, werden eine Reihe von
asynchronen Prozessen generiert, welche jeweils einen Knoten aus der Queue löschen,
dessen ausgehende Kanten betrachten und Knoten mit kürzeren Pfaden in die Queue
einfügen.
8
Kapitel 3: Minimum Cost Spanning Tree
public SHORTEST PATH():
Globale Variablen: entfernung [I]
//Entfernung von S zu I
S
//Startknoten
wert[U,V]
//Wert der Kante zwischen U und V
halt
//true wenn Algorithmus beendet
wartend[i]
//true, wenn Prozess i wartet
Parameter:
n
//Anzahl Knoten im Graph
p
//Anzahl Prozessoren
1. begin
2.
for (alle Pi where 1 ≤ i ≤ p ){
3.
for (j = i to n step p) {
4.
initialisiere(j);
5.
}
6.
}
7.
enqueue(S);
8.
halt = false;
9.
for (alle Pi where 1 ≤ i ≤ p ){
10.
repeat {suche(i);} until (halt == true);
11.
}
12. end
private SUCHE(i):
Lokal Variablen: neue.entfernung
//Entfernung zu V über U
U
//Startknoten der betrachteten Kante
V
//Endknoten der betrachteten Kante
Übergebene Variablen: i
//Prozessnummer
1. begin
2.
sperre (queue);
3.
if (queue ist leer){
4.
wartend[i] = true;
5.
if (i = 1 && wartend[1] && wartend[2] &&...&& wartend[p]){
6.
halt = true;
//Terminierungsbedingung
7.
}
8.
entsperre (queue);
9.
}
10.
else {
11.
dequeue(U);
12.
wartend[i] = false;
13.
entsperre (queue);
14.
for (jede Kante {U,V} im Graphen){
15.
neue.entfernung = entfernung[U] + wert[U,V];
16.
sperre (entfernung[V]);
17.
if (neue.entfernung < entfernung[V]){
18.
entfernung[V] = neue.entfernung;
19.
entsperre (entfernung[V]);
20.
if (V ist nicht in der queue){
21.
sperre (queue);
22.
if (V ist nicht in der queue){
23.
enqueue(V);
24.
}
25.
}
26.
entsperre (queue);
27.
}
28.
else {entsperre (entfernung[V]);};
29.
}
30.
}
31. end
Abbildung 5: parallele Version von Moores Algorithmus
9
Kapitel 3: Minimum Cost Spanning Tree
3.1.3 Terminierungsüberlegungen
Da man oben einige asynchrone Prozesse initialisiert, kann man nicht davon ausgehen,
dass der Algorithmus automatisch terminiert, wenn die Queue leer ist. Um festzustellen,
ob die Queue leer ist und gerade keine Knoten bearbeitet werden, führt man zwei neue
Variablen ein. Die Erste ist das array waiting[], in welchem für jeden Prozessor eine
boolesche Variable gespeichert ist, die den Wert false hat, wenn der Prozessor gerade
arbeitet. Des Weiteren wird die boolesche Variable halt eingeführt, welche true ist,
wenn alle Prozessoren warten, also wenn alle Werte in waiting[] true sind und die
Queue leer ist. Dies überprüft Prozess 1 jedes mal, wenn er mit einem Prozess fertig ist
und eine leere Queue vorfindet (Zeilen 3-7 / Abb.5). Ist dies der Fall so setzt er halt auf
true, was den Algorithmus terminiert. Anfangs werden alle Werte von waiting[], sowie
der Wert von halt auf false initialisiert.
3.1.4 Probleme
Enqeue und Dequeue sind keine atomaren Operationen, deshalb kann es zu Fehlern
kommen, wenn während dieser Operationen die Queue verändert wird. Für die Zeit dieser Operationen benötigt der betreffende Prozess also exklusiven Zugriff auf die Queue.
Die Queue muss zusätzlich noch während des Vergleiches (Zeilen 13-15 / Abb.4) für
andere Prozesse gesperrt werden, um zu verhindern, dass zwei Prozesse gleichzeitig den
selben Wert aktualisieren und somit der erste geschriebene Wert überschrieben wird.
Schließlich muss die Queue noch gesperrt werden, während Prozess 1 überprüft, ob alle
Prozesse warten.
Durch das häufige Sperren der Queue wird das Beschleunigungspotential durch die Parallelisierung stark eingeschränkt.
Dieses Problem lässt sich umgehen, indem jeder Prozess seine eigene private Queue
bekommt, was jedoch zu einer ungleichen Auslastung der einzelnen Prozessoren führen
kann. Eine Kompromisslösung sieht vor, dass jeder Prozess Elemente in seinen privaten
Bereich einfügen darf. Dann werden diese Listen zusammengefügt und jeder der p Prozesse löscht und bearbeitet genau jedes p-te Element dieser großen Liste. Als Datenstruktur für diese Liste bietet sich ein Linked-Array an, da man in diese Datenstruktur
sehr einfach unterschiedlich große Elementketten einfügen kann. Im Folgenden wird
das Linked-Array genauer beschrieben:
10
Kapitel 3: Minimum Cost Spanning Tree
Es ist p(w+p) Elemente groß, wobei w die maximal von einem Prozess in einer Iteration
einzufügenden Elemente beschreibt. Jeder Prozessor hat also einen Bereich der Größe
w+p, in dem er seine Elemente ablegen kann. Die Namen der von Prozessor i
( 1 ≤ i ≤ p ) erzeugten Knoten ei, die in der nächsten Iteration untersucht werden sollen,
stehen im Speicherbereich von (i-1)(w+p)+1 (erster Wert, den Prozessor i einfügt) bis
(i-1)(w+p)+ei (letzter Wert, den Prozessor i einfügt). Die Elemente mit den Adressen (i1)(w+p)+ei +1 bis (i-1)(w+p)+ei +p (der Teil des Adressraumes der nicht mit zu berechnenden Werten belegt wird) enthalten die Werte (Zeiger) –i(w+p+1) bis –[i(w+p)+p].
Ein Beispiel:
p=4, w=3 => Das Array hat die Länge 28
Prozessor 1 fügt zwei Elemente in die Queue ein, Prozessor 2 drei, Prozessor 3 zwei und Prozessor 4 ein Element.
Bereich Prozessor 1
Bereich Prozessor 2
Bereich Prozessor 3
Bereich Prozessor 4
5
10
15
20
25
e e 8 9 10 11
e e e 15 16 17 18 e e 22 23 24 25
e 29 30 31 32
1 2 3 4 1 2
3 4 1 2 3 4 1 2 3 4 1 2 3
4 1 2 3 4
Die doppelt umrandete Zeile stellt das Array da. Die grünen Kästchen stehen für die Felder, in
denen zu berechnende Werte e stehen, die Blauen sind Pointer und in der Implementierung eigentlich negativ um sie von den Werten zu unterscheiden. Die untere Zahl beschreibt, welcher
Prozess welche Felder abgeht. So bearbeitet Prozess 1 beispielsweise die Elemente 1,5,10,14,18
und 23. Der Zeiger in Feld 23 zeigt auf Feld 29, daher merkt der Prozess, dass er am Ende des
Arrays angelangt ist.
Abbildung 6: Ein Beispiel zur Funktionsweise des Linked-Array.
Jeder Prozessor i untersucht nun jede p-te Speicherstelle, angefangen mit Speicherstelle
i. Ist der zu durchsuchende Wert negativ, so wird er als Zeiger interpretiert und das
nächste Element, auf welches der Zeiger zeigt, wird überprüft. Ist der Wert hingegen
positiv, so wird er durch den Algorithmus untersucht. Anschließend springt der Prozess
p Elemente weiter und bearbeitet den Wert, den er dort findet. Ist der vorgefundene
Wert kleiner als –p(w+p), zeigt der Zeiger also auf einen Wert außerhalb des Arrays, so
ist das Array durchlaufen und der Prozess ist mit dieser Iteration fertig.
Bei dieser Art der Abarbeitung der Elemente des Linked-Arrays bei mehr als einem
Prozessor beträgt der Unterschied in der Belastung der Prozessoren höchstens ein Element, die Arbeit wird also optimal aufgeteilt [Quinn 83].
11
Kapitel 3: Minimum Cost Spanning Tree
Ein Problem des Linked-Array ist jedoch, dass im worst case ein Prozess alle zu bearbeitenden Knoten einfügt, was dazu führt, dass der p-fache Speicherplatz gegenüber
einem normalen Array benötigt wird. Diesem Problem kann begegnet werden, indem
die neu zu bearbeitenden Knoten zunächst auf die privaten Listen aufgeteilt werden,
bevor die große Liste erstellt wird.
Praktischerweise werden zwei Linked-Arrays benutzt. Aus dem Einen wird gelesen,
während in das andere geschrieben wird. In der folgenden Iteration tauschen die beiden
Arrays die Rollen.
Eine andere für dieses Problem geeignete Datenstruktur ist die Double-Ended-Queue.
4 Minimum Cost Spanning Tree
Das Minimum Cost Spanning Tree Problem ist mit dem Connected-ComponentsProblem verwandt und hat somit auch eine ähnliche Komplexität. So beträgt die Komplexität für das Finden des Minimum-Cost-Spanning-Tree in einem ungerichteten Gra-
(
)
phen mit n Knoten auf einem Pyramiden-SIMD-Computer O log n .
Die wichtigsten drei sequentiellen Algorithmen sind die nach Sollin, Kruskal und PrimDijkstra. Die ersten Beiden werden im Folgenden näher betrachtet.
4.1 Sollins Algorithmus
4.1.1 Die Idee
Am Anfang wird jeder einzelne Knoten als Baum gesehen. In jeder Iteration sucht sich
jeder Knoten die Kante mit dem niedrigsten Wert, welche den aktuellen Baum mit einem anderen Baum verbindet. Diese Kante wird dann zum Minimum-Cost-SpanningTree hinzugefügt, wenn sich in diesem dadurch kein Zyklus ergibt. Tritt ein Problem
auf, weil zwei gleichwertige Kanten in einer Iteration eingefügt werden sollen, aber
wegen der Zyklenbedingung nur eine erlaubt ist, so wird zur Laufzeit willkürlich entschieden, welche Kante genommen wird (Kante (C,D) oder Kante (E,F) in Iteration 3
im Beispiel).
12
Kapitel 4: Verbundene Komponenten
Vorher:
2
A
5
Nach Iteration 1:
4
B
3
4
I
7
C
5
1
G
A
2
5
E
2
2
D
5
G
H
4
B
3
4
7
Nach Iteration 2:
C
5
1
F
2
5
2
D
A
G
H
3
5
E
2
4
B
4
7
C
5
D
1
F
2
5
E
2
H
Abbildung 7: Sollins Algorithmus
4.1.2 Parallelisierung
public Minimum Cost Spanning Tree():
Parameter:
n
//Anzahl Knoten
Globale Variablen:nächster[i] //Entfernung von Baum i zum nächsten Knoten
kante[U,V] //Kante zwischen den Knoten U und V, die den
aktuellen
Baum
mit
dem
nahesten
Baum
verbindet
T
//Menge der Kanten im Minimum-Cost-SpanningTree
V,W
//Knoten, zwischen denen die zu betrachtende
Kante liegt
wert[U,V]
//Wert der Kante zwischen U und V
i
//Teilbäume
1. begin
2.
for (i=1 to n){
3.
Knoten i gehört zu Teilbaum i;
4.
}
5.
T ist leer;
6.
while (Anzahl Kanten in T < n-1){
7.
for (alle i){
8.
nächster[i]=∞;
9.
}
10.
for (alle kante[V,W]){
11.
if (find(V) ≠ find(W)){
12.
if (wert[V,W] < nächster[find(V)){
13.
nächster[find(V)] = wert[V,W];
14.
kante[find(V)] = [V,W];
15.
}
16.
}
17.
}
18.
for (alle Bäume i){
19.
[V,W] = kante[i];
20.
if (find(V) ≠ find(W)){
21.
T = T ∪ [V,W];
22.
union(V,W);
23.
}
24.
}
25.
}
26. end
Abbildung 8: sequentielle Version von Sollins Algorithmus
13
Kapitel 4: Verbundene Komponenten
Hier wird die Parallelisierung auf dem UMA Multiprozessor Modell näher betrachtet.
Die Funktion Find(k) sucht hierbei die Knotengruppe zu einem Knoten k, während die
Funktion Union(v,w) zwei Knotengruppen miteinander verbindet. Da es bei Parallelisierungen immer günstig ist, das Gesamtproblem möglichst weit außen aufzuteilen, würde
man hier am liebsten den äußeren while-loop parallelisieren, das geht allerdings aufgrund der starken Abhängigkeiten zwischen den einzelnen Iterationen nicht. Also werden die drei for-Schleifen folgendermaßen parallelisiert:
1. Schleife (Zeilen 7-9): Prescheduling. Jedem Prozessor werden 1/p Bäume zugeteilt.
2. Schleife (Zeilen 10-17): Prescheduling. Die Bäume werden unter den Prozessoren
fair aufgeteilt (z.B. nach Anzahl der zu überprüfenden Kanten).
3. Schleife (Zeilen 18-24): Hier kann das Prescheduling nicht einfach eingesetzt werden, weil es zu einem Problem kommen kann, wenn ein Teilbaum A sich mit einem Teilbaum B verbinden möchte und dieser Teilbaum B versucht sich gleichzeitig auch mit Teilbaum A zu verbinden. Wenn nun beide Prozesse Zeile 20
vor der Unionoperation in Zeile 22 abarbeiten, werden beide Kanten hinzugefügt und es ergibt sich ein Zyklus. Dies lässt sich lösen, indem die kritische Region (Zeilen 20-23) gesperrt wird, sobald sich ein Prozess in ihr befindet.
4.1.3 Komplexität
Weil die Anzahl der Bäume mit jeder Iteration mindestens halbiert wird, kommt der
Algorithmus mit log n  Iterationen aus. In einer Iteration werden höchstens O(n2) Vergleiche benötigt. Es ergibt sich also zusammen für den sequentiellen Algorithmus eine
Komplexität von O(n 2 log n ) . Die Operationen Find und Union lassen sich quasi in konstanter Zeit O(log* n ) realisieren und brauchen deshalb hier nicht gesondert betrachtet
werden [Hopcraft, Ullmann 1973].
Um auf die Komplexität des parallelen Algorithmus zu kommen, muss man sich die
einzelnen Schleifen ansehen:
Zeile Komplexität Begründung
7-9
1017
1824
O
(
n
p
O
(
n2
p
+p
O
(
n
p
p+ p
+p
)
n Bäume werden auf p Prozessoren aufgeteilt.
Es müssen höchstens n2 Verbindungen, aufgeteilt auf p Prozesso-
)
ren überprüft werden.
)
Das Zusammenfügen von maximal n Bäumen wird auf die p Prot ilt Ei P
i
l
l di B
b i
14
Kapitel 4: Verbundene Komponenten
= O(n + p )
24
zessorenverteilt. Ein Prozess muss maximal p mal die Bearbeitungszeit darauf warten, dass er in den kritischen Bereich eintreten
darf.
Der Summand p hinter den Komplexitäten beschreibt den Aufwand für die
Synchronisation der p Prozessoren.
(
(
Es ergibt sich also insgesamt eine Komplexität von O log n 
n2
p
+ np + n + p
)) für den
parallelen Sollin Algorithmus auf der UMA-Multiprozessormaschine.
4.2 Kruskals Algorithmus
4.2.1 Die Idee
Am Anfang wird beim Kruskal-Algorithmus ein weiteres Mal jeder Knoten als Baum
betrachtet. Daraufhin werden die Kanten des Graphen in den Minimum Cost Spanning
Tree nach ihrem Wert in aufsteigender Reihenfolge eingefügt. Würde durch das Einfügen ein Zyklus entstehen, so wird die betreffende Kante nicht eingefügt, sondern verworfen.
Iteration 1:
A
4
2
C
3
B
7
Iteration 2:
2
4
D
8
A
E
1
F
5
6
2
G
B
Iteration 4:
A
4
2
B
7
4
D
C
3
7
Iteration 3:
2
4
D
8
A
E
1
F
5
6
2
G
B
Iteration 5:
2
C
3
4
8
F
A
E
1
5
6
2
G
B
4
7
4
D
3
7
2
C
4
D
8
E
6
1
F
5
G
Iteration 6:
2
C
3
4
8
F
A
E
1
5
6
2
G
B
4
C
3
7
4
D
2
E
6
8 1
F
5
G
Abbildung 9: Kruskals Algorithmus
4.2.2 Der Heap als Datenstruktur
Bei diesem Algorithmus lässt man die meiste Arbeit die Datenstruktur machen. Da immer nur die jeweils kleinstbewertete Kante benötigt wird, bietet sich hier der Heap an.
15
Kapitel 4: Verbundene Komponenten
Lemma: Man kann mit einer UMA-Multiprozessormaschine mit log n  Prozessoren
ein Element aus einer n-elementigen Menge in konstanter Zeit aus dem Heap auslesen.
[Yoo 1983].
Der Heap ist hier ein vollständiger Binärbaum der Tiefe p= log n  , wobei fehlenden
Blättern der Wert ∞ zugewiesen wird. Er ist in einem Array gespeichert, wobei sich die
Kinder eines Knotens i bei den Adressen 2i und 2i+1 befinden. Jedem der p benötigten
Prozessoren wird eine Ebene zugeteilt. Der Algorithmus läuft nun so ab, dass für jede
Ebene festgehalten wird, ob sie vollständig ist oder nicht. Ist das nicht der Fall, so wird
der leere Knoten in der Variable empty-node(i) notiert. Der Prozessor, der für Ebene i
Ebene
5
Voll Keins
2
2
3
7
5
4
3
4
Flag
1
1
6
7
21
8
9
10
25
Empty
Node
Heap
18
11 12
12
30
14
27
15
30
Leer
2
Leer
6
Voll Keins
zuständig ist, füllt den Knoten nun mit dem Kind des leeren Knotens auf, welches den
kleinsten Wert hat. Wird ein Blatt leer, so bekommt es den Wert ∞. Prozessor 1 entnimmt nun kontinuierlich Werte aus Knoten 1 bis dieser den Wert ∞ hat, also alle Werte
ausgelesen wurden.
Abbildung 10: Ein Beispielheap während des Algorithmus
4.2.3 Heapaufbau
Der Loop, mit dem der Heap aufgebaut wird, kann durch Prescheduling parallelisiert
werden. Der Heap wird nun von unten nach oben erstellt. Bemerkt ein Knoten, dass
seine beiden Kinder Heaps sind, so bildet er mit ihnen einen neuen Heap.
16
Kapitel 4: Verbundene Komponenten
4.2.4 Der parallele Algorithmus
Der parallele Algorithmus baut zunächst den Heap auf. Danach entnimmt ein Prozess
fortlaufend Kanten und überprüft, ob diese in den Minimum Cost Spanning Tree eingefügt werden, oder nicht. Die restlichen Prozesse stellen währenddessen den Heap wieder
her. Die Entscheidung, ob durch die Hinzunahme einer bestimmten Kante ein Zyklus
entstünde oder nicht geschieht mit Hilfe der Operationen Find und Union in quasi konstanter Zeit.
Der Entnahmeprozess kann in konstanter Zeit Werte auslesen, da der Heap für den
Wiederaufbau konstante Zeit benötigt und dadurch immer Werte zum Auslesen vorhanden sind. Da jeder einzelne Teilbereich dieses Algorithmus in konstanter Zeit zu bewältigen ist, ergibt sich auch für den Gesamtalgorithmus eine Komplexität von O(n).
5 Schlusswort
Es wurden nun einige parallele Algorithmen und Probleme, welche durch die Parallelisierung auftauchen, vorgestellt. Ein Hauptproblem ist oft die Zugriffssteuerung auf Ressourcen, welche von mehreren Prozessen genutzt werden. Es ist nötig die so entstehenden kritischen Bereiche im Quellcode so abzusichern, dass sich nur ein Prozess in ihm
befindet. Dies beeinträchtigt den Geschwindigkeitsvorteil, den die Parallelisierung ja
bringen soll, oft stark. Ein weitere Schwierigkeit stellt die Koordination der einzelnen
Prozesse da, weil z. T. Wechselwirkungen zwischen den einzelnen Iterationen bestehen.
Einige Lösungsansätze können durch geschickte Wahl der zugrundeliegenden Datenstruktur wesentlich effizienter gestaltet werden. Andere Ansätze nutzen eine bestimmte
Rechnerarchitektur optimal aus und erreichen so gute Ergebnisse.
Da sich durch Kombinationen von bekannten Algorithmen, verschiedenen Datenstrukturen und Rechnerarchitekturen viele mögliche neue Lösungsansätze ergeben erschließt
sich in der Gestaltung von parallelen Algorithmen, welche auf die jeweilige Hardwareund Datenstruktur des Problems zugeschnitten sind ein breites Feld für Innovationen.
17
Kapitel 6: Literaturverzeichnis
6 Literaturverzeichnis
Quinn, M.J.: Parallel Computing Theory and Practise. McGraw-Hill 1994
Quinn, M.J.: Algorithmenbau und Parallelcomputer McGraw-Hill, 1988
[Dekel et al.] Delkel E., D. Nassimi, S. Sahni, 1981 Parallel matrix and graph algorithms SIAM Journal on computing vol. 10, no. 4, Nov., pp. 657-675
[Chin, Lam, Chen] Chin, F. Y., J. Lam and I. N. Chen, 1981 Optimal parallel algorithms
for the connected components problem. In the proceedings of the 1981 international
conference on parallel processing IEEE New York, Aug. pp.170-175
1982 Efficient parallel algorithms for some graph problems. Communications of the
ACM vol. 25 no. 9 Sept. pp 659-665
[DaBr] Data Broadcasting in SIMD computers, University of Minnesota, Technical Report # 79-17, 1979 IEEE Trans Comput.
[Nassimi, Sahni] Nassimi D., S. Sahni, 1980, Finding connected components and connected ones on a mesh-connected parallel computer. SIAM Journal on computing vol 9,
no.4, Nov. pp.744-757
[Quinn 83]: Quinn, M.J., The design and analysis of algorithms and data structures for
the efficient solution of graph-theoretic problems on MIMD computers. Ph.D. dissertation, Computer Science Dept., Washington State University, Pullman
[Hopcroft, Ullmann] Hopcroft, Ullmann 1973, Set merging Algorithms, SIAM Journal
on Computing, vol. 2, pp. 294-303
[Yoo1983] Yoo, 1983, Parallel processing for some network optimization problems. Ph.
D. dissertation, Computer Science Dept., Washington State University, Pullman
Herunterladen