Algorithmen und Datenstrukturen 2

Werbung
Skriptum zur Vorlesung
Algorithmen und Datenstrukturen 2
Univ.-Prof. Dr. Petra Mutzel
Ass.-Prof. Dr. Günther Raidl
I NSTITUT
W INTERSEMESTER 2001/2002
F ÜR C OMPUTERGRAPHIK UND A LGORITHMEN
T ECHNISCHE U NIVERSIT ÄT W IEN
c A LLE R ECHTE VORBEHALTEN
2
Inhaltsverzeichnis
1 Graphenalgorithmen
1.1 Definition von Graphen . . . . . . . . . . . . . . . . . . . . . . . .
1.2 Darstellung von Graphen im Rechner . . . . . . . . . . . . . . . .
1.3 Durchlaufen von Graphen . . . . . . . . . . . . . . . . . . . . . . .
1.3.1 Tiefensuche (Depth-First-Search, DFS) . . . . . . . . . . .
1.4 Minimale aufspannende Bäume . . . . . . . . . . . . . . . . . . .
1.4.1 Der Algorithmus von Kruskal . . . . . . . . . . . . . . . .
1.4.2 Implementierung von Kruskal mittels Union-Find . . . . . .
1.4.3 Abstrakter Datentyp: Dynamische Disjunkte Menge (DDM)
1.4.4 Der Algorithmus von Prim . . . . . . . . . . . . . . . . . .
2 Optimierungsalgorithmen
2.1 Exakte Algorithmen für schwierige Optimierungsprobleme
2.1.1 Enumerationsverfahren . . . . . . . . . . . . . . .
2.1.2 Dynamische Programmierung . . . . . . . . . . .
2.1.3 Beschränkte Enumeration . . . . . . . . . . . . .
2.1.4 Branch-and-Bound . . . . . . . . . . . . . . . . .
2.2 Approximative Algorithmen und Gütegarantien . . . . . .
2.2.1 Konstruktive Heuristiken mit Gütegarantien . . . .
2.2.2
-Approximative Algorithmen . . . . . . . . . . .
2.3 Verbesserungsheuristiken . . . . . . . . . . . . . . . . . .
2.3.1 Einfache Austauschverfahren . . . . . . . . . . .
2.3.2 Simulated Annealing . . . . . . . . . . . . . . . .
2.3.3 Evolutionäre Algorithmen . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
3 Geometrische Algorithmen
3.1 Scan-Line Prinzip . . . . . . . . . . . . . . . . . . . . . . . .
3.1.1 Das Schnittproblem für Iso-orientierte Liniensegmente
3.1.2 Schnitt von Allgemeinen Liniensegmenten . . . . . .
3.2 Mehrdimensionale Bereichssuche . . . . . . . . . . . . . . .
3.2.1 Zweidimensionale Bäume . . . . . . . . . . . . . . .
3.2.2 Höhere Dimensionen . . . . . . . . . . . . . . . . . .
3
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
11
12
13
15
15
20
20
21
23
26
.
.
.
.
.
.
.
.
.
.
.
.
29
30
30
32
37
41
48
48
55
56
57
58
61
.
.
.
.
.
.
67
67
68
71
74
75
79
INHALTSVERZEICHNIS
4
4
Suchen in Texten
4.1 Naives Verfahren . . . . . . . . .
4.2 Verfahren von Knuth-Morris-Pratt
4.3 Verfahren von Boyer-Moore . . .
4.3.1 Berechnung von last[] . .
4.3.2 Berechnung von suffix[] .
4.3.3 Analyse von Boyer-Moore
4.4 Tries . . . . . . . . . . . . . . . .
4.4.1 Radix Trie . . . . . . . .
4.4.2 Indexed Trie . . . . . . .
4.4.3 Linked Trie . . . . . . . .
4.4.4 Suffix Compression . . . .
4.4.5 Packed Trie . . . . . . . .
4.4.6 Suchen . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
81
82
83
85
86
87
88
89
89
92
95
95
96
98
5
Randomisierte Algorithmen
5.1 Randomisierter Primzahltest . . . . . . . . . . . . . . . . . . . . . . . . .
5.1.1 Algorithmus von Miller-Rabin . . . . . . . . . . . . . . . . . . . .
101
102
102
6
Parallele Algorithmen
6.1 Ein Modell einer parallelen Maschine . . . . . . .
6.2 Parallele Minimum-Berechnung . . . . . . . . . .
6.3 Parallele Präfixsummen-Berechnung . . . . . . . .
6.4 Anwendungen der Präfixsummen-Berechnung . . .
6.4.1 Komprimieren eines dünn besetzten Arrays
6.4.2 Simulation eines endlichen Automaten . .
6.4.3 Addier-Schaltung . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
107
. 108
. 108
. 112
. 115
. 115
. 115
. 116
INHALTSVERZEICHNIS
5
Vorwort
Die Vorlesung Algorithmen und Datenstrukturen 2 ist die Fortsetzung der VO Algorithmen
und Datenstrukturen 1 und wendet sich an interessierte Studierende der Fachrichtungen Informatik, Wirtschaftsinformatik und Mathematik. Sie ist Pflichtvorlesung der Fachrichtung
Informatik.
Während im vorangegangenen Semester in der Vorlesung Algorithmen und Datenstrukturen 1 die Grundlagen im Design und in der Analyse von Algorithmen sowie grundlegende
Datenstrukturen (balancierte Suchbäume, Heaps, . . . ) mit Schwerpunkt Sortier- und Suchverfahren behandelt wurden, untersuchen wir nun Algorithmen und Datenstrukturen in
vielfältigen Anwendungsbereichen.
Im Kapitel über Graphenalgorithmen werden wir sehen, wie die Laufzeit des Greedy-Algorithmus zur Berechnung minimaler aufspannender Bäume mit Hilfe der Union-FindDatenstruktur deutlich verbessert werden kann. Dabei sind sowohl der Greedy-Algorithmus
als auch die Union-Find-Datenstruktur allgemeine, in vielfältigen Anwendungen und
Szenarien einsetzbare Konstrukte im Algorithmendesign.
Optimierungsprobleme treten in vielen Bereichen in Industrie und Wirtschaft auf und
ihre erfolgreiche Behandlung wird zunehmend wichtiger. So lassen sich zum Beispiel das
Stückgut-Transportproblem oder die Auswahl von Werbekampagnen als Rucksackproblem
formulieren (s. Kapitel 2). Ein Verschnittproblem bei Bilderrahmen oder Fensterbau
lässt sich als Bin-Packing-Problem formulieren. Das TSP-Problem taucht in vielfältigen
Fragestellungen im Bereich der Routenplanung (Lieferwagen, Müllabfuhr, Spedition,
Außendienstmitarbeiter) und im Bereich der Produktion (Steuerung von NC-Maschinen,
wie Bohren, Löten, Schweißen, Verdrahtung von Leiterplatten) auf.
Ein großer Teil der Vorlesung widmet sich den grundlegenden Konzepten zu Optimierungsalgorithmen. Wichtige Konzepte hierbei sind Dynamische Programmierung und
Branch-and-Bound-Algorithmen, die oft zur exakten Lösung NP-schwieriger Optimierungsaufgaben eingesetzt werden. Eine Alternative zu exakten Optimierungsverfahren sind
approximative Algorithmen. Hierbei unterscheidet man zwischen Konstruktionsheuristiken
und Verbesserungsheuristiken. Seit wenigen Jahren spielt der sogenannte Approximationsfaktor bzw. das Gütemaß einer Heuristik eine zunehmende Rolle in der Algorithmentheorie.
Kapitel 3 befasst sich mit Algorithmen ganz anderer Art, nämlich den geometrischen
Algorithmen. Ein wichtiges Prinzip für vielfältige Fragestellungen in dem Fall, dass sich
die Daten auf geometrische Daten abbilden lassen, ist das Scan-Line-Prinzip. Eine wichtige
Aufgabenstellung ist die ein- oder mehrdimensionale Bereichssuche.
In Kapitel 4 behandeln wir zwei klassische Algorithmen zur effizienten Suche in Texten,
nämlich den Knuth-Morris-Pratt- und den Boyer-Moore-Algorithmus. Außerdem wird die
6
INHALTSVERZEICHNIS
Datenstruktur des Tries besprochen, die für die Textsuche und -analyse große Bedeutung hat.
In Kapitel 5 erhalten wir einen Einblick in die randomisierten (d.h. zufallsbasierten)
Algorithmen. Wir verstehen darunter deterministische Algorithmen, die als zusätzliche
Elementaroperation Zufallsexperimente durchführen können.
Kapitel 6 schließlich befasst sich mit Algorithmen für parallele Hardwarearchitekturen.
Natürlich können wir in der 3-stündigen Vorlesung nicht alle möglichen Anwendungsbereiche für Algorithmen abdecken. Ziel der Vorlesung ist es, exemplarisch verschiedene
Bereiche aufzuzeigen, die häufig in der Praxis auftreten und Ihnen verdeutlichen sollen,
dass für die meisten auftretenden Probleme bereits effiziente Algorithmen, Approximationsalgorithmen oder zumindest generelle Methoden in der Literatur bekannt sind. Sie sollten
also nicht jedes Mal “das Rad neu erfinden”.
Die in der Vorlesung behandelten Themen zeigen auch wie vielseitig die Algorithmentheorie
ist. Vielleicht hat Ihnen ja ein Thema, wie z.B. die Optimierungs- oder die geometrischen
Algorithmen, besonders viel Spaß gemacht, und Sie wollen sich eventuell darin vertiefen.
Dann melden Sie sich einfach bei uns und/oder Sie besuchen weiterführende Veranstaltungen zu diesem Themenbereich. Wir bieten durchgehend Proseminare, Seminare und
Praktika zu den in der Vorlesung und Übung behandelten Themenbereichen an. Außerdem
bieten wir regelmäßig Vorlesungen über Optimierung (z.B. Evolutionäre Algorithmen, das
Handlungsreisendenproblem und seine Verwandten (TSP)) und Graphenalgorithmen (z.B.
Algorithmen zum Zeichnen von Graphen) an. Wir würden uns freuen, Sie wiederzusehen,
und wünschen Ihnen viel Erfolg für Ihre Zukunft.
Wien, im Jänner 2002
Petra Mutzel, Günther Raidl und die Assistent/inn/en von 186/1.
INHALTSVERZEICHNIS
7
Organisatorisches
Organisatorisches zur Vorlesung
Die Vorlesung findet im WS 2001/2002 ab dem 3. Oktober 2001 Mittwochs von 14:45–15:52
Uhr und Donnerstags von 14:45–15:52 Uhr im Auditorium Maximum statt. Sie wird von
Frau Univ.-Prof. Dr. Petra Mutzel und Herrn Ass.-Prof. Dr. Günther Raidl vom Institut für
Computergraphik und Algorithmen gehalten. Der Haupttermin für die Vorlesungsprüfung
ist Freitag, der 1. Februar 2002 ab 16:15 Uhr.
Beachten Sie auch aktuelle Hinweise unter:
http://www.ads.tuwien.ac.at/teaching/ LVA/186648.html
Das vorliegende Skript wurde gerade vollkommen neu erstellt. Natürlich werden Sie einige
Fehler finden, die sich eingeschlichen haben. Ein Skript steht nie von heute auf morgen
korrekt da, sondern verbessert sich und wächst im Laufe der Zeit. Wir hoffen, dass Sie
Verständnis dafür aufbringen.
Das vorliegende Skript ist nicht dazu da, um ausschließlich daraus den Stoff zu lernen,
sondern um festzustellen, welche Themengebiete in der Vorlesung behandelt werden und
worauf der Schwerpunkt gelegt wird. Darauf basierend sollen Sie, die Studierenden, sich
mit dem Lehrstoff aus den Büchern weitergehend beschäftigen. Dazu empfehlen wir ganz
besonders die Bücher aus der Literaturliste (s. Abschnitt Literaturliste).
Organisatorisches zur Übung
Die Übung soll dazu dienen, die Inhalte der Vorlesung anzuwenden, zu verstehen und zu
vertiefen.
Wichtig: Zur Teilnahme an der Laborübung ist eine Anmeldung zu einer konkreten Übungsgruppe über unser Web-Formular
http://www.ads.tuwien.ac.at/teaching/Anmeldung/
erforderlich. Diese Anmeldung ist ab Donnerstag, den 4. Oktober 2001 bis spätestens
Mittwoch, den 17. Oktober 2001 möglich!
Die Übungen finden in Kleingruppen unter Anleitung Ihres/Ihrer Übungsleiters/-leiterin an
vier Tagen über das Semester verteilt statt. An den drei ersten Terminen werden Aufgaben
von Übungsblättern durchgenommen, zum vierten Termin findet ein Übungstest statt.
Jedes Übungsblatt beinhaltet 10 Beispiele. Das erste Übungsblatt erhalten Sie in einer der
ersten Vorlesungseinheiten, die weiteren dann immer in der Übungsgruppe. Außerdem
8
INHALTSVERZEICHNIS
werden diese Übungsblätter auch auf unserer Webseite zur Laborübung oder im Sekretariat
unserer Abteilung erhältlich sein.
Am Beginn einer derartigen Übungstunde tragen alle Teilnehmer in eine Liste Ihre Anwesenheit ein und vermerken jene Beispiele, die Sie Ihrer Meinung nach richtig gelöst haben.
Der/die Leiter/in der Übungsgruppe wählt nun entsprechend diesen Angaben Teilnehmer
aus, die ihre vorbereiteten Lösungen mit entsprechenden Erläuterungen an der Tafel
präsentieren. Diese Präsentationen fließen auch in die Beurteilung ein.
Den Schwerpunkt der Beurteilung bildet jedoch der Übungstest. Dieser Test dauert 60
Minuten. Hierbei sind keine Unterlagen oder andere Hilfsmittel erlaubt. Die Beispiele
in diesem Test umfassen prinzipiell den bis dahin in der Vorlesung durchgenommenen
Stoff, sind aber vor allem an die davor in den Übungsstunden durchgenommenen Beispiele
angelehnt. Arbeiten Sie daher in den Übungsstunden mit — es lohnt sich spätestens beim
Test!
Sollten Sie bei diesem Test nicht teilnehmen können oder einfach ,,einen schlechten Tag“
erwischt haben, so ist dies auch kein prinzipielles Problem, da die Vorlesungsprüfung am 1.
Februar 2002 auf Wunsch auch gleichzeitig als Ersatzübungstest gewertet werden kann.
Weiters erhalten Sie Mitte November in der Übungsgruppe Angaben zu einer Programmieraufgabe. Diese ist eigenständig zu lösen. Das fertige Programm stellen Sie Ihrem/r
Übungsgruppenleiter/in bei einem Abgabegespräch in der zweiten Jännerwoche vor. Dabei
müssen Sie die Funktionsweise auch im Detail erklären können. Nur wenn Sie bei der Abgabe davon überzeugen können, dass Sie der/die Autor/in des Programmes sind, wird dieses
positiv gewertet. Zum Lösen der Programmieraufgaben stehen Ihnen im Informatiklabor,
Favoritenstraße 9-11, EG, zahlreiche PCs mit Linux bzw. Microsoft Windows 2000 und
verschiedenen Programmiersprachen zur Verfügung. Den notwendigen Account erhalten
Sie mit der Ausgabe der Aufgabenstellungen bei Ihrem/Ihrer Übungsgruppenleiter/in.
Für eine positive Note im Übungszeugnis der Laborübung muss die Hälfte der Übungsaufgaben erfolgreich bearbeitet werden, der Übungstest bestanden werden (Hälfte der Punkte)
und die Programmieraufgabe erfolgreich abgegeben werden. Ferner gibt es für die aktive
Teilnahme in den Übungsgruppen, d.h. gute Präsentationen von Übungsbeispielen, eine
bestimmte Punkteanzahl. Details zur Bewertung werden im Übungsverlauf auf der Webseite
zur Laborübung bekannt gegeben.
Die Übung wird hauptsächlich von den Universitätsassistent/inn/en Gabriele Kodydek,
Gunnar Klau, René Weiskircher und unserem Studienassistenten Georg Kraml organisiert.
Wir möchten uns an dieser Stelle für die Mitarbeit der zahlreichen Tutorinnen und Tutoren bedanken, ohne deren Engagement die Laborübung in Kleingruppen nicht möglich wäre.
INHALTSVERZEICHNIS
9
Mit allen Fragen, Problemen, Anregungen und Beschwerden, die vom Leiter bzw. von der
Leiterin Ihrer Übungsgruppen nicht behandelt werden können, wenden Sie sich bitte an
uns. Wir haben für Sie dazu die email-Hotline [email protected]
eingerichtet. Natürlich stehen wir Ihnen auch persönlich zu Verfügung. Kommen
Sie aber bitte ausschließlich in den Sprechstundenzeiten oder nach vorheriger Vereinbarung. Die aktuellen Sprechstundenzeiten erfahren Sie auf unserer Homepage
(http://www.ads.tuwien.ac.at).
Aktuelle Informationen zur Laborübung (Termine, Übungsblätter, etc.) finden Sie auf unserer
Webseite unter:
http://www.ads.tuwien.ac.at/teaching/LVA/186659.html
Wir wünschen Ihnen
VIEL ERFOLG!
10
INHALTSVERZEICHNIS
Literaturliste
Die Bücher (1)-(3) beinhalten für viele der hier behandelten Themenbereiche ein einführendes Kapitel. Ein interessantes multimediales Buch ist (4), das interaktive Vorlesungen zu
weiterführenden Algorithmen enthält. (5) ist ein sehr gutes und interessantes Buch zum Themenbereich der Optimierung. Interessieren Sie sich eher für Heuristiken und Lokale Suchverfahren, so empfehlen wir Ihnen, auch kurz in (6) zu blicken. (7) ist ein immer noch aktueller Klassiker im Algorithmen-Bereich. Daneben finden Sie am Ende jedes Kapitels weitere
Literaturhinweise, die sich speziell auf die dort behandelten Kapitel beziehen. Darüber hinaus steht es Ihnen frei, jedes andere Buch, das den Stoff behandelt, auszuwählen.
(1) T.H. Cormen, C.E. Leiserson und R.L. Rivest: “Introduction to Algorithms”, MIT
Press, Cambridge, 1990
(2) R. Sedgewick: “Algorithmen in C++”, Addison-Wesley, 1992
(3) T. Ottmann und P. Widmayer: “Algorithmen und Datenstrukturen”, B.I. Wissenschaftsverlag, Mannheim, 1990
(4) T. Ottman (Hrsg.): “Prinzipien des Algorithmenentwurfs”, Spektrum Akademischer
Verlag, Berlin 1998
(5) C.H. Papadimitriou und K. Steiglitz: “Combinatorial Optimization: Algorithms and
Complexity”, Dover Publications, 1998
(6) E. Aarts und J.K. Lenstra: “Local Search in Combinatorial Optimization”, John Wiley
& Sons, Chichester, 1997
(7) A.V. Aho, J.E. Hopcroft und J.D. Ullman: “Data Structures and Algorithms”, AddisonWesley, 1982
Dankesworte
An dieser Stelle sei allen gedankt, die zum Entstehen dieses Skriptums beigetragen haben:
Verantwortlich für den Inhalt sind Prof. Dr. Petra Mutzel und Dr. Günther Raidl. Dipl.Inf. René Weiskircher, Dr. Gunnar Klau, Dr. Gabriele Kodydek, Dr. Martin Schönhacker,
Mag. Ivana Ljubić, und Herr Georg Kraml haben zur Verbesserung des Skriptums entscheidend beigetragen. Wir danken Martin Gruber für seine unermüdliche Soft- und HardwareUnterstützung, vor allem aber auch Barbara Hufnagel für die Umsetzung zahlreicher handschriftlicher Notizen in LaTeX.
Kapitel 1
Graphenalgorithmen
Viele Probleme lassen sich mit Hilfe graphentheoretischer Konzepte modellieren. Graphen
bestehen aus Objekten und deren Beziehungen zueinander. Sie modellieren also diskrete
Strukturen, wie z.B. Netzwerke oder Prozesse. Betrachten wir als Beispiel das Bahnnetz
von Österreich. Die Objekte (sogenannte Knoten) können hier die Bahnhöfe (und eventuell
Weichen) sein, die Beziehungen (sogenannte Kanten) die Schienen des Bahnnetzes.
Belegt man die Kanten mit zusätzlichen Attributen (bzw. Gewichten), so kann man damit
verschiedene Probleme formulieren. Bedeutet das Attribut einer Kante zwischen und die
Fahrzeit eines Zuges von nach , so besteht das Problem der kürzesten Wege darin, die
minimale Fahrzeit von einer beliebigen Stadt zu einer beliebigen Stadt zu ermitteln.
Andere Beispiele, die mit Hilfe von graphentheoretischen Konzepten gelöst werden können,
sind Routenplanungsprobleme. Hier sollen z.B. Güter von mehreren Produzenten zu den
Verbrauchern transportiert werden.
Im Bereich Produktionsplanung geht es u.a. darum, die auftretenden Teilaufgaben auf
bestehende Maschinen zuzuordnen, so dass die Herstellungszeit minimiert wird. Da hierbei
die Reihenfolge der Abarbeitung der Teilaufgaben zentral ist, werden diese Probleme auch
Scheduling-Probleme genannt.
Ein klassisches Graphenproblem ist das Königsberger Brückenproblem“, das Euler 1736
”
gelöst und damit die Theorie der Durchlaufbarkeit von Graphen gegründet hat. Die topologische Graphentheorie hat sehr von den Forschungen am Vierfarbenproblem“ profitiert: Die
”
ursprüngliche Aufgabe ist, eine Landkarte so zu färben, dass jeweils benachbarte Länder
unterschiedliche Farben bekommen. Dabei sollen möglichst wenige Farben verwendet
werden. Während ein Beweis, dass dies immer mit maximal fünf Farben möglich ist, relativ
schnell gefunden wurde, existiert bis heute nur ein sogenannter Computerbeweis (erstmals
1976 von Appel und Haken) dafür, dass auch vier Farben ausreichen.
Im Bereich der chemischen Isomere ist es heutzutage von großer Bedeutung für die Industrie (im Rahmen der Entwicklung neuer künstlicher Stoffe und Materialien), folgende be11
KAPITEL 1. GRAPHENALGORITHMEN
12
v2
e1
v1
v1
e2
e3
e2
e3
e4
v3
v2
e1
v3
(a)
e4
(b)
Abbildung 1.1: Beispiele eines (a) gerichteten und (b) ungerichteten Graphen
reits 1890 von Caley gestellte Frage zu beantworten: Wie viele verschiedene Isomere einer
bestimmten Zusammensetzung existieren? Caley hat damit die Abzähltheorie von Graphen
begründet. Oftmals werden auch Beziehungsstrukturen bzw. Netzwerke mit Hilfe von Graphen dargestellt. Bei der Umorganisation von Betrieben kann man damit z.B. relativ leicht
feststellen, wer die wichtigsten“ bzw. einflussreichsten“ Mitarbeiter sind. Zunehmend wer”
”
den Betriebsabläufe und Geschäftsprozesse als Graphen modelliert (z.B. UML-Darstellung).
1.1 Definition von Graphen
Ein Graph ist ein Tupel , wobei eine endliche Menge ist, deren Elemente Knoten
(engl. vertices, nodes) genannt werden. Die Menge besteht aus Paaren von Knoten.
Sind diese geordnet , spricht man von gerichteten Graphen, sind die Paare
ungeordnet, spricht man von ungerichteten Graphen. Die Elemente in heißen gerichtete
bzw. ungerichtete Kanten (engl. edges), gerichtete Kanten nennt man auch B ögen (engl.
arcs). Eine Kante heißt Schleife (engl. loop). Abbildung 1.1(a) und 1.1(b) zeigen je
ein Beispiel eines gerichteten und ungerichteten Graphen.
Ist eine Kante in , dann sagen wir:
und sind adjazent
(bzw. ) und sind inzident
und sind Nachbarn
Die eingehende Nachbarmenge eines Knotens ist definiert als
"!# $ %&('
und die ausgehende Nachbarmenge als
)
Graphen gilt:
0
102
*)
+,-"!# ./.' . Für ungerichtete
. Der (Ausgangs- bzw. Eingangs-)Knotengrad
1.2. DARSTELLUNG VON GRAPHEN IM RECHNER
13
von
ist definiert als 1 und bezeichnet die Anzahl seiner (ausgehenden bzw.
eingehenden) inzidenten Kanten. Dabei wird eine Schleife an Knoten als 2 gezählt.
Für ungerichtete Graphen gilt folgendes Lemma.
Lemma: In einem ungerichteten Graphen ungeradem Grad gerade.
ist die Anzahl der Knoten mit
Beweis: Summiert man über alle Knotengrade, so zählt man jede Kante genau zwei Mal:
! " !
Da die rechte Seite gerade ist, folgt daraus, dass auch die linke Seite gerade sein muss. Also
ist die Anzahl der ungeraden Summanden auch gerade.
1.2 Darstellung von Graphen im Rechner
Wir stellen im folgenden zwei verschiedene Möglichkeiten vor, Graphen im Rechner zu
speichern. Sie unterscheiden sich in Hinblick auf den benötigten Speicherplatz und die
benötigte Laufzeit für die bei manchen Algorithmen auftretenden, immer wiederkehrenden
Abfragen.
Wir nehmen in der Folge an, dass die Knotenmenge ! "#"#" %$ '
und &
!
!.
(A) Speicherung in einer Adjazenzmatrix
Eine Möglichkeit ist, einen Graphen ohne Mehrfachkanten als Adjazenzmatrix zu speichern.
Das sieht dann folgendermaßen aus:
-.
'
'*)
falls $ %&
,+ sei eine &(& -Matrix mit Einträgen
/
sonst
Für die Beispiele aus Abb. 1.1(a) und 1.1(b) ergeben sich die folgenden Matrizen:
,
,
20
21
0
0
1
%0
1
0
0
%1
,
%0
,
20
0
1
1
21
0
1
1
1
0
1
%1
1
1
1
Analyse typischer Abfragen:
Abfrage: Existiert die Kante ?
Iteration über alle Nachbarn eines Knotens
Platzverbrauch zum Speichern
.
3( 3(4& 3.5&
0
selbst wenn der Graph dünn ist
0
(d.h. viel weniger als & Kanten enthält)
KAPITEL 1. GRAPHENALGORITHMEN
14
(B) Speicherung mit Hilfe von Adjazenzlisten
Eine Alternative (die am meisten verwendete) ist die Speicherung mittels Adjazenzlisten.
Für jeden Knoten , existiert eine Liste der ausgehenden Kanten. Man kann dies
folgendermaßen als einfach verkettete Listenstruktur umsetzen.
Für die Beispiele aus Abb. 1.1(a) und 1.1(b) ergeben sich hier folgende Listen:
1:2
2:3
3:1,3
1:2,3
2:1,3
3:1,2,3
gerichtet
ungerichtet
Als Realisierung hierfür bieten sich z.B. einfach verkettete lineare Listen an:
1
2
.
1
2
3
.
2
3
.
2
1
3
.
3
1
3
3
1
2
3
.
(a) gerichtet
.
(b) ungerichtet
Als alternative Umsetzung bieten sich Felder (Arrays) an: Hierbei gibt es einen Eintrag
& für jeden Knoten, der angibt, an welcher Stelle in der Kantenliste (edgelist) die
Nachbarliste von beginnt. Für das Beispiel aus Abb.1(b) ungerichtet ergibt sich:
)
index +
1 3 5
)
edgelist +
2 3! 1 3! 1 2 3
Analyse typischer Abfragen:
Abfrage: Existiert die Kante ?
Iteration über alle Nachbarn eines Knotens
Platzverbrauch
3(
1 3(
1 3. ! !
!
!
Für die Effizienz von Graphenalgorithmen ist es wichtig, jeweils die richtige Datenstruktur zur Speicherung von Graphen zu wählen. Ist der Graph dünn“, d.h. enthält er relativ
”
wenige Kanten, also 3( ! ! -viele Kanten, so sind Adjazenzlisten meist besser geeignet. Ist
0
der Graph sehr dicht“, d.h. enthält er 3( ! ! -viele Kanten und sind viele Anfragen der
”
Art Existiert die Kante $ ?“ zu erwarten, dann ist die Darstellung als Adjazenzmatrix
”
vorzuziehen.
1.3. DURCHLAUFEN VON GRAPHEN
15
1.3 Durchlaufen von Graphen
Die wichtigsten Verfahren zum Durchlaufen von Graphen sind Tiefensuche und Breitensuche. Wir konzentrieren uns auf die Tiefensuche.
1.3.1 Tiefensuche (Depth-First-Search, DFS)
Tiefensuche ist für folgende Graphenprobleme gut geeignet:
(1) Finde alle Knoten, die von aus erreichbar sind.
Gegeben sei ein Knoten . Bestimme alle Knoten , für die es einen Pfad von nach
gibt. Dabei ist ein Pfad der Länge eine Folge 2 #"#"" ) ) mit
& .
Ein Weg ist ein Pfad, bei dem alle Knoten und Kanten verschieden sind.
Ein Pfad ist ein Kreis, wenn .
(2) Bestimme die Zusammenhangskomponenten von .
Ein ungerichteter Graph heißt zusammenhängend, wenn für alle $ * gilt: Es
existiert ein Pfad von nach .
Ein gerichteter Graph heißt stark zusammenhängend, wenn für alle $ gilt:
Es existiert ein (gerichteter) Pfad von nach .
Daneben gibt es für gerichtete Graphen auch den Begriff des schwachen Zusammenhangs: ein solcher Graph heißt schwach zusammenhängend (oder auch nur zusammenhängend), wenn der zugrundeliegende ungerichtete Graph zusammenhängend ist.
Eine Zusammenhangskomponente ist ein maximal zusammenhängender Untergraph
Graphen, sodass und , so
von . Sind und heißt Untergraph von .
(3) Enthält einen Kreis?
(4) Ist 2-zusammenhängend?
Ein zusammenhängender Graph ist 2-zusammenhängend, wenn ' (das ist
nach Löschen des Knotens
und seiner inzidenten Kanten) für alle zusammenhängend ist. (2-Zusammenhang ist z.B. bei Telefonnetzen wichtig; Stichwort:
Survivability“.)
”
Die Idee des DFS-Algorithmus ist die folgende: Verlasse jeden Knoten so schnell wie
möglich, um tiefer“ in den Graphen zu laufen.
”
1. sei der letzte besuchte Knoten: markiere
2. Sei
eine Kante und der Knoten ist noch unmarkiert, dann: markiere , setze
und gehe zu 1.
2 /
3. Sonst: wenn kein unmarkierter Nachbar von
gänger von .
mehr existiert, gehe zurück zum Vor-
KAPITEL 1. GRAPHENALGORITHMEN
16
3
2
1
4
5
2
1
3
3
1
7
7
6
4
6
5
4
6
7
5
2
(a)
(b)
Abbildung 1.2: Ein Graph und ein möglicher DFS-Baum
Wir betrachten als Beispiel den Graphen in Abb. 1.2(a). Abb. 1.2(b) zeigt eine mögliche
Abarbeitungsreihenfolge des DFS-Algorithmus. Die kleinen Nummern neben den Knoten
geben die Reihenfolge an, in der diese markiert wurden. Die gerichteten Kanten geben für
jeden besuchten Knoten dessen eindeutigen Vorgänger und seine direkten Nachfolger an.
Der Graph, der durch diese gerichteten Kanten definiert wird, ist der zu dem DFS-Lauf
gehörige DFS-Baum.
Im folgenden präsentieren wir mögliche Lösungen zu den oben genannten Problemen (1)–
(3). Wir nehmen dabei an, dass der gegebene Graph ungerichtet ist. Wir überlassen es Ihnen,
die dazugehörigen Varianten für gerichtete Graphen zu entwickeln.
Basisalgorithmus zur Tiefensuche
Algorithmus 1/2 realisiert die Grundidee der Tiefensuche. Hier werden alle Knoten bestimmt, die von dem gegebenen Knoten durch einen Pfad erreichbar sind. Der
Knoten ist von aus erreichbar, wenn entweder oder wenn es einen Knoten gibt
mit und von aus erreichbar
ist.
Nach
Ausführung
von Depth-First-Search1 +
.
)
gilt für alle & : markiert + genau dann wenn es einen Pfad von nach gibt.
Analyse der Laufzeit:
Initialisierung: 3(
!
!
DFS wird für jeden Knoten höchstens einmal aufgerufen und hat (ohne Folgekosten
der dort gestarteten DFS-Aufrufe) Kosten
Insgesamt ergibt dies Kosten von:
!
!
Dies ist asymptotisch optimal.
!
!
! "! !
! ! "! "
1.3. DURCHLAUFEN VON GRAPHEN
17
Algorithmus 1 Depth-First-Search1 +
Input: Graph , Knoten Output: Ausgabe aller Knoten, die von erreichbar sind
)
/
1: Initialisierung: setze markiert + für alle ;
2: DFS1 + ;
3: für alle Knoten & .
)
4:
falls (markiert + ) dann 5:
Ausgabe von ;
'
6:
7:
'
Algorithmus 2 DFS1
.
)
1: markiert ,+
;
2: für alle Knoten aus
)
3:
falls (markiert + 4:
DFS1 ;
/
dann
'
5:
6:
'
Bestimmung der Zusammenhangskomponenten
Algorithmus 3/4 realisiert einen Tiefensuche-Algorithmus zur Lösung von Problem (2) für
ungerichtete Graphen. Ein Aufruf von DFS2 besucht und markiert alle Knoten, die in
der gleichen Zusammenhangskomponente wie liegen, mit der aktuellen Komponentennummer. Bleiben nach dem Lauf DFS2 noch unmarkierte Knoten übrig, erhöht sich
die Komponentennummer compnum um eins, und DFS2 wird aufgerufen; jeder in diesem Lauf entdeckte Knoten wird der neuen Komponente mit Nummer compnum zugeordnet.
Algorithmus 3 Depth-First-Search2 Input: Graph )
Output: markiert + enthält für jeden Knoten 0 die ID der ihm zugeordneten Zusammenhangskomponente, compnum enthält am Ende die Anzahl der Komponenten.
)
/
1: Initialisierung: setze markiert + für alle ;
/
2: compnum ;
3: für alle aus )
/
4:
falls (markiert + ) dann 5:
compnum ;
6:
DFS2 ;
7:
8:
'
'
KAPITEL 1. GRAPHENALGORITHMEN
18
Algorithmus 4 DFS2 1
)
1: markiert + compnum;
2: für alle Knoten aus
)
/
3:
falls (markiert + ) dann
4:
DFS2 ;
5:
6:
'
'
Analyse der Laufzeit: Mit Hilfe der gleichen Argumentation wie für Algorithmus 1 folgt
eine Laufzeit von 3( ! ! ! "! .
Kreissuche in einem Graphen
Algorithmus 5/6 zeigt einen Tiefensuche-Algorithmus zur Lösung von Problem (3) für
ungerichtete Graphen. Falls der betrachtete Nachbarknoten von in Algorithmus 6 noch
unmarkiert ist, dann wird die Kante in den DFS-Baum aufgenommen. Um am Ende
leichter den Kreis zu erhalten, wird als direkter Vorgänger von in father gespeichert.
Die weitere Durchsuchung wird bei mit dem Aufruf DFS3 fortgesetzt.
Sobald auf einen Nachbarn trifft, der bereits markiert ist (und nicht sein direkter
Vorgänger ist), wird ein Kreis gefunden. Der Graph enthält also genau dann keinen Kreis,
wenn ist. Für jeden im DFS-Lauf entdeckten Kreis wird eine Kante in der Menge
gespeichert. (Achtung: Sie können sich leicht überlegen, dass nicht alle Kreise in dadurch
gefunden werden. Machen Sie sich klar, dass dies kein Widerspruch zur obigen Aussage
ist.)
Mit Hilfe des DFS-Baumes (der hier sowohl in als auch in father() gespeichert ist) kann
man nun ganz einfach die Menge der gefundenen Kreise abrufen. Für jede Kante in
erhält man den dazugehörigen Kreis, indem man so lange den eindeutigen Vorfahren (father)
von im DFS-Baum folgt, bis man auf trifft. Dazu ist es notwendig, die Menge
als gerichtete Kantenmenge abzuspeichern, damit man Anfangsknoten und Endknoten unterscheiden kann. (Anmerkung: zu diesem Zweck wäre es eigentlich nicht nötig gewesen,
die Menge zu speichern. Wir haben Sie nur zur besseren Veranschaulichung eingeführt.)
Analyse der Laufzeit: Die gleiche Argumentation wie für Algorithmus 1 liefert die Laufzeit
3( ! ! ! "! .
1.3. DURCHLAUFEN VON GRAPHEN
19
Algorithmus 5 Depth-First-Search3 +
Input: Graph Output: Findet einen Kreis in , falls einer existiert
)
/
1: Initialisierung: setze markiert + für alle ;
2: Setze
; ;
3: für alle aus )
/
4:
falls (markiert + ) dann 5:
DFS3 ;
6:
7:
8:
9:
10:
'
'
falls ( ) dann Ausgabe Kreis gefunden“;
”
'
Algorithmus 6 DFS3
.
)
1: markiert ,+
;
2: für alle Knoten aus
. )
3:
falls ((markiert + ) und (father 1
4:
Setze ;
' sonst 5:
6:
Setze ;
7:
father ;
8:
DFS3 ;
9:
10:
'
'
)) dann
KAPITEL 1. GRAPHENALGORITHMEN
20
8
b
4
11
a
7
c
d
2
8
9
6
7
h
14
4
i
1
e
10
g
2
f
Abbildung 1.3: Die durchgezogenen Linien stellen einen MST des gezeigten Graphen dar.
1.4 Minimale aufspannende Bäume
Ein Graph wird Baum genannt, wenn er kreisfrei . (im ungerichteten Sinne) und zusammenhängend ist. Für einen Baum gilt: ! ! ! "! . Ferner gibt es in einem Baum jeweils
genau einen ungerichteten Weg zwischen je zwei Knoten. Entfernt man eine Kante aus
einem Baum, so zerfällt dieser in genau zwei Komponenten. Bei Wegnahme eines Knotens
mit Grad
und seiner inzidenten Kanten zerfällt ein Baum in
Komponenten. Man
kann sich überlegen, dass die obigen Aussagen jeweils äquivalente Definitionen eines
Baumes sind.
Das MST-Problem
Gegeben ist ein zusammenhängender und gewichteter (ungerichteter) Graph mit Kantengewichten für $ % .
Gesucht ist ein minimaler aufspannender Baum (Minimum Spanning Tree, MST) in , d.h.
ein zusammenhängender, zyklenfreier Untergraph mit
, dessen Kanten
alle Knoten aufspannen und für den so klein wie möglich ist.
Abbildung 1.3 zeigt einen gewichteten Graphen und einen minimalen aufspannenden
Baum von . Das Gesamtgewicht des MST ist (bzw. dessen Kosten sind) 37. Beachten
Sie, dass der MST nicht eindeutig sein muss. So kann z.B. in Abb. 1.3 die Kante + durch
ersetzt werden, ohne dass sich das Gewicht ändert. Eindeutigkeit des MST ist jedoch
dann gegeben, wenn die Gewichte aller Kanten unterschiedlich sind.
Das MST-Problem taucht in der Praxis z.B. als Unterproblem beim Design von Kommunikationsnetzwerken auf.
1.4.1 Der Algorithmus von Kruskal
Kruskal schlug 1956 einen Algorithmus zur exakten Lösung des MST-Problems vor. Dieser
Algorithmus fällt unter das allgemein anwendbare Greedy-Prinzip. Greedy-Algorithmen
sind gierige Verfahren zur exakten oder approximativen Lösung von Optimierungsaufgaben,
welche die Lösung iterativ aufbauen (z.B. durch schrittweise Hinzunahme von Objekten)
und sich in jedem Schritt für die jeweils lokal beste Lösung entscheiden. Eine getroffene
1.4. MINIMALE AUFSPANNENDE BÄUME
21
Entscheidung wird hierbei niemals wieder geändert. Daher sind Greedy-Verfahren bezogen
auf die Laufzeit meist effizient, jedoch führen sie nur in seltenen Fällen – wie z.B. beim
MST-Problem – zur optimalen Lösung.
Die Idee des Kruskal-Algorithmus lautet wie folgt:
Sortiere die Kanten von nach ihren Gewichten: Für
.
#"#"" : Falls
'
kreisfrei ist, dann setze
"#"#"
'
.
Der Algorithmus ist greedy, weil er in jedem Schritt die jeweils billigste“ noch hinzufügba”
re Kante aufnimmt.
Korrektheit:
kreisfrei?
zusammenhängend?
aufspannend?
minimal?
Indirekte Annahme: wäre nicht minimal.
Dann existiert ein aufspannender
Baum
mit . Außerdem existiert
eine Kante mit . Wir bauen nun schrittweise zu
um; wenn wir
dies schaffen, ohne eine teurere Kante durch eine billigere zu ersetzen, haben wir den
erwünschten Widerspruch erreicht.
' enthält einen Kreis . In existiert eine Kante mit &
und .
, denn sonst wäre vor angeschaut worden und zu
Behauptung: +
hinzugefügt worden. Denn das Hinzufügen von zu dem aktuellen Unterbaum von
hätte keinen Kreis erzeugen können, da sonst und somit einen Kreis
' ' . ist weiterhin ein aufspannender
enthalten würde. Wir setzen also 1
.
Baum und es gilt Wir bauen weiter um bis erreicht ist. 1.4.2 Implementierung von Kruskal mittels Union-Find
Möchte man den Kruskal-Algorithmus implementieren, so stellt sich die Frage, wie man
effizient das folgende Problem löst: Teste, ob ' einen Kreis enthält.
Eine naive Lösung wäre es, für jede Abfrage den DFS-Algorithmus für ' aufzurufen.
Dies würde eine Gesamtlaufzeit von
! ! ! "! ergeben, da sich in
bis zu ! ! Kanten
befinden können, was einer Laufzeit von
! ! für einen DFS-Aufruf entspricht.
KAPITEL 1. GRAPHENALGORITHMEN
22
Im folgenden werden wir sehen, wie wir für den zweiten Teil des Kruskal-Algorithmus,
d.h. alles nach dem Sortieren der Kanten, mit Hilfe der Datenstruktur Union-Find eine fast
lineare Laufzeit erhalten können und damit der Gesamtaufwand durch das Kantensortieren
bestimmt wird ( ! !! "! ).
Die Idee besteht darin, eine Menge von Bäumen, die der aktuellen Kantenmenge
entspricht, iterativ zu einem Baum zusammenwachsen zu lassen ( Wir lassen Wälder
”
wachsen“). Man beginnt mit Bäumen, die jeweils nur aus einem Knoten des gegebenen
Graphen bestehen. Dann werden nach jeder Hinzunahme einer Kante $ zur
anfangs leeren Menge die Bäume, in denen und liegen, zu einem Baum verbunden
(union). Eine Hilfsfunktion liefert hierzu jeweils die Information, in welchem Baum ein
Knoten liegt. Liegen und bereits im gleichen Baum, dann würde die Hinzunahme der
Kante ($ ) zu einem Kreis führen.
Wir betrachten das folgende Beispiel zum Graphen aus Abb. 1.3:
Komponenten (Bäume)
' ' ' ' ' ' ' +
' ' ' +
' ' ' + ' ' ' ' + ' ' ' ' ' ' ' ' '
' ' ' ' ' ' ' nächste betrachtete Kante
'
'
'
'
1
+ ' '
Hinzunahme zu
nein!
wird nicht hinzugefügt, weil und bereits im gleichen Baum liegen, d.h. verbunden
sind. Die Hinzunahme der Kante würde also einen (verbotenen) Kreis erzeugen.
Weiter geht es dann z.B. wie folgt:
Komponenten (Bäume)
' + ' +
' +
+
+
' nächste betrachtete Kante Hinzunahme zu
' '
+
' '
' '
' '
nein!
' '
+
nein!
In diesem Schritt wurde ein kompletter MST erzeugt, daher werden die restlichen Kanten
abgelehnt. Man kann also den Algorithmus bereits an dieser Stelle abbrechen, weil ohnehin
nur mehr Kreise entstehen würden, wie die probeweise Weiterführung zeigt:
Komponenten (Bäume)
+
+
+
'
'
'
nächste betrachtete Kante Hinzunahme zu
nein!
nein!
nein!
Im folgenden betrachten wir die Union-Find Datenstruktur genauer.
1.4. MINIMALE AUFSPANNENDE BÄUME
23
1.4.3 Abstrakter Datentyp: Dynamische Disjunkte Menge (DDM)
Wir betrachten den abstrakten Datentyp DDM, der die Union-Find Datenstruktur beschreibt.
Die Objekte sind eine Familie 0 "#"#" ' disjunkter Teilmengen einer endlichen
'
Menge . Jede Teilmenge hat einen Repräsentanten $
.
'
Die Operationen sind die folgenden: Seien ,
makeset : erzeugt eine. neue Menge
für alle #"#"#" .
dass '
0
'
; hierbei gilt die Annahme,
union : vereinigt die Mengen und , deren Repräsentanten und sind, zu
einer neuen Menge . Der Repräsentant von ist ein beliebiges Element
aus ; $' ' .
findset
: liefert den Repräsentanten der (eindeutigen) Menge, die
enthält.
Mit Hilfe dieses abstrakten Datentyps können wir nun den Kruskal-Algorithmus genauer
formulieren:
Algorithmus 7 Kruskal-Minimal-Spanning-Tree Input: Graph mit Gewichten für alle Output: Minimum Spanning
Tree von .
1:
; ; ;
2: Sortiere Kanten: #0 3: für alle Knoten aus 4:
makeset ;
&
5:
6:
7:
8:
9:
10:
11:
12:
13:
'
solange mehr als eine Menge enthält /* Sei die nächste Kante */
falls (findset findset ) dann * ;
union(findset ,findset );
'
i++;
'
Man beachte, dass nicht unbedingt jede Kante von betrachtet werden muss, sondern abgebrochen wird, wenn ein vollständiger Spannbaum erreicht wurde. An der Ordnung des
Aufwands ändert dies jedoch nichts, da im worst case gerade die teuerste Kante benötigt
wird.
Realisierung des Datentyps DDM:
Jede Menge wird durch einen gerichteten Baum repräsentiert. Die Knoten des Baumes
sind die Elemente der Menge. Jeder Knoten im Baum enthält einen Zeiger auf seinen Vater.
KAPITEL 1. GRAPHENALGORITHMEN
24
g
i
g
i
f
f
c
c
h
h
(a)
(b)
Abbildung 1.4: Dynamische disjunkte Mengen: (a) zwei gerichtete Bäume, die die Mengen
+ ' und ' repräsentieren; (b) nach der Vereinigung der Mengen.
Die Wurzel markieren wir dadurch, dass sie auf sich selbst zeigt. So besitzt jeder Knoten
genau eine ausgehende Kante, und zur Speicherung der Bäume genügt ein einfaches Feld
)
father + . Abbildung 1.4 zeigt die Situation unseres Beispiels vor und nach dem Hinzufügen
der Kante + .
)
father + sieht danach folgendermaßen aus:
)
father +
Die Realisierung der Operationen der Union-Find Datenstruktur DDM ist nun nicht nur
offensichtlich, sondern vor allem auch kurz und elegant, wie man an den Implementierungen
Algorithmus 8, Algorithmus 9, und Algorithmus 10 sieht.
Analyse der Laufzeit:
.
makeset:
.
union:
findset:
(Baum)) nach
Union-Operationen
Problem: Da degenerierte Bäume die Höhe 3( besitzen können, würde jeder Schritt im
dauern. Wir hätten also bezüglich worst-case Gesamtlaufzeit von
! ! ! "! mer noch
nichts gewonnen. besitzen. (Sie können sich leicht selbst eine Operationen-Folge überlegen,
bei der ein degenerierter Baum der Höhe 3. erzeugt wird.)
Verbesserung 1: Vereinigung nach Größe oder Höhe:
Idee: Um zwei Bäume mit Wurzeln und zu vereinigen, macht man die Wurzel des
Baumes mit kleinerer Größe (bzw. Höhe) zum direkten weiteren Sohn des Baumes mit
1.4. MINIMALE AUFSPANNENDE BÄUME
Algorithmus 8 makeset
)
1: father + 25
Algorithmus 9 union
)
1: father + Algorithmus 10 findset
1:
)
2: solange (father !+ )
3:
father !+ ;
4:
5:
'
)
Return ;
größerer Größe (bzw. Höhe). Der Repräsentant des größeren bzw. höheren Baumes wird
somit zum Repräsentanten der neuen Menge. Hierzu muss man sich zusätzlich zu jedem
Baum dessen Größe bzw. Höhe in der Wurzel merken.
Lemma: Dieses Verfahren liefert Bäume mit Höhe
Beweis)
Folgerung: Die findset-Operation benötigt im Worst-Case 3(
nach
Operationen. (ohne
Schritte.
Verbesserung 2: Methode der Pfadverkürzung (Kompressionsmethode):
Idee: Man erhält eine weitere Verkürzung der Pfade, und somit eine Laufzeiteinsparung der
findset-Operation, wenn man beim Durchlauf jeder findset-Operation alle durchlaufenen
Knoten direkt an die Wurzel anhängt. Bei der Ausführung von findset durchläuft man also
den gleichen Pfad zweimal: das erste Mal, um die Wurzel zu finden und das zweite Mal,
um den Vater aller Knoten auf dem durchlaufenen Pfad auf die Wurzel zu setzen. Dies
verteuert zwar die findset-Operation geringfügig, führt aber zu einer drastischen Reduktion
der Pfadlängen, denn man kann das folgende Theorem zeigen:
'
Theorem (Amortisierte Analyse): Sei
die Anzahl aller union-, makeset-, findset
'
Operationen und
die Anzahl der Elemente in allen Mengen,
: Die Ausführung
'
'
'
der
Operationen benötigt 3(
Schritte.
'
'
heißt die inverse Ackermannfunktion. Ihr Wert erhöht sich mit wachsendem
'
bzw. nur extrem langsam.
So ist
für alle praktisch auftretenden Werte von
.
'
'
/
und (solange
).
Damit ist die Ausführung einer findset-Operation durchschnittlich (gemittelt über alle
'
KAPITEL 1. GRAPHENALGORITHMEN
26
Operationen) innerhalb des Kruskal-Algorithmus in praktisch konstanter Zeit möglich. Dies
führt zu einer praktisch linearen Laufzeit des zweiten Teils des Kruskal-Algorithmus, nachdem die Kanten sortiert worden sind. Der Gesamtaufwand wird nun durch das Kantensortieren bestimmt und ist somit
! !! "! .
1.4.4 Der Algorithmus von Prim
Ein weiterer, exakter Algorithmus zur Bestimmung eines MST wurde von Prim 1957 vorgeschlagen. Dieser Ansatz arbeitet ebenfalls nach dem Greedy-Prinzip. Wir wollen diesen
Algorithmus hier nur kurz zusammenfassen. Im Gegensatz zu Kruskals Algorithmus wird
von einem Startknoten ausgegangen, und neue Kanten werden immer nur zu einem bestehenden Baum hinzugefügt:
Algorithmus 11 Prim-MST Input: Graph mit Gewichten für alle Output: Minimum Spanning Tree von 1: Wähle einen beliebigen Startknoten 2: '
3:
4: solange ! ! ! "! 5:
Ermittle eine Kante $ mit &
& ' ;
6:
' ;
7:
&
und minimalem Gewicht 8:
'
Schritt (5), das Ermitteln einer günstigsten Kante zwischen dem bestehenden Baum und
einem noch freien Knoten, kann effizient gelöst werden, indem alle noch freien Knoten
in einem Heap verwaltet werden. Die Sortierung erfolgt dabei nach dem Gewicht der
jeweils günstigsten Kante, die einen freien Knoten mit dem aktuellen Baum verbindet.
Der Heap muss nach dem Hinzufügen jeder Kante aktualisiert werden. Konkret wird für
jeden verbleibenden freien Knoten überprüft, ob es eine Kante zum gerade angehängten
Knoten gibt und diese günstiger ist als die bisher bestmögliche Verbindung zum Baum.
Unter Verwendung des beschriebenen Heaps ist der Gesamtaufwand des Algorithmus von
0
0
Prim
! ! . Damit ist dieser Ansatz für dichte Graphen ( ! "!%
3( ! ! ) etwas besser
geeignet als jener von Kruskal. Umgekehrt ist Kruskals Algorithmus für dünne Graphen
( ! "! 3( ! ! ) effizienter.
1.4. MINIMALE AUFSPANNENDE BÄUME
27
Weiterführende Literatur
Das Minimum Spanning Tree Problem wird z.B. in den Büchern von Sedgewick und Cormen, Leiserson und Rivest (siehe Literaturliste (2) und (1)) ausführlich beschrieben. In letzterem Buch finden sich auch eine weitergehende Beschreibung zu Greedy-Algorithmen, eine
genaue Definition und Behandlung der Ackermann-Funktion und ihrer Inversen, sowie Informationen zur amortisierten Analyse.
28
KAPITEL 1. GRAPHENALGORITHMEN
Kapitel 2
Optimierungsalgorithmen
Optimierungsprobleme sind Probleme, die im Allgemeinen viele zulässige Lösungen besitzen. Jeder Lösung ist ein bestimmter Wert (Zielfunktionswert, Kosten) zugeordnet. Optimierungsalgorithmen suchen in der Menge aller zulässigen Lösungen diejenigen mit dem
besten, dem optimalen, Wert. Beispiele für Optimierungsprobleme sind:
Minimal aufspannender Baum
Kürzeste Wege in Graphen
Längste Wege in Graphen
Handelsreisendenproblem
Rucksackproblem
Scheduling (z.B. Maschinen, Crew)
Zuschneideprobleme (z.B. Bilderrahmen, Anzüge)
Packungsprobleme
Wir unterscheiden zwischen Optimierungsproblemen, für die wir Algorithmen mit polynomiellem Aufwand kennen (“in P”), und solchen, für die noch kein polynomieller
Algorithmus bekannt ist. Darunter fällt auch die Klasse der NP-schwierigen Optimierungsprobleme. Die Optimierungsprobleme dieser Klasse besitzen die Eigenschaft, dass
das Finden eines polynomiellen Algorithmus für eines dieser Optimierungsprobleme auch
polynomielle Algorithmen für alle anderen Optimierungsprobleme dieser Klasse nach sich
ziehen würde. Die meisten Informatiker glauben, dass für NP-schwierige Optimierungsprobleme keine polynomiellen Algorithmen existieren.
Ein Algorithmus hat polynomielle Laufzeit, wenn die Laufzeitfunktion 5& durch ein
Polynom in & beschränkt ist. Dabei steht & für die Eingabegröße der Instanz. Z.B. ist
das Kürzeste-Wegeproblem polynomiell lösbar, während das Längste-Wegeproblem im
29
30
KAPITEL 2. OPTIMIERUNGSALGORITHMEN
Allgemeinen NP-schwierig ist. (Transformation vom Handlungsreisendenproblem: Wäre
ein polynomieller Algorithmus für das Längste–Wegeproblem bekannt, dann würde dies
direkt zu einem polynomiellen Algorithmus für das Handlungsreisendenproblem führen.)
Dies scheint auf den ersten Blick ein Widerspruch zu sein: wenn man minimieren kann,
dann sollte man nach einer Kostentransformation (“mal -1”) auch maximieren können.
Allerdings ist für das Kürzeste–Wegeproblem nur dann ein polynomieller Algorithmus
bekannt, wenn die Instanz keine Kreise mit negativen Gesamtkosten enthält. Und genau das
ist nach der Kostentransformation nicht mehr gewährleistet.
Für polynomielle Optimierungsaufgaben existieren verschiedene Strategien zur Lösung.
Die meisten davon sind speziell für das jeweilige Problem entwickelte Algorithmen.
Manchmal jedoch greifen auch allgemeine Strategien, wie z.B. das Greedy-Prinzip oder die
Dynamische Programmierung.
In diesem Abschnitt wollen wir uns auf Lösungsansätze für NP-schwierige Optimierungsprobleme konzentrieren, da diese in der Praxis weitaus häufiger auftauchen als polynomielle
Optimierungsprobleme. Zunächst werden wir typische exakte Lösungsverfahren betrachten.
Dies sind auf Enumeration beruhende Verfahren, Branch-and-Bound Verfahren und das
Verfahren der Dynamischen Programmierung. Danach konzentrieren wir uns auf die
approximativen Algorithmen (Heuristiken), die meist die optimale Lösung nur annähern.
Man unterscheidet hier zwischen Algorithmen mit und ohne Gütegarantie, sowie zwischen
Konstruktionsheuristiken und Verbesserungsheuristiken.
2.1 Exakte Algorithmen für schwierige Optimierungsprobleme
2.1.1 Enumerationsverfahren
Exakte Verfahren, die auf einer vollständigen Enumeration beruhen, eignen sich für
NP-schwierige Optimierungsprobleme diskreter Natur. Für solche kombinatorische Optimierungsprobleme ist es immer möglich, die Menge aller zulässigen Lösungen aufzuzählen
und mit der Kostenfunktion zu bewerten. Die am besten bewertete Lösung ist die optimale
Lösung. Natürlich ist die Laufzeit dieses Verfahrens nicht durch ein Polynom in der
Eingabegröße beschränkt.
Im Folgenden betrachten wir ein Enumerationsverfahren für das 0/1-Rucksackproblem.
Das 0/1-Rucksack-Problem
Gegeben: Gegenstände mit Gewicht (Größe) , und Wert (Kosten) , und ein Rucksack
der Größe .
Gesucht: Menge der in den Rucksack gepackten Gegenstände mit maximalem Gesamtwert;
2.1. EXAKTE ALGORITHMEN FÜR SCHWIERIGE OPTIMIERUNGSPROBLEME 31
dabei darf das Gesamtgewicht den Wert
nicht überschreiten.
Wir führen 0/1-Entscheidungsvariablen für die Wahl der Gegenstände ein:
-
#"#"" wobei /
falls Element nicht gewählt wird
falls Element gewählt wird
.
Das Rucksackproblem lässt sich nun formal folgendermaßen formulieren:
!
*
/
.
'
.
Beispiel: Das Rucksack-Problem mit
Gegenstand a
Gewicht
3
Wert
3
Nutzen
1
'
und folgenden Daten:
b
c
d
e
f
g
h
4
4
6
6
8
8
9
5
5
10
10
11
11
13
1,25 1,25 1,66 1,66 1,375 1,375 1,44
Der Nutzen eines Gegenstands errechnet sich aus .
Das Packen der Gegenstände a, b, c und d führt zu einem Gesamtwert von 23 bei dem
maximal zulässigen Gesamtgewicht von 17. Entscheidet man sich für die Gegenstände c, d
und e, dann ist das Gewicht sogar nur 16 bei einem Gesamtwert von 25.
Ein Enumerationsalgorithmus für das 0/1-Rucksackproblem
Eine Enumeration aller zulässigen Lösungen für das 0/1-Rucksackproblem entspricht der
Aufzählung aller Teilmengen einer -elementigen Menge (bis auf diejenigen Teilmengen,
die nicht in den Rucksack passen). Der Algorithmus Enum() (s. Algorithmus 12) basiert auf
genau dieser Idee.
Zu jedem Lösungsvektor gehört ein Zielfunktionswert (Gesamtkosten von ) und ein Gesamtgewichtswert . Die bisher beste gefundene Lösung wird in
globalen Vektor und der zugehörige Lösungswert in der globalen Variablen / / /
gespeichert. Der Algorithmus wird mit dem Aufruf & gestartet, wobei Anfang der Nullvektor ist.
dem
am
Analyse:
Korrektheit: Zeilen (6)-(9) enumerieren über alle möglichen Teilmengen einer
elementigen Menge. Zeilen (1)-(5) sorgen dafür, dass nur zulässige Lösungen betrachtet
werden und die optimale Lösung gefunden wird. KAPITEL 2. OPTIMIERUNGSALGORITHMEN
32
Algorithmus 12 Enum 1 ;
1: falls ( ) dann 2:
falls ( ) dann 3:
;
4:
"
;
5:
6:
7:
8:
9:
10:
11:
'
für
)
. + .
"
""
;
Enum ( , )
/
+ ;
)
+,
)
+,
);
'
'
Laufzeit:
.
Wegen der exponentiellen Laufzeit ist das Enumerationsverfahren i.A. nur für kleine
/
Instanzen des 0/1-Rucksackproblems geeignet. Bereits für
ist das Verfahren nicht
mehr praktikabel. Deutlich besser geeignet für die Praxis ist das Verfahren der Dynamischen
Programmierung.
2.1.2 Dynamische Programmierung
Idee: Zerlege das Problem in kleinere Teilprobleme ähnlich wie bei “Divide & Conquer”.
Allerdings: während die bei Divide & Conquer unabhängig sind, sind sie hier voneinander
abhängig.
Dazu: Löse jedes Teilproblem und speichere das Ergebnis ab, so dass zur Lösung
größerer Probleme verwendet werden kann.
Allgemeines Vorgehen:
1. Wie ist das Problem zerlegbar? Definiere den Wert einer optimalen Lösung rekursiv.
2. Bestimme den Wert der optimalen Lösung bottom-up“.
”
Wir betrachten im Folgenden wieder das 0/1-Rucksackproblem.
Dynamische Programmierung für das 0/1-Rucksackproblem
Eine Möglichkeit, das Problem in Teilprobleme zu zerlegen, ist die folgende: Wir betrachten
zunächst die Situation nach der Entscheidung über die ersten Gegenstände und merken uns
für alle möglichen Gesamtwerte diejenigen Lösungen, die jeweils das kleinste Gewicht
erhalten. Wir definieren:
2.1. EXAKTE ALGORITHMEN FÜR SCHWIERIGE OPTIMIERUNGSPROBLEME 33
.
Für jedes #"
Die Funktion ""
! .
*
/
'
'
erhalten wir somit ein Teilproblem.
kann rekursiv folgendermaßen bestimmt werden:
und )
)
#
0 '
"#"" falls
falls
sonst
/
für /
In Worten bedeutet dies: Wir können aus den bereits bekannten Funktionswerten
für kleinere folgendermaßen berechnen: Im Entscheidungsschritt wird entweder der
Gegenstand nicht in den Rucksack gepackt; in diesem Fall wird der Wert von )
einfach übernommen. Oder, Gegenstand wird in den Rucksack gepackt. Das minimale
Gewicht des neu gepackten Rucksacks mit Gegenstand und Wert erhalten wir dann als
Summe aus dem minimalen Gewicht des Rucksacks vor dem Packen von mit Wert gleich
und dem Gewicht von Gegenstand .
/
Problem: Effiziente Bestimmung aller #""#" Lösung: Entscheide iterativ über alle Gegenstände
? .
#"
""
.
In unserem Beispiel sieht das folgendermaßen aus:
1. Entscheidung über den ersten Gegenstand a:
-
/
/
. 2
2
2. Entscheidung über den zweiten Gegenstand b:
Man sieht hier, dass nicht für alle möglichen wirklich berechnet wird, sondern
-
0
/
/
0
. 2
2
0
/
/
0
0
nur für die relevanten Fälle. Man kann sich das sehr gut mit einem Entscheidungsbaum
veranschaulichen (s. Abb. 2.1). Entscheidend ist, dass bei zwei Rucksäcken mit gleichen
-Werten derjenige mit dem kleinsten Gewicht behalten wird und der andere verworfen
wird. Einträge mit sind irrelevant und werden nicht betrachtet.
KAPITEL 2. OPTIMIERUNGSALGORITHMEN
34
(0.0)
0
(0.0)
1
(3.3)
1
(5.4)
0
(0.0)
0
(0.0)
0
(5.4)
1
(5.4)
0
(3.3)
0
1
0
1
(0.0) (10.6) (5.4) (15.10)
1
(10.8)
0
(3.3)
1
(8.7)
1
(8.7)
0
(8.7)
1
(13.11)
0
1
0
1
0
1
(10.8) (20.14) (3.3) (13.9) (8.7) (18.13)
Abbildung 2.1: Teil des Entscheidungsbaumes bis Programmierung
0
1
(13.11) (23.17)
unseres Beispiels zur Dynamischen
Dynamischer Programmierungsalgorithmus für das Rucksackproblem
'
Sei Menge aller Elemente in Stufe ' , wobei die Menge der jeweils eingepackten Gegenstände ist. Der Algorithmus
Dynamische Programmierung 0/1-Knapsack
.
'
" " " .
berechnet iterativ für alle Algorithmus 13 Dynamische Programmierung 0/1-Knapsack
Input:
Gegenstände mit Gewicht und Wert , sowie Rucksack der Größe
Output: Eingepackte Gegenstände mit maximalem Wert unter allen Rucksäcken mit Gewicht kleiner gleich
'
/ /
1: Sei . ' ;
2: für #" " " '
) 3:
für jedes + '
10:
11:
* falls 0 '
'
4:
5:
6:
7:
8:
9:
'
'
'
dann
' 0 '
Untersuche auf Paare mit gleichem und behalte für jedes solche Paar das Element
mit kleinstem Gewicht
'
Optimale Lösung ist dasjenige Element
in
'
mit maximalem .
Analyse:
.
Korrektheit: Es wird jeweils in Schritt ( ) unter allen möglichen Lösungen mit
" " " Wert die “leichteste” behalten. Nach Schritten muss die optimale Lösung dabei sein. Laufzeit: Die Laufzeit hängt von der Realisierung der Schritte (3)-(6) sowie (9) ab.
Um Schritt (9) effizient durchzuführen, führen wir ein Array
'
ein; dabei ist der größte auftretende -Wert in .
)/
.
"
""
+
von Tripeln
2.1. EXAKTE ALGORITHMEN FÜR SCHWIERIGE OPTIMIERUNGSPROBLEME 35
Algorithmus 14 Beispiel für Realisierung von Schritt 9
'
1: Bestimme unter den Elementen in '
nach aufsteigenden -Werten
+ in
2: Sortiere . die Elemente
#" " " 3: für )
+ 4:
;
'
5:
für alle Tripel + in : )
6:
falls + dann 7:
Übernehme das Tripel
8:
' sonst '
9:
Entferne Tripel + aus 10:
11:
12:
'
'
'
'
Jede Menge enthält höchstens Elemente, wobei den maximalen Lösungswert
bezeichnet. Die Laufzeit der Schritte (5)-(7) hängt davon ab, wie man die Kopien der Menge
realisiert; kopiert man sie jedesmal, dann ergibt sich insgesamt eine Gesamtlaufzeit von
0
, wobei den optimalen Lösungwert bezeichnet. Gibt man sich mehr Mühe bei
der Organisation der Menge , dann kann man auch Laufzeit
erreichen.
Der folgende Algorithmus 0/1-Rucksack Dynamische Programmierung Tabelle zeigt eine alternative Möglichkeit für eine Implementierung der Idee der Dynamischen Programmierung.
Wir verwenden die folgende Datenstruktur:
)/
/
" " + , wobei hier eine obere Schranke für
Wir speichern #""#" im Feld
ist. Für die Berechnung von gibt es mehrere Möglichkeiten:
1. Wir sortieren die Gegenstände absteigend nach ihrem Nutzen. Der Nutzen eines
Gegenstands ist definiert als der relative Wert im Vergleich zum Gewicht :
Intuitiv sind Gegenstände mit einem größeren Nutzen wertvoller als welche mit geringerem Nutzen, und sind deshalb vorzuziehen. (Jedoch führt ein einfacher GreedyAlgorithmus hier nicht zur Optimallösung.) Wir packen nun zunächst den Gegenstand
mit größtem Nutzen, dann denjenigen mit zweitgrößtem Nutzen, etc. bis das Gesamtgewicht
ist.
2. Wir nehmen den Gegenstand mit dem größten Nutzen Mal, solange bis 3. Wir multiplizieren den größten Nutzen
mit
.
.
KAPITEL 2. OPTIMIERUNGSALGORITHMEN
36
Algorithmus 15
. Rucksack Dynamische Programmierung Tabelle
/
1: für #" " " )
)
)/
/
2:
+ + ;
+ 3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
'
.
für #" " " .
/
" " " für )
+ falls
)
/* Fall: + )
+ falls
)
+ '
/
)
)
dann */
)
+ dann
+
.
/* Fall: + */
)
)
falls
+ 0
+
)
'
)
++ )
)
+ 0
)
)
+ + .
)
+ 0
+
)
+ dann
'
'
für
'
)
.
/
+ )
#"
+
""
)
+ '
Korrektheit: Auch hier wird jeweils die kleinste Lösung unter allen Werten gleich nach
Schritten berechnet. Für ist also die Lösung des Rucksack-Problems korrekt.
Laufzeit:
.
Oberflächlich betrachtet sehen die Laufzeiten der beiden Dynamischen Programmierungsal
polynomiell aus. Allerdings kann (bzw. bzw.
gorithmen von
im allgemeinen sehr groß sein. Nur in dem Fall dass durch ein Polynom in beschränkt
ist, besitzt der Algorithmus polynomielle Laufzeit. Da die Gesamtlaufzeit nicht nur von
der Eingabegröße , sondern auch von den Werten , und
abhängt, nennt man die
Laufzeit pseudopolynomielle Laufzeit.
Zusammenfassend können wir festhalten, dass das Rucksackproblem mit Hilfe von Dynamischer Programmierung in pseudopolynomieller Zeit exakt lösbar ist. Da in der Praxis meist durch ein Polynom beschränkt ist, führt dies dann zu einer polynomiellen Laufzeit
(obwohl das Problem im Allgemeinen NP-schwierig ist).
2.1. EXAKTE ALGORITHMEN FÜR SCHWIERIGE OPTIMIERUNGSPROBLEME 37
2.1.3 Beschränkte Enumeration
Das Acht-Damen Problem
Wir wollen nun ein weiteres kombinatorisches Problem betrachten, das wir mit einer
Enumeration aller Möglichkeiten lösen wollen. Das Acht-Damen Problem ist ein klassisches
Schachproblem, das von Bezzel 1845 formuliert wurde. Die Aufgabe verlangt, acht Damen
auf einem Schachbrett so aufzustellen, dass keine Dame von einer anderen bedroht wird.
Bedrohungen gehen dabei gemäß den Schachregeln in horizontaler, vertikaler und diagonaler Richtung von jeder Dame aus, wie Abbildung 2.2 zeigt.
Bei diesem Problem geht es daher im Gegensatz zu vielen anderen nicht darum, aus einer
großen Menge gültiger Lösungen eine zu finden, die eine Zielfunktion minimiert, sondern
nur darum, überhaupt eine gültige Lösung zu finden ( Constraint satisfaction“ Problem).
”
Wir wollen hier dieses Problem verallgemeinern und gehen von & Damen und einem & &
Spielfeld aus. Die einzelnen
Spalten bezeichnen wir anstatt mit Buchstaben nun wie die
.
Zeilen mit den Zahlen #"#"" & . Außerdem wollen wir nicht nur eine richtige Lösung,
sondern alle erzeugen.
Ein naiver Ansatz, alle Möglichkeiten der Damenaufstellungen zu enumerieren ist in
)
+ ein Vektor, der die Position für jede Dame
Algorithmus
16 beschrieben. Dabei ist
.
"#"#" & speichert.
Ein Aufruf dieses rekursiven Algorithmus mit Naive-Enumeration(D,1) erzeugt
alle Kombinationen von allen möglichen Platzierungen jeder einzelnen Dame auf eines
0 $
0$
der & & Felder. Insgesamt werden daher 5& &
komplette Stellungen erzeugt und
auf Korrektheit überprüft. Das Überprüfen auf Korrektheit erfordert aber nochmals einen
0
0$ 0
ist!
Aufwand von 3.5& , womit der Gesamtaufwand 3(4&
8
7
6
5
4
3
2
1
A
B
C
D
E
F
G
H
Abbildung 2.2: Bedrohte Felder einer Dame im Acht-Damen Problem
KAPITEL 2. OPTIMIERUNGSALGORITHMEN
38
Algorithmus 16 . Naive-Enumeration
.
)
1: für
+ bis 4& & 2:
falls & dann .
3:
Naive-Enumeration 4:
' sonst 5:
Überprüfe und gib es aus, falls korrekt /* Komplette Platzierung erreicht */
6:
7:
'
'
.
/
Für & sind ca. Stellungen zu erzeugen. Selbst wenn 1 Million Stellungen pro
Sekunde überprüft werden könnten, würde das ca. 9 Jahre dauern. Wir sehen also, dass ein
derart naives Vorgehen sehr teuer kommen würde.
Durch Berücksichtigung folgender Beobachtungen kann das Verfahren entscheidend verbessert werden:
1. In einer korrekten Lösung muss in jeder Zeile genau eine Dame stehen.
2. Ebenso muss in jeder Spalte genau eine Dame stehen.
3. Ein paarweises Vertauschen von Damen ändert nichts an einer Lösung.
Wir fixieren daher zunächst, dass Dame immer in Spalte zu stehen kommt. Die Zeilen
aller Damen müssen immer unterschiedlich sein. Daher können wir nun alle möglichen
Stellungen, welche. die oberen Bedingungen erfüllen, enumerieren, indem wir alle Permuta)
tionen der Werte #""#" & ' erzeugen. Wenn eine solche Permutation ist, stellt dann
+
die Zeile für die Dame in der -ten Spalte dar.
Mit Algorithmus 17 können wir alle Permutationen erzeugen und die entsprechenden
Stellungen auf Richtigkeit überprüfen. ist dabei die Menge aller bisher . noch nicht
verwendeten Zeilen. Der Algorithmus wird mit Alle-Permutationen( , #""#" & ' ,1)
gestartet, wobei nicht initialisiert zu sein braucht.
/
/
Nun werden nur“ mehr & Stellungen erzeugt und auf Korrektheit untersucht ( ).
”
Die rekursiven Aufrufe können wie in Abbildung 2.3 gezeigt als Baum dargestellt werden.
Wir können das Verfahren weiter beschleunigen, indem wir so früh wie möglich ganze
Gruppen von Permutationen erkennen, die keine gültige Lösungen darstellen, und diese
erst gar nicht vollständig erzeugen. In Abbildung 2.3 würde das bedeuten, dass wir bei
Konflikten in bereits fixierten Knoten alle weiteren Unterbäume abschneiden“.
”
2.1. EXAKTE ALGORITHMEN FÜR SCHWIERIGE OPTIMIERUNGSPROBLEME 39
Algorithmus 17 Alle-Permutationen
1: für alle . )
2:
3:
4:
5:
6:
7:
8:
+ falls & dann .
)
Alle-Permutationen +' ' sonst Überprüfe auf diagonales Schlagen und gib Stellung aus, falls korrekt /* Komplette Stellung erreicht */
'
'
Man bezeichnet allgemein ein solches Vorgehen als begrenzte Enumeration im Gegensatz
zur vollständigen Enumeration.
Die folgende Variante des Algorithmus überprüft unmittelbar bei der Platzierung der -ten
Dame, ob diese nicht von einer bereits vorher platzierten bedroht wird (s. Algorithmus 18).
Nur wenn dies nicht der Fall ist, wird mit der Rekursion fortgesetzt. Der Überprüfung auf
diagonales Schlagen zweier Damen liegt die Idee zu Grunde, dass bei einer Bedrohung der
Absolutbetrag der Spaltendifferenz gleich dem Absolutbetrag der Zeilendifferenz sein muss.
Die exakte Berechnung des Aufwands ist nun schwierig, in der Praxis aber kommt es bei
nun nur mehr zu 2057 rekursiven Aufrufen im Vergleich zu 69281 beim Enumerieren
&
aller Permutationen. Eine richtige Lösung ist in Abbildung 2.4 dargestellt.
Eine weitere Verbesserung ist die Ausnutzung von Symmetrien.. So können wir uns in der
Enumeration bei der Platzierung der ersten Dame auf die Zeilen #"#"#" & beschränken. Zu
jeder dann gefundenen Lösung generieren wir automatisch gleich die um die Horizontale
gespiegelte Lösung:
Alle−Permutationen( Π,{1,...,8},1)
Π[1] fixiert
Π[2] fixiert
2
3 4 5 6 7 8
3
1
4
5
2
6
7
3
4
5
6
8
Π[3] fixiert
Abbildung 2.3: Rekursive Aufrufe in Alle-Permutationen.
7
8
KAPITEL 2. OPTIMIERUNGSALGORITHMEN
40
Algorithmus 18 Begrenzte-Enumeration
1: für alle ( )
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
+ .
solange
'
.
und
!
)
+
)
+ ! falls dann /* keine Bedrohung */
falls & dann )
+' Begrenzte-Enumeration ' sonst /* Komplette, gültige Stellung erreicht */
Gib Stellung aus
.
'
'
'
8
7
6
5
4
3
2
1
A
B
C
D
E
F
G
H
Abbildung 2.4: Eine Lösung zum Acht-Damen Problem.
2.1. EXAKTE ALGORITHMEN FÜR SCHWIERIGE OPTIMIERUNGSPROBLEME 41
)
+ &
)
+ .
.
#""#" & "
Der Aufwand der Enumeration sinkt dadurch um den Faktor 2.
Auch horizontale Spiegelungen und Rotationen können ausgenutzt werden, um den Aufwand
der Enumeration weiter zu senken.
2.1.4 Branch-and-Bound
Eine spezielle Form der beschränkten Enumeration ist das Branch-and-Bound Verfahren.
Dabei werden durch Benutzung von Primal- und Dualheuristiken möglichst große Gruppen
von Lösungen als nicht optimal bzw. gültig erkannt und von der vollständigen Enumeration
ausgeschlossen.
Die Idee von Branch-and-Bound ist einfach. Wir gehen hier von einem Problem aus, bei
dem eine Zielfunktion minimiert werden soll. (Ein Maximierungsproblem kann durch
Vorzeichenumkehr in ein Minimierungsproblem überführt werden.)
Zunächst berechnen wir z.B. mit einer Heuristik eine zulässige (im Allgemeinen nicht
optimale) Startlösung mit Wert
und eine untere Schranke
für alle möglichen
Lösungswerte (ein Verfahren hierzu nennt man auch Dualheuristiken).
Falls /
sein sollte, ist man fertig, denn die gefundene Lösung muss optimal sein.
Ansonsten wird die Lösungsmenge partitioniert (Branching) und die Heuristiken
werden auf die Teilmengen angewandt (Bounding).
Ist für eine (oder mehrere) dieser Teilmengen die für sie berechnete untere Schranke
(lower bound) nicht kleiner als die beste überhaupt bisher gefundene obere Schranke
(upper bound = eine Lösung des Problems), braucht man die Lösungen in dieser
Teilmenge nicht mehr beachten. Man erspart sich also die weitere Enumeration.
Ist die untere Schranke kleiner als die beste gegenwärtige obere Schranke, muss man
die Teilmengen weiter zerkleinern. Man fährt solange mit der Zerteilung fort, bis für
alle Lösungsteilmengen die untere Schranke mindestens so groß ist wie die (global)
beste obere Schranke.
Die Hauptschwierigkeit besteht darin, “gute” Zerlegungstechniken und einfache Datenstrukturen zu finden, die eine effiziente Abarbeitung und Durchsuchung der einzelnen
Teilmengen ermöglichen. Außerdem sollten die Heuristiken möglichst gute Schranken,
damit möglichst große Teilprobleme ausgeschlossen werden können.
KAPITEL 2. OPTIMIERUNGSALGORITHMEN
42
Branch-and-Bound für das asymmetrische TSP
Der Großvater“ aller Branch-and-Bound Algorithmen in der kombinatorischen Optimie”
rung ist das Verfahren von Little, Murty, Sweeny und Karel zur Lösung asymmetrischer
Travelling-Salesman-Probleme, das 1963 veröffentlicht wurde. Weil dieses Verfahren so
einfach ist, wollen wir es hier vorstellen.
Asymmetrisches Travelling Salesman Problem (ATSP)
! ! Knoten, die Städten entGegeben: Gerichteter vollständiger Graph mit
/
sprechen, und eine Distanzmatrix mit und .
Gesucht: Rundtour durch alle
Städte, die jede Stadt genau einmal besucht und
minimalen Distanzwert
aufweist.
Branch-and-Bound Verfahren von Little et al.
Idee: Zeilen- und Spaltenreduktion der Matrix und sukzessive Erzeugung von Teilproble
men beschrieben durch 1 .
Dabei bedeutet:
: Menge der bisher auf 1 fixierten Kanten
: Menge der bisher auf 0 fixierten Kanten
: Noch relevante Zeilenindizes von : Noch relevante Spaltenindizes von : Distanzmatrix des Teilproblems
: Untere Schranke für das Teilproblem
: Obere Schranke für das globale Problem
Vorgangsweise:
1. Das Anfangsproblem ist
.
.
#"#"" & ' "#"#" & ' /
wobei die Ausgangsmatrix ist.
Setze dieses Problem auf eine globale Liste der zu lösenden Probleme.
2. Sind alle Teilprobleme gelöst STOP.
2.1. EXAKTE ALGORITHMEN FÜR SCHWIERIGE OPTIMIERUNGSPROBLEME 43
Andernfalls wähle ein ungelöstes Teilproblem
aus der Problemliste.
3. Bounding:
a) Zeilenreduktion:
Für alle :
Berechne das Minimum der -ten Zeile von Setze und
b) Spaltenreduktion:
Für alle . :
Berechne das Minimum der -ten Spalte von und Setze c) Ist
, so entferne das gegenwärtige Teilproblem aus der Problemliste und
gehe zu 2.
Die Schritte (d) und (e) dienen dazu, eine neue (globale) Lösung des Gesamtproblems
zu finden. Dabei verwenden wir die Information der in diesem Teilproblem bereits
festgesetzten Entscheidungen.
d) Definiere den “Nulldigraphen” /
2
0! '
mit
e) Versuche, mittels einer Heuristik, eine Rundtour in zu finden. Wurde keine
Tour gefunden, so gehe zu 4: Branching.
Sonst hat die gefundene Tour die Länge . In diesem Fall:
2 #0 #1 Enferne alle Teilprobleme aus der Problemliste, deren lokale, untere Schranke größer gleich ist.
Setze in allen noch nicht gelösten Teilproblemen .
Gehe zu 2.
KAPITEL 2. OPTIMIERUNGSALGORITHMEN
44
4. Branching:
a) Wähle nach einem Plausibilitätskriterium eine Kante
gesetzt wird.
b) Definiere die neuen Teilprobleme
# ' ' ' /
.
, die oder
'
wobei aus durch Streichen der Zeile und der Spalte entsteht – von
darf nicht noch einmal hinaus- und nach nicht noch einmal hineingelaufen
werden.
0 ' wobei aus entsteht durch 2 ,
"
Füge diese Teilprobleme zur Problemliste hinzu und gehe zu 2.
Bemerkungen zum Algorithmus:
zu 3. Bounding:
Hinter der Berechnung des Zeilenminimums steckt die folgende Überlegung: Jede
Stadt muss in einer Tour genau einmal verlassen werden. Zeile der Matrix enthält
jeweils die Kosten für das Verlassen von Stadt . Und das geht auf keinen Fall billiger
als durch das Zeilenminimum . Das gleiche Argument gilt für das Spaltenminimum, denn jede Stadt muss genau einmal betreten werden, und die Kosten dafür sind
in Spalte gegeben.
Schritte (a) und (b) dienen nur dazu, eine untere Schranke zu berechnen (Bounding
des Teilproblems). Offensichtlich erhält man eine untere Schranke dieses Teilproblems
durch das Aufsummieren des Zeilen- und Spaltenminimums.
Für die Berechnung einer globalen oberen Schranke können Sie statt (d) und (e) auch
jede beliebige TSP-Heuristik verwenden.
zu 4. Branching:
Ein Plausibilitätskriterium ist beispielsweise das folgende:
Seien
!
' '
!
die minimalen Zusatzkosten, die entstehen, wenn wir von
Kante zu benutzen. Wir wählen eine Kante /
und
' '
nach gehen, ohne die
, so dass
! /
',"
2.1. EXAKTE ALGORITHMEN FÜR SCHWIERIGE OPTIMIERUNGSPROBLEME 45
Dahinter steckt die Überlegung: Wenn die Zusatzkosten sehr hoch sind, sollten wir
lieber direkt von nach gehen. Damit werden gute“ Kanten beim Enumerieren erst
”/
einmal bevorzugt. Wichtig dabei ist, dass sein muss, sonst stimmen die Berechnungen von und nicht mehr.
Bei der Definition eines neuen Teilproblems kann es vorkommen, dass die dort (in
EINS oder NULL) fixierten Kanten zu keiner zulässigen Lösung mehr führen können
(weil z.B. die zu EINS fixierten Kanten bereits einen Kurzzyklus enthalten). Es ist sicher von Vorteil, den Algorithmus so zu erweitern, dass solche Teilprobleme erst gar
nicht zu der Problemliste hinzugefügt werden (denn in diesem Zweig des Enumerationsbaumes kann sich keine zulässige Lösung mehr befinden).
Durchführung des Branch & Bound Algorithmus an einem ATSP Beispiel
Die Instanz des Problems ist gegeben durch die Distanzmatrix :
.
.
.
.
. .
Das Ausgangsproblem:
.
#"
""
.
"
' ""
' /
Bounding:
Die Zeilenreduktion ergibt:
/
.
.
/
.
/
/ /
/
.
/
.
.
.
/
.
/
/
.
.
/
Die Spaltenreduktion ändert hier nichts, da bereits in jeder Spalte das Minimum 0 beträgt.
Nun wird der Nulldigraph aufgestellt (siehe Abbildung 2.5). In diesem Nulldigraphen
existiert keine Tour, da 6 nur über 2 erreicht werden kann.
KAPITEL 2. OPTIMIERUNGSALGORITHMEN
46
6
1
5
2
3
4
Abbildung 2.5: Der Nulldigraph in Branch & Bound
Branching: Wähle Kante (2,6) und setze diese einmal gleich 1 und einmal gleich 0. Dies
ergibt zwei neue Teilprobleme.
Das erste neue Teilproblem:
' .
/
1#"
""
.
#"
/ /
""
.
/
.
/
.
/
/
' .
/
/
.
' Die untere Schranke für dieses Teilproblem nach Zeilen- und Spaltenreduktion ist
da nur die Zeile mit Index (6) um 2 reduziert werden muss.
Das zweite neue Teilproblem:
.
' 1
entsteht aus nach Setzen von 0 auf ebenfalls 12.
.
' 1
.
' /
.
,
. Die untere Schranke für dieses Problem ist
Abbildung 2.6 zeigt einen möglichen Enumerationsbaum für dieses Beispiel. (Dabei wird
nicht das vorgestellte Plausibilitätskriterium genommen.) Hier wird als nächstes die Kante
im Branching betrachtet, was Teilprobleme (4) und (5) erzeugt. Danach wird die Kante
1 . fixiert, was die Teilprobleme (6) und (7) begründet. Nach dem Fixieren der Kante
wird zum ersten Mal eine globale obere Schranke von 13 gefunden (d.h. eine Tour
im Nulldigraphen). Nun können alle Teilprobleme, deren untere Schranke gleich 13 ist, aus
2.1. EXAKTE ALGORITHMEN FÜR SCHWIERIGE OPTIMIERUNGSPROBLEME 47
1
L
1
L=10
Problemnummmer
untere Schranke
(2,6)
(2,6)
2
12
(4,2)
(4,2)
(2,4)
(2,4)
4
12
5
15
9
12
10
15
(3,4) nach 8 (6,2)
(3,4)
6
13
7
12
(5,1) nach 8 (3,1)
8
13
3
12
nach 8
11
15
(6,2)
12
18
13
13
(3,1)
14
13
Abbildung 2.6: Möglicher Enumerationsbaum für das Beispiel
der Problemliste entfernt werden. In unserem Beispiel wird als nächstes das Teilproblem (3)
gewählt, wobei dann über die Kante gebrancht wird, was Teilprobleme (9) und (10)
erzeugt. Weil wir mit Teilproblem (10) niemals eine bessere Lösung als 15 erhalten können
(untere Schranke), können wir diesen Teilbaum beenden. Wir machen mit Teilproblem (9)
weiter und erzeugen die Probleme (11) und (12), die auch sofort beendet werden
. können. Es
bleibt Teilproblem (7), das jedoch auch mit dem Branchen auf der Kante 1 zu “toten”
Teilproblemen führt.
Analyse der Laufzeit:
Die Laufzeit ist im Worst-Case exponentiell, denn jeder Branching-Schritt verdoppelt
die Anzahl der neuen Teilprobleme. Dies ergibt im schlimmsten Fall bei
Städten eine
Laufzeit von . Allerdings greift das Bounding i.A. recht gut, so dass man deutlich weniger
/
Städten mit Hilfe
Teilprobleme erhält. Doch bereits das Berechnen einer Instanz mit dieses Branch-and-Bound Verfahrens dauert schon zu lange. Das vorgestellte Verfahren ist
für TSPs mit bis zu 50 Städten anwendbar.
In der Praxis hat sich für die exakte Lösung von TSPs die Kombination von Branchand-Bound Methoden mit linearer Programmierung bewährt. Solche Verfahren, die als
Branch-and-Cut Verfahren bezeichnet werden, sind heute in der Lage in relativ kurzer Zeit
TSPs mit ca. 500 Städten exakt zu lösen. Die größten nicht-trivialen bisher exakt gelösten
TSPs wurden mit Hilfe von Branch-and-Cut Verfahren gelöst und umfassen ca. 15.000
Städte.
KAPITEL 2. OPTIMIERUNGSALGORITHMEN
48
Typische Anwendungen von Branch-and-Bound Verfahren liegen in Brettspielen, wie z.B.
Schach oder Springer-Probleme. Die dort erfolgreichsten intelligenten Branch-and-Bound
Verfahren funktionieren in Kombination mit parallelen Berechnungen.
2.2 Approximative Algorithmen und Gütegarantien
In diesem Abschnitt behandeln wir Heuristiken bzw. approximative Algorithmen für
Optimierungsprobleme. Eine Heuristik ist ein Verfahren, das eine zulässige Lösung für
ein Optimierungsproblem findet, die nicht notwendigerweise optimal ist. Man nennt eine
Heuristik auch approximativer Algorithmus, wenn man etwas über die Qualität (Güte) der
errechneten Lösungen aussagen kann.
Es gibt konstruktive Heuristiken, die eine zulässige Lösung konstruieren, und Verbesserungsheuristiken, die bereits als Input eine zulässige Lösung erhalten und versuchen diese zu verbessern (s. Abschnitt 2.3).
2.2.1 Konstruktive Heuristiken mit Gütegarantien
Wir unterscheiden zwischen Algorithmen mit und ohne Gütegarantie. Die “Güte” eines
Algorithmus sagt etwas über seine Fähigkeiten aus, optimale Lösungen gut oder schlecht
anzunähern. Die formale Definition lautet wie folgt:
Sei ein Algorithmus, der für jede Probleminstanz eines Optimierungsproblems eine
zulässige Lösung mit positivem Wert liefert. Dann definieren wir als den Wert der
Lösung des Algorithmus für Probleminstanz ; sei der optimale Wert für .
Für Minimierungsprobleme gilt: Falls
/
für alle Probleminstanzen und , dann heißt
die Zahl heißt Gütegarantie von Algorithmus .
ein -approximativer Algorithmus und
Für Maximierungsprobleme gilt: Falls
/
für alle Probleminstanzen und , dann heißt
die Zahl heißt Gütegarantie von Algorithmus .
ein -approximativer Algorithmus und
2.2. APPROXIMATIVE ALGORITHMEN UND GÜTEGARANTIEN
49
Wir betrachten als Beispiel ein Minimierungsproblem und jeweils die Lösungen einer Heuristik und die optimale Lösung zu verschiedenen Probleminstanzen :
.
0 / /
und
und
/
0 /
/
Die Information, die wir daraus erhalten, ist lediglich, dass die Güte der Heuristik
besser als 2 sein kann. Nur, falls
nicht
für alle möglichen Probleminstanzen gilt, ist
ein 2-approximativer Algorithmus.
Bemerkung: Es gilt
.
für Minimierungsprobleme:
für Maximierungsprobleme:
,
.
.
%
ist exakter Algorithmus
Im folgenden betrachten wir verschiedene approximative Algorithmen für das Bin-PackingProblem und für das Travelling Salesman Problem.
Beispiel 1: Packen von Kisten (Bin-Packing)
.
Gegeben: Gegenstände #""#" der Größe ; und beliebig viele Kisten der Größe
Gesucht: Finde die kleinste Anzahl von Kisten, die alle Gegenstände aufnehmen.
.
/
.
.
Beispiel: Gegeben sind beliebig viele Kisten der Größe
und 37 Gegenstände der
folgenden Größe: 7 Größe 6, 7 Größe 10, 3 Größe 16, 10 Größe 34, und 10 Größe
51.
Die optimale Lösung sieht folgendermaßen aus:
3
7
51 34 16
51 34 10 6
.
.
.
/
/
.
Die dargestellte Lösung benötigt nur 10 Kisten. Offensichtlich ist diese auch die optimale
Lösung, da jede Kiste auch tatsächlich ganz voll gepackt ist.
First-Fit-Heuristik (FF):
Die Gegenstände werden in beliebiger Reihenfolge behandelt. Jeder Gegenstand wird in die
Kiste gelegt, in die er passt.
KAPITEL 2. OPTIMIERUNGSALGORITHMEN
50
Unser Beispiel:
1
1
5
10
(7 (2 (2 (1 )6 (5 )10
)10 (3 )16
)34
)51
.
Das ergibt insgesamt 17 Kisten. Daraus ergibt sich
.
.
/
für diese Probleminstanz . Dies lässt bisher nur den Rückschluss zu, dass die Gütegarantie
der FF-Heuristik auf keinen Fall besser als sein kann.
Analyse der Gütegarantie:
Wir beweisen zunächst eine Güte von 2 für die First-Fit Heuristik.
.
Theorem: Es gilt:
für alle .
Beweis: Offensichtlich gilt: Jede FF-Lösung füllt alle bis auf eine der belegten Kisten
mindestens bis zur Hälfte. Daraus folgt:
.
.
für alle Man kann sogar noch eine schärfere Güte zeigen. Es gilt (ohne Beweis):
.
.
/
Weiterhin kann man zeigen, dass der asymptotisch beste Approximationsfaktor für die
FF-Heuristik ist. Für interessierte Studierende ist der Beweis in Johnson, Demers, Ullman,
Garey, Graham in SIAM Journal on Computing, vol. 3 no. 4, S. 299-325, 1974, zu finden.
Beispiel 2: Symmetrisches TSP
Gegeben: ungerichteter vollständiger Graph $ mit Distanzmatrix .
Gesucht: Rundtour kleinsten Gewichts, die alle Städte genau einmal besucht.
Eine beliebte Heuristik für das symmetrische TSP ist die Spanning Tree Heuristik. Sie
basiert auf der Idee einen minimal aufspannenden Baum zu generieren und daraus eine Tour
zu basteln.
2.2. APPROXIMATIVE ALGORITHMEN UND GÜTEGARANTIEN
51
Spanning-Tree-Heurisik (ST)
(1) Bestimme einen minimalen aufspannenden Baum
(2) Verdopple alle Kanten aus
Graph
von
$
.
0 (3) Bestimme eine Eulertour im Graphen 0 . Eine Eulertour ist eine Tour, die jede
Kante des Graphen genau einmal enthält. Man kann zeigen, dass ein Graph, in dem
jeder Knoten geraden Grad hat, immer eine Eulertour besitzt. Gib dieser Tour eine
Orientierung, wähle einen Knoten , markiere , setze 2 & (4) Sind alle Knoten markiert, setze -2 STOP;
'
ist die Ergebnis-Tour
(5) Laufe von entlang der Orientierung von bis ein unmarkierter Knoten
ist. Setze -2 ' , markiere , setze "2 gehe zu (4)
erreicht
Diskussion der Gütegarantie:
.
und einen
polynomiellen Algorithmus , der für jedes
Man kann zeigen: Gibt es ein .
symmetrische TSP eine Tour liefert mit , dann ist Theorem: Das -Approximationsproblem des symmetrischen TSP ist NP-vollständig (ohne
Beweis).
Doch es gibt auch positive Nachrichten. Wenn die gegebene TSP-Instanz gewisse Eigenschaften aufweist, dann ist es doch approximierbar:
Ein TSP heißt euklidisch wenn für die Distanzmatrix die Dreiecks-Ungleichung gilt, d.h.
für alle Knoten gilt: . Das heißt: es kann nie kürzer sein, von nach
nicht direkt, sondern über eine andere Stadt zu laufen. Dies ist meistens in der Praxis
erfüllt. (Denkt man beispielsweise an Wegstrecken, so gilt hier meist die Dreiecksgleichung.) Gute Nachrichten bringt das folgende Theorem:
Theorem: Für das euklidische TSP und die ST-Heuristik gilt:
Beweis: Es gilt:
für alle "
"
Die erste Abschätzung gilt wegen der Dreiecksungleichung, die letzte, da ein Minimum
Spanning Tree die billigste Möglichkeit darstellt, in einem Graphen alle Knoten zu verbinden.
KAPITEL 2. OPTIMIERUNGSALGORITHMEN
52
W
W
D
A
D
A
K
K
B
B
F
F
(a) MST
(b) Verdopplung der Kanten
W
W
D
A
D
A
K
K
B
B
F
F
(c) Orientierung
(d) Tour mittels ST-Heuristik
Abbildung 2.7: ST-Heuristik
Abbildung 2.7 zeigt eine Visualisierung der Spanning-Tree Heuristik anhand eines Beispiels
(Städte in Nord-Rhein-Westfalen und Hessen: Aachen, Köln, Bonn, Düsseldorf, Frankfurt,
Wuppertal).
Christophides-Heuristik (CH)
Diese 1976 publizierte Heuristik funktioniert ähnlich wie die Spanning-Tree-Heuristik,
jedoch wird Schritt (2) durch folgenden Schritt (2’) ersetzt. Dadurch erspart man sich die
Verdopplung der Kanten.
(2’) Sei die Menge der Knoten in
a) Bestimme im von kleinsten Gewichts
b) Setze 0
'
mit ungeradem Grad;
induzierten Untergraphen von
$
ein perfektes Matching
'
2.2. APPROXIMATIVE ALGORITHMEN UND GÜTEGARANTIEN
53
Anmerkungen:
Das Matching “geht auf”, denn wir wissen: ! !
ist immer gerade (s. Abschnitt 1.1).
Ein perfektes Matching in einem Graphen ist eine Kantenmenge, die jeden Knoten
genau einmal enthält. Sie ordnet jedem Knoten einen eindeutigen Partnerknoten zu
( Alle Knoten werden zu Paaren zusammengefasst).
Ein solches perfektes Matching kann in polynomieller Zeit gefunden werden.
Da eine Eulertour existiert, wenn die Knoten eines Graphen alle geraden Grad haben,
kann nun in den weiteren Schritten des Algorithmus wiederum sicher eine Rundreise
gefunden werden.
Abbildung 2.8 zeigt die Konstruktion an unserem Beispiel. Dabei betrachten wir in Schritt
(2’)a) den vollständigen Untergraphen, der von den Knoten induziert wird.
Analyse der Gütegarantie:
Theorem: Für das euklidische TSP und die Christophides-Heuristik gilt:
für alle Beweis: Seien 0 #"#"" 0 die Knoten von mit ungeradem Grad und zwar so numme
riert, wie sie in einer optimalen Tour vorkommen, d.h. #"#"#" 0 #"#"#" 0"#"#" ' .
'
'
0 + 1 #"#"#"#' und
0 0 1 "#"#" 0 ' . Es gilt:
Sei
Die erste Abschätzung gilt wegen der Dreiecks-Ungleichung (Euklidisches TSP). Die zweite
'
Abschätzung gilt, weil
das Matching kleinsten Gewichts ist. Weiterhin gilt:
%
.
"
Bemerkungen:
Die Christophides-Heuristik (1976) war lange Zeit die Heuristik mit der besten Gütegarantie. Vor kurzem zeigte Arora (1996), dass
das euklidische TSP beliebig nah ap.
/
proximiert werden kann: die Gütegarantie , kann mit Laufzeit
approximiert werden. Eine solche Familie von Approximationsalgorithmen wird als
polynomial time approximation scheme (PTAS) bezeichnet (s. Abschnitt 2.2.2).
Konstruktionsheuristiken für das symmetrische TSP erreichen in der Praxis meistens
eine Güte von ca. 10-15% Abweichung von der optimalen Lösung. Die ChristophidesHeuristik liegt bei ca. 14%.
KAPITEL 2. OPTIMIERUNGSALGORITHMEN
54
W
W
D
A
D
A
K
K
B
B
F
F
(b) (a) MST
W
W
D
A
D
A
K
B
K
B
F
(c) Orientierung
F
(d) Tour
Abbildung 2.8: Die CH-Heuristik an einem Beispiel
2.2. APPROXIMATIVE ALGORITHMEN UND GÜTEGARANTIEN
2.2.2
55
-Approximative Algorithmen
Definition: Ein Algorithmus ist ein PTAS (polynomial-time approximation scheme) für ein
Optimierungsproblem , falls er für jede Instanz in polynomieller Zeit (für fixiert) eine
-approximative Lösung berechnet.
Definition: Ein PTAS-Algorithmus heißt FPTAS (fully polynomial-time approximation
scheme) wenn die Laufzeit zusätzlich zur Eingabegröße der Instanz polynomiell in ist.
Beispiel: 0/1-Rucksackproblem
Wir betrachten noch einmal den bereits bekannten Algorithmus Dynamische Programmierung 0/1-Knapsack (s. Algorithmus13). Seine Laufzeit um den optimalen Lösungswert
0
zu berechnen ist
. Wir wenden den Algorithmus auf das folgende Beispiel an,
wobei und jeweils das Gewicht und die Kosten des Gegenstands bezeichnen.
.
/
/
.
.
. /
. /
.
. .
/
. .
/
.
Die Durchführung des Algorithmus ergibt: /
1
'
mit
. Der
Algorithmus musste 91 Paare konstruieren. Der Algorithmus müsste deutlich weniger
Paare berücksichtigen, wenn wir jeweils die letzte Stelle der -Werte ignoriert hätten (nach
Streichung erhalten
wir
Kosten ). In diesem
.
/ Fall hätte der Algorithmus die Lösung
(ca. 5% weg von Optimum) berechnet und
1 ' mit Wert
hätte dabei nur noch 36 Paare berücksichtigen müssen.
Nun stellt sich natürlich die Frage, wie groß der Fehler maximal werden kann, wenn wir die
letzte Stelle “vernachlässigen´´ um eine Beschleunigung der Laufzeit des Algorithmus zu
erhalten. Es gilt:
.
/
.
.
/
/
Allgemein gilt:
Beim Abschneiden der letzten Dezimalstellen entsteht ein Fehler von
.
/ maximal
.
KAPITEL 2. OPTIMIERUNGSALGORITHMEN
56
Als nächstes betrachten wir die neue Laufzeit. Sei der größte Wert aller ’s, dann gilt:
1
. Daraus ergibt sich eine bisherige Laufzeit von
, sowie nach dem
1
Abschneiden der letzten Stellen, eine neue Laufzeit von
.
Behauptung:
Für jede Wahl von ergibt der Algorithmus
Dynamische Programmierung
.
einen -approximativen Algorithmus mit .
.
Beweis: Sei
rung gilt: zeigen:
. Laut Definition eines -approximativen
Algorithmus bei Maximie.
.
. Und genau das können wir
, was gleichbedeutend ist mit .
/ .
/
Theorem: Für jeden Wert existiert ein
0/1-Rucksackproblem, der in Zeit
läuft.
-approximativer Algorithmus für das
.
Beweis: Um aus Algorithmus 13 einen ( man lediglich die letzten
)-approximativen Algorithmus zu machen, muß
Stellen
vernachlässigen. Dies ergibt einen FPTAS für das 0/1-Rucksackproblem mit Laufzeit
.
.
Algorithmus 19 faßt den PTAS noch einmal zusammen.
Algorithmus 19 FPTAS für das 0/1-Rucksackproblem
1: Sei der
Koeffizient und . größte
#" " " .
2: für / 3:
4:
5:
'
Wende DynProg auf
an.
2.3 Verbesserungsheuristiken
Verbesserungsheuristiken liegt die Idee zugrunde, eine gegebene zulässige Lösung durch
lokale Änderungen zu verbessern.
2.3. VERBESSERUNGSHEURISTIKEN
57
Wir unterscheiden zwischen einfache Austauschverfahren (s. Abschnitt 2.3.1) und lokalen
Suchverfahren bzw. Meta-Heuristiken, wie Simulated Annealing (s. Abschnitt 2.3.2) oder
evolutionäre Algorithmen (s. Abschnitt 2.3.3).
2.3.1 Einfache Austauschverfahren
Idee: Entferne einige Elemente
aus der gegenwärtigen Lösung
um eine Menge
zu erhalten. Nun versuchen wir, alle möglichen zulässigen Lösungen, die enthalten, zu
erzeugen. Davon wählen wir die beste.
Beispiel: Zweier-Austausch für das Symmetrische TSP (2-OPT):
(1) Wähle eine beliebige Anfangstour
(2) Setze
+
'!
.
0 + 0 1 #"#"#" +
$ '
1
.
.
& '
(3) Für alle Kantenpaare + ' aus :
Ist :
setzte ' '
gehe zu (2)
(4)
ist das Ergebnis.
Beispiel: r-Austausch für das Symmetrische TSP (r-OPT)
(1) Wähle eine beliebige Anfangstour
(2) Sei
0 + 0 1 #"#"#" + $1
'
die Menge aller -elementigen Teilmenge von
(3) Für alle :
Setze und konstruiere alle Touren, die enthalten. Gibt es eine unter diesen,
die besser als ist, wird diese das aktuelle und gehe zu (2).
(4)
ist das Ergebnis.
Eine Tour, die durch einen -Austausch nicht mehr verbessert werden kann, heißt in diesem
Zusammenhang -optimal; -optimale Touren sind sogenannte “lokale Optima”.
Bemerkungen:
Austauschverfahren werden sehr oft in der Praxis verwendet, um bereits vorhandene
Lösungen zu verbessern.
Für das TSP kommt 3-Opt meist sehr nah an die optimale Lösung (ca. 3-4%). Jedoch
/ /
erhält man bereits für
Städte sehr hohe Rechenzeiten, da der Aufwand bei
1
liegt.
KAPITEL 2. OPTIMIERUNGSALGORITHMEN
58
In der Praxis hat sich für das TSP als beste Heuristik die Heuristik von Lin und Kernighan (1973) herausgestellt, die oft bis zu 1-2% nahe an die Optimallösung herankommt. Diese Heuristik ist im Wesentlichen eine Mischung aus 2- und 3-OptVerfahren.
2.3.2 Simulated Annealing
Einfachen Austauschheuristiken wie 2-Opt ist es oft nicht möglich, mitunter schlechten
lokalen Optima zu entkommen“. Bei aufwändigeren Austauschheuristiken (z.B. -Opt,
”
) steigt aber der Aufwand meist allzu stark mit der Problemgröße an. Simulated
Annealing ist eine allgemeine Verbesserungsstrategie (eine Meta-Heuristik), die es bei
skalierbarem Zeitaufwand erlaubt, lokalen Optima grundsätzlich auch zu entkommen.
Abgeleitet wurde dieses Verfahren von dem physikalischen Prozess, ein Metall in einen
Zustand möglichst geringer innerer Energie zu bringen, in dem die Teilchen möglichst
strukturiert angeordnet sind. Dabei wird das Metall erhitzt und sehr langsam abgekühlt. Die
Teilchen verändern anfangs ihre Positionen und damit ihre Energieniveaus sehr stark, mit
sinkender Temperatur werden die Bewegungen von lokal niedrigen Energieniveaus weg
aber immer geringer.
Der von Kirkpatrick et al. (1983) vorgeschlagene Algorithmus des Simulated Annealing
(s. Algorithmus 20).
Algorithmus 20 Simulated Annealing
/
1: 2:
Tinit
3: Ausgangslösung
4: Wiederhole
5: /* leite eine Nachbarlösung ab
6: falls besser als dann 7:
8: ' sonst )
) 9:
falls
dann 10:
'
11:
12:
. 13:
14: '
15: Bis Abbruchkriterium erfüllt
ist hierbei eine Zufallszahl
*/
)/
.
, /
die Bewertung der Lösung .
2.3. VERBESSERUNGSHEURISTIKEN
59
Die Ausgangslösung kann entweder zufällig oder – meist sinnvoller – mit einer Konstruktionsheuristik generiert werden. In jeder Iteration wird dann ausgehend von der aktuellen
Lösung eine ähnliche Lösung durch eine kleine zufällige Änderung abgeleitet. Ist
besser als , so wird in jedem Fall als neue aktuelle Lösung akzeptiert. Ansonsten,
wenn also eineschlechtere Lösung darstellt, wird diese mit einer Wahrscheinlichkeit
)
)
akzeptiert. Dadurch ist die Möglichkeit gegeben, lokalen Optima
zu entkommen.
Die Wahrscheinlichkeit hängt von der Differenz der Bewertungen von und ab
– nur geringfügig schlechtere Lösungen werden mit höherer Wahrscheinlichkeit akzeptiert
als viel schlechtere. Außerdem spielt die Temperatur eine Rolle, die über die Zeit hinweg
kleiner wird: Anfangs werden Änderungen mit größerer Wahrscheinlichkeit erlaubt als
später.
Als Abbruchkriterium sind unterschiedliche Bedingungen vorstellbar:
Ablauf einer bestimmten Zeit; eine gefundene Lösung ist gut genug“; oder keine Verbesse
”
rung in den letzten Iterationen.
Wie initialisiert und in Abhängigkeit der Zeit vermindert wird, beschreibt das CoolingSchema.
Geometrisches Cooling:
$ 2 z.B. Sind bzw. verwendet.
nicht bekannt, so werden Schranken bzw. Schätzungen hierfür
.
(z.B. 0,999)
Adaptives Cooling:
Es wird der Anteil der Verbesserungen an den letzten erzeugten Lösungen gemessen und auf
Grund dessen stärker oder schwächer reduziert.
Generierung von Nachbarlösungen
Die Nachbarschaftsfunktion
Bedingungen erfüllen muss:
ist eine stochastische Funktion, die die zwei folgenden
Erreichbarkeit eines Optimums:
Ausgehend von jeder möglichen Lösung muss eine optimale Lösung durch wiederholte Anwendung der Nachbarschaftsfunktion mit einer Wahrscheinlichkeit größer Null
erreichbar sein.
KAPITEL 2. OPTIMIERUNGSALGORITHMEN
60
Lokalitätsbedingung:
Eine Nachbarlösung muss aus ihrer Ausgangslösung durch ein kleine“ Änderung
”
abgeleitet werden können. Dieser kleine Unterschied muss im Allgemeinen (d.h.
mit Ausnahmen) auch eine kleine Änderung der Bewertung bewirken. Sind diese
Voraussetzungen erfüllt, spricht man von hoher Lokalität – eine effiziente Optimierung ist möglich. Haben eine Ausgangslösung und ihre abgeleitete Nachbarlösung
im Allgemeinen große Unterschiede in der Bewertung, ist die Lokalität schwach. Die
Optimierung ähnelt dann der Suche einer Nadel im Heuhaufen“ bzw. einer reinen
”
Zufallssuche und ist nicht effizient.
Es folgen Beispiele für sinnvolle Nachbarschaftsfunktionen.
Beispiel: TSP
Inversion: Ähnlich wie im 2-Opt werden zwei nicht benachbarte Kanten einer aktuellen
Tour zufällig ausgewählt und entfernt. Die so verbleibenden zwei Pfade werden mit zwei
neuen Kanten zu einer neuen Tour zusammengefügt.
Anmerkung: Dass dieser Operator keinerlei Problemwissen wie etwa Kantenkosten ausnutzt,
ist einerseits eine Schwäche; andererseits ist das Verfahren aber so auch für schwierigere Varianten des TSPs, wie dem blinden TSP, einsetzbar. Bei dieser Variante sind die Kosten einer
Tour nicht einfach die Summe fixer Kantenkosten, sondern im allgemeinen eine nicht näher
bekannte oder komplizierte, oft nicht-lineare Funktion. Beispielsweise können die Kosten
einer Verbindung davon abhängen, wann diese verwendet wird. (Wenn Sie mit einem PKW
einerseits in der Stoßzeit, andererseits bei Nacht durch die Stadt fahren, wissen Sie, was
gemeint ist.)
Beispiel: Mehrdimensionales Rucksack-Problem
Das einfache 0/1-Rucksack-Problem, wie es in Abschnitt 2.1.1 beschrieben wurde, kann mit
exakten Verfahren auch für sehr große Instanzen im Allgemeinen gut gelöst werden. Hier
ist der Einsatz einer Heuristik wie Simulated Annealing nicht sinnvoll. Es gibt jedoch auch
hier deutlich schwierigere Varianten, wie das mehrdimensionale Rucksackproblem:
/
Gegeben: & Gegenstände
mit Werten und Resourcen der. Größe #"#"#" . Jeder
.
#""#" &
#""#" Gegenstand verbraucht von jeder Resource
eine bestimmte
/
Menge .
Gesucht: Teilmenge von Gegenständen, beschrieben durch einen Vektor maximalem Gesamtwert
$
/
.
'
$
, mit
2.3. VERBESSERUNGSHEURISTIKEN
61
wobei die vorhandenen Resourcen nicht überschritten werden dürfen:
$
.
#""#" "
Eine sinnvolle Nachbarschaftsfunktion, die nur zulässige Lösungen erzeugt, wäre beispielsweise:
Ein zufällig ausgewählter, bisher nicht eingepackter Gegenstand wird zur Lösung hinzugenommen. Überschreitet diese Lösung nun irgendeine Resource, wird so lange ein
eingepackter Gegenstand entfernt, bis die Lösung wieder gültig ist.
Beispiel: Quadratic Assignment Problem
und & Standorte eines Betriebs.
Gegeben: & Abteilungen
.
/
, . #"#"" & , seien die Distanzen zwischen den Standorten.
/
, "#"#" & , sei das Verkehrsaufkommen zwischen den Abteilungen und .
.
.
Gesucht: Eineindeutige Zuordnung *2 "#"#" & '
orten mit minimalem Gesamtverkehrsaufkommen
$
$
#"
"" & '
von Abteilungen zu Stand-
.
Vergleichbar mit dem TSP beschreiben auch hier alle Permutationen von "#"#" & ' gültige
Lösungen. Die Nachbarschaftsfunktion vom TSP ist hier jedoch nicht geeignet, da sie
hier geringe Lokalität besitzt: Lösungen sind nun nicht rotationsinvariant; es kommt auf
absolute“ Positionen und nicht auf Kanten an.
”
Besser: Vertausche zwei zufällig gewählte Abteilungen.
(Diese Nachbarschaftsfunktion hat umgekehrt für das TSP geringere Lokalität.)
2.3.3 Evolutionäre Algorithmen
Unter dem Begriff evolutionäre Algorithmen werden eine Reihe von Meta-Heuristiken
(genetische Algorithmen, Evolutionsstrategien, evolutionary programming, genetic programming, etc.) zusammengefasst, die Grundprinzipien der natürlichen Evolution in
einfacher Weise nachahmen. Konkret sind diese Mechanismen vor allem die Selektion
(natürliche Auslese, survival of the fittest“), die Rekombination (Kreuzung) und die
”
Mutation (kleine, zufällige Änderungen).
KAPITEL 2. OPTIMIERUNGSALGORITHMEN
62
Ein wesentlicher Unterschied zu Simulated Annealing ist, dass nun nicht mehr mit nur einer
aktuelle Lösung, sondern einer ganze Menge (= Population) gearbeitet wird. Durch diese
größere Vielfalt ist die Suche nach einer optimalen Lösung breiter“ und robuster, d.h. es
”
wird nicht so leicht bei einem lokalem Optimum hängengeblieben.
Algorithmus 21 zeigt den Basisablauf eines evolutionären Algorithmus.
Algorithmus 21 Grundprinzip eines evolutionären Algorithmus
1: = Menge von Ausgangslösungen
2: bewerte( )
3: wiederhole
4:
Qs = Selektion( )
5:
Qr = Rekombination(Qs)
6:
P = Mutation(Qr)
7:
bewerte( )
8: bis Abbruchkriterium erfüllt
Ausgangslösungen können wiederum entweder zufällig oder mit einfachen Erzeugungsheuristiken generiert werden. Wichtig ist jedoch, dass sich diese Lösungen unterscheiden und
so Vielfalt gegeben ist.
Selektion
Die Selektion kopiert aus der aktuellen Population Lösungen, die in den weiteren
Schritten neue Nachkommen produzieren werden. Dabei werden grundsätzlich bessere
Lösungen mit höherer Wahrscheinlichkeit gewählt. Schlechtere Lösungen haben aber im
Allgemeinen auch Chancen, selektiert zu werden, damit lokalen Optima entkommen werden
kann.
Fitness-proportionale Selektion:
/
Sei die Bewertung (= Fitness) jeder Lösung wir davon aus, dass diese Funktion maximiert werden soll.
#"#"#" '
und gehen
Wir ordnen jeder Lösung eine Selektionswahrscheinlichkeit
zu. Die Auswahl erfolgt nun zufällig entsprechend den Wahrscheinlichkeiten .
Zu achten ist auf die Verhältnisse zwischen den Selektionswahrscheinlichkeiten
der Lösun.
#"#"#" ' und *
gen in . Sei ! ! . Als Selektionsdruck
2.3. VERBESSERUNGSHEURISTIKEN
63
wird das Verhältnis bezeichnet, d.h. der Faktor, um den die beste Lösung
erwartungsgemäß häufiger selektiert wird als eine durchschnittliche.
Ist der Selektionsdruck zu niedrig (alle Lösungen haben große und sehr ähnliche Bewertungen), werden gute Lösungen zu wenig bevorzugt und der evolutionäre Algorithmus ähnelt
einer Zufallssuche.
Ist umgekehrt der Selektionsdruck zu hoch, werden gute Lösungen zu stark bevorzugt, die
Vielfalt in der Population verringert sich rasch und der evolutionäre Algorithmus konvergiert
zu einem lokalen Optimum.
Um den Selektionsdruck steuern zu können, skaliert man die Bewertungsfunktion beispielsweise linear über mit geeigneten Werten und und verwendet dann
diese skalierten Fitnesswerte für die Selektion. Skalierung ist auch notwendig, wenn ein
/
Minimierungsproblem vorliegt ( ist dann negativ) oder sein kann.
Tournament Selektion:
Eine andere häufig eingesetzte Variante zur Selektion einer Lösung funktioniert wie folgt:
(1) Wähle aus der Population Lösungen gleichverteilt zufällig aus
(Mehrfachauswahl ist üblicherweise erlaubt).
(2) Die beste der Lösungen ist die selektierte.
Für die Selektionswahrscheinlichkeit einer Lösung spielen die relativen Unterschiede der
Fitnesswerte keine Rolle mehr, sondern nur mehr der fitness-basierte Rang. Eine Skalierung
ist deshalb nicht erforderlich. Der Selektionsdruck kann über die Gruppengröße gesteuert
werden.
Rekombination
Die Aufgabe der Rekombination ist, aus zwei selektierten Elternlösungen eine neue Lösung
zu generieren. Vergleichbar mit der bereits beim Simulated Annealing beschriebenen
Lokalitätsbedingung ist hier wichtig, dass die neue Lösung möglichst ausschließlich aus
Merkmalen der Eltern aufgebaut wird.
Mutation
Die Mutation entspricht der Nachbarschaftsfunktion beim Simulated-Annealing. Sie dient
meist dazu, neue oder verlorengegangene Merkmale in die Population hineinzubringen.
Häufig werden die Rekombination und Mutation nicht immer, sondern nur mit einer
bestimmten Wahrscheinlichkeit ausgeführt, sodass vor allem gute Lösungen manchmal auch
KAPITEL 2. OPTIMIERUNGSALGORITHMEN
64
unverändert in die Nachfolgegeneration übernommen werden.
Wir sehen uns im Folgenden konkrete Beispiele an.
Beispiel: TSP
Edge-recombination (ERX):
0
Ziel: Aus zwei Elterntouren
und
soll eine neue Tour
nur aus Kanten der Eltern aufgebaut ist.
(1) Beginne bei einem beliebigen Startknoten
(2) Sei (3) Ist ;
erstellt werden, die möglichst
'
die Menge der noch unbesuchten Knoten, die in
'
, wähle einen Nachfolgeknoten 0
adjazent zu
sind.
zufällig aus.
(4) Ansonsten wähle einen zufälligen noch nicht besuchten Nachfolgeknoten .
(5) Setze
' '
und
.
(6) Gibt es noch unbesuchte Knoten, gehe zu Schritt 2.
(7) Schließe die Tour:
' '
Hier werden neue Kanten, die nicht von den Eltern stammen, nur dann zu hinzugefügt,
wenn leer ist. Um die Wahrscheinlichkeit, dass es zu diesen Situationen kommt, möglichst
gering zu halten, kann die Auswahl in Schritt 3 wie folgt verbessert werden:
(3’) Ermittle für jeden Knoten die Anzahl der gültigen Knoten, die von diesem
dann weiter unter Verwendung von Elternkanten angelaufen werden können, d.h. 0 !
! + ' noch nicht besucht in ' !"
Wähle ein für das minimal ist.
Mutation: Inversion
Beispiel: Quadratic Assignment Problem
Rekombination:
Um größtmögliche Lokalität zu gewährleisten, wollen wir hier aus zwei Elternlösungen
0
(Abteilungen/Standorte-Zuordnungen) und eine neue Lösung ableiten, in welcher
0
der Standort einer jeden Abteilung immer entweder von oder von übernommen wird.
Das Ergebnis dabei muss wiederum eine gültige Permutation sein.
Man bezeichnet die folgende Rekombination als cycle crossover:
2.3. VERBESSERUNGSHEURISTIKEN
(1) Initialisiere alle (2)
leer für
.
#""#" &
.
(3) Setze $
(4) Sei
.
0 ; ermittle "!
&
(6) Wenn (5) Setze
(7)
65
leer, gehe zu Schritt 3.
.
#"#"#" : Wenn &
leer, dann setze 0 Mutation: Vertauschen der Standorte zweier Abteilungen.
Beispiel: Mehrdimensionales Rucksackproblem
Initialisierung: Zufallszahl
/
.
.
'
&
Rekombination:
Seien und 0
die Elternlösungen, die neue Lösung.
1-point crossover:
.
Wähle einen crossover-point * # ""#" & .
#"#"#" &
& 2
.
'
zufällig.
0
für
sonst
oder uniform crossover:
.
#""#" Mutation: Wähle ein
&
setze per Zufallsentscheidung 2
.
*
#""#" & '
zufällig und setze .
oder 0
"
.
Problem: Ungültige Lösungen, die Resourcen überschreiten!
Lösungsmöglichkeiten:
Repair“-Algorithmus auf jede generierte (ungültige) Lösung anwenden. Hier z.B. so lan”
ge einen zufälligen Gegenstand entfernen, bis die Lösung alle Resourcenbeschränkungen erfüllt.
KAPITEL 2. OPTIMIERUNGSALGORITHMEN
66
“Bestrafung” zur Bewertung hinzufügen (Lagrange’scher Ansatz)
$
/
$
Adaptive Einstellung der Lagrange-Faktoren
.
/
Anfang:
Alle Generationen:
$
Für alle , für die "
0
$
/
0 0 ""#"
:
"" : setze
(
/
).
Abschließend sei zu evolutionären Algorithmen noch angemerkt, dass sie mit anderen Heuristiken und lokalen Verbesserungstechniken sehr effektiv kombiniert werden können. So ist
es möglich
. . . Ausgangslösungen mit anderen Heuristiken zu erzeugen.
. . . in der Rekombination und Mutation Heuristiken einzusetzen (z.B. können beim
TSP kostengünstige Kanten mit höherer Wahrscheinlichkeit verwendet werden).
. . . alle (oder einige) erzeugte Lösungen mit einem anderen Verfahren (z.B. 2-Opt)
noch lokal weiter zu verbessern.
Auch können die Vorteile paralleler Hardware gut genutzt werden – die Evolution ist ja quasi
von Natur aus“ parallel.
”
Weiterführende Literatur
W.J. Cook, W.H. Cunningham, W.R. Pulleyblank und A. Schrijver: “Combinatorial
Optimization”, John Wiley & Sons, Chichester, 1998
C. H. Papadimitriou und K. Steiglitz: “Combinatorial Optimization: Algorithms and
Complexity”, Prentice-Hall, Inc., Englewood Cliffs, New Jersey, 1982
E. Aarts und J.K. Lenstra: “Local Search in Combinatorial Optimization”, John Wiley
& Sons, Chichester, 1997
Z. Michalewicz: “Genetic Algorithms + Data Structures = Evolution Programs”,
Springer, 1996
Kapitel 3
Geometrische Algorithmen
In der Praxis kommt es immer wieder vor, dass geometrische Daten gegeben sind, d.h.
Daten, die gewisse Koordinaten im -dimensionalen Raum besitzen. Denken Sie z.B. an
Städte auf einer Landkarte oder auch an Komponenten im Chip-Design. Geometrische
Algorithmen, auch “Algorithmische Geometrie” oder “Computational Geometry” genannt,
nutzen die geometrischen Eigenschaften der Daten aus, um effiziente Algorithmen zu erhalten. Anwendungsgebiete liegen in der Bildverarbeitung, Computergraphik, Geographie,
CAD, oder auch im Chip-Layout.
Ein grundlegendes Prinzip im Bereich der geometrischen Algorithmen ist das Scan-Line
Prinzip, das wir im folgenden Abschnitt betrachten. Ein weiteres wichtiges Problemfeld ist
die Bereichssuche, die im darauffolgenden Abschnitt behandelt wird.
3.1 Scan-Line Prinzip
Das Scan-Line Prinzip ist ein wichtiges Paradigma im Bereich der geometrischen Algorithmen. Es ist immer dann anwendbar, wenn eine Menge von Objekten auf einer 2dimensionalen Fläche gegeben ist.
Die grundlegende Idee ist, eine vertikale Linie von links nach rechts über die Objektmenge zu
führen, um dadurch das zweidimensionale (statische) Problem in eine dynamische Folge von
eindimensionalen Problemen umzuwandeln. Die Scan-Line teilt zu jeder Zeit die Objekte
ein in
tote Objekte, die vollständig links von
aktive Objekte, die gegenwärtig von
inaktive Objekte, die künftig von
liegen
geschnitten werden
geschnitten werden
Wir betrachten im folgenden das Schnittproblem für Liniensegmente, in dem es darum geht,
einen oder alle Schnittpunkte von Liniensegmenten zu bestimmen. Zunächst diskutieren wir
den einfacheren Fall der iso-orientierten Liniensegmente.
67
KAPITEL 3. GEOMETRISCHE ALGORITHMEN
68
B
C
A
D
E
A A AAADD 0
EDDE
EE
(A,B)
(A,C)
(C,D)
Abbildung 3.1: Eine Probleminstanz für den Schnitt von iso-orientierten Liniensegmenten
3.1.1 Das Schnittproblem für Iso-orientierte Liniensegmente
Bei diesem Problem geht es darum, alle Schnittpunkte einer Menge von senkrechten und
horizontalen Geradensegmenten zu bestimmen.
Gegeben:
Gesucht:
Menge von insgesamt & vertikalen und horizontalen Liniensegmenten in der
Ebene #""#" #$ '
alle Paare sich schneidender Segmente
Ein Beispiel für eine solche Problemstellung finden Sie in Abbildung 3.1 (die Buchstaben
unter der Zeichnung werden später erklärt). Eine naive Methode zur Lösung des Problems
wäre es, alle Paare von Segmenten auf Schnitt zu testen. Dies ergäbe eine Gesamtlaufzeit
0
3(4& . Tatsächlich gibt es Probleminstanzen, für die es keine schnellere Lösung gibt, da es
0
3(4& Schnittpunkte gibt. Wenn allerdings die Anzahl der Schnittpunkte klein ist und nicht
quadratisch in & wächst, dann liefert der Scan-Line-Algorithmus eine bessere Laufzeit.
Wir machen eine vereinfachende Annahme: Alle Anfangs- und Endpunkte horizontaler
Segmente und alle vertikalen Segmente haben paarweise verschiedene -Koordinaten.
Algorithmus 22 verwendet das Scan-Line Prinzip für die Lösung unseres Problems. Er nutzt
dabei folgende Beobachtungen aus:
Trifft man mit der Scan-Line auf ein vertikales Segment , so kann
punkte mit den gerade aktiven horizontalen Segmenten haben.
nur Schnitt-
3.1. SCAN-LINE PRINZIP
69
Man muss die Scan-Line nicht kontinuierlich über die Fläche führen. Es genügt,
sie jeweils an Haltepunkten zu beobachten. Haltepunkte sind die -Koordinaten der
Anfangs- und Endpunkte horizontaler Segmente und die -Koordinaten vertikaler Segmente.
Algorithmus 22 Scan-Line für Schnitt iso-orientierter Liniensegmente
1:
: Menge der Haltepunkte in aufsteigender -Reihenfolge;
2:
; /* Menge
der aktiven Segmente, aufsteigend nach sortiert */
3: solange : nächster Haltepunkt von ;
4:
5:
falls ( ist linker Endpunkt eines horizontalen Segments ) dann 6:
Füge in ein;
' sonst 7:
8:
falls ( ist rechter Endpunkt eines horizontalen Segments ) dann 9:
Entferne aus ;
10:
' sonst 11:
/* ist -Wert eines vertikalen Segments mit unterem Endpunkt ( ) und
oberem Endpunkt ( ) */
12:
Bestimme alle horizontalen Segmente aus , deren -Koordinaten im Be)
reich + liegen und gib als Paar sich schneidender Segmente aus;
13:
14:
15:
'
'
'
Die Spalten von Buchstaben unter den Haltepunkten in Abbildung 3.1 geben jeweils den
Inhalt der Menge an. Darunter finden Sie die von dem Algorithmus berechneten Paare
von sich kreuzenden Segmenten mit Pfeilen zu den Haltepunkten, bei denen sie ausgegeben
werden.
offen.
Allerdings lässt der Algorithmus noch die Realisierung der geordneten Menge
Hierfür wird eine Datenstruktur benötigt, die die folgenden Operationen effizient ausführen
kann. Dies sind:
Einfügen eines neuen Elements
Entfernen eines Elements
Bestimmen aller Elemente, die in einen gegebenen Bereich
frage)
)
+
fallen (Bereichsab-
Eine mögliche Lösung bieten z.B. balancierte Binärbäume an (z.B. AVL-Bäume). Damit
können die Einfüge-Operationen und die Entfernungs-Operationen in Zeit ! ! durchgeführt werden. Die Bereichsabfrage in Zeile 12 von Algorithmus 22 könnte dann folgendermaßen realisiert werden:
KAPITEL 3. GEOMETRISCHE ALGORITHMEN
70
1. Suche in den Knoten mit kleinstem Schlüssel
falls sein Schlüssel .
und gib als Segment aus,
2. Laufe ab Knoten in Inorder-Reihenfolge durch, bis Knoten mit Schlüssel
erreicht wird, gib alle dabei durchlaufenen Segmente außer aus.
Algorithmus 23 zeigt die Realisierung des ersten Schritts. Die Bezeichnungen für die balancierten Binärbaüme wurden vom SS01 Skript Algorithmen und Datenstrukturen 1 übernommen. Die Realisierung des zweiten Schritts wird hier nicht ausgeführt, weil es sich um eine
schlichte Inorder-Durchmusterung handelt.
Algorithmus 23 Suche Knoten mit kleinstem Schlüssel
1: root; NULL;
2: solange (( NULL)
(p.key )) ;
3:
4:
falls ( p.key) dann p.leftson;
5:
' sonst 6:
p.rightson;
7:
8:
9:
10:
11:
12:
'
'
falls ( NULL) dann Return /* Schlüssel gefunden */
falls (root NULL) dann Return NULL
/* leerer Baum */
falls ( NULL) dann Return NULL bzw. root abhängig von root.key
besteht aus einem Knoten */
13: falls ( " ) dann Return
sonst Return Successor(q)
/* Baum
Analyse der Laufzeit
Wir betrachten zunächst die Laufzeit der Bereichssuche: Ist die Anzahl der Elemente im
)
Bereich + , so ist die Laufzeit für eine Bereichsabfrage & ( & für den
ersten Schritt der Bereichssuche und für den zweiten). Somit ergibt sich für den ScanLine Algorithmus eine Gesamtlaufzeit von 4& & , wobei die Anzahl der sich
schneidenden Segmente ist. Der Platzverbrauch ist hingegen nur linear: 5& .
Bemerkungen:
1. Das Scan-Line Verfahren ist dem naiven Verfahren überlegen, wenn
quadratisch mit der Anzahl der Segmente wächst.
schwächer als
2. Man kann zeigen, dass im schlechtesten Fall 5& & Schritte erforderlich sind,
um das RSS-Problem zu lösen. Somit ist das hier beschriebene Verfahren worst case
optimal.
3.1. SCAN-LINE PRINZIP
71
y
x
Abbildung 3.2: Eine Menge von Geradensegmenten in allgemeiner Lage
3.1.2 Schnitt von Allgemeinen Liniensegmenten
Bei diesem Problem ist eine Menge von Liniensegmenten in allgemeiner Lage gegeben und
wieder sollen die Paare sich schneidender Segmente berechnet werden. Im Gegensatz zum
vorigen Problem sind nun nicht nur waagerechte und senkrechte Segmente erlaubt
Gegeben:
Gesucht:
Menge von & Liniensegmenten #"#"" $ in der Ebene
Menge echter Schnittpunkte (echte Schnittpunkte sind Kreuzungen von Segmenten, die keine Endpunkte sind)
Um den Algorithmus nicht durch viele Fallunterscheidungen zu komplizieren, machen wir
auch hier wieder vereinfachende Annahmen, die man auch zusammengefaßt Allgemeine Lage der Segmente nennt:
1. Kein Segment ist senkrecht.
2. Der Schnitt zweier Segmente ist stets leer oder genau ein Punkt.
3. Es schneiden sich nie mehr als zwei Segmente in einem Punkt.
4. Alle Endpunkte und Schnittpunkte haben paarweise verschiedene -Koordinaten.
Abbildung 3.2 zeigt eine Menge von Geradensegmenten in allgemeiner Lage. Wir benutzen
auch hier wieder einen Sweepline-Algorithmus mit einer senkrechten Sweepline, die sich
von links nach rechts über die Fläche bewegt.
Beobachtung 1: Wenn sich das Segment mit dem Segment
bei -Koordinate schneidet, dann sind und bei -Koordinate Nachbarn auf der Sweepline für ein
hinreichend kleines aber positives . Daraus ergibt sich, dass es genügt, auf der Sweepline
benachbarte Segmente auf Schnitt zu testen.
KAPITEL 3. GEOMETRISCHE ALGORITHMEN
72
Beobachtung 2: Am Schnittpunkt zweier Segmente vertauscht sich die Reihenfolge der betroffenen Segmente auf der Sweepline. Daraus ergeben sich folgende Konsequenzen:
1. Auch Schnittpunkte müssen Ereignisse sein.
2. Schnittpunkte müssen während des Sweeps in die Ereignisstruktur eingefügt werden.
Wir definieren ein Ereignis als einen Haltepunkt der Sweepline. Es gibt 3 Typen von Ereignissen:
1. Anfang eines Segments
2. Ende eines Segments
3. Schnitt zweier Segmente
Zusammen mit den jeweiligen -Koordinaten merken wir uns auch die IDs der betroffenen
Segmente. Wir führen eine Ereignisdatenstruktur (ES) mit den folgenden Operationen ein:
1. “NächstesEreignis()”: liefert das Ereignis mit der kleinsten -Koordinate aus ES und
löscht es.
2. “FügeEin(Ereignis)”: fügt ein neues Ereignis in ES ein.
Diese Datenstruktur kann durch einen balancierten Baum oder durch einen Heap implementiert werden. Bei beiden Implementierungen ist die Laufzeit der Operationen logarithmisch
in der Größe von ES.
Weiterhin benötigen wir eine Sweep-Status-Struktur (SSS). Diese speichert Segmente geordnet nach der -Koordinate ihres momentanen Schnittpunkts mit der Sweepline. Die Datenstruktur stellt folgende Operationen zur Verfügung:
1. “FügeEin(Segment)”: Fügt ein Segment in SSS ein
2. “Entferne(Segment)”: Entfernt ein Segment aus SSS
3. “Vorg(Segment)”: Liefert den Vorgänger eines Segments auf der Sweepline
4. “Nachf(Segment)”: Liefert den Nachfolger eines Segments auf der Sweepline
5. “Vertausche(Segment1, Segment2)”: Vertauscht die Reihenfolge zweier Segmente auf
der Sweepline
Diese Struktur kann durch einen blattorientierten balancierten binären Suchbaum mit
Verkettung der Blätter (z.B. B-Baum) realisiert werden.
Algorithmus 24 zeigt unseren Sweep-Line Algorithmus zur Lösung des Problems.
3.1. SCAN-LINE PRINZIP
Algorithmus 24 Algorithmus für das Segment-Schnitt Problem in allgemeiner Lage
Initialisiere ES und SSS;
Sortiere die 2& Endpunkte aufsteigend nach -Koordinate;
Speichere resultierende Ereignisse in ;
solange ES nicht leer ES.NächstesEreignis();
falls ist Segment Anfang dann SSS.FügeEin(E.Segment);
VS SSS.Vorg(E.Segment);
TesteSchnittErzeugeEreignis(E,VS);
falls dann ES.FügeEin(E’);
'
NS SSS.Nachf(E.Segment);
TesteSchnittErzeugeEreignis(E,NS);
'
falls ist Segment Ende dann VS SSS.Vorg(E.Segment);
NS SSS.Nachf(E.Segment);
SSS.Entferne(E.Segment);
TesteSchnittErzeugeEreignis(VS,NS)
falls dann ES.FügeEin(E’);
'
'
falls ist Schnittpunkt dann Gib E.SegmentO und E.SegmentU aus;
SSS.Vertausche(E.SegmentO,E.SegmentU);
VS SSS.Vorg(E.SegmentU);
TesteSchnittErzeugeEreignis(E.SegmentU,VS);
falls dann ES.FügeEin(E’);
'
NS
SSS.Nachf(E.SegmentO);
TesteSchnittErzeugeEreignis(E.SegmentO,VS);
falls dann ES.FügeEin(E’);
'
'
'
73
KAPITEL 3. GEOMETRISCHE ALGORITHMEN
74
Die Funktion TesteSchnittErzeugeEreignis(Segment1,Segment2) berechnet die -Koordinate
des Schnittpunkts zweier Segmente und erzeugt ein entsprechendes Ereignis, falls sich die
Segmente schneiden. Ansonsten wird ein leeres Ereignis erzeugt. Ein Schnitt-Ereignis speichert die IDs der beiden betroffenen Segmente als SegmentU (Segment das vor dem Schnitt
unterhalb des zweiten war) und SegmentO (Segment das vor dem Schnitt oben war).
Analyse der Laufzeit
implementiert werden, wobei
Der Schnitt von Liniensegmenten kann in Zeit 5& & & die Anzahl der Segmente und die Anzahl der Schnittpunkte bezeichnet. Der benötigte
0
Speicherplatz ist 5& .
Das Sortieren der 2& Endpunkte nach ihrer -Koordinate erfolgt in Zeit 5& & . Insge
0
samt gibt es 2& verschiedene Ereignisse, von denen sich nie mehr als 4& Ereignisse
0
in ES befinden (weil es maximal 4& Schnittpunkte gibt). Der Zugriff auf ES ist also in
0
Zeit & %
& möglich. In SSS befinden sich nie mehr als & Elemente, weshalb
ein Zugriff auf diese Datenstruktur auch nur Zeit & benötigt.
Die Schleife wird maximal 2& mal durchlaufen und bei jedem Durchlauf werden nie
mehr als fünf Operationen auf ES und SSS durchgeführt. Der Speicherplatzbedarf ist im
schlimmsten Fall quadratisch, weil die Kreuzungen in der Ereignisstruktur abgelegt werden
und es quadratisch viele Kreuzungen geben kann.
3.2 Mehrdimensionale Bereichssuche
Bei der mehrdimensionalen Bereichssuche geht es darum, effizient die Menge all der Punkte
in einem mehrdimensionalen Raum zu finden, die innerhalb eines Suchbereichs liegen.
Die Menge aller Punkte ist dabei schon im Vorhinein bekannt und es geht darum, mehrere
Bereichsanfragen möglichst schnell zu beantworten.
Die zentrale Idee ist dabei, die Menge aller Punkte in einer Datenstruktur abzulegen, die das
Ausführen einer Bereichssabfrage beschleunigt. Dies zahlt sich aus, wenn auf der gleichen
Punktmenge mehrere Anfragen gestellt werden mit unterschiedlichen Suchbereichen. Die
Problemstellung ist die folgende:
Gegeben:
Gesucht:
Beispiele:
.
-dimensionaler rechteckiger Bereich
Raum.
Finde alle Punkte, die in liegen.
und & Punkte im -dimensionalen
: Aufzählen aller Elemente in einer Folge mit Schlüssel zwischen
und
3.2. MEHRDIMENSIONALE BEREICHSSUCHE
Wien
75
: Aufzählen aller Städte im Quadrat mit 100 km Seitenlänge und Mittelpunkt
: Datenbankabfragen, Aufzählen aller Personen, die
– Informatik studiert haben
– zwischen 25 und 39 Jahre alt sind
– mit Einkommen zwischen 30.000 EUR und 50.000 EUR
– kein Handy besitzen
Satz: Eindimensionale Bereichssuche kann mit 4& & Schritten für die Vorverarbeitung
und & Schritten für die Bereichssuche ausgeführt werde, wobei die Anzahl
der Punkte ist, die tatsächlich im Bereich liegen.
Beweis: Man legt die Folge in einem balancierten Suchbaum ab, z.B. in einem AVL-Baum.
Wir wollen für höhere Dimensionen nun ein ähnlich gutes Verfahren entwickeln. Eine naive
Lösung des Problems für höhere Dimensionen ist das Sequentielle Durchlaufen aller Punkte
wobei man jeweils testet, ob der gerade betrachtete Punkt im Bereich liegt. Dies bedeutet
einen Aufwand von 4& Schritten für jede Bereichsabfrage.
Liegt bei jeder Anfrage mindestens ein konstanter Anteil aller Punkte im Suchbereich ,
so ist dieses naive Verfahren asymptotisch optimal. Liefert aber jede Suchanfrage nur eine
kleine konstante Anzahl von Punkten als Ergebnis, so ist das Verfahren sehr ineffizient. Wir
wollen einen output sensitiven Algorithmus, bei dem die Laufzeit von der Anzahl der im
Suchbereich liegenden Punkte und somit von der Größe der Ausgabe abhängig ist.
Man könnte nun ein regelmäßiges Gitter über die Menge aller Punkte legen, um die Anfragen
schneller zu bearbeiten. Dies liefert gute Ergebnisse, wenn die Punkte gleichmäßigverteilt
sind. Sind die Punkte aber in bestimmten Bereichen des Raumes konzentriert, so gibt es viele
leere Gitterzellen während andere Gitterzellen sehr viele Punkte enthalten. Das folgende
Verfahren umgeht dieses Problem.
3.2.1 Zweidimensionale Bäume
Die Idee ist, den zweidimensionalen Raum aufzuteilen, ähnlich wie wir den eindimensionalen Raum mittels binäre Suchbäume aufgeteilt haben. Anders als bei den binären
Suchbäumen verwenden wir hier alternierend die - und -Koordinaten als Schlüssel.
Wir konstruieren also einen binären Baum, der eine Aufteilung der Ebene repräsentiert.
Dabei enthält jeder Knoten des Baumes einen der & Punkte. Verwenden wir an einem
Knoten der den Punkt repräsentiert die -Koordinate zur Bestimmung des linken und
rechten Teilbaums, so enthält der Linke Teilbaum von die Punkte die unterhalb von KAPITEL 3. GEOMETRISCHE ALGORITHMEN
76
liegen und der rechte Teilbaum die Knoten, die oberhalb von liegen. Verwenden wir aber
die -Koordinate, so enthält der linke Teilbaum von die Knoten links von und der rechte
Teilbaum die Knoten rechts von .
Sind die Teilbäume von
durch die -Koordinate bestimmt worden, so werden die
Teilbäume der Kinder von durch die -Koordinate bestimmt. Sind aber die Teilbäume
von durch die -Koordinate bestimmt worden, so werden die Teilbäume der Kinder von
durch die -Koordinate bestimmt. Es liegt also jeder Punkt der gegebenen Punktmenge auf
einem vertikalen oder horizontalen Segment, welches die Zerlegung definiert die an dem
entsprechenden Knoten im binären Baum vorgenommen wurde. Abbildung 3.3 zeigt ein
Beispiel für diese Aufteilung der Ebene.
A
D
C
B
B
A
E
G
E
D
G
F
C
F
Abbildung 3.3: Ein 2-D-Suchbaum und die dadurch definierte Zerlegung der Ebene
Die Suche in einem 2-D-Baum funktioniert ganz ähnlich wie die Suche in einem binären
Suchbaum. Allerdings muß man bei jedem durchlaufenen Knoten darauf achten, ob die
Teilbäume des Knotens durch die - oder -Koordinate bestimmt sind. Außerdem kann
es sein, dass die Suche nach den Punkten in einem Bereich in beiden Teilbäumen eines
Knotens durchgeführt werden muss.
Algorithmus 25 stellt die Bereichssuche in einem 2-D-Baum als Funktion dar. Der Aufruf
erfolgt in der Form + , wobei die Wurzel des Baums ist und wir
davon ausgehen, dass die Kinder der Wurzel durch die -Koordinate bestimmt sind.
Es stellt sich nun noch die Frage wie man einen solchen Baum effizient aufbauen kann, damit
eine schnelle Abarbeitung der Suchanfragen möglich ist. Es muss sich um einen balancierten Baum handeln, damit für kleine Ergebnismengen logarithmische Laufzeit in & erreicht
werden kann. Wir erreichen dies, indem wir stets am Median Punkt der aktuellen Folge
aufteilen. Dadurch wird garantiert, dass in den neu entstehenden Teilen jeweils gleich viele
3.2. MEHRDIMENSIONALE BEREICHSSUCHE
77
Algorithmus 25 Bereichssuche(Knoten p, Richtung d,Bereich D)
falls NULL dann falls vert dann (" (" 20 ;
coord " ;
rNeu horiz;
' sonst (" (" 0 ;
coord " ;
rNeu vert;
'
falls dann Ausgabe von ;
falls coord dann Bereichssuche(p.left,rNeu,D);
falls coord dann Bereichssuche(p.right,rNeu,D);
'
Punkte enthalten sind (wobei es zu einem maximalen Unterschied von einem Punkt kommen
kann). Wir gehen dazu wie folgt vor:
1. Wir sortieren alle Punkte sowohl nach der - als auch nach der -Koordinate. Dadurch
erhalten wir zwei sortierte Folgen und .
2. Wir teilen die Folge am Median Element und machen dieses zur Wurzel des Baums.
Es entstehen zwei neue Folgen
und 0 . Nun teilen wir auch die Folge
in zwei
Teile, so dass Folge
die selben Punkte wie enthält und 0 die selben Punkte wie
0 (natürlich jeweils nach -Koordinate sortiert).
.
3. Wir führen nun dasselbe rekursiv mit den Folgen und für ' durch,
wobei wir nun aber die -Folgen am Median Element aufteilen. Wir teilen immer
weiter abwechselnd am Median der - und -Folgen auf, bis die Folgen nur noch aus
einem Punkt bestehen. Diese Punkte sind dann die Blätter des Baums.
Am Beispiel der Punkte in der Punktmenge von Abbildung 3.3 geht der Aufbau des Baums
wie folgt von Statten. Sortiert man erst alle Punkte in den Folgen und , so erhält man:
:
:
Das Median-Element der
Folgen so aus:
: G
: F
G D
F G
A C
C E
B
A
F
B
E
D
-Folge ist fett gedruckt. Nach der ersten Unterteilung sehen die
C F
G C
0
: D
A
0 :
A B
B D
KAPITEL 3. GEOMETRISCHE ALGORITHMEN
78
Im nächsten Schritt wir dann an den fett gedruckten Median Elementen der -Folgen
aufgeteilt. Es ist klar, dass dieser Aufteilungsschritt in Zeit durchgeführt werden kann,
wenn die Länge der Folge vor der Teilung ist.
Algorithmus 26 zeigt eine Funktion, die einen balancierten 2-D-Baum in Zeit 5& & aufbaut. Es wird davon ausgegangen, dass das Feld die Menge aller Punkte sortiert nach
-Koordinate enthält und Feld die Menge aller Punkte sortiert nach -Koordinate.
Algorithmus 26 2D-Aufbau(l,r,knoten,Xdir)
Output: Konstruktion eines balancierten 2-D-Baums
1: falls dann 2:
0 ;
3:
falls Xdir == true dann )
+;
4:
punkt ' sonst 5:
)
+;
6:
punkt 7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
'
knoten.eintrag punkt;
falls Xdir false dann Partitioniere Feld +" ;
' sonst Partitioniere Feld +" ;
'
lsohn neuer linker Sohn( & #& );
rsohn neuer rechter
Sohn( & #& );
.
2D-Aufbau . lsohn !Xdir ;
+ rsohn !Xdir ;
2D-Aufbau '
Da das Aufteilen einer Folge in linearer Zeit möglich ist. ergibt sich für die Laufzeit des
Algorithmus die folgende Rekursionsformel:
4& &
&
Diese Formel hatten wir schon bei der Funktion Mergesort gesehen und wie schon dort
erläutert ist die Lösung der Gleichung 3(4& & . Der Aufwand für eine Bereichsabfrage ist
& , wobei die Anzahl der Punkte im Bereich
ist. Der Beweis wird hier wegen
seiner Komplexität nicht ausgeführt. Es ist aber deutlich, dass der Aufwand der Bereichsabfrage für kleine deutlich geringer ist als bei der naiven Methode, bei der einfach alle
Punkte geprüft werden.
3.2. MEHRDIMENSIONALE BEREICHSSUCHE
79
3.2.2 Höhere Dimensionen
Man kann die obigen Algorithmen recht einfach an höhere Dimensionen anpassen, indem
man reihum nach den Dimensionen aufteilt (beim Baumaufbau) b.z.w. entscheidet, welchen Teilbaum man betrachtet (bei der Bereichssuche). Man kann dann zeigen, dass der
Aufbau des entsprechenden Baumes Zeit 3. & & benötigt und die Bereichssuche Zeit
)
&
, wobei die Dimension ist.
Weiterführende Literatur
Die beiden hier aufgeführten Problemfelder (Scan-Line-Algorithmen und mehrdimensionale Bereichssuche) sind besonders gut beschrieben in dem Buch “Algorithms” von Robert
Sedgewick (s. Literaturliste (2)).
80
KAPITEL 3. GEOMETRISCHE ALGORITHMEN
Kapitel 4
Suchen in Texten
Dieser Abschnitt beschäftigt sich mit dem Suchen einer gegebenen Zeichenfolge (Muster
bzw. “pattern”) in einem Text . Dieses Problem (Pattern Matching, String Matching)
tritt häufig in der Textverarbeitung auf, etwa in Form der “Suchfunktion” in Texteditoren.
Auch in der Molekularbiologie wird häufig nach Mustern in einer gegebenen DNS-Sequenz
'
gesucht. Meist ist dabei die Länge
des Musters relativ klein im Vergleich zur Länge
des zu durchsuchenden Textes.
Wir betrachten in diesem Abschnitt das folgende Problem: Gegeben ist eine Text
'
Zeichenfolge der Länge
und eine Muster-Zeichenfolge (“Pattern”) der Länge
jeweils aus einem endlichen Alphabet . . Gesucht sind alle Vorkommen
von innerhalb
.
'
von , d.h. jeweils die Anfangs-Indizes (
.
). Wir nehmen
dabei an, dass
)
).
'
der Text und das Muster in jeweils einem Feld von
"#"#"
+ bzw. "#"#"
+ . Formal ist
das Problem folgendermaßen definiert.
Pattern Matching Problem
"#"#"
von Zeichen aus einem endlichen
Alphabet
Gegeben: eine Zeichenkette (Text)
.
'
und eine Zeichenkette (Muster/Pattern) "#"#" , ebenfalls mit $
.
""#"
Gesucht:
ein oder alle
Vorkommen. von ""#" in
, d.h. jene Indizes mit
.
.
'
'
, so dass #"#"" 2
) .
Wir setzen voraus:
'
.
In der Regel gilt:
Beispiel: Oxford English Dictionary mit
/ / / / /
"
Definitionen
Dynamischer Fall: Text und Pattern ändern sich häufig
(Wäre der Text immer der gleiche, würde es sinnvoll sein, eine Index-Datenstruktur
aufzubauen.)
81
KAPITEL 4. SUCHEN IN TEXTEN
82
4.1 Naives Verfahren
Die offensichtliche Methode besteht darin, das Muster der Reihe nach (von links nach rechts)
'
an jeden Teilstring des Textes mit Länge
anzulegen und dann zu prüfen, ob tatsächlich
Übereinstimmung an der momentanen Stelle vorliegt.
Algorithmus 27 Naive-Search
). ). '
Input: Text " " + und Muster ""
+
Output: Ausgabe aller Vorkommen von in
/
/* Position vor der Anlegestelle von */
1: '
2: solange.
3:
)
)
'
.
+ + 4:
solange .
5:
6:
7:
8:
9:
10:
11:
'
'
.
falls dann .
Ausgabe: “ an Stelle im Text gefunden”;
'
.
'
Das Programm verwendet einen Textzeiger , der jeweils auf den Index vor der Anlegestelle
des Musters zeigt. Stimmen die Zeichen an der Stelle überein (“match”) wird
hochgesetzt und das nächste Zeichen wird verglichen. Andernfalls (“mismatch”) wird das
'
Muster um eine Stelle weiter rechts angesetzt. Wurde
-Mal hintereinander hochgezählt,
wurde das Muster gefunden.
'
.
Analyse der Laufzeit: Das Muster wird genau
Mal an den Text angelegt.
'
Im schlimmsten Fall müssen bei jedem solchen Anlegen 3( Vergleiche durchgeführt
werden (was in der Praxis nur selten vorkommt). Insgesamt ergibt dies eine Worst-Case
'
Laufzeit von
. Das Problem des naiven Algorithmus ist es, dass die Information,
die während der Vergleiche erhalten wird, nicht benutzt wird, sondern vergessen wird. Man
nennt dies auch einen Algorithmus “ohne Gedächtnis”.
Die folgenden beiden Abschnitte behandeln Algorithmen mit Gedächtnis.
4.2. VERFAHREN VON KNUTH-MORRIS-PRATT
83
4.2 Verfahren von Knuth-Morris-Pratt
Die Idee hierbei ist es, die Informationen, die wir jeweils bis zu einem “Mismatch” über
den Text an den jeweiligen Stellen erhalten haben, auszunutzen. Dabei wird versucht, das
Muster nach jedem Mismatch um mehr als nur eine Position nach rechts zu rücken. Ziel ist
es, zu verhindern, dass die Vergleiche noch einmal über bereits bekannte Zeichen geführt
werden müssen.
Beispiel:
Position:
123456789...
Text 1:
NANANA DAS IST
Muster (erstes Anlegen):
ANANAS
Muster (zweites Anlegen):
ANANAS
Beim ersten Anlegen des Musters “ANANAS” an den Text “NANANA DAS IST” erfolgt
sogleich ein Mismatch. Jedoch beim zweiten Anlegen (an Position 2) erfolgen fünf Übereinstimmungen bevor der erste Mismatch an Position 7 auftaucht. Wegen der Übereinstimmungen kennen wir bereits die fünf Zeichen des Textes vor der aktuellen Position, d.h. diese
brauchen wir nicht noch einmal anzuschauen. Doch wie weit müssen wir das Muster nach
rechts schieben? Es ist folgendes zu beachten:
(1) Nach der Verschiebung muss garantiert sein, dass links von der aktuellen Position im
Muster nur Zeichen stehen, die alle mit dem jeweiligen Zeichen im Text übereinstimmen.
(2) Dabei müssen wir beachten, dass wir das Muster keinesfalls zu weit nach rechts schieben dürfen.
Die erste Eigenschaft (1) ist in unserem Beispiel bei Position 4 im Text sowie bei Position 6
im Text erfüllt. Würden wir das Muster auf Anlegeposition 6 legen, dann könnte uns eventuell ein Vorkommen des Musters im Text (Pattern Matching) verloren gehen. Es wäre also
in diesem Fall richtig, das Muster um 2 Positionen nach rechts zu schieben.
Wir nehmen an:
Die letzten gelesenen Zeichen im Text stimmen
mit den ersten Zeichen des Musters
).
)
+ bis %+ .
überein. Es sei das Teilmuster von Das gerade gelesene -te Zeichen im Text ist verschieden vom
Muster.
.
-ten Zeichen im
Wir bestimmen vom Anfangsstück des Musters mit Länge (dem gematchten Teil des
Musters) ein Endstück maximaler Länge , das ebenfalls Anfangsstück des Musters ist.
.
Das Muster kann dann um Stellen nach rechts geschoben werden. Position ist dann im Muster die erste Stelle, die man mit dem -ten Zeichen im Text als nächstes
vergleichen muss.
KAPITEL 4. SUCHEN IN TEXTEN
84
Beispiel:
ababababca
1 a#
2 ab#
3 aba#
4 abab#
5 ababa#
6 ababab#
7 abababa#
8 abababab#
9 ababababc#
10 ababababca
0
0
1
2
3
4
5
6
0
1
Shift nach rechts ( 1
2
2
2
2
2
2
2
9
9 (kompletter Match)
)
Algorithmus
28 zeigt eine Realisierung der Knuth-Morris-Pratt Idee. Die Werte von für .
'
"#"#" werden dabei. vom Algorithmus InitNext( ) (s. Algorithmus 29) vorausberechnet
) '
""
+ abgespeichert. Im Wesentlichen geschieht die Berechnung von
und in dem Feld InitNext( ) sehr ähnlich wie die eigentliche Idee des Knuth-Morris-Pratt Algorithmus: das
Muster wird mit sich selbst verglichen.
Algorithmus 28 Knuth-Morris-Pratt
). ). '
" " + und Muster ""
+
Input: Text Output: Ausgabe aller Vorkommen von in
1: InitNext( ); /* Initialisiere das Feld next[] */
/
2: ; .
3: für #""#" . )
)
/
4:
solange
+ ) + 5:
+;
6:
7:
8:
9:
10:
11:
12:
13:
'
falls
)
'
falls
.
.
+ )
+
;
'
dann gefunden: Ausgabe;
dann
)
+
;
'
'
Korrektheit: Die Korrektheit des Knuth-Morris-Pratt Algorithmus ergibt sich aus der obigen
Argumentation und der korrekten Berechnung des Feldes next[] in InitNext( ), die wir
näher diskutieren
wollen.
.
)
)
Fall 1: + + in Zeile (3). In diesem Fall ist das Endstück des Musters , das
)
Anfangsstück von
ist, das bisherige (von ) ) vereinigt mit %+ .
.
)
)
Fall 2: + %+ in Zeile (3). Dann geht man am besten genauso vor wie im
4.3. VERFAHREN VON BOYER-MOORE
85
Algorithmus 29 Prozedur
InitNext ). '
Input: Muster ""
+
Output: .Initialisiert das Feld next[] für Muster /
/
)
1: + ; ;
'
2: für "#"#" . )
)
/
3:
solange .
+ %+ )
+;
4:
5:
6:
7:
8:
9:
10:
'
)
falls (
'
)
%+ .
.
+ )
+
dann
;
;
'
Knuth-Morris-Pratt Algorithmus, nämlich man vergleicht der Reihe nach
) )
+ + , . . . , bis man bei 0 angekommen ist.
)
+
mit )
+
,
Mal und der Index
Analyse der Laufzeit: Im Algorithmus wird der Index genau
maximal Mal erhöht. Der Index kann allerdings höchstens so oft zurückgesetzt werden,
wie er erhöht wurde, also insgesamt höchstens
Mal. Das gleiche Argument gilt auch
für Algorithmus InitNext( ). Dort kann höchstens so oft zurückgesetzt werden, wie es
'
insgesamt erhöht wurde, und das ist maximal . Wir erhalten also als Gesamtlaufzeit des
'
Knuth-Morris-Pratt Algorithmus 3( .
Bemerkung: Bei Knuth-Morris-Pratt wird der Text ausschließlich sequentiell durchlaufen,
da der Index niemals zurückgesetzt wird. Dies ist in manchen Anwendungen von Vorteil,
z.B., wenn der Text auf externen Medien gespeichert ist, die nur sequentiell in einer Richtung
durchlaufen werden können (z.B. Bänder).
4.3 Verfahren von Boyer-Moore
Die Grundidee von Boyer-Moore ist die folgende: Eine Verschiebung des Musters bei Mismatch ist hier davon abhängig, welches Zeichen im Text für den Mismatch verantwortlich
ist und wo dieses im Muster auftritt. Dazu wird das Muster von links nach rechts angelegt,
aber die Zeichen werden von rechts nach links gelesen. Die Motivation hierfür liegt in der
Beobachtung, dass die meisten Textzeichen im Muster nicht auftauchen (da die Musterlänge
im Verhältnis zum Alphabet bzw. Text sehr klein ist), und so das Muster sehr oft um die
ganze Länge verschoben werden kann.
Wenn man allerdings nur diese Idee (Last-Verschiebung) implementiert, dann kann es im
'
schlimmsten Fall zu einer Laufzeit von
kommen. Deswegen verwendet man noch
KAPITEL 4. SUCHEN IN TEXTEN
86
ein zweites Verschiebungskriterium, das so weit verschiebt, bis die letzten gematchten
Zeichen im Text mit den ersten Zeichen in übereinstimmen (Suffix-Verschiebung).
Es gibt also zwei verschiedene Verschiebungskriterien, die jeweils unabhängig voneinander berechnet werden und von denen dann die größere Verschiebung durchgeführt wird.
Algorithmus Boyer-Moore( ) (s. Algorithmus 30) gibt den Algorithmus an. Dabei wird
zunächst die Berechnung der Verschiebungstabellen last[] und suffix[] . aufgerufen.
Die Ersetzung von Zeile (11) und (13) durch den Befehl ergibt einen naiven
'
.
Algorithmus mit Laufzeit 3(
Algorithmus 30 Boyer-Moore
). ). '
Input: Text " " + und Muster ""
+
Output: Ausgabe aller Vorkommen von in
1: InitLast ;
2: InitSuffix ;
/
3: ; /* Position vor der Anlegestelle von */
'
4: solange
'
5:
;
)
)
/
6:
solange . + + 7:
;
8:
9:
10:
11:
12:
13:
14:
15:
'
falls
/
dann gefunden: Ausgabe;
.
)'
+;
sonst '
'
)
+ )
)
++
;
'
4.3.1 Berechnung von last[]
Wir betrachten die Situation, . dass die erste Nichtübereinstimmung bei
auftrat,
)
)
'
+ für ein . Das Zeichen an dieser Stelle im Text sei
d.h. + . In diesem Fall verschieben wir so weit nach rechts, bis im Text über dem rechtesten
Zeichen gleich in ist. Falls nicht in vorkommt, dann kann bis hinter die Position
verschoben werden.
.
'
)
Lemma: Sei der größte Index aus
, für den gilt
/
zu erhöhen.
vorhanden, sonst . Dann ist es korrekt, um Beweis:
Fall 1:
/
. Dann sind alle anderen Zeichen links von ungleich
)
+
+
)
+
, falls
4.3. VERFAHREN VON BOYER-MOORE
87
Fall 2: . Das rechteste Erscheinen des Mismatch-Zeichens in liegt links von in
/
. In diesem Fall ist eine Verschiebung um
nach rechts korrekt.
/
Fall 3: . In diesem Fall ist und es wird keine Verschiebung (wegen last)
durchgeführt.
Der Algorithmus InitLast( ) (s. Algorithmus 31) berechnet jeweils die Position des
rechtesten Vorkommens für alle Zeichen im Alphabet .
Algorithmus 31 Prozedur InitLast 1: für alle aus dem Alphabet
/
2:
3:
4:
5:
6:
'
für
'
)
.
)
#"#"#" ++ '
4.3.2 Berechnung von suffix[]
Verschiebe soweit nach rechts, bis die bisher untersuchten gematchten Zeichen im Text mit
den ersten Zeichen im Muster übereinstimmen. Falls keinerlei Übereinstimmung herrscht,
dann kann das Muster bis ganz hinter die Position geschoben werden.
Wir betrachten
wieder die Situation, dass der erste Mismatch bei
aufgetaucht ist,
)
)
d.h. + +.
Wir definieren den Suffix als die kleinste Verschiebung , so dass
)
.
)'
+
)
..
.
+
..
.
)'
)
.
+
+
) . +.
Eine alternative
Sichtweise ist die folgende:
.
)
)'
+#"#"#" + Suffix von ist, wobei + ""#"
'
)
+
,
/
'
, so dass
.
Zur Berechnung unterscheiden wir zwei Fälle.
1. Fall: Der Suffix befindet sich nur am Anfang von .
)
'
)'
)'
Dann gilt
+ +
, wobei
+ die Länge des längsten Präfixes von ist,
das echter Suffix von ist.
KAPITEL 4. SUCHEN IN TEXTEN
88
2. Fall: Der Suffix befindet sich nicht nur am Anfang von .
)
In diesem Fall kann der Suffix im invertierten Muster Hilfe von
Ab. next[] (s.
)
)
'
mit
schnitt 4.2) ausgerechnet werden. Es gilt
+
+ für alle
und
'
)
%+ . Intuitiv suchen wir alle diejenigen Längen , deren Endungen mit dem ge'
)
+ .
suchten Präfix übereinstimmen. Und das sind genau diejenigen, für die gilt Der Suffix kann berechnet werden als
)
suffix +
'
)'
+' )
next +$2
.
'
und
'
)
next +'
Der Algorithmus InitSuffix( ) (s. Algorithmus 32) bestimmt zunächst im gegebenen Muster
den Suffix, der Fall 1 entspricht (d.h. am Anfang des Musters). Danach bestimmt er im
)
invertierten Muster den Suffix, der Fall 2 entspricht. Dazu berechnet er im invertierten
Muster das Feld [].
Algorithmus 32 Prozedur InitSuffix )
1: Berechne InitNext + )
)
2: Berechne InitNext +
'
/
3: für #"#"#" )
'
)'
4:
+ +;
5:
6:
7:
8:
9:
10:
11:
'
für
.
'
falls #"#"#" )
) + '
+
)
+
;
) )
%+
;
%+
dann
'
'
4.3.3 Analyse von Boyer-Moore
Man kann zeigen, dass die Laufzeit des Boyer-Moore Verfahrens in der Ordnung von
'
3(
liegt. Dies gilt aber nur, wenn beide Verschiebeverfahren verwendet werden.
Die Verschiebung mittels last ist sehr einfach, die Suffix-Verschiebung etwas aufwendiger zu
programmieren. Aus diesem Grund wird der Algorithmus in der Praxis oft ohne die SuffixVerschiebung implementiert. Das Verfahren ist in der Praxis in beiden Fällen sehr schnell.
4.4. TRIES
89
4.4 Tries
Für verschiedene Aufgaben in der Textverarbeitung ist der Trie eine wichtige Datenstruktur, die wir deshalb genauer behandeln wollen. Der Name Trie“ kommt vom englischen
”
retrieve“ (wiederauffinden).
”
4.4.1 Radix Trie
Grundsätzlich ist ein Radix Trie ein binärer Baum, bei dem jedoch nur die Blätter Datensätze
beinhalten, die nach einem Schlüssel geordnet sind.
)/
).
+ und + , die entweder auf NachfolgeJeder innere Knoten besitzt zwei Zeiger knoten – den linken bzw. rechten Teilbaum – verweisen oder den Wert NULL besitzen. Ein
Blattknoten schließlich, beinhaltet den Schlüssel und eventuelle Anwenderdaten.
0 #"#"#" einer fixen, vorgegebenen
Der Schlüssel ist hier immer ein binärer String
Länge . Andere Datentypen müssen gegebenenfalls in eine solche Binärrepräsentation fixer
Länge umgewandelt werden.
Für den Radix Trie gilt Folgendes:
In einem inneren Knoten sind nie beide Verweise gleich NULL.
Hat ein innerer Knoten nur einen Nachfolger, so ist dies immer ein weiterer innerer
Knoten, keinesfalls ein Blatt. Diese und die vorige Bedingung stellen sicher, dass der
Radix Trie keine unnötigen“ inneren Knoten besitzt.
”
.
Jeder Radix Trie hat immer maximal Ebenen und kann Datensätze aufnehmen.
Er kann somit nicht entarten, wie dies etwa beim einfachen binären Suchbaum möglich
ist.
wurzel
wurzel
1
0
E
I
N
U
V
00101
01001
01110
10101
10110
0
1
1
0
V
1
0
0
E
E
0
1
0
1
I
N
I
N
1
0
1
U
V
Abbildung 4.1: Ein Radix Trie vor und nach dem Einfügen von ‘U’.
KAPITEL 4. SUCHEN IN TEXTEN
90
.
Für jeden inneren Knoten der Ebene #"#"#" ' des Trie gilt, dass in seinem linken
Teilbaum nur Datensätze gespeichert sind, deren (das -te Bit des Schlüssels). gleich
0 ist. Umgekehrt beinhaltet der rechte Teilbaum nur jene Datensätze mit .
Der leere Baum wird lediglich durch eine Verweis-Variable tiert.
repräsen-
Abbildung 4.1 zeigt einen Radix Trie vor und nach dem Einfügen von ‘U’ (=10101). Im
Folgenden betrachten wir die Such-, Einfüge- und Löschoperationen genauer.
Einfügen
Randbedingung: Wir gehen davon aus, dass zwei Datensätze niemals gleiche Schlüssel besitzen. Treten in einer Anwendung dennoch Datensätze mit gleichen Schlüsseln auf, so kann
dies beispielsweise durch eine zusätzliche äußere Verkettung gelöst werden, d.h. die Blätter
sind jeweils lineare Listen, die alle Datensätze mit identen Schlüsseln beinhalten.
Algorithmus 33 Einfügen ( , , )
1: falls NULL dann 2:
= Erzeuge Blattknoten für Datensatz mit Schlüssel ;
3: ' sonst falls ist innerer Knoten
. dann )
+ , , );
4:
Einfügen ( " 5: ' sonst 6:
/* ist ein Blatt; ein neuer innerer Knoten wird eingefügt: */
;
7:
8:
= neuer innerer Knoten;
)
9:
;
" . " + )
10:
; .
" " + )
11:
Einfügen ( " + , , );
12:
'
Diese rekursive Prozedur wird für einen gegebenen Schlüssel
aufgerufen:
und Trie wurzel wie folgt
Einfügen ( , , 1);
Stößt die Prozedur auf einen Blattknoten, so werden in den weiteren Schritten so lange
innere Knoten erzeugt, bis sich dieser Datensatz und der neu einzufügende in einem Bit
unterscheiden. Im Worst-Case wird in einem anfangs aus einem Datensatz bestehender
Trie ein zweiter Datensatz eingefügt, der sich vom ersten nur im letzten Bit unterscheidet.
Vergleiche auch Abbildung 4.1.
Aufwand: Da der Baum maximal Tiefe .
hat, ist der Aufwand des Einfügens
.
4.4. TRIES
91
Suchen
Algorithmus 34 Suchen ( , , )
1: falls NULL dann 2:
Datensatz nicht enthalten;
3: ' sonst falls ist innerer Knoten
dann .
)
4:
Suchen ( " + , , );
5: ' sonst 6:
/* ist ein Blatt: */
#"#"" dann
7:
falls " "#"#" " 8:
Datensatz gefunden;
' sonst 9:
10:
Datensatz nicht enthalten;
'
11:
12:
'
Aufwand: Wiederum ist durch die beschränkte Tiefe des Baumes der Aufwand
Prozedur kommt sogar mit einem einzigen Schlüsselvergleich aus.
Entfernen
Algorithmus 35 Entfernen ( , , )
1: falls NULL dann 2:
Datensatz nicht in Trie;
3: ' sonst falls ist innerer Knoten. dann )
4:
Entfernen ( " , , );
+ )/
).
+ . + ist Blatt dann 5:
falls " && " )
" + ; /* . Knoten löschen */
6:
)
)/
' sonst falls " + + ist Blatt dann
7:
&& " )/
" + ; /* Knoten löschen */
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
'
sonst /* ist Blatt: */
falls " "#"#" " #"#"" dann
/* Zu löschenden Datensatz gefunden */
; /* Datensatz löschen */
' sonst Datensatz nicht enthalten;
'
'
'
. Die
KAPITEL 4. SUCHEN IN TEXTEN
92
Diese Prozedur entfernt auch alle inneren Knoten, die durch das Entfernen eines Unterknotens nur mehr ein einzelnes Blatt haben würden. Dadurch ist auch sichergestellt, dass es
keine inneren Knoten ohne Nachfolger geben kann.
Aufwand:
.
Eine Eigenschaft aller Tries, die sie von den meisten anderen Baumstrukturen unterscheidet
ist, dass es für eine gegebene Menge von Datensätzen immer nur genau einen bestimmten
Trie gibt, unabhängig davon, in welcher Reihenfolge die Datensätze eingefügt wurden.
In vielen Anwendungen ist ein Nachteil des Radix Trie jedoch, dass die maximale Tiefe
durch eine große Schlüssellänge sehr hoch werden kann. Beispiel: Strings, die aus maximal
. / /
/ /
200 Zeichen kodiert durch je 8 Bit bestehen, haben eine Schlüssellänge .
%
In diesem Fall ist es dann sinnvoll, bei jedem inneren Knoten mehr als nur zwei mögliche
Nachfolger zu verwalten. Beispielsweise kann ein innerer Knoten so viele Nachfolger besitzen, wie es unterschiedliche Zeichen für jede Position eines String-Schlüssels gibt. Dieses
Vorgehen führt uns weiter zum Indexed Trie.
4.4.2 Indexed Trie
Der Indexed Trie dient primär zur Speicherung von Worten, zum Beispiel für eine Rechtschreibprüfung. Die Aufgabe besteht beim Suchen lediglich darin, festzustellen, ob ein Wort
gültig“, d.h. in der Datenstruktur enthalten ist oder nicht.
”
Wir gehen hier als Beispiel davon aus, dass jedes Wort 0 "#"#" aus beliebig
vielen Zeichen des Alphabets ‘a’, ‘b’,. . . ,‘z’ ' besteht.
Im Unterschied zum Radix Trie ist nun jeder Knoten ein Feld der Größe ! ! , das durch
die Elemente des Alphabets indiziert ist und folgende beiden Variablen für jedes beinhaltet:
einen Verweis next auf einen Nachfolgeknoten und
eine Boolsche Variable end, die angibt, ob ein gültiges Wort mit dem entsprechenden
Buchstaben (= Index) an dem Knoten endet.
4.4. TRIES
93
wurzel
(1)
a b c d
end: T F F F
next:
...
...
...
z
F
a b c d
(2)
end: F T F F
next:
...
...
...
z
F
a b c d
(3)
end: T F T F
next:
...
...
...
z
F
a b c d
(4)
end: F F F F
next:
...
...
...
z
F
a b c d
(5)
end: F F F F
next:
...
...
...
z
F
a b c d
(7)
end: T F F F
next:
...
...
...
z
F
a b c d
(8)
end: T F F F
next:
...
...
...
z
F
a b c d
(6)
end: T F F T
next:
...
...
...
z
F
Abbildung 4.2: Ein Indexed Trie der die Worte a, ab, abda, da, dc, dcda, dda, ddd '
beinhaltet.
Einfügen
Da nun keine speziellen Blattknoten mehr verwendet werden, muss beim Einfügen eines
Wortes 0 "#"#" $ für jedes Zeichen ein Knoten erzeugt werden, falls dieser
nicht bereits existiert. Abbildung 4.2 zeigt ein Beispiel eines Indexed Trie.
Algorithmus 36 Einfügen ( , 1: falls NULL dann = Erzeuge Knoten;
2:
3:
für alle )
+ " 4:
;
)
;
+ " 5:
6:
7:
8:
9:
10:
11:
12:
'
'
falls
)
&
dann
;
+" sonst )
Einfügen ( +" , '
, )
, .
);
'
Die maximale Tiefe des Trie ist immer gleich der Länge des längsten darin enthaltenen
Wortes.
KAPITEL 4. SUCHEN IN TEXTEN
94
Aufwand: 3(4& !
!
Suchen
Algorithmus 37 Suchen ( , , )
1: falls NULL dann 2:
Wort nicht enthalten;
3:
4:
5:
6:
7:
8:
9:
10:
'
falls & dann )
Suchen ( + " , , )
' sonst falls + " Wort gefunden;
' sonst Wort nicht enthalten;
.
);
dann
'
5& Aufwand:
(& . . . Länge des zu suchenden Wortes)
Entfernen
Algorithmus 38 Entfernen ( , , )
1: falls NULL dann 2:
Wort nicht enthalten; Abbruch
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
'
falls & dann .
)
Entfernen ( +" , , );
)
' sonst falls + " dann
/* Wort gefunden */
)
;
+" ' sonst Wort nicht enthalten; Abbruch
'
falls
2
)
5&
!
)
+ " +"
; /* Knoten entfernen */
'
Aufwand:
!
dann
4.4. TRIES
95
Weitere Anwendungen des Indexed Trie
Suche in einem (dynamischen) Text gleichzeitig nach mehreren Worten bzw. unterschiedlichen Wortformen. Fortgeschrittenere Varianten des Trie können auch Wortfamilien, wie sie durch reguläre Ausdrücke gegeben sind, repräsentieren. Hier besteht
auch ein gewisser Zusammenhang zu endlichen Automaten.
#"#"#" Ist ein fixer Text gegeben, für den sehr oft überprüft werden soll, ob
ein Wort darin vorkommt, kann ein Trie aus allen möglichen Suffixen von , d.h. aus
"#"#" ,
0 #"#"#" , . . . ,
' aufgebaut werden. Das Überprüfen, ob ein
Wort der Länge & Substring von ist, kann dann in 5& durchgeführt werden. Die
Variablen end sind in diesem Fall nicht notwendig. Ein derartiger Trie wird auch als
Suffix-Tree bezeichnet.
4.4.3 Linked Trie
In den Knoten eines Indexed Trie gibt es oft sehr viele Einträge mit NULL
False. Wir nennen solche Einträge leere“ Einträge. Dadurch
”
bedingt ist die Datenstruktur nicht sehr kompakt.
Beim Linked Trie wird das Feld für jeden Knoten durch eine lineare
Liste ersetzt, die nur
next- und end-Variablen für jene beinhaltet, für die NULL ! ! False.
D.h. alle leeren Einträge werden nicht gespeichert. Dafür muss nun zusätzlich immer das
jeweilige vermerkt werden.
Aufwand: Bei größerem ! ! und dichten Tries kann durch das notwendige Iterieren durch
die linearen Listen der Aufwand für das Einfügen, Suchen und Entfernen deutlich höher als
beim Indexed Trie sein: 5& ! ! .
4.4.4 Suffix Compression
Haben mehrere Worte genau die gleiche Endung (Suffix), so kann man einen Indexed oder
Linked Trie durch eine Suffix Compression kompaktieren. Dabei speichert man den Subtrie
für die gemeinsame Endung nur mehr einmal und verweist darauf mehrfach. Abbildung 4.3
zeigt den Trie aus Abbildung 4.2 nach einer erfolgten Suffix Compression. Die idente
Endung da der Worte abda und dcda wird nunmehr durch einen gemeinsamen Subtrie
repräsentiert. Der Trie hat damit keine Baumstruktur mehr.
Achtung: Führt man Suffix Compression durch, können die betroffenen Worte nicht mehr
individuell verändert werden. Würde man im gezeigten Beispiel das Wort abdd einfügen,
so würde automatisch auch das Wort dcdd im Trie enthalten sein. Suffix Compression ist
daher ein Schritt, der erst durchgeführt werden sollte, wenn im Trie keine Änderungen mehr
gemacht werden.
KAPITEL 4. SUCHEN IN TEXTEN
96
wurzel
(1)
a b c d
end: T F F F
next:
a b c d
(2)
end: F T F F
next:
...
...
...
z
F
...
...
...
z
F
a b c d
(3)
end: T F T F
next:
...
...
...
z
F
(5)
a b c d
end: F F F F
next:
...
...
...
z
F
a b c d
(8)
end: T F F F
next:
...
...
...
z
F
a b c d
(6)
end: T F F T
next:
...
...
...
z
F
Abbildung 4.3: Der Indexed Trie für die Worte a, ab, abda, da, dc, dcda, dda, ddd '
nach einer Suffix Compression.
Ein Algorithmus zur Suffix Compression auf einem Indexed oder Linked Trie sieht abstrakt
wie folgt aus:
Algorithmus 39 Suffix Compression ()
1: solange zwei Knoten und existieren, die gleich sind
(sowohl alle end-Flags als auch alle next-Verweise!) 2:
Lösche Knoten ;
3:
Ersetze im gesamten Trie alle Verweise auf durch Verweise auf ;
4:
'
4.4.5 Packed Trie
Für den Indexed Trie gibt es noch eine weitere Möglichkeit der Komprimierung. Die
Umwandlung eines Indexed Trie in einen Packed Trie ist ein Schritt, der im Allgemeinen
eine sehr große Menge an Speicher befreit. Fast alle Felder, die im Indexed Trie leer sind
(end==False
next==NULL), werden eliminiert und es muss dennoch nicht auf die
Geschwindigkeitsvorteile des Index-basierten Zugriffs verzichtet werden.
Der Packed Trie wird nicht mehr durch eine Menge von Feldern mit Verweisen dargestellt,
sondern durch ein einziges, größeres Feld , in welchem alle Knoten teilweise ineinander
verzahnt gespeichert sind. ist mit ganzen Zahlen indiziert, und jedes Feldelement besteht
4.4. TRIES
Index:
(2)
...
(5)
2
#$
...
7
...
...
1
(3)
(8)
a b c d e ... z
(1)
(6)
97
...
...
6565 6565
...
!!
""
:9:9
--. &%&% )*+, *) ''(( 87870/0/ 8787 34 >=<;2121 >=<;
4
...
6
...
...
Knoten des Indexed Tries
Index:
1
2
3 4 5 6 7
...
10
...
8 9 10 11 12 13 14
...
Packed Trie:
Index: 1 2 3
c: a a c
end: T T T
next: 0 7 4
4 5 6 7
8 9 10
d d a d b d a
F F T F T T T
6
Index der Wurzel: 2
1 0 10 4 0 0
Abbildung 4.4: Der Indexed Trie nach der Suffix Compression wird weiter in einen Packed
Trie umgewandelt.
aus folgenden drei Komponenten:
c – das Zeichen des Alphabets (= der Index eines Knotenelements im Indexed Trie)
end – die Wortende-Markierung
next – der Verweis auf den Beginn des Nachfolgeknotens im Feld (= der Index des
ersten Buchstaben des Nachfolgeknotens). Anstatt NULL“ wird hier z.B. der Wert 0
”
verwendet, um anzuzeigen, dass es keinen Nachfolger gibt.
Beim Aufbau
des Packed Trie ist wichtig, dass Elemente, für die im Indexed Trie end False
oder next NULL gilt, niemals kollidieren, da sonst Information verloren gehen würde.
Weiters dürfen niemals zwei Knoten den gleichen Anfangsindex haben, da sonst nicht
zwischen ihren Elementen unterschieden werden kann.
Zum Packed Trie gehört auch noch die (globale) Variable wurzel, die den Anfangsindex
des Wurzelknotens angibt. Der Wurzelknoten muss nämlich nicht unbedingt bei Index 1
beginnen.
Abbildung 4.4 zeigt als Beispiel, wie unser Indexed Trie aus Abbildung 4.3 weiter in einen
Packed Trie kodiert werden kann. Die leeren Felder am Ende der zusammengefügten Knoten
brauchen dabei – wie in der Abbildung ersichtlich – nicht gespeichert zu werden, sofern
KAPITEL 4. SUCHEN IN TEXTEN
98
bei der Suche eine entsprechende Bereichsüberprüfung der Indizes durchgeführt wird. In
diesem Beispiel weist der Packed Trie keine einzige Lücke auf. Das ist aber nicht immer so.
Eine große Menge von Knoten eines Indexed Trie tatsächlich optimal zu komprimieren,
d.h. in ein Feld minimaler Länge mit möglichst wenigen Lücken, ist im Allgemeinen ein
schwieriges Problem, das praktisch nicht exakt gelöst werden kann. Man erreicht jedoch
mit relativ einfachen greedy-Heuristiken zumeist sehr gute Resultate, da in der Praxis viele
Knoten sehr dünn besetzt sind, oft sogar nur ein einziges belegtes Element besitzen und so
als Lückenfüller“ verwendet werden können.
”
Eine einfache Heuristik ist beispielsweise, die Knoten nach ihrer Anzahl belegter Elemente
absteigend zu sortieren und dann in dieser Reihenfolge einen Knoten nach dem anderen an
der ersten geeigneten Stelle im Packed Trie einzuordnen. Das Auffinden dieser Stelle ist eine
Pattern-Matching Aufgabe, die mit Varianten der Algorithmen zur Textsuche (z.B. BoyerMoore) effizient gelöst werden kann.
4.4.6 Suchen
Abschließend hier noch der Algorithmus zur Suche eines Wortes Algorithmus
40 Suchen ( , , wurzel)
.
1: ;
2:
;
3: wiederhole
.
$ ;
4:
5:
falls $ dann 6:
/* Index außerhalb von : */
7:
Wort nicht enthalten; Abbruch;
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
'
)
+ " dann falls
Wort nicht enthalten; Abbruch;
'
)
falls & +" #&
Wort gefunden; Abbruch;
'
)
. +"
;
;
/
bis &! ! ;
Wort nicht enthalten;
Aufwand:
5& dann
im Packed Trie :
4.4. TRIES
99
Weiterführende Literatur
Suchen in Texten wird z.B. in den Büchern von Sedgewick und Cormen, Leiserson und
Rivest (s. Literaturliste (2) und (3)) ausführlich beschrieben.
100
KAPITEL 4. SUCHEN IN TEXTEN
Kapitel 5
Randomisierte Algorithmen
Unter einem Randomisierten Algorithmus (bzw. Probabilistischen Algorithmus) verstehen wir einen deterministischen Algorithmus, der als zusätzliche Elementaroperationen
Zufallsexperimente durchführen kann.
Man unterscheidet grundsätzlich zwei Klassen von randomisierten Algorithmen, nämlich
Las-Vegas-Verfahren, die stets ein korrektes Ergebnis berechnen und Monte-CarloVerfahren, die ein Ergebnis berechnen, das mit einer gewissen Fehlerwahrscheinlichkeit
behaftet ist.
Ein wesentlicher Unterschied zwischen randomisierten und deterministischen Algorithmen
besteht in der Analyse. Während man bei deterministischen Algorithmen in erster Linie
die worst-case Komplexität betrachtet, analysiert man bei randomisierten Algorithmen
die sogenannte expected-case Komplexität, die das Verhalten des Algorithmus über alle
möglichen Ausgänge der durchgeführten Zufallsexperimente mittelt.
Tabelle 5.1 zeigt weitere Unterschiede. Während der Aufbau deterministischer Algorithmen sehr komplex sein kann, ist der Aufbau randomisierter Algorithmen meist sehr einfach.
Durch die expected-case Analyse ist jedoch die Analyse relativ komplex im Vergleich zu deterministischen Algorithmen. Die Laufzeit randomisierter Algorithmen kann garantiert sein,
oft hängt diese jedoch vom Ausgang der Zufallsexperimente ab.
Algorithmus
Zufallsexperimente
Aufbau
Analyse
Laufzeit
Korrektheit
Deterministisch
nein
komplex
einfach
garantiert
garantiert
Randomisiert
ja
einfach
komplex
variabel
garantiert/probabilistisch
Tabelle 5.1: Unterschiede zwischen deterministischen und randomisierten Algorithmen
101
KAPITEL 5. RANDOMISIERTE ALGORITHMEN
102
5.1 Randomisierter Primzahltest
Im folgenden wenden wir ein Monte-Carlo Verfahren, nämlich den Algorithmus von
Miller-Rabin, auf das Primzahlproblem an.
Primzahltest
Gegeben: Ganze Zahl & (üblicherweise sehr groß)
Gesucht: Antwort auf die Frage: Ist & eine Primzahl?
Dieses Problem ist in der Kryptographie (z.B. für das RSA-Verfahren) eines der zentralen
Probleme überhaupt. Die Zahlen & , um die es dort geht, sind Teile von Schl üsseln, die
bereits jetzt eine Länge von über 100 Stellen besitzen. Mit steigender Rechnerkapazität wird
diese Länge weiter ansteigen.
Weil man solche langen Zahlen nicht mehr in konstanter Zeit addieren kann, gehen wir
hier zur sogenannten
Bitkomplexität über. D.h., wir stellen & als Binärzahl mit genau
.
4& Bits dar und zählen die Bitoperationen einzeln.
Das folgende Lemma zeigt, dass etwa jede
Lemma: Sei 5&
& &
5& &
&
. Für &
.
$
gilt:
&
&
.
5& & .
Für den Grenzwert gilt:
.
die Anzahl der Primzahlen
-te Zahl eine Primzahl ist.
&
Zunächst untersuchen wir die naive Divisionsmethode. Diese testet nacheinander, ob &
durch 2 oder durch eine ungerade Zahl aus dem Bereich 1#""#" & ' teilbar ist. Die
Anzahl der Divisionen ist in diesem Fall in & , wobei
3( & (Bitkomplexität). Somit ist das naive Verfahren für große Zahlen nicht praktikabel. Bis
heute ist kein deterministischer Algorithmus bekannt, der das Problem Primzahltest in
polynomialer Laufzeit lösen kann.
5.1.1 Algorithmus von Miller-Rabin
Der Algorithmus von Miller-Rabin ist ein randomisiertes Verfahren zum Primzahltest und
gehört der Klasse der Monte-Carlo Verfahren an. D.h., das Ergebnis ist mit einer gewissen
Fehlerwahrscheinlichkeit behaftet. Die Idee beruht auf dem folgenden wichtigen Satz von
Fermat.
5.1. RANDOMISIERTER PRIMZAHLTEST
103
Satz von Fermat
.
.
Ist & eine Primzahl, so gilt für alle * "#"#" & $ )
.
'
& "
(5.1)
Der Satz von Fermat liefert ein nützliches Kriterium zum Test der Primalität von & . Fixieren
wir z.B. eine Basis , und liefert unser Test
$ )
.
& so ist & sicherlich keine Primzahl, und heißt Zeuge für die nicht-Primheit von & . Tatsächlich
gibt es sogenannte Pseudoprimzahlen, die die Gleichung für ein zwar erfüllen, aber keine
Primzahlen sind. Jedoch liefert bereits .dieser einfache Test für 100-stellige Zahlen & und / ) 41
eine sehr kleine Fehlerrate von etwa
. Weiterhin gibt es die sogenannten Carmichael
$ die Gleichung 5.1 erfüllen, aber keine Primzahlen sind. Dabei
Zahlen, die für alle ist $ der Restklassenring bezüglich & definiert als
$
.
#"#"#" *
&
.
'
!
ggt
.
& ',"
.
Carmichael-Zahlen sind allerdings sehr selten: es gibt nur 255 von ihnen, die kleiner als
sind.
/
Diese Überlegungen führen uns direkt zur Idee des Algorithmus von Miller-Rabin
. (s. Algo.
rithmus 41). Wir wählen eine Basis zufällig und gleich verteilt aus der Menge #" " " & '
aus und machen den Test
.
)
$
& "
Wenn die Gleichung nicht erüllt ist, dann haben wir einen Zeugen für die Nicht-Primheit von
& , andernfalls wählen wir eine weitere Basis. Dieser Test wird insgesamt Mal wiederholt.
Das zugrundeliegende Prinzip nennt man Random Search, da ein Raum mit unbekannter
Struktur zufällig auf Elemente mit einer gewünschten Eigenschaft durchsucht wird.
Algorithmus. 41 Primzahltest von Miller-Rabin
#" " " 1: für .
.
2:
falls Zeuge (Random ( & & ) dann
3:
Return “nicht prim”
4:
5:
6:
'
'
Return “prim”
Es fehlt nun noch ein effizienter Algorithmus für die Berechnung von
rithmus Zeuge (siehe 42) löst das Problem durch Repeated Squaring. Sei $ )
)
#"
& . Algo" " die
KAPITEL 5. RANDOMISIERTE ALGORITHMEN
104
Algorithmus
42 Zeuge ( & )
.
/
1:
/* . /
2: für #"#"" .
& /* 3:
4:
falls dann & /* 5:
6:
7:
8:
'
'
Return NOT
.
.
.
Binärdarstellung von & . Die Funktion Witness() vermeidet große Zahlen, indem sie die
Vertauschbarkeit von Multiplikation und Modulo-Berechnung ausnutzt:
0
&
&
0
& & &
(5.2)
(5.3)
&
Dabei gilt in jedem Schleifendurchlauf die Invariante
& .
wobei durch Verdoppeln und Inkrementieren von 0 auf & erhöht wird. Dabei gilt in
)
#"#"#" . Somit gilt nach dem letzten Schleifenjedem Schleifendurchlauf , dass /
) #"#"#" & .
durchlauf , dass Analyse der Laufzeit
Die Multiplikation zweier -Bit Zahlen (Zeile (3)) hat Bitkomplexität
1
für jeden der Aufrufe von Witness().
0
. Dies sind
Satz: Der Monte-Carlo Algorithmus von Miller-Rabin zum Test der Primalität von & mit
1
Binärdarstellung ) #"#"" hat eine polynomielle Laufzeit von
.
Analyse der Fehlerrate
Man kann das folgende Theorem mit Hilfe von gruppentheoretischen Überlegungen bewei
.)
sen. Unter anderem gehören alle Nicht-Zeugen zur Menge
$
Satz: Für & , ungerade und nicht prim, gibt es mindestens 0 Zeugen für die Zusammengesetztheit.
Daraus folgt, dass die . zufällige Auswahl von in jeder Iteration mit einer Wahrscheinlichkeit von mindestens einen Zeugen für die Zusammengesetztheit einer nicht-primen
Zahl & liefert. Da der Algorithmus Miller-Rabin nur dann ein falsches Testergebnis
ausgibt, wenn nach Iterationen immer noch kein Zeuge gefunden worden ist und die
Zahl & tatsächlich nicht prim ist, erhalten wir insgesamt eine Fehlerwahrscheinlichkeit von
höchstens 0 . Tatsächlich ist die Fehlerrate in der Praxis um einige Größenordnungen kleiner.
5.1. RANDOMISIERTER PRIMZAHLTEST
105
Weiterführende Literatur
Eine kurze Einführung in das Themengebiet randomisierte Algorithmen finden Sie in dem
Artikel von T. Roos, “Randomisierte Algorithmen” (in dem Buch von T. Ottmann, “Prinzipien des Algorithmenentwurfs”, Spektrum Akademischer Verlag, 1998), das auch als Vorlage für diesen Teil des Skripts war. Für weitergehende Literatur empfehlen wir das Buch
von R. Motwani und P. Raghavan,“Randomized Algorithms”, Cambridge University Press,
1995.
106
KAPITEL 5. RANDOMISIERTE ALGORITHMEN
Kapitel 6
Parallele Algorithmen
Beim Parallelen Rechnen geht es um das schnelle Lösen algorithmischer Probleme durch
den Einsatz vieler Prozessoren (Rechner). Man untersucht die Frage, wann und unter
welchen Bedingungen sich der Einsatz vieler Prozessoren zur schnelleren Berechnung eines
gegebenen Problems lohnt. Dabei spielen auch verschiedene Rechnermodelle eine wichtige
Rolle.
).
Wir nehmen an, wir wollen & Zahlen aufsummieren, die sich in einem Array
""& +
befinden. Die sequentielle Addition ist in Abbildung 6.1(a) visualisiert. Wir berechnen
zunächst die Summe zweier Zahlen, und nehmen dieses Ergebnis, um es zur dritten Zahl
zu addieren, u.s.w. Diese Operationen sind abhängig voneinander. Es kann nur jeweils ein
Prozessor sinnvoll eingesetzt werden.
+
+
+
+
A(1)
+
A(4)
+
A(3)
A(2)
+
A(1)
+
A(2)
(a)
A(3)
+
A(4)
A(5)
+
A(6)
A(7)
A(8)
(b)
Abbildung 6.1: Verlauf einer sequentiellen und einer parallelen Summation
Ein alternativer paralleler Verlauf ist in Abbildung 6.1(b) visualisiert. Hier werden jeweils
Paare gebildet und diese unabhängig voneinander aufsummiert. Dies ist mit Hilfe von & Prozessoren parallel durchführbar. Im nächsten Schritt werden diese Paare wieder paarweise
107
KAPITEL 6. PARALLELE ALGORITHMEN
108
aufsummiert, u.s.w. Insgesamt ist die Summation für & Zahlen hier mit
& Schritten
mit Hilfe von & Prozessoren durchführbar. Diese Methode ist also die günstigere Methode.
Nach dieser kurzen Einführung stellen wir das von uns im folgenden betrachtete parallele
Rechnermodell vor.
6.1 Ein Modell einer parallelen Maschine
Es gibt viele Möglichkeiten für parallele Maschinen. Wir betrachten als paralleles Rechnermodell eine PRAM (Parallel Random Access Machine). Abbildung 6.2 zeigt den Aufbau
einer PRAM.
P1
Pp
P2
viele Prozessoren
einige lokale Speicherplaetze
gemeinsamer Speicher
Abbildung 6.2: Modell einer PRAM
Jeder Prozessor kann auf den gemeinsamen Speicher direkt zugreifen. Jeder Prozessor besitzt daneben auch selbst noch lokale Speicherplätze. Die PRAM wird von einem einzigen
Programm gesteuert. Ein Schritt in einer PRAM besteht aus drei Teilen:
Lesephase, d.i. die Übernahme eines Speicherwertes auf dem gemeinsamen Speicher
in den lokalen Speicher.
Rechenphase, d.h. jeder Prozessor kann konstant viele Operationen mit Werten in seinem Speicher durchführen.
Schreibphase, d.h. jeder Prozessor kann einen Speicherwert von seinem lokalen in den
gemeinsamen Speicher schreiben.
6.2 Parallele Minimum-Berechnung
In diesem Abschnitt diskutieren wir zwei parallele Verfahren, die jeweils mit & und &
Prozessoren eine parallele Minimum-Berechnung auf der PRAM durchführen.
Gegeben: & Zahlen in dem Array
Gesucht: Minimum der Zahlen in
0
6.2. PARALLELE MINIMUM-BERECHNUNG
109
Min
Min
Min
Min
Min
Min
Min
Abbildung 6.3: Parallele Minimum-Berechnung mit & Prozessoren
Parallele Minimum-Berechnung mit & Prozessoren
Abbildung 6.3 zeigt das Berechnungsschema des Verfahrens. Eine Realisierung ist in
Algorithmus 43 beschrieben. Der Algorithmus gibt für jeden Prozessor an, was dieser in
welchem Schritt der Rechnung zu tun hat. Man sagt auch, dass ein solche Algorithmus mit
der Prozessornummer parametrisiert ist. Die beiden Arrays
und
befinden sich im
gemeinsamen Speicher. Die Speicherplätze , , und befinden sich jeweils im lokalen
Speicher
von Prozessor . Der Algorithmus durchläuft für & Eingabewerte die Schleife
& Mal. Beim ersten Schleifendurchlauf sind & Prozessoren beteiligt, beim zweiten
Durchlauf nur noch & , u.s.w. Zu Beginn eines jeden Schleifendurchlaufs überprüft
jeder Prozessor , ob er noch beteiligt ist. Das ist beim
-ten Durchlauf der Fall, wenn
seine Nummer. kleiner gleich & ist. Ist die Schleife.
& Mal durchlaufen, dann legt
)
Prozessor das Minimum in der Speicherzelle
+ ab. Siehe auch Abbildung 6.4.
Algorithmus 43 Parallele Minimum-Berechnung für & Prozessoren
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
)
+ .
)
+
für bis & $
. dann
falls
) 0
+
)
) +
$1
+ '
'
.
falls dann
)
Ausgabe von
+
'
Der beschriebene Algorithmus ist nur dann korrekt, wenn wir annehmen, dass die Prozesso-
KAPITEL 6. PARALLELE ALGORITHMEN
110
Pi’s
B:
h=1:
h=2:
h=3: i=1
Abbildung 6.4: Visualisierung der Minimum-Berechnung mit Algorithmus 43
ren zeitlich synchron arbeiten können, d.h., dass alle Prozessoren dieselben Arbeitsschritte
zu exakt der gleichen Zeit ausführen.
0
Parallele Minimum-Berechnung mit &
Prozessoren
Der alternative Algorithmus (s. Algorithmus
44) berechnet das Minimum mit Hilfe von qua.
& , in konstant vielen Schritten. Die Arrays und
dratisch vielen Prozessoren ,
)
stehen im gemeinsamen Speicher. Im Element + vermerken wir, ob die Zahl noch als
Minimum in Frage kommt
oder nicht. Dieses Feld wird im ersten Schritt mit 1 initialisiert.
.
Alle Prozessoren ,
& , schreiben im ersten Schritt jeweils eine 1 an die Stelle
)
+ . Im zweiten Schritt passiert die eigentliche Minimum-Berechnung. Jeder Prozessor )
)
)
)
)
vergleicht jeweils die Werte
+ und
+ . Ist
+
+ , dann scheidet
+ als Mini)
mum aus, und der Prozessor schreibt eine 0 an die Stelle + . Wenn wir annehmen, dass
das Minimum an . Stelle im Feld steht, dann wird allein durch die Berechnungen der &
)
Prozessoren ,
& , an allen Stellen , die größer als das Minimum sind, + mit
0 beschrieben.
0
Algorithmus 44 Parallele Minimum-Berechnung
mit & Prozessoren
.
)
1: Für Prozessoren : + )
)
)
/
2: Für Prozessoren : Falls
+ +
. + dann )
)
/
3: Für Prozessoren : Falls + . dann + )
)
+ dann Ausgabe von
+
4: Für Prozessoren : Falls Abbildung 6.5 zeigt eine (sequentielle) Visualisierung anhand eines Beispiels, die verdeutlicht was in Schritt 2 genau geschieht. Die. erste Zeile des Blocks ist zu lesen als das Ergebnis
der Berechnungen aller Prozessoren , .
& , wenn diese zuerst rechnen würden. Die
nächste Zeile betrifft die Prozessoren 0 ,
& , u.s.w. (Das Bild ist leicht irreführend
hinsichtlich der Parallelität. Natürlich arbeiten alle Prozessoren gleichzeitig.) Ist das Minimum eindeutig, dann genügt dieser Schritt. Schritt 3 sorgt nur noch dafür, dass von allen
Minimumwerten derjenige mit dem kleinsten Index übrigbleibt. Im letzten Schritt wird das
Minimum ausgegeben.
6.2. PARALLELE MINIMUM-BERECHNUNG
2 8
6
3
h[i]
1
1 1
1: 2,3,4 ...
1
1 1
2: 1,3,4 ...
3:
0
1
4:
0
0
111
2 4
5 7
1
1 1
1
0
1
1 0
0 0
1
1
1 0 1 0
0 1 0 1 0 0 0
0 1 0 1 0 0 0
Abbildung 6.5: Visualisierung der Minimum-Berechnung mit Algorithmus 2
Für diesen Algorithmus wird eine PRAM benötigt, die gleichzeitige Lesezugriffe mehrerer
Prozessoren gestattet. Eine solche Maschine nennt man auch CR-PRAM für Concurrent
Read. Ausserdem ist ein gleichzeitiger Schreibzugriff nötig. Eine solche Maschine nennt
man CW-PRAM für Concurrent Write. Bei unserem Algorithmus tritt dieses Problem
allerdings nur in einer besonderen Form auf: die Prozessoren schreiben zwar in die gleiche
Speicherzelle, aber sie schreiben jeweils denselben Wert. Eine PRAM, die erlaubt den
gleichen Wert zu schreiben, nennt man eine weak CW-PRAM. Zusammenfassend kann
man sagen, dass der Algorithmus eine CRCW-PRAM benötigt (im Gegensatz zu einer
EREW-PRAM: exclusive Read, exclusive Write).
Erstaunlicherweise kann das obige Verfahren das Minimum immer in vier Schritten
(konstant!) berechnen — und dies unabhängig von der Anzahl der Ememente & . Allerdings
benötigen wir für die Berechnung eine große Anzahl von Prozessoren, die natürlich
abhängig von & ist.
Effizienzmaße für parallele Algorithmen
Nun kann man sich fragen, welcher von beiden Algorithmen der bessere ist. Für die Effizienz
von Algorithmen gibt es veschiedene Maße. Zu diesen gehören die Anzahl der benötigten
Prozessoren 4& , die parallele Laufzeit 4& , die Arbeit, die Effizienz, sowie der Speedup.
Die Arbeit ist definiert als
die Effizienz als
und der Speedup als
5& 5& 4& Arbeit sequentiell
Arbeit parallel
Laufzeit sequentiell
"
Laufzeit parallel
Tabelle 6.1 gibt eine Übersicht über die verschiedenen Werte für die beiden Algorithmen.
KAPITEL 6. PARALLELE ALGORITHMEN
112
Algorithmus 43:
Algorithmus 44:
Prozessoren
Laufzeit
Arbeit
Effizienz
3(4& 0
3(4& 3( . & 3( 3(5& & 0
3 5& (
3( %$ 3
(
$ Speedup
$
3. 2$ 3.5& Tabelle 6.1: Übersicht über die Maße zu den beiden Algorithmen zur Minimum-Berechnung
& für
Wir nennen einen parallelen Algorithmus optimal, wenn er Laufzeit 5& 4& , wobei 4& die sequentielle Laufzeit ist. Ein parein besitzt und Arbeit 4& alleler Algorithmus heißt effizient, wenn 5& & und 4& 4& & .
Der Algorithmus 43 ist also effizient, währenddessen Algorithmus 44 nicht effizient ist. Das
liegt an dem Einsatz der sehr großen Prozessorenanzahl, die die Arbeit im Vergleich zum
sequentiellen Fall sehr vergrößert.
6.3 Parallele Präfixsummen-Berechnung
In diesem Abschnitt untersuchen wir einen parallelen Algorithmus für das folgende Problem:
Das Präfixsummen-Problem
Gegeben: Feld von & ganzen Zahlen
Gesucht: Feld , das die Präfixsummen zu
)
Für das. Beispiel
.
.
/
+ enthält, also für jedes Anfangsstück die Summe
.
)
+
ist die gewünschte Präfixsummenfolge
1 1
. Wie gehen wir vor, wenn wir diese Folge parallel berechnen
wollen? Die Idee hierzu basiert auf der rekursiven Paarbildung. Zunächst werden Paare
gebildet, die dann
unabhängig
werden.
In unserem Beispiel. wären
.
voneinander
.
addiert
.
das
.
z.B. . Die neue Folge ist nur. noch
halb
so
lang
wie
die
Originalfolge.
Die
Präfixsummenfolge
davon
ist
.
/
. Wenn wir daraus nun die Präfixsummenfolge unserer Originalfolge
1 berechnen wollen, geht das folgendermaßen. Für jedes gerade Element ( gerade), können
)
)
wir das Ergebnis
+ direkt aus der errechneten Präfixsummenfolge %+ übernehmen.
Dies ist nicht möglich für) die Elemente mit ungeradem Index. Für diese Elemente muss
)
)
noch der Term
+ zu 0 + hinzuaddiert werden.
1
1
Algorithmus 45 realisiert diese Idee. Dabei wird davon ausgegangen, dass & für ein
ganzzahliges gilt. Falls das nicht der Fall ist, kann um entsprechend viele Stellen mit
6.3. PARALLELE PRÄFIXSUMMEN-BERECHNUNG
113
Null-Einträgen verlängert werden. Schritt 1 legt das Rekursionsende fest. In Schritt 2 wird
jeweils die Paarbildung durchgeführt. Schritt 3 startet jeweils einen neuen rekursiven Aufruf
für alle Prozessoren gemeinsam. Und Schritt 4 setzt die Ergebnisse der PräfixsummenBerechnung der Paare um in die korrekten Präfixsummen der gegebenen Originalfolge.
Algorithmus 45 . Parallel Präfixsummen
Rekursiv
).
).
+ + ; STOP
1: Falls & dann
.
) )
+ 2: Für alle : Falls. gerade dann 0 + )
$
#" " " 0 : Berechne rekursiv + 3: Für alle , )
) 0 + sonst
4: Für alle : Falls gerade dann
+ )
.) +
)
+ + """ )
))
)
+ 0 + +
)
+
Das benötigte Rechnermodell ist hier die CREW, denn in Schritt (4) wird jeweils das
gleiche Feld von verschiedenen Prozessoren gelesen. Allerdings kann dieser Algorithmus
leicht modifiziert werden, sodass er bereits auf einer EREW-PRAM läuft: Es genügt z.B.
aus Schritt (4) zwei Schritte zu machen, die jeweils nur die geraden bzw. die ungeraden
abarbeiten.
Analyse der Effizienz: Schritte (1)-(4) benötigen jeweils bis zu 5& & Prozessoren und
$
konstante Zeit 4& . Schritt (3) führt eine Rekursion auf 0 Werten durch und benötigt hierfür
$
bis zu 0 Prozessoren bei einer Laufzeit von 5& & . Dies ergibt die parallele Arbeit
5& 4& & . Die serielle Arbeit der Präfixsummen-Berechnung ist 4& 5& .
Daraus folgt, dass Algorithmus 45 ein effizienter Algorithmus ist, jedoch kein optimaler.
Im folgenden werden wir sehen, wie wir den Algorithmus durch leichte Modifikation in
einen optimalen Algorithmus umwandeln können. Die Idee dabei ist, die parallele Arbeit
durch Einsatz von weniger Prozessoren zu verringern. Sie beruht auf der Beobachtung,
dass die & Prozessoren nur in der ersten Rekursionsstufe benötigt werden. In der zweiten
$
Rekursionsstufe genügen bereits 0 , u.s.w. Über alle & Stufen fällt also nicht wirklich
$
$
$
& & Arbeit an, sondern nur & 0 5& .
Brents Lemma beschreibt, wie man im Allgemeinen einen parallelen Algorithmus, der viele
Prozessoren benötigt, durch einen Algorithmus mit wenigen Prozessoren simulieren kann.
Genauer besagt es das folgende:
Brents Lemma
Gegeben sei eine parallele Rechnung mit Schritten, die in jedem Schritt parallele
Operationen ausführt. Somit ist die Anzahl der verwendeten Prozessoren .
Sei — mit
. Man kann diese Rechnung — unter gewissen Voraussetzungen
nur & Prozessoren durchführen, indem man jeden einzelnen Schritt durch Schritte
KAPITEL 6. PARALLELE ALGORITHMEN
114
ersetzt. Die neue parallele Laufzeit ist dann
.
"
Wir wenden Brent’s Lemma auf unser Beispiel an. Hier ist 5&
Um einen optimalen Algorithmus zu erreichen, muss gelten
4& 4& & 4& 5& und & .
und
5& Dies erreichen wir durch die Wahl von
$4&
&
&
Konkrete Umsetzung
$
Wir zeigen im folgenden, wie wir die 5& $ Prozessoren genau einsetzen. Jedem
2$
Prozessor ist nun in Schritt (2) nicht mehr nur ein Zahlenpaar, sondern 0 Zahlenpaare
zugeordnet. Diese arbeitet er sequentiell ab. Prozessor 1 übernimmt das erste & starke
Zahlenpaket, Prozessor 2 das nächste, u.s.w.
In Schritt
(3) erfolgt der erste Rekursionsaufruf, von dem wiederum jeder Prozessor statt
$
eine 0 Berechnungen auszuführen hat. Mit jeder weiteren Rekursion halbiert sich die
“Überlast”, die jeweils sequentiell ausgerechnet wird. Und zwar solange, bis die Länge der
aktuellen Folge kleiner gleich der Anzahl der Prozessoren ist. Danach ist jeder Prozessor
wieder nur für eine Zahl zuständig (wie in unserem Originalalgorithmus 45). Insgesamt ergibt sich eine neue Gesamtlaufzeit von
4& &
&
&
.
.
.
& "
$
Daraus folgt, dass die Prozessorallokation bei der Präfixsumme mit nur $ Prozessoren
keinerlei Probleme bereitet. Somit haben wir einen optimalen Algorithmus, denn die Arbeit
ist
4& 4& &
&
&
5& "
6.4. ANWENDUNGEN DER PRÄFIXSUMMEN-BERECHNUNG
115
6.4 Anwendungen der Präfixsummen-Berechnung
6.4.1 Komprimieren eines dünn besetzten Arrays
Wir wollen das folgende Problem mit Hilfe der Präfixsummen-Berechnung lösen.
Array-Komprimierung Gegeben: Array mit vielen Nullen und wenigen anderen Zeichen
Gesucht: Array , das aus den Nicht-Null-Zeichen von
besteht; die Reihenfolge der
Zeichen muss erhalten bleiben.
/
/
.
/
/
/
/
Zum Beispiel
ist das Ergebnis der Folge
die Folge
.
. Dies können wir nun sehr einfach mit Hilfe von Algorithmus 45 berechnen,
siehe Algorithmus 46.
Algorithmus 46 Komprimierung eines Arrays
)
)
/
/
1: Für alle : Falls
dann
sonst
+ + )
2: Für alle : Berechne die Präfixsumme für
+
) /
) )
)
3: Für alle : Falls
dann
+ ++ +
.
)
)
+ +
Die Analyse ist einfach. Schritte (1) und (3) laufen in konstanter Zeit bei linear vielen Pro$
zessoren, Schritt (2) benötigt Zeit
& bei $ Prozessoren. Beachten Sie, dass in
Schritt (2) alle Prozessoren zusammen genau einmal Algorithmus 45 aufrufen. Der Algo$
rithmus läuft auf einer EREW-PRAM. Die Arbeit ist 4& &
& $ &
5& .
Somit ist der parallele Algorithmus optimal.
6.4.2 Simulation eines endlichen Automaten
Ein endlicher Automat besteht aus einer endlichen Menge von Zuständen und Zustandsübergängen. Er liest Zeichen für Zeichen ein und verarbeitet diese. Ein endlicher
Automat lässt sich als Graph darstellen. Abbildung 6.6 zeigt ein Beispiel eines endlichen
Automaten. Der Anfangszustand sei 1. Bei Eingabe eines Zeichens ‘ ’ geht der Automat in
den Zustand 3 über, während er bei Eingabe von ‘ ’ vom Anfangszustand in den Endzustand 4 übergeht. Unser Ziel ist es nun, für einen beliebigen gegebenen Eingabestring den
Endzustand eines endlichen Automaten zu berechnen.
Wir lösen diese Aufgabe mit Hilfe der Präfixsummen-Berechnung. Dazu ersetzen wir
zunächst jedes Eingabezeichen durch seine Übergangsfunktion:
.
2
.
2
. 1
1
.
Mit jedem Eingabestring verbinden wir nun eine zusammengesetzte Übergangsfunktion, z.B.
KAPITEL 6. PARALLELE ALGORITHMEN
116
b
b
2
a
a
b
1
3
a
a
4
b
Abbildung 6.6: Beispiel eines endlichen Automaten
.
.
2
Zwei Übergangsfunktionen werden zusammengesetzt, indem sie nacheinander ausgeführt
werden. Der Präfixsummen-Algorithmus kann hierfür benutzt werden, indem die Addition
durch die Zusammensetzungsfunktion ersetzt wird. Damit sind die Präfixsummen gerade
die Übergangsfunktionen aller Anfangsstücke der Eingabe.
Endliche Automaten können sehr vielseitig in der Informatik und in Anwendungen eingesetzt werden. Im folgenden sehen wir, wie man die Addition zweier Zahlen auf der Ebene
der digitalen Schaltung als endlichen Automaten darstellen kann.
6.4.3 Addier-Schaltung
Gegeben sind zwei Zahlen $
in Binärdarstellung.
in Binärdarstellung mit & Bits. Gesucht ist die Summe
Beispiel:
1 0 1 0 1 1 1
0 0 1 0 0 1 1
Übertrag 0 0 1 0 1 1 1 Summe 1 1 0 1 0 1 0
Bei der dualen Addition von und bilden sich an gewissen Stellen Überträge, die sich
jeweils ein Bit weiter nach links fortpflanzen. Mit einem Prozessor kann man die Addition
von und in & Schritten (unter Berücksichtigung der Überträge) von rechts nach links
durchführen. Das Propagieren der Überträge kann durch den in Abbildung 6.7 dargestellten
endlichen Automaten modelliert werden. Ein Zustand entspricht dabei dem Übertrag in die
nächste Stelle bei der Addition. Befindet sich z.B. der Automat nach dem -ten Übergang im
6.4. ANWENDUNGEN DER PRÄFIXSUMMEN-BERECHNUNG
117
2
0
1
0,1
1,2
0
Abbildung 6.7: Endlicher Automat für Übertragsbit
.
Zustand 0, so ist der Übertrag an die -te Stelle 0. Die Übergangsfunktion hängt von
der Summe der beiden -ten Bits von und ab. Sind, z.B. beide Bits 1, d.h. die Summe ist
2, und ist der Zustand 0, so geht der Automat in den Zustand 1 über; er merkt also eine 1 als
Übertrag vor.
Algorithmus 47 Addier-Schaltung
)
+ für alle
Input: Eingabe der bitweisen Summe in
1: Für alle : Simulation des endlichen Automaten durch Präfixsumme
)
)
2: Für alle : Addiere Ausgabezustand
+ zum dazugehörigen Input
3: Für alle : Falls Summe ungerade dann setze Bit=1 sonst Bit=0
)
.+
+
Algorithmus 47 berechnet die Summe mit Hilfe des endlichen Automaten. Als Eingabe
des Algorithmus genügen die bitweisen Summen der beiden Zahlen und beginnend
bei
.
.
/
/
.
den niederwertigsten Bits. Dies ist in unserem Beispiel der Vektor
$
Die Eingabe kann mit & Prozessoren in konstanter Zeit, und mit $ Prozessoren in
logarithmischer Zeit berechnet werden. Danach simulieren wir den endlichen Automaten
mit Hilfe der Präfixsummen-Berechnung in Zeit
& . Nun wird für jedes Bit der
jeweilige Zustand des Übertragungsbits zum dazugehörigen Input addiert. Falls die Summe
ungerade ist, wird das Bit gleich eins gesetzt und sonst 0. Auch dieser Schritt kann mit
$
2$ Prozessoren in logarithmischer Zeit berechnet werden. Insgesamt berechnet der
$
Algorithmus die Summe mit Hilfe von $ Prozessoren in Zeit
& und ist somit
optimal.
Dieser Algorithmus kann direkt in eine digitale Schaltung übertragen werden. Man erhält
eine Schaltung aus linear vielen Bausteinen, die eine duale Addition in logarithmischer Zeit
ausführt.
Weiterführende Literatur
Eine kurze Einführung, die auch als Vorlage für diesen Abschnitt genommen wurde, ist der
Artikel Widmayer: “Parallele Präfix-Summation” in dem Buch von T. Ottmann: “Prinzipien des Algorithmenentwurfs” (s. Literaturhinweise (4)). Eine ausführliche Einführung und
Behandlung bietet das Buch von J. Jaja: “An Introduction to Parallel Algorithms”, Addison
Wesley, 1992.
Herunterladen