Parallele Algorithmen - Ra.informatik.tu-darmstadt.de - Technische ...
Parallele Algorithmen - Ra.informatik.tu-darmstadt.de - Technische ...
Parallele Algorithmen - Ra.informatik.tu-darmstadt.de - Technische ...
Erfolgreiche ePaper selbst erstellen
Machen Sie aus Ihren PDF Publikationen ein blätterbares Flipbook mit unserer einzigartigen Google optimierten e-Paper Software.
Globaler Zellularautomat:<br />
<strong>Parallele</strong> <strong>Algorithmen</strong><br />
Diplomarbeit<br />
von<br />
Christine Ehrt<br />
Prüfer: Prof. Dr. Rolf Hoffmann<br />
Betreuer: Dipl.-Ing. Wolfgang Heenes<br />
Fachgebiet Rechnerarchitek<strong>tu</strong>r<br />
Fachbereich Informatik<br />
<strong>Technische</strong> Universität Darmstadt<br />
29. November 2005
Erklärung<br />
Ich versichere, dass ich diese Arbeit ohne unzulässige frem<strong>de</strong> Hilfe und nur unter Benutzung<br />
<strong>de</strong>r in <strong>de</strong>r Arbeit angegebenen Litera<strong>tu</strong>r und sonstigen Hilfsmitteln angefertigt<br />
habe.<br />
Darmstadt, <strong>de</strong>n 29. November 2005<br />
Danksagung<br />
Im <strong>Ra</strong>hmen meines S<strong>tu</strong>diums und meiner Diplomarbeit haben mir viele Menschen sehr<br />
geholfen. Bei einigen möchte ich mich gerne persönlich bedanken:<br />
Dipl.-Ing. Wolfgang Heenes, <strong>de</strong>r mit seinem Repititorium erstmals in mir das Interesse<br />
für die <strong>Technische</strong> Informatik weckte und dies dann mit <strong>de</strong>m Rechnertechnologiepraktikum<br />
noch vertiefte. Außer<strong>de</strong>m war er während meiner Diplomarbeitsphase immer für<br />
mich da und hat eine für mich individuell sehr intensive und gute Betreuung geboten.<br />
Prof. Dr. Rolf Hoffmann, <strong>de</strong>r mit seinen für mich interessanten Vorlesungen dieses Interesse<br />
noch weiter vertieft hat und mir schließlich auch ermöglichte, in diesem Gebiet<br />
meine Diplomarbeit zu schreiben.<br />
Dipl.-Inform. Mathias Halbach, <strong>de</strong>r mir bei Problemen <strong>de</strong>r Implementierung geholfen<br />
hat.<br />
Herrn Endisch, <strong>de</strong>r sein 1999 gegebenes Versprechen eingehalten und meine gesamte<br />
Diplomarbeit auf grammatikalische und Rechtschreibfehler überprüft hat. Außer<strong>de</strong>m<br />
hat er mich mit seinen Anmerkungen oft zum Nach<strong>de</strong>nken gebracht und somit zu einer<br />
besseren Arbeit beigetragen.<br />
Meinen Freun<strong>de</strong>n, die mir sowohl als Stütze als auch Korrek<strong>tu</strong>rleser zur Seite stan<strong>de</strong>n:<br />
Marcel Thies, Silke Schnei<strong>de</strong>r, Denis Endro und Stefan Müller.<br />
Ihnen allen spreche ich hiermit meinen herzlichen Dank aus und hoffe, mit meiner Diplomarbeit<br />
ihre Erwar<strong>tu</strong>ngen erfüllt zu haben.<br />
2
Inhaltsverzeichnis<br />
1. Einlei<strong>tu</strong>ng 10<br />
1.1. Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10<br />
1.2. Aufwandsbetrach<strong>tu</strong>ng . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11<br />
1.3. Struk<strong>tu</strong>r <strong>de</strong>r Arbeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11<br />
2. Zellularautomaten und an<strong>de</strong>re Mo<strong>de</strong>lle 13<br />
2.1. Die <strong>Ra</strong>ndom-Access-Maschinen . . . . . . . . . . . . . . . . . . . . . . . 13<br />
2.1.1. Prinzip <strong>de</strong>r RAM . . . . . . . . . . . . . . . . . . . . . . . . . . . 14<br />
2.1.2. Die P-RAM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14<br />
2.2. Die Pointer-Maschinen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15<br />
2.2.1. Maschinenmo<strong>de</strong>lle <strong>de</strong>r Pointer-Maschinen . . . . . . . . . . . . . . 16<br />
2.2.1.1. Kolmogorov-Uspenskii Maschinen . . . . . . . . . . . . . 16<br />
2.2.1.2. Storage Modification Maschines . . . . . . . . . . . . . . 16<br />
2.2.1.3. Knuths Linking Automat . . . . . . . . . . . . . . . . . 17<br />
2.2.1.4. Die LISP Maschinen . . . . . . . . . . . . . . . . . . . . 17<br />
2.2.2. Programmiermo<strong>de</strong>lle <strong>de</strong>r Pointer-Maschinen . . . . . . . . . . . . 17<br />
2.2.3. Parallel-Pointer Maschinen . . . . . . . . . . . . . . . . . . . . . . 18<br />
2.3. Die Zellularautomaten . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18<br />
2.3.1. Der klassische Cellular Automata“ (CA) . . . . . . . . . . . . . 19<br />
”<br />
2.3.1.1. Tesselation CA . . . . . . . . . . . . . . . . . . . . . . . 20<br />
2.3.1.2. Iterative CA . . . . . . . . . . . . . . . . . . . . . . . . 21<br />
2.3.1.3. Dynamische CA . . . . . . . . . . . . . . . . . . . . . . 21<br />
2.3.1.4. CA Networks (CAN) . . . . . . . . . . . . . . . . . . . . 21<br />
2.3.2. Der Struc<strong>tu</strong>rally Dynamic Cellular Automata“ (SDCA) . . . . . 21<br />
”<br />
2.3.2.1. Das Relative Location Mo<strong>de</strong>ll . . . . . . . . . . . . . . . 22<br />
2.3.2.2. Das Labeled Link Mo<strong>de</strong>ll . . . . . . . . . . . . . . . . . 23<br />
2.3.2.3. Das Symmetric Mo<strong>de</strong>ll . . . . . . . . . . . . . . . . . . . 23<br />
2.3.3. Der Dynamic Struc<strong>tu</strong>re Cellular Automata“ (DSCA) . . . . . . . 23<br />
”<br />
2.3.4. Der Global Cellular Automata“ (GCA) . . . . . . . . . . . . . . 24<br />
”<br />
3. Die Mo<strong>de</strong>llierung von Graphenalgorithmen auf <strong>de</strong>m GCA 27<br />
3.1. Erkennung von zusammenhängen<strong>de</strong>n Komponenten eines Graphen . . . . 28<br />
3.1.1. Der Warshall-Algorithmus . . . . . . . . . . . . . . . . . . . . . . 28<br />
3.1.1.1. Der Warshall auf einem Einprozessor-System . . . . . . 29<br />
3
3.1.1.2. Der Warshall-Algorithmus auf <strong>de</strong>m GCA . . . . . . . . . 30<br />
3.1.2. Der Floyd-Warshall-Algorithmus . . . . . . . . . . . . . . . . . . 34<br />
3.1.3. Der Algorithmus von Hirschberg et al. . . . . . . . . . . . . . . . 36<br />
3.1.3.1. Ablauf auf <strong>de</strong>r P-RAM . . . . . . . . . . . . . . . . . . . 36<br />
3.1.3.2. Komplexitätsbetrach<strong>tu</strong>ng auf <strong>de</strong>r P-RAM . . . . . . . . 42<br />
3.1.3.3. Mo<strong>de</strong>llierung auf <strong>de</strong>m GCA . . . . . . . . . . . . . . . . 46<br />
3.1.3.4. Komplexitätsbetrach<strong>tu</strong>ng und Verbesserungen auf <strong>de</strong>m<br />
GCA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49<br />
3.1.3.4.1. Speicherzellenmo<strong>de</strong>llierung . . . . . . . . . . . . 49<br />
3.1.3.4.2. Laufzeitkomplexitätsverbesserung . . . . . . . . 53<br />
3.2. Minimal aufspannen<strong>de</strong> Bäume . . . . . . . . . . . . . . . . . . . . . . . . 54<br />
3.2.1. Der Kruskal-Algorithmus . . . . . . . . . . . . . . . . . . . . . . . 54<br />
3.2.1.1. Der Heap-Sort . . . . . . . . . . . . . . . . . . . . . . . 55<br />
3.2.1.2. Der Heap-Sort auf <strong>de</strong>m GCA . . . . . . . . . . . . . . . 58<br />
3.2.1.3. Der Kruskal auf <strong>de</strong>m GCA . . . . . . . . . . . . . . . . . 65<br />
3.2.2. Modifikation <strong>de</strong>s Hirschberg für minimal aufspannen<strong>de</strong> Bäume . . 68<br />
3.2.2.1. Realisierung auf <strong>de</strong>m GCA . . . . . . . . . . . . . . . . 73<br />
3.2.3. Derzeitiger Forschungsstand . . . . . . . . . . . . . . . . . . . . . 74<br />
3.3. NP-vollständige Probleme . . . . . . . . . . . . . . . . . . . . . . . . . . 75<br />
3.3.1. Das Graphenfärbbarkeitsproblem . . . . . . . . . . . . . . . . . . 76<br />
3.3.1.1. Kantenfärbung von bipartiten Graphen . . . . . . . . . . 78<br />
3.4. Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88<br />
4. Die Mo<strong>de</strong>llierung von Krypto-<strong>Algorithmen</strong> auf <strong>de</strong>m GCA 91<br />
4.1. Der erweiterte euklidische Algorithmus . . . . . . . . . . . . . . . . . . . 91<br />
4.2. Die Anwendung <strong>de</strong>s Chinesische Restsatzes . . . . . . . . . . . . . . . . . 93<br />
4.3. Einordnung <strong>de</strong>r Anwendung <strong>de</strong>s Chinesischen Restsatzes . . . . . . . . . 97<br />
5. Implementierungseigenheiten 99<br />
6. Zusammenfassung und Ausblick 101<br />
A. Traversierungsstrategien 103<br />
A.1. Backtracking . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103<br />
A.2. Divi<strong>de</strong> and Conquer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106<br />
B. <strong>Parallele</strong> Algorithmische Techniken 109<br />
B.1. Die Balanced-Binary-Tree-Technik . . . . . . . . . . . . . . . . . . . . . . 109<br />
B.2. Die Doubling-Technik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110<br />
B.3. Die Divi<strong>de</strong>-and-Conquer-Technik . . . . . . . . . . . . . . . . . . . . . . 111<br />
C. Implementierungen <strong>de</strong>r <strong>Algorithmen</strong> 112<br />
C.1. Warshall-Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112<br />
C.2. Hirschberg-Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . 113<br />
C.3. Kruskal-Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115<br />
4
C.4. Modifizierter Hirschberg-Algorithmus . . . . . . . . . . . . . . . . . . . . 117<br />
C.5. Euler-Färbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119<br />
C.6. Chinesischer Restsatz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125<br />
5
Abbildungsverzeichnis<br />
2.1. Darstellung <strong>de</strong>r Nachbarschaftsbeziehung nach von Neumann . . . . . . . 19<br />
2.2. Darstellung <strong>de</strong>r Nachbarschaftsbeziehung nach Moore . . . . . . . . . . . 20<br />
2.3. [CNG + 01]: CAN network of the Sarno SCIDDICA mo<strong>de</strong>l . . . . . . . . . 22<br />
2.4. Entscheidungsbaum zur Bestimmung <strong>de</strong>r Verän<strong>de</strong>rung <strong>de</strong>r Daten . . . . 26<br />
3.1. Beispielgraph Warshall . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27<br />
3.2. Schematische Darstellung <strong>de</strong>s Warshall-Algorithmus auf <strong>de</strong>m GCA . . . . 31<br />
3.3. Zelleninitialisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31<br />
3.4. Ablauf <strong>de</strong>s Warshall-Algorithmus in <strong>de</strong>r Theorie . . . . . . . . . . . . . . 32<br />
3.5. Ablauf <strong>de</strong>s Warshall-Algorithmus auf <strong>de</strong>m GCA . . . . . . . . . . . . . . 33<br />
3.6. Beispielgraph Hirschberg . . . . . . . . . . . . . . . . . . . . . . . . . . . 38<br />
3.7. Hirschberg Ergebnis erster Schritt, erster Durchlauf . . . . . . . . . . . . 39<br />
3.8. Hirschberg Ergebnis vierter Schritt, erster Durchlauf . . . . . . . . . . . 39<br />
3.9. Hirschberg Ergebnis fünfter Schritt, erster Durchlauf . . . . . . . . . . . 40<br />
3.10. Hirschberg Ergebnis erster Schritt, zweiter Durchlauf . . . . . . . . . . . 40<br />
3.11. Hirschberg Ergebnis zweiter Schritt, zweiter Durchlauf . . . . . . . . . . 41<br />
3.12. Hirschberg Ergebnis vierter Schritt, zweiter Durchlauf . . . . . . . . . . . 41<br />
3.13. Hirschberg fünfter Schritt, zweiter Durchlauf . . . . . . . . . . . . . . . . 42<br />
3.14. Minimumbestimmung mit <strong>de</strong>r Doubling-Technik . . . . . . . . . . . . . . 44<br />
3.15. Balanced-Binary-Tree in <strong>de</strong>r Graphenrepräsentation . . . . . . . . . . . . 45<br />
3.16. Schematische Darstellung <strong>de</strong>s Hirschbergs auf <strong>de</strong>m GCA . . . . . . . . . 47<br />
3.17. Speicherzellenrealisierung 1 . . . . . . . . . . . . . . . . . . . . . . . . . . 49<br />
3.18. Speicherzellenrealisierung 1a . . . . . . . . . . . . . . . . . . . . . . . . . 50<br />
3.19. Speicherzellenrealisierung 1b . . . . . . . . . . . . . . . . . . . . . . . . . 51<br />
3.20. Speicherzellenrealisierung 2 . . . . . . . . . . . . . . . . . . . . . . . . . . 52<br />
3.21. Min-Heap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55<br />
3.22. In einen Heap eingelesener Vektor . . . . . . . . . . . . . . . . . . . . . . 56<br />
3.23. Min-Heap nach <strong>de</strong>m Entfernen <strong>de</strong>s ersten Elements . . . . . . . . . . . . 57<br />
3.24. Erster Schritt zur Wie<strong>de</strong>rherstellung <strong>de</strong>r Heap-Eigenschaft . . . . . . . . 57<br />
3.25. Der wie<strong>de</strong>r hergestellte Heap . . . . . . . . . . . . . . . . . . . . . . . . . 57<br />
3.26. Darstellung <strong>de</strong>s Heaps auf <strong>de</strong>m GCA . . . . . . . . . . . . . . . . . . . . 59<br />
3.27. Beispiel <strong>de</strong>s parallel ablaufen<strong>de</strong>n Heap-Sorts, Bild1 . . . . . . . . . . . . 59<br />
3.28. Beispiel <strong>de</strong>s parallel ablaufen<strong>de</strong>n Heap-Sorts, Bild2 . . . . . . . . . . . . 60<br />
3.29. Beispiel <strong>de</strong>s parallel ablaufen<strong>de</strong>n Heap-Sorts, Bild3 . . . . . . . . . . . . 60<br />
6
3.30. Beispiel <strong>de</strong>s parallel ablaufen<strong>de</strong>n Heap-Sorts, Bild4 . . . . . . . . . . . . 61<br />
3.31. Beispiel <strong>de</strong>s parallel ablaufen<strong>de</strong>n Heap-Sorts, Bild5 . . . . . . . . . . . . 61<br />
3.32. Beispiel <strong>de</strong>s parallel ablaufen<strong>de</strong>n Heap-Sorts, Bild6 . . . . . . . . . . . . 62<br />
3.33. Beispiel <strong>de</strong>s parallel ablaufen<strong>de</strong>n Heap-Sorts, Bild7 . . . . . . . . . . . . 62<br />
3.34. Repräsentation <strong>de</strong>r Ebenen <strong>de</strong>s Baums durch die Zellen <strong>de</strong>s GCA . . . . 63<br />
3.35. Realisierung <strong>de</strong>s Kruskal-Algorithmus auf <strong>de</strong>m GCA . . . . . . . . . . . . 66<br />
3.36. Realisierung <strong>de</strong>s Kruskal-Algorithmus ohne Bus . . . . . . . . . . . . . . 67<br />
3.37. Realisierung <strong>de</strong>s Kruskal-Algorithmus mit Bus-Zelle . . . . . . . . . . . . 67<br />
3.38. Beispielgraph für <strong>de</strong>n Hirschbergalgorithmus zur Berechnung minimaler<br />
Spannbäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71<br />
3.39. Teilergebnisgraph nach <strong>de</strong>m ersten Schritt <strong>de</strong>s Hirschberg . . . . . . . . . 71<br />
3.40. Teilergebnis nach <strong>de</strong>m zweiten Schritt <strong>de</strong>s Hirschberg . . . . . . . . . . . 72<br />
3.41. Teilergebnis nach <strong>de</strong>m ersten Schritt <strong>de</strong>s zweiten Durchlaufs . . . . . . . 72<br />
3.42. Teilergebnis nach <strong>de</strong>m zweiten Schritt <strong>de</strong>s zweiten Durchlaufs . . . . . . 73<br />
3.43. Bipartite und nicht bipartite Graphen . . . . . . . . . . . . . . . . . . . . 77<br />
3.44. Outerplanarer Graph . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77<br />
3.45. Ein Halin-Graph . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78<br />
3.46. Ein Bipartiter Graph mit Euler-Zerlegung . . . . . . . . . . . . . . . . . 79<br />
3.47. Bipartiter Beispielgraph für die Euler-Zerlegung . . . . . . . . . . . . . . 82<br />
3.48. Bipartiter Beispielgraph nach <strong>de</strong>r Ersetzung <strong>de</strong>r Kanten . . . . . . . . . . 82<br />
3.49. Bipartiter Beispielgraph nach <strong>de</strong>r Kantensortierung . . . . . . . . . . . . 83<br />
3.50. Successorbestimmung für die Kanten <strong>de</strong>s bipartiten Beispielgraphen . . . 84<br />
3.51. Die Kanten-Zellen nach <strong>de</strong>r Doubling-Technik . . . . . . . . . . . . . . . 85<br />
3.52. Auswahl eines Zyklusses . . . . . . . . . . . . . . . . . . . . . . . . . . . 85<br />
3.53. Festlegen <strong>de</strong>r Ablaufreihenfolge . . . . . . . . . . . . . . . . . . . . . . . 86<br />
4.1. Initialisierung <strong>de</strong>s GCA um <strong>de</strong>n Chinesischen Restsatz anzuwen<strong>de</strong>n . . . 96<br />
4.2. Die Variable m wur<strong>de</strong> mit <strong>de</strong>r Doubling-Technik ausgerechnet . . . . . . 96<br />
4.3. Alle Zellen haben <strong>de</strong>n korrekten Wert in <strong>de</strong>r Variable m . . . . . . . . . . 97<br />
A.1. Die erste Dame ist auf <strong>de</strong>m Feld positioniert . . . . . . . . . . . . . . . . 103<br />
A.2. Die zweite Dame ist auf <strong>de</strong>m Feld positioniert . . . . . . . . . . . . . . . 104<br />
A.3. Die dritte Dame ist auf <strong>de</strong>m Feld positioniert . . . . . . . . . . . . . . . 104<br />
A.4. Die vierte Dame ist auf <strong>de</strong>m Feld positioniert . . . . . . . . . . . . . . . 105<br />
A.5. Die fünfte Dame ist auf <strong>de</strong>m Feld positioniert . . . . . . . . . . . . . . . 105<br />
A.6. Die fünfte Dame ist auf <strong>de</strong>m Feld repositioniert . . . . . . . . . . . . . . 105<br />
A.7. Die vierte Dame ist auf <strong>de</strong>m Feld repositioniert . . . . . . . . . . . . . . 106<br />
A.8. Startsi<strong>tu</strong>ation <strong>de</strong>s Quicksorts . . . . . . . . . . . . . . . . . . . . . . . . . 107<br />
A.9. Quicksort nach <strong>de</strong>r Vorsortierung . . . . . . . . . . . . . . . . . . . . . . 107<br />
A.10.Rekursiver Aufruf <strong>de</strong>s Quicksorts . . . . . . . . . . . . . . . . . . . . . . 108<br />
B.1. Mittelwertbestimmung mit <strong>de</strong>r Balanced-Binary-Tree-Technik . . . . . . 110<br />
B.2. Abstandsbestimmung mit <strong>de</strong>r Doubling-Technik . . . . . . . . . . . . . . 111<br />
7
Listings<br />
3.1. Der Warshall-Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . 29<br />
3.2. Der leicht modifizierte Warshall-Algorithmus . . . . . . . . . . . . . . . . 29<br />
3.3. Der Hirschberg-Algorithmus auf <strong>de</strong>r P-RAM . . . . . . . . . . . . . . . . 37<br />
3.4. Weitere Unterteilung <strong>de</strong>s ersten Schritts <strong>de</strong>s Hirschberg-Algorithmus . . . 43<br />
3.5. Minimumbestimmung mit <strong>de</strong>r Doubling-Technik . . . . . . . . . . . . . . 43<br />
3.6. Eulerfärbung für bipartite Graphen . . . . . . . . . . . . . . . . . . . . . 80<br />
4.1. Der erweiterte euklidische Algorithmus in C . . . . . . . . . . . . . . . . 93<br />
5.1. Effizienztest für Threads in C . . . . . . . . . . . . . . . . . . . . . . . . 99<br />
B.1. Berechnung <strong>de</strong>r Summe mit <strong>de</strong>r Balanced-Binary-Tree-Technik . . . . . . 110<br />
C.1. Implementierung <strong>de</strong>s Warshall-Algorithmus . . . . . . . . . . . . . . . . . 112<br />
C.2. Implementierung <strong>de</strong>s Hirschberg-Algorithmus . . . . . . . . . . . . . . . . 113<br />
C.3. Implementierung <strong>de</strong>s Kruskal-Algorithmus . . . . . . . . . . . . . . . . . 115<br />
C.4. Implementierung <strong>de</strong>s modifizierten Hirschberg-Algorithmus . . . . . . . . 117<br />
C.5. Implementierung <strong>de</strong>r Eulerfärbung . . . . . . . . . . . . . . . . . . . . . 119<br />
C.6. Implementierung <strong>de</strong>s Chinesischen Restsatzes . . . . . . . . . . . . . . . 125<br />
8
Tabellenverzeichnis<br />
3.1. Laufzeit <strong>de</strong>r betrachteten <strong>Algorithmen</strong> . . . . . . . . . . . . . . . . . . . 89<br />
3.2. Abhängigkeiten <strong>de</strong>r Daten für die Graphen-<strong>Algorithmen</strong> . . . . . . . . . 90<br />
3.3. Abhängigkeiten <strong>de</strong>r Verbindungen für die Graphen-<strong>Algorithmen</strong> . . . . . 90<br />
4.1. Berechnungstabelle <strong>de</strong>s euklidischen Algorithmus . . . . . . . . . . . . . 92<br />
4.2. Berechnungstabelle <strong>de</strong>s erweiterten euklidischen Algorithmus . . . . . . . 92<br />
4.3. Berechnung <strong>de</strong>s Inversen von M 1 modulo m 1 . . . . . . . . . . . . . . . . 95<br />
4.4. Berechnung <strong>de</strong>s Inversen von M 2 modulo m 2 . . . . . . . . . . . . . . . . 95<br />
4.5. Berechnung <strong>de</strong>s Inversen von M 3 modulo m 3 . . . . . . . . . . . . . . . . 95<br />
4.6. Abhängigkeiten <strong>de</strong>r Daten und Verbindungen für <strong>de</strong>n Chinesischen Restsatz. 98<br />
5.1. Ausführungszeiten ohne Threads und mit Threads . . . . . . . . . . . . . 100<br />
9
1. Einlei<strong>tu</strong>ng<br />
Dieses Kapitel wird zuerst eine Motivation liefern, parallele Systeme näher zu betrachten<br />
und anschließend einen Überblick über diese Arbeit geben. Zu<strong>de</strong>m wird in diesem Kapitel<br />
<strong>de</strong>r Begriff <strong>de</strong>r Aufwandsbetrach<strong>tu</strong>ng, wie er in dieser Arbeit verwen<strong>de</strong>t wird, <strong>de</strong>finiert.<br />
1.1. Motivation<br />
Schon seit Jahren liest man oft, dass die Möglichkeiten zur Leis<strong>tu</strong>ngssteigerung <strong>de</strong>r Prozessoren<br />
durch Vermehrung <strong>de</strong>r Transistoren bald ausgereizt seien. Es wird befürchtet,<br />
dass die Struk<strong>tu</strong>ren dann nicht mehr in <strong>de</strong>m Maße weiter verkleinert wer<strong>de</strong>n können,<br />
wie es nötig wäre, um eine Leis<strong>tu</strong>ngssteigerung durch kleinere und dadurch mehr Transistoren<br />
zu erzielen. Eine Möglichkeit, die Leis<strong>tu</strong>ngsfähigkeit weiter zu steigern, bieten<br />
die Quantenrechner. Diese sind zur Zeit aber noch nicht ausreichend erforscht und produzierbar<br />
1 , um Marktreife zu erlangen.<br />
Davon abgesehen stellen Quantenrechner die Informatik vor bislang ungeahnte Probleme,<br />
müssen dann doch neue Kryptoverfahren entwickelt 2 und auf eine an<strong>de</strong>re als bislang<br />
gewohnte Weise implementiert wer<strong>de</strong>n. Es ist davon auszugehen, dass die Quantenrechner<br />
in <strong>de</strong>n nächsten Jahren noch nicht reif für die Massenproduktion sind, weshalb man<br />
sich nach an<strong>de</strong>ren Alternativen zur Leis<strong>tu</strong>ngssteigerung umgesehen hat.<br />
Der geschickte Aufbau von parallelen Architek<strong>tu</strong>ren und eine gute Programmierung für<br />
sie bieten die Möglichkeit, Laufzeitverbesserungen zu erlangen, ohne die Anzahl <strong>de</strong>r<br />
Transistoren pro Chip bzw. Prozessor zu erhöhen. Aus diesem Grund sind parallele<br />
Systeme und <strong>de</strong>r Aufbau von neuen parallelen Architek<strong>tu</strong>ren in letzter Zeit wie<strong>de</strong>r vermehrt<br />
im Interesse <strong>de</strong>r Wirtschaft. So stellten Sony, Toshiba und IBM letztens <strong>de</strong>n<br />
Cell-Prozessor [Web05] vor, einen Chip, <strong>de</strong>r über insgesamt neun Prozessoren verfügt<br />
und unter an<strong>de</strong>rem in die PS3 (Playstation 3) eingebaut wer<strong>de</strong>n soll.<br />
Mit dieser Entwicklung wer<strong>de</strong>n Parallelrechner für die breite Bevölkerung und damit<br />
auch für Nicht-Aka<strong>de</strong>miker interessant. Es wird dadurch nötig, sich Gedanken zu machen,<br />
wie man die bisherigen sequentiellen <strong>Algorithmen</strong> geschickt auf ein paralleles System<br />
übertragen kann.<br />
1 Es ist bereits gelungen, im Labor einen Quantenrechner zu bauen, <strong>de</strong>r die Zahl 15 faktorisiert. Größere<br />
Quantenrechner sind bislang noch nicht gelungen.<br />
2 Elliptische Kurven gelten bislang als sicher gegenüber Angriffe mit Quantenrechnern. Zu<strong>de</strong>m wur<strong>de</strong>n<br />
auch schon Kryptoverfahren auf Quantenrechnern entwickelt, die sicher sind. Allerdings ist auch hier<br />
die Technik noch nicht ausgereift genug, als dass Marktreife erlangt wür<strong>de</strong>.<br />
10
Natürlich kann man die <strong>Algorithmen</strong> unverän<strong>de</strong>rt ausführen und nur ein Prozessor <strong>de</strong>s<br />
Systems berechnet das Problem. Allerdings verliert man so viel Performance und es<br />
scheint angeraten, sich zu überlegen, wie man die neuen Möglichkeiten besser nutzen<br />
kann.<br />
Diese Arbeit gibt einen Überblick über einige <strong>Algorithmen</strong>, welche teilweise an die neuen<br />
Umstän<strong>de</strong> angepasst sind, teilweise schon für sie entwickelt wur<strong>de</strong>n und effizient auf parallelen<br />
Systemen arbeiten. Um die Effizienz zu bestimmen, wird eine Aufwandsbetrach<strong>tu</strong>ng<br />
<strong>de</strong>r einzelnen <strong>Algorithmen</strong> betrieben, welche auch auf die Parallelität angepasst<br />
wur<strong>de</strong>. Im Folgen<strong>de</strong>n wird eine Erklärung zur Aufwandsbetrach<strong>tu</strong>ng abgegeben, danach<br />
wird dargestellt, wie sich die Arbeit aufbaut.<br />
1.2. Aufwandsbetrach<strong>tu</strong>ng<br />
Bei Mehrprozessorsystemen ist immer zu beachten, dass nicht nur die Laufzeitkomplexität<br />
für die Komplexitätsbetrach<strong>tu</strong>ng wichtig ist. Statt<strong>de</strong>ssen ist immer auch die Anzahl<br />
<strong>de</strong>r Prozessoren sowie die benötigten Verbindungen zwischen <strong>de</strong>n Prozessoren wichtig.<br />
Ohne diese Angaben ist kein korrekter Vergleich zwischen zwei verschie<strong>de</strong>nen Realisierungen<br />
möglich. Wenn hier also eine Abschätzung <strong>de</strong>r Komplexität gegeben wird, dann<br />
ist es auch zwingend notwendig, die Anzahl <strong>de</strong>r Prozessoren und sofern möglich die<br />
Anzahl <strong>de</strong>r Verbindungen anzugeben.<br />
Die O-Notation wird hier nicht nur für die Zeitabschätzung benutzt, son<strong>de</strong>rn auch für<br />
die Anzahl <strong>de</strong>r Prozessoren verwen<strong>de</strong>t, d. h. O(1) Prozessoren be<strong>de</strong>utet eine konstante<br />
Anzahl an Prozessoren. Die gegebenen Abschätzungen sind für die maximal sinnvolle<br />
Anzahl an Prozessoren gegeben. Manchmal ließe sich das Ergebnis auch mit weniger<br />
Prozessoren erzielen, allerdings dann zu ungunsten <strong>de</strong>r Laufzeit. Damit die Abschätzung<br />
also vergleichbar bleibt, wur<strong>de</strong> hier auf Modifikationen, welche die Anzahl <strong>de</strong>r Prozessoren<br />
auf Kosten <strong>de</strong>r Laufzeit minimieren, verzichtet. Ist es jedoch möglich, die Anzahl<br />
<strong>de</strong>r Prozessoren zu verringern, ohne dabei Laufzeiteinbußen hinzunehmen, so sind diese<br />
Modifikationen o<strong>de</strong>r eine Referenz auf die entsprechen<strong>de</strong> Litera<strong>tu</strong>r angegeben.<br />
1.3. Struk<strong>tu</strong>r <strong>de</strong>r Arbeit<br />
In <strong>de</strong>r Wissenschaft wer<strong>de</strong>n parallele Systeme schon seit langem erforscht, was zu einer<br />
Vielzahl an Mo<strong>de</strong>llen geführt hat. Meist sind diese Mo<strong>de</strong>lle auf eine bestimmte Aufgabe<br />
hin mo<strong>de</strong>lliert o<strong>de</strong>r haben sich aus einem Single-Prozessor-Mo<strong>de</strong>ll heraus entwickelt. In<br />
Kapitel 2 soll eine Übersicht über einige parallele Mo<strong>de</strong>lle gegeben wer<strong>de</strong>n. Da viele<br />
Mo<strong>de</strong>lle existieren, erhebt diese Arbeit keinen Anspruch auf Vollständigkeit o<strong>de</strong>r Gewich<strong>tu</strong>ng<br />
bezüglich <strong>de</strong>r Auswahl.<br />
Im dritten Kapitel wer<strong>de</strong>n dann Graphenalgorithmen auf <strong>de</strong>m parallelen Mo<strong>de</strong>ll <strong>de</strong>s<br />
Global Cellular Automata“ (GCA) angegeben und eine Aufwandsbetrach<strong>tu</strong>ng <strong>de</strong>r einzelnen<br />
<strong>Algorithmen</strong><br />
”<br />
betrieben.<br />
11
Im vierten Kapitel wird ein kryptographischer Algorithmus auf <strong>de</strong>m GCA mo<strong>de</strong>lliert<br />
und auch hierfür eine Komplexitätsbetrach<strong>tu</strong>ng angestellt.<br />
Im fünften Kapitel wird auf die Eigenheiten <strong>de</strong>r Implementierung, wie z. B. das unterschiedliche<br />
Laufzeitverhalten eines Algorithmus bei Modifikationen <strong>de</strong>r Implementierung,<br />
eingegangen.<br />
Im sechsten Kapitel wird dann ein Fazit gezogen und aufgezeigt, wo noch Forschungsbedarf<br />
besteht.<br />
12
2. Zellularautomaten und an<strong>de</strong>re<br />
Mo<strong>de</strong>lle<br />
Innerhalb <strong>de</strong>r Automatentheorie war es schon immer so, dass mehrere gleichmächtige<br />
Mo<strong>de</strong>lle existierten. Diese wur<strong>de</strong>n teilweise zeitgleich entwickelt, und unterschie<strong>de</strong>n sich<br />
oft nur in Kleinigkeiten o<strong>de</strong>r einer verschie<strong>de</strong>nen Herangehensweise an die Problematiken.<br />
Wird ein Mo<strong>de</strong>ll entwickelt, so richtet sich <strong>de</strong>r Aufbau <strong>de</strong>s Mo<strong>de</strong>lls immer danach, was<br />
man mit <strong>de</strong>m Mo<strong>de</strong>ll erreichen möchte. Möchte man mit einem Mo<strong>de</strong>ll z. B. natürliche<br />
Vorgänge in ihrer Feinheit möglichst realitätsnah nachempfin<strong>de</strong>n, wird man es an<strong>de</strong>rs<br />
entwickeln als ein Mo<strong>de</strong>ll, das dazu dient, S<strong>tu</strong><strong>de</strong>nten die Möglichkeiten von Automaten<br />
zu ver<strong>de</strong>utlichen.<br />
Bei einem Mo<strong>de</strong>ll, das eine bestimmte Si<strong>tu</strong>ation simulieren soll, wird das Mo<strong>de</strong>ll even<strong>tu</strong>ell<br />
komplexer und unverständlicher, wenn sich dadurch die Problematik in <strong>de</strong>m Mo<strong>de</strong>ll<br />
leichter formulieren lässt. Ein Mo<strong>de</strong>ll hingegen, dass für die Lehre entwickelt wur<strong>de</strong>, wird<br />
so einfach wie möglich gehalten wer<strong>de</strong>n, damit es auch für einen Laien leicht verständlich<br />
ist. Lösungen sollen auf diesem Mo<strong>de</strong>ll dann zwar immer noch möglich sein, aber<br />
komplexere Probleme sind <strong>de</strong>utlich schwieriger auf einem solchem Mo<strong>de</strong>ll zu simulieren<br />
als auf einem speziell entwickelten Automatenmo<strong>de</strong>ll. Ein Problem bei einem einfachen<br />
Mo<strong>de</strong>ll ist stets auch, dass die Gefahr besteht, dass von <strong>de</strong>r Realität so weit abstrahiert<br />
wird, dass das resultieren<strong>de</strong> Mo<strong>de</strong>ll nicht mehr <strong>de</strong>r Realität entspricht.<br />
So wie sich verschie<strong>de</strong>ne Mo<strong>de</strong>lle in <strong>de</strong>r klassischen Automatentheorie gebil<strong>de</strong>t haben,<br />
wur<strong>de</strong>n auch mehrere Mo<strong>de</strong>lle für parallele Systeme gebil<strong>de</strong>t, teilweise davon beeinflusst,<br />
welches klassische Mo<strong>de</strong>ll zu Grun<strong>de</strong> gelegt wur<strong>de</strong>, teilweise aber auch auf an<strong>de</strong>ren I<strong>de</strong>en<br />
aufbauend. In <strong>de</strong>n anschließen<strong>de</strong>n Abschnitten sollen einige Automatenmo<strong>de</strong>lle vorgestellt<br />
wer<strong>de</strong>n. Falls vorhan<strong>de</strong>n, wird dabei zunächst auf die Single-Prozessor-Variante<br />
eingegangen, bevor die parallelen Möglichkeiten erläutert wer<strong>de</strong>n.<br />
2.1. Die <strong>Ra</strong>ndom-Access-Maschinen<br />
Ein Mo<strong>de</strong>ll, das neben <strong>de</strong>r Turing-Maschine 1 verwen<strong>de</strong>t wird, um S<strong>tu</strong><strong>de</strong>nten die Berechenbarkeit<br />
verständlicher zu machen, ist die <strong>Ra</strong>ndom-Access-Maschine (RAM). Dieses<br />
Mo<strong>de</strong>ll ist in seiner Programmiersprache ähnlich zu Assembler und eignet sich aufgrund<br />
1 Turing-Maschinen wer<strong>de</strong>n u. a. in [Sch01] eingeführt und erklärt.<br />
13
seiner Einfachheit u. a. zur Betrach<strong>tu</strong>ng <strong>de</strong>r Terminierung von Programmen. Das Prinzip<br />
<strong>de</strong>r RAM soll nun erläutert wer<strong>de</strong>n, bevor das Mo<strong>de</strong>ll dann zur parallelen RAM<br />
(P-RAM) erweitert wird.<br />
2.1.1. Prinzip <strong>de</strong>r RAM<br />
Die RAM verfügt über eine CPU mit abzählbar unendlich vielen Registern, in <strong>de</strong>nen<br />
beliebig große natürliche Zahlen gespeichert wer<strong>de</strong>n können. Auf <strong>de</strong>n Werten kann die<br />
CPU arithmetische Operationen ausführen, zusätzlich kann die CPU je<strong>de</strong>rzeit auf <strong>de</strong>n<br />
Speicher zugreifen und von dort Werte in die Register lesen o<strong>de</strong>r Werte in <strong>de</strong>n Speicher<br />
schreiben.<br />
Der Arbeitsablauf <strong>de</strong>r CPU wird von einem Programm festgelegt, welches sich im Speicher<br />
befin<strong>de</strong>t. Even<strong>tu</strong>ell benötigte Eingaben für das Programm stehen ebenfalls an <strong>de</strong>finierten<br />
Stellen im Speicher. Das Programm besteht aus arithmetischen Instruktionen<br />
sowie Speicherzugriffsbefehlen. Um <strong>de</strong>n normalen Programmfluss zu modifizieren, sind<br />
außer<strong>de</strong>m bedingte Sprünge erlaubt. Die Instruktionen <strong>de</strong>s Programms wer<strong>de</strong>n gemäß<br />
eines Takts abgearbeitet und die CPU been<strong>de</strong>t ihre Arbeit bei Programmen<strong>de</strong>.<br />
Bei <strong>de</strong>r Aufwandsbetrach<strong>tu</strong>ng eines Programms auf <strong>de</strong>r RAM müssen drei Aspekte unterschie<strong>de</strong>n<br />
wer<strong>de</strong>n:<br />
• Die Programmgröße bestimmt sich aus <strong>de</strong>r Anzahl <strong>de</strong>r Instruktionen, welche das<br />
Programm bil<strong>de</strong>n.<br />
• Die Speichergröße bestimmt sich aus <strong>de</strong>r Anzahl <strong>de</strong>r vom Programm genutzten<br />
Speicherzellen.<br />
• Der Zeitaufwand, bestimmt sich aus <strong>de</strong>r Anzahl <strong>de</strong>r Takte, welche benötigt wer<strong>de</strong>n,<br />
bis das Programm abgearbeitet ist.<br />
2.1.2. Die P-RAM<br />
Das erste Kapitel von [KKT01] liefert eine Möglichkeit zur Parallelisierung <strong>de</strong>s RAM-<br />
Mo<strong>de</strong>lls. Da alle Berechnungen von <strong>de</strong>r CPU ausgeführt wer<strong>de</strong>n, kann die RAM dadurch<br />
parallelisiert wer<strong>de</strong>n, dass es nicht mehr eine einzige CPU gibt, son<strong>de</strong>rn mehrere Prozessoren.<br />
Diese Prozessoren arbeiten synchron, besitzen also einen gemeinsamen Takt und<br />
haben Zugriff auf einen gemeinsamen Speicher.<br />
Der Datenaustausch zwischen <strong>de</strong>n Prozessoren geschieht über <strong>de</strong>n gemeinsamen Speicher.<br />
Ein Prozessor schreibt seine Werte in eine Speicherzelle und an<strong>de</strong>re Prozessoren<br />
können sie dort bei Bedarf lesen. Ein Nachteil an dieser Mo<strong>de</strong>llierung ist, dass sichergestellt<br />
wer<strong>de</strong>n muss, dass keine zwei Prozessoren zur gleichen Zeit auf eine Speicherzelle<br />
schreiben wollen.<br />
14
Aufgrund solcher Schreibkonflikte kann es zu Dateninkonsistenzen kommen. Diese Dateninkosistenzen<br />
können dadurch vermie<strong>de</strong>n wer<strong>de</strong>n, dass eine Speicherzelle, auf die schreibend<br />
zugegriffen wird, gesperrt und erst nach <strong>de</strong>m erfolgreichen Schreibvorgang wie<strong>de</strong>r<br />
freigegeben wird. Um die synchrone Arbeitsweise <strong>de</strong>r Prozessoren zu gewährleisten wird<br />
angenommen, dass alle Speicheroperationen die gleiche Zeit benötigen.<br />
Neben <strong>de</strong>m gemeinsamen Speicher gibt es noch einen privaten Speicher, auf <strong>de</strong>n nur<br />
jeweils ein Prozessor Zugriff hat. Zu<strong>de</strong>m verfügt je<strong>de</strong>r Prozessor über eine ein<strong>de</strong>utige<br />
Kennung (ID) damit die Prozessoren unterschie<strong>de</strong>n wer<strong>de</strong>n können, da verschie<strong>de</strong>ne<br />
Prozessoren verschie<strong>de</strong>ne Instruktionen ausführen können.<br />
Der gemeinsame Speicher kann in vier verschie<strong>de</strong>nen Modi betrieben wer<strong>de</strong>n: EREW,<br />
CREW, CRCW und CROW.<br />
EREW be<strong>de</strong>utet ”<br />
exclusive read, exclusive write“ und bezeichnet <strong>de</strong>n Modus, in <strong>de</strong>m<br />
pro Zeittakt jeweils nur maximal ein Prozessor lesend o<strong>de</strong>r schreibend auf die gleiche<br />
Speicherzelle zugreifen darf.<br />
CREW be<strong>de</strong>utet ”<br />
concurrent read, exclusive write“ und erlaubt <strong>de</strong>n lesen<strong>de</strong>n Zugriff<br />
mehrerer Prozessoren auf eine Speicherzelle, aber auf eine Speicherzelle schreibend zugreifen<br />
darf nur ein Prozessor pro Takt.<br />
CRCW be<strong>de</strong>utet ”<br />
concurrent read, concurrent write“ und erlaubt sowohl <strong>de</strong>n parallel lesen<strong>de</strong>n<br />
als auch <strong>de</strong>n parallel schreiben<strong>de</strong>n Zugriff auf eine Speicherzelle. In diesem Modus<br />
muss im Programm darauf geachtet wer<strong>de</strong>n, dass keine Dateninkonsistenzen entstehen.<br />
CROW be<strong>de</strong>utet ”<br />
concurrent read, owners write“. In diesem Modus wird je<strong>de</strong>r Speicherzelle<br />
genau ein Prozessor zugeordnet, welcher auf diese Speicherstelle schreibend<br />
zugreifen darf. Alle an<strong>de</strong>ren Prozessoren dürfen nur lesend zugreifen. Bei diesem Mo<strong>de</strong>ll<br />
entstehen keine Dateninkonsistenzen. Nachzulesen ist dieser Modus u. a. in [DR86].<br />
2.2. Die Pointer-Maschinen<br />
Unter <strong>de</strong>m Begriff Pointer-Maschine fin<strong>de</strong>n sich viele verschie<strong>de</strong>ne Mo<strong>de</strong>lle in <strong>de</strong>r Litera<strong>tu</strong>r.<br />
Obwohl all diese Mo<strong>de</strong>lle Gemeinsamkeiten haben, differieren sie auch an diversen<br />
Stellen. In [BA95] wur<strong>de</strong> <strong>de</strong>r Versuch unternommen, sowohl die Gemeinsamkeiten als<br />
auch die Unterschie<strong>de</strong> <strong>de</strong>r verschie<strong>de</strong>nen Pointer-Maschinen herauszuarbeiten.<br />
Ziel dieses Abschnittes ist es, die verschie<strong>de</strong>nen Mo<strong>de</strong>lle vorzustellen. Anschließend wird<br />
dann erläutert, wie sich eine Parallel-Pointer-Maschine aufbaut. Bei <strong>de</strong>r Vorstellung <strong>de</strong>r<br />
Pointer-Maschinen wird dabei Bezug auf die Unterteilung von [BA95] genommen und<br />
zwischen Maschinenmo<strong>de</strong>llen und Programmiermo<strong>de</strong>llen unterschie<strong>de</strong>n.<br />
Allen Pointer-Maschinen gemeinsam ist die Mo<strong>de</strong>llierung <strong>de</strong>s Speichers als durch Pointer<br />
verbun<strong>de</strong>ne Knoten eines Graphen. Aufgrund dieser Gemeinsamkeit kann diese Art<br />
<strong>de</strong>r Mo<strong>de</strong>llierung <strong>de</strong>s Speichers als Charakteristikum <strong>de</strong>r Pointer-Maschinen verwen<strong>de</strong>t<br />
wer<strong>de</strong>n.<br />
15
2.2.1. Maschinenmo<strong>de</strong>lle <strong>de</strong>r Pointer-Maschinen<br />
Innerhalb <strong>de</strong>r Maschinenmo<strong>de</strong>lle muss eine weitergehen<strong>de</strong> Unterscheidung getroffen wer<strong>de</strong>n.<br />
[BA95] unterschei<strong>de</strong>t beispielsweise zwischen atomistischen und High-level Mo<strong>de</strong>llen,<br />
wobei die High-level Mo<strong>de</strong>lle nicht auf Symbolen, son<strong>de</strong>rn auf Datentypen arbeiten.<br />
Unter Datentypen wird hierbei ein Konstrukt wie die Integer unter C verstan<strong>de</strong>n. Es<br />
gibt also für einen Datentypen einen <strong>de</strong>finierten Wertebereich und darauf <strong>de</strong>finierte<br />
Funktionen. High-level Mo<strong>de</strong>lle befin<strong>de</strong>n sich <strong>de</strong>mnach näher an <strong>de</strong>m Prinzip <strong>de</strong>r Programmiersprachen.<br />
Allerdings ist ein Mo<strong>de</strong>ll, das auf bestimmte Datentypen festgelegt<br />
ist, nicht mehr so allgemein vergleichbar, wie Mo<strong>de</strong>lle, welche auf Symbolen arbeiten.<br />
Hier sollen repräsentativ drei verschie<strong>de</strong>ne atomistische Maschinen vorgestellt wer<strong>de</strong>n,<br />
um zu ver<strong>de</strong>utlichen, welche Vielfalt sich hinter <strong>de</strong>m Begriff Pointer-Maschine verbringt.<br />
2.2.1.1. Kolmogorov-Uspenskii Maschinen<br />
In <strong>de</strong>r Kolmogorov-Uspenskii Maschine wird <strong>de</strong>r Speicher als ungerichteter endlicher<br />
Graph dargestellt. Zusätzlich existiert eine endliche Menge an Labeln und je<strong>de</strong> Kante hat<br />
eines dieser Label. Zwei zu <strong>de</strong>m gleichen Knoten inzi<strong>de</strong>nte Kanten müssen verschie<strong>de</strong>ne<br />
Labels haben.<br />
Innerhalb <strong>de</strong>s Graphen gibt es einen ausgezeichneten Knoten, <strong>de</strong>n aktiven Knoten, <strong>de</strong>ssen<br />
Nachbarschaft die aktive Zone beschreibt. Dabei ist mit <strong>de</strong>r Nachbarschaft aber nicht nur<br />
die direkte Nachbarschaft gemeint, son<strong>de</strong>rn alle Knoten, welche innerhalb eines festen,<br />
aber beliebigen <strong>Ra</strong>dius liegen.<br />
Innerhalb <strong>de</strong>s Mo<strong>de</strong>lls sind unbedingte Sprünge, Eingaben, Ausgaben, bedingte Sprünge<br />
und Speichermodifiktationen erlaubt. Die Speichermodifikationen ermöglichen das Erzeugen<br />
von neuen Knoten sowie das Umbiegen von Kanten.<br />
2.2.1.2. Storage Modification Maschines<br />
Die Storage Modification Maschinen, kurz SMM, <strong>de</strong>finieren <strong>de</strong>n Speicher als gerichteten<br />
endlichen Graph. Auch hier gibt es eine endliche Menge an Labels, woraus je<strong>de</strong> Kante<br />
ein Label als Kantenmarkierung erhält. Allerdings müssen bei diesem Mo<strong>de</strong>ll nur zu<br />
<strong>de</strong>m selben Knoten eingehen<strong>de</strong> Kanten verschie<strong>de</strong>ne Labels haben, aus einem Knoten<br />
ausgehen<strong>de</strong> Kanten können die gleiche Markierung haben.<br />
Eine weitere Verän<strong>de</strong>rung gegenüber <strong>de</strong>m Kolmogorov-Uspenskii Maschinen ist <strong>de</strong>r nicht<br />
<strong>de</strong>finierte <strong>Ra</strong>dius <strong>de</strong>r aktiven Zone 2 . Ansonsten erlaubt die SMM die gleichen Operationen<br />
wie die Kolmogorov-Uspenskii Maschinen. In <strong>de</strong>r Litera<strong>tu</strong>r hat sich die SMM<br />
größtenteils als Repräsentant <strong>de</strong>r atomistischen Pointer-Maschinen durchgesetzt.<br />
2 In <strong>de</strong>r Definition von Schönhage war keine For<strong>de</strong>rung zur Definition <strong>de</strong>s <strong>Ra</strong>dius enthalten.<br />
16
2.2.1.3. Knuths Linking Automat<br />
Dieser Automat ist <strong>de</strong>r SMM sehr nahe verwandt. Der einzige Unterschied besteht darin,<br />
dass in Knuths Mo<strong>de</strong>ll für je<strong>de</strong>n Knoten zusätzlich eine feste Anzahl an Wertefel<strong>de</strong>rn<br />
vorgesehen sind, in <strong>de</strong>nen Symbole eines gegebenen Alphabets stehen.<br />
2.2.1.4. Die LISP Maschinen<br />
Die Atomistic Full LISP Maschine (AFLM) <strong>de</strong>finiert, vergleichbar zur SMM, <strong>de</strong>n Speicher<br />
als gerichteten endlichen Graphen und auch sonst ist das Mo<strong>de</strong>ll <strong>de</strong>m <strong>de</strong>r SMM<br />
sehr nahe. Allerdings beinhaltet in <strong>de</strong>m AFLM Mo<strong>de</strong>ll je<strong>de</strong>r Knoten noch exakt zwei<br />
Wertefel<strong>de</strong>r, in <strong>de</strong>nen entwe<strong>de</strong>r Symbole <strong>de</strong>s Alphabets o<strong>de</strong>r Pointer stehen können.<br />
Die Atomistic Pure LISP Maschine (APLM) <strong>de</strong>finiert sich genauso wie die AFLM, mit<br />
<strong>de</strong>r Einschränkung, dass die Wertefel<strong>de</strong>r nach <strong>de</strong>r Erzeugung <strong>de</strong>s Knotens nicht mehr<br />
verän<strong>de</strong>rt wer<strong>de</strong>n dürfen. Da auch die Pointer in <strong>de</strong>n Wertefel<strong>de</strong>rn stehen, kann so kein<br />
Zyklus entstehen, weil ein Pointer bei <strong>de</strong>r Erzeugung eines neuen Knotens nur auf einen<br />
bereits vorhan<strong>de</strong>nen Knoten gesetzt wer<strong>de</strong>n kann und somit auch noch kein Pointer auf<br />
<strong>de</strong>n neuen Knoten zeigt. Mit dieser Einschränkung erreicht man, dass keine Nebeneffekte<br />
von Funktionen auftreten und dass die Semantik einfacher wird.<br />
2.2.2. Programmiermo<strong>de</strong>lle <strong>de</strong>r Pointer-Maschinen<br />
In [BA95] ist das Programmiermo<strong>de</strong>ll <strong>de</strong>r Pointer-Maschine so <strong>de</strong>finiert, dass man für eine<br />
gegebene abstrakte Datenstruk<strong>tu</strong>r eine geeignete Repräsentation als gerichteten Graphen<br />
fin<strong>de</strong>t. Auf diesem gerichteten Graphen sind die Operationen ”<br />
add no<strong>de</strong>“, ”<br />
add<br />
pointer“, ”<br />
get pointer“ und ”<br />
<strong>de</strong>lete pointer“ erlaubt. Alle gewünschten Funktionen auf<br />
<strong>de</strong>r abstrakten Datenstruk<strong>tu</strong>r müssen mit Hilfe dieser Operationen implementiert wer<strong>de</strong>n.<br />
Die Zeitkomplexität wird dann durch die Gesamtzahl <strong>de</strong>r Operationsaufrufe bestimmt,<br />
während <strong>de</strong>r Platzbedarf sich anhand <strong>de</strong>r Anzahl <strong>de</strong>r ”<br />
add no<strong>de</strong>“ bestimmt.<br />
Bei <strong>de</strong>r Implementierung <strong>de</strong>s Union-Find-Problems 3 hat sich noch eine Klasse von speziellen<br />
Pointer-<strong>Algorithmen</strong> herauskristallisiert, die ”<br />
Separable Pointer“-<strong>Algorithmen</strong>.<br />
Diese <strong>Algorithmen</strong> garantieren, dass nach je<strong>de</strong>r Operation <strong>de</strong>r Graph in disjunkte Untergraphen<br />
zerlegt wer<strong>de</strong>n kann, wobei keine Kante von einem Untergraphen zu einem<br />
an<strong>de</strong>ren führt.<br />
3 Das Union-Find-Problem startet mit Mengen, die im Verlauf eines Algorithmus vereinigt wer<strong>de</strong>n<br />
können (Union). Zu<strong>de</strong>m können Elemente in diesen Mengen gesucht wer<strong>de</strong>n (Find). Da dies die<br />
einzigen Operationen sind, die bei <strong>de</strong>r Klasse an Problemen zugelassen sind, haben sie <strong>de</strong>r Klasse<br />
ihren Namen gegeben.<br />
17
2.2.3. Parallel-Pointer Maschinen<br />
In [GK96] haben die Autoren, aufbauend auf Knuths Linking Automat, die Parallel-<br />
Pointer-Maschine (PPM) <strong>de</strong>finiert, um darauf einen Sortieralgorithmus zu mo<strong>de</strong>llieren.<br />
Das Mo<strong>de</strong>ll besitzt mehrere synchronisierte Prozessoren, welche auf einen gemeinsamen<br />
Speicher zugreifen. Der Speicher kann entwe<strong>de</strong>r nach <strong>de</strong>m EREW, <strong>de</strong>m CREW o<strong>de</strong>r <strong>de</strong>m<br />
CRCW Pinzip betrieben wer<strong>de</strong>n. Er ist als gerichteter Graph repräsentiert und erlaubt<br />
nur die Zugriffe, welche durch die Pointer-Maschinen <strong>de</strong>finiert sind.<br />
Innerhalb <strong>de</strong>s Graphen beinhaltet je<strong>de</strong>r Knoten eine konstante Anzahl an Wertefel<strong>de</strong>rn<br />
und eine konstante Anzahl an Pointern zu an<strong>de</strong>ren Knoten.<br />
Prozessoren haben eine konstante Anzahl an Registern, in <strong>de</strong>nen Pointer stehen, welche<br />
einen Zugriff auf <strong>de</strong>n Speicher ermöglichen. Der gesamte Datenaustausch <strong>de</strong>r Prozessoren<br />
geschieht über <strong>de</strong>n gemeinsamen Speicher, es existiert keine an<strong>de</strong>re Kommunikationsmöglichkeit.<br />
Die Prozessoren können <strong>de</strong>n Speicher aktiv modifizieren, in<strong>de</strong>m sie bei<br />
Bedarf in einem Schritt neue Speicherzellen erstellen.<br />
Da die gesamte Ein- und Ausgabe <strong>de</strong>r PPM aus Pointern besteht, ist es wichtig festzulegen,<br />
welche Operationen erlaubt sind und welche nicht. Erlaubte Pointer Operationen<br />
sind:<br />
• Kopieren eines Pointers in ein Pointer-Register <strong>de</strong>s Prozessors.<br />
• Schreiben eines Pointer-Registers-Inhalts <strong>de</strong>s Prozessors in eine Pointer-Speicherzelle<br />
• Vergleichen von zwei Pointer-Registerinhalten auf Äquivalenz<br />
• Lesen <strong>de</strong>s Speicherinhalts, auf <strong>de</strong>n ein Pointer zeigt.<br />
Verboten ist Pointer-Arithmetik wie z. B. in<strong>de</strong>xierte Addressierung. Zusätzlich darf ein<br />
Prozessor auf Werte (nicht auf Pointer) die normalen Arithmetik- und Vergleichsoperationen<br />
ausführen.<br />
2.3. Die Zellularautomaten<br />
Das erste Mal wur<strong>de</strong>n Zellularautomaten von John von Neumann vorgeschlagen, welcher<br />
auf <strong>de</strong>r Suche nach einer selbstreproduzieren<strong>de</strong>n Maschine war. Es gelang ihm,<br />
einen Zellularautomaten anzugeben, welcher mit 29 Zustän<strong>de</strong>n fähig war, sich selbst zu<br />
reproduzieren. Später wur<strong>de</strong> dieser Automat von Signorini auf einer SIMD-Maschine<br />
implementiert ([Sig89]).<br />
Schon von Neumann zeigte, dass man mit <strong>de</strong>m Zellularautomaten je<strong>de</strong> beliebige Turingmaschine<br />
simulieren kann. Dieser Beweis wird beispielsweise in [Sar00] und [ARS71]<br />
nachvollzogen. [Sar00] bietet zu<strong>de</strong>m eine gute Übersicht über die Entwicklung von Zellularautomaten.<br />
18
Im Folgen<strong>de</strong>n wird zuerst <strong>de</strong>r klassische Zellularautomat (CA) vorgestellt und auf einige<br />
seiner Varianten eingegangen. In <strong>de</strong>n nachfolgen<strong>de</strong>n Abschnitten wer<strong>de</strong>n dann weitergehen<strong>de</strong><br />
Variationen <strong>de</strong>s CA vorgestellt. Es wird zusätzlich erläutert, in welcher Art und<br />
Weise sie sich von <strong>de</strong>m Original unterschei<strong>de</strong>n.<br />
2.3.1. Der klassische ”<br />
Cellular Automata“ (CA)<br />
Der Zellularautomat, <strong>de</strong>n von Neumann ursprünglich vorschlug, besteht aus in einem unendlichen<br />
Gitter angeordneten Zellen (mesh-connected). Das Gitter kann eine beliebige<br />
Dimension haben, jedoch arbeiten die meisten <strong>Algorithmen</strong> auf <strong>de</strong>m ein- o<strong>de</strong>r zweidimensionalen<br />
Gitter. Je<strong>de</strong> Zelle innerhalb <strong>de</strong>s Gitters führt ein eigenes Programm aus<br />
und kann auf seine Nachbarzellen lesend, aber nicht schreibend zugreifen.<br />
Die Art und <strong>de</strong>r <strong>Ra</strong>dius <strong>de</strong>r Nachbarschaft können vom Benutzer festgelegt wer<strong>de</strong>n, allerdings<br />
ist man auf die Möglichkeiten <strong>de</strong>r Gitterstruk<strong>tu</strong>r beschränkt. Die am weitesten verbreiteten<br />
Nachbarschaften sind die von Neumann“ und die Moore“ Nachbarschaften.<br />
” ”<br />
Diese bei<strong>de</strong>n Nachbarschaftsbeziehungen unterschei<strong>de</strong>n sich ab einem zweidimensionalen<br />
Gitter darin, dass die von Neumann“ Nachbarschaft nur die horizontalen und vertikalen<br />
”<br />
Zellen als Nachbarn betrachtet (siehe Abbildung 2.1), während in <strong>de</strong>r Nachbarschaft von<br />
Moore“ auch die diagonalen Zellen als Nachbarn betrachtet wer<strong>de</strong>n (siehe Abbildung<br />
”<br />
2.2).<br />
Der CA verfügt über keinen gemeinsamen Speicher, das heißt, alle Daten, welche im<br />
Verlauf <strong>de</strong>r Berechnung gebraucht wer<strong>de</strong>n, müssen entwe<strong>de</strong>r aus <strong>de</strong>r Initialkonfiguration<br />
errechenbar o<strong>de</strong>r ein Teil <strong>de</strong>r Initialisierung sein.<br />
Abbildung 2.1.: Nachbarschaftsbeziehung <strong>de</strong>r Zellen eines CA in einem zweidimensionalen<br />
unendlichen Gitters nach von Neumann.<br />
Formal besteht <strong>de</strong>r CA aus einem Vier<strong>tu</strong>pel M=(N,d,r,δ), wobei die einzelnen Werte<br />
folgen<strong>de</strong> Be<strong>de</strong>u<strong>tu</strong>ng haben:<br />
N = die Definition, welche Nachbarschaftsbeziehung benutzt wird,<br />
d = die Dimension <strong>de</strong>s Gitters,<br />
r = <strong>de</strong>r <strong>Ra</strong>dius <strong>de</strong>r Nachbarschaft,<br />
19
Abbildung 2.2.: Nachbarschaftsbeziehung <strong>de</strong>r Zellen eines CA in einem zweidimensionalen<br />
unendlichen Gitters nach Moore.<br />
δ = die Überführungsfunktion.<br />
Bei dieser Definition wird vorausgesetzt, dass die Zellen anfangs initialisiert sind.<br />
In <strong>de</strong>r Litera<strong>tu</strong>r fin<strong>de</strong>t man oft (z. B. [Maj94], [Sar00]) eine Unterteilung <strong>de</strong>r Zellularautomaten<br />
in vier Klassen:<br />
• Die erste Klasse <strong>de</strong>r CA been<strong>de</strong>t ihre Berechnung zu einem Zeitpunkt in einem<br />
stabilen Zustand, welcher sich von da an nicht mehr än<strong>de</strong>rt.<br />
• Die zweite Klasse zeigt ein periodisches Verhalten, d. h. es gibt einen immer wie<strong>de</strong>rkehren<strong>de</strong>n<br />
Ablauf von Konfigurationen, welcher sich nach einiger Zeit einstellt<br />
und dann nicht mehr unterbrochen wird.<br />
• Die dritte Klasse zeigt ein chaotisches Verhalten. Es wird im Verlauf <strong>de</strong>r Evolution<br />
(Berechnungen) keine Konfigurationsfolge erreicht, welche sich periodisch<br />
wie<strong>de</strong>rholt.<br />
• Die vierte Klasse führt im Laufe <strong>de</strong>r Evolution zu komplexen lokalen Struk<strong>tu</strong>ren,<br />
welche manchmal über mehrere Generationen bestehen bleiben. [Maj94] stellt fest,<br />
dass alle CA, welche zu universellen Berechnungen fähig sind, in dieser Klasse<br />
liegen, auch wenn nicht alle Automaten dieser Klasse universelle Berechnungen<br />
durchführen können.<br />
Da die Zellularautomaten zu vielen Forschungen angeregt haben, wur<strong>de</strong>n bald einige<br />
Modifikationen eingeführt. Eine gute Zusammenfassung dieser Modifikationen bietet<br />
[Sar00], dort sind alle Modifikationen außer <strong>de</strong>m CA Network zusammengefasst. Nachfolgend<br />
sollen die Modifikationen erläutert wer<strong>de</strong>n.<br />
2.3.1.1. Tesselation CA<br />
Diese Spezialform <strong>de</strong>s CA benötigt keine Initialisierung, son<strong>de</strong>rn verfügt über eine Eingabe,<br />
welche beim Start an alle Zellen angelegt wird. Anschaulich kann man sich das so<br />
20
vorstellen, dass je<strong>de</strong> Zelle über eine endliche Anzahl an Regeln verfügt und die Eingabe<br />
darüber entschei<strong>de</strong>t, welche Regel angewen<strong>de</strong>t wer<strong>de</strong>n soll. Tesselation CAs wer<strong>de</strong>n auch<br />
zeitverän<strong>de</strong>rliche CA genannt.<br />
2.3.1.2. Iterative CA<br />
Bei dieser Spezialform erhält nur eine Zelle eine Eingabe. Interessant ist diese Art von<br />
CA u. a. bei S<strong>tu</strong>dien zur Spracherkennung. Die eindimensionalen iterativen CA erkennen<br />
kontextfreie Sprachen in O(n 2 ) Schritten und die nicht<strong>de</strong>terministischen zweidimensionalen<br />
iterativen CA können in linearer Zeit je<strong>de</strong>s Wort akzeptieren, welches von einer<br />
nicht<strong>de</strong>terministischen Mehrkopf-Turingmaschine akzeptiert wird. Dennoch sind die iterativen<br />
CA merklich langsamer in <strong>de</strong>r Ausführung als die konventionellen CA.<br />
2.3.1.3. Dynamische CA<br />
Bei <strong>de</strong>n dynamischen CA darf mit <strong>de</strong>r Zeit sowohl die Anzahl <strong>de</strong>r Zellen als auch <strong>de</strong>ren<br />
Verbindungen zu <strong>de</strong>n Nachbarn variieren. Es kann also geschehen, dass eine Zelle von<br />
einem Zeitpunkt zu einem an<strong>de</strong>ren ihre Nachbarschaft än<strong>de</strong>rt o<strong>de</strong>r dass eine Zelle neu<br />
erschaffen o<strong>de</strong>r zerstört wird. Eine Verallgemeinerung dieses Ansatzes fin<strong>de</strong>t sich im<br />
GCA, welcher später vorgestellt wird.<br />
2.3.1.4. CA Networks (CAN)<br />
In [CNG + 01] versuchen die Autoren mit Hilfe von Zellularautomaten Schlammlawinen<br />
und Muren (Gerölllawinen) zu simulieren. Da solche Simulationen oft komplexe Zusammenhänge<br />
mit vielen einwirken<strong>de</strong>n Eigenschaften abbil<strong>de</strong>n müssen, wer<strong>de</strong>n viele Zellularautomaten<br />
in einem Netzwerk eingesetzt. Dabei hat je<strong>de</strong>r CA <strong>de</strong>s Netzwerks eine<br />
bestimmte Aufgabe und an<strong>de</strong>re CAs können lesend auf die Ergebnisse zugreifen.<br />
Das setzt natürlich voraus, dass die CA nicht nur parallel, son<strong>de</strong>rn auch sequentiell<br />
nacheinan<strong>de</strong>r arbeiten können. Die aus [CNG + 01] entnommene Abbildung 2.3 zeigt <strong>de</strong>n<br />
von <strong>de</strong>n Autoren vorgestellten Mo<strong>de</strong>llaufbau und ver<strong>de</strong>utlicht, dass die CA in einem<br />
CAN sowohl parallel als auch seriell arbeiten können. Dabei entsprechen die mit A1 bis<br />
A5 nummerierten Knoten <strong>de</strong>n Zellularautomaten und die dunklen Knoten dienen zur<br />
Ablaufsteuerung.<br />
Mit <strong>de</strong>m Netzwerk wird neben einer Datenparallelität auch eine Aufgabenparallelität erreicht<br />
und es können auch solche Probleme berechnet wer<strong>de</strong>n, die sich aus unvereinbaren<br />
Teilaufgaben zusammensetzen.<br />
2.3.2. Der ”<br />
Struc<strong>tu</strong>rally Dynamic Cellular Automata“ (SDCA)<br />
Die <strong>de</strong>n SDCA auszeichnen<strong>de</strong> Neuerung ist, dass die Verbindungen zwischen <strong>de</strong>n Zellen<br />
<strong>de</strong>s Automaten zur Laufzeit verän<strong>de</strong>rt wer<strong>de</strong>n können. Dafür gibt es zwei Funktionen,<br />
21
Abbildung 2.3.: Das CAN-Netzwerk, welches von [CNG + 01] verwen<strong>de</strong>t wird, um das<br />
SCIDDICA-Mo<strong>de</strong>ll darzustellen.<br />
die Kopplungs- und die Entkopplungsfunktion, die entwe<strong>de</strong>r neue Verbindungen anlegen<br />
(koppeln) o<strong>de</strong>r bestehen<strong>de</strong> Verbindungen löschen (entkoppeln). Eine Einschränkung<br />
dabei ist, dass nur bereits verbun<strong>de</strong>ne Zellen entkoppelt wer<strong>de</strong>n dürfen und nur solche<br />
Zellen aneinan<strong>de</strong>r gekoppelt wer<strong>de</strong>n dürfen, die über genau einen Zwischenknoten eine<br />
Verbindung haben. Dieses Mo<strong>de</strong>ll wur<strong>de</strong> von Ilachinsky und Halpern vorgestellt, um so<br />
in <strong>de</strong>r Lage zu sein, u. a. das menschliche Gehirn besser nachbil<strong>de</strong>n zu können.<br />
In [Maj94] stellt <strong>de</strong>r Autor Majercik das von Ilachinsky und Halpern entworfene SDCA-<br />
Mo<strong>de</strong>ll vor und äußert seine Kritik. Daraufhin entwickelt er drei alternative Mo<strong>de</strong>lle,<br />
welche im Folgen<strong>de</strong>n näher erläutert wer<strong>de</strong>n. Zu<strong>de</strong>m enthält <strong>de</strong>r Text auch eine Betrach<strong>tu</strong>ng<br />
<strong>de</strong>r Laufzeitgeschwindigkeiten <strong>de</strong>r verschie<strong>de</strong>nen Mo<strong>de</strong>lle sowie eine Konstruktion,<br />
um einen beliebigen CA mit einem ULRL 4 SDCA mit Speed-Up von zwei zu simulieren.<br />
2.3.2.1. Das Relative Location Mo<strong>de</strong>ll<br />
In diesem Mo<strong>de</strong>ll kann die Zustandsübergangsfunktion nicht nur auf <strong>de</strong>n Sta<strong>tu</strong>s einer<br />
je<strong>de</strong>n Nachbarzelle zugreifen, son<strong>de</strong>rn kennt auch <strong>de</strong>ren (von <strong>de</strong>r anfragen<strong>de</strong>n Zelle aus<br />
gesehen) relative Adresse. Die Verbindungsübergangsfunktion kennt zu je<strong>de</strong>r Zelle <strong>de</strong>s<br />
Auotmaten <strong>de</strong>n Verbindungssta<strong>tu</strong>s (0 = unverbun<strong>de</strong>n, 1 = verbun<strong>de</strong>n, 2 = im nächsten<br />
Schritt verbindbar) und han<strong>de</strong>lt dann <strong>de</strong>n Regeln entsprechend, legt also im nächsten<br />
Schritt eine Verbindung an, zerstört Verbindungen im nächsten Schritt o<strong>de</strong>r lässt die<br />
Verbindung unberührt. Sowohl die Zustandsübergangs- als auch die Verbindungsübergangsfunktion<br />
müssen rekursiv berechenbar sein.<br />
Dieses Mo<strong>de</strong>ll kann variiert wer<strong>de</strong>n, in<strong>de</strong>m die Anzahl <strong>de</strong>r Verbindungen beschränkt<br />
wird (Boun<strong>de</strong>d Links Relative Location Mo<strong>de</strong>l) o<strong>de</strong>r nicht (Unboun<strong>de</strong>d Links Relative<br />
Location Mo<strong>de</strong>l).<br />
Der Nachteil dieses Mo<strong>de</strong>lls ist, dass man bei vielen Simulationen nicht voraussetzen<br />
kann, dass eine Zelle wirklich die relative Adresse ihrer Nachbarn kennt. So ist einer<br />
Zelle <strong>de</strong>s menschlichen Gehirns z. B. nicht bekannt, wo genau ihr Verbindungspartner<br />
sitzt. Sie benötigt dieses Wissen auch nicht, um ein korrektes Ergebnis zu liefern.<br />
4 ULRL = Unboun<strong>de</strong>d Links Relative Location<br />
22
2.3.2.2. Das Labeled Link Mo<strong>de</strong>ll<br />
In diesem Mo<strong>de</strong>ll kennt die Zelle die relative Adresse ihrer Nachbarn nicht. Dafür hat<br />
je<strong>de</strong> Verbindung <strong>de</strong>r Zelle ein Label, welches die Zustandsübergangsfunktion mit in die<br />
Berechnungen einbeziehen kann.<br />
In diesem Mo<strong>de</strong>ll (wie in allen an<strong>de</strong>ren hier vorgestellten SDCA Mo<strong>de</strong>llen) wer<strong>de</strong>n die<br />
Verbindungen als bidirektional angesehen. Trotz<strong>de</strong>m können zwei Nachbarzellen verschie<strong>de</strong>ne<br />
Labels für ein und dieselbe Verbindung haben. Auf diese Weise können auch<br />
unidirektionale Verbindungen mo<strong>de</strong>lliert wer<strong>de</strong>n, in<strong>de</strong>m eine Zelle das Label <strong>de</strong>r Verbindung<br />
auf ignorieren setzt.<br />
Die Verbindungsübergangsfunktion arbeitet vergleichbar zu <strong>de</strong>r <strong>de</strong>s vorhergehen<strong>de</strong>n Mo<strong>de</strong>lls.<br />
Auch hier kann wie<strong>de</strong>r die Anzahl <strong>de</strong>r Verbindungen einer Zelle beschränkt wer<strong>de</strong>n<br />
(Boun<strong>de</strong>d Links Labeled Link Mo<strong>de</strong>l) o<strong>de</strong>r nicht (Unboun<strong>de</strong>d Links Labeled Link Mo<strong>de</strong>l).<br />
Auch wenn dieses Mo<strong>de</strong>ll schon annehmbarer als das vorherige ist, so verfügt es doch<br />
noch nicht über eine endliche Übergangstabelle wie <strong>de</strong>r konventionelle CA. Um diese<br />
endliche Übergangstabelle zu erreichen, wird das Boun<strong>de</strong>d Links Labeled Link Mo<strong>de</strong>l<br />
weiter eingeschränkt, in<strong>de</strong>m die Menge <strong>de</strong>r möglichen Labels endlich ist (Finite Labels<br />
Labeled Links Mo<strong>de</strong>l). Da je<strong>de</strong> Zelle eine endliche Anzahl an Verbindungen hat und<br />
diese Verbindungen jeweils eine von endlich vielen unterschiedlichen Labels haben kann,<br />
entstehen nur endlich viele Einträge in <strong>de</strong>r Übergangstabelle.<br />
2.3.2.3. Das Symmetric Mo<strong>de</strong>ll<br />
In diesem Mo<strong>de</strong>ll weiß eine Zelle nur, welche Zustän<strong>de</strong> wie oft unter ihren Nachbarn<br />
vertreten sind, aber nicht, welcher Nachbar welchen Zustand hat. Bei Problemen wie<br />
<strong>de</strong>m ”<br />
Game of Life“ ist dies keine Einschränkung, aber es kann Probleme geben, bei<br />
<strong>de</strong>nen diese Einschränkung massiv ist.<br />
Majercik betont, dass dieses Mo<strong>de</strong>ll als Vermittler zwischen <strong>de</strong>m ursprünglichen Mo<strong>de</strong>ll<br />
von Ilachinsky und Halpern und <strong>de</strong>n eben vorgestellten Mo<strong>de</strong>llen dienen soll.<br />
Auch in diesem Mo<strong>de</strong>ll kann wie<strong>de</strong>r zwischen einer beschränkten Anzahl von Verbindungen<br />
pro Zelle (Boun<strong>de</strong>d Links Symmetric) und einer unbeschränkten Anzahl (Unboun<strong>de</strong>d<br />
Links Symmetric) unterschei<strong>de</strong>n.<br />
2.3.3. Der ”<br />
Dynamic Struc<strong>tu</strong>re Cellular Automata“ (DSCA)<br />
Der DSCA erweitert <strong>de</strong>n CA <strong>de</strong>rmaßen, dass das Mo<strong>de</strong>ll nun auch auf Eingaben von<br />
außen reagieren kann.<br />
Innerhalb <strong>de</strong>s CA können Nachrichten versen<strong>de</strong>t wer<strong>de</strong>n, die verschie<strong>de</strong>ne Eventtypen<br />
haben und die Funktion <strong>de</strong>r Zelle beeinflussen. In [MWIS02] wird dieses Prinzip anhand<br />
23
von Feuer-Simulationen erklärt, wobei <strong>de</strong>r CA die Simulation <strong>de</strong>r Feuerausbrei<strong>tu</strong>ng vornimmt.<br />
Damit auch Funkenflug in die Berechnungen eingehen kann, existiert ein Generator,<br />
welcher entsprechen<strong>de</strong> Nachrichten an <strong>de</strong>n CA sen<strong>de</strong>t und damit <strong>de</strong>ssen Zustän<strong>de</strong><br />
aktiv beeinflusst.<br />
Die Vorstellung von Nachrichten, welche an Zellen <strong>de</strong>s CA geschickt wer<strong>de</strong>n, ob über<br />
einen internen Verteiler o<strong>de</strong>r nicht, scheint zunächst einmal <strong>de</strong>m Prinzip <strong>de</strong>s CA zu<br />
wi<strong>de</strong>rsprechen. Dieses besagt, dass nur lesend auf Zellen zugegriffen wer<strong>de</strong>n kann.<br />
Allerdings kann <strong>de</strong>r interne Verteiler so organisiert sein, dass er selbst kein aktives Mitglied<br />
<strong>de</strong>s CA ist, aber alle Zellen <strong>de</strong>s CA lesend auf ihn zugreifen können. Dieser Verteiler<br />
stellt dann für je<strong>de</strong> Zelle einen Port bereit, auf <strong>de</strong>n die Zelle jeweils vor ihrem Berechnungsschritt<br />
lesend zugreift, um zu sehen, ob eine Nachricht für sie vorhan<strong>de</strong>n ist. Da <strong>de</strong>r<br />
verwen<strong>de</strong>te CA asynchron ist, muss <strong>de</strong>r Verteiler reagieren, wenn die Nachricht abgeholt<br />
wird und diese löschen, da sonst eine Nachricht mehrfach von <strong>de</strong>r Zelle abgeholt wer<strong>de</strong>n<br />
kann.<br />
2.3.4. Der ”<br />
Global Cellular Automata“ (GCA)<br />
Der GCA ([HVH03]) erlaubt es Zellen, im Laufe <strong>de</strong>r Evolution ihre Nachbarschaft zu<br />
verän<strong>de</strong>rn. Außer<strong>de</strong>m können zwei verschie<strong>de</strong>ne Zellen eines GCA verschie<strong>de</strong>ne Übergangsfunktionen<br />
und Nachbarschaften haben. Die Zellen eines GCA können also verschie<strong>de</strong>ne<br />
Aufgaben erfüllen.<br />
Mit <strong>de</strong>r Möglichkeit, die Nachbarschaft immer an die ak<strong>tu</strong>elle Aufgabenstellung anzupassen,<br />
ergibt sich ein sehr mächtiges Mo<strong>de</strong>ll, welches aber Einschränkungen unterliegt,<br />
um noch realisierbar zu sein. So muss z. B. die Gesamtheit <strong>de</strong>r im Verlauf <strong>de</strong>r Berechnungen<br />
einbezogenen Nachbarzellen schon beim Aufbau bekannt sein, damit die benötigten<br />
Verbindungen eingeplant wer<strong>de</strong>n können.<br />
Um die ak<strong>tu</strong>elle Nachbarschaft bestimmen zu können, ist es nötig, dass die Nachbarschaft<br />
entwe<strong>de</strong>r wie beim SDCA über eine Übergangsfunktion bestimmt wer<strong>de</strong>n kann o<strong>de</strong>r von<br />
Anfang an für je<strong>de</strong> Zelle für je<strong>de</strong> Generation die Nachbarschaft, z. B. in einer Tabelle,<br />
festgelegt ist.<br />
Der GCA erlaubt es, dass Zellen, welche nicht direkt zur Zelle benachbart liegen, in die<br />
Nachbarschaft <strong>de</strong>r Zelle aufgenommen wer<strong>de</strong>n. Somit verzichtet dieses Mo<strong>de</strong>ll auf eine<br />
Gitterstruk<strong>tu</strong>r wodurch sich Nachrichten schneller im Automaten ausbreiten können.<br />
Die zellulare Lichtgeschindigkeit 5 (s. u.a. [Hoc98]) wird in diesem Mo<strong>de</strong>ll überschritten.<br />
Einen Überblick über <strong>de</strong>n GCA liefert auch [Hee01], dort wird <strong>de</strong>r Begriff <strong>de</strong>r zellularen<br />
Lichtgeschwindigkeit näher erläutert.<br />
5 Im CA spricht man von <strong>de</strong>r Zellularen Lichtgeschwindigkeit, da Nachrichten bei je<strong>de</strong>r Generation nur<br />
um eine Zelle weitergereicht wer<strong>de</strong>n können. Der GCA durchbricht diese Beschränkung, da nun die<br />
Nachbarschaftsbeziehung dynamisch ist und somit mehrere Zellen bei einer Generation übersprungen<br />
wer<strong>de</strong>n können (bezüglich <strong>de</strong>r Gitterstruk<strong>tu</strong>r).<br />
24
Da <strong>de</strong>r GCA dynamisch ist, können sich seine Daten und seine Verbindungen (Pointer)<br />
zur Laufzeit abhängig von gewissen Gegebenheiten än<strong>de</strong>rn. Eine grobe Unterteilung, die<br />
auch in [Hee01] getroffen wird, ist die Unterteilung in Zeitabhängigkeit, Ortsabhängigkeit<br />
und Datenabhängigkeit. Zusätzlich wird hier noch eine Abhängigkeit von <strong>de</strong>n Verbindungen<br />
an sich eingeführt. Diese vier Punkte können (und wer<strong>de</strong>n im weiteren) noch<br />
weiter differenziert wer<strong>de</strong>n:<br />
• Die Datenabhängigkeit kann dahingehend untersucht wer<strong>de</strong>n, ob die Daten bzw.<br />
die Verbindungen <strong>de</strong>r Zelle im nächsten Schritt von <strong>de</strong>n eigenen Daten und/o<strong>de</strong>r<br />
von <strong>de</strong>n Daten ihrer Nachbarn abhängt. Im weiteren wer<strong>de</strong>n die eigenen Daten mit<br />
d und die Daten <strong>de</strong>r Nachbar-Zellen mit d* bezeichnet.<br />
• Die Zeitabhängigkeit benötigt keiner weiteren Verfeinerung. Es muss nur festgestellt<br />
wer<strong>de</strong>n, ob die Zellen zu einer bestimmten Zeit immer einer bestimmten<br />
Verän<strong>de</strong>rung unterliegen. Die Zeit wird im weiteren mit t bezeichnet.<br />
• Die Ortsabhängigkeit kann auch dahingehend unterteilt wer<strong>de</strong>n, dass geschaut<br />
wer<strong>de</strong>n muss, ob eine Verän<strong>de</strong>rung <strong>de</strong>r Daten bzw. <strong>de</strong>r Verbindungen einer Zelle<br />
davon abhängig ist, wo die eigene Zelle bzw. die Nachbarzelle angeordnet ist. Der<br />
Ort <strong>de</strong>r eigenen Zelle wird im weiteren mit l <strong>de</strong>r Ort <strong>de</strong>r Nachbar-Zelle mit l*<br />
bezeichnet.<br />
• Die Verbindungsabhängigkeit bezeichnet eine Abhängigkeit <strong>de</strong>r Daten o<strong>de</strong>r <strong>de</strong>r<br />
Verbindungen in <strong>de</strong>r nächsten Generation von <strong>de</strong>r momentanen Verbindung <strong>de</strong>r<br />
eigenen Zelle o<strong>de</strong>r <strong>de</strong>r Nachbar-Zellen. Ein Beispiel hierfür fin<strong>de</strong>t sich in Kapitel<br />
4. Die Verbindung <strong>de</strong>r Zelle wird mit p, die Verbindung <strong>de</strong>r Nachbar-Zelle mit p*<br />
bezeichnet.<br />
Die Unterteilungen sind immer für eine Zelle mit einem Nachbarn formuliert. Betrachtet<br />
man eine Zelle, die mehrere Nachbarn hat, dann bezeichnen d*,l* und p* eine geeignete<br />
Verknüpfung aller Nachbarn. Es erscheint im weiteren Verlauf sinnvoll, die Daten und<br />
die Verbindungen einer Zelle getrennt zu betrachten, da sie sich unabhängig voneinan<strong>de</strong>r<br />
än<strong>de</strong>rn können. Mit Hilfe <strong>de</strong>r Unterteilung 6 kann man die Daten (d) und die Verbindung<br />
(p) <strong>de</strong>r Zelle in <strong>de</strong>r nächsten Generation als Funktion ihrer Abhängigkeiten formulieren:<br />
d’= f(d,d*,t,l,l*,p,p*) und p’= g(d,d*,t,l,l*,p,p*).<br />
Je nach auf <strong>de</strong>m GCA realisierten Algorithmus ist die Funktion nur von einem Teil <strong>de</strong>r<br />
Parameter abhängig. In Abbildung 2.4 wur<strong>de</strong> die weitere Unterteilung <strong>de</strong>r Daten-, Ortsund<br />
Verbindungsabhängigkeit ignoriert. Mit Hilfe <strong>de</strong>s dort angegebenen Baums können<br />
die Parameter <strong>de</strong>r Verbindungs- bzw. Datenübergangsfunktion gefun<strong>de</strong>n wer<strong>de</strong>n.<br />
6 Die Unterteilung repräsentiert nur entwe<strong>de</strong>r die Abhängigkeit <strong>de</strong>r Daten o<strong>de</strong>r <strong>de</strong>r Verbindungen.<br />
Zwar kann die Unterteilung auf bei<strong>de</strong> angewen<strong>de</strong>t wer<strong>de</strong>n, aber es ist nicht möglich, bei<strong>de</strong> Abhängigkeiten<br />
mit Hilfe dieser Unterteilung zeitgleich zu bestimmen.<br />
25
Abbildung 2.4.: Innerhalb <strong>de</strong>s Baums steht in <strong>de</strong>m Kasten immer eine Abhängigkeit. Ist<br />
diese Abhängigkeit gegeben, so muss die Abzweigung nach links, sonst<br />
die nach rechts verwen<strong>de</strong>t wer<strong>de</strong>n. Wur<strong>de</strong>n alle Knoten abgelaufen, kann<br />
man ablesen, von welchen Variablen z. B. die Daten <strong>de</strong>r nächsten Generation<br />
abhängig sind.<br />
Da das Mo<strong>de</strong>ll <strong>de</strong>s GCA im Weiteren zur Mo<strong>de</strong>llierung von <strong>Algorithmen</strong> genutzt wird,<br />
wer<strong>de</strong>n dort noch Beispiele aufgezeigt, wie man die Möglichkeiten <strong>de</strong>s GCA effizient<br />
nutzen kann.<br />
26
3. Die Mo<strong>de</strong>llierung von<br />
Graphenalgorithmen auf <strong>de</strong>m GCA<br />
Graphenalgorithmen haben auch heute noch eine große Be<strong>de</strong>u<strong>tu</strong>ng in <strong>de</strong>r Informatik, da<br />
man mit ihrer Hilfe viele Probleme sehr anschaulich und effizient lösen kann. Graphen<br />
wer<strong>de</strong>n <strong>de</strong>finiert durch zwei Mengen: die Menge V <strong>de</strong>r Knoten und die Menge E <strong>de</strong>r<br />
Kanten. Dabei verbin<strong>de</strong>t eine Kante aus E immer zwei Knoten aus V. Kanten können<br />
gerichtet (die Kante kann nur in eine Rich<strong>tu</strong>ng durchlaufen wer<strong>de</strong>n) o<strong>de</strong>r ungerichtet<br />
(die Kante kann in bei<strong>de</strong>n Rich<strong>tu</strong>ngen durchlaufen wer<strong>de</strong>n) sein.<br />
Ein Graph selber kann in vielen Varianten im Rechner repräsentiert sein, wobei eine <strong>de</strong>r<br />
häufigeren Darstellungen die Matrix ist. Zur Veranschaulichung dieser Matrixrepräsentation<br />
soll hier <strong>de</strong>r Graph aus Abbildung 3.1 dienen, welcher durch die folgen<strong>de</strong> Matrix<br />
A repräsentiert wer<strong>de</strong>n kann.<br />
Abbildung 3.1.: Ein gerichteter Beispielgraph, welcher sowohl einen Zyklus enthält als<br />
auch einen Knoten, <strong>de</strong>r keine ausgehen<strong>de</strong>n Kanten hat.<br />
⎛<br />
A =<br />
⎜<br />
⎝<br />
0 1 0 1 0<br />
0 0 1 0 0<br />
1 0 0 0 0<br />
0 0 0 0 1<br />
0 0 0 0 0<br />
Der Vorteil dieser Darstellung ist, dass die i-te Zeile <strong>de</strong>r Matrix die ausgehen<strong>de</strong>n Verbindungen<br />
und die i-te Spalte die eingehen<strong>de</strong>n Verbindungen <strong>de</strong>s i-ten Knotens repräsentiert.<br />
Allerdings muss man alle Zeileneinträge <strong>de</strong>r i-ten Zeile abgehen, um genau festzu-<br />
⎞<br />
⎟<br />
⎠<br />
27
stellen, welche Nachfolger <strong>de</strong>r Knoten i hat. Möchte man aber testen, ob eine Verbindung<br />
zwischen <strong>de</strong>m i-ten und <strong>de</strong>m j-ten Knoten existiert, so reicht eine Anfrage.<br />
Einer <strong>de</strong>r größten Nachteile dieser Repräsentation ist allerdings, dass man auf einem<br />
klassischen Einprozessor-System minimal O(n 2 ) Schritte benötigt, wenn ein Problem<br />
mit <strong>de</strong>r Matrizendarstellung arbeitet und einen Großteil <strong>de</strong>s Graphen betrachten muss.<br />
Im Laufe dieser Arbeit wird <strong>de</strong>utlich wer<strong>de</strong>n, dass es nicht möglich ist, diesen Aufwand zu<br />
minimieren. Allerdings ist es möglich, Teile <strong>de</strong>s Aufwands auf die Hardware zu verlagern<br />
und so einen Speed-Up zu erzielen.<br />
Diese Aussage wird am Beispiel <strong>de</strong>s Warshall-Algorithmus noch <strong>de</strong>utlicher wer<strong>de</strong>n, wobei<br />
klar sein sollte, dass ein Aufwand von O(n) be<strong>de</strong>utet, dass <strong>de</strong>r Algorithmus eine Laufzeit<br />
von O(n) hat, falls nicht explizit etwas an<strong>de</strong>res erwähnt wird.<br />
3.1. Erkennung von zusammenhängen<strong>de</strong>n Komponenten<br />
eines Graphen<br />
In vielen Aufgabenstellungen aus <strong>de</strong>r Praxis ist es wichtig, zu erkennen, ob Komponenten<br />
eines Graphen zusammenhängen. So möchte man z. B. effizient testen, ob alle Computer<br />
eines Netzwerks verbun<strong>de</strong>n sind.<br />
Auf einem Rechner mit einem Prozessor kann man hier u. a. <strong>de</strong>n Warshall-Algorithmus<br />
anwen<strong>de</strong>n, welcher die transitive Hülle berechnet und somit die Erreichbarkeitsmatrix<br />
liefert. Dabei steht eine 1 in Zeile A und Spalte B <strong>de</strong>r Matrix immer dafür, dass es einen<br />
Weg zwischen <strong>de</strong>m Punkt A und <strong>de</strong>m Punkt B gibt, eine 0 <strong>de</strong>mentsprechend dafür, dass<br />
kein Weg existiert. Der Algorithmus von Warshall benötigt auf einem Einprozessor-<br />
System eine Laufzeit von O(n 3 ). Im folgen<strong>de</strong>n Abschnitt wird schrittweise hergeleitet,<br />
wie man <strong>de</strong>n Warshall-Algorithmus auf <strong>de</strong>m GCA mo<strong>de</strong>llieren kann und wie es möglich<br />
ist, eine Laufzeit von O(n) zu erreichen.<br />
3.1.1. Der Warshall-Algorithmus<br />
Der Warshall-Algorithmus ist eine Spezialisierung <strong>de</strong>s Kleene-Algorithmus und je nach<br />
Litera<strong>tu</strong>r berechnet er die reflexive-transitive-Hülle 1 o<strong>de</strong>r nur die transitive Hülle 2 . Ein<br />
Vorteil <strong>de</strong>r Berechnung <strong>de</strong>r reinen transitiven Hülle ist, dass man sehr leicht mit <strong>de</strong>m<br />
Warshall-Algorithmus testen kann, ob ein Graph Zyklen enthält.<br />
Da es für die Erkennung von zusammenhängen<strong>de</strong>n Komponenten irrelevant ist, ob die<br />
Hülle reflexiv ist o<strong>de</strong>r nicht, wird im weiteren <strong>de</strong>r Warshall-Algorithmus verwen<strong>de</strong>t,<br />
1 Bei <strong>de</strong>r reflexiven Hülle existiert von je<strong>de</strong>m Knoten eine Verbindung zu sich selber. Auf <strong>de</strong>r Diagonalen<br />
stehen also nur Einsen. Die reflexive-transitive Hülle erfüllt die Bedingung <strong>de</strong>r reflexiven Hülle<br />
zusätzlich zu <strong>de</strong>n Bedingungen <strong>de</strong>r transitiven Hülle.<br />
2 Hier steht auf <strong>de</strong>r Diagonale nur dann eine 1, wenn es von <strong>de</strong>m Knoten aus möglich ist, wie<strong>de</strong>r zu<br />
<strong>de</strong>m Knoten zu kommen<br />
28
welcher rein die transitive Hülle berechnet. Nachzulesen sind <strong>de</strong>r Warshall-Algorithmus<br />
und <strong>de</strong>r im nächsten Kapitel folgen<strong>de</strong> Floyd-Warshall-Algorithmus u.a. in [CLRS01] auf<br />
Seite 632-634.<br />
3.1.1.1. Der Warshall auf einem Einprozessor-System<br />
Eine Eigenschaft, die alle <strong>Algorithmen</strong> haben, welche vom Kleene-Algorithmus abstammen,<br />
ist die Nutzung <strong>de</strong>s Transitknotenkonzepts. Transitknoten be<strong>de</strong>utet hierbei, dass<br />
je<strong>de</strong>r Knoten i nur dann neue Verbindungen aufnehmen darf, wenn für ihn eine Verbindung<br />
zum Transitknoten besteht. Alle an<strong>de</strong>ren Verbindungen <strong>de</strong>s Knotens wer<strong>de</strong>n für<br />
diesen Schritt ignoriert.<br />
Besteht die Verbindung zum Transitknoten, so kann <strong>de</strong>r Knoten i zu je<strong>de</strong>m Knoten j, mit<br />
<strong>de</strong>m <strong>de</strong>r Transitknoten verbun<strong>de</strong>n ist, seinerseits eine Verbindung aufbauen. Es wer<strong>de</strong>n<br />
also Wege <strong>de</strong>r Länge zwei erlaubt, welche über <strong>de</strong>n Transitknoten gehen müssen.<br />
Programmiert man <strong>de</strong>n Warshall-Algorithmus, so gestaltet er sich einfach: er besteht<br />
aus drei ineinan<strong>de</strong>r geschachtetelten Schleifen und <strong>de</strong>r Formulierung <strong>de</strong>s Transitknotenprinzips<br />
(Zeile 4 im nachfolgen<strong>de</strong>n Algorithmus). In C formuliert sieht <strong>de</strong>r Algorithmus<br />
dann folgen<strong>de</strong>rmaßen aus:<br />
Listing 3.1: Der Warshall-Algorithmus<br />
1 for (k=0; k < AnzahlKnoten ; k++) {<br />
2 for ( i =0; i < AnzahlKnoten ; i++) {<br />
3 for ( j =0; j < AnzahlKnoten ; j++) {<br />
4 A[ i ] [ j ]= A[ i ] [ j ] | | (A[ i ] [ k ] && A[ k ] [ j ] )<br />
5 }<br />
6 }<br />
7 }<br />
Man kann <strong>de</strong>n Algorithmus noch ein wenig verbessern, in<strong>de</strong>m man die Abfrage, ob eine<br />
Verbindung von i nach k besteht (die Abfrage A[i][k] in Zeile 4), vor die innere Schleife<br />
zieht. In <strong>de</strong>r Mo<strong>de</strong>llierung auf <strong>de</strong>m GCA wird <strong>de</strong>r modifiziert Warshall-Algorithmus<br />
verwen<strong>de</strong>t, welche auch noch ausnutzt, dass eine Überprüfung keine Än<strong>de</strong>rung ergibt,<br />
wenn <strong>de</strong>r zu modifizieren<strong>de</strong> Knoten <strong>de</strong>r Transitknoten ist:<br />
Listing 3.2: Der leicht modifizierte Warshall-Algorithmus<br />
1 for (k=0; k < AnzahlKnoten ; k++) {<br />
2 for ( i =0; i < AnzahlKnoten ; i++) {<br />
3 if (k!= i && A[ i ] [ k]==1) {<br />
4 for ( j =0; j < AnzahlKnoten ; j++) {<br />
5 A[ i ] [ j ]= A[ i ] [ j ] | | A[ k ] [ j ] )<br />
6 }<br />
7 }<br />
8 }<br />
9 }<br />
29
Trotz dieser Verbesserung spart man im worst-case nur einen Durchlauf <strong>de</strong>r inneren<br />
Schleife und somit ist <strong>de</strong>r Aufwand auf O(n 3 ) abzuschätzen. Da <strong>de</strong>r best-case (die innere<br />
Schleife wird so selten wie möglich durchlaufen, das be<strong>de</strong>utet, nur auf <strong>de</strong>r Diagonalen<br />
stehen Einser) sehr unwahrscheinlich ist, wäre nur noch <strong>de</strong>r average-case interessant. In<br />
<strong>de</strong>r Regel wird man eine Zeile ca. zur Hälte mit Einsen befüllt haben. Daraus folgt, dass<br />
man ca. die Hälfte <strong>de</strong>r inneren Schleifendurchläufe einsparen kann. Dies ergibt dann<br />
eine Laufzeit von O(n 2 × n 2 ), was aber in <strong>de</strong>r O-Notation wie<strong>de</strong>r auf O(n3 ) abgeschätzt<br />
wird. Diese Modifikation bringt also einen Laufzeitgewinn, aber die Laufzeitkomplexität<br />
än<strong>de</strong>rt sich dadurch nicht.<br />
3.1.1.2. Der Warshall-Algorithmus auf <strong>de</strong>m GCA<br />
Um einen Algorithmus auf einem parallelen Mo<strong>de</strong>ll wie <strong>de</strong>m GCA effizient zu mo<strong>de</strong>llieren,<br />
muss zunächst hergeleitet wer<strong>de</strong>n, welche Schritte parallelisierbar sind:<br />
Bei <strong>de</strong>m Warshall-Algorithmus ist ersichtlich, dass die Herlei<strong>tu</strong>ng <strong>de</strong>r Verbindungen <strong>de</strong>r<br />
Länge 2 jeweils nur von <strong>de</strong>m Startknoten und <strong>de</strong>m Transitknoten abhängig sind. Alle<br />
an<strong>de</strong>ren Knoten wer<strong>de</strong>n an dieser Stelle ignoriert. Also kann man alle Knoten parallel<br />
als Startknoten betrachten und die Verbindungsmöglichkeiten testen. Hierfür benötigt<br />
man O(n) Prozessoren, um wirklich alle Knoten parallel abzuarbeiten.<br />
Eine weitere Möglichkeit zur Parallelisierung bietet sich für je<strong>de</strong>n Knoten bei <strong>de</strong>r Herstellung<br />
<strong>de</strong>r Verbindungen an sich. Im Normalfall wird zuerst überprüft, ob <strong>de</strong>r Startknoten<br />
eine Verbindung zum Transitknoten hat, und falls dies <strong>de</strong>r Fall ist, wird für je<strong>de</strong>n an<strong>de</strong>ren<br />
Knoten geprüft, ob es nun eine Verbindung zu ihm gibt o<strong>de</strong>r schon früher gab. Parallelisiert<br />
man diesen Vorgang, so wer<strong>de</strong>n die Verbindungen zu allen Knoten ausgehend von<br />
einem festem Startknoten und einem festen Transitknoten gleichzeitig gesucht.<br />
Mo<strong>de</strong>lliert man nun diese Parallelisierungsvorschläge auf <strong>de</strong>m GCA, so äußert sich das<br />
folgen<strong>de</strong>rmaßen:<br />
• Um bei einem gegebenem Transitknoten alle Knoten parallel als Startknoten abzuarbeiten,<br />
muss dafür gesorgt wer<strong>de</strong>n, dass je<strong>de</strong> Zeile <strong>de</strong>r Matrix durch eine Zelle<br />
repräsentiert wird [3.2]. Die Zellen können alle parallel arbeiten und garantieren<br />
so die gewünschte Parallelität. Da nur ein lesen<strong>de</strong>r Zugriff auf die Zelle, welche<br />
<strong>de</strong>n Transitknoten repräsentiert, nötig ist, ist diese Art <strong>de</strong>r Mo<strong>de</strong>llierung mit <strong>de</strong>n<br />
Bedingungen <strong>de</strong>s GCA konform.<br />
• Um bei einem gegebenem Transitknoten und einem gegebenem Startknoten alle<br />
Verbindungen parallel zu prüfen und zu modifizieren, gibt es zwei Möglichkeiten:<br />
Bei <strong>de</strong>r ersten Möglichkeit wird je<strong>de</strong>r einzelne Eintrag einer Zeile durch eine Zelle<br />
repräsentiert. Es wer<strong>de</strong>n also n 2 Zellen benötigt. Eines <strong>de</strong>r dabei auftreten<strong>de</strong>n<br />
Probleme ist, dass die Zellen feststellen müssen, mit welchen an<strong>de</strong>ren Zellen sie zu<br />
kommunizieren haben. Da eine Zeile durch mehrere Zellen repräsentiert wird, ist<br />
einerseits auf die Zelle <strong>de</strong>r eigenen Zeile lesend zuzugreifen, welche die Verbindung<br />
30
zum Transitknoten repräsentiert, und an<strong>de</strong>rerseits ist die entsprechen<strong>de</strong> Zelle <strong>de</strong>s<br />
Transitknotens zu lesen. Dies führt einerseits zu einem hohen Kommunikationsbedarf<br />
3 und an<strong>de</strong>rerseits zu <strong>de</strong>r For<strong>de</strong>rung einer Logik, die erkennt, welche Zelle<br />
angesprochen wer<strong>de</strong>n muss.<br />
Die zweite Möglichkeit besteht darin, die Zeilen <strong>de</strong>r Matrix jeweils wie<strong>de</strong>r durch<br />
eine Zelle repräsentieren zu lassen. Um dabei einen parallelen Abgleich <strong>de</strong>r Zeileneinträge<br />
zu garantieren, wird ein Bussystem verwen<strong>de</strong>t. Je<strong>de</strong>r Knoten weiß, welcher<br />
Knoten ak<strong>tu</strong>ell <strong>de</strong>r Transitknoten ist, also auch <strong>de</strong>r Transitknoten an sich. Beim<br />
Start <strong>de</strong>s Taktes legt <strong>de</strong>r Transitknoten seine Zeile auf <strong>de</strong>n Bus und je<strong>de</strong>r Knoten,<br />
welcher eine Verbindung zu <strong>de</strong>m Transitknoten hat, kann sich <strong>de</strong>ssen Werte<br />
dann vom Bus holen. Zellen, welche keine Verbindung zum Transitknoten haben,<br />
ignorieren <strong>de</strong>n Bus bis zum nächsten Taktzyklus. Bei dieser Realisierung wer<strong>de</strong>n<br />
O(n) Verbindungen benötigt, allerdings ist es möglich, gleich eine Logik einzubauen,<br />
welche <strong>de</strong>n i-ten Zeileneintrag mit <strong>de</strong>m i-ten Eintrag auf <strong>de</strong>m Bus vero<strong>de</strong>rt.<br />
Die zugrun<strong>de</strong>liegen<strong>de</strong> Logik ist also sehr einfach und ebenso effizient wie <strong>de</strong>r erste<br />
Lösungsansatz.<br />
Bei<strong>de</strong> Lösungsansätze erfüllen die Anfor<strong>de</strong>rung. Der erste Lösungsansatz benötigt<br />
insgesamt O(n 2 ) Prozessoren und O(1) Lei<strong>tu</strong>ngen. Der zweite Ansatz kommt mit<br />
O(n) Prozessoren aus, benötigt dafür aber auch ein Verbindungssystem mit O(n)<br />
Lei<strong>tu</strong>ngen.<br />
Abbildung 3.2.: Je<strong>de</strong> Zelle kennt eine Zeile <strong>de</strong>r Matrix und ihre ID.<br />
Abbildung 3.3.: Die 5 Zellen <strong>de</strong>s GCA dieses Beispiels wer<strong>de</strong>n wie abgebil<strong>de</strong>t initialisiert.<br />
Beispiel Wenn <strong>de</strong>r Graph aus Abbildung 3.1 auf <strong>de</strong>n GCA abgebil<strong>de</strong>t wer<strong>de</strong>n soll,<br />
dann wird <strong>de</strong>r GCA wie in 3.3 initialisiert. Der Ablauf <strong>de</strong>s Warshall-Algorithmus, <strong>de</strong>r<br />
in 3.4 anhand <strong>de</strong>s Graphen veranschaulicht wird, geschieht auf <strong>de</strong>m GCA dann wie in<br />
Abbildung 3.5.<br />
3 Die i-te Zelle einer Zeile muss sowohl eine Verbindung zu allen an<strong>de</strong>ren Zellen ihrer eigenen Zeile als<br />
auch eine Verbindung zu <strong>de</strong>r i-ten Zelle aller an<strong>de</strong>ren Zeilen haben.<br />
31
Abbildung 3.4.: Der Ablauf <strong>de</strong>s Warshall-Algorithmus in <strong>de</strong>r Graphen-Darstellung. Der<br />
Transitknoten ist grau hinterlegt, neu hinzugefügte Kanten gestrichelt<br />
dargestellt.<br />
32
In Abbildung 3.4 entspricht <strong>de</strong>r Graph a) <strong>de</strong>m ersten Schleifendurchlauf <strong>de</strong>r Schleife<br />
aus Zeile 1. Knoten v 0 wird als Transitknoten gewählt und alle Kanten eingefügt, die<br />
durch Wege über diesen Transitknoten möglich wer<strong>de</strong>n. In diesem Fall hat nur v 2 eine<br />
Kante zu v 0 und <strong>de</strong>mentsprechend kann auch nur v 2 neue Kanten erhalten. Diese neuen<br />
Kanten von v 2 nach v 1 und v 3 wer<strong>de</strong>n in Abbildung 3.4 gestrichelt dargestellt. Nach<br />
<strong>de</strong>m gleichen Prinzip sind auch die restlichen Schritte <strong>de</strong>s Warshall-Algorithmus in <strong>de</strong>n<br />
Abbildungsteilen b) - e) dargestellt.<br />
Abbildung 3.5.: Der Ablauf <strong>de</strong>s Warshall-Algorithmus auf <strong>de</strong>m GCA. Da die Zell-ID hier<br />
nicht benötigt wird, wur<strong>de</strong>n sie in <strong>de</strong>r Abbildung zugunsten <strong>de</strong>r Übersichtlichkeit<br />
weggelassen.<br />
In Abbildung 3.5 sind die einzelnen Generationen <strong>de</strong>s GCA dargestellt. Dabei sind für<br />
je<strong>de</strong> Generation die Zellen mit Zellinhalt und Verbindung angegeben. In <strong>de</strong>r ersten Generation<br />
testen alle Zellen, ob sie eine Verbindung zur ersten Zelle haben und fragen gegebenenfalls<br />
<strong>de</strong>ren Zeile ab. In <strong>de</strong>r nächsten Generation ist dann die Zeile schon mit <strong>de</strong>r<br />
abgefragten Zeile vero<strong>de</strong>rt und es wird auf die nächste Zelle zugegriffen. Dies geschieht<br />
bis zur n-ten Generation, in <strong>de</strong>r auf die n-te Zelle lesend zugegriffen und somit <strong>de</strong>r letzte<br />
Schritt <strong>de</strong>s Warshall-Algorithmus ausgeführt wird. Am En<strong>de</strong> dieses n-ten Schritts repräsentieren<br />
die Zellen <strong>de</strong>s GCA die einzelnen Zeilen <strong>de</strong>r Matrix, welche die transitive<br />
Hülle <strong>de</strong>r Ausgangsmatrix ist.<br />
Es kann das in Kapitel 2.3.4 vorgestellte Schema angewen<strong>de</strong>t wer<strong>de</strong>n, um die Abhängigkeit<br />
<strong>de</strong>r Daten und <strong>de</strong>r Verbindungen für die nächste Generation festzustellen. Dabei<br />
33
wird die Realisierung betrachtet, die keinen Bus verwen<strong>de</strong>t. Statt<strong>de</strong>ssen wer<strong>de</strong>n Verbindungen<br />
zwischen <strong>de</strong>n Zellen aufgebaut, mit <strong>de</strong>ren Hilfe <strong>de</strong>r Zellinhalt ausgelesen wird.<br />
Es bedarf einer getrennten Betrach<strong>tu</strong>ng <strong>de</strong>r Abängigkeit für die Daten und für die Verbindungen:<br />
• Die Daten sind nicht abhängig von <strong>de</strong>r Zeit. Es kann nicht festgestellt wer<strong>de</strong>n,<br />
dass sich die Daten zu einem Zeitpunkt auf einen bestimmten Wert setzen. Eine<br />
Abhängigkeit von <strong>de</strong>r Zeit wür<strong>de</strong> z. B. bestehen, wenn die Daten <strong>de</strong>r Zelle in<br />
regelmässigen Abstän<strong>de</strong>n neu initialisiert wür<strong>de</strong>n.<br />
Die Daten einer Zelle än<strong>de</strong>rn sich auch nicht in Abhängigkeit von <strong>de</strong>m Ort, an<br />
<strong>de</strong>m die Zelle sich im GCA befin<strong>de</strong>t.<br />
Allerdings sind die Daten <strong>de</strong>r nächsten Generation abhängig von <strong>de</strong>n Daten <strong>de</strong>r<br />
momentanen Generation. Betrachtet man diese Abhängigkeit genauer, so stellt<br />
man fest, dass die Daten auch von <strong>de</strong>n Daten <strong>de</strong>r Nachbar-Zelle abhängig sind.<br />
Eine Abhängigkeit von <strong>de</strong>r Verbindung liegt nicht vor, die Daten än<strong>de</strong>rn sich nicht<br />
unter zur Hilfenahme <strong>de</strong>r Verbindung an sich, son<strong>de</strong>rn nur <strong>de</strong>r Daten auf die die<br />
Verbindung zeigt.<br />
Es ergibt sich die Verän<strong>de</strong>rungsfunktion: d’ = f(d,d*).<br />
• Da in <strong>de</strong>r i-ten Generation auf die i-te Zelle zugegriffen wird, sind die Verbindungen<br />
abhängig von <strong>de</strong>r Zeit.<br />
Die Verbindungen <strong>de</strong>r Zelle in <strong>de</strong>r nächsten Generation sind nicht abhängig von<br />
<strong>de</strong>m Ort o<strong>de</strong>r <strong>de</strong>r Verbindung <strong>de</strong>r Zelle.<br />
Eine Abhängigkeit <strong>de</strong>r Verbindung <strong>de</strong>r Zelle in <strong>de</strong>r nächsten Generation von <strong>de</strong>n<br />
Daten <strong>de</strong>r Zelle liegt in <strong>de</strong>r konkreten Simulation vor. Bei <strong>de</strong>m Warshall-Algorithmus<br />
repräsentiert eine Zelle immer einen Knoten. Man kann nun immer eine Verbindung<br />
aufbauen, egal ob von <strong>de</strong>m Knoten eine Verbindung zu <strong>de</strong>m i-ten Knoten<br />
existiert o<strong>de</strong>r nicht. In <strong>de</strong>m Falle sind die Verbindungen nur abhängig von <strong>de</strong>r<br />
Zeit. Allerdings kann man <strong>de</strong>n Verbindungsaufbau auch davon abhängig machen,<br />
ob in <strong>de</strong>m Graphen eine Verbindung zu <strong>de</strong>m i-ten Knoten existiert. In <strong>de</strong>m Falle<br />
ist dann die Verbindung <strong>de</strong>r nächsten Generation sowohl von <strong>de</strong>r Zeit als auch<br />
von <strong>de</strong>n eigenen Daten abhängig. Bei dieser Betrach<strong>tu</strong>ng wird davon ausgegangen,<br />
dass nur dann eine Verbindung aufgebaut wird, wenn auch im Ursprungsgraphen<br />
eine Verbindung existierte.<br />
Daraus folgt die Funktion: p’ = g(t,d).<br />
3.1.2. Der Floyd-Warshall-Algorithmus<br />
Die Ergebnismatrix <strong>de</strong>s Floyd-Warshall-Algorithmus erlaubt nicht nur die Einsicht, ob<br />
ein beliebiger Punkt A mit einem beliebigem Punkt B verbun<strong>de</strong>n ist, son<strong>de</strong>rn auch wie<br />
groß die Summe <strong>de</strong>r Kantenwer<strong>tu</strong>ngen <strong>de</strong>r kürzesten Verbindung zwischen ihnen ist.<br />
34
Da sowohl Warshall-Algorithmus als auch Floyd-Warshall-Algorithmus Spezialisierungen<br />
<strong>de</strong>s Kleene-Algorithmus sind, unterschei<strong>de</strong>n sie sich lediglich in <strong>de</strong>n Funktionen, die sie<br />
verwen<strong>de</strong>n, um die einzelnen Matrix-Einträge zu verknüpfen. Während <strong>de</strong>r Warshall-<br />
Algorithmus die logischen Funktionen und und o<strong>de</strong>r benutzt, verwen<strong>de</strong>t <strong>de</strong>r Floyd-<br />
Warshall-Algorithmus die Funktionen min und plus.<br />
Der Floyd-Warshall-Algorithmus wird immer dann benutzt, wenn nicht nur wichtig ist,<br />
ob es eine Verbindung gibt, son<strong>de</strong>rn es auch nötig ist, zu wissen, wie teuer diese Verbindung<br />
ist. Eine mögliche Anwendung wäre beispielsweise <strong>de</strong>r Aufbau eines Mehrprozessorsystems<br />
mit verschie<strong>de</strong>nen Verbindungen. Dann ist es einerseits wichtig zu wissen,<br />
dass kein Prozessor isoliert ist (also entwe<strong>de</strong>r keine an<strong>de</strong>ren Prozessoren ansprechen o<strong>de</strong>r<br />
von keinem an<strong>de</strong>ren Prozessor angesprochen wer<strong>de</strong>n kann). An<strong>de</strong>rerseits ist es wichtig<br />
zu wissen, welche Kommunikationszeiten zwischen <strong>de</strong>n Prozessoren benötigt wer<strong>de</strong>n.<br />
Der globale Takt ist dann min<strong>de</strong>stens auf <strong>de</strong>n höchsten Wert <strong>de</strong>r Ergebnismatrix <strong>de</strong>s<br />
Floyd-Warshall-Algorithmus zu setzen.<br />
Die Realisierung auf <strong>de</strong>m GCA gestaltet sich ebenso wie die Realisierung <strong>de</strong>s Warshall-<br />
Algorithmus: je<strong>de</strong> Zeile <strong>de</strong>r Matrix wird durch eine Zelle <strong>de</strong>s GCA repräsentiert. Allerdings<br />
muss beachtet wer<strong>de</strong>n, dass <strong>de</strong>r Floyd-Warshall-Algorithmus nicht auf einer<br />
binären Matrix arbeitet, son<strong>de</strong>rn auf einer Matrix von Integern.<br />
Eine Reduktion <strong>de</strong>s Zeitaufwands auf O(n) mittels O(n) Lei<strong>tu</strong>ngen ist hier zwar auch<br />
möglich, allerdings kann <strong>de</strong>r Zeileninhalt nicht einfach auf einen Bus gelegt wer<strong>de</strong>n.<br />
Möchte man alle Inhalte einer Zeile übertragen, so hat man die Möglichkeit, entwe<strong>de</strong>r<br />
jeweils <strong>de</strong>n i-ten Zeileneintrag komplett o<strong>de</strong>r das jeweils i-te Bit aller Zeileneinträge<br />
gleichzeitig zu übertragen. Der Aufwand mag bei bei<strong>de</strong>n Möglichkeiten gleich erscheinen,<br />
allerdings benötigt man beim Übertragen <strong>de</strong>s jeweils i-ten Elements O(n) Schritte,<br />
während man beim Übertragen <strong>de</strong>r Bits einer Zeile nur O(1) Schritte benötigt, da die<br />
Größe eines Integers nicht von <strong>de</strong>r Eingabegröße abhängig ist. Für kleine Graphen ist<br />
also die erste Lösung zu präferieren, während bei zunehmen<strong>de</strong>r Knotenanzahl die zweite<br />
Lösung ein besseres Verhalten an <strong>de</strong>n Tag legt.<br />
Auch hier besteht die Möglichkeit, insgesamt O(n 2 ) Prozessoren zu benutzen und je<strong>de</strong>n<br />
Eintrag <strong>de</strong>r Matrix durch eine Zelle darzustellen. Diese Lösung bedarf einer sinnvollen<br />
Verbindungslogik, welche zeitgleich die Verbindung <strong>de</strong>s eigenen Knotens zum Transitknoten<br />
und die Verbindung vom Transitknoten zum gewünschten Zielknoten abfragt.<br />
Bevor eine Addition stattfin<strong>de</strong>n darf, muss getestet wer<strong>de</strong>n, ob <strong>de</strong>r Verbindungswert<br />
zum Transitknoten gleich 0 ist. Ist <strong>de</strong>r Wert gleich 0, so ist <strong>de</strong>r alte Wert unverän<strong>de</strong>rt<br />
zurückzuschreiben und die Berechnung ist been<strong>de</strong>t. Ist er ungleich 0, müssen die bei<strong>de</strong>n<br />
erhaltenen Werte addiert wer<strong>de</strong>n und das Minimum zwischen <strong>de</strong>m Ergebnis aus <strong>de</strong>r<br />
Addition und <strong>de</strong>m alten Wert ist nun in <strong>de</strong>n Zellspeicher zu schreiben.<br />
Der Floyd-Warshall-Algorithmus funktioniert auf <strong>de</strong>m GCA entsprechend <strong>de</strong>m Warshall-<br />
Algorithmus, es wird lediglich die Berechnungsfunktion geän<strong>de</strong>rt. Diese geht aber nicht<br />
in das Schema aus Kapitel 2.3.4 ein, weswegen sich die selben Abhängigkeiten wie beim<br />
Warshall-Algorithmus ergeben:<br />
d’ = f(d,d*) und p’ = g(t,d).<br />
35
3.1.3. Der Algorithmus von Hirschberg et al.<br />
Auf einem Multiprozessor-System kann man entwe<strong>de</strong>r <strong>de</strong>n modifizierten Warshall verwen<strong>de</strong>n,<br />
um zusammenhängen<strong>de</strong> Komponenten zu erkennen, o<strong>de</strong>r man nutzt <strong>de</strong>n Algorithmus<br />
von Hirschberg et al., welcher bereits so mo<strong>de</strong>lliert wur<strong>de</strong>, dass er die Möglichkeiten<br />
eines Multiprozessor-Systems ausnutzt.<br />
Der Algorithmus von Hirschberg et al. ist ursprünglich in [GR98] als ein Algorithmus auf<br />
<strong>de</strong>r P-RAM vorgestellt wor<strong>de</strong>n. Bei diesem Algorithmus wird versucht, unter Ausnutzung<br />
<strong>de</strong>r nebenläufigen 4 Fähigkeiten eines Mo<strong>de</strong>lls wie <strong>de</strong>r P-RAM, die zusammenhängen<strong>de</strong>n<br />
Komponenten eines Graphen zu bestimmen. Auch hier wird <strong>de</strong>r Algorithmus zunächst<br />
schematisch im Original vorgestellt und dann wird dargestellt, wie dieser Algorithmus<br />
auf <strong>de</strong>m GCA mo<strong>de</strong>lliert wer<strong>de</strong>n kann.<br />
3.1.3.1. Ablauf auf <strong>de</strong>r P-RAM<br />
Der Algorithmus von Hirschberg et al. durchläuft insgesamt O(log(n))-mal eine Schleife.<br />
Nach je<strong>de</strong>m Schleifendurchlauf hat er eine Menge von zusammenhängen<strong>de</strong>n Komponenten.<br />
Diese Komponenten wer<strong>de</strong>n in <strong>de</strong>m n-dimensionalen Vektor C gespeichert, wobei<br />
an <strong>de</strong>r Stelle i genau dann <strong>de</strong>r Wert j steht, wenn <strong>de</strong>r Knoten i zu <strong>de</strong>r Komponente<br />
j gehört. Innerhalb einer Komponente gilt immer, dass die kleinste Nummer aller zugehörigen<br />
Knoten die Nummer <strong>de</strong>r Komponente bestimmt. So wird die Ein<strong>de</strong>utigkeit<br />
<strong>de</strong>r Benennung gewährleistet. Der Algorithmus bestimmt diese Komponenten in zwei<br />
Phasen nun folgen<strong>de</strong>rmaßen:<br />
• Die erste Phase bestimmt eine Verbindung von je<strong>de</strong>r Komponente zu <strong>de</strong>r erreichbaren<br />
Komponente mit <strong>de</strong>r niedrigsten Nummer, die in <strong>de</strong>m n-dimensionalen Vektor<br />
T gespeichert wird.<br />
Zu diesem Zweck muss erst von je<strong>de</strong>m <strong>de</strong>r Komponente angehörigen Knoten die<br />
Verbindung zu <strong>de</strong>m kleinsten Knoten, <strong>de</strong>r nicht <strong>de</strong>r Komponente angehört, gesucht<br />
wer<strong>de</strong>n. Der Wert von <strong>de</strong>ssen Komponente muss anschließend gespeichert wer<strong>de</strong>n.<br />
Danach wird von all diesen Verbindungen diejenige ausgesucht, die zu <strong>de</strong>r niedrigsten<br />
Komponente ungleich <strong>de</strong>r eigenen führt. Diese Verbindung wird gleichzeitig<br />
<strong>de</strong>m Repräsentanten <strong>de</strong>r eigenen Komponente zugeordnet.<br />
Diese Phase setzt sich aus zwei Teilschritten zusammen.<br />
• Die zweite Phase sorgt dafür, dass alle Komponenten, die eine Verbindung zueinan<strong>de</strong>r<br />
haben, zusammengelegt wer<strong>de</strong>n und alle in ihnen enthaltenen Knoten auf<br />
<strong>de</strong>n Knoten mit <strong>de</strong>r niedrigsten Nummer verweisen.<br />
4 Man spricht davon, dass zwei Befehle parallel abarbeitbar sind, wenn man die Befehle zeitgleich<br />
ausführen kann. Nebenläufig be<strong>de</strong>utet hingegen, dass es egal ist, in welcher Reihenfolge die Befehle<br />
ausgeführt wer<strong>de</strong>n. Die Befehle können also parallel abgearbeitet wer<strong>de</strong>n o<strong>de</strong>r aber in je<strong>de</strong>r<br />
Kombination sequentiell hintereina<strong>de</strong>r ohne dass es zu Konflikten kommt.<br />
36
Um diese Aufgabe zu erfüllen, muss zuerst eine Kopie <strong>de</strong>s Vektors T angelegt<br />
wer<strong>de</strong>n. Diese Kopie wird in <strong>de</strong>m Vektor B abgespeichert.<br />
Danach wird O(logn)-mal <strong>de</strong>r Schritt <strong>de</strong>r Doubling-Technik 5 angewen<strong>de</strong>t, in <strong>de</strong>r<br />
Hoffnung, somit auf <strong>de</strong>n niedrigsten Knoten <strong>de</strong>r Gesamtkomponente zu verweisen.<br />
Innerhalb <strong>de</strong>r Komponente kann es dazu kommen, dass ein Zyklus <strong>de</strong>r Länge zwei<br />
entsteht, <strong>de</strong>r dafür sorgt, dass bei <strong>de</strong>r Doubling-Technik die Komponente wie<strong>de</strong>r<br />
in zwei zerfällt. Aus diesem Grund wird an die Doubling-Technik anschließend das<br />
Minimum <strong>de</strong>s Ergebnisses <strong>de</strong>r Doubling-Technik mit <strong>de</strong>m Nachfolger <strong>de</strong>s ursprünglichen<br />
Werts gebil<strong>de</strong>t. Dieser Wert wird dann wie<strong>de</strong>r an die entsprechen<strong>de</strong> Stelle<br />
<strong>de</strong>s C-Vektors geschrieben.<br />
Somit stehen dann die neu gefun<strong>de</strong>nen Komponenten wie<strong>de</strong>r im Vektor C und <strong>de</strong>r<br />
nächste Durchlauf <strong>de</strong>s Algoritmus kann beginnen. Die zweite Phase besteht aus<br />
drei Teilschritten.<br />
Nach<strong>de</strong>m die Funktionsweise im Groben beschrieben wur<strong>de</strong>, soll nun <strong>de</strong>r gesamte Algorithmus<br />
in Pseudoco<strong>de</strong> beschrieben wer<strong>de</strong>n. Dabei wird die gleiche Terminologie wie<br />
in [GR98] benutzt. Im Pseudoco<strong>de</strong> wird eine parallele Abarbei<strong>tu</strong>ng <strong>de</strong>r Aufgabe immer<br />
durch ein ”<br />
for all ... in parallel“gefor<strong>de</strong>rt. Ansonsten ist <strong>de</strong>r Pseudoco<strong>de</strong> ähnlich<br />
<strong>de</strong>r gewohnten Lesart von C, wobei min j be<strong>de</strong>utet, dass das Minimum bezüglich j gesucht<br />
wird. Der angegebene Pseudoco<strong>de</strong> muss log(n)-mal wie<strong>de</strong>rholt wer<strong>de</strong>n, damit das<br />
korrekte Ergebnis erreicht wird.<br />
Listing 3.3: Der Hirschberg-Algorithmus auf <strong>de</strong>r P-RAM<br />
1 for a l l vertices i in parallel do<br />
T( i )
⎛<br />
B =<br />
⎜<br />
⎝<br />
0 0 0 1 0 0 0 0<br />
0 0 1 1 0 0 1 0<br />
0 1 0 0 1 1 0 0<br />
1 1 0 0 0 0 0 1<br />
0 0 1 0 0 0 0 0<br />
0 0 1 0 0 0 0 0<br />
0 1 0 0 0 0 0 0<br />
0 0 0 1 0 0 0 0<br />
⎞<br />
⎟<br />
⎠<br />
Abbildung 3.6.: Beispielgraph für <strong>de</strong>n Algorithmus von Hirschberg<br />
Im ersten Teilschritt wird von je<strong>de</strong>m Knoten ausgehend die Kante zum Nachbarn mit<br />
<strong>de</strong>r kleinsten Nummer gesucht, <strong>de</strong>r noch nicht in <strong>de</strong>r gleichen Komponente enthalten ist.<br />
Dieser Nachbar wird in <strong>de</strong>n Vektor T gespeichert, welchen man wie<strong>de</strong>r als Grundlage<br />
für einen gerichteten Graphen verwen<strong>de</strong>n kann (<strong>de</strong>r Wert j an <strong>de</strong>r Stelle i impliziert eine<br />
gerichtete Verbindung von i nach j). Interpretiert man <strong>de</strong>n Vektor T in dieser Weise, so<br />
entsteht am En<strong>de</strong> <strong>de</strong>s ersten Schritts <strong>de</strong>r Graph aus Abbildung 3.7.<br />
Im ersten Durchlauf än<strong>de</strong>rt <strong>de</strong>r zweite Teilschritt nichts am Vektor T, da bislang 8<br />
Komponenten existieren, in <strong>de</strong>nen jeweils nur ein Knoten, <strong>de</strong>r Repräsentant, enthalten<br />
ist. Ein Minimum über alle Elemente <strong>de</strong>r Komponente wird also nichts än<strong>de</strong>rn und<br />
Abbildung 3.7 repräsentiert somit auch das Ergebnis <strong>de</strong>s zweiten Teilschritts <strong>de</strong>s ersten<br />
Durchlaufs.<br />
Im dritten Teilschritt wird nunmehr eine Kopie angelegt, welche für <strong>de</strong>n fünften und<br />
letzten Teilschritt benötigt wird.<br />
Im vierten Teilschritt wird nun log(n) mal die Doubling-Technik angewen<strong>de</strong>t. Dabei setzt<br />
je<strong>de</strong>r Knoten seinen Nachfolger auf <strong>de</strong>n Nachfolger seines ursprünglichen Nachfolgers.<br />
Diese Aktion wird im Algorithmus log(n)-wie<strong>de</strong>rholt, bis schließlich als Ergebnis <strong>de</strong>r<br />
Graph aus Abbildung 3.8 entsteht. Der Vektor T wird dafür wie<strong>de</strong>r wie vorne beschrieben<br />
interpretiert.<br />
Auffällig am Ergebnis nach <strong>de</strong>m vierten Schritt ist, dass nun vier und nicht mehr, wie<br />
nach <strong>de</strong>m zweiten Schritt, zwei Komponenten vorhan<strong>de</strong>n sind. Solches Auseinan<strong>de</strong>rreißen<br />
von Komponenten ist allerdings unerwünscht, weshalb <strong>de</strong>r fünfte Schritt wie<strong>de</strong>r<br />
38
Abbildung 3.7.: Ergebnis nach <strong>de</strong>m ersten und zweiten Teilschritt <strong>de</strong>s ersten Durchlaufs.<br />
Abbildung 3.8.: Ergebnis nach <strong>de</strong>m vierten Teilschritt <strong>de</strong>s ersten Durchlaufs.<br />
dafür sorgt, dass Komponenten, die im zweiten Schritt zusammenhängend waren, auch<br />
nach <strong>de</strong>m fünften Schritt wie<strong>de</strong>r zusammenhängend sind. Da das Auseinan<strong>de</strong>rgehen einer<br />
Komponente nur durch die Doubling-Technik auftreten konnte, muss dieser Schritt<br />
im Nachhinein noch einmal kritisch nachgeprüft wer<strong>de</strong>n. Dies kann man erreichen in<strong>de</strong>m<br />
man das Minimum bil<strong>de</strong>t, da immer <strong>de</strong>r Knoten mit <strong>de</strong>r kleinsten Nummer als<br />
Repräsentant <strong>de</strong>r Komponente <strong>de</strong>finiert ist.<br />
Das Minimum wird über zwei Werte gebil<strong>de</strong>t. Der erste Wert ist <strong>de</strong>r direkte Nachfolger<br />
<strong>de</strong>s Knotens i nach <strong>de</strong>m vierten Teilschritt. Der zweite Wert berechnet sich mit Hilfe <strong>de</strong>r<br />
Kopie aus Teilschritt drei. Zunächst wird in <strong>de</strong>r Kopie nachgesehen, welchen Nachfolger<br />
<strong>de</strong>r Knoten i nach <strong>de</strong>m zweiten Teilschritt hatte. Dann wird für diesen Nachfolger <strong>de</strong>r<br />
Nachfolger nach <strong>de</strong>m vierten Teilschritt gesucht. Dieser Nachfolger schließlich ist die<br />
zweite Eingabe für das Minimum. Das Ergebnis <strong>de</strong>s Minimums ist <strong>de</strong>r neue Nachfolger<br />
<strong>de</strong>s betrachteten Knotens. Nach<strong>de</strong>m dieses Verfahren für je<strong>de</strong>n Knoten angewen<strong>de</strong>t<br />
wur<strong>de</strong>, erhält man <strong>de</strong>n Graphen aus 3.9.<br />
Die zwei neu entstan<strong>de</strong>nen Komponenten versucht man im zweiten Durchlauf zusammenzufassen.<br />
Gäbe es in diesem Durchlauf noch mehr als zwei Komponenten, so wäre<br />
mit einer Verringerung <strong>de</strong>r Komponentenanzahl von min<strong>de</strong>stens <strong>de</strong>r Hälfte zu rechnen,<br />
außer die Komponenten hängen nicht zusammen 6 .<br />
6 Da nicht zusammenhängen<strong>de</strong> Komponenten nicht zusammengefasst wer<strong>de</strong>n können, kommt es bei<br />
einem nicht zusammenhängen<strong>de</strong>n Graphen dazu, dass sich die Anzahl <strong>de</strong>r zusammenhängen<strong>de</strong>n<br />
Komponenten in einem Durchlauf even<strong>tu</strong>ell nicht halbiert.<br />
39
Abbildung 3.9.: Ergebnis nach <strong>de</strong>m fünften Teilschritt <strong>de</strong>s ersten Durchlaufs.<br />
Abbildung 3.10.: Ergebnis nach <strong>de</strong>m ersten Teilschritt <strong>de</strong>s zweiten Durchlaufs.<br />
Im ersten Teilschritt <strong>de</strong>s zweiten Durchlaufs wird wie<strong>de</strong>r in je<strong>de</strong>m Knoten gesucht, ob<br />
eine Verbindung zu einem Knoten aus einer an<strong>de</strong>ren Komponente möglich ist und falls<br />
ja wird die günstigste genommen. In diesem Beispiel ist es nur <strong>de</strong>n Knoten 4 und 2<br />
möglich, eine Verbindung zu einer an<strong>de</strong>ren Komponente herzustellen. Das Ergebnis am<br />
En<strong>de</strong> dieses Teilschritts ist in Abbildung 3.10 zu sehen.<br />
Im zweiten Teilschritt wird nun für je<strong>de</strong>n Knoten i überprüft, ob es einen Knoten j gibt,<br />
welcher am Anfang <strong>de</strong>s Durchlaufs auf <strong>de</strong>n Knoten i gezeigt hat (C(j)=i) und nun nicht<br />
mehr auf i zeigt (T(j)!=i). Existiert min<strong>de</strong>stens ein solcher Knoten, so wird <strong>de</strong>r kleinste<br />
Knoten j gewählt und <strong>de</strong>r Nachfolger von i auf <strong>de</strong>n Nachfolger von j gesetzt (T(i)=T(j)).<br />
Solch eine Anweisung wird nur für die Knoten ausgeführt, die vorher Repräsentanten<br />
ihrer Gruppe waren, da nur solche noch am Anfang im Vektor C auftauchen. In diesem<br />
Beispiel trifft das auf <strong>de</strong>n Knoten 1 zu; dieser war vorher Repräsentant seiner Komponente<br />
und nun gibt es einen Knoten (Knoten 4), welcher nicht mehr auf <strong>de</strong>n Knoten 1<br />
zeigt. Also zeigt Knoten 1 nun auch nicht mehr auf sich selber, son<strong>de</strong>rn auf Knoten 2,<br />
<strong>de</strong>n Nachfolger von Knoten 4. Der einzige an<strong>de</strong>re Knoten, <strong>de</strong>r für eine Verän<strong>de</strong>rung in<br />
Frage käme, ist Knoten 2. Da dieser aber in seiner Komponente <strong>de</strong>r Einzige ist, welcher<br />
eine Verbindung in die an<strong>de</strong>re Komponente fin<strong>de</strong>t, muss <strong>de</strong>r Nachfolger nicht mehr umgesetzt<br />
wer<strong>de</strong>n. Ist ein Knoten nicht <strong>de</strong>r Repräsentant seiner Komponente, so wird er<br />
wie<strong>de</strong>r auf seinen Ausgangswert gesetzt (T(i)=C(i)), da die Verbindung zu <strong>de</strong>r an<strong>de</strong>ren<br />
Komponente nur einfach benötigt wird und diese vom Repräsentanten ausgeht. Das Ergebnis<br />
nach Teilschritt zwei <strong>de</strong>s zweiten Durchlaufs sieht dann wie in Abbildung 3.11<br />
40
Abbildung 3.11.: Ergebnis nach <strong>de</strong>m zweiten Teilschritt <strong>de</strong>s zweiten Durchlaufs. Um Verwirrung<br />
aufgrund von Kantenüberschneidungen zu vermei<strong>de</strong>n, wur<strong>de</strong>n<br />
die Positionen <strong>de</strong>r Knoten teilweise variiert.<br />
aus.<br />
Teilschritt drei legt wie<strong>de</strong>r eine Kopie <strong>de</strong>s Vektors T für <strong>de</strong>n letzten Schritt an. Im vierten<br />
Teilschritt wird nun wie<strong>de</strong>r die Doubling-Technik angewen<strong>de</strong>t, was zu Abbildung 3.12<br />
führt.<br />
Abbildung 3.12.: Ergebnis nach <strong>de</strong>m vierten Teilschritt <strong>de</strong>s zweiten Durchlaufs.<br />
Auch hier sind wie<strong>de</strong>r mehrere Komponenten entstan<strong>de</strong>n, obwohl eigentlich die Komponenten<br />
zusammengefasst wer<strong>de</strong>n sollten. Das liegt daran, dass im Graphen ein Zyklus<br />
enthalten war und somit je<strong>de</strong>r <strong>de</strong>r beteiligten Knoten zwangsläufig früher o<strong>de</strong>r später auf<br />
sich selber zeigt. Dieser Missstand wird im fünften Teilschritt bereinigt. Da <strong>de</strong>r Knoten<br />
1 die im gesamten Graphen kleinste Nummer hat, wird sich sein Wert bei <strong>de</strong>r Minimumbildung<br />
nicht än<strong>de</strong>rn, gleiches gilt für alle an<strong>de</strong>ren Knoten, welche auf Knoten 1 zeigen.<br />
Knoten 2 zeigt nach <strong>de</strong>m vierten Teilschritt auf sich selber, aber in <strong>de</strong>r Kopie (B) zeigt<br />
er auf Knoten 1, welcher nach <strong>de</strong>m vierten Teilschritt auch auf sich selber zeigt. Das<br />
Minimum von 2 und 1 ist 1, Knoten 2 zeigt also ab jetzt auf Knoten 1. Das gleiche<br />
Prinzip wird nun noch auf Knoten 4 und 8 angewen<strong>de</strong>t. Bei<strong>de</strong> zeigen in <strong>de</strong>r Kopie auf 1<br />
und wer<strong>de</strong>n somit im fünften Teilschritt wie<strong>de</strong>r auf <strong>de</strong>n Knoten 1 umgebogen.<br />
41
Da nun alle Knoten auf Knoten 1 zeigen, kann man erkennen, dass es eine große zusammenhängen<strong>de</strong><br />
Komponente im Graphen gibt, mit an<strong>de</strong>rn Worten, <strong>de</strong>r Graph ist<br />
zusammenhängend. Der Algorithmus wird die fünf Teilschritte noch einmal durchlaufen,<br />
da log 2 (8) = 3. Es wird sich jedoch nichts mehr än<strong>de</strong>rn, da keine Verbindung zu<br />
einer an<strong>de</strong>ren Komponente mehr gefun<strong>de</strong>n wer<strong>de</strong>n kann. Das Beispiel kann also hier als<br />
abgeschlossen betrachtet wer<strong>de</strong>n. Das En<strong>de</strong>rgebnis wird in Abbildung 3.13 graphisch<br />
dargestellt. Bei einem nicht zusammenhängen<strong>de</strong>n Graphen wür<strong>de</strong>n nach Terminierung<br />
<strong>de</strong>s Algorithmus im Vektor C min<strong>de</strong>stens zwei unterschiedliche Repräsentanten stehen.<br />
Man kann direkt aus <strong>de</strong>m Vektor auslesen, wie groß die Komponenten sind und welche<br />
Knoten zu ihnen gehören.<br />
Abbildung 3.13.: Ergebnis nach <strong>de</strong>m fünften Schritt <strong>de</strong>s zweiten Durchlaufs. Um Verwirrung<br />
aufgrund von Kantenüberschneidungen zu vermei<strong>de</strong>n, wur<strong>de</strong>n<br />
auch hier die Position <strong>de</strong>r Knoten teilweise variiert.<br />
3.1.3.2. Komplexitätsbetrach<strong>tu</strong>ng auf <strong>de</strong>r P-RAM<br />
Der Algorithmus benötigt, so wie er vorgestellt wur<strong>de</strong>, eine Laufzeit von<br />
O(log 2 (n)) bei <strong>de</strong>r Benutzung von O( n2 ) Prozessoren. In diesem Abschnitt soll ver<strong>de</strong>utlicht<br />
wer<strong>de</strong>n, dass diese Abschätzung gerechtfertigt<br />
log(n)<br />
ist.<br />
Da die fünf Teilschritte (im Folgen<strong>de</strong>n nur noch Schritte genannt) <strong>de</strong>s Algorithmus<br />
log(n)-mal wie<strong>de</strong>rholt wer<strong>de</strong>n, darf je<strong>de</strong>r Schritt nicht mehr als eine Laufzeit von<br />
O(log(n)) haben, damit die Gesamtlaufzeit von O(log 2 (n)) erfüllt ist. Die Aufgabe dieses<br />
Abschnitts ist es also, für je<strong>de</strong>n Schritt darzulegen, dass er wirklich in O(log(n)) Zeit<br />
ablaufen kann.<br />
Für <strong>de</strong>n ersten Schritt ist nicht offensichtlich, dass er in O(log(n)) ausführbar ist. Immerhin<br />
müssen potentiell n Werte bestimmt und daraus das Minimum gebil<strong>de</strong>t wer<strong>de</strong>n.<br />
Um diesen Schritt in <strong>de</strong>r gewünschten Zeit ausführen zu können, muss man geschickt<br />
zusätzliche Prozessoren einsetzen. In [GR98] wird <strong>de</strong>r erste Schritt in drei Teilschritte<br />
42
1(a) bis 1(c) aufgeteilt und für je<strong>de</strong>n <strong>de</strong>r Teilschritte die Komplexität bestimmt. Die<br />
Teilschritte gestalten sich wie folgt (∞ aus <strong>de</strong>m Original wur<strong>de</strong> hier durch 2n ersetzt):<br />
Listing 3.4: Weitere Unterteilung <strong>de</strong>s ersten Schritts <strong>de</strong>s Hirschberg-Algorithmus<br />
1 1(a) : for a l l i , j , 1
Abbildung 3.14.: Eine Möglichkeit, aus n Werten in log(n) Schritten das Minimum zu<br />
fin<strong>de</strong>n. Die Prozessoren vergleichen mit ihrem Nachbarn und speichern<br />
das Minimum ab. Danach wird <strong>de</strong>r Nachbar umgesetzt. Das Minimum<br />
steht nach <strong>de</strong>r Ausführung immer an erster Stelle.<br />
Wen<strong>de</strong>t man statt <strong>de</strong>r Doubling-Technik die modifizierte Balanced-Binary-Tree-Technik 7<br />
an, dann reichen<br />
n Prozessoren pro Spalte und dabei wird eine Laufzeit von O(log(n))<br />
log(n)<br />
garantiert. Insgesamt benötigt <strong>de</strong>r erste Schritt dann eine Laufzeit von O(log(n)) und<br />
O( n2 ) Prozessoren. Im Folgen<strong>de</strong>n soll zuerst die Balanced-Binary-Tree-Technik vorgestellt<br />
wer<strong>de</strong>n und dann wird dargestellt, wie man die Anzahl <strong>de</strong>r Prozessoren dahinge-<br />
log(n)<br />
n<br />
hend verän<strong>de</strong>rn kann, dass man mit O( ) Prozessoren die gleiche Laufzeitkomplexität<br />
log(n)<br />
erreicht.<br />
Bei <strong>de</strong>r Balanced-Binary-Tree-Technik [Abbildung 3.15] hält je<strong>de</strong> Zelle zwei Werte <strong>de</strong>r<br />
Matrixspalte aus 1(a) und bil<strong>de</strong>t hieraus das Minimum.<br />
Es berechnen also n−1 Prozessoren parallel das Minimum ihrer Werte. Je<strong>de</strong> Zelle hat in<br />
diesem Mo<strong>de</strong>ll eine Vaterzelle und je<strong>de</strong> Vaterzelle hat zwei Kindzellen, was dazu führt,<br />
dass die Balanced-Binary-Tree-Technik immer n = 2 m Werte erwartet (ist dies nicht<br />
<strong>de</strong>r Fall, müssen Dummy-Einträge ergänzt wer<strong>de</strong>n, bis die Bedingung erfüllt ist). Die<br />
Vaterzelle bestimmt nun das Minimum <strong>de</strong>r Kindzellen und liefert diesen Wert an seine<br />
Vaterzelle weiter. Insgesamt halbiert sich die Anzahl <strong>de</strong>r Zellen auf je<strong>de</strong>r S<strong>tu</strong>fe, bis in<br />
<strong>de</strong>r Spitze nur noch eine Zelle existiert, die Wurzel. Nach<strong>de</strong>m diese Wurzelzelle ihrerseits<br />
das Minimum gebil<strong>de</strong>t hat, kann <strong>de</strong>r korrekte Wert aus <strong>de</strong>r Wurzel ausgelesen wer<strong>de</strong>n.<br />
Die Laufzeitkomplexität entspricht nun <strong>de</strong>r maximalen Weglänge von <strong>de</strong>r Wurzel zu<br />
einem Blatt, was in einem vollständigen Binärbaum log(n) ist. Wie bei <strong>de</strong>r Doubling-<br />
Technik wer<strong>de</strong>n hier jedoch O(n) Prozessoren o<strong>de</strong>r Zellen pro Spalte benötigt, was zu<br />
einer Gesamtzahl von O(n 2 ) benötigten Zellen führt.<br />
7 Ein Beispiel für die Balanced-Binary-Tree-Technik wird gegeben. Eine genaue Beschreibung fin<strong>de</strong>t<br />
sich im Anhang.<br />
44
Abbildung 3.15.: Die Balanced-Binary-Tree Technik mit <strong>de</strong>r maximalen Anzahl <strong>de</strong>r Prozessoren.<br />
Bei <strong>de</strong>r weniger prozessoraufwendigen Variante wer<strong>de</strong>n die<br />
Aufgaben <strong>de</strong>r Prozessoren <strong>de</strong>r oberen Schichten noch von <strong>de</strong>n Prozessoren<br />
<strong>de</strong>r untersten Schicht mit erledigt.<br />
Die Anzahl <strong>de</strong>r Zellen lässt sich leicht minimieren, in<strong>de</strong>m man Zellen erlaubt, ihre eigene<br />
Vaterzelle zu sein um somit mehrfach benutzt zu wer<strong>de</strong>n. Durch diese Maßnahme lässt<br />
sich die Anzahl <strong>de</strong>r benötigten Zellen zwar auf n reduzieren, jedoch entspricht dies in <strong>de</strong>r<br />
2<br />
O-Notation immer noch einer Anzahl von O(n) Prozessoren pro Spalte. Um die Anzahl<br />
n<br />
<strong>de</strong>r Prozessoren also auf O( ) zu reduzieren, muss man also die Anzahl <strong>de</strong>r in <strong>de</strong>r<br />
log(n)<br />
untersten Reihe benötigten Prozessoren minimieren.<br />
Um die Anzahl <strong>de</strong>r Prozessoren nun weiter zu minimieren, teilt man die n Werte, aus<br />
<strong>de</strong>nen das Minimum gesucht wer<strong>de</strong>n soll, in p Gruppen. Je<strong>de</strong> dieser p Gruppen hat ⌊ n⌋<br />
p<br />
Elemente, außer einer, welche n−(p−1)⌊ n ⌋ Elemente hat. Es wer<strong>de</strong>n nun p Prozessoren<br />
p<br />
gebraucht, welche jeweils in einer Gruppe sequentiell das Minimum suchen. Da je<strong>de</strong><br />
Gruppe maximal ⌊ n⌋ Elemente hat, wer<strong>de</strong>n maximal p ⌊n ⌋ -1 Schritte gebraucht, um<br />
p<br />
n<br />
das Minimum <strong>de</strong>r Gruppe zu bestimmen. Setzt man p auf , so wer<strong>de</strong>n O( n<br />
)<br />
log(n) log(n)<br />
Prozessoren für die Gesamtlösung benötigt und die Laufzeitkomplexität ist weiterhin<br />
O(log(n)).<br />
Um die Laufzeitkomplexität zu ver<strong>de</strong>utlichen, wer<strong>de</strong>n die Arbeitsschritte noch einmal<br />
aufgeführt: Es wer<strong>de</strong>n insgesamt log(n)−1 Minimumbestimmungen aus zwei Elementen<br />
benötigt (<strong>de</strong>r obere Teil <strong>de</strong>s Baums), die in O(1) möglich sind und eine Minimumbestimmung<br />
aus einer Gruppe mit ⌈ n ⌉ Elementen. Dieses Minimum <strong>de</strong>r Gruppe benötigt<br />
p<br />
⌈ n n<br />
⌉ − 1 Schritte, wobei p als gewählt wur<strong>de</strong>. Die Gruppenminimumsbestimmung<br />
p log(n)<br />
benötigt <strong>de</strong>mnach log(n) − 1 Schritte, was insgesamt 2log(n) − 2 Schritte ergibt, also<br />
O(log(n)). Es wur<strong>de</strong> also eine Metho<strong>de</strong> gefun<strong>de</strong>n, sowohl die Zeitschranke als auch die<br />
gewünschte Schranke <strong>de</strong>r Prozessoren einzuhalten.<br />
Der zweite Schritt kann genauso wie <strong>de</strong>r erste Schritt in drei Teilschritte zerlegt wer<strong>de</strong>n,<br />
die sich dann ähnlich wie die drei Teilschritte <strong>de</strong>s ersten Schritts in O(log(n)) Laufzeit<br />
und mit O( n2 ) Prozessoren realisiern lassen.<br />
log(n)<br />
Schritt drei ist offensichtlich in O(1) realisierbar. Das Kopieren von n Werten kann mit n<br />
45
Prozessoren in zwei Schritten erledigt wer<strong>de</strong>n (ein Schritt <strong>de</strong>n Wert lesen und ein Schritt<br />
<strong>de</strong>n Wert in die neue Variable schreiben).<br />
Die Zuweisung <strong>de</strong>s vierten Schritts ist mit n Prozessoren in O(1) möglich, allerdings<br />
muss die Anweisung log(n) mal wie<strong>de</strong>rholt wer<strong>de</strong>n, was sich auch mit mehr Prozessoren<br />
nicht vermei<strong>de</strong>n lässt. Der Zeitaufwand <strong>de</strong>s vierten Schritts ist also O(log(n)) und kann<br />
nicht optimiert wer<strong>de</strong>n. Das be<strong>de</strong>utet, dass auch die Laufzeit <strong>de</strong>s Gesamtalgorithmus<br />
nicht mehr verbessert wer<strong>de</strong>n kann.<br />
Der fünfte Schritt ist wie<strong>de</strong>r mit n Prozessoren in O(1) realisierbar.<br />
Da alle Schritte in maximal O(log(n)) ausführbar sind und <strong>de</strong>r vierte Schritt minimal<br />
O(log(n)) Schritte benötigt, ist anschaulich dargelegt, weshalb <strong>de</strong>r Algorithmus eine<br />
Laufzeitkomplexität von O(log(n) 2 ) hat. Die Anzahl <strong>de</strong>r Prozessoren lässt sich noch auf<br />
O( n2<br />
log 2 n)<br />
) reduzieren, wie in [CLC82] dargestellt ist.<br />
3.1.3.3. Mo<strong>de</strong>llierung auf <strong>de</strong>m GCA<br />
Da <strong>de</strong>r Algorithmus von Hirschberg schon so konstruiert ist, dass er die parallelen<br />
Möglichkeiten <strong>de</strong>r P-RAM ausnutzt, ist es nicht mehr nötig, zu überprüfen, an welchen<br />
Stellen er sich parallelisieren lässt. Die eigentliche Aufgabe bei diesem Algorithmus<br />
liegt nunmehr darin, zu überprüfen, ob alle parallelen Möglichkeiten <strong>de</strong>r P-RAM, die hier<br />
genutzt wer<strong>de</strong>n, sich auch auf <strong>de</strong>m GCA nutzen lassen. Allerdings wird hier zunächst<br />
einmal ein Ansatz <strong>de</strong>r Mo<strong>de</strong>llierung verfolgt, <strong>de</strong>r verständlich ist, auch wenn dadurch die<br />
Laufzeitkomplexität schlechter wird. Nach<strong>de</strong>m die Mo<strong>de</strong>llierung veranschaulicht wur<strong>de</strong>,<br />
wird darauf eingegangen, wie die Laufzeit optimiert wer<strong>de</strong>n kann.<br />
Auf die erste Abweichung trifft man bei <strong>de</strong>n Vektoren T,B und C. Diese wer<strong>de</strong>n im Algorithmus<br />
von Hirschberg als globale Variablen genutzt, auf die alle Prozessoren sowohl<br />
lesend als auch schreibend zugreifen dürfen. Da <strong>de</strong>r GCA über keinen globalen Speicher<br />
verfügt und auch auf die Zellinhalte frem<strong>de</strong>r Zellen nicht schreibend zugegriffen wer<strong>de</strong>n<br />
darf, ist das ein Problem.<br />
Diese Problematik lässt sich mit <strong>de</strong>m GCA jedoch elegant umgehen, in<strong>de</strong>m man die<br />
Vektoren auf die Prozessoren aufteilt. Mit diesem Lösungsansatz hält dann je<strong>de</strong> Zelle i<br />
die Werte T[i],B[i] und C[i]. Da je<strong>de</strong> Zelle i nur auf <strong>de</strong>n i-ten Wert schreibend zugreift,<br />
ist damit bereits das Problem <strong>de</strong>s Schreibens auf Zellinhalte frem<strong>de</strong>r Zellen gelöst.<br />
Ein Nachteil dieses Lösungansatzes ist allerdings, dass <strong>de</strong>r Kommunikationsaufwand<br />
recht hoch wird. Allein im vierten Schritt ist es sehr wahrscheinlich, dass auf <strong>de</strong>n Zellinhalt<br />
an<strong>de</strong>rer Zellen zugegriffen wer<strong>de</strong>n muss. Der Kommunikationsaufwand ist dabei<br />
weniger das Problem, als vielmehr die Tatsache, dass viele Verbindungen am Anfang<br />
hergestellt wer<strong>de</strong>n müssen, die im weiteren Verlauf even<strong>tu</strong>ell ungenutzt bleiben.<br />
Um auch diese Problematik zu umgehen, wird eine Zelle als Speicherzelle ausgezeichnet,<br />
auf die alle Zellen lesend zugreifen. Damit diese Zelle aber immer die korrekten<br />
Variableninhalte bereit hält, muss sie am En<strong>de</strong> je<strong>de</strong>s Schritts bei je<strong>de</strong>r Zelle <strong>de</strong>n ak<strong>tu</strong>ellen<br />
Stand <strong>de</strong>r entsprechen<strong>de</strong>n Variable abfragen. Das heißt, die Speicherzelle fragt nach<br />
46
Abbildung 3.16.: Schematischer Aufbau <strong>de</strong>s GCA, welcher <strong>de</strong>n Hirschberg-Algorithmus<br />
bearbeitet. Es existieren sowohl von <strong>de</strong>r Speicherzelle“ zu je<strong>de</strong>r Rechenzelle“<br />
Verbindungen als auch von je<strong>de</strong>r Rechenzelle“ zu <strong>de</strong>r<br />
”<br />
” ”<br />
” Speicherzelle“. Je<strong>de</strong> Rechenzelle“ beinhaltet neben <strong>de</strong>n Variablen T,<br />
”<br />
B und C auch noch eine Zeile <strong>de</strong>r Matrix.<br />
Schritt eins bei allen Zellen an, welchen Wert sie in ihrer Variable T halten (da nur T<br />
geän<strong>de</strong>rt wur<strong>de</strong>, ist es nicht nötig, die an<strong>de</strong>ren Variablen abzufragen). Die Antwort <strong>de</strong>r<br />
Zelle i wird dann an die i-te Stelle <strong>de</strong>s Vektors T <strong>de</strong>r Speicherzelle geschrieben. In je<strong>de</strong>m<br />
Schritt <strong>de</strong>s Algorithmus sind also k+1 Takte nötig; k Takte um die Aufgabe <strong>de</strong>s Schritts<br />
zu lösen und ein Takt, um die Variablen in die Speicherzelle zu übertragen. Zusätzlich<br />
muss je<strong>de</strong> Zelle, außer <strong>de</strong>r Speicherzelle, noch eine Zeile <strong>de</strong>r Matrix halten, so dass Zelle<br />
i die i-te Zeile <strong>de</strong>r Matrix bearbeitet. Der Aufbau wird in Abbildung 3.16 noch einmal<br />
veranschaulicht.<br />
Nach<strong>de</strong>m <strong>de</strong>r Aufbau <strong>de</strong>r Zellen festgelegt wur<strong>de</strong>, muss überlegt wer<strong>de</strong>n, wie die einzelnen<br />
Schritte <strong>de</strong>s Algorithmus damit abgearbeitet wer<strong>de</strong>n können. Hier wird o. B. d.<br />
A. davon ausgegangen, dass es kein Problem ist, <strong>de</strong>n Ablauf log(n)-mal zu wie<strong>de</strong>rholen,<br />
d.h. wenn es gelingt, die einzelnen Schritte auf <strong>de</strong>m GCA lauffähig zu mo<strong>de</strong>llieren, wird<br />
<strong>de</strong>r gesamte Algorithmus auf <strong>de</strong>m GCA lauffähig sein.<br />
• Erster Schritt: Im ersten Schritt muss für je<strong>de</strong> Zelle jeweils <strong>de</strong>r Nachbar mit<br />
<strong>de</strong>m kleinsten In<strong>de</strong>x gesucht wer<strong>de</strong>n. Dies geschieht einfach, in<strong>de</strong>m je<strong>de</strong> Zelle die<br />
Matrixzeile, die sie beinhaltet, von vorne durchläuft und bei <strong>de</strong>r ersten gefun<strong>de</strong>nen<br />
Eins stoppt. Nun muss noch gesucht wer<strong>de</strong>n, ob dieser Nachbar j gewählt wer<strong>de</strong>n<br />
darf. Dazu wird eine Anfrage an die Speicherzelle gestellt, wobei <strong>de</strong>r Eintrag C(j)<br />
angefragt wird. Daraufhin testet die Zelle, ob <strong>de</strong>r zurückgelieferte Wert gleich <strong>de</strong>m<br />
Wert in <strong>de</strong>r eigenen C Variable ist. Ist dies <strong>de</strong>r Fall, so muss die Zeile so lange<br />
weiter durchlaufen wer<strong>de</strong>n, bis ein j gefun<strong>de</strong>n wird, das bei<strong>de</strong> Bedingungen erfüllt.<br />
Kommt die Zelle an das En<strong>de</strong> <strong>de</strong>r Zeile ohne ein passen<strong>de</strong>s j gefun<strong>de</strong>n zu haben,<br />
wird <strong>de</strong>r Wert <strong>de</strong>r C Variable in T gespeichert.<br />
47
Da alle Zellen parallel arbeiten, ist <strong>de</strong>r erste Schritt hier eigentlich been<strong>de</strong>t, allerdings<br />
muss in dieser Mo<strong>de</strong>llierung noch dafür gesorgt wer<strong>de</strong>n, dass die Speicherzelle<br />
wie<strong>de</strong>r ak<strong>tu</strong>alisiert wird. Um dieses Ziel zu erreichen fragt die Speicherzelle nun<br />
parallel bei allen Zellen an und speichert <strong>de</strong>ren Wert T in ihrem Vektor T ab.<br />
• Zweiter Schritt: Im zweiten Schritt sind immer die Paare T(j) und C(j) interessant.<br />
Natürlich kann man je<strong>de</strong> Zelle bei <strong>de</strong>r Speicherzelle nachfragen lassen,<br />
allerdings wollen ja alle Zellen alle T(j) und C(j) wissen, weshalb es geschickter ist,<br />
die Speicherzelle die Koordination übernehmen zu lassen. Innerhalb von n Takten<br />
stellt sie jeweils das Paar (T(j), C(j)) bereit und je<strong>de</strong> Zelle, die noch auf <strong>de</strong>r Suche<br />
nach ihrem Wert ist, kann sich dieses Paar abholen. Die Zellen selbst übernehmen<br />
dann intern die Überprüfung, ob das Tupel die Bedingung erfüllt o<strong>de</strong>r nicht.<br />
Da nur T(j) abgespeichert wer<strong>de</strong>n muss, kann dieses Verfahren noch dahingehend<br />
optimiert wer<strong>de</strong>n, dass die Speicherzelle nur solche Paare bereitstellt, die ungleich<br />
sind. Allerdings wird dann ein Abbruch<strong>tu</strong>pel benötigt, damit die Zellen wissen,<br />
dass sie ihren Wert C in <strong>de</strong>r Variablen T speichern sollen.<br />
Anschließend muss wie<strong>de</strong>r die Speicherzelle von allen Zellen die T-Werte abfragen<br />
und an <strong>de</strong>r korrekten Stelle ihres Vektors T speichern.<br />
• Dritter Schritt: Da je<strong>de</strong> Zelle i ihren ak<strong>tu</strong>ellen Wert T(i) kennt, kann das Kopieren<br />
<strong>de</strong>s Vektors T in <strong>de</strong>r Speicherzelle und <strong>de</strong>r Werte T(i) in <strong>de</strong>n Zellen gleichzeitig<br />
geschehen.<br />
• Vierter Schritt: Im vierten Schritt wird log(n)-mal <strong>de</strong>r folgen<strong>de</strong> Ablauf durchgeführt:<br />
Je<strong>de</strong> Zelle fragt bei <strong>de</strong>r Speicherzelle <strong>de</strong>n Wert an, <strong>de</strong>r im Vektor T an <strong>de</strong>r Stelle<br />
T(i) steht (<strong>de</strong>n Wert T(i) kennt die Zelle ja bereits, da er in ihrer eigenen Variable<br />
T steht). Die Antwort <strong>de</strong>r Speicherzelle wird in <strong>de</strong>r Variablen T <strong>de</strong>r anfragen<strong>de</strong>n<br />
Zelle gespeichert.<br />
Nachfolgend muss wie<strong>de</strong>r die Speicherzelle ihren Vektor T ak<strong>tu</strong>alisieren und erst<br />
dann darf die Schleife erneut durchlaufen wer<strong>de</strong>n.<br />
• Fünfter Schritt: In diesem Schritt muss je<strong>de</strong> Zelle genau einmal bei <strong>de</strong>r Speicherzelle<br />
anfragen. Gefragt wird in diesem Fall nach <strong>de</strong>m Wert <strong>de</strong>s Vektors B an<br />
<strong>de</strong>r Stelle T(i). Daraufhin bil<strong>de</strong>t je<strong>de</strong> Zelle für sich das Minimum und speichert<br />
<strong>de</strong>n Wert in C. Nach<strong>de</strong>m je<strong>de</strong> Zelle mit ihrer Berechnung fertig ist, fragt die Speicherzelle<br />
wie<strong>de</strong>r alle Werte C <strong>de</strong>r Zellen ab und ak<strong>tu</strong>alisiert so ihren Vektor C.<br />
Die Simulation auf <strong>de</strong>m GCA ist also möglich. Aufgrund <strong>de</strong>r Speicherzelle hat man zwar<br />
mehr Ak<strong>tu</strong>alisierungsaufwand, allerdings müssen die Zellen nicht mehr untereinan<strong>de</strong>r<br />
kommunizieren. Dadurch können viele Verbindungen eingespart wer<strong>de</strong>n. Diese Mo<strong>de</strong>llierung<br />
ist nicht laufzeitoptimal. Im nächsten Abschnitt wer<strong>de</strong>n Möglichkeiten untersucht,<br />
die Speicherzelle effizient zu mo<strong>de</strong>llieren und die Laufzeitkomplexität zu verbessern.<br />
48
Abbildung 3.17.: Erster Ansatz zur Realisierung <strong>de</strong>r Speicherzelle. Je<strong>de</strong> Zelle hat n Verbindungen<br />
zur Speicherzelle und die Speicherzelle hat eine Verbindung<br />
zu je<strong>de</strong>r Zelle.<br />
3.1.3.4. Komplexitätsbetrach<strong>tu</strong>ng und Verbesserungen auf <strong>de</strong>m GCA<br />
Gegenüber <strong>de</strong>r Mo<strong>de</strong>llierung auf <strong>de</strong>r P-RAM sind bei <strong>de</strong>r vorgestellten Mo<strong>de</strong>llierungsvariante<br />
erhebliche Geschwindigkeitseinbußen hinzunehmen. Während <strong>de</strong>r Originalalgorithmus<br />
mit O(log(n) 2 ) auskommt, benötigt schon <strong>de</strong>r erste Schritt in <strong>de</strong>r eben erläuterten<br />
Realisierung im worst-case O(n) Schritte. Eine Laufzeitkomplexität besser als<br />
O(nlog(n)) ist hiermit also nicht zu realisieren. Dennoch wird zunächst auf die Realisierungsmöglichkeiten<br />
<strong>de</strong>r Speicherzelle eingegangen, bevor erläutert wird, wie die Laufzeitkomplexität<br />
verringert wer<strong>de</strong>n kann.<br />
3.1.3.4.1. Speicherzellenmo<strong>de</strong>llierung Die Grün<strong>de</strong>, die für die Einführung einer Speicherzelle<br />
in <strong>de</strong>r Mo<strong>de</strong>llierung sprachen, waren einerseits die bessere Verständlichkeit und<br />
an<strong>de</strong>rerseits die Reduktion <strong>de</strong>r benötigten Lei<strong>tu</strong>ngen und ein geringerer Kommunikationsaufwand.<br />
Ohne Speicherzelle muss je<strong>de</strong> Zelle auf je<strong>de</strong> an<strong>de</strong>re Zelle lesend zugreifen<br />
können, d.h. bei n Zellen wer<strong>de</strong>n O(n 2 ) Lei<strong>tu</strong>ngen benötigt, um eine korrekte Kommunikation<br />
zu ermöglichen.<br />
Ein erster Ansatz ist es, <strong>de</strong>r Speicherzelle n Ports zur Verfügung zu stellen, an <strong>de</strong>nen sie<br />
dann immer die passen<strong>de</strong> Variable anlegt [Abbildung 3.17]. Im ersten Schritt wür<strong>de</strong> beispielsweise<br />
an <strong>de</strong>n Port i <strong>de</strong>r Wert C(i) angelegt. Damit die Zellen sich <strong>de</strong>n gewünschten<br />
Wert abholen können, muss je<strong>de</strong> Zelle über eine Verbindung zu je<strong>de</strong>m einzelnen Port <strong>de</strong>r<br />
Speicherzelle verfügen. Da je<strong>de</strong> Zelle einen Teil <strong>de</strong>s ak<strong>tu</strong>ellen Werts <strong>de</strong>r gera<strong>de</strong> verän<strong>de</strong>rten<br />
Variable hält, muss die Speicherzelle auch eine Verbindung zu je<strong>de</strong>r Zelle haben.<br />
Insgesamt benötigt man also n 2 + n Verbindungen (n Verbindungen pro Zelle zuzüglich<br />
49
<strong>de</strong>n n Verbindungen <strong>de</strong>r Speicherzelle). Ein Vorteil ist, dass die Kommunikation in zwei<br />
Takten (erster Takt: Speicherzelle stellt <strong>de</strong>n Wert bereit, zweiter Takt: Zelle liest <strong>de</strong>n<br />
Wert) abläuft und gleich alle interessanten Variableninhalte abgefragt wer<strong>de</strong>n können.<br />
Diesen ersten Ansatz kann man noch überschaubarer gestalten, in<strong>de</strong>m man einen Bus<br />
einführt, welcher die Variablen bereitstellt [Abbildung 3.18].<br />
Abbildung 3.18.: Modifizierter Ansatz <strong>de</strong>r Speicherzellenrealisierung. Je<strong>de</strong> Zelle sowie<br />
die Speicherzelle haben Zugriff auf je<strong>de</strong>n Wert <strong>de</strong>s Busses.<br />
Wenn die Zellen einen Wert von <strong>de</strong>r Speicherzelle erfragen wollen, schreibt die Speicherzelle<br />
<strong>de</strong>n gesamten Vektor <strong>de</strong>r Variablen auf <strong>de</strong>n Bus. Die Zellen können dann vom Bus<br />
die Werte lesen, welche sie interessieren. In <strong>de</strong>r Ak<strong>tu</strong>alisierungsphase <strong>de</strong>r Speicherzelle<br />
schreiben dann die Zellen ihren Variablenwert, welcher ihrer Zell-ID entspricht, auf die<br />
Lei<strong>tu</strong>ng <strong>de</strong>s Busses. Da je<strong>de</strong> Zelle eine einzigartige Zell-ID hat, kommt es zu keinerlei<br />
Schreibkollisionen.<br />
Diese Realisierung erscheint zwar struk<strong>tu</strong>rierter, allerdings wer<strong>de</strong>n noch mehr Verbindungen<br />
benötigt: zuzüglich zu <strong>de</strong>n n 2 + n Verbindung <strong>de</strong>r ersten Realisierung sind auch<br />
noch die n Verbindungen <strong>de</strong>s Busses nötig. Außer<strong>de</strong>m gilt es zu beachten, dass in <strong>de</strong>r<br />
ersten Realsierung nur unidirektionale Verbindungen gebraucht wur<strong>de</strong>n, in dieser Modifikation<br />
allerdings die n Verbindungen <strong>de</strong>r Speicherzelle sowie min<strong>de</strong>stens eine Verbindung<br />
pro Zelle bidirektional sind. Der Zugriff ist weiterhin in zwei Takten möglich und somit<br />
ist diese Variante zumin<strong>de</strong>st dahingehend interessant, dass sie <strong>de</strong>n Denkansatz für <strong>de</strong>n<br />
nächsten Ansatz liefert.<br />
In <strong>de</strong>r zweiten Modifikation <strong>de</strong>s ersten Ansatzes wird auf die Speicherzelle gänzlich verzichtet<br />
[Abbildung 3.19]. In <strong>de</strong>r letzten Realisierungsvariante wur<strong>de</strong> ersichtlich, dass <strong>de</strong>r<br />
50
Abbildung 3.19.: Realisierung durch einen Datenbus ohne Speicherzell. Je<strong>de</strong> Zelle kann<br />
auf eine Stelle <strong>de</strong>s Busses schreibend und auf alle lesend zugreifen.<br />
Bus die Kommunikation min<strong>de</strong>stens genauso gut koordinieren kann wie die Speicherzelle.<br />
Der Vorteil <strong>de</strong>r Speicherzelle, dass sie Daten zwischenspeichern kann, wird hier<br />
nicht effektiv genutzt und <strong>de</strong>mentsprechend kann auf die Speicherzelle auch verzichtet<br />
wer<strong>de</strong>n. Wenn Variablen abgefragt wer<strong>de</strong>n sollen, schreibt je<strong>de</strong> Zelle i die entsprechen<strong>de</strong><br />
Variable auf die i-te Stelle <strong>de</strong>s Busses. Da dies alle Zellen parallel machen, kann sofort im<br />
nächsten Takt darauf gelesen wer<strong>de</strong>n. Für einen Variablenaustausch sind also weiterhin<br />
nur zwei Takte nötig. Allerdings kann eine Zelle (die Speicherzelle) eingespart wer<strong>de</strong>n<br />
und die Anzahl <strong>de</strong>r benötigten Verbindungen sinkt wie<strong>de</strong>r auf n 2 + 2n unidirektionale<br />
Verbindungen. Erlaubt man bidirektionale Verbindungen, so wer<strong>de</strong>n nur noch n 2 + n<br />
Verbindungen benötigt, wovon n Verbindungen bidirektional sind.<br />
Ein völlig an<strong>de</strong>rer Ansatz wird in <strong>de</strong>r in Abbildung 3.20 dargestellten zweiten Realisierung<br />
verfolgt. Hier wur<strong>de</strong> die Anzahl <strong>de</strong>r Verbindungen auf 2n reduziert. Die Speicherzelle<br />
stellt je<strong>de</strong>r Zelle einen Port zur Verfügung, aus <strong>de</strong>m sie die für sie interessanten Werte<br />
auslesen kann. Zusätzlich stellt je<strong>de</strong> Zelle <strong>de</strong>r Speicherzelle einen Port zur Verfügung, an<br />
<strong>de</strong>m die Speicherzelle ihrerseits Werte auslesen kann.<br />
Der Verbindungsaufwand ist <strong>de</strong>utlich geringer als bei <strong>de</strong>n ersten Realisierungen, allerdings<br />
ist mehr Logik und Kommunikation notwendig, damit je<strong>de</strong> Zelle auch wirklich <strong>de</strong>n<br />
Wert erhält, <strong>de</strong>n sie benötigt. Voraussetzung für diese Realisierung ist, dass alle Zellen<br />
zeitgleich funktionieren, also immer <strong>de</strong>n gleichen Schritt <strong>de</strong>s Algorithmus bearbeiten und<br />
die Speicherzelle zu<strong>de</strong>m weiß, welcher Schritt gera<strong>de</strong> ak<strong>tu</strong>ell ist.<br />
Je<strong>de</strong> Zelle, die eine Anfrage an die Speicherzelle stellen möchte, schreibt <strong>de</strong>n benötigten<br />
In<strong>de</strong>x in ihren Port, ansonsten wird ein als ”<br />
none“ <strong>de</strong>finierter Wert in die Variable <strong>de</strong>s<br />
Ports geschrieben. Im nächsten Schritt liest die Speicherzelle parallel die Portvariablen<br />
aller Zellen aus und bearbeitet die Werte in einem Takt. Wie<strong>de</strong>r einen Schritt später<br />
schreibt sie die angefragten Werte an die entsprechen<strong>de</strong>n Ports, wo sich die Zellen im<br />
nächsten Takt die Werte abholen können. Der Ablauf benötigt also fünf Takte.<br />
Einer <strong>de</strong>r Vorteile dieser Realisierung sind allerdings die ”<br />
none“ Werte, die möglich sind,<br />
was im Weiteren noch genauer erläutert wird. In vielen Schritten ist nicht klar, wie<br />
lange ein Prozessor wirklich braucht, bis er ein Ergebnis gefun<strong>de</strong>n hat. So muss z. B. im<br />
ersten Schritt einerseits die Zeile durchlaufen und dann noch <strong>de</strong>r entsprechen<strong>de</strong> Wert C<br />
51
Abbildung 3.20.: Je<strong>de</strong> Zelle hat einen Port in <strong>de</strong>r Speicherzelle, auf <strong>de</strong>n sie lesend zugreift<br />
und zusätzlich einen eigenen Port, auf <strong>de</strong>n die Speicherzelle lesend<br />
zugreifen kann.<br />
abgefragt wer<strong>de</strong>n. Wenn nicht im ersten Anlauf ein passen<strong>de</strong>r Wert gefun<strong>de</strong>n wird, muss<br />
<strong>de</strong>r Ablauf wie<strong>de</strong>rholt wer<strong>de</strong>n. Wur<strong>de</strong> <strong>de</strong>r Wert gefun<strong>de</strong>n, wird die Zelle <strong>de</strong>n none“ Wert ”<br />
in die Portvariable schreiben und so anzeigen, dass sie fertig ist. Haben alle Zellen einen<br />
none“ Wert in <strong>de</strong>r Portvariable, so kann die Speicherzelle schließen, dass <strong>de</strong>r Schritt<br />
”<br />
erfolgreich abgearbeitet wur<strong>de</strong> und dies <strong>de</strong>n Zellen mitteilen, in<strong>de</strong>m sie einen <strong>de</strong>finierten<br />
Wert an ihre Portvariablen anlegt. Dies setzt natürlich voraus, dass die Zellen auch dann<br />
lesend auf die Portvariable zugreifen, wenn sie selber keinen Wert angefor<strong>de</strong>rt haben.<br />
Die Laufzeitkomplexität wird durch diese Modifikation zwar nicht verringert, aber <strong>de</strong>r<br />
Algorithmus wird <strong>de</strong>nnoch beschleunigt.<br />
Wählt man <strong>de</strong>n zweiten Mo<strong>de</strong>llierungsansatz, so benötigt man O(n) Prozessoren mit<br />
O(n) Verbindungen und einer Laufzeit von O(nlog(n)), wobei n die Anzahl <strong>de</strong>r Knoten<br />
ist.<br />
Mit diesem zweiten Ansatz kann man nun wie<strong>de</strong>r versuchen, <strong>de</strong>n Algorithmus in das<br />
in Kapitel 2.3.4 vorgestellte Schema einzuordnen. Auch hier ist wie<strong>de</strong>r eine getrennte<br />
Betrach<strong>tu</strong>ng <strong>de</strong>r Abhängigkeiten <strong>de</strong>r Daten und <strong>de</strong>r Verbindungen <strong>de</strong>r nächsten Generation<br />
notwendig. Da <strong>de</strong>r Algorithmus von Hirschberg allerdings aus fünf Schritten<br />
besteht, wird nachfolgend die Betrach<strong>tu</strong>ng nach diesen Schritten geglie<strong>de</strong>rt:<br />
• Im ersten Schritt wird abhängig von <strong>de</strong>n eigenen Daten <strong>de</strong>r Zelle eine Anfrage<br />
an die Speicherzelle gestellt. Abhängig von <strong>de</strong>ren Antwort än<strong>de</strong>rn sich die Daten<br />
<strong>de</strong>r Zelle. Daraus ergibt sich eine Abhängigkeit <strong>de</strong>r Daten von <strong>de</strong>n eigenen Daten<br />
und <strong>de</strong>n Daten <strong>de</strong>r Nachbar-Zelle (in diesem Fall die Speicherzelle). Von <strong>de</strong>r Zeit,<br />
<strong>de</strong>r genauen Position <strong>de</strong>r Zelle o<strong>de</strong>r <strong>de</strong>r Verbindung an sich sind die Daten nicht<br />
52
abhängig. Die einzige Verbindung <strong>de</strong>r Zelle in diesem Schritt ist die Verbindung<br />
zur Speicherzelle und diese wird nicht geän<strong>de</strong>rt. Aus diesem Grund ergeben sich<br />
in diesem Schritt keine Abhängigkeiten <strong>de</strong>r Verbindungen.<br />
• Im zweiten Schritt wer<strong>de</strong>n Tupel von <strong>de</strong>r Speicherzelle bereitgestellt und von <strong>de</strong>n<br />
an<strong>de</strong>ren Zellen gelesen. Abhängig von <strong>de</strong>n eigenen Daten wer<strong>de</strong>n die Daten <strong>de</strong>r<br />
Speicherzelle weiter verwen<strong>de</strong>t o<strong>de</strong>r nicht. Somit ergibt sich wie<strong>de</strong>r eine Abhängigkeit<br />
<strong>de</strong>r Daten von <strong>de</strong>n eigenen Daten sowie <strong>de</strong>n Daten <strong>de</strong>r Nachbar-Zelle. Auch<br />
hier verän<strong>de</strong>rt sich die Verbindung nicht, weshalb hier wie<strong>de</strong>r keine Abhängigkeiten<br />
für diese festgestellt wer<strong>de</strong>n können.<br />
• Der dritte Schritt umfasst das Kopieren <strong>de</strong>s Wertes (bzw. Vektors für die Speicherzelle)<br />
T in <strong>de</strong>n Wert (Vektor) B. Das heißt, dass sich die Daten nur abhängig von<br />
<strong>de</strong>n eigenen Daten än<strong>de</strong>rn. Es wer<strong>de</strong>n keine Verbindungen benutzt o<strong>de</strong>r verän<strong>de</strong>rt,<br />
weshalb wie<strong>de</strong>r keine Abhängigkeiten für die Verbingungen bestehen.<br />
• Im vierten Schritt wird wie<strong>de</strong>r von <strong>de</strong>r Zelle ein Wert <strong>de</strong>r Speicherzelle gelesen und<br />
abhängig davon <strong>de</strong>r eigene Wert geän<strong>de</strong>rt. Da <strong>de</strong>r aus <strong>de</strong>r Speicherzelle gelesene<br />
Wert abhängig von <strong>de</strong>n eigenen Daten ist, ergibt sich wie<strong>de</strong>r die Abhängigkeit <strong>de</strong>r<br />
Daten von <strong>de</strong>n eigenen Daten und <strong>de</strong>n Daten <strong>de</strong>r Nachbar-Zelle. Es wer<strong>de</strong>n auch<br />
hier keine Verbindungen verän<strong>de</strong>rt.<br />
• Im fünften Schritt wird wie<strong>de</strong>r ein Wert aus <strong>de</strong>r Speicherzelle ausgelesen. Abhängig<br />
von diesem gelesenen Wert und <strong>de</strong>m eigenen Wert wird dann <strong>de</strong>r neue Wert bestimmt.<br />
Es ergibt sich also eine Abhängigkeit von <strong>de</strong>n eigenen Daten und <strong>de</strong>n<br />
Daten <strong>de</strong>r Nachbar-Zelle. Auch hier wer<strong>de</strong>n keine Verbindungen verän<strong>de</strong>rt, es ergeben<br />
sich dafür also keine Abhängigkeiten.<br />
Mit Hilfe <strong>de</strong>s Speicherzellen-Konzepts ist es gelungen, dass <strong>de</strong>r Algorithmus im Laufe<br />
<strong>de</strong>r Zeit keine Än<strong>de</strong>rungen an <strong>de</strong>n Verbindungen vornehmen muss. Auch sind die Daten<br />
immer nur abhängig von <strong>de</strong>n eigenen Daten und <strong>de</strong>n Daten <strong>de</strong>r Speicherzelle. Die Speicherzelle<br />
bil<strong>de</strong>t keine Ausnahme bei <strong>de</strong>n Zellen. Sie hängt implizit von <strong>de</strong>n eigenen Daten<br />
ab, da die Zellen ihren Wert abfragen und mit <strong>de</strong>ren Hilfe ihren neuen Wert berechnen.<br />
Es ergeben sich also die Funktionen: d’ = f(d,d*) und p’ = g().<br />
3.1.3.4.2. Laufzeitkomplexitätsverbesserung Alle Techniken, welche in <strong>de</strong>r Komplexitätsbetrach<strong>tu</strong>ng<br />
<strong>de</strong>s Algorithmus auf <strong>de</strong>r P-RAM angewen<strong>de</strong>t wur<strong>de</strong>n, sind auch auf<br />
<strong>de</strong>m GCA anwendbar. Mit Hilfe <strong>de</strong>r Speicherzelle verhält sich <strong>de</strong>r GCA fast ebenso wie<br />
die P-RAM und die regelmäßige Auffrischung <strong>de</strong>s Speichers <strong>de</strong>r Speicherzelle ist aufgrund<br />
<strong>de</strong>r hohen Nebenläufigkeit in O(1) möglich und wirkt sich somit nicht negativ auf<br />
die Komplexitätsbetrach<strong>tu</strong>ng aus.<br />
53
3.2. Minimal aufspannen<strong>de</strong> Bäume<br />
Bäume sind zusammenhängen<strong>de</strong>, azyklische Graphen. Das be<strong>de</strong>utet in einem ungerichteten<br />
Graphen, dass es von je<strong>de</strong>m Knoten zu je<strong>de</strong>m an<strong>de</strong>ren Knoten genau einen Weg gibt.<br />
Ein aufspannen<strong>de</strong>r Baum ist ein Baum, welcher aus einem Graphen durch das Entfernen<br />
von Kanten entstan<strong>de</strong>n ist. Ein minimal aufspannen<strong>de</strong>r Baum ist ein aufspannen<strong>de</strong>r<br />
Baum, <strong>de</strong>ssen Summe an Kantenwer<strong>tu</strong>ng minimal ist. Solche minimal aufspannen<strong>de</strong>n<br />
Bäume wer<strong>de</strong>n z. B. in Netzwerken verwen<strong>de</strong>t, um aus einer Menge von möglichen<br />
Verbindungen diejenigen auszuwählen, welche die geringsten Kosten verursachen und<br />
trotz<strong>de</strong>m eine Verbindung zwischen allen Komponenten garantieren.<br />
In <strong>de</strong>r sequentiellen Programmierung gibt es viele <strong>Algorithmen</strong> zur Bestimmung von<br />
minimal aufspannen<strong>de</strong>n Bäumen. Die <strong>Algorithmen</strong>, welche im Grunds<strong>tu</strong>dium <strong>de</strong>r Informatik<br />
gelehrt wer<strong>de</strong>n, sind <strong>de</strong>r Algorithmus von Kruskal und <strong>de</strong>r Algorithmus von<br />
Prim. Bei<strong>de</strong> liefern einen minimal aufspannen<strong>de</strong>n Baum, allerdings stecken vollkommen<br />
unterschiedliche Ansätze hinter <strong>de</strong>n <strong>Algorithmen</strong>. Aus diesem Grund wer<strong>de</strong>n die bei<strong>de</strong>n<br />
<strong>Algorithmen</strong> auch meist verwen<strong>de</strong>t, um zu illustrieren, dass verschie<strong>de</strong>ne Ansätze zu<br />
richtigen Ergebnissen führen können.<br />
Der Algorithmus von Kruskal startet mit einer sortierten Menge an Kanten und <strong>de</strong>n<br />
Knoten <strong>de</strong>s Baums, die als eigenständige Wäl<strong>de</strong>r 8 betrachtet wer<strong>de</strong>n. Im Verlauf <strong>de</strong>s<br />
Algorithmus wird dann die kleinste Kante <strong>de</strong>s Graphen genommen und überprüft, ob<br />
durch das Aufnehmen dieser Kante zwei Wäl<strong>de</strong>r verbun<strong>de</strong>n wer<strong>de</strong>n o<strong>de</strong>r nicht. Wenn<br />
keine zwei Wäl<strong>de</strong>r verbun<strong>de</strong>n wer<strong>de</strong>n, entstün<strong>de</strong> mit <strong>de</strong>m Aufnehmen <strong>de</strong>r Kante ein<br />
Zyklus, weshalb die Kante verworfen wird. Ansonsten wird die Kante aufgenommen und<br />
die bei<strong>de</strong>n Wäl<strong>de</strong>r wer<strong>de</strong>n zu einem Wald zusammengefasst.<br />
Der Algorithmus von Prim hat einen Startknoten, welcher anfangs die Menge <strong>de</strong>r aufgenommenen<br />
Knoten repräsentiert. Im Verlauf <strong>de</strong>s Algorithmus wird immer die Kante<br />
gesucht, die einen Knoten aus dieser Menge mit einem Knoten außerhalb <strong>de</strong>r Menge<br />
verbin<strong>de</strong>t und dabei die minimale Kantenwer<strong>tu</strong>ng hat. Diese Kante wird dann in <strong>de</strong>n<br />
minimalen Spannbaum aufgenommen und die Menge wird um <strong>de</strong>n Knoten erweitert, <strong>de</strong>r<br />
nicht in <strong>de</strong>r Menge war und nun durch die Kante verbun<strong>de</strong>n wur<strong>de</strong>. Da keine Kanten<br />
betrachtet wer<strong>de</strong>n, die nicht genau einen Knoten in <strong>de</strong>r Menge als Start o<strong>de</strong>r Ziel haben,<br />
können keine Zyklen entstehen.<br />
3.2.1. Der Kruskal-Algorithmus<br />
Auch beim Algorithmus von Kruskal 9 muss betrachtet wer<strong>de</strong>n, inwiefern <strong>de</strong>r Algorithmus<br />
parallelisierbar ist. Betrachtet man <strong>de</strong>n Algorithmus kritisch, so muss man feststellen,<br />
8 Ein Baum ist ein zusammenhängen<strong>de</strong>r, azyklischer Graph. Ein Wald besteht aus mehreren Bäumen,<br />
die nicht unbedingt zusammenhängen müssen.<br />
9 Eine Erklärung zu Kruskal fin<strong>de</strong>t sich in [Gü92] auf Seite 170-173. In diesem Buch fin<strong>de</strong>n sich auch<br />
an<strong>de</strong>re Graphenalgorithmen, u. a. <strong>de</strong>r Warshall-Algorithmus.<br />
54
dass die Kanten nicht parallel überprüft wer<strong>de</strong>n können, weil nicht parallel getestet wer<strong>de</strong>n<br />
kann, ob ein Zyklus ensteht. Da <strong>de</strong>r Kruskal allerdings eine Sortierung <strong>de</strong>r Kanten<br />
for<strong>de</strong>rt, bietet sich dort eine Möglichkeit zur Parallelisierung 10 . Da <strong>de</strong>r Kruskal in je<strong>de</strong>m<br />
Schritt nur die kleinste Kante for<strong>de</strong>rt und nicht nötigerweise von Anfang an eine sortierte<br />
Liste benötigt, kann die Sortierung parallel zur Zyklenüberprüfung stattfin<strong>de</strong>n. Auf <strong>de</strong>m<br />
GCA existieren viele Sortieralgorithmen, u. a. das bitonische Mischen (s. [Hee01]), das<br />
sich hier aber nicht so gut eignet. Ein Sortier-Algorithmus, welcher sich sowohl parallelisieren<br />
lässt als auch in je<strong>de</strong>m Schritt die kleinste Kante zur Verfügung stellen kann, ist<br />
<strong>de</strong>r Heap-Sort. Dieser soll in <strong>de</strong>m folgen<strong>de</strong>n Abschnitt an einem Beispiel näher erklärt<br />
wer<strong>de</strong>n, bevor dann <strong>de</strong>r Kruskal mit <strong>de</strong>m Heap-Sort auf <strong>de</strong>m GCA mo<strong>de</strong>lliert wird.<br />
3.2.1.1. Der Heap-Sort<br />
Da hier immer die kleinste Kantenwer<strong>tu</strong>ng gesucht wird, kann in diesem Falle ein Min-<br />
Heap verwen<strong>de</strong>t wer<strong>de</strong>n, d. h. <strong>de</strong>r Vaterknoten enthält immer einen kleineren Wert als<br />
seine Söhne. Damit steht dann immer <strong>de</strong>r kleinste Wert <strong>de</strong>s Baums in <strong>de</strong>r Wurzel.<br />
Abbildung 3.21.: Ein Min-Heap: In <strong>de</strong>r Wurzel steht immer ein kleinerer Wert als in<br />
<strong>de</strong>n Söhnen. Außer<strong>de</strong>m ist <strong>de</strong>r Baum fast vollständig.<br />
Um <strong>de</strong>n Heap aufzubauen und am En<strong>de</strong> alle Bedingung wie in Abbildung 3.21 zu erfüllen,<br />
wird die zu sortieren<strong>de</strong> Menge als Vektor eingelesen. In diesem Fall sollen Kanten sortiert<br />
wer<strong>de</strong>n, es wird also ein Baum <strong>de</strong>r Höhe log(|E|) bei <strong>de</strong>r Wurzel beginnend schrittweise<br />
gefüllt. Dabei kommt das erste Element in die Wurzel, das zweite in <strong>de</strong>n linken Sohn<br />
<strong>de</strong>r Wurzel, das dritte in <strong>de</strong>n rechten und dann wird die nächste Ebene von links nach<br />
rechts aufgefüllt. Im weiteren wird <strong>de</strong>r Heap allgemein für n Werte beschrieben.<br />
Ein Baum, in <strong>de</strong>m je<strong>de</strong> Ebene bis auf die letzte vollständig gefüllt und <strong>de</strong>ssen letzte<br />
Ebene von links beginnend ohne Lücken gefüllt ist, wird fast vollständig genannt. Wird<br />
<strong>de</strong>r Baum wie beschrieben aufgebaut, so ist er fast vollständig. Der Baum dient dabei<br />
lediglich <strong>de</strong>r Visualisierung <strong>de</strong>r Vorgänge beim Ablauf <strong>de</strong>s Algorithmus, <strong>de</strong>nn <strong>de</strong>r<br />
Algorithmus selbst arbeitet auf <strong>de</strong>m Vektor. Dieser Vektor kann genau dann als fast<br />
vollständiger Baum interpretiert wer<strong>de</strong>n, wenn es keine freien Einträge im Vektor gibt.<br />
10 Dieser Vorschlag wird auch in [Kru05] gemacht. Dort wird allerdings eine an<strong>de</strong>re Möglichkeit vorgeschlagen,<br />
<strong>de</strong>n Heap zu parallelisieren.<br />
55
Im Folgen<strong>de</strong>n wird <strong>de</strong>r Algorithmus weiterhin am Baum erklärt, da dies anschaulicher<br />
ist.<br />
Ist <strong>de</strong>r Baum wie oben beschrieben gefüllt wor<strong>de</strong>n, so entspricht er noch nicht <strong>de</strong>r Bedingung,<br />
dass die Wurzel immer kleiner als ihre Söhne sein muss. Dazu wer<strong>de</strong>n die Knoten<br />
<strong>de</strong>r vorletzten Ebene überprüft, ob sie Söhne haben, <strong>de</strong>ren Wert kleiner ist als ihr eigener.<br />
Ist dies <strong>de</strong>r Fall, so wird mit <strong>de</strong>m Sohn getauscht, <strong>de</strong>r <strong>de</strong>n kleineren Wert enthält.<br />
Als Beispiel wird <strong>de</strong>r Baum betrachtet, <strong>de</strong>r durch <strong>de</strong>n Vektor (7, 5, 10, 1, 3, 4, 9, 6)<br />
beschrieben wird. Eine graphische Darstellung <strong>de</strong>s Baums fin<strong>de</strong>t sich in Abbildung 3.22.<br />
Abbildung 3.22.: Nach <strong>de</strong>m Einlesen <strong>de</strong>s Vektors in <strong>de</strong>n Baum ist zwar ein fast<br />
vollständiger Baum entstan<strong>de</strong>n, er erfüllt allerdings noch nicht die<br />
Heap-Eigenschaft.<br />
Um die Heapeigenschaft herzustellen wird zuerst <strong>de</strong>r Wert 1 überprüft, ob er kleiner<br />
als sein Sohn ist. Dies ist <strong>de</strong>r Fall, also wird als nächstes <strong>de</strong>r Wert 10 überprüft, ob er<br />
kleiner als seine Söhne mit <strong>de</strong>n Werten 4 und 9 ist. Da hier die Bedingung nicht erfüllt<br />
ist, wird <strong>de</strong>r Wert 10 mit <strong>de</strong>m Wert 4 getauscht und dieser Teilbaum erfüllt nun für<br />
sich die Heap-Bedingung. Anschließend wird <strong>de</strong>r Wert 5 und <strong>de</strong>ssen zwei Söhne mit <strong>de</strong>n<br />
Werten 1 und 3 verglichen. Nun muss <strong>de</strong>r Wert 1 mit <strong>de</strong>m Wert 5 getauscht wer<strong>de</strong>n, was<br />
dazu führt, dass auch nachgeprüft wer<strong>de</strong>n muss, ob <strong>de</strong>r Wert 5 kleiner als <strong>de</strong>r Wert 6 ist,<br />
<strong>de</strong>r nun sein Sohn ist. Dies ist <strong>de</strong>r Fall, <strong>de</strong>shalb ist die Vertauschung damit abgeschlossen<br />
und die Wurzel kann nun geprüft wer<strong>de</strong>n. Da 7 aber größer als die 1 ist, muss auch hier<br />
wie<strong>de</strong>r getauscht wer<strong>de</strong>n. In diesem Fall ist die 7 aber auch größer als ihre neuen Söhne 5<br />
und 3 und muss <strong>de</strong>shalb mit 3 getauscht wer<strong>de</strong>n. Damit ist die initiale Heap-Herstellung<br />
abgeschlossen, <strong>de</strong>r Heap aus Abbildung 3.21 wur<strong>de</strong> hergestellt und die erste sortierte<br />
Kante kann entnommen wer<strong>de</strong>n.<br />
Das Entnehmen eines Werts in einem Heap gestaltet sich <strong>de</strong>rmaßen, dass er mit <strong>de</strong>m Wert<br />
getauscht wird, <strong>de</strong>r im Heap in <strong>de</strong>r untersten Ebene an <strong>de</strong>r rechtesten Stelle steht. Diese<br />
Stelle wird dann in allen nachfolgen<strong>de</strong>n Schritten ignoriert. Durch die Vertauschung hat<br />
<strong>de</strong>r Baum nun ein zu betrachten<strong>de</strong>s Element weniger. Der hieraus resultieren<strong>de</strong> Baum<br />
ist in 3.23 dargestellt.<br />
Die Wie<strong>de</strong>rherstellung <strong>de</strong>r Heap-Eigenschaft gestaltet sich immer gleich und soll hier nur<br />
exemplarisch erklärt wer<strong>de</strong>n. Da vor <strong>de</strong>m Vertauschen die Heap-Eigenschaft erfüllt war,<br />
müssen maximal log(n) Vertauschungen durchgeführt wer<strong>de</strong>n, bis die Heap-Eigenschaft<br />
56
Abbildung 3.23.: Der kleinste Wert wur<strong>de</strong> aus <strong>de</strong>m Heap entfernt und die Heap-<br />
Eigenschaft ist verletzt.<br />
Abbildung 3.24.: Der Wert <strong>de</strong>r Wurzel wur<strong>de</strong> mit <strong>de</strong>m Wert <strong>de</strong>s linken Sohns vertauscht.<br />
Die Heap-Eigenschaft muss für <strong>de</strong>n linken Sohn wie<strong>de</strong>r hergestellt wer<strong>de</strong>n.<br />
wie<strong>de</strong>r hergestellt ist. Zuerst wird die Wurzel mit <strong>de</strong>m kleineren Wert ihrer Söhne vertauscht,<br />
in diesem Fall ist dies die 3 aus <strong>de</strong>m linken Teilbaum. Damit muss am rechten<br />
Teilbaum nichts mehr geän<strong>de</strong>rt und nur noch <strong>de</strong>r linke Teilbaum betrachtet wer<strong>de</strong>n. Der<br />
Baum, <strong>de</strong>r nach <strong>de</strong>r ersten Vertauschung entsteht, ist in Abbildung 3.24 zu sehen.<br />
Im linken Teilbaum wer<strong>de</strong>n wie<strong>de</strong>r <strong>de</strong>r Wert <strong>de</strong>r Wurzel mit <strong>de</strong>n Werten <strong>de</strong>r Söhne<br />
verglichen und anschließend wird die 6 mit <strong>de</strong>r 5 ausgetauscht. Die Wie<strong>de</strong>rherstellung <strong>de</strong>r<br />
Heap-Eigenschaft ist somit abgeschlossen und Abbildung 3.25 zeigt <strong>de</strong>n resultieren<strong>de</strong>n<br />
Heap.<br />
Abbildung 3.25.: Nach <strong>de</strong>r zweiten Vertauschung ist die Heap-Eigenschaft wie<strong>de</strong>r hergestellt.<br />
Nun folgt wie<strong>de</strong>r das Entfernen eines Wertes, die Wie<strong>de</strong>rherstellung usw. bis <strong>de</strong>r Heap<br />
nur noch aus einem Element besteht. Im sequentiellen Fall läuft <strong>de</strong>r Heap mit einer<br />
Laufzeit von O(nlog(n)), da n mal die Heap-Eigenschaft wie<strong>de</strong>r hergestellt wer<strong>de</strong>n muss<br />
und das Wie<strong>de</strong>rherstellen maximal log(n) Schritte benötigt.<br />
57
Der Heap-Sort eignet sich zur Parallelisierung, da je<strong>de</strong>r Wert innerhalb <strong>de</strong>s Heaps durch<br />
einen Prozessor repräsentiert wer<strong>de</strong>n kann. Die Möglichkeiten zur Parallelisierungen und<br />
wieviele Prozessoren wirklich benötigt wer<strong>de</strong>n, sollen im nächsten Abschnitt erläutert<br />
wer<strong>de</strong>n.<br />
3.2.1.2. Der Heap-Sort auf <strong>de</strong>m GCA<br />
Der Heap-Sort kann auf <strong>de</strong>m GCA dadurch realisiert wer<strong>de</strong>n, dass je<strong>de</strong>r zu sortieren<strong>de</strong><br />
Wert durch eine Zelle repräsentiert wird. Durch diese Realisierung wer<strong>de</strong>n O(n) Zellen<br />
benötigt. Je<strong>de</strong> Zelle muss wissen, ob sie einen Blattknoten o<strong>de</strong>r einen Wurzelknoten<br />
repräsentiert. Der Knoten, <strong>de</strong>r die Wurzel <strong>de</strong>s gesamten Baums repräsentiert, muss dies<br />
auch wissen, da er eine Son<strong>de</strong>rstellung hat.<br />
In regelmässigen Abstän<strong>de</strong>n muss ein Wurzelknoten abfragen, ob er einen größeren Wert<br />
besitzt, als seine Söhne. Da beim GCA nicht schreibend auf an<strong>de</strong>re Zellen zugegriffen<br />
wer<strong>de</strong>n darf, müssen auch die Söhne überprüfen, ob sie einerseits einen kleineren Wert<br />
als ihr Vater haben und an<strong>de</strong>rerseits, ob ihr Wert kleiner ist als <strong>de</strong>r Wert <strong>de</strong>s an<strong>de</strong>ren<br />
Sohns <strong>de</strong>r Wurzel. Ist einer <strong>de</strong>r bei<strong>de</strong>n Werte kleiner als <strong>de</strong>r eigene, so muss ein<br />
Sohnknoten nichts machen, ein Wurzelknoten hingegen erkennt dadurch, dass <strong>de</strong>r Wert<br />
getauscht wer<strong>de</strong>n muss. Ein Sohnknoten erkennt einen nötigen Tausch dadurch, dass er<br />
bei allen Vergleichen <strong>de</strong>n kleinsten Wert hat. Ein Son<strong>de</strong>rfall muss hier noch betrachtet<br />
wer<strong>de</strong>n: Sind gleiche Werte innerhalb <strong>de</strong>s Heaps erlaubt, so wer<strong>de</strong>n Zell-IDs eingeführt,<br />
um festzustellen, welches <strong>de</strong>r linke und welches <strong>de</strong>r rechte Sohn einer Zelle ist. Zu<strong>de</strong>m<br />
kann dann eine Zelle feststellen, ob sie ein linker o<strong>de</strong>r ein rechter Sohn ist. Hat nun eine<br />
Sohnzelle <strong>de</strong>n gleichen Wert wie die Wurzel, so wird <strong>de</strong>swegen nicht getauscht. Haben<br />
bei<strong>de</strong> Söhne <strong>de</strong>n gleichen Wert und es muss getauscht wer<strong>de</strong>n, so wird immer <strong>de</strong>r Wert<br />
<strong>de</strong>s linken Sohns genommen. In an<strong>de</strong>ren Worten be<strong>de</strong>utet das, dass eine Zelle, wenn sie<br />
bei einem Vergleich feststellt, dass sie <strong>de</strong>n gleichen Wert hat, nachprüft, ob <strong>de</strong>r gleiche<br />
Wert in einer Zelle mit niedriegerer ID steht. Ist dies <strong>de</strong>r Fall, so ist <strong>de</strong>r gleiche Wert<br />
entwe<strong>de</strong>r in <strong>de</strong>r Wurzel o<strong>de</strong>r im linken Sohn und es muss nichts getan wer<strong>de</strong>n. Ansonsten<br />
wird wie gewohnt verglichen, ob ein Tausch notwendig ist.<br />
Für <strong>de</strong>n Schritt <strong>de</strong>s Vertauschens ist es wichtig, dass <strong>de</strong>r GCA synchron arbeitet, <strong>de</strong>nn<br />
um die Werte zu tauschen, muss einerseits die Sohnzelle auf die Wurzelzelle und die<br />
Wurzelzelle ihrerseits auf die Sohnzelle lesend zugreifen. Wenn bei diesem Schritt <strong>de</strong>r<br />
Automat nicht synchron arbeitet, ist einer <strong>de</strong>r Werte bereits überschrieben, bevor die<br />
an<strong>de</strong>re Zelle ihn gelesen hat. Nach <strong>de</strong>m Vertauschen muss die Sohnzelle überprüfen,<br />
ob sie ein Blatt ist o<strong>de</strong>r ihrerseits wie<strong>de</strong>r Söhne hat. Der Wert <strong>de</strong>r Vaterzelle ist zu<br />
diesem Zeitpunkt schon wie<strong>de</strong>r abgreifbar. Eine Zelle muss sowohl Verbindungen zu<br />
ihren Söhnen als auch zu ihrem Vater haben. Die Möglichkeiten <strong>de</strong>n Heap auf <strong>de</strong>m GCA<br />
darzustellen ist in Abbildung 3.26 zu sehen. Auffallend ist hierbei, dass die Wurzel-Zelle<br />
<strong>de</strong>s Heaps lesen<strong>de</strong>n Zugriff auf alle Zellen hat, um die Vertauschungen durchzuführen,<br />
die jeweils notwendig sind, um die Liste zu sortieren. Da <strong>de</strong>r Heap hier in einer Form<br />
verwen<strong>de</strong>t wird, bei <strong>de</strong>r es egal ist, dass die eingegebene Liste im Verlauf <strong>de</strong>s Algorithmus<br />
zerstört wird (wichtig ist nur, dass immer zum richtigen Zeitpunkt <strong>de</strong>r jeweils kleinste<br />
58
Abbildung 3.26.: Die Heap-Zellen <strong>de</strong>s GCA kennen sowohl ihren Vater als auch ihre<br />
Söhne, sowie ihre Nachbarn. Die Wurzel-Zelle <strong>de</strong>s Heaps hat eine Verbindung<br />
zu je<strong>de</strong>r Zelle, damit sie die Werte tauschen kann. Je<strong>de</strong> Zelle<br />
weiß zu<strong>de</strong>m, ob sie ein Blatt darstellt o<strong>de</strong>r nicht.<br />
Abbildung 3.27.: Der in <strong>de</strong>n Heap eingelesene Vektor.<br />
Wert in <strong>de</strong>r Wurzel steht), reicht es, wenn die Wurzel die Werte liest und die Blattknoten<br />
sich <strong>de</strong>aktivieren.<br />
Die Synchronisation <strong>de</strong>r Deaktivierungen kann wie<strong>de</strong>r durch die Zell-ID erfolgen, in<strong>de</strong>m<br />
die Zellen aufsteigend in <strong>de</strong>r Reihenfolge durchnummeriert wer<strong>de</strong>n, in <strong>de</strong>r sie auch befüllt<br />
wer<strong>de</strong>n. Zu<strong>de</strong>m erhält je<strong>de</strong> Zelle das Wissen, wieviele Zellen es insgesamt im Heap gibt<br />
und das Wissen, nach wievielen Takten ein Blatt <strong>de</strong>aktiviert wird. Die Zellen testen alle<br />
parallel zur gleichen Zeit, ob sie die zu <strong>de</strong>aktivieren<strong>de</strong> Zelle sind und <strong>de</strong>krementieren dann<br />
die maximale Anzahl an Zellen. Die Zelle, die festgestellt hat, dass sie <strong>de</strong>aktiviert wer<strong>de</strong>n<br />
muss, <strong>tu</strong>t dies dann auch. Eine Zelle, die vor <strong>de</strong>m <strong>de</strong>aktivieren ihre Söhne überprüfen<br />
musste, kann leicht feststellen, ob dies auch nach <strong>de</strong>r Deaktivierung notwendig ist, in<strong>de</strong>m<br />
sie die Nummer ihres Sohns mit <strong>de</strong>r noch nicht <strong>de</strong>krementierten Anzahl an maximalen<br />
Zellen vergleicht. Ist <strong>de</strong>r einzige Sohn die zu <strong>de</strong>aktivieren<strong>de</strong> Zelle, so muss kein Vergleich<br />
mehr durchgeführt wer<strong>de</strong>n, ansonsten läuft <strong>de</strong>r Vergleich wie bereits beschrieben ab.<br />
Der Ablauf <strong>de</strong>s Heap-Algorithmus wird parallelisiert und im Folgen<strong>de</strong>n anhand von<br />
Graphiken erklärt. Damit das Beispiel aussagekräftig wird, muss ein größeres Beispiel<br />
gewählt wer<strong>de</strong>n, da das vorherige Beispiel nicht <strong>de</strong>utlich machen wür<strong>de</strong>, dass <strong>de</strong>r Algorithmus<br />
wirklich so funktioniert. Das Beispiel baut auf <strong>de</strong>m Vektor (7, 5, 10, 1, 3, 4, 9,<br />
6, 12, 15, 2, 20, 22, 14) auf und wird in Abbildung 3.27 als Baum dargestellt.<br />
59
Abbildung 3.28.: Der Heap nach Herstellung <strong>de</strong>r Heap-Eigenschaft. Der Schritt <strong>de</strong>r Herstellung<br />
muss noch sequentiell erfolgen, mehrere Prozessoren können<br />
dazu zwar verwen<strong>de</strong>t wer<strong>de</strong>n, ermöglichen aber keine Laufzeitverbesserung.<br />
Abbildung 3.29.: Der Eintrag <strong>de</strong>r Wurzel wird mit <strong>de</strong>m letzten Eintrag getauscht. Der<br />
letzte Eintrag wird für <strong>de</strong>n weiteren Ablauf <strong>de</strong>s Heap-Algorithmus ignoriert.<br />
Die initiale Herstellung <strong>de</strong>r Heap-Eigenschaft kann zwar parallel geschehen, aber eine<br />
Verbesserung <strong>de</strong>r Laufzeit ist nicht zu erwarten. Nach <strong>de</strong>r Herstellung <strong>de</strong>r Heap-<br />
Eigenschaft sieht <strong>de</strong>r Baum wie in Abbildung 3.28 aus.<br />
Im nächsten Schritt wird nun <strong>de</strong>r oberste Eintrag mit <strong>de</strong>m Eintrag <strong>de</strong>r untersten Ebene,<br />
welcher am weitesten rechts steht, vertauscht. In diesem Fall wird also die 1 mit <strong>de</strong>r 14<br />
getauscht. Dies wird in Abbildung 3.29 ver<strong>de</strong>utlicht.<br />
Im nächsten Schritt muss die Wurzel ihren Wert mit <strong>de</strong>n Werten ihrer Söhne vergleichen.<br />
Da dies <strong>de</strong>r Anfang <strong>de</strong>s Algorithmus ist, müssen nur die Wurzel und ihre bei<strong>de</strong>n Söhne<br />
arbeiten, die restlichen Zellen sind inaktiv. Nach <strong>de</strong>m Vertauschen sieht <strong>de</strong>r Baum wie in<br />
Abbildung 3.30 aus. Nach <strong>de</strong>r Vertauschung steht <strong>de</strong>r kleinste Wert wie<strong>de</strong>r in <strong>de</strong>r Wurzel<br />
<strong>de</strong>s Baums, die Wurzel kann also wie<strong>de</strong>r mit <strong>de</strong>m letzten Wert getauscht wer<strong>de</strong>n.<br />
Im nächsten Schritt tauscht die Wurzel wie<strong>de</strong>r ihren Wert mit <strong>de</strong>m letzten Wert und zeitgleich<br />
führt <strong>de</strong>r linke Sohn die Vertauschung aus, die nötig ist, um die Heap-Eigenschaft<br />
wie<strong>de</strong>r herstellen zu können. Der linke Sohn hat nach dieser Vertauschung <strong>de</strong>n kleinsten<br />
Wert <strong>de</strong>s linken Teilbaums und kann im nächsten Schritt <strong>de</strong>r Wurzel wie<strong>de</strong>r zum<br />
60
Abbildung 3.30.: Die Wurzel wur<strong>de</strong> vertauscht und <strong>de</strong>r Vergleich mit <strong>de</strong>n Söhnen fand<br />
statt. In <strong>de</strong>r Wurzel steht nun wie<strong>de</strong>r <strong>de</strong>r kleinste Wert <strong>de</strong>s Baums.<br />
Abbildung 3.31.: Die Wurzel wur<strong>de</strong> erneut vertauscht. Zeitgleich hat <strong>de</strong>r linke Sohn seine<br />
Ausgleichsfunktion ausgeführt und beinhaltet wie<strong>de</strong>r <strong>de</strong>n kleinsten Wert<br />
dieses Teilbaums.<br />
Vergleich dienen. In diesem Schritt sind also die Wurzel, ihr linker Sohn, sowie <strong>de</strong>ssen<br />
bei<strong>de</strong>n Söhne beschäftigt. Ver<strong>de</strong>utlicht wird dieser Vorgang in Abbildung 3.31.<br />
Nun muss sowohl die Wurzel wie<strong>de</strong>r mit ihren Söhnen <strong>de</strong>n Vergleich ausführen. Genauso<br />
muss auch <strong>de</strong>r zweite Knoten <strong>de</strong>r dritten Ebene einen Vergleich mit seinen Söhnen<br />
ausführen. Die Vergleiche können zeitgleich stattfin<strong>de</strong>n und somit wer<strong>de</strong>n zwei Vertauschungen<br />
in diesem Schritt ausgeführt. Nach <strong>de</strong>m Vertauschen hat die Wurzel wie<strong>de</strong>r<br />
<strong>de</strong>n kleinsten Wert. Der resultieren<strong>de</strong> Baum ist in Abbildung 3.32 zu sehen.<br />
Der Algorithmus arbeitet nach <strong>de</strong>m selben Prinzip weiter. Ein letztes Beispiel soll Abbildung<br />
3.33 liefern. In einem Schritt tauscht die Wurzel ihren Eintrag mit <strong>de</strong>m letzten Wert<br />
und im nächsten wird dann die Vertauschung mit <strong>de</strong>n Söhnen ausgeführt. Die an<strong>de</strong>ren<br />
Knoten verhalten sich in ihrer Ebene immer gleich. Die Ebenen mit gera<strong>de</strong>r Nummer<br />
prüfen im zweiten Takt, ob sie mit ihrem Vater tauschen müssen, und im dritten, ob ein<br />
Tausch mit <strong>de</strong>n Söhnen nötig ist. Ebenen mit einer ungera<strong>de</strong>n Nummer testen bei <strong>de</strong>n<br />
ungera<strong>de</strong>n Takten, ob ein Tausch mit <strong>de</strong>m Vater nötig ist, und in <strong>de</strong>n gera<strong>de</strong>n Takten<br />
vergleichen sie ihren Wert mit <strong>de</strong>m ihrer Söhne.<br />
Nach <strong>de</strong>r Initialisierungs- und Herstellungsphase ermöglicht <strong>de</strong>r parallele Heap-Sort nach<br />
je<strong>de</strong>m zweiten Schritt das Entnehmen <strong>de</strong>s kleinsten Werts. Der Heap-Sort muss n mal<br />
61
Abbildung 3.32.: Die Wurzel hat wie<strong>de</strong>r mit <strong>de</strong>m kleineren Wert ihrer Söhne getauscht.<br />
Zeitgleich hat <strong>de</strong>r zweite Knoten <strong>de</strong>r dritte Ebene <strong>de</strong>n Vergleich und<br />
die Vertauschung mit seinen Söhnen durchgeführt.<br />
Abbildung 3.33.: Die Wurzel wur<strong>de</strong> wie<strong>de</strong>r mit <strong>de</strong>m letzten Element vertauscht, zeitgleich<br />
hat <strong>de</strong>r linke Sohn <strong>de</strong>n Wert mit seinem linken Sohn getauscht.<br />
<strong>de</strong>n niedrigsten Wert liefern, bevor die gesamte Liste sortiert ist. Damit hat <strong>de</strong>r Heap-<br />
Sort eine Komplexität von O(n). Innerhalb dieser Zeitgrenze können auch die Initialisierung<br />
sowie die anfängliche Herstellung <strong>de</strong>r Heapeigenschaft erledigt wer<strong>de</strong>n.<br />
Die Zelle, <strong>de</strong>ren Wert in die Wurzel gelesen wird, nach<strong>de</strong>m <strong>de</strong>r minimale Wert aus <strong>de</strong>r<br />
Wurzel ausgewertet wur<strong>de</strong>, muss sich nach <strong>de</strong>m Lesen <strong>de</strong>aktivieren. Wie dies ermöglicht<br />
wer<strong>de</strong>n kann, wur<strong>de</strong> bereits beschrieben. Zu<strong>de</strong>m muss diese Zelle bei ungera<strong>de</strong>n Takten<br />
aus <strong>de</strong>m Vergleich ausgeklammert wer<strong>de</strong>n, da sonst ein Element even<strong>tu</strong>ell doppelt im<br />
Baum vorkommt und ein Element fälschlicherweise gelöscht wird. Durch die Parallelisierung<br />
kann die Heap-Eigenschaft nicht mehr garantiert wer<strong>de</strong>n und <strong>de</strong>r Algorithmus<br />
funktioniert nur <strong>de</strong>shalb, weil die Heap-Eigenschaft anfangs hergestellt ist und die Wie<strong>de</strong>rherstellung<br />
von oben beginnt und somit <strong>de</strong>r Wurzel recht schnell <strong>de</strong>r nächste minimale<br />
Wert zur Verfügung gestellt wer<strong>de</strong>n kann.<br />
Diese Art <strong>de</strong>r Realisierung benötigt unnötig viele Zellen, da die meisten Zellen einer<br />
Ebene inaktiv sind und <strong>de</strong>r Heap zu<strong>de</strong>m verhältnismäßig schnell abgebaut wird.<br />
Um Zellen zu sparen, können die Zellen einer Ebene zusammengefasst wer<strong>de</strong>n. Je<strong>de</strong><br />
Ebenen-Zelle beinhaltet dann eine Liste, welche die Werte beinhaltet, die auf einer Ebene<br />
sind. Die einzige Zelle, welche even<strong>tu</strong>ell zwei Aktionen in einem Takt durchführen müsste,<br />
62
Abbildung 3.34.: Je<strong>de</strong> Zelle repräsentiert eine Ebene <strong>de</strong>s Baums aus Abbildung 3.21. Die<br />
Wurzel muss lesend auf alle Zellen zugreifen können.<br />
ist die Zelle, welche die letzte Ebene repräsentiert, da diese zeitgleich in einen Vergleich<br />
nach oben und eine Austauschaktion von <strong>de</strong>r Wurzel verstrickt sein kann. Aber auch<br />
wenn man für je<strong>de</strong> Vertauschungsaktion zwei Takte ansetzt, damit kein Konflikt entsteht,<br />
verschlechtert sich die Komplexitätsklasse nicht.<br />
Die Zelle, welche die letzte Ebene repräsentiert, muss dies wissen, damit sie sich entsprechend<br />
verhält. Unbelegte Plätze wer<strong>de</strong>n in dieser Zelle durch ein x gekennzeichnet und<br />
wenn die Wurzel sich <strong>de</strong>n letzten Wert geholt hat, muss dieser aus <strong>de</strong>r Liste gelöscht<br />
wer<strong>de</strong>n. Sind alle Werte <strong>de</strong>r Zelle auf x, so geht sie in <strong>de</strong>n inaktiven Modus. Die Zelle <strong>de</strong>r<br />
darüberliegen<strong>de</strong>n Ebene erkennt nun, dass sie die letzte Ebene repräsentiert und verhält<br />
sich <strong>de</strong>mentsprechend. Wenn die Wurzel die letzte Ebene ist, dann ist <strong>de</strong>r Algorithmus<br />
been<strong>de</strong>t. Eine Möglichkeit, die Zellen die Ebenen <strong>de</strong>s Baums darstellen zu lassen, ist in<br />
Abbildung 3.34 gegeben.<br />
Zu klären ist noch, wie <strong>de</strong>r Nachbar, <strong>de</strong>r Vater und die Söhne eines Werts <strong>de</strong>r Zelle<br />
i<strong>de</strong>ntifiziert wer<strong>de</strong>n können. Rechnet man nach, welchen Wert <strong>de</strong>r j-te Wert <strong>de</strong>r i-ten<br />
Ebene repräsentiert, so stellt man fest, dass er genau <strong>de</strong>n (2 i−1 + (j − 1))-ten Wert<br />
im gesamten Baum repräsentiert. Innerhalb eines Baums, <strong>de</strong>ssen Werte von 1 bis n<br />
nummeriert wer<strong>de</strong>n, hat <strong>de</strong>r i-te Wert seine Söhne an <strong>de</strong>r (2 ∗ i)-ten und <strong>de</strong>r (2 ∗ i + 1)-<br />
ten Stelle. Die Zelle muss zum Ausrechnen <strong>de</strong>r Stelle <strong>de</strong>s Vaters eines Werts nur die Stelle<br />
<strong>de</strong>s zu vergleichen<strong>de</strong>n Werts durch zwei teilen und abrun<strong>de</strong>n und hat somit die Position<br />
<strong>de</strong>s Vaters bestimmt. Die Stellen <strong>de</strong>r Söhne bestimmen sich dadurch, dass die Stelle mal<br />
zwei für <strong>de</strong>n einen Sohn und mal zwei und plus eins für <strong>de</strong>n zweiten Sohn genommen<br />
wird. Natürlich benötigt die Zelle auch das Wissen darüber, ob <strong>de</strong>r Wert überhaupt<br />
einen Vater bzw. Söhne hat. Dazu dient die Information über die Ebene und die Blatt-<br />
Einträge, die für je<strong>de</strong>n Wert angeben, ob er ein Blatt repräsentiert. Der Nachbar wird<br />
abhängig davon, ob <strong>de</strong>r Wert selber an einer gera<strong>de</strong>n o<strong>de</strong>r einer ungera<strong>de</strong>n Stelle steht,<br />
gefun<strong>de</strong>n: Der Nachbar eines Werts an einer ungera<strong>de</strong>n Stelle steht rechts vom Wert,<br />
ansonsten links.<br />
Soll <strong>de</strong>r Heap am En<strong>de</strong> eine sortierte Liste liefern und nicht wie hier nur regelmäßig<br />
die kleinste Kante zur Abfrage bereitstellen, benötigt je<strong>de</strong> Zelle zu<strong>de</strong>m einen Zähler,<br />
welcher ihrer Werte für <strong>de</strong>n Heap noch betrachtet wer<strong>de</strong>n. Die Zelle <strong>de</strong>r letzten Ebene<br />
63
muss dann auch dafür sorgen, dass ihr letzter Wert nicht nur gelesen wird, son<strong>de</strong>rn auch<br />
<strong>de</strong>r Wert <strong>de</strong>r Wurzel an diese Stelle geschrieben und <strong>de</strong>r Zähler <strong>de</strong>r zu beachten<strong>de</strong>n<br />
Werte um eins erniedrigt wird. Da je<strong>de</strong>s Da<strong>tu</strong>m einmal zurückgeliefert wer<strong>de</strong>n muss,<br />
kann die Laufzeit nicht besser wer<strong>de</strong>n. Allerdings wird das Wie<strong>de</strong>rherstellen <strong>de</strong>s Heaps<br />
parallel zu <strong>de</strong>r Ausgabe <strong>de</strong>r Kanten ausgeführt und kann somit für <strong>de</strong>n Algorithmus<br />
als konstant angesehen wer<strong>de</strong>n. Der Heap-Sort benötigt <strong>de</strong>mnach auf <strong>de</strong>m GCA eine<br />
Laufzeit von O(n).<br />
Möchte man nun <strong>de</strong>n Heap-Sort wie<strong>de</strong>r in das in Kapitel 2.3.4 vorgestellte Schema<br />
einordnen, so sind vorher ein paar Design-Entscheidungen zu treffen:<br />
Es wird davon ausgegangen, dass eine Sohnzelle ihren Sta<strong>tu</strong>s (letzte Ebene o<strong>de</strong>r nicht)<br />
daran feststellen kann, dass die Verbindung zu ihrem Sohn inaktiv ist. Die Daten sind<br />
also abhängig von <strong>de</strong>r eigenen Verbindung.<br />
Innerhalb <strong>de</strong>r Zelle <strong>de</strong>r letzten Ebene wird abhängig von <strong>de</strong>r Zeit jeweils ein an<strong>de</strong>res<br />
Da<strong>tu</strong>m von <strong>de</strong>r obersten Ebene ausgelesen. Dieses Da<strong>tu</strong>m wird anschließend von <strong>de</strong>r<br />
Zelle auf x gesetzt und somit ignoriert. Die Daten sind also abhängig von <strong>de</strong>r Zeit.<br />
Die Vertauschungen innerhalb <strong>de</strong>s Heaps sind abhängig von <strong>de</strong>n Daten <strong>de</strong>r eigenen und<br />
<strong>de</strong>r Vater- bzw. Sohnzelle. Da eine Vertauschung eine Verän<strong>de</strong>rung <strong>de</strong>r Daten be<strong>de</strong>utet,<br />
ergibt sich hier eine Abhängigkeit <strong>de</strong>r Daten von <strong>de</strong>n eigenen Daten sowie <strong>de</strong>r Daten <strong>de</strong>r<br />
Nachbar-Zellen.<br />
Der Sta<strong>tu</strong>s ist implizit abhängig von <strong>de</strong>m Ort <strong>de</strong>r Zelle. Allerdings wird <strong>de</strong>r Sta<strong>tu</strong>s<br />
geschickter durch die Verbindung getestet, dann kann die Zelle sich an einem beliebigem<br />
Ort innerhalb <strong>de</strong>s GCA befin<strong>de</strong>n. Aus diesem Grund sind die Daten nicht ortsabhängig.<br />
Die Verbindung <strong>de</strong>r eigenen Zelle sind von <strong>de</strong>n Verbindungen <strong>de</strong>r Nachbar-Zellen abhängig.<br />
So setzt eine Zelle ihre eigene Sohn-Verbindung auf inaktiv, wenn <strong>de</strong>r Sohn seine Verbindung<br />
zum Vater auf inaktiv gesetzt hat. Die Verbindung ist also abhängig von <strong>de</strong>r<br />
Verbindung <strong>de</strong>s Sohns, allerdings nicht von <strong>de</strong>r eigenen Verbindung.<br />
Wur<strong>de</strong> das letzte Da<strong>tu</strong>m aus <strong>de</strong>r Zelle ausgelesen (alle Werte sind auf x), so wird die<br />
Verbindung zum Vaterknoten <strong>de</strong>aktiviert. Die Verbindung ist damit abhängig von <strong>de</strong>n<br />
eigenen Daten.<br />
Die oberste Ebene nimmt innerhalb <strong>de</strong>s Heap-Sorts eine Son<strong>de</strong>rstellung ein, da sie auf<br />
alle Kanten lesend zugreifen muss. Diese Verbindungen wer<strong>de</strong>n mit <strong>de</strong>r Zeit verän<strong>de</strong>rt,<br />
es wird nacheinan<strong>de</strong>r auf verschie<strong>de</strong>ne Zellen (entspr. Ebenen) lesend zugegriffen. Da<br />
diese Verän<strong>de</strong>rung nur die Vaterzelle betrifft, welcher eine beson<strong>de</strong>re Position hat, ist<br />
die Verän<strong>de</strong>rung <strong>de</strong>r Verbindungen abhängig von <strong>de</strong>m eigenen Ort innerhalb <strong>de</strong>s GCA.<br />
Zusätzlich ergibt sich hier eine Abhängigkeit <strong>de</strong>r Verbindungen <strong>de</strong>r Zelle von <strong>de</strong>r Zeit.<br />
Mit Hilfe dieser Betrach<strong>tu</strong>ngen ergeben sich folgen<strong>de</strong> Abhängigkeitsfunktionen:<br />
d’= f(t,p,d,d*) und p’ = g(t,l,p*,d).<br />
64
3.2.1.3. Der Kruskal auf <strong>de</strong>m GCA<br />
Die Hauptarbeit <strong>de</strong>s Kruskals, das Sortieren <strong>de</strong>r Kanten nach ihrem Kantengewicht, wur<strong>de</strong><br />
im vorherigen Abschnitt erklärt. Nun gilt es noch, die Wäl<strong>de</strong>rverwal<strong>tu</strong>ng geschickt auf<br />
<strong>de</strong>m GCA zu mo<strong>de</strong>llieren. Prinzipiell kann diese Verwal<strong>tu</strong>ng in einem Vektor geschehen,<br />
in <strong>de</strong>m für je<strong>de</strong>n Knoten die Nummer steht, welche <strong>de</strong>n Wald repräsentiert, zu <strong>de</strong>m <strong>de</strong>r<br />
Knoten gehört. Allerdings muss dann immer dafür gesorgt wer<strong>de</strong>n, dass eine neu aufgenommene<br />
Kante auch dazu führt, dass die ganze Liste ak<strong>tu</strong>alisiert wird, da alle Knoten<br />
eines Wal<strong>de</strong>s <strong>de</strong>n gleichen Repräsentanten haben müssen. Dies wür<strong>de</strong> im ungünstigsten<br />
Fall O(n) Schritte benötigen, weshalb anzuraten ist, sich Gedanken darüber zu machen,<br />
wie diese Information geschickter abgespeichert wer<strong>de</strong>n kann.<br />
Wird je<strong>de</strong>r Knoten durch eine Zelle repräsentiert, so kann dafür gesorgt wer<strong>de</strong>n, dass er<br />
eine Verbindung zu allen Knoten herstellt, die zum gleichen Wald wie er gehören. Wird<br />
dann <strong>de</strong>r Wert eines Knoten <strong>de</strong>s Wal<strong>de</strong>s geän<strong>de</strong>rt, da zwei Wäl<strong>de</strong>r zusammengefasst<br />
wur<strong>de</strong>n, stellen alle benachbarten Zellen dies fest und übernehmen die Verän<strong>de</strong>rung.<br />
Diese Art <strong>de</strong>r Verwal<strong>tu</strong>ng ist sehr verbindungsaufwendig, da im schlimmsten Fall je<strong>de</strong><br />
einen Knoten repräsentieren<strong>de</strong> Zelle eine Verbindung zu an<strong>de</strong>ren Zellen <strong>de</strong>r gleichen Art<br />
benötigt. Dies ergibt einen Bedarf von n 2 Lei<strong>tu</strong>ngen allein für die Informationsverwal<strong>tu</strong>ng.<br />
Wie beim Warshall-Algorithmus bietet sich hier jedoch ein Bus an, um das Problem zu<br />
umgehen. Je<strong>de</strong>r Knoten speichert für sich nur, zu welchem Wald er gehört. Muss dieser<br />
Wert geän<strong>de</strong>rt wer<strong>de</strong>n, so wird auf <strong>de</strong>n Bus einerseits <strong>de</strong>r Wert i gelegt, <strong>de</strong>r geän<strong>de</strong>rt<br />
wer<strong>de</strong>n soll, und an<strong>de</strong>rerseits <strong>de</strong>r neue Wert j, auf <strong>de</strong>n <strong>de</strong>r Wert i gesetzt wer<strong>de</strong>n soll.<br />
Somit kann man mit einem Bus <strong>de</strong>r Größe 2∗log 2 (n) auskommen, wobei n die Anzahl <strong>de</strong>r<br />
Knoten beschreibt. Je<strong>de</strong>r Knoten braucht ebensoviele Lei<strong>tu</strong>ngen, mit <strong>de</strong>nen er lesend auf<br />
<strong>de</strong>n Bus zugreifen kann. Zu<strong>de</strong>m benötigt man noch eine Zelle, welche <strong>de</strong>n eigentlichen<br />
Kruskal ausführt und somit schreibend auf <strong>de</strong>n Bus und lesend auf alle Informationszellen<br />
zugreifen können muss.<br />
Hat diese Zelle sich eine Kante von <strong>de</strong>r Wurzelzelle <strong>de</strong>s Heaps geholt, so liest sie die<br />
Werte <strong>de</strong>r bei<strong>de</strong>n beteiligten Knoten aus. Sind die die Nummern <strong>de</strong>s Wal<strong>de</strong>s, zu <strong>de</strong>m<br />
die Knoten gehören, gleich, so wird die Kante verworfen. Ansonsten wird die größere<br />
<strong>de</strong>r bei<strong>de</strong>n Nummern durch die kleinere ersetzt, in<strong>de</strong>m auf die oberen log 2 (n) Lei<strong>tu</strong>ngen<br />
<strong>de</strong>s Busses <strong>de</strong>r größere und auf die unteren log 2 (n) Lei<strong>tu</strong>ngen <strong>de</strong>r kleinere Wert gelegt<br />
wird. Im nächsten Takt ak<strong>tu</strong>alisieren sich dann alle Zellen, die zum Wald <strong>de</strong>s größeren<br />
Wertes gehören. Innerhalb dieses Takts liest die rechnen<strong>de</strong> Zelle die nächste Kante.<br />
Dieser Vorgang wird solange wie<strong>de</strong>rholt, bis entwe<strong>de</strong>r nur noch ein Baum vorhan<strong>de</strong>n ist<br />
o<strong>de</strong>r keine Kanten mehr vorhan<strong>de</strong>n sind (Wurzel <strong>de</strong>s Heaps auf inaktiv gesetzt). Um<br />
festzustellen, ob alle Knoten verbun<strong>de</strong>n sind, muss die rechnen<strong>de</strong> Zelle nur einen Zähler<br />
mitführen, <strong>de</strong>r je<strong>de</strong>s Mal inkrementiert wird, wenn eine Kante aufgenommen wird. Ein<br />
Baum mit n Knoten hat genau n − 1 Kanten und <strong>de</strong>mnach kann aufgehört wer<strong>de</strong>n,<br />
wenn diese n − 1 Kanten gefun<strong>de</strong>n wur<strong>de</strong>n. Der schematisierte Aufbau <strong>de</strong>s Kruskals ist<br />
in Abbildung 3.35 zu sehen.<br />
Mit <strong>de</strong>r Parallelisierung <strong>de</strong>s Kruskals kann eine Verbesserung <strong>de</strong>r Laufzeit von<br />
65
Abbildung 3.35.: Die rechnen<strong>de</strong> Zelle <strong>de</strong>s Kruskal braucht schreiben<strong>de</strong>n Zugriff auf <strong>de</strong>n<br />
Bus und lesen<strong>de</strong>n Zugriff auf alle Zellen, die Knoten repräsentieren als<br />
auch auf die Wurzel <strong>de</strong>r Heap-Zellen.<br />
O(|E|log(|E|)) auf O(|E|) erreicht wer<strong>de</strong>n. Allerdings ist die wirkliche Laufzeit <strong>de</strong>s Kruskal<br />
direkt vom ak<strong>tu</strong>ellen Problem abhängig, da nicht nötigerweise alle Kanten überprüft<br />
wer<strong>de</strong>n müssen. Die benötigten Zellen <strong>de</strong>s Heap-Algorithmus sind abhängig von <strong>de</strong>r Anzahl<br />
<strong>de</strong>r Kanten, da diese sortiert wer<strong>de</strong>n. Um die Laufzeitverbesserung zu erreichen,<br />
wur<strong>de</strong>n log 2 (|E|) Zellen für <strong>de</strong>n Heap sowie |V |+1 Zellen für <strong>de</strong>n Kruskal und die Informationsverwal<strong>tu</strong>ng<br />
investiert. Der Heap benötigt 3 ∗ (log 2 (|E|) − 1) − 1 unidirektionale<br />
Verbindungen 11 und <strong>de</strong>r eigentliche Kruskal noch einmal (2 ∗ log 2 (|V |) ∗ |V |) + (|V | + 1)<br />
unidirektionale Verbindungen sowie einen Bus <strong>de</strong>r Breite 2 ∗ log 2 (|V |).<br />
Ist es nicht nötig, dass die Kruskal-Zelle im nächsten Schritt ihre Werte schon wie<strong>de</strong>r<br />
überschreiben kann, ist ein Bus nicht notwendig. Die Kruskal-Zelle legt ihren Wert nicht<br />
mehr auf <strong>de</strong>n Bus, son<strong>de</strong>rn je<strong>de</strong> Knoten-Zelle hat eine Verbindung zur Kruskal-Zelle und<br />
fragt direkt dort die Än<strong>de</strong>rungen ab. Um zu vermei<strong>de</strong>n, dass ungültige Daten abgefragt<br />
wer<strong>de</strong>n, wird ein Valid-Bit eingeführt, welches dafür sorgt, dass nur gültige Daten abgeholt<br />
wer<strong>de</strong>n. Die Kruskal-Zelle schreibt nun <strong>de</strong>n zu än<strong>de</strong>rn<strong>de</strong>n Wert in die Variable<br />
Change und <strong>de</strong>n Wert, durch <strong>de</strong>n ersetzt wird, in die Variable Keep. Je<strong>de</strong> Knoten-Zelle,<br />
<strong>de</strong>ren Waldnummer mit <strong>de</strong>m Wert aus Change übereinstimmt, setzt diesen nun auf <strong>de</strong>n<br />
Wert aus Keep. Damit kann ein Takt gespart wer<strong>de</strong>n, in <strong>de</strong>m sonst die Kruskal-Zelle<br />
auf <strong>de</strong>n Bus geschrieben hätte. Da alle Zellen nur lesen<strong>de</strong>n Zugriff auf die Kruskal-Zelle<br />
erhalten, entstehen bei diesem Vorgehen keine Konflikte. Der Aufbau wird in 3.36 veranschaulicht.<br />
Wenn es wie im Fall <strong>de</strong>s Warshall-Algorithmus keine ausgezeichnete Zelle gibt, auf die<br />
alle Zellen lesend zugreifen können, dann muss eine Extra-Zelle genutzt wer<strong>de</strong>n, die<br />
11 Zwischen allen Ebenen <strong>de</strong>s Baumes wird eine Verbindung nach oben und eine nach unten benötigt.<br />
Bei log 2 (|E|) Ebenen sind dies 2∗log 2 (|E|)−2 Verbindungen. Zu<strong>de</strong>m wird noch eine Verbindung von<br />
<strong>de</strong>r Wurzelzelle zu allen an<strong>de</strong>ren Zellen benötigt; dies sind noch einmal log 2 (|E|) − 2 Verbindungen.<br />
66
Abbildung 3.36.: Die rechnen<strong>de</strong> Zelle <strong>de</strong>s Kruskal braucht lesen<strong>de</strong>n Zugriff auf alle Zellen,<br />
die Knoten repräsentieren, sowie die Wurzel <strong>de</strong>r Heap-Zellen. Die<br />
Knoten-Zellen holen sich die Werte Valid, Change und Keep von <strong>de</strong>r<br />
Kruskal-Zelle und wissen dann, ob ihre Variable Waldnr. geän<strong>de</strong>rt wer<strong>de</strong>n<br />
muss.<br />
Abbildung 3.37.: Die rechnen<strong>de</strong> Zelle <strong>de</strong>s Kruskal braucht lesen<strong>de</strong>n Zugriff auf alle Zellen,<br />
die Knoten repräsentieren, sowie die Wurzel <strong>de</strong>r Heap-Zellen. Die<br />
Bus-Zelle holt die Werte Valid, Change und Keep von <strong>de</strong>r Kruskal-<br />
Zelle und speichert diese. Die Knoten-Zellen können dann diese Werte<br />
aus <strong>de</strong>r Bus-Zelle auslesen.<br />
<strong>de</strong>n Bus ersetzt. Die Extra-Zelle hat lesen<strong>de</strong>n Zugriff auf je<strong>de</strong> Zelle, die im Laufe <strong>de</strong>r<br />
Berechnungen einen Wert bereitstellt. Im Beispiel <strong>de</strong>s Kruskal-Algorithmus betrifft dies<br />
nur die eine Zelle und solch ein Aufbau sähe wie in 3.37 aus. Die Bus-Zelle holt <strong>de</strong>n Wert<br />
67
von <strong>de</strong>r betreffen<strong>de</strong>n Zelle ab und alle Zellen, welche auf diesen Wert zugreifen müssen,<br />
können <strong>de</strong>n Wert dann bei dieser Zelle auslesen. Der Aufbau spart im Falle <strong>de</strong>s Kruskal<br />
keine Lei<strong>tu</strong>ngen ein, son<strong>de</strong>rn im Vergleich mit <strong>de</strong>m oben beschriebenen Aufbau wer<strong>de</strong>n<br />
sogar mehr Lei<strong>tu</strong>ngen benötigt, da die Bus-Zelle lesend auf die Kruskal-Zelle zugreifen<br />
muss.<br />
Betrachtet man aber als Vergleich <strong>de</strong>n Warshall-Algorithmus, so wür<strong>de</strong> bei einer lesen<strong>de</strong>n<br />
Verbindung von je<strong>de</strong>r <strong>de</strong>r n Zellen zu je<strong>de</strong>r Zelle mit n Lei<strong>tu</strong>ngen insgesamt (n − 1) ∗ n 2<br />
Lei<strong>tu</strong>ngen benötigt. Im Aufbau mit <strong>de</strong>r Bus-Zelle benötigt man allerdings nur von je<strong>de</strong>r<br />
<strong>de</strong>r n Zellen n Lei<strong>tu</strong>ngen zum lesen<strong>de</strong>n Zugriff auf die Bus-Zelle und n Lei<strong>tu</strong>ngen von<br />
<strong>de</strong>r Bus-Zelle zu je<strong>de</strong>r an<strong>de</strong>ren Zelle. Insgesamt sind dies dann (n+1) ∗n Lei<strong>tu</strong>ngen und<br />
es ergibt sich eine Ersparnis um <strong>de</strong>n Faktor n.<br />
Die Komplexität eines Algorithmus wird sich bei <strong>de</strong>r Verwendung <strong>de</strong>r Bus-Zelle gegenüber<br />
<strong>de</strong>m Bus nicht än<strong>de</strong>rn, da die gleiche Anzahl an Schritten wie bei <strong>de</strong>r Verwendung<br />
<strong>de</strong>s Busses benötigt wer<strong>de</strong>n. Der einzige Unterschied besteht darin, dass auf<br />
<strong>de</strong>n Bus etwas geschrieben wer<strong>de</strong>n muss, während die Bus-Zelle sich aktiv <strong>de</strong>n ak<strong>tu</strong>ellen<br />
Wert holt.<br />
Je nach Aufgabenstellung und Aufbau kann eine <strong>de</strong>r bei<strong>de</strong>n Lösungen Vorteile gegenüber<br />
<strong>de</strong>r an<strong>de</strong>ren haben. Die Verwendung <strong>de</strong>r Bus-Zelle abstrahiert das Verbindungsnetzwerk<br />
und passt somit even<strong>tu</strong>ell besser in das Mo<strong>de</strong>ll <strong>de</strong>s GCA. Im Prinzip wur<strong>de</strong>n die gleichen<br />
Überlegungen wie in Kapitel 3.1.3.4.1 angestellt, bloß dass hier versucht wird, <strong>de</strong>n Bus<br />
einzusparen, ohne dabei mehr Lei<strong>tu</strong>ngen zu benötigen.<br />
Die Einordnung <strong>de</strong>s Algorithmus in das in Kapitel 2.3.4 vorgestellte Schema bedarf in<br />
<strong>de</strong>m Fall <strong>de</strong>s Kruskals (Heap-Sort wur<strong>de</strong> bereits getrennt betrachtet) nur noch einer<br />
Betrach<strong>tu</strong>ng <strong>de</strong>r Daten. Die Verbindungen sind in <strong>de</strong>r Lösung statisch und somit ist für<br />
diese die Funktion p’ = g().<br />
Die Daten <strong>de</strong>r einzelnen Zellen sind sowohl von <strong>de</strong>n Daten <strong>de</strong>r eigenen Zelle als auch von<br />
<strong>de</strong>n Daten <strong>de</strong>r Nachbar-Zellen abhängig. So ist die Kruskal-Zelle auf die oberste Zelle<br />
<strong>de</strong>s Heaps angewiesen, dass sie von dort die Daten erhält. Gleichzeitig benötigt sie auch<br />
die Daten <strong>de</strong>r Knoten-Zellen um festzustellen, ob zwei Wäl<strong>de</strong>r verbun<strong>de</strong>n wer<strong>de</strong>n o<strong>de</strong>r<br />
ein Zyklus entsteht.<br />
Von <strong>de</strong>r eigenen Position o<strong>de</strong>r <strong>de</strong>r Zeit sind die Daten nicht abhängig. Auch von <strong>de</strong>n<br />
Verbindungen an sich sind die Daten nicht abhängig.<br />
Es ergibt sich also als Funktion für die Daten: d’ = f(d,d*).<br />
3.2.2. Modifikation <strong>de</strong>s Hirschberg für minimal aufspannen<strong>de</strong><br />
Bäume<br />
Der Hirschberg bestimmt die zusammenhängen<strong>de</strong>n Komponenten für einen Graphen, in<strong>de</strong>m<br />
er die Knoten als eine Menge von Wäl<strong>de</strong>rn auffasst und die Kanten sucht, die Wäl<strong>de</strong>r<br />
verbin<strong>de</strong>n. Der Algorithmus leistet also im Grun<strong>de</strong> schon das Gefor<strong>de</strong>rte. Allerdings wer<strong>de</strong>n<br />
momentan noch nicht die verwen<strong>de</strong>ten Kanten gespeichert und auch die Auswahl<br />
68
geschieht noch nicht nach <strong>de</strong>r minimalen Kantenwer<strong>tu</strong>ng. Um dies zu ermöglichen, muss<br />
einerseits <strong>de</strong>r Matrix erlaubt wer<strong>de</strong>n, auch Werte größer als eins zu speichern 12 . An<strong>de</strong>rerseits<br />
muss dafür gesorgt wer<strong>de</strong>n, dass die verwen<strong>de</strong>ten Kanten gespeichert wer<strong>de</strong>n.<br />
Um die Kanten zu speichern, muss man zuerst betrachten, nach welchem Schritt die<br />
Kanten feststehen und wieviele und welche Informationen bis dahin verloren gegangen<br />
sind.<br />
Als erstes stellt man fest, dass nach <strong>de</strong>m zweiten Teilschritt die Wahl <strong>de</strong>r Kanten abgeschlossen<br />
ist. Die darauf folgen<strong>de</strong>n Teilschritte drei bis fünf dienen dazu, die Komponenten<br />
zusammenzufassen. Nach <strong>de</strong>m zweiten Teilschritt können also die Kanten in die<br />
Kantenliste <strong>de</strong>s minimalen aufspannen<strong>de</strong>n Baum aufgenommen wer<strong>de</strong>n. Um diese Kanten<br />
korrekt aufzunehmen, muss nun noch betrachtet wer<strong>de</strong>n, welche Informationen im<br />
Original-Hirschberg-Algorithmus in <strong>de</strong>n ersten bei<strong>de</strong>n Teilschritten verloren gehen. Falls<br />
diese Informationen für die minimal aufspannen<strong>de</strong>n Bäume wichtig sind, so müssen sie<br />
geeignet gesichert wer<strong>de</strong>n 13 . Anzumerken ist hier, dass <strong>de</strong>r Algorithmus, falls <strong>de</strong>r Graph<br />
nicht zusammenhängt, keinen minimalen aufspannen<strong>de</strong>n Baum, son<strong>de</strong>rn einen Wald von<br />
minimal aufspannen<strong>de</strong>n Bäumen fin<strong>de</strong>t. Eine offensichtliche Verän<strong>de</strong>rung ist, dass nun<br />
nicht mehr die Kante zur Komponente mit <strong>de</strong>r geringsten Nummer gesucht wird, son<strong>de</strong>rn<br />
die Kante zu einer benachbarten Komponente, welche die geringste Kantenwer<strong>tu</strong>ng<br />
hat.<br />
Im ersten Teilschritt wählt je<strong>de</strong>r Knoten eine Kante zu einer benachbarten Komponente<br />
und speichert die Nummer <strong>de</strong>r Komponente ab. Der Knoten, zu <strong>de</strong>m die Kante führt,<br />
geht dabei verloren. Um später auf <strong>de</strong>n Ziel-Knoten <strong>de</strong>r Kante zugreifen zu können,<br />
muss dieser abgespeichert wer<strong>de</strong>n. Somit steht <strong>de</strong>r Ziel-Knoten beim zweiten Teilschritt<br />
als Information zur Verfügung. Auch wenn diese Information nur für die Kanten wichtig<br />
ist, ist sie essentiell, da ohne die korrekten Kanten kein minimal aufspannen<strong>de</strong>r Baum<br />
gebil<strong>de</strong>t wer<strong>de</strong>n kann.<br />
Im zweiten Teilschritt wird nun für je<strong>de</strong> Komponente die Kante mit <strong>de</strong>r geringsten<br />
Wer<strong>tu</strong>ng gesucht und für die weiteren Teilschritte verwen<strong>de</strong>t. Die verwen<strong>de</strong>te Kante<br />
wird zum Repräsentanten <strong>de</strong>r Komponente umgebogen. In diesem Schritt geht also die<br />
Information verloren, von welchem Knoten die Kante ursprünglich ausging. Auch diese<br />
Information muss gespeichert wer<strong>de</strong>n.<br />
Damit sind alle nötigen Informationen gesichert, es bleibt nur noch das Problem, dass<br />
beim zweiten Schritt pro neuer Komponente auch ein Zyklus <strong>de</strong>r Länge zwei entsteht,<br />
welcher natürlich nicht in <strong>de</strong>n minimal aufspannen<strong>de</strong>n Baum aufgenommen wer<strong>de</strong>n darf.<br />
Dieser Zyklus muss erkannt wer<strong>de</strong>n und die diesen Zyklus auslösen<strong>de</strong> Kante 14 darf nur<br />
12 Um Konflikte zu vermei<strong>de</strong>n, wird hier davon ausgegangen, dass alle Werte positiv sind. Da in <strong>de</strong>n<br />
meisten Fällen minimale Kosten für irgendwelche Aktionen gesucht wer<strong>de</strong>n und die Kosten dabei<br />
nicht negativ wer<strong>de</strong>n können, ist dies eine gerechtfertigte Annahme.<br />
13 In [GR98] wird die Theorie hinter <strong>de</strong>n Modifikationen erklärt, aber welche Modifikationen explizit<br />
wirklich nötig sind, wird nicht erwähnt. Die hier vorgestellten Aktionen wer<strong>de</strong>n dort unter ”<br />
housekeeping“<br />
zusammengefasst.<br />
14 Der Zyklus entsteht dadurch, dass zwei Komponenten die gleiche Kante wählen und sie somit im<br />
gerichteten Graphen <strong>de</strong>s ersten Teilschritts in je<strong>de</strong> Rich<strong>tu</strong>ng einmal verwen<strong>de</strong>t wird.<br />
69
einmal aufgenommen wer<strong>de</strong>n. Um diesen Zyklus zu fin<strong>de</strong>n, kann man die bereits gespeicherten<br />
Informationen nutzen. So ist <strong>de</strong>r Nachfolger <strong>de</strong>s Nachfolgers eines Knotens dieses<br />
Zykluses immer wie<strong>de</strong>r <strong>de</strong>r Knoten selber. Es gilt also für je<strong>de</strong>n Knoten i eines solchen<br />
Zyklus, dass T[T[i]]=i ist. Natürlich kann man argumentieren, dass es mehr als eine minimale<br />
Kante zwischen <strong>de</strong>n bei<strong>de</strong>n Komponenten geben kann. In diesem Fall wäre nicht<br />
sicher, dass bei<strong>de</strong> Komponenten wirklich die gleiche Kante gewählt haben. Allerdings<br />
darf auch in diesem Fall nur eine von bei<strong>de</strong>n Kanten genutzt wer<strong>de</strong>n, da sonst ein Zyklus<br />
entsteht und <strong>de</strong>r entstehen<strong>de</strong> Graph kein Baum mehr ist. Da bei<strong>de</strong> Kanten minimal<br />
sind, ist es egal, welche Kante weiter verwen<strong>de</strong>t und welche verworfen wird. An dieser<br />
Stelle entsteht ein In<strong>de</strong>terminismus, <strong>de</strong>r sich aber in einer konkreten Implementierung<br />
aufgrund <strong>de</strong>r dort verwen<strong>de</strong>ten Auswahlkriterien nicht mehr auftritt.<br />
Im Weiterem wird erst ein Beispiel für die Berechnung von minimalen aufspannen<strong>de</strong>n<br />
Bäumen mit Hilfe <strong>de</strong>s Hirschberg-Algorithmus gegeben und dann wird gezeigt, dass die<br />
nötigen Informationen auch auf <strong>de</strong>m GCA gespeichert und verwen<strong>de</strong>t wer<strong>de</strong>n können.<br />
Beispiel Für dieses Beispiel wird das Beispiel aus Kapitel 3.1.3.1 dahingehend geän<strong>de</strong>rt,<br />
dass es einerseits gewichtet ist (d. h. je<strong>de</strong> Kante hat zusätzlich ein Gewicht, welches die<br />
Kosten ausdrückt) und an<strong>de</strong>rerseits noch Kanten hinzugenommen wer<strong>de</strong>n, damit <strong>de</strong>r<br />
Graph nicht schon an sich ein minimal aufspannen<strong>de</strong>r Baum ist. Als Beispiel soll also<br />
<strong>de</strong>r von <strong>de</strong>r Matrix<br />
⎛<br />
C =<br />
⎜<br />
⎝<br />
0 3 0 1 5 0 0 0<br />
3 0 1 2 0 0 2 0<br />
0 1 0 0 1 1 2 0<br />
1 2 0 0 0 0 0 1<br />
5 0 1 0 0 0 0 0<br />
0 0 1 0 0 0 0 0<br />
0 2 2 0 0 0 0 2<br />
0 0 0 1 0 0 2 0<br />
beschriebene Graph dienen. Gezeichnet gestaltet sich <strong>de</strong>r Graph wie in Abbildung 3.38.<br />
⎞<br />
⎟<br />
⎠<br />
Da <strong>de</strong>r Ablauf <strong>de</strong>s Algorithmus von Hirschberg bereits früher in dieser Arbeit erklärt<br />
wur<strong>de</strong>, beschränke ich mich hier auf die Erklärung und Abbildung <strong>de</strong>s ersten und zweiten<br />
Teilschritts je<strong>de</strong>s Durchlaufs. Diese bei<strong>de</strong>n Teilschritte sind immerhin für die Bestimmung<br />
<strong>de</strong>s minimalen aufspannen<strong>de</strong>n Baums ausschlaggebend.<br />
Abbildung 3.39 repräsentiert <strong>de</strong>n Graphen nach <strong>de</strong>m Ablauf <strong>de</strong>s ersten Schritts. Abweichend<br />
von <strong>de</strong>n Abbildungen in Kapitel 3.1.3.1 wer<strong>de</strong>n hier alle Kanten beibehalten<br />
und die von <strong>de</strong>n Knoten gewählten Kanten wer<strong>de</strong>n dadurch gekennzeichnet, dass sie<br />
gestrichelt und gerichtet dargestellt wer<strong>de</strong>n. Da in diesem Schritt je<strong>de</strong>r Knoten noch<br />
eine Komponente darstellt, wer<strong>de</strong>n diese Kanten alle genommen. Die doppelt genutzten<br />
Kanten von 0 nach 3 und von 1 nach 2 wer<strong>de</strong>n nur einmal in <strong>de</strong>n resultieren<strong>de</strong>n Graphen<br />
aufgenommen. Nach <strong>de</strong>m zweiten Schritt ergibt sich also <strong>de</strong>r Graph aus Abbildung 3.40.<br />
70
Abbildung 3.38.: Dieser ungerichtete Graph verfügt sowohl über Zyklen als auch über unterschiedliche<br />
Kantenwer<strong>tu</strong>ngen. Anhand dieses Graphen soll im weiteren<br />
<strong>de</strong>r Algorithmus ver<strong>de</strong>utlicht wer<strong>de</strong>n.<br />
Abbildung 3.39.: Je<strong>de</strong>r Knoten hat sich die Kante mit <strong>de</strong>r minimalen Kantenwer<strong>tu</strong>ng<br />
ausgewählt. Wenn mehrere solcher Kanten existieren, wird die zum<br />
Knoten mit <strong>de</strong>r kleinsten Nummer gewählt.<br />
Die in <strong>de</strong>n minimalen Spannbaum aufgenommenen Kanten sind dick und ungerichtet<br />
dargestellt. In <strong>de</strong>n nachfolgen<strong>de</strong>n Teilschritten wer<strong>de</strong>n nun die Knoten 0, 3 und 7 sowie<br />
die Knoten 1, 2, 4, 5 und 6 zu Komponenten zusammengefasst und für <strong>de</strong>n Algorithmus<br />
auch entsprechend gekennzeichnet.<br />
Im ersten Teilschritt <strong>de</strong>s zweiten Durchgangs wird dann von je<strong>de</strong>m Knoten eine Kante<br />
mit minimaler Kantenwer<strong>tu</strong>ng zu <strong>de</strong>r an<strong>de</strong>ren Komponente gesucht. Es entsteht <strong>de</strong>r<br />
Graph aus Abbildung 3.41. Die im vorhergehen<strong>de</strong>n Durchgang gefun<strong>de</strong>nen Baumkanten<br />
sind weiterhin dick, die neu gewählten Kanten sind wie<strong>de</strong>r gestrichelt dargestellt.<br />
Nun sucht je<strong>de</strong> Komponente die von ihr ausgehen<strong>de</strong> Kante mit <strong>de</strong>r minimalen Wer<strong>tu</strong>ng.<br />
Bei diesem Beispiel könnte es zu <strong>de</strong>m weiter vorne beschriebenen Problem kommen, da es<br />
<strong>de</strong>nkbar ist, dass die erste Komponente die Kante (3,1) und die zweite Komponente die<br />
71
Abbildung 3.40.: Nach <strong>de</strong>m zweiten Teilschritt sind die Kanten bereits in <strong>de</strong>n minimalen<br />
Spannbaum aufgenommen. Dabei wur<strong>de</strong> darauf geachtet, dass doppelt<br />
benutzte Kanten nur einfach aufgenommen wer<strong>de</strong>n. Die Kanten, die<br />
schon sicher im minimal aufspannen<strong>de</strong>n Baum enthalten sind, sind<br />
hier durch dicke Linien gekennzeichnet.<br />
Abbildung 3.41.: Nach <strong>de</strong>m ersten Teilschritt <strong>de</strong>s zweiten Durchlaufs sind wie<strong>de</strong>r von je<strong>de</strong>m<br />
Knoten eine Kante zu einer benachbarten Komponente ausgewählt<br />
wor<strong>de</strong>n. Da Knoten 5 und 2 über keine Verbindung zu <strong>de</strong>r an<strong>de</strong>ren<br />
Komponente verfügen, wählen sie sich keine Kante. Die bereits im minimal<br />
aufspannen<strong>de</strong>n Baum enthaltenen Kanten sind dick dargestellt,<br />
die neu gewählten Kanten wie<strong>de</strong>r gestrichelt und gerichtet.<br />
Kante(6,7) wählt. In <strong>de</strong>r praktischen Realisierung ist dies jedoch eher unwahrscheinlich,<br />
nimmt man doch in <strong>de</strong>r Regel immer <strong>de</strong>n ersten Wert, <strong>de</strong>n man fin<strong>de</strong>t, außer es lässt<br />
sich ein kleinerer fin<strong>de</strong>n. Dem folgend gehen wir auch hier davon aus, dass die Kante<br />
gewählt wird, die von <strong>de</strong>m kleineren Knoten ausgeht. Abbildung 3.42 repräsentiert <strong>de</strong>n<br />
Graphen nach <strong>de</strong>m Ablauf <strong>de</strong>s zweiten Teilschritts <strong>de</strong>s zweiten Durchlaufs. Auch hier<br />
sind wie<strong>de</strong>r alle Baumkanten dick dargestellt und es ist ersichtlich, dass <strong>de</strong>r minimale<br />
72
aufspannen<strong>de</strong> Baum gefun<strong>de</strong>n wur<strong>de</strong>. Da <strong>de</strong>r Graph am En<strong>de</strong> <strong>de</strong>s zweiten Durchlaufs<br />
auch nur noch über eine Komponente verfügt, wird sich im dritten und finalen Durchlauf<br />
nichts mehr än<strong>de</strong>rn. Es wer<strong>de</strong>n also keine Kanten mehr in <strong>de</strong>n minimalen aufspannen<strong>de</strong>n<br />
Baum aufgenommen und Abbildung 3.42 repräsentiert das En<strong>de</strong>rgbnis <strong>de</strong>s Algorithmus,<br />
wobei nur die dicken Kanten die Ausgabe darstellen.<br />
Abbildung 3.42.: Dieser Graph repräsentiert nicht nur das Teilergebnis nach <strong>de</strong>m zweiten<br />
Schritt <strong>de</strong>s zweiten Durchlaufs, son<strong>de</strong>rn er stellt auch gleichzeitig<br />
das En<strong>de</strong>rgebnis dar. Alle dick dargestellten Kanten gehören zum minimal<br />
aufspannen<strong>de</strong>n Baum und wer<strong>de</strong>n als Ergebnis zurückgegeben.<br />
3.2.2.1. Realisierung auf <strong>de</strong>m GCA<br />
Der erste Schritt zur Modifikation ist die Eingabe eines gewichteten Graphen, was aber<br />
kein Problem darstellt, da man nur innerhalb <strong>de</strong>r Matrix auch Werte größer eins erlauben<br />
muss. Der GCA unterliegt in dieser Rich<strong>tu</strong>ng keinerlei Beschränkung und diese<br />
Modifikation kann einfach übernommen wer<strong>de</strong>n. Je<strong>de</strong> Zelle (außer <strong>de</strong>r Speicherzelle)<br />
beinhaltet weiterhin eine Zeile <strong>de</strong>r Matrix. Eine weitere Modifikation ist, dass nicht<br />
mehr nach <strong>de</strong>r Kante zu <strong>de</strong>m Nachbarn mit <strong>de</strong>m kleinsten Knoten, son<strong>de</strong>rn nach <strong>de</strong>r<br />
Kante mit <strong>de</strong>r kleinsten Wer<strong>tu</strong>ng zur an<strong>de</strong>ren Komponente gesucht wird. Dies ist eine<br />
algorithmische Än<strong>de</strong>rung, die keine Verän<strong>de</strong>rungen im Aufbau <strong>de</strong>s GCA, son<strong>de</strong>rn nur<br />
im darauf ablaufen<strong>de</strong>n Programm, bedingt.<br />
Des weiteren muss im ersten Schritt gespeichert wer<strong>de</strong>n, wohin die gewählte Kante führt.<br />
Dieser Wert kann ohne Anfrage bei <strong>de</strong>r Speicherzelle gefun<strong>de</strong>n wer<strong>de</strong>n, da einfach die<br />
Stelle <strong>de</strong>s gewählten Werts in <strong>de</strong>r Zeile gespeichert wer<strong>de</strong>n muss. Im vorher vorgestellten<br />
Beispiel wird dabei von 0 beginnend gezählt (also 0,1,...,i-1). Zusätzlich sollte gespeichert<br />
wer<strong>de</strong>n, welche Kosten die gewählte Kante hat. Um im zweiten Schritt auf diese Werte<br />
zugreifen zu können, wer<strong>de</strong>n sie mit in die Speicherzelle übernommen.<br />
Auch im zweiten Schritt darf nicht mehr nach <strong>de</strong>r kleinsten Knotennummer ausgewählt<br />
wer<strong>de</strong>n, son<strong>de</strong>rn es muss eine Selektion nach <strong>de</strong>r kleinsten Kantenwer<strong>tu</strong>ng geschehen.<br />
73
Dabei muss wie<strong>de</strong>r mitgespeichert wer<strong>de</strong>n, welcher Knoten eigentlich die Kante ausgewählt<br />
hat. Ist dies geschehen, so kann die Kante in <strong>de</strong>n minimal aufspannen<strong>de</strong>n Baum<br />
aufgenommen wer<strong>de</strong>n. Stellt man sich nun vor, dass die Speicherzelle am En<strong>de</strong> <strong>de</strong>n minimal<br />
aufspannen<strong>de</strong>n Baum enthält, so erscheint es sinnvoll, dort die Kanten entwe<strong>de</strong>r<br />
als Tripel (i,j,k) mit i = Startknoten, j = Zielknoten und k = Kantenwer<strong>tu</strong>ng o<strong>de</strong>r als<br />
Matrix zu speichern. Für <strong>de</strong>n Fall, dass die Tripel verwen<strong>de</strong>t wer<strong>de</strong>n, sollte das Ergebnis<br />
als Menge von ungerichteten Kanten interpretiert wer<strong>de</strong>n. Eine Zelle kann nun mit <strong>de</strong>m<br />
Wissen, welcher Knoten die Kante ausgewählt hat, bei <strong>de</strong>r Speicherzelle sowohl das Ziel<br />
<strong>de</strong>r Kante als auch <strong>de</strong>ren Wert abfragen. Anschließend kann das Ergebnis von <strong>de</strong>r Speicherzelle<br />
ausgelesen und geeignet abgespeichert wer<strong>de</strong>n. Bei diesem Algorithmus wird<br />
beibehalten, dass <strong>de</strong>r Knoten mit <strong>de</strong>r kleinsten Nummer <strong>de</strong>r Repräsentant <strong>de</strong>r Gruppe<br />
ist. Die Kante, die von <strong>de</strong>m Repräsentanten <strong>de</strong>r neu entstan<strong>de</strong>nen Komponente ausgeht,<br />
muss nun noch entfernt wer<strong>de</strong>n, damit keine Zyklen entstehen. Dieser Schritt erspart<br />
die Zyklen<strong>de</strong>tektion, bewirkt aber, dass die Kanten erst nach <strong>de</strong>m fünften Teilschritt<br />
endgültig feststehen. Um zu sehen, dass dieses Vorgehen auch zum Erfolg führt, muss<br />
man sich ver<strong>de</strong>utlichen, dass am En<strong>de</strong> <strong>de</strong>s zweiten Schritts immer <strong>de</strong>r neue Repräsentant<br />
<strong>de</strong>r nach <strong>de</strong>m fünften Schritt entstehen<strong>de</strong>n Komponente in <strong>de</strong>n Zyklus involviert ist.<br />
Die Modifikationen, die am Algorithmus vorgenommen wur<strong>de</strong>n, än<strong>de</strong>rn we<strong>de</strong>r etwas an<br />
<strong>de</strong>r Laufzeitkomplexität noch an <strong>de</strong>r Anzahl <strong>de</strong>r benötigten Zellen. Es ist also möglich,<br />
<strong>de</strong>n minimal aufspannen<strong>de</strong>n Baum eines ungerichteten, gewerteten Graphen in O(log(n) 2 )<br />
mit O(<br />
n 2<br />
log(n) 2 ) Zellen fin<strong>de</strong>n.<br />
Auch bei <strong>de</strong>m modifizierten Algorithmus von Hirschberg än<strong>de</strong>rt sich (wie beim Floyd-<br />
Warshall-Algorithmus gegenüber <strong>de</strong>m Warshall-Algorithmus) nichts an <strong>de</strong>r Einordnung<br />
in das Schema aus Kapitel 2.3.4. Die Abhängigkeit <strong>de</strong>r Funktionen lässt sich also weiterhin<br />
ausdrücken mit: d’ = f(d,d*) und p’ = g().<br />
3.2.3. Derzeitiger Forschungsstand<br />
Momentan gibt es zwei <strong>Algorithmen</strong>, welche die benötigte Laufzeitkomplexität zum Auffin<strong>de</strong>n<br />
<strong>de</strong>s minimalen aufspannen<strong>de</strong>n Baum noch weiter verbessern. Dabei ist anzumerken,<br />
dass alle vorgestellten <strong>Algorithmen</strong> auf einer EREW P-RAM laufen und <strong>de</strong>shalb<br />
größeren Restriktionen als auf einer CRCW P-RAM unterliegen. Da das EREW und<br />
das CREW Prinzip eher die Möglichkeiten <strong>de</strong>s GCA wi<strong>de</strong>rspiegeln, wird hier darauf<br />
verzichtet, <strong>Algorithmen</strong> auf <strong>de</strong>r CRCW P-RAM vorzustellen.<br />
Donald Johnson und Panagiotis Metaxas stellen in [JM92] einen Algorithmus vor, <strong>de</strong>r<br />
mit einer Laufzeit von O(log(|E|) 3 2 ) bei einer Verwendung von |E|+|V | Prozessoren auskommt.<br />
Dieser Algorithmus wird als bahnbrechend betrachtet, da vorher die Meinung<br />
vertreten wur<strong>de</strong>, dass <strong>de</strong>r minimale aufspannen<strong>de</strong> Baum auf einer EREW P-RAM in<br />
schnellstens O(log(|E|) 2 ) gefun<strong>de</strong>n wer<strong>de</strong>n kann. Mit <strong>de</strong>m Algorithmus wur<strong>de</strong> das Gegenteil<br />
bewiesen und Ka Wong Chong, Yijie Han und Tak Wah Lam schafften es sogar<br />
in [CHL99] einen Algorithmus anzugeben, <strong>de</strong>r in O(log(|E|)) das Ergebnis fin<strong>de</strong>t und<br />
dabei nur O(|E| + |V |) Prozessoren verwen<strong>de</strong>t.<br />
74
Bei bei<strong>de</strong>n <strong>Algorithmen</strong> ist die Angabe <strong>de</strong>r Laufzeit kritisch zu betrachten, da die Autoren<br />
mehr Sorgfalt darauf verwen<strong>de</strong>t haben, die Korrektheit <strong>de</strong>r <strong>Algorithmen</strong> als die<br />
Korrektheit <strong>de</strong>r Laufzeit zu beweisen 15 . Davon abgesehen, bieten die <strong>Algorithmen</strong> gute<br />
Ansätze zum Bestimmen <strong>de</strong>s minimal aufspannen<strong>de</strong>n Baums, weshalb sie kurz in ihrer<br />
Funktionalität vorgestellt wer<strong>de</strong>n sollen.<br />
Der Algorithmus von Johnson und Metaxas startet wie <strong>de</strong>r Kruskal und <strong>de</strong>r Hirschberg<br />
mit einer Menge von Wäl<strong>de</strong>rn, die jeweils aus genau einem Knoten bestehen. Im<br />
Gegensatz zum Hirschberg arbeitet <strong>de</strong>r Algorithmus allerdings nicht mit einer Adjazenzmatrix,<br />
son<strong>de</strong>rn mit Adjazenzlisten, d. h. je<strong>de</strong>r Knoten kennt explizit seine Nachfolger.<br />
Damit entfällt die Suche nach Nachbarn. Entwe<strong>de</strong>r ist die Liste leer, dann existiert kein<br />
Nachbar√mehr, o<strong>de</strong>r man entnimmt das erste Element <strong>de</strong>r Liste. Der Algorithmus setzt<br />
sich aus log(n) Phasen zusammen. Je<strong>de</strong> Phase fasst dabei Wäl<strong>de</strong>r weiter zusammen, so<br />
√<br />
log(n)∗(i+1)<br />
dass nach Beendigung <strong>de</strong>r i-ten Phase je<strong>de</strong>r Wald, falls möglich, min<strong>de</strong>stens 2<br />
Knoten beinhaltet. Beim Zusammenfügen von zwei Wäl<strong>de</strong>rn wer<strong>de</strong>n zwei wichtige Operationen<br />
durchgeführt:<br />
• Die Adjazenzlisten <strong>de</strong>r bei<strong>de</strong>n zusammengefassten Wäl<strong>de</strong>r wer<strong>de</strong>n zusammengefasst.<br />
Verwen<strong>de</strong>t man hierfür Mergesort 16 , so erhält man am En<strong>de</strong> wie<strong>de</strong>r eine<br />
sortierte Adjazenzliste.<br />
• Je<strong>de</strong> Kante (u,v) innerhalb <strong>de</strong>r Adjazenzliste wird umbenannt in (p(u),p(v)), wobei<br />
p(u) und p(v) <strong>de</strong>n Repräsentanten <strong>de</strong>r Gruppen von u bzw. v bezeichnet. Interne<br />
Kanten (gleicher Start- und Endknoten) wer<strong>de</strong>n entfernt.<br />
Aufgrund dieser Verwal<strong>tu</strong>ngsarbeiten gelingt es, dass <strong>de</strong>r Algorithmus schneller abläuft<br />
als die bis dahin bekannten <strong>Algorithmen</strong> auf <strong>de</strong>r EREW P-RAM.<br />
Die Autoren von [CHL99] erweitern diesen Algorithmus noch in <strong>de</strong>r Weise, dass die<br />
Phasen parallel ablaufen. Dazu startet Phase i, sobald Phase ⌈ i ⌉ abgeschlossen ist und<br />
2<br />
beginnt, schon einmal eine Vorauswahl zu treffen. Je<strong>de</strong> Phase, die danach been<strong>de</strong>t wird,<br />
sorgt dafür, dass diese Vorauswahl verfeinert und an das En<strong>de</strong>rgebnis angenähert wer<strong>de</strong>n<br />
kann. Mit Hilfe dieser Parallelisierung schaffen es die Autoren, dass <strong>de</strong>r Algorithmus<br />
einen minimal aufspannen<strong>de</strong>n Baum in O(⌊log(n)⌋) bestimmt.<br />
3.3. NP-vollständige Probleme<br />
In die Klasse NP fallen diejenigen Probleme, für die man zwar auf einer nicht<strong>de</strong>terministischen<br />
Turingmaschine einen Lösungsalgorithmus in polynomieller Zeit gefun<strong>de</strong>n<br />
15 Die Autoren schätzen z. B. an einer Stelle <strong>de</strong>n Aufwand einer Aktion in einem Baum mit n Knoten<br />
und m Kanten mit O(log(m)) ab und fassen dies dann in O(log(n)) zusammen. Diese Aussage ist<br />
richtig, wenn man betrachtet, dass ein Graph mit n Knoten maximal n 2 Kanten hat, allerdings<br />
wer<strong>de</strong>n die Aussagen dadurch schwerer nachvollziehbar.<br />
16 Mergesort kann in O(log(n)) Zeit mit n Prozessoren zwei sortierte Listen zu einer sortierten Liste<br />
zusammenfügen.<br />
75
hat, <strong>de</strong>ren beste bislang gefun<strong>de</strong>ne Lösung auf einer <strong>de</strong>terministischen Turingmaschine<br />
jedoch exponentiell viel Zeit benötigt. Die Klasse P umfasst die <strong>Algorithmen</strong>, die auf<br />
einer <strong>de</strong>terministischen Turingmaschine in polynomieller Zeit laufen. Die Frage, ob NP<br />
= P gilt, ist zur Zeit noch ungeklärt, es wird aber vermutet, dass diese Gleichung nicht<br />
gilt. Die Theorie zu diesen Klassen ist in vielen (Lehr-) Büchern zu fin<strong>de</strong>n, u. a. auch in<br />
[Sch01].<br />
Die Klasse <strong>de</strong>r NP-vollständigen Probleme ist eine Unterklasse <strong>de</strong>r Klasse NP. Dabei<br />
lässt sich je<strong>de</strong>s NP-vollständige Problem auf je<strong>de</strong>s an<strong>de</strong>re NP-vollständige Problem reduzieren.<br />
Mit an<strong>de</strong>ren Worten, fin<strong>de</strong>t man eine effiziente Lösung für eines <strong>de</strong>r Probleme<br />
dieser Klasse, so kann man auch alle an<strong>de</strong>ren Probleme effizient lösen.<br />
Auch in <strong>de</strong>r Graphentheorie existieren einige NP-vollständige Probleme, z. B. das Graphenfärbbarkeitsproblem,<br />
das ”<br />
Travelling-Salesman“-Problem und die Bestimmung <strong>de</strong>r<br />
maximalen Clique eines Graphen. In <strong>de</strong>r Regel lassen sich solche NP-vollständige Probleme<br />
nur durch Backtracking 17 lösen und sind somit bezüglich <strong>de</strong>r Laufzeit ineffizient.<br />
Auch die Möglichkeiten, die parallele Automaten und Rechenmo<strong>de</strong>lle bieten, än<strong>de</strong>rn<br />
wenig an dieser Problematik. Trotz<strong>de</strong>m gibt es einige Unterklassen <strong>de</strong>r Probleme, für<br />
die es effiziente Lösungen gibt. Für an<strong>de</strong>re Probleme existieren Approximationsalgorithmen,<br />
die zwar kein optimales Ergebnis liefern, aber <strong>de</strong>m optimalen Ergebnis sehr nahe<br />
kommen.<br />
Die Möglichkeiten, für einige Spezialfälle eine effiziente Lösung zu fin<strong>de</strong>n, wird im Folgen<strong>de</strong>n<br />
exemplarisch am Beispiel <strong>de</strong>s Graphenfärbbarkeitsproblem erläutert.<br />
3.3.1. Das Graphenfärbbarkeitsproblem<br />
Das Graphenfärbbarkeitsproblem besteht darin, dass man versucht, einen Graphen mit<br />
einer minimalen Anzahl an Farben einzufärben. Dabei kann man entwe<strong>de</strong>r die Knoten<br />
o<strong>de</strong>r die Kanten färben, wobei darauf zu achten ist, dass keine benachbarten Knoten<br />
(Kanten) die gleiche Farbe haben.<br />
Für allgemeine Graphen ist dieses Problem nur mittels Backtracking zu lösen, was zu<br />
einer exponentiellen Laufzeit führt. Das Backtracking-Verfahren ist in Kapitel A.1 <strong>de</strong>s<br />
Anhangs beschrieben.<br />
Allerdings existieren Graphenklassen, für die sich das Graphenfärbungsproblem effizient<br />
lösen lassen, dazu gehören u. a. die bipartiten, outerplanaren und Halin-Graphen.<br />
Bipartite Graphen sind solche Graphen, <strong>de</strong>ren Knotenmenge V sich <strong>de</strong>rart in zwei Mengen<br />
zerlegen lässt, dass V 1 ∪ V 2 = V und V 1 ∩ V 2 = ∅ gilt und es zu<strong>de</strong>m nur Kanten<br />
gibt, die V 1 und V 2 verbin<strong>de</strong>n. Das heißt es existieren keine Kanten, die von V 1 nach V 1<br />
o<strong>de</strong>r von V 2 nach V 2 gehen. Diese Graphen sind bezüglich <strong>de</strong>r Knoten zweifärbar (alle<br />
17 Backtracking beschreibt eine struk<strong>tu</strong>rierte Art und Weise, Probleme per Austesten zu lösen und<br />
falsche Annahmen wie<strong>de</strong>r zurückzunehmen. Da dieses Verfahren im worst-case alle Möglichkeiten<br />
austestet, kann es keine effiziente Laufzeit haben. Eine Beschreibung <strong>de</strong>s Verfahrens ist in Kapitel<br />
A.1 zu fin<strong>de</strong>n.<br />
76
Knoten aus V 1 wer<strong>de</strong>n in einer gemeinsamen Farbe gefärbt und alle Knoten aus V 2 in<br />
einer an<strong>de</strong>ren Farbe), weshalb die Betrach<strong>tu</strong>ng <strong>de</strong>r Kanten hier interessanter ist. Die<br />
Definition von bipartiten Graphen wird in Abbildung 3.43 ver<strong>de</strong>utlicht. Abbildungsteil<br />
a) zeigt einen bipartiten Graph, in b) ist nur eine Kante zusätzlich vorhan<strong>de</strong>n, <strong>de</strong>nnoch<br />
ist es kein bipartiter Graph mehr.<br />
Abbildung 3.43.: Der Graph in a) ist bipartit, da seine Knotenmenge in zwei Teilmengen<br />
zerlegen lässt, so dass alle Kanten eine Verbindung zwischen diesen<br />
Mengen schaffen und keine Kanten zwei Knoten innerhalb einer Menge<br />
verbin<strong>de</strong>n. Aus diesem Grund ist <strong>de</strong>r Graph aus b) nicht bipartit, seine<br />
Knotenmenge lässt sich nicht in zwei Teilengen mit dieser Eigenschaft<br />
zerlegen.<br />
Outerplanare Graphen sind planare Graphen, bei <strong>de</strong>nen alle Knoten an einer gemeinsamen<br />
Fläche liegen. Planare Graphen lassen sich so darstellen, dass keine Kantenüberschneidungen<br />
existieren. Ohne Einschränkung <strong>de</strong>r Allgemeinheit wird angenommen, dass<br />
die gemeinsame Fläche <strong>de</strong>r outerplanaren Kanten die äußere Fläche ist 18 . Ein outerplanarer<br />
Graph ist in Abbildung 3.44 gegeben.<br />
Abbildung 3.44.: Der angegebene Graph ist planar (es existieren keine Kantenüberschneidungen)<br />
und alle Knoten liegen an <strong>de</strong>r Außenfläche. Damit ist<br />
<strong>de</strong>r Graph outerplanar.<br />
Halin-Graphen sind planare Graphen mit <strong>de</strong>r folgen<strong>de</strong>n Eigenschaft: Der Graph setzt sich<br />
aus einem Baum zusammen, <strong>de</strong>r keine Knoten mit <strong>de</strong>m Grad zwei enthält. Es existiert<br />
zu<strong>de</strong>m ein Zyklus, <strong>de</strong>r alle Blätter <strong>de</strong>s Baums und keine inneren Knoten beinhaltet. Ein<br />
möglicher Halin-Graph ist in Abbildung 3.45 abgebil<strong>de</strong>t.<br />
18 Es existieren Theoreme, dass je<strong>de</strong>r planare Graph, <strong>de</strong>ssen Knoten eine gemeinsame Fläche haben, so<br />
dargestellt wer<strong>de</strong>n kann, dass diese Fläche die Außenfläche ist.<br />
77
Abbildung 3.45.: Die Baumkanten sind hervorgehoben. Der Baum beinhaltet keine Knoten<br />
mit <strong>de</strong>m Grad 2. Zu<strong>de</strong>m sind in <strong>de</strong>m Graphen alle Blätter <strong>de</strong>s<br />
Baums durch einen Zyklus verbun<strong>de</strong>n. Zusammen sorgen diese Eigenschaften<br />
dafür, dass dieser Graph ein Halin-Graph ist.<br />
Nachfolgend wird ein Algorithmus angegeben, mit <strong>de</strong>ssen Hilfe die Kanten von bipartiten<br />
Graphen gefärbt wer<strong>de</strong>n können.<br />
3.3.1.1. Kantenfärbung von bipartiten Graphen<br />
Im diesem Abschnitt sei ∆ <strong>de</strong>finiert als <strong>de</strong>r maximale Grad eines Knotens <strong>de</strong>s zu untersuchen<strong>de</strong>n<br />
Graphen. Da es also min<strong>de</strong>stens einen Knoten gibt, <strong>de</strong>ssen Grad ∆ ist, wer<strong>de</strong>n<br />
min<strong>de</strong>stens ∆ Farben benötigt, um die Kanten <strong>de</strong>s Graphen zu färben. In [GR98] wird<br />
zuerst ein Algorithmus angegeben, <strong>de</strong>r <strong>de</strong>n Graphen einfärbt für <strong>de</strong>n Fall, dass ∆ eine<br />
Zweierpotenz ist. Darauf aufbauend wur<strong>de</strong> dann erst ein Algorithmus vorgestellt, <strong>de</strong>r<br />
die Kantenfärbung für bipartite Graphen mit beliebigem ∆ ermöglicht. Im Folgen<strong>de</strong>n<br />
soll gezeigt wer<strong>de</strong>n, wie <strong>de</strong>r Algorithmus zur Färbung von bipartiten Graphen mit maximalem<br />
Knotengrad ∆ = 2 x auf <strong>de</strong>m GCA implementiert wer<strong>de</strong>n kann. Der Algorithmus<br />
zum Kantenfärben allgemeiner bipartiter Graphen hingegen wird in dieser Arbeit nicht<br />
auf <strong>de</strong>m GCA mo<strong>de</strong>lliert. An <strong>de</strong>m Algorithmus interessierte Leser wer<strong>de</strong>n an dieser Stelle<br />
auf [GR98] verwiesen.<br />
Der Algorithmus zur Färbung bipartiter Graphen mit einer Zweierpotenz als maximalen<br />
Grad (Euler-Färbung) verwen<strong>de</strong>t das parallele Divi<strong>de</strong>-and-Conquer-Prinzip (siehe A.2<br />
und B.3). Da vorausgesetzt wur<strong>de</strong>, dass ∆ eine Zweierpotenz ist, kann <strong>de</strong>r Graph in zwei<br />
Teilgraphen zerlegt wer<strong>de</strong>n, die dann auch wie<strong>de</strong>r ein ∆ haben, welches eine Zweierpotenz<br />
darstellt. Der Ausgangsgraph war bipartit, daher sind auch die Teilgraphen bipartit, da<br />
keine neuen Kanten hinzukommen, welche die Bedingungen verletzen könnten.<br />
Entschei<strong>de</strong>nd ist zunächst, wie diese Zerlegung gefun<strong>de</strong>n wer<strong>de</strong>n kann. Die Euler-Zerlegung<br />
erlaubt es <strong>de</strong>m Algorithmus, <strong>de</strong>n Graphen so zu teilen, dass bei<strong>de</strong> Teilgraphen<br />
eigenständig bearbeitet wer<strong>de</strong>n können und am En<strong>de</strong> das korrekte Ergebnis ohne großen<br />
Aufwand aus <strong>de</strong>n Teilergebnissen ermittelt wer<strong>de</strong>n kann. Unter <strong>de</strong>r Euler-Zerlegung<br />
versteht man die Aufteilung <strong>de</strong>s Graphen in mehrere kantendisjunkte Zyklen und Wege.<br />
Dabei ist je<strong>de</strong>r Knoten mit ungera<strong>de</strong>m Grad Endpunkt genau eines Wegs. Diese<br />
Euler-Zerlegung bil<strong>de</strong>t die Grundlage für die anschließend vorgenommenen Zerlegung,<br />
die darauf beruht, dass die Kanten alternierend mit 0 und 1 beschriftet wer<strong>de</strong>n. Alle<br />
78
Kanten mit <strong>de</strong>r Beschrif<strong>tu</strong>ng 0 sind im Teilgraphen G 1 , alle mit 1 im Teilgraphen G 2<br />
enthalten. Da die Euler-Zerlegung also <strong>de</strong>n Startpunkt und die Reihenfolge bestimmt, in<br />
<strong>de</strong>r die Kanten betrachtet wer<strong>de</strong>n, wird im nächsten Absatz näher darauf eingegangen,<br />
wie die Euler-Zerlegung gefun<strong>de</strong>n wer<strong>de</strong>n kann.<br />
Abbildung 3.46.: In a) ist <strong>de</strong>r Graph zu sehen, Knoten 2 hat Grad 4, somit ist ∆ eine<br />
Zweierpotenz. Teil b) zeigt die Zerlegung in die Wege und Zyklen. In<br />
Teil c) sind alle Kanten <strong>de</strong>s Graphen G 1 gestrichelt, die Kanten von<br />
G 2 sind wie gewohnt abgebil<strong>de</strong>t.<br />
Bei einem Eulerschem Graphen 19 gibt es genau einen Zyklus, <strong>de</strong>r bestimmt wer<strong>de</strong>n kann.<br />
Ist <strong>de</strong>r Graph nicht eulersch, so wird ein Zusatzknoten und von je<strong>de</strong>m Knoten mit ungera<strong>de</strong>m<br />
Knotengrad eine Kante zu diesem eingeführt. Dann wird die Euler-Zerlegung für<br />
diesen Graphen G ∗ gesucht. Die Euler-Zerlegung von G erhält man, in<strong>de</strong>m <strong>de</strong>r Zusatzknoten<br />
und alle zu ihm führen<strong>de</strong>n Kanten gelöscht wer<strong>de</strong>n. Dadurch kann es passieren,<br />
dass Zyklen zu Wegen zerfallen, aber auch diese sind in <strong>de</strong>r Euler-Zerlegung erlaubt.<br />
Betrachtet man <strong>de</strong>n Graphen aus Abbildung 3.46.a, so sieht <strong>de</strong>ssen Euler-Zerlegung wie<br />
in 3.46.b aus. Der Graph wird dann in die zwei Teilgraphen G 1 und G 2 zerlegt, in<strong>de</strong>m<br />
alle Kanten mit einer 0 als Markierung in G 1 und alle Kanten mit <strong>de</strong>r Markierung 1 in<br />
G 2 enthalten sind.<br />
Diese Zerlegung wird solange fortgesetzt, bis ∆=1 gilt. Dann können alle Kanten mit<br />
einer Farbe eingefärbt wer<strong>de</strong>n. Je<strong>de</strong>r <strong>de</strong>r Teilgraphen färbt die Kanten mit einer Nummer<br />
von 1 bis ∆ . Beim Zusammenfassen <strong>de</strong>r Teilgraphen wer<strong>de</strong>n dann die Farben im<br />
2<br />
Teilgraphen G 2 umbenannt, in<strong>de</strong>m man ∆ hinzuaddiert. Damit hat man insgesamt alle<br />
2<br />
Kanten mit einer Nummer versehen und da in G 1 und G 2 nach <strong>de</strong>r Umbenennung keine<br />
19 Man spricht davon, dass ein Graph eulersch ist o<strong>de</strong>r von einem Eulerschem Graphen, wenn es in<br />
<strong>de</strong>m Graphen einen Eulerschen Kreis gibt. Dieser Eulersche Kreis beschreibt einen Zyklus, <strong>de</strong>r je<strong>de</strong><br />
Kante <strong>de</strong>s Graphen genau einmal benutzt. Ein ungerichteter Graph kann nur dann eulersch sein,<br />
wenn alle Knoten einen gera<strong>de</strong>n Grad haben.<br />
79
gemeinsamen Farben (die Nummern entsprechen Farben) mehr existieren, ist das Gesamtergebnis<br />
eine korrekte Färbung, wenn bei<strong>de</strong> Teilgraphen korrekt gefärbt waren. In<br />
Pseudoco<strong>de</strong> wird <strong>de</strong>r Algorithmus wie folgt formuliert:<br />
Listing 3.6: Eulerfärbung für bipartite Graphen<br />
1 procedure Euler−colour (G)<br />
2 begin<br />
3 /∗ G hat maximalen Grad ∆ ∗/<br />
4 if ∆ = 1 then<br />
5 markiere a l l e Kanten in G mit <strong>de</strong>r Farbe 1<br />
6 else<br />
7 begin<br />
8 fin<strong>de</strong> eine Euler−Zerlegung von G<br />
9 mit Hilfe <strong>de</strong>r Euler−Zerlegung bil<strong>de</strong> die zwei Graphen G 1<br />
und G 2 , bei<strong>de</strong> mit maximalen Grad ∆ 2<br />
10 for G = G 1 und G = G 2 in parallel do Euler−colour (G)<br />
11 s t e l l e G aus G 1 und G 2 wie<strong>de</strong>r her , in<strong>de</strong>m die Farben aus<br />
G 2 umbenannt wer<strong>de</strong>n , damit G 1 und G 2 disjunkte<br />
Farbmengen benutzen .<br />
12 end<br />
13 end<br />
Für die Mo<strong>de</strong>llierung auf <strong>de</strong>m GCA ist es notwendig, die einzelnen Schritte genauer zu<br />
betrachten. Kann man je<strong>de</strong>n Schritt auf <strong>de</strong>m GCA mo<strong>de</strong>llieren, so ist auch <strong>de</strong>r gesamte<br />
Algorithmus auf <strong>de</strong>m GCA mo<strong>de</strong>llierbar. Nachfolgend sollen die Pseudo-Befehle aus <strong>de</strong>m<br />
obigen Programm näher erläutert und auf <strong>de</strong>m GCA mo<strong>de</strong>lliert wer<strong>de</strong>n:<br />
• In Zeile 4 <strong>de</strong>s Programms fin<strong>de</strong>t sich die Abbruchbedingung <strong>de</strong>s rekursiven Algorithmus.<br />
Um parallel testen zu können, ob alle Knoten einen maximalen Grad von<br />
eins haben, braucht man auf <strong>de</strong>m GCA für je<strong>de</strong>n Knoten eine Zelle. Je<strong>de</strong> Zelle<br />
kennt ihren eigenen Grad, <strong>de</strong>r von an<strong>de</strong>ren Zellen abgefragt wer<strong>de</strong>n kann. Sinnvoll<br />
ist an dieser Stelle auch eine Master-Zelle, die von allen Knoten-Zellen <strong>de</strong>n Grad<br />
abfragt und aufgrund <strong>de</strong>r Daten entschei<strong>de</strong>d, ob das Abbruchkriterium erfüllt ist<br />
o<strong>de</strong>r nicht. Dies kann unter an<strong>de</strong>rem dadurch geschehen, dass die Master-Zelle alle<br />
Knotengra<strong>de</strong> vero<strong>de</strong>rt und, wenn das Ergebnis größer als eins ist, entschei<strong>de</strong>t, dass<br />
das Kriterium noch nicht erfüllt ist.<br />
Ist das Abbruchkriterium erfüllt, so müssen alle im Graphen enthaltenen Kanten<br />
mit <strong>de</strong>r Farbe 1 gefärbt wer<strong>de</strong>n. Eine Färbung kann als Label an <strong>de</strong>r Kante<br />
dargestellt wer<strong>de</strong>n.<br />
In Bezug auf <strong>de</strong>n weiteren Algorithmus ist es sinnvoll, die Kanten auch als Zellen<br />
zu mo<strong>de</strong>llieren. Da aber eine Kante immer zwei Knoten verbin<strong>de</strong>t, ist es im<br />
Weiteren notwendig, für je<strong>de</strong> Kanten-Zelle die Knoten-Zelle zu bestimmen, welche<br />
im weiteren Verlauf <strong>de</strong>s Algorithmus regelt, ob und wie umgefärbt wer<strong>de</strong>n muss.<br />
80
Deswegen wird hier festgelegt, dass je<strong>de</strong> Kanten-Zelle, die (i,j) repräsentiert, lesend<br />
auf die Knoten-Zelle i zugreift.<br />
Die Knoten-Zellen haben lesen<strong>de</strong>n Zugriff auf alle Kanten-Zellen, die sie berühren,<br />
und können somit anhand ihrer Verbindungen feststellen, welchen Grad sie haben.<br />
Das Färben einer Kante gestaltet sich dann so, dass die Knoten-Zellen alle die<br />
Farbe 1 in <strong>de</strong>n Ausgabeport schreiben und die Kanten-Zellen diese lesen und ihre<br />
Kanten labeln.<br />
• In Zeile 8 fin<strong>de</strong>t sich die nächste interessante Anweisung. Diese Anweisung wird<br />
immer dann ausgeführt, wenn das Abbruchkriterium nicht erfüllt ist. Sie besteht<br />
aus mehreren Schritten, die im Pseudo-Co<strong>de</strong> nicht <strong>de</strong>utlich wer<strong>de</strong>n. Die Schritte<br />
um eine Euler-Zerlegung zu fin<strong>de</strong>n wer<strong>de</strong>n in <strong>de</strong>r folgen<strong>de</strong>n Aufzählung benannt<br />
und gleich <strong>de</strong>ren Mo<strong>de</strong>llierung auf <strong>de</strong>m GCA erläutert.<br />
Zur Ver<strong>de</strong>utlichung <strong>de</strong>r Schritte wer<strong>de</strong>n auch immer die Auswirkungen auf <strong>de</strong>n<br />
Beispielgraphen 3.47 anhand von Graphiken aufgezeigt. Wichtig ist hierbei, dass<br />
<strong>de</strong>r Graph für diesen Schritt eulersch sein muss; wie er angepasst wer<strong>de</strong>n kann,<br />
wur<strong>de</strong> bereits weiter vorne beschrieben.<br />
Da eine Dummy-Zelle nicht zur Laufzeit erzeugt wer<strong>de</strong>n kann, muss diese Zelle<br />
von vornherein vorhan<strong>de</strong>n sein. Die Dummy-Knoten-Zelle übernimmt es, je<strong>de</strong>n<br />
Knoten auf seinen Grad zu überprüfen. Haben alle Knoten einen gera<strong>de</strong>n Grad,<br />
so <strong>de</strong>aktiviert sie sich. Ansonsten geht sie in einen Zustand, welche <strong>de</strong>n Dummy-<br />
Kanten-Zellen anzeigt, dass diese sich mit einem Wert zu initialisieren haben (auch<br />
die Dummy-Kanten-Zellen müssen aktiv nach einer nötigen Verbindung suchen, da<br />
beim GCA nicht schreibend auf Zellen zugegriffen wer<strong>de</strong>n darf).<br />
Da somit eine große Anzahl even<strong>tu</strong>ell unbeschäftigter Zellen nötig ist, erscheint es<br />
sinnvoller, entwe<strong>de</strong>r nur eulersche Graphen mit Hilfe <strong>de</strong>s GCA zu bearbeiten o<strong>de</strong>r<br />
die Speicherzellen-Konstruktion zu verwen<strong>de</strong>n. Bei <strong>de</strong>r zweiten Variante wür<strong>de</strong> die<br />
Speicherzelle alle relevanten Daten abspeichern (u. a. die Kanten) und könnte einfach<br />
bei sich feststellen, wenn ein neuer Knoten und dazugehörige Kanten eingefügt<br />
wer<strong>de</strong>n müssen. Auch dieses Konstrukt ist nicht einfach, da die Knoten dann bei<br />
<strong>de</strong>r Speicherzelle abfragen müssen, ob neue Kanten für sie hinzugekommen sind<br />
o<strong>de</strong>r nicht, aber es entsteht kein so hoher Prozessorbedarf wie bei <strong>de</strong>r an<strong>de</strong>ren<br />
Möglichkeit.<br />
– Um die Euler-Zerlegung zu fin<strong>de</strong>n wird ein Teil <strong>de</strong>s Algorithmus verwen<strong>de</strong>t,<br />
<strong>de</strong>r <strong>de</strong>n Euler-Kreis eines eulerschen Graphen bestimmt. Da dieser Algorithmus<br />
einen gerichteten Graphen als Eingabe erhält und <strong>de</strong>r bislang behan<strong>de</strong>lte<br />
Graph ungerichtet ist, müssen alle Kanten durch gerichtete Kanten in bei<strong>de</strong><br />
Rich<strong>tu</strong>ngen ersetzt wer<strong>de</strong>n. Dies könnte dadurch mo<strong>de</strong>lliert wer<strong>de</strong>n, dass je<strong>de</strong><br />
Kante (i,j) repräsentieren<strong>de</strong> Zelle zusätzlich die Kante (j,i) speichert und<br />
somit eine ungerichtete Kante durch zwei gerichtete ersetzt.<br />
Allerdings ist es für die folgen<strong>de</strong>n Schritte besser, wenn je<strong>de</strong> gerichtete Kante<br />
durch eine eigene Zelle repräsentiert wird. Dazu wer<strong>de</strong>n |E| zusätzliche Zellen<br />
81
Abbildung 3.47.: Ein einfacher bipartiter Graph, <strong>de</strong>r in <strong>de</strong>n folgen<strong>de</strong>n Schritten dazu<br />
verwen<strong>de</strong>t wer<strong>de</strong>n soll, <strong>de</strong>n Algorithmus zu ver<strong>de</strong>utlichen.<br />
Abbildung 3.48.: Die Kanten <strong>de</strong>s Graphen wur<strong>de</strong>n alle durch zwei gerichtete Kanten ersetzt.<br />
Dieser Vorgang ist auch noch einmal anhand <strong>de</strong>r Zellen ver<strong>de</strong>utlicht.<br />
Für je<strong>de</strong> Kante und je<strong>de</strong>n Knoten existiert eine repräsentieren<strong>de</strong><br />
Zelle.<br />
benötigt, die jeweils die Knoten einer Kante abfragen und sie in umgekehrter<br />
Reihenfolge speichern. Wichtig ist, dass dies struk<strong>tu</strong>riert geschieht, damit die<br />
Knoten-Zellen auch lesend auf diese Zellen zugreifen können.<br />
Zu<strong>de</strong>m muss je<strong>de</strong> neue Kanten-Zelle eine lesen<strong>de</strong> Verbindung zu bei<strong>de</strong>n Knoten-<br />
Zellen, die sie berührt, aufbauen. Das ist eine Abweichung zu <strong>de</strong>r vorher getroffenen<br />
Aussage, ist aber für diesen Schritt sehr hilfreich und kann ohne<br />
weiteres realisiert wer<strong>de</strong>n. Die für <strong>de</strong>n vorhergehen<strong>de</strong>n Schritt unnötige lesen<strong>de</strong><br />
Verbindung wird dort einfach <strong>de</strong>aktiviert (nicht genutzt). Das Ergebnis<br />
dieses Schritts ist in Abbildung 3.48 graphisch dargestellt.<br />
– Damit eine Kante <strong>de</strong>s Original-Graphen auch nur ein Label erhält und damit<br />
<strong>de</strong>r Euler-Algorithmus erfolgreich ist, muss nun eine <strong>de</strong>r bei<strong>de</strong>n gerichteten<br />
Kanten eliminiert wer<strong>de</strong>n. Die Auswahl, welche <strong>de</strong>r bei<strong>de</strong>n gerichteten Kanten<br />
eliminiert wer<strong>de</strong>n muss, geschieht in diesem und <strong>de</strong>n nächsten Schritten.<br />
Zunächst wird die Liste <strong>de</strong>r Kanten sortiert. In vorangehen<strong>de</strong>n Abschnitten<br />
wur<strong>de</strong> bereits <strong>de</strong>r Heap-Sort als ein möglicher Sortier-Algorithmus vorgestellt,<br />
82
Abbildung 3.49.: Die Kanten wur<strong>de</strong>n in diesem Schritt sortiert. An <strong>de</strong>n Verbindungen<br />
zu <strong>de</strong>n Knoten än<strong>de</strong>rt sich nichts.<br />
allerdings hat dieser auch in <strong>de</strong>r parallelen Variante eine Laufzeit von O(n).<br />
In [GR98] wer<strong>de</strong>n parallele Sortieralgorithmen vorgestellt, die in O(log(n))<br />
sortieren. Da dies ein <strong>de</strong>utlich besseres Laufzeitverhalten ist, sollte ein entsprechen<strong>de</strong>r<br />
Sortieralgorithmus gewählt wer<strong>de</strong>n.<br />
Zum Sortieren wird in [GR98] Coles Sortieralgorithmus vorgeschlagen. Allerdings<br />
wird in [Nat90] sehr anschaulich illustriert, dass dieser Sortieralgorithmus<br />
zwar eine Laufzeitkomplexität von O(log(n)) hat, aber in <strong>de</strong>r konkreten<br />
Implementierung eine schlechtere Laufzeit aufweist als das bitonische Sortieren<br />
mit einer Laufzeit von O(log 2 (n)). Da sich dort <strong>de</strong>r Sortieralgorithmus<br />
von Cole bis zu einer Eingabegröße von n = 2 69 schlechter als das bitonische<br />
Sortieren verhält (dabei wur<strong>de</strong>n manche Verbesserungsmöglichkeiten <strong>de</strong>s bitonischen<br />
Sortierens bewusst nicht genutzt), wird hier darauf verzichtet, <strong>de</strong>n<br />
Algorithmus von Cole auf <strong>de</strong>m GCA zu implementieren. Dies erscheint vor<br />
allem <strong>de</strong>swegen sinnvoll, da das bitonische Sortieren bereits in [Hee01] sehr<br />
anschaulich und effizient auf <strong>de</strong>m GCA mo<strong>de</strong>lliert wur<strong>de</strong>.<br />
Die einzige Aufgabe besteht nun darin, <strong>de</strong>n Komperator <strong>de</strong>rgestalt anzupassen,<br />
dass eine Kante (i,j) genau dann als kleiner als eine Kante (k,l) <strong>de</strong>finiert<br />
wird, wenn i < k∨(i = k∧j < l) gilt. In Abbildung 3.49 wur<strong>de</strong> die Sortierung<br />
<strong>de</strong>r Kanten für <strong>de</strong>n Beispielgraph vorgenommen.<br />
– Mit Hilfe <strong>de</strong>r Sortierung wer<strong>de</strong>n nun die Successoren (Nachfolger) <strong>de</strong>r Kanten<br />
bestimmt. Da die Kanten untereinan<strong>de</strong>r über keine Verbindungen verfügen,<br />
muss die Zuweisung <strong>de</strong>s Successors von <strong>de</strong>n Knoten-Zellen aus erfolgen. Je<strong>de</strong><br />
Knoten-Zelle bestimmt anhand <strong>de</strong>r von ihr ausgehen<strong>de</strong>n Kanten die Successoren<br />
<strong>de</strong>r in sie eingehen<strong>de</strong>n Kanten (momentan wird auf einem gerichteten<br />
Graphen gearbeitet, <strong>de</strong>swegen sind die Kanten so am einfachsten zu<br />
beschreiben). Die Kanten-Zellen, welche die eingehen<strong>de</strong>n Kanten repräsentieren,<br />
können dann nach <strong>de</strong>r Berechnung ihren Successor bei <strong>de</strong>r Knoten-Zelle<br />
auslesen und abspeichern.<br />
Um auszurechnen, welche eingehen<strong>de</strong> Kante welche ausgehen<strong>de</strong> Kante zugewiesen<br />
bekommt, wer<strong>de</strong>n die sortierten Kanten betrachtet. Alle Kanten, die<br />
vom Knoten i ausgehen, sind in dieser sortierten Kantenfolge hintereinan<strong>de</strong>r<br />
angeordnet. Da <strong>de</strong>r Knoten lesen<strong>de</strong>n Zugriff auf alle an ihm anliegen<strong>de</strong>n Kanten<br />
hat, kann davon ausgegangen wer<strong>de</strong>n, dass seine Verweise auch in einer<br />
Art abgespeichert sind, dass sie <strong>de</strong>r Sortierung entsprechen.<br />
83
Abbildung 3.50.: Für je<strong>de</strong> Kantenzelle wur<strong>de</strong> <strong>de</strong>r Successor bestimmt. Die Zelle hat sich<br />
ihren Successor nicht nur gespeichert, son<strong>de</strong>rn auch eine Verbindung<br />
zu ihm aufgebaut.<br />
Da <strong>de</strong>r Graph eulersch ist, hat je<strong>de</strong>r Knoten i eine gera<strong>de</strong> Anzahl ausgehen<strong>de</strong>r<br />
Kanten, welche mit (i,v 0 )...(i,v k ) bezeichnet wer<strong>de</strong>n. Die v l (die Durchnummerierung<br />
folgt <strong>de</strong>r Sortierung und je<strong>de</strong>s v l bezeichnet einen Endknoten<br />
einer Kante) sind wichtig, da sie bestimmen, welche eingehen<strong>de</strong> Kante<br />
und welche ausgehen<strong>de</strong> Kante ein Successor-Paar bil<strong>de</strong>n. Für je<strong>de</strong>s ungera<strong>de</strong><br />
l berechnet <strong>de</strong>r Knoten i nun SUCCESSOR((v l ,i)) ← (i,v l+1 ) und<br />
SUCCESSOR((v l+1 ,i)) ← (i,v l ).<br />
Dabei bil<strong>de</strong>n die letzte Kante <strong>de</strong>r Liste und die erste Kante <strong>de</strong>r Liste ein Paar<br />
(dies geschieht in <strong>de</strong>r Berechnung, in<strong>de</strong>m einfach modulo <strong>de</strong>r Anzahl <strong>de</strong>r<br />
ausgehen<strong>de</strong>n Kanten gerechnet wird). Sinnvoll ist bei diesem Schritt, dass die<br />
Kanten-Zellen sich nicht nur <strong>de</strong>n Wert ihres Successors abspeichern, son<strong>de</strong>rn<br />
auch eine Verbindung zu <strong>de</strong>r entsprechen<strong>de</strong>n Zelle herstellen. Betrachtet man<br />
nur die Zellen nach <strong>de</strong>m Ablauf <strong>de</strong>s Schritts, so ergibt sich Abbildung 3.50.<br />
– Nun wird die Doubling-Technik auf die Kanten-Zellen angewen<strong>de</strong>t und gleichzeitig<br />
speichert die Kanten-Zelle einen Wert. Dieser Wert gibt am En<strong>de</strong> <strong>de</strong>s<br />
Schritts die kleinste Kante <strong>de</strong>s Zyklusses an, zu <strong>de</strong>m die Kanten-Zelle gehört.<br />
Je<strong>de</strong> Kanten-Zelle berechnet <strong>de</strong>swegen:<br />
V alue ← min{eigener V alue,V alue <strong>de</strong>s Successors}.<br />
Initialwert <strong>de</strong>r Kanten-Zelle ist die eigene Kante. Nach<strong>de</strong>m <strong>de</strong>r neue Value<br />
ausgerechnet wur<strong>de</strong>, wird die Successor-Verbindung auf <strong>de</strong>n Successor <strong>de</strong>s<br />
Successors gesetzt. Somit ist nach log(n) Schritten <strong>de</strong>r gesamte Zyklus durchlaufen<br />
und je<strong>de</strong> Kanten-Zelle enthält als Value die kleinste Kante <strong>de</strong>s eigenen<br />
Zyklusses. Die Kanten-Zellen nach Been<strong>de</strong>n <strong>de</strong>r Doubling-Technik sind in<br />
Abbildung 3.51 dargestellt.<br />
– Um nun die gerichteten Kanten auszuwählen, die die Kantenfärbung bestimmen,<br />
wird zwischen einem Kantenpaar (i,j) und (j,i) immer diejenige ausgewählt,<br />
<strong>de</strong>ren Value kleiner ist. Dabei wird die oben beschriebene Vergleichsoperation<br />
verwen<strong>de</strong>t.<br />
Dieser Vergleich kann entwe<strong>de</strong>r durch die Kanten-Zellen erfolgen, falls sie<br />
lesen<strong>de</strong>n Zugriff auf die jeweils an<strong>de</strong>re Kanten-Zelle haben, o<strong>de</strong>r durch die<br />
Knoten-Zellen. Da Knoten-Zellen lesen<strong>de</strong>n Zugriff auf alle sie berühren<strong>de</strong><br />
84
Abbildung 3.51.: Nach<strong>de</strong>m die Doubling-Technik auf die Kanten-Zellen angewen<strong>de</strong>t wur<strong>de</strong>,<br />
hat je<strong>de</strong> Zelle sich selber als Successor und <strong>de</strong>r Value steht auf <strong>de</strong>r<br />
kleinsten Kante <strong>de</strong>s Zyklusses, zu <strong>de</strong>m die Kante gehört.<br />
Abbildung 3.52.: Anhand <strong>de</strong>s Values wur<strong>de</strong> jetzt für je<strong>de</strong> Kanten-Zelle entschie<strong>de</strong>n, ob<br />
sie aktiv o<strong>de</strong>r inaktiv ist. Grau hinterlegte Kanten sind inaktiv, die<br />
an<strong>de</strong>ren aktiv.<br />
Kanten-Zellen haben, können sie <strong>de</strong>n Vergleich ohne weiteren Verbindungsaufwand<br />
durchführen. Wichtig ist, dass die Kanten-Zellen mit <strong>de</strong>m größeren<br />
Value <strong>de</strong>aktiviert wer<strong>de</strong>n und somit für die nachfolgen<strong>de</strong>n Schritte nicht mehr<br />
zur Verfügung stehen. Auch die Knoten-Zellen merken, welche Kanten-Zellen<br />
<strong>de</strong>aktiviert wur<strong>de</strong>n und ignorieren eine Verbindung dorthin. Die <strong>de</strong>aktivierten<br />
Kanten-Zellen sind in Abbildung 3.52 durch eine graue Hinterlegung gekennzeichnet.<br />
– Der letzte Schritt besteht nun darin, für die aktiven Kanten-Zellen einen<br />
Nachfolger zu fin<strong>de</strong>n, so dass die Kanten mit 0 o<strong>de</strong>r 1 gelabelt wer<strong>de</strong>n können.<br />
Hierbei ist anzumerken, dass bei diesem Beispiel zwar ein Zyklus entsteht, dies<br />
aber nicht immer so sein muss. Die einzige Garantie, die <strong>de</strong>r Algorithmus an<br />
dieser Stelle liefert, ist, dass <strong>de</strong>r Graph in kantendisjunkte Kreise und Wege<br />
zerlegt wur<strong>de</strong>. Da ein Euler-Kreis aber auch nicht für <strong>de</strong>n weiteren Verlauf<br />
<strong>de</strong>s Algorithmus notwendig ist, reicht diese Garantie.<br />
Um nun die Nachfolger zu bestimmen, müssen im Original-P-RAM-Algorithmus<br />
<strong>de</strong>r Kantenvektor und <strong>de</strong>r Successorvektor (die P-RAM arbeitet auf<br />
Vektoren) jeweils sortiert wer<strong>de</strong>n. Der Successorvektor nimmt dafür <strong>de</strong>n schon<br />
bekannten Komperator. Bei <strong>de</strong>m Kantenvektor ist eine Kante (i,j) kleiner als<br />
(k,l), wenn j < l ∨ (j = l ∧ i < k). Mit an<strong>de</strong>ren Worten, <strong>de</strong>r Kantenvektor<br />
wird nach <strong>de</strong>r zweiten Komponente je<strong>de</strong>r Kante sortiert.<br />
Auf <strong>de</strong>m GCA lässt sich dieser Schritt folgen<strong>de</strong>rmaßen realisieren: je<strong>de</strong> Knoten-<br />
Zelle kennt die an sie angrenzen<strong>de</strong>n Kanten-Zellen. Außer<strong>de</strong>m hat je<strong>de</strong>s Kante-<br />
85
Abbildung 3.53.: Je<strong>de</strong> Kanten-Zelle kennt nun ihren Successor (explizit in <strong>de</strong>r Kanten-<br />
Zelle gespeichert und als Link).<br />
Successor-Paar genau einen Knoten gemeinsam: die zweite Komponente <strong>de</strong>r<br />
Kante und die erste Komponente <strong>de</strong>s Successors. Da bei<strong>de</strong> Kanten also <strong>de</strong>r<br />
Knoten-Zelle bekannt sind, kann diese auch die Zuordnung übernehmen. Dazu<br />
muss sie intern ihre Kanten aufspalten in die Hälfte <strong>de</strong>r von ihr ausgehen<strong>de</strong>n<br />
Kanten (erste Komponente ist gleich <strong>de</strong>r Zell-ID <strong>de</strong>s Knotens) und <strong>de</strong>r eingehen<strong>de</strong>n<br />
Kanten (zweite Komponente ist gleich <strong>de</strong>r Zell-ID <strong>de</strong>s Knotens).<br />
Die ausgehen<strong>de</strong>n Kanten wer<strong>de</strong>n wie <strong>de</strong>r Successorvektor sortiert. Da hierbei<br />
die erste Komponente immer gleich ist, reduziert sich <strong>de</strong>r Vergleich nun<br />
darauf, ob die zweite Komponente kleiner ist.<br />
Die eingehen<strong>de</strong>n Kanten wer<strong>de</strong>n wie <strong>de</strong>r Kantenvektor sortiert. Auch hier<br />
reicht es, nach <strong>de</strong>r ersten Komponente zu sortieren, da die zweite Komponente<br />
bei allen ausgehen<strong>de</strong>n Kanten gleich <strong>de</strong>r Zell-ID <strong>de</strong>s Knotens ist.<br />
Nun wird <strong>de</strong>r i-ten eingehen<strong>de</strong>n Kante (aus <strong>de</strong>r sortierten eingehen<strong>de</strong>n Kanten-<br />
Liste) die i-te ausgehen<strong>de</strong> Kante (aus <strong>de</strong>r sortierten ausgehen<strong>de</strong>n Kanten-<br />
Liste) als Successor zugewiesen und die Kanten fragen wie<strong>de</strong>r bei <strong>de</strong>r entsprechen<strong>de</strong>n<br />
Knoten-Zelle ihren Successor ab.<br />
In <strong>de</strong>m hier gewählten Beispiel hat je<strong>de</strong>r Knoten nur eine eingehen<strong>de</strong> und<br />
eine ausgehen<strong>de</strong> Kante, das Sortieren ist also nicht notwendig. In Abbildung<br />
3.53 wur<strong>de</strong> je<strong>de</strong>r in <strong>de</strong>n Knoten eingehen<strong>de</strong>n Kante die aus <strong>de</strong>m Knoten<br />
ausgehen<strong>de</strong> als Successor zugewiesen.<br />
• In Zeile 9 wird nun gefor<strong>de</strong>rt, dass mit Hilfe <strong>de</strong>r eben gefun<strong>de</strong>nen Zerlegung <strong>de</strong>r<br />
Graph in zwei Teilgraphen zerlegt wird. Dies geschieht dadurch, dass alle noch aktiven<br />
Kanten-Zellen (ab diesem Schritt wer<strong>de</strong>n sie wie<strong>de</strong>r als ungerichtete Kanten<br />
betrachtet) entwe<strong>de</strong>r eine 0 o<strong>de</strong>r eine 1 als Label erhalten. Anschließend wer<strong>de</strong>n<br />
die Kanten, die mit <strong>de</strong>m Dummy verbun<strong>de</strong>n sind, entfernt.<br />
Die Kanten können mit Hilfe <strong>de</strong>r Doubling-Technik mit <strong>de</strong>m Label versehen wer<strong>de</strong>n.<br />
Wichtig dafür ist, dass eine Zelle mit einem Label initialisiert wird. Bereits<br />
in <strong>de</strong>n vorangehen<strong>de</strong>n Schritten benötigten die Kanten einen lesen<strong>de</strong>n Zugriff auf<br />
<strong>de</strong>n Master, also kann dieser die Initialisierung <strong>de</strong>r Kante veranlassen. Dies geschieht,<br />
in<strong>de</strong>m er die Kante und <strong>de</strong>n Label, auf <strong>de</strong>n sie zu setzen ist, in seinen<br />
86
Zustand kodiert. Die Kanten-Zellen lesen diesen Zustand und diejenige, die ihre<br />
Werte liest (Start- und Endpunkt sind gleich) initialisiert sich auf das angegebene<br />
Label. Anschließend entfernt sie ihre Verbindung zum Nachfolger.<br />
Die Kanten-Zellen fragen nun das Label ihres Nachbarn ab. Ist das Label we<strong>de</strong>r<br />
0 noch 1, so setzen sie ihre Nachfolgerverbindung auf die ihres Nachbarn. Dieser<br />
Schritt wird solange wie<strong>de</strong>rholt, bis <strong>de</strong>r Nachbar ein gültiges Label besitzt (nach<br />
maximal O(log(n)) Schritten).<br />
Mit Hilfe dieses Labels wird nun das eigene Label gesetzt. Wur<strong>de</strong> das an<strong>de</strong>re Label<br />
im ersten Schritt gefun<strong>de</strong>n, so ist das gefun<strong>de</strong>ne Label zu negieren, ansonsten ist<br />
<strong>de</strong>r Wert einfach zu übernehmen.<br />
• In Zeile 10 fin<strong>de</strong>t <strong>de</strong>r rekursive Aufruf statt. Es gibt die Möglichkeit, bei<strong>de</strong> Aufrufe<br />
nacheinan<strong>de</strong>r abzuarbeiten (auch das liefert schon eine Laufzeitverbesserung<br />
gegenüber an<strong>de</strong>ren <strong>Algorithmen</strong>) o<strong>de</strong>r bei<strong>de</strong> Aufrufe parallel zu bearbeiten. Das<br />
be<strong>de</strong>utet allerdings, dass die Knoten-Zellen bei je<strong>de</strong>m Aufruf dupliziert wer<strong>de</strong>n<br />
müssen. Sinnvoller ist es, <strong>de</strong>r Master-Zelle die Koordination zu überlassen und<br />
einen Aufruf nach <strong>de</strong>m an<strong>de</strong>ren zu bearbeiten.<br />
• Als letzter Schritt (Zeile 11) müssen nun die Farben im Ergebnis <strong>de</strong>s zweiten<br />
Aufrufs umbenannt wer<strong>de</strong>n. Auch dies kann die Master-Zelle erledigen. Sie muss<br />
lediglich für die Kanten-Zellen lesbar hinterlegen, ob es sich um <strong>de</strong>n ersten o<strong>de</strong>r<br />
<strong>de</strong>n zweiten Aufruf han<strong>de</strong>lt und wie groß das ∆ auf dieser Aufruf-Ebene war. Die<br />
Kanten addieren dann alle ∆ zu ihrer Farbe und die Umbenennung ist abgeschlossen.<br />
2<br />
Insgesamt wer<strong>de</strong>n also für diesen Algorithmus die Master-Zelle, |V | Knoten-Zellen und<br />
|2E| Kanten-Zellen benötigt. Zusätzlich wird entwe<strong>de</strong>r eine Speicherzelle o<strong>de</strong>r eine Dummy-Knoten-Zelle<br />
und maximal 2(|V |−1) Dummy-Kanten-Zellen benötigt. Daraus ergibt<br />
sich ein Zellenaufwand von O(|V |+|E|), was <strong>de</strong>utlich schlechter ist als <strong>de</strong>r Aufwand von<br />
O(|V |) auf <strong>de</strong>r P-RAM. Auch die Laufzeitkomplexität ist mit O(log 3 (n)) schlechter als<br />
auf <strong>de</strong>r P-RAM. Dies liegt aber an <strong>de</strong>r bewussten Entscheidung, nicht Coles Algorithmus<br />
zum Sortieren zu verwen<strong>de</strong>n.<br />
Auch die Euler-Färbung soll mit Hilfe <strong>de</strong>s Schemas aus Kapitel 2.3.4 klassifiziert wer<strong>de</strong>n.<br />
Als erstes soll dabei die Abhängigkeit <strong>de</strong>r Daten untersucht wer<strong>de</strong>n. Anschließend wer<strong>de</strong>n<br />
die Abhängigkeiten <strong>de</strong>r Verbindungen beleuchtet.<br />
In diesem Algorithmus existieren drei Arten von Zellen: Die Kanten-Zellen, die Knoten-<br />
Zellen und die Master-Zelle (sowie even<strong>tu</strong>ell eine Speicherzelle). Die Zellen verhalten sich<br />
unterschiedlich je nach<strong>de</strong>m, welcher Art sie angehören. Ordnet man die Zellen gemäß<br />
ihrer Art an, so kann man dieses Verhalten durch ihren Ort im GCA bestimmen. Es<br />
besteht dann also eine Ortsabhängigkeit <strong>de</strong>r Daten.<br />
Die Kanten-Zellen än<strong>de</strong>rn ihr Label, wenn sie dies von <strong>de</strong>r Master-Zelle o<strong>de</strong>r ihrer<br />
Knoten-Zelle vorgegeben bekommen. Die Daten <strong>de</strong>r Zellen sind also abhängig von <strong>de</strong>n<br />
Daten <strong>de</strong>r Nachbarzellen. Da das eigene Label durch hinzuaddieren <strong>de</strong>s gelesenen Werts<br />
87
aus <strong>de</strong>r Nachbarzelle gewonnen wird, sind die Daten auch abhängig von <strong>de</strong>n eigenen<br />
Daten.<br />
Es ergibt sich keine ersichtliche Verän<strong>de</strong>rung <strong>de</strong>r Daten aufgrund <strong>de</strong>r Zeit, weshalb dafür<br />
keine Abhängigkeit besteht. Auch eine Abhängigkeit von <strong>de</strong>n Verbindungen ist nicht<br />
festzustellen.<br />
Bereits bei <strong>de</strong>r Betrach<strong>tu</strong>ng <strong>de</strong>r Abhängigkeiten <strong>de</strong>r Daten wur<strong>de</strong> festgestellt, dass sich<br />
Zellen unterschiedlicher Arten verschie<strong>de</strong>n verhalten. Auch hier wird dies als eine Ortsabhängigkeit<br />
beschrieben.<br />
Die Verbindungen wer<strong>de</strong>n innerhalb <strong>de</strong>s Algorithmus dynamisch gehandhabt. So wird u.<br />
a. die Doubling-Technik mehrfach angewen<strong>de</strong>t. Da diesmal die Doubling-Technik ohne<br />
die Zuhilfenahme einer Speicherzelle angewen<strong>de</strong>t wird, sind die Verbindungen abhängig<br />
von <strong>de</strong>n Verbindungen <strong>de</strong>r Nachbar-Zelle.<br />
Im Laufe <strong>de</strong>s Algorithmus wer<strong>de</strong>n Successoren bestimmt und auch eine Verbindung zu<br />
diesen hergestellt. Die Successoren wer<strong>de</strong>n aber aufgrund <strong>de</strong>r Daten <strong>de</strong>r Nachbar-Zelle<br />
(<strong>de</strong>r Knoten) gebil<strong>de</strong>t und <strong>de</strong>swegen ist die Verbindung abhängig von <strong>de</strong>n Daten <strong>de</strong>r<br />
Nachbar-Zelle.<br />
Die oben festgestellten Abhängigkeiten treten in <strong>de</strong>m Algorithmus mehrfach auf und<br />
die angeführten Argumente sind nur als Beispiele zu sehen, weshalb die Abhängigkeit<br />
besteht. An<strong>de</strong>re Abhängigkeiten wur<strong>de</strong>n nicht festgestellt.<br />
3.4. Zusammenfassung<br />
In diesem Kapitel wur<strong>de</strong>n einige Graphen-<strong>Algorithmen</strong> vorgestellt. Da die Graphentheorie<br />
ein sehr weites und umfangreiches Feld ist, wur<strong>de</strong>n hier drei Problematiken exemplarisch<br />
beleuchtet: die Bestimmung zusammenhängen<strong>de</strong>r Komponenten, die Bestimmung<br />
eines minimal aufspannen<strong>de</strong>n Baums und die Färbung bipartiter Graphen.<br />
Für die Bestimmung zusammenhängen<strong>de</strong>r Komponenten und die Berechnung <strong>de</strong>s minimal<br />
aufspannen<strong>de</strong>n Baums wur<strong>de</strong>n zuerst klassische sequentielle <strong>Algorithmen</strong> vorgestellt.<br />
Diese wur<strong>de</strong>n dann anschließend auf <strong>de</strong>m GCA mo<strong>de</strong>liert und es wur<strong>de</strong> versucht,<br />
eine Laufzeitverbesserung zu erreichen.<br />
Im Falle <strong>de</strong>s Warshall-Algorithmus (zusammenhängen<strong>de</strong> Komponenten) ist es gelungen,<br />
die Laufzeitkomplexität von O(n 3 ) auf O(n) zu reduzieren. Allerdings wur<strong>de</strong> dies nur<br />
dadurch erreicht, dass ein hoher Aufwand in <strong>de</strong>r Hardware betrieben wur<strong>de</strong> (O(n) Zellen<br />
und O(n) Lei<strong>tu</strong>ngen).<br />
Es hat sich herausgestellt, dass die sequentiellen <strong>Algorithmen</strong> durch die parallelen Möglichkeiten<br />
<strong>de</strong>s GCA beschleunigt wer<strong>de</strong>n können. Allerdings schnei<strong>de</strong>n sie in <strong>de</strong>r Laufzeitanalyse<br />
schlechter ab als <strong>Algorithmen</strong>, die bereits für parallele Systeme entwickelt wur<strong>de</strong>n.<br />
Es wur<strong>de</strong>n einige <strong>Algorithmen</strong>, die für die P-RAM entwickelt wur<strong>de</strong>n auf <strong>de</strong>m GCA<br />
mo<strong>de</strong>lliert und eine Laufzeitanalyse betrieben. Im Laufe dieser Analyse wur<strong>de</strong> das Mo-<br />
88
Algorithmus Sequentiell Zeit P-RAM Proz. P-RAM Zeit GCA Zellen GCA<br />
Warshall O(n 3 ) - - O(n) O(n)<br />
Floyd-Warshall O(n 3 ) - - O(n) O(n)<br />
Hirschberg - O(log 2 (n)) O( n2<br />
log(n) ) O(log2 (n)) O( n2<br />
log(n) )<br />
Heapsort O(n log(n)) - - O(n) O(log(n))<br />
Kruskal O(|E|) - - O(|E|) O(|V |)<br />
mod. Hirschberg - O(log 2 (n)) O( n2<br />
log(n) ) O(log2 (n)) O( n2<br />
log(n) )<br />
Euler-Färbung - O(log 2 (n)) O(m) O(n log(n)) O(m + n)<br />
Tabelle 3.1.: Eine Betrach<strong>tu</strong>ng <strong>de</strong>r Laufzeit <strong>de</strong>r behan<strong>de</strong>lten <strong>Algorithmen</strong>. Falls vorhan<strong>de</strong>n<br />
sind die sequentielle o<strong>de</strong>r die P-RAM-Laufzeit angegeben. Wur<strong>de</strong> <strong>de</strong>r<br />
Algorithmus ursprünglich auf <strong>de</strong>r P-RAM angegeben und eine Abschätzung<br />
<strong>de</strong>r benötigten Prozessoren gegeben, so ist diese auch hier angegeben. Abschließend<br />
sind die Laufzeit und die benötigte Anzahl an Zellen angegeben.<br />
<strong>de</strong>ll einer Speicherzelle entwickelt, welche ermöglicht, dass alle betrachteten P-RAM-<br />
<strong>Algorithmen</strong> auf <strong>de</strong>m GCA mo<strong>de</strong>lliert wer<strong>de</strong>n können.<br />
Eine Zusammenfassung <strong>de</strong>r betrachteten <strong>Algorithmen</strong> zusammen mit ihrer Laufzeit im<br />
sequentiellen Fall, auf <strong>de</strong>r P-RAM und <strong>de</strong>m GCA ist in Tabelle 3.1 gegeben. Es erscheint<br />
immer interessant, bei parallelen <strong>Algorithmen</strong> nicht nur die Laufzeit son<strong>de</strong>rn auch die<br />
Anzahl <strong>de</strong>r benötigten Prozessoren (bzw. Zellen) zu betrachten. In [GR98] fin<strong>de</strong>t sich<br />
auf S. 1 folgen<strong>de</strong>s Zitat:<br />
A subclass of problems of particular interst are those which have optimal<br />
parallel algorithms. An optimal parallel algorithm is an algorithm for which<br />
the product of the parallel time t with the number of processors p used is<br />
linear in the problem size n. That is, pt = O(n). Optimality may also mean<br />
that the product pt is equal to the computation time of the fastest known<br />
sequential-time algorithm for the problem. Here we specifically refer to the<br />
problem as having optimal speed-up.<br />
Das Produkt pt wird an an<strong>de</strong>ren Stellen (u. a. in [KKT01]) auch als work bezeichnet.<br />
Alle betrachteten <strong>Algorithmen</strong> wur<strong>de</strong>n in das in Kapitel 2.3.4 vorgestellte Schema eingeordnet.<br />
Dabei wur<strong>de</strong>n die Abhängigkeiten <strong>de</strong>r Daten und <strong>de</strong>r Verbindungen <strong>de</strong>r nächsten<br />
Generationen von <strong>de</strong>n momentanen Gegebenenheiten untersucht. Das Ergebnis dieser<br />
Untersuchungen ist in Tabelle 3.2 (Daten) und 3.3 (Verbindungen) gegeben. Dabei stehen<br />
die Abkürzungen für folgen<strong>de</strong> Werte:<br />
• t = Zeit: kennzeichnet eine Verän<strong>de</strong>rung <strong>de</strong>s betrachteten Werts (Daten / Verbindung)<br />
abhängig von <strong>de</strong>r Zeit.<br />
• l = Eigener Ort: kennzeichnet eine Verän<strong>de</strong>rung <strong>de</strong>s betrachteten Werts abhängig<br />
von <strong>de</strong>m Ort <strong>de</strong>r eigenen Zelle.<br />
89
t l l* d d* p p*<br />
Warshall - - - + + - -<br />
Floyd-Warshall - - - + + - -<br />
Hirschberg - - - + + - -<br />
Heapsort + - - + + + -<br />
Kruskal + - - + + - -<br />
mod. Hirschberg - - - + + - -<br />
Euler-Färbung - + - + + + -<br />
Tabelle 3.2.: Abhängigkeit <strong>de</strong>r Daten einer Zelle in <strong>de</strong>m jeweils links stehen<strong>de</strong>n Algorithmus.<br />
Ein + steht für eine Abhängigkeit, ein - für keine Abhängigkeit. Die<br />
Be<strong>de</strong>u<strong>tu</strong>ng <strong>de</strong>r Abkürzungen ist <strong>de</strong>m Text zu entnehmen.<br />
t l l* d d* p p*<br />
Warshall + - - + - - -<br />
Floyd-Warshall + - - + - - -<br />
Hirschberg - - - - - - -<br />
Heapsort - + - + - - +<br />
Kruskal - - - - - - -<br />
mod. Hirschberg - - - - - - -<br />
Euler-Färbung - + - - + - +<br />
Tabelle 3.3.: Abhängigkeit <strong>de</strong>r Verbindungen einer Zelle in <strong>de</strong>m jeweils links stehen<strong>de</strong>n<br />
Algorithmus. Ein + steht für eine Abhängigkeit, ein - für keine Abhängigkeit.<br />
Die Be<strong>de</strong>u<strong>tu</strong>ng <strong>de</strong>r Abkürzungen ist <strong>de</strong>m Text zu entnehmen.<br />
• l* = Ort <strong>de</strong>r Nachbarn: kennzeichnet eine Verän<strong>de</strong>rung <strong>de</strong>s betrachteten Werts<br />
abhängig von <strong>de</strong>m Ort <strong>de</strong>r Nachbar-Zellen.<br />
• d = Eigene Daten: kennzeichnet eine Verän<strong>de</strong>rung <strong>de</strong>s betrachteten Werts abhängig<br />
von <strong>de</strong>n eigenen Daten einer Zelle.<br />
• d* = Daten <strong>de</strong>r Nachbarn: kennzeichnet eine Verän<strong>de</strong>rung <strong>de</strong>s betrachteten Werts<br />
abhängig von <strong>de</strong>n Daten <strong>de</strong>r Nachbar-Zellen.<br />
• p = Eigene Verbindung (Pointer): kennzeichnet eine Verän<strong>de</strong>rung <strong>de</strong>s betrachteten<br />
Werts abhängig von <strong>de</strong>n eigenen Verbindungen (nicht <strong>de</strong>n Werten, auf die sie<br />
zeigen).<br />
• p* = Verbindungen <strong>de</strong>r Nachbarn: kennzeichnet eine Verän<strong>de</strong>rung <strong>de</strong>s betrachteten<br />
Werts abhängig von <strong>de</strong>n Verbindungen <strong>de</strong>r Nachbar-Zellen.<br />
90
4. Die Mo<strong>de</strong>llierung von<br />
Krypto-<strong>Algorithmen</strong> auf <strong>de</strong>m GCA<br />
In <strong>de</strong>r heutigen Zeit wer<strong>de</strong>n die meisten Dokumente elektronisch verwaltet und Nachrichten<br />
wer<strong>de</strong>n elektronisch verschickt. Daher ist es wichtig, dass die Daten vor frem<strong>de</strong>n<br />
Zugriff geschützt wer<strong>de</strong>n können. Die Kryptographie ist eine Disziplin, die Metho<strong>de</strong>n<br />
<strong>de</strong>r Mathematik und Informatik kombiniert, um u. a. zu verhin<strong>de</strong>rn, dass Unbefugte,<br />
die in Besitz von Daten gelangen, daraus Informationen gewinnen können. Gera<strong>de</strong> da<br />
<strong>de</strong>r Briefverkehr sich immer mehr in die elektronische Variante, die E-Mail, verlagert,<br />
ist es wichtig, diese Informationen vor unbefugten Zugriff zu schützen.<br />
Viele in <strong>de</strong>r Kryptographie genutzte <strong>Algorithmen</strong> lassen sich leicht parallelisieren. Meist<br />
wer<strong>de</strong>n mehrere Berechnungen <strong>de</strong>r gleichen Art benötigt, um am En<strong>de</strong> das Gesamtergebnis<br />
zu erhalten. Diese Berechnungen können (falls sie nicht voneinan<strong>de</strong>r abhängig sind)<br />
parallel ausgeführt wer<strong>de</strong>n und somit kann eine Laufzeitverringerung erreicht wer<strong>de</strong>n.<br />
Ein solcher Algorithmus ist <strong>de</strong>r Chinesische Restsatz. Mit Hilfe <strong>de</strong>s Chinesischen Restsatzes<br />
ist es u. a. möglich, simultane Kongruenzen zu lösen. Um n Kongruenzen zu<br />
lösen, muss unabhängig voneinan<strong>de</strong>r n mal <strong>de</strong>r erweiterte euklidische Algorithmus ausgeführt<br />
wer<strong>de</strong>n. Der erweiterte euklidische Algorithmus (EEA) selber eignet sich lei<strong>de</strong>r<br />
nicht zur Parallelisierung. Da er aber nötig ist, um <strong>de</strong>n Chinesischen Restsatz anwen<strong>de</strong>n<br />
zu können, wird im folgen<strong>de</strong>n erst <strong>de</strong>r EEA erklärt. Erst danach wird die Anwendung<br />
<strong>de</strong>s Chinesischen Restsatzes erläutert und auf <strong>de</strong>m GCA simuliert. Bei<strong>de</strong> <strong>Algorithmen</strong><br />
verwen<strong>de</strong>n Grundlagen <strong>de</strong>r Gruppentheorie, auf die in dieser Arbeit aber nicht näher eingegangen<br />
wird. Eine gute Zusammenfassung <strong>de</strong>r Gruppentheorie und eine Beschreibung<br />
<strong>de</strong>r <strong>Algorithmen</strong> liefert [Buc01].<br />
4.1. Der erweiterte euklidische Algorithmus<br />
Der Euklidische Algorithmus dient dazu, <strong>de</strong>n größten gemeinsamen Teiler (ggT) zweier<br />
Zahlen a und b zu berechnen. Der erweiterte euklidische Algorithmus nutzt die Einzelschritte<br />
<strong>de</strong>s euklidischen Algorithmuses, um zusätzlich das Inverse von a mod b und<br />
von b mod a zu berechnen. Für <strong>de</strong>n Chinesischen Restsatz ist es notwendig, genau diese<br />
Inversen zu bestimmen, weshalb <strong>de</strong>r erweiterte euklidische Algorithmus so wichtig ist.<br />
Der euklidische Algorithmus benötigt maximal log(b)/log( 1+√ 5<br />
2<br />
) + 1 Schritte.<br />
Um <strong>de</strong>n ggT zweier ganzer Zahlen a und b mit a > b zu berechnen, wird a zerlegt in<br />
a = q × b + r. Dabei ist q das Ergebnis <strong>de</strong>r Ganzzahldivision ohne Rest von a durch b<br />
91
und r <strong>de</strong>r dabei anfallen<strong>de</strong> Rest. Im Algorithmus wird <strong>de</strong>shalb a durch b und b durch r<br />
ersetzt. Dieser Schritt wird solange wie<strong>de</strong>rholt, bis b = 0 ist. Ist dies <strong>de</strong>r Fall, so wird a<br />
als Ergebnis zurückgegeben.<br />
Die Schritte <strong>de</strong>s euklidischen Algorithmus können sehr anschaulich in einer Tabelle dargestellt<br />
wer<strong>de</strong>n. Die erste Zeile beinhaltet die Werte aus <strong>de</strong>nen <strong>de</strong>r ggT berechnet wer<strong>de</strong>n<br />
soll (die Werte <strong>de</strong>s ersten Durchlaufs stehen in <strong>de</strong>r ersten und zweiten Spalte, die Werte<br />
<strong>de</strong>s zweiten Durchlaufs stehen in <strong>de</strong>r zweiten und dritten Spalte usw.). Die zweite Zeile<br />
gibt das q an (also die Anzahl, wie oft b in a passt).<br />
k 0 1 2 3<br />
r k 27 6 3 0<br />
q k 4 2<br />
Tabelle 4.1.: Beispiel: Der euklidische Algorithmus wird verwen<strong>de</strong>t um <strong>de</strong>n ggT von 27<br />
und 6 zu berechnen.<br />
Der erweiterte euklidische Algorithmus nutzt nun die q k , um ein x und ein y zu berechnen,<br />
so dass gilt: ggT(a,b) = ax + by. Dies gelingt mit Hilfe <strong>de</strong>r Erkenntnis, dass r n =<br />
(−1) n x n a + (−1) n+1 y n b, d. h. es wird lediglich eine Berechnungsvorschrift benötigt, um<br />
die x k und y k (für alle 1 ≤ k ≤ n) zu berechnen. Durch die oben genannte Endbedingung<br />
ergeben sich die Berechnungsvorschriften: x k+1 = q k x k + x k−1 und y k+1 = q k y k + y k−1 .<br />
Da die oben genannte Erkenntnis für alle n ≤ k (also insbeson<strong>de</strong>re auch für r 0 = a und<br />
r 1 = b) gilt, ergeben sich folgen<strong>de</strong> Initialisierungen: x 0 = 1, x 1 = 0, y 0 = 0 und y 1 = 1.<br />
Wen<strong>de</strong>t man <strong>de</strong>n erweiterten euklidischen Algorithmus auf das vorherige Beispiel an, so<br />
muss die Tabelle um die Einträge x k und y k erweitert wer<strong>de</strong>n. Die Tabelle 4.2 berechnet<br />
neben <strong>de</strong>m ggT auch noch das x und das y.<br />
Da in <strong>de</strong>r Tabelle <strong>de</strong>r letzte Wert von r k ≠ 0 <strong>de</strong>n ggT angibt, kann man aus <strong>de</strong>r Tabelle<br />
folgen<strong>de</strong>s entnehmen:<br />
ggT(27, 6) = 3 = 1 × 27 + (−4) × 6 = 27 − 24 = 3.<br />
Für Werte a und b, die teilerfremd sind (d. h. ggT(a,b) = 1), ist <strong>de</strong>r Wert x gleichzeitig<br />
auch das Inverse von a modulo b. Da modulo b gerechnet wird gilt:<br />
k 0 1 2 3<br />
r k 27 6 3 0<br />
q k 4 2<br />
x k (+)1 (-)0 (+)1 (-)2<br />
y k (-)0 (+)1 (-)4 (+)9<br />
Tabelle 4.2.: Beispiel: Der erweiterte euklidische Algorithmus wird verwen<strong>de</strong>t um <strong>de</strong>n<br />
ggT von 27 und 6 zu berechnen. Gleichzeitig wer<strong>de</strong>n das x und das y bestimmt,<br />
für die gilt: ggT(27, 6) = 27 × x + 6 × y. In <strong>de</strong>n Klammern ist das<br />
alternieren<strong>de</strong> Vorzeichen gegeben, welches aber lediglich für die endgültigen<br />
Werte von x und y benötigt wer<strong>de</strong>n.<br />
92
1 mod b = ggT(a,b) mod b = (a × x + b × y) mod b = a × x mod b. Somit liefert<br />
<strong>de</strong>r erweiterte euklidische Algorithmus für teilerfrem<strong>de</strong> Zahlen eine einfache Metho<strong>de</strong><br />
zur Bestimmung von Inversen in einem Modulo-Ring. Der Source-Co<strong>de</strong> in C sieht im<br />
Prinzip folgen<strong>de</strong>rmaßen aus:<br />
Listing 4.1: Der erweiterte euklidische Algorithmus in C<br />
1 int euklid ( int a , int b , int ∗x , int ∗y){<br />
2 int xPrev , xCur , yPrev , yCur , xNext , yNext , r , q ;<br />
3 int sign = 1;<br />
4 xPrev = 1;<br />
5 xCur = 0;<br />
6 yPrev = 0;<br />
7 yCur = 1;<br />
8 while (b != 0) {<br />
9 r = a%b ;<br />
10 q = a/b ;<br />
11 a = b ;<br />
12 b = r ;<br />
13 xNext = q∗xCur + xPrev ;<br />
14 xPrev = xCur ;<br />
15 xCur = xNext ;<br />
16 yNext = q∗yCur + yPrev ;<br />
17 yPrev = yCur ;<br />
18 yCur = yNext ;<br />
19 sign = −sign ;<br />
20 }<br />
21 ∗x = sign ∗ xCur ;<br />
22 ∗y = −sign ∗ yCur ;<br />
23 re<strong>tu</strong>rn a ;<br />
24 }<br />
Da in <strong>de</strong>r Anwendung <strong>de</strong>s Chinesischen Restsatzes nur Kongruenzen <strong>de</strong>r Form<br />
yM ≡ 1 mod m nach <strong>de</strong>r Verwendung <strong>de</strong>s erweiterten euklidischen Algorithmus verlangen<br />
(es wird das Inverse von M modulo m gesucht) und dabei i. A. gilt, dass M > m,<br />
wird nur die Berechnung <strong>de</strong>s Wertes x benötigt.<br />
4.2. Die Anwendung <strong>de</strong>s Chinesische Restsatzes<br />
Der Chinesische Restsatz lautet: Seien m,n teilerfremd. Dann hat das Gleichungssystem<br />
∣<br />
x = a mod m<br />
x = b mod n<br />
∣<br />
93
eine ein<strong>de</strong>utige Lösung modulo m × n.<br />
Der Chinesische Restsatz wird u. a. angewen<strong>de</strong>t, um simultane Kongruenzen zu lösen.<br />
Dabei müssen die Faktoren, nach <strong>de</strong>nen modularisiert wird paarweise teilerfremd sein.<br />
Betrachtet man nun das Beispiel aus [Buc01] S. 41, so stehen dort mehrere Kongruenzen,<br />
die ein gemeinsames Ergebnis x haben. Diese allgemeinen Kongruenzen möchte man<br />
dann mit Hilfe <strong>de</strong>s Chinesischen Restsatzes lösen:<br />
x ≡ a 1 mod m 1 ,x ≡ a 2 mod m 2 , · · · ,x ≡ a n mod m n .<br />
Alle m i (1 ≤ i ≤ n) sind untereinan<strong>de</strong>r teilerfremd. Dann setzt man m = ∏ n<br />
i=1 m i und<br />
berechnet anschließend für je<strong>de</strong>s i ein eigenes M i = m m i<br />
. Dann wird für je<strong>de</strong>s M i sein<br />
Inverses modulo m i gesucht, es muss also die Gleichung y i M i ≡ 1 mod m i für je<strong>de</strong>s<br />
i größer 1 und kleiner n gelöst wer<strong>de</strong>n. Um die y i zu berechnen, wird <strong>de</strong>r erweiterte<br />
euklidische Algorithmus verwen<strong>de</strong>t.<br />
Mit Hilfe dieser Ergebnisse kann nun das Gesamtergebnis x <strong>de</strong>r simultanen Kongruenz<br />
bestimmt wer<strong>de</strong>n, da x = ( ∑ n<br />
i=1 a i y i M i ) mod m gilt.<br />
Interessant ist diese Metho<strong>de</strong>, wenn entwe<strong>de</strong>r bekannt ist, dass mehrere Kongruenzen<br />
das gleiche (noch unbekannte) Ergebnis haben, o<strong>de</strong>r wenn man quadratische Gleichungen<br />
modulo einer Zahl m berechnen muss.<br />
Betrachtet man das Beispiel x 2 = 1 mod 30, so kann diese Gleichung in die drei Kongruenzen<br />
x 2 = 1 mod 2, x 2 = 1 mod 3 und x 2 = 1 mod 5 zerlegt wer<strong>de</strong>n (man zerlegt<br />
die 30 in seine Primfaktoren).<br />
Der Chinesische Restsatz gilt zwar nur für lineare Gleichungen, es gestaltet sich jedoch<br />
einfacher, quadratische Gleichungen für die Primfaktoren zu lösen als für m. So kann<br />
die Gleichung x 2 = 1 mod 2 gelöst wer<strong>de</strong>n, in<strong>de</strong>m alle Werte i mit 0 ≤ i ≤ überprüft<br />
wer<strong>de</strong>n, ob sie die Gleichung erfüllen.<br />
Die 0 wird für keine <strong>de</strong>r Gleichungen eine Lösung sein, also müssen pro Gleichung maximal<br />
4 Werte (für m i = 5) eingesetzt und geprüft wer<strong>de</strong>n. Die erste Gleichung ist ein<strong>de</strong>utig<br />
mit x = 1 mod 2 bestimmt, bei <strong>de</strong>r zweiten und dritten gibt es jedoch jeweils zwei Lösungen.<br />
Die zwei Lösungen <strong>de</strong>r zweiten Gleichung sind x = 1 mod 3 und x = 2 mod 3. Die<br />
dritte Gleichung dagegen hat die Lösungen x = 1 mod 5 und x = 4 mod 5.<br />
Es gibt vier Möglichkeiten, diese Gleichungen zu kombinieren, also vier verschie<strong>de</strong>ne<br />
” Sets“ an a i:<br />
• a 1 = 1, a 2 = 1, a 3 = 1<br />
• a 1 = 1, a 2 = 2, a 3 = 1<br />
• a 1 = 1, a 2 = 1, a 3 = 4<br />
• a 1 = 1, a 2 = 2, a 3 = 4<br />
Die Inversen können unabhängig von <strong>de</strong>n a i berechnet wer<strong>de</strong>n, aber beim Bestimmen<br />
<strong>de</strong>r Lösung für die ursprüngliche Gleichung muss je<strong>de</strong>s ”<br />
Set“ eingesetzt wer<strong>de</strong>n. Es gibt<br />
also vier Lösungen für das Gleichungssystem.<br />
94
Für die Anwendung <strong>de</strong>s Chinesischen Restsatzes gelten folgen<strong>de</strong> Werte: m 1 = 2, m 2 = 3,<br />
m 3 = 5 und m = 30. Daraus berechnen sich nun die M i : M 1 = 15, M 2 = 10 und M 3 = 6.<br />
Anschließend muss für je<strong>de</strong>s M i sein Inverses modulo m i bestimmt wer<strong>de</strong>n. Dies geschieht<br />
mit <strong>de</strong>m erweiterten euklidischen Algorithmus:<br />
k 0 1 2 3<br />
r k 15 2 1 0<br />
q k 7 2<br />
x k (+)1 (-)0 (+)1 (-)2<br />
y k (-)0 (+)1 (-)7 (+)15<br />
Tabelle 4.3.: Zur Berechnung <strong>de</strong>s Inversen von M 1 modulo m 1 wird <strong>de</strong>r erweiterte euklidische<br />
Algorithmus verwen<strong>de</strong>t.<br />
Aus <strong>de</strong>n Tabellen 4.3, 4.4 und 4.5 lassen sich nun die y i nun jeweils in <strong>de</strong>r Spalte k = 2<br />
und Zeile x k ablesen. Daraus ergeben sich die folgen<strong>de</strong>n y i : y 1 = y 2 = y 3 = 1.<br />
Setzt man nun dieses Ergebnis ein, so erhält man die Lösungen:<br />
• x = 1 × 1 × 15 + 1 × 1 × 10 + 1 × 1 × 6 mod 30 = 1 mod 30<br />
• x = 1 × 1 × 15 + 1 × 2 × 10 + 1 × 1 × 6 mod 30 = 41 mod 30 = 11 mod 30<br />
• x = 1 × 1 × 15 + 1 × 1 × 10 + 1 × 4 × 6 mod 30 = 49 mod 30 = 19 mod 30<br />
• x = 1 × 1 × 15 + 1 × 2 × 10 + 1 × 4 × 6 mod 30 = 59 mod 30 = 29 mod 30<br />
k 0 1 2 3<br />
r k 10 3 1 0<br />
q k 3 3<br />
x k (+)1 (-)0 (+)1 (-)3<br />
y k (-)0 (+)1 (-)3 (+)9<br />
Tabelle 4.4.: Auch zur Berechnung <strong>de</strong>s Inversen von M 2 modulo m 2 wird <strong>de</strong>r erweiterte<br />
euklidische Algorithmus verwen<strong>de</strong>t.<br />
k 0 1 2 3<br />
r k 6 5 1 0<br />
q k 1 5<br />
x k (+)1 (-)0 (+)1 (-)5<br />
y k (-)0 (+)1 (-)1 (+)6<br />
Tabelle 4.5.: Das letzte benötigte Inverse (von M 3 modulo m 3 ) wird auch mit Hilfe <strong>de</strong>s<br />
erweiterten euklidischen Algorithmus berechnet.<br />
95
Abbildung 4.1.: Je<strong>de</strong> Zelle <strong>de</strong>s GCA wird mit einem a i und einem m i initialisiert. Daraufhin<br />
setzt die Zelle m auf m i damit die Doubling-Technik erfolgreich<br />
angewen<strong>de</strong>t wer<strong>de</strong>n kann. Die letzte Zelle hat eine inaktive Verbindung,<br />
dies ist dadurch dargestellt, dass die Zelle grau hinterlegt wur<strong>de</strong>.<br />
Abbildung 4.2.: Nach log(n) (hier zwei) Schritten steht das korrekte Ergebnis in <strong>de</strong>r ersten<br />
Zelle. Die an<strong>de</strong>ren Zellen haben eine <strong>de</strong>aktivierte Verbindung. Dies<br />
wur<strong>de</strong> auch hier wie<strong>de</strong>r durch eine graue Hinterlegung gekennzeichnet.<br />
Wie anhand <strong>de</strong>s Beispiels ersichtlich wird, gibt es Lösungen für das Gleichungssystem,<br />
welche man ohne <strong>de</strong>n Chinesischen Restsatz nicht so einfach erkannt hätte.<br />
Für die Implementierung <strong>de</strong>r Anwendung <strong>de</strong>s Chinesischen Restsatzes wird angenommen,<br />
dass bereits die linearen Kongruenzen vorhan<strong>de</strong>n sind. Dann wird je<strong>de</strong> Zelle mit<br />
einer Kongruenz initialisiert, d. h. sie erhält ein a i und ein m i (s. Abbildung 4.1).<br />
Als ersten Schritt muss m berechnet wer<strong>de</strong>n. Dazu wird wie<strong>de</strong>r die Doubling-Technik<br />
verwen<strong>de</strong>t. Je<strong>de</strong> Zelle überprüft, ob die Verbindung <strong>de</strong>r Nachbar-Zelle noch aktiv ist.<br />
Falls dies nicht <strong>de</strong>r Fall ist, so multipliziert die Zelle ihren eigenen Wert m mit <strong>de</strong>m<br />
Wert m ihrer Nachbar-Zelle und <strong>de</strong>aktiviert seine Verbindung. Ist die Verbindung <strong>de</strong>r<br />
Nachbar-Zelle nicht <strong>de</strong>aktiviert, so multipliziert die Zelle ihren eigenen Wert m mit <strong>de</strong>m<br />
Wert m ihrer Nachbar-Zelle und setzt seine Verbindung auf die Verbindung <strong>de</strong>r Nachbar-<br />
Zelle. Alle Zellen mit einer <strong>de</strong>aktivierten Verbindung warten, bis die log(n) Schritte <strong>de</strong>r<br />
Ausrechenphase been<strong>de</strong>t sind, bevor sie wie<strong>de</strong>r etwas machen.<br />
Nach log(n) Schritten hat nur die erste Zelle einen korrekten Wert (s. Abbildung 4.2),<br />
also müssen alle Zellen eine Verbindung zu <strong>de</strong>r ersten Zelle aufbauen. In 4.3 haben alle<br />
Zellen außer <strong>de</strong>r ersten eine Verbindung zu <strong>de</strong>r ersten Zelle aufgebaut und <strong>de</strong>ren Wert<br />
von m übernommen.<br />
In <strong>de</strong>n nachfolgen<strong>de</strong>n Schritten ist keinerlei Kommunikationsaufwand nötig. Die Zellen<br />
berechnen ihr eigenes M i in<strong>de</strong>m sie m durch m i teilen. Anschließend wird <strong>de</strong>r erweiterte<br />
euklidische Algorithmus verwen<strong>de</strong>t, um das y i zu berechnen. Nach<strong>de</strong>m die Zelle y i , a i<br />
und M i kennt, kann sie diese miteinan<strong>de</strong>r multiplizieren und schreibt sie in <strong>de</strong>n x-Wert.<br />
96
Abbildung 4.3.: Alle Zellen haben eine Verbindung zu <strong>de</strong>r ersten Zelle aufgebaut. Anschließend<br />
wur<strong>de</strong> <strong>de</strong>r Wert m bei <strong>de</strong>r ersten Zelle ausgelesen und in die<br />
eigene Variable m geschrieben.<br />
Nachfolgend wird wie<strong>de</strong>r eine Kommunikation notwendig, <strong>de</strong>nn zum Abschluss <strong>de</strong>s Algorithmus<br />
müssen die gefun<strong>de</strong>nen Werte x aller Zellen aufaddiert wer<strong>de</strong>n. Dies kann<br />
wie das initiale Berechnen <strong>de</strong>s Wertes m mit <strong>de</strong>r Doubling-Technik geschehen. Dazu<br />
addieren die Zellen ihren eigenen x-Wert und <strong>de</strong>n x-Wert ihres Nachbarn falls die eigene<br />
Verbindung aktiv ist. Ist die eigene Verbindung und die Verbindung <strong>de</strong>s Nachbarn aktiv,<br />
so wird anschließend die eigene Verbindung auf die Verbindung <strong>de</strong>s Nachbarn gesetzt.<br />
Die Lösung <strong>de</strong>r simultanen Kongruenz steht dann in <strong>de</strong>r ersten Zelle und kann dort<br />
ausgelesen wer<strong>de</strong>n.<br />
Da die Kongruenzen alle parallel gelöst wer<strong>de</strong>n, ist die Laufzeit <strong>de</strong>r Anwendung <strong>de</strong>s Chinesischen<br />
Restsatzes von <strong>de</strong>r Laufzeit <strong>de</strong>s erweiterten euklidischen Algorithmus abhängig.<br />
Wie bereits bei <strong>de</strong>r Beschreibung <strong>de</strong>s erweiterten euklidischen Algorithmus erwähnt<br />
wur<strong>de</strong>, sind maximal log(b)/log( 1+√ 5) + 1 Schritte nötig. Allerdings fallen in <strong>de</strong>m erweiterten<br />
euklidischen Algorithmus auch zwei Multiplikationen an, welche in [Buc01]<br />
2<br />
als <strong>de</strong>r zeitkritische Faktor angesehen wer<strong>de</strong>n und <strong>de</strong>swegen die Laufzeit dominieren.<br />
Es ergibt sich <strong>de</strong>shalb eine Laufzeit für <strong>de</strong>n erweiterten euklidischen Algorithmus von<br />
O(log(a) × log(b)). Betrachte man die Anwendung <strong>de</strong>s Chinesischen Restsatzes unter<br />
diesen Gesichtspunkten, so hat die Initialisierung auf das gemeinsame m anfangs trotz<br />
<strong>de</strong>r benötigten log(m) Schritte die gleiche Laufzeitkomplexität wie <strong>de</strong>r erweiterte euklidische<br />
Algorithmus. Insgesamt benötigt die Anwendung <strong>de</strong>s Chinesischen Restsatzes<br />
also eine Laufzeit von O(log(m) × log(n)) wobei m das größte und n das zweitgrößte<br />
Ausgangsmodul ist.<br />
4.3. Einordnung <strong>de</strong>r Anwendung <strong>de</strong>s Chinesischen<br />
Restsatzes<br />
In diesem Abschnitt wird die Anwendung <strong>de</strong>s Chinesischen Restsatzes in das in Kapitel<br />
2.3.4 vorgestellte Schema eingeordnet. Da die Kommunikation innerhalb dieses Algorithmus<br />
auf die Initialisierung und das Zusammenrechnen am En<strong>de</strong> beschränkt, reicht eine<br />
Betrach<strong>tu</strong>ng dieser bei<strong>de</strong>n Abläufe.<br />
Betrachtet man diese bei<strong>de</strong>n Abläufe genauer, so erkennt man, dass es sich bei<strong>de</strong> Male<br />
um eine Doubling-Technik han<strong>de</strong>lt. Bei <strong>de</strong>r Initialisierung müssen die Werte <strong>de</strong>r Zellen<br />
97
t l l* d d* p p*<br />
Daten - - - + + + -<br />
Verbindungen + - - - - + +<br />
Tabelle 4.6.: Abhängigkeit <strong>de</strong>r Verbindungen und <strong>de</strong>r Daten einer Zelle für die Anwendung<br />
<strong>de</strong>s Chinesischen Restsatzes. Ein + steht für eine Abhängigkeit, ein<br />
- für keine Abhängigkeit. Die Be<strong>de</strong>u<strong>tu</strong>ng <strong>de</strong>r Abkürzungen ist entsprechend<br />
<strong>de</strong>nen in Kapitel 3.4.<br />
multipliziert, am En<strong>de</strong> muss addiert wer<strong>de</strong>n. Ansonsten sind bei<strong>de</strong> Abläufe gleich und<br />
können somit gemeinsam betrachtet wer<strong>de</strong>n.<br />
Die Daten sind abhängig von <strong>de</strong>n eigenen Daten und <strong>de</strong>n Daten <strong>de</strong>r Nachbarn.<br />
Eine Abhängigkeit <strong>de</strong>r Daten von <strong>de</strong>r Zeit ist nicht feststellbar.<br />
Auch von <strong>de</strong>m eigenen Ort sind die Daten nicht abhängig, außer dass das Ergebnis <strong>de</strong>r<br />
Rechnung am En<strong>de</strong> in <strong>de</strong>r ersten Zelle steht.<br />
Interessant ist, dass sich die Daten in Abhängigkeit <strong>de</strong>r eigenen Verbindung än<strong>de</strong>rn. Nur<br />
wenn die eigene Verbindung aktiv ist, wer<strong>de</strong>n die eigenen Daten geän<strong>de</strong>rt.<br />
Die Verbindungen sind abhängig von <strong>de</strong>r Verbindung <strong>de</strong>s Nachbarn. Ist die Verbindung<br />
<strong>de</strong>s Nachbarn aktiv, so wird die eigene Verbindung auf diese Verbindung geän<strong>de</strong>rt, ansonsten<br />
wird die eigene Verbindung <strong>de</strong>aktiviert. Außer<strong>de</strong>m sind sie abhängig von <strong>de</strong>n<br />
eigenen Verbindungen, da auf <strong>de</strong>n Nachbar nur zugegriffen wird, wenn die eigene Verbindung<br />
aktiv ist.<br />
Da nach <strong>de</strong>r Doubling-Technik alle Zellen eine Verbindung zu <strong>de</strong>r ersten Zelle aufbauen,<br />
ist die Verbindung auch abhängig von <strong>de</strong>r Zeit. Es ist vorhersehbar, dass zu diesem<br />
Zeitpunkt genau diese Verbindungen existieren.<br />
Die Abhängigkeit vom Ort bedarf <strong>de</strong>r Interpretation. Man kann sich einerseits auf <strong>de</strong>n<br />
Standpunkt stellen, dass alle Zellen, die nicht an <strong>de</strong>r ersten Stelle sind, eine Verbindung<br />
zu <strong>de</strong>r ersten Zelle aufbauen. Mit dieser Betrach<strong>tu</strong>ng ist die Verbindung ortsabhängig.<br />
An<strong>de</strong>rerseits kann man die Auffassung vertreten, dass je<strong>de</strong> Zelle eine Verbindung zu<br />
<strong>de</strong>r ersten Zelle aufbaut (also auch die erste Zelle). In diesem Fall ist die Verbindung<br />
ortsunabhängig. Da sich die Abhängigkeit hier sehr leicht vermei<strong>de</strong>n lässt und keine<br />
Fehler dadurch entstehen, wird <strong>de</strong>r zweite Ansatz vertreten.<br />
Es ergeben sich also die Funktionen: d’ = f(d,d*,p) und p’ = g(t,p,p*).<br />
98
5. Implementierungseigenheiten<br />
Die in Kapitel 3 und Kapitel 4 vorgestellten <strong>Algorithmen</strong> wur<strong>de</strong>n nicht nur in <strong>de</strong>r Theorie<br />
auf <strong>de</strong>m GCA mo<strong>de</strong>lliert, son<strong>de</strong>rn auch in C implementiert. Allerdings besteht bei <strong>de</strong>r<br />
Programmierung in C das Problem, dass diese Sprache selbst nicht parallel ist. Die<br />
einzige Möglichkeit, Parallelität in C zu programmieren, sind Threads.<br />
Threads bieten die Möglichkeit, mehrere Prozesse zur gleichen Zeit aufzurufen. Auf einem<br />
Single-Prozessor-System ergibt sich dadurch keine Laufzeitbeschleunigung. Da <strong>de</strong>r<br />
Prozessor versucht, alle Prozesse möglichst gleich zu behan<strong>de</strong>ln, schaltet er nach gewissen<br />
Zeitspannen zwischen <strong>de</strong>n Prozessen um (Scheduling).<br />
Durch dieses Verhalten wird ein Programm mit Threads auf einem Einprozessor-System<br />
eher langsamer laufen als das gleiche Programm ohne Threads.<br />
Aus Vergleichsgrün<strong>de</strong>n wur<strong>de</strong> <strong>de</strong>r Warshall-Algorithmus mit und ohne Threads implementiert.<br />
Es stellte sich heraus, dass <strong>de</strong>r Warshall-Algorithmus mit Threads auch auf<br />
einem Multiprozessor-System langsamer ist als ohne Threads. Ein Grund dafür ist die<br />
Synchronisation, die für <strong>de</strong>n Ablauf <strong>de</strong>s Warshall-Algorithmus notwendig ist. Außer<strong>de</strong>m<br />
stellte sich heraus, dass die Problemgröße zu gering ist, als dass sich die Verwendung<br />
von Threads positiv auf die Laufzeit auswirken könnte.<br />
Um zu überprüfen, ob dies die einzigen Grün<strong>de</strong> sind, wur<strong>de</strong> ein Testprogramm geschrieben,<br />
welches keiner Kommunikation bedarf. Das Programm besteht aus zwei Prozessen,<br />
die 1000000000 mal eine <strong>Ra</strong>ndom-Variable bestimmen, sie aufaddiert und modulo rechnet.<br />
Das charakteristische Programmfragment ist im Folgen<strong>de</strong>m gegeben.<br />
Listing 5.1: Effizienztest für Threads in C<br />
1 #<strong>de</strong>fine ANZAHL WIEDERHOLUNGEN 100000000<br />
2<br />
3 void some thread (void∗ some){<br />
4 int i , x1 , x2 ;<br />
5 for ( i =0; i
Es stellte sich heraus, dass sich mit Threads eine Laufzeitverbesserung erreichen lässt<br />
(siehe Tabelle 5.1). Allerdings stellte sich auch heraus, dass Programme mit hohem<br />
Kommunikationsaufwand und geringen Problemgrößen nicht dazu geeignet sind, mit<br />
Threads parallelisiert zu wer<strong>de</strong>n.<br />
Prozessoren Intel ohne Threads mit Threads<br />
Singleprozessor P4 Hyperthreading 3,2 GHz 94 s 80 s<br />
Dualprozessor Xeon Hyperthreading 3,0 GHz 80 s 41 s<br />
Tabelle 5.1.: Ausführungszeiten ohne Threads und mit Threads<br />
Die in Kapitel 3 betrachteten <strong>Algorithmen</strong> haben alle einen hohen Kommunikationsaufwand<br />
und sind zu<strong>de</strong>m auf ein synchrones System ausgelegt. Aus diesem Grund sind<br />
diese <strong>Algorithmen</strong> nicht geeignet, um mit Hilfe von Threads parallelisiert zu wer<strong>de</strong>n.<br />
Der kryptographische Algorithmus aus Kapitel 4 hat einen geringeren Kommunikationsaufwand,<br />
allerdings sind die Berechnungen nicht sehr aufwendig. Eine Parallelisierung<br />
ist hier also möglich aber nicht sinnvoll. Außer<strong>de</strong>m bedarf die beschriebene Initialisierung<br />
und Endberechnung wie<strong>de</strong>r eines synchronen Systems. Ein Thread kann also nur<br />
sinnvoll zwischen Initialisierung und Endrechnung betrieben wer<strong>de</strong>n.<br />
Der Warshall-Algorithmus wur<strong>de</strong> auf verschie<strong>de</strong>ne Arten implementiert, um zu testen, ob<br />
eine geeignete Implementierung die Laufzeit verbessert. Die meisten Implementierungen<br />
benötigten ungefähr die gleiche Laufzeit, außer <strong>de</strong>m modularen Warshall-Algorithmus.<br />
Dieser stellte sich als sehr laufzeitineffizient heraus. Es wur<strong>de</strong> bei <strong>de</strong>n weiteren <strong>Algorithmen</strong><br />
<strong>de</strong>swegen auf eine Modularisierung und Kapselung verzichtet.<br />
Der Hirschberg wur<strong>de</strong> mit Hilfe von structs implementiert. Dabei gab es ein Array (<strong>de</strong>n<br />
GCA) von structs (<strong>de</strong>n Zeilen), welche die Eingabe bil<strong>de</strong>ten. Alle weiteren Schritte wur<strong>de</strong>n<br />
sequentiell abgearbeitet. Programmiert man einen parallelen Algorithmus sequentiell,<br />
so muss je<strong>de</strong> parallele Anweisung durch eine Schleife ersetzt wer<strong>de</strong>n.<br />
Um Datenkonflikte (die bei synchroner Parallelität im Programm nicht auftreten) zu vermei<strong>de</strong>n,<br />
müssen Variablen doppelt gehalten wer<strong>de</strong>n. Im Schritt <strong>de</strong>r parallelen Verän<strong>de</strong>rung<br />
wird die Än<strong>de</strong>rung nicht in die eigentliche Variable geschrieben, son<strong>de</strong>rn in <strong>de</strong>ren<br />
Kopie. Nach Durchlauf <strong>de</strong>r Schleife muss dann das Ergebnis in die Variable zurückgespeichert<br />
wer<strong>de</strong>n.<br />
Im konkreten Fall wur<strong>de</strong> mit Pointern gearbeitet und nach einem Durchlauf <strong>de</strong>r Pointer<br />
auf die ak<strong>tu</strong>elle Variable umgebogen.<br />
Dieses Verfahren wur<strong>de</strong> nicht nur bei <strong>de</strong>m Algorithmus von Hirschberg angewen<strong>de</strong>t, son<strong>de</strong>rn<br />
auch bei <strong>de</strong>n an<strong>de</strong>ren <strong>Algorithmen</strong>. Eine Ausnahme bil<strong>de</strong>te dabei die Anwendung<br />
<strong>de</strong>s Chinesischen Restsatzes, da dieser keine Kommunikation bedarf. Soll das Programm<br />
für die Anwendung <strong>de</strong>s Chinesischen Restsatzes parallelisiert wer<strong>de</strong>n, muss lediglich die<br />
Schleife durch eine parallele Anweisung ersetzt wer<strong>de</strong>n.<br />
100
6. Zusammenfassung und Ausblick<br />
In dieser Arbeit wur<strong>de</strong> anfangs ein Überblick über momentan verwen<strong>de</strong>te parallele Mo<strong>de</strong>lle<br />
geliefert. Die Auswahl umfasst hierbei die P-RAM, die parallelen Pointer-Maschinen<br />
und die Zellularautomaten. Innerhalb <strong>de</strong>r Zellularautomaten wur<strong>de</strong> <strong>de</strong>r ”<br />
Global Cellular<br />
Automata “ (GCA) als Mo<strong>de</strong>ll vorgestellt. Dieser wur<strong>de</strong> im weiteren verwen<strong>de</strong>t.<br />
In Kapitel 3 und Kapitel 4 wur<strong>de</strong>n dann <strong>Algorithmen</strong> auf <strong>de</strong>m GCA mo<strong>de</strong>lliert. Im<br />
<strong>Ra</strong>hmen <strong>de</strong>r Mo<strong>de</strong>llierungen <strong>de</strong>r Graphen-<strong>Algorithmen</strong> in Kapitel 3 wur<strong>de</strong> auch eine<br />
Speicherzelle entwickelt. Diese ist als Konstrukt portabel und erlaubt es <strong>de</strong>m GCA,<br />
einen gemeinsamen Speicher zu simulieren. Es können allerdings keine Daten in <strong>de</strong>n<br />
Speicher geschrieben wer<strong>de</strong>n, son<strong>de</strong>rn die Speicherzelle muss sich aktiv die Daten bei<br />
<strong>de</strong>n Zellen holen und an <strong>de</strong>n von <strong>de</strong>n Zellen vorgeschriebenen Platz schreiben. Auch<br />
gestaltet sich ein Lese-Zugriff auf <strong>de</strong>n Speicher schwerer, da eine Zelle erst die Stelle<br />
angeben muss, von <strong>de</strong>r sie lesen möchte, bevor die Speicherzelle <strong>de</strong>n Wert bereitstellt.<br />
Trotz<strong>de</strong>m ermöglicht die Speicherzelle eine Mo<strong>de</strong>llierung von P-RAM-Programmen auf<br />
<strong>de</strong>m GCA. Der Autorin sind momentan keine Fälle bekannt, in <strong>de</strong>nen ein sofortiger<br />
wahlfreier Zugriff auf die Daten <strong>de</strong>s Speichers nötig ist.<br />
Ein weiteres neu entwickeltes Konstrukt ist die Bus-Zelle. Erscheint es in einer Anwendung<br />
als sinnvoll, einen Bus zu verwen<strong>de</strong>n, ist dies aber aus diversen Grün<strong>de</strong>n nicht<br />
möglich, so kann die Bus-Zelle verwen<strong>de</strong>t wer<strong>de</strong>n. Allerdings ist bei <strong>de</strong>r Bus-Zelle eine<br />
geeignete Verwal<strong>tu</strong>ng notwendig, damit die Bus-Zelle weiß, bei welcher Zelle sie momentan<br />
lesen muss. Im Falle <strong>de</strong>s Warshall-Algorithmus ist das einfach, allerdings gestaltet<br />
sich die Verwendung <strong>de</strong>s Busses nicht immer einfach.<br />
Für eine Entscheidung, ob die Bus-Zelle sinnvoll verwendbar ist o<strong>de</strong>r nicht, kann das<br />
in Kapitel 2.3.4 vorgestellte Schema verwen<strong>de</strong>t wer<strong>de</strong>n. Es eignen sich vor allem solche<br />
<strong>Algorithmen</strong>, <strong>de</strong>ren Verbindungen möglichst von nichts o<strong>de</strong>r nur <strong>de</strong>r Zeit (und even<strong>tu</strong>ell<br />
wie beim Warshall-Algorithmus von <strong>de</strong>n Daten) abhängen.<br />
Im weiteren sind die in Kapitel 3 gegebenen <strong>Algorithmen</strong> ein gutes Beispiel dafür, dass<br />
parallel entwickelte <strong>Algorithmen</strong> meist effizienter sind als parallelisierte sequentielle <strong>Algorithmen</strong>.<br />
Die zeigt sich u. a. in <strong>de</strong>m Vergleich zwischen Warshall-Algorithmus (O(n))<br />
und <strong>de</strong>m Algorithmus von Hirschberg (O(log 2 (n))).<br />
Allerdings ist die Laufzeitkomplexität meist trügerisch. Die <strong>Algorithmen</strong> <strong>de</strong>r Klasse O(n)<br />
beispielsweise müssen erst ab einem Wert n 0 langsamer o<strong>de</strong>r gleich schnell wie n wachsen.<br />
Dieses n 0 wird in <strong>de</strong>r Laufzeitkomplexität allerdings nicht angegeben.<br />
Betrachtet man laufzeiteffiziente <strong>Algorithmen</strong> genauer, kann man also u. U. feststellen,<br />
dass dieses n 0 größer als normale Problemgrößen ist. Ein Beispiel hierfür ist Coles Al-<br />
101
gorithmus, <strong>de</strong>r erst ab einem n 0 ≥ 2 70 genauso langsam wie log(n) wächst. Es erscheint<br />
in solchen Fällen sinnvoller, einen langsameren Algorithmus (statt Coles Algorithmus z.<br />
B. das Bitonische Mischen) zu verwen<strong>de</strong>n, <strong>de</strong>r innerhalb <strong>de</strong>r gewünschten Problemgröße<br />
ein besseres Laufzeitverhalten aufweist.<br />
Auch in Zukunft wird es interessant sein, parallele <strong>Algorithmen</strong> zu entwickeln. Da zur<br />
heutigen Zeit wie<strong>de</strong>r vermehrt parallele Architek<strong>tu</strong>ren entworfen und gebaut wer<strong>de</strong>n,<br />
erscheint es auch sinnvoll, die Möglichkeiten dieser Architek<strong>tu</strong>ren genauer zu s<strong>tu</strong>dieren.<br />
Ein Algorithmus, <strong>de</strong>r sich optimal an die Gegebenheiten <strong>de</strong>r Architek<strong>tu</strong>r anpasst und für<br />
sie entwickelt wur<strong>de</strong>, wird in <strong>de</strong>n meisten Fällen um einiges besser sein als ein allgemeiner<br />
Algorithmus. Da solche <strong>Algorithmen</strong> jedoch nicht mehr portabel sind, ist zu bezweifeln,<br />
dass eine Architek<strong>tu</strong>r es schafft, einen so großen Ausbrei<strong>tu</strong>ngsgrad zu erreichen, dass<br />
sich solch ein Aufwand lohnt.<br />
Bis zu <strong>de</strong>m Zeitpunkt an <strong>de</strong>m es eine ”<br />
universelle“ Architek<strong>tu</strong>r gibt, die mehr als 50%<br />
<strong>de</strong>s Marktanteils ausmacht, wer<strong>de</strong>n nur teure Speziallösungen wirklich auf die Architek<strong>tu</strong>r<br />
angepasst wer<strong>de</strong>n. Es ist zu erwarten, dass solange es keine solche Architek<strong>tu</strong>r gibt,<br />
auch weiterhin <strong>Algorithmen</strong> auf Automatenmo<strong>de</strong>llen entwickelt wer<strong>de</strong>n. Denn Automatenmo<strong>de</strong>lle<br />
erlauben es, dass man einen Algorithmus entwickeln kann, ohne die genaue<br />
Architek<strong>tu</strong>r zu kennen, auf <strong>de</strong>m das Programm später läuft. Wer <strong>de</strong>n Algorithmus dann<br />
implementieren will, kann sich immer noch entschei<strong>de</strong>n, ob er dies sehr architek<strong>tu</strong>rnah<br />
o<strong>de</strong>r so abstrakt wie möglich realisieren möchte.<br />
102
A. Traversierungsstrategien<br />
Im folgen<strong>de</strong>n sollen einige Traversierungsstrategien angegeben wer<strong>de</strong>n, die verwen<strong>de</strong>t<br />
wer<strong>de</strong>n, um schwierige Probleme zu lösen.<br />
A.1. Backtracking<br />
Backtracking liefert einen Algorithmus, um struk<strong>tu</strong>riert alle möglichen Lösungen durchzutesten.<br />
Dabei wird immer versucht, auf <strong>de</strong>r bislang erzielten Teillösung aufbauend, <strong>de</strong>m<br />
En<strong>de</strong>rgebnis näher zu kommen. Entsteht dabei ein Konflikt, so wird die vorhergehen<strong>de</strong><br />
Entscheidung zurückgenommen und somit die Ausgangslage verän<strong>de</strong>rt. Das Verfahren<br />
soll anhand <strong>de</strong>s Acht-Damen-Problems erläutert wer<strong>de</strong>n. Aufgabe ist es dabei, auf einem<br />
8x8-Schachbrett acht Damen so zu positionieren, dass keine Dame eine an<strong>de</strong>re schlagen<br />
kann.<br />
Die erste Dame wird in die obere linke Ecke gesetzt, Abb. A.1 ver<strong>de</strong>utlicht zu<strong>de</strong>m auch<br />
noch alle Fel<strong>de</strong>r, auf die keine Dame mehr gestellt wer<strong>de</strong>n kann.<br />
Die zweite Dame kann nun we<strong>de</strong>r auf <strong>de</strong>m ersten noch <strong>de</strong>m zweiten Feld positioniert<br />
wer<strong>de</strong>n, also lan<strong>de</strong>t sie auf <strong>de</strong>m dritten Feld <strong>de</strong>r zweiten Zeile. In Abb. A.2 ist die Dame<br />
schon positioniert und es wur<strong>de</strong> wie<strong>de</strong>r zum Verständnis markiert, welche Fel<strong>de</strong>r alle<br />
für die zukünftige Positionierung von Damen gesperrt ist. Die dritte Dame kann jetzt<br />
erst an die vierte Stelle <strong>de</strong>r dritten Zeile positioniert wer<strong>de</strong>n, auch dies wird wie<strong>de</strong>r mit<br />
einer Abbildung ver<strong>de</strong>utlicht. Abb. A.3 zeigt das Schachbrett nach <strong>de</strong>r Positionierung<br />
Abbildung A.1.: Die erste Dame wur<strong>de</strong> in die obere linke Ecke positioniert. Da Damen<br />
gera<strong>de</strong>aus und diagonal gehen können, ist die restliche Zeile sowie die<br />
Spalte und die Diagonale als gesperrt markiert. Diese Markierung dient<br />
nur <strong>de</strong>m Verständnis, <strong>de</strong>r Algorithmus liefert sie nicht.<br />
103
Abbildung A.2.: Die zweite Dame wur<strong>de</strong> an die dritte Position <strong>de</strong>r zweiten Zeile positioniert.<br />
Da Damen gera<strong>de</strong>aus und diagonal gehen können, ist die restliche<br />
Zeile sowie die Spalte und die Diagonale als gesperrt markiert.<br />
Abbildung A.3.: Die dritte Dame wur<strong>de</strong> auf das erste Feld in <strong>de</strong>r dritten Zeile gesetzt,<br />
welches keinen Konflikt auslöst. Es wur<strong>de</strong>n wie<strong>de</strong>r alle Fel<strong>de</strong>r markiert,<br />
die von <strong>de</strong>r dritten Dame geschlagen wer<strong>de</strong>n.<br />
<strong>de</strong>r dritten Dame. Im nächsten Schritt wird dann die vierte Dame in <strong>de</strong>r vierten Zeile<br />
positioniert.<br />
Das Schachbrett nach <strong>de</strong>m positionieren <strong>de</strong>r vierten Dame zeigt Abb. A.4.<br />
Nach<strong>de</strong>m die fünfte Dame positioniert wur<strong>de</strong>, sieht das Feld wie in Abb. A.5 aus. Durch<br />
die Markierung wird <strong>de</strong>utlich, dass die sechste Dame nicht mehr gesetzt wer<strong>de</strong>n kann,<br />
ohne dass sie sofort geschlagen wird. Eine korrekte Lösung ist also bei <strong>de</strong>r vorher getroffenen<br />
Annahme nicht möglich.<br />
Da die sechste Dame also nicht positioniert wer<strong>de</strong>n kann, muss die Positionierung <strong>de</strong>r<br />
fünften Dame falsch sein. Also wird die fünfte Dame wie<strong>de</strong>r vom Brett genommen und<br />
neu positioniert. Dabei wird darauf geachtet, dass die fünfte Dame nicht mehr an die<br />
Stelle kommen kann, an <strong>de</strong>r sie davor saß. Dieses Zurücknehmen einer Entscheidung<br />
kennzeichnet das Backtracking. Allerdings wird auch durch die Neupositionierung <strong>de</strong>r<br />
fünften Dame (s. Abb. A.6) kein Feld in <strong>de</strong>r sechsten Zeile frei, weshalb die fünfte Dame<br />
komplett vom Feld genommen wird und die vierte Dame neu positioniert wird. Die neue<br />
Si<strong>tu</strong>ation ist in Abb. A.7 dargestellt.<br />
Diese Schritte <strong>de</strong>s Setzens und Zurücknehmens von Damen wird solange wie<strong>de</strong>rholt, bis<br />
104
Abbildung A.4.: Die vierte Dame wur<strong>de</strong> auf das erste Feld in <strong>de</strong>r dritten Zeile gesetzt,<br />
welches keinen Konflikt auslöst. Es wur<strong>de</strong>n wie<strong>de</strong>r alle Fel<strong>de</strong>r markiert,<br />
die von <strong>de</strong>r dritten Dame geschlagen wer<strong>de</strong>n.<br />
Abbildung A.5.: Die fünfte Dame ist auf <strong>de</strong>m Feld positioniert wor<strong>de</strong>n und es ist ersichtlich,<br />
dass die sechste Dame nicht mehr positioniert wer<strong>de</strong>n kann.<br />
Abbildung A.6.: Die fünfte Dame ist auf <strong>de</strong>m Feld reposiotioniert wor<strong>de</strong>n, in <strong>de</strong>r sechsten<br />
Zeile kann trotz<strong>de</strong>m keine Dame positioniert wer<strong>de</strong>n. Es ist also<br />
ein weiterer Backtracking-Schritt notwendig.<br />
105
Abbildung A.7.: Nach<strong>de</strong>m die vierte Dame auf <strong>de</strong>m Feld repositioniert wur<strong>de</strong>, gibt es<br />
zwei interessante Stellen, an die die fünfte Dame positioniert wer<strong>de</strong>n<br />
kann.<br />
die achte Dame positioniert wur<strong>de</strong>, dann ist eine Lösung für das Problem gefun<strong>de</strong>n.<br />
Anzumerken ist, dass <strong>de</strong>r Algorithmus auch die Stellen, die in <strong>de</strong>n Abbildungen mit<br />
einem * gekennzeichnet sind, abtestet und somit viele Tests vonnöten sind. Beim Acht-<br />
Damen-Problem ist die Laufzeit aus zwei Grün<strong>de</strong>n erträglich:<br />
1. Die Anzahl <strong>de</strong>r Damen ist begrenzt und sehr klein.<br />
2. Es existiert mehr als eine Lösung, je<strong>de</strong> Lösung wird als gleichwertig angesehen,<br />
weshalb nach <strong>de</strong>m Fin<strong>de</strong>n einer Lösung <strong>de</strong>r Algorithmus been<strong>de</strong>t ist.<br />
Diese zwei Punkte gelten nicht für alle Probleme, die mittels Backtracking gelöst wer<strong>de</strong>n<br />
müssen, so dass das exponentielle Verhalten <strong>de</strong>s Backtrackings die meisten Lösungen<br />
uninteressant machen.<br />
A.2. Divi<strong>de</strong> and Conquer<br />
Für das Divi<strong>de</strong>-and-Conquer-Verfahren eignen sich solche Problemstellungen, die sich in<br />
unabhängige Teilprobleme zerlegen lassen. Der bekannteste Algorithmus, <strong>de</strong>r nach <strong>de</strong>m<br />
Divi<strong>de</strong>-and-Conquer-Verfahren arbeitet, ist <strong>de</strong>r Quicksort.<br />
Quicksort macht eine Vorsortierung auf <strong>de</strong>r gegebenen Liste und teilt diese dann in<br />
zwei Teillisten. Je<strong>de</strong> <strong>de</strong>r Teillisten kann für sich allein sortiert wer<strong>de</strong>n, und am Schluss<br />
müssen die Teillisten nur noch zusammengehängt wer<strong>de</strong>n, um das korrekte Gesamtergebnis<br />
zu erhalten. Divi<strong>de</strong>-and-Conquer wird in <strong>de</strong>r Regel rekursiv verwen<strong>de</strong>t, da die<br />
Lösung so verständlicher ist 1 . Im Falle <strong>de</strong>s Quicksorts be<strong>de</strong>utet dies, dass je<strong>de</strong> Liste vorsortiert<br />
wird und dann auf die Teillisten wie<strong>de</strong>r <strong>de</strong>r Quicksort angewen<strong>de</strong>t wird. Dabei<br />
1 Sequentielle rekursive Lösungen weisen meist ein schlechteres Laufzeitverhalten auf als iterative<br />
Lösungen. Trotz<strong>de</strong>m verzichtet man meist darauf, die <strong>Algorithmen</strong> iterativ zu programmieren, da<br />
rekursive Programme übersichtlicher und somit für <strong>de</strong>n Fachmann leichter lesbar sind. Grundsätzlich<br />
können alle rekursiven Programme in iterative Programme umgearbeitet wer<strong>de</strong>n, allerdings ist<br />
oft <strong>de</strong>r Aufwand dafür recht groß und das Programm wird unleserlich.<br />
106
Abbildung A.8.: Die Startinitialisierung <strong>de</strong>s Quicksorts. Das Pivot ist durch einen<br />
dicken Kasten markiert, die ersten zu vergleichen<strong>de</strong>n Elemente durch<br />
die Zeiger i und j. Das Pivot-Element wird anfangs nicht verglichen<br />
son<strong>de</strong>rn am En<strong>de</strong> <strong>de</strong>s Vorsortierens an <strong>de</strong>n korrekten Platz getauscht.<br />
Abbildung A.9.: Die Vorsortierung wur<strong>de</strong> been<strong>de</strong>t. Da j links von i steht, wird die Vorsortierung<br />
abgeschlossen.<br />
wird eine einelementige Liste als sortiert betrachtet. Die Länge <strong>de</strong>r Liste bietet also das<br />
Abbruchkriterium, mit <strong>de</strong>m die Rekursion kontrolliert been<strong>de</strong>t wird.<br />
Zur Veranschaulichung soll die Liste (7, 5, 10, 1, 3, 4, 9, 6, 12, 15, 2, 20, 22, 14) sortiert wer<strong>de</strong>n.<br />
Dazu wird ein Vergleichselement, das sogenannte Pivot, gewählt und alle Werte<br />
mit diesem verglichen. Werte die kleiner als das Pivot sind, wer<strong>de</strong>n nach links, größere<br />
Werte nach rechts sortiert. Wie dies genau geschieht hängt von <strong>de</strong>r jeweiligen Implementierung<br />
ab, hier wer<strong>de</strong>n zwei Zeiger i und j verwen<strong>de</strong>t, die <strong>de</strong>n Vergleich kleinergleich<br />
(i) bzw. größer (j) durchführen und dazu am linken En<strong>de</strong> (i) bzw. rechten En<strong>de</strong> (j) <strong>de</strong>r<br />
Liste starten. Als Pivot wird hier das linke Element <strong>de</strong>r Liste gewählt. Die Ausgangssi<strong>tu</strong>ation<br />
ist in Abbildung A.8 dargestellt. Das Pivot-Element ist dick umran<strong>de</strong>t, i zeigt<br />
auf das zweite Feld von links (da das linke Element das Pivot ist, erübrigt sich hier ein<br />
Vergleich), j auf das letzte Element <strong>de</strong>r Liste.<br />
Im Laufe <strong>de</strong>s Algorithmus wird <strong>de</strong>r linke Zeiger i immer eine Stelle nach rechts gerückt,<br />
wenn <strong>de</strong>r Wert auf <strong>de</strong>n i momentan zeigt kleinergleich <strong>de</strong>m Pivot-Element ist. Ist <strong>de</strong>r<br />
Vergleich nicht erfolgreich (ist also <strong>de</strong>r Wert an <strong>de</strong>r Stelle i größer als das Pivot-Element),<br />
so bleibt i stehen und <strong>de</strong>r Zeiger j wird solange nach links gerückt, bis entwe<strong>de</strong>r ein Wert<br />
gefun<strong>de</strong>n wird, <strong>de</strong>r kleinergleich <strong>de</strong>m Pivot ist o<strong>de</strong>r bis <strong>de</strong>r Zeiger j links vom Zeiger i<br />
steht. Bleibt j auf einem Feld stehen, <strong>de</strong>ssen Inhalt kleinergleich <strong>de</strong>m Pivot ist, so wird<br />
getauscht, ansonsten kann <strong>de</strong>r rekursive Aufruf erfolgen.<br />
Für <strong>de</strong>n rekursiven Aufruf wird zuerst das Pivot-Element mit <strong>de</strong>m Wert an <strong>de</strong>r j-ten<br />
Stelle getauscht. Dann wird Quicksort auf die Liste von <strong>de</strong>r ersten Stelle bis zur Stelle j-1<br />
und von <strong>de</strong>r j+1-ten Stelle bis zur letzten Stelle <strong>de</strong>r Liste aufgerufen. Abbildung A.9 zeigt<br />
die Liste nach <strong>de</strong>r Vorsortierung, wobei das Pivot-Element noch nicht getauscht wur<strong>de</strong>.<br />
In Abbildung A.10 wur<strong>de</strong> <strong>de</strong>r Tausch <strong>de</strong>s Pivots bereits durchgeführt, das Pivot-Element<br />
steht an <strong>de</strong>r korrekten Stelle. Zu<strong>de</strong>m wur<strong>de</strong> die Liste aufgeteilt, dies wird durch die Zeiger<br />
i1, j1, i2 und j2 ver<strong>de</strong>utlicht, welche für die linke bzw. rechte Teilliste die Grenzen mar-<br />
107
kieren. Für bei<strong>de</strong> Teillisten wur<strong>de</strong> das Pivot dick umran<strong>de</strong>t. Für diese bei<strong>de</strong>n Teillisten<br />
wird wie<strong>de</strong>r wie beschrieben die Vorsortierung ausgeführt und wie<strong>de</strong>r geteilt. Dabei wird<br />
bei je<strong>de</strong>r Vorsortierung min<strong>de</strong>stens ein Element (das Pivot-Element) endgültig sortiert.<br />
Abbildung A.10.: Das Pivot wur<strong>de</strong> an seine endgültige Stelle getauscht und die Liste in<br />
zwei Teillisten zerlegt. Die umran<strong>de</strong>ten Fel<strong>de</strong>r geben die Pivots für die<br />
Teillisten an, i1,j1,i2 und j2 bezeichnen die Grenzen <strong>de</strong>r Teillisten.<br />
Der Quicksort wird mit diesen Teillisten rekursiv ausgeführt.<br />
108
B. <strong>Parallele</strong> Algorithmische Techniken<br />
Bei parallelen Systemen gibt es zusätzlich zu <strong>de</strong>n bekannten sequentiellen Techniken<br />
(s. vorheriges Kapitel) noch die Möglichkeit, die Kommunikation <strong>de</strong>r Prozessoren nach<br />
bestimmten Prinzipien aufzubauen und dadurch schnelle Lösungen zu erhalten. In <strong>de</strong>n<br />
nachfolgen<strong>de</strong>n Abschnitten sollen einige <strong>de</strong>r Techniken, die im <strong>Ra</strong>hmen dieser Ausarbei<strong>tu</strong>ng<br />
verwen<strong>de</strong>t o<strong>de</strong>r erwähnt wer<strong>de</strong>n, näher erklärt wer<strong>de</strong>n. Meist fin<strong>de</strong>t sich bereits<br />
im Text eine kurze Erläuterung zu <strong>de</strong>n Metho<strong>de</strong>n, allerdings wer<strong>de</strong>n hier noch einmal<br />
an<strong>de</strong>re Beispiele angeführt, um die Metho<strong>de</strong>n weiter zu ver<strong>de</strong>utlichen. Die benötigte Anzahl<br />
an Prozessoren kann bei <strong>de</strong>n meisten Metho<strong>de</strong>n verringert wer<strong>de</strong>n, aber in diesem<br />
Anhang wird nicht näher auf diese Möglichkeiten eingegangen, <strong>de</strong>r interessierte Leser sei<br />
an [GR98] verwiesen.<br />
B.1. Die Balanced-Binary-Tree-Technik<br />
Grundlage <strong>de</strong>r Balanced-Binary-Tree-Technik ist ein vollständiger Binärbaum. Dieser<br />
Baum wird <strong>de</strong>rmaßen belegt, dass in die Blätter die Werte geschrieben wer<strong>de</strong>n, die bearbeitet<br />
wer<strong>de</strong>n sollen. Die inneren Knoten wer<strong>de</strong>n durch Prozessoren dargestellt, die<br />
auf <strong>de</strong>n Werten ihrer Söhne einen Teil <strong>de</strong>r gefor<strong>de</strong>rten Berechnung ausführen. Dementsprechend<br />
arbeiten zuerst die Prozessoren, die in <strong>de</strong>r untersten Ebene angeordnet sind,<br />
und erst wenn diese ihr Ergebnis haben, können die Prozessoren <strong>de</strong>r Ebene darüber<br />
ihre Berechnung ausführen. Probleme, die sich effizient mit <strong>de</strong>r Balanced-Binary-Tree-<br />
Technik lösen lassen, sind das Fin<strong>de</strong>n <strong>de</strong>s Minimums, <strong>de</strong>s Maximums o<strong>de</strong>r <strong>de</strong>r Summe<br />
einer gegebenen Menge von Zahlen.<br />
Das erfolgreiche Auffin<strong>de</strong>n <strong>de</strong>s Minimums <strong>de</strong>r Liste mit Hilfe <strong>de</strong>r Technik wur<strong>de</strong> bereits<br />
in Kapitel 3.1.3.2 vorgestellt, ein Beispiel für das Maximum fin<strong>de</strong>t sich u. a. in [GR98].<br />
Aus diesem Grund soll hier das Beispiel zur Bestimmung <strong>de</strong>r Summe anhand <strong>de</strong>r Abbildung<br />
B.1 veranschaulicht wer<strong>de</strong>n. Die zu untersuchen<strong>de</strong>n Werte stehen in <strong>de</strong>n Blättern<br />
<strong>de</strong>s Baums und je<strong>de</strong>r Knoten addiert seine Söhne. Damit ist die Summe <strong>de</strong>s Knotens berechnet<br />
und sein Vater kann mit diesem Teilergebnis weiterrechnen. Die Wurzel enthält<br />
am En<strong>de</strong> das Gesamtergebnis. Möchte man die Summe einer Liste bestimmen, <strong>de</strong>ren<br />
Länge keine Zweierpotenz ist, so müssen die freien Blätter mit <strong>de</strong>m Wert 0 gefüllt wer<strong>de</strong>n,<br />
damit das Ergebnis nicht verfälscht wird.<br />
109
Abbildung B.1.: In <strong>de</strong>n Blättern stehen die Werte <strong>de</strong>ren Summe gebil<strong>de</strong>t wer<strong>de</strong>n soll.<br />
Je<strong>de</strong>r Prozessor (innerer Knoten) berechnet die Summe für seine Nachfolger.<br />
Das Ergebnis steht nach log(n) Schritten in <strong>de</strong>r Wurzel.<br />
Der Pseudoco<strong>de</strong> zur Bestimmung <strong>de</strong>r Summe lautet:<br />
Listing B.1: Berechnung <strong>de</strong>r Summe mit <strong>de</strong>r Balanced-Binary-Tree-Technik<br />
1 for k
Abbildung B.2.: Je<strong>de</strong>r Prozessor repräsentiert einen Wert <strong>de</strong>r Liste. Für je<strong>de</strong>s Listenelement<br />
wird nacheinan<strong>de</strong>r <strong>de</strong>r Abstand zum Listenen<strong>de</strong> bestimmt. Die<br />
Graphik zeigt die einzelnen Schleifendurchläufe <strong>de</strong>r Doubling-Technik,<br />
die unterste Abbildung entspricht <strong>de</strong>m En<strong>de</strong>rgebnis.<br />
B.3. Die Divi<strong>de</strong>-and-Conquer-Technik<br />
Die parallele Divi<strong>de</strong>-and-Conquer-Technik ist <strong>de</strong>r seriellen sehr ähnlich, allerdings wird<br />
bei <strong>de</strong>r parallelen gefor<strong>de</strong>rt, dass die Teilprobleme einer Rekursionsebene sich vollständig<br />
unabhängig voneinan<strong>de</strong>r lösen lassen 1 . Die Balanced-Binary-Tree-Technik kann als ein<br />
Spezialfall <strong>de</strong>r Divi<strong>de</strong>-and-Conquer-Technik angesehen wer<strong>de</strong>n, da auch dort die Knoten<br />
einer Ebene die Berechnungen unabhängig voneinan<strong>de</strong>r ausführen. Allerdings verfolgt<br />
die Balanced-Binary-Tree-Technik die Bottom-up-Metho<strong>de</strong> 2 , während die Divi<strong>de</strong>-and-<br />
Conquer-Technik die Top-down-Metho<strong>de</strong> 3 verwen<strong>de</strong>t. Die Divi<strong>de</strong>-and-Conquer-Technik<br />
kann für alle Probleme angewen<strong>de</strong>t wer<strong>de</strong>n, die auch bei <strong>de</strong>r Balanced-Binary-Tree-<br />
Technik angeführt wur<strong>de</strong>n (natürlich sind dies nicht die einzigen Probleme, für die die<br />
bei<strong>de</strong>n Techniken sich eignen, allerdings sind sie einfach und damit leicht nachvollziehbar).<br />
So kann das Minimum einer Liste z. B. dadurch bestimmt wer<strong>de</strong>n, dass man das<br />
Minimum in <strong>de</strong>r linken Hälfte und in <strong>de</strong>r rechten Hälfte <strong>de</strong>r Liste bestimmt. Danach<br />
muss aus diesen bei<strong>de</strong>n Werten nur noch das Minimum bestimmt wer<strong>de</strong>n und das Gesamtergebnis<br />
ist gefun<strong>de</strong>n.<br />
1 Meist wird diese Bedingung auch im sequentiellen Fall erfüllt, da die Probleme dadurch überschaubarer<br />
bleiben<br />
2 Bei <strong>de</strong>r Bottom-up-Metho<strong>de</strong> wer<strong>de</strong>n zuerst die Teilprobleme gelöst und diese nach und nach zu <strong>de</strong>r<br />
Gesamtlösung zusammengesetzt.<br />
3 Bei <strong>de</strong>r Top-down-Metho<strong>de</strong> wird das zu lösen<strong>de</strong> Problem solange weiter zerlegt, bis die gefun<strong>de</strong>nen<br />
Teilprobleme leicht lösbar sind.<br />
111
C. Implementierungen <strong>de</strong>r <strong>Algorithmen</strong><br />
C.1. Warshall-Algorithmus<br />
Listing C.1: Implementierung <strong>de</strong>s Warshall-Algorithmus<br />
1<br />
2 #inclu<strong>de</strong> <br />
3 #inclu<strong>de</strong> <br />
4 #inclu<strong>de</strong> <br />
5 #inclu<strong>de</strong> <br />
6 #<strong>de</strong>fine N 30<br />
7 #<strong>de</strong>fine DATEI ”Matrix0 . dat”<br />
8 #<strong>de</strong>fine ANZAHL WIEDERHOLUNGEN 1<br />
9 #<strong>de</strong>fine Debug 1<br />
10 FILE ∗ f i l e ;<br />
11 struct handle {<br />
12 int row [N] ;<br />
13 int ID ;<br />
14 };<br />
15<br />
16 int takt [N] ;<br />
17 int currentID ;<br />
18 int done ;<br />
19 int Bus [N] ;<br />
20 int ak<strong>tu</strong>al ;<br />
21 int NumReady;<br />
22 int Result [N] [N] ;<br />
23<br />
24 void c e l l ( struct handle ∗ give ) {<br />
25 int i ;<br />
26 int ownRow[N] ;<br />
27 int ownID=(∗give ) . ID ;<br />
28 for ( i =0; i
67 void main () {<br />
68 struct handle give [N] ;<br />
69 int i , j , k ;<br />
70 char da<strong>tu</strong>m ;<br />
71 LARGE INTEGER start ticks , en<strong>de</strong> ticks , frequenz ;<br />
72 double t i c k d i f f = 0;<br />
73 for ( i =0; i
24<br />
25 void hirschberg ( int GCA[N] [N] ) {<br />
26 int i , j , k , l ;<br />
27 int Thelp [N] ;<br />
28 int ∗ ak<strong>tu</strong>al ;<br />
29 int ∗ start ;<br />
30 int ∗ old ;<br />
31 int found ; /∗ z e i g t an , ob <strong>de</strong>r gesuchte Eintrag gefun<strong>de</strong>n wur<strong>de</strong> ∗/<br />
32<br />
33 for ( l =1; l
114 int complete [N]={0 ,0 ,0 ,0 ,0 ,0 ,0 ,0};<br />
115 LARGE INTEGER start ticks , en<strong>de</strong> ticks , frequenz ;<br />
116 double t i c k d i f f = 0;<br />
117 QueryPerformanceFrequency(&frequenz ) ;<br />
118 printf (”Beginne mit <strong>de</strong>r Messung <strong>de</strong>r benoetigten Zeit <strong>de</strong>s Hirschbergalgorithmus \n”) ;<br />
119 /∗ I n i t i a l i s i e r u n g Zeitmessung ∗/<br />
120 s t a r t t i c k s . QuadPart = 0;<br />
121 en<strong>de</strong> ticks . QuadPart = 0;<br />
122 QueryPerformanceCounter(& s t a r t t i c k s ) ;<br />
123 hirschberg (GCA) ;<br />
124 /∗ Abschliessen <strong>de</strong>r Zeitmessung ∗/<br />
125 QueryPerformanceCounter(&en<strong>de</strong> ticks ) ;<br />
126 t i c k d i f f = ((double) en<strong>de</strong> ticks . QuadPart − (double) s t a r t t i c k s . QuadPart) /<br />
127 frequenz . QuadPart ;<br />
128 printf (” Benoetigte Zeit in Sekun<strong>de</strong>n %f \n” , t i c k d i f f ) ;<br />
129 printf (”Ergebnis : ”) ;<br />
130 /∗Ausgabe <strong>de</strong>r Komponenten ∗/<br />
131 for ( i =0; i
51 Graph [ last ]=Graph [ 0 ] ;<br />
52 last −−;<br />
53 heap (0 , last ) ;<br />
54 re<strong>tu</strong>rn ret ;<br />
55 }<br />
56<br />
57 void main () {<br />
58 int tree [N] ;<br />
59 int number ;<br />
60 int in , i , j , tested , trunc , keep ;<br />
61 struct vector edge ;<br />
62 struct vector spanntree [N−1];<br />
63 for ( i =0; i
141 number++;<br />
142 in = 1;<br />
143<br />
144 /∗Die Teilbaeume zusammenfuegen . ∗/<br />
145 if ( in==1){<br />
146 for ( j =0; j
56 }<br />
57 Tglobal [ i ] = Cglobal [ to [ i ] ] ;<br />
58 value [ i ] = GCA[ i ] [ to [ i ] ] ;<br />
59 }/∗En<strong>de</strong> erster T e i l s c h r i t t<br />
60 / ∗2. T e i l s c h r i t t von Schritt 1. Auch hier i s t die s e r i e l l e Loesung wie<strong>de</strong>r schlechter<br />
61 als die parallele , das Minimum muss e x p l i z i t gesucht wer<strong>de</strong>n und es muss gespeichert wer<strong>de</strong>n , von<br />
62 wo die Kante kam. ∗/<br />
63 for ( i =0; i 0) ) {<br />
66 doppelt [ i ]= to [ i ] ;<br />
67 }<br />
68 else doppelt [ i ]=N;<br />
69 }<br />
70 for ( i =0; i
146 /∗ Abschliessen <strong>de</strong>r Zeitmessung ∗/<br />
147 QueryPerformanceCounter(&en<strong>de</strong> ticks ) ;<br />
148 t i c k d i f f = ((double) en<strong>de</strong> ticks . QuadPart − (double) s t a r t t i c k s . QuadPart) /<br />
149 frequenz . QuadPart ;<br />
150 printf (” Benoetigte Zeit in Sekun<strong>de</strong>n %f \n” , t i c k d i f f ) ;<br />
151 printf (”Ergebnis : ”) ;<br />
152 /∗Ausgabe <strong>de</strong>r Komponenten ∗/<br />
153 for ( i =0; ikey . points [ 1 ] != key2 . points [ 1 ] ) ) {<br />
42 ac<strong>tu</strong>al = ac<strong>tu</strong>al −>next ;<br />
43 }<br />
44 if ( ac<strong>tu</strong>al == NULL | | ac<strong>tu</strong>al −>next == NULL && ( ac<strong>tu</strong>al −>key . points [ 0 ] != key2 . points [ 0 ] | | ac<strong>tu</strong>al<br />
−>key . points [ 1 ] != key2 . points [ 1 ] ) ) {<br />
45 help = malloc ( sizeof ( struct list element ) ) ;<br />
46 (∗ help ) . key = key2 ;<br />
47 help−>value = value ;<br />
48 help−>next = NULL;<br />
49 if ( Liste == NULL) Liste = help ;<br />
50 else ac<strong>tu</strong>al −>next = help ;<br />
51 }<br />
52 else {<br />
53 ac<strong>tu</strong>al −>value = value ;<br />
119
54 }<br />
55 re<strong>tu</strong>rn Liste ;<br />
56 }<br />
57<br />
58 /∗Die Funktion get l i e f e r t zu einem Key <strong>de</strong>n zugehoerigen Value einer Kante . Die Liste b l e i b t dabei<br />
59 unveraen<strong>de</strong>rt . ∗/<br />
60 struct EDGE get ( struct list element ∗Liste , struct EDGE key ) {<br />
61 struct list element ∗ac<strong>tu</strong>al ;<br />
62 struct EDGE err ;<br />
63 int finished ;<br />
64 ac<strong>tu</strong>al = Liste ;<br />
65 finished = 0;<br />
66 err . points [0]= 0;<br />
67 err . points [1]= 0;<br />
68 while( ac<strong>tu</strong>al != NULL && finished == 0) {<br />
69 if ( ac<strong>tu</strong>al −>key . points [ 0 ] == key . points [ 0 ] && ac<strong>tu</strong>al −>key . points [ 1 ] == key . points [ 1 ] ) {<br />
70 finished = 1;<br />
71 re<strong>tu</strong>rn ac<strong>tu</strong>al −>value ;<br />
72 }<br />
73 else ac<strong>tu</strong>al= ac<strong>tu</strong>al −>next ;<br />
74 }<br />
75 re<strong>tu</strong>rn err ;<br />
76 }<br />
77<br />
78 /∗Die Funktion doubling succ fuehrt einen Durchlauf <strong>de</strong>r Doubling −Technik fuer die Successor −Liste<br />
aus . ∗/<br />
79 struct list element ∗ doubling succ ( struct list element ∗ Liste ) {<br />
80 struct list element ∗ ac<strong>tu</strong>al , ∗ start ;<br />
81<br />
82 ac<strong>tu</strong>al = Liste ;<br />
83 start = NULL;<br />
84 while( ac<strong>tu</strong>al != NULL) {<br />
85 start = set ( start , ac<strong>tu</strong>al −>key , get ( Liste , ac<strong>tu</strong>al −>value ) ) ;<br />
86 ac<strong>tu</strong>al = ac<strong>tu</strong>al −>next ;<br />
87 }<br />
88 re<strong>tu</strong>rn start ;<br />
89 }<br />
90<br />
91 /∗Fuehrt einen Schritt <strong>de</strong>r doubling −Technik fuer die Kanten aus , dabei wird die Edge−Liste<br />
a k t u a l i s i e r t .<br />
92 Im Laufe <strong>de</strong>s Schritts wird eine modifizierte Kopie <strong>de</strong>r Edge−Liste angelegt und die uebergebene<br />
Liste dann geloescht . ∗/<br />
93<br />
94 struct list element ∗ doubling edge ( struct list element ∗ Liste , struct list element ∗Successor ) {<br />
95 struct list element ∗ac<strong>tu</strong>al , ∗ start ;<br />
96 struct EDGE kante , vergleich ;<br />
97<br />
98 ac<strong>tu</strong>al = Liste ;<br />
99 start = NULL;<br />
100 while( ac<strong>tu</strong>al != NULL) {<br />
101 kante = ac<strong>tu</strong>al −>value ;<br />
102 vergleich = get ( Liste , get ( Successor , ac<strong>tu</strong>al −>key ) ) ;<br />
103 if ( kante . points [ 0 ] < vergleich . points [ 0 ] | | ( kante . points [ 0 ] == vergleich . points [ 0 ] && kante .<br />
points [ 1 ] < vergleich . points [ 1 ] ) ) {<br />
104 start = set ( start , ac<strong>tu</strong>al −>key , kante ) ;<br />
105 }<br />
106 else start = set ( start , ac<strong>tu</strong>al −>key , vergleich ) ;<br />
107 ac<strong>tu</strong>al = ac<strong>tu</strong>al −>next ;<br />
108 }<br />
109 re<strong>tu</strong>rn start ;<br />
110 }<br />
111<br />
112 /∗Die Funktion size l i e f e r t die Anzahl <strong>de</strong>r Elemente <strong>de</strong>r Liste zurueck . ∗/<br />
113 int size ( struct list element ∗ Liste ) {<br />
114 int i ;<br />
115 struct list element ∗ac<strong>tu</strong>al ;<br />
116 ac<strong>tu</strong>al = Liste ;<br />
117 i= 0;<br />
118 if ( Liste == NULL) re<strong>tu</strong>rn 0;<br />
119 else {<br />
120 while( ac<strong>tu</strong>al != NULL ) {<br />
121 i++;<br />
122 ac<strong>tu</strong>al = ac<strong>tu</strong>al −>next ;<br />
123 }<br />
124 re<strong>tu</strong>rn i ;<br />
125 }<br />
126 }<br />
127<br />
128 /∗Die Funktion s o r t l i s t l i e f e r t die s o r t i e r t e Liste zurueck . Als Sortieralgorithmus dient hier<br />
129 Bubblesort . ∗/<br />
130 struct list element ∗ s o r t l i s t ( struct list element ∗ Liste ) {<br />
131 struct list element ∗switcher , ∗ac<strong>tu</strong>al , ∗previous ;<br />
132 struct EDGE help ;<br />
133 int i , j ;<br />
134<br />
135 for ( i = 0; i< size ( Liste ) ; i++){<br />
136 ac<strong>tu</strong>al = Liste ;<br />
137 previous = Liste ;<br />
138 for ( j =0; j< size ( Liste ) −1; j++){<br />
139 if ( ac<strong>tu</strong>al != NULL && ac<strong>tu</strong>al −>next != NULL) {<br />
120
140 if (( ac<strong>tu</strong>al −>key . points [ 0 ] > ac<strong>tu</strong>al −>next−>key . points [ 0 ] ) | | ( ac<strong>tu</strong>al −>key . points [ 0 ] ==<br />
ac<strong>tu</strong>al −>next−>key . points [ 0 ] && ac<strong>tu</strong>al −>key . points [ 1 ] > ac<strong>tu</strong>al −>next−>key . points [ 1 ] ) ) {<br />
141 if ( j==0){<br />
142 switcher = ac<strong>tu</strong>al −>next ;<br />
143 ac<strong>tu</strong>al −>next = switcher −>next ;<br />
144 switcher −>next = ac<strong>tu</strong>al ;<br />
145 Liste = switcher ;<br />
146 }<br />
147 else {<br />
148 switcher = ac<strong>tu</strong>al −>next ;<br />
149 ac<strong>tu</strong>al −>next = switcher −>next ;<br />
150 switcher −>next = ac<strong>tu</strong>al ;<br />
151 previous −>next = switcher ;<br />
152 help=get ( Liste , ac<strong>tu</strong>al −>key ) ;<br />
153 previous = previous −>next ;<br />
154 }<br />
155 }<br />
156 else {<br />
157 if ( j != 0) previous = previous −>next ;<br />
158 ac<strong>tu</strong>al = ac<strong>tu</strong>al −>next ;<br />
159 }<br />
160 }<br />
161 }<br />
162 }<br />
163 re<strong>tu</strong>rn Liste ;<br />
164 }<br />
165 /∗Die Funktion strange sort l i e f e r t die s o r t i e r t e Liste zurueck . Als Sortieralgorithmus dient hier<br />
166 Bubblesort . ∗/<br />
167 struct list element ∗ strange sort ( struct list element ∗ Liste ) {<br />
168 struct list element ∗switcher , ∗ac<strong>tu</strong>al , ∗previous ;<br />
169 struct EDGE help ;<br />
170 int i , j ;<br />
171<br />
172 for ( i = 0; i< size ( Liste ) ; i++){<br />
173 ac<strong>tu</strong>al = Liste ;<br />
174 previous = Liste ;<br />
175 for ( j =0; j< size ( Liste ) −1; j++){<br />
176 if ( ac<strong>tu</strong>al != NULL && ac<strong>tu</strong>al −>next != NULL) {<br />
177 if (( ac<strong>tu</strong>al −>key . points [ 1 ] > ac<strong>tu</strong>al −>next−>key . points [ 1 ] ) | | ( ac<strong>tu</strong>al −>key . points [ 1 ] ==<br />
ac<strong>tu</strong>al −>next−>key . points [ 1 ] && ac<strong>tu</strong>al −>key . points [ 0 ] > ac<strong>tu</strong>al −>next−>key . points [ 0 ] ) ) {<br />
178 if ( j==0){<br />
179 switcher = ac<strong>tu</strong>al −>next ;<br />
180 ac<strong>tu</strong>al −>next = switcher −>next ;<br />
181 switcher −>next = ac<strong>tu</strong>al ;<br />
182 Liste = switcher ;<br />
183 }<br />
184 else {<br />
185 switcher = ac<strong>tu</strong>al −>next ;<br />
186 ac<strong>tu</strong>al −>next = switcher −>next ;<br />
187 switcher −>next = ac<strong>tu</strong>al ;<br />
188 previous −>next = switcher ;<br />
189 help=get ( Liste , ac<strong>tu</strong>al −>key ) ;<br />
190 previous = previous −>next ;<br />
191 }<br />
192 }<br />
193 else {<br />
194 if ( j != 0) previous = previous −>next ;<br />
195 ac<strong>tu</strong>al = ac<strong>tu</strong>al −>next ;<br />
196 }<br />
197 }<br />
198 }<br />
199 }<br />
200 re<strong>tu</strong>rn Liste ;<br />
201 }<br />
202<br />
203 /∗Die Funktion d e f i n e s t a r t s d e f i n i e r t fuer je<strong>de</strong>n Knoten das Element <strong>de</strong>r Linked−List , welches als<br />
204 erstes eine ausgehen<strong>de</strong> Kante <strong>de</strong>s Knotens repraesentiert . ∗/<br />
205 void <strong>de</strong>fine starts ( struct list element ∗ Liste ) {<br />
206 int i ;<br />
207 struct list element ∗pos ;<br />
208<br />
209 pos= Liste ;<br />
210 for ( i =0; ikey . points [ 0 ] == i +1){<br />
212 if ( pos−>key . points [ 0 ] == i+1 && Cells [ i ] . f i r s t == NULL) {<br />
213 Cells [ i ] . f i r s t = pos ;<br />
214 }<br />
215 pos = pos−>next ;<br />
216 }<br />
217 }<br />
218 }<br />
219<br />
220 /∗Die Funktion c o p y l i s t l i e f e r t eine Copie <strong>de</strong>r uebergebenen Liste zurueck . ∗/<br />
221 struct list element ∗ copy list ( struct list element ∗ Liste ) {<br />
222 struct list element ∗ac<strong>tu</strong>al , ∗neustart ;<br />
223 neustart = NULL;<br />
224 ac<strong>tu</strong>al = Liste ;<br />
225 while( ac<strong>tu</strong>al != NULL) {<br />
226 neustart=set ( neustart , ac<strong>tu</strong>al −>key , ac<strong>tu</strong>al −>value ) ;<br />
227 ac<strong>tu</strong>al = ac<strong>tu</strong>al −>next ;<br />
121
228 }<br />
229 re<strong>tu</strong>rn neustart ;<br />
230 }<br />
231<br />
232 /∗Die Funktion <strong>de</strong>fine successor bestimmt fuer je<strong>de</strong> Kante ihre Successor −Kante . ∗/<br />
233 struct list element ∗ <strong>de</strong>fine successor ( struct list element ∗ Liste ) {<br />
234 struct list element ∗ac<strong>tu</strong>al , ∗ start ;<br />
235 struct EDGE kante ;<br />
236 int i ;<br />
237<br />
238 start = copy list ( Liste ) ;<br />
239 ac<strong>tu</strong>al = start ;<br />
240 i =0;<br />
241 while( ac<strong>tu</strong>al != NULL) {<br />
242 i++;<br />
243 if ( ac<strong>tu</strong>al −>key . points [ 0 ] == Cells [ i −1]. f i r s t −>key . points [ 0 ] && ac<strong>tu</strong>al −>key . points [ 1 ] == Cells [<br />
i −1]. f i r s t −>key . points [ 1 ] ) {<br />
244 ac<strong>tu</strong>al = ac<strong>tu</strong>al −>next ;<br />
245 }<br />
246 while( ac<strong>tu</strong>al != NULL && ac<strong>tu</strong>al −>key . points [ 0 ] == i ) {<br />
247 if ( ac<strong>tu</strong>al −>next != NULL && ac<strong>tu</strong>al −>next−>key . points [ 0 ] ==i ) {<br />
248 kante . points [0]= ac<strong>tu</strong>al −>key . points [ 1 ] ;<br />
249 kante . points [1]= i ;<br />
250 start = set ( start , kante , ac<strong>tu</strong>al −>next−>key ) ;<br />
251 kante . points [ 0 ] = ac<strong>tu</strong>al −>next−>key . points [ 1 ] ;<br />
252 kante . points [ 1 ] = i ;<br />
253 start = set ( start , kante , ac<strong>tu</strong>al −>key ) ;<br />
254 ac<strong>tu</strong>al = ac<strong>tu</strong>al −>next−>next ;<br />
255 }<br />
256 else {<br />
257 kante . points [ 0 ] = ac<strong>tu</strong>al −>key . points [ 1 ] ;<br />
258 kante . points [ 1 ] = i ;<br />
259 start = set ( start , kante , Cells [ i −1]. f i r s t −>key ) ;<br />
260 kante . points [ 0 ] = Cells [ i −1]. f i r s t −>key . points [ 1 ] ;<br />
261 kante . points [ 1 ] = i ;<br />
262 start = set ( start , kante , ac<strong>tu</strong>al −>key ) ;<br />
263 ac<strong>tu</strong>al = ac<strong>tu</strong>al −>next ;<br />
264 }<br />
265 }<br />
266 }<br />
267 re<strong>tu</strong>rn start ;<br />
268 }<br />
269<br />
270 struct list element ∗ select elements ( struct list element ∗ Liste ) {<br />
271 struct list element ∗ ac<strong>tu</strong>al , ∗ start ;<br />
272 struct EDGE kante , vergleich ;<br />
273 ac<strong>tu</strong>al = Liste ;<br />
274 start = NULL;<br />
275 while( ac<strong>tu</strong>al != NULL) {<br />
276 kante . points [ 0 ] = ac<strong>tu</strong>al −>key . points [ 1 ] ;<br />
277 kante . points [ 1 ] = ac<strong>tu</strong>al −>key . points [ 0 ] ;<br />
278 vergleich = get ( Liste , kante ) ;<br />
279 kante= ac<strong>tu</strong>al −>value ;<br />
280 if ( kante . points [ 0 ] < vergleich . points [ 0 ] | | ( kante . points [ 0 ] == vergleich . points [ 0 ] && kante .<br />
points [ 1 ] < vergleich . points [ 1 ] ) ) {<br />
281 start = set ( start , ac<strong>tu</strong>al −>key , ac<strong>tu</strong>al −>key ) ;<br />
282 }<br />
283<br />
284 ac<strong>tu</strong>al = ac<strong>tu</strong>al −>next ;<br />
285 }<br />
286 d e s t r o y l i s t ( Liste ) ;<br />
287 re<strong>tu</strong>rn start ;<br />
288 }<br />
289<br />
290 struct list element ∗ rearange ( struct list element ∗Liste , struct list element ∗Successor ) {<br />
291 struct list element ∗ac<strong>tu</strong>alEdge , ∗ac<strong>tu</strong>alSucc , ∗ start ;<br />
292 struct EDGE succ ;<br />
293 int i ;<br />
294 ac<strong>tu</strong>alEdge = Liste ;<br />
295 ac<strong>tu</strong>alSucc = Successor ;<br />
296 start = NULL;<br />
297 for ( i =0; i < size ( Liste ) ; i++){<br />
298 start = set ( start , ac<strong>tu</strong>alEdge−>key , ac<strong>tu</strong>alSucc −>key ) ;<br />
299 succ = ac<strong>tu</strong>alSucc −>key ;<br />
300 ac<strong>tu</strong>alEdge = Liste ;<br />
301 ac<strong>tu</strong>alSucc = Successor ;<br />
302 while (( ac<strong>tu</strong>alEdge−>key . points [ 0 ] != succ . points [ 0 ] | | ac<strong>tu</strong>alEdge−>key . points [ 1 ] != succ . points<br />
[ 1 ] ) && ac<strong>tu</strong>alEdge != NULL) {<br />
303 ac<strong>tu</strong>alEdge = ac<strong>tu</strong>alEdge−>next ;<br />
304 ac<strong>tu</strong>alSucc = ac<strong>tu</strong>alSucc −>next ;<br />
305 }<br />
306 }<br />
307 d e s t r o y l i s t ( Liste ) ;<br />
308 re<strong>tu</strong>rn start ;<br />
309 }<br />
310<br />
311 /∗Damit <strong>de</strong>r Speicher auch nach mehrmaligem Ausfuehren <strong>de</strong>s Programms nicht zu v o l l ist , wird <strong>de</strong>r<br />
312 r e s e r v i e r t e Speicherbereich hier wie<strong>de</strong>r freigegeben . ∗/<br />
313<br />
314 void d e s t r o y l i s t ( struct list element ∗ Liste ) {<br />
122
315 struct list element ∗help ;<br />
316 while( Liste != NULL && Liste −>next != NULL) {<br />
317 help = Liste −>next ;<br />
318 free ( Liste ) ;<br />
319 Liste = help ;<br />
320 }<br />
321 free ( Liste ) ;<br />
322 }<br />
323<br />
324 struct list element ∗ Euler partition ( struct list element ∗ Liste ) {<br />
325 struct list element ∗ EDGES ,∗SUCCESSORS, ∗help ;<br />
326 int i ;<br />
327 EDGES = Liste ;<br />
328 EDGES = s o r t l i s t (EDGES) ;<br />
329 help = EDGES;<br />
330 <strong>de</strong>fine starts (EDGES) ;<br />
331 SUCCESSORS = NULL;<br />
332 SUCCESSORS = <strong>de</strong>fine successor (EDGES) ;<br />
333 for ( i =0; i < LOGN; i++){<br />
334 EDGES = doubling edge (EDGES,SUCCESSORS) ;<br />
335 SUCCESSORS = doubling succ (SUCCESSORS) ;<br />
336 }<br />
337 EDGES = select elements (EDGES) ;<br />
338 d e s t r o y l i s t (SUCCESSORS) ;<br />
339 SUCCESSORS = copy list (EDGES) ;<br />
340 EDGES = strange sort (EDGES) ;<br />
341 SUCCESSORS = s o r t l i s t (SUCCESSORS) ;<br />
342 EDGES = rearange (EDGES,SUCCESSORS) ;<br />
343 help = EDGES;<br />
344 i =0;<br />
345 while( help != NULL) {<br />
346 help−>key . label=i ;<br />
347 i = ( i == 0) ;<br />
348 help = help−>next ;<br />
349 }<br />
350 d e s t r o y l i s t (SUCCESSORS) ;<br />
351 re<strong>tu</strong>rn EDGES;<br />
352 }<br />
353<br />
354 int No<strong>de</strong>Degree ( int knoten , struct list element ∗ Liste ) {<br />
355 int i ;<br />
356 struct list element ∗ac<strong>tu</strong>al ;<br />
357 i= 0;<br />
358 ac<strong>tu</strong>al = Liste ;<br />
359<br />
360 while( ac<strong>tu</strong>al != NULL) {<br />
361 if ( ac<strong>tu</strong>al −>key . points [ 0 ] == knoten | | ac<strong>tu</strong>al −>key . points [ 1 ] == knoten ) i++;<br />
362 ac<strong>tu</strong>al= ac<strong>tu</strong>al −>next ;<br />
363 }<br />
364 re<strong>tu</strong>rn i ;<br />
365 }<br />
366<br />
367 int MaxDegree( struct list element ∗ Liste ) {<br />
368 int i , <strong>de</strong>gree , temp ;<br />
369 <strong>de</strong>gree = 0;<br />
370 for ( i =0; i <strong>de</strong>gree ) <strong>de</strong>gree = temp ;<br />
373 }<br />
374 re<strong>tu</strong>rn <strong>de</strong>gree ;<br />
375 }<br />
376<br />
377 struct list element ∗ directEdges ( struct list element ∗ Liste ) {<br />
378 struct list element ∗ac<strong>tu</strong>al , ∗ start ;<br />
379 struct EDGE other ;<br />
380 ac<strong>tu</strong>al = Liste ;<br />
381 start = NULL;<br />
382 while( ac<strong>tu</strong>al != NULL) {<br />
383 start = set ( start , ac<strong>tu</strong>al −>key , ac<strong>tu</strong>al −>value ) ;<br />
384 other . points [ 0 ] = ac<strong>tu</strong>al −>key . points [ 1 ] ;<br />
385 other . points [ 1 ] = ac<strong>tu</strong>al −>key . points [ 0 ] ;<br />
386 other . label = ac<strong>tu</strong>al −>key . label ;<br />
387 start = set ( start , other , other ) ;<br />
388 ac<strong>tu</strong>al = ac<strong>tu</strong>al −>next ;<br />
389 }<br />
390 d e s t r o y l i s t ( Liste ) ;<br />
391 re<strong>tu</strong>rn start ;<br />
392 }<br />
393<br />
394 struct list element ∗ Euler colour ( struct list element ∗ Liste ) {<br />
395 struct list element ∗ac<strong>tu</strong>al , ∗one , ∗two ;<br />
396 int <strong>de</strong>gree ;<br />
397 ac<strong>tu</strong>al = Liste ;<br />
398 <strong>de</strong>gree = MaxDegree( Liste ) ;<br />
399 if ( <strong>de</strong>gree == 1) {<br />
400 while( ac<strong>tu</strong>al != NULL) {<br />
401 ac<strong>tu</strong>al −>key . label = 1;<br />
402 ac<strong>tu</strong>al = ac<strong>tu</strong>al −>next ;<br />
403 }<br />
404 re<strong>tu</strong>rn Liste ;<br />
123
405 }<br />
406 else {<br />
407 Liste = addDummy( Liste ) ;<br />
408 ac<strong>tu</strong>al = directEdges ( Liste ) ;<br />
409 ac<strong>tu</strong>al = Euler partition ( ac<strong>tu</strong>al ) ;<br />
410 ac<strong>tu</strong>al = removeDummy( ac<strong>tu</strong>al ) ;<br />
411 one = NULL;<br />
412 two = NULL;<br />
413 while( ac<strong>tu</strong>al != NULL) {<br />
414 if ( ac<strong>tu</strong>al −>key . label == 0) one = set (one , ac<strong>tu</strong>al −>key , ac<strong>tu</strong>al −>value ) ;<br />
415 else two = set (two , ac<strong>tu</strong>al −>key , ac<strong>tu</strong>al −>value ) ;<br />
416 ac<strong>tu</strong>al = ac<strong>tu</strong>al −>next ;<br />
417 }<br />
418 one = Euler colour ( one ) ;<br />
419 two = Euler colour (two) ;<br />
420 ac<strong>tu</strong>al = two ;<br />
421 <strong>de</strong>gree = <strong>de</strong>gree /2;<br />
422 while( ac<strong>tu</strong>al != NULL) {<br />
423 ac<strong>tu</strong>al −>key . label = ac<strong>tu</strong>al −>key . label + <strong>de</strong>gree ;<br />
424 one = set (one , ac<strong>tu</strong>al −>key , ac<strong>tu</strong>al −>value ) ;<br />
425 ac<strong>tu</strong>al = ac<strong>tu</strong>al −>next ;<br />
426 }<br />
427 d e s t r o y l i s t (two) ;<br />
428 re<strong>tu</strong>rn one ;<br />
429 }<br />
430 }<br />
431<br />
432 struct list element ∗ addDummy( struct list element ∗ Liste ) {<br />
433 int i ;<br />
434 struct list element ∗ intern ;<br />
435 struct EDGE kante ;<br />
436 intern=Liste ;<br />
437<br />
438 for ( i =0; ikey . points [ 0 ] != (N+1)) && ( ac<strong>tu</strong>al −>key . points [ 1 ] != (N+1)) ) {<br />
454 start = set ( start , ac<strong>tu</strong>al −>key , ac<strong>tu</strong>al −>value ) ;<br />
455 }<br />
456 ac<strong>tu</strong>al = ac<strong>tu</strong>al −>next ;<br />
457 }<br />
458 d e s t r o y l i s t ( Liste ) ;<br />
459 re<strong>tu</strong>rn start ;<br />
460 }<br />
461<br />
462 /∗In <strong>de</strong>r Funktion main e r f o l g t die I n i t i a l i s i e r u n g <strong>de</strong>s Graphen und die Koordination <strong>de</strong>s Ablaufs<br />
463 <strong>de</strong>s Algorithmus . ∗/<br />
464 void main () {<br />
465 int i ;<br />
466 struct EDGE key , value ;<br />
467 struct list element ∗help , ∗EDGES;<br />
468 EDGES = NULL;<br />
469 for ( i =0; i
495 EDGES= Euler colour (EDGES) ;<br />
496 d e s t r o y l i s t (EDGES) ;<br />
497 }<br />
C.6. Chinesischer Restsatz<br />
Listing C.6: Implementierung <strong>de</strong>s Chinesischen Restsatzes<br />
1 #inclu<strong>de</strong> <br />
2 #inclu<strong>de</strong> <br />
3 #inclu<strong>de</strong> <br />
4 #inclu<strong>de</strong> <br />
5<br />
6 struct list element {<br />
7 int x ;<br />
8 int mi ;<br />
9 int Mi;<br />
10 int m;<br />
11 int a ;<br />
12 struct list element ∗next ;<br />
13 struct list element ∗ link ;<br />
14 };<br />
15<br />
16 int euklid ( int a , int b , int ∗x , int ∗y){<br />
17 int xPrev , xCur , yPrev , yCur , xNext , yNext , r , q ;<br />
18 int sign = 1;<br />
19 int Sa , Sb ;<br />
20<br />
21 xPrev = 1;<br />
22 xCur = 0;<br />
23 yPrev = 0;<br />
24 yCur = 1;<br />
25 Sa = a ;<br />
26 Sb = b ;<br />
27 while(b != 0) {<br />
28 r = a%b ;<br />
29 q = a/b ;<br />
30 a = b ;<br />
31 b = r ;<br />
32 xNext = q∗xCur + xPrev ;<br />
33 xPrev = xCur ;<br />
34 xCur = xNext ;<br />
35 yNext = q∗yCur + yPrev ;<br />
36 yPrev = yCur ;<br />
37 yCur = yNext ;<br />
38 sign = −sign ;<br />
39 }<br />
40 ∗x = sign ∗ xPrev ;<br />
41 if (∗x < 0) ∗x= Sb + ∗x ;<br />
42 ∗y = −sign ∗ yPrev ;<br />
43 re<strong>tu</strong>rn a ;<br />
44 }<br />
45<br />
46 int chin ( int a , int Mi, int mi){<br />
47 int y , x , help ;<br />
48 help = euklid (Mi,mi,&y,&x) ;<br />
49 printf (”Ergebnis <strong>de</strong>s erweiterten Euklid : %i \n” ,y) ;<br />
50 x = Mi ∗ y ∗ a ;<br />
51 re<strong>tu</strong>rn x ;<br />
52 }<br />
53<br />
54 int size ( struct list element ∗ Liste ) {<br />
55 struct list element ∗ac<strong>tu</strong>al ;<br />
56 int i ;<br />
57 ac<strong>tu</strong>al = Liste ;<br />
58 i = 0;<br />
59 while( ac<strong>tu</strong>al != NULL) {<br />
60 i++;<br />
61 ac<strong>tu</strong>al = ac<strong>tu</strong>al −>next ;<br />
62 }<br />
63 re<strong>tu</strong>rn i ;<br />
64 }<br />
65<br />
66 void calc ( struct list element ∗ Liste ) {<br />
67 double logm ;<br />
68 int i ;<br />
69 struct list element ∗ac<strong>tu</strong>al ;<br />
70 ac<strong>tu</strong>al = Liste ;<br />
71 while( ac<strong>tu</strong>al != NULL && ac<strong>tu</strong>al −>next != NULL) {<br />
72 ac<strong>tu</strong>al −>link = ac<strong>tu</strong>al −>next ;<br />
73 (∗ ac<strong>tu</strong>al ) .m = ac<strong>tu</strong>al −>mi ;<br />
74 ac<strong>tu</strong>al = ac<strong>tu</strong>al −>next ;<br />
75 }<br />
76 ac<strong>tu</strong>al −>m = ac<strong>tu</strong>al −>mi ;<br />
77 ac<strong>tu</strong>al −>link = NULL;<br />
125
78 logm = log ((double) size ( Liste ) ) ;<br />
79 for ( i =0; ilink != NULL) {<br />
83 ac<strong>tu</strong>al −>m= ( ac<strong>tu</strong>al −>m) ∗ ( ac<strong>tu</strong>al −>link −>m) ;<br />
84 ac<strong>tu</strong>al −>link = ac<strong>tu</strong>al −>link −>link ;<br />
85 }<br />
86 ac<strong>tu</strong>al = ac<strong>tu</strong>al −>next ;<br />
87 }<br />
88 }<br />
89 ac<strong>tu</strong>al = Liste ;<br />
90 while( ac<strong>tu</strong>al != NULL) {<br />
91 ac<strong>tu</strong>al −>m = Liste −>m;<br />
92 ac<strong>tu</strong>al −>Mi = ac<strong>tu</strong>al −>m /ac<strong>tu</strong>al −>mi ;<br />
93 ac<strong>tu</strong>al = ac<strong>tu</strong>al −>next ;<br />
94 }<br />
95 ac<strong>tu</strong>al = Liste ;<br />
96 while( ac<strong>tu</strong>al != NULL) {<br />
97 ac<strong>tu</strong>al −>x = chin ( ac<strong>tu</strong>al −>a , ac<strong>tu</strong>al −>Mi, ac<strong>tu</strong>al −>mi) ;<br />
98 ac<strong>tu</strong>al = ac<strong>tu</strong>al −>next ;<br />
99 }<br />
100 ac<strong>tu</strong>al = Liste ;<br />
101 while( ac<strong>tu</strong>al != NULL && ac<strong>tu</strong>al −>next != NULL) {<br />
102 ac<strong>tu</strong>al −>link = ac<strong>tu</strong>al −>next ;<br />
103 ac<strong>tu</strong>al = ac<strong>tu</strong>al −>next ;<br />
104 }<br />
105 ac<strong>tu</strong>al −>link = NULL;<br />
106 for ( i =0; ilink != NULL) {<br />
110 ac<strong>tu</strong>al −>x= ( ac<strong>tu</strong>al −>x + ac<strong>tu</strong>al −>link −>x)%(Liste −>m) ;<br />
111 ac<strong>tu</strong>al −>link = ac<strong>tu</strong>al −>link −>link ;<br />
112 }<br />
113 ac<strong>tu</strong>al = ac<strong>tu</strong>al −>next ;<br />
114 }<br />
115 }<br />
116 printf (”Das Ergebnis <strong>de</strong>r simultanen Kongruenz i s t : %i \n” , Liste −>x) ;<br />
117 }<br />
118<br />
119 struct list element ∗ set ( struct list element ∗Liste , struct list element item ) {<br />
120 struct list element ∗help , ∗ac<strong>tu</strong>al ;<br />
121 ac<strong>tu</strong>al = Liste ;<br />
122 while( ac<strong>tu</strong>al != NULL && ac<strong>tu</strong>al −>next !=NULL ) {<br />
123 ac<strong>tu</strong>al = ac<strong>tu</strong>al −>next ;<br />
124 }<br />
125 help = malloc ( sizeof ( struct list element ) ) ;<br />
126 (∗ help ) = item ;<br />
127 help−>next = NULL;<br />
128 if ( Liste == NULL) Liste = help ;<br />
129 else ac<strong>tu</strong>al −>next = help ;<br />
130 re<strong>tu</strong>rn Liste ;<br />
131 }<br />
132<br />
133 void <strong>de</strong>stroyList ( struct list element ∗ Liste ) {<br />
134 struct list element ∗ac<strong>tu</strong>al ;<br />
135 while ( Liste != NULL) {<br />
136 ac<strong>tu</strong>al = Liste −>next ;<br />
137 free ( Liste ) ;<br />
138 Liste = ac<strong>tu</strong>al ;<br />
139 }<br />
140 }<br />
141<br />
142 void main () {<br />
143 struct list element value ;<br />
144 struct list element ∗gca , ∗ac<strong>tu</strong>al ;<br />
145 gca = NULL;<br />
146<br />
147 value . mi= 6;<br />
148 value . a = 2;<br />
149 gca = set ( gca , value ) ;<br />
150<br />
151 value . mi = 19;<br />
152 value . a = 12;<br />
153 gca = set ( gca , value ) ;<br />
154<br />
155 value . mi = 23;<br />
156 value . a = 12;<br />
157 gca = set ( gca , value ) ;<br />
158<br />
159 value . mi = 7;<br />
160 value . a = 4;<br />
161 gca = set ( gca , value ) ;<br />
162<br />
163 ac<strong>tu</strong>al = gca ;<br />
164 calc ( gca ) ;<br />
165 <strong>de</strong>stroyList ( gca ) ;<br />
166 }<br />
126
Litera<strong>tu</strong>rverzeichnis<br />
[ARS71] Alvy <strong>Ra</strong>y Smith, III: Simple Computation-Universal Cellular Spaces. J.<br />
ACM, 18(3):339–353, 1971.<br />
[BA95]<br />
[Buc01]<br />
[CHL99]<br />
[CLC82]<br />
Ben-Amram, Amir M.: What is a ”<br />
Pointer Machine“ ? In: SIGACTN: SI-<br />
GACT News (ACM Special Interest Group on Automata and Computability<br />
Theory), 1995.<br />
Buchmann, Johannes: Einführung in die Kryptographie<br />
2., erweiterte Auflage. Springer, 2001.<br />
Chong, Ka Wong, Yijie Han und Tak Wah Lam: On the parallel time complexity<br />
of undirected connectivity and minimum spanning trees. In: SODA ’99:<br />
Proceedings of the tenth annual ACM-SIAM symposium on Discrete algorithms,<br />
Seiten 225–234, Phila<strong>de</strong>lphia, PA, USA, 1999. Society for Industrial<br />
and Applied Mathematics.<br />
Chin, Francis Y., John Lam und I-Ngo Chen: Efficient parallel algorithms for<br />
some graph problems. Commun. ACM, 25(9):659–665, 1982.<br />
[CLRS01] Cormen, Thomas H., Charles E. Leiserson, Ronald L. Rivest und Clifford<br />
Stein: Introduction to Algorithms, Second Edition. The MIT Press, Cambridge,<br />
London, 2001.<br />
[CNG + 01] Calidonna, Claudia R., Claudia Di Napoli, Maurizio Giordano, Mario Mango<br />
Furnari und Salvatore Di Gregorio: A network of cellular automata for<br />
a landsli<strong>de</strong> simulation. In: ICS ’01: Proceedings of the 15th international<br />
conference on Supercomputing, Seiten 419–426, New York, NY, USA, 2001.<br />
ACM Press.<br />
[DR86]<br />
[GK96]<br />
Dymond, Patrick W. und Walter L. Ruzzo: Parallel RAMs with Owned Global<br />
Memory and Deterministic Context-Free Language Recognition (Exten<strong>de</strong>d<br />
Abstract). In: Automata, Languages and Programming, Seiten 95–104, 1986.<br />
Goodrich, Michael T. und S. <strong>Ra</strong>o Kosaraju: Sorting on a Parallel Pointer<br />
Machine with Applications to Set Expression Evaluation. In: Journal of the<br />
ACM, 1996.<br />
127
[GR98]<br />
[Gü92]<br />
[Hee01]<br />
[Hoc98]<br />
[HVH03]<br />
[JM92]<br />
[KKT01]<br />
Gibbons, Alan und Woiciech Ritter: Efficient Parallel Algorithms. Cambridge<br />
University Press, New York, Port Chester, Melbourne, Sidney, 1998.<br />
Güting, <strong>Ra</strong>lf Hartmut: Datenstruk<strong>tu</strong>ren und <strong>Algorithmen</strong>. B. G. Teubner,<br />
1992.<br />
Heenes, Wolfgang: Globaler Zellularer Automat: <strong>Algorithmen</strong> und Struk<strong>tu</strong>ren.<br />
Diplomarbeit, <strong>Technische</strong> Universität Darmstadt, 2001.<br />
Hochberger, Christian: CDL - Eine Sprache für die Zellularverarbei<strong>tu</strong>ng auf<br />
verschie<strong>de</strong>nen Zielplattformen. Doktorarbeit, <strong>Technische</strong> Universität Darmstadt,<br />
1998.<br />
Hoffmann, Rolf, Klaus-Peter Völkmann und Wolfgang Heenes: GCA: A Massively<br />
Parallel Mo<strong>de</strong>l. In: IPDPS ’03: Proceedings of the 17th International<br />
Symposium on Parallel and Distributed Processing, Seite 270.2, Washington,<br />
DC, USA, 2003. IEEE Computer Society.<br />
Johnson, Donald B. und Panagiotis Metaxas: A parallel algorithm for computing<br />
minimum spanning trees. In: SPAA ’92: Proceedings of the fourth annual<br />
ACM symposium on Parallel algorithms and architec<strong>tu</strong>res, Seiten 363–372,<br />
New York, NY, USA, 1992. ACM Press.<br />
Keller, Jörg, Christoph W. Keßler und Jesper Larsson Träff: Practical PRAM<br />
Programming. John Wiley & Sons, INC., New York, Chichester, Weinheim,<br />
Brisbane, Singapore, Toronto, 2001.<br />
[Kru05] Kruthoff, Bernd: Ausarbei<strong>tu</strong>ng Graph-<strong>Algorithmen</strong>, 2005.<br />
http://www-wi.uni-muenster.<strong>de</strong>/pi/lehre/SS03/Seminar/Graph<br />
Bernd Kruthoff.pdf.<br />
[Maj94]<br />
Majercik, Stephen Michael: Struc<strong>tu</strong>rally Dynamic Cellular Automata. Diplomarbeit,<br />
The University of Southern Maine School of Applied Science,<br />
1994.<br />
[MWIS02] Muzy, Alexandre, Gabriel Wainer, Eric Innocenti und Antoine Aiello Jean-<br />
Franšois San<strong>tu</strong>cci: Comparing Simulation Methods For Fire Spreading Across<br />
A Fuel Bed. In: Proceedings of AIS’2002, Lisbon, Por<strong>tu</strong>gal, 2002.<br />
[Nat90]<br />
[Sar00]<br />
Natvig, Lasse: Logarithmic time cost optimal parallel sorting is not yet fast<br />
in practice! In: Supercomputing ’90: Proceedings of the 1990 ACM/IEEE<br />
conference on Supercomputing, Seiten 486–494, Washington, DC, USA, 1990.<br />
IEEE Computer Society.<br />
Sarkar, Palash: A brief history of cellular automata. ACM Comput. Surv.,<br />
32(1):80–107, 2000.<br />
128
[Sch01]<br />
[Sig89]<br />
Schöning, Uwe: Theoretische Informatik - kurzgefasst. Spektrum Aka<strong>de</strong>mischer<br />
Verlag, 2001.<br />
Signorini, J.: How a SIMD machine can implement a complex cellular automata?<br />
a case s<strong>tu</strong>dy: von Neumann’s 29-state cellular automaton. In: Supercomputing<br />
’89: Proceedings of the 1989 ACM/IEEE conference on Supercomputing,<br />
Seiten 175–186, New York, NY, USA, 1989. ACM Press.<br />
[Web05] Webseite über <strong>de</strong>n Cell-Prozessor: 2005.<br />
http://www.blachford.info/computer/Cell/Cell0 v2.html.<br />
129