200 19 8MB
German Pages 1339 Year 2013
Algorithmen – Eine Einführung von
Prof. Dr. Thomas H. Cormen Prof. Dr. Charles E. Leiserson Prof. Dr. Ronald Rivest Prof. Dr. Clifford Stein Aus dem Englischen von Prof. Dr. rer. nat. habil. Paul Molitor Martin-Luther-Universität Halle-Wittenberg
4., durchgesehene und korrigierte Auflage
Oldenbourg Verlag München
Autorisierte Übersetzung der englischensprachigen Ausgabe, die bei The MIT Press, Massachusetts Institute of Technology und McGraw-Hill Book Company unter dem Titel Introduction to Algorithms, Third Edition erschienen ist. Copyright © 2009 MIT Press, McGraw-Hill Book Company Übersetzung: Prof. Dr. rer. nat. habil. Paul Molitor, Institut für Informatik, Martin-Luther-Universität Halle-Wittenberg
Lektorat: Johannes Breimeier Herstellung: Tina Bonertz Titelbild: www.thinkstockphotos.de Einbandgestaltung: hauser lacour Bibliografische Information der Deutschen Nationalbibliothek Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar. Library of Congress Cataloging-in-Publication Data A CIP catalog record for this book has been applied for at the Library of Congress. Dieses Werk ist urheberrechtlich geschützt. Die dadurch begründeten Rechte, insbesondere die der Übersetzung, des Nachdrucks, des Vortrags, der Entnahme von Abbildungen und Tabellen, der Funksendung, der Mikroverfilmung oder der Vervielfältigung auf anderen Wegen und der Speicherung in Datenverarbeitungsanlagen, bleiben, auch bei nur auszugsweiser Verwertung, vorbehalten. Eine Vervielfältigung dieses Werkes oder von Teilen dieses Werkes ist auch im Einzelfall nur in den Grenzen der gesetzlichen Bestimmungen des Urheberrechtsgesetzes in der jeweils geltenden Fassung zulässig. Sie ist grundsätzlich vergütungspflichtig. Zuwiderhandlungen unterliegen den Strafbestimmungen des Urheberrechts. © 2013 Oldenbourg Wissenschaftsverlag GmbH Rosenheimer Straße 143, 81671 München, Deutschland www.degruyter.com/oldenbourg Ein Unternehmen von De Gruyter Gedruckt in Deutschland Dieses Papier ist alterungsbeständig nach DIN/ISO 9706. ISBN
978-3-486-74861-1
Inhaltsverzeichnis Vorwort
I
Grundlagen
1 Die Rolle von Algorithmen in der elektronischen Datenverarbeitung
XIII
1 5
1.1
Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5
1.2
Algorithmen als Technologie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
11
2 Ein einführendes Beispiel
17
2.1
Sortieren durch Einfügen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
17
2.2
Analyse von Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
24
2.3
Entwurf von Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
30
3 Wachstum von Funktionen
45
3.1
Asymptotische Notation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
45
3.2
Standardnotationen und Standardfunktionen . . . . . . . . . . . . . . . . . . . . . . . . . .
55
4 Teile-und-Beherrsche
67
4.1
Das Max-Teilfeld-Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
70
4.2
Strassens Algorithmus zur Matrizenmultiplikation . . . . . . . . . . . . . . . . . . . . .
77
4.3
Die Substitutionsmethode zum Lösen von Rekursionsgleichungen . . . . . .
85
4.4
Die Rekursionsbaum-Methode zum Lösen von Rekursionsgleichungen . .
89
4.5
Die Mastermethode zum Lösen von Rekursionsgleichungen . . . . . . . . . . . .
95
4.6
∗Beweis des Mastertheorems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
99
5 Probabilistische Analyse und randomisierte Algorithmen
115
5.1
Das Bewerberproblem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
115
5.2
Indikatorfunktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
118
VI
II
Inhaltsverzeichnis 5.3
Randomisierte Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
123
5.4
∗ Probabilistische Analyse und mehr zur Verwendung der Indikatorfunktion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
130
Sortieren und Ranggrößen
6 Heapsort
153
6.1
Heaps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
153
6.2
Die Heap-Eigenschaft aufrechterhalten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
156
6.3
Einen Heap bauen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
158
6.4
Der Heapsort-Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
161
6.5
Prioritätswarteschlangen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
162
7 Quicksort
171
7.1
Beschreibung von Quicksort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
171
7.2
Die Performanz von Quicksort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
175
7.3
Eine randomisierte Version von Quicksort . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
179
7.4
Analyse von Quicksort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
181
8 Sortieren in linearer Zeit
191
8.1
Untere Schranken für das Sortieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
191
8.2
Countingsort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
194
8.3
Radixsort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
197
8.4
Bucketsort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
200
9 Mediane und Ranggrößen
III
147
213
9.1
Minimum und Maximum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
213
9.2
Auswahl in linearer erwarteter Zeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
215
9.3
Auswahl in linearer Zeit im schlechtesten Fall . . . . . . . . . . . . . . . . . . . . . . . . .
219
Datenstrukturen
10 Elementare Datenstrukturen
227 233
10.1 Stapel und Warteschlangen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
233
10.2 Verkettete Listen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
237
Inhaltsverzeichnis 10.3 Implementierung von Zeigern und Objekten . . . . . . . . . . . . . . . . . . . . . . . . . . .
242
10.4 Darstellung von gerichteten Bäumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
246
11 Hashtabellen
255
11.1 Adresstabellen mit direktem Zugriff . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
256
11.2 Hashtabellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
258
11.3 Hashfunktionen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
264
11.4 Offene Adressierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
272
11.5 ∗ Perfektes Hashing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
280
12 Binäre Suchbäume
289
12.1 Was ist ein binärer Suchbaum? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
289
12.2 Abfragen in einem binären Suchbaum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
292
12.3 Einfügen und Löschen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
296
12.4 ∗ Zufällig erzeugte binäre Suchbäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
302
13 Rot-Schwarz-Bäume
311
13.1 Eigenschaften von Rot-Schwarz-Bäumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
311
13.2 Rotationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
315
13.3 Einfügen eines Knotens . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
317
13.4 Löschen eines Knotens . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
325
14 Erweitern von Datenstrukturen
IV
VII
341
14.1 Dynamische Ranggröße . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
341
14.2 Wie man eine Datenstruktur erweitert . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
347
14.3 Intervallbäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
350
Fortgeschrittene Entwurfs- und Analysetechniken
15 Dynamische Programmierung
359 363
15.1 Schneiden von Eisenstangen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
364
15.2 Matrizen-Kettenmultiplikation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
374
15.3 Elemente dynamischer Programmierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
381
15.4 Längste gemeinsame Teilsequenz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
393
15.5 Optimale binäre Suchbäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
399
VIII
Inhaltsverzeichnis
16 Greedy-Algorithmen 16.1 Ein Aktivitäten-Auswahl-Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
418
16.2 Elemente der Greedy-Strategie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
425
16.3 Huffman-Codierungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
431
16.4 ∗ Matroiden und Greedy-Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
440
16.5 ∗ Ein Task-Scheduling-Problem als Matroid . . . . . . . . . . . . . . . . . . . . . . . . . . .
447
17 Amortisierte Analyse
V
417
455
17.1 Aggregat-Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
456
17.2 Account-Methode. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
460
17.3 Die Potentialmethode. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
462
17.4 Dynamische Tabellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
466
Höhere Datenstrukturen
18 B-Bäume
483 489
18.1 Die Definition von B-Bäumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
493
18.2 Grundoperationen auf B-Bäumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
496
18.3 Löschen eines Schlüssels aus einem B-Baum . . . . . . . . . . . . . . . . . . . . . . . . . . .
504
19 Fibonacci-Heaps
511
19.1 Die Struktur von Fibonacci-Heaps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
513
19.2 Operationen der fusionierbaren Heaps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
516
19.3 Verringern eines Schlüssels und Entfernen eines Knotens . . . . . . . . . . . . . .
525
19.4 Beschränkung des maximalen Grades . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
529
20 van-Emde-Boas-Bäume
539
20.1 Vorbereitende Ansätze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
540
20.2 Eine rekursive Datenstruktur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
544
20.3 Die van-Emde-Boas-Bäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
553
21 Datenstrukturen disjunkter Mengen
569
21.1 Operationen auf disjunkten Mengen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
569
21.2 Darstellung disjunkter Mengen mithilfe verketteter Listen . . . . . . . . . . . . .
572
Inhaltsverzeichnis
VI
IX
21.3 Wälder disjunkter Mengen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
576
21.4 ∗ Analyse der Vereinigung nach dem Rang mit Pfadverkürzung . . . . . . .
580
Graphenalgorithmen
22 Elementare Graphenalgorithmen
595 599
22.1 Darstellungen von Graphen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
599
22.2 Breitensuche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
603
22.3 Tiefensuche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
613
22.4 Topologisches Sortieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
622
22.5 Starke Zusammenhangskomponenten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
626
23 Minimale Spannbäume
635
23.1 Aufbau eines minimalen Spannbaums . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
636
23.2 Die Algorithmen von Kruskal und Prim . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
641
24 Kürzeste Pfade von einem Startknoten aus
655
24.1 Der Bellman-Ford-Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
663
24.2 Kürzeste Pfade von einem Startknoten aus in DAGs . . . . . . . . . . . . . . . . . .
667
24.3 Dijkstras Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
670
24.4 Differenzbedingungen und kürzeste Pfade . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
677
24.5 Beweise der Eigenschaften kürzester Pfade . . . . . . . . . . . . . . . . . . . . . . . . . . . .
683
25 Kürzeste Pfade für alle Knotenpaare
697
25.1 Kürzeste Pfade und Matrizenmultiplikation . . . . . . . . . . . . . . . . . . . . . . . . . . .
699
25.2 Der Floyd-Warshall-Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
705
25.3 Johnsons Algorithmus für dünn besetzte Graphen . . . . . . . . . . . . . . . . . . . . .
713
26 Maximaler Fluss
721
26.1 Flussnetzwerke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
722
26.2 Die Ford-Fulkerson-Methode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
727
26.3 Maximales bipartites Matching . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
745
26.4 ∗ Push/Relabel-Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
749
26.5 ∗ Der Relabel-to-Front-Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
762
X
Inhaltsverzeichnis
VII
Ausgewählte Themen
781
27 Mehrfädige Algorithmen
785
27.1 Grundlagen von dynamischem Multithreading . . . . . . . . . . . . . . . . . . . . . . . . .
787
27.2 Mehrfädige Matrizenmultiplikation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
806
27.3 Mehrfädiges Sortieren durch Mischen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
811
28 Operationen auf Matrizen
827
28.1 Lösen linearer Gleichungssysteme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
827
28.2 Matrixinversion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
841
28.3 Symmetrische positiv definite Matrizen, Summe der quadratischen Fehler
846
29 Lineare Programmierung
857
29.1 Standard- und Schlupfform . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
864
29.2 Darstellung von Problemen als lineare Programme . . . . . . . . . . . . . . . . . . . .
872
29.3 Der Simplexalgorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
878
29.4 Dualität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
893
29.5 Die initiale zulässige Basislösung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
899
30 Polynome und die FFT
911
30.1 Darstellung von Polynomen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
913
30.2 Die DFT und FFT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
919
30.3 Effiziente Implementierung der FFT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
927
31 Zahlentheoretische Algorithmen
937
31.1 Elementare zahlentheoretische Begriffe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
938
31.2 Größter gemeinsamer Teiler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
944
31.3 Modulare Arithmetik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
950
31.4 Lösen modularer linearer Gleichungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
957
31.5 Der chinesische Restsatz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
962
31.6 Potenzen eines Elements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
965
31.7 Das RSA-Kryptosystem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
970
31.8 ∗ Primzahltests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
977
31.9 ∗ Primfaktorzerlegung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
987
Inhaltsverzeichnis 32 String-Matching 32.1 Der naive String-Matching-Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
XI 997 999
32.2 Der Rabin-Karp-Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1002 32.3 String-Matching mit endlichen Automaten . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1007 32.4 ∗ Der Knuth-Morris-Pratt-Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1014 33 Algorithmische Geometrie
1025
33.1 Eigenschaften von Strecken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1026 33.2 Bestimmung von Schnittpunkten in einer Menge von Strecken . . . . . . . . . 1032 33.3 Bestimmen der konvexen Hülle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1039 33.4 Berechnung des dichtesten Punktepaares . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1050 34 NP-Vollständigkeit
1059
34.1 Polynomielle Zeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1064 34.2 Verifikation in polynomieller Zeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1072 34.3 NP-Vollständigkeit und Reduktion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1077 34.4 NP-Vollständigkeitsbeweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1088 34.5 NP-vollständige Probleme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1096 35 Approximationsalgorithmen
1117
35.1 Das Knotenüberdeckungsproblem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1119 35.2 Das Problem des Handelsreisenden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1122 35.3 Das Mengenüberdeckungsproblem. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1128 35.4 Randomisierung und lineare Programmierung . . . . . . . . . . . . . . . . . . . . . . . . . 1134 35.5 Das Teilsummenproblem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1139
VIII
Anhang
A Summen
1151 1155
A.1 Summenformeln und Eigenschaften . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1155 A.2 Abschätzungen für Summen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1159 B Mengen usw.
1169
B.1 Mengen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1169 B.2 Relationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1174
XII
Inhaltsverzeichnis B.3 Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1176 B.4 Graphen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1178 B.5 Bäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1183
C Kombinatorik und Wahrscheinlichkeitstheorie
1193
C.1 Kombinatorik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1193 C.2 Wahrscheinlichkeiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1199 C.3 Diskrete Zufallsvariablen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1205 C.4 Die geometrische Verteilung und die Binomialverteilung . . . . . . . . . . . . . . . 1211 C.5 ∗ Die Ränder der Binomialverteilung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1217 D Matrizen
1227
D.1 Matrizen und Matrizenoperationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1227 D.2 Elementare Matrizeneigenschaften . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1232 Literaturverzeichnis
1241
Index
1265
Vorwort Bevor es Rechner gab, gab es Algorithmen. Aber jetzt, wo es Rechner gibt, gibt es sogar noch mehr Algorithmen. Die elektronische Datenverarbeitung beruht auf Algorithmen. Dieses Buch bietet eine umfassende Einführung in das moderne Studium von Algorithmen. Es stellt viele Algorithmen vor, behandelt sie detailliert und macht zudem deren Entwurf und deren Analyse allen Leserschichten zugänglich. Wir haben uns bemüht, Erklärungen elementar zu halten, ohne auf Tiefe oder mathematische Exaktheit zu verzichten. Jedes Kapitel stellt einen Algorithmus, eine Entwurfstechnik, ein Anwendungsgebiet oder ein verwandtes Thema vor. Algorithmen werden in deutscher Sprache beschrieben und in Pseudocode entworfen, der für jeden lesbar sein sollte, der schon selbst ein wenig programmiert hat. Das Buch enthält 244 Abbildungen – viele bestehen aus mehreren Teilen – die verdeutlichen, wie die Algorithmen arbeiten. Da wir Effizienz als Entwurfskriterium betonen, schließen wir eine sorgfältige Analyse der Laufzeiten all unserer Programme mit ein. Der Text ist in erster Linie für den Gebrauch in Grundvorlesungen und graduierten Lehrveranstaltungen zu Algorithmen und Datenstrukturen gedacht. Da er sowohl technische Fragen als auch mathematische Aspekte des Algorithmenentwurfs diskutiert, ist er ebenso zum Selbststudium für Menschen mit technischen Berufen geeignet. In dieser dritten Auflage haben wir abermals das gesamte Buch aktualisiert. Die Änderungen sind vielfältig und umfassen insbesondere neue Kapitel, überarbeiteter Pseudocode und einen lebhafteren Schreibstil.1
An die Lehrenden Wir haben das Buch so gestaltet, dass es sowohl vielseitig als auch vollständig ist. Es sollte für Sie für eine Vielzahl von Lehrveranstaltungen von Nutzen sein, sowohl in einer Lehrveranstaltung zu Datenstrukturen für Studienanfänger als auch in einem Graduiertenkurs über Algorithmen. Da wir erheblich mehr Stoff bereitgestellt haben als eine typische, einsemestrige Veranstaltung umfasst, können Sie dieses Buch als eine Art „Buffet“ oder „bunte Mischung“ betrachten, aus der Sie denjenigen Stoff auswählen 1 Die vorliegende deutsche Übersetzung enthält nicht nur die in der dritten Auflage des englischen Buches enthaltenen Änderungen. Wir haben auch die alte deutsche Auflage auf umständliche Übersetzungen durchforstet und sie entsprechend überarbeitet. Darüber hinaus sind in dem vorliegenden Druck Korrekturen aller Fehler aus dem Errata des englischen Originals (siehe http://www.cs.dartmouth.edu/ thc/clrs-bugs/bugs-3e.php) bis einschließlich 24.02.2013 eingepflegt.
XIV
Vorwort
und entnehmen können, der die Lehrveranstaltung, die Sie halten möchten, am besten unterstützt. Es sollte Ihnen leicht fallen, Ihre Lehrveranstaltung einfach aus den Kapiteln aufzubauen, die sie benötigen. Wir haben die Kapitel relativ eigenständig gestaltet, sodass Sie sich keine Gedanken über unerwartete und unnötige Abhängigkeiten eines Kapitels gegenüber einem anderen machen müssen. Jedes Kapitel stellt zuerst den einfacheren Stoff bereit, später dann die komplizierteren Sachverhalte, wobei Abschnitte logische Zusammenhänge markieren. Innerhalb einer Lehrveranstaltung für Studienanfänger werden Sie möglicherweise nur die ersten Abschnitte eines Kapitels verwenden; innerhalb einer Graduiertenveranstaltung könnten Sie das gesamte Kapitel behandeln. Wir haben insgesamt 957 Übungen und 158 Problemstellungen eingebunden. Jeder Abschnitt endet mit Übungen, Kapitel schließen mit Problemstellungen ab. Die Übungen sind im Allgemeinen kurze Fragen, die das Beherrschen des Lehrstoffs testen. Einige sind einfache, als Selbsttest gedachte Aufgaben, während andere wesentlich umfangreicher und als Hausarbeiten geeignet sind. Die Problemstellungen sind aufwendigere Fallstudien, die häufig neuen Stoff einführen; sie bestehen häufig aus mehreren Fragen, die die Studierenden durch die zum Erhalt der Lösung notwendigen Schritte führen. Aus der Erfahrung mit den vorherigen Auflagen dieses Buches haben wir Lösungen zu einigen, aber beileibe nicht allen Problemstellungen und Übungen öffentlich zugänglich gemacht. Sie können über unsere Webseite http://mitpress.mit.edu/algorithms auf diese zugreifen. Sie sollten sich die Webseite anschauen, um sicher zu gehen, dass sie nicht eine Lösung einer Übungsaufgabe oder einer Problemstellung enthält, die Sie stellen wollen. Wir erwarten, dass sich die Menge der veröffentlichten Lösungen kontinuierlich über die Zeit vergrößern wird, sodass Sie sich die Webseite jedesmal anschauen sollten, wenn Sie die Lehrveranstaltung anbieten. Wir haben diejenigen Abschnitte und Übungen mit einem Stern (∗) versehen, die eher für fortgeschrittene Studierende als für Studienanfänger geeignet sind. Ein mit Stern versehener Abschnitt ist nicht notwendigerweise schwieriger als ein Abschnitt ohne Stern, er kann aber mehr Verständnis von höherer Mathematik erfordern. Ebenso kann eine mit Stern versehene Übung ein höheres Niveau oder mehr als nur durchschnittliche Kreativität voraussetzen.
An die Studierenden Wir hoffen, dass Ihnen dieses Lehrbuch eine unterhaltsame Einführung in das Gebiet der Algorithmen liefert. Wir haben uns bemüht, jeden Algorithmus leicht zugänglich und interessant zu gestalten. Um Ihnen zu helfen, wenn Sie auf ungewohnte oder schwierige Algorithmen stoßen, beschreiben wir jeden Algorithmus Schritt für Schritt. Wir liefern außerdem sorgfältige Erklärungen zur Mathematik, die notwendig ist, um die Analyse der Algorithmen zu verstehen. Wenn Sie bereits über einige Vorkenntnisse auf einem Gebiet verfügen, dann werden Sie feststellen, dass die Kapitel so eingerichtet sind, dass Sie die einführenden Abschnitte überfliegen und schnell mit dem höheren Stoff fortfahren können.
Vorwort
XV
Dies ist ein umfangreiches Buch und Ihre Vorlesung wird wahrscheinlich nur einen Teil des enthaltenen Stoffes behandeln. Wir haben uns bemüht, dieses Buch so zu gestalten, dass es für Sie jetzt als Lehrbuch und später während Ihrer Karriere auch als mathematisches Nachschlagewerk oder als technisches Handbuch nützlich sein wird. Was sind die fachlichen Voraussetzungen für die Lektüre dieses Buches? • Sie sollten etwas Programmiererfahrung haben. Insbesondere sollten Sie rekursive Prozeduren und einfache Datenstrukturen, wie zum Beispiel Felder und verkettete Listen, verstehen. • Sie sollten Übung mit mathematischen Beweisen, im besonderen mit mathematischer Induktion haben. Einige Teile dieses Buches stützen sich auf Kenntnisse in elementarer Analysis. Darüber hinaus vermitteln Ihnen die Teile I und VIII dieses Buches alle mathematischen Methoden, die Sie benötigen werden. Wir haben laut und deutlich Ihre Forderung gehört, Lösungen zu Problemstellungen und Übungen bereitzustellen. Unsere Webseite http://mitpress.mit.edu/algorithms verlinkt zu Lösungen für einige der Problemstellungen und Übungen. Überprüfen Sie Ihre Lösungen mit unseren. Wir bitten aber darum, uns Ihre Lösungen nicht zuzuschicken.
An die Fachleute Die große Auswahl an Themen in diesem Buch macht dieses zu einem ausgezeichneten Handbuch zum Thema Algorithmen. Da jedes Kapitel in sich relativ geschlossen ist, können Sie sich auf die Themen konzentrieren, die Sie am meisten interessieren. Die meisten der hier besprochenen Algorithmen haben großen praktischen Nutzen. Daher befassen wir uns auch mit den Belangen der Implementierung und anderen technischen Fragen. Wir stellen in der Regel praktische Alternativen zu den wenigen Algorithmen vor, die vorrangig von theoretischem Interesse sind. Falls Sie einen der Algorithmen implementieren möchten, dann sollte es für Sie ziemlich einfach sein, den Pseudocode in Ihre bevorzugte Programmiersprache zu übersetzen. Wir haben den Pseudocode so entworfen, dass er jeden Algorithmus klar und knapp präsentiert. Folglich befassen wir uns nicht mit Fehlerbehandlung und anderen softwaretechnischen Fragen, die spezielle Annahmen über Ihre Programmierumgebung erfordern. Wir versuchen, jeden Algorithmus einfach und genau darzustellen, ohne es den Eigenheiten einer speziellen Programmiersprache zu ermöglichen, dessen Wesen zu verdecken. Wir sehen ein, dass Sie Ihre Lösungen zu Problemen und Übungen nicht mit den von einem Dozenten zur Verfügung gestellten Lösungen vergleichen können, wenn Sie dieses Buch im Selbststudium benutzen. Unsere Webseite http://mitpress.mit.edu/algorithms verlinkt zu Lösungen für einige der Problemstellungen und Übungen, sodass Sie Ihre Lösungen überprüfen können. Senden Sie uns aber bitte nicht Ihre Lösungen zu.
XVI
Vorwort
An unsere Kollegen Wir haben ein umfangreiches Quellenverzeichnis und Hinweise auf die aktuelle Literatur bereitgestellt. Jedes Kapitel endet mit einer Reihe von Kapitelbemerkungen, die historische Details und Hinweise geben. Die Kapitelbemerkungen bilden dennoch keine vollständige Referenz zum gesamten Gebiet der Algorithmen. Obwohl es von einem Buch dieses Umfangs schwer zu glauben sein mag, hinderte Platzmangel uns daran, viele interessante Algorithmen mit aufzunehmen.
Änderungen innerhalb der dritten Auflage Was hat sich in der dritten Auflage in Bezug auf die zweite geändert? Die Anzahl der Änderungen entspricht in etwa der bei der zweiten Auflage. Wie wir bereits in Bezug auf diese Änderungen gesagt haben, hat sich das Buch also kaum oder ziemlich viel geändert. Ein kurzer Blick in das Inhaltsverzeichnis zeigt, dass die meisten Kapitel und Abschnitte der zweiten Auflage auch in der dritten Auflage vorkommen. Wir haben zwei Kapitel und einen Abschnitt entfernt, haben dafür drei neue Kapitel und ansonsten zwei neue Abschnitte hinzugefügt. Wir haben die gemischte Organisation der ersten beiden Auflagen beibehalten. Anstatt die Kapitel ausschließlich nach Problembereichen oder ausschließlich nach Techniken zu gliedern, findet man in diesem Buch beides vor. Es enthält technikbasierte Kapitel wie die Kapitel über die Teile-und-Beherrsche-Methode, dynamische Programmierung, Greedy-Algorithmen, amortisierte Analyse, NP-Vollständigkeit und Approximationsalgorithmen. Es enthält jedoch auch jeweils ganze Teile über Sortieren, Datenstrukturen für dynamische Mengen und Graphalgorithmen. Dies ist dadurch begründet, dass, wenngleich Sie wissen müssen, wie Techniken zum Entwurf oder zur Analyse von Algorithmen anzuwenden sind, aus den Problemstellungen nicht unmittelbar für Sie zu ersehen ist, welche Technik am ehesten hilft, das Problem zu lösen. Wir geben eine Zusammenfassung der wichtigsten Änderungen in der dritten Auflage: • Wir haben neue Kapitel zu van-Emde-Boas-Bäume und mehrfädigen (engl.: multithreaded ) Algorithmen eingefügt. Die Grundlagen von Matrizen haben wir in den Anhang verschoben. • Wir haben das Kapitel zu Rekursionsgleichungen überarbeitet, sodass es nunmehr die Teile-und-Beherrsche-Methode besser abdeckt. Die ersten zwei Abschnitte wenden die Teile-und-Beherrsche-Methode an, um zwei verschiedene Problemstellungen zu lösen. Der zweite Abschnitt dieses Kapitels stellt Strassens Algorithmus zur Matrixmultiplikation vor, den wir aus dem Kapitel über Matrixoperationen hierhin verschoben haben. • Wir haben zwei Kapitel, die nur selten gelehrt werden, entfernt: binomiale Heaps und Sortiernetzwerke. Eine zentrale Idee bei Sortiernetzwerken, das 0-1-Prinzip,
Vorwort
XVII
findet sich in dieser Auflage als 0-1-Sortierlemma für Vergleiche-Vertausche Algorithmen in der Problemstellung 8-7 wieder. Die Behandlung von Fibonacci-Heaps beruht nicht mehr auf binomialen Heaps. • Wir haben unsere Betrachtungen zu dynamischer Programmierung und GreedyAlgorithmen überarbeitet. Dynamische Programmierung leiten wir nun ab mit einem interessanteren Problem, dem Schneiden von Stahlstangen, als mit der Ablaufkoordinierung von Montagebändern aus der zweiten Auflage. Zudem heben wir ein bisschen mehr auf Memoisation ab, als wir dies in der zweiten Auflage gemacht haben, und führen den Begriff des Teilproblem-Graphen als eine Möglichkeit, die Laufzeit eines auf dynamischer Programmierung beruhender Algorithmus zu verstehen, ein. In unserem Anfangsbeispiel zu Greedy-Algorithmen, dem Aktivitäten-Auswahl-Problem, leiten wir Greedy-Algorithmen direkter ab, als wir dies in der zweiten Auflage getan haben. • Die Methode, mit der wir einen Knoten aus einem binären Suchbaum (insbesondere aus Rot-Schwarz-Bäumen) löschen, gewährleistet nun, dass genau der Knoten, der gelöscht werden soll, auch tatsächlich gelöscht wird. In den ersten beiden Auflagen konnte in bestimmten Fällen ein anderer Knoten gelöscht werden, wobei zuvor dessen Inhalt in den Knoten kopiert wurde, der der Lösche-Prozedur an sich übergeben worden war. Mit dieser neuen Methode, Knoten zu löschen, kann es nicht mehr passieren, dass andere Programmteile, die Zeiger auf Knoten des Baumes verwalten, irrtümlicherweise alte Zeiger auf Knoten, die gelöscht wurden, haben. • Die Ausführungen zu Flussnetzwerken basieren nun vollständig auf Flüssen auf Kanten. Dieser Ansatz ist intuitiver als der über den Nettofluss aus den ersten beiden Auflagen. • Da wir die Ausführungen zu den Grundlagen von Matrizen und zu Strassens Algorithmus in andere Kapitel verschoben haben, ist das Kapitel zu Matrixoperationen kürzer als in der zweiten Auflage. • Wir haben die Behandlung des Knuth-Morris-Patt String-Matching Algorithmus abgeändert. • Wir haben mehrere Fehler korrigiert. Die meisten von ihnen, wenn auch nicht alle, wurden uns über das auf unserer Webseite verfügbare Fehlerverzeichnis zugeschickt. • Aufgrund vieler Anfragen haben wir die (bisherige) Syntax unseres Pseudocodes geändert. Wir benutzen nun, wie dies auch in C, C++, Java und Python der Fall ist, “ = ” für eine Zuweisung und “= =” für den Test auf Gleichheit. Desgleichen haben wir die Schlüsselwörter do und then entfernt und übernehmen “//” als unser Kommentarsymbol, wenn der Rest der entsprechende Zeile Kommentar sein soll. Zudem benutzen wir Punktnotation für die Angabe von Objektattributen. Unser Pseudocode bleibt prozedural. Anders formuliert, anstatt Methoden auf Objekten laufen zu lassen, rufen wir einfach Prozeduren auf, denen wir Objekte als Parameter mitgeben.
XVIII
Vorwort
• Wir haben 100 neue Übungsaufgaben und 28 neue Problemstellungen eingefügt. Wir haben auch viele Bibliographieeinträge aktualisiert und mehrere neue hinzugefügt. • Schlussendlich sind wir das ganze Buch durchgegangen und haben Sätze, Absätze und Abschnitte umgeschrieben, damit die Ausführungen klarer werden und der Schreibstil lebhafter wird.
Internetpräsenz Sie können unsere Webseite http://mitpress.mit.edu/algorithms/ nutzen, um zusätzliche Informationen zu bekommen und um mit uns zu kommunizieren. Die Webseite verlinkt zu einer Liste bekannter Fehler, zu Lösungen ausgewählter Übungsaufgaben und Problemen sowie zu anderem Inhalt, den wir gegebenenfalls noch bereitstellen werden. Zudem erklärt sie (natürlich) die blöden Professorenwitze. Die Webseite sagt Ihnen auch, wie Sie uns Fehler oder Anregungen zukommen lassen können.
Wie wir das Buch hergestellt haben2 Wie die zweite Auflage haben wir auch die dritte Auflage mit LATEX 2ε erstellt. Wir benutzten den Times Font mit dem auf den MathTime Pro 2 Fonts basierenden mathematischen Schriftsatz. Wir bedanken uns für die entsprechende technische Unterstützung bei Michael Spivak von Publish or Perish, Inc., Lance Carnes von Personal TeX, Inc., und Tim Tregubov vom Dartmouth College. Wie in den vorangehenden zwei Auflagen haben wir den Index mit Windex, einem C Programm, das wir geschrieben haben, übersetzt. Das Literaturverzeichnis wurde mit BibTEX erzeugt. Die PDF-Dateien für dieses Buch wurden auf einem MacBook unter OS 10.5 generiert. Die Abbildungen der dritten Auflage haben wir mit MacDraw Pro gezeichnet, unter Zuhilfenahme des psfrag Pakets von LATEX 2ε zur Darstellung mathematischer Ausdrücke in den Abbildungen. Leider ist MacDraw Pro eine alte Software, die seit über einem Jahrzehnt nicht mehr verkauft wird. Glücklicherweise haben wir noch eine Handvoll Macintoshes, die unter dem Betriebssystem OS 10.4 laufen und so MacDraw Pro ausführen können – jedenfalls meistens. Sogar unter dieser alten Umgebung finden wir MacDraw Pro um ein Vielfaches einfacher zu benutzen als irgendeine andere Zeichensoftware, wenn es um Zeichnungen geht, die Informatiktexte begleiten sollen. Zudem erzeugt dieses Programm schöne Ausgaben.3 Wer weiß, wie lange unsere alten Macs noch laufen werden; also falls jemand von Apple zuhört: Bitte bauen Sie eine MacDraw Pro Version, die unter OS X läuft! 2 Diese
Ausführungen beziehen sich zum Teil nur auf die dritte Auflage der englischen Ausgabe. haben uns mehrere Zeichenprogramme, die unter Mac OS X laufen, angeschaut, aber alle haben erhebliche Nachteile verglichen mit MacDraw Pro. Wir haben kurz versucht, die Abbildungen für dieses Buch mit einem anderen, gut bekannten Zeichenprogramm zu erstellen. Es stellte sich heraus, dass es wenigstens fünf Mal so lange dauert, eine Abbildung zu erzeugen, als mit MacDraw Pro, und das Ergebnis sah schlechter aus. Aus diesem Grund unsere Entscheidung, zu den alten Macintoshes und MacDraw Pro zurückzukehren. 3 Wir
Vorwort
XIX
Danksagungen zur dritten Auflage Wir arbeiten nunmehr seit über zwei Jahrzehnte mit The MIT Press zusammen und was war das für eine wunderbare Zusammenarbeit! Wir danken Ellen Faran, Bob Prior, Ada Brunstein und Mary Reilly für ihre Hilfe und Unterstützung. Während der Erstellung der dritten Auflage arbeiteten wir an geografisch unterschiedlichen Orten, am Dartmouth College Department of Computer Science, im MIT Computer Science and Artificial Intelligence Laboratory, und am Columbia University Department of Industrial Engineering and Operations Research. Wir danken unseren jeweiligen Universitäten und Kollegen, dass wir in dermaßen unterstützenden und anregenden Umgebungen arbeiten konnten. Julie Sussman, P.P.A., stand uns erneut als Lektorin zur Verfügung. Wieder und wieder waren wir verblüfft über die Fehler, die wir übersehen hatten, die Julie aber fand. Sie half uns auch, die Darstellung an mehreren Stellen zu verbessern. Gäbe es eine Ruhmeshalle für Lektorinnen und Lektoren, Julie wäre ein Platz sicher. Sie ist einfach nur phänomenal. Danke, danke, danke, Julie! Priya Natarajan fand ebenfalls einige Fehler, die wir noch vor Drucklegung korrigieren konnten. Alle Fehler, die sich im Buch noch befinden (und es werden sicherlich welche geben) liegen in der Verantwortung der Autoren (und wurden vermutlich eingefügt, nachdem Julie den Text gelesen hat). Die Behandlung der van-Emde-Boas-Bäume basiert auf Notizen von Erik Demaine, die wiederum durch Michael Bender beeinflusst sind. Wir haben auch Ideen von Javed Aslam, Bradley Kuszmaul und Hui Zha in diese Auflage eingearbeitet. Das Kapitel über Multithreading basiert auf Notizen, die ursprünglich zusammen mit Harald Prokop geschrieben worden sind, und wurde beeinflusst durch mehrere andere Kollegen, die am Cilk Projekt des MIT beteiligt sind. Zu nennen sind insbesondere Bradley Kuszmaul und Matteo Frigo. Der Entwurf des mehrfädigen Pseudocodes wurde durch die MIT Cilk Erweiterung von C und die Cilk Arts Cilk++ Erweiterung von C++ inspiriert. Wir bedanken uns auch bei den vielen Lesern der ersten und zweiten Auflage, die uns auf Fehler aufmerksam machten oder uns Anregungen zukommen ließen, wie dieses Buch verbessert werden könnte. Wir korrigierten alle echten Fehler, die uns gemeldet worden sind, und haben so viele Anregungen, wie wir konnten, eingebunden. Wir sind glücklich darüber, dass die Anzahl dieser Mitwirkenden so groß geworden ist, dass wir bedauern müssen, dass es nicht mehr möglich ist, alle aufzuzählen.
XX
Vorwort
Zu guter Letzt danken wir unseren Frauen – Nicole Cormen, Wendy Leiserson, Gail Rivest, and Rebecca Ivry – und unseren Kindern – Ricky, Will, Debby und Katie Leiserson; Alex und Christopher Rivest; und Molly, Noah und Benjamin Stein – für ihre Liebe und Unterstützung, während wir das Buch geschrieben haben. Die Geduld und der Zuspruch unserer Familien machten dieses Projekt möglich. Wir widmen ihnen liebevoll dieses Buch.
Thomas H. Cormen Charles E. Leiserson Ronald L. Rivest Clifford Stein Februar 2009
Lebanon, New Hampshire Cambridge, Massachusetts Cambridge, Massachusetts New York, New York
Teil I
Grundlagen
Einführung Dieser Teil wird Sie an den Entwurf und die Analyse von Algorithmen heranführen. Er ist als behutsame Einführung gedacht – eine Einführung zu wie wir in diesem Buch Algorithmen spezifizieren, zu einigen Entwurfsstrategien, die wir innerhalb dieses Buches verwenden werden, und zu vielen fundamentalen Begriffen, die bei der Analyse von Algorithmen benutzt werden. Die nachfolgenden Teile des Buches werden auf dieser Grundlage aufbauen. Kapitel 1 gibt einen Überblick über Algorithmen und ihre Rolle in modernen Rechensystemen. Dieses Kapitel definiert, was wir unter einem Algorithmus verstehen, und listet einige Beispiele auf. Es liefert Argumente dafür, dass wir Algorithmen als eine Technologie ansehen sollten, genauso wie schnelle Hardware, grafische Benutzeroberflächen, objektorientierte Systeme und Netzwerke Technologien sind. In Kapitel 2 werden wir unsere ersten Algorithmen kennen lernen. Sie behandeln das Problem, n Zahlen zu sortieren. Die Algorithmen sind in Pseudocode beschrieben, der, obwohl er nicht unmittelbar in eine konventionelle Programmiersprache übersetzbar ist, die Struktur eines Algorithmus hinreichend klar vermittelt, sodass Sie ihn in einer Programmiersprache Ihrer Wahl implementieren können sollten. Die behandelten Sortieralgorithmen sind Sortieren durch Einfügen (engl.: insertion sort), das eine iterative Methode benutzt, und Sortieren durch Mischen (engl.: merge sort ), das eine als „Teile-und-Beherrsche“ (engl.: divide and conquer ) bekannte rekursive Technik verwendet. Obwohl die von beiden benötigte Zeit mit dem Wert n anwächst, unterscheiden sich die Wachstumsraten beider Algorithmen voneinander. Wir bestimmen deren Laufzeiten im Kapitel 2 und entwickeln eine nützliche Notation, um diese zu beschreiben. Kapitel 3 legt diese Notation genau fest, die wir als asymptotische Notation bezeichnen werden. Kapitel 3 beginnt mit der Definition einiger asymptotischer Notationen, die wir benutzen, um obere und/oder untere Schranken für Laufzeiten von Algorithmen anzugeben. Der Rest von Kapitel 3 ist in erster Linie eine Vorstellung mathematischer Notationen, mit dem vorrangigen Ziel, Ihnen unsere mathematischen Notationen nahe zu bringen, und nicht Ihnen neue Konzepte zu vermitteln. Kapitel 4 befasst sich eingehender mit der Teile-und-Beherrsche-Methode, die bereits in Kapitel 2 eingeführt wurde. Es zeigt weitere Beispiele von Teile-und-BeherrscheAlgorithmen, insbesondere Strassens erstaunliche Methode zur Multiplikation zweier n × n-Matrizen. Kapitel 4 enthält Methoden zum Auflösen von Rekursionsgleichungen, die für die Beschreibung von Laufzeiten rekursiver Algorithmen nützlich sind. Eine sehr leistungsfähige Methode ist die „Mastermethode“, die wir oft beim Lösen von Rekursionsgleichungen, die bei Teile-und-Beherrsche-Algorithmen auftreten, anwenden. Wenngleich ein großer Teil von Kapitel 4 sich mit dem Korrektheitsbeweis der Mastermethode beschäftigt, können Sie den Beweis überspringen und doch die Mastermethode anwenden.
4
Teil I Grundlagen
Kapitel 5 führt die probabilistische Analyse und randomisierte Algorithmen ein. Üblicherweise benutzen wir die probabilistische Analyse, um die Laufzeit eines Algorithmus zu bestimmen, wenn es sich um einen Algorithmus handelt, dessen Laufzeit bei Eingaben gleicher Größe aufgrund einer inhärenten Wahrscheinlichkeitsverteilung unterschiedlich sein kann. In einigen Fällen gehen wir davon aus, dass die Eingaben gemäß einer bekannten Wahrscheinlichkeitsverteilung verteilt sind, sodass wir die Laufzeit über alle möglichen Eingaben mitteln. In anderen Fällen ergibt sich die Wahrscheinlichkeitsverteilung nicht aus den Eingaben, sondern aus Zufallsentscheidungen, die während des Ablaufs des Algorithmus getroffen werden. Einen Algorithmus, dessen Verhalten nicht nur durch seine Eingabe bestimmt ist, sondern auch durch von einem Zufallszahlengenerator erzeugte Größen, nennt man randomisierter Algorithmus. Wir können randomisierte Algorithmen benutzen, um den Eingaben eine Wahrscheinlichkeitsverteilung aufzuerlegen und damit abzusichern, dass keine spezielle Eingabe eine schwache Performanz verursacht. Wir können die Idee der Randomisierung auch benutzen, um die Fehlerrate von Algorithmen zu begrenzen, bei denen es zulässig ist, dass sie eine beschränkte Zahl inkorrekter Resultate liefern. Die Anhänge A–D enthalten zusätzlichen Stoff, der Ihnen hilfreich sein kann, wenn Sie dieses Buch lesen. Sie werden wahrscheinlich einen Großteil des Stoffes in den Kapiteln des Anhangs bereits kennen, auch wenn die verwendeten Definitionen und Schreibweisen an einigen Stellen möglicherweise von denen Ihnen bisher bekannten abweichen. Sie sollten deshalb die Anhänge als Referenzmaterial betrachten. Andererseits wird Ihnen wahrscheinlich ein großer Teil des Stoffes aus Teil I noch nicht vertraut sein. Alle Kapitel in Teil I und in den Anhängen haben einen einführenden Charakter.
1
Die Rolle von Algorithmen in der elektronischen Datenverarbeitung
Was sind Algorithmen? Warum ist das Studium von Algorithmen lohnenswert? Welche Rolle spielen Algorithmen im Vergleich zu anderen, in Rechnern verwendeten Technologien? Diese Fragen werden wir in diesem Kapitel beantworten.
1.1
Algorithmen
Ein Algorithmus ist grob gesprochen eine wohldefinierte Rechenvorschrift, die eine Größe oder eine Menge von Größen als Eingabe verwendet und eine Größe oder eine Menge von Größen als Ausgabe erzeugt. Somit ist ein Algorithmus eine Folge von Rechenschritten, die die Eingabe in die Ausgabe umwandeln. Wir können einen Algorithmus auch als Hilfsmittel betrachten, um ein genau festgelegtes Rechenproblem zu lösen. Die Formulierung des Problems legt in allgemeiner Form die benötigte Eingabe-Ausgabe-Beziehung fest. Der Algorithmus beschreibt eine spezifische Rechenvorschrift, die diese Eingabe-Ausgabe-Beziehung erzeugt. Es kann zum Beispiel sein, dass wir eine Folge von Zahlen in nichtfallender Reihenfolgen sortieren müssen. Dieses Problem kommt in der Praxis häufig vor und bietet eine ergiebige Grundlage für die Einführung vieler standardmäßiger Entwurfstechniken und Analysewerkzeuge. Das Sortierproblem definieren wir formal wie folgt: Eingabe: Eine Folge von n Zahlen a1 , a2 , . . . , an . Ausgabe: Eine Permutation (Umordnung) a1 , a2 , . . . , an der Eingabefolge, sodass a1 ≤ a2 ≤ · · · ≤ an gilt. Ist zum Beispiel eine Eingabefolge 31, 41, 59, 26, 41, 58 gegeben, so gibt ein Sortieralgorithmus die Folge 26, 31, 41, 41, 58, 59 als Ausgabe zurück. Solch eine Eingabefolge wird als Instanz des Sortierproblems bezeichnet. Im Allgemeinen besteht eine Instanz eines Problems aus einer Eingabe, die jegliche durch die Formulierung des Problems auferlegten Bedingungen erfüllt und alle notwendigen Daten umfasst, um die Lösung des Problems berechnen zu können. Da viele Programme Daten in Zwischenschritten sortieren müssen, ist Sortieren eine Operation in der Informatik, die elementar ist. Infolgedessen stehen uns heute eine
6
1 Die Rolle von Algorithmen in der elektronischen Datenverarbeitung
Vielzahl guter Sortieralgorithmen zur Verfügung. Welcher Algorithmus der beste für eine gegebene Anwendung ist, hängt – neben anderen Faktoren – von der Zahl der zu sortierenden Daten, dem Umfang, in dem die Daten bereits vorsortiert sind, möglichen Einschränkungen, denen die Daten unterliegen, der Architektur des Rechners und der Art des eingesetzten Speichers (Hauptspeicher, Festplatten oder Magnetbänder) ab. Ein Algorithmus wird als korrekt bezeichnet, wenn er für jede Eingabeinstanz mit der korrekten Ausgabe stoppt. Wir sagen, dass ein korrekter Algorithmus ein gegebenes Rechenproblem löst. Ein inkorrekter Algorithmus stoppt bei einigen Eingabeinstanzen möglicherweise überhaupt nicht, oder er kann mit einer falschen Antwort anhalten. Im Gegensatz zu dem, was Sie erwarten würden, können inkorrekte Algorithmen manchmal nützlich sein, falls wir deren Fehlerrate kontrollieren können. Wir werden uns in Kapitel 31 ein Beispiel ansehen, wenn wir Algorithmen zum Auffinden von großen Primzahlen untersuchen. Gewöhnlich werden wir uns jedoch nur mit korrekten Algorithmen zu beschäftigen haben. Ein Algorithmus kann mithilfe der natürlichen Sprache, als Computerprogramm oder sogar als Hardwareentwurf beschrieben werden. Die einzige Anforderung ist, dass die Spezifikation eine genaue Beschreibung der zu befolgenden Rechenvorschrift enthalten muss.
Welche Art von Problemen werden durch Algorithmen gelöst? Sortieren ist keineswegs das einzige Rechenproblem, für das Algorithmen entwickelt wurden. (Was Sie bei dem Umfang des Buches wahrscheinlich bereits geahnt haben.) Praktische Anwendungen von Algorithmen sind ubiquitär und schließen die folgenden Beispiele ein: • Das Human-Genome-Projekt hat große Fortschritte in Bezug auf das Ziel gemacht, alle 100 000 Gene der menschlichen DNA zu identifizieren, die Folge der 3 Milliarden chemischen Basenpaare, die eine menschliche DNA ausmachen, zu bestimmen, diese Informationen in einer Datenbank zu speichern und Werkzeuge zur Datenanalyse zu entwickeln. Jeder dieser Schritte erfordert anspruchsvolle Algorithmen. Obwohl die Lösungen der damit zusammenhängenden mannigfaltigen Probleme weit über den Rahmen dieses Buches hinausgehen, benutzen viele Methoden zur Lösung biologischer Probleme Ideen aus mehreren Kapiteln dieses Buches. Diese ermöglichen es den Wissenschaftlern, die Aufgaben unter effizienter Nutzung der Ressourcen zu bewältigen. Die Einsparungen liegen in der aufgewendeten Zeit, sowohl der menschlichen als auch der maschinellen, und dem Geld, da mehr Informationen aus Labortechniken gewonnen werden können. • Das Internet macht Menschen auf der ganzen Welt eine große Menge an Informationen zugänglich und erlaubt ihnen, schnell darauf zuzugreifen. Mit Hilfe intelligenter Algorithmen können entsprechende Internetanbieter diese großen Datenmengen handhaben. Beispiele von Problemen, bei denen Algorithmen das A und O sind, sind insbesondere das Berechnen guter Routen für den Datentransport (Methoden zur Lösung solcher Probleme sind in Kapitel 24 zu finden) und die Verwendung von Suchmaschinen, um schnell diejenigen Seiten zu finden, die eine
1.1 Algorithmen
7
bestimmte Information enthalten (entsprechende Methoden befinden sich in den Kapiteln 11 und 32). • E-Commerce ermöglicht den elektronischen Handel von Waren und Dienstleistungen. Er hängt vom Schutz privater Informationen wie Kreditkartennummern, Passwörter und Bankauszügen ab. Die im elektronischen Handel benutzten Schlüsseltechnologien sind insbesondere Public-Key-Kryptographie und digitale Unterschriften, die in Kapitel 31 behandelt werden. Sie beruhen auf numerischen Algorithmen und Zahlentheorie. • Verarbeitende Industrie und andere Unternehmen haben oft knappe Ressourcen bestmöglichst einzusetzen. Ein Ölkonzern wird wissen wollen, wo er seine Bohrlöcher platzieren soll, um den erwarteten Ertrag zu maximieren. Ein Politiker möchte entscheiden, an welcher Stelle er das Geld für Wahlwerbung ausgeben soll, um die Chancen für den Gewinn einer Wahl zu maximieren. Eine Fluggesellschaft möchte die Crews für Flüge so zusammenstellen, dass nur minimale Kosten entstehen. Es muss sichergestellt sein, dass jeder Flug abgedeckt ist und dass die staatlichen Vorschriften über die Einsatzzeiten für jede Crew eingehalten werden. Ein Internet-Service-Provider möchte bestimmen, wo zusätzliche Ressourcen eingesetzt werden sollen, um seine Kunden effizienter zu bedienen. All dies sind Beispiele für Probleme, die mit Hilfe linearer Programmierung gelöst werden können. Lineare Programmierung wird in Kapitel 29 untersucht. Obwohl einige Details dieser Beispiele über den Umfang dieses Buches hinausgehen, geben wir doch die diesen Problemen und Problemgebieten zugrunde liegenden Methoden an. Wir zeigen in diesem Buch auch, wie verschiedene konkrete Probleme gelöst werden können, darunter die folgenden: • Gegeben ist eine Straßenkarte, auf der der Abstand zwischen jedem Paar benachbarter Kreuzungen eingezeichnet ist. Wir wollen den kürzesten Weg von einer Kreuzung zu einer anderen finden. Die Zahl der möglichen Wege kann riesig sein, selbst wenn wir Wege, die sich selbst kreuzen, verwerfen. Wie entscheiden wir tatsächlich, welcher der möglichen Wege der kürzeste ist? Wir modellieren in diesem Fall die Straßenkarte (die selbst nur ein Modell für die tatsächlichen Straßen ist) durch einen Graphen (siehe Teil VI und Anhang B) und wollen innerhalb des Graphen den kürzesten Pfad von einem Knoten zu einem anderen finden. In Kapitel 24 werden wir sehen, wie dieses Problem effizient gelöst werden kann. • Gegeben seien zwei geordnete Folgen X = x1 , x2 , . . . , xm and Y = y1 , y2 , . . . , yn von Symbolen und wir möchten eine längste gemeinsame Teilfolge von X und Y finden. Eine Teilfolge von X ist einfach nur X, aus der einige (möglicherweise alle oder auch keines) ihrer Elemente entfernt worden sind. Zum Beispiel ist eine Teilfolge von A, B, C, D, E, F, G die Folge B, C, E, G. Die Länge einer längsten gemeinsamen Teilfolge von X und Y ist ein Maß, das angibt, wie ähnlich sich diese beiden Folgen sind. Sind die beiden Folgen Basenpaare in DNA-Strängen, so können wir diese Stränge als ähnlich ansehen, wenn sie eine lange gemeinsame Teilfolge besitzen. Besteht X aus m Symbolen und Y aus n Symbolen, dann besitzen X und Y genau 2m beziehungsweise 2n mögliche Teilfolgen. Alle Teilfolgen
8
1 Die Rolle von Algorithmen in der elektronischen Datenverarbeitung von X mit allen Teilfolgen von Y zu vergleichen, benötigt untragbar viel Zeit, es sei denn m und n sind sehr klein. Wir werden in Kapitel 15 sehen, wie dieses Problem mit einer allgemeinen Methode, die als dynamische Programmierung bekannt ist, gelöst werden kann. • Gegeben sei ein mechanischer Bauplan in Form einer Bibliothek von Teilen, wobei jedes Teil aus Instanzen anderer Teile aufgebaut sein kann. Wir benötigen eine Liste der Teile, in der jedes Teil vor jedem anderen Teil, bei dem es gebraucht wird, aufgezählt wird. Wenn der Bauplan aus n Teilen besteht, dann gibt es n! mögliche Reihenfolgen der Objekte, wobei n! die Fakultät von n bezeichnet. Da die Fakultätsfunktion sogar noch schneller wächst als die Exponentialfunktion, können wir nicht jede Reihenfolge betrachten und für diese überprüfen, ob sie die angegebene Eigenschaft erfüllt (es sei denn, der Bauplan besteht nur aus sehr wenigen Teilen). Diese Problemstellung ist eine Instanz des Problems des topologischen Sortierens. Wir werden in Kapitel 22 sehen, wie dieses Problem effizient gelöst werden kann. • Gegeben seien n Punkte in einer Ebene, und wir möchten die konvexe Hülle dieser Punkte finden. Die konvexe Hülle ist das kleinste konvexe Polygon, das die Punkte enthält. Anschaulich können wir uns jeden Punkt als einen Nagel vorstellen, der aus einer Tafel herausragt. Die konvexe Hülle würde durch ein enges Gummiband, das alle Nägel umschließt, dargestellt. Jeder Nagel, um den sich das Gummiband legt, ist eine Ecke der konvexen Hülle. (Ein Beispiel finden Sie in Abbildung 33.6 auf Seite 1040.) Jede der 2n Teilmengen der Punkte kann die Menge der Ecken der konvexen Hülle darstellen. Zudem reicht das Wissen, welche Punkte Ecken der konvexen Hülle sind, nicht ganz aus, da wir auch wissen müssen, in welcher Reihenfolge die Ecken auf der Hülle vorkommen. Folglich gibt es einen großen Suchraum, in dem wir die Lösung, sprich die Ecken der konvexen Hülle, finden müssen. Kapitel 33 stellt zwei effiziente Methoden zur Berechnung der konvexen Hülle vor.
Diese Auflistung ist weit davon entfernt, vollständig zu sein (was Sie wahrscheinlich aufgrund des Gewichtes des Buches bereits vermutet haben), zeigt aber zwei Charakteristiken, die vielen interessanten algorithmischen Problemen gemeinsam sind. 1. Sie besitzen viele Lösungskandidaten; die überwiegende Mehrheit von ihnen lösen das vorliegende Problem jedoch nicht. Einen Lösungskandidaten zu finden, der dies tut, oder der der „beste“ ist, kann eine ziemliche Herausforderung darstellen. 2. Sie haben praktische Anwendungen. Aus der Liste der obigen Problemstellungen liefert das Problem der kürzesten Wege uns die einfachsten Beispiele. Ein Transportunternehmen, wie zum Beispiel eine Speditionsfirma oder eine Eisenbahngesellschaft, hat ein finanzielles Interesse daran, die kürzesten Wege innerhalb eines Straßen- oder Schienennetzes zu finden, da das Benutzen kürzerer Wege zu niedrigerem Arbeitsaufwand und niedrigeren Treibstoffkosten führt. Oder ein RoutingKnoten muss den kürzesten Weg durch das Netzwerk finden, um eine Meldung schnell weiterleiten zu können. Oder eine Person, die von New York nach Boston
1.1 Algorithmen
9
fahren will, möchte eine Fahrroute von einer entsprechenden Webseite erhalten oder sie benutzt ihr GPS-Navigationsgerät während der Fahrt. Nicht jedes Problem, das durch einen Algorithmus gelöst werden kann, besitzt eine leicht zu identifizierende Menge von Lösungskandidaten. Wir wollen ein Beispiel angeben. Nehmen Sie an, wir hätten eine Menge von numerischen Werten gegeben, die Abtastpunkte eines Signals darstellen, und wir wollen die diskrete Fourier-Transformierte dieser Punkte berechnen. Die diskrete Fourier-Transformation erlaubt die Transformation des Zeitbereiches in den Frequenzbereich, indem numerische Koeffizienten berechnet werden, mit denen wir die unterschiedlichen Frequenzen des abgetasteten Signals bestimmen können. Neben der Tatsache, dass die diskrete Fourier-Transformation eine zentrale Rolle in der Signalverarbeitung spielt, hat sie Anwendungen im Bereich der Datenkompression und bei der Multiplikation großer Polynome und ganzer Zahlen. Kapitel 30 stellt einen effizienten Algorithmus, die so genannte schnelle FourierTransformation (oft einfach nur FFT genannt) für dieses Problem vor. Zudem skizziert das Kapitel einen Entwurf einer Schaltung, mit der die FFT berechnet werden kann.
Datenstrukturen Dieses Buch stellt auch verschiedene Datenstrukturen vor. Eine Datenstruktur ist eine Methode, Daten abzuspeichern und zu organisieren sowie den Zugriff auf die Daten und die Modifikation der Daten zu erleichtern. Keine Datenstruktur arbeitet für alle Zwecke gleich gut. Deshalb ist es wichtig, die Vorteile und Einschränkungen jeder einzelnen zu kennen.
Methodik Obwohl es möglich ist, dieses Buch wie ein „Kochbuch“ für Algorithmen zu verwenden, könnten Sie eines Tages auf ein Problem stoßen, für das Sie nicht einfach einen veröffentlichten Algorithmus finden werden – die Übungsaufgaben und Problemstellungen, die Sie jeweils im Anschluss an die Abschnitte und Kapitel finden, sind zum Teil Beispiele hierfür. Dieses Buch wird Ihnen Methoden des Algorithmenentwurfes und der Analyse vermitteln, sodass Sie selbstständig Algorithmen entwickeln können, zeigen können, dass diese die korrekte Antwort berechnen, und deren Effizienz verstehen können. Verschiedene Kapitel zeigen unterschiedliche Aspekte des algorithmischen Problemlösens. Einige Kapitel behandeln spezielle Probleme, wie zum Beispiel Kapitel 9 die Berechnung von Medianen und Ranggrößen, Kapitel 23 die Berechnung minimaler Spannbäume und Kapitel 26 die Bestimmung maximaler Flüsse in Netzwerken. Andere Kapitel gehen auf Techniken ein, wie zum Beispiel Kapitel 4 auf Teile-und-Beherrsche, Kapitel 15 auf dynamische Programmierung und Kapitel 17 auf amortisierte Analyse.
Harte Probleme Der größte Teil des Buches behandelt effiziente Algorithmen. Unser übliches Maß für Effizienz ist Geschwindigkeit, d. h. die Zeit, die der Algorithmus benötigt, um sein Ergebnis zu produzieren. Dennoch gibt es einige Problemstellungen, für die keine effiziente
10
1 Die Rolle von Algorithmen in der elektronischen Datenverarbeitung
Lösungsmethode bekannt ist. Kapitel 34 untersucht eine interessante Teilmenge dieser Probleme, die als NP-vollständig bekannt sind. Weshalb sind NP-vollständige Probleme interessant? Erstens hat noch niemand bewiesen, dass ein effizienter Algorithmus für diese nicht existieren kann, obwohl bisher kein effizienter Algorithmus für ein NP-vollständiges Problem gefunden worden ist. Mit anderen Worten, keiner weiß, ob ein effizienter Algorithmus für ein NP-vollständiges Problem existiert oder nicht. Zweitens haben NP-vollständige Probleme die bemerkenswerte Eigenschaft, dass, wenn ein effizienter Algorithmus für eines von ihnen existiert, effiziente Algorithmen für alle von ihnen existieren. Die Verknüpfung unter den NP-vollständigen Problemen macht das Fehlen effizienter Lösungen um so spannender. Drittens sind verschiedene NP-vollständige Probleme ähnlich zu Problemen, für die wir effiziente Algorithmen tatsächlich kennen. Sie sind aber nicht identisch mit unseren NPvollständigen Problemen. Informatiker sind fasziniert davon, dass eine kleine Änderung an der Problemstellung einen großen Unterschied in Bezug auf die Effizienz des jeweils besten bekannten Algorithmus zur Folge haben kann. Sie sollten etwas über NP-vollständige Probleme wissen, da einige von ihnen überraschend häufig in realen Anwendungen auftreten. Wenn Sie dazu aufgefordert werden, einen effizienten Algorithmus für ein NP-vollständiges Problem zu finden, dann könnten Sie ansonsten viel Zeit mit einer ergebnislosen Suche verbringen. Wenn Sie zeigen können, dass das Problem NP-vollständig ist, dann können Sie stattdessen Ihre Zeit damit verbringen, einen effizienten Algorithmus zu entwickeln, der eine gute, wenn in der Regel auch nicht die bestmögliche Lösung liefert. Als ein konkretes Beispiel betrachten wir einen Dienstleister für Kurierdienstleistungen mit einem zentralen Lager. Jeden Tag belädt er die Lieferwagen am Lager und schickt sie jeweils zu den verschiedenen Adressen, wo die entsprechenden Güter abzugeben sind. Am Ende des Tages müssen die Lieferwagen wieder zurück ins Lager kommen, damit sie für den nächsten Tag wieder beladen werden können. Um die Kosten zu reduzieren, möchte das Unternehmen für jeden der Lieferwagen diejenige Reihenfolge von Lieferstopps finden, die zu der geringsten von ihm insgesamt zurückzulegenden Entfernung führt. Diese Problemstellung ist das wohlbekannte Problem des „Handelsreisenden“ und ist NP-vollständig. Es ist kein effizienter Algorithmus dafür bekannt. Sind jedoch bestimmte Annahmen erfüllt, so kennen wir effiziente Algorithmen, die jeweils Touren mit einer Gesamtdistanz berechnen, die nicht allzu weit über der Distanz der kürzesten Tour liegt. Kapitel 35 diskutiert solche „Approximationsalgorithmen“.
Parallelität Während vielen Jahren konnten wir darauf zählen, dass die Taktfrequenz der Prozessoren stetig anwächst. Physikalische Beschränkungen stellen aber eine elementare Hürde für eine ewig steigende Taktfrequenz dar: Da die Leistungsdichte superlinear mit der Taktfrequenz wächst, riskieren die Chips zu „schmelzen“, sobald die Taktfrequenz zu hoch ist. Um dennoch die Anzahl der Operationen pro Sekunde weiter zu steigern, werden Chips deshalb heutzutage so entworfen, dass sie nicht nur einen sondern mehrere Berechnungskerne enthalten. Wir können diese Mehrkern-Rechner (engl.: multicore computer ) ansehen als ob mehrere sequentielle Rechner auf einem Chip integriert wä-
1.2 Algorithmen als Technologie
11
ren, d. h. sie stellen eine Art „Parallelrechner“ dar. Um diesen Mehrkern-Rechnern die bestmögliche Performanz zu entlocken, müssen wir die vorhandene Parallelität im Auge behalten, wenn wir Algorithmen entwerfen. Kapitel 27 stellt ein Modell für „mehrfädige“ Algorithmen vor, die mehrere Kerne ausnutzen können. Dieses Modell ist aus theoretischer Sicht von Nutzen und stellt die Basis für einige erfolgreiche Computerprogramme, insbesondere einem preisgekrönten Schachprogramm.
Übungen 1.1-1 Geben Sie ein Beispiel aus der Praxis an, in dem sortiert werden muss oder in dem eine konvexe Hülle berechnet werden muss. 1.1-2 Welche anderen Maße könnte man außer der Geschwindigkeit in einem realistischen Szenario für die Effizienz verwenden? 1.1-3 Wählen Sie eine Datenstruktur aus, der Sie schon begegnet sind, und diskutieren Sie deren Stärken und Schwächen. 1.1-4 Worin ähneln sich die oben erwähnten Probleme der kürzesten Wege und des Handelsreisenden? Worin unterscheiden sie sich? 1.1-5 Lassen Sie sich ein realistisches Problem einfallen, für das nur die beste Lösung brauchbar ist. Benennen Sie anschließend ein Problem, bei dem die „annähernd“ beste Lösung als Ergebnis bereits ausreicht.
1.2
Algorithmen als Technologie
Angenommen, Rechner wären unendlich schnell und Speicher gäbe es umsonst. Gäbe es irgendeinen Grund, Algorithmen zu untersuchen? Die Antwort lautet ja. Wenn aus keinem anderen Grund, dann deshalb, weil Sie immer noch daran interessiert sind zu zeigen, dass Ihr Lösungsverfahren terminiert und es dies mit der korrekten Antwort tut. Wenn Rechner unendlich schnell wären, wäre jedes korrekte Verfahren zur Lösung eines Problems okay. Sie würden zwar wahrscheinlich verlangen, dass Ihre Implementierung softwaretechnischen Kriterien genügt (beispielsweise sollte Ihre Implementierung gut entworfen und dokumentiert sein), Sie würden aber in der Regel das am einfachsten zu implementierende Verfahren einsetzen. Natürlich mögen Rechner schnell sein, aber sie sind nicht unendlich schnell. Und Speicher mag billig sein, aber er ist nicht kostenlos. Rechenzeit ist deshalb eine beschränkte Ressource wie auch der Speicherplatz. Sie sollten diese Ressourcen mit Vernunft einsetzen. Algorithmen, die effizient im Sinne von Zeit oder Platzbedarf sind, werden Ihnen dabei helfen.
Effizienz Verschiedene Algorithmen, die zur Lösung ein und desselben Problems entwickelt wurden, können sich häufig dramatisch in ihrer Effizienz unterscheiden. Diese Unterschiede können viel größer sein als die durch Hardware und Software bedingten Unterschiede.
12
1 Die Rolle von Algorithmen in der elektronischen Datenverarbeitung
Als Beispiel dafür werden wir in Kapitel 2 zwei Sortieralgorithmen betrachten. Der erste, der unter dem Namen Sortieren durch Einfügen (engl.: insertion sort ) bekannt ist, benötigt ungefähr die Zeit c1 n2 , um n Elemente zu sortieren, wobei c1 eine von n unabhängige Konstante ist. Das heißt, er benötigt eine Zeit, die ungefähr proportional zu n2 ist. Der zweite, Sortieren durch Mischen (engl.: merge sort), benötigt ungefähr die Zeit c2 n lg n, wobei lg n für log2 n steht und c2 ebenfalls eine von n unabhängige Konstante ist. Sortieren durch Einfügen hat normalerweise einen kleineren konstanten Faktor als Sortieren durch Mischen, sodass c1 < c2 gilt. Wir werden sehen, dass die konstanten Faktoren aber einen weit geringeren Einfluss auf die Laufzeit haben als die Abhängigkeit von der Eingabegröße n. Lassen Sie uns hierfür die Laufzeit von Sortieren durch Einfügen umschreiben zu c1 n · n und die Laufzeit von Sortieren durch Mischen zu c2 n · lg n. Dann sehen wir, dass die Laufzeit von Sortieren durch Einfügen einen Faktor von n enthält und die Laufzeit von Sortieren durch Mischen nur einen Faktor von lg n, was viel kleiner ist. (Ist beispielsweise n = 1000, dann ist lg n ungefähr 10, und wenn n gleich einer Million ist, ist lg n ungefähr nur 20.) Obwohl Sortieren durch Einfügen für kleine Eingabegrößen gewöhnlich schneller ist als Sortieren durch Mischen, wird der auf dem Verhältnis zwischen lg n und n beruhende Vorteil von Sortieren durch Mischen den Unterschied im konstanten Faktor mehr als kompensieren, wenn die Eingabegröße n groß genug ist. Ganz gleich wieviel kleiner c1 gegenüber c2 ist, es wird immer eine Schwelle geben, ab dem Sortieren durch Mischen schneller ist. Um ein konkretes Beispiel anzugeben, lassen wir einen schnellen Rechner (Rechner A), auf dem Sortieren durch Einfügen läuft, gegen einen langsamen Rechner (Rechner B), auf dem Sortieren durch Mischen läuft, antreten. Jeder der beiden hat ein Feld von 10 Millionen Zahlen zu sortieren. (Auch wenn sich 10 Millionen Zahlen nach viel anhören, das Feld belegt bei 64-Bit Zahlen ungefähr 80 Megabytes, was in den Hauptspeicher sogar eines einfachen Laptops passt.) Nehmen Sie an, dass Rechner A zehn Milliarden Anweisungen pro Sekunde (was weit mehr ist als ein einzelner sequentieller Rechner zum Zeitpunkt der Erstellung dieses Buches leisten konnte) und Rechner B nur zehn Millionen Anweisungen pro Sekunde ausführt, sodass Rechner A von der Leistungsfähigkeit her 1000-mal schneller als Rechner B ist. Um den Unterschied noch drastischer zu gestalten, nehmen wir an, dass auf Rechner A die weltweit pfiffigsten Programmierer Sortieren durch Einfügen in Maschinensprache kodiert haben und der entstandene Code 2 n2 Anweisungen ausführen muss, um n Zahlen zu sortieren. Wir nehmen desweiteren an, dass für Rechner B Sortieren durch Mischen von einem durchschnittlichen Programmierer in einer höheren Programmiersprache mit Hilfe eines ineffizienten Compilers programmiert wurde, wobei der entstandene Code 50 n lg n Anweisungen benötigt. Um zehn Millionen Zahlen zu sortieren, benötigt Rechner A also 2 · (107 )2 Anweisungen = 20.000 Sekunden (mehr als 5 12 Stunden) , 1010 Anweisungen/Sekunde während Rechner B 50 · 107 lg 107 Anweisungen ≈ 1163 Sekunden (weniger als 20 Minuten) 107 Anweisungen/Sekunde benötigt. Unter Verwendung eines Algorithmus, dessen Laufzeit langsamer anwächst, läuft Rechner B sogar mit einem schwachen Compiler mehr als 17-mal schneller als
1.2 Algorithmen als Technologie
13
Rechner A! Der Vorteil von Sortieren durch Mischen wird sogar noch deutlicher, wenn wir 100 Millionen Zahlen sortieren: Während Sortieren durch Einfügen ungefähr 23 Tage benötigt, braucht Sortieren durch Mischen weniger als vier Stunden. Im Allgemeinen gilt, dass in dem Maße, in dem der Umfang des Problems anwächst, der relative Vorteil von Sortieren durch Mischen größer wird.
Algorithmen und andere Technologien Das oben genannte Beispiel zeigt, dass wir Algorithmen, ähnlich wie Computer-Hardware, als Technologie verstehen sollten. Die Gesamtleistung des Systems hängt in gleicher Weise von der Wahl eines effizienten Algorithmus wie von der Wahl schneller Hardware ab. Genauso schnell wie Fortschritte in anderen Computertechnologien gemacht werden, werden sie auch auf dem Gebiet der Algorithmen erzielt. Sie werden sich fragen, ob Algorithmen wirklich so wesentlich für heutige Rechner sind, angesichts anderer fortgeschrittener Technologien wie • moderne Rechnerarchitekturen und Fabrikationstechnologien, • einfach zu bedienende, intuitive grafische Benutzeroberflächen (GUIs), • objektorientierte Systeme • integrierte Webtechnologien und • schnelle Netzwerke, sowohl Kabel- als auch Funkbasierte. Die Antwort lautet ja, wenngleich einige Anwendungen auf der Anwendungsebene keiner komplexen Algorithmen bedürfen (wie zum Beispiel einige einfache internetbasierte Anwendungen). Die meisten erfordern zu ihrer Realisierung jedoch ein gewisses Maß an algorithmischem Gehalt. Betrachten wir zum Beispiel einen internetbasierten Dienst, der bestimmt, wie man von einem Ort zu einem anderen gelangt. Dessen Implementierung würde auf schneller Hardware, einer grafischen Benutzeroberfläche, wide-area Vernetzung und möglicherweise Objektorientierung aufbauen. Für bestimmte Operationen müsste der Dienst auch Algorithmen nutzen. Dies könnten Algorithmen zum Finden von Routen (unter Verwendung kürzester-Pfad-Algorithmen), zur Wiedergabe von Karten oder zur Interpolation von Adressen sein. Darüber hinaus ist sogar eine Anwendung, die auf der Anwendungsebene keine algorithmischen Inhalte erfordert, stark auf Algorithmen angewiesen. Ist die Anwendung von schneller Hardware abhängig? Bei der Hardwareentwicklung werden Algorithmen benutzt. Ist die Anwendung auf eine grafische Benutzeroberfläche angewiesen? Der Entwurf jeder grafischen Benutzeroberfläche stützt sich auf Algorithmen. Ist die Anwendung auf Vernetzung angewiesen? Das Routing in Netzwerken basiert stark auf Algorithmen. Wurde die Anwendung in einer anderen Programmiersprache als in Maschinencode geschrieben? Dann wurde sie durch einen Compiler, Interpreter oder Assembler bearbeitet, die alle umfangreichen Gebrauch von Algorithmen machen. Algorithmen stehen im Zentrum der meisten in modernen Rechnern verwendeten Technologien.
14
1 Die Rolle von Algorithmen in der elektronischen Datenverarbeitung
Des Weiteren benutzen wir die Rechner mit deren ständig anwachsender Kapazität, um immer größere Probleme zu lösen als jemals zuvor. Wie wir in dem vorangegangenen Vergleich zwischen Sortieren durch Einfügen und Sortieren durch Mischen gesehen haben, ist es bei größerem Problemumfang so, dass die Effizienzunterschiede zwischen Algorithmen besonders markant werden können. Eine Eigenschaft, die die wirklich qualifizierten Programmierer von Anfängern unterscheidet, ist der Besitz einer soliden Basis von algorithmischem Fachwissen und Methoden. Mit moderner Rechentechnologie kann man einige Aufgaben ausführen, ohne viel über Algorithmen zu wissen; mit einem guten Hintergrund auf dem Gebiet der Algorithmen können Sie jedoch viel, viel mehr tun.
Übungen 1.2-1 Geben Sie ein Beispiel für eine Anwendung an, die algorithmischen Gehalt auf der Anwendungsebene erfordert, und diskutieren Sie die Funktion der eingesetzten Algorithmen. 1.2-2 Nehmen Sie an, dass wir die Implementierung von Sortieren durch Einfügen und Sortieren durch Mischen auf demselben Rechner vergleichen. Für Eingaben der Größe n benötigt Sortieren durch Einfügen 8 n2 Schritte, während Sortieren durch Mischen 64 n lg n Schritte benötigt. Für welche Werte von n wird Sortieren durch Einfügen schneller als Sortieren durch Mischen sein? 1.2-3 Welcher ist der kleinste Wert von n, für den ein Algorithmus, dessen Laufzeit 100 n2 ist, auf demselben Rechner schneller läuft als ein Algorithmus, dessen Laufzeit 2n beträgt.
Problemstellungen 1-1 Vergleich von Laufzeiten Bestimmen Sie für jede Funktion f (n) und jede Zeit t den größten Wert von n für ein Problem, das innerhalb der Zeit t gelöst werden kann, wenn der Algorithmus f (n) Mikrosekunden benötigt, um das Problem zu lösen. 1 1 1 1 1 1 1 Sekunde
lg n √ n n n lg n n2 n3 2n n!
Minute
Stunde
Tag
Monat
Jahr
Jahrhundert
Problemstellungen zu Kapitel 1
15
Kapitelbemerkungen Es gibt viele ausgezeichnete Lehrbücher über allgemeine Fragen zu Algorithmen, einschließlich denen von Aho, Hopcroft, and Ullman [5, 6], Baase and Van Gelder [28], Brassard and Bratley [54], Dasgupta, Papadimitriou, and Vazirani [82], Goodrich and Tamassia [148], Hofri [175], Horowitz, Sahni, and Rajasekaran [181], Johnsonbaugh and Schaefer [193], Kingston [205], Kleinberg and Tardos [208], Knuth [209, 210, 211], Kozen [220], Levitin [235], Manber [242], Mehlhorn [249, 250, 251], Purdom and Brown [287], Reingold, Nievergelt, and Deo [293], Sedgewick [306], Sedgewick and Flajolet [307], Skiena [318], und Wilf [356]. Einige der eher praktischen Aspekte des Algorithmenentwurfs werden von Bentley [42, 43] und Gonnet [145] diskutiert. Einen Abriss über das Gebiet der Algorithmen findet man auch im Handbook of Theoretical Computer Science, Band A [342] und im CRC Handbook on Algorithms and Theory of Computation [25]. Einen Überblick über Algorithmen, die in der Bioinformatik benutzt werden, findet man in den Lehrbüchern von Gusfield [156], Pevzner [275], Setubal und Meidanis [310] und Waterman [350].
2
Ein einführendes Beispiel
Dieses Kapitel wird Sie mit der Methodik vertraut machen, die wir in diesem Buch benutzen werden, um über den Entwurf und die Analyse von Algorithmen nachzudenken. Das Kapitel ist in sich geschlossen, enthält allerdings verschiedene Verweise auf den Stoff, der in Kapitel 3 und 4 eingeführt wird. (Es enthält auch einige Summenformeln, auf deren Berechnung im Anhang A eingegangen wird.) Wir beginnen mit einer genaueren Betrachtung des in Kapitel 1 bereits eingeführten Algorithmus Sortieren durch Einfügen (engl.: insertion sort), der das Sortierproblem löst. Wir führen einen „Pseudocode“ ein, der Ihnen vertraut sein sollte, wenn Sie bereits programmiert haben. Alle unsere Algorithmen werden wir über solchen Pseudocode beschreiben. Nachdem wir den Algorithmus zum Sortieren durch Einfügen beschrieben haben, diskutieren wir, warum er korrekt sortiert, und analysieren seine Laufzeit. Die Analyse führt eine Notation ein, die sich darauf bezieht, wie die Zeit mit der Anzahl der zu sortierenden Elemente anwächst. Nach der Diskussion von Sortieren durch Einfügen führen wir die Teile-und-Beherrsche-Methode (engl.: divide and conquer ) für den Entwurf von Algorithmen ein und wenden diese Methode an, um einen Algorithmus zu entwickeln, den wir Sortieren durch Mischen (engl.: merge sort ) nennen. Wir schließen mit der Analyse der Laufzeit von Sortieren durch Mischen.
2.1
Sortieren durch Einfügen
Unser erster Algorithmus, Sortieren durch Einfügen, löst das in Kapitel 1 eingeführte Sortierproblem: Eingabe: Eine Folge von n Zahlen a1 , a2 , . . . , an . Ausgabe: Eine Permutation (Umordnung) a1 , a2 , . . . , an der Eingabefolge, sodass a1 ≤ a2 ≤ · · · ≤ an gilt. Die Zahlen, die wir sortieren wollen, werden auch als Schlüssel bezeichnet. Auch wenn wir das Sortierproblem in Bezug auf eine zu sortierende Folge begreifen, die Eingabe erfolgt in Form eines Feldes, das aus n Elementen besteht. In diesem Buch werden wir Algorithmen üblicherweise als Programme darstellen, die in einem Pseudocode geschrieben sind, der C, C++, Java, Python oder Pascal in vielerlei Hinsicht ähnelt. Wenn Sie mit einer dieser Sprachen vertraut sind, dann sollten Sie wenig Probleme beim Lesen unserer Algorithmen haben. Was den Pseudocode von „echtem“ Code unterscheidet, ist, dass wir im Pseudocode diejenige Ausdrucksweise
18
2 Ein einführendes Beispiel
♣♣ ♣ ♣♣ 10 5♣ ♣ 4 ♣♣ ♣♣ ♣ ♣ ♣ ♣ ♣♣ ♣ 7 ♣
0 ♣♣ ♣ 5♣ ♣♣ ♣ 4 2♣ ♣ ♣ ♣ ♣♣ ♣ ♣♣
7 ♣
2 ♣
1
Abbildung 2.1: Sortieren von Karten auf einer Hand unter Verwendung von Sortieren durch Einfügen.
verwenden, die einen gegebenen Algorithmus am deutlichsten und prägnantesten spezifiziert. Manchmal ist die natürliche Sprache am anschaulichsten; seien Sie deshalb nicht überrascht, wenn Ihnen eine deutsche Wortgruppe oder ein Satz inmitten von „echtem“ Code begegnet. Ein weiterer Unterschied zwischen Pseudocode und echtem Code liegt darin, dass der Pseudocode sich üblicherweise nicht mit Aufgaben der Softwaretechnik befasst. Aufgaben der Datenabstraktion, der Modularität und der Fehlerbehandlung werden häufig ignoriert, um das Wesentliche des Algorithmus prägnanter zu vermitteln. Wir beginnen mit Sortieren durch Einfügen, einem Algorithmus, der für das Sortieren einer kleinen Anzahl von Elementen effizient ist. Sortieren durch Einfügen arbeitet auf die Art und Weise, wie viele Menschen Spielkarten in einer Hand sortieren. Wir beginnen mit einer leeren linken Hand und die Karten liegen umgedreht auf dem Tisch. Dann nehmen wir pro Zeiteinheit eine Karte vom Tisch und fügen sie an der richtigen Position in der linken Hand ein. Um die korrekte Position für eine Karte zu finden, vergleichen wir sie von rechts nach links mit jeder bereits auf der Hand befindlichen Karte, wie in Abbildung 2.1 dargestellt. Zu jeder Zeit sind die Karten auf der Hand sortiert, wobei diese Karten ursprünglich die obersten Karten des Haufens auf dem Tisch waren. Insertion-Sort(A) 1 for j = 2 to A.l¨a nge 2 schl¨u ssel = A[j] 3 // Füge A[j] in die sortierte Sequenz A[1 . . j − 1] ein. 4 i = j−1 5 while i > 0 und A[i] > schl¨u ssel 6 A[i + 1] = A[i] 7 i = i−1 8 A[i + 1] = schl¨u ssel
2.1 Sortieren durch Einfügen 1
2
3
4
5
6
(a)
5
2
4
6
1
3
1
2
3
4
5
6
(d)
2
4
5
6
1
3
19 1
2
3
4
5
(b)
2
5
4
6
1 3
6
1
2
3
4
5
6
(e)
1
2
4
5
6
3
1
2
3
4
5
6
(c)
2
4
5
6
1
3
1
2
3
4
5
6
(f)
1
2
3
4
5
6
Abbildung 2.2: Die Arbeitsweise von Insertion-Sort auf dem Feld A = 5, 2, 4, 6, 1, 3. Die Feldindizes sind oberhalb der Rechtecke und die an den Positionen gespeicherten Werte innerhalb der Rechtecke vermerkt. (a)–(e) Die Iteration der for-Schleife in den Zeilen 1–8. Bei jeder Iteration enthält das schwarze Rechteck den aus A[j] entnommenen Schlüssel, der im Test in Zeile 5 mit den Werten verglichen wird, die in den schattierten Rechtecken links davon stehen. Schattierte Pfeile zeigen Feldvariablen, die in Zeile 6 eine Stelle nach rechts verschoben werden und schwarze Pfeile kennzeichnen, wohin der Schlüssel in Zeile 8 verschoben wird. (f ) Das endgültig sortierte Feld.
Wir stellen den Pseudocode für Sortieren durch Einfügen als eine Prozedur mit dem Namen Insertion-Sort dar, die als Eingabeparameter ein Feld A[1 . . n] erhält, das die Folge der n zu sortierenden Elementen enthält. (Im Code wird die Zahl n der Elemente in A mit A.l¨a nge bezeichnet.) Der Algorithmus sortiert die eingegebenen Elemente in-place, d. h. er ordnet die Elemente innerhalb des Feldes A um, wobei zu jedem Zeitpunkt höchstens eine konstante Anzahl von Elementen außerhalb des Feldes gespeichert werden. Das Eingabefeld A enthält die sortierte Ausgabefolge, wenn Insertion-Sort beendet ist.
Schleifeninvarianten und die Korrektheit von Sortieren durch Einfügen Abbildung 2.2 zeigt, wie dieser Algorithmus angewendet auf A = 5, 2, 4, 6, 1, 3 arbeitet. Der Index j kennzeichnet die „aktuelle Karte“, die gerade in das Blatt einsortiert wird. Zu Beginn jeder Iteration der for-Schleife, die über die Laufvariable j gesteuert wird, bildet das aus den Elementen A[1 . . j − 1] bestehende Teilfeld das gegenwärtige sortierte Blatt, und die Elemente A[j + 1 . . n] entsprechen dem sich noch auf dem Tisch befindlichen Kartenhaufen. Tatsächlich sind die Elemente A[1 . . j − 1] auch diejenigen Elemente, die sich ursprünglich an den Positionen 1 bis j − 1 befunden haben, sind aber nun in geordneter Reihenfolge. Wir bezeichnen diese Eigenschaften von A[1 . . j − 1] formal als Schleifeninvariante: Zu Beginn jeder Iteration der for-Schleife in den Zeilen 1–8 besteht das Teilfeld A[1 . . j − 1] aus den ursprünglich in A[1 . . j − 1] enthaltenen Elementen, allerdings in geordneter Reihenfolge. Wir benutzen Schleifeninvarianten als Hilfsmittel, um zu beweisen, dass ein Algorithmus korrekt ist. Wir müssen zeigen, dass eine Schleifeninvariante drei Dinge erfüllt:
20
2 Ein einführendes Beispiel
Initialisierung: Sie ist vor der Iteration der Schleife wahr. Fortsetzung: Wenn sie vor der Iteration einer Schleife wahr ist, dann bleibt sie auch bis zum Beginn der nächsten Iteration wahr. Terminierung: Wenn die Schleife abbricht, dann liefert uns die Invariante eine zweckdienliche Eigenschaft, die uns hilft zu zeigen, dass der Algorithmus korrekt ist. Wenn die ersten beiden Eigenschaften erfüllt sind, dann ist die Schleifeninvariante vor jeder Iteration der Schleife wahr. (Natürlich dürfen Sie auch andere begründete Sachverhalte als die Schleifeninvariante selbst benutzen, um zu beweisen, dass die Schleifeninvariante vor jeder Iteration erfüllt ist.) Beachten Sie die Ähnlichkeit mit der mathematischen Induktion, bei der Sie einen Induktionsanfang und einen Induktionsschritt beweisen, um zu zeigen, dass eine Eigenschaft erfüllt ist. Der Nachweis, dass die Invariante vor der ersten Iteration erfüllt ist, entspricht hier dem Induktionsanfang und der Nachweis, dass die Invariante von Iteration zu Iteration erhalten bleibt, dem Induktionsschritt. Die dritte Eigenschaft ist die wahrscheinlich wichtigste, da wir die Schleifeninvariante benutzen, um die Korrektheit des untersuchten Algorithmus zu zeigen. Üblicherweise wenden wir die Schleifeninvariante in Verbindung mit der Bedingung an, unter der die Schleife beendet wird. Eine solche Terminierungseigenschaft findet man in der üblichen Verwendung der mathematischen Induktion, in der der Induktionsschritt unendlich oft angewendet wird, nicht vor; hier beenden wir die „Induktion“, wenn die Schleife abbricht. Sehen wir uns an, ob die oben formulierte Schleifenvariante beim Sortieren durch Einfügen erfüllt ist. Initialisierung Wir zeigen zunächst, dass die Schleifeninvariante vor der ersten Iteration, in der die Laufvariable j = 2 ist, gilt.1 Das Teilfeld A[1 . . j − 1] besteht zu diesem Zeitpunkt nur aus dem einzigen Element A[1], das tatsächlich das ursprüngliche Element in A[1] ist. Zudem ist dieses Teilfeld sortiert (trivialerweise natürlich), was zeigt, dass die Schleifeninvariante vor der ersten Iteration der Schleife gilt. Fortsetzung Als nächstes schauen wir uns die zweite Eigenschaft an. Es ist zu zeigen, dass die Schleifeninvariante bei jeder Iteration erhalten bleibt. Informal formuliert, arbeitet der Ausführungsblock der for-Schleife so, dass A[j − 1], A[j − 2], A[j −3] und so weiter jeweils um eine Stelle nach rechts verschoben werden, bis die richtige Position von A[j] gefunden ist (Zeilen 4–7) und der Wert von A[j] dann an dieser Stelle eingefügt wird (Zeile 8). Das Teilfeld A[1 . . j] besteht dann aus 1 Wenn die Schleife eine for-Schleife ist, dann ist der Zeitpunkt, an dem wir die Schleifeninvariante vor der ersten Iteration testen, sofort nach der Anfangszuweisung an die Laufvariable und genau vor dem ersten Test im Schleifenkopf. Im Falle von Insertion-Sort ist dies, nachdem j der Wert 2 zugewiesen wurde und vor dem ersten Test, ob j ≤ A. l¨ a nge gilt.
2.1 Sortieren durch Einfügen
21
Elementen, die ursprünglich in A[1 . . j] waren, jetzt aber in geordneter Reihenfolge. Das Inkrementieren von j für die nächste Iteration der for-Schleife erhält die Schleifeninvariante. Eine formalere Behandlung der zweiten Eigenschaft würde erfordern, eine Schleifeninvariante für die while-Schleife in Zeile 5–7 anzugeben und zu beweisen. An diesem Punkt ziehen wir es jedoch vor, uns nicht in solch einem Formalismus zu verlieren. Wir verlassen uns auf unsere informale Analyse, um zu zeigen, dass die zweite Eigenschaft für die äußere Schleife gilt. Terminierung Zum Abschluss untersuchen wir, was passiert, wenn die Schleife abbricht. Die for-Schleife in unserem Algorithmus bricht ab, wenn j > A.l¨a nge = n gilt. Da jede Schleifeniteration j um 1 erhöht, müssen wir zu diesem Zeitpunkt j = n+1 haben. Setzen wir n+1 für j innerhalb der Formulierung der Schleifeninvariante ein, dann erhalten wir, dass das Teilfeld A[1 . . n] aus den ursprünglich in A[1 . . n] enthaltenen Elementen besteht, allerdings in geordneter Reihenfolge. Das Teilfeld A[1 . . n] ist aber bereits das gesamte Feld! Somit können wir schließen, dass das gesamte Feld sortiert ist. Der Algorithmus arbeitet also korrekt. Wir werden diesen Ansatz der Schleifeninvarianten später im Kapitel und auch in anderen Kapiteln verwenden, um die Korrektheit von Algorithmen zu zeigen.
Pseudocode-Konventionen Wir verwenden in unserem Pseudocode die folgenden Konventionen. • Einrücken kennzeichnet die Blockstruktur. Zum Beispiel besteht der Schleifenrumpf der for-Schleife, der in Zeile 1 beginnt, aus den Zeilen 2–8, und der Schleifenrumpf der while-Schleife, der in Zeile 5 beginnt, enthält die Zeilen 6–7, aber nicht die Zeile 8. Diesen Stil wenden wir auch bei if -else-Anweisungen2 an. Das Verwenden von Einrückungen anstelle von konventionellen Markierungen der Blockstruktur, wie begin- und end-Anweisungen, bewahrt die Übersichtlichkeit der Programme, wenn sie sie nicht sogar erhöht.3 • Die Schleifenkonstrukte while, for und repeat-until und das Konstrukt if -else zur bedingten Abfrage haben Interpretationen, die denen in C, C++, Java, Python und Pascal ähneln.4 In diesem Buch behält die Laufvariable ihren Wert nach Verlassen der Schleife, im Unterschied zu C++, Java und Pascal. Deshalb ist der Wert der Laufvariablen unmittelbar nach der for-Schleife derjenige Wert, 2 In einer if -else-Anweisung, rücken wir else an die gleiche Stelle ein wie das zugehörige if. Auch wenn wir das Schlüsselwort then weglassen, sprechen wir bisweilen dennoch vom then-Fall, um auf das Codestück zu verweisen, das ausgeführt wird, wenn die Bedingung der if -Anweisung wahr ist. Bei einer Vielfachabfrage, verwenden wir elseif, um zu der nächsten Abfrage zu gelangen. 3 Alle Pseudocode-Prozeduren in diesem Buch erscheinen jeweils auf einer Seite ohne Seitenumbruch, sodass Sie nicht die Einrücktiefen in Code, der über mehrere Seiten geht, zu bestimmen haben. 4 Die meisten blockstrukturierten Sprachen besitzen äquivalente Konstrukte, auch wenn sich die genaue Syntax unterscheiden kann. In Python fehlen repeat-until-Schleifen und die for-Schleifen arbeiten ein bisschen anders als die for-Schleifen in diesem Buch.
22
2 Ein einführendes Beispiel der zuerst die Schranke der for-Schleife verletzt hat. Wir haben diese Eigenschaft im Korrektheitsbeweis von Sortieren durch Einfügen benutzt. Der Kopf der forSchleife in Zeile 1 lautet for j = 2 to A.l¨a nge und so gilt j = A.l¨a nge + 1 (d. h. j = n + 1, wegen n = A.l¨a nge), wenn die Schleife abbricht. Wir verwenden das Schlüsselwort to, falls die for-Schleife ihre Laufvariable in jeder Iteration inkrementiert, und das Schlüsselwort downto, falls die for-Schleife ihre Laufvariable in jeder Iteration dekrementiert. Falls die Laufvariable um einen Betrag größer als 1 verändert werden soll, wird die entsprechende Schrittweite nach dem Schlüsselwort by angegeben. • Das Symbol „//“ zeigt an, dass der Rest der Zeile ein Kommentar ist. • Eine Mehrfachzuweisung der Form i = j = e übergibt den beiden Variablen i und j den Wert des Ausdruckes e; sie sollte als äquivalent zu der Zuweisung j = e gefolgt von der Zuweisung i = j betrachtet werden. • Variablen (wie zum Beispiel i, j und schl¨u ssel ) gelten als lokal innerhalb der gegebenen Prozedur. Wir werden ohne eine jeweilige explizite Erwähnung keine globalen Variablen verwenden. • Wir greifen auf Elemente eines Feldes zu, indem der Name des Feldes, gefolgt vom Index, der in eckigen Klammern steht, angegeben wird. Zum Beispiel bezeichnet A[i] das i-te Element des Feldes A. Die Bezeichnung „. .“ wird verwendet, um einen Bereich von Werten innerhalb eines Feldes zu kennzeichnen. So bezeichnet A[1 . . j] ein Teilfeld von A, das aus den j Elementen A[1], A[2], . . . , A[j] besteht. • Normalerweise organisieren wir zusammenhängende Daten in Objekten, die sich aus Attributen zusammensetzen. Wir greifen auf ein spezielles Attribut eines Objektes zu, indem wir die Syntax benutzen, die in vielen objektorientierten Programmiersprachen zu finden ist: Objektname gefolgt von einem Punkt und dem Attributnamen. So betrachten wir ein Feld als ein Objekt mit dem Attribut l¨a nge, das die Anzahl der Elemente angibt, die in dem Feld enthalten sind. Um die Anzahl der Elemente eines Feldes A anzugeben, schreiben wir A.l¨a nge. Wir behandeln eine Variable, die ein Feld oder ein Objekt darstellt, als Zeiger auf die sie repräsentierenden Daten. Für jedes Attribut f eines Objektes x bewirkt die Zuweisung y = x, dass anschließend y.f gleich x.f ist. Wenn wir dann x.f = 3 setzen, dann gilt anschließend nicht nur x.f = 3, sondern auch y.f = 3. Mit anderen Worten, x und y zeigen nach der Zuweisung y = x auf das gleiche Objekt. Unsere Notation für Attribute können wir „hintereinander schalten“. Nehmen Sie beispielsweise an, dass das Attribut f selbst ein Zeiger auf ein Objekt ist, das ein Attribut g besitzt. Dann wird die Notation x.f .g implizit geklammert als (x.f ).g. Anders formuliert, wenn y = x.f gilt, dann ist x.f .g das gleiche wie y.g. Zuweilen wird ein Zeiger auf überhaupt kein Objekt verweisen. In diesem Fall weisen wir ihm den speziellen Wert nil zu. • Parameter werden einer Prozedur als Werte übergeben („call by value“): Die aufgerufene Prozedur erhält ihre eigene Kopie der Parameter, und falls sie einem Parameter einen Wert zuweist, dann wird diese Veränderung von der aufrufenden
2.1 Sortieren durch Einfügen
23
Prozedur nicht wahrgenommen. Falls Objekte übergeben werden, wird der Zeiger auf die das Objekt repräsentierenden Daten kopiert, die Attribute des Objektes allerdings nicht. Wenn zum Beispiel x ein Parameter einer aufgerufenen Prozedur ist, dann ist die Zuweisung x = y innerhalb der aufgerufenen Prozedur für die aufrufende Prozedur selbst nicht sichtbar. Die Zuweisung x.f = 3 ist jedoch sichtbar. Gleichermaßen werden Felder als Zeiger übergeben, sodass nur ein Zeiger und nicht das gesamte Feld übergeben wird. Änderungen an einzelnen Elementen des Feldes sind für die aufrufende Prozedur sichtbar. • Eine return-Anweisung übergibt unmittelbar die Kontrolle zurück an die aufrufende Prozedur, die an der Stelle des Prozeduraufrufs weiterarbeitet. Die meisten return-Anweisungen erhalten einen Wert, den sie an die aufrufende Prozedur zurückgeben. Unser Pseudocode unterscheidet sich von vielen Programmiersprachen dahingehend, dass wir erlauben, dass mehrere Werte durch eine einzige returnAnweisung zurückgegeben werden können. • Die Booleschen Operatoren “und” und “oder” sind träge Operatoren. Das heißt, wenn wir den Ausdruck “x und y” bestimmen, dann werten wir zuerst x aus. Wenn x den Wert falsch annimmt, dann kann der gesamte Ausdruck auch nicht mehr wahr sein, weshalb wir y gar nicht erst auswerten. Wenn x andererseits den Wert wahr annimmt, dann müssen wir y auswerten, um den Wert für den gesamten Ausdruck zu bestimmen. Analog dazu werten wir bei dem Ausdruck “x oder y” den Ausdruck y nur dann aus, wenn x den Wert falsch annimmt. Träge Operatoren erlauben es uns, Boolesche Ausdrücke wie „x = nil und x.f = y“ zu schreiben, ohne uns darüber Gedanken machen zu müssen, was passiert, wenn wir versuchen, x.f auszuwerten, wenn x nil ist. • Das Schlüsselwort error gibt an, dass ein Fehler aufgetreten ist, weil Bedingungen einer Prozedur, die aufgerufen worden ist, verletzt worden sind. Die aufrufende Prozedur ist für die Fehlerbehandlung verantwortlich, sodass wir nicht anzugeben brauchen, welche Maßnahmen im Falle eines Fehlers getroffen werden müssen.
Übungen 2.1-1 Zeigen Sie analog zu Abbildung 2.2 die Arbeitsweise von Insertion-Sort, wenn die Prozedur auf das Feld A = 31, 41, 59, 26, 41, 58 angewendet wird. 2.1-2 Schreiben Sie die Prozedur Insertion-Sort so um, dass in nichtsteigender anstatt nichtfallender Ordnung sortiert wird. 2.1-3 Betrachten Sie das Suchproblem: Eingabe: Eine Sequenz von n Zahlen A = a1 , a2 , . . . , an und ein Wert v. Ausgabe: Ein Index i mit v = A[i], falls v in A vorkommt, und den speziellen Wert nil, wenn der Wert v nicht in A vorkommt. Schreiben Sie ein Programm in Pseudocode für die lineare Suche, die eine Sequenz nach v durchsucht. Beweisen Sie unter Verwendung einer Schleifeninvariante, dass Ihr Algorithmus korrekt ist. Achten Sie darauf, dass Ihre Schleifeninvariante die drei notwendigen Eigenschaften erfüllt.
24
2 Ein einführendes Beispiel
2.1-4 Betrachten Sie das Problem der Addition von zwei n-Bit Binärzahlen, die in zwei Feldern A und B der Länge n gespeichert sind. Die Summe der beiden ganzen Zahlen soll in einem Feld C der Länge n + 1 gespeichert werden. Beschreiben Sie das Problem formal und geben Sie ein Programm in Pseudocode für die Addition von zwei ganzen Zahlen an.
2.2
Analyse von Algorithmen
Die Analyse eines Algorithmus ist von Bedeutung, um die Ressourcen vorauszubestimmen, die der Algorithmus benötigt. Gelegentlich sind Ressourcen wie Speicher, Kommunikationsbandbreite oder Computerhardware von grundlegendem Interesse, aber am häufigsten ist es die Rechenzeit, die wir abschätzen möchten. Gewöhnlich können wir durch Analyse mehrerer Algorithmen für ein Problem den effizientesten von ihnen identifizieren. Eine solche Analyse kann auf mehr als einen brauchbaren Kandidaten hinweisen, die schlechteren Algorithmen können jedoch durch diesen Prozess verworfen werden. Bevor wir einen Algorithmus analysieren können, müssen wir über ein Modell der Technologie, auf der wir den Algorithmus implementieren wollen, verfügen, einschließlich eines Modells der Ressourcen dieser Technologie und deren Kosten. Meistens werden wir in diesem Buch von dem allgemeinen Modell einer Maschine mit wahlfreiem Zugriff (RAM) (mit einem Prozessor) ausgehen und annehmen, dass die von uns angegebenen Algorithmen als Computerprogramme auf diesem Modell ausgeführt werden. Innerhalb des RAM-Modells werden Anweisungen nacheinander ausgeführt. Operationen können nicht parallel ablaufen. Streng genommen sollte man die Befehle des RAM-Modells und deren Aufwand genau bestimmen. Dies zu tun wäre jedoch mühevoll und würde wenig Einblick in den Entwurf von Algorithmen und deren Analyse bieten. Jedoch müssen wir vorsichtig sein, das RAM-Modell nicht zu missbrauchen. Was wäre zum Beispiel, wenn ein RAM über einen Befehl zum Sortieren verfügte? Dann könnten wir mit nur einem Befehl sortieren. Solch ein RAM wäre unrealistisch, da reale Rechner nicht über solche Befehle verfügen. Wir wollen uns am Aufbau realer Rechner orientieren. Das RAM-Modell enthält Befehle, die man üblicherweise in realen Rechnern findet: arithmetische (wie zum Beispiel Addieren, Subtrahieren, Multiplizieren, Dividieren, Restbilden, Abrunden, Aufrunden), Daten bewegende (Laden, Speichern, Kopieren) und kontrollierende (bedingte und unbedingte Verzweigung, Aufruf einer Unterroutine und Rücksprung) Maschinenbefehle. Jeder dieser Befehle benötigt ein festes Maß an Zeit. Die Datentypen innerhalb des RAM-Modells sind Integer-Zahlen und Gleitkommazahlen (zum Speichern reeller Zahlen). Obwohl wir uns in diesem Buch normalerweise nicht mit Rechengenauigkeit beschäftigen, ist bei einigen Anwendungen die Rechengenauigkeit ausschlaggebend. Wir gehen im Buch außerdem davon aus, dass die Breite eines Datenwortes begrenzt ist. Wenn wir zum Beispiel mit Werten bis n arbeiten, dann nehmen wir in der Regel an, dass Integer-Zahlen mit c lg n Bits repräsentiert werden, wobei c ≥ 1 eine von n unabhängige Konstante ist. Wir fordern c ≥ 1, sodass jedes Wort den Wert n speichern kann. Wir fordern, dass c eine Konstante ist, damit die Wortbreite
2.2 Analyse von Algorithmen
25
nicht beliebig anwachsen kann. (Wenn die Wortbreite beliebig wachsen könnte, könnten wir riesige Datenmengen in einem Wort speichern und Operationen auf ihnen jeweils in konstanter Zeit ausführen – eindeutig ein unrealistisches Szenario.) Reale Rechner enthalten weitere Befehle, die wir oben nicht aufgelistet haben. Solche Befehle bilden eine Grauzone im RAM-Modell. Ist zum Beispiel die Potenzierung ein zeitkonstanter Befehl? Im allgemeinen Fall nein; es sind einige Befehle notwendig, um xy zu berechnen, wenn x und y reelle Zahlen sind. Dennoch ist die Potenzierung in eingeschränkten Fällen eine zeitkonstante Operation. Viele Rechner verfügen über einen Maschinenbefehl für einen Linksshift einer Integer-Zahl um k Positionen. Ein solcher Maschinenbefehl wird in konstanter Zeit ausgeführt. Bei den meisten Rechnern ist das Verschieben der Bits einer ganzen Zahl um eine Position nach links äquivalent zur Multiplikation mit 2, sodass eine Verschiebung der Bits um k Positionen nach links äquivalent zur Multiplikation mit 2k ist. Deshalb können solche Rechner den Wert 2k in einem zeitkonstanten Befehl berechnen, indem sie die Integer-Zahl 1 um k Positionen nach links verschieben, solange k nicht größer als die Wortbreite ist. Wir werden uns bemühen, solche Grauzonen des RAM-Modells zu meiden. Wir werden aber die Berechnung von 2k als zeitkonstante Operation ansehen, wenn k eine ausreichend kleine positive ganze Zahl ist. Im RAM-Modell versuchen wir nicht, die Konzepte der Speicherhierarchie, wie sie in heutigen Rechnern realisiert sind, zu modellieren. Wir modellieren weder den oder die Caches noch virtuellen Speicher. Verschiedene Modelle versuchen, die zum Teil signifikanten Speicherhierarchie-Effekte bei realen Programmen auf realen Rechnern zu berücksichtigen. Eine Hand voll von Problemen, die Sie in diesem Buch finden, untersuchen Speicherhierarchie-Effekte. In den meisten Fällen betrachten die Analysen in diesem Buch diese Effekte aber nicht. Modelle, die die Speicherhierarchie einschließen, sind um einiges komplexer als das RAM-Modell, sodass es schwieriger sein kann, mit ihnen zu arbeiten. Darüber hinaus liefern die RAM-Modell-Analysen gewöhnlich ausgezeichnete Vorhersagen des Leistungsverhaltens auf derzeitigen Maschinen. Schon einen einfachen Algorithmus im RAM-Modell zu untersuchen, kann eine große Herausforderung sein. Die erforderlichen mathematischen Hilfsmittel können Kombinatorik, Wahrscheinlichkeitstheorie, Algebra und die Fähigkeit, die maßgeblichen Terme in einer Formel zu identifizieren, einschließen. Da sich das Verhalten eines Algorithmus für jede mögliche Eingabe unterscheiden kann, benötigen wir ein Hilfsmittel, um dieses Verhalten in einfachen, leicht verständlichen Formeln zusammenzufassen. Selbst wenn wir normalerweise nur ein Maschinenmodell auswählen, um einen gegebenen Algorithmus zu analysieren, müssen wir uns noch für einen Weg entscheiden, wie wir unsere Analysen formulieren. Hier existieren verschiedene Alternativen. Wir hätten gern einen Weg, der einfach aufzuschreiben und zu handhaben ist, die wichtigsten Charakteristiken eines Algorithmus für dessen Anforderungen an die Ressourcen aufzeigt und unwesentliche Details vernachlässigt.
Analyse des Sortierens durch Einfügen Die beim Sortieren durch Einfügen benötigte Zeit hängt von der Eingabe ab: Das Sortieren von Tausenden von Zahlen dauert länger als das Sortieren von drei Zahlen. Au-
26
2 Ein einführendes Beispiel
ßerdem kann das Sortieren durch Einfügen unterschiedlichen Zeitaufwand für Eingabefolgen der gleichen Länge erfordern, da dieser davon abhängt, wie gut die Folgen bereits vorsortiert sind. Im Allgemeinen wächst die Zeit, die ein Algorithmus benötigt, mit der Größe der Eingabe. Daher ist es üblich, die Laufzeit eines Programmes als Funktion der Eingabegröße zu beschreiben. Um dies zu tun, müssen wir die Begriffe „Laufzeit“ und „Eingabegröße“ sorgfältiger definieren. Was die beste Definition für die Eingabegröße ist, hängt vom betrachteten Problem ab. Für viele Probleme, wie für das Sortieren oder Berechnen von diskreten FourierTransformierten, ist das natürliche Maß die Anzahl der Datensätze der Eingabe – beim Sortieren zum Beispiel die Länge n des zu sortierenden Feldes. Für viele andere Probleme, wie zum Beispiel die Multiplikation zweier Zahlen, ist das beste Maß für die Eingabegröße die Anzahl der Bits, die nötig sind, um die Eingabe in gewöhnlicher binärer Notation darzustellen. Manchmal ist es angemessener, die Eingabegröße durch zwei Zahlen anstatt durch eine einzige zu beschreiben. Wenn zum Beispiel die Eingabe eines Algorithmus ein Graph ist, kann die Eingabegröße durch die Anzahl der Knoten und die Anzahl der Kanten des Graphen beschrieben werden. Wir werden für jedes betrachtete Problem angeben, welches Maß für die Eingabegröße benutzt wird. Die Laufzeit eines Algorithmus für eine spezielle Eingabe ist die Anzahl der ausgeführten Grundoperationen oder „Schritte“. Es ist günstig, den Begriff Schritt so zu definieren, dass er so maschinenunabhängig wie möglich wird. Zunächst wollen wir uns die folgende Sichtweise zueigen machen. Für das Ausführen jeder Zeile unseres Pseudocodes ist ein konstanter Zeitaufwand erforderlich. Verschiedene Zeilen können unterschiedlichen Zeitaufwand erfordern. Wir gehen aber davon aus, dass jede Zeile i nur einen konstanten Zeitaufwand ci benötigt. Diese Sichtweise ist im Einklang mit dem RAM-Modell und sie spiegelt auch wider, wie der Pseudocode auf den meisten heutigen Rechnern zu implementieren wäre.5 In der folgenden Diskussion wird sich unser Ausdruck für die Laufzeit des Sortierens durch Einfügen von einer unhandlichen Formel, die die Kosten ci für sämtliche Anweisungen berücksichtigt, zu einer wesentlich einfacheren Notation entwickeln. Dieser Ausdruck ist prägnanter und einfacher zu handhaben. Wir beginnen, indem wir die Prozedur des Sortierens durch Einfügen nochmals hinschreiben und jede Anweisung mit den Kosten, also dem Zeitbedarf für die einmalige Ausführung, und der Anzahl der Durchläufe annotieren. Für jedes j = 2, 3, . . . , n mit n = A.l¨a nge geben wir mit tj an, wie viele Male die Abfrage der while-Schleife in Zeile 5 für den Wert j ausgeführt wird. Wenn eine for- oder while-Schleife auf normalem Weg verlassen wird (d. h. infolge der Abfrage im Schleifenkopf), dann wird die Abfrage genau einmal mehr ausgeführt als der Schleifenrumpf. Wir gehen davon aus, dass Kommentare keine ausführbaren Anweisungen sind und daher keine Zeit beanspruchen. 5 Es gibt hier ein paar Feinheiten. Rechenschritte, die wir in natürlicher Sprache formulieren, sind häufig Varianten einer Prozedur, die mehr als nur einen konstanten Zeitaufwand erfordern. Zum Beispiel werden wir weiter hinten im Buch als ein Rechenschritt „sortiere die Punkte nach der x-Koordinate“ angeben, was, wie wir sehen werden, einen nichtkonstanten Zeitaufwand erfordert. Beachten Sie außerdem, dass eine Anweisung, die eine Unterroutine aufruft, eine konstante Zeit benötigt, während die Unterroutine selbst unterschiedlich lange dauern kann. Das heißt, dass wir den Prozess des Aufrufs einer Unterroutine – das Übergeben der Parameter, usw. – vom Prozess des Ausführens einer Unterroutine unterscheiden.
2.2 Analyse von Algorithmen
27
Insertion-Sort(A) 1 for j = 2 to A.l¨a nge 2 schl¨u ssel = A[j] 3 // Setze A[j] in das sortierte // Teilfeld A[1 . . j − 1] ein. 4 i = j−1 5 while i > 0 und A[i] > schl¨u ssel 6 A[i + 1] = A[i] 7 i = i−1 8 A[i + 1] = schl¨u ssel
Kosten c1 c2
Anzahl n n−1 n−1 n − 1
0 c4 c5 c6 c7 c8
n
tj j=2 n (t j − 1) j=2 n j=2 (tj − 1) n−1
Die Laufzeit des Algorithmus ist die Summe der Laufzeiten aller ausgeführten Anweisungen; eine Anweisung, die ci Schritte benötigt, um ausgeführt zu werden und die n-mal ausgeführt wird, liefert den Beitrag ci n zur gesamten Laufzeit.6 Um die Laufzeit T (n) für das Sortieren durch Einfügen zu berechnen, summieren wir die Produkte aus Kosten und Anzahl und erhalten T (n) = c1 n + c2 (n − 1) + c4 (n − 1) + c5
n j=2
+ c7
n
tj + c 6
n (tj − 1) j=2
(tj − 1) + c8 (n − 1) .
j=2
Selbst wenn die Eingabe eine fest vorgegebene Größe hat, kann die Laufzeit eines Algorithmus davon abhängen, welche Eingabe dieser Größe betrachtet wird. Zum Beispiel tritt beim Sortieren durch Einfügen der günstigste Fall ein, wenn das zu sortierende Feld bereits sortiert ist. Für jedes j = 2, 3, . . . , n stellen wir fest, dass in Zeile 5 A[i] ≤ schl¨u ssel gilt, wenn i seinen Startwert j − 1 annimmt. Daher gilt in diesem Fall tj = 1 für j = 2, 3, . . . , n und die bestmögliche Laufzeit ist T (n) = c1 n + c2 (n − 1) + c4 (n − 1) + c5 (n − 1) + c8 (n − 1) = (c1 + c2 + c4 + c5 + c8 )n − (c2 + c4 + c5 + c8 ) . Wir können diese Laufzeit in der Form a n + b ausdrücken, wobei a und b Konstanten sind, die von den Kosten ci der Anweisungen abhängen; sie ist also eine lineare Funktion in n. Falls das Feld in umgekehrter Reihenfolge – d. h. in absteigender Reihenfolge – sortiert ist, liegt der schlechteste Fall vor. Wir müssen jedes Element A[j] mit sämtlichen Elementen des bereits sortierten Teils A[1 . . j − 1] des Feldes vergleichen, und damit wird tj = j für alle j = 2, 3, . . . , n. Wegen n j=2
j=
n(n + 1) −1 2
6 Dies gilt nicht zwingend auch für eine Ressource wie den Speicherplatz. Eine Anweisung, die m Speicherwörter belegt und n-mal ausgeführt wird, muss insgesamt nicht notwendigerweise Speicherplatz in der Größe von m n Wörtern verbrauchen.
28
2 Ein einführendes Beispiel
und n j=2
(j − 1) =
n(n − 1) 2
(Anhang A gibt einen Überblick, wie diese Summenformeln berechnet werden) erhalten wir, dass die Laufzeit beim Sortieren durch Einfügen im schlechtesten Fall n(n + 1) −1 T (n) = c1 n + c2 (n − 1) + c4 (n − 1) + c5 2 n(n − 1) n(n − 1) +c6 + c7 + c8 (n − 1) 2 2 c c6 c7 2 c6 c7 c5 5 = + + n + c1 + c2 + c4 + − − + c8 n 2 2 2 2 2 2 −(c2 + c4 + c5 + c8 )
ist. Wir können also die Laufzeit im schlechtesten Fall in der Form a n2 +b n+c angeben, wobei die Konstanten a, b und c wiederum nur von den Kosten ci der Anweisungen abhängen. Dies ist eine quadratische Funktion in n. Typischerweise ist die Laufzeit eines Algorithmus wie beim Sortieren durch Einfügen für eine bestimmte Eingabe konstant. Allerdings werden wir in späteren Kapiteln einige interessante „randomisierte“ Algorithmen betrachten, deren Verhalten selbst bei fester Eingabe variieren kann.
Analyse des schlechtesten Falls und des mittleren Falls Bei unserer Analyse des Sortierens durch Einfügen haben wir uns sowohl den günstigsten Fall, in dem das Eingabefeld bereits sortiert war, als auch den schlechtesten Fall, in dem das Eingabefeld in umgekehrter Reihenfolge sortiert war, angesehen. Für den Rest des Buches werden wir uns jedoch üblicherweise darauf konzentrieren, nur die Laufzeit im schlechtesten Fall, d. h. die längste Laufzeit, die das entsprechende Verfahren angewendet auf beliebige Eingaben der Größe n (höchstens) benötigt, anzugeben. Wir geben drei Gründe für diese Ausrichtung an. • Die Laufzeit eines Algorithmus im schlechtesten Fall gibt uns eine obere Schranke für die Laufzeit einer beliebigen Eingabe. Deren Kenntnis gibt uns eine Garantie dafür, dass der Algorithmus niemals länger brauchen wird. • Bei einigen Algorithmen tritt der schlechteste Fall ziemlich häufig auf. Zum Beispiel tritt beim Durchsuchen einer Datenbank nach einer speziellen Information der schlechteste Fall für den Suchalgorithmus oft dann auf, wenn die gesuchte Information nicht in der Datenbank vorhanden ist, und bei einigen Anwendungen mag das Suchen nach nicht vorhandener Information häufig vorkommen.
2.2 Analyse von Algorithmen
29
• Der „mittlere Fall“ ist oft annähernd genauso schlecht wie der schlechteste Fall. Nehmen wir an, dass wir n Zahlen zufällig auswählen und Sortieren durch Einfügen darauf anwenden. Wie lange dauert es zu bestimmen, an welcher Stelle des Teilfeldes A[1 . . j − 1] das Element A[j] einzuordnen ist? Im Mittel ist die Hälfte der Elemente in A[1 . . j −1] kleiner als A[j] und die Hälfte der Elemente ist größer. Deshalb prüfen wir im Mittel die Hälfte des Teilfeldes A[1 . . j − 1] und so ist tj ungefähr j/2. Es stellt sich heraus, dass die daraus resultierende mittlere Laufzeit eine quadratische Funktion in der Größe der Eingabe ist, genau wie die Laufzeit im schlechtesten Fall. In einigen besonderen Fällen sind wir an der mittleren Laufzeit eines Algorithmus interessiert. Wir werden die Methode der probabilistischen Analyse angewendet auf unterschiedliche Algorithmen das ganze Buch hindurch begegnen. Der Anwendungsbereich der Analyse des mittleren Falls ist beschränkt, da es nicht unbedingt offensichtlich ist, wie die „durchschnittliche“ Eingabe eines speziellen Problems aussieht. Häufig werden wir annehmen, dass alle Eingaben einer gegebenen Größe gleichwahrscheinlich sind. In der Praxis mag diese Annahme verletzt werden, aber wir können in einigen Fällen einen randomisierten Algorithmus benutzen, der durch „Würfeln“ eine probabilistische Analyse ermöglicht und zu einer erwarteten Laufzeit führt. Wir untersuchen randomisierte Algorithmen in Kapitel 5 und in einzelnen weiteren, nachfolgenden Kapiteln.
Wachstumsgrad Wir haben einige vereinfachende Abstraktionen benutzt, um unsere Analyse des Sortierens durch Einfügen zu erleichtern. Zuerst haben wir von den tatsächlichen Kosten jeder Anweisung abstrahiert, indem wir die Konstanten ci für die Darstellung dieser Kosten benutzt haben. Dann haben wir beobachtet, dass uns sogar diese Konstanten mehr Details liefern als wir tatsächlich benötigen: Wir haben die Laufzeit im schlechtesten Fall mit a n2 + b n + c angegeben, wobei a, b und c Konstanten sind, die (nur) von den Kosten ci der Anweisungen abhängen. So haben wir nicht nur von den tatsächlichen Kosten der Anweisung, sondern auch von den abstrakten Kosten ci abstrahiert. Wir werden nun eine weitere vereinfachende Abstraktion machen: Es ist die Wachstumsrate, oder der Wachstumsgrad der Laufzeit, der uns eigentlich interessiert. Wir betrachten deshalb nur den führenden Term einer Formel (zum Beispiel a n2 ), da die Terme niedrigerer Ordnung für große Werte von n relativ unwesentlich sind. Wir ignorieren auch den konstanten Koeffizienten des führenden Terms, da konstante Faktoren weniger signifikant als die Wachstumsrate sind, wenn man Recheneffizienz für große Eingaben bestimmt. Bei Sortieren durch Einfügen bleiben wir mit dem Faktor von n2 des führenden Terms, wenn wir von den Termen niedrigerer Ordnung und von der Konstante des führenden Terms abstrahieren. Wir schreiben, dass Sortieren durch Einfügen eine Laufzeit im schlechtesten Fall von Θ(n2 ) (gesprochen „Theta von n-Quadrat“) besitzt. Wir werden die Θ-Notation bereits in diesem Kapitel informal benutzen und sie im Kapitel 3 exakt definieren. Gewöhnlich betrachten wir einen Algorithmus als effizienter als einen anderen, wenn seine Laufzeit im schlechtesten Fall eine geringere Wachstumsrate aufweist. Aufgrund
30
2 Ein einführendes Beispiel
der konstanten Faktoren und Terme niedrigerer Ordnung kann ein Algorithmus, dessen Laufzeit eine höhere Wachstumsrate hat, weniger Zeit bei kleinen Eingabegrößen haben, als ein Algorithmus, dessen Laufzeit einer niedrigeren Wachstumsrate unterliegt. Für hinreichend große Eingaben wird jedoch zum Beispiel ein Θ(n2 )-Algorithmus im schlechtesten Fall schneller laufen als ein Θ(n3 )-Algorithmus.
Übungen 2.2-1 Drücken Sie die Formel n3 /1000 − 100 n2 − 100 n + 3 im Sinne der Θ-Notation aus. 2.2-2 Betrachten Sie das Sortieren von n Zahlen, die in einem Feld A gespeichert sind. Finden Sie zuerst das kleinste Element in A und tauschen Sie es mit A[1] aus. Finden Sie dann das zweitkleinste Element und tauschen Sie es mit A[2] aus. Fahren Sie für die ersten n − 1 Elemente von A in dieser Weise fort. Schreiben Sie ein Programm in Pseudocode für diesen als Sortieren durch Auswählen (engl.: selection sort ) bekannten Algorithmus. Welche Schleifeninvariante gilt für diesen Algorithmus? Warum müssen nur die ersten n − 1 Elemente anstatt n Elemente durchlaufen werden? Geben Sie die günstigste Laufzeit und die Laufzeit im schlechtesten Fall für Sortieren durch Auswählen in der Θ-Notation an. 2.2-3 Betrachten Sie noch einmal die lineare Suche (siehe Übung 2.1-3). Wie viele Elemente der Eingabefolge müssen im Mittel geprüft werden, wenn Sie davon ausgehen können, dass das gesuchte Element gleichwahrscheinlich irgendein Element von A ist? Wie verhält es sich im schlechtesten Fall? Wie ist die mittlere Laufzeit und wie die Laufzeit im schlechtesten Fall der linearen Suche, ausgedrückt in Θ-Notation? Begründen Sie ihre Antworten. 2.2-4 Wie können wir fast jeden Algorithmus modifizieren, um eine gute Laufzeit im günstigsten Fall zu erhalten?
2.3
Entwurf von Algorithmen
Wir können aus einer Vielfalt von Techniken zum Entwurf von Algorithmen auswählen. Bei Sortieren durch Einfügen wählten wir eine inkrementelle Herangehensweise: Nachdem wir das Teilfeld A[1 . . j − 1] sortiert haben, fügen wir das Element A[j] an der passenden Stelle ein, woraus sich das sortierte Teilfeld A[1 . . j] ergibt. In diesem Abschnitt untersuchen wir eine alternative Entwurfstechnik, die als „Teileund-Beherrsche“ (engl.: divide and conquer ) bekannt ist und die wir in Kapitel 4 detailliert untersuchen werden. Wir werden die Teile-und-Beherrsche-Methode benutzen, um einen Sortieralgorithmus zu entwerfen, dessen Laufzeit im schlechtesten Fall viel geringer als die von Sortieren durch Einfügen ist. Ein Vorteil solcher „Teile-und-Beherrsche“Algorithmen ist es, dass deren Laufzeiten häufig einfach durch die in Kapitel 4 eingeführten Techniken bestimmt werden können.
2.3 Entwurf von Algorithmen
2.3.1
31
Die Teile-und-Beherrsche-Methode
Viele nützliche Algorithmen sind bezüglich ihrer Struktur rekursiv : Um ein gegebenes Problem zu lösen, rufen sie sich selbst einmal oder mehrmals auf, um eng verwandte Teilprobleme zu behandeln. Diese Algorithmen folgen üblicherweise der Methode von Teile-und-Beherrsche: Sie teilen das Problem in mehrere Teilprobleme auf, die dem Ausgangsproblem ähneln, jedoch von kleinerer Größe sind. Sie lösen die Teilprobleme rekursiv und kombinieren diese Lösungen dann, um die Lösung des eigentlichen Problems zu erstellen. Das Paradigma von Teile-und-Beherrsche umfasst drei Schritte auf jeder Rekursionsebene: Teile das Problem in mehrere Teilprobleme auf, die kleinere Instanzen des gleichen Problems darstellen. Beherrsche die Teilprobleme rekursiv. Wenn die Teilprobleme klein genug sind, dann löse die Teilprobleme auf direktem Wege. Vereinige die Lösungen der Teilprobleme zur Lösung des ursprünglichen Problems. Der Algorithmus des Sortierens durch Mischen folgt genau dem Paradigma von Teile-und-Beherrsche. Intuitiv arbeitet er wie folgt. Teile: Teile die Folge von n Elementen in zwei Teilfolgen von je n/2 Elementen auf. Beherrsche: Sortiere die zwei Teilfolgen rekursiv mithilfe von Sortieren durch Mischen. Vereinige: Mische die zwei sortierten Teilfolgen, um die sortierte Lösung zu erzeugen. Die Rekursion bricht ab, wenn die zu sortierende Teilfolge die Länge 1 hat. In diesem Fall ist nichts mehr zu tun, da jede Folge der Länge 1 bereits sortiert ist. Die zentrale Operation des Sortierens durch Mischen ist das Mischen der zwei sortierten Folgen im dritten Schritt. Wir mischen, indem wir eine Hilfsprozedur Merge(A, p, q, r) aufrufen, wobei A ein Feld ist und p, q und r Indizes des Feldes sind, für die p ≤ q < r gilt. Die Prozedur geht davon aus, dass sich die Teilfelder A[p . . q] und A[q + 1 . . r] in sortierter Reihenfolge befinden. Sie mischt diese, um so ein einziges sortiertes Teilfeld zu bekommen, das das aktuelle Teilfeld A[p . . r] ersetzt. Unsere Merge-Prozedur benötigt Zeit Θ(n), wobei n = r − p + 1 die Gesamtanzahl der zu mischenden Elemente ist. Sie arbeitet wie folgt: In der Sprache des Kartenspiels formuliert, haben wir zwei aufgedeckte Kartenhaufen auf dem Tisch liegen. Jeder Haufen ist sortiert, wobei die Karte mit dem kleinsten Wert oben liegt. Wir möchten diese zwei Haufen zu einem einzigen sortierten Ausgabehaufen vereinigen, der verdeckt auf dem Tisch liegt. Unser grundlegender Schritt besteht darin, die kleinere der beiden auf den zwei Haufen oben liegenden Karten auszuwählen, sie vom Haufen wegzunehmen (wobei eine neue oberste Karte aufgedeckt wird) und diese Karte mit dem Gesicht nach unten auf den Ausgabehaufen zu legen. Wir wiederholen diesen Schritt, bis einer der
32
2 Ein einführendes Beispiel
beiden ursprünglichen Haufen aufgebraucht ist, wobei wir danach einfach den verbliebenen Haufen nehmen und ihn mit dem Gesicht nach unten auf den Ausgabehaufen legen. Von der Berechnung her benötigt jeder Grundschritt eine konstante Zeit, da wir lediglich die zwei obersten Karten zu vergleichen haben. Da wir höchstens n Grundschritte ausführen, benötigt das Mischen die Zeit Θ(n). Der folgende Pseudocode setzt obige Ideen um, allerdings mit einem zusätzlichen Trick, der vermeidet, dass wir bei jedem Grundschritt überprüfen müssen, ob einer der beiden Haufen aufgebraucht ist. Wir legen an das Ende eines jeden Haufens eine Wächterkarte, die einen speziellen Wert enthält. Wir benutzen sie, um unseren Code zu vereinfachen. Als Wächterwert verwenden wir hier ∞, sodass wenn eine Karte mit dem Wert ∞ aufgedeckt wird, dies nicht die kleinere Karte sein kann, außer wenn bei beiden Haufen die Wächterkarte aufgedeckt ist. Allerdings sind, wenn dies eintritt, bereits alle „normalen“ Karten auf den Ausgabehaufen gelegt worden. Da wir im Voraus wissen, dass genau r − p + 1 Karten auf den Ausgabehaufen gelegt werden, können wir stoppen, wenn wir genau so viele Grundschritte ausgeführt haben. Merge(A, p, q, r) 1 n1 = q − p + 1 2 n2 = r − q 3 seien L[1 . . n1 + 1] und R[1 . . n2 + 1] zwei neue Felder 4 for i = 1 to n1 5 L[i] = A[p + i − 1] 6 for j = 1 to n2 7 R[j] = A[q + j] 8 L[n1 + 1] = ∞ 9 R[n2 + 1] = ∞ 10 i = 1 11 j = 1 12 for k = p to r 13 if L[i] ≤ R[j] 14 A[k] = L[i] 15 i = i+1 16 else A[k] = R[j] 17 j = j+1 Im Detail arbeitet die Prozedur Merge wie folgt. Zeile 1 bestimmt die Länge n1 des Teilfeldes A[p . . q] und Zeile 2 die Länge n2 des Teilfeldes A[q + 1 . . r]. In Zeile 3 legen wir die Felder L und R („links“ und „rechts“) mit der Länge n1 + 1 beziehungsweise n2 + 1 an. Die for-Schleife in den Zeilen 4–5 kopiert das Teilfeld A[p . . q] in L[1 . . n1 ] und die for-Schleife in den Zeilen 6–7 kopiert das Teilfeld A[q + 1 . . r] in R[1 . . n2 ]. Die Zeilen 8–9 setzen die Wächter an das Ende von L und R. Die Zeilen 10–17, die in Abbildung 2.3 illustriert werden, führen die r − p + 1 Grundschritte aus, wobei die folgende Schleifeninvariante aufrecht erhalten wird: Zu Beginn jeder Iteration der for-Schleife in den Zeilen 12–17 enthält das Teilfeld A[p . . k − 1] die k − p kleinsten Elemente aus L[1 . . n1 + 1] und
2.3 Entwurf von Algorithmen 8
9
A … 2 k L
33 8
10 11 12 13 14 15 16 17
4
5
7
1
2
3
9
A … 1
6 …
1
2
3
4
5
1
2
3
4
2 i
4
5
7 ∞
R 1 j
2
3
6 ∞
5
L
10 11 12 13 14 15 16 17
4 k
5
8
9
L
1
2
3
2
4 i
5
5 k
4
5
7 ∞
7
1
3
6 …
2
3
4
5
1
2
3
4
4
5
7 ∞
R 1
2 j
3
6 ∞
5
(b)
2
3
8
1
2
3
2 j
3
4
9
A … 1
6 …
R 1 (c)
2
1
10 11 12 13 14 15 16 17
2
1
2 i
(a)
A … 1
7
5
6 ∞
L
1
2
3
2
4 i
5
10 11 12 13 14 15 16 17
2 2 4
7 1 k
5
7 ∞
2
3
6 …
1
2
3
R 1
2
3 j
4
5
6 ∞
(d)
Abbildung 2.3: Die Operation der Zeilen 10–17 beim Aufruf von Merge(A, 9, 12, 16), wenn das Teilfeld A[9 . . 16] die Folge 2, 4, 5, 7, 1, 2, 3, 6 enthält. Nach dem Kopieren und dem Einfügen der Wächter enthält das Feld L die Folge 2, 4, 5, 7, ∞ und das Feld R die Folge 1, 2, 3, 6, ∞. Schwach schattierte Positionen in A enthalten bereits ihre endgültigen Werte, und schwach schattierte Positionen in L und R enthalten Werte, die noch nach A zurückkopiert werden müssen. Zusammengenommen beinhalten die schattierten Positionen immer die ursprünglich in A[9 . . 16] enthaltenen Werte zusammen mit den beiden Wächtern. Stark schattierte Positionen in A enthalten Werte, die noch überschrieben werden, und stark schattierte Positionen in L und R enthalten Werte, die bereits nach A zurückkopiert wurden. (a)–(h) Die Felder A, L und R und deren entsprechenden Indizes k, i und j vor jeder Iteration der Schleife in den Zeilen 12–17.
R[1 . . n2 + 1] in sortierter Reihenfolge. Darüber hinaus sind L[i] und R[j] die kleinsten Elemente ihrer Felder, die noch nicht nach A zurückkopiert wurden. Wir müssen zeigen, dass die Schleifeninvariante vor der ersten Iteration der for-Schleife in den Zeilen 12–17 erfüllt wird, keine Iteration der Schleife die Invariante verletzt und die Invariante bei Abbruch der Schleife eine Eigenschaft liefert, die geeignet ist, um die Korrektheit des Algorithmus zu zeigen. Initialisierung Vor der ersten Iteration der Schleife gilt k = p, sodass das Teilfeld A[p . . k − 1] leer ist. Dieses Teilfeld enthält die k − p = 0 kleinsten Elemente von L und R, und wegen i = j = 1 sind sowohl L[i] als auch R[j] die kleinsten Elemente ihrer Felder, die noch nicht nach A zurückkopiert wurden. Fortsetzung Um zu zeigen, dass jede Iteration die Schleifeninvariante erhält, nehmen wir zunächst L[i] ≤ R[j] an. Dann ist L[i] das kleinste Element, das noch nicht nach A zurückkopiert worden ist. Weil A[p . . k − 1] die k − p kleinsten Elemente enthält, wird das Teilfeld A[p . . k] die k − p + 1 kleinsten Elemente enthalten,
34
2 Ein einführendes Beispiel 8
9
A … 1
L
10 11 12 13 14 15 16 17
2
2
3
1 k
2
3
8
6 …
1
2
3
4
5
1
2
3
4
2
4 i
5
7 ∞
R 1
2
3
6 ∞ j
5
L
9
10 11 12 13 14 15 16 17
A … 1 2
2
1
2
3
4
5
1
2
3
4
2
4
5 i
7 ∞
R 1
2
3
6 ∞ j
(e)
8
9
A … 1
L
1
2
3
2
4
5
2
4
5
3
7 ∞ i
4
5
3 k
8
6 …
1
2
3
R 1
2
3
4
8
9
L
9
A … 1 5
6 ∞ j
(g)
A … 1
4
2 k
3
6 … 5
(f)
10 11 12 13 14 15 16 17
2
3
L
1
2
3
2
4
5
10 11 12 13 14 15 16 17
2
2
4
5
7 ∞ i
3
5
6 6 … k
1
2
3
R 1 2
3
4
4
5
6 ∞ j
(h)
10 11 12 13 14 15 16 17
2
2 5
1
2
3
4
2
4
5
7 ∞ i
3
4
5 6 1
7 … k
2
3
4
R 1 2
3
6 ∞ j
5
(i)
Abbildung 2.3, fortgesetzt: (i) Die Felder und Indizes nach Terminierung. Zu diesem Zeitpunkt ist das Teilfeld A[9 . . 16] sortiert und die zwei Wächter in L und R sind die einzigen zwei Elemente in diesen Feldern, die nicht nach A kopiert worden sind.
nachdem in Zeile 14 der Wert L[i] nach A[k] kopiert wurde. Inkrementieren von k (die Inkrementierung erfolgt durch die implizite Laufvariablen-Aktualisierung der for-Schleife) und i (in Zeile 15) stellt die Schleifeninvariante für die nächste Iteration wieder her. Wenn dagegen L[i] > R[j] gilt, führen die Zeilen 16–17 die entsprechende Aktion aus, um die Schleifeninvariante zu erhalten. Terminierung Beim Abbruch gilt k = r + 1. Wegen der Schleifeninvariante enthält das Teilfeld A[p . . k − 1], d. h. A[p . . r], die k − p = r − p + 1 kleinsten Elemente von L[1 . . n1 + 1] und R[1 . . n2 + 1] in sortierter Reihenfolge. Die Felder L und R enthalten zusammen n1 + n2 + 2 = r − p + 3 Elemente. Alle Elemente außer den beiden größten sind zurück nach A kopiert worden, wobei diese größten Elemente die beiden Wächter sind. Um zu zeigen, dass die Prozedur Merge in Zeit Θ(n) mit n = r − p + 1 läuft, stellen wir fest, dass jede der Zeilen 1–3 und 8–11 konstante Zeit benötigt, die for-Schleifen in den Zeilen 4–7 benötigen die Zeit Θ(n1 + n2 ) = Θ(n) 7 und es gibt n Iterationen der 7 Wir werden in Kapitel 3 sehen, wie wir Gleichungen formal zu interpretieren haben, die die ΘNotation enthalten.
2.3 Entwurf von Algorithmen
35
for-Schleife in den Zeilen 12–17, von denen jede konstante Zeit benötigt. Wir können nun die Prozedur Merge als Unterroutine des Algorithmus Sortieren durch Mischen benutzen. Die Prozedur Merge-Sort(A, p, r) sortiert die Elemente im Teilfeld A[p . . r]. Falls p ≥ r gilt, hat das Teilfeld höchstens ein Element und ist deshalb bereits sortiert. Anderenfalls berechnet der Schritt des Teilens lediglich einen Index q, der A[p . . r] in zwei Teilfelder aufteilt: A[p . . q] enthält n/2 Elemente und A[q + 1 . . r] enthält n/2 Elemente.8 Merge-Sort(A, p, r) 1 if p < r 2 q = (p + r)/2 3 Merge-Sort(A, p, q) 4 Merge-Sort(A, q + 1, r) 5 Merge(A, p, q, r) Um eine Folge A zu sortieren, rufen wir Merge-Sort(A, 1, A.l¨a nge) auf, wobei wieder einmal A.l¨a nge = n gilt. Abbildung 2.4 illustriert die Vorgehensweise der Prozedur für den Fall, dass n eine Potenz von 2 ist. Der Algorithmus besteht darin, Paare von einelementigen Folgen zu mischen und Folgen der Länge 2 zu bilden, Paare von Folgen der Länge 2 zu mischen und Folgen der Länge 4 zu bilden, usw., bis zwei Folgen der Länge n/2 gemischt werden und die endgültig sortierte Folge der Länge n gebildet wird.
2.3.2
Analyse von Teile-und-Beherrsche-Algorithmen
Wenn ein Algorithmus sich selbst rekursiv aufruft, dann können wir oftmals seine Laufzeit in Form einer Rekursionsgleichung angeben, die die Gesamtlaufzeit für ein Problem der Größe n in Abhängigkeit der Laufzeit für Probleme kleinerer Größe beschreibt. Wir können dann mittels mathematischer Hilfsmittel die Rekursionsgleichung lösen und Schranken für die Performanz des Algorithmus liefern. Eine Rekursionsgleichung für die Laufzeit eines Teile-und-Beherrsche-Algorithmus basiert auf den drei Schritten des zugrunde liegenden Paradigmas. Wie vorhin sei T (n) die Laufzeit eines Problems der Größe n. Wenn die Größe des Problems hinreichend klein ist, zum Beispiel n ≤ c für eine Konstante c, dann benötigt die direkte Lösung eine konstante Zeit, die wir mit Θ(1) beschreiben. Nehmen wir an, dass unsere Aufteilung des Problems zu a Teilproblemen führt, von denen jedes die Größe 1/b der Größe des ursprünglichen Problems hat. (Für Sortieren durch Mischen haben sowohl a als auch b den Wert 2; wir werden aber viele Teile-und-Beherrsche Algorithmen kennenlernen, bei denen a = b gilt.) Das Lösen eines Teilproblems der Größe n/b dauert Zeit T (n/b) und so benötigt das Lösen von a Problemen der Größe n/b Zeit a T (n/b). Wenn wir die Zeit D(n) benötigen, um das Problem in Teilprobleme aufzuteilen und die Zeit C(n), um die 8 Der Ausdruck x bezeichnet die kleinste ganze Zahl, die größer oder gleich x ist und x bezeichnet die größte ganze Zahl, die kleiner oder gleich x ist. Diese Bezeichnungen werden in Kapitel 3 definiert. Der einfachste Weg, um zu zeigen, dass wir zwei Teilfelder A[p . . q] und A[q + 1 . . r], die jeweils die Größe n/2 bzw. n/2 haben, erhalten, wenn wir q gleich (p + r)/2 setzen, ist, die vier Fälle zu untersuchen, die abhängig davon, ob p und r ungerade oder gerade sind, auftreten.
36
2 Ein einführendes Beispiel sortierte Sequenz 1
2
2
3
4
5
6
7
1
2
3
mischen 2
4
5
7
mischen 2
mischen
5
4
mischen 5
7
1
mischen 2
6
4
3
2
mischen 7
1
6
mischen 3
2
6
Anfangssequenz Abbildung 2.4: Die Vorgehensweise von Sortieren durch Mischen auf dem Feld A = 5, 2, 4, 7, 1, 3, 2, 6. Die Länge der zu mischenden sortierten Folgen erhöht sich mit Fortschreiten des Algorithmus von unten nach oben.
Lösung der Teilprobleme zur Lösung des ursprünglichen Problems zusammenzufügen, dann erhalten wir die Rekursionsgleichung Θ(1) falls n ≤ c , T (n) = a T (n/b) + D(n) + C(n) sonst . In Kapitel 4 werden wir sehen, wie wir häufig auftretende Rekursionsgleichungen dieser Art lösen können. Analyse von Sortieren durch Mischen. Auch wenn der Pseudocode von Merge-Sort korrekt arbeitet, wenn die Anzahl der Elemente nicht gerade ist, vereinfacht sich unsere auf einer Rekursionsgleichung basierende Analyse, wenn wir annehmen, dass die Größe des ursprünglichen Problems eine Potenz von 2 ist. Das Teilen führt dann zu zwei Teilproblemen, die genau die Größe n/2 besitzen. In Kapitel 4 werden wir sehen, dass diese Annahme den Wachstumsgrad der Laufzeit für die Lösung der Rekursionsgleichung nicht beeinflusst. Wir erörtern im Folgenden, wie wir zu der Rekursionsgleichung für die Laufzeit T (n) von Sortieren durch Mischen von n Zahlen im schlechtesten Fall kommen können. Sortieren durch Mischen benötigt für das Bearbeiten eines Elementes eine konstante Zeit. Wenn wir n > 1 Elemente vorliegen haben, dann schlüsseln wir die Laufzeit wie folgt auf. Teile Während der Teile-Phase wird einfach nur die Mitte des Feldes berechnet. Dies benötigt (bei einmaliger Ausführung) konstante Zeit. Somit gilt D(n) = Θ(1).
2.3 Entwurf von Algorithmen
37
Beherrsche Wir lösen zwei Teilprobleme der Größe n/2 rekursiv, was 2 T (n/2) zur Laufzeit beiträgt. Vereinige Wir haben bereits festgestellt, dass die Prozedur Merge auf einem Teilfeld der Länge n die Zeit Θ(n) benötigt, und so C(n) = Θ(n) gilt. Wenn wir die Funktionen D(n) und C(n) zur Analyse des Sortierens durch Mischen addieren, dann addieren wir eine Funktion der Ordnung Θ(n) und eine Funktion der Ordnung Θ(1). Diese Summe ist eine lineare Funktion in n, d. h. der Ordnung Θ(n). Wenn wir sie zum Term 2 T (n/2) aus dem Schritt „Beherrsche“ addieren, dann ergibt sich als Rekursionsgleichung für die Laufzeit T (n) von Sortieren durch Mischen im schlechtesten Fall Θ(1) falls n = 1 , T (n) = (2.1) 2 T (n/2) + Θ(n) falls n > 1 . In Kapitel 4 werden wir das Mastertheorem kennen lernen, mit dem wir zeigen können, dass T (n) von der Ordnung Θ(n lg n) ist, wobei lg n für log2 n steht. Da die Logarithmusfunktion langsamer anwächst als die lineare Funktion, ist Sortieren durch Mischen mit seiner Laufzeit von Θ(n lg n) bei hinreichend großen Zahlen besser als Sortieren durch Einfügen, dessen Laufzeit im schlechtesten Fall Θ(n2 ) ist. Um intuitiv zu verstehen, warum sich aus der Lösung der Rekursionsgleichung (2.1) T (n) = Θ(n lg n) ergibt, benötigen wir das Mastertheorem nicht. Wir formen die Rekursionsgleichung (2.1) zu c falls n = 1 , T (n) = (2.2) 2 T (n/2) + c n falls n > 1 , um, wobei die Konstante c sowohl für die zum Lösen eines Problems der Größe 1 als auch für die pro Feldelement für die Schritte des Teilens und Kombinierens benötigte Zeit steht.9 Abbildung 2.5 zeigt, wie wir die Rekursionsgleichung (2.2) lösen können. Der Einfachheit halber nehmen wir an, dass n eine Potenz von 2 ist. Teil (a) der Abbildung zeigt T (n), das in Teil (b) zu einem äquivalenten Baum erweitert wurde, der die Rekursionsgleichung veranschaulicht. Der Term c n steht in der Wurzel und stellt den Aufwand der Rekursion in der ersten Ebene dar; die Teilbäume der Wurzel stehen für die beiden kleineren Rekurrenzen T (n/2). Teil (c) zeigt diesen Prozess, nachdem die Rekursionsgleichung jeweils einmal auf T (n/2) angewendet wurde. Die Kosten für jeden der beiden Knoten in der zweiten Rekursionsebene sind c n/2. Wir setzen die Expansion der Baumknoten fort, indem wir sie in ihre durch die Rekursion bestimmten Bestandteile zerle9 Es ist unwahrscheinlich, dass genau dieselbe Konstante sowohl für die zum Lösen eines Problems der Größe 1 als auch für die pro Feldelement in den Schritten des Teilens und Zusammenfügens benötigte Zeit steht. Wir können dieses Problem umgehen, indem wir c den größeren dieser beiden Werte zuweisen und annehmen, dass unsere Rekursionsgleichung eine obere Schranke für die Laufzeit liefert, oder indem wir c den kleineren der beiden Werte zuweisen und annehmen, dass unsere Rekursionsgleichung eine untere Schranke für die Laufzeit liefert. Beide Schranken sind von der Ordnung n lg n, und zusammengenommen ergibt dies eine Laufzeit von Θ(n lg n).
38
2 Ein einführendes Beispiel
T(n)
cn
T(n/2)
cn
T(n/2)
cn/2
T(n/4) (a)
cn/2
T(n/4)
(b)
T(n/4)
T(n/4)
(c)
cn
cn
cn/2
cn/2
cn
lg n cn/4
cn/4
cn/4
cn
…
cn/4
c
c
c
c
c
…
c
c
cn
n (d)
Insgesamt: cn lg n + cn
Abbildung 2.5: Die Konstruktion eines Rekursionsbaumes für die Rekursionsgleichung T (n) = 2 T (n/2)+c n. (a) zeigt T (n), welches sich in (b)–(d) schrittweise entwickelt, was zum Rekursionsbaum führt. Der voll expandierte Baum, der in Teil (d) gezeigt wird, hat lg n + 1 Ebenen (d. h. er besitzt wie angegeben die Höhe lg n). Jede Ebene des voll expandierten Baumes trägt die Kosten c n zu den Gesamtkosten bei. Deshalb ergibt sich für die Gesamtkosten c n lg n + c n, was von der Ordnung Θ(n lg n) ist.
2.3 Entwurf von Algorithmen
39
gen, bis Probleme der Größe 1 vorliegen, die jeweils mit konstanten Kosten c gelöst werden können. Teil (d) zeigt den resultierenden Rekursionsbaum. Als nächstes addieren wir die Kosten innerhalb jeder Ebene des Baumes. Für die oberste Ebene ergeben sich die Kosten c n, für die nächste Ebene ergeben sich Gesamtkosten von c(n/2)+ c(n/2) = c n, die Ebene darunter hat die Kosten c(n/4)+ c(n/4)+ c(n/4)+ c(n/4) = c n usw. Im Allgemeinen besitzt die i-te Ebene unterhalb der Wurzel 2i Knoten, jeder von ihnen trägt mit c(n/2i ) zu den Kosten bei, sodass sich für die i-te Ebene die Gesamtkosten von 2i c(n/2i ) = c n ergeben. Die unterste Ebene besteht aus n Knoten, die jeweils den Wert c zu den Kosten beitragen, was wiederum Gesamtkosten von c n ergibt. Die Gesamtzahl der Ebenen des Rekursionsbaumes in Abbildung 2.5 beträgt lg n+1, wobei n die Anzahl der Blätter darstellt, welche zu der Größe der Eingabe korrespondiert. Ein (eher informaler) Induktionsbeweis beweist diese Aussage. Den Induktionsanfang bildet der Fall n = 1, in dem nur eine Ebene existiert. Wegen lg 1 = 0 ergibt sich aus lg n + 1 die korrekte Anzahl der Ebenen für diesen Fall. Nehmen Sie nun als Induktionshypothese an, dass die Anzahl der Ebenen eines Rekursionsbaumes mit 2i Blättern gleich lg 2i + 1 = i + 1 ist (für jeden Wert von i gilt lg 2i = i). Da wir annehmen, dass die tatsächliche Eingabegröße eine Potenz von 2 ist, ist die nächste zu betrachtende Eingabegröße 2i+1 . Ein Baum mit 2i+1 Blättern hat eine Ebene mehr als ein Baum mit 2i Blättern und so erhalten wir für die Gesamtanzahl der Ebenen (i + 1) + 1 = lg 2i+1 + 1. Um die Gesamtkosten, die durch die Rekursionsgleichung (2.2) beschrieben werden, zu berechnen, addieren wir einfach die Kosten aller Ebenen. Der Rekursionsbaum besteht aus lg n + 1 Ebenen. Jede dieser Ebenen verursacht Kosten in Höhe von c n, was zu Gesamtkosten von c n(lg n+1) = c n lg n+c n führt. Ignorieren wir den Term niedrigerer Ordnung und die Konstante c, so erhalten wir das gewünschte Resultat von Θ(n lg n).
Übungen 2.3-1 Illustrieren Sie gemäß Abbildung 2.4 die Arbeitsweise von Sortieren durch Mischen angewendet auf das Feld A = 3, 41, 52, 26, 38, 57, 9, 49 2.3-2 Schreiben Sie die Prozedur Merge so um, dass sie keine Wächter benutzt, sondern dann anhält, wenn entweder aus L oder R alle Elemente nach A zurückkopiert wurden. Der Rest des anderen Feldes soll danach in A zurückkopiert werden. 2.3-3 Zeigen Sie durch mathematische Induktion, dass sich für die Lösung der Rekursionsgleichung 2 falls n = 2 , T (n) = 2 T (n/2) + n falls n = 2k , für k > 1 der Wert T (n) = n lg n ergibt, wenn n eine Potenz von 2 ist. 2.3-4 Wir können Sortieren durch Einfügen wie folgt als rekursive Prozedur beschreiben. Um A[1 . . n] zu ordnen, sortieren wir A[1 . . n − 1] rekursiv und setzen A[n] in das sortierte Feld A[1 . . n − 1] ein. Schreiben Sie eine Rekursionsgleichung für
40
2 Ein einführendes Beispiel die Laufzeit im schlechtesten Fall dieser rekursiven Version von Sortieren durch Einfügen.
2.3-5 Wir kommen noch einmal auf das Suchproblem (siehe Übung 2.1-3) zurück. Beachten Sie, dass, wenn die Folge A sortiert ist, wir v mit dem mittleren Element der Folge vergleichen können und nach diesem Vergleich die Hälfte der Folge nicht weiter betrachten brauchen. Die binäre Suche ist ein Algorithmus, der diese Prozedur wiederholt anwendet, wobei der verbleibende Teil der Folge jedes Mal halbiert wird. Schreiben Sie ein iteratives oder rekursives Programm in Pseudocode für die binäre Suche. Begründen Sie, warum die Laufzeit der binären Suche im schlechtesten Fall Θ(lg n) ist. 2.3-6 Beachten Sie, dass die while-Schleife in den Zeilen 5–7 der Prozedur InsertionSort in Abschnitt 2.1 die lineare Suche benutzt, um das sortierte Teilfeld A[1 . . j − 1] (rückwärts) zu durchsuchen. Können wir stattdessen die binäre Suche (siehe Übung 2.3-5) benutzen, um die Laufzeit Θ(n lg n) von Sortieren durch Einfügen im schlechtesten Fall zu verbessern? 2.3-7∗ Beschreiben Sie einen Algorithmus der Ordnung Θ(n lg n), der für eine gegebene Menge S von n ganzen Zahlen und eine weitere ganze Zahl x bestimmt, ob es zwei Elemente in S gibt, deren Summe genau x ist.
Problemstellungen 2-1 Sortieren durch Einfügen auf kleinen Feldern in Sortieren durch Mischen Obwohl die Laufzeit von Sortieren durch Mischen im schlechtesten Fall von der Ordnung Θ(n lg n) und die von Sortieren durch Einfügen von der Ordnung Θ(n2 ) ist, ist Sortieren durch Einfügen bei kleinen n aufgrund der unterschiedlichen in den Θ-Notationen versteckten konstanten Faktoren auf vielen Maschinen schneller. Deshalb ist es sinnvoll, Sortieren durch Einfügen innerhalb des Sortierens durch Mischen zu verwenden, wenn die Teilprobleme hinreichend klein sind. Betrachten Sie eine Modifikation von Sortieren durch Mischen, bei der n/k Teillisten der Länge k durch Sortieren durch Einfügen sortiert und anschließend durch den Standardmechanismus gemischt werden, wobei k eine zu bestimmende Größe ist. a. Zeigen Sie, dass Sortieren durch Einfügen n/k Teillisten jeweils der Länge k im schlechtesten Fall in Zeit Θ(n k) sortieren kann. b. Zeigen Sie, wie man die Teillisten im schlechtesten Fall in Zeit Θ(n lg(n/k)) mischen kann. c. Davon ausgehend, dass der modifizierte Algorithmus im schlechtesten Fall in Zeit Θ(n k + n lg(n/k)) läuft, welches ist der größte Wert von k als Funktion von n, für den der modifizierte Algorithmus die gleiche asymptotische Laufzeit besitzt wie das normale Sortieren durch Mischen? Geben Sie den Wert von k in Θ-Notation an! d. Wie sollte k in der Praxis gewählt werden?
Problemstellungen zu Kapitel 2
41
2-2 Die Korrektheit von Sortieren durch Vertauschen Sortieren durch Vertauschen (engl.: bubblesort ) ist ein populärer, wenn auch ineffizienter Sortieralgorithmus. Er arbeitet, indem er wiederholt benachbarte Elemente vertauscht, die sich nicht in der richtigen Reihenfolge befinden. Bubblesort(A) 1 for i = 1 to A.l¨a nge − 1 2 for j = A.l¨a nge downto i + 1 3 if A[j] < A[j − 1] 4 vertausche A[j] mit A[j − 1] a. Sei A die Ausgabe von Bubblesort(A). Um die Korrektheit von Bubblesort zu beweisen, müssen wir zeigen, dass die Prozedur terminiert und dass A [1] ≤ A [2] ≤ · · · ≤ A [n]
(2.3)
gilt, mit n = A.l¨a nge. Was müssen wir außerdem beweisen, um zu zeigen, dass Bubblesort tatsächlich sortiert? Die nächsten beiden Punkte beweisen die Ungleichung (2.3). b. Geben Sie eine Schleifeninvariante für die for-Schleife in den Zeilen 2–4 an und beweisen Sie, dass die Schleifeninvariante erhalten bleibt. Ihr Beweis sollte wie die in diesem Kapitel gezeigten Beweise von Schleifeninvarianten aufgebaut sein. c. Geben Sie unter Verwendung der Abbruchbedingung für die in (b) bewiesene Schleifeninvariante eine Schleifeninvariante für die for-Schleife in den Zeilen 1–4 an, die es Ihnen erlaubt, die Ungleichung (2.3) zu beweisen. Wiederum sollte Ihr Beweis wie die in diesem Kapitel gezeigten Beweise von Schleifeninvarianten aufgebaut sein. d. Was ist die Laufzeit von Sortieren durch Vertauschen im schlechtesten Fall? Wie steht diese Laufzeit im Vergleich zu der von Sortieren durch Einfügen? 2-3 Die Korrektheit des Horner-Schemas Das folgende Codestück führt das Horner-Schema ein, um ein Polynom P (x) =
n
ak xk
k=0
= a0 + x(a1 + x(a2 + · · · + x(an−1 + xan ) · · · )) bei gegebenen Koeffizienten a0 , a1 , . . . , an und einem gegebenen Wert für x auszuwerten: 1 y =0 2 for i = n downto 0 3 y = ai + x · y
42
2 Ein einführendes Beispiel a. Welche Laufzeit (in Θ-Notation) hat dieses Codestück für das Horner-Schema? b. Schreiben Sie ein Programm in Pseudocode, das den naiven Algorithmus zur Polynomauswertung, der jeden Term des Polynoms von Grund auf neu berechnet, implementiert. Wie groß ist die Laufzeit dieses Algorithmus? Wie ist sie im Vergleich zu der des Horner-Schemas? c. Betrachten Sie die folgende Schleifeninvariante: Zu Beginn jeder Iteration der for-Schleife in den Zeilen 2–3 gilt
n−(i+1)
y=
ak+i+1 xk .
k=0
Interpretieren Sie eine Summenformel, die keine Terme enthält, als den Wert 0. Wenden Sie die Schleifenvariante an, um nach dem Vorbild der in diesem Kapitel bereits gezeigten Schleifeninvariantenbeweise zu beweisen, dass nach n Abbruch der Schleife y = k=0 ak xk gilt. d. Schließen Sie den Beweis ab, indem Sie erörtern, warum das gegebene Codestück ein Polynom, das durch die Koeffizienten a0 , a1 , . . . , an bestimmt ist, korrekt berechnet. 2-4 Inversionen Sei A[1 . . n] ein Feld mit n verschiedenen Zahlen. Wenn i < j und A[i] > A[j] gelten, dann wird das Paar (i, j) als Inversion von A bezeichnet. a. Geben Sie die fünf Inversionen des Feldes 2, 3, 8, 6, 1 an. b. Welches Feld mit Elementen aus der Menge {1, 2, . . . , n} besitzt die meisten Inversionen? Wie viele Inversionen sind in diesem Feld enthalten? c. Wie ist die Beziehung zwischen der Laufzeit von Sortieren durch Einfügen und der Anzahl der Inversionen im Eingabefeld? Begründen Sie Ihre Antwort. d. Geben Sie einen Algorithmus an, der die Anzahl der Inversionen einer Permutation von n Elementen bestimmt und dessen Laufzeit im schlechtesten Fall in Θ(n lg n) liegt. (Hinweis: Modifizieren Sie Sortieren durch Mischen.)
Kapitelbemerkungen Im Jahre 1968 veröffentlichte Knuth den ersten von drei Bänden mit dem allgemeinen Titel The Art of Computer Programming [209, 210, 211]. Der erste Band führte in das zeitgemäße Studium der Computeralgorithmen unter dem Gesichtspunkt der Analyse der Laufzeit ein. Die gesamte Ausgabe bleibt eine verbindliche und wertvolle Referenz für viele der hier vorgestellten Themen. Nach Knuth ist das Wort „Algorithmus“ vom Namen des Persischen Mathematikers „al-Khowârizmî“ aus dem neunten Jahrhundert abgeleitet. Aho, Hopcroft und Ullman [5] verwenden die asymptotische Analyse von Algorithmen als Mittel zum Vergleich der relativen Performanz von Algorithmen – sie verwendeten
Problemstellungen zu Kapitel 2
43
hierbei Notationen, die Kapitel 3 einführt, insbesondere Θ-Notation. Sie machten auch die Verwendung von Rekursionsgleichungen bekannt, um die Laufzeiten von rekursiven Algorithmen zu bestimmen. Knuth [211] liefert eine enzyklopädische Betrachtung vieler Sortieralgorithmen. Sein Vergleich von Sortieralgorithmen (Seite 381) schließt eine exakte schrittzählende Analyse, ähnlich der hier für Sortieren durch Einfügen durchgeführten, ein. Knuths Diskussion des Sortierens durch Einfügen umfasst verschiedene Variationen des Algorithmus. Die wichtigste von ihnen ist das Sortieren nach Shell, eingeführt von D. L. Shell, der Sortieren durch Einfügen auf periodische Teilsequenzen der Eingabe anwendet, was zu einem schnelleren Sortieralgorithmus führt. Sortieren durch Mischen wird von Knuth ebenfalls beschrieben. Er erwähnt, dass 1938 ein mechanischer Kartenmischer erfunden wurde, der in der Lage war, zwei Kartenstapel aus Lochkarten in einem Durchlauf zu mischen. J. von Neumann, einer der Pioniere der Informatik, entwickelte 1945 offenbar ein Programm für das Sortieren durch Mischen auf dem EDVAC-Computer. Die frühe Geschichte der Korrektheitsbeweise von Programmen wird durch Gries [153] beschrieben, der P. Naur für den ersten Artikel auf diesem Gebiet würdigt. Gries schreibt R. W. Floyd die Einführung der Schleifeninvarianten zu. Das Lehrbuch von Mitchell [256] beschreibt neueste Fortschritte auf dem Gebiet der Korrektheitsbeweise von Programmen.
3
Wachstum von Funktionen
Die in Kapitel 2 definierte Wachstumsrate der Laufzeit eines Algorithmus stellt ein einfaches Kriterium für dessen Effizienz dar und erlaubt es uns, die relative Leistungsfähigkeit alternativer Algorithmen zu vergleichen. Ist die Eingabegröße n hinreichend groß, dann schlägt Sortieren durch Mischen mit seiner Laufzeit von Θ(n lg n) im schlechtesten Fall das Sortieren durch Einfügen, dessen Laufzeit im schlechtesten Fall Θ(n2 ) ist. Obwohl wir manchmal die exakte Laufzeit eines Algorithmus bestimmen können – siehe z. B. Kapitel 2, in dem wir dies für Sortieren durch Einfügen getan haben –,ist die zusätzliche Genauigkeit den Aufwand, sie zu berechnen, nicht wert. Für ausreichend große Eingaben werden die multiplikativen Konstanten und Terme niedrigerer Ordnung der exakten Laufzeit von den Effekten der Eingabegröße selbst dominiert. Wenn wir Eingaben betrachten, die hinreichend groß sind, dass nur die Ordnung des Wachstums der Laufzeit relevant ist, dann untersuchen wir die asymptotische Effizienz eines Algorithmus. Das heißt, wir beschäftigen uns damit, wie die Laufzeit eines Algorithmus mit der Größe der Eingabe im Limes zunimmt, wenn die Eingabe ohne Beschränkung anwächst. Gewöhnlich wird ein asymptotisch effizienterer Algorithmus bei allen Eingaben, eventuell mit Ausnahme der kleinen Eingaben, die beste Wahl sein. Dieses Kapitel gibt verschiedene Standardmethoden zur Vereinfachung der asymptotischen Analyse von Algorithmen an. Der nächste Abschnitt beginnt mit der Definition verschiedener Typen von „asymptotischer Notation“, von denen wir bereits ein Beispiel in Θ-Notation kennen gelernt haben. Wir stellen dann verschiedene Bezeichnungskonventionen vor, die durchgehend im Buch verwendet werden. Abschließend geben wir einen Überblick über das Verhalten von Funktionen, die bei der Analyse von Algorithmen häufig auftreten.
3.1
Asymptotische Notation
Die Notationen, die wir verwenden, um die asymptotischen Laufzeiten von Algorithmen zu beschreiben, werden mithilfe von Funktionen formuliert, deren Definitionsbereich die Menge der natürlichen Zahlen N = {0, 1, 2, . . .} ist. Diese Bezeichnungen sind für die Beschreibung der Funktionen T (n), die die Laufzeit im schlechtesten Fall beschreiben und gewöhnlich nur für ganzzahlige Eingabegrößen definiert sind, geeignet. Wir finden es manchmal zweckmäßig, die asymptotische Notation auf verschiedene Art und Weisen zu missbrauchen. Wir dehnen an einigen Stellen die Notation z. B. auf den Definitionsbereich der reellen Zahlen aus, oder schränken sie auf eine Teilmenge der natürlichen Zahlen ein. Wir sollten uns aber sicher sein, dass wir die exakte Bedeutung der Notation verstehen, damit wir sie nicht falsch anwenden, wenn wir sie missbräuchlich verwenden. Dieser Abschnitt definiert die grundlegenden asymptotischen Notationen und führt auch
46
3 Wachstum von Funktionen
häufig benutzte missbräuchliche Anwendungen von ihnen ein.
Asymptotische Notationen, Funktionen und Laufzeiten Wir nutzen asymptotische Notationen hauptsächlich, um Laufzeiten von Algorithmen anzugeben, so wie wir schon in einem der vorherigen Kapiteln angegeben haben, dass die Laufzeit von Sortieren durch Einfügen im schlechtesten Fall gleich Θ(n2 ) ist. Asymptotische Notationen werden aber eigentlich auf Funktionen angewendet. Erinnern Sie sich daran, dass wir die Laufzeit von Sortieren durch Einfügen im schlechtesten Fall durch a n2 + b n + c für geeignete Konstanten a, b und c charakterisiert haben. Die Laufzeit von Sortieren durch Einfügen als Θ(n2 ) anzugeben, bedeutet, dass wir von einigen Details der Funktion abstrahiert haben. Da asymptotische Notationen auf Funktionen angewendet werden, haben wir mit Θ(n2 ) die Funktion a n2 + b n + c charakterisiert, die in diesem Fall zufälligerweise für die Laufzeit von Sortieren durch Einfügen im schlechtesten Fall steht. In diesem Buch werden die Funktionen, auf die wir asymptotische Notationen anwenden werden, üblicherweise Laufzeiten von Algorithmen charakterisieren. Asymptotische Notationen können jedoch auch auf Funktionen angewendet werden, die andere Aspekte eines Algorithmus (z. B. die Menge an Platz, die er benötigt) beschreiben, oder sogar überhaupt nichts mit Algorithmen zu tun haben. Auch dann wenn wir asymptotische Notationen auf Laufzeiten von Algorithmen anwenden, müssen wir verstehen, welche Laufzeit wir meinen. In einigen Fällen sind wir an der Laufzeit im schlechtesten Fall interessiert. Oft wollen wir aber auch die Laufzeit für eine beliebige Eingabe, welche das auch immer sein mag, charakterisieren. In anderen Worten formuliert: wir wollen oft eine umfassende Aussage haben, die über jede Eingabe etwas aussagt und nicht nur über die schlechtesten. Wir werden sehen, dass asymptotische Notationen auch hierfür gut geeignet sind.
Θ-Notation Im Kapitel 2 haben wir herausgefunden, dass die Laufzeit T (n) von Sortieren durch Einfügen im schlechtesten Fall Θ(n2 ) ist. Lassen Sie uns definieren, was diese Notation bedeutet. Für eine gegebene Funktion g bezeichnen wir mit Θ(g) die Menge der Funktionen Θ(g) = {f : es existieren positive Konstanten c1 , c2 , und n0 , sodass 0 ≤ c1 g(n) ≤ f (n) ≤ c2 g(n) für alle n ≥ n0 } .1
2
Eine Funktion f gehört zur Menge Θ(g), wenn positive Konstanten c1 und c2 existieren, sodass f (n) zwischen c1 g(n) und c2 g(n) für hinreichend große n eingeschlossen werden kann. Da Θ(g) eine Menge ist, könnten wir „f ∈ Θ(g)“ schreiben, um deutlich zu machen, dass f ein Element von Θ(g) ist. Stattdessen werden wir gewöhnlich „f = Θ(g)“ 1 Innerhalb
der Mengendefinition sollte ein Doppelpunkt als „sodass“ gelesen werden. des Übersetzers: Der Formalismus wurde an dieser Stelle gegenüber dem englischsprachigen Buch geringfügig geändert. 2 Bemerkung
3.1 Asymptotische Notation
47
c2 g(n)
cg(n) f (n)
f (n)
f (n) cg(n)
c1 g(n)
n0
n f (n) = Θ(g(n)) (a)
n0
n f (n) = O(g(n)) (b)
n0
n f (n) = Ω(g(n)) (c)
Abbildung 3.1: Grafische Beispiele für die Θ-, O- und Ω-Notation. In jeder Teilabbildung ist illustriert, wie klein der Wert n0 höchstens gewählt werden kann; jeder größere Wert wäre auch möglich. (a) Die Θ-Notation schränkt eine Funktion bis auf konstante Faktoren ein. Wir schreiben f (n) = Θ(g(n)), wenn positive Konstanten n0 , c1 und c2 existieren, sodass ab der Stelle n0 der Wert von f (n) immer zwischen c1 g(n) und c2 g(n) liegt. (b) Die O-Notation liefert eine obere Schranke (bis auf einen konstanten Faktor) für eine Funktion. Wir schreiben f (n) = O(g(n)), wenn positive Konstanten n0 und c existieren, sodass ab der Stelle n0 der Wert von f (n) immer unterhalb von c g(n) liegt. (c) Die Ω-Notation liefert eine untere Schranke (bis auf einen konstanten Faktor) einer Funktion. Wir schreiben f (n) = Ω(g(n)), wenn positive Konstanten n0 und c existieren, sodass ab der Stelle n0 die Werte von f (n) immer oberhalb von c g(n) liegen.
schreiben, um den gleichen Sachverhalt zu beschreiben, oder sogar „f (n) = Θ(g(n))“, um deutlich zu machen, dass die Funktionen über der Variable n definiert sind. Sie sind vielleicht verwirrt, dass wir das Gleichheitszeichen in dieser Art und Weise missbrauchen, aber wir werden in diesem Abschnitt noch sehen, dass diese Notation Vorteile hat. Abbildung 3.1(a) gibt ein intuitives Bild der Funktionen f (n) und g(n) für den Fall f (n) = Θ(g(n)). Für n gleich n0 und alle rechts von n0 liegenden Werte n liegen die Werte von f (n) über c1 g(n) (genauer: f (n) ≥ c1 g(n)) und unter c2 g(n) (genauer: f (n) ≤ c2 g(n)). Mit anderen Worten: die Funktion f (n) ist bis auf einen konstanten Faktor für alle n ≥ n0 gleich g(n). Wir sagen, dass g(n) eine asymptotisch scharfe Schranke von f (n) ist. Die Definition von Θ(g) fordert, dass jedes Element f ∈ Θ(g) asymptotisch nichtnegativ ist, d. h. dass f (n) für hinreichend große n nichtnegativ ist. (Eine asymptotisch positive Funktion ist eine Funktion, die für hinreichend große n positiv ist.) Folglich muss die Funktion g selbst asymptotisch nichtnegativ sein, anderenfalls ist die Menge Θ(g) leer. Wir werden aus diesem Grund annehmen, dass jede Funktion innerhalb der Θ-Notation asymptotisch nichtnegativ ist. Diese Annahme trifft ebenso auf die anderen in diesem Kapitel eingeführten Notationen zu. Im Kapitel 2 haben wir eine formlose Version der Θ-Notation eingeführt, die darin bestand, die Terme niedrigerer Ordnung zu vernachlässigen und den führenden Koeffizienten des Terms mit der höchsten Ordnung zu ignorieren. Lassen Sie uns kurz diese
48
3 Wachstum von Funktionen
heuristische Herangehensweise rechtfertigen, indem wir die formale Definition benutzen, um zu zeigen, dass 12 n2 − 3n = Θ(n2 ) erfüllt ist. Dafür müssen wir positive Konstanten c1 , c2 und n0 bestimmen, sodass c1 n 2 ≤
1 2 n − 3n ≤ c2 n2 2
für alle n ≥ n0 gilt. Division durch n2 ergibt 3 1 − ≤ c2 . 2 n Wir können die Ungleichung auf der rechten Seite für jeden Wert n ≥ 1 erfüllen, indem wir für c2 eine beliebige Konstante größer gleich 1/2 wählen. Entsprechend können wir die Ungleichung auf der linken Seite für jedes n ≥ 7 erfüllen, indem wir für c1 eine beliebige Konstante kleiner gleich 1/14 wählen. Indem wir c1 = 1/14, c2 = 1/2 und n0 = 7 wählen, können wir überprüfen, dass 12 n2 − 3n = Θ(n2 ) gilt. Natürlich gibt es für die Konstanten auch andere Wahlmöglichkeiten, aber der wesentliche Punkt ist, dass irgendeine Wahlmöglichkeit existiert. Beachten Sie, dass diese Konstanten von der Funktion 12 n2 − 3n abhängen; eine andere zu Θ(n2 ) gehörige Funktion würde normalerweise andere Konstanten erfordern. c1 ≤
Wir können auch die formale Definition benutzen, um nachzuprüfen, dass 6n3 = Θ(n2 ) gilt. Zum indirekten Beweis nehmen wir an, dass c2 und n0 existieren, sodass 6n3 ≤ c2 n2 für alle n ≥ n0 erfüllt ist. Mit Division durch n2 folgt dann aber n ≤ c2 /6, was für hinreichend große n doch nicht gilt, da c2 konstant ist. Intuitiv können die Terme niedrigerer Ordnung einer asymptotisch positiven Funktion ignoriert werden, wenn asymptotisch scharfe Schranken bestimmt werden. Terme niedrigerer Ordnung sind für große n nicht signifikant. Für große n reicht ein winziger Bruchteil des Terms mit der höchsten Ordnung aus, um die Terme niedrigerer Ordnung zu dominieren. Wählen wir für c1 einen Wert, der etwas kleiner ist als der Koeffizient des Terms höchster Ordnung und für c2 einen etwas höheren Wert, dann sind damit die Ungleichungen in der Definition der Θ-Notation erfüllt. Der Koeffizient des Terms höchster Ordnung kann auch ignoriert werden, da er c1 und c2 seinem Wert entsprechend nur um einen konstanten Faktor ändert. Als Beispiel betrachten wir eine quadratische Funktion f (n) = a n2 + b n + c, wobei a, b und c Konstanten sind und a > 0 gilt. Vernachlässigen der Terme niedrigerer Ordnung und Ignorieren der Konstanten liefert f (n) = Θ(n2 ). Um dies formal zu zeigen, nehmen wir die Konstanten c1 = a/4, c2 = 7a/4 und n0 = 2 · max((|b| /a), (|c| /a)). Sie können überprüfen, dass für alle n ≥ n0 die Ungleichung 0 ≤ c1 n2 ≤ a n2 + b n + c ≤ c2 n2 d erfüllt ist. Allgemeinen gilt p(n) = Θ(nd ) für jedes Polynom p(n) = i=0 ai ni , wenn ai Konstanten sind und ad > 0 gilt (siehe Problemstellung 3-1). Da jede Konstante ein Polynom vom Grad 0 ist, können wir jede Konstante als Θ(n0 ) oder Θ(1) angeben. Letztere Bezeichnung stellt jedoch einen geringfügigen Missbrauch dar, da der Ausdruck nicht angibt, über welche Variable die Funktion definiert ist.3 Wir werden häufig die Bezeichnung Θ(1) benutzen, womit wir entweder eine Konstante oder eine Funktion meinen, die bezüglich einer Variablen konstant ist. 3 Das
eigentliche Problem ist, dass unsere einfache Notation Funktionen nicht von Werten unter-
3.1 Asymptotische Notation
49
O-Notation Die Θ-Notation beschränkt eine Funktion asymptotisch von oben und unten. Wenn wir nur über eine obere asymptotische Schranke verfügen, benutzen wir die O-Notation. Bei einer gegebenen Funktion g(n) bezeichnen wir mit O(g(n)) (ausgesprochen als „groß O von g von n“ oder manchmal einfach als „O von g von n“) die Menge der Funktionen O(g(n)) = {f (n) : es existieren positive Konstanten c und n0 , sodass 0 ≤ f (n) ≤ c g(n) für alle n ≥ n0 } .4 Wir benutzen die O-Notation, um bis auf einen konstanten Faktor eine obere Schranke einer Funktion anzugeben. Abbildung 3.1(b) zeigt die Idee hinter der O-Notation. Für n gleich n0 und alle rechts von n0 liegenden Werte n ist der Wert von f (n) kleiner gleich c g(n). Wir schreiben f (n) = O(g(n)), um anzugeben, dass f (n) ein Element der Menge O(g(n)) ist. Beachten Sie, dass aus f (n) = Θ(g(n)) auch f (n) = O(g(n)) folgt, da die ΘNotation stärker als die O-Notation ist. In der Sprache der Mengenlehre haben wir Θ(g(n)) ⊆ O(g(n)). Somit zeigt unser Beweis zu a n2 + b n + c = Θ(n2 ), mit a > 0, dass diese quadratische Funktion auch in O(n2 ) liegt. Was überraschender sein mag, ist, dass jede lineare Funktion a n + b mit a > 0 in O(n2 ) liegt. Dies können wir aber leicht nachprüfen, indem man c = a + |b| und n0 = max {1, − ab } setzen. Wenn Sie bereits mit der O-Notation vertraut sind, finden Sie es möglicherweise ungewöhnlich, dass wir beispielsweise n = O(n2 ) schreiben. In der Literatur finden wir zuweilen die O-Notation, um asymptotisch scharfe Schranken anzugeben; dies entspricht unserer Definition der Θ-Notation. In diesem Buch behaupten wir mit f (n) = O(g(n)) jedoch lediglich, dass ein konstantes Vielfaches von g(n) eine asymptotisch obere Schranke von f (n) ist, ohne zu zeigen, wie scharf diese obere Schranke ist. Das Unterscheiden asymptotisch oberer Schranken von asymptotisch scharfen Schranken ist Standard in der Literatur zu Algorithmen. Mit der O-Notation können wir die Laufzeit eines Algorithmus oft angeben, indem wir lediglich die Gesamtstruktur des Algorithmus untersuchen. Zum Beispiel liefert die doppelt verschachtelte Schleifenstruktur des Algorithmus Sortieren durch Einfügen aus Kapitel 2 sofort eine obere Schranke von O(n2 ) für die Laufzeit im schlechtesten Fall: Die Kosten jeder Iteration der inneren Schleife sind von oben durch O(1) (konstant) beschränkt, die Indizes i und j haben höchstens den Wert n, und die innere Schleife wird höchstens einmal für jedes der n2 Paare der Werte von i und j ausgeführt. Weil die O-Notation eine obere Schranke darstellt, wenn wir sie anwenden, um die Laufzeit im schlechtesten Fall zu beschränken, haben wir eine Schranke für die Laufzeit scheidet. Innerhalb des λ-Kalküls werden Parameter einer Funktion klar spezifiziert: Die Funktion n2 könnte als λn.n2 oder sogar als λr.r 2 geschrieben werden. Die Annahme einer strengeren Notation würde jedoch die algebraischen Manipulationen komplizieren, weshalb wir den eigentlich unzulässigen Gebrauch tolerieren. 4 Bemerkung des Übersetzers: Nachdem wir im Abschnitt zu der Θ-Notation eine mathematisch saubere Definition angegeben haben, wollen wir es im Folgenden bei der einfacheren Formulierung aus dem englischsprachigen Buch belassen, in der nicht zwischen einer Funktion und einem Funktionswert unterschieden wird. Aus dem Kontext sollte jedoch jeweils klar sein, was an welcher Stelle gemeint ist.
50
3 Wachstum von Funktionen
des Algorithmus für jede Eingabe – also eine umfassende Aussage, wie vorhin diskutiert. Folglich gilt die O(n2 )-Schranke für die Laufzeit von Sortieren durch Einfügen im schlechtesten Fall auch für dessen Laufzeit bei beliebigen Eingaben. Die Θ(n2 )-Schranke für die Laufzeit für Sortieren durch Einfügen im schlechtesten Fall impliziert jedoch keine Θ(n2 )-Schranke für die Laufzeit von Sortieren durch Einfügen für jede Eingabe. Wir haben in Kapitel 2 beispielsweise gesehen, dass die Laufzeit von Sortieren durch Einfügen in Θ(n) ist, wenn die Eingabe bereits sortiert ist. Technisch gesehen ist es an sich missbräuchlich zu sagen, dass die Laufzeit von Sortieren durch Einfügen in O(n2 ) ist, da für ein gegebenes n die tatsächliche Laufzeit abhängig von der speziellen Eingabe der Größe n variiert. Wenn wir sagen „die Laufzeit ist in O(n2 )“, meinen wir, dass es eine Funktion f (n) in O(n2 ) gibt, sodass für jeden Wert von n, ungeachtet der speziell gewählten Eingabe der Größe n, die Laufzeit für diese Eingabe von oben durch den Wert f (n) beschränkt ist. Entsprechend meinen wir, dass die Laufzeit im schlechtesten Fall in O(n2 ) liegt.
Ω-Notation Ebenso wie die O-Notation eine asymptotische obere Schranke einer Funktion liefert, liefert die Ω-Notation eine asymptotische untere Schranke. Wenn eine Funktion g(n) gegeben ist, dann bezeichnen wir mit Ω(g(n)) (ausgesprochen als „groß Omega von g von n“ oder manchmal einfach „Omega von g von n“) die Menge der Funktionen {f (n) : es existieren positive Konstanten c und n0 , sodass 0 ≤ c g(n) ≤ f (n) für alle n ≥ n0 } . Die Idee hinter der Ω-Notation wird in Abbildung 3.1(c) veranschaulicht. Für n gleich n0 und alle rechts von n0 liegenden Werte n ist der Wert von f (n) größer gleich c g(n). Ausgehend von den Definitionen für die asymptotischen Notationen, die wir bisher kennen gelernt haben, ist es einfach, das folgende wichtige Theorem zu beweisen (siehe Übung 3.1-5). Theorem 3.1 Für zwei beliebige Funktionen f (n) und g(n) gilt f (n) = Θ(g(n)) genau dann, wenn die Gleichungen f (n) = O(g(n)) und f (n) = Ω(g(n)) erfüllt sind. Als Anwendungsbeispiel dieses Theorems impliziert unser Beweis zu a n2 + b n + c = Θ(n2 ) für alle Konstanten a, b und c mit a > 0, dass a n2 + b n + c = Ω(n2 ) und a n2 + b n + c = O(n2 ) gilt. Wir haben in diesem Beispiel das Theorem benutzt, um aus asymptotisch scharfen Schranken asymptotisch untere und obere Schranken zu erhalten. In der Praxis benutzen wir Theorem 3.1 gewöhnlich eher dazu, asymptotisch scharfe Schranken ausgehend von asymptotisch unteren und oberen Schranken zu beweisen. Wenn wir sagen, dass die Laufzeit (ohne den Laufzeitbegriff in welcher Form auch immer zu beschränken) eines Algorithmus in Ω(g(n)) liegt, meinen wir, dass für jeden ausreichend großen Wert von n die Laufzeit des Algorithmus angewendet auf egal welche
3.1 Asymptotische Notation
51
spezielle Eingabe der Größe n bis auf einen konstanten Faktor größer gleich g(n) ist. Dies ist gleichbedeutend dazu, dass wir eine untere Schranke für die Laufzeit eines Algorithmus im günstigsten Fall angeben. Beispielsweise ist die Laufzeit von Sortieren durch Einfügen im günstigsten Fall in Ω(n), woraus sich ergibt, dass die Laufzeit von Sortieren durch Einfügen in Ω(n) liegt. Die Laufzeit von Sortieren durch Einfügen ist demnach sowohl in Ω(n) als auch in O(n2 ), da sie an allen Stellen zwischen einer in n linearen Funktion und einer in n quadratischen Funktion liegt. Darüber hinaus sind diese Schranken so scharf wie möglich: Beispielsweise ist die Laufzeit von Sortieren durch Einfügen nicht in Ω(n2 ), da eine Eingabe existiert, für die Sortieren durch Einfügen in Θ(n) läuft (zum Beispiel wenn die Eingabe bereits sortiert ist). Es ist jedoch kein Widerspruch zu sagen, dass die Laufzeit von Sortieren durch Einfügen im schlechtesten Fall Zeit Ω(n2 ) benötigt, denn es gibt eine Eingabe, die bewirkt, dass der Algorithmus eine Zeit der Ordnung Ω(n2 ) benötigt.
Asymptotische Notation in Gleichungen und Ungleichungen Wir haben bereits gesehen, wie die asymptotische Notation in mathematischen Formeln benutzt wird. Zum Beispiel haben wir bei der Einführung der O-Notation „n = O(n2 )“ geschrieben. Wir könnten auch schreiben 2n2 + 3n + 1 = 2n2 + Θ(n). Wie interpretieren wir solche Formeln? Wenn die asymptotische Notation auf der rechten Seite einer Gleichung (oder Ungleichung) allein (d. h. nicht als Teil eines größeren Ausdrucks) steht, wie zum Beispiel in n = O(n2 ), dann haben wir bereits definiert, dass das Gleichheitszeichen die Zugehörigkeit zu einer Menge bezeichnet, also in unserem Falle n ∈ O(n2 ). Tritt die asymptotische Notation jedoch in einer Formel auf, dann interpretieren wir sie als Platzhalter für eine anonyme Funktion, die wir nicht näher benennen wollen. Die Formel 2n2 + 3n + 1 = 2n2 + Θ(n) zum Beispiel bedeutet, dass 2n2 + 3n + 1 = 2n2 + f (n) für eine bestimmte Funktion f (n) aus der Menge Θ(n) gilt. In diesem Beispiel gilt f (n) = 3n + 1, die tatsächlich von der Ordnung Θ(n) ist. Die Verwendung der asymptotischen Notation kann auf diese Weise dazu beitragen, unwesentliche Details und Wirrwarr zu eliminieren. Zum Beispiel haben wir in Kapitel 2 die Laufzeit von Sortieren durch Mischen im schlechtesten Fall durch die Rekursionsgleichung T (n) = 2 T (n/2) + Θ(n) ausgedrückt. Wenn wir nur am asymptotischen Verhalten von T (n) interessiert sind, dann gibt es keinen Grund dafür, alle Terme niedrigerer Ordnung genau zu spezifizieren; wir betrachten sie alle als in der anonymen Funktion Θ(n) enthalten. Die Anzahl der anonymen Funktionen in einem Ausdruck ist dadurch gegeben, wie oft die asymptotische Notation in dem Ausdruck auftritt. In dem Ausdruck n i=1
O(i)
52
3 Wachstum von Funktionen
zum Beispiel gibt es nur eine anonyme Funktion (eine Funktion von i). Dieser Ausdruck ist deshalb nicht gleich O(1) + O(2) + · · · + O(n), wofür es tatsächlich keine klare Interpretation gibt. In einigen Fällen tritt die asymptotische Notation auf der linken Seite einer Gleichung auf, wie im Ausdruck 2n2 + Θ(n) = Θ(n2 ) . Wir interpretieren solche Gleichungen unter Verwendung der folgenden Regel: Unabhängig davon, wie die anonymen Funktionen auf der linken Seite des Gleichheitszeichens gewählt werden, ist es möglich, die anonymen Funktionen rechts vom Gleichheitszeichen so zu wählen, dass Gleichheit gilt. Somit bedeutet unser Beispiel, dass es für jede Funktion f (n) ∈ Θ(n) eine Funktion g(n) ∈ Θ(n2 ) gibt, sodass 2n2 + f (n) = g(n) für alle n gilt. Anders formuliert: die rechte Seite einer Gleichung bietet eine gröbere Detailsicht als die linke. Wir können mehrere dieser Beziehungen hintereinander schreiben: 2n2 + 3n + 1 = 2n2 + Θ(n) = Θ(n2 ) . Wir können jede Gleichung unter den oben genannten Regeln für sich interpretieren. Die erste Gleichung besagt, dass es eine Funktion f (n) ∈ Θ(n) gibt, sodass 2n2 + 3n + 1 = 2n2 + f (n) für alle n erfüllt ist. Die zweite Gleichung sagt aus, dass es für jede Funktion g(n) ∈ Θ(n) (wie die bereits benutzte Funktion f (n)) eine Funktion h(n) ∈ Θ(n2 ) gibt, sodass 2n2 + g(n) = h(n) für alle n gilt. Beachten Sie, dass aus der Hintereinanderausführung der Gleichungen die Gleichung 2n2 + 3n + 1 = Θ(n2 ) folgt.
o-Notation Die durch die O-Notation festgelegte asymptotisch obere Schranke kann asymptotisch scharf sein, muss aber nicht. Die Schranke 2n2 = O(n2 ) ist asymptotisch scharf, während es die Schranke 2n = O(n2 ) nicht ist. Wir verwenden die o-Notation zur Bezeichnung einer oberen Schranke, die nicht asymptotisch scharf ist. Wir definieren o(g(n)) („kleinoh von g von n“) formal als die Menge o(g(n)) = {f (n) : für jede positive Konstante c > 0 existiert ein konstantes n0 > 0, sodass 0 ≤ f (n) < c g(n) für alle n ≥ n0 } . Zum Beispiel gilt 2n = o(n2 ) und 2n2 = o(n2 ). Die Definitionen der O-Notation und der o-Notation sind einander ähnlich. Der wesentliche Unterschied besteht darin, dass in f (n) = O(g(n)) die Schranke 0 ≤ f (n) ≤ c g(n) für eine Konstante c > 0 gilt, während in f (n) = o(g(n)) die Schranke 0 ≤ f (n) < c g(n) für alle Konstanten c > 0 gilt. Intuitiv ist klar, dass in der o-Notation die Funktion f (n) unbedeutend gegenüber g(n) wird, wenn n gegen Unendlich geht; das heißt lim
n→∞
f (n) =0. g(n)
(3.1)
3.1 Asymptotische Notation
53
Einige Autoren verwenden diesen Limes zur Definition der o-Notation; die Definition in diesem Buch fordert dagegen auch, dass die anonymen Funktionen asymptotisch nichtnegativ sind.
ω-Notation Was die o-Notation im Vergleich zur O-Notation ist, ist die ω-Notation für die ΩNotation. Wir benutzen die ω-Notation, um eine untere Schranke anzugeben, die nicht asymptotisch scharf ist. Ein Weg, diese Notation einzuführen, ist f (n) ∈ ω(g(n)) genau dann, wenn g(n) ∈ o(f (n)) . Formal haben wir ω(g(n)) („klein-omega von g von n“) jedoch als die Menge ω(g(n)) = {f (n) : für jede positive Konstante c > 0 existiert eine Konstante n0 > 0, sodass 0 ≤ c g(n) < f (n) für alle n ≥ n0 } zu definieren. Zum Beispiel gilt bedeutet, dass lim
n→∞
n2 2
= ω(n), aber
n2 2
= ω(n2 ). Die Relation f (n) = ω(g(n))
f (n) =∞ g(n)
gilt, wenn der Limes existiert. Das heißt, dass f (n) gegenüber g(n) beliebig groß wird, wenn n gegen Unendlich geht.
Vergleich von Funktionen Viele der Relationseigenschaften reeller Zahlen können auch auf asymptotische Vergleiche angewendet werden. Im Folgenden nehmen wir an, dass f (n) und g(n) asymptotisch positiv sind. Transitivität: f (n) = Θ(g(n)) f (n) = O(g(n)) f (n) = Ω(g(n)) f (n) = o(g(n)) f (n) = ω(g(n))
und und und und und
Reflexivität: f (n) = Θ(f (n)) , f (n) = O(f (n)) , f (n) = Ω(f (n)) .
g(n) = Θ(h(n)) g(n) = O(h(n)) g(n) = Ω(h(n)) g(n) = o(h(n)) g(n) = ω(h(n))
impliziert impliziert impliziert impliziert impliziert
f (n) = Θ(h(n)) , f (n) = O(h(n)) , f (n) = Ω(h(n)) , f (n) = o(h(n)) , f (n) = ω(h(n)) .
54
3 Wachstum von Funktionen
Symmetrie: f (n) = Θ(g(n)) genau dann, wenn g(n) = Θ(f (n)) . Austausch-Symmetrie: f (n) = O(g(n)) genau dann, wenn g(n) = Ω(f (n)) , f (n) = o(g(n)) genau dann, wenn g(n) = ω(f (n)) . Da die asymptotischen Notationen diese Eigenschaften erfüllen, kann man eine Analogie zwischen dem asymptotischen Vergleich zweier Funktionen f und g und dem Vergleich von zwei reellen Zahlen a und b ziehen: f (n) = O(g(n)) f (n) = Ω(g(n)) f (n) = Θ(g(n)) f (n) = o(g(n)) f (n) = ω(g(n))
entspricht entspricht entspricht entspricht entspricht
a≤b a≥b a=b ab
, , , , .
Wir sagen, dass f (n) asymptotisch kleiner als g(n) ist, wenn f (n) = o(g(n)) erfüllt ist und dass f (n) asymptotisch größer als g(n) ist, wenn f (n) = ω(g(n)) gilt. Eine Eigenschaft reeller Zahlen kann jedoch nicht auf die asymptotische Notation übertragen werden: Trichotomie: Für zwei beliebige reelle Zahlen a und b muss exakt eine der folgenden Relationen erfüllt sein: a < b, a = b oder a > b. Obwohl jedes beliebige Paar reeller Zahlen verglichen werden kann, sind nicht alle Funktionen asymptotisch vergleichbar. Das heißt, für zwei Funktionen f (n) und g(n) kann der Fall eintreten, dass weder f (n) = O(g(n)) noch f (n) = Ω(g(n)) gilt. Beispielsweise können wir die Funktionen n und n1+sin n nicht mithilfe der asymptotischen Notation vergleichen, da der Wert des Exponenten in n1+sin n zwischen 0 und 2 oszilliert und alle Zwischenwerte angenommen werden.
Übungen 3.1-1 Seien f (n) und g(n) asymptotisch nichtnegative Funktionen. Beweisen Sie max(f (n), g(n)) = Θ(f (n) + g(n)) unter Verwendung der Definition der Θ-Notation. 3.1-2 Zeigen Sie, dass für beliebige reelle Konstanten a und b mit b > 0 (n + a)b = Θ(nb ) gilt.
(3.2)
3.2 Standardnotationen und Standardfunktionen
55
3.1-3 Erklären Sie, warum die Aussage „Die Laufzeit eines Algorithmus A beträgt mindestens O(n2 )“ keinen Sinn macht. 3.1-4 Gilt 2n+1 = O(2n )? Gilt 22n = O(2n )? 3.1-5 Beweisen Sie Theorem 3.1. 3.1-6 Beweisen Sie, dass die Laufzeit eines Algorithmus genau dann Θ(g(n)) beträgt, wenn seine Laufzeit im schlechtesten Fall in O(g(n)) und seine Laufzeit im günstigsten Fall in Ω(g(n)) liegt. 3.1-7 Beweisen Sie, dass o(g(n)) ∩ ω(g(n)) die leere Menge ist. 3.1-8 Wir können unsere Notation auf den Fall von zwei Parametern n und m, die unabhängig voneinander mit verschiedenen Geschwindigkeiten gegen Unendlich gehen, erweitern. Für eine Funktion g(n, m) bezeichnen wir mit O(g(n, m)) die Menge der Funktionen O(g(n, m)) = {f (n, m) : es existieren positive Konstanten c, n0 und m0 , sodass 0 ≤ f (n, m) ≤ c g(n, m) für alle n ≥ n0 oder m ≥ m0 } . Geben Sie entsprechende Definitionen für Ω(g(n, m)) und Θ(g(n, m)) an.
3.2
Standardnotationen und Standardfunktionen
Dieser Abschnitt gibt einen Überblick über einige mathematische Standardfunktionen und Bezeichnungen und untersucht die Beziehungen zwischen ihnen. Er illustriert auch die Verwendung der asymptotischen Notation.
Monotonie Eine Funktion f (n) ist monoton steigend, wenn m ≤ n impliziert, dass f (m) ≤ f (n) gilt. Entsprechend ist sie monoton fallend, wenn aus m ≤ n folgt, dass f (m) ≥ f (n) ist. Eine Funktion f (n) wird als streng monoton steigend bezeichnet, wenn aus m < n folgt, dass f (m) < f (n) ist. Als streng monoton fallend wird sie bezeichnet, wenn m < n die Beziehung f (m) > f (n) impliziert.
Aufrunden und Abrunden Gegeben sei eine beliebige reelle Zahl x. Wir bezeichnen die größte ganze Zahl, die kleiner oder gleich x ist, mit x (gesprochen „floor von x“) und die kleinste ganze Zahl, die größer oder gleich x ist, mit x (gesprochen „ceil von x“). Für alle reellen Zahlen x gilt x − 1 < x ≤ x ≤ x < x + 1 .
(3.3)
56
3 Wachstum von Funktionen
Für beliebige ganze Zahlen n gilt
n n + =n. 2 2 Für beliebige reelle Zahlen n ≥ 0 und ganze Zahlen a, b > 0 gelten die Beziehungen
x/a b
x/a b
a b a b
x , ab x , = ab a + (b − 1) ≤ , b a − (b − 1) ≥ . b =
(3.4) (3.5) (3.6) (3.7)
Die floor -Funktion f (x) = x ist monoton steigend, ebenso wie die ceil -Funktion f (x) = x .
Modulare Arithmetik Für eine beliebige ganze Zahl a und eine beliebige positive ganze Zahl n ist der Wert a mod n der Rest des Quotienten a/n: a . (3.8) a mod n = a − n n Es folgt 0 ≤ a mod n < n .
(3.9)
Ist eine wohldefinierte Definition des Restes einer Division einer ganzen Zahl durch eine andere gegeben, so ist es zweckmäßig, einige spezielle Bezeichnungen einzuführen, um die Gleichheit von Resten auszudrücken. Wenn (a mod n) = (b mod n) gilt, dann schreiben wir a ≡ b (mod n) und sagen, dass a äquivalent zu b modulo n ist. Mit anderen Worten gilt a ≡ b (mod n), wenn a und b bei der Division durch n den gleichen Rest haben. Entsprechend gilt a ≡ b (mod n) genau dann, wenn n ein Teiler von b − a ist. Wir schreiben a ≡ b (mod n), wenn a nicht äquivalent zu b modulo n ist.
Polynome Gegeben sei eine nichtnegative ganze Zahl d. Ein Polynom in n vom Grad d ist eine Funktion p(n) der Form p(n) =
d i=0
ai ni ,
3.2 Standardnotationen und Standardfunktionen
57
wobei die Konstanten a0 , a1 , . . . , ad als Koeffizienten des Polynoms bezeichnet werden und ad = 0 gilt. Ein Polynom ist demnach asymptotisch positiv, wenn ad > 0 gilt. Für ein asymptotisch positives Polynom p(n) vom Grad d gilt p(n) = Θ(nd ). Die Funktion na ist für beliebige reelle Konstanten a ≥ 0 monoton steigend und für beliebige reelle Konstanten a ≤ 0 monoton fallend. Wir sagen, dass eine Funktion f (n) polynomiell beschränkt ist, wenn für eine Konstante k die Gleichung f (n) = O(nk ) gilt.
Exponentialfunktionen Für alle reellen Zahlen a > 0, m und n gelten folgende Identitäten: a0 = 1 , a1 = a , a−1 = 1/a , m n (a ) = amn , (am )n = (an )m , am an = am+n . Für alle n und a ≥ 1 ist die Funktion an monoton steigend in n. Wenn es zweckmäßig ist, nehmen wir 00 = 1 an. Wir können die Wachstumsraten von Polynomen und Exponentialfunktionen wie folgt zueinander in Beziehung setzen. Für alle reellen Konstanten a und b, mit a > 1 gilt nb =0, n→∞ an
(3.10)
lim
woraus wir schließen können, dass nb = o(an ) gilt. Somit wächst jede Exponentialfunktion, deren Basis echt größer als 1 ist, schneller als jedes Polynom. Unter Verwendung von e = 2,71828 . . ., der Basis des natürlichen Logarithmus, erhalten wir für alle reellen Zahlen x ∞
ex = 1 + x +
xi x3 x2 + + ··· = , 2! 3! i! i=0
(3.11)
wobei „ ! “ den später noch in diesem Abschnitt definierten Fakultätsoperator bezeichnet. Für alle reellen Zahlen x gilt die Ungleichung ex ≥ 1 + x ,
(3.12)
wobei Gleichheit nur für x = 0 gilt. Für |x| ≤ 1 gilt die Approximation 1 + x ≤ ex ≤ 1 + x + x2 .
(3.13)
58
3 Wachstum von Funktionen
Für x → 0 ist die Näherung 1 + x für ex ziemlich gut: ex = 1 + x + Θ(x2 ) . (In dieser Gleichung wird die asymptotische Notation dazu benutzt, das Verhalten im Limes x → 0 zu beschreiben als im Limes x → ∞.) Für alle x gilt x n lim 1 + = ex . (3.14) n→∞ n
Logarithmen Wir werden die folgenden Bezeichnungen verwenden: lg n = log2 n ln n = loge n lgk n = (lg n)k lg lg n = lg(lg n)
(binärer Logarithmus) , (natürlicher Logarithmus) , (Potenzierung) , (Hintereinanderausführung) .
Wir übernehmen die wichtige Konvention hinsichtlich der Bezeichnungen, dass sich die logarithmischen Funktionen nur auf den unmittelbar nachfolgenden Term beziehen; lg n + k bedeutet also (lg n) + k und nicht lg(n + k). Wenn wir b > 1 konstant halten, dann ist die Funktion logb n für n > 0 streng monoton steigend. Für alle reellen Zahlen a > 0, b > 0, c > 0 und n gilt a = blogb a , logc (ab) = logc a + logc b , logb an = n logb a , logc a logb a = , logc b 1 logb = − logb a , a 1 , logb a = loga b alogb c = clogb a ,
(3.15)
(3.16)
wobei die Basen der Logarithmen in jeder der obigen Gleichungen ungleich 1 sind. Aus Gleichung (3.15) geht hervor, dass die Änderung der Basis des Logarithmus von einer Konstanten zu einer anderen den Wert des Logarithmus nur um einen konstanten Faktor verändert. Deshalb werden wir häufig die Bezeichnung „lg n“ verwenden, wenn uns konstante Faktoren, wie zum Beispiel in der O-Notation, nicht interessieren. Informatiker empfinden die Zahl 2 sowieso als natürlichste Basis eines Logarithmus, da so viele Algorithmen und Datenstrukturen das Aufteilen eines Problems in zwei Teile beinhalten.
3.2 Standardnotationen und Standardfunktionen
59
Wenn |x| < 1 gilt, gibt es für den Ausdruck ln(1 + x) eine einfache Reihenentwicklung: ln(1 + x) = x −
x3 x4 x5 x2 + − + − ··· . 2 3 4 5
Ist x > −1, so gilt x ≤ ln(1 + x) ≤ x , 1+x
(3.17)
wobei Gleichheit nur für x = 0 gilt. Wir sagen, dass eine Funktion f (n) polylogarithmisch beschränkt ist, wenn für eine Konstante k die Beziehung f (n) = O(lgk n) erfüllt ist. Wir können das Wachstum der Polynome und Polylogarithmen vergleichen, indem wir in Gleichung (3.10) n durch lg n und a durch 2a ersetzen, was zu lgb n lgb n = lim =0 n→∞ (2a )lg n n→∞ na lim
führt. Aus diesem Limes können wir schlussfolgern, dass für jede Konstante a > 0 die Beziehung lgb n = o(na ) gilt. Somit wächst jedes Polynom schneller als jede polylogarithmische Funktion.
Fakultät Die Bezeichnung n! (gesprochen „n Fakultät“) ist für ganze Zahlen n ≥ 0 als 1 falls n = 0 , n! = n · (n − 1)! falls n > 0 definiert. Somit gilt n! = 1 · 2 · 3 · . . . · n. Eine grobe obere Schranke der Fakultät ist n! ≤ nn , da jeder der n Terme der Fakultät kleiner oder gleich n ist. Die Stirlingsche Näherung n n √ 1 n! = 2πn 1+Θ , (3.18) e n wobei e die Basis des natürlichen Logarithmus ist, liefert uns eine schärfere obere Schranke und zusätzlich eine untere Schranke. Wie Übung 3.2-3 von Ihnen verlangt, zu beweisen, gilt n! = o(nn ) , n! = ω(2n ) , lg(n!) = Θ(n lg n) ,
(3.19)
60
3 Wachstum von Funktionen
wobei die Stirlingsche Näherung nützlich zum Beweisen der Gleichung (3.19) ist. Die Gleichung n n √ n! = 2πn eαn , (3.20) e ist auch für alle n ≥ 1 erfüllt, wobei 1 1 < αn < 12n + 1 12n
(3.21)
gilt.
Funktionale Iteration Wir benutzen die Notation f (i) (n), um die Funktion f (n), wenn i-mal iterativ auf einen Anfangswert n angewendet, zu beschreiben. Um den Begriff der funktionalen Iteration formal definieren zu können, nehmen wir an, dass f (n) eine Funktion über den reellen Zahlen ist. Für nichtnegative ganze Zahlen i können wir dann die i-mal iterierte Funktion f (i) (n) wie folgt rekursiv definieren: n falls i = 0 , f (i) (n) = f (f (i−1) (n)) falls i > 0 . Wenn beispielsweise f (n) = 2n gilt, dann ist f (i) (n) = 2i n.
Iterierte logarithmische Funktion Wir benutzen die Notation lg∗ n (gesprochen „log Stern von n“) als Bezeichnung für den iterierten Logarithmus, der wie folgt definiert ist. Sei lg(i) n wie oben definiert, mit f (n) = lg n. Da der Logarithmus einer nichtpositiven Zahl nicht definiert ist, ist lg(i) n nur definiert, wenn lg(i−1) n > 0 gilt. Es sei daran erinnert, dass mit lg(i) n der i-mal in Folge auf den Startwert n angewendete Logarithmus gemeint ist und nicht die Funktion logi n, die die i-te Potenz des Logarithmus von n darstellt. Dann definieren wir die iterierte logarithmische Funktion durch lg∗ n = min i ≥ 0 : lg(i) n ≤ 1 . Der iterierte Logarithmus ist eine sehr langsam steigende Funktion: lg∗ 2 = 1 lg∗ 4 = 2 lg∗ 16 = 3 lg∗ 65536 = 4
, , , ,
lg∗ (265536 ) = 5 . Da die Anzahl der Atome im beobachtbaren Universum auf ungefähr 1080 geschätzt wird, was viel weniger als 265536 ist, werden wir selten auf eine Eingabegröße n stoßen, für die lg∗ n > 5 gilt.
3.2 Standardnotationen und Standardfunktionen
61
Fibonacci-Zahlen Wir definieren die Fibonacci-Zahlen durch die folgenden Rekursionsgleichungen: F0 = 0 , F1 = 1 , Fi = Fi−1 + Fi−2
(3.22) für i ≥ 2 .
Somit ist jede Fibonacci-Zahl die Summe der beiden vorhergehenden, was zu der Folge 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, . . . führt. Fibonacci-Zahlen haben einen Bezug zum goldenen Schnitt φ und dessen kon die für die beiden Lösungen der Gleichung jugiert komplexen Wert φ, x2 = x + 1 stehen; sie sind durch die Formeln √ 1+ 5 φ= 2 = 1,61803 . . . , √ 1− 5 φ = 2 = −0,61803 . . . .
(3.23)
(3.24)
gegeben. Es gilt speziell Fi =
φi − φi √ , 5
< 1 ist, gilt was wir mit Induktion beweisen können (Übung 3.2-7). Da |φ| i φ 1 √ < √ 5 5 1 < , 2 aus dem i 1 φ Fi = √ + , 5 2
(3.25)
folgt. Dies sagt aus, dass√die i-te Fibonacci-Zahl Fi gleich dem auf eine ganze Zahl gerundeten Wert von φi / 5 ist. Die Fibonacci-Zahlen wachsen also exponentiell.
62
3 Wachstum von Funktionen
Übungen 3.2-1 Zeigen Sie, dass für monoton steigende Funktionen f (n) und g(n) auch die Funktionen f (n)+ g(n) und f (g(n)) monoton steigend sind, und dass f (n)·g(n) monoton steigend ist, wenn f (n) und g(n) zudem nichtnegativ sind. 3.2-2 Beweisen Sie die Gleichung (3.16). 3.2-3 Beweisen Sie die Gleichung (3.19). Beweisen Sie auch die Beziehungen n! = ω(2n ) und n! = o(nn ). 3.2-4∗ Ist die Funktion lg n ! polynomial beschränkt? Ist die Funktion lg lg n ! polynomial beschränkt? 3.2-5∗ Was ist asymptotisch größer: lg(lg∗ n) oder lg∗ (lg n)? 3.2-6 Zeigen Sie, dass der goldene Schnitt φ und dessen konjugiert komplexer Wert φ beide die Gleichung x2 = x + 1 erfüllen. 3.2-7 Beweisen Sie durch Induktion, dass die i-te Fibonacci-Zahl die Gleichung Fi =
φi − φi √ 5
erfüllt, wobei φ der goldene Schnitt und φ dessen konjugiert komplexer Wert ist. 3.2-8 Zeigen Sie, dass aus k ln k = Θ(n) die Gleichung k = Θ(n/ ln n) folgt.
Problemstellungen 3-1 Asymptotisches Verhalten von Polynomen Gegeben seien ein Polynom p(n) =
d
ai ni
i=0
d-ten Grades in n mit ad > 0 und eine Konstante k. Benutzen Sie die Definitionen der asymptotischen Notationen, um die folgenden Eigenschaften zu beweisen. a. Ist k ≥ d, dann gilt p(n) = O(nk ). b. Ist k ≤ d, dann gilt p(n) = Ω(nk ). c. Ist k = d, dann gilt p(n) = Θ(nk ). d. Ist k > d, dann gilt p(n) = o(nk ). e. Ist k < d, dann gilt p(n) = ω(nk ).
Problemstellungen zu Kapitel 3
63
3-2 Relatives asymptotisches Wachstum Tragen Sie in der folgende Tabelle für jedes Paar (A, B) ein, ob A in der Ordnung O, o, Ω, ω oder Θ von B ist. Nehmen Sie an, dass k ≥ 1, > 0 und c > 1 Konstanten sind. A lgk n
B n cn
c.
nk √ n
nsin n
d.
2n
2n/2
a. b.
e. f.
n
lg c
lg(n!)
O
o
Ω
ω
Θ
clg n lg(nn )
3-3 Ordnen nach asymptotischen Wachstumsraten a. Ordnen Sie die folgenden Funktionen nach ihrem Wachstumsgrad, d. h. finden Sie eine Reihenfolge g1 , g2 , . . . , g30 der unten angegebenen Funktionen, sodass g1 = Ω(g2 ), g2 = Ω(g3 ), . . . , g29 = Ω(g30 ) gilt. Teilen Sie Ihre Liste in Äquivalenzklassen auf, sodass sich Funktionen f (n) und g(n) genau dann in derselben Äquivalenzklasse befinden, wenn f (n) = Θ(g(n)) gilt. lg(lg∗ n)
∗
2lg
n
√ ( 2)lg n 2
n2
n! n
( 32 )n
n3
lg n
lg(n!)
22
ln ln n
lg∗ n
n · 2n
nlg lg n
ln n
2 ∗
lg n
lg (lg n)
lg n
(lg n) 2
√ 2 lg n
e
n
n
4
lg n
2
n
(n + 1)! n lg n
(lg n)! n1/ lg n √ 2
1 lg n
2n+1
b. Geben Sie ein Beispiel für eine einzelne nichtnegative Funktion f (n) an, die für jede der Funktionen gi (n) aus Teil (a) weder in O(gi (n)) noch in Ω(gi (n)) ist. 3-4 Eigenschaften der asymptotischen Notation Seien f (n) und g(n) asymptotisch positive Funktionen. Beweisen oder widerlegen Sie jede der folgenden Vermutungen. a. f (n) = O(g(n)) impliziert g(n) = O(f (n)). b. f (n) + g(n) = Θ(min(f (n), g(n))). c. f (n) = O(g(n)) impliziert lg(f (n)) = O(lg(g(n))), wenn lg(g(n)) ≥ 1 und f (n) ≥ 1 für alle hinreichend großen n gilt. d. f (n) = O(g(n)) impliziert 2f (n) = O 2g(n) . e. f (n) = O (f (n))2 . f. f (n) = O(g(n)) impliziert g(n) = Ω(f (n)). g. f (n) = Θ(f (n/2)).
64
3 Wachstum von Funktionen h. f (n) + o(f (n)) = Θ(f (n)).
3-5 Variationen zu O und Ω Einige Autoren definieren Ω geringfügig anders als wir. Wir wollen die Notation ∞ Ω (gesprochen „omega unendlich“) für diese alternative Definition benutzen. Wir ∞ sagen, dass f (n) = Ω(g(n)) gilt, wenn eine positive Konstante c existiert, sodass für unendlich viele ganze Zahlen n die Ungleichung f (n) ≥ c g(n) ≥ 0 erfüllt ist. a. Zeigen Sie, dass für zwei beliebige asymptotisch nichtnegative Funktionen f (n) ∞ und g(n) entweder f (n) = O(g(n)) oder f (n) = Ω(g(n)) oder beides gilt. Zeigen ∞ Sie, dass dies nicht zutrifft, wenn wir Ω anstelle von Ω einsetzen. b. Beschreiben Sie die potentiellen Vorteile und Nachteile bei der Verwendung von ∞ Ω anstelle von Ω, um Laufzeiten von Programmen zu charakterisieren. Einige Autoren definieren auch O geringfügig anders. Wir wollen die Bezeichnung O für die alternative Definition verwenden. Wir sagen, dass f (n) = O (g(n)) genau dann gilt, wenn |f (n)| = O(g(n)) erfüllt ist. c. Gelten die beiden Implikationen der Aussage aus Theorem 3.1 noch, wenn wir O durch O ersetzen, aber weiterhin Ω benutzen? (gesprochen „weiches-oh“), womit O unter VerEinige Autoren definieren auch O nachlässigung logarithmischer Faktoren gemeint ist, genauer O(g(n)) = {f (n) : es existieren positive Konstanten c, k und n0 , sodass 0 ≤ f (n) ≤ c g(n) lgk (n) für alle n ≥ n0 } . und Θ. Beweisen Sie das Analogon zu Theod. Definieren Sie in gleicher Weise Ω rem 3.1. 3-6 Iterierte Funktionen Wir können den in der Funktion lg∗ verwendete Iterationsoperator ∗ auf jede monoton steigende Funktion f (n) über dem Körper der reellen Zahlen anwenden. Wir definieren die iterierte Funktion fc∗ für eine gegebene Konstante c ∈ R durch fc∗ (n) = min i ≥ 0 : f (i) (n) ≤ c , was nicht in allen Fällen wohldefiniert zu sein braucht. Die Größe fc∗ (n) ist demnach die Anzahl der iterierten Anwendungen der Funktion f , die notwendig sind, um deren Argument kleiner oder gleich c zu machen. Geben Sie für jede der folgenden Funktionen f (n) und Konstanten c die schärfste Schranke für fc∗ (n) an.
Kapitelbemerkungen zu Kapitel 3
a.
f (n) n−1
c 0
b.
lg n
1
c.
n/2
1
d.
n/2 √ n √ n
2 2
1/3
2
e. f. g. h.
n
n/ lg n
65
fc∗ (n)
1 2
Kapitelbemerkungen Knuth [209] führt den Ursprung der O-Notation auf eine Arbeit über Zahlentheorie von P. Bachmann aus dem Jahre 1892 zurück. Die o-Notation wurde 1909 von E. Landau im Rahmen seiner Erörterung zur Verteilung der Primzahlen eingeführt. Die Ω- und ΘNotationen setzte Knuth [213] durch, um die in der Literatur populäre, aber technisch gesehen unsaubere Praxis zu korrigieren, die O-Notation sowohl für obere als auch für untere Schranken anzuwenden. Häufig wird die O-Notation auch heute noch an Stellen benutzt, an denen die Θ-Notation technisch präziser ist. Weitere Bemerkungen zur Geschichte und zur Entwicklung der asymptotischen Notationen finden sich in den Arbeiten von Knuth [209, 213] sowie Brassard und Bratley [54]. Nicht alle Autoren definieren die asymptotischen Notationen in gleicher Weise, obwohl die verschiedenen Definitionen in den meisten Fällen übereinstimmen. Einige der alternativen Definitionen umfassen Funktionen, die nicht asymptotisch nichtnegativ sind, so lange deren Beträge hinreichend beschränkt sind. Gleichung (3.20) geht auf Robbins [297] zurück. Andere Eigenschaften elementarer mathematischer Funktionen finden sich in jedem guten Nachschlagewerk, wie zum Beispiel in Abramowitz und Stegun [1] oder Zwillinger [362] oder in einem Buch über Analysis, wie zum Beispiel in Apostol [18] oder in Thomas et al. [334]. Die Werke von Knuth [209] und Graham, Knuth und Patashnik [152] enthalten eine Fülle von Informationen zur diskreten Mathematik, wie sie in der Informatik angewendet wird.
4
Teile-und-Beherrsche
In Abschnitt 2.3.1 haben wir gesehen, wie Sortieren mit Hilfe des Teile-und-BeherrscheParadigma gelöst werden kann. Erinnern Sie sich daran, dass wir in dem Teile-undBeherrsche-Ansatz ein Problem rekursiv lösen, wobei auf einer Rekursionsebene drei Schritte ausgeführt werden: Teilen Sie das Problem in mehrere Teilprobleme, die kleinere Instanzen des gleichen Problems darstellen, auf. Beherrschen Sie die Teilprobleme, indem Sie sie rekursiv lösen. Wenn die Teilprobleme klein genug sind, dann lösen Sie die Teilprobleme auf direktem Wege. Vereinigen Sie die Lösungen der Teilprobleme zur Lösung des ursprünglichen Problems. Wenn die Teilprobleme ausreichend groß sind, um rekursiv gelöst werden zu können, dann sprechen wir vom rekursiven Fall . Sind die Teilprobleme so klein, dass es keinen Sinn macht, weiter rekursiv abzusteigen, so hat die Rekursion „ihren Boden gefunden“ und der Basisfall (auch Rekursionsverankerung genannt) ist erreicht. Manchmal haben wir neben den Teilproblemen, die kleinere Instanzen des gleichen Problems sind, Teilprobleme zu lösen, die dem ursprünglichen Problem nicht ganz entsprechen. Das Lösen solcher Teilprobleme sehen wir als Bestandteil des Vereinigungsschritts an. In diesem Kapitel werden wir weitere Algorithmen kennenlernen, die auf dem Teile-undBeherrsche-Paradigma beruhen. Das erste Verfahren löst das Max-Teilfeld-Problem: das Verfahren erhält als Eingabe ein (eindimensionales) Feld von Zahlen und berechnet ein zusammenhängendes Teilfeld, sodass die Summe der in diesem Teilfeld gespeicherten Zahlen maximal ist. Dann werden wir zwei Teile-und-Beherrsche-Algorithmen zum Multiplizieren von n × n Matrizen kennenlernen. Der eine Algorithmus läuft in Zeit Θ(n3 ), was nicht besser ist als die direkte Methode zum Multiplizieren quadratischer Matrizen. Der andere Algorithmus, Strassens Algorithmus, jedoch läuft in Zeit O(n2,81 ), und schlägt somit die direkte Methode asymptotisch.
Rekursionsgleichungen Rekursionsgleichungen gehen Hand in Hand mit dem Teile-und-Beherrsche-Paradigma, weil sie uns erlauben, in natürlicher Art und Weise die Laufzeit von Teile-und-BeherrscheAlgorithmen zu charakterisieren. Eine Rekursionsgleichung ist eine Gleichung oder eine Ungleichung, die eine Funktion durch ihre eigenen Funktionswerte für kleinere Eingaben beschreibt. In Abschnitt 2.3.2 haben wir zum Beispiel gesehen, dass die Laufzeit
68
4 Teile-und-Beherrsche
T (n) der Prozedur Merge-Sort im schlechtesten Fall durch die Rekursionsgleichung Θ(1) falls n = 1 , T (n) = (4.1) 2 T (n/2) + Θ(n) falls n > 1 , beschrieben werden kann, deren Lösung, wie wir behauptet haben, T (n) = Θ(n lg n) ist. Rekursionsgleichungen können vielfältige Formen haben. Beispielsweise könnte ein rekursiver Algorithmus das ursprüngliche Problem in Teilprobleme unterschiedlicher Größe teilen, z. B. in einem 2/3-zu-1/3-Verhältnis. Falls die entsprechenden Teilungs- und Vereinigungsschritte jeweils lineare Zeit benötigen, würde ein solcher Algorithmus zu der Rekursionsgleichung T (n) = T (2n/3) + T (n/3) + Θ(n) führen. Die Größe eines Teilproblems muss nicht ein konstanter Bruchteil der Größe des ursprünglichen Problems sein. Beispielsweise würde eine rekursive Version des linearen Suchens (siehe Übung 2.1-3) genau ein Teilproblem konstruieren, wobei dieses Teilproblem nur ein Element weniger als das ursprüngliche Problem hätte. Jeder rekursive Aufruf benötigt konstante Zeit plus die Zeit, die der von ihm aufgerufene rekursive Aufruf benötigt, was zur Rekursionsgleichung T (n) = T (n − 1) + Θ(1) führt. Dieses Kapitel bietet drei Methoden zur Lösung von Rekursionsgleichungen an – d. h. um asymptotische „Θ“- oder „O“-Schranken der Lösung zu erhalten: • Bei der Substitutionsmethode erraten wir eine Schranke und benutzen dann mathematische Induktion, um die Korrektkeit unserer Vermutung zu beweisen. • Die Rekursionsbaum-Methode wandelt die Rekursionsgleichung in einen Baum um, dessen Knoten die in den verschiedenen Ebenen der Rekursion anfallenden Kosten darstellen; wir benutzen Techniken zur Beschränkung von Summenformeln zum Lösen der Rekursionsgleichung. • Die Mastermethode liefert Schranken für Rekursionsgleichungen der Form T (n) = a T (n/b) + f (n) ,
(4.2)
wobei a ≥ 1, b > 1 gilt und f (n) eine gegebene Funktion ist. Solche Rekursionsgleichungen treten häufig auf. Eine Rekursionsgleichung der Form (4.2) charakterisiert die Laufzeit eines Teile-und-Beherrsche-Algorithmus, der das ursprüngliche Problem in a Teilprobleme, die alle eine Größe von 1/b der Größe des ursprünglichen Problems haben, aufteilt und bei dem die Teilungs- und Vereinigungsschritte jeweils Zeit f (n) benötigen. Um die Mastermethode anwenden zu können, müssen Sie sich drei Fälle einprägen. Wenn Sie dies getan haben, dann sind Sie fähig, asymptotische Schranken von vielen einfachen Rekursionsgleichungen ohne Umstände zu bestimmen. Wir werden die Mastermethode verwenden, um sowohl die Laufzeiten der Teile-undBeherrsche-Algorithmen für das Max-Teilfeld-Problem und für die Matrizenmultiplikation zu bestimmen als auch die von vielen anderen Algorithmen aus diesem Buch, die auf Teile-und-Beherrsche basieren.
4 Teile-und-Beherrsche
69
Bisweilen werden wir Rekursionen sehen, die keine Gleichungen sondern Ungleichungen sind, wie z. B. T (n) ≤ 2 T (n/2) + Θ(n). Da eine solche Rekursion eine obere Schranke von T (n) beschreibt, werden wir ihre Lösung in O-Notation und nicht in Θ-Notation ausdrücken. Wäre die Ungleichung anders herum, also T (n) ≥ 2 T (n/2) + Θ(n), so würden wir die Ω-Notation verwenden, da die Rekursion eine untere Schranke von T (n) beschreibt.
Technische Details in Rekursionsgleichungen In der Praxis vernachlässigen wir bestimmte technische Details, wenn wir Rekursionsgleichungen aufstellen und lösen. Wenn wir beispielsweise Merge-Sort angewendet auf eine ungerade Anzahl n von Elementen aufrufen, erhalten wir Teilprobleme der Größe n/2 und n/2 . Keiner der beiden Teilprobleme hat Größe n/2, da n/2 keine ganze Zahl ist, wenn n ungerade ist. Aus dieser technischer Sicht heraus lautet die Rekursionsgleichung für die Laufzeit von Merge-Sort im schlechtesten Fall tatsächlich T (n) =
Θ(1) T (n/2 ) + T ( n/2) + Θ(n)
falls n = 1 , falls n > 1 .
(4.3)
Randbedingungen stellen eine andere Klasse von Details dar, die wir typischerweise ignorieren. Da die Laufzeit eines Algorithmus bei konstanter Eingabegröße eine Konstante ist, sind bei den Rekursionsgleichungen, die sich für die Laufzeiten von Algorithmen ergeben, die Funktionswerte T (n) für hinreichend kleine n im Allgemeinen in Θ(1). Folglich werden wir in der Regel der Einfachheit halber die Angabe von Randbedingungen für Rekursionsgleichungen weglassen und annehmen, dass T (n) für kleine n konstant ist. So geben wir zum Beispiel die Rekursionsgleichung (4.1) normalerweise als T (n) = 2 T (n/2) + Θ(n)
(4.4)
an, ohne explizit Werte für kleine n festzulegen. Gerechtfertigt wird dies dadurch, dass das Verändern des Wertes T (1) die exakte Lösung der Rekursionsgleichung zwar beeinflusst, die Lösung aber üblicherweise nur um einen konstanten Faktor verändert wird und so der Wachstumsgrad unverändert bleibt. Wenn wir Rekursionsgleichungen aufstellen und lösen, vernachlässigen wir oft das Runden auf ganze Zahlen und Randbedingungen. Wir kommen ohne diese Details besser voran und können später entscheiden, ob sie von Bedeutung sind oder nicht. Gewöhnlich sind sie es nicht. Sie sollten aber wissen, wann sie es sind. Die Erfahrung hilft, und ebenso einige Theoreme, die Aussagen darüber machen, dass diese Details die asymptotischen Schranken vieler Rekursionsgleichungen, die die Laufzeiten von Teile-und-BeherrscheAlgorithmen beschreiben, nicht beeinflussen (siehe Theorem 4.1). In diesem Kapitel werden wir jedoch auf einige dieser Details zu sprechen kommen und die Feinheiten der Lösungsmethoden für Rekursionsmethoden illustrieren.
70
4 Teile-und-Beherrsche
4.1
Das Max-Teilfeld-Problem
Nehmen Sie an, dass Sie das Angebot bekommen haben, in die Volatile Chemische Aktiengesellschaft zu investieren. Wie die Chemikalien, die das Unternehmen produziert, ist der Aktienkurs der Gesellschaft ziemlich volatil. Sie dürfen nur einmal Aktien kaufen, die sie dann später an einem der folgenden Tage wieder verkaufen dürfen. Kaufen und Verkaufen erfolgt jeweils abends nach Börsenschluss. Um diese Einschränkung zu kompensieren, sagt man Ihnen, wie der Kurs der Aktie sich in der Zukunft tagesgenau entwickeln wird. Ihr Ziel besteht darin, Ihren Profit zu maximieren. Abbildung 4.1 zeigt die Kurse der Aktie über einen Zeitraum von 17 Tagen. Sie dürfen die Aktien an einem beliebigen Tag kaufen, beginnend bei Tag 0, an dem der Aktienkurs bei 100 $ steht. Natürlich wollen Sie „billig kaufen und teuer verkaufen“ – also zu dem niedrigsten möglichen Kurs kaufen und zu dem höchsten möglichen Kurs verkaufen –, um Ihren Profit zu maximieren. Leider wird Ihnen das aber in der Regel nicht möglich sein. In Abbildung 4.1 ist der Kurs der Aktie nach Börsenschluss des 7. Tages am niedrigsten und nach Börsenschluss des 1. Tages am höchsten. 120 110 100 90 80 70 60 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Tag 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Aktienpreis 100 113 110 85 105 102 86 63 81 101 94 106 101 79 94 90 97 13 −3 −25 20 −3 −16 −23 18 20 −7 12 −5 −22 15 −4 7 Veränderung Abbildung 4.1: Kurse der Aktie der Volatilen Chemischen Aktiengesellschaft nach Börsenschluss für eine Periode von 17 Tagen. Die horizontale Achse des Diagramms gibt die Tage an, und die vertikale den Aktienpreis. Die untere Reihe der Tabelle enthält die Veränderungen des Aktienpreises jeweils verglichen mit dem Preis vom Vortag.
Möglicherweise denken Sie, dass Sie den Profit immer maximieren können, wenn Sie entweder beim tiefsten Kurs kaufen oder beim höchsten Kurs verkaufen. In Abbildung 4.1, würden wir den Profit maximieren, wenn wir nach Börsenschluss des 7. Tages kaufen würden. Wenn diese Strategie funktionieren würde, dann wäre es einfach, den Profit zu maximieren: finde den höchsten und niedrigsten Kurs, suche dann links vom höchsten Kurs den vordran niedrigsten Kurs und rechts vom niedrigsten Kurs den höchsten Kurs, der sich nach diesem Tag noch einstellt, und nehme als Lösung dann das Zeitpunktenpaar mit der größten Differenz. Abbildung 4.2 zeigt ein einfaches Gegenbeispiel: sie zeigt, dass manchmal der maximale Profit erzielt werden kann, wenn man weder zum niedrigsten Kurs kauft noch zum höchsten Kurs verkauft.
4.1 Das Max-Teilfeld-Problem
71
11 10 9 8 7 6
Tag 0 1 2 3 4 Aktienkurs 10 11 7 10 6 1 −4 3 −4 Veränderung
0
1
2
3
4
Abbildung 4.2: Das Beispiel zeigt, dass der maximale Profit nicht immer beim niedrigsten Kurs startet oder beim höchsten Kurs endet. Wie vorhin gibt die horizontale Achse die Tage an und die vertikale den Aktienkurs. In diesem Beispiel liegt der maximale Profit bei 3 $ pro Aktie, den man erzielen kann, wenn man nach Börsenschluss des 2. Tages kaufen und nach Börsenschluss des 3. Tages verkaufen würde. Der Aktienkurs von 7 $ nach Börsenschluss des 2. Tages ist nicht der insgesamt niedrigste Kurs und der von 10 $ nach Börsenschluss des 3. Tages ist nicht der insgesamt höchste Kurs.
Ein Brute-Force-Ansatz Wir können sehr leicht einen Brute-Force-Ansatz zum Lösen diese Problems angeben: wir überprüfen einfach jedes mögliche Paar von Kauf- und Verkaufszeitpunkten, in denen das Kaufdatum Verkaufsdatum liegt. Eine Periode über n Tage hat n2 n vor dem solcher Paare. Da 2 in Θ(n2 ) liegt, und eine konstante Laufzeit zur Auswertung eines jeden dieser Paare das Beste ist, was wir uns erhoffen können, würde dieser Ansatz Ω(n2 ) Zeit benötigen. Können wir es besser?
Das Problem aus einem anderen Blickwinkel Um einen Algorithmus mit einer Laufzeit von o(n2 ) zu entwerfen, wollen wir uns das Problem aus einem leicht anderen Blickwinkel anschauen. Wir wollen eine Folge von Tagen finden, über denen die aufgerechneten Tageskursänderungen vom ersten zum letzten Tag dieser Folge maximal ist. Anstatt auf den täglichen Aktienkurs zu schauen, betrachten wir die täglichen Kursänderungen, wobei die Kursänderung am i. Tag die Differenz des Aktienkurs zum Börsenschluss des i. Tages und des Aktienkurses zum Börsenschluss des i − 1. Tages ist. Die Tabelle in Abbildung 4.1 zeigt diese täglichen Veränderungen in der unteren Zeile. Wenn wir diese Zeile, wie in Abbildung 4.3 gezeigt, als ein Feld A betrachten, besteht die Aufgaben darin, ein nichtleeres, zusammenhängendes Teilfeld von A zu finden, deren Werte aufaddiert zu der größten Summe führt. Wir nennen dieses Teilfeld das maximale Teilfeld . Im Feld aus Abbildung 4.3 zum Beispiel ist das maximale Teilfeld des Feldes A[1 . . 16] das Teilfeld A[8 . . 11], mit einer Summe von 43. Sie sollten also die Aktie nach Börsenschluss des 7. Tages kaufen und nach Börsenschluss des 11. Tages verkaufen, um einen Profit von 43 $ pro Aktie einzufahren. Auf den ersten Blick hilft diese Transformation des Problems nichts. Wir haben weiterhin n−1 = Θ(n2 ) Teilfelder bei einer Periode von n Tagen zu betrachten. Übung 4.1-2 2 verlangt von Ihnen zu zeigen, dass die Berechnung so organisiert werden kann, dass die Berechnung der Summe eines Teilfeldes in Zeit O(1) erfolgen kann, wenn die im Vorfeld bereits berechneten Werte seiner Teilfelder gegeben sind, sodass der Brute-Force-Ansatz
72
4 Teile-und-Beherrsche 15
16
A 13 –3 –25 20 –3 –16 –23 18 20 –7 12 –5 –22 15 –4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
7
maximales Teilfeld Abbildung 4.3: Die Änderungen des Aktienkurses als Max-Teilfeld-Problem. In diesem Beispiel besitzt das Teilfeld A[8 . . 11], mit einer Summe von 43, die größte aufaddierte Summe aller zusammenhängenden Teilfelder des Feldes A.
Zeit Θ(n2 ) benötigt – wenngleich die Berechnung der Kosten eines Teilfeldes möglicherweise eine Laufzeit proportional zu der Länge des Teilfeldes hat. Lassen Sie uns also nach einer effizienteren Lösung für das Max-Teilfeld-Problem suchen. In diesem Zusammenhang werden wir üblicherweise von „einem“ maximalen Teilfeld und nicht von „dem“ maximalen Teilfeld sprechen, da es mehr als ein Teilfeld geben könnte, das maximal ist. Das Max-Teilfeld-Problem ist nur dann interessant, wenn das Feld einige negative Zahlen enthält. Wenn alle Feldeinträge nichtnegativ wären, würde das Max-Teilfeld-Problem keine Herausforderung darstellen, da das gesamte Feld die größte Summe ergeben würde.
Eine auf Teile-und-Beherrsche basierende Lösung Lassen Sie uns darüber nachdenken, wie wir das Max-Teilfeld-Problem mittels der Teileund-Beherrsche-Technik lösen können. Nehmen Sie an, wir wollten ein maximales Teilfeld des Teilfeldes A[links . . rechts] finden. Das Teile-und-Beherrsche-Paradigma regt an, das Teilfeld in zwei Teilfelder gleicher Größe zu teilen, soweit dies möglich ist. Wir haben also die Mitte des Teilfeldes, die wir mit mitte bezeichnen wollen, zu berechnen und dann die Teilfelder A[links . . mitte] und A[mitte + 1 . . rechts] zu betrachten. Wie Abbildung 4.4(a) zeigt, muss jedes zusammenhängende Teilfeld A[i . . j] von A[links . . rechts] in genau einer der folgenden Lagen liegen: • komplett im Teilfeld A[links . . mitte], d. h. es gilt links ≤ i ≤ j ≤ mitte, • komplett im Teilfeld A[mitte + 1 . . rechts], d. h. es gilt mitte < i ≤ j ≤ rechts, oder • mittig, d. h. es gilt links ≤ i ≤ mitte < j ≤ rechts. Somit muss auch das maximale Teilfeld von A[links . . rechts] in genau einer dieser Lagen liegen. In der Tat, das maximale Teilfeld von A[links . . rechts] muss die größte aufaddierte Summe von allen Teilfeldern haben, die komplett in A[links . . mitte] liegen, komplett in A[mitte + 1 . . rechts] liegen oder mittig angeordnet sind. Wir können maximale Teilfelder von A[links . . mitte] und A[mitte + 1 . . rechts] rekursiv berechnen, da diese zwei Teilprobleme kleinere Instanzen des Max-Teilfeld-Problems sind. Das einzige, das dann noch zu tun ist, ist ein maximales mittiges Teilfeld zu finden und dann das Teilfeld als
4.1 Das Max-Teilfeld-Problem
73
mittig links
A[mitte + 1 . . j]
mitte
rechts
mitte + 1
in A[links . . mitte]
in A[mitte + 1 . . rechts] (a)
links
i
mitte
rechts mitte + 1
j
A[i . . mitte] (b)
Abbildung 4.4: (a) Mögliche Lagen der Teilfelder von A[links . . rechts ]: komplett in A[links . . mitte], komplett in A[mitte + 1 . . rechts ], oder mittig. (b) Jedes Teilfeld von A[links . . rechts ], das die Mitte enthält, besteht aus zwei Teilfeldern A[i . . mitte] und A[mitte + 1 . . j] mit links ≤ i ≤ mitte und mitte < j ≤ rechts .
Lösung zu nehmen, das von diesen drei berechneten Teilfeldern die größte aufaddierte Summe besitzt. Wir können ein maximales mittiges Teilfeld ohne Umstände in einer Zeit, die linear in der Größe des Teilfeldes A[links . . rechts] ist, berechnen. Dieses Problem ist keine kleinere Instanz des ursprünglichen Problems, da es die zusätzliche Einschränkung hat, dass das gesuchte Teilfeld die Mitte überschreiten muss. Wie Abbildung 4.4(b) zeigt, besteht jedes mittige Teilfeld selbst aus zwei Teilfelder A[i . . mitte] und A[mitte + 1 . . j] mit links ≤ i ≤ mitte und mitte < j ≤ rechts. Wir haben aus diesem Grund einfach nur maximale Teilfelder der Form A[i . . mitte] und A[mitte + 1 . . j] zu finden und diese zu vereinigen. Die Prozedur Find-Max-Crossing-Subarray bekommt als Eingabe ein Feld A und die Indizes links, mitte und rechts und gibt ein Tupel bestehend aus den Indizes, die ein maximales mittiges Teilfeld demarkieren, sowie die Summe der Werte in einem maximalen Teilfeld zurück.
Find-Max-Crossing-Subarray(A, links, mitte, rechts) 1 linke-summe = −∞ 2 summe = 0 3 for i = mitte downto links 4 summe = summe + A[i] 5 if summe > linke-summe 6 linke-summe = summe 7 max -links = i 8 rechte-summe = −∞ 9 summe = 0 10 for j = mitte + 1 to rechts 11 summe = summe + A[j] 12 if summe > rechte-summe 13 rechte-summe = summe 14 max -rechts = j 15 return (max -links, max -rechts, linke-summe + rechte-summe)
74
4 Teile-und-Beherrsche
Diese Prozedur arbeitet wie folgt. Die Zeilen 1–7 berechnen ein maximales Teilfeld in der linken Hälfte A[links . . mitte] des Feldes. Da das Teilfeld, das in diesen Zeilen berechnet werden soll, A[mitte] enthalten muss, startet die Laufvariable i der for-Schleife in den Zeilen 3–7 bei mitte; die Schleife arbeitet sich dann von der Mitte aus nach links zum linken Rand des Feldes vor, sodass jedes Teilfeld, das durch die Schleife betrachtet wird, von der Form A[i . . mitte] ist. Die Zeilen 1–2 initialisieren die Variablen linke-summe, in der die bis dahin größte gefundene Summe abgespeichert wird, und summe, die die Summe der Werte aus A[i . . mitte] enthält. Immer wenn wir in Zeile 5 ein Teilfeld A[i . . mitte] finden, dessen aufaddierte Werte größer als linke-summe ist, aktualisieren wir in Zeile 6 linke-summe auf die Summe dieses Teilfeldes und in Zeile 7 die Variable max -links, in der wir uns den entsprechenden Index i merken. Die Zeilen 8–14 arbeiten analog für die rechte Hälfte A[mitte + 1 . . rechts]. Hier startet die Laufvariable j der for-Schleife in den Zeilen 10–14 bei mitte + 1 und die Schleife arbeitet sich von dieser Stelle aus nach rechts zum rechten Rand des Feldes vor, sodass sie jedes Teilfeld der Form A[mitte + 1 . . j] betrachtet. In Zeile 15 werden die Indizes max -links and max -rechts, die ein maximales mittiges Teilfeld demarkieren, zusammen mit der Summe linke-summe + rechte-summe der Werte des Teilfeldes A[max -links . . max -rechts] schlussendlich zurückgegeben. Wir behaupten, dass Find-Max-Crossing-Subarray(A, links, mitte, rechts) Zeit Θ(n) benötigt, wenn das Teilfeld A[links . . rechts] aus n Einträgen besteht, d. h. wenn n = rechts − links + 1 gilt. Da jede Iteration einer jeden der zwei for-Schleifen jeweils Θ(1) Zeit benötigt, haben wir nur zu zählen, wie viele Iterationen insgesamt ausgeführt werden. Die for-Schleife in den Zeilen 3–7 wird mitte − links + 1 mal ausgeführt und die for-Schleife in den Zeilen 10–14 rechts − mitte mal. Somit beläuft sich die Gesamtzahl der Iterationen auf (mitte − links + 1) + (rechts − mitte) = rechts − links + 1 = n. Nachdem wir die in linearer Zeit ausführbare Prozedur Find-Max-Crossing-Subarray kennengelernt haben, können wir den Pseudocode für den Teile-und-Beherrsche-Algorithmus für das Max-Teilfeld-Problem aufschreiben: Find-Maximum-Subarray(A, links, rechts) 1 if rechts = = links 2 return (links, rechts, A[links]) // Basisfall: nur ein Element 3 else mitte = (links + rechts)/2 4 (links-links, links-rechts, linke-summe) = Find-Maximum-Subarray(A, links, mitte) 5 (rechts-links, rechts-rechts, rechte-summe) = Find-Maximum-Subarray(A, mitte + 1, rechts) 6 (mittig-links, mittig-rechts, mittige-summe) = Find-Max-Crossing-Subarray(A, links, mitte, rechts) 7 if linke-summe ≥ rechte-summe und linke-summe ≥ mittige-summe 8 return (links-links, links-rechts, linke-summe) 9 elseif rechte-summe ≥ linke-summe und rechte-summe ≥ mittige-summe 10 return (rechts-links, rechts-rechts, rechte-summe) 11 else return (mittig-links, mittig-rechts, mittige-summe)
4.1 Das Max-Teilfeld-Problem
75
Der initiale Aufruf Find-Maximum-Subarray(A, 1, A.l¨a nge) berechnet das maximale Teilfeld von A[1 . . n]. Wie Find-Max-Crossing-Subarray gibt die rekursive Prozedur Find-MaximumSubarray ein Tupel zurück, das die Indizes enthält, die das maximale Teilfeld demarkieren, zusammen mit der Summe der Werte eines maximalen Teilfeldes. Zeile 1 testet auf den Basisfall, in dem das Teilfeld nur aus einem Element besteht. Ein Teilfeld, das nur aus einem Element besteht, hat nur ein Teilfeld – nämlich sich selbst – und so gibt Zeile 2 das Tupel bestehend aus dem Index dieses einzigen Elementes zusammen mit seinem Wert zurück. Die Zeilen 3–11 behandeln den rekursiven Fall. In Zeile 3 erfolgt das Aufteilen des Problems, indem der Index mitte der Mitte des Teilfeldes berechnet wird. Lassen Sie uns das Teilfeld A[links . . mitte] das linke Teilfeld und das Teilfeld A[mitte + 1 . . rechts] das rechte Teilfeld nennen. Da wir wissen, dass das Teilfeld A[links . . rechts] wenigstens zwei Elemente enthält, enthält das linke und das rechte Teilfeld jeweils wenigstens ein Element. Die Beherrsche-Phase erfolgt in den Zeilen 4 und 5, in denen das maximale Teilfeld des linken Teilfeldes und das des rechten Teilfeldes berechnet werden. Die Zeilen 6–11 realisieren die Vereinigung der rekursiv berechneten Lösungen der Teilprobleme. Zeile 6 berechnet das maximale mittige Teilfeld. (Erinnern Sie sich bitte daran, dass Zeile 6 ein Teilproblem löst, das nicht eine kleinere Instanz des ursprünglichen Problems ist, und wir somit die Zeile zum Vereinigungsschritt zählen.) Zeile 7 testet, ob das linke Teilfeld ein Teilfeld mit maximaler Summe enthält und Zeile 8 gibt in diesem Fall dieses maximale Teilfeld zurück. Ansonsten testet Zeile 9, ob das rechte Teilfeld ein Teilfeld mit maximaler Summe enthält und Zeile 10 gibt dieses maximale Teilfeld zurück. Wenn weder das linke noch das rechte Teilfeld ein Teilfeld mit maximaler Summe enthält, so muss ein maximales Teilfeld mittig angeordnet sein; Zeile 11 gibt dieses zurück.
Analyse des Teile-und-Beherrsche-Algorithmus Als nächstes stellen wir eine Rekursionsgleichung auf, die die Laufzeit der rekursiven Prozedur Find-Maximum-Subarray beschreibt. Genauso wie wir dies bei der Analyse von Sortieren durch Mischen in Abschnitt 2.3.2 getan haben, machen wir auch hier die vereinfachende Annahme, dass die Größe des ursprünglichen Problems eine Zweierpotenz ist, sodass die Größe aller Teilprobleme ebenfalls Zweierpotenzen sind und bei der Berechnung der Mitte nicht abgerundet werden muss. Wir bezeichnen die Laufzeit von Find-Maximum-Subarray angewendet auf ein Teilfeld von n Elementen mit T (n). Zunächst einmal benötigt Zeile 1 konstante Zeit. Der Basisfall, also wenn n = 1 gilt, ist einfach: Zeile 2 benötigt konstante Zeit und somit gilt: T (1) = Θ(1) .
(4.5)
Der rekursive Fall liegt vor, wenn n > 1 gilt. Die Zeilen 1 und 3 benötigen konstante Zeit. Jedes der Teilprobleme, die in den Zeilen 4 und 5 gelöst werden, sind von der Größe n/2 (unsere Annahme, dass die Größe des ursprünglichen Problems eine Zweierpotenz ist, gewährleistet, dass n/2 eine ganze Zahl ist) und somit verbrauchen wir T (n/2) Zeit, um jedes von ihnen zu lösen. Da wir zwei Teilprobleme – für das linke Teilfeld und für das rechte Teilfeld – zu lösen haben, tragen die Zeilen 4 und 5 zusammen 2 T (n/2)
76
4 Teile-und-Beherrsche
zur Gesamtlaufzeit bei. Wie wir bereits gesehen haben, benötigt die Ausführung von Find-Max-Crossing-Subarray in Zeile 6 Zeit Θ(n). Die Zeilen 7–11 benötigen nur Zeit Θ(1). Wir erhalten demnach für den rekursiven Fall T (n) = Θ(1) + 2 T (n/2) + Θ(n) + Θ(1) = 2 T (n/2) + Θ(n) .
(4.6)
Die Gleichungen (4.5) und (4.6) stellen zusammen die Rekursionsgleichung für die Laufzeit T (n) von Find-Maximum-Subarray dar: Θ(1) falls n = 1 , T (n) = (4.7) 2 T (n/2) + Θ(n) falls n > 1 . Diese Rekursionsgleichung ist die gleiche wie die Rekursionsgleichung (4.1) für Sortieren durch Mischen. Wie wir bei der in Abschnitt 4.5 vorgestellten Mastermethode sehen werden, hat diese Rekursionsgleichung die Lösung T (n) = Θ(n lg n). Sie können sich auch den Rekursionsbaum in Abbildung 2.5 nochmals anschauen, um zu verstehen, warum die Lösung T (n) = Θ(n lg n) sein sollte. Wir haben also gesehen, dass die Teile-und-Beherrsche-Methode zu einem Algorithmus führt, der asymptotisch schneller als die Brute-Force-Methode ist. Mit Sortieren durch Mischen und jetzt mit dem Max-Teilfeld-Problem bekommen wir eine Idee, wie mächtig die Teile-und-Beherrsche-Methode sein kann. In einigen Fällen liefert sie den asymptotisch besten Algorithmus für ein Problem, in anderen Fällen gibt es bessere Lösungen. Wie Übung 4.1-5 zeigt, gibt es tatsächlich ein Linearzeit-Algorithmus für das Max-Teilfeld-Problem und dieser benutzt Teile-und-Beherrsche nicht.
Übungen 4.1-1 Was berechnet Find-Maximum-Subarray, wenn alle Einträge von A negativ sind? 4.1-2 Geben Sie den Pseudocode für die Brute-Force-Methode zum Lösen des MaxTeilfeld-Problems an. Ihre Prozedur sollte in Θ(n2 ) Zeit laufen. 4.1-3 Implementieren Sie sowohl die Brute-Force-Methode als auch den rekursiven Algorithmus für das Max-Teilfeld-Problem auf Ihrem Rechner. Ab welcher Größe n0 schlägt der rekursive Algorithmus den Brute-Force-Algorithmus? Ändern Sie dann den Basisfall des rekursiven Algorithmus, sodass die Brute-Force-Methode immer dann benutzt wird, wenn die Problemgröße kleiner als n0 ist. Ab welcher Größe schlägt nun der rekursive Algorithmus die Brute-Force-Methode? 4.1-4 Nehmen Sie an, wir würden die Definition des Max-Teilfeld-Problems so ändern, dass das leere Teilfeld als Ergebnis zulässig ist, wobei die Summe der Werte eines leeren Teilfeldes gleich 0 ist. Wie müssten Sie die Algorithmen ändern? 4.1-5 Benutzen Sie die folgenden Ideen, um einen nichtrekursiven Algorithmus für das Max-Teilfeld-Problem, der lineare Zeit benötigt, zu entwickeln. Beginnen
4.2 Strassens Algorithmus zur Matrizenmultiplikation
77
Sie am linken Rand des Feldes und arbeiten Sie sich nach rechts vor, wobei Sie sich jeweils das bisher gefundene maximale Teilfeld merken. Nutzen Sie diese Information jeweils aus, um ein maximales Teilfeld, das an der Stelle j + 1 endet, zu berechnen. Zentral ist hierbei die folgende Beobachtung: ein maximales Teilfeld von A[1 . . j + 1] ist entweder ein maximales Teilfeld von A[1 . . j] oder ein Teilfeld A[i . . j +1] für ein 1 ≤ i ≤ j +1. Berechnen Sie in konstanter Zeit ein maximales Teilfeld der Form A[i . . j + 1], indem Sie ausnutzen, dass Sie bereits ein maximales Teilfeld, das an der Stelle j endet, kennen.
4.2
Strassens Algorithmus zur Matrizenmultiplikation
Wenn Sie Matrizen kennen, wissen Sie höchstwahrscheinlich auch wie man diese multipliziert. (Ansonsten sollten Sie den Abschnitt D.1 im Anhang D lesen.) Wenn A = (aij ) und B = (bij ) quadratische n × n Matrizen sind, dann sind die Einträge cij , für i, j = 1, 2, . . . , n, im Produkt C = A · B durch cij =
n
aik · bkj
(4.8)
k=1
gegeben. Wir müssen also n2 Matrizeneinträge berechnen und jeder dieser Einträge ist eine Summe von n Werten. Die folgende Prozedur erhält als Eingabe n × n-Matrizen A und B und multipliziert sie. Wir nehmen an, dass jede Matrix ein Attribut zeilen besitzt, das die Anzahl der Zeilen in der Matrix angibt. Square-Matrix-Multiply(A, B) 1 n = A.zeilen 2 sei C eine neue n × n-Matrix 3 for i = 1 to n 4 for j = 1 to n 5 cij = 0 6 for k = 1 to n 7 cij = cij + aik · bkj 8 return C Die Prozedur Square-Matrix-Multiply arbeitet wie folgt. Die for-Schleife in den Zeilen 3–7 berechnet die Einträge einer jeden Matrixzeile i und innerhalb einer gegebenen Matrixzeile i berechnet die for-Schleife in den Zeilen 4–7 die Einträge cij für jede Spalte j. Zeile 5 initialisiert cij mit 0, um dann mit der Berechnung der Summe aus Gleichung (4.8) zu beginnen. Jede Iteration der for-Schleife der Zeilen 6–7 addiert einen weiteren Term der Gleichung (4.8). Da jede der dreifach geschachtelten for-Schleifen genau n Iterationen durchläuft und die Zeile 7 jeweils in konstanter Zeit ausgeführt werden kann, benötigt die Prozedur Square-Matrix-Multiply Θ(n3 ) Zeit.
78
4 Teile-und-Beherrsche
Auf den ersten Blick könnten Sie vielleicht glauben, dass jeder Algorithmus zur Matrizenmultiplikation Ω(n3 ) Zeit benötigen muss, da die normale Definition der Matrizenmultiplikation so viele Multiplikationen verlangt. Sie hätten unrecht: Wir kennen eine Möglichkeit, Matrizen in o(n3 ) Zeit zu multiplizieren. In diesem Abschnitt werden wir Strassens bemerkenswerten rekursiven Algorithmus zur Multiplikation von n × nMatrizen kennenlernen. Er benötigt Θ(nlg 7 ) Zeit. Dies werden wir in Abschnitt 4.5 zeigen. Da lg 7 zwischen 2, 80 und 2, 81 liegt, benötigt Strassens Algorithmus O(n2,81 ) Zeit, was asymptotisch besser ist als die einfache Square-Matrix-Multiply Prozedur.
Ein einfacher Teile-und-Beherrsche-Algorithmus Der Einfachheit halber nehmen wir bei Teile-und-Beherrsche-Algorithmen zur Berechnung des Matrizenproduktes C = A · B von quadratischen n × n-Matrizen A und B an, dass n eine Zweierpotenz ist. Wir machen diese Annahme, da in jedem Teilungsschritt n × n-Matrizen jeweils in vier n/2 × n/2-Matrizen aufgeteilt werden und durch unsere Annahme, dass n eine Zweierpotenz ist, wir garantieren können, dass die Größe n/2 eine ganze Zahl ist, solange n ≥ 2 gilt. Nehmen Sie an, dass wir jede der Matrizen A, B und C in jeweils vier n/2×n/2-Matrizen partitionieren A=
A11 A12 A21 A22
,
B=
B11 B12 B21 B22
,
C=
C11 C12 C21 C22
,
(4.9)
sodass wir die Gleichung C = A · B umschreiben können zu
C11 C12 C21 C22
=
A11 A12 A21 A22
B11 B12 · . B21 B22
(4.10)
Gleichung (4.10) entspricht den vier Gleichungen
C11 C12 C21 C22
= = = =
A11 · B11 + A12 · B21 A11 · B12 + A12 · B22 A21 · B11 + A22 · B21 A21 · B12 + A22 · B22
, , , .
(4.11) (4.12) (4.13) (4.14)
Jede dieser vier Gleichungen schreibt zwei Multiplikationen von n/2×n/2-Matrizen und die Addition ihrer n/2 × n/2-Produkte vor. Wir können diese Gleichungen anwenden, um einen einfachen rekursiven Teile-und-Beherrsche-Algorithmus zu erhalten:
4.2 Strassens Algorithmus zur Matrizenmultiplikation
79
Square-Matrix-Multiply-Recursive(A, B) 1 n = A.zeilen 2 sei C eine neue n × n-Matrix 3 if n = = 1 4 c11 = a11 · b11 5 else partitioniere A, B und C gemäß Gleichung (4.9) 6 C11 = Square-Matrix-Multiply-Recursive(A11 , B11 ) + Square-Matrix-Multiply-Recursive(A12 , B21 ) 7 C12 = Square-Matrix-Multiply-Recursive(A11 , B12 ) + Square-Matrix-Multiply-Recursive(A12 , B22 ) 8 C21 = Square-Matrix-Multiply-Recursive(A21 , B11 ) + Square-Matrix-Multiply-Recursive(A22 , B21 ) 9 C22 = Square-Matrix-Multiply-Recursive(A21 , B12 ) + Square-Matrix-Multiply-Recursive(A22 , B22 ) 10 return C Dieser Pseudocode geht auf ein ausgetüfteltes aber wichtiges Detail der Implementierung nicht ein. Wie partitionieren wir die Matrizen in Zeile 5? Wenn wir 12 neue n/2 × n/2-Matrizen generieren würden, müssten wir Θ(n2 ) Zeit aufbringen, um die Einträge zu kopieren. Tatsächlich können wir die Matrizen partitionieren, ohne die Einträge zu kopieren. Der Trick besteht darin, dass wir mit Indexberechnungen arbeiten. Wir identifizieren eine Teilmatrix durch einen Bereich von Zeilenindizes und einen Bereich von Spaltenindizes der ursprünglichen Matrix. Dadurch müssen wir die Teilmatrizen ein bisschen anders darstellen als wir die ursprüngliche Matrix darstellen. Der Vorteil, dass wir Teilmatrizen mittels Indexberechnungen spezifizieren können, besteht darin, dass die Ausführung von Zeile 5 nur Θ(1) Zeit kostet (wenngleich wir sehen werden, dass es keinen Einfluss auf die asymptotische Gesamtlaufzeit hat, ob wir kopieren oder „in-place“ partitionieren). Wie leiten nun die Rekursionsgleichung her, die die Laufzeit von Square-MatrixMultiply-Recursive beschreibt. Sei T (n) die Laufzeit, die diese Prozedur zum Multiplizieren zweier n × n-Matrizen benötigt. Im Basisfall, d. h. wenn n = 1 gilt, führen wir nur eine skalare Multiplikation in Zeile 4 aus und somit gilt T (1) = Θ(1) .
(4.15)
Der rekursive Fall liegt vor, wenn n > 1 gilt. Wie bereits diskutiert, benötigt das Partitionieren der Matrizen in Zeile 5 Zeit Θ(1), sofern wir mit Indexberechnungen arbeiten. In den Zeilen 6–9 rufen wir acht Mal Square-Matrix-Multiply-Recursive rekursiv auf. Da jeder rekursive Aufruf zwei n/2 × n/2-Matrizen multipliziert und somit T (n/2) Zeit zur Gesamtlaufzeit beiträgt, benötigen diese acht rekursiven Aufrufe zusammen 8 T (n/2) Zeit. Wir müssen auch die vier Matrizenadditionen aus den Zeilen 6–9 berücksichtigen. Jeder dieser Matrizen enthält n2 /4 Einträge und so benötigt jede der vier Matrizenadditionen Θ(n2 ) Zeit. Da die Anzahl auszuführender Matrizenadditionen konstant ist, ist die Gesamtzeit, die für die Matrizenadditionen aus den Zeilen 6–9 aufgebracht werden muss, gleich Θ(n2 ). (Auch hier arbeiten wir mit Indexberechnungen,
80
4 Teile-und-Beherrsche
um die Ergebnisse der Matrizenaddition auf der richtigen Stelle in der Matrix C abzuspeichern; die Berechnungszeit pro Eintrag ist hierfür in Θ(1).) Die Gesamtzeit für den rekursiven Fall ist deshalb gleich der Summe der Zeit zum Partitionieren der Matrizen, der Zeit für alle rekursiven Aufrufe und die Zeit zum Addieren der Matrizen, die durch die rekursiven Aufrufe erzeugt worden sind: T (n) = Θ(1) + 8 T (n/2) + Θ(n2 ) = 8 T (n/2) + Θ(n2 ) .
(4.16)
Hätten wir das Partitionieren der Matrizen durch Kopieren realisiert – was Θ(n2 ) Zeit benötigt –, würde sich die Rekursionsgleichung nicht ändern und sich die Gesamtlaufzeit somit nur um einen konstanten Faktor erhöhen. Die Gleichungen (4.15) und (4.16) stellen die vollständige Rekursionsgleichung für die Laufzeit von Square-Matrix-Multiply-Recursive dar: Θ(1) falls n = 1 , T (n) = (4.17) 8 T (n/2) + Θ(n2 ) falls n > 1 . Wie wir später mit Hilfe der Mastermethode aus Abschnitt 4.5 sehen werden, hat die Rekursionsgleichung (4.17) die Lösung T (n) = Θ(n3 ). Demnach ist dieser einfache Teileund-Beherrsche-Ansatz nicht schneller als die Square-Matrix-Multiply Prozedur. Bevor wir weitermachen mit Strassens Algorithmus, lassen Sie uns nochmals zusammenfassen, wie sich die einzelnen Teilausdrücke der Gleichung (4.16) begründen. Das Partitionieren jeder n × n-Matrix mit Indexberechnung kostet Θ(1) Zeit. Wir haben aber zwei Matrizen zu partitionieren. Wenngleich Sie argumentieren könnten, dass das Partitionieren von zwei Matrizen dann Θ(2) Zeit benötigt, wird die Konstante 2 durch die Θ-Notation subsumiert. Das Addieren von zwei Matrizen, jede sagen wir mit k Einträgen, benötigt Θ(k) Zeit. Da die Matrizen, die wir addieren, n2 /4 Einträge besitzen, können Sie behaupten, dass das Addieren eines Paares solcher Matrizen in Θ(n2 /4) Zeit erfolgt. Wiederum subsumiert jedoch die Θ-Notation den konstanten Faktor von 1/4 und wir stellen fest, dass das Addieren von zwei n/2×n/2-Matrizen Θ(n2 ) Zeit benötigt. Wir haben vier solcher Matrizenadditionen auszuführen und erneut können wir sagen, dass diese in einer Gesamtzeit von Θ(n2 ) ausgeführt werden, anstatt zu sagen, dass sie Zeit Θ(4n2 ) benötigen. (Möglicherweise haben Sie bemerkt, dass wir natürlich auch sagen können, dass die vier Matrizenadditionen zusammen Θ(4n2 /4) benötigen und dass 4n2 /4 = n2 gilt. Der Punkt an dieser Stelle ist aber, dass die Θ-Notation konstante Faktoren subsumiert, unabhängig welche Konstanten das sind.) Somit bleiben nur zwei Terme aus Θ(n2 ) übrig, die wir in einem Term vereinigen können. Wenn wir jedoch die Laufzeit der acht rekursiven Aufrufe betrachten, können wir nicht einfach den konstanten Faktor von 8 subsumieren. Anders formuliert, wir müssen angeben, dass diese rekursiven Aufrufe zusammen 8 T (n/2) Zeit und nicht nur T (n/2) Zeit benötigen. Warum das so ist, können wir uns klarmachen, wenn wir nochmals auf den Rekursionsbaum aus Abbildung 2.5 schauen – dieser Reduktionsbaum illustriert die Rekursionsgleichung (2.1), T (n) = 2 T (n/2) + Θ(n), die identisch zu der Rekursionsgleichung (4.7) ist. Der Faktor von 2 gibt an, wie viele Kinder jeder Baumknoten hat,
4.2 Strassens Algorithmus zur Matrizenmultiplikation
81
was wiederum angibt, wie viele Terme zur Summe einer jeden Baumebene beitragen. Würden wir den Faktor von 8 in Gleichung (4.16) ignorieren, wäre der Rekursionsbaum nur linear anstatt „buschig“ und jede Ebene würde nur einen Term zur Summe beitragen. Sie sollten sich aus diesem Grund merken, dass, wenngleich asymptotische Notationen konstante multiplikative Faktoren subsumieren, rekursive Notationen, wie z. B. T (n/2), dies nicht tun.
Strassens Methode Der entscheidende Punkt bei Strassens Methode ist, dass der Rekursionsbaum weniger buschig ist. Genauer, anstatt acht rekursive Multiplikationen von n/2 × n/2-Matrizen auszuführen, führt Strassens Methode nur sieben aus. Den Preis, den wir für das Eliminieren einer Matrizenmultiplikation bezahlen müssen, besteht darin, dass wir zusätzliche Additionen von n/2 × n/2-Matrizen auszuführen haben, aber weiterhin immer noch nur konstant viele. Wie vorhin wird die konstante Anzahl von Matrizenadditionen durch die Θ-Notation subsumiert, wenn wir die Rekursionsgleichung zur Beschreibung der Laufzeit aufstellen. Strassens Methode ist keinesfalls naheliegend. (Das könnte die größte Untertreibung in diesem Buch sein.) Die Methode besteht aus vier Schritten: 1. Teilen Sie die Eingabematrizen A und B sowie die Ausgabematrix C wie in Gleichung (4.9) angegeben in n/2 × n/2-Teilmatrizen. Dieser Schritt kann in Θ(1) Zeit ausgeführt werden, wenn wie in Square-Matrix-Multiply-Recursive mit Indexberechnung gearbeitet wird, 2. Konstruieren Sie 10 Matrizen S1 , S2 , . . . , S10 , die jeweils die Größe n/2×n/2 haben und die Summe oder die Differenz von zwei Matrizen, die in Schritt 1 konstruiert wurden, darstellen. Wir können diese 10 Matrizen in Θ(n2 ) Zeit konstruieren. 3. Berechnen Sie rekursiv sieben Matrizenprodukte P1 , P2 , . . . , P7 unter Zuhilfenahme der Teilmatrizen, die in Schritt 1 generiert worden sind, und der 10 Matrizen, die in Schritt 2 berechnet worden sind. Jede Matrix Pi ist eine n/2 × n/2-Matrix. 4. Berechnen Sie die gewünschten Teilmatrizen C11 , C12 , C21 , C22 der Ergebnismatrix C durch Addieren und Subtrahieren geeigneter Kombinationen der Pi Matrizen. Wir können alle vier Teilmatrizen in Θ(n2 ) Zeit berechnen. Wir werden uns die Details zu den Schritten 2–4 gleich anschauen, aber wir haben bereits jetzt ausreichend Informationen, um die Rekursionsgleichung für die Laufzeit von Strassens Methode aufzustellen. Wir nehmen an, dass wir, wie bereits in Zeile 4 von Square-Matrix-Multiply-Recursive, eine einfache skalare Multiplikation auszuführen haben, wenn die Matrizengröße n gleich 1 geworden ist. Im Fall n > 1 benötigen die Schritte 1, 2 und 4 zusammen Θ(n2 ) Zeit. Schritt 3 verlangt von uns, sieben Multiplikationen von n/2 × n/2-Matrizen auszuführen. Wir erhalten demnach die folgende
82
4 Teile-und-Beherrsche
Rekursionsgleichung für die Laufzeit T (n) von Strassens Algorithmus: T (n) =
Θ(1) 7 T (n/2) + Θ(n2 )
falls n = 1 , falls n > 1 .
(4.18)
Wir haben eine Matrizenmultiplikation gegen eine konstante Anzahl von Matrizenadditionen eingetauscht. Sobald wir Rekursionsgleichungen und ihre Auflösungen verstanden haben, werden wir sehen, dass dieser Handel tatsächlich zu einer kleineren asymptotischen Laufzeit führt. Mit Hilfe der Mastermethode aus Abschnitt 4.5 können wir folgern, dass die Rekursionsgleichung (4.18) die Lösung T (n) = Θ(nlg 7 ) besitzt. Wir kommen nun zu den Details der Methode. In Schritt 1 konstruieren wir die folgenden 10 Matrizen: S1 S2 S3 S4 S5 S6 S7 S8 S9 S10
= = = = = = = = = =
B12 − B22 A11 + A12 A21 + A22 B21 − B11 A11 + A22 B11 + B22 A12 − A22 B21 + B22 A11 − A21 B11 + B12
, , , , , , , , , .
Da wir zehnmal jeweils zwei n/2 × n/2-Matrizen addieren oder subtrahieren müssen, benötigt dieser Schritt in der Tat Θ(n2 ) Zeit. In Schritt 3 multiplizieren wir siebenmal rekursiv n/2 × n/2-Matrizen, um die folgenden n/2 × n/2-Matrizen zu berechnen. Jede dieser Matrizen lässt sich auch als Summe oder Differenz von Produkten von Teilmatrizen von A und B schreiben: P1 P2 P3 P4 P5 P6 P7
= = = = = = =
A11 · S1 S2 · B22 S3 · B11 A22 · S4 S5 · S6 S7 · S8 S9 · S10
= = = = = = =
A11 · B12 − A11 · B22 , A11 · B22 + A12 · B22 , A21 · B11 + A22 · B11 , A22 · B21 − A22 · B11 , A11 · B11 + A11 · B22 + A22 · B11 + A22 · B22 , A12 · B21 + A12 · B22 − A22 · B21 − A22 · B22 , A11 · B11 + A11 · B12 − A21 · B11 − A21 · B12 .
Nehmen Sie bitte zur Kenntnis, dass die einzigen Multiplikationen, die wir auszuführen haben, die sind, die in der mittleren Spalte der obigen Gleichungen stehen. Die rechte Spalte gibt lediglich an, wie diese Produkte von den ursprünglichen, in Schritt 1 konstruierten Teilmatrizen abhängen. Schritt 4 addiert und subtrahiert die Pi Matrizen, die in Schritt 3 erzeugt worden sind, um die vier n/2 × n/2 Teilmatrizen des Produktes C zu erhalten. Wir beginnen mit C11 = P5 + P4 − P2 + P6 .
4.2 Strassens Algorithmus zur Matrizenmultiplikation
83
Expandiert man die rechte Seite dieser Gleichung – für eine bessere Übersicht ordnen wir jedem Pi eine eigene Zeile zu und schreiben die Terme, die sich gegenseitig herauskürzen, übereinander –, so sehen wir, dass sich C11 zu A11 · B11 +A11 · B22 +A22 · B11 +A22 · B22 −A22 · B11 +A22 · B21 −A11 · B22 −A12 · B22 −A22 · B22 −A22 · B21 +A12 · B22 +A12 · B21 A11 · B11
+A12 · B21
ergibt, was der Gleichung (4.11) entspricht. Gleichermaßen setzen wir C12 = P1 + P2 und erhalten für C12 A11 · B12 −A11 · B22 +A11 · B22 +A12 · B22 A11 · B12
+A12 · B22 .
Dies entspricht Gleichung (4.12). Die Zuweisung C21 = P3 + P4 ergibt für C21 A21 · B11 +A22 · B11 −A22 · B11 +A22 · B21 A21 · B11
+A22 · B21 ,
was der Gleichung (4.13) entspricht. Schlussendlich setzen wir C22 = P5 + P1 − P3 − P7 , sodass C22 gleich A11 · B11 +A11 · B22 +A22 · B11 +A22 · B22 −A11 · B22 +A11 · B12 −A22 · B11 −A21 · B11 −A11 · B11 −A11 · B12 +A21 · B11 +A21 · B12 A22 · B22
+A21 · B12
84
4 Teile-und-Beherrsche
ist, was wiederum Gleichung (4.14) entspricht. Insgesamt haben wir in Schritt 4 achtmal n/2 × n/2-Matrizen addiert oder subtrahiert. Somit benötigt dieser Schritt Θ(n2 ) Zeit. Wir sehen also, dass Strassens Algorithmus, der aus den Schritten 1–4 besteht, das korrekte Matrizenprodukt berechnet und dass die Rekursionsgleichung (4.18) seine Laufzeit beschreibt. Da wir in Abschnitt 4.5 sehen werden, dass diese Rekursionsgleichung die Lösung T (n) = Θ(nlg 7 ) besitzt, ist Strassens Methode asymptotisch schneller als die einfache Square-Matrix-Multiply Prozedur. Die Bemerkungen am Ende dieses Kapitels greifen noch verschiedene praktische Aspekte von Strassens Algorithmus auf.
Übungen Bemerkung: Auch wenn die Übungen 4.2-3, 4.2-4 und 4.2-5 Varianten von Strassens Algorithmus behandeln, sollten Sie Abschnitt 4.5 lesen, bevor Sie versuchen, diese Aufgaben zu lösen. 4.2-1 Berechnen Sie das Matrizenprodukt 1 3 6 8 7 5 4 2 mit Hilfe von Strassens Algorihmus. 4.2-2 Geben Sie den Pseudocode für Strassens Algorithmus an. 4.2-3 Wie würden Sie Strassens Algorithmus modifizieren, um n × n-Matrizen, bei denen n keine Potenz von 2 ist, miteinander zu multiplizieren? Zeigen Sie, dass der resultierende Algorithmus in Zeit Θ(nlg 7 ) läuft. 4.2-4 Welcher ist der größte Wert k, für den Folgendes gilt: Wenn Sie 3 × 3-Matrizen unter Verwendung von k Multiplikationen (ohne die Kommutativität der Multiplikation anzunehmen) miteinander multiplizieren können, dann können Sie n × n-Matrizen in Zeit o(nlg 7 ) miteinander multiplizieren. Wie groß wäre die Laufzeit dieses Algorithmus? 4.2-5 V. Pan hat eine Möglichkeit gefunden, 68 × 68-Matrizen unter Verwendung von 132.464 Multiplikationen miteinander zu multiplizieren, eine Möglichkeit 70 × 70-Matrizen unter Verwendung von 143.640 Multiplikationen miteinander zu multiplizieren und eine Möglichkeit 72 × 72-Matrizen unter Verwendung von 155.424 Multiplikationen miteinander zu multiplizieren. Welche Methode liefert die beste asymptotische Laufzeit, wenn diese innerhalb eines Teile-undBeherrsche-Algorithmus zur Matrizenmultiplikation verwendet wird? Wie ist sie im Vergleich zur Laufzeit von Strassens Algorithmus? 4.2-6 Wie schnell können Sie eine k n × n-Matrix mit einer n × k n-Matrix multiplizieren, wenn Sie Strassens Algorithmus als Unterprogramm verwenden? Beantworten Sie die gleiche Frage, wenn die Reihenfolge der Eingabematrizen vertauscht ist.
4.3 Die Substitutionsmethode zum Lösen von Rekursionsgleichungen
85
4.2-7 Zeigen Sie, wie man komplexe Zahlen a + b i and c + d i mit nur drei Multiplikationen reeller Zahlen multiplizieren kann. Der Algorithmus sollte a, b, c und d als Eingabe bekommen und den Realteil ac − bd sowie den Imaginärteil ad + bc getrennt ausgeben.
4.3
Die Substitutionsmethode zum Lösen von Rekursionsgleichungen
Jetzt, wo wir gesehen haben, wie Rekursionsgleichungen die Laufzeit von Teile-undBeherrsche-Algorithmen beschreiben können, werden wir lernen wie man Rekursionsgleichungen lösen kann. Wir beginnen in diesem Abschnitt mit der Substitutionsmethode. Die Substitutionsmethode zur Lösung von Rekursionsgleichungen besteht aus zwei Schritten: 1. Erraten Sie die Form der Lösung. 2. Verwenden Sie mathematische Induktion, um die Konstanten zu finden und zu zeigen, dass die Lösung okay ist. Wir ersetzen die Funktion durch die erratene Lösung, wenn wir die Induktionsannahme auf kleinere Werte anwenden; aus diesem Grunde der Name „Substitutionsmethode“. Diese Methode ist mächtig, wir müssen aber fähig sein, die Form der Lösung zu erraten, um sie anwenden zu können. Wir können die Substitutionsmethode anwenden, um entweder obere oder untere Schranken für Rekursionsgleichungen aufzustellen. Als Beispiel wollen wir eine obere Schranke für die Rekursionsgleichung T (n) = 2 T ( n/2) + n
(4.19)
beweisen, die den Rekursionsgleichungen (4.3) und (4.4) ähnelt. Wir vermuten, dass die Lösung T (n) = O(n lg n) ist. Die Substitutionsmethode verlangt von uns, zu beweisen, dass für eine geeignete Wahl der Konstanten c > 0 die Gleichung T (n) ≤ c n lg n gilt. Zuerst nehmen wir an, dass diese Schranke für alle positiven m < n, im Besonderen für m = n/2 gilt. Mit dieser Annahme erhalten wir T ( n/2) ≤ c n/2 lg( n/2). Das Einsetzen in die Rekursionsgleichung führt zu T (n) ≤ 2(c n/2 lg( n/2)) + n ≤ c n lg(n/2) + n = c n lg n − c n lg 2 + n = c n lg n − c n + n ≤ c n lg n ,
86
4 Teile-und-Beherrsche
wobei der letzte Schritt richtig ist, solange c ≥ 1 gilt. Mathematische Induktion verlangt von uns, zu zeigen, dass unsere Lösung die Randbedingungen erfüllt. Üblicherweise machen wir dies, indem wir zeigen, dass die Randbedingungen als Induktionsanfang für den induktiven Beweis geeignet sind. Im Falle der Rekursionsgleichung (4.19) müssen wir zeigen, dass wir die Konstante c hinreichend groß wählen können, sodass die Schranke T (n) ≤ c n lg n auch für die Randbedingungen gilt. Diese Forderung kann manchmal zu Problemen führen. Nehmen wir für unsere Argumentation an, dass T (1) = 1 die einzige Randbedingung der Rekursionsgleichung ist. Dann führt n = 1 in der Schranke T (n) ≤ c n lg n zu T (1) ≤ c 1 lg 1 = 0, was im Widerspruch zu T (1) = 1 steht. Somit ist der Induktionsanfang unseres Induktionsbeweises nicht erfüllt. Wir können diese Schwierigkeit beim Beweis der Induktionsvermutung für eine spezielle Randbedingung leicht in den Griff bekommen. In der Rekursionsgleichung (4.19) zum Beispiel können wir ausnutzen, dass asymptotische Notation von uns nur verlangt, die Ungleichung T (n) ≤ c n lg n für n ≥ n0 zu beweisen, wobei n0 eine Konstante ist, die wir wählen dürfen. Wir behalten die lästige Randbedingung T (1) = 1 bei, betrachten sie aber nicht in dem Induktionsbeweis. Wir gehen so vor, dass wir zuerst mal bemerken, dass die Rekursionsgleichung für n > 3 nicht direkt von T (1) abhängt. Somit nehmen wir T (2) und T (3) statt T (1) als die Basisfälle des Induktionsbeweises, und setzen n0 = 2. Beachten Sie, dass wir einen Unterschied zwischen dem Basisfall der Rekursionsgleichung (n = 1) und dem Induktionsanfang im Induktionsbeweis (n = 2 und n = 3) machen. Mit T (1) = 1 erhalten wir aus der Rekursionsgleichung T (2) = 4 und T (3) = 5. Wir können nun den Induktionsbeweis, dass T (n) ≤ c n lg n für eine Konstante c ≥ 1 gilt, vervollständigen, indem wir c hinreichend groß wählen, sodass T (2) ≤ c 2 lg 2 und T (3) ≤ c 3 lg 3 gelten. Es stellt sich heraus, dass eine beliebige Konstante c ≥ 2 gewählt werden kann, damit die Induktionsvermutung für die Basisfälle n = 2 und n = 3 erfüllt ist. Für die meisten Rekursionsgleichungen, die wir untersuchen werden, ist es einfach, die Randbedingungen so auszubauen, dass die Induktionsannahme für kleine n zutrifft. Wir werden nicht jedesmal die entsprechenden Details ausarbeiten.
Gut raten Unglücklicherweise gibt es keinen allgemeingültigen Weg, um die korrekten Lösungen von Rekursionsgleichungen zu erraten. Dies setzt Erfahrung und gelegentlich Kreativität voraus. Glücklicherweise können Sie einige Heuristiken benutzen, die Ihnen helfen, gut zu raten. Sie können zum Generieren guter Vermutungen auch Rekursionsbäume verwenden, die wir in Abschnitt 4.4 vorstellen werden. Wenn eine Rekursionsgleichung einer anderen ähnlich ist, die Sie bereits kennen gelernt haben, dann ist es naheliegend, eine ähnliche Lösung zu vermuten. Wir betrachten beispielsweise die Rekursionsgleichung T (n) = 2 T ( n/2 + 17) + n , die wegen der auf der rechten Seite im Argument von T hinzugefügten „17“ schwierig erscheint. Die Intuition sagt uns jedoch, dass dieser zusätzliche Term keinen substanziellen Effekt auf die Lösung der Rekursionsgleichung ausüben kann. Wenn n groß ist,
4.3 Die Substitutionsmethode zum Lösen von Rekursionsgleichungen
87
dann ist der Unterschied zwischen n/2 und n/2+17 nicht allzu groß; beide halbieren n in etwa. Folglich vermuten wir, dass T (n) = O(n lg n) gilt, was Sie mit Hilfe der Substitutionsmethode als korrekt verifizieren können (siehe Übung 4.3-6). Eine andere Möglichkeit, gute Vermutungen aufzustellen, besteht darin, grobe obere und untere Schranken für die Rekursionsgleichung zu beweisen und damit den Bereich der Unsicherheit einzuschränken. Wir könnten zum Beispiel im Fall der Rekursionsgleichung (4.19) mit der unteren Schranke T (n) = Ω(n) beginnen, da der Term n in der Rekursionsgleichung auftaucht. Und wir können zeigen, dass T (n) = O(n2 ) eine erste obere Schranke ist. Dann senken wir allmählich die obere Schranke und erhöhen die untere Schranke, bis das Verfahren zur korrekten, asymptotischen Lösung von T (n) = Θ(n lg n) konvergiert.
Feinheiten Manchmal werden Sie möglicherweise zwar die asymptotische Schranke einer Rekursionsgleichung korrekt erraten haben, aber irgendwie scheitert die Mathematik daran, die Schranke im Rahmen einer Induktion zu beweisen. Gewöhnlich besteht das Problem darin, dass die Induktionsannahme nicht stark genug ist, um die genaue Schranke zu beweisen. Wenn Sie in einem solchen Fall Ihre Vermutung verändern, indem Sie einen Term niedrigerer Ordnung subtrahieren, dann führt dies oft zum Erfolg. Betrachten Sie die Rekursionsgleichung T (n) = T ( n/2) + T (n/2 ) + 1 . Wir vermuten, dass die Lösung T (n) = O(n) ist und versuchen zu zeigen, dass für eine geeignete Wahl der Konstanten c die Ungleichung T (n) ≤ c n erfüllt ist. Setzen wir unsere Vermutung in die Rekursionsgleichung ein, dann erhalten wir T (n) ≤ c n/2 + c n/2 + 1 = cn+ 1 anstelle der gewünschten Ungleichung T (n) ≤ c n. Möglicherweise sind wir versucht, eine größere Schätzung auszuprobieren, sagen wir T (n) = O(n2 ). Wenngleich wir diese erratene größere Schranke für unsere Rekursionsgleichung beweisen können, ist unsere ursprüngliche Vermutung T (n) = O(n) korrekt. Um zu beweisen, dass sie korrekt ist, müssen wir eine stärkere Induktionsannahme wählen. Unsere Vermutung ist intuitiv fast richtig: Uns fehlt nur eine Konstante 1, ein Term niedrigerer Ordnung. Trotzdem arbeitet die mathematische Induktion nicht, es sei denn wir beweisen die exakte Form der Induktionsannahme. Wir bewältigen unsere Schwierigkeiten, indem wir einen Term niedrigerer Ordnung von unserer vorherigen Vermutung subtrahieren. Unsere neue Vermutung lautet T (n) ≤ c n − d, wobei d ≥ 0 konstant ist. Wir haben nun T (n) ≤ (c n/2 − d) + (c n/2 − d) + 1 = c n − 2d + 1 ≤ cn− d ,
88
4 Teile-und-Beherrsche
sofern d ≥ 1 gilt. Wie vorhin muss die Konstante c hinreichend groß gewählt werden, um die Randbedingungen in den Griff zu bekommen. Möglicherweise finden Sie die Idee, einen Term niedriger Ordnung zu subtrahieren, nicht eingängig. Sollten wir nicht besser unsere Schätzung erhöhen, wenn die Mathematik unsere Vermutung nicht beweisen kann? Nicht unbedingt! Es kann tatsächlich bei einem Induktionsbeweis einer oberen Schranke sein, dass es schwieriger ist, eine schwächere Schranke zu beweisen, da um die schwächere Schranke zu beweisen, wir die gleiche schwächere Schranke induktiv im Beweis verwenden müssen. In unserem aktuellen Beispiel haben wir den Term niedriger Ordnung für jeden rekursiven Term jeweils einmal abzuziehen. Im obigen Beispiel subtrahieren wir die Konstante d zweimal, einmal für den T ( n/2)-Term und einmal für den T (n/2 )-Term. Somit kamen wir zu der Ungleichung T (n) ≤ c n − 2d + 1 und es war dann einfach, Werte für d zu finden, sodass c n − 2d + 1 kleiner oder gleich c n − d ist.
Fallen vermeiden Es ist leicht, sich bei der Verwendung der asymptotischen Notation zu vertun. Im Fall der Rekursionsgleichung (4.19) können wir zum Beispiel fälschlicherweise die Aussage T (n) = O(n) „beweisen“, indem wir vermuten, dass T (n) ≤ c n gilt und anschließend wie folgt argumentieren: T (n) ≤ 2(c n/2) + n ≤ cn+ n = O(n) , ⇐= falsch!! da c eine Konstante ist. Der Fehler besteht darin, dass wir nicht die exakte Form T (n) ≤ c n der Induktionsannahme bewiesen haben. Wir werden aus diesem Grund explizit beweisen, dass T (n) ≤ c n gilt, wenn wir T (n) = O(n) zeigen wollen.
Variablen transformieren Manchmal können Sie eine Ihnen unbekannte Rekursionsgleichung durch eine kleine algebraische Umformung auf eine Ihnen bereits bekannte Gleichung zurückführen. Als Beispiel betrachten wir die Rekursionsgleichung √ T (n) = 2 T ( n ) + lg n , die schwierig aussieht. Wir können diese Rekursionsgleichung jedoch durch eine Variablentransformation vereinfachen. Der Bequemlichkeit halber werden √ wir uns jetzt nicht um die Rundung der Werte kümmern, zum Beispiel darum, ob n ganzzahlig ist. Definieren wir m als lg n und ersetzen wir n entsprechend, dann erhalten wir T (2m ) = 2 T (2m/2 ) + m . Nun können wir die Umbenennung S(m) = T (2m ) vornehmen, um die neue Rekursionsgleichung S(m) = 2S(m/2) + m
4.4 Die Rekursionsbaum-Methode zum Lösen von Rekursionsgleichungen
89
zu erhalten, die sehr ähnlich zur Rekursionsgleichung (4.19) ist. Diese Rekursionsgleichung hat in der Tat die gleiche Lösung: S(m) = O(m lg m). Transformieren wir S(m) zu T (n) zurück, erhalten wir T (n) = T (2m ) = S(m) = O(m lg m) = O(lg n lg lg n).
Übungen 4.3-1 Zeigen Sie, dass die Lösung von T (n) = T (n − 1) + n in O(n2 ) liegt. 4.3-2 Zeigen Sie, dass die Lösung von T (n) = T (n/2 ) + 1 in O(lg n) liegt. 4.3-3 Wir haben gesehen, dass die Lösung von T (n) = 2 T ( n/2) + n in O(n lg n) liegt. Zeigen Sie, dass die Lösung dieser Rekursionsgleichung auch in Ω(n lg n) liegt. Schlussfolgern Sie, dass die Lösung dann auch in Θ(n lg n) liegt. 4.3-4 Zeigen Sie, dass wir bei der Rekursionsgleichung (4.19) durch das Aufstellen einer anderen Induktionsannahme die Schwierigkeiten mit der Randbedingung T (1) = 1 umgehen können, ohne die Randbedingungen für den Induktionsbeweis anpassen zu müssen. 4.3-5 Zeigen Sie, dass Θ(n lg n) die Lösung für die „exakte“ Rekursionsgleichung (4.3) von Sortieren durch Mischen ist. 4.3-6 Zeigen Sie, dass die Lösung von T (n) = 2 T ( n/2 + 17) + n in O(n lg n) liegt. 4.3-7 Mit Hilfe der Mastermethode aus Abschnitt 4.5 können Sie zeigen, dass die Lösung der Rekursionsgleichung T (n) = 4 T (n/3) + n gleich T (n) = Θ(nlog3 4 ) ist. Zeigen Sie, dass ein Substitutionsbeweis mit der Vermutung T (n) ≤ c nlog3 4 scheitert. Zeigen Sie dann, wie Sie durch Subtraktion eines Terms niedriger Ordnung einen Substitutionsbeweis hinbekommen. 4.3-8 Mit Hilfe der Mastermethode aus Abschnitt 4.5 können Sie zeigen, dass die Lösung der Rekursionsgleichung T (n) = 4 T (n/2) + n gleich T (n) = Θ(n2 ) ist. Zeigen Sie, dass ein Substitutionsbeweis mit der Vermutung T (n) ≤ c n2 scheitert. Zeigen Sie dann, wie Sie durch Subtraktion eines Terms niedriger Ordnung einen Substitutionsbeweis hinbekommen. √ 4.3-9 Lösen Sie die Rekursionsgleichung T (n) = 3 T ( n) + log n, indem Sie die Variablen geeignet transformieren. Ihre Lösung sollte asymptotisch scharf sein. (Hinweis: Machen Sie sich keine Sorgen, wenn Ihre Werte Integralwerte sind.)
4.4
Die Rekursionsbaum-Methode zum Lösen von Rekursionsgleichungen
Wenngleich Sie die Substitutionsmethode benutzen können, um einen prägnanten Beweis zu bekommen, dass eine Lösung in Bezug auf eine Rekursionsgleichung korrekt ist, könnten Sie Probleme haben, eine gute Vermutung aufzustellen. Das Zeichnen eines Rekursionsbaumes, wie wir es bei unserer Analyse von Sortieren durch Mischen in
90
4 Teile-und-Beherrsche
Abschnitt 2.3.2 getan haben, ist ein direkter Weg, eine gute Vermutung zu erhalten. In einem Rekursionsbaum stellt jeder Knoten die Kosten eines Teilproblems dar, das sich irgendwo in der Menge der rekursiven Funktionsaufrufe befindet. Wir addieren die Kosten innerhalb jeder Ebene des Baumes. Anschließend summieren wir über die Kosten der Ebenen und bestimmen so die Gesamtkosten der Rekursion. Den meisten Nutzen eines Rekursionsbaumes haben Sie, wenn Sie ihn zum Aufstellen einer guten Vermutung benutzen, die dann mithilfe der Substitutionsmethode überprüft werden kann. Wenn Sie einen Rekursionsbaum zum Aufstellen einer guten Vermutung verwenden, dann können Sie sich häufig ein gewisses Maß an „Unschärfe“ erlauben, da Sie ja Ihre Vermutung später noch verifizieren werden. Wenn Sie beim Entwerfen des Rekursionsbaumes und der Summierung der Kosten sehr sorgsam vorgehen, dann können Sie einen Rekursionsbaum auch als einen direkten Beweis einer Lösung einer Rekursionsgleichung verwenden. In diesem Abschnitt werden wir einen Rekursionsbaum zum Aufstellen einer guten Vermutung verwenden. In Abschnitt 4.6 werden wir den Rekursionsbaum direkt zum Beweis des Theorems benutzen, das die Basis für die Mastermethode bildet. Sehen wir uns an, wie uns ein Rekursionsbaum eine gute Vermutung zum Beispiel für die Rekursionsgleichung T (n) = 3 T ( n/4) + Θ(n2 ) liefert. Wir beginnen damit, uns auf das Auffinden einer oberen Schranke für die Lösung zu konzentrieren. Da wir wissen, dass das Aufrunden und das Abrunden beim Lösen von Rekursionsgleichungen in der Regel unerheblich sind (das ist ein Beispiel für die Unschärfe, die wir tolerieren können), entwerfen wir einen Rekursionsbaum für die Rekursionsgleichung T (n) = 3 T (n/4) + c n2 , wobei wir den implizit enthaltenen Koeffizienten c > 0 ausgeschrieben haben. Abbildung 4.5 zeigt, wie wir den Rekursionsbaum für T (n) = 3 T (n/4) + c n2 ableiten. Der Einfachheit halber nehmen wir an, dass n eine Potenz von 4 ist (ein anderes Beispiel für die tolerierbare Unschärfe), sodass n/4 für alle n ≥ 4 eine ganze Zahl ist. Teil (a) der Abbildung zeigt T (n), den wir in Teil (b) zu einem äquivalenten Baum, der die Rekursionsgleichung darstellt, expandieren. Der Term c n2 in der Wurzel repräsentiert die „direkten Kosten“ von T (n), d. h. die Kosten ohne die Kosten des rekursiven Abstiegs. Die drei von der Wurzel ausgehenden Teilbäume repräsentieren die aus den Teilproblemen der Größe n/4 hinzuzuziehenden Kosten. Teil (c) zeigt diesen Prozess expandiert um einen weiteren Schritt, indem jeder Knoten mit den Kosten T (n/4) aus Teil (b) aufgefächert wird. Die Kosten jeder der drei Kinder der Wurzel sind c(n/4)2 . Wir setzen die Expansion der Knoten fort, indem wir jeden Knoten in die durch die Rekursionsgleichung vorgegebenen Bestandteile zerlegen. Da sich die Größen der Teilprobleme jedesmal um einen Faktor von 4 verringern, wenn wir um eine weitere Ebene absteigen, müssen wir letztendlich an einer Randbedingung ankommen. In welcher Entfernung von der Wurzel erreichen wir diese? Die Größe des Teilproblems für einen Knoten der Tiefe i ist n/4i . Somit erreichen wir Teilprobleme der Größe n = 1, wenn n/4i = 1 ist, d. h. wenn i = log4 n gilt. Also hat der Baum log4 n + 1 Ebenen (der Tiefen 0, 1, 2, . . . , log4 n).
4.4 Die Rekursionsbaum-Methode zum Lösen von Rekursionsgleichungen
cn2
T (n)
T
n 4
T
n 4
cn2
T
n 4
T (a)
91
n 16
c
n 2
T
n
4
16
T
n 16
T
n 16
c
n 2
T
n
(b)
4
16
T
n 16
T
n 16
c
n 2
T
n
4
16
T
log4 n
cn2
n 2
c
4
n 2 n 2 n 2 c 16 c 16 16
c
n 2
c
4
n 2 n 2 n 2 c 16 c 16 16
c
n 2 4
n 2 n 2 n 2 c 16 c 16 16
3 16
cn2
3 2 16
cn2
…
c
16
(c)
cn2
c
n
T (1) T (1) T (1) T (1) T (1) T (1) T (1) T (1) T (1) T (1)
…
T (1) T (1) T (1)
Θ(nlog4 3 )
nlog4 3 (d)
Insgesamt: O(n2 )
Abbildung 4.5: Konstruieren eines Rekursionsbaumes für die Rekursionsgleichung T (n) = 3 T (n/4) + c n2 . Teil (a) zeigt T (n), der in (b)-(d) schrittweise zum Rekursionsbaum aufgefächert wird. Der voll expandierte Baum, der in Teil (d) zu sehen ist, besitzt die Höhe log 4 n (er verfügt über log4 n + 1 Ebenen).
92
4 Teile-und-Beherrsche
Als nächstes bestimmen wir die Kosten in jeder Ebene des Baumes. Jede Ebene hat dreimal so viele Knoten als die jeweils darüberliegende. Die Anzahl der Knoten in der Tiefe i beträgt somit 3i . Da sich die Größe der Teilprobleme ausgehend von der Wurzel in jeder Ebene um den Faktor 4 reduziert, hat jeder Knoten der Tiefe i, für i = 0, 1, 2, . . . , log4 n−1 die Kosten c (n/4i )2 . Multiplizieren wir dies aus, sehen wir, dass sich für die Gesamtkosten aller Knoten einer Ebene i mit i = 0, 1, 2, . . . , log4 n − 1 der Wert 3i c (n/4i )2 = (3/16)i c n2 ergibt. Die untere Ebene, d. h. die Ebene der Tiefe log4 n, besitzt 3log4 n = nlog4 3 Knoten, von denen jeder die Kosten T (1) zu den Gesamtkosten beiträgt. Daraus ergeben sich für die unterste Ebene Gesamtkosten von nlog4 3 T (1), was in Θ(nlog4 3 ) liegt, da wir annehmen, dass T (1) eine Konstante ist. Nun addieren wir die Kosten aller Ebenen, um die Kosten des gesamten Baumes zu bestimmen: 2 log4 n−1 3 3 3 2 2 2 T (n) = c n + cn + cn + ··· + c n2 + Θ(nlog4 3 ) 16 16 16 i log4 n−1 3 = c n2 + Θ(nlog4 3 ) 16 i=0 =
(3/16)log4 n − 1 2 c n + Θ(nlog4 3 ) (3/16) − 1
(wegen Gleichung (A.5)) .
Diese letzte Formel erscheint etwas kompliziert. Allerdings stellen wir fest, dass wir wiederum Nutzen aus einer unscharfen Betrachtungsweise ziehen können, indem wir als obere Schranke eine unendliche, fallende geometrische Reihe verwenden. Gehen wir eine Zeile zurück und wenden wir Gleichung (A.6) an, erhalten wir i log4 n−1 3 T (n) = c n2 + Θ(nlog4 3 ) 16 i=0 i ∞ 3 c n2 + Θ(nlog4 3 ) < 16 i=0 1 c n2 + Θ(nlog4 3 ) 1 − (3/16) 16 2 = c n + Θ(nlog4 3 ) 13 = O(n2 ) .
=
Wir haben für unsere ursprüngliche Rekursionsgleichung T (n) =3 T ( n/4) + Θ(n2 ) somit die Vermutung T (n) = O(n2 ) abgeleitet. In diesem Beispiel bilden die Koeffizienten von c n2 eine fallende geometrische Reihe. Aus Gleichung (A.6) folgt, dass die Summe dieser Koeffizienten von oben durch die Konstante 16/13 beschränkt ist. Da der Beitrag der Wurzel an den Gesamtkosten c n2 beträgt, trägt die Wurzel einen konstanten Bruchteil zu den Gesamtkosten bei. Mit anderen Worten, die Kosten der Wurzel dominieren die Gesamtkosten. Wenn O(n2 ) tatsächlich eine obere Schranke der Rekursionsgleichung ist (was wir gleich zeigen werden), dann muss sie eine scharfe Schranke sein. Warum? Der erste rekursive
4.4 Die Rekursionsbaum-Methode zum Lösen von Rekursionsgleichungen cn
c
93
cn
n
c
2n
2n
c
3
cn
3
log3/2 n c
n 9
c
2n
c
9
9
4n 9
cn
…
…
Insgesamt: O(n lg n) Abbildung 4.6: Rekursionsbaum für die Rekursionsgleichung T (n) = T (n/3) + T (2n/3) + c n.
Aufruf steuert die Kosten Θ(n2 ) bei, und somit muss Ω(n2 ) eine untere Schranke für die Rekursionsgleichung sein. Nun können wir die Substitutionsmethode benutzen, um die Korrektheit unserer Vermutung zu verifizieren, nämlich dass T (n) = O(n2 ) eine obere Schranke der Rekursionsgleichung T (n) = 3 T ( n/4) + Θ(n2 ) ist. Wir wollen zeigen, dass für eine Konstante d > 0 die Beziehung T (n) ≤ d n2 gilt. Verwenden wir dieselbe Konstante c > 0 wie vorhin, dann erhalten wir T (n) ≤ 3 T ( n/4) + c n2 2
≤ 3 d n/4 + c n2 ≤ 3 d(n/4)2 + cn2 3 d n2 + c n2 = 16 ≤ d n2 , wobei der letzte Schritt korrekt ist, falls d ≥ (16/13)c gilt. Als weiteres, komplizierteres Beispiel zeigt Abbildung 4.6 einen Rekursionsbaum für T (n) = T (n/3) + T (2n/3) + O(n) . (Wiederum sparen wir Auf- und Abrunden von Werten der Einfachheit halber aus.) Wie zuvor bezeichnen wir den konstanten Faktor in dem O(n)-Term mit c. Addieren wir die Werte in den Ebenen des in der Abbildung gezeigten Rekursionsbaumes, dann erhalten wir für jede Ebene den Betrag c n. Der längste einfache Pfad von der Wurzel zu einem Blatt ist n → (2/3) n → (2/3)2 n → · · · → 1. Da (2/3)k n = 1 ist, wenn k = log3/2 n gilt, ist die Höhe des Baumes log3/2 n. Intuitiv erwarten wir, dass die Lösung der Rekursionsgleichung sich aus der Anzahl der Ebenen gewichtet mit den jeweiligen Kosten der Ebenen ergibt, also in O(c n log3/2 n),
94
4 Teile-und-Beherrsche
d. h. in O(n lg n), liegt. Abbildung 4.6 zeigt nur die oberen Ebenen des Rekursionsbaums, nicht jede Ebene des Baumes trägt jedoch Kosten von c n bei. Betrachten Sie die Kosten der Blätter. Wäre dieser Rekursionsbaum ein vollständiger binärer Baum der Höhe log3/2 n, dann gäbe es 2log3/2 n = nlog3/2 2 Blätter. Da die Kosten jedes Blattes konstant sind, wären die Gesamtkosten der Blätter in Θ(nlog3/2 2 ), was in ω(n lg n) liegt, da log3/2 2 eine Konstante echt größer 0 ist. Dieser Rekursionsbaum ist aber kein vollständiger binärer Baum und hat weniger als nlog3/2 2 Blätter. Darüber hinaus fehlen immer mehr innere Knoten, je weiter wir uns von der Wurzel entfernen. Folglich liefern nicht alle Ebenen genau die Kosten von c n; die unteren Ebenen tragen weniger zu den Gesamtkosten bei. Wir könnten eine exakte Bilanz der Kosten aufstellen, aber wir sollten uns daran erinnern, dass wir nur versuchen, eine Vermutung aufzustellen, um sie in der Substitutionsmethode zu verwenden. Lassen Sie uns also diese unscharfe Sichtweise tolerieren und versuchen, zu zeigen, dass unsere Vermutung, dass O(n lg n) eine obere Schranke ist, korrekt ist. Tatsächlich können wir die Substitutionsmethode anwenden, um zu verifizieren, dass O(n lg n) eine obere Schranke für die Lösung der Rekursionsgleichung ist. Wir zeigen, dass T (n) ≤ d n lg n für eine geeignete positive Konstante d ist. Es gilt T (n) ≤ T (n/3) + T (2n/3) + c n ≤ d (n/3) lg(n/3) + d (2n/3) lg(2n/3) + c n = (d (n/3) lg n − d (n/3) lg 3) + (d (2n/3) lg n − d (2n/3) lg(3/2)) + c n = d n lg n − d ((n/3) lg 3 + (2n/3) lg(3/2)) + c n = d n lg n − d ((n/3) lg 3 + (2n/3) lg 3 − (2n/3) lg 2) + c n = d n lg n − d n(lg 3 − 2/3) + c n ≤ d n lg n , so lange wie d ≥ c/(lg 3 − (2/3)) ist. Es ist also nicht notwendig, dass wir eine präzisere Berechnung der Kosten im Rekursionsbaum vornehmen.
Übungen 4.4-1 Benutzen Sie einen Rekursionsbaum, um eine gute asymptotisch obere Schranke für die Rekursionsgleichung T (n) = 3 T ( n/2) + n zu bestimmen. Verwenden Sie die Substitutionsmethode, um Ihre Antwort zu verifizieren. 4.4-2 Benutzen Sie einen Rekursionsbaum, um eine gute asymptotisch obere Schranke für die Rekursionsgleichung T (n) = T (n/2) + n2 zu bestimmen. Verwenden Sie die Substitutionsmethode, um Ihre Antwort zu verifizieren. 4.4-3 Benutzen Sie einen Rekursionsbaum, um eine gute asymptotisch obere Schranke für die Rekursionsgleichung T (n) = 4 T (n/2 + 2) + n zu bestimmen. Verwenden Sie die Substitutionsmethode, um Ihre Antwort zu verifizieren.
4.5 Die Mastermethode zum Lösen von Rekursionsgleichungen
95
4.4-4 Benutzen Sie einen Rekursionsbaum, um eine gute asymptotisch obere Schranke für die Rekursionsgleichung T (n) = 2 T (n − 1) + 1 zu bestimmen. Verwenden Sie die Substitutionsmethode, um Ihre Antwort zu verifizieren. 4.4-5 Benutzen Sie einen Rekursionsbaum, um eine gute asymptotisch obere Schranke für die Rekursionsgleichung T (n) = T (n − 1) + T (n/2) + n zu bestimmen. Verwenden Sie die Substitutionsmethode, um Ihre Antwort zu verifizieren. 4.4-6 Zeigen Sie, dass die Lösung der Rekursionsgleichung T (n) = T (n/3)+T (2n/3)+ c n in Ω(n lg n) ist, indem Sie auf das Konzept der Rekursionsbäume zurückgreifen. c steht für eine Konstante. 4.4-7 Entwerfen Sie einen Rekursionsbaum für T (n) = 4 T ( n/2) + c n, wobei c eine Konstante ist und geben Sie eine asymptotisch scharfe Schranke für die Lösung der Rekursionsgleichung an. Verifizieren Sie ihre Schranke mithilfe der Substitutionsmethode. 4.4-8 Verwenden Sie einen Rekursionsbaum, um eine asymptotisch scharfe Lösung für die Rekursionsgleichung T (n) = T (n − a) + T (a) + c n anzugeben, wobei a ≥ 1 und c > 0 Konstanten sind. 4.4-9 Benutzen Sie einen Rekursionsbaum, um eine asymptotisch scharfe Lösung für die Rekursionsgleichung T (n) = T (α n) + T ((1 − α) n) + c n zu bestimmen, wobei α eine Konstante aus dem Bereich 0 < α < 1 ist und c > 0 ebenfalls eine Konstante ist.
4.5
Die Mastermethode zum Lösen von Rekursionsgleichungen
Die Mastermethode stellt ein Art „Kochbuchrezept“ für das Lösen von Rekursionsgleichungen der Form T (n) = a T (n/b) + f (n)
(4.20)
dar, wobei a ≥ 1 und b > 1 Konstanten sind und f (n) eine asymptotisch positive Funktion. Um die Mastermethode anwenden zu können, müssen Sie sich drei Fälle einprägen; dann aber werden Sie in der Lage sein, viele Rekursionsgleichungen mehr oder weniger spielend zu lösen, oft sogar ohne Papier und Bleistift. Die Rekursionsgleichung (4.20) beschreibt die Laufzeit eines Algorithmus, der ein Problem der Größe n in a Teilprobleme zerlegt, jedes mit der Größe n/b, wobei a und b positive Konstanten sind. Die a Teilprobleme werden rekursiv gelöst, jedes in der Zeit T (n/b). Die Funktion f (n) umfasst die Kosten, um das Problem in die Teilprobleme aufzuteilen und um die Lösungen der Teilprobleme zu vereinigen. Bei der Rekursionsgleichung von Strassens Algorithmus beispielsweise ist a = 7, b = 2 und f (n) = Θ(n2 ). Im Hinblick auf technische Korrektheit ist die Rekursionsgleichung nicht einmal wohldefiniert, da n/b möglicherweise keine ganze Zahl ist. Das Ersetzen der a Terme T (n/b)
96
4 Teile-und-Beherrsche
durch entweder T ( n/b) oder T (n/b ) beeinflusst das asymptotische Verhalten der Rekursionsgleichung nicht. (Wir werden diese Aussage im nächsten Abschnitt beweisen.) Aus diesem Grunde lassen wir üblicherweise die Funktionen floor und ceil aus Bequemlichkeit einfach weg, wenn wir Rekursionsgleichungen für Teile-und-BeherrscheAlgorithmen aufschreiben.
Das Mastertheorem Die Mastermethode beruht auf folgendem Theorem. Theorem 4.1: (Mastertheorem) Seien a ≥ 1 und b > 1 Konstanten. Sei f (n) eine Funktion und sei T (n) über den nichtnegativen ganzen Zahlen durch die Rekursionsgleichung T (n) = a T (n/b) + f (n) definiert, wobei wir n/b so interpretieren, dass damit entweder n/b oder n/b gemeint ist. Dann besitzt T (n) die folgenden asymptotischen Schranken: 1. Gilt f (n) = O(nlogb a− ) für eine Konstante > 0, dann gilt T (n) = Θ(nlogb a ). 2. Gilt f (n) = Θ(nlogb a ), dann gilt T (n) = Θ(nlogb a lg n). 3. Gilt f (n) = Ω(nlogb a+ ) für eine Konstante > 0 und a f (n/b) ≤ c f (n) für eine Konstante c < 1 und hinreichend großen n, dann ist T (n) = Θ(f (n)). Bevor wir das Mastertheorem auf einige Beispiele anwenden, verwenden wir einen Moment darauf, die Aussage zu verstehen. In jedem der drei Fälle vergleichen wir die Funktion f (n) mit der Funktion nlogb a . Offenbar bestimmt die größere der beiden Funktionen die Lösung der Rekursionsgleichung. Wenn, wie im Fall 1, die Funktion nlogb a die größere Funktion ist, dann ist die Lösung durch T (n) = Θ(nlogb a ) gegeben. Wenn, wie im Fall 3, die Funktion f (n) die größere Funktion ist, dann ist die Lösung durch T (n) = Θ(f (n)) gegeben. Wenn, wie im Fall 2, die beiden Funktionen gleich schnell wachsen, dann ist die Lösung durch T (n) = Θ(nlogb a lg n) = Θ(f (n) lg n) gegeben. Jenseits der Intuition müssen Sie sich einiger Formalien bewusst sein. Im ersten Fall muss f (n) nicht nur kleiner als nlogb a sein, sondern sogar polynomial kleiner, d. h. f (n) muss für eine Konstante > 0 um den Faktor n asymptotisch kleiner als nlogb a sein. Im dritten Fall muss f (n) nicht nur größer als nlogb a sein, sondern polynomial größer und zusätzlich die „Regularitätsbedingung“ a f (n/b) ≤ c f (n) erfüllen. Diese Bedingung wird von den meisten polynomial beschränkten Funktionen, denen wir begegnen werden, erfüllt. Bemerken Sie bitte, dass die drei Fälle nicht alle Möglichkeiten für f (n) abdecken. Es gibt eine Lücke zwischen Fall 1 und Fall 2, wenn f (n) kleiner als nlogb a ist, aber nicht polynomial kleiner. Analog dazu gibt es eine Lücke zwischen den Fällen 2 und 3, wenn f (n) größer als nlogb a ist, aber nicht polynomial größer. Wenn die Funktion f (n) in eine
4.5 Die Mastermethode zum Lösen von Rekursionsgleichungen
97
dieser Lücken fällt oder die Regularitätsbedingung im Fall 3 nicht erfüllt ist, dann können Sie die Mastermethode nicht anwenden, um die entsprechende Rekursionsgleichung zu lösen.
Die Mastermethode anwenden Um die Mastermethode anzuwenden, bestimmen wir einfach, welcher Fall des Mastertheorems, wenn überhaupt einer, vorliegt und schreiben die Antwort auf. Als erstes Beispiel betrachten Sie T (n) = 9 T (n/3) + n . Für diese Rekursionsgleichung gilt a = 9, b = 3, f (n) = n und somit nlogb a = nlog3 9 = Θ(n2 ). Da f (n) = O(nlog3 9− ) mit = 1 gilt, können wir Fall 1 des Mastertheorems anwenden und schlussfolgern, dass T (n) = Θ(n2 ) gilt. Betrachten Sie nun die Rekursionsgleichung T (n) = T (2n/3) + 1, in der a = 1, b = 3/2, f (n) = 1 und nlogb a = nlog3/2 1 = n0 = 1 gelten. Es kommt Fall 2 zur Anwendung, da f (n) = Θ(nlogb a ) = Θ(1) ist. Somit ist die Lösung der Rekursionsgleichung T (n) = Θ(lg n). Für die Rekursionsgleichung T (n) = 3 T (n/4) + n lg n gilt a = 3, b = 4, f (n) = n lg n und nlogb a = nlog4 3 = O(n0,793 ). Da f (n) = Ω(nlog4 3+ ) mit ≈ 0,2 gilt, kommt Fall 3 zur Anwendung, wenn wir zeigen können, dass die Regularitätsbedingung erfüllt ist. Für hinreichend große n ist a f (n/b) = 3 (n/4) lg(n/4) ≤ (3/4) n lg n = c f (n) für c = 3/4. Folglich ist nach Fall 3 die Lösung der obigen Rekursionsgleichung T (n) = Θ(n lg n). Die Mastermethode ist auf die Rekursionsgleichung T (n) = 2 T (n/2) + n lg n nicht anwendbar, obwohl es so aussieht, als ob sie die korrekte Form hätte: a = 2, b = 2, f (n) = n lg n und nlogb a = n. Möglicherweise denken Sie irrtümlicherweise, dass Fall 3 angewendet werden könnte, da f (n) = n lg n asymptotisch größer als nlogb a = n ist. Das Problem besteht darin, das f (n) nicht polynomial größer ist. Das Verhältnis f (n)/nlogb a = (n lg n)/n = lg n ist asymptotisch kleiner als n für jede positive Konstante . Folglich fällt die Rekursionsgleichung in die Lücke zwischen Fall 2 und Fall 3. (Siehe Übung 4.6-2 für eine Lösung.) Lassen Sie uns die Mastermethode anwenden, um die Rekursionsgleichungen zu lösen, die wir in den Abschnitten 4.1 und 4.2 begegnet haben. Rekursionsgleichung (4.7) T (n) = 2 T (n/2) + Θ(n) ,
98
4 Teile-und-Beherrsche
beschreibt die Laufzeiten der Teile-und-Beherrsche-Algorithmen für das Max-TeilfeldProblem und für Sortieren durch Mischen. (Gemäß unserer Vereinbarung, geben wir die Basisfälle der Rekursionsgleichungen nicht an.) Bei dieser Rekursionsgleichung gilt a = 2, b = 2, f (n) = Θ(n), und somit gilt nlogb a = nlog2 2 = n. Fall 2 ist anwendbar, da f (n) = Θ(n), and wir erhalten als Lösung T (n) = Θ(n lg n). Rekursionsgleichung (4.17) T (n) = 8 T (n/2) + Θ(n2 ) , beschreibt die Laufzeit des ersten Teile-und-Beherrsche-Algorithmus, den wir uns für Matrizenmultiplikation überlegt haben. Hier gilt a = 8, b = 2 und f (n) = Θ(n2 ) und somit nlogb a = nlog2 8 = n3 . Da n3 polynomial größer als f (n) ist (d. h. f (n) = O(n3− ) für = 1), kommt Fall 1 zum Tragen und es gilt T (n) = Θ(n3 ). Betrachten Sie schlussendlich Rekursionsgleichung (4.18) T (n) = 7 T (n/2) + Θ(n2 ) , die die Laufzeit von Strassens Algorithmus angibt. Hier haben wir a = 7, b = 2, f (n) = Θ(n2 ), und somit nlogb a = nlog2 7 . Schreiben wir log2 7 als lg 7 und erinnern wir uns, dass 2, 80 < lg 7 < 2, 81 gilt, so sehen wir, dass f (n) = O(nlg 7− ) für = 0, 8 gilt. Wieder kommt Fall 1 zum Tragen und wir erhalten die Lösung T (n) = Θ(nlg 7 ).
Übungen 4.5-1 Wenden Sie die Mastermethode an, um asymptotisch scharfe Schranken für die folgenden Rekursionsgleichungen zu bestimmen: (a) T (n) = 2 T (n/4) + 1. √ (b) T (n) = 2 T (n/4) + n. (c) T (n) = 2 T (n/4) + n. (d) T (n) = 2 T (n/4) + n2 . 4.5-2 Professor Caesar will einen Algorithmus für Matrizenmultiplikation entwickeln, der asymptotisch schneller als Strassens Algorithmus ist. Sein Algorithmus verwendet die Teile-und-Beherrsche-Methode und teilt jede Matrix in Teilmatrizen der Größe n/4 × n/4, wobei der Teilungsschritt und der Vereinigungsschritt zusammen Θ(n2 ) Zeit benötigen. Professor Caesar muss bestimmen, wie viele Teilprobleme sein Algorithmus höchstens generieren darf, um Strassens Algorithmus schlagen zu können. Wenn sein Algorithmus a Teilprobleme generiert, ist die Rekursionsgleichung für die Laufzeit T (n) seines Algorithmus durch T (n) = a T (n/4) + Θ(n2 ) gegeben. Wie groß darf die ganze Zahl a höchstens sein, damit der Algorithmus von Professor Caesar asymptotisch schneller als Strassens Algorithmus ist? 4.5-3 Zeigen Sie mit der Mastermethode, dass die Lösung der Rekursionsgleichung T (n) = T (n/2) + Θ(1) für die Laufzeit der binären Suche in Θ(lg n) liegt. (Siehe Übung 2.3-5 für eine Beschreibung der binären Suche.)
4.6 ∗Beweis des Mastertheorems
99
4.5-4 Kann die Mastermethode auf die Rekursionsgleichung T (n) = 4 T (n/2)+n2 lg n angewendet werden? Warum oder warum nicht? Geben Sie eine asymptotisch obere Schranke für diese Rekursionsgleichung an. 4.5-5∗ Betrachten Sie die Regularitätsbedingung a f (n/b) ≤ c f (n) für eine Konstante c < 1, die eine Voraussetzung zu Fall 3 des Mastertheorems ist. Geben Sie ein Beispiel für die Konstanten a ≥ 1 und b > 1 und die Funktion f (n) an, die bis auf die Regularitätsbedingung alle Voraussetzungen zu Fall 3 des Mastertheorems erfüllen.
∗ 4.6
Beweis des Mastertheorems
Dieser Abschnitt enthält den Beweis des Mastertheorems (Theorem 4.1). Sie müssen den Beweis nicht verstehen, um das Theorem anwenden zu können. Der Beweis besteht aus zwei Teilen. Der erste Teil analysiert die Masterrekursionsgleichung (4.20) unter der vereinfachenden Annahme, dass T (n) nur für Potenzen von b > 1 definiert ist, d. h. für n = 1, b, b2 , . . .. Dieser Teil gibt die Intuition, die notwendig ist, um zu verstehen, warum das Mastertheorem korrekt ist. Der zweite Teil zeigt, wie die Analyse auf alle positiven ganzen Zahlen n ausgedehnt werden kann; er wendet mathematische Techniken zum Auf- und Abrunden reeller Zahlen auf das Problem an. In diesem Abschnitt werden wir unsere asymptotische Notation manchmal geringfügig missbräuchlich verwenden, indem wir sie verwenden, um das Verhalten von Funktionen zu beschreiben, die nur über Potenzen von b definiert sind. Erinnern Sie sich daran, dass die Definitionen der asymptotischen Notationen fordern, dass die Schranken für alle hinreichend großen Zahlen bewiesen werden, nicht nur für diejenigen, die Potenzen von b sind. Da wir neue asymptotische Notationen einführen könnten, die nur auf die Menge bi : i = 0, 1, . . . anstatt auf die nichtnegativen Zahlen anwendbar sind, ist dieser Missbrauch geringfügig. Trotzdem müssen wir immer vorsichtig sein, wenn wir die asymptotische Notation über einem eingeschränkten Bereich verwenden, damit wir keine falschen Schlüsse ziehen. Der Beweis, dass T (n) = O(n) gilt, wenn n eine Potenz von 2 ist, garantiert zum Beispiel nicht, dass T (n) = O(n) für alle n gilt. Die Funktion T (n) könnten wir als n falls n = 1, 2, 4, 8, . . . , T (n) = n2 sonst definieren. In diesem Falle ist die beste zu beweisende obere Schranke, die für alle Werte von n gilt, T (n) = O(n2 ). Aufgrund dieser dramatischen Konsequenz sollten wir niemals die asymptotische Notation über einem eingeschränkten Bereich benutzen, ohne im Kontext hervorzuheben, dass wir dies tun.
4.6.1
Der Beweis für exakte Potenzen
Der erste Teil des Beweises des Mastertheorems analysiert die Rekursionsgleichung (4.20) T (n) = a T (n/b) + f (n)
100
4 Teile-und-Beherrsche
unter der Annahme, dass n eine Potenz von b > 1 ist, wobei b nicht ganzzahlig sein muss. Wir unterteilen die Analyse in drei Lemmata. Das erste Lemma reduziert das Problem, eine Lösung der Masterrekursionsgleichung zu finden, auf das Problem, einen Ausdruck auszuwerten, der eine Summenformel enthält. Das zweite Lemma bestimmt Schranken für die Summenformel. Das dritte Lemma führt die ersten beiden Ergebnisse zusammen, um eine Version des Mastertheorems zu beweisen, in der n eine exakte Potenz von b ist. Lemma 4.2 Seien a ≥ 1 und b > 1 Konstanten und sei f (n) eine nichtnegative Funktion, die über den Potenzen von b definiert ist. Definieren wir T (n) über die Potenzen von b durch die Rekursionsgleichung Θ(1) falls n = 1 , T (n) = a T (n/b) + f (n) falls n = bi , wobei i eine positive ganze Zahl ist, dann gilt
logb n−1
T (n) = Θ(nlogb a ) +
aj f (n/bj ) .
(4.21)
j=0
Beweis: Wir benutzen den Rekursionsbaum aus Abbildung 4.7. Die Wurzel des Baumes hat die Kosten f (n), besitzt a Kinder, von denen jedes die Kosten f (n/b) verursacht. (Es ist zweckmäßig, wenn auch von der Mathematik her nicht notwendig, a als eine ganze Zahl anzunehmen, wenn man den Rekursionsbaum grafisch darstellen will.) Jedes dieser Kinder hat selbst wieder a Kinder, und somit gibt es insgesamt a2 Knoten der Tiefe 2, wobei jedes dieser Kinder Kosten f (n/b2 ) hat. Im Allgemeinen gibt es aj Knoten der Tiefe j und jeder dieser Knoten hat Kosten f (n/bj ). Die Kosten jedes Blattes betragen T (1) = Θ(1), wobei jedes Blatt wegen n/blogb n = 1 die Tiefe logb n besitzt. Der Baum verfügt somit über alogb n = nlogb a Blätter. Wir können Gleichung (4.21) erhalten, indem wir die Kosten der Knoten einer jeden Ebene im Baum aufsummieren, wie dies in der Abbildung gezeigt wird. Die Kosten aller Knoten der Tiefe j betragen aj f (n/bj ), sodass sich die Gesamtkosten über alle Knoten durch
logb n−1
aj f (n/bj )
j=0
ergeben. Im zugrunde liegenden Teile-und-Beherrsche-Algorithmus repräsentiert diese Summe die Kosten dafür, das Problem in Teilprobleme zu teilen und am Ende die Lösungen der Teilprobleme zu vereinigen. Die Kosten aller Blätter, also die Kosten dafür, alle nlogb a Teilprobleme der Größe 1 zu lösen, sind in Θ(nlogb a ). Bezogen auf den Rekursionsbaum entsprechen die drei, im Mastertheorem unterschiedenen Fälle, den folgenden Fällen: Im Fall (1) werden die Gesamtkosten des Baumes
4.6 ∗Beweis des Mastertheorems
101 f (n)
f (n) a f (n/b)
…
f (n/b)
a
af (n/b)
f (n/b)
a
a
logb n f (n/b2 )f (n/b2 )…f (n/b2 )
a
a
a
a
a
a
a
a
a
…
…
…
…
…
…
…
…
…
Θ(1) Θ(1) Θ(1) Θ(1) Θ(1) Θ(1) Θ(1) Θ(1) Θ(1) Θ(1)
…
a2 f (n/b2 )
…
f (n/b2 )f (n/b2 )…f (n/b2 ) f (n/b2 )f (n/b2 )…f (n/b2 )
Θ(1) Θ(1) Θ(1)
nlogb a
Θ(nlogb a )
logb n−1
Insgesamt: Θ(nlogb a ) +
aj f (n/bj )
j=0
Abbildung 4.7: Der durch T (n) = a T (n/b) + f (n) generierte Rekursionsbaum. Der Baum ist ein vollständiger a-närer Baum mit nlogb a Blättern und Höhe logb n. Die Kosten der Knoten auf jeder Ebene stehen rechts und deren Summe ist durch Gleichung (4.21) gegeben.
von den Kosten der Blätter dominiert, im Fall (2) sind sie gleichmäßig über den Ebenen des Baumes verteilt und im Fall (3) werden Sie von den Kosten der Wurzel dominiert. Die Summenformel in Gleichung (4.21) beschreibt die Kosten des Teilens und Zusammensetzens im zugrunde liegenden Teile-und-Beherrsche-Algorithmus. Das nächste Lemma liefert asymptotische Schranken für das Wachstum der Summenformel. Lemma 4.3 Seien a ≥ 1 und b > 1 Konstanten und sei f (n) eine nichtnegative Funktion, die über den Potenzen von b definiert ist. Eine über den Potenzen von b durch
logb n−1
g(n) =
aj f (n/bj )
(4.22)
j=0
definierte Funktion g(n) besitzt die folgenden asymptotischen Schranken für Potenzen von b: 1. Gilt f (n) = O(nlogb a− ) für eine Konstante > 0, dann gilt g(n) = O(nlogb a ). 2. Gilt f (n) = Θ(nlogb a ), dann gilt g(n) = Θ(nlogb a lg n).
102
4 Teile-und-Beherrsche 3. Gilt a f (n/b) ≤ c f (n) für eine Konstante c < 1 und für alle hinreichend großen n, dann gilt g(n) = Θ(f (n)).
Beweis: Im Fall 1 gilt f (n) = O(nlogb a− ), was f (n/bj ) = O((n/bj )logb a− ) impliziert. Das Einsetzen in Gleichung (4.22) führt zu ⎛ g(n) = O ⎝
logb n−1
aj
n logb a− bj
j=0
⎞ ⎠ .
(4.23)
Wir beschränken die Summenformel innerhalb der O-Notation, indem wir Terme ausklammern und vereinfachen, sodass wir zu einer steigenden geometrischen Reihe kommen:
logb n−1
a
j
n logb a− bj
j=0
logb n−1
=n
logb a−
j=0
ab
j
blogb a
logb n−1
= nlogb a−
(b )j
j=0
b logb n − 1 b − 1 n −1 = nlogb a− . b − 1
= nlogb a−
Da b und Konstanten sind, können wir den letzten Term zu nlogb a− O(n ) = O(nlogb a ) umformen. Setzen wir diesen Ausdruck an Stelle der Summenformel in Gleichung (4.23) ein, führt dies zu g(n) = O(nlogb a ) . Damit ist Fall 1 bewiesen. Da Fall 2 annimmt, dass f (n) = Θ(nlogb a ) gilt, erhalten wir in diesem Fall f (n/bj ) = Θ((n/bj )logb a ). Das Einsetzen in Gleichung (4.22) führt zu ⎛
logb n−1
g(n) = Θ ⎝
j=0
aj
n logb a bj
⎞ ⎠ .
(4.24)
Wir beschränken die Summenformel innerhalb der Θ-Notation wie im Fall 1. Wir erhalten aber diesmal keine geometrische Reihe. Stattdessen stellen wir fest, dass alle Terme
4.6 ∗Beweis des Mastertheorems
103
der Summenformel gleich sind:
logb n−1
aj
j=0
n logb a bj
= nlogb a
logb n−1
j
blogb a
j=0
a
logb n−1
= nlogb a
1
j=0
= nlogb a logb n . Setzen wir diesen Ausdruck für die Summenformel in Gleichung (4.24) ein, führt dies zu g(n) = Θ(nlogb a logb n) = Θ(nlogb a lg n) , womit Fall 2 bewiesen ist. Wir beweisen Fall 3 ganz ähnlich. Da f (n) in der Definition (4.22) von g(n) vorkommt und alle Terme von g(n) nichtnegativ sind, können wir schlussfolgern, dass g(n) = Ω(f (n)) für die Potenzen von b gilt. Wir nehmen in der Aussage des Lemmas an, dass die Ungleichung a f (n/b) ≤ c f (n) für eine Konstante c < 1 und hinreichend große n gilt. Wir schreiben die Ungleichung um zu f (n/b) ≤ (c/a)f (n). Iterieren wir j-mal, erhalten wir f (n/bj ) ≤ (c/a)j f (n) oder äquivalent dazu aj f (n/bj ) ≤ cj f (n). Hierbei nehmen wir an, dass die Werte, die wir iterieren, hinreichend groß sind. Diese Ungleichung wird von höchstens konstant vielen Termen verletzt, nämlich möglicherweise von den Termen, bei denen n/bj−1 nicht hinreichend groß ist. Für diese gilt jedoch aj f (n/bj ) = O(1). Das Einsetzen in Gleichung (4.22) und Vereinfachen führt zu einer geometrischen Reihe; aber im Gegensatz zum Fall 1 hat diese fallende Terme. Wir benutzen einen O(1)-Term, um all die Terme zu erfassen, die nicht durch unsere Prämisse „n hinreichend groß“ abgedeckt werden. Es gilt
logb n−1
g(n) =
aj f (n/bj )
j=0
logb n−1
≤
cj f (n) + O(1)
j=0
≤ f (n)
∞
cj + O(1)
j=0
1 1−c = O(f (n)) , = f (n)
+ O(1)
da c eine Konstante ist. Somit folgt, dass g(n) = Θ(f (n)) für die Potenzen von b gilt. Damit ist Fall 3 bewiesen und der Beweis des Lemmas abgeschlossen.
104
4 Teile-und-Beherrsche
Wir können nun eine Version des Mastertheorems für den Fall beweisen, in dem n eine Potenz von b ist. Lemma 4.4 Seien a ≥ 1 und b > 1 Konstanten definierte nichtnegative Funktion. Sei kursionsgleichung Θ(1) T (n) = a T (n/b) + f (n)
und sei f (n) eine über den Potenzen von b T (n) über den Potenzen von b durch die Refalls n = 1 , falls n = bi ,
definiert, wobei i eine positive ganze Zahl ist. Dann besitzt T (n) die folgenden asymptotischen Schranken für Potenzen von b: 1. Gilt f (n) = O(nlogb a− ) für eine Konstante > 0, dann gilt T (n) = Θ(nlogb a ). 2. Gilt f (n) = Θ(nlogb a ), dann gilt T (n) = Θ(nlogb a lg n). 3. Gilt f (n) = Ω(nlogb a+ ) für eine Konstante > 0 und a f (n/b) ≤ c f (n) für eine Konstante c < 1 und alle hinreichend großen n, dann gilt T (n) = Θ(f (n)). Beweis: Wir wenden die Schranken aus Lemma 4.3 an, um die Summenformel (4.21) in Lemma 4.2 auszuwerten. Im Fall 1 gilt T (n) = Θ(nlogb a ) + O(nlogb a ) = Θ(nlogb a ) und im Fall 2 T (n) = Θ(nlogb a ) + Θ(nlogb a lg n) = Θ(nlogb a lg n) . Im Fall 3 ergibt sich T (n) = Θ(nlogb a ) + Θ(f (n)) = Θ(f (n)) , da f (n) = Ω(nlogb a+ ) gilt.
4.6.2
Aufrunden und Abrunden
Um den Beweis des Mastertheorems zu vervollständigen, müssen wir unsere Analyse auf den Fall ausdehnen, in dem Aufrunden und Abrunden in der Masterrekursionsgleichung
4.6 ∗Beweis des Mastertheorems
105
verwendet werden. Damit ist die Rekursionsgleichung für alle ganzen Zahlen definiert, nicht nur für die Potenzen von b. Eine untere Schranke für T (n) = a T (n/b ) + f (n)
(4.25)
und eine obere Schranke für T (n) = a T ( n/b) + f (n)
(4.26)
zu erhalten, ist eine einfache Übung, da wir im ersten Fall die Ungleichung n/b ≥ n/b anwenden können, um das gewünschte Resultat zu erhalten, und im zweiten Fall die Ungleichung n/b ≤ n/b. Wir wenden im Wesentlichen die gleiche Technik an, um die Rekursionsgleichung (4.26) nach unten zu beschränken, wie die, um die Rekursionsgleichung (4.25) nach oben zu beschränken, sodass wir nur die letztere der beiden Schranken hier beweisen werden. Wir modifizieren den Rekursionsbaum in Abbildung 4.7, um den Rekursionsbaum in Abbildung 4.8 zu erhalten. Wenn wir im Rekursionsbaum nach unten absteigen, dann erhalten wir eine Folge von zu den rekursiven Aufrufen gehörigen Argumenten n n/b n/b /b n/b /b /b
, , , , .. .
Lassen Sie uns das j-te Argument der Folge mit nj bezeichnen, wobei nj =
n nj−1 /b
falls j = 0 , falls j > 0
(4.27)
gilt. Unser erstes Ziel ist es, die Tiefe k zu bestimmen, sodass nk eine Konstante ist. Unter Verwendung der Ungleichung x ≤ x + 1 erhalten wir n0 ≤ n , n n1 ≤ + 1 , b n 1 n2 ≤ 2 + + 1 , b b n 1 1 n3 ≤ 3 + 2 + + 1 , b b b .. .
106
4 Teile-und-Beherrsche f (n)
f (n) a f (n1 )
f (n1 )
a
a
…
af (n1 )
f (n1 ) a
logb n f (n2 ) f (n2 ) … f (n2 )
f (n2 ) f (n2 )
… f (n
a
a
a
a
a
a
a
a
a
…
…
…
…
…
…
…
…
…
Θ(1) Θ(1) Θ(1) Θ(1) Θ(1) Θ(1) Θ(1) Θ(1) Θ(1) Θ(1)
…
a2 f (n2 )
2)
…
f (n2 ) f (n2 ) … f (n2 )
Θ(nlogb a )
Θ(1) Θ(1) Θ(1)
Θ(nlogb a )
logb n−1
Insgesamt: Θ(nlogb a ) +
aj f (nj )
j=0
Abbildung 4.8: Der durch die Rekursionsgleichung T (n) = a T (n/b) + f (n) generierte Rekursionsbaum. Das rekursive Argument nj ist durch Gleichung (4.27) gegeben.
Im Allgemeinen gilt n 1 nj ≤ j + b bi i=0 j−1
∞
b + b/(b − 1) und eine Konstante c < 1 erfüllt ist, dann folgt daraus aj f (nj ) ≤ cj f (n). Deshalb können wir die Summe in Gleichung (4.29) wie in Lemma 4.3 berechnen. Im Fall 2 gilt f (n) = Θ(nlogb a ). Wenn wir zeigen können, dass f (nj ) = O(nlogb a /aj ) = O((n/bj )logb a ) gilt, dann könnten wir den Beweis für Fall 2 aus Lemma 4.3 übernehmen. Beachten Sie, dass j ≤ logb n die Ungleichung bj /n ≤ 1 impliziert. Die Schranke f (n) = O(nlogb a ) bedeutet, dass eine Konstante c > 0 existiert, sodass für alle hinreichend großen nj gilt
logb a n b + bj b−1 logb a n b bj · =c j 1+ b n b−1 log a j logb a n b b b · =c 1 + aj n b−1 log a logb a n b b ≤c 1 + aj b−1 log a b n =O , aj
f (nj ) ≤ c
da c (1+b/(b−1))logb a eine Konstante ist. Damit haben wir Fall 2 bewiesen. Der Beweis von Fall 1 ist fast identisch. Die Idee besteht darin zu beweisen, dass die Schranke f (nj ) = O((n/bj )logb a− ) gilt. Dies geht analog zu dem entsprechenden Beweis von Fall 2, wenngleich die Rechnung aufwendiger ist. Wir haben nun für alle ganzzahligen Werte n die oberen Schranken aus dem Mastertheorem bewiesen. Der Beweis der unteren Schranken läuft ähnlich ab.
108
4 Teile-und-Beherrsche
Übungen 4.6-1∗ Geben Sie einen einfachen und exakten Ausdruck für nj in Gleichung (4.27) für den Fall an, dass b statt einer beliebigen reellen Zahl eine positive ganze Zahl ist. 4.6-2∗ Zeigen Sie, dass die Masterrekursionsgleichung T (n) = Θ(nlogb a lgk+1 n) als Lösung hat, wenn f (n) = Θ(nlogb a lgk n) mit k ≥ 0 gilt. Beschränken Sie Ihre Analyse der Einfachheit halber auf Potenzen von b. 4.6-3∗ Zeigen Sie, dass Fall 3 des Mastertheorems in dem Sinne zu streng formuliert ist, dass die Regularitätsbedingung a f (n/b) ≤ c f (n) für eine Konstante c < 1 impliziert, dass eine Konstante > 0 existiert, sodass f (n) = Ω(nlogb a+ ) gilt.
Problemstellungen 4-1 Beispiele für Rekursionsgleichungen Geben Sie für jede der folgenden Rekursionsgleichungen eine asymptotisch obere und untere Schranke für T (n) an. Nehmen Sie an, dass T (n) für n ≤ 2 konstant ist. Geben Sie möglichst scharfe Schranken an und begründen Sie Ihre Antworten. a. T (n) = 2 T (n/2) + n4 . b. T (n) = T (7n/10) + n. c. T (n) = 16 T (n/4) + n2 . d. T (n) = 7 T (n/3) + n2 . e. T (n) = 7 T (n/2) + n2 . √ f. T (n) = 2 T (n/4) + n. g. T (n) = T (n − 2) + n2 . 4-2 Kosten der Parameterübergabe Wir nehmen im Buch immer an, dass die Parameterübergabe beim Aufruf einer Prozedur konstante Zeit benötigt, auch wenn ein N -elementiges Feld übergeben wird. Diese Annahme ist in den meisten Systemen zutreffend, da ein Zeiger auf das Feld übergeben wird, nicht das Feld selbst. Die vorliegende Problemstellung untersucht die Folgen von drei Strategien zur Parameterübergabe: 1. Ein Feld wird durch einen Zeiger übergeben. Die hiefür benötigte Zeit liegt in Θ(1). 2. Ein Feld wird durch Kopieren übergeben. Dies erfolgt in Zeit Θ(N ), wobei N die Größe des Feldes ist. 3. Ein Feld wird übergeben, indem nur der Bereich kopiert wird, auf den die aufgerufene Prozedur (möglicherweise) zugreift. Dies erfolgt in Zeit Θ(q − p + 1), wenn das Teilfeld A[p . . q] übergeben wird.
Problemstellungen zu Kapitel 4
109
a. Betrachten Sie den Algorithmus der rekursiven binären Suche zum Finden einer Zahl in einem sortierten Feld (siehe Übung 2.3-5). Geben Sie für jeden der drei Methoden zur Parameterübergabe Rekursionsgleichungen für die Laufzeit der binären Suche im schlechtesten Fall an und geben Sie gute obere Schranken für die Lösungen der Rekursionsgleichungen an. N sei die Größe des ursprünglichen Problems und n die Größe des Teilproblems. b. Lösen Sie Teil (a) auch für Merge-Sort aus Abschnitt 2.3.1. 4-3 Weitere Beispiele für Rekursionsgleichungen Geben Sie asymptotisch obere und untere Schranken für T (n) in jeder der folgenden Rekursionsgleichungen an. Gehen Sie davon aus, dass T (n) für hinreichend kleine n konstant ist. Geben Sie möglichst scharfe Schranken an und begründen Sie Ihre Antworten. a. b. c. d. e. f. g. h. i. j.
T (n) = 4 T (n/3) + n lg n. T (n) = 3 T (n/3) + n/ lg n. √ T (n) = 4 T (n/2) + n2 n. T (n) = 3 T (n/3 − 2) + n/2. T (n) = 2 T (n/2) + n/ lg n. T (n) = T (n/2) + T (n/4) + T (n/8) + n. T (n) = T (n − 1) + 1/n. T (n) = T (n − 1) + lg n. T (n) = T (n − 2) + 1/ lg n. √ √ T (n) = n T ( n) + n.
4-4 Fibonacci-Zahlen Diese Problemstellung erarbeitet Eigenschaften der Fibonacci-Zahlen, die durch die Rekursionsgleichung (3.22) definiert werden. Wir werden formale Potenzreihen benutzen, um die Fibonaccische Rekursionsgleichung zu lösen. Wir definieren die formale Potenzreihe F durch F(z) =
∞
Fi z i
i=0
= 0 + z + z 2 + 2z 3 + 3z 4 + 5z 5 + 8z 6 + 13z 7 + 21z 8 + · · · , wobei Fi die i-te Fibonacci-Zahl ist. a. Zeigen Sie, dass F(z) = z + z F(z) + z 2 F(z) gilt. b. Zeigen Sie, dass z F(z) = 1 − z − z2 z = (1 − φ z)(1 − φ z) 1 1 1 − =√ 5 1 − φz 1 − φ z
110
4 Teile-und-Beherrsche gilt, wobei √ 1+ 5 = 1,61803 . . . φ= 2 und
√ 1− 5 φ= = −0,61803 . . . 2
ist. c. Zeigen Sie, dass F(z) =
∞ 1 √ (φi − φi ) z i 5 i=0
gilt. √ d. Beweisen Sie mit Hilfe von Teil (c) die Gleichung Fi = φi / 5, gerundet auf die nächste ganze Zahl, für i > 0. (Hinweis: Es gilt φ < 1). 4-5 Chips testen Professor Diogenes besitzt n angeblich identische integrierte Schaltungen als Chips, die sich an sich gegenseitig testen können. Die Testvorrichtung des Professors nimmt jeweils zwei Chips auf. Ist die Testvorrichtung mit zwei Chips belegt, dann testet jeder der beiden Chips den anderen und meldet, ob dieser fehlerfrei oder fehlerbehaftet ist. Ein fehlerfreier Chip gibt immer die korrekte Antwort, ob der andere Chip fehlerfrei oder fehlerbehaftet ist; aber der Professor kann der Antwort eines fehlerbehafteten Chips nicht trauen. Somit sind die folgenden vier Ausgänge eines Testes möglich: Chip A sagt B fehlerfrei B fehlerfrei B fehlerbehaftet B fehlerbehaftet
Chip B sagt A fehlerfrei A fehlerbehaftet A fehlerfrei A fehlerbehaftet
Schlussfolgerung beide fehlerfrei oder beide fehlerbehaftet mindestens einer ist fehlerbehaftet mindestens einer ist fehlerbehaftet mindestens einer ist fehlerbehaftet
a. Zeigen Sie, dass der Professor mithilfe einer auf diesen paarweisen Tests aufbauenden Strategie nicht notwendigerweise bestimmen kann, welche Chips fehlerfrei sind, wenn mindestens n/2 Chips fehlerbehaftet sind. Nehmen Sie an, dass sich die fehlerbehafteten Chips verschwören können, um den Professor zu täuschen. b. Betrachten Sie das Problem, einen einzigen fehlerfreien Chip unter n Chips zu finden, unter der Annahme, dass mehr als n/2 der Chips fehlerfrei sind. Zeigen Sie, dass n/2 Tests ausreichend sind, um die Problemgröße nahezu zu halbieren. c. Zeigen Sie, dass die fehlerfreien Chips mit Θ(n) Tests identifiziert werden können, unter der Annahme, dass mehr als n/2 der Chips fehlerfrei sind. Geben Sie die Rekursionsgleichung an, die die Anzahl der Tests beschreibt, und lösen Sie diese.
Problemstellungen zu Kapitel 4
111
4-6 Monge-Felder Ein m × n Feld A reeller Zahlen ist ein Monge-Feld, wenn für alle i, j, k und l mit 1 ≤ i < k ≤ m und 1 ≤ j < l ≤ n A[i, j] + A[k, l] ≤ A[i, l] + A[k, j] gilt. Mit anderen Worten, wann immer wir zwei Zeilen und zwei Spalten eines Monge-Feldes auswählen und die vier Elemente an den Überschneidungen von Zeilen und Spalten betrachten, ist die Summe aus den Elementen links oben und rechts unten kleiner oder gleich der Summe aus den Elementen links unten und rechts oben. Zum Beispiel ist das folgende Feld ein Monge-Feld: 10 17 24 11 45 36 75
17 22 28 13 44 33 66
13 16 22 6 32 19 51
28 29 34 17 37 21 53
23 23 24 7 23 6 34
a. Beweisen Sie, dass ein Feld genau dann ein Monge-Feld ist, wenn für alle i = 1, 2, ..., m − 1 und j = 1, 2, ..., n − 1 A[i, j] + A[i + 1, j + 1] ≤ A[i, j + 1] + A[i + 1, j] gilt. (Hinweis: Benutzen Sie für den „wenn“-Teil Induktion getrennt nach Zeilen und Spalten.) b. Das folgende Feld ist kein Monge-Feld. Verändern Sie ein Element, um es zu einem Monge-Feld zu machen. (Hinweis: Wenden Sie Teil (a) an.) 37 21 53 32 43
23 6 34 13 21
22 7 30 9 15
32 10 31 6 8
c. Sei f (i) der Index der Spalte, die das am weitesten links stehende minimale Element der Zeile i enthält. Beweisen Sie, dass für jedes m × n Monge-Feld die Ungleichung f (1) ≤ f (2) ≤ · · · ≤ f (m) gilt. d. Hier ist die Beschreibung eines Algorithmus nach dem Teile-und-BeherrschePrinzip, der das am weitesten links stehende minimale Element in jeder Zeile eines m × n Monge-Feldes berechnet: Konstruieren Sie eine Teilmatrix A von A, die aus den geradzahligen Zeilen von A besteht. Bestimmen Sie rekursiv das am weitesten links stehende minimale Element jeder Zeile von A . Bestimmen Sie dann die am weitesten links stehenden minimalen Elemente der ungeradzahligen Zeilen von A.
112
4 Teile-und-Beherrsche Erklären Sie, wie Sie die am weitesten links stehenden minimalen Elemente der ungeradzahligen Zeilen von A in Zeit O(m + n) berechnen können. Setzen Sie hierbei voraus, dass Sie die am weitesten links stehenden minimalen Elemente der geradzahligen Zeilen bereits kennen. e. Geben Sie die Rekursionsgleichung für die Laufzeit des in Teil (d) beschriebenen Algorithmus an. Zeigen Sie, dass die Lösung in O(m + n log m) liegt.
Kapitelbemerkungen Teile-und-Beherrsche ist eine Technik zum Entwurf von Algorithmen, die zumindest bereits im Jahr 1962 bekannt war und in einem Artikel von Karatsuba und Ofman [194] zu finden ist. Es kann jedoch gut sein, dass die Technik bereits schon früher angewendet worden ist; laut Heideman, Johnson und Burrus [163], hat C. F. Gauss sich in 1805 den ersten Algorithmus für Schnelle Fourier-Transformation überlegt, wobei die Beschreibung von Gauss das Problem in kleinere Probleme zerlegt, deren Lösungen miteinander kombiniert werden. Das Max-Feld-Problem aus Abschnitt 4.1 ist eine leichte Variation eines Problems, welches durch Bentley [43, Kapitel 7] untersucht wurde. Strassens Algorithmus [325] erregte große Aufmerksamkeit, als er in 1969 veröffentlicht wurde. Davor konnten sich nur wenige vorstellen, dass es einen Algorithmus zur Matrizenmultiplikation geben könnte, der asymptotisch schneller als die elementare SquareMatrix-Multiply Prozedur ist. Die asymptotisch obere Schranke für Matrizenmultiplikation wurde seitdem verbessert. Der asymptotisch effizienteste Algorithmus, um n × n-Matrizen zu multipizieren, geht auf Coppersmith und Winograd [78] zurück und hat eine Laufzeit von O(n2,376 ). Die beste untere Schranke, die bekannt ist, ist die offensichtliche Ω(n2 ) Schranke (offensichtlich, da wir n2 Elemente in die Produktmatrix eintragen müssen). Aus praktischer Sicht ist Strassens Algorithmus oft nicht die Methode, die wir für Matrizenmultiplikation wählen sollten. Dies hat vier Gründe: 1. Die konstanten Faktoren, die in der Θ(nlg 7 )-Laufzeit versteckt sind, sind größer als die konstanten Faktoren, die in der Θ(n3 )-Laufzeit der Prozedur SquareMatrix-Multiply versteckt sind. 2. Wenn die Matrizen dünn besetzt sind, sind Methoden, die auf diesen speziellen Fall zugeschnitten sind, schneller. 3. Strassens Algorithmus ist numerisch nicht ganz so stabil als dies Square-MatrixMultiply ist. Anders formuliert, aufgrund der eingeschränkten Rechengenauigkeit der Computerarithmetik auf nichtganzen Zahlen ist der akkumulierte Fehler in Strassens Algorithmus größer als in Square-Matrix-Multiply. 4. Die Teilmatrizen, die auf den einzelnen Rekursionsebenen generiert werden müssen, belegen Speicherplatz.
Kapitelbemerkungen zu Kapitel 4
113
Die beiden letzten Gründe haben sich um 1990 entschärft. Higham [167] zeigt, dass der Unterschied in Bezug auf die numerische Stabilität überbetont wurde; wenngleich Strassens Algorithmus für einige Anwendungen numerisch zu instabil ist, arbeitet er bei anderen Anwendungen durchaus in einem tolerablen Bereich. Bailey, Lee und Simon [32] diskutieren Techniken, mit denen der Speicherplatzbedarf von Strassens Algorithmus reduziert werden kann. In der Praxis setzen Implementierungen für die schnelle Multiplikation dichter Matrizen Strassens Algorithmus ab einer bestimmten Matrizengröße ein und wechseln zu einfacheren Methoden, wenn die Größe der Teilmatrizen unter diesen Schwellenwert fällt. Der exakte Wert dieses Schwellenwertes hängt stark von dem darunterliegenden System ab. Analysen, die die Operationen zählen, aber Effekte, die von Caches oder der Befehlspipeline herrühren, ignorieren, kommen auf Werte von n = 8 (siehe Higham [167]) oder n = 12 (siehe Huss-Lederman et al. [186]) für diesen Schwellenwert. D’Alberto und Nicolau [81] entwickelten ein adaptives Schema, das den Schwellenwert anhand von während der Installation ablaufenden Testläufen bestimmt. Sie fanden Schwellenwerte für unterschiedliche Systeme, die von n = 400 bis n = 2150 reichten. Rekursionsgleichungen sind früh, 1202 von L. Fibonacci, nach dem die Fibonacci-Zahlen benannt sind, untersucht worden. A. de Moivre führte die Methode der formalen Potenzreihen (siehe Problemstellung 4-4) zur Lösung von Rekursionsgleichungen ein. Die Mastermethode wurde von Bentley, Haken und Saxe in [44] eingeführt, die die erweiterte Methode zur Verfügung stellt, die in Übung 4.6-2 begründet wird. Knuth [209] und Liu [237] zeigen, wie lineare Rekursionsgleichungen unter Verwendung der Methode der formalen Potenzreihen gelöst werden. Die Arbeiten von Purdom und Brown [287] und Graham, Knuth und Patashnik [152] enthalten umfangreiche Abhandlungen zur Lösung von Rekursionsgleichungen. Viele Wissenschaftler, darunter Akra und Bazzi [13], Roura [299], Verma [346] und Yap [360], haben Methoden zur Lösung allgemeinerer Rekursionsgleichungen angegeben als die, die durch einen Teile-und-Beherrsche-Algorithmus begründet sind und mithilfe der Mastermethode gelöst werden. Wir beschreiben hier den Ansatz von Akra und Bazzi, wie von Leighton [228] überarbeitet, der für Rekursionsgleichungen der Form T (x) =
Θ(1) k
i=1 ai T (bi x) + f (x)
falls 1 ≤ x ≤ x0 , falls x > x0 ,
angewendet werden kann, mit • x ≥ 1 ist eine reelle Zahl, • x0 ist eine Konstante mit x0 ≥ 1/bi und x0 ≥ 1/(1 − bi ) für i = 1, 2, . . . , k, • ai ist eine positive Konstante für i = 1, 2, . . . , k, • bi ist eine Konstante aus dem Bereich 0 < bi < 1 für i = 1, 2, . . . , k, • k ≥ 1 ist eine ganzzahlige Konstante, und
(4.30)
114
4 Teile-und-Beherrsche
• f (x) ist eine nichtnegative Funktion, die der polynomialen Wachstumsbedingung genügt: es gibt positive Konstanten c1 und c2 , sodass für alle x ≥ 1, für i = 1, 2, . . . , k und für alle u mit bi x ≤ u ≤ x, die Ungleichung c1 f (x) ≤ f (u) ≤ c2 f (x) gilt. (Ist |f (x)| nach oben durch ein Polynom in x beschränkt, dann erfüllt f (x) die polynomiale Wachstumsbedingung. Die Funktion f (x) = xα lgβ x beispielsweise erfüllt diese Bedingung für jede reelle Konstante α und β.) Wenngleich die Mastermethode auf eine Rekursionsgleichung wie zum Beispiel T (n) = T ( n/3) + T ( 2n/3) + O(n) nicht anwendbar ist, die Akra-Bazzi-Methode ist es. Um die Rekursionsgleichung (4.30) zu lösen, müssen wir zuerst die einzige reelle Zahl p finden, für die ki=1 ai bpi = 1 gilt. (Eine solche Zahl p existiert immer.) Die Lösung der Rekursionsgleichung ist dann durch ! x f (u) p du T (n) = Θ x 1 + p+1 1 u gegeben. Die Anwendung der Akra-Bazzi-Methode kann in gewissem Sinne schwierig sein, sie hilft aber Rekursionsgleichungen zu lösen, die die Laufzeit für den Fall beschreiben, in dem das Problem in Teilprobleme aufgeteilt werden, die von ihrer Größe her stark unterschiedlich sind. Die Mastermethode ist einfacherer, aber nur anwendbar, wenn die Größe der Teilprobleme gleich ist.
5
Probabilistische Analyse und randomisierte Algorithmen
Dieses Kapitel führt probabilistische Analyse und randomisierte Algorithmen ein. Wenn Sie mit den Grundlagen der Wahrscheinlichkeitstheorie nicht vertraut sind, sollten Sie Anhang C lesen, der einen Überblick über diesen Stoff gibt. Wir werden an verschiedenen Stellen in diesem Buch auf probabilistische Analyse und randomisierte Algorithmen zurückkommen.
5.1
Das Bewerberproblem
Nehmen Sie an, Sie müssten einen neuen Sekretär für Ihre Geschäftsstelle einstellen. Ihre vorangegangenen Versuche waren nicht erfolgreich, und Sie beschließen, eine Vermittlungsagentur einzuschalten. Die Agentur schickt Ihnen jeden Tag einen Kandidaten. Sie interviewen diese Person und entscheiden dann, ob Sie sie einstellen oder nicht. Sie müssen der Agentur eine kleine Gebühr dafür bezahlen, dass Sie einen Kandidaten interviewen. Den Bewerber tatsächlich einzustellen, ist jedoch wesentlich teurer, denn Sie müssen Ihren derzeitigen Sekretär entlassen und außerdem an die Agentur eine hohe Vermittlungsgebühr zahlen. Sie sind beauftragt, jederzeit die für den Job geeignetste Person zu beschäftigen. Deshalb beschließen Sie, jedes Mal wenn Sie einen Bewerber interviewt haben und dieser besser als der gegenwärtig Beschäftigte ist, Ihren derzeitigen Sekretär zu entlassen und den neuen Bewerber einzustellen. Sie sind bereit, den aus dieser Strategie resultierenden Preis zu zahlen, aber Sie wollen gern abschätzen, wie hoch dieser Preis sein wird. Die weiter unten angegebene Prozedur Hire-Assistant formuliert diese Strategie in Pseudocode. Es wird angenommen, dass die Bewerber mit 1 bis n nummeriert sind. Die Prozedur setzt voraus, dass Sie, nachdem Sie Bewerber i interviewt haben, in der Lage sind, festzustellen, ob dieser der beste unter den bisher Interviewten ist. Zwecks Initialisierung erzeugt die Prozedur einen Dummy-Kandidaten mit der Nummer 0, der weniger qualifiziert ist als alle übrigen. Hire-Assistant(n) 1 bester = 0 // Kandidat 0 ist ein am wenigsten qualifizierter Dummy-Kandidat 2 for i = 1 to n 3 interviewe Kandidat i 4 if Kandidat i besser als Kandidat bester 5 bester = i 6 stelle Kandidat i ein
116
5 Probabilistische Analyse und randomisierte Algorithmen
Das Kostenmodell für dieses Problem unterscheidet sich von dem in Kapitel 2 beschriebenen. Wir konzentrieren uns nicht auf die Laufzeit von Hire-Assistant, sondern auf die Kosten, die durch die Interviews und die Einstellungen entstehen. Oberflächlich betrachtet scheint die Kostenanalyse für diesen Algorithmus sehr verschieden von der Analyse der Laufzeit, beispielsweise der von Sortieren durch Mischen, zu sein. Die bei der Analyse verwendeten Methoden sind jedoch identisch, egal ob wir Kosten oder die Laufzeit untersuchen. In beiden Fällen zählen wir, wie oft bestimmte grundlegende Operationen ausgeführt werden. Ein Interview hat geringe Kosten ci , während eine Einstellung mit den Kosten ch teuer ist. Es sei m die Anzahl der eingestellten Personen. Dann betragen die mit dieser Strategie verbundenen Gesamtkosten O(ci n + ch m). Egal, wie viele Personen wir einstellen, müssen wir immer n Kandidaten interviewen, sodass in jedem Fall die mit den Interviews verbundenen Kosten ci n in die Gesamtkosten eingehen. Wir konzentrieren uns daher auf die Analyse der Einstellungskosten ch m. Diese Größe variiert mit jedem Lauf des Algorithmus. Dieses Szenario dient als Modell für ein allgemeines Paradigma der Informatik. Wir haben oft den maximalen oder den minimalen Wert in einer Folge zu suchen, indem wir jedes Element der Folge betrachten und einen aktuellen „Gewinner“ verwalten. Das Bewerberproblem modelliert, wie oft wir unsere Meinung, welches Element aktuell das beste ist, aktualisieren.
Analyse des schlechtesten Falls Im schlechtesten Fall stellen wir jeden interviewten Kandidaten ein. Diese Situation tritt ein, wenn sich die Kandidaten in streng ansteigender Ordnung ihrer Qualifikation vorstellen. In diesem Fall stellen wir n-mal ein, was die Gesamteinstellungskosten O(ch n) verursacht. Natürlich kommen die Kandidaten nicht immer in aufsteigender Reihenfolge ihrer Qualifikation. Tatsächlich haben wir weder eine Vorstellung über die Reihenfolge, in denen sie kommen, noch haben wir irgendeine Kontrolle über diese Reihenfolge. Daher ist es natürlich zu fragen, was wir in einem typischen, mittleren Fall zu erwarten haben.
Probabilistische Analyse In der probabilistischen Analyse wenden wir den Wahrscheinlichkeitsbegriff während der Problemanalyse an. Am häufigsten wenden wir die probabilistische Analyse bei der Untersuchung der Laufzeit von Algorithmen an. Manchmal verwenden wir sie auch, um andere Größen zu analysieren, wie zum Beispiel die Einstellungskosten bei der Prozedur Hire-Assistant. Um eine probabilistische Analyse durchzuführen, müssen wir von unserem Wissen über die Verteilung der Eingaben Gebrauch machen, oder Annahmen über sie treffen. Dann analysieren wir unseren Algorithmus, um eine Laufzeit für den mittleren Fall zu berechnen, wobei wir den Durchschnitt über die Verteilung der möglichen Eingaben nehmen. Auf diese Weise mitteln wir in der Tat die Laufzeit über alle möglichen Eingaben. Wenn wir eine solche Laufzeit angeben, bezeichnen wir sie als mittlere Laufzeit oder Laufzeit im mittleren Fall.
5.1 Das Bewerberproblem
117
Wir müssen sehr sorgfältig sein, wenn wir Annahmen über die Verteilung der Eingaben treffen. Für einige Probleme können wir sinnvollerweise etwas über die Menge der möglichen Eingaben voraussetzen und dann die probabilistische Analyse als Technik verwenden, um einen effizienten Algorithmus zu entwerfen oder um eine bessere Einsicht in das Problem zu bekommen. Bei anderen Problemen können wir die Verteilung der Eingaben nicht sinnvoll beschreiben. In diesen Fällen können wir die probabilistische Analyse nicht gewinnbringend anwenden. Beim Bewerberproblem können wir annehmen, dass die Kandidaten in zufälliger Reihenfolge eintreffen. Was bedeutet dies für unser Problem? Wir setzen voraus, dass wir je zwei Kandidaten vergleichen können und dann entscheiden können, wer der am besten qualifizierte von beiden ist. Wir setzen also voraus, dass es eine vollständige Ordnung auf der Menge der Kandidaten gibt. (Eine Definition einer vollständigen Ordnung finden Sie in Anhang B.) Wir können also jedem Kandidaten einen eindeutigen Rang zuordnen, d. h. eine eindeutige Zahl zwischen 1 und n. Den Rang eines Bewerbers bezeichnen wir mit rang(i). Wir übernehmen die Konvention, dass je höher der Rang eines Bewerbers, je besser seine Qualifikation. Die geordnete Liste rang(1), rang(2), . . . , rang(n) ist eine Permutation der Liste 1, 2, . . . , n. Die Aussage, dass sich die Bewerber in zufälliger Reihenfolge vorstellen, ist äquivalent zu der Aussage, dass diese Rangliste jede der n! Permutationen der Zahlen 1 bis n mit gleicher Wahrscheinlichkeit annimmt. Alternativ sagen wir, dass die Rangliste eine gleichförmig verteilte zufällige Permutation ist; das bedeutet, dass jede der möglichen n! Permutation mit gleicher Wahrscheinlichkeit vorkommt. Abschnitt 5.2 führt eine probabilistische Analyse des Bewerberproblems durch.
Randomisierte Algorithmen Um die probabilistische Analyse anwenden zu können, müssen wir etwas über die Verteilung der Eingaben wissen. In vielen Fällen wissen wir darüber nur sehr wenig. Selbst wenn wir etwas über die Verteilung wissen, sind wir mitunter nicht in der Lage, dieses Wissen zu modellieren. Dennoch können wir Wahrscheinlichkeit und Zufälligkeit häufig als Werkzeug für den Entwurf und die Analyse einsetzen, indem wir einen Teil des Verhaltens eines Algorithmus randomisieren. Beim Bewerberproblem scheint es so, als ob sich die Kandidaten in einer zufälligen Reihenfolge vorstellen. Wir haben jedoch keine Möglichkeit zu erfahren, ob dies tatsächlich der Fall ist. Um also einen randomisierten Algorithmus für das Bewerberproblem zu entwickeln, benötigen wir eine größere Kontrolle über die Reihenfolge, in der wir die Kandidaten interviewen. Wir werden daher das Modell ein wenig modifizieren. Wir nehmen an, dass die Vermittlungsagentur n Kandidaten hat und uns im Voraus eine Liste mit den Kandidaten schickt. An jedem Tag wählen wir per Zufall aus, welchen Kandidaten wir interviewen wollen. Obwohl wir nichts über die Kandidaten wissen (außer ihren Namen), haben wir nun eine signifikant geänderte Situation. Anstatt uns auf die Vermutung zu verlassen, dass die Kandidaten in zufälliger Reihenfolge kommen, haben wir Kontrolle über diesen Prozess gewonnen und eine zufällige Reihenfolge erzwungen. Wir nennen einen Algorithmus randomisiert, wenn dessen Verhalten nicht alleine durch die Eingabe bestimmt wird, sondern auch durch Werte, die von einem Zufalls-
118
5 Probabilistische Analyse und randomisierte Algorithmen
zahlengenerator erzeugt werden. Wir wollen annehmen, dass wir einen Zufallszahlengenerator Random zur Verfügung haben. Ein Aufruf von Random(a, b) gibt eine ganze Zahl zwischen a und b (a und b inklusive) zurück, wobei jede dieser Zahlen gleichwahrscheinlich ist. Beispielsweise erzeugt Random(0, 1) mit Wahrscheinlichkeit 1/2 eine 0 und mit Wahrscheinlichkeit 1/2 eine 1. Ein Aufruf von Random(3, 7) gibt entweder 3, 4, 5, 6 oder 7 zurück, jeweils mit der Wahrscheinlichkeit 1/5. Jede Zahl, die von Random erzeugt wird, ist unabhängig von den Zahlen, die bei vorherigen Aufrufen erzeugt wurden. Sie können sich Random als (b − a + 1)-seitigen Würfel vorstellen. (In der Praxis bieten die meisten Programmierumgebungen einen Pseudo-Zufallszahlengenerator an: einen deterministischen Algorithmus, der Werte zurückgibt, die zufällig verteilt „aussehen“.) Bei der Analyse der Laufzeit eines randomisierten Algorithmus nehmen wir den Erwartungswert der Laufzeit über die Verteilung der Werte, die durch den Zufallszahlengenerator erzeugt werden. Wir unterscheiden diese Algorithmen von solchen, bei denen die Eingabe zufällig ist, indem wir die Laufzeit eines randomisierten Algorithmus erwartete Laufzeit nennen. Wir diskutieren also mittlere Laufzeiten, wenn die Wahrscheinlichkeitsverteilung über den Eingaben des Algorithmus definiert ist, und wir diskutieren erwartete Laufzeiten, wenn der Algorithmus selbst zufällige Entscheidungen trifft.
Übungen 5.1-1 Zeigen Sie, dass die Annahme, dass wir in Zeile 4 der Prozedur Hire-Assistant immer fähig sind, zu entscheiden, welcher der Kandidaten der beste ist, impliziert, dass wir die totale Ordnung der Ränge der Kandidaten kennen. 5.1-2∗Geben Sie eine Implementierung der Prozedur Random(a, b) an, die nur Aufrufe von Random(0, 1) ausführt. Wie hoch ist die erwartete Laufzeit Ihrer Prozedur als Funktion von a und b? 5.1-3∗ Nehmen Sie an, Sie wollten die Ausgaben 0 und 1 jeweils mit Wahrscheinlichkeit 1/2 erhalten. Ihnen steht eine Prozedur Biased-Random zur Verfügung, die jeweils entweder 0 oder 1 ausgibt. Die Ausgabe 1 tritt mit Wahrscheinlichkeit p und die Ausgabe 0 mit Wahrscheinlichkeit 1 − p auf, wobei 0 < p < 1 gilt. Sie wissen aber nicht, wie groß p ist. Geben Sie einen Algorithmus an, der Biased-Random als Unterroutine verwendet und die Werte 0 und 1 mit Wahrscheinlichkeit 1/2 zurückgibt. Wie groß ist die erwartete Laufzeit für ihren Algorithmus als Funktion von p?
5.2
Indikatorfunktionen
Bei der Analyse vieler Algorithmen, einschließlich des Bewerberproblems, werden wir Indikatorfunktionen einsetzen. Indikatorfunktionen bilden eine geeignete Methode für den Übergang zwischen Wahrscheinlichkeiten und Erwartungswerten. Wir setzen voraus, dass wir einen Wahrscheinlichkeitsraum S und ein Ereignis A gegeben haben. Dann
5.2 Indikatorfunktionen ist die mit dem Ereignis A verknüpfte Indikatorfunktion I {A} durch 1 falls A eintritt , I {A} = 0 falls A nicht eintritt
119
(5.1)
definiert. Als einfaches Beispiel bestimmen wir die erwartete Anzahl für das Ereignis, dass wir beim Werfen einer fairen Münze „Kopf“ erhalten. Unser Wahrscheinlichkeitsraum ist S = {K, Z} mit Pr {K} = Pr {Z} = 1/2. Dann können wir eine Indikatorfunktion XK definieren, die mit dem Auftreten des Ereignisses „Kopf“, also mit K verknüpft ist. Diese Variable gibt an, ob wir beim Wurf das Ergebnis Kopf erhalten haben. Ihr Wert ist 1, wenn die Münze Kopf zeigt, und 0 sonst. Wir schreiben XK = I {K} 1 wenn K auftritt , = 0 wenn Z auftritt . Die erwartete Anzahl des beim Werfen der Münze auftretenden Ereignisses Kopf ist einfach der Erwartungswert der Indikatorfunktion XK : E [XK ] = E [I {K}] = 1 · Pr {K} + 0 · Pr {Z} = 1 · (1/2) + 0 · (1/2) = 1/2 . Somit ist die erwartete Anzahl, mit der das Ereignis „Kopf“ bei einem Wurf einer fairen Münze auftritt, 1/2. Wie das folgende Lemma zeigt, ist der Erwartungswert einer mit einem Ereignis A verknüpften Indikatorfunktion, gleich der Wahrscheinlichkeit, dass A eintritt. Lemma 5.1 Gegeben seien ein Wahrscheinlichkeitsraum S und ein Ereignis A im Wahrscheinlichkeitsraum S. Es sei XA = I {A}. Dann gilt E [XA ] = Pr {A}. Beweis: Nach der Definition der Indikatorfunktion in Gleichung (5.1) und der Definition des Erwartungswertes gilt E [XA ] = E [I {A}]
= 1 · Pr {A} + 0 · Pr A = Pr {A} ,
wobei A das Komplement von A, also S − A bezeichnet.
Für eine Anwendung, wie das Zählen der erwarteten Anzahl der Ereignisse Kopf beim Münzwurf, mag die Verwendung der Indikatorfunktion etwas umständlich erscheinen.
120
5 Probabilistische Analyse und randomisierte Algorithmen
Aber sie ist sehr nützlich für die Analyse von Situationen, in denen wir wiederholt zufällige Versuche ausführen. Zum Beispiel bietet uns die Indikatorfunktion eine einfache Möglichkeit, das Resultat der Gleichung (C.37) auf anderem Wege zu erhalten. In dieser Gleichung berechnen wir die Anzahl der Ereignisse Kopf bei n Münzwürfen, indem wir die Wahrscheinlichkeit 0-mal Kopf, 1-mal Kopf, 2-mal Kopf usw. separat betrachten. Die in Gleichung (C.38) vorgeschlagene Methode, die einfacher ist, verwendet stattdessen implizit die Indikatorfunktionen. Wir wollen dieses Argument noch weiter verdeutlichen. Sei Xi eine Indikatorfunktion, die mit dem Ereignis verknüpft ist, dass im i-ten Wurf Kopf erscheint: Xi = I {das Ergebnis des i-ten Wurfes ist K}. Sei X eine Zufallsvariable, die die Gesamtanzahl der Ereignisse Kopf bei n Münzwürfen beschreibt. Es gilt X=
n
Xi .
i=1
Wir möchten die erwartete Anzahl der Ereignisse Kopf berechnen. Dazu bilden wir auf beiden Seiten der Gleichung den Erwartungswert, wodurch wir
E [X] = E
" n
# Xi
i=1
erhalten. Die obige Gleichung gibt den Erwartungswert der Summe von n Indikatorfunktionen an. Mithilfe von Lemma 5.1 können wir den Erwartungswert von jeder der Zufallsvariablen einfach berechnen. Mit Gleichung (C.21) – der Linearität des Erwartungswertes – lässt sich der Erwartungswert der Summe einfach ausrechnen: Er ist gleich der Summe der Erwartungswerte der n Zufallsvariablen. Die Linearität des Erwartungswertes macht die Verwendung der Indikatorfunktion zu einem leistungsfähigen analytischen Verfahren. Es ist sogar dann anwendbar, wenn es eine Abhängigkeit unter den Zufallsvariablen gibt. Nun können wir die erwartete Anzahl der Ereignisse Kopf einfach berechnen: E [X] = E
" n
# Xi
i=1
= =
n i=1 n
E [Xi ] 1/2
i=1
= n/2 . Im Vergleich zu der in Gleichung (C.37) verwendeten Methode vereinfacht die Indikatorfunktion die Berechnung außerordentlich. Wir werden die Indikatorfunktion an vielen Stellen im Buch anwenden.
5.2 Indikatorfunktionen
121
Analyse des Bewerberproblems mithilfe der Indikatorfunktion Kehren wir zum Bewerberproblem zurück. Wir wollen nun den Erwartungswert für die Anzahl der Einstellungen neuer Bewerber berechnen. Um die probabilistische Analyse anwenden zu können, setzen wir – wie im vorigen Abschnitt diskutiert – voraus, dass die Kandidaten in zufälliger Reihenfolge eintreffen. (Wir werden in Abschnitt 5.3 sehen, wie wir diese Annahme umgehen können.) Sei X die Zufallsvariable, die beschreibt, wie oft wir einen neuen Bewerber einstellen. Wir können nun die Definition des Erwartungswertes aus Gleichung (C.20) anwenden, um E [X] =
n
x Pr {X = x}
x=1
zu erhalten. Diese Berechnung wäre aber umständlich. Wir werden stattdessen Indikatorfunktionen anwenden, um die Rechnung wesentlich zu vereinfachen. Statt E [X] zu berechnen, indem wir eine Variable definieren, die beschreibt, wie oft wir einen neuen Bewerber einstellen, definieren wir n Indikatorfunktionen, die angeben, ob ein bestimmter Bewerber angestellt wird. Wir definieren Xi als die Indikatorfunktion, die mit dem Ereignis, dass der i-te Kandidat eingestellt wird, verbunden ist. Somit gilt Xi = I {Kandidat i wird eingestellt} 1 wenn Kandidat i eingestellt wird , = 0 wenn Kandidat i nicht eingestellt wird und X = X1 + X2 + · · · + Xn .
(5.2)
Mit Lemma 5.1 ergibt sich E [Xi ] = Pr {Kandidat i wird eingestellt} . Wir müssen also die Wahrscheinlichkeit dafür berechnen, dass die Zeilen 5–6 in HireAssistant ausgeführt werden. Kandidat i wird in Zeile 5 genau dann eingestellt, wenn er besser ist, als alle Kandidaten von 1 bis i−1. Da wir vorausgesetzt haben, dass die Kandidaten in zufälliger Reihenfolge eintreffen, sind insbesondere auch die ersten i Kandidaten in zufälliger Reihenfolge erschienen. Jeder der ersten i Kandidaten hat die gleiche Wahrscheinlichkeit, der am besten qualifizierte zu sein. Für Kandidat i beträgt die Wahrscheinlichkeit 1/i, besser qualifiziert zu sein als die Kandidaten 1 bis i − 1. Damit ist die Wahrscheinlichkeit, dass er eingestellt wird ebenfalls 1/i. Aus Lemma 5.1 schließen wir, dass E [Xi ] = 1/i
(5.3)
122
5 Probabilistische Analyse und randomisierte Algorithmen
gilt. Nun können wir E [X] berechnen: " n # E [X] = E Xi (wegen Gleichung (5.2))
(5.4)
i=1
= =
n i=1 n
E [Xi ]
(wegen der Linearität des Erwartungswertes)
1/i
(wegen Gleichung (5.3))
i=1
= ln n + O(1)
(wegen Gleichung (A.7)) .
(5.5)
Selbst wenn wir n Kandidaten interviewen, stellen wir im Mittel nur etwa ln n von ihnen ein. Wir fassen dieses Resultat im folgenden Lemma zusammen. Lemma 5.2 Wenn die Kandidaten in zufälliger Reihenfolge präsentiert werden, hat der Algorithmus Hire-Assistant mittlere Gesamteinstellungskosten in Höhe von O(ch ln n). Beweis: Die Schranke folgt unmittelbar aus unserer Definition der Anstellungskosten und Gleichung (5.5), die zeigt, dass die erwartete Anzahl von Einstellungen ungefähr ln n ist. Die mittleren Einstellungskosten stellen eine signifikante Verbesserung gegenüber den Einstellungskosten im schlechtesten Fall, die bei O(ch n) lagen, dar.
Übungen 5.2-1 In der Prozedur Hire-Assistant setzen wir voraus, dass die Kandidaten in zufälliger Reihenfolge präsentiert werden. Wie hoch ist die Wahrscheinlichkeit, dass Sie genau eine Einstellung durchführen? Wie hoch ist die Wahrscheinlichkeit, dass Sie genau n Einstellungen durchführen? 5.2-2 In der Prozedur Hire-Assistant setzen wir voraus, dass die Kandidaten in zufälliger Reihenfolge präsentiert werden. Wie hoch ist die Wahrscheinlichkeit, dass Sie genau zwei Einstellungen durchführen? 5.2-3 Wenden Sie Indikatorfunktionen an, um den Erwartungswert der Summe der Augen zu berechnen, wenn Sie n-mal würfeln. 5.2-4 Wenden Sie Indikatorfunktionen an, um das folgende Problem zu lösen, das als Hut-Problem bekannt ist: Jeder von n Besuchern eines Restaurants gibt seinen Hut in der Garderode ab. Die Garderobenfrau gibt jedem Besucher in zufälliger Reihenfolge einen Hut zurück. Wie hoch ist die erwartete Anzahl von Besuchern, die ihren eigenen Hut zurückbekommen?
5.3 Randomisierte Algorithmen
123
5.2-5 Sei A[1 . . n] ein Feld mit n paarweise verschiedenen Zahlen. Wenn i < j und A[i] > A[j] gilt, dann wird das Paar (i, j) als Inversion von A bezeichnet. (Mehr Beispiele für Inversionen finden Sie in Problemstellung 2-4.) Nehmen Sie an, dass die Elemente von A eine gleichmäßig verteilte zufällige Permutation von 1, 2, . . . , n bilden. Wenden Sie Indikatorfunktionen an, um die erwartete Anzahl von Inversionen zu berechnen.
5.3
Randomisierte Algorithmen
Im vorangegangenen Abschnitt haben wir gezeigt, wie uns die Kenntnis der Verteilung der Eingaben helfen kann, das Verhalten eines Algorithmus im Mittel zu analysieren. Häufig steht uns dieses Wissen nicht zur Verfügung und die Analyse des mittleren Falls ist nicht möglich. Wie bereits in Abschnitt 5.1 erwähnt, sind wir aber möglicherweise in der Lage, einen randomisierten Algorithmus zu verwenden. Für ein Problem wie das Bewerberproblem ist es hilfreich anzunehmen, dass alle Eingaben gleichwahrscheinlich sind. Eine probabilistische Analyse kann die Entwicklung eines randomisierten Algorithmus leiten. Anstatt eine Verteilung der Eingaben anzunehmen, erlegen wir ihnen eine Verteilung auf. In unserem speziellen Beispiel permutieren wir die Kandidaten in zufälliger Art und Weise, bevor wir den eigentlichen Algorithmus starten, um die Eigenschaft zu erzwingen, dass alle Permutationen gleichwahrscheinlich sind. Obwohl wir den Algorithmus modifiziert haben, erwarten wie immer noch, dass wir ungefähr ln n Einstellungen durchführen. Aber jetzt erwarten wir, dass dies für jede Eingabe der Fall ist und nicht nur für Eingaben, die einer bestimmten Verteilung unterliegen. Lassen Sie uns nun den Unterschied zwischen probabilistischer Analyse und randomisierten Algorithmen weiter ausführen. Im Abschnitt 5.2 haben wir behauptet, dass die erwartete Anzahl, wie häufig wir einen neuen Bewerber einstellen, etwa den Wert ln n hat, wenn die Kandidaten in zufälliger Reihenfolge präsentiert werden. Beachten Sie, dass hier der Algorithmus deterministisch ist; für eine bestimmte Eingabe wird die Anzahl, wie oft wir einen neuen Bewerber einstellen, immer gleich sein. Darüber hinaus wird sich die Anzahl, wie oft wir einen neuen Bewerber einstellen, für verschiedene Eingaben unterscheiden. Sie hängt vom Rang der einzelnen Bewerber ab. Da diese Anzahl nur vom Rang der Kandidaten abhängt, können wir jede Eingabe darstellen, indem wir die Ränge der Kandidaten in der Reihenfolge ihres Erscheinens auflisten, d. h. als rang(1), rang(2), . . . , rang(n). Erscheinen die Bewerber in der durch die Rangliste A1 = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 gegebenen Reihenfolge, werden wir zehnmal einen neuen Bewerber einstellen, da jeder nachfolgende Kandidat besser als der vorhergehende ist und die Zeilen 5–6 bei jeder Iteration des Algorithmus ausgeführt werden. Ist die Rangliste A2 = 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 gegeben, dann wird nur einmal ein neuer Bewerber eingestellt und zwar in der ersten Iteration. Für die Rangliste A3 = 5, 2, 1, 8, 4, 7, 10, 9, 3, 6 wird dreimal ein neuer Bewerber eingestellt, nämlich dann, wenn die Kanditaten mit den Rängen 5, 8 und 10 interviewt werden. Erinnern wir uns daran, dass die Kosten unseres Algorithmus davon abhängen, wie häufig wir einen neuen Bewerber einstellen. Wir sehen, dass es kostspielige Eingaben, wie zum Beispiel A1 , kostengünstige Eingaben, wie zum Beispiel A2 , und moderat teure Eingaben,
124
5 Probabilistische Analyse und randomisierte Algorithmen
wie zum Beispiel A3 , gibt. Betrachten wir andererseits den randomisierten Algorithmus, der zuerst die Kandidaten permutiert und dann den besten Kandidaten bestimmt. Hier randomisieren wir also im Algorithmus und nicht über die Eingabeverteilung. Für eine gegebene Eingabe, zum Beispiel die obige Eingabe A3 , können wir nicht sagen, wie oft das Maximum aktualisiert wird, weil diese Größe bei den einzelnen Durchläufen des Algorithmus differiert. Beim ersten Durchlauf des Algorithmus kann die Permutation A1 erzeugt werden und es werden 10 Aktualisierungen durchgeführt, während beim zweiten Durchlauf des Algorithmus die Permutation A2 erzeugt werden kann und nur eine Aktualisierung durchgeführt wird. Beim dritten Durchlauf kann es sein, dass wir irgendeine andere Anzahl von Aktualisierungen durchführen müssen. Bei jedem Durchlauf des Algorithmus hängt die Ausführung von der zufällig getroffenen Wahl ab und unterscheidet sich wahrscheinlich von der vorhergehenden Ausführung des Algorithmus. Für diesen Algorithmus und viele andere randomisierte Algorithmen gilt: Es gibt keine spezielle Eingabe, die sein Verhalten im schlechtesten Fall auslöst. Nicht einmal Ihr schlimmster Feind kann ein schlechtes Eingabefeld erzeugen, da die zufälligen Permutationen die in der Eingabe gegebene Reihenfolge irrelevant machen. Der randomisierte Algorithmus verhält sich nur dann schlecht, wenn der Zufallszahlengenerator eine „unglückliche“ Permutation erzeugt. Für das Bewerberproblem besteht die einzige Veränderung, die wir im Code machen müssen, darin, das Feld zufällig zu permutieren. Randomized-Hire-Assistant(n) 1 permutiere zufällig die Liste der Kandidaten 2 bester = 0 // Kandidat 0 ist der am wenigsten qualifizierte Dummy-Kandidat 3 for i = 1 to n 4 interviewe Kandidat i 5 if Kandidat i ist besser als Kandidat bester 6 bester = i 7 stelle Kandidat i ein Mit dieser einfachen Veränderung haben wir einen randomisierten Algorithmus erzeugt, dessen Verhalten demjenigen entspricht, das wir erhalten, wenn sich die Kandidaten in einer zufälligen Reihenfolge vorstellen würden. Lemma 5.3 Die erwarteten Einstellungskosten aus der Prozedur Randomized-Hire-Assistant sind von der Ordnung O(ch ln n). Beweis: Nach der Permutation des Feldes haben wir eine Situation erreicht, die identisch zu der probabilistischen Analyse der Prozedur Hire-Assistant ist.
5.3 Randomisierte Algorithmen
125
Vergleicht man die Lemmata 5.2 und 5.3, so sieht man den Unterschied zwischen probabilistischer Analyse und randomisierten Algorithmen. In Lemma 5.2 treffen wir eine Annahme bezüglich der Eingabe. In Lemma 5.3 treffen wir keine solche Annahme, wenngleich die Randomisierung der Eingabe zusätzliche Zeit benötigt. Um konsistent mit unserer Terminologie zu bleiben, sprechen wir in Lemma 5.2 von mittleren Einstellungskosten und in Lemma 5.3 von erwarteten Einstellungskosten. Im Rest dieses Abschnitts diskutieren wir einige Fragen, die in Verbindung mit der zufälligen Permutation der Eingaben auftreten.
Zufällige Permutation von Feldern Viele randomisierte Algorithmen randomisieren die Eingabe, indem sie ein gegebenes Eingabefeld permutieren. (Es gibt auch andere Möglichkeiten, die Randomisierung vorzunehmen.) Hier werden wir zwei Methoden diskutieren, dies zu tun. Wir setzen voraus, dass uns ein Feld A gegeben ist, das ohne Beschränkung der Allgemeinheit die Elemente 1 bis n enthält. Unsere Absicht ist es, eine zufällige Permutation des Feldes zu erzeugen. Eine häufig angewendete Methode besteht darin, jedem Element A[i] des Feldes eine zufällige Priorität P [i] zuzuordnen und dann die Elemente von A hinsichtlich dieser Priorität zu ordnen. Wenn unser Eingabefeld zum Beispiel A = 1, 2, 3, 4 ist und wir die zufälligen Prioritäten P = 36, 3, 62, 19 auswählen, dann erzeugen wir ein Feld B = 2, 4, 1, 3, weil die zweite Priorität die kleinste ist, gefolgt von der vierten, dann der ersten und schließlich der dritten. Wir nennen diese Prozedur Permute-By-Sorting: Permute-By-Sorting(A) 1 n = A.l¨a nge 2 sei P [1 . . n] ein neues Feld 3 for i = 1 to n 4 P [i] = Random(1, n3 ) 5 sortiere A unter Verwendung von P als Sortierschlüssel Zeile 4 wählt eine Zufallszahl zwischen 1 und n3 . Wir verwenden einen Bereich von 1 bis n3 , damit es wahrscheinlich ist, dass alle Prioritäten in P eindeutig sind. (In Übung 5.3-5 sollen Sie beweisen, dass die Wahrscheinlichkeit, dass alle Einträge eindeutig sind, mindestens 1 − 1/n beträgt. Übung 5.3-6 fragt danach, wie der Algorithmus zu implementieren ist, wenn zwei oder mehr Prioritäten identisch sind.) Lassen Sie uns annehmen, dass alle Prioritäten eindeutig sind. Der zeitaufwendigste Schritt der Prozedur ist das Sortieren in Zeile 5. Wie wir in Kapitel 8 sehen werden, benötigt das Sortieren Zeit Ω(n lg n), wenn wir ein vergleichendes Sortierverfahren verwenden. Wir können diese untere Schranke erreichen, da wir bereits wissen, dass Sortieren durch Mischen Zeit Θ(n lg n) benötigt. (Wir werden in Teil II weitere vergleichende Sortieralgorithmen kennenlernen, deren Laufzeiten in Θ(n lg n) liegen. Übung 8.3-4 verlangt von Ihnen, das sehr ähnliche Problem, Zahlen aus dem Bereich 0 bis n3 − 1 zu sortieren, in Zeit O(n) zu lösen.) Nach dem Sortieren wird A[i] an der Position j der Ausgabe zu finden sein, wenn P [i] die j-kleinste Priorität ist.
126
5 Probabilistische Analyse und randomisierte Algorithmen
Auf diese Weise erhalten wir eine Permutation. Es bleibt zu beweisen, dass diese Prozedur eine gleichförmig verteilte zufällige Permutation erzeugt, d. h. dass jede Permutation der Zahlen 1 bis n mit gleicher Wahrscheinlichkeit erzeugt wird. Lemma 5.4 Die Prozedur Permute-by-Sorting erzeugt eine gleichförmig verteilte zufällige Permutation der Eingabe, falls alle Prioritäten verschieden sind. Beweis: Wir beginnen damit, eine spezielle Permutation zu betrachten, in der für alle i das Element A[i] die i-kleinste Priorität erhält. Wir werden zeigen, dass diese Permutation genau mit der Wahrscheinlichkeit 1/n! auftritt. Für i = 1, 2, . . . , n sei Ei das Ereignis, dass das Element A[i] die i-kleinste Priorität erhält. Wir wollen die Wahrscheinlichkeit dafür berechnen, dass für alle i das Ereignis Ei eintritt. Sie ist durch Pr {E1 ∩ E2 ∩ E3 ∩ · · · ∩ En−1 ∩ En } . gegeben. Nach Übung C.2-5 ist diese Wahrscheinlichkeit gleich Pr {E1 } · Pr {E2 | E1 } · Pr {E3 | E2 ∩ E1 } · Pr {E4 | E3 ∩ E2 ∩ E1 } · · · Pr {Ei | Ei−1 ∩ Ei−2 ∩ · · · ∩ E1 } · · · Pr {En | En−1 ∩ · · · ∩ E1 } . Es gilt Pr {E1 } = 1/n, da dies die Wahrscheinlichkeit dafür ist, dass eine zufällig gewählte Priorität aus einer Menge von n Prioritäten die kleinste Priorität ist. Als nächstes stellen wir fest, dass Pr {E2 | E1 } = 1/(n − 1) gilt. Davon ausgehend, dass Element A[1] die kleinste Priorität hat, ist es für jedes der n − 1 verbleibenden Elemente gleichwahrscheinlich, die zweitkleinste Priorität zu haben. Im Allgemeinen gilt für i = 2, 3, . . . , n Pr {Ei | Ei−1 ∩ Ei−2 ∩ · · · ∩ E1 } = 1/(n − i + 1), denn, wenn wir voraussetzen, dass die Elemente A[1] bis A[i − 1] die i − 1 kleinsten Prioritäten (in der entsprechenden Reihenfolge) haben, dann hat jedes der verbleibenden n − (i − 1) Elemente mit gleicher Wahrscheinlichkeit die i-kleinste Priorität. Somit erhalten wir 1 1 1 1 Pr {E1 ∩ E2 ∩ E3 ∩ · · · ∩ En−1 ∩ En } = ··· n n−1 2 1 1 , = n! und wir haben gezeigt, dass die Wahrscheinlichkeit dafür, die Identität als Permutation zu erhalten, gleich 1/n! ist. Wir können den Beweis so erweitern, dass er für jede beliebige Permutation von Prioritäten greift. Betrachten wir eine beliebige feste Permutation σ = σ(1), σ(2), . . . , σ(n) der Menge {1, 2, . . . , n}. Bezeichnen wir mit ri den Rang der dem Element A[i] zugeordneten Priorität, wobei das Element mit der j-kleinsten Priorität den Rang j hat. Wenn wir Ei als dasjenige Ereignis definieren, in dem das Element A[i] die σ(i)-kleinste Priorität erhält, oder ri = σ(i) gilt, können wir den gleichen Beweis führen. Wenn wir
5.3 Randomisierte Algorithmen
127
die Wahrscheinlichkeit ermitteln, eine bestimmte Permutation zu erhalten, ist deshalb die Berechnung zu der oben gezeigten Wahrscheinlichkeit identisch. Somit ist die Wahrscheinlichkeit, diese Permutation zu erhalten, ebenfalls 1/n!. Sie könnten möglicherweise denken, dass es ausreicht zu zeigen, dass die Wahrscheinlichkeit, dass ein Element A[i] an die Position j kommt, gleich 1/n ist, um zu beweisen, dass eine Permutation eine gleichförmig verteilte zufällige Permutation ist. Übung 5.3-4 zeigt jedoch, dass diese schwächere Bedingung tatsächlich nicht ausreicht. Eine bessere Methode, eine zufällige Permutation zu erzeugen, besteht darin, das gegebene Feld in-place zu permutieren.Die Prozedur Randomize-In-Place macht dies in Zeit O(n). In der i-ten Iteration wählt die Prozedur das Element A[i] zufällig aus den Elementen A[i] bis A[n] aus. Das Element A[i] wird anschließend nicht mehr geändert. Randomize-In-Place(A) 1 n = A.l¨a nge 2 for i = 1 to n 3 vertausche A[i] mit A[Random(i, n)] Wir werden nun eine Schleifeninvariante verwenden, um zu zeigen, dass die Prozedur Randomize-In-Place eine gleichförmig verteilte zufällige Permutation erzeugt. Eine k-Permutation über einer Menge von n Elementen ist eine Folge, die k dieser n Elemente enthält, wobei kein Element zweimal vorkommen darf. (Schauen Sie in Anhang C nach.) Es gibt n!/(n − k)! solcher k-Permutationen. Lemma 5.5 Die Prozedur Randomize-In-Place berechnet eine gleichförmig verteilte zufällige Permutation. Beweis: Wir verwenden die folgende Schleifeninvariante: Für jede mögliche (i − 1)-Permutation über den n Elementen enthält das Teilfeld A[1 . . i−1] unmittelbar vor der i-ten Iteration der for-Schleife in den Zeilen 2–3 diese (i−1)-Permutation mit der Wahrscheinlichkeit (n−i+1)!/n!.
Wir müssen zeigen, dass diese Invariante vor der ersten Schleifeniteration wahr ist, dass jede Iteration der Schleife die Invariante erhält, und dass die Invariante eine nützliche Eigenschaft darstellt, um zu zeigen, dass die Schleife korrekt abbricht. Initialisierung: Betrachten wir die Situation unmittelbar vor der ersten Schleifeniteration, sodass i = 1 gilt. Die Schleifeninvariante legt fest, dass für jede mögliche 0Permutation, das Teilfeld A[1 . . 0] diese 0-Permutation mit der Wahrscheinlichkeit
128
5 Probabilistische Analyse und randomisierte Algorithmen (n − i + 1)!/n! = n!/n! = 1 enthält. Das Teilfeld A[1 . . 0] ist ein leeres Teilfeld und eine 0-Permutation hat keine Elemente. Somit enthält A[1 . . 0] jede 0-Permutation mit der Wahrscheinlichkeit 1 und die Schleifeninvariante ist vor der ersten Iteration erfüllt.
Fortsetzung: Wir nehmen an, dass unmittelbar vor der i-ten Iteration jede mögliche (i − 1)-Permutation im Teilfeld A[1 . . i − 1] mit der Wahrscheinlichkeit (n− i + 1)!/ n! vorkommt, und werden zeigen, dass nach der i-ten Iteration jede mögliche iPermutation im Teilfeld A[1 . . i] mit der Wahrscheinlichkeit (n − i)!/n! vorkommt. Das Inkrementieren von i um 1 erhält die Schleifeninvariante für die nächste Iteration. Lassen Sie uns die i-Iteration näher anschauen. Wir betrachten eine spezielle iPermutation und bezeichnen die Elemente darin mit x1 , x2 , . . . , xi . Diese Permutation besteht aus einer (i − 1)-Permutation gefolgt von dem Wert xi , den der Algorithmus an die Stelle A[i] setzt. Wir bezeichnen mit E1 das Ereignis, dass die ersten i − 1 Iterationen die spezielle, in A[1 . . i − 1] enthaltene (i − 1)-Permutation erzeugt haben. Aus der Schleifeninvariante ergibt sich Pr {E1 } = (n − i + 1)!/n!. Sei E2 das Ereignis, dass bei der i-ten Iteration xi an die Stelle A[i] gesetzt wird. Die i-Permutation x1 , . . . , xi wird genau dann in A[1 . . i] auftreten, wenn sowohl Ereignis E1 als auch Ereignis E2 eintritt, und so wollen wir die Wahrscheinlichkeit Pr {E2 ∩ E1 } berechnen. Unter Verwendung der Gleichung (C.14) erhalten wir Pr {E2 ∩ E1 } = Pr {E2 | E1 } Pr {E1 } . Die Wahrscheinlichkeit Pr {E2 | E1 } ist gleich 1/(n − i + 1), da der Algorithmus in Zeile 3 den Wert xi zufällig aus den n − i + 1 Werten der Positionen A[i . . n] auswählt. Damit ergibt sich Pr {E2 ∩ E1 } = Pr {E2 | E1 } Pr {E1 } =
(n − i + 1)! 1 · n−i+1 n!
=
(n − i)! . n!
Terminierung: Beim Abbruch der Schleife gilt i = n + 1 und das Teilfeld A[1 . . n] enthält eine gegebene n-Permutation mit der Wahrscheinlichkeit (n − (n + 1) + 1)!/n! = 0!/n! = 1/n! . Also erzeugt die Prozedur Randomize-In-Place eine gleichförmig verteilte, zufällige Permutation. Ein randomisierter Algorithmus stellt häufig den einfachsten und effizientesten Weg dar, ein Problem zu lösen. Wir werden randomisierte Algorithmen in diesem Buch gelegentlich verwenden.
5.3 Randomisierte Algorithmen
129
Übungen 5.3-1 Professor Marceau beanstandet die im Beweis von Lemma 5.5 verwendete Schleifeninvariante. Er argumentiert, wir könnten doch einfach festlegen, dass ein leeres Teilfeld keine 0-Permutation enthält. Deshalb sollte die Wahrscheinlichkeit, dass ein leeres Teilfeld eine 0-Permutation enthält, null sein. Damit würde die Schleifenvariante vor der ersten Iteration nicht gelten. Schreiben Sie die Prozedur Randomize-In-Place so um, dass die zugehörige Schleifeninvariante auch vor der ersten Iteration auf ein nichtleeres Teilfeld angewendet wird. Modifizieren Sie für Ihre Prozedur den Beweis von Lemma 5.5. 5.3-2 Professor Kelp beschließt, eine Prozedur zu schreiben, die zufällig eine von der Identität verschiedene Permutation erzeugt. Er schlägt die folgende Prozedur vor: Permute-Without-Identity(A) 1 n = A.l¨a nge 2 for i = 1 to n − 1 3 vertausche A[i] mit A[Random(i + 1, n)] Tut dieser Code das, was Professor Kelp beabsichtigt? 5.3-3 Nehmen Sie an, dass wir, anstatt das Element A[i] mit einem zufällig gewählten Element aus dem Teilfeld A[i . . n] zu vertauschen, das Element A[i] mit einem aus dem gesamten Feld zufällig gewählten Element vertauschen: Permute-With-All(A) 1 n = A.l¨a nge 2 for i = 1 to n 3 vertausche A[i] mit A[Random(1, n)] Erzeugt dieser Code eine gleichförmig verteilte zufällige Permutation? Weshalb oder weshalb nicht? 5.3-4 Professor Armstrong schlägt die folgende Prozedur vor, um eine gleichförmig verteilte zufällige Permutation zu generieren: Permute-By-Cyclic(A) 1 n = A.l¨a nge 2 sei B[1 . . n] ein neues Feld 3 offset = Random(1, n) 4 for i = 1 to n 5 ziel = i + offset 6 if ziel > n 7 ziel = ziel − n 8 B[ziel ] = A[i] 9 return B
130
5 Probabilistische Analyse und randomisierte Algorithmen Zeigen Sie, dass das Element A[i] mit der Wahrscheinlichkeit 1/n an eine bestimmte Stelle in B gelangt. Weisen Sie dann nach, dass Professor Armstrong einen Fehler gemacht hat, indem Sie zeigen, dass die resultierende Permutation nicht gleichförmig zufällig verteilt ist.
5.3-5∗ Beweisen Sie, dass für das Feld P in der Prozedur Permute-By-Sorting die Wahrscheinlichkeit, dass alle Elemente eindeutig sind, mindestens 1 − 1/n ist. 5.3-6 Erklären Sie, wie Sie den Algorithmus Permute-By-Sorting implementieren müssen, um den Fall behandeln zu können, dass zwei oder mehr Prioritäten identisch sind. Ihr Algorithmus sollte also eine gleichförmig verteilte zufällige Permutation erzeugen, auch wenn zwei oder mehr Prioritäten identisch sind. 5.3-7 Nehmen Sie an, wir wollten eine zufällige Stichprobe aus {1, 2, 3, . . . , n} entnehmen, d. h. eine m-elementige Teilmenge S mit 0 ≤ m ≤ n, sodass jede m-elementige Teilmenge mit der gleichen Wahrscheinlichkeit erzeugt wird. Eine Methode, dies zu tun, besteht darin, A[i] = i für i = 1, 2, 3, . . . , n zu setzen, Randomize-In-Place(A) aufzurufen und dann einfach die ersten m Elemente des Feldes zu nehmen. Diese Methode würde die Random-Prozedur n-mal aufrufen. Ist n viel größer als m, können wir eine zufällige Stichprobe mit weniger Aufrufen von Random erzeugen. Zeigen Sie, dass die folgende Prozedur mit nur m Aufrufen von Random eine zufällige m-elementige Teilmenge S von {1, 2, 3, . . . , n} berechnet, wobei jede m-elementige Teilmenge mit der gleichen Wahrscheinlichkeit durch die Prozedur erzeugt wird: Random-Sample(m, n) 1 if m = = 0 2 return ∅ 3 else S = Random-Sample(m − 1, n − 1) 4 i = Random(1, n) 5 if i ∈ S 6 S = S ∪ {n} 7 else S = S ∪ {i} 8 return S
∗ 5.4
Probabilistische Analyse und mehr zur Verwendung der Indikatorfunktion
Dieser vertiefende Abschnitt veranschaulicht die probabilistische Analyse anhand von vier Beispielen. Das erste Beispiel bestimmt die Wahrscheinlichkeit, dass es unter k in einem Raum anwesenden Personen zwei Personen gibt, die am gleichen Tag Geburtstag haben. Das zweite Beispiel schaut sich näher an, was passiert, wenn man Bälle zufällig in Urnen wirft. Das dritte Beispiel untersucht Glückssträhnen beim Werfen einer Münze, d. h. dass mehrfach hintereinander „Kopf“ geworfen wird. Abschließend analysieren wir eine Variante des Bewerberproblems, bei der Entscheidungen getroffen werden müssen, ohne tatsächlich alle Kandidaten interviewt zu haben.
5.4 ∗ Probabilistische Analyse und mehr zur Verwendung der Indikatorfunktion
5.4.1
131
Das Geburtstagsparadoxon
Unser erstes Beispiel ist das Geburtstagsparadoxon: Wie viele Personen müssen in einem Raum anwesend sein, damit es eine Wahrscheinlichkeit von 50 % gibt, dass zwei von ihnen am gleichen Tag des Jahres Geburtstag haben? Die Antwort ist, dass erstaunlich wenige Personen im Raum sein müssen. Paradoxerweise sind es weit weniger, als ein Jahr Tage hat; wie wir sehen werden, sogar weit weniger als die Hälfte der Tage eines Jahres. Um die Frage zu beantworten, ordnen wir jeder Person im Raum eine ganzzahligen Zahl 1, 2, . . . , k als Index zu, wobei k die Anzahl der Personen ist. Wir vernachlässigen die Tatsache, dass es Schaltjahre gibt und nehmen an, dass jedes Jahr n = 365 Tage hat. Für i = 1, 2, . . . , k sei bi der Tag des Jahres, an dem Person i Geburtstag hat, wobei 1 ≤ bi ≤ n gilt. Wir nehmen an, dass die Geburtstage über das Jahr gleichverteilt sind, sodass Pr {bi = r} = 1/n für i = 1, 2, . . . , k und r = 1, 2, . . . , n gilt. Die Wahrscheinlichkeit, dass zwei gegebene Personen i und j am gleichen Tag Geburtstag haben, hängt davon ab, ob die zufällige Auswahl von Geburtstagen unabhängig ist. Wir wollen die Unabhängigkeit im Folgenden voraussetzen. Somit ist die Wahrscheinlichkeit, dass die Geburtstage von i und j beide auf den Tag r fallen Pr {bi = r und bj = r} = Pr {bi = r} Pr {bj = r} = 1/n2 . Die Wahrscheinlichkeit, dass beide am gleichen Tag Geburtstag haben, ist folglich Pr {bi = bj } = =
n r=1 n
Pr {bi = r und bj = r} (1/n2 )
r=1
= 1/n .
(5.6)
Einfacher formuliert, wenn bi einmal gewählt ist, ist die Wahrscheinlichkeit, dass bj auf den gleichen Tag fällt, 1/n. Die Wahrscheinlichkeit, dass i und j am gleichen Tag Geburtstag haben, ist also genauso groß wie die Wahrscheinlichkeit, dass der Geburtstag von einer der beiden Personen auf einen gegebenen Tag fällt. Man beachte jedoch, dass diese Koinzidenz nur unter der Voraussetzung der Unabhängigkeit der Geburtstage gilt. Wir können nun die Wahrscheinlichkeit analysieren, dass wenigstens zwei von k Personen am gleichen Tag Geburtstag haben, indem wir das komplementäre Ereignis betrachten. Die Wahrscheinlichkeit, dass wenigstens zwei Geburtstage übereinanderfallen, ist 1 minus die Wahrscheinlichkeit, dass alle Geburtstage paarweise verschieden sind. Das Ereignis, dass k Personen verschiedene Geburtstage haben, ist Bk =
k $ i=1
Ai .
132
5 Probabilistische Analyse und randomisierte Algorithmen
Hierbei ist Ai das Ereignis, dass der Geburtstag von Person i für alle j < i verschieden ist von dem der Person j. Da Bk = Ak ∩ Bk−1 gilt, erhalten wir aus Gleichung (C.16) die Rekursionsgleichung Pr {Bk } = Pr {Bk−1 } Pr {Ak | Bk−1 } ,
(5.7)
wobei wir Pr {B1 } = Pr {A1 } = 1 als Anfangsbedingung wählen. Mit anderen Worten, die Wahrscheinlichkeit, dass b1 , b2 , . . . , bk paarweise verschieden sind, ist gleich der Wahrscheinlichkeit, dass bk = bi für i = 1, 2, . . . , k − 1 gilt, unter der Bedingung, dass b1 , b2 , . . . , bk−1 paarweise verschieden sind. Wenn b1 , b2 , . . . , bk−1 paarweise verschieden sind, ist die bedingte Wahrscheinlichkeit, dass bk = bi für i = 1, 2, . . . , k − 1 gilt, gegeben durch Pr {Ak | Bk−1 } = (n − k + 1)/n, denn unter den n Tagen des Jahres gibt es n − (k − 1), die noch nicht gewählt wurden. Durch Iteration der Gleichung (5.7) erhalten wir Pr {Bk } = Pr {Bk−1 } Pr {Ak | Bk−1 } = Pr {Bk−2 } Pr {Ak−1 | Bk−2 } Pr {Ak | Bk−1 } .. . = Pr {B1 } Pr {A2 | B1 } Pr {A3 | B2 } · · · Pr {Ak | Bk−1 } n−1 n−2 n−k+1 = 1· ··· n n n 2 k−1 1 1− ··· 1 − . = 1· 1− n n n Aus Ungleichung (3.12), 1 + x ≤ ex , folgt Pr {Bk } ≤ e−1/n e−2/n · · · e−(k−1)/n k−1
= e− i=1 i/n = e−k(k−1)/2n ≤ 1/2 falls −k (k − 1)/(2 n) ≤ ln(1/2) gilt. Die Wahrscheinlichkeit, dass alle k Geburtstage paarweise verschieden sind, ist also höchstens 1/2, wenn k (k − 1) ≥ 2 n ln 2, oder, nach Auflösung der quadratischen Gleichung, k ≥ (1 + 1 + (8 ln 2) n)/2 gilt. Für n = 365 muss k ≥ 23 gelten. Wenn also wenigstens 23 Personen anwesend sind, ist die Wahrscheinlichkeit größer oder gleich 1/2, dass mindestens zwei von ihnen am gleichen Tag Geburtstag haben. Auf dem Mars ist ein Jahr 669 Tage lang; es sind deshalb 31 Marsmenschen nötig, um den gleichen Effekt zu erreichen. Analyse mithilfe von Indikatorfunktionen. Wir können Indikatorfunktionen verwenden, um eine einfachere, wenn auch nur approximative Analyse des Geburtstagsparadoxons zu erhalten. Für jedes Paar (i, j) der k anwesenden Personen (1 ≤ i < j ≤ k)
5.4 ∗ Probabilistische Analyse und mehr zur Verwendung der Indikatorfunktion
133
definieren wir die Indikatorfunktion Xij = I {Person i und Person j haben am gleichen Tag Geburtstag} 1 falls Person i und Person j am gleichen Tag Geburtstag haben, = 0 sonst . Nach Gleichung (5.6) ist die Wahrscheinlichkeit, dass zwei Personen am gleichen Tag Geburtstag haben, 1/n. Aus Lemma 5.1 folgt daher E [Xij ] = Pr {Person i und Person j haben am gleichen Tag Geburtstag} = 1/n . Bezeichnen wir mit X die Zufallsgröße, die die Anzahl der Paare zählt, die am gleichen Tag Geburtstag haben, dann ist X=
k−1
k
Xij .
i=1 j=i+1
Bilden wir den Erwartungswert und nutzen wir die Linearität aus, so erhalten wir ⎡
k−1
E [X] = E ⎣
k
⎤ Xij ⎦
i=1 j=i+1
=
k−1
k
E [Xij ]
i=1 j=i+1
k 1 2 n k (k − 1) = . 2n
=
Für k (k −1) ≥ 2n ist also der Erwartungswert für die Anzahl der Paare, die am gleichen √ Tag Geburtstag haben, mindestens 1. Wenn wenigstens 2n+1 Personen in einem Raum sind, können wir erwarten, dass wenigstens zwei von ihnen am gleichen Tag Geburtstag haben. Für n = 365 und k = 28 ist der Erwartungswert der Anzahl der Paare mit gleichem Geburtstag (28 · 27)/(2 · 365) ≈ 1,0356. Bei wenigstens 28 Personen erwarten wir also, dass wenigstens zwei Personen am gleichen Tag Geburtstag haben. Auf dem Mars, wo ein Jahr 669 Marstage lang ist, brauchen wir mindestens 38 Marsmenschen. Die erste Analyse, bei der nur Wahrscheinlichkeiten verwendet wurden, hat die Anzahl der Personen bestimmt, die nötig sind, um eine Wahrscheinlichkeit von 1/2 für ein Paar mit gleichem Geburtstag zu erreichen. Die zweite Analyse, die Indikatorfunktionen verwendet, bestimmt die Zahl so, dass der Erwartungswert für einen gemeinsamen Geburtstag gleich 1 ist. Obwohl die exakten Zahlen √ in beiden Fällen voneinander abweichen, sind sie asymptotisch gleich, nämlich Θ( n).
134
5.4.2
5 Probabilistische Analyse und randomisierte Algorithmen
Bälle und Urnen
Betrachten Sie einen Prozess, in dem wir identische Bälle zufällig in b Urnen, die mit 1, 2, . . . , b durchnummeriert sind, werfen. Die Würfe sind unabhängig, und bei jedem Wurf landet der Ball mit gleicher Wahrscheinlichkeit in einer der Urnen. Die Wahrscheinlichkeit, dass ein geworfener Ball in einer gegebenen Urne landet, ist also jeweils 1/b. Der betrachtete Prozess ist eine Folge von Bernoulli-Versuchen (siehe Anhang C.4) mit der Erfolgswahrscheinlichkeit 1/b, wobei Erfolg bedeutet, dass der Ball in einer gegebenen Urne landet. Dieses Modell ist besonders nützlich bei der Analyse von Hashing (siehe Kapitel 11) und erlaubt uns, eine Reihe interessanter Fragen zu dem Prozess des Ballwerfens zu beantworten. (Problemstellung C-1 geht auf weitere Fragestellungen zu Bällen und Urnen ein.) Wie viele Bälle fallen in eine gegebene Urne? Die Anzahl der Bälle, die in eine gegebene Urne fallen, genügt einer Binomialverteilung b(k; n, 1/b). Wenn wir n Bälle werfen, sagt uns Gleichung (C.37), dass der Erwartungswert für die Anzahl der Bälle, die in die gegebene Urne gefallen sind, gleich n/b ist. Wie viele Bälle müssen wir im Mittel werfen, bis eine gegebene Urne einen Ball enthält? Die Anzahl der Würfe, die nötig sind, bis ein Ball in die gegebene Urne fällt, folgt einer geometrischen Verteilung mit dem Parameter 1/b. Nach Gleichung (C.32) ist der Erwartungswert der Anzahl der Würfe gleich 1/(1/b) = b. Wie viele Bälle müssen wir werfen, bis jede Urne wenigstens einen Ball enthält? Lassen Sie uns einen Wurf, bei dem der Ball in eine bis dahin leere Urne fällt, als „Treffer“ bezeichnen. Was wir wissen wollen, ist der Erwartungswert für die Anzahl der Würfe, die wir brauchen, um b Treffer zu landen. Wir können die Treffer benutzen, um die Folge der n Würfe in Abschnitte zu partitionieren. Der i-te Abschnitt besteht aus den Würfen nach dem (i − 1)-ten Treffer bis zum i-ten Treffer. Der erste Abschnitt besteht nur aus dem ersten Wurf, denn da noch alle Urnen leer sind, werden wir mit Sicherheit einen Treffer landen. Für jeden Wurf im i-ten Abschnitt gibt es i − 1 Urnen, die Bälle enthalten und b − i + 1 leere Urnen. Für jeden Wurf im i-ten Abschnitt beträgt die Trefferwahrscheinlichkeit also (b − i + 1)/b. Bezeichnen wir mit ni die Anzahl der Würfe im i-ten Abschnitt, dann ist die Anzahl der b notwendigen Würfe, um b Treffer zu landen, gleich n = i=1 ni . Jede der Zufallsvariablen ni hat eine geometrische Verteilung mit der Erfolgswahrscheinlichkeit (b − i + 1)/b. Somit gilt nach Gleichung (C.32)
E [ni ] =
b . b−i+1
Wegen der Linearität des Erwartungswertes gilt
5.4 ∗ Probabilistische Analyse und mehr zur Verwendung der Indikatorfunktion
E [n] = E
" b
135
# ni
i=1
=
b
E [ni ]
i=1
=
b i=1
= b
b b−i+1
b 1 i=1
i
= b(ln b + O(1))
(wegen Gleichung (A.7)) .
Es sind also ungefähr b ln b Würfe notwendig, bevor wir erwarten können, dass jede Urne einen Ball enthält. Dieses Problem ist auch als Coupon-Sammler-Problem bekannt: Eine Person, die versucht, n verschiedene Coupons zu erhalten, erwartet, ungefähr b ln b Coupons erwerben zu müssen, um seine Sammlung vollständig zu bekommen.
5.4.3
Glückssträhnen
Nehmen Sie an, Sie werfen eine faire Münze n-mal. Wenn mehrfach hintereinander ohne Unterbrechung „Kopf“ oben liegt, nennen wir dies eine Glückssträhne. Wie lang ist die längste Glückssträhne, die Sie erwarten können? Die Antwort lautet Θ(lg n), wie die folgende Analyse zeigt. Wir beweisen zunächst, dass die erwartete Länge der längsten Glückssträhne O(lg n) ist. Die Wahrscheinlichkeit für das Ereignis „Kopf“ bei jedem Münzwurf ist 1/2. Es sei Aik das Ereignis, dass eine Glückssträhne der Mindestlänge k mit dem i-ten Münzwurf beginnt, oder genauer, das Ereignis, dass die k aufeinander folgenden Münzwürfe i, i + 1, . . . , i + k − 1 alle „Kopf“ ergeben, wobei 1 ≤ k ≤ n und 1 ≤ i ≤ n − k + 1 gilt. Da die Münzwürfe voneinander unabhängig sind, ist die Wahrscheinlichkeit für jedes gegebene Ereignis Aik Pr {Aik } = 1/2k .
(5.8)
Für k = 2 lg n gilt Pr Ai,2 lg n = 1/22 lg n
≤ 1/22 lg n = 1/n2 , und so ist die Wahrscheinlichkeit sehr klein, dass an Position i eine Glückssträhne der Mindestlänge 2 lg n beginnt. Es gibt höchstens n − 2 lg n + 1 Positionen, an denen eine solche Glückssträhne beginnen kann. Die Wahrscheinlichkeit, dass an einer dieser
136
5 Probabilistische Analyse und randomisierte Algorithmen
Stellen eine Glückssträhne der Mindestlänge 2 lg n beginnt, ist daher
Pr
⎧ ⎨n−2 lg ,n +1 ⎩
⎫ ⎬ Ai,2 lg n
i=1
⎭
n−2 lg n +1
≤
1/n2
i=1
aj durch, um ihre relative Reihenfolge zu bestimmen. Wir können uns die Werte der Elemente nicht ansehen oder auf irgendeinem anderen Wege Informationen über sie erlangen. In diesem Abschnitt setzen wir ohne Beschränkung der Allgemeinheit voraus, dass die Eingabeelemente paarweise verschieden sind. Unter dieser Voraussetzung sind Vergleichsoperationen der Form ai = aj sinnlos, sodass wir davon ausgehen können, dass solche Vergleiche nicht durchgeführt werden. Wir stellen außerdem fest, dass die Vergleichsoperationen ai ≤ aj , ai ≥ aj , ai > aj und ai < aj alle dahingehend äquivalent sind, dass sie zu identischen Informationen über die Reihenfolge von ai und aj führen. Wir nehmen daher an, dass alle Vergleichsoperationen die Form ai ≤ aj haben.
Das Entscheidungsbaum-Modell Wir können vergleichende Sortierverfahren abstrakt als Entscheidungsbäume ansehen. Ein Entscheidungsbaum ist ein vollständiger binärer Baum, der die Vergleiche zwi-
192
8 Sortieren in linearer Zeit 1:2 ≤
>
>
≤
2:3
1:3
≤ 〈1,2,3〉
〈2,1,3〉
1:3 ≤ 〈1,3,2〉
> 〈3,1,2〉
> 2:3 ≤ 〈2,3,1〉
> 〈3,2,1〉
Abbildung 8.1: Der Entscheidungsbaum für Sortieren durch Einfügen auf einem Feld aus drei Elementen. Ein mit i:j gekennzeichneter innerer Knoten stellt einen Vergleich zwischen ai und aj dar. Ein mit der Permutation π(1), π(2), . . . , π(n) gekennzeichnetes Blatt gibt die Reihenfolge aπ(1) ≤ aπ(2) ≤ · · · ≤ aπ(n) an. Der schattierte Pfad zeigt die Entscheidungen, die beim Sortieren der Eingabefolge a1 = 6, a2 = 8, a3 = 5 getroffen werden; die Permutation 3, 1, 2 am Blattknoten dieses Pfades besagt, dass die sortierte Reihenfolge a3 = 5 ≤ a1 = 6 ≤ a2 = 8 ist. Es gibt 3! = 6 mögliche Permutationen der Eingabeelemente und so muss der Entscheidungsbaum mindestens 6 Blätter haben.
schen den Elementen repräsentiert, die von einem speziellen Sortieralgorithmus auf einem Eingabefeld einer vorgegebenen Größe durchgeführt werden. Steuerung, Datenbewegung und alle anderen Aspekte des Algorithmus werden vernachlässigt. Abbildung 8.1 zeigt den Entscheidungsbaum, der zu Sortieren durch Einfügen aus Abschnitt 2.1 angewendet auf ein Eingabefeld bestehend aus drei Elementen korrespondiert. In einem Entscheidungsbaum annotieren wir jeden inneren Knoten mit i : j für ein i und j aus dem Bereich 1 ≤ i, j ≤ n, wobei n die Anzahl der Elemente in der Eingabefolge angibt. Wir annotieren jedes Blatt mir einer Permutation π(1), π(2), . . . , π(n). (In Abschnitt C.1 erfahren Sie mehr über Permutationen.) Die Ausführung des Sortieralgorithmus entspricht der Verfolgung eines einfachen Pfades von der Wurzel des Entscheidungsbaumes bis hinunter zu einem Blatt des Entscheidungsbaumes. Ein mit i : j annotierter innerer Knoten steht für den Vergleich ai ≤ aj . Der linke Teilbaum schreibt dann die nachfolgenden Vergleiche vor, wenn wir wissen, dass ai ≤ aj gilt, und der rechte Teilbaum die nachfolgenden Vergleiche für den Fall ai > aj . Wenn wir ein Blatt erreichen, hat der Sortieralgorithmus die Reihenfolge aπ(1) ≤ aπ(2) ≤ · · · ≤ aπ(n) festgelegt. Da jeder korrekte Sortieralgorithmus in der Lage sein muss, jede Permutation seiner Eingabe zu erzeugen, besteht eine notwendige Bedingung für die Korrektheit eines vergleichenden Sortierverfahrens darin, dass jede der n! Permutationen von n Elementen als eines der Blätter des Entscheidungsbaumes vorkommen muss. Zudem muss jedes dieser Blätter über einen an der Wurzel startenden einfachen Pfad erreichbar sein, der einer tatsächlichen Ausführung des vergleichenden Sortierverfahrens entspricht. (Wir werden solche Blätter im Folgenden als „erreichbar“ bezeichnen.) Wir werden also nur Entscheidungsbäume betrachten, in denen jede Permutation als erreichbares Blatt vorkommt.
8.1 Untere Schranken für das Sortieren
193
Eine untere Schranke für den schlechtesten Fall Die Länge des längsten einfachen Pfades von der Wurzel eines Entscheidungsbaumes bis zu einem seiner erreichbaren Blätter entspricht der Anzahl der Vergleichsoperationen, die der Algorithmus im schlechtesten Fall ausführt. Folglich ist für ein gegebenes vergleichendes Sortierverfahren die Anzahl der Vergleichsoperationen im schlechtesten Fall gleich der Höhe des Entscheidungsbaumes. Eine untere Schranke für die Höhen aller Entscheidungsbäume, in denen jede Permutation als erreichbares Blatt erscheint, ist daher eine untere Schranke für die Laufzeit jedes vergleichenden Sortierverfahrens. Das folgende Theorem stellt eine solche untere Schranke bereit. Theorem 8.1 Für jeden vergleichenden Sortieralgorithmus sind im schlechtesten Fall Ω(n lg n) Vergleichsoperationen erforderlich. Beweis: Nach der vorangegangenen Diskussion genügt es, die Höhe eines Entscheidungsbaumes zu bestimmen, in dem jede Permutation als erreichbares Blatt vorkommt. Betrachten Sie einen Entscheidungsbaum der Höhe h mit l erreichbaren Blättern, der für ein vergleichendes Sortierverfahren angewendet auf n Elemente steht. Da jede der n! Permutationen der Eingabefolge als Blatt vorkommt, gilt n! ≤ l. Da ein binärer Baum der Höhe h nicht mehr als 2h Blätter hat, gilt n! ≤ l ≤ 2h , sodass durch Anwenden des Logarithmus die Aussage folgt: h ≥ lg(n!) (da die lg-Funktion monoton steigend ist) = Ω(n lg n) (nach Gleichung (3.19)) .
Korollar 8.2 Heapsort und Sortieren durch Mischen sind asymptotisch optimale vergleichende Sortierverfahren. Beweis: Die oberen Schranken O(n lg n) für die Laufzeiten von Heapsort und Sortieren durch Mischen entsprechen der in Theorem 8.1 angegebenen unteren Schranke Ω(n lg n) für den schlechtesten Fall.
194
8 Sortieren in linearer Zeit
Übungen 8.1-1 Was ist die kleinste mögliche Tiefe eines Blattes in einem Entscheidungsbaum eines vergleichenden Sortierverfahrens? 8.1-2 Finden Sie asymptotisch scharfe Schranken für lg(n!), ohne die Stirlingsche Formel zu benutzen. Werten Sie stattdessen die Summe nk=1 lg k aus, indem Sie die Methoden aus Abschnitt A.2 anwenden. 8.1-3 Zeigen Sie, dass es kein vergleichendes Sortierverfahren gibt, dessen Laufzeit für wenigstens die Hälfte der n! Eingabefolgen der Länge n linear ist. Wie verhält es sich in Bezug auf einen Anteil von 1/n der Eingabefolgen der Länge n und wie bei einem Anteil von 1/2n? 8.1-4 Nehmen Sie an, Sie hätten eine Folge von n Elementen zu sortieren. Die Eingabe besteht aus n/k Teilfolgen, von denen jede k Elemente enthält. Die Elemente einer gegebenen Teilfolge sind kleiner als die der nachfolgenden Teilfolge und größer als diejenigen der vorhergehenden Teilfolge. Somit hat man zum Sortieren der Gesamtfolge nur jeweils die k Elemente in den n/k Teilfolgen zu sortieren. Zeigen Sie, dass für die Anzahl der erforderlichen Vergleichsoperationen zur Lösung dieser Variante des Sortierproblems die untere Schranke Ω(n lg k) gilt. (Hinweis: Es reicht nicht aus, einfach die unteren Schranken für die einzelnen Teilfolgen zu kombinieren.)
8.2
Countingsort
Countingsort setzt voraus, dass für eine ganze Zahl k jedes der n Eingabeelemente eine ganze Zahl zwischen 0 und k ist. Ist k = O(n), so benötigt das Verfahren Zeit Θ(n). Countingsort bestimmt für jedes Eingabeelement x die Anzahl der Elemente, die kleiner sind als x. Sie nutzt diese Information, um das Element x direkt an seiner Position im Ausgabefeld zu platzieren. Wenn zum Beispiel 17 Elemente kleiner sind als x, dann gehört x an Position 18 des Ausgabefeldes. Wir haben dieses Vorgehen noch ein wenig abzuwandeln, um auch den Fall, in dem mehrere Elemente den gleichen Wert haben können, in den Griff zu bekommen, denn schließlich wollen wir diese nicht alle an der gleichen Position einordnen. Im Code für Countingsort setzen wir voraus, dass die Eingabe aus einem Feld A[1 . . n] besteht und somit A.l¨a nge = n gilt. Wir benötigen zwei weitere Felder: Das Feld B[1 . . n] speichert die sortierte Ausgabe und das Feld C[0 . . k] stellt einen temporären Arbeitsspeicher dar.
8.2 Countingsort
195
1
2
3
4
5
6
7
8
A 2
5
3
0
2
3
0
3
0
1
2
3
4
5
C 2
0
2
3
0
1
1 0
1
2
3
4
5
C 2
2
4
7
7
8
1
B
2
3
4
5
0
1
2
6
7
3
8
1
B
2
3
4
5
0
5
6
7
8
3
4
5
(c) 6
7
3
3
0
1
2
3
4
5
0
1
2
3
4
5
C 1
2
4
6
7
8
C 1
2
4
5
7
8
(e)
4
2 4 6 7 8
(b)
0
(d)
3
3
C 2
(a)
2
B
8 1
B 0
5
6
7
0 2 2 3
2
3
4
3
3 5
8
(f)
Abbildung 8.2: Die Arbeitsweise von Counting-Sort angewendet auf ein Eingabefeld A[1 . . 8], dessen Elemente sämtlich nichtnegative ganze Zahlen kleiner oder gleich 5 sind. (a) Das Feld A und das Hilfsfeld C nach Ausführung von Zeile 5. (b) Das Feld C nach Zeile 8. (c)–(e) Das Ausgabefeld B und das Hilfsfeld C nach einer, zwei bzw. drei Iterationen der Schleife der Zeilen 10–12. Nur die leicht schattierten Elemente des Feldes B sind eingefügt worden. (f ) Das fertig sortierte Ausgabefeld B.
Counting-Sort(A, B, k) 1 sei C[0 . . k] ein neues Feld 2 for i = 0 to k 3 C[i] = 0 4 for j = 1 to A.l¨a nge 5 C[A[j]] = C[A[j]] + 1 6 // C[i] enthält nun die Anzahl der Elemente, die gleich i sind. 7 for i = 1 to k 8 C[i] = C[i] + C[i − 1] 9 // C[i] enthält nun die Anzahl der Elemente, die kleiner oder gleich i sind. 10 for j = A.l¨a nge downto 1 11 B[C[A[j]]] = A[j] 12 C[A[j]] = C[A[j]] − 1 Abbildung 8.2 illustriert Countingsort. Nachdem die for-Schleife der Zeilen 2–3 das Feld C mit 0 initialisiert hat, sichtet die for-Schleife der Zeilen 4–5 jedes Eingabeelement. Ist der Wert des Eingabeelementes gleich i, inkrementieren wir C[i]. Damit enthält C[i] nach Zeile 5 für jedes i = 0, 1, . . . , k die Anzahl der Elemente, die gleich i sind. Die Zeilen 7–8 bestimmen durch Aufsummieren, für jedes i = 0, 1, . . . , k, wie viele Eingabeelemente kleiner oder gleich i sind. Schließlich platziert die for-Schleife der Zeilen 10–12 jedes Element A[j] an seiner korrekten Position in dem Ausgabefeld B. Wenn alle n Elemente paarweise verschieden sind, dann ist bei erstmaliger Ausführung von Zeile 10 für jedes A[j] der Wert C[A[j]] dessen korrekte Endposition im Ausgabefeld, denn es gibt C[A[j]] Elemente, die klei-
196
8 Sortieren in linearer Zeit
ner oder gleich A[j] sind. Da die Elemente nicht paarweise verschieden sein müssen, dekrementieren wir C[A[j]] jedes Mal, wenn wir einen Wert A[j] in das Feld B einfügen. Das Dekrementieren von C[A[j]] bewirkt, dass das nächste Eingabeelement mit dem gleichen Wert wie A[j] (falls ein solches existiert) im Ausgabefeld an der Position unmittelbar vor A[j] eingefügt wird. Wie viel Zeit benötigt Countingsort? Die for-Schleife der Zeilen 2–3 verbraucht die Zeit Θ(k), die for-Schleife der Zeilen 4–5 die Zeit Θ(n), die for-Schleife der Zeilen 7–8 die Zeit Θ(k) und die for-Schleife der Zeilen 10–12 die Zeit Θ(n). Demzufolge ist die Gesamtlaufzeit von Countingsort in Θ(n + k). In der Praxis verwenden wir Countingsort gewöhnlich, wenn k = O(n) gilt. In diesem Fall ist die Laufzeit von Countingsort in Θ(n). Für Countingsort gilt die in Abschnitt 8.1 bewiesene untere Schranke nicht und Countingsort unterbietet sie tatsächlich auch, da es kein vergleichendes Sortierverfahren ist. In der Tat, im Code kommen nirgendwo Vergleiche zwischen Elementen vor. Stattdessen verwendet Countingsort die realen Werte der Elemente als Index in einem Feld. Die untere Schranke Ω(n lg n) gilt nicht, wenn wir Sortierverfahren betrachten, die nicht vergleichend sind. Eine wichtige Eigenschaft von Countingsort ist, dass der Algorithmus stabil ist: Zahlen mit dem gleichen Wert erscheinen im Ausgabefeld in der gleichen Reihenfolge wie im Eingabefeld. Das heißt, sind zwei Zahlen gleich, so entscheidet das Verfahren die Zahl als erstes in das Ausgabefeld zu schreiben, das auch als erstes im Eingabefeld steht. Normalerweise ist die Eigenschaft der Stabilität nur von Bedeutung, wenn Satellitendaten mit den zu sortierenden Elementen mitgeführt werden. Die Stabilität von Countingsort ist jedoch noch aus einem anderen Grund wichtig: Countingsort wird häufig als Unterroutine von Radixsort verwendet. Wie wir im nächsten Abschnitt sehen werden, muss Countingsort stabil sein, damit Radixsort korrekt arbeitet.
Übungen 8.2-1 Erläutern Sie analog zu Abbildung 8.2, wie Counting-Sort auf dem Feld A = 6, 0, 2, 0, 1, 3, 4, 6, 1, 3, 2 arbeitet. 8.2-2 Beweisen Sie, dass Counting-Sort stabil ist. 8.2-3 Nehmen Sie an, dass wir den Kopf der for-Schleife (Zeile 10) abändern zu 10 for j = 1 to A.l¨a nge Zeigen Sie, dass der Algorithmus noch immer korrekt arbeitet. Ist der modifizierte Algorithmus stabil? 8.2-4 Gegeben sind n ganze Zahlen zwischen 0 und k. Geben Sie einen Algorithmus an, der die Eingabe vorverarbeitet und dann jede Anfrage nach der Anzahl der Zahlen, die in einem Intervall [a . . b] liegen, in Zeit O(1) beantwortet. Ihr Algorithmus sollte für den Vorverarbeitungsschritt Zeit Θ(n + k) benötigen.
8.3 Radixsort 329 457 657 839 436 720 355
720 355 436 457 657 329 839
197 720 329 436 839 355 457 657
329 355 436 457 657 720 839
Abbildung 8.3: Die Arbeitsweise von Radixsort auf einer Liste aus sieben dreistelligen Zahlen. Die Spalte ganz links zeigt die Eingabefolge. Die übrigen Spalten zeigen die Listen, jeweils sortiert nach der, gemäß steigender Wertigkeit, nächsten Stelle. Die Schattierung zeigt die Stelle an, nach der sortiert wurde, um die Liste aus der vorherigen zu erzeugen.
8.3
Radixsort
Radixsort ist ein Algorithmus, der von den Lochkartensortiermaschinen verwendet wurde, die man heute nur noch in Computermuseen findet. Die Lochkarten bestehen aus 80 Spalten und in jeder Spalte kann eine Maschine an einem von 12 Plätzen ein Loch stanzen. Die Sortiermaschine kann mechanisch so „programmiert“ werden, dass eine gegebene Spalte einer jeden Lochkarte eines Stapels untersucht wird und die Karte in Abhängigkeit davon, an welchem Platz das Loch in dieser Spalte gestanzt ist, in einen von 12 Kästen gelegt wird. Ein Operator kann dann die Karten Kasten für Kasten einsammeln, sodass die Karten, auf denen das Loch am ersten Platz gestanzt ist, über jenen liegen, auf denen das Loch an der zweiten Position ist und so weiter. Zur Darstellung von Dezimalzahlen verwendet jede Spalte nur 10 Plätze. (Die anderen beiden Plätze sind reserviert für die Codierung nichtnumerischer Zeichen.) Eine d-stellige Zahl würde dann einen aus d Spalten bestehenden Bereich belegen. Da die Sortiermaschine zu jedem Zeitpunkt nur auf eine Spalte schauen kann, erfordert das Problem, n Karten nach d-stelligen Zahlen zu sortieren, einen Sortieralgorithmus. Rein intuitiv würden Sie möglicherweise zuerst die Zahlen nach dem höchstwertigen Bit sortieren, dann jeden resultierenden Kasten für sich rekursiv sortieren und dann die sortierten Kartenstapel aus den Kästen in geeigneter Reihenfolge zusammenlegen. Leider erzeugt diese Prozedur viele temporäre Kartenhaufen, die Sie verwalten müssten, da die Karten aus 9 der 10 Kästen zur Seite gelegt werden müssten, um jeweils einen Kasten sortieren zu können (siehe Übung 8.3-5). Entgegen dieser Intuition löst Radixsort das Problem des Lochkartensortierens, indem zuerst nach der Stelle mit der geringsten Wertigkeit sortiert wird. fügt dann die Karten zu einem Kartenstapel zusammen, wobei die Karten aus dem Kasten 0 vor denen des Kastens 1 kommen, diese wiederum vor denen aus Kasten 2 und so weiter. Dann sortiert er den gesamten Kartenstapel nach der Stelle mit der zweitgeringsten Wertigkeit und fügt sie in der gleichen Art und Weise wieder zusammen. Der Prozess setzt sich fort, bis die Karten nach allen d Stellen sortiert wurden. Bemerkenswerterweise sind die Karten zu diesem Zeitpunkt vollständig nach der d-stelligen Zahl sortiert. Somit sind zum Sortieren nur d Durchläufe durch den Kartenstapel erforderlich. Abbildung 8.3 zeigt, wie Radixsort auf einem „Stapel“ aus sieben 3-stelligen Zahlen arbeitet.
198
8 Sortieren in linearer Zeit
Damit Radixsort korrekt sortiert, muss das Verfahren zum Sortieren nach einer Stelle stabil sein. Das durch die Sortiermaschine vorgenommene Sortieren ist stabil, aber der Operator muss darauf achten, die Reihenfolge der Lochkarten, in der sie aus einem Kasten kommen, nicht zu verändern, auch dann nicht wenn alle Lochkarten eines Kastens in der gewählten Spalte die gleiche Ziffer stehen haben. In typischen, auf wahlfreiem Zugriff basierenden sequentiellen Rechnern wenden wir Radixsort in einigen Fällen an, um Datensätze zu sortieren, die aus mehreren Komponenten bestehende Schlüssel haben. Beispielsweise wenn wir Jahresdaten nach drei Schlüsseln sortieren wollen: Jahr, Monat und Tag. Wir könnten einen vergleichsbasierten Sortieralgorithmus anwenden: Für zwei gegebene Datensätze werden die Jahre verglichen, im Falle einer Übereinstimmung die Monate und, wenn auch diese übereinstimmen, die Tage. Alternativ könnten wir die Informationen durch ein stabiles Sortierverfahren sortieren: zuerst nach dem Tag, dann nach dem Monat und schließlich nach dem Jahr. Der Code für Radixsort ist einfach. Die folgende Prozedur setzt voraus, dass jedes Element des n-elementigen Feldes A aus d Stellen besteht, wobei die Stelle 1 diejenige mit der niedrigsten Wertigkeit ist und d diejenige mit der höchsten Wertigkeit. Radix-Sort(A, d) 1 for i = 1 to d 2 wende ein stabiles Sortierverfahren an, um A nach Stelle i zu sortieren Lemma 8.3 Sind n d-stellige Zahlen gegeben, bei denen jede Stelle k mögliche Werte annehmen kann, dann sortiert Radix-Sort diese Zahlen in der Zeit Θ(d (n + k)), wenn das in den Zwischenschritten verwendete stabile Sortierverfahren jeweils Zeit Θ(n + k) benötigt. Beweis: Die Korrektheit von Radixsort folgt durch Induktion bezüglich der sortierten Spalte (siehe Übung 8.3-3). Die Analyse der Laufzeit hängt von dem im Zwischenschritt verwendeten stabilen Sortierverfahren ab. Wenn jede Stelle im Bereich von 0 bis k − 1 liegt (sodass sie k mögliche Werte annehmen kann) und k nicht zu groß ist, dann ist Countingsort die naheliegende Wahl. Jeder Durchlauf über n d-stellige Zahlen benötigt dann die Zeit Θ(n+k). Es gibt d Durchläufe und so ist die gesamte Laufzeit für Radixsort Θ(d (n + k)). Für konstantes d und k = O(n) läuft Radixsort in linearer Zeit. Allgemein haben wir eine gewisse Flexibilität, wie jeder Schlüssel in Stellen aufzulösen ist. Lemma 8.4 Sind n b-Bit-Zahlen und eine positive ganze Zahl r ≤ b gegeben, dann sortiert Radix-Sort diese Zahlen korrekt in der Zeit Θ((b/r)(n + 2r )), wenn das in den
8.3 Radixsort
199
Zwischenschritten verwendete stabile Sortierverfahren angewendet auf Zahlen aus dem Bereich 0 bis k jeweils Zeit Θ(n + k) benötigt. Beweis: Für einen Wert r ≤ b betrachten wir jeden Schlüssel als Zahl mit d = b/r Stellen von jeweils r Bit. Jede Stelle ist eine ganze Zahl aus dem Bereich 0 bis 2r − 1, sodass wir Countingsort mit k = 2r verwenden können. (Beispielsweise können wir ein 32-Bit-Wort als aus vier 8-Bit-Stellen bestehend auffassen, sodass b = 32, r = 8, k = 2r = 256 und d = b/r = 4 ist.) Jeder Lauf von Countingsort benötigt die Zeit Θ(n + k) = Θ(n + 2r ), und da es d Läufe gibt, beträgt die gesamte Laufzeit Θ(d (n + 2r )) = Θ((b/r)(n + 2r )). Für gegebene Werte n und b wollen wir den Wert r mit r ≤ b wählen, der den Ausdruck (b/r)(n + 2r ) minimiert. Falls b < lg n ist, dann gilt für jedes r ≤ b die Gleichung (n + 2r ) = Θ(n). Die Wahl r = b führt auf eine Laufzeit von (b/b)(n + 2b ) = Θ(n), was asymptotisch optimal ist. Für b ≥ lg n führt die Wahl r = lg n auf die, bis auf eine Konstante, beste Laufzeit, was wir wie folgt sehen können. Die Wahl r = lg n führt auf eine Laufzeit von Θ(b n/ lg n). Wenn wir r größer werden lassen als lg n, dann wächst der 2r -Term im Zähler schneller als der Term r im Nenner. Somit führt das Anwachsen von r über lg n auf eine Laufzeit von Ω(b n/ lg n). Wenn wir dagegen r unter lg n absenken, dann wächst der Term b/r und der Term n + 2r bleibt bei Θ(n). Ist Radixsort nun einem vergleichenden Sortieralgorithmus wie Quicksort vorzuziehen? Ist b = O(lg n), was häufig vorkommt, und wählen wir r ≈ lg n, dann ist die Laufzeit von Radixsort Θ(n). Dies scheint besser zu sein als die erwartete Laufzeit Θ(n lg n) von Quicksort. Die in der Θ-Notation versteckten konstanten Faktoren unterscheiden sich jedoch. Zwar führt Radixsort eventuell weniger Läufe über die n Schlüssel aus als Quicksort, aber es kann sein, dass jeder Lauf von Radixsort eine signifikant längere Zeit braucht. Welchen Sortieralgorithmus wir vorziehen sollten, hängt von den Eigenschaften der Implementierung, von der verwendeten Maschine (zum Beispiel nutzt Quicksort häufig Hardware-Caches effizienter als Radixsort) und von den Eingabedaten ab. Außerdem sortiert die Version von Radixsort, die im Zwischenschritt Countingsort als stabiles Sortierverfahren verwendet, nicht in-place, im Gegensatz zu vielen der vergleichenden Sortierverfahren mit Laufzeit Θ(n lg n). Wenn also der Verbrauch von primärem Hauptspeicher im Vordergrund steht, dann sollten wir einen in-place-Algorithmus vorziehen.
Übungen 8.3-1 Beschreiben Sie analog zu Abbildung 8.3 die Arbeitsweise von Radix-Sort angewendet auf die folgende Liste englischer Wörter: COW, DOG, SEA, RUG, ROW, MOB, BOX, TAB, BAR, EAR, TAR, DIG, BIG, TEA, NOW, FOX. 8.3-2 Welche der folgenden Sortieralgorithmen sind stabil: Sortieren durch Einfügen, Sortieren durch Mischen, Heapsort und Quicksort? Geben Sie eine einfache Methode an, mit der ein jeder vergleichende Sortieralgorithmus in ein stabiles Verfahren transformiert werden kann. Wie viel zusätzliche Zeit und wie viel zusätzlichen Speicherplatz benötigt Ihre Methode?
200
8 Sortieren in linearer Zeit
8.3-3 Beweisen Sie durch mathematische Induktion, dass Radixsort korrekt sortiert. An welcher Stelle macht Ihr Beweis von der Annahme Gebrauch, dass das Sortieren im Zwischenschritt stabil ist? 8.3-4 Zeigen Sie, wie n ganze Zahlen mit Werten aus dem Bereich von 0 bis n3 − 1 in der Zeit O(n) sortiert werden können. 8.3-5∗ Betrachten Sie noch einmal den ersten in diesem Abschnitt diskutierten Algorithmus für das Sortieren von Lochkarten. Wie viele Sortierdurchläufe sind im schlechtesten Fall exakt notwendig, um d-stellige Dezimalzahlen zu sortieren? Wie viele Kartenhaufen müsste ein Operator im schlechtesten Fall verwalten?
8.4
Bucketsort
Bucketsort setzt voraus, dass die Eingabe einer Gleichverteilung unterliegt und hat eine mittlere Laufzeit von O(n). Wie Countingsort ist Bucketsort schnell, weil er gewisse Annahmen über die Eingabefolge trifft. Während Countingsort voraussetzt, dass die Eingabe aus ganzen Zahlen eines kleinen Bereiches besteht, geht Bucketsort davon aus, dass die Eingabefolge durch einen zufälligen Prozess erzeugt wird, der die Elemente gleichmäßig und unabhängig voneinander über das Intervall [0, 1) verteilt. (Eine Definition der gleichmäßigen Verteilung finden Sie in Abschnitt C.2.) Bucketsort teilt das Interval [0, 1) in n gleichgroße Teilintervalle, auch Buckets genannt, und verteilt die n Eingabezahlen in diese Buckets. Da die Eingabezahlen über [0, 1) gleichmäßig und unabhängig voneinander verteilt sind, erwarten wir nicht, dass viele Zahlen auf das gleiche Bucket entfallen. Um die Ausgabe zu erzeugen, sortieren wir einfach die Zahlen innerhalb jedes Buckets, gehen dann der Reihe nach durch jedes Bucket und listen alle Elemente auf. Unser Code für Bucketsort setzt voraus, dass die Eingabe ein Feld A der Länge n ist und dass jedes Feldelement A[i] die Bedingung 0 ≤ A[i] < 1 erfüllt. Der Code erfordert ein Hilfsfeld B[0 . . n − 1] von verketteten Listen (Buckets) und setzt voraus, dass es einen Mechanismus für die Verwaltung solcher Listen gibt. (Abschnitt 10.2 beschreibt, wie die grundlegenden Operationen mit verketteten Listen zu implementieren sind.) Bucket-Sort(A) 1 n = A.l¨a nge 2 sei B[0 . . n − 1] ein neues Feld 3 for i = 0 to n − 1 4 mache B[i] zu einer leeren Liste 5 for i = 1 to n 6 füge A[i] in die Liste B[ nA[i]] ein 7 for i = 0 to n − 1 8 sortiere die Liste B[i] mit Sortieren durch Einfügen 9 hänge die Listen B[0], B[1], . . . , B[n − 1] in dieser Reihenfolge hintereinander
8.4 Bucketsort A .78 2 .17 3 .39 4 .26 1
201
B 0 1 2 3
.12 .21 .39
.72 6 .94 7 .21
4
6
.68
8
.12 .23 10 .68
7
.72
9
8
5
(a)
.17 .23
.26
5
9
.78
.94 (b)
Abbildung 8.4: Die Arbeitsweise von Bucketsort für n = 10. (a) Das Eingabefeld A[1 . . 10]. (b) Das Feld B[0 . . 9] der sortierten Listen (Buckets) nach Ausführung der Zeile 8 des Algorithmus. Bucket i enthält die Werte aus dem halboffenen Intervall [i/10, (i + 1)/10). Die sortierte Ausgabe besteht aus einer Verknüpfung der Listen B[0], B[1], . . . , B[9].
Abbildung 8.4 zeigt, wie Bucketsort auf einem Eingabefeld aus 10 Zahlen arbeitet. Um zu verstehen, dass dieser Algorithmus korrekt arbeitet, betrachten wir zwei Elemente A[i] und A[j]. Wir nehmen ohne Beschränkung der Allgemeinheit A[i] ≤ A[j] an. Wegen nA[i] ≤ nA[j] wird A[i] entweder in das gleiche Bucket wie A[j] oder in ein Bucket mit kleinerem Index eingefügt. Falls die Elemente A[i] und A[j] in das gleiche Bucket eingefügt werden, dann setzt die for-Schleife in den Zeilen 7–8 sie in die richtige Reihenfolge. Falls A[i] und A[j] in verschiedene Buckets eingefügt werden, dann setzt Zeile 9 sie in die richtige Reihenfolge. Bucketsort arbeitet somit korrekt. Um die Laufzeit zu analysieren, stellen wir zunächst fest, dass alle Zeilen außer Zeile 8 im schlechtesten Fall die Zeit O(n) benötigen. Wir müssen noch die Zeit bestimmen, die durch die n Aufrufe von Sortieren durch Einfügen in Zeile 8 verbraucht wird. Um die Kosten für die Aufrufe von Sortieren durch Einfügen zu untersuchen, führen wir die Zufallsvariable ni ein, die die Anzahl der Elemente in Bucket B[i] beschreibt. Da Sortieren durch Einfügen in quadratischer Zeit läuft (siehe Abschnitt 2.2), ist die Laufzeit von Bucketsort
T (n) = Θ(n) +
n−1
O(n2i ) .
i=0
Wir bestimmen nun die Laufzeit im Mittel von Bucketsort, indem wir die erwartete Höhe der Laufzeit berechnen und hierbei die Eingangsverteilung zu Grunde legen. Bilden wir auf beiden Seiten den Erwartungswert und nutzen wir dessen Linearität aus, so erhalten
202
8 Sortieren in linearer Zeit
wir
" E [T (n)] = E Θ(n) +
# O(n2i )
i=0 n−1
4 5 E O(n2i )
= Θ(n) +
= Θ(n) +
n−1
(wegen der Linearität des Erwartungswertes)
i=0 n−1
4 5 O E n2i (nach Gleichung (C.22)) .
(8.1)
i=0
Wir behaupten, dass 4 5 E n2i = 2 − 1/n
(8.2) 4 5 für i = 0, 1, . . . , n − 1 gilt. Es überrascht nicht, dass jedes Bucket i für E n2i den gleichen Wert hat, denn jeder Wert des Eingabefeldes A hat für jedes Bucket die gleiche Wahrscheinlichkeit, in dieses eingefügt zu werden. Um Gleichung (8.2) zu beweisen, definieren wir die Indikatorfunktionen Xij = I {A[j] wird in Bucket i eingefügt} für i = 0, 1, . . . , n − 1 und j = 1, 2, . . . , n. Damit gilt ni =
n
Xij .
j=1
4 5 Um E n2i zu berechnen, multiplizieren wir das Quadrat aus und ordnen die Terme um: ⎡⎛ ⎞2 ⎤ n 4 5 ⎢ ⎥ E n2i = E ⎣⎝ Xij ⎠ ⎦ j=1
⎡ ⎤ n n Xij Xik ⎦ = E⎣ j=1 k=1
⎡ n 2 = E⎢ Xij + ⎣ j=1
=
n j=1
1≤j≤n 1≤k≤n k =j
4 25 + E Xij
⎤ Xij Xik ⎥ ⎦
E [Xij Xik ] .
(8.3)
1≤j≤n 1≤k≤n k =j
Die letzte Zeile folgt aus der Linearität des Erwartungswertes. Wir werten die beiden Summen separat aus. Die Indikatorfunktion Xij ist mit Wahrscheinlichkeit 1/n gleich
8.4 Bucketsort
203
1 und mit Wahrscheinlichkeit 1 − 1/n gleich 0; also gilt 4 25 1 1 E Xij = 12 · + 02 · 1 − n n 1 . = n Für k = j sind die Variablen Xij und Xik unabhängig, sodass E [Xij Xik ] = E [Xij ] · E [Xik ] 1 1 = · n n 1 = 2 n gilt. Substituieren wir diese beiden Erwartungswerte in Gleichung (8.3), so erhalten wir n 4 5 1 + E n2i = n j=1
1≤j≤n 1≤k≤n k =j
1 n2
1 1 + n (n − 1) 2 n n n−1 = 1+ n 1 = 2− , n = n
womit Gleichung (8.2) bewiesen ist. Setzen wir diese Erwartungswerte in Gleichung (8.1) ein, so sehen wir, dass die Laufzeit von Bucketsort im Mittel in Θ(n) + n · O(2 − 1/n) = Θ(n) ist. Selbst wenn die Eingabefolge nicht einer gleichmäßigen Verteilung genügt, kann Bucketsort in linearer Zeit laufen. Solange die Eingabe die Eigenschaft hat, dass die Summe der Quadrate der Bucketgrößen linear in der Gesamtanzahl der Elemente ist, sagt uns Gleichung (8.1), dass Bucketsort in linearer Zeit laufen wird.
Übungen 8.4-1 Erläutern Sie analog zu Abbildung 8.4 die Arbeitsweise von Bucket-Sort angewendet auf das Feld A = .79, .13, .16, .64, .39, .20, .89, .53, .71, .42. 8.4-2 Erklären Sie, warum die Laufzeit im schlechtesten Fall von Bucketsort in Θ(n2 ) liegt. Durch welche einfache Änderung am Algorithmus kann die lineare mittlere Laufzeit erhalten und die Laufzeit im schlechtesten Fall zu O(n lg n) gemacht werden?
204
8 Sortieren in linearer Zeit
8.4-3 Sei X eine Zufallsvariable, die beschreibt, wie häufig bei4 zwei 5 Würfen einer fairen Münze das Ereignis Kopf auftritt. Bestimmen Sie E X 2 und E2 [X]. 8.4-4∗ Gegeben seien n Punkte pi = (xi , yi ) im Einheitskreis, sodass 0 < x2i + yi2 ≤ 1 für i = 1, 2, . . . , n gilt. Nehmen Sie an, die Punkte wären gleichmäßig verteilt, d. h. die Wahrscheinlichkeit, einen Punkt in einem Bereich des Kreises zu finden, ist proportional zum Flächeninhalt dieses Bereiches. Entwerfen Sie einen Algorithmus mit mittlerer Laufzeit Θ(n), der die n Punkte nach ihrem Abstand 2 di = xi + yi2 zum Ursprung sortiert. (Hinweis: Entwerfen Sie die Größen der Buckets so, dass sie die gleichmäßige Verteilung der Punkte im Einheitskreis widerspiegeln.) 8.4-5∗ Eine Verteilungsfunktion P (x) einer Zufallsvariablen X ist definiert durch P (x) = Pr {X ≤ x}. Nehmen Sie an, dass wir eine Liste von n Zufallsvariablen X1 , X2 , . . . , Xn gemäß einer stetigen Verteilungsfunktion P wählen würden, was in Zeit O(1) erfolgen kann. Geben Sie einen Algorithmus an, der diese Zahlen im Mittel in linearer Zeit sortiert.
Problemstellung 8-1 Probabilistische untere Schranken bei vergleichenden Sortierverfahren In dieser Problemstellung beweisen wir eine probabilistische untere Schranke von Ω(n lg n) für die Laufzeit eines jeden deterministischen oder randomisierten vergleichenden Sortierverfahrens angewendet auf n paarweise verschiedene Eingabeelemente. Wir beginnen mit der Untersuchung eines deterministischen vergleichenden Sortierverfahrens A mit dem Entscheidungsbaum TA . Wir setzen voraus, dass jede Permutation der Eingaben von A gleichwahrscheinlich ist. a. Nehmen Sie an, dass jedes Blatt von TA mit der Wahrscheinlichkeit markiert wäre, mit der es bei einer gegebenen zufälligen Eingabe erreicht wird. Beweisen Sie, dass genau n! Blätter mit 1/n! und die übrigen mit 0 markiert sind. b. Sei D(T ) die externe Pfadlänge eines Entscheidungsbaumes T , d. h. die Summe der Tiefen aller Blätter von T . Sei T ein Entscheidungsbaum mit k > 1 Blättern und LT bzw. RT der linke bzw. rechte Teilbaum von T . Zeigen Sie, dass D(T ) = D(LT ) + D(RT ) + k gilt. c. Sei d(k) der minimale Wert von D(T ) über alle Entscheidungsbäume T mit k > 1 Blättern. Zeigen Sie, dass d(k) = min1≤i≤k−1 {d(i) + d(k − i) + k} gilt. (Hinweis: Betrachten Sie einen Entscheidungsbaum T mit k Blättern, der das Minimum annimmt. Sei i0 die Anzahl der Blätter in LT und k − i0 die Anzahl der Blätter in RT .) d. Beweisen Sie, dass für einen gegebenen Wert k > 1 und i mit 1 ≤ i ≤ k − 1 die Funktion i lg i + (k − i) lg(k − i) für i = k/2 minimiert wird. Schlussfolgern Sie daraus, dass d(k) = Ω(k lg k) gilt. e. Beweisen Sie, dass D(TA ) = Ω(n! lg(n!)) gilt und schlussfolgern Sie, dass die Zeit im Mittel für das Sortieren von n Elementen in Ω(n lg n) liegt.
Problemstellungen zu Kapitel 8
205
Betrachten Sie nun ein randomisiertes vergleichendes Sortierverfahren B. Wir können das Entscheidungsbaum-Modell auf die Behandlung der Randomisierung erweitern, indem wir zwei Arten von Knoten vorsehen: gewöhnliche Vergleichsknoten und „Randomisierungsknoten“. Ein Randomisierungsknoten modelliert eine vom Algorithmus B vorgenommene zufällige Wahl der Form Random(1, r). Der Knoten hat r Söhne, von denen während einer Ausführung des Algorithmus jeder mit der gleichen Wahrscheinlichkeit gewählt wird. f. Zeigen Sie, dass zu jedem randomisierten vergleichenden Sortierverfahren B ein deterministisches vergleichendes Sortierverfahren A existiert, dessen erwartete Anzahl von Vergleichen nicht größer als die von B ist. 8-2 In-place-Sortieren in linearer Zeit Nehmen Sie an, wir wollten ein Feld mit n Datensätzen sortieren und der Schlüssel eines jeden dieser Datensätze hätte den Wert 0 oder 1. Wünscheswerte Eigenschaften eines Algorithmus zum Sortieren einer Menge solcher Datensätze sind: 1. Der Algorithmus läuft in der Zeit O(n). 2. Der Algorithmus ist stabil. 3. Der Algorithmus sortiert in-place, wobei er zusätzlich zum ursprünglichen Feld nicht mehr als konstant viel Speicherplatz benötigt. a. b. c. d.
Geben Sie einen Algorithmus an, der die Kriterien 1 und 2 erfüllt. Geben Sie einen Algorithmus an, der die Kriterien 1 und 3 erfüllt. Geben Sie einen Algorithmus an, der die Kriterien 2 und 3 erfüllt. Können Sie einen Ihrer Sortieralgorithmen aus (a)–(c) als Sortiermethode in Zeile 2 von Radix-Sort verwenden. sodass Radix-Sort n Datensätze mit b-Bit-Schlüsseln in Zeit O(b n) sortiert? Falls ja, erklären Sie wie; falls nein, erklären Sie, warum dies nicht möglich ist. e. Nehmen Sie an, dass die n Datensätze Schlüssel aus dem Bereich von 1 bis k hätten. Zeigen Sie, wie Countingsort zu modifizieren ist, dass er die Datensätze in-place in der Zeit O(n + k) sortiert. Sie dürfen O(k) Speicherplatz außerhalb des Eingabefeldes verwenden. Ist Ihr Algorithmus stabil? (Hinweis: Wie würden Sie das Problem für k = 3 lösen?)
8-3 Sortieren von Elementen unterschiedlicher Längen a. Gegeben sei ein Feld mit ganzzahligen Werten, wobei die einzelnen Zahlen unterschiedlich viele Stellen haben können. Die Gesamtanzahl der Stellen aller Zahlen des Feldes hat jedoch den festen Wert n. Zeigen Sie, wie das Feld in der Zeit O(n) sortiert werden kann. b. Gegeben sei ein Feld mit Zeichenketten, wobei die einzelnen Zeichenketten aus unterschiedlich vielen Zeichen bestehen können. Die Gesamtanzahl der Zeichen aller Zeichenketten hat jedoch den festen Wert n. Zeigen Sie, wie die Zeichenketten in der Zeit O(n) sortiert werden können. (Beachten Sie, dass hier die gewünschte Ordnung die alphabetische ist, also zum Beispiel a < ab < b.)
206
8 Sortieren in linearer Zeit
8-4 Wasserkrüge Gegeben seien n rote und n blaue Wasserkrüge, die alle verschiedene Formen und Größen haben. Alle roten Krüge nehmen unterschiedliche Mengen Wasser auf; das Gleiche gilt für die blauen Krüge. Außerdem gibt es zu jedem roten Krug einen blauen Krug, der die gleiche Menge Wasser aufnehmen kann, und umgekehrt. Ihre Aufgabe besteht darin, die Krüge in Paare von jeweils einem roten und einem blauen Krug zu gruppieren, die jeweils die gleiche Menge Wasser aufnehmen können. Hierzu können Sie die folgende Operation ausführen: Wählen Sie einen roten und einen blauen Krug aus, füllen Sie den roten mit Wasser und gießen Sie das Wasser dann in den blauen Krug. Dieses Experiment gibt Auskunft, ob der rote oder der blaue Krug mehr Wasser aufnehmen kann oder ob sie das gleiche Volumen haben. Nehmen Sie an, dass ein solcher Vergleich eine Zeiteinheit beansprucht. Ihr Ziel ist es, einen Algorithmus zu finden, der eine minimale Anzahl von Vergleichen ausführt, um die Gruppierung zu bestimmen. Beachten Sie aber, dass Sie zwei rote bzw. zwei blaue Krüge nicht direkt miteinander vergleichen dürfen. a. Geben Sie einen deterministischen Algorithmus an, der Θ(n2 ) Vergleiche ausführt, um die Krüge in Paare zu gruppieren. b. Beweisen Sie eine untere Schranke von Ω(n lg n) für die Anzahl der Vergleiche, die ein Algorithmus, der dieses Problem löst, ausführen muss. c. Geben Sie einen randomisierten Algorithmus an, dessen Erwartungswert für die Anzahl der Vergleiche O(n lg n) ist, und beweisen Sie, dass diese Schranke korrekt ist. Wie viele Vergleiche führt Ihr Algorithmus im schlechtesten Fall aus? 8-5 Sortieren im Mittel Nehmen Sie an, wir würden die Elemente eines Feldes nur im Mittel in aufsteigender Reihenfolge anordnen wollen, anstatt das Feld zu sortieren. Genauer gesagt heißt ein n-elementiges Feld A k-sortiert, wenn für alle i = 1, 2, . . . , n − k Folgendes gilt: i+k i+k−1 A[j] j=i j=i+1 A[j] ≤ . k k a. Was heißt es für ein Feld 1-sortiert zu sein? b. Geben Sie eine Permutation der Zahlen 1, 2, . . . , 10 an, die 2-sortiert, aber nicht sortiert ist. c. Beweisen Sie, dass ein n-elementiges Feld genau dann k-sortiert ist, wenn für alle i = 1, 2, . . . , n − k die Ungleichung A[i] ≤ A[i + k] gilt. d. Geben Sie einen Algorithmus an, der ein n-elementiges Feld in Zeit O(n lg(n/k)) k-sortiert. Wir können auch eine untere Schranke für die Zeit beweisen, die nötig ist, um ein k-sortiertes Feld zu erzeugen, falls k eine Konstante ist. e. Zeigen Sie, dass ein k-sortiertes Feld der Länge n in der Zeit O(n lg k) sortiert werden kann. (Hinweis: Verwenden Sie die Lösung von Übung 6.5-9.)
Problemstellungen zu Kapitel 8
207
f. Für den Fall, dass k eine Konstante ist, zeigen Sie, dass das k-Sortieren eines n-elementigen Feldes Zeit Ω(n lg n) benötigt. (Hinweis: Verwenden Sie die Lösung des vorhergehenden Teilproblems zusammen mit der unteren Schranke für vergleichende Sortierverfahren.) 8-6 Eine untere Schranke für das Mischen von sortierten Listen Das Problem, zwei sortierte Listen zu mischen, kommt häufig vor. Wir haben bereits eine Prozedur zum Mischen zweier sortierter Listen gesehen: die Unterroutine Merge in Abschnitt 2.3.1. In dieser Problemstellung werden wir eine untere Schranke von 2n − 1 für die Anzahl der Vergleiche im schlechtesten Fall beweisen, die notwendig sind, um zwei sortierte, jeweils aus n Elementen bestehenden Listen zu mischen. Zuerst beweisen wir mithilfe eines Entscheidungsbaumes eine untere Schranke von 2n − o(n) Vergleichen. a. Gegeben seien 2n Zahlen. Berechnen Sie die Anzahl der Möglichkeiten, diese Zahlen in zwei sortierte, jeweils aus n Elementen bestehenden Listen aufzuteilen. b. Benutzen Sie einen Entscheidungsbaum und wenden Sie Ihre Antwort aus Teil (a) an, um zu zeigen, dass jeder Algorithmus, der zwei sortierte Listen korrekt mischt, mindestens 2n − o(n) Vergleiche ausführen muss. Nun beweisen wir die etwas schärfere Schranke von 2n − 1. c. Zeigen Sie, dass zwei Elemente miteinander verglichen werden müssen, wenn Sie in der sortierten Ordnung aufeinanderfolgen und aus unterschiedlichen Listen kommen. d. Wenden Sie Ihre Antwort zu Teil (c) an, um eine untere Schranke von 2n − 1 Vergleichen für das Mischen von zwei sortierten Listen zu beweisen. 8-7 Das Null-Eins-Prinzip und Columnsort Unter einer Vergleiche-Vertausche-Operation zweier Komponenten A[i] und A[j] eines Feldes A mit i < j verstehen wir die Operation der Form Compare-Exchange(A, i, j) 1 if A[i] > A[j] 2 vertausche A[i] mit A[j] Nach Ausführung der Vergleiche-Vertausche-Operation gilt A[i] ≤ A[j]. Ein kontextunabhängiger Vergleiche-Vertausche-Algorithmus arbeitet einzig und allein eine Folge von vorspezifizierten Vergleiche-Vertausche-Operationen ab. Die Indizes der Komponenten, auf die die Operationen angewendet werden, sind vorab festgelegt und wenngleich sie von der Anzahl der zu sortierenden Elementen abhängen können, so hängen sie weder von den zu sortierenden Werten noch von dem Ausgang irgendeiner der vorangegangenen Vergleiche-Vertausche-Operationen ab. So kann beispielsweise Sortieren durch Mischen wie folgt als kontextunabhängiger Vergleiche-Vertausche-Algorithmus dargestellt werden:
208
8 Sortieren in linearer Zeit Insertion-Sort(A) 1 for j = 2 to A.l¨a nge 2 for i = j − 1 downto 1 3 Compare-Exchange(A, i, i + 1) Das 0-1-Prinzip gibt uns ein mächtiges Werkzeug an die Hand, um zu beweisen, dass ein kontextunabhängiger Vergleiche-Vertausche-Algorithmus ein sortiertes Ergebnis berechnet. Das Lemma sagt aus, dass wenn ein kontextunabhängiger Vergleiche-Vertausche-Algorithmus alle Eingabefolgen, die nur aus 0en und 1en bestehen, korrekt sortiert, dann sortiert er auch alle aus beliebigen Werten bestehenden Eingabefolgen korrekt. Sie werden das 0-1-Prinzip beweisen, indem Sie die Kontraposition beweisen: wenn ein kontextunabhängiger Vergleiche-Vertausche-Algorithmus eine Eingabefolge nicht korrekt sortiert, dann sortiert er auch nicht alle 0-1 Eingaben korrekt. Nehmen Sie an, ein kontextunabhängiger Vergleiche-Vertausche-Algorithmus X könnte das Feld A[1 . . n] nicht korrekt sortieren. Sei A[p] der kleinste Wert aus A, den Algorithmus X in der Ausgabe falsch platziert, und sei A[q] der Wert, den Algorithmus X an die Stelle platziert, wo A[p] hingehört hätte. Definieren Sie ein Feld B[1 . . n] über 0en und 1en wie folgt: 0 falls A[i] ≤ A[p] , B[i] = 1 falls A[i] > A[p] . a. Überlegen Sie sich, dass A[q] > A[p] gilt und somit B[p] = 0 and B[q] = 1 gelten. b. Um den Beweis des 0-1-Prinzips abzuschließen, beweisen Sie, dass der Algorithmus X das Feld B nicht korrekt sortiert. Wir werden nun das 0-1-Prinzip anwenden, um zu zeigen, dass ein spezieller Sortieralgorithmus korrekt sortiert. Der Algorithmus, der Columnsort genannt wird, arbeitet auf einem rechteckförmigen Feld aus n Elementen. Das Feld besteht aus r Zeilen und s Spalten (sodass n = r s gilt) und unterliegt den folgenden drei Einschränkungen: • r muss gerade sein, • s muss ein Teiler von r sein und • r ≥ 2s2 . Wenn Columnsort terminiert, dann ist das Feld in spaltenorientierter Ordnung sortiert: Liest man von links nach rechts die Spalten nacheinander von oben nach unten, so wachsen die Elemente monoton steigend an. Columnsort besteht aus acht Schritten, unabhängig vom Wert von n. Die ungeraden Schritte sind immer gleich. Sie sortieren die einzelnen Spalten jeweils für sich. Jeder gerade Schritt besteht aus der Ausführung einer bestimmten festen Permutation:
Problemstellungen zu Kapitel 8
209
1. Sortieren Sie jede Spalte. 2. Transponieren Sie das Feld und gestalten Sie es wieder als Feld mit r Zeilen und s Spalten. In anderen Worten, machen Sie aus der linkesten Spalte die oberen r/s Zeilen, ohne die Reihenfolge der Elemente zu verändern, machen Sie dann aus der nächsten Spalte die nächsten r/s Zeilen und so weiter. 3. Sortieren Sie jede Spalte. 4. Führen Sie die inverse, in Schritt 2 ausgeführte Permutation aus. 5. Sortieren Sie jede Spalte. 6. Verschieben Sie die obere Hälfte einer jeden Spalte in die untere Hälfte der gleichen Spalte und die untere Hälfte einer jeden Spalte in die obere Hälfte der nächsten zur rechten Seite liegenden Spalte. Lassen Sie die obere Hälfte der linkesten Spalte leer. Verschieben Sie die untere Hälfte der letzten Spalte in die obere Hälfte einer neuen rechtesten Spalte und lassen Sie die untere Hälfte dieser neuen Spalte leer. 7. Sortieren Sie jede Spalte. 8. Führen Sie die inverse, in Schritt 6 ausgeführte Permutation aus. Abbildung 8.5 illustriert die einzelnen Schritte von Columnsort an einem Beispiel mit r = 6 und s = 3. (Obwohl das Beispiel die Bedingung r ≥ 2s2 verletzt, funktioniert das Verfahren hier.) c. Überlegen Sie, dass wir Columnsort als einen kontextunabhängigen VergleicheVertausche-Algorithmus ansehen können, auch wenn wir nicht wissen, welche Sortiermethode in den ungeraden Schritten verwendet wird. Auch wenn es möglicherweise schwer zu glauben ist, sortiert Columnsort tatsächlich. Sie werden das 0-1-Prinzip anwenden, um dies zu beweisen. Das 0-1Prinzip kann angewendet werden, da wir Columnsort als einen kontextunabhängigen Vergleiche-Vertausche-Algorithmus ansehen können. Einige Definitionen werden uns helfen, das 0-1-Prinzip anzuwenden. Wir sagen, dass ein Bereich des Feldes sauber ist, wenn wir wissen, dass er entweder nur 0en oder nur 1en enthält. Ansonsten enthält der Bereich sowohl 0en als auch 1en und er ist schmutzig. Ab jetzt gehen wir davon aus, dass das Eingabefeld nur 0en und 1en enthält und aus r Zeilen und s Spalten besteht. d. Beweisen Sie, dass nach den Schritten 1–3 das Feld oben aus einigen sauberen 0-Zeilen, unten aus einigen sauberen 1-Zeilen und aus höchstens s schmutzigen Zeilen zwischen diesen sauberen Zeilen besteht. e. Beweisen Sie, dass nach Schritt 4 das Feld, liest man es in spaltenorientierter Ordnung, am Anfang aus einem sauberen 0-Bereich, am Ende aus einem sauberen 1-Bereich und dazwischen aus einem aus höchstens s2 Elementen bestehenden schmutzigen Bereich besteht. f. Beweisen Sie, dass die Schritte 5–8 eine vollständig sortierte 0-1-Ausgabe erzeugt. Folgern Sie, dass Columnsort somit alle aus beliebigen Werten bestehenden Eingaben korrekt sortiert.
210
8 Sortieren in linearer Zeit
10 8 12 16 4 18
14 5 7 17 1 6 9 11 15 2 3 13 (a)
4 8 10 12 16 18
1 2 3 5 7 6 9 11 14 13 15 17 (b)
4 12 1 9 2 11
8 10 16 18 3 7 14 15 5 6 13 17 (c)
1 2 4 9 11 12
3 6 5 7 8 10 13 15 14 17 16 18 (d)
1 2 3 5 6 7
4 11 8 12 9 14 10 16 13 17 15 18 (f)
5 6 7 1 4 2 8 3 9
10 16 13 17 15 18 11 12 14 (g)
4 5 6 1 7 2 8 3 9
10 16 11 17 12 18 13 14 15 (h)
1 2 3 4 5 6
7 13 8 14 9 15 10 16 11 17 12 18 (i)
1 3 6 2 5 7
4 11 8 14 10 17 9 12 13 16 15 18 (e)
Abbildung 8.5: Die Schritte von Columnsort. (a) Das Eingabefeld bestehend aus 6 Zeilen und 3 Spalten. (b) Das Feld, nach dem in Schritt 1 jede Spalte sortiert worden ist. (c) Nach dem Transponieren und Herstellen der Dimension des Feldes in Schritt 2. (d) Nachdem in Schritt 3 jede Spalte sortiert wurde. (e) Nachdem Schritt 4 ausgeführt worden ist, in dem die Permutation aus Schritt 2 rückgängig gemacht worden ist. (f ) Nachdem in Schritt 5 jede Spalte sortiert wurde. (g) Nach dem Verschieben der Spaltenhälften in Schritt 6. (h) Nachdem in Schritt 7 jede Spalte sortiert wurde. (i) Nachdem Schritt 8 ausgeführt worden ist, in dem die Permutation aus Schritt 6 rückgängig gemacht worden ist. Das Feld ist nun spaltenorientiert sortiert.
g. Nehmen Sie nun an, dass s kein Teiler von r ist. Zeigen Sie, dass nach den Schritten 1–3 das Feld oben aus einigen sauberen 0-Zeilen, unten aus einigen sauberen 1-Zeilen und aus höchstens 2s − 1 schmutzigen Zeilen zwischen diesen sauberen Zeilen besteht. Wie groß muss r im Vergleich zu s sein, damit Columnsort korrekt sortiert, auch wenn s kein Teiler von r ist? h. Schlagen Sie eine einfache Änderung von Schritt 1 vor, die dazu führt, dass die Bedingung r ≥ 2s2 weiterhin erfüllt wird, auch wenn s kein Teiler von r ist, und beweisen Sie, dass Columnsort mit Ihrer Änderung immer noch korrekt sortiert.
Kapitelbemerkungen Das Entscheidungsbaum-Modell für die Untersuchung von vergleichenden Sortierverfahren wurde von Ford und Johnson [110] eingeführt. Knuths umfassende Abhandlung über das Sortieren [211] deckt viele Varianten des Sortierproblems einschließlich der hier angegebenen informationstheoretischen unteren Schranke für die Komplexität des Sortierens ab. Ben-Or [39] untersuchte untere Schranken für Sortieren mit Hilfe eines erweiterten Entscheidungsbaum-Modells.
Kapitelbemerkungen zu Kapitel 8
211
Knuth schreibt H. H. Seward die Einführung von Countingsort im Jahr 1954 zu, ebenso die Idee, Countingsort mit Radixsort zu kombinieren. Das Sortieren mit Radixsort, beginnend bei der niedrigsten Wertigkeit, scheint ein populärer Algorithmus zu sein, der vom Bedienpersonal mechanischer Lochkartensortiermaschinen häufig angewendet wurde. Nach Knuth ist der erste veröffentlichte Hinweis auf die Methode ein Dokument von L. J. Comrie aus dem Jahr 1929, das eine Lochkartenmaschine beschreibt. Bucketsort wird seit 1956 angewendet, nachdem die grundlegende Idee durch E. J. Isaac und R. C. Singleton [188] vorgeschlagen wurde. Munro und Raman [263] geben einen stabilen Sortieralgorithmus an, der im schlechtesten Fall O(n1+ ) Vergleiche ausführt. Hierbei ist eine beliebige Konstante mit 0 < ≤ 1. Zwar führt jeder der Algorithmen mit Laufzeit O(n lg n) weniger Vergleiche durch, jedoch hat der Algorithmus von Munro und Raman den Vorteil, dass er nur O(n) Datenbewegungen ausführt und in-place arbeitet. Sortieren von n b-Bit-Integer-Zahlen in Zeit o(n lg n) ist von vielen Wissenschaftlern untersucht worden. Es wurden verschiedene positive Ergebnisse erzielt, wobei jedem etwas andere Annahmen bezüglich des Berechnungsmodells und der Einschränkungen des Algorithmus zugrunde liegen. Alle Ergebnisse setzen voraus, dass der Speicher in adressierbare b-Bit-Worte unterteilt ist. Fredman und Willard [115] führten die Datenstruktur des Verschmelzungsbaumes ein und verwendeten sie, um n ganze Zahlen in der Zeit √ O(n lg n/ lg lg n) zu sortieren. Diese Schranke wurde später von Andersson [16] auf O(n lg n) verbessert. Diese Algorithmen erfordern die Verwendung der Multiplikation sowie verschiedene zuvor berechnete Konstanten. Andersson, Hagerup, Nilsson und Raman [17] haben gezeigt, wie n ganze Zahlen ohne Verwendung der Multiplikation in der Zeit O(n lg lg n) zu sortieren sind, jedoch kann der Speicherplatz, der für ihre Methode erforderlich ist, in n unbeschränkt sein. Durch Verwendung multiplikativen Hashings können wir den Speicherverbrauch auf O(n) reduzierem. Die Schranke O(n lg lg n) für die Laufzeit im schlechtesten Fall wird dann aber eine Schranke für die erwartete Laufzeit. Unter Verallgemeinerung der exponentiellen Suchbäume von Andersson [16] gab Thorup [335] einen Sortieralgorithmus mit der Laufzeit O(n(lg lg n)2 ) an, der weder Multiplikationen noch Randomisierung verwendet und dessen Speicherverbrauch linear ist. Durch Kombination dieser Methoden mit einigen neuen Ideen verbesserte Han [158] die Schranke für das Sortieren auf die Zeit O(n lg lg n lg lg lg n). Diese Algorithmen sind zwar bahnbrechend für die Theorie, sie sind jedoch alle recht kompliziert, und zum gegenwärtigen Zeitpunkt scheint es sehr unwahrscheinlich, dass sie in der Praxis mit den existierenden Algorithmen konkurrieren können. Der Columnsort-Algorithmus aus der Problemstellung 8-7 geht auf Leighton [227] zurück.
9
Mediane und Ranggrößen
Die i-te Ranggröße einer n-elementigen Menge ist das i-kleinste Element. So ist beispielsweise das Minimum einer Menge die erste Ranggröße (i = 1) und das Maximum die n-te Ranggröße (i = n). Ein Median ist, etwas salopp formuliert, der „Mittelpunkt“ der Menge. Wenn n ungerade ist, ist der Median eindeutig bestimmt und liegt bei i = (n + 1)/2. Wenn n gerade ist, gibt es zwei Mediane, nämlich bei i = n/2 und bei i = n/2 + 1. Somit liegen Mediane, ungeachtet der Parität von n, bei i = (n + 1)/2 (der untere Median) und i = (n + 1)/2 (der obere Median). Der Einfachheit halber werden wir in diesem Buch die Bezeichnung „Median“ auf den unteren Median beziehen. Dieses Kapitel behandelt das Problem, die i-te Ranggröße einer Menge von n paarweise verschiedenen Zahlen zu finden. Wir setzen der Bequemlichkeit halber voraus, dass die Menge paarweise verschiedene Zahlen enthält, wenngleich nahezu alles, was wir in diesem Kapitel machen, sich auf den Fall übertragen lässt, in dem die Menge Werte mehrfach enthält. Wir definieren das Auswahlproblem wie folgt: Eingabe: Eine Menge A aus n (paarweise verschiedenen) Zahlen und eine ganze Zahl i mit 1 ≤ i ≤ n. Ausgabe: Das Element x ∈ A, das größer als genau i − 1 andere Elemente von A ist. Wir können das Auswahlproblem in Zeit O(n lg n) lösen, denn wir können die Zahlen mithilfe von Heapsort oder Sortieren durch Mischen sortieren und dann einfach das i-te Element des Ausgabefeldes nehmen. Dieses Kapitel stellt schnellere Algorithmen vor. In Abschnitt 9.1 untersuchen wir das Problem, das Minimum und das Maximum aus einer Menge von Elementen auszuwählen. Interessanter ist das allgemeine Auswahlproblem, das wir in den beiden folgenden Abschnitten untersuchen. Abschnitt 9.2 analysiert einen praktisch anwendbaren randomisierten Algorithmus, der eine erwartete Laufzeit von O(n) erreicht, vorausgesetzt die Elemente sind paarweise verschieden. Abschnitt 9.3 stellt einen Algorithmus von eher theoretischem Interesse vor, der im schlechtesten Fall die Laufzeit O(n) erreicht.
9.1
Minimum und Maximum
Wie viele Vergleiche sind notwendig, um das Minimum einer Menge aus n Elementen zu bestimmen? Wir können leicht eine obere Schranke von n − 1 Vergleichen erhalten: Schauen Sie sich alle Elemente der Menge der Reihe nach an und merken Sie sich das kleinste bisher aufgetretene Element. In der folgenden Prozedur setzen wir voraus, dass die Menge in einem Feld A der Länge n gespeichert ist, d. h. A.l¨a nge = n gilt.
214
9 Mediane und Ranggrößen
Minimum(A) 1 min = A[1] 2 for i = 2 to A.l¨a nge 3 if min > A[i] 4 min = A[i] 5 return min Wir können natürlich auch das Maximum mit n − 1 Vergleichen bestimmen. Ist dies das Beste, was wir tun können? Ja, denn wir können eine untere Schranke von n − 1 Vergleichen für die Bestimmung des Minimums beweisen. Denken Sie an einen beliebigen Algorithmus, der das Minimum durch ein „Turnier“ zwischen den Elementen bestimmt. Jeder Vergleich ist ein Wettkampf des Turniers, bei dem das kleinere der beiden Elemente gewinnt. Die wichtigste Feststellung ist, dass jedes Element außer dem Sieger wenigstens ein Kampf verlieren muss. Folglich sind n − 1 Vergleiche notwendig, um das Minimum zu bestimmen. Somit ist der Algorithmus Minimum optimal, was die Anzahl der durchgeführten Vergleiche betrifft.
Simultane Bestimmung des Minimums und des Maximums Bei manchen Anwendungen müssen wir sowohl das Minimum als auch das Maximum einer Menge von n Elementen bestimmen. Beispielsweise muss ein Grafikprogramm eine Menge von Daten der Form (x, y) skalieren, um sie an einen rechteckigen Bildschirm oder ein anderes grafisches Ausgabegerät anzupassen. Um dies tun zu können, muss das Programm zuerst den minimalen und maximalen Wert der beiden Koordinaten bestimmen. An dieser Stelle des Buches sollte es bereits klar sein, wie man beides, das Minimum und das Maximum von n Elementen, mittels Θ(n) Vergleichen bestimmen kann, was asymptotisch optimal ist: Sie haben einfach nur das Minimum und das Maximum unabhängig voneinander zu bestimmen, indem sie jedesmal n − 1 Vergleiche machen, sodass Sie zusammen 2n − 2 Vergleiche benötigen. Wir können aber tatsächlich das Minimum und Maximum mit einer Gesamtanzahl von höchstens 3 n/2 Vergleichen bestimmen. Dies erreichen wir dadurch, dass wir uns die bei Vergleichen bisher gefundenen kleineren Werte und die bei den Vergleichen bisher gefundenen größeren Werte merken. Anstatt jedes Element der Eingabe mit dem aktuellen Maximum und dem aktuellen Minimum zu vergleichen, was zu jeweils zwei Vergleichen pro Element führt, verarbeiten wir die Elemente zunächst paarweise. Wir vergleichen Paare von Elementen der Eingabe zuerst miteinander und anschließend das kleinere von beiden mit dem aktuellen Minimum und das größere mit dem aktuellen Maximum. Bei diesem Vorgehen sind drei Vergleiche für je zwei Elemente notwendig. Wie wir den Wert des aktuellen Minimums und des aktuellen Maximums initialisieren, hängt davon ab, ob n gerade oder ungerade ist. Ist n ungerade, setzen wir Maximum und Minimum auf den Wert des ersten Elementes und verarbeiten dann die restlichen Elemente paarweise. Wenn n gerade ist, führen wir einen Vergleich zwischen den beiden ersten Elementen durch, um die Anfangswerte für das Maximum und das Minimum zu
9.2 Auswahl in linearer erwarteter Zeit
215
bestimmen, und verarbeiten dann die restlichen Elemente paarweise wie in dem Fall, in dem n ungerade ist. Lassen Sie uns die Gesamtanzahl der Vergleiche analysieren. Für ungerades n führen wir 3 n/2 Vergleiche durch. Für gerades n kommen zu dem initialen Vergleich 3(n − 2)/2 Vergleiche hinzu, was insgesamt zu 3n/2 − 2 Vergleichen führt. Damit ist in beiden Fällen die Gesamtanzahl der Vergleiche höchstens 3 n/2.
Übungen 9.1-1 Zeigen Sie, dass das zweitkleinste Element aus n Elementen im schlechtesten Fall mit n + lg n − 2 Vergleichen gefunden werden kann. (Hinweis: Bestimmen Sie auch das kleinste Element.) 9.1-2∗ Zeigen Sie, dass im schlechtesten Fall 3n/2 − 2 Vergleiche notwendig sind, um sowohl das Maximum als auch das Minimum von n Zahlen zu bestimmen. (Hinweis: Untersuchen Sie, wie viele Zahlen potentiell entweder Maximum oder Minimum sein können und wie ein Vergleich diese Anzahl beeinflusst.)
9.2
Auswahl in linearer erwarteter Zeit
Das allgemeine Auswahlproblem erscheint viel schwieriger als das einfache Problem, das Minimum und das Maximum zu bestimmen. Doch überraschender Weise ist die asymptotische Laufzeit für beide Probleme die gleiche: Θ(n). In diesem Abschnitt stellen wir einen Teile-und-Beherrsche-Algorithmus für das Auswahlproblem vor. Der Algorithmus Randomized-Select ist in Anlehnung an den Quicksort-Algorithmus aus Kapitel 7 aufgebaut. Wie bei Quicksort zerlegen wir das Eingabefeld rekursiv. Im Unterschied zum Quicksort-Algorithmus, der beide Seiten der Partition rekursiv bearbeitet, arbeitet Randomized-Select jeweils nur auf einer Seite der Partition weiter. Dieser Unterschied zeigt sich in der Analyse: Während Quicksort eine erwartete Laufzeit von Θ(n lg n) hat, ist die erwartete Laufzeit von Randomized-Select Θ(n), vorausgesetzt, die Elemente sind paarweise verschieden. Randomized-Select verwendet die Prozedur Randomized-Partition, die in Abschnitt 7.3 eingeführt wurde. Daher ist der Algorithmus wie Randomized-Quicksort ein randomisierter Algorithmus, dessen Verhalten teilweise durch die Ausgabe eines Zufallszahlengenerators bestimmt wird. Der folgende Code für Randomized-Select gibt die i-te Ranggröße des Feldes A[p . . r] zurück.
216
9 Mediane und Ranggrößen
Randomized-Select(A, p, r, i) 1 if p = = r 2 return A[p] 3 q = Randomized-Partition(A, p, r) 4 k = q−p+1 5 if i = = k // der Pivotwert wird zurückgegeben 6 return A[q] 7 elseif i < k 8 return Randomized-Select(A, p, q − 1, i) 9 else return Randomized-Select(A, q + 1, r, i − k) Die Prozedur Randomized-Select arbeitet wie folgt. Zeile 1 überprüft den Basisfall der Rekursion, in dem das Teilfeld A[p . . r] nur aus einem Element besteht. In diesem Fall muss i gleich 1 sein und wir geben in Zeile 2 einfach nur A[p] als i-kleinstes Element zurück. Ansonsten partitioniert der Aufruf von Randomized-Partition in Zeile 3 das Feld A[p . . r] so in zwei (möglicherweise leere) Teilfelder A[p . . q − 1] und A[q + 1 . . r], dass jedes Element aus A[p . . q −1] kleiner oder gleich A[q] ist, welches wiederum kleiner als jedes Element aus A[q + 1 . . r] ist. Wie bei Quicksort bezeichnen wir A[q] als Pivotelement. Zeile 4 von Randomized-Select berechnet die Anzahl k der Elemente im Teilfeld A[p . . q], d. h. die Anzahl der Elemente im unteren Teil der Zerlegung, plus eins wegen des Pivotelements. Zeile 5 prüft dann, ob A[q] das i-kleinste Element ist. Falls ja, wird A[q] zurückgegeben. Anderenfalls ermittelt der Algorithmus, in welchem der beiden Teilfelder A[p . . q − 1] und A[q + 1 . . r] die i-te Ranggröße liegt. Im Falle i < k liegt das gesuchte Element im unteren Teil der Zerlegung und Zeile 8 bestimmt es rekursiv in diesem Teilfeld. Im Falle i > k dagegen liegt das gesuchte Element im anderen Teil der Zerlegung. Da wir bereits k Elemente kennen, die kleiner sind als das i-kleinste Element von A[p . . r] – nämlich die Elemente von A[p . . q] –, ist das gesuchte Elemente die (i − k)-te Ranggröße von A[q + 1 . . r], die Zeile 9 rekursiv bestimmt. Der Programmcode scheint rekursive Aufrufe für Felder mit 0 Elementen zu erlauben. In Übung 9.2-1 sollen Sie jedoch zeigen, dass diese Situation nicht eintreten kann. Die Laufzeit von Randomized-Select im schlechtesten Fall liegt selbst für die Bestimmung des Minimums in Θ(n2 ), da wir extremes Pech haben könnten und das Feld immer bezüglich des größten verbleibenden Elementes zerlegt werden könnte. Das Zerlegen benötigt dann Zeit Θ(n). Wir werden sehen, dass der Algorithmus jedoch eine lineare erwartete Laufzeit hat, und, da er randomisiert ist, keine spezielle Eingabe das Verhalten im schlechtesten Fall auslöst. Um die erwartete Laufzeit von Randomized-Select zu analysieren, beschreiben wir die Laufzeit des Verfahrens angewendet auf ein n-elementiges Eingabefeld A[p . . r] durch eine Zufallsvariable, die wir mit T (n) bezeichnen. Eine obere Schranke für den Erwartungswert E[T (n)] erhalten wir durch die folgenden Überlegungen. Die Prozedur Randomized-Partition gibt jedes Element mit gleicher Wahrscheinlichkeit als Pivotelement zurück. Für jedes k mit 1 ≤ k ≤ n hat das Teilfeld A[p . . q] daher mit Wahrscheinlichkeit 1/n genau k Elemente (die alle kleiner oder gleich dem Pivotelement sind). Für k = 1, 2, . . . , n können wir dann Indikatorfunktionen Xk durch Xk = I {das Teilfeld A[p . . q] enthält genau k Elemente}
9.2 Auswahl in linearer erwarteter Zeit
217
definieren. Unter der Annahme, dass die Elemente paarweise verschieden sind, gilt dann (9.1)
E [Xk ] = 1/n .
Wenn wir Randomized-Select aufrufen und A[q] als Pivotelement wählen, wissen wir a priori nicht, ob der Algorithmus sofort (mit der richtigen Antwort) terminiert, ob er auf dem Teilfeld A[p . . q − 1] oder ob er auf dem Teilfeld A[q + 1 . . r] rekursiv weiterarbeitet. Diese Entscheidung hängt davon ab, wo die i-te Ranggröße relativ zu A[q] liegt. Wenn T (n) monoton steigend in n ist, dann können wir die Laufzeit, der durch den rekursiven Aufruf benötigt wird, nach oben beschränken durch die Laufzeit des rekursiven Aufrufes mit dem größten der beiden Teilfeldern. Mit anderen Worten, um eine obere Schranke zu erhalten, nehmen wir an, dass die i-te Ranggröße immer auf der Seite der Partition liegen würde, die die größere Anzahl von Elementen enthält. Für einen gegebenen Aufruf von Randomized-Select hat die Indikatorfunktion Xk für genau einen Wert von k den Wert 1 und für alle anderen Werten von k den Wert 0. Im Falle Xk = 1 haben die beiden Teilfelder, von denen in eines rekursiv abgestiegen wird, die Längen k − 1 und n − k. Folglich gilt die Rekursionsgleichung T (n) ≤ =
n k=1 n
Xk · (T (max(k − 1, n − k)) + O(n)) Xk · T (max(k − 1, n − k)) + O(n) .
k=1
Nehmen wir die Erwartungswerte, dann führt dies zu " E [T (n)] ≤ E
n
# Xk · T (max(k − 1, n − k)) + O(n)
k=1
=
n
E [Xk · T (max(k − 1, n − k))] + O(n)
k=1
(wegen der Linearität des Erwartungswertes) =
n
E [Xk ] · E [T (max(k − 1, n − k))] + O(n)
k=1
(nach Gleichung (C.24)) n 1 · E [T (max(k − 1, n − k))] + O(n) = n k=1
(nach Gleichung (9.1)) . Um Gleichung (C.24) anwenden zu können, haben wir ausgenutzt, dass die Zufallsvariablen Xk und T (max(k − 1, n − k)) voneinander unabhängig sind. In Übung 9.2-2 sollen Sie diese Behauptung nachweisen.
218
9 Mediane und Ranggrößen
Betrachten wir nun den Ausdruck max(k − 1, n − k). Es gilt k−1 für k > n/2 , max(k − 1, n − k) = n−k für k ≤ n/2 . Wenn n gerade ist, erscheint jeder Term von T (n/2 ) bis T (n − 1) genau zweimal in der Summe; für ungerades n erscheinen alle diese Terme zweimal und T ( n/2) einmal. Somit gilt 2 n
E [T (n)] ≤
n−1
E [T (k)] + O(n) .
k=n/2
Wir zeigen E [T (n)] = O(n) mit Hilfe der Substitutionsmethode. Nehmen Sie an, es würde E [T (n)] ≤ c n für eine Konstante c gelten, die die Anfangsbedingungen der Rekursionsgleichung erfüllt. Wir nehmen weiter an, dass T (n) = O(1) für n kleiner als eine noch näher zu bestimmende Konstante gilt – diese Konstante werden später noch bestimmen. Außerdem wählen wir eine Konstante a derart, dass die Funktion, die in der obigen Ungleichung durch den O(n)-Term beschrieben wird, für alle n > 0 von oben durch a n beschränkt ist. Verwenden wir dies als Induktionsvoraussetzung, erhalten wir E [T (n)] ≤
2 n
n−1
ck + an
k=n/2
⎛ ⎞ n/2−1 n−1 2c ⎝ = k− k⎠ + a n n k=1 k=1 2c (n − 1)n ( n/2 − 1) n/2 − + an = n 2 2 2c (n − 1)n (n/2 − 2)(n/2 − 1) − ≤ + an n 2 2 2c n2 − n n2 /4 − 3n/2 + 2 − = + an n 2 2 n c 3n2 + − 2 + an = n 4 2 3n 1 2 =c + − + an 4 2 n 3c n c + + an ≤ 4 2 cn c = cn− − − an . 4 2 Um den Beweis zu vervollständigen, müssen wir zeigen, dass der letzte Ausdruck für hinreichend große n höchstens gleich c n ist, oder, äquivalent dazu, dass die Ungleichung c n/4 − c/2 − a n ≥ 0 gilt. Wenn wir auf beiden Seiten der Ungleichung c/2 addieren
9.3 Auswahl in linearer Zeit im schlechtesten Fall
219
und auf der linken Seite n ausklammern, erhalten wir n(c/4 − a) ≥ c/2. Wenn wir die Konstante c so wählen, dass c/4 − a > 0 gilt (d. h. c > 4a), können wir auf beiden Seiten durch c/4 − a dividieren, was zu n≥
2c c/2 = c/4 − a c − 4a
führt. Wenn wir also voraussetzen, dass für n < 2c/(c − 4a) die Gleichung T (n) = O(1) gilt, erhalten wir E [T (n)] = O(n). Somit können wir jede Ranggröße, insbesondere den Median, in linearer erwarteter Zeit bestimmen, vorausgesetzt, die Elemente sind paarweise verschieden.
Übungen 9.2-1 Zeigen Sie, dass Randomized-Select niemals einen rekursiven Aufruf macht, dessen Parameter ein Feld der Länge 0 ist. 9.2-2 Erklären Sie, weshalb die Indikatorfunktion Xk und T (max(k − 1, n − k)) voneinander unabhängig sind. 9.2-3 Geben Sie eine iterative Version von Randomized-Select an. 9.2-4 Nehmen Sie an, wir würden Randomized-Select anwenden, um das kleinste Element des Feldes A = 3, 2, 9, 0, 7, 5, 4, 8, 6, 1 zu bestimmen. Beschreiben Sie eine Folge von Partitionen, die zum Verhalten von Randomized-Select im schlechtesten Fall führt.
9.3
Auswahl in linearer Zeit im schlechtesten Fall
Wir untersuchen nun einen Algorithmus, dessen Laufzeit im schlechtesten Fall O(n) ist. Wie Randomized-Select findet der Algorithmus Select das gesuchte Element durch rekursive Zerlegung des Eingabefeldes. Hier garantieren wir jedoch, dass das Feld gut partitioniert wird. Select verwendet den deterministischen Zerlegungsalgorithmus Partition von Quicksort (siehe Abschnitt 7.1), der aber so modifiziert ist, dass er das Pivotelement als Eingabeparameter erhält. Der Algorithmus Select bestimmt die i-te Ranggröße eines Eingabefeldes mit n > 1 paarweise verschiedenen Elementen durch Ausführung der folgenden Schritte. (Im Falle n = 1 gibt Select einfach seinen einzigen Eingabewert als i-kleinster Wert zurück.) 1. Teilen Sie die n Elemente des Eingabefeldes in n/5 Gruppen von jeweils 5 Elementen und höchstens eine weitere Gruppe, die die verbleibenden n mod 5 Elemente enthält. 2. Bestimmen Sie den Median für jede der n/5 Gruppen. Wenden Sie dazu zunächst Sortieren durch Einfügen auf die Elemente einer jeden Gruppe an (es gibt davon jeweils höchstens 5) und wählen Sie dann jeweils den Median aus der sortierten Liste der Gruppenelemente aus.
220
9 Mediane und Ranggrößen
x
Abbildung 9.1: Analyse des Algorithmus Select. Die n Elemente sind durch kleine Kreise dargestellt, und jede Gruppe von jeweils fünf Elementen nimmt eine Spalte ein. Die Mediane der Gruppen sind weiß dargestellt, und der Median x der Mediane ist entsprechend gekennzeichnet. (Wenn der Median einer geraden Anzahl von Elementen zu bestimmen ist, suchen wir nach dem unteren Median.) Die eingezeichneten Pfeile weisen von den größeren zu den kleineren Elementen. Aus diesen ist abzulesen, dass drei Elemente aus jeder vollständigen Gruppe mit fünf Elementen auf der rechten Seite von x größer als x sind und dass drei Elemente aus jeder Gruppe von fünf Elementen auf der linken Seite von x kleiner als x sind. Der schattierte Bereich enthält die Elemente, von denen wir wissen, dass sie größer als x sind.
3. Wenden Sie Select rekursiv an, um den Median x der n/5 Mediane zu finden, die in Schritt 2 bestimmt wurden. (Wenn es eine gerade Zahl von Medianen gibt, dann ist nach unserer Konvention mit x der untere Median gemeint.) 4. Zerlegen Sie das Eingabefeld um den Median x der Mediane mithilfe der modifizierten Version von Partition. Sei k um eins größer als die Anzahl der Elemente in dem Teilfeld der Partition, die die kleineren Werte enthält. Somit ist x die k-te Ranggröße, und in dem Teilfeld der Partition mit den größeren Werten sind n − k Elemente. 5. Wenn i = k gilt, so ist der Rückgabewert x. Anderenfalls wenden Sie Select rekursiv an, um im Falle i < k die i-te Ranggröße in dem Teilfeld mit den kleineren Werten oder im Falle i > k die (i − k)-te Ranggröße in dem Teilfeld mit den größeren Elementen zu finden. Um die Laufzeit von Select zu analysieren, bestimmen wir zunächst eine untere Schranke für die Anzahl der Elemente, die größer als das Pivotelement x sind. Abbildung 9.1 hilft uns, die Zählung visuell zu erfassen. Mindestens die Hälfte der Mediane, die in Schritt 2 gefunden werden, sind größer oder gleich dem Median der Mediane.1 Somit trägt mindestens die Hälfte der n/5 Gruppen drei Elemente bei, die größer als x sind, abgesehen von derjenigen Gruppe, die weniger als fünf Elemente besitzt, wenn n nicht ohne Rest durch 5 teilbar ist, und der Gruppe, die x selbst enthält. Lassen wir 1 Da wir vorausgesetzt haben, dass die Zahlen paarweise verschieden sind, sind alle Mediane mit Ausnahme von x entweder größer oder kleiner als x.
9.3 Auswahl in linearer Zeit im schlechtesten Fall
221
diese beiden Gruppen unberücksichtigt, folgt für die Anzahl der Elemente, die größer als x sind, die untere Schranke 1 n 3n −6 . 3 −2 ≥ 2 5 10 Entsprechend sind mindestens 3n/10 − 6 Elemente kleiner als x. Somit ruft Schritt 5 die Prozedur Select für höchstens 7n/10 + 6 viele Elemente rekursiv auf. Wir können nun eine Rekursionsgleichung für die Laufzeit T (n) von Select im schlechtesten Fall entwickeln. Die Schritte 1, 2 und 4 benötigen Zeit O(n). (Schritt 2 besteht aus O(n) Aufrufen von Sortieren durch Einfügen auf einer Menge der Größe O(1).) Schritt 3 braucht die Zeit T (n/5 ), und Schritt 5 braucht höchstens die Zeit T (7n/10 + 6), vorausgesetzt, T ist monoton steigend. Wir setzen voraus, dass jede Eingabe mit weniger als 140 Elementen in Zeit O(1) bearbeitet werden kann. Diese Voraussetzung erscheint sicherlich zunächst ziemlich unmotiviert. Woher diese magische Zahl 140 kommt, wird aber bald klar werden. Wir erhalten damit die Rekursionsgleichung O(1) falls n < 140 , T (n) ≤ T (n/5 ) + T (7n/10 + 6) + O(n) falls n ≥ 140 . Wir zeigen durch Substitution, dass die Laufzeit linear ist. Genauer gesagt, werden wir zeigen, dass für eine geeignet große Konstante c und alle n > 0 die Ungleichung T (n) ≤ c n gilt. Wir starten mit der Annahme, dass T (n) ≤ c n für eine geeignet große Konstante c und alle n mit n < 140 gilt. Dies ist erfüllt, wenn c tatsächlich hinreichend groß gewählt worden ist. Wir wählen außerdem eine Konstante a derart, dass die Funktion, die in der obigen Ungleichung durch den O(n)-Term (dieser beschreibt den nichtrekursiven Anteil der Laufzeit) beschrieben wird, für alle n > 0 von oben durch a n beschränkt ist. Setzen wir diese Induktionsannahme in die rechte Seite der obigen Rekursionsgleichung ein, so erhalten wir T (n) ≤ c n/5 + c(7n/10 + 6) + a n ≤ c n/5 + c + 7c n/10 + 6c + a n = 9c n/10 + 7c + a n = c n + (−c n/10 + 7c + a n) , was höchstens c n ist, falls die Ungleichung −c n/10 + 7c + a n ≤ 0
(9.2)
erfüllt ist. Für n > 70 ist Ungleichung (9.2) äquivalent zu c ≥ 10 a (n/(n − 70)). Da wir n ≥ 140 voraussetzen, gilt n/(n − 70) ≤ 2. Wenn wir also c ≥ 20 a wählen, ist die Ungleichung (9.2) erfüllt. (Man beachte, dass das Gesagte nicht von der speziellen Wahl der Konstante 140 abhängt; wir könnten diese durch eine beliebige ganze Zahl, die größer als 70 ist, ersetzen und dann c entsprechend wählen.) Die Laufzeit von Select im schlechtesten Fall ist also linear. Wie bei einem vergleichenden Sortierverfahren (siehe Abschnitt 8.1) ermitteln Select und Randomized-Select Informationen über die Reihenfolge der Elemente nur durch
222
9 Mediane und Ranggrößen
Vergleichen von Elementen. Rufen wir uns aus Kapitel 8 in Erinnerung, dass das auf Vergleichen basierte Sortieren selbst im mittleren Fall Zeit Ω(n lg n) erfordert (siehe Problemstellung 8-1). Die Sortieralgorithmen mit linearer Laufzeit, die wir in Kapitel Kapitel 8 gesehen haben, setzen bestimmte Annahmen über die Eingabe voraus. Im Gegensatz zu diesen setzen die in diesem Kapitel betrachteten Auswahlalgorithmen mit linearer Laufzeit keinerlei Annahmen über die Eingabe voraus. Sie sind jedoch nicht der unteren Schranke Ω(n lg n) unterworfen, da sie in der Lage sind, das Auswahlproblem ohne Sortieren zu lösen. Das Lösen des Auswahlproblems unter Zuhilfenahme von Algorithmen zum Sortieren und zur Indexberechnung, wie am Anfang dieses Kapitels vorgestellt, ist also asymptotisch ineffizient.
Übungen 9.3-1 Im Algorithmus Select werden die Elemente in Gruppen der Größe 5 aufgeteilt. Läuft der Algorithmus in linearer Zeit, wenn sie in Gruppen der Größe 7 aufgeteilt werden? Erläutern Sie, warum Select nicht in linearer Zeit läuft, wenn Dreiergruppen verwendet werden. 9.3-2 Analysieren Sie Select, um zu zeigen, dass für n ≥ 140 mindestens n/4 Elemente größer als der Median x der Mediane und mindestens n/4 Elemente kleiner als x sind. 9.3-3 Zeigen Sie, wie man unter der Voraussetzung, dass die Elemente paarweise verschieden sind, Quicksort auf eine Laufzeit O(n lg n) für den schlechtesten Fall bringen kann. 9.3-4∗ Nehmen Sie an, ein Algorithmus würde nur Vergleiche verwenden, um das i-kleinste Element aus einer Menge mit n Elementen zu bestimmen. Zeigen Sie, dass er auch die i − 1 kleineren Elemente und die n − i größeren Elemente ohne zusätzliche Vergleiche berechnen kann. 9.3-5 Nehmen Sie an, Sie hätten eine „Blackbox-Routine“ für die Bestimmung des Medians mit linearer Laufzeit im schlechtesten Fall. Geben Sie einen einfachen Algorithmus mit linearer Laufzeit an, der das Auswahlproblem für beliebige Ranggrößen löst. 9.3-6 Die k-ten Quantilen einer n-elementigen Menge sind die k − 1 Ranggrößen, die die sortierte Menge in k gleichgroße Mengen aufteilen. Geben Sie einen Algorithmus mit Laufzeit O(n lg k) an, der die k-ten Quantilen einer Menge auflistet. 9.3-7 Geben Sie einen Algorithmus mit Laufzeit O(n) an, der zu einer gegebenen Menge S von n paarweise verschiedenen Zahlen und einer positiven ganzen Zahl k ≤ n diejenigen k Zahlen bestimmt, die am nächsten am Median von S liegen. 9.3-8 Es seien X[1 . . n] und Y [1 . . n] zwei Felder, die jeweils n Zahlen in sortierter Reihenfolge enthalten. Geben Sie einen Algorithmus mit Laufzeit O(lg n) an, der den Median aller 2n in den Feldern X und Y enthaltenen Elemente bestimmt.
Problemstellungen zu Kapitel 9
223
Abbildung 9.2: Professor Olay soll die Lage der Ost-West-Pipeline so bestimmen, dass die Gesamtlänge der Nord-Süd-Verbindungen minimal wird.
9.3-9 Professor Olay berät eine Ölgesellschaft, die eine große von Ost nach West verlaufende Pipeline durch ein Ölfeld mit n Brunnen plant. Die Ölgesellschaft will, wie in Abbildung 9.2 gezeigt, die Nebenleitung von einem jeden Brunnen jeweils direkt über einen kürzesten Weg (entweder Richtung Norden oder Richtung Süden) an die Hauptpipeline verbinden. Wie sollte der Professor bei gegebenen xund y-Koordinaten der Brunnen die Lage der Hauptpipeline wählen, damit diese optimal in dem Sinne wird, dass die Gesamtlänge der Nebenpipelines minimiert wird? Zeigen Sie, wie die optimale Lage in linearer Zeit bestimmt werden kann.
Problemstellungen 9-1 Die i größten Zahlen in sortierter Reihenfolge Aus einer gegebenen Menge von n Zahlen wollen wir die i größten in sortierter Reihenfolge bestimmen, wobei wir einen vergleichsbasierten Algorithmus verwenden wollen. Überlegen Sie sich für jeden der folgenden Ansätze jeweils einen Algorithmus mit bestmöglicher asymptotischer Laufzeit im schlechtesten Fall und analysieren Sie die Laufzeiten in Abhängigkeit von n und i. a. Sortieren Sie die Zahlen und listen Sie die i größten auf. b. Konstruieren Sie aus den Zahlen eine Max-Prioritätswarteschlange und rufen Sie Extract-Max i-mal auf. c. Verwenden Sie einen Algorithmus zur Bestimmung der Ranggröße, um die igrößte Zahl zu finden, verwenden Sie diese Zahl als Pivot, um die Eingabefolge zu partitionieren, und sortieren Sie die i größten Zahlen.
224
9 Mediane und Ranggrößen
9-2 Gewichteter Median Für n paarweise verschiedene Elemente x1 , x2 , . . . , xn mit positiven Gewichten n w1 , w2 , . . . , wn , für die i=1 wi = 1 gilt, ist der gewichtete (untere) Median das Element xk , das die Bedingungen 1 wi < 2 x xk
wi ≤
1 2
erfüllt. Der gewichtete Median beispielsweise der Elemente 0.1, 0.35, 0.05, 0.1, 0.15, 0.05, 0.2 ist 0.2, setzt man das Gewicht eines jeden Elementes jeweils auf das Element selbst (d. h. wi = xi für i = 1, 2, . . . , 7). Der „normale“ Median ist gleich 0.1. a. Erläutern Sie, weshalb der Median von x1 , x2 , . . . , xn der gewichtete Median der xi mit den Gewichten wi = 1/n für i = 1, 2, . . . , n ist. b. Zeigen Sie, wie der gewichtete Median von n Elementen mittels Sortieren in einer Laufzeit O(n lg n) im schlechtesten Fall berechnet werden kann. c. Zeigen Sie, wie der gewichtete Median mithilfe eines Median-Algorithmus mit linearer Laufzeit (zum Beispiel mit Select aus Abschnitt 9.3) in einer Laufzeit Θ(n) im schlechtesten Fall berechnet werden kann. Das Postamt-Standortproblem ist wie folgt definiert. Gegeben seien n Punkte p1 , p2 , . . . , pn mit den Gewichten w1 , w2 , . . . , wn . Wir wollen einen Punkt p finden, der die Summe ni=1 wi d(p, pi ) minimiert. Dies muss nicht notwendigerweise einer der Eingabepunkte sein. Hierbei ist d(a, b) der Abstand zwischen den Punkten a und b. d. Erläutern Sie, weshalb der gewichtete Median eine beste Lösung für das eindimensionale Postamt-Standortproblem darstellt. Bei diesem Problem sind die Punkte reelle Zahlen und der Abstand zwischen zwei Punkten a und b ist durch d(a, b) = |a − b| gegeben. e. Bestimmen Sie die beste Lösung für das zweidimensionale Postamt-Standortproblem, bei dem die Punkte Koordinatenpaare (x, y) sind und der Abstand zwischen zwei Punkten a = (x1 , y1 ) und b = (x2 , y2 ) durch die ManhattanDistanz d(a, b) = |x1 − x2 | + |y1 − y2 | gegeben ist. 9-3 Kleine Ranggrößen Wir haben gezeigt, dass die Anzahl T (n) der Vergleiche, die Select im schlechtesten Fall benötigt, um die i-te Ranggröße aus n Zahlen zu bestimmen, in Θ(n) liegt. Allerdings ist die in der Θ-Notation versteckte Konstante ziemlich groß. Wenn i im Vergleich zu n klein ist, können wir eine andere Prozedur implementieren, die Select als Unterroutine verwendet, die aber im schlechtesten Fall weniger Vergleiche ausführt.
Kapitelbemerkungen zu Kapitel 9
225
a. Geben Sie einen Algorithmus an, der Ui (n) Vergleiche benötigt, um die i-te Ranggröße von n Elementen zu finden, wobei T (n) falls i ≥ n/2 , Ui (n) = sonst
n/2 + Ui (n/2 ) + T (2i) gilt. (Hinweis: Beginnen Sie mit n/2 paarweise disjunkten Vergleichen und bearbeiten Sie rekursiv die Menge, die von jedem Paar das kleinere Element enthält.) b. Zeigen Sie, dass für i < n/2 die Gleichung Ui (n) = n + O(T (2i) lg(n/i)) gilt. c. Zeigen Sie, dass Ui (n) = n + O(lg n) gilt, falls i eine Konstante ist, die kleiner als n/2 ist. d. Zeigen Sie, dass Ui (n) = n + O(T (2n/k) lg k) für i = n/k und k ≥ 2 gilt. 9-4 Alternative Analyse der randomisierten Auswahl In dieser Problemstellung verwenden wir Indikatorfunktionen, um die Prozedur Randomized-Select in einer Art und Weise zu analysieren, die dem Vorgehen bei unserer Analyse der Prozedur Randomized-Quicksort aus Abschnitt 7.4.2 sehr ähnlich ist. Wie bei der Analyse von Quicksort setzen wir voraus, dass alle Elemente paarweise verschieden sind. Wir nennen die Elemente des Eingabefeldes A um und bezeichnen Sie mit z1 , z2 , . . . , zn , wobei zi das i-kleinste Element darstellt. Somit gibt der Aufruf von Randomized-Select(A, 1, n, k) das Element zk zurück. Für 1 ≤ i < j ≤ n, sei Xijk = I { zi wird während der Ausführung des Algorithmus mit zj verglichen, wenn der Algorithmus zk als Ergebnis bestimmt} . a. Geben Sie einen exakten Ausdruck für E [Xijk ] an. (Hinweis: Ihr Ausdruck kann abhängig von den Werten von i, j und k unterschiedlich aussehen.) b. Xk bezeichne die Gesamtanzahl von Vergleichen zwischen Elementen des Feldes A, wenn zk als Ergebnis bestimmt wird. Zeigen Sie ⎞ ⎛ k n k−2 n 1 j−k−1 k − i − 1⎠ E [Xk ] ≤ 2 ⎝ + + . j − i + 1 j − k + 1 k−i+1 i=1 i=1 j=k
j=k+1
c. Zeigen Sie E [Xk ] ≤ 4n. d. Schlussfolgern Sie, dass Randomized-Select eine erwartete Laufzeit von O(n) hat, falls alle Elemente des Feldes A paarweise verschieden sind.
Kapitelbemerkungen Der Algorithmus zur Bestimmung des Medians mit linearer Laufzeit für den schlechtesten Fall wurde von Blum, Floyd, Pratt, Rivest und Tarjan [50] entwickelt. Die schnelle
226
9 Mediane und Ranggrößen
randomisierte Version des Algorithmus geht auf Hoare [169] zurück. Floyd und Rivest [108] haben eine verbesserte randomisierte Version entwickelt, bei der das Feld um ein Pivotelement partitioniert wird, das rekursiv aus einer kleinen Stichprobe der Elemente ausgewählt wird. Es ist immer noch offen, wie viele Vergleiche exakt notwendig sind, um den Median zu bestimmen. Bent und John [41] beweisen eine untere Schranke von 2n Vergleichen zur Berechnung des Medians und Schönhage, Paterson, und Pippenger [302] geben eine obere Schranke von 3n an. Dor und Zwick haben beide Schranken verbessert. Ihre obere Schranke [93] liegt etwas unterhalb von 2,95 n und ihre untere Schranke [94] liegt bei (2 + ) n für eine kleine positive Konstante , was ein bisschen besser ist als entsprechende Arbeiten von Dor et al. [92]. Paterson [272] beschreibt einige dieser Ergebnisse zusammen mit weiteren Arbeiten zu diesem Thema.
Teil III
Datenstrukturen
Einführung Mengen sind in der Informatik ebenso wichtig wie in der Mathematik. Während Mengen in der Mathematik unverändert bleiben, können die von Algorithmen manipulierten Mengen wachsen, schrumpfen oder sich anderswie über die Zeit verändern. Wir nennen solche Mengen dynamisch. Die folgenden fünf Kapitel stellen einige grundlegende Methoden zur Darstellung von endlichen dynamischen Mengen und deren Manipulation auf einem Rechner vor. Algorithmen können mehrere unterschiedliche Operationen auf Mengen erfordern. Viele benötigen beispielsweise nur die Möglichkeit, Elemente in eine Menge einzufügen, Elemente aus einer Menge zu entfernen und testen zu können, ob ein Element in einer Menge enthalten ist. Eine dynamische Menge, die diese Operationen unterstützt, wird als Wörterbuch bezeichnet. Andere Algorithmen benötigen kompliziertere Operationen. Min-Prioritätswarteschlangen beispielsweise, die in Kapitel 6 im Zusammenhang mit der Heap-Datenstruktur eingeführt wurden, unterstützen das Einfügen eines Elementes in eine Menge und das Entnehmen des kleinsten Elementes aus einer Menge. Wie eine dynamische Menge am besten implementiert werden sollte, hängt von den Operationen ab, die unterstützt werden sollen.
Elemente einer dynamischen Menge In einer typischen Realisierung einer dynamischen Menge wird jedes Objekt durch ein Element repräsentiert, dessen Attribute geprüft und manipuliert werden können, wenn wir einen Zeiger auf das Objekt haben. (Abschnitt 10.3 behandelt die Implementierung von Objekten und Zeigern in Programmierumgebungen, die diese nicht als grundlegende Datentypen enthalten.) Verschiedene dynamische Mengen setzen voraus, dass eines der Objektattribute ein identifizierender Schlüssel ist. Wenn die Schlüssel paarweise verschieden sind, dann können wir die dynamische Menge als eine Menge von Schlüsselwerten ansehen. Das Objekt kann Satellitendaten enthalten, die in anderen Objektattributen mitgeführt werden, aber ansonsten von der Implementierung der Menge nicht verwendet werden. Das Objekt kann auch Attribute besitzen, die durch Mengenoperationen manipuliert werden. Diese Attribute können Daten oder Zeiger auf andere Objekte der Menge enthalten. Einige dynamische Mengen setzen voraus, dass die Schlüssel Elemente einer vollständig geordneten Menge sind, wie zum Beispiel der Menge der reellen Zahlen oder der Menge aller Wörter unter Verwendung der üblichen alphabetischen Reihenfolge. Eine vollständige Ordnung erlaubt uns beispielsweise, das minimale Element der Menge zu definieren, oder von dem zu einem gegebenen Element nächstgrößeren Element zu sprechen.
230
Teil III Datenstrukturen
Operationen auf dynamischen Mengen Operationen auf dynamischen Mengen können in zwei Kategorien unterteilt werden: Abfragen, die einfach nur Informationen über die Menge zurückgeben, und modifizierende Operationen, die die Menge verändern. Hier nun eine Liste typischer Operationen auf dynamischen Mengen, wobei viele Anwendungen jeweils nur eine Teilmenge dieser Operationen benutzen: Search(S, k) Eine Anfrage, die für eine gegebene Menge S und einen Schlüsselwert k einen Zeiger x auf ein Element aus S mit x.schl¨u ssel = k zurückgibt bzw. nil zurückgibt, wenn kein solches Element zu S gehört. Insert(S, x) Eine modifizierende Operation, die die Menge S um dasjenige Element erweitert, auf das x zeigt. Wir setzen gewöhnlich voraus, dass die Attribute des Elements x, die bei der Mengenimplementierung benötigt werden, bereits initialisiert worden sind. Delete(S, x) Eine modifizierende Operation, die x aus der Menge S entfernt, wobei x ein Zeiger auf ein Element von S ist. (Beachten Sie, dass diese Operation einen Zeiger auf ein Element als Parameter erhält, keinen Schlüsselwert.) Minimum(S) Eine Anfrage an eine vollständig geordnete Menge S, die einen Zeiger auf das Element von S mit dem kleinsten Schlüssel zurückgibt. Maximum(S) Eine Anfrage an eine vollständig geordnete Menge S, die einen Zeiger auf das Element von S mit dem größten Schlüssel zurückgibt. Successor(S, x) Eine Anfrage, die einen Zeiger auf das zu x nächstgrößere Element in S zurückgibt bzw. nil zurückgibt, wenn x bereits das Maximum von S ist. Predecessor(S, x) Eine Anfrage, die einen Zeiger auf das zu x nächstkleinere Element in S zurückgibt bzw. nil zurückgibt, wenn x bereits das Minimum von S ist. In einigen Situation können wir die Anfragen Successor und Predecessor so erweitern, dass sie anwendbar auf Mengen mit nichtdisjunkten Schlüsseln sind. Für eine Menge von n Schlüsseln setzen wir normalerweise voraus, dass der Aufruf von Minimum, gefolgt von n − 1 Aufrufen der Funktion Successor, die Elemente der Menge gemäß der sortierten Reihenfolge aufzählt. Wir messen üblicherweise die Zeit, die benötigt wird, um eine Mengenoperation auszuführen, bezüglich der Größe der Menge. Beispielsweise beschreibt Kapitel 13 eine Datenstruktur, die jede der oben aufgeführten Operationen auf einer Menge der Größe n in Zeit O(lg n) unterstützt.
Einführung
231
Überblick über Teil III Die Kapitel 10–14 beschreiben mehrere Datenstrukturen, die wir zur Implementierung dynamischer Mengen verwenden können; wir werden viele von ihnen weiter hinten im Buch zur Konstruktion effizienter Algorithmen für eine Vielzahl von Problemen anwenden. Wir haben bereits in Kapitel 6 eine andere wichtige Datenstruktur für dynamische Mengen gesehen – der Heap. Kapitel 10 stellt die wesentlichen Grundlagen vor, um mit einfachen Datenstrukturen, wie Stapeln, Warteschlangen, verketteten Listen und gerichteten Bäumen arbeiten zu können. Das Kapitel erläutert auch, wie Objekte und Zeiger in Programmiersprachen implementiert werden können, die diese nicht als Grunddatentypen enthalten. Wenn Sie bereits einen Programmierkurs belegt haben, dann sollte Ihnen ein Großteil dieses Stoffes bekannt sein. Kapitel 11 führt Hashtabellen ein, die die Wörterbuchoperationen Insert, Delete und Search unterstützen. Im schlechtesten Fall benötigt das Hashing zum Ausführen der Operation Search Zeit Θ(n), während die mittlere Zeit für Operationen auf HashTabellen in O(1) liegt. Die Analyse des Hashings greift auf Wahrscheinlichkeitstheorie zurück; die meisten Teile dieses Kapitels erfordern jedoch kein Hintergrundwissen auf diesem Gebiet. Die in Kapitel 12 behandelten binären Suchbäume unterstützen alle oben genannten Operationen. Jede dieser Operationen benötigt auf einem Baum mit n Elementen im schlechtesten Fall Zeit Θ(n); auf einem zufällig konstruierten Baum ist jedoch die erwartete Zeit für jede Operation nur O(lg n). Binäre Suchbäume dienen als Basis für viele andere Datenstrukturen. Kapitel 13 führt Rot-Schwarz-Bäume ein, die eine Variante binärer Suchbäume sind. Anders als bei normalen binären Suchbäumen ist es bei Rot-Schwarz-Bäumen garantiert, dass sie gut arbeiten: Die Operationen benötigen auch im schlechtesten Fall Zeit O(lg n). Ein Rot-Schwarz-Baum ist ein balancierter Suchbaum; Kapitel 18 in Teil V stellt eine weitere Variante eines balancierten Suchbaumes vor, die als B-Baum bezeichnet wird. Obwohl der Mechanismus von Rot-Schwarz-Bäumen etwas kompliziert ist, können Sie die meisten ihrer Eigenschaften aus dem Kapitel verstehen, ohne den Mechanismus im Detail zu studieren. Nichtsdestotrotz werden Sie es wahrscheinlich ziemlich aufschlussreich finden, wenn Sie sich den Code näher anschauen. Im Kapitel 14 zeigen wir, wie Rot-Schwarz-Bäume erweitert werden können, damit sie über die oben genannten Grundoperationen hinaus auch andere Operationen unterstützen. Zuerst erweitern wir sie so, dass wir die Ranggrößen für eine Menge von Schlüsseln dynamisch verwalten können. Dann erweitern wir sie auf andere Weise, damit auch die Verwaltung von Intervallen reeller Zahlen unterstützt wird.
10
Elementare Datenstrukturen
In diesem Kapitel behandeln wir die Darstellung dynamischer Mengen durch einfache Datenstrukturen, die Zeiger verwenden. Obwohl wir viele komplexe Datenstrukturen mithilfe von Zeigern modellieren können, stellen wir nur die elementarsten dar, nämlich Stapel, Warteschlangen, verkettete Listen und gerichtete Bäume. Zudem zeigen wir, wie Objekte und Zeiger mit Feldern realisiert werden können.
10.1
Stapel und Warteschlangen
Stapel und Warteschlangen sind dynamische Mengen, bei denen das Element, das durch die Operation Delete aus der Menge entfernt wird, vorbestimmt ist. In einem Stapel wird dasjenige Element entfernt, das als letztes zur Menge hinzugefügt wurde. Damit wird durch den Stapel eine last-in-, first-out-Strategie, abgekürzt LIFO-Strategie, implementiert. Ganz ähnlich dazu wird in einer Warteschlange immer das Element entfernt, das sich am längsten in der Menge befindet. Die Warteschlange implementiert somit eine first-in-, first-out-Strategie, also eine FIFO-Strategie. Es gibt mehrere effiziente Methoden, Stapel und Warteschlangen auf einem Rechner zu implementieren. In diesem Abschnitt zeigen wir, wie wir dies in beiden Fällen durch ein einfaches Feld realisieren können.
Stapel Auf einem Stapel wird die Operation Insert häufig mit Push bezeichnet. Die Operation Delete, die kein Argument benötigt, wird häufig Pop genannt. Diese Bezeichnungen sind aus Anlehnungen an reale Stapel, wie zum Beispiel Tellerstapel, entstanden. Die Reihenfolge, in der die Teller vom Stapel genommen werden, ist umgekehrt zu der Reihenfolge, in der die Teller auf den Stapel gestellt wurden, da nur der oberste Teller zugänglich ist. Wie Abbildung 10.1 zeigt, können wir einen Stapel, der zu jedem Zeitpunkt höchstens n Elemente enthalten darf, durch ein Feld S[1 . . n] implementieren. Das Feld besitzt ein Attribut S.top, das das zuletzt abgelegte Element indiziert. Der Stapel besteht aus den Elementen S[1 . . S.top], wobei S[1] das Element am Boden des Stapels und S[S.top] das oberste Element des Stapels ist. Im Fall S.top = 0 enthält der Stapel keine Elemente und ist leer. Wir können mittels der Anfrageoperation Stack-Empty testen, ob der Stapel leer ist. Wenn wir versuchen, von einem leeren Stapel ein Element zu entnehmen, dann sprechen wir von Unterlauf des Stapels (engl.: stack underflow ), was normalerweise ein Fehler sein sollte. Wenn
234
10 Elementare Datenstrukturen 1
2
3
4
S 15 6
2
9
5
6
7
1
2
3
4
S 15 6
2
9 17 3
S.top = 4
(a)
5
6
7
S.top = 6
(b)
1
2
3
4
5
6
S 15 6
2
9 17 3
7
S.top = 5
(c)
Abbildung 10.1: Die Implementierung eines Stapels mithilfe eines Feldes. Stapelelemente befinden sich nur an den schwach schattierten Positionen. (a) Der Stapel S hat 4 Elemente. Das oberste Element ist 9. (b) Der Stapel S nach den Aufrufen Push(S, 17) und Push(S, 3). (c) Der Stapel S, nachdem der Aufruf Pop(S) das Element 3 zurückgegeben hat. Dies ist zugleich das zuletzt abgelegte Element. Obwohl Element 3 noch im Feld enthalten ist, ist es nicht mehr im Stapel enthalten. Das oberste Element des Stapels ist der Wert 17.
S.top den Wert n überschreitet, dann sprechen wir von einem Überlauf des Stapels (engl.: stack overflow ). (In unserer Pseudocode-Implementierung achten wir nicht auf Überlauf des Stapels.) Wir können jede der Stapeloperationen mit wenigen Zeilen Code implementieren: Stack-Empty(S) 1 if S.top == 0 2 return wahr 3 else return falsch Push(S, x) 1 S.top = S.top + 1 2 S[S.top] = x Pop(S) 1 if Stack-Empty(S) 2 error “Unterlauf” 3 else S.top = S.top − 1 4 return S[S.top + 1] Abbildung 10.1 zeigt die Effekte der modifizierenden Operationen Push und Pop. Jede der drei Stapeloperationen benötigt Zeit O(1).
Warteschlangen Wir bezeichnen die Operation Insert auf einer Warteschlange mit Enqueue. Die Operation Delete bezeichnen wir mit Dequeue. Wie die Stapeloperation Pop verwendet Dequeue kein Argument. Die FIFO-Eigenschaft bewirkt, dass die Warteschlange wie eine Schlange von Bankkunden vor einem Bankschalter arbeitet. Die Schlange hat einen
10.1 Stapel und Warteschlangen 1
(a)
2
3
4
5
6
Q
7
235
8
9
10 11 12
15 6
9
8
Q.kopf = 7
(b)
1
2
Q 3
5
3
4
5
(c)
2
Q 3
5
3
4
Q.ende = 3
7
Q.ende = 12
8
9
10 11 12
15 6
9
8
8
9
10 11 12
15 6
9
8
4 17
Q.kopf = 7
Q.ende = 3 1
6
4
5
6
7
4 17
Q.kopf = 8
Abbildung 10.2: Die Implementierung einer Warteschlange mithilfe eines Feldes Q[1 . . 12]. Elemente der Warteschlange befinden sich nur an den schwach schattierten Positionen. (a) Die Warteschlange hat 5 Elemente an den Stellen Q[7 . . 11]. (b) Die Konfiguration der Warteschlange nach den Aufrufen Enqueue(Q, 17), Enqueue(Q, 3) und Enqueue(Q, 5). (c) Die Konfiguration der Warteschlange, nachdem der Aufruf Dequeue(Q) den Schlüsselwert 15 zurückgegeben hat, der sich ehemals am Kopf der Warteschlange befand. Der neue Kopf hat den Schlüssel 6.
Kopf und ein Ende. Wenn ein Element eingereiht wird, dann nimmt es seinen Platz am Ende der Schlange ein, genau wie ein neu ankommender Bankkunde seinen Platz am Ende der Reihe einnimmt. Das Element, das aus einer Warteschlange entfernt wird, ist immer dasjenige am Kopf der Schlange, genau wie der Bankkunde vorne in der Reihe – er wartet bereits am längsten – als nächster dran kommt. Abbildung 10.2 zeigt eine Möglichkeit, wie wir eine Warteschlange, die zu jedem Zeitpunkt höchstens n−1 Elemente enthalten kann, mithilfe eines Feldes Q[1 . . n] implementieren können. Die Warteschlange besitzt ein Attribut Q.kopf , das den Kopf indiziert bzw. darauf zeigt. Das Attribut Q.ende indiziert die nächste Stelle, an die ein neu ankommendes Element in die Warteschlange eingefügt wird. Die Elemente innerhalb der Warteschlange befinden sich an den Stellen Q.kopf , Q.kopf + 1, . . . , Q.ende − 1, wobei wir das Feld als einen Ring auffassen, in dem die Stelle 1 unmittelbar auf Stelle n folgt. Falls Q.kopf = Q.ende gilt, ist die Warteschlange leer. Zu Beginn gilt Q.kopf = Q.ende = 1. Wenn wir versuchen, ein Element aus einer leeren Warteschlange zu entnehmen, so kommt es zu einem Unterlauf. Ist Q.kopf = Q.ende + 1 oder gilt sowohl Q.kopf = 1 als auch Q.ende = Q.länge, so ist die Warteschlange voll und, wenn wir versuchen ein weiteres Element in die Warteschlange einzufügen, erhalten wir einen Überlauf. In unseren Prozeduren Enqueue und Dequeue haben wir die Fehlerkontrolle bezüglich eines Überlaufs und Unterlaufs ausgespart. (In Übung 10.1-4 sollen Sie den Pseudocode
236
10 Elementare Datenstrukturen
so erweitern, dass er auf diese beiden Fehlerereignisse überprüft.) Der Pseudocode geht davon aus, dass n = Q.l¨a nge gilt. Enqueue(Q, x) 1 Q[Q.ende] = x 2 if Q.ende = = Q.l¨a nge 3 Q.ende = 1 4 else Q.ende = Q.ende + 1 Dequeue(Q) 1 x = Q[Q.kopf ] 2 if Q.kopf == Q.l¨a nge 3 Q.kopf = 1 4 else Q.kopf = Q.kopf + 1 5 return x Abbildung 10.2 illustriert die Arbeitsweise der Operationen Enqueue und Dequeue. Jede Operation benötigt Zeit in O(1).
Übungen 10.1-1 Illustrieren Sie analog zu Abbildung 10.1 das Ergebnis jeder Operation der Befehlsfolge Push(S, 4), Push(S, 1), Push(S, 3), Pop(S), Push(S, 8) und Pop(S) auf einem anfangs leeren Stapel S, der in einem Feld S[1 . . 6] gespeichert ist. 10.1-2 Erklären Sie, wie man zwei Stapel in einem Feld A[1 . . n] implementieren kann, sodass keiner der Stapel einen Überlauf verursacht, außer wenn die Gesamtanzahl der Elemente in beiden Stapeln zusammen n beträgt. Die Operationen Push und Pop sollen in Zeit O(1) laufen. 10.1-3 Illustrieren Sie analog zu Abbildung 10.2 das Ergebnis jeder Operation der Befehlsfolge Enqueue(Q, 4), Enqueue(Q, 1), Enqueue(Q, 3), Dequeue(Q), Enqueue(Q, 8) und Dequeue(Q) in einer anfangs leeren Warteschlange Q, die im Feld Q[1 . . 6] gespeichert ist. 10.1-4 Schreiben Sie Enqueue und Dequeue so um, dass Unter- und Überlauf einer Warteschlange erkannt werden. 10.1-5 Während ein Stapel das Einfügen und Löschen von Elementen nur an einem gleichen Ende zulässt und eine Warteschlange Einfügen nur an einem Ende und Entfernen nur am anderen Ende zulässt, ist es bei einer Doppelschlange möglich, an beiden Enden Elemente sowohl einzufügen als auch zu entnehmen. Schreiben Sie vier Prozeduren, die jeweils in Zeit O(1) arbeiten und Elemente an beiden Enden einer Doppelschlange, die durch ein Feld implementiert wird, einfügen und entnehmen können.
10.2 Verkettete Listen
237
10.1-6 Zeigen Sie, wie eine Warteschlange unter Verwendung von zwei Stapeln implementiert werden kann. Analysieren Sie die Laufzeit der WarteschlangeOperationen. 10.1-7 Zeigen Sie, wie ein Stapel unter Verwendung von zwei Warteschlangen implementiert werden kann. Analysieren Sie die Laufzeit der Stapeloperationen.
10.2
Verkettete Listen
Eine verkettete Liste ist eine Datenstruktur, in der die Objekte in linearer Reihenfolge angeordnet sind. Im Unterschied zu einem Feld, bei dem die lineare Reihenfolge durch die Feldindizes bestimmt wird, wird die Reihenfolge bei verketteten Listen durch einen in jedem Objekt zur Verfügung stehenden Zeiger festgelegt. Verkettete Listen bilden eine einfache, flexible Darstellung dynamischer Mengen, die (nicht notwendigerweise effizient) alle Operationen unterstützt, die auf Seite 230 aufgezählt wurden. Wie Abbildung 10.3 zeigt, ist jedes Element einer doppelt verketteten Liste L ein Objekt mit einem Attribut schl¨u ssel und zwei anderen Attributen nachf und vorg, die Zeiger darstellen. Das Objekt kann auch noch weitere Satellitendaten enthalten. Für ein gegebenes Element x der Liste zeigt x.nachf auf dessen Nachfolger in der verketteten Liste und x.vorg auf dessen Vorgänger. Im Falle x.vorg = nil besitzt das Element x keinen Vorgänger und ist deshalb das erste Element oder der Kopf der Liste. Analog hat das Element x im Falle x.nachf = nil keinen Nachfolger und ist deshalb das letzte Element oder das Ende der Liste. Ein Attribut L.kopf zeigt auf das erste Element der Liste. Wenn L.kopf = nil gilt, so ist die Liste leer. vorg schlüssel nachf (a)
L.kopf
9
16
4
1
(b)
L.kopf
25
9
16
4
(c)
L.kopf
25
9
16
1
1
Abbildung 10.3: (a) Eine doppelt verkettete Liste L, die die dynamische Menge {1, 4, 9, 16} repräsentiert. Jedes Element in der Liste ist ein Objekt mit Attributen für den Schl u ¨ ssel und die Zeiger (durch Pfeile veranschaulicht) auf das nachfolgende und das vorangegangene Objekt. Das Attribut nachf des letzten Elements und das Attribut vorg des Kopfes sind nil, was durch die diagonalen Striche gekennzeichnet ist. Das Attribut L. kopf der Liste L zeigt auf den Kopf der Liste. (b) Nach der Ausführung von List-Insert(L, x) mit x. schl u ¨ ssel = 25 hat die verkettete Liste ein neues Objekt mit dem Schl u ¨ ssel 25, das den neuen Kopf der Liste bildet. Dieses neue Objekt zeigt auf den alten Kopf mit dem Schlüssel 9. (c) Das Ergebnis des anschließenden Aufrufes List-Delete(L, x), wenn x auf das Objekt mit dem Schl u ¨ ssel 4 zeigt.
Eine Liste kann verschiedene Formen annehmen. Sie kann entweder eine einfach oder eine doppelt verkettete Liste sein, sie kann sortiert oder nichtsortiert sein und sie kann
238
10 Elementare Datenstrukturen
zyklisch oder nichtzyklisch sein. Wenn eine Liste einfach verkettet ist, dann steht der Zeiger vorg in den Elementen nicht zur Verfügung. Wenn eine Liste sortiert ist, dann entspricht die lineare Reihenfolge der Liste der linearen Reihenfolge der Schlüssel, die in den Elementen der Liste abgespeichert sind. Das minimale Element bildet dann den Kopf der Liste und das maximale Element das Ende. Wenn die Liste unsortiert ist, können die Elemente in beliebiger Reihenfolge vorkommen. In einer zyklischen Liste zeigt der Zeiger vorg des Kopfes der Liste auf das Ende und der Zeiger nachf des letzten Elementes auf den Kopf der Liste. Wir können uns eine zyklische Liste als eine ringförmige Anordnung von Elementen vorstellen. Im Rest dieses Abschnitts setzen wir immer voraus, dass die Listen, mit denen wir arbeiten, unsortiert und doppelt verkettet sind.
Durchsuchen einer verketteten Liste Die Prozedur List-Search(L, k) bestimmt in einer Liste das erste Element mit dem Schlüssel k und gibt einen Zeiger auf dieses Element zurück. Wenn in der Liste kein Objekt mit dem Schlüssel k vorkommt, dann gibt die Prozedur nil zurück. Für die verkettete Liste in Abbildung 10.3(a) gibt der Aufruf von List-Search(L, 4) einen Zeiger auf das dritte Element zurück. Der Aufruf von List-Search(L, 7) liefert nil. List-Search(L, k) 1 x = L.kopf 2 while x = nil and x.schl¨u ssel = k 3 x = x.nachf 4 return x Zum Durchsuchen einer Liste von n Objekten benötigt die Prozedur List-Search im schlechtesten Fall Zeit Θ(n), da möglicherweise die gesamte Liste zu durchsuchen ist.
Einfügen in eine verkettete Liste Die Prozedur List-Insert fügt ein gegebenes Element x, dessen Attribut schl¨u ssel bereits gesetzt ist, vorne in die verkettete Liste ein (siehe Abbildung 10.3(b)). List-Insert(L, x) 1 x.nachf = L.kopf 2 if L.kopf = nil 3 L.kopf .vorg = x 4 L.kopf = x 5 x.vorg = nil (Erinnern Sie sich daran, dass unsere Notation für Attribute hintereinandergeschaltet werden kann, sodass L.kopf .vorg das Attribut vorg des Objektes darstellt, auf das L.kopf zeigt.) Die Laufzeit von List-Insert auf einer Liste von n Elementen ist O(1).
10.2 Verkettete Listen
239
Entfernen aus einer verketteten Liste Die Prozedur List-Delete entfernt ein Element x aus einer verketteten Liste L. Es muss ein Zeiger auf x gegeben sein. Dann wird x durch Aktualisierung von Zeigern aus der Liste „entfernt“. Wenn wir ein Element mit einem gegebenen Schlüssel entfernen wollen, dann müssen wir zuerst List-Search aufrufen, um den Zeiger auf das entsprechende Element aufzufinden. List-Delete(L, x) 1 if x.vorg = nil 2 x.vorg .nachf = x.nachf 3 else L.kopf = x.nachf 4 if x.nachf = nil 5 x.nachf .vorg = x.vorg Abbildung 10.3(c) zeigt, wie ein Element aus einer verketteten Liste entfernt wird. Die Prozedur List-Delete läuft in Zeit O(1). Wollen wir ein Element mit einem gegebenen Schlüssel entfernen, dann ist im schlechtesten Fall Zeit Θ(n) notwendig, da wir zuerst die Prozedur List-Search aufrufen müssen, um das Element zu finden.
Wächter Der Code für List-Delete wäre einfacher, wenn wir die Randbedingungen am Kopf und am Ende der Liste ignorieren könnten. List-Delete (L, x) 1 x.vorg .nachf = x.nachf 2 x.nachf .vorg = x.vorg Ein Wächter ist ein Dummy-Objekt, das es uns erlaubt, die Randbedingungen zu vereinfachen. Nehmen Sie zum Beispiel an, dass wir mit der Liste L ein Objekt L.nil zur Verfügung stellen würden, das den Wert nil darstellt, sonst aber alle Attribute der anderen Listenelemente besitzt. Wann immer wir im Quellcode für Listen einen Verweis auf nil haben, ersetzen wir ihn durch einen Verweis auf den Wächter L.nil . Wie in Abbildung 10.4 illustriert, wandelt dies eine normale doppelt verkettete Liste in eine zyklische doppelt verkettete Liste mit einem Wächter um, in der sich der Wächter zwischen dem Kopf und dem Ende der Liste befindet. Das Attribut L.nil.nachf zeigt auf den Kopf der Liste und L.nil.vorg zeigt auf deren Ende. Ebenso zeigen sowohl das Attribut nachf des letzten Elementes als auch das Attribut vorg des Kopfes der Liste L auf L.nil . Da L.nil.nachf auf den Kopf der Liste zeigt, benötigen wir das Attribut L.kopf nicht mehr, da wir alle Verweise auf den Kopf der Liste mittels L.nil.nachf realisieren können. Abbildung 10.4(a) zeigt, dass eine leere Liste lediglich aus dem Wächter besteht und sowohl L.nil.nachf als auch L.nil .vorg auf L.nil zeigen. Der Pseudocode für List-Search bleibt unverändert bis auf die gerade angegebenen Veränderungen in Bezug auf die Verweise auf nil und L.kopf .
240
10 Elementare Datenstrukturen
(a)
L.nil
(b)
L.nil
9
16
4
1
(c)
L.nil
25
9
16
4
(d)
L.nil
25
9
16
4
1
Abbildung 10.4: Eine zyklische, doppelt verkettete Liste mit einem Wächter. Der Wächter L. nil liegt zwischen Kopf und Ende der Liste. Das Attribut L. kopf wird nicht mehr benötigt, da wir mithilfe von L. nil . nachf auf den Kopf der Liste zugreifen können. (a) Eine leere Liste. (b) Die verkettete Liste aus Abbildung 10.3(a) mit dem Schüssel 9 am Kopf und dem Schlüssel 1 am ¨ ssel = 25 Ende der Liste. (c) Die Liste nach Ausführen von List-Insert (L, x), wenn x. schl u gilt. Das neue Objekt wird Kopf der Liste. (d) Die Liste nach Entfernen des Objektes mit dem Schlüssel 1. Das neue Ende der Liste ist das Objekt mit dem Schlüssel 4.
List-Search (L, k) 1 x = L.nil.nachf 2 while x = L.nil and x.schl¨u ssel = k 3 x = x.nachf 4 return x Wir verwenden die zweizeilige Prozedur List-Delete von vorhin, um Elemente aus der Liste zu entfernen. Die folgende Prozedur fügt ein Element in die Liste ein: List-Insert (L, x) 1 x.nachf = L.nil .nachf 2 L.nil .nachf .vorg = x 3 L.nil .nachf = x 4 x.vorg = L.nil Abbildung 10.4 zeigt die Arbeitsweise von List-Insert und List-Delete angewendet auf eine Beispielliste. Wächter reduzieren selten die asymptotischen Zeitschranken von Datenstruktur-Operationen, können jedoch zu kleineren konstanten Faktoren führen. Der Vorteil der Verwendung von Wächtern innerhalb von Schleifen liegt gewöhnlich eher in der Klarheit des Codes als in der Geschwindigkeit. Zum Beispiel wird der Code für verkettete Listen durch die Verwendung von Wächtern einfacher. Die in den Prozeduren List-Insert und List-Delete gesparte Laufzeit ist jedoch nur konstant. In anderen Situationen hilft die Verwendung von Wächtern, den Code innerhalb der Schleife zu straffen. Dies kann in der Laufzeit zu kleineren Koeffizienten zum Beispiel von n oder n2 führen.
10.2 Verkettete Listen
241
Wir sollten Wächter mit Bedacht anwenden. Wenn es sich um viele kleine Listen handelt, dann kann der für deren Wächter verwendete zusätzliche Speicher eine signifikante Verschwendung von Arbeitsspeicher darstellen. In diesem Buch benutzen wir Wächter nur, wenn sie den Programmcode wirklich vereinfachen.
Übungen 10.2-1 Können Sie die Operation Insert für über einfach verkettete Listen realisierte dynamische Mengen in Zeit O(1) implementieren? Wie verhält es sich mit der Prozedur Delete? 10.2-2 Implementieren Sie einen Stapel mithilfe einer einfach verketteten Liste L. Die Operationen Push und Pop sollten weiterhin in Zeit O(1) arbeiten. 10.2-3 Implementieren Sie eine Warteschlange mithilfe einer einfach verketteten Liste. Die Operationen Enqueue und Dequeue sollten weiterhin in Zeit O(1) arbeiten. 10.2-4 Wie bereits beschrieben, erfordert jede Iteration der Schleife in List-Search zwei Tests: einen auf x = L.nil und einen auf x.schl¨u ssel = k. Zeigen Sie, wie der Test auf x = L.nil in jeder Iteration eliminiert werden kann. 10.2-5 Implementieren Sie die Wörterbuchoperationen Insert, Delete und Search mithilfe von einfach verketteten zyklischen Listen. Wie sind die Laufzeiten Ihrer Prozeduren? 10.2-6 Die Operation Union auf dynamischen Mengen erhält zwei disjunkte Mengen S1 und S2 als Eingabe und gibt eine Menge S = S1 ∪ S2 zurück, die aus allen Elementen von S1 und S2 besteht. Die Mengen S1 und S2 können durch diese Operation gelöscht werden. Zeigen Sie, wie Union durch Verwendung einer geeigneten Listen-Datenstruktur realisiert werden kann, sodass die Laufzeit der Operation in O(1) liegt. 10.2-7 Geben Sie eine in Zeit Θ(n) laufende nichtrekursive Prozedur an, die eine einfach verkettete Liste aus n Elementen spiegelt. Die Prozedur sollte nur konstant viel Speicherplatz benutzen, abgesehen von dem Platz, der durch die Liste selbst belegt wird. 10.2-8∗ Erklären Sie, wie eine doppelt verkettete Liste mit nur einem Zeiger x.np pro Eintrag implementiert werden kann – gewöhnlich werden doppelt verkettete Listen mit zwei Zeigern (nachf und vorg) implementiert. Nehmen Sie an, dass alle Zeigerwerte als k-bit Integer-Zahlen interpretiert werden können und definieren Sie x.np als x.np = x.nachf XOR x.vorg , also als das komponentenweise „exklusiv-Oder“ von x.nachf und x.vorg . (Der Wert nil wird durch 0 repräsentiert.) Stellen Sie sicher, dass Sie die Information zur Verfügung stellen, die Sie benötigen, um auf den Kopf der Liste zugreifen zu können. Zeigen Sie, wie die Operationen Search, Insert und Delete auf einer solchen Liste implementiert werden können. Zeigen Sie auch, wie eine solche Liste in Zeit O(1) gespiegelt werden kann.
242 L
10 Elementare Datenstrukturen 7
nachf schlüssel vorg
1
2
3 4 5
3
1 2
4
5
2 16 7
6
7
8
5 9
Abbildung 10.5: Die verkettete Liste aus Abbildung 10.3(a), dargestellt durch die Felder schl u ¨ ssel , nachf und vorg . Jede Spalte des Feldes entspricht einem einzelnen Objekt. Die gespeicherten Zeiger entsprechen den oben angezeigten Feldindizes. Die Pfeile zeigen, wie die Zeiger zu interpretieren sind. Schwach schattierte Objektpositionen enthalten Listenelemente. Die Variable L enthält den Index des Kopfes.
10.3
Implementierung von Zeigern und Objekten
Wie werden Zeiger und Objekte in Programmiersprachen implementiert, die solche Konstrukte an sich nicht bereitstellen? In diesem Abschnitt werden wir zwei Möglichkeiten kennenlernen, wie verkettete Datenstrukturen ohne expliziten Zeiger-Datentyp implementiert werden können. Wir werden Objekte und Zeiger aus Feldern und Feldindizes zusammensetzen.
Darstellung der Objekte durch mehrere Felder Wir können eine Menge von Objekten, die die gleichen Attribute haben, darstellen, indem wir für jedes Attribut ein extra Feld verwenden. Beispielsweise zeigt Abbildung 10.5, wie wir die verkettete Liste aus Abbildung 10.3(a) mithilfe von drei Feldern implementieren können. Das Feld schl¨u ssel enthält die Werte der Schlüssel, die sich gegenwärtig in der dynamischen Menge befinden. Die Zeiger werden in den Feldern nachf und vorg gespeichert. Für einen gegebenen Feldindex x stellt schl¨u ssel [x], nachf [x] und vorg[x] ein Objekt in der verketteten Liste dar. In dieser Interpretation ist ein Zeiger x einfach ein gemeinsamer Index zu den Feldern schl¨u ssel , nachf und vorg. In der verketteten Liste in Abbildung 10.3(a) folgt das Objekt mit dem Schlüssel 4 dem Objekt mit dem Schlüssel 16. In Abbildung 10.5 erscheint der Schlüssel 4 in schl¨u ssel [2] und der Schlüssel 16 erscheint in schl¨u ssel [5] und somit gilt nachf [5] = 2 und vorg[2] = 5. Zur Darstellung der Konstante nil im Attribut nachf des letzten Elements und im Attribut vorg des Kopfes der Liste, benutzen wir gewöhnlich eine Integer-Zahl (wie zum Beispiel 0 oder −1), die keinem wirklichen Index innerhalb der Felder entspricht. Die Variable L enthält den Index des Kopfes der Liste.
Darstellung der Objekte durch ein einzelnes Feld Die Wörter im Speicher sind typischerweise durch Integer-Zahlen von 0 bis M − 1 adressiert, wobei M eine hinreichend große Integer-Zahl ist. In vielen Programmiersprachen belegt ein Objekt einen zusammenhängenden Speicherbereich. Ein Zeiger ist dann ein-
10.3 Implementierung von Zeigern und Objekten
L
1
19 A
2
3
4
5
6
7
4
7 13 1
8
9
4
243
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
16 4 19
9 13
schlüssel vorg nachf Abbildung 10.6: Die verkettete Liste aus den Abbildungen 10.3(a) und 10.5, dargestellt mit einem einzigen eindimensionalen Feld A. Jedes Listenelement ist ein Objekt, das ein zusammenhängendes Teilfeld der Länge 3 im Feld belegt. Die drei Attribute schl u ¨ ssel , nachf und vorg entsprechen den Offsets 0, 1 und 2 innerhalb eines jeden Objekts. Ein Zeiger auf ein Objekt ist der Index des ersten Elementes des Objektes. Objekte, die Listenelemente enthalten, sind schwach schattiert. Pfeile zeigen die Listenreihenfolge.
fach die Adresse der ersten zum Objekt gehörenden Speicherzelle. Andere Speicherbereiche innerhalb des Objektes können wir durch Addieren von Offsets auf diesen Zeiger adressieren. Wir können dieselbe Strategie verwenden, um Objekte in Programmiersprachen zu implementieren, die nicht explizit Zeiger-Datentypen unterstützen. Abbildung 10.6 beispielsweise zeigt, wie wir in einem einzigen Feld A die verketteten Listen aus den Abbildungen 10.3(a) und 10.5 speichern können. Ein Objekt belegt ein zusammenhängendes Teilfeld A[j . . k]. Jedes Attribut des Objektes ist über einen Offset aus dem Bereich von 0 bis k − j adressierbar und der Zeiger auf das Objekt ist gleich dem Index j. In Abbildung 10.6 sind die Offsets zu den Komponenten schl¨u ssel , nachf und vorg durch 0, 1 bzw. 2 gegeben. Um den Wert von i.prev zu erhalten, addieren wir den Wert des gegebenen Zeigers i auf den Offset 2 und greifen dann auf A[i + 2] zu. Diese Darstellung ist insofern flexibel, als sie es ermöglicht, Objekte unterschiedlicher Länge in ein und demselben Feld zu speichern. Das Problem, solch eine heterogene Menge von Objekten zu handhaben, ist aber viel schwieriger als das Problem, eine homogene Menge, bei der alle Objekte aus gleichen Attributen bestehen, zu verwalten. Da die meisten Datenstrukturen, die wir betrachten werden, aus homogenen Elementen zusammengesetzt sind, reicht die Darstellung von Objekten durch mehrere Felder für unsere Zwecke vollends aus.
Allokieren und Freigeben von Objekten Um einen Schlüssel in eine dynamische Menge einzufügen, die durch eine doppelt verkettete Liste dargestellt ist, müssen wir einen Zeiger auf ein Objekt, das momentan nicht in der verketteten Liste benutzt wird, allokieren. Aus diesem Grunde ist es sinnvoll, den Speicher der momentan in der verketteten Liste nicht benutzten Objekte zu verwalten, sodass ein solches Objekt allokiert werden kann. Bei einigen Systemen ist ein Freispeichersammler (engl.: garbage collector ) dafür verantwortlich, die nichtbenutzten Objekte zu verwalten. Viele Anwendungen sind jedoch einfach genug, dass sie selbst für die Rückgabe unbenutzter Objekte an eine Speicherverwaltung verant-
244
10 Elementare Datenstrukturen
frei
4
L
7
1
nachf schlüssel vorg
2
3 4 5
3
4
5
6
7
8
6
1 2
8 2 1 5 16 9 7 (a)
frei
5
L
4
1
nachf schlüssel vorg
2
3
4
5
3 4 7
7 8 1 25 2
frei
8
L
4
1
nachf schlüssel vorg
2
3
4
5
6
3 4 5
7 2 1 1 25 16 2 7
7
8
5 6 9 4
(b) 6
7
8
1 2 9 4
6
(c) Abbildung 10.7: Die Arbeitsweise der Prozeduren Allocate-Object und Free-Object. (a) Die Liste aus Abbildung 10.5 (schwach schattiert) und eine Freiliste (stark schattiert). Die Pfeile kennzeichnen die Struktur der Freiliste. (b) Das Ergebnis des Aufrufes Allocate-Object() (der den Index 4 zurückgibt), das Setzen von schl u ¨ ssel [4] auf 25 und der Aufruf von List-Insert(L, 4). Der neue Kopf der Freiliste ist Objekt 8, das das Objekt nachf [4] in der Freiliste war. (c) Nach Ausführen von List-Delete(L, 5) rufen wir Free-Object(5) auf. Objekt 5 wird der neue Kopf der Freiliste, gefolgt von Objekt 8.
wortlich sein können. Wir erläutern nun das Problem des Allokierens und Freigebens (oder Deallokierens) homogener Objekte am Beispiel einer doppelt verketteten Liste in Mehrfelddarstellung. Nehmen Sie an, die Felder in der Mehrfelddarstellung hätten die Länge m und die dynamische Menge würde zu irgendeinem Zeitpunkt n ≤ m Elemente enthalten. Dann repräsentieren n Objekte die momentan in der dynamischen Menge enthaltenen Elemente, und die verbleibenden m − n Objekte sind frei. Die freien (unbenutzten) Objekte können für zukünftig einzufügende Elemente verwendet werden. Wir halten die freien Objekte in einer einfach verketteten Liste, die wir als Freiliste bezeichnen. Die Freiliste verwendet nur das Feld nachf , um den Nachfolger in der Liste zu speichern. Der Kopf der Freiliste wird in der globalen Variable frei gespeichert. Wenn die durch die Liste L dargestellte dynamische Menge nichtleer ist, kann die Freiliste wie in Abbildung 10.7 gezeigt mit der Liste L verflochten sein. Beachten Sie, dass jedes Objekt in der Darstellung entweder in L oder in der Freiliste enthalten ist, jedoch nicht in beiden zugleich. Die Freiliste arbeitet wie ein Stapel: Das als nächstes zu allokierende Objekt ist das zuletzt freigegebene. Wir können eine Listenimplementierung der Stapeloperationen Push und Pop verwenden, um die Prozeduren für das Allokieren beziehungsweise das Freigeben zu implementieren. Wir gehen davon aus, dass die globale Variable frei , die in der folgenden Prozedur benutzt wird, auf das erste Element der Freiliste zeigt.
10.3 Implementierung von Zeigern und Objekten frei 10 L2 9 L1
1
nachf 5
2
3
4
6
8
schlüssel k1 k2 k3 3 vorg 7 6
5
6
7
2
8
9
10
1
7
4
k5 k6 k7 1 3 9
k9
245
Abbildung 10.8: Zwei verkettete Listen, L1 (schwach schattiert) und L2 (stark schattiert) und eine Freiliste (geschwärzt), die miteinander verflochten sind.
Allocate-Object() 1 if frei = = nil 2 error “Speicherüberlauf” 3 else x = frei 4 frei = x.nachf 5 return x Free-Object(x) 1 x.nachf = frei 2 frei = x Die Freiliste enthält anfangs alle n nicht allokierten Objekte. Wenn die Freiliste aufgebraucht ist, dann meldet die Prozedur Allocate-Object einen SpeicherüberlaufFehler. Wir können sogar mehrere verkettete Listen mit einer einzigen Freiliste bedienen. Abbildung 10.8 zeigt zwei verkettete Listen und eine Freiliste, die über die Attribute schl¨u ssel , nachf und vorg miteinander verflochten sind. Beide Prozeduren haben eine Laufzeit O(1), was sie recht nützlich macht. Wir können sie so modifizieren, dass sie für jede homogene Menge von Objekten arbeiten, indem eine der Komponenten des Objektes die Rolle des Attributes nachf in der Freiliste übernimmt.
Übungen 10.3-1 Zeichnen Sie ein Bild der Folge 13, 4, 8, 19, 5, 11, wenn sie als doppelt verkettete Liste unter Verwendung der Mehrfelddarstellung realisiert ist. Wiederholen Sie die Aufgabe für den Fall, dass die Folge in einem einzelnen Feld abgespeichert ist. 10.3-2 Geben Sie die Prozeduren Allocate-Object und Free-Object für eine homogene Menge von Objekten an, wenn diese durch ein einzelnes Feld realisiert ist. 10.3-3 Warum müssen wir die vorg-Attribute der Objekte in der Implementierung der Prozeduren Allocate-Object und Free-Object nicht setzen bzw. zurücksetzen?
246
10 Elementare Datenstrukturen
10.3-4 Es ist häufig wünschenswert, alle Elemente einer doppelt verketteten Liste kompakt im Speicher zu halten, indem wir sie beispielsweise in den vorderen Indizes der Mehrfelddarstellung speichern. (Dies ist bei einer seitenorientierten virtuellen Speicherumgebung der Fall.) Erklären Sie, wie die Prozeduren Allocate-Object und Free-Object zu implementieren sind, damit die Darstellung kompakt ist. Setzen Sie voraus, dass es keine außerhalb der Liste liegenden Zeiger auf Elemente der verketteten Liste gibt. (Hinweis: Verwenden Sie die Implementierung eines Stapels als Feld.) 10.3-5 Sei L eine doppelt verkettete Liste der Länge n, die in den Feldern schl¨u ssel , vorg und nachf der Länge m gespeichert ist. Nehmen Sie an, dass diese Felder durch die Prozeduren Allocate-Object und Free-Object verwaltet werden und hierbei eine doppelt verkettete Freiliste F verwendet wird. Nehmen Sie darüber hinaus an, dass sich n Einträge in der Liste L und m − n in der Freiliste befinden. Schreiben Sie eine Prozedur Compactify-List(L, F ), die bei gegebener Liste L und gegebener Freiliste F die Einträge aus L an die Positionen 1, 2, . . . , n speichert und die Freiliste so anpasst, dass sie korrekt bleibt und die Feldpositionen n + 1, n + 2, . . . , m belegt. Die Laufzeit Ihrer Prozedur sollte in Θ(n) liegen und nur konstant viel zusätzlichen Speicher benötigen. Beweisen Sie, warum Ihre Prozedur korrekt arbeitet.
10.4
Darstellung von gerichteten Bäumen
Die im vorangegangenen Abschnitt angegebenen Methoden zur Darstellung von Listen lassen sich für jede homogene Datenstruktur erweitern. In diesem Abschnitt sehen wir uns speziell das Problem an, gerichtete Bäume durch verkettete Datenstrukturen darzustellen. Zuerst beschäftigen wir uns mit binären Bäumen. Anschließend stellen wir eine Methode für gerichtete Bäume vor, bei denen die Knoten eine beliebige Anzahl Kinder haben können. Wir stellen jeden Knoten des Baumes als ein Objekt dar. Wie bei verketteten Listen nehmen wir an, dass jeder Knoten ein Attribut schl¨u ssel enthält. Die uns weiter interessierenden verbleibenden Attribute sind Zeiger auf andere Knoten. Sie variieren in Abhängigkeit vom Typ des Baumes.
10.4 Darstellung von gerichteten Bäumen
247
T.wurzel
Abbildung 10.9: Die Darstellung eines binären Baumes T . Jeder Knoten x hat die Attribute x. vater (oben), x. links (unten links) und x. rechts (unten rechts). Das Attribut schl u ¨ ssel ist in dieser Skizze nicht eingezeichnet. T.wurzel
Abbildung 10.10: Die linkes-Kind-, rechter-Bruder-Darstellung eines Baumes T . Jeder Knoten x hat die Attribute x. vater (oben), x. linkes-kind (unten links) und x. rechter -bruder (unten rechts). Die schl u ¨ ssel -Attribute sind nicht eingezeichnet.
248
10 Elementare Datenstrukturen
Binäre Bäume Abbildung 10.9 illustriert, wie wir die Attribute vater , links und rechts verwenden, um Zeiger auf den Vater, das linke Kind und das rechte Kind jedes Knotens in einem binären Baum T zu speichern. Im Falle x.vater = nil ist x die Wurzel des Baumes. Wenn der Knoten x kein linkes Kind hat, dann gilt x.links = nil und, analog dazu, x.rechts = nil, um darzustellen, dass der Knoten x kein rechtes Kind hat. Auf die Wurzel des gesamten Baumes zeigt das Attribut T.wurzel . Wenn T.wurzel = nil gilt, dann ist der Baum leer.
Gewurzelte Bäume mit unbeschränktem Grad Wir können das Schema zur Darstellung eines binären Baumes auf jede Klasse von Bäumen erweitern, in denen die Anzahl der Kinder eines jeden Knotens höchstens eine Konstante k ist: Wir ersetzen die Attribute links und rechts durch kind 1 , kind 2 , . . ., kind k . Dieses Schema ist nicht einsetzbar, wenn die Anzahl der Kinder der Knoten unbeschränkt ist, da wir nun nicht wissen, wie viele solche Attribute (Felder in der Mehrfelddarstellung) wir im Voraus allokieren müssen. Außerdem würden wir viel Speicherplatz verschwenden, wenn die Anzahl der Kinder zwar durch eine große Konstante beschränkt ist, die meisten Knoten aber nur eine kleine Anzahl Kinder haben. Glücklicherweise gibt es eine effiziente Methode, um Bäume mit beliebig vielen Kindern darzustellen. Die linkes-Kind-, rechter-Bruder-Darstellung (engl.: left-child, right-sibling representation) ist in Abbildung 10.10 illustriert. Wie zuvor enthält jeder Knoten einen Zeiger vater auf den Vater, und T.wurzel zeigt auf die Wurzel des Baumes T . Anstatt jedoch Zeiger auf jedes seiner Kinder zu haben, verfügt nun jeder Knoten über lediglich zwei Zeiger: 1. x.linkes-kind zeigt auf das am weitesten links stehende Kind des Knotens x und 2. x.rechter -bruder zeigt auf den unmittelbaren Bruder rechts von x. Wenn ein Knoten x keine Kinder hat, dann gilt x.linkes-kind = nil. Wenn der Knoten x das am weitesten rechts liegende Kind seines Vaters ist, dann gilt x.rechter -bruder = nil.
Andere Baumdarstellungen In einigen Fällen stellen wir gerichtete Bäume auf andere Weise dar. In Kapitel 6 haben wir beispielsweise einen Heap, der auf einem vollständigen binären Baum basiert, durch ein einziges Feld sowie ein Index, der auf den letzten Knoten des Heaps zeigt, dargestellt. Die in Kapitel 21 vorkommenden Bäume werden nur in Richtung der Wurzel traversiert, und so kommen nur die Zeiger auf die Väter vor; es gibt keine Zeiger auf die Kinder. Viele andere Schemata sind möglich. Welches Schema das beste ist, hängt von der jeweiligen Anwendung ab.
Problemstellungen zu Kapitel 10
249
Übungen 10.4-1 Zeichnen Sie den binären Baum, der durch die Attribute index 1 2 3 4 5 6 7 8 9 10
schl¨u ssel 12 15 4 10 2 18 7 14 21 5
links 7 8 10 5 nil 1 nil 6 nil nil
rechts 3 nil nil 9 nil 4 nil 2 nil nil
dargestellt sind. Knoten 6 sei die Wurzel des binären Baumes. 10.4-2 Schreiben Sie eine rekursive Prozedur, die in Zeit O(n) die Schlüssel aller Knoten eines gegebenen binären Baumes mit n Knoten ausgibt. 10.4-3 Schreiben Sie eine nichtrekursive Prozedur, die in Zeit O(n) die Schlüssel aller Knoten eines gegebenen binären Baumes mit n Knoten ausgibt. Verwenden Sie einen Stapel als Hilfsdatenstruktur. 10.4-4 Schreiben Sie eine Prozedur, die in Zeit O(n) alle Schlüssel eines gegebenen beliebigen gewurzelten Baumes mit n Knoten ausgibt, wobei der Baum mit der linkes-Kind-, rechter-Bruder-Darstellung abgespeichert ist. 10.4-5∗ Schreiben Sie eine nichtrekursive Prozedur, die in Zeit O(n) die Schlüssel aller Knoten eines gegebenen binären Baumes mit n Knoten ausgibt. Verwenden Sie nicht mehr als konstanten Speicherplatz neben dem durch den Baum selbst belegten Speicherplatz. Modifizieren Sie im Verlaufe der Prozedur den Baum nicht, auch nicht temporär. 10.4-6∗ Die linkes-Kind-, rechter-Bruder-Darstellung eines beliebigen gerichteten Baumes verwendet in jedem Knoten drei Zeiger: linkes-kind , rechter -bruder und vater . Von jedem Knoten aus kann der Vater in konstanter Zeit erreicht und identifiziert werden. All seine Kinder können Zeit, die linear in der Zahl der Kinder ist, erreicht und identifiziert werden. Überlegen Sie, wie man mit zwei Zeigern und einem Booleschen Ausdruck pro Knoten auskommen kann, sodass der Vater eines Knotens und all seine Kinder jeweils in einer Zeit, die linear in der Anzahl der Kinder ist, erreicht und identifiziert werden können.
Problemstellungen 10-1 Vergleich zwischen Listen Welche asymptotischen Laufzeiten haben die in der Tabelle links aufgeführten
250
10 Elementare Datenstrukturen Operationen dynamischer Mengen im schlechtesten Fall für die vier in der Tabelle angegebenen Listentypen? unsortiert, einfach verkettet
sortiert, einfach verkettet
unsortiert, doppelt verkettet
sortiert, doppelt verkettet
Search(L, k) Insert(L, x) Delete(L, x) Successor(L, x) Predecessor(L, x) Minimum(L) Maximum(L) 10-2 Fusionierbare Heaps unter Verwendung von verketteten Listen Ein fusionierbarer Heap (engl.: mergeable heap) unterstützt die Operationen Make-Heap (die einen leeren fusionierbaren Heap anlegt), Insert, Minimum, Extract-Min und Union.1 Zeigen Sie, wie fusionierbare Heaps implementiert werden können, wenn sie durch verkettete Listen dargestellt werden. Betrachten Sie die drei unten angegebenen Fälle. Bemühen Sie sich, jede Operation so effizient wie möglich zu gestalten. Analysieren Sie die Laufzeit jeder Operation in Abhängigkeit von der Größe der dynamischen Menge(n), auf der (denen) gearbeitet wird. a. Die Listen sind sortiert. b. Die Listen sind unsortiert. c. Die Listen sind unsortiert und die zu vereinigenden dynamischen Mengen sind disjunkt. 10-3 Durchsuchen einer sortierten kompakten Liste In Übung 10.3-4 wird danach gefragt, wie wir eine Liste mit n Elementen kompakt in den ersten n Stellen eines Feldes halten können. Wir setzen voraus, dass die Schlüssel paarweise verschieden sind und dass die kompakte Liste auch sortiert ist. Es gilt also schl¨u ssel [i] < schl¨u ssel [nachf [i]] für alle i = 1, 2, . . . , n mit nachf [i] = nil. Wir wollen zudem voraussetzen, dass wir eine Variable L haben, in der der Index des ersten Elements der Liste gespeichert ist. Sie haben unter diesen Voraussetzungen zu zeigen, dass wir den folgenden randomisierten Algo√ rithmus anwenden können, um die Liste in einer erwarteten Laufzeit von O( n) durchsuchen zu können. 1 Da wir einen fusionierbaren Heap so definiert haben, dass er Minimum und Extract-Min unterstützt, können wir ihn auch als fusionierbarer Min-Heap bezeichnen. Alternativ wäre es ein fusionierbarer Max-Heap, wenn er Maximum und Extract-Max unterstützen würde.
Problemstellungen zu Kapitel 10
251
Compact-List-Search(L, n, k) 1 i=L 2 while i = nil and schl¨u ssel [i] < k 3 j = Random(1, n) 4 if schl¨u ssel [i] < schl¨u ssel [j] and schl¨u ssel[j] ≤ k 5 i=j 6 if schl¨u ssel [i] = = k 7 return i 8 i = nachf [i] 9 if i = = nil or schl¨u ssel [i] > k 10 return nil 11 else return i Wenn wir die Zeilen 3–7 der Prozedur ignorieren, haben wir einen gewöhnlichen Algorithmus zum Durchsuchen einer sortierten Liste, in der der Index i der Reihe nach auf jede Position in der Liste zeigt. Die Suche terminiert, wenn der Index i am Ende der Liste angelangt ist oder wenn schl¨u ssel [i] ≥ k gilt. Liegt der letztere Fall vor und gilt schl¨u ssel [i] = k, so haben wir offensichtlich einen Schlüssel mit dem Wert k gefunden. Wenn jedoch schl¨u ssel [i] > k gilt, dann werden wir niemals einen Schlüssel mit dem Wert k finden und wir können die Suche, wie dies in der Prozedur getan wird, abbrechen. Die Zeilen 3–7 versuchen zu einer zufällig gewählten Position j zu springen. Dies ist dann ein Gewinn für uns, wenn schl¨u ssel [j] größer als schl¨u ssel [i] und nicht größer als k ist. In solchen Fällen markiert j in der Liste eine Position, die i mithilfe einer gewöhnlichen Listensuche noch erreichen würde. Da die Liste kompakt ist, wissen wir, dass jede Wahl von j zwischen 1 und n ein Objekt aus der Liste und nicht einen Platz aus der Freiliste indiziert. Anstatt die Performanz von Compact-List-Search direkt zu analysieren, werden wir einen verwandten Algorithmus Compact-List-Search untersuchen, der zwei separate Schleifen ausführt. Dieser Algorithmus verwendet einen zusätzlichen Parameter t, über den eine obere Schranke für die Anzahl der Iterationen der ersten Schleife bestimmt wird. Compact-List-Search (L, n, k, t) 1 i=L 2 for q = 1 to t 3 j = Random(1, n) 4 if schl¨u ssel [i] < schl¨u ssel [j] and schl¨u ssel[j] ≤ k 5 i=j 6 if schl¨u ssel [i] = = k 7 return i 8 while i = nil and schl¨u ssel [i] < k 9 i = nachf [i] 10 if i = = nil or schl¨u ssel [i] > k 11 return nil 12 else return i
252
10 Elementare Datenstrukturen Um die Ausführung der beiden Algorithmen Compact-List-Search(L, n, k) und Compact-List-Search (L, n, k, t) zu vergleichen, nehmen wir an, dass die Folge der Integer-Zahlen, die beim Aufruf von Random(1, n) erzeugt wird, in beiden Algorithmen dieselbe wäre. a. Nehmen Sie an, dass Compact-List-Search(L, n, k) t Iterationen der while-Schleife (Zeilen 2–8) ausführen würde. Zeigen Sie, dass unter dieser Annahme Compact-List-Search (L, n, k, t) die gleiche Antwort liefert und die Gesamtanzahl der Iterationen sowohl der for- als auch der while-Schleife in Compact-List-Search wenigstens t ist. Beim Aufruf von Compact-List-Search (L, n, k, t) sei Xt die Zufallsvariable, die den Abstand innerhalb der verketteten Liste (d. h. über die Kette der nachf Zeiger) von der Position i zum gewünschten Schlüssel k beschreibt, nachdem t Iterationen der for-Schleife in den Zeilen 2–7 ausgeführt worden sind. b. Beweisen Sie, dass die erwartete Laufzeit von Compact-List-Search (L, n, k, t) in O(t + E [Xt ]) ist. n c. Zeigen Sie, dass E [Xt ] ≤ r=1 (1 − r/n)t gilt. (Hinweis: Wenden Sie Gleichung (C.25) an.) t t+1 /(t + 1) gilt. d. Zeigen Sie, dass n−1 r=0 r ≤ n e. Beweisen Sie, dass E [Xt ] ≤ n/(t + 1) gilt. f. Zeigen Sie, dass die erwartete Laufzeit von Compact-List-Search (L, n, k, t) in O(t + n/t) liegt. √ g. Folgern Sie, dass Compact-List-Search in erwarteter Zeit O( n) läuft. h. Weshalb setzen wir voraus, dass die Schlüssel in Compact-List-Search paarweise verschieden sind? Erläutern Sie, warum zufällige Sprünge das asymptotische Verhalten nicht notwendigerweise verbessern, wenn Schlüssel in der Liste den gleichen Wert haben dürfen.
Kapitelbemerkungen Aho, Hopcroft und Ullman [6] und Knuth [209] sind exzellente Nachschlagewerke zu elementaren Datenstrukturen. Viele andere Werke behandeln sowohl grundlegende Datenstrukturen als auch deren Implementierung in einer speziellen Programmiersprache. Beispiele für diese Art von Lehrbüchern sind Goodrich und Tamassia [147], Main [241], Shaffer [311] und Weiss [352, 353, 354]. Gonnet [145] stellt experimentelle Daten zur Performanz vieler Operationen auf Datenstrukturen bereit. Der Ursprung von Stapeln und Warteschlangen als Datenstrukturen in der Informatik ist nicht geklärt, da bereits vor der Einführung digitaler Rechner entsprechende Bemerkungen in der mathematischen Literatur und in sonstigen Veröffentlichungen existierten. Knuth [209] zitiert A. M. Turing aus dem Jahr 1947 im Zusammenhang mit der Entwicklung von Stapeln zur Verlinkung von Unterroutinen.
Kapitelbemerkungen zu Kapitel 10
253
Zeigerbasierte Datenstrukturen scheinen ebenso eine Volkserfindung zu sein. So wurden Zeiger nach Knuth offenbar schon in frühen Rechnern mit Trommelspeichern benutzt. Die A-1-Sprache, die von G. M. Hopper 1951 entwickelt wurde, stellt algebraische Formeln als binäre Bäume dar. Knuth schreibt der im Jahre 1956 von A. Newell, J. C. Shaw und H. A. Simon entwickelten Sprache IPL-II die Entdeckung der Bedeutung und die Förderung des Gebrauchs von Zeigern zu. Ihre IPL-III-Sprache, die 1957 entwickelt wurde, schließt explizite Stapeloperationen ein.
11
Hashtabellen
Viele Anwendungen benötigen eine dynamische Menge, die nur die Wörterbuchoperationen Insert, Search und Delete unterstützt. Ein Compiler für eine Programmiersprache beispielsweise verwaltet eine Symboltabelle, in der die in einem Programm verwendeten Bezeichner, die durch beliebige Zeichenfolgen gegeben sind, die Schlüssel der Elemente darstellen. Eine Hashtabelle ist eine effektive Datenstruktur für die Implementierung von Wörterbüchern. Obwohl das Suchen nach einem Element in einer Hashtabelle genauso viel Zeit benötigen kann wie das Suchen nach einem Element in einer verketteten Liste – nämlich Θ(n) im schlechtesten Fall – ist das Hashing ausgesprochen leistungsfähig. Unter vernünftigen Voraussetzungen ist die Laufzeit im Mittel für die Suche nach einem Element in einer Hashtabelle in O(1). Eine Hashtabelle verallgemeinert den einfachen Begriff eines normalen Feldes. Die direkte Adressierung in einem normalen Feld macht effektiv Gebrauch von der Möglichkeit, in Zeit O(1) auf eine beliebige Position des Feldes zugreifen zu können. In Abschnitt 11.1 wird die direkte Adressierung detaillierter behandelt. Wir können den Vorteil direkter Adressierung dann ausnutzen, wenn wir es uns leisten können, ein Feld anzulegen, das für jeden möglichen Schlüssel eine Position bereithält. Wenn die Anzahl der Schlüssel, die tatsächlich gespeichert werden, im Verhältnis zu der Gesamtanzahl der möglichen Schlüssel klein ist, dann stellen Hashtabellen eine Alternative zur direkten Adressierung in einem Feld dar, denn eine Hashtabelle verwendet typischerweise ein Feld, dessen Größe proportional zur Anzahl der tatsächlich gespeicherten Schlüssel ist. Anstatt den Schlüssel direkt als Feldindex zu benutzen, wird der Feldindex aus dem Schlüssel berechnet. Abschnitt 11.2 stellt die grundlegenden Ideen vor, wobei besonderes Augenmerk auf der „Verkettung“ als Möglichkeit der Auflösung von „Kollisionen“ liegt. Eine Kollision liegt vor, wenn mehr als ein Schlüssel auf denselben Index abgebildet wird. Abschnitt 11.3 beschreibt, wie wir Feldindizes mithilfe von Hashfunktionen aus Schlüsseln berechnen können. Wir stellen verschiedene Varianten vor und analysieren diese. Abschnitt 11.4 behandelt die „offene Adressierung“, die eine andere Möglichkeit für die Auflösung von Kollisionen darstellt. Unterm Strich ist Hashing eine ausgesprochen effektive und praktikable Methode: Die grundlegenden Wörterbuchoperationen benötigen im Mittel Zeit O(1). Abschnitt 11.5 erklärt, wie „perfektes Hashing“ im Falle einer statischen Menge von Schlüsseln Suchen in Zeit O(1) im schlechtesten Fall ermöglicht (statisch bedeutet in diesem Zusammenhang, dass die Menge der Schlüssel sich nicht mehr ändert, nachdem die Schlüssel der Menge einmal abgespeichert sind.)
256
11.1
11 Hashtabellen
Adresstabellen mit direktem Zugriff
Die direkte Adressierung ist eine einfache Methode, die gut funktioniert, wenn die Universalmenge U der Schlüssel einigermaßen klein ist. Angenommen, eine Anwendung benötigt eine dynamische Menge, in der jedes Element einen Schlüssel aus der Menge U = {0, 1, . . . , m − 1} hat, wobei m hinreichend klein ist. Wir setzen voraus, dass keine zwei Elemente den gleichen Schlüssel haben. Um die dynamische Menge darzustellen, verwenden wir ein Feld, das auch direkt adressierbare Tabelle genannt wird und das wir mit T [0 . . m − 1] bezeichnen und in der jede Position, d. h. jeder Slot, einem Schlüssel der Universalmenge U entspricht. Abbildung 11.1 illustriert diesen Ansatz; Slot k zeigt auf ein Element der Menge mit dem Schlüssel k. Wenn die Menge kein Element mit dem Schlüssel k enthält, dann gilt T [k] = nil. Die Implementierung der Wörterbuchoperationen ist trivial. Direct-Address-Search(T, k) 1 return T [k] Direct-Address-Insert(T, x) 1 T [x.schl¨u ssel ] = x Direct-Address-Delete(T, x) 1 T [x.schl¨u ssel ] = nil Jede dieser Operationen benötigt nur Zeit O(1). In einigen Anwendungen speichert die direkt adressierbare Tabelle die Elemente selbst der dynamischen Menge. Anstatt also in der direkt adressierbaren Tabelle einen Zeiger auf die Satellitendaten, die in einem Objekt außerhalb der direkt adressierbaren Tabelle gespeichert sind, zusammen mit dem Schlüssel des Elements abzulegen, können wir das Objekt selbst in dem Slot ablegen. Wir würden einen speziellen Schlüssel innerhalb eines Objektes verwenden, um anzugeben, dass der entsprechende Slot in der Tabelle nicht belegt ist. Außerdem ist es häufig nicht notwendig, den Schlüssel des Objektes zu speichern, denn, wenn wir den Index eines Objektes in der Tabelle kennen, kennen wir seinen Schlüssel. Falls die Schlüssel nicht gespeichert werden, müssen wir jedoch eine Möglichkeit haben, anzugeben, dass ein Slot nicht belegt ist.
Übungen 11.1-1 Nehmen Sie an, eine dynamische Menge S würde durch eine direkt adressierbare Tabelle T der Länge m dargestellt. Geben Sie eine Prozedur an, die das maximale Element von S bestimmt. Wie ist die Laufzeit Ihrer Prozedur im schlechtesten Fall?
11.1 Adresstabellen mit direktem Zugriff
257 T 0
9
U (Universalmenge) 0 6 7 4
1 2 K (derzeitige Schlüssel)5
1 2 3
Satellitendaten
2 3
4 5
3
schlüssel
5
6
8
7 8
8
9
Abbildung 11.1: Implementierung einer dynamischen Menge durch eine direkt adressierbare Tabelle T . Jeder Schlüssel der Universalmenge U = {0, 1, . . . , 9} entspricht einem Index der Tabelle. Die Menge K = {2, 3, 5, 8} der tatsächlichen Schlüssel bestimmt die Slots der Tabelle, die Zeiger auf Elemente enthalten. Die anderen Slots (stark schattiert) enthalten nil.
11.1-2 Ein Bitvektor ist ein Feld aus Bits (Nullen und Einsen). Ein Bitvektor der Länge m nimmt weniger Speicherplatz ein als ein Feld aus m Zeigern. Beschreiben Sie, wie ein Bitvektor benutzt werden kann, um eine dynamische Menge von paarweise verschiedenen Elementen ohne Satellitendaten darzustellen. Wörterbuchoperationen sollten in Zeit O(1) laufen. 11.1-3 Schlagen Sie eine Implementierung einer direkt adressierbaren Tabelle vor, in der die Schlüssel der gespeicherten Elemente nicht paarweise verschieden sein müssen und deren Elemente Satellitendaten haben können. Alle drei Wörterbuchoperationen (Insert, Delete und Search) sollten in Zeit O(1) laufen. (Denken Sie daran, dass Delete keinen Schlüssel übergeben bekommt, sondern einen Zeiger auf das Objekt, das gelöscht werden soll.) 11.1-4∗Wir wollen ein Wörterbuch implementieren, indem wir die direkte Adressierung auf einem riesigen Feld anwenden. Zu Beginn können die Feldeinträge „Müll“ enthalten; die Initialisierung des gesamten Feldes ist aber wegen seiner Größe nicht praktikabel. Beschreiben Sie eine Methode, um ein direkt adressierbares Wörterbuch in einem riesigen Feld zu implementieren. Jedes gespeicherte Objekt soll einen Speicherverbrauch O(1) haben; die Operationen Search, Insert und Delete sollen jeweils Zeit O(1) kosten und die Initialisierung der Datenstruktur soll ebenfalls Zeit O(1) kosten. (Hinweis: Verwenden Sie ein zusätzliches Feld, das irgendwie als Stapel arbeitet, dessen Größe gleich der Anzahl der tatsächlich im Wörterbuch gespeicherten Schlüssel ist und das bei der Entscheidung hilft, ob ein gegebener Eintrag in dem riesigen Feld gültig ist oder nicht.)
258
11 Hashtabellen
11.2
Hashtabellen
Der Nachteil der direkten Adressierung ist offensichtlich: Wenn die Universalmenge U groß ist, kann die Speicherung einer Tabelle T der Größe |U | unpraktikabel oder in Anbetracht des verfügbaren Speichers eines typischen Rechners gar unmöglich sein. Außerdem kann die Menge K der Schlüssel, die tatsächlich gespeichert werden, im Vergleich zu U so klein sein, dass der größte Teil des T zugewiesenen Speicherplatzes verschwendet wäre. Wenn die Menge K der Schlüssel, die in einem Wörterbuch gespeichert wird, viel kleiner ist als die Universalmenge U der möglichen Schlüssel, dann erfordert eine Hashtabelle wesentlich weniger Speicherplatz als eine direkt adressierbare Tabelle. Insbesondere können wir den Speicherverbrauch auf Θ(|K|) reduzieren, wobei der Vorteil erhalten bleibt, dass das Suchen nach einem Element in der Hashtabelle auch nur Zeit O(1) benötigt. Der einzige Haken ist der, dass diese Schranke für die Zeit im Mittel gilt, während sie bei direkter Adressierung für die Zeit im schlechtesten Fall gilt. Bei der direkten Adressierung wird ein Element mit dem Schlüssel k im Slot k gespeichert. Beim Hashing wird dieses Element am Platz h(k) gespeichert; wir verwenden also eine Hashfunktion h, um den Slot für den Schlüssel k zu berechnen. Hierbei bildet h die Universalmenge U der Schlüssel in die Menge der Slots einer Hashtabelle T [0 . . m − 1] ab: h : U → {0, 1, . . . , m − 1} , wobei die Größe m der Hashtabelle üblicherweise viel kleiner als |U | ist. Wir sagen, dass h(k) der Hashwert des Schlüssels k ist. Abbildung 11.2 veranschaulicht die zugrunde liegende Idee. Die Hashfunktion reduziert den Bereich der Feldindizes und somit die Größe des Feldes. Anstatt Größe |U | hat das Feld nun Größe m. T 0 U (Universalmenge) k1 K k (derzeitige 4 Schlüssel) k2
k5 k3
h(k1) h(k4) h(k2) = h(k5) h(k3) m–1
Abbildung 11.2: Anwendung einer Hashfunktion h, um Schlüssel auf Slots einer Hashtabelle abzubilden. Die Schlüssel k2 und k5 werden auf denselben Slot abgebildet: sie kollidieren.
Die Sache hat einen Haken: Zwei Schlüssel können auf den gleichen Slot abgebildet
11.2 Hashtabellen
259 T
U (Universalmenge)
k1
k4
k5
k2
k3 k8
k6
k1 K k k5 (derzeitige 4 k 7 Schlüssel) k2 k3 k8 k6
k7
Abbildung 11.3: Kollisionsauflösung durch Verkettung. Jeder Slot T [j] der Hashtabelle enthält eine verkettete Liste der in der Menge K enthaltenen Schlüssel, deren Hashwert j ist. Beispielsweise gilt h(k1 ) = h(k4 ) und h(k5 ) = h(k7 ) = h(k2 ). Die verkettete Liste kann entweder einfach oder doppelt verkettet sein; wir zeigen sie hier als doppelt verkettete Liste, da in einer solchen Objekte schneller gelöscht werden können.
werden. Wir nennen eine solche Situation eine Kollision. Zum Glück gibt es effektive Verfahren zur Behandlung der durch die Kollisionen verursachten Konflikte. Die ideale Lösung wäre natürlich, Kollisionen zu vermeiden. Wir könnten versuchen, dieses Ziel durch die Wahl einer geeigneten Hashfunktion h zu erreichen. Eine Möglichkeit besteht darin, h scheinbar „zufällig“ zu machen und so Kollisionen zu vermeiden oder zumindest ihre Anzahl zu minimieren. Bereits die Bezeichnung „Hashing“ (deutsch: zerhacken, durcheinander bringen), die die Vorstellung des zufälligen Mischens hervorruft, bringt die Idee dieses Ansatzes zum Ausdruck. (Selbstverständlich muss eine Hashfunktion h deterministisch in dem Sinne sein, dass für einen gegebenen Eingabewert k immer dieselbe Ausgabe h(k) erzeugt werden sollte.) Wegen |U | > m muss es jedoch mindestens zwei Schlüssel geben, die den gleichen Hashwert haben; Kollisionen ganz und gar zu vermeiden, ist also unmöglich. Obwohl eine gut entworfene, „zufällig“ erscheinende Hashfunktion die Anzahl der Kollisionen minimieren kann, benötigen wir also dennoch eine Methode zur Auflösung der Kollisionen, die auftreten. Der verbleibende Teil dieses Abschnitts stellt die einfachste Methode zur Auflösung von Kollisionen vor. Diese wird als Verkettung bezeichnet. Abschnitt 11.4 führt ein alternatives Verfahren ein, die so genannte offene Adressierung.
Kollisionsauflösung durch Verkettung Bei der Verkettung speichern wir alle Elemente, die auf den gleichen Slot abgebildet werden, in einer verketteten Liste, wie dies Abbildung 11.3 zeigt. Slot j enthält einen Zeiger auf den Kopf der Liste aller gespeicherten Elemente, die auf j abgebildet werden. Gibt es keine solchen Elemente, so enthält Slot j den Wert nil. Die Wörterbuchoperationen auf einer Hashtabelle T sind einfach zu implementieren,
260
11 Hashtabellen
wenn Kollisionen durch Verkettung aufgelöst werden. Chained-Hash-Insert(T, x) 1 füge x an den Kopf der Liste T [h(x.schl¨u ssel)] Chained-Hash-Search(T, k) 1 suche in der Liste T [h(k)] nach einem Element mit dem Schlüssel k Chained-Hash-Delete(T, x) 1 entferne x aus der Liste T [h(x.schl¨u ssel )] Die Laufzeit für das Einfügen ist im schlechtesten Fall O(1). Dass die Prozedur zum Einfügen schnell ist, liegt zum Teil daran, dass sie voraussetzt, dass das einzufügende Element noch nicht in der Tabelle vorhanden ist; wenn notwendig können wir diese Voraussetzung überprüfen, in dem wir in der Hashtabelle nach einem Element mit Schlüssel x.schl¨u ssel suchen (was natürlich mit zusätzlichen Kosten verbunden ist), bevor wir die eigentliche Einfügeoperation durchführen. Die Laufzeit für das Suchen ist im schlechtesten Fall proportional zur Länge der entsprechenden Liste; wir werden diese Operation weiter unten näher betrachten. Wir können ein Element in Zeit O(1) entfernen, wenn die Listen doppelt verkettet sind. (Beachten Sie, dass die Prozedur Chained-HashDelete das Element x und nicht dessen Schlüssel als Parameter bekommt, sodass wir nicht als erstes nach x suchen müssen. Wenn die Hashtabelle das Löschen von Elementen unterstützt, dann sollte ihre verketteten Listen doppelt verkettet sein, sodass wir ein Element schnell löschen können. Wenn die Listen einfach verkettet wären, müssten wir, um x zu löschen, zuerst x in der Liste T [h(x.schl¨u ssel )] finden, damit das Attribut nachf des Vorgängers von x richtig aktualisiert werden kann. Mit einfach verketteten Listen hätten Entfernen und Suchen die gleichen asymptotischen Laufzeiten.)
Analyse des Hashings mit Verkettung Wie gut ist die Performanz des Hashings mit Verkettung? Wie lange dauert es insbesondere, ein Element mit einem gegebenen Schlüssel zu suchen? Zu einer gegebenen Hashtabelle T mit m Slots, die n Elemente speichert, definieren wir den Belegungsfaktor α als n/m, d. h. als die mittlere Anzahl der Elemente, die in einer Kette der Hashtabelle gespeichert sind. Unsere Analyse erfolgt in Abhängigkeit von der Größe α, die kleiner, gleich oder größer als 1 sein kann. Das Verhalten des Hashings mit Verkettung im schlechtesten Fall ist fürchterlich: Alle n Schlüssel könnten auf denselben Slot abgebildet werden, wodurch eine Liste der Länge n erzeugt werden würde. Die Zeit für das Suchen im schlechtesten Fall ist daher Θ(n) zuzüglich der Zeit für die Berechnung der Hashfunktion – also nicht besser, als wenn wir eine verkettete Liste für alle Elemente verwendet hätten. Natürlich wenden wir Hashtabellen nicht wegen ihres Verhaltens im schlechtesten Fall an. (Perfektes Hashing, das in Abschnitt 11.5 vorgestellt wird, liefert, falls die Menge der Schlüssel statisch ist, sogar eine gute Performanz im schlechtesten Fall.)
11.2 Hashtabellen
261
Die mittlere Laufzeit des Hashings hängt davon ab, wie gut die Hashfunktion h die Menge der zu speichernden Schlüssel im Mittel auf die m Slots verteilt. Abschnitt 11.3 behandelt dieses Thema. An dieser Stelle wollen wir voraussetzen, dass jedes beliebige gegebene Element mit gleicher Wahrscheinlichkeit und unabhängig von den anderen Elementen auf einen der m Slots abgebildet wird. Wir bezeichnen diese Annahme als einfaches gleichmäßiges Hashing. Für j = 0, 1, . . . , m − 1 bezeichnen wir die Länge der Liste T [j] mit nj , sodass n = n0 + n1 + · · · + nm−1
(11.1)
gilt. Der Mittelwert von nj ist E [nj ] = α = n/m. Wir setzen voraus, dass O(1) Zeit ausreicht, um den Hashwert h(k) zu berechnen, sodass die Zeit, die für das Suchen eines Elementes mit dem Schlüssel k benötigt wird, linear von der Länge nh(k) der Liste T [h(k)] abhängt. Ohne die konstante Zeit zu berücksichtigen, die die Berechnung eines Hashwertes und der Zugriff auf Slot h(k) jeweils benötigen, lassen Sie uns die erwartete Anzahl der durch den Suchalgorithmus zu überprüfenden Elemente betrachten, d. h. die erwartete Anzahl der Elemente in der Liste T [h(k)], die der Algorithmus überprüft, um festzustellen, ob eines dieser Elemente den Schlüssel k hat. Wir werden zwei Fälle betrachten. Im ersten Fall ist die Suche erfolglos: Kein Element der Hashtabelle hat den Schlüssel k. Im zweiten Fall verläuft die Suche erfolgreich, d. h. es wird ein Element mit dem Schlüssel k gefunden. Theorem 11.1 In einer Hashtabelle, in der Kollisionen durch Verkettung aufgelöst werden, benötigt eine erfolglose Suche unter der Annahme des einfachen gleichmäßigen Hashings eine Laufzeit im Mittel von Θ(1 + α). Beweis: Unter der Annahme des einfachen gleichmäßigen Hashings wird ein Schlüssel k, der noch nicht in der Hashtabelle gespeichert ist, gleichwahrscheinlich auf einen der m Slots abgebildet. Die erwartet Zeit für die erfolglose Suche nach einem Schlüssel k ist gleich der erwarteten Zeit 4 für5 eine Suche bis an das Ende der Liste T [h(k)]. Die erwartete Länge der Liste ist E nh(k) = α. Also ist die erwartete Anzahl der Elemente, die bei einer erfolglosen Suche zu prüfen sind, gleich α, und die nötige Gesamtzeit (einschließlich der Zeit für die Berechnung von h(k)) ist Θ(1 + α). Bei einer erfolgreichen Suche ist die Situation etwas anders, da nicht alle Listen mit der gleichen Wahrscheinlichkeit durchsucht werden. Stattdessen ist die Wahrscheinlichkeit, dass eine Liste durchsucht wird, proportional zu der Anzahl ihrer Elemente. Trotzdem ist die erwartete Suchzeit auch in diesem Fall Θ(1 + α). Theorem 11.2 In einer Hashtabelle, in der Kollisionen durch Verkettung aufgelöst werden, benötigt eine erfolgreiche Suche unter der Annahme des einfachen gleichmäßigen Hashings eine Laufzeit im Mittel von Θ(1 + α).
262
11 Hashtabellen
Beweis: Wir nehmen an, dass jedes der n Elemente, die in der Tabelle gespeichert sind, mit gleicher Wahrscheinlichkeit das gesuchte Element ist. Die Anzahl der Elemente, die im Laufe einer erfolgreichen Suche nach einem Element x zu prüfen sind, ist um eins größer als die Anzahl der Elemente, die an Positionen vor x in der zu x gehörenden Liste stehen. Diese Elemente sind alle nach x eingefügt worden, da neue Elemente immer ganz vorn in die Liste eingeordnet werden. Um die erwartete Anzahl der zu prüfenden Elemente zu bestimmen, bilden wir den Mittelwert bezüglich der n in der Tabelle abgespeicherten Elemente von 1 plus der erwarteten Anzahl der Elemente, die in die zu x gehörenden Liste eingefügt worden ist, nachdem x in die Liste eingefügt wurde. Für i = 1, 2, . . . , n bezeichnen wir mit xi das i-te Element, das in die Tabelle eingefügt wurde, und mit ki den Schlüssel von xi , d. h. ki = xi .schl¨u ssel . Für die Schlüssel ki und kj definieren wir die Zufallsvariable Xij = I{h(ki ) = h(kj )}. Unter der Annahme des einfachen gleichmäßigen Hashings gilt P {h(ki ) = h(kj )} = 1/m und damit wegen Lemma 5.1 auch E[Xij ] = 1/m. Demnach ist der Erwartungswert für die Anzahl der bei einer erfolgreichen Suche zu prüfenden Elemente ⎡ ⎛ ⎞⎤ n n 1 ⎝1 + E⎣ Xij ⎠⎦ n i=1 j=i+1 ⎞ ⎛ n n 1 ⎝ E [Xij ]⎠ = 1+ n i=1 j=i+1 (wegen der Linearität des Erwartungswertes) ⎞ ⎛ n n 1 ⎝ 1⎠ = 1+ n i=1 m j=i+1 1 (n − i) n m i=1 2 n 3 n 1 1+ n− i n m i=1 i=1 1 n(n + 1) 1+ n2 − nm 2 (wegen Gleichung (A.1)) n−1 1+ 2m α α . 1+ − 2 2n n
= 1+
= =
= =
Damit ist die für eine erfolgreiche Suche erforderliche Gesamtzeit (einschließlich der Zeit für die Berechnung der Hashfunktion) Θ(2 + α/2 − α/2n) = Θ(1 + α).
Was bedeutet diese Aussage? Wenn die Anzahl der Slots der Hashtabelle mindestens
11.2 Hashtabellen
263
proportional zur Anzahl der Elemente in der Tabelle ist, dann haben wir n = O(m) und folglich auch α = n/m = O(m)/m = O(1). Die Suche benötigt also im Mittel konstante Zeit. Da die Zeit für das Einfügen im schlechtesten Fall und die Zeit für das Entfernen bei doppelt verketteten Listen im schlechtesten Fall jeweils O(1) ist, können alle Wörterbuchoperationen im Mittel in Zeit O(1) ausgeführt werden.
Übungen 11.2-1 Nehmen Sie an, wir würden eine Hashfunktion h benutzen, die n paarweise verschiedene Schlüssel in ein Feld der Länge m abbildet. Wie groß ist der Erwartungswert für die Anzahl der Kollisionen bei einfachem gleichmäßigem Hashing? Etwas präziser ausgedrückt, wie groß ist die erwartete Kardinalität der Menge {{k, l} : k = l und h(k) = h(l)}? 11.2-2 Illustrieren Sie, was passiert, wenn wir die Schlüssel 5, 28, 19, 15, 20, 33, 12, 17, 10 in eine Hashtabelle einfügen und die Kollisionen durch Verkettung aufgelöst werden. Die Tabelle habe 9 Slots, und die Hashfunktion sei h(k) = k mod 9. 11.2-3 Professor Marley behauptet, dass er eine erhebliche Performanzsteigerung erzielen kann, wenn er die Verkettungsmethode so modifiziert, dass jede Liste in sortierter Ordnung gehalten wird. Wie beeinflusst die durch den Professor vorgeschlagene Änderung die Laufzeit für erfolgreiches Suchen, erfolgloses Suchen, Einfügen und Löschen? 11.2-4 Machen Sie einen Vorschlag, wie der Speicherplatz für Elemente in der Hashtabelle allokiert und freigegeben werden kann, wenn alle unbenutzten Slots in einer Freiliste verkettet werden. Setzen Sie voraus, dass in einem Slot eine Markierung (engl.: flag) und entweder ein Element plus ein Zeiger oder zwei Zeiger abgespeichert werden können. Alle Operationen auf dem Wörterbuch und der Freiliste sollten in einer erwarteten Zeit von O(1) laufen. Muss die Freiliste doppelt verkettet sein oder genügt eine einfach verkettete Liste? 11.2-5 Nehmen Sie an, wir würden eine Menge von n Schlüsseln in einer Hashtabelle der Größe m speichern. Zeigen Sie, dass U eine Teilmenge der Größe n besitzt, die nur Schlüssel enthält, die auf den gleichen Slot abgebildet werden, wenn die Schlüssel aus einer Universalmenge U mit |U | > n m kommen. Damit hätten Sie gezeigt, dass die Suchzeit von Hashing mit Verkettung im schlechtesten Fall in Θ(n) liegt. 11.2-6 Nehmen Sie an, wir würden n Schlüssel in einer Hashtabelle der Größe m speichern, in der Kollisionen durch Verkettung aufgelöst werden, und die Länge einer jeden Kette kennen, insbesondere die Länge L der längsten Kette. Geben Sie eine Prozedur an, die in Zeit O(L (1 + 1/α)) zufällig einen Schlüssel aus den Schlüsseln, die in der Hashtabelle gespeichert sind, auswählt und ausgibt. Ein jeder Schlüssel soll mit gleicher Wahrscheinlichkeit von der Prozedur ausgewählt werden.
264
11.3
11 Hashtabellen
Hashfunktionen
In diesem Abschnitt diskutieren wir einige Fragen, die den Entwurf guter Hashfunktionen betreffen und stellen anschließend drei Schemata für deren Erzeugung vor. Zwei dieser Schemata – Hashing durch Division und Hashing durch Multiplikation – sind heuristischer Natur, während das dritte – universelles Hashing – eine Randomisierung benutzt, um eine nachweisbar gute Performanz zu erzielen.
Was macht eine gute Hashfunktion aus? Eine gute Hashfunktion erfüllt (näherungsweise) die Annahme des einfachen gleichmäßigen Hashings: Jeder Schlüssel wird mit der gleichen Wahrscheinlichkeit auf einen der m Slots abgebildet, unabhängig davon, wie irgendein anderer Schlüssel abgebildet wurde. Leider haben wir nur selten die Möglichkeit, diese Bedingung zu überprüfen, da wir nur selten die Wahrscheinlichkeitsverteilung kennen, nach der die Schlüssel verteilt sind. Zudem müssen die Schlüssel nicht mal unabhängig verteilt sein. Gelegentlich kennen wir die Verteilung. Wenn beispielsweise bekannt ist, dass die Schlüssel reellwertige Zufallszahlen k sind, die unabhängig und gleichmäßig im Intervall 0 ≤ k < 1 verteilt sind, dann erfüllt die Hashfunktion h(k) = k m die Bedingung des einfachen gleichmäßigen Hashings. In der Praxis können wir häufig heuristische Verfahren anwenden, um eine Hashfunktion mit guter Performanz zu erzeugen. Qualitative Informationen über die Verteilung der Schlüssel können in diesem Entwurfsprozess von Nutzen sein. Betrachten Sie beispielsweise die Symboltabelle eines Compilers. Die Schlüssel sind in dieser Tabelle die Zeichenketten, die die Bezeichner eines Programms repräsentieren. Eng verwandte Symbole, wie pt und pts, kommen häufig im gleichen Programm vor. Eine gute Hashfunktion sollte die Wahrscheinlichkeit minimieren, dass solche Varianten auf den gleichen Slot abgebildet werden. Ein guter Ansatz leitet den Hashwert in einer Weise ab, von der wir erwarten können, dass der Hashwert unabhängig von jeglichen Mustern ist, die möglicherweise in den Daten existieren. Die „Divisionsmethode“ (Abschnitt 11.3.1) berechnet zum Beispiel den Hashwert als den Rest, der bei der Division des Schlüssels durch eine bestimmte Primzahl bleibt. Diese Methode liefert oft gute Ergebnisse, vorausgesetzt, wir wählen eine Primzahl, die nicht in Beziehung zu Mustern in der Verteilung der Schlüssel steht. Anzumerken ist auch, dass einige Anwendungen von Hashfunktionen stärkere Eigenschaften erfordern können, als die, die durch einfaches gleichmäßiges Hashing bereitgestellt werden. Zum Beispiel könnten wir wollen, dass Schlüssel, die in einem gewissen Sinne „eng“ zusammenliegen, Hashwerte haben, die weit auseinander liegen. (Diese Eigenschaft ist vor allem dann wünschenswert, wenn wir lineares Sondieren verwenden, das in Abschnitt 11.4 definiert wird.) Universelles Hashing (Abschnitt 11.3.3) liefert häufig die gewünschten Eigenschaften.
11.3 Hashfunktionen
265
Die Schlüssel als natürliche Zahlen interpretieren Die meisten Hashfunktionen setzen voraus, dass die Universalmenge der Schlüssel die Menge N = {0, 1, 2, . . .} der natürlichen Zahlen ist. Wenn die Schlüssel keine natürlichen Zahlen sind, müssen wir einen Weg finden, sie als natürliche Zahlen zu interpretieren. So können wir zum Beispiel eine Zeichenkette als ganze Zahl über einer geeigneten Basis interpretieren. Den Bezeichner pt beispielsweise können wir als das Paar (112, 116) ganzer Zahlen interpretieren, denn im ASCII-Zeichensatz ist p = 112 und t = 116; pt kann so als Zahl zur Basis 128 interpretiert werden und steht für die ganze Zahl (112 · 128) + 116 = 14452. Im Rahmen einer gegebenen Anwendung können wir in der Regel eine Methode ableiten, um jeden Schlüssel als (möglicherweise große) natürliche Zahl zu interpretieren. Im Folgenden nehmen wir an, dass die Schlüssel natürliche Zahlen sind.
11.3.1
Die Divisionsmethode
Bei der Divisionsmethode zum Erzeugen von Hashfunktionen bilden wir einen Schlüssel k auf einen von m Schlüsseln ab, indem wir k durch m teilen und den Rest dieser Division als Hashwert nehmen. Die Hashfunktion lautet also h(k) = k mod m . Wenn die Hashtabelle zum Beispiel die Größe m = 12 hat und der Schlüssel k = 100 ist, dann ist der Hashwert h(k) = 4. Da nur eine einzige Division erforderlich ist, ist Hashing durch Division recht schnell. Wenn wir die Divisionsmethode verwenden, vermeiden wir üblicherweise bestimmte Werte von m. Beispielsweise sollte m keine Potenz von 2 sein, denn für m = 2p ist h(k) einfach nur gleich den p niederwertigsten Bits von k. Wir sollten besser eine Hashfunktion konstruieren, die von allen Bits des Schlüssels abhängen, es sei denn, wir wissen, dass jede binäre Folge der Länge p gleichwahrscheinlich für die p niederwertigsten Bits ist. In Übung 11.3-3 sollen Sie zeigen, dass m = 2p − 1 auch eine schlechte Wahl sein kann, wenn k eine Zeichenkette ist, die als Zahl zur Basis 2p interpretiert wird, denn das Permutieren der Zeichen des Schlüssels k ändert seinen Hashwert nicht. Eine Primzahl, die nicht zu nahe an einer Potenz von 2 liegt, ist oftmals eine gute Wahl für m. Nehmen Sie zum Beispiel an, wir wollten eine Hashtabelle anlegen, um ungefähr n = 2000 Zeichenketten abzulegen, wobei ein Zeichen aus 8 Bit besteht, und in der Kollisionen durch Verkettung aufgelöst werden. Wir haben nichts dagegen, wenn bei einer erfolglosen Suche im Mittel drei Elemente zu überprüfen sind. Daher legen wir eine Hashtabelle der Größe m = 701 an. Wir können die Zahl 701 wählen, weil sie eine Primzahl in der Nähe von 2000/3 ist, jedoch nicht in der Nähe irgendeiner Potenz von 2. Fassen wir jeden Schlüssel k als ganze Zahl auf, so ist unsere Hashfunktion h(k) = k mod 701 .
266
11 Hashtabellen w Bits k s = A · 2w
× r1
r0 extrahiere p Bits h(k)
Abbildung 11.4: Die Multiplikationsmethode für Hashing. Die w-Bit-Darstellung des Schlüssels k wird mit dem w-Bit-Wert s = A · 2w multipliziert. Die p höherwertigsten Bits der niederwertigen w-Bit umfassenden Hälfte des Produktes bilden den gewünschten Hashwert h(k).
11.3.2
Die Multiplikationsmethode
Die Multiplikationsmethode zur Erzeugung von Hashfunktionen arbeitet in zwei Schritten. Zunächst multiplizieren wir den Schlüssel k mit einer Konstanten A aus aus dem Bereich 0 < A < 1 und extrahieren den gebrochenen Teil von k A. Dann multiplizieren wir diesen Wert mit m und nehmen den abgerundeten Wert des Ergebnisses. Die Hashfunktion lautet also h(k) = m (k A mod 1) , wobei „k A mod 1“ der gebrochene Teil von k A ist, also k A − k A. Ein Vorteil der Multiplikationsmethode besteht darin, dass der Wert von m nicht kritisch ist. Wir wählen ihn normalerweise als Potenz von 2 (m = 2p für eine ganze Zahl p), da wir die Funktion dann auf den meisten Rechnern in der folgenden Weise leicht implementieren können. Nehmen Sie an, die Wortlänge der Maschine würde w Bit betragen und k würde in ein einziges Wort passen. Wir schränken die Wahl von A ein, indem wir fordern, dass A ein Bruch der Form s/2w ist, wobei s eine ganze Zahl aus dem Bereich 0 < s < 2w ist. Gemäß Abbildung 11.4 multiplizieren wir k zunächst mit der w-Bit-Integerzahl s = A·2w . Das Ergebnis ist ein w-Bit-Wert der Form r1 2w +r0 , wobei r1 das höherwertige Wort des Produktes ist und r0 das niederwertige. Der gewünschte p-Bit-Hashwert besteht aus den p höherwertigsten Bits von r0 . Wenngleich diese Methode im Prinzip mit jedem Wert der Konstante A funktioniert, arbeitet sie doch mit manchen Werten besser als mit anderen. Die optimale Wahl hängt von den Charakteristika der abzubildenden Daten ab. Knuth [211] schlägt √ A ≈ ( 5 − 1)/2 = 0,6180339887 . . . (11.2) als Wert vor, der zu vernünftigen Ergebnissen führt. Nehmen wir beispielsweise an, wir haben k = 123456, p = 14, m = 214 = 16384 und w = 32. Nach dem Vorschlag √von Knuth wählen wir A als denjenigen Bruch der Form s/232 , der am nächsten an ( 5 − 1)/2 liegt, also A = 2654435769/232. Dann ist k · s = 327706022297664 = (76300 · 232 ) + 17612864 und somit r1 = 76300 und r0 = 17612864. Die 14 höherwertigsten Bits von r0 liefern den Wert h(k) = 67.
11.3 Hashfunktionen
267
∗ 11.3.3 Universelles Hashing Wenn ein arglistiger Gegner die Schlüssel auswählt, die durch eine feste Hashfunktion abgebildet werden sollen, dann kann der Gegner n Schlüssel auswählen, die alle auf denselben Slot abgebildet werden. Dies führt auf eine mittlere Abfragezeit von Θ(n). Jede feste Hashfunktion ist anfällig in Bezug auf derartige schlimmstmögliche Fälle. Das einzig wirksame Mittel gegen diese Situation besteht darin, die Hashfunktion zufällig auszuwählen, und zwar unabhängig von den Schlüsseln, die tatsächlich gespeichert werden sollen. Dieser Ansatz, universelles Hashing genannt, kann eine nachweisbar gute Performanz bringen, egal welche Schlüssel von dem Angreifer gewählt werden. Bei universellem Hashing wählen wir zu Beginn der Ausführung die Hashfunktion zufällig aus einer sorgfältig entworfenen Menge von Funktionen. Wie im Falle von Quicksort garantiert die Randomisierung, dass keine spezielle Eingabe jedes Mal das schlechteste Verhalten hervorrufen wird. Da wir die Hashfunktion zufällig auswählen, kann sich der Algorithmus bei jeder Ausführung unterschiedlich verhalten, selbst bei der gleichen Eingabe. Dies garantiert für jede Eingabe ein gutes Verhalten im Mittel. Kehren wir noch einmal zu dem Beispiel der Symboltabelle eines Compilers zurück, so stellen wir fest, dass die Wahl des Programmierers bezüglich der Bezeichner nun nicht zu durchgängig schlechter Performanz führen kann. Schlechte Performanz tritt nur dann auf, wenn der Compiler eine zufällige Hashfunktion wählt, die dazu führt, dass die Menge der Bezeichner schlecht verteilt wird; die Wahrscheinlichkeit für das Eintreten dieser Situation ist jedoch gering und für jede gleichgroße Menge von Bezeichnern die gleiche. Sei H eine endliche Menge von Hashfunktionen, die eine gegebene Universalmenge U von Schlüsseln in die Menge {0, 1, . . . , m − 1} abbilden. Eine solche Menge wird als universell bezeichnet, wenn für jedes Paar voneinander verschiedener Schlüssel k, l ∈ U die Anzahl der Hashfunktionen, für die h(k) = h(l) gilt, höchstens gleich |H| /m ist. Mit anderen Worten, mit einer zufällig aus H ausgewählten Hashfunktion ist die Wahrscheinlichkeit für eine Kollision zwischen den Schlüsseln k und l nicht größer als die Wahrscheinlichkeit 1/m einer Kollision, wenn h(k) und h(l) zufällig und unabhängig von einander aus der Menge {0, 1, . . . , m − 1} gewählt werden. Das folgende Theorem zeigt, dass eine universelle Klasse von Hashfunktionen zu einem guten mittleren Verhalten führt. Es sei daran erinnert, dass ni die Länge der Liste T [i] bezeichnet. Theorem 11.3 Angenommen, eine Hashfunktion h wird zufällig aus einer universellen Menge von Hashfunktionen ausgewählt und verwendet, um n Schlüssel in eine Tabelle T der Größe m abzubilden, wobei Kollisionen durch Verkettung aufgelöst werden.4 Wenn5 Schlüssel k nicht in der Tabelle enthalten ist, dann ist die erwartete Länge E nh(k) der Liste, auf die k abgebildet wird, höchstens gleich dem Belegungsfaktor4 α = n/m. 5 Ist Schlüssel k in der Tabelle enthalten, dann ist die erwartete Länge E nh(k) der Liste höchstens 1 + α. Beweis: Wir nehmen zur Kenntnis, dass die Erwartungswerte hier über die möglichen Hashfunktionen gebildet werden und von keinerlei Voraussetzungen bezüglich der
268
11 Hashtabellen
Verteilung der Schlüssel abhängen. Für jedes Paar k und l voneinander verschiedener Schlüssel definieren wir die Indikatorfunktion Xkl = I {h(k) = h(l)}. Da nach Definition einer universellen Menge von Hashfunktionen ein bestimmtes Paar von Schlüsseln mit einer Wahrscheinlichkeit von höchstens 1/m kollidiert, gilt Pr {h(k) = h(l)} ≤ 1/m. Wegen Lemma 5.1 folgt daraus E [Xkl ] ≤ 1/m. Als nächstes definieren wir zu jedem Schlüssel k die Zufallsvariable Yk als die Anzahl der von k verschiedenen Schlüssel, die auf den gleichen Slot wie k abgebildet werden, sodass Yk = Xkl l∈T l =k
gilt. Hiermit erhalten wir ⎡
⎤
⎢ ⎥ Xkl ⎥ E [Yk ] = E ⎢ ⎣ ⎦ =
l∈T l =k
E [Xkl ]
(wegen der Linearität des Erwartungswertes)
l∈T l =k
≤
1 . m l∈T l =k
Für den Rest des Beweises müssen wir unterscheiden, ob Schlüssel k in der Tabelle T enthalten ist oder nicht. • Im4 Falle5 k ∈ T gilt nh(k) = Yk und |{l : l ∈ T und l = k}| = n. Somit gilt E nh(k) = E [Yk ] ≤ n/m = α. • Im Falle k ∈ T gilt nh(k) = Yk + 1 und |{l : l ∈ T und l = k}| = n − 1, da Schlüssel k 4in der5 Liste T [h(k)] enthalten ist und nicht von Yk erfasst wird. Somit gilt E nh(k) = E [Yk ] + 1 ≤ (n − 1)/m + 1 = 1 + α − 1/m < 1 + α. Das folgende Korollar sagt aus, dass universelles Hashing den gewünschten Erfolg bringt: Es ist nun für einen arglistigen Gegner unmöglich, eine Folge von Operationen auszuwählen, die sicher zur Laufzeit im schlechtesten Fall führt. Durch intelligente Randomisierung der Wahl der Hashfunktion zur Laufzeit stellen wir sicher, dass wir jede Folge von Operationen mit einer guten Laufzeit im Mittel ausführen können.
11.3 Hashfunktionen
269
Korollar 11.4 Bei Verwendung universellen Hashings und Kollisionsauflösung durch Verkettung in einer anfangs leeren Tabelle mit m Slots ist die erwartete Laufzeit für die Ausführung jeder Folge von n Insert-, Search- und Delete-Operationen, in der O(m) InsertOperationen enthalten sind, in Θ(n). Beweis: Da die Anzahl der Einfügeoperationen O(m) ist, gilt n = O(m) und somit α = O(1). Die Insert- und Delete-Operationen benötigen konstante Zeit, und nach Theorem 11.3 ist die erwartete Zeit für jede Suchoperation O(1). Wegen der Linearität des Erwartungswertes ist die erwartete Zeit für die gesamte Folge der Operationen O(n). Da jede der Operationen Zeit Ω(1) benötigt, folgt die Schranke Θ(n).
Entwurf einer universellen Klasse von Hashfunktionen. Es ist ziemlich einfach, eine universelle Klasse von Hashfunktionen zu entwerfen, was wir mit ein wenig Zahlentheorie beweisen können. Sollten Sie mit Zahlentheorie nicht vertraut sein, so sollten Sie sich Kapitel 31 anschauen. Wir wählen zunächst eine Primzahl p, die hinreichend groß ist, sodass jeder mögliche Schlüssel k einen der Werte von 0 bis p − 1 annimmt. Wir bezeichnen mit Zp die Menge {0, 1, . . . , p − 1} und mit Z∗p die Menge {1, 2, . . . , p − 1}. Da p eine Primzahl ist, können wir Gleichungen modulo p mit den in Kapitel 31 vorgestellten Methoden lösen. Aus unserer Voraussetzung, dass die Größe der Universalmenge der Schlüssel größer als die Anzahl der Slots in der Hashtabelle ist, folgt p > m. Wir definieren nun die Hashfunktion hab für beliebige a ∈ Z∗p und b ∈ Zp durch eine lineare Transformation, gefolgt von einer Reduktion modulo p und anschließender Division modulo m: hab (k) = ((a k + b) mod p) mod m .
(11.3)
Mit p = 17 und m = 6 beispielsweise erhalten wir h3,4 (8) = 5. Die Familie aller derartigen Hashfunktionen ist (11.4) Hpm = hab : a ∈ Z∗p und b ∈ Zp . Jede Hashfunktion hab bildet Zp in Zm ab. Diese Klasse von Hashfunktionen hat die angenehme Eigenschaft, dass die Größe m des Ausgabebereichs beliebig gewählt werden kann – und nicht notwendigerweise eine Primzahl sein muss – was wir in Abschnitt 11.5 ausnutzen werden. Da wir p − 1 Möglichkeiten für die Wahl von a und p Möglichkeiten für die Wahl von b haben, enthält die Menge Hpm p (p − 1) Hashfunktionen. Theorem 11.5 Die Klasse Hpm der durch die Gleichungen (11.3) und (11.4) definierten Hashfunktionen ist universell.
270
11 Hashtabellen
Beweis: Betrachten wir zwei voneinander verschiedene Schlüssel k und l aus Zp . Für eine gegebene Hashfunktion hab sei r = (a k + b) mod p , s = (a l + b) mod p . Wir halten zunächst fest, dass r ungleich s ist, denn es gilt r − s ≡ a (k − l) (mod p) . Daraus folgt r = s, da p eine Primzahl ist und sowohl a als auch (k − l) nicht ohne Rest durch p teilbar sind. Nach Theorem 31.6 ist auch ihr Produkt nicht ohne Rest durch p teilbar. Bei Verwendung einer beliebigen Hashfunktion hab aus Hpm werden daher verschiedene Eingaben k und l auf verschiedene Werte r und s modulo p abgebildet; es gibt also keine Kollisionen auf der „mod p-Ebene“. Außerdem führt jede der p (p − 1) Wahlmöglichkeiten für das Paar (a, b) mit a = 0 auf ein anderes Paar (r, s) mit r = s. Um dies zu sehen, lösen wir die obigen Gleichungen für gegebene r und s nach a und b auf: a = (r − s)((k − l)−1 mod p) mod p , b = (r − a k) mod p , wobei ((k−l)−1 mod p) die eindeutige multiplikative Inverse modulo p von k−l bezeichnet. Da es nur p (p − 1) mögliche Paare (r, s) mit r = s gibt, gibt es eine eineindeutige Zuordnung zwischen den Paaren (a, b) mit a = 0 und den Paaren (r, s) mit r = s. Wenn wir (a, b) zufällig und gleichmäßig verteilt aus Z∗p × Zp auswählen, ist das resultierende Paar (r, s) mit gleicher Wahrscheinlichkeit eines der aus unterschiedlichen Werten modulo p bestehenden Paare. Daraus folgt, dass die Wahrscheinlichkeit für eine Kollision voneinander verschiedener Schlüssel k und l gleich der Wahrscheinlichkeit ist, dass r ≡ s (mod m) gilt, wenn r und s zufällig als verschiedene Werte modulo p gewählt werden. Zu einem gegebenen Wert von r ist von den verbleibenden p − 1 möglichen Werten für s die Anzahl derjenigen s, für die s = r und s ≡ r (mod m) gilt, höchstens p/m − 1 ≤ ((p + m − 1)/m) − 1 = (p − 1)/m .
(nach Ungleichung (3.6))
Die Wahrscheinlichkeit, dass s mit r (modulo m) kollidiert, beträgt also höchstens ((p − 1)/m)/(p − 1) = 1/m. Es gilt daher für jedes Paar voneinander verschiedener Werte k, l ∈ Zp Pr {hab (k) = hab (l)} ≤ 1/m , sodass Hpm in der Tat universell ist.
11.3 Hashfunktionen
271
Übungen 11.3-1 Nehmen Sie an, wir wollten eine verkettete Liste der Länge n durchsuchen, in der jedes Element einen Schlüssel k zusammen mit einem Hashwert h(k) enthält. Jeder Schlüssel ist eine lange Zeichenkette. Wie könnten wir Gebrauch von den Hashwerten machen, wenn wir die Liste nach einem Element mit einem gegebenen Schlüssel durchsuchen? 11.3-2 Nehmen Sie an, wir würden eine aus r Zeichen bestehende Zeichenkette auf m Slots abbilden, indem wir die Zeichenkette als Zahl zur Basis 128 betrachten und die Divisionsmethode benutzen würden. Wir können die Zahl m leicht als 32-Bit-Wort darstellen, aber die Zeichenkette mit r Zeichen, aufgefasst als Zahl zur Basis 128, belegt viele Speicherwörter. Wie können wir die Divisionsmethode anwenden, um den Hashwert der Zeichenkette zu berechnen, sodass wir mit einer konstanten Anzahl von Speicherwörtern neben denen, die wir für die Zeichenketten selbst verwenden, auskommen? 11.3-3 Betrachten Sie eine Version der Divisionsmethode, bei der die Hashfunktion durch h(k) = k mod m mit m = 2p − 1 gegeben ist. Dabei ist k eine Zeichenkette, die als ganze Zahl zur Basis 2p interpretiert wird. Zeigen Sie, dass zwei Zeichenketten x und y auf den gleichen Hashwert abgebildet werden, wenn wir x aus y durch Permutation der Zeichen erhalten können. Geben Sie ein Beispiel einer Anwendung an, bei dem diese Eigenschaft für die Hashfunktion unerwünscht ist. 11.3-4 Betrachten Sie eine Hashtabelle der Größe m = √ 1000 und eine zugehörige Hashfunktion h(k) = m (k A mod 1) mit A = ( 5 − 1)/2. Berechnen Sie die Slots, auf die die Schlüssel 61, 62, 63, 64 und 65 abgebildet werden. 11.3-5∗ Definieren Sie eine Familie H von Hashfunktionen, die eine endliche Menge U in eine endliche Menge B abbildet, als -universell, wenn für alle Paare voneinander verschiedener Elemente k und l aus U Pr {h(k) = h(l)} ≤ gilt, wobei sich die Wahrscheinlichkeit auf die zufällige Auswahl der Hashfunktion aus der Familie H bezieht. Zeigen Sie, dass für eine -universelle Familie von Hashfunktionen ≥
1 1 − |B| |U |
gelten muss. 11.3-6∗ Sei U die Menge der n-Tupel mit Werten aus Zp und sei B = Zp , wobei p eine Primzahl ist. Definieren Sie die Hashfunktion hb : U → B mit b ∈ Zp für ein n-Tupel a0 , a1 , . . . , an−1 aus U durch ⎞ ⎛ n−1 hb (a0 , a1 , . . . , an−1 ) = ⎝ aj bj ⎠ mod p j=0
272
11 Hashtabellen und betrachten Sie die Familie H = {hb : b ∈ Zp }. Zeigen Sie, dass H gemäß der Definition der -Universalität aus Übung 11.3-5 ((n − 1)/p)-universell ist. (Hinweis: Siehe Übung 31.4-4.)
11.4
Offene Adressierung
Offene Adressierung bedeutet, dass alle Elemente in der Hashtabelle direkt gespeichert werden. Jeder Tabelleneintrag enthält also entweder ein Element der dynamischen Menge oder nil. Bei der Suche nach einem Element überprüfen wir systematisch die Tabellenslots, bis wir entweder das Element gefunden haben oder wir uns davon überzeugt haben, dass das Element nicht in der Tabelle gespeichert ist. Im Unterschied zu Verkettung ist keine Liste und kein Element außerhalb der Tabelle gespeichert. Daher kann sich bei offener Adressierung die Tabelle „füllen“, bis schließlich keine weiteren Einfügeoperationen mehr ausgeführt werden können; eine Konsequenz davon ist, dass der Belegungsfaktor α nie größer als 1 sein kann. Natürlich könnten wir zur Verkettung die verketteten Listen in den ansonsten ungenutzten Slots der Tabelle speichern (siehe Übung 11.2-4), der Vorteil der offenen Adressierung ist jedoch, dass sie Zeiger überhaupt vermeidet. Anstatt Zeigern zu folgen, berechnen wir die Folge der Slots, die zu überprüfen sind. Der zusätzliche Speicher, der dadurch gewonnen wird, dass keine Zeiger gespeichert werden, ermöglicht für die Hashtabelle eine größere Anzahl von Slots bei gleicher Speichergröße, potentiell weniger Kollisionen und schnelleres Wiederauffinden der Daten. Um das Einfügen mithilfe offener Adressierung durchzuführen, müssen wir die Hashtabelle sukzessive überprüfen oder sondieren, bis wir einen leeren Slot finden, an dem wir den Schlüssel speichern können. Die Slots werden nicht in der festen Reihenfolge 0, 1, . . . , m − 1 sondiert (was zu der Suchzeit Θ(n) führen würde). Vielmehr hängt die Reihenfolge, in der wir die Slots besuchen, von dem einzufügenden Schlüssel ab. Um die Slots zu bestimmen, die sondiert werden, erweitern wir die Hashfunktion um einen zusätzlichen Eingabeparameter, über den eine Sondierungszahl (mit 0 beginnend) übergeben wird. Die Hashfunktion wird damit zu h : U × {0, 1, . . . , m − 1} → {0, 1, . . . , m − 1} . Bei der offenen Adressierung fordern wir, dass die Sondierungssequenz h(k, 0), h(k, 1), . . . , h(k, m − 1) für jeden Schlüssel k eine Permutation von 0, 1, . . . , m − 1 ist, sodass letztendlich jede Position der Hashtabelle als Slot für einen neuen Schlüssel berücksichtigt wird, wenn die Tabelle sich füllt. Im folgenden Pseudocode setzen wir voraus, dass die Elemente in der Hashtabelle T Schlüssel ohne Satelliteninformationen sind. Der Schlüssel k ist identisch mit dem Element, das den Schlüssel k enthält. Jeder Slot enthält entweder einen Schlüssel oder nil (falls der Slot leer ist). Die Prozedur Hash-Insert hat als Eingangsparameter eine Hashtabelle T und einen Schlüssel k. Sie gibt die Nummer des Slots zurück, in das sie den Schlüssel k speichert, oder setzt ein Fehlerflag, um anzuzeigen, dass die Hashtabelle bereits voll ist.
11.4 Offene Adressierung
273
Hash-Insert(T, k) 1 i=0 2 repeat 3 j = h(k, i) 4 if T [j] = = nil 5 T [j] = k 6 return j 7 else i = i + 1 8 until i = = m 9 error “Überlauf der Hashtabelle” Der Algorithmus für die Suche nach dem Schlüssel k sondiert die gleiche Folge von Slots, die der Einfüge-Algorithmus prüft, wenn Schlüssel k eingefügt werden soll. Daher kann die Suche (erfolglos) abbrechen, wenn ein leerer Slot gefunden wird, denn k wäre an dieser Stelle, und nicht weiter hinten, in der Sondierungssequenz eingefügt worden. (Dieses Argument setzt voraus, dass keine Schlüssel aus der Tabelle entfernt werden.) Die Prozedur Hash-Search hat eine Hashtabelle T und einen Schlüssel k als Eingabeparameter und gibt j zurück, falls sie herausfindet, dass Slot j den Schlüssel k enthält, und nil, wenn der Schlüssel k sich nicht in der Tabelle T befindet. Hash-Search(T, k) 1 i=0 2 repeat 3 j = h(k, i) 4 if T [j] = = k 5 return j 6 i = i+1 7 until T [j] = = nil or i = = m 8 return nil Das Entfernen eines Elements aus einer Tabelle mit offener Adressierung ist schwierig. Wenn wir einen Schlüssel aus Slot i entfernen, können wir nicht einfach den Slot dadurch als frei kennzeichnen, indem wir dort nil speichern. Wenn wir dies tun würden, würden wir einen Schlüssel k eventuell nicht wiederfinden, wenn wir beim Einfügen dieses Schlüssels k den Slot i sondiert und als besetzt vorgefunden hatten. Wir können dieses Problem lösen, indem wir in den Slot den speziellen Wert entfernt anstelle des Wertes nil speichern. Wir würden dann die Prozedur Hash-Insert modifizieren, sodass sie einen solchen Slot behandelt, als wäre er leer, sodass wir dort einen neuen Schlüssel einfügen können. Wir brauchen die Prozedur Hash-Search nicht zu verändern, da sie die entfernt-Werte bei der Suche wie belegte Slots behandelt. Wenn wir den speziellen Wert entfernt benutzen, sind allerdings die Suchzeiten nicht länger mehr vom Belegungsfaktor α abhängig. Aus diesem Grunde wird eher die Verkettung als Methode zur Kollisionsauflösung gewählt, wenn Schlüssel entfernt werden müssen. Bei unserer Analyse setzen wir gleichmäßiges Hashing voraus: Jede der m! Permutationen von 0, 1, . . . , m − 1 ist mit gleicher Wahrtscheinlichkeit die Sondierungssequenz
274
11 Hashtabellen
eines Schlüssels. Gleichmäßiges Hashing verallgemeinert den weiter vorn definierten Begriff des einfachen gleichmäßigen Hashings auf den Fall, dass die Hashfunktion nicht nur eine einzelne Zahl, sondern eine ganze Sondierungssequenz erzeugt. Wirkliches gleichmäßiges Hashing ist jedoch schwierig zu implementieren. In der Praxis werden geeignete Näherungen, wie das unten definierte doppelte Hashing, verwendet. Wir werden uns drei bekannte Methoden anschauen, mit denen die für die offene Adressierung notwendigen Sondierungssequenzen berechnet werden können: lineares Sondieren, quadratisches Sondieren und doppeltes Hashing. Diese Methoden garantieren alle, dass für jeden Schlüssel k die Sequenz h(k, 0), h(k, 1), . . . , h(k, m − 1) eine Permutation von 0, 1, . . . , m − 1 ist. Keine dieser Methoden erfüllt jedoch die Voraussetzung des gleichmäßigen Hashings, da keine von ihnen in der Lage ist, mehr als m2 verschiedene Sondierungssequenzen zu erzeugen (für gleichmäßiges Hashing wären m! erforderlich). Doppeltes Hashing liefert die größte Anzahl von Sondierungssequenzen und wie es scheint auch die besten Ergebnisse.
Lineares Sondieren Zu einer gegebenen normalen Hashfunktion h : U → {0, 1, . . . , m − 1}, die wir als Hilfshashfunktion bezeichnen wollen, verwendet die Methode des linearen Sondierens die Hashfunktion h(k, i) = (h (k) + i) mod m mit i = 0, 1, . . . , m − 1. Zu einem gegebenen Schlüssel k sondieren wir zuerst T [h (k)], d. h. denjenigen Slot, der durch die Hilfshashfunktion vorgegeben wird. Als nächstes wird Slot T [h (k)+1] sondiert usw. bis zum Slot T [m−1], um dann zu den Slots T [0], T [1], . . . überzugehen, bis schließlich T [h (k) − 1] erreicht ist. Da der erste sondierte Slot die gesamte Sondierungssequenz festlegt, gibt es nur m verschiedene Sondierungssequenzen. Lineares Sondieren ist leicht zu implementieren, krankt aber an einem Problem, das als primäres Clustern bekannt ist. Es bilden sich lange Folgen besetzter Slots, wodurch sich die Suchzeit erhöht. Cluster entstehen, weil ein leerer Slot, der auf i besetzte Slots folgt, mit der Wahrscheinlichkeit (i + 1)/m als nächstes belegt wird. Lange Folgen besetzter Slots haben daher die Tendenz, noch länger zu werden, wodurch sich die mittlere Suchzeit erhöht.
Quadratisches Sondieren Quadratisches Sondieren verwendet eine Hashfunktion der Form h(k, i) = (h (k) + c1 i + c2 i2 ) mod m
(11.5)
wobei h eine Hilfshashfunktion ist, c1 und c2 positive Hilfskonstanten und i = 0, 1, . . . , m − 1. Der erste sondierte Slot ist T [h (k)]; die weiteren sondierten Slots sind um einen Betrag verschoben, der quadratisch von i abhängt. Dieses Verfahren arbeitet wesentlich besser als lineares Sondieren. Um aber die Hashtabelle voll auszunutzen, müssen die
11.4 Offene Adressierung 0 1 2 3 4 5 6 7 8 9 10 11 12
275
79
69 98 72 14 50
Abbildung 11.5: Einfügen durch doppeltes Hashing. Die Tabelle hat in diesem Beispiel die Länge 13, die Hilfshashfunktionen sind h1 (k) = k mod 13 und h2 (k) = 1 + (k mod 11). Wegen 14 ≡ 1 (mod 13) und 14 ≡ 3 (mod 11) fügen wir den Schlüssel 14 in den leeren Slot 9 ein, nachdem wir die Slots 1 und 5 geprüft und als besetzt erkannt haben.
Werte von c1 , c2 und m bestimmte Nebenbedingungen erfüllen. Problemstellung 11-3 zeigt eine Möglichkeit auf, diese Parameter zu wählen. Außerdem sind zwei Sondierungssequenzen gleich, wenn die Anfangsslots die gleichen sind, denn aus h(k1 , 0) = h(k2 , 0) folgt h(k1 , i) = h(k2 , i). Diese Eigenschaft führt zu einer abgeschwächten Form der Clusterbildung, die als sekundäres Clustern bezeichnet wird. Wie beim linearen Sondieren legt die erste sondierte Position die gesamte Sondierungssequenz fest und so werden nur m verschiedene Sondierungssequenzen verwendet.
Doppeltes Hashing Doppeltes Hashing stellt eines der besten für offene Adressierung verfügbaren Verfahren dar, da die erzeugten Permutationen viele Charakteristika zufällig ausgewählter Permutationen aufweisen. Doppeltes Hashing verwendet eine Hashfunktion der Form h(k, i) = (h1 (k) + i h2 (k)) mod m , wobei h1 und h2 Hilfshashfunktionen sind. Die erste Sondierung erfolgt an der Position T [h1 (k)]; die nachfolgenden Positionen sind bezüglich der vorhergehenden jeweils um h2 (k) modulo m verschoben. Das bedeutet, dass die Sondierungssequenz, anders als im Fall des linearen und quadratischen Sondierens, in zweifacher Hinsicht vom Schlüssel k abhängt, da die Anfangsposition, die Verschiebung oder beide variieren können. Abbildung 11.5 zeigt ein Beispiel für das Einfügen durch doppeltes Hashing. Der Wert h2 (k) muss teilerfremd zur Länge m der Hashtabelle sein, damit die gesamte Hashtabelle durchsucht wird (siehe Übung 11.4-4). Eine geeignete Möglichkeit, diese
276
11 Hashtabellen
Bedingung sicherzustellen, besteht darin, m als Potenz von 2 zu wählen und die Funktion h2 so zu konstruieren, dass sie immer eine ungerade Zahl erzeugt. Eine andere Möglichkeit besteht darin, m als Primzahl zu wählen und die Funktion h2 so zu konstruieren, dass sie immer eine positive ganze Zahl kleiner als m ergibt. Beispielsweise könnten wir für m eine Primzahl wählen und als Hilfshashfunktionen h1 (k) = k mod m , h2 (k) = 1 + (k mod m ) verwenden, wobei m etwas kleiner ist als m (zum Beispiel m = m−1). Für k = 123456, m = 701 und m = 700 erhalten wir h1 (k) = 80 und h2 (k) = 257, sodass die erste Sondierung an Position 80 erfolgt und dann jeder 257-te Slot (modulo m) sondiert wird, bis wir den Schlüssel finden oder jeden Slot überprüft haben. Wenn m eine Primzahl oder eine Potenz von 2 ist, dann ist doppeltes Hashing gegenüber linearem oder quadratischem Hashing in der Hinsicht besser, dass Θ(m2 ) Sondierungssequenzen verwendet werden, anstatt nur Θ(m), denn jedes mögliche Paar (h1 (k), h2 (k)) liefert eine andere Sondierungssequenz. Im Ergebnis scheint für solche Werte von m die Performanz des doppelten Hashings sehr nahe an der Performanz der „idealen“ Methode des gleichmäßigen Hashings zu sein. Wenngleich prinzipiell auch andere Werte als Primzahlen und Zweierpotenzen für m im Rahmen des doppelten Hashings benutzt werden könnten, ist es in der Praxis schwieriger, die Funktion h2 (k) effizient so zu generieren, dass sie teilerfremd zu m ist. Dies ist zum Teil darin begründet, dass die relative Dichte φ(m)/m solcher Zahlen klein sein kann (siehe Gleichung (31.24)).
Analyse des Hashings mit offener Adressierung Wie in unserer Analyse von Hashing mit Verkettung, führen wir unsere Analyse von Hashing mit offener Adressierung bezüglich dem Belegungsfaktor α = n/m einer Hashtabelle aus. Natürlich gibt es bei offener Adressierung höchstens ein Element pro Slot, sodass n ≤ m und damit α ≤ 1 gilt. Wir setzen voraus, dass wir gleichmäßiges Hashing verwenden. In diesem idealisierten Schema hat jede Permutation von 0, 1, . . . , m − 1 die gleiche Wahrscheinlichkeit, als Sondierungssequenz h(k, 0), h(k, 1), . . . , h(k, m − 1) beim Einfügen oder Suchen eines beliebigen Schlüssels k verwendet zu werden. Natürlich ist mit einem gegebenen Schlüssel eine eindeutige feste Sondierungssequenz verbunden; gemeint von uns ist hier, dass bezüglich der Wahrscheinlichkeitsverteilung auf dem Raum der Schlüssel und der Arbeitsweise der Hashfunktion jede mögliche Sondierungssequenz gleichwahrscheinlich ist. Wir analysieren nun die erwartete Anzahl der Sondierungen für das Hashing mit offener Adressierung unter der Voraussetzung des gleichmäßigen Hashings. Beginnen werden wir mit einer Analyse der Anzahl der Sondierungen bei einer erfolglosen Suche. Theorem 11.6 Für eine Hashtabelle mit offener Adressierung und dem Belegungsfaktor α = n/m < 1
11.4 Offene Adressierung
277
ist unter der Voraussetzung gleichmäßigen Hashings die Anzahl der Sondierungen bei einer erfolglosen Suche höchstens 1/(1 − α). Beweis: Bei einer erfolglosen Suche greift jede Sondierung außer der letzten auf einen besetzten Slot zu, der nicht das gewünschte Element enthält. Der letzte sondierte Slot ist leer. Es sei X die Zufallsvariable für die Anzahl der durchgeführten Sondierungen in einer erfolglosen Suche und Ai das Ereignis, dass es eine i-te Sondierung gibt und diese auf einen besetzten Slot zugreift (i = 1, 2, . . .). Dann ist das Ereignis {X ≥ i} der Durchschnitt der Ereignisse A1 ∩ A2 ∩ · · · ∩ Ai−1 . Wir schätzen Pr {X ≥ i} ab, indem wir eine Schranke für Pr {A1 ∩ A2 ∩ · · · ∩ Ai−1 } beweisen. Nach Übung C.2-5 ist Pr {A1 ∩ A2 ∩ · · · ∩ Ai−1 } = Pr {A1 } · Pr {A2 | A1 } · Pr {A3 | A1 ∩ A2 } · · · Pr {Ai−1 | A1 ∩ A2 ∩ · · · ∩ Ai−2 } . Da es n Elemente und m Slots gibt, gilt Pr {A1 } = n/m. Für j > 1 ist die Wahrscheinlichkeit, dass es eine j-te Sondierung gibt und diese auf einen besetzten Slot zugreift, unter der Voraussetzung, dass die ersten j −1 Sondierungen auf besetzten Slots stattfanden, gleich (n − j + 1)/(m − j + 1). Diese Wahrscheinlichkeit ergibt sich, da wir eines der verbleibenden (n − (j − 1)) Elemente auf einem der (m − (j − 1)) noch nicht überprüften Slots auswählen und die Wahrscheinlichkeit unter der Voraussetzung gleichmäßigen Hashings das Verhältnis dieser beiden Größen ist. Aus n < m folgt für alle j mit 0 ≤ j < m die Ungleichung (n − j)/(m − j) ≤ n/m. Deshalb gilt für alle i mit 1 ≤ i ≤ m n i−1 n n−1 n−2 n−i+2 Pr {X ≥ i} = · · ··· ≤ = αi−1 . m m−1 m−2 m−i+2 m Natürlich gilt Pr {X ≥ i} = 0 für i > m. Wir wenden nun Gleichung (C.25) an, um eine Schranke für die erwartete Anzahl der Sondierungen zu erhalten: E [X] = = ≤
∞ i=1 m i=1 m
Pr {X ≥ i} Pr {X ≥ i} +
∞
Pr {X ≥ i}
i=m+1
αi−1 + 0
i=1
≤
∞
αi
i=0
=
1 . 1−α
278
11 Hashtabellen
Diese Schranke von 1/(1 − α) =1 + α + α2 + α3 + · · · hat eine intuitive Interpretation. Wir machen immer eine erste Sondierung. Mit einer Wahrscheinlichkeit von etwa α findet die erste Sondierung auf einem besetzten Slot statt, sodass wir eine zweite Sondierung machen müssen. Mit einer Wahrscheinlichkeit von etwa α2 sind die beiden ersten sondierten Slots besetzt, sodass wir eine dritte Sondierung machen müssen und so weiter. Wenn α eine Konstante ist, sagt Theorem 11.6 eine Laufzeit von O(1) für eine erfolglose Suche voraus. Wenn die Hashtabelle zum Beispiel zur Hälfte gefüllt ist, beträgt die mittlere Anzahl der Sondierungen bei einer erfolglosen Suche höchstens 1/(1 − 0,5) = 2. Ist sie zu 90 Prozent gefüllt, ist die mittlere Anzahl der Sondierungen höchstens 1/(1 − 0,9) = 10. Aus Theorem 11.6 folgt fast unmittelbar eine Aussage über die Performanz der Prozedur Hash-Insert.
Korollar 11.7 Das Einfügen eines Elements in eine Hashtabelle mit offener Adressierung mit dem Belegungsfaktor α erfordert unter der Voraussetzung gleichmäßigen Hashings im Mittel höchstens 1/(1 − α) Sondierungen. Beweis: Ein Element wird nur dann eingefügt, wenn Platz in der Tabelle ist, d. h. wenn α < 1 gilt. Das Einfügen eines Schlüssels erfordert eine erfolglose Suche, gefolgt vom Abspeichern des Schlüssels in den ersten leeren Slot, der gefunden wird. Daher ist die erwartete Anzahl der Sondierungen höchstens 1/(1 − α). Wir haben ein klein wenig mehr Arbeit, um die erwartete Anzahl der Sondierungen bei einer erfolgreichen Suche zu berechnen. Theorem 11.8 Für eine Hashtabelle mit offener Adressierung und dem Belegungsfaktor α ist die erwartete Anzahl der Sondierungen bei einer erfolgreichen Suche unter Voraussetzung gleichmäßigen Hashings höchstens 1 1 ln , α 1−α wenn vorausgesetzt wird, dass nach jedem Schlüssel mit der gleichen Wahrscheinlichkeit gesucht wird. Beweis: Eine Suche nach dem Schlüssel k folgt der gleichen Sondierungssequenz wie das Einfügen des Schlüssels k. Nach Korollar 11.7 ist die erwartete Anzahl der Sondierungen bei einer Suche nach k höchstens 1/(1 − i/m) = m/(m − i), wenn k der (i + 1)-te
11.4 Offene Adressierung
279
Schlüssel ist, der eingefügt wurde. Die Mittelung über alle n Schlüssel liefert uns die erwartete Anzahl der Sondierungen bei einer erfolgreichen Suche n−1 n−1 1 m m 1 = n i=0 m − i n i=0 m − i
1 = α
m k=m−n+1
1 k
! 1 m ≤ (1/x) dx α m−n m 1 = ln α m−n 1 1 . = ln α 1−α
(nach Ungleichung (A.12))
Wenn die Hashtabelle zur Hälfte gefüllt ist, ist die erwartete Anzahl der Sondierungen bei einer erfolgreichen Suche kleiner als 1,387. Ist die Hashtabelle zu 90 Prozent gefüllt, ist die erwartete Anzahl der Sondierungen kleiner als 2,559.
Übungen 11.4-1 Betrachten Sie das Einfügen der Schlüssel 10, 22, 31, 4, 15, 28, 17, 88, 59 in eine Hashtabelle mit offener Adressierung der Länge m = 11 mit der Hilfshashfunktion h (k) = k. Illustrieren Sie das Ergebnis des Einfügens dieser Schlüssel mithilfe linearen Sondierens, quadratischen Sondierens mit c1 = 1 und c2 = 3 sowie durch doppeltes Hashing mit h1 (k) = k und h2 (k) = 1+(k mod (m−1)). 11.4-2 Schreiben Sie ein Programm in Pseudocode für die Prozedur Hash-Delete, wie sie im Text skizziert wurde, und modifizieren Sie die Prozedur HashInsert so, dass sie mit dem speziellen Wert entfernt arbeiten kann. 11.4-3 Betrachten Sie eine Hashtabelle mit offener Adressierung und gleichmäßigen Hashing. Geben Sie obere Schranken für die erwartete Anzahl von Sondierungen einer erfolglosen Suche und die erwartete Anzahl von Sondierungen einer erfolgreichen Suche an, wenn der Belegungsfaktor gleich 3/4 und wenn er gleich 7/8 ist. 11.4-4 Nehmen Sie an, wir wollten doppeltes Hashing zur Kollisionsauflösung verwenden, d. h. wir würden die Hashfunktion h(k, i) = (h1 (k) + i h2 (k)) mod m verwenden. Zeigen Sie, dass, wenn m und h2 (k) für einen Schlüssel k den größten gemeinsamen Teiler d ≥ 1 haben, eine erfolglose Suche nach dem Schlüssel k (1/d)-tel der Slots überprüft, bevor sie zum Slot h1 (k) zurückkehrt. Im Fall von d = 1 kann also die Suche die gesamte Tabelle überprüfen. (Hinweis: Siehe Kapitel 31.)
280
11 Hashtabellen T
0 1 2
S m 0 a0 b0 0 1 0 0 10 m 2 a2 b2 9 10 18
60 72
3
0
4
S5
5 6 7 8
S2
0
1
2
3
4
75 5
6
7
8
m 5 a5 b5 1 0 0 70 m 7 a7 b7 16 23 88
S7
0
40 52 22 0
1
2
3
4
5
6
7
8
9
37 10
11
12
13
14
15
Abbildung 11.6: Die Speicherung der Menge K = {10, 22, 37, 40, 52, 60, 70, 72, 75} unter Verwendung perfekten Hashings. Die äußere Hashfunktion ist h(k) = ((a k + b) mod p) mod m mit a = 3, b = 42, p = 101 und m = 9. Beispielsweise gilt h(75) = 2 und so wird der Schlüssel 75 im Slot 2 der Tabelle T abgespeichert. Eine sekundäre Hashtabelle Sj speichert alle Schlüssel, die auf den Slot j abgebildet werden. Die Länge der Hashtabelle ist mj = n2j , und die dazugehörige Hashfunktion ist hj (k) = ((aj k + bj ) mod p) mod mj . Wegen h2 (75) = 7 wird Schlüssel 75 im Slot 7 der sekundären Hashtabelle S2 gespeichert. Es treten in keiner der sekundären Hashtabellen Kollisionen auf und so benötigt die Suche im schlechtesten Fall konstante Zeit.
11.4-5∗ Betrachten Sie eine Hashtabelle mit offener Adressierung und dem Belegungsfaktor α. Bestimmen Sie den von Null verschiedenen Wert α, für den die erwartete Anzahl der Sondierungen bei einer erfolglosen Suche doppelt so groß wie die erwartete Anzahl der Sondierungen bei einer erfolgreichen Suche ist. Verwenden Sie für diese Erwartungswerte die in den Theoremen 11.6 und 11.8 angegebenen oberen Schranken.
∗ 11.5 Perfektes Hashing Wenngleich Hashing zumeist wegen seines exzellenten Verhaltens im Mittel angewendet wird, kann Hashing auch ein exzellentes Verhalten im schlechtesten Fall vorweisen, wenn die Menge der Schlüssel statisch ist: Sind die Schlüssel einmal in der Tabelle gespeichert, so verändert sich die Menge der Schlüssel nie mehr. Manche Anwendungen haben von Natur aus statische Schlüsselmengen, zum Beispiel die Menge der reservierten Wörter in einer Programmiersprache oder die Menge der Dateinamen auf einer CD-ROM. Wir bezeichnen ein Hashverfahren als perfektes Hashing, wenn im schlechtesten Fall O(1) viele Speicherzugriffe zum Suchen notwendig sind. Um eine Methode für perfektes Hashing zu erhalten, verwenden wir zwei Hash-Ebenen, wobei auf jeder Ebene universelles Hashing angewendet wird. Abbildung 11.6 veranschaulicht diesen Ansatz. Die erste Stufe gleicht im Wesentlichen Hashing mit Verkettung: Die n Schlüssel wer-
11.5 ∗ Perfektes Hashing
281
den durch eine sorgfältig aus einer Familie universeller Hashfunktionen ausgewählte Funktion h auf m Slots abgebildet. Anstatt eine verkettete Liste der auf Slot j abgebildeten Schlüssel zu verwenden, benutzen wir eine kleine sekundäre Hashtabelle Sj mit einer zugeordneten Hashfunktion hj . Wenn wir die Hashfunktionen hj mit Bedacht wählen, können wir sicherstellen, dass es keine Kollisionen auf der zweiten Stufe gibt. Um garantieren zu können, dass es keine Kollisionen auf der zweiten Ebene gibt, muss die Länge mj der Hashtabelle Sj gleich dem Quadrat der Anzahl nj der auf Slot j abgebildeten Schlüssel sein. Auch wenn Sie möglicherweise glauben, dass die Forderung einer solchen quadratischen Abhängigkeit den Gesamtspeicherverbrauch übermäßig in die Höhe treibt, können wir zeigen, dass wir den erwarteten Gesamtumfang des benutzten Speichers auf O(n) beschränken können, wenn wir die Hashfunktion auf der ersten Ebene geschickt wählen. Wir verwenden Hashfunktionen aus einer universellen Klasse von Hashfunktionen (siehe Abschnitt 11.3.3). Die Hashfunktion der ersten Stufe kommt aus der Klasse Hp m , wobei p eine Primzahl größer als alle Schlüsselwerte ist. Diejenigen Schlüssel, die auf Slot j abgebildet werden, werden nochmals durch eine Hashfunktion hj aus der Klasse Hp mj in eine sekundäre Hashtabelle Sj der Länge mj abgebildet.1 Unser weiteres Vorgehen besteht aus zwei Schritten. Zunächst werden wir bestimmen, wie sichergestellt werden kann, dass es in den sekundären Tabellen keine Kollisionen gibt. Dann werden wir zeigen, dass die erwartete Größe des insgesamt benutzten Speichers – für die primäre und die sekundären Hashtabellen – O(n) ist. Theorem 11.9 Nehmen Sie an, wir würden n Schlüssel in eine Hashtabelle der Größe m = n2 unter Verwendung einer zufällig aus einer universellen Klasse von Hashfunktionen ausgewählten Funktion h speichern. Dann ist die Wahrscheinlichkeit, dass eine Kollision vorkommen kann, kleiner als 1/2. Beweis: Es gibt n2 Paare von Schlüsseln, die kollidieren können; jedes Paar kollidiert mit Wahrscheinlichkeit 1/m, wenn h zufällig aus einer universellen Klasse H von Hashfunktionen ausgewählt wird. Es sei X eine Zufallsvariable, die die Anzahl der Kollisionen beschreibt. Für m = n2 ist die erwartete Anzahl der Kollisionen n 1 E [X] = · 2 2 n n2 − n 1 · 2 = 2 n < 1/2 . 1 Im Falle n = m = 1 brauchen wir eigentlich keine Hashfunktion für den Slot j; wenn wir eine j j Hashfunktion hab (k) = ((a k + b) mod p) mod mj für einen solchen Slot wählen, verwenden wir einfach a = b = 0.
282
11 Hashtabellen
(Diese Analyse ähnelt der des Geburtstagsparadoxons in Abschnitt 5.4.1.) Die Anwendung der Markovschen Ungleichung (C.30), Pr {X ≥ t} ≤ E [X] /t mit t = 1, vervollständigt den Beweis. In der in Theorem 11.9 beschriebenen Situation mit m = n2 ist es sehr wahrscheinlich, dass für eine zufällig aus H ausgewählte Hashfunktion h keine Kollisionen auftreten. Für eine gegebene Menge K aus n abzubildenden Schlüsseln (es sei daran erinnert, dass K statisch ist), ist es daher leicht, mit wenigen zufälligen Versuchen eine kollisionsfreie Hashfunktion zu finden. Wenn n groß ist, wird allerdings die Länge der Hashtabelle m = n2 beträchtlich. Daher wenden wir ein zweistufiges Hashverfahren an und nutzen das Verfahren aus Theorem 11.9 nur, um die Einträge innerhalb eines Slots abzubilden. Wir benutzen eine äußere Hashfunktion h, auch Hashfunktion erster Stufe genannt, um die Schlüssel auf m = n Slots abzubilden. Anschließend benutzen wir eine sekundäre Hashtabelle Sj der Größe mj = n2j , wobei nj die Anzahl der Schlüssel sind, die durch die Hashfunktion erster Stufe auf Slot j abgebildet werden, um ein kollisionsfreies Suchen in konstanter Zeit zu ermöglichen. Wir kommen nun zu der Frage, wie man erreichen kann, dass der gesamte verwendete Speicherplatz in O(n) ist. Da die Länge mj der j-ten sekundären Hashtabelle quadratisch mit der Anzahl nj der gespeicherten Schlüssel wächst, laufen wir Gefahr, dass der benutzte Speicher exzessiv wächst. Wenn die Länge m der Tabelle erster Stufe gleich n ist, dann ist der verwendete Speicherplatz für die primäre Hashtabelle, das Speichern der Größen mj der sekundären Hashtabellen und das Speichern der Parameter aj und bj , die zur Spezifizierung der sekundären Hashfunktionen hj aus der Klasse Hp mj dienen (siehe Abschnitt 11.3.3), in O(n) (es sei denn es gilt nj = 1 und wir verwenden a = b = 0). Das folgende Theorem und das sich anschließende Korollar liefern eine Schranke für die erwartete aufaddierte Länge aller sekundären Hashtabellen. Ein zweites Korollar schätzt die Wahrscheinlichkeit ab, dass die aufaddierte Länge aller sekundären Hashtabellen superlinear ist (eigentlich die Wahrscheinlichkeit, dass sie größer gleich 4n ist). Theorem 11.10 Nehmen Sie an, wir würden n Schlüssel unter Verwendung einer Hashfunktion h, die zufällig aus einer universellen Klasse von Hashfunktionen ausgewählt wurde, in einer Hashtabelle der Länge m = n speichern. Es gilt dann ⎡ ⎤ m−1 E⎣ n2j ⎦ < 2n , j=0
wobei nj die Anzahl der Schlüssel ist, die auf Slot j abgebildet werden. Beweis: Wir beginnen unseren Beweis mit der folgenden Identität, die für beliebige
11.5 ∗ Perfektes Hashing
283
nichtnegative ganze Zahlen a gilt: a a2 = a + 2 . 2
(11.6)
Es gilt ⎡
m−1
E⎣
⎤
⎡
⎤ nj ⎦ nj + 2 2
m−1
n2j ⎦ = E ⎣
j=0
j=0
(nach Gleichung (11.6)) ⎤ ⎡ ⎤ ⎡ m−1 m−1 nj ⎦ nj ⎦ + 2 E ⎣ = E⎣ 2 j=0 j=0 (wegen der Linearität des Erwartungswertes) ⎤ ⎡ m−1 nj ⎦ = E [n] + 2 E ⎣ 2 j=0 (nach Gleichung (11.1)) ⎡ ⎤ m−1 nj ⎦ = n +2E⎣ 2 j=0 (da n keine Zufallsvariable ist). m−1 nj
auszuwerten, stellen wir fest, dass diese Summe gerade Um die Summe j=0 2 die Gesamtanzahl von Schlüsselpaare angibt, die in der Hashtabelle zu einer Kollision führen. Aufgrund der Eigenschaften des universellen Hashings ist deren Erwartungswert höchstens n 1 n(n − 1) = 2 m 2m n−1 = , 2 denn es gilt m = n. Damit erhalten wir die Abschätzung ⎡ ⎤ m−1 n−1 E⎣ n2j ⎦ ≤ n + 2 2 j=0 = 2n − 1 < 2n .
284
11 Hashtabellen
Korollar 11.11 Nehmen Sie an, wir würden n Schlüssel unter Verwendung einer Hashfunktion, die zufällig aus einer universellen Klasse von Hashfunktionen ausgewählt wurde, in einer Hashtabelle der Länge m = n speichern und die Größe jeder sekundären Hashtabelle auf mj = n2j (j = 0, 1, . . . , m−1) festlegen. Dann ist der erwartete Speicherverbrauch für alle sekundären Hashtabellen bei perfektem Hashing kleiner als 2n. Beweis: Wegen mj = n2j für j = 0, 1, . . . , m − 1 folgt aus Theorem 11.10 ⎡
m−1
E⎣
⎤
⎡
m−1
mj ⎦ = E ⎣
j=0
⎤ n2j ⎦
j=0
< 2n ,
(11.7)
womit die Aussage bewiesen ist.
Korollar 11.12 Nehmen Sie an, wir würden n Schlüssel unter Verwendung einer Hashfunktion, die zufällig aus einer universellen Klasse von Hashfunktion ausgewählt wurde, in einer Hashtabelle der Länge m = n speichern und die Größe jeder sekundären Hashtabelle auf mj = n2j (j = 0, 1, . . . , m − 1) festlegen. Dann ist die Wahrscheinlichkeit, dass der gesamte Speicherverbrauch für die sekundären Hashtabellen größer gleich 4n ist, kleiner als 1/2. Beweis: Wir wenden die Markovsche Ungleichung (C.30), Pr {X ≥ t} ≤ E [X] /t, an m−1 und zwar diesmal auf die Ungleichung (11.7) mit X = j=0 mj und t = 4n:
Pr
⎧ ⎨m−1 ⎩
j=0
mj ≥ 4n
⎫ ⎬ ⎭
≤
2 lg n} = O(1/n2 ) gilt. Die Zufallsvariable X = max1≤i≤n Xi sei die maximale Anzahl der erforderlichen Sondierungen über alle n Einfügungen. c. Zeigen Sie, dass Pr {X > 2 lg n} = O(1/n) gilt. d. Zeigen Sie, dass die erwartete Länge E [X] der längsten Sondierungssequenz O(lg n) ist. 11-2 Eine Schranke für die Slotlänge bei Hashing mit Verkettung Nehmen Sie an, wir hätten eine Hashtabelle mit n Slots, in die n Schlüssel eingefügt werden. Kollisionen werden durch Verkettung aufgelöst. Jeder Schlüssel wird mit gleicher Wahrscheinlichkeit auf einen der Slots abgebildet. Sei M die maximale Anzahl der auf dem gleichen Slot gespeicherten Schlüssel, nachdem alle Schlüssel eingefügt wurden. Ihre Aufgabe besteht darin, eine obere Schranke von O(lg n/ lg lg n) für E [M ], den Erwartungswert von M , zu beweisen. a. Zeigen Sie, dass die Wahrscheinlichkeit Qk , dass genau k Schlüssel auf einen bestimmten Slot abgebildet werden, durch k n−k 1 n 1 Qk = 1− k n n gegeben ist.
286
11 Hashtabellen b. Sei Pk die Wahrscheinlichkeit, dass M gleich k ist, d. h. die Wahrscheinlichkeit, dass der Slot mit den meisten Schlüsseln genau k Schlüssel enthält. Zeigen Sie, dass Pk ≤ n Qk gilt. c. Verwenden Sie die Stirlingsche Näherung, Gleichung (3.18), um zu zeigen, dass Qk < ek /k k gilt. d. Zeigen Sie, dass eine Konstante c > 1 existiert, sodass Qk0 < 1/n3 für k0 = c lg n/ lg lg n gilt. Folgern Sie hieraus, dass Pk < 1/n2 für k ≥ k0 = c lg n/ lg lg n gilt. e. Zeigen Sie, dass die Abschätzung : : c lg n c lg n c lg n E [M ] ≤ Pr M > · n + Pr M ≤ · lg lg n lg lg n lg lg n gilt. Schlussfolgern Sie, dass E [M ] = O(lg n/ lg lg n) gilt.
11-3 Quadratisches Sondieren Nehmen Sie an, wir hätten einen Schlüssel k gegeben, nach dem wir in einer Hashtabelle mit den Slots 0, 1, . . . , m − 1 suchen müssten, und nehmen Sie an, wir hätten eine Hashfunktion h, die den Schlüsselraum in die Menge {0, 1, . . . , m − 1} abbildet. Das Suchschema sieht wie folgt aus: 1. Berechnen Sie den Wert j = h(k) und setzen Sie i = 0. 2. Überprüfen Sie, ob Position j den gesuchten Schlüssel k enthält. Wenn Sie ihn finden oder wenn diese Position leer ist, beenden Sie die Suche. 3. Setzen Sie i = i + 1. Beenden Sie die Suche, wenn i nun gleich m ist, d. h. wenn die Tabelle voll ist. Anderenfalls setzen Sie j = (i + j) mod m und fahren Sie wieder mit Schritt 2 fort. Nehmen Sie an, dass m eine Potenz von 2 ist. a. Zeigen Sie, dass diese Methode eine Instanz der allgemeinen Methode der „quadratischen Sondierung“ ist, indem Sie die Konstanten c1 und c2 aus Gleichung (11.5) geeignet wählen. b. Beweisen Sie, dass dieser Algorithmus im schlechtesten Fall jeden Slot überprüft. 11-4 Hashing und Authentifikation Es sei H eine Klasse von Hashfunktionen, in der jede Funktion h ∈ H die Universalmenge U der Schlüssel in die Menge {0, 1, . . . , m − 1} abbildet. Wir bezeichnen H als k-universell, falls für jede feste, aus k verschiedenen Schlüsseln bestehende Folge x(1) , x(2) , . . . , x(k) und jede zufällig aus H gewählte Funktion h, die Folge h(x(1) ), h(x(2) ), . . . , h(x(k) ) jeweils mit gleicher Wahrscheinlichkeit eine der mk möglichen Sequenzen der Länge k mit Elementen aus {0, 1, . . . , m − 1} ist. a. Zeigen Sie, dass eine Familie H von Hashfunktionen universell ist, wenn sie 2-universell ist.
Kapitelbemerkungen zu Kapitel 11
287
b. Nehmen Sie an, die Universalmenge U wäre die Menge der n-Tupel mit Werten aus Zp = {0, 1, . . . , p − 1}, wobei p eine Primzahl ist. Betrachten Sie ein x = x0 , x1 , . . . , xn−1 ∈ U . Für ein beliebiges n-Tupel a = a0 , a1 , . . . , an−1 ∈ U definieren Sie die Hashfunktion ha durch ⎞ ⎛ n−1 aj xj ⎠ mod p . ha (x) = ⎝ j=0
Sei H = {ha : a ∈ U }. Zeigen Sie, dass H universell ist, jedoch nicht 2-universell. (Hinweis: Finden Sie einen Schlüssel, für den alle Hashfunktionen aus H den gleichen Wert erzeugen.) c. Nehmen Sie an, wir würden die Klasse H aus Problemteil (b) ein wenig modifizieren: Für jedes a ∈ U und jedes b ∈ Zp definieren Sie ⎛ ⎞ n−1 hab (x) = ⎝ aj xj + b⎠ mod p j=0
und H = {hab : a ∈ U, b ∈ Zp }. Zeigen Sie, dass H 2-universell ist. (Hinweis: Betrachten Sie zwei feste n-Tupel x ∈ U und y ∈ U mit xi = yi für ein i. Was geschieht mit hab (x) und hab (y), wenn ai und b den Bereich von Zp durchlaufen?) d. Nehmen Sie an, Alice und Bob hätten eine Hashfunktion h aus einer 2-universellen Familie H von Hashfunktionen geheim unter sich vereinbart. Jede Funktion h ∈ H ist eine Abbildung aus der Universalmenge U der Schlüssel in die Menge Zp , wobei p eine Primzahl ist. Später sendet Alice über das Internet eine Nachricht m mit m ∈ U an Bob. Sie authentifiziert diese Nachricht, indem sie zusätzlich eine Authentifizierungsmarke t = h(m) an Bob schickt, und Bob prüft, ob das Paar (m, t), das er empfangen hat, tatsächlich die Bedingung t = h(m) erfüllt. Nehmen Sie an, dass ein Feind (m, t) unterwegs abfangen würde und versuchen würde, Bob zu täuschen, indem er das Paar (m, t) durch ein anderes Paar (m , t ) ersetzt. Zeigen Sie, dass die Wahrscheinlichkeit, dass es dem Feind gelingt, Bob zu täuschen, und dieser das Paar (m , t ) akzeptiert, höchstens 1/p ist, egal welche Rechenleistung dem Feind zur Verfügung steht und selbst dann, wenn der Feind die Familie H der Hashfunktionen kennt.
Kapitelbemerkungen Knuth [211] und Gonnet [145] sind exzellente Referenzen für die Analyse von Hashalgorithmen. Knuth schreibt H. P. Luhn (1953) die Einführung der Hashtabellen zu, ebenso wie die Verkettung für die Auflösung von Kollisionen. Etwa zur gleichen Zeit kreierte G. M. Amdahl die Idee der offenen Adressierung. Carter und Wegman [58] führten 1979 den Begriff der universellen Klasse von Hashfunktionen ein.
288
11 Hashtabellen
Fredman, Komlós und Szemerédi [112] entwickelten das Schema des perfekten Hashings für statische Mengen, das in Abschnitt 11.5 vorgestellt wurde. Eine Erweiterung ihrer Methode auf dynamische Mengen, die Einfügen und Löschen in amortisierter erwarteter Zeit O(1) bewältigt, wurde von Dietzfelbinger et al. [86] angegeben.
12
Binäre Suchbäume
Die Suchbaum-Datenstruktur unterstützt viele Operationen dynamischer Mengen, einschließlich der Prozeduren Search, Minimum, Maximum, Predecessor, Successor, Insert und Delete. Daher kann ein Suchbaum sowohl als Wörterbuch als auch als Prioritätswarteschlange verwendet werden. Die Grundoperationen auf einem binären Suchbaum benötigen Zeit, die proportional zur Höhe des Baumes ist. Für einen vollständigen binären Baum mit n Knoten laufen solche Operationen im schlechtesten Fall in Zeit Θ(lg n). Wenn der Baum eine lineare Kette aus n Knoten ist, benötigen die gleichen Operationen im schlechtesten Fall jedoch Zeit Θ(n). Wir werden in Abschnitt 12.4 sehen, dass die erwartete Höhe eines zufällig erzeugten binären Suchbaums O(lg n) ist, sodass die Operationen für dynamische Mengen auf einem solchen Baum im Mittel Zeit Θ(lg n) benötigen. In der Praxis können wir nicht immer garantieren, dass binäre Suchbäume zufällig erzeugt werden; wir können aber Varianten binärer Suchbäume entwerfen, die beweisbar eine gute Performanz der Grundoperationen im schlechtesten Fall aufweisen. Kapitel 13 stellt eine dieser Varianten, die Rot-Schwarz-Bäume, vor, die eine Höhe von O(lg n) haben. Kapitel 18 führt die B-Bäume ein, die besonders für das Halten von Datenbanken auf sekundärem (Platten-)Speicher geeignet sind. Nach der Darstellung der grundlegenden Eigenschaften binärer Suchbäume zeigen die folgenden Abschnitte, wie ein binärer Suchbaum traversiert werden kann, um seine Werte in sortierter Reihenfolge auszugeben, wie in einem binären Suchbaum nach einem Wert gesucht werden kann, wie man das minimale oder das maximale Element finden kann und wie Elemente in binäre Suchbäume eingefügt oder gelöscht werden können. Die wichtigsten mathematischen Eigenschaften von Bäumen finden Sie in Anhang B.
12.1
Was ist ein binärer Suchbaum?
Ein binärer Suchbaum ist, wie der Name vermuten lässt, als binärer Baum organisiert (siehe Abbildung 12.1). Ein solcher Baum kann durch eine verkettete Datenstruktur repräsentiert werden, in der jeder Knoten ein Objekt ist. Neben dem Attribut schlüssel und den Satellitendaten enthält jeder Knoten die Attribute links, rechts und vater , die jeweils auf den Knoten zeigen, der seinem linken Kind, seinem rechten Kind bzw. seinem Vater entspricht. Wenn ein Kind oder der Vater fehlt, enthält das entsprechende Attribut den Wert nil. Der Wurzelknoten ist der einzige Knoten des Baumes, dessen Vater-Attribut nil ist.
290
12 Binäre Suchbäume 6 5
2
2 5
7 5
7
8 6
8
5 (a)
(b)
Abbildung 12.1: Binäre Suchbäume. Für jeden Knoten x sind die Schlüssel im linken Teilbaum von x kleiner oder gleich x. schl u ¨ ssel und die Schlüssel im rechten Teilbaum von x sind größer oder gleich x. schl u ¨ ssel . Unterschiedliche binäre Suchbäume können die gleiche Menge von Werten repräsentieren. Für die meisten Suchbaum-Operationen ist die Laufzeit im schlechtesten Fall proportional zur Höhe des Baumes. (a) Ein binärer Suchbaum auf sechs Knoten mit der Höhe 2. (b) Ein weniger effizienter Suchbaum mit der Höhe 4, der die gleichen Schlüssel enthält.
Die Schlüssel werden immer so gespeichert, dass die binäre-Suchbaum-Eigenschaft erfüllt ist: Sei x ein Knoten in einem binären Suchbaum. Wenn y ein Knoten im linken Teilbaum von x ist, dann gilt y.schl¨u ssel ≤ x.schl¨u ssel . Wenn y ein Knoten im rechten Teilbaum von x ist, dann gilt x.schl¨u ssel ≤ y.schl¨u ssel . In Abbildung 12.1(a) ist der Schlüssel der Wurzel gleich 6, die Schlüssel 2, 5 und 5 in ihrem linken Teilbaum sind nicht größer als 6, und die Schlüssel 7 und 8 in ihrem rechten Teilbaum sind nicht kleiner als 6. Die gleiche Eigenschaft gilt für jeden Knoten des Baumes. Zum Beispiel ist der Schlüssel 5 in dem linken Kind der Wurzel nicht kleiner als der Schlüssel 2 in seinem linken Teilbaum und nicht größer als der Schlüssel 5 in seinem rechten Teilbaum. Die binäre-Suchbaum-Eigenschaft erlaubt es uns, mit einem einfachen rekursiven Algorithmus alle Schlüssel eines binären Suchbaums in sortierter Reihenfolge auszugeben. Dieser Algorithmus wird Inorder-Traversierung genannt. Der Name erklärt sich daraus, dass der Algorithmus den Schlüssel der Wurzel eines Teilbaums zwischen der Ausgabe der im linken Teilbaum enthaltenen Werte und der Ausgabe der im rechten Teilbaum enthaltenen Werte ausgibt. (Entsprechend gibt eine Preorder-Traversierung die Wurzel vor den Werten beider Teilbäume aus, und eine Postorder-Traversierung gibt die Wurzel nach den Werten ihrer Teilbäume aus.) Die folgende Prozedur zum Ausgeben aller Elemente eines binären Suchbaumes T rufen wir mit Inorder-TreeWalk(T.wurzel ) auf.
12.1 Was ist ein binärer Suchbaum?
291
Inorder-Tree-Walk(x) 1 if x = nil 2 Inorder-Tree-Walk(x.links) 3 print x.schl¨u ssel 4 Inorder-Tree-Walk(x.rechts) Zum Beispiel werden bei der Inorder-Traversierung für jeden der beiden binären Suchbäume aus Abbildung 12.1 die Schlüssel in der Reihenfolge 2, 5, 5, 6, 7, 8 ausgegeben. Die Korrektheit des Algorithmus folgt per Induktion direkt aus der binären-SuchbaumEigenschaft. Die Traversierung eines binären Suchbaumes benötigt Zeit Θ(n), denn nach dem initialen Aufruf ruft sich die Prozedur für jeden Knoten des Baumes genau zweimal selbst rekursiv auf – einmal für den linken Teilbaum und einmal für den rechten Teilbaum. Das folgende Theorem liefert einen formalen Beweis, dass die Ausführung einer InorderTraversierung nur lineare Zeit erfordert. Theorem 12.1 Wenn x die Wurzel eines Teilbaums mit n Knoten ist, dann benötigt InorderTree-Walk(x) Zeit Θ(n). Beweis: Es sei T (n) die Zeit, die die Prozedur Inorder-Tree-Walk benötigt, wenn sie auf der Wurzel eines Teilbaums mit n Knoten aufgerufen wird. Da Inorder-TreeWalk jeden der n Knoten des Teilbaumes besucht, gilt T (n) = Ω(n). Es bleibt T (n) = O(n) zu zeigen. Da Inorder-Tree-Walk angesetzt auf einen leeren Teilbaum einen (kleinen) konstanten Zeitaufwand (für den Test x = nil) benötigt, gilt T (0) = c für eine positive Konstante c. Für n > 0 nehmen Sie an, dass Inorder-Tree-Walk auf einem Knoten aufgerufen würde, dessen rechter Teilbaum n − k − 1 Knoten hat. Die Zeit für die Ausführung von Inorder-Tree-Walk(x) ist T (n) ≤ T (k) + T (n − k − 1) + d für eine positive Konstante d, die die Zeit für die Ausführung von Inorder-Tree-Walk(x) ohne die Zeit, die durch die rekursiven Aufrufen benötigt wird, widerspiegelt. Wir zeigen mithilfe der Substitutionsmethode, dass T (n) = O(n) gilt, indem wir die Gleichung T (n) = (c + d) n + c beweisen. Für n = 0 gilt (c + d) · 0 + c = c = T (0) und für n > 0 T (n) ≤ T (k) + T (n − k − 1) + d = ((c + d) k + c) + ((c + d)(n − k − 1) + c) + d = (c + d) n + c − (c + d) + c + d = (c + d) n + c , womit das Theorem bewiesen ist.
292
12 Binäre Suchbäume
Übungen 12.1-1 Zeichnen Sie für die Menge {1, 4, 5, 10, 16, 17, 21} von Schlüsseln binäre Suchbäume mit den Höhen 2, 3, 4, 5 und 6. 12.1-2 Worin liegt der Unterschied zwischen der binären-Suchbaum-Eigenschaft und der Min-Heap-Eigenschaft (siehe Seite 155)? Kann die Min-Heap-Eigenschaft verwendet werden, um die Schlüssel eines Baumes mit n Knoten in sortierter Reihenfolge in Zeit O(n) auszugeben? Zeigen Sie, wie dies möglich ist, oder erklären Sie, warum dies nicht möglich ist. 12.1-3 Geben Sie einen nichtrekursiven Algorithmus für eine Inorder-Traversierung an. (Hinweis: Eine einfache Lösung verwendet einen Stapel als Hilfsdatenstruktur. Eine etwas kompliziertere, dafür aber elegantere Lösung verwendet keinen Stapel, setzt aber voraus, dass wir zwei Zeiger auf Gleichheit testen können.) 12.1-4 Geben Sie rekursive Algorithmen an, die eine Preorder- bzw. eine PostorderTraversierung auf einem Baum mit n Knoten in Zeit Θ(n) ausführen. 12.1-5 Zeigen Sie: Da das vergleichsbasierte Sortieren von n Elementen im schlechtesten Fall Zeit Ω(n lg n) benötigt, braucht jeder vergleichsbasierte Algorithmus zur Konstruktion eines binären Suchbaums aus einer beliebigen Liste mit n Elementen im schlechtesten Fall Zeit Ω(n lg n).
12.2
Abfragen in einem binären Suchbaum
Wir müssen oft nach einem in einem binären Suchbaum abgespeicherten Schlüssel suchen. Neben der Search-Operation können binäre Suchbäume auch Abfragen wie die nach dem Minimum, dem Maximum, dem Vorgänger und dem Nachfolger unterstützen. In diesem Abschnitt werden wir uns diese Operationen näher anschauen und zeigen, wie jede dieser Operationen in Zeit O(h) auf einem Suchbaum der Höhe h realisiert werden kann.
Suchen Wir verwenden folgende Prozedur, um einen Knoten mit einem gegebenen Schlüssel in einem binären Suchbaum zu suchen. Bei gegebenem Zeiger auf die Wurzel des Baumes und gegebenem Schlüssel k, gibt Tree-Search einen Zeiger auf einen Knoten mit Schlüssel k zurück, sofern ein solcher existiert; ansonsten gibt sie den Wert nil zurück. Tree-Search(x, k) 1 if x = = nil or k = = x.schl¨u ssel 2 return x 3 if k < x.schl¨u ssel 4 return Tree-Search(x.links , k) 5 else return Tree-Search(x.rechts, k)
12.2 Abfragen in einem binären Suchbaum
293
15 6 7
3 2
18
4
17
20
13 9
Abbildung 12.2: Abfragen auf einem binären Suchbaum. Um den Schlüssel 13 im Baum zu suchen, folgen wir von der Wurzel ausgehend dem Weg 15 → 6 → 7 → 13. Der kleinste Schlüssel im Baum ist 2. Er kann gefunden werden, indem man ausgehend von der Wurzel den links-Zeigern folgt. Der größte Schlüssel 20 wird gefunden, indem man ausgehend von der Wurzel den rechts -Zeigern folgt. Der Nachfolger des Knotens mit dem Schlüssel 15 ist der Knoten mit dem Schlüssel 17, denn er ist der kleinste Schlüssel im rechten Teilbaum von 15. Der Knoten mit dem Schlüssel 13 hat keinen rechten Teilbaum. Daher ist sein Nachfolger der letzte Vorfahre, dessen linkes Kind auch einer seiner Vorfahren ist. In diesem Fall ist dies der Knoten mit dem Schlüssel 15.
Die Prozedur beginnt ihre Suche an der Wurzel und traversiert einen einfachen, nach unten gerichteten Pfad im Baum, wie in Abbildung 12.2 gezeigt ist. Für jeden Knoten x entlang des Pfades vergleicht sie den Schlüssel k mit x.schl¨u ssel . Wenn die beiden Schlüssel gleich sind, wird die Suche beendet. Wenn k kleiner als x.schl¨u ssel ist, dann wird die Suche im linken Teilbaum von x fortgesetzt, da aus der binären-SuchbaumEigenschaft folgt, dass k nicht im rechten Teilbaum gespeichert sein kann. Entsprechend wird die Suche im rechten Teilbaum fortgesetzt, wenn k größer als x.schl¨u ssel ist. Die während der Rekursion besuchten Knoten bilden einen einfachen Pfad von der Wurzel nach unten. Deshalb ist die Laufzeit von Tree-Search in O(h), wobei h die Höhe des Baumes ist. Wir können diese Prozedur zu einer iterativen Prozedur umschreiben, indem wir die Rekursion in eine while-Schleife ausrollen. Auf den meisten Rechnern ist die iterative Version effizienter.
Iterative-Tree-Search(x, k) 1 while x = nil and k = x.schl¨u ssel 2 if k < x.schl¨u ssel 3 x = x.links 4 else x = x.rechts 5 return x
294
12 Binäre Suchbäume
Minimum und Maximum Wir können das Element eines binären Suchbaums mit dem kleinsten Schlüssel immer finden, indem wir ausgehend von der Wurzel des Baumes den links-Zeigern folgen, bis wir einem nil begegnen (siehe Abbildung 12.2). Die folgende Prozedur gibt einen Zeiger auf das kleinste Element des Teilbaumes eines gegebenen Knotens x zurück, von dem wir voraussetzen, dass er nicht nil ist.
Tree-Minimum(x) 1 while x.links = nil 2 x = x.links 3 return x
Die binäre Suchbaum-Eigenschaft garantiert, dass Tree-Minimum korrekt ist. Wenn ein Knoten x keinen linken Teilbaum hat, dann ist der kleinste Schlüssel in dem vom Knoten x ausgehenden Teilbaum gleich x.schl¨u ssel , da jeder Schlüssel im rechten Teilbaum von x mindestens so groß wie x.schl¨u ssel ist. Wenn Knoten x einen linken Teilbaum hat, dann ist der kleinste Schlüssel im Teilbaum von x im Teilbaum des Knotens x.links zu finden, denn kein Schlüssel im rechten Teilbaum ist kleiner als x.schl¨u ssel und kein Schlüssel im linken Teilbaum ist größer als x.schl¨u ssel . Das Programm für Tree-Maximum ist analog aufgebaut:
Tree-Maximum(x) 1 while x.rechts = nil 2 x = x.rechts 3 return x
Beide Prozeduren laufen auf einem Baum der Höhe h in Zeit O(h), da die Folge der besuchten Knoten wie bei Tree-Search einen von der Wurzel aus abwärts laufenden einfachen Pfad bildet.
Vorgänger und Nachfolger In einigen Fällen müssen wir den Nachfolger eines gegebenen Knotens bezüglich der durch eine Inorder-Traversierung definierten sortierten Ordnung finden. Wenn die Schlüssel paarweise verschieden sind, dann ist der Nachfolger eines Knotens x derjenige Knoten mit dem kleinsten Schlüssel, der größer als x.schl¨u ssel ist. Die Struktur des binären Suchbaums erlaubt es uns, den Nachfolger eines Knotens zu bestimmen, ohne jemals Schlüssel zu vergleichen. Die folgende Prozedur gibt den Nachfolger eines Knotens x in einem binären Suchbaum zurück, falls dieser existiert, und nil, falls x den größten Wert von allen Schlüsseln des Baumes hat.
12.2 Abfragen in einem binären Suchbaum
295
Tree-Successor(x) 1 if x.rechts = nil 2 return Tree-Minimum(x.rechts) 3 y = x.vater 4 while y = nil and x = = y.rechts 5 x=y 6 y = y.vater 7 return y Wir unterscheiden im Code von Tree-Successor zwei Fälle. Falls der rechte Teilbaum des Knotens x nichtleer ist, dann ist der Nachfolger von x der am weitesten links liegende Knoten des rechten Teilbaums, den wir in Zeile 2 finden, indem wir Tree-Minimum(x.rechts) aufrufen. In Abbildung 12.2 zum Beispiel ist der Nachfolger des Knotens mit dem Schlüssel 15 der Knoten mit dem Schlüssel 17. Wenn dagegen der rechte Teilbaum des Knotens x leer ist und x einen Nachfolger y hat, dann ist y der jüngste Vorfahre von x, dessen linkes Kind ebenfalls ein Vorfahre von x ist. Dies sollen Sie in Übung 12.2-6 zeigen. In Abbildung 12.2 ist der Nachfolger des Knotens mit dem Schlüssel 13 der Knoten mit dem Schlüssel 15. Um y zu finden, gehen wir einfach im Baum von x aus nach oben, bis wir einen Knoten finden, der das linke Kind seines Vaters ist; die Zeilen 3–7 behandeln diesen Fall. Die Laufzeit von Tree-Successor auf einem Baum der Höhe h ist O(h), da wir entweder einem aufwärts laufenden einfachen Pfad oder einem abwärts laufenden einfachen Pfad im Baum folgen. Die Prozedur Tree-Predecessor, die analog zu TreeSuccessor arbeitet, läuft ebenfalls in Zeit O(h). Auch wenn die Schlüssel nicht paarweise verschieden sind, definieren wir den Nachfolger bzw. den Vorgänger eines beliebigen Knotens x als denjenigen Knoten, der von der Prozedur Tree-Successor(x) bzw. Tree-Predecessor(x) zurückgegeben wird. Zusammenfassend haben wir das folgende Theorem bewiesen. Theorem 12.2 Wir können die Operationen Search, Minimum, Maximum, Successor und Predecessor für dynamische Mengen so implementieren, dass sie auf einem binären Suchbaum der Höhe h jeweils in Zeit O(h) ausgeführt werden.
Übungen 12.2-1 Nehmen Sie an, wir hätten Zahlen zwischen 1 und 1000 in einem binären Suchbaum gespeichert und wollten nach der Zahl 363 suchen. Welche der folgenden Sequenzen kann nicht die überprüfte Knotenfolge sein? a. b. c.
2, 252, 401, 398, 330, 344, 397, 363. 924, 220, 911, 244, 898, 258, 362, 363 925, 202, 911, 240, 912, 245, 363.
296
12 Binäre Suchbäume d.
2, 399, 387, 219, 266, 382, 381, 278, 363.
e.
935, 278, 347, 621, 299, 392, 358, 363.
12.2-2 Geben Sie jeweils eine rekursive Version der Prozeduren Tree-Minimum und Tree-Maximum an. 12.2-3 Schreiben Sie die Prozedur Tree-Predecessor. 12.2-4 Professor Bunyan glaubt, er habe eine bemerkenswerte Eigenschaft binärer Suchbäume entdeckt. Nehmen Sie an, die Suche in einem binären Suchbaum nach dem Schlüssel k würde in einem Blatt enden. Betrachten Sie drei Mengen: A, die Menge der Schlüssel, die links vom Suchpfad liegen, B, die Menge der Schlüssel auf dem Suchpfad, und C, die Menge der Schlüssel rechts vom Suchpfad. Professor Bunyan behauptet, dass jedes Tripel von Schlüsseln a ∈ A, b ∈ B und c ∈ C die Ungleichung a ≤ b ≤ c erfüllen muss. Geben Sie ein möglichst kurzes Gegenbeispiel zu der Behauptung des Professors an. 12.2-5 Zeigen Sie folgende Behauptung: In einem binären Suchbaum gilt für einen Knoten, der zwei Kinder hat, dass sein Nachfolger kein linkes Kind hat und dass sein Vorgänger kein rechtes Kind hat. 12.2-6 Betrachten Sie einen binären Suchbaum T mit paarweise verschiedenen Schlüsseln. Zeigen Sie folgende Behauptung: Falls der rechte Teilbaum eines Knotens x aus T leer ist und x einen Nachfolger y hat, dann ist y der letzte Vorfahre von x, dessen linkes Kind ebenfalls Vorfahre von x ist. (Es sei daran erinnert, dass jeder Knoten Vorfahre von sich selbst ist.) 12.2-7 Eine alternative Methode, eine Inorder-Traversierung in einem aus n Knoten bestehenden binären Suchbaum durchzuführen, berechnet das kleinste Element des Baumes, indem sie die Prozedur Tree-Minimum aufruft, und ruft anschließend (n − 1)-mal die Prozedur Tree-Successor auf. Beweisen Sie, dass dieser Algorithmus eine Laufzeit von Θ(n) hat. 12.2-8 Beweisen Sie, dass k sukzessive Aufrufe von Tree-Successor Zeit O(k + h) benötigen, egal von welchem Knoten eines binären Suchbaums der Höhe h wir starten. 12.2-9 Es sei T ein binärer Suchbaum mit paarweise verschiedenen Schlüsseln, x ein Blattknoten und y dessen Vater. Zeigen Sie, dass y.schl¨u ssel entweder der kleinste Schlüssel aus T ist, der größer als x.schl¨u ssel ist, oder der größte Schlüssel aus T , der kleiner als x.schl¨u ssel ist.
12.3
Einfügen und Löschen
Die Operationen Einfügen und Löschen bewirken, dass sich die dynamische Menge, die durch einen binären Suchbaum repräsentiert wird, verändert. Die Datenstruktur muss
12.3 Einfügen und Löschen
297
12 5 2
18 9
19
15 13
17
Abbildung 12.3: Das Einfügen eines Elementes mit dem Schlüssel 13 in einen binären Suchbaum. Die schwach schattierten Knoten markieren den Weg von der Wurzel nach unten zu der Position, an der das Element eingefügt wird. Die gestrichelte Linie stellt die Verbindung dar, die dem Baum hinzugefügt wurde, um das Element einzufügen.
aktualisiert werden, um diese Änderung widerzuspiegeln, jedoch derart, dass die binäreSuchbaum-Eigenschaft beibehalten wird. Wie wir sehen werden, ist es relativ unkompliziert, den Baum in Bezug auf das Einfügen eines neuen Elementes zu aktualisieren; der Fall des Löschens ist jedoch etwas knifflig.
Einfügen Um einen neuen Wert v in einen binären Suchbaum T einzufügen, verwenden wir die Prozedur Tree-Insert. Die Prozedur bekommt als Eingabeparameter einen Knoten z, für den z.schl¨u ssel = v, z.links = nil und z.rechts = nil gilt. Sie aktualisiert T und einige der Attribute von z so, dass sie z an eine geeignete Position im Baum eingefügt. Tree-Insert(T, z) 1 y = nil 2 x = T.wurzel 3 while x = nil 4 y =x 5 if z.schl¨u ssel < x.schl¨u ssel 6 x = x.links 7 else x = x.rechts 8 z.vater = y 9 if y = = nil 10 T.wurzel = z // Baum T war leer 11 elseif z.schl¨u ssel < y.schl¨u ssel 12 y.links = z 13 else y.rechts = z Abbildung 12.3 zeigt die Arbeitsweise von Tree-Insert. Ebenso wie die Prozeduren Tree-Search und Iterative-Tree-Search startet Tree-Insert an der Wurzel des Baumes; der Zeiger x durchläuft einen einfachen Pfad nach unten und sucht nach einem nil, das durch das Eingabelement z ersetzt werden kann. Die Prozedur führt einen sogenannten Pfadzeiger y, der auf den Vater von x zeigt. Nach der Initialisierung bewirkt
298
12 Binäre Suchbäume
die while-Schleife in den Zeilen 3–7, dass diese beiden Zeiger im Baum abwärts laufen und dabei nach links oder rechts gehen, je nachdem wie die Vergleiche von z.schl¨u ssel mit x.schl¨u ssel ausgehen, solange bis x nil wird. Dieses nil belegt den Platz, an dem wir das Eingabeelement z einfügen wollen. Wir benötigen den Pfadzeiger y, da die Suche in dem Moment, zu dem wir das nil finden, wo z hingehört, bereits ein Schritt hinter dem Knoten ist, welcher aktualisiert werden muss. Die Zeilen 8–13 setzen die Zeiger so, dass das Element z gerade an diese Stelle eingefügt wird. Wie die anderen einfachen Operationen auf Suchbäumen läuft die Prozedur TreeInsert auf einem binären Suchbaum der Höhe h in Zeit O(h).
Löschen Die allgemeine Strategie, um einen Knoten z aus einem binären Baum T zu löschen, besteht aus drei Basisfällen; einer dieser Fälle ist, wie wir noch sehen werden, ein wenig verzwickt. • Wenn z keine Kinder hat, dann entfernen wir den Knoten einfach, indem wir in z’s Vater das Kind z durch nil ersetzen. • Wenn z genau ein Kind hat, dann lassen wir dieses Kind um eine Ebene im Baum hochsteigen, sodass es im Baum die Position von z übernimmt, indem wir in z’s Vater z durch das Kind von z ersetzen. • Wenn z zwei Kinder hat, dann suchen wir z’s Nachfolger y – der in dem rechten Teilbaum von z sein muss – und lassen y die Position von z im Baum einnehmen. Der verbleibende Rest von dem ursprünglichen rechten Teilbaum von z wird zum neuen rechten Teilbaum von y und der linke Teilbaum von z wird der linke Teilbaum von y. Dieser Fall ist ein wenig verzwickt, da, wie wir sehen werden, es wichtig ist, ob y das rechte Kind von z ist. Die Prozedur zum Löschen eines gegebenen Knotens z aus einem binären Suchbaum T erhält als Eingabeparameter Zeiger auf T und z. Sie organisiert die Fälle ein wenig anders, als dies gerade von uns mit den drei oben skizzierten Fällen gemacht worden ist, und betrachtet die vier in Abbildung 12.4 illustrierten Fälle. • Wenn z kein linkes Kind hat (Teil (a) der Abbildung), dann ersetzen wir z durch sein rechtes Kind, unabhängig davon, ob dieses nil oder nicht nil ist. Wenn das rechte Kind gleich nil ist, dann liegt der Fall vor, in dem z keine Kinder hat. Ist das rechte Kind von z nicht nil, dann liegt der Fall vor, in dem z genau ein Kind hat, das sein rechtes Kind ist. • Wenn z genau ein Kind hat, das sein linkes Kind ist (Teil (b) der Abbildung), dann ersetzen wir z durch sein linkes Kind. • Andernfalls hat z sowohl ein linkes als auch ein rechtes Kind. Wir suchen z’s Nachfolger y, der in z’s rechtem Teilbaum liegt und selbst kein linkes Kind hat (siehe Übung 12.2-5). Wir wollen den Knoten y aus seiner aktuellen Position herausschneiden und ihn im Baum z ersetzen lassen.
12.3 Einfügen und Löschen
299
– Wenn y das rechte Kind von z ist (Teil (c)), dann ersetzen wir z so durch y, dass der linke Teilbaum von y (der nur aus nil besteht) entfernt wird und nur das rechte Kind von y erhalten bleibt. – Andernfalls liegt y inmitten dem rechten Teilbaum von z, ist aber nicht z’s rechtes Kind (Teil (d)). In diesem Fall ersetzen wir zuerst y durch sein eigenes rechtes Kind und ersetzen z durch y. Um Teilbäume im binären Suchbaum umhängen zu können, definieren wir eine Unterroutine Transplant, die einen Teilbaum als ein Kind seines Vaters durch einen anderen Teilbaum ersetzt. Wenn Transplant den Teilbaum mir Wurzel u durch einen Teilbaum mit Wurzel v ersetzt, dann wird u’s Vater der Vater von v und u’s Vater hat damit v als Kind. Transplant(T, u, v) 1 if u.vater = = nil 2 T.wurzel = v 3 elseif u = = u.vater .links 4 u.vater .links = v 5 else u.vater .rechts = v 6 if v = nil 7 v.vater = u.vater Die Zeilen 1–2 behandeln den Fall, in dem u die Wurzel von T ist. Andernfalls ist u entweder ein linkes oder ein rechtes Kind seines Vaters. Die Zeilen 3–4 sind verantwortlich, u.vater .links zu aktualisieren, wenn u ein linkes Kind ist, und Zeile 5 aktualisiert u.vater .rechts, wenn u ein rechtes Kind ist. Wir erlauben, dass v gleich nil ist und die Zeilen 6–7 aktualisieren v.vater , wenn v nicht nil ist. Bemerken Sie bitte, dass Transplant nicht versucht, v.links and v.rechts zu aktualisieren; es obliegt der Verantwortung der Prozedur, die Transplant aufruft, diese Aktualisierung durchzuführen oder dies nicht zu tun. Mit der Prozedur Transplant in Händen können wir nun die Prozedur angeben, die einen Knoten z aus dem binären Suchbaum T löscht: Tree-Delete(T, z) 1 if z.links = = nil 2 Transplant(T, z, z.rechts) 3 elseif z.rechts = = nil 4 Transplant(T, z, z.links) 5 else y = Tree-Minimum(z.rechts) 6 if y.vater = z 7 Transplant(T, y, y.rechts) 8 y.rechts = z.rechts 9 y.rechts.vater = y 10 Transplant(T, z, y) 11 y.links = z.links 12 y.links.vater = y
300
12 Binäre Suchbäume
q
q
(a)
z
r r
nil
q
q
(b)
z l
l
nil
q
q
(c)
z l
y y
l x
nil
q
q
(d)
z l
y
q z
r
nil
x
l
y nil
x
y r
l
r
x
x
Abbildung 12.4: Löschen eines Knotens z aus einem binären Suchbaum. Knoten z kann die Wurzel sein, das linke Kind eines Knotens q oder das rechte Kind eines Knotens q. (a) Knoten z hat kein linkes Kind. Wir ersetzen z durch sein rechtes Kind r, welches durchaus nil sein kann. (b) Knoten z hat ein linkes Kind l, aber kein rechtes Kind. Wir ersetzen z durch l. (c) Knoten z hat zwei Kinder; sein linkes Kind ist Knoten l, sein rechtes Kind ist sein Nachfolger y und y’s rechtes Kind ist Knoten x. Wir ersetzen z durch y, aktualisieren y’s linkes Kind, sodass dieses l wird, lassen aber x das rechte Kind von y bleiben. (d) Knoten z hat zwei Kinder (das linke Kind l und das rechte Kind r) und sein Nachfolger y = r liegt inmitten des Teilbaumes mit Wurzel r. Wir ersetzen y durch sein eigenes rechtes Kind x und machen y zu r’s Vater. Dann positionieren wir y so, dass er das Kind von q und der Vater von l ist.
12.3 Einfügen und Löschen
301
Die Prozedur Tree-Delete führt die vier Fälle wie folgt aus. Die Zeilen 1–2 behandeln den Fall, in dem z kein linkes Kind hat, und die Zeilen 3–4 den Fall, in dem z ein linkes, aber kein rechtes Kind hat. Die Zeilen 5–12 beschäftigen sich mit den zwei restlichen Fällen, in denen z zwei Kinder hat. Zeile 5 findet den Knoten y, der Nachfolger von z ist. Da z einen nichtleeren rechten Teilbaum besitzt, muss sein Nachfolger der Knoten mit dem kleinsten Schlüssel in diesem Teilbaum sein; aus diesem Grunde rufen wir Tree-Minimum(z.rechts) auf. Wie wir vorhin festgestellt haben, hat y kein linkes Kind. Wir wollen y aus seiner jetzigen Position rausschneiden, damit er im Baum z ersetzt. Wenn y das rechte Kind von z ist, dann ersetzen die Zeilen 10–12 z als ein Kind seines Vaters durch y und y’s linkes Kind durch z’s linkes Kind. Wenn y nicht das rechte Kind von z ist, dann ersetzen die Zeilen 7–9 y als Kind seines Vaters durch y’s rechtes Kind und ändern z’s rechtes Kind zu y’s rechtes Kind. Die Zeilen 10–12 ersetzen dann z als Kind seines Vaters durch y und ersetzen y’s linkes Kind durch z’s linkes Kind. Jede Zeile von Tree-Delete, inklusive der Aufrufe von Transplant, benötigt konstante Zeit, mit Ausnahme des Aufrufs von Tree-Minimum in Zeile 5. Somit läuft Tree-Delete auf einem Baum der Höhe h in Zeit O(h). Zusammengefasst haben wir das folgende Theorem bewiesen. Theorem 12.3 Wir können die Operationen Insert und Delete für dynamische Mengen so implementieren, dass sie auf einem binären Suchbaum der Höhe h in der Zeit O(h) laufen.
Übungen 12.3-1 Geben Sie eine rekursive Version der Prozedur Tree-Insert an. 12.3-2 Nehmen Sie an, wir hätten einen binären Suchbaum konstruiert, indem wir sukzessive verschiedene Werte in den Baum eingefügt haben. Zeigen Sie, dass die Anzahl der Knoten, die bei der Suche nach einem Wert im Baum besucht werden, um eins größer ist als die Anzahl der Knoten, die überprüft wurden, als der Wert in den Baum eingefügt wurde. 12.3-3 Wir können eine gegebene Menge von n Zahlen sortieren, indem wir zunächst einen binären Suchbaum konstruieren, der diese Zahlen enthält (d. h. wir wenden Tree-Insert wiederholt an, um die Zahlen eine nach der anderen einzufügen), und dann die Zahlen durch eine Inorder-Traversierung ausgeben. Wie ist die Laufzeit für diesen Sortieralgorithmus im besten und im schlechtesten Fall? 12.3-4 Ist die Lösche-Operation in dem Sinne „kommutativ“, dass das Löschen von x und dann von y aus einem binären Suchbaum zu dem gleichen Baum führt, als wenn man zuerst y und dann x löscht? Beweisen Sie, warum dies so ist, wenn es so ist, oder geben Sie ein Gegenbeispiel an.
302
12 Binäre Suchbäume
12.3-5 Nehmen Sie an, dass jeder Knoten x das Attribut x.nachf , das auf x’s Nachfolger zeigt, und nicht das Attribut x.vater , welches auf den Vater von x zeigt, speichert. Geben Sie den Pseudocode für die Prozeduren Search, Insert und Delete auf einem binären Suchbaum T an, die mit dieser Darstellung arbeiten. Diese Prozeduren sollten jeweils in Zeit O(h) arbeiten, wobei h die Höhe des Baumes T ist. (Hinweis: Sie wünschen sich eine Unterroutine, die den Vater eines Knotens zurückgibt.) 12.3-6 Wenn der Knoten z in Tree-Delete zwei Kinder hat, könnten wir für Knoten y seinen Vorgänger anstelle seines Nachfolgers nehmen. Welche weiteren Änderungen sind an der Prozedur Tree-Delete notwendig, wenn wir dies tun? Einige haben behauptet, dass eine faire Strategie, in der Vorgänger und Nachfolger mit der gleichen Priorität ausgewählt werden, zu einer besseren empirischen Performanz führt. Wie könnte Tree-Delete geändert werden, um eine solche faire Strategie zu implementieren?
∗ 12.4 Zufällig erzeugte binäre Suchbäume Wir haben gezeigt, dass jede der Grundoperationen auf einem binären Suchbaum der Höhe h in Zeit O(h) läuft. Allerdings variiert die Höhe eines binären Suchbaums, wenn Elemente eingefügt und gelöscht werden. Wenn zum Beispiel die n Elemente in streng aufsteigender Ordnung eingefügt werden, ist der Baum eine Kette der Höhe n − 1. Andererseits zeigt Übung B.5-4, dass h ≥ lg n gilt. Wie für Quicksort können wir zeigen, dass das Verhalten für den mittleren Fall viel näher am besten Fall als am schlechtesten Fall liegt. Leider ist nur wenig über die mittlere Höhe eines binären Suchbaums bekannt, wenn er sowohl durch Einfüge- als auch durch Löschoperationen erzeugt wird. Wenn der Baum nur durch Einfügen erzeugt wird, wird die Analyse leichter handhabbar. Wir wollen daher einen zufällig erzeugten binären Suchbaum mit n Schlüsseln als einen Suchbaum definieren, der durch das Einfügen der Schlüssel in zufälliger Reihenfolge in einen anfangs leeren Baum entsteht, wobei jede der n! Permutationen der Eingabe gleichwahrscheinlich ist. (In Übung 12.4-3 sollen Sie zeigen, dass diese Voraussetzung verschieden von der Voraussetzung ist, dass jeder binäre Suchbaum mit n Schlüsseln gleichwahrscheinlich ist.) In diesem Abschnitt werden wir folgendes Theorem beweisen: Theorem 12.4 Die erwartete Höhe eines zufälligen erzeugten binären Baumes aus n paarweise verschiedenen Schlüsseln liegt in O(lg n). Beweis: Wir definieren zunächst drei Zufallsvariablen, die für die Beschreibung der Höhe eines zufällig erzeugten binären Suchbaums hilfreich sind. Mit Xn bezeichnen wir die Höhe eines zufällig erzeugten binären Suchbaums mit n Schlüsseln. Die exponentielle Höhe ist durch Yn = 2Xn definiert. Wenn wir einen binären Suchbaum aus n Schlüsseln
12.4 ∗ Zufällig erzeugte binäre Suchbäume
303
konstruieren, wählen wir zunächst einen der Schlüssel für die Wurzel aus. Mit Rn bezeichnen wir die Zufallsvariable, die den Rang dieses Schlüssels innerhalb der Menge der n Schlüssel aufnimmt, d. h. Rn hält die Position, die der Schlüssel belegen würde, wenn die n Schlüssel geordnet wären. Rn nimmt jeden der Werte {1, 2, . . . , n} mit der gleichen Wahrscheinlichkeit an. Im Falle Rn = i ist der linke Teilbaum der Wurzel ein zufällig erzeugter binärer Suchbaum mit i − 1 Schlüsseln, und der rechte Teilbaum ist ein zufällig erzeugter binärer Suchbaum mit n − i Schlüsseln. Da die Höhe eines binären Suchbaums um eins größer ist als die Höhe des größeren der beiden Teilbäume der Wurzel, ist die exponentielle Höhe eines binären Suchbaums doppelt so groß wie die größere der exponentiellen Höhen der beiden Teilbäume der Wurzel. Wenn wir wissen, dass Rn = i gilt, folgt Yn = 2 · max(Yi−1 , Yn−i ) . Als Basisfall haben wir, dass Y1 = 1 gilt, da die exponentielle Höhe eines Baumes mit einem Knoten 20 = 1 ist. Der Einfachheit halber definieren wir Y0 = 0. Als nächstes definieren wir Indikatorfunktionen Zn,1 , Zn,2 , . . . , Zn,n durch Zn,i = I {Rn = i} . Da Rn jeden der Werte 1, 2, . . . , n mit gleicher Wahrscheinlichkeit annimmt, folgt, dass Pr {Rn = i} = 1/n für alle i = 1, 2, . . . , n gilt, und damit nach Lemma 5.1 E [Zn,i ] = 1/n
(12.1)
für i = 1, 2, . . . , n. Da genau ein Zn,i gleich 1 ist und alle anderen 0 sind, gilt außerdem
Yn =
n
Zn,i (2 · max(Yi−1 , Yn−i )) .
i=1
Wir werden zeigen, dass E [Yn ] polynomiell in n ist, woraus unmittelbar E [Xn ] = O(lg n) folgt. Wir behaupten, dass die Indikatorfunktion Zn,i = I {Rn = i} unabhängig von Yi−1 und Yn−i ist. Haben wir Rn = i gewählt, so wird der linke Teilbaum, dessen exponentielle Höhe Yi−1 ist, zufällig aus den i − 1 Schlüsseln mit den Rängen kleiner i erzeugt. Dieser Teilbaum ist genau wie jeder andere zufällig erzeugte binäre Suchbaum, der aus i − 1 Schlüsseln besteht. Im Unterschied zu der Anzahl der enthaltenen Schlüssel wird die Struktur dieses Teilbaums nicht von der Wahl von Rn = i beeinflusst. Somit sind die Zufallsvariablen Yi−1 und Zn,i voneinander unabhängig. Entsprechend wird der rechte Teilbaum, dessen exponentielle Höhe durch Yn−i gegeben ist, zufällig aus den n − i Schlüsseln mit den Rängen größer i erzeugt. Seine Struktur ist unabhängig von der Wahl von Rn , sodass die Zufallsvariablen Yn−i und Zn,i voneinander unabhängig sind.
304
12 Binäre Suchbäume
Folglich gilt
E [Yn ] = E
" n
# Zn,i (2 · max(Yi−1 , Yn−i ))
i=1
=
n
E [Zn,i (2 · max(Yi−1 , Yn−i ))]
(wegen der Linearität des Erwartungswertes)
E [Zn,i ] E [2 · max(Yi−1 , Yn−i )]
(wegen der Unabhängigkeit)
1 · E [2 · max(Yi−1 , Yn−i )] n
(nach Gleichung (12.1))
i=1
= =
n i=1 n i=1
2 E [max(Yi−1 , Yn−i )] n i=1 n
=
2 (E [Yi−1 ] + E [Yn−i ]) ≤ n i=1
(nach Gleichung(C.22))
n
(nach Übung C.3-4) .
Da jeder der Terme E [Y0 ] , E [Y1 ] , . . . , E [Yn−1 ] in der letzten Summe doppelt erscheint, einmal als E [Yi−1 ] und einmal als E [Yn−i ], erhalten wir die Rekursionsgleichung
E [Yn ] ≤
n−1 4 E [Yi ] . n i=0
(12.2)
Durch Anwendung der Substitutionsmethode werden wir zeigen, dass die Rekursionsgleichung (12.2) für alle positiven ganzen Zahlen n die Lösung
E [Yn ] ≤
1 n+3 4 3
hat. Dabei verwenden wir die Identität n−1 i=0
i+3 3
=
n+3 . 4
(12.3)
(In Übung 12.4-1 sollen Sie diese Gleichung beweisen.) Für die Basisfälle verifizieren wir, dass die Schranken 0 = Y0 = E [Y0 ] ≤ (1/4) 33 = 1/4
12.4 ∗ Zufällig erzeugte binäre Suchbäume
305
= 1 gelten. Für den induktiven Fall gilt und 1 = Y1 = E [Y1 ] ≤ (1/4) 1+3 3 E [Yn ] ≤ ≤ = = = = =
n−1 4 E [Yi ] n i=0 n−1 4 1 i+3 (wegen der Induktionsannahme) n i=0 4 3 n−1 1 i+3 3 n i=0 1 n+3 (nach Gleichung (12.3)) n 4 (n + 3)! 1 · n 4! (n − 1)! 1 (n + 3)! · 4 3! n! 1 n+3 . 4 3
Wir haben damit E [Yn ] abgeschätzt; aber unser eigentliches Ziel ist eine Abschätzung von E [Xn ]. In Übung 12.4-4 sollen Sie zeigen, dass die Funktion f (x) = 2x konvex ist (siehe Seite 1208). Daher können wir die Jensensche Ungleichung (C.26) anwenden, die besagt, dass 5 4 2E[Xn ] ≤ E 2Xn = E [Yn ] gilt. Hieraus leiten wir 1 n+3 4 3 1 (n + 3)(n + 2)(n + 1) · = 4 6 n3 + 6n2 + 11n + 6 . = 24
2E[Xn ] ≤
ab. Bilden wir auf beiden Seiten den Logarithmus, ergibt sich E [Xn ] = O(lg n).
Übungen 12.4-1 Beweisen Sie Gleichung (12.3).
306
12 Binäre Suchbäume
12.4-2 Beschreiben Sie einen binären Suchbaum mit n Knoten, der so beschaffen ist, dass die mittlere Tiefe eines Knotens des Baumes Θ(lg n) ist, die Höhe des Baumes jedoch ω(lg n). Geben Sie eine asymptotisch obere Schranke für die Höhe eines binären Suchbaums mit n Knoten an, in dem die mittlere Tiefe eines Knotens Θ(lg n) ist. 12.4-3 Zeigen Sie, dass der Begriff des zufällig ausgewählten binären Suchbaums mit n Schlüsseln (wobei jeder dieser Bäume mit gleicher Wahrscheinlichkeit gewählt wird) sich von dem Begriff eines zufällig erzeugten binären Suchbaums, wie er in diesem Abschnitt definiert wurde, unterscheidet. (Hinweis: Listen Sie die Möglichkeiten für n = 3 auf.) 12.4-4 Zeigen Sie, dass die Funktion f (x) = 2x konvex ist. 12.4-5∗ Betrachten Sie die Arbeitsweise der Prozedur Randomized-Quicksort auf einer Folge von n paarweise verschiedenen Eingabewerten. Beweisen Sie, dass für jede Konstante k > 0 jede, bis auf O(1/nk ) viele, der n! möglichen Eingabefolgen zu einer Laufzeit von O(n lg n) führt.
Problemstellungen 12-1 Binäre Suchbäume mit gleichen Schlüsseln Gleiche Schlüssel stellen für die Implementierung eines binären Suchbaums ein Problem dar. a. Wie ist die asymptotische Laufzeit von Tree-Insert, wenn die Prozedur verwendet wird, um n Elemente mit identischen Schlüsseln in einen anfangs leeren binären Suchbaum einzufügen? Wir schlagen vor, Tree-Insert dadurch zu verbessern, dass wir vor Zeile 5 testen, ob z.schl¨u ssel = x.schl¨u ssel gilt, und vor Zeile 11, ob z.schl¨u ssel = y.schl¨u ssel gilt. Bei Gleichheit implementieren wir eine der folgenden Strategien. Bestimmen Sie für jede der Strategien die asymptotische Laufzeit für das Einfügen von n Elementen mit identischen Schlüsseln in einen anfangs leeren binären Suchbaum. (Die Strategien werden für Zeile 5 beschrieben, wo die Schlüssel z und x verglichen werden. Um die Strategien für Zeile 11 zu erhalten, setzen Sie y statt x ein.) b. Führen Sie eine Boolesche Marke x.b am Knoten x und setzen Sie x in Abhängigkeit der Belegung von x.b entweder auf x.links oder auf x.rechts. Die Belegung der Marke x.b wechselt jedes Mal zwischen den Werten falsch und wahr, wenn der Knoten x während des Einfügens eines Knotens mit dem gleichen Schlüssel wie x besucht wird. c. Verwalten Sie eine Liste der Knoten mit gleichen Schlüsseln an x, und fügen Sie z in die Liste ein. d. Setzen Sie x zufällig entweder auf x.links oder x.rechts. (Geben Sie die Laufzeit im schlechtesten Fall an, und leiten Sie informal die erwartete Laufzeit ab.)
Problemstellungen zu Kapitel 12
0
307
1
0 1
0 10 1 011
0 100
1 1 1011
Abbildung 12.5: Ein Radix-Baum, der die Bitketten 1011, 10, 011, 100 und 0 speichert. Wir können den Schlüssel eines jeden Knotens bestimmen, indem wir den einfachen Pfad von der Wurzel zu diesem Knoten verfolgen. Es gibt daher keine Notwendigkeit, die Schlüssel in den Knoten zu speichern; die Schlüssel werden in der Abbildung nur aus Gründen der Anschaulichkeit gezeigt. Ein Knoten ist stark schattiert, wenn der zugehörige Schlüssel nicht im Baum ist. Solche Knoten sind in dem Baum enthalten, nur um einen Pfad zu den anderen Knoten herstellen zu können.
12-2 Radix-Bäume Seien a = a0 a1 . . . ap und b = b0 b1 . . . bq zwei Zeichenketten, wobei die ai und die bj Elemente einer geordneten Menge von Zeichen sind. Wir sagen, die Zeichenkette a sei lexikographisch kleiner als die Zeichenkette b, falls 1. entweder eine ganze Zahl j mit 0 ≤ j ≤ min(p, q) existiert, sodass ai = bi für alle i = 0, 1, . . . , j − 1 und aj < bj gilt, oder 2. p < q und ai = bi für alle i = 0, 1, . . . , p gilt. Wenn a und b beispielsweise Bitketten sind, dann gilt nach der ersten Regel (mit j = 3) 10100 < 10110 und nach der zweiten Regel 10100 < 101000. Diese Reihenfolge ähnelt der in einem normalen Wörterbuch. Die in Abbildung 12.5 gezeigte Datenstruktur eines Radix-Baumes speichert die Bitketten 1011, 10, 011, 100 und 0. Um nach einem Schlüssel a = a0 a1 . . . ap zu suchen, gehen wir bei einem Knoten der Tiefe i nach links, falls ai = 0 gilt, und bei ai = 1 nach rechts. Es sei S nun eine Menge paarweise verschiedener Binärketten, deren Längen sich zu n addieren. Zeigen Sie, wie ein Radix-Baum verwendet werden kann, um S in Zeit Θ(n) lexikographisch zu sortieren. Im Beispiel aus Abbildung 12.5 sollte die Ausgabefolge 0, 011, 10, 100, 1011 sein. 12-3 Mittlere Knotentiefe in einem zufällig erzeugten binären Suchbaum In dieser Problemstellung beweisen wir, dass die mittlere Tiefe eines Knotens in einem zufällig erzeugten binären Suchbaum mit n Knoten O(lg n) ist. Zwar ist dieses Ergebnis schwächer als das von Theorem 12.4, jedoch offenbart die Methode, die wir verwenden werden, eine überraschende Ähnlichkeit zwischen dem Erzeugen eines binären Suchbaums und dem Ausführen der Prozedur RandomizedQuicksort aus Abschnitt 7.3.
308
12 Binäre Suchbäume Wir definieren die Gesamtpfadlänge P (T ) eines binären Baumes T als die Summe der Tiefen der Knoten über alle Knoten x von T und bezeichnen die Tiefe des Knoten x durch d(x, T ). a. Zeigen Sie, dass die mittlere Tiefe eines Knotens aus T durch 1 1 d(x, T ) = P (T ) . n n x∈T
gegeben ist. Wir wollen zeigen, dass der Erwartungswert von P (T ) O(n lg n) ist. b. Es seien TL und TR der linke bzw. der rechte Teilbaum von T . Zeigen Sie, dass für einen Baum mit n Knoten P (T ) = P (TL ) + P (TR ) + n − 1 gilt. c. Es sei P (n) die mittlere Gesamtpfadlänge eines zufällig erzeugten binären Suchbaums mit n Knoten. Zeigen Sie P (n) =
n−1 1 (P (i) + P (n − i − 1) + n − 1) . n i=0
d. Zeigen Sie, wie P (n) zu n−1 2 P (k) + Θ(n) P (n) = n k=1
umgeformt werden kann. e. Rufen Sie sich die alternative Analyse der randomisierten Version von Quicksort in Erinnerung, die in Problemstellung 7-3 gegeben worden ist, und folgern Sie, dass P (n) = O(n lg n) gilt. Bei jedem rekursiven Aufruf von Quicksort wählen wir ein zufälliges Pivotelement, bezüglich dem die zu sortierende Menge partitioniert wird. Jeder Knoten eines binären Suchbaums zerlegt die Menge der Elemente in einen Teil, der zum linken Teilbaum dieses Knotens gehört, und einen Teil, der zum rechten Teilbaum des Knotens gehört. f. Geben Sie eine Implementierung von Quicksort an, in der die Vergleiche zum Sortieren einer Menge genau die gleichen wie beim Einfügen eines Elementes in einen binären Suchbaum sind. (Die Reihenfolge, in der die Vergleiche ausgeführt werden, darf unterschiedlich sein; es müssen aber die gleichen Vergleiche sein.)
Problemstellungen zu Kapitel 12
309
12-4 Anzahl verschiedener binärer Bäume Sei bn die Anzahl der verschiedenen binären Bäume mit n Knoten. In dieser Problemstellung sollen Sie eine Formel für bn sowie eine asymptotische Abschätzung für bn berechnen. a. Zeigen Sie, dass b0 = 1 gilt und dass für n ≥ 1 bn =
n−1
bk bn−1−k
k=0
gilt. b. Gemäß der Definition einer erzeugenden Funktion in Problemstellung 4-4 sei B(x) die erzeugende Funktion B(x) =
∞
bn xn .
n=0
Beweisen Sie B(x) = x B(x)2 + 1 und damit die Möglichkeit, B(x) in geschlossener Form durch √ 1 1 − 1 − 4x B(x) = 2x auszudrücken. Die Taylor-Entwicklung von f (x) im Punkt x = a ist durch f (x) =
∞ f (k) (a) k=0
gegeben, wobei f
(k)
k!
(x − a)k
(x) die k-te Ableitung von f im Punkt x ist.
c. Zeigen Sie, dass
2n 1 bn = n+1 n
(dies ist die n-te Catalan-Zahl ) gilt, indem Sie die Taylor-Entwicklung von √ 1 − 4x im Punkt x = 0 anwenden. (Wenn Sie wollen, können Sie anstatt der Taylor-Entwicklung die Verallgemeinerung der Binomialentwicklung (C.4) auf nicht-ganzzahlige Exponenten n anwenden. Dabei interpretieren wir den Ausdruck nk für jede reelle Zahl n und jede ganze Zahl k als n(n − 1) · · · (n − k + 1)/k!, falls k ≥ 0 gilt; ansonsten interpretieren wir den Ausdruck als 0.) d. Zeigen Sie, dass 4n bn = √ 3/2 (1 + O(1/n)) πn gilt.
310
12 Binäre Suchbäume
Kapitelbemerkungen Das Buch von Knuth [211] enthält eine gute Diskussion zu einfachen binären Suchbäumen sowie zu vielen Varianten von Suchbäumen. Binäre Suchbäume sind vermutlich in den späten 1950er Jahren unabhängig voneinander von einer Reihe von Wissenschaftlern eingeführt worden. Radix-Bäume werden häufig als Tries bezeichnet. Die Bezeichnung kommt von den drei mittleren Buchstaben des englischen Wortes „retrieval“ (deutsch: Abfrage). Knuth [211] diskutiert diese ebenfalls. Viele Bücher, inklusive den ersten zwei Auflagen dieses Buches, beschreiben eine etwas einfachere Methode zum Löschen eines Knotens aus einem binären Suchbaum. Anstatt den Knoten z durch seinen Nachfolger y zu ersetzen, löschen wir den Knoten y und kopieren seine Schlüssel und seine Satellitendaten in den Knoten z. Der Nachteil dieses Ansatzes besteht darin, dass der Knoten, der letztendlich gelöscht wird, möglicherweise nicht der Knoten ist, der der Prozedur als Eingabeparameter übergeben worden ist. Wenn andere Programmteile Zeiger auf Knoten in den Baum besitzen, könnten sie irrtümlicherweise mit „alten“ Zeigern dastehen, die auf Knoten zeigen, die gelöscht worden sind. Wenngleich die in dieser Auflage dieses Buches vorgestellte Methode zum Löschen ein bisschen schwieriger ist, garantiert sie, dass ein Aufruf zum Löschen von Knoten z den Knoten z und nur diesen löscht. Abschnitt 15.5 wird zeigen, wie ein optimaler binärer Suchbaum erzeugt werden kann, wenn wir die Suchhäufigkeiten kennen, bevor wir den Baum konstruieren: Wenn wir für jeden Schlüssel wissen, wie häufig nach diesem gesucht wird, und wir wissen, wie häufig wir nach Werten suchen, die zwischen die Schlüssel des Baumes fallen, dann konstruieren wir einen binären Suchbaum, mit dem eine Menge von Suchoperationen, die dieser Wahrscheinlichkeitsverteilung genügt, minimal viele Knoten besucht. Der Beweis für die Abschätzung der erwarteten Höhe eines zufällig erzeugten binären Suchbaums in Abschnitt 12.4 geht auf Aslam [24] zurück. Martínez und Roura [243] geben einen randomisierten Algorithmus für das Einfügen in binäre Suchbäume und das Löschen aus binären Suchbäumen an, bei dem das Ergebnis jeder dieser Operationen ein zufälliger binärer Suchbaum ist. Ihre Definition eines zufälligen binären Suchbaums unterscheidet sich allerdings – wenn auch nur wenig – von der in diesem Kapitel gegebenen Definition eines zufällig erzeugten binären Suchbaums.
13
Rot-Schwarz-Bäume
Kapitel 12 hat gezeigt, dass jede der Grundoperationen für dynamische Mengen, wie Search, Predecessor, Successor, Minimum, Maximum, Insert und Delete mit einem binären Suchbaum in Zeit O(h) realisiert werden kann. Die Operationen sind also schnell, wenn die Höhe des Baumes klein ist. Ist seine Höhe groß, so kann es jedoch sein, dass die Operationen nicht schneller laufen als mit einer verketteten Liste. RotSchwarz-Bäume sind eine von vielen Suchbaum-Varianten, die „balanciert“ sind. Damit wird sichergestellt, dass die Grundoperationen für dynamische Mengen im schlechtesten Fall Zeit O(lg n) benötigen.
13.1
Eigenschaften von Rot-Schwarz-Bäumen
Ein Rot-Schwarz-Baum ist ein binärer Suchbaum, der pro Knoten ein zusätzliches Bit Speicherplatz vorhält. Dieses Bit dient der Speicherung seiner Farbe, die entweder rot oder schwarz sein kann. Indem die erlaubten Farbreihenfolgen auf einem Pfad von der Wurzel zu einem Blatt eingeschränkt werden, wird sichergestellt, dass kein solcher Pfad mehr als doppelt so lang wie irgend ein anderer solcher Pfad des Baumes sein kann. Dadurch ist der Baum fast balanciert. Jeder Knoten des Baumes enthält die Attribute farbe, schl¨u ssel , links, rechts und vater. Wenn ein Kind oder der Vater eines Knotens nicht existiert, dann enthält das entsprechende Zeigerattribut des Knotens den Wert nil. Wir werden diese nil-Werte als Zeiger auf Blätter (externe Knoten) des binären Suchbaums und die normalen, mit einem Schlüssel behafteten Knoten als innere Knoten des Baumes ansehen. Ein Rot-Schwarz-Baum ist ein binärer Suchbaum, der die folgenden Eigenschaften, die als Rot-Schwarz-Eigenschaften bezeichnet werden, erfüllt: 1. Jeder Knoten ist entweder rot oder schwarz. 2. Die Wurzel ist schwarz. 3. Jedes Blatt (nil) ist schwarz. 4. Wenn ein Knoten rot ist, dann sind seine beiden Kinder schwarz. 5. Für jeden Knoten enthalten alle einfachen Pfade, die an diesem Knoten starten und in einem Blatt des Teilbaumes dieses Knotens enden, die gleiche Anzahl schwarzer Knoten.
312
13 Rot-Schwarz-Bäume 3 3 2 2 1
1
7
3
NIL
1 NIL
12
1
NIL
21
2 1
NIL
41
17
14
10
16
15
NIL
26
1
NIL
19
NIL
NIL
2 1
1
20
NIL
23
NIL
1
NIL
30
1
28
NIL
NIL
NIL
1
1
38
35
1
NIL
NIL
2
NIL
47
NIL
NIL
39
NIL
NIL
(a)
26 17
41
14
21
10 7
16 12
19
15
30 23
47
28
38
20
35
39
3
T.nil (b) 26 17
41
14
21
10 7 3
16 12
15
19
30 23
47
28
20
38 35
39
(c)
Abbildung 13.1: Ein Rot-Schwarz-Baum. Die dunklen Kreise entsprechen den schwarzen Knoten und die schattierten Kreise den roten Knoten. Jeder Knoten eines Rot-SchwarzBaumes ist entweder rot oder schwarz, die Kinder eines roten Knotens sind beide schwarz, und jeder einfache Pfad von einem Knoten zu einem Blatt, das ein Nachfolger von diesem Knoten ist, enthält die gleiche Anzahl von schwarzen Knoten. (a) Jedes Blatt, dargestellt als nil, ist schwarz. Jeder Nicht-nil-Knoten ist mit seiner Schwarz-Höhe gekennzeichnet; nil’s haben die Schwarz-Höhe 0. (b) Der gleiche Rot-Schwarz-Baum; jedoch sind hier alle nil’s durch einen einzigen Wächter T. nil ersetzt, der immer schwarz ist; die Schwarz-Höhen sind nicht eingezeichnet. Der Wächter ist auch der Vater der Wurzel. (c) Der gleiche Rot-SchwarzBaum, jedoch sind die Blätter und der Vater der Wurzel nicht eingezeichnet. Wir werden diese Form der Darstellung im Folgenden benutzen.
13.1 Eigenschaften von Rot-Schwarz-Bäumen
313
Abbildung 13.1(a) zeigt ein Beispiel für einen Rot-Schwarz-Baum. Aus Gründen der Bequemlichkeit verwenden wir bei der Behandlung von Randbedingungen in Rot-Schwarz-Bäumen einen einzelnen Wächter, um nil darzustellen (siehe Seite 239). Für einen Rot-Schwarz-Baum ist der Wächter T.nil ein Objekt mit den gleichen Attributen wie ein normaler Knoten des Baumes. Sein Farbattribut ist schwarz, und die anderen Attribute – vater, links, rechts und schl¨u ssel – können auf beliebige Werte gesetzt werden. Wie Abbildung 13.1(b) zeigt, werden alle Zeiger auf nil durch Zeiger auf den Wächter T.nil ersetzt. Wir verwenden den Wächter, damit wir ein nil-Kind eines Knotens x wie einen gewöhnlichen Knoten mit dem Vater x behandeln können. Wenngleich wir stattdessen für jedes nil des Baumes einen eigenen Wächterknoten hinzufügen könnten, sodass der Vater für jedes nil wohldefiniert wäre, würde dieser Ansatz viel Speicherplatz verschwenden. Stattdessen verwenden wir nur den einen Wächter T.nil für alle nil’s, d. h. für alle Blätter und den Vater der Wurzel. Die Belegung der Attribute vater, links, rechts und schl¨u ssel des Wächters sind a priori nicht von Bedeutung; wir können sie während einer Prozedur nach Belieben benutzen. Im Allgemeinen beschränken wir unser Interesse auf die inneren Knoten eines RotSchwarz-Baumes, da sie die Werte der Schlüssel enthalten. Wir werden im Folgenden die Blätter, wie in Abbildung 13.1(c) gezeigt, nicht einzeichnen, wenn wir Rot-SchwarzBäume grafisch darstellen. Wir bezeichnen die Anzahl der schwarzen Knoten auf einem, von der Wurzel aus gesehen, nach unten verlaufenden einfachen Pfad vom Knoten x zu einem Blatt als dessen Schwarz-Höhe bh(x); der Knoten x selbst wird hierbei nicht mitgezählt. Nach Eigenschaft 5 ist der Begriff der Schwarz-Höhe wohldefiniert, da alle von einem Knoten ausgehenden abwärts laufenden einfachen Pfade die gleiche Anzahl schwarzer Knoten haben. Die Schwarz-Höhe eines Rot-Schwarz-Baumes definieren wir als die SchwarzHöhe seiner Wurzel. Das folgende Lemma zeigt, weshalb Rot-Schwarz-Bäume gute Suchbäume sind. Lemma 13.1 Ein Rot-Schwarz-Baum mit n inneren Knoten hat höchstens die Höhe 2 lg(n + 1). Beweis: Wir zeigen zunächst, dass der Teilbaum zu einem beliebigen Knoten x mindestens 2bh(x) − 1 innere Knoten hat. Wir beweisen diese Behauptung durch Induktion über die Höhe von x. Falls die Höhe von x gleich 0 ist, dann muss x ein Blatt sein (T.nil ), und der Teilbaum von x enthält tatsächlich mindestens 2bh(x) −1 = 20 −1 = 0 innere Knoten. Für den Induktionsschritt betrachten wir einen Knoten x, der eine positive Höhe hat und ein innerer Knoten mit zwei Kindern ist. Jedes Kind hat entweder die Schwarz-Höhe bh(x) oder bh(x) − 1, in Abhängigkeit davon, ob seine Farbe rot oder schwarz ist. Da die Höhe eines Kindes von x kleiner als die Höhe von x selbst ist, können wir aus der Induktionsannahme schließen, dass jedes Kind mindestens 2bh(x)−1 −1 innere Knoten hat. Also enthält der Teilbaum von x mindestens (2bh(x)−1 −1)+(2bh(x)−1 −1)+1 = 2bh(x) −1 innere Knoten, womit unsere Behauptung bewiesen ist.
314
13 Rot-Schwarz-Bäume
Um den Beweis des Lemmas zu vervollständigen, betrachten wir die Höhe h des Baumes. Nach Eigenschaft 4 muss mindestens die Hälfte der Knoten auf jedem einfachen Pfad von der Wurzel zu einem Blatt (ohne die Wurzel mitzuzählen) schwarz sein. Folglich ist die Schwarz-Höhe der Wurzel mindestens h/2; es gilt also n ≥ 2h/2 − 1 . Bringen wir in dieser Ungleichung die 1 auf die linke Seite und bilden wir auf beiden Seiten den Logarithmus, dann erhalten wir lg(n + 1) ≥ h/2 bzw. h ≤ 2 lg(n + 1). Als unmittelbare Folgerung dieses Lemmas können wir die Operationen Search, Minimum, Maximum, Successor und Predecessor für dynamische Mengen auf RotSchwarz-Bäumen in Zeit O(lg n) implementieren, da jede von ihnen auf einem Suchbaum der Höhe h in Zeit O(h) läuft (was in Kapitel 12 gezeigt wurde) und jeder Rot-SchwarzBaum mit n Knoten ein Suchbaum der Höhe O(lg n) ist. (Natürlich müssen Verweise auf nil in den Algorithmen des Kapitels 12 durch T.nil ersetzt werden.) Zwar laufen die Algorithmen Tree-Insert und Tree-Delete aus Kapitel 12 in Zeit O(lg n), wenn die Eingabe ein Rot-Schwarz-Baum ist, sie unterstützen jedoch die Operationen Insert und Delete für dynamische Mengen nicht direkt, da sie nicht sicherstellen, dass der durch sie erzeugte binäre Suchbaum ein Rot-Schwarz-Baum ist. Wir werden in den Abschnitten 13.3 und 13.4 sehen, wie diese beiden Operationen dennoch in Zeit O(lg n) unterstützt werden können.
Übungen 13.1-1 Zeichnen Sie entsprechend Abbildung 13.1(a) den vollständigen binären Suchbaum der Höhe 3 auf der Schlüsselmenge {1, 2, . . . , 15}. Fügen Sie die nilBlätter hinzu und färben Sie die Knoten auf drei unterschiedliche Weisen, sodass die Schwarz-Höhen der resultierenden Rot-Schwarz-Bäume 2, 3 bzw. 4 sind. 13.1-2 Zeichnen Sie den Rot-Schwarz-Baum, der sich ergibt, nachdem Tree-Insert für den Baum aus Abbildung 13.1 mit dem Schlüssel 36 aufgerufen wurde. Ist der resultierende Baum ein Rot-Schwarz-Baum, wenn der eingefügte Knoten rot ist? Wie verhält es sich im Falle, dass der Knoten schwarz ist? 13.1-3 Wir definieren einen relaxierten Rot-Schwarz-Baum als einen binären Suchbaum, der die Rot-Schwarz-Eigenschaften 1, 3, 4 und 5 erfüllt. Mit anderen Worten, die Wurzel darf entweder rot oder schwarz sein. Betrachten Sie einen relaxierten Rot-Schwarz-Baum T , dessen Wurzel rot ist. Ist der resultierende Baum ein Rot-Schwarz-Baum, wenn wir die Wurzel von T schwarz färben, aber sonst keine Änderungen an T vornehmen? 13.1-4 Nehmen Sie an, wir würden jeden roten Knoten eines Rot-Schwarz-Baumes in seinem schwarzen Vater „absorbieren“, sodass die Kinder des roten Knoten Kinder von dem schwarzen Vater werden. (Ignorieren Sie, was mit den Schlüsseln passiert.) Was sind die möglichen Grade eines schwarzen Knotens,
13.2 Rotationen
315 LEFT-ROTATE(T, x)
y
γ
x
α
x
β
RIGHT-ROTATE(T, y)
α
y
β
γ
Abbildung 13.2: Die Rotationsoperationen auf einem binären Suchbaum. Die Operation Left-Rotate(T, x) transformiert die Konfiguration der beiden Knoten auf der rechten Seite in die Konfiguration auf der linken Seite, indem sie eine konstante Anzahl von Zeigern ändert. Die inverse Operation Right-Rotate(T, y) transformiert die Konfiguration auf der linken Seite in die Konfiguration auf der rechten Seite. Die Buchstaben α, β und γ stellen beliebige Teilbäume dar. Eine Rotation erhält die binäre-Suchbaum-Eigenschaft: Die Schlüssel in α sind kleiner als x. schl u ¨ ssel , der kleiner als die Schlüssel in β ist. Die Schlüssel aus β sind kleiner als y. schl u ¨ ssel , der kleiner als die in γ enthaltenen Schlüssel ist.
nachdem alle seine roten Kinder absorbiert wurden? Was können Sie über die Tiefen der Blätter des resultierenden Baumes aussagen? 13.1-5 Zeigen Sie, dass der längste einfache Pfad von einem Knoten x eines RotSchwarz-Baumes zu einem in seinem Teilbaum enthaltenen Blatt höchstens doppelt so lang als der kürzeste einfache Pfad von Knoten x zu einem in seinem Teilbaum enthaltenen Blatt ist. 13.1-6 Wie groß ist die maximale Anzahl innerer Knoten in einem Rot-Schwarz-Baum mit der Schwarz-Höhe h? Wie groß ist die minimale Anzahl? 13.1-7 Geben Sie einen Rot-Schwarz-Baum mit n Knoten an, der das größtmögliche Verhältnis von roten zu schwarzen inneren Knoten realisiert. Wie groß ist dieses Verhältnis? Welcher Baum realisiert das kleinstmögliche Verhältnis und wie groß ist dieses?
13.2
Rotationen
Die Suchbaum-Operationen Tree-Insert und Tree-Delete benötigen auf einem RotSchwarz-Baum mit n Schlüsseln die Laufzeit O(lg n). Da sie den Baum modifizieren, kann das Ergebnis die in Abschnitt 13.1 aufgezählten Rot-Schwarz-Eigenschaften verletzen. Um diese Eigenschaften wiederherzustellen, müssen wir die Farben einiger Knoten des Baumes sowie die Zeigerstruktur ändern. Die Zeigerstruktur verändern wir durch Rotation. Dies ist eine lokale Operation in einem Suchbaum, die die binäre-Suchbaum-Eigenschaft erhält. Abbildung 13.2 zeigt die beiden Arten von Rotationen: Linksrotationen und Rechtsrotationen. Wenn wir eine Linksrotation auf einem Knoten x ausführen, setzen wir voraus, dass sein rechtes Kind y nicht T.nil ist; x darf ein beliebiger Knoten im Baum sein, dessen rechtes Kind nicht T.nil ist. Die Linksrotation „dreht“ um die Verbindung zwischen x und y herum. Sie
316
13 Rot-Schwarz-Bäume 7 4 3
11 x 6
9
18 y
2
14 12
LEFT-ROTATE(T, x)
19 17
22 20
7 4 3
18 y 6
2
x 11 9
19 14
12
22 17
20
Abbildung 13.3: Ein Beispiel dafür, wie die Prozedur Left-Rotate(T, x) einen binären Suchbaum modifiziert. Inorder-Traversierungen im Eingabebaum und im modifizierten Baum erzeugen die gleiche Folge von Schlüsselwerten.
macht aus y die neue Wurzel des Teilbaums, wobei x das linke Kind von y wird und das (vor der Rotation) linke Kind von y das rechte Kind von x. Der Pseudocode für die Linksrotation Left-Rotate setzt voraus, dass x.rechts = T.nil gilt und dass der Vater der Wurzel T.nil ist. Left-Rotate(T, x) 1 y = x.rechts 2 x.rechts = y.links 3 if y.links = T.nil 4 y.links.vater = x 5 y.vater = x.vater 6 if x.vater = = T.nil 7 T.wurzel = y 8 elseif x = = x.vater .links 9 x.vater .links = y 10 else x.vater .rechts = y 11 y.links = x 12 x.vater = y
// bestimme y // mache y’s linker Teilbaum zu x’s rechtem
// setze y’s Vater auf x’s Vater
// mache x zu y’s linkem Kind
Abbildung 13.3 zeigt ein Beispiel, wie Left-Rotate einen binären Baum modifiziert. Der Code für Right-Rotate ist in analoger Weise aufgebaut. Sowohl Left-Rotate
13.3 Einfügen eines Knotens
317
als auch Right-Rotate laufen in Zeit O(1). Es werden nur Zeiger bei einer Rotation verändert, alle anderen Attribute eines Knotens bleiben unverändert.
Übungen 13.2-1 Schreiben Sie die Prozedur Right-Rotate in Pseudocode. 13.2-2 Zeigen Sie, dass es in jedem binären Suchbaum mit n Knoten genau n − 1 mögliche Rotationen gibt. 13.2-3 Seien a, b und c beliebige Knoten in den Teilbäumen α, β bzw. γ des rechten Baumes aus Abbildung 13.2. Wie ändern sich die Tiefen von a, b und c, wenn eine Linksrotation auf dem Knoten x ausgeführt wird? 13.2-4 Zeigen Sie, dass jeder beliebige binäre Suchbaum mit n Knoten durch O(n) Rotationen in jeden anderen binären Suchbaum mit n Knoten überführt werden kann. (Hinweis: Zeigen Sie zunächst, dass n − 1 Rechtsrotationen ausreichen, um den Baum in eine rechtsläufige Kette zu überführen.) 13.2-5∗ Wir sagen, dass ein binärer Suchbaum T1 in einen binären Suchbaum T2 rechts-konvertiert werden kann, wenn es möglich ist, T2 aus T1 durch eine Folge von Aufrufen von Right-Rotate zu erzeugen. Geben Sie ein Beispiel für zwei binäre Suchbäume T1 und T2 an, sodass T1 nicht in T2 rechts-konvertiert werden kann. Zeigen Sie dann, dass, falls ein binärer Suchbaum T1 in T2 rechtskonvertiert werden kann, dies mit O(n2 ) Aufrufen von Right-Rotate möglich ist.
13.3
Einfügen eines Knotens
Wir können einen Knoten in einen aus n Knoten bestehenden Rot-Schwarz-Baum in Zeit O(lg n) einfügen. Hierzu verwenden wir eine leicht modifizierte Version der Prozedur Tree-Insert (siehe Abschnitt 12.3), um den Knoten z in den Baum T einzufügen, so als wäre er ein gewöhnlicher Suchbaum. Anschließend färben wir den Knoten z rot. (Übung 13.3-1 verlangt, dass Sie erklären, warum wir den Knoten z rot und nicht schwarz färben.) Um sicherzustellen, dass die Rot-Schwarz-Eigenschaften erhalten bleiben, rufen wir dann eine Hilfsprozedur RB-Insert-Fixup auf, um die Knoten umzufärben und Rotationen auszuführen. Der Aufruf RB-Insert(T, z) fügt den Knoten z in den RotSchwarz-Baum ein, davon ausgehend, dass das Schlüsselattribut von z bereits gesetzt ist.
318
13 Rot-Schwarz-Bäume
RB-Insert(T, z) 1 y = T.nil 2 x = T.wurzel 3 while x = T.nil 4 y =x 5 if z.schl¨u ssel < x.schl¨u ssel 6 x = x.links 7 else x = x.rechts 8 z.vater = y 9 if y = = T.nil 10 T.wurzel = z 11 elseif z.schl¨u ssel < y.schl¨u ssel 12 y.links = z 13 else y.rechts = z 14 z.links = T.nil 15 z.rechts = T.nil 16 z.farbe = rot 17 RB-Insert-Fixup(T, z) Die Prozeduren Tree-Insert und RB-Insert unterscheiden sich in vier Punkten. Erstens werden alle Instanzen von nil in Tree-Insert durch T.nil ersetzt. Zweitens setzen wir z.links und z.rechts in den Zeilen 14–15 von RB-Insert auf T.nil , um die korrekte Baumstruktur aufrechtzuerhalten. Drittens färben wir z in Zeile 16 rot. Da das Rotfärben von z eine Verletzung einer der Rot-Schwarz-Eigenschaften verursachen kann, rufen wir viertens in Zeile 17 von RB-Insert die Prozedur RB-Insert-Fixup(T, z) auf, um die Rot-Schwarz-Eigenschaften wiederherzustellen. RB-Insert-Fixup(T, z) 1 while z.vater .farbe == rot 2 if z.vater = = z.vater .vater .links 3 y = z.vater .vater .rechts 4 if y.farbe = = rot 5 z.vater .farbe = schwarz 6 y.farbe = schwarz 7 z.vater .vater .farbe = rot 8 z = z.vater .vater 9 else if z = = z.vater .rechts 10 z = z.vater 11 Left-Rotate(T, z) 12 z.vater .farbe = schwarz 13 z.vater .vater .farbe = rot 14 Right-Rotate(T, z.vater .vater ) 15 else (analog zum then-Fall, nur “rechts” und “links” vertauschen) 16 T.wurzel .farbe = schwarz
// // // //
Fall Fall Fall Fall
1 1 1 1
// // // // //
Fall Fall Fall Fall Fall
2 2 3 3 3
13.3 Einfügen eines Knotens
319
Um zu verstehen, wie RB-Insert-Fixup arbeitet, unterteilen wir unsere Analyse des Codes in drei Schritte. Zuerst werden wir bestimmen, welche der Rot-Schwarz-Eigenschaften in RB-Insert verletzt werden können, wenn Knoten z eingefügt und rot gefärbt wird. Anschließend überlegen wir uns, was die while-Schleife in den Zeilen 1–15 zu erreichen versucht. Zuletzt werden wir jeden der drei in der while-Schleife behandelten Fälle1 näher betrachten und sehen, wie sie das Ziel erfüllen. Abbildung 13.4 zeigt an einem Beispiel eines Rot-Schwarz-Baumes, wie RB-Insert-Fixup arbeitet. Welche der Rot-Schwarz-Eigenschaften können überhaupt durch den Aufruf von RBInsert-Fixup verletzt werden? Eigenschaft 1 gilt mit Sicherheit weiterhin, ebenso die Eigenschaft 3, denn beide Kinder des neu eingefügten roten Knotens sind der Wächter T.nil. Eigenschaft 5, die besagt, dass die Anzahl der schwarzen Knoten auf jedem einfachen Pfad von einem gegebenen Knoten aus gleich ist, ist ebenfalls erfüllt, da der Knoten z den (schwarzen) Wächter ersetzt, rot ist und T.nil -Kinder hat. Die einzigen Eigenschaften, die verletzt werden können, sind daher die Eigenschaft 2, die fordert, dass die Wurzel schwarz ist, und Eigenschaft 4, nach der ein roter Knoten keine roten Kinder haben darf. Beide Verletzungen sind darauf zurückzuführen, dass z rot ist. Eigenschaft 2 ist verletzt, falls z die Wurzel ist, und Eigenschaft 4 ist verletzt, falls der Vater von z rot ist. Abbildung 13.4(a) zeigt eine Verletzung der Eigenschaft 4, nachdem der Knoten z eingefügt wurde. Die while-Schleife in den Zeilen 1–15 erfüllt die folgende aus drei Punkten bestehende Schleifeninvariante zu Beginn einer jeden Iteration der Schleife: Zu Beginn jeder Iteration der Schleife gilt: a. Knoten z ist rot. b. Falls z.vater die Wurzel ist, dann ist z.vater schwarz. c. Falls der Baum eine der Rot-Schwarz-Eigenschaften verletzt, dann verletzt er höchstens eine von ihnen und die Verletzung betrifft entweder Eigenschaft 2 oder Eigenschaft 4. Wenn der Baum Eigenschaft 2 verletzt, dann weil z die Wurzel ist und rot ist. Wenn der Baum Eigenschaft 4 verletzt, dann weil z und z.vater beide rot sind. Teil (c), der sich mit der Verletzung der Rot-Schwarz-Eigenschaften beschäftigt, ist von zentraler Bedeutung, wenn es darum geht zu zeigen, dass RB-Insert-Fixup die Rot-Schwarz-Eigenschaften wiederherstellt. Die Teile (a) und (b) verwenden wir, um die verschiedenen Fälle im Code zu verstehen. Da wir uns auf den Knoten z und die Knoten in seiner Nähe konzentrieren, hilft es, aus Teil (a) zu wissen, dass z rot gefärbt ist. Wir werden Teil (b) verwenden, um zu zeigen, dass der Knoten z.vater .vater existiert, wenn wir in den Zeilen 2, 3, 7, 8, 13 und 14 auf ihn zugreifen. Rufen Sie sich in Erinnerung, dass wir zeigen müssen, dass eine Schleifeninvariante vor der ersten Iteration der Schleife wahr ist, und dass uns die Schleifeninvariante eine nützliche Eigenschaft bei der Terminierung der Schleife liefert. 1 Fall
2 geht in Fall 3 über, sodass die beiden Fälle sich nicht gegenseitig ausschließen.
320
13 Rot-Schwarz-Bäume
11 2 (a)
14
1
7
15
5 z
8 y
4
Fall 1
11 2 (b)
14 y
1
7 5
z
15 8
4
Fall 2
11 7 (c)
z
14 y
2
8
1
15
5 Fall 3 4 7 z
(d)
2
11
1
5 4
8
14 15
Abbildung 13.4: Die Arbeitsweise von RB-Insert-Fixup. (a) Ein Knoten z nach dem Einfügen. Da sowohl z als auch sein Vater z. vater rot sind, liegt eine Verletzung der Eigenschaft 4 vor. Da der Onkel y von z rot ist, kann Fall 1 des Codes angewendet werden. Wir färben die Knoten um und bewegen den Zeiger z aufwärts im Baum, wodurch der in (b) gezeigte Baum entsteht. Wieder sind z und sein Vater beide rot, aber z’s Onkel y ist schwarz. Da z das rechte Kind von z. vater ist, kann Fall 2 angewendet werden. Wir führen eine Linksrotation aus. Der Baum, der dadurch entsteht, wird in (c) gezeigt. Jetzt ist z das linke Kind seines Vaters, und Fall 3 kann angewendet werden. Ein Umfärben der Knoten und eine Rechtsrotation führt zu dem in (d) gezeigten Baum, der ein korrekter Rot-Schwarz-Baum ist.
13.3 Einfügen eines Knotens
321
Wir beginnen mit der Initialisierung und der Terminierung. Wenn wir dann detaillierter untersuchen, wie der Schleifenrumpf arbeitet, werden wir zeigen, dass die Schleife die Invariante für jede Iteration erhält. Dabei werden wir beweisen, dass jede Iteration der Schleife jeweils eine von zwei möglichen Wirkungen hat: Entweder bewegt sich der Zeiger z im Baum nach oben oder wir führen einige Rotationen aus und die Schleife terminiert. Initialisierung: Wenn wir zur ersten Iteration der Schleife kommen, lag zuvor ein RotSchwarz-Baum ohne Verletzungen vor und wir haben einen roten Knoten z eingefügt. Wir zeigen, dass zu dem Zeitpunkt, zu dem RB-Insert-Fixup aufgerufen wird, jeder Teil der Schleifeninvariante gilt: a. Wenn RB-Insert-Fixup aufgerufen wird, dann ist z der rote Knoten, der hinzugefügt wurde. b. Wenn z.vater die Wurzel ist, dann war z.vater anfangs schwarz und hat seine Farbe vor dem Aufruf von RB-Insert-Fixup nicht geändert. c. Wir haben bereits gesehen, dass die Eigenschaften 1, 3 und 5 gelten, wenn RB-Insert-Fixup aufgerufen wird. Falls der Baum Eigenschaft 2 verletzt, dann muss die rote Wurzel der neu hinzugefügte Knoten z sein, welcher der einzige innere Knoten des Baumes ist. Da der Vater und beide Kinder von z der Wächter sind, der schwarz ist, verletzt der Baum nicht auch Eigenschaft 4. Demzufolge ist diese Verletzung der Eigenschaft 2 die einzige Verletzung der Rot-Schwarz-Eigenschaften im ganzen Baum. Falls der Baum Eigenschaft 4 verletzt, dann muss die Verletzung darin bestehen, dass sowohl z als auch z.vater rot sind, denn die Kinder des Knotens z sind schwarze Wächter und der Baum hatte keine anderen Verletzungen, bevor z hinzugefügt wurde. Außerdem verletzt der Baum keine anderen RotSchwarz-Eigenschaften. Terminierung: Wenn die Schleife terminiert, dann geschieht dies, weil z.vater schwarz ist. (Falls z die Wurzel ist, dann ist z.vater der Wächter T.nil , der schwarz ist.) Bei Beendigung der Schleife gibt es also keine Verletzung der Eigenschaft 4. Wegen der Schleifeninvariante ist die einzige Eigenschaft, die verletzt sein kann, die Eigenschaft 2. Zeile 16 stellt auch diese Eigenschaft wieder her, sodass alle RotSchwarz-Eigenschaften gelten, wenn RB-Insert-Fixup terminiert. Fortsetzung: Wir haben an sich sechs Fälle in der while-Schleife, aber drei von ihnen sind symmetrisch zu den anderen drei, abhängig davon, ob Zeile 2 feststellt, dass z’s Vater z.vater ein linkes oder ein rechtes Kind von z’s Großvater z.vater .vater ist. Wir haben den Code nur für den Fall angegeben, dass z.vater ein linkes Kind ist. Der Knoten z.vater .vater existiert, denn nach Teil (b) der Schleifeninvariante ist z.vater schwarz, falls z.vater die Wurzel ist. Da wir eine Schleifeniteration nur beginnen, wenn z.vater rot ist, wissen wir, dass z.vater nicht die Wurzel sein kann. Folglich muss z.vater .vater existieren. Wir unterscheiden Fall 1 von den Fällen 2 und 3 über die Farbe, die der Bruder von z’s Vater, also dessen „Onkel“, hat. Zeile 3 sorgt dafür, dass y auf z’s Onkel
322
13 Rot-Schwarz-Bäume
neu z
C (a)
A
A
D y
α
δ
B z
β
ε
D
α
γ
β
α
γ
A
β
δ
C
B
D y
ε α
D
γ
A
ε
γ
neu z
B z
δ
B
C (b)
C
δ
ε
β
Abbildung 13.5: Fall 1 der Prozedur RB-Insert-Fixup. Eigenschaft 4 ist verletzt, da z und sein Vater z. vater beide rot sind. Unabhängig davon, ob (a) z ein rechtes Kind oder (b) z ein linkes Kind ist, führen wir dieselbe Aktion durch. Die Teilbäume α, β, γ, δ und ε haben alle eine schwarze Wurzel, und jeder hat die gleiche Schwarz-Höhe. Der Code für den Fall 1 ändert die Farben einiger Knoten, um Eigenschaft 5 zu erhalten: Alle von einem gleichen Knoten startenden, abwärts laufenden einfachen Pfade zu einem Blatt haben die gleiche Anzahl schwarzer Knoten. Die while-Schleife fährt mit dem Großvater z. vater . vater des Knotens z als neuem Knoten z fort. Eine Verletzung der Eigenschaft 4 kann nun höchstens zwischen dem neuen Knoten z, der rot ist, und seinem Vater auftreten, falls dieser ebenfalls rot ist.
z.vater .vater .rechts zeigt. In Zeile 4 wird die Farbe von y getestet. Falls y rot ist, führen wir Fall 1 aus. Anderenfalls sind die Fälle 2 und 3 auszuführen. In allen drei Fällen ist der Großvater z.vater .vater von z schwarz, da der Vater z.vater rot ist, und Eigenschaft 4 nur zwischen z und z.vater verletzt ist. Fall 1: z’s Onkel y ist rot Abbildung 13.5 zeigt die Situation für den Fall 1 (Zeilen 5–8), die auftritt, wenn z.vater und y beide rot sind. Da z.vater .vater schwarz ist, können wir sowohl z.vater als auch y schwarz färben. Damit ist das Problem gelöst, dass z und z.vater beide rot sind, und wir können den Großvater z.vater .vater rot färben. Somit bleibt Eigenschaft 5 erhalten. Dann wiederholen wir die while-Schleife mit z.vater.vater als neuem Knoten z. Der Zeiger z steigt zwei Ebenen im Baum auf. Nun zeigen wir, dass Fall 1 die Schleifeninvariante erhält, sodass sie zu Beginn der nächsten Iteration ebenfalls gilt. Wir verwenden z zur Bezeichnung des Knotens z in der aktuellen Iteration und z = z.vater .vater , um den Knoten zu bezeichnen, der im Test in Zeile 1 der nächsten Iteration die Rolle von z einnimmt. a. Da die aktuelle Iteration z.vater .vater rot färbt, ist der Knoten z zu Beginn der nächsten Iteration rot. b. Der Knoten z .vater ist in der aktuellen Iteration z.vater .vater .vater , und die
13.3 Einfügen eines Knotens
323
Farbe dieses Knotens ändert sich nicht. Falls dieser Knoten die Wurzel ist, war er vor dieser Iteration schwarz und bleibt auch zu Beginn der nächsten Iteration schwarz. c. Wir haben bereits gezeigt, dass Fall 1 die Eigenschaft 5 erhält und er nicht zu einer Verletzung der Eigenschaften 1 oder 3 führt. Falls Knoten z zu Beginn der nächsten Iteration die Wurzel ist, dann hätte Fall 1 in der aktuellen Iteration die einzige Verletzung der Eigenschaft 4 korrigiert. Da der Knoten z rot und die Wurzel ist, ist Eigenschaft 2 nun die einzige Eigenschaft, die verletzt ist, und diese Verletzung wird durch z bedingt.. Falls Knoten z zu Beginn der nächsten Iteration nicht die Wurzel ist, dann hat Fall 1 keine Verletzung der Eigenschaft 2 hervorgerufen. Fall 1 hat die Verletzung der Eigenschaft 4 korrigiert, die zu Beginn der Iteration existierte. Dann wurde z rot gefärbt und z .vater unverändert gelassen. Wenn z .vater schwarz war, gibt es keine Verletzung der Eigenschaft 4. Wenn z .vater rot war, erzeugt das Rotfärben von z eine Verletzung der Eigenschaft 4 zwischen z und z .vater . Fall 2: z’s Onkel y ist schwarz und z ist ein rechtes Kind Fall 3: z’s Onkel y ist schwarz und z ist ein linkes Kind In beiden Fällen ist die Farbe von z’s Onkel y schwarz. Wir unterscheiden die beiden Fällen nach dem, ob z ein rechtes oder ein linkes Kind von z.vater ist. Die Zeilen 10–11 beschreiben den Fall 2, der in Abbildung 13.6 zusammen mit dem Fall 3 gezeigt ist. Im Fall 2 ist der Knoten z ein rechtes Kind seines Vaters. Wir wenden eine Linksrotation an, um die Situation in den Fall 3 zu überführen (Zeilen 12–14), in der Knoten z ein linkes Kind ist. Da z und z.vater beide rot sind, berührt die Rotation weder die Schwarz-Höhe der Knoten noch die Eigenschaft 5. Unabhängig davon, ob wir direkt oder über Fall 2 zu Fall 3 kommen, ist z’s Onkel y schwarz, da wir sonst Fall 1 ausgeführt hätten. Außerdem existiert der Knoten z.vater .vater , denn wir haben gezeigt, dass der Knoten zu dem Zeitpunkt existierte, als die Zeilen 2 und 3 ausgeführt wurden. Nachdem z in Zeile 10 um eine Ebene nach oben und dann in Zeile 11 um eine Ebene nach unten gestiegen ist, hat sich die Identität von z.vater .vater nicht geändert. In Fall 3 führen wir einige Farbänderungen und eine Rechtsrotation aus, die Eigenschaft 5 erhalten. Damit sind wir fertig, da es nun keine zwei aufeinander folgenden roten Knoten mehr gibt. Die while-Schleife iteriert kein weiteres Mal, da z.vater nun schwarz ist. Nun werden wir zeigen, dass die Fälle 2 und 3 die Schleifeninvariante erhalten. (Wie wir bereits erläutert haben, ist z.vater beim nächsten Test in der Zeile 1 schwarz, und der Schleifenrumpf wird nicht noch einmal ausgeführt.) a. Fall 2 bewirkt, dass z auf den Knoten z.vater zeigt, der rot ist. In den Fällen 2 und 3 treten keine weiteren Änderungen an z oder seiner Farbe auf. b. Im Fall 3 wird der Knoten z.vater schwarz gefärbt, sodass er schwarz ist, wenn er zu Beginn der nächsten Iteration die Wurzel wäre. c. Wie im Fall 1 bleiben auch in den Fällen 2 und 3 die Eigenschaften 1, 3 und 5 erhalten.
324
13 Rot-Schwarz-Bäume
C
C
δ y
A
α
z
γ
α
β Fall 2
δ y
B
B z
γ
A
B z
α
A
C
β
γ
δ
β Fall 3
Abbildung 13.6: Die Fälle 2 und 3 der Prozedur RB-Insert. Wie in Fall 1 ist die Eigenschaft 4 in den Fällen 2 und 3 verletzt, da z und sein Vater z. vater beide rot sind. Alle Teilbäume α, β, γ und δ haben eine schwarze Wurzel (α, β und γ wegen Eigenschaft 4 und δ, weil ansonsten Fall 1 gelten würde) und alle haben die gleiche Schwarz-Höhe. Wir führen Fall 2 mit einer Linksrotation in Fall 3 über. Dies erhält die Eigenschaft 5: Alle von einem gemeinsamen Knoten startenden, abwärts laufenden einfachen Pfade zu einem Blatt haben die gleiche Anzahl schwarzer Knoten. Fall 3 bewirkt einige Farbänderungen und eine Rechtsrotation, die ebenfalls Eigenschaft 5 erhält. Dann terminiert die while-Schleife, da Eigenschaft 4 erfüllt ist: Es gibt nun keine zwei aufeinander folgenden roten Knoten mehr.
Da der Knoten z in den Fällen 2 und 3 nicht die Wurzel ist, wissen wir, dass es keine Verletzung der Eigenschaft 2 gibt. Die Fälle 2 und 3 verursachen keine Verletzung der Eigenschaft 2, da der einzige Knoten, der rot gefärbt wurde, durch die Rotation im Fall 3 ein Kind eines schwarzen Knotens wird. Die Fälle 2 und 3 korrigieren die Verletzung der Eigenschaft 4 und führen zu keiner weiteren Verletzung. Indem wir bewiesen haben, dass jeder Schleifendurchlauf die Invariante erhält, haben wir gezeigt, dass RB-Insert-Fixup die Rot-Schwarz-Eigenschaften wiederherstellt.
Analyse Wie ist die Laufzeit von RB-Insert? Da die Höhe eines Rot-Schwarz-Baumes mit n Knoten O(lg n) ist, benötigen die Zeilen 1–16 von RB-Insert Zeit O(lg n). In RBInsert-Fixup wird die while-Schleife nur wiederholt, wenn Fall 1 vorliegt, und dann der Zeiger z zwei Ebenen im Baum hochsteigt. Die while-Schleife kann daher nur O(lg n)-mal ausgeführt werden. Somit benötigt RB-Insert eine Gesamtzeit O(lg n). Zudem führt sie niemals mehr als zwei Rotationen aus, da die while-Schleife terminiert, wenn die Fälle 2 oder 3 ausgeführt werden.
Übungen 13.3-1 In Zeile 16 von RB-Insert setzen wir die Farbe des neu eingefügten Knotens auf rot. Hätten wir seine Farbe stattdessen auf schwarz gesetzt, dann wäre die Eigenschaft 4 eines Rot-Schwarz-Baumes nicht verletzt. Warum haben wir dies nicht getan?
13.4 Löschen eines Knotens
325
13.3-2 Zeichnen Sie diejenigen Rot-Schwarz-Bäume, die durch sukzessives Einfügen der Schlüssel 41, 38, 31, 12, 19, 8 in einen anfangs leeren Baum entstehen. 13.3-3 Nehmen Sie an, die Schwarz-Höhe eines jeden der Teilbäume α, β, γ, δ, ε in den Abbildungen 13.5 und 13.6 wäre k. Markieren Sie jeden Knoten in den Abbildungen mit seiner Schwarz-Höhe, um zu überprüfen, dass Eigenschaft 5 durch die gezeigte Transformation erhalten bleibt. 13.3-4 Professor Teach ist besorgt, dass RB-Insert-Fixup das Attribut T.nil .farbe auf rot setzen könnte, was dazu führen würde, dass der Test in Zeile 1 nicht zur Terminierung der Schleife führen würde, wenn z die Wurzel ist. Zeigen Sie, dass die Sorge des Professors unbegründet ist, indem Sie erläutern, warum RB-Insert-Fixup das Attribut T.nil.farbe niemals auf rot setzt. 13.3-5 Betrachten Sie einen Rot-Schwarz-Baum, der durch das Einfügen von n Knoten mithilfe von RB-Insert entsteht. Zeigen Sie, dass der Baum im Falle n > 1 mindestens einen roten Knoten hat. 13.3-6 Machen Sie einen Vorschlag, wie RB-Insert effizient implementiert werden kann, wenn die Darstellung für Rot-Schwarz-Bäume keinen Speicherplatz für die Zeiger auf die Väter zur Verfügung stellt.
13.4
Löschen eines Knotens
Wie die anderen Grundoperationen auf einem Rot-Schwarz-Baum mit n Knoten, benötigt das Entfernen eines Knotens Zeit O(lg n). Das Entfernen eines Knotens aus einem Rot-Schwarz-Baum ist ein bisschen komplizierter als das Einfügen eines Knotens. Die Prozedur zum Entfernen eines Knotens aus einem Rot-Schwarz-Baum basiert auf der Prozedur Tree-Delete aus Abschnitt 12.3. Zuerst müssen wir die Unterroutine Transplant anpassen, sodass sie auf Rot-Schwarz-Bäume angewendet werden kann: RB-Transplant(T, u, v) 1 if u.vater = = T.nil 2 T.wurzel = v 3 elseif u = = u.vater .links 4 u.vater .links = v 5 else u.vater .rechts = v 6 v.vater = u.vater Die Prozedur RB-Transplant unterscheidet sich in zwei Punkten von der Prozedur Transplant. Erstens arbeitet Zeile 1 mit dem Wächter T.nil und nicht mit nil. Zweitens erfolgt die Zuweisung an v.vater in Zeile 6 unbedingt: Wir können die Zuweisung an v.vater auch dann machen, wenn v auf den Wächter zeigt. Wir sollten die Möglichkeit nutzen, v.vater einen Wert zuzuweisen, wenn v = T.nil gilt.
326
13 Rot-Schwarz-Bäume
Die Prozedur RB-Delete ähnelt der Prozedur Tree-Delete, hat aber noch zusätzliche Zeilen im Pseusocode. Einige der zusätzlichen Zeilen halten Knoten y im Auge, der Verletzungen der Rot-Schwarz-Eigenschaften verursachen kann. Wenn wir Knoten z löschen wollen und z hat weniger als zwei Kinder, dann entfernen wir z aus dem Baum und wollen, dass y z ist. Wenn z zwei Kinder hat, dann sollte y der Nachfolger von z sein und y sollte z’s Position einnehmen. Wir erinnern uns an y’s Farbe, bevor dieser Knoten entfernt oder im Baum bewegt wurde, und wir halten den Knoten x, der auf y’s ursprüngliche Position im Baum wandert, im Auge, denn Knoten x könnte nun auch Verletzungen der Rot-Schwarz-Eigenschaften verursachen. Nachdem Knoten z gelöscht ist, ruft RB-Delete eine Hilfsroutine RB-Delete-Fixup auf, die Knoten umfärbt und Rotationen ausführt, um die Rot-Schwarz-Eigenschaften wiederherzustellen. RB-Delete(T, z) 1 y =z 2 y-urspr u ¨ ngliche-farbe = y.farbe 3 if z.links = = T.nil 4 x = z.rechts 5 RB-Transplant(T, z, z.rechts) 6 elseif z.rechts = = T.nil 7 x = z.links 8 RB-Transplant(T, z, z.links) 9 else y = Tree-Minimum(z.rechts) 10 y-urspr u ¨ ngliche-farbe = y.farbe 11 x = y.rechts 12 if y.vater = = z 13 x.vater = y 14 else RB-Transplant(T, y, y.rechts) 15 y.rechts = z.rechts 16 y.rechts.vater = y 17 RB-Transplant(T, z, y) 18 y.links = z.links 19 y.links.vater = y 20 y.farbe = z.farbe 21 if y-urspr u ¨ ngliche-farbe = = schwarz 22 RB-Delete-Fixup(T, x) Wenngleich RB-Delete fast doppelt so viele Zeilen an Pseudocode umfasst wie TreeDelete, verfügen die beiden Prozeduren über die gleiche Basisstruktur. Sie können jede Zeile von Tree-Delete innerhalb von RB-Delete finden (mit dem Unterschied, dass nil durch T.nil und Aufrufe von Transplant durch Aufrufe von RB-Transplant ersetzt sind), die unter den gleichen Bedingungen ausgeführt wird. Hier nun die wichtigsten Unterschiede zwischen den beiden Prozeduren: • Wir führen den Knoten y als den Knoten, der aus dem Baum entfernt oder innerhalb des Baumes an eine andere Stelle gesetzt wird. Zeile 1 lässt y auf Knoten z zeigen, der entfernt wird, falls z weniger als zwei Kinder hat. Falls z zwei Kinder
13.4 Löschen eines Knotens
327
hat, lässt Zeile 9 y auf den Nachfolger von z zeigen – genau wie in Tree-Delete – und y wandert im Baum an z’s Position. • Da die Farbe von Knoten y sich möglicherweise ändert, speichert die Variable y-urspr u ¨ ngliche-farbe die Farbe von y, bevor eine Änderung erfolgt. Die Zeilen 2 und 10 setzen diese Variable sofort nach Zuweisungen an y. Falls z zwei Kinder hat, dann gilt y = z und Knoten y wandert an die ursprüngliche Position von z im Rot-Schwarz-Baum; Zeile 20 gibt y die gleiche Farbe wie z. Wir müssen die ursprüngliche Farbe von y retten, um sie am Ende der Prozedur RB-Delete überprüfen zu können; falls sie schwarz war, dann kann das Entfernen oder das Bewegen von y Verletzungen der Rot-Schwarz-Eigenschaften verursachen. • Wie bereits diskutiert, behalten wir Knoten x, der an die ursprüngliche Position von y wandert, im Auge. Die Zuweisungen der Zeilen 4, 7, und 11 lassen x entweder auf y’s einzigstes Kind oder, falls y keine Kinder hat, auf den Wächter T.nil zeigen. (Erinnern Sie sich aus Abschnitt 12.3 daran, dass y kein linkes Kind hat.) • Da Knoten x auf die ursprüngliche Position von y wandert, wird x.vater immer so gesetzt, dass es auf die ursprüngliche Position von y’s Vater im Baum zeigt, sogar dann, wenn x der Wächter T.nil ist. Sofern z nicht y’s ursprünglicher Vater ist (was nur dann der Fall ist, wenn z zwei Kinder hat und sein Nachfolger y das rechte Kind von z ist), erfolgt die Zuweisung an x.vater in Zeile 6 von RB-Transplant. (Bemerken Sie, dass, wenn RB-Transplant in der Zeile 5, 8 oder 14 aufgerufen wird, der dritte Parameter, der übergeben wird, x ist.) Wenn der ursprüngliche Vater von y aber z ist, brauchen wir x.vater nicht auf y’s ursprünglichen Vater zeigen zu lassen, da wir diesen Knoten aus dem Baum löschen. Da Knoten y im Baum nach oben wandern wird, um z’s Position im Baum einzunehmen, erreichen wir durch Setzen von x.vater auf y in Zeile 13, dass x.vater auf die ursprüngliche Position von y’s Vater zeigt, sogar dann, wenn x = T.nil gilt. • Falls Knoten y schwarz war, haben wir möglicherweise eine oder mehrere Verletzungen der Rot-Schwarz-Eigenschaften verursacht, sodass wir schlussendlich in Zeile 22 die Prozedur RB-Delete-Fixup aufrufen, um die Rot-Schwarz-Eigenschaften wieder zu restaurieren. Falls y rot ist, dann bleiben die Rot-SchwarzEigenschaften aus folgenden beiden Gründen erhalten, wenn y entfernt oder im Baum umgehängt wird: 1. Keine Schwarz-Höhe im Baum hat sich verändert. 2. Keine roten Knoten wurden benachbart. Da y den Platz von z im Baum einnimmt, und dabei die Farbe von z übernimmt, können wir keine zwei adjazente rote Knoten an y’s neuer Position im Baum haben. Zudem ersetzt y’s ursprüngliches rechtes Kind x y im Baum, falls y nicht z’s rechtes Kind war. Falls y rot war, dann muss x schwarz sein, und das Ersetzen von y durch x kann nicht die Ursache sein, dass zwei rote Knoten zueinander adjazent werden. 3. Da der Knoten y nicht die Wurzel sein konnte, wenn er rot war, bleibt die Wurzel schwarz.
328
13 Rot-Schwarz-Bäume
Falls der Knoten y schwarz war, können drei Probleme auftreten, die mit RB-DeleteFixup behoben werden können. Erstens, wenn y die Wurzel war und ein rotes Kind von y die neue Wurzel wird, dann verletzen wir Eigenschaft 2. Zweitens, wenn x und x.vater beide rot sind, dann haben wir Eigenschaft 4 verletzt. Drittens bewirkt das Umsetzen von y innerhalb der Baumes, dass jeder einfache Pfad, der zuvor y enthielt, nun einen schwarzen Knoten weniger hat. Damit ist nun Eigenschaft 5 durch jeden Vorfahren von y verletzt. Wir können die Verletzung von Eigenschaft 5 heilen, indem wir festlegen, dass der Knoten x, der nun y’s ursprüngliche Position einnimmt, ein „zusätzliches“ Schwarz hat. Damit meinen wir, dass, wenn wir die Anzahl der schwarzen Knoten auf jedem Pfad, der x enthält, um eins erhöhen, Eigenschaft 5 mit dieser Interpretation wieder gilt. Wenn wir den schwarzen Knoten y entfernen oder im Baum umhängen, dann „übertragen“ wir seine Eigenschaft „schwarz“ auf den Knoten x. Das Problem dabei ist, dass der Knoten x nun weder rot noch schwarz ist und somit Eigenschaft 1 verletzt. Stattdessen ist x entweder „doppelt schwarz“ oder „rot-schwarz“, und trägt daher auf einfachen Pfaden, die x enthalten, entweder 2 oder 1 zu der Anzahl schwarzer Knoten bei. Das Attribut farbe von x ist noch immer entweder rot (falls x rot-schwarz ist) oder schwarz (falls x doppelt schwarz ist). In anderen Worten, das Extra-Schwarz eines Knotens ist erkennbar über die Zeiger von x und nicht über das Attribut farbe. Wir können nun zur Prozedur RB-Delete-Fixup kommen und uns näher anschauen, wie sie die Rot-Schwarz-Eigenschaften in dem Suchbaum wiederherstellt.
RB-Delete-Fixup(T, x) 1 while x = T.wurzel und x.farbe = = schwarz 2 if x = = x.vater .links 3 w = x.vater .rechts 4 if w.farbe = = rot 5 w.farbe = schwarz // Fall 1 6 x.vater .farbe = rot // Fall 1 7 Left-Rotate(T, x.vater ) // Fall 1 8 w = x.vater .rechts // Fall 1 9 if w.links.farbe == schwarz und w.rechts.farbe = = schwarz 10 w.farbe = rot // Fall 2 11 x = x.vater // Fall 2 12 else if w.rechts.farbe = = schwarz 13 w.links.farbe = schwarz // Fall 3 14 w.farbe = rot // Fall 3 15 Right-Rotate(T, w) // Fall 3 16 w = x.vater .rechts // Fall 3 17 w.farbe = x.vater .farbe // Fall 4 18 x.vater .farbe = schwarz // Fall 4 19 w.rechts.farbe = schwarz // Fall 4 20 Left-Rotate(T, x.vater ) // Fall 4 21 x = T.wurzel // Fall 4 22 else (analog zum then-Fall nur “rechts” und “links” vertauschen) 23 x.farbe = schwarz
13.4 Löschen eines Knotens
329
Die Prozedur RB-Delete-Fixup stellt die Eigenschaften 1, 2 und 4 wieder her. In den Übungen 13.4-1 und 13.4-2 sollen Sie zeigen, dass die Eigenschaften 2 und 4 durch die Prozedur wiederhergestellt werden, sodass wir uns im Rest des Abschnitts auf die Eigenschaft 1 konzentrieren werden. Das Ziel der while-Schleife in den Zeilen 1–22 ist es, das Extra-Schwarz im Baum nach oben zu bewegen, bis 1. x auf einen rot-schwarzen Knoten zeigt und wir x in Zeile 23 (einfach) schwarz färben, 2. x auf die Wurzel zeigt und wir das Extra-Schwarz einfach „entfernen“, oder 3. geeignete Rotationen und Umfärbungen durchgeführt wurden und wir die Schleife verlassen können. Innerhalb der while-Schleife zeigt x immer auf einen doppelt schwarzen Knoten, der nicht die Wurzel ist. Wir bestimmen in Zeile 2, ob x ein linkes oder ein rechtes Kind seines Vaters x.vater ist. (Wir haben den Code für den Fall angegeben, dass x ein linkes Kind ist; der Fall, dass x ein rechtes Kind ist – siehe Zeile 22 –, ist dazu symmetrisch.) Wir verwalten einen Zeiger w auf den Bruder von x. Da der Knoten x doppelt schwarz ist, kann w nicht T.nil sein, denn anderenfalls wäre die Anzahl von Schwarz auf dem einfachen Pfad von x.vater zum (einfach schwarzen) Blatt w kleiner als auf dem Pfad von x.vater nach x. Die vier Fälle 2 des Codes sind in Abbildung 13.7 illustriert. Bevor wir jeden Fall im Detail untersuchen, schauen wir uns allgemeiner an, wie wir überprüfen können, dass in jedem der Fälle Eigenschaft 5 erhalten bleibt. Die Idee besteht darin, dass in jedem der Fälle die angewandte Transformation die Anzahl der schwarzen Knoten (einschließlich des zusätzliche Schwarz von x) von der Wurzel des Teilbaumes (einschließlich der Wurzel selbst) zu jedem der Teilbäume α, β, . . . , ζ nicht verändert. Wenn also Eigenschaft 5 vor der Transformation gültig war, dann gilt sie auch danach. In Abbildung 13.7(a), die Fall 1 illustriert, ist zum Beispiel die Anzahl der schwarzen Knoten von der Wurzel zu den Teilbäumen α und β vor und nach der Transformation jeweils 3. (Es sei nochmals daran erinnert, dass der Knoten x ein zusätzliches Schwarz beiträgt.) Gleichermaßen ist die Anzahl der schwarzen Knoten von der Wurzel bis zu einem der Teilbäume γ, δ, ε, und ζ vor und nach der Transformation jeweils 2. In Abbildung 13.7(b) muss die Zählung den Wert c des Attributs farbe der Wurzel des gezeigten Teilbaums mitberücksichtigen. Dieser Wert kann rot oder schwarz sein. Definieren wir Anzahl(rot) = 0 und Anzahl(schwarz) = 1, dann ist die Anzahl der schwarzen Knoten von der Wurzel bis α vor und nach der Transformation 2 + Anzahl(c). In diesem Falle hat der neue Knoten x das Farbattribut c. Dieser Knoten ist jedoch in Wirklichkeit entweder rot-schwarz (für c = rot) oder doppelt schwarz (für c = schwarz). Sie können die anderen Fälle in der gleichen Art und Weise überprüfen (siehe Übung 13.4-5) Fall 1: x’s Bruder w ist rot Fall 1 (Zeilen 5–8 von RB-Delete-Fixup und Abbildung 13.7(a)) liegt vor, wenn der Knoten w, der Bruder des Knotens x, rot ist. Da w schwarze Kinder haben muss, können 2 Wie bei RB-Insert-Fixup schließen sich die verschiedenen Fälle bei RB-Delete-Fixup einander nicht aus.
330
13 Rot-Schwarz-Bäume
Fall 1
B (a)
x A
α
B
D w
β
C
γ
x A
E
δ
ε
C
γ
ε
C
γ
neu x
B c
β
C
E
γ
δ
x A
ε
ε
ζ
B c C neu w
α
E
δ
ζ
D
ζ
D w
β
δ
Fall 3
x A
α
γ
α
E
δ
ε
C
A
B c (c)
β
D w
β
E
neu w
Fall 2
x A
α
α
ζ
B c (b)
D
β
γ
D
ζ
δ
E
ε Fall 4
B c (d)
x A
α
D c B
D w
β
C
γ
c′
δ
E
A
E
ε
ζ
ζ
α
C
β
γ
c′ ε
δ
ζ neu x = T.wurzel
Abbildung 13.7: Die verschiedenen Fälle der while-Schleife der Prozedur RB-DeleteFixup. Das Attribut farbe ist für die dunklen Knoten schwarz, für die stark schattierten Knoten rot und für die schwach schattierten Knoten durch die Variablen c und c dargestellt, die die Werte rot oder schwarz annehmen können. Die Buchstaben α, β, . . . , ζ stellen beliebige Teilbäume dar. Jeder Fall transformiert die Konfiguration links in die Konfiguration rechts, indem einige Farben geändert und/oder Rotationen ausgeführt werden. Jeder Knoten, auf den x zeigt, hat ein zusätzliches Schwarz und ist entweder doppelt schwarz oder rot-schwarz. Nur Fall 2 verursacht, dass die Schleife ein weiteres Mal ausgeführt werden muss. (a) Fall 1 wird in einen der Fälle 2, 3 oder 4 transformiert, indem die Farben der Knoten D und B vertauscht werden und eine Linksrotation ausgeführt wird. (b) Im Fall 2 wird das zusätzliche Schwarz, das durch den Zeiger x dargestellt wird, im Baum nach oben bewegt, indem der Knoten D rot gefärbt und x so gesetzt wird, dass x auf den Knoten B zeigt. Wenn wir von Fall 1 aus zu Fall 2 gelangten, dann terminiert die while-Schleife, da der neue Knoten rot-schwarz ist und daher der Wert c des farbe-Attributs rot ist. (c) Fall 3 wird in Fall 4 transformiert, indem die Farben der Knoten C und D vertauscht werden und eine Rechtsrotation ausgeführt wird. (d) Fall 4 entfernt das durch x repräsentierte zusätzliche Schwarz, indem er einige Farben ändert und eine Linksrotation ausführt (ohne hierbei Rot-Schwarz-Eigenschaften zu verletzen); anschließend terminiert die Schleife.
13.4 Löschen eines Knotens
331
wir die Farben von w und x.vater vertauschen und eine Linksrotation auf x.vater ausführen, ohne eine der Rot-Schwarz-Eigenschaften zu verletzen. Der neue Bruder von x, der vor der Rotation eines der Kinder von w war, ist nun schwarz. Somit haben wir Fall 1 in Fall 2, 3 oder 4 überführt. Die Fälle 2, 3 und 4 liegen vor, wenn der Knoten w schwarz ist. Sie unterscheiden sich hinsichtlich der Farben der Kinder von w. Fall 2: x’s Bruder w ist schwarz und beide Kinder von w sind schwarz In Fall 2 (Zeilen 10–11 von RB-Delete-Fixup und Abbildung 13.7(b)) sind beide Kinder von w schwarz. Da w ebenfalls schwarz ist, nehmen wir von x und w ein Schwarz weg, sodass x nur noch einfach schwarz und w rot ist. Um das Entfernen eines Schwarz bei x und w zu kompensieren, wollen wir ein zusätzliches Schwarz zum Knoten x.vater hinzufügen, der ursprünglich entweder rot oder schwarz war. Wir tun dies, indem wir die while-Schleife mit x.vater als neuem Knoten x erneut ausführen. Man beachte, dass der neue Knoten x rot-schwarz ist, wenn wir Fall 2 über Fall 1 erreichen, da dann der ursprüngliche Knoten x.vater rot war. Demzufolge ist der Wert c des Attributs farbe des neuen Knotens x rot, und die Schleife terminiert, wenn die Schleifenbedingung getestet wird. Wir färben dann den neuen Knoten in Zeile 23 (einfach) schwarz. Fall 3: x’s Bruder w ist schwarz, das linke Kind von w ist rot und das rechte Kind von w ist schwarz Fall 3 (Zeilen 13–16 und Abbildung 13.7(c)) liegt vor, wenn w schwarz, sein linkes Kind rot und sein rechtes Kind schwarz ist. Wir können die Farben von w und seinem linken Kind w.links vertauschen und dann eine Rechtsrotation auf w ausführen, ohne eine der Rot-Schwarz-Eigenschaften zu verletzen. Der neue Bruder w von x ist nun ein schwarzer Knoten mit einem roten rechten Kind, sodass wir Fall 3 in Fall 4 überführt haben. Fall 4: x’s Bruder w ist schwarz und das rechte Kind von w ist rot Fall 4 (Zeilen 17–21 und Abbildung 13.7(d)) liegt vor, wenn der Bruder w des Knotens x schwarz und das rechte Kind von w rot ist. Durch einige Farbänderungen und eine Linksrotation auf x.vater können wir das zusätzliche Schwarz von x entfernen und x einfach schwarz machen, ohne eine der Rot-Schwarz-Eigenschaften zu verletzen. Wird x zur Wurzel, dann terminiert die while-Schleife, wenn die Schleifenbedingung getestet wird.
Analyse Wie ist die Laufzeit von RB-Delete? Da die Höhe eines Rot-Schwarz-Baums mit n Knoten O(lg n) ist, benötigt die Prozedur ohne den Aufruf von RB-Delete-Fixup Zeit O(lg n). Innerhalb von RB-Delete-Fixup führt jeder der Fälle 1, 3 und 4 nach einer konstanten Anzahl von Farbänderungen und nach höchstens drei Rotationen zur Terminierung der Prozedur. Fall 2 ist der einzige Fall, in dem die while-Schleife wiederholt werden kann. Hier steigt der Zeiger x im Baum höchstens O(lg n)-mal nach oben, ohne dass hierbei Rotationen ausgeführt werden. Daher benötigt die Prozedur RB-DeleteFixup Zeit O(lg n) und führt höchstens drei Rotationen aus. Die Gesamtlaufzeit von RB-Delete ist also ebenfalls O(lg n).
332
13 Rot-Schwarz-Bäume
Übungen 13.4-1 Zeigen Sie, dass die Wurzel des Baumes nach Ausführen von RB-DeleteFixup schwarz sein muss. 13.4-2 Zeigen Sie, dass Eigenschaft 4 durch den Aufruf von RB-Delete-Fixup(T, x) wiederhergestellt wird, wenn in RB-Delete sowohl x als auch x.vater rot sind. 13.4-3 In Übung 13.3-2 haben Sie den Rot-Schwarz-Baum bestimmt, der durch das sukzessive Einfügen der Schlüssel 41, 38, 31, 12, 19, 8 in einen anfangs leeren Baum entsteht. Wie sehen die Rot-Schwarz-Bäume aus, die durch sukzessives Entfernen der Schlüssel in der Reihenfolge 8, 12, 19, 31, 38, 41 entstehen? 13.4-4 In welchen Codezeilen von RB-Delete-Fixup wird der Wächter T.nil überprüft oder modifiziert? 13.4-5 Geben Sie für jeden der in Abbildung 13.7 gezeigten Fälle die Anzahl der schwarzen Knoten von der Wurzel des gezeigten Teilbaums zu den Teilbäumen α, β, . . . , ζ an und überprüfen Sie, dass jede Anzahl nach der Transformation unverändert bleibt. Wenn das farbe-Attribut eines Knotens den Wert c oder den Wert c hat, benutzen Sie die Notation Anzahl(c) und Anzahl(c ), um die Anzahl symbolisch anzugeben. 13.4-6 Die Professoren Skelton und Baron sind besorgt, dass zu Beginn des Falls 1 der Prozedur RB-Delete-Fixup der Knoten x nicht schwarz sein könnte. Wenn die Professoren recht haben, dann sind die Zeilen 5–6 falsch. Zeigen Sie, dass x.vater zu Beginn des Falls 1 schwarz sein muss und die Professoren keinen Grund zur Besorgnis haben. 13.4-7 Nehmen Sie an, ein Knoten x würde mithilfe von RB-Insert in einen RotSchwarz-Baum eingefügt und unmittelbar danach mit RB-Delete entfernt werden. Ist der entstehende Rot-Schwarz-Baum der gleiche wie der ursprüngliche? Begründen Sie Ihre Antwort.
Problemstellungen 13-1 Persistente dynamische Mengen Im Verlaufe eines Algorithmus tritt manchmal die Notwendigkeit auf, sich frühere Versionen einer dynamischen Menge zu merken, wenn diese aktualisiert wird. Wir nennen eine solche Menge persistent. Eine Möglichkeit, eine persistente Menge zu implementieren, besteht darin, die gesamte Menge bei jeder Modifikation zu kopieren. Diese Herangehensweise kann aber ein Programm verlangsamen und außerdem viel Speicher verbrauchen. In einigen Fällen können wir es viel besser machen. Betrachten Sie eine persistente Menge S mit den Operationen Insert, Delete und Search, die wir, wie in Abbildung 13.8(a) gezeigt, mithilfe eines binären
Problemstellungen zu Kapitel 13
4
333
r
3
r
7
4
3
8
2
4
10
r′
8
2
7
7
8
10
5 (a)
(b)
Abbildung 13.8: (a) Ein binärer Suchbaum mit den Schlüsseln 2, 3, 4, 7, 8, 10. (b) Der persistente binäre Suchbaum, der sich aus dem Einfügen des Schlüssels 5 ergibt. Die aktuellste Version der Menge besteht aus den Knoten, die von der Wurzel r aus erreichbar sind. Die vorhergehende Version besteht aus den Knoten, die von r aus erreichbar sind. Stark schattierte Knoten wurden hinzugefügt als der Schlüssel 5 eingefügt wurde.
Suchbaumes implementieren. Für jede Version der Menge halten wir eine separate Wurzel. Um den Schlüssel 5 in die Menge einzufügen, legen wir einen neuen Knoten mit dem Schlüssel 5 an. Dieser Knoten wird zum linken Kind eines neuen Knotens mit dem Schlüssel 7, da wir den bereits existierenden Knoten mit dem Schlüssel 7 nicht modifizieren können. In gleicher Weise wird der neue Knoten mit dem Schlüssel 7 das linke Kind eines neuen Knotens mit dem Schlüssel 8, dessen rechtes Kind der bereits existierende Knoten mit dem Schlüssel 10 ist. Der neue Knoten mit Schlüssel 8 wird wiederum das rechte Kind einer neuen Wurzel r mit Schlüssel 4, deren linkes Kind der bereits existierende Knoten mit dem Schlüssel 3 ist. Wir kopieren somit nur einen Teil des Baumes und benutzen einige Knoten des ursprünglichen Baumes gemeinsam (siehe Abbildung 13.8(b)). Setzen Sie voraus, dass jeder Knoten die Attribute schl¨u ssel, links und rechts aber kein vater -Attribut besitzt (siehe auch Übung 13.3-6). a. Identifizieren Sie für einen allgemeinen persistenten binären Suchbaum die Knoten, die wir ändern müssen, um einen Schlüssel k einzufügen oder einen Knoten y zu entfernen. b. Schreiben Sie eine Prozedur Persistent-Tree-Insert, die für einen gegebenen persistenten Baum T und einen gegebenen Schlüssel k, der eingefügt werden soll, einen neuen persistenten Baum T zurückgibt, der das Ergebnis des Einfügens von k in T darstellt. c. Wie groß sind der erforderliche Zeitaufwand und der Speicherbedarf Ihrer Implementierung von Persistent-Tree-Insert, wenn die Höhe eines persistenten binären Suchbaumes T durch h gegeben ist? (Der Speicherbedarf ist proportional zur Anzahl der neu allokierten Knoten.)
334
13 Rot-Schwarz-Bäume d. Nehmen Sie an, wir hätten jedem Knoten das Attribut vater hinzugefügt. In diesem Fall würde Persistent-Tree-Insert mehr kopieren müssen. Beweisen Sie, dass Persistent-Tree-Insert in diesem Fall Zeit und Speicher Ω(n) erfordern würde, wobei n die Anzahl der Knoten im Baum ist. e. Zeigen Sie, wie man einen Rot-Schwarz-Baum verwendet, um abzusichern, dass die Laufzeit und der Speicherbedarf für jedes Einfügen und Entfernen im schlechtesten Fall in O(lg n) liegen.
13-2 Vereinigen auf Rot-Schwarz-Bäumen Die Operation Vereinigen erhält als Operanden zwei dynamische Mengen S1 und S2 sowie ein Element x mit der Eigenschaft, dass für jedes x1 ∈ S1 und x2 ∈ S2 die Beziehung x1 .schl¨u ssel ≤ x.schl¨u ssel ≤ x2 .schl¨u ssel gilt. Sie gibt eine Menge S = S1 ∪ {x} ∪ S2 zurück. In dieser Problemstellung untersuchen wir, wie die Operation Vereinigen auf Rot-Schwarz-Bäumen zu implementieren ist. a. Lassen Sie uns für einen gegebenen Rot-Schwarz-Baum T seine Schwarz-Höhe in dem Attribut T.bh abspeichern. Erklären Sie, warum RB-Insert und RBDelete dieses bh-Attribut verwalten kann, ohne zusätzlichen Speicherplatz in den Knoten des Baumes zu benötigen und ohne die asymptotische Laufzeit zu erhöhen. Zeigen Sie, dass wir, während wir im Baum T absteigen, die SchwarzHöhe jedes besuchten Knotens in Zeit O(1) pro besuchtem Knoten bestimmen können. Wir wollen die Operation RB-Join(T1 , x, T2 ) implementieren, bei der T1 und T2 nicht erhalten bleiben müssen und die einen Rot-Schwarz-Baum T = T1 ∪{x}∪T2 zurückgibt. Sei n die Gesamtanzahl der Knoten in T1 und T2 . b. Nehmen Sie an, dass T1 .bh ≥ T2 .bh gelten würde. Geben Sie einen Algorithmus mit Laufzeit O(lg n) an, der einen schwarzem Knoten y aus T1 bestimmt, der den größten Schlüssel aller Knoten mit der Schwarz-Höhe T2 .bh hat. c. Sei Ty der von y ausgehende Teilbaum. Beschreiben Sie, wie Ty ∪ {x} ∪ T2 in Zeit O(1) den Teilbaum Ty ersetzen kann, ohne die binäre SuchbaumEigenschaft zu verletzen. d. Welche Farbe sollten wir x geben, damit die Rot-Schwarz-Eigenschaften 1, 3 und 5 erhalten bleiben? Beschreiben Sie, wie wir die Eigenschaften 2 und 4 in Zeit O(lg n) erzwingen können. e. Zeigen Sie, dass wir ohne Beschränkung der Allgemeinheit die Annahme in Teil (b) machen können. Beschreiben Sie die symmetrische Situation, die im Falle T1 .bh ≤ T2 .bh vorliegt. f. Zeigen Sie, dass die Laufzeit von RB-Join in O(lg n) liegt. 13-3 AVL Bäume Ein AVL-Baum ist ein binärer Suchbaum, der höhenbalanciert ist: Für jeden Knoten x unterscheiden sich die Höhen des linken und rechten Teilbaums von x höchstens um 1. Um einen AVL-Baum zu implementieren, halten wir in jedem Knoten als zusätzliches Attribut die Höhe x.h des Knotens x. Wie für jeden anderen binären Suchbaum T setzen wir voraus, dass T.wurzel auf den Wurzelknoten zeigt.
Problemstellungen zu Kapitel 13
335
G: 4 B: 7 A: 10
H: 5 E: 23
K: 65 I: 73
Abbildung 13.9: Ein Treap. Jeder Knoten x ist durch x. schl u ¨ ssel : x. priorit¨ a t gekennzeichnet.
a. Beweisen Sie, dass ein AVL-Baum mit n Knoten die Höhe O(lg n) hat. (Hinweis: Beweisen Sie, dass ein AVL-Baum der Höhe h mindestens Fh Knoten enthält, wobei Fh die h-te Fibonacci-Zahl ist.) b. Um einen Knoten in einen AVL-Baum einzufügen, platzieren wir zuerst den Knoten entsprechend der Ordnung für binäre Suchbäume an die passende Stelle. Nach diesem Einfügen ist der Baum möglicherweise nicht mehr höhenbalanciert. Insbesondere können die Höhen der linken und rechten Kinder einiger Knoten sich um 2 unterscheiden. Geben Sie eine Prozedur Balance(x) an, die einen von x ausgehenden Teilbaum, dessen linke und rechte Kinder höhenbalanciert sind und Höhen haben, die sich um höchstens 2 unterscheiden (d. h. |x.rechts.h − x.links.h| ≤ 2), so abändert, dass er dann höhenbalanciert ist. (Hinweis: Verwenden Sie Rotationen.) c. Beschreiben Sie unter Verwendung von Teil (b) eine rekursive Prozedur AVLInsert(x, z), die für einen Knoten x aus einem AVL-Baum und einen neu angelegten Knoten z, den neuen Knoten z so in den von x ausgehenden Teilbaum einfügt, dass x die Wurzel eines AVL-Baumes bleibt. Setzen Sie wie in der Prozedur Tree-Insert aus Abschnitt 12.3 voraus, dass z.schl¨u ssel bereits seinen richtigen Wert besitzt und dass z.links = nil und z.rechts = nil gilt. Setzen Sie außerdem voraus, dass z.h = 0 gilt. Wir rufen also AVL-Insert(T.wurzel, z) auf, um einen Knoten z in einen AVL-Baum einzufügen. d. Zeigen Sie, dass AVL-Insert Zeit O(lg n) benötigt und O(1) viele Rotationen ausführt, wird sie auf einen AVL-Baum mit n Knoten angewendet. 13-4 Treaps Wenn wir in einen binären Suchbaum eine Menge von n Einträgen einfügen, dann kann der daraus resultierende Baum extrem unbalanciert sein, was zu sehr langen Suchzeiten führt. Wie wir jedoch in Abschnitt 12.4 gesehen haben, neigen zufällig erzeugte binäre Suchbäume eher dazu, balanciert zu sein. Deshalb besteht eine Strategie, die durchschnittlich einen balancierten Baum für eine feste Menge von Einträgen konstruiert, darin, die Einträge zufällig zu permutieren und sie anschließend in dieser Reihenfolge in den Baum einzufügen. Was tun, wenn wir nicht alle Einträge auf einmal zur Verfügung haben? Wenn wir die Einträge nacheinander erhalten, können wir dann immer noch einen binären
336
13 Rot-Schwarz-Bäume Suchbaum zufällig aus ihnen erzeugen? Wir werden uns eine Datenstruktur anschauen, mit der wir diese Frage positiv beantworten können. Ein Treap ist ein binärer Suchbaum, in dem die Knoten in einer modifizierten Art und Weise geordnet sind. Abbildung 13.9 zeigt ein Beispiel. Wie gewöhnlich besitzt jeder Knoten x im Baum einen Schlüssel x.schl¨u ssel . Zusätzlich weisen wir jedem Knoten eine unabhängig gewählte Zufallszahl x.priorit¨a t zu. Wir setzen voraus, dass alle Prioritäten sowie alle Schlüssel paarweise verschieden sind. Die Knoten des Treaps werden so geordnet, dass die Schlüssel die binäre Suchbaum-Eigenschaft und die Prioritäten die Min-HeapEigenschaft erfüllen: • Wenn v ein linkes Kind von u ist, dann gilt schl¨u ssel [v] < schl¨u ssel [u]. • Wenn v ein rechtes Kind von u ist, dann gilt schl¨u ssel [v] > schl¨u ssel [u]. • Wenn v ein Kind von u ist, dann gilt v.priorit¨a t > u.priorit¨a t . (Diese Kombination der Eigenschaften ist der Grund dafür, warum wir den Baum als „Treap“ bezeichnen. Er besitzt sowohl Eigenschaften eines binären Suchbaumes (engl.: binary search tree) als auch eines Heaps.) Es hilft, sich die Treaps in folgender Weise vorzustellen. Nehmen Sie an, wir würden die Knoten x1 , x2 , . . . , xn mit den zugehörigen Schlüsseln in einen Treap einfügen. Dann ist der resultierende Treap derjenige Baum, den wir erhalten hätten, wenn wir die Knoten in der Reihenfolge ihrer (zufällig gewählten) Prioritäten in einen gewöhnlichen binären Suchbaum eingefügt hätten. Die Bedingung xi .priorit¨a t < xj .priorit¨a t bedeutet also, dass xi vor xj eingefügt worden wäre. a. Zeigen Sie, dass der Treap für eine gegebene Menge von Knoten x1 , x2 , . . . , xn mit zugehörigen Schlüsseln und Prioritäten, die jeweils paarweise verschieden sind, eindeutig ist. b. Zeigen Sie, dass die erwartete Höhe eines Treaps in Θ(lg n) und somit die für das Suchen nach einen Wert im Treap erwartete Zeit in Θ(lg n) liegt. Lassen Sie uns nun anschauen, wie ein neuer Knoten in einen existierenden Treap eingefügt wird. Zuerst weisen wir dem neuen Knoten eine zufällige Priorität zu. Danach rufen wir den Algorithmus zum Einfügen auf, den wir mit Treap-Insert bezeichnen und dessen Arbeitsweise in Abbildung 13.10 illustriert ist. c. Erklären Sie, wie die Prozedur Treap-Insert arbeitet. Erklären Sie die Idee in Worten und geben Sie ein Programm in Pseudocode an. (Hinweis: Führen Sie die übliche Prozedur zum Einfügen eines Schlüssels in einen binären Suchbaum aus und wenden Sie dann Rotationen an, um die Min-Heap-Eigenschaft wiederherzustellen.) d. Zeigen Sie, dass die erwartete Laufzeit von Treap-Insert in Θ(lg n) liegt. Die Prozedur Treap-Insert führt eine Suche und anschließend eine Reihe von Rotationen durch. Obwohl diese zwei Operationen die gleiche erwartete Laufzeit besitzen, verursachen sie in der Praxis unterschiedlichen Aufwand. Eine Suche
Problemstellungen zu Kapitel 13
337
G: 4
G: 4 B: 7 A: 10
C: 25
H: 5 E: 23
B: 7 A: 10
K: 65
(b)
G: 4
G: 4 B: 7
H: 5 E: 23
C: 25
A: 10
K: 65
K: 65 I: 73
(c)
(d)
G: 4
F: 2
B: 7
H: 5 D: 9
C: 25
E: 23
C: 25
D: 9
A: 10
H: 5
D: 9
I: 73
K: 65 I: 73
(a)
B: 7 A: 10
E: 23 C: 25
I: 73
D: 9
H: 5
F: 2 K: 65
E: 23
I: 73
… A: 10
B: 7
G: 4 D: 9
C: 25
H: 5
E: 23
K: 65 I: 73
(e)
(f)
Abbildung 13.10: Die Arbeitsweise von Treap-Insert. (a) Der ursprüngliche Treap vor dem Einfügen. (b) Der Treap nach dem Einfügen des Knotens mit dem Schlüssel C und der Priorität 25. (c)-(d) Zwischenzustände während des Einfügens eines Knotens mit dem Schlüssel D und der Priorität 9. (e) Der Treap, nachdem das Einfügen aus den Teilen (c) und (d) abgeschlossen ist. (f ) Der Treap nach dem Einfügen eines Knotens mit dem Schlüssel F und der Priorität 2.
338
13 Rot-Schwarz-Bäume 15 9
3
15 18
9
12
25
6
21 (a)
3
18 12
25
6
21 (b)
Abbildung 13.11: Rückgrate eines binären Suchbaumes. Das linke Rückgrat ist in (a) schattiert und das rechte Rückgrat ist in (b) schattiert.
liest Informationen aus dem Treap, ohne ihn zu modifizieren. Im Gegensatz dazu ändert eine Rotation die Zeiger von Vater und Kind innerhalb des Treaps. Auf den meisten Rechnern sind die Lese-Operationen viel schneller als die Schreibzugriffe. Deshalb wäre es wünschenswert, wenn Treap-Insert nur wenige Rotationen ausführt. Wir werden zeigen, dass die erwartete Anzahl der auszuführenden Rotationen durch eine Konstante beschränkt ist. Um dies zu beweisen, benötigen wir einige Definitionen, die in Abbildung 13.11 illustriert sind. Das linke Rückgrat eines binären Suchbaumes T ist der einfache Pfad von der Wurzel zum Knoten mit dem kleinsten Schlüssel. Mit anderen Worten, das linke Rückgrat ist der von der Wurzel ausgehende einfache Pfad, der nur aus linken Kanten besteht. Analog ist das rechte Rückgrat von T als der von der Wurzel ausgehende einfache Pfad definiert, der nur aus rechten Kanten besteht. Die Länge eines Rückgrats ist die Anzahl der Knoten, die dieses enthält. e. Betrachten Sie den Treap T unmittelbar, nachdem Treap-Insert den Knoten x eingefügt hat. Sei C die Länge des rechten Rückgrats des linken Teilbaumes von x. Sei D die Länge des linken Rückgrats des rechten Teilbaumes von x. Beweisen Sie, dass die Gesamtanzahl der Rotationen, die während des Einfügens von x durchgeführt wurden, gleich C + D ist. Wir werden nun die Erwartungswerte von C und D berechnen. Da wir die Schlüssel lediglich untereinander vergleichen, können wir ohne Beschränkung der Allgemeinheit voraussetzen, dass die Schlüssel die Werte 1, 2, . . . , n haben. Für die Knoten x und y im Treap T mit y = x sei k = x.schl¨u ssel und i = y.schl¨u ssel . Wir definieren die Indikatorfunktionen Xik = I {y liegt im rechten Rückgrat des linken Teilbaums von x} . f. Zeigen Sie, dass Xik = 1 genau dann gilt, wenn y.priorit¨a t > x.priorit¨a t und y.schl¨u ssel < x.schl¨u ssel gelten, und dass für jeden Knoten z mit y.schl¨u ssel < z.schl¨u ssel < x.schl¨u ssel die Ungleichung y.priorit¨a t < z.priorit¨a t gilt.
Kapitelbemerkungen zu Kapitel 13
339
g. Zeigen Sie (k − i − 1)! (k − i + 1)! 1 . = (k − i + 1)(k − i)
Pr {Xik = 1} =
h. Zeigen Sie E [C] =
k−1 j=1
=1−
1 j(j + 1) 1 . k
i. Verwenden Sie ein symmetrisches Argument, um E [D] = 1 −
1 n−k+1
zu zeigen. j. Folgern Sie, dass die erwartete Anzahl der Rotationen, die während des Einfügens eines Knotens in den Treap ausgeführt werden, kleiner als 2 ist.
Kapitelbemerkungen Die Idee des Balancierens eines Suchbaumes geht auf Adel’son-Vel’ski˘ı und Landis [2] zurück, die 1962 eine als „AVL-Bäume“ bezeichnete Klasse balancierter Suchbäume eingeführt haben. Diese wurden in der Problemstellung 13-3 beschrieben. Eine andere Klasse von Suchbäumen, die als „2-3 Bäume“ bezeichnet werden, wurde 1970 von J. E. Hopcroft (unveröffentlicht) eingeführt. Ein 2-3 Baum hält die Balance durch Manipulieren des Grades der Knoten im Baum. Kapitel 18 behandelt eine Verallgemeinerung von 2-3 Bäumen, die B-Bäume, die von Bayer und McCreight [35] eingeführt worden sind. Rot-Schwarz-Bäume wurden von Bayer [34] unter der Bezeichnung „symmetrische binäre B-Bäume“ entwickelt. Guibas and Sedgewick [155] studierten deren Eigenschaften ausführlich und führten die rot/schwarz-Farbkonvention ein. Andersson [15] hat eine einfacher zu programmierende Variante der Rot-Schwarz-Bäume vorgeschlagen. Weiss [351] bezeichnet diese Variante als AA-Bäume. Ein AA-Baum ist ein dem Rot-SchwarzBaum ähnlicher Baum, mit dem Unterschied, dass die linken Kinder niemals rot sind. Treaps, die wir in der Problemstellung 13-4 vorgestellt haben, wurden von Seidel und Aragon [309] vorgeschlagen. Sie gehören zur Standardimplementierung eines Wörterbuchs in LEDA [253], das eine gut implementierte Sammlung von Datenstrukturen und Algorithmen bereitstellt. Es gibt viele andere Variationen balancierter binärer Suchbäume, zum Beispiel gewichtsbalancierte Bäume [264], k-Nachbar-Bäume [245] und Scapegoat-Bäume [127]. Die vielleicht faszinierendsten sind die von Sleator und Tarjan [320] eingeführten „Splay-Baum“,
340
13 Rot-Schwarz-Bäume
die „selbstregulierend“ sind. (Eine gute Beschreibung der Splay-Bäume bietet Tarjan [330]). Splay-Bäume halten die Balance ohne jede explizite Balance-Bedingung, wie zum Beispiel Farbe. Stattdessen werden im Baum jedes Mal „Splay-Operationen“ (die Rotationen einschließen) durchgeführt, wenn ein Zugriff erfolgt. Die amortisierten Kosten (siehe Kapitel 17) jeder Operation auf einem Baum mit n Knoten sind O(lg n). Skip-Listen [286] bilden eine Alternative zu balancierten binären Suchbäumen. Eine Skip-Liste ist eine verkettete Liste, die um zusätzliche Zeiger erweitert ist. Jede Wörterbuch-Operation läuft auf einer Skip-Liste mit n Einträgen in erwarteter Zeit O(lg n).
14
Erweitern von Datenstrukturen
Beim Entwickeln von Software kommen wir in manchen Fällen exakt mit den Datenstrukturen aus, die in Fachbüchern beschrieben sind, viele andere Fälle benötigen jedoch eine Spur Kreativität, wenngleich es nur in seltenen Fällen nötig sein wird, dass Sie einen vollständig neuen Typ von Datenstruktur entwickeln müssen. Häufig reicht es aus, eine Datenstruktur aus einem Lehrbuch zu erweitern, indem man zusätzliche Informationen in ihr speichert. Man kann dann neue Operationen für die Datenstruktur programmieren, die die gewünschte Anwendung unterstützen. Das Erweitern einer Datenstruktur ist jedoch nicht ganz einfach, da die zusätzlichen Informationen von den gewöhnlichen Operationen auf einer Datenstruktur aktualisiert und verwaltet werden müssen. Dieses Kapitel diskutiert zwei Datenstrukturen, die wir erhalten, wenn wir Rot-SchwarzBäume geeignet erweitern. Abschnitt 14.1 beschreibt eine Datenstruktur, die allgemeine Operationen für die Ranggrößen auf einer dynamischen Menge unterstützt. Wir können damit schnell die i-kleinste Zahl in einer Menge finden oder den Rang eines gegebenen Elementes bezüglich der totalen Ordnung der Menge bestimmen. Abschnitt 14.2 abstrahiert den Prozess des Erweiterns einer Datenstruktur und liefert ein Theorem, das das Erweitern von Rot-Schwarz-Bäumen vereinfacht. Abschnitt 14.3 verwendet dieses Theorem als Hilfsmittel, um eine Datenstruktur zu entwerfen, die eine dynamische Menge von Intervallen – wie zum Beispiel von Zeitintervallen – verwaltet. Diese Datenstruktur erlaubt es zum Beispiel, ein Intervall aus der Menge zu finden, das ein gegebenes Intervall schneidet.
14.1
Dynamische Ranggröße
In Kapitel 9 wurde der Begriff der Ranggröße eingeführt. Die i-te Ranggröße einer Menge von n Elementen ist einfach das Element der Menge mit dem i-kleinsten Schlüssel, wobei i ∈ {1, 2, . . . , n}. Wir haben gesehen, wie eine Ranggröße in Zeit O(n) aus einer ungeordneten Menge bestimmt werden kann. In diesem Abschnitt werden wir sehen, wie wir Rot-Schwarz-Bäume zu modifizieren haben, sodass wir eine Ranggröße in Zeit O(lg n) in einer dynamischen Menge bestimmen können. Wir werden auch sehen, wie der Rang eines Elementes – seine Position innerhalb der linearen Reihenfolge der Menge – in Zeit O(lg n) bestimmt werden kann. Abbildung 14.1 stellt eine Datenstruktur, die schnelle Ranggrößen-Operationen unterstützen kann, dar. Ein Ranggrößen-Baum T ist ein Rot-Schwarz-Baum, in dessen Knoten zusätzliche Informationen gespeichert sind. Neben den üblichen Attributen x.schl¨u ssel , x.farbe, x.vater , x.links und x.rechts für einen Knoten x des Rot-SchwarzBaumes, gibt es ein zusätzliches Attribut x.gr¨o ß e. Dieses Attribut enthält die Anzahl der (inneren) Knoten des von x ausgehenden Teilbaumes (einschließlich x selbst), d. h.
342
14 Erweitern von Datenstrukturen 26 20
17
41
12
21
30
7
4
5
10 4
3
7
14 16
19
2
2
7
12
14
20
2
1
1
1
21
28
1
1
schlüssel größe
47 1
38 3
35
39
1
1
1
Abbildung 14.1: Ein Ranggrößen-Baum, der ein erweiterter Rot-Schwarz-Baum ist. Schattierte Knoten sind rot und geschwärzte Knoten sind schwarz. Zusätzlich zu den üblichen Attributen besitzt jeder Knoten x ein Attribut x. gr¨ o ß e, das die Anzahl der Knoten des von x ausgehenden Teilbaums ohne den Wächter angibt.
die Größe seines Teilbaumes. Wenn wir die Größe des Wächters als 0 definieren, d. h. wenn wir T.nil .gr¨o ß e auf 0 setzen, dann gilt die Identität x.gr¨o ß e = x.links.gr¨o ß e + x.rechts.gr¨o ß e + 1 . Wir fordern nicht, dass die Schlüssel in einem Ranggrößen-Baum paarweise verschieden sind. (Beispielsweise hat der Baum in Abbildung 14.1 zwei Schlüssel mit dem Wert 14 und zwei Schlüssel mit dem Wert 21.) Wenn gleiche Schlüssel existieren, dann ist der obige Begriff des Ranges nicht wohldefiniert. Wir beseitigen die Mehrdeutigkeit für einen Ranggrößen-Baum dadurch, dass wir den Rang eines Elementes als die Position definieren, an der es bei einer Inorder-Traversierung ausgegeben würde. In Abbildung 14.1 hat beispielsweise der in einem schwarzen Knoten gespeicherte Schlüssel 14 den Rang 5 und der in einem roten Knoten gespeicherte Schlüssel 14 den Rang 6.
Finden eines Elementes mit einem gegebenen Rang Bevor wir zeigen, wie wir diese Größeninformation während des Einfügens und Entfernens von Knoten aufrechterhalten, schauen wir uns die Implementierung von zwei Anfragen zu Ranggrößen, die diese zusätzliche Information verwenden, näher an. Wir beginnen mit einer Operation, die das Element mit einem gegebenen Rang sucht. Die Prozedur OS-Select(x, i) gibt einen Zeiger auf den Knoten zurück, der den i-kleinsten Schlüssel des von x ausgehenden Teilbaumes enthält. Um diesen Knoten in einem Ranggrößen-Baum T zu finden, rufen wir OS-Select(T.wurzel , i) auf. OS-Select(x, i) 1 r = x.links.gr¨o ß e + 1 2 if i = = r 3 return x 4 elseif i < r 5 return OS-Select(x.links, i) 6 else return OS-Select(x.rechts, i − r)
14.1 Dynamische Ranggröße
343
In Zeile 1 von OS-Select berechnen wir r, den Rang des Knotens x in dem von x ausgehenden Teilbaum. Der Wert von x.links.gr¨o ß e entspricht der Anzahl der Knoten, die bei einer Inorder-Traversierung des von x ausgehenden Teilbaumes vor x ausgegeben werden. Somit ist x.links.gr¨o ß e + 1 der Rang von x innerhalb des von x ausgehenden Teilbaums. Im Falle i = r ist der Knoten x das i-kleinste Element, sodass in Zeile 3 der Knoten x zurückgegeben wird. Im Falle i < r befindet sich die i-te Ranggröße im linken Teilbaum von x und so steigen wir in Zeile 5 rekursiv in den linken Teilbaum x.links ab. Im Falle i > r befindet sich die i-te Ranggröße im rechten Teilbaum von x. Da es in dem von x ausgehenden Teilbaum r Elemente gibt, die bei einer InorderTraversierung vor dem rechten Teilbaum von x vorkommen, ist die i-te Ranggröße in dem von x ausgehenden Teilbaum die (i − r)-te Ranggröße in dem von x.rechts ausgehenden Teilbaum. Zeile 6 bestimmt diesen Wert rekursiv. Um zu sehen, wie OS-Select arbeitet, betrachten wir die Suche nach der 17-ten Ranggröße im Ranggrößen-Baum aus Abbildung 14.1. Wir beginnen mit demjenigen x als Wurzel, dessen Schlüssel 26 ist, und mit i = 17. Da die Größe des linken Teilbaums von 26 den Wert 12 hat, ist der Rang von x gleich 13. Damit wissen wir, dass der Knoten mit dem Rang 17 die 17 − 13 = 4-te Ranggröße im rechten Teilbaum von 26 ist. Nach dem rekursiven Aufruf ist x der Knoten mit dem Schlüssel 41 und i = 4. Da die Größe des linken Teilbaums des Knotens x = 41 gleich 5 ist, ist der Rang von x in seinem Teilbaum 6. Damit wissen wir, dass der Knoten mit dem Rang 4 die 4-te Ranggröße im linken Teilbaum von 41 ist. Nach dem entsprechenden rekursiven Aufruf ist x der Knoten mit dem Schlüssel 30 und sein Rang in seinem Teilbaum ist 2. Somit steigen wir erneut rekursiv im Baum ab, um die 4 − 2 = 2-te Ranggröße des Teilbaumes zu finden, der vom Knoten mit dem Schlüssel 38 ausgeht. Wir stellen nun fest, dass sein linker Teilbaum die Größe 1 hat, was bedeutet, dass dieser Knoten das zweitkleinste Element ist. Somit gibt die Prozedur einen Zeiger auf den Knoten mit dem Schlüssel 38 zurück. Da jeder rekursive Aufruf im Ranggrößen-Baum eine Ebene nach unten geht, ist die Gesamtlaufzeit für OS-Select im schlechtesten Fall proportional zur Höhe des Baumes. Weil der Baum ein Rot-Schwarz-Baum ist, beträgt seine Höhe O(lg n), wobei n die Anzahl der Knoten ist. Somit liegt die Laufzeit von OS-Select für eine dynamische Menge aus n Elementen in O(lg n).
Bestimmen des Ranges eines Elementes Gegeben sei ein Zeiger auf einen Knoten x in einem Ranggrößen-Baum T . Die Prozedur OS-Rank gibt die Position von x in der linearen Reihenfolge zurück, die durch eine Inorder-Traversierung von T bestimmt werden würde. OS-Rank(T, x) 1 r = x.links.gr¨o ß e + 1 2 y =x 3 while y = T.wurzel 4 if y == y.vater .rechts 5 r = r + y.vater .links.gr¨o ß e + 1 6 y = y.vater 7 return r
344
14 Erweitern von Datenstrukturen
Die Prozedur arbeitet wie folgt. Wir können uns den Rang von Knoten x als 1 plus die Anzahl der Knoten vorstellen, die vor x in einer Inorder-Traversierung besucht werden. Die Prozedur OS-Rank verwendet die folgende Schleifeninvariante: Zu Beginn jeder Iteration der while-Schleife in den Zeilen 3–6 ist r der Rang von x.schl¨u ssel in dem vom Knoten y ausgehenden Teilbaum. Wir benutzen diese Schleifenvariante, um zu zeigen, dass OS-Rank korrekt arbeitet: Initialisierung: Vor der ersten Iteration erhält r in Zeile 1 als Wert den Rang von x.schl¨u ssel in dem von x ausgehenden Teilbaum. Durch die Zuweisung y = x in Zeile 2 wird die Invariante wahr, wenn der Test in Zeile 3 das erste Mal ausgeführt wird. Fortsetzung: Am Ende jeder Iteration der while-Schleife setzen wir y = y.vater . Daher müssen wir zeigen, dass r am Ende des Schleifenkörpers der Rang von x.schl¨u ssel in dem von y.vater ausgehenden Teilbaum ist, falls r zu Beginn des Schleifenkörpers der Rang von x.schl¨u ssel in dem von y ausgehenden Teilbaum ist. Bei jeder Iteration der while-Schleife betrachten wir den von y.vater ausgehenden Teilbaum. Wir haben bereits die Anzahl der Knoten in dem von Knoten y ausgehenden Teilbaum bestimmt, die bei einer Inorder-Traversierung vor x liegen. Somit müssen wir die Anzahl der Knoten, die in dem vom Bruder von y ausgehenden Teilbaum enthalten sind und bei einer Inorder-Traversierung vor x liegen, aufaddieren, plus 1 für y.vater , wenn dieser ebenfalls vor x steht. Wenn y ein linkes Kind ist, dann steht weder y.vater noch irgendein Knoten im rechten Teilbaum von y.vater vor x, daher lassen wir r unverändert. Anderenfalls ist y ein rechtes Kind und alle Knoten im linken Teilbaum von y.vater stehen genau wie y.vater selbst vor dem Knoten x. Daher addieren wir in Zeile 5 den Wert y.vater .links.gr¨o ß e + 1 zum aktuellen Wert von r. Terminierung: Die Schleife terminiert, wenn y = T.wurzel gilt, sodass der von y ausgehende Teilbaum der gesamte Baum ist. Somit ist der Wert von r der Rang von x.schl¨u ssel im gesamten Baum. Wenn wir OS-Rank beispielsweise auf dem Ranggrößen-Baum in Abbildung 14.1 laufen lassen, um den Rang des Knotens mit dem Schlüssel 38 zu bestimmen, erhalten wir die folgende Folge von Werten für y.schl¨u ssel und r jeweils zu Beginn der while-Schleife: Iteration 1 2 3 4
y.schl¨u ssel 38 30 41 26
r 2 4 4 17
Die Prozedur gibt als Rang den Wert 17 zurück. Da jede Iteration der while-Schleife Zeit O(1) benötigt und y mit jeder Iteration im Baum eine Ebene aufwärts bewegt wird, ist die Laufzeit von OS-Rank im schlechtesten Fall proportional zur Höhe des Baumes, also O(lg n) auf einem Ranggrößen-Baum mit n Knoten.
14.1 Dynamische Ranggröße
93 19
42 11
LEFT-ROTATE(T, x)
42
y
19
x 93
x 7
6
345
RIGHT-ROTATE(T, y)
12
y
6
4
4
7
Abbildung 14.2: Das Aktualisieren von Teilbaumgrößen während der Rotationen. Die Verbindung, um die wir rotieren, ist inzident zu den beiden Knoten, deren größe-Attribute aktualisiert werden müssen. Die Aktualisierungen sind lokal und benötigen nur die Werte des Attributs größe der Knoten x, y und der Wurzeln der durch die Dreiecke gekennzeichneten Teilbäume.
Verwaltung der Teilbaumgrößen Ist das Attribut größe für jeden Knoten gegeben, können die Prozeduren OS-Select und OS-Rank die Ranggrößeninformationen schnell berechnen. Wenn aber diese Attribute von den modifizierenden Grundoperationen auf Rot-Schwarz-Bäumen, dem Einfügen oder dem Entfernen eines Knotens, nicht unterstützt werden würden, wäre unsere Arbeit umsonst gewesen. Wir werden nun sehen, wie die Teilbaumgrößen sowohl während des Einfügens als auch während des Löschens korrekt verwaltet werden können, ohne die asymptotische Laufzeit einer der beiden Operationen zu berühren. Wir haben in Abschnitt 13.3 festgestellt, dass das Einfügen in einen Rot-Schwarz-Baum aus zwei Phasen besteht. In der ersten Phase steigen wir von der Wurzel aus ab, wobei wir den neuen Knoten als ein Kind eines bereits existierenden Knotens einfügen. In der zweiten Phase steigen wir den Baum aufwärts, wobei wir Farben ändern und Rotationen ausführen, um die Rot-Schwarz-Eigenschaften wiederherzustellen. Um die Teilbaumgrößen in der ersten Phase korrekt zu verwalten, inkrementieren wir auf dem einfachen Pfad, den wir von der Wurzel bis zu den Blättern traversieren, einfach x.gr¨o ß e für jeden Knoten x. Der neu hinzugefügte Knoten erhält eine Größe größe von 1. Da sich auf dem traversierten Pfad O(lg n) Knoten befinden, betragen die zusätzlichen Kosten für das Verwalten des Attributs größe O(lg n). In der zweiten Phase werden die einzigen strukturellen Veränderungen des zugrunde liegenden Rot-Schwarz-Baumes durch Rotationen bewirkt, von denen es höchstens zwei gibt. Darüber hinaus ist eine Rotation eine lokale Operation: Lediglich für zwei Knoten werden die Attribute größe ungültig. Die Verbindung, um die die Rotation ausgeführt wird, besteht zwischen diesen beiden Knoten. Wir fügen dem Code von Left-Rotate(T, x) in Abschnitt 13.2 die folgenden Zeilen hinzu: 13 y.gr¨o ß e = x.gr¨o ß e 14 x.gr¨o ß e = x.links.gr¨o ß e + x.rechts.gr¨o ß e + 1 Abbildung 14.2 illustriert, wie die Attribute aktualisiert werden. Die Veränderung in Right-Rotate gestaltet sich symmetrisch. Da höchstens zwei Rotationen während des Einfügens in einen Rot-Schwarz-Baum ausgeführt werden, spendieren wir für das Aktualisieren der Attribute größe in der zweiten
346
14 Erweitern von Datenstrukturen
Phase nur Zeit O(1). Damit ist die Gesamtzeit für das Einfügen eines Elementes in einen Ranggrößen-Baum mit n Knoten in O(lg n), was asymptotisch die gleiche Laufzeit wie für einen gewöhnlichen Rot-Schwarz-Baum ist. Das Löschen aus einem Rot-Schwarz-Baum besteht ebenfalls aus zwei Phasen. Die erste arbeitet auf dem zugrunde liegenden Suchbaum, und die zweite verursacht höchstens drei Rotationen und führt ansonsten keine strukturellen Veränderungen durch (siehe Abschnitt 13.4). Die erste Phase entfernt entweder einen Knoten oder bewegt ihn innerhalb des Baumes nach oben. Um die Teilbaumgröße zu aktualisieren, traversieren wir den einfachen Pfad vom Knoten y (wobei wir von der ursprünglichen Position von y starten) bis zur Wurzel, wobei wir das Attribut größe jedes Knotens auf dem Pfad dekrementieren. Da dieser Pfad in einem Rot-Schwarz-Baum mit n Knoten die Länge O(lg n) besitzt, ist die in der ersten Phase zum Aktualisieren der Attribute größe aufgewendete Zeit in O(lg n). Wir behandeln die O(1) vielen Rotationen in der zweiten Phase genauso, wie wir dies beim Einfügen tun. Somit laufen auf einem Ranggrößen-Baum mit n Knoten sowohl das Einfügen als auch das Entfernen, einschließlich des Aktualisierens der Attribute größe, in Zeit O(lg n).
Übungen 14.1-1 Illustrieren Sie, wie OS-Select(T.wurzel, 10) auf dem Rot-Schwarz-Baum T aus Abbildung 14.1 arbeitet. 14.1-2 Illustrieren Sie, wie OS-Rank(T, x) auf dem Rot-Schwarz-Baum T aus Abbildung 14.1 für den Knoten x mit x.schl¨u ssel = 35 arbeitet. 14.1-3 Schreiben Sie eine nichtrekursive Version von OS-Select. 14.1-4 Schreiben Sie eine rekursive Prozedur OS-Key-Rank(T, k), die als Eingabe einen Ranggrößenbaum T und einen Schlüssel k erhält und den Rang von k in der durch T repräsentierten dynamischen Menge zurückgibt. Gehen Sie davon aus, dass die Schlüssel von T paarweise verschieden sind. 14.1-5 Gegeben seien ein Element x in einem Ranggrößen-Baum mit n Knoten und eine natürliche Zahl i. Wie können wir den i-ten Nachfolger von x bezüglich der durch den Baum definierten linearen Ordnung in Zeit O(lg n) bestimmen? 14.1-6 Überprüfen Sie für sich, dass, wenn immer wir in den Prozeduren OS-Select oder OS-Rank auf das Attribut größe eines Knotens zugreifen, so tun wir das nur, um den Rang eines Elementes zu berechnen. Nehmen Sie entsprechend dieser Beobachtung an, wir würden in jedem Knoten den Rang des Knotens bezogen auf den Teilbaum, von dem er die Wurzel ist, speichern. Zeigen Sie, wie diese Information beim Einfügen und Entfernen aktualisiert werden kann. (Denken Sie daran, dass die zwei Operationen jeweils Rotationen verursachen können.) 14.1-7 Zeigen Sie, wie man einen Ranggrößen-Baum dazu verwenden kann, die Zahl der Inversionen (siehe Problemstellung 2-4) in einem Feld der Größe n in Zeit O(n lg n) zu berechnen.
14.2 Wie man eine Datenstruktur erweitert
347
14.1-8∗ Betrachten Sie n Sehnen in einem Kreis, von denen jede durch ihre Endpunkte definiert ist. Geben Sie einen Algorithmus mit Laufzeit O(n lg n) an, der die Anzahl der Sehnenpaare bestimmt, die sich innerhalb des Kreises schneiden. (Wenn zum Beispiel alle n Sehnen Durchmesser sind, die sich im Mittelpunkt treffen, dann lautet die korrekte Antwort n2 .) Gehen Sie davon aus, dass keine zwei Sehnen einen gemeinsamen Endpunkt haben.
14.2
Wie man eine Datenstruktur erweitert
Der Prozess, eine elementare Datenstruktur so zu erweitern, dass diese zusätzliche Funktionalitäten unterstützt, tritt beim Entwurf von Algorithmen ziemlich häufig auf, so zum Beispiel auch im nächsten Abschnitt, wo es darum geht, eine Datenstruktur zu entwerfen, die spezielle Operationen auf Intervallen unterstützt. In diesem Abschnitt werden wir uns die in einem solchen Prozess vorkommenden Schritte näher anschauen. Wir werden außerdem ein Theorem beweisen, das es uns erlaubt, Rot-Schwarz-Bäume in vielen Fällen in einfacher Art und Weise zu erweitern. Wir können den Prozess, der bei einer Erweiterung einer Datenstruktur durchlaufen wird, in vier Schritte aufteilen: 1. Wählen Sie eine zugrunde liegende Datenstruktur aus; 2. Legen Sie die Informationen fest, die zusätzlich in der zugrunde liegenden Datenstruktur gespeichert werden sollen; 3. Überprüfen Sie, ob wir auf der zugrunde liegenden Datenstruktur die zusätzlichen Informationen während den modifizierenden Grundoperationen verwalten können; 4. Entwickeln Sie die neuen Operationen. Wie bei jeder vorgeschriebenen Entwurfstechnik sollten Sie nicht blind den Arbeitschritten in der gegebenen Reihenfolge folgen. Der größte Teil der Entwurfsarbeit ist ein „trial and error“-Prozess und die Fortschritte laufen gewöhnlich in allen Arbeitsschritten parallel ab. Es hat zum Beispiel keinen Sinn, zusätzliche Informationen festzulegen und neue Operationen zu entwickeln (Arbeitsschritte 2 und 4), wenn wir nicht in der Lage sind, die zusätzlichen Informationen effizient zu verwalten. Dennoch bietet diese VierSchritt-Methode eine gute Richtlinie für die erfolgreiche Erweiterung einer Datenstruktur. Sie stellt außerdem eine gute Möglichkeit dar, die Dokumentation einer erweiterten Datenstruktur zu organisieren. Wir sind diesen Arbeitsschritten in Abschnitt 14.1 gefolgt, um unsere RanggrößenBäume zu entwerfen. Für Schritt 1 haben wir Rot-Schwarz-Bäume als die zu Grunde liegende Datenstruktur gewählt. Ein Anhaltspunkt dafür, dass Rot-Schwarz-Bäume geeignet sind, ergibt sich aus deren Effizienz bei der Unterstützung anderer Operationen auf vollständig geordneten dynamischen Mengen, wie zum Beispiel Minimum, Maximum, Successor und Predecessor.
348
14 Erweitern von Datenstrukturen
In Schritt 2 haben wir der Datenstruktur das Attribut größe hinzugefügt, in dem für jeden Knoten x die Größe des von x ausgehenden Teilbaumes gespeichert ist. Im Allgemeinen macht die zusätzliche Information Operationen effizienter. Zum Beispiel hätten wir OS-Select und OS-Rank unter alleiniger Verwendung der im Baum gespeicherten Schlüssel implementieren können, sie wären dann aber nicht in Zeit O(lg n) gelaufen. Manchmal besteht die zusätzliche Information aus einem Zeiger anstatt aus Daten, was zum Beispiel in Übung 14.2-1 der Fall ist. In Schritt 3 haben wir sichergestellt, dass Einfügen und Entfernen das Attribut größe unterstützen, wobei diese Operationen immer noch in Zeit O(lg n) laufen. Idealerweise sollten wir jeweils nur einige wenige Elemente der Datenstruktur verändern müssen, um die zusätzliche Information korrekt zu verwalten. Wenn wir zum Beispiel einfach in jedem Knoten dessen Rang innerhalb des Baumes speichern, würden die Prozeduren OS-Select und OS-Rank schnell laufen, aber das Einfügen eines neuen minimalen Elementes würde eine Veränderung dieser Information an jedem Knoten des Baumes verursachen. Wenn wir stattdessen die Größe der Teilbäume speichern, macht das Einfügen eines neuen minimalen Elementes nur die Veränderung der Information in O(lg n) Knoten notwendig. In Schritt 4 haben wir die Operationen OS-Select und OS-Rank entwickelt. Im Grunde genommen ist es in erster Linie der Bedarf an neuen Operationen, weshalb wir uns die Mühe machen, eine Datenstruktur zu erweitern. Gelegentlich benutzen wir wie in Übung 14.2-1 die zusätzliche Information dazu, bereits existierende Operationen zu beschleunigen anstatt neue zu entwickeln.
Erweitern von Rot-Schwarz-Bäumen Wenn Rot-Schwarz-Bäume einer erweiterten Datenstruktur zugrunde liegen, können wir beweisen, dass Einfügen und Entfernen immer bestimmte Arten von zusätzlicher Information effizient verwalten können, was Schritt 3 sehr einfach macht. Der Beweis des folgenden Theorems ähnelt dem Argument aus Abschnitt 14.1, dass wir das Attribut größe in Ranggrößen-Bäumen effizient verwalten können. Theorem 14.1: (Erweitern eines Rot-Schwarz-Baumes) Sei f ein Attribut, das einen Rot-Schwarz-Baum T mit n Knoten erweitert. Setzen Sie voraus, dass der Wert von f für einen Knoten x nur von den an x, x.links und x.rechts abgespeicherten Informationen, eventuell auch noch von x.links .f und x.rechts.f abhängt. Dann können wir die Werte von f in allen Knoten von T während des Einfügens und des Entfernens verwalten, ohne die asymptotische O(lg n)Performanz dieser Operationen zu beeinflussen. Beweis: Die wesentliche Idee des Beweises besteht darin, dass sich eine Veränderung eines Attributs f in einem Knoten nur über die Vorfahren von x im Baum ausbreitet. Das heißt, dass eine Änderung von x.f das Aktualisieren von x.vater .f erfordern kann, sonst aber nichts. Die Aktualisierung von x.vater .f kann eine Aktualisierung von x.vater .vater .f zur Folge haben. Dies setzt sich über den gesamten Baum fort. Wenn
14.2 Wie man eine Datenstruktur erweitert
349
einmal T.wurzel.f aktualisiert wurde, dann hängt kein anderer Knoten mehr von dem neuen Wert ab und der Prozess terminiert. Da die Höhe eines Rot-Schwarz-Baumes O(lg n) ist, kostet das Verändern eines Attributes f an einem Knoten Zeit O(lg n) für das Aktualisieren der von der Veränderung abhängigen Knoten. Das Einfügen eines Knotens x in T besteht aus zwei Phasen (siehe Abschnitt 13.3). Die erste Phase fügt x als Kind eines bereits existierenden Knotens x.vater ein. Der Wert von x.f kann in Zeit O(1) berechnet werden, da er nach Voraussetzung nur von der Information in anderen Attributen von x selbst und den Informationen der Kinder von x abhängt. Beide Kinder von x sind aber der Wächterknoten T.nil. Nachdem x.f berechnet wurde, propagiert die Änderung auf dem Baum nach oben. Somit ist die benötigte Gesamtzeit der ersten Phase von Einfügen O(lg n). Während der zweiten Phase entstehen die einzigen strukturellen Veränderungen durch Rotationen. Da jeweils nur zwei Knoten während einer Rotation verändert werden, ist die Gesamtzeit für das Aktualisieren des Attributes f pro Rotation O(lg n). Die Anzahl der Rotationen während des Einfügens ist höchstens zwei. Deshalb ist die Gesamtzeit für Einfügen O(lg n). So wie das Einfügen besteht auch das Entfernen aus genau zwei Phasen (siehe Abschnitt 13.4). In der ersten Phase treten Veränderungen auf, wenn der gelöschte Knoten aus dem Baum entfernt wird. Falls der gelöschte Knoten zu dieser Zeit zwei Kinder hatte, dann nimmt sein Nachfolger die Position des gelöschten Knotens ein. Die Propagation der Änderung von f , die durch diese Modifikation verursacht wird, kostet höchstens Zeit O(lg n), da der Baum nur lokal modifiziert wurde. Die Wiederherstellung der Rot-Schwarz-Eigenschaften innerhalb der zweiten Phase benötigt höchstens drei Rotationen, wobei jede Rotation höchstens Zeit O(lg n) für die Propagation der Änderung von f erfordert. Somit ist, wie beim Einfügen, die Gesamtzeit für das Entfernen in O(lg n). In vielen Fällen, wie zum Beispiel beim Verwalten des Attributes größe in RanggrößenBäumen, entstehen durch das Aktualisieren nach der Rotation Kosten in Höhe von O(1) anstatt die im Beweis von Theorem 14.1 abgeleiteten Kosten von O(lg n). Übung 14.2-3 liefert ein Beispiel hierfür.
Übungen 14.2-1 Zeigen Sie, wie durch zusätzliche Zeiger auf Knoten jede der auf dynamischen Mengen definierten Anfragen Minimum, Maximum, Successor und Predecessor auf einem erweiterten Ranggrößen-Baum in Zeit O(1) im schlechtesten Fall unterstützt werden kann. Die asymptotische Performanz anderer Operationen auf einem Ranggrößen-Baum sollte davon nicht berührt werden. 14.2-2 Können wir die Schwarz-Höhe eines Knotens in einem Rot-Schwarz-Baum als Attribut in den Knoten halten, ohne die asymptotische Performanz einer der Rot-Schwarz-Operationen negativ zu beeinflussen? Zeigen Sie, wie dies realisiert werden kann, oder erklären Sie, warum dies nicht machbar ist. Wie ist dies, wenn wir die Tiefe der Knoten als Attribut in den Knoten abspeichern wollen?
350
14 Erweitern von Datenstrukturen
14.2-3∗ Sei ⊗ ein assoziativer binärer Operator und a ein Attribut, das in jedem Knoten des Rot-Schwarz-Baumes gehalten wird. Nehmen Sie an, dass wir jedem Knoten x ein zusätzliches Attribut f hinzufügen wollen, sodass x.f = x1 .a ⊗ x2 .a ⊗ . . . ⊗ xm .a gilt, wobei x1 , x2 , . . . , xm die Inorder-Reihenfolge von Knoten in dem von x ausgehenden Teilbaum ist. Zeigen Sie, dass die f -Attribute nach jeder Rotation in Zeit O(1) korrekt aktualisiert werden können. Modifizieren Sie Ihre Argumentation, sodass sie auf die größe-Attribute in Ranggrößen-Bäumen anwendbar ist. 14.2-4∗ Wir wollen einen Rot-Schwarz-Baum um die Operation RB-Enumerate(x, a, b) erweitern, die in einem von x ausgehenden Rot-Schwarz-Baum alle Schlüssel k ausgibt, für die a ≤ k ≤ b gilt. Beschreiben Sie, wie RB-Enumerate mit einer Laufzeit von Θ(m + lg n) implementiert werden kann, wobei m die Anzahl der auszugebenden Schlüssel und n die Anzahl der im Baum enthaltenen Knoten ist. (Hinweis: Sie brauchen keine neuen Attribute zu dem Rot-Schwarz-Baum hinzuzufügen.)
14.3
Intervallbäume
In diesem Abschnitt werden wir einen Rot-Schwarz-Baum so erweitern, dass er Operationen auf dynamischen Mengen von Intervallen unterstützt. Ein abgeschlossenes Intervall ist ein geordnetes Paar reeller Zahlen [t1 , t2 ] mit t1 ≤ t2 . Das Intervall [t1 , t2 ] stellt die Menge {t ∈ R : t1 ≤ t ≤ t2 } dar. Bei offenen bzw. halboffenen Intervallen gehören beide bzw. einer der beide Endpunkte nicht mit zur Menge. In diesem Abschnitt werden wir voraussetzen, dass die Intervalle geschlossen sind. Das Übertragen der Ergebnisse auf offene und halboffene Intervalle ist konzeptuell einfach. Intervalle sind dazu geeignet, Ereignisse darzustellen, die eine zusammenhängende Zeitspanne in Anspruch nehmen. Zum Beispiel kann es sein, dass wir eine Datenbank mit Zeitintervallen abfragen wollen, um herauszufinden, welche Ereignisse während eines gegebenen Intervalls stattgefunden haben. Die Datenstruktur in diesem Abschnitt bietet ein effizientes Hilfsmittel, um eine solche Intervall-Datenbank zu unterstützen. Wir können ein Intervall [t1 , t2 ] als ein Objekt i mit den Attributen i.unten = t1 (dem unteren Endpunkt) und i.oben = t2 (dem oberen Endpunkt) darstellen. Wir sagen, dass die Intervalle i und i sich überlappen, wenn i ∩ i = ∅ gilt, d. h. wenn die Ungleichungen i.unten ≤ i .oben und i .unten ≤ i.oben erfüllt sind. Wie Abbildung 14.3 zeigt, erfüllen zwei Intervalle i und j jeweils die Intervalltrichotomie, d. h. es gilt genau eine der folgenden drei Eigenschaften: a. i und i überlappen sich, b. i befindet sich links von i (d. h. i.oben < i .unten), c. i befindet sich rechts von i (d. h. i .oben < i.unten). Ein Intervallbaum ist ein Rot-Schwarz-Baum, der eine dynamische Menge von Elementen verwaltet, wobei jedes Element x ein Intervall x.int darstellt. Intervallbäume unterstützen die folgenden Operationen:
14.3 Intervallbäume
351
i i′
i i′
i i′
i i′
(a) i′
i
i′
(b)
i (c)
Abbildung 14.3: Die Intervalltrichotomie für zwei geschlossene Intervalle i und i . (a) Wenn i und i sich überlappen, gibt es vier verschiedene Möglichkeiten; es gilt jeweils i. unten ≤ i . oben und i . unten ≤ i. oben . (b) Die Intervalle überlappen sich nicht, und es gilt i. oben < i . unten . (c) Die Intervalle überlappen sich nicht, und es gilt i . oben < i. unten .
Interval-Insert(T, x) fügt das Element x, dessen int -Attribut ein Intervall enthält, in den Intervallbaum T ein. Interval-Delete(T, x) entfernt das Element x aus dem Intervallbaum T . Interval-Search(T, i) gibt einen Zeiger auf ein Element x im Intervallbaum zurück, sodass sich x.int und das Intervall i überlappen, falls ein solches Element im Intervallbaum enthalten ist, oder einen Zeiger auf den Wächter T.nil, falls kein solches Element in der Menge existiert. Abbildung 14.4 illustriert, wie ein Intervallbaum eine Menge von Intervallen darstellt. Wir werden der Vier-Schritt-Methode aus Abschnitt 14.2 folgen, um den Entwurf eines Intervallbaumes und der darauf definierten Operationen zu besprechen.
Schritt 1: Zugrunde liegende Datenstruktur Wir wählen einen Rot-Schwarz-Baum, in dem jeder Knoten x ein Intervall x.int enthält, und der Schlüssel von x der untere Endpunkt x.int .unten des Intervalls ist. Somit listet eine Inorder-Traversierung der Datenstruktur die Intervalle sortiert nach dem linken Endpunkt auf.
Schritt 2: Zusätzliche Information Zusätzlich zu den Intervallen selbst, enthält jeder Knoten x ein Attribut x.max , das den maximalen Wert aller gespeicherten Endpunkte des von x ausgehenden Teilbaumes angibt.
Schritt 3: Aktualisierung der Information Wir müssen überprüfen, ob die Operationen Einfügen und Entfernen auf einem Intervallbaum mit n Knoten in Zeit O(lg n) ausgeführt werden können. Wir können den
352
14 Erweitern von Datenstrukturen 26 26 25 19 17
(a)
19
16
21
15 8
23
9
6
10
5 0
30
20
8
3 0
5
10
15
20
25
30
[16,21] 30
[8,9]
[25,30]
23
30
(b)
int max
[5,8]
[15,23]
[17,19]
[26,26]
10
23
20
26
[0,3]
[6,10]
[19,20]
3
10
20
Abbildung 14.4: Ein Intervallbaum. (a) Eine Menge von 10 Intervallen, sortiert in aufsteigender Reihenfolge nach dem linken Endpunkt. (b) Der Intervallbaum, der diese Intervalle darstellt. Jeder Knoten x enthält ein Intervall, das oberhalb der gestrichelten Linie angegeben ist, und den maximalen Wert von den Endknoten der Intervalle, die in dem Teilbaum von x enthalten sind – dieser ist unterhalb der gestrichelten Linie zu sehen. Eine Inorder-Traversierung des Baumes listet die Knoten nach dem linken Endpunkt sortiert auf.
Wert x.max bestimmen, wenn wir das Intervall x.int und die max -Werte der Kinder von x kennen: x.max = max( x.int .oben, x.links.max , x.rechts.max ) . Also benötigen Einfügen und Entfernen nach Theorem 14.1 Zeit O(lg n). Tatsächlich kann das Aktualisieren der max -Attribute nach einer Rotation in Zeit O(1) ausgeführt werden, wie dies die Übungen 14.2-3 und 14.3-1 zeigen.
Schritt 4: Entwickeln neuer Operationen Die einzige neue Operation, die wir benötigen, ist Interval-Search(T, i), die einen Knoten im Baum T bestimmt, dessen Intervall sich mit dem Intervall i überlappt. Wenn kein Intervall im Baum existiert, das sich mit i überlappt, gibt die Prozedur einen Zeiger auf den Wächter T.nil zurück.
14.3 Intervallbäume
353
Interval-Search(T, i) 1 x = T.wurzel 2 while x = T.nil und i überlappt sich nicht mit x.int 3 if x.links = T.nil und x.links.max ≥ i.unten 4 x = x.links 5 else x = x.rechts 6 return x Die Suche nach einem Intervall, das sich mit dem Intervall i überlappt, beginnt an der Wurzel (x zeigt auf den jeweils aktuellen Knoten) und steigt dann in den Baum ab. Sie terminiert, entweder wenn sie ein überlappendes Intervall gefunden hat oder wenn x auf den Wächter T.nil zeigt. Da jede Iteration der Basisschleife Zeit O(1) benötigt und da die Höhe eines Rot-Schwarz-Baumes mit n Knoten in O(lg n) liegt, benötigt die Prozedur Interval-Search Zeit O(lg n). Bevor wir uns überlegen, warum die Operation Interval-Search korrekt ist, wollen wir uns anschauen, wie sie auf dem Intervallbaum in Abbildung 14.4 arbeitet. Nehmen Sie an, wir würden ein Intervall finden wollen, das das Intervall i = [22, 25] überlappt. Wir beginnen mit x an der Wurzel, die das Intervall [16, 21] enthält und i nicht überlappt. Da x.links.max = 23 größer als i.unten = 22 ist, fährt die Schleife mit x gleich dem linken Kind der Wurzel fort. Das ist der Knoten, der das Intervall [8, 9] enthält, welches i ebenfalls nicht überlappt. Diesmal ist x.links.max = 10 kleiner als i.unten = 22, sodass die Schleife mit dem rechten Kind von x als neues x fortfährt. Da sich das in diesem Knoten gespeicherte Intervall mit i überlappt, gibt die Prozedur diesen Knoten zurück. Als Beispiel für eine erfolglose Suche nehmen wir an, dass wir im Intervallbaum aus Abbildung 14.4 ein Intervall finden wollen, das sich mit dem Intervall i = [11, 14] überlappt. Wiederum beginnen wir mit x an der Wurzel. Da das Intervall [16, 21] der Wurzel sich nicht mit dem Intervall i überlappt und da x.links.max = 23 größer als i.unten = 11 ist, gehen wir zum linken Knoten, der das Intervall [8, 9] enthält. Das Intervall [8, 9] überlappt sich auch nicht mit dem Intervall i und x.links.max = 10 ist kleiner als i.unten = 11, daher gehen wir nach rechts. (Beachten Sie, dass kein Intervall im linken Teilbaum das Intervall i überlappt.) Intervall [15, 23] überlappt sich ebenfalls nicht mit i und dessen linkes Kind ist T.nil . Wir gehen also erneut nach rechts, die Schleife terminiert und wir geben den Wächter T.nil zurück. Um zu sehen, warum Interval-Search korrekt arbeitet, müssen wir verstehen, warum es ausreicht, einen einzigen von der Wurzel ausgehenden Pfad zu untersuchen. Die wesentliche Idee besteht darin, dass die Suche an jedem Knoten x in die sichere Richtung fortgesetzt wird, wenn sich das Intervall x.int nicht mit dem Intervall i überlappt. Ein sich mit dem Intervall i überlappendes Intervall wird definitiv gefunden, falls im Baum ein solches existiert. Das folgende Theorem legt diese Eigenschaft präziser dar. Theorem 14.2 Jede Ausführung der Prozedur Interval-Search(T, i) gibt entweder einen Knoten zurück, dessen Intervall sich mit i überlappt, oder sie gibt T.nil zurück und der Baum T enthält keinen Knoten, der sich mit dem Intervall i überlappt.
354
14 Erweitern von Datenstrukturen i′′ i′′ i′ i′
i
i
i′′ i′
(b)
(a)
Abbildung 14.5: Intervalle im Beweis von Theorem 14.2. Der Wert von x. links. max ist jeweils durch eine gestrichelte Linie gekennzeichnet. (a) Die Suche wendet sich nach rechts. Kein Intervall i im linken Teilbaum von x überlappt sich mit i. (b) Die Suche wendet sich nach links. Der linke Teilbaum von x enthält ein Intervall, das sich mit i überlappt (dieser Fall ist hier nicht gezeigt) oder es gibt ein Intervall i im linken Teilbaum von x, für das i . oben = x. links. max gilt. Da i sich mit dem Intervall i nicht überlappt, überlappt es sich auch nicht mit einem der Intervalle i im rechten Teilbaum von x, da i . unten ≤ i . unten gilt.
Beweis: Die while-Schleife in den Zeilen 2–5 bricht entweder ab, weil x = T.nil gilt oder weil das Intervall i sich mit dem Intervall x.int überlappt. Im letzteren Fall ist es zweifellos richtig, x zurückzugeben. Deshalb konzentrieren wir uns auf den ersten Fall, in dem die while-Schleife wegen x = T.nil terminiert. Wir verwenden für die while-Schleife in den Zeilen 2–5 die folgende Invariante: Wenn der Baum T ein Intervall enthält, das sich mit dem Intervall i überlappt, dann enthält der von x ausgehende Teilbaum ein solches Intervall. Wir verwenden die Schleifeninvariante wie folgt: Initialisierung: Vor der ersten Iteration setzt Zeile 1 den Wert für x auf die Wurzel von T , sodass die Invariante gilt. Fortsetzung: Jede Iteration der while-Schleife führt entweder Zeile 4 oder Zeile 5 aus. Wir werden zeigen, dass beide Fälle die Schleifeninvariante erhalten. Falls Zeile 5 ausgeführt wird, dann gilt wegen der Verzweigungsbedingung in Zeile 3 x.links = T.nil oder x.links.max < i.unten. Im Falle x.links = T.nil enthält der von x.links ausgehende Teilbaum mit Sicherheit kein Intervall, das sich mit dem Intervall i überlappt. Setzen wir also x auf x.rechts, bleibt die Schleifeninvariante erhalten. Deshalb nehmen wir an, dass x.links = T.nil und x.links.max < i.unten gelten. Wie Abbildung 14.5(a) zeigt, gilt für jedes Intervall i im linke Teilbaum von x i .oben ≤ x.links.max < i.unten . Wegen der Intervalltrichotomie überlappen sich deshalb i und i nicht, sodass die Schleifeninvariante erhalten bleibt, wenn wir x auf x.rechts setzen.
14.3 Intervallbäume
355
Wenn andererseits Zeile 4 ausgeführt wird, dann gilt, wie wir zeigen werden, die Umkehrung der Schleifeninvariante. Genauer gesagt, wenn der von x.links ausgehende Teilbaum kein Intervall enthält, das sich mit dem Intervall i überlappt, dann überlappt kein im Baum enthaltenes Intervall das Intervall i. Da Zeile 4 ausgeführt wird, gilt wegen der Verzweigungsbedingung in Zeile 3 die Ungleichung x.links.max ≥ i.unten. Darüber hinaus muss nach Definition des Attributes max ein Intervall i im linken Teilbaum von x existieren, sodass i .oben = x.links.max ≥ i.unten gilt. (Abbildung 14.5(b) illustriert diese Situation.) Da sich die Intervalle i und i nicht überlappen, und weil i .oben < i.unten nicht wahr ist, folgt aus der Intervalltrichotomie, dass i.oben < i .unten gilt. Die Schlüssel der Intervallbäume werden durch die unteren Endpunkte der Intervalle gebildet, und deshalb impliziert die Suchbaum-Eigenschaft, dass für jedes Intervall i im rechten Teilbaum von x i.oben < i .unten ≤ i .unten gilt. Wegen der Intervalltrichotomie überlappen sich die Intervalle i und i nicht. Wir können folgern, dass, egal ob ein Intervall im linken Teilbaum von x sich mit dem Intervall i überlappt oder nicht, die Schleifeninvariante erhalten bleibt, wenn wir x auf x.links setzen. Terminierung: Wenn die Schleife abbricht, weil x = T.nil gilt, dann enthält der von x ausgehende Teilbaum kein Intervall, das sich mit dem Intervall i überlappt. Die Umkehrung der Schleifeninvariante sagt dann aus, dass T kein sich mit dem Intervall i überlappendes Intervall enthält. Somit ist es korrekt, x = T.nil zurückzugeben. Die Prozedur Interval-Search arbeitet also korrekt.
Übungen 14.3-1 Schreiben Sie ein Programm in Pseudocode für Left-Rotate, das auf dem Knoten eines Intervallbaumes arbeitet und die max -Attribute in Zeit O(1) aktualisiert. 14.3-2 Schreiben Sie das Programm für Interval-Search so um, dass es korrekt arbeitet, wenn wir mit offenen Intervallen arbeiten. 14.3-3 Geben Sie einen effizienten Algorithmus an, der bei gegebenem Intervall i ein Intervall mit kleinstem unterem Endpunkt zurückgibt, das sich mit i überlappt. Falls kein solches Intervall existiert, soll er T.nil zurückgeben.
356
14 Erweitern von Datenstrukturen
14.3-4 Gegeben seien ein Intervallbaum T und ein Intervall i. Beschreiben Sie, wie alle Intervalle in T , die sich mit dem Intervall i überlappen, in Zeit O(min(n, k lg n)) aufgelistet werden können, wobei k die Anzahl der Intervalle in der Ausgabeliste ist. (Hinweis: Eine einfache Methode stellt mehrere Anfragen, wobei sie den Baum zwischen den Anfragen verändert. Ein etwas komplizierterer Ansatz verändert den Baum nicht.) 14.3-5 Schlagen Sie Modifikationen der Intervallbaum-Prozedur vor, damit die neue Operation Interval-Search-Exactly(T, i) unterstützt wird. T ist hierbei ein Intervallbaum und i ein Intervall. Die Operation soll einen Zeiger auf einen Knoten x aus T zurückgeben, sodass x.int .unten = i.unten und x.int .oben = i.oben gilt, oder T.nil , falls T keinen solchen Knoten enthält. Alle Operationen, einschließlich Interval-Search-Exactly, sollten auf einem Baum mit n Knoten in Zeit O(lg n) laufen. 14.3-6 Zeigen Sie, wie man eine dynamische Menge Q von Zahlen verwalten kann, die die Operation Min-Gap unterstützt, und die den Differenzbetrag von zwei in Q am dichtesten liegenden Zahlen liefert. Zum Beispiel gibt Min-Gap(Q) für Q = {1, 5, 9, 15, 18, 22} den Wert 18 − 15 = 3 zurück, weil 15 und 18 die in Q zueinander am dichtesten liegenden Zahlen sind. Gestalten Sie die Operationen Insert, Delete, Search und Min-Gap so effizient wie möglich und analysieren Sie deren Laufzeiten. 14.3-7∗VLSI-Datenbanken stellen einen integrierten Schaltkreis üblicherweise als Liste von Rechtecken dar. Nehmen Sie an, dass jedes Rechteck geradlinig orientiert ist (die Seiten liegen parallel zur x- und y-Achse), sodass wir ein Rechteck über seine minimalen und maximalen x- und y-Koordinaten darstellen. Geben Sie einen in Zeit O(n lg n) arbeitenden Algorithmus an, der für eine Menge von n so dargestellten Rechtecken entscheidet, ob zwei Rechtecke sich überlappen oder nicht. Ihr Algorithmus braucht nicht alle Schnittpunktpaare anzugeben. Überdeckt aber ein Rechteck ein anderes vollständig, so muss der Algorithmus auch in diesem Fall angeben, dass eine Überlappung vorliegt, obwohl sich die Randlinien nicht schneiden. (Hinweis: Bewegen Sie eine „Abtast“-Gerade (engl.: sweep line) über die Menge der Rechtecke.)
Problemstellungen 14-1 Position maximaler Überlappung Nehmen Sie an, dass wir eine Position maximaler Überlappung in einer Menge von Intervallen beobachten wollen – also eine Position, an der sich die meisten Intervalle der Menge überlappen. a. Zeigen Sie, dass es immer eine Position maximaler Überlappung gibt, welche ein Endpunkt eines der Segmente ist. b. Entwerfen Sie eine Datenstruktur, die die Operationen Interval-Insert, Interval-Delete und Find-POM, die eine Position maximaler Überlappung
Kapitelbemerkungen zu Kapitel 14
357
zurückgibt, effizient unterstützt. (Hinweis: Halten Sie einen Rot-Schwarz-Baum aller Endpunkte. Assoziieren Sie den Wert +1 mit jedem linken Endpunkt und den Wert −1 mit jedem rechten Endpunkt. Erweitern Sie jeden Knoten des Baumes um zusätzliche Informationen, mit denen Sie die Position maximaler Überlappung berechnen können.) 14-2 Josephus-Permutation Wir definieren das Josephus-Problem wie folgt. Nehmen Sie an, n Personen stünden in einem Kreis und es wäre eine positive Integer-Zahl m ≤ n gegeben. Beginnend bei einer festgelegten ersten Person durchlaufen wir den Kreis, wobei wir jede m-te Person ausschließen. Nach jedem Ausschließen einer Person machen wir mit dem Abzählen im verbliebenen Kreis weiter. Dieser Prozess wird solange fortgesetzt, bis wir alle n Personen ausgeschlossen haben. Die Reihenfolge, in der die Personen aus dem Kreis ausgeschlossen werden, definiert die (n, m)-Josephus-Permutation der Integer-Zahlen 1, 2, . . . , n. Die (7, 3)Josephus-Permutation lautet zum Beispiel 3, 6, 2, 7, 5, 1, 4. a. Angenommen, m ist konstant. Geben Sie einen Algorithmus an, der zu gegebenem n in Zeit O(n) die (n, m)-Josephus-Permutation ausgibt. b. Nehmen Sie an, dass m nicht konstant ist. Geben Sie einen Algorithmus an, der zu gegebenem n und m in Zeit O(n lg n) die (n, m)-Josephus-Permutation ausgibt.
Kapitelbemerkungen In ihrem Buch beschreiben Preparata und Shamos [282] verschiedene in der Literatur vorkommende Intervallbäume, wobei Arbeiten von H. Edelsbrunner (1980) und E. M. McCreight (1981) zitiert werden. Das Buch geht näher auf einen Intervallbaum ein, der es uns bei einer gegebenen statischen Datenbank mit n Intervallen erlaubt, alle k Intervalle, die sich mit einem gegebenen Anfrageintervall überlappen, in Zeit O(k +lg n) aufzuzählen.
Teil IV
Fortgeschrittene Entwurfsund Analysetechniken
Einführung Dieser Teil behandelt drei wesentliche Techniken, die beim Entwurf und der Analyse effizienter Algorithmen angewendet werden: Dynamische Programmierung (Kapitel 15), Greedy-Algorithmen (Kapitel 16) und amortisierte Analyse (Kapitel 17). Die vorhergehenden Kapitel haben bereits andere, breit anwendbare Techniken vorgestellt, wie zum Beispiel Teile-und-Beherrsche-Algorithmen, Randomisierung und wie wir Rekursionsgleichungen lösen können. Die Techniken, die wir in diesem Teil vorstellen, sind etwas komplizierter, aber sie helfen uns, viele Probleme erfolgreich anzugehen. Wir werden den in diesem Teil behandelten Themen später im Buch wieder begegnen. Dynamische Programmierung wird gewöhnlich auf Optimierungsprobleme angewendet, in denen wir eine Menge von Entscheidungen treffen müssen, um zu einer optimalen Lösung zu gelangen. Wenn wir eine Entscheidung treffen, treten häufig Teilprobleme der gleichen Form auf. Dynamische Programmierung ist dann effektiv, wenn ein gegebenes Teilproblem auf verschiedenen Wegen, d. h. über unterschiedliche Entscheidungen, auftreten kann. Die Schlüsseltechnik besteht darin, die Lösung zu jedem dieser Teilprobleme für den Fall zu speichern, dass dieses wieder auftritt. Kapitel 15 zeigt, wie durch dieses einfache Konzept einige Algorithmen, die in exponentieller Zeit laufen, in Algorithmen umgewandelt werden können, die polynomiale Laufzeit besitzen. Wie auf dynamischer Programmierung basierende Algorithmen werden Greedy-Algorithmen gewöhnlich auf Optimierungsprobleme angewendet, bei denen wir viele Entscheidungen treffen müssen, um zu einer optimalen Lösung zu gelangen. Die Idee eines Greedy-Algorithmus besteht darin, jede Entscheidung in lokal optimaler Weise zu treffen. Ein einfaches Beispiel hierfür ist der Münzwechsel: Um die Anzahl der Münzen zu minimieren, die einen gegebenen Betrag ergeben, können wir iterativ die Münze mit dem höchsten Nennwert auswählen, der nicht höher als der noch zu bezahlende Betrag ist. Ein Greedy-Algorithmus liefert eine optimale Lösung für viele solche Probleme weitaus schneller als dies ein Ansatz über dynamische Programmierung tun würde. Wir können jedoch nicht immer einfach feststellen, ob ein Greedy-Ansatz effektiv ist. Kapitel 16 führt in die Matroidtheorie ein, die eine mathematische Basis zur Verfügung stellt, die uns helfen kann, zu zeigen, ob ein Greedy-Algorithmus zu einer optimalen Lösung führt. Wir verwenden amortisierte Analyse, um bestimmte Algorithmen zu untersuchen, die jeweils eine Folge gleichartiger Operationen ausführen. Anstatt die Kosten der Folge der Operationen abzuschätzen, indem die tatsächlichen Kosten jeder einzelnen Operation für sich berechnet und dann aufaddiert werden, kann eine amortisierte Analyse verwendet werden, um eine Schranke für die tatsächlichen Kosten der Gesamtfolge zu erhalten. Ein Vorteil dieses Ansatzes besteht darin, dass, wenngleich einige Operationen möglicherweise teuer sind, viele andere Operationen der Folge möglicherweise billig sind.
362
Teil IV Fortgeschrittene Entwurfs- und Analysetechniken
In anderen Worten, viele Operationen der Folge können möglicherweise weit schneller ausgeführt werden als im schlechtesten Fall. Die amortisierte Analyse ist jedoch nicht nur ein Analysewerkzeug, sondern bietet auch die Möglichkeit, über den Entwurf von Algorithmen nachzudenken, weil der Entwurf eines Algorithmus und die Analyse seiner Laufzeit sich gegenseitig stark beeinflussen. Kapitel 17 stellt drei Methoden vor, mit denen die amortisierte Analyse eines Algorithmus durchgeführt werden kann.
15
Dynamische Programmierung
Wie die Teile-und-Beherrsche-Methode löst dynamische Programmierung Probleme, indem sie Lösungen von Teilproblemen kombiniert. („Programmierung“ bezieht sich in diesem Kontext auf eine tabellarische Methode, nicht auf das Schreiben von Code.) Wie wir in den Kapiteln 2 und 4 gesehen haben, zerlegen Teile-und-Beherrsche-Algorithmen ein Problem in disjunkte Teilprobleme, lösen die Teilprobleme rekursiv und kombinieren dann deren Lösungen, um das ursprüngliche Problem zu lösen. Im Gegensatz dazu wird dynamische Programmierung angewendet, wenn die Teilprobleme sich überlappen, d. h. wenn Teilprobleme ihrerseits jeweils die gleichen Teilteilprobleme lösen müssen. In diesem Zusammenhang führt ein Teile-und-Beherrsche-Algorithmus mehr Arbeit als notwendig aus, indem er die gemeinsamen Teilteilprobleme wiederholt löst. Ein dynamisches Programm löst jedes Teilteilproblem genau einmal und speichert dessen Lösung in einer Tabelle ab. Dadurch vermeiden wir den Aufwand, die Lösung eines Teilteilproblems jedes Mal wieder neu zu berechnen, wenn es uns wieder begegnet. Typischerweise wenden wir dynamische Programmierung auf Optimierungsprobleme an. Solche Probleme können viele mögliche Lösungen haben. Jede Lösung besitzt einen Wert und wir möchten eine Lösung mit dem optimalen (minimalen oder maximalen) Wert finden. Wir bezeichnen eine solche Lösung als eine optimale Lösung des Problems, und nicht als die optimale Lösung, da es verschiedene Lösungen geben kann, die den optimalen Wert besitzen. Wenn wir einen auf dynamischer Programmierung basierenden Algorithmus entwickeln, folgen wir den folgenden vier Schritten: 1. Wir charakterisieren die Struktur einer optimalen Lösung. 2. Wir definieren den Wert einer optimalen Lösung rekursiv. 3. Wir berechnen den Wert einer optimalen Lösung, typischerweise über einen bottomup-Ansatz. 4. Wir konstruieren eine zugehörige optimale Lösung aus den schon berechneten Daten. Die Schritte 1–3 bilden die Basis einer auf dynamischer Programmierung basierenden Lösung eines Problems. Wenn wir nur den Wert einer optimalen Lösung wissen wollen und nicht die Lösung selbst, so können wir Schritt 4 weglassen. Wenn wir aber Schritt 4 ausführen, dann verwalten wir in Schritt 3 manchmal zusätzliche Informationen, um eine optimale Lösung einfacher konstruieren zu können. Die Abschnitte, die nun folgen, verwenden die Methode der dynamischen Programmierung, um verschiedene Optimierungsprobleme zu lösen. Abschnitt 15.1 untersucht das
364
15 Dynamische Programmierung
Problem, eine Eisenstange so in Stäbe kürzerer Länge zu schneiden, dass ihr Gesamtwert maximiert wird. Abschnitt 15.2 betrachtet das Problem, wie eine Kette von Matrizen mit minimal vielen skalaren Multiplikationen multipliziert werden kann. Nachdem wir uns diese zwei Beispiele dynamischer Programmierung angeschaut haben, diskutiert Abschnitt 15.3 zwei Schlüsseleigenschaften, die ein Problem haben muss, damit dynamisches Programmieren eine brauchbare Lösungsmethode für dieses Problem ist. Abschnitt 15.4 zeigt dann, wie eine längste gemeinsame Teilfolge zweier Folgen mittels dynamischer Programmierung gefunden werden kann. Schließlich wendet Abschnitt 15.5 dynamisches Programmieren an, um binäre Suchbäume, die für eine gegebene Verteilung der Schlüssel optimal sind, zu konstruieren.
15.1
Schneiden von Eisenstangen
Unser erstes Beispiel wendet dynamische Programmierung an, um ein einfaches Problem zu lösen, nämlich wie eine Eisenstange in kürzere Stäbe zu schneiden ist, d. h. an welchen Positionen der Stange zu schneiden ist. Die Serling GmbH kauft lange Eisenstangen und schneidet sie in kürzere Stäbe, die sie dann verkaufen. Das Schneiden der Stange kostet nichts. Die Geschäftsführung der Serling GmbH möchte wissen, wie die Eisenstangen am besten in Stücke geschnitten werden können. Wir setzen voraus, dass wir für i = 1, 2, . . . den Preis pi in Euros kennen, die die Serling GmbH für einen Eisenstab der Länge i cm verlangt. Die Längen der Eisenstäbe ausgedrückt in Zentimeter sind immer eine ganze Zahl. Abbildung 15.1 zeigt beispielhaft eine Preisliste. Länge i Preis pi
1 1
2 5
3 8
4 9
5 10
6 17
7 17
8 20
9 24
10 30
Abbildung 15.1: Ein Beispiel einer Preisliste für Eisenstäbe. An jedem Stab der Länge i cm verdient das Unternehmen pi Euros.
Unter dem Stabzerlegungsproblem verstehen wir das folgende Problem. Gegeben sei eine Stange der Länge n cm und eine Preisliste pi für i = 1, 2, . . . , n, bestimmen Sie den maximalen Erlös rn , der erzielt werden kann, indem die Stange in kleinere Stäbe zerlegt wird und diese Stücke dann verkauft werden. Beachten Sie, dass, wenn der Preis pn für einen Stab der Länge n ausreichend hoch ist, die optimale Lösung die Stange nicht zu schneiden braucht. Betrachten Sie den Fall n = 4. Abbildung 15.2 zeigt alle Möglichkeiten, wie die Eisenstange von 4 cm Länge zerlegt werden kann, inklusive der Möglichkeit, die Stange überhaupt nicht zu schneiden. Wir sehen, dass das Zerlegen der 4 cm langen Stange in zwei 2 cm lange Teilstücke den Erlös p2 + p2 = 5 + 5 = 10 erzielt, was optimal ist. Wir können eine Eisenstange der Länge n in 2n−1 verschiedenen Weisen zerlegen, da wir für jedes i = 1, 2, . . . , n − 1 an der Position i (i ist die Distanz von dem linken Ende der Stange aus gesehen) unabhängig von den übrigen Positionen die freie Wahl haben, ob wir schneiden oder nicht schneiden.1 Wir stellen eine Zerlegung einer Stange 1 Wenn
wir verlangen, dass die Stücke in der Reihenfolge, in der wir sie schneiden, nie kleiner
15.1 Schneiden von Eisenstangen 9
1
(a) 1
1
365
8
5
(b) 5
1
(e)
5
5
8
(c) 1
5
(f)
1
1
(d) 1
1
(g)
1
1
1
(h)
Abbildung 15.2: Die 8 Möglichkeiten, eine Eisenstange der Länge 4 zu zerlegen. Über jedem Teilstab steht der Wert dieses Stücks gemäß der Preisliste aus Abbildung 15.1. Die optimale Strategie ist die unter (c) gezeigte – das Schneiden der Stange in zwei Stäbe der Länge 2 – die zu einem Gesamterlös von 10 führt.
in Teilstäbe über eine normale additive Notation dar, sodass 7 = 2 + 2 + 3 angibt, dass die Eisenstange der Länge 7 in drei Teilstäbe zerlegt wurde – zwei der Länge 2 und eine der Länge 3. Falls eine optimale Lösung den Eisenstab in k Teilstäbe für ein 1 ≤ k ≤ n schneidet, dann erbringt eine optimale Zerlegung n = i1 + i2 + · · · + ik des Eisenstabs in Teilstäbe der Längen i1 , i2 , . . . , ik einen maximalen dazugehörigen Erlös rn = pi1 + pi2 + · · · + pik . Bei unserem Beispielproblem können wir die optimalen Beträge ri für i = 1, 2, . . . , 10 schrittweise mit folgenden optimalen Zerlegungen bestimmen: r1 r2 r3 r4 r5 r6 r7 r8 r9 r10
=1 =5 =8 = 10 = 13 = 17 = 18 = 22 = 25 = 30
ergibt ergibt ergibt ergibt ergibt ergibt ergibt ergibt ergibt ergibt
sich sich sich sich sich sich sich sich sich sich
aus 1 = 1 aus 2 = 2 aus 3 = 3 aus 4 = 2 + 2 aus 5 = 2 + 3 aus 6 = 6 aus 7 = 1 + 6 aus 8 = 2 + 6 aus 9 = 3 + 6 aus 10 = 10
(keine Schnitte) , (keine Schnitte) , (keine Schnitte) , , , (keine Schnitte) , oder 7 = 2 + 2 + 3 , , , (keine Schnitte) .
werden, dann gäbe es weniger Möglichkeiten, eine Stange zu zerlegen. Für n = 4, hätten wir nur 5 Zerlegungsmöglichkeiten: die in (a), (b), (c), (e), und (h) der Abbildung 15.2 gezeigten Zerlegungen. Die Anzahl wird als Partitionsfunktion bezeichnet; sie ist ungefähr √ der Zerlegungsmöglichkeiten √ gleich eπ 2n/3 /4n 3. Diese Anzahl ist kleiner als 2n−1 , aber immer noch größer als jedes Polynom in n. Wir werden diese Diskussion aber hier nicht weiter verfolgen.
366
15 Dynamische Programmierung
Generell können wir die Werte rn für n ≥ 1 aus den optimalen Erlösen kürzerer Stäbe formen: rn = max (pn , r1 + rn−1 , r2 + rn−2 , . . . , rn−1 + r1 ) .
(15.1)
Das erste Argument in dieser Maximumsbildung, pn , gibt den Erlös an, den wir erzielen, wenn der Stab überhaupt nicht geschnitten wird und wir die Eisenstange der Länge n, so wie sie ist, verkaufen. Die anderen n − 1 Argumente von max geben den maximalen Erlös an, den wir erzielen können, wenn wir mit einem ersten Schnitt die Eisenstange in zwei Teilstäbe teilen, einen Teilstab der Länge i und einen Teilstab der Länge n − i für i = 1, 2, . . . , n − 1, und dann diese beiden Teilstäbe optimal weiter in kleinere Teilstäbe zerschneiden, sodass wir für diese zwei Teilstäbe die Erlöse ri und rn−i erzielen. Da wir im Vorfeld nicht wissen, mit welchem Wert von i wir den Erlös optimieren, müssen wir alle möglichen Werte von i betrachten und dann den Wert von i auswählen, der den Erlös maximiert. Dabei haben wir auch die Option, überhaupt kein Wert für i zu wählen, wenn wir einen höheren Erlös erzielen können, indem wir die Eisenstange ungeschnitten als Ganzes verkaufen. Sie bemerken, dass wir kleinere Probleme des gleichen Typs lösen, um das ursprüngliche Problem der Größe n zu lösen. Nachdem wir den ersten Schnitt gemacht haben, können wir die zwei Teilstäbe als voneinander unabhängige Instanzen des Stabzerlegungsproblems ansehen. Die optimale Gesamtlösung setzt sich aus den optimalen Lösungen der beiden zugehörigen Teilprobleme, die den Erlös für jeden der beiden Teilstäbe maximieren, zusammen. Wir sagen, dass das Stabzerlegungsproblem die optimaleTeilstruktur-Eigenschaft besitzt: Optimale Lösungen eines Problems setzen sich aus optimalen Lösungen zugehöriger Teilprobleme, die unabhängig voneinander gelöst werden können, zusammen. Eine verwandte, aber leicht einfachere Möglichkeit, eine rekursive Struktur für das Stabzerlegungsproblem aufzubauen, besteht darin, dass wir eine Zerlegung ansehen, als bestünde sie aus einem ersten, am linken Ende der Eisenstange abgeschnittenen Teilstab der Länge i und einer rechts davon liegenden Reststange der Länge n − i. Nur diese Reststange der Länge n − i kann weiter unterteilt werden, nicht aber das erste Stück. Wir können jede Zerlegung einer Eisenstange der Länge n so verstehen: als ein erster Teilstab gefolgt von einer Zerlegung der Reststange. Wenn wir so vorgehen, können wir die Lösung, die überhaupt keinen Schnitt enthält, als eine Zerlegung verstehen, die aus einem ersten Teilstab der Länge n mit Erlös pn und einer Reststange der Länge 0 mit Erlös r0 = 0 besteht. Wir erhalten so die folgende, etwas leichtere Version der Gleichung (15.1): rn = max (pi + rn−i ) . 1≤i≤n
(15.2)
Legt man diese Formulierung zugrunde, so enthält eine optimale Lösung die Lösung von nur einem verwandten Teilproblem – der Reststange – und nicht von zweien.
Eine rekursive top-down Implementierung Die folgende Prozedur implementiert die in Gleichung (15.2) implizit gegebene Berechnung top-down in direkter und rekursiver Art und Weise.
15.1 Schneiden von Eisenstangen
367
Cut-Rod(p, n) 1 if n = = 0 2 return 0 3 q = −∞ 4 for i = 1 to n 5 q = max(q, p[i] + Cut-Rod(p, n − i)) 6 return q Die Prozedur Cut-Rod erhält als Eingabe ein Feld p[1 . . n] von Preisen und eine ganze Zahl n und gibt den maximal erreichbaren Erlös für eine Eisenstange der Länge n zurück. Ist n = 0, dann ist kein Erlös möglich und so gibt Cut-Rod den Wert 0 in Zeile 2 zurück. Zeile 3 initialisiert den maximalen Erlös q mit −∞, sodass die forSchleife in den Zeilen 4–5 den Wert q = max1≤i≤n (pi + Cut-Rod(p, n − i)) berechnet; Zeile 6 gibt dann diesen Wert zurück. Ein einfacher Beweis durch Induktion nach n zeigt mittels Gleichung (15.2), dass diese Antwort gleich der gewünschten Antwort rn ist. Wenn Sie die Prozedur Cut-Rod in Ihrer bevorzugten Programmiersprache kodieren und sie auf Ihrem Rechner laufen lassen, werden Sie sehen, dass die Laufzeit Ihres Programms recht hoch ist, wenn der Eingabeparameter n einen mäßig großen Wert annimmt. Für n = 40 zum Beispiel werden Sie feststellen, dass Ihr Programm wenigstens einige Minuten benötigt, wahrscheinlich sogar mehr als eine Stunde. In der Tat, Sie werden feststellen, dass sich die Laufzeit Ihres Programms ungefähr verdoppelt jedes Mal, wenn Sie n um 1 erhöhen. Warum ist Cut-Rod so ineffizient? Das Problem besteht darin, dass Cut-Rod sich immer wieder selbst mit den gleichen Parameterwerten rekursiv aufruft; Cut-Rod löst wiederholt die gleichen Teilprobleme. Abbildung 15.3 illustriert, was bei n = 4 passiert: Cut-Rod(p, n) ruft Cut-Rod(p, n − i) für i = 1, 2, . . . , n auf, d. h. Cut-Rod(p, n) ruft Cut-Rod(p, j) für jedes j = 0, 1, . . . , n − 1 auf. Wenn dieser Prozess sich rekursiv entfaltet, wächst der Arbeitsaufwand als Funktion in n explosionsartig. Um die Laufzeit von Cut-Rod zu analysieren, lassen Sie uns die Gesamtanzahl der Prozeduraufrufe, die durch den Aufruf von Cut-Rod(p, n) insgesamt verursacht werden, mit T (n) bezeichnen, d. h. T (n) zählt die Prozeduraufrufe, wenn der zweite Parameter von Cut-Rod gleich n ist. Diese Zahl ist gleich der Anzahl der Knoten in einem Teilbaum (des Rekursionsbaums), dessen Wurzel mit n markiert ist. Hierbei zählen wir den zur Wurzel gehörigen Aufruf mit. Somit gilt T (0) = 1 und T (n) = 1 +
n−1
T (j) .
(15.3)
j=0
Die im Ausdruck vorne stehende 1 ist der Aufruf an der Wurzel und der Term T (j) zählt die Anzahl der Aufrufe (inklusive den rekursiven Aufrufen), die durch den Aufruf Cut-Rod(p, n − i) mit j = n − i verursacht werden. Mit Übung 15.1-1 folgt T (n) = 2n ,
(15.4)
368
15 Dynamische Programmierung 4 3 1
2 1
0
1
2
0
0
1
0
0
0
0
0 Abbildung 15.3: Der Rekursionbaum, der die rekursiven Aufrufe, die durch einen Aufruf von Cut-Rod(p, n) für n = 4 entstehen, zeigt. Die Knotenmarkierung gibt die Größe n des dazugehörigen Teilproblems an, sodass eine Kante von einem Vater mit Markierung s zu einem Kind mit Markierung t für das Abschneiden eines ersten Teilstabes der Länge s − t, sodass eine Reststange der Länge t übrigbleibt, steht. Ein Pfad von der Wurzel zu einem Blatt entspricht einer der 2n−1 Möglichkeiten, eine Eisenstange der Länge n in Stücke zu zerschneiden. Allgemein gilt, dass dieser Rekursionsbaum 2n Knoten und 2n−1 Blätter besitzt.
und so ist die Laufzeit von Cut-Rod exponentiell in n. Diese exponentielle Laufzeit ist nicht so überraschend. Cut-Rod betrachtet explizit alle 2n−1 Möglichkeiten, eine Eisenstange der Länge n in Stücke zu schneiden. Der dazugehörige Rekursionsbaum besitzt 2n−1 Blätter: für jede Möglichkeit, die Eisenstange zu zerlegen, ein Blatt. Eine jede Knotenmarkierung auf einem einfachen Pfad von der Wurzel zu einem Blatt gibt die jeweilige Länge der verbleibenden (rechten) Reststange an, bevor der nächste Schnitt gemacht wird, d. h. die Markierungen geben die Positionen (von rechts aus gesehen) an, an denen die Eisenstange geschnitten wird.
Lösen des Stabzerlegungsproblems mittels dynamischer Programmierung Wir zeigen nun, wie wir Cut-Rod mittels dynamischer Programmierung in einen effizienten Algorithmus verwandeln können. Die Methode der dynamischen Programmierung arbeitet wie folgt. Wenn wir sehen, dass ein naiver rekursiver Ansatz ineffizient ist, weil er die gleichen Teilproblem immer wieder löst, sorgen wir dafür, dass jedes Teilproblem nur einmal gelöst wird, indem wir uns seine Lösung, einmal berechnet, merken. Falls wir dieses Teilproblem wieder zum Lösen erhalten, so schlagen wir einfach nur nach anstatt es nochmals zu lösen. Dynamisches Programmieren benutzt also zusätzlichen Speicherplatz, um Laufzeit zu sparen; die Methode ist ein Beispiel für ein Laufzeit-Speicher-Tradeoff. Die durch diesen Ansatz erzielbare Laufzeitreduzierung ist drastisch: ein Ansatz mit exponentieller Laufzeit kann möglicherweise in einen Algorithmus mit polynomieller Laufzeit transformiert werden. Ein auf dynamischer Programmierung basierender Ansatz läuft in polynomieller Zeit, wenn die Anzahl der unterschiedlichen Teilprobleme, die zu lösen sind, polynomiell in der Größe der Eingabe ist and wir jedes dieser Teilprobleme in polynomieller Zeit lösen können.
15.1 Schneiden von Eisenstangen
369
Üblicherweise gibt es zwei äquivalente Möglichkeiten, einen auf dynamischer Programmierung basierenden Ansatz zu implementieren. Wir werden beide Möglichkeiten an unserem Beispiel des Schneidens einer Eisenstange illustrieren. Der erste Ansatz ist top-down mit Memoisation.2 In diesem Ansatz schreiben wir die Prozedur rekursiv wie gehabt und ändern sie nur dahingehend, dass das Ergebnis eines jeden Teilproblems gespeichert wird (üblicherweise in einem Feld oder in einer Hashtabelle). Die Prozedur prüft also zuerst, ob sie das Teilproblem bereits gelöst hat. Falls ja, so gibt sie den gespeicherten Wert zurück und spart damit weitere Berechnungen auf dieser Ebene; falls nein, so berechnet die Prozedur den Wert in der üblichen Art und Weise. Wir sagen, dass die rekursive Prozedur memoisiert wurde; sie „erinnert“ sich, welche Ergebnisse bereits berechnet worden sind. Der zweite Ansatz ist die bottom-up-Methode. Dieser Ansatz hängt in dem Sinne von einem natürlichen Begriff „Größe“ eines Teilproblems ab, dass das Lösen eines speziellen Teilproblems nur von der Lösung „kleinerer“ Teilprobleme abhängt. Wir sortieren die Teilprobleme nach ihrer Größe und lösen sie in dieser Reihenfolge, beginnend bei den kleinsten. Wenn wir ein spezielles Teilproblem lösen, dann haben wir bereits alle kleineren Teilprobleme, von denen seine Lösung abhängt, gelöst und ihre Lösungen abgespeichert. Wir lösen jedes Teilproblem nur einmal, und wenn wir es das erste Mal sehen, haben wir bereits all seine Teilprobleme gelöst. Diese zwei Ansätze führen zu Algorithmen mit der gleichen asymptotischen Laufzeit, es sei denn es liegen ungewöhnliche Umstände vor, unter denen der top-down-Ansatz sich nicht alle möglichen Teilprobleme tatsächlich (rekursiv) anschauen muss. Der bottomup-Ansatz hat meist bessere konstanten Faktoren, da bei ihm nicht der durch Prozeduraufrufe anfallende Overhead anfällt. Hier nun der Pseudocode für die top-down-Methode mit Memoisation: Memoized-Cut-Rod(p, n) 1 sei r[0 . . n] ein neues Feld 2 for i = 0 to n 3 r[i] = −∞ 4 return Memoized-Cut-Rod-Aux(p, n, r) Memoized-Cut-Rod-Aux(p, n, r) 1 if r[n] ≥ 0 2 return r[n] 3 if n = = 0 4 q =0 5 else q = −∞ 6 for i = 1 to n 7 q = max(q, p[i] + Memoized-Cut-Rod-Aux(p, n − i, r)) 8 r[n] = q 9 return q 2 Dies
ist kein Schreibfehler. Das Wort ist wirklich Memoisation. Memoisation kommt von Memo, also von Notizzettel, da die Methode darin besteht, dass ein Wert niedergeschrieben wird, sodass wir ihn später nachschlagen können.
370
15 Dynamische Programmierung
Die Hauptprozedur Memoized-Cut-Rod initialisiert ein neues Hilfsfeld r[0 . . n] mit dem Wert −∞, der für „unbekannt“ steht. (Bekannte Erlöse sind immer nichtnegativ.) Dann ruft sie ihre Hilfsprozedur Memoized-Cut-Rod-Aux auf. Die Prozedur Memoized-Cut-Rod-Aux ist lediglich die memoisierte Version der vorherigen Prozedur Cut-Rod. Sie überprüft zuerst in Zeile 1, ob der gewünschte Wert bereits bekannt ist, und gibt ihn in Zeile 2 zurück, falls er es ist. Andernfalls berechnen die Zeilen 3–7 den gewünschten Wert q in der üblichen Art und Weise, Zeile 8 rettet ihn in r[n] und Zeile 9 gibt ihn zurück. Die bottom-up-Version ist sogar noch einfacher: Bottom-Up-Cut-Rod(p, n) 1 sei r[0 . . n] ein neues Feld 2 r[0] = 0 3 for j = 1 to n 4 q = −∞ 5 for i = 1 to j 6 q = max(q, p[i] + r[j − i]) 7 r[j] = q 8 return r[n] In dem bottom-up-Ansatz für dynamisches Programmieren benutzt Bottom-Up-CutRod die natürliche Ordnung der Teilprobleme: ein Teilproblem der Größe i ist „kleiner“ als ein Teilproblem der Größe j, wenn i < j gilt. Somit löst die Prozedur die Teilprobleme der Größen j = 0, 1, . . . , n und zwar in dieser Reihenfolge. Zeile 1 der Prozedur Bottom-Up-Cut-Rod erzeugt ein neues Feld r[0 . . n], in dem wir die Lösungen der Teilprobleme abspeichern, und Zeile 2 initialisiert r[0] mit 0, da wir für eine Eisenstange der Länge 0 keinen Erlös erzielen können. Die Zeilen 3–6 lösen in aufsteigender Reihenfolge nach der Größe jedes Teilproblem der Größe j, für j = 1, 2, . . . , n. Der Ansatz, der zum Lösen eines Problems einer speziellen Größe j verwendet wird, ist der gleiche wie der bei Cut-Rod mit dem einzigen Unterschied, dass Zeile 6 jetzt sofort auf den Feldeintrag r[j − i] zugreift, anstatt einen rekursiven Aufruf zu machen, um das Teilproblem der Größe j − i zu lösen. Zeile 7 speichert die Lösung des Teilproblems der Größe j in r[j] ab. Zeile 8 schließlich gibt r[n] zurück, der gleich dem optimalen Wert rn ist. Die bottom-up-Version und die top-down-Version haben die gleiche asymptotische Laufzeit. Die Laufzeit der Prozedur Bottom-Up-Cut-Rod ist aufgrund der doppelt verschachtelten Schleifenstruktur in Θ(n2 ). Die Anzahl der Iterationen der inneren forSchleife in den Zeilen 5–6 ergibt eine arithmetische Reihe. Die Laufzeit seines topdown-Gegenstücks Memoized-Cut-Rod ist ebenfalls Θ(n2 ), auch wenn seine Laufzeit ein wenig schwerer zu bestimmen ist. Da ein rekursiver Aufruf für ein bereits vorher gelöstes Teilproblem sofort wieder zurückspringt, löst Memoized-Cut-Rod jedes Teilproblem genau einmal. Es löst Teilprobleme der Größen 0, 1, . . . , n. Um ein Teilproblem der Größe n zu lösen, iteriert die for-Schleife der Zeilen 6–7 n Mal. Somit ergibt sich für die Gesamtanzahl der Iterationen dieser for-Schleife über alle rekursiven Aufrufe
15.1 Schneiden von Eisenstangen
371
4 3 2 1 0
Abbildung 15.4: Der Teilproblem-Graph für das Stabzerlegungsproblem mit n = 4. Die Knotenmarkierungen geben die Größen der jeweiligen Teilprobleme an. Eine gerichtete Kante (x, y) gibt an, dass wir eine Lösung des Teilproblems y benötigen, wenn wir Teilproblem x lösen. Dieser Graph ist eine reduzierte Version des Baumes aus Abbildung 15.3, in dem alle Knoten mit der gleichen Markierung durch einen einzigen Knoten dargestellt werden und alle Kanten jeweils vom Vater zum Kind gerichtet sind.
von Memoized-Cut-Rod eine arithmetische Reihe, also eine Gesamtanzahl von Θ(n2 ) Iterationen, genau wie bei der inneren for-Schleife von Bottom-Up-Cut-Rod. (Wir haben hier in Wirklichkeit eine Form aggregierter Analyse verwendet. Wir werden uns aggregierte Analyse im Detail in Abschnitt 17.1 anschauen.)
Teilproblem-Graphen Wenn wir über ein auf dynamischer Programmierung basierendes Problem nachdenken, sollten wir verstehen, welche Menge von Teilprobleme involviert sind und wie Teilprobleme von einander abhängen. Der Teilproblem-Graph für das Problem enthält genau diese Information. Abbildung 15.4 zeigt den Teilproblem-Graphen für das Stabzerlegungsproblem mit n = 4. Es ist ein gerichteter Graph, der nur einen Knoten für jedes unterschiedliche Teilproblem enthält. Der Teilproblem-Graph enthält eine gerichtete Kante von dem Knoten für Teilproblem x zu dem Knoten für Teilproblem y, wenn wir eine optimale Lösung für Teilproblem y betrachten, um eine optimale Lösung für Teilproblem x zu bestimmen. Beispielsweise enthält der Teilproblem-Graph eine Kante von x nach y, wenn die rekursive top-down-Prozedur zum Lösen von x sich selbst direkt für y aufrufen muss. Wir können den Teilproblem-Graphen als eine „reduzierte“ oder „zusammengefaltete“ Version des Rekursionsbaumes einer rekursiven top-down-Methode ansehen, in dem wir alle Knoten für das gleiche Teilproblem in einem einzigen Knoten vereinigen und alle Kanten jeweils vom Vater zum Kind ausrichten. Die bottom-up-Methode für dynamische Programmierung betrachtet die Knoten des Teilproblem-Graphen in einer Reihenfolge, dass wir die Teilprobleme y, die zu dem gegebenen Teilproblem x adjazent sind, lösen, bevor wir das Teilproblem x lösen. (Erinnern Sie sich aus Abschnitt B.4 daran, dass die Adjazenz-Relation nicht symmetrisch sein braucht.) Mit der Terminologie aus Kapitel 22 gesprochen, betrachten wir bei
372
15 Dynamische Programmierung
bottom-up dynamischem Programmieren die Knoten des Teilproblem-Graphen in einer Reihenfolge, die eine „umgekehrte topologische Sortierung“ des Teilproblem-Graphens darstellt (siehe Abschnitt 22.4). Anders formuliert, kein Teilproblem wird betrachtet, bis nicht alle die Teilprobleme, von denen dieses abhängt, gelöst worden sind. Analog dazu, kann man die top-down-Methode (mit Memoisation) für dynamische Programmierung als eine „Tiefensuche“ des Teilproblem-Graphen ansehen (siehe Abschnitt 22.3), verwendet man die Begrifflichkeit aus dem gleichen Kapitel. Die Größe des Teilproblem-Graphen G = (V, E) kann uns helfen, die Laufzeit des auf dynamischer Programmierung basierenden Algorithmus zu bestimmen. Da wir jedes Teilproblem nur einmal lösen, besteht die Laufzeit aus der Summe der Zeiten, die benötigt werden, um jedes Teilproblem zu lösen. Typischerweise ist die Zeit, um die Lösung eines Teilproblems zu berechnen, proportional zum Grad (Anzahl der ausgehenden Kanten) des zugehörigen Knoten in dem Teilproblem-Graphen und die Anzahl der Teilprobleme ist gleich der Anzahl der Knoten in dem Teilproblem-Graphen. In diesem üblichen Fall, ist die Laufzeit für dynamische Programmierung linear in der Anzahl der Knoten und Kanten.
Rekonstruieren einer Lösung Die Lösungen für das Stabzerlegungsproblem, die durch unser dynamisches Programm erzeugt werden, geben den Wert einer optimalen Lösung zurück, aber nicht die tatsächliche Lösung, d. h. wie der Eisenstab zu schneiden ist. Wir können den Ansatz der dynamischen Programmierung derart erweitern, dass nicht nur der optimale Wert für jedes Teilproblem berechnet wird, sondern auch eine Wahl, die zum optimalen Wert führt. Mit dieser Information können wir leicht eine optimale Lösung ausgeben. Hier nun eine erweiterte Version von Bottom-Up-Cut-Rod, die für jede Eisenstange der Länge j nicht nur den maximalen Erlös rj , sondern auch die optimale Größe sj des ersten Teilstabes, welches abgeschnitten wird, berechnet: Extended-Bottom-Up-Cut-Rod(p, n) 1 seien r[0 . . n] und s[0 . . n] neue Felder 2 r[0] = 0 3 for j = 1 to n 4 q = −∞ 5 for i = 1 to j 6 if q < p[i] + r[j − i] 7 q = p[i] + r[j − i] 8 s[j] = i 9 r[j] = q 10 return r und s Diese Prozedur ist Bottom-Up-Cut-Rod sehr ähnlich. Sie unterscheidet sich nur dadurch, dass sie in Zeile 1 ein Feld s erzeugt und in Zeile 8 den Eintrag s[j] aktualisiert, um die optimale Größe i des ersten Teilstabes, der abgeschnitten wird, zu speichern, wenn ein Teilproblem der Größe j gelöst wird.
15.1 Schneiden von Eisenstangen
373
Die folgende Prozedur erhält eine Preistafel p und eine Länge n einer Eisenstange als Eingabe. Sie ruft Extended-Bottom-Up-Cut-Rod auf, um das Feld s[1 . . n] der optimalen Längen der ersten Stäbe zu berechnen, und druckt dann die vollständige Liste der Stabgrößen in einer optimalen Zerlegung einer Eisenstange der Länge n aus: Print-Cut-Rod-Solution(p, n) 1 (r, s) = Extended-Bottom-Up-Cut-Rod(p, n) 2 while n > 0 3 print s[n] 4 n = n − s[n] In unserem Beispiel würde der Aufruf Extended-Bottom-Up-Cut-Rod(p, 10) die folgenden Felder zurückgeben: i 0 1 2 3 4 5 6 7 8 9 10 r[i] 0 1 5 8 10 13 17 18 22 25 30 s[i] 0 1 2 3 2 2 6 1 2 3 10 Ein Aufruf von Print-Cut-Rod-Solution(p, 10) würde einfach nur 10 ausgeben, aber ein Aufruf mit n = 7 würde 1 and 6 ausgeben, die den Positionen einer optimalen Zerlegung eines Eisenstabes der Länge 7 entsprechen.
Übungen 15.1-1 Zeigen Sie, dass Gleichung (15.4) aus Gleichung (15.3) folgt und die initiale Bedingung T (0) = 1 gilt. 15.1-2 Zeigen Sie mittels eines Gegenbeispiels, dass die folgende „gefräßige“ (engl.: greedy) Strategie Eisenstangen nicht immer optimal in Teilstäbe zerschneidet. Definieren Sie die Dichte eines Eisenstabes der Länge i als pi /i, also als den Preis pro Zentimeter. Die Greedy-Strategie für eine Eisenstange der Länge n schneidet als erstes ein Teilstab der Länge i ab, der maximale Dichte hat (1 ≤ i ≤ n). Sie fährt dann fort, indem sie die Greedy-Strategie auf die Reststange der Länge n − i anwendet. 15.1-3 Betrachten Sie eine Modifikation des Stabzerlegungsproblems, in der neben einem Preis pi für jeden Eisenstab jeder Schnitt fixe Kosten c verursacht. Der durch eine Lösung erzielbare Erlös ist nun die Summe der Preise der Teilstäbe abzüglich der Kosten, die durch die Schnitte anfallen. Geben Sie ein dynamisches Programm an, um dieses modifizierte Problem zu lösen. 15.1-4 Modifizieren Sie Memoized-Cut-Rod derart, dass nicht nur der Wert sondern auch die tatsächliche Lösung zurückgegeben wird. 15.1-5 Die Fibonacci-Zahlen sind durch die Rekursionsgleichung (3.22) definiert. Geben Sie ein dynamisches Programm an, das in Zeit O(n) die n-te FibonacciZahl berechnet. Zeichnen Sie den Teilproblem-Graph. Wie viele Knoten und Kanten enthält der Graph?
374
15.2
15 Dynamische Programmierung
Matrizen-Kettenmultiplikation
Unser nächstes Beispiel für dynamische Programmierung ist ein Algorithmus, der das Problem der Matrix-Kettenmultiplikation löst. Gegeben ist eine Folge (Kette) A1 , A2 , . . . , An von n Matrizen, die miteinander multipliziert werden sollen. Wir wollen das Produkt A1 · A2 · . . . · An
(15.5)
berechnen. Wir können den Ausdruck (15.5) auswerten, indem wir den Standardalgorithmus zur Multiplikation von Matrizenpaaren als Unterroutine verwenden, nachdem wir den Ausdruck vollständig geklammert haben, um alle Mehrdeutigkeiten, wie die Matrizen miteinander zu multiplizieren sind, zu beseitigen. Matrizenmultiplikation ist assoziativ und so führen alle Klammerungen zu dem gleichen Produkt. Ein Produkt von Matrizen wird als vollständig geklammert bezeichnet, wenn es sich entweder um eine einzelne Matrix oder um das Produkt zweier vollständig geklammerter Matrizenprodukte handelt, das selbst wieder von Klammern umschlossen ist. Wenn die Kette von Matrizen beispielsweise gleich A1 , A2 , A3 , A4 ist, dann kann das Produkt auf fünf verschiedene Weisen vollständig geklammert werden: (A1 (A2 (A3 A4 ))) (A1 ((A2 A3 )A4 )) ((A1 A2 )(A3 A4 )) ((A1 (A2 A3 ))A4 ) (((A1 A2 )A3 )A4 )
, , , , .
Die Art und Weise, in der wir eine Kette von Matrizen klammern, kann drastische Auswirkungen auf die Kosten der Berechnung des Produktes haben. Betrachten Sie zunächst die Kosten für die Multiplikation zweier Matrizen. Der Standardalgorithmus ist durch den folgenden Pseudocode gegeben, der die Prozedur Square-Matrix-Multiply aus Abschnitt 4.2 verallgemeinert. Die Attribute zeilen und spalten geben die Anzahl der Zeilen und Spalten einer Matrix an. Matrix-Multiply(A, B) 1 if A.spalten = B.zeilen 2 error “inkompatible Dimensionen” 3 else sei C eine neue A.zeilen × B.spalten-Matrix 4 for i = 1 to A.zeilen 5 for j = 1 to B.spalten 6 cij = 0 7 for k = 1 to A.spalten 8 cij = cij + aik · bkj 9 return C Wir können zwei Matrizen A und B nur dann miteinander multiplizieren, wenn sie zueinander kompatibel sind: Die Anzahl der Spalten von A muss gleich der Anzahl der
15.2 Matrizen-Kettenmultiplikation
375
Zeilen in B sein. Wenn A eine p × q-Matrix und B eine q × r-Matrix ist, dann ist die resultierende Matrix C eine p × r-Matrix. Die Zeit zur Berechnung von C wird durch die Anzahl der skalaren Multiplikationen in Zeile 8 dominiert. Diese Anzahl ist p · q · r. Im Folgenden werden wir die Kosten als Funktion der Anzahl skalarer Multiplikationen ausdrücken. Um die durch die unterschiedliche Klammerung eines Matrizenproduktes verursachten unterschiedlichen Kosten zu illustrieren, betrachten wir eine Kette A1 , A2 , A3 von drei Matrizen. Angenommen, die Dimensionen der Matrizen seien 10 × 100, 100 × 5 beziehungsweise 5×50. Wenn wir gemäß der Klammerung ((A1 A2 )A3 ) multiplizieren, führen wir 10·100·5 = 5.000 skalare Multiplikationen aus, um das 10×5-Matrizenprodukt A1 A2 zu berechnen, plus weitere 10 · 5 · 50 = 2.500 skalare Multiplikationen, um diese Matrix mit A3 zu multiplizieren, woraus sich insgesamt 7.500 skalare Multiplikationen ergeben. Wenn wir stattdessen gemäß der Klammerung (A1 (A2 A3 )) multiplizieren, führen wir 100 · 5 · 50 = 25.000 skalare Multiplikationen aus, um das 100 × 50-Matrizenprodukt A2 A3 zu berechnen, plus weitere 10 · 100 · 50 = 50.000 skalare Multiplikationen, um A1 mit dieser Matrix zu multiplizieren, woraus sich insgesamt 75.000 skalare Multiplikationen ergeben. Die Berechnung des Produktes gemäß der ersten Klammerung ist also 10-mal schneller. Wir definieren das Problem der Matrizen-Kettenmultiplikation wie folgt: Gegeben sei eine Kette A1 , A2 , . . . , An von n Matrizen. Für i = 1, 2, . . . , n besitzt die Matrix Ai die Dimension pi−1 × pi . Klammern Sie das Produkt A1 A2 · · · An derart vollständig, dass die Anzahl skalarer Multiplikationen minimiert wird. Beachten Sie, dass wir beim Problem der Matrizen-Kettenmultiplikation die Matrizen nicht wirklich multiplizieren. Unser Ziel besteht lediglich darin, eine Reihenfolge, in der Matrizen zu multiplizieren sind, zu bestimmen, die die niedrigsten Kosten verursacht. Üblicherweise wird die für das Bestimmen der optimalen Reihenfolge aufgewendete Zeit durch die Zeit mehr als wettgemacht, die wir später einsparen, wenn wir die Matrizenmultiplikationen tatsächlich ausführen (wie zum Beispiel nur 7.500 statt 75.000 skalarer Multiplikationen).
Zählen der Anzahl der Klammerungen Bevor wir das Problem der Matrizen-Kettenmultiplikation mithilfe der dynamischen Programmierung lösen, überzeugen wir uns davon, dass vollständiges Ausprobieren aller möglichen Klammerungen zu keinem effizienten Algorithmus führt. Bezeichnen wir die Anzahl möglicher Klammerungen einer Folge von n Matrizen mit P (n). Für n = 1 haben wir lediglich eine Matrix und deshalb auch nur eine Möglichkeit, das Matrizenprodukt vollständig zu klammern. Im Falle n ≥ 2 besteht das vollständig geklammerte Matrizenprodukt aus dem Produkt zweier vollständig geklammerter Matrizenteilprobleme. Der Schnitt, mit dem man die beiden Teilprodukte definiert, kann an einer beliebigen Stelle k in der Matrizenkette liegen, für ein k ∈ {1, 2, . . . , n − 1}. Somit erhalten wir die Rekursionsgleichung ⎧ ⎪ ⎨ 1n−1 P (n) = P (k)P (n − k) ⎪ ⎩ k=1
falls n = 1 , falls n ≥ 2 .
(15.6)
376
15 Dynamische Programmierung
In Problemstellung 12-4 sollen Sie zeigen, dass die Lösung einer ähnlichen Rekursionsgleichung die Folge der Catalan-Zahlen ist, die wie Ω(4n /n3/2 ) wachsen. Eine einfachere Aufgabe (siehe Übung 15.2-3) besteht darin, zu zeigen, dass die Lösung der Rekursionsgleichung (15.6) in Ω(2n ) liegt. Die Anzahl der möglichen Klammerungen wächst somit exponentiell mit n und eine erschöpfende vollständige Suche („bruteforce“-Methode) ist eine mehr als schlechte Strategie, eine optimale Klammerung einer Matrizenkette zu bestimmen.
Anwenden von dynamischem Programmieren Wir werden die Methode der dynamischen Programmierung anwenden, um eine optimale Klammerung einer Matrizenkette zu bestimmen. Dabei werden wir den folgenden vier Schritten folgen, die wir bereits zu Beginn des Kapitels angegeben haben: 1. Wir charakterisieren die Struktur einer optimalen Lösung. 2. Wir definieren den Wert einer optimalen Lösung rekursiv. 3. Wir berechnen den Wert einer optimalen Lösung. 4. Wir konstruieren eine optimale Lösung aus den berechneten Daten. Wir werden uns diese Schritte in der angegebenen Reihenfolge anschauen und zeigen, wie wir jeden dieser Schritte auf unserem Problem ausführen.
Schritt 1: Die Struktur einer optimalen Klammerung Zur Realisierung des ersten Schritts des Paradigma der dynamischen Programmierung haben wir die optimale Teilstruktur zu finden und diese zu benutzen, um aus optimalen Lösungen der Teilprobleme eine optimale Lösung von dem Problem zu konstruieren. Bei dem Problem der Matrizen-Kettenmultiplikation können wir diesen Schritt wie folgt ausführen. Der Einfachheit halber führen wir für die Matrix, die durch die Berechnung des Produktes Ai Ai+1 · · · Aj (i ≤ j) gebildet wird, die Bezeichnung Ai..j ein. Sie sollten bemerken, dass dann, wenn das Problem nicht trivial ist, d. h. wenn i < j gilt, wir das Produkt zwischen Ak und Ak+1 für eine ganze Zahl k aus dem Bereich i ≤ k < j aufschneiden müssen, um das Produkt Ai Ai+1 · · · Aj vollständig zu klammern. Das heißt, für einen Wert k berechnen wir zunächst die Matrizen Ai..k und Ak+1..j und multiplizieren sie dann, um das endgültige Produkt Ai..j zu erhalten. Die Kosten dieser Klammerung ergeben sich daher aus den Kosten für die Berechnung der Matrix Ai..k , zuzüglich der Kosten für die Berechnung von Ak+1..j und der Kosten, um beide miteinander zu multiplizieren. Die optimale Teilstruktur dieses Problems ergibt sich folgendermaßen. Nehmen Sie an, dass wir das Produkt zwischen Ak und Ak+1 aufspalten würden, um Ai Ai+1 · · · Aj optimal zu klammern. Dann muss die Weise, in der wir die „Präfix“-Teilkette Ai Ai+1 · · · Ak innerhalb einer optimalen Klammerung von Ai Ai+1 · · · Aj klammern, eine optimale Klammerung von Ai Ai+1 · · · Ak sein. Weshalb? Wenn es eine kostengünstigere Möglichkeit gäbe, Ai Ai+1 · · · Ak zu klammern, dann könnten wir diese Klammerung in der optimalen Klammerung von Ai Ai+1 · · · Aj einsetzen, um so eine andere Klammerung von Ai Ai+1 · · · Aj zu erhalten, deren Kosten kleiner als die des Optimums wä-
15.2 Matrizen-Kettenmultiplikation
377
ren: Widerspruch! Eine ähnliche Beobachtung trifft für die Klammerung der Teilkette Ak+1 Ak+2 · · · Aj in der optimalen Klammerung von Ai Ai+1 · · · Aj zu: Sie muss eine optimale Klammerung von Ak+1 Ak+2 · · · Aj sein. Nun benutzen wir die optimale Teilstruktur, um zu zeigen, dass wir eine optimale Lösung des Problems aus den optimalen Lösungen der Teilprobleme konstruieren können. Wir haben bereits gesehen, dass jede Lösung für eine nichttriviale Instanz des Problems der Matrizen-Kettenmultiplikation das Aufteilen des Produktes erfordert, und dass jede optimale Lösung ihrerseits optimale Lösungen für Teilproblem-Instanzen beinhaltet. Somit können wir eine optimale Lösung für eine Instanz des Problems der MatrizenKettenmultiplikation konstruieren, indem wir das Problem in Teilprobleme (optimales Klammern von Ai Ai+1 · · · Ak und Ak+1 Ak+2 · · · Aj ) zerlegen, optimale Lösungen für die Teilproblem-Instanzen finden und anschließend diese optimalen Teilproblemlösungen kombinieren. Wenn wir nach der korrekten Position zum Aufteilen des Produktes suchen, dann müssen wir sicherstellen, dass wir alle möglichen Positionen anschauen. Damit gehen wir sicher, auch die optimale betrachtet zu haben.
Schritt 2: Eine rekursive Lösung Als nächstes definieren wir rekursiv die Kosten für eine optimale Lösung als Funktion der optimalen Lösungen der Teilprobleme. Für das Problem der Matrix-Kettenmultiplikation haben wir die minimalen Kosten zu bestimmen, die anfallen, um Ai Ai+1 · · · Aj für 1 ≤ i ≤ j ≤ n zu klammern. Sei m[i, j] das Minimum für die Anzahl der zur Berechnung der Matrix Ai..j benötigten skalaren Multiplikationen. Für das Gesamtproblem wären die Kosten für die billigste Möglichkeit, A1..n zu berechnen, demnach m[1, n]. Wir können m[i, j] folgendermaßen rekursiv definieren. Für i = j ist das Problem trivial. Dann besteht die Kette aus nur einer Matrix Ai..i = Ai , sodass zur Berechnung des Produktes keine skalaren Multiplikationen notwendig sind. Damit gilt m[i, i] = 0 für i = 1, 2, . . . , n. Um m[i, j] für i < j zu berechnen, nutzen wir die in Schritt 1 gefundene Struktur einer optimalen Lösung aus. Lassen Sie uns annehmen, dass wir das Produkt Ai Ai+1 · · · Aj zwischen Ak und Ak+1 aufspalten würden, um das Produkt optimal zu klammern, wobei i ≤ k < j gilt. Dann ist m[i, j] gleich den minimalen Kosten, um die Teilprodukte Ai..k und Ak+1..j zu berechnen, zuzüglich der Kosten für die Multiplikation dieser beiden Matrizen. Rufen wir uns in Erinnerung, dass jede Matrix Ai die Dimension pi−1 × pi hat, so sehen wir, dass zur Berechnung des Matrizenproduktes Ai..k Ak+1..j genau pi−1 · pk · pj skalare Multiplikationen notwendig sind. Damit erhalten wir m[i, j] = m[i, k] + m[k + 1, j] + pi−1 pk pj . Diese rekursive Gleichung setzt voraus, dass wir den Wert k kennen, was nicht der Fall ist. Es gibt jedoch nur j − i mögliche Werte für k, nämlich k = i, i + 1, . . . , j − 1. Da die optimale Klammerung einen dieser Werte von k verwenden muss, müssen wir nur alle testen, um die beste zu finden. Somit ergibt sich unsere rekursive Definition der minimalen Kosten für die Klammerung des Produktes Ai Ai+1 · · · Aj durch < m[i, j] =
0
falls i = j ,
min {m[i, k] + m[k + 1, j] + pi−1 pk pj } falls i < j .
i≤k 1 .
k=1
Beachten wir, dass für i = 1, 2, . . . , n − 1 jeder Term T (i) einmal als T (k) und einmal als T (n − k) auftaucht, und addieren wir die n − 1 Einsen in der Summenformel zu der Eins vor dem Summenzeichen, so können wir die Rekursionsgleichung in der Form T (n) ≥ 2
n−1
(15.8)
T (i) + n
i=1
schreiben. Wir werden unter Verwendung der Substitutionsmethode beweisen, dass T (n) = Ω(2n ) gilt. Speziell werden wir zeigen, dass für alle n ≥ 1 die Ungleichung T (n) ≥ 2n−1 gilt. Der Induktionsanfang ist wegen T (1) ≥ 1 = 20 einfach. Induktiv ergibt sich für n ≥ 2 T (n) ≥ 2 =2
n−1 i=1 n−2
2i−1 + n 2i + n
i=0 n−1
= 2(2 − 1) + n = 2n − 2 + n ≥ 2n−1 ,
(wegen Gleichung (A.5))
womit der Beweis fertig ist. Der durch den Aufruf Recursive-Matrix-Chain(p, 1, n) verursachte gesamte Arbeitsaufwand ist also mindestens exponentiell in n. Vergleichen Sie diesen rekursiven top-down-Algorithmus (ohne Memoisation) mit einem dynamisch programmierten bottom-up-Algorithmus. Letzterer ist effizienter, da er die Eigenschaft überlappender Teilprobleme ausnutzt. Die Matrizen-Kettenmultiplikation hat nur Θ(n2 ) verschiedene Teilprobleme, und das dynamische Programm löst jedes genau einmal. Dagegen muss der rekursive Algorithmus jedes Teilproblem immer wieder neu lösen, wenn es im Rekursionsbaum wieder auftaucht. Immer wenn ein Rekursionsbaum für die natürliche rekursive Lösung eines Problems wiederholt dasselbe Teilproblem enthält und die Gesamtanzahl der Teilprobleme klein ist, dann kann dynamische Programmierung die Effizienz verbessern, manchmal sogar drastisch.
390
15 Dynamische Programmierung
Rekonstruktion einer optimalen Lösung Aus praktischen Gründen speichern wir in der Regel die Entscheidung, die wir bei jedem Teilproblem getroffen haben, in einer Tabelle ab, sodass wir diese Information nicht aus den Kosten, die wir gespeichert haben, rekonstruieren brauchen. Bei der Matrizen-Kettenmultiplikation spart die Tabelle s[i, j] uns einen signifikanten Teil der Arbeit bei der Rekonstruktion einer optimalen Lösung. Nehmen Sie an, wir könnten nicht auf die Tabelle s[i, j] zurückgreifen und hätten nur die Tabelle m[i, j] ausgefüllt, die die optimalen Kosten der Teilprobleme enthält. Wir treffen unsere Entscheidung aus j − i Möglichkeiten, wenn wir bestimmen wollen, welche Teilprobleme für eine optimale Lösung zur Klammerung von Ai Ai+1 · · · Aj zu verwenden sind. Da j − i keine Konstante ist, wäre Zeit Θ(j − i) = ω(1) notwendig, um zu rekonstruieren, welche Teilprobleme wir für die Lösung eines gegebenen Problems verwendet haben. Indem wir in s[i, j] den Index der Matrix speichern, an der wir das Produkt Ai Ai+1 · · · Aj aufspalten, können wir jede Entscheidung in Zeit O(1) rekonstruieren.
Memoisation Wie wir bei dem Stabzerlegungsproblem gesehen haben, gibt es einen alternativen Ansatz zu dynamischer Programmierung, der oft genau so effizient wie der bottom-upAnsatz von dynamischer Programmierung ist, aber weiterhin top-down arbeitet. Die Idee besteht darin, den gewöhnlichen, aber ineffizienten, rekursiven Algorithmus zu memoisieren. Wie in dem bottom-up-Ansatz verwalten wir eine Tabelle mit den Teilproblemlösungen, aber die Kontrollstruktur zum Ausfüllen der Tabelle entspricht eher dem rekursiven Algorithmus. Ein memoisierter rekursiver Algorithmus verwaltet einen Tabelleneintrag für die Lösung jedes Teilproblems. Jeder Tabelleneintrag enthält anfangs einen speziellen Wert, um zu kennzeichnen, dass der Eintrag noch eingetragen werden muss. Wenn uns während der Ausführung des rekursiven Algorithmus das Teilproblem zum ersten Mal begegnet, wird dessen Lösung berechnet und anschließend in der Tabelle gespeichert. Wenn wir das Teilproblem später wieder begegnen, schlagen wir den in der Tabelle gespeicherten Wert einfach nur nach und geben ihn zurück.5 Hier ist nun eine memoisierte Version von Recursive-Matrix-Chain. Überlegen Sie sich, an welchen Stellen die Prozedur der memoisierten top-down-Methode für das Stabzerlegungsproblem ähnelt. Memoized-Matrix-Chain(p) 1 n = p.l¨a nge − 1 2 sei m[1 . . n, 1 . . n] eine neue Tabelle 3 for i = 1 to n 4 for j = i to n 5 m[i, j] = ∞ 6 return Lookup-Chain(m, p, 1, n) 5 Diese Herangehensweise setzt voraus, dass wir die Menge aller möglichen Teilproblem-Parameter kennen und dass wir die Beziehung zwischen Tabellenpositionen und Teilproblemen festgelegt haben. Ein anderer, allgemeinerer Ansatz besteht darin, Hashing im Rahmen von Memoisation zu benutzen, wobei die Teilproblem-Parameter als Schlüssel dienen.
15.3 Elemente dynamischer Programmierung
391
Lookup-Chain(m, p, i, j) 1 if m[i, j] < ∞ 2 return m[i, j] 3 if i = = j 4 m[i, j] = 0 5 else for k = i to j − 1 6 q = Lookup-Chain(m, p, i, k) + Lookup-Chain(m, p, k + 1, j) + pi−1 pk pj 7 if q < m[i, j] 8 m[i, j] = q 9 return m[i, j] Wie die Prozedur Matrix-Chain-Order verwaltet auch Memoized-Matrix-Chain eine Tabelle m[1 . . n, 1 . . n] mit den berechneten Werten von m[i, j], der minimalen Anzahl skalarer Multiplikationen, die zur Berechnung der Matrix Ai..j notwendig sind. Jeder Tabelleneintrag enthält anfangs den Wert ∞, um zu kennzeichnen, dass der Eintrag noch nicht erfolgt ist. Wenn der Aufruf Lookup-Chain(m, p, i, j) ausgeführt wird und Zeile 1 feststellt, dass m[i, j] < ∞ gilt, dann gibt die Prozedur in Zeile 2 einfach die vorher bereits berechneten Kosten m[i, j] zurück. Anderenfalls werden die Kosten wie in Recursive-Matrix-Chain berechnet, in m[i, j] gespeichert und zurückgegeben. Somit gibt Lookup-Chain(m, p, i, j) immer den Wert m[i, j] zurück, berechnet ihn aber nur beim erstmaligen Aufruf von Lookup-Chain mit diesen spezifischen Werten von i und j. Abbildung 15.7 illustriert, wie Memoized-Matrix-Chain im Vergleich zu RecursiveMatrix-Chain Zeit einspart. Schattierte Teilbäume zeigen die Werte, die die Prozedur nachschlägt anstatt sie erneut zu berechnen. Wie der bottom-up-Algorithmus Matrix-Chain-Order läuft die Prozedur MemoizedMatrix-Chain in Zeit O(n3 ). Zeile 5 der Prozedur Memoized-Matrix-Chain wird Θ(n2 ) oft ausgeführt. Wir können die Aufrufe von Lookup-Chain in zwei Klassen einteilen: 1. Aufrufe, bei denen m[i, j] = ∞ gilt, sodass die Zeilen 3–9 ausgeführt werden, und 2. Aufrufe, bei denen m[i, j] < ∞ gilt, sodass Lookup-Chain einfach den Wert in Zeile 2 zurückgibt. Es gibt Θ(n2 ) Aufrufe des ersten Typs, einen pro Tabelleneintrag. Alle Aufrufe des zweiten Typs sind rekursive Aufrufe, die durch Aufrufe des ersten Typs ausgelöst werden. Immer wenn ein vorgegebener Aufruf von Lookup-Chain zu rekursiven Aufrufen führt, sind dies genau O(n) Aufrufe. Deshalb gibt es insgesamt O(n3 ) Aufrufe des zweiten Typs. Jeder Aufruf des zweiten Typs benötigt Zeit O(1) und jeder des ersten Typs O(n) zuzüglich der in den rekursiven Aufrufen aufgewendeten Zeit. Die Gesamtlaufzeit ist deshalb in O(n3 ). Memoisation verwandelt somit einen Algorithmus mit Laufzeit Ω(2n ) in einen Algorithmus mit Laufzeit O(n3 ).
392
15 Dynamische Programmierung
Zusammenfassend können wir sagen, dass wir das Problem der Matrix-Kettenmultiplikation mithilfe dynamischer Programmierung entweder über einen memoisierten topdown-Ansatz oder über einen bottom-up-Algorithmus in Zeit O(n3 ) lösen können. Beide Methoden nutzen die Eigenschaft überlappender Teilprobleme. Es gibt insgesamt nur Θ(n2 ) verschiedene Teilprobleme und jede dieser Methoden berechnet die Lösung eines Teilproblems nur einmal. Ohne Memoisation läuft der natürliche rekursive Algorithmus in exponentieller Zeit, da bereits gelöste Teilprobleme wiederholt gelöst werden. Allgemein gilt, dass, falls alle Teilprobleme mindestens einmal gelöst werden müssen, ein auf dynamischer Programmierung basierender bottom-up-Algorithmus gewöhnlich um einen konstanten Faktor besser als der zugehörige top-down-Algorithmus mit Memoisation ist. Dies ist darauf zurückzuführen, dass der bottom-up-Algorithmus keinen Aufwand für die Rekursionen und weniger Aufwand für das Verwalten der Tabelle hat. Darüber hinaus können wir bei einigen Problemen das reguläre Muster der Tabellenzugriffe ausnutzen, um den Zeitaufwand oder den Speicherbedarf noch weiter zu reduzieren. Wenn dagegen einige Teilprobleme aus dem Teilproblem-Raum überhaupt nicht gelöst werden müssen, besitzt die memoisierte Variante den Vorteil, nur die letztlich benötigten Teilprobleme zu lösen.
Übungen 15.3-1 Welcher ist der effizientere Weg, die optimale Anzahl an skalaren Multiplikationen beim Problem der Matrizen-Kettenmultiplikation zu bestimmen: alle Möglichkeiten für die Klammerung des Produktes aufzuzählen und für jedes die Anzahl der Multiplikationen zu berechnen, oder Recursive-Matrix-Chain laufen zu lassen? Begründen Sie Ihre Antwort! 15.3-2 Zeichnen Sie den Rekursionsbaum für die Prozedur Merge-Sort aus Abschnitt 2.3.1 angewendet auf ein Feld aus 16 Elementen. Erklären Sie, warum Memoisation scheitert, einen guten Teile-und-Beherrsche-Algorithmus wie beispielsweise Merge-Sort zu beschleunigen. 15.3-3 Betrachten Sie eine Variante des Problems der Matrizen-Kettenmultiplikation, bei der das Ziel darin besteht, die Folge der Matrizen so zu klammern, dass die Anzahl der skalaren Multiplikationen maximiert statt minimiert wird. Besitzt dieses Problem die optimale-Teilstruktur-Eigenschaft? 15.3-4 Wie bereits dargestellt, lösen wir bei dynamischer Programmierung zuerst die Teilprobleme und entscheiden dann, welche von ihnen für eine optimale Lösung des Problems zu verwenden sind. Professor Capulet behauptet, dass wir nicht immer alle Teilprobleme lösen brauchen, um eine optimale Lösung zu finden. Sie behauptet, dass wir eine optimale Lösung des Problems der Matrizen-Kettenmultiplikation finden können, indem wir stets die Matrix Ak , nach dem wir das Teilprodukt Ai Ai+1 · · · Aj teilen, bestimmen können (indem wir k so wählen, dass der Wert pi−1 · pk · pj minimal ist), bevor wir die Teilprobleme gelöst haben. Finden Sie eine Instanz des Problems der MatrizenKettenmultiplikation, für das diese Greedy-Methode eine suboptimale Lösung liefert.
15.4 Längste gemeinsame Teilsequenz
393
15.3-5 Nehmen Sie an, wir hätten bei unserem Stabzerlegungsproblem aus Abschnitt 15.1 für jedes i = 1, 2, . . . , n eine obere Schranke li für die Anzahl der Teilstäbe der Länge i, die wir produzieren dürfen. Zeigen Sie, dass dieses Problem die optimale-Teilstruktur-Eigenschaft, wie sie in Abschnitt 15.1 beschrieben worden ist, nicht mehr besitzt. 15.3-6 Stellen Sie sich vor, Sie würden eine Währung in eine andere tauschen wollen. Sie stellen fest, dass es möglicherweise geschickter ist, eine Reihe von Tauschgeschäften in andere Währungen durchzuführen, bevor Sie schlussendlich in die gewünschte Währung tauschen, anstatt direkt in die gewünschte Währung zu tauschen. Setzen Sie voraus, dass Sie in n verschiedene Währungen tauschen können, die mit 1, 2, . . . , n durchnummeriert sind, wobei Sie mit der Währung 1 starten und schlussendlich die Währung n haben wollen. Gegeben haben Sie für jedes Paar von Währungen i und j einen Umtauschkurs rij , der angibt, dass Sie d Einheiten der Währung i in d · rij Einheiten der Währung j eintauschen können. Eine Folge von Tauschgeschäften bringt eine Courtage mit sich, die von der Anzahl der Tauschgeschäfte abhängt. Sei ck die Höhe der Courtage, die Sie bezahlen müssen, wenn Sie k Tauschgeschäfte machen. Zeigen Sie, dass, wenn ck = 0 für alle k = 1, 2, . . . , n gilt, das Problem eine beste Sequenz von Geschäften, die die Währung 1 in die Währung n tauscht, die optimale-Teilstruktur-Eigenschaft aufweist. Zeigen Sie dann, dass das Problem nicht notwendigerweise die optimale-Teilstruktur-Eigenschaft aufweist, wenn die Courtagen ck beliebige Werte annehmen können.
15.4
Längste gemeinsame Teilsequenz
Biologische Anwendungen müssen häufig die DNA von zwei (oder mehr) verschiedenen Organismen vergleichen. Ein DNA-Strang besteht aus einer Kette von Molekülen, die als Basen bezeichnet werden, wobei es sich bei den möglichen Basen um Adenin, Cytosin, Guanin und Thymin handelt. Stellen wir jede Base durch ihren Anfangsbuchstaben dar, so können wir einen DNA-Strang als einen String über der endlichen Menge {A, C, G, T} darstellen. (Siehe Anhang C für die Definition eines Strings.) Die DNA eines Organismus kann zum Beispiel S1 = ACCGGTCGAGTGCGCGGAAGCCGGCCGAA sein, während S2 = GTCGTTCGGAATGCCGTTGCTCTGTAAA die DNA eines anderen Organismus darstellt. Ein Ziel des Vergleichens zweier DNA-Stränge besteht darin, zu bestimmen, wie „ähnlich“ die Stränge sind. Dies ist ein Maß dafür, wie eng die beiden Organismen miteinander verwandt sind. Wir können, und wir tun es auch, den Begriff der Ähnlichkeit in vielen verschiedenen Art und Weisen definieren. Beispielsweise können wir definieren, dass zwei DNA-Stränge ähnlich sind, wenn der eine ein Teilstring des anderen ist. (In Kapitel 32 werden Algorithmen zur Lösung dieses Problems untersucht.) In unserem Beispiel ist weder S1 noch S2 ein Teilstring des jeweils anderen Stranges. Alternativ dazu könnten wir definieren, dass zwei Stränge ähnlich sind, wenn die Anzahl der notwendigen Veränderungen, um den einen in den anderen zu überführen, klein ist. (Problemstellung 15-5 befasst sich mit dieser Idee.) Eine weitere Möglichkeit, die Ähnlichkeit der Stränge S1 und S2 zu messen, besteht darin, einen dritten Strang S3 zu finden, der die Eigenschaft
394
15 Dynamische Programmierung
hat, dass jede seiner Basen auch in S1 und S2 vorkommt. Dabei müssen diese Basen in der gleichen Reihenfolge, aber nicht notwendigerweise hintereinander auftreten. Je länger der Strang S3 ist, den wir finden können, desto ähnlicher sind sich S1 und S2 . In unserem Beispiel besteht der längste solche Strang S3 aus GTCGTCGGAAGCCGGCCGAA. Wir formalisieren den letztgenannten Begriff der Ähnlichkeit als das Problem der längsten gemeinsamen Teilsequenz. Eine Teilsequenz einer gegebenen Sequenz ist dabei gerade die gegebene Sequenz, aus der wir kein oder mehrere Elemente entfernt haben. Ist eine Sequenz X = x1 , x2 , . . . , xm gegeben, dann ist eine andere Sequenz Z = z1 , z2 , . . . , zk eine Teilsequenz von X, wenn es eine streng steigende Sequenz i1 , i2 , . . . , ik von Indizes von X gibt, sodass für alle j = 1, 2, . . . , k die Gleichung xij = zj gilt. Zum Beispiel ist Z = B, C, D, B eine Teilsequenz von X = A, B, C, B, D, A, B mit der zugehörigen Indexsequenz 2, 3, 5, 7. Sind zwei Sequenzen X und Y gegeben, dann sagen wir, dass eine Sequenz Z eine gemeinsame Teilsequenz von X und Y ist, wenn Z sowohl von X als auch von Y eine Teilsequenz ist. Wenn zum Beispiel X = A, B, C, B, D, A, B und Y = B, D, C, A, B, A gilt, dann ist die Sequenz B, C, A eine gemeinsame Teilsequenz von X und Y . Die Sequenz B, C, A ist jedoch nicht die längste gemeinsame Teilsequenz (engl.: longest common subsequenz , abgekürzt LCS) von X und Y , denn ihre Länge ist 3, während die Sequenz B, C, B, A, die sowohl Teilsequenz von X als auch von Y ist, die Länge 4 besitzt. Die Sequenz B, C, B, A ist ebenso wie die Sequenz B, D, A, B eine LCS von X und Y , da X und Y keine gemeinsame Teilsequenz der Länge 5 oder länger besitzen. Beim Problem der längsten gemeinsamen Teilsequenz sind zwei Sequenzen X = x1 , x2 , . . . , xm und Y = y1 , y2 , . . . , yn gegeben, deren längste gemeinsame Teilsequenz zu bestimmen ist. Dieser Abschnitt zeigt, wie das LCS-Problem mittels dynamischer Programmierung effizient gelöst werden kann.
Schritt 1: Charakterisierung einer LCS In einem brute-force-Ansatz für das LCS-Problem würden wir alle Teilsequenzen von X aufzählen und jede Teilsequenz überprüfen, ob diese auch eine Teilsequenz von Y ist. Die längste gefundene Teilsequenz würden wir uns merken. Jede Teilsequenz von X entspricht einer Untermenge der Indizes {1, 2, . . . , m} von X. Da X 2m Teilsequenzen besitzt, benötigt diese Methode exponentielle Zeit, was sie für lange Sequenzen ungeeignet macht. Das LCS-Problem besitzt jedoch die optimale-Teilstruktur-Eigenschaft, wie das folgende Theorem zeigt. Wie wir sehen werden, entspricht die natürliche Klasse von Teilproblemen Paaren von „Präfixen“ der zwei Eingabesequenzen. Um es formaler formulieren zu können, definieren wir zu einer gegebenen Sequenz X = x1 , x2 , . . . , xm den iten Präfix von X für i = 0, 1, . . . , m als Xi = x1 , x2 , . . . , xi . Zum Beispiel ist für X = A, B, C, B, D, A, B der vierte Präfix X4 gleich A, B, C, B und X0 ist die leere Sequenz. Theorem 15.1: (Optimale Teilstruktur einer LCS) Seien X = x1 , x2 , . . . , xm und Y = y1 , y2 , . . . , yn zwei Sequenzen und sei Z = z1 , z2 , . . . , zk eine LCS von X und Y .
15.4 Längste gemeinsame Teilsequenz
395
1. Ist xm = yn , so gilt zk = xm = yn und Zk−1 ist eine LCS von Xm−1 und Yn−1 . 2. Ist xm = yn , so folgt aus zk = xm , dass Z eine LCS von Xm−1 und Y ist. 3. Ist xm = yn , so folgt aus zk = yn , dass Z eine LCS von X und Yn−1 ist. Beweis: (1) Wenn zk = xm wäre, dann könnten wir xm = yn an Z anhängen, um eine gemeinsame Teilsequenz von X und Y der Länge k + 1 zu erhalten, was unserer Annahme, dass Z eine längste gemeinsame Teilsequenz von X und Y ist, widerspricht. Somit muss zk = xm = yn gelten. Nun ist der Präfix Zk−1 eine gemeinsame Teilsequenz von Xm−1 und Yn−1 der Länge k − 1. Wir wollen zeigen, dass dies eine LCS ist und die Annahme zum Widerspruch führen, dass es eine gemeinsame Teilsequenz W von Xm−1 und Yn−1 gibt, deren Länge größer als k − 1 ist. Gäbe es eine solche Teilsequenz W , dann würde das Anhängen von xm = yn an W zu einer gemeinsamen Teilsequenz von X und Y führen, deren Länge größer als k ist. Das wäre ein Widerspruch. (2) Wenn zk = xm gilt, dann ist Z eine gemeinsame Teilsequenz von Xm−1 und Y . Wenn es eine gemeinsame Teilsequenz W von Xm−1 und Y mit einer Länge größer als k gäbe, dann wäre W auch eine gemeinsame Teilsequenz von Xm und Y , was der Voraussetzung widersprechen würde, dass Z eine LCS von X und Y ist. (3) Punkt 3 beweist man analog zu (2).
Die Art und Weise, in der Theorem 15.1 längste gemeinsame Teilsequenzen charakterisiert, zeigt uns, dass eine LCS von zwei Sequenzen eine LCS von Präfixen der zwei Sequenzen enthält. Damit besitzt das LCS-Problem die optimale-Teilstruktur-Eigenschaft. Eine rekursive Lösung besitzt auch die Eigenschaft überlappender Teilprobleme, wie wir gleich sehen werden.
Schritt 2: Eine rekursive Lösung Aus Theorem 15.1 folgt, dass wir entweder ein oder zwei Teilprobleme zu untersuchen haben, wenn eine LCS von X = x1 , x2 , . . . , xm und Y = y1 , y2 , . . . , yn bestimmt werden soll. Wenn xm = yn gilt, müssen wir eine LCS von Xm−1 und Yn−1 finden. Hängen wir xm = yn an diese LCS an, dann erhalten wir eine LCS von X und Y . Wenn xm = yn gilt, dann müssen wir zwei Teilprobleme lösen, nämlich eine LCS von Xm−1 und Y und eine LCS von X und Yn−1 bestimmen. Die längere der beiden LCS ist eine LCS von X und Y . Da diese Fälle alle Möglichkeiten abdecken, wissen wir, dass eine der optimalen Teilproblemlösungen in einer LCS von X und Y verwendet werden muss. Wir können die Eigenschaft überlappender Teilprobleme beim Problem der LCS leicht erkennen. Um eine LCS von X und Y zu finden, müssen wir LCS von X und Yn−1 und von Xm−1 und Y finden. Aber jedes dieser Teilprobleme besitzt wiederum das Teilproblem, eine LCS von Xm−1 und Yn−1 zu finden. Viele andere Teilprobleme haben ihrerseits Teilteilprobleme gemeinsam. Wie beim Problem der Matrizen-Kettenmultiplikation beinhaltet unser rekursiver Ansatz für das LCS-Problem die Bestimmung einer Rekursionsgleichung für den Wert der
396
15 Dynamische Programmierung
optimalen Lösung. Wir definieren c[i, j] als die Länge einer LCS der Sequenzen Xi und Yj . Wenn entweder i = 0 oder j = 0 gilt, dann hat eine der Sequenzen die Länge 0 und so hat die LCS die Länge 0. Die optimale Teilstruktur des LCS-Problems führt auf die rekursive Gleichung ⎧ wenn i = 0 oder j = 0 , ⎨0 wenn i, j > 0 und xi = yj , c[i, j] = c[i − 1, j − 1] + 1 (15.9) ⎩ max(c[i, j − 1], c[i − 1, j]) wenn i, j > 0 und xi = yj . Beachten Sie, dass in dieser rekursiven Formulierung eine Beschaffenheit des Problems festlegt, welche Teilprobleme wir betrachten sollen. Wenn xi = yj gilt, dann können und sollten wir das Teilproblem betrachten, eine LCS von Xi−1 und Yj−1 zu bestimmen. Anderenfalls betrachten wir die beiden Teilprobleme, eine LCS von Xi und Yj−1 und eine von Xi−1 und Yj zu bestimmen. Bei den vorherigen dynamischen Programmen, die wir untersucht haben – für das Zerlegen von Eisenstangen und die Multiplkation von Matrizenketten – wurden keine Teilprobleme aufgrund von Beschaffenheiten des speziellen Problems ausgeschlossen. Die LCS-Berechnung ist aber nicht das einzige dynamische Programm, bei dem Teilprobleme aufgrund von Problembeschaffenheiten ausgeklammert werden. Das Editierdistanz-Problem (siehe Problemstellung 15-5) besitzt ebenfalls dieses Charakteristikum.
Schritt 3: Berechnung der Länge einer LCS Auf der Grundlage von Gleichung (15.9) könnten wir leicht einen rekursiven Algorithmus mit exponentieller Zeit schreiben, um die Länge einer LCS zweier Sequenzen zu berechnen. Da das LCS-Problem nur Θ(mn) verschiedene Teilprobleme besitzt, können wir aber dynamische Programmierung anwenden, um die Lösung bottom-up zu berechnen. Die Prozedur LCS-Length verwendet als Eingabe zwei Sequenzen X = x1 , x2 , . . . , xm und Y = y1 , y2 , . . . , yn . Sie speichert die Werte c[i, j] in einer Tabelle c[0 . . m, 0 . . n], deren Einträge zeilenweise berechnet werden. (Das heißt, die Prozedur füllt zuerst die erste Zeile von c von links nach rechts, dann die zweite Zeile von c und so weiter.) Die Prozedur verwaltet außerdem die Tabelle b[1 . . m, 1 . . n], um uns zu helfen, eine optimale Lösung auch wirklich zu konstruieren. Sie können sich das so vorstellen, als ob b[i, j] auf den Tabelleneintrag zeigen würde, welcher der optimalen Teilproblemlösung entspricht, die bei der Berechnung von c[i, j] ausgewählt wird. Die Prozedur gibt die Tabellen b und c zurück; c[m, n] enthält die Länge einer LCS von X und Y .
15.4 Längste gemeinsame Teilsequenz
397
LCS-Length(X, Y ) 1 m = X.l¨a nge 2 n = Y.l¨a nge 3 seien b[1 . . m, 1 . . n] und c[0 . . m, 0 . . n] neue Tabellen 4 for i = 1 to m 5 c[i, 0] = 0 6 for j = 0 to n 7 c[0, j] = 0 8 for i = 1 to m 9 for j = 1 to n 10 if xi = = yj 11 c[i, j] = c[i − 1, j − 1] + 1 12 b[i, j] = “” 13 elseif c[i − 1, j] ≥ c[i, j − 1] 14 c[i, j] = c[i − 1, j] 15 b[i, j] = “↑” 16 else c[i, j] = c[i, j − 1] 17 b[i, j] = “←” 18 return c und b Abbildung 15.8 zeigt die Tabelle, die die Prozedur LCS-Length, angewendet auf die Sequenzen X = A, B, C, B, D, A, B und Y = B, D, C, A, B, A erzeugt. Die Laufzeit der Prozedur ist Θ(mn), da für die Berechnung jedes Tabelleneintrages Zeit Θ(1) benötigt wird.
Schritt 4: Konstruktion einer LCS Die von LCS-Length zurückgegebene Tabelle b erlaubt uns, eine LCS von X = x1 , x2 , . . . , xm und Y = y1 , y2 , . . . , yn zu konstruieren. Wir beginnen einfach bei b[m, n] und folgen den Pfeilen durch die Tabelle. Immer, wenn wir bei einem Eintrag b[i, j] auf das Symbol „“ stoßen, folgt daraus, dass xi = yj ein Element der LCS ist, die LCS-Length gefunden hat. Die Elemente der LCS treten bei dieser Methode in umgekehrter Reihenfolge auf. Die folgende rekursive Prozedur gibt eine LCS von X und Y in der korrekten, vorwärts gerichteten Reihenfolge aus. Der initiale Aufruf lautet Print-LCS(b, X, X.l¨a nge, Y.l¨a nge). Print-LCS(b, X, i, j) 1 if i = = 0 or j = = 0 2 return 3 if b[i, j] = = “” 4 Print-LCS(b, X, i − 1, j − 1) 5 print xi 6 elseif b[i, j] = = “↑” 7 Print-LCS(b, X, i − 1, j) 8 else Print-LCS(b, X, i, j − 1)
398
15 Dynamische Programmierung j
i
0
1
2
3
4
5
6
yj
B
D
C
A
B
A
0
xi
0
0
0
0
0
0
0
1
A
0
0
0
0
1
1
1
2
B
0
1
1
1
1
2
2
3
C
0
1
1
2
2
2
2
4
B
0
1
1
2
2
3
3
5
D
0
1
2
2
2
3
3
6
A
0
1
2
2
3
3
4
7
B
0
1
2
2
3
4
4
Abbildung 15.8: Die von LCS-Length auf den Sequenzen X = A, B, C, B, D, A, B und Y = B, D, C, A, B, A berechneten Tabellen c und b. Das Quadrat in Zeile i und Spalte j enthält den Wert von c[i, j] und den zu dem Wert b[i, j] gehörigen Zeiger. Der Eintrag 4 in c[7, 6] – der unteren rechten Ecke der Tabelle – ist die Länge einer LCS B, C, B, A von X und Y . Für i, j > 0 hängt der Eintrag c[i, j] nur davon ab, ob xi = yj gilt, sowie von den Werten der Einträge c[i − 1, j], c[i, j − 1] und c[i − 1, j − 1], die vor c[i, j] berechnet wurden. Um die Elemente einer LCS zu rekonstruieren, folgen wir den Zeigern b[i, j] von der unteren rechten Ecke ausgehend; die Sequenz ist durch Schattierung gekennzeichnet. Jedes Symbol „ “ auf dem schattierten Pfad gehört zu einem (hervorgehobenen) Eintrag, für den xi = yj ein Element einer LCS ist.
Für die Tabelle b in Abbildung 15.8 gibt diese Prozedur „BCBA“ aus. Die Prozedur benötigt Zeit O(m + n), da sie bei jedem rekursiven Aufruf wenigstens eine der beiden Variablen i und j dekrementiert.
Optimierung des Codes Wenn Sie einmal einen Algorithmus entwickelt haben, werden Sie häufig feststellen, dass Sie ihn bezüglich der benötigten Laufzeit oder des verwendeten Speichers verbessern können. Verschiedene Veränderungen können den Code vereinfachen und die konstanten Faktoren verbessern, führen aber nicht zu asymptotischen Verbesserungen in Bezug auf die Performanz. Andere Veränderungen können zu substantiellen Einsparungen bezüglich Zeit und Speicherbedarf führen. In dem LCS-Algorithmus können wir zum Beispiel die Tabelle b vollkommen eliminieren. Jeder Eintrag c[i, j] hängt lediglich von drei anderen Einträgen in Tabelle c ab: c[i − 1, j − 1], c[i − 1, j] und c[i, j − 1]. Ist der Wert von c[i, j] gegeben, können wir in Zeit O(1) bestimmen, welcher der drei Werte zur Berechnung von c[i, j] benutzt wurde, ohne die Tabelle b zu betrachten. Somit können wir eine LCS in Zeit O(m + n) rekonstruieren, indem wir eine Prozedur verwenden, die der Prozedur Print-LCS ähnelt. (In
15.5 Optimale binäre Suchbäume
399
Übung 15.4-2 sollen Sie den entsprechenden Pseudocode angeben.) Wenngleich wir bei dieser Vorgehensweise Θ(mn) an Speicher sparen, verringert sich der Arbeitsspeicherbedarf für die Berechnung einer LCS nicht asymptotisch, da wir in jedem Fall den Platz Θ(mn) für die Tabelle c benötigen. Wir können jedoch den Speicherbedarf für LCS-Length asymptotisch reduzieren, da zu jedem Zeitpunkt nur zwei Zeilen der Tabelle c benötigt werden: die zu berechnende Zeile und die vorhergehende Zeile. (Tatsächlich brauchen wir nur etwas mehr Platz als für eine Zeile von c, um die Länge einer LCS zu bestimmen, wie Sie in Übung 15.44 zeigen sollen.) Diese Optimierung ist möglich, wenn wir nur die Länge einer LCS bestimmen müssen; wenn wir die Elemente der LCS rekonstruieren müssen, dann enthält die kleinere Tabelle nicht genug Informationen, um unsere Schritte in Zeit O(m + n) zurückzuverfolgen.
Übungen 15.4-1 Bestimmen Sie eine LCS von 1, 0, 0, 1, 0, 1, 0, 1 und 0, 1, 0, 1, 1, 0, 1, 1, 0. 15.4-2 Geben Sie den Pseudocode an, der eine LCS aus einer vollständig gefüllten Tabelle c und den ursprünglichen Sequenzen X = x1 , x2 , . . . , xm und Y = y1 , y2 , . . . , yn in Zeit O(m+n) rekonstruiert, ohne die Tabelle b zu verwenden. 15.4-3 Geben Sie eine memoisierte Version von LCS-Length an, die in Zeit O(mn) läuft. 15.4-4 Zeigen Sie, wie man lediglich unter Verwendung von 2 · min(m, n) Einträgen in der Tabelle c und zusätzlichem Speicher O(1) die Länge einer LCS berechnen kann. Zeigen Sie anschließend, wie Sie dies unter Verwendung von nur min(m, n) Einträgen und O(1) zusätzlichem Speicher erreichen. 15.4-5 Geben Sie einen Algorithmus mit Laufzeit O(n2 ) an, der die längste monoton steigende Teilsequenz einer Sequenz von n Zahlen bestimmt. 15.4-6∗ Geben Sie einen Algorithmus mit Laufzeit O(n lg n) an, der die längste monoton steigende Teilsequenz einer Sequenz von n Zahlen bestimmt. (Hinweis: Beachten Sie, dass das letzte Element einer Kandidaten-Teilsequenz der Länge i mindestens genauso groß ist wie das letzte Element einer KandidatenTeilsequenz der Länge i − 1. Verwalten Sie die Kandidaten-Teilsequenzen, indem Sie sie über die Eingabesequenz „verlinken“.)
15.5
Optimale binäre Suchbäume
Nehmen Sie an, wir würden einen Algorithmus entwerfen, um einen Text aus dem Englischen ins Französische zu übersetzen. Für jedes Auftreten eines englischen Wortes müssen wir dessen französische Übersetzung nachschlagen. Wir könnten diese Nachschlageoperationen ausführen, indem wir einen binären Suchbaum mit n englischen
400
15 Dynamische Programmierung k2
k2
k1 d0
k4 d1
k1
k3 d2
k5 d3
d4
d0
k5 d1
d5
k4 k3
d2 (a)
d5 d4
d3 (b)
Abbildung 15.9: Zwei binäre Suchbäume für eine Menge von n = 5 Schlüsseln mit den folgenden Wahrscheinlichkeiten: i pi qi
0 0,05
1 0,15 0,10
2 0,10 0,05
3 0,05 0,05
4 0,10 0,05
5 0,20 0,10
(a) Ein binärer Suchbaum mit den erwarteten Kosten 2,80. (b) Ein binärer Suchbaum mit den erwarteten Kosten 2,75. Dieser Baum ist optimal.
Wörtern als Schlüssel und deren französischen Übersetzungen als Satellitendaten konstruieren. Da wir den Baum für jedes einzelne Wort im Text durchsuchen werden, wollen wir die für die gesamte Suche aufzuwendende Zeit so gering wie möglich halten. Wir können eine Suchzeit O(lg n) für jedes Ereignis sicherstellen, indem wir einen RotSchwarz-Baum oder irgendeinen anderen balancierten binären Suchbaum verwenden. Wörter kommen jedoch mit unterschiedlicher Häufigkeit vor, und ein häufig vorkommendes Wort wie beispielsweise the könnte sich im Baum weit von der Wurzel entfernt wiederfinden, während ein selten benutztes Wort wie beispielsweise machicolation sich nahe an der Wurzel befinden könnte. Solch eine Organisation würde die Übersetzung verlangsamen, denn die Anzahl der besuchten Knoten ist Eins plus die Tiefe des Knotens, der den Schlüssel enthält, wenn wir im binären Suchbaum nach einem Schlüssel suchen. Wir wollen Wörter, die im Text häufig vorkommen, in der Nähe der Wurzel platzieren.6 Darüber hinaus kann es im Text Wörter geben, für die es keine französische Übersetzung gibt 7 , und solche Wörter würden überhaupt nicht im binären Suchbaum auftauchen. Wie haben wir einen binären Suchbaum zu organisieren, sodass wir die Anzahl der bei allen Suchvorgängen besuchten Knoten minimieren, wenn wir wissen, wie häufig jedes Wort vorkommt? Was wir benötigen, ist als optimaler binärer Suchbaum bekannt. Gegeben ist eine Folge K = k1 , k2 , . . . , kn von n paarweise verschiedenen Schlüsseln mit k1 < k2 < · · · < kn und wir wollen einen binären Suchbaum aus diesen Schlüsseln konstruieren. Für 6 Wenn das Thema des Textes sich um die Architektur von Burgen drehen würde, würden wir möglicherweise wollen, dass „machicolation“ nahe der Wurzel vorkommt. 7 Ja, machicolation hat ein französisches Gegenstück: mâchicoulis.
15.5 Optimale binäre Suchbäume
401
jeden Schlüssel ki ist die Wahrscheinlichkeit pi , dass nach ki gesucht wird, gegeben. Es kann vorkommen, dass nach Werten gesucht wird, die nicht in K enthalten sind. Deshalb gibt es auch n + 1 „Dummy-Schlüssel“ d0 , d1 , d2 , . . . , dn , die die nicht in K enthaltenen Werte repräsentieren. Insbesondere steht d0 für alle Werte, die kleiner als k1 sind, und dn für alle Werte, die größer als kn sind. Für i = 1, 2, . . . , n − 1 steht der Schlüssel di für alle Werte zwischen ki und ki+1 . Zu jedem Dummy-Schlüssel di ist eine Wahrscheinlichkeit qi gegeben, dass eine Suche nach di durchgeführt wird. Abbildung 15.9 zeigt zwei binäre Suchbäume für eine Menge aus fünf Schlüsseln. Jeder Schlüssel ki ist ein innerer Knoten und jeder Dummy-Schlüssel ist ein Blatt. Jede Suche ist entweder erfolgreich (ein Schlüssel ki wird gefunden) oder erfolglos (ein Dummy-Schlüssel di wird gefunden), und somit gilt n
pi +
i=1
n
(15.10)
qi = 1 .
i=0
Da die Wahrscheinlichkeiten für die Suche nach jedem Schlüssel und jedem DummySchlüssel gegeben sind, können wir die erwarteten Kosten für die Suche in einem gegebenen binären Suchbaum T bestimmen. Lassen Sie uns voraussetzen, dass sich die tatsächlichen Kosten für eine Suche aus der Anzahl der besuchten Knoten ergeben, d. h. aus der Tiefe des bei der Suche in T gefundenen Knotens plus 1. Dann betragen die erwarteten Kosten für eine Suche in T n n E [Suchkosten in T ] = (tiefeT (ki ) + 1) · pi + (tiefeT (di ) + 1) · qi i=1
=1+
i=0 n
tiefeT (ki ) · pi +
i=1
n
tiefeT (di ) · qi ,
(15.11)
i=0
wobei tiefeT die Tiefe eines Knotens im Baum T bezeichnet. Das letzte Gleichheitszeichen folgt aus Gleichung (15.10). In Abbildung 15.9(a) können wir die erwarteten Kosten für eine Suche Knoten für Knoten berechnen: Knoten k1 k2 k3 k4 k5 d0 d1 d2 d3 d4 d5 Summe
Tiefe 1 0 2 1 2 2 2 3 3 3 3
Wahrscheinlichkeit 0,15 0,10 0,05 0,10 0,20 0,05 0,10 0,05 0,05 0,05 0,10
Beitrag 0,30 0,10 0,15 0,20 0,60 0,15 0,30 0,20 0,20 0,20 0,40 2,80
Für eine gegebene Menge von Wahrscheinlichkeiten wollen wir einen binären Suchbaum konstruieren, dessen erwartete Kosten für eine Suche minimal ist. Wir bezeichnen einen
402
15 Dynamische Programmierung
solchen Baum als einen optimalen binären Suchbaum. Abbildung 15.9(b) zeigt einen optimalen Suchbaum für die in der Bildunterschrift gegebenen Wahrscheinlichkeiten; seine erwarteten Kosten betragen 2,75. Dieses Beispiel zeigt, dass ein optimaler binärer Suchbaum nicht notwendigerweise ein Baum ist, dessen Gesamthöhe minimal ist. Auch können wir einen optimalen binären Suchbaum nicht notwendigerweise dadurch konstruieren, dass wir immer den Schlüssel mit der höchsten Wahrscheinlichkeit an der Wurzel platzieren. In diesem Fall besitzt Schlüssel k5 die größte Suchwahrscheinlichkeit von allen Schlüsseln, die Wurzel des gezeigten optimalen binären Suchbaumes ist jedoch k2 . (Die minimalen erwarteten Kosten eines binären Suchbaumes mit k5 als Wurzel betragen 2,85.) Wie bei der Matrizen-Kettenmultiplikation führt das vollständige Überprüfen aller Möglichkeiten zu keinem effizienten Algorithmus. Wir können die Knoten jedes binären Suchbaumes mit n Knoten mit den Schlüsseln k1 , k2 , . . . , kn kennzeichnen, um einen binären Suchbaum zu konstruieren und anschließend die Dummy-Schlüssel als Blätter hinzufügen. In Problemstellung 12-4 haben wir gesehen, dass die Anzahl der binären Bäume mit n Knoten Ω(4n /n3/2 ) beträgt und wir somit bei einer vollständigen Suche exponentiell viele binäre Suchbäume überprüfen müssten. Es ist nicht überraschend, dass wir dieses Problem mithilfe dynamischer Programmierung lösen werden.
Schritt 1: Die Struktur eines optimalen binären Suchbaumes Um die optimale Teilstruktur eines binären Suchbaumes zu charakterisieren, beginnen wir mit einer Beobachtung, die die Teilbäume betrifft. Betrachten Sie irgendeinen Teilbaum eines binären Suchbaumes. Er muss Schlüssel in einem zusammenhängenden Bereich ki , . . . , kj mit 1 ≤ i ≤ j ≤ n enthalten. Zusätzlich muss ein Teilbaum, der die Schlüssel ki , . . . , kj enthält, als Blätter auch die Dummy-Schlüssel di−1 , . . . , dj besitzen. Nun können wir die optimale Teilstruktur angeben: Wenn ein optimaler binärer Suchbaum T einen Teilbaum T besitzt, der die Schlüssel ki , . . . , kj enthält, dann muss dieser Teilbaum T auch für das Teilproblem mit den Schlüsseln ki , . . . , kj und den Dummy-Schlüsseln di−1 , . . . , dj optimal sein. Wir können das übliche Austauschargument anwenden. Wenn es einen Teilbaum T gäbe, dessen erwartete Kosten niedriger als die von T wären, dann könnten wir T aus T ausschneiden und dafür T einfügen, was einen binären Suchbaum mit niedrigeren erwarteten Kosten als T ergeben würde, also der Optimalität von T widersprechen würde. Wir haben die optimale Teilstruktur zu verwenden, um zu zeigen, dass wir eine optimale Lösung des Problems aus den optimalen Lösungen der Teilprobleme konstruieren können. Sind die Schlüssel ki , . . . , kj gegeben, dann ist einer der Schlüssel, zum Beispiel kr (i ≤ r ≤ j), die Wurzel des optimalen Teilbaumes, der diese Schlüssel enthält. Der linke Teilbaum der Wurzel kr enthält die Schlüssel ki , . . . , kr−1 (und die Dummy-Schlüssel di−1 , . . . , dr−1 ), und der rechte Teilbaum enthält die Schlüssel kr+1 , . . . , kj (und die Dummy-Schlüssel dr , . . . , dj ). Wenn wir alle als Wurzel in Frage kommenden Knoten kr mit i ≤ r ≤ j untersuchen und alle optimalen binären Suchbäume, die ki , . . . , kr−1 enthalten, sowie alle optimalen binären Suchbäume, die kr+1 , . . . , kj enthalten, bestimmen, können wir sicher sein, einen optimalen binären Suchbaum zu finden. Es gibt im Zusammenhang mit „leeren“ Teilbäumen ein erwähnenswertes Detail. Neh-
15.5 Optimale binäre Suchbäume
403
men Sie an, wir würden in einem Teilbaum mit den Schlüsseln ki , . . . , kj das Element ki als Wurzel wählen. Aufgrund des obigen Argumentes enthält der linke Teilbaum von ki die Schlüssel ki , . . . , ki−1 . Wir interpretieren diese Folge als eine Folge, die keine Schlüssel enthält. Wir müssen aber dabei bedenken, dass der Teilbaum auch DummySchlüssel enthält. Wir übernehmen die Konvention, dass ein Teilbaum, der die Schlüssel ki , . . . , ki−1 enthält, keine wirklichen Schlüssel, sondern lediglich den einzelnen DummySchlüssel di−1 enthält. Wenn wir analog dazu kj als Wurzel auswählen würden, würde der rechte Teilbaum von kj die Schlüssel kj+1 , . . . , kj enthalten; der rechte Teilbaum besitzt keine wirklichen Schlüssel, sondern nur den Dummy-Schlüssel dj .
Schritt 2: Eine rekursive Lösung Wir sind nun in der Lage, den Wert einer optimalen Lösung rekursiv zu definieren. Wir wählen uns als den zu betrachtenden Teilproblembereich die Teilprobleme, einen optimalen binären Suchbaum für die Schlüssel ki , . . . , kj zu bestimmen, mit i ≥ 1, j ≤ n und j ≥ i − 1. (Gilt j = i − 1, so gibt es keine tatsächlichen Schlüssel, sondern nur den Dummy-Schlüssel di−1 .) Wir definieren e[i, j] als die erwarteten Kosten, die für das Suchen in einem optimalen binären Suchbaum, der die Schlüssel ki , . . . , kj enthält, aufgebracht werden müssen. Letztendlich wollen wir e[1, n] berechnen. Der einfache Fall tritt auf, wenn j = i − 1 gilt. Dann haben wir nur den DummySchlüssel di−1 . Die erwarteten Suchkosten betragen e[i, i − 1] = qi−1 . Im Falle j ≥ i müssen wir unter den ki , . . . , kj eine Wurzel kr auswählen und als deren linken Teilbaum einen optimalen binären Suchbaum mit den Schlüsseln ki , . . . , kr−1 sowie als deren rechten Teilbaum einen optimalen binären Suchbaum mit den Schlüsseln kr+1 , . . . , kj erzeugen. Was passiert mit den erwarteten Suchkosten eines Teilbaumes, wenn er zu einem Teilbaum eines Knotens wird? Die Tiefe jedes Knotens im Teilbaum erhöht sich um 1. Nach Gleichung (15.11) erhöhen sich die erwarteten Suchkosten dieses Teilbaumes um die Summe aller Wahrscheinlichkeiten im Teilbaum. Für einen Teilbaum mit den Schlüsseln ki , . . . , kj lassen Sie uns diese Summe von Wahrscheinlichkeiten mit w(i, j) =
j l=i
pl +
j
ql .
(15.12)
l=i−1
bezeichnen. Wenn kr die Wurzel eines optimalen Teilbaumes mit den Schlüsseln ki , . . . , kj ist, gilt also e[i, j] = pr + (e[i, r − 1] + w(i, r − 1)) + (e[r + 1, j] + w(r + 1, j)) . Beachten Sie, dass w(i, j) = w(i, r − 1) + pr + w(r + 1, j) gilt, und so können wir e[i, j] in die Form e[i, j] = e[i, r − 1] + e[r + 1, j] + w(i, j) bringen.
(15.13)
404
15 Dynamische Programmierung
In der rekursiven Gleichung (15.13) wird vorausgesetzt, dass wir den als Wurzel benutzten Knoten kennen. Wir wählen die Wurzel, die zu den niedrigsten erwarteten Suchkosten führt, was zu der finalen rekursiven Formulierung < e[i, j] =
qi−1
falls j = i − 1 ,
min {e[i, r − 1] + e[r + 1, j] + w(i, j)} falls i ≤ j
(15.14)
i≤r≤j
führt. Die Werte e[i, j] geben die erwarteten Suchkosten in optimalen binären Suchbäumen an. Um die Struktur der optimalen binären Suchbäume zu konstruieren, definieren wir wurzel [i, j] für 1 ≤ i ≤ j ≤ n als denjenigen Index r, für den kr die Wurzel eines optimalen binären Suchbaumes mit den Schlüssel ki , . . . , kj ist. Wir werden sehen, wie die Werte von wurzel[i, j] zu berechnen sind, überlassen aber die Konstruktion eines optimalen binären Suchbaumes aus diesen Werten der Übung 15.5-1.
Schritt 3: Berechnen der erwarteten Suchkosten in einem optimalen binären Suchbaum An dieser Stelle haben Sie vielleicht einige Gemeinsamkeiten zwischen unserer Charakterisierung eines optimalen binären Suchbaumes und der Matrizen-Kettenmultiplikation bemerkt. Für beide Problembereiche bestehen unsere Teilprobleme aus zusammenhängenden Indexteilbereichen. Eine direkte, rekursive Implementierung von Gleichung (15.14) wäre genauso ineffizient wie ein direkter, rekursiver Algorithmus für die Matrix-Kettenmultiplikation. Stattdessen speichern wir die Werte e[i, j] in einer Tabelle e[1 . . n + 1, 0 . . n]. Der erste Index muss bis n + 1 statt bis n laufen, da wir e[n + 1, n] berechnen und speichern müssen, um einen Teilbaum zu erhalten, der nur den Dummy-Schlüssel dn enthält. Der zweite Index muss bei 0 starten, da wir e[1, 0] berechnen und speichern müssen, um einen Teilbaum zu erhalten, der nur den Dummy-Schlüssel d0 enthält. Wir werden nur die Einträge e[i, j] benutzen, für die j ≥ i − 1 gilt. Wir verwenden außerdem eine Tabelle wurzel [i, j], um die Wurzel des Teilbaumes, der die Schlüssel ki , . . . , kj enthält, zu speichern. Diese Tabelle verwendet nur die Einträge mit 1 ≤ i ≤ j ≤ n. Aus Effizienzgründen benötigen wir eine weitere Tabelle. Anstatt die Werte w(i, j) jedes Mal für die Berechnung von e[i, j] von neuem zu berechnen – was Θ(j − i) Additionen benötigen würde – speichern wir diese Werte in einer Tabelle w[1 . . n + 1, 0 . . n]. Der Basisfall ergibt sich aus w[i, i − 1] = qi−1 für 1 ≤ i ≤ n + 1. Für j ≥ i berechnen wir w[i, j] = w[i, j − 1] + pj + qj .
(15.15)
Somit können wir die Θ(n2 ) Werte von w[i, j] jeweils in Zeit Θ(1) bestimmen. Der folgende Pseudocode verwendet als Eingabe die Wahrscheinlichkeiten p1 , . . . , pn und q0 , . . . , qn sowie die Größe n und gibt die Tabellen e und wurzel zurück.
15.5 Optimale binäre Suchbäume
405
Optimal-BST(p, q, n) 1 seien e[1 . . n + 1, 0 . . n], w[1 . . n + 1, 0 . . n], und wurzel [1 . . n, 1 . . n] neue Tabellen 2 for i = 1 to n + 1 3 e[i, i − 1] = qi−1 4 w[i, i − 1] = qi−1 5 for l = 1 to n 6 for i = 1 to n − l + 1 7 j = i+l−1 8 e[i, j] = ∞ 9 w[i, j] = w[i, j − 1] + pj + qj 10 for r = i to j 11 t = e[i, r − 1] + e[r + 1, j] + w[i, j] 12 if t < e[i, j] 13 e[i, j] = t 14 wurzel [i, j] = r 15 return e und wurzel
Nach der obigen Beschreibung und wegen den Ähnlichkeiten mit der Prozedur MatrixChain-Order aus Abschnitt 15.2 sollten Sie die Arbeitsweise dieser Prozedur ziemlich einfach verstehen. Die for-Schleife in den Zeilen 2–4 initialisiert die Werte e[i, i − 1] und w[i, i−1]. Die for-Schleife in den Zeilen 5–14 verwendet dann die Rekursionsgleichungen (15.14) und (15.15), um e[i, j] und w[i, j] für alle 1 ≤ i ≤ j ≤ n zu berechnen. In der ersten Iteration, für die l = 1 gilt, wird in der Schleife e[i, i] und w[i, i] für i = 1, 2, . . . , n berechnet. Die zweite Iteration, mit l = 2, berechnet e[i, i + 1] und w[i, i + 1] für i = 1, 2, . . . , n − 1, usw. Die innerste for-Schleife in den Zeilen 10–14 schaut sich jeden Kandidatenindex r an, um zu bestimmen, welcher Schlüssel kr als Wurzel eines optimalen binären Suchbaumes, der die Schlüssel ki , . . . , kj enthält, zu verwenden ist. Diese for-Schleife speichert jedes Mal den aktuellen Wert des Index r in wurzel [i, j], wenn sie einen Schlüssel findet, der besser als Wurzel geeignet ist. Abbildung 15.10 zeigt die Tabellen e[i, j], w[i, j] und wurzel [i, j], die von der Prozedur Optimal-BST angewendet auf die in Abbildung 15.9 gezeigte Schlüsselverteilung bestimmt werden. Wie im Beispiel der Matrizen-Kettenmultiplikation aus Abbildung 15.5 sind die Tabellen gedreht, damit die Diagonalen horizontal verlaufen. Die Prozedur Optimal-BST berechnet die Zeilen von unten nach oben und innerhalb jeder Zeile von links nach rechts. Die Prozedur Optimal-BST benötigt genau wie Matrix-Chain-Order Zeit Θ(n3 ). Wir können einfach sehen, dass die Laufzeit der Prozedur in O(n3 ) ist, da ihre forSchleifen dreifach verschachtelt sind und jeder Schleifenindex maximal n Werte annimmt. Die Schleifenindizes in Optimal-BST haben nicht genau die gleichen Schranken wie die in Matrix-Chain-Order, aber sie unterscheiden sich höchstens um 1 in allen Richtungen. Somit benötigt die Prozedur Optimal-BST, genau wie MatrixChain-Order, Zeit Ω(n3 ).
406
15 Dynamische Programmierung e
w
1 2,75 i 2 1,75 2,00 3 3 1,25 1,20 1,30 2 4 0,90 0,70 0,60 0,90 1 5 0,45 0,40 0,25 0,30 0,50 0 6 0,05 0,10 0,05 0,05 0,05 0,10
1 1,00 i 2 0,70 0,80 3 3 0,55 0,50 0,60 2 4 0,45 0,35 0,30 0,50 1 5 0,30 0,25 0,15 0,20 0,35 0 6 0,05 0,10 0,05 0,05 0,05 0,10
5
j
5
j
4
4
wurzel 5 j
2
3 2
2 1
1 1
1 2
4
2 2
2
i
2 4
3 5
4 3
4 5
4
5 5
Abbildung 15.10: Die von der Prozedur Optimal-BST angewendet auf die in Abbildung 15.9 gezeigte Schlüsselverteilung berechneten Tabellen e[i, j], w[i, j] und wurzel [i, j]. Die Tabellen wurden gedreht, sodass die Diagonalen horizontal verlaufen.
Übungen 15.5-1 Schreiben Sie Pseudocode für die Prozedur Construct-Optimal-BST(wurzel ), die bei gegebener Tabelle wurzel die Struktur eines optimalen binären Suchbaumes ausgibt. Für das in Abbildung 15.10 gezeigte Beispiel sollte Ihre Prozedur die Struktur k2 k1 d0 d1 k5 k4 k3 d2 d3 d4 d5
ist ist ist ist ist ist ist ist ist ist ist
die Wurzel das linke Kind von k2 das linke Kind von k1 das rechte Kind von k1 das rechte Kind von k2 das linke Kind von k5 das linke Kind von k4 das linke Kind von k3 das rechte Kind von k3 das rechte Kind von k4 das rechte Kind von k5
ausgeben, die dem in Abbildung 15.9(b) gezeigten optimalen binären Suchbaum entspricht. 15.5-2 Bestimmen Sie die Kosten und die Struktur eines optimalen binären Suchbau-
Problemstellungen zu Kapitel 15
407
mes für eine Menge von n = 7 Schlüsseln mit den folgenden Wahrscheinlichkeiten: i pi qi
0 0,06
1 0,04 0,06
2 0,06 0,06
3 0,08 0,06
4 0,02 0,05
5 0,10 0,05
6 0,12 0,05
7 0,14 0,05
15.5-3 Nehmen Sie an, wir würden die Werte w(i, j) in Zeile 9 von Optimal-BST direkt aus Gleichung (15.12) berechnen und diese berechneten Werte in Zeile 11 verwenden, anstatt die Tabelle w[i, j] zu verwalten. Wie würde sich diese Veränderung auf die Laufzeit von Optimal-BST auswirken? 15.5-4∗ Knuth [212] hat gezeigt, dass es immer Wurzeln von optimalen Suchbäumen mit wurzel[i, j − 1] ≤ wurzel [i, j] ≤ wurzel[i + 1, j] für alle 1 ≤ i < j ≤ n gibt. Benutzen Sie diese Tatsache, um die Prozedur Optimal-BST so zu modifizieren, dass sie in Zeit Θ(n2 ) läuft.
Problemstellungen 15-1 Längster einfacher Pfad in einem gerichteten azyklischen Graphen Nehmen Sie an, wir hätten einen gerichteten azyklischen Graphen G = (V, E) mit reel-wertigen Kantengewichten und zwei ausgezeichneten Knoten s und t gegeben. Geben Sie einen auf dynamischer Programmierung basierenden Ansatz an, um einen längsten gewichteten einfachen Pfad von s nach t zu bestimmen. Wie sieht der Teilproblem-Graph aus? Wie effizient ist Ihr Algorithmus? 15-2 Längste Palindrom-Teilfolge Ein Palindrom ist ein nichtleerer String über einem Alphabet, der sich vorwärts und rückwärts gleich liest. Beispiele von Palindrome sind alle Strings der Länge 1, Otto, Rentner und Rotor. Geben Sie einen effizienten Algorithmus an, der zu einem gegebenen Eingabestring die längste Teilfolge bestimmt, die ein Palindrom ist. Ist die Eingabe beispielsweise der String character, sollte Ihr Algorithmus carac ausgeben. Welche Laufzeit hat Ihr Algorithmus? 15-3 Bitonisches euklidisches Handelsreisenden-Problem Im euklidischen Handelsreisenden-Problem haben wir eine Menge von n Punkten in der Ebene gegeben und wir wollen eine kürzeste Tour, die alle n Punkten besucht, bestimmen. Abbildung 15.11(a) zeigt die Lösung für ein Problem mit 7 Punkten. Das allgemeine Problem ist NP-hart, und deshalb erwarten wir von dessen Lösung, dass dafür mehr als polynomiale Laufzeit erforderlich ist (siehe Kapitel 34). J. L. Bentley hat vorgeschlagen, das Problem zu vereinfachen, indem wir unsere Aufmerksamkeit auf bitonische Wege beschränken, d. h. nur Touren zu betrachten, die am weitesten links liegenden Punkt starten, zuerst strikt von links nach rechts verlaufen und anschließend strikt von rechts nach links zum Ausgangspunkt
408
15 Dynamische Programmierung
(a)
(b)
Abbildung 15.11: Sieben Punkte in der Ebene, gezeichnet auf einem Einheitsgitter. (a) kürzeste geschlossene Tour, deren Länge näherungsweise 24, 89 beträgt. Diese Tour ist nicht bitonisch. (b) Die kürzeste bitonische Tour derselben Menge von Punkten. Ihre Länge beträgt näherungsweise 25, 58.
zurückkehren. Abbildung 15.11(b) zeigt den kürzesten bitonischen Weg für dieselben 7 Punkte. In diesem Fall ist ein in polynomialer Zeit laufender Algorithmus möglich. Geben Sie einen Algorithmus mit Laufzeit O(n2 ) an, der eine optimale bitonische Tour bestimmt. Sie dürfen voraussetzen, dass keine zwei Punkte dieselbe xKoordinate besitzen. (Hinweis: Durchsuchen Sie von links nach rechts und merken Sie sich optimale Möglichkeiten für die zwei Teile der Tour.) 15-4 Sauberes Drucken Wir betrachten das Problem, einen Textabsatz auf einem Drucker mit nichtproportionaler Schrift (alle Zeichen haben die gleiche Breite) sauber zu drucken. Der Eingabetext besteht aus einer Sequenz von n Wörtern der Länge l1 , l2 , . . . , ln , in Buchstaben gemessen. Wir wollen diesen Absatz sauber auf einer Anzahl von Zeilen drucken, die jeweils maximal M Buchstaben enthalten können. Unser Kriterium für „sauber“ besteht in Folgendem. Wenn eine gegebene Zeile die Wörter i bis j enthält, wobei i ≤ j gilt, und wir genau ein Leerzeichen zwischen den Wörtern setzen, dann beträgt die Anzahl der zusätzlichen Leerzeichen am Ende j der Zeile M − j + i − k=i lk , was nichtnegativ sein muss, damit die Wörter auf die Zeile passen. Wir wollen die Summe der Kuben der Anzahl der Leerzeichen am Ende der Zeilen, ohne die Leerzeichen am Ende der letzten Zeile, minimieren. Geben Sie ein dynamisches Programm an, um einen Absatz auf einem Drucker sauber zu drucken. Analysieren Sie die Laufzeit und den Speicherbedarf Ihres Algorithmus. 15-5 Editier-Distanz Um die Quellzeichenkette x[1 . . m] in eine Zielzeichenkette y[1 . . n] zu überführen, können wir verschiedene Transformationsoperationen ausführen. Unser Ziel besteht darin, für gegebene x und y eine Reihe von Transformationen zu erzeugen, die x in y überführen. Wir verwenden ein Feld z zum Speichern der Zwischenresultate – wir setzen voraus, dass es hinreichend groß ist, um alle notwendigen Zeichen aufzunehmen. Zu Beginn ist z leer und bei Terminierung sollte z[j] = y[j]
Problemstellungen zu Kapitel 15
409
für j = 1, 2, . . . , n gelten. Wir verwalten einen Index i, der auf eine Stelle in x zeigt, und einen Index j, der auf eine Stelle in z zeigt, und die Operationen dürfen z und diese beiden Indizes verändern. Anfangs gilt i = j = 1. Wir müssen uns während der Transformation jedes Zeichen aus x anschauen, was bedeutet, dass am Ende der Sequenz der Transformationsoperationen i = m + 1 gelten muss. Wir können jeweils unter sechs Transformationsoperationen wählen: Kopieren eines Zeichens aus x in z, indem z[j] = x[i] gesetzt wird und i und j inkrementiert werden. Diese Operation inspiziert x[i]. Ersetzen eines Zeichens aus x durch ein anderes Zeichen c, indem z[j] = c gesetzt wird und i und j inkrementiert werden. Diese Operation inspiziert x[i]. Löschen eines Zeichens aus x durch Inkrementieren von i und Beibehalten von j. Diese Operation inspiziert x[i], Einfügen eines Zeichens c in z, indem z[j] = c gesetzt und j inkrementiert wird, wobei i beibehalten wird. Diese Operation inspiziert keine Zeichen in x. Drehen (d. h. Vertauschen) der nächsten zwei Zeichen, indem sie von x nach z in umgekehrter Reihenfolge kopiert werden. Dabei setzen wir z[j] = x[i + 1] und z[j +1] = x[i] und anschließend i = i+2 und j = j +2. Diese Operation inspiziert x[i] und x[i + 1]. Vernichten des Restes von x, indem wir i = m + 1 setzen. Diese Operation inspiziert alle Zeichen in x, die noch nicht untersucht wurden. Wird diese Operation ausgeführt, so muss es die letzte sein, die ausgeführt wird. Beispielsweise ist es möglich, den Eingabestring algorithm durch folgende Sequenz von Operationen in den String altruistic zu transformieren – die in der Tabelle unterstrichenen Zeichen geben x[i] und z[j] nach den einzelnen Operationen an: Operation Eingabestrings kopieren kopieren ersetzen durch t löschen kopieren einfügen u einfügen i einfügen s vertauschen einfügen c vernichten
x algorithm algorithm algorithm algorithm algorithm algorithm algorithm algorithm algorithm algorithm algorithm algorithm
z a al alt alt altr altru altrui altruis altruisti altruistic altruistic
Beachten Sie, dass es noch mehrere andere Sequenzen von Transformationsoperationen gibt, die algorithm in altruistic überführen. Jeder dieser Transformationsoperationen sind Kosten zugeordnet. Die Kosten einer Operation hängen von der spezifischen Anwendung ab; wir wollen jedoch
410
15 Dynamische Programmierung voraussetzen, dass die Kosten uns bekannt sind und für jede der Operationen konstant sind. Wir setzen weiter voraus, dass die individuellen Kosten der Operationen Kopieren und Ersetzen geringer sind als die zusammengefassten Kosten der Operationen Löschen und Einfügen; anderenfalls würden die Operationen Kopieren und Ersetzen nicht verwendet. Die Kosten einer gegebenen Sequenz von Transformationsoperationen bestehen aus der Summe der Kosten der einzelnen Operationen der Sequenz. Für die oben stehende Sequenz betragen die Kosten für die Transformation von algorithm nach altruistic (3 · cost(kopieren)) + cost(ersetzen) + cost(löschen) + (4 · cost(einfügen)) + cost(drehen) + cost(vernichten) . a. Sind zwei Sequenzen x[1 . . m] und y[1 . . n] und die Kosten für die Transformationsoperationen gegeben, dann ist die Editier-Distanz von x nach y gleich den Kosten der kostengünstigsten Operationensequenz, die x in y transformiert. Geben Sie ein dynamisches Programm an, das die Editier-Distanz von x[1 . . m] nach y[1 . . n] bestimmt und eine optimale Operationensequenz ausgibt. Analysieren Sie die Laufzeit und den Speicherbedarf Ihres Algorithmus. Das Editier-Distanz-Problem verallgemeinert das Problem des Ausrichtens zweier DNA-Sequenzen (siehe zum Beispiel Setubal und Meidanis [310, Abschnitt 3.2]). Es gibt mehrere Methoden für das Messen der Ähnlichkeit zweier DNA-Sequenzen, die auf dem Ausrichten der Sequenzen basieren. Eine dieser Methoden für das Ausrichten zweier Sequenzen x und y besteht darin, an geeigneten Positionen Leerstellen in die zwei Sequenzen einzufügen (einschließlich beider Enden), sodass die resultierenden Sequenzen x und y dieselbe Länge besitzen, aber keine Leerstellen an den gleichen Positionen haben (d. h. für keine Position j sind sowohl x [j] als auch y [j] eine Leerstelle). Dann weisen wir jeder Position eine „Punktzahl“ zu. Die Position j erhält eine Punktzahl wie folgt: • +1, wenn x [j] = y [j] gilt und keines der beiden eine Leerstelle ist, • −1, wenn x [j] = y [j] gilt und keines der beiden eine Leerstelle ist, • −2, wenn entweder x [j] oder y [j] eine Leerstelle ist. Die Punktzahl der Ausrichtung ergibt sich aus der Summe der Punktzahlen der einzelnen Positionen. Sind zum Beispiel die Sequenzen x = GATCGGCAT und y = CAATGTGAATC gegeben, dann ist eine Ausrichtung beispielsweise G ATCG GCAT CAAT GTGAATC -*++*+*+-++* Ein + unter einer Position bedeutet, dass diese Position die Punktzahl +1 hat, ein - kennzeichnet eine Punktzahl von −1 und ein * kennzeichnet eine Punktzahl von −2. Diese Ausrichtung hat also die Gesamtpunktzahl von 6 · 1 − 2 · 1 − 4 · 2 = −4. b. Erklären Sie, wie das Problem der Berechnung einer optimalen Ausrichtung als Editier-Distanz-Problem interpretiert werden kann, indem Sie eine Teilmenge
Problemstellungen zu Kapitel 15
411
der Transformationsoperationen Kopieren, Ersetzen, Löschen, Einfügen, Vertauschen und Vernichten verwenden. 15-6 Planung einer Firmenfeier Professor Stewart berät den Präsidenten eines Unternehmens, das eine Firmenfeier plant. Das Unternehmen besitzt eine hierarchische Struktur, d. h. das Vorgesetztenverhältnis bildet einen Baum, dessen Wurzel der Präsident ist. Das Personalbüro hat jeden Angestellten mithilfe eines reellwertigen Geselligkeitsfaktors eingestuft. Um die Feier für alle Anwesenden fröhlich zu gestalten, möchte der Präsident nicht, dass sowohl ein Angestellter als auch ihr oder sein unmittelbarer Vorgesetzter anwesend sind. Professor Stewart hat einen Baum bekommen, der die Struktur des Unternehmens unter Verwendung der in Abschnitt 10.4 beschriebenen linkes-Kind-, rechterBruder-Darstellung beschreibt. Jeder Knoten des Baumes verfügt neben den Zeigern über den Namen des Angestellten und dessen Geselligkeitsfaktor. Geben Sie einen Algorithmus an, der eine Gästeliste erstellt, die die Summe der Geselligkeitsfaktoren der Gäste maximiert. Analysieren Sie die Laufzeit ihres Algorithmus. 15-7 Viterbi-Algorithmus Wir können dynamische Programmierung auf einem gerichteten Graphen G = (V, E) für die Spracherkennung verwenden. Jede Kante (u, v) ∈ E wird mit einem Klang σ(u, v) aus einer endlichen Menge von Klangmustern markiert. Der markierte Graph bildet ein formales Modell für eine Person, die eine eingeschränkte Sprache spricht. Jeder Pfad auf dem Graphen, der bei einem ausgewählten Knoten v0 ∈ V startet, entspricht einer möglichen Sequenz von Klängen, die durch das Modell produziert wird. Wir definieren die Markierung eines gerichteten Pfades als Konkatenation der Markierungen der Kanten auf diesem Pfad. a. Geben Sie einen effizienten Algorithmus an, der für einen gegebenen kantenmarkierten Graphen G mit einem ausgezeichneten Knoten v0 und einer Sequenz s = σ1 , σ2 , . . . , σk von Klängen aus Σ einen Pfad G zurückgibt, der bei v0 beginnt und s als Markierung besitzt, falls solch ein Pfad existiert. Anderenfalls sollte der Algorithmus den Wert no-such-path zurückgeben. Analysieren Sie die Laufzeit Ihres Algorithmus. (Hinweis: Die Konzepte aus Kapitel 22 könnten hilfreich sein.) Nehmen wir nun an, dass zu jeder Kante (u, v) ∈ E eine zugehörige nichtnegative Wahrscheinlichkeit p(u, v), mit der die Kante (u, v) vom Knoten u aus traversiert wird und somit den dazugehörigen Klang produziert, gegeben ist. Die Summe der Wahrscheinlichkeiten der Kanten, die einen Knoten verlassen, ist gleich 1. Die Wahrscheinlichkeit eines Pfades ist definiert als Produkt der Wahrscheinlichkeiten seiner Kanten. Wir können die Wahrscheinlichkeit eines bei v0 beginnenden Pfades als die Wahrscheinlichkeit betrachten, dass ein zufällig gewählter Weg, der bei v0 beginnt, diesem speziellen Pfad folgen wird. Dabei wird die Entscheidung, welche Kante an einem Knoten u zu wählen ist, probabilistisch getroffen, gemäß den Wahrscheinlichkeiten der aus dem Knoten u austretenden Kanten.
412
15 Dynamische Programmierung b. Erweitern Sie Ihre Antwort zu Teil (a) so, dass der zurückgegebene Pfad ein wahrscheinlichster Pfad ist, der bei v0 startet und die Markierung s besitzt. Analysieren Sie die Laufzeit Ihres Algorithmus.
15-8 Bildkomprimierung durch Finden einer Naht Gegeben haben wir ein Farbbild, das aus einem m× n Feld A[1 . . m, 1 . . n] von Pixeln besteht, wobei jeder Pixel als Tripel, das die Rot-, Grün- und Blau-Intensität (RGB) angibt, spezifiziert ist. Nehmen Sie an, wir würden das Bild gerne leicht komprimieren. Genauer gesagt, wir würden gerne ein Pixel in jeder der m Zeilen entfernen, sodass das Gesamtbild um ein Pixel schmaler wird. Um störende visuelle Effekte zu vermeiden, verlangen wir jedoch, dass die entfernten Pixel in zwei benachbarten Zeilen in der gleichen oder in adjazenten Spalten liegen: die entfernten Pixel liegen in einer „Naht“, die in der obersten Zeile beginnt und in der untersten Zeile endet, wobei zwei aufeinander folgende Pixel der Naht vertikal oder diagonal zueinander adjazent sind. a. Zeigen Sie, dann die Anzahl solcher Nähte wenigstens exponentiell in m wächst, davon ausgehend, dass n > 1 gilt. b. Setzen Sie nun voraus, dass wir zu jedem Pixel A[i, j] einen reellwertigen Bruchwert d[i, j] berechnet haben, der angibt, wie störend es wäre, Pixel A[i, j] zu entfernen. Intuitiv gesehen gilt, dass je kleiner der Bruchwert eines Pixels ist, umso ähnlicher ist der Pixel zu seinen Nachbarn. Setzen Sie desweiteren voraus, dass wir den Bruchwert einer Naht als die Summe der Bruchwerte ihrer Pixel definieren. Geben Sie einen Algorithmus an, um eine Naht mit einem minimalen Bruchwert zu berechnen. Wie effizient ist Ihr Algorithmus? 15-9 Aufteilen eines Strings Eine bestimmte String-verarbeitende Sprache erlaubt einem Programmierer einen String in zwei Teilstrings aufzuteilen. Da diese Operation den String kopiert, benötigt sie n Zeiteinheiten, um einen String, der aus n Zeichen besteht, in zwei Teilstrings aufzuteilen. Nehmen Sie an, ein Programmierer würde gerne einen String in viele Teilstrings aufteilen. Die Reihenfolge, in der die Aufteilungen erfolgen, kann die Gesamtlaufzeit beeinflussen. Nehmen Sie beispielsweise an, die Programmiererin würde einen String bestehend aus 20 Zeichen hinter den Positionen 2, 8 und 10 aufbrechen wollen (wobei wir die Zeichen beginnend mit 1 aufsteigend von links nach rechts durchnummerieren). Wenn sie das Aufbrechen so programmiert, dass die am weit links liegenden Bruchstellen zuerst realisiert werden, kostet die erste Aufteilung 20 Zeiteinheiten, die zweite kostet 18 Zeiteinheiten (der String bestehend aus den Zeichen 3 bis 20 wird an Zeichen 8 aufgebrochen) und die dritte kostet 12 Zeiteinheiten, was zu einer Gesamtlaufzeit von 50 Zeiteinheiten führt. Werden dagegen die am weit rechts liegenden Bruchstellen als erste realisiert, kostet die erste Aufteilung 20 Zeiteinheiten, die zweite 10 Zeiteinheiten und die dritte 8 Zeiteinheiten, was zu einer Gesamtlaufzeit von 38 Zeiteinheiten führt. In einer anderen Reihenfolge, könnte sie zuerst die Bruchstelle an Postion 8 realisieren (was 20 Zeiteinheiten kostet), dann den linken Teilstring an Position 2 (was 8 Zeiteinheiten kostet) und schlussendlich den rechten Teil-
Problemstellungen zu Kapitel 15
413
string an Position 10 (was 12 Zeiteinheiten kostet), was zu einer Gesamtlaufzeit von 40 Zeiteinheiten führt. Entwerfen Sie einen Algorithmus, der zu gegebenen Positionen, an denen der String aufzubrechen ist, eine billigste Möglichkeit, die Aufteilungen zu realisieren, bestimmt, d. h. gegeben ein String S bestehend aus n Zeichen und ein Feld L[1 . . m], das die Positionen enthält, an der der String aufzubrechen ist, berechnen Sie die minimalen Kosten, die aufgebracht werden müssen, um den String S entsprechend L[1 . . m] aufzubrechen. 15-10 Planung einer Anlagestrategie Ihr Wissen über Algorithmen hilft Ihnen, einen aufregenden Job bei der Firma Acme Computer zu bekommen, wobei Sie bei Unterschrift eine Prämie von 10.000 Dollars erhalten. Sie entscheiden sich, dieses Geld mit dem Ziel zu investieren, dass Sie nach 10 Jahren maximalen Gewinn erzielen. Sie entscheiden sich, die Firma Amalgamated Investments zu beauftragen, Ihre Anlagen zu verwalten. Amalgamated Investments gibt folgende Regeln vor. Sie bietet n verschiedene Anlagen, die mit 1 bis n durchnummeriert sind. In jedem Jahr j erzielt die Anlage i einen Kapitalzuwachs von rij . In anderen Worten, wenn Sie d Dollars im Jahr j in Anlage i investieren, dann erhalten Sie d · rij Dollars am Ende des Jahres j zurück. Der Kapitalzuwachs ist über die Jahre garantiert, d. h. sie erhalten für jede Anlage den Kapitelzuwachs für die nächsten 10 Jahre. Sie treffen einmal pro Jahr Ihre Anlageentscheidungen. Am Ende eines jeden Jahres können Sie Ihr Geld in der gleiche Anlage wie in dem abgelaufenen Jahr belassen oder Sie können Geld in andere Anlagen investieren, indem Sie Geld zwischen bereits existierenden Anlagen verschieben oder in eine neue Anlage investieren. Wenn Sie Ihr Geld zwischen zwei aufeinanderfolgenden Jahren nicht verschieben, bezahlen Sie eine Gebühr von f1 Dollars; wenn Sie das Geld verschieben, bezahlen Sie eine Gebühr von f2 Dollars, wobei f2 > f1 gilt. a. Wie gerade formuliert, erlaubt Ihnen das Problem, Ihr Geld jedes Jahr in mehrere Anlagen zu investieren. Zeigen Sie, dass es eine optimale Anlagestrategie gibt, die jedes Jahr all Ihr Geld in eine einzige Anlage investiert. (Erinnern Sie sich daran, dass eine optimale Anlagestrategie den Wert Ihres Geldes nach 10 Jahren maximiert und sich nicht um andere Ziele wie beispielsweise Risikominimierung kümmert.) b. Zeigen Sie, dass das Problem, eine optimale Anlagestrategie zu bestimmen, die optimale-Teilstruktur-Eigenschaft besitzt. c. Entwerfen Sie einen Algorithmus, der eine optimale Anlagestrategie bestimmt. Wie hoch ist die Laufzeit Ihres Algorithmus? d. Nehmen Sie an, Amalgamated Investments würde die zusätzliche Auflage machen, dass Sie zu jedem Zeitpunkt nicht mehr als 15.000 Dollars pro Anlage investiert haben dürfen. Zeigen Sie, dass das Problem, den Gewinn nach 10 Jahren zu maximieren, nicht länger die optimale-Teilstruktur-Eigenschaft besitzt. 15-11 Planung von Lagerbeständen Die Firma Rinky Dink produziert Maschinen, die Eislaufbahnen erneuern. Die
414
15 Dynamische Programmierung Nachfrage nach solchen Maschinen ändert sich von Monat zu Monat und so muss die Firma eine Strategie entwickeln, um ihre Fertigung an die veränderliche, aber vorhersagbare Nachfrage anzupassen. Die Firma will einen Plan für die nächsten n Monate entwerfen. Die Firma kennt für jeden Monat i die di , d. h. Nachfrage n die Anzahl der Maschinen, die sie verkaufen wird. Sei D = i=1 di die Gesamtnachfrage über die nächsten n Monate. Die Firma hält eine vollzeitbeschäftigte Belegschaft vor, die bis zu m Maschinen pro Monat fertigen kann. Wenn die Firma in einem gegebenen Monat mehr als m Maschinen fertigen muss, kann Sie zusätzliche Teilzeitarbeiter einstellen, was zu Mehrkosten in Höhe von c Dollars pro Maschine führt. Zudem hat die Firma Lagerkosten zu bezahlen, wenn die Firma am Ende eines Monats über nicht verkaufte Maschinen verfügt. Die Lagerkosten von j Maschinen sind gegeben durch eine Funktion h(j) für j = 1, 2, . . . , D, wobei h(j) ≥ 0 für 1 ≤ j ≤ D und h(j) ≤ h(j + 1) für 1 ≤ j ≤ D − 1 gilt. Geben Sie einen Algorithmus an, der einen Plan für die Firma berechnet, der die Kosten für die Firma minimiert, aber die gesamte Nachfrage erfüllt. Die Laufzeit sollte polynomiell in n und D sein.
15-12 Verpflichten freier Baseball-Spieler Nehmen Sie an, Sie wären Teammanager eines Baseball-Teams, das in der obersten Spielklasse spielt. In der Nebensaison haben Sie einige freie Spieler neu unter Vertrag zu nehmen. Der Besitzer des Teams gibt Ihnen für neue Spieler ein Budget von X Dollars. Sie dürfen weniger als X Dollars ausgeben, aber der Besitzer wird Sie feuern, wenn Sie mehr als X Dollars ausgeben. Sie betrachten N unterschiedliche Positionen, und für jede Position sind P freie Spieler, die auf dieser Position spielen, verfügbar.8 Da Sie für keine der Positionen zu viele Spieler im Kader haben wollen, wollen Sie pro Position höchstens einen freien Spieler, der diese Position spielen kann, verpflichten. (Wenn Sie für eine spezielle Position keinen Spieler neu verpflichten, müssen Sie sich an die Spieler halten, die bereits zur Mannschaft gehören und diese Position spielen können.) Um festzustellen zu können, wie wertvoll ein Spieler für Sie sein wird, beschließen Sie Sabermetrics9 einzusetzen; das dazugehörige Maß ist als „VORP“ (engl.: value over replacement player ) bekannt ist. Ein Spieler mit einer höheren VORP ist wertvoller als ein Spieler mit einer niedrigeren VORP, muss aber bei der Verpflichtung nicht unbedingt teurer sein, da andere Faktoren als der Wert des Spielers bestimmen, wie teuer eine Verpflichtung von ihm ist. Zu jedem verfügbaren freien Spieler besitzen Sie drei Informationen: • die Position des Spielers, • die Kosten, die anfallen, um den Spieler zu verpflichten und 8 Wenngleich es in einem Baseball-Team neun Positionen gibt, muss N nicht unbedingt gleich 9 sein, da Teammanager eine spezielle Art haben, über Positionen nachzudenken. So unterscheidet ein Teammanager möglicherweise zwischen einem rechtshändigen Werfer und einem linkshändigen Werfer und sieht diese als unterschiedliche „Positionen“; ebenso gibt es die „Position“ eines Startwerfers, eines Werfers, der in mehreren Innings wirft, oder eines Werfers, der üblicherweise nur in einem Inning wirft. 9 Sabermetrics steht für statistische Analysen im Baseball. Es stellt mehrere Möglichkeiten zur Verfügung, um die relativen Werte der einzelnen Spieler miteinander zu vergleichen.
Kapitelbemerkungen zu Kapitel 15
415
• den VORP-Wert des Spielers. Entwerfen Sie einen Algorithmus, der die Gesamt-VORP der Spieler, die Sie neu unter Vertrag nehmen, maximiert, wobei Sie aber insgesamt nicht mehr als X Dollars ausgeben. Sie können voraussetzen, dass jeder Spieler für ein Vielfaches von 100.000 Dollars unterschreibt. Ihr Algorithmus sollte die Gesamt-VORP der Spieler, die Sie verpflichten, die Gesamtsumme an Geld, das Sie ausgeben, und eine Liste der Spieler, die Sie unter Vertrag nehmen, ausgeben. Analysieren Sie die Laufzeit und der Speicherbedarf Ihres Algorithmus.
Kapitelbemerkungen R. Bellman begann 1955 mit dem systematischen Studium dynamischer Programmierung. Das Wort „Programmierung“ bezieht sich sowohl hier als auch bei linearer Programmierung auf die Verwendung einer tabellarischen Lösungsmethode. Zwar waren Optimierungstechniken, die Elemente dynamischer Programmierung einschließen, bereits früher bekannt; Bellman [37] hat dieses Gebiet jedoch auf eine solide mathematische Basis gestellt. Galil und Park [125] klassifizieren dynamische Programme nach der Größe der Tabelle und der Anzahl der Tabelleneinträgen, von denen jeder Eintrag abhängt. Sie nennen ein dynamisches Programm tD/eD, falls die Tabellengröße in O(nt ) liegt und jeder Eintrag von O(ne ) anderen Einträgen abhängt. Beispielsweise wäre der Algorithmus zur Matrizen-Kettenmultiplikation aus Abschnitt 15.2 2D/1D und der Algorithmus zur Bestimmung einer längsten gemeinsamen Teilsequenz aus Abschnitt 15.4 2D/0D. Hu und Shing [182, 183] geben einen Algorithmus mit Laufzeit O(n lg n) für die MatrixKettenmultiplikation an. Der Algorithmus mit Laufzeit O(mn) für das Problem der längsten gemeinsamen Teilsequenz scheint ein populärer Algorithmus zu sein. Knuth [70] warf die Frage auf, ob subquadratische Algorithmen für das LCS-Problem existieren. Masek und Paterson [244] beantworteten diese Frage positiv, indem sie einen in Zeit O(mn/ lg n) laufenden Algorithmus angaben, wobei n ≤ m gilt und die Sequenzen aus einer beschränkten Menge gezogen werden. Für diesen speziellen Fall, bei dem kein Element mehr als einmal in der Eingabesequenz auftaucht, zeigt Szymanski [326], wie das Problem in Zeit O((n + m) lg(n + m)) gelöst werden kann. Viele dieser Resultate lassen sich auf das Problem der Berechnung von Zeichenketten-Editier-Distanzen (Problemstellung 15-5) ausdehnen. Eine frühe Veröffentlichung über binäre Kodierungen variabler Länge (engl.: variablelength binary encoding) von Gilbert und Moore [133] fand Anwendung bei der Konstruktion optimaler binärer Suchbäume für den Fall, dass alle Wahrscheinlichkeiten pi den Wert 0 haben; diese Veröffentlichung enthält einen Algorithmus mit Laufzeit O(n3 ). Aho, Hopcroft und Ullman [5] präsentieren den Algorithmus aus Abschnitt 15.5. Übung 15.5-4 geht auf Knuth [212] zurück. Hu und Tucker [184] haben einen Algorithmus für den Fall entwickelt, dass alle Wahrscheinlichkeiten pi den Wert 0 haben, der in Zeit O(n2 ) läuft und Platz O(n) benötigt; später reduzierte Knuth [211] die Zeit auf O(n lg n).
416
15 Dynamische Programmierung
Die Problemstellung 15-8 geht auf Avidan und Shamir [27] zurück, die ein wunderschönes Video ins Internet gestellt haben, das diese Bildkompressionstechnik illustriert.
16
Greedy-Algorithmen
Algorithmen für Optimierungsprobleme durchlaufen gewöhnlich eine Reihe von Schritten, wobei in jedem Schritt eine Reihe von Entscheidungen zu treffen sind. Bei vielen Optimierungsproblemen ist es übertrieben, dynamische Programmierung zu verwenden, um die beste Entscheidung zu treffen; hier genügt es, einfachere, effizientere Algorithmen zu verwenden. Ein Greedy-Algorithmus trifft stets diejenige Entscheidung, die im Moment am besten erscheint. Das heißt, er trifft eine lokal optimale Entscheidung in der Hoffnung, dass diese Entscheidung zu einer global optimalen Lösung führen wird. Dieses Kapitel sondiert Optimierungsprobleme, für die Greedy-Algorithmen optimale Lösungen berechnen. Bevor Sie sich diesem Kapitel zuwenden, sollten Sie etwas über dynamische Programmierung in Kapitel 15 lesen, insbesondere den Abschnitt 15.3. Greedy-Algorithmen führen nicht immer zu optimalen Lösungen, aber für viele Probleme tun sie dies. Zunächst werden wir in Abschnitt 16.1 ein einfaches, aber nichttriviales Problem untersuchen, das Aktivitäten-Auswahl-Problem, für das ein GreedyAlgorithmus effizient eine optimale Lösung berechnet. Wir entwickeln den GreedyAlgorithmus, indem wir zuerst einen auf dynamischer Programmierung basierenden Ansatz betrachten und dann zeigen, dass wir immer eine „gierige“ (engl.: greedy) Entscheidung treffen können, um eine optimale Lösung zu erhalten. Abschnitt 16.2 behandelt die Grundlagen des Greedy-Ansatzes, wobei ein direkter Ansatz vorgestellt wird, um die Korrektheit von Greedy-Algorithmen zu beweisen. Abschnitt 16.3 stellt eine wichtige Anwendung der Greedy-Technik vor, nämlich den Entwurf von (Huffman-)Codierungen zur Datenkompression. In Abschnitt 16.4 untersuchen wir einige der Theorie zugrunde liegenden kombinatorischen Strukturen, die als „Matroide“ bezeichnet werden und für die ein Greedy-Algorithmus stets eine optimale Lösung produziert. Schließlich wendet Abschnitt 16.5 Matroide auf das Problem der Ablaufplanung von Unit-TimeTasks mit Terminen und Vertragsstrafen an. Die Greedy-Methode ist recht leistungsfähig und arbeitet für einen großen Bereich von Problemen gut. Spätere Kapitel werden viele Algorithmen vorstellen, die wir als Anwendungen der Greedy-Methode ansehen können. Dazu gehören die Algorithmen zur Bestimmung minimaler aufspannender Bäume im Kapitel 23, Dijkstras Algorithmus für die kürzesten Pfade bei einem einzigen Startknoten im Kapitel 24 und Chvátals Greedy-Heuristik zum Mengenüberdeckungsproblem im Kapitel 35. Algorithmen zur Berechnung minimaler aufspannender Bäume bilden ein klassisches Beispiel für die Greedy-Methode. Wenngleich Sie dieses Kapitel und Kapitel 23 unabhängig voneinander durcharbeiten können, empfiehlt es sich, sie im Zusammenhang zu lesen.
418
16 Greedy-Algorithmen
16.1
Ein Aktivitäten-Auswahl-Problem
Unser erstes Beispiel ist das Problem der Koordination verschiedener konkurrierender Aktivitäten, die eine gemeinsame Ressource exklusiv nutzen. Das Ziel besteht darin, eine maximale Menge paarweise zueinander kompatibler Aktivitäten auszuwählen. Nehmen Sie an, wir hätten eine Menge S = {a1 , a2 , . . . , an } von n anstehenden Aktivitäten gegeben, die eine zu jedem Zeitpunkt jeweils nur von einer Aktivität verwendbare Ressource, wie zum Beispiel einen Hörsaal, benutzen möchten. Jede Aktivität ai besitzt eine Startzeit si und eine Endzeit fi , wobei 0 ≤ si < fi < ∞ gilt. Wenn Aktivität ai ausgewählt wird, dann findet diese im halboffenen Zeitintervall [si , fi ) statt. Die Aktivitäten ai und aj werden als kompatibel bezeichnet, wenn sich die Intervalle [si , fi ) und [sj , fj ) nicht überlappen. Das heißt, ai und aj sind kompatibel, wenn si ≥ fj oder sj ≥ fi gilt. Im Aktivitäten-Auswahl-Problem wollen wir eine maximale Teilmenge paarweise zueinander kompatibler Aktivitäten finden. Wir setzen voraus, dass die Aktivitäten in monoton aufsteigender Reihenfolge nach ihren Endzeiten sortiert sind: f1 ≤ f2 ≤ f3 ≤ · · · ≤ fn−1 ≤ fn .
(16.1)
(Wir werden später den Vorteil sehen, der diese Voraussetzung mit sich bringt.) Betrachten Sie beispielsweise die folgende Menge S von Aktivitäten: i si fi
1 1 4
2 3 5
3 0 6
4 5 7
5 3 9
6 5 9
7 6 10
8 8 11
9 8 12
10 2 14
11 12 16
In diesem Beispiel besteht die Teilmenge {a3 , a9 , a11 } aus paarweise zueinander kompatiblen Aktivitäten. Dies ist jedoch keine maximale Teilmenge, da die Teilmenge {a1 , a4 , a8 , a11 } größer ist. Tatsächlich ist {a1 , a4 , a8 , a11 } eine maximale Teilmenge paarweise zueinander kompatibler Aktivitäten; eine andere maximale Teilmenge ist {a2 , a4 , a9 , a11 }. Wir werden dieses Problem in mehreren Schritten lösen. Wir beginnen damit, über eine auf dynamischer Programmierung basierenden Lösung nachzudenken, indem wir verschiedene Auswahlmöglichkeiten betrachten, wenn wir die für die optimale Lösung zu verwendenden Teilprobleme bestimmen. Wir werden dann feststellen, dass wir nur eine Wahlmöglichkeit – die gierige Auswahl – betrachten müssen und dass jeweils nur ein Teilproblem zu lösen ist, wenn wir diese Entscheidung treffen. Auf der Grundlage dieser Feststellung werden wir einen rekursiven Greedy-Algorithmus entwickeln, um das Aktivitäten-Auswahl-Problem zu lösen. Wir werden die Entwicklung einer GreedyLösung abschließen, indem wir den rekursiven Algorithmus in einen iterativen Algorithmus umwandeln. Obwohl die in diesem Abschnitt zu durchlaufenden Schritte ein wenig aufwendiger sind, als es für die Entwicklung eines Greedy-Algorithmus typisch ist, illustrieren sie die Beziehung zwischen Greedy-Algorithmen und dynamischer Programmierung gut.
Die optimale Teilstruktur des Aktivitäten-Auswahl-Problems Wir können leicht überprüfen, dass das Aktivitäten-Auswahl-Problem die optimaleTeilstruktur-Eigenschaft besitzt. Lassen Sie uns die Menge der Aktivitäten, die starten,
16.1 Ein Aktivitäten-Auswahl-Problem
419
nachdem die Aktivität ai endet, und enden, bevor die Aktivität aj startet, mit Sij bezeichnen. Nehmen Sie an, wir würden gerne eine maximale Menge von paarweise zueinander kompatiblen Aktivitäten aus Sij bestimmen, und nehmen Sie weiter an, dass Aij eine solche maximale Menge ist und diese die Aktivität ak enthält. Wenn wir ak in eine optimale Lösung aufnehmen, so bleiben wir mit zwei Teilproblemen: wir haben paarweise zueinander kompatible Aktivitäten in der Menge Sik (Aktivitäten, die starten, nachdem Aktivität ai endet, und enden, bevor Aktivität ak startet) und paarweise zueinander kompatible Aktivitäten in der Menge Skj (Aktivitäten, die starten, nachdem Aktivität ak endet, und enden, bevor Aktivität aj startet) zu bestimmen. Sei Aik = Aij ∩ Sik und Akj = Aij ∩ Skj , sodass Aik die Aktivitäten aus Aij enthält, die enden, bevor ak startet, und Akj die Aktivitäten aus Aij , die starten, nachdem ak endet. Es gilt somit Aij = Aik ∪ {ak } ∪ Akj und die maximal größte Menge Aij paarweise zueinander kompatibler Aktivitäten aus Sij setzt sich aus |Aij | = |Aik | + |Akj | + 1 Aktivitäten zusammen. Das übliche Austauschargument zeigt, dass die optimale Lösung Aij optimale Lösungen der zwei Teilprobleme Sik und Skj enthalten muss. Wenn wir eine Menge Akj von paarweise zueinander kompatiblen Aktivitäten aus Skj mit |Akj | > |Akj | finden könnten, dann könnten wir Akj anstelle von Akj in einer Lösung des Teilproblems für Sij nehmen. Wir hätten damit eine Menge mit |Aik | + |Akj | + 1 > |Aik | + |Akj | + 1 = |Aij | paarweise zueinander kompatibler Aktivitäten konstruiert, was ein Widerspruch zu der Voraussetzung, dass Aij eine optimale Lösung ist, wäre. Ein symmetrisches Argument lässt sich auf die Aktivitäten aus Sik anwenden. Diese Überlegungen zu der optimalen Teilstruktur deutet an, dass wir das AktivitätenAuswahl-Problem möglicherweise mittels dynamischer Programmierung lösen können. Wenn wir die Größe einer optimalen Lösung für die Menge Sij mit c[i, j] bezeichnen, dann hätten wir die Rekursionsgleichung c[i, j] = c[i, k] + c[k, j] + 1 . Wenn wir nicht gewusst hätten, dass eine optimale Lösung für die Menge Sij die Aktivität ak enthält, dann hätten wir uns natürlich alle Aktivitäten aus Sij als mögliche Kandidaten anschauen müssen, was zu < c[i, j] =
0 max {c[i, k] + c[k, j] + 1}
ak ∈Sij
falls Sij = ∅ , falls Sij = ∅
(16.2)
geführt hätte. Wir könnten nun einen rekursiven Algorithmus entwickeln und ihn memoisieren oder wir könnten bottom-up arbeiten und die entsprechende Tabelle füllen. Wir würden jedoch dabei eine andere wichtige Eigenschaft des Aktivitäten-AuswahlProblems übersehen, aus der wir einen großen Vorteil schlagen können.
Treffen der Greedy-Wahl Was wäre, wenn wir eine Aktivität auswählen und unserer optimalen Lösung hinzufügen könnten, ohne zuerst alle Teilprobleme gelöst zu haben? Wir bräuchten nicht mehr
420
16 Greedy-Algorithmen
alle die in der Rekursionsgleichung (16.2) angedeuteten Wahlmöglichkeiten betrachten. Und in der Tat, beim Aktivitäten-Auswahl-Problem haben wir nur eine dieser Wahlmöglichkeiten zu betrachten: die Greedy-Wahl. Was meinen wir mit der Greedy-Wahl beim Aktivitäten-Auswahl-Problem? Folgen wir unserer Intuition, so sollten wir eine Aktivität auswählen, die die Ressource für so viele andere Aktivitäten wie möglich frei lässt. Nun, von den Aktivitäten, aus denen wir auswählen, wird eine von ihnen als erste abgeschlossen sein. Unsere Intuition sagt uns, dass wir die Aktivität aus S mit der frühesten Endzeit wählen sollten, da diese die Ressource für maximal viele Aktivitäten, die ihr zeitlich folgen, verfügbar lässt. (Wenn mehr als eine Aktivität aus S die früheste Endzeit hat, dann wählen wir aus diesen eine beliebige aus.) In anderen Worten, da die Aktivitäten nach ihren Endzeiten monoton aufsteigend sortiert sind, fällt die Greedy-Wahl auf a1 . Die erste Aktivität zu wählen, ist nicht die einzige Möglichkeit für eine Greedy-Wahl bei diesem Problem; Übung 16.1-3 fragt Sie nach anderen Möglichkeiten. Wenn wir unsere Greedy-Wahl machen, dann haben wir nur ein verbleibendes Teilproblem zu lösen, nämlich Aktivitäten zu bestimmen, die starten, nachdem a1 fertig ist. Warum haben wir keine Aktivitäten zu betrachten, die enden, bevor a1 startet? Wir haben s1 < f1 und f1 ist die früheste Endzeit von allen Aktivitäten und somit kann keine Aktivität eine Endzeit haben, die kleiner oder gleich s1 ist. Somit müssen alle Aktivitäten, die kompatibel zu a1 sind, starten, nachdem a1 endet. Zudem haben wir bereits festgestellt, dass das Aktivitäten-Auswahl-Problem die optimale-Teilstruktur-Eigenschaft besitzt. Sei Sk = {ai ∈ S : si ≥ fk } die Menge der Aktivitäten, die starten, nachdem ak endet. Wenn wir in unserer Greedy-Entscheidung a1 auswählen, dann ist S1 das einzige Teilproblem, das noch zu lösen ist.1 Die optimaleTeilstruktur-Eigenschaft sagt uns, dass wenn a1 in der optimalen Lösung enthalten ist, dann besteht eine optimale Lösung zu dem ursprünglichen Problem aus Aktivität a1 und allen Aktivitäten einer optimalen Lösung des Teilproblems S1 . Eine letzte Frage ist noch zu klären: Ist unsere Intuition richtig? Ist die Greedy-Wahl – die erste Aktivität zu wählen, die endet – immer Teil einer optimalen Lösung? Das folgende Theorem zeigt, dass dies in der Tat der Fall ist. Theorem 16.1 Betrachten Sie ein nichtleeres Teilproblem Sk und sei am eine Aktivität aus Sk mit der frühesten Endzeit. Dann ist am in einer maximal großen Teilmenge von paarweise zueinander kompatiblen Aktivitäten aus Sk enthalten. Beweis: Sei Ak eine maximal große Teilmenge paarweise zueinander kompatibler Aktivitäten von Sk und sei aj eine Aktivität aus Ak mit der frühesten Endzeit. Ist aj = am , dann haben wir nichts mehr zu zeigen, da am damit in einer maximal großen Teilmenge paarweise zueinander kompatibler Aktivitäten von Sk enthalten ist. 1 Wir interpretieren in einigen Fällen die Menge S als Teilproblem und nicht einfach nur als Menge k von Aktivitäten. Aus dem Kontext wird aber immer klar sein, ob wir uns auf Sk als Menge von Aktivitäten oder als ein Teilproblem, das diese Menge als Eingabe hat, beziehen.
16.1 Ein Aktivitäten-Auswahl-Problem
421
Ist aj = am , dann betrachten Sie die Menge Ak = Ak − {aj } ∪ {am }, d. h. Ak , in der aj durch am ersetzt worden ist. Die Aktivitäten aus Ak sind alle disjunkt, da die Aktivitäten aus Ak alle disjunkt sind, aj die erste Aktivität aus Ak ist, die endet, und fm ≤ fj gilt. Wegen |Ak | = |Ak |, können wir schließen, dass Ak eine maximal große Teilmenge paarweise zueinander kompatibler Aktivitäten von Sk ist und am enthält.
Wir haben also gesehen, dass, wenngleich wir das Aktivitäten-Auswahl-Problem mit dynamischer Programmierung lösen könnten, wir dies nicht tun brauchen. (Nebenbei bemerkt, wir haben bis jetzt noch nicht untersucht, ob das Aktivitäten-Auswahl-Problem nicht sogar die Eigenschaft überlappender Teilprobleme besitzt.) Wir können an Stelle dynamischer Programmierung immer die Aktivität wählen, die jeweils als erste endet, nur die Aktivitäten weiter betrachten, die zu dieser Aktivität kompatibel sind, und dies solange iterieren, bis keine Aktivitäten mehr übrig sind. Mehr noch, da wir immer die Aktivität mit der frühesten Endzeit wählen, müssen die Endzeiten der Aktivitäten streng monoton wachsen. Wir brauchen also jede Aktivität nur einmal zu betrachten, und das, wie gesagt, in monoton steigender Reihenfolge nach den Endzeiten. Ein Algorithmus, der das Aktivitäten-Auswahl-Problem löst, braucht nicht wie ein Tabellen-basiertes dynamisches Programm bottom-up zu arbeiten. Er kann top-down arbeiten, indem er jeweils eine Aktivität auswählt, die er in die optimale Lösung legt, und dann das Teilproblem löst, geeignete Aktivitäten aus den Aktivitäten zu bestimmen, die mit den bereits gewählten kompatibel sind. Greedy-Algorithmen haben typischerweise dieses top-down-Vorgehen: treffe eine Wahl und löse dann das Teilproblem, anstelle der bottom-up-Methode, Teilprobleme zu lösen, bevor eine Wahl getroffen wird.
Ein rekursiver Greedy-Algorithmus Nachdem wir gesehen haben, wie wir den dynamischen-Programmier-Ansatz umgehen und anstelle dessen einen top-down Greedy-Algorithmus benutzen können, können wir eine einfache rekursive Prozedur schreiben, die das Aktivitäten-Auswahl-Problem löst. Die Prozedur Recursive-Activity-Selector erhält die Start- und Endzeiten der Aktivitäten als Eingabe, die durch Felder s und f dargestellt sind,2 den Index k, der das zu lösende Teilproblem Sk definiert, und die Größe n des ursprünglichen Problems. Sie gibt eine maximal große Menge von paarweise zueinander kompatiblen Aktivitäten aus Sk zurück. Wir setzen voraus, dass die n Aktivitäten der Eingabe bereits entsprechend Gleichung (16.1) monoton steigend nach ihren Endzeiten sortiert sind. Wenn nicht, können wir sie in Laufzeit O(n lg n) in diese Reihenfolge bringen, wobei wir Mehrdeutigkeiten beliebig auflösen. Um die Prozedur zu starten, fügen wir die fiktive Aktivität a0 mit f0 = 0 ein, sodass das Teilproblem S0 gleich der gesamten Menge S der Aktivitäten ist. Der initiale Aufruf, der das vollständige Problem löst, ist Recursive-Activity-Selector(s, f, 0, n). 2 Da s und f Felder in dem Pseudocode sind, indexiert der Pseudocode sie durch eckige Klammern und nicht als Indizes.
422
16 Greedy-Algorithmen
Recursive-Activity-Selector(s, f, k, n) 1 m = k+1 2 while m ≤ n und s[m] < f [k] // finde die Aktivität aus Sk , die als erste endet 3 m = m+1 4 if m ≤ n 5 return {am } ∪ Recursive-Activity-Selector(s, f, m, n) 6 else return ∅ Abbildung 16.1 zeigt die Arbeitsweise dieses Algorithmus. Bei einem gegebenen rekursiven Aufruf Recursive-Activity-Selector(s, f, k, n) sucht die while-Schleife in den Zeilen 2–3 nach der ersten Aktivität in Sk . Die Schleife untersucht ak+1 , ak+2 , . . . , an , bis sie die erste Aktivität am findet, die mit ak kompatibel ist; für eine solche Aktivität gilt sm ≥ fk . Wenn die Schleife terminiert, weil eine solche Aktivität gefunden wurde, gibt Zeile 5 die Vereinigung von {am } und der maximal großen Teilmenge von Sm , die durch den rekursiven Aufruf Recursive-Activity-Selector(s, f, m, n) zurückgegeben wird, zurück. Alternativ kann die Schleife terminieren, weil m > n gilt. In diesem Fall haben wir alle Aktivitäten geprüft, ohne eine mit ak kompatible Aktivität zu finden. Dann gilt Sk = ∅ und die Prozedur gibt in Zeile 6 den Wert ∅ zurück. Falls alle Aktivitäten bereits nach den Endzeiten sortiert waren, liegt die Laufzeit des Aufrufs Recursive-Activity-Selector(s, f, 0, n) in Θ(n), was wir wie folgt sehen können. Nach allen rekursiven Aufrufen ist jede Aktivität genau einmal im Test der while-Schleife in Zeile 2 geprüft worden. Insbesondere wird Aktivität ai beim letzten Aufruf untersucht, für den k < i gilt.
Ein iterativer Greedy-Algorithmus Wir können unseren rekursiven Algorithmus einfach in einen iterativen umwandeln. Die Prozedur Recursive-Activity-Selector ist nahezu „endrekursiv“ (engl.: tail recursive) (siehe Problemstellung 7-4): Sie endet mit einem rekursiven Aufruf ihrer selbst, gefolgt von einer Vereinigungsoperation. Es ist gewöhnlich eine einfache Aufgabe, eine endrekursive Prozedur in eine iterative Form umzuwandeln; tatsächlich führen einige Compiler bestimmter Programmiersprachen diese Aufgabe automatisch aus. Wie beschrieben arbeitet die Prozedur Recursive-Activity-Selector für Teilprobleme Sk , d. h. für Teilprobleme, die aus den zuletzt endenden Aktivitäten bestehen. Die Prozedur Greedy-Activity-Selector ist eine iterative Version der Prozedur Recursive-Activity-Selector. Sie setzt ebenfalls voraus, dass die Aktivitäten in der Eingabe monoton steigend nach ihren Endzeiten geordnet sind. Sie sammelt die ausgewählten Aktivitäten in einer Menge A und gibt diese Menge am Ende zurück.
16.1 Ein Aktivitäten-Auswahl-Problem
k
sk
fk
0
–
0
1
1
4
2
3
5
3
0
6
4
5
7
423
a0 a1 a0
RECURSIVE -ACTIVITY-SELECTOR (s, f, 0, 11)
m=1 a2
RECURSIVE -ACTIVITY-SELECTOR (s, f, 1, 11)
a1 a3 a1 a4 a1
m=4
a1
a5 a4
a1
a4
a1
a4
a1
a4
RECURSIVE -ACTIVITY-SELECTOR (s, f, 4, 11) 5
3
9
6
5
9
7
6
10
8
8
11
9
8
12
10
2
14
11
12
16
a6
a7
a8 m=8 a9
RECURSIVE -ACTIVITY -SELECTOR (s, f, 8, 11) a1 a4
a8 a10
a1
a4
a8
a1
a4
a8
m = 11
a8
a11
a11
RECURSIVE -ACTIVITY -SELECTOR (s, f, 11, 11) a1 a4
Zeit 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Abbildung 16.1: Die Arbeitsweise von Recursive-Activity-Selector auf den 11 vorhin angegebenen Aktivitäten. Die bei jedem rekursiven Aufruf betrachteten Aktivitäten erscheinen zwischen horizontalen Linien. Die fiktive Aktivität a0 endet zur Zeit 0, und der initiale Aufruf Recursive-Activity-Selector(s, f, 0, 11) wählt Aktivität a1 aus. Bei jedem rekursiven Aufruf sind die bereits ausgewählten Aktivitäten schattiert dargestellt, und die weiß gezeichnete Aktivität wird gerade betrachtet. Wenn die Startzeit einer Aktivität vor der Endzeit der zuletzt hinzugefügten Aktivität liegt (der Pfeil zwischen ihnen zeigt nach links), wird diese verworfen. Anderenfalls (der Pfeil weist direkt nach oben oder nach rechts) wird sie ausgewählt. Der letzte rekursive Aufruf Recursive-Activity-Selector(s, f, 11, 11) gibt ∅ zurück. Die resultierende Menge ausgewählter Aktivitäten ist {a1 , a4 , a8 , a11 }.
424
16 Greedy-Algorithmen
Greedy-Activity-Selector(s, f ) 1 n = s.l¨a nge 2 A = {a1 } 3 k =1 4 for m = 2 to n 5 if s[m] ≥ f [k] 6 A = A ∪ {am } 7 k =m 8 return A Die Prozedur arbeitet wie folgt. Die Variable k indiziert das zuletzt zu A hinzugefügte Element, was der Aktivität ak in der rekursiven Version entspricht. Da wir die Aktivitäten bezüglich der Endzeiten in monoton steigender Reihenfolge betrachten, ist fk stets die späteste Endzeit für die Aktivitäten in A. Es gilt also fk = max {fi : ai ∈ A} .
(16.3)
Die Zeilen 2–3 wählen die Aktivität a1 aus, initialisieren die Menge A, die dann nur diese Aktivität enthält und initialisieren den Index k entsprechend dieser Aktivität. Die for-Schleife in den Zeilen 4–7 findet die zuerst endende Aktivität in Sk . Die Schleife prüft jede Aktivität der Reihe nach und fügt am der Menge A hinzu, falls diese zu allen bisher ausgewählten Aktivitäten kompatibel ist; solch eine Aktivität ist die am frühesten endende Aktivität aus Sk . Um festzustellen, ob Aktivität am kompatibel zu jeder sich gegenwärtig in A befindlichen Aktivität ist, ist es nach Gleichung (16.3) hinreichend zu überprüfen (in Zeile 5), ob deren Startzeit sm nicht vor der Endzeit fk der zuletzt zur Menge A hinzugefügten Aktivität liegt. Wenn Aktivität am kompatibel ist, dann fügen die Zeilen 6–7 die Aktivität am zu A hinzu und setzen den Index k auf m. Die durch den Aufruf Greedy-Activity-Selector(s, f ) zurückgegebene Menge A ist gerade die auch vom Aufruf Recursive-Activity-Selector(s, f, 0, n) zurückgegebene Menge. Wie die rekursive Version plant auch die Prozedur Greedy-Activity-Selector eine Menge von n Aktivitäten in Zeit Θ(n), vorausgesetzt, die Aktivitäten sind bereits nach ihren Endzeiten sortiert.
Übungen 16.1-1 Geben Sie auf Grundlage der Rekursionsgleichung (16.2) ein dynamisches Programm für das Aktivitäten-Auswahl-Problem an. Lassen Sie Ihren Algorithms die oben definierten Größen c[i, j] berechnen und die maximale Teilmenge paarweise zueinander kompatibler Aktivitäten erzeugen. Setzen Sie voraus, dass die Eingaben gemäß Gleichung (16.1) sortiert sind. Vergleichen Sie die Laufzeit Ihrer Lösung mit der Laufzeit von Greedy-Activity-Selector. 16.1-2 Nehmen Sie an, dass wir statt der zuerst endenden Aktivität stets die zuletzt beginnende Aktivität auswählen würden, die zu den bereits ausgewählten Aktivitäten kompatibel ist. Erklären Sie, warum dies ein Greedy-Algorithmus ist, und beweisen Sie, dass dieser Ansatz zu einer optimalen Lösung führt.
16.2 Elemente der Greedy-Strategie
425
16.1-3 Nicht jeder Greedy-Ansatz für das Aktivitäten-Auswahl-Problem erzeugt eine maximal große Menge paarweise zueinander kompatibler Aktivitäten. Geben Sie ein Beispiel dafür an, dass die Methode, die Aktivität mit der kleinsten Dauer unter den Aktivitäten auszuwählen, die kompatibel zu den vorher ausgewählten Aktivitäten sind, nicht immer zu einer optimalen Lösung führt. Geben Sie ebenfalls jeweils ein Gegenbeispiel für den Ansatz an, immer eine kompatible Aktivität auszuwählen, die sich mit den wenigsten anderen noch verbliebenen Aktivitäten überlappt, und für den Ansatz, immer eine verbliebene Aktivität mit der frühesten Startzeit, die kompatibel zu den vorher ausgewählten Aktivitäten ist, auszuwählen. 16.1-4 Nehmen Sie an, wir hätten eine Menge von Aktivitäten zu planen, jede Aktivität würde in einem beliebigen Hörsaal stattfinden, von denen ausreichend viele vorhanden wären, um alle Aktivitäten stattfinden zu lassen. Wir wollen die Aktivitäten so planen, dass alle Aktivitäten stattfinden und wir so wenig wie möglich Hörsäle benutzen. Geben Sie einen effizienten Greedy-Algorithmus an, der bestimmt, in welchem Hörsaal welche Aktivität stattfinden soll. (Dieses Problem ist auch als das Intervallgraph-Färbungsproblem bekannt. Wir können einen Intervallgraphen erzeugen, dessen Knoten die gegebenen Aktivitäten sind und dessen Kanten inkompatible Aktivitäten verbinden. Die Bestimmung der minimalen Anzahl von Farben, mit denen alle Knoten so gefärbt werden können, dass kein Paar benachbarter Knoten mit der gleichen Farbe versehen ist, entspricht dem Problem, die minimal benötigte Anzahl von Hörsälen zu finden, um alle gegebenen Aktivitäten zu planen.) 16.1-5 Betrachten Sie eine Modifikation des Aktivitäten-Auswahl-Problems, in der zu jeder Aktivität ai neben der Start- und Endzeit auch noch ein Wert vi bekannt ist. Das Ziel besteht nun nicht mehr darin, die Anzahl sondern den Gesamtwert der geplanten Aktivitäten zu maximieren. Das heißt, wir wollen eine Menge A von paarweise zueinander kompatiblen Aktivitäten bestimmen, sodass ak ∈A vk maximal ist. Geben Sie einen Algorithmus mit polynomialer Laufzeit für dieses Problem an.
16.2
Elemente der Greedy-Strategie
Ein Greedy-Algorithmus bestimmt eine optimale Lösung eines Problems, indem er eine Folge von Entscheidungen trifft. Bei jeder Entscheidung trifft der Algorithmus die Wahl, die zu diesem Zeitpunkt die bestmögliche zu sein scheint. Diese heuristische Strategie erzeugt nicht immer eine optimale Lösung, aber wie wir beim Aktivitäten-AuswahlProblem gesehen haben, tut sie dies bei einigen Problem doch. Dieser Abschnitt diskutiert einige allgemeine Eigenschaften der Greedy-Methode. Der Ansatz, den wir in Abschnitt 16.1 verfolgt haben, um einen Greedy-Algorithmus zu entwickeln, war ein wenig komplizierter als gewöhnlich. Wir haben die folgenden Schritte ausgeführt:
426
16 Greedy-Algorithmen
1. Bestimmung der optimalen Teilstruktur des Problems. 2. Entwicklung einer rekursiven Lösung. (Für das Aktivitäten-Auswahl-Problem haben wir die Rekursionsgleichung (16.2) aufgestellt, haben es aber dann vermieden, eine rekursiven Algorithmus, der auf dieser Gleichung basiert, zu entwickeln.) 3. Überlegung, dass nur ein zu lösendes Teilproblem übrigbleibt, wenn wir die GreedyWahl treffen. 4. Beweis, dass es immer richtig ist, die Greedy-Auswahl zu treffen.3 5. Entwicklung eines rekursiven Algorithmus, der die Greedy-Strategie implementiert. 6. Umwandlung des rekursiven Algorithmus in einen iterativen Algorithmus. Als wir diese Schritte ausgeführt haben, haben wir im Detail gesehen, wie dynamische Programmierung Greedy-Algorithmen untermauert. Beim Aktivitäten-AuswahlProblem zum Beispiel haben wir zuerst die Teilprobleme Sij definiert, wobei sowohl i als auch j variierten. Wir haben dann festgestellt, dass wir die Teilprobleme auf die Form Sk beschränken können, wenn wir immer die Greedy-Wahl treffen. Alternativ dazu hätten wir unsere optimale Teilstruktur im Hinblick auf die GreedyWahl, die wir im Hinterkopf hatten und bei der immer nur ein zu lösendes Teilproblem übrig bleibt, modellieren können. Im Aktivitäten-Auswahl-Problem hätten wir den zweiten Index fallen gelassen und nur Teilprobleme der Form Sk definiert. Dann hätten wir beweisen können, dass eine Greedy-Auswahl (Wahl der in Sk zuerst endende Aktivität am ), kombiniert mit einer optimalen Lösung kompatibler Aktivitäten für die verbleibende Menge Sm , zu einer optimalen Lösung für Sk führt. In größer Allgemeinheit formuliert, entwerfen wir einen Greedy-Algorithmus über die folgenden Schritte: 1. Stellen Sie das Optimierungsproblem in einer Form dar, dass wir jeweils eine Entscheidung zu treffen haben und wir nur ein dann noch zu lösendes Teilproblem haben. 2. Beweisen Sie, dass es immer eine optimale Lösung des Ausgangsproblems gibt, die die Greedy-Wahl enthält. Somit ist es nie falsch, die Greedy-Auswahl zu treffen. 3. Zeigen Sie, dass die optimale-Teilstruktur-Eigenschaft gilt, indem Sie zeigen, dass nachdem die Greedy-Auswahl getroffen wurde, ein Teilproblem mit folgender Eigenschaft übrig bleibt: Wenn wir eine optimale Lösung des entstandenen Teilproblems mit der Greedy-Wahl kombinieren, dann führt dies zu einer optimalen Lösung des ursprünglichen Problems. Wir werden diese direktere Vorgehensweise in den späteren Abschnitten dieses Kapitels verwenden. Nichtsdestotrotz gibt es neben jedem Greedy-Algorithmus fast immer eine ein bisschen schwerfälligere Lösung, die auf dynamischer Programmierung basiert. 3 Die
Schritte 3 und 4 können in beliebiger Reihenfolge ausgeführt werden.
16.2 Elemente der Greedy-Strategie
427
Wie kann man entscheiden, ob ein Greedy-Algorithmus ein spezielles Optimierungsproblem löst? Es gibt keinen allgemeingültigen Weg, aber die Greedy-Auswahl-Eigenschaft und die optimale-Teilstruktur-Eigenschaft sind die zwei Hauptbestandteile. Wenn wir zeigen können, dass ein Problem diese Eigenschaften besitzt, dann sind wir auf einem guten Weg, einen Greedy-Algorithmus für das Problem zu entwickeln.
Die Greedy-Auswahl-Eigenschaft Der erste Hauptbestandteil ist die Greedy-Auswahl-Eigenschaft: Wir können eine global optimale Lösung erreichen, indem wir lokal optimale (gierige) Entscheidungen treffen. Mit anderen Worten, wenn wir überlegen, welche Auswahl zu treffen ist, dann treffen wir die Wahl, die für das aktuelle Problem am besten erscheint, ohne die Lösungen von Teilproblemen zu betrachten. An dieser Stelle unterscheiden sich Greedy-Algorithmen von dynamischer Programmierung. Bei der dynamischen Programmierung treffen wir bei jedem Schritt eine Auswahl, aber die Auswahl hängt gewöhnlich von den Lösungen der Teilprobleme ab. Folglich lösen wir dynamisch programmierte Probleme bottom-up, beginnend mit den kleinsten Teilproblemen hin zu den größeren. (Alternativ können wir diese Probleme top-down mit Memoisierung lösen. Aber auch hier müssen wir die Teilprobleme lösen, bevor wir eine unserer Entscheidungen treffen können, auch wenn der Code top-down ausgerichtet ist.) Bei einem Greedy-Algorithmus treffen wir diejenige Auswahl, die in diesem Moment am besten geeignet erscheint und lösen dann das nach der Auswahl verbliebene Teilproblem. Die von einem Greedy-Algorithmus getroffene Auswahl kann von den bis dahin getroffenen Entscheidungen abhängen, aber nicht von zukünftigen Entscheidungen oder von Lösungen von Teilproblemen. Im Unterschied zu dynamischen Programmen trifft ein Greedy-Algorithmus seine erste Auswahl, bevor irgendein Teilproblem gelöst worden ist. Ein dynamisches Programm arbeitet bottom-up, während eine Greedy-Strategy top-down arbeitet, indem sie eine Greedy-Wahl nach der anderen trifft, was jede gegebene Probleminstanz auf eine kleinere reduziert. Dies steht im Gegensatz zu dynamischen Programmen, die die Teilprobleme von unten nach oben lösen. Natürlich müssen wir beweisen, dass die Greedy-Auswahl in jedem Schritt zu einer global optimalen Lösung führt. Typischerweise betrachtet der Beweis, wie im Falle von Theorem 16.1, eine global optimale Lösung zu einem Teilproblem. Dann zeigt der Beweis, wie die Lösung so modifiziert werden kann, dass die Greedy-Wahl für eine andere Wahl eingesetzt werden kann, was zu einem ähnlichen, aber kleineren Teilproblem führt. Wir können normalerweise die Greedy-Wahl effizienter treffen, als wenn wir eine größere Menge von Auswahlmöglichkeiten betrachten müssten. Beim Aktivitäten-AuswahlProblem mussten wir beispielsweise jede Aktivität nur einmal betrachten, wenn wir die Aktivitäten bereits in monoton steigender Reihenfolge nach den Endzeiten sortiert hatten. Durch einen Vorverarbeitungsschritt oder eine geeignete Datenstruktur (häufig einer Prioritätswarteschlange) können wir häufig die Greedy-Entscheidung schnell durchführen, sodass wir einen effizienten Algorithmus erhalten.
428
16 Greedy-Algorithmen
Optimale Teilstruktur Ein Problem besitzt die optimale-Teilstruktur-Eigenschaft, wenn eine optimale Lösung des Problems optimale Lösungen von Teilproblemen beinhaltet. Diese Eigenschaft ist wesentlich, damit dynamische Programmierung und Greedy-Algorithmen zur Anwendung kommen können. Denken Sie an Abschnitt 16.1, in dem wir ein Beispiel für die optimale-Teilstruktur-Eigenschaft gegeben haben: Wir haben gezeigt, dass, wenn eine optimale Lösung des Teilproblems Sij eine Aktivität ak enthält, dann muss sie auch optimale Lösungen von den Teilproblemen Sik und Skj enthalten. Mit dieser gegebenen optimalen Teilstruktur haben wir wie folgt argumentiert: Wenn wir wüssten, welche der Aktivitäten als ak zu verwenden ist, dann könnten wir eine optimale Lösung für Sij konstruieren, indem wir ak zusammen mit allen Aktivitäten aus optimalen Lösungen der Teilprobleme Sik und Skj auswählen. Aufgrund dieser Feststellung hinsichtlich der optimalen Teilstruktur waren wir in der Lage, die Rekursionsgleichung (16.2) zu formulieren, die den Wert einer optimalen Lösung beschreibt. Wir verwenden gewöhnlich einen direkteren Ansatz hinsichtlich der optimalen Teilstruktur, wenn wir diese auf Greedy-Algorithmen anwenden. Wie bereits oben erwähnt, sind wir in der glücklichen Lage, voraussetzen zu können, dass wir nur ein Teilproblem zu lösen haben, nachdem wir die Greedy-Wahl im ursprünglichen Problem getroffen haben. Alles, was wir wirklich tun müssen, ist zu zeigen, dass eine optimale Lösung des Teilproblems, kombiniert mit den bereits getroffenen gierigen Entscheidungen, zu einer optimalen Lösung des ursprünglichen Problems führt. Diese Methode wendet implizit Induktion nach den Teilproblemen an, um zu beweisen, dass die bei jedem Schritt getroffenen Greedy-Entscheidungen insgesamt eine optimale Lösung erzeugen.
Greedy-Strategie versus dynamische Programmierung Da sowohl die Greedy-Strategie als auch das Konzept der dynamischen Programmierung die optimale-Teilstruktur-Eigenschaft des Problems ausnutzen, könnten Sie versucht sein, eine dynamisch programmierte Lösung für ein Problem zu bauen, wenn eine Greedy-Lösung ausreicht, oder Sie könnten fälschlicherweise annehmen, dass eine Greedy-Lösung anwendbar wäre, wenn eine Lösung mithilfe dynamischer Programmierung tatsächlich erforderlich ist. Um die Feinheiten zwischen den beiden Techniken zu illustrieren, untersuchen wir zwei Varianten eines klassischen Optimierungsproblems. Das 0-1-Rucksackproblem (engl.: 0-1 knapsack problem) ist wie folgt gegeben. Ein Dieb, der gerade ein Kaufhaus ausraubt, findet n Gegenstände vor; der i-te Gegenstand ist vi Dollar wert und wiegt wi Pfund, wobei vi und wi ganze Zahlen sind. Er möchte eine Beute mitnehmen, die so wertvoll wie möglich ist, kann aber in seinem Rucksack höchstens W Pfund tragen, wobei W eine ganze Zahl ist. Welche Gegenstände soll er nehmen? (Wir bezeichnen dieses Problem als 0-1-Rucksackproblem, da der Dieb jeden Gegenstand mitnehmen oder zurücklassen kann; der Dieb kann keinen Gegenstand teilweise oder mehrfach mitnehmen.) Beim gebrochenen Rucksackproblem ist der Aufbau der gleiche, aber der Dieb kann Teile eines Gegenstandes mitnehmen, anstatt für jeden Gegenstand eine binäre (0-1)-Entscheidung treffen zu müssen. Sie können sich jeden Gegenstand beim 0-1-
16.2 Elemente der Greedy-Strategie
429
Rucksackproblem als einen Goldbarren vorstellen, während ein Gegenstand beim gebrochenen Rucksackproblem eher Goldstaub entspricht. Beide Rucksackprobleme haben die optimale-Teilstruktur-Eigenschaft. Betrachten Sie beim 0-1-Problem die wertvollste Beute, die höchstens W Pfund schwer ist. Wenn wir den Gegenstand j aus dieser Beute entfernen, dann muss die verbleibende Beute die wertvollste, maximal W − wj Pfund wiegende Beute sein, die der Dieb aus den n − 1 ursprünglichen Gegenständen unter Ausschluss von j zusammenstellen kann. Betrachten Sie beim vergleichbaren gebrochenen Problem das Szenario, dass wir ein Gewicht w des Gegenstandes j aus der optimalen Beute entfernen. Die verbleibende Beute muss dann die wertvollste, höchstens W − w Pfund wiegende Beute sein, die der Dieb aus den n − 1 ursprünglichen Gegenständen und den noch verbliebenen wj − w Pfund des Gegenstandes j zusammenstellen kann. Obwohl die Probleme ähnlich sind, können wir das gebrochene Rucksackproblem mit Hilfe einer Greedy-Strategie lösen, die 0-1-Problem jedoch nicht. Um das gebrochene Problem zu lösen, berechnen wir zunächst den Wert pro Pfund vi /wi jedes Gegenstandes. Wenden wir eine Greedy-Strategie an, dann nimmt der Dieb zuerst so viel wie möglich vom Gegenstand mit dem größten Wert pro Pfund. Wenn der Vorrat dieses Gegenstandes erschöpft ist und der Dieb noch mehr tragen kann, dann nimmt er so viel wie möglich vom Gegenstand mit dem nächstgrößten Wert pro Pfund und so weiter, bis er sein Gewichtslimit W erreicht. Weil wir die Gegenstände nach ihrem Wert pro Pfund ordnen, läuft der Greedy-Algorithmus in Zeit O(n lg n). Wir verweisen auf Übung 16.2-1 für den Beweis, dass das gebrochene Rucksackproblem die Greedy-Auswahl-Eigenschaft besitzt. Um zu verstehen, dass die Greedy-Strategie nicht auf das 0-1-Problem angewendet werden kann, betrachten Sie die in Abbildung 16.2(a) illustrierte Probleminstanz. In diesem Beispiel gibt es drei Gegenstände und ein Rucksack, der Gegenstände mit einem Gesamtgewicht von 50 Pfund tragen kann. Gegenstand 1 wiegt 10 Pfund und ist 60 Dollar wert. Gegenstand 2 wiegt 20 Pfund und ist 100 Dollar wert. Gegenstand 3 wiegt 30 Pfund und ist 120 Dollar wert. Somit beträgt der Wert pro Pfund für Gegenstand 1 genau 6 Dollar pro Pfund, was größer als der Wert pro Pfund der beiden Gegenstände 2 (5 Dollar pro Pfund) oder 3 (4 Dollar pro Pfund) ist. Deshalb würde die Greedy-Strategie zuerst Gegenstand 1 nehmen. Wie Sie jedoch der Fallanalyse in Abbildung 16.2(b) entnehmen können, verwendet die optimale Lösung die Gegenstände 2 und 3, während Gegenstand 1 zurückgelassen wird. Die zwei möglichen Lösungen, die Gegenstand 1 enthalten, sind beide suboptimal. Beim vergleichbaren gebrochenen Problem führt jedoch die Greedy-Strategie, die Gegenstand 1 zuerst verwendet, zu einer optimalen Lösung (siehe Abbildung 16.2(c)). Das Entwenden von Gegenstand 1 funktioniert beim 0-1-Problem nicht, weil der Dieb anschließend nicht in der Lage ist, die Rucksackkapazität auszufüllen und der Leerraum den effektiven Wert der Beute pro Pfund verringert. Wenn wir beim 0-1-Problem über die Aufnahme eines Gegenstandes in den Rucksack entscheiden, müssen wir die Lösung des Teilproblems, bei dem wir Gegenstand 1 aufnehmen, mit der Lösung des Teilproblems, bei dem wir Gegenstand 1 ausschließen, vergleichen, bevor wir eine Wahl treffen können. Das in dieser Weise formulierte Problem hat viele sich überlappende Teilprobleme zur Folge – was darauf hindeutet, dass dynamische Programmierung an-
430
16 Greedy-Algorithmen
Gegenstand 3 50
+
30 20 $100
20 10 $60
30 $120 20 $100 + 10
$120 Rucksack
$100 (a)
$80 +
Gegenstand 2 Gegenstand 1
20 30
30 $120
= $220
$60
= $160 (b)
10
+
20 $100 +
$60
10
= $180
$60
= $240 (c)
Abbildung 16.2: Ein Beispiel, das zeigt, dass die Greedy-Strategie nicht auf das 0-1Rucksackproblem angewendet werden kann. (a) Der Dieb muss eine Teilmenge der gezeigten Gegenstände auswählen, deren Gewicht 50 Pfund nicht übersteigen darf. (b) Die optimale Teilmenge enthält die Gegenstände 2 und 3. Jede Lösung mit Gegenstand 1 ist suboptimal, wenngleich Gegenstand 1 den größten Wert pro Pfund besitzt. (c) Beim gebrochenen Rucksackproblem führt die Mitnahme der Gegenstände in der Reihenfolge der größten Werte pro Pfund zu einer optimalen Lösung.
wendbar ist, und in der Tat, wir können dynamische Programmierung anwenden (siehe Übung 16.2-2).
Übungen 16.2-1 Beweisen Sie, dass das gebrochene Rucksackproblem die Greedy-Auswahl-Eigenschaft besitzt. 16.2-2 Geben Sie ein dynamisches Programm an, das das 0-1-Rucksackproblem in Laufzeit O(n W ) löst, wobei n die Anzahl der Gegenstände und W das maximale Gewicht der Gegenstände ist, die der Dieb in den Rucksack stecken kann. 16.2-3 Setzen Sie voraus, dass beim 0-1-Rucksackproblem die Reihenfolge der Gegenstände bei Sortierung nach steigendem Gewicht der Reihenfolge der Gegenstände bei Sortierung nach fallendem Wert entspricht. Geben Sie einen effizienten Algorithmus an, der eine optimale Lösung dieser Variante des Rucksackproblems bestimmt. Zeigen Sie, dass Ihr Algorithmus korrekt ist. 16.2-4 Professor Gekko hat immer davon geträumt, einmal North Dakota mit Inlineskater zu durchqueren. Er plant, den Staat auf dem Highway U.S. 2 zu durchqueren, der bei Grand Forks an der östlichen Grenze zu Minnesota startet und in Williston an der westlichen Grenze zu Montana endet. Der Professor kann zwei Liter Wasser bei sich tragen und er kann m Meilen mit den Inlineskater zurücklegen, bevor ihm das Trinkwasser ausgeht. (Da North Dakota ziemlich eben ist, braucht sich der Professor keine Gedanken zu machen, dass er bergauf öfters Wasser trinken muss als bei bergab oder auf flachen Teilstücken.) Der Professor will in Grand Forks mit zwei vollen Litern Trinkwasser starten. Seine
16.3 Huffman-Codierungen
431
offizielle Karte des Staates North Dakota zeigt alle Orte entlang der U.S. 2, an denen er sein Wasser wieder auffüllen kann, und weist die Distanzen zwischen diesen Positionen aus. Das Ziel des Professors besteht darin, die Anzahl der Wasserstopps entlang seiner Route durch den Staat zu minimieren. Geben Sie eine effiziente Methode an, mit der er bestimmen kann, welche Wasserstopps er machen sollte. Beweisen Sie, dass Ihre Strategie zu einer optimalen Lösung führt, und geben Sie die Laufzeit Ihrer Methode an. 16.2-5 Geben Sie einen effizienten Algorithmus an, der aus einer gegebenen Menge von Punkten {x1 , x2 , . . . , xn } auf der reellen Achse die kleinste Menge von geschlossenen Intervallen der Länge 1 bestimmt, die alle gegebenen Punkte enthalten. Zeigen Sie, dass Ihr Algorithmus korrekt ist. 16.2-6∗ Zeigen Sie, wie das gebrochene Rucksackproblem in Zeit O(n) zu lösen ist. 16.2-7 Setzen Sie voraus, dass Sie zwei Mengen A und B gegeben haben, die jeweils n positive ganze Zahlen enthalten. Sie können jede Menge beliebig umordnen. Nach der Umordnung sei ai das i-te Element der Menge A und = bi das in te Element der Menge B. Sie erhalten dann eine Auszahlung von i=1 ai bi . Geben Sie einen Algorithmus an, der die Auszahlung maximiert. Beweisen Sie, dass Ihr Algorithmus die Auszahlung maximiert, und geben Sie dessen Laufzeit an.
16.3
Huffman-Codierungen
Huffman-Codierungen komprimieren Daten sehr effektiv: Abhängig von den Eigenschaften der zu komprimierenden Daten sind Einsparungen von 20% bis 90% typisch. Wir betrachten die Daten als Zeichenketten. Huffmans Greedy-Algorithmus verwendet eine Tabelle, die angibt, wie oft welches Zeichen vorkommt (also die Häufigkeit der Zeichen), um eine optimale Darstellung eines jeden Zeichens als binärer String zu konstruieren. Nehmen Sie an, wir wollten ein Datenfile mit 100.000 Zeichen kompakt speichern. Wir stellen fest, dass die Zeichen im File mit den in Abbildung 16.3 angegebenen Häufigkeiten vorkommen. Das heißt, nur 6 verschiedene Zeichen treten überhaupt auf, und das Zeichen a kommt 45.000 Mal vor. Wir haben mehrere Möglichkeiten, ein solches File von Daten zu codieren. Wir betrachten hier das Problem, einen binären Zeichencode (oder kurz Code) zu entwerfen, in dem jedes Zeichen durch einen eindeutigen binären String, den wir Codewort nennen, dargestellt wird. Wenn wir einen Code fester Länge verwenden, benötigen wir 3 Bits, um 6 Zeichen darzustellen: a = 000, b = 001, . . . , f = 101. Diese Methode benötigt 300.000 Bits, um das gesamte File zu codieren. Können wir es besser machen? Ein Code variabler Länge kann das File wesentlich besser als ein Code fester Länge codieren, indem er häufig auftretenden Zeichen kurze Codewörter und selten auftretenden Zeichen lange Codewörter zuweist. Abbildung 16.3 zeigt einen solchen Code;
432
16 Greedy-Algorithmen
Häufigkeit (in Tausend) Codewort fester Länge Codewort variabler Länge
a 45 000 0
b 13 001 101
c 12 010 100
d 16 011 111
e 9 100 1101
f 5 101 1100
Abbildung 16.3: Ein Zeichencodierungsproblem. Ein Datenfile aus 100.000 Zeichen, das nur die Zeichen a–f mit den angegebenen Häufigkeiten enthält. Wenn jedem Zeichen ein 3-BitCodewort zugeordnet wird, kann das File mit 300.000 Bits codiert werden. Verwenden wir den gezeigten Code variabler Länge, kann das File mit nur 224.000 Bits codiert werden.
hier stellt die 1-Bit-Zeichenkette 0 das Zeichen a und die 4-Bit-Zeichenkette 1100 das Zeichen f dar. Dieser Code benötigt (45 · 1 + 13 · 3 + 12 · 3 + 16 · 3 + 9 · 4 + 5 · 4) · 1000 = 224.000 Bits, um das File darzustellen, was einer Ersparnis von etwa 25% entspricht. Tatsächlich ist dies ein optimaler Zeichencode für dieses File, wie wir noch sehen werden.
Präfix-Code Wir betrachten hier nur Codierungen, bei denen kein Codewort gleichzeitig Präfix eines anderen Codewortes ist. Solche Codierungen werden als Präfix-Code bezeichnet.4 Wenngleich wir es hier nicht beweisen werden, kann ein Präfix-Code immer die optimale Datenkompression (verglichen mit jedem anderen Zeichencode) erreichen, sodass wir uns nicht wirklich einschränken, wenn wir unsere Aufmerksamkeit den Präfix-Codes schenken. Das Codieren ist bei binären Zeichencodes immer einfach; wir verbinden einfach nur die Codewörter, die jedes Zeichen des Files darstellen. Mit dem Präfix-Code variabler Länge aus Abbildung 16.3 codieren wir beispielsweise das aus 3 Zeichen bestehende File abc durch 0 · 101 · 100 = 0101100, wobei „·“ die Konkatenation darstellt. Präfix-Codes sind wohlgelitten, weil sie die Decodierung vereinfachen. Da kein Codewort Präfix eines anderen ist, ist das Codewort, mit dem ein codiertes File beginnt, eindeutig. Wir können das Anfangscodewort einfach identifizieren, es in das ursprüngliche Zeichen zurückübersetzen und den Decodierungsprozess für den Rest des codierten Files wiederholen. In unserem Beispiel wird die Zeichenkette 001011101 eindeutig als 0 · 0 · 101 · 1101 analysiert, was zu aabe decodiert wird. Der Decodierungsprozess benötigt eine geeignete Darstellung des Präfix-Codes, damit wir das Anfangscodewort einfach bestimmen können. Ein binärer Baum, dessen Blätter die gegebenen Zeichen sind, liefert eine solche Darstellung. Wir interpretieren das binäre Codewort eines Zeichens als Pfad von der Wurzel zu diesem Zeichen, wobei 0 „gehe zum linken Kind“ und 1 „gehe zum rechten Kind“ bedeutet. Abbildung 16.4 zeigt die Bäume für die beiden Codes aus unserem Beispiel. Beachten Sie, dass dies keine binären Suchbäume sind, da die Blätter nicht notwendigerweise in sortierter Reihenfolge auftreten, und die inneren Knoten keine Zeichenschlüssel enthalten. 4 Vielleicht wäre „präfixfreier Code“ eine bessere Bezeichnung, aber in der Literatur ist der Ausdruck „Präfix-Code“ Standard.
16.3 Huffman-Codierungen
433
100
100
0
1
0
86 0
14 1
58 0 a:45
0 c:12
55
0 28
1 b:13
1
a:45 0
14 1 d:16
0 e:9
1
25 1 f:5
0 c:12
30 1 b:13 0 f:5
(a)
0 14
1 d:16 1 e:9
(b)
Abbildung 16.4: Die zu den Codierungen aus Abbildung 16.3 gehörenden Bäume. Jedes Blatt ist mit einem Zeichen und dessen Häufigkeit beschriftet. Jeder innere Knoten ist mit der Summe der Häufigkeiten der Blätter in seinem Teilbaum gekennzeichnet. (a) Der zur Codierung fester Länge a = 000, . . . ,f = 101 gehörende Baum. (b) Der zu dem optimalen Präfix-Code a = 0, b = 101, . . . , f = 1100 gehörige Baum.
Eine optimale Codierung eines Files wird immer durch einen vollständigen binären Baum dargestellt, bei dem jeder innere Knoten zwei Kinder hat (siehe Übung 16.3-2). Die Codierung fester Länge aus unserem Beispiel ist nicht optimal, da ihr in Abbildung 16.4(a) gezeigter Baum kein vollständiger binärer Baum ist: Er enthält zwar mit 10. . . beginnende Codewörter, aber keine mit 11. . . beginnende. Wir können unsere Aufmerksamkeit auf vollständige binäre Bäume beschränken und Folgendes feststellen. Wenn C das Alphabet ist, aus dem die Zeichen sind, und alle Häufigkeiten der Zeichen positiv sind, dann besitzt der Baum zum optimalen Präfix-Code genau |C| Blätter, eines für jeden Buchstaben des Alphabets, und genau |C| − 1 innere Knoten (siehe Übung B.5-3). Gegeben sei ein zum Präfix-Code gehörender Baum T , dann können wir leicht die Anzahl der zur Codierung des Files benötigten Bits berechnen. Für jedes Zeichen c aus dem Alphabet C lassen wir die Häufigkeit von c im File durch das Attribut c.freq anzeigen und bezeichnen die Tiefe des Blattes c im Baum mit dT (c). Beachten Sie, dass dT (c) auch die Länge des Codewortes für das Zeichen c angibt. Die Anzahl der für die Codierung des Files benötigten Bits ergibt sich somit zu c.freq · dT (c) , (16.4) B(T ) = c∈C
was wir als die Kosten des Baumes T definieren.
Konstruktion einer Huffman-Codierung Huffman entwickelte einen Greedy-Algorithmus, der einen als Huffman-Codierung bezeichneten optimalen Präfix-Code konstruiert. Übereinstimmend mit unserer Beobachtung aus Abschnitt 16.2 baut dessen Korrektheitsbeweis auf der Greedy-Auswahl-
434
16 Greedy-Algorithmen
Eigenschaft und der optimale-Teilstruktur-Eigenschaft auf. Anstatt zu zeigen, dass diese Eigenschaften erfüllt sind und erst dann das Programm zu entwickeln, stellen wir den Pseudocode zuerst vor. Dies wird dabei helfen, zu verstehen, wie der Algorithmus die Greedy-Entscheidung trifft. Im folgenden Pseudocode setzen wir voraus, dass C eine Menge von n Zeichen ist und dass jedes Zeichen c ∈ C ein Objekt mit einem Attribut c.freq ist, das seine Häufigkeit angibt. Der Algorithmus erzeugt bottom-up den der optimalen Codierung entsprechenden Baum T . Er beginnt mit einer Menge von |C| Blättern und führt eine Folge von |C| − 1 „Verschmelzungsoperationen“ durch, um den finalen Baum zu konstruieren. Der Algorithmus benutzt eine nach den freq-Attributen sortierte MinPrioritätswarteschlange Q, um die beiden seltensten Objekte zu identifizieren, die verschmolzen werden sollen. Wenn wir zwei Objekte miteinander verschmelzen, ist das Ergebnis dieses Schrittes ein neues Objekt, dessen Häufigkeit durch die Summe der Häufigkeiten der beiden verschmolzenen Objekte gegeben ist. Huffman(C) 1 n = |C| 2 Q=C 3 for i = 1 to n − 1 4 Allokiere einen neuen Knoten z 5 z.links = x = Extract-Min(Q) 6 z.rechts = y = Extract-Min(Q) 7 z.freq = x.freq + y.freq 8 Insert(Q, z) 9 return Extract-Min(Q) // Gib die Wurzel des Baumes zurück In unserem Beispiel arbeitet Huffmans Algorithmus, wie dies in Abbildung 16.5 gezeigt ist. Da das Alphabet 6 Zeichen enthält, ist die Anfangsgröße der Warteschlange n = 6 und es sind 5 Verschmelzungsschritte notwendig, um den Baum zu konstruieren. Der finale Baum stellt den optimalen Präfix-Code dar. Das Codewort für ein Zeichen ist die Sequenz der Kantenmarkierungen auf dem einfachen Pfad von der Wurzel zum Zeichen. Zeile 2 initialisiert die Min-Prioritätswarteschlange Q mit den Zeichen aus C. Die forSchleife in den Zeilen 3–8 wählt wiederholt die beiden Knoten x und y mit der niedrigsten Häufigkeit aus der Warteschlange aus, und ersetzt sie in der Warteschlange durch einen neuen Knoten z, welcher deren Verschmelzung darstellt. Die Häufigkeit von z wird in Zeile 7 als Summe der Häufigkeiten von x und y berechnet. Der Knoten z besitzt x als linkes Kind und y als rechtes Kind. (Diese Reihenfolge ist beliebig; das Vertauschen des linken und rechten Kindes eines Knotens führt auf eine andere Codierung mit denselben Kosten.) Nach n − 1 Verschmelzungen gibt Zeile 9 den einzigen in der Warteschlange verbliebenen Knoten zurück; dieser ist die Wurzel des Codebaums. Obwohl der Algorithmus das gleiche Ergebnis produzieren würde, wenn wir die Variablen x und y entfernen würden – indem wir in den Zeilen 5 und 6 z.links and z.rechts direkt ihre Werte zuweisen würden und wir Zeile 7 zu z.freq = z.links.freq + z.rechts.freq ändern würden – werden wir im Korrektheitsbeweis die Knotennamen x und y verwenden. Aus diesem Grund belassen wir sie beide auch im Pseudocode.
16.3 Huffman-Codierungen
(a)
f:5
e:9
c:12
b:13
435
d:16
a:45
(b)
c:12
14
b:13 0 f:5
14
(c) 0 f:5
(e)
25
d:16 1 e:9
0 c:12
a:45
55
a:45 0
0 f:5
0 14
30 1 b:13
0 14
a:45 1 d:16
0
1
f:5
e:9
1 55
a:45 0
1 d:16 1 e:9
a:45
100 0
30 1 b:13
0 c:12
(f) 1
25 0 c:12
25
(d)
1 b:13
d:16 1 e:9
1
25 0 c:12
30 1 b:13
0 14
1 d:16
0
1
f:5
e:9
Abbildung 16.5: Die Schritte von Huffmans Algorithmus für die in Abbildung 16.3 angegebenen Häufigkeiten. Jeder Teil zeigt den Inhalt der Warteschlange, der hier in steigender Reihenfolge nach der Häufigkeit sortiert ist. Bei jedem Schritt werden die beiden Bäume mit der geringsten Häufigkeit verschmolzen. Blätter sind als Rechtecke gekennzeichnet, die ein Zeichen und seine Häufigkeit enthalten. Innere Knoten sind als Kreise dargestellt, die die Summe der Häufigkeiten ihrer Kinder enthalten. Eine Kante, die einen inneren Knoten mit dessen Kind verbindet, wird mit 0 gekennzeichnet, wenn es eine Kante zu einem linken Kind ist, und mit 1, wenn es sich um eine Kante zu einem rechten Kind handelt. Das Codewort eines Zeichens ergibt sich aus der Folge der Kantenmarkierungen, die die Wurzel mit dem Blatt für diesen Buchstaben verbinden. (a) Die Ausgangsmenge aus n = 6 Knoten, einer für jeden Buchstaben. (b)–(e) Zwischenzustände. (f ) Der endgültige Baum.
436
16 Greedy-Algorithmen
Um die Laufzeit von Huffmans Algorithmus zu analysieren, setzen wir voraus, dass Q als ein binärer Min-Heap (siehe Kapitel 6) implementiert ist. Für eine Menge C von n Zeichen können wir Q in Zeile 2 in Laufzeit O(n) initialisieren, indem wir die in Abschnitt 6.3 diskutierte Prozedur Build-Min-Heap verwenden. Die for-Schleife in den Zeilen 3–8 wird genau n − 1 Mal ausgeführt, und, da jede Heap-Operation Zeit O(lg n) erfordert, trägt die Schleife mit O(n lg n) zur Gesamtlaufzeit bei. Somit beläuft sich die Gesamtlaufzeit des Huffman-Algorithmus auf einer Menge von n Zeichen auf O(n lg n). Wir können die Laufzeit auf O(n lg lg n) reduzieren, indem wir den binären Min-Heap durch einen van-Emde-Boas-Baum ersetzen (siehe Kapitel 20).
Korrektheit des Huffman-Algorithmus Um zu beweisen, dass der Greedy-Algorithmus Huffman korrekt ist, zeigen wir, dass das Problem, einen optimalen Präfix-Code zu bestimmen, die Greedy-Auswahl-Eigenschaft und die optimale-Teilstruktur-Eigenschaft besitzt. Das nachfolgende Lemma zeigt, dass das Problem die Greedy-Auswahl-Eingenschaft besitzt. Lemma 16.2 Sei C ein Alphabet, in dem jedes Zeichen c ∈ C die Häufigkeit c.freq besitzt. Seien x und y zwei Zeichen aus C mit den niedrigsten Häufigkeiten. Dann existiert ein optimaler Präfix-Code für C, in dem die Codewörter für x und y die gleiche Länge haben und sich nur im letzten Bit unterscheiden. Beweis: Die Idee des Beweises ist, den Baum T , der einen beliebigen optimalen PräfixCode darstellt, in einen Baum zu transformieren, der einen (anderen) optimalen PräfixCode darstellt und in dem die Zeichen x und y Geschwisterblätter maximaler Tiefe sind. Wenn wir eine solchen Baum konstruieren können, dann werden die Codewörter von x und y die gleiche Länge haben und sich nur im letzten Bit unterscheiden. Seien a und b zwei Zeichen, die in T durch Geschwisterblätter maximaler Tiefe dargestellt werden. Ohne Beschränkung der Allgemeinheit setzen wir voraus, dass a.freq ≤ b.freq und x.freq ≤ y.freq gelten. Da x.freq und y.freq die niedrigsten Häufigkeiten auf den Blättern sind, und a.freq und b.freq zwei beliebige Häufigkeiten sind, gelten x.freq ≤ a.freq und y.freq ≤ b.freq. Im Rest des Beweises ist es möglich, dass wir x.freq = a.freq oder y.freq = b.freq haben könnten. Wenn wir aber x.freq = b.freq hätten, dann würden wir auch a.freq = b.freq = x.freq = y.freq haben (siehe Übung 16.3-1) und das Lemma wäre trivialerweise wahr. Aus diesem Grunde werden wir voraussetzen, dass x.freq = b.freq gilt, woraus x = b folgt. Wie Abbildung 16.6 zeigt, vertauschen wir in T die Positionen von a und x, um einen Baum T zu erzeugen. Dann vertauschen wir in T die Positionen von b und y, um einen Baum T zu erzeugen, in dem x und y Geschwisterblätter maximaler Tiefe sind. (Bemerken Sie, dass, wenn x = b und y = a gilt, Baum T x und y nicht als Geschwisterblätter maximaler Tiefe hat. Da wir vorausgesetzt haben, dass x = b gilt, kann diese
16.3 Huffman-Codierungen
437 T′
T
T′′
x
a
y
y a
b
a b
x
b
x
y
Abbildung 16.6: Eine Illustration des Hauptschrittes im Beweis von Lemma 16.2. Im optimalen Baum T sind die Blätter a und b zwei Geschwister maximaler Tiefe. Die Blätter x und y sind die zwei Zeichen mit der geringsten Häufigkeit; sie erscheinen in T an zwei beliebigen Stellen. Setzen wir x = b voraus, dann erzeugt die Vertauschung der Blätter a und x den Baum T und die Vertauschung der Blätter b und y den Baum T . Da keiner der Schritte die Kosten erhöht, ist der resultierende Baum T ein optimaler Baum.
Situation nicht vorkommen.) Nach Gleichung (16.4) beträgt die Differenz der Kosten zwischen T und T B(T ) − B(T ) = c.freq · dT (c) − c.freq · dT (c) c∈C
c∈C
= x.freq · dT (x) + a.freq · dT (a) − x.freq · dT (x) − a.freq · dT (a) = x.freq · dT (x) + a.freq · dT (a) − x.freq · dT (a) − a.freq · dT (x) = (a.freq − x.freq )(dT (a) − dT (x)) ≥0, da sowohl a.freq − x.freq als auch dT (a) − dT (x) nichtnegativ sind. Genauer gesagt, a.freq −x.freq ist nichtnegativ, da x ein Blatt mit minimaler Häufigkeit ist, und dT (a)− dT (x) ist nichtnegativ, da a ein Blatt maximaler Tiefe in T ist. Analog erhöht das Vertauschen von y und b die Kosten nicht, und somit ist B(T ) − B(T ) nichtnegativ. Deshalb gilt B(T ) ≤ B(T ), und, weil T optimal ist, auch B(T ) ≤ B(T ), was B(T ) = B(T ) impliziert. Somit ist T ein optimaler Baum, in dem x und y als Geschwisterblätter maximaler Tiefe erscheinen, woraus das Lemma folgt. Lemma 16.2 impliziert, dass die Konstruktion eines optimalen Baumes durch Verschmelzen ohne Beschränkung der Allgemeinheit mit der Greedy-Entscheidung, die beiden Zeichen mit den niedrigsten Häufigkeiten zu verschmelzen, beginnen kann. Warum ist dies eine Greedy-Entscheidung? Wir können die Kosten einer einzelnen Verschmelzung als Summe der Häufigkeiten der beiden verschmolzenen Elemente ansehen. Übung 16.3-4 zeigt, dass dann die Gesamtkosten des konstruierten Baumes gleich der Summe der Kosten seiner Verschmelzungen ist. Bei jedem Schritt wählt der Huffman-Algorithmus aus allen möglichen Verschmelzungen diejenige, die die geringsten Kosten verursacht. Das nächste Lemma zeigt, dass das Problem, einen optimalen Präfix-Code zu bestimmen, die optimale-Teilstruktur-Eigenschaft besitzt.
438
16 Greedy-Algorithmen
Lemma 16.3 Sei C ein gegebenes Alphabet, für das für jedes Zeichen c ∈ C die Häufigkeit c.freq definiert ist. Seien x und y zwei Zeichen in C mit minimaler Häufigkeit. Sei C das Alphabet C, aus dem die Zeichen x und y entfernt wurden und zu dem ein neues Zeichen z hinzugefügt wurde, sodass C = C − {x, y} ∪ {z} gilt. Definieren Sie die Häufigkeit freq auf C wie auf C und setzen Sie z.freq = x.freq + y.freq. Sei T ein Baum, der einen optimalen Präfix-Code für das Alphabet C darstellt. Dann stellt der Baum T , den wir aus T erhalten, indem wir das Blatt z durch einen inneren Knoten ersetzen, der x und y als Kinder hat, einen optimalen Präfix-Code für das Alphabet C dar. Beweis: Wir zeigen zunächst, wie sich die Kosten B(T ) des Baumes T als Funktion der Kosten B(T ) des Baumes T beschreiben lassen, indem wir die Kosten der Komponenten in Gleichung (16.4) betrachten. Für jedes Zeichen c ∈ C − {x, y} gilt dT (c) = dT (c), und somit c.freq · dT (c) = c.freq · dT (c). Wegen dT (x) = dT (y) = dT (z) + 1 gilt x.freq · dT (x) + y.freq · dT (y) = (x.freq + y.freq)(dT (z) + 1) = z.freq · dT (z) + (x.freq + y.freq) , woraus wir schlussfolgern, dass B(T ) = B(T ) + x.freq + y.freq d. h. B(T ) = B(T ) − x.freq − y.freq gilt. Wir beweisen das Lemma nun indirekt. Nehmen Sie an, dass T keinen optimalen PräfixCode für C darstellen würde. Dann gibt es einen optimalen Baum T mit B(T ) < B(T ). T enthält ohne Beschränkung der Allgemeinheit (nach Lemma 16.2) die Knoten x und y als Brüder. Sei T der Baum T , bei dem der gemeinsame Vater von x und y durch ein Blatt z mit der Häufigkeit z.freq = x.freq + y.freq ersetzt wurde. Dann gilt B(T ) = B(T ) − x.freq − y.freq < B(T ) − x.freq − y.freq = B(T ) , was zu einem Widerspruch zu unserer Annahme, dass T einen optimalen Präfix-Code für C darstellt, führt. Somit muss T einen optimalen Präfix-Code für das Alphabet C darstellen.
16.3 Huffman-Codierungen
439
Theorem 16.4 Die Prozedur Huffman erzeugt einen optimalen Präfix-Code. Beweis: Der Beweis folgt unmittelbar aus den Lemmata 16.2 und 16.3.
Übungen 16.3-1 Erklären Sie, warum in dem Beweis von Lemma 16.2 a.freq = b.freq = x.freq = y.freq gelten muss, wenn x.freq = b.freq gilt. 16.3-2 Beweisen Sie, dass ein binärer Baum, der nicht voll ist, keinem optimalen Präfix-Code entsprechen kann. 16.3-3 Wie sieht eine optimale Huffman-Codierung für die folgende Menge von Häufigkeiten, die die ersten acht Fibonacci-Zahlen darstellen, aus? a:1 b:1 c:2 d:3 e:5 f:8 g:13 h:21 Können Sie Ihre Antwort auf den Fall, dass die Häufigkeiten die ersten n Fibonacci-Zahlen sind, verallgemeinern? 16.3-4 Beweisen Sie, dass wir die Gesamtkosten eines Baumes für einen Code auch als die Summe über alle inneren Knoten der kombinierten Häufigkeiten der zwei Kinder der Knoten ausdrücken können. 16.3-5 Beweisen Sie, dass ein optimaler Code existiert, dessen Codewort-Längen monoton steigend sind, wenn wir die Zeichen des Alphabets monoton fallend nach ihren Häufigkeiten ordnen. 16.3-6 Nehmen Sie an, wir hätten einen optimalen Präfix-Code auf der Menge C = {0, 1, . . . , n − 1} von Zeichen, und wollten diesen Code mit so wenig Bits wie möglich übertragen. Zeigen Sie, wie jeder optimale Präfix-Code auf C unter Verwendung von nur 2n − 1 + n lg n Bits dargestellt werden kann. (Hinweis: Benutzen Sie 2n − 1 Bits, um die Struktur des Baumes zu spezifizieren, wie man sie bei einer Traversierung durch den Baum sieht.) 16.3-7 Verallgemeinern Sie den Huffman-Algorithmus auf ternäre Codewörter (d. h. Codewörter, die die Symbole 0, 1 und 2 verwenden), und beweisen Sie, dass der Algorithmus jeweils einen optimalen ternären Code konstruiert. 16.3-8 Setzen Sie voraus, dass ein Datenfile eine Sequenz aus 8-Bit-Zeichen enthält, in der alle 256 Zeichen etwa gleichhäufig auftreten: die maximale Zeichenhäufigkeit ist geringer als das Doppelte der minimalen Zeichenhäufigkeit. Beweisen Sie, dass die Huffman-Codierung in diesem Fall nicht effizienter als eine gewöhnliche 8-Bit-Codierung fester Länge ist.
440
16 Greedy-Algorithmen
16.3-9 Zeigen Sie, dass wir bei keiner Komprimierungsmethode erwarten können, ein File mit zufällig gewählten 8-Bit-Zeichen auch nur um ein einziges Bit zu komprimieren. (Hinweis: Vergleichen Sie die Anzahl der Files mit der Anzahl der möglichen codierten Files.)
∗ 16.4 Matroiden und Greedy-Methoden In diesem Abschnitt skizzieren wir eine wunderschöne Theorie zu Greedy-Algorithmen. Diese Theorie beschreibt viele Szenarien, in denen die Greedy-Methode optimale Lösungen bestimmt. Sie beinhaltet kombinatorische Strukturen, die als „Matroide“ bekannt sind. Obwohl diese Theorie nicht alle Fälle abdeckt, bei denen die GreedyMethode anwendbar ist (beispielsweise werden das Aktivitäten-Auswahl-Problem aus Abschnitt 16.1 und das Problem, eine Huffman-Codierung zu bestimmen, aus Abschnitt 16.3 nicht abgedeckt), behandelt sie viele Fälle, die von praktischem Interesse sind. Darüberhinaus wurde die Theorie weiterentwickelt, um weitere Anwendungen abzudecken; entsprechende Literaturhinweise finden Sie in den Kapitelbemerkungen am Ende dieses Kapitels.
Matroide Ein Matroid ist ein geordnetes Paar M = (S, I), das die folgenden Bedingungen erfüllt. 1. S ist eine endliche Menge. 2. I ist eine nichtleere Familie von Teilmengen von S, die als unabhängige Teilmengen von S bezeichnet werden, sodass aus B ∈ I und A ⊆ B die Beziehung A ∈ I folgt. Wir sagen, dass I erblich ist, wenn es diese Eigenschaft erfüllt. Beachten Sie, dass die leere Menge ∅ notwendigerweise ein Element von I ist. 3. Wenn A ∈ I, B ∈ I und |A| < |B| gelten, dann gibt es ein Element x ∈ B − A, sodass A ∪ {x} ∈ I gilt. Wir sagen, dass M die Austauscheigenschaft erfüllt. Das Wort „Matroid“ geht auf Hassler Whitney zurück. Er untersuchte matrische Matroide, bei denen die Elemente von S die Zeilen einer gegebenen Matrix sind, und eine Menge von Zeilen unabhängig ist, wenn die Zeilen im gewöhnlichen Sinne linear unabhängig sind. In Übung 16.4-2 sollen Sie zeigen, dass diese Struktur tatsächlich ein Matroid definiert. Als weiteres Beispiel für Matroide betrachten wir das graphische Matroid MG = (SG , IG ), das in Bezug auf einen gegebenen ungerichteten Graphen G = (V, E) wie folgt definiert ist: • Die Menge SG ist durch die Menge E der Kanten von G definiert. • Wenn A eine Teilmenge von E ist, dann gilt A ∈ IG genau dann, wenn A azyklisch ist. Das heißt, eine Menge von Kanten A ist genau dann unabhängig, wenn der Teilgraph GA = (V, A) einen Wald bildet.
16.4 ∗ Matroiden und Greedy-Methoden
441
Das graphische Matroid MG ist eng mit dem Problem minimaler aufspannender Bäume verwandt, das in Kapitel 23 im Detail behandelt wird. Theorem 16.5 Wenn G = (V, E) ein ungerichteter azyklischer Graph ist, dann ist MG = (SG , IG ) ein Matroid. Beweis: Offensichtlich ist SG = E eine endliche Menge. Darüber hinaus ist IG erblich, da eine Teilmenge eines Waldes wiederum ein Wald ist. Anders formuliert: Das Entfernen von Kanten aus einer azyklischen Menge von Kanten kann keine Zyklen erzeugen. Somit bleibt zu zeigen, dass MG die Austauscheigenschaft erfüllt. Setzen Sie voraus, dass GA = (V, A) und GB = (V, B) Wälder von G sind und dass |B| > |A| gilt. Das heißt, A und B sind azyklische Mengen von Kanten, und B enthält mehr Kanten als A. Wir behaupten, dass in einem Wald F = (VF , EF ) genau |VF | − |EF | Bäume sind. Um zu verstehen, warum dies so ist, setzen Sie voraus, dass F aus t Bäumen besteht, wobei der i-te Baum vi Knoten und ei Kanten enthält. Es gilt dann |EF | = =
=
t i=1 t i=1 t
ei (vi − 1)
(wegen Theorem B.2)
vi − t
i=1
= |VF | − t , woraus t = |VF | − |EF | folgt. Somit besteht der Wald GA aus |V | − |A| Bäumen und Wald GB aus |V | − |B| Bäumen. Da der Wald GB weniger Bäume als der Wald GA besitzt, muss Wald GB einen Baum T enthalten, dessen Knoten sich im Wald GA in zwei verschiedenen Bäumen befinden. Da der Baum T zusammenhängend ist, muss er darüber hinaus eine Kante (u, v) enthalten, deren Knoten u und v sich im Wald GA in verschiedenen Bäumen befinden. Da die Kante (u, v) im Wald GA die Knoten zweier verschiedener Bäume verbindet, können wir die Kante (u, v) zum Wald GA hinzufügen, ohne einen Zyklus zu erzeugen. Deshalb erfüllt MG die Austauscheigenschaft, womit bewiesen ist, dass MG ein Matroid ist. Gegeben sei ein Matroid M = (S, I). Wir bezeichnen ein Element x ∈ / A als Erweiterung von A ∈ I, wenn wir x zu A hinzufügen können und die Unabhängigkeit dabei erhalten bleibt. Das heißt, x ist eine Erweiterung von A, wenn A ∪ {x} ∈ I gilt. Betrachten Sie beispielsweise ein graphisches Matroid MG . Wenn A eine unabhängige Menge von Kanten ist, dann ist die Kante e genau dann eine Erweiterung von A, wenn e nicht zu A gehört und das Hinzufügen von e zu A keinen Zyklus erzeugt.
442
16 Greedy-Algorithmen
Wir nennen eine unabhängige Teilmenge A im Matroid M maximal, wenn sie keine Erweiterungen besitzt. Das heißt, die Teilmenge A ist maximal, wenn sie nicht in einer größeren unabhängigen Teilmenge von M enthalten ist. Die folgende Eigenschaft ist häufig nützlich. Theorem 16.6 Alle maximale unabhängige Teilmengen in einem Matroid haben dieselbe Größe. Beweis: Um einen Widerspruch herzuleiten, nehmen Sie an, dass A eine maximale unabhängige Teilmenge in M wäre und eine andere, größere maximale unabhängige Teilmenge B in M existieren würde. Dann würde aus der Austauscheigenschaft folgen, dass A für ein x ∈ B − A zu einer größeren unabhängigen Teilmenge A∪{x} erweiterbar wäre, was der Annahme widersprechen würde, dass A maximal ist. Zur Illustration dieses Theorems betrachten Sie ein graphisches Matroid MG für einen zusammenhängenden ungerichteten Graphen G. Jede maximale unabhängige Teilmenge von MG muss ein freier Baum mit genau |V | − 1 Kanten sein, der alle Knoten von G verbindet. Ein solcher Baum wird aufspannender Baum von G genannt. Wir sagen, dass ein Matroid M = (S, I) gewichtet ist, wenn ihm eine Gewichtsfunktion w zugeordnet ist, die jedem Element x ∈ S ein positives Gewicht echt größer Null zuordnet. Die Gewichtsfunktion w lässt sich auf Teilmengen A von S durch w(A) = w(x) x∈A
erweitern. Wenn wir beispielsweise in einem graphischen Matroid das Gewicht einer Kante e mit w(e) bezeichnen, dann ist w(A) das Gesamtgewicht der Kanten in der Kantenmenge A.
Greedy-Algorithmen auf einem gewichteten Matroid Viele Probleme, für die die Greedy-Methode optimale Lösungen liefert, können jeweils als Problem, eine unabhängige Teilmenge mit maximalem Gewicht in einem gewichteten Matroiden zu bestimmen, verstanden werden. Hierzu ist ein gewichtetes Matroid M = (S, I) gegeben und wir wollen eine unabhängige Menge A ∈ I bestimmen, für die w(A) maximal ist. Wir bezeichnen eine solche Teilmenge als optimale Teilmenge eines Matroids. Da das Gewicht w(x) jedes Elementes x ∈ S positiv ist, ist eine optimale Teilmenge immer eine maximale unabhängige Teilmenge – es hilft immer, A so groß wie möglich zu machen. Beim Problem minimaler aufspannender Bäume sind beispielsweise ein zusammenhängender ungerichteter Graph G = (V, E) und eine Längenfunktion w gegeben, sodass w(e) die (positive) Länge der Kante e ist. (Wir beziehen hier den Begriff „Länge“ auf das ursprüngliche Kantengewicht eines Graphen, während wir uns den Ausdruck „Ge-
16.4 ∗ Matroiden und Greedy-Methoden
443
wicht“ für das Gewicht im zugehörigen Matroid vorbehalten.) Wir wollen eine Teilmenge von Kanten bestimmen, die alle Knoten miteinander verbindet und minimale Gesamtlänge besitzt. Um dieses Problem auf die Berechnung einer optimalen Teilmenge eines Matroids zurückzuführen, betrachten wir ein gewichtetes Matroid MG mit der Gewichtsfunktion w , wobei w (e) = w0 − w(e) gilt und w0 größer als die Länge einer jeden Kante ist. In diesem gewichteten Matroid sind alle Gewichte positiv und eine optimale Teilmenge ist ein aufspannender Baum minimaler Gesamtlänge im ursprünglichen Graphen. Insbesondere entspricht jede maximale unabhängige Teilmenge A einem aufspannenden Baum mit |V | − 1 Kanten und, da w (A) =
w (e)
e∈A
=
(w0 − w(e))
e∈A
= (|V | − 1)w0 −
w(e)
e∈A
= (|V | − 1)w0 − w(A) für jede maximale unabhängige Teilmenge A gilt, muss eine unabhängige Teilmenge, die die Größe w (A) maximiert, w(A) minimieren. Somit kann jeder Algorithmus, der eine optimale Teilmenge A in einem beliebigen Matroid bestimmen kann, das Problem des minimalen aufspannenden Baumes lösen. In Kapitel 23 werden Algorithmen für das Problem, einen minimalen aufspannenden Baum zu finden, vorgestellt. Hier geben wir jedoch einen Greedy-Algorithmus an, der auf jedes gewichtete Matroid anwendbar ist. Der Algorithmus erhält als Eingabe ein gewichtetes Matroid M = (S, I) mit einer zugehörigen positiven Gewichtsfunktion w und gibt eine optimale Teilmenge A zurück. In unserem Pseudocode bezeichnen wir die Komponenten von M mit M.S und M.I und die Gewichtsfunktion mit w. Dieser Algorithmus ist ein Greedy-Algorithmus, da er nacheinander in der Reihenfolge monoton fallender Gewichte jedes Element x ∈ S betrachtet und es der Menge A hinzufügt, wenn A ∪ {x} unabhängig ist. Greedy(M, w) 1 A=∅ 2 sortiere M.S monoton fallend nach dem Gewicht w 3 for alle x ∈ M.S , in, nach dem Gewicht w(x), monoton fallender Reihenfolge betrachtet 4 if A ∪ {x} ∈ M.I 5 A = A ∪ {x} 6 return A Zeile 4 überprüft, ob das Hinzufügen des Elements x zu A die Teilmenge A weiter unabhängig belässt. Wenn A weiterhin unabhängig ist, dann fügt Zeile 5 das Element x zu A hinzu. Anderenfalls wird x verworfen. Da die leere Menge unabhängig ist und
444
16 Greedy-Algorithmen
da jede Iteration der for-Schleife die Unabhängigkeit von A erhält, ist die Teilmenge A durch Induktion immer unabhängig. Deshalb gibt Greedy immer eine unabhängige Teilmenge A zurück. Wir werden gleich sehen, dass A eine Teilmenge mit maximalem Gewicht ist, sodass A eine optimale Teilmenge ist. Die Laufzeit von Greedy kann leicht analysiert werden. Lassen Sie uns mit n die Anzahl |S| bezeichnen. Die Sortierphase von Greedy benötigt Zeit O(n lg n). Die Zeile 4 wird für jedes Element von S genau einmal ausgeführt, insgesamt also n-mal. Jede Ausführung von Zeile 4 erfordert einen Test, ob die Menge A ∪ {x} unabhängig ist. Wenn jeder dieser Tests Zeit O(f (n)) benötigt, dann läuft der gesamte Algorithmus in Zeit O(n lg n + nf (n)). Wir beweisen nun, dass die Prozedur Greedy eine optimale Teilmenge zurückgibt.
Lemma 16.7: (Matroide besitzen die Greedy-Auswahl-Eigenschaft.) Setzen Sie voraus, dass M = (S, I) ein gewichtetes Matroid mit der Gewichtsfunktion w ist, und dass S in monoton fallender Reihenfolge nach dem Gewicht sortiert ist. Sei x das erste Element von S, für das {x} unabhängig ist, vorausgesetzt ein solches x existiert. Wenn x existiert, dann gibt es eine optimale Teilmenge A von S, die x enthält.
Beweis: Wenn kein solches x existiert, dann ist die einzige unabhängige Teilmenge die leere Menge und das Lemma ist offensichtlich wahr. Anderenfalls sei B eine beliebige, nichtleere optimale Teilmenge. Setzen Sie voraus, dass x ∈ / B gilt; anderenfalls erhalten Sie mit A = B eine optimale Teilmenge von S, die x enthält. Kein Element von B besitzt ein größeres Gewicht als w(x). Um dies zu sehen, sollten Sie wahrnehmen, dass aus y ∈ B bereits folgt, dass {y} unabhängig ist, da B ∈ I und I erblich ist. Unsere Wahl von x sichert deshalb, dass w(x) ≥ w(y) für jedes y ∈ B gilt. Konstruieren Sie die Menge A wie folgt. Beginnen Sie mit A = {x}. Aufgrund der Wahl von x ist A unabhängig. Unter Verwendung der Austauscheigenschaft finden Sie in jedem Schritt ein neues Element von B, das wir zu der Menge A hinzufügen können, bis |A| = |B| gilt, wobei die Unabhängigkeit von A erhalten bleibt. Es gilt dann, dass A und B gleich sind bis auf, dass A das Element x enthält und B dafür ein anderes Element y. Das heißt, dass A = B − {y} ∪ {x} für ein y ∈ B gilt und so auch w(A) = w(B) − w(y) + w(x) ≥ w(B) . Da B optimal ist, muss auch A, das x enthält, optimal sein.
Wir zeigen nun, dass ein Element, welches zu Beginn keine Option darstellt, auch später keine Option sein kann.
16.4 ∗ Matroiden und Greedy-Methoden
445
Lemma 16.8 Sei M = (S, I) ein beliebiges Matroid. Wenn x ein Element von S ist, das eine Erweiterung einer beliebigen unabhängigen Teilmenge A von S ist, dann ist x auch eine Erweiterung von ∅. Beweis: Da x eine Erweiterung von A ist, ist A ∪ {x} unabhängig. Da I erblich ist, muss {x} unabhängig sein. Damit ist x eine Erweiterung von ∅.
Korollar 16.9 Sei M = (S, I) ein beliebiges Matroid. Wenn x ein Element von S ist, das keine Erweiterung von ∅ ist, dann ist x keine Erweiterung irgendeiner unabhängigen Teilmenge A von S. Beweis: Dieses Korollar ist einfach nur die Umkehrung von Lemma 16.8.
Korollar 16.9 besagt, dass ein beliebiges Element, das nicht sofort verwendet werden kann, niemals verwendet wird. Deshalb kann die Prozedur Greedy keinen Fehler machen, wenn sie zu Beginn Elemente in S übergeht, die keine Erweiterung von ∅ sind, denn diese können niemals verwendet werden. Lemma 16.10: (Matroide besitzen die optimale-Teilstruktur-Eigenschaft) Sei x das erste von Greedy für das gewichtete Matroid M = (S, I) ausgewählte Element von S. Das verbleibende Problem, eine unabhängige Teilmenge mit maximalem Gewicht zu finden, die x enthält, reduziert sich darauf, eine unabhängige Teilmenge mit maximalem Gewicht des gewichteten Matroids M = (S , I ) zu finden, wobei S = {y ∈ S : {x, y} ∈ I} , I = {B ⊆ S − {x} : B ∪ {x} ∈ I} und die Gewichtsfunktion für M die auf S beschränkte Gewichtsfunktion für M ist. (Wir bezeichnen M als Kontraktion von M durch das Element x.) Beweis: Wenn A eine beliebige unabhängige Teilmenge mit maximalem Gewicht in M ist, die x enthält, dann ist A = A−{x} ebenfalls eine unabhängige Teilmenge in M . Umgekehrt führt jede beliebige Teilmenge A in M zu einer unabhängigen Teilmenge A = A ∪ {x} in M . Da in beiden Fällen w(A) = w(A ) + w(x) gilt, führt eine Lösung mit maximalem Gewicht in M , die x enthält, zu einer Lösung mit maximalem Gewicht in M und umgekehrt.
446
16 Greedy-Algorithmen
Theorem 16.11: (Korrektheit des Greedy-Algorithmus auf Matroiden) Wenn M = (S, I) ein gewichtetes Matroid mit der Gewichtsfunktion w ist, dann gibt Greedy(M, w) eine optimale Teilmenge zurück. Beweis: Elemente, die zu Beginn übergangen wurden, weil sie keine Erweiterungen von ∅ sind, können nach Korollar 16.9 verworfen werden, da sie niemals nützlich sein können. Sobald Greedy einmal das erste Element x ausgewählt hat, impliziert Lemma 16.7, dass sich der Algorithmus nicht irrt, wenn er x zur Menge A hinzufügt, da es dann eine optimale Teilmenge gibt, die x enthält. Schließlich folgt aus Lemma 16.10, dass das verbleibende Problem darin besteht, eine optimale Teilmenge im Matroid M zu finden, die die Kontraktion von M durch x ist. Nachdem die Prozedur Greedy A gleich {x} gesetzt hat, können wir alle verbleibenden Schritte als im Matroid M = (S , I ) wirkend interpretieren, da, für alle Mengen B ∈ I , B genau dann in M unabhängig ist, wenn B ∪ {x} unabhängig in M ist. Somit werden die nachfolgenden Operationen von Greedy eine unabhängige Teilmenge mit maximalem Gewicht in M finden, und die Gesamtoperation von Greedy wird eine unabhängige Teilmenge mit maximalem Gewicht in M finden.
Übungen 16.4-1 Sei S eine beliebige endliche Menge und Ik die Menge aller Teilmengen von S, deren Größe jeweils höchstens k ist, wobei k ≤ |S| gilt. Zeigen Sie, dass (S, Ik ) ein Matroid ist. 16.4-2∗ Gegeben sei eine m × n Matrix T über einem Bereich (zum Beispiel der reellen Zahlen). Zeigen Sie, dass (S, I) ein Matroid ist, wobei S die Menge der Spalten von T ist. Es gilt A ∈ I genau dann, wenn die Spalten in A linear unabhängig sind. 16.4-3∗ Zeigen Sie, dass (S, I ) ein Matroid ist, wenn (S, I) ein Matroid ist. Dabei soll I = {A : S − A enthält ein maximales A ∈ I} gelten. Die maximalen unabhängigen Mengen von (S, I ) sind also einfach die Komplemente der maximalen unabhängigen Mengen von (S, I). 16.4-4∗ Sei S eine endliche Menge und sei S1 , S2 , . . . , Sk eine Partition von S in nichtleere disjunkte Teilmengen. Definieren Sie die Struktur (S, I) durch I = {A : |A ∩ Si | ≤ 1 für i = 1, 2, . . . , k}. Zeigen Sie, dass (S, I) ein Matroid ist. Das heißt, die Menge aller Mengen A, die jeweils höchstens ein Element aus jeder Teilmenge der Partition enthalten, definiert die unabhängigen Mengen des Matroids. 16.4-5 Zeigen Sie, wie die Gewichtsfunktion eines gewichteten Matroidproblems zu transformieren ist, bei dem die gesuchte optimale Lösung eine maximale unabhängige Teilmenge mit minimalem Gewicht ist, um ein übliches gewichtetes Matroidproblem zu erhalten. Begründen Sie sorgfältig, warum Ihre Transformation korrekt ist.
16.5 ∗ Ein Task-Scheduling-Problem als Matroid
447
∗ 16.5 Ein Task-Scheduling-Problem als Matroid Ein interessantes Problem, das wir mithilfe von Matroiden lösen können, ist das Problem der optimalen Koordination von Unit-Time-Tasks auf einem einzigen Prozessor, wobei jeder Task einen Termin besitzt, zusammen mit einer Strafe, die bezahlt werden muss, wenn der Termin nicht eingehalten wird. Das Problem erscheint kompliziert, es kann jedoch auf überraschend einfache Weise gelöst werden, indem wir es als ein Matroid formulieren und einen Greedy-Algorithmus verwenden. Ein Unit-Time-Task ist ein Auftrag, wie zum Beispiel ein auf einem Rechner laufendes Programm, der genau eine Zeiteinheit benötigt, um abgearbeitet zu werden. Ist eine endliche Menge S von Unit-Time-Tasks gegeben, dann ist ein Ablaufplan für S eine Permutation von S, die die Reihenfolge bestimmt, in der diese Tasks auszuführen sind. Der erste Task im Ablaufplan beginnt zur Zeit 0 und endet zur Zeit 1, der zweite Task im Ablaufplan beginnt zur Zeit 1 und endet zur Zeit 2 usw. Das Problem der Ablaufkoordination von Unit-Time-Tasks mit Terminen und Strafen bei einen einzigen Prozessor bekommt die folgenden Eingaben: • eine Menge S = {a1 , a2 , . . . , an } von n Unit-Time-Tasks; • eine Menge von n ganzzahligen Terminen d1 , d2 , . . . , dn , wobei jedes di die Beziehung 1 ≤ di ≤ n erfüllt und erwartet wird, dass Task ai bis zur Zeit di abgearbeitet ist; und • eine Menge von nichtnegativen Gewichten oder Strafen w1 , w2 , . . . , wn , wobei wir eine Strafe von wi zu zahlen haben, wenn Task ai nicht bis zum Termin di abgearbeitet wurde. Wir müssen keine Strafe zahlen, wenn ein Task termingerecht beendet wurde. Wir wollen einen Ablaufplan für S finden, der die Gesamtstrafe aufgrund verpasster Termine minimiert. Betrachten Sie einen gegebenen Ablaufplan. Wir sagen, ein Task ist in diesem Ablaufplan zu spät, wenn er erst nach seinem Termin abgearbeitet ist. Anderenfalls ist der Task im Ablaufplan zu früh. Wir können einen beliebigen Ablaufplan immer in Early-First-Form bringen, in der die zu frühen Tasks den zu späten vorangehen. Um zu verstehen, warum dies so ist, haben Sie nur zu verstehen, dass wir die Positionen von ai und aj vertauschen können, wenn ein zu früher Task ai auf einen zu späten Task aj folgt. Nach diesem Vertauschen wird ai weiterhin zu früh und aj weiterhin zu spät sein. Zudem können wir behaupten, dass wir einen beliebigen Ablaufplan immer in kanonische Form bringen können, in der die zu frühen Tasks den zu späten vorangehen und die zu frühen Tasks in der Reihenfolge monoton steigender Termine geplant sind. Dazu bringen wir den Ablaufplan zunächst in die Early-First-Form. Danach vertauschen wir die Positionen von ai und aj , solange es zwei zu frühe Tasks ai und aj gibt, die zu den Zeiten k bzw. k + 1 im Ablaufplan abgearbeitet werden, sodass dj < di gilt. Da aj vor dem Tausch zu früh ist, gilt k + 1 ≤ dj . Deshalb gilt k + 1 < di und ai ist auch nach dem Tausch noch zu früh. Da Task aj zu einem noch früheren Zeitpunkt im Ablaufplan verschoben worden ist, bleibt er nach dem Tausch zu früh.
448
16 Greedy-Algorithmen
Die Suche nach einem optimalen Ablaufplan reduziert sich somit darauf, eine Menge A von Tasks zu bestimmen, die wir in dem optimalen Plan zu früh realisieren. Nachdem A bestimmt ist, können wir den eigentlichen Ablaufplan erstellen, indem wir die Elemente von A in monoton steigender Reihenfolge nach den Terminen sortieren. Danach führen wir die zu späten Tasks (d. h. S−A) in beliebiger Reihenfolge auf, wodurch ein optimaler Ablaufplan in kanonischer Form erzeugt wird. Wir sagen, dass eine Menge A von Tasks unabhängig ist, wenn ein Ablaufplan für diese Tasks existiert, in dem keine Tasks zu spät sind. Es ist klar, dass die Menge der zu frühen Tasks eines Ablaufplans eine unabhängige Menge von Tasks bildet. Wir bezeichnen mit I die Menge aller unabhängigen Mengen von Tasks. Betrachten Sie das Problem, zu entscheiden, ob eine gegebene Menge A von Tasks unabhängig ist. Für t = 0, 1, 2, . . . , n bezeichne Nt (A) die Anzahl von Tasks in A, deren Termine gleich t oder früher sind. Beachten Sie, dass N0 (A) = 0 für jede Menge A gilt. Lemma 16.12 Für eine beliebige Menge A von Tasks sind die folgenden Aussagen äquivalent. 1. Die Menge A ist unabhängig. 2. Für t = 0, 1, 2, . . . , n gilt Nt (A) ≤ t. 3. Wenn die Tasks in A in der Reihenfolge monoton steigender Termine geplant werden, dann ist kein Task zu spät. Beweis: Wenn für ein t die Gleichung Nt (A) > t gilt, dann gibt es keine Möglichkeit, für die Menge A einen Ablaufplan ohne zu späte Tasks zu erstellen, da nicht mehr als t Tasks vor der Zeit t abgearbeitet werden können. Deshalb folgt aus (1) die Aussage (2). Wenn (2) erfüllt ist, dann muss (3) folgen. Es gibt keine Möglichkeit „hängen zu bleiben“, wenn wir die Tasks in monoton steigender Reihenfolge nach den Terminen planen, da aus (2) folgt, dass der i-größte Termin höchstens i ist. Schließlich impliziert (3) trivialerweise (1). Verwenden wir Eigenschaft 2 aus Lemma 16.12, so können wir einfach feststellen, ob eine gegebene Menge von Tasks unabhängig ist oder nicht (siehe Übung 16.5-2). Das Problem, die Summe von Strafen für die zu späten Tasks zu minimieren, entspricht dem Problem, die Summe der Strafen für die zu frühen Tasks zu maximieren. Das folgende Theorem sichert somit, dass wir einen Greedy-Algorithmus verwenden können, um eine unabhängige Menge A von Tasks mit maximaler Gesamtstrafe zu berechnen. Theorem 16.13 Wenn S eine Menge von Unit-Time-Tasks mit Terminen und I die Menge aller unabhängigen Mengen von Tasks ist, dann ist das zugehörige System (S, I) ein Matroid.
16.5 ∗ Ein Task-Scheduling-Problem als Matroid
ai di wi
1 4 70
2 2 60
3 4 50
Task 4 3 40
5 1 30
6 4 20
449
7 6 10
Abbildung 16.7: Eine Instanz für das Problem der Ablaufplanung von Unit-Time-Tasks mit Terminen und Strafen bei einem einzelnen Prozessor.
Beweis: Jede Teilmenge einer unabhängigen Menge ist zweifellos unabhängig. Um die Austauscheigenschaft zu beweisen, setzen wir voraus, dass B und A unabhängige Mengen von Tasks sind und dass |B| > |A| gilt. Sei k das größte t, für das Nt (B) ≤ Nt (A) erfüllt ist. (Ein solcher Wert t existiert, da N0 (A) = N0 (B) = 0 gilt.) Da Nn (B) = |B| und Nn (A) = |A| erfüllt sind, aber |B| > |A| gilt, muss k < n und Nj (B) > Nj (A) für alle j im Bereich k + 1 ≤ j ≤ n gelten. Deshalb enthält B mehr Tasks mit Termin k + 1 als A. Sei ai ein Task in B − A mit Termin k + 1. Sei A = A ∪ {ai }. Wir zeigen nun, dass A unabhängig sein muss, indem wir Eigenschaft 2 aus Lemma 16.12 verwenden. Für 0 ≤ t ≤ k gilt Nt (A ) = Nt (A) ≤ t, da A unabhängig ist. Für k < t ≤ n gilt Nt (A ) ≤ Nt (B) ≤ t, da B unabhängig ist. Deshalb ist A unabhängig, womit der Beweis, dass (S, I) ein Matroid ist, abgeschlossen ist. Nach Theorem 16.11 können wir einen Greedy-Algorithmus verwenden, um eine unabhängige Menge A von Tasks mit maximalem Gewicht zu bestimmen. Wir können dann einen optimalen Ablaufplan erstellen, der die Tasks in A als die zu frühen Tasks enthält. Diese Methode stellt einen effizienten Algorithmus zur Ablaufplanung von Unit-TimeTasks mit Terminen und Strafen bei einem einzelnen Prozessor dar. Verwenden wir Greedy, liegt die Laufzeit in O(n2 ), da jeder der vom Algorithmus durchgeführten O(n) Tests auf Unabhängigkeit Zeit O(n) benötigt (siehe Übung 16.5-2). Problemstellung 16-4 gibt eine schnellere Implementierung an. Abbildung 16.7 zeigt ein Beispiel für die Ablaufplanung von Unit-Time-Tasks mit Terminen und Strafen bei einem einzelnen Prozessor. In diesem Beispiel wählt der GreedyAlgorithmus die Tasks a1 , a2 , a3 und a4 aus, verwirft a5 und a6 , und akzeptiert schließlich a7 . Der endgültige optimale Ablaufplan lautet a2 , a4 , a1 , a3 , a7 , a5 , a6 , und führt zu einer Gesamtstrafe von w5 + w6 = 50.
Übungen 16.5-1 Lösen Sie die Instanz des in Abbildung 16.7 dargestellten Task-SchedulingProblems für den Fall, dass jede Strafe wi durch 80 − wi ersetzt wird. 16.5-2 Zeigen Sie, wie Eigenschaft 2 aus Lemma 16.12 zu verwenden ist, um in Zeit O(|A|) zu entscheiden, ob eine gegebene Menge A von Tasks unabhängig ist.
450
16 Greedy-Algorithmen
Problemstellungen 16-1 Münzwechsel Betrachten Sie das Problem, Wechselgeld für n Cent so zusammenzustellen, dass so wenige Münzen wie möglich verwendet werden. Setzen Sie voraus, dass der Nennwert jeder Münze eine ganze Zahl ist. a. Geben Sie einen Greedy-Algorithmus an, der das Wechselgeld aus Quartern, Dimes, Nickels und Pennies 5 zusammenstellt. Beweisen Sie, dass Ihr Algorithmus zu einer optimalen Lösung führt. b. Nehmen Sie an, dass die verfügbaren Münzen Nennwerte haben, die Potenzen von c sind, also c0 , c1 , . . . , ck für eine ganze Zahl c > 1 und k ≥ 1. Zeigen Sie, dass der Greedy-Algorithmus immer zu einer optimalen Lösung führt. c. Geben Sie eine Menge von Münznennwerten an, für die ein Greedy-Algorithmus zu keiner optimalen Lösung führt. Ihre Menge sollte einen Penny enthalten, sodass es für jeden Wert von n eine Lösung gibt. d. Geben Sie einen Algorithmus in Zeit O(nk) an, der das Wechselgeld für eine beliebige Menge von k verschiedenen Münzwerten unter der Voraussetzung erstellt, dass einer von ihnen ein Penny ist. 16-2 Ablaufplanung zum Minimieren der mittleren Fertigstellungszeit Gegeben sei eine Menge S = {a1 , a2 , . . . , an } von Tasks, wobei Task ai nach dem Start pi Einheiten der Prozesszeit benötigt, um abgearbeitet zu werden. Sie haben einen Rechner, auf dem diese Tasks laufen sollen, und der Rechner kann nur einen Task zu jeder Zeit ausführen. Sei ci die Fertigstellungszeit des Tasks von ai endet. Ihr Ziel ist es, die mittlere ai , d. h. die Zeit, zu der die Bearbeitung n Fertigstellungszeit, also (1/n) i=1 ci , zu minimieren. Nehmen wir beispielsweise an, dass es zwei Tasks a1 und a2 mit p1 = 3 und p2 = 5 gibt. Wir betrachten den Ablaufplan, bei dem zuerst a2 und dann a1 läuft. Dann gilt c2 = 5, c1 = 8, und die mittlere Fertigstellungszeit beträgt (5 + 8)/2 = 6, 5. Wenn jedoch zuerst Task a1 ausgeführt wird, dann gilt c1 = 3, c2 = 8 und die mittlere Fertigungszeit ist (3 + 8)/2 = 5, 5. a. Geben Sie einen Algorithmus an, der die Tasks so plant, dass die mittlere Fertigstellungszeit minimiert wird. Jeder Task muss ohne Unterbrechung laufen, d. h. wenn ein Task ai gestartet wurde, muss dieser kontinuierlich für pi Zeiteinheiten laufen. Beweisen Sie, dass Ihr Algorithmus die mittlere Fertigstellungszeit minimiert und geben Sie die Laufzeit ihres Algorithmus an. b. Nehmen Sie nun an, dass nicht alle Tasks auf einmal verfügbar wären. Das heißt, dass jeder Task eine Freigabezeit ri besitzt, vor der er nicht ausgeführt werden kann. Setzen Sie außerdem voraus, dass wir Unterbrechungen erlauben, dass also ein Task unterbrochen und zu einem späteren Zeitpunkt fortgeführt werden kann. Beispielsweise könnte ein Task ai mit der Bearbeitungszeit pi = 6 und der Freigabezeit ri = 1 zum Zeitpunkt 1 starten und zum 5 Anmerkung
der Übersetzer: Die Nennwerte dieser Münzen sind 25, 10, 5 und 1 Cent.
Problemstellungen zu Kapitel 16
451
Zeitpunkt 4 unterbrochen werden. Er könnte dann zum Zeitpunkt 10 fortgesetzt werden und zum Zeitpunkt 11 erneut unterbrochen werden. Schließlich könnte er zum Zeitpunkt 13 wieder fortgeführt werden und zum Zeitpunkt 15 fertig abgearbeitet sein. Task ai wäre in diesem Szenario insgesamt sechs Zeiteinheiten gelaufen, aber seine Laufzeit wäre in drei Teile zerlegt worden. Die Fertigstellungszeit von Task ai ist 15. Geben Sie einen Algorithmus an, der die Tasks so plant, dass die mittlere Fertigstellungszeit unter diesen neuen Umständen minimiert wird. Beweisen Sie, dass Ihr Algorithmus die mittlere Fertigstellungszeit minimiert, und geben Sie die Laufzeit Ihres Algorithmus an. 16-3 Azyklische Teilgraphen a. Die Inzidenzmatrix eines ungerichteten Graphen G = (V, E) ist eine |V | × |E|-Matrix M mit Mve = 1, falls die Kante e mit dem Knoten v inzidiert, und Mve = 0 sonst. Zeigen Sie, dass eine Spaltenmenge M genau dann linear unabhängig über dem Körper der ganzen Zahlen modulo 2 ist, wenn die zugehörige Kantenmenge azyklisch ist. b. Setzen Sie voraus, dass wir jeder Kante eines ungerichteten Graphen G = (V, E) ein nichtnegatives Gewicht w(e) zuordnen. Geben Sie einen effizienten Algorithmus an, der eine azyklische Teilmenge von E mit maximalem Gesamtgewicht bestimmt. c. Sei G(V, E) ein beliebiger gerichteter Graph, und sei (E, I) so definiert, dass A ∈ I genau dann gilt, wenn A keine gerichteten Zyklen enthält. Geben Sie ein Beispiel für einen gerichteten Graphen G an, für den das zugehörige System (E, I) kein Matroid ist. Welche Eigenschaft der Definition eines Matroids ist nicht erfüllt? d. Die Inzidenzmatrix eines gerichteten Graphen ohne Schlingen G = (V, E) ist eine |V | × |E|-Matrix M mit Mve = −1, falls die Kante e aus dem Knoten v austritt, Mve = 1, falls die Kante e in den Knoten v eintritt, und Mve = 0 sonst. Zeigen Sie, dass die Kantenmenge keinen gerichteten Zyklus enthält, wenn die entsprechende Spaltenmenge von M linear unabhängig ist. e. Übung 16.4-2 besagt, dass die Menge linear unabhängiger Spaltenmengen einer Matrix M ein Matroid bildet. Erklären Sie ausführlich, warum die Resultate aus Teil (c) und Teil (d) nicht widersprüchlich sind. Weshalb kann es vorkommen, dass keine perfekte Korrespondenz zwischen den beiden Aussagen besteht, dass eine Kantenmenge azyklisch ist und dass die zugehörige Spaltenmenge der Inzidenzmatrix linear unabhängig ist? 16-4 Variationen der Ablaufplanung Betrachten Sie den folgenden Algorithmus für das Problem der Ablaufplanung von Unit-Time-Tasks mit Terminen und Strafen aus Abschnitt 16.5. Zu Beginn seien alle n Zeitslots leer, wobei Zeitslot i die Zeitnische für Tasks, die zur Zeit i enden, ist. Wir betrachten die Tasks in monoton fallender Reihenfolge nach ihren Strafgebühren. Wenn Sie Task aj betrachten und wenn es einen Zeitslot zu aj ’s Termin oder früher gibt, der noch leer ist, weisen Sie aj den letzten dieser Slots
452
16 Greedy-Algorithmen zu. Wenn es keinen solchen Slot gibt, dann weisen Sie Task aj dem letzten der bis dahin noch freien Slots zu. a. Zeigen Sie, dass dieser Algorithmus immer eine optimale Antwort liefert. b. Benutzen Sie den in Abschnitt 21.3 vorgestellten Wald disjunkter Mengen, um den Algorithmus effizient zu implementieren. Setzen Sie voraus, dass die Eingabetasks bereits monoton fallend nach ihren Strafgebühren sortiert sind. Analysieren Sie die Laufzeit Ihrer Implementierung.
16-5 Offline-Caching Moderne Rechner benutzen einen Zwischenspeicher (engl.: cache), um eine kleine Menge von Daten in einem schnellen Speicher zu halten. Wenn auch ein Programm auf größere Mengen von Daten zugreift, kann man die Gesamtzugriffszeit erheblich verringern, indem man eine kleine Teilmenge des Hauptspeichers in dem Cache – ein kleiner, aber schneller Speicher – speichert. Wenn ein Computerprogramm ausgeführt wird, dann führt es eine Folge r1 , r2 , . . . , rn von n Speicheranfragen durch, wobei sich jede Anfrage auf ein spezielles Datenelement bezieht. Beispielsweise könnte ein Programm, das auf 4 verschiedene Elemente {a, b, c, d} zugreift, die Folge d, b, d, b, d, a, c, d, b, a, c, b von Anfragen haben. Sei k die Größe des Caches. Wenn der Cache k Elemente enthält und das Programm fragt das (k + 1)-te Element an, dann muss das System für diese und jede folgende Anfrage entscheiden, welche k Elemente im Cache verbleiben. Genauer, die Cache-Verwaltung überprüft für jede Anfrage ri , ob ri bereits im Cache ist. Wenn dies der Fall ist, dann haben wir einen Cache-Treffer (engl.: cache hit ); anderenfalls haben wir einen Cache-Fehlzugriff (engl.: cache miss). Bei einem Cache-Fehlzugriff holt das System ri aus dem Hauptspeicher und die Cache-Verwaltung muss entscheiden, ob ri im Cache gehalten wird. Wenn sie sich entscheidet, ri im Cache zu halten, und der Cache bereits k Elemente enthält, dann muss sie ein Element aus dem Cache hinauswerfen, um Platz für ri zu machen. Der der Cache-Verwaltung zugrunde liegende Algorithmus entfernt Daten aus dem Speicher mit dem Ziel, die Anzahl der Cache-Fehlzugriffe über die gesamte Anfragefolge zu minimieren. Typischerweise ist Caching ein online-Problem, d. h. wir haben zu entscheiden, welche Daten wir im Cache behalten wollen, ohne die in der Zukunft liegenden Anfragen zu kennen. Hier wollen wir aber die offline-Version dieses Problems betrachten, in der wir im Voraus die gesamte Folge der n Anfragen und die Größe k des Caches kennen und wir die Gesamtanzahl der Cache-Fehlzugriffe minimieren wollen. Wir können dieses offline-Problem durch eine Greedy-Strategie lösen, die als die am-weitesten-in-der-Zukunft-Strategie bekannt ist und das Element aus dem Cache entfernt, dessen nächster Zugriff am weitesten in der Zukunft liegt. a. Geben Sie den Pseudocode für einen Cache-Verwalter an, der die am-weitestenin-der-Zukunft-Strategie benutzt. Die Eingabe sollte eine Folge r1 , r2 , . . . , rn von Anfragen und eine Cache-Größe k sein und die Ausgabe sollte die Folge von Datenelementen sein, die bei den Anfragen durch den Cache-Verwalter aus dem Cache entfernt werden. Welche Laufzeit hat Ihr Algorithmus?
Kapitelbemerkungen zu Kapitel 16
453
b. Zeigen Sie, dass das offline-Problem die optimale-Teilstruktur-Eigenschaft besitzt. c. Zeigen Sie, dass die am-weitesten-in-der-Zukunft-Strategie minimal viele CacheFehlzugriffe produziert.
Kapitelbemerkungen Mehr zu Greedy-Algorithmen und Matroiden finden Sie in Lawler [224] und Papadimitriou und Steiglitz [271]. Der Greedy-Algorithmus tauchte erstmals 1971 in der Literatur zur kombinatorischen Optimierung in einem Artikel von Edmonds [101] auf, obwohl die Theorie der Matroide bereits auf einen Artikel von Whitney [355] aus dem Jahr 1935 zurückgeht. Unser Korrektheitsbeweis des Greedy-Algorithmus für das Aktivitäten-Auswahl-Problem basiert auf dem Beweis von Gavril [131]. Das Task-Scheduling-Problem wurde von Lawler [224], Horowitz, Sahni und Rajasekaran [181] sowie Brassard und Bratley [54] untersucht. Huffman-Codierungen wurden 1952 entwickelt [185]; Lelewer und Hirschberg [231] betrachteten Datenkomprimierungstechniken, wie sie seit 1987 bekannt sind. Die Erweiterung der Matroid-Theorie zur Greedoid-Theorie erfolgte durch Korte und Lovász [216, 217, 218, 219], die die hier dargestellte Theorie stark verallgemeinern.
17
Amortisierte Analyse
Bei einer amortisierten Analyse mitteln wir die Zeit, die benötigt wird, um eine Folge von Datenstrukturoperationen auszuführen, über alle ausgeführten Operationen. Mit amortisierter Analyse können wir zeigen, dass die mittleren Kosten einer Operation klein sind, wenn wir über eine Folge von Operationen mitteln, auch wenn eine einzelne Operation in der Folge möglicherweise teuer ist. Die amortisierte Analyse unterscheidet sich von der Analyse des mittleren Falls darin, dass keine Wahrscheinlichkeiten einbezogen werden. Eine amortisierte Analyse garantiert die mittlere Performanz jeder Operation im schlechtesten Fall. Die ersten drei Abschnitte dieses Kapitels behandeln die drei häufigsten Techniken, die bei der amortisierten Analyse verwendet werden. Abschnitt 17.1 beginnt mit der Aggregat-Analyse, bei der wir eine obere Schranke T (n) für die Gesamtkosten einer Folge von n Operationen bestimmen. Die mittleren Kosten pro Operation betragen dann T (n)/n. Wir betrachten die mittleren Kosten als die amortisierten Kosten jeder Operation, sodass alle Operationen dieselben amortisierten Kosten besitzen. Abschnitt 17.2 beschäftigt sich mit der Account-Methode, bei der wir die amortisierten Kosten jeder Operation bestimmen. Wenn es mehr als einen Operationentyp gibt, kann jeder Operationentyp andere amortisierte Kosten besitzen. Die Account-Methode überschätzt die Kosten einiger Operationen zu Beginn der Folge, wobei sie den Überschuss als „vorausbezahlten Kredit“ in spezifischen Objekten der Datenstruktur speichert. Später in der Folge wird der Kredit verwendet, um für Operationen zu zahlen, für die weniger berechnet wurde, als sie tatsächlich kosten. Abschnitt 17.3 diskutiert die Potentialmethode, die der Account-Methode ähnlich ist. Hier bestimmen wir die amortisierten Kosten jeder Operation und können die Kosten früherer Operationen überschätzen, um später damit die Kosten unterschätzter Operationen zu kompensieren. Die Potentialmethode verwaltet den Kredit als Potentialenergie der Datenstruktur als Ganzes, anstatt den Kredit mit individuellen Objekten innerhalb der Datenstruktur zu verbinden. Wir werden diese drei Methoden an zwei Beispielen illustrieren. Ein Beispiel ist ein Stapel mit der zusätzlichen Operation Multipop, die mehrere Objekte in einem Schritt vom Stapel nimmt. Das andere Beispiel ist der Binärzähler, der mithilfe der Operation Increment von 0 beginnend hochzählt. Behalten Sie beim Lesen dieses Kapitels im Gedächtnis, dass die bei der amortisierten Analyse zugewiesenen Kosten nur Analysezwecken dienen. Sie brauchen und sollten im Code nicht auftauchen. Wenn wir beispielsweise bei der Verwendung der AccountMethode einem Objekt x einen Kredit zuweisen, dann gibt es für uns keine Notwendigkeit, im Code einem Attribut x.credit den entsprechenden Wert zuzuweisen.
456
17 Amortisierte Analyse
Wenn wir eine amortisierte Analyse durchführen, erhalten wir oft Einsicht in eine spezielle Datenstruktur; diese Kenntnis kann uns helfen, einen Entwurf zu optimieren. Im Abschnitt 17.4 werden wir zum Beispiel die Potentialmethode verwenden, um eine dynamisch erweiterbare und kontrahierbare Tabelle zu analysieren.
17.1
Aggregat-Analyse
Bei der Aggregat-Analyse zeigen wir, dass eine Sequenz von n Operationen für alle n im schlechtesten Fall insgesamt Zeit T (n) benötigt. Im schlechtesten Fall betragen deshalb die mittleren oder amortisierten Kosten pro Operation T (n)/n. Beachten Sie, dass diese amortisierten Kosten für jede Operation gelten, auch wenn es verschiedene Operationentypen in der Sequenz gibt. Die anderen beiden Methoden, die wir in diesem Kapitel untersuchen werden, die Account-Methode und die Potentialmethode, weisen verschiedenen Operationentypen verschiedene amortisierte Kosten zu.
Stapeloperationen In unserem ersten Beispiel zur Aggregat-Analyse untersuchen wir Stapel, die um eine neue Operation erweitert wurden. Abschnitt 10.1 stellte zwei fundamentale Stapeloperationen vor, von denen jede Zeit O(1) benötigt: Push(S, x) legt das Objekt x auf den Stapel S. Pop(S) entnimmt das oberste Element von S und gibt das entnommene Objekt zurück. Pop angewendet auf einen leeren Stapel generiert einen Fehler. Da jede dieser Operationen in Zeit O(1) läuft, nehmen wir deren Kosten jeweils als 1 an. Die Gesamtkosten einer Sequenz von n Push- und Pop-Operationen betragen deshalb n, und die tatsächliche Laufzeit für n Operationen ergibt sich zu Θ(n). Wir fügen nun eine Stapeloperation Multipop(S, k) ein, die die k obersten Objekte vom Stapel entfernt beziehungsweise den gesamten Stapel leert, wenn dieser weniger als k Objekte besitzt. Wir setzen natürlich voraus, dass k positiv ist; anderenfalls verändert die Operation Multipop den Stapel nicht. Im folgenden Pseudocode gibt die Operation Stack-Empty den Wert wahr zurück, wenn sich aktuell keine Objekte mehr im Stapel befinden. Anderenfalls wird der Wert falsch zurückgegeben. Multipop(S, k) 1 while nicht Stack-Empty(S) und k > 0 2 Pop(S) 3 k = k−1 Abbildung 17.1 illustriert die Arbeitsweise von Multipop. Wie groß ist die Laufzeit von Multipop(S, k) auf einem Stapel S mit s Objekten? Da die tatsächliche Laufzeit linear in der Anzahl der tatsächlich ausgeführten PopOperationen ist, können wir die Prozedur Multipop unter Verwendung der abstrakten
17.1 Aggregat-Analyse top
23 17 6 39 10 47 (a)
top
457
10 47 (b)
(c)
Abbildung 17.1: Die Arbeitsweise von Multipop auf dem Stapel S, dessen Anfangszustand in Teil (a) gezeigt ist. Die obersten vier Objekte werden von Multipop(S, 4) entnommen, das Resultat wird in Teil (b) gezeigt. (c) Die nächste Operation lautet Multipop(S, 7). Diese leert den Stapel, da weniger als sieben Objekte verblieben waren.
Kosten von 1 für jede Push- und Pop-Operation analysieren. Die Anzahl der Iterationen der while-Schleife ergibt sich aus der Anzahl min(s, k) der aus dem Stapel entnommenen Objekte. Jede Iteration der Schleife ruft in Zeile 2 einmal Pop auf. Somit betragen die Gesamtkosten von Multipop min(s, k) und die tatsächliche Laufzeit ist eine lineare Funktion dieser Kosten. Lassen Sie uns eine Sequenz von n Push-, Pop- und Multipop-Operationen auf einem anfangs leeren Stapel analysieren. Für die Kosten der Multipop-Operation ergibt sich im schlechtesten Fall O(n), da die Stapelgröße höchstens n beträgt. Die Zeit für jede Stapeloperation im schlechtesten Fall ist deshalb in O(n). Daher kostet eine Sequenz von n Operationen Zeit O(n2 ), da O(n) Multipop-Operationen möglich sind, von denen jede O(n) kostet. Wenngleich diese Analyse korrekt ist, so ist dieses O(n2 )-Resultat, das wir erhalten haben, indem wir die Kosten im schlechtesten Fall einer jeden Operation betrachtet haben, nicht scharf. Mithilfe der Aggregat-Analyse können wir zu einer besseren oberen Schranke kommen, die die Gesamtsequenz von n Operationen betrachtet. Tatsächlich kann eine beliebige Folge von n Push-, Pop- und Multipop-Operationen auf einem anfangs leeren Stapel höchstens O(n) kosten, auch wenn eine einzelne Multipop-Operation teuer sein kann. Weshalb? Wir können jedes Objekt nur so oft vom Stapel entfernen, wie wir es auf den Stapel gelegt haben. Deshalb ist die Anzahl der Pop-Aufrufe auf einem nichtleeren Stapel, einschließlich aller Multipop-Aufrufe, höchstens gleich der Anzahl der Push-Operationen, die höchstens gleich n ist. Für jeden Wert n benötigt demnach eine beliebige Sequenz von n Push-, Pop- und Multipop-Operationen eine Gesamtzeit von O(n). Die mittleren Kosten einer Operation betragen O(n)/n = O(1). Bei der Aggregat-Analyse nehmen wir als mittlere Kosten die amortisierten Kosten einer jeden Operation. Deshalb haben in diesem Beispiel alle drei Stapeloperationen amortisierte Kosten von O(1). Wir betonen nochmals, dass wir keine wahrscheinlichkeitstheoretische Argumentation benutzt haben, obwohl wir gerade gezeigt haben, dass die mittleren Kosten, und damit die Laufzeit, einer Stapeloperation O(1) beträgt. Wir haben tatsächlich eine Schranke im schlechtesten Fall von O(n) auf einer Sequenz von n Operationen bewiesen. Dividieren wir diese Gesamtkosten durch n, führt dies zu mittleren Kosten pro Operation oder den amortisierten Kosten.
458
17 Amortisierte Analyse
Inkrementieren eines Binärzählers Als ein weiteres Beispiel für die Aggregat-Analyse betrachten Sie das Problem, einen k-Bit-Binärzähler zu implementieren, der von 0 an aufwärts zählt. Wir verwenden als Zähler ein Feld A[0 . . k − 1] von Bits, wobei A.l¨a nge = k gilt. Eine Binärzahl x, die im Zähler gespeichert wird, hat das niederwertigste Bit in A[0] und das höchstwertigste in k−1 A[k − 1], sodass x = i=0 A[i] · 2i gilt. Zu Beginn ist x = 0, und somit gilt A[i] = 0 für i = 0, 1, . . . , k − 1. Um zu dem Wert im Zähler eins (modulo 2k ) zu addieren, verwenden wir die folgende Prozedur.
Increment(A) 1 i=0 2 while i < A.l¨a nge und A[i] = = 1 3 A[i] = 0 4 i = i+1 5 if i < A.l¨a nge 6 A[i] = 1
Abbildung 17.2 zeigt, was passiert, wenn wir startend beim Anfangswert 0 einen binären Zähler 16-mal inkrementieren, sodass wir anschließend den Wert 16 im Zähler gespeichert haben. Zu Beginn jeder Iteration der while-Schleife in den Zeilen 2–4 wollen wir an Position i eine 1 addieren. Wenn A[i] = 1 gilt, dann kippt die Addition von 1 das Bit an Position i auf 0 und generiert einen Übertrag von 1, welcher in der nächsten Iteration der Schleife der Postion i + 1 hinzugefügt werden muss. Anderenfalls endet die Schleife und, wenn i < k gilt, dann wissen wir, dass A[i] = 0 ist, und Zeile 6 addiert eine 1 an Position i, indem es die 0 durch eine 1 ersetzt. Die Kosten jeder Increment-Operation sind linear in der Anzahl der zu kippenden Bits. Wie im Beispiel des Stapels führt eine oberflächliche Analyse zu einer Schranke, die zwar korrekt, aber nicht scharf ist. Eine einfache Ausführung von Increment benötigt im schlechtesten Fall Zeit Θ(k). In diesem Fall enthält das Feld überall den Wert 1. Somit kann eine Sequenz von n Increment-Operationen auf einem anfangs auf Null gesetzten Zähler im schlechtesten Fall Zeit O(nk) benötigen. Wir können unsere Analyse genauer durchführen, um so nachzuweisen, dass eine Sequenz von n Increment-Operationen Kosten O(n) hat. Hierzu verwenden wir die Tatsache, dass nicht jedes Mal alle Bits kippen, wenn Increment aufgerufen wird. Wie Abbildung 17.2 zeigt, kippt A[0] bei jedem Aufruf von Increment. Das nächsthöhere Bit A[1] kippt nur in jedem zweiten Schritt: Eine Sequenz von n Increment-Operationen auf einem anfangs auf Null gesetzten Zähler verursacht, dass A[1] n/2-mal kippt. Entsprechend kippt Bit A[2] nur in jedem vierten Schritt, d. h. n/4-mal in einer Sequenz von ni Increment-Aufrufen. Allgemein gilt, dass Bit A[i] für i = 0, 1, . . . , k − 1 genau n/2 -mal kippt, wenn wir eine Sequenz von n Increment-Operationen auf einen anfangs auf Null gesetzten Zähler anwenden. Für i ≥ k existiert Bit A[i] nicht und so kann es auch nicht kippen. Die Gesamtzahl, wie oft Bits in der Sequenz gekippt werden, ist
Zählerwert 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
459
A[ 7 A[ ] 6] A[ 5 A[ ] 4 A[ ] 3] A[ 2 A[ ] 1] A[ 0]
17.1 Aggregat-Analyse
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1
0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0
0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1 0
0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1 0
0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0
Gesamtkosten 0 1 3 4 7 8 10 11 15 16 18 19 22 23 25 26 31
Abbildung 17.2: Ein 8-Bit-Binärzähler, dessen Wert durch eine Sequenz von 16 IncrementOperationen von 0 auf 16 ansteigt. Bits, die zum Generieren des nächsten Wertes gekippt werden, sind schattiert. Die fortlaufenden Kosten für das Kippen der Bits sind rechts dargestellt. Beachten Sie, dass die Gesamtkosten immer kleiner sind als zweimal die Gesamtzahl der Increment-Operationen.
folglich nach Gleichung (A.6) k−1 i=0
∞ n 1 < n i 2i 2 i=0
= 2n . Die Laufzeit für eine Sequenz von n Increment-Operationen auf einem anfangs auf Null gesetzten Zähler ist deshalb im schlechtesten Fall O(n). Die mittleren Kosten jeder Operation, und damit die amortisierten Kosten pro Operation, sind O(n)/n = O(1).
Übungen 17.1-1 Gilt die O(1)-Schranke für die amortisierten Kosten von Stapeloperationen weiterhin, wenn die Menge der Stapeloperationen eine Multipush-Operation enthält, die k Elemente auf dem Stapel ablegt? 17.1-2 Zeigen Sie, dass, wenn in dem k-Bit-Zähler aus obigem Beispiel noch eine Decrement-Operation zur Verfügung stehen würde, n Operationen etwa Zeit Θ(nk) benötigen würden. 17.1-3 Nehmen Sie an, wir würden eine Sequenz von n Operationen auf einer Datenstruktur ausführen, in der die i-te Operation Kosten i hat, wenn i eine
460
17 Amortisierte Analyse Zweierpotenz ist, und 1, sonst. Verwenden Sie die Aggregat-Analyse, um die amortisierten Kosten pro Operation zu bestimmen.
17.2
Account-Methode
Bei der Account-Methode der amortisierten Analyse weisen wir verschiedenen Operationen unterschiedliche Kosten zu, wobei wir für einige der Operationen höhere oder niedrigere Kosten ansetzen, als sie in Wirklichkeit haben. Wir nennen die Beträge, die wir für eine Operation ansetzen, ihre amortisierten Kosten. Wenn die amortisierten Kosten einer Operation deren tatsächlichen Kosten übersteigen, dann weisen wir die Differenz den einzelnen Objekten in der Datenstruktur als Kredit zu. Der Kredit kann später beim Bezahlen späterer Operationen helfen, deren amortisierte Kosten kleiner als ihre tatsächlichen Kosten sind. Wir können also die amortisierten Kosten einer Operation so interpretieren, als wären sie in tatsächliche Kosten und einen Kredit aufgeteilt, der entweder hinterlegt oder aufgebraucht wird. Verschiedene Operationen können unterschiedliche amortisierte Kosten haben. Diese Methode unterscheidet sich stark von der Aggregat-Analyse, bei der alle Operationen dieselben amortisierten Kosten besitzen. Wir müssen die amortisierten Kosten der Operationen mit Bedacht wählen. Wenn wir zeigen wollen, dass im schlechtesten Fall die mittleren Kosten pro Operation klein sind, indem wir amortisierte Kosten analysieren, dann müssen wir sicherstellen, dass die amortisierten Gesamtkosten einer Sequenz von Operationen eine obere Schranke für die tatsächlichen Gesamtkosten dieser Sequenz darstellen. Zudem muss diese Beziehung wie in der Aggregat-Analyse für alle Sequenzen von Operationen gelten. Wenn wir die tatsächlichen Kosten der i-ten Operation mit ci bezeichnen und die amortisieren Kosten der i-ten Operation mit ci , so verlangen wir für alle Sequenzen von n Operationen n i=1
ci ≥
n
ci .
(17.1)
i=1
n n Der in der Datenstruktur gespeicherte Gesamtkredit ist die Differenz i=1 ci − i=1 ci zwischen den amortisierten Gesamtkosten und den tatsächlichen Gesamtkosten. Wegen Ungleichung (17.1) muss der zur Datenstruktur gehörende Kredit jederzeit nichtnegativ sein. Wenn der Gesamtkredit jemals negativ werden könnte (das Resultat aus der Unterschätzung der Kosten früherer Operationen mit dem Versprechen, diesen Betrag später zurückzuzahlen), dann wären die zu diesem Moment übernommenen amortisierten Gesamtkosten kleiner als die tatsächlichen Gesamtkosten. Somit wären die amortisierten Gesamtkosten keine obere Schranke für die tatsächlichen Gesamtkosten. Deshalb müssen wir sicherstellen, dass der Gesamtkredit in der Datenstruktur niemals negativ wird.
Stapeloperationen Um die Account-Methode für die amortisierte Analyse zu illustrieren, schauen wir uns nochmals unsere Beispiel mit den Stapeloperationen an. Erinnern Sie sich daran, dass die tatsächlichen Kosten unserer Operationen
17.2 Account-Methode Push Pop Multipop
461
1, 1, min(k, s)
waren, wobei k das an Multipop übergebene Argument und s die Stapelgröße beim Aufruf ist. Lassen Sie uns den Operationen die folgenden amortisierten Kosten zuweisen: Push Pop Multipop
2, 0, 0.
Beachten Sie, dass die amortisierten Kosten von Multipop konstant (nämlich 0) sind, obwohl die tatsächlichen Kosten variabel sind. Hier sind alle drei amortisierten Kosten in O(1). Allgemein gilt, dass die amortisierten Kosten der betrachteten Operationen sich voneinander unterscheiden können und sich sogar asymptotisch unterscheiden können. Wir werden nun zeigen, dass wir für jede Sequenz von Stapeloperationen bezahlen können, indem wir die amortisierten Kosten verrechnen. Nehmen Sie an, dass wir einen Dollarschein verwenden würden, um die Kosteneinheiten darzustellen. Wir beginnen mit einem leeren Stapel. Erinnern Sie sich an die Analogie aus Abschnitt 10.1 zwischen der Datenstruktur eines Stapels und einem Tellerstapel. Wenn wir einen Teller auf den Stapel legen, dann geben wir einen Dollar, um die tatsächlichen Kosten des Ablegens zu bezahlen, und haben einen Kredit von einem Dollar (aus den zwei berechneten Dollar) übrig, den wir oben auf den Stapel legen. Zu jeder Zeit befindet sich auf jedem Teller des Stapels ein Dollar als Kredit. Der auf dem Teller abgelegte Dollar ist eine Vorauszahlung für die Kosten, einen Teller vom Stapel zu nehmen. Wenn wir eine Pop-Operation ausführen, berechnen wir für die Operation nichts und bezahlen die tatsächlichen Kosten mithilfe des auf dem Stapel gespeicherten Kredits. Um einen Teller wegzunehmen, nehmen wir den Dollar Kredit vom Teller und verwenden ihn, um die tatsächlichen Kosten der Operation zu bezahlen. Folglich müssen wir für die Pop-Operation keine amortisierten Kosten ansetzen, weil wir für die Push-Operation etwas mehr bezahlen. Darüber hinaus brauchen wir ebenfalls für die Multipop-Operationen keine amortisierten Kosten anzusetzen. Um den ersten Teller wegzunehmen, nehmen wir den Dollar Kredit vom Teller und verwenden ihn, um die tatsächlichen Kosten der Pop-Operation zu bezahlen. Um einen zweiten Teller wegzunehmen, haben wir wiederum einen Dollar Kredit auf dem Teller, um für die Pop-Operation zu bezahlen, usw. Somit haben wir immer genug für die Push-Operationen im Voraus berechnet, um für die Multipop-Operationen bezahlen zu können. Wir haben also sichergestellt, dass der Betrag des Kredits immer nichtnegativ ist, weil sich auf jedem Teller des Stapels ein Dollar Kredit befindet, und der Stapel immer eine nichtnegative Anzahl von Tellern enthält. Somit bilden die amortisierten Kosten für eine beliebige Sequenz von n Push-, Popund Multipop-Operationen eine obere Schranke für die tatsächlichen Kosten. Da die amortisierten Gesamtkosten in O(n) sind, sind es auch die tatsächlichen Gesamtkosten.
Inkrementieren eines Binärzählers Zur weiteren Illustration der Account-Methode analysieren wir die Increment-Operation auf einem Binärzähler, der bei 0 startet. Wir haben bereits festgestellt, dass die
462
17 Amortisierte Analyse
Laufzeit dieser Operation proportional zur Anzahl der gekippten Bits ist, die wir als Kosten in diesem Beispiel ansehen wollen. Wir benutzen wiederum einen Dollarschein, um jede Kosteneinheit darzustellen (in diesem Beispiel das Kippen eines Bits). Bei der amortisierten Analyse wollen wir die amortisierten Kosten von zwei Dollar berechnen, um ein Bit auf 1 zu setzen. Wenn ein Bit gesetzt wurde, verwenden wir einen Dollar (aus den zwei berechneten Dollar), um für das tatsächliche Setzen des Bits zu bezahlen und platzieren den anderen Dollar auf diesem Bit, der später verwendet werden kann, wenn wir das Bit wieder auf 0 zurücksetzen. Zu jedem Zeitpunkt hat jede 1 im Zähler einen Dollar Kredit, und somit müssen wir für das Rücksetzen des Bits auf 0 kein weiteres Geld vorsehen; wir bezahlen einfach das Rücksetzen mit dem Dollarschein auf dem Bit. Nun können wir die amortisierten Kosten von Increment bestimmen. Die Kosten für das Rücksetzen der Bits in der while-Schleife werden von den Dollars, die auf den zurückzusetzenden Bits liegen, bezahlt. Die Prozedur Increment setzt in Zeile 6 jeweils immer höchstens ein Bit, und deshalb betragen die amortiserten Kosten einer Increment-Operation höchstens zwei Dollar. Die Anzahl der auf 1 gesetzten Bits im Zähler ist niemals negativ, weshalb der Betrag des Kredites zu jedem Moment nichtnegativ bleibt. Somit sind die amortisierten Gesamtkosten für n Increment-Operationen in O(n), was die tatsächlichen Gesamtkosten beschränkt.
Übungen 17.2-1 Nehmen Sie an, wir würden eine Sequenz von Stapeloperationen auf einem Stapel ausführen, dessen Größe niemals den Wert k übersteigen würde. Nach jeweils k Operationen erstellen wir zu Backup-Zwecken eine Kopie des gesamten Stapels. Zeigen Sie, dass die Kosten von n Stapeloperationen, einschließlich des Kopierens des Stapels, in O(n) liegen, indem sie den verschiedenen Stapeloperationen geeignete amortisierte Kosten zuweisen. 17.2-2 Lösen Sie Übung 17.1-3 erneut. Benutzen Sie diesmal eine Account-Methode. 17.2-3 Nehmen Sie an, wir wollten nicht nur einen Zähler inkrementieren, sondern auch auf 0 zurücksetzen können (d. h. alle seine Bits auf 0 setzen). Zeigen Sie, wie ein Zähler so als Bitfeld implementiert werden kann, dass eine beliebige Sequenz von Increment und Reset-Operationen auf einem anfangs auf 0 gesetzten Zähler Zeit O(n) benötigt, wenn die Zeit, um ein Bit zu überprüfen oder zu modifizieren, mit O(1) in die Gesamtlaufzeit eingeht. (Hinweis: Lassen Sie einen Zeiger auf das höchstwertigste 1-Bit zeigen.)
17.3
Die Potentialmethode
Anstatt vorausbezahlte Arbeit als Kredit darzustellen, der in speziellen Objekten der Datenstruktur gespeichert wird, stellt die Potentialmethode der amortisierten Analyse die vorausbezahlte Arbeit als „potentielle Energie“ oder einfach „Potential“ dar, das
17.3 Die Potentialmethode
463
freigegeben werden kann, um zukünftige Operationen zu bezahlen. Wir verbinden das Potential mit der Datenstruktur als Ganzes anstatt mit speziellen Objekten innerhalb der Datenstruktur. Die Potentialmethode arbeitet wie folgt. Wir wollen n Operationen ausführen, wobei wir mit einer initialen Datenstruktur D0 starten. Für jedes i = 1, 2, . . . , n seien ci die tatsächlichen Kosten der i-ten Operation und Di die Datenstruktur, die sich nach Anwendung der i-ten Operation auf die Datenstruktur Di−1 ergibt. Eine Potentialfunktion Φ bildet jede Datenstruktur Di auf eine reelle Zahl Φ(Di ) ab, die für das Potential der Datenstruktur Di steht. Die amortisierten Kosten ci der i-ten Operation sind in Bezug auf die Potentialfunktion Φ durch ci = ci + Φ(Di ) − Φ(Di−1 )
(17.2)
definiert. Die amortisierten Kosten jeder Operation ergeben sich somit aus deren tatsächlichen Kosten zuzüglich des Zuwachses im Potential aufgrund der Operation. Mit Gleichung (17.2) sind die amortisierten Gesamtkosten von n Operationen n
ci =
i=1
=
n i=1 n
(ci + Φ(Di ) − Φ(Di−1 )) ci + Φ(Dn ) − Φ(D0 ) .
(17.3)
i=1
Das zweite Gleichheitszeichen folgt aus Gleichung (A.9), da sich die Terme Φ(Di ) gegenseitig aufheben. Wenn wir eine Potentialfunktion Φ definieren n können, für die Φ(Dn ) ≥ Φ(D0 ) gilt, dann ci eine obere Schranke für die tatsächlichen bilden die amortisierten Gesamtkosten i=1 n Gesamtkosten i=1 ci . In der Praxis wissen wir nicht immer, wie viele Operationen ausgeführt werden. Wenn wir deshalb fordern, dass Φ(Di ) ≥ Φ(D0 ) für alle i gilt, dann ist wie bei der Account-Methode sichergestellt, dass wir im Voraus bezahlen. Wir definieren üblicherweise Φ(D0 ) einfach nur als 0 und zeigen dann, dass Φ(Di ) ≥ 0 für alle i gilt. (Übung 17.3-1 stellt eine einfache Möglichkeit vor, die Fälle, in denen Φ(D0 ) = 0 gilt, zu behandeln.) Offenbar stellen die amortisierten Kosten ci eine Überschätzung der i-ten Operation dar, wenn die Potentialdifferenz Φ(Di ) − Φ(Di−1 ) positiv ist. Das Potential der Datenstruktur erhöht sich somit. Wenn die Potentialdifferenz negativ ist, dann stellen die amortisierten Kosten eine Unterschätzung der i-ten Operation dar, und die tatsächlichen Kosten der Operation werden über eine Verringerung des Potentials bezahlt. Die durch die Gleichungen (17.2) und (17.3) definierten amortisierten Kosten hängen von der Wahl der Potentialfunktion Φ ab. Verschiedene Potentialfunktionen können zu verschiedenen amortisierten Kosten führen, die aber alle obere Schranken für die tatsächlichen Kosten sind. Wir finden öfters Kompromisse, die wir bei der Wahl der Potentialfunktion machen können; die am besten geeignete Potentialfunktion hängt jeweils von den gewünschten Zeitschranken ab.
464
17 Amortisierte Analyse
Stapeloperationen Um die Potentialmethode zu illustrieren, schauen wir uns nochmals das Beispiel mit den Stapeloperationen Push, Pop und Multipop an. Wir definieren die Potentialfunktion Φ auf einem Stapel als die Anzahl der Objekte im Stapel. Für den leeren Stapel D0 , mit dem wir starten, gilt Φ(D0 ) = 0. Da die Anzahl der Objekte im Stapel niemals negativ ist, besitzt der sich nach der i-ten Operation ergebende Stapel ein nichtnegatives Potential, und es gilt Φ(Di ) ≥ 0 = Φ(D0 ) . Die amortisierten Gesamtkosten von n Operationen in Bezug auf Φ stellen deshalb eine obere Schranke für die tatsächlichen Gesamtkosten dar. Lassen Sie uns nun die amortisierten Kosten der verschiedenen Stapeloperationen berechnen. Wenn die i-te Operation auf einem Stapel mit s Objekten eine Push-Operation ist, dann ergibt sich für die Potentialdifferenz Φ(Di ) − Φ(Di−1 ) = (s + 1) − s =1. Mit Gleichung (17.2) betragen die amortisierten Kosten dieser Push-Operation ci = ci + Φ(Di ) − Φ(Di−1 ) =1+1 =2. Nehmen Sie an, die i-te Operation auf dem Stapel wäre die Operation Multipop(S, k), die k = min(k, s) Objekte vom Stapel entnimmt. Die tatsächlichen Kosten der Operation betragen k und die Potentialdifferenz ist Φ(Di ) − Φ(Di−1 ) = −k . Somit sind die amortisierten Kosten der Multipop-Operation ci = ci + Φ(Di ) − Φ(Di−1 ) = k − k =0. Analog dazu sind die amortisierten Kosten einer einfachen Pop-Operation gleich 0. Die amortisierten Kosten jeder der drei Operationen sind in O(1), sodass die gesamten amortisierten Kosten einer Sequenz von n Operationen in O(n) liegen. Da wir bereits gezeigt haben, dass Φ(Di ) ≥ Φ(D0 ) gilt, bilden die amortisierten Gesamtkosten von n Operationen eine obere Schranke für die tatsächlichen Gesamtkosten. Die Kosten im schlechtesten Fall von n Operationen sind deshalb O(n).
17.3 Die Potentialmethode
465
Inkrementieren eines Binärzählers Als weiteres Beispiel für die Potentialmethode schauen wir uns nochmals das Inkrementieren eines Binärzählers an. Diesmal definieren wir das Potential des Zählers nach der i-ten Increment-Operation als die Anzahl bi der Einser im Zähler nach der i-ten Operation. Lassen Sie uns die amortisierten Kosten einer Increment-Operation berechnen. Nehmen Sie an, dass die i-te Increment-Operation ti Bits zurücksetzen würde. Die tatsächlichen Kosten der Operation sind deshalb höchstens ti +1, da zusätzlich zum Rücksetzen der ti Bits höchstens ein Bit auf 1 gesetzt wird. Im Falle bi = 0 setzt die i-te Operation alle k Bits zurück, und es gilt bi−1 = ti = k. Wenn bi > 0 ist, dann gilt bi = bi−1 − ti + 1. In beiden Fällen gilt bi ≤ bi−1 − ti + 1 und die Potentialdifferenz ist Φ(Di ) − Φ(Di−1 ) ≤ (bi−1 − ti + 1) − bi−1 = 1 − ti . Die amortisierten Kosten betragen deshalb ci = ci + Φ(Di ) − Φ(Di−1 ) ≤ (ti + 1) + (1 − ti ) =2. Wenn der Zähler bei Null startet, dann gilt Φ(D0 ) = 0. Da für alle i die Beziehung Φ(Di ) ≥ 0 erfüllt ist, bilden die amortisierten Gesamtkosten einer Sequenz von n Increment-Operationen eine obere Schranke für die tatsächlichen Gesamtkosten. Somit betragen die Kosten von n Increment-Operationen im schlechtesten Fall O(n). Die Potentialmethode bietet uns eine einfache Möglichkeit, den Zähler auch dann zu untersuchen, wenn dieser nicht bei Null startet. Der Zähler startet mit b0 Einsen und nach n Increment-Operationen hat er bn Einsen, wobei 0 ≤ b0 , bn ≤ k gilt. (Erinnern Sie sich, dass k die Anzahl der Bits im Zähler angibt.) Wir können Gleichung (17.3) zu n
ci =
i=1
n
ci − Φ(Dn ) + Φ(D0 )
(17.4)
i=1
umformen. Für alle i mit 1 ≤ i ≤ n gilt die Gleichung ci ≤ 2. Wegen Φ(D0 ) = b0 und Φ(Dn ) = bn betragen die tatsächlichen Gesamtkosten von n Increment-Operationen n i=1
ci ≤
n
2 − bn + b0
i=1
= 2n − bn + b0 . Beachten Sie insbesondere, dass die tatsächlichen Gesamtkosten wegen b0 ≤ k in O(n) liegen, solange k = O(n) gilt. Mit anderen Worten, wenn wir mindestens n = Ω(k) Increment-Operationen ausführen, dann sind die tatsächlichen Gesamtkosten in O(n), unabhängig davon, welchen Anfangswert der Zähler enthält.
466
17 Amortisierte Analyse
Übungen 17.3-1 Nehmen Sie an, wir hätten eine Potentialfunktion Φ mit Φ(Di ) ≥ Φ(D0 ) für alle i gegeben und es würde Φ(D0 ) = 0 gelten. Zeigen Sie, dass eine Potentialfunktion Φ existiert, für die Φ (D0 ) = 0, Φ (Di ) ≥ 0 für alle i ≥ 1 gilt. Zeigen Sie außerdem, dass die amortisierten Kosten unter Verwendung von Φ gleich den amortisierten Kosten unter Verwendung von Φ sind. 17.3-2 Lösen Sie Übung 17.1-3 noch einmal. Benutzen Sie diesmal die Potentialmethode. 17.3-3 Betrachten Sie eine gewöhnliche binäre Min-Heap-Datenstruktur mit n Elementen, die die Befehle Insert und Extract-Min im schlechtesten Fall in Zeit O(lg n) unterstützt. Geben Sie eine Potentialfunktion Φ an, für die die amortisierten Kosten von Insert in O(lg n) und die amortisierten Kosten von Extract-Min in O(1) sind, und zeigen Sie, dass das funktioniert. 17.3-4 Wie hoch sind die Gesamtkosten für die Ausführung von n Stapeloperationen Push, Pop und Multipop unter der Voraussetzung, dass der Stapel anfangs s0 Objekte und am Ende sn Objekte enthält? 17.3-5 Nehmen Sie an, ein Zähler würde nicht bei der Null sondern bei einer Zahl, in deren binärer Darstellung b Bits den Wert 1 haben, starten. Zeigen Sie, dass die Kosten für das Ausführen von n Increment-Operationen O(n) betragen, wenn n = Ω(b) gilt. (Setzen Sie nicht voraus, dass b konstant ist.) 17.3-6 Zeigen Sie, wie eine Warteschlange durch zwei gewöhnliche Stapel so implementiert werden kann (Übung 10.1-6), dass die amortisierten Kosten jeder Enqueue- und jeder Dequeue-Operation in O(1) sind. 17.3-7 Entwerfen Sie eine Datenstruktur, die die zwei folgenden Operationen für eine dynamische Multimenge S (siehe Anhang B) von ganzen Zahlen unterstützt: Insert(S, x) fügt x in S ein. Delete-Larger-Half(S) löscht die |S| /2 größten Elemente aus S. Erklären Sie, wie diese Datenstruktur so implementiert werden kann, dass jede beliebige Sequenz von m Insert- und Delete-Larger-Half-Operationen in Zeit O(m) läuft. Ihre Implementierung sollte auch eine Möglichkeit vorsehen, die Elemente aus S in Zeit O(|S|) auszugeben.
17.4
Dynamische Tabellen
In einigen Anwendungen wissen wir nicht im Voraus, wie viele Elemente in einer Tabelle zu speichern sind. Es kann sein, dass wir Platz für die Tabelle im Speicher allokieren, nur um später festzustellen, dass der Platz nicht ausreicht. Die Tabelle muss dann mit einem größeren Speicherplatz erneut allokiert werden, und alle ursprünglich in der Tabelle gespeicherten Objekte müssen in die neue, größere Tabelle kopiert werden. Analog dazu
17.4 Dynamische Tabellen
467
kann es sinnvoll sein, die Tabelle mit einem kleineren Speicherplatz zu allokieren, wenn viele Elemente aus der Tabelle gelöscht wurden. In diesem Abschnitt untersuchen wir, wie eine Tabelle dynamisch erweitert und kontrahiert werden kann. Unter Verwendung der amortisierten Analyse werden wir zeigen, dass die amortisierten Kosten für das Einfügen und das Löschen nur O(1) sind, auch wenn die tatsächlichen Kosten einer Operation groß sind, wenn sie eine Expansion oder Kontraktion auslöst. Darüber hinaus werden wir sehen, wie wir sicherstellen können, dass der in einer dynamischen Tabelle unbenutzte Speicherplatz niemals einen konstanten Anteil des gesamten Speicherplatzes überschreitet. Wir setzen voraus, dass die dynamische Tabelle die Operationen Table-Insert und Table-Delete unterstützt. Table-Insert fügt einen Eintrag in die Tabelle ein, der einen einzelnen Slot belegt, d. h. pro Speicherplatz gibt es einen Eintrag. Ebenso löscht Table-Delete einen Eintrag aus der Tabelle, sodass dann dieser Slot für einen neuen Eintrag verfügbar ist. Die Details der datenstrukturierenden Methoden, die zur Organisation der Tabelle verwendet werden, sind unwichtig. Wir können einen Stapel (Abschnitt 10.1), einen Heap (Kapitel 6) oder eine Hashtabelle (Kapitel 11) verwenden. Wir könnten auch ein Feld oder eine Menge von Feldern benutzen, um den Objektspeicher zu implementieren, wie wir es in Abschnitt 10.3 getan haben. Es ist zweckmäßig, dass wir ein Konzept benutzen, das wir bei der Analyse des Hashing eingeführt haben (Kapitel 11). Wir definieren den Belegungsfaktor α(T ) einer nichtleeren Tabelle T als die Anzahl der in der Tabelle gespeicherten Einträge dividiert durch die Größe (Anzahl der Slots) der Tabelle. Wir weisen einer leeren Tabelle (einer Tabelle ohne Slots) die Größe 0 zu und definieren ihren Belegungsfaktor als 1. Wenn der Belegungsfaktor einer dynamischen Tabelle von unten durch eine Konstante beschränkt ist, nimmt der in der Tabelle unbenutzte Platz niemals mehr als einen konstanten Anteil des Gesamtspeicherplatzes ein. Wir beginnen mit der Analyse einer dynamischen Tabelle, in die wir nur Elemente einfügen. Danach betrachten wir den allgemeineren Fall, in dem wir Elemente sowohl einfügen als auch löschen.
17.4.1
Tabellenexpansion
Lassen Sie uns voraussetzen, dass der Speicher für eine Tabelle als ein Feld von Speicherplätzen allokiert ist. Eine Tabelle ist voll, wenn alle Slots belegt sind, oder anders formuliert, wenn deren Belegungsfaktor den Wert 1 hat.1 Bei einigen Softwareumgebungen gibt es keine andere Alternative, als mit einem Fehler abzubrechen, wenn der Versuch unternommen wird, einen Eintrag in einer bereits gefüllten Tabelle vorzunehmen. Wir werden jedoch voraussetzen, dass unsere Softwareumgebung ein Speichermanagementsystem bereitstellt, das bei Bedarf freie Speicherblöcke allokieren kann (was bei vielen modernen Softwareumgebungen der Fall ist). Somit können wir, wenn ein Eintrag in eine volle Tabelle eingefügt werden soll, die Tabelle expandieren, indem wir eine Tabelle allokieren, die mehr Slots als die alte Tabelle besitzt. Da sich die Tabelle immer in 1 In einigen Situationen, wie beispielsweise bei einer Hashtabelle mit offener Adressierung, kann es von Vorteil sein, eine Tabelle als voll anzusehen, wenn deren Belegungsfaktor gleich einer Konstanten ist, die echt kleiner als 1 ist (siehe Übung 17.4-1).
468
17 Amortisierte Analyse
einem zusammenhängenden Speicherbereich befinden muss, müssen wir ein neues Feld für die größere Tabelle allokieren und anschließend die Einträge aus der alten Tabelle in die neue Tabelle kopieren. Eine verbreitete heuristische Methode allokiert eine neue Tabelle, die doppelt soviele Slots wie die alte besitzt. Wenn nur Einfügeoperationen ausgeführt werden, beträgt der Belegungsfaktor stets mindestens 1/2. Damit übersteigt der Anteil des ungenutzten Speicherplatzes niemals die Hälfte des Gesamtspeicherplatzes in der Tabelle. Im folgenden Pseudocode setzen wir voraus, dass T ein Objekt ist, welches die Tabelle darstellt. Das Attribut T.tabelle enthält einen Zeiger auf den Speicherblock, der die Tabelle verkörpert. Das Attribut T.anz enthält die Anzahl der Tabelleneinträge und das Attribut T.gr¨o ß e gibt die Gesamtzahl der Tabellenslots an. Zu Beginn ist die Tabelle leer; es gilt also T.anz = T.gr¨o ß e = 0. Table-Insert(T, x) 1 if T.gr¨o ß e = = 0 2 allokiere T.tabelle mit einem Slot 3 T.gr¨o ß e = 1 4 if T.anz = = T.gr¨o ß e 5 allokiere neue-Tabelle mit 2 · T.gr¨o ß e vielen Slots 6 füge alle Elemente von T.tabelle in neue-Tabelle 7 gib T.tabelle frei 8 T.tabelle = neue-Tabelle 9 T.gr¨o ß e = 2 · T.gr¨o ß e 10 füge x in T.tabelle ein 11 T.anz = T.anz + 1 Beachten Sie, dass es hier zwei „Einfüge“-Prozeduren gibt: die Prozedur Table-Insert selbst und das elementare Einfügen in eine Tabelle in den Zeilen 6 und 10. Wir können die Laufzeit von Table-Insert in Abhängigkeit von der Anzahl elementarer Einfügeoperationen analysieren, indem wir jedem elementaren Einfügen die Kosten 1 zuweisen. Wir setzen voraus, dass die tatsächliche Laufzeit von Table-Insert linear in der Zeit für das Einfügen der einzelnen Einträge ist, sodass die Gesamtkosten für das Allokieren der Ausgangstabelle in Zeile 2 konstant sind, und die Gesamtkosten für das Allokieren und Freigeben des Speichers in den Zeilen 5 und 7 von den Kosten für das Übertragen der Einträge in Zeile 6 dominiert werden. Wir bezeichnen das Ereignis, dass die Zeilen 5–9 ausgeführt werden, als Expansion. Wir wollen nun eine Sequenz von n Table-Insert-Operationen auf einer ursprünglich leeren Tabelle analysieren. Wie hoch sind die Kosten ci der i-ten Operation? Wenn die aktuelle Tabelle Platz für das neue Element hat (oder wenn dies die erste Operation ist), dann gilt ci = 1, da wir nur eine elementare Einfügeoperation in Zeile 10 ausführen müssen. Wenn die aktuelle Tabelle jedoch voll ist, und die Expansion stattfindet, dann gilt ci = i: Die Kosten sind 1 für das elementare Einfügen in Zeile 10 plus i − 1 für die Einträge, die wir in Zeile 6 aus den alten Tabelle in die neue Tabelle kopieren müssen. Wenn n Operationen ausgeführt werden, dann betragen die Kosten einer Operation im
17.4 Dynamische Tabellen
469
schlechtesten Fall O(n), was zu einer oberen Schranke von O(n2 ) für die Gesamtlaufzeit von n Operationen führt. Diese Schranke ist nicht scharf, da wir selten die Tabelle im Verlauf von n TableInsert-Operationen kopieren müssen. Insbesondere hat die i-te Operation nur dann eine Expansion zur Folge, wenn i − 1 eine Zweierpotenz ist. Die amortisierten Kosten einer Operation betragen tatsächlich O(1), was wir mithilfe der Aggregat-Analyse zeigen können. Die Kosten der i-ten Operation sind i wenn i − 1 eine Zweierpotenz ist ci = 1 sonst . Die Gesamtkosten von n Table-Insert-Operationen betragen deshalb n i=1
lg n
ci ≤ n +
2j
j=0
< n + 2n = 3n , da höchstens n Operationen Kosten 1 haben, und die Kosten der verbleibenden Operationen eine geometrische Reihe bilden. Da die Gesamtkosten von n Table-InsertOperationen nach oben durch den Wert 3n beschränkt sind, sind die amortisierten Kosten einer einzelnen Operation höchstens gleich 3. Mithilfe der Account-Methode können wir ein gewisses Gefühl dafür bekommen, warum die amortisierten Kosten einer Table-Insert-Operationen 3 sein sollten. Offenbar bezahlt jeder Eintrag für drei elementare Einfüge-Operationen: das Einfügen des Eintrages in die aktuelle Tabelle, das Kopieren des Eintrages, wenn die Tabelle expandiert wird, sowie das Kopieren eines anderen Eintrages der Tabelle, der bereits einmal verschoben wurde, als die Tabelle expandiert wurde. Nehmen Sie an, die Größe der Tabelle gleich nach einer Expansion wäre gleich m. Dann enthält die Tabelle m/2 Einträge, und die Tabelle enthält keinen Kredit. Wir setzen für jedes Einfügen drei Dollar an. Das elementare Einfügen kostet sofort einen Dollar. Wir platzieren einen anderen Dollar als Kredit auf das eingefügte Element und den dritten Dollar als Kredit auf eines der m/2 Elemente, die bereits in der Tabelle enthalten sind. Die Tabelle wird nicht voll sein, bevor wir nicht weitere m/2 − 1 Elemente eingefügt haben. Zu dem Zeitpunkt, an dem die Tabelle m Einträge enthält und voll ist, haben wir einen Dollar auf jedes Element der Tabelle platziert, um das Kopieren während einer Expansion bezahlen zu können. Wir können die Potentialmethode verwenden, um eine Sequenz von n Table-InsertOperationen zu analysieren. Wir werden sie im Abschnitt 17.4.2 verwenden, um eine Table-Delete-Operation zu entwerfen, die ebenfalls die amortisierten Kosten von O(1) hat. Wir definieren zunächst eine Potentialfunktion Φ, die unmittelbar nach der Expansion den Wert 0 hat, sich aber in der Zeit, bis die Tabelle voll ist, aufbaut, sodass wir mit dem Potential die nächste Expansion bezahlen können. Die Funktion Φ(T ) = 2 · T.anz − T.gr¨o ß e
(17.5)
470
17 Amortisierte Analyse
ist eine Möglichkeit. Unmittelbar nach einer Expansion gilt T.anz = T.gr¨o ß e/2 und somit wie gewünscht Φ(T ) = 0. Unmittelbar vor einer Expansion gilt T.anz = T.gr¨o ß e und somit wie gewünscht Φ(T ) = T.anz . Der Anfangswert des Potentials ist 0, und da die Tabelle stets mindestens halbvoll ist, gilt T.anz ≥ T.gr¨o ß e/2, was impliziert, dass Φ(T ) immer nichtnegativ ist. Somit liefert die Summe der amortisierten Kosten für n Table-Insert-Operationen eine obere Schranke für die Summe der tatsächlichen Kosten. Um die amortisierten Kosten der i-ten Table-Insert-Operation zu analysieren, bezeichnen wir die Anzahl der nach der i-ten Operation in der Tabelle gespeicherten Einträge mit anz i . Mit gr¨o ß e i bezeichnen wir die Gesamtgröße der Tabelle nach der i-ten Operation und mit Φi das Potential nach der i-ten Operation. Zu Beginn gilt anz 0 = 0, gr¨o ß e 0 = 0 und Φ0 = 0. Wenn die i-te Table-Insert-Operation keine Expansion auslöst, dann ist gr¨o ß e i = gr¨o ß e i−1 und die amortisierten Kosten der Operation sind ci = ci + Φi − Φi−1 = 1 + (2 · anz i − gr¨o ß e i ) − (2 · anz i−1 − gr¨o ß e i−1 ) = 1 + (2 · anz i − gr¨o ß e i ) − (2(anz i − 1) − gr¨o ß e i ) =3. Wenn dagegen die i-te Operation eine Expansion auslöst, dann ist gr¨o ß e i = 2·gr¨o ß e i−1 und gr¨o ß e i−1 = anz i−1 = anz i − 1, was gr¨o ß e i = 2 · (anz i − 1) impliziert. Damit betragen die amortisierten Kosten der Operation ci = ci + Φi − Φi−1 = anz i + (2 · anz i − gr¨o ß e i ) − (2 · anz i−1 − gr¨o ß e i−1 ) = anz i + (2 · anz i − 2 · (anz i − 1)) − (2(anz i − 1) − (anz i − 1)) = anz i + 2 − (anz i − 1) =3. Abbildung 17.3 stellt die Werte von anz i , gr¨o ß e i und Φi graphisch über i dar. Beachten Sie, wie sich das Potential aufbaut, um für die Expansion der Tabelle bezahlen zu können.
17.4.2
Expansion und Kontraktion einer Tabelle
Eine Table-Delete-Operation können wir so implementieren, dass wir den spezifischen Eintrag einfach aus der Tabelle entfernen. Um den ungenutzten Speicherplatz nicht übermäßig groß werden zu lassen, wollen wir möglicherweise jedoch die Tabelle kontrahieren, wenn der Belegungsfaktor zu klein geworden ist. Die Kontraktion einer Tabelle erfolgt analog zur Expansion einer Tabelle: Wenn die Anzahl der Einträge in der Tabelle zu gering wird, allokieren wir eine neue, kleinere Tabelle und kopieren dann die Einträge aus der alten Tabelle in die neue. Wir können den Speicherplatz der alten Tabelle dann freigeben, indem wir ihn in die Verwaltung des Speichermanagementsystems zurückgeben. Idealerweise sollten zwei Eigenschaften erhalten bleiben:
17.4 Dynamische Tabellen
471
32
größei
24
16
anz
i
Φi
8
0
i 0
8
16
24
32
Abbildung 17.3: Die Auswirkungen einer Sequenz von n Table-Insert-Operationen auf die o ß e i der Speicherplätze in der Tabelle und Anzahl anz i der Tabelleneinträge, die Anzahl gr¨ o ß e i , jeweils nach der i-ten Operation gemessen. Die dünne das Potential Φi = 2 · anz i − gr¨ o ß e i und die dicke Linie zeigt Φi . Beachten Sie, Linie zeigt anz i , die gestrichelte Linie zeigt gr¨ dass sich das Potential unmittelbar vor einer Expansion bis zur Anzahl der Tabelleneinträge aufgebaut hat und deshalb für das Kopieren aller Einträge in die neue Tabelle bezahlen kann. Danach fällt es auf 0, wird aber sofort um 2 erhöht, wenn der Eintrag, der die Expansion verursacht hat, eingefügt wird.
• Der Belegungsfaktor der dynamischen Tabelle ist nach unten durch eine positive Konstante beschränkt, und • die amortisierten Kosten einer Tabellenoperation sind nach oben durch eine Konstante beschränkt. Wir setzen voraus, dass wir die Kosten über die Anzahl der elementaren Operationen Einfügen und Löschen messen. Sie denken möglicherweise, dass wir die Tabellengröße verdoppeln sollten, wenn wir ein Element in eine volle Tabelle einfügen, und halbieren sollten, wenn ein Löschen eines Elementes dazu führen würde, dass die Tabelle weniger als halbvoll ist. Diese Strategie würde sicherstellen, dass der Belegungsfaktor der Tabelle niemals unter 1/2 fällt. Leider kann sie aber ziemlich große amortisierte Kosten verursachen. Betrachten Sie die folgende Situation. Wir führen n Operationen auf der Tabelle T aus, wobei n eine Potenz von 2 ist. Die ersten n/2 Operationen sind Einfügeoperationen, die nach unserer vorangegangenen Analyse insgesamt Θ(n) kosten. Am Ende dieser Sequenz von Einfügeoperationen gilt T.anz = T.gr¨o ß e = n/2. In den nächsten n/2 Operationen führen wir die folgende Sequenz aus: einfügen, löschen, löschen, einfügen, einfügen, löschen, löschen, einfügen, einfügen, . . . .
472
17 Amortisierte Analyse
Das erste Einfügen bewirkt eine Expansion der Tabelle auf die Größe n. Die beiden folgenden Löschoperationen bewirken eine Kontraktion der Tabelle zurück auf die Größe n/2. Zwei weitere Einfügeoperationen bewirken eine weitere Expansion usw. Die Kosten jeder Expansion und Kontraktion betragen Θ(n) und es gibt Θ(n) von ihnen. Somit ergibt sich für die Gesamtkosten von n Operationen Θ(n2 ), und die amortisierten Kosten einer Operation sind Θ(n). Der Haken an dieser Strategie ist offensichtlich: Nach dem Expandieren der Tabelle führen wir nicht genug Löschoperationen aus, um für die Kontraktion bezahlen zu können. Entsprechend führen wir nach der Kontraktion der Tabelle nicht genug Einfügeoperationen aus, um für die Expansion bezahlen zu können. Wir können diese Strategie verbessern, indem wir es zulassen, dass der Belegungsfaktor der Tabelle unter 1/2 fällt. Insbesondere verdoppeln wir die Tabellengröße weiterhin, wenn ein Eintrag in die volle Tabelle vorgenommen werden soll, halbieren die Tabellengröße jedoch nur dann, wenn die Tabelle durch eine Löschoperation zu weniger als 1/4 gefüllt ist, anstatt wie bisher zu weniger als 1/2 gefüllt ist. Der Belegungsfaktor der Tabelle ist somit von unten durch die Konstante 1/4 beschränkt. An sich würden wir den Belegungsfaktor von 1/2 als ideal ansehen; das Potential der Tabelle wäre bei diesem Belegungsfaktor gleich 0. Wenn der Belegungsfaktor von 1/2 abweicht, dann wächst das Potential in dem Maße, dass wenn wir die Tabelle expandieren oder halbieren, die Tabelle ausreichend Potential gesammelt hat, um die Elemente in die neu allokierte Tabelle zu kopieren. Somit werden wir eine Potentialfunktion benötigen, die zu T.anz angewachsen ist, wenn der Belegungsfaktor zu 1 oder zu 1/4 geworden ist. Nachdem die Tabelle expandiert oder kontrahiert worden ist, reduziert sich das Potential der Tabelle wieder auf 0. Wir übergehen den Code für Table-Delete, da er analog zu Table-Insert ist. Für unsere Analyse, wollen wir voraussetzen, dass wir den Speicherplatz der Tabelle freigeben, wenn immer die Anzahl der Elemente in der Tabelle zu 0 wird. Im Falle T.anz = 0 gilt also T.gr¨o ß e = 0. Wir können nun die Potentialmethode benutzen, um die Kosten einer Sequenz von n Table-Insert- und Table-Delete-Operationen zu analysieren. Wir beginnen mit der Definition einer Potentialfunktion Φ, die unmittelbar nach der Expansion oder Kontraktion den Wert 0 hat und sich aufbaut, bis der Belegungsfaktor zu 1 oder zu 1/4 wird. Lassen Sie uns den Belegungsfaktor einer nichtleeren Tabelle T mit α(T ) = T.anz /T.gr¨o ß e bezeichnen. Da für eine leere Tabelle T.anz = T.gr¨o ß e = 0 und α[T ] = 1 gelten, ist stets T.anz = α(T ) · T.gr¨o ß e unabhängig davon, ob die Tabelle leer ist oder nicht. Wir werden als Potentialfunktion < 2 · T.anz − T.gr¨o ß e falls α(T ) ≥ 1/2 , Φ(T ) = (17.6) T.gr¨o ß e/2 − T.anz falls α(T ) < 1/2 benutzen. Beachten Sie, dass das Potential einer leeren Tabelle 0 ist und dass das Potential niemals negativ ist. Somit bilden die amortisierten Gesamtkosten einer Sequenz von Operationen bezüglich Φ eine obere Schranke für die tatsächlichen Kosten der Sequenz. Bevor wir mit einer genauen Analyse fortfahren, wollen wir einige Eigenschaften der Potentialfunktion festhalten, die in Abbildung 17.4 illustriert werden. Beachten Sie,
17.4 Dynamische Tabellen
473
32
24
größei
16
anz
i
8 Φi 0
0
8
16
24
32
40
48
i
Abbildung 17.4: Die Auswirkungen einer Sequenz von n Table-Insert- und Tableo ß e i der SpeiDelete-Operationen auf die Anzahl anz i der Tabelleneinträge, die Anzahl gr¨ cherplätze und das Potential 2 · anz i − gr¨ o ß e i falls αi ≥ 1/2 , Φi = falls αi < 1/2 , gr¨ o ß e i /2 − anz i jeweils nach der i-ten Operation gemessen. Die dünne Linie zeigt anz i , die gestrichelte Linie zeigt gr¨ o ß e i und die dicke Linie zeigt Φi . Beachten Sie, dass sich das Potential unmittelbar vor einer Expansion bis zur Anzahl der Tabelleneinträge aufgebaut hat und deshalb für das Kopieren aller Einträge in die neue Tabelle bezahlen kann. Entsprechend hat sich das Potential unmittelbar vor einer Kontraktion bis zur Anzahl der Tabelleneinträge aufgebaut.
dass das Potential 0 ist, wenn der Belegungsfaktor den Wert 1/2 hat. Wenn der Belegungsfaktor den Wert 1 hat, gilt T.gr¨o ß e = T.anz , was Φ(T ) = T.anz impliziert. Somit kann das Potential für eine Expansion bezahlen, wenn ein Eintrag eingefügt wird. Wenn der Belegungsfaktor den Wert 1/4 hat, gilt T.gr¨o ß e = 4 · T.anz , was Φ(T ) = T.anz impliziert. Somit kann das Potential für eine Kontraktion bezahlen, wenn ein Eintrag gelöscht wird. Um eine Sequenz von n Table-Insert- und Table-Delete-Operationen zu analysieren, bezeichnen wir die tatsächlichen Kosten der i-ten Operation mit ci , deren amortisierte Kosten in Bezug auf Φ mit ci , die Anzahl der in der Tabelle nach der i-ten Operation gespeicherten Einträge mit anz i , die Gesamtgröße der Tabelle nach der iten Operation mit gr¨o ß e i , den Belegungsfaktor der Tabelle nach der i-ten Operation mit αi und das Potential nach der i-ten Operation mit Φi . Zu Beginn gilt anz 0 = 0, gr¨o ß e 0 = 0, α0 = 1 und Φ0 = 0. Wir beginnen mit dem Fall, dass die i-te Operation Table-Insert ist. Die Analyse ist zu der für die Expansion einer Tabelle in Abschnitt 17.4.1 durchgeführten Analyse identisch, wenn αi−1 ≥ 1/2 gilt. Ob die Tabelle expandiert wird oder nicht, die amortisierten Kosten ci einer Operation sind höchstens 3. Im Falle αi−1 < 1/2 kann die
474
17 Amortisierte Analyse
Tabelle nicht als Resultat der Operation expandiert werden, da eine Expansion nur im Falle αi−1 = 1 auftritt. Wenn auch αi < 1/2 gilt, dann betragen die amortisierten Kosten der i-ten Operation ci = ci + Φi − Φi−1 = 1 + (gr¨o ß e i /2 − anz i ) − (gr¨o ß e i−1 /2 − anz i−1 ) = 1 + (gr¨o ß e i /2 − anz i ) − (gr¨o ß e i /2 − (anz i − 1)) =0. Für αi−1 < 1/2 und αi ≥ 1/2 gilt ci = ci + Φi − Φi−1 = 1 + (2 · anz i − gr¨o ß e i ) − (gr¨o ß e i−1 /2 − anz i−1 ) = 1 + (2(anz i−1 + 1) − gr¨o ß e i−1 ) − (gr¨o ß e i−1 /2 − anz i−1 ) 3 = 3 · anz i−1 − gr¨o ß e i−1 + 3 2 3 = 3αi−1 gr¨o ß e i−1 − gr¨o ß e i−1 + 3 2 3 3 < gr¨o ß e i−1 − gr¨o ß e i−1 + 3 2 2 =3. Damit sind die amortisierten Kosten einer Table-Insert-Operation höchstens gleich 3. Wir wenden uns nun dem Fall zu, dass die i-te Operation Table-Delete ist. In diesem Fall gilt anz i = anz i−1 − 1. Für αi−1 < 1/2 müssen wir überlegen, ob die Operation eine Kontraktion verursacht. Wenn sie dies nicht tut, dann gilt gr¨o ß e i = gr¨o ß e i−1 und die amortisierten Kosten der Operation sind ci = ci + Φi − Φi−1 = 1 + (gr¨o ß e i /2 − anz i ) − (gr¨o ß e i−1 /2 − anz i−1 ) = 1 + (gr¨o ß e i /2 − anz i ) − (gr¨o ß e i /2 − (anz i + 1)) =2. Wenn αi−1 < 1/2 gilt und die i-te Operation eine Kontraktion auslöst, dann sind die tatsächlichen Kosten der Operation ci = anz i + 1, da wir einen Eintrag löschen und anz i Einträge kopieren. Es gilt gr¨o ß e i /2 = gr¨o ß e i−1 /4 = anz i−1 = anz i + 1, und die amortisierten Kosten der Operation betragen ci = ci + Φi − Φi−1 = (anz i + 1) + (gr¨o ß e i /2 − anz i ) − (gr¨o ß e i−1 /2 − anz i−1 ) = (anz i + 1) + ((anz i + 1) − anz i ) − ((2 · anz i + 2) − (anz i + 1)) =1. Wenn die i-te Operation eine Table-Delete-Operation ist und αi−1 ≥ 1/2 gilt, dann sind die amortisierten Kosten ebenfalls nach oben durch eine Konstante beschränkt. Wir überlassen die Analyse der Übung 17.4-2.
Problemstellungen zu Kapitel 17
475
Zusammenfassend können wir sagen, dass die tatsächliche Laufzeit für eine beliebige Sequenz von n Operationen auf einer dynamischen Tabelle O(n) ist, da die amortisierten Kosten jeder Operation nach oben durch eine Konstante beschränkt sind.
Übungen 17.4-1 Nehmen Sie an, wir wollten eine dynamische Hashtabelle mit offener Adressierung implementieren. Warum können wir die Tabelle als voll ansehen, wenn deren Belegungsfaktor einen Wert α erreicht, der echt kleiner als 1 ist? Beschreiben Sie, wie wir Einfügen in eine dynamische Hashtabelle mit offener Adressierung so implementieren können, dass der Erwartungswert der amortisierten Kosten pro Einfügeoperation in O(1) ist. Warum ist der Erwartungswert der tatsächlichen Kosten pro Einfügeoperation nicht notwendigerweise für alle Einfügeoperationen in O(1)? 17.4-2 Zeigen Sie, dass die amortisierten Kosten der i-ten Operation in Bezug auf die Potentialfunktion (17.6) nach oben durch eine Konstante beschränkt sind, wenn αi−1 ≥ 1/2 gilt und die i-te Operation auf einer dynamischen Tabelle Table-Delete ist. 17.4-3 Nehmen Sie an, dass wir, anstatt eine Tabelle durch Halbierung der Größe zu kontrahieren, wenn der Belegungsfaktor unter 1/4 fällt, die Tabelle auf 2/3 ihrer Größe kontrahieren würden, wenn ihr Belegungsfaktor unter 1/3 fällt. Zeigen Sie unter Verwendung der Potentialfunktion Φ(T ) = |2 · T.anz − T.gr¨o ß e| , dass die amortisierten Kosten von Table-Delete durch eine Konstante nach oben beschränkt sind, wenn diese Strategie verwendet wird.
Problemstellungen 17-1 Bit-umkehrende Binärzähler In Kapitel 30 wird ein wichtiger Algorithmus untersucht, der als schnelle Fouriertransformation (engl.: Fast Fourier Transform), abgekürzt FFT, bekannt ist. Der erste Schritt beim FFT-Algorithmus führt eine Bit-umkehrende Permutation auf einem Eingabefeld A[0 . . n − 1] aus, dessen Länge den Wert n = 2k für ein nichtnegatives ganzzahliges k hat. Diese Permutation vertauscht Elemente, deren Indizes in binärer Darstellung zueinander gespiegelt sind. Wir können jeden Index a als k-Bit-Sequenz ak−1 , ak−2 , . . . , a0 darstellen, wobei i a = k−1 i=0 ai 2 gilt. Wir definieren revk (ak−1 , ak−2 , . . . , a0 ) = a0 , a1 , . . . , ak−1 , sodass revk (a) =
k−1 i=0
ak−i−1 2i
476
17 Amortisierte Analyse gilt. Wenn beispielsweise n = 16 gilt (oder äquivalent dazu k = 4), dann gilt revk (3) = 12, da die 4-Bit-Darstellung von 3 die Sequenz 0011 ist, was in umgekehrter Reihenfolge 1100 ergibt. Das ist die 4-Bit-Darstellung von 12. a. Es sei eine Funktion revk gegeben, die in Zeit Θ(k) läuft. Schreiben Sie einen Algorithmus, der die Bit-umkehrende Permutation eines Feldes der Länge n = 2k in Zeit O(nk) ausführt. Wir können einen auf amortisierter Analyse basierenden Algorithmus verwenden, um die Laufzeit der Bit-umkehrenden Permutation zu verbessern. Wir verwalten einen „Bit-umkehrenden Zähler“ und verwenden eine Prozedur Bit-ReversedIncrement, die bei einem gegebenen Wert a des Bit-umkehrenden Zählers den Ausdruck revk (revk (a) + 1) erzeugt. Wenn beispielsweise k = 4 gilt, und der Bit-umkehrende Zähler bei 0 startet, dann erzeugen sukzessive Aufrufe von BitReversed-Increment die Sequenz 0000, 1000, 0100, 1100, 0010, 1010, . . . = 0, 8, 4, 12, 2, 10, . . . . b. Setzen Sie voraus, dass die Wörter in Ihrem Rechner k-Bit-Werte speichern und dass Ihr Rechner Operationen auf binären Werten wie dem Shiften nach links oder rechts um einen beliebigen Betrag, dem bitweisen UND, dem bitweisen ODER usw. in jeweils einer Zeiteinheit ausführt. Geben Sie eine Implementierung der Prozedur Bit-Reversed-Increment an, mit der das Ausführen der Bit-umkehrenden Permutation eines Feldes mit n Elementen in Gesamtzeit O(n) ermöglicht wird. c. Nehmen Sie nun an, dass Sie ein Wort um nur ein Bit pro Zeiteinheit nach links oder rechts shiften könnten. Ist es unter diesen Umständen noch immer möglich, eine Bit-umkehrende Permutation in Zeit O(n) zu implementieren?
17-2 Aufbau einer dynamischen binären Suche Die binäre Suche in einem sortierten Feld benötigt logarithmische Suchzeit; die Zeit für das Einfügen eines neuen Elementes ist jedoch eine lineare Funktion der Größe des Feldes. Wir können die Zeit für das Einfügen verbessern, indem wir mehrere sortierte Felder verwalten. Genauer gesagt wollen wir die Operationen Search und Insert auf einer Menge von n Elementen unterstützen. Sei k = lg(n + 1) und sei die binäre Darstellung von n gleich nk−1 , nk−2 , . . . , n0 . Wir haben k sortierte Felder A0 , A1 , . . . , Ak−1 , wobei für i = 0, 1, . . . , k − 1 die Länge des Feldes Ai genau 2i ist. Jedes Feld ist entweder voll oder leer, abhängig davon, ob ni = 1 oder ni = 0 gilt. Die Gesamtanzahl der Elemente, die in den k Feldern verwaltet werden, ist deshalb k−1 i i=0 ni 2 = n. Obwohl jedes einzelne Feld für sich sortiert ist, haben Elemente aus unterschiedlichen Feldern keine spezielle Beziehung untereinander. a. Beschreiben Sie, wie die Search-Operation für diese Datenstruktur auszuführen ist. Analysieren Sie die Laufzeit im schlechtesten Fall. b. Beschreiben Sie, wie die Insert-Operation für diese Datenstruktur auszuführen ist. Analysieren Sie die Laufzeit im schlechtesten Fall und die amortisierte Laufzeit.
Problemstellungen zu Kapitel 17
477
c. Diskutieren Sie, wie Delete zu implementieren ist. 17-3 Amortisierte gewichtsbalancierte Bäume Betrachten Sie einen gewöhnlichen binären Suchbaum, der dadurch erweitert wurde, dass jedem Knoten x das Attribut x.gr¨o ß e hinzugefügt wurde, welches die Anzahl der Schlüssel angibt, die in dem von x ausgehenden Teilbaum gespeichert sind. Sei α eine Konstante im Bereich 1/2 ≤ α < 1. Wir sagen, dass ein gegebener Knoten x α-balanciert ist, wenn x.links.gr¨o ß e ≤ α · x.gr¨o ß e and x.rechts.gr¨o ß e ≤ α · x.gr¨o ß e gilt. Der Baum als Ganzes ist α-balanciert, wenn jeder Knoten im Baum α-balanciert ist. Die folgende amortisierte Methode für das Verwalten gewichtsbalancierter Bäume wurde von G. Varghese vorgeschlagen. a. In gewissem Sinne ist ein 1/2-balancierter Baum so gut balanciert, wie dies nur möglich ist. Zeigen Sie für einen beliebigen Knoten x in einem binären Suchbaum, wie der von x ausgehende Teilbaum umzubauen ist, damit er 1/2balanciert wird. Ihr Algorithmus sollte in Zeit Θ(x.gr¨o ß e) laufen und Hilfsspeicher im Umfang von O(x.gr¨o ß e) benutzen. b. Zeigen Sie, dass das Ausführen einer Suche in einem α-balancierten binären Suchbaum mit n Knoten im schlechtesten Fall Zeit O(lg n) benötigt. Für den Rest der Problemstellung setzen Sie voraus, dass die Konstante α echt größer als 1/2 ist. Nehmen Sie an, wir würden Insert und Delete wie für einen binären Suchbaum mit n Knoten üblich implementieren, außer dass nach jeder solchen Operation, wir im Falle, dass irgendein Knoten im Baum nicht mehr α-balanciert ist, den Teilbaum zu einem 1/2-balancierten Teilbaum „umbauen“, dessen Wurzel der höchste dieser Knoten im Baum ist. Wir werden dieses Umbauen mithilfe der Potentialmethode analysieren. Für einen Knoten x im binären Suchbaum T definieren wir Δ(x) = |x.links .gr¨o ß e − x.rechts.gr¨o ß e| , und das Potential von T durch Φ(T ) = c Δ(x) , x∈T :Δ(x)≥2
wobei c eine hinreichend große Konstante ist, die von α abhängt. c. Zeigen Sie, dass jeder binäre Suchbaum ein nichtnegatives Potential besitzt und dass ein 1/2-balancierter Baum das Potential 0 hat. d. Nehmen Sie an, dass m Potentialeinheiten den Umbau eines Teilbaumes mit m Knoten finanzieren könnten. Wie groß muss c in Abhängigkeit von α sein, damit O(1) amortisierte Zeit benötigt wird, um einen Teilbaum umzubauen, der nicht α-balanciert ist? e. Zeigen Sie, dass das Einfügen eines Knotens in oder das Löschen eines Knotens aus einem α-balancierten Baum mit n Knoten O(lg n) amortisierte Zeit kostet.
478
17 Amortisierte Analyse
17-4 Die Kosten, um einen Rot-Schwarz-Baum zu rekonstruieren Es gibt auf Rot-Schwarz-Bäumen vier Grundoperationen, die strukturelle Modifikationen ausführen: Einfügen eines Knotens, Entfernen eines Knotens, Rotationen und Umfärben eines Knotens. Wir haben gesehen, dass RB-Insert und RB-Delete nur O(1) Rotationen verwenden, damit das Einfügen und Entfernen von Knoten die Rot-Schwarz-Eigenschaften erhält. Sie können jedoch wesentlich mehr Umfärbungen vornehmen. a. Beschreiben Sie einen gültigen Rot-Schwarz-Baum mit n Knoten, für den der Aufruf von RB-Insert zum Einfügen des (n + 1)-ten Knotens Ω(lg n) Umfärbungen bewirkt. Beschreiben Sie dann einen gültigen Rot-Schwarz-Baum mit n Knoten, für den der Aufruf von RB-Delete angewendet auf einen speziellen Knoten Ω(lg n) Umfärbungen bewirkt. Obwohl die Anzahl der Umfärbungen pro Operation im schlechtesten Fall logarithmisch sein kann, werden wir beweisen, dass eine beliebige Sequenz von m RBInsert- und RB-Delete-Operationen auf einem anfangs leeren Rot-SchwarzBaum im schlechtesten Fall O(m) strukturelle Modifikationen verursacht. Wir zählen jedes Umfärben eines Knotens als eine strukturelle Modifikation. b. Einige der Fälle, die sowohl im Code von RB-Insert-Fixup als auch von RBDelete-Fixup in der Hauptschleife behandelt werden, sind terminierend : Sobald wir auf sie treffen, führen sie nach einer konstanten Anzahl zusätzlicher Operationen zum Abbruch der Schleife. Geben Sie für jeden der Fälle in RB-Insert-Fixup und RB-Delete-Fixup an, welche terminierend sind und welche nicht. (Hinweis: Sehen Sie sich die Abbildungen 13.5, 13.6 und 13.7 an.) Wir werden zunächst die strukturellen Modifikationen für den Fall analysieren, dass nur Einfügeoperationen ausgeführt werden. Sei T ein Rot-Schwarz-Baum. Wir definieren Φ(T ) als die Anzahl der roten Knoten in T . Setzen Sie voraus, dass eine Einheit des Potentials für das Bezahlen der strukturellen Modifikationen, die durch jede der drei Fälle von RB-Insert-Fixup durchgeführt werden, ausreicht. c. Sei T das Resultat aus der Anwendung von Fall 1 von RB-Insert-Fixup auf T . Zeigen Sie, dass Φ(T ) = Φ(T ) − 1 gilt. d. Wenn wir einen Knoten in einen Rot-Schwarz-Baum mittels RB-Insert einfügen, dann können wir die Operation in drei Teile zerlegen. Zählen Sie die strukturellen Modifikationen und die Potentialveränderungen auf, die sich aus den Zeilen 1–16 von RB-Insert und aus den terminierenden Fällen von RBInsert-Fixup ergeben. e. Zeigen Sie unter Verwendung von Teil (d), dass die Anzahl ausgeführter struktureller Modifikationen, die durch den Aufruf RB-Insert ausgeführt werden, in O(1) ist. Wir wollen nun beweisen, dass es O(m) strukturelle Modifikationen gibt, wenn sowohl die Operation Einfügen als auch die Operation Entfernen ausgeführt wer-
Problemstellungen zu Kapitel 17
479
den. Lassen Sie uns für jeden Knoten x ⎧ 0 falls x rot ist , ⎪ ⎨ 1 falls x schwarz ist und keine roten Kinder hat , w(x) = ⎪ ⎩ 0 falls x schwarz ist und ein rotes Kind hat , 2 falls x schwarz ist und zwei rote Kinder hat definieren. Nun definieren wir das Potential eines Rot-Schwarz-Baumes T neu als Φ(T ) = w(x) . x∈T
Sei T der Baum, der sich aus der Anwendung eines beliebigen nicht terminierenden Falles aus RB-Insert-Fixup oder RB-Delete-Fixup auf T ergibt. f. Zeigen Sie, dass Φ(T ) ≤ Φ(T ) − 1 für alle nicht terminierenden Fälle in RBInsert-Fixup gilt. Zeigen Sie, dass die amortisierte Anzahl der von einem beliebigen Aufruf von RB-Insert-Fixup ausgeführten strukturellen Modifikationen in O(1) ist. g. Zeigen Sie, dass Φ(T ) ≤ Φ(T ) − 1 für alle nicht terminierenden Fälle in RBDelete-Fixup gilt. Zeigen Sie, dass die amortisierte Anzahl der von einem beliebigen Aufruf von RB-Delete-Fixup ausgeführten strukturellen Modifikationen in O(1) ist. h. Vervollständigen Sie den Beweis, dass im schlechtesten Fall jede beliebige Sequenz von m RB-Insert- und RB-Delete-Operationen O(m) strukturelle Modifikationen ausführt. 17-5 Konkurrenzfähigkeitsanalyse von selbstorganisierenden Listen, die nach der nach-vorne-Kopier-Heuristik arbeiten Eine selbstorganisierende Liste ist eine verkettete Liste von n Elementen, in der jedes Element einen eindeutigen Schlüssel besitzt. Wenn wir nach einem Element in der Liste suchen, dann kennen wir einen Schlüssel und wir wollen ein Element mit diesem Schlüssel finden. Eine selbstorganisierende Liste besitzt zwei wichtige Eigenschaften: (a) Um ein Element, das in der Liste enthalten ist, zu finden, müssen wir die Liste vom Kopf her durchsuchen, bis wir auf das Element mit dem gegebenen Schüssel treffen. Ist dieses Element das k-te Element vom Kopf der Liste aus gesehen, so fallen Kosten in Höhe von k an, um das Element zu finden. (b) Wir können nach jeder Operation die Listenelemente gemäß einer gegebenen Regel mit entsprechenden Kosten umordnen. Wir dürfen eine beliebige Heuristik wählen, um zu entscheiden, wie wir die Liste umordnen wollen. Nehmen Sie an, wir würden mit einer gegebenen Liste bestehend aus n Elementen starten und wir hätten eine Sequenz σ = σ1 , σ2 , . . . , σm von Schlüsseln gegeben, nach den wir in dieser Reihenfolge suchen wollen, um auf die entsprechenden Elemente in der Liste zuzugreifen. Die Kosten der Sequenz ist die Summe der Kosten der einzelnen Zugriffe der Sequenz.
480
17 Amortisierte Analyse Wir wollen nach einer Operation die Liste umordnen, indem wir adjazente Listenelemente vertauschen – d. h. indem wir ihre Positionen in der Liste vertauschen –, wobei eine Vertauscheoperation eine Kosteneinheit kostet. Sie werden mithilfe einer Potentialfunktion zeigen, dass eine spezielle Heuristik, die Liste umzuordnen, die als nach-vorne-kopier-Heuristik bekannt ist, zu Gesamtkosten führt, die nicht höher sind als viermal die Kosten einer beliebigen anderen Heuristik, um die Listenordnung zu erhalten – sogar wenn die andere Heuristik die Zugriffssequenz im Vorhinein kennt! Wir nennen diese Art von Analyse eine Konkurrenzfähigkeitsanalyse. Lassen Sie uns, für eine Heuristik H und eine gegebene initiale Ordnung der Liste die Kosten der Sequenz σ mit CH (σ) bezeichnen. Sei m die Anzahl der Zugriffe aus σ. a. Zeigen Sie, dass wenn die Heuristik H die Zugriffsfolge im Vorhinein nicht kennt, die Kosten CH (σ) für H angewendet auf eine Zugriffsfolge σ im schlechtesten Fall in Ω(mn) liegen. Mit der nach-vorne-kopier-Heuristik platzieren wir das Element x an den Kopf der Liste, sofort nachdem wir nach x gesucht haben. Sei rankL (x) der Rang von dem Element x in der Liste L, d. h. die Position von x in der Liste L. Ist x beispielsweise das vierte Element in L, so gilt rankL (x) = 4. Sei ci die Kosten des Zugriffs σi bei Verwendung der nach-vorne-kopier-Heuristik, die sich aus den Kosten, das Element in der Liste zu finden, und den Kosten, das Element durch eine Reihe von Transpositionen mit jeweils adjazenten Listenelementen an den Kopf der Liste zu platzieren, zusammensetzen. b. Zeigen Sie, dass, wenn σi unter Verwendung der nach-vorne-kopier-Heuristik auf das Element x der Liste L zugreift, ci = 2 · rankL (x) − 1 gilt. Wir vergleichen nun die nach-vorne-kopier-Heuristik mir einer anderen Heuristik H, die eine Zugriffsfolge gemäß den zwei weiter oben angegebenen Eigenschaften bearbeitet. Heuristik H darf die Elemente in der Liste in einer beliebigen Art und Weise vertauschen und sie darf sogar die gesamte Anfragefolge im Vorhinein wissen. Sei Li die Liste nach dem Zugriff σi unter Verwendung der nach-vorne-kopierHeuristik und sei L∗i die Liste nach Zugriff σi durch Heuristik H. Wir bezeichnen die Kosten des Zugriffs σi unter Verwendung der nach-vorne-kopier-Heuristik mit ci und die Kosten des Zugriffs σi durch Heuristik H mit c∗i . Setzen Sie voraus, dass Heuristik H t∗i Transpositionen während des Zugriffs σi durchführt. c. In Teil (b) zeigten Sie, dass ci = 2 · rankLi−1 (x) − 1 gilt. Zeigen Sie nun c∗i = rankL∗i−1 (x) + t∗i . Wir definieren eine Inversion in der Liste Li als ein Paar von Elementen y und z, sodass y in der Liste Li vor z steht und in der Liste L∗i hinter z steht. Nehmen Sie an, die Liste Li hätte qi Inversionen, nachdem die Teilsequenz σ1 , σ2 , . . . , σi bearbeitet worden ist. Wir definieren dann eine Potentialfunktion Φ, die Li auf
Problemstellungen zu Kapitel 17
481
die Zahl Φ(Li ) = 2qi abbildet. Besteht Li beispielsweise aus den Elementen e, c, a, d, b und L∗i aus den Elementen c, a, b, d, e, dann hat Li 5 Inversionen ((e, c), (e, a), (e, d), (e, b), (d, b)) und so gilt Φ(Li ) = 10. Bemerken Sie, dass Φ(Li ) ≥ 0 für alle i gilt und dass, wenn die nach-vorne-kopier-Heuristik und die Heuristik H mit der gleichen Liste L0 starten, Φ(L0 ) = 0 gilt. d. Zeigen Sie, dass eine Transposition das Potential entweder um 2 erhöht oder um 2 senkt. Nehmen Sie an, dass die Anfrage σi das Element x findet. Um zu verstehen, wie sich das Potential aufgrund des Zugriffs σi ändert, lassen Sie uns die Elemente (außer x) in vier Mengen partitionieren, abhängig davon, wo sich die Elemente genau vor dem i-ten Zugriff in den Listen befinden: • Die Menge A besteht aus den Elementen, die sowohl in Li−1 als auch in L∗i−1 vor x stehen. • Die Menge B besteht aus den Elementen, die in Li−1 vor x und in L∗i−1 hinter x stehen. • Die Menge C besteht aus den Elementen, die in Li−1 hinter x und in L∗i−1 vor x stehen. • Die Menge D besteht aus den Elementen, die sowohl in Li−1 als auch in L∗i−1 hinter x stehen. e. Zeigen Sie, dass rankLi−1 (x) = |A| + |B| + 1 und rankL∗i−1 (x) = |A| + |C| + 1 gelten. f. Zeigen Sie, dass der Zugriff σi eine Änderung des Potentials in Höhe von Φ(Li ) − Φ(Li−1 ) ≤ 2(|A| − |B| + t∗i ) bewirkt, wobei die Heuristik H wie zuvor t∗i Transpositionen während des Zugriffs σi durchführt. Definieren Sie die amortisierten Kosten ci des Zugriffs σi durch ci = ci + Φ(Li ) − Φ(Li−1 ) . g. Zeigen Sie, dass die amortisierten Kosten ci des Zugriffs σi nach oben durch 4c∗i beschränkt sind. h. Schlussfolgern Sie, dass die Kosten CNVK (σ) der Zugriffsfolge σ unter Verwendung der nach-vorne-kopier-Heuristik höchstens viermal die Kosten CH (σ) von σ sind, die entstehen, wenn man eine andere Heuristik H verwendet, sofern beide Heuristiken mit der gleichen Liste starten.
482
17 Amortisierte Analyse
Kapitelbemerkungen Aho, Hopcroft und Ullman [5] verwenden die Aggregat-Analyse, um die Laufzeit von Operationen auf Wäldern disjunkter Mengen zu bestimmen; wir werden diese Datenstruktur unter Verwendung der Potentialmethode in Kapitel 21 analysieren. Tarjan [331] gibt einen Überblick zu Accounting-Methoden und Potentialmethoden der amortisierten Analyse und stellt verschiedene Anwendungen vor. Er schreibt die Account-Methode verschiedenen Autoren zu, unter ihnen sind M. R. Brown, R. E. Tarjan, S. Huddleston und K. Mehlhorn. Die Potentialmethode schreibt er D. D. Sleator zu. Der Ausdruck „amortisiert“ geht auf D. D. Sleator und R. E. Tarjan zurück. Potentialfunktionen sind auch für den Beweis von unteren Schranken für bestimmte Problemtypen nützlich. Für jede Konfiguration des Problems definieren wir eine Potentialfunktion, die die Konfiguration auf eine reelle Zahl abbildet. Dann bestimmen wir das Potential Φanf der Anfangskonfiguration, das Potential Φend der Endkonfiguration und die maximale Änderung ΔΦmax im Potential infolge eines beliebigen Schrittes. Die Anzahl der Schritte muss deshalb mindestens |Φfinal − Φinit | / |ΔΦmax | sein. Beispiele von Potentialfunktionen für den Beweis unterer Schranken bei der I/O-Komplexität erscheinen in Arbeiten von Cormen, Sundquist und Wisniewski [79], Floyd [107] sowie Aggarwal und Vitter [3]. Krumme, Cybenko und Venkataraman [221] wendeten Potentialfunktionen an, um untere Schranken für Gossiping zu finden: Das Übermitteln eines einzelnen Gegenstandes von einem Knoten in einem Graphen zu jedem anderen Knoten. Die nach-vorne-kopier-Heuristik aus der Problemstellung 17-5 arbeitet ziemlich gut in der Praxis. Zudem können wir zeigen, dass die Kosten der nach-vorne-kopier-Heuristik höchstens zweimal die Kosten einer jeden anderen Heuristik sind (auch dann wenn man die gesamte Zugriffsfolge im Vorhinein kennt), wenn wir es schaffen, ein Element, das wir gefunden haben, in konstanter Zeit aus der Liste auszuschneiden und an den Kopf der Liste zu platzieren.
Teil V
Höhere Datenstrukturen
Einführung Dieser Teil untersucht nochmals Datenstrukturen, die Operationen auf dynamischen Mengen unterstützen, allerdings im Vergleich zu Teil III auf einem höheren Niveau. Beispielsweise machen zwei der Kapitel umfassend Gebrauch von den Techniken der amortisierten Analyse, die wir in Kapitel 17 kennengelernt haben. Kapitel 18 stellt B-Bäume vor. Dabei handelt es sich um balancierte Suchbäume, die speziell für die Speicherung auf Festplatten entwickelt wurden. Da Festplatten viel langsamer arbeiten als der Arbeitsspeicher, messen wir die Performanz von B-Bäumen nicht nur daran, wie viel Rechenzeit die Operationen auf der dynamischen Menge verbrauchen, sondern auch daran, wie viele Speicherzugriffe sie ausführen. Für jede Operation auf einem B-Baum nimmt die Anzahl der Speicherzugriffe mit der Höhe des B-Baumes zu. B-Baum-Operationen halten die Höhe aber niedrig. Kapitel 19 stellt eine Implementierung fusionierbarer Heaps vor, die die Operationen Insert, Minimum, Extract-Min und Union unterstützen.1 Die Operation Union vereinigt, oder fusioniert, zwei Heaps. Fibonacci-Heaps – die in Kapitel 19 vorgestellte Datenstruktur – unterstützt auch die Operationen Delete und Decrease-Key. Wir verwenden amortisierte Zeitschranken, um die Performanz von Fibonacci-Heaps abzuschätzen. Die Operationen Insert, Minimum und Union benötigen auf FibonacciHeaps tatsächlich und amortisiert nur Zeit O(1), und die Operationen Extract-Min und Delete benötigen die amortisierte Zeit O(lg n). Der signifikanteste Vorteil von Fibonacci-Heaps besteht jedoch darin, dass Decrease-Key nur amortisierte Zeit O(1) benötigt. Da die Operation Decrease-Key nur konstante amortisierte Laufzeit benötigt, sind Fibonacci-Heaps bis heute die Schlüsselkomponenten einiger der asymptotisch schnellsten Algorithmen für Graphenprobleme. Wir haben gesehen, dass wir die untere Schranke für Sortieren von Ω(n lg n) schlagen können, wenn die Schlüssel ganze Zahlen aus einem eingeschränkten Bereich sind. Kapitel 20 untersucht aus diesem Grunde, ob wir eine Datenstruktur für dynamische Mengen entwerfen können, auf der die Operationen Search, Insert, Delete, Minimum, Maximum, Successor und Predecessor in Zeit von o(lg n) ausgeführt werden können, wenn die Schlüssel ganze Zahlen aus einem eingeschränkten Bereich sind. Die Antwort lautet, dass wir dies können, indem wir eine rekursive Datenstruktur benutzen, die als van-Emde-Boas-Baum bekannt ist. Wenn die Schlüssel eindeutige ganze Zahlen aus der Menge {0, 1, 2, . . . , u − 1} sind und u eine Zweierpotenz ist, dann unterstützen van-Emde-Boas-Bäume jede der oben genannten Operationen in Zeit O(lg lg u). 1 Wie in Problemstellung 10-2 haben wir einen fusionierbaren Heap so definiert, dass er Minimum und Extract-Min unterstützt, sodass wir ihn auch als fusionierbaren Min-Heap bezeichnen können. Wenn andererseits Maximum und Extract-Max unterstützt würden, dann würde es sich um einen fusionierbaren Max-Heap handeln. Wenn nicht anders spezifiziert, dann sind fusionierbare Heaps standardmäßig Min-Heaps.
486
Teil V Datenstrukturen
Schließlich stellt Kapitel 21 Datenstrukturen für disjunkte Mengen vor. Gegeben haben wir ein Universum bestehend aus n Elementen, das in dynamische Mengen partitioniert ist. Anfangs gehört jedes Element zu seiner eigenen einelementigen Menge. Die Operation Union vereinigt zwei Mengen und die Anfrage Find-Set identifiziert die eindeutige Menge, die das gegebene Element zu diesem Zeitpunkt enthält. Indem wir jede Menge durch einen einfachen gerichteten Baum darstellen, erhalten wir überraschend schnelle Operationen: Eine Folge von m Operationen läuft in Zeit O(m · α(n)), wobei α(n) eine extrem langsam anwachsende Funktion ist – α(n) hat bei jeder denkbaren Anwendung höchstens den Wert 4. Die amortisierte Analyse, die diese Zeitschranke beweist, ist so komplex, wie die Datenstruktur einfach ist. Die in diesem Teil behandelten Themen sind bei weitem nicht die einzigen Beispiele für „höhere“ Datenstrukturen. Andere höhere Datenstrukturen sind zum Beispiel die folgenden: • Die von Sleator und Tarjan [319] eingeführten und von Tarjan [330] diskutierten dynamischen Bäume verwalten einen Wald aus disjunkten gerichteten Bäumen. Allen Baumkanten sind reellwertige Kosten zugeordnet. Dynamische Bäume unterstützen Abfragen, um Väter, Wurzeln, Kantenkosten und die minimalen Kantenkosten auf einem einfachen Pfad, der von einem Knoten zu einer Wurzel verläuft, zu finden. Bäume können manipuliert werden, indem Kanten durchtrennt werden, alle Kantenkosten auf einem einfachen Pfad von einem Knoten zu einer Wurzel aktualisiert werden, eine Wurzel in einen anderen Baum verknüpft wird und ein Knoten zur Wurzel des Baumes wird, in dem er enthalten ist. Eine einfache Implementierung eines dynamischen Baumes liefert eine amortisierte Zeitschranke von O(lg n) für jede Operation; eine ausgefeiltere Implementierung führt im schlechtesten Fall auf eine Zeitschranke von O(lg n). Dynamische Bäume werden in einigen der asymptotisch schnellsten Flussnetzwerk-Algorithmen verwendet. • Die von Sleator und Tarjan [320] entwickelten und von Tarjan [330] diskutierten Splay-Bäume sind eine Form binärer Suchbäume, bei denen die gewöhnlichen Suchbaum-Operationen in amortisierter Zeit O(lg n) laufen. Eine Anwendung von Splay-Bäumen vereinfacht dynamische Bäume. • Persistente Datenstrukturen erlauben Abfragen und manchmal auch Aktualisierungen von früheren Versionen der Datenstruktur. Driscoll, Sarnak, Sleator und Tarjan [97] stellen eine Methode vor, um mit nur geringen Zeit- und Speicherkosten verkettete Datenstrukturen persistent zu machen. Problemstellung 13-1 stellt ein einfaches Beispiel einer persistenten dynamischen Menge vor. • Wie bereits in Kapitel 20 ermöglichen mehrere Datenstrukturen eine schnellere Implementierung von Wörterbuchoperationen (Insert, Delete und Search), wenn das Universum der Schlüssel eingeschränkt ist. Indem sie sich diese Einschränkungen zunutze machen, sind sie in der Lage, im schlechtesten Fall bessere asymptotische Laufzeiten zu erreichen als vergleichsbasierte Datenstrukturen. Fredman und Willard führten Fusionsbäume [115] ein, die die ersten Datenstrukturen waren, die schnellere Wörterbuchoperationen ermöglichten, wenn das Universum der Schlüssel auf ganze Zahlen beschränkt ist. Sie zeigten, wie diese
Einführung
487
Operationen in Zeit O(lg n/ lg lg n) zu implementieren sind. Mehrere später entwickelte Datenstrukturen, einschließlich exponentieller Suchbäume [16], haben ebenfalls verbesserte Schranken für einige oder alle Wörterbuchoperationen geliefert und werden in den verschiedenen Kapitelbemerkungen dieses Buches erwähnt. • Datenstrukturen für dynamische Graphen unterstützen verschiedene Anfragen, wobei sich die Struktur des Graphen während der Operationen, die Knoten oder Kanten einfügen oder entfernen, ändern kann. Beispiele für Anfragen, die sie unterstützen, sind die Knotenzusammenhangszahl (engl.: vertex connectivity) [166], die Kantenzusammenhangszahl (engl.: edge connectivity), die minimalen aufspannenden Bäume [165], der Zweifachzusammenhang (engl.: biconnectivity) und die transitive Hülle [164]. In den Kapitelbemerkungen des Buches werden zusätzliche Datenstrukturen erwähnt.
18
B-Bäume
B-Bäume sind balancierte Suchbäume, die so entwickelt wurden, dass sie auf Festplatten oder anderen sekundären Speichereinrichtungen mit direktem Zugriff gut arbeiten. BBäume sind den in Kapitel 13 vorgestellten Rot-Schwarz-Bäumen ähnlich, aber in Bezug auf die Minimierung der Festplatten-Zugriffe besser geeignet. Viele Datenbanksysteme verwenden B-Bäume oder Varianten von B-Bäumen, um Informationen abzuspeichern. B-Bäume unterscheiden sich von Rot-Schwarz-Bäumen darin, dass die Knoten in BBäumen viele Kinder haben können, von einigen wenigen bis hin zu einigen tausenden. Der „Verzweigungsgrad“ eines B-Baumes kann ziemlich groß sein und hängt üblicherweise von den Eigenschaften der benutzten Festplatte ab. B-Bäume sind Rot-SchwarzBäumen insofern ähnlich, als dass jeder B-Baum mit n Knoten die Höhe O(lg n) besitzt. Allerdings kann die exakte Höhe eines B-Baumes beträchtlich geringer als die des RotSchwarz-Baumes sein, da sein Verzweigungsgrad und somit die Basis des Logarithmus, über die seine Höhe angeben wird, viel größer sein kann. Deshalb können wir B-Bäume auch verwenden, um viele Operationen auf dynamischen Mengen in Zeit O(lg n) zu implementieren. B-Bäume verallgemeinern binäre Suchbäume auf natürliche Weise. Abbildung 18.1 zeigt einen einfachen B-Baum. Wenn ein innerer Knoten x eines B-Baumes x.n Schlüssel enthält, dann besitzt x genau x.n + 1 Kinder. Die Schlüssel im Knoten x dienen als Verzweigungspunkte, die den von x verwalteten Bereich in x.n + 1 Teilbereiche unterteilen, wobei jeder dieser Teilbereiche von einem Kind von x verwaltet wird. Wenn wir in einem B-Baum nach einem Schlüssel suchen, treffen wir eine Entscheidung mit x.n + 1 Wahlmöglichkeiten auf der Grundlage von Vergleichen mit den x.n im Knoten x gespeicherten Schlüsseln. Die Struktur der Blätter unterscheidet sich von der innerer Knoten; wir werden diese Unterschiede in Abschnitt 18.1 untersuchen. Abschnitt 18.1 gibt eine präzise Definition von B-Bäumen und zeigt, dass die Höhe eines B-Baumes nur logarithmisch mit der Anzahl der enthaltenen Knoten wächst. Abschnitt 18.2 beschreibt, wie nach einem Schlüssel zu suchen ist und wie Schlüssel in einen B-Baum einzufügen sind. Abschnitt 18.3 diskutiert das Entfernen von Schlüsseln. Bevor wir jedoch fortfahren, sollten wir uns fragen, warum wir Datenstrukturen, die speziell entworfen sind, um auf Festplatten zu arbeiten, anders analysieren als die Datenstrukturen, die entworfen sind, um im Wesentlichen im Hauptspeicher zu arbeiten.
Datenstrukturen auf Sekundärspeichern Rechnersysteme benutzen verschiedenartige Technologien, um Speicherkapazität bereitzustellen. Der Primärspeicher (oder Hauptspeicher ) eines Rechnersystems besteht normalerweise aus Silizium-Speicherchips. Diese Technologie ist pro gespeicher-
490
18 B-Bäume T.wurzel
M D H B C
F G
Q T X J K L
N P
R S
V W
Y Z
Abbildung 18.1: Ein B-Baum, dessen Schlüssel von den Konsonanten unseres Alphabets gebildet werden. Ein innerer Knoten x, der x. n Schlüssel enthält, hat x. n + 1 Kinder. Alle Blätter des Baumes befinden sich in gleicher Tiefe. Die schwach schattierten Knoten werden bei einer Suche nach dem Buchstaben R besucht.
Platte
Drehachse
Spur
Lese/SchreibeKopf
Arme Abbildung 18.2: Ein typisches Festplattenlaufwerk. Es besteht aus einer oder mehrerer Platten (zwei Platten sind hier gezeigt), die um eine Spindel rotieren. Jede Platte wird von einem Lese/Schreibe-Kopf gelesen oder beschrieben, der sich am Ende eines Armes befindet. Eine Spur ist die Oberfläche, die sich unter einem Lese/Schreib-Kopf hinwegbewegt, wenn dieser an einer festen Position steht.
tem Bit üblicherweise um mehr als eine Größenordnung teurer als die MagnetspeicherTechnologie, wie beispielsweise Bänder oder Platten. Die meisten Rechnersysteme verfügen außerdem über sekundären Speicher, der auf Magnetspeicherplatten basiert. Der Umfang eines solchen Sekundärspeichers übersteigt den Umfang des Primärspeichers in der Regel um mindestens zwei Größenordnungen. Abbildung 18.2 zeigt ein typisches Festplattenlaufwerk. Das Laufwerk besteht aus einer oder mehreren Platten, die mit konstanter Geschwindigkeit um eine gemeinsame Achse rotieren. Ein magnetisierbares Material beschichtet die Oberfläche einer jeden Platte. Jede Platte wird von einem Lese/Schreibe-Kopf gelesen oder beschrieben, der sich am Ende eines Armes befindet. Die Arme können ihre Lese/Schreibe-Köpfe auf die Achse zu- oder von ihr wegbewegen. Wenn ein gegebener Kopf auf einer festen Position steht, dann wird die unter ihm durchlaufende Oberfläche als Spur bezeichnet.
18 B-Bäume
491
Mehrere Platten in einer Festplatte erhöhen die Kapazität der Festplatte, aber nicht ihre Performanz. Zwar sind Festplatten billiger und besitzen eine höhere Kapazität als Hauptspeicher, sie sind jedoch sehr viel langsamer, da sie bewegliche mechanische Teile enthalten. Die mechanische Bewegung besteht aus zwei Komponenten: die Plattenrotation und die Armbewegung. Zum Zeitpunkt des Erscheinens dieses Buches rotieren handelsübliche Platten mit einer Geschwindigkeit von 5.400–15.000 Umdrehungen pro Minute (UPM)1 . Wir finden 15.000 UPM typischerweise in Servern, 7.200 UPM in Desktops und 5.400 UPM in Laptops. Wenngleich 7.200 UPM schnell erscheint, benötigt eine Umdrehung 8,33 Millisekunden, was mehr als fünf Größenordnungen langsamer als die Zugriffszeiten von 50 Nanosekunden, die typischerweise bei Siliziumspeichern erreicht werden, ist. Wenn wir also eine ganze Umdrehung warten müssten, damit ein bestimmtes Datenwort unter dem Lese/Schreibe-Kopf erscheint, dann könnten wir innerhalb dieser Zeitspanne mehr als 100.000-mal auf den Hauptspeicher zugreifen. Im Mittel müssen wir nur eine halbe Umdrehung warten, aber die Unterschiede zwischen den Zugriffszeiten für Siliziumspeicher und Festplatten sind immer noch gewaltig. Das Bewegen der Arme benötigt ebenfalls etwas Zeit. Zum Zeitpunkt des Erscheinens dieses Buches liegen die mittleren Zugriffszeiten für handelsübliche Festplatten bei 8 bis 11 Millisekunden. Um die Wartezeit für mechanische Bewegungen zu amortisieren, greifen Festplatten nicht nur auf ein Datenwort, sondern jeweils in einem Schritt gleich auf mehrere Datenwörter zu. Die Daten werden in eine Anzahl von gleichgroßen Seiten von Bits, die jeweils hintereinander auf einer Spur gespeichert sind, unterteilt und jeder Lese- oder Schreibezugriff auf die Festplatte umfasst eine oder mehrere vollständige Seiten. Bei einer typischen Festplatte hat eine Seite eine Länge zwischen 211 und 214 Bytes. Sobald der Lese/Schreibe-Kopf seine korrekte Position eingenommen hat und die Platte zum Beginn der gewünschten Seite rotiert ist, erfolgt das Lesen oder Beschreiben einer Platte ausschließlich auf elektronischem Wege (abgesehen von der Rotation der Platte) und die Festplatte kann große Datenmengen schnell lesen oder schreiben. Oftmals dauert der Zugriff auf eine Seite und das Lesen der entsprechenden Daten von der Festplatte länger als die Verarbeitung der gelesenen Daten. Aus diesem Grund werden wir uns in diesem Kapitel die zwei Hauptkomponenten der Laufzeit separat anschauen: • die Anzahl der Festplattenzugriffe und • die CPU-Zeit, d. h. die Rechenzeit. Wir messen die Anzahl der Plattenzugriffe über die Anzahl der Datenseiten, die von der Festplatte gelesen oder auf die Festplatte geschrieben werden müssen. Leider ist die Festplattenzugriffszeit nicht konstant – sie hängt von dem Abstand zwischen der aktuellen Spur und der gewünschten Spur sowie von der anfänglichen Rotationsposition 1 Zum Zeitpunkt des Erscheinens dieses Buches haben so genannte Halbleiterlaufwerke (engl.: solidstate drive, SSD) Zugang zum Verbrauchermarkt gefunden. Wenngleich sie schneller als mechanische Festplatten sind, so kosten sie pro Gigabyte mehr und verfügen über kleinere Kapazitäten als mechanische Festplatten.
492
18 B-Bäume
der Platte ab. Nichtsdestotrotz werden wir die Anzahl der beschriebenen oder gelesenen Seiten als erste Näherung für die Gesamtzeit, die für Festplattenzugriffe verbraucht wird, nehmen. Bei einer typischen B-Baum-Anwendung ist die zu verarbeitende Datenmenge so groß, dass nicht alle Daten gleichzeitig in den Hauptspeicher passen. Die B-Baum-Algorithmen kopieren bei Bedarf ausgewählte Seiten von der Festplatte in den Hauptspeicher und schreiben die veränderten Seiten auf die Festplatte zurück. B-Baum-Algorithmen halten zu jeder Zeit nur eine konstante Anzahl von Seiten im Hauptspeicher. Daher beschränkt die Größe des Hauptspeichers die Größe der verarbeitbaren B-Bäume nicht. Wir modellieren die Festplattenoperationen in unserem Pseudocode wie folgt. Sei x ein Zeiger auf ein Objekt. Wenn sich das Objekt gegenwärtig im Hauptspeicher des Rechners befindet, dann können wir wie gewohnt auf die Attribute des Objektes zugreifen: beispielsweise mit x.schl¨u ssel . Wenn das Objekt, auf das x zeigt, sich jedoch auf der Festplatte befindet, dann müssen wir die Operation Disk-Read(x) ausführen, um das Objekt x in den Hauptspeicher einzulesen, bevor wir auf dessen Attribute zugreifen können. (Wir nehmen an, dass die Operation Disk-Read(x) keinen Festplattenzugriff erfordert, wenn sich x bereits im Hauptspeicher befindet; sie ist dann „ohne Effekt“, eine so genannte no-operation.) Analog wird die Operation Disk-Write(x) verwendet, um Änderungen zu speichern, die an den Attributen des Objektes x vorgenommen wurden. Demnach sieht das typische Muster für die Arbeit mit einem Objekt folgendermaßen aus: x = ein Zeiger auf ein beliebiges Objekt Disk-Read(x) Operationen, die auf die Attribute von x lesend und/oder schreibend zugreifen Disk-Write(x) // nur wenn Attribute von x geändert wurden. andere Operationen, die auf die Attribute von x nur lesend zugreifen Das System kann lediglich eine begrenzte Anzahl von Seiten gleichzeitig im Hauptspeicher halten. Wir werden voraussetzen, dass das System die nicht mehr verwendeten Seiten aus dem Hauptspeicher löscht; unsere B-Baum-Algorithmen werden auf diesen Punkt nicht weiter eingehen. Da die Laufzeit eines B-Baum-Algorithmus bei den meisten Systemen vorrangig durch die Anzahl der ausgeführten Disk-Read- und Disk-Write-Operationen bestimmt wird, wollen wir normalerweise, dass jede dieser Operationen jeweils so viele Daten wie möglich liest oder schreibt. Daher ist ein Knoten eines B-Baumes gewöhnlich so groß wie eine gesamte Seite und diese Größe beschränkt die Anzahl der Kinder, die ein B-Baum-Knoten haben kann. Bei einem großen B-Baum, der auf einer Festplatte gespeichert ist, sehen wir in Abhängigkeit vom Verhältnis zwischen der Größe eines Schlüssels und der Größe einer Seite oft Verzweigungsgrade zwischen 50 und 2000. Ein großer Verzweigungsgrad reduziert sowohl die Höhe des Baumes als auch die Anzahl der zum Auffinden eines beliebigen Schlüssels benötigten Festplattenzugriffe dramatisch. Abbildung 18.3 zeigt einen B-Baum mit einem Verzweigungsgrad von 1001 und einer Höhe von 2, der über eine
18.1 Die Definition von B-Bäumen
493
T.wurzel
1 Knoten, 1000 Schlüssel
1000 1001
1000
1000
1001
1001
1000
1000
…
1000
1001 Knoten, 1,001,000 Schlüssel
1001
…
1000
1,002,001 Knoten, 1,002,001,000 Schlüssel
Abbildung 18.3: Ein B-Baum der Höhe 2, der mehr als eine Milliarde Schlüssel enthält. In jedem Knoten x ist die Anzahl der Schlüssel x. n in x angegeben. Jeder innere Knoten und jedes Blatt enthält 1000 Schlüssel. Dieser B-Baum hat 1001 Knoten der Tiefe 1 und mehr als eine Million Blätter der Tiefe 2.
Milliarde Schlüssel speichern kann; dennoch können wir mit maximal zwei Festplattenzugriffe einen beliebigen Schlüssel in diesem Baum finden, da wir den Wurzelknoten permanent im Hauptspeicher halten können.
18.1
Die Definition von B-Bäumen
Der Einfachheit halber nehmen wir wie für binäre Suchbäume und Rot-Schwarz-Bäume an, dass jede „Satelliteninformation“, die zu einem Schlüssel gehört, auch in demselben Knoten wie der Schlüssel gespeichert ist. In der Praxis könnte man neben jedem Schlüssel einen Zeiger auf eine andere Festplattenseite speichern, das die Satelliteninformation zu diesem Schlüssel enthält. Der Pseudocode in diesem Kapitel setzt implizit voraus, dass die zu einem Schlüssel gehörige Satelliteninformation mit dem Schlüssel „mitreist“, wenn der Schlüssel von einem Knoten zu einem anderen verschoben wird. Eine oft verwendete Variante eines B-Baumes, die als B+ -Baum bekannt ist, speichert die gesamte Satelliteninformation in den Blättern und nur die Schlüssel und die Zeiger auf die Kinder in den inneren Knoten, was zu einem höheren Verzweigungsgrad der inneren Knoten führt. Ein B-Baum T ist ein gerichteter gewurzelter Baum (dessen Wurzel T.wurzel ist), der die folgenden Eigenschaften besitzt: 1. Jeder Knoten x besitzt die folgenden Attribute: a. die Anzahl x.n der gegenwärtig im Knoten x gespeicherten Schlüssel, b. die x.n Schlüssel x.schl¨u ssel 1 ≤ x.schl¨u ssel 2 ≤ · · · ≤ x.schl¨u ssel x. n , die monoton steigend nach ihren Werten geordnet sind, c. einen Booleschen Wert x.blatt , der wahr ist, wenn x ein Blatt ist, und falsch, wenn x ein innerer Knoten ist. 2. Jeder innere Knoten x enthält außerdem x.n + 1 Zeiger x.c1 , x.c2 , . . . , x.cx. n+1 auf
494
18 B-Bäume
seine Kinder. Blätter haben keine Kinder, sodass ihre Attribute ci nicht definiert sind. 3. Die Schlüssel x.schl¨u ssel i unterteilen die Bereiche der in jedem Teilbaum gespeicherten Schlüssel: Wenn ki ein im Teilbaum mit der Wurzel x.ci gespeicherter Schlüssel ist, dann gilt k1 ≤ x.schl¨u ssel 1 ≤ k2 ≤ x.schl¨u ssel 2 ≤ · · · ≤ x.schl¨u ssel x. n ≤ kx. n+1 . 4. Alle Blätter haben die gleiche Tiefe, die als die Höhe h des Baumes bezeichnet wird. 5. Die Knoten unterliegen in Bezug auf die Anzahl der Schlüssel, die sie jeweils enthalten können, einer unteren und oberen Schranke. Wir stellen diese Schranken mittels einer festen ganzen Zahl t ≥ 2 dar, die wir als der minimale Grad des B-Baumes bezeichnen: a. Jeder Knoten außer der Wurzel muss mindestens t − 1 Schlüssel enthalten. Abgesehen von der Wurzel besitzt somit jeder innere Knoten mindestens t Kinder. Wenn der Baum nichtleer ist, muss die Wurzel mindestens einen Schlüssel haben. b. Jeder Knoten darf höchstens 2t − 1 Schlüssel enthalten. Deshalb darf ein innerer Knoten höchstens 2t Kinder haben. Wir sagen, dass ein Knoten voll ist, wenn er genau 2t − 1 Schlüssel enthält.2 Den einfachsten B-Baum erhalten wir mit t = 2. Jeder innere Knoten besitzt dann entweder 2, 3 oder 4 Kinder, und wir erhalten einen 2-3-4-Baum. In der Praxis werden jedoch größere Werte von t benutzt, die zu B-Bäumen mit kleinerer Höhe führen.
Die Höhe eines B-Baumes Die Anzahl der erforderlichen Festplattenzugriffe ist für die meisten Operationen auf B-Bäumen proportional zur Höhe des B-Baumes. Wir analysieren nun die Höhe eines B-Baumes im schlechtesten Fall. Theorem 18.1 Im Falle n ≥ 1 gilt für jeden B-Baum T mit n Schlüsseln, Höhe h und minimalem Grad t ≥ 2 h ≤ logt
n+1 . 2
Beweis: Die Wurzel eines B-Baumes T enthält mindestens einen Schlüssel und alle anderen Knoten enthalten mindestens t − 1 Schlüssel. Folglich hat T , dessen Höhe h ist, in der Tiefe 1 mindestens 2 Knoten, in der Tiefe 2 mindestens 2t Knoten, in der Tiefe 3 mindestens 2t2 Knoten usw. bis zu einer Tiefe h, in der er mindestens 2th−1 Knoten 2 Eine andere oft benutzte Variante eines B-Baumes, die als B∗ -Baum bekannt ist, fordert, dass jeder innere Knoten mindestens zu zwei Dritteln anstatt zur Hälfte gefüllt ist, wie es B-Bäume verlangen.
18.1 Die Definition von B-Bäumen
495
T.wurzel
1
t–1
t–1
t
t
…
t–1 t
t–1
…
t–1
t–1
t–1
t–1
t
t
…
t–1
t–1
…
…
t–1
Tiefe
Anzahl Knoten
0
1
1
2
2
2t
3
2t2
t
t–1
t–1
…
t–1
Abbildung 18.4: Ein B-Baum der Höhe 3, der die minimale Anzahl von Schlüsseln enthält. Innerhalb jedes Knotens x ist x. n eingetragen.
hat. Abbildung 18.4 illustriert einen solchen Baum für h = 3. Somit erfüllt die Anzahl n der Schlüssel die Ungleichung n ≥ 1 + (t − 1)
h
2ti−1
i=1
= 1 + 2(t − 1)
th − 1 t−1
= 2th − 1 . Durch eine einfache Umformung erhalten wir th ≤ (n + 1)/2. Bilden wir auf beiden Seiten den Logarithmus zur Basis t, so folgt das Theorem. Hier sehen wir die Stärke von B-Bäumen im Vergleich zu Rot-Schwarz-Bäumen. Zwar wächst die Höhe des Baumes in beiden Fällen wie O(lg n) (denken sie daran, dass t eine Konstante ist), die Basis des Logarithmus kann jedoch bei B-Bäumen viel größer sein. Folglich sparen B-Bäume gegenüber Rot-Schwarz-Bäumen bei den meisten Baumoperationen einen Faktor von etwa lg t in der Anzahl der besuchten Knoten. Da wir üblicherweise auf die Festplatte zugreifen müssen, um einen beliebigen Knoten in einem Baum zu besuchen, reduzieren B-Bäume die Anzahl der Festplattenzugriffe wesentlich.
Übungen 18.1-1 Weshalb lassen wir den minimalen Grad t = 1 nicht zu? 18.1-2 Für welche Werte von t ist der Baum in Abbildung 18.1 ein zulässiger B-Baum? 18.1-3 Zeichnen Sie alle zulässigen B-Bäume mit minimalem Grad 2, die die Menge {1, 2, 3, 4, 5} darstellen.
496
18 B-Bäume
18.1-4 Wie groß ist die maximale Anzahl von Schlüsseln, die in einem B-Baum der Höhe h gespeichert werden kann? Geben Sie die Anzahl als Funktion des minimalen Grades t an! 18.1-5 Beschreiben Sie die Datenstruktur, die entstehen würde, wenn jeder schwarze Knoten eines Rot-Schwarz-Baums seine roten Kinder absorbieren würde, indem er deren Kinder mit seinen eigenen Kindern vereint.
18.2
Grundoperationen auf B-Bäumen
In diesem Abschnitt stellen wir die Details der Operationen B-Tree-Search, B-TreeCreate und B-Tree-Insert vor. Bei diesen Prozeduren übernehmen wir die beiden folgenden Konventionen: • Die Wurzel des B-Baumes befindet sich immer im Hauptspeicher, sodass wir nie ein Disk-Read für die Wurzel ausführen brauchen; wir müssen jedoch eine DiskWrite-Operation der Wurzel ausführen, wenn der Wurzelknoten geändert wird. • Auf allen Knoten, die als Parameter übergeben werden, muss bereits eine DiskRead-Operation ausgeführt worden sein. Alle vorgestellten Prozeduren sind „Einpass“-Algorithmen, die den Baum von der Wurzel ausgehend nach unten durchlaufen, ohne zu einem bereits besuchten Knoten des Baumes zurückkehren zu müssen.
Durchsuchen eines B-Baumes Das Suchen in einem B-Baum ähnelt dem Suchen in einem binären Suchbaum, außer dass wir bei jedem Knoten statt einer binären Verzweigungsentscheidung, d. h. einer Entscheidung aus zwei Möglichkeiten, entsprechend der Anzahl der Kinder eine Verzweigungsentscheidung aus mehreren Möglichkeiten treffen. Genauer gesagt, treffen wir bei jedem inneren Knoten x eine Verzweigungsentscheidung aus x.n + 1 Möglichkeiten. Die Operation B-Tree-Search ist eine direkte Verallgemeinerung der für binäre Suchbäume definierten Prozedur Tree-Search. B-Tree-Search verwendet als Eingabe einen Zeiger auf den Wurzelknoten x eines Teilbaumes und einen Schlüssel k, nach dem in diesem Teilbaum gesucht werden soll. Der Aufruf in der höchsten Ebene ist also von der Form B-Tree-Search(T.wurzel , k). Wenn sich k im B-Baum befindet, gibt BTree-Search das aus einem Knoten y und einem Index i bestehende geordnete Paar (y, i) zurück, für das y.schl¨u ssel i = k gilt. Anderenfalls gibt die Prozedur den Wert nil zurück.
18.2 Grundoperationen auf B-Bäumen
497
B-Tree-Search(x, k) 1 i=1 2 while i ≤ x.n und k > x.schl¨u ssel i 3 i = i+1 4 if i ≤ x.n und k = = x.schl¨u ssel i 5 return (x, i) 6 elseif x.blatt 7 return nil 8 else Disk-Read(x.ci ) 9 return B-Tree-Search(x.ci , k) Mithilfe einer Prozedur zur linearen Suche bestimmen die Zeilen 1–3 den kleinsten Index i, für den k ≤ x.schl¨u ssel i gilt, oder sie setzen i auf x.n + 1. Die Zeilen 4–5 überprüfen, ob wir nun den gesuchten Schlüssel gefunden haben, und geben diesen gegebenenfalls zurück. Anderenfalls brechen die Zeilen 6–9 die Suche entweder erfolglos ab (wenn x ein Blatt ist) oder steigen rekursiv in den geeigneten Teilbaum von x ab, nachdem die notwendige Disk-Read-Operation auf diesem Kind ausgeführt wurde. Abbildung 18.1 illustriert die Arbeitsweise von B-Tree-Search. Die Prozedur besucht die schwach schattierten Knoten während einer Suche nach dem Schlüssel R. Wie bei der Prozedur Tree-Search für binäre Suchbäume bilden die Knoten, die wir während der Rekursion besuchen, einen von der Wurzel abwärts führenden einfachen Pfad. Die Prozedur B-Tree-Search greift deshalb auf O(h) = O(logt n) Festplattenseiten zu, wobei h die Höhe des B-Baumes und n die Anzahl der Schlüssel im B-Baum ist. Da x.n < 2t gilt, benötigt die while-Schleife in den Zeilen 2–3 innerhalb jedes Knotens Zeit O(t) und die gesamte CPU-Zeit ist in O(th) = O(t logt n).
Erzeugen eines leeren B-Baumes Um einen B-Baum T zu konstruieren, verwenden wir zunächst B-Tree-Create, um einen leeren Wurzelknoten zu erzeugen, und rufen dann B-Tree-Insert auf, um neue Schlüssel hinzuzufügen. Beide Prozeduren verwenden eine Hilfsprozedur AllocateNode, die in Zeit O(1) eine Festplattenseite allokiert, die als neuer Knoten benutzt wird. Wir können voraussetzen, dass für einen von Allocate-Node erzeugten Knoten keine Disk-Read-Operation erforderlich ist, da noch keine nützliche Information für diesen Knoten auf der Festplatte gespeichert ist. B-Tree-Create(T ) 1 x = Allocate-Node() 2 x.blatt = wahr 3 x.n = 0 4 Disk-Write(x) 5 T.wurzel = x B-Tree-Create benötigt O(1) Festplattenoperationen und CPU-Zeit O(1).
498
18 B-Bäume
Einfügen eines Schlüssels in einen B-Baum Das Einfügen eines Schlüssels in einen B-Baum ist wesentlich komplizierter als das Einfügen eines Schlüssels in einen binären Suchbaum. Wie bei binären Suchbäumen suchen wir nach einem Blatt, an dem der neue Schlüssel einzufügen ist. Bei einem B-Baum können wir jedoch nicht einfach einen neuen Knoten erzeugen und den Schlüssel einfügen, da der daraus resultierende Baum kein gültiger B-Baum mehr wäre. Stattdessen fügen wir den neuen Schlüssel in ein bereits existierendes Blatt ein. Da wir einen Schlüssel nicht in ein volles Blatt einfügen können, führen wir eine Operation ein, die einen vollen Knoten y (der 2t − 1 Schlüssel verwaltet) am mittleren Schlüssel y.schl¨u ssel t in zwei Knoten aufspaltet, von denen jeder nur t − 1 Schlüssel enthält. Der mittlere Schlüssel steigt in den Vater von y auf, um den Verzweigungspunkt zwischen den beiden neuen Bäumen zu kennzeichnen. Wenn aber auch der Vater von y voll ist, müssen wir diesen teilen, bevor wir den neuen Schlüssel einfügen können und so haben wir im schlechtesten Fall volle Knoten den ganzen einfachen Pfad hoch bis zur Wurzel aufzuspalten. Wie bei binären Suchbäumen können wir einen Schlüssel während eines einzigen Durchlaufs des Baumes von der Wurzel zu den Blättern einfügen. Um dies zu schaffen, dürfen wir nicht warten, bis wir herausgefunden haben, ob wir tatsächlich einen vollen Knoten aufspalten müssen, um die neue Einfügeoperation ausführen zu können. Stattdessen teilen wir jeden vollen Knoten, dem wir begegnen (einschließlich des Blattes selbst), wenn wir uns auf der Suche nach der zum neuen Schlüssel gehörigen Position den Baum abwärts bewegen. Folglich sind wir uns sicher, dass, wenn immer wir einen vollen Knoten y aufspalten, sein Vater nicht voll ist.
Aufspalten eines Knotens in einem B-Baum Die Prozedur B-Tree-Split-Child verwendet als Eingabe einen nicht vollen inneren Knoten x (von dem wir voraussetzen, dass er sich im Hauptspeicher befindet), einen Index i, sodass x.ci (von dem wir ebenfalls voraussetzen, dass er sich im Hauptspeicher befindet) ein volles Kind von x ist. Die Prozedur spaltet dann dieses Kind in zwei Teile auf und korrigiert den Knoten x so, dass er nun ein zusätzliches Kind besitzt. Um eine volle Wurzel aufzuspalten, machen wir die Wurzel zunächst zu einem Kind eines neuen leeren Wurzelknotens, sodass wir B-Tree-Split-Child verwenden können. Die Höhe des Baumes vergrößert sich damit um 1; Aufspalten ist die einzige Aktion, durch die der Baum wächst. Abbildung 18.5 illustriert diesen Prozess. Wir spalten den vollen Knoten y = x.ci an seinem mittleren Schlüssel S auf, der in y’s Vaterknoten x aufsteigt. Diejenigen Schlüssel in y, die größer als der mittlere Schlüssel sind, werden in einem neuen Knoten z untergebracht, der zu einem neuen Kind von x wird.
18.2 Grundoperationen auf B-Bäumen
499
1
-1 el i sel i s üs üs hl chl c s x x. x.s … N W …
y = x.ci
x …
1 + el i- el i el i ss üss üss ü l l l ch ch ch x.s x.s x.s N S W …
y = x.ci
z = x.ci+1
P Q R S T U V
P Q R
T U V
T1 T2 T3 T4 T5 T6 T7 T8
T1 T2 T3 T4
T5 T6 T7 T8
Abbildung 18.5: Aufspalten eines Knotens mit t = 4. Der Knoten y wird in zwei neue Knoten y und z aufgeteilt und der mittlere Schlüssel S von y wird in den Vaterknoten von y verschoben.
B-Tree-Split-Child(x, i) 1 z = Allocate-Node() 2 y = x.ci 3 z.blatt = y.blatt 4 z.n = t − 1 5 for j = 1 to t − 1 6 z.schl¨u ssel j = y.schl¨u ssel j+t 7 if nicht y.blatt 8 for j = 1 to t 9 z.cj = y.cj+t 10 y.n = t − 1 11 for j = x.n + 1 downto i + 1 12 x.cj+1 = x.cj 13 x.ci+1 = z 14 for j = x.n downto i 15 x.schl¨u ssel j+1 = x.schl¨u ssel j 16 x.schl¨u ssel i = y.schl¨u ssel t 17 x.n = x.n + 1 18 Disk-Write(y) 19 Disk-Write(z) 20 Disk-Write(x) Die Prozedur B-Tree-Split-Child arbeitet durch einfaches „Ausschneiden und Einfügen“. Hierbei ist x der Vater des Knoten, den wir aufspalten, und y ist das i-te Kind von x (Zeile 2). Der Knoten y besitzt ursprünglich 2t Kinder (2t − 1 Schlüssel), die aber durch diese Operation auf t Kinder (t − 1 Schlüssel) reduziert werden. Der Knoten z holt sich die t größten Kinder (t − 1 Schlüssel) von y und z wird ein neues Kind von x, das gleich nach y in der Tabelle der Kinder von x positioniert wird. Der mittlere Schlüssel von y steigt auf und wird zu dem Schlüssel in x, der y und z trennt.
500
18 B-Bäume
Die Zeilen 1–9 erstellen den Knoten z und weisen ihm die t−1 größten Schlüssel und die entsprechenden t Kinder von y zu. Zeile 10 passt den Schlüsselzähler für y an. Schließlich fügen die Zeilen 11–17 den Knoten z als ein Kind von x ein und verschieben den mittleren Schlüssel von y nach x, um y und z zu trennen und passen den Schlüsselzähler von x an. Die Zeilen 18–20 schreiben alle modifizierten Festplattenseiten zurück. Die von B-TreeSplit-Child benötigte CPU-Zeit ist wegen den Schleifen in den Zeilen 5–6 und 8–9 in Θ(t). (Die anderen Schleifen durchlaufen O(t) Iterationen.) Die Prozedur führt O(1) Festplattenoperationen aus.
Einfügen eines Schlüssels in einen B-Baum in einem einzigen Durchlauf Wir fügen einen Schlüssel k in einen B-Baum T der Höhe h ein, indem wir den Baum einmal abwärts durchlaufen, wobei O(h) Festplattenzugriffe erforderlich sind. Die benötigte CPU-Zeit ist O(th) = O(t logt n). Die Prozedur B-Tree-Insert verwendet B-Tree-Split-Child, um sicherzustellen, dass die Rekursion niemals zu einem vollen Knoten absteigt. B-Tree-Insert(T, k) 1 r = T.wurzel 2 if r.n = = 2t − 1 3 s = Allocate-Node() 4 T.wurzel = s 5 s.blatt = falsch 6 s.n = 0 7 s.c1 = r 8 B-Tree-Split-Child(s, 1) 9 B-Tree-Insert-Nonfull(s, k) 10 else B-Tree-Insert-Nonfull(r, k) Die Zeilen 3–9 behandeln den Fall, dass der Wurzelknoten r voll ist: Die Wurzel wird aufgespalten und ein neuer Knoten s (der zwei Kinder hat) wird zur Wurzel. Das Aufspalten der Wurzel ist die einzige Operation, bei der die Höhe eines B-Baumes vergrößert wird. Abbildung 18.6 illustriert diesen Fall. Anders als bei einem binären Suchbaum vergrößert sich die Höhe eines B-Baumes von oben statt von unten. Die Prozedur endet mit dem Aufruf B-Tree-Insert-Nonfull, um den Schlüssel k in den Baum einzufügen, der von dem nicht vollen Wurzelknoten ausgeht. Falls notwendig steigt BTree-Insert-Nonfull den Baum rekursiv ab, wobei zu jedem Zeitpunkt garantiert ist, dass der Knoten, der von der Prozedur dann besucht wird, nicht voll ist, indem B-Tree-Split-Child aufgerufen wird, wenn dies nötig ist. Die rekursive Hilfsprozedur B-Tree-Insert-Nonfull fügt den Schlüssel k in den Knoten x ein, von dem vorausgesetzt wird, dass dieser bei Aufruf der Prozedur nicht voll ist. Die Arbeitsweise von B-Tree-Insert und die rekursive Operation B-Tree-InsertNonfull stellen sicher, dass diese Voraussetzung erfüllt ist.
18.2 Grundoperationen auf B-Bäumen
501 T.wurzel
s H T.wurzel
r
r
A D F H L N P
A D F
L N P
T1 T2 T3 T4 T5 T6 T7 T8
T1 T2 T3 T4
T5 T6 T7 T8
Abbildung 18.6: Aufspalten der Wurzel für t = 4. Der Wurzelknoten r wird aufgespaltet, und ein neuer Wurzelknoten s wird erzeugt. Die neue Wurzel enthält den mittleren Schlüssel von r und besitzt die beiden Hälften von r als Kinder. Die Höhe des B-Baumes wächst um 1, wenn die Wurzel geteilt wird.
B-Tree-Insert-Nonfull(x, k) 1 i = x.n 2 if x.blatt 3 while i ≥ 1 und k < x.schl¨u ssel i 4 x.schl¨u ssel i+1 = x.schl¨u ssel i 5 i = i−1 6 x.schl¨u ssel i+1 = k 7 x.n = x.n + 1 8 Disk-Write(x) 9 else while i ≥ 1 und k < x.schl¨u ssel i 10 i = i−1 11 i = i+1 12 Disk-Read(x.ci ) 13 if x.ci .n = = 2t − 1 14 B-Tree-Split-Child(x, i) 15 if k > x.schl¨u ssel i 16 i = i+1 17 B-Tree-Insert-Nonfull(x.ci , k) Die Prozedur B-Tree-Insert-Nonfull arbeitet wie folgt. Die Zeilen 3–8 behandeln den Fall, dass x ein Blatt ist. Dann wird der Schlüssel k in den Knoten x eingefügt. Wenn x kein Blatt ist, müssen wir k in ein geeignetes Blatt des Teilbaumes einfügen, der vom inneren Knoten x ausgeht. In diesem Fall bestimmen die Zeilen 9–11 das Kind von x, zu dem die Rekursion absteigt. Zeile 13 prüft, ob die Rekursion zu einem vollen Kind absteigen würde. Trifft dies zu, ruft Zeile 14 die Prozedur B-Tree-Split-Child auf, um dieses Kind in zwei nicht volle Kinder aufzuteilen. Die Zeilen 15–16 bestimmen, zu welchem der beiden Kinder nun korrekterweise abzusteigen ist. (Beachten Sie, dass es keine Notwendigkeit für ein Disk-Read(x.ci ) gibt, nachdem Zeile 16 i inkrementiert hat, da die Rekursion in diesem Fall zu einem Kind absteigen wird, das gerade von
502
18 B-Bäume
B-Tree-Split-Child erzeugt wurde.) Insgesamt stellen die Zeilen 13–16 also sicher, dass die Prozedur niemals zu einem vollen Knoten absteigt. Zeile 17 fährt dann mit dem Einfügen von k in den passenden Teilbaum fort. Abbildung 18.7 illustriert diese verschiedenen Fälle des Einfügens in einen B-Baum. Die Anzahl der von B-Tree-Insert ausgeführten Festplattenzugriffe ist für einen BBaum der Höhe h in O(h), da lediglich O(1) Disk-Read- und Disk-Write-Operationen zwischen den Aufrufen von B-Tree-Insert-Nonfull vorkommen. Die insgesamt benötigte CPU-Zeit ist O(th) = O(t logt n). Da die Prozedur B-Tree-Insert-Nonfull endrekursiv ist, können wir sie alternativ als while-Schleife implementieren. Dies zeigt, dass die Anzahl der gleichzeitig im Hauptspeicher zu haltenden Seiten in O(1) ist.
Übungen 18.2-1 Zeigen Sie, wie jeweils der B-Baum aussieht, wenn wir die Schlüssel F, S, Q, K, C, L, H, T, V, W, M, R, N, P, A, B, X, Y, D, Z, E in dieser Reihenfolge in einen leeren B-Baum mit minimalem Grad 2 einfügen. Zeichnen Sie nur die Konfigurationen des Baumes, unmittelbar bevor ein Knoten aufgeteilt werden muss. Zeichnen Sie auch die Endkonfiguration. 18.2-2 Erklären Sie, unter welchen Umständen, wenn überhaupt, beim Ausführen des Aufrufs B-Tree-Insert redundante Disk-Read- oder Disk-Write-Operationen vorgenommen werden. (Ein redundantes Disk-Read liegt vor, wenn Disk-Read für eine Seite aufgerufen wird, die sich bereits im Hauptspeicher befindet. Ein redundantes Disk-Write speichert eine Festplattenseite mit Informationen zurück, die identisch zu den dort bereits gespeicherten Informationen sind.) 18.2-3 Erklären Sie, wie der im B-Baum gespeicherte minimale Schlüssel bestimmt werden kann und wie der Vorgänger eines gegebenen Schlüssels bestimmt wird, der im B-Baum gespeichert ist. 18.2-4∗ Nehmen Sie an, wir würden die Schlüssel {1, 2, . . . , n} in einen leeren B-Baum mit minimalem Grad 2 einfügen. Wie viele Knoten besitzt der finale B-Baum? 18.2-5 Da die Blätter keine Zeiger auf Kinder benötigen, könnten sie einen anderen (größeren) Wert t als die inneren Knoten verwenden, unter Beibehaltung der Größe der Festplattenseiten. Zeigen Sie, wie die Prozeduren für das Erzeugen und das Einfügen in einen B-Baum zu modifizieren sind, um diese Variante zu realisieren. 18.2-6 Nehmen Sie an, wir würden B-Tree-Search so implementieren, dass in jedem Knoten statt linearer Suche binäre Suche verwendet wird. Zeigen Sie, dass durch diese Veränderung die benötigte CPU-Zeit O(lg n) wird, unabhängig davon, wie wir t als Funktion von n wählen.
18.2 Grundoperationen auf B-Bäumen
(a) ursprünglicher Baum A C D E
503
G M P X
J K
N O
(b) B eingefügt
R S T U V
Y Z
G M P X
A B C D E
J K
(c) Q eingefügt
N O
R S T U V
G M P T X
A B C D E
J K
N O
Q R S
(d) L eingefügt
U V
Y Z
P G M
A B C D E
T X
J K L
(e) F eingefügt
N O
Q R S
U V
Y Z
P C G M
A B
Y Z
D E F
J K L
T X N O
Q R S
U V
Y Z
Abbildung 18.7: Einfügen von Schlüsseln in einen B-Baum. Der minimale Grad t für diesen B-Baum ist 3. Somit kann ein Knoten maximal fünf Schlüssel verwalten. Die Knoten, die während des Einfügens modifiziert werden, sind jeweils schwach schattiert. (a) Der ursprüngliche Baum in diesem Beispiel. (b) Das Ergebnis des Einfügens von B in den ursprünglichen Baum. Dabei handelt es sich um ein einfaches Einfügen in ein Blatt. (c) Das Ergebnis des Einfügens von Q in den vorhergehenden Baum. Der Knoten RST U V wird in zwei Knoten geteilt, die RS und U V enthalten, der Schlüssel T wird in die Wurzel verschoben und Q wird in das linke Kind (den RS-Knoten) eingefügt. (d) Das Ergebnis des Einfügens von L in den vorhergehenden Baum. Die Wurzel wird sofort geteilt, da sie voll ist, und der B-Baum wächst um 1 in der Höhe. Dann wird L in das Blatt eingefügt, das JK enthält. (e) Das Ergebnis des Einfügens von F in den vorhergehenden Baum. Der Knoten ABCDE wird aufgespaltet, bevor F in das dann linke Kind (den Knoten DE) eingefügt wird.
504
18 B-Bäume
18.2-7 Setzen Sie voraus, dass die Festplattenhardware es uns erlaubt, die Größe einer Festplattenseite geeignet zu wählen. Die zum Lesen einer Festplattenseite benötigte Zeit sei aber a + bt, wobei a und b gegebene Konstanten sind und t der minimale Grad eines B-Baumes ist, der Seiten der ausgewählten Größe benutzt. Überlegen Sie, wie t zu wählen ist, damit die Suchzeit im B-Baum (näherungsweise) minimal wird. Schlagen Sie einen optimalen Wert für t für den Fall vor, in dem a = 5 Millisekunden und b = 10 Mikrosekunden ist.
18.3
Löschen eines Schlüssels aus einem B-Baum
Das Entfernen aus einem B-Baum erfolgt analog zum Einfügen, ist allerdings ein wenig komplizierter, da wir aus jedem beliebigen Knoten einen Schlüssel löschen können – nicht nur aus einem Blatt – und wir die Kinder eines inneren Knoten neu ordnen müssen, wenn wir aus diesem Knoten einen Schlüssel gelöscht haben. Wie beim Einfügen müssen wir absichern, dass das Löschen keinen Baum erzeugt, dessen Struktur die B-BaumEigenschaften verletzt. Genau wie wir sicherzustellen hatten, dass kein Knoten durch das Einfügen zu groß wurde, müssen wir nun dafür sorgen, dass kein Knoten aufgrund des Löschens zu klein wird (nur die Wurzel darf weniger als die minimale Anzahl t−1 von Schlüsseln haben). Genau wie ein einfacher Algorithmus zum Einfügen eines Knotens gegebenenfalls den Baum hochlaufen muss, wenn ein Knoten auf dem Pfad zu der Stelle, an der der Schlüssel eingefügt werden muss, voll ist, muss ein einfacher Algorithmus zum Löschen eines Knotens den Baum hochlaufen, wenn ein (von der Wurzel verschiedener) Knoten auf dem Pfad zu der Stelle, an der der Knoten zu löschen ist, die minimale Anzahl von Schlüssels hat. Die Prozedur B-Tree-Delete löscht den Schlüssel k aus dem von x ausgehenden Teilbaum. Wir entwerfen diese Prozedur so, dass garantiert ist, dass die Anzahl der Schlüssel in einem Knoten x wenigstens gleich dem minimalen Grad t ist, wenn immer die Prozedur am Knoten x rekursiv absteigt. Beachten Sie, dass diese Bedingung einen Schlüssel mehr fordert als in der B-Baum-Bedingung geforderte Minimum, sodass manchmal ein Schlüssel in einen Kindknoten verschoben werden muss, bevor die Rekursion zu diesem Kind absteigt. Diese verschärfte Bedingung erlaubt es uns, einen Schlüssel aus dem Baum innerhalb eines Abstieges zu entfernen, ohne dass wir wieder „hochsteigen“ müssen (mit einer Ausnahme, die wir noch erläutern werden). Sie sollten die folgende Spezifikation des Löschens eines internen Knotens mit dem folgenden Verständnis interpretieren: Wenn ein Wurzelknoten x zu einem inneren Knoten wird, der keine Schlüssel besitzt (diese Situation kann in den unten aufgeführten Fällen 2c und 3b auftreten), dann löschen wir x und das einzige Kind x.c1 von x wird zur neuen Wurzel des Baumes. Dadurch verringert sich die Höhe des Baumes um 1 und die Eigenschaft, dass die Wurzel eines Baumes mindestens einen Schlüssel enthält, bleibt erhalten (es sei denn der Baum ist leer).
18.3 Löschen eines Schlüssels aus einem B-Baum
(a) initialer Baum
505
P C G M
A B
D E F
T X
J K L
N O
(b) F entfernt: Fall 1
Q R S
D E
T X
J K L
N O
(c) M entfernt: Fall 2a
Q R S
D E
Y Z
J K
T X N O
(d) G entfernt: Fall 2c
Q R S
D E J K
U V
Y Z
P
C L A B
U V
P
C G L A B
Y Z
P
C G M A B
U V
T X N O
Q R S
U V
Y Z
Abbildung 18.8: Löschen von Schlüsseln aus einem B-Baum. Der minimale Grad für diesen B-Baum ist t = 3. Somit kann ein Knoten (abgesehen von der Wurzel) nicht weniger als 2 Schlüssel besitzen. Modifizierte Knoten sind schwach schattiert. (a) Der B-Baum aus Abbildung 18.7(e). (b) Löschen von F . Hier handelt es sich um Fall 1: das einfache Entfernen aus einem Blatt. (c) Löschen von M . Hier handelt es sich um Fall 2a: Der Vorgänger L von M wird nach oben verschoben und nimmt die Position von M ein. (d) Löschen von G. Hier handelt es sich um Fall 2c: wir schieben G nach unten, um den Knoten DEGJK zu bilden, und löschen G aus diesem Blatt (Fall 1).
506
18 B-Bäume
(e) D entfernt: Fall 3b C L P T X A B
E J K
(e′) Baum schrumpft A B
E J K
(f) B entfernt: Fall 3a A C
J K
N O
Q R S
U V
Y Z
U V
Y Z
C L P T X N O
Q R S
E L P T X N O
Q R S
U V
Y Z
Abbildung 18.8, fortgesetzt: (e) Löschen von D. Hier handelt es sich um Fall 3b: Die Rekursion kann nicht zum Knoten CL absteigen, da dieser nur 2 Schlüssel besitzt. Wir schieben P nach unten und fassen ihn mit CL und T X zu dem Knoten CLP T X zusammen; danach löschen wir D aus dem Blatt (Fall 1). (e ) Nach (e) löschen wir die Wurzel und die Höhe des Baumes verringert sich um 1. (f ) Löschen von B. Hier handelt es sich um den Fall 3a: C nimmt die Position von B ein und E die von C.
Wir skizzieren die Arbeitsweise der Prozedur zum Löschen eines Knotens, ohne den dazugehörigen Pseudocode anzugeben. Abbildung 18.8 illustriert die verschiedenen Fälle für das Löschen von Schlüsseln aus einem B-Baum. 1. Wenn sich der Schlüssel k in einem Knoten x befindet und x ein Blatt ist, dann entfernen wir den Schlüssel k aus x. 2. Wenn sich der Schlüssel k in x befindet und x ein innerer Knoten ist, dann gehen wir wie folgt vor. a. Wenn das Kind y, das im Knoten x vor dem Schlüssel k steht, mindestens t Schlüssel besitzt, dann bestimmen wir den Vorgänger k von k in dem von y ausgehenden Teilbaum. Wir löschen k rekursiv und ersetzen in x k durch k . (Das Finden und Löschen von k kann in einem einzigen Abstieg ausgeführt werden.) b. Wenn y weniger als t Schlüssel enthält, dann betrachten wir analog zu dem vorherigen Fall das Kind z, das im Knoten x hinter dem Schlüssel k steht. Wenn z mindestens t Schlüssel hat, so bestimmen wir den Nachfolger k von k in dem von z ausgehenden Teilbaum. Wir löschen k rekursiv und ersetzen in x k durch k . (Das Finden und Löschen von k kann in einem einzigen Abstieg ausgeführt werden.) c. Anderenfalls, wenn sowohl y als auch z nur t − 1 Schlüssel haben, legen wir k und den Inhalt von z nach y, sodass x sowohl k als auch den Zeiger auf z verliert und
Problemstellungen zu Kapitel 18
507
y dann 2t − 1 Schlüssel hat. Wir geben anschließend z frei und löschen k rekursiv aus y. 3. Wenn der Schlüssel nicht in dem inneren Knoten x vorhanden ist, bestimmen wir die Wurzel x.ci des passenden Teilbaumes, der k enthalten muss, wenn sich k überhaupt im Baum befindet. Wenn x.ci nur t − 1 Schlüssel besitzt, dann führen wir Schritt 3a oder 3b aus, um sicherzustellen, dass wir zu einem Knoten absteigen, der mindestens t Schlüssel enthält. Um den Fall abzuschließen, steigen wir rekursiv zu dem passenden Kind von x ab. a. Wenn x.ci nur t−1 Schlüssel hat, aber einen unmittelbaren Bruder mit mindestens t Schlüsseln besitzt, dann ordnen wir x.ci einen zusätzlichen Schlüssel zu, indem wir einen Schlüssel aus x nach x.ci , einen Schlüssel aus dem unmittelbaren linken oder rechten Bruder von x.ci nach x und den entsprechenden Zeiger auf das Kind vom Bruder nach x.ci verschieben. b. Wenn x.ci und die beiden unmittelbaren Brüder von x.ci genau t−1 Schlüssel besitzen, dann legen wir x.ci mit einem Bruder zusammen. Hierzu müssen wir einen Schlüssel aus x nach unten in den neuen zusammengesetzten Knoten verschieben, sodass dieser der mittlere Schlüssel dieses Knotens ist. Da sich in einem B-Baum die meisten Schlüssel in den Blättern befinden, können wir erwarten, dass die Entferne-Operation in der Praxis meistens benutzt wird, um Schlüssel aus den Blättern zu löschen. Die Prozedur B-Tree-Delete arbeitet dann in einem Abwärtslauf durch den Baum, ohne jemals zwischendurch aufsteigen zu müssen. Wenn jedoch ein Schlüssel eines inneren Knotens entfernt wird, läuft die Prozedur den Baum zwar auch abwärts, muss jedoch möglicherweise zu dem Knoten wieder hochsteigen, aus dem der Schlüssel gelöscht worden ist, um ihn durch seinen Vorgänger oder seinen Nachfolger zu ersetzen (Fälle 2a und 2b). Obwohl diese Prozedur kompliziert erscheint, beinhaltet sie für einen B-Baum der Höhe h nur O(h) Festplattenoperationen, da lediglich O(1) Aufrufe von Disk-Read und Disk-Write zwischen den rekursiven Aufrufen der Prozedur gemacht werden. Die benötigte CPU-Zeit liegt in O(th) = O(t logt n).
Übungen 18.3-1 Zeichnen Sie jeweils den B-Baum nach dem Löschen von C, P und V (in dieser Reihenfolge) aus dem Baum aus Abbildung 18.8(f). 18.3-2 Schreiben Sie die Prozedur B-Tree-Delete in Pseudocode.
Problemstellungen 18-1 Stapel auf Sekundärspeichern Betrachten Sie die Implementierung eines Stapels auf einem Rechner, der nur in relativ geringem Umfang über schnellen Primärspeicher verfügt und einen relativ
508
18 B-Bäume großen, langsamen Festplattenspeicher besitzt. Die Operationen Push und Pop arbeiten auf Werten, die jeweils in einem Wort gespeichert werden können. Der zu unterstützende Stapel kann weit über den Umfang des Primärspeichers hinaus anwachsen, sodass ein Großteil des Stapels auf der Festplatte gespeichert werden muss. Eine einfache, aber ineffiziente Implementierung des Stapels hält den gesamten Stapel auf der Festplatte. Wir halten im Primärspeicher einen Stapelzeiger, bei dem es sich um die Adresse des obersten Elements des Stapels handelt. Wenn der Zeiger den Wert p hat, ist das oberste Element das (p mod m)-te Wort der Seite
p/m der Festplatte, wobei m die Anzahl der Wörter pro Seite angibt. Um die Push-Operation zu realisieren, inkrementieren wir den Stapelzeiger, lesen die entsprechende Seite von der Festplatte in den Hauptspeicher, kopieren das abzulegende Element in das entsprechende Wort der Seite und schreiben die Seite zurück auf die Festplatte. Die Pop-Operation arbeitet ähnlich. Wir dekrementieren den Stapelzeiger, lesen die entsprechende Seite von der Festplatte und geben das oberste Element des Stapels zurück. Wir müssen die Seite nicht wieder beschreiben, da sie nicht modifiziert wurde. Da Festplattenoperationen relativ kostspielig sind, berechnen wir für jede Implementierung zwei Kostenarten: die Gesamtanzahl der Festplattenzugriffe und die gesamte CPU-Zeit. Jeder Festplattenzugriff auf eine Seite mit m Datenwörtern zieht die Kosten eines Festplattenzugriffs und eine CPU-Zeit von Θ(m) nach sich. a. Wie hoch ist die asymptotische Anzahl der Festplattenzugriffe bei n Stapeloperationen im schlechtesten Fall, die diese einfache Implementierung verwenden? Wie hoch ist die CPU-Zeit für n Stapeloperationen? (Geben Sie Ihre Antwort in diesem und den folgenden Teilen als Funktion in m und n an.) Betrachten Sie nun eine Implementierung des Stapels, bei der wir eine Seite des Stapels im Hauptspeicher halten. (Wir benötigen außerdem noch ein wenig Hauptspeicher, um uns zu merken, welche Seite sich aktuell im Hauptspeicher befindet.) Wir können die Stapeloperation nur ausführen, wenn sich die fragliche Festplattenseite im Hauptspeicher befindet. Falls notwendig, können wir die Festplattenseite, die sich aktuell im Hauptspeicher befindet, auf die Festplatte zurückschreiben und die neue Seite von der Festplatte in den Hauptspeicher einlesen. Wenn sich die fragliche Festplattenseite bereits im Hauptspeicher befindet, sind keine Festplattenzugriffe erforderlich. b. Wie hoch ist die Anzahl der benötigten Festplattenzugriffe für n Push-Operationen im schlechtesten Fall? Wie hoch ist die CPU-Zeit? c. Wie hoch ist die Anzahl der benötigten Festplattenzugriffe für n Stapeloperationen im schlechtesten Fall? Wie hoch ist die CPU-Zeit? Nehmen Sie an, wir würden nun den Stapel implementieren, indem wir zwei Seiten im Hauptspeicher halten (zuzüglich einer geringen Anzahl von Datenwörtern für die „Buchhaltung“).
Kapitelbemerkungen zu Kapitel 18
509
d. Beschreiben Sie, wie die Seiten zu verwalten sind, damit die amortisierte Anzahl der Festplattenzugriffe für jede Stapeloperation O(1/m) und die amortisierte CPU-Zeit für jede Stapeloperation O(1) ist. 18-2 Vereinigen und Aufspalten von 2-3-4-Bäumen Die Operation Vereinigen verarbeitet zwei dynamische Mengen S und S sowie ein Element x mit der Eigenschaft, dass für jedes x ∈ S und x ∈ S die Ungleichung x .schl¨u ssel < x.schl¨u ssel < x .schl¨u ssel gilt. Sie gibt eine Menge S = S ∪ {x} ∪ S zurück. Die Operation Aufspalten funktioniert wie ein „inverses“ Vereinigen: Sind eine dynamische Menge S und ein Element x ∈ S gegeben, dann erzeugt die Operation eine Menge S , die aus allen Elementen von S − {x} besteht, deren Schlüssel kleiner als x.schl¨u ssel sind, und eine Menge S , die aus allen Elementen von S − {x} besteht, deren Schlüssel größer als x.schl¨u ssel sind. In dieser Problemstellung untersuchen wir, wie diese Operationen auf 2-3-4Bäumen zu implementieren sind. Wir setzen der Einfachheit halber voraus, dass die Elemente nur aus den Schlüsseln bestehen und die Schlüsselwerte paarweise verschieden sind. a. Zeigen Sie, wie für jeden Knoten x in einem 2-3-4-Baum die Höhe des von x ausgehenden Teilbaumes durch ein Attribut x.h¨o he verwaltet werden kann. Stellen Sie sicher, dass Ihre Implementierung die asymptotische Laufzeit für das Suchen, das Einfügen und das Löschen nicht berührt. b. Zeigen Sie, wie das Vereinigen zu implementieren ist. Sind die zwei 2-3-4Bäume T und T sowie ein Schlüssel k gegeben, so sollte die Prozedur für das Vereinigen in Zeit O(1 + |h − h |) laufen, wobei h und h die Höhen von T bzw. T sind. c. Betrachten Sie den einfachen Pfad p von der Wurzel eines 2-3-4-Baumes T zu einem gegebenen Schlüssel k, die Menge der Schlüssel S in T , die kleiner als k sind, und die Menge der Schlüssel S in T , die größer als k sind. Zeigen } und eine Sie, dass p die Menge S in eine Menge von Bäumen {T0 , T1 , . . . , Tm Menge von Schlüsseln {k1 , k2 , . . . , km } teilt. Dabei gilt für i = 1, 2, . . . , m die und z ∈ Ti . Ungleichung y < ki < z für jeden beliebigen Schlüssel y ∈ Ti−1 Wie ist die Beziehung zwischen den Höhen von Ti−1 und Ti ? Beschreiben Sie, wie p die Menge S in eine Menge von Bäumen und Schlüsseln aufteilt. d. Zeigen Sie, wie das Aufspalten von T zu implementieren ist. Benutzen Sie die Operation zum Vereinigen, um die Schlüssel aus S in einem einzigen 2-3-4Baum T und die Schlüssel aus S in einem einzigen 2-3-4-Baum T zu sammeln. Die Laufzeit der Operation zum Aufspalten sollte O(lg n) sein, wobei n die Anzahl der Schlüssel in T ist. (Hinweis: Die Kosten für das Verschmelzen sollten eine Teleskopreihe bilden.)
Kapitelbemerkungen Knuth [211], Aho, Hopcroft und Ullman [5] sowie Sedgewick [306] liefern weiterführende Diskussionen zum Entwurf balancierter Bäume und zu B-Bäumen. Comer [74]
510
18 B-Bäume
bietet einen umfassenden Überblick zu B-Bäumen. Guibas und Sedgewick [155] diskutieren die Beziehungen zwischen verschiedenen Arten des Entwurfs balancierter Bäume, einschließlich Rot-Schwarz-Bäumen und 2-3-4-Bäumen. Im Jahre 1970 entwickelte J. E. Hopcroft 2-3-Bäume, die Vorgänger der B-Bäume und der 2-3-4-Bäume, bei denen jeder innere Knoten entweder zwei oder drei Kinder besitzt. Bayer und McCreight [35] führten B-Bäume im Jahre 1972 ein; sie haben die Wahl des Namens nicht kommentiert. Bender, Demaine und Farach-Colton [40] untersuchten, wie B-Bäume zu gestalten sind, damit sie unter Berücksichtigung von Speicherhierarchie-Effekten gut arbeiten. Ihre Cache-oblivious-Algorithmen arbeiten effizient, ohne explizit die Datentransfergrößen innerhalb der Speicherhierarchie zu kennen.
19
Fibonacci-Heaps
Der Fibonacci-Heap ist eine Datenstruktur, die zwei Zielsetzungen unterstützt. Zum einen unterstützt sie eine Menge von Operationen, die einen „fusionierbaren Heap“ kennzeichnen. Zum anderen haben einige Operationen auf Fibonacci-Heaps konstante amortisierte Laufzeiten, sodass diese Datenstruktur für Anwendungen, die diese Operationen häufig ausführen, gut geeignet ist.
Fusionierbare Heaps Ein fusionierbarer Heap ist eine Datenstruktur, die die folgenden fünf Operationen unterstützt, in der jedes Element einen Schlüssel besitzt: Make-Heap() erzeugt einen neuen Heap, der keine Elemente enthält, und gibt diesen zurück. Insert(H, x) fügt ein Element x, dessen Schlüssel bereits einen Wert zugewiesen bekommen hat, in den Heap H ein. Minimum(H) gibt einen Zeiger auf das Element im Heap H zurück, dessen Schlüssel minimal ist. Extract-Min(H) entfernt das Element mit dem minimalen Schlüssel aus dem Heap H, wobei ein Zeiger auf das Element zurückgegeben wird. Union(H1 , H2 ) erzeugt einen neuen Heap, der alle Elemente der Heaps H1 und H2 enthält, und gibt diesen zurück. Die Heaps H1 und H2 werden durch diese Operation „zerstört“. Neben den oben aufgezählten Operationen fusionierbarer Heaps unterstützen FibonacciHeaps auch die folgenden zwei Operationen Decrease-Key(H, x, k) weist dem Element x innerhalb des Heaps H den neuen Schlüsselwert k zu, von dem wir voraussetzen, dass er nicht größer als der aktuelle Schlüsselwert ist.1 Delete(H, x) löscht das Element x aus dem Heap H. 1 Wie in der Einführung zu Teil V erwähnt, sind unsere fusionierbaren Heaps standardmäßig fusionierbare Min-Heaps, sodass wir die Operationen Minimum, Extract-Min und Decrease-Key anwenden. Alternativ könnten wir fusionierbare Max-Heaps mit den Operationen Maximum, ExtractMax und Increase-Key definieren.
512
Prozedur Make-Heap Insert Minimum Extract-Min Union Decrease-Key Delete
19 Fibonacci-Heaps binärer Heap (im schlechtesten Fall) Θ(1) Θ(lg n) Θ(1) Θ(lg n) Θ(n) Θ(lg n) Θ(lg n)
Fibonacci-Heap (amortisiert) Θ(1) Θ(1) Θ(1) O(lg n) Θ(1) Θ(1) O(lg n)
Abbildung 19.1: Laufzeiten für Operationen auf den zwei Implementierungen fusionierbarer Heaps. n bezeichnet die Anzahl der Elemente in den Heaps zum Zeitpunkt, zu dem die Operation ausgeführt wird.
Wie die Tabelle in Abbildung 19.1 zeigt, arbeiten gewöhnliche binäre Heaps ziemlich gut, wenn wir, wie beispielsweise in Heapsort (Kapitel 6), die Operation Union nicht benötigen. Alle Operationen außer Union laufen auf einem binären Heap im schlechtesten Fall in Zeit O(lg n).Wenn jedoch die Operation Union ebenfalls unterstützt werden soll, arbeiten binäre Heaps schlecht. Konkatenieren wir die beiden Felder, die die zu fusionierenden binären Heaps enthalten und lassen dann Build-Min-Heap (siehe Abschnitt 6.3) laufen, so benötigt die Operation Union im schlechtesten Fall Zeit Θ(n). Demgegenüber erzielen Fibonacci-Heaps bessere asymptotische Zeitschranken für die Operationen Insert, Union und Decrease-Key als binäre Heaps und haben jeweils die gleichen asymptotischen Laufzeiten für die restlichen Operationen. Beachten Sie aber, dass es sich bei den in Abbildung 19.1 angegebenen Laufzeiten für Fibonacci-Heaps um amortisierte Laufzeiten handelt und nicht um Laufzeiten im schlechtesten Fall. Die Operation Union benötigt nur konstante amortisierte Laufzeit in einem FibonacciHeap, was signifikant besser ist als die in einem binären Heap benötigte lineare Laufzeit im schlechtesten Fall (wobei wir natürlich hier davon ausgehen, dass eine amortisierte Zeitschranke für die Anwendung ausreichend ist).
Fibonacci-Heaps in Theorie und Praxis Von einem theoretischen Standpunkt aus gesehen, sind Fibonacci-Heaps ganz besonders hilfreich, wenn die Anzahl der auszuführenden Extract-Min- und Delete-Operationen klein im Verhältnis zur Anzahl der anderen auszuführenden Operationen ist. Diese Situation tritt bei vielen Anwendungen auf. Beispielsweise rufen bestimmte Algorithmen für Graphprobleme Decrease-Key einmal pro Kante auf. Bei dicht besetzten Graphen, d. h. Graphen mit vielen Kanten, summiert sich die amortisierte Laufzeit O(1) jedes Aufrufs von Decrease-Key zu einer großen Verbesserung gegenüber der Laufzeit Θ(lg n) im schlechtesten Fall von binären Heaps. Schnelle Algorithmen für Probleme wie das Berechnen minimaler aufspannender Bäume (Kapitel 23) und das Bestimmen kürzester Pfade mit einem einzigen gegebenen Startknoten (Kapitel 24) machen wesentlichen Gebrauch von Fibonacci-Heaps.
19.1 Die Struktur von Fibonacci-Heaps
513
Unter praktischen Gesichtspunkten sind Fibonacci-Heaps für die meisten Anwendungen jedoch aufgrund der konstanten Faktoren und der Komplexität der Programmierung weniger beliebt als gewöhnliche binäre (oder k-näre) Heaps (es sei denn die Anwendungen haben eine große Anzahl von Daten zu verwalten). Folglich sind Fibonacci-Heaps vorrangig von theoretischem Interesse. Wenn eine viel einfachere Datenstruktur mit denselben amortisierten Zeitschranken wie Fibonacci-Heaps entwickelt werden würde, dann wäre diese auch von praktischem Nutzen. Sowohl binäre Heaps als auch Fibonacci-Heaps sind ineffizient in Bezug auf wie sie die Operation Search unterstützen; es kann eine Zeit lang dauern, um ein Element mit einem gegebenen Schlüssel zu finden. Aus diesem Grunde verlangen Operationen wie Decrease-Key und Delete, die auf ein gegebenes Element zugreifen müssen, einen Zeiger auf das Element als Eingabeparameter. Wie in unserer Diskussion zu Prioritätswarteschlangen in Abschnitt 6.5, wo wir in einer Anwendung einen fusionierbaren Heap benötigten, bereits andiskutiert, speichern wir häufig in jedem Element des fusionierbaren Heaps ein Handle (Verweis oder Bezeichner) auf das dazugehörige Anwendungsobjekt als auch in jedem Anwendungsobjekt ein Handle auf das dazugehörige Element im fusionierbaren Heap. Die genaue Natur von diesen Handles hängt von der Anwendung und ihrer Implementierung ab. Wie mehrere andere Datenstrukturen, die wir gesehen haben, basieren Fibonacci-Heaps auf gewurzelten Bäumen, d. h. Bäumen mit einem ausgewiesenen Wurzelknoten. Wir stellen jedes Element durch einen Knoten im Baum dar und jeder Knoten besitzt ein Attribut schl¨u ssel . Im Rest dieses Kapitels werden wir den Begriff „Knoten“ anstelle des Begriffs „Element“ benutzen. Wir werden auch nicht auf die Aufgaben, Knoten zu allokieren, bevor sie eingefügt werden, und Knoten freizugeben, nachdem sie gelöscht worden sind, eingehen, davon ausgehend, dass der Aufrufcode der entsprechenden HeapProzeduren sich um diese Details kümmert. Abschnitt 19.1 definiert Fibonacci-Heaps, diskutiert, wie wir sie darstellen, und stellt die für deren amortisierte Analyse benutzte Potentialfunktion vor. Abschnitt 19.2 zeigt, wie die Operationen der fusionierbaren Heaps zu implementieren und die in Abbildung 19.1 gezeigten amortisierten Zeitschranken zu erreichen sind. Die verbleibenden beiden Operationen Decrease-Key und Delete werden in Abschnitt 19.3 vorgestellt. Abschnitt 19.4 vollendet den Hauptbestandteil der Analyse und erklärt den Ursprung des ausgefallenen Namens der Datenstruktur.
19.1
Die Struktur von Fibonacci-Heaps
Ein Fibonacci-Heap besteht aus einer Menge von gewurzelten Bäumen, die MinHeap-geordnet sind, d. h. jeder Baum genügt der Min-Heap-Eigenschaft: der Schlüssel eines Knoten ist größer oder gleich dem Schlüssel seines Vaters. Abbildung 19.2(a) zeigt ein Beispiel für einen Fibonacci-Heap. Wie Abbildung 19.2(b) zeigt, enthält jeder Knoten x einen Zeiger x.vater auf seinen Vater und einen Zeiger x.kind auf eines seiner Kinder. Die Kinder von x sind miteinander in Form einer zirkularen doppelt verketteten Liste verknüpft, die wir als die Kindliste
514
19 Fibonacci-Heaps H.min
(a)
23
7
3 18
52
39
17 38
30
41
24 26
46
35
H.min
(b)
23
7
3
18
39
52
17
38
41
30
24
26
46
35
Abbildung 19.2: (a) Ein Fibonacci-Heap, der aus fünf Min-Heap-geordneten Bäumen und 14 Knoten besteht. Die gestrichelte Linie stellt die Wurzelliste dar. Der minimale Knoten des Heaps ist der Knoten mit dem Schlüssel 3. Markierte Knoten sind geschwärzt. Das Potential dieses speziellen Fibonacci-Heaps ist 5 + 2 · 3 = 11. (b) Eine vollständigere Darstellung, die die Zeiger vater (nach oben zeigende Pfeile), kind (nach unten zeigende Pfeile) sowie links und rechts (nach links und rechts zeigende Pfeile) veranschaulicht. Die in diesem Kapitel noch folgenden Abbildungen werden diese Details übergehen, da alle hier gezeigten Informationen aus der Darstellung in Teil (a) entnommen werden können.
von x bezeichnen. Jedes Kind y in einer Kindliste besitzt die Zeiger y.links und y.rechts, die auf den linken beziehungsweise rechten Bruder von y verweisen. Wenn der Knoten y ein Einzelkind ist, dann gilt y.links = y.rechts = y. Geschwister erscheinen in einer Kindliste in beliebiger Reihenfolge. Zirkulare doppelt verkettete Listen (siehe Abschnitt 10.2) haben hinsichtlich der Verwendung in Fibonacci-Heaps zwei Vorteile. Zum einen können wir einen Knoten an einer beliebigen Stelle einer zirkularen doppelt verketteten Liste in Zeit O(1) einfügen oder einen Knoten von einer beliebigen Stelle aus einer zirkularen doppelt verketteten Liste in Zeit O(1) löschen. Zum anderen können wir zwei gegebene zirkulare doppelt verkettete Listen in Zeit O(1) zu einer zirkularen doppelt verketteten Liste konkatenieren (oder „zusammenkleben“). Bei der Beschreibung der Operationen auf Fibonacci-Heaps werden wir auf diese Operationen informal verweisen und dem Leser die Details der Implementierung überlassen. Jeder Knoten besitzt zwei weitere Attribute. Wir speichern die Anzahl der Kinder in der Kindliste von Knoten x in dem Attribut x.grad . Das Boolesche Attribut x.marke gibt an, ob der betreffende Knoten x ein Kind verloren hat, seitdem x zum letzten
19.1 Die Struktur von Fibonacci-Heaps
515
Mal zu einem Kind eines anderen Knotens gemacht wurde. Neu erzeugte Knoten sind unmarkiert. Ein Knoten x wird zu einem unmarkierten Knoten, wenn er zu einem Kind eines anderen Knotens gemacht wird. Bis wir uns die Operation Decrease-Key in Abschnitt 19.3 ansehen, werden wir das Attribut marke eines jeden Knotens einfach auf falsch setzen. Wir greifen auf einen gegebenen Fibonacci-Heap H mithilfe eines Zeigers H.min auf die Wurzel des Baumes zu, der einen minimalen Schlüssel enthält; wir nennen diesen Knoten minimaler Knoten des Fibonacci-Heaps. Wenn mehr als eine Wurzel einen Schlüssel mit minimalen Wert hat, dann darf ein beliebiger dieser Knoten die Rolle des minimalen Knotens übernehmen. Wenn ein Fibonacci-Heap H leer ist, dann gilt H.min = nil. Die Wurzeln aller Bäume in einem Fibonacci-Heap sind über ihre Zeiger links und rechts in einer zirkularen doppelt verketteten Liste miteinander verkettet; wir nennen diese Liste Wurzelliste des Fibonacci-Heaps. Der Zeiger H.min verweist also auf den Knoten der Wurzelliste mit minimalem Schlüssel. Bäume können in beliebiger Reihenfolge in der Wurzelliste erscheinen. Wir verwenden ein weiteres Attribut bei einem Fibonacci-Heap H: die Anzahl H.n der gegenwärtig in H befindlichen Knoten.
Potentialfunktion Wir bereits erwähnt, werden wir die Potentialmethode aus Abschnitt 17.3 verwenden, um die Performanz der Fibonacci-Heap-Operationen zu analysieren. Für einen gegebenen Fibonacci-Heap H bezeichnen wir mit t(H) die Anzahl der Bäume in der Wurzelliste von H und mit m(H) die Anzahl der markierten Knoten in H. Wir definieren dann das Potential eines Fibonacci-Heaps durch Φ(H) = t(H) + 2 m(H)
(19.1)
(Wir werden in Abschnitt 19.3 ein gewisses Gespür für diese Potentialfunktion gewinnen.) Beispielsweise ist das Potential des in Abbildung 19.2 dargestellten FibonacciHeaps 5 + 2 · 3 = 11. Das Potential einer Menge von Fibonacci-Heaps wird durch die Summe der Potentiale der einzelnen Fibonacci-Heaps gebildet. Wir werden annehmen, dass eine Potentialeinheit für einen konstanten Arbeitsaufwand bezahlen kann. Dabei ist die Konstante hinreichend groß, um für die Kosten jedes spezifischen Arbeitsschrittes konstanter Zeit aufkommen zu können, der uns begegnen kann. Wir setzen voraus, dass eine Fibonacci-Heap-Anwendung ohne Heap startet. Das Anfangspotential hat deshalb den Wert 0, und mit Gleichung (19.1) ist das Potential zu allen nachfolgenden Zeiten nichtnegativ. Aus Gleichung (17.3) folgt, dass eine obere Schranke für die amortisierten Gesamtkosten auch eine obere Schranke für die tatsächlichen Gesamtkosten der Operationsfolge darstellt.
Maximaler Grad Die amortisierte Analyse, die wir nun in den restlichen Abschnitten dieses Kapitels durchführen werden, setzt voraus, dass wir eine obere Schranke D(n) für den maximalen
516
19 Fibonacci-Heaps
Grad der Knoten in einem Fibonacci-Heap mit n Knoten kennen. Wir werden es nicht beweisen, aber wenn nur die Operationen der fusionierbaren Heaps unterstützt werden, dann gilt D(n) ≤ lg n. (Die Problemstellung 19-2(d) fordert sie auf, diese Eigenschaft zu beweisen.) In den Abschnitten 19.3 und 19.4 werden wir zeigen, dass D(n) = O(lg n) gilt, wenn auch Decrease-Key und Delete unterstützt werden.
19.2
Operationen der fusionierbaren Heaps
Die Operationen fusionierbarer Heaps auf Fibonacci-Heaps verzögern die anfallende Arbeit so lang wie möglich. Die verschiedenen Operationen unterliegen einem Trade-off in Bezug auf ihre Performanz. Beispielsweise fügen wir einen Knoten ein, indem wir ihn in die Wurzelliste hinzufügen, was nur konstante Laufzeit benötigt. Wenn wir mit einem leeren Fibonacci-Heap starten und dann k Knoten einfügen, würde der FibonacciHeap einfach nur aus einer Wurzelliste mit k Knoten bestehen. Der Trade-off besteht darin, dass, wenn wir dann die Operation Extract-Min auf einem Fibonacci-Heap H ausführen, wir nach dem Löschen des Knotens, auf den H.min zeigt, uns jeden der verbliebenen k −1 Knoten der Wurzelliste anschauen müssten, um den neuen minimalen Knoten zu finden. So lange wie wir während der Operation Extract-Min durch die gesamte Wurzelliste durchgehen müssen, fügen wir Knoten in Min-Heap-geordneten Bäumen zusammen, um die Größe der Wurzelliste zu reduzieren. Wir werden sehen, dass, wie auch immer die Wurzelliste vor der Ausführung einer Operation ExtractMin aussieht, jeder Knoten der Wurzelliste nach der Ausführung einen eindeutigen Grad innerhalb der Wurzelliste hat, was zu einer Wurzelliste führt, die höchstens die Größe D(n) + 1 hat.
Erzeugen eines neuen Fibonacci-Heaps Um einen leeren Fibonacci-Heap zu erzeugen, allokiert die Prozedur Make-Fib-Heap das Fibonacci-Heap-Objekt H und gibt dieses zurück, wobei H.n = 0 und H.min = nil gilt; es gibt keine Bäume in H. Wegen t(H) = 0 und m(H) = 0 ist das Potential des leeren Fibonacci-Heaps Φ(H) = 0. Die amortisierten Kosten von Make-Fib-Heap sind deshalb gleich seinen tatsächlichen Kosten O(1).
Einfügen eines Knotens Die folgende Prozedur fügt den Knoten x in einen Fibonacci-Heap H ein, wobei vorausgesetzt wird, dass der Knoten bereits allokiert und das Attribut x.schl¨u ssel bereits belegt wurde.
19.2 Operationen der fusionierbaren Heaps
517
H.min
23
7
H.min
3
17
18 52
38
39
41
30
24 26
23
7
21
46
35
3
17
18 52 38
30
39
(a)
41
24 26
46
35
(b)
Abbildung 19.3: Einfügen eines Knotens in einen Fibonacci-Heap. (a) Ein FibonacciHeap H. (b) Der Fibonacci-Heap H, nachdem der Knoten mit dem Schlüssel 21 eingefügt wurde. Der Knoten wird sein eigener Min-Heap-geordneter Baum und wird anschließend der Wurzelliste hinzugefügt, wodurch er zum linken Bruder der Wurzel wird.
Fib-Heap-Insert(H, x) 1 x.grad = 0 2 x.vater = nil 3 x.kind = nil 4 x.marke = falsch 5 if H.min = = nil 6 erzeuge eine Wurzelliste für H, die nur aus x besteht 7 H.min = x 8 else füge x in H’s Wurzelliste ein 9 if x.schl¨u ssel < H.min.schl¨u ssel 10 H.min = x 11 H.n = H.n + 1 Die Zeilen 1–4 initialisieren einige der Strukturattribute des Knotens x. Zeile 5 testet, ob der Fibonacci-Heap leer ist. Ist er das, so erzeugen die Zeilen 6–7 eine Wurzelliste, die x als einzigen Knoten enthält, und lassen H.min auf x zeigen. Anderenfalls fügen die Zeilen 8–10 x in die Wurzelliste von H ein und aktualisieren H.min, falls dies nötig ist. Schließlich inkrementiert Zeile 11 H.n, da ein neuer Knoten in den Fibonacci-Heap eingefügt worden ist. Abbildung 19.3 zeigt, wie ein neuer Knoten mit dem Schlüssel 21 in den Fibonacci-Heap aus Abbildung 19.2 eingefügt wird. Um die amortisierten Kosten von Fib-Heap-Insert zu bestimmen, sei H der initiale und H der resultierende Fibonacci-Heap. Dann gilt t(H ) = t(H) + 1 und m(H ) = m(H), und die Potentialerhöhung beträgt ((t(H) + 1) + 2 m(H)) − (t(H) + 2 m(H)) = 1 . Da die tatsächlichen Kosten O(1) sind, gilt für die amortisierten Kosten O(1)+1 = O(1).
Finden des minimalen Knotens Der minimale Knoten eines Fibonacci-Heaps H ist durch den Zeiger H.min gegeben, sodass wir den minimalen Knoten in tatsächlicher Zeit O(1) bestimmen können. Da
518
19 Fibonacci-Heaps
sich das Potential von H nicht ändert, sind die amortisierten Kosten dieser Operation gleich den tatsächlichen Kosten O(1).
Vereinigen von zwei Fibonacci-Heaps Die folgende Prozedur vereinigt die Fibonacci-Heaps H1 und H2 , wobei H1 und H2 während des Prozesses zerstört werden. Sie konkateniert einfach nur die Wurzellisten von H1 und H2 und bestimmt anschließend den neuen minimalen Knoten. Hinterher werden die Objekte, die H1 und H2 darstellen, nie wieder benutzt. Fib-Heap-Union(H1 , H2 ) 1 H = Make-Fib-Heap() 2 H.min = H1 .min 3 konkateniere die Wurzelliste von H2 mit der Wurzelliste von H 4 if (H1 .min = = nil) oder (H2 .min = nil und H2 .min.schl¨u ssel < H1 .min.schl¨u ssel ) 5 H.min = H2 .min 6 H.n = H1 .n + H2 .n 7 return H Die Zeilen 1–3 konkatenieren die Wurzellisten von H1 und H2 zu einer neuen Wurzelliste, der Wurzelliste von H. Die Zeilen 2, 4 und 5 setzen den minimalen Knoten von H und Zeile 6 aktualisiert die Gesamtanzahl H.n der Knoten. Zeile 7 gibt den resultierenden Fibonacci-Heap H zurück. Wie schon in der Prozedur Fib-Heap-Insert bleiben alle Wurzeln Wurzeln. Die Potentialänderung beträgt Φ(H) − (Φ(H1 ) + Φ(H2 )) = (t(H) + 2 m(H)) − ((t(H1 ) + 2 m(H1 )) + (t(H2 ) + 2 m(H2 ))) =0, da t(H) = t(H1 ) + t(H2 ) und m(H) = m(H1 ) + m(H2 ) ist. Die amortisierten Kosten von Fib-Heap-Union sind deshalb gleich den tatsächlichen Kosten.
Extrahieren des minimalen Knotens Der Prozess des Extrahierens des minimalen Knotens ist die komplizierteste der in diesem Abschnitt vorgestellten Operationen. Es ist auch die Stelle, an der letztlich die bisher aufgeschobene Aufgabe der Vereinigung von Bäumen in der Wurzelliste ausgeführt wird. Der folgende Pseudocode extrahiert den minimalen Knoten. Im Code wird der Einfachheit halber angenommen, dass die in der Liste verbleibenden Zeiger aktualisiert werden, wenn ein Knoten aus einer verketteten Liste entfernt wird. Die Zeiger im extrahierten Knoten bleiben aber unverändert. Außerdem ruft er die Hilfsprozedur Consolidate auf, die wir uns in Kürze anschauen werden.
19.2 Operationen der fusionierbaren Heaps
519
Fib-Heap-Extract-Min(H) 1 z = H.min 2 if z = nil 3 for jedes Kind x von z 4 füge x zur Wurzelliste von H hinzu 5 x.vater = nil 6 entferne z aus der Wurzelliste von H 7 if z = = z.rechts 8 H.min = nil 9 else H.min = z.rechts 10 Consolidate(H) 11 H.n = H.n − 1 12 return z Wie Abbildung 19.4 zeigt, arbeitet die Prozedur Fib-Heap-Extract-Min, indem sie zunächst alle Kinder des minimalen Knotens zu Wurzeln macht und den minimalen Knoten aus der Wurzelliste entfernt. Sie konsolidiert die Wurzelliste, indem sie Wurzeln mit gleichem Grad verkettet, bis für jeden Grad höchstens eine Wurzel mit diesem Grad übrigbleibt. Wir beginnen in Zeile 1, in der wir einen Zeiger z auf den minimalen Knoten zeigen lassen; die Prozedur gibt diesen Zeiger am Ende zurück. Wenn z = nil gilt, ist der Fibonacci-Heap H bereits leer und wir sind fertig. Anderenfalls löschen wir den Knoten z aus H, indem wir in den Zeilen 3–5 alle Kinder von z zu Wurzeln in H machen (durch Einfügen in die Wurzelliste) und z in Zeile 6 aus der Wurzelliste entfernen. Wenn nach Ausführung der Zeile 6 der Knoten z sein eigener rechter Bruder ist, dann war z der einzige Knoten in der Wurzelliste und hatte keine Kinder, sodass das Einzige, das dann noch zu tun bleibt, darin besteht, den Fibonacci-Heap in Zeile 8 zu leeren, bevor z zurückgegeben wird. Anderenfalls setzen wir den Zeiger H.min auf eine von z verschiedene Wurzel (in diesem Fall auf z’s rechten Bruder), der nicht notwendigerweise der neue minimale Knoten sein braucht, wenn Fib-Heap-Extract-Min fertig abgearbeitet ist. Abbildung 19.4(b) zeigt den Fibonacci-Heap aus Abbildung 19.4(a), nachdem Zeile 9 ausgeführt wurde. Der nächste Schritt, in dem wir die Anzahl der Bäume im Fibonacci-Heap verringern, besteht im Konsolidieren der Wurzelliste von H, das durch den Prozeduraufruf Consolidate(H) ausgeführt wird. Die Konsolidierung der Wurzelliste erfolgt, indem die folgenden Schritte so lange ausgeführt werden, bis sich die Wurzeln in der Wurzelliste in ihrem Wert grad paarweise unterscheiden: 1. Finde in der Wurzelliste zwei Wurzeln x und y mit dem gleichen Grad. Sei ohne Beschränkung der Allgemeinheit x.schl¨u ssel ≤ y.schl¨u ssel . 2. Verkette y mit x: lösche y aus der Wurzelliste und mache y zu einem Kind von x, índem die Prozedur Fib-Heap-Link aufgerufen wird. Die Prozedur inkrementiert das Attribut x.grad und löscht die Marke von y.
520
19 Fibonacci-Heaps
H.min
(a)
23
7
21
H.min
3 18
17
52
38
39
24
30
26
41
(b)
23
7
21
18
52
39
46
38
17
41
30
24 26
46
35
35
0 1 2 3
0 1 2 3
A
A w,x
(c)
23
7
21
18
52
39
38
17
41
30
w,x 24 26
(d)
23
7
21
46
18
52
39
38
17
41
30
35 A w,x 7
21
18
52
39
38
17
41
30
(f)
24 26
46
7
21
23
18
52
39
38
17
41
30
35 0 1 2 3
26
46
0 1 2 3
A
w,x
w,x 7
30
24
35
A
17
46
0 1 2 3
A
(g)
26 35
0 1 2 3
w,x (e) 23
24
21 23
18 39
52
38 41
24 26 35
21
7
(h) 46 26
24
17
46
30
23
18 39
52
38 41
35
Abbildung 19.4: Die Arbeitsweise von Fib-Heap-Extract-Min. (a) Ein Fibonacci-Heap H. (b) Die Situation, nachdem der minimale Knoten z aus der Wurzelliste entfernt wurde und dessen Kinder der Wurzelliste hinzugefügt wurden. (c)–(e) Das Feld A und die Bäume nach den ersten drei Iterationen der for-Schleife in den Zeilen 4–14 der Prozedur Consolidate. Die Prozedur bearbeitet die Wurzelliste, indem sie an dem Knoten beginnt, auf den H. min zeigt, und dann den rechts -Zeigern folgt. Jedes Teilbild zeigt die Werte von w und x am Ende einer Iteration. (f )–(h) Die nächste Iteration der for-Schleife, mit den angegebenen Werten von w und x am Ende jeder Iteration der while-Schleife in den Zeilen 7–13. Teil (f ) zeigt die Situation nach dem ersten Durchlauf der while-Schleife. Der Knoten mit dem Schlüssel 23 wurde unter den Knoten mit dem Schlüssel 7 gehängt, auf den x nun zeigt. In Teil (g) ist der Knoten mit dem Schlüssel 17 unter den Knoten mit dem Schlüssel 7 gehängt worden, auf den x immer noch zeigt. In Teil (h) ist der Knoten mit dem Schlüssel 24 unter den Knoten mit dem Schlüssel 7 gehängt worden. Da A[3] bisher auf keinen Knoten verweist, wird am Ende der Iteration der for-Schleife der Zeiger von A[3] auf die Wurzel des resultierenden Baumes gesetzt.
19.2 Operationen der fusionierbaren Heaps
521
0 1 2 3
0 1 2 3
A
A
w,x 7
(i)
21
24 17 23
18
52
39
38
7
(j)
41
21
24 17 23
26 46 30
26
35
w,x 18 52
38
39
41
46 30
35 0 1 2 3
0 1 2 3
A
A x
(k)
7 24 17 23 26 46 30
18 21
38 39
(l)
7
41
52 w
26
35
w,x 38
18
24 17 23
21
46 30
52
39
41
35 H.min
(m)
7 24 17 23 26 46 30
18 21
38 39
41
52
35
Abbildung 19.4, fortgesetzt: (i)–(l) Die Situation, nach jeder der nächsten vier Iterationen der for-Schleife. (m) Der Fibonacci-Heap H nach der Rekonstruktion der Wurzelliste aus dem Feld A und der Bestimmung des neuen Zeigers H. min.
522
19 Fibonacci-Heaps
Die Prozedur Consolidate verwendet ein Hilfsfeld A[0 . . D(H.n)], um Buch über die Wurzeln hinsichtlich ihrer Grade zu führen. Wenn A[i] = y gilt, dann ist y gegenwärtig eine Wurzel mit y.grad = i. Natürlich müssen wir wissen, wie wir die obere Schranke D(H.n) für den maximalen Grad berechnen können, um das Feld zu allokieren. Wir werden in Abschnitt 19.4 sehen, wie wir das tun können. Consolidate(H) 1 sei A[0 . . D(H.n)] ein neues Feld 2 for i = 0 to D(H.n) 3 A[i] = nil 4 for jeden Knoten w aus der Wurzelliste von H 5 x=w 6 d = x.grad 7 while A[d] = nil 8 y = A[d] // ein anderer Knoten mit dem gleichen Grad wie x 9 if x.schl¨u ssel > y.schl¨u ssel 10 vertausche x mit y 11 Fib-Heap-Link(H, y, x) 12 A[d] = nil 13 d = d+1 14 A[d] = x 15 H.min = nil 16 for i = 0 to D(H.n) 17 if A[i] = nil 18 if H.min = = nil 19 erzeuge eine Wurzelliste für H, die nur aus A[i] besteht 20 H.min = A[i] 21 else füge A[i] in die Wurzelliste von H ein 22 if A[i].schl¨u ssel < H.min.schl¨u ssel 23 H.min = A[i] Fib-Heap-Link(H, y, x) 1 lösche y aus der Wurzelliste von H 2 mache y zu einem Kind von x, inkrementiere x.grad 3 y.marke = falsch Die Prozedur Consolidate arbeitet im Detail wie folgt. Die Zeilen 2–3 allokieren und initialisieren A, indem jeder Eintrag auf nil gesetzt wird. Die for-Schleife in den Zeilen 4–14 bearbeitet jede Wurzel w der Wurzelliste. Da wir Wurzeln untereinander hängen, kann w möglicherweise unter einen anderen Knoten gehängt werden und ist dann nicht länger mehr eine Wurzel. Nichtsdestotrotz ist w immer in einem Baum enthalten, dessen Wurzel ein Knoten x ist, der gleich oder ungleich w ist. Da wir höchstens einen Baum von jedem Grad haben wollen, schauen wir in dem Feld A nach, ob es eine Wurzel y gibt, die den gleichen Grad wie x hat. Wenn es einen solchen Knoten y gibt, hängen wir die Wurzeln x und y untereinander; wir garantieren aber, dass x nach diesem Schritt
19.2 Operationen der fusionierbaren Heaps
523
weiterhin eine Wurzel darstellt. Genauer gesagt, wir hängen y unter x, nachdem wir zuerst die Zeiger auf die beiden Wurzeln vertauscht haben, wenn der Schlüssel von y kleiner als der von x ist. Nachdem wir y unter x gehängt haben, ist der Grad von x um 1 größer geworden. Wir setzen diesen Prozess, x mit einer anderen Wurzel zu verknüpfen, deren Grad gleich dem neuen Grad von x ist, bis keine andere Wurzel, die wir bereits bearbeitet haben, den gleichen Grad wie x hat. Wir lassen dann die entsprechende Komponente von A auf x zeigen, um uns so zu merken, dass x die einzige Wurzel mit ihrem Grad ist, die wir bereits bearbeitet haben. Wenn diese for-Schleife terminiert, existiert höchstens eine Wurzel von jedem Grad und das Feld A zeigt auf jede verbliebene Wurzel. Die while-Schleife der Zeilen 7–13 verknüpft wiederholt die Wurzel x des Baumes, der den Knoten w enthält, mit einem anderen Baum, dessen Wurzel den gleichen Grad wie x besitzt, bis keine andere Wurzel den gleichen Grad mehr hat. Diese while-Schleife erhält folgende Invariante: Zu Beginn jeder Iteration der while-Schleife gilt d = x.grad . Wir verwenden diese Schleifeninvariante wie folgt: Initialisierung: Zeile 6 sichert, dass die Schleifeninvariante gilt, bevor die Schleife das erste Mal durchlaufen wird. Fortsetzung: In jeder Iteration der while-Schleife zeigt A[d] auf irgendeine Wurzel y. Da d = x.grad = y.grad gilt, wollen wir x und y verknüpfen. Derjenige der beiden Knoten x und y, der den kleineren Schlüssel besitzt, wird als Ergebnis der Verknüpfungsoperation zum Vater des anderen, und daher vertauschen die Zeilen 9–10 gegebenenfalls die Zeiger auf x und y. Anschließend hängen wir y mithilfe des Aufrufs Fib-Heap-Link(H, y, x) in Zeile 11 unter x. Dieser Aufruf inkrementiert x.grad , belässt y.grad aber bei d. Da der Knoten y keine Wurzel mehr ist, wird der Zeiger darauf in Zeile 12 aus dem Feld A gelöscht. Weil der Aufruf von Fib-Heap-Link den Wert von x.grad inkrementiert, stellt Zeile 13 die Invariante d = x.grad wieder her. Terminierung: Wir wiederholen die while-Schleife bis A[d] = nil gilt. In diesem Fall gibt es keine andere Wurzel mehr mit dem gleichen Grad wie x. Nachdem die while-Schleife terminiert, setzen wir in Zeile 14 A[d] auf x und führen die nächste Iteration der for-Schleife aus. Die Abbildungen 19.4(c)–(e) zeigen das Feld A und den resultierenden Baum nach den ersten drei Iterationen der for-Schleife der Zeilen 4–14. Bei der nächsten Iteration der for-Schleife erfolgen drei Verknüpfungen. Deren Resultate werden in den Abbildungen 19.4(f)–(h) dargestellt. Die Abbildungen 19.4(i)–(l) zeigen das Resultat der nächsten vier Iterationen der for-Schleife. Abschließend muss noch aufgeräumt werden. Terminiert die for-Schleife in den Zeilen 4–14, dann leert Zeile 15 die Wurzelliste und die Zeilen 16–23 rekonstruieren die Wurzelliste aus dem Feld A. Der resultierende Fibonacci-Heap ist in Abbildung 19.4(m)
524
19 Fibonacci-Heaps
dargestellt. Nach dem Konsolidieren der Wurzelliste schließt die Prozedur Fib-HeapExtract-Min, indem sie H.n in Zeile 11 dekrementiert und den Zeiger auf den gelöschten Knoten z in Zeile 12 zurückgibt. Wir sind nun in der Lage zu zeigen, dass die amortisierten Kosten für das Extrahieren des minimalen Knotens aus einem Fibonacci-Heap mit n Knoten O(D(n)) sind. Wir bezeichnen im Folgenden mit H den Fibonacci-Heap unmittelbar vor der Ausführung der Operation Fib-Heap-Extract-Min. Wir beginnen damit, uns die tatsächlichen Kosten zu überlegen, die beim Extrahieren des minimalen Knotens anfallen. Ein Beitrag von O(D(n)) kommt daher, dass es höchstens D(n) Kinder des minimalen Knotens gibt, die in Fib-Heap-Extract-Min verarbeitet werden, sowie von der Arbeit in den Zeilen 2–3 und 15–23 von Consolidate. Es muss noch der Beitrag der for-Schleife in den Zeilen 4–14 von Consolidate analysiert werden; hierzu verwenden wir eine Aggregat-Analyse. Die Größe der Wurzelliste beim Aufruf von Consolidate ist höchstens D(n) + t(H) − 1, da diese aus den ursprünglichen t(H) Knoten der Wurzelliste abzüglich des extrahierten Wurzelknotens und zuzüglich der Kinder des extrahierten Knotens besteht, deren Anzahl höchstens D(n) ist. Innerhalb einer gegebenen Iteration der for-Schleife in den Zeilen 4–14 hängt die Anzahl der Iterationen der while-Schleife in den Zeilen 7–13 von der Wurzelliste ab. Wir wissen aber, dass jedes Mal, wenn die while-Schleife durchlaufen wird, eine der Wurzeln unter eine andere gehängt wird, und somit die Gesamtanzahl der Iterationen der while-Schleife über alle Iterationen der for-Schleife höchstens gleich der Anzahl der Wurzeln in der Wurzelliste ist. Folglich ist der gesamte, innerhalb der for-Schleife geleistete Arbeitsaufwand höchstens proportional zu D(n)+t(H). Somit ist der gesamte Arbeitsaufwand beim Extrahieren des minimalen Knotens O(D(n) + t(H)). Das Potential vor dem Extrahieren des minimalen Knotens ist t(H) + 2 m(H). Das Potential nach der Ausführung der Operation ist höchstens (D(n) + 1) + 2 m(H), da höchstens D(n) + 1 Wurzeln erhalten bleiben und keine Knoten während der Operation markiert werden. Die amortisierten Kosten betragen deshalb höchstens O(D(n) + t(H)) + ((D(n) + 1) + 2 m(H)) − (t(H) + 2 m(H)) = O(D(n)) + O(t(H)) − t(H) = O(D(n)) , da wir die Einheiten des Potentials so skalieren können, dass die in O(t(H)) versteckte Konstante dominiert wird. Anschaulich erklärt werden die Kosten für das Ausführen jeder Verknüpfung von der Reduktion des Potentials bezahlt, die sich daraus ergibt, dass die Verknüpfung die Anzahl der Wurzeln um 1 reduziert. Wir werden in Abschnitt 19.4 sehen, dass D(n) = O(lg n) gilt, sodass die amortisierten Kosten für das Extrahieren des minimalen Knotens O(lg n) sind.
Übungen 19.2-1 Stellen Sie den Fibonacci-Heap dar, der sich aus dem Aufruf von Fib-HeapExtract-Min angewendet auf den in Abbildung 19.4(m) gezeigten FibonacciHeap ergibt.
19.3 Verringern eines Schlüssels und Entfernen eines Knotens
19.3
525
Verringern eines Schlüssels und Entfernen eines Knotens
In diesem Abschnitt zeigen wir, wie der Schlüssel eines Knotens in einem FibonacciHeap in amortisierter Zeit O(1) verringert werden kann und wie ein beliebiger Knoten aus einem Fibonacci-Heap mit n Knoten in amortisierter Zeit O(D(n)) entfernt werden kann. In Abschnitt 19.4 werden wir zeigen, dass der maximale Grad D(n) in O(lg n) ist; dies impliziert, dass Fib-Heap-Extract-Min und Fib-Heap-Delete in amortisierter Zeit O(lg n) laufen.
Verringern eines Schlüssels Im folgenden Pseudocode für die Operation Fib-Heap-Decrease-Key setzen wir wie zuvor voraus, dass das Entfernen eines Knotens aus einer verketteten Liste keines der strukturellen Attribute im entfernten Knoten verändert. Fib-Heap-Decrease-Key(H, x, k) 1 if k > x.schl¨u ssel 2 error “der neue Schlüssel ist größer als der aktuelle Schlüssel” 3 x.schl¨u ssel = k 4 y = x.vater 5 if y = nil und x.schl¨u ssel < y.schl¨u ssel 6 Cut(H, x, y) 7 Cascading-Cut(H, y) 8 if x.schl¨u ssel < H.min.schl¨u ssel 9 H.min = x Cut(H, x, y) 1 entferne x aus der Kindliste von y, dekrementiere y.grad 2 füge x in die Wurzelliste von H ein 3 x.vater = nil 4 x.marke = falsch Cascading-Cut(H, y) 1 z = y.vater 2 if z = nil 3 if y.marke = = falsch 4 y.marke = wahr 5 else Cut(H, y, z) 6 Cascading-Cut(H, z) Die Prozedur Fib-Heap-Decrease-Key arbeitet wie folgt. Die Zeilen 1–3 sichern, dass der neue Schlüssel nicht größer als der aktuelle Schlüssel von x ist und weisen dann x
526
19 Fibonacci-Heaps
den neuen Schlüssel zu. Wenn x eine Wurzel ist oder wenn x.schl¨u ssel ≥ y.schl¨u ssel gilt, wobei y Vater von x ist, dann müssen keine strukturellen Veränderungen erfolgen, da die Min-Heap-Ordnung nicht verletzt wurde. Zeilen 4–5 testen, ob diese Bedingung erfüllt ist. Wenn die Min-Heap-Ordnung verletzt wurde, können viele Veränderungen erfolgen. Wir beginnen mit dem Trennen von x in Zeile 6. Die Prozedur Cut „durchtrennt“ die Verknüpfung zwischen x und seinem Vater y, wodurch x eine Wurzel wird. Wir verwenden die marke-Attribute, um die gewünschten Zeitschranken zu erhalten. Sie speichern ein kleines Stück der Geschichte eines jeden Knoten. Setzen Sie voraus, dass die folgenden Ereignisse auf Knoten x zutreffen: 1. x war zu einer Zeit eine Wurzel, 2. dann wurde x als Kind unter einen anderen Knoten gehängt, 3. dann wurden zwei Kinder von x getrennt. Sobald das zweite Kind verloren gegangen ist, trennen wir den Knoten x von seinem Vater, wobei wir ihn zu einer neuen Wurzel machen. Das Attribut x.marke hat den Wert wahr, wenn die Ereignisse 1 und 2 eingetreten sind und ein Kind von x getrennt wurde. Die Prozedur Cut setzt deshalb x.marke in Zeile 4 zurück, da sie Schritt 1 ausführt. (Wir können nun sehen, weshalb Zeile 3 von Fib-Heap-Link y.marke zurücksetzt: Knoten y ist mit einem anderen Knoten verknüpft worden und somit wird Schritt 2 ausgeführt. Beim nächsten Mal, wenn ein Kind von y getrennt wird, wird y.marke auf wahr gesetzt.) Wir sind noch nicht fertig, weil x das zweite von seinem Vater y getrennte Kind sein kann, seitdem y mit einem anderen Knoten verknüpft wurde. Deshalb versucht Zeile 7 von Fib-Heap-Decrease-Key, eine Kaskadentrennung auf y auszuführen. Wenn y eine Wurzel ist, dann bewirkt der Test in Zeile 2, dass die Prozedur CascadingCut einfach zurückzuspringt. Wenn der Knoten y unmarkiert ist, dann markiert ihn die Prozedur in Zeile 4, da sein erstes Kind gerade getrennt wurde, und springt zurück. Wenn y jedoch markiert ist, hat es gerade sein zweites Kind verloren; y wird in Zeile 5 getrennt und Cascading-Cut ruft sich in Zeile 6 angewendet auf den Vater z von y selbst rekursiv auf. Die Prozedur Cascading-Cut ruft sich immer wieder selbst auf, bis sie entweder eine Wurzel oder einen unmarkierten Knoten findet. Wenn alle Kaskadentrennungen stattgefunden haben, aktualisieren die Zeilen 8–9 von Fib-Heap-Decrease-Key H.min, falls dies notwendig ist, und beenden die Prozedur. Der einzige Knoten, dessen Schlüssel sich verändert hat, ist Knoten x, dessen Schlüssel verringert wurde. Somit ist der neue minimale Knoten entweder der ursprüngliche minimale Knoten oder der Knoten x. Abbildung 19.5 zeigt die Ausführung von zwei Aufrufen Fib-Heap-Decrease-Key angewendet auf den in Abbildung 19.5(a) gezeigten Fibonacci-Heap. Der erste Aufruf, der in Abbildung 19.5(b) gezeigt wird, schließt keine Kaskadentrennungen ein. Der zweite Aufruf, der in den Abbildungen 19.5(c)–(e) illustriert ist, ruft zwei Kaskadentrennungen auf.
19.3 Verringern eines Schlüssels und Entfernen eines Knotens
H.min
H.min
7
(a)
26
24
17
46
30
527
18 23
21
38 39
(b) 15
7
41
24
52
26
35
17
23
30
21
38 39
41
52
35 H.min
(c) 15
18
5
H.min
7 24
17
26
30
26
24
18 23
21
38 39
(d) 15
41
5
26
7 24
52
17 30
18 23
21
38 39
41
52
H.min
(e) 15
5
7 17 30
18 23
21
38 39
41
52
Abbildung 19.5: Zwei Aufrufe von Fib-Heap-Decrease-Key. (a) Der initiale FibonacciHeap. (b) In dem Knoten mit dem Schlüssel 46 ist der Schlüssel auf 15 verringert worden. Der Knoten wird zu einer Wurzel und sein Vater (mit Schlüssel 24), der vorher unmarkiert war, wird markiert. (c)-(e) In dem Knoten mit dem Schlüssel 35 wurde der Schlüssel auf 5 verringert. In Teil (c) wird der Knoten, nun mit dem Schlüssel 5, zu einer Wurzel. Sein Vater mit dem Schlüssel 26 ist markiert, sodass eine Kaskadentrennung auftritt. Der Knoten mit dem Schlüssel 26 wird von seinem Vater getrennt und in (d) zu einer unmarkierten Wurzel gemacht. Eine weitere Kaskadentrennung tritt auf, da der Knoten mit dem Schlüssel 24 ebenfalls markiert ist. Dieser Knoten wird von seinem Vater getrennt und in Teil (e) in eine unmarkierte Wurzel überführt. Die Kaskadentrennung endet an diesem Punkt, da der Knoten mit dem Schlüssel 7 eine Wurzel ist. (Auch wenn dieser Knoten keine Wurzel wäre, würde die Kaskadentrennung stoppen, da dieser Knoten unmarkiert ist). Teil (e) zeigt das Ergebnis der Operation FibHeap-Decrease-Key, wobei H. min auf den neuen minimalen Knoten zeigt.
528
19 Fibonacci-Heaps
Wir werden nun zeigen, dass die amortisierten Kosten von Fib-Heap-Decrease-Key nur O(1) sind. Wir beginnen mit dem Bestimmen der tatsächlichen Kosten. Die Prozedur Fib-Heap-Decrease-Key benötigt Zeit O(1) plus die Zeit für das Ausführen der Kaskadentrennungen. Nehmen Sie an, ein gegebener Aufruf von Fib-Heap-DecreaseKey würde zu c Aufrufen von Cascading-Cut führen (der Aufruf, der in Zeile 7 von Fib-Heap-Decrease-Key gemacht wird, gefolgt von den c − 1 rekursiven Aufrufen von Cascading-Cut). Jeder Aufruf von Cascading-Cut benötigt, ohne Berücksichtigung des rekursiven Abstiegs, Zeit O(1). Folglich liegen die tatsächlichen Kosten von Fib-Heap-Decrease-Key einschließlich aller rekursiven Aufrufe in O(c). Wir berechnen als nächstes die Potentialänderung. Mit H bezeichnen wir den FibonacciHeap unmittelbar vor der Ausführung der Operation Fib-Heap-Decrease-Key. Der Aufruf von Cut in Zeile 6 von Fib-Heap-Decrease-Key erzeugt einen neuen Baum mit Wurzel x und setzt das Markierungsbit von x zurück (das möglicherweise schon den Wert falsch hatte). Jeder Aufruf von Cascading-Cut, außer dem letzten, trennt einen markierten Knoten und löscht das Markierungsbit. Danach enthält der FibonacciHeap t(H) + c Bäume (die initialen t(H) Bäume, c − 1 durch die Kaskadentrennung erzeugte Bäume und der von x ausgehende Baum) und höchstens m(H) − c + 2 markierte Knoten (c − 1 Markierungen wurden durch die Kaskadentrennung zurückgesetzt und der letzte Aufruf von Cascading-Cut kann einen Knoten markiert haben.) Die Potentialänderung ist deshalb höchstens ((t(H) + c) + 2(m(H) − c + 2)) − (t(H) + 2 m(H)) = 4 − c . Folglich betragen die amortisierten Kosten von Fib-Heap-Decrease-Key höchstens O(c) + 4 − c = O(1) , da wir die Einheiten des Potentials so skalieren können, dass die in O(c) verborgenen Konstanten dominiert werden. Sie können nun verstehen, warum wir die Potentialfunktion so definiert haben, dass sie einen Term beinhaltet, der zweimal die Anzahl der markierten Knoten ist. Wenn ein markierter Knoten y durch eine Kaskadentrennung abgetrennt wird, wird dessen Markierungsbit zurückgesetzt, sodass das Potential um 2 reduziert wird. Eine Potentialeinheit bezahlt für das Trennen und das Zurücksetzen des Markierungsbits und die andere Einheit kompensiert den Anstieg um eine Potentialeinheit, der dadurch verursacht wird, dass y zu einer Wurzel wird.
Entfernen eines Knotens Der folgende Pseudocode löscht in amortisierter Zeit O(D(n)) einen Knoten aus einem Fibonacci-Heap mit n Knoten. Wir setzen voraus, dass sich gegenwärtig kein Schlüsselwert −∞ im Fibonacci-Heap befindet. Fib-Heap-Delete(H, x) 1 Fib-Heap-Decrease-Key(H, x, −∞) 2 Fib-Heap-Extract-Min(H)
19.4 Beschränkung des maximalen Grades
529
Die Prozedur Fib-Heap-Delete macht x zum minimalen Knoten im Fibonacci-Heap, indem sie ihm einen eindeutig kleinsten Schlüssel −∞ zuweist. Die Prozedur Fib-HeapExtract-Min entfernt dann den Knoten x aus dem Fibonacci-Heap. Die amortisierte Zeit von Fib-Heap-Delete ergibt sich aus der Summe der amortisierten Zeit O(1) von Fib-Heap-Decrease-Key und der amortisierten Zeit O(D(n)) von Fib-HeapExtract-Min. Da wir in Abschnitt 19.4 sehen werden, dass D(n) = O(lg n) gilt, ist die amortisierte Zeit von Fib-Heap-Delete O(lg n).
Übungen 19.3-1 Nehmen Sie an, die Wurzel x in einem Fibonacci-Heap wäre markiert. Erklären Sie, wie x zu einer markierten Wurzel werden konnte. Begründen Sie, warum es für die Analyse ohne Bedeutung ist, dass x markiert ist, sogar dann wenn es keine Wurzel ist, die zuerst mit einem anderen Knoten verknüpft war und dann ein Kind verloren hat. 19.3-2 Begründen Sie die amortisierte Zeit O(1) von Fib-Heap-Decrease-Key als mittlere Kosten pro Operation, indem Sie die Aggregate-Analyse verwenden.
19.4
Beschränkung des maximalen Grades
Um die amortisierte Zeit O(lg n) von Fib-Heap-Extract-Min und Fib-Heap-Delete zu beweisen, müssen wir zeigen, dass die obere Schranke D(n) für den Grad jedes beliebigen Knotens eines Fibonacci-Heaps mit n Knoten in O(lg n) liegt. Insbesondere werden wir zeigen, dass D(n) ≤ logφ n gilt, wobei φ der in Gleichung (3.24) definierte goldene Schnitt √ φ = (1 + 5)/2 = 1.61803 . . . ist. Die Idee der Analyse basiert auf folgender Überlegung. Für jeden Knoten x in einem Fibonacci-Heap definieren wir gr¨ oße(x) als die Anzahl der Knoten in dem von x ausgehenden Teilbaum, einschließlich x selbst. (Beachten Sie, dass sich x nicht in der Wurzelliste befinden muss – es kann irgendein Knoten sein.) Wir werden zeigen, dass gr¨ oße(x) exponentiell in x.grad ist. Denken Sie daran, dass x.grad den genauen Grad von x angibt. Lemma 19.1 Sei x ein beliebiger Knoten in einem Fibonacci-Heap, und es gelte x.grad = k. Wir bezeichnen mit y1 , y2 , . . . , yk die Kinder von x in der Reihenfolge, in der sie mit x verknüpft wurden, beginnend bei dem Knoten, der als erster mit x verknüpft wurde. Dann gilt y1 .grad ≥ 0 und yi .grad ≥ i − 2 für i = 2, 3, . . . , k. Beweis: Offensichtlich gilt y1 .grad ≥ 0.
530
19 Fibonacci-Heaps
Für i ≥ 2 stellen wir fest, dass, als yi mit x verknüpft wurde, y1 , y2 , . . . , yi−1 Kinder von x waren, sodass x.grad ≥ i − 1 gewesen sein muss. Da yi (durch Consolidate) nur dann mit x verknüpft wird, wenn x.grad = yi .grad gilt, muss zu diesem Zeitpunkt auch yi .grad ≥ i − 1 gewesen sein. Seitdem hat yi höchstens ein Kind verloren, da er (durch Cascading-Cut) von x getrennt worden wäre, wenn er zwei Kinder verloren hätte. Wir schlussfolgern, dass yi .grad ≥ i − 2 gilt. Wir kommen nun zu dem Teil der Analyse, der die Bezeichnung „Fibonacci-Heaps“ erklärt. Erinnern wir uns an Abschnitt 3.2, wo für k = 0, 1, 2, . . . die k-te FibonacciZahl durch die Rekursiongleichung ⎧ falls k = 0 , ⎨0 falls k = 1 , Fk = 1 ⎩ Fk−1 + Fk−2 falls k ≥ 2 definiert wurde. Das folgende Lemma liefert eine andere Möglichkeit, Fk auszudrücken. Lemma 19.2 Für alle ganzen Zahlen k ≥ 0 gilt Fk+2 = 1 +
k
Fi .
i=0
Beweis: Der Beweis folgt durch Induktion nach k. Wenn k = 0 ist, dann gilt
1+
0
Fi = 1 + F0
i=0
=1+0 = F2 . Als Induktionsannahme verwenden wir, dass Fk+1 = 1 + Fk+2 = Fk + Fk+1 2 = Fk +
1+
k−1
k−1 i=0
Fi gilt. Dann folgt
3 Fi
i=0
=1+
k
Fi .
i=0
19.4 Beschränkung des maximalen Grades
531
Lemma 19.3 Für alle ganzen Zahlen k ≥ 0 gilt für die (k + 2)-te Fibonacci-Zahl Fk+2 ≥ φk . Beweis: Der Beweis folgt durch Induktion nach k. Die Basisfälle sind die Fälle k = 0 und k = 1. Wenn k = 0 ist, dann haben wir F2 = 1 = φ0 , und wenn k = 1 ist, dann haben wir F3 = 2 > 1.619 > φ1 . Der Induktionsschritt erfolgt für k ≥ 2, und wir nehmen an, dass Fi+2 > φi für i = 0, 1, . . . , k − 1 gelten würde. Erinnern Sie sich daran, dass φ die positive Wurzel von Gleichung (3.23), x2 = x + 1, ist. Somit haben wir Fk+2 = Fk+1 + Fk ≥ φk−1 + φk−2 = φk−2 (φ + 1) = φk−2 · φ2 = φk .
(wegen der Induktionsannahme) (wegen Gleichung (3.23))
Das folgende Lemma und sein Korollar vervollständigen die Analyse. Lemma 19.4 Sei x ein beliebiger Knoten in einem Fibonacci-Heap und sei k = x.grad . Dann ist √ gr¨oße(x) ≥ Fk+2 ≥ φk , wobei φ = (1 + 5)/2 gilt. Beweis: Wir bezeichnen im Folgenden die minimal mögliche Größe eines beliebigen Knotens mit dem Grad k in einem Fibonacci-Heap mit sk . Offensichtlich gilt s0 = 1 und s1 = 2. Die Zahl sk ist höchstens gleich gr¨ oße(x) und, weil das Hinzufügen eines Kindes zu einem Knoten die Größe des Knotens nicht verringert, wächst der Wert von sk monoton mit k. Betrachten Sie einen Knoten z in einem beliebigen Fibonacci-Heap, oße(x) gilt, bestimmen wir für den z.grad = k und gr¨oße(z) = sk gelten. Da sk ≤ gr¨ eine untere Schranke für gr¨oße(x), indem wir eine untere Schranke für sk berechnen. Hierzu bezeichnen wir wie in Lemma 19.1 mit y1 , y2 , . . . , yk die Kinder von z in der Reihenfolge, wie sie mit z verknüpft worden sind. Um die untere Schranke für sk zu erhalten, zählen wir einen Knoten für z selbst und einen Knoten für das erste Kind y1 (für das gr¨ oße(y1 ) ≥ 1 gilt), was zu gr¨ oße(x) ≥ sk ≥2+
k
syi . grad
i=2
≥2+
k i=2
si−2
532
19 Fibonacci-Heaps
führt, wobei die letzte Zeile aus Lemma 19.1 (dass yi .grad ≥ i − 2 gilt) und der Monotonie von sk (sodass syi . grad ≥ si−2 gilt) folgt. Wir zeigen nun per Induktion nach k, dass sk ≥ Fk+2 für alle nichtnegativen ganzen Zahlen k gilt. Der Induktionsanfang für k = 0 und k = 1 ist offensichtlich. Für den Induktionsschritt nehmen wir k ≥ 2 und si ≥ Fi+2 für i = 0, 1, . . . , k − 1 an. Es gilt sk ≥ 2 +
k
si−2
i=2
≥2+
k
Fi
i=2
=1+
k
Fi
i=0
= Fk+2 ≥ φk
(wegen Lemma 19.2) (wegen Lemma 19.3) .
Damit haben wir gezeigt, dass gr¨oße(x) ≥ sk ≥ Fk+2 ≥ φk gilt.
Korollar 19.5 Der maximale Grad D(n) eines beliebigen Knotens in einem Fibonacci-Heap mit n Knoten ist O(lg n). Beweis: Sei x ein beliebiger Knoten in einem Fibonacci-Heap mit n Knoten, und sei k = x.grad . Wegen Lemma 19.4 gilt n ≥ gr¨oße(x) ≥ φk . Bilden wir auf beiden Seiten den Logarithmus zur Basis φ, erhalten wir k ≤ logφ n. (Tatsächlich gilt k ≤ logφ n , da k eine ganze Zahl ist.) Der maximale Grad D(n) eines beliebigen Knotens ist folglich O(lg n).
Übungen 19.4-1 Professor Pinocchio behauptet, dass die Höhe eines Fibonacci-Heaps mit n Knoten O(lg n) beträgt. Zeigen Sie, dass sich der Professor irrt, indem Sie für jede positive Zahl n eine Folge von Fibonacci-Heap-Operationen angeben, die einen Fibonacci-Heap erzeugt, der nur aus einem Baum besteht, der eine lineare Kette von n Knoten ist. 19.4-2 Nehmen Sie an, wir würden die Regel zur Kaskadentrennung so verallgemeinern, dass ein Knoten x von seinem Vater getrennt werden würde, sobald er sein k-tes Kind verliert, wobei k eine ganzzahlige Konstante ist. (Die Regel in Abschnitt 19.3 verwendet k = 2.) Für welche Werte von k gilt D(n) = O(lg n), ohne dass sich die amortisierten Kosten asymptotisch verändern?
Problemstellungen zu Kapitel 19
533
Problemstellungen 19-1 Alternative Implementierung für das Entfernen eines Knotens Professor Pisano hat die folgende Variante der Prozedur Fib-Heap-Delete vorgeschlagen, von der er behauptet, dass sie schneller läuft, wenn der zu löschende Knoten nicht der Knoten ist, auf den H.min zeigt. Pisano-Delete(H, x) 1 if x = = H.min 2 Fib-Heap-Extract-Min(H) 3 else y = x.vater 4 if y = nil 5 Cut(H, x, y) 6 Cascading-Cut(H, y) 7 füge die Kindliste von x zu der Wurzelliste von H hinzu 8 entferne x aus der Wurzelliste von H a. Die Behauptung des Professors, dass diese Prozedur schneller läuft, ist teilweise in der Annahme begründet, dass Zeile 7 in tatsächlicher Zeit O(1) ausgeführt werden kann. Was ist an dieser Annahme falsch? b. Geben Sie eine gute obere Schranke für die tatsächliche Laufzeit von PisanoDelete an, wenn x nicht H.min ist. Geben Sie Ihre Schranke als Funktion in x.grad und der Anzahl c der Aufrufe der Prozedur Cascading-Cut an. c. Nehmen Sie an, wir würden Pisano-Delete(H, x) aufrufen und H sei der zurückgelieferte Fibonacci-Heap. Schätzen Sie das Potential von H in Abhängigkeit von x.grad , c, t(H) und m(H) unter der Voraussetzung ab, dass der Knoten x keine Wurzel ist. d. Schlussfolgern Sie, dass die amortisierte Laufzeit von Pisano-Delete nicht besser als die von Fib-Heap-Delete ist, selbst dann nicht, wenn x = H.min gilt. 19-2 Binomiale Bäume und binomiale Heaps Ein binomialer Baum Bk ist ein rekursiv definierter, geordneter Baum (siehe Abschnitt B.5.2). Wie in Abbildung 19.6(a) dargestellt, besteht der binomiale Baum B0 aus einem einzigen Knoten. Der binomiale Baum Bk besteht aus zwei binomialen Bäumen Bk−1 , die miteinander verknüpft sind: die Wurzel des einen ist das linke Kind der Wurzel des anderen. Abbildung 19.6(b) zeigt die binomialen Bäume B0 bis B4 . a. Zeigen Sie folgende Eigenschaften für den binomialen Baum Bk : 1. 2. 3. 4.
Er besteht aus 2k Knoten. Die Höhe des Baumes ist k. Es gibt genau ki Knoten der Tiefe i für i = 0, 1, . . . , k. Die Wurzel hat den Grad k, was größer ist als der Grad von allen anderen Knoten; darüber hinaus gilt, dass, wenn wir die Kinder der Wurzel von
534
19 Fibonacci-Heaps
(a) Bk–1
Bk–1 B0
Bk
Tiefe 0 1 2
(b)
3 4 B0
B1
B2
B3
B4
(c) B0
Bk–1
B2
Bk–2
B1
Bk Abbildung 19.6: (a) Die rekursive Definition des binomialen Baumes Bk . Dreiecke stellen gewurzelte Teilbäume dar. (b) Die binomialen Bäume B0 bis B4 . Die Knotentiefen in B4 sind annotiert. (c) Eine andere Möglichkeit, sich den binomialen Baum Bk vorzustellen.
links nach rechts mit k − 1, k − 2, . . . , 0 durchnummerieren, das Kind i, wie in Abbildung 19.6(c) illustriert, die Wurzel eines Teilbaums Bi ist. Ein binomialer Heap H ist eine Menge binomialer Bäume, die die folgenden Eigenschaften erfüllen: 1. Jeder Knoten besitzt einen Schlüssel (wie bei einem Fibonacci-Heap). 2. Jeder binomiale Baum aus H erfüllt die Min-Heap-Eigenschaft. 3. Für jede nichtnegative ganze Zahl k gibt es höchstens einen binomialen Baum in H, dessen Wurzel den Grad k hat. b. Nehmen Sie an, ein binomialer Heap H bestünde insgesamt aus n Knoten. Diskutieren Sie, welche Beziehung es zwischen den binomialen Bäumen, die H enthält, und der binären Darstellung von n gibt. Überlegen Sie, dass H aus höchstens lg n + 1 binomialen Bäumen besteht.
Problemstellungen zu Kapitel 19
535
Nehmen Sie an, wir würden einen binomialen Heap wie folgt darstellen. Die linkesKind, rechter-Bruder-Darstellung aus Abschnitt 10.4 benutzen wir zur Darstellung eines jeden binomialen Baums des binomialen Heaps. Jeder Knoten enthält seinen Schlüssel, Zeiger zu seinem Vater, zu seinem linken Kind und zu seinem direkten rechten Bruder (diese Zeiger sind gegebenenfalls gleich nil) sowie seinen Grad (wie in Fibonacci-Heaps gibt der Grad an, wie viele Kinder der Knoten hat). Die Wurzeln sind in einer einfach verketteten Wurzelliste organisiert, geordnet nach dem Grad der Wurzeln (von klein nach groß), und wir greifen auf den binomialen Heap über einen Zeiger auf den ersten Knoten aus der Wurzelliste zu. c. Vervollständigen Sie die Beschreibung, wie ein binomialer Heap darzustellen ist (d. h. geben Sie die Namen der Attribute an, spezifizieren Sie, wann Attribute den Wert nil annehmen, und definieren Sie, wie die Wurzelliste genau organisiert ist), und zeigen Sie, wie die sieben Operationen, die wir in diesem Kapitel für Fibonacci-Heaps implementiert haben, auf binomialen Heaps zu implementieren sind. Jede Operation sollte eine Laufzeit im schlechtesten Fall von O(lg n) haben, wobei n die Anzahl der Knoten im binomialen Heap ist (oder im Fall der Operation Union die Anzahl der Knoten der zwei binomialen Heaps, die vereinigt werden). Die Operation Make-Heap sollte konstante Laufzeit haben. c. Nehmen Sie an, wir würden nur die für fusionierbare Heaps charakteristischen Operationen auf Fibonacci-Heaps implementieren wollen (d. h. wir wollten weder Decrease-Key noch Delete implementieren). Wie würden die Bäume in einem Fibonacci-Heap denen in einem binomialen Heap ähneln? Wie würden sie sich unterscheiden? Zeigen Sie, dass in diesem Fall der maximale Grad in einem Fibonacci-Heap mit n Knoten höchstens lg n wäre. c. Professor McGee hat eine neue auf Fibonacci-Heaps basierende Datenstruktur ausgearbeitet. Ein McGee-Heap hat die gleiche Struktur wie ein FibonacciHeap und unterstützt nur die Operationen fusionierbarer Heaps. Die Implementierungen der Operationen sind die gleichen wie die für Fibonacci-Heaps mit dem Unterschied, dass das Einfügen eines Schlüssels und das Vereinigen von zwei Heaps zum Schluss jeweils die Wurzelliste konsolidieren. Was ist die Laufzeit im schlechtesten Fall der Operationen auf McGee-Heaps? 19-3 Mehr Fibonacci-Heap-Operationen Wir wollen einen Fibonacci-Heap H so erweitern, dass zwei neue Operationen unterstützt werden, ohne die amortisierte Laufzeit der anderen Fibonacci-Operationen zu verändern. a. Die Operation Fib-Heap-Change-Key(H, x, k) ändert den Schlüssel eines Knotens x auf den Wert k. Geben Sie eine effiziente Implementierung von Fib-Heap-Change-Key an und analysieren Sie die amortisierte Laufzeit Ihrer Implementierung für den Fall, dass k größer als, kleiner als oder gleich x.schl¨u ssel ist. b. Geben Sie eine effiziente Implementierung von Fib-Heap-Prune(H, r) an, die q = min(r, H.n) Knoten aus H löscht. Sie dürfen frei wählen, welche q Knoten
536
19 Fibonacci-Heaps gelöscht werden. Analysieren Sie die amortisierte Laufzeit Ihrer Implementierung. (Hinweis: Es kann notwendig sein, dass Sie die Datenstruktur und die Potentialfunktion ändern müssen.)
19-4 2-3-4-Heaps In Kapitel 18 wurden 2-3-4-Bäume eingeführt, bei denen jeder innere Knoten (mit Ausnahme der Wurzel) über zwei, drei oder vier Kinder verfügt und jedes Blatt die gleiche Tiefe besitzt. In dieser Problemstellung werden wir 2-3-4-Heaps implementieren, die die Operationen fusionierbarer Heaps unterstützen. Der 2-3-4-Heap unterscheidet sich von einem 2-3-4-Baum in der folgenden Art und Weise. Bei 2-3-4-Heaps werden Schlüssel nur in den Blättern gespeichert, und jedes Blatt x speichert genau einen Schlüssel im Attribut x.schl¨u ssel . Die Schlüssel in den Blättern können in einer beliebigen Reihenfolge erscheinen. Jeder innere Knoten x enthält einen Wert x.kleinster_schl¨u ssel, der gleich dem kleinsten Schlüssel ist, der in dem von x ausgehenden Teilbaum gespeichert ist. Die Wurzel r enthält ein Attribut r.h¨o he, das die Höhe des Baumes angibt. 2-3-4Heaps wurden entworfen, um im Hauptspeicher gehalten zu werden, sodass keine lesenden und schreibenden Festplattenzugriffe notwendig sind. Implementieren Sie die folgenden 2-3-4-Heap-Operationen. Jede der Operationen (a)–(e) soll auf einem 2-3-4-Heap mit n Knoten in Zeit O(lg n) laufen. Die Operation Union soll in Zeit O(lg n) laufen, wobei n die Anzahl der Knoten in den zu vereinigenden Heaps ist. a. Minimum, die einen Zeiger auf das Blatt mit dem kleinsten Schlüssel zurückgibt. b. Decrease-Key, die einen Schlüssel eines gegebenen Blattes x auf einen gegebenen Wert k ≤ x.schl¨u ssel verringert. c. Insert, die ein Blatt mit dem Schlüssel k einfügt. d. Delete, die ein gegebenes Blatt x löscht. e. Extract-Min, die das Blatt mit dem kleinsten Schlüssel entnimmt. f. Union, die zwei 2-3-4-Heaps vereinigt, wobei ein einziger 2-3-4-Heap zurückgegeben wird und die Eingabeheaps zerstört werden.
Kapitelbemerkungen Fredman und Tarjan [114] führten die Fibonacci-Heaps ein. Ihre Veröffentlichung beschreibt auch die Anwendung von Fibonacci-Heaps auf folgende Probleme: die Bestimmung der kürzesten Pfade mit einem einzigen gegebenen Startknoten, die Bestimmung der kürzesten Pfade für alle Knotenpaare, die Bestimmung eines gewichteten bipartiten Matchings und die Bestimmung minimaler aufspannender Bäume. Später entwickelten Driscoll, Gabow, Shrairman und Tarjan [96] „relaxierte Heaps“ als Alternative zu Fibonacci-Heaps. Sie arbeiteten zwei Varianten relaxierter Heaps aus. Die eine ergibt dieselben amortisierten Zeitschranken wie Fibonacci-Heaps. Die andere macht es möglich, dass Decrease-Key im schlechtesten Fall (nicht amortisiert) in
Kapitelbemerkungen zu Kapitel 19
537
Zeit O(1) sowie Extract-Min und Delete im schlechtesten Fall in Zeit O(lg n) laufen. Relaxierte Heaps haben außerdem einige Vorteile gegenüber Fibonacci-Heaps bei parallelen Algorithmen. Wir verweisen zudem auf die Kapitelbemerkungen zu Kapitel 6 für andere Datenstrukturen, die schnelle Decrease-Key-Operationen unterstützen, wenn die Folge der von den Aufrufen Extract-Min zurückgegebenen Werte bezüglich der Zeit monoton steigend ist und die Daten in einem speziellen Bereich der ganzen Zahlen liegen.
20
van-Emde-Boas-Bäume
In den vorangegangenen Kapiteln haben wir Datenstrukturen gesehen, die die für Prioritätswarteschlangen charakteristischen Operationen unterstützen – binäre Heaps in Kapitel 6, Rot-Schwarz-Bäume in Kapitel 131 und Fibonacci-Heaps in Kapitel 19. In jeder dieser Datenstrukturen benötigte wenigstens eine der zentralen Operationen Zeit O(lg n), sei es im schlechtesten Fall oder amortisiert. In der Tat, da jede dieser Datenstrukturen ihre Entscheidungen auf Basis des Vergleichs von Schlüsseln trifft, folgt aus der unteren Schranke Ω(n lg n) für Sortieren aus Abschnitt 8.1, dass wenigstens eine Operation Laufzeit Ω(lg n) haben muss. Warum? Wenn wir sowohl die Operation Insert als auch die Operation Extract-Min in Zeit o(lg n) ausführen könnten, dann könnten wir n Schlüssel in Zeit o(n lg n) sortieren, indem wir n Insert-Operationen gefolgt von n Extract-Min-Operationen ausführen würden. Wir haben in Kapitel 8 jedoch auch gesehen, dass wir manchmal zusätzliche Informationen über die Schlüssel ausnutzen können, um so diese Schlüssel in Zeit o(n lg n) zu sortieren. Insbesondere können wir mit Counting-Sort n Schlüssel in Zeit Θ(n + k) sortieren, wenn jeder Schlüssel eine ganze Zahl aus dem Bereich von 0 bis k ist. Ist k = O(n), dann liegt Θ(n + k) in Θ(n). Da wir die untere Schranke von Ω(n lg n) für Sortieren umgehen können, wenn die Schlüssel ganze Zahlen aus einem eingeschränkten Bereich sind, könnten Sie sich fragen, ob wir in einem ähnlichen Szenario nicht auch jede der PrioritätswarteschlangenOperationen in Zeit o(lg n) ausführen können. In diesem Kapitel werden wir sehen, dass wir dies können: van-Emde-Boas-Bäume unterstützen jede einzelne der für Prioritätswarteschlangen charakteristischen Operationen und einige weitere Operationen in Zeit O(lg lg n). Der Haken ist, dass die Schlüssel ganze Zahlen aus dem Bereich von 0 bis n − 1 sein müssen und keine Duplikate erlaubt sind. Namentlich unterstützen van-Emde-Boas-Bäume jede der auf Seite 230 angegebenen Operationen für dynamische Mengen – Search, Insert, Delete, Minimum, Maximum, Successor und Predecessor – in Zeit O(lg lg n). In diesem Kapitel werden wir keine Diskussion um die Satellitendaten führen und richten unsere Aufmerksamkeit nur auf das Speichern der Schlüssel. Da wir uns auf die Schlüssel konzentrieren und wir nicht erlauben, dass doppelte Schlüssel gespeichert werden, werden wir anstelle der Operation Search die einfachere Operation Member(S, x) implementieren, die einen Booleschen Wert zurückgibt, der angibt, ob der Wert x gegenwärtig in der dynamischen Menge S enthalten ist. Bis jetzt haben wir den Parameter n für zwei verschiedene Zwecke benutzt: für die 1 Kapitel 13 diskutiert nicht explizit, wie Extract-Min und Decrease-Key zu implementieren sind, aber wir können diese Operationen für jede Datenstruktur, die Minimum, Delete und Insert unterstützt, leicht realisieren.
540
20 van-Emde-Boas-Bäume
Anzahl der Elemente in der dynamischen Menge und für den Bereich der erlaubten Werte. Um weitere Verwirrungen zu vermeiden, werden wir ab hier n benutzen, um die Anzahl der Elemente, die gegenwärtig in der dynamischen Menge enthalten sind, anzugeben, und u für den Bereich der erlaubten Werte, sodass jede der Operationen für vanEmde-Boas-Bäume Zeit O(lg lg u) benötigt. Wir nennen die Menge {0, 1, 2, . . . , u − 1} das Universum der Werte, die gespeichert werden können, und u die Größe des Universums. Wir setzen in diesem Kapitel voraus, dass u eine Zweierpotenz ist, d. h. u = 2k für eine ganze Zahl k ≥ 1 gilt. Abschnitt 20.1 untersucht einige einfache Ansätze, die in die richtige Richtung gehen. Wir verbessern diese Ansätze in Abschnitt 20.2, in dem wir rekursive protovan-Emde-Boas-Strukturen einführen, die aber noch nicht unser Ziel, Operationen mit Laufzeit O(lg lg u) zu implementieren, erreichen. Abschnitt 20.3 modifiziert proto-vanEmde-Boas-Strukturen zu van-Emde-Boas-Bäume und zeigt, wie jede Operation in Zeit O(lg lg u) ausgeführt werden kann.
20.1
Vorbereitende Ansätze
In diesem Abschnitt untersuchen wir verschiedene Ansätze zur Speicherung dynamischer Mengen. Wenngleich keiner dieser Ansätze die von uns gewünschten O(lg lg u) Zeitschranken erreicht, werden wir Einsichten erhalten, die uns helfen werden, vanEmde-Boas-Bäume, die wir später in diesem Kapitel einführen werden, zu verstehen.
Direkte Adressierung Direkte Adressierung, wie wir sie in Abschnitt 11.1 gesehen haben, stellt einen einfachen Ansatz zum Speichern einer dynamischen Menge dar. Da wir uns in diesem Kapitel nur auf das Speichern von Schlüsseln konzentrieren, können wir den auf direkter Adressierung basierenden Ansatz, wie in Übung 11.1-2 angegeben, vereinfachen, indem wir die dynamische Menge als Bit-Vektor speichern. Um eine dynamische Menge mit Werten aus dem Universum {0, 1, 2, . . . , u − 1} zu speichern, benötigen wir ein aus u Bits bestehendes Feld A[0 . . u − 1]. Der Eintrag A[x] enthält eine 1, wenn der Wert x gegenwärtig in der dynamischen Menge enthalten ist; anderenfalls enthält der Eintrag eine 0. Wenngleich wir mit einem Bit-Vektor jede der Operationen Insert, Delete und Member in Zeit O(1) ausführen können, benötigt jede der restlichen Operationen – Minimum, Maximum, Successor und Predecessor – Zeit Θ(u) im schlechtesten Fall, da wir uns Θ(u) Elemente anschauen müssen.2 Wenn eine Menge beispielsweise nur die Werte 0 und u − 1 enthält, dann müssen wir uns die Einträge 1 bis u − 2 anschauen, bevor wir die 1 in A[u − 1] finden, um den Nachfolger von 0 zu bestimmen.
2 Wir setzen in diesem Kapitel voraus, dass Minimum und Maximum den Wert nil zurückgeben, wenn die dynamische Menge leer ist, und Successor und Predecessor den Wert nil zurückgeben, wenn das gegebene Element keinen Nachfolger beziehungsweise keinen Vorgänger hat.
20.1 Vorbereitende Ansätze
541
1 1
1
1
1
0
1
0
1
1
1
0
0
0 0
0
1
A 0
0
1
1
1
1
0
1
0
0
0
0
1
2
3
4
5
6
7
8
9
10 11 12 13 14 15
0
1
1
Abbildung 20.1: Ein binärer Baum, der auf einem Bit-Vektor aufgesetzt ist, der für den Fall u = 16 die Menge {2, 3, 4, 5, 7, 14, 15} darstellt. Jeder innere Knoten enthält genau dann eine 1, wenn ein Blatt in seinem Teilbaum eine 1 enthält. Die Pfeile geben den Pfad an, dem gefolgt werden kann, um den Vorgänger von 14 in der Menge zu bestimmen.
Aufsetzen eines binären Baumes Wir können das lange Suchen in dem Bit-Vektor abkürzen, indem wir einen binären Baum auf den Bit-Vektor aufsetzen. Abbildung 20.1 zeigt ein Beispiel. Die Einträge des Bit-Vektors stellen die Blätter des binären Baumes dar und jeder innere Knoten enthält genau dann eine 1, wenn es in seinem Teilbaum wenigstens ein Blatt gibt, das eine 1 enthält. Anders ausgedrückt, das in einem inneren Knoten gespeicherte Bit ist das logische-Oder seiner zwei Kinder. Die Operationen, die auf dem schlichten Bit-Vektor eine Laufzeit von Θ(u) im schlechtesten Fall haben, benutzen nun die Baumstruktur: • Um den minimalen Wert in der Menge zu finden, starten wir an der Wurzel und laufen nach unten Richtung Blätter, wobei wir jedesmal zu dem am weitesten links stehenden Knoten gehen, der eine 1 enthält. • Um den maximalen Wert in der Menge zu finden, starten wir an der Wurzel und laufen nach unten Richtung Blätter, wobei wir jedesmal zu dem am weitesten rechts stehenden Knoten gehen, der eine 1 enthält. • Um den Nachfolger von x zu finden, starten wir an dem mit x indizierten Blatt und steigen den Baum hoch, bis wir von links zu einem Knoten kommen, dessen rechtes Kind z eine 1 enthält. Wir steigen dann über Knoten z nach unten ab, wobei wir immer zu dem am weitesten links stehenden Knoten gehen, der eine 1 enthält (d. h. wir suchen nach dem minimalen Wert in dem Teilbaum, dessen Wurzel das rechte Kind z ist). • Um den Vorgänger von x zu finden, starten wir an dem mit x indizierten Blatt und steigen den Baum hoch, bis wir von rechts zu einem Knoten kommen, dessen linkes Kind z eine 1 enthält. Wir steigen dann über Knoten z nach unten ab,
542
20 van-Emde-Boas-Bäume 0
1 1
1
0
1
2
3
4
5
6
7
2
3
8
9
10 11 12 13 14 15
√ u Bits √ u Bits
1
A 0 0 1 1 1 1 0 1 0 0 0 0 0 0 1 1 0
1
u bersicht 1 1 0 1 ¨
A 0 0 1 1 1 1 0 1 0 0 0 0 0 0 1 1 0
1
2
3
4
5
(a)
6
7
8
9
10 11 12 13 14 15
(b)
√ Abbildung 20.2: (a) Ein Baum vom Grad u, der auf dem gleichen Bit-Vektor wie in Abbildung 20.1 aufgesetzt wurde. Jeder innere Knoten speichert das logische-Oder der Bits seines Teilbaumes. (b) Eine Sicht auf die gleiche Struktur, wobei aber die internen Knoten der Tie√ u bersicht [i] ergibt sich aus dem fe 1 als Feld ¨ u bersicht [0 . . u − 1] dargestellt sind; der Wert ¨ √ √ logischen-Oder des Teilfeldes A[i u . . (i + 1) u − 1].
wobei wir immer zu dem am weitesten rechts stehenden Knoten gehen, der eine 1 enthält (d. h. wir suchen nach dem maximalen Wert in dem Teilbaum, dessen Wurzel das linke Kind z ist). Abbildung 20.1 zeigt den Pfad, der genommen wird, um den Vorgänger 7 des Wertes 14 zu finden. Wir erweitern die Operationen Insert und Delete entsprechend. Um einen Wert einzufügen, speichern wir eine 1 in jedem Knoten auf dem einfachen Pfad von dem entsprechenden Blatt zur Wurzel. Um einen Wert zu löschen, steigen wir von dem entsprechenden Blatt zur Wurzel auf und berechnen das Bit von jedem Knoten auf dem Pfad neu als logisches-Oder seiner zwei Kinder. Da die Höhe des Baumes lg u ist und jede der oben diskutierten Operationen höchstens einmal den Baum hochlaufen und höchstens einmal den Baum runterlaufen braucht, benötigt jede Operation Laufzeit O(lg u) im schlechtesten Fall. Dieser Ansatz ist nur geringfügig besser als einfach nur einen Rot-Schwarz-Baum zu benutzen. Wir können damit die Operation Member in Zeit O(1) ausführen. Suchen in einem Rot-Schwarz-Baum benötigt Zeit O(lg n). Damit gilt, dass, wenn die Anzahl n der gespeicherten Elemente viel kleiner als die Größe u des Universums ist, ein RotSchwarz-Baum schneller für alle anderen Operationen ist.
Aufsetzen eines Baumes mit konstanter Höhe Was passiert, wenn wir einen Baum mit einem höheren Grad auf einen Bit-Vektor aufsetzen? Lassen Sie uns voraussetzen, dass die Größe u des Universums gleich 22k für ein ganze Zahl k ist. Anstatt einen binären Baum auf den Bit-Vektor aufzusetzen, √ setzen wir einen Baum vom Grad u auf. Abbildung 20.2(a) zeigt einen solchen Baum für den gleichen Bit-Vektor wie in Abbildung 20.1. Die Höhe des resultierenden Baumes ist immer 2. Wie vorhin speichert Teil√ jeder innere Knoten das logische-Oder der Bits aus seinem √ baum, sodass die u inneren Knoten der Tiefe 1 die Gruppen von jeweils u Werten
20.1 Vorbereitende Ansätze
543
zusammenfassen. Wie √ Abbildung 20.2(b) zeigt, können wir uns diese Knoten als ein Feld ¨ u bersicht [0√ . . u − 1] vorstellen, wobei ¨ u bersicht [i] genau dann ein 1√enthält, wenn √ das Teilfeld A[i u . . (i + 1) u − 1] eine 1 enthält. Wir nennen dieses u-Bit-Teilfeld von A der i-te √ Cluster. Für einen gegebenen Wert von x, findet sich Bit A[x] in dem Cluster x/ u wieder. Nun wird Insert eine Operation √ mit Laufzeit O(1): um x einzufügen, setzen wir sowohl A[x] als auch ¨ u bersicht [ x/ u] auf 1. Wir können das Feld ¨ u bersicht benutzen, um jede der Operationen Minimum, Maximum, Successor, √ Predecessor, und Delete in Zeit O( u) auszuführen: • Um den minimalen (maximalen) Wert zu finden, bestimmen wir den am weitesten links (rechts) stehenden Eintrag in ¨ u bersicht , der eine 1 enthält. Nehmen Sie an, dies wäre ¨ u bersicht [i]. Wir führen dann eine lineare Suche nach der am weitesten links (rechts) stehenden 1 in dem i-ten Cluster durch. • Um den Nachfolger (Vorgänger) von x zu finden, suchen wir zuerst ausgehend von x rechts (links) innerhalb seines Clusters. Finden wir eine 1, so ist die Position√der ersten 1, die wir finden, das Ergebnis der Anfrage. Anderenfalls, sei i = x/ u und wir suchen rechts (links) von der i-ten Position aus innerhalb des Feldes u bersicht . Die erste Position, die eine 1 enthält, gibt den Index eines Clusters. ¨ Wir suchen in diesem Cluster nach der am weitesten links (rechts) stehenden 1. Diese Position ist der Nachfolger (Vorgänger) von x. √ • Um den Wert x zu löschen, sei i = x/ u. Wir setzen A[x] auf 0 und setzen dann ¨ u bersicht [i] auf das logische-Oder der Bits aus dem i-ten Cluster. Bei jeder der √ oben aufgezählten Operationen durchsuchen wir höchstens zwei Cluster √ mit jeweils u Bits und das Feld ¨ u bersicht und so benötigt jede Operation Zeit O( u). Auf den ersten Blick erscheint es, als ob wir uns verschlechtert hätten. Das Aufsetzen eines binären Baumes führte O(lg u), was asymptotisch √ uns zu Operationen mit Laufzeit √ schneller als Laufzeit O( u) ist. Bäume vom Grad u zu benutzen, wird jedoch eine Hauptidee bei van-Emde-Boas-Bäumen sein. Wir entwickeln die Idee in dem nächsten Abschnitt weiter.
Übungen 20.1-1 Modifizieren Sie die Datenstrukturen aus diesem Abschnitt derart, dass Schlüssel auch mehrfach auftreten dürfen. 20.1-2 Modifizieren Sie die Datenstrukturen aus diesem Abschnitt derart, dass Schlüssel mit dazugehörigen Satellitendaten unterstützt werden. 20.1-3 Bemerken Sie, dass mit den Datenstrukturen aus diesem Abschnitt die Methode, wie wir den Nachfolger und den Vorgänger eines Wertes x bestimmen, nicht davon abhängt, ob x zu dem gegenwärtigen Zeitpunkt in der Menge enthalten ist. Zeigen Sie, wie der Nachfolger von x in einem binären Suchbaum bestimmt werden kann, wenn x nicht in dem Baum enthalten ist.
544
20 van-Emde-Boas-Bäume
1/k 20.1-4 Nehmen anstatt einen Baum vom √Sie an, dass wir einen Baum vom Grad u Grade u auf den Bit-Vektor aufsetzen würden, wobei k > 1 eine Konstante ist. Was wäre die Höhe eines solchen Baumes und wie viel Zeit würden die einzelnen Operationen benötigen?
20.2
Eine rekursive Datenstruktur
√ In diesem Abschnitt modifizieren wir die Idee, einen Baum vom Grad u auf einen BitVektor aufzusetzen. In dem vorherigen Abschnitt benutzen wir die Struktur u bersicht ¨ √ √ der Größe u, wobei jeder Eintrag auf eine andere Struktur der Größe u zeigte. Wir machen diese Struktur nun zu einer rekursiven Struktur, indem wir die Größe des Universums auf jeder Rekursionsebene um die Quadratwurzel verkleinern. Beginnend mit √ einem Universum der Größe u, erzeugen wir Strukturen, die u = u1/2 viele Komponenten enthalten, die selbst wiederum Strukturen mit u1/4 Komponenten enthalten, die Strukturen mit u1/8 Komponenten enthalten, und so weiter bis zu einer Basisgröße von 2. k
Der Einfachtheit halber, setzen wir in diesem Abschnitt voraus, dass u = 22 für eine ganze Zahl k gilt, sodass u, u1/2 , u1/4 , . . . ganze Zahlen sind. Diese Einschränkung wäre in der Praxis ziemlich heftig, da nur Werte für u aus der Folge 2, 4, 16, 256, 65536, . . . erlaubt wären. Wir werden in dem nächsten Abschnitt sehen, wie wir die Voraussetzung abschwächen können, sodass wir nur noch voraussetzen müssen, dass u = 2k für eine ganze Zahl k ist. Da die Datenstruktur, die wir in diesem Abschnitt untersuchen, nur eine Vorstufe der wahren van-Emde-Boas-Baum-Datenstruktur ist, tolerieren wir diese Einschränkung zugunsten eines besseren Verständnisses der grundlegenden Idee. Um unser Ziel, für die Operationen Laufzeiten von O(lg lg u) zu erreichen, nicht aus den Augen zu verlieren, lassen Sie uns darüber nachdenken, wie wir solche Laufzeiten erreichen könnten. Am Ende des Abschnitts 4.3 sahen wir, dass wir mit Variablenersetzung zeigen können, dass die Rekursionsgleichung √ T (n) = 2T n + lg n (20.1) die Lösung T (n) = O(lg n lg lg n) hat. Lassen Sie uns eine ähnliche, wenn auch einfachere Rekursionsgleichung betrachten: √ T (u) = T ( u) + O(1) . (20.2) Wenn wir die gleiche Technik, also Variablenersetzung, anwenden, dann können wir zeigen, dass die Rekursionsgleichung (20.2) die Lösung T (u) = O(lg lg u) besitzt. Sei m = lg u, sodass u = 2m gilt. Es gilt dann T (2m ) = T (2m/2 ) + O(1) . Wenn wir nun S(m) = T (2m ) umbenennen, erhalten wir die neue Rekursionsgleichung S(m) = S(m/2) + O(1) .
20.2 Eine rekursive Datenstruktur
545
Mit Fall 2 der Mastermethode folgt, dass diese Rekursionsgleichung die Lösung S(m) = O(lg m) hat. Durch die Rücktransformation von S(m) zu T (u) erhalten wir T (u) = T (2m) = S(m) = O(lg m) = O(lg lg u). Die Rekursionsgleichung (20.2) wird uns bei unserer Suche nach einer Datenstruktur leiten. Wir werden eine rekursive √ Datenstruktur entwerfen, die auf jedem ihrer Rekursionsebenen um den Faktor u schrumpfen wird. Wenn eine Operation diese Datenstruktur traversiert, wird sie auf jeder Ebene eine konstante Zeit lang verweilen, bevor sie auf die nächsttiefere Ebene rekursiv absteigt. Rekursionsgleichung (20.2) wird dann die Laufzeit der Operation charakterisieren. Eine andere Möglichkeit, sich zu überlegen, dass der Term lg lg u die Lösung der Rekursionsgleichung (20.2) darstellt, ist die folgende: Wenn wir uns die Größe der Universen der einzelnen Ebenen in der rekursiven Datenstruktur anschauen, sehen wir die Folge u, u1/2 , u1/4 , u1/8 , . . .. Wenn wir betrachten, wie viele Bits wir benötigen würden, um die Größe des jeweiligen Universums auf jeder Ebene abzuspeichern, stellen wir fest, dass wir auf der obersten Ebene lg u Bits benötigen, und auf jeder der folgenden Ebenen dann jeweils die Hälfte der Bits der darüberliegenden Ebene. Allgemein, wenn wir mit b Bits starten und auf jeder Ebene die Anzahl der Bits halbieren, dann haben wir nach lg b Ebenen nur noch ein Bit. Da b = lg u gilt, sehen wir, dass wir nach lg lg u Ebenen ein Universum der Größe 2 haben. Lassen Sie uns einen Blick zurück auf die Datenstruktur√ aus Abbildung 20.2 werfen. Wir sehen, dass ein gegebener Wert x in dem Cluster x/ u liegt. √ Wenn wir x als eine lg u-Bit lange binäre Zahl ansehen, dann ist der Clusterindex x/ u durch die (lg u)/2 höherwertigen Bits von x gegeben. Innerhalb seines Clusters erscheint x an Position √ x mod u, die durch die (lg u)/2 niederwertigen Bits von x gegeben ist. Wir werden in dieser Art und Weise indexieren. Lassen Sie uns aus diesem Grunde einige Funktionen definieren, die uns dabei helfen werden: √ high(x) = x/ u , √ low(x) = x mod u , √ index(x, y) = x u + y . Die Funktion high(x) gibt die (lg u)/2 höherwertigen Bits von x an, was dem Index des Clusters von x entspricht. Die Funktion low(x) gibt die (lg u)/2 niederwertigen Bits von x an und stellt damit die Position von x in seinem Cluster zur Verfügung. Die Funktion index(x, y) berechnet ein Element aus x und y, indem sie x als die (lg u)/2 höherwertigen Bits des Elements und y als die (lg u)/2 niederwertigen Bits behandelt. Es gilt die Gleichung x = index(high(x), low(x)). Der Wert von u, die durch jede dieser Funktionen benutzt wird, ist immer die Größe des Universums der Datenstruktur, aus der wir die Funktion aufrufen; diese verändert sich, wenn wir in der rekursiven Struktur absteigen.
Proto-van-Emde-Boas-Strukturen Lassen Sie uns eine rekursive Datenstruktur entwerfen, die die Operationen unterstützen, wobei wir die Rekursionsgleichung (20.2) weiterhin im Blick behalten wollen. Wenn-
546
20 van-Emde-Boas-Bäume
proto-vEB (u) u
u bersicht ¨
0
1
2
3
…
√ u−1
cluster
√ proto-vEB ( u)-Struktur
√
√ u proto-vEB ( u)-Strukturen
Abbildung 20.3: Die Information in einer proto-vEB (u)-Struktur, wenn u ≥ 4 gilt.√Die Struktur enthält ein Universum der Zeiger ¨ u bersicht auf eine √ √ proto-vEB ( u)√ Größe u, einen Struktur und ein Feld cluster [0 . . u − 1] von u Zeigern auf proto-vEB ( u)-Strukturen.
gleich diese Datenstruktur unser Ziel verfehlen wird, für die Operationen Laufzeiten von O(lg lg u) zu erreichen, dient sie als Basis für die van-Emde-Boas-Baum-Datenstruktur, die wir in Abschnitt 20.3 einführen werden. Für das Universum {0, 1, 2, . . . , u − 1} definieren wir eine proto-van-Emde-BoasStruktur oder proto-vEB-Struktur, die wir mit proto-vEB (u) bezeichnen, rekursiv wie folgt. Jede proto-vEB (u)-Struktur enthält ein Attribut u, das die Größe seines Universums angibt. Zusätzlich enthält die Struktur das Folgende: • Ist u = 2, dann ist das die Basisgröße und die Struktur enthält ein Feld A[0 . . 1], das aus zwei Bits besteht. k
• Anderenfalls ist u = 22 für eine ganze Zahl k ≥ 1, sodass u ≥ 4 ist. Neben der Größe u des Universums enthält die Datenstruktur proto-vEB (u), wie in Abbildung 20.3 illustriert, die folgenden Attribute: √ – einen Zeiger mit Namen ¨ u bersicht auf eine proto-vEB ( u)-Struktur und √ √ – ein Feld cluster [0 . . u − 1] √ bestehend aus u Zeigern, wobei jeder dieser Zeiger auf eine proto-vEB ( u)-Struktur verweist. Das Element x, für das 0 ≤ x < u gilt, wird rekursiv als Element low(x) innerhalb des Clusters high(x) gespeichert. In der in dem vorherigen Abschnitt vorgestellten zwei-Ebenen-Struktur speichert je√ der Knoten ein Übersichtsfeld der Größe u, in dem jeder Eintrag ein Bit enthält. Aus dem √ Index eines jeden Eintrages können wir den Startindex des Teilfeldes der Größe u berechnen, über das das Bit Auskunft gibt. In der proto-vEB-Struktur benutzen wir explizit Zeiger anstelle von Indexberechnungen. Das Feld ¨ u bersicht enthält die Übersichtsbits, die rekursiv in einer proto-vEB-Struktur gespeichert sind, und das √ Feld cluster enthält u Zeiger. Abbildung 20.4 zeigt eine voll expandierte proto-vEB (16)-Struktur, die die Menge {2, 3, 4, 5, 7, 14, 15} darstellt. Wenn i in der proto-vEB-Struktur enthalten ist, die durch u bersicht referenziert wird, dann enthält der i-te Cluster wenigstens einen Wert aus ¨ der dargestellten dynamischen √ Menge. Wie √ in einem Baum konstanter Höhe, repräsentiert cluster [i] die Werte i u bis (i + 1) u − 1, die den i-ten Cluster darstellen.
20.2 Eine rekursive Datenstruktur
547
proto-vEB(16)
Elemente 0,1
u 2 A 0 1 0 0
proto-vEB(2)
proto-vEB(2)
proto-vEB(4) u übersicht 4
u 2 A 0 1 0 0
1
u 2 A 1 1 1 0
proto-vEB(4) u übersicht 4
u 2 A 1 1 1 0
Elemente 2,3
cluster 0
1
u 2 A 0 1 0 0
Elemente 8,9 Elemente 10,11
u 2 A 1 1 1 0
Elemente 4,5
proto-vEB(4) u übersicht 4
u 2 A 0 1 1 0
u 2 A 0 1 0 0
cluster 0
proto-vEB(2)
A 0 1 0 0
0
1
u 2 A 0 1 1 0
Elemente 6,7
cluster 0
proto-vEB(2)
Cluster 2,3
u 2
cluster
proto-vEB(2)
A 0 1 1 0
3
proto-vEB(2)
u 2
2
proto-vEB(2)
A 0 1 1 0
proto-vEB(4) u übersicht 4
proto-vEB(2)
u 2
1
cluster
proto-vEB(2)
Cluster 0,1
1
proto-vEB(2)
A 1 1 1 0
0
proto-vEB(2)
A 1 1 1 0
u 2
cluster
proto-vEB(2)
u 2
proto-vEB(2)
proto-vEB(2)
proto-vEB(4) u übersicht 4
0
übersicht
proto-vEB(2)
u 16
1
u 2 A 1 1 1 0
Elemente 12,13 Elemente 14,15
Abbildung 20.4: Eine proto-vEB(16)-Struktur, die die Menge {2, 3, 4, 5, 7, 14, 15} darstellt. Sie zeigt in cluster[0 . . 3] auf vier proto-vEB(4)-Strukturen und auf eine Übersichtsstruktur, die ebenfalls ein proto-vEB(4) ist. Jede proto-vEB(4)-Struktur zeigt auf zwei proto-vEB(2)Strukturen und auf eine Übersichtsstruktur proto-vEB(2). Jede proto-vEB(2)-Struktur enthält ein Feld A[0 . . 1], das aus zwei Bits besteht. Die mit „Elemente i,j“ gekennzeichneten protovEB(2)-Strukturen speichern die Bits i und j der gegenwärtigen dynamischen Menge und die mit „Cluster i,j“ gekennzeichnete proto-vEB(2)-Strukturen speichern die Übersichtsbits für die Cluster i und j der proto-vEB(16)-Struktur. Die dunkel schattierten Kästen geben die oberste Ebene einer proto-vEB-Struktur an, die die Übersichtsinformationen für seine Vaterstruktur speichert; ansonsten ist eine solche proto-vEB-Struktur identisch zu jeder anderen proto-vEBStruktur mit einem Universum der gleichen Größe.
548
20 van-Emde-Boas-Bäume
Auf der untersten Ebene sind die Elemente der gegenwärtigen dynamischen Menge in einigen der proto-vEB (2)-Strukturen gespeichert und die restlichen proto-vEB (2)Strukturen speichern Übersichtsbits. Neben jeder der Basisstrukturen, die keine Übersichtsinformationen darstellen, gibt die Abbildung an, welche Bits sie speichert. Beispielsweise speichert die mit „Elemente 6,7“ gekennzeichnete proto-vEB (2)-Struktur das Bit 6 in dem Eintrag A[0] (der mit 0 belegt ist, da das Element 6 nicht in der Menge ist) und das Bit 7 in dem Eintrag A[1] (der mit 1 belegt ist, da das Element 7 in der Menge enthalten ist). Wie die Cluster ist√jede Übersicht einfach nur eine dynamische Menge über einem √ Universum der Größe u und somit stellen wir jede Übersicht durch eine proto-vEB ( u)Struktur dar. Die vier Übersichtsbits der Haupt-proto-vEB (16)-Struktur sind in der linken proto-vEB (4)-Struktur zu sehen und sind letztendlich in zwei proto-vEB (2)Strukturen gespeichert. Zum Beispiel ist der Eintrag A[0] der mit „Cluster 2,3“ gekennzeichneten proto-vEB (2)-Struktur gleich 0, was aussagt, dass der Cluster 2 der proto-vEB (16)-Struktur (die für die Elemente 8, 9, 10, 11 zuständig ist) überall 0 ist, und es gilt A[1] = 1, was uns sagt, dass Cluster 3 (das für die Elemente 12, 13, 14, 15 verantwortlich ist) mindestens eine 1 enthält. Jede proto-vEB (4)-Struktur zeigt auf ihre eigene Übersicht, die als eine proto-vEB (2)-Struktur gespeichert ist. Betrachten Sie beispielsweise die proto-vEB (2)-Struktur direkt links von der mit „Elemente 0,1“ gekennzeichneten Struktur. Weil ihr Eintrag A[0] gleich 0 ist, wissen wir, dass die „Elemente 0,1“-Struktur keine 1 enthält, und weil ihr Eintrag A[1] gleich 1 ist, wissen wir, dass die „Elemente 2,3“-Struktur wenigstens eine 1 enthält.
Operationen auf einer proto-van-Emde-Boas-Struktur Wir werden nun angeben, wie die Operationen auf einer proto-vEB-Struktur ausgeführt werden können. Zuerst schauen wir uns die Anfrage-Operationen Member, Minimum und Successor an – die die proto-vEB-Struktur unverändert lassen. Wir diskutieren dann Insert und Delete. Wir überlassen die Operationen Maximum und Predecessor, die symmetrisch zu den Operationen Minimum beziehungsweise Successor realisiert werden können, als Übung 20.2-1. Jede der Operationen Member, Successor, Predecessor, Insert und Delete haben als Eingabeparameter x und eine proto-vEB-Struktur V . Jede dieser Operationen setzt 0 ≤ x < V.u voraus. Testen, ob ein Wert in der Menge enthalten ist Um Member(x) auszuführen, haben wir das zu x gehörige Bit in der geeigneten protovEB (2)-Struktur zu finden. Wir können dies in Zeit O(lg lg u) tun, indem wir die Übersichtsstrukturen alle umgehen. Die folgende Prozedur erhält die proto-vEB -Struktur V und einen Wert x als Eingabeparameter und gibt das Bit zurück, das angibt, ob x in der in V gespeicherten dynamischen Menge enthalten ist. Proto-vEB-Member(V, x) 1 if V.u = = 2 2 return V.A[x] 3 else return Proto-vEB-Member(V.cluster [high(x)], low(x))
20.2 Eine rekursive Datenstruktur
549
Die Prozedur Proto-vEB-Member arbeitet wie folgt. Zeile 1 überprüft, ob wir im Basisfall sind, in dem V eine proto-vEB (2)-Struktur ist. Zeile 2 behandelt den Basisfall, indem sie einfach das geeignete Bit des Feldes A zurückgibt. Zeile 3 kümmert sich um den rekursiven Fall, indem sie in die geeignete √ kleinere proto-vEB-Struktur absteigt. Der Wert high(x) gibt an, welche proto-vEB ( u)-Struktur wir√besuchen müssen, und low(x) gibt an, welches Element wir innerhalb der proto-vEB ( u)-Struktur anfragen. Lassen Sie uns anschauen, was passiert, wenn wir Proto-vEB-Member(V, 6) auf die proto-vEB (16)-Struktur aus Abbildung 20.4 anwenden. Da high(6) = 1 gilt, wenn u = 16 ist, steigen wir rekursiv in die proto-vEB (4)-Struktur oben rechts ab und suchen in dieser Struktur nach dem Element low(6) = 2. Bei diesem rekursiven Aufruf ist u = 4 und so steigen wir weiter rekursiv ab. Mit u = 4 gilt high(2) = 1 und low(2) = 0, and so suchen wir nach dem Element 0 in der proto-vEB (2)-Struktur oben rechts. Dieser rekursive Aufruf führt zu einem Basisfall und so geben wir A[0] = 0 über die Folge der rekursiven Aufrufe zurück. Demnach erhalten wir das Ergebnis, dass Proto-vEB-Member(V, 6) den Wert 0 zurückgibt, was besagt, dass 6 nicht in der Menge enthalten ist. Um die Laufzeit der Prozedur Proto-vEB-Member zu bestimmen, sei T (u) ihre Laufzeit auf einer proto-vEB (u)-Struktur. Jeder rekursive Aufruf benötigt konstante Laufzeit, rechnet man die Zeit, die der weitere rekursive Abstieg benötigt, nicht mit. Wenn Proto-vEB-Member sich rekursiv aufruft, dann geht dieser Aufruf zu einer √ proto-vEB √ ( u)-Struktur. Wir können also die Laufzeit durch die Rekursionsgleichung T (u) = T ( u) + O(1) charakterisieren, die wir bereits als Rekursionsgleichung (20.2) gesehen haben. Ihre Lösung ist T (u) = O(lg lg u) und so können wir schlussfolgern, dass die Laufzeit von Proto-vEB-Member in O(lg lg u) liegt. Finden des minimalen Elements Wir wollen uns nun anschauen, wie wir die Operation Minimum ausführen können. Die Prozedur Proto-vEB-Minimum(V ) gibt das minimale Element aus der proto-vEBStruktur V zurück, oder nil, wenn V eine leere Menge darstellt. Proto-vEB-Minimum(V ) 1 if V.u = = 2 2 if V.A[0] = = 1 3 return 0 4 elseif V.A[1] = = 1 5 return 1 6 else return nil 7 else min-cluster = Proto-vEB-Minimum(V.¨ u bersicht ) 8 if min-cluster = = nil 9 return nil 10 else offset = Proto-vEB-Minimum(V.cluster [min-cluster]) 11 return index(min-cluster , offset ) Diese Prozedur arbeitet wie folgt. Zeile 1 überprüft auf den Basisfall, der in den Zeilen 2–6 in direkter Weise behandelt wird. Die Zeilen 7–11 behandeln den rekursiven Fall.
550
20 van-Emde-Boas-Bäume
Zuerst bestimmt Zeile 7 den Index des ersten Clusters, der ein Element aus der Menge enthält. Sie macht dies, indem sie Proto-vEB-Minimum angewendet auf V.¨ u bersicht , √ bei der es sich um eine proto-vEB ( u)-Struktur handelt, rekursiv aufruft. Zeile 7 weist den Index dieses Clusters der Variablen min-cluster zu. Ist die Menge leer, dann gibt der rekursive Aufruf den Wert nil zurück und Zeile 9 gibt nil zurück. Anderenfalls befindet sich das minimale Element irgendwo in dem Cluster mit dem Index min-cluster . Der rekursive Aufruf in Zeile 10 bestimmt den Offset innerhalb des Clusters des minimalen Elements in diesem Cluster. Zuletzt konstruiert Zeile 11 den Wert des minimalen Elements aus dem Index des Clusters und dem Offset und gibt diesen Wert zurück. Wenngleich die Verwendung der Übersichtsinformation es uns erlaubt, schnell den Cluster zu finden, der das minimale Element enthält, liegt die Laufzeit im schlechtesten√Fall dieser Prozedur nicht in O(lg lg u), da sie zwei rekursive Aufrufe auf proto-vEB ( u)Strukturen durchführt. Sei T (u) die Laufzeit im schlechtesten Fall von Proto-vEBMinimum angewendet auf eine proto-vEB (u)-Struktur, so gilt √ T (u) = 2T ( u) + O(1) . (20.3) Wieder können wir durch Variablenersetzung diese Rekursionsgleichung lösen, indem wir m = lg u setzen, woraus T (2m ) = 2T (2m/2 ) + O(1) folgt. Ersetzen wir S(m) = T (2m ), so erhalten wir die Rekursionsgleichung S(m) = 2S(m/2) + O(1) , die mittels Fall 1 der Mastermethode die Lösung S(m) = Θ(m) hat. Durch Rückeinsetzen von T (u) für S(m) erhalten wir T (u) = T (2m ) = S(m) = Θ(m) = Θ(lg u). Wir sehen also, dass Proto-vEB-Minimum wegen des zweiten rekursiven Aufrufs in Zeit Θ(lg u) und nicht in der gewünschten Zeit O(lg lg u) läuft. Berechnen des Nachfolgers Die Operation Successor ist sogar noch schlimmer. Im schlechtesten Fall macht sie zwei rekursive Aufrufe und einen Aufruf der Prozedur Proto-vEB-Minimum. Die Prozedur Proto-vEB-Successor(V, x) gibt das kleinste Element in der proto-vEBStruktur V zurück, das größer als x ist, oder nil, wenn kein Element aus V größer als x ist. Sie setzt nicht voraus, dass x ein Element der Menge ist; es muss aber 0 ≤ x < V.u gelten. Proto-vEB-Successor(V, x) 1 if V.u = = 2 2 if x = = 0 und V.A[1] = = 1 3 return 1 4 else return nil 5 else offset = Proto-vEB-Successor(V.cluster [high(x)], low(x)) 6 if offset = nil 7 return index(high(x), offset ) 8 else succ-cluster = Proto-vEB-Successor(V.¨ u berblick , high(x)) 9 if succ-cluster = = nil 10 return nil 11 else offset = Proto-vEB-Minimum(V.cluster [succ-cluster]) 12 return index(succ-cluster, offset)
20.2 Eine rekursive Datenstruktur
551
Die Prozedur Proto-vEB-Successor arbeitet wie folgt. Wie üblich überprüft Zeile 1, ob der Basisfall vorliegt, der von den Zeilen 2–4 in direkter Weise bearbeitet wird: nur wenn x = 0 und A[1] gleich 1 ist, kann x innerhalb einer proto-vEB (2)-Struktur einen Nachfolger haben. Die Zeilen 5–12 kümmern sich um den rekursiven Fall. Zeile 5 sucht nach einem Nachfolger von x innerhalb des Clusters von x und speichert das Ergebnis der Suche in die Variable offset . Zeile 6 überprüft, ob x einen Nachfolger innerhalb seines Clusters hat; wenn dies der Fall ist, dann berechnet Zeile 7 den Wert dieses Nachfolgers und gibt ihn zurück. Anderenfalls, haben wir in anderen Clustern zu suchen. Zeile 8 weist der Variablen succ-cluster den Index des nächsten nichtleeren Clusters zu, indem sie die Überblicksinformation benutzt, um ihn zu bestimmen. Zeile 9 überprüft, ob succ-cluster gleich nil ist, wobei Zeile 10 den Wert nil zurückgibt, wenn alle nachfolgenden Cluster leer sind. Wenn succ-cluster ungleich nil ist, weist Zeile 11 der Variablen offset das erste Element aus diesem Cluster zu und Zeile 12 berechnet das minimale Element aus diesem Cluster und gibt es zurück. Im schlechtesten Fall ruft sich Proto-vEB-Successor zweimal selbst rekursiv mit √ proto-vEB (√u)-Strukturen auf und ruft einmal Proto-vEB-Minimum mit einer proto-vEB ( u)-Struktur auf. Somit ist die Laufzeit T (u) von Proto-vEB-Successor im schlechtesten Fall √ √ T (u) = 2T ( u) + Θ(lg u) √ = 2T ( u) + Θ(lg u) . Wir können die gleiche Technik anwenden wie die, die wir für die Rekursionsgleichung (20.1) angewendet haben, um zu zeigen, dass diese Rekursionsgleichung die Lösung T (u) = Θ(lg u lg lg u) hat. Proto-vEB-Successor ist also asymptotisch langsamer als Proto-vEB-Minimum. Einfügen eines Elementes Um ein Element einzufügen, müssen wir es in den zuständigen Cluster einfügen und das Überblickbit für dieses Cluster auf 1 setzen. Die Prozedur Proto-vEB-Insert(V, x) fügt den Wert x in die proto-vEB-Struktur V ein. Proto-vEB-Insert(V, x) 1 if V.u = = 2 2 V.A[x] = 1 3 else Proto-vEB-Insert(V.cluster [high(x)], low(x)) 4 Proto-vEB-Insert(V.¨ u berblick , high(x)) Im Basisfall setzt Zeile 2 das zuständige Bit im Feld A auf 1. Im rekursiven Fall fügt der rekursive Aufruf in Zeile 3 den Wert x in das zuständige Cluster ein und Zeile 4 setzt das Überblickbit für dieses Cluster auf 1. Da die Prozedur Proto-vEB-Insert im schlechtesten Fall zwei rekursive Aufrufe durchführt, charakterisiert die Rekursionsgleichung (20.3) ihre Laufzeit. Somit benötigt Proto-vEB-Insert Laufzeit Θ(lg u).
552
20 van-Emde-Boas-Bäume
Löschen eines Elementes Die Operation Delete ist komplizierter als das Einfügen. Während wir beim Einfügen immer das Überblickbit setzen können, dürfen wir beim Löschen das Überblickbit nicht immer zurücksetzen. Wir müssen überprüfen, ob ein Bit in dem betroffenen √ Cluster gleich 1 ist. Wie wir proto-vEB-Strukturen definiert haben, müssten wir uns alle u Bits innerhalb eines Clusters anschauen, um zu überprüfen, ob einer von ihnen gleich 1 ist. Alternativ könnten wir ein Attribut n der proto-vEB-Struktur hinzufügen, das die Elemente der Struktur zählt. Wir überlassen die Implementierung von Proto-vEBDelete den Übungen 20.2-2 und 20.2-3. Offensichtlich müssen wir die proto-vEB-Struktur modifizieren, damit jede Operation höchstens einen rekursiven Aufruf durchführt. Wir werden in dem nächsten Abschnitt sehen, wie wir das machen können.
Übungen 20.2-1 Geben Sie den Pseudocode für die Prozeduren Proto-vEB-Maximum und Proto-vEB-Predecessor an. 20.2-2 Geben Sie den Pseudocode für die Prozedur Proto-vEB-Delete an. Er sollte das entsprechende Überblickbit aktualisieren, indem er über alle zugehörigen Bits des Clusters läuft. Wie hoch ist die Laufzeit Ihrer Prozedur im schlechtesten Fall? 20.2-3 Fügen Sie das Attribut n zu jeder proto-vEB-Struktur hinzu, das die Anzahl der Elemente der gegenwärtigen Menge angibt, für die diese Struktur verantwortlich ist, und geben Sie den Pseudocode für Proto-vEB-Delete an, der das Attribut n benutzt, um zu entscheiden, ob Überblickbits auf 0 zurückgesetzt werden müssen. Wie hoch ist die Laufzeit Ihrer Prozedur im schlechtesten Fall? Welche anderen Prozeduren müssen wegen dem neuen Attribut geändert werden? Beeinträchtigen diese Änderungen ihre Laufzeiten? 20.2-4 Modifizieren Sie die proto-vEB-Struktur so, dass Mehrfachschlüssel erlaubt sind. 20.2-5 Modifizieren Sie die proto-vEB-Struktur so, dass sie Schlüssel mit dazugehörigen Satellitendaten unterstützt. 20.2-6 Geben Sie den Pseudocode für eine Prozedur an, die eine proto-vEB (u)-Struktur erzeugt. 20.2-7 Erklären Sie, dass die proto-vEB-Struktur leer ist, nachdem die Zeile 9 in Proto-vEB-Minimum ausgeführt worden ist. 20.2-8 Nehmen Sie an, wir würden eine proto-vEB-Struktur entwerfen, in der jedes Feld cluster nur aus u1/4 Elementen bestehen würde. Was wären die Laufzeiten der einzelnen Operationen?
20.3 Die van-Emde-Boas-Bäume
20.3
553
Die van-Emde-Boas-Bäume
Die proto-vEB-Struktur aus dem vorherigen Abschnitt ist sehr nah an dem, was wir brauchen, um die O(lg lg u) Laufzeiten zu erreichen. Sie scheitert, da wir in den meisten der Operationen zu häufig rekursiv absteigen müssen. In diesem Abschnitt werden wir eine Datenstruktur entwerfen, die der proto-vEB-Struktur sehr ähnlich ist, aber ein bisschen mehr Information speichert, mit der einige der rekursiven Abstiege vermieden werden können. In Abschnitt 20.2 haben wir bereits festgestellt, dass die von uns gemachte Vorausk setzung zu der Größe des Universums – nämlich, dass u = 22 für eine ganze Zahl k gelten muss – übermäßig restriktiv ist, da die erlaubten Werte für u sich auf eine allzu dünne Menge beschränken. Ab jetzt erlauben wir√deshalb, dass die Größe u des Universums eine exakte Zweierpotenz ist, und wenn u keine ganze Zahl ist – d. h. wenn u eine ungerade Zweierpotenz ist (u = 22k+1 for eine ganze Zahl k ≥ 0) – dann teilen wir die lg u Bits einer Zahl in die (lg u)/2 höherwertigen Bits und in die (lg u)/2 niederwertigen Bits. Der√Einfachheit halber, bezeichnen wir 2 (lg u)/2 (die „obere Qua√ dratwurzel“ √ von u) mit ↑ u und 2(lg u)/2 (die „untere Quadratwurzel“ von u) mit ↓ u, √ 2k sodass u = ↑√u · ↓ u√gilt. Wenn u eine gerade Zweierpotenz ist (u = 2 für eine ganze √ Zahl k), gilt ↑ u = ↓ u = u. Da wir also erlauben, dass u eine ungerade Zweierpotenz sein darf, müssen wir unsere hilfreichen Funktionen aus Abschnitt 20.2 neu definieren: √ high(x) = x/ ↓ u , √ low(x) = x mod ↓ u , √ index(x, y) = x ↓ u + y .
van-Emde-Boas-Bäume Der van-Emde-Boas-Baum oder vEB-Baum ähnelt der proto-vEB-Struktur. Wir schreiben vEB (u) für einen vEB-Baum mit einem Universum der Größe u und, sofern u √ ↑ nicht gleich der Basisgröße 2 ist, zeigt das Attribut u ¨ bersicht auf einen vEB ( u)-Baum √ √ √ und das Feld cluster [0 . . ↑ u − 1] auf ↑ u vEB ( ↓ u)-Bäume. Wie Abbildung 20.5 zeigt, enthält ein vEB-Baum zwei Attribute, die nicht in einer proto-vEB-Struktur enthalten waren: • min speichert das minimale Element in dem vEB-Baum und • max speichert das maximale Element in dem vEB-Baum. Darüberhinaus erscheint das Element, welches in min gespeichert ist, in keinem der √ rekursiven vEB ( ↓ u)-Bäumen, auf die das Feld cluster zeigt. Die Elemente, die in einem vEB (u)-Baum V gespeichert sind, bestehen somit aus V.min plus alle Elemente, die √ √ rekursiv in den vEB ( ↓ u)-Bäumen gespeichert sind, auf die V.cluster [0 . . ↑ u − 1] zeigt. Bemerken Sie, dass, wenn ein vEB-Baum zwei oder mehr Elemente enthält, wir min und max unterschiedlich behandeln: das Element, das in min gespeichert ist, erscheint in keinem der Cluster, das Element, das in max gespeichert ist, dagegen schon, es sei denn der vEB-Baum enthält nur ein Element (sodass das minimale und das maximale Element das gleiche Element darstellen).
554
20 van-Emde-Boas-Bäume
vEB (u)
u
min
max 0
u berblick ¨
√ vEB ( ↑ u)
1
2
3
…
√ ↑ u−1
cluster
√ √ ↑ u vEB ( ↓ u)-Bäume
Abbildung 20.5: Die Information in einem vEB (u)-Baum im Falle von u > 2. Die Struktur enthält u berblick auf einen √ein Universum der Größe u, Elemente √ min und max , einen√Zeiger ¨ √ vEB ( ↑ u)-Baum und ein Feld cluster [0 . . ↑ u − 1] bestehend aus ↑ u Zeigern auf vEB ( ↓ u)Bäumen.
Da die Basisgröße 2 ist, benötigt ein vEB (2)-Baum das Feld A, das in der proto-vEB (2)Struktur enthalten ist, nicht. Wir können seine Elemente aus seinen Attributen min and max entnehmen. In einem vEB-Baum ohne Element ist sowohl min als auch max gleich nil, unabhängig von der Größe u seines Universums. Abbildung 20.6 zeigt einen vEB (16)-Baum V , der die Menge {2, 3, 4, 5, 7, 14, 15} darstellt. Da das kleinste Element gleich 2 ist, gilt V.min = 2 und, obwohl high(2) = 0, erscheint das Element 2 nicht in dem vEB (4)-Baum, auf den V.cluster [0] zeigt: Sie sollten bemerken, dass V.cluster [0].min gleich 3 ist und somit 2 nicht in diesem vEB-Baum ist. Da V.cluster [0].min gleich 3 ist und 2 und 3 die einzigen Elemente in V.cluster [0] sind, sind die vEB (2)-Cluster innerhalb V.cluster [0] leer. Die Attribute min und max werden eine zentrale Rolle spielen, um die Anzahl der rekursiven Aufrufe innerhalb der Operationen auf vEB-Bäumen zu reduzieren. Diese Attribute werden uns wie folgt helfen: 1. Die Operationen Minimum und Maximum brauchen überhaupt nicht mehr rekursiv abzusteigen, da sie einfach nur die Werte von min oder max zurückgeben können. 2. Die Operation Successor kann den rekursiven Aufruf, um festzustellen, ob der Nachfolger des Wertes x innerhalb des Clusters high(x) liegt, vermeiden. Das kommt daher, dass der Nachfolger von x genau dann innerhalb seines Clusters liegt, wenn x echt kleiner ist als das Attribut max seines Clusters. Ein symmetrisches Argument gilt für Predecessor und min. 3. Wir können in konstanter Zeit aus den Werten von min und max bestimmen, ob ein vEB-Baum keine Elemente, exakt ein Element oder wenigstens zwei Elemente enthält. Wenn min und max beide gleich nil sind, dann enthält der vEB-Baum keine Elemente. Wenn min und max ungleich nil sind und den gleichen Wert haben, dann enthält der vEB-Baum genau ein Element. Anderenfalls sind min und max beide ungleich nil, aber ungleich und der vEB-Baum enthält zwei oder mehr Elemente.
20.3 Die van-Emde-Boas-Bäume
555
u 16
vEB(16)
min 2 0
übersicht
vEB(4) u 4
min 0 max 3 0
übersicht
vEB(2) u 2
vEB(4)
u 4
min 3 max 3 0
übersicht
vEB(2)
vEB(2)
u 2
u 2
vEB(2)
3
vEB(4)
u 4
min 0 max 3
1
cluster
0
übersicht
vEB(2)
vEB(2)
u 2
u 2
u 2
2
cluster
1
cluster
max 15 1
vEB(2)
1
cluster
vEB(2)
vEB(2)
u 2
u 2
u 2
min 0
min 1
min 1
min
min
min
min 0
min 1
min 1
max 1
max 1
max 1
max
max
max
max 1
max 1
max 1
vEB(4)
u 4
min
max 0
übersicht
vEB(2) u 2
vEB(4)
cluster
vEB(2) u 2
u 4
min 2 max 3
1
0
übersicht
vEB(2) u 2
vEB(2) u 2
1
cluster
vEB(2) u 2
vEB(2) u 2
min
min
min
min 1
min
min 1
max
max
max
max 1
max
max 1
Abbildung 20.6: Ein zu dem proto-vEB-Baum aus Abbildung 20.4 korrespondierender vEB(16)-Baum Er speichert die Menge {2, 3, 4, 5, 7, 14, 15}. Querstriche stehen für die nilWerte. Der Wert, der in dem Attribut min eines vEB-Baumes gespeichert ist, erscheint in keinem seiner Cluster. Die dunklen Schattierungen dienen dem gleichen Zweck wie in Abbildung 20.4.
556
20 van-Emde-Boas-Bäume
4. Wenn wir wissen, dass ein vEB-Baum leer ist, dann können wir ein Element in ihn einfügen, indem wir nur seine Attribute min und max aktualisieren. Wir können also in diesem Fall ein Element in konstanter Zeit in den vEB-Baum einfügen. Ähnlich verhält es sich, wenn wir wissen, dass ein vEB-Baum genau ein Element enthält und wir dieses Element löschen wollen: wir können es in konstanter Zeit löschen, indem wir wiederum nur min und max aktualisieren. Diese Eigenschaften werden es uns erlauben, die Anzahl der rekursiven Aufrufe zu reduzieren. Auch wenn die Größe des Universums eine ungerade Zweierpotenz ist, wird die Differenz der Größen in dem Übersicht-vEB-Baum und den Clustern die asymptotischen Laufzeiten der vEB-Baum-Operationen nicht beeinträchtigen. Die rekursiven Prozeduren, die die vEB-Baum-Operationen implementieren, werden alle Laufzeiten haben, die durch die Rekursionsgleichung √ T (u) ≤ T ( ↑ u) + O(1) (20.4) charakterisiert werden. Diese Rekursionsgleichung sieht der Rekursionsgleichung (20.2) ähnlich. Mit m = lg u können wir sie umschreiben zu T (2m ) ≤ T (2 m/2 ) + O(1) . Indem wir bemerken, dass m/2 ≤ 2m/3 für alle m ≥ 2 gilt, haben wir damit T (2m ) ≤ T (22m/3 ) + O(1) . Mit S(m) = T (2m ) können wie die letzte Rekursionsgleichung umschreiben zu S(m) ≤ S(2m/3) + O(1) , auf die wir Fall 2 der Mastermethode anwenden können, sodass sie die Lösung S(m) = O(lg m) hat. (In Bezug auf die asymptotische Lösung macht der Faktor 2/3 keinen Unterschied zu dem Faktor 1/2, da, wenn wir die Mastermethode anwenden, wir feststellen, dass log3/2 1 = log2 1 = 0. gilt.) Somit gilt T (u) = T (2m ) = S(m) = O(lg m) = O(lg lg u). Bevor wir einen van-Emde-Boas-Baum benutzen, müssen wir die Größe u seines Universums kennen, sodass wir einen van-Emde-Boas-Baum geeigneter Größe erzeugen können, der mit der leeren Menge initialisiert wird. Der gesamte Speicherbedarf eines vanEmde-Boas-Baumes ist, wie Problemstellung 20-1 von Ihnen verlangt zu zeigen, O(u) und es ist einfach einen leeren Baum in Zeit Θ(u) zu erzeugen. Im Gegensatz dazu können wir einen leeren Rot-Schwarz-Baum in konstanter Zeit erzeugen. Aus diesem Grunde sollten wir keinen van-Emde-Boas-Baum benutzen, wenn wir nur eine kleine Anzahl von Operationen durchzuführen haben, da die Zeit, die Datenstruktur zu erzeugen, würde die Zeit übersteigen, die wir später bei den einzelnen Operationen sparen würden. Dieser Nachteil ist normalerweise nicht signifikant, da wir typischerweise einfache Datenstrukturen wie ein Feld oder eine verkettete Liste benutzen, um eine Menge mit nur wenigen Elementen darzustellen.
20.3 Die van-Emde-Boas-Bäume
557
Operationen auf einem van-Emde-Boas-Baum Wir sind jetzt soweit, um uns zu überlegen, wie die Operationen auf einem vanEmde-Boas-Baum auszuführen sind. Wie wir dies bereits für die proto-van-Emde-BoasStruktur gemacht haben, werden wir zuerst die Anfrage-Operationen betrachten und dann erst Insert und Delete. Aufgrund der leichten Asymmetrie zwischen dem minimalen Element und dem maximalen Element in einem vEB-Baum – wenn ein vEBBaum wenigstens zwei Elemente enthält, dann erscheint das minimale Element nicht innerhalb eines Clusters, das maximale Element aber schon – werden wir den Pseudocode für alle fünf Anfrage-Operationen angeben. Wie die Operationen auf proto-vanEmde-Boas-Strukturen haben die Operationen die beiden Parameter V und x, wobei V ein van-Emde-Boas-Baum ist und x ein Element, von dem wir voraussetzen, dass 0 ≤ x < V.u gilt. Finden des minimalen und maximalen Elements Da wir das Minimum und das Maximum in den Attributen min und max speichern, sind zwei der Operationen Einzeiler, die zur Ausführung konstante Zeit benötigen: vEB-Tree-Minimum(V ) 1 return V.min vEB-Tree-Maximum(V ) 1 return V.max Testen, ob ein Wert in der Menge ist Die Prozedur vEB-Tree-Member(V, x) enthält wie Proto-vEB-Member einen rekursiven Fall, der Basisfall unterscheidet sich aber leicht zu dem der Prozedur auf protovEB-Strukturen. Wir überprüfen auch hier direkt, ob x gleich dem minimalen oder maximalen Wert ist. Da ein vEB-Baum im Unterschied zu einer proto-vEB-Struktur keine Bits speichert, entwerfen wir vEB-Tree-Member so, dass sie entweder den Wert wahr oder falsch und nicht 1 oder 0 zurückgibt. vEB-Tree-Member(V, x) 1 if x = = V.min oder x = = V.max 2 return wahr 3 elseif V.u == 2 4 return falsch 5 else return vEB-Tree-Member(V.cluster [high(x)], low(x)) Zeile 1 überprüft, ob x gleich dem minimalen oder dem maximalen Wert ist. Wenn einer dieser beiden Fälle vorliegt, gibt Zeile 2 den Wert wahr zurück. Anderenfalls testet Zeile 3 auf den Basisfall. Da ein vEB (2)-Baum keine anderen Elemente als min
558
20 van-Emde-Boas-Bäume
und max enthält, gibt Zeile 4 den Wert falsch zurück, wenn der Basisfall vorliegt. Wenn weder x gleich min oder max ist, noch der Basisfall vorliegt, dann steigt die Prozedur in Zeile 5 rekursiv ab. Rekursionsgleichung (20.4) charakterisiert die Laufzeit von vEB-Tree-Member und so benötigt diese Prozedur Zeit O(lg lg u). Berechnen des Nachfolgers und des Vorgängers Als nächstes schauen wir uns an, wie die Operation Successor zu implementieren ist. Rufen Sie sich in Erinnerung, dass die Prozedur Proto-vEB-Successor(V, x) zwei rekursive Aufrufe machen konnte: einen, um festzustellen, ob der Nachfolger von x in dem gleichen Cluster wie x liegt, und, wenn nicht, einen, um den Cluster zu bestimmen, der den Nachfolger von x enthält. Da wir den maximalen Wert in einem vEB-Baum schnell bestimmen können, vermeiden wir es, zwei rekursive Aufrufe zu machen, und stattdessen nur einen rekursiven Aufruf durchzuführen, entweder auf einem Cluster oder auf der Übersicht, aber nicht auf beiden. vEB-Tree-Successor(V, x) 1 if V.u = = 2 2 if x = = 0 und V.max = = 1 3 return 1 4 else return nil 5 elseif V.min = nil und x < V.min 6 return V.min 7 else max -low = vEB-Tree-Maximum(V.cluster [high(x)]) 8 if max -low = nil und low(x) < max -low 9 offset = vEB-Tree-Successor(V.cluster [high(x)], low(x)) 10 return index(high(x), offset ) 11 else nachfolger -cluster = vEB-Tree-Successor(V.¨ u bersicht , high(x)) 12 if nachfolger -cluster = = nil 13 return nil 14 else offset = vEB-Tree-Minimum(V.cluster [nachfolger -cluster ]) 15 return index(nachfolger -cluster , offset ) Diese Prozedur enthält sechs return-Anweisungen und mehrere Fälle. Wir beginnen mit dem Basisfall in den Zeilen 2–4, die das Element 1 in Zeile 3 zurückgibt, wenn wir versuchen, den Nachfolger von 0 zu bestimmen, und 1 in der Menge enthalten ist; anderenfalls gibt der Basisfall in Zeile 4 den Wert nil zurück. Wenn wir uns nicht im Basisfall befinden, überprüfen wir als nächstes in Zeile 5, ob x echt kleiner als das minimale Element ist. Ist dem so, dann können wir einfach das minimale Element zurückgeben (Zeile 6). Wenn wir Zeile 7 erreichen, dann wissen wir, dass wir nicht in einem Basisfall sind und dass x größer oder gleich dem minimalen Wert des vEB-Baumes V ist. Zeile 7 weist der Variablen max -low das maximale Element aus dem Cluster von x zu. Wenn das Cluster von x ein Element enthält, welches größer als x ist, dann wissen wir, dass der Nachfolger
20.3 Die van-Emde-Boas-Bäume
559
von x irgendwo in dem gleichen Cluster wie x liegt. Zeile 8 überprüft diesen Fall. Wenn der Nachfolger von x in dem Cluster von x enthalten ist, dann bestimmt Zeile 9, wo genau der Nachfolger von x in dem Cluster liegt und Zeile 10 gibt den Nachfolger in der gleichen Art und Weise zurück wie Zeile 7 aus Proto-vEB-Successor. Wir erreichen Zeile 11, wenn x größer oder gleich dem größten Element seines Clusters ist. In diesem Fall bestimmen die Zeilen 11–15 den Nachfolger von x in der gleichen Art und Weise, wie dies die Zeilen 8–12 aus Proto-vEB-Successor tun. Es ist einfach zu sehen, dass Rekursionsgleichung (20.4) die Laufzeit von vEB-TreeSuccessor charakterisiert. Abhängig von dem Ausgang des Tests in Zeile 8 ruft die Prozedur die √ sich selbst entweder in Zeile 9 (auf einem vEB-Baum, dessen Universum √ Größe ↓ u hat) oder in Zeile 11 (auf einem vEB-Baum, dessen Universum die Größe ↑ u hat) rekursiv auf. In jedem der beiden Fälle erfolgt der √ eine rekursive Aufruf auf einem vEB-Baum, dessen Universum höchstens die Größe ↑ u hat. Die restlichen Zeilen der Prozedur, inklusive der Aufrufe von vEB-Tree-Minimum und vEB-Tree-Maximum, benötigen Zeit O(1). Somit benötigt vEB-Tree-Successor eine Laufzeit O(lg lg u) im schlechtesten Fall. Die Prozedur vEB-Tree-Predecessor ist im Wesentlichen symmetrisch zu der Prozedur vEB-Tree-Successor, enthält aber noch einen zusätzlichen Fall: vEB-Tree-Predecessor(V, x) 1 if V.u = = 2 2 if x = = 1 und V.min == 0 3 return 0 4 else return nil 5 elseif V.max = nil und x > V.max 6 return V.max 7 else min-low = vEB-Tree-Minimum(V.cluster [high(x)]) 8 if min-low = nil und low(x) > min-low 9 offset = vEB-Tree-Predecessor(V.cluster [high(x)], low(x)) 10 return index(high(x), offset ) 11 else vorg¨a nger -cluster = vEB-Tree-Predecessor(V.¨ u bersicht , high(x)) 12 if vorg¨a nger -cluster = = nil 13 if V.min = nil und x > V.min 14 return V.min 15 else return nil 16 else offset = vEB-Tree-Maximum(V.cluster [vorg¨a nger -cluster]) 17 return index(vorg¨a nger -cluster, offset ) Die Zeilen 13–14 behandeln den zusätzlichen Fall. Dieser Fall liegt vor, wenn der Vorgänger von x, sofern er überhaupt existiert, nicht in dem gleichen Cluster wie x liegt. In der Prozedur vEB-Tree-Successor waren wir uns sicher, dass, wenn der Nachfolger von x ausserhalb von dem Cluster von x liegt, dann muss er in einem Cluster mit einem höheren Index liegen. Wenn aber der Vorgänger von x der minimale Wert aus dem vEB-Baum V ist, dann liegt der Vorgänger von x in überhaupt keinem Cluster.
560
20 van-Emde-Boas-Bäume
Zeile 13 überprüft auf diesen Fall und Zeile 14 gibt gegebenenfalls den minimalen Wert zurück. Dieser zusätzliche Fall beeinträchtigt die asymptotische Laufzeit von vEB-TreePredecessor, verglichen mit der von vEB-Tree-Successor, nicht und so benötigt vEB-Tree-Predecessor eine Laufzeit von O(lg lg u) im schlechtesten Fall. Einfügen eines Elementes Wir untersuchen nun, wie ein Element in einen vEB-Baum eingefügt werden kann. Erinnern Sie sich daran, dass Proto-vEB-Insert zwei rekursive Aufrufe machte: einen, um das Element einzufügen, und einen, um den Index des Clusters des Elements in die Übersicht einzutragen. Die Prozedur vEB-Tree-Insert wird nur einen rekursiven Aufruf machen. Wie erreichen wir das? Wenn wir ein Element einfügen, dann enthält das Cluster, in dem es eingefügt wird, entweder bereits ein Element oder es enthält noch kein Element. Im ersten Fall ist der Index des Clusters bereits in der Übersicht eingetragen und wir brauchen den rekursiven Aufruf nicht zu machen. Wenn der Cluster bis jetzt noch kein Element enthalten hat, dann wird das Element, das wir jetzt einfügen, das einzige Element in dem Cluster sein, und wir wissen, dass wir nicht rekursiv absteigen brauchen, um ein Element in einen leeren vEB-Baum einzufügen: vEB-Empty-Tree-Insert(V, x) 1 V.min = x 2 V.max = x Mit dieser Prozedur können wir nun den Pseudocode für vEB-Tree-Insert(V, x) angeben, welcher voraussetzt, dass x nicht bereits ein Element der durch den vEB-Baum V dargestellten Menge ist: vEB-Tree-Insert(V, x) 1 if V.min = = nil 2 vEB-Empty-Tree-Insert(V, x) 3 else if x < V.min 4 vertausche x mit V.min 5 if V.u > 2 6 if vEB-Tree-Minimum(V.cluster [high(x)]) = = nil 7 vEB-Tree-Insert(V.¨ u bersicht , high(x)) 8 vEB-Empty-Tree-Insert(V.cluster [high(x)], low(x)) 9 else vEB-Tree-Insert(V.cluster [high(x)], low(x)) 10 if x > V.max 11 V.max = x Diese Prozedur arbeitet wie folgt. Zeile 1 überprüft, ob V ein leerer vEB-Baum ist, und wenn er einer ist, dann behandelt Zeile 2 diesen einfachen Fall. Die Zeilen 3–11 setzen voraus, dass V nicht leer ist, und somit wird ein Element in einen der Cluster
20.3 Die van-Emde-Boas-Bäume
561
von V eingefügt. Dieses Element muss nicht notwendigerweise das Element x sein, das der Prozedur vEB-Tree-Insert übergeben worden ist. Wenn x < min ist – dies wird in Zeile 3 getestet –, dann muss x das neue min werden. Wir dürfen jedoch das ursprüngliche min nicht verlieren, und so müssen wir dieses in eines der Cluster von V einfügen. Liegt dieser Fall vor, vertauscht Zeile 4 die Werte x und min, sodass wir das ursprüngliche min in eines der Cluster von V speichern werden. Wir führen die Zeilen 6–9 nur dann aus, wenn V kein Basis-vEB-Baum ist. Zeile 6 überprüft, ob das Cluster, in das x eingefügt werden wird, gegenwärtig leer ist. Ist dem so, dann fügt Zeile 7 den Index des Clusters von x in die Übersicht ein und Zeile 8 bearbeitet den einfachen Fall, das Element x in einen leeren Cluster einzufügen. Wenn der Cluster von x gegenwärtig nicht leer ist, dann fügt Zeile 9 das Element x in seinen Cluster ein. In diesem Fall brauchen wir die Übersicht nicht zu aktualisieren, da der Cluster von x bereits ein Element der Übersicht ist. Letztendlich sind die Zeilen 10–11 verantwortlich, das Attribut max zu aktualisieren, wenn x > max gilt. Sie sollten bemerken, dass, wenn V ein Basis-vEB-Baum ist, der nicht leer ist, die Zeilen 3–4 und 10–11 die Attribute min and max korrekt aktualisieren. Erneut können wir uns leicht überlegen, dass die Rekursionsgleichung (20.4) die Laufzeit charakterisiert. Abhängig von dem Ausgang des Tests in Zeile 6, macht die Prozedur entweder √ den rekursiven Aufruf in Zeile 7 (auf einem vEB-Baum, dessen Universum die Größe ↑ u hat) oder den √ rekursiven Aufruf in Zeile 9 (auf einem vEB-Baum, dessen Universum die Größe ↓ u hat). In jedem der beiden√ Fälle erfolgt der eine rekursive Aufruf auf einem Universum höchstens der Größe ↑ u. Da die restlichen Zeilen der Prozedur vEB-Tree-Insert Zeit O(1) benötigen, greift die Rekursionsgleichung (20.4) und so ist die Laufzeit im schlechtesten Fall in O(lg lg u). Löschen eines Elementes Zum Abschluss überlegen wir uns, wie ein Element aus einem vEB-Baum gelöscht werden kann. Die Prozedur vEB-Tree-Delete(V, x) setzt voraus, dass x ein Element der gegenwärtig durch den vEB-Baum V dargestellten Menge ist.
562
20 van-Emde-Boas-Bäume
vEB-Tree-Delete(V, x) 1 if V.min = = V.max 2 V.min = nil 3 V.max = nil 4 elseif V.u = = 2 5 if x = = 0 6 V.min = 1 7 else V.min = 0 8 V.max = V.min 9 else if x = = V.min 10 erster -cluster = vEB-Tree-Minimum(V.¨ u bersicht ) 11 x = index(erster -cluster, vEB-Tree-Minimum(V.cluster [erster -cluster])) 12 V.min = x 13 vEB-Tree-Delete(V.cluster [high(x)], low(x)) 14 if vEB-Tree-Minimum(V.cluster [high(x)]) = = nil 15 vEB-Tree-Delete(V.¨ u bersicht , high(x)) 16 if x = = V.max 17 u bersicht -max = vEB-Tree-Maximum(V.¨ ¨ u bersicht ) 18 if ¨ u bersicht -max = = nil 19 V.max = V.min 20 else V.max = index(¨ u bersicht -max , vEB-Tree-Maximum(V.cluster [¨ u bersicht -max ])) 21 elseif x = = V.max 22 V.max = index(high(x), vEB-Tree-Maximum(V.cluster [high(x)])) Die Prozedur vEB-Tree-Delete arbeitet wie folgt. Wenn der vEB-Baum V nur ein Element enthält, dann ist es genauso einfach es zu löschen wie ein Element in einen leeren vEB-Baum einzufügen: Setzen Sie einfach nur die Attribute min und max auf nil. Die Zeilen 1–3 behandeln diesen Fall. Anderenfalls enthält V mindestens zwei Elemente. Zeile 4 überprüft, ob V ein Basis-vEB-Baum ist, und wenn dem so ist, dann setzen die Zeilen 5–8 die Attribute min und max auf das eine verbleibende Element. Die Zeilen 9–22 setzen voraus, dass V zwei oder mehr Elemente enthält und u ≥ 4 ist. In diesem Fall, haben wir ein Element aus einem Cluster zu entfernen. Das Element, das wir entfernen, muss nicht notwendigerweise x sein, da, wenn x gleich min ist, dann wird ein anderes Element aus einem der Cluster von V das neue min (nachdem wir x gelöscht haben), sodass wir dieses dann aus seinem Cluster entfernen müssen. Wenn der Test in Zeile 9 feststellt, dass wir uns in diesem Fall befinden, dann setzt Zeile 10 die Variable erster -cluster auf den Index des Clusters, der das kleinste Element verschieden von min enthält, und Zeile 11 setzt x auf den Wert dieses kleinsten Elements. Dieses Element wird das neue min (Zeile 12) und, da wir x auf diesen Wert gesetzt haben, ist x das Element, das aus seinem Cluster entfernt werden muss. Wenn wir Zeile 13 erreichen, dann wissen wir, dass wir das Element x aus seinem Cluster entfernen müssen, unabhängig davon, ob x der Wert ist, der ursprünglich der Proze-
20.3 Die van-Emde-Boas-Bäume
563
dur vEB-Tree-Delete als Parameter übergeben worden ist, oder x das Element ist, welches das neue Minimum geworden ist. Zeile 13 löscht x aus seinem Cluster. Dieses Cluster kann dadurch möglicherweise leer werden, was Zeile 14 überprüft, und, wenn dies der Fall ist, dann müssen wir den Index des Clusters x aus der Übersicht herausnehmen, was Zeile 15 tut. Nachdem die Übersicht aktualisiert worden ist, müssen wir eventuell noch das Attribut max aktualisieren. Die Zeile 16 überprüft, ob wir gerade das maximale Element aus V gelöscht haben, und, wenn dem so ist, dann setzt Zeile 17 die Variable ¨u bersicht -max auf den Index des nichtleeren Clusters mit dem größten Index. (Der Aufruf vEB-Tree-Maximum(V.¨ u bersicht ) führt dies korrekt aus, da wir bereits die Prozedur vEB-Tree-Delete rekursiv auf V.¨ u bersicht angewendet haben und deshalb V.¨ u bersicht .max bereits wie verlangt aktualisiert worden ist.) Wenn alle Cluster von V leer sind, dann ist min das einzige verbleibende Element in V ; die Zeile 18 testet auf diesen Fall und Zeile 19 aktualisiert das Attribut max gegebenfalls entsprechend. Anderenfalls setzt Zeile 20 das Attribut max auf das maximale Element aus dem nichtleeren Cluster mit dem größten Index. (Wenn dieses Cluster das Cluster ist, aus dem wir gerade das Element gelöscht haben, dann können wir aufgrund des rekursiven Aufrufs in Zeile 13 davon ausgehen, dass das Attribut max des Clusters bereits korrekt aktualisiert worden ist.) Letztendlich müssen wir noch den Fall behandeln, dass der Cluster von x nach dem Löschen von x aus dem Cluster nicht leer ist. Wenngleich wir in diesem Fall die Übersicht nicht zu aktualisieren haben, haben wir möglicherweise das Attribut max zu aktualisieren. Die Zeile 21 testet auf diesen Fall, und, falls dieser vorliegt, erfolgt die Aktualisierung von max durch Zeile 22. Wir zeigen nun, dass die Prozedur vEB-Tree-Delete Zeit O(lg lg u) im schlechtesten Fall benötigt. Auf den ersten Blick denken Sie möglicherweise, dass die Rekursionsgleichung (20.4) nicht immer greift, da ein einziger Aufruf von vEB-Tree-Delete zwei rekursive Aufrufe machen kann: einen in Zeile 13 und einen in Zeile 15. Wenngleich die Prozedur beide rekursive Aufrufe machen kann, lassen Sie uns überlegen, was passiert, wenn dies erfolgt. Damit der rekursive Aufruf in Zeile 15 erfolgen kann, muss der Test in Zeile 14 gezeigt haben, dass der Cluster von x leer ist. Die einzige Möglichkeit, die ín diesem Falle besteht, ist, dass x das einzigste Element in seinem Cluster war, als wir den rekursiven Aufruf in Zeile 13 gemacht haben. Wenn aber x das einzigste Element in seinem Cluster war, dann benötigt der entsprechende rekursive Aufruf Zeit O(1), da er nur die Zeilen 1–3 ausführt. Wir haben also zwei sich gegenseitig ausschließende Möglichkeiten: • Der rekursive Aufruf in Zeile 13 benötigt konstante Zeit. • Der rekursive Aufruf in Zeile 15 erfolgt nicht. In beiden Fällen charakterisiert die Rekursionsgleichung (20.4) die Laufzeit von vEBTree-Delete und somit ist die Laufzeit im schlechtesten Fall in O(lg lg u).
Übungen 20.3-1 Modifizieren Sie vEB-Bäume so, dass sie Mehrfachschlüssel erlauben.
564
20 van-Emde-Boas-Bäume
20.3-2 Modifizieren Sie vEB-Bäume so, dass sie Schlüssel mit dazugehörigen Satellitendaten unterstützen. 20.3-3 Geben Sie den Pseudocode für eine Prozedur an, die einen leeren van-EmdeBoas-Baum erzeugt. 20.3-4 Was passiert, wenn Sie vEB-Tree-Insert mit einem Element aufrufen, welches bereits im vEB-Baum enthalten ist? Was passiert, wenn Sie vEB-TreeDelete mit einem Element aufrufen, welches nicht in dem vEB-Baum enthalten ist? Erklären Sie, warum die Prozeduren das tun, was sie tun. Zeigen Sie, wie die vEB-Bäume und ihre Operationen zu modifizieren sind, damit wir in konstanter Zeit überprüfen können, ob ein gegebenes Element im vEB-Baum enthalten ist. 20.3-5 Nehmen Sie an, wir würden vEB-Bäume konstruieren, die aus u1/k Clustern bestehen, jeder mit einem √ Universum der Größe u1−1/k , wobei k > 1 eine ↑ Konstante ist (anstatt aus u Clustern, jeder mit einem Universum der Grö√ ße ↓ u). Wenn wir die Operationen entsprechend modifizieren würden, was wäre ihre Laufzeiten? Bei Ihrer Analyse sollten Sie voraussetzen, dass u1/k und u1−1/k immer ganze Zahlen sind. 20.3-6 Das Erzeugen eines vEB-Baumes mit einem Universum der Größe u benötigt Zeit Θ(u). Nehmen Sie an, wir wollten explizit Rechenschaft über diese Zeit ablegen. Was ist die kleinste Zahl n von Operationen, deren amortisierte Laufzeiten auf einem vEB-Baum in O(lg lg u) liegen, die wir ausführen müssten?
Problemstellungen 20-1 Speicherplatzbedarf von van-Emde-Boas-Bäumen Diese Problemstellung untersucht den Speicherplatzbedarf von van-Emde-BoasBäumen und schlägt eine Möglichkeit vor, die Datenstruktur so zu modifizieren, dass ihr Speicherplatzbedarf von der Anzahl n der gegenwärtig in dem Baum gespeicherten Elemente und nicht mehr von der√Größe u des Universums abhängt. Der Einfachheit halber setzen wir voraus, das u immer eine ganze Zahl ist. a. Erklären Sie, warum die folgende Rekursionsgleichung den Speicherplatzbedarf P (u) von einem van-Emde-Boas-Baum mit einem Universum der Größe u charakterisiert: √ √ √ P (u) = ( u + 1)P ( u) + Θ( u) . (20.5) b. Beweisen Sie, dass die Rekursionsgleichung (20.5) die Lösung P (u) = O(u) hat. Um den Speicherplatzbedarf zu reduzieren, lassen Sie uns einen van-EmdeBoas-Baum mit reduziertem Platzbedarf (oder RS-vEB-Baum) als einen vEB-Baum V mit folgenden Änderungen definieren:
Problemstellungen zu Kapitel 20
565
• Das Attribut V.cluster ist als eine Hashtabelle (siehe Kapitel 11) realisiert, die als dynamische Tabelle (siehe Abschnitt 17.4) abgespeichert ist (anstelle eines einfachen Feldes √ von Zeigern auf vEB-Bäumen mit jeweils einem Universum der Größe u). Entsprechend der Realisierung von V.cluster durch ein Feld speichert die Hashtabelle Zeiger auf RS-vEB-Bäume mit jeweils ei√ nem Universum der Größe u. Um den i-te Cluster zu finden, schlagen wir in der Hashtabelle unter dem Schlüssel i nach, sodass wir das i-te Cluster durch eine einfache Suche in der Hashtabelle finden können. • Die Hashtabelle speichert nur Zeiger auf nichtleere Cluster. Eine Suche in der Hashtabelle nach einem leeren Cluster gibt den Wert nil zurück, was angibt, dass der Cluster leer ist. • Das Attribut V.¨ u berblick ist nil, wenn alle Cluster leer sind. Anderenfalls zeigt V.¨ u bersicht auf einen RS-vEB-Baum mit einem Universum der Grö√ ße u. Da die Hashtabelle durch eine dynamische Tabelle implementiert ist, benötigt sie Speicherplatz, der proportional in der Anzahl der nichtleeren Cluster ist. Wenn wir ein Element in einen leeren RS-vEB-Baum einfügen müssen, erzeugen wir einen RS-vEB-Baum, indem wir die folgende Prozedur aufrufen, bei der der Parameter u die Größe des Universums des RS-vEB-Baums angibt: Create-New-RS-vEB-Tree(u) 1 allokiere einen neuen vEB-Baum V 2 V.u = u 3 V.min = nil 4 V.max = nil 5 V.¨ u bersicht = nil 6 erzeuge V.cluster als eine leere dynamische Hashtabelle 7 return V c. Modifizieren Sie die Prozedur vEB-Tree-Insert, um den Pseudocode der Prozedur RS-vEB-Tree-Insert(V, x) zu erhalten, der das Element x in den RS-vEB-Baum V einfügt und in dem die Prozedur Create-New-RS-vEBTree geeignet aufgerufen wird. d. Modifizieren Sie die Prozedur vEB-Tree-Successor, um den Pseudocode der Prozedur RS-vEB-Tree-Successor(V, x) zu erhalten, der den Nachfolger von x in dem RS-vEB-Baum V zurückgibt oder nil, wenn x keinen Nachfolger in V hat. e. Beweisen Sie, dass unter der Annahme eines einfachen uniformen Hashings Ihre Prozeduren RS-vEB-Tree-Insert und RS-vEB-Tree-Successor in erwarteter amortisierter Zeit O(lg lg u) laufen. f. Unter der Annahme, dass Elemente nie aus einem vEB-Baum gelöscht werden, beweisen Sie, dass der Speicherplatzbedarf einer RS-vEB-Baum-Struktur in O(n) liegt, wobei n die Anzahl der gegenwärtig in dem RS-vEB-Baum gespeicherten Elemente ist.
566
20 van-Emde-Boas-Bäume g. RS-vEB-Bäume besitzen einen Vorteil gegenüber vEB-Bäumen: sie benötigen weniger Zeit, um erzeugt zu werden. Wie viel Zeit brauchen wir, um einen leeren RS-vEB-Baum zu erzeugen?
20-2 y-schnelle-Tries Diese Problemstellung untersucht die von D. Willard eingeführten „y-schnelleTries“, die, wie van-Emde-Boas-Bäume, jede der Operationen Member, Minimum, Maximum, Predecessor und Successor angewendet auf Elemente aus einem Universum der Größe u in Zeit O(lg lg u) im schlechtesten Fall durchführen. Die Operationen Insert und Delete benötigen amortisierte Zeit O(lg lg u). Wie van-Emde-Boas-Bäume mit reduziertem Speicherplatzbedarf (siehe Problemstellung 20-1) haben y-schnelle-Tries nur einen Speicherplatzbedarf von O(n), um n Elemente zu speichern. Der Entwurf von y-schnellen-Tries basiert auf perfektem Hashing (siehe Abschnitt 11.5). Als vorbereitende Struktur nehmen Sie an, wir würden eine perfekte Hashtabelle erzeugen, die nicht nur jedes Element aus einer dynamischen Menge, sondern auch jeden Präfix der binären Darstellung eines jeden Elements aus der Menge enthält. Ist u beispielsweise u = 16, sodass lg u = 4 gilt, und x = 13 ein Element der Menge, dann würde die Hashtabelle die Strings 1, 11, 110 und 1101 enthalten, da 1101 die binäre Darstellung der 13 ist. Neben der Hashtabelle erzeugen wir eine doppelt verkettete Liste von den Elementen, die gegenwärtig in der Menge sind, in aufsteigender Reihenfolge geordnet. a. Wie groß ist der Speicherplatzbedarf dieser Struktur? b. Zeigen Sie, wie die Operationen Minimum und Maximum in Zeit O(1), die Operationen Member, Predecessor und Successor in Zeit O(lg lg u) und die Operationen Insert und Delete in Zeit O(lg u) auf dieser Struktur realisiert werden können. Um den Speicherbedarf auf O(n) zu senken, führen wir folgende Modifikationen auf der Datenstruktur durch: • Wir fassen die n Elemente in n/ lg u Gruppen der Größe lg u zusammen. (Setzen Sie im Folgenden voraus, dass lg u ein Teiler von n ist.) Die erste Gruppe besteht aus den lg u kleinsten Elementen der Menge, die zweite besteht aus den lg u nächstkleinsten Elementen und so weiter. • Wir legen für jede Gruppe einen Repräsentanten fest. Der Repräsentant der i-ten Gruppe ist wenigstens so groß wie das größte Element der i-ten Gruppe, aber kleiner als jedes Element der (i + 1)-ten Gruppe. (Der Repräsentant der letzten Gruppe kann der maximale mögliche Wert u−1 sein.) Ein Repräsentant muss nicht notwendigerweise in der gegenwärtig dargestellten Menge enthalten sein. • Wir speichern die lg u Elemente einer jeden Gruppe in einem balancierten binären Suchbaum, beispielsweise in einem Rot-Schwarz-Baum. Jeder Repräsentant zeigt auf den balancierten binären Suchbaum seiner Gruppe und jeder balancierter binärer Suchbaum zeigt auf den Repräsentanten seiner Gruppe.
Kapitelbemerkungen zu Kapitel 20
567
• Die perfekte Hashtabelle speichert die Repräsentanten, die zudem in einer doppelt verketteten Liste in aufsteigender Reihenfolge geordnet gespeichert sind. Wir nennen diese Struktur y-schneller-Trie. c. Zeigen Sie, dass ein y-schneller-Trie nur einen Speicherplatzbedarf von O(n) hat, um n Elemente zu speichern. d. Zeigen Sie, wie die Operationen Minimum und Maximum auf einem yschnellen-Trie in Zeit O(lg lg u) ausgeführt werden können. e. Zeigen Sie, wie die Operation Member in Zeit O(lg lg u) ausgeführt werden kann. f. Zeigen Sie, wie die Operationen Predecessor und Successor in Zeit O(lg lg u) ausgeführt werden können. g. Erklären Sie, warum die Operationen Insert und Delete Zeit Ω(lg lg u) benötigen. h. Zeigen Sie, wie wir die Forderung abschwächen können, dass jede Gruppe in einem y-schnellen-Trie genau lg u Elemente enthält, um die Prozeduren Insert und Delete in amortisierter Zeit von O(lg lg u) laufen lassen zu können, ohne dass dadurch die asymptotischen Laufzeiten der anderen Operationen beeinträchtigt werden.
Kapitelbemerkungen Die Datenstruktur aus diesem Kapitel hat ihren Namen nach P. van Emde Boas, der die Idee in einer ersten Form in 1975 beschrieben hat [339]. Spätere Arbeiten von van Emde Boas [340] und van Emde Boas, Kaas und Zijlstra [341] entwickelten die Idee und die Darstellung weiter. Mehlhorn und Näher [252] erweiterten anschließend die Ideen, um sie auf Universen anzuwenden, deren Größen Primzahlen sind. Das Buch von Mehlhorn [249] enthält eine leicht andere Einführung von van-Emde-Boas-Bäumen als die in diesem Kapitel vorgestellte. Unter Verwendung der Ideen von van-Emde-Boas-Bäumen entwickelten Dementiev et al. [83] einen nichtrekursiven Suchbaum mit drei Ebenen, der in ihren Versuchen schneller als die van-Emde-Boas-Bäumen liefen. Wang und Lin [347] entwickelten eine Hardware-basierte Version von van-Emde-BoasBäumen unter Verwendung von Pipelines, die konstante amortisierte Laufzeit pro Operation erreichte und O(lg lg u) Stufen in der Pipeline benutzte. Eine untere Schranke von Pˇatraşcu und Thorup [273, 274] für das Bestimmen des Vorgängers zeigt, dass van-Emde-Boas-Bäume optimal für diese Operation sind, sogar dann wenn Randomisierung erlaubt ist.
21
Datenstrukturen disjunkter Mengen
Einige Anwendungen müssen n verschiedene Elemente in ein Ensemble von disjunkten Mengen aufteilen. Diese Anwendungen müssen häufig speziell zwei Operationen oft ausführen: die eindeutige Menge bestimmen, die ein gegebenes Element enthält, und zwei Mengen vereinigen. Dieses Kapitel untersucht Methoden, eine Datenstruktur zu implementieren, die diese Operationen unterstützt. Abschnitt 21.1 beschreibt die Operationen, die durch eine Datenstruktur für disjunkte Mengen unterstützt werden sollten, und stellt eine einfache Anwendung vor. In Abschnitt 21.2 sehen wir uns eine einfache, auf verketteten Listen basierende Implementierung für disjunkte Mengen an. Abschnitt 21.3 stellt eine effizientere Darstellung vor, die gewurzelte Bäume benutzt. Benutzen wir die Baumdarstellung, so ist die Laufzeit für alle praktischen Anwendungen linear, wenngleich sie theoretisch superlinear ist. Abschnitt 21.4 definiert und diskutiert eine sehr schnell wachsende Funktion und deren sehr langsam wachsende Inverse, die in der Laufzeit der Operationen im Falle der baumbasierten Implementierung vorkommt. Mittels einer amortisierten Analyse beweisen wir in diesem Abschnitt eine obere Schranke für die Laufzeit, die nur geringfügig superlinear ist.
21.1
Operationen auf disjunkten Mengen
Eine Datenstruktur disjunkter Mengen verwaltet ein Ensemble S = {S1 , S2 , . . . , Sk } von disjunkten dynamischen Mengen. Wir kennzeichnen jede Menge durch einen Repräsentanten, der ein beliebiges Element der Menge ist. Bei einigen Anwendungen spielt es keine Rolle, welches Element als Repräsentant verwendet wird; wir sorgen nur dafür, dass wir immer dieselbe Antwort erhalten, wenn wir zweimal den Repräsentanten einer dynamischen Menge abfragen und zwischen den beiden Abfragen die Menge nicht modifiziert wird. Andere Anwendungen benötigen eine vordefinierte Regel für die Wahl des Repräsentanten, wie beispielsweise die Auswahl des kleinsten Elementes in der Menge (natürlich unter der Annahme, dass die Elemente geordnet werden können). Wie bei den anderen Implementierungen dynamischer Mengen, die wir untersucht haben, stellen wir jedes Element einer Menge durch ein Objekt dar. Wir wollen die folgenden Operationen unterstützen – x bezeichne hierbei ein Objekt: Make-Set(x) erzeugt eine neue Menge, deren einziges Element x ist (sodass x auch Repräsentant dieser Menge ist). Da die Mengen disjunkt sind, fordern wir, dass x nicht bereits in einer anderen Menge enthalten ist.
570
21 Datenstrukturen disjunkter Mengen
Union(x, y) vereinigt die dynamischen Mengen, die x und y enthalten – seien dies Sx und Sy –, zu einer neuen Menge, welche die Vereinigung dieser beiden Mengen darstellt. Wir setzen voraus, dass die beiden Mengen vor der Operation disjunkt sind. Der Repräsentant der resultierenden Menge ist ein beliebiges Element von Sx ∪ Sy , obwohl bei vielen Implementierungen von Union speziell entweder der Repräsentant von Sx oder Sy als neuer Repräsentant ausgewählt wird. Da wir fordern, dass die Mengen im Ensemble disjunkt sind, „zerstören“ wir die Mengen Sx und Sy , indem wir sie aus dem Ensemble S entfernen. In der Praxis, nehmen wir die Elemente der einen der beiden Mengen in der anderen Menge auf. Find-Set(x) gibt einen Zeiger auf den Repräsentanten der (eindeutigen) Menge zurück, die x enthält. In diesem Kapitel werden wir die Laufzeiten von Datenstrukturen disjunkter Mengen in Abhängigkeit von zwei Parametern untersuchen: der Anzahl n der MakeSet-Operationen und der Gesamtanzahl m der Make-Set-, Union- und Find-SetOperationen. Da die Mengen disjunkt sind, reduziert jede Union-Operation die Anzahl der Mengen um 1. Nach n − 1 Union-Operationen verbleibt deshalb nur eine Menge. Die Anzahl der Union-Operationen beträgt folglich höchstens n − 1. Beachten Sie außerdem, dass m ≥ n gilt, weil die Make-Set-Operationen in der Gesamtanzahl der m Operationen enthalten sind. Wir setzen voraus, dass die n Make-Set-Operationen die ersten n ausgeführten Operationen sind.
Eine Anwendung für Datenstrukturen disjunkter Mengen Eine von vielen Anwendungen von Datenstrukturen disjunkter Mengen ist die Bestimmung der Zusammenhangskomponenten eines ungerichteten Graphen (siehe Abschnitt B.4). Abbildung 21.1(a) zeigt zum Beispiel einen Graphen mit vier Zusammenhangskomponenten. Die folgende Prozedur Connected-Components verwendet die Operationen disjunkter Mengen, um die Zusammenhangskomponenten eines Graphen zu berechnen. Nachdem Connected-Components den Graphen bearbeitet hat, beantwortet SameComponent die Abfragen, ob sich zwei Knoten in derselben Zusammenhangskomponente befinden.1 (Im Pseudocode bezeichnen wir die Knotenmenge eines Graphen G mit G.V und die Kantenmenge mit G.E .) Connected-Components(G) 1 for jeden Knoten v ∈ G.V 2 Make-Set(v) 3 for jede Kante (u, v) ∈ G.E 4 if Find-Set(u) = Find-Set(v) 5 Union(u, v) 1 Wenn die Kanten des Graphen „statisch“ sind – sich also über die Zeit nicht verändern –, dann können wir die Zusammenhangskomponenten schneller berechnen, indem wir Tiefensuche anwenden (siehe Übung 22.3-12). Manchmal werden jedoch Kanten „dynamisch“ hinzugefügt und wir müssen die Zusammenhangskomponenten beim Hinzufügen jeder Kante aktualisieren. In diesem Fall kann die hier angegebene Implementierung effizienter sein, als eine neue Tiefensuche für jede neue Kante zu starten.
21.1 Operationen auf disjunkten Mengen a
b
e
c
d
g
f
h
571 j
i (a)
bearbeitete Kante Ausgangsmengen (b,d) (e,g) (a,c) (h,i) (a,b) (e, f ) (b,c)
{a} {a} {a} {a,c} {a,c} {a,b,c,d} {a,b,c,d} {a,b,c,d}
Ensembles disjunkter Mengen {b} {c} {d} {e} {f} {g} {b,d} {c} {e} {f} {g} {b,d} {c} {e,g} {f} {b,d} {e,g} {f} {b,d} {e,g} {f} {e,g} {f} {e, f,g} {e, f,g}
{h} {h} {h} {h} {h,i} {h,i} {h,i} {h,i}
{i} {i} {i} {i}
{j} {j} {j} {j} {j} {j} {j} {j}
(b) Abbildung 21.1: (a) Ein Graph mit vier Zusammenhangskomponenten: {a, b, c, d}, {e, f, g}, {h, i} und {j}. (b) Das Ensemble disjunkter Mengen, jeweils nachdem die jeweilige Kante bearbeitet wurde.
Same-Component(u, v) 1 if Find-Set(u) = = Find-Set(v) 2 return wahr 3 else return falsch Die Prozedur Connected-Components platziert anfangs jeden Knoten v in seine eigene Menge. Anschließend vereinigt sie für jede Kante (u, v) die Mengen, die die Knoten u und v enthalten. Nachdem alle Kanten bearbeitet wurden, befinden sich nach Übung 21.1-2 zwei Knoten genau dann in derselben Zusammenhangskomponente, wenn sich die zugehörigen Objekte in der gleichen Menge befinden. Folglich berechnet Connected-Components die Mengen so, dass die Prozedur Same-Component bestimmen kann, ob sich zwei Knoten in derselben Zusammenhangskomponente befinden. Abbildung 21.1(b) illustriert, wie Connected-Components die disjunkten Mengen bestimmt. Bei der tatsächlichen Implementierung dieses Algorithmus zur Bestimmung der Zusammenhangskomponenten müssen die Darstellung des Graphen und die Datenstruktur disjunkter Mengen aufeinander verweisen. Das heißt, ein Objekt, das einen Knoten repräsentiert, muss einen Zeiger auf das zugehörige Objekt innerhalb der disjunkten Mengen enthalten und umgekehrt. Diese Programmierdetails hängen von der zur Implementierung verwendeten Sprache ab. Wir werden uns hier nicht weiter um diese Details kümmern.
572
21 Datenstrukturen disjunkter Mengen
Übungen 21.1-1 Nehmen Sie an, Connected-Components würde auf dem ungerichteten Graphen G = (V, E) mit V = {a, b, c, d, e, f, g, h, i, j, k} angewendet, wobei die Kanten von E in der Reihenfolge (d, i), (f, k), (g, i), (b, g), (a, h), (i, j), (d, k), (b, j), (d, f ), (g, j), (a, e) bearbeitet würden. Geben Sie die Knoten jeder Zusammenhangskomponente nach jeder Iteration der Zeilen 3–5 an. 21.1-2 Zeigen Sie, dass sich, nachdem alle Kanten von Connected-Components bearbeitet wurden, zwei Knoten genau dann in derselben Zusammenhangskomponente befinden, wenn sie zu derselben Menge gehören. 21.1-3 Wie oft wird Find-Set beim Ausführen von Connected-Components auf einem ungerichteten Graphen G = (V, E) mit k Zusammenhangskomponenten aufgerufen? Wie oft wird Union aufgerufen? Geben Sie Ihre Antwort als Funktion in |V |, |E| und k an.
21.2
Darstellung disjunkter Mengen mithilfe verketteter Listen
Abbildung 21.2(a) zeigt eine einfache Möglichkeit zur Implementierung einer Datenstruktur disjunkter Mengen: jede Menge wird durch eine eigene verkettete Liste dargestellt. Das Objekt einer jeden Menge besitzt das Attribut kopf , das auf das erste Objekt der Liste zeigt, und das Attribut ende, das auf das letzte Objekt der Liste zeigt. Jedes Objekt einer Liste enthält ein Element der Menge, einen Zeiger auf das nächste Objekt in der Liste und einen Zeiger zurück auf das Mengenobjekt. Innerhalb jeder verketteten Liste können die Objekte in einer beliebigen Reihenfolge gespeichert sein. Der Repräsentant ist das Element der Menge, das in dem ersten Objekt in der Liste gespeichert ist. Mit der Darstellung durch verkettete Listen sind sowohl Make-Set als auch Find-Set einfach. Sie erfordern Zeit O(1). Um Make-Set(x) auszuführen, erzeugen wir eine neue verkettete Liste, deren einziges Objekt x ist. Bei Find-Set(x) folgen wir einfach nur dem Zeiger von x zurück zu seinem Mengenobjekt und geben dann das Mengenelement aus dem Objekt zurück, auf das kopf zeigt. In Abbildung 21.2(a) würde beispielsweise der Aufruf Find-Set(g) das Element f zurückgeben.
Eine einfache Implementierung der Vereinigung Die einfachste Implementierung der Union-Operation, welche die Darstellung mithilfe verketteter Listen verwendet, benötigt signifikant mehr Zeit als Make-Set oder FindSet. Wie Abbildung 21.2(b) zeigt, führen wir Union(x, y) aus, indem wir die Liste von y an das Ende der Liste von x anhängen. Der Repräsentant von x’s Liste wird der Repräsentant der resultierenden Liste. Wir verwenden den Zeiger ende der Liste von x,
21.2 Darstellung disjunkter Mengen mithilfe verketteter Listen
f
(a)
g
d
c
kopf
573
h
e
b
kopf
S1
S2 ende
ende
f
(b)
g
d
c
h
e
b
kopf S1 ende
Abbildung 21.2: (a) Darstellungen zweier Mengen mithilfe verketteter Listen. Die Menge S1 enthält die Elemente d, f und g mit dem Repräsentanten f , und die Menge S2 enthält die Elemente b, c, e und h mit dem Repräsentanten c. Jedes Objekt der Liste enthält ein Element der Menge, einen Zeiger auf das nächste Objekt in der Liste und einen Zeiger auf das Mengenobjekt. Jede Liste besitzt die Zeiger kopf und ende auf das erste bzw. letzte Objekt. (b) Das Ergebnis von Union(g, e), das die verkettete Liste, die e enthält, hinter die verkettete Liste, die g enthält, hängt. Der Repräsentant der resultierenden Menge ist f . Das Mengenobjekt S2 von e’s Liste wird zerstört.
um schnell herauszufinden, wo die Liste von y anzuhängen ist. Da alle Elemente von y’s Liste in die Liste von x eingefügt werden, können wir das Mengenelement von y zerstören. Leider müssen wir den Zeiger auf das Mengenobjekt in allen Objekten, die ursprünglich in der Liste von y waren, aktualisieren, was eine zur Länge der Liste von y lineare Zeit in Anspruch nimmt. In Abbildung 21.2 beispielsweise müssen wir aufgrund der Operation Union(g, e) die Zeiger in den Objekten b, c, e und h aktualisieren. Tatsächlich können wir einfach eine Folge von m Operationen auf n Objekten konstruieren, die Zeit Θ(n2 ) benötigt. Nehmen Sie an, wir hätten die Objekte x1 , x2 , . . . , xn gegeben. Wir führen, wie in Abbildung 21.3 gezeigt, n Make-Set-Operationen gefolgt von n−1 Union-Operationen aus, sodass m = 2n−1 gilt. Wir verbrauchen Zeit Θ(n) für das Ausführen der n Make-Set-Operationen. Da die i-te Union-Operation i Objekte aktualisiert, ergibt sich für die Gesamtanzahl der von den n − 1 Union-Operationen aktualisierten Objekte n−1
i = Θ(n2 ) .
i=1
Die Gesamtanzahl der Operationen ist 2n − 1. Folglich nimmt jede Operation im Mittel Zeit Θ(n) in Anspruch.Die amortisierte Laufzeit einer Operation beträgt also Θ(n).
574
21 Datenstrukturen disjunkter Mengen
Operation Make-Set(x1 ) Make-Set(x2 ) .. .
Anzahl der aktualisierten Objekte 1 1 .. .
Make-Set(xn ) Union(x2 , x1 ) Union(x3 , x2 ) Union(x4 , x3 ) .. .
1 1 2 3 .. .
Union(xn , xn−1 )
n−1
Abbildung 21.3: Eine Folge von 2n − 1 Operationen auf n Objekten, die Zeit Θ(n2 ) benötigt, oder im Mittel Zeit Θ(n) pro Operation, wenn eine Darstellung mithilfe verketteter Listen und die einfache Implementierung von Union verwendet wird.
Die Heuristik der gewichteten Vereinigung Im schlechtesten Fall benötigt die obige Implementierung der Prozedur Union im Mittel Zeit Θ(n) pro Aufruf, da es sein kann, dass wir eine längere Liste an eine kürzere anhängen; wir müssen den Zeiger auf das Mengenobjekt für jedes Element der längeren Liste aktualisieren. Setzen Sie stattdessen voraus, dass jede Liste auch die Länge der Liste abspeichert (die wir einfach verwalten können) und dass wir immer die kürzere Liste an die längere anhängen – sind beide Listen gleich lang, so wählen wir eine von beiden beliebig aus, um sie an die andere anzuhängen. Mit dieser einfachen Heuristik der gewichteten Vereinigung kann eine Union-Operation immer noch Zeit Ω(n) benötigen, wenn beide Mengen Ω(n) Elemente enthalten. Wie das folgende Theorem zeigt, benötigt jedoch eine Folge von m Make-Set-, Union- und Find-Set-Operationen, von denen n Operationen Make-Set-Operationen sind, nur Zeit O(m + n lg n). Theorem 21.1 Werden verkettete Listen zur Darstellung disjunkter Mengen verwendet und zum Vereinigen von Mengen die Heuristik der gewichteten Vereinigung angewendet, dann benötigt eine Folge von m Make-Set-, Union- und Find-Set-Operationen, von denen n Operationen Make-Set-Operationen sind, Zeit O(m + n lg n). Beweis: Da jede Operation Union zwei disjunkte Mengen vereinigt, führen wir insgesamt höchstens n − 1 Union-Operationen aus. Wir beschränken nun die Gesamtzeit, die durch diese Union-Operationen benötigt werden. Wir beginnen damit, uns für jedes Objekt eine obere Schranke zu überlegen, wie oft der Zeiger des Objektes auf sein Mengenobjekt aktualisiert wird. Betrachten Sie ein festes Objekt x. Wir wissen, dass jedes Mal, wenn der zu x gehörige Zeiger auf den Repräsentanten aktualisiert wird, sich x in der kürzeren Liste befunden haben muss. Deshalb muss die resultierende Menge bei der ersten Aktualisierung des Zeigers von x auf den Repräsentanten mindestens zwei Elemente gehabt haben. Analog dazu muss die resultierende Menge bei der nächsten
21.2 Darstellung disjunkter Mengen mithilfe verketteter Listen
575
Aktualisierung des Zeigers von x mindestens vier Elemente gehabt haben. Fahren wir in dieser Weise fort, stellen wir fest, dass die resultierende Menge für beliebige k ≤ n nach lg k Aktualisierungen mindestens k Elemente haben muss. Da die größte Menge höchstens n Elemente besitzt, wird der Zeiger eines jeden Objekts insgesamt über alle Union-Operationen höchstens lg n Mal aktualisiert. Damit ist die Gesamtzeit, die für das Aktualisieren der Objektzeigern benötigt wird, über alle Union-Operationen in O(n lg n). Wir müssen auch die Aktualisierungen der Zeiger kopf und ende sowie der Listenlänge berücksichtigen, die aber lediglich Zeit Θ(1) pro Union-Operation benötigen. Die für die Aktualisierung der n Objekte aufgewendete Gesamtzeit ist folglich O(n lg n). Die Laufzeit der gesamten Folge von m Operationen ist leicht abzuleiten. Jede MakeSet- und Find-Set-Operation benötigt Zeit O(1), und es gibt O(m) von ihnen. Die Gesamtzeit für die gesamte Folge ist folglich O(m + n lg n).
Übungen 21.2-1 Geben Sie den Pseudocode für die Operationen Make-Set, Find-Set und Union an, in dem die disjunkten Mengen über verkettete Listen dargestellt werden und die Heuristik der gewichteten Vereinigung benutzt wird. Gewährleisten Sie, dass die Attribute, die sie für Mengenobjekte und Listenobjekte voraussetzen, spezifiert sind. 21.2-2 Geben Sie die Datenstrukturen an, die sich durch die Find-Set-Operationen im folgenden Programm ergeben, sowie die dabei zurückgegebenen Daten. Verwenden Sie die Darstellung mithilfe verketteter Listen und die Heuristik der gewichteten Vereinigung. 1 2 3 4 5 6 7 8 9 10 11
for i = 1 to 16 Make-Set(xi ) for i = 1 to 15 by 2 Union(xi , xi+1 ) for i = 1 to 13 by 4 Union(xi , xi+2 ) Union(x1 , x5 ) Union(x11 , x13 ) Union(x1 , x10 ) Find-Set(x2 ) Find-Set(x9 )
Setzen Sie dabei voraus, dass, wenn die Mengen, die die Elemente xi und xj enthalten, die gleiche Größe besitzen, die Operation Union(xi , xj ) xj ’s Liste an die von xi hängt. 21.2-3 Passen Sie den Aggregat-Beweis aus Theorem 21.1 an, um amortisierte Zeitschranken von O(1) für Make-Set und Find-Set und von O(lg n) für Union
576
21 Datenstrukturen disjunkter Mengen zu erhalten. Setzen Sie dabei voraus, dass die disjunkten Mengen durch verkettete Listen dargestellt werden und die Heuristik der gewichteten Vereinigung angewendet wird.
21.2-4 Geben Sie asymptotisch scharfe Schranken für die Laufzeit der Folge der Operationen aus Abbildung 21.3 an. Setzen Sie dabei voraus, dass die disjunkten Mengen durch verkettete Listen dargestellt werden und die Heuristik der gewichteten Vereinigung angewendet wird. 21.2-5 Professor Gompers vermutet, dass es sein könnte, nur mit einem Zeiger (anstelle der zwei Zeigern kopf und ende) pro Mengenobjekt auszukommen, und dabei die Anzahl der Zeiger pro Listenelement bei zwei zu belassen. Zeigen Sie, dass die Vermutung des Professors wohlbegründet ist, indem Sie angeben, wie jede Menge so durch eine verkettete Liste dargestellt werden kann, dass jede Operation die gleiche Laufzeit hat wie die Operationen, die in diesem Abschnitt beschrieben worden sind. Beschreiben Sie auch, wie die Operationen arbeiten. Ihre Idee sollte die Heuristik der gewichteten Vereinigung mit einbeziehen und zwar mit den gleichen Auswirkungen wie die, die wir in diesem Abschnitt angegeben haben. (Hinweis: Benutzen Sie das Ende der verketteten Liste als Repräsentant der Liste.) 21.2-6 Schlagen Sie eine einfache Modifikation der Prozedur Union für die Darstellung disjunkter Mengen durch verkettete Listen vor, welche die Notwendigkeit beseitigt, den Zeiger ende auf das letzte Objekt in jeder Liste zu verwalten. Unabhängig davon, ob die Heuristik der gewichteten Vereinigung angewendet wird, sollte Ihre Veränderung die asymptotische Laufzeit der Prozedur Union nicht beeinflussen. (Hinweis: „Verkleben“ Sie die Listen, statt eine Liste an die andere anzuhängen.)
21.3
Wälder disjunkter Mengen
Bei einer in Bezug auf die Laufzeit bessere Implementierung disjunkter Mengen stellen wir die Mengen durch gewurzelte Bäume dar, wobei jeder Knoten ein Element der Menge und jeder Baum eine Menge darstellt. In einem Wald disjunkter Mengen zeigt jedes Element, wie in Abbildung 21.4(a) illustriert, nur auf seinen Vater. Die Wurzel jedes Baumes enthält den Repräsentanten und ist sein eigener Vater. Wie wir sehen werden, erhalten wir durch zwei Heuristiken – „Vereinigung nach dem Rang“ und „Pfadverkürzung“ – eine asymptotisch optimale Datenstruktur disjunkter Mengen, obwohl die einfachen Algorithmen, die wir vorhin angegeben haben, nicht schneller auf dieser neuen Datenstruktur sind als auf verketteten Listen. Wir führen die drei Operationen auf disjunkten Mengen wie folgt aus. Eine MakeSet-Operation erzeugt einen Baum mit nur einem Knoten. Wir führen eine Find-SetOperation aus, indem wir den Vaterzeigern folgen, bis wir auf die Wurzel des Baumes treffen. Die auf dem Pfad zur Wurzel besuchten Knoten bilden den Bestimmungspfad . Eine Union-Operation, wie in Abbildung 21.4(b) gezeigt, bewirkt, dass die Wurzel eines Baumes auf die Wurzel des anderen zeigt.
21.3 Wälder disjunkter Mengen
c h
577
f e
f
d
b
g
(a)
c h b
d e
g
(b)
Abbildung 21.4: Ein Wald disjunkter Mengen. (a) Zwei Bäume, die die beiden Mengen aus Abbildung 21.2 repräsentieren. Der Baum auf der linken Seite stellt die Menge {b, c, e, h} mit dem Repräsentanten c dar, und der Baum auf der rechten Seite stellt die Menge {d, f, g} mit dem Repräsentanten f dar. (b) Das Ergebnis von Union(e, g).
Heuristiken zum Verbessern der Laufzeit Bisher haben wir von der Laufzeit her gesehen die Implementierung durch verkettete Listen nicht verbessert. Eine Folge von n − 1 Union-Operationen kann einen Baum erzeugen, der einfach aus einer linearen Kette von n Knoten besteht. Unter Verwendung von zwei Heuristiken können wir jedoch eine Laufzeit erzielen, die fast linear in der Gesamtanzahl m der Operationen ist. Die erste Heuristik, Vereinigung nach dem Rang (engl.: union by rank ), ähnelt der Heuristik der gewichteten Vereinigung, die wir bei der Darstellung durch verkettete Listen verwendet haben. Der naheliegendste Ansatz besteht darin, die Wurzel des Baumes mit weniger Knoten auf die Wurzel des Baumes mit mehr Knoten zeigen zu lassen. Anstatt die Größe des entsprechenden Teilbaumes an jedem Knoten explizit zu speichern, werden wir eine Methode verwenden, die die Analyse erleichtert. Für jeden Knoten verwalten wir einen Rang , der eine obere Schranke für die Höhe des Knotens darstellt. Bei der Vereinigung nach dem Rang lassen wir die Wurzel mit dem kleineren Rang auf die Wurzel mit dem größeren Rang zeigen. Die zweite Heuristik, Pfadverkürzung (engl.: path compression), ist ebenfalls recht einfach und sehr effektiv. Wie in Abbildung 21.5 gezeigt, verwenden wir sie während der Find-Set-Operationen, um zu erreichen, dass jeder Knoten auf dem Bestimmungspfad direkt auf die Wurzel zeigt. Die Pfadverkürzung verändert den Rang der Bäume nicht.
Pseudocode für Wälder disjunkter Mengen Um einen Wald disjunkter Mengen mithilfe der Heuristik der Vereinigung nach dem Rang zu implementieren, müssen wir die Ränge verwalten. Mit jedem Knoten x verwalten wir einen ganzzahligen Wert x.rang, der eine obere Schranke für die Höhe von x (die Anzahl der Kanten auf dem längsten einfachen Pfad zwischen einem Nachfolger-Blatt von x und x) angibt. Wenn Make-Set eine einelementige Menge erzeugt, so hat der eine Knoten in dem Baum einen initialen Rang von 0. Bei der Union-Operation gibt es in Abhängigkeit, ob die Wurzeln der Bäume gleichen Rang haben, zwei Fälle. Wenn die
578
21 Datenstrukturen disjunkter Mengen f
e f d
c
a
b
c
d
e
b
a
(a)
(b)
Abbildung 21.5: Pfadverkürzung während der Operation Find-Set. Pfeile und Schlingen wurden weggelassen. (a) Ein Baum, der eine Menge vor der Ausführung von Find-Set(a) darstellt. Dreiecke repräsentieren Teilbäume, deren Wurzeln die angegebenen Knoten sind. Jeder Knoten besitzt einen Zeiger auf seinen Vater. (b) Dieselbe Menge nach der Ausführung von Find-Set(a). Jeder Knoten auf dem Bestimmungspfad zeigt nun direkt auf die Wurzel.
Ränge der Wurzeln ungleich sind, machen wir die Wurzel mit größerem Rang zum Vater der Wurzel mit niedrigerem Rang, die Ränge selbst bleiben aber unverändert. Wenn dagegen die Wurzeln gleichen Rang besitzen, wählen wir willkürlich eine der Wurzeln als Vater aus und inkrementieren deren Rang. Lassen Sie uns diese Methode in Pseudocode gießen. Wir bezeichnen den Vater des Knotens x mit x.vater . Die Prozedur Link, eine von Union aufgerufenen Unterroutine, erhält als Eingabe die Zeiger auf die zwei Wurzeln. Make-Set(x) 1 x.vater = x 2 x.rang = 0 Union(x, y) 1 Link(Find-Set(x), Find-Set(y)) Link(x, y) 1 if x.rang > y.rang 2 y.vater = x 3 else x.vater = y 4 if x.rang = = y.rang 5 y.rang = y.rang + 1
21.3 Wälder disjunkter Mengen
579
Die Prozedur Find-Set mit Pfadverkürzung ist ziemlich einfach.
Find-Set(x) 1 if x = x.vater 2 x.vater = Find-Set(x.vater ) 3 return x.vater Die Prozedur Find-Set ist ein Zweischrittverfahren (engl.: two-pass method ): Wenn sie rekursiv absteigt, dann durchläuft sie einmal den Bestimmungpfad nach oben, um die Wurzel zu finden, und, wenn sie aus der Rekursion wieder hochsteigt, durchläuft sie den Bestimmungspfad erneut und aktualisiert jeden Knoten auf dem Bestimmungspfad so, dass er danach direkt auf die Wurzel seines Baumes zeigt. Jeder Aufruf von Find-Set(x) gibt in Zeile 3 x.vater zurück. Wenn x die Wurzel ist, dann wird Zeile 2 nicht ausgeführt und gibt nur x.vater zurück, was bei der Wurzel gleich x ist. In diesem Fall terminiert die Rekursion. Anderenfalls wird Zeile 2 ausgeführt und der rekursive Aufruf mit dem Parameter x.vater gibt einen Zeiger auf die Wurzel zurück. Zeile 2 aktualisiert den Knoten x so, dass er direkt auf die Wurzel zeigt, und Zeile 3 gibt diesen Zeiger zurück.
Auswirkungen der Heuristik auf die Laufzeit Einzeln betrachtet verbessern sowohl die Heuristik der Vereinigung nach dem Rang als auch die Pfadverkürzung die Laufzeit der Operationen auf Wäldern disjunkter Mengen. Die Verbesserung ist noch größer, wenn wir beide Heuristiken zusammen verwenden. Isoliert betrachtet führt die Heuristik der Vereinigung nach dem Rang zu einer Laufzeit von O(m lg n) (siehe Übung 21.4-4), und diese Schranke ist scharf (siehe Übung 21.3-3). Obwohl wir dies hier nicht beweisen werden, gilt für eine Folge von n Make-Set-Operationen (und folglich höchstens n − 1 Union-Operationen) und f Find-Set-Operationen, dass allein die Heuristik der Pfadverkürzung zu einer Laufzeit im schlechtesten Fall von Θ(n + f · (1 + log2+f /n n)) führt. Wenn wir die Heuristik der Vereinigung nach dem Rang als auch die Pfadverkürzung beide zusammen benutzen, dann ist die Laufzeit im schlechtesten Fall O(mα(n)), wobei α(n) eine sehr langsam wachsende Funktion ist, die wir in Abschnitt 21.4 definieren. Bei jeder denkbaren Anwendung der Datenstruktur disjunkter Mengen gilt α(n) ≤ 4. Folglich können wir die Laufzeit in allen praktischen Situationen als linear in m betrachten. Streng genommen ist die Laufzeit jedoch superlinear. In Abschnitt 21.4 beweisen wir diese obere Schranke.
Übungen 21.3-1 Lösen Sie die Übung 21.2-2 erneut, diesmal indem sie als Darstellung disjunkter Mengen einen Wald voraussetzen und davon ausgehen, dass die beiden Heuristiken Vereinigung nach dem Rang und Pfadverkürzung benutzt werden. 21.3-2 Geben Sie eine nichtrekursive Version von Find-Set mit Pfadverkürzung an.
580
21 Datenstrukturen disjunkter Mengen
21.3-3 Geben Sie eine Folge von m Make-Set-, Union- und Find-Set-Operationen an, von denen n Operationen Make-Set-Operationen sind, die Laufzeit Ω(m lg n) benötigt, wenn wir lediglich die Heuristik der Vereinigung nach dem Rang verwenden. 21.3-4 Nehmen Sie an, wir wollten gerne die Operation Print-Set(x), die zu einem gegebenen Knoten x alle Elemente der Menge von x in einer beliebigen Reihenfolge druckt, zu den Operationen hinzufügen. Zeigen Sie, wie wir nur ein einziges Attribut zu jedem Knoten in einem Wald disjunkter Menge so hinzufügen können, dass Print-Set(x) eine Laufzeit benötigt, die linear in der Anzahl der Elemente der Menge von x ist, und die asymptotischen Laufzeiten der anderen Operationen unverändert bleiben. Setzen Sie voraus, dass wir ein Element der Menge in Zeit O(1) drucken können. 21.3-5∗ Zeigen Sie, dass jede beliebige Folge aus m Make-Set-, Find-Set- und Link-Operationen lediglich Zeit O(m) in Anspruch nimmt, wenn alle LinkOperationen vor den Find-Set-Operationen auftreten und wir sowohl die Heuristik der Pfadverkürzung als auch die Heuristik der Vereinigung nach dem Rang benutzen. Was passiert in dieser Situation, wenn wir nur die Heuristik der Pfadverkürzung verwenden?
∗ 21.4 Analyse der Vereinigung nach dem Rang mit Pfadverkürzung Wie in Abschnitt 21.3 erwähnt, ist die Laufzeit für m Operationen auf disjunkten Mengen mit n Elementen in O(m α(n)), wenn die beiden Heuristiken Pfadverkürzung und Vereinigung nach dem Rang gemeinsam eingesetzt werden. In diesem Abschnitt werden wir die Funktion α definieren und untersuchen, um zu sehen, wie langsam diese wächst. Dann beweisen wir die genannte Laufzeit unter Verwendung der Potentialmethode der amortisierten Analyse.
Eine sehr schnell wachsende Funktion und deren sehr langsam wachsende Inverse Für die ganzen Zahlen k ≥ 0 und j ≥ 1 definieren wir die Funktion Ak (j) durch < Ak (j) =
j+1
falls k = 0 ,
(j+1) Ak−1 (j)
falls k ≥ 1 ,
(j+1)
wobei der Ausdruck Ak−1 (j) die in Abschnitt 3.2 eingeführte Notation der funktionalen (0)
(i)
(i−1)
Iteration benutzt. Speziell gilt Ak−1 (j) = j und Ak−1 (j) = Ak−1 (Ak−1 (j)) für i ≥ 1. Wir werden den Parameter k als die Ebene der Funktion A bezeichnen.
21.4 ∗ Analyse der Vereinigung nach dem Rang mit Pfadverkürzung
581
Die Funktion Ak (j) ist sowohl in j als auch in k streng monoton steigend. Um genau zu sehen, wie schnell diese Funktion wächst, verschaffen wir uns zuerst geschlossene Ausdrücke für A1 (j) und A2 (j). Lemma 21.2 Für eine beliebige ganze Zahl j ≥ 1 gilt A1 (j) = 2j + 1. (i)
Beweis: Wir zeigen zunächst durch Induktion bezüglich i, dass A0 (j) = j + i gilt. (0) Den Induktionsanfang bildet A0 (j) = j = j + 0. Im Induktionsschritt nehmen wir (i−1) (i) (i−1) (j) = j + (i − 1) an. Dann gilt A0 (j) = A0 (A0 (j)) = (j + (i − 1)) + 1 = j + i. A0 (j+1) (j) = j + (j + 1) = 2j + 1 gilt. Schließlich stellen wir fest, dass A1 (j) = A0
Lemma 21.3 Für eine beliebige ganze Zahl j ≥ 1 gilt A2 (j) = 2j+1 (j + 1) − 1. (i)
Beweis: Wir zeigen zunächst durch Induktion bezüglich i, dass A1 (j) = 2i (j + 1) − 1 (0) gilt. Den Induktionsanfang bildet A1 (j) = j = 20 (j + 1) − 1. Im Induktionsschritt (i−1) (i) (i−1) i−1 (j) = 2 (j + 1) − 1 an. Dann gilt A1 (j) = A1 (A1 (j)) = nehmen wir A1 i−1 i−1 i i A1 (2 (j + 1) − 1) = 2 · (2 (j + 1) − 1) + 1 = 2 (j + 1) − 2 + 1 = 2 (j + 1) − 1. (j+1) Schließlich stellen wir fest, dass A2 (j) = A1 (j) = 2j+1 (j + 1) − 1 gilt. Nun können wir uns davon überzeugen, wie schnell Ak (j) wächst, indem wir einfach den Ausdruck Ak (1) für die Ebenen k = 0, 1, 2, 3, 4 untersuchen. Aus der Definition von A0 (k) und den obigen Lemmata wissen wir, dass A0 (1) = 1+1 = 2, A1 (1) = 2·1+1 = 3 und A2 (1) = 21+1 · (1 + 1) − 1 = 7 gilt. Außerdem gilt (2)
A3 (1) = A2 (1) = A2 (A2 (1)) = A2 (7) = 28 · 8 − 1 = 211 − 1 = 2047 und
582
21 Datenstrukturen disjunkter Mengen
(2)
A4 (1) = A3 (1) = A3 (A3 (1)) = A3 (2047) (2048)
(2047) = A2 A2 (2047) = 22048 · 2048 − 1 > 22048 = (24 )512 = 16512 1080 , was der geschätzten Anzahl der Atome im beobachtbaren Universum entspricht. (Das Zeichen „“ bezeichnet die „sehr-viel-größer-als“-Relation.) Wir definieren die Inverse der Funktion Ak (n) für ganzzahlige n ≥ 0 durch α(n) = min {k : Ak (1) ≥ n} . Mit anderen Worten, α(n) gibt die niedrigste Ebene k an, für die Ak (1) mindestens den Wert n hat. Aus den obigen Werten von Ak (1) können wir entnehmen, dass ⎧ 0 für 0 ≤ n ≤ 2 , ⎪ ⎪ ⎪ ⎪ ⎪ ⎨ 1 für n = 3 , α(n) = 2 für 4 ≤ n ≤ 7 , ⎪ ⎪ ⎪ 3 für 8 ≤ n ≤ 2047 , ⎪ ⎪ ⎩ 4 für 2048 ≤ n ≤ A4 (1) gilt. Nur für Werte von n so groß, dass der Begriff „astronomisch“ sie noch nicht groß genug beschreibt (größer als A4 (1), also eine riesige Zahl), gilt α(n) > 4. Für alle praktischen Zwecke können wir also α(n) ≤ 4 voraussetzen.
Eigenschaften des Ranges Im verbleibenden Teil dieses Abschnitts beweisen wir eine O(m α(n))-Schranke für die Laufzeit von Operationen auf disjunkten Mengen für den Fall, dass die beiden Heuristiken Pfadverkürzung und Vereinigung nach dem Rang verwendet werden. Um diese Schranken zu beweisen, zeigen wir zuerst einige einfache Eigenschaften der Ränge. Lemma 21.4 Für alle Knoten x gilt x.rang ≤ x.vater .rang, wobei die strenge Ungleichung für x = x.vater gilt. Der Wert von x.rang ist anfangs 0 und steigt mit der Zeit, bis
21.4 ∗ Analyse der Vereinigung nach dem Rang mit Pfadverkürzung
583
x = x.vater gilt; x.rang verändert sich von diesem Zeitpunkt an nicht mehr. Der Wert von x.vater .rang ist bezüglich der Zeit monoton steigend. Beweis: Der Beweis folgt durch Induktion bezüglich der Anzahl der Operationen, unter Verwendung der in Abschnitt 21.3 vorgestellten Implementierungen von MakeSet, Union und Find-Set. Wir überlassen den Beweis der Übung 21.4-1.
Korollar 21.5 Wenn wir den einfachen Pfad von einem Knoten zu einer Wurzel verfolgen, so ist der Knotenrang streng steigend. Lemma 21.6 Jeder Knoten besitzt höchstens den Rang n − 1. Beweis: Der Rang jedes Knotens beginnt bei 0 und erhöht sich nur infolge von LinkOperationen. Da es höchstens n−1 Union-Operationen gibt, gibt es ebenfalls höchstens n− 1 Link-Operationen. Da jede Link-Operation entweder die Ränge unverändert lässt oder die Ränge einiger Knoten um 1 erhöht, haben alle Ränge höchstens den Wert n−1. Lemma 21.6 liefert nur eine schwache Schranke für die Ränge. Tatsächlich besitzt jeder Knoten höchstens den Rang lg n (siehe Übung 21.4-2). Die schwache Schranke aus Lemma 21.6 wird jedoch für unsere Zwecke ausreichen.
Beweis der Zeitschranke Wir werden die Potentialmethode der amortisierten Analyse (siehe Abschnitt 17.3) verwenden, um die Zeitschranke O(m α(n)) zu beweisen. Um die amortisierte Analyse durchzuführen, werden wir es zweckmäßig finden, vorauszusetzen, dass wir die LinkOperation anstelle der Union-Operation aufrufen. Das heißt, wir tun so, als ob wir die Find-Set-Operationen zur Bestimmung der Zeiger auf die beiden Wurzeln, die der Prozedur Link übergeben werden, separat ausführen würden. Das folgende Lemma zeigt, dass die asymptotische Laufzeit unverändert bleibt, auch wenn wir die durch die Union-Aufrufe verursachten zusätzlichen Find-Set-Operationen mitzählen. Lemma 21.7 Nehmen Sie an, wir würden eine Folge S von m Make-Set-, Union- und Find-Set-Operationen in eine Folge S von m Make-Set-, Link- und FindSet-Operationen überführen, indem wir jede Union-Operation in zwei Find-SetOperationen gefolgt von einer Link-Operation umwandeln. Dann läuft die Folge S in Zeit O(m α(n)), wenn die Folge S in Zeit O(m α(n)) läuft.
584
21 Datenstrukturen disjunkter Mengen
Beweis: Da jede Union-Operation in der Folge S in drei Operationen in S überführt wird, gilt m ≤ m ≤ 3m . Da m = O(m ) ist, impliziert eine O(m α(n))-Zeitschranke für die überführte Folge S eine O(m α(n))-Zeitschranke für die ursprüngliche Folge S . Im verbleibenden Teil dieses Abschnitts werden wir voraussetzen, dass die Ausgangssequenz von m Make-Set-, Union- und Find-Set-Operationen in eine Folge von m Make-Set-, Link- und Find-Set-Operationen überführt wurde. Wir beweisen nun eine O(m α(n))-Zeitschranke für die überführte Folge und wenden Lemma 21.7 an, um die Laufzeit O(m α(n)) der ursprünglichen Folge von m Operationen zu beweisen.
Die Potentialfunktion Die von uns verwendete Potentialfunktion ordnet nach q Operationen jedem Knoten x aus dem Wald disjunkter Mengen ein Potential φq (x) zu. Wir summieren überdie Knotenpotentiale, um das Potential des gesamten Waldes zu bestimmen: Φq = x φq (x), wobei Φq das Potential des Waldes nach q Operationen bezeichnet. Der Wald ist vor der ersten Operation leer und wir setzen entsprechend Φ0 = 0. Das Potential Φq wird niemals negativ sein. Der Wert von φq (x) hängt davon ab, ob x nach der q-ten Operation eine Wurzel eines Baumes des Waldes ist. Wenn dies der Fall ist, oder wenn x.rang = 0 gilt, dann gilt φq (x) = α(n) · x.rang. Nehmen Sie nun an, x wäre nach der q-ten Operation keine Wurzel und es gelte x.rang ≥ 1. Wir müssen zwei Hilfsfunktionen auf x definieren, bevor wir φq (x) festlegen können. Zuerst definieren wir ebene(x) = max {k : x.vater .rang ≥ Ak (x.rang)} . Das heißt, ebene(x) ist die höchste Ebene k, für die Ak , angewendet auf den Rang von x, nicht größer als der Rang des Vaters von x ist. Wir behaupten, dass 0 ≤ ebene(x) < α(n)
(21.1)
gilt, was wir wie folgt sehen. Es gilt x.vater .rang ≥ x.rang + 1 (wegen Lemma 21.4) = A0 (x.rang ) (wegen der Definition von A0 (j)) , woraus ebene(x) ≥ 0 folgt, sowie Aα(n) (x.rang) ≥ Aα(n) (1) ≥ n > x.vater .rang
(da Ak (j) streng steigend ist) (wegen der Definition von α(n)) (wegen Lemma 21.6) ,
21.4 ∗ Analyse der Vereinigung nach dem Rang mit Pfadverkürzung
585
woraus ebene(x) < α(n) folgt. Beachten Sie, dass x.vater .rang monoton steigend mit der Zeit ist und somit auch ebene(x). Die zweite Hilfsfunktion gilt für x.rang ≥ 1 und ist gegeben durch (i) iter(x) = max i : x.vater .rang ≥ Aebene(x) (x.rang) . Das heißt, iter(x) gibt an, wie oft wir Aebene(x) maximal auf den initialen Rang von x iterativ anwenden können, bevor wir einen größeren Wert als den Rang des Vaters von x erhalten. Wir behaupten, dass im Falle von x.rang ≥ 1 1 ≤ iter(x) ≤ x.rang
(21.2)
gilt, was wir wie folgt sehen. Es gilt x.vater .rang ≥ Aebene(x) (x.rang ) (wegen der Definition von ebene(x)) (1)
= Aebene(x) (x.rang ) (wegen der Definition der funktionalen Iteration) , woraus iter(x) ≥ 1 folgt, sowie (x. rang+1)
Aebene(x)
(x.rang ) = Aebene(x)+1 (x.rang ) (wegen der Definition von Ak (j)) > x.vater .rang (wegen der Definition von ebene(x)) ,
woraus iter(x) ≤ x.rang folgt. Beachten Sie, dass ebene(x) größer werden muss, damit iter(x) kleiner werden kann, da x.vater .rang monoton steigend in der Zeit ist. Solange ebene(x) unverändert bleibt, muss sich iter(x) entweder erhöhen oder unverändert bleiben. Mit diesen Hilfsfunktionen sind wir nun in der Lage, das Potential eines Knotens x nach q Operationen zu definieren: ⎧ α(n) · x.rang ⎪ ⎪ ⎨ wenn x eine Wurzel ist oder x.rang = 0 gilt , φq (x) = ⎪ ⎪ ⎩(α(n) − ebene(x))·x.rang − iter(x) wenn x keine Wurzel ist und x.rang ≥ 1 . Als nächstes werden wir einige nützliche Eigenschaften von Knotenpotentialen beweisen.
586
21 Datenstrukturen disjunkter Mengen
Lemma 21.8 Für jeden Knoten x und für jede Anzahl q von Operationen gilt 0 ≤ φq (x) ≤ α(n) · x.rang . Beweis: Wenn x eine Wurzel ist oder x.rang = 0 gilt, dann ist per Definition φq (x) = α(n)·x.rang . Nehmen Sie nun an, x wäre keine Wurzel und es gelte x.rang ≥ 1. Wir erhalten dann eine untere Schranke für φq (x), indem wir ebene(x) und iter(x) maximieren. Wegen der Schranke (21.1) gilt ebene(x) ≤ α(n) − 1 und wegen der Schranke (21.2) gilt iter(x) ≤ x.rang . Folglich ist φq (x) = (α(n) − ebene(x)) · x.rang − iter(x) ≥ (α(n) − (α(n) − 1)) · x.rang − x.rang = x.rang − x.rang =0. Analog dazu erhalten wir für φq (x) eine obere Schranke, indem wir ebene(x) und iter(x) minimieren. Wegen der Schranke (21.1) gilt ebene(x) ≥ 0, und wegen der Schranke (21.2) gilt iter(x) ≥ 1. Folglich ist φq (x) ≤ (α(n) − 0) · x.rang − 1 = α(n) · x.rang − 1 < α(n) · x.rang .
Korollar 21.9 Wenn der Knoten x keine Wurzel ist und x.rang > 0 gilt, dann gilt φq (x) < α(n) · x.rang .
Potentialänderungen und amortisierte Operationenkosten Wir sind nun in der Lage zu untersuchen, wie Operationen auf disjunkten Mengen die Knotenpotentiale beeinflussen. Mit dem Verständnis der Potentialänderungen durch jede Operation können wir die amortisierten Kosten jeder Operation bestimmen.
21.4 ∗ Analyse der Vereinigung nach dem Rang mit Pfadverkürzung
587
Lemma 21.10 Sei x ein Knoten, der keine Wurzel ist, und nehmen Sie an, die q-te Operation wäre entweder Link oder Find-Set. Dann gilt nach der q-ten Operation φq (x) ≤ φq−1 (x). Darüber hinaus gilt φq (x) ≤ φq−1 (x) − 1, wenn x.rang ≥ 1 ist und sich entweder ebene(x) oder iter(x) aufgrund der q-ten Operation ändert. Das heißt, das Potential von x kann sich nicht erhöhen und das Potential von x reduziert sich mindestens um 1, wenn der Knoten einen positiven Rang hat und sich entweder ebene(x) oder iter(x) ändert.
Beweis: Da x keine Wurzel ist, ändert die q-te Operation x.rang nicht, und, da sich n nach den initialen n Make-Set-Operationen nicht ändert, bleibt auch α(n) unverändert. Somit bleiben diese Komponenten in der Formel für das Potential von x nach der q-ten Operation gleich. Wenn x.rang = 0 gilt, dann ist φq (x) = φq−1 (x) = 0. Nehmen Sie nun an, es gelte x.rang ≥ 1. Rufen Sie sich in Erinnerung, dass ebene(x) bezüglich der Zeit monoton steigend ist. Wenn die q-te Operation ebene(x) unverändert lässt, dann erhöht sich iter(x) oder bleibt unverändert. Wenn sowohl ebene(x) als auch iter(x) unverändert bleiben, dann gilt φq (x) = φq−1 (x). Wenn ebene(x) unverändert bleibt und sich iter(x) erhöht, dann erhöht es sich mindestens um 1, sodass φq (x) ≤ φq−1 (x) − 1 gilt. Wenn die q-te Operation schließlich den Wert von ebene(x) erhöht, dann erhöht er sich mindestens um 1, sodass der Wert des Terms (α(n) − ebene(x)) · x.rang mindestens um x.rang fällt. Weil sich ebene(x) erhöht, kann der Wert von iter(x) fallen. Aufgrund der Schranke (21.2) beträgt der Abfall aber höchstens x.rang − 1. Folglich ist der Potentialanstieg aufgrund der Veränderung von iter(x) geringer als der Potentialabfall aufgrund der Veränderung von ebene(x). Daraus folgt φq (x) ≤ φq−1 (x) − 1. Unsere letzten drei Lemmata zeigen, dass die amortisierten Kosten jeder Make-Set-, Link- und Find-Set-Operation O(α(n)) sind. Rufen Sie sich aus Gleichung (17.2) in Erinnerung, dass sich die amortisierten Kosten jeder Operation aus den tatsächlichen Kosten zuzüglich des durch die Operation bedingten Potentialanstiegs ergeben.
Lemma 21.11 Die amortisierten Kosten jeder Make-Set-Operation sind O(1).
Beweis: Nehmen Sie an, die q-te Operation wäre Make-Set(x). Diese Operation erzeugt einen Knoten x mit dem Rang 0, sodass φq (x) = 0 gilt. Es verändert sich kein anderer Rang und kein anderes Potential, und folglich gilt Φq = Φq−1 . Beachten wir, dass die tatsächlichen Kosten der Make-Set-Operation O(1) sind, so ist der Beweis abgeschlossen.
588
21 Datenstrukturen disjunkter Mengen
Lemma 21.12 Die amortisierten Kosten jeder Link-Operation sind O(α(n)). Beweis: Nehmen Sie an, die q-te Operation wäre Link(x, y). Die tatsächlichen Kosten der Link-Operation sind O(1). Ohne Beschränkung der Allgemeinheit setzen wir voraus, dass Link y zum Vater von x macht. Um die durch Link bedingte Potentialveränderung zu bestimmen, stellen wir fest, dass die einzigen Knoten, deren Potentiale sich verändern können, x, y und die Kinder von y unmittelbar vor der Operation sind. Wir werden zeigen, dass der einzige Knoten, dessen Potential sich aufgrund von Link erhöhen kann, y ist, und dass dessen Anstieg höchstens α(n) ist: • Wegen Lemma 21.10 kann sich das Potential keines Knotens, der unmittelbar vor der Link-Operation ein Kind von y war, aufgrund von Link erhöhen. • Aus der Definition von φq (x) sehen wir, dass φq−1 (x) = α(n) · x.rang gilt, weil x unmittelbar vor der q-ten Operation eine Wurzel war. Im Falle x.rang = 0 gilt φq (x) = φq−1 (x) = 0. Anderenfalls ist φq (x) < α(n) · x.rank = φq−1 (x) ,
(wegen Korollar 21.9)
und so verringert sich das Potential von x. • Weil y vor der Link-Operation eine Wurzel ist, gilt φq−1 (y) = α(n) · y.rang. Die Link-Operation belässt y als Wurzel und lässt den Rang von y entweder unberührt oder erhöht ihn um 1. Deshalb gilt entweder φq (y) = φq−1 (y) oder φq (y) = φq−1 (y) + α(n). Der durch eine Link-Operation bedingte Potentialanstieg ist deshalb höchstens α(n). Die amortisierten Kosten der Link-Operation sind O(1) + α(n) = O(α(n)).
Lemma 21.13 Die amortisierten Kosten jeder Find-Set-Operation sind O(α(n)). Beweis: Nehmen Sie an, die q-te Operation wäre Find-Set und der Bestimmungspfad würde s Knoten enthalten. Die tatsächlichen Kosten der Find-Set-Operation sind O(s). Wir werden zeigen, dass sich das Potential keines Knotens aufgrund von Find-Set erhöht und dass sich das Potential von mindestens max(0, s − (α(n) + 2)) Knoten um jeweils mindestens 1 verringert. Um uns davon zu überzeugen, dass sich das Potential keines Knotens erhöht, wenden wir für alle Knoten außer der Wurzel Lemma 21.10 an. Wenn x die Wurzel ist, dann ist deren Potential α(n) · x.rang, das sich nicht ändert.
21.4 ∗ Analyse der Vereinigung nach dem Rang mit Pfadverkürzung
589
Nun zeigen wir, dass sich das Potential von mindestens max(0, s − (α(n) + 2)) Knoten um jeweils wenigstens 1 verringert. Sei x ein Knoten auf dem Bestimmungspfad, für den x.rang > 0 gilt und auf den irgendwo im Bestimmungspfad ein anderer Knoten y folgt, der keine Wurzel ist, und für den unmittelbar vor der Find-Set-Operation rang(y) = rang(x) gilt. (Der Knoten y muss im Bestimmungspfad nicht unmittelbar auf x folgen.) Alle Knoten auf dem Bestimmungspfad, abgesehen von höchstens α(n) + 2 Knoten, erfüllen diese Bedingung hinsichtlich x. Diejenigen, die diese Bedingung nicht erfüllen, sind der erste Knoten auf dem Bestimmungspfad (wenn er den Rang 0 besitzt), der letzte Knoten auf dem Bestimmungspfad (d. h. die Wurzel) und der letzte Knoten w auf dem Bestimmungspfad, für den rang(w) = k für jedes k = 0, 1, 2, . . . , α(n) − 1 gilt. Lassen Sie uns einen solchen Knoten x festhalten. Wir werden zeigen, dass sich das Potential von x mindestens um 1 verringert. Sei k = ebene(x) = ebene(y). Unmittelbar vor der durch Find-Set verursachten Pfadverkürzung gilt (iter(x))
x.vater .rang ≥ Ak
(x.rang)
y.vater .rang ≥ Ak (y.rang) y.rang ≥ x.vater .rang
(wegen der Definition von iter(x)) , (wegen der Definition von ebene(y)) , (wegen Korollar 21.5 und da y x auf dem Bestimmungspfad folgt) .
Fassen wir diese Ungleichungen zusammen und nehmen wir an, dass i vor der Pfadverkürzung den Wert iter(x) hätte, so gilt y.vater .rang ≥ Ak (y.rang) ≥ Ak (x.vater .rang) ≥ =
(da Ak (j) streng steigend ist)
(iter(x)) Ak (Ak (x.rang )) (i+1) Ak (x.rang ) .
Da die Pfadverkürzung bewirkt, dass x und y denselben Vater haben, wissen wir, dass nach der Pfadverkürzung x.vater .rang = y.vater .rang gilt und dass die Pfadverkürzung y.vater .rang nicht verringert. Da sich x.rang nicht verändert, gilt nach der Pfadver(i+1) kürzung x.vater .rang ≥ Ak (x.rang ). Folglich wird die Pfadverkürzung entweder eine Erhöhung von iter(x) (auf mindestens i + 1) oder eine Erhöhung von ebene(x) zur Folge haben. Eine Erhöhung von ebene(x) tritt auf, wenn sich iter(x) auf mindestens x.rang + 1 erhöht. In beiden Fällen gilt wegen Lemma 21.10 φq (x) ≤ φq−1 (x) − 1. Folglich verringert sich das Potential von x um mindestens 1. Die amortisierten Kosten der Find-Set-Operation ergeben sich aus den tatsächlichen Kosten zuzüglich der Potentialänderung. Die tatsächlichen Kosten sind O(s), und wir haben gezeigt, dass sich das Gesamtpotential um mindestens max(0, s − (α(n) + 2)) verringert. Die amortisierten Kosten betragen deshalb höchstens O(α(n)), da wir die Einheiten des Potentials so skalieren können, dass die in O(s) verborgenen Konstanten dominiert werden. Fassen wir die vorhergehenden Lemmata zusammen, führt dies zu folgendem Theorem.
590
21 Datenstrukturen disjunkter Mengen
Theorem 21.14 Eine Folge von m Make-Set-, Union- und Find-Set-Operationen, von denen n Operationen Make-Set-Operationen sind, kann auf einem Wald disjunkter Mengen bei Verwendung der beiden Heuristiken Pfadverkürzung und Vereinigung nach dem Rang in Zeit O(m α(n)) im schlechtesten Fall ausgeführt werden. Beweis: Der Beweis folgt unmittelbar aus den Lemmata 21.7, 21.11, 21.12 und 21.13.
Übungen 21.4-1 Beweisen Sie Lemma 21.4. 21.4-2 Beweisen Sie, dass jeder Knoten höchstens den Rang lg n besitzt. 21.4-3 In Anbetracht von Übung 21.4-2, wie viele Bits sind notwendig, um x.rang für jeden Knoten x zu speichern? 21.4-4 Geben Sie unter Verwendung von Übung 21.4-2 einen einfachen Beweis dafür an, dass die Operationen auf einem Wald disjunkter Mengen bei Verwendung der Heuristik der Vereinigung nach dem Rang, aber ohne Verwendung der Heuristik der Pfadverzögerung in Zeit O(m lg n) laufen. 21.4-5 Professor Dante kommt zu dem Schluss, dass die Ebenen der Knoten auf einem einfachen Pfad zur Wurzel monoton wachsen müssen, weil die Ränge der Knoten auf dem Pfad streng steigend sind. Mit anderen Worten, wenn x.rang > 0 gilt und x.vater keine Wurzel ist, dann ist ebene(x) ≤ ebene(x.vater ). Hat der Professor recht? 21.4-6∗ Betrachten Sie die Funktion α (n) = min {k : Ak (1) ≥ lg(n + 1)}. Zeigen Sie, dass α (n) ≤ 3 für alle praktischen Werte von n gilt. Zeigen Sie außerdem unter Verwendung von Übung 21.4-2, wie das Argument der Potentialfunktion zu modifizieren ist, um zu beweisen, dass wir eine Folge von m Make-Set-, Union- und Find-Set-Operationen, von denen n Operationen Make-SetOperationen sind, auf einem Wald disjunkter Mengen bei Verwendung der beiden Heuristiken Pfadverzögerung und Vereinigung nach dem Rang in Zeit O(m α (n)) im schlechtesten Fall ausführen können.
Problemstellungen 21-1 Offline-Minimum-Problem Beim Offline-Minimum-Problem ist eine dynamische Menge T von Elementen aus dem Bereich {1, 2, . . . , n} unter den Operationen Insert und Extract-Min zu verwalten. Gegeben ist eine Folge S von n Insert- und m Extract-MinAufrufen, wobei jeder Schlüssel in {1, 2, . . . , n} genau einmal eingefügt wird. Wir
Problemstellungen zu Kapitel 21
591
wollen bestimmen, welcher Schlüssel von jedem Extract-Min-Aufruf zurückgegeben wird. Speziell wollen wir ein Feld extrahiert [1 . . m] konstruieren, wobei extrahiert [i] für i = 1, 2, . . . , m der vom i-ten Extract-Min-Aufruf zurückgegebene Schlüssel ist. Das Problem ist in dem Sinne „offline“, dass wir das Ausführen der gesamten Folge S zulassen, bevor wir irgendeinen der zurückgegebenen Schlüssel bestimmen. a. Bei der nun folgenden Instanz des Offline-Minimum-Problems wird jede Operation Insert(i) durch eine Zahl i und jede Extract-Min-Operation durch den Buchstaben E repräsentiert: 4, 8, E, 3, E, 9, 2, 6, E, E, E, 1, 7, E, 5 . Setzen Sie die korrekten Werte in das Feld extrahiert ein. Um einen Algorithmus für dieses Problem zu entwickeln, teilen wir die Folge S in homogene Teilfolgen auf. Das heißt, wir stellen S durch I1 , E, I2 , E, I3 , . . . , Im , E, Im+1 dar, wobei E einen einzelnen Extract-Min-Aufruf und jedes Ij eine (möglicherweise leere) Folge von Insert-Aufrufen darstellt. Für jede Teilfolge Ij legen wir zuerst die von diesen Operationen eingefügten Schlüssel in einer Menge Kj ab, die leer ist, wenn Ij leer ist. Dann führen wir folgende Prozedur aus. Off-Line-Minimum(m, n) 1 for i = 1 to n 2 bestimme j so, dass i ∈ Kj gilt 3 if j = m + 1 4 extrahiert [j] = i 5 sei l der kleinste Wert, der größer als j ist, für den die Menge Kl existiert 6 Kl = Kj ∪ Kl , wobei Kj zerstört wird 7 return extrahiert b. Zeigen Sie, dass das von Off-Line-Minimum zurückgegebene Feld extrahiert korrekt ist. c. Beschreiben Sie, wie Off-Line-Minimum mithilfe einer Datenstruktur disjunkter Mengen effizient implementiert werden kann. Geben Sie eine scharfe Schranke für die Laufzeit Ihrer Implementierung im schlechtesten Fall an. 21-2 Tiefenbestimmung Bei dem Problem der Tiefenbestimmung verwalten wir einen Wald F = {Ti } von gewurzelten Bäumen unter Verwendung von drei Operationen: Make-Tree(v) erzeugt einen Baum, dessen einziger Knoten v ist. Find-Depth(v) gibt die Tiefe eines Knotens v in seinem Baum zurück.
592
21 Datenstrukturen disjunkter Mengen Graft(r, v) macht den Knoten r, von dem wir voraussetzen, dass er eine Wurzel eines Baumes des Waldes ist, zu einem Kind des Knotens v, von dem wir voraussetzen, dass er sich in einem anderen Baum als r befindet und selbst eine Wurzel sein darf, aber nicht muss. a. Nehmen Sie an, wir würden eine Darstellung für Bäume verwenden, die einem Wald disjunkter Mengen ähnlich ist: v.vater ist der Vater des Knotens v, ausgenommen für Wurzeln v, bei denen v.vater = v gilt. Nehmen Sie weiter an, wir würden Graft(r, v) implementieren, indem wir r.vater = v setzen, und Find-Depth(v), indem wir dem Bestimmungspfad bis zur Wurzel folgen und dann die Anzahl aller besuchten Knoten, ohne v, zurückgeben. Zeigen Sie, dass dann die Laufzeit im schlechtesten Fall einer Folge von m Make-Tree-, Find-Depth- und Graft-Operationen in Θ(m2 ) ist. Unter Verwendung der Heuristiken der Vereinigung nach dem Rang und der Pfadverkürzung können wir die Zeit im schlechtesten Fall reduzieren. Wir benutzen den Wald disjunkter Bäume S = {Si }, wobei jede Menge Si (die selbst ein Baum ist) einem Baum Ti im Wald F entspricht. Die Baumstruktur innerhalb der Menge Si entspricht jedoch nicht notwendigerweise der von Ti . Tatsächlich speichert die Implementierung von Si keine exakten Vater-Kind-Beziehungen, erlaubt es uns aber trotzdem, die Tiefe jedes Knotens in Ti zu bestimmen. Die Hauptidee besteht darin, in jedem Knoten einen „Pseudoabstand“ v.d zu verwalten, der so definiert ist, dass die Summe der Pseudoabstände entlang des einfachen Pfades vom Knoten v zur Wurzel seiner Menge Si gleich dem Rang von v in Ti ist. Das heißt, wenn der einfache Pfad von v zu seiner Wurzel in Si gleich v0 , v1 , . . . , vk ist, wobei v0 = v gilt und vk die Wurzel von Si ist, dann entspricht k die Tiefe von v in Ti der Summe j=0 vj .d . b. Geben Sie eine Implementierung von Make-Tree an. c. Zeigen Sie, wie Find-Set zur Implementierung von Find-Depth zu modifizieren ist. Ihre Implementierung sollte die Heuristik der Pfadverkürzung benutzen. Die Laufzeit sollte linear in der Länge des Bestimmungspfades sein. Stellen Sie sicher, dass Ihre Implementierung die Pseudoabstände korrekt aktualisiert. d. Zeigen Sie, wie die Prozedur Graft(r, v) zu implementieren ist, die die Mengen, die r bzw. v enthalten, vereint. Modifizieren Sie dazu die Prozeduren Union und Link. Stellen Sie sicher, dass Ihre Implementierung die Pseudoabstände korrekt aktualisiert. Beachten Sie, dass die Wurzel einer Menge Si nicht notwendigerweise die Wurzel des zugehörigen Baumes Ti ist. e. Geben Sie eine scharfe Schranke für die Laufzeit im schlechtesten Fall für eine Folge von m Make-Tree-, Find-Depth- und Graft-Operationen an, von denen n Operationen Make-Tree-Operationen sind.
21-3 Tarjans Offline-Algorithmus zum Finden der letzten gemeinsamen Vorfahren Der letzte gemeinsame Vorfahre zweier Knoten u und v in einem gewurzelten Baum T ist ein Knoten w, der sowohl ein Vorfahre von u als auch von v
Kapitelbemerkungen zu Kapitel 21
593
ist und der von diesen den größten Rang in T besitzt. Beim Offline-Problem zur Bestimmung der letzten gemeinsamen Vorfahren sind ein gewurzelter Baum T und eine beliebige Menge P = {{u, v}} von ungeordneten Knotenpaaren in T gegeben und wir wollen den letzten gemeinsamen Vorfahren jedes Paares in P bestimmen. Um das Offline-Problem zur Bestimmung der letzten gemeinsamen Vorfahren zu lösen, führt die folgende Prozedur eine Traversierung von T mit dem initialen Aufruf LCA(T.wurzel ) aus. Wir setzen voraus, dass jeder Knoten vor der Traversierung weiß gefärbt ist. LCA(u) 1 Make-Set(u) 2 Find-Set(u).vorfahre = u 3 for jedes Kind v von u in T 4 LCA(v) 5 Union(u, v) 6 Find-Set(u).vorfahre = u 7 u.farbe = schwarz 8 for jeden Knoten v mit {u, v} ∈ P 9 if v.farbe = = schwarz 10 print “Der letzte gemeinsame Vorfahre von” u “und” v “ist” Find-Set(v).vorfahre a. Zeigen Sie, dass Zeile 10 für jedes Paar {u, v} ∈ P genau einmal ausgeführt wird. b. Zeigen Sie, dass die Anzahl der Mengen in der Datenstruktur disjunkter Mengen zum Zeitpunkt des Aufrufs LCA(u) gleich der Tiefe von u in T ist. c. Beweisen Sie, dass LCA den letzten gemeinsamen Vorfahren von u und v für jedes Paar {u, v} ∈ P korrekt ausgibt. d. Analysieren Sie die Laufzeit von LCA unter der Voraussetzung, dass wir die Implementierung der Datenstruktur disjunkter Mengen aus Abschnitt 21.3 verwenden.
Kapitelbemerkungen Viele der wichtigsten Ergebnisse für Datenstrukturen disjunkter Mengen gehen zumindest teilweise auf R. E. Tarjan zurück. Unter Verwendung der Aggregat-Analyse gab Tarjan [328, 330] die erste scharfe obere Schranke als Funktion der sehr langsam wachsenden Inversen α (m, n) der Ackermann-Funktion an. (Die in Abschnitt 21.4 angegebene Funktion ist der Ackermann-Funktion ähnlich, und die Funktion α(n) ähnelt deren Inversen. Sowohl α(n) als auch α (m, n) sind für alle denkbaren Werte von m und n höchstens gleich 4.) Eine obere Schranke von O(m lg ∗ n) wurde bereits früher von Hopcroft und Ullman [5, 179] bewiesen. Die Betrachtungen in Abschnitt 21.4 wurden aus einer späteren Analyse von Tarjan [332] übernommen, die wiederum auf einer Analyse
594
21 Datenstrukturen disjunkter Mengen
von Kozen [220] basiert. Harfst und Reingold [161] geben eine potentialbasierte Version von Tarjans früherer Schranke an. Tarjan und van Leeuwen [333] diskutieren Varianten der Heuristik der Pfadverkürzung einschließlich „Einschrittverfahren“, die manchmal bessere konstante Faktoren in ihrer Performanz bieten als Zweischrittverfahren. Wie Tarjans frühere Analysen der grundlegenden Heuristik der Pfadverkürzung handelt es sich auch bei der Analyse von Tarjan und van Leeuwen um eine Aggregat-Analyse. Harfst und Reingold [161] zeigten später, wie man durch eine kleine Veränderung in der Potentialfunktion die Pfadverkürzungsanalyse an diese Einschrittverfahren anpassen kann. Gabow und Tarjan [121] zeigen, dass bei bestimmten Anwendungen erreicht werden kann, dass die Operationen auf disjunkten Mengen in Zeit O(m) laufen. Tarjan [329] zeigte, dass die Laufzeit für Operationen auf einer beliebigen Datenstruktur nach unten durch Ω(m α (m, n)) beschränkt ist, falls bestimmte technische Bedingungen zu erfüllen sind. Diese untere Schranke wurde später von Fredman und Saks [113] verallgemeinert. Sie zeigten, dass im schlechtesten Fall auf Ω(m α (m, n)) viele (lg n)Bit-Wörter im Speicher zugegriffen werden muss.
Teil VI
Graphenalgorithmen
Einführung Probleme auf Graphen durchdringen die Informatik und Algorithmen, die es erlauben, auf Graphen zu arbeiten, sind zentral für dieses Gebiet. Hunderte von interessanten Problemen sind als Graphprobleme modelliert. In diesem Teil des Buches schauen wir uns einige der wichtigsten dieser Probleme an. Kapitel 22 zeigt, wie wir Graphen auf einem Rechner darstellen können, und behandelt Algorithmen, die auf dem Durchsuchen eines Graphen basieren, indem sie entweder eine Breitensuche oder eine Tiefensuche durchführen. Das Kapitel stellt zwei Anwendungen von Tiefensuche vor: das topologische Sortieren eines gerichteten azyklischen Graphen und das Zerlegen eines gerichteten Graphen in seine starken Zusammenhangskomponenten. Kapitel 23 beschreibt, wie wir für einen Graphen einen aufspannenden Baum mit minimalem Gewicht berechnen können, d. h. einen Teilbaum des Graphen, der alle Knoten des Graphen so miteinander verbindet, dass er geringste Kosten hat, wobei jeder Kante ein Gewicht zugeordnet ist. Die Algorithmen zur Berechnung von minimalen aufspannenden Bäumen dienen als gute Beispiele für Greedy-Algorithmen (siehe Kapitel 16). Die Kapiteln 24 und 25 beschäftigen sich mit dem Problem, kürzeste Pfade zwischen Knoten zu bestimmen, wenn jeder Kante eine Länge oder ein „Gewicht“ zugeordnet ist. Kapitel 24 zeigt, wie wir kürzeste Pfade von einem gegebenen Startknoten zu allen anderen Knoten finden können, und Kapitel 25 untersucht Methoden, um kürzeste Pfade zwischen allen Paaren von Knoten zu bestimmen. Schließlich zeigt Kapitel 26, wie wir den maximalen Materialfluss in einem Netzwerk bestimmen können, das durch einen gerichteten Graphen dargestellt wird, mit einem spezifizierten Knoten, der Quelle, aus dem das Material fließt, einem spezifizierten Knoten, der Senke, zu dem das Material fließt, und spezifizierten Kapazitäten für jede Kante, die jeweils angeben, wie viel Material über die Kante fließen kann. Dieses allgemeine Problem tritt in verschiedenen Formen auf. Ein guter Algorithmus für die Berechnung des maximalen Flusses kann helfen, eine Vielzahl verwandter Probleme effizient zu lösen. Wenn wir die Laufzeit eines Graphalgorithmus angewendet auf einen gegebenen Graphen G = (V, E) beschreiben, dann geben wir die Größe der Eingabe über die Anzahl |V | der Knoten und die Anzahl |E| der Kanten des Graphen an. Das heißt, wir beschreiben die Größe der Eingabe mit zwei Parametern und nicht nur mit einem. Wir übernehmen eine verbreitete Konvention für die Schreibweise dieser Parameter. Innerhalb einer asymptotischen Notation (wie der O-Notation oder der Θ-Notation) – und zwar nur dort – bezeichnet das Symbol V die Kardinalität |V | und das Symbol E die Kardinalität |E|. Wenn wir zum Beispiel schreiben, „der Algorithmus läuft in Zeit
598
21 Einführung
O(V E)“, bedeutet dies, dass der Algorithmus in Zeit O(|V | |E|) läuft. Diese Konvention macht die Formeln für die Laufzeiten leichter lesbar, ohne Verwechslungen zu riskieren. Eine weitere Konvention, die wir übernehmen, tritt im Pseudocode auf. Wir bezeichnen die Knotenmenge des Graphen G mit G.V und seine Kantenmenge mit G.E . Der Pseudocode betrachtet also Knoten- und Kantenmengen als Attribute eines Graphen.
22
Elementare Graphenalgorithmen
Dieses Kapitel stellt Methoden zur Darstellung von Graphen und für das Durchsuchen eines Graphen vor. Durchsuchen eines Graphen bedeutet, in systematischer Weise den Kanten des Graphen zu folgen und die Knoten des Graphen aufzusuchen (oder zu besuchen). Ein Durchsuchungsalgorithmus auf einem Graphen kann viele Eigenschaften der Struktur des Graphen aufdecken. Viele Algorithmen beginnen mit dem Durchsuchen des Eingabegraphen, um diese strukturellen Informationen zu erhalten. Viele andere Graphalgorithmen bauen auf einfachen Durchsuchungsalgorithmen auf. Verfahren zum Durchsuchen eines Graphen sind zentral für Graphenalgorithmen. Abschnitt 22.1 diskutiert die beiden üblicherweise benutzten Darstellungen von Graphen: die Darstellung durch eine Adjazenzliste und die Darstellung durch eine Adjazenzmatrix. Abschnitt 22.2 stellt einen einfachen Durchsuchungsalgorithmus auf einem Graphen vor, die so genannte Breitensuche (engl.: breadth-first search) und zeigt, wie ein Breitensuchbaum erzeugt wird. Abschnitt 22.3 stellt die Tiefensuche (engl.: depthfirst search) vor. Wir beweisen einige Standardergebnisse über die Reihenfolge, in der die Tiefensuche die Knoten aufsucht. Abschnitt 22.4 stellt unsere erste echte Anwendung der Tiefensuche vor: das topologische Sortieren in einem gerichteten azyklischen Graphen. Eine zweite Anwendung der Tiefensuche, die Berechnung der starken Zusammenhangskomponenten eines gerichteten Graphen, ist Thema von Abschnitt 22.5.
22.1
Darstellungen von Graphen
Wir können zwischen zwei gängigen Möglichkeiten wählen, einen Graphen G = (V, E) darzustellen: als eine Menge von Adjazenzlisten oder als Adjazenzmatrix. Beide Möglichkeiten sind sowohl für gerichtete als auch für ungerichtete Graphen anwendbar. Die Adjazenzlisten-Darstellung wird in der Regel vorgezogen, da sie für dünn besetzte Gra2 phen – d. h. für Graphen, für die |E| viel kleiner als |V | ist – eine kompakte Darstellung liefert. Die meisten Graphenalgorithmen, die in diesem Buch vorgestellt werden, setzen voraus, dass der Eingabegraph durch Adjazenzlisten dargestellt ist. Wir präferieren eine Adjazenzmatrix-Darstellung, wenn der Graph dicht ist – d. h. wenn |E| nahe an |V |2 liegt – oder wenn wir in der Lage sein müssen, schnell herauszufinden, ob zwei gegebene Knoten verbunden sind. Beispielsweise setzen zwei der in Kapitel 25 vorgestellten Algorithmen zur Bestimmung kürzester Pfade für alle Knotenpaare voraus, dass ihre Eingabegraphen durch Adjazenzmatrizen dargestellt sind. Die Adjazenzlisten-Darstellung eines Graphen G = (V, E) besteht aus einem Feld Adj aus |V | Listen, wobei es für jeden Knoten aus V eine Liste gibt. Für jeden Knoten
600
1
22 Elementare Graphenalgorithmen
1 2 3
2 3
5
4 5
4
2 1
5 5
3
2 2
4 5
3
4
1
2
(a)
4
1
2
3
4
5
1 2 3
0 1 0
1 0 1
0 1 0
0 1 1
1 1 0
4 5
0 1
1 1
1 0
0 1
1 0
(b)
(c)
Abbildung 22.1: Zwei Darstellungen eines ungerichteten Graphen. (a) Ein ungerichteter Graph G mit 5 Knoten und 7 Kanten. (b) Eine Adjazenzlisten-Darstellung von G. (c) Die Adjazenzmatrix-Darstellung von G.
1
4
2
5 (a)
3
6
1
1 0
4
2 3 4 5
0 0 0 0
2 3 4 5 6 1 0 1 0 0 0 0 0 1 0 0 0 0 1 1 1 0 0 0 0 0 0 1 0 0
6
6
0
0
1
2
4
2 3 4 5
5 6
5
6
2
(b)
0
0
0
1
(c)
Abbildung 22.2: Zwei Darstellungen eines gerichteten Graphen. (a) Ein gerichteter Graph G mit 6 Knoten und 8 Kanten. (b) Eine Adjazenzlisten-Darstellung von G. (c) Die Adjazenzmatrix-Darstellung von G.
u ∈ V enthält die Adjazenzliste Adj [u] alle Knoten v, für die es eine Kante (u, v) ∈ E gibt, d. h. alle Knoten, die in G mit u adjazent sind. (Alternativ kann sie auch Zeiger auf diese Knoten enthalten.) Da Adjazenzlisten die Kanten eines Graphen darstellen, behandeln wir im Pseudocode das Feld Adj wie ein Attribut des Graphen, genau wie wir die Kantenmege E als Attribut des Graphen behandeln. Im Pseudocode werden wir deshalb die Notation G.Adj [u] benutzen. Abbildung 22.1(b) ist eine Adjazenzlisten-Darstellung des ungerichteten Graphen aus Abbildung 22.1(a). Analog ist Abbildung 22.2(b) eine Adjazenzlisten-Darstellung des gerichteten Graphen aus Abbildung 22.2(a). Für einen gerichteten Graphen G ist die Summe der Längen aller Adjazenzlisten gleich |E|, da die Existenz einer Kante (u, v) dadurch dargestellt wird, dass v in Adj [u] vorkommt. Für einen ungerichteten Graphen G ist die Summe der Längen aller Adjazenzlisten gleich 2 |E|, denn wenn (u, v) eine ungerichtete Kante ist, dann erscheint u in der Adjazenzliste von v und umgekehrt. Für gerichtete wie für ungerichtete Graphen hat die Adjazenzlisten-Darstellung die schöne Eigenschaft, dass der erforderliche Speicherumfang in Θ(V + E) ist. Wir können Adjazenzlisten leicht so anpassen, dass sich auch gewichtete Graphen durch sie darstellen lassen, d. h. Graphen, in denen jeder Kante ein Gewicht zugeordnet ist, wobei üblicherweise die Gewichte durch eine Gewichtsfunktion w : E → R
22.1 Darstellungen von Graphen
601
gegeben sind. Sei beispielsweise G = (V, E) ein gewichteter Graph mit der Gewichtsfunktion w. Wir speichern einfach das Gewicht w(u, v) der Kante (u, v) ∈ E zusammen mit dem Knoten v in der Adjazenzliste von u. Die Adjazenzlisten-Darstellung ist sehr robust in der Hinsicht, dass wir sie leicht modifizieren können, damit sie auch viele andere Graphvarianten unterstützt. Ein potentieller Nachteil der Adjazenzlisten-Darstellung besteht darin, dass sie keine schnellere Möglichkeit bereitstellt, um zu überprüfen, ob eine gegebene Kante (u, v) in dem Graphen vorhanden ist, als v in der Adjazenzliste Adj [u] zu suchen. Die Adjazenzmatrix-Darstellung des Graphen behebt diesen Nachteil, aber auf Kosten von asymptotisch mehr Speicherplatz. (Siehe Übung 22.1-8 für Vorschläge, wie man Adjazenzlisten modifizieren müsste, um ein schnelleres Nachschlagen von Kanten zu ermöglichen.) Für die Adjazenzmatrix-Darstellung eines Graphen G = (V, E) setzen wir voraus, dass die Knoten in beliebiger Weise von 1 bis |V | nummeriert sind. Dann besteht die Adjazenzmatrix-Darstellung des Graphen G aus einer |V | × |V |-Matrix A = (aij ) mit den Elementen 1 falls (i, j) ∈ E , aij = 0 sonst . Die Abbildungen 22.1(c) und 22.2(c) zeigen die Adjazenzmatrizen des ungerichteten bzw. des gerichteten Graphen aus Abbildung 22.1(a) bzw. 22.2(a). Die Adjazenzmatrix eines Graphen benötigt unabhängig von dessen Kantenzahl Speicherplatz Θ(V 2 ). Beachten Sie die Symmetrie der Adjazenzmatrix in Abbildung 22.1(c) bezüglich der Hauptdiagonale. Da in einem ungerichteten Graphen (u, v) und (v, u) die gleiche Kante repräsentieren, ist die Adjazenzmatrix A für einen ungerichteten Graphen gleich ihrer eigenen Transponierten, d. h. es gilt A = AT . Bei einigen Anwendungen lohnt es sich, nur die Einträge oberhalb der Hauptdiagonale der Adjazenzmatrix zu speichern und damit den für den Graphen erforderlichen Speicherplatz nahezu zu halbieren. Wie die Adjazenzlisten-Darstellung eines Graphen kann eine Adjazenzmatrix-Darstellung auch einen gewichteten Graphen darstellen. Ist zum Beispiel G = (V, E) ein gewichteter Graph mit der Kantengewichtsfunktion w, so können wir das Gewicht w(u, v) der Kante (u, v) ∈ E einfach als Eintrag in Zeile u und Spalte v der Adjazenzmatrix speichern. Existiert eine Kante nicht, so können wir den Wert nil in der entsprechenden Position der Matrix speichern; allerdings ist es für viele Probleme zweckdienlicher einen Wert wie 0 oder ∞ zu verwenden. Wenngleich die Adjazenzlisten-Darstellung asymptotisch mindestens so Speicherplatzeffizient ist wie die Adjazenzmatrix-Darstellung, sind Adjazenzmatrizen einfacher, und so können wir durchaus Adjazenzmatrizen bevorzugen, wenn die Graphen einigermaßen klein sind. Außerdem haben Adjazenzmatrizen einen Vorteil bei ungewichteten Graphen: sie benötigen nur ein Bit pro Eintrag.
Darstellung der Attribute Die meisten Algorithmen, die auf Graphen arbeiten, benötigen Attribute für Knoten und/oder Kanten. Wir geben diese Attribute in unserer üblichen Notation an, wie zum
602
22 Elementare Graphenalgorithmen
Beispiel v.d für ein Attribut d eines Knotens v. Wenn wir Kanten als Paare von Knoten angeben, verwenden wir die gleiche Art von Notation. Wenn Kanten beispielsweise ein Attribut f besitzen, dann geben wir dieses Attribut für die Kante (u, v) durch (u, v).f an. Um Algorithmen vorzustellen und zu verstehen, reicht diese Notation für Attribute vollkommen aus. Es ist aber eine andere Geschichte, wenn wir Knoten- und Kantenattribute in wirklichen Programmen implementieren wollen. Es gibt nicht die beste Methode, Knotenund Kantenattribute zu speichern und auf sie zuzugreifen. Für ein gegebenes Szenario wird Ihre Entscheidung voraussichtlich von der Programmiersprache abhängen, die Sie benutzen, dem Algorithmus, den Sie implementieren, und wie der Rest Ihres Programms den Graphen nutzt. Wenn Sie einen Graphen durch Adjazenzlisten darstellen, ist eine mögliche Implementierung, die Knotenattribute in zusätzlichen Feldern zu speichern, wie beispielsweise einem Feld d[1 . . |V |], das parallel zu dem Feld Adj gehalten wird. Wenn die zu u adjazenten Knoten in Adj [u] gespeichert sind, dann würde das, was wir das Attribut u.d nennen, in Wirklichkeit in dem Feldeintrag d[u] gespeichert sein. Viele andere Möglichkeiten zur Implementierung von Attributen sind möglich. In einer objektorientierten Programmiersprache zum Beispiel könnten Knotenattribute als Instanzvariablen innerhalb einer Subklasse einer Klasse Knoten dargestellt sein.
Übungen 22.1-1 Gegeben sei eine Adjazenzlisten-Darstellung eines gerichteten Graphen. Wie viel Zeit ist nötig, um den Ausgangsgrad eines jeden Knoten zu berechnen? Wie viel Zeit ist nötig, um den Eingangsgrad eines jeden Knoten zu berechnen? 22.1-2 Geben Sie eine Adjazenzlisten-Darstellung für einen vollständigen binären Baum mit 7 Knoten an. Geben Sie eine äquivalente AdjazenzmatrixDarstellung an. Setzen Sie voraus, dass die Knoten wie in einem binären Heap von 1 bis 7 nummeriert sind. 22.1-3 Der zu einem gerichteten Graphen G = (V, E) transponierte Graph ist der Graph GT = (V, E T ) mit E T = {(v, u) ∈ V × V : (u, v) ∈ E}. Der Graph GT ist also gleich G bis auf, dass die Richtungen der Kanten alle gedreht sind. Geben Sie effiziente Algorithmen an, die GT aus der Adjazenzlisten-Darstellung bzw. der Adjazenzmatrix-Darstellung von G berechnen. Analysieren Sie die Laufzeiten Ihrer Algorithmen. 22.1-4 Gegeben sei eine Adjazenzlisten-Darstellung eines Multigraphen G = (V, E). Geben Sie einen Algorithmus mit Laufzeit O(V +E) an, der die AdjazenzlistenDarstellung des „äquivalenten“ ungerichteten Graphen G = (V, E ) berechnet, dessen Kantenmenge E aus der Kantenmenge E entsteht, indem alle multiplen Kanten zwischen zwei Knoten durch eine einzige Kante ersetzt und Schlingen entfernt werden. 22.1-5 Das Quadrat eines gerichteten Graphen G = (V, E) ist der Graph G2 = (V, E 2 ), in dem (u, v) ∈ E 2 genau dann gilt, wenn G einen Pfad zwischen u und v enthält, der aus höchstens zwei Kanten besteht. Geben Sie effiziente Algorithmen an, die G2 aus der Adjazenzlisten-Darstellung bzw. der
22.2 Breitensuche
603
Adjazenzmatrix-Darstellung von G berechnen. Analysieren Sie die Laufzeiten Ihrer Algorithmen. 22.1-6 Die meisten Graphalgorithmen, die eine Adjazenzmatrix als Eingabe bekommen, benötigen Zeit Ω(V 2 ); es gibt jedoch einige Ausnahmen. Zeigen Sie, dass es möglich ist, in Zeit O(V ) festzustellen, ob ein gerichteter Graph G eine universelle Senke (d. h. einen Knoten mit Eingangsgrad |V | − 1 und Ausgangsgrad 0) enthält, wenn eine Adjazenzmatrix von G gegeben ist. 22.1-7 Die Inzidenzmatrix eines gerichteten Graphen G = (V, E), der keine Schlingen enthält, ist eine |V | × |E|-Matrix B = (bij ) mit ⎧ ⎪ ⎨ −1 falls Kante j aus Knoten i austritt , falls Kante j in Knoten i eintritt , bij = 1 ⎪ ⎩0 sonst . Beschreiben Sie, was die Einträge des Matrizenprodukts BB T darstellen. (B T ist die zu B transponierte Matrix.) 22.1-8 Nehmen Sie an, jeder Feldeintrag Adj [u] wäre eine Hashtabelle und keine verkettete Liste, die die Knoten v enthält, für die (u, v) ∈ E gilt. Wie ist die erwartete Zeit, um zu überprüfen, ob eine Kante im Graphen enthalten ist, wenn alle Kanten mit der gleichen Wahrscheinlichkeit nachgeschlagen werden? Welche Nachteile hat diese Methode? Schlagen Sie eine modifizierte Datenstruktur für jede Kantenliste vor, mit der diese Probleme behoben werden können. Hat Ihre Alternative Nachteile gegenüber der Hashtabelle?
22.2
Breitensuche
Die Breitensuche ist einer der einfachsten Algorithmen für das Durchsuchen eines Graphen und Vorbild für viele wichtige Graphenalgorithmen. Prims Algorithmus zur Bestimmung eines minimalen aufspannenden Baumes (Abschnitt 23.2) und Dijkstras Algorithmus zur Bestimmung kürzester Pfade von einem gegebenen Startknoten aus (Abschnitt 24.3) verwenden Ideen, die der Idee der Breitensuche ähnlich sind. Für einen gegebenen Graphen G = (V, E) und einen ausgezeichneten Startknoten s erforscht die Breitensuche systematisch alle Kanten von G, um alle Knoten zu „entdecken“, die von s aus erreichbar sind. Der Algorithmus berechnet die Distanz (die kleinste Anzahl von Kanten) von s zu jedem erreichbaren Knoten. Er erzeugt außerdem einen „Breitensuchbaum“ mit der Wurzel s, der alle erreichbaren Knoten enthält. Für jeden Knoten v, der von s aus erreichbar ist, entspricht der einfache Pfad von s nach v im Breitensuchbaum einem „kürzesten Pfad“ von s nach v in G, d. h. einem Pfad, der die geringste Anzahl von Kanten enthält. Der Algorithmus arbeitet sowohl für gerichtete als auch für ungerichtete Graphen. Die Breitensuche verdankt ihren Namen der Tatsache, dass sich die Front zwischen entdeckten und unentdeckten Knoten gleichmäßig über die gesamte Breite der Front
604
22 Elementare Graphenalgorithmen
ausdehnt. Das heißt, der Algorithmus besucht zunächst alle Knoten, die die Distanz k von s haben, bevor er irgendeinen Knoten mit Distanz k + 1 besucht. Breitensuche führt Buch über den Status der Knoten des gegebenen Graphen, indem sie jeden Knoten weiß, grau oder schwarz färbt. Alle Knoten sind anfangs weiß und können später grau und schließlich schwarz werden. Ein Knoten wird entdeckt, wenn die Suche das erste Mal auf ihn trifft; zu diesem Zeitpunkt verliert er seine weiße Farbe. Graue und schwarze Knoten sind also bereits entdeckt worden. Bei der Breitensuche wird zwischen beiden Farben unterschieden, um sicherzustellen, dass die Suche tatsächlich als Breitensuche arbeitet1 . Gilt (u, v) ∈ E und ist der Knoten u schwarz, dann ist der Knoten v entweder grau oder schwarz, d. h. alle Knoten, die mit schwarzen Knoten adjazent sind, wurden bereits entdeckt. Graue Knoten können mit weißen Knoten benachbart sein; sie stellen die Front zwischen entdeckten und unentdeckten Knoten dar. Die Breitensuche erzeugt einen Breitensuchbaum, der anfangs nur seine Wurzel, den Startknoten s, enthält. Immer, wenn die Suche beim Durchsuchen der Adjazenzliste eines bereits entdeckten Knotens u einen weißen Knoten v entdeckt, werden der Knoten v und die Kante (u, v) zum Baum hinzugefügt. Wir sagen, dass u der Vorgänger oder Vater von v im Breitensuchbaum ist. Da ein Knoten höchstens einmal entdeckt wird, hat er höchstens einen Vater. Die Beziehungen zwischen Vorfahren und Nachfahren innerhalb des Baums werden wie gewöhnlich relativ zur Wurzel s definiert: Falls u auf einem einfachen Pfad von der Wurzel s zum Knoten v liegt, dann ist u ein Vorfahre von v und v ist ein Nachfahre von u. Die unten angegebene Prozedur BFS zur Breitensuche setzt voraus, dass der Eingabegraph G = (V, E) durch Adjazenzlisten dargestellt ist. Sie verwaltet zu jedem Knoten des Graphen neben den bereits besprochenen Attributen noch weitere Attribute. Wir speichern die Farbe eines jeden Knoten u ∈ V in dem Attribut u.farbe und den Vorgänger von u in dem Attribut u.π. Falls u keinen Vorgänger hat (zum Beispiel weil u die Wurzel ist oder noch nicht entdeckt wurde), so setzen wir u.π gleich nil. Das Attribut u.d speichert die durch den Algorithmus berechnete Distanz vom Startknoten s zum Knoten u. Der Algorithmus verwendet außerdem eine FIFO-Warteschlange Q (siehe Abschnitt 10.1), um die Menge der grauen Knoten zu verwalten.
1 Wir unterscheiden zwischen grauen und schwarzen Knoten, da sie uns helfen zu verstehen, wie Breitensuche arbeitet. Tatsächlich würden wir, wie Übung 22.2-3 dies auch zeigt, zum gleichen Ergebnis kommen, wenn wir nicht zwischen grauen und schwarzen Knoten unterscheiden würden.
22.2 Breitensuche
605
BFS(G, s) 1 for jeden Knoten u ∈ G.V − {s} 2 u.farbe = weiss 3 u.d = ∞ 4 u.π = nil 5 s.farbe = grau 6 s.d = 0 7 s.π = nil 8 Q=∅ 9 Enqueue(Q, s) 10 while Q = ∅ 11 u = Dequeue(Q) 12 for jeden Knoten v ∈ G.Adj [u] 13 if v.farbe = = weiss 14 v.farbe = grau 15 v.d = u.d + 1 16 v.π = u 17 Enqueue(Q, v) 18 u.farbe = schwarz Abbildung 22.3 illustriert, wie BFS auf einem Beispielgraphen arbeitet. Die Prozedur BFS arbeitet folgendermaßen. Mit Ausnahme des Startknotens s wird in den Zeilen 1–4 allen Knoten die Farbe weiss zugeordnet und die Distanz zum Startknoten auf unendlich sowie den Vater auf nil gesetzt. Zeile 5 färbt den Startknoten s grau, da wir ihn als entdeckt ansehen, wenn der Algorithmus startet. In Zeile 6 wird s.d mit 0 initialisiert und in Zeile 7 wird der Vorgänger des Startknotens auf nil gesetzt. In den Zeilen 8–9 wird die Warteschlange Q so initialisiert, dass sie nur den Startknoten s enthält. Die while-Schleife der Zeilen 10–18 iteriert so lange, wie noch graue Knoten übrig sind, d. h. Knoten, die entdeckt sind und deren Adjazenzlisten noch nicht vollständig untersucht wurden. Die while-Schleife erhält die folgende Schleifeninvariante: Beim Test in Zeile 10 besteht die Warteschlange Q aus den grauen Knoten. Wenngleich wir diese Schleifeninvariante nicht verwenden werden, um die Korrektheit zu beweisen, ist es einfach zu sehen, dass sie vor der ersten Iteration gilt und in jeder Iteration erhalten bleibt. Vor der ersten Iteration ist der einzige graue Knoten und der einzige Knoten in Q der Startknoten s. In Zeile 11 wird der am Kopf der Warteschlange Q stehende graue Knoten der Variable u zugewiesen und aus Q entfernt. Die for-Schleife in den Zeilen 12–17 untersucht jeden Knoten v aus der Adjazenzliste von u. Wenn v weiß ist, dann ist der Knoten noch nicht entdeckt worden, und der Algorithmus entdeckt ihn, indem er die Zeilen 14–17 ausführt. Die Prozedur färbt den Knoten v grau, setzt seine Distanz v.d auf u.d +1, speichert u als seinen Vater v.π und hängt ihn an das
606
22 Elementare Graphenalgorithmen
r
s
t
u
r
s
t
u
∞
0
∞
∞
1
0
∞
∞
∞ v
∞ w
∞ x
∞ y
∞ v
1 w
∞ x
∞ y
r
s
t
u
r
s
1
0
2
∞
1
0
t 2
∞
(a)
Q
(c) 1 w
2 x
∞ y
r
s
1
0
t 2
u 3 Q
(e) 1 w
2 x
∞ y
r
s
1
0
t 2
u 3
2 v
1 w
2 x
3 y
r 1
s 0
t 2
u 3
2 v
1 w
2 x
3 y
2 v
(i)
r t x 1 2 2
x
v
u
u
y
3 3
Q
Q
1 w
2 x
∞ y
r
s
1
0
t 2
u 3
2 v
(f) 2 v
w r 1 1
u
(d)
2 2 3
Q
(g)
(b)
0
Q ∞ v
s
1 w
2 x
3 y
r
s
1
0
t 2
u 3
2 v
1 w
2 x
3 y
Q
t x v 2 2 2
Q
v u
2 3 3
Q
(h)
y
y 3
Ze
Abbildung 22.3: Die Arbeitsweise von BFS auf einem ungerichteten Graphen. Die Kanten des Baums sind schattiert eingezeichnet, sobald sie durch BFS erzeugt worden sind. Innerhalb eines jeden Knoten u ist der Wert u. d angegeben. Die Warteschlange Q ist jeweils in ihrem Zustand zu Beginn der Iterationen der while-Schleife der Zeilen 10–18 dargestellt. Unterhalb eines jeden Knotens der Warteschlange steht die Distanz des Knotens zum Startknoten.
22.2 Breitensuche
607
Ende der Schlange Q. Wenn die Prozedur alle Knoten von u’s Adjazenzliste untersucht hat, färbt er u in Zeile 18 schwarz. Die Schleifeninvariante bleibt erhalten, denn jedes Mal, wenn ein Knoten grau gefärbt wird (Zeile 14), wird er auch in die Warteschlange eingefügt (Zeile 17), und jedes Mal, wenn ein Knoten aus der Warteschlange entnommen wird (Zeile 11), wird er auch schwarz gefärbt (Zeile 18). Das Ergebnis der Breitensuche kann davon abhängen, in welcher Reihenfolge die Nachbarn eines gegebenen Knotens in Zeile 12 aufgesucht werden. Der Breitensuchbaum kann variieren, nicht jedoch die durch den Algorithmus berechnete Distanz d (siehe Übung 22.2-5.)
Analyse Bevor wir die verschiedenen Eigenschaften der Breitensuche beweisen, wenden wir uns der etwas einfacheren Aufgabe zu, ihre Laufzeit auf einem Eingabegraphen G = (V, E) zu analysieren. Wir verwenden die Aggregat-Analyse, die wir in Abschnitt 17.1 kennengelernt haben. Nach der Initialisierung färbt die Breitensuche nie mehr einen Knoten weiß, und daher stellt der Test in Zeile 13 sicher, dass jeder Knoten nur einmal in die Warteschlange eingefügt und folglich höchstens einmal aus ihr entnommen wird. Das Einfügen in und das Entnehmen aus der Warteschlange benötigen Zeit O(1). Daher ist die Gesamtzeit, die für Warteschlangenoperationen aufgewendet wird, O(V ). Da die Prozedur die Adjazenzliste eines jeden Knotens nur durchsucht, wenn der Knoten entnommen wird, durchsucht sie jede Adjazenzliste nur einmal. Da die Summe über alle Längen der einzelnen Adjazenzlisten in Θ(E) ist, ist die Gesamtzeit für das Durchsuchen der Adjazenzlisten O(E). Der Aufwand für das Initialisieren ist O(V ), und somit ist die Gesamtlaufzeit der Prozedur BFS O(V + E). Die Laufzeit der Breitensuche ist also linear in der Größe der Adjazenzlisten-Darstellung von G.
Kürzeste Pfade Zu Beginn dieses Abschnitts hatten wir behauptet, dass die Breitensuche die Distanz jedes erreichbaren Knotens eines Graphen G = (V, E) zu einem gegebenen Startknoten s ∈ V bestimmt. Definieren Sie die kleinste Distanz δ(s, v) des Knotens v zum Startknoten s als die minimale Anzahl von Kanten über alle Pfade von s nach v. Wenn es keinen Pfad von s nach v gibt, dann sei δ(s, v) = ∞. Wir nennen einen Pfad der Länge δ(s, v) von s nach v ein kürzester Pfad 2 . Bevor wir zeigen, dass die Breitensuche tatsächlich die kleinsten Distanzen korrekt berechnet, untersuchen wir eine wichtige Eigenschaft kleinster Distanzen. Lemma 22.1 Sei G = (V, E) ein gerichteter oder ungerichteter Graph und s ∈ V ein beliebiger 2 In den Kapiteln 24 und 25 werden wir unsere Untersuchungen über kürzeste Pfade auf den Fall gewichteter Graphen verallgemeinern. Dabei hat jede Kante ein reellwertiges Gewicht, und das Gewicht eines Pfades ist die Summe der Gewichte der einzelnen Kanten, aus denen er besteht. Die im aktuellen Kapitel betrachteten Graphen sind ungewichtet, oder anders formuliert, alle Kanten haben das Gewicht 1.
608
22 Elementare Graphenalgorithmen
Knoten. Dann gilt für jede Kante (u, v) ∈ E δ(s, v) ≤ δ(s, u) + 1 . Beweis: Falls u von s aus erreichbar ist, dann gilt dies auch für v. In diesem Fall kann der kürzeste Pfad von s nach v nicht länger sein als der kürzeste Pfad von s nach u zuzüglich der Kante (u, v), sodass die zu beweisende Ungleichung gilt. Falls u nicht von s aus erreichbar ist, gilt δ(s, u) = ∞, sodass die Ungleichung ebenfalls erfüllt ist. Wir werden zeigen, dass die Prozedur BFS die Distanz v.d = δ(s, v) für jeden Knoten v ∈ V korrekt berechnet. Zunächst zeigen wir, dass δ(s, v) nach oben durch v.d beschränkt ist. Lemma 22.2 Sei G = (V, E) ein gerichteter oder ungerichteter Graph, auf dem die Prozedur BFS ausgehend von einem gegebenen Startknoten s ∈ V angewendet wird. Dann erfüllt bei Terminierung der durch BFS für jeden Knoten v ∈ V berechnete Wert v.d die Ungleichung v.d ≥ δ(s, v). Beweis: Wir beweisen die Aussage durch Induktion über die Anzahl der Einfügeoperationen in die Warteschlange. Unsere Induktionsannahme ist, dass für alle v ∈ V die Ungleichung v.d ≥ δ(s, v) gilt. Den Induktionsanfang bildet der Zeitpunkt, unmittelbar nachdem s in Zeile 9 der Prozedur BFS in die Warteschlange eingefügt wurde. Die Induktionsannahme ist hier erfüllt, da die Gleichung s.d = 0 = δ(s, s) und für alle v ∈ V − {s} die Gleichung v.d = ∞ ≥ δ(s, v) gelten. Im Induktionsschritt betrachten wir einen weißen Knoten v, der während der Suche von einem Knoten u aus entdeckt wurde. Aus der Induktionsannahme folgt u.d ≥ δ(s, u). Wegen der in Zeile 15 ausgeführten Zuweisung und wegen Lemma 22.1 erhalten wir v.d = u.d + 1 ≥ δ(s, u) + 1 ≥ δ(s, v) . Der Knoten v wird dann in die Warteschlange eingefügt. Er wird kein weiteres Mal eingefügt, da er grau gefärbt wurde und der then-Zweig in den Zeilen 14–17 nur für weiße Knoten ausgeführt wird. Also ändert sich der Wert von v.d nie mehr, und die Induktionsannahme bleibt gültig. Um zu beweisen, dass v.d = δ(s, v) gilt, müssen wir zunächst genauer zeigen, wie die Warteschlange Q während der Ausführung von BFS arbeitet. Das nächste Lemma zeigt, dass die Warteschlange zu jedem Zeitpunkt höchstens zwei verschiedene Werte von d enthält.
22.2 Breitensuche
609
Lemma 22.3 Nehmen Sie an, die Warteschlange Q würde während der Ausführung von BFS auf einem Graphen G = (V, E) die Knoten v1 , v2 , . . . , vr enthalten, wobei v1 der Kopf und vr das Ende von Q wäre. Dann gelten vr .d ≤ v1 .d + 1 und vi .d ≤ vi+1 .d für i = 1, 2, . . . , r − 1. Beweis: Wir beweisen die Aussage durch Induktion über die Anzahl der Warteschlangen-Operationen. Zu Beginn, wenn die Warteschlange nur den Startknoten s enthält, gilt das Lemma mit Sicherheit. Im Induktionsschritt müssen wir beweisen, dass das Lemma sowohl für den Fall, dass ein Knoten eingefügt wurde, als auch für den Fall, dass ein Knoten entnommen wurde, gilt. Wenn der Kopf v1 der Warteschlange entnommen wurde, dann wird v2 der neue Kopf. (Falls die Warteschlange durch die Entnahme leer wird, dann gilt das Lemma trivialerweise.) Nach der Induktionsannahme gilt v1 .d ≤ v2 .d . Dann gilt aber vr .d ≤ v1 .d + 1 ≤ v2 .d + 1, und die übrigen Ungleichungen bleiben unberührt. Also folgt die Aussage des Lemmas mit v2 als Kopf. Um zu verstehen, was genau passiert, wenn wir einen Knoten in die Schlange einfügen, müssen wir uns den Pseudocode ein bisschen genauer anschauen. Wenn wir in Zeile 17 der Prozedur BFS einen Knoten v in die Warteschlange einfügen, wird dieser zu vr+1 . Zu diesem Zeitpunkt haben wir aus der Warteschlange Q bereits den Knoten u entfernt, dessen Adjazenzliste gerade durchsucht wird. Nach der Induktionsannahme gilt für den neuen Kopf v1 die Ungleichung v1 .d ≥ u.d . Somit gilt vr+1 .d = v.d = u.d + 1 ≤ v1 .d + 1. Nach der Induktionsannahme gilt außerdem vr .d ≤ u.d + 1, und daher vr .d ≤ u.d + 1 = v.d = vr+1 .d . Die übrigen Ungleichungen bleiben unberührt. Also gilt das Lemma, wenn v in die Warteschlange eingefügt wird. Das folgende Korollar zeigt, dass die Werte von d zum Zeitpunkt des Einfügens in die Warteschlange mit der Zeit monoton steigen.
Korollar 22.4 Nehmen Sie an, die Knoten vi und vj würden während der Ausführung der Prozedur BFS in die Warteschlange eingefügt werden, wobei vi vor vj eingefügt werden würde. Dann gilt vi .d ≤ vj .d zum Zeitpunkt des Einfügens von vj . Beweis: Der Beweis folgt unmittelbar aus Lemma 22.3 und der Eigenschaft, dass jeder Knoten während der Ausführung von BFS höchstens einmal einen endlichen Wert für d zugewiesen bekommt. Wir können nun beweisen, dass die Breitensuche die kleinsten Distanzen korrekt bestimmt.
610
22 Elementare Graphenalgorithmen
Theorem 22.5: (Korrektheit der Breitensuche) Sei G = (V, E) ein gerichteter oder ungerichteter Graph, auf dem die Prozedur BFS ausgehend von einem Startknoten s ∈ V angewendet wird. Dann entdeckt die Prozedur BFS während ihrer Ausführung jeden Knoten v ∈ V , der von dem Startknoten s aus erreichbar ist, und bei der Terminierung gilt v.d = δ(s, v) für alle v ∈ V . Außerdem ist für jeden beliebigen von s aus erreichbaren Knoten v = s einer der kürzesten Pfade von s nach v ein kürzester Pfad von s nach v.π, gefolgt von der Kante (v.π, v). Beweis: Wir wollen die Annahme, dass ein Knoten für d einen Wert ungleich seiner kleinsten Distanz erhalten würde, zum Widerspruch führen. Sei v der Knoten mit kleinster Distanz δ(s, v) aller Knoten, die einen inkorrekten Wert d durch BFS zugewiesen bekommen. Es gilt offensichtlich v = s. Nach Lemma 22.2 ist v.d ≥ δ(s, v) und somit v.d > δ(s, v). Der Knoten v muss von s aus erreichbar sein, denn wäre er dies nicht, würde δ(s, v) = ∞ ≥ v.d gelten. Sei u der Knoten, der auf einem kürzesten Pfad von s nach v unmittelbar vor v liegt, sodass δ(s, v) = δ(s, u) + 1 gilt. Wegen δ(s, u) < δ(s, v) und unserer speziellen Wahl von v gilt u.d = δ(s, u). All diese Eigenschaften zusammen führen zu v.d > δ(s, v) = δ(s, u) + 1 = u.d + 1 .
(22.1)
Betrachten Sie nun den Zeitpunkt, zu dem die Prozedur BFS entscheidet, den Knoten u in Zeile 11 aus Q zu entfernen. Zu diesem Zeitpunkt ist der Knoten v entweder weiß, grau oder schwarz. Wir werden zeigen, dass sich in jedem dieser Fälle ein Widerspruch zu Ungleichung (22.1) ableiten lässt. Falls v weiß ist, dann wird in Zeile 15 v.d = u.d +1 gesetzt, was Ungleichung (22.1) widerspricht. Falls der Knoten v schwarz ist, dann wurde er bereits aus der Warteschlange entfernt, und nach Korollar 22.4 gilt v.d ≤ u.d , was wiederum der Ungleichung (22.1) widerspricht. Falls der Knoten v grau ist, dann ist er beim Entfernen eines Knotens w aus der Warteschlange grau gefärbt worden. Dieser Knoten w wurde früher aus Q entfernt als u, und für ihn gilt v.d = w.d + 1. Nach Korollar 22.4 gilt aber w.d ≤ u.d , sodass die Ungleichung v.d = w.d + 1 ≤ u.d + 1 folgt, was wiederum ein Widerspruch zu Ungleichung (22.1) ist. Wir schlussfolgern, dass für alle v ∈ V die Gleichung v.d = δ(s, v) gilt. Alle von s aus erreichbaren Knoten müssen entdeckt werden, denn wäre dies nicht der Fall, so würde ∞ = v.d > δ(s, v) gelten. Um den Beweis des Theorems abzuschließen, haben Sie nur noch zu bemerken, dass v.d = u.d + 1 gilt, wenn v.π = u ist. Wir erhalten also einen kürzesten Pfad von s nach v, indem wir einen kürzesten Pfad von s nach v.π nehmen und dann der Kante (v.π, v) folgen.
Breitensuchbäume Die Prozedur BFS erzeugt, während sie den Graphen durchsucht, einen Breitensuchbaum, wie er in Abbildung 22.3 illustriert ist. Der Baum wird durch die π-Attribute
22.2 Breitensuche
611
definiert. Formaler ausgedrückt, definieren wir für einen Graphen G = (V, E) mit Startknoten s den Vorgängerteilgraphen von G durch Gπ = (Vπ , Eπ ) mit Vπ = {v ∈ V : v.π = nil} ∪ {s} und Eπ = {(v.π, v) : v ∈ Vπ − {s}} . Der Vorgängerteilgraph Gπ ist ein Breitensuchbaum, falls Vπ aus den Knoten besteht, die von s aus erreichbar sind, und Gπ für jeden Knoten v ∈ Vπ einen einzigen einfachen Pfad von s nach v enthält, der auch ein kürzester Pfad von s nach v in G ist. Ein Breitensuchbaum ist tatsächlich ein Baum, da er zusammenhängend ist und da |Eπ | = |Vπ | − 1 gilt (siehe Theorem B.2). Wir nennen die Kanten aus Eπ Baumkanten. Das folgende Lemma zeigt, dass nachdem die Prozedur BFS auf einem Graphen G ausgehend von einem Startknoten gelaufen ist, der Vorgängerteilgraph ein Breitensuchbaum ist. Lemma 22.6 Die Prozedur BFS, angewendet auf einen gerichteten oder ungerichteten Graphen G = (V, E), erzeugt π so, dass der Vorgängerteilgraph Gπ = (Vπ , Eπ ) ein Breitensuchbaum ist. Beweis: Zeile 16 der Prozedur BFS setzt v.π = u genau dann, wenn (u, v) ∈ E und δ(s, v) < ∞ gilt, d. h. wenn v von s aus erreichbar ist. Vπ besteht somit aus denjenigen Knoten von V , die von s aus erreichbar sind. Da Gπ ein Baum ist, folgt mit Theorem B.2, dass es jeweils einen einzigen einfachen Pfad von s zu jedem anderen Knoten aus Vπ gibt. Indem wir Theorem 22.5 rekursiv anwenden, können wir schließen, dass jeder derartige Pfad ein kürzester Pfad in G ist. Die folgende Prozedur gibt die Knoten entlang eines kürzesten Pfades von s nach v aus, wobei sie voraussetzt, dass BFS bereits einen Breitensuchbaum berechnet hat. Print-Path(G, s, v) 1 if v = = s 2 print s 3 elseif v.π = = nil 4 print “es gibt keinen Pfad von” s “nach” v 5 else Print-Path(G, s, v.π) 6 print v Die Laufzeit dieser Prozedur ist linear in der Anzahl der Knoten, die auf dem ausgegebenen Pfad liegen, denn jeder rekursive Aufruf erfolgt für einen Pfad, der einen Knoten kürzer ist.
612
22 Elementare Graphenalgorithmen
Übungen 22.2-1 Ermitteln Sie die Werte für d und π, die sich durch das Ausführen einer Breitensuche auf dem gerichteten Graphen aus Abbildung 22.2(a) ergeben, wenn der Knoten 3 als Startknoten dient. 22.2-2 Ermitteln Sie die Werte für d und π, die sich durch das Ausführen einer Breitensuche auf dem ungerichteten Graphen aus Abbildung 22.3 ergeben, wenn der Knoten u als Startknoten dient. 22.2-3 Zeigen Sie, dass zum Abspeichern der Farbe der Knoten jeweils ein einziges Bit ausreicht, indem Sie beweisen, dass die Prozedur BFS das gleiche Ergebnis produzieren würde, wenn die Zeile 18 in der Prozedur gestrichen werden würden. 22.2-4 Wie groß ist die Laufzeit der Prozedur BFS, wenn wir seinen Eingabegraph als Adjazenzmatrix darstellen und den Algorithmus so modifizieren, dass er mit dieser Form der Eingabe umgehen kann? 22.2-5 Zeigen Sie, dass bei einer Breitensuche der Wert u.d , der einem Knoten u zugewiesen wird, unabhängig von der Reihenfolge ist, in der die Knoten in den Adjazenzlisten angegeben sind. Zeigen Sie anhand von Abbildung 22.3, dass der durch BFS berechnete Breitensuchbaum von der Reihenfolge innerhalb der Adjazenzlisten abhängen kann. 22.2-6 Geben Sie ein Beispiel an für einen gerichteten Graphen G = (V, E), einen Startknoten s und eine Menge von Baumkanten Eπ ⊆ E mit der Eigenschaft, dass für jeden Knoten v ∈ V der einzige Pfad von s nach v im Graphen (V, Eπ ) ein kürzester Pfad in G ist, aber die Kantenmenge Eπ nicht durch das Ausführen von BFS auf G erzeugt werden kann, egal wie die Knoten innerhalb der Adjazenzlisten geordnet sind. 22.2-7 Es gibt zwei Arten von professionellen Wrestlern: „gute Jungs“ und „böse Jungs“. Zwischen jedem Paar von professionellen Wrestlern kann eine Rivalität bestehen oder auch nicht. Nehmen Sie an, wir hätten n professionelle Wrestler und wir hätten eine Liste mit r Paaren von Wrestlern, zwischen denen Rivalität herrscht. Geben Sie einen Algorithmus mit Laufzeit O(n + r) an, der bestimmt, ob es möglich ist, einigen der Wrestler die Rolle der gute Jungs und den übrigen die der schlechten Jungs zuzuweisen, sodass alle Rivalitäten zwischen guten und bösen Jungs bestehen. Wenn es möglich ist, eine solche Zuordnung zu machen, dann sollte Ihr Algorithmus diese erzeugen. 22.2-8∗Der Durchmesser eines Baumes T = (V, E) ist als maxu,v∈V δ(u, v) definiert, d. h. als die größte Distanz im Baum. Geben Sie einen effizienten Algorithmus zur Berechnung des Durchmessers an und analysieren Sie dessen Laufzeit. 22.2-9 Sei G = (V, E) ein zusammenhängender ungerichteter Graph. Geben Sie einen Algorithmus mit Laufzeit O(V + E) an, der einen Pfad in G berechnet, der jede Kante in E in jeder Richtung genau einmal durchläuft. Beschreiben Sie,
22.3 Tiefensuche
613
wie Sie den Weg aus einem Irrgarten heraus finden können, wenn Sie einen großen Vorrat von Centstücken bei sich haben.
22.3
Tiefensuche
Die Strategie der Tiefensuche besteht, wie ihr Name bereits andeutet, darin, „tiefer“ im Graphen zu suchen, wann immer dies möglich ist. Tiefensuche untersucht Kanten, die von dem zuletzt entdeckten Knoten v ausgehen, der noch nicht untersuchte ausgehende Kanten hat. Sobald alle Kanten von v untersucht wurden, „kehrt die Suche zurück“ und untersucht die Kanten, die von demjenigen Knoten ausgehen, von dem aus v entdeckt wurde. Dieser Prozess wird fortgesetzt, bis alle vom ursprünglichen Startknoten aus erreichbaren Knoten entdeckt wurden. Falls unentdeckte Knoten übrig bleiben, wählt die Tiefensuche einen von ihnen als neuen Startknoten aus und wiederholt die Suche von diesem Startknoten aus. Der Algorithmus wiederholt diesen gesamten Prozess, bis er alle Knoten entdeckt hat. Wie bei der Breitensuche merkt sich die Tiefensuche, wenn sie bei der Durchsuchung einer Adjazenzliste eines bereits entdeckten Knotens u einen Knoten v entdeckt, indem sie v’s Vorgängerattribut v.π auf u setzt. Im Unterschied zur Breitensuche, bei der der Vorgängerteilgraph einen Baum bildet, kann der bei der Tiefensuche erzeugte Vorgängerteilgraph aus mehreren Bäumen bestehen, da die Suche von verschiedenen Startknoten aus wiederholt wird.3 Der Vorgängerteilgraph bei einer Tiefensuche ist daher etwas anders definiert als der Vorgängerteilgraph bei einer Breitensuche. Es sei Gπ = (V, Eπ ) mit Eπ = {(v.π, v) : v ∈ V und v.π = nil} . Der Vorgängerteilgraph einer Tiefensuche bildet einen Tiefensuchwald, der aus mehreren Tiefensuchbäumen zusammengesetzt ist. Die Kanten aus Eπ sind Baumkanten. Wie die Breitensuche färbt die Tiefensuche die Knoten während dem Durchsuchen eines Graphen, um sich ihre Zustände zu merken. Jeder Knoten ist anfangs weiß, wird grau gefärbt, wenn er entdeckt wird, und geschwärzt, wenn er abgearbeitet ist. Dieses Vorgehen gewährleistet, dass sich jeder Knoten letztendlich in genau einem Tiefensuchbaum befinden wird, sodass diese Bäume paarweise disjunkt sind. Neben dem Erzeugen eines Tiefensuchwaldes versieht die Tiefensuche alle Knoten mit Zeitstempeln. Jeder Knoten v hat zwei Zeitstempel: Der erste Zeitstempel v.d zeichnet auf, wann v entdeckt (und grau gefärbt) wurde, und der zweite Zeitstempel v.f zeichnet 3 Es mag willkürlich erscheinen, dass die Breitensuche auf einen einzigen Startknoten beschränkt ist, während die Tiefensuche ausgehend von mehreren Startknoten suchen kann. Obwohl vom Konzept her auch die Breitensuche mit mehreren Startknoten arbeiten und die Tiefensuche auf einen einzigen Startknoten beschränkt werden könnte, spiegelt unsere Darstellung wider, wie die Ergebnisse dieser beiden Suchalgorithmen üblicherweise verwendet werden. Die Breitensuche dient der Berechnung kleinster Distanzen (und des zugehörigen Vorgängerteilgraphen). Die Tiefensuche wird häufig als Unterroutine in einem anderen Algorithmus verwendet, wie wir weiter hinten in diesem Kapitel sehen werden.
614
22 Elementare Graphenalgorithmen
auf, wann die Suche das Durchsuchen der Adjazenzliste von v beendet und v schwarz gefärbt wird. Diese Zeitstempel werden in vielen Graphenalgorithmen verwendet und sind allgemein hilfreich bei Überlegungen zum Verhalten der Tiefensuche. Die nachfolgend angegebene Prozedur DFS zeichnet in der Variable u.d auf, wann sie den Knoten u entdeckt hat, und in der Variable u.f , wann sie den Knoten abgearbeitet hat. Diese Zeitstempel sind ganze Zahlen zwischen 1 und 2 |V |, da es für jeden der |V | Knoten genau ein Ereignis „entdeckt“ und genau ein Ereignis „abgearbeitet“ gibt. Für jeden Knoten u gilt (22.2)
u.d < u.f .
Der Knoten u ist vor dem Zeitpunkt u.d weiß, zwischen dem Zeitpunkt u.d und dem Zeitpunkt u.f grau und danach schwarz. Der folgende Pseudocode gibt den grundlegenden Algorithmus der Tiefensuche an. Der Eingabegraph G kann ungerichtet oder gerichtet sein. Die Variable zeit ist eine globale Variable, die wir für das Zeitstempeln benötigen. DFS(G) 1 for jeden Knoten u ∈ G.V 2 u.farbe = weiss 3 u.π = nil 4 zeit = 0 5 for jeden Knoten u ∈ G.V 6 if u.farbe = = weiss 7 DFS-Visit(G, u) DFS-Visit(G, u) 1 zeit = zeit + 1 2 u.d = zeit 3 u.farbe = grau 4 for jeden Knoten v ∈ G.Adj [u] 5 if v.farbe = = weiss 6 v.π = u 7 DFS-Visit(G, v) 8 u.farbe = schwarz 9 zeit = zeit + 1 10 u.f = zeit
// der weiße Knoten u wurde gerade entdeckt
// verfolge die Kante (u, v)
// färbe u schwarz; er ist abgearbeitet
Abbildung 22.4 illustriert die Arbeitsweise der Prozedur DFS auf dem in Abbildung 22.2 gezeigten Graphen. Die Prozedur DFS arbeitet folgendermaßen. Die Zeilen 1–3 färben alle Knoten weiß und initialisieren ihr Vorgängerattribut π mit nil. Zeile 4 setzt den globalen Zeitzähler zurück. Die Zeilen 5–7 überprüfen der Reihe nach alle Knoten aus V . Wenn ein weißer Knoten gefunden wird, wird die Prozedur DFS-Visit auf ihn angewendet. Jedes Mal,
22.3 Tiefensuche
615
u 1/
v
w
u 1/
x
y (a)
z
x
u 1/
v 2/
w
u 1/
3/ y
F
u 1/
v 2/
y (b)
z
x
3/ y (c)
v 2/
w
u 1/
v 2/
z
4/5 x
w
u 1/8 F
B
z
w
u 1/
v 2/
4/ x
3/ y (d)
u 1/
v 2/7
z
4/5 x
z
w
u 1/8 F
B
z
w
3/6 y
(g)
v 2/7
w
B
3/6 y
(f)
v 2/7
w
B
3/ y
4/5 x
(e) u 1/
w
B
B
4/ x
v 2/
z
(h)
v 2/7
w 9/
u 1/8 F
B
v 2/7 B
w 9/ C
4/5 x
3/6 y (i)
z
4/5 x
3/6 y (j)
z
4/5 x
3/6 y (k)
z
4/5 x
3/6 y (l)
z
u 1/8
v 2/7
w 9/
u 1/8
v 2/7
w 9/
u 1/8
v 2/7
w 9/
u 1/8
v 2/7
w 9/12
F
B
C
F
B
C
F
B
C
F
B
4/5 x
3/6 y (m)
10/ z
4/5 x
3/6 y (n)
10/ z
B
C
B
4/5 x
3/6 y (o)
10/11 z
B
4/5 x
3/6 y
10/11 z
(p)
Abbildung 22.4: Die Arbeitsweise der Prozedur DFS zur Tiefensuche auf einem ungerichteten Graphen. Kanten, die durch den Algorithmus sondiert werden, sind entweder schattiert (falls es sich um Baumkanten handelt) oder mit Strichlinien (sonst) gezeichnet. Kanten, die keine Baumkanten sind, sind mit B, C oder F gekennzeichnet, je nachdem, ob es sich um Rückwärtskanten, Querkanten oder Vorwärtskanten handelt. Die Knoten erhalten Zeitstempel, wenn sie entdeckt werden bzw. wenn sie fertig abgearbeitet sind – d. h. die Knoten werden mit ihrem jeweiligen Entdeckungszeitpunkt und ihrem jeweiligen Endzeitpunkt annotiert.
616
22 Elementare Graphenalgorithmen
wenn DFS-Visit(G, u) in Zeile 7 angewendet wird, wird der Knoten u die Wurzel eines neuen Baumes des Tiefensuchwaldes. Wenn DFS terminiert, ist jedem Knoten u ein Entdeckungszeitpunkt u.d und ein Endzeitpunkt u.f zugeordnet worden. Bei jedem Aufruf von DFS-Visit(G, u) ist der Knoten u anfangs weiß. Zeile 1 inkrementiert die globale Variable zeit , Zeile 2 ordnet den neuen Wert von zeit dem Attribut u.d von u zu und Zeile 3 färbt u grau. Die Zeilen 4–7 untersuchen alle mit u benachbarten Knoten v und steigen zu v rekursiv ab, falls v weiß ist. Wenn in Zeile 4 ein Knoten v ∈ Adj [u] betrachtet wird, sagen wir, die Kante (u, v) wird durch die Tiefensuche sondiert. Nachdem schließlich alle Kanten, die aus u austreten, sondiert wurden, färben die Zeilen 8–10 den Knoten u schwarz und speichern in u.f den Zeitpunkt, zu dem u fertig abgearbeitet worden ist. Sie sollten bemerken, dass die Ergebnisse der Tiefensuche von der Reihenfolge abhängen können, in der die Zeile 5 von DFS sich die Knoten anschaut, und von der Reihenfolge, in der die Zeile 4 der Prozedur DFS-Visit die Nachbarn eines Knotens besucht. Diese unterschiedlichen Reihenfolgen führen in der Praxis nicht zu Problemen, da wir normalerweise jedes Resultat einer Tiefensuche effektiv verwenden können und mit ihnen zu gleichen Ergebnissen kommen. Wie hoch ist die Laufzeit von DFS? Die Schleifen in den Zeilen 1–3 und 5–7 von DFS benötigen ohne die Zeit für das Ausführen von DFS-Visit Zeit Θ(V ). Wie für die Breitensuche verwenden wir im Folgenden die Aggregat-Analyse. Die Prozedur DFSVisit wird für jeden Knoten v ∈ V genau einmal aufgerufen, da der Knoten u, auf dem DFS-Visit aufgerufen wird, weiß sein muss und das erste, was die Prozedur DFSVisit tut, darin besteht, dass sie den Knoten u grau färbt. Während der Ausführung von DFS-Visit(G, v) wird die Schleife in den Zeilen 4–7 |Adj [v]|-mal durchlaufen. Wegen |Adj [v]| = Θ(E) v∈V
sind die Gesamtkosten für die Zeilen 4–7 von DFS-Visit in Θ(E). Die Laufzeit der Prozedur DFS ist also in Θ(V + E).
Eigenschaften der Tiefensuche Die Tiefensuche liefert wertvolle Informationen über die Struktur eines Graphen. Die vielleicht wichtigste Eigenschaft der Tiefensuche ist, dass der Vorgängerteilgraph Gπ tatsächlich einen Wald bildet, da die Struktur der Tiefensuchbäume gerade die Struktur der rekursiven Aufrufe von DFS-Visit widerspiegelt. Es gilt also u = v.π genau dann, wenn beim Durchsuchen der Adjazenzliste von u die Prozedur DFS-Visit(G, v) aufgerufen wurde. Außerdem ist der Knoten v genau dann im Tiefensuchwald ein Nachfahre des Knotens u, wenn v in der Zeit entdeckt wurde, in der u grau ist. Eine weitere wichtige Eigenschaft der Tiefensuche ist, dass Entdeckungs- und Endzeitpunkte eine Klammerstruktur haben. Wenn wir die Entdeckung des Knotens u durch eine linke Klammer „(u“ und das Ende seiner Abarbeitung durch eine rechte Klammer „u)“ darstellen, dann bildet die Historie der Entdeckungen und Abarbeitungen einen wohlgeformten Ausdruck in dem Sinne, dass er korrekt geklammert ist. Beispielsweise
22.3 Tiefensuche
617
entspricht der Tiefensuchbaum aus Abbildung 22.5(a) der in Abbildung 22.5(b) gezeigten Klammerung. Das folgende Theorem zeigt eine andere Möglichkeit auf, die Klammerstruktur zu charakterisieren. Theorem 22.7: (Klammerungstheorem) Bei jeder Tiefensuche in einem (gerichteten oder ungerichteten) Graphen G = (V, E) ist für jedes Paar von Knoten u und v genau eine der folgenden drei Bedingungen erfüllt: • die Intervalle [u.d , u.f ] und [v.d , v.f ] sind paarweise disjunkt und weder u noch v ist im Tiefensuchwald ein Nachfahre des anderen; • das Intervall [u.d , u.f ] ist vollständig im Intervall [v.d , v.f ] enthalten und u ist im Tiefensuchwald ein Nachfahre von v; • das Intervall [v.d , v.f ] ist vollständig im Intervall [u.d , u.f ] enthalten und v ist im Tiefensuchwald ein Nachfahre von u. Beweis: Wir beginnen mit dem Fall u.d < v.d . Es sind zwei Teilfälle zu unterscheiden, je nachdem, ob v.d < u.f gilt oder nicht. Im Fall v.d < u.f wurde v entdeckt, als u noch grau war. Das bedeutet, dass v ein Nachfahre von u ist. Da v später als u entdeckt wurde, sind außerdem alle aus v austretenden Kanten sondiert und v ist abgearbeitet, bevor die Suche zurückkehrt und u abarbeitet. Daher ist in diesem Fall das Intervall [v.d , v.f ] vollständig in dem Intervall [u.d , u.f ] enthalten. Im Fall u.f < v.d folgt aus Ungleichung (22.2), dass die Intervalle [u.d , u.f ] und [v.d , v.f ] disjunkt sind. Aus diesem Grund wurde keiner der Knoten entdeckt, während der andere grau war, und somit ist keiner der Knoten ein Nachfahre des jeweils anderen. Der Fall v.d < u.d ist analog zu behandeln, wobei die Rollen von u und v vertauscht sind. Korollar 22.8: (Intervalle der Nachfahren) Der Knoten v ist im Tiefensuchwald eines (gerichteten oder ungerichteten) Graphen G genau dann ein echter Nachfahre des Knotens u, wenn u.d < v.d < v.f < u.f gilt. Beweis: Der Beweis folgt unmittelbar aus Theorem 22.7.
Das nächste Theorem liefert eine weitere wichtige Charakteristik dafür, wann ein Knoten im Tiefensuchwald ein Nachfahre eines anderen Knotens ist. Theorem 22.9: (Theorem der weißen Pfade) In einem Tiefensuchwald eines (gerichteten oder ungerichteten) Graphen G = (V, E) ist der Knoten v genau dann ein Nachfahre des Knotens u, wenn es zum Zeitpunkt u.d , zu dem die Durchmusterung den Knoten u entdeckt hat, einen Pfad von u nach v gibt, der nur aus weißen Knoten besteht.
618
22 Elementare Graphenalgorithmen
y 3/6
z 2/9
s 1/10
B
(a) 4/5 x
F
7/8 w
C
t 11/16
C 12/13 v
C
B 14/15 u
C
s
t
z (b)
v
y
u
w
x 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 (s (z (y (x x) y) (w w) z) s) (t (v v) (u u) t)
s
t B
C z
B
F
v
C
u
C
(c) y
w C
x Abbildung 22.5: Eigenschaften der Tiefensuche. (a) Das Ergebnis einer Tiefensuche auf einem gerichteten Graphen. Die Knoten erhalten Zeitstempel und die Kantentypen sind wie in Abbildung 22.4 gekennzeichnet. (b) Die Spanne zwischen Entdeckungs- und Endzeitpunkt jedes Knotens spiegelt sich in der angegebenen Klammerung wider. Jedes Rechteck symbolisiert für den entsprechenden Knoten die Zeitspanne zwischen seiner Entdeckung und dem Ende seiner Abarbeitung. Nur Baumkanten sind eingezeichnet. Falls sich zwei Intervalle überlappen, ist eines davon vollständig in dem anderen eingeschlossen, und der Knoten, der zu dem kleineren Intervall gehört, ist ein Nachfolger des Knotens, der dem größeren Intervall entspricht. (c) Nochmals der Graph aus Teil (a) mit allen Baum- und Vorwärtskanten, die in einem Tiefensuchbaum jeweils nach unten zu einem Nachfahren gehen, und allen Rückwärtskanten, die jeweils zu einem Vorfahren gehen.
22.3 Tiefensuche
619
Beweis: ⇒: Wenn v = u gilt, dann enthält der Pfad von u nach v nur den Knoten u, der noch weiß ist, wenn wir den Wert von u.d zuweisen. Nehmen Sie nun an, v wäre ein echter Nachfolger von u in dem Tiefensuchwald. Wegen Korollar 22.8 gilt u.d < v.d und so ist v zum Zeitpunkt u.d weiß. Da v ein beliebiger Nachfolger von u ist, sind alle Knoten auf dem einzigen einfachen Pfad von u nach v in dem Tiefensuchwald zum Zeitpunkt u.d weiß. ⇐: Nehmen Sie an, es gäbe zum Zeitpunkt u.d einen Pfad von weißen Knoten von u nach v, aber v wäre kein Nachfahre von u im Tiefensuchbaum. Ohne Beschränkung der Allgemeinheit setzen wir voraus, dass jeder andere Knoten entlang des Pfades ein Nachfahre von u wird. (Anderenfalls sei v der auf dem Pfad zu u am nächsten liegende Knoten, der kein Nachfahre von u wird.) Sei w der Vorgänger von v auf dem Pfad, sodass w ein Nachfahre von u ist (Knoten w und Knoten u können auch identisch sein). Nach Korollar 22.8 gilt w.f ≤ u.f . Da v entdeckt wird, nachdem u entdeckt worden ist, aber bevor w abgearbeitet ist, gilt u.d < v.d < w.f ≤ u.f . Aus Theorem 22.7 folgt dann, dass das Intervall [v.d , v.f ] vollständig in [u.d , u.f ] enthalten ist. Nach Korollar 22.8 muss v also doch ein Nachfahre von u sein.
Klassifikation der Kanten Eine weitere interessante Eigenschaft der Tiefensuche besteht darin, dass die Suche verwendet werden kann, um die Kanten des Eingabegraphen G = (V, E) zu klassifizieren. Der Typ einer jeden Kante kann wichtige Informationen über einen Graphen geben. Zum Beispiel werden wir im nächsten Abschnitt feststellen, dass ein gerichteter Graph genau dann azyklisch ist, wenn eine Tiefensuche keine „Rückwärtskanten“ liefert (Lemma 22.11). Wir können anhand des Tiefensuchwaldes Gπ , der bei einer Tiefensuche auf G erzeugt wird, vier Kantentypen definieren: 1. Baumkanten sind Kanten in dem Tiefensuchwald Gπ . Kante (u, v) ist eine Baumkante, falls v bei der Sondierung der Kante (u, v) entdeckt wurde. 2. Rückwärtskanten sind diejenigen Kanten (u, v), die einen Knoten u mit einem Vorfahren v im Tiefensuchbaum verbinden. Wir sehen Schlingen, die in gerichteten Graphen vorkommen können, als Rückwärtskanten an. 3. Vorwärtskanten sind diejenigen Kanten (u, v), die einen Knoten u mit einem Nachfahren v im Tiefensuchbaum verbinden, aber keine Baumkanten sind. 4. Querkanten sind alle übrigen Kanten. Sie können zwischen zwei Knoten des gleichen Tiefensuchbaums verlaufen, solange nicht der eine Knoten ein Vorfahre des anderen ist. Sie können aber auch zwischen Knoten verschiedener Tiefensuchbäume verlaufen. In den Abbildungen 22.4 und 22.5 sind die Kanten entsprechend ihres Typs gekennzeichnet. Abbildung 22.5(c) zeigt außerdem, wie der Graph aus Abbildung 22.5(a) so
620
22 Elementare Graphenalgorithmen
gezeichnet werden kann, dass alle Baum- und Vorwärtskanten in einem Tiefensuchbaum nach unten, also abwärts, und alle Rückwärtskanten nach oben, also aufwärts, gerichtet sind. Wir können jeden Graphen in dieser Art und Weise zeichnen. Der Algorithmus DFS kann so modifiziert werden, dass er die Kanten klassifiziert, die er begegnet. Die Idee besteht darin, dass, wenn wir eine Kante (u, v) erstmals sondieren, die Farbe des Knotens v uns etwas über die Kante (u, v) sagt: 1. weiß bedeutet, dass es sich um eine Baumkante handelt, 2. grau bedeutet, dass es sich um eine Rückwärtskante handelt, 3. schwarz bedeutet, dass es sich um eine Vorwärts- oder eine Querkante handelt. Der erste Fall folgt unmittelbar aus der Spezifikation des Algorithmus. Für den zweiten Fall müssen Sie sich überlegen, dass die grauen Knoten immer eine lineare Kette von Nachfolgern darstellt, die den gerade aktiven Aufrufen von DFS-Visit entsprechen. Die Anzahl der grauen Kanten ist um 1 größer als die Tiefe des zuletzt entdeckten Knotens im Tiefensuchwald. Die Sondierungen starten immer vom tiefsten grauen Knoten, sodass eine Kante, deren Zielknoten ein grauer Knoten ist, zu einem seiner Vorfahren geht. Der dritte Fall behandelt die verbleibende Möglichkeit. Übung 22.3-5 verlangt von Ihnen, zu zeigen, dass eine solche Kante (u, v) im Falle u.d < v.d eine Vorwärtskante und im Falle u.d > v.d eine Querkante ist. In einem ungerichteten Graphen kann es bei der Klassifizierung zu Mehrdeutigkeiten kommen, da (u, v) die gleiche Kante wie (v, u) ist. In diesem Fall geben wir der Kante den Typ, der in der obigen Aufzählung als erster auf sie zutrifft. Oder anders formuliert (siehe Übung 22.3-6): wir klassifizieren die Kante in Anhängigkeit, ob die Durchmusterung zuerst (u, v) oder (v, u) sondiert. Wir zeigen nun, dass weder Vorwärts- noch Querkanten während einer Tiefensuche auf einem ungerichteten Graphen vorkommen. Theorem 22.10 Bei einer Tiefensuche auf einem ungerichteten Graphen G ist jede Kante entweder eine Baumkante oder eine Rückwärtskante. Beweis: Sei (u, v) eine beliebige Kante von G. Wir können ohne Beschränkung der Allgemeinheit voraussetzen, dass u.d < v.d gilt. Dann muss die Tiefensuche den Knoten v entdeckt und fertig abgearbeitet haben, bevor u fertig abgearbeitet ist (u ist grau währenddessen), da v in der Adjazenzliste von u enthalten ist. Falls die Durchmusterung die Kante zuerst in der Richtung von u nach v sondiert, dann ist v bis zu diesem Zeitpunkt unentdeckt (weiß) gewesen, da wir diese Kante sonst bereits in der Richtung von v nach u sondiert hätten. Somit wird (u, v) eine Baumkante. Falls die Durchmusterung die Kante (u, v) zuerst in der Richtung von v nach u sondiert, dann ist (u, v) eine Rückwärtskante, da u zu dem Zeitpunkt, zu dem die Kante erstmals sondiert wird, noch grau ist.
22.3 Tiefensuche
621 q
s
r t
v
w
x
u y
z Abbildung 22.6: Ein gerichteter Graph, der in den Übungen 22.3-2 und 22.5-2 verwendet wird.
Wir werden in den nächsten Abschnitten verschiedene Anwendungen dieses Theorems beweisen.
Übungen 22.3-1 Fertigen Sie eine 3×3-Tabelle mit den Zeilen- und Spaltenköpfen weiß, grau und schwarz an. Vermerken Sie in jeder Zelle (i, j), ob es zu irgendeinem Zeitpunkt der Tiefensuche auf einem gerichteten Graphen eine Kante von einem Knoten der Farbe i zu einem Knoten der Farbe j geben kann. Falls es eine solche Kante geben kann, tragen Sie außerdem ein, um welche Kantentypen es sich handeln kann. Fertigen Sie eine zweite solche Tabelle für die Tiefensuche auf einem ungerichteten Graphen an. 22.3-2 Zeigen Sie, wie die Tiefensuche auf dem Graphen aus Abbildung 22.6 arbeitet. Setzen Sie voraus, dass die for-Schleife der Zeilen 5–7 der Prozedur DFS die Kanten in alphabetischer Reihenfolge abarbeitet und dass jede Adjazenzliste alphabetisch geordnet ist. Bestimmen Sie für jeden Knoten den Entdeckungsund Endzeitpunkt sowie die Klassifikation einer jeden Kante. 22.3-3 Bestimmen Sie die Klammerstruktur der in Abbildung 22.4 gezeigten Tiefensuche. 22.3-4 Zeigen Sie, dass ein einziges Bit pro Knoten ausreicht, um die Farben der Knoten abzuspeichern, indem Sie zeigen, dass die Prozedur DFS das gleiche Ergebnis liefern würde, wenn wir die Zeile 8 aus der Prozedur DFS-Visit streichen würden. 22.3-5 Zeigen Sie, dass die Kante (u, v) a. genau dann eine Baum- oder Vorwärtskante ist, wenn u.d < v.d < v.f < u.f gilt, b. genau dann eine Rückwärtskante ist, wenn v.d ≤ u.d < u.f ≤ v.f gilt, und c. genau dann eine Querkante ist, wenn v.d < v.f < u.d < u.f gilt.
622
22 Elementare Graphenalgorithmen
22.3-6 Zeigen Sie, dass in einem ungerichteten Graphen die Klassifizierung einer Kante (u, v) als Baumkante oder Rückwärtskante, je nachdem, ob bei der Tiefensuche zuerst (u, v) oder (v, u) sondiert wird, äquivalent mit der Klassifizierung anhand der Priorität der Typen in der Klassifizierungsliste ist. 22.3-7 Schreiben Sie die Prozedur DFS so um, dass ein Stapel verwendet wird und damit die Rekursion vermieden wird. 22.3-8 Geben Sie ein Gegenbeispiel zu folgender Vermutung an: Falls ein gerichteter Graph G einen Pfad von u nach v enthält und bei einer Tiefensuche auf G u.d < v.d gilt, dann ist v im erzeugten Tiefensuchwald ein Nachfahre von u. 22.3-9 Geben Sie ein Gegenbeispiel zu folgender Vermutung an: Falls ein gerichteter Graph G einen Pfad von u nach v enthält, dann muss jede Tiefensuche zu v.d ≤ u.f führen. 22.3-10 Modifizieren Sie den Pseudocode für die Tiefensuche so, dass sie jede Kante des gerichteten Graphen G zusammen mit ihrem Typ ausgibt. Welche Modifikationen müssen (wenn überhaupt) gemacht werden, wenn G ungerichtet ist? 22.3-11 Erklären Sie, wie es passieren kann, dass ein Knoten u eines gerichteten Graphen in einem Tiefensuchbaum landet, der allein u enthält, obwohl u sowohl eintretende als auch austretende Kanten hat. 22.3-12 Zeigen Sie, dass wir eine Tiefensuche auf einem ungerichteten Graphen G verwenden können, um die Zusammenhangskomponenten von G zu berechnen, und dass der Tiefensuchwald aus so vielen Bäumen besteht, wie der Graph G Zusammenhangskomponenten besitzt. Genauer gesagt sollen Sie zeigen, wie wir die Tiefensuche modifizieren müssen, damit jedem Knoten v ein ganzzahliger Wert v.cc zwischen 1 und k zugeordnet wird, sodass u.cc = v.cc genau dann gilt, wenn u und v zur gleichen Zusammenhangskomponente gehören, wobei k die Anzahl der Zusammenhangskomponenten von G ist. 22.3-13∗ Ein gerichteter Graph G = (V, E) heißt einfach zusammenhängend, falls für alle Knoten u, v ∈ V aus u ; v folgt, dass G höchstens einen einfachen Pfad von u nach v enthält. Geben Sie einen effizienten Algorithmus an, der bestimmt, ob ein gerichteter Graph einfach zusammenhängend ist.
22.4
Topologisches Sortieren
Dieser Abschnitt zeigt, wie wir die Tiefensuche nutzen können, um eine topologische Sortierung eines gerichteten azyklischen Graphen (auch „DAG“ genannt, für engl. directed acyclic graph) durchzuführen. Eine topologische Sortierung eines gerichteten azyklischen Graphen G = (V, E) ist eine lineare Anordnung aller seiner Knoten mit der Eigenschaft, dass u in der Anordnung vor v liegt, falls es in G eine Kante (u, v) gibt. (Wenn der Graph einen Zyklus enthält, dann ist eine lineare Anordnung nicht möglich.)
22.4 Topologisches Sortieren
623
(a) 11/16
Unterhosen
Socken 17/18 Uhr
12/15
Hosen
9/10
Schuhe 13/14 Hemd 1/8
6/7
Gürtel Krawatte 2/5
Jackett 3/4
(b) Socken
Unterhosen
Hosen
Schuhe
Uhr
Hemd
Gürtel
Krawatte
Jackett
17/18
11/16
12/15
13/14
9/10
1/8
6/7
2/5
3/4
Abbildung 22.7: (a) Professor Bumstead führt eine topologische Sortierung seiner Kleidungsstücke durch, wenn er sich anzieht. Jede gerichtete Kante (u, v) bedeutet, dass das Kleidungsstück u vor dem Kleidungsstück v angezogen sein muss. Die Entdeckungs- und Endzeitpunkte einer Tiefensuche sind neben den Knoten angegeben. (b) Der gleiche Graph, dargestellt in topologisch sortierter Form, wobei seine Knoten von links nach rechts monoton fallend nach ihren Endzeitpunkten angeordnet sind. Alle gerichteten Kanten verlaufen von links nach rechts.
Wir können eine topologische Sortierung eines Graphen als Anordnung seiner Knoten entlang einer horizontalen Linie auffassen, sodass alle gerichteten Kanten von links nach rechts zeigen. Topologisches Sortieren ist also etwas anderes als das gewöhnliche „Sortieren“, das in Teil II untersucht wurde. Viele Anwendungen setzen gerichtete azyklische Graphen ein, um die Priorität von Ereignissen darzustellen. Abbildung 22.7 skizziert ein Beispiel, das auftritt, wenn sich Professor Bumstead morgens ankleidet. Der Professor muss bestimmte Kleidungsstücke vor anderen angezogen haben (zum Beispiel die Socken vor den Schuhen). Andere Sachen können in beliebiger Reihenfolge angezogen werden (zum Beispiel Socken und Hose). Eine gerichtete Kante (u, v) des gerichteten azyklischen Graphen aus Abbildung 22.7(a) zeigt an, dass das Kleidungsstück u vor dem Kleidungsstück v angezogen sein muss. Eine topologische Sortierung dieses gerichteten azyklischen Graphen gibt also eine Reihenfolge für das Ankleiden an. Abbildung 22.7(b) zeigt den topologisch sortierten gerichteten azyklischen Graphen in Form einer Anordnung der Knoten entlang einer horizontalen Linie, wobei alle gerichteten Kanten von links nach rechts zeigen. Der folgende einfache Algorithmus sortiert einen gerichteten azyklischen Graphen topologisch.
624
22 Elementare Graphenalgorithmen
Topological-Sort(G) 1 rufe DFS(G) auf, um die Endzeitpunkte v.f aller Knoten zu berechnen 2 füge jeden abgearbeiteten Knoten an den Kopf einer verketteten Liste ein 3 return die verkettete Liste der Knoten Abbildung 22.7(b) zeigt, wie die topologisch sortierten Knoten in der umgekehrten Reihenfolge nach ihren Endzeitpunkten angeordnet sind. Wir können eine topologische Sortierung in Zeit Θ(V +E) ausführen, da die Tiefensuche Zeit Θ(V + E) und das Einfügen jedes der |V | Knoten an den Kopf der verketteten Liste Zeit O(1) benötigt. Wir beweisen die Korrektheit des Algorithmus unter Verwendung des folgenden Lemmas, das gerichtete azyklische Graphen charakterisiert. Lemma 22.11 Ein gerichteter Graph G ist genau dann azyklisch, wenn eine Tiefensuche auf G keine Rückwärtskanten liefert. Beweis: ⇒: Nehmen Sie an, eine Tiefensuche würde eine Rückwärtskante (u, v) produzieren. Dann ist der Knoten v ein Vorfahre des Knotens u im Tiefensuchwald. Somit enthält G einen Pfad von v nach u, der zusammen mit der Rückwärtskante (u, v) einen Zyklus ergibt. ⇐: Nehmen Sie an, G würde einen Zyklus c enthalten. Wir zeigen, dass eine Tiefensuche auf G dann eine Rückwärtskante liefert. Sei v der erste Knoten, der auf c entdeckt wird, und sei (u, v) die Kante des Zyklus c, die in v eingeht. Zum Zeitpunkt v.d bilden die Knoten von c einen weißen Pfad von v nach u. Wegen des Theorems der weißen Pfade wird der Knoten u ein Nachfahre von v im Tiefensuchwald. Also ist (u, v) eine Rückwärtskante.
Theorem 22.12 Topological-Sort erzeugt eine topologische Sortierung des gerichteten azyklischen Graphen, der der Prozedur als Eingabe übergeben wird. Beweis: Nehmen Sie an, der Algorithmus DFS würde auf einen gegebenen gerichteten azyklischen Graphen G = (V, E) angewendet, um die Endzeitpunkte für dessen Knoten zu bestimmen. Wir müssen nur zeigen, dass für jedes Paar von unterschiedlichen Knoten u, v ∈ V v.f < u.f gilt, falls G eine Kante von u nach v enthält. Betrachten Sie eine beliebige Kante (u, v), die von DFS(G) sondiert wird. Wenn die Kante sondiert wird, kann v nicht grau sein, da v sonst ein Vorfahre von u und (u, v) eine Rückwärtskante wäre, was im Widerspruch zu Lemma 22.11 stünde. Also muss v entweder weiß oder schwarz sein. Falls der Knoten v weiß ist, wird er ein Nachfahre von u, sodass v.f < u.f
22.4 Topologisches Sortieren m
n q
t
o r
u x
625 p s
v y
w z
Abbildung 22.8: Ein gerichteter azyklischer Graph für topologisches Sortieren.
gilt. Falls der Knoten v schwarz ist, ist er bereits abgearbeitet und v.f somit schon gesetzt. Da wir immer noch von u aus sondieren, müssen wir u den Zeitstempel u.f noch zuordnen, und wenn wir dies getan haben, wird ebenfalls v.f < u.f gelten. Es gilt also für alle Kanten (u, v) des gerichteten azyklischen Graphen v.f < u.f , womit das Theorem bewiesen ist.
Übungen 22.4-1 Geben Sie die Reihenfolge der Knoten an, die entsteht, wenn wir die Prozedur Topological-Sort auf den in Abbildung 22.8 gezeigten gerichteten azyklischen Graphen anwenden. Gehen Sie dabei von den gleichen Annahmen wie in Übung 22.3-2 aus. 22.4-2 Geben Sie einen Algorithmus mit linearer Laufzeit an, dessen Eingabe ein gerichteter azyklischer Graph G = (V, E) und zwei Knoten s und t sind und die Anzahl der einfachen Pfade von s nach t in G berechnet. Beispielsweise enthält der gerichtete azyklische Graph aus Abbildung 22.8 genau vier einfache Pfade von Knoten p nach Knoten v: p o v, p o r y v, p o s r y v und p s r y v. (Ihr Algorithmus soll die Pfade nur zählen, nicht ausgeben.) 22.4-3 Geben Sie einen Algorithmus an, der bestimmt, ob ein ungerichteter Graph G = (V, E) einen einfachen Zyklus enthält. Ihr Algorithmus sollte unabhängig von |E| in Zeit O(V ) laufen. 22.4-4 Beweisen oder widerlegen Sie die folgende Behauptung: Falls ein gerichteter Graph G Zyklen enthält, dann erzeugt Topological-Sort(G) eine Knotenanordnung, die die Anzahl „schlechter“ Kanten, d. h. Kanten, die mit der erzeugten Ordnung inkonsistent sind, minimiert. 22.4-5 Eine andere Möglichkeit, auf einem gerichteten azyklischen Graphen G = (V, E) eine topologische Sortierung durchzuführen, besteht darin, iterativ einen Knoten vom Eingangsgrad 0 zu bestimmen und diesen sowie alle aus ihm austretenden Kanten aus dem Graphen zu entfernen. Erklären Sie, wie wir diese Idee so implementieren können, dass die Laufzeit O(V + E) ist. Wie verhält sich dieser Algorithmus, wenn G Zyklen enthält?
626
22.5
22 Elementare Graphenalgorithmen
Starke Zusammenhangskomponenten
Wir betrachten nun eine klassische Anwendung der Tiefensuche: die Zerlegung eines gerichteten Graphen in seine starken Zusammenhangskomponenten. Dieser Abschnitt zeigt, wie wir diese Zerlegung mithilfe von zwei Tiefensuchen berechnen können. Viele Algorithmen, die auf gerichteten Graphen arbeiten, zerlegen zuerst den Graphen in seine starken Zusammenhangskomponenten. Nach der Zerlegung des Graphen in seine starken Zusammenhangskomponenten, arbeiten die Algorithmen auf jeder der starken Zusammenhangskomponenten separat und vereinen dann die Lösungen entsprechend der Verbindungsstruktur der Komponenten untereinander. Rufen Sie sich aus Anhang B in Erinnerung, dass eine starke Zusammenhangskomponente eines gerichteten Graphen G = (V, E) eine maximale Menge C ⊆ V von Knoten ist, für die für jedes Paar u, v aus C sowohl u ; v als auch v ; u gilt, d. h. die Knoten u und v sind von dem jeweils anderen aus erreichbar. Abbildung 22.9 zeigt ein Beispiel. Unser Algorithmus zur Bestimmung starker Zusammenhangskomponenten eines Graphen G = (V, E) verwendet den zu G transponierten Graphen, den wir in Übung 22.1-3 als den Graphen GT = (V, E T ) mit E T = {(u, v) : (v, u) ∈ E} definiert haben. Die Kantenmenge E T besteht also aus den Kanten von G, aber jeweils entgegengesetzt gerichtet. Ist eine Adjazenzlisten-Darstellung von G gegeben, so ist die Zeit zum Erzeugen von GT in O(V + E). Interessanterweise haben G und GT die gleichen starken Zusammenhangskomponenten: u und v sind genau dann in G untereinander erreichbar, wenn sie in GT untereinander erreichbar sind. Abbildung 22.9(b) zeigt den transponierten Graphen des Graphen aus Abbildung 22.9(a), wobei die starken Zusammenhangskomponenten jeweils schattiert dargestellt sind. Der folgende Algorithmus, der lineare Laufzeit hat, (d. h. eine Laufzeit Θ(V + E)) ermittelt die starken Zusammenhangskomponenten des gerichteten Graphen G = (V, E) durch zwei Tiefensuchen, einer auf G und einer auf GT . Strongly-Connected-Components(G) 1 rufe DFS(G) auf, um die Endzeitpunkte u.f aller Knoten zu berechnen 2 berechne GT 3 rufe DFS(GT ) auf, betrachte jedoch in der Hauptschleife von DFS die Knoten in der Reihenfolge fallender u.f (wie sie in Zeile 1 berechnet wurden) 4 gib die Knoten jedes Baumes des in Zeile 3 gebildeten Tiefensuchwaldes als separate starke Zusammenhangskomponente aus Die Idee hinter diesem Algorithmus leitet sich aus einer wesentlichen Eigenschaft des Komponentengraphen GSCC = (V SCC , E SCC ) ab, der wie folgt definiert ist. Nehmen Sie an, G würde die starken Zusammenhangskomponenten C1 , C2 , . . . , Ck haben. Dann ist die Knotenmenge V SCC des Komponentengraphen gegeben durch {v1 , v2 , . . . , vk } und enthält einen Knoten vi für jede starke Zusammenhangskomponente Ci von G. Es gibt eine Kante (vi , vj ) ∈ E SCC , falls G für ein x ∈ Ci und ein y ∈ Cj eine gerichtete Kante (x, y) enthält. Anders formuliert: Der Graph GSCC entsteht, indem alle Kanten, deren zwei inzidenten Knoten zu derselben starken Zusammenhangskomponente von
22.5 Starke Zusammenhangskomponenten
627
a 13/14
b 11/16
c 1/10
d 8/9
12/15 e
3/4 f
2/7 g
5/6 h
a
b
c
d
e
f
g
h
(a)
(b)
cd (c)
abe fg
h
Abbildung 22.9: (a) Ein gerichteter Graph G. Die schattierten Bereiche stellen die starken Zusammenhangskomponenten von G dar. Jeder Knoten ist mit seinen während einer Tiefensuche ermittelten Entdeckungs- und Endzeitpunkten markiert; Baumkanten sind schattiert eingezeichnet. (b) Der zu G transponierte Graph GT . Gezeigt ist der Tiefensuchwald, der in Zeile 3 von Strongly-Connected-Components berechnet wird; Baumkanten sind schattiert dargestellt. Jede starke Zusammenhangskomponente entspricht einem Tiefensuchbaum. Die stark schattierten Knoten b, c, g und h sind die Wurzeln der Tiefensuchbäume, die durch die Tiefensuche auf GT erzeugt werden. (c) Der azyklische Komponentengraph GSCC , den wir enthalten, wenn wir alle Kanten innerhalb jeder starken Zusammenhangskomponente von G kontrahieren, sodass jede Komponente zu einem einzigen Knoten zusammenschrumpft.
G gehören, kontrahiert werden. Abbildung 22.9(c) zeigt den Komponentengraph des Graphen aus Abbildung 22.9(a). Die wesentliche Eigenschaft ist, dass, wie das folgende Lemma zeigt, der Komponentengraph ein gerichteter azyklischer Graph ist. Lemma 22.13 Seien C und C zwei voneinander verschiedene starke Zusammenhangskomponenten eines gerichteten Graphen G = (V, E), u, v ∈ C und u , v ∈ C . Falls G einen Pfad u ; u enthält, kann G keinen Pfad v ; v enthalten. Beweis:
Falls G einen Pfad v ; v in G enthalten würde, dann würde er Pfade
628
22 Elementare Graphenalgorithmen
u ; u ; v und v ; v ; u enthalten. Das bedeutet, dass u und v jeweils voneinander erreichbar wären, was der Voraussetzung widersprechen würde, dass C und C zwei voneinander verschiedene starke Zusammenhangskomponenten sind. Wir werden sehen, dass, wenn wir die Knoten bei der zweiten Tiefensuche in der umgekehrten Reihenfolge nach ihren bei der ersten Tiefensuche berechneten Endzeitpunkten betrachten, wir die Knoten des Komponentengraphen (von denen jeder einer starken Zusammenhangskomponente von G entspricht) in topologisch sortierter Reihenfolge besuchen. Da die Prozedur Strongly-Connected-Components zwei Tiefensuchen ausführt, besteht die Gefahr einer Mehrdeutigkeit, wenn wir von u.d oder u.f sprechen. In diesem Abschnitt beziehen sich diese Werte immer auf die beim ersten Aufruf von DFS (in Zeile 1) berechneten Zeitpunkte. Wir erweitern die Begriffe der Entdeckungs- und Endzeitpunkte auf Mengen von Knoten. Für U ⊆ V definieren wir d(U ) = minu∈U {u.d } und f (U ) = maxu∈U {u.f }. Das heißt, d(U ) und f (U ) stehen für den frühesten Entdeckungszeitpunkt beziehungsweise den spätesten Endzeitpunkt aller Knoten aus U . Das folgende Lemma und das zugehörige Korollar formulieren einen wichtigen Zusammenhang zwischen starken Zusammenhangskomponenten und Endzeitpunkten bei der Tiefensuche. Lemma 22.14 Seien C und C zwei voneinander verschiedene starke Zusammenhangskomponenten eines gerichteten Graphen G = (V, E). Falls eine Kante (u, v) ∈ E mit u ∈ C und v ∈ C existiert, gilt f (C) > f (C ). Beweis: Wir betrachten zwei Fälle, in Abhängigkeit davon, welche der beiden starken Zusammenhangskomponenten, C oder C , den Knoten enthält, der während der Tiefensuche als erster entdeckt wird. Im Falle d(C) < d(C ) sei x der erste entdeckte Knoten aus C. Zum Zeitpunkt x.d sind alle Knoten in C und C weiß. Zu diesem Zeitpunkt enthält G jeweils ein Pfad von x zu jedem Knoten aus C, der nur aus weißen Knoten besteht. Wegen der Kante (u, v) ∈ E gibt es für jeden Knoten w ∈ C zum Zeitpunkt x.d auch einen Pfad in G von x nach w, der nur aus weißen Knoten besteht, nämlich x ; u → v ; w. Nach dem Theorem der weißen Pfade werden alle Knoten von C und C im Tiefensuchbaum Nachfahren von x. Nach Korollar 22.8 gilt somit f (C) = x.f > f (C ). Gilt dagegen d(C) > d(C ), dann sei y der erste entdeckte Knoten in C . Zum Zeitpunkt y.d sind alle Knoten in C weiß und G enthält einen Pfad von y zu jedem Knoten von C , der nur aus weißen Knoten besteht. Wegen des Theorems der weißen Pfade werden alle Knoten aus C bei einer Tiefensuche Nachfahren von y, sodass nach Korollar 22.8 y.f = f (C ) gilt. Zum Zeitpunkt y.d sind alle Knoten aus C weiß. Da es eine Kante (u, v) von C nach C gibt, folgt aus Lemma 22.13, dass es keinen Pfad von C nach C
22.5 Starke Zusammenhangskomponenten
629
geben kann. Somit ist kein Knoten aus C von y aus erreichbar. Zum Zeitpunkt y.f sind deshalb immer noch alle Knoten aus C weiß. Also gilt für alle Knoten w ∈ C die Ungleichung w.f > y.f und damit f (C) > f (C ). Das folgende Korollar besagt, dass jede Kante von GT , die zwischen voneinander verschiedenen starken Zusammenhangskomponenten verläuft, von der Komponente mit dem früheren Endzeitpunkt (der Tiefensuche) ausgeht. Korollar 22.15 Seien C und C zwei voneinander verschiedene starke Zusammenhangskomponenten in einem gerichteten Graphen G = (V, E). Nehmen Sie an, es würde eine Kante (u, v) ∈ E T mit u ∈ C und v ∈ C existieren. Dann gilt f (C) < f (C ). Beweis: Wegen (u, v) ∈ E T gilt (v, u) ∈ E. Da die starken Zusammenhangskomponenten von G die gleichen sind wie die von GT , folgt aus Lemma 22.14 f (C) < f (C ). Korollar 22.15 liefert den Schlüssel, um zu verstehen, warum unser Algorithmus zur Berechnung der starken Zusammenhangskomponenten korrekt arbeitet. Lassen Sie uns untersuchen, was bei der zweiten Tiefensuche passiert, die auf GT angewendet wird. Wir beginnen mit der starken Zusammenhangskomponente C, deren Endzeitpunkt f (C) der maximale ist. Die Suche startet von einem Knoten x ∈ C und besucht alle Knoten von C. Nach Korollar 22.15 enthält GT keine Kanten, die von C zu einer anderen starken Zusammenhangskomponente verlaufen. Daher wird die von x startende Suche keine Knoten aus anderen Komponenten besuchen. Der von x ausgehende Baum enthält also genau die Knoten von C. Wenn alle Knoten von C besucht wurden, wählt der Algorithmus in Zeile 3 als Wurzel einen Knoten aus einer anderen starken Zusammenhangskomponente f (C ), deren Endzeitpunkt der maximale von allen Komponenten außer C ist. Wiederum wird die Suche alle Knoten von C besuchen, aber nach Korollar 22.15 sind die einzigen Kanten von GT , die zu einer anderen Komponente verlaufen, jene, die nach C gehen. Diese Komponente wurde bereits besucht. Allgemein gilt: Wenn die Tiefensuche auf GT in Zeile 3 eine starke Zusammenhangskomponente besucht, dann enden alle Kanten, die aus dieser Komponente herausführen, bei Komponenten, die die Tiefensuche bereits besucht hat. Jeder Tiefensuchbaum wird daher genau eine starke Zusammenhangskomponente darstellen. Das folgende Theorem formalisiert dieses Argument. Theorem 22.16 Die Prozedur Strongly-Connected-Components(G) bestimmt die starken Zusammenhangskomponenten von dem eingegebenen gerichteten Graphen korrekt. Beweis: Wir führen den Beweis durch Induktion über die Anzahl der Tiefensuchbäume, die bei der Tiefensuche auf GT in Zeile 3 gefunden werden, und zeigen, dass jeder
630
22 Elementare Graphenalgorithmen
Baum eine starke Zusammenhangskomponente darstellt. Die Induktionsannahme ist, dass die ersten k Bäume, die in Zeile 3 erzeugt werden, starke Zusammenhangskomponenten sind. Der Induktionsanfang mit k = 0 ist trivial. Im Induktionsschritt setzen wir voraus, dass jeder der ersten k in Zeile 3 erzeugten Tiefensuchbäume eine starke Zusammenhangskomponente ist, und betrachten den (k + 1)-ten erzeugten Baum. Die Wurzel dieses Baumes sei der Knoten u, der in der starken Zusammenhangskomponente C liegt. Aufgrund der Art und Weise, in der wir bei der Tiefensuche in Zeile 3 die Wurzeln wählen, gilt u.f = f (C) > f (C ) für jede starke Zusammenhangskomponente C außer C, die bereits besucht wurde. Wegen der Induktionsannahme sind zu dem Zeitpunkt, zu dem die Suche den Knoten u besucht, alle anderen Knoten von C weiß. Nach dem Theorem der weißen Pfade sind daher alle anderen Knoten von C Nachfahren von u in dessen Tiefensuchbaum. Außerdem müssen wegen der Induktionsannahme und Korollar 22.15 alle Kanten von GT , die aus C herausführen, zu starken Zusammenhangskomponenten führen, die bereits besucht wurden. Also wird kein Knoten einer starken Zusammenhangskomponente außer C bei der Tiefensuche auf GT ein Nachfahre von u. Die Knoten des Tiefensuchbaums von GT , der von u ausgeht, bilden folglich genau eine starke Zusammenhangskomponente, was den Induktionsschritt und damit den Beweis vervollständigt. Eine andere Möglichkeit, auf die Arbeitsweise der zweiten Tiefensuche zu schauen, ist die folgende. Betrachten Sie den Komponentengraphen (GT )SCC von GT . Wenn wir jede starke Zusammenhangskomponente, die während der zweiten Tiefensuche besucht wird, auf einen Knoten von (GT )SCC abbilden, werden die Knoten von (GT )SCC in umgekehrter topologisch sortierter Reihenfolge besucht. Wenn wir die Kanten von (GT )SCC umkehren, erhalten wir den Graphen ((GT )SCC )T . Wegen ((GT )SCC )T = GSCC (siehe Übung 22.5-4) besucht die zweite Tiefensuche die Knoten von GSCC in topologisch sortierter Reihenfolge.
Übungen 22.5-1 Wie kann sich die Anzahl starker Zusammenhangskomponenten eines Graphen ändern, wenn eine neue Kante hinzugefügt wird? 22.5-2 Beschreiben Sie, wie die Prozedur Strongly-Connected-Components auf dem Graphen aus Abbildung 22.6 arbeitet. Geben Sie insbesondere die in Zeile 1 bestimmten Endzeitpunkte und den in Zeile 3 erzeugten Wald an. Setzen Sie voraus, dass die Schleife der Zeilen 5–7 von DFS die Knoten in alphabetischer Reihenfolge bearbeitet und dass die Adjazenzlisten alphabetisch geordnet sind. 22.5-3 Professor Bacon behauptet, dass der Algorithmus für starke Zusammenhangskomponenten einfacher wäre, wenn er bei der zweiten Tiefensuche den ursprünglichen Graphen (statt den transponierten Graphen) verwendet und die Knoten in aufsteigender Reihenfolge nach den Endzeitpunkten sondiert. Berechnet dieser einfachere Algorithmus immer ein korrektes Ergebnis?
Problemstellungen zu Kapitel 22
631
22.5-4 Beweisen Sie, dass für gerichtete Graphen G die Gleichung ((GT )SCC )T = GSCC gilt. Das heißt, der zum Komponentengraphen GT transponierte Graph ist der gleiche Graph wie der Komponentengraph von G. 22.5-5 Geben Sie einen Algorithmus mit Laufzeit O(V + E) an, der den Komponentengraphen eines gerichteten Graphen G = (V, E) bestimmt. Stellen Sie sicher, dass es in dem von Ihrem Algorithmus erzeugten Komponentengraphen höchstens eine Kante zwischen zwei Knoten gibt. 22.5-6 Gegeben sei ein gerichteter Graph G = (V, E). Erklären Sie, wie wir einen zweiten Graphen G = (V, E ) so erzeugen können, dass´ (a) G die gleichen starken Zusammenhangskomponenten wie G hat, (b) G den gleichen Komponentengraph wie G hat und (c) E so klein wie möglich ist. Geben Sie einen schnellen Algorithmus zur Bestimmung von G an. 22.5-7 Ein gerichteter Graph G = (V, E) wird als halbzusammenhängend bezeichnet, falls für alle Knotenpaare u, v ∈ V u ; v oder v ; u gilt. Geben Sie einen effizienten Algorithmus an, der bestimmt, ob G halbzusammenhängend ist oder nicht. Beweisen Sie, dass Ihr Algorithmus korrekt ist und analysieren Sie seine Laufzeit.
Problemstellungen 22-1 Klassifizierung der Kanten mittels Breitensuche Ein Tiefensuchwald klassifiziert die Kanten eines Graphen in Baum-, Rückwärts-, Vorwärts- und Querkanten. Ein Breitensuchbaum kann ebenfalls verwendet werden, um die von einem Startknoten aus erreichbaren Knoten jeweils einer dieser vier Kategorien zuzuordnen. a. Beweisen Sie, dass bei einer Breitensuche auf einem ungerichteten Graphen die folgenden Eigenschaften gelten: 1. Es gibt keine Rückwärtskanten und keine Vorwärtskanten. 2. Für jede Baumkante (u, v) gilt v.d = u.d + 1. 3. Für jede Querkante (u, v) gilt v.d = u.d oder v.d = u.d + 1. b. Beweisen Sie, dass bei einer Breitensuche auf einem gerichteten Graphen die folgenden Eigenschaften gelten: 1. 2. 3. 4.
Es gibt keine Vorwärtskanten. Für jede Baumkante (u, v) gilt v.d = u.d + 1. Für jede Querkante (u, v) gilt v.d ≤ u.d + 1. Für jede Rückwärtskante (u, v) gilt 0 ≤ v.d ≤ u.d .
22-2 Verbindungspunkte, Brücken und Zweifach-Zusammenhangskomponenten Sei G = (V, E) ein zusammenhängender ungerichteter Graph. Ein Verbindungspunkt von G ist ein Knoten, dessen Entfernen dazu führt, dass der Graph nicht
632
22 Elementare Graphenalgorithmen
2 1
6
4 3 5
Abbildung 22.10: Die Verbindungspunkte, die Brücken und die Zweifach-Zusammenhangskomponenten eines zusammenhängenden ungerichteten Graphen, der in Problemstellung 22-2 verwendet wird. Die Verbindungspunkte sind die stark schattierten Knoten, die Brücken sind die stark schattierten Kanten und die Zweifach-Zusammenhangskomponenten sind jeweils die Kanten innerhalb der schattierten Bereiche, denen jeweils ein zzk -Wert zugeordnet ist.
mehr zusammenhängend ist. Eine Brücke von G ist eine Kante, deren Entfernen dazu führt, dass der Graph nicht mehr zusammenhängend ist. Eine ZweifachZusammenhangskomponente ist eine maximale Menge von Kanten mit der Eigenschaft, dass je zwei Kanten aus der Menge auf einem gemeinsamen einfachen Zyklus liegen. Abbildung 22.10 illustriert diese Definitionen. Wir können Verbindungspunkte, Brücken und Zweifach-Zusammenhangskomponenten mithilfe einer Tiefensuche bestimmen. Sei Gπ = (V, Eπ ) ein Tiefenbaum von G. a. Beweisen Sie, dass die Wurzel von Gπ genau dann ein Verbindungspunkt von G ist, wenn sie mindestens zwei Kinder in Gπ hat. b. Sei v ein von der Wurzel verschiedener Knoten von Gπ . Beweisen Sie, dass v genau dann ein Verbindungspunkt von G ist, wenn v ein Kind s mit der Eigenschaft hat, dass es keine Rückwärtskante von s oder einem Nachfahren von s zu einem echten Vorfahren von v gibt. c. Es sei ⎧ ⎨ v.d , v.low = min w.d : (u, w) ist eine Rückwärtskante ⎩ für einen Nachfahren u von v . Zeigen Sie, wie v.low für alle Knoten v ∈ V in Zeit O(E) berechnet werden kann. d. Zeigen Sie, wie alle Verbindungspunkte in Zeit O(E) berechnet werden können. e. Beweisen Sie, dass eine Kante von G genau dann eine Brücke ist, wenn sie nicht auf einem einfachen Zyklus von G liegt. f. Zeigen Sie, wie wir alle Brücken von G in Zeit O(E) berechnen können.
Kapitelbemerkungen zu Kapitel 22
633
g. Beweisen Sie, dass die Zweifach-Zusammenhangskomponenten von G die Nicht-Brückenkanten von G partitionieren. h. Geben Sie einen Algorithmus mit Laufzeit O(E) an, der jede Kante e von G so mit einer positiven ganzen Zahl e.zkk kennzeichnet, dass genau dann e.zzk = e .zzk gilt, wenn e und e in der gleichen Zweifach-Zusammenhangskomponente liegen. 22-3 Euler-Zug Ein Euler-Zug in einem stark zusammenhängenden gerichteten Graphen G = (V, E) ist ein Zyklus, der jede Kante von G genau einmal traversiert, wobei jeder Knoten mehrmals besucht werden kann. a. Zeigen Sie, dass G genau dann einen Euler-Zug enthält, wenn für jeden Knoten v ∈ V die Anzahl der in v eingehenden Kanten gleich der Anzahl der aus v austretenden Kanten ist, d. h. Eingangsgrad(v) = Ausgangsgrad(v) gilt. b. Geben Sie einen Algorithmus mit Laufzeit O(E) an, der einen Euler-Zug von G bestimmt, falls ein solcher existiert. (Hinweis: Verschmelzen Sie kantendisjunkte Zyklen.) 22-4 Erreichbarkeit Sei G = (V, E) ein gerichteter Graph, in dem jeder Knoten u ∈ V mit einer eindeutigen ganzen Zahl L(u) aus der Menge {1, 2, . . . , |V |} markiert ist. Für jeden Knoten u ∈ V sei R(u) = {v ∈ V : u ; v} die Menge der Knoten, die von u aus erreichbar sind. Wir definieren min(u) als denjenigen Knoten aus R(u), dessen Marke minimal ist. Das heißt, min(u) ist der Knoten v aus R(u), für den L(v) = min {L(w) : w ∈ R(u)} gilt. Geben Sie einen Algorithmus mit Laufzeit O(V + E) an, der min(u) für alle Knoten u ∈ V berechnet.
Kapitelbemerkungen Even [103] und Tarjan [330] sind exzellente Referenzen zu Graphenalgorithmen. Breitensuche wurde von Moore [260] im Zusammenhang mit dem Auffinden von Wegen durch Labyrinthe entwickelt. Lee [226] entdeckte unabhängig den gleichen Algorithmus im Zusammenhang mit der Planung der Leitungsführung auf Leiterplatten. Hopcroft und Tarjan [178] empfahlen die Verwendung der Adjazenzlisten-Darstellung gegenüber der Adjazenzmatrix-Darstellung für dünn besetzte Graphen. Sie waren die ersten, die die algorithmische Bedeutung der Tiefensuche erkannten. Die Verwendung der Tiefensuche ist seit den späten 1950er Jahren weit verbreitet, insbesondere im Zusammenhang mit künstlicher Intelligenz. Tarjan [327] hat einen Algorithmus mit linearer Laufzeit für die Bestimmung starker Zusammenhangskomponenten angegeben. Der Algorithmus für starke Zusammenhangskomponenten in Abschnitt 22.5 wurde von Aho, Hopcroft und Ullman [6] übernommen, die ihn S. R. Kosaraju (unveröffentlicht) und M. Sharir [314] zuschreiben. Gabow [119]
634
22 Elementare Graphenalgorithmen
hat ebenfalls einen Algorithmus für starke Zusammenhangskomponenten entwickelt, der auf kontrahierenden Zyklen basiert und zwei Stapel verwendet, um eine lineare Laufzeit zu erhalten. Knuth [209] war der erste, der einen Algorithmus mit linearer Laufzeit für das topologische Sortieren angegeben hat.
23
Minimale Spannbäume
Entwürfe elektronischer Schaltkreise müssen häufig die Anschlüsse mehrerer Komponenten auf das gleiche Potential legen, indem sie diese Anschlüsse miteinander verbinden. Um eine Menge von n Anschlüssen miteinander zu verbinden, können wir n − 1 Leitungen benutzen, die jeweils zwei Anschlüsse verbinden. Von allen möglichen Anordnungen ist gewöhnlich diejenige zu bevorzugen, bei der die Gesamtlänge der Leitungen minimal ist. Wir können dieses Verdrahtungsproblem durch einen zusammenhängenden ungerichteten Graphen G = (V, E) modellieren, wobei V die Menge der Anschlüsse und E die Menge der möglichen Verbindungen zwischen allen Paaren von Anschlüssen repräsentiert. Für jede Kante (u, v) ∈ E ist ein Gewicht w(u, v) gegeben, das die Kosten (die benötigte Leitungslänge) für die Verbindung zwischen u und v angibt. Wir wollen eine azyklische Teilmenge T ⊆ E bestimmen, die alle Knoten verbindet und deren Gesamtgewicht w(T ) = w(u, v) (u,v)∈T
minimal ist. Da die Kantenmenge T azyklisch ist und alle Knoten verbindet, muss sie einen Baum darstellen. Wir nennen einen solchen Baum einen aufspannenden Baum, oder einfach nur Spannbaum, da er den Graphen G „aufspannt“. Wir bezeichnen das Problem, einen solchen Baum T zu finden, als das minimaler-SpannbaumProblem 1 . Abbildung 23.1 zeigt ein Beispiel für einen zusammenhängenden Graphen und einen minimalen Spannbaum. In diesem Kapitel werden wir zwei Algorithmen untersuchen, die das Problem der Berechnung minimaler Spannbäume löst: den Algorithmus von Prim und den Algorithmus von Kruskal. Wir können leicht jeden der beiden so implementieren, dass er in Zeit O(E lg V ) läuft, indem wir gewöhnliche binäre Heaps verwenden. Verwenden wir Fibonacci-Heaps, so läuft der Algorithmus von Prim in Zeit O(E + V lg V ), was gegenüber der Implementierung mit binären Heaps eine Verbesserung darstellt, wenn |V | wesentlich kleiner als |E| ist. Beide Algorithmen sind Greedy-Algorithmen, wie sie in Kapitel 16 beschrieben wurden. In jedem Schritt eines Greedy-Algorithmus muss eine von mehreren Möglichkeiten ausgewählt werden. Die Greedy-Strategie besteht darin, dass immer die Möglichkeit gewählt wird, die in dem Moment die beste zu sein scheint. Eine solche Strategie garantiert im Allgemeinen nicht, dass sie global optimale Lösungen findet. Für das Problem 1 Die Bezeichnung „minimaler Spannbaum“ ist eine Abkürzung für „Spannbaum mit minimalem Gewicht“. Wir minimieren nicht die Anzahl der Kanten von T , da alle Spannbäume nach Theorem B.2 genau |V | − 1 Kanten haben.
636
23 Minimale Spannbäume
a
8
b
4
11
i 7
8 h
7
c 2
4
d 14
6 1
9 e 10
g
2
f
Abbildung 23.1: Ein minimaler Spannbaum für einen zusammenhängenden Graphen. Neben jeder Kante ist deren Gewicht angegeben; die schattierten Kanten stellen einen minimalen Spannbaum dar. Das Gesamtgewicht des gezeigten Baumes ist 37. Der minimale Spannbaum dieses Graphen ist nicht eindeutig: Ersetzen wir die Kante (b, c) durch die Kante (a, h), so erhalten wir einen anderen Spannbaum, der ebenfalls das Gewicht 37 hat.
der Berechnung minimaler Spannbäume können wir jedoch beweisen, dass bestimmte Greedy-Strategien einen Spannbaum mit minimalem Gewicht liefern. Die in diesem Kapitel vorgestellten Methoden sind eine klassische Anwendung der in Kapitel 16 eingeführten theoretischen Konzepte. Abschnitt 23.1 führt eine „generische“ Methode zur Berechnung minimaler Spannbäume ein, die einen Spannbaum aufbaut, indem in jedem Schritt eine Kante hinzugefügt wird. Abschnitt 23.2 stellt zwei Algorithmen vor, die die generische Methode implementieren. Der erste Algorithmus, der auf Kruskal zurückgeht, ähnelt dem Algorithmus für die Bestimmung von Zusammenhangskomponenten aus Abschnitt 21.1. Der zweite Algorithmus, der von Prim entwickelt wurde, ähnelt Dijkstras Algorithmus für das Problem der Berechnung kürzester Pfade (siehe Abschnitt 24.3). Da ein Baum ein Graph ist, dürfen wir einen Baum nicht nur über seine Kanten definieren, sondern müssen auch angeben, welche Knoten er enthält, um formal exakt vorzugehen. Wenngleich dieses Kapitel Bäume nur über ihre Kanten sieht, sollten wir im Hinterkopf haben, dass die Knoten eines Baumes T genau die sind, die zu einer Kante aus T inzident sind.
23.1
Aufbau eines minimalen Spannbaums
Nehmen Sie an, wir hätten einen zusammenhängenden gerichteten Baum G = (V, E) mit einer Gewichtsfunktion w : E → R gegeben und wir würden gerne einen minimalen Spannbaum von G bestimmen. Die zwei Algorithmen, die wir in diesem Kapitel betrachten, basieren beide auf einem Greedy-Ansatz, unterscheiden sich jedoch in der Art, wie sie diesen Ansatz anwenden. Diese Greedy-Strategie ist in der folgenden „generischen“ Methode umgesetzt, die den minimalen Spannbaum Kante für Kante aufbaut. Die generische Methode verwaltet eine Kantenmenge A und erhält dabei folgende Schleifeninvariante: Vor jeder Iteration ist A eine Teilmenge eines minimalen Spannbaums. In jedem Schritt bestimmen wir eine Kante (u, v), die wir zu A hinzufügen können, ohne
23.1 Aufbau eines minimalen Spannbaums
637
die Invariante zu verletzen. Das bedeutet, dass A∪{(u, v)} ebenfalls eine Teilmenge eines minimalen Spannbaums ist. Wir bezeichnen eine solche Kante als sichere Kante für A, da wir sie mit Sicherheit zu A hinzufügen können, ohne die Schleifeninvariante zu verletzen. Generic-MST(G, w) 1 A=∅ 2 while A ist kein Spannbaum 3 finde eine sichere Kante (u, v) für A 4 A = A ∪ {(u, v)} 5 return A Wir beweisen die Schleifeninvariante wie folgt: Initialisierung: Nach Zeile 1 erfüllt die Menge A trivialerweise die Schleifeninvariante. Fortsetzung: Die Schleife in den Zeilen 2–4 erhält die Schleifeninvariante, da nur sichere Kanten hinzugefügt werden. Terminierung: Alle Kanten, die zu A hinzugefügt wurden, befinden sich in einem minimalen Spannbaum. Daher ist die Menge A, die in Zeile 5 zurückgegeben wird, ein minimaler Spannbaum. Der knifflige Teil ist natürlich die Bestimmung einer sicheren Kante in Zeile 3. Eine solche muss existieren, denn wenn Zeile 3 ausgeführt wird, ist durch die Schleifeninvariante festgelegt, dass es einen Spannbaum T mit A ⊆ T gibt. Im Rumpf der while-Schleife muss A eine echte Teilmenge von T sein, sodass es eine Kante (u, v) ∈ T geben muss, die sicher für A ist und nicht zu A gehört. Im Rest dieses Abschnitts beschäftigen wir uns mit einer Regel (Theorem 23.1), mit der sichere Kanten erkannt werden können. Der nächste Abschnitt beschreibt dann zwei Algorithmen, die mithilfe dieser Regel sichere Kanten effizient bestimmen. Zunächst benötigen wir einige Definitionen. Ein Schnitt (S, V − S) eines ungerichteten Graphen G = (V, E) ist eine Partitionierung von V . Abbildung 23.2 illustriert diesen Begriff. Wir sagen, dass eine Kante (u, v) ∈ E den Schnitt (S, V − S) kreuzt, falls einer ihrer Endpunkte in S und der andere in V − S liegt; er respektiert eine Kantenmenge A, falls keine Kante von A den Schnitt kreuzt. Eine Kante ist eine leichte, den Schnitt kreuzende Kante, falls sie das kleinste Gewicht aller Kanten hat, die den Schnitt kreuzen. Beachten Sie, dass es mehr als eine leichte, den Schnitt kreuzende Kante geben kann. Allgemein nennen wir eine Kante leichte Kante, die eine gegebene Eigenschaft erfüllt, falls sie das kleinste Gewicht aller Kanten mit dieser Eigenschaft hat. Unsere Regel, mit der wir sichere Kanten erkennen können, ist durch das folgende Theorem gegeben.
638
23 Minimale Spannbäume h
8 a 4
7 11
i 6
b 2
4 S V–S
a
8
b 11
i 7
8 h
2
d
d 9
14
4
6 1
7
c
e 10
g (a)
2
f
S V–S
g
8 7
9 e
1
14 10
c
2
4 f
S V–S (b)
Abbildung 23.2: Zwei Möglichkeiten, einen Schnitt (S, V −S) des Graphen aus Abbildung 23.1 zu betrachten. (a) Schwarze Knoten sind in der Menge S und weiße Knoten sind in der Menge V − S. Die Kanten, die den Schnitt kreuzen, sind diejenigen, die weiße Knoten mit schwarzen Knoten verbinden. Die Kante (d, c) ist die einzige leichte, den Schnitt kreuzende Kante. Eine Teilmenge A der Kanten ist schattiert dargestellt. Beachten Sie, dass der Schnitt (S, V − S) die Kantenmenge A respektiert, denn keine Kante von A kreuzt den Schnitt. (b) Der gleiche Graph, wobei die Knoten der Menge S auf der linken und die Knoten der Menge V − S auf der rechten Seite liegen. Eine Kante kreuzt den Schnitt, wenn sie einen Knoten auf der linken Seite mit einem Knoten auf der rechten Seite verbindet.
Theorem 23.1 Sei G = (V, E) ein zusammenhängender ungerichteter Graph, für den eine reellwertige Gewichtsfunktion auf E definiert ist. Seien A eine Teilmenge von E, die in einem minimalen Spannbaum von G enthalten ist, (S, V − S) ein beliebiger Schnitt von G, der A respektiert, und (u, v) eine leichte Kante, die (S, V − S) kreuzt. Dann ist die Kante (u, v) für A sicher. Beweis: Sei T ein minimaler Spannbaum, der A enthält. Wir nehmen an, dass T die leichte Kante (u, v) nicht enthält, denn wenn dies der Fall wäre, wären wir mit dem Beweis fertig. Über ein Austauschargument konstruieren wir einen anderen minimalen Spannbaum T , der A ∪ {(u, v)} enthält, und zeigen somit, dass (u, v) eine sichere Kante für A ist. Die Kante (u, v) bildet mit den Kanten des Pfades p, der in T von u nach v führt, einen Zyklus, siehe Abbildung 23.3 zur Illustration. Da u und v auf verschiedenen Seiten des Schnittes (S, V − S) liegen, liegt mindestens eine Kante von T , die auch den Schnitt kreuzt, auf dem einfachen Pfad p. Sei (x, y) eine solche Kante. Die Kante (x, y) gehört
23.1 Aufbau eines minimalen Spannbaums
639
x p
u
y
v
Abbildung 23.3: Der Beweis des Theorems 23.1. Schwarze Knoten sind in S und weiße Knoten sind in V − S. Eingezeichnet sind die Kanten des minimalen Spannbaums T , nicht aber die Kanten des Graphen. Die Kanten von A sind schattiert dargestellt. Die Kante (u, v) ist eine leichte, den Schnitt (S, V −S) kreuzende Kante. Die Kante (x, y) ist eine Kante, die auf dem in T eindeutigen einfachen Pfad p von u nach v liegt. Um einen minimalen Spannbaum T , der (u, v) enthält, zu erhalten, entfernen wir die Kante (x, y) aus T und fügen die Kante (u, v) hinzu.
nicht zu A, da der Schnitt A respektiert. Da (x, y) auf dem einzigen einfachen Pfad liegt, der in T von u nach v führt, zerfällt T durch das Entfernen von (x, y) in zwei Komponenten. Das Hinzufügen von (u, v) verbindet diese Komponenten wieder zu einem neuen Spannbaum T = T − {(x, y)} ∪ {(u, v)}. Als nächstes zeigen wir, dass T ein minimaler Spannbaum ist. Da (u, v) eine leichte, den Schnitt (S, V − S) kreuzende Kante ist und (x, y) diesen Schnitt ebenfalls kreuzt, gilt w(u, v) ≤ w(x, y). Somit gilt w(T ) = w(T ) − w(x, y) + w(u, v) ≤ w(T ) . T ist jedoch ein minimaler Spannbaum, sodass w(T ) ≤ w(T ) gilt; also muss T ebenfalls ein minimaler Spannbaum sein. Es bleibt zu zeigen, dass (u, v) tatsächlich eine sichere Kante für A ist. Wegen A ⊆ T und (x, y) ∈ A gilt A ⊆ T und somit A∪{(u, v)} ⊆ T . Da T ein minimaler Spannbaum ist, ist folglich (u, v) eine sichere Kante für A. Theorem 23.1 gibt uns ein besseres Verständnis der Arbeitsweise der Methode GenericMST auf einem zusammenhängenden Graphen G = (V, E). Während die Methode arbeitet, bleibt die Menge A immer azyklisch; anderenfalls würde ein minimaler Spannbaum, der A umfasst, einen Zyklus enthalten. Dies wäre ein Widerspruch. Zu jedem Zeitpunkt der Ausführung der Methode ist der Graph GA = (V, A) ein Wald und jede seiner Zusammenhangskomponenten ist ein Baum. (Einige Bäume bestehen eventuell
640
23 Minimale Spannbäume
nur aus einem einzigen Knoten, zum Beispiel dann, wenn der Algorithmus startet: A ist dann leer und der Wald enthält |V | Bäume, einen für jeden Knoten.) Außerdem verbindet jede für A sichere Kante (u, v) unterschiedliche Komponenten von GA , da A ∪ {(u, v)} azyklisch sein muss. Die while-Schleife der Zeilen 2–4 der Prozedur Generic-MST wird (|V | − 1)-mal ausgeführt, da sie genau eine der |V | − 1 Kanten eines minimalen Spannbaumes in jeder Iteration bestimmt. Anfangs, wenn A leer ist, gibt es |V | Bäume in GA . Jede Iteration reduziert die Anzahl um 1. Wenn der Wald nur einen einzigen Baum enthält, terminiert die Methode. Die beiden Algorithmen, die in Abschnitt 23.2 behandelt werden, verwenden das folgende Korollar zu Theorem 23.1. Korollar 23.2 Sei G = (V, E) ein zusammenhängender ungerichteter Graph, für den eine reellwertige Gewichtsfunktion auf E definiert ist. A sei eine Teilmenge von E, die in einem minimalen Spannbaum von G enthalten ist, und C = (VC , EC ) eine Zusammenhangskomponente (ein Baum) aus dem Wald GA = (V, A). Falls (u, v) eine leichte Kante ist, die C mit einer anderen Komponente von GA verbindet, dann ist (u, v) für A sicher. Beweis: Der Schnitt (VC , V − VC ) respektiert A und (u, v) ist eine leichte Kante für diesen Schnitt. Daher ist (u, v) sicher für A.
Übungen 23.1-1 Sei (u, v) eine Kante mit minimalem Gewicht in einem Graphen G. Zeigen Sie, dass (u, v) in einem minimalen Spannbaum von G enthalten ist. 23.1-2 Professor Sabatier vermutet, dass die folgende Umkehrung von Theorem 23.1 gilt. Sei G = (V, E) ein zusammenhängender ungerichteter Graph, auf dessen Kantenmenge E eine reellwertige Gewichtsfunktion w definiert ist. Sei A eine Teilmenge von E, die zu einem minimalen Spannbaum von G gehört, (S, V −S) ein Schnitt von G, der A respektiert, und (u, v) eine sichere Kante für A, die den Schnitt (S, V −S) kreuzt. Dann ist (u, v) eine leichte Kante für den Schnitt. Zeigen Sie durch ein Gegenbeispiel, dass die Vermutung des Professors nicht korrekt ist. 23.1-3 Zeigen Sie, dass eine Kante (u, v), die zu einem minimalen Spannbaum gehört, für einen Schnitt des Graphen eine leichte Kante ist. 23.1-4 Geben Sie ein einfaches Beispiel für einen zusammenhängenden Graphen mit folgender Eigenschaft an: Die Menge aller Kanten {(u, v) : es existiert ein Schnitt (S, V − S), für den (u, v) eine leichte, den Schnitt (S, V − S) kreuzende Kante ist} bildet keinen minimalen Spannbaum.
23.2 Die Algorithmen von Kruskal und Prim
641
23.1-5 Sei e eine Kante mit maximalem Gewicht auf einem Zyklus eines zusammenhängenden Graphen G = (V, E). Beweisen Sie, dass es einen minimalen Spannbaum von G = (V, E − {e}) gibt, der gleichzeitig ein minimaler Spannbaum von G ist. Das heißt, es gibt einen minimalen Spannbaum von G, der e nicht enthält. 23.1-6 Zeigen Sie, dass ein Graph einen eindeutigen minimalen Spannbaum besitzt, falls es für jeden Schnitt des Graphen eine eindeutige leichte Kante gibt, die diesen Schnitt kreuzt. Zeigen Sie durch ein Gegenbeispiel, dass die Umkehrung dieser Aussage nicht gilt. 23.1-7 Zeigen Sie, dass, falls die Kantengewichte in einem Graphen alle positiv sind, jede Teilmenge der Kantenmenge, die alle Knoten verbindet und deren Gesamtgewicht minimal ist, ein Baum sein muss. Geben Sie ein Beispiel dafür an, dass die gleiche Schlussfolgerung nicht zutrifft, wenn wir zulassen, dass einige Gewichte nichtpositiv sind. 23.1-8 Sei T ein minimaler Spannbaum eines Graphen G und L eine sortierte Liste der Kantengewichte von T . Zeigen Sie, dass die Liste L auch die sortierte Liste der Kantengewichte jedes anderen minimalen Spannbaumes T von G ist. 23.1-9 Sei T ein minimaler Spannbaum eines Graphen G = (V, E) und V eine Teilmenge von V . Sei T der durch V induzierte Teilgraph von T und G der durch V induzierte Teilgraph von G. Zeigen Sie, dass T ein minimaler Spannbaum von G ist, falls T zusammenhängend ist. 23.1-10 Gegeben sei ein Graph G und ein minimaler Spannbaum T von G. Nehmen Sie an, wir würden das Gewicht einer der Kanten aus T reduzieren. Zeigen Sie, dass T dann immer noch ein minimaler Spannbaum von G ist. Formaler ausgedrückt, sei T ein minimaler Spannbaum von G mit Kantengewichten, die durch die Gewichtsfunktion w gegeben sind. Wir wählen eine Kante (x, y) ∈ T und eine positive Zahl k und definieren die Gewichtsfunktion w durch w(u, v) falls (u, v) = (x, y) , w (u, v) = w(x, y) − k falls (u, v) = (x, y) . Zeigen Sie, dass T ein minimaler Spannbaum von G mit den durch w gegebenen Kantengewichten ist. 23.1-11∗ Gegeben sei ein Graph G und ein minimaler Spannbaum T von G. Nehmen Sie an, wir würden das Gewicht einer der Kanten, die nicht in T enthalten ist, reduzieren. Geben Sie einen Algorithmus an, der den minimalen Spannbaum von dem modifizierten Graphen bestimmt.
23.2
Die Algorithmen von Kruskal und Prim
Die beiden in diesem Abschnitt beschriebenen Algorithmen zur Bestimmung minimaler Spannbäume sind Ausarbeitungen der generischen Methode. Beide verwenden eine
642
23 Minimale Spannbäume
bestimmte Regel, um in Zeile 3 von Generic-MST eine sichere Kante zu bestimmen. Im Algorithmus von Kruskal ist die Menge A ein Wald, dessen Knoten alle Knoten des gegebenen Graphen sind. Die zu A hinzugefügte sichere Kante ist immer eine Kante mit minimalem Gewicht, die zwei verschiedene Komponenten verbindet. Im Algorithmus von Prim bildet die Menge A einen einzigen Baum. Die zu A hinzugefügte sichere Kante ist immer eine Kante mit minimalem Gewicht, die den Baum mit einem Knoten verbindet, der nicht zum Baum gehört.
Der Algorithmus von Kruskal Der Algorithmus von Kruskal bestimmt eine sichere Kante, die dem wachsenden Wald hinzugefügt wird, indem von allen Kanten, die zwei beliebige Bäume des Waldes verbinden, eine Kante (u, v) mit dem kleinsten Gewicht ausgewählt wird. Seien C1 und C2 die beiden Bäume, die durch (u, v) verbunden sind. Da (u, v) eine leichte Kante sein muss, die C1 mit einem anderen Baum verbindet, folgt aus Korollar 23.2, dass (u, v) eine sichere Kante für C1 ist. Kruskals Algorithmus ist ein Greedy-Algorithmus, da er dem Wald in jedem Schritt eine Kante hinzufügt, die das kleinste mögliche Gewicht hat. Unsere Implementierung von Kruskals Algorithmus ähnelt dem Algorithmus aus Abschnitt 21.1, der die Zusammenhangskomponenten bestimmt. Er verwendet eine Datenstruktur disjunkter Mengen, um die verschiedenen disjunkten Mengen von Elementen zu verwalten. Jede Menge enthält die Knoten aus einem Baum des aktuellen Waldes. Die Operation Find-Set(u) gibt einen Repräsentanten der Menge, die u enthält, zurück. Damit können wir feststellen, ob zwei Knoten u und v zum gleichen Baum gehören, indem wir überprüfen, ob Find-Set(u) gleich Find-Set(v) ist. Um Bäume zu verschmelzen, ruft Kruskals Algorithmus die Prozedur Union auf. MST-Kruskal(G, w) 1 A=∅ 2 for jeden Knoten v ∈ G.V 3 Make-Set(v) 4 sortiere die Kanten aus G.E in nichtfallender Reihenfolge nach dem Gewicht w 5 for jede Kante (u, v) ∈ G.E , in nichtfallender Reihenfolge nach ihren Gewichten 6 if Find-Set(u) = Find-Set(v) 7 A = A ∪ {(u, v)} 8 Union(u, v) 9 return A Abbildung 23.4 zeigt, wie Kruskals Algorithmus arbeitet. Die Zeilen 1–3 initialisieren die Menge A mit der leeren Menge und erzeugen |V | Bäume, die jeweils einen Knoten enthalten. Die for-Schleife der Zeilen 5–8 durchläuft die Kanten in nichtfallender Reihenfolge nach ihren Gewichten. Sie prüft für jede Kante, ob die Endpunkte u und v zum gleichen Baum gehören. Wenn dies der Fall ist, dann kann die Kante (u, v) nicht zum Wald hinzugefügt werden, ohne dass ein Zyklus erzeugt wird. Die Kante wird deshalb verworfen. Wenn die beiden Knoten zu voneinander verschiedenen Bäumen gehören, fügt Zeile 7 die Kante (u, v) zu A hinzu und Zeile 8 verschmilzt die Knoten der zwei Bäume.
23.2 Die Algorithmen von Kruskal und Prim
4
8
b
7
c
d
643
4
9
8
b
a
11
i 7
8
4
h
4
e
(b)
a
2 7
c
8
f
d
11
i 7
10 g
8
b
14
6 1
9
4
h
a
11 8
4
h
4
e
(d)
a
8
b
2 7
c
8
f
d
11
a
11 8
4
h
4
9
4 e
(f)
a
h
a
11
2 7
c
8
h
9
14
e 10
g
8
b
d
11 8
f
4
9
14
6 1
d
2 7
c
f
d
9
i
4
h
4
8
e 10
g
1
b
14
6 2 7
c
f
d
9
2
i 7
4
1
2 (g)
7
f
6
7
10 g
8
b
14
6 1
e
2
i 7
2
c
i
2 (e)
14 10
g
1
7
10 g
8
b
14
6 1
9
2
i 7
4 6
2 (c)
d
2
2 (a)
7
c
e 10
g
2
f
(h)
a
11
i 7
8
h
4
14
6 1
e 10
g
2
f
Abbildung 23.4: Die Ausführung von Kruskals Algorithmus auf dem Graphen aus Abbildung 23.1. Schattiert dargestellte Kanten gehören zum Wald A, der aufgebaut wird. Der Algorithmus betrachtet die Kanten in aufsteigender Reihenfolge nach ihren Gewichten. In jedem Schritt zeigt ein Pfeil auf die Kante, die gerade verarbeitet wird. Falls die Kante zwei verschiedene Bäume des Waldes verbindet, wird sie zu dem Wald hinzugefügt, wodurch zwei Bäume verschmolzen werden.
644
23 Minimale Spannbäume
4
8
b
7
c
d
9
4
8
b
2 (i)
a
11 8
4
h
4
e
(j)
a
2 7
c
8
f
d
11
i 7
10 g
8
b
14
6 1
9
4
h
a
11 8
4
h
4
e
(l)
a
8
b
2 7
c
8
f
d
11
a
11 8
h
4
9
14
6 1
e
7
f
d
9
4
h
4
8
e 10
g
1
b
14
6 2 7
c
f
d
9
2
i 7
2
c
i
2 (m)
14 10
g
1
7
10 g
8
b
14
6 1
9
2
i 7
4 6
2 (k)
d
2
i 7
7
c
e 10
g
2
f
(n)
a
11
i 7
8
h
4
14
6 1
e 10
g
2
f
Abbildung 23.4, fortgesetzt: Weitere Schritte, die durch Kruskal’s Algorithmus gemacht werden.
Die Laufzeit von Kruskals Algorithmus angewendet auf einem Graphen G = (V, E) hängt davon ab, wie wir die Datenstruktur für die disjunkten Mengen implementieren. Wir nehmen an, wir würden die Implementierung durch einen Wald disjunkter Mengen aus Abschnitt 21.3 verwenden, zusammen mit den Heuristiken der Vereinigung nach dem Rang und der Pfadverkürzung, da dies die asymptotisch schnellste derzeit bekannte Implementierung ist. Das Initialisieren der Menge A in Zeile 1 benötigt Zeit O(1). Die Zeit für das Sortieren der Kanten in Zeile 4 ist O(E lg E). (Zu den Kosten der |V | Make-Set-Operationen in der for-Schleife der Zeilen 2–3 kommen wir gleich.) Die for-Schleife in den Zeilen 5–8 führt O(E) Find-Set- und Union-Operationen auf dem Wald der disjunkten Mengen aus. Zusammen mit den |V | Make-Set-Operationen benötigen diese eine Gesamtzeit von O((V + E) α(V )), wobei α die in Abschnitt 21.4 definierte, sehr langsam wachsende Funktion ist. Da G als zusammenhängend vorausgesetzt wird, gilt |E| ≥ |V | − 1, sodass die Operationen für die disjunkten Mengen Zeit O(E α(V )) benötigen. Somit ist die Gesamtlaufzeit von Kruskals Algorithmus we2 gen α(|V |) = O(lg V ) = O(lg E) in O(E lg E). Berücksichtigen wir, dass |E| < |V | gilt, dann folgt lg |E| = O(lg V ), sodass wir für die Laufzeit von Kruskals Algorithmus O(E lg V ) erhalten.
23.2 Die Algorithmen von Kruskal und Prim
645
Der Algorithmus von Prim Wie Kruskals Algorithmus ist auch der Algorithmus von Prim ein Spezialfall der generischen Methode zur Berechnung minimaler Spannbäume, die in Abschnitt 23.1 vorgestellt wurde. Prims Algorithmus arbeitet ähnlich wie Dijkstras Algorithmus zur Bestimmung kürzester Pfade in einem Graphen, den wir in Abschnitt 24.3 noch kennenlernen werden. Der Algorithmus von Prim hat die Eigenschaft, dass die Kanten der Menge A stets einen Baum bilden. Wie Abbildung 23.5 zeigt, startet der Baum von einem beliebigen Wurzelknoten und wächst, bis der Baum sich über alle Knoten aus V erstreckt. Jeder Schritt fügt eine leichte Kante zum Baum A hinzu, die A mit einem isolierten Knoten von GA = (V, A) verbindet – also mit einem Knoten, zu dem keine Kante aus A inzident ist. Nach Korollar 23.2 werden durch diese Regel nur Kanten hinzugefügt, die sicher für A sind. Daher bilden die Kanten von A einen minimalen Spannbaum, wenn der Algorithmus terminiert. Diese Strategie ist eine Greedy-Strategie, da der Baum in jedem Schritt um eine Kante erweitert wird, die das zu diesem Zeitpunkt kleinstmögliche Gewicht zum Baum beiträgt. Um Prims Algorithmus effizient zu implementieren, benötigen wir eine schnelle Methode, um eine neue Kante zum Baum, der durch die Kanten von A gebildet wird, hinzuzufügen. Im unten angegebenen Pseudocode sind der zusammenhängende Graph G und die Wurzel r des aufzubauenden minimalen Spannbaums Eingabe des Algorithmus. Während der Ausführung des Algorithmus werden alle nicht zum Baum gehörenden Knoten in einer Min-Prioritätswarteschlange Q gespeichert, die auf einem Attribut schl¨u ssel basiert. Für jeden Knoten v gibt das Attribut v.schl¨u ssel das kleinste Gewicht aller Kanten an, die v mit einem Knoten des Baumes verbinden. Falls keine solche Kante existiert, gilt nach Konvention v.schl¨u ssel = ∞. Das Attribut v.π bezeichnet den Vater von v im Baum. Der Algorithmus verwaltet implizit die Menge A aus Generic-MST durch A = {(v, v.π) : v ∈ V − {r} − Q} Wenn der Algorithmus terminiert, ist die Min-Prioritätswarteschlange Q leer; der minimale Spannbaum A für G ist dann also A = {(v, v.π) : v ∈ V − {r}} . MST-Prim(G, w, r) 1 for alle u ∈ G.V 2 u.schl¨u ssel = ∞ 3 u.π = nil 4 r.schl¨u ssel = 0 5 Q = G.V 6 while Q = ∅ 7 u = Extract-Min(Q) 8 for alle v ∈ G.Adj [u] 9 if v ∈ Q und w(u, v) < v.schl¨u ssel 10 v.π = u 11 v.schl¨u ssel = w(u, v)
646
23 Minimale Spannbäume
4
8
b
7
c
d
9
4
8
b
2 (a)
a
11 8
4
h
4
e
(b)
a
2 7
c
8
f
d
11
i 7
10 g
8
b
14
6 1
9
4
h
a
11 8
4
h
4
e
(d)
a
8
b
2 7
c
8
f
d
11
a
11 8
4
h
4
9
4 e
(f)
a
h
a
11
2 7
c
8
4
h
4
d
9
4
14
e
14
2 7
c
i
e
f
d
h
4
9
8
e 10
g
1
b
14
6
2 7
c
a
11 8
f
d
(h)
2 7
c
i 7
10 g
8
b
9
10 g
8
b 11
8
f
6 1
d
f
d
9
2
i 7
4
1
2 (g)
7
f
6
7
10 g
8
b
14
6 1
e
2
i 7
2
c
i
2 (e)
14 10
g
1
7
10 g
8
b
14
6 1
9
2
i 7
4 6
2 (c)
d
2
i 7
7
c
h
4
14
6 1
e 10
g
2
f
9
2 (i)
a
11
i 7
8
h
4
14
6 1
e 10
g
2
f
Abbildung 23.5: Die Ausführung des Algorithmus von Prim auf dem Graphen aus Abbildung 23.1. Der Wurzelknoten ist a. Schattierte Kanten gehören zu dem Baum, der aufgebaut wird, und schwarze Knoten sind im jeweiligen Baum enthalten. In jedem Schritt des Algorithmus legen die Knoten des Baumes einen Schnitt des Graphen fest, und eine leichte Kante, die den Schnitt kreuzt, wird zum Baum hinzugefügt. Im zweiten Schritt hat der Algorithmus zum Beispiel die Wahl, entweder die Kante (b, c) oder die Kante (a, h) zum Baum hinzuzufügen, da beide leichte Kanten sind, die den Schnitt kreuzen.
23.2 Die Algorithmen von Kruskal und Prim
647
Abbildung 23.5 zeigt, wie der Algorithmus von Prim arbeitet. Die Zeilen 1–5 setzen für alle Knoten das Attribut schl¨u ssel auf ∞ (außer für die Wurzel r, für die der Schlüssel auf 0 gesetzt wird, sodass die Wurzel der erste Knoten ist, der verarbeitet wird) und den Vater eines jeden Knoten auf nil; außerdem wird die Min-Prioritätswarteschlange Q so initialisiert, dass sie alle Knoten enthält. Der Algorithmus erhält die folgende Schleifeninvariante: Vor jeder Iteration der while-Schleife in den Zeilen 6–11 gilt 1. A = {(v, v.π) : v ∈ V − {r} − Q}. 2. Die Knoten, die sich bereits im minimalen Spannbaum befinden, sind jene, die zu V − Q gehören. 3. Für alle Knoten v ∈ Q mit v.π = nil gilt v.schl¨u ssel < ∞ und v.schl¨u ssel ist das Gewicht einer leichten Kante (v, v.π), die v mit einem Knoten verbindet, der bereits zum minimalen Spannbaum gehört. Zeile 7 identifiziert einen Knoten u ∈ Q, der inzident zu einer Kante ist, die den Schnitt (V − Q, Q) kreuzt (mit Ausnahme der ersten Iteration, in der wegen Zeile 4 u = r gilt). Durch das Entfernen des Knotens u aus der Menge Q wird dieser zur Menge V − Q der Baumknoten und somit (u, u.π) zu A hinzugefügt. Die for-Schleife der Zeilen 8–11 aktualisiert die Attribute schl¨u ssel und π für alle Knoten v, die mit u benachbart sind, aber nicht zum Baum gehören. Die Aktualisierung erhält den dritten Teil der Schleifeninvariante. Die Laufzeit von Prims Algorithmus hängt davon ab, wie wir die Min-Prioritätswarteschlange Q implementieren. Wenn wir Q als binären Min-Heap implementieren (siehe Kapitel 6), können wir die Prozedur Build-Min-Heap verwenden, um die Zeilen 1–5 in Zeit O(V ) auszuführen. Der Rumpf der while-Schleife wird |V |-mal ausgeführt, und da jede Extract-Min-Operation Zeit O(lg V ) benötigt, ist die Gesamtzeit für alle Aufrufe von Extract-Min in O(V lg V ). Die for-Schleife der Zeilen 8–11 wird insgesamt O(E)-mal ausgeführt, da die Summe der Längen aller Adjazenzlisten 2 |E| ist. Innerhalb der for-Schleife können wir den Test in Zeile 9, ob der Knoten v in Q enthalten ist, in konstanter Zeit implementieren. Dazu verwalten wir für jeden Knoten ein Bit, das angibt, ob der Knoten zu Q gehört, und aktualisieren dieses Bit, wenn der Knoten aus Q entfernt wird. Die Zuweisung in Zeile 11 beinhaltet eine implizite Decrease-KeyOperation auf dem Min-Heap, die ein binärer Heap in Zeit O(lg V ) ausführen kann. Somit ist die Gesamtlaufzeit von Prims Algorithmus O(V lg V + E lg V ) = O(E lg V ), was asymptotisch gleich der Laufzeit unserer Implementierung von Kruskals Algorithmus ist. Wir können die asymptotische Laufzeit von Prims Algorithmus mittels Fibonacci-Heaps verbessern. Kapitel 19 zeigt, dass, wenn ein Fibonacci-Heap |V | Elemente enthält, eine Extract-Min-Operation amortisierte Zeit O(lg V ) und eine Decrease-KeyOperation (um Zeile 11 zu implementieren) amortisierte Zeit O(1) benötigt. Wenn wir also einen Fibonacci-Heap für die Implementierung der Min-Prioritätswarteschlange Q verwenden, verbessert sich die Laufzeit von Prims Algorithmus auf O(E + V lg V ).
648
23 Minimale Spannbäume
Übungen 23.2-1 Kruskals Algorithmus kann verschiedene Spannbäume für den gleichen Graphen G zurückgeben, in Abhängigkeit davon, wie Mehrdeutigkeiten aufgelöst werden, wenn die Knoten geordnet werden. Zeigen Sie, dass es für jeden minimalen Spannbaum T von G eine Möglichkeit gibt, die Kanten von G in Kruskals Algorithmus so zu ordnen, dass der Algorithmus T zurückgibt. 23.2-2 Nehmen Sie an, wir würden den Graphen G = (V, E) durch eine Adjazenzmatrix darstellen. Geben Sie für diesen Fall eine einfache Implementierung von Prims Algorithmus an, die in Zeit O(V 2 ) läuft. 23.2-3 Ist für einen dünn besetzten Graphen G = (V, E), d. h. einen Graphen mit |E| = Θ(V ) die Implementierung von Prims Algorithmus mit Fibonacci-Heap asymptotisch schneller als die Implementierung mit einem binären Heap? Wie verhält es sich bei dichten Graphen, bei denen |E| = Θ(V 2 ) gilt? Wie müssen |E| und |V | voneinander abhängen, damit die Implementierung mit einem Fibonacci-Heap schneller ist als die Implementierung mit einem binären Heap? 23.2-4 Setzen Sie voraus, dass die Kantengewichte eines Graphen ganze Zahlen aus dem Bereich von 1 bis |V | sind. Wie schnell können Sie Kruskals Algorithmus machen? Wie schnell können Sie ihn machen, wenn die Kantengewichte ganze Zahlen aus dem Bereich 1 bis W für eine Konstante W sind. 23.2-5 Setzen Sie voraus, dass die Kantengewichte eines Graphen ganze Zahlen aus dem Bereich von 1 bis |V | sind. Wie schnell können Sie Prims Algorithmus machen? Wie schnell können Sie ihn machen, wenn die Kantengewichte ganze Zahlen aus dem Bereich 1 bis W für eine Konstante W sind. 23.2-6∗ Setzen Sie voraus, dass die Kantengewichte eines Graphen gleichverteilt über dem halboffenen Intervall [0, 1) sind. Welchen Algorithmus können Sie schneller machen, Kruskals Algorithmus oder Prims Algorithmus? 23.2-7∗ Setzen Sie voraus, dass ein minimaler Spannbaum eines Graphen G schon berechnet ist. Wie schnell können wir den minimalen Spannbaum aktualisieren, wenn wir einen neuen Knoten und Kanten, die zu diesem inzident sind, zu G hinzufügen? 23.2-8 Professor Bordon schlägt einen neuen Teile-und-Beherrsche-Algorithmus für die Berechnung minimaler Spannbäume vor, der folgendermaßen arbeitet. Für einen gegebenen Graphen G = (V, E) wird die Knotenmenge V so in zwei Mengen V1 und V2 zerlegt, dass sich |V1 | und |V2 | um höchstens 1 unterscheiden. Sei E1 die Menge der Kanten, die nur zu Knoten aus V1 inzident sind, und E2 die Menge der Kanten, die nur mit Knoten aus V2 inzident sind. Rekursiv wird dann jeweils ein minimaler Spannbaum für jeden der beiden Teilgraphen G1 = (V1 , E1 ) und G2 = (V2 , E2 ) bestimmt. Schließlich wird eine Kante von E mit minimalem Gewicht gewählt, die den Schnitt (V1 , V2 ) kreuzt, um die beiden minimalen Spannbäume zu einem einzigen zu vereinigen.
Problemstellungen zu Kapitel 23
649
Zeigen Sie entweder, dass der Algorithmus einen minimalen Spannbaum für G korrekt berechnet, oder geben Sie ein Beispiel dafür an, dass der Algorithmus versagt.
Problemstellungen 23-1 Zweitbester minimaler Spannbaum Sei G = (V, E) ein ungerichteter zusammenhängender Graph mit der Gewichtsfunktion w : E → R. Setzen Sie im Folgenden voraus, dass |E| ≥ |V | gilt und die Kantengewichte paarweise verschieden sind. Wir definieren einen zweitbesten minimalen Spannbaum wie folgt. Sei T die Menge aller Spannbäume von G und T ein minimaler Spannbaum von G. Dann ist ein zweitbester minimaler Spannbaum ein Spannbaum T , für den w(T ) = minT ∈T−{T } {w(T )} gilt. a. Zeigen Sie, dass der minimale Spannbaum eindeutig ist, während dies für den zweitbesten minimalen Spannbaum nicht notwendigerweise der Fall ist. b. Sei T der minimale Spannbaum von G. Beweisen Sie, dass Kanten (u, v) ∈ T und (x, y) ∈ T existieren, sodass T − {(u, v)} ∪ {(x, y)} ein zweitbester minimaler Spannbaum von G ist. c. Sei T ein Spannbaum von G und sei für jedes Knotenpaar u, v ∈ V max [u, v] eine Kante mit maximalem Gewicht auf dem eindeutigen einfachen Pfad von u nach v in T . Geben Sie einen Algorithmus mit Laufzeit O(V 2 ) an, der zu gegebenem T für alle Knotenpaare u, v ∈ V max [u, v] bestimmt. d. Geben Sie einen effizienten Algorithmus an, der den zweitbesten minimalen Spannbaum von G berechnet. 23-2 Minimaler Spannbaum in dünn besetzten Graphen Für einen sehr dünn besetzten Graphen G = (V, E) können wir die Laufzeit von O(E + V lg V ) für Prims Algorithmus mit Fibonacci-Heaps noch weiter verbessern, indem wir eine Vorverarbeitung auf G anwenden, mit der wir die Anzahl der Knoten reduzieren können, bevor wir Prims Algorithmus anwenden. Speziell wählen wir für jeden Knoten u die zu u inzidente Kante (u, v) mit minimalem Gewicht und fügen diese in den minimalen Spannbaum, der gerade aufgebaut wird, ein. Dann kontrahieren wir alle ausgewählten Kanten (siehe Abschnitt B.4). Anstatt diese Kanten eine nach der anderen zu kontrahieren, identifizieren wir zunächst Knotenmengen, die in dem gleichen neuen Knoten vereinigt werden. Dann erzeugen wir den Graphen, den wir erhalten hätten, wenn wir eine Kante nach der anderen kontrahiert hätten. Wir tun dies jedoch, indem wir die Kanten entsprechend der Mengen, in denen ihre Endpunkte liegen, „umbenennen“. Einige Kanten aus dem ursprünglichen Graphen könnten dann identische Bezeichnungen haben. In einem solchen Fall ergibt sich nur eine Kante, deren Gewicht auf das Minimum aller Gewichte der zugehörigen Kanten im ursprünglichen Graphen gesetzt wird.
650
23 Minimale Spannbäume Zu Beginn der Berechnung ist der aufzubauende minimale Spannbaum T leer. Wir initialisieren für jede Kante (u, v) ∈ E die Attribute (u, v).orig = (u, v) und (u, v).c = w(u, v). Wir verwenden das Attribut orig , um die Kante aus dem initialen Graphen mit einer Kante aus dem kontrahierten Graphen in Beziehung zu setzen. Das Attribut c enthält das Gewicht einer Kante, und wenn Kanten kontrahiert werden, dann aktualisieren wir es nach der oben genannten Regel. Die Prozedur MST-Reduce erhält als Eingabe G und T und gibt einen kontrahierten Graphen G mit den aktualisierten Attributen orig und c zurück. Zudem sammelt die Prozedur Kanten von G und fügt sie in den minimalen Spannbaum T ein. MST-Reduce(G, T ) 1 for alle v ∈ G.V 2 v.marke = falsch 3 Make-Set(v) 4 for alle u ∈ G.V 5 if u.marke = = falsch 6 wähle v ∈ G.Adj [u] so, dass (u, v).c minimal ist 7 Union(u, v) 8 T = T ∪ {(u, v).orig } 9 u.marke = v.marke = wahr 10 G .V = {Find-Set(v) : v ∈ G.V } 11 G .E = ∅ 12 for alle (x, y) ∈ G.E 13 u = Find-Set(x) 14 v = Find-Set(y) 15 if (u, v) ∈ G .E 16 G .E = G .E ∪ {(u, v)} 17 (u, v).orig = (x, y).orig 18 (u, v).c = (x, y).c 19 else if (x, y).c < (u, v).c 20 (u, v).orig = (x, y).orig 21 (u, v).c = (x, y).c 22 konstruiere die Adjazenzlisten G .Adj für G 23 return G und T a. Sei T die Kantenmenge, die von MST-Reduce zurückgegeben wird, und A der minimale Spannbaum von dem Graphen G , der durch den Aufruf von MST-Prim(G , c , r) konstruiert wird, wobei c das Gewichtsattribut der Kanten G .E darstellt und r ein Knoten aus G .V ist. Beweisen Sie, dass T ∪ {(x, y).orig : (x, y) ∈ A} ein minimaler Spannbaum von G ist. b. Zeigen Sie |G .V | ≤ |V | /2. c. Zeigen Sie, wie wir die Prozedur MST-Reduce so implementieren können, dass sie in Zeit O(E) läuft. (Hinweis: Verwenden Sie einfache Datenstrukturen.)
Problemstellungen zu Kapitel 23
651
d. Setzen Sie voraus, dass wir MST-Reduce k-mal hintereinander ausführen, wobei wir die Ausgabe G , die in einem Durchlauf erzeugt wird, als Eingabe G im nächsten Durchlauf verwenden und die Kanten in T akkumulieren. Zeigen Sie, dass die Gesamtlaufzeit der k Durchläufe O(kE) ist. e. Setzen Sie voraus, dass wir nach k Durchläufen von MST-Reduce, die wir wie in Teil (d) geschildert ausführen, Prims Algorithmus laufen lassen, indem wir MST-Prim(G , c , r) aufrufen, wobei G zusammen mit dem Gewichtsattribut c die Ausgabe des letzten Durchlaufs von MST-Reduce darstellt und r ein beliebiger Knoten aus G .V ist. Zeigen Sie, wie k gewählt werden muss, damit die Gesamtlaufzeit in O(E lg lg V ) ist. Zeigen Sie, dass durch Ihre Wahl von k die asymptotische Gesamtlaufzeit minimiert wird. f. Für welche Werte von |E| (angegeben als Funktion in |V |) schlägt Prims Algorithmus mit Vorverarbeitung den Algorithmus ohne Vorverarbeitung asymptotisch? 23-3 Flaschenhals-Spannbaum Ein Flaschenhals-Spannbaum T eines ungerichteten Graphen G ist ein Spannbaum, dessen größtes Kantengewicht unter allen Spannbäumen von G minimal ist. Wir sagen, dass der Wert des Flaschenhals-Spannbaums das Gewicht der schwersten Kante von T ist. a. Zeigen Sie, dass ein minimaler Spannbaum ein Flaschenhals-Spannbaum ist. Teil (a) zeigt, dass das Berechnen eines Flaschenhals-Spannbaums nicht schwieriger ist als das Berechnen eines minimalen Spannbaums. In den restlichen Teilen der Problemstellung werden wir zeigen, wie wir einen Flaschenhals-Spannbaum in linearer Zeit berechnen können. b. Geben Sie einen Algorithmus mit linearer Laufzeit an, der für einen gegebenen Graphen G und eine ganze Zahl b feststellt, ob der Wert des Flaschenhals-Spannbaums höchstens b ist. c. Verwenden Sie Ihren Algorithmus aus Teil (b) als Unterroutine in einem Algorithmus mit linearer Laufzeit für das Probem des FlaschenhalsSpannbaums. (Hinweis: Verwenden Sie eine Unterroutine wie die in der Problemstellung 23-2 beschriebene Prozedur MST-Reduce, die Mengen von Kanten kontrahiert.) 23-4 Alternative Algorithmen zur Bestimmung minimaler Spannbäume In dieser Problemstellung geben wir den Pseudocode von drei verschiedenen Algorithmen an. Alle drei erhalten als Eingabe einen zusammenhängenden Graphen sowie eine Gewichtsfunktion und geben eine Kantenmenge T zurück. Sie sollen für jeden der Algorithmen entweder beweisen, dass T in jedem Fall ein minimaler Spannbaum ist oder dass T nicht unbedingt ein minimaler Spannbaum sein muss. Außerdem sollen Sie für jeden Algorithmus unabhängig davon, ob er einen minimalen Spannbaum berechnet oder nicht, die effizienteste Implementierung angeben.
652
23 Minimale Spannbäume Maybe-MST-A(G, w) 1 sortiere die Kanten in nichtsteigender Reihenfolge nach ihren Gewichten w 2 T =E 3 for jede Kante e, in der eben berechneten Reihenfolge betrachtet 4 if T − {e} ist ein zusammenhängender Graph 5 T = T − {e} 6 return T Maybe-MST-B(G, w) 1 T =∅ 2 for jede Kante e, in einer beliebigen Reihenfolge betrachtet 3 if T ∪ {e} enthält keine Zyklen 4 T = T ∪ {e} 5 return T Maybe-MST-C(G, w) 1 T =∅ 2 for jede Kante e, in einer beliebigen Reihenfolge betrachtet 3 T = T ∪ {e} 4 if T enthält einen Zyklus c 5 sei e eine Kante von c mit maximalem Gewicht 6 T = T − {e } 7 return T
Kapitelbemerkungen Tarjan [330] gibt einen Überblick über das Problem der Berechnung minimaler Spannbäume und eine hervorragende Darstellung weiterführender Themen. Graham und Hell [151] haben die Geschichte der Entstehung und der Entwicklung des Problems aufgeschrieben. Tarjan schreibt den ersten Algorithmus zur Berechnung minimaler Spannbäume einem Artikel von O. Boruvka ˙ aus dem Jahr 1926 zu. Der Algorithmus von Boruvka ˙ besteht aus O(lg V ) Iterationen der Prozedur MST-Reduce, die in der Problemstellung 23-2 beschrieben wird. Kruskal hat seinen Algorithmus 1956 in [222] publiziert. Der Algorithmus, der gemeinhin als Prims Algorithmus bekannt ist, wurde in der Tat von Prim [285] erfunden, er wurde jedoch auch schon 1930 von V. Jarník vorgeschlagen. Der Grund, weshalb Greedy-Algorithmen für die Bestimmung minimaler Spannbäumen effizient sind, besteht darin, dass die Menge der Wälder eines Graphen ein graphisches Matroid bildet (siehe Abschnitt 16.4.) Im Falle |E| = Ω(V lg V ) läuft Prims Algorithmus mit einem Fibonacci-Heap in Zeit O(E). Für dünner besetzte Graphen haben Fredman und Tarjan [114] einen Algorithmus angegeben, der in Zeit O(E lg∗ V ) läuft. Dabei werden Ideen von Kruskals Algorithmus, Prims Algorithmus und Boruvkas ˙ Algorithmus sowie erweiterte Datenstrukturen
Kapitelbemerkungen zu Kapitel 23
653
verwendet. Gabow, Galil, Spencer und Tarjan [120] haben die Laufzeit dieses Algorithmus auf O(E lg lg∗ V ) verbessert. Chazelle [60] hat einen Algorithmus mit Laufzeit O(E α (E, V )) vorgeschlagen, wobei α (E, V ) das inverse Funktional der AckermannFunktion ist. (Eine kurze Diskussion der Ackermann-Funktion und ihrer Inversen finden Sie in den Kapitelbemerkungen zu Kapitel 21.) Anders als die bisher vorgestellten Algorithmen verfolgt der Algorithmus von Chazelle keine Greedy-Strategie. Ein verwandtes Problem ist die Verifizierung von Spannbäumen. Hierbei sind ein Graph G = (V, E) und ein Baum T ⊆ E gegeben, und wir wollen bestimmen, ob T ein minimaler Spannbaum für G ist. King [203] gibt, aufbauend auf früheren Arbeiten von Komlós [215] und Dixon, Rauch und Tarjan [90], einen Algorithmus mit linearer Laufzeit für die Verifizierung von Spannbäumen an. Die oben erwähnten Algorithmen sind alle deterministisch und entsprechen dem vergleichsbasierten Modell, das in Kapitel 8 beschrieben wurde. Karger, Klein und Tarjan [195] geben einen randomisierten Algorithmus zur Berechnung minimaler Spannbäume an, der in erwarteter Zeit O(V + E) läuft. Dieser Algorithmus arbeitet mit Rekursion, ähnlich wie der Auswahlalgorithmus mit linearer Laufzeit aus Abschnitt 9.3: Ein rekursiver Aufruf auf einem Hilfsproblem identifiziert eine Teilmenge der Kanten E , die nicht zu einem minimalen Spannbaum gehören kann. Ein weiterer rekursiver Aufruf auf E − E bestimmt dann den minimalen Spannbaum. Der Algorithmus verwendet auch Ideen von Boruvkas ˙ Algorithmus und von Kings Algorithmus für die Verifizierung von minimalen Spannbäumen. Fredman und Willard [116] haben gezeigt, wie ein minimaler Spannbaum in Zeit O(V + E) mit einem deterministischen, nicht vergleichsbasierten Algorithmus bestimmt werden kann. Ihr Algorithmus setzt voraus, dass die Daten b-Bit-Integerzahlen sind und dass der Arbeitsspeicher aus adressierbaren b-Bit-Wörtern besteht.
24
Kürzeste Pfade von einem Startknoten aus
Professor Patrick möchte die kürzeste Route von Phoenix nach Indianapolis bestimmen. Wie kann sie diese kürzeste Route finden, wenn sie eine Straßenkarte der Vereinigten Staaten besitzt, auf der die Abstände zwischen allen Paaren benachbarter Kreuzungen eingezeichnet sind? Eine Möglichkeit bestünde darin, alle Routen von Phoenix nach Indianapolis aufzulisten und die kürzeste auszuwählen. Es ist jedoch offensichtlich, dass selbst dann, wenn Professor Patrick Routen mit Zyklen ausschließt, sie eine riesige Anzahl von Möglichkeiten zu untersuchen hätte, von denen viele einfach nicht wert sind, betrachtet zu werden. Zum Beispiel ist eine Route von Phoenix nach Indianapolis, die über Seattle geht, ganz sicher eine schlechte Wahl, da Seattle einige hundert Meilen abseits liegt. In diesem Kapitel und in Kapitel 25 zeigen wir, wie Probleme dieser Art effizient gelöst werden können. Bei einem kürzeste-Pfade-Problem ist ein gewichteter gerichteter Graph G = (V, E) mit einer Gewichtsfunktion w : E → R, die die Kanten auf reellwertige Gewichte abbildet, gegeben. Das Gewicht w(p) eines Pfades p = v0 , v1 , . . . , vk ist die Summe der Gewichte aller Kanten, aus denen er zusammengesetzt ist, also w(p) =
k
w(vi−1 , vi ) .
i=1
Wir definieren das Gewicht des kürzesten Pfades von u nach v durch < p min{w(p) : u ; v} falls ein Pfad von u nach v existiert , δ(u, v) = ∞ sonst . Ein kürzester Pfad vom Knoten u zum Knoten v ist dann als ein Pfad p mit dem Gewicht w(p) = δ(u, v) definiert. In dem Phoenix-nach-Indianapolis-Beispiel können wir die Straßenkarte durch einen Graphen modellieren. Dabei repräsentieren die Knoten die Kreuzungen, die Kanten die Straßenabschnitte zwischen den Kreuzungen und die Kantengewichte die Entfernungen. Unser Ziel ist es, einen kürzesten Pfad von einer gegebenen Kreuzung in Phoenix zu einer gegebenen Kreuzung in Indianapolis zu finden. Die Kantengewichte können auch andere Metriken als die Entfernungen sein, wie z. B. Zeit, Kosten, Nachteile, Verlust oder beliebige andere Größen, die sich entlang eines Pfades linear akkumulieren und die wir minimieren möchten.
656
24 Kürzeste Pfade von einem Startknoten aus
Die Breitensuche aus Abschnitt 22.2 ist für den Fall, dass der Graph ungewichtet ist, d. h. alle Kanten das Gewicht 1 haben, ein Algorithmus, der das kürzeste-PfadeProblem löst. Da viele Konzepte der Breitensuche bei der Untersuchung des kürzestePfade-Problems auf gewichteten Graphen vorkommen, empfehlen wir dem Leser, Abschnitt 22.2 zunächst durchzusehen.
Varianten In diesem Kapitel beschäftigen wir uns mit dem kürzeste-Pfade-Problem mit einem Startknoten (engl.: single-source shortest-paths problem): Für einen gegebenen Graphen G = (V, E) und einen gegebenen Startknoten s ∈ V wollen wir jeweils einen kürzesten Pfad von diesem Startknoten s zu jedem Knoten v ∈ V berechnen. Ein Algorithmus für dieses Problem kann viele andere Probleme lösen, insbesondere die folgenden Varianten: Kürzeste-Pfade-Problem mit einem Zielknoten: Finde für jeden Knoten v jeweils einen kürzesten Pfad von v zu einem einzigen gegebenen Zielknoten t. Indem wir die Richtung aller Kanten umkehren, können wir dieses Problem auf ein kürzeste-Pfade-Problem mit einem Startknoten zurückführen. Problem der Berechnung eines kürzesten Pfades für ein Knotenpaar: Finde einen kürzesten Pfad für einen gegebenen Startknoten u und einen gegebenen Zielknoten v. Wenn wir das kürzeste-Pfade-Problem mit dem Startknoten u lösen, dann lösen wir auch dieses Problem. Mehr noch, alle bekannten Algorithmen für dieses Problem haben die gleiche asymptotische Laufzeit im schlechtesten Fall wie die besten Algorithmen für das kürzeste-Pfade-Problem mit einem Startknoten. Kürzeste-Pfade-Problem für alle Knotenpaare: Finde für jedes Knotenpaar u und v einen kürzesten Pfad von u nach v. Zwar kann dieses Problem gelöst werden, indem wir für jeden Knoten u einen Algorithmus für das kürzeste-Pfade-Problem mit einem Startknoten laufen lassen; wir können aber das Problem in der Regel schneller lösen. Außerdem ist die Struktur des Problems für sich interessant. Kapitel 25 beschäftigt sich im Detail mit dem kürzeste-PfadeProblem für alle Knotenpaare.
Optimale Teilstruktur eines kürzesten Pfades Algorithmen für das kürzeste-Pfade-Problem beruhen in der Regel auf der Eigenschaft, dass ein kürzester Pfad zwischen zwei Knoten andere kürzeste Pfade enthält. (Der Edmonds-Karp-Algorithmus für die Berechnung eines maximalen Flusses, der in Kapitel 26 vorgestellt wird, beruht ebenfalls auf dieser Eigenschaft.) Wir wissen, dass diese optimale-Teilstruktur-Eigenschaft einer der Hauptindikatoren dafür ist, dass wir möglicherweise dynamische Programmierung (Kapitel 15) oder die Greedy-Methode (Kapi-
24 Kürzeste Pfade von einem Startknoten aus
657
tel 16) anwenden können. Dijkstras Algorithmus, den wir in Abschnitt 24.3 kennenlernen werden, ist ein Greedy-Algorithmus, der Floyd-Warshall-Algorithmus, der die kürzesten Pfade zwischen allen Knotenpaaren bestimmt, arbeitet dagegen nach dem Prinzip der dynamischen Programmierung (siehe Abschnitt 25.2). Das folgende Lemma formuliert die optimale-Teilstruktur-Eigenschaft der kürzesten Pfade genauer. Lemma 24.1: (Teilpfade von kürzesten Pfaden sind kürzeste Pfade.) Gegeben sei ein gewichteter gerichteter Graph G = (V, E) mit der Gewichtsfunktion w : E → R. Sei p = v0 , v1 , . . . , vk ein kürzester Pfad vom Knoten v0 zum Knoten vk und pij = vi , vi+1 , . . . , vj für alle i und j mit 0 ≤ i ≤ j ≤ k der Teilpfad von p vom Knoten vi zum Knoten vj . Dann ist pij ein kürzester Pfad von vi nach vj . p0i
pij
pjk
Beweis: Zerlegen wir den Pfad p in v0 ; vi ; vj ; vk , dann gilt w(p) = w(p0i ) + w(pij ) + w(pjk ). Nehmen Sie nun an, es gäbe einen Pfad pij von vi nach vj mit dem p0i
pij
pjk
Gewicht w(pij ) < w(pij ). Dann wäre v0 ; vi ; vj ; vk ein Pfad von v0 nach vk , dessen Gewicht w(p1i ) + w(pij ) + w(pjk ) kleiner als w(p) ist, was der Voraussetzung widersprechen würde, dass p ein kürzester Pfad von v0 nach vk ist.
Kanten mit negativen Gewichten Manche Instanzen des kürzeste-Pfade-Problems enthalten Kanten mit negativen Gewichten. Wenn der Graph G = (V, E) keine Zyklen mit negativem Gewicht enthält, die vom Startknoten s aus erreichbar sind, dann bleibt das Gewicht δ(s, v) eines kürzesten Pfades von s nach v für alle v ∈ V selbst dann wohldefiniert, wenn er ein negatives Gewicht hat. Wenn der Graph einen Zyklus mit negativem Gewicht enthält, der von s aus erreichbar ist, dann sind die Gewichte der kürzesten Pfade jedoch nicht mehr wohldefiniert. Kein Pfad von s zu einem Knoten, der auf dem Zyklus liegt, kann ein kürzester Pfad sein – wir können immer einen Pfad mit kleinerem Gewicht finden, indem wir den vermeintlich „kürzesten“ Pfad nehmen, und dann den Zyklus mit negativem Gewicht durchlaufen. Wenn es auf einem Pfad von s nach v einen Zyklus mit negativem Gewicht gibt, dann definieren wir δ(s, v) = −∞. Abbildung 24.1 illustriert die Auswirkungen von negativen Kantengewichten und von Zyklen mit negativem Gewicht auf die Gewichte der kürzesten Pfade. Da es nur einen Pfad von s nach a gibt (nämlich den Pfad s, a), gilt δ(s, a) = w(s, a) = 3. Entsprechend gibt es nur einen Pfad von s nach b, und somit ist δ(s, b) = w(s, a)+w(a, b) = 3+(−4) = −1. Es gibt unendlich viele Pfade von s nach c: s, c, s, c, d, c, s, c, d, c, d, c und so weiter. Da der Zyklus c, d, c das Gewicht 6+(−3) = 3 > 0 hat, ist der kürzeste Pfad von s nach c der Pfad s, c; er hat das Gewicht δ(s, c) = 5. Ähnlich ist der kürzeste Pfad von s nach d der Pfad s, c, d mit dem Gewicht δ(s, d) = w(s, c) + w(c, d) = 11. Analog gibt es unendlich viele Pfade von s nach e: s, e, s, e, f, e, s, e, f, e, f, e und so weiter. Da jedoch der Zyklus e, f, e das Gewicht 3 + (−6) = −3 < 0 hat, gibt es keinen kürzesten Pfad von s nach e. Wir können Pfade von s nach e mit beliebig großen negativen Gewichten finden, indem wir den Zyklus e, f, e mit negativem Gewicht beliebig oft
658
24 Kürzeste Pfade von einem Startknoten aus a 3
–4
b –1
3 s 0
5
4 c 5
6
d 11
8
f
7
–3 2
e
3
–∞
g –∞
h ∞
i ∞
2
–8
3 ∞ j
–∞ –6
Abbildung 24.1: Negative Kantengewichte in einem gerichteten Graphen. Das Gewicht eines jeweils kürzesten Pfades ist innerhalb eines jeden Knotens angegeben. Da die Knoten e und f einen Zyklus mit negativem Gewicht bilden, der von s aus erreichbar ist, ist das Gewicht ihres kürzesten Pfades −∞. Der Knoten g ist von einem Knoten aus erreichbar, dessen kürzester Pfad vom Startknoten s aus Gewicht −∞ hat, sodass das Gewicht des kürzestens Pfades von s zu ihm ebenfalls −∞ ist. Die Knoten h, i und j sind von s aus nicht erreichbar. Die Gewichte ihrer kürzesten Pfade sind deshalb ∞, obwohl sie auf einem Zyklus mit negativem Gewicht liegen.
durchlaufen, sodass δ(s, e) = −∞ gilt. Entsprechend gilt δ(s, f ) = −∞. Da g von f aus erreichbar ist, können wir auch Pfade von s nach g mit beliebig großen negativen Gewichten finden, sodass auch δ(s, g) = −∞ gilt. Die Knoten h, i und j bilden ebenfalls einen Zyklus mit negativem Gewicht. Sie sind jedoch von s aus nicht erreichbar, sodass δ(s, h) = δ(s, i) = δ(s, j) = ∞ gilt. Einige Algorithmen für das kürzeste-Pfade-Problem, wie zum Beispiel Dijkstras Algorithmus, setzen voraus, dass alle Kantengewichte des Eingabegraphen nichtnegativ sind, wie im Beispiel der Straßenkarte. Andere Algorithmen, wie zum Beispiel der BellmanFord-Algorithmus, erlauben negative Kantengewichte im Eingabegraphen und erzeugen eine korrekte Lösung, sofern es keine von s aus erreichbaren Zyklen mit negativem Gewicht gibt. Falls ein solcher Zyklus existiert, kann der Algorithmus diesen in der Regel entdecken und seine Existenz melden.
Zyklen Kann ein kürzester Pfad einen Zyklus enthalten? Wie wir gerade gesehen haben, kann er keinen Zyklus mit negativem Gewicht enthalten. Er kann auch keinen Zyklus mit positivem Gewicht enthalten, da dann das Entfernen des Zyklus aus dem Pfad einen Pfad mit dem gleichen Startknoten und dem gleichen Zielknoten, aber mit einem niedrigeren Gewicht erzeugen würde. Das heißt, wenn p = v0 , v1 , . . . , vk ein Pfad und c = vi , vi+1 , . . . , vj (mit vi = vj und w(c) > 0) ein Zyklus mit positivem Gewicht auf diesem Pfad ist, dann hat der Pfad p = v0 , v1 , . . . , vi , vj+1 , vj+2 , . . . , vk das Gewicht w(p ) = w(p) − w(c) < w(p), sodass p kein kürzester Pfad von v0 nach vk sein kann. Damit bleibt nur die Möglichkeit von Zyklen mit dem Gewicht 0. Wir können einen Zyklus mit dem Gewicht 0 aus jedem Pfad entfernen und auf diese Weise einen ande-
24 Kürzeste Pfade von einem Startknoten aus
659
ren Pfad mit dem gleichen Gewicht erzeugen. Wenn es also einen kürzesten Pfad vom Startknoten s zu einem Zielknoten v gibt, der einen Zyklus mit dem Gewicht 0 enthält, dann gibt es einen anderen Pfad von s nach v, der diesen Zyklus nicht enthält. Solange ein kürzester Pfad Zyklen vom Gewicht 0 enthält, können wir diese Zyklen sukzessive aus dem Pfad entfernen, bis wir einen zyklenfreien kürzesten Pfad erhalten. Wenn wir kürzeste Pfade bestimmen, können wir daher ohne Beschränkung der Allgemeinheit voraussetzen, dass diese keine Zyklen enthalten. Da jeder azyklische Pfad in einem Graphen G = (V, E) maximal |V | verschiedene Knoten enthält, kann er auch höchstens |V | − 1 Kanten haben. Somit können wir unsere Suche nach kürzesten Pfaden auf Pfade mit höchstens |V | − 1 Kanten beschränken.
Darstellung kürzester Pfade Häufig wollen wir nicht nur die Gewichte der kürzesten Pfade bestimmen, sondern auch die Knoten, die auf den kürzesten Pfaden liegen. Wir stellen kürzeste Pfade so ähnlich dar, wie wir Breitensuchbäume in Abschnitt 22.2 dargestellt haben. Für einen gegebenen Graphen G = (V, E) verwalten wir für jeden Knoten v ∈ V einen Vorgänger v.π, der entweder ein anderer Knoten oder nil ist. Die Algorithmen für das kürzeste-PfadeProblem, die wir in diesem Kapitel behandeln, setzen die Attribute π so, dass die bei v beginnende Kette der Vorgänger rückwärts entlang eines kürzesten Pfades von s nach v verläuft. Damit kann für einen gegebenen Knoten v mit v.π = nil die Prozedur Print-Path(G, s, v) aus Abschnitt 22.2 verwendet werden, um einen kürzesten Pfad von s nach v auszugeben. Inmitten der Ausführung eines Algorithmus für das kürzeste-Pfade-Problem müssen die π-Werte allerdings nicht unbedingt kürzeste Pfade widerspiegeln. Wie bei der Breitensuche werden wir uns für den Vorgängerteilgraph Gπ = (Vπ , Eπ ) interessieren, der durch die π-Werte erzeugt wird. Auch hier definieren wir die Knotenmenge Vπ als die Menge aller Knoten von G, deren Vorgänger nicht nil sind, zuzüglich dem Startknoten s: Vπ = {v ∈ V : v.π = nil} ∪ {s} . Die Menge Eπ gerichteter Kanten setzt sich aus den Kanten zusammen, die durch die π-Werte der Knoten der Menge Vπ induziert werden: Eπ = {(v.π, v) ∈ E : v ∈ Vπ − {s}} . Wir werden beweisen, dass die π-Werte, die von den Algorithmen aus diesem Kapitel erzeugt werden, die Eigenschaft haben, dass Gπ bei der Terminierung ein „Baum kürzester Pfade“ ist – dies ist, lax formuliert, ein gewurzelter Baum, der einen kürzesten Pfad von dem Startknoten s zu jedem von s aus erreichbaren Knoten enthält. Ein Baum kürzester Pfade ähnelt dem Breitensuchbaum aus Abschnitt 22.2, er enthält jedoch kürzeste Pfade vom Startknoten aus, die durch die Kantengewichte und nicht durch die Anzahl der Kanten bestimmt sind. Um genauer zu sein, sei G = (V, E) ein gewichteter gerichteter Graph mit der Gewichtsfunktion w : E → R und setzen Sie voraus, dass G keine Zyklen mit negativem Gewicht enthält, die vom Startknoten s aus erreichbar sind, sodass die kürzesten Pfade wohldefiniert sind. Ein von s ausgehender Baum kürzester
660
24 Kürzeste Pfade von einem Startknoten aus t 3
3 2
s 0 5
x 9
6 1
4
2
3 7
6
2
s 0
3 5 y (a)
t 3
11 z
5
x 9
6 1
4
2
3 7
6
2
s 0
3 5 y (b)
t 3
11 z
5
x 9
6 1
4
2
7
3 5 y (c)
6
11 z
Abbildung 24.2: (a) Ein gewichteter gerichteter Graph mit den Gewichten der kürzesten Pfade vom Startknoten s aus, die innerhalb der Knoten vermerkt sind. (b) Die schattierten Kanten bilden einen Baum kürzester Pfade, dessen Wurzel der Startknoten s ist. (c) Ein anderer Baum kürzester Pfade mit der gleichen Wurzel.
Pfade ist ein gerichteter Teilgraph G = (V , E ) mit V ⊆ V und E ⊆ E, für den Folgendes gilt: 1. V ist die Menge der in G von s aus erreichbaren Knoten, 2. G bildet einen gerichteten Baum mit der Wurzel s, 3. für alle Knoten v ∈ V ist der eindeutige einfache Pfad von s nach v in G ein kürzester Pfad von s nach v in G. Kürzeste Pfade sind nicht notwendigerweise eindeutig und Bäume kürzester Pfade ebenfalls nicht. Beispielsweise zeigt Abbildung 24.2 einen gewichteten gerichteten Graphen und zwei Bäume kürzester Pfade mit der gleichen Wurzel.
Relaxation Die in diesem Kapitel behandelten Algorithmen verwenden die Methode der Relaxation. Für jeden Knoten v ∈ V verwalten wir ein Attribut v.d , das eine obere Schranke für das Gewicht des kürzesten Pfades vom Startknoten s nach v speichert. Wir bezeichnen v.d als Schätzung des kürzesten Pfades. Wir initialisieren die Schätzungen der kürzesten Pfade und die Vorgänger durch die folgende Prozedur, die in Zeit Θ(V ) läuft. Initialize-Single-Source(G, s) 1 for jeden Knoten v ∈ G.V 2 v.d = ∞ 3 v.π = nil 4 s.d = 0 Nach der Initialisierung gilt v.π = nil für jeden Knoten v ∈ V sowie s.d = 0 und v.d = ∞ für v ∈ V − {s}.
24 Kürzeste Pfade von einem Startknoten aus u 5
v 9
2
u 5
2
v 7
(a)
v 6
2
RELAX(u,v,w)
RELAX(u,v,w) u 5
661
u 5
2
v 6
(b)
Abbildung 24.3: Relaxation einer Kante (u, v) mit dem Gewicht w(u, v) = 2. In jedem Knoten ist seine Schätzung des kürzesten Pfades eingetragen. (a) Da vor der ersten Relaxation v. d > u. d + w(u, v) gilt, wird der Wert von v. d verkleinert. (b) Hier gilt vor der ersten Relaxation v. d ≤ u. d + w(u, v), sodass v. d durch die Relaxation nicht verändert wird.
Der Prozess des Relaxierens 1 einer Kante (u, v) besteht darin, zu testen, ob wir den bisher gefundenen kürzesten Pfad nach v verbessern können, indem wir über u laufen, und, wenn dies der Fall ist, die Attribute v.d und v.π entsprechend zu aktualisieren. Ein Relaxationschritt kann den Wert der Schätzung des kürzesten Pfades verringern und das Vorgängerattribut v.π des Knotens v aktualisieren. Der folgende Code führt einen Relaxationsschritt auf der Kante (u, v) in Zeit O(1) aus.
Relax(u, v, w) 1 if v.d > u.d + w(u, v) 2 v.d = u.d + w(u, v) 3 v.π = u
Abbildung 24.3 zeigt zwei Beispiele für das Relaxieren einer Kante. In Teil (a) verringert sich die Schätzung, in Teil (b) ändert sich die Schätzung nicht. Alle Algorithmen in diesem Kapitel rufen Initialize-Single-Source auf und relaxieren dann die Kanten sukzessive. Zudem ist die Relaxation die einzige Methode, durch die die Schätzungen der kürzesten Pfade und die Vorgänger geändert werden. Die in diesem Kapitel vorgestellten Algorithmen unterscheiden sich darin, wie oft jede Kante relaxiert wird, sowie in der Reihenfolge, in der die Kanten relaxiert werden. Dijkstras Algorithmus und der Algorithmus zur Bestimmung der kürzesten Pfade in gerichteten azyklischen Graphen relaxieren jede Kante genau einmal. Der Bellman-Ford-Algorithmus relaxiert jede Kante (|V | − 1)-mal. 1 Es mag seltsam erscheinen, dass der Begriff „Relaxation“ für eine Operation verwendet wird, die eine obere Schranke festlegt. Der Verwendung des Begriffes hat historische Gründe. Das Ergebnis eines Relaxationschrittes kann als Relaxation der Bedingung v. d ≤ u. d + w(u, v) aufgefasst werden, die wegen der Dreiecksungleichung (Lemma 24.10) erfüllt sein muss, wenn u. d = δ(s, u) und v. d = δ(s, v) gelten. Das heißt, wenn v. d ≤ u. d + w(u, v) gilt, gibt es keinen „Druck“, diese Bedingung zu erfüllen, sodass die Bedingung „relaxiert“ ist.
662
24 Kürzeste Pfade von einem Startknoten aus
Eigenschaften kürzester Pfade und der Relaxation Um zu beweisen, dass die in diesem Kapitel vorgestellten Algorithmen korrekt sind, werden wir von verschiedenen Eigenschaften kürzester Pfade und der Relaxation Gebrauch machen. Wir wollen diese Eigenschaften hier bereits formulieren und in Abschnitt 24.5 formal beweisen. Zu Ihrer Orientierung ist zu jeder Eigenschaft die Nummer des zugehörigen Lemmas oder Korollars aus Abschnitt 24.5 angegeben. Die letzten fünf Eigenschaften, die sich auf Schätzungen kürzester Pfade oder den Vorgängerteilgraph beziehen, setzen implizit voraus, dass der Graph mit einem Aufruf von InitializeSingle-Source(G, s) initialisiert wird und dass der einzige Weg, dass Schätzungen der kürzesten Pfade und der Vorgängerteilgraph sich ändern, darin besteht, dass eine Folge von Relaxationsschritten ausgeführt wird. Dreiecksungleichung (Lemma 24.10) Für alle Kanten (u, v) ∈ E gilt δ(s, v) ≤ δ(s, u) + w(u, v). Eigenschaft der oberen Schranke (Lemma 24.11) Für alle Knoten v ∈ V gilt stets v.d ≥ δ(s, v), und wenn v.d einmal den Wert δ(s, v) erreicht hat, dann ändert sich das Attribut nicht mehr. Kein-Pfad-Eigenschaft (Korollar 24.12) Wenn es keinen Pfad von s nach v gibt, dann gilt stets v.d = δ(s, v) = ∞. Konvergenzeigenschaft (Lemma 24.14) Wenn s ; u → v für ein Knotenpaar u, v ∈ V ein kürzester Pfad von s nach v in G ist und u.d = δ(s, u) zu einem Zeitpunkt vor der Relaxation der Kante (u, v) gilt, dann gilt zu jedem Zeitpunkt nach der Relaxation von (u, v) die Gleichung v.d = δ(s, v). Pfadrelaxationseigenschaft (Lemma 24.15) Wenn p = v0 , v1 , . . . , vk ein kürzester Pfad von s = v0 nach vk ist und wir die Kanten von p in der Reihenfolge (v0 , v1 ), (v1 , v2 ), . . . , (vk−1 , vk ) relaxieren, dann gilt vk .d = δ(s, vk ). Diese Eigenschaft gilt ungeachtet aller anderen auftretenden Relaxationsschritte, selbst wenn diese mit Relaxationen von Kanten von p vermischt werden. Vorgängerteilgraph-Eigenschaft (Lemma 24.17) Wenn v.d = δ(s, v) für alle Knoten v ∈ V gilt, dann ist der Vorgängerteilgraph ein Baum kürzester Pfade mit der Wurzel s.
Kapitelübersicht Abschnitt 24.1 stellt den Bellman-Ford-Algorithmus vor, der das kürzeste-PfadeProblem mit einem Startknoten für den allgemeinen Fall löst, in dem die Kanten negative Gewichte haben können. Der Bellman-Ford-Algorithmus ist bemerkenswert einfach und hat den zusätzlichen Vorteil, dass er herausfindet, ob vom Startknoten aus ein Zyklus mit negativem Gewicht erreichbar ist. Abschnitt 24.2 stellt einen Algorithmus mit
24.1 Der Bellman-Ford-Algorithmus
663
linearer Laufzeit für das kürzeste-Pfade-Problem mit einem Startknoten in einem gerichteten azyklischen Graphen vor. Abschnitt 24.3 behandelt Dijkstras Algorithmus, der eine geringere Laufzeit als der Bellman-Ford-Algorithmus hat, aber nichtnegative Kantengewichte voraussetzt. Abschnitt 24.4 zeigt, wie wir den Bellman-Ford-Algorithmus verwenden können, um einen Spezialfall der linearen Programmierung zu lösen. Schließlich beweisen wir in Abschnitt 24.5 die oben formulierten Eigenschaften kürzester Pfade und der Relaxation. Wir verwenden einige Konventionen beim Rechnen mit unendlichen Größen. Wir werden voraussetzen, dass für jede reelle Zahl a = −∞ die Gleichung a + ∞ = ∞ + a = ∞ gilt. Damit unsere Beweise auch im Falle von Zyklen mit negativem Gewicht korrekt bleiben, setzen wir außerdem voraus, dass für jede reelle Zahl a = ∞ die Gleichung a + (−∞) = (−∞) + a = −∞ gilt. Alle Algorithmen aus dem vorliegenden Kapitel setzen voraus, dass der gerichtete Graph G über Adjazenzlisten gespeichert ist. Zusätzlich wird zu jeder Kante deren Gewicht gespeichert, sodass wir durch Traversieren der Adjazenzlisten die Kantengewichte in Zeit O(1) pro Kante bestimmen können.
24.1
Der Bellman-Ford-Algorithmus
Der Bellman-Ford-Algorithmus löst das kürzeste-Pfade-Problem mit einem Startknoten für den allgemeinen Fall, in dem die Kantengewichte auch negativ sein können. Für einen gewichteten gerichteten Graphen G = (V, E) mit einem Startknoten s und einer Gewichtsfunktion w : E → R gibt der Bellman-Ford-Algorithmus einen Booleschen Wert zurück, der anzeigt, ob es einen Zyklus mit negativem Gewicht gibt, der vom Startknoten aus erreichbar ist. Falls es einen solchen Zyklus gibt, meldet der Algorithmus, dass keine Lösung existiert. Anderenfalls bestimmt der Algorithmus die kürzesten Pfade und ihre Gewichte. Der Algorithmus relaxiert Kanten und bestimmt so immer kleinere Schätzungen v.d für das jeweilige Gewicht eines kürzesten Pfades vom Startknoten s zu einem jeden Knoten v ∈ V , bis er schließlich das tatsächliche Gewicht des kürzesten Pfades δ(s, v) erreicht. Der Algorithmus gibt genau dann wahr zurück, wenn der Graph keine Zyklen mit negativem Gewicht enthält, die vom Startknoten aus erreichbar sind. Bellman-Ford(G, w, s) 1 Initialize-Single-Source(G, s) 2 for i = 1 to |G.V | − 1 3 for jede Kante (u, v) ∈ G.E 4 Relax(u, v, w) 5 for jede Kante (u, v) ∈ G.E 6 if v.d > u.d + w(u, v) 7 return falsch 8 return wahr Abbildung 24.4 illustriert die Arbeitsweise des Bellman-Ford-Algorithmus auf einem
664
24 Kürzeste Pfade von einem Startknoten aus
6 s 0
t ∞
5 –2 –3
8 7
x ∞ –4 7
6 s 0
2 ∞ y
9
6 s 0
5 –2
x 4 –3 –4 7
2 7 y (d)
–3 –4 7
7 y
9
∞ z
(b)
8 7
x ∞
2
(a) t 2
5 –2
8 7
∞ z
t 6
9
2 z
6 s 0
t 2
s 0
5 –2 –3
8 7
x 4 –4 7
2 7 y
9
2 z
(c) 5 –2
x 4 –3 –4 7
8 7
6
t 6
2 7 y
9
–2 z
(e)
Abbildung 24.4: Die Arbeitsweise des Bellman-Ford-Algorithmus. Der Startknoten ist s. Die d-Werte sind innerhalb der Knoten annotiert. Die schattierten Kanten zeigen den Vorgängerwert an: Falls die Kante (u, v) schattiert gezeichnet ist, gilt v. π = u. In diesem speziellen Beispiel relaxiert jeder Durchlauf die Kanten in der Reihenfolge (t, x), (t, y), (t, z), (x, t), (y, x), (y, z), (z, x), (z, s), (s, t), (s, y). (a) Die Situation unmittelbar vor dem ersten Durchlauf. (b)-(e) Die Situation jeweils nach einem (weiteren) Durchlauf. Die in Teil (e) angegebenen Werte für d und π sind die endgültigen. Der Bellman-Ford-Algorithmus gibt in diesem Beispiel den Wert wahr zurück.
Graphen mit fünf Knoten. Nachdem Zeile 1 die Attribute d und π für alle Knoten initialisiert hat, führt der Algorithmus |V | − 1 Durchläufe über die Kanten des Graphen aus. Jeder Durchlauf ist eine Iteration der for-Schleife der Zeilen 2–4 und besteht darin, alle Kanten des Graphen jeweils einmal zu relaxieren. Die Abbildungen 24.4(b)–(e) zeigen den Zustand nach jedem der vier Durchläufe. Nach |V | − 1 Durchläufen prüfen die Zeilen 5–8, ob Zyklen mit nichtnegativem Gewicht existieren und geben den entsprechenden Booleschen Wert zurück. (Wir werden in Kürze sehen, warum dieser Test funktioniert.) Der Bellman-Ford-Algorithmus läuft in Zeit O(V E), denn die Initialisierung in Zeile 1 benötigt Zeit Θ(V ), jeder der |V | − 1 Durchläufe über die Kanten in den Zeilen 2–4 Zeit Θ(E) und die for-Schleife in den Zeilen 5–7 Zeit O(E). Um die Korrektheit des Bellman-Ford-Algorithmus zu beweisen, zeigen wir zuerst, dass der Algorithmus die Gewichte der kürzesten Pfade für alle Knoten, die vom Startknoten aus erreichbar sind, korrekt berechnet, falls es keine Zyklen mit negativem Gewicht gibt. Lemma 24.2 Sei G = (V, E) ein gewichteter gerichteter Graph mit dem Startknoten s und der
24.1 Der Bellman-Ford-Algorithmus
665
Gewichtsfunktion w : E → R, der keine von s aus erreichbaren Zyklen mit negativem Gewicht enthält. Dann gilt nach |V |−1 Iterationen der for-Schleife der Zeilen 2–4 der Prozedur Bellman-Ford die Gleichung v.d = δ(s, v) für alle von s aus erreichbaren Knoten v. Beweis: Wir beweisen das Lemma, indem wir von der Pfadrelaxationseigenschaft Gebrauch machen. Betrachten Sie einen beliebigen Knoten v, der von s aus erreichbar ist, und einen beliebigen kürzesten Pfad p von s nach v, d. h. p = v0 , v1 , . . . , vk mit v0 = s und vk = v. Da kürzeste Pfade einfach sind, enthält der Pfad p höchstens |V | − 1 Kanten, sodass k ≤ |V | − 1 gilt. Jede der |V | − 1 Iterationen der for-Schleife in den Zeilen 2–4 relaxiert alle |E| Kanten. Für i = 1, 2, . . . k ist die Kante (vi−1 , vi ) unter den in der i-ten Iteration relaxierten Kanten. Wegen der Pfadrelaxationseigenschaft gilt daher v.d = vk .d = δ(s, vk ) = δ(s, v).
Korollar 24.3 Sei G = (V, E) ein gewichteter gerichteter Graph mit dem Startknoten s und der Gewichtsfunktion w : E → R. Dann gilt für jeden Knoten v ∈ V , dass es genau dann einen Pfad von s nach v gibt, wenn die Prozedur Bellman-Ford angewendet auf den Graphen G mit v.d < ∞ terminiert. Beweis: Der Beweis ist Gegenstand von Übung 24.1-2. Bemerken Sie bitte, dass im Gegensatz zu Lemma 24.2 dieses Korollar auch dann gilt, wenn G Zyklen mit negativem Gewicht enthält, die von s aus erreichbar sind. Theorem 24.4: (Korrektheit des Bellman-Ford-Algorithmus) Wenden Sie den Bellman-Ford-Algorithmus auf einen gewichteten gerichteten Graphen G = (V, E) mit dem Startknoten s und der Gewichtsfunktion w : E → R an. Falls G keine Zyklen mit negativem Gewicht enthält, die von s aus erreichbar sind, so gibt der Algorithmus den Wert wahr zurück und bei Terminierung gilt v.d = δ(s, v) für jeden Knoten v ∈ V und der Vorgängerteilgraph Gπ ist ein Baum kürzester Pfade mit der Wurzel s. Falls G einen Zyklus mit negativem Gewicht enthält, der von s aus erreichbar ist, dann gibt der Algorithmus den Wert falsch zurück. Beweis: Setzen Sie voraus, dass der Graph G keine Zyklen mit negativem Gewicht hat, die vom Startknoten s aus erreichbar sind. Wir beweisen zunächst die Behauptung, dass für alle Knoten v ∈ V bei Terminierung v.d = δ(s, v) gilt. Wenn der Knoten v von s aus erreichbar ist, dann ist die Behauptung durch Lemma 24.2 bewiesen. Wenn v von s aus nicht erreichbar ist, dann folgt die Behauptung aus der Kein-Pfad-Eigenschaft. Damit ist die Behauptung bewiesen. Aus der Vorgängerteilgraph-Eigenschaft und der soeben bewiesenen Behauptung folgt, dass Gπ ein Baum kürzester Pfade ist. Nun benutzen
666
24 Kürzeste Pfade von einem Startknoten aus
wir die Behauptung, um zu zeigen, dass die Prozedur Bellman-Ford den Wert wahr zurückgibt. Bei Terminierung gilt für alle Kanten (u, v) ∈ E v.d = δ(s, v) ≤ δ(s, u) + w(u, v) = u.d + w(u, v) ,
(wegen der Dreiecksungleichung)
sodass keiner der Tests in Zeile 6 dazu führt, dass Bellman-Ford den Wert falsch zurückgibt. Setzen Sie nun voraus, dass der Graph G einen Zyklus mit negativem Gewicht enthält, der vom Startknoten s aus erreichbar ist; sei dies der Zyklus c = v0 , v1 , . . . , vk mit v0 = vk . Dann gilt k
(24.1)
w(vi−1 , vi ) < 0 .
i=1
Nehmen Sie nun an, dass der Bellman-Ford-Algorithmus den Wert wahr zurückgeben würde; wir wollen diese Annahme zum Widerspruch führen. Mit dieser Annahme gilt vi .d ≤ vi−1 .d + w(vi−1 , vi ) für i = 1, 2, . . . , k. Summieren wir diese Ungleichungen über den gesamten Zyklus auf, so erhalten wir
k
vi .d ≤
i=1
k
(vi−1 .d + w(vi−1 , vi ))
i=1
=
k
vi−1 .d +
i=1
k
w(vi−1 , vi ) .
i=1
Wegen v0 = vk erscheint jeder Knoten von c genau einmal in der Summe k und genau einmal in der Summe i=1 vi−1 .d , sodass k
vi .d =
i=1
k
k i=1
vi .d
vi−1 .d
i=1
gilt. Außerdem ist vi .d nach Korollar 24.3 für i = 1, 2, . . . , k endlich. Somit gilt 0≤
k
w(vi−1 , vi ) ,
i=1
was ein Widerspruch zu Ungleichung (24.1) ist. Wir schlussfolgern, dass der BellmanFord-Algorithmus den Wert wahr zurückgibt, wenn der Graph G keine Zyklen mit negativem Gewicht enthält, die vom Startknoten aus erreichbar sind, und anderenfalls den Wert falsch zurückgibt.
24.2 Kürzeste Pfade von einem Startknoten aus in DAGs
667
Übungen 24.1-1 Wenden Sie den Bellman-Ford-Algorithmus auf den gerichteten Graphen aus Abbildung 24.4 mit z als Startknoten an. Relaxieren Sie die Kanten bei jedem Durchlauf in der gleichen Reihenfolge wie in der Abbildung und geben Sie nach jedem Durchlauf die Werte von d und π an. Ändern Sie nun das Gewicht der Kante (z, x) auf den Wert 4 und lassen Sie den Algorithmus erneut laufen, wobei s diesmal der Startknoten ist. 24.1-2 Beweisen Sie Korollar 24.3. 24.1-3 Sei G = (V, E) ein gewichteter gerichteter Graph ohne Zyklen mit negativem Gewicht und m das über alle Knotenpaare u, v ∈ V genommene Maximum der minimalen Anzahl von Kanten in einem kürzesten Pfad von u nach v. (Gemeint ist hier der vom Gewicht her „kürzeste“ Pfad, nicht der von der Kantenanzahl her „kürzeste“ Pfad.) Schlagen Sie eine einfache Änderung des Bellman-Ford-Algorithmus vor, sodass er nach m + 1 Durchläufen terminiert, auch wenn m im Voraus nicht bekannt ist. 24.1-4 Modifizieren Sie den Bellman-Ford-Algorithmus so, dass er das Attribut v.d für jeden Knoten v, zu dem es einen von s startenden Pfad gibt, der einen Zyklus mit negativem Gewicht enthält, auf −∞ setzt. 24.1-5∗ Sei G = (V, E) ein gewichteter gerichteter Graph mit der Gewichtsfunktion w : E → R. Geben Sie einen Algorithmus mit Laufzeit O(V E) an, der für jeden Knoten v ∈ V den Wert δ ∗ (v) = minu∈V {δ(u, v)} bestimmt. 24.1-6∗ Setzen Sie voraus, dass ein gewichteter gerichteter Graph G = (V, E) einen Zyklus mit negativem Gewicht besitzt. Geben Sie einen effizienten Algorithmus an, der die Knoten auf einem dieser Zyklen auflistet. Beweisen Sie, dass Ihr Algorithmus korrekt arbeitet.
24.2
Kürzeste Pfade von einem Startknoten aus in gerichteten azyklischen Graphen
Wir können in einem gewichteten gerichteten azyklischen Graphen G = (V, E) das kürzeste-Pfade-Problem mit einem Startknoten in Zeit Θ(V + E) lösen, wenn wir die Kanten des Graphen in einer Reihenfolge relaxieren, die einer topologischen Sortierung der Knoten entspricht. In einem gerichteten azyklischen Graphen können keine Zyklen mit negativem Gewicht existieren, selbst wenn es Kanten mit negativem Gewicht gibt. Der Algorithmus beginnt mit dem topologischen Sortieren des gerichteten azyklischen Graphen (siehe Abschnitt 22.4), um eine lineare Reihenfolge der Knoten herzustellen. Wenn der gerichtete azyklische Graph einen Pfad vom Knoten u zum Knoten v enthält, dann steht u in der topologischen Sortierung vor v. Wir führen nur einen Durchlauf über die Knoten in topologisch sortierter Reihenfolge aus. Während der Verarbeitung jedes Knotens werden alle aus dem Knoten austretenden Kanten relaxiert.
668
r ∞
24 Kürzeste Pfade von einem Startknoten aus
5
s 0
2
6 t ∞
7
3
x ∞
–1
1 y ∞
–2
z ∞
r ∞
5
2
4
s 0
2
6 t ∞
3
5
s 0
2
6 t 2
7
3
∞
5
s
x 6
–1
1 y ∞
4
0
2
6 t 2
7
3
–2
z ∞
r ∞
5
s 0
2
6 t 2
∞
5
s 0
2
2
7
3
z ∞
2
x 6
–1
1 y 6
4
–2
z 4
2
(d)
x 6
–1
1 y 5
4
6 t
7
3
2
–2
z
r
4
∞
5
s 0
2
6 t 2
7
3
2
x 6
–1
4
1 y 5
–2
z 3
2
(f)
(e)
r
–2
(b)
(c)
r
–1
1 y ∞
4
(a)
r ∞
7
x ∞
x 6 4
–1
1 y 5
–2
z 3
2
(g)
Abbildung 24.5: Die Arbeitsweise des Algorithmus zur Bestimmung kürzester Pfade in einem gerichteten azyklischen Graphen. Die Knoten sind von links nach rechts topologisch sortiert. Der Startknoten ist s. Die d-Werte sind innerhalb der Knoten annotiert. Schattierte Kanten stellen die π-Werte dar. (a) Der Zustand vor der ersten Iteration der for-Schleife der Zeilen 3–5. (b)-(g) Der Zustand jeweils nach einer weiteren Iteration der for-Schleife. Der neu geschwärzte Knoten stellt den Knoten u in der jeweiligen Iteration dar. Die in Teil (g) gezeigten Werte sind die endgültigen Werte.
Dag-Shortest-Paths(G, w, s) 1 berechne eine topologische Sortierung der Knoten aus G 2 Initialize-Single-Source(G, s) 3 for jeden Knoten u in der Reihenfolge nach der topologischen Sortierung 4 for alle v ∈ G.Adj [u] 5 Relax(u, v, w) Abbildung 24.5 illustriert die Arbeitsweise dieses Algorithmus. Die Laufzeit dieses Algorithmus ist einfach zu analysieren. Wie in Abschnitt 22.4 gezeigt wurde, kann die topologische Sortierung in Zeile 1 in Zeit Θ(V + E) ausgeführt werden. Der Aufruf von Initialize-Single-Source in Zeile 2 benötigt Zeit Θ(V ). Die for-Schleife der Zeilen 3–5 macht eine Iteration pro Knoten. Zusammen genommen re-
24.2 Kürzeste Pfade von einem Startknoten aus in DAGs
669
laxiert die innere for-Schleife der Zeilen 4–5 jede Kante genau einmal. (Wir haben hier eine Aggregat-Analyse verwendet.) Da jede Iteration der inneren for-Schleife Zeit Θ(1) benötigt, ist die Gesamtlaufzeit in Θ(V +E), also linear in der Größe der AdjazenzlistenDarstellung des Graphen. Das folgende Theorem zeigt, dass die Prozedur Dag-Shortest-Paths die kürzesten Pfade korrekt berechnet. Theorem 24.5 Falls ein gewichteter gerichteter Graph G = (V, E) mit dem Startknoten s keine Zyklen enthält, dann gilt bei Terminierung der Prozedur Dag-Shortest-Paths v.d = δ(s, v) für alle Knoten v ∈ V und der Vorgängerteilgraph Gπ ist ein Baum kürzester Pfade. Beweis: Wir zeigen zunächst, dass für alle Knoten v ∈ V bei der Terminierung v.d = δ(s, v) gilt. Wenn v von s aus nicht erreichbar ist, dann gilt wegen der KeinPfad-Eigenschaft v.d = δ(s, v) = ∞. Setzen Sie nun voraus, dass v von s aus erreichbar ist, sodass es einen kürzesten Pfad p = v0 , v1 , . . . , vk mit v0 = s und vk = v gibt. Da wir die Knoten in topologisch sortierter Reihenfolge verarbeiten, relaxieren wir die Kanten auf p in der Reihenfolge (v0 , v1 ), (v1 , v2 ), . . . , (vk−1 , vk ). Aus der Pfadrelaxationseigenschaft folgt, dass zum Zeitpunkt der Terminierung für i = 0, 1, . . . , k die Gleichung vi .d = δ(s, vi ) gilt. Schließlich folgt aus der Vorgängerteilgraph-Eigenschaft, dass Gπ ein Baum kürzester Pfade ist. Eine interessante Anwendung dieses Algorithmus ist die Bestimmung kritischer Pfade bei der PERT-Diagramm-Analyse2 . Die Kanten repräsentieren die auszuführenden Jobs und die Kantengewichte die Zeiten, die zu deren Ausführung benötigt werden. Wenn die Kante (u, v) in den Knoten v eintritt und die Kante (v, x) aus v austritt, dann muss der Job (u, v) vor dem Job (v, x) ausgeführt werden. Ein Pfad in diesem gerichteten azyklischen Graphen stellt eine Folge von Jobs dar, die in einer bestimmten Reihenfolge ausgeführt werden müssen. Ein kritischer Pfad ist ein längster Pfad durch den gerichteten azyklischen Graphen, entsprechend der längsten Zeit, die zur Ausführung einer geordneten Folge von Jobs nötig ist. Somit ist das Gewicht eines kritischen Pfades eine untere Schranke für die Gesamtzeit, die nötig ist, um alle Jobs auszuführen. Wir können einen kritischen Pfad finden, indem wir entweder • die Kantengewichte negieren und Dag-Shortest-Paths auf den so modifizierten gewichteten Graphen anwenden, oder • Dag-Shortest-Paths mit der Modifikation laufen lassen, dass wir in Zeile 2 von Initialize-Single-Source „∞“ durch „−∞“ und in der Prozedur Relax „>“ durch „ 1 Diese Änderung bewirkt, dass die while-Schleife nur (|V | − 1)-mal statt |V |mal ausgeführt wird. Arbeitet der Algorithmus mit der vorgeschlagenen Änderung korrekt? 24.3-4 Professor Gaedel hat ein Programm geschrieben, von dem er behauptet, dass es eine Implementierung von Dijkstras Algorithmus ist. Das Programm erzeugt für jeden Knoten v ∈ V die Attribute v.d und v.π. Geben Sie einen Algorithmus mit Laufzeit O(V + E) an, der die Ausgabe des Programms des Professors auf Korrektheit überprüft. Der Algorithmus sollte bestimmen, ob die d- und π-Attribute denen eines Baumes kürzester Pfade entspricht. Sie dürfen voraussetzen, dass alle Kantengewichte nichtnegativ sind.
676
24 Kürzeste Pfade von einem Startknoten aus
24.3-5 Professor Newman glaubt, er hätte einen einfacheren Korrektheitsbeweis zu Dijkstras Algorithmus gefunden. Er behauptet, dass Dijkstras Algorithmus die Kanten eines jeden kürzesten Pfades in der Reihenfolge relaxiert, in der sie auf dem Pfad auftreten und dass somit die Pfadrelaxationseigenschaft auf jeden Knoten, der vom Startknoten aus erreichbar ist, anwendbar ist. Zeigen Sie, dass der Professor irrt, indem Sie einen gerichteten Graphen konstruieren, für den Dijkstras Algorithmus die Kanten eines kürzesten Pfades in einer anderen Reihenfolge relaxieren könnte. 24.3-6 Gegeben sei ein gerichteter Graph G = (V, E), in dem jeder Kante (u, v) ∈ E ein reeller Wert r(u, v) mit 0 ≤ r(u, v) ≤ 1 zugeordnet ist, der die Zuverlässigkeit eines Kommunikationskanals von Knoten u nach Knoten v darstellt. Wir interpretieren r(u, v) als die Wahrscheinlichkeit, dass der Kanal von u nach v nicht versagt, und setzen voraus, dass diese Wahrscheinlichkeiten paarweise unabhängig sind. Geben Sie einen effizienten Algorithmus an, der den zuverlässigsten Kanal zwischen zwei gegebenen Knoten bestimmt. 24.3-7 Sei G = (V, E) ein gewichteter gerichteter Graph mit der positiven Gewichtsfunktion w : E → {1, 2, . . . , W }, wobei W eine positive ganze Zahl ist. Setzen Sie voraus, dass je zwei Knoten kürzeste Pfade vom Startknoten s aus mit unterschiedlichem Gewicht haben. Nehmen Sie an, wir würden einen ungewichteten gerichteten Graphen G = (V ∪ V , E ) definieren, indem wir jede Kante (u, v) ∈ E durch w(u, v) aufeinander folgende Kanten vom Gewicht 1 ersetzen. Wie viele Knoten hat G ? Nehmen Sie nun an, wir würden eine Breitensuche auf G laufen lassen. Zeigen Sie, dass die Reihenfolge, in der die Breitensuche auf G die Knoten aus V schwarz färbt, die gleiche ist, in der Dijkstras Algorithmus angewendet auf G die Knoten von V aus der Prioritätswarteschlange extrahiert. 24.3-8 Sei G = (V, E) ein gewichteter gerichteter Graph mit der nichtnegativen Gewichtsfunktion w : E → {0, 1, . . . , W } für eine nichtnegative ganze Zahl W . Modifizieren Sie Dijkstras Algorithmus so, dass er die kürzesten Pfade von einem gegebenen Startknoten s aus in Zeit O(W V + E) berechnet. 24.3-9 Modifizieren Sie Ihren Algorithmus aus Übung 24.3-8 so, dass er in Zeit O((V + E) lg W ) läuft. (Hinweis: Wie viele unterschiedliche Schätzungen des kürzesten Pfades kann es in V − S zu jedem Zeitpunkt geben?) 24.3-10 Gegeben sei ein gewichteter gerichteter Graph G = (V, E), in dem die aus dem Startknoten s austretenden Kanten negative Gewichte haben können. Alle anderen Kanten haben jedoch nichtnegative Gewichte. Setzen Sie zudem voraus, dass es keine Zyklen mit negativem Gewicht gibt. Zeigen Sie, dass Dijkstras Algorithmus in diesem Graphen die kürzesten Pfade vom Startknoten s aus korrekt bestimmt.
24.4 Differenzbedingungen und kürzeste Pfade
24.4
677
Differenzbedingungen und kürzeste Pfade
Kapitel 29 betrachtet das allgemeine Problem der linearen Programmierung. Dieses Problem besteht darin, eine lineare Funktion zu optimieren, die eine Reihe von linearen Ungleichungen erfüllen muss. In diesem Abschnitt untersuchen wir einen Spezialfall der linearen Programmierung, den wir auf das kürzeste-Pfade-Problem mit einem Startknoten zurückführen können. Das resultierende kürzeste-Pfade-Problem können wir dann mithilfe des Bellman-Ford-Algorithmus lösen, womit wir das lineare Programm lösen.
Lineare Programmierung Beim allgemeinen Problem der linearen Programmierung sind eine m × n Matrix A, ein Vektor b der Länge m und ein Vektor c der Länge n gegeben. Wir wollen einen n Vektor x der Länge n bestimmen, der die Zielfunktion i=1 ci xi maximiert, wobei die durch Ax ≤ b gegebenen Nebenbedingungen erfüllt sein müssen. Wenngleich der Simplexalgorithmus, der in Kapitel 29 behandelt wird, nicht immer in einer Laufzeit, die polynomiell in der Eingabegröße ist, läuft, gibt es andere Algorithmen zum Lösen linearer Programme, die nur polynomielle Zeit benötigen. Wir geben zwei Gründe an, warum es wichtig ist, zu verstehen, wie lineare Programme aufgebaut sind. Erstens, wenn wir wissen, dass sich ein gegebenes Problem als lineares Programm polynomieller Größe darstellen lässt, dann haben wir auch sofort einen Algorithmus, der polynomielle Zeit benötigt, um das Problem zu lösen. Zweitens gibt es viele Spezialfälle linearer Programme, für die schnellere Algorithmen existieren. Beispielsweise sind das kürzeste-Pfad-Problem für ein gegebenes Knotenpaar (Übung 24.4-4) und das maximale-Fluss-Problem (Übung 26.1-5) Spezialfälle linearer Programme. Manchmal kümmern wir uns nicht wirklich um die Zielfunktion, sondern wollen einfach nur eine zulässige Lösung bestimmen, d. h. einen Vektor x, der die Bedingung Ax ≤ b erfüllt, oder aber zeigen, dass keine zulässige Lösung existiert. Wir wollen uns auf ein derartiges Zulässigkeitsproblem konzentrieren.
Systeme von Differenzbedingungen In einem System von Differenzbedingungen enthält jede Zeile der Matrix A eine 1 und eine −1, alle anderen Elemente von A sind 0. Die durch Ax ≤ b gegebenen Nebenbedingungen sind also ein System von m Differenzbedingungen mit n Unbekannten. Jede Bedingung ist eine einfache lineare Ungleichung der Form xj − xi ≤ bk mit 1 ≤ i, j ≤ n, i = j, und 1 ≤ k ≤ m. Betrachten Sie zum Beispiel das Problem, den aus fünf Komponenten bestehenden
678
24 Kürzeste Pfade von einem Startknoten aus
Vektor x = (xi ) so zu bestimmen, dass ⎛ ⎜ ⎜ ⎜ ⎜ ⎜ ⎜ ⎜ ⎜ ⎜ ⎝
1 −1 0 0 0 1 0 0 0 −1 0 1 0 0 −1 −1 0 1 0 0 −1 0 0 1 0 0 0 −1 1 0 0 0 −1 0 1 0 0 0 −1 1
⎞
⎛
⎟⎛ ⎞ ⎜ ⎟ x1 ⎜ ⎟ ⎜ ⎟ ⎜ x2 ⎟ ⎜ ⎟⎜ ⎟ ⎜ ⎟ ⎜ x3 ⎟ ≤ ⎜ ⎟⎝x ⎠ ⎜ ⎟ ⎜ 4 ⎟ x ⎜ 5 ⎠ ⎝
0 −1 1 5 4 −1 −3 −3
⎞ ⎟ ⎟ ⎟ ⎟ ⎟ ⎟ . ⎟ ⎟ ⎟ ⎠
erfüllt ist. Dieses Problem ist äquivalent zu dem Problem, Werte für die Unbekannten xi für i = 1, 2, . . . , 5 so zu bestimmen, dass die folgenden acht Differenzbedingungen erfüllt sind: x1 − x2 x1 − x5 x2 − x5 x3 − x1 x4 − x1 x4 − x3 x5 − x3 x5 − x4
≤ ≤ ≤ ≤ ≤ ≤ ≤ ≤
0 −1 1 5 4 −1 −3 −3
, , , , , , , .
(24.3) (24.4) (24.5) (24.6) (24.7) (24.8) (24.9) (24.10)
Eine Lösung dieses Problems ist x = (−5, −3, 0, −1, −4), was wir einfach überprüfen können, indem wir diese Werte in die Ungleichungen einsetzen. Tatsächlich hat dieses Problem mehrere Lösungen. Eine andere Lösung ist x = (0, 2, 5, 4, 1). Diese beiden Lösungen sind verwandt: Jede Komponente von x ist um 5 größer als die entsprechende Komponente von x. Dies ist kein Zufall. Lemma 24.8 Sei x = (x1 , x2 , . . . , xn ) eine Lösung eines Systems Ax ≤ b von Differenzbedingungen und d eine beliebige Konstante. Dann ist x + d = (x1 + d, x2 + d, . . . , xn + d) ebenfalls eine Lösung von Ax ≤ b. Beweis: Für jedes Paar xi und xj gilt (xj + d) − (xi + d) = xj − xi . Wenn daher x die Bedingung Ax ≤ b erfüllt, so gilt dies auch für x + d. Systeme von Differenzbedingungen kommen in vielen verschiedenen Anwendungen vor. Zum Beispiel können die Unbekannten xi die Zeitpunkte darstellen, zu denen bestimmte Ereignisse eintreten. Jede Bedingung sagt aus, dass zwischen zwei Ereignissen mindestens oder höchstens eine bestimmte Zeitspanne liegt. Die Ereignisse können beispielsweise Aufgaben sein, die während der Montage eines Produktes ausgeführt werden müssen.
24.4 Differenzbedingungen und kürzeste Pfade
679
Wenn wir zur Zeit x1 ein Bindemittel anwenden, das 2 Stunden braucht, um auszuhärten, und wir warten müssen, bis es ausgehärtet ist, bevor wir einen Gegenstand befestigen können, dann erhalten wir die Bedingung x2 ≥ x1 + 2, was äquivalent zu x1 − x2 ≤ −2 ist. Alternativ könnten wir fordern, dass der Gegenstand befestigt wird, nachdem das Bindemittel aufgebracht wurde, aber nicht später als bis zu dem Zeitpunkt, zu dem das Bindemittel zur Hälfte ausgehärtet ist. In diesem Fall erhalten wir die beiden Nebenbedingungen x2 ≥ x1 und x2 ≤ x1 + 1, was äquivalent zu x1 − x2 ≤ 0 und x2 − x1 ≤ 1 ist.
Bedingungsgraphen Wir können Systeme von Differenzbedingungen graphentheoretisch interpretieren. In einem System Ax ≤ b von Differenzbedingungen können wir die m × n-Matrix A als Transponierte einer Inzidenzmatrix (siehe Übung 22.1-7) eines Graphen mit n Knoten und m Kanten auffassen. Jeder Knoten vi , i = 1, 2, . . . , n, des Graphen entspricht einer der unbekannten Variablen xi . Jede gerichtete Kante des Graphen entspricht einer der m Ungleichungen, in der jeweils zwei Variablen vorkommen. Formal definieren wir für ein gegebenes System Ax ≤ b von Differenzbedingungen den zugehörigen Bedingungsgraphen als einen gewichteten gerichteten Graphen G = (V, E) mit V = {v0 , v1 , . . . , vn } und E = {(vi , vj ) : xj − xi ≤ bk ist eine Bedingung} ∪ {(v0 , v1 ), (v0 , v2 ), (v0 , v3 ), . . . , (v0 , vn )} . Der Bedingungsgraph enthält einen zusätzlichen Knoten v0 , um, wie wir später sehen werden, zu garantieren, dass der Graph einen Knoten enthält, von dem alle anderen Knoten erreichbar sind. Die Knotenmenge V besteht also aus je einem Knoten für jede Variable und einem zusätzlichen Knoten v0 . Die Kantenmenge E besteht aus je einer Kante für jede Differenzbedingung und jeweils einer Kante (v0 , vi ) für jede Variable xi . Wenn xj − xi ≤ bk eine Differenzbedingung ist, dann ist das Gewicht der Kante (vi , vj ) durch w(vi , vj ) = bk gegeben. Das Gewicht aller aus v0 austretenden Kanten ist 0. Abbildung 24.8 zeigt den Bedingungsgraphen für das System der Differenzbedingungen (24.3)–(24.10). Das folgende Theorem zeigt, dass wir eine Lösung für ein System von Differenzbedingungen finden können, indem wir die Gewichte kürzester Pfade im zugehörigen Bedingungsgraph bestimmen. Theorem 24.9 Sei G = (V, E) der zu einem System Ax ≤ b von Differenzbedingungen gehörende Bedingungsgraph. Falls G keine Zyklen mit negativem Gewicht enthält, dann ist x = (δ(v0 , v1 ), δ(v0 , v2 ), δ(v0 , v3 ), . . . , δ(v0 , vn ))
(24.11)
680
24 Kürzeste Pfade von einem Startknoten aus
0
v1 –5
0 v5 –4
–1
0 1
0 v0 0
–3
–3
–3 v2
5
4 0
–1 v4
–1
0 v3
0 Abbildung 24.8: Der zu dem System (24.3)–(24.10) korrespondierende Bedingungsgraph. Der Wert von δ(v0 , vi ) ist innerhalb des Knotens vi annotiert. Eine zulässige Lösung des Systems ist (−5, −3, 0, −1, −4).
eine zulässige Lösung des Systems. Falls G einen Zyklus mit negativem Gewicht enthält, dann besitzt das System keine zulässige Lösung. Beweis: Wir zeigen zunächst, dass die Gleichung (24.11) eine zulässige Lösung angibt, wenn der Bedingungsgraph keine Zyklen mit negativem Gewicht enthält. Betrachten Sie eine beliebige Kante (vi , vj ) ∈ E. Wegen der Dreiecksungleichung gilt δ(v0 , vj ) ≤ δ(v0 , vi ) + w(vi , vj ), was äquivalent zu δ(v0 , vj ) − δ(v0 , vi ) ≤ w(vi , vj ) ist. Somit erfüllen xi = δ(v0 , vi ) und xj = δ(v0 , vj ) die Differenzbedingung xj − xi ≤ w(vi , vj ), die der Kante (vi , vj ) entspricht. Nun zeigen wir, dass das System der Differenzbedingungen keine zulässige Lösung besitzt, wenn der Bedingungsgraph einen Zyklus mit negativem Gewicht enthält. Ohne Beschränkung der Allgemeinheit sei c = v1 , v2 , . . . , vk mit v1 = vk der Zyklus mit negativem Gewicht. (Der Knoten v0 kann nicht auf dem Zyklus c liegen, da er keine eintretenden Kanten besitzt.) Der Zyklus c entspricht den Differenzbedingungen x2 − x1 ≤ w(v1 , v2 ) , x3 − x2 ≤ w(v2 , v3 ) , .. . xk−1 − xk−2 ≤ w(vk−2 , vk−1 ) , xk − xk−1 ≤ w(vk−1 , vk ) . Wir wollen annehmen, dass es eine Belegung von x gäbe, die alle k Ungleichungen erfüllt, und führen diese Annahme zum Widerspruch. Die Lösung muss auch die Ungleichung erfüllen, die durch Aufsummieren aller Ungleichungen entsteht. Wenn wir die linken Seiten summieren, dann wird jede Variable xi einmal addiert und einmal wieder subtrahiert (es sei daran erinnert, dass aus der Gleichheit der Knoten v1 = vk die
24.4 Differenzbedingungen und kürzeste Pfade
681
Gleichheit x1 = xk folgt). Die linke Seite der durch Summation entstehenden Ungleichung ist also 0. Die rechte Seite summiert sich zu w(c), sodass wir die Ungleichung 0 ≤ w(c) erhalten. Da aber c ein Zyklus mit negativem Gewicht ist, gilt w(c) < 0, und wir erhalten den Widerspruch 0 ≤ w(c) < 0.
Lösen von Systemen von Differenzbedingungen Theorem 24.9 sagt aus, dass wir den Bellman-Ford-Algorithmus verwenden können, um ein System von Differenzbedingungen zu lösen. Da es vom Startknoten v0 Kanten zu jedem anderen Knoten des Bedingungsgraphen gibt, ist jeder Zyklus des Bedingungsgraphen mit negativem Gewicht von v0 aus erreichbar. Wenn der Bellman-FordAlgorithmus den Wert wahr zurückgibt, dann liefern die berechneten Gewichte der kürzesten Pfade eine zulässige Lösung des Systems. In Abbildung 24.8 zum Beispiel erhalten wir so die zulässige Lösung x = (−5, −3, 0, −1, −4), und nach Lemma 24.8 ist x = (d − 5, d − 3, d, d − 1, d − 4) für eine beliebige Konstante d ebenfalls eine Lösung. Wenn der Bellman-Ford-Algorithmus den Wert falsch zurückgibt, dann hat das System der Differenzbedingungen keine zulässige Lösung. Ein System von Differenzbedingungen mit m Bedingungen und n Unbekannten erzeugt einen Graphen mit n + 1 Knoten und n + m Kanten. Daher können wir das System mit dem Bellman-Ford-Algorithmus in Zeit O((n + 1)(n + m)) = O(n2 + nm) lösen. In Übung 24.4-5 sollen Sie den Algorithmus so modifizieren, dass er in Zeit O(nm) läuft, selbst wenn m wesentlich kleiner als n ist.
Übungen 24.4-1 Bestimmen Sie für das folgende System von Differenzbedingungen eine zulässige Lösung, oder zeigen Sie, dass es keine zulässigen Lösungen besitzt: x1 − x2 x1 − x4 x2 − x3 x2 − x5 x2 − x6 x3 − x6 x4 − x2 x5 − x1 x5 − x4 x6 − x3
≤ ≤ ≤ ≤ ≤ ≤ ≤ ≤ ≤ ≤
1 −4 2 7 5 10 2 −1 3 −8
, , , , , , , , , .
682
24 Kürzeste Pfade von einem Startknoten aus
24.4-2 Bestimmen Sie für das folgende System von Differenzbedingungen eine zulässige Lösung, oder zeigen Sie, dass es keine zulässigen Lösungen besitzt: x1 − x2 x1 − x5 x2 − x4 x3 − x2 x4 − x1 x4 − x3 x4 − x5 x5 − x3 x5 − x4
≤ ≤ ≤ ≤ ≤ ≤ ≤ ≤ ≤
4 5 −6 1 3 5 10 −4 −8
, , , , , , , , .
24.4-3 Können Gewichte kürzester Pfade von dem neuen Knoten v0 in einem Bedingungsgraphen positiv sein? Begründen Sie Ihre Antwort. 24.4-4 Formulieren Sie das Problem, einen kürzesten Pfad für ein gegebenes Knotenpaar zu bestimmen, als lineares Programm. 24.4-5 Zeigen Sie, wie wir den Bellman-Ford-Algorithmus geringfügig so modifizieren können, dass seine Laufzeit in O(nm) ist, wenn wir ihn einsetzen, um ein System von Differenzbedingungen mit m Ungleichungen und n Unbekannten zu lösen. 24.4-6 Nehmen Sie an, wir wollten zusätzlich zu einem System von Differenzbedingungen Gleichheitsbedingungen der Form xi = xj + bk bearbeiten können. Zeigen Sie, wie wir den Bellman-Ford-Algorithmus anpassen können, um diese Variante von Systemen zu lösen. 24.4-7 Zeigen Sie, wie wir ein System von Differenzbedingungen durch einen BellmanFord-ähnlichen Algorithmus lösen können, der auf einem Bedingungsgraphen ohne den zusätzlichen Knoten v0 arbeitet. 24.4-8∗Sei Ax ≤ b ein System von m Differenzbedingungen in n Unbekannten. Zeigen Sie, dass der Bellman-Ford-Algorithmus angewendet auf den Bedingungsgran phen die Größe i=1 xi unter Einhaltung der Bedingung Ax ≤ b und xi ≤ 0 für alle xi maximiert. 24.4-9∗ Zeigen Sie, dass der Bellman-Ford-Algorithmus angewendet auf den Bedingungsgraphen eines Systems Ax ≤ b von Differenzbedingungen die Größe (max {xi } − min {xi }) unter Einhaltung der Bedingung Ax ≤ b minimiert. Erklären Sie, warum uns diese Tatsache sehr gelegen kommt, wenn wir den Algorithmus einsetzen, um Montageaufgaben zu planen. 24.4-10 Nehmen Sie an, jede Zeile der Matrix A eines linearen Programms würde einer Differenzbedingung mit jeweils nur einer Variablen entsprechen; jede Bedingung hätte also entweder die Form xi ≤ bk oder die Form −xi ≤ bk . Zeigen Sie,
24.5 Beweise der Eigenschaften kürzester Pfade
683
wie wir den Bellman-Ford-Algorithmus anpassen müssen, um diese Variante eines Differenzensystems zu lösen. 24.4-11 Geben Sie einen effizienten Algorithmus an, der ein System Ax ≤ b von Differenzbedingungen löst, wenn alle Komponenten des Vektors b reellwertig und alle Unbekannten xi ganzzahlig sind. 24.4-12∗ Geben Sie einen effizienten Algorithmus an, der ein System Ax ≤ b von Differenzbedingungen löst, wenn alle Komponenten des Vektors b reellwertig und eine gegebene Teilmenge der Unbekannten xi , jedoch nicht unbedingt alle, ganzzahlig sind.
24.5
Beweise der Eigenschaften kürzester Pfade
Im Verlauf dieses Kapitels haben wir uns in unseren Korrektheitsbeweisen wiederholt auf die Dreiecksungleichung, die Eigenschaft der oberen Schranke, die KeinPfad-Eigenschaft, die Konvergenzeigenschaft, die Pfadrelaxationseigenschaft und die Vorgängerteilgraph-Eigenschaft berufen. Wir haben diese Eigenschaften zu Beginn des Kapitels ohne Beweis angegeben. In diesem Abschnitt wollen wir sie beweisen.
Die Dreiecksungleichung Bei der Behandlung der Breitensuche (Abschnitt 22.2) haben wir eine einfache Eigenschaft kürzester Abstände in ungewichteten Graphen (Lemma 22.1) bewiesen. Die Dreiecksungleichung verallgemeinert diese Eigenschaft auf den Fall gewichteter Graphen.
Lemma 24.10: (Dreiecksungleichung) Sei G = (V, E) ein gewichteter gerichteter Graph mit der Gewichtsfunktion w : E → R und dem Startknoten s. Dann gilt für jede Kante (u, v) ∈ E δ(s, v) ≤ δ(s, u) + w(u, v) .
Beweis: Nehmen Sie an, p wäre ein kürzester Pfad vom Startknoten s zum Knoten v. Dann hat p kein größeres Gewicht als irgendein anderer Pfad von s nach v. Insbesondere hat der Pfad p kein größeres Gewicht als derjenige Pfad, der aus dem kürzesten Pfad von s nach u und der Kante (u, v) besteht. In Übung 24.5-3 sollen Sie den Fall betrachten, dass es keinen kürzesten Pfad von s nach v gibt.
684
24 Kürzeste Pfade von einem Startknoten aus
Auswirkungen der Relaxation auf die Schätzungen kürzester Pfade Die nächsten Lemmata beschreiben, wie Schätzungen kürzester Pfade beeinflusst werden, wenn wir eine Folge von Relaxationsschritten auf die Kanten eines gewichteten gerichteten Graphen anwenden, der durch die Prozedur Initialize-Single-Source initialisiert wurde. Lemma 24.11: (Eigenschaft der oberen Schranke) Sei G = (V, E) ein gewichteter gerichteter Graph mit der Gewichtsfunktion w : E → R und dem Startknoten s. Der Graph sei durch die Prozedur InitializeSingle-Source(G, s) initialisiert worden. Dann gilt v.d ≥ δ(s, v) für alle v ∈ V , und diese Invariante wird über jede beliebige Folge von Relaxationsschritten auf den Kanten von G aufrechterhalten. Außerdem ändert sich v.d nicht mehr, wenn es einmal seine untere Schranke δ(s, v) erreicht hat. Beweis: Wir beweisen die Gültigkeit der Invariante v.d ≥ δ(s, v) für alle Knoten v ∈ V durch Induktion über die Anzahl der Relaxationsschritte. Die Ungleichungen v.d ≥ δ(s, v) sind mit Sicherheit nach der denn v.d = ∞ impliziert v.d ≥ δ(s, v) für alle Knoten v ∈ s.d = 0 ≥ δ(s, s) (beachten Sie, dass δ(s, s) = −∞ gilt, wenn s negativem Gewicht liegt, und ansonsten gleich 0 ist). Somit ist bewiesen.
Initialisierung erfüllt, V − {s}, und es gilt auf einem Zyklus mit der Induktionsanfang
Zum Beweis des Induktionsschrittes betrachten Sie die Relaxation einer Kante (u, v). Nach der Induktionsannahme gilt vor der Relaxation der Kante für alle x ∈ V die Ungleichung x.d ≥ δ(s, x). Der einzige d-Wert, der sich ändern kann, ist v.d . Wenn er sich ändert, gilt v.d = u.d + w(u, v) ≥ δ(s, u) + w(u, v) ≥ δ(s, v)
(wegen der Induktionsannahme) (wegen der Dreiecksungleichung) ,
sodass die Invariante weiterhin gilt. Um zu sehen, dass sich der Wert von v.d nicht mehr ändert, wenn einmal v.d = δ(s, v) gilt, müssen Sie nur bemerken, dass v.d in diesem Fall mehr nicht kleiner werden kann, da wir soeben gezeigt haben, dass v.d ≥ δ(s, v) gilt. Der Wert kann auch nicht zunehmen, da Relaxationsschritte die d-Werte nicht erhöhen.
Korollar 24.12: (Kein-Pfad-Eigenschaft) Setzen Sie voraus, dass in einem gewichteten gerichteten Graphen G = (V, E) mit der Gewichtsfunktion w : E → R kein Pfad existiert, der den Startknoten s mit einem
24.5 Beweise der Eigenschaften kürzester Pfade
685
gegebenen Knoten v ∈ V verbindet. Dann gilt, nachdem der Graph durch die Prozedur Initialize-Single-Source(G, s) initialisiert wurde, v.d = δ(s, v) = ∞, und diese Gleichung bleibt als Invariante über beliebige Folgen von Relaxationsschritten auf den Kanten von G erhalten. Beweis: Wegen der Eigenschaft der oberen Schranke gilt immer ∞ = δ(s, v) ≤ v.d und somit v.d = ∞ = δ(s, v).
Lemma 24.13 Sei G = (V, E) ein gewichteter gerichteter Graph mit der Gewichtsfunktion w : E → R und sei (u, v) eine Kante aus E. Dann gilt unmittelbar nach dem Relaxieren der Kante (u, v) durch die Prozedur Relax(u, v, w) die Ungleichung v.d ≤ u.d + w(u, v). Beweis: Wenn unmittelbar vor dem Relaxieren der Kante (u, v) die Ungleichung v.d > u.d + w(u, v) gilt, dann gilt danach v.d = u.d + w(u, v). Wenn dagegen vor dem Relaxieren v.d ≤ u.d + w(u, v) gilt, dann ändern sich weder u.d noch v.d , sodass auch danach v.d ≤ u.d + w(u, v) gilt.
Lemma 24.14: (Konvergenzeigenschaft) Sei G = (V, E) ein gewichteter gerichteter Graph mit der Gewichtsfunktion w : E → R, s ein Startknoten und s ; u → v ein kürzester Pfad in G für zwei Knoten u, v ∈ V . Setzen Sie voraus, dass der Graph G durch die Prozedur Initialize-Single-Source(G, s) initialisiert und anschließend eine Folge von Relaxationsschritten auf den Kanten von G ausgeführt wurde, bei denen die Prozedur Relax(u, v, w) aufgerufen wurde. Falls zu irgendeinem Zeitpunkt vor diesem Aufruf u.d = δ(s, u) war, dann gilt zu jedem Zeitpunkt nach dem Aufruf v.d = δ(s, v). Beweis: Falls zu einem Zeitpunkt vor dem Relaxieren der Kante (u, v) die Gleichung u.d = δ(s, u) erfüllt war, dann gilt diese Gleichung wegen der Eigenschaft der oberen Schranke auch zu jedem späteren Zeitpunkt. Insbesondere ist nach dem Relaxieren der Kante (u, v) v.d ≤ u.d + w(u, v) = δ(s, u) + w(u, v) = δ(s, v)
(nach Lemma 24.13) (nach Lemma 24.1) .
Wegen der Eigenschaft der oberen Schranke gilt v.d ≥ δ(s, v), woraus wir schlussfolgern, dass v.d = δ(s, v) ist und diese Gleichung danach erhalten bleibt.
686
24 Kürzeste Pfade von einem Startknoten aus
Lemma 24.15: (Pfadrelaxationseigenschaft) Sei G = (V, E) ein gewichteter gerichteter Graph mit der Gewichtsfunktion w : E → R und s ∈ V ein Startknoten. Betrachten Sie einen beliebigen kürzesten Pfad p = v0 , v1 , . . . , vk von s = v0 nach vk . Wenn G durch die Prozedur Initialize-Single-Source(G, s) initialisiert wird und anschließend eine Folge von Relaxationsschritten ausgeführt wird, die die Kanten in der Reihenfolge (v0 , v1 ), (v1 , v2 ), . . ., (vk−1 , vk ) relaxiert, dann gilt nach diesen Relaxationen und zu jedem späteren Zeitpunkt die Gleichung vk .d = δ(s, vk ). Diese Eigenschaft gilt unabhängig davon, welche anderen Kantenrelaxationen ausgeführt werden, einschließlich denen, die zwischen den Relaxationen der Kanten von p erfolgen. Beweis: Wir zeigen durch Induktion, dass vi .d = δ(s, vi ) gilt, nachdem die i-te Kante des Pfades p relaxiert wurde. Für den Induktionsanfang ist i = 0, und bevor irgendwelche Kanten von p relaxiert wurden, gilt wegen der Initialisierung v0 .d = s.d = 0 = δ(s, s). Wegen der Eigenschaft der oberen Schranke ändert sich der Wert von s.d nach der Initialisierung nicht. Für den Induktionsschritt nehmen wir an, dass vi−1 .d = δ(s, vi−1 ) gelten würde und betrachten die Relaxation der Kante (vi−1 , vi ). Wegen der Konvergenzeigenschaft gilt nach dieser Relaxation vi .d = δ(s, vi ), und diese Gleichung bleibt zu jedem späteren Zeitpunkt erhalten.
Relaxation und Bäume kürzester Pfade Wir zeigen nun, dass, wenn die Schätzungen der kürzesten Pfade nach einer Folge von Relaxationen die Gewichte der kürzesten Pfade angenommen haben, dann ist der Vorgängerteilgraph, der durch die resultierenden π-Werte induziert wird, ein Baum kürzester Pfade für G. Wir beginnen mit dem folgenden Lemma, welches zeigt, dass der Vorgängerteilgraph stets einen gerichteten Baum bildet, dessen Wurzel die Quelle ist. Lemma 24.16 Sei G = (V, E) ein gewichteter gerichteter Graph mit der Gewichtsfunktion w : E → R und s ∈ V ein Startknoten. Setzen sie desweiteren voraus, dass G keine Zyklen mit negativem Gewicht enthält, die von s aus erreichbar sind. Dann bildet, nachdem der Graph durch die Prozedur Initialize-Single-Source(G, s) initialisiert wurde, der Vorgängerteilgraph Gπ einen gerichteten Baum mit der Wurzel s, und jede Folge von Relaxationsschritten auf den Kanten von G erhält diese Eigenschaft als Invariante. Beweis: Anfangs ist der einzige Knoten in Gπ der Startknoten und das Lemma ist trivialerweise erfüllt. Betrachten Sie einen Vorgängerteilgraphen Gπ nach einer Folge von Relaxationsschritten. Wir werden zunächst beweisen, dass Gπ azyklisch ist. Dazu führen wir die Annahme zum Widerspruch, dass ein Relaxationsschritt einen Zyklus im Graphen Gπ erzeugen würde. Dieser Zyklus sei c = v0 , v1 , . . . , vk mit vk = v0 . Dann
24.5 Beweise der Eigenschaften kürzester Pfade
687
gilt vi .π = vi−1 für i = 1, 2, . . . , k, und ohne Beschränkung der Allgemeinheit können wir voraussetzen, dass die relaxierte Kante (vk−1 , vk ) den Zyklus in Gπ entstanden ließ. Wir behaupten, dass alle Knoten auf dem Zyklus c vom Startknoten s aus erreichbar sind. Warum? Jeder Knoten auf c hat einen Vorgänger, der nicht nil ist, und daher ist jedem Knoten auf c zusammen mit dem von nil verschiedenen Wert von π eine endliche Schätzung des kürzesten Pfades zugewiesen worden. Wegen der Eigenschaft der oberen Schranke ist für jeden Knoten auf dem Zyklus c das Gewicht seiner kürzesten Pfade endlich, was bedeutet, dass er von s aus erreichbar ist. Wir werden die Schätzungen der kürzesten Pfade auf c unmittelbar vor dem Aufruf Relax(vk−1 , vk , w) untersuchen und zeigen, dass c ein Zyklus mit negativem Gewicht ist. Dies würde unserer Voraussetzung widersprechen, dass G keinen Zyklus mit negativem Gewicht enthält, der vom Startknoten aus erreichbar ist. Unmittelbar vor dem Aufruf gilt vi .π = vi−1 für i = 1, 2, . . . , k − 1. Also erfolgte für i = 1, 2, . . . , k − 1 die letzte Aktualisierung von vi .d durch die Zuweisung vi .d = vi−1 .d + w(vi−1 , vi ). Falls vi−1 .d sich seitdem geändert hat, ist sein Wert kleiner geworden. Daher gilt direkt vor dem Aufruf Relax(vk−1 , vk , w) vi .d ≥ vi−1 .d + w(vi−1 , vi )
für alle i = 1, 2, . . . , k − 1 .
(24.12)
Da vk .π durch den Aufruf geändert wird, gilt unmittelbar zuvor auch die strenge Ungleichung vk .d > vk−1 .d + w(vk−1 , vk ) . Summieren wir diese strenge Ungleichung und die Ungleichungen (24.12) auf, so erhalten wir die Summe der Schätzungen der kürzesten Pfade über den Zyklus c: k
vi .d >
i=1
k
(vi−1 .d + w(vi−1 , vi ))
i=1
=
k
vi−1 .d +
i=1
k
w(vi−1 , vi ) .
i=1
Es gilt jedoch k
vi .d =
i=1
k
vi−1 .d ,
i=1
da jeder Knoten auf dem Zyklus genau einmal in jeder Summe vorkommt. Diese Gleichung impliziert 0>
k
w(vi−1 , vi ) .
i=1
Das bedeutet, dass die Summe der Gewichte der Kanten des Zyklus negativ ist, was zu dem gewünschten Widerspruch führt.
688
24 Kürzeste Pfade von einem Startknoten aus x
s
z u
v
y
Abbildung 24.9: Illustration der Aussage, dass ein einfacher Pfad in Gπ vom Startknoten s zum Knoten v eindeutig ist. Wenn es zwei Pfade p1 (s ; u ; x → z ; v) und p2 (s ; u ; y → z ; v) mit x = y gäbe, dann müsste z. π = x und z. π = y gelten, was ein Widerspruch wäre.
Wir haben nun bewiesen, dass Gπ ein gerichteter azyklischer Graph ist. Um zu zeigen, dass er einen gerichteten Baum mit Wurzel s bildet, genügt es zu beweisen, dass es in Gπ für jeden Knoten v ∈ Vπ einen eindeutigen einfachen Pfad von s nach v gibt (siehe Übung B.5-2). Wir müssen zunächst zeigen, dass für jeden Knoten in Vπ ein Pfad von s aus existiert. Die Knoten in Vπ sind diejenigen, deren π-Wert nicht nil ist, sowie s. Die Idee besteht darin, durch Induktion zu beweisen, dass ein Pfad von s zu einem jeden Knoten aus Vπ existiert. Die Details überlassen wir der Übung 24.5-6. Um den Beweis des Lemmas abzuschließen, müssen wir noch zeigen, dass Gπ für jeden beliebigen Knoten v ∈ Vπ höchstens einen einfachen Pfad von s nach v enthält. Nehmen Sie das Gegenteil an, d. h. nehmen Sie, wie in Abbildung 24.9 illustriert, an, dass es zwei einfache Pfade von s zu einem Knoten v gäbe: den Pfad p1 , der in s ; u ; x → z ; v zerlegt werden kann, und den Pfad p2 , der in s ; u ; y → z ; v mit x = y zerlegt werden kann. Dann muss aber z.π = x und z.π = y gelten, woraus im Widerspruch zu unserer Annahme x = y folgt. Wir schließen daraus, dass Gπ einen einzigen einfachen Pfad von s nach v enthält und somit einen gerichteten Baum mit der Wurzel s bildet. Wir können nun zeigen, dass der Vorgängerteilgraph Gπ ein Baum kürzester Pfade ist, wenn nach einer Folge von Relaxationsschritten jedem Knoten das korrekte Gewicht seiner kürzesten Pfade zugewiesen ist. Lemma 24.17: (Vorgängerteilgraph-Eigenschaft) Sei G = (V, E) ein gewichteter gerichteter Graph mit der Gewichtsfunktion w : E → R und dem Startknoten s ∈ V . Setzen Sie voraus, dass G einen Zyklus mit negativem Gewicht enthält, der von s aus erreichbar ist. Nehmen Sie an, wir würden die Prozedur Initialize-Single-Source(G, s) aufrufen und dann eine Folge von Relaxationsschritten auf den Kanten von G ausführen, die für alle v ∈ V zu v.d = δ(s, v) führen. Dann ist der Vorgängerteilgraph Gπ ein Baum kürzester Pfade mit der Wurzel s. Beweis: Wir müssen beweisen, dass die drei Eigenschaften von Bäumen kürzester Pfade, die auf Seite 660 angegeben sind, für Gπ gelten. Um die erste Eigenschaft zu
24.5 Beweise der Eigenschaften kürzester Pfade
689
beweisen, müssen wir zeigen, dass Vπ die Menge der von s aus erreichbaren Knoten ist. Definitionsgemäß ist ein Gewicht des kürzesten Pfades δ(s, v) genau dann endlich, wenn v von s aus erreichbar ist. Daher sind die Knoten, die von s aus erreichbar sind, genau jene mit endlichen d-Werten. Einem Knoten v ∈ V − {s} ist aber genau dann ein endlicher Wert für v.d zugewiesen worden, wenn v.π = nil gilt. Also sind die Knoten in Vπ genau diejenigen, die von s aus erreichbar sind. Die zweite Eigenschaft folgt direkt aus Lemma 24.16. Es bleibt also noch die letzte Eigenschaft von Bäumen kürzester Pfade zu zeigen: Für p jeden Knoten v ∈ Vπ ist der in Gπ enthaltene, eindeutige einfache Pfad s ; v ein kürzester Pfad von s nach v in G. Sei p = v0 , v1 , . . . , vk mit v0 = s und vk = v. Für i = 1, 2, . . . , k gilt sowohl vi .d = δ(s, vi ) als auch vi .d ≥ vi−1 .d + w(vi−1 , vi ), woraus wir auf die Ungleichung w(vi−1 , vi ) ≤ δ(s, vi ) − δ(s, vi−1 ) schließen. Summieren wir die Gewichte entlang des Pfades p, so erhalten wir w(p) =
k
w(vi−1 , vi )
i=1
≤
k
(δ(s, vi ) − δ(s, vi−1 ))
i=1
= δ(s, vk ) − δ(s, v0 ) = δ(s, vk )
(da die Summe eine Teleskopreihe ist) (wegen δ(s, v0 ) = δ(s, s) = 0) .
Somit gilt w(p) ≤ δ(s, vk ). Da δ(s, vk ) eine untere Schranke für das Gewicht eines beliebigen Pfades von s nach vk ist, schließen wir, dass w(p) = δ(s, vk ) gilt, und p daher ein kürzester Pfad von s nach v = vk ist.
Übungen 24.5-1 Geben Sie für den gerichteten Graphen aus Abbildung 24.2 auf Seite 660 zwei weitere Bäume kürzester Pfade an. 24.5-2 Geben Sie ein Beispiel für einen gewichteten gerichteten Graphen G = (V, E) mit einer Gewichtsfunktion w : E → R und einem Startknoten s an, der die folgende Eigenschaft erfüllt: Für jede Kante (u, v) ∈ E gibt es einen Baum kürzester Pfade mit der Wurzel s, der (u, v) enthält, sowie einen anderen Baum kürzester Pfade mit der Wurzel s, der (u, v) nicht enthält. 24.5-3 Bauen Sie den Beweis von Lemma 24.10 so aus, dass auch die Fälle ∞ und −∞ abgedeckt sind. 24.5-4 Sei G = (V, E) ein gewichteter gerichteter Graph mit Startknoten s und sei G durch die Prozedur Initialize-Single-Source(G, s) initialisiert worden. Beweisen Sie, dass G keinen Zyklus mit negativem Gewicht enthält, wenn eine Folge von Relaxationsschritten das Attribut s.π auf einen von nil verschiedenen Wert setzt.
690
24 Kürzeste Pfade von einem Startknoten aus
24.5-5 Sei G = (V, E) ein gewichteter gerichteter Graph, der keine Kanten mit negativem Gewicht besitzt. Sei s ∈ V der Startknoten und wir erlauben, dass v.π der Vorgänger von v auf einem beliebigen kürzesten Pfad vom Startknoten s nach v ist, falls v ∈ V − {s} von s aus erreichbar ist, und sonst nil. Geben Sie ein Beispiel für einen solchen Graphen an, sowie eine Zuweisung von πWerten, die einen Zyklus in Gπ erzeugen. (Nach Lemma 24.16 kann eine solche Anordnung nicht durch Relaxationsschritte erzeugt werden.) 24.5-6 Sei G = (V, E) ein gewichteter gerichteter Graph mit einer Gewichtsfunktion w : E → R, der keine Zyklen mit negativem Gewicht enthält. Sei s ∈ V der Startknoten und sei G durch die Prozedur Initialize-Single-Source(G, s) initialisiert worden. Beweisen Sie, dass in Gπ für alle Knoten v ∈ Vπ ein Pfad von s nach v existiert und dass diese Eigenschaft als Invariante über beliebige Folgen von Relaxationen erhalten bleibt. 24.5-7 Sei G = (V, E) ein gewichteter gerichteter Graph, der keine Zyklen mit negativem Gewicht enthält. Sei s ∈ V der Startknoten und sei G durch die Prozedur Initialize-Single-Source(G, s) initialisiert worden. Beweisen Sie, dass es eine Folge von |V | − 1 Relaxationsschritten gibt, die für alle Knoten v ∈ V zu v.d = δ(s, v) führt. 24.5-8 Sei G ein beliebiger gewichteter gerichteter Graph mit einem Zyklus mit negativem Gewicht, der vom Startknoten s aus erreichbar ist. Geben Sie eine unendliche Folge von Relaxationen von Kanten aus G an, die bewirkt, dass jede Relaxation eine Änderung der Schätzung eines kürzesten Pfades bewirkt.
Problemstellungen 24-1 Yens Verbesserung des Bellman-Ford-Algorithmus Setzen Sie voraus, dass wir die Kantenrelaxationen in jedem Durchlauf des Bellman-Ford-Algorithmus wie folgt ordnen. Vor dem ersten Durchlauf legen wir eine beliebige lineare Anordnung v1 , v2 , . . . , v|V | der Knoten des Eingabegraphen G = (V, E) fest. Dann zerlegen wir die Kantenmenge E in Ef ∪ Eb mit Ef = {(vi , vj ) ∈ E : i < j} und Eb = {(vi , vj ) ∈ E : i > j}. (Setzen Sie voraus, dass G keine Schlingen enthält, sodass jede Kante entweder zu Ef oder zu Eb gehört.) Sei Gf = (V, Ef ) und Gb = (V, Eb ). a. Beweisen Sie, dass Gf azyklisch und v1 , v2 , . . . , v|V | eine topologische Sortierung auf Gf ist und dass Gb azyklisch und v|V | , v|V |−1 , . . . , v1 eine topologische Sortierung auf Gb ist. Nehmen Sie an, wir würden jeden Durchlauf des Bellman-Ford-Algorithmus in folgender Weise implementieren. Wir besuchen die Knoten in der Reihenfolge v1 , v2 , . . . , v|V | und relaxieren die Kanten von Ef , die aus dem jeweiligen Knoten austreten. Dann besuchen wir die Knoten in der Reihenfolge v|V | , v|V |−1 , . . . , v1 und relaxieren die Kanten von Eb , die aus dem jeweiligen Knoten austreten.
Problemstellungen zu Kapitel 24
691
b. Beweisen Sie, dass mit diesem Vorgehen nach nur |V | /2 Durchläufen v.d = δ(s, v) für alle Knoten v ∈ V gilt, falls G keinen Zyklus mit negativem Gewicht enthält, der vom Startknoten s aus erreichbar ist. c. Verbessert dieses Vorgehen die asymptotische Laufzeit des Bellman-FordAlgorithmus? 24-2 Kisten ineinander schachteln Eine d-dimensionale Kiste mit den Abmessungen (x1 , x2 , . . . , xd ) passt in eine andere Kiste mit den Abmessungen (y1 , y2 , . . . , yd ), falls eine Permutation π von {1, 2, . . . , d} existiert, für die xπ(1) < y1 , xπ(2) < y2 , . . . , xπ(d) < yd gilt. a. Zeigen Sie, dass die Relation des „Hineinpassens“ transitiv ist. b. Geben Sie einen effizienten Algorithmus an, der überprüft, ob eine d-dimensionale Kiste in eine andere hineinpasst. c. Gegeben sei eine Menge {B1 , B2 , . . . , Bn } von n d-dimensionalen Kisten. Geben Sie einen effizienten Algorithmus an, um die längste Folge Bi1 , Bi2 , . . . , Bik von Kisten zu bestimmen, die ineinander geschachtelt werden können, d. h. sodass für j = 1, 2, . . . , k − 1 Kiste Bij in Bij+1 hineinpasst. Geben Sie die Laufzeit Ihres Algorithmus als Funktion in n und d an. 24-3 Arbitrage-Geschäfte In Arbitrage-Geschäften werden Kursunterschiede ausgenutzt, um aus einer Einheit einer Währung mehr als eine Einheit der gleichen Währung zu machen. Nehmen sie beispielweise an, dass man für einen US-Dollar 49 indische Rupien kaufen kann, für eine indische Rupie 2 japanische Yen und für einen Yen 0,0107 US-Dollar. Dann kann ein Händler für einen US-Dollar durch Wechseln 49 · 2 · 0,0107 = 1,0486 US-Dollar erwerben, was einem Profit von 4,86 Prozent entspricht. Gegeben seien n Währungen c1 , c2 , . . . , cn und eine n × n-Tabelle R mit den Wechselkursen. Dabei bedeutet der Wert R[i, j], dass man für eine Einheit der Währung ci R[i, j] Einheiten der Währung cj kaufen kann. a. Geben Sie einen effizienten Algorithmus an, der feststellt, ob es eine Folge von Währungen gibt, für die R[i1 , i2 ] · R[i2 , i3 ] · · · R[ik−1 , ik ] · R[ik , i1 ] > 1 gilt. Analysieren Sie die Laufzeit Ihres Algorithmus. b. Geben Sie einen effizienten Algorithmus an, der eine solche Folge ausgibt, falls eine existiert. Analysieren Sie die Laufzeit Ihres Algorithmus. 24-4 Gabows Skalierungsalgorithmus für das kürzeste-Pfade-Problem mit einem Startknoten Ein Skalierungsalgorithmus löst ein Problem, indem für jeden relevanten Eingabewert (zum Beispiel ein Kantengewicht) zunächst nur das Bit mit der höchsten
692
24 Kürzeste Pfade von einem Startknoten aus Wertigkeit betrachtet wird. Dann verfeinert er die Anfangslösung, indem er die ersten zwei Bits mit der höchsten Wertigkeit betrachtet. Er betrachtet schrittweise immer mehr höherwertige Bits und verfeinert so die Lösung schrittweise, bis er alle Bits betrachtet und die korrekte Lösung berechnet hat. In dieser Problemstellung untersuchen wir einen Algorithmus für die Berechnung der kürzesten Pfade von einem Startknoten aus, der auf der Skalierung der Kanten beruht. Gegeben sei ein gerichteter Graph G = (V, E) mit nichtnegativen ganzzahligen Kantengewichten w. Sei W = max(u,v)∈E {w(u, v)}. Unser Ziel ist es, einen Algorithmus zu entwickeln, der in Zeit O(E lg W ) läuft. Wir setzen voraus, dass alle Knoten vom Startknoten aus erreichbar sind. Der Algorithmus deckt die Bits in der Binärdarstellung der Kantengewichte schrittweise auf, beginnend mit dem signifikantesten Bit bis zu dem Bit mit der niedrigsten Wertigkeit. Genauer, sei k = lg(W + 1) die Anzahl der Bits in der Binärdarstellung von W und sei wi (u, v) = w(u, v)/2k−i für i = 1, 2, . . . , k. Der Wert wi (u, v) ist also die „herunterskalierte“ Version von w(u, v), die durch die i signifikantesten Bits von w(u, v) gegeben ist. (Somit gilt wk (u, v) = w(u, v) für alle (u, v) ∈ E.) Zum Beispiel gilt für k = 5 und w(u, v) = 25 (25 hat die Binärdarstellung 11001) w3 (u, v) = 110 = 6. Ein weiteres Beispiel ist k = 5 und w(u, v) = 00100 = 4, in dem w3 (u, v) = 001 = 1 gilt. Lassen sie uns δi (u, v) als das Gewicht des kürzesten Pfades vom Knoten u zum Knoten v unter Verwendung der Gewichtsfunktion wi definieren. Es gilt also δk (u, v) = δ(u, v) für alle u, v ∈ V . Für einen gegebenen Startknoten s berechnet der Skalierungsalgorithmus für alle v ∈ V die Gewichte δ1 (s, v), anschließend für alle v ∈ V die Gewichte δ2 (s, v) und so weiter, bis schließlich für alle Knoten δk (s, v) berechnet ist. Wir setzen im Folgenden immer |E| ≥ |V | − 1 voraus und werden sehen, dass die Berechnung von δi aus δi−1 Zeit O(E) benötigt, sodass der gesamte Algorithmus eine Laufzeit von O(kE) = O(E lg W ) hat. a. Setzen Sie voraus, dass für alle Knoten v ∈ V die Ungleichung δ(s, v) ≤ |E| gilt. Zeigen Sie, dass wir in Zeit O(E) δ(s, v) für alle v ∈ V berechnen können. b. Zeigen Sie, dass wir in Zeit O(E) δ1 (s, v) für alle v ∈ V berechnen können. Lassen Sie uns jetzt darauf konzentrieren, wie wir δi aus δi−1 berechnen können. c. Beweisen Sie, dass für i = 2, 3, . . . , k entweder wi (u, v) = 2 wi−1 (u, v) oder wi (u, v) = 2 wi−1 (u, v) + 1 gilt. Beweisen Sie dann, dass 2 δi−1 (s, v) ≤ δi (s, v) ≤ 2 δi−1 (s, v) + |V | − 1 für alle v ∈ V gilt. d. Definieren Sie für i = 2, 3, . . . , k und alle (u, v) ∈ E w i (u, v) = wi (u, v) + 2 δi−1 (s, u) − 2 δi−1 (s, v) . Beweisen Sie, dass für i = 2, 3, . . . , k und alle u, v ∈ V das „neue“ Gewicht w i (u, v) der Kante (u, v) eine nichtnegative ganze Zahl ist.
Problemstellungen zu Kapitel 24
693
e. Definieren Sie nun δi (s, v) als das Gewicht des kürzesten Pfades von s nach v unter Verwendung der Gewichtsfunktion w i . Beweisen Sie, dass für i = 2, 3, . . . , k und alle v ∈ V δi (s, v) = δi (s, v) + 2 δi−1 (s, v) und δi (s, v) ≤ |E| gilt. f. Zeigen Sie, wie wir in Zeit O(E) für alle v ∈ V δi (s, v) aus δi−1 (s, v) berechnen können. Schlussfolgern Sie, dass wir δ(s, v) für alle v ∈ V in Zeit O(E lg W ) berechnen können. 24-5 Karps Algorithmus zur Bestimmung eines Zyklus mit minimalem mittleren Gewicht Sei G = (V, E) ein gerichteter Graph mit der Gewichtsfunktion w : E → R und sei n = |V |. Wir definieren das mittlere Gewicht eines Zyklus c = e1 , e2 , . . . , ek von Kanten aus E durch μ(c) =
k 1 w(ei ) . k i=1
Sei μ∗ = minc μ(c), wobei das Minimum über alle gerichteten Zyklen c von G genommen wird. Wir bezeichnen einen Zyklus c, für den μ(c) = μ∗ gilt, als Zyklus mit minimalem mittlerem Gewicht. Diese Problemstellung untersucht einen effizienten Algorithmus für die Bestimmung von μ∗ . Wir setzen ohne Beschränkung der Allgemeinheit voraus, dass alle Knoten v ∈ V von einem Startknoten s ∈ V aus erreichbar sind. Sei δ(s, v) das Gewicht eines kürzesten Pfades von s nach v und sei δk (s, v) das Gewicht eines kürzesten Pfades von s nach v, der aus genau k Kanten besteht. Falls es keinen Pfad von s nach v mit genau k Kanten gibt, gilt δk (s, v) = ∞. a. Zeigen Sie, dass im Falle μ∗ = 0 G keinen Zyklus mit negativem Gewicht enthält und für jeden Knoten v ∈ V die Gleichung δ(s, v) =
min
0≤k≤n−1
δk (s, v)
gilt. b. Zeigen Sie, dass im Falle μ∗ = 0 für alle Knoten v ∈ V max
0≤k≤n−1
δn (s, v) − δk (s, v) ≥0 n−k
gilt. (Hinweis: Verwenden Sie beide Eigenschaften aus Teil (a).) c. Sei c ein Zyklus mit dem Gewicht 0 und u, v ein beliebiges Knotenpaar auf c. Setzen Sie voraus, dass μ∗ = 0 gilt und das Gewicht des einfachen Pfades von u nach v auf dem Zyklus c gleich x ist. Beweisen Sie, dass δ(s, v) = δ(s, u)+x gilt. (Hinweis: Das Gewicht des einfachen Pfades von v nach u auf dem Zyklus ist −x.)
694
24 Kürzeste Pfade von einem Startknoten aus d. Zeigen Sie, dass im Falle μ∗ = 0 auf jedem Zyklus mit minimalem mittlerem Gewicht ein Knoten v existiert, für den max
0≤k≤n−1
δn (s, v) − δk (s, v) =0 n−k
gilt. (Hinweis: Zeigen Sie, wie wir einen kürzesten Pfad zu einem beliebigen Knoten auf einem Zyklus mit minimalem mittlerem Gewicht auf dem Zyklus fortsetzen können, sodass ein kürzester Pfad zum nächsten Knoten des Zyklus entsteht.) e. Zeigen Sie, dass im Falle μ∗ = 0 min
max
v∈V 0≤k≤n−1
δn (s, v) − δk (s, v) =0 n−k
gilt. f. Zeigen Sie, dass μ∗ sich um t erhöht, wenn wir eine Konstante t zu dem Gewicht jeder Kante von G addieren. Verwenden Sie diese Tatsache, um zu zeigen, dass μ∗ = min v∈V
δn (s, v) − δk (s, v) 0≤k≤n−1 n−k max
gilt. g. Geben Sie einen Algorithmus an, der μ∗ in Zeit O(V E) berechnet. 24-6 Bitonische kürzeste Pfade Eine Sequenz ist bitonisch, wenn sie zunächst monoton steigt und dann monoton fällt, oder sie nach einer Rotation, d. h. einer ringförmigen Verschiebung, diese Eigenschaft hat. Zum Beispiel sind die Sequenzen 1, 4, 6, 8, 3, −2, 9, 2, −4, −10, −5 und 1, 2, 3, 4 bitonisch, die Sequenz 1, 3, 12, 4, 2, 10 dagegen nicht. (Siehe Problemstellung 15-3 für das bitonische euklidische Problem des Handelsreisenden.) Gegeben sei ein gerichteter Graph G = (V, E) mit paarweise verschiedenen Kantengewichten. Wir wollen die kürzesten Pfade von einem Startknoten s aus bestimmen. Uns ist eine zusätzliche Information bekannt: Für jeden Knoten v ∈ V bilden die Gewichte entlang jedes kürzesten Pfades von s nach v eine bitonische Sequenz. Geben Sie einen möglichst effizienten Algorithmus für die Lösung dieses Problems an und analysieren Sie seine Laufzeit.
Kapitelbemerkungen Dijkstras Algorithmus [88] wurde 1959 veröffentlicht, es gab aber keine Erwähnung einer Prioritätswarteschlange. Der Bellman-Ford-Algorithmus basiert auf zwei separaten Algorithmen von Bellman [38] und Ford [109]. Bellman beschreibt die Beziehung zwischen dem kürzeste-Pfade-Problem und Systemen von Differenzbedingungen. Lawler
Kapitelbemerkungen zu Kapitel 24
695
[224] beschreibt den Algorithmus mit linearer Laufzeit für das kürzeste-Pfade-Problem in einem gerichteten azyklischen Graphen, den er für allgemein bekannt hält. Wenn die Kantengewichte relativ kleine nichtnegative ganze Zahlen sind, dann kennen wir effizientere Algorithmen, um das kürzeste-Pfade-Problem mit einem Startknoten zu lösen. Die Sequenz der Werte, die bei den Aufrufen von Extract-Min in Dijkstras Algorithmus zurückgegeben werden, steigt monoton mit der Zeit. Wie bereits in den Kapitelbemerkungen zu Kapitel 6 diskutiert, können in diesem Fall mehrere Datenstrukturen die verschiedenen Operationen für Prioritätswarteschlangen effizienter implementieren, als dies ein binärer Heap oder ein Fibonacci-Heap tut. Ahuja, Mehlhorn, Orlin und Tarjan [8] geben einen Algorithmus an, der auf Graphen mit nichtnegati√ ven Kantengewichten in Zeit O(E + V lg W ) läuft, wobei W das größte Gewicht aller Kanten des Graphen ist. Die besten Schranken stammen von Thorup [337], der einen Algorithmus mit Laufzeit O(E lg lg V ) angibt, sowie von Raman [291], dessen Algorithmus eine Laufzeit von O E + V min{(lg V )1/3+ , (lg W )1/4+ } hat. Der von diesen beiden Algorithmen verwendete Speicherplatz hängt von der Wortbreite des Rechners ab. Zwar kann der verwendete Speicher unbeschränkt in der Eingabegröße sein, er kann jedoch durch randomisiertes Hashing auf eine lineare Abhängigkeit von der Eingabegröße reduziert werden. Für ungerichtete Graphen mit ganzzahligen Gewichten gibt Thorup [336] einen Algorithmus mit Laufzeit O(V + E) für das kürzeste-Pfade-Problem mit einem Startknoten an. Im Unterschied zu den Algorithmen, die im letzten Absatz erwähnt wurden, ist dieser Algorithmus keine Implementierung von Dijkstras Algorithmus, da die Sequenz der Werte, die von Extract-Min zurückgegeben werden, nicht mit der Zeit monoton steigt. Für Graphen mit negativen Kantengewichten gibt es einen auf Gabow und Tar√ jan [122] zurückgehenden Algorithmus mit Laufzeit O( V E lg(V W )) sowie einen √ Algorithmus von Goldberg [137], der in Zeit O( V E lg W ) läuft, wobei W = max(u,v)∈E {|w(u, v)|} ist. Cherkassky, Goldberg und Radzik [64] führten umfangreiche Versuche durch, um die verschiedenen Algorithmen zur Berechnung kürzester Pfade zu vergleichen.
25
Kürzeste Pfade für alle Knotenpaare
In diesem Kapitel betrachten wir das Problem, die kürzesten Pfade zwischen allen Knotenpaaren eines Graphen zu bestimmen. Dieses Problem tritt zum Beispiel auf, wenn für eine Straßenkarte eine Tabelle mit den kürzesten Entfernungen zwischen allen Paaren von Städten hergestellt werden soll. Gegeben ist wie in Kapitel 24 ein gewichteter gerichteter Graph G = (V, E) mit einer Gewichtsfunktion w : E → R, die die Kanten auf reellwertige Gewichte abbildet. Wir wollen für jedes Paar u, v ∈ V einen kürzesten Pfad, d. h. einen Pfad mit geringstem Gewicht, von u nach v finden, wobei das Gewicht eines Pfades die Summe der Gewichte seiner Kanten ist. Das Ergebnis soll in Tabellenform erfolgen: Der Eintrag in Zeile u und Spalte v soll das Gewicht eines kürzesten Pfades von u nach v angeben. Wir können das kürzeste-Pfade-Problem für alle Knotenpaare lösen, indem wir |V |-mal einen Algorithmus laufen lassen, der die kürzeste Pfade von einem gegebenen Startknoten aus berechnet, wobei jeder Knoten einmal als Startknoten verwendet wird. Sind alle Gewichte nichtnegativ, können wir Dijkstras Algorithmus anwenden. Wenn wir die Prioritätswarteschlange durch eine geordnete Liste implementieren, dann ist die Laufzeit O(V 3 + V E) = O(V 3 ). Eine Implementierung durch einen binären Heap führt zu einer Laufzeit in O(V E lg V ), was bei dünn besetzten Graphen eine Verbesserung darstellt. Alternativ dazu können wir die Prioritätswarteschlange durch einen Fibonacci-Heap implementieren, was zu einer Laufzeit in O(V 2 lg V + V E) führt. Wenn der Graph Kanten mit negativem Gewicht enthält, können wir Dijkstras Algorithmus nicht anwenden. Stattdessen müssen wir von jedem Knoten aus einmal den langsameren Bellman-Ford-Algorithmus laufen lassen. Die resultierende Laufzeit ist O(V 2 E), was bei dicht besetzten Graphen O(V 4 ) ergibt. In diesem Kapitel werden wir sehen, wie wir das Problem effizienter lösen können. Wir werden außerdem den Zusammenhang des kürzeste-Pfade-Problems für alle Knotenpaare und dem Problem der Matrizenmultiplikation sowie die algebraische Struktur des kürzeste-Pfade-Problems für alle Knotenpaare untersuchen. Anders als die Algorithmen zur Berechnung kürzester Pfade von einem gegebenen Startknoten aus, die die Darstellung des Graphen durch Adjazanzlisten voraussetzen, benutzen die meisten Algorithmen in diesem Kapitel als Darstellung des Graphen eine Adjazenzmatrix. (Johnsons Algorithmus für dünn besetzte Graphen aus Abschnitt 25.3 benutzt Adjazenzlisten.) Wir setzen der Einfachheit halber voraus, dass die Knoten mit 1, . . . , |V | durchnummeriert sind, sodass die Eingabe eine n × n-Matrix ist, die die Kantengewichte eines gerichteten Graphen G = (V, E) mit n Knoten darstellt. Sie hat
698
25 Kürzeste Pfade für alle Knotenpaare
also die Form W = (wij) mit ⎧ ⎪ falls i = j ⎨0 wij = Gewicht der gerichteten Kante (i, j) falls i = j und (i, j) ∈ E ⎪ ⎩∞ falls i = j und (i, j) ∈ E . (25.1) Wir erlauben Kanten mit negativem Gewicht, setzen aber vorerst voraus, dass der Eingabegraph keine Zyklen mit negativem Gewicht enthält. Der Algorithmus zur Lösung des kürzeste-Pfade-Problems für alle Knotenpaare liefert eine tabellarische Ausgabe in Form einer n × n-Matrix D = (dij ). Dabei ist das Element dij das Gewicht eines kürzesten Pfades vom Knoten i zum Knoten j. Wenn wir also das Gewicht für einen kürzesten Pfad von Knoten i nach Knoten j wie in Kapitel 24 mit δ(i, j) bezeichnen, dann gilt bei Terminierung dij = δ(i, j). Um das kürzeste-Pfade-Problem für alle Knotenpaare ausgehend von einer Adjazenzmatrix zu lösen, müssen wir nicht nur die Gewichte der kürzesten Pfade, sondern auch eine Vorgängermatrix Π = (πij ) berechnen. Dabei ist πij gleich nil, wenn i = j gilt oder wenn es keinen Weg von i nach j gibt; anderenfalls ist πij der Vorgänger des Knotens j auf einem kürzesten Pfad von i nach j. So wie der Vorgängerteilgraph Gπ aus Kapitel 24 ein Baum kürzester Pfade für einen gegebenen Startknoten ist, ist der durch die i−te Zeile der Matrix Π erzeugte Teilgraph ein Baum kürzester Pfade mit der Wurzel i. Für jeden Knoten i ∈ V definieren wir den Vorgängerteilgraph von G durch Gπ,i = (Vπ,i , Eπ,i ) mit Vπ,i = {j ∈ V : πij = nil} ∪ {i} und Eπ,i = {(πij , j) : j ∈ Vπ,i − {i}} . Wenn Gπ,i ein Baum kürzester Pfade ist, dann gibt die folgende Prozedur, die eine modifizierte Version der Prozedur Print-Path aus Kapitel 22 ist, einen kürzesten Pfad vom Knoten i zum Knoten j aus. Print-All-Pairs-Shortest-Path(Π, i, j) 1 if i = = j 2 print i 3 elseif πij = = nil 4 print “es gibt keinen Pfad von” i “nach” j 5 else Print-All-Pairs-Shortest-Path(Π, i, πij ) 6 print j Um uns auf die wesentlichen Eigenschaften der Algorithmen für das kürzeste-PfadeProblem für alle Knotenpaare konzentrieren zu können, werden wir das Aufstellen und die Eigenschaften der Vorgängermatrizen nicht so ausführlich behandeln, wie wir es in Kapitel 22 für die Vorgängerteilgraphen getan haben. Einige der Übungen beschäftigen sich mit diesen Grundlagen.
25.1 Kürzeste Pfade und Matrizenmultiplikation
699
Kapitelüberblick Abschnitt 25.1 stellt ein dynamisches Programm zum Lösen des kürzeste-PfadeProblems für alle Knotenpaare vor, das auf der Matrizenmultiplikation beruht. Mithilfe der Methode des „wiederholten Quadrierens“ kann man diesen Algorithmus auf eine Laufzeit in O(V 3 lg V ) bringen. Abschnitt 25.2 stellt einen weiteren Algorithmus, der auf dynamischer Programmierung beruht, vor, den Floyd-Warshall-Algorithmus, dessen Laufzeit in O(V 3 ) liegt. Abschnitt 25.2 befasst sich außerdem mit dem Problem, den transitiven Abschluss eines gerichteten Graphen zu berechnen, das in engem Zusammenhang mit dem kürzeste-Pfade-Problem für alle Knotenpaare steht. Schließlich stellt Abschnitt 25.3 Johnsons Algorithmus vor, der das kürzeste-Pfade-Problem für alle Knotenpaare in Zeit O(V 2 lg V + V E) löst und eine gute Alternative bei großen dünn besetzten Graphen ist. Bevor wir uns an die Arbeit machen, müssen wir verschiedene Konventionen in Bezug auf die Darstellung von Graphen durch Adjazenzmatrizen festlegen. Erstens werden wir grundsätzlich voraussetzen, dass der Eingabegraph G = (V, E) n Knoten hat, d. h. es gilt n = |V |. Zweitens werden wir Matrizen mit Großbuchstaben wie W, L oder D bezeichnen und die einzelnen Matrixelemente mit Kleinbuchstaben unter Verwendung tiefgestellter Indizes, also zum Beispiel wij , lij oder dij . Einige Matrizen werden ge(m) klammerte, hochgestellte Indizes haben, die Iterationen angeben (L(m) = (lij ) oder (m)
D(m) = (dij )). Zudem setzen wir voraus, dass für eine gegebene n × n-Matrix A der Wert von n in dem Attribut A.zeilen gespeichert ist.
25.1
Kürzeste Pfade und Matrizenmultiplikation
Dieser Abschnitt stellt ein dynamisches Programm für das kürzeste-Pfade-Problem für alle Knotenpaare auf einem gerichteten Graphen G = (V, E) vor. Jede Iteration des dynamischen Programms führt eine Operation aus, die der Matrizenmultiplikation sehr ähnlich ist. Daher sieht der Algorithmus wie eine wiederholte Matrizenmultiplikation aus. Wir werden mit der Entwicklung eines Algorithmus für das kürzeste-Pfade-Problem für alle Knotenpaare beginnen, der eine Laufzeit in Θ(V 4 ) hat, und seine Laufzeit anschließend auf Θ(V 3 lg V ) verbessern. Bevor wir fortfahren, lassen Sie uns kurz die im Kapitel 15 angegebenen Schritte bei der Entwicklung von Algorithmen zur dynamischen Programmierung rekapitulieren. 1. Charakterisiere die Struktur einer optimalen Lösung. 2. Definiere rekursiv den Wert einer optimalen Lösung. 3. Berechne bottom-up den Wert einer optimalen Lösung. Wir überlassen den vierten Schritt – die Konstruktion einer optimalen Lösung aus den berechneten Informationen – als Übung.
700
25 Kürzeste Pfade für alle Knotenpaare
Die Struktur eines kürzesten Pfades Wir beginnen mit der Charakterisierung der Struktur einer optimalen Lösung. Für das kürzeste-Pfade-Problem für alle Knotenpaare auf einem Graphen G = (V, E) haben wir bewiesen (Lemma 24.1), dass alle Teilpfade eines kürzesten Pfades kürzeste Pfade sind. Nehmen Sie an, wir würden den Graphen durch eine Adjazenzmatrix W = (wij ) darstellen. Betrachten Sie einen kürzesten Pfad p von Knoten i nach Knoten j und nehmen Sie an, dass p höchstens m Kanten enthalten würde. Wenn wir voraussetzen, dass es keine Zyklen mit negativem Gewicht gibt, dann ist m endlich. Im Falle i = j hat p das Gewicht 0 und enthält keine Kante. Falls i und j zwei verschiedene Knoten p
sind, dann zerlegen wir den Pfad in i ; k → j, wobei der Pfad p nun höchstens m − 1 Kanten enthält. Nach Lemma 24.1 ist p ein kürzester Pfad von i nach k, und daher gilt δ(i, j) = δ(i, k) + wkj .
Eine rekursive Lösung des kürzeste-Pfade-Problems für alle Knotenpaare (m)
Sei nun lij das Gewicht eines kürzesten Pfades von Knoten i nach Knoten j, der höchstens m Kanten enthält. Im Falle m = 0 gibt es genau dann einen kürzesten Pfad von i nach j, wenn i gleich j ist. Somit gilt 0 falls i = j , (0) lij = ∞ falls i = j . (m)
(m−1)
(dem Gewicht eines kürzesFür m ≥ 1 berechnen wir lij als das Minimum von lij ten Pfades von i nach j mit höchstens m − 1 Kanten) und dem minimalen Gewicht aller Pfade von i nach j mit höchstens m Kanten, die wir erhalten, indem wir alle möglichen Vorgänger k von j betrachten. Wir definieren daher rekursiv (m)
lij
(m−1) (m−1) = min lij , min lik + wkj 1≤k≤n (m−1) = min lik + wkj . 1≤k≤n
(25.2)
Die letzte Gleichung folgt aus wjj = 0 für alle j. Was sind die tatsächlichen Gewichte δ(i, j) der kürzesten Pfade? Wenn der Graph keine Zyklen mit negativem Gewicht enthält, dann gibt es für jedes Knotenpaar i und j mit δ(i, j) < ∞ einen kürzesten Pfad von i nach j, der einfach ist und somit höchstens n − 1 Kanten enthält. Ein Pfad von Knoten i nach Knoten j mit mehr als n − 1 Kanten kann kein geringeres Gewicht als ein kürzester Pfad von i nach j haben. Die tatsächlichen Gewichte der kürzesten Pfade sind daher durch (n−1)
δ(i, j) = lij gegeben.
(n)
(n+1)
= lij = lij
= ···
(25.3)
25.1 Kürzeste Pfade und Matrizenmultiplikation
701
Bottom-up-Berechnung der Gewichte der kürzesten Pfade Mit der Matrix W = (wij ) als Eingabe berechnen wir nun eine Folge von Matrizen (m) (1) (2) (n−1) (m) für m = 1, 2, . . . , n − 1. Die letzte Matrix mit L = lij L ,L ,...,L (1)
enthält die tatsächlichen Gewichte der kürzesten Pfade. Sie sollten sehen, dass lij = wij für alle Knotenpaare i, j ∈ V gilt und somit auch L(1) = W . Das Herzstück des Algorithmus ist die folgende Prozedur, die zu den gegebenen Matrizen L(m−1) und W die Matrix L(m) zurückgibt. Sie erweitert also die bisher berechneten kürzesten Pfade um eine Kante. Extend-Shortest-Paths(L, W ) 1 n = L.zeilen eine neue n × n-Matrix 2 sei L = lij 3 for i = 1 to n 4 for j = 1 to n 5 lij =∞ 6 for k = 1 to n = min(lij , lik + wkj ) 7 lij 8 return L Die Prozedur berechnet eine Matrix L = (lij ), die sie am Ende zurückgibt. Sie tut dies, indem sie Gleichung (25.2) für alle i und j auswertet, wobei L für L(m−1) und L für L(m) steht. (Die Indizes wurden im Pseudocode weggelassen, damit die Eingabematrizen und die Ausgabematrix der Prozedur unabhängig von m sind.) Ihre Laufzeit ist wegen der drei verschachtelten for-Schleifen Θ(n3 ).
Nun können wir den Zusammenhang mit der Matrizenmultiplikation erkennen. Nehmen Sie an, wir wollten das Produkt C = A·B der beiden n×n-Matrizen A und B berechnen. Für i, j = 1, 2, . . . , n werden die Matrixelemente nach der Formel cij =
n
aik · bkj
(25.4)
k=1
berechnet. Wenn wir in Gleichung (25.2) die Substitutionen l(m−1) → a , w→b, l(m) → c , min → + , +→· vornehmen, dann erhalten wir Gleichung (25.4). Wenn wir also diese Änderungen in die Prozedur Extend-Shortest-Paths übernehmen und außerdem ∞ (das neutrale Element bezüglich der Minimumsbildung) durch 0 ersetzen, dann erhalten wir die gleiche, in Zeit Θ(n3 ) laufende Prozedur für Matrizenmultiplikation wie die aus Abschnitt 4.2:
702
25 Kürzeste Pfade für alle Knotenpaare
Square-Matrix-Multiply(A, B) 1 n = A.zeilen 2 sei C eine neue n × n-Matrix 3 for i = 1 to n 4 for j = 1 to n 5 cij = 0 6 for k = 1 to n 7 cij = cij + aik · bkj 8 return C Lassen Sie uns wieder zum kürzeste-Pfade-Problem für alle Knotenpaare zurückkommen. Wir berechnen die Gewichte der kürzesten Pfade, indem wir kürzeste Pfade Kante um Kante erweitern. Dazu berechnen wir die Folge der n − 1 Matrizen L(1) = L(0) · W = W , L(2) = L(1) · W = W 2 , L(3) = L(2) · W = W 3 , .. . L(n−1) = L(n−2) · W = W n−1 , wobei wir mit A · B das von Extend-Shortest-Paths(A, B) zurückgegebene „Matrizenprodukt“ bezeichnen. Wie wir zuvor gezeigt haben, enthält die Matrix L(n−1) = W n−1 die Gewichte der kürzesten Pfade. Die folgende Prozedur berechnet diese Folge in Zeit Θ(n4 ). Slow-All-Pairs-Shortest-Paths(W ) 1 n = W.zeilen 2 L(1) = W 3 for m = 2 to n − 1 4 sei L(m) eine neue n × n-Matrix 5 L(m) = Extend-Shortest-Paths(L(m−1) , W ) 6 return L(n−1) Abbildung 25.1 zeigt einen Graphen und die von der Prozedur Slow-All-PairsShortest-Paths berechneten Matrizen L(m) .
Verbesserung der Laufzeit Unser Ziel ist es jedoch nicht, alle L(m) Matrizen zu berechnen. Wir sind nur an der Matrix L(n−1) interessiert. Rufen Sie sich in Erinnerung, dass aus Gleichung (25.3) L(m) = L(n−1) für alle ganzen Zahlen m, n mit m ≥ n − 1 folgt, wenn es keine Zyklen mit negativem Gewicht gibt. Wie die gewöhnliche Matrizenmultiplikation ist auch die durch die Prozedur Extend-Shortest-Paths definierte Matrizenmultiplikation
25.1 Kürzeste Pfade und Matrizenmultiplikation
703
2 3
4
2
8
1 –4
7
1
5 ⎛ ⎜ ⎜ L(1) = ⎜ ⎜ ⎝
0 ∞ ∞ 2 ∞
3 8 ∞ −4 0 ∞ 1 7 4 0∞ ∞ ∞ −5 0 ∞ ∞ ∞ 6 0
⎛
⎞
⎜ ⎟ ⎟ (2) ⎜ ⎟L =⎜ ⎜ ⎟ ⎝ ⎠
(3)
L
⎞ 0 3 −3 2 −4 ⎜ 3 0 −4 1 −1 ⎟ ⎜ ⎟ ⎟ =⎜ ⎜ 7 4 0 5 11 ⎟ ⎝ 2 −1 −5 0 −2 ⎠ 8 5 16 0
–5
4
6
⎛
3
0 3 8 2 −4 3 0 −4 1 7 ∞ 4 0 5 11 2 −1 −5 0 −2 8 ∞ 16 0
⎛
(4)
L
⎞ ⎟ ⎟ ⎟ ⎟ ⎠
⎞
0 1 −3 2 −4 ⎜ 3 0 −4 1 −1 ⎜ =⎜ ⎜ 7 4 05 3 ⎝ 2 −1 −5 0 −2 8 5 16 0
⎟ ⎟ ⎟ ⎟ ⎠
Abbildung 25.1: Ein gerichteter Graph und die Folge der Matrizen L(m) , die von der Prozedur Slow-All-Pairs-Shortest-Paths berechnet werden. Sie können überprüfen, dass die durch L(4) · W definierte Matrix L(5) gleich L(4) ist und somit L(m) = L(4) für alle m ≥ 4 gilt.
assoziativ (siehe Übung 25.1-4). Daher können wir L(n−1) mit nur lg(n − 1) Matrizenmultiplikationen berechnen, indem wir die Folge
L(1) L(2) L(4) L(8) lg(n−1)
L(2
)
= = = =
W , W2 W4 W8 .. . lg(n−1)
= W2
= W ·W , = W2 · W2 = W4 · W4 , lg(n−1)−1
= W2
lg(n−1)−1
· W2
.
lg(n−1)
berechnen. Wegen 2 lg(n−1) ≥ n − 1 ist das letzte Produkt L(2
)
gleich L(n−1) .
Die folgende Prozedur berechnet die obige Folge mit einer Technik, die wir als wiederholtes Quadrieren kennen.
704
25 Kürzeste Pfade für alle Knotenpaare 1
1 –4
2 4
2
–1 3
7 5
2
5
3 10
–8 6
Abbildung 25.2: Ein gewichteter gerichteter Graph, der in den Übungen 25.1-1, 25.2-1 und 25.3-1 verwendet wird.
Faster-All-Pairs-Shortest-Paths(W ) 1 n = W.zeilen 2 L(1) = W 3 m=1 4 while m < n − 1 5 sei L(2m) eine neue n × n-Matrix 6 L(2m) = Extend-Shortest-Paths(L(m) , L(m) ) 7 m = 2m 8 return L(m)
In jeder Iteration der while-Schleife der Zeilen 4–7 berechnen wir mit m = 1 beginnend 2 L(2m) = L(m) . Am Ende jeder Iteration verdoppeln wir den Wert von m. Die letzte Iteration berechnet L(n−1) , wobei tatsächlich L(2m) für ein n − 1 ≤ 2m < 2n − 2 berechnet wird. Nach Gleichung (25.3) ist L(2m) = L(n−1) . Wenn das nächste Mal der Test in Zeile 4 durchgeführt wird, dann ist m verdoppelt worden, sodass nun m ≥ n − 1 gilt. Der Test liefert daher den Wert falsch und die Prozedur gibt die zuletzt berechnete Matrix zurück. Die Laufzeit der Prozedur Faster-All-Pairs-Shortest-Paths ist in Θ(n3 lg n), da jede der lg(n − 1) Matrizenmultiplikation Zeit Θ(n3 ) benötigt. Beachten Sie, dass der Code kurz ist und keine komplizierten Datenstrukturen enthält, und die in der Θ-Notation verborgene Konstante daher klein ist.
Übungen 25.1-1 Lassen Sie die Prozedur Slow-All-Pairs-Shortest-Paths auf dem gewichteten gerichteten Graphen aus Abbildung 25.2 laufen, und geben Sie die Matrizen an, die durch die einzelnen Iterationen der Schleife erzeugt werden. Tun Sie das Gleiche für die Prozedur Faster-All-Pairs-Shortest-Paths. 25.1-2 Warum fordern wir wii = 0 für alle 1 ≤ i ≤ n? 25.1-3 Was entspricht der in Algorithmen zur Berechnung kürzester Pfade verwende-
25.2 Der Floyd-Warshall-Algorithmus ten Matrix
⎛
L(0)
0 ∞ ∞ ··· ⎜∞ 0 ∞ ··· ⎜∞ ∞ 0 ··· =⎜ ⎜ . . . . ⎝ .. .. .. . .
705
⎞ ∞ ∞⎟ ∞⎟ ⎟ .. ⎟ . ⎠
∞ ∞ ∞ ··· 0
in der normalen Matrizenmultiplikation? 25.1-4 Zeigen Sie, dass die durch Extend-Shortest-Paths definierte Matrizenmultiplikation assoziativ ist. 25.1-5 Zeigen Sie, wie wir das kürzeste-Pfade-Problem mit einem Startknoten durch ein Matrizenprodukt und einen Vektor ausdrücken können. Beschreiben Sie, wie dieses Produkt durch einen Bellman-Ford-ähnlichen Algorithmus ausgewertet werden kann (siehe Abschnitt 24.1). 25.1-6 Nehmen Sie an, wir wollten mit den Algorithmen aus diesem Abschnitt auch die Knoten auf kürzesten Pfaden berechnen. Zeigen Sie, wie wir die Vorgängermatrix Π aus der fertigen Matrix L der Gewichte der kürzesten Pfade in Zeit O(n3 ) berechnen können. 25.1-7 Wir können die Knoten auf kürzesten Pfaden gleichzeitig mit den Gewichten (m) der kürzesten Pfade berechnen. Definieren Sie πij als den Vorgänger des Knotens j auf einem beliebigen Pfad minimalen Gewichts von i nach j, der höchstens m Kanten enthält. Modifizieren Sie die Prozeduren Extend-ShortestPaths und Slow-All-Pairs-Shortest-Paths so, dass sie die Matrizen Π(1) , Π(2) , . . . , Π(n−1) zusammen mit den Matrizen L(1) , L(2) , . . . , L(n−1) berechnen. 25.1-8 Die Prozedur Faster-All-Pairs-Shortest-Paths erfordert, dass wir lg(n − 1) Matrizen speichern. Da jede Matrix aus n2 Elemente besteht, bedeutet dies einen Gesamtspeicherbedarf von Θ(n2 lg n). Modifizieren Sie die Prozedur so, dass nur zwei n × n-Matrizen verwendet werden und sie nur Θ(n2 ) Speicher benötigt. 25.1-9 Modifizieren Sie die Prozedur Faster-All-Pairs-Shortest-Paths so, dass sie feststellen kann, ob der Graph einen Zyklus mit negativem Gewicht enthält. 25.1-10 Geben Sie einen effizienten Algorithmus an, der die Länge (d. h. die Anzahl der Kanten) eines kürzesten Zyklus mit negativem Gewicht eines Graphen bestimmt.
25.2
Der Floyd-Warshall-Algorithmus
In diesem Abschnitt wollen wir eine andere Formulierung mithilfe dynamischer Programmierung verwenden, um das kürzeste-Pfade-Problem für alle Knotenpaare auf einem gerichteten Graphen G = (V, E) zu lösen. Der resultierende Algorithmus, bekannt
706
25 Kürzeste Pfade für alle Knotenpaare
unter dem Namen Floyd-Warshall-Algorithmus, läuft in Zeit Θ(V 3 ). Wie zuvor sind Kanten mit negativem Gewicht zugelassen; wir wollen jedoch wieder voraussetzen, dass es keine Zyklen mit negativem Gewicht gibt. Wie in Abschnitt 25.1 folgen wir der üblichen Vorgehensweise bei der Entwicklung des dynamischen Programms. Nachdem wir den resultierenden Algorithmus untersucht haben, stellen wir eine ähnliche Methode vor, mit der wir den transitiven Abschluss eines gerichteten Graphen bestimmen können.
Die Struktur eines kürzesten Pfades Im Floyd-Warshall-Algorithmus charakterisieren wir die Struktur eines kürzesten Pfades anders als wir dies in Abschnitt 25.1 gemacht haben. Der Algorithmus betrachtet die Zwischenknoten eines kürzesten Pfades. Zwischenknoten eines einfachen Pfades p = v1 , v2 , . . . , vl sind alle Knoten von p außer v1 und vl , d. h. alle Knoten der Menge {v2 , v3 , . . . , vl−1 }. Der Floyd-Warshall-Algorithmus beruht auf der folgenden Beobachtung. Unter unserer Voraussetzung, dass die Menge V der Knoten von G gleich {1, 2, . . . , n} ist, lassen Sie uns für ein k eine Teilmenge {1, 2, . . . , k} der Knotenmenge betrachten. Betrachten Sie für jedes Knotenpaar i, j ∈ V alle Pfade von i nach j, deren Zwischenknoten alle in der Menge {1, 2, . . . , k} liegen, und sei p unter all diesen Pfaden einer mit minimalem Gewicht. (Der Pfad p ist einfach.) Der Floyd-Warshall-Algorithmus nutzt eine Beziehung zwischen dem Pfad p und den kürzesten Pfaden von i nach j aus, deren Knoten alle in der Menge {1, 2, . . . , k − 1} liegen. Die Beziehung hängt davon ab, ob k ein Zwischenknoten des Pfades p ist. • Falls k kein Zwischenknoten des Pfades p ist, dann gehören alle Zwischenknoten von p zu der Menge {1, 2, . . . , k − 1}. Also ist ein kürzester Pfad von Knoten i nach Knoten j, dessen Zwischenknoten alle in der Menge {1, 2, . . . , k − 1} liegen, auch ein kürzester Pfad von i nach j, dessen Zwischenknoten alle in der Menge {1, 2, . . . , k} liegen. • Ist k ein Zwischenknoten des Pfades p, dann zerlegen wir, wie in Abbildung 25.3 p1 p2 illustriert, p in i ; k ; j. Nach Lemma 24.1 ist p1 ein kürzester Pfad von i nach k und alle Zwischenknoten von p1 liegen in der Menge {1, 2, . . . , k}. Tatsächlich können wir die Aussage noch genauer fassen. Da der Knoten k kein Zwischenknoten von p1 ist, sind alle Zwischenknoten des Pfades p1 in der Menge {1, 2, . . . , k − 1}. Somit ist p1 ein kürzester Pfad von i nach k und all seine Zwischenknoten liegen in der Menge {1, 2, . . . , k − 1}. Entsprechend ist p2 ein kürzester Pfad von Knoten k nach Knoten j, dessen Zwischenknoten alle in {1, 2, . . . , k − 1} liegen.
Eine rekursive Lösung des kürzeste-Pfade-Problems für alle Knotenpaare Auf der Basis der obigen Überlegungen definieren wir eine rekursive Formulierung der Schätzungen für kürzeste Pfade, die sich von derjenigen aus Abschnitt 25.1 unterschei-
25.2 Der Floyd-Warshall-Algorithmus
707
alle Zwischenknoten aus {1, 2, . . . , k − 1} p1
alle Zwischenknoten aus {1, 2, . . . , k − 1} k
p2
j
i p: alle Zwischenknoten aus {1, 2, . . . , k} Abbildung 25.3: Pfad p ist ein kürzester Pfad von Knoten i nach Knoten j, und k ist der Zwischenknoten von p mit der höchsten Nummer. Die Zwischenknoten des Pfades p1 , dem Teilpfad von i nach k, liegen alle in der Menge {1, 2, . . . , k − 1}. Das Gleiche gilt für den Pfad p2 von k nach j. (k)
det. Sei dij das Gewicht eines kürzesten Pfades von Knoten i nach Knoten j, für den alle Zwischenknoten in der Menge {1, 2, . . . , k} liegen. Im Falle k = 0 hat ein Pfad von i nach j, der keine Zwischenknoten mit einer höheren Nummer als 0 haben soll, überhaupt keine Zwischenknoten. Ein solcher Pfad hat höchstens eine Kante, und folglich (0) (k) gilt dij = wij . Gemäß der obigen Diskussion, definieren wir dij rekursiv: < (k) dij
=
wij
(k−1) (k−1) (k−1) , dik + dkj min dij
für k = 0 , für k ≥ 1
(25.5)
Da für jeden Pfad alle Zwischenknoten in der Menge {1, 2, . . . , n} liegen, liefert die (n) (n) (n) Matrix D = dij die korrekte Lösung, nämlich dij = δ(i, j) für alle i, j ∈ V .
Bottom-up-Berechnung der Gewichte der kürzesten Pfade Wir können die folgende, auf der Rekursionsgleichung (25.5) basierende bottom-up(k) Prozedur verwenden, um die Werte dij in der Reihenfolge steigender k zu berechnen. Ihre Eingabe ist eine n × n-Matrix W , die wie in Gleichung (25.1) definiert ist. Die Prozedur gibt die Matrix D(n) der Gewichte der kürzesten Pfade zurück. Floyd-Warshall(W ) 1 n = W.zeilen 2 D(0) = W 3 for k = 1 to n (k) 4 sei D(k) = dij eine neue n × n-Matrix 5 for i = 1 to n 6 for j = 1 to n (k) (k−1) (k−1) (k−1) 7 dij = min dij , dik + dkj 8 return D(n)
708
25 Kürzeste Pfade für alle Knotenpaare
⎛ ⎜ ⎜ D(0) = ⎜ ⎜ ⎝ ⎛ ⎜ ⎜ D(1) = ⎜ ⎜ ⎝ ⎛ ⎜ ⎜ D(2) = ⎜ ⎜ ⎝
0 ∞ ∞ 2 ∞
3 8 ∞ −4 0 ∞ 1 7 4 0∞ ∞ ∞ −5 0 ∞ ∞ ∞ 6 0
0 ∞ ∞ 2 ∞
3 8 ∞ −4 0 ∞ 1 7 4 0∞ ∞ 5 −5 0 −2 ∞ ∞ 6 0
0 ∞ ∞ 2 ∞
3 8 4 −4 0 ∞1 7 4 0 5 11 5 −5 0 −2 ∞ ∞6 0
⎛
D(3)
D
(4)
⎜ ⎜ =⎜ ⎜ ⎝ ⎛
D
(5)
⎜ ⎜ =⎜ ⎜ ⎝
0 3 −1 4 −4 3 0 −4 1 −1 7 4 05 3 2 −1 −5 0 −2 8 5 16 0 0 1 −3 2 −4 3 0 −4 1 −1 7 4 05 3 2 −1 −5 0 −2 8 5 16 0
1 nil 3 nil nil
1 nil nil 4 nil
nil 2 nil nil 5
⎞ 1 2 ⎟ ⎟ nil ⎟ ⎟ nil ⎠ nil
1 nil 3 1 nil
1 nil nil 4 nil
nil 2 nil nil 5
⎞ 1 2 ⎟ ⎟ nil ⎟ ⎟ 1 ⎠ nil
nil ⎜ nil ⎜ =⎜ ⎜ nil ⎝ 4 nil
1 nil 3 1 nil
1 nil nil 4 nil
2 2 2 nil 5
⎞ 1 2 ⎟ ⎟ 2 ⎟ ⎟ 1 ⎠ nil
⎛ nil ⎜ nil ⎟ ⎟ (3) ⎜ ⎟ Π = ⎜ nil ⎜ ⎟ ⎝ 4 ⎠ nil
1 nil 3 3 nil
1 nil nil 4 nil
2 2 2 nil 5
⎞ 1 2 ⎟ ⎟ 2 ⎟ ⎟ 1 ⎠ nil
1 nil 3 3 3
4 4 nil 4 4
2 2 2 nil 5
⎞ 1 1 ⎟ ⎟ 1 ⎟ ⎟ 1 ⎠ nil
3 nil 3 3 3
4 4 nil 4 4
5 2 2 nil 5
⎞ 1 1 ⎟ ⎟ 1 ⎟ ⎟ 1 ⎠ nil
⎟ ⎟ (0) ⎟Π ⎟ ⎠ ⎞
nil ⎜ nil ⎜ =⎜ ⎜ nil ⎝ 4 nil ⎛
⎟ ⎟ (1) ⎟Π ⎟ ⎠
⎞
nil ⎜ nil ⎜ =⎜ ⎜ nil ⎝ 4 nil ⎛
⎟ ⎟ ⎟ Π(2) ⎟ ⎠
0 3 8 4 −4 ⎜ ∞ 0 ∞1 7 ⎜ =⎜ ⎜ ∞ 4 0 5 11 ⎝ 2 −1 −5 0 −2 ∞ ∞ ∞6 0 ⎛
⎛
⎞
⎞
⎞ ⎟ ⎟ ⎟ ⎟ ⎠
⎛
Π(4)
⎞ ⎟ ⎟ ⎟ ⎟ ⎠
nil ⎜ 4 ⎜ =⎜ ⎜ 4 ⎝ 4 4 ⎛
Π(5)
nil ⎜ 4 ⎜ =⎜ ⎜ 4 ⎝ 4 4
Abbildung 25.4: Die durch den Floyd-Warshall-Algorithmus berechnete Folge der Matrizen D(k) und Π(k) für den Graphen aus Abbildung 25.1.
25.2 Der Floyd-Warshall-Algorithmus
709
Abbildung 25.4 zeigt die durch den Floyd-Warshall-Algorithmus berechneten Matrizen D(k) für den Graphen aus Abbildung 25.1. Die Laufzeit des Floyd-Warshall-Algorithmus wird durch die dreifach verschachtelten for-Schleifen in den Zeilen 3–7 bestimmt. Da jede Ausführung von Zeile 7 Zeit O(1) benötigt, läuft der Algorithmus in Zeit Θ(n3 ). Wie im Falle des Algorithmus aus Abschnitt 25.1 ist der Code kurz und enthält keine komplizierten Datenstrukturen, sodass die in der Θ-Notation verborgene Konstante klein ist. Damit ist der Floyd-WarshallAlgorithmus selbst für Eingabegraphen mittlerer Größe recht gut zu gebrauchen.
Konstruktion eines kürzesten Pfades Es gibt für den Floyd-Warshall-Algorithmus eine Reihe unterschiedlicher Methoden zur Konstruktion kürzester Pfade. Eine Möglichkeit besteht darin, die Matrix D der Gewichte der kürzesten Pfade zu bestimmen und dann die Vorgängermatrix Π aus der Matrix D zu berechnen. Übung 25.1-6 verlangt von Ihnen, diese Methode so zu implementieren, dass sie in Zeit O(n3 ) läuft. Ist die Vorgängermatrix Π gegeben, so kann die Prozedur Print-All-Pairs-Shortest-Path die Knoten eines gegebenen kürzesten Pfades ausgeben. Alternativ dazu, können wir die Vorgängermatrix Π berechnen, während der FloydWarshall-Algorithmus die Matrizen D(k) berechnet. Genauer gesagt, berechnen wir eine Folge von Matrizen Π(0) , Π(1) , . . . , Π(n) mit Π = Π(n) und wir definieren das Matrixele(k) ment πij als der Vorgänger des Knotens j auf einem kürzesten Pfad vom Knoten i, dessen Zwischenknoten vollständig in der Menge {1, 2, . . . , k} liegen. (k)
Wir können eine rekursive Formulierung für πij angeben. Im Falle k = 0 hat ein kürzester Pfad von i nach j überhaupt keine Zwischenknoten. Daher gilt nil falls i = j oder wij = ∞ , (0) πij = (25.6) i falls i = j und wij < ∞ . Betrachten Sie nun den Fall k ≥ 1. Wenn wir den Pfad i ; k ; j mit k = j nehmen, dann ist der Vorgänger von j, den wir wählen, der gleiche wie der Vorgänger von j, den wir auf einem kürzesten Pfad von k aus wählen, dessen Knoten alle in der Menge {1, 2, . . . , k − 1} enthalten sind. Anderenfalls wählen wir den gleichen Vorgänger von j, den wir auf einem kürzesten Pfad von i aus wählen, dessen Zwischenknoten alle in der Menge {1, 2, . . . , k − 1} liegen. Formal gilt für k ≥ 1 < (k−1) (k−1) (k−1) (k−1) falls dij ≤ dik + dkj , πij (k) πij = (25.7) (k−1) (k−1) (k−1) (k−1) falls dij > dik + dkj . πkj Wir überlassen die Einarbeitung der Berechnung der Matrix Π(k) in die Prozedur Floyd-Warshall der Übung 25.2-3. Abbildung 25.4 zeigt die Folge der Matrizen Π(k) , die der resultierende Algorithmus für den Graphen aus Abbildung 25.1 berechnet. Die Übung stellt außerdem die schwierigere Aufgabe, zu beweisen, dass der Vorgängerteilgraph Gπ,i ein Baum kürzester Pfade mit der Wurzel i ist. Übung 25.2-7 fragt nach einer weiteren Möglichkeit, kürzeste Pfade zu rekonstruieren.
710
25 Kürzeste Pfade für alle Knotenpaare
Der transitive Abschluss eines gerichteten Graphen Für einen gegebenen Graphen G = (V, E) mit der Knotenmenge V = {1, 2, . . . , n} wollen wir möglicherweise für alle Knotenpaare i, j ∈ V wissen, ob G einen Pfad von i nach j enthält. Wir definieren den transitiven Abschluss von G als der Graph G∗ = (V, E ∗ ) mit E ∗ = {(i, j) : es existiert ein Pfad von Knoten i nach Knoten j in G} . Eine Möglichkeit, den transitiven Abschluss eines Graphen in Zeit Θ(n3 ) zu berechnen, besteht darin, jeder Kante von E das Gewicht 1 zuzuordnen und den Floyd-WarshallAlgorithmus laufen zu lassen. Falls es einen Pfad von i nach j gibt, erhalten wir dij < n. Anderenfalls erhalten wir dij = ∞. Es gibt einen anderen, ähnlichen Weg, den transitiven Abschluss von G in Zeit Θ(n3 ) zu berechnen, der in der Praxis Laufzeit und Speicherplatz sparen kann. Diese Methode substituiert im Floyd-Warshall-Algorithmus die arithmetischen Operationen min und + durch die logischen Operationen ∨ (logisches ODER) und ∧ (logisches UND). (k) Für i, j, k = 1, 2, . . . , n definieren wir tij als 1, falls G einen Pfad von Knoten i nach Knoten j enthält, dessen Zwischenknoten alle in der Menge {1, 2, . . . , k} enthalten sind, und 0 sonst. Wir konstruieren den transitiven Abschluss G∗ = (V, E ∗ ), indem wir die (n) Kante (i, j) genau dann in die Kantenmenge E ∗ aufnehmen, wenn tij = 1 gilt. Eine (k)
rekursive Definition von tij , analog zu der Rekursionsgleichung (25.5), ist 0 falls i = j und (i, j) ∈ E , (0) tij = 1 falls i = j oder (i, j) ∈ E , und für k ≥ 1 (k)
(k−1)
tij = tij
(k−1) (k−1) . ∨ tik ∧ tkj
Wie beim Floyd-Warshall-Algorithmus berechnen wir die Matrizen T (k) = aufsteigender Reihenfolge nach k. Transitive-Closure(G) 1 n = |G.V | (0) 2 sei T (0) = tij eine neue n × n-Matrix 3 for i = 1 to n 4 for j = 1 to n 5 if i = = j oder (i, j) ∈ G.E (0) 6 tij = 1 (0) 7 else tij = 0 8 for k = 1 to n (k) 9 sei T (k) = tij eine neue n × n-Matrix 10 for i = 1 to n 11 for j = 1 to n 12 13 return T (n)
(k)
(k−1)
tij = tij
(k−1)
∨ tik
(k−1)
∧ tkj
(25.8)
(k)
tij
in
25.2 Der Floyd-Warshall-Algorithmus 1
2
4
3
T (0)
T
(3)
⎛ 1 ⎜0 ⎜ =⎝ 0 1 ⎛ 1 ⎜0 ⎜ =⎝ 0 1
0 1 1 0
0 1 1 1
0 1 1 1
0 1 1 1
⎞ 0 1⎟ ⎟ 0⎠ 1 ⎞ 0 1⎟ ⎟ 1⎠ 1
T (1)
T
(4)
⎛ 1 ⎜0 ⎜ =⎝ 0 1 ⎛ 1 ⎜1 ⎜ =⎝ 1 1
0 1 1 0
0 1 1 1
0 1 1 1
0 1 1 1
⎞ 0 1⎟ ⎟ 0⎠ 1 ⎞ 0 1⎟ ⎟ 1⎠ 1
711
⎛
T (2)
1 ⎜0 ⎜ =⎝ 0 1
0 1 1 0
0 1 1 1
⎞ 0 1⎟ ⎟ 1⎠ 1
Abbildung 25.5: Ein gerichteter Graph und die Matrizen T (k) , die durch den Algorithmus zur Berechnung des transitiven Abschlusses berechnet werden.
Abbildung 25.5 zeigt die Matrizen T (k) , die von der Prozedur Transitive-Closure auf einem Beispielgraphen berechnet werden. Die Prozedur Transitive-Closure läuft wie der Floyd-Warshall-Algorithmus in Zeit Θ(n3 ). Auf manchen Rechnern werden logische Operationen für Ein-Bit-Werte allerdings schneller als arithmetische Operationen auf ganzen Datenwörtern ausgeführt. Da der direkte Algorithmus zur Berechnung des transitiven Abschlusses nur Boolesche Werte statt ganzzahliger Werte benutzt, ist außerdem sein Speicherverbrauch gegenüber dem des Floyd-Warshall-Algorithmus um einen Faktor geringer, der der Wortbreite des Speichers entspricht.
Übungen 25.2-1 Lassen Sie den Floyd-Warshall-Algorithmus auf dem gewichteten gerichteten Graphen aus Abbildung 25.2 laufen. Geben Sie die Matrix D(k) an, die sich jeweils aus einer Iteration der äußeren Schleife ergibt. 25.2-2 Zeigen Sie, wie man mithilfe der Methode aus Abschnitt 25.1 den transitiven Abschluss berechnen kann. 25.2-3 Modifizieren Sie die Prozedur Floyd-Warshall so, dass sie die Matrizen Π(k) gemäß den Gleichungen (25.6) und (25.7) berechnet. Zeigen Sie durch einen formalen Beweis, dass für alle i ∈ V der Vorgängerteilgraph Gπ,i ein Baum kürzester Pfade mit der Wurzel i ist. (Hinweis: Um zu beweisen, dass Gπ,i (k) azyklisch ist, zeigen Sie zunächst, dass aus πij = l gemäß der Definition von (k)
(k)
(k)
πij die Ungleichung dij ≥ dil + wlj folgt. Passen Sie dann den Beweis von Lemma 24.16 entsprechend an.)
712
25 Kürzeste Pfade für alle Knotenpaare
25.2-4 Nach den obigen Ausführungen hat es den Anschein, dass der Speicherbedarf (k) des Floyd-Warshall-Algorithmus in Θ(n3 ) ist, da wir dij für i, j, k = 1, 2, . . . , n berechnen. Zeigen Sie, dass die folgende Prozedur, bei der einfach nur alle Indizes weggelassen werden, korrekt ist, und somit nur Speicherplatz Θ(n2 ) erforderlich ist. Floyd-Warshall (W ) 1 n = W.zeilen 2 D =W 3 for k = 1 to n 4 for i = 1 to n 5 for j = 1 to n 6 dij = min (dij , dik + dkj ) 7 return D 25.2-5 Nehmen Sie an, wir würden Gleichung (25.7) in folgender Art und Weise modifizieren: < (k−1) (k−1) (k−1) (k−1) πij falls dij < dik + dkj , (k) πij = (k−1) (k−1) (k−1) (k−1) falls dij ≥ dik + dkj . πkj Ist diese alternative Definition der Vorgängermatrix Π korrekt? 25.2-6 Wie können wir die Ausgabe des Floyd-Warshall-Algorithmus verwenden, um festzustellen, ob ein Zyklus mit negativem Gewicht vorhanden ist? 25.2-7 Eine andere Möglichkeit, im Floyd-Warshall-Algorithmus kürzeste Pfade zu (k) (k) rekonstruieren, verwendet die Werte φij für i, j, k = 1, 2, . . . , n, wobei φij der Zwischenknoten mit der höchsten Nummer auf einem kürzesten Pfad von i nach j ist, dessen Zwischenknoten alle in der Menge {1, 2, . . . , k} enthalten sind. (k) Geben Sie einen rekursiven Ausdruck für φij an. Modifizieren Sie die Prozedur (k)
Floyd-Warshall so, dass sie die Werte φij berechnet, und schreiben Sie die Prozedur Print-All-Pairs-Shortest-Path so um, dass sie die Matrix (n) Φ = φij als Eingabe verwendet. Wie ähnelt die Matrix Φ der Tabelle s beim Problem der Matrix-Kettenmultiplikation (siehe Abschnitt 15.2)? 25.2-8 Geben Sie einen Algorithmus mit Laufzeit O(V E) an, der den transitiven Abschluss eines gerichteten Graphen G = (V, E) berechnet. 25.2-9 Nehmen Sie an, wir könnten den transitiven Abschluss eines gerichteten azyklischen Graphen in Zeit f (|V | , |E|) berechnen, wobei f eine monoton steigende Funktion in |V | und |E| ist. Zeigen Sie, dass wir dann den transitiven Abschluss G∗ = (V, E ∗ ) eines allgemeinen gerichteten Graphen G = (V, E) in Zeit f (|V | , |E|) + O(V + E ∗ ) berechnen können.
25.3 Johnsons Algorithmus für dünn besetzte Graphen
25.3
713
Johnsons Algorithmus für dünn besetzte Graphen
Der Algorithmus von Johnson bestimmt die kürzesten Pfade zwischen allen Knotenpaaren in Zeit O(V 2 lg V + V E). Für dünn besetzte Graphen ist dies asymptotisch schneller als das wiederholte Quadrieren von Matrizen und als der Floyd-Warshall-Algorithmus. Der Algorithmus gibt entweder eine Matrix mit den Gewichten der kürzesten Pfade für alle Knotenpaare zurück oder meldet, dass der Eingabegraph einen Zyklus mit negativem Gewicht enthält. Der Algorithmus von Johnson verwendet als Unterroutinen sowohl Dijkstras Algorithmus als auch den Bellman-Ford-Algorithmus, die beide in Kapitel 24 beschrieben sind. Johnsons Algorithmus verwendet die Methode der Umwichtung , die folgendermaßen arbeitet. Falls alle Kantengewichte eines Graphen G = (V, E) nichtnegativ sind, dann können wir die kürzesten Pfade zwischen allen Knotenpaare bestimmen, indem wir für jeden Knoten einmal Dijkstras Algorithmus laufen lassen. Implementiert man die Min-Prioritätswarteschlange mit einem Fibonacci-Heap, so hat dieser Algorithmus die Laufzeit O(V 2 lg V + V E). Falls G Kanten mit negativem Gewicht, aber keinen Zyklus mit negativem Gewicht hat, berechnen wir einfach eine neue Menge von nichtnegativen Kantengewichten, die es uns erlaubt, den gleichen Weg zu gehen. Die neue Menge von Kantengewichten w muss zwei wichtige Bedingungen erfüllen: 1. Für alle Knotenpaare u, v ∈ V ist ein Pfad p genau dann ein kürzester Pfad von u nach v unter der Gewichtsfunktion w, wenn p auch unter der Gewichtsfunktion w ein kürzester Pfad von u nach v ist. 2. Für alle Kanten (u, v) ist das neue Kantengewicht nichtnegativ. Wie wir gleich sehen werden, können wir die neue Gewichtsfunktion w für G in Zeit O(V E) bestimmen.
Erhaltung der kürzesten Pfade bei Umwichtung Das folgende Lemma zeigt, wie einfach wir den Kanten neue Gewichte so zuordnen können, dass die erste Bedingung erfüllt ist. Mit δ bezeichnen wir die Gewichte der kürzesten Pfade, die sich aus der Gewichtsfunktion w ergeben und mit δ die Gewichte der kürzesten Pfade bezüglich der Gewichtsfunktion w. Lemma 25.1: (Die Umwichtung ändert die kürzesten Pfade nicht.) Sei G = (V, E) ein gewichteter gerichteter Graph mit der Gewichtsfunktion w : E → R und einer Funktion h : V → R, die die Knotenmenge auf die Menge der reellen Zahlen abbildet. Für jede Kante (u, v) ∈ E definieren wir w(u, v) = w(u, v) + h(u) − h(v) .
(25.9)
Sei p = v0 , v1 , . . . , vk ein beliebiger Pfad von Knoten v0 nach Knoten vk . Der Pfad p ist unter der Gewichtsfunktion w genau dann ein kürzester Pfad von v0
714
25 Kürzeste Pfade für alle Knotenpaare
nach vk , wenn er unter der Gewichtsfunktion w ein kürzester Pfad ist. Das heißt, 0 , vk ) gilt. Zudem besitzt = δ(v w(p) = δ(v0 , vk ) gilt genau dann, wenn auch w(p) G unter der Gewichtsfunktion w genau dann einen Zyklus mit negativem Gewicht, wenn G unter der Gewichtsfunktion w einen Zyklus mit negativem Gewicht besitzt.
Beweis: Wir zeigen zunächst, dass w(p) = w(p) + h(v0 ) − h(vk )
(25.10)
gilt. Es ist w(p) =
k
w(v i−1 , vi )
i=1
=
k
(w(vi−1 , vi ) + h(vi−1 ) − h(vi ))
i=1
=
k
w(vi−1 , vi ) + h(v0 ) − h(vk )
(da Teleskopreihe)
i=1
= w(p) + h(v0 ) − h(vk ) . Daher gilt w(p) = w(p) + h(v0 ) − h(vk ) für jeden Pfad p von v0 nach vk . Falls ein Pfad von v0 nach vk unter der Gewichtsfunktion kürzer als ein anderer ist, dann ist er auch 0 , vk ) gilt. = δ(v unter w kürzer. Also gilt w(p) = δ(v0 , vk ) genau dann, wenn w(p) Wir müssen noch zeigen, dass der Graph G unter der Gewichtsfunktion w genau dann einen Zyklus mit negativem Gewicht hat, wenn er unter der Gewichtsfunktion w einen solchen besitzt. Betrachten wir einen beliebigen Zyklus c = v0 , v1 , . . . , vk mit v0 = vk . Nach Gleichung (25.10) gilt w(c) = w(c) + h(v0 ) − h(vk ) = w(c) . Also ist das Gewicht von c genau dann negativ, wenn sein Gewicht unter w negativ ist.
Das Erzeugen nichtnegativer Gewichte durch Umwichtung Unser nächstes Ziel ist es, sicherzustellen, dass die zweite Bedingung erfüllt ist; d. h. wir fordern, dass w(u, v) für alle Kanten (u, v) ∈ E nichtnegativ ist. Zu einem gewichteten gerichteten Graphen G = (V, E) mit der Gewichtsfunktion w : E → R bilden wir einen neuen Graphen G = (V , E ) mit V = V ∪ {s} für einen neuen Knoten s ∈ V und E = E ∪{(s, v) : v ∈ V }. Wir setzen die Gewichtsfunktion w fort, indem wir w(s, v) = 0 für alle v ∈ V setzen. Beachten Sie, dass keine kürzesten Pfade in G, außer jenen, die
25.3 Johnsons Algorithmus für dünn besetzte Graphen
715
s als Startknoten haben, den Knoten s enthalten, da s keine eingehenden Kanten hat. Außerdem hat G genau dann keine Zyklen mit nichtnegativem Gewicht, wenn G keine solchen Zyklen enthält. Abbildung 25.6(a) zeigt den Graphen G , der zum Graphen G aus Abbildung 25.1 korrespondiert. Nehmen Sie nun an, dass G und G keine Zyklen mit negativem Gewicht enthalten würden. Für alle Knoten v ∈ V definieren wir h(v) = δ(s, v). Wegen der Dreiecksungleichung (Lemma 24.10) gilt h(v) ≤ h(u) + w(u, v) für alle Kanten (u, v) ∈ E . Wenn wir die neuen Gewichte w gemäß Gleichung (25.9) definieren, gilt also w(u, v) = w(u, v) + h(u) − h(v) ≥ 0, sodass wir die zweite Bedingung erfüllt haben. Abbildung 25.6(b) zeigt den Graphen G aus Abbildung 25.6(a) mit den neuen Gewichten.
Berechnung der kürzesten Pfade für alle Knotenpaare Johnsons Algorithmus zur Berechnung kürzester Pfade zwischen allen Knotenpaaren verwendet den Bellman-Ford-Algorithmus (Abschnitt 24.1) und Dijkstras Algorithmus (Abschnitt 24.3) als Unterroutinen. Er setzt implizit voraus, dass die Kanten in Adjazenzlisten gespeichert sind. Der Algorithmus gibt die übliche |V | × |V |-Matrix D = dij mit dij = δ(i, j) zurück oder meldet, dass der Eingabegraph einen Zyklus mit negativem Gewicht enthält. Wie bei einem Algorithmus zur Bestimmung kürzester Pfade für alle Knotenpaare üblich, setzen wir voraus, dass die Knoten von 1 bis |V | nummeriert sind. Johnson(G, w) 1 berechne G , mit G .V = G.V ∪ {s}, G .E = G.E ∪ {(s, v) : v ∈ G.V } und w(s, v) = 0 für alle v ∈ G.V 2 if Bellman-Ford(G , w, s) = = falsch 3 print “der Eingangsgraph enthält einen Zyklus mit negativem Gewicht” 4 else for jeden Knoten v ∈ G .V 5 setze h(v) auf den Wert δ(s, v), der durch den Bellman-Ford-Algorithmus berechnet wurde 6 for jede Kante (u, v) ∈ G .E 7 w(u, v) = w(u, v) + h(u) − h(v) 8 sei D = (duv ) eine neue n × n-Matrix 9 for jeden Knoten u ∈ G.V v) für alle v ∈ G.V zu berechnen 10 rufe Dijkstra(G, w, u) auf, um δ(u, 11 for jeden Knoten v ∈ G.V v) + h(v) − h(u) 12 duv = δ(u, 13 return D Dieser Code führt einfach nur die zuvor von uns spezifizierten Aktionen aus. Zeile 1 erzeugt G . Zeile 2 ruft den Bellman-Ford-Algorithmus für G mit der Gewichtsfunktion w und dem Startknoten s auf. Falls G , und somit auch G, einen Zyklus mit negativem Gewicht enthält, meldet Zeile 3 dieses Problem. Die Zeilen 4–12 setzen voraus, dass
716
25 Kürzeste Pfade für alle Knotenpaare
0 0 3 0
0
–4
4
1 6
5
0
0
0
0
3 2/–3
4
13
10
0
10
2/2
2/–1
4
5
(c)
0
13
2 0
2/–2 5
3 0/–5
2 (f)
2
5
0
2
0 4
0
4 1 2/7
13
2 0
0
10
0/1 4
2/3 5
3 0/0 0
0 2 (e)
0/5 4
0
4 1 4/8
13
2 0
10
0
2 2/5 0
4
3 0/–4 0
(d)
2 0/–1 1 2/2
13
2 0
2
10
–5 3
2 0/4 0
1 2/3
2
5
13
2 0/0 0
0/–4
2
(b)
2 2/1 4
0
–4
0 4
(a)
1 0/0
4 1 0
0 –5
7 –4
0
–5 3
8
2
2 –1
1 4
1 0
0
5
2 –1
0
0
10
0/0 4
0/0 5
3 2/1 0
0 2 (g)
2/6 4
Abbildung 25.6: Johnsons Algorithmus für das kürzeste-Pfade-Problem für alle Knotenpaare angewendet auf den Graphen aus Abbildung 25.1. Die Knotennummern sind außerhalb der Knoten angegeben. (a) Der Graph G mit der ursprünglichen Gewichtsfunktion w. Der neue Knoten s ist schwarz gekennzeichnet. Innerhalb eines Knotens v steht h(v) = δ(s, v). (b) Nachdem jeder Kante (u, v) das neue Gewicht w(u, v) = w(u, v) + h(u) − h(v) zugeordnet worden ist. (c)-(g) Das Ergebnis der Anwendung von Dijkstras Algorithmus auf alle Knoten von G mit der Gewichtsfunktion w. In jedem Teil der Abbildung ist der Startknoten u schwarz gezeichnet und die schattierten Kanten gehören zum Baum der kürzesten Pfade, der durch den Algorith v) und δ(u, v), getrennt mus berechnet wurde. Innerhalb eines Knotens v sind die Werte von δ(u, v) + h(v) − h(u). durch einen Schrägstrich, angegeben. Der Wert duv = δ(u, v) ist gleich δ(u,
25.3 Johnsons Algorithmus für dünn besetzte Graphen
717
G keine Zyklen mit negativem Gewicht enthält. In den Zeilen 4–5 wird h(v) auf das Gewicht δ(s, v) eines kürzesten Pfades gesetzt, das durch den Bellman-Ford-Algorithmus für alle v ∈ V berechnet wurde. Die Zeilen 6–7 berechnen die neuen Gewichte w. Für alle v) der Knotenpaare u, v ∈ V berechnet die for-Schleife der Zeilen 9–12 die Gewichte δ(u, kürzesten Pfade, indem Dijkstras Algorithmus für jeden Knoten aus V einmal aufgerufen wird. Zeile 12 speichert das korrekte Gewicht δ(u, v) der kürzesten Pfade, das mithilfe der Gleichung (25.10) berechnet wurde, im Matrixelement duv . Schließlich gibt Zeile 13 die fertige Matrix D zurück. Abbildung 25.6 illustriert die Arbeitsweise von Johnsons Algorithmus. Wenn wir die Min-Prioritätswarteschlange in Dijkstras Algorithmus durch einen Fibonacci-Heap implementieren, dann läuft Johnsons Algorithmus in Zeit O(V 2 lg V + V E). Die einfachere Implementierung durch einen binären Heap liefert eine Laufzeit von O(V E lg V ), was für dünn besetzte Graphen asymptotisch immer noch schneller ist als die Laufzeit des Floyd-Warshall-Algorithmus.
Übungen 25.3-1 Wenden Sie Johnsons Algorithmus an, um die kürzesten Pfade zwischen allen Knotenpaaren des Graphen aus Abbildung 25.2 zu bestimmen. Geben Sie die durch den Algorithmus berechneten Werte von h und w an. 25.3-2 Warum wird der neue Knoten s zu V hinzugenommen, um die neue Knotenmenge V zu bilden? 25.3-3 Setzen Sie voraus, dass w(u, v) ≥ 0 für alle Kanten (u, v) ∈ E gilt. Welche Beziehung besteht zwischen den Gewichtsfunktionen w und w? 25.3-4 Professor Greenstreet behauptet, dass es eine einfachere Möglichkeit als die in Johnsons Algorithmus verwendete Methode gäbe, um die Kanten neu zu wichten. Setze einfach w(u, v) = w(u, v) − w∗ mit w∗ = min(u,v)∈E {w(u, v)} für alle Kanten (u, v) ∈ E. Was ist falsch an der Methode des Professors? 25.3-5 Nehmen Sie an, wir würden Johnsons Algorithmus auf einem gerichteten Graphen G mit der Gewichtsfunktion w laufen lassen. Zeigen Sie: Falls G einen Zyklus c vom Gewicht 0 enthält, dann gilt w(u, v) = 0 für alle Kanten (u, v) von c. 25.3-6 Professor Michener behauptet, dass es nicht notwendig ist, in Zeile 1 von Johnson einen neuen Startknoten zu erzeugen. Er behauptet, dass wir stattdessen einfach G = G und für s einen beliebigen Knoten des Graphen verwenden könnten. Geben Sie ein Beispiel für einen gewichteten gerichteten Graphen G an, für den die Idee des Professors zu falschen Ergebnissen führt. Zeigen Sie dann, dass die Ergebnisse, die für stark zusammenhängende Graphen G (jeder Knoten ist von jedem anderen aus erreichbar) von der Prozedur Johnson mit der Modifikation des Professors zurückgegeben werden, korrekt sind.
718
25 Kürzeste Pfade für alle Knotenpaare
Problemstellungen 25-1 Transitiver Abschluss eines dynamischen Graphen Nehmen Sie an, wir wollten den transitiven Abschluss eines gerichteten Graphen G = (V, E) aufrechterhalten, wenn wir Kanten zu E hinzufügen. Das heißt, wir würden nach jedem Hinzufügen einer Kante den transitiven Abschluss der bisherigen Kanten aktualisieren wollen. Setzen Sie voraus, dass der Graph G anfangs keine Kanten enthält und dass wir den transitiven Abschluss durch eine Boolesche Matrix darstellen wollen. a. Zeigen Sie, wie wir den transitiven Abschluss G∗ = (V, E ∗ ) eines Graphen G = (V, E) in Zeit O(V 2 ) aktualisieren können, wenn eine neue Kante zu G hinzugefügt wird. b. Geben Sie ein Beispiel für einen Graphen G und eine Kante e an, bei dem unabhängig vom verwendeten Algorithmus Zeit Ω(V 2 ) erforderlich ist, um den transitiven Abschluss nach dem Einfügen der Kante e zu aktualisieren. c. Geben Sie einen effizienten Algorithmus an, der den transitiven Abschluss nach dem Einfügen von Kanten aktualisiert. Ihr Algorithmus sollte für jede n Folge von n Einfügeoperationen in einer Gesamtzeit i=1 ti = O(V 3 ) laufen, wobei ti die Zeit für das Aktualisieren des transitiven Abschlusses ist, nachdem die i-te Kante eingefügt wurde. Beweisen Sie, dass Ihr Algorithmus dieser Zeitschranke genügt. 25-2 Kürzeste Pfade in -dichten Graphen Ein Graph G = (V, E) ist -dicht, falls |E| = Θ(V 1+ ) für eine Konstante mit 0 < ≤ 1 gilt. Durch die Verwendung von d-nären Min-Heaps (siehe Problemstellung 6-2) in Algorithmen zur Berechnung kürzester Pfade in -dichten Graphen können wir die Laufzeiten von Algorithmen erreichen, die auf Fibonacci-Heaps basieren, ohne eine solch komplizierte Datenstruktur einsetzen zu müssen. a. Geben Sie die asymptotischen Laufzeiten der Operationen Insert, Extract-Min und Decrease-Key als Funktion in d und in der Anzahl n der Elemente in einem d-nären Min-Heap an? Wie verhalten sich diese Laufzeiten, wenn wir d = Θ(nα ) für eine Konstante 0 < α ≤ 1 wählen? Vergleichen Sie diese Laufzeiten mit den amortisierten Kosten dieser Operationen auf Fibonacci-Heaps. b. Zeigen Sie, wie wir auf einem -dichten gerichteten Graphen G = (V, E), der keine Kanten mit negativem Gewicht enthält, in Zeit O(E) die kürzesten Pfade von einem einzigen Startknoten aus berechnen können. (Hinweis: Wählen Sie d als Funktion in .) c. Zeigen Sie, wie wir das kürzeste-Pfade-Problem für alle Knotenpaare auf einem -dichten gerichteten Graphen G = (V, E), der keine Kanten mit negativem Gewicht enthält, in Zeit O(V E) lösen können. d. Zeigen Sie, wie wir das kürzeste-Pfade-Problem für alle Knotenpaare auf einem -dichten gerichteten Graphen G = (V, E) in Zeit O(V E) lösen können, wenn der Graph Kanten mit negativem Gewicht enthalten darf, aber keine Zyklen mit negativem Gewicht hat.
Kapitelbemerkungen zu Kapitel 25
719
Kapitelbemerkungen Lawler [224] macht eine gute Diskussion des kürzeste-Pfade-Problems für alle Knotenpaare, allerdings analysiert er die Lösungen für den Fall dünn besetzter Graphen nicht. Er schreibt den Algorithmus der Matrizenmultiplikation dem Allgemeinwissen zu. Der Floyd-Warshall-Algorithmus geht auf Floyd [105] zurück, der ihn auf einem Theorem von Warshall [349] aufgebaut hat, der beschreibt, wie der transitive Abschluss Boolescher Matrizen berechnet werden kann. Johnsons Algorithmus wurde der Referenz [192] entnommen. Verschiedene Wissenschaftler haben verbesserte Algorithmen für die Berechnung kürzester Pfade mittels Matrizenmultiplikation vorgeschlagen. Fredman [111] zeigt, wie das kürzeste-Pfade-Problem für alle Knotenpaare mit O(V 5/2 ) Vergleichen zwischen Summen von Kantengewichten gelöst werden kann, und erhält einen Algorithmus, der in Zeit O(V 3 (lg lg V / lg V )1/3 ) läuft, was etwas besser ist als die Laufzeit des FloydWarshall-Algorithmus. Han [159] reduziert die Laufzeit auf O(V 3 (lg lg V / lg V )5/4 ). Ein anderer Forschungsansatz zeigt, dass Algorithmen zur schnellen Matrizenmultiplikation (siehe Kapitelbemerkungen zu Kapitel 4) zur Lösung des kürzeste-Pfade-Problems für alle Knotenpaare angewendet werden können. Sei O(nω ) die Laufzeit des schnellsten Algorithmus für die Multiplikation von n × n-Matrizen, so gilt gegenwärtig ω < 2,376 [78]. Galil und Margalit [123, 124] und Seidel [308] haben Algorithmen entworfen, die das kürzeste-Pfade-Problem für alle Knotenpaare in ungewichteten ungerichteten Graphen in Zeit (V ω p(V )) lösen, wobei p eine spezielle Funktion ist, die polylogarithmisch in n beschränkt ist. Für dicht besetzte Graphen sind diese Algorithmen schneller als O(V E) und damit schneller als die Ausführung von |V | Graphtraversierungen mittels Breitensuche. Verschiedene Wissenschaftler haben diese Ergebnisse erweitert und haben Algorithmen entwickelt, die das kürzeste-Pfade-Problem für alle Knotenpaare in ungerichteten Graphen mit ganzzahligen Kantengewichten aus dem Bereich {1, 2, . . . , W } lösen. Der asymptotisch schnellste dieser Algorithmen geht auf Shoshan und Zwick [316] zurück und läuft in Zeit O(W V ω p(V W )). Karger, Koller und Phillips [196] und unabhängig von ihnen McGeoch [247] haben eine Zeitschranke angegeben, die von E ∗ abhängt, der Menge aller Kanten von E, die zu einem kürzesten Pfad gehören. Auf einem Graphen mit nichtnegativen Kantengewichten laufen ihre Algorithmen in Zeit O(V E ∗ + V 2 lg V ), was im Fall |E ∗ | = o(E) schneller ist als |V | Läufe von Dijkstras Algorithmus. Baswana, Hariharan und Sen [33] untersuchen dekrementierende Algorithmen, um die Informationen zu kürzesten Pfaden für alle Knotenpaare und zu dem transitiven Abschluss zu verwalten. Dekrementierende Algorithmen erlauben eine Folge von Kantenlöschungen und Anfragen, im Unterschied zu Problemstellung 25-1, in der Kanten eingefügt werden und ein inkrementierender Algorithmus verlangt ist. Die Algorithmen von Baswana, Hariharan und Sen sind randomisiert und, wenn ein Pfad existiert, kann ihr Algorithmus zur Bestimmung des transitiven Abschlusses mit Wahrscheinlichkeit 1/nc für ein beliebiges c > 0 in dem Sinne fehlschlagen, dass er nicht angibt, dass es einen solchen Pfad gibt. Die Anfragezeiten sind mit hoher Wahrscheinlichkeit in O(1). Für den transitiven Abschluss ist die amortisierte Zeit für jede Aktualisierung in O(V 4/3 lg1/3 V ). Bei kürzesten Pfade für alle Knotenpaare hängt die Aktualisierungs-
720
25 Kürzeste Pfade für alle Knotenpaare
zeit von den Abfragen ab. Für Anfragen nach den Gewichten kürzester Pfade ist die amortisierte Zeit pro Aktualisierung in O(V 3 /E lg2 V ). Für die√Ausgabe des tatsächlichen kürzesten Pfades ist die amortisierte Zeit in min(O(V 3/2 lg V ), O(V 3 /E lg2 V )). Demetrescu und Italiano [84] zeigten, wie Aktualisierungen und Anfrage-Operationen zu handhaben sind, wenn Kanten sowohl eingefügt als auch gelöscht werden, jede gegebene Kante dabei aber nur Werte aus einem eingeschränkten Bereich der reellen Zahlen annehmen darf. Aho, Hopcroft und Ullman [5] haben eine algebraische Struktur definiert, die als „abgeschlossener Semiring“ bezeichnet wird und die zum Lösen von Pfadproblemen auf gerichteten Graphen dient. Sowohl der Floyd-Warshall-Algorithmus als auch der Algorithmus für den transitiven Abschluss aus Abschnitt 25.2 sind Instanzen eines Algorithmus für alle Knotenpaare, der auf abgeschlossenen Semiringen basiert. Maggs und Plotkin [240] zeigten, wie man mithilfe eines abgeschlossenen Semirings minimale Spannbäume bestimmen kann.
26
Maximaler Fluss
Ebenso wie wir eine Straßenkarte durch einen gerichteten Graphen modellieren können, um den kürzesten Weg von einem Punkt zu einem anderen zu finden, können wir einen gerichteten Graphen auch als „Flussnetzwerk“ interpretieren und ihn verwenden, um Fragen über Materialflüsse zu beantworten. Stellen Sie sich ein Material vor, das durch ein System fließt. Der Ausgangspunkt ist eine Quelle, wo das Material produziert wird, und der Endpunkt eine Senke, wo es verbraucht wird. Die Quelle produziert das Material mit einer konstanten Rate und die Senke verbraucht es mit der gleichen Rate. Der „Fluss“ des Materials an einem beliebigen Punkt des Systems ist die Rate, mit der sich das Material bewegt. Flussnetzwerke können viele Probleme modellieren, zum Beispiel flüssiges Material, das durch Leitungen fließt, Bauteile auf Fließbändern, Strom in elektrischen Netzwerken oder Information in Kommunikationsnetzwerken. Wir können uns jede gerichtete Kante in einem Flussnetzwerk als Kanal vorstellen, durch den das Material fließt. Jeder Kanal hat eine festgelegte Kapazität, die durch die maximale Rate gegeben ist, mit der das Material durch den Kanal fließen kann, also zum Beispiel eine Angabe wie 800 Liter Flüssigkeit pro Stunde durch ein Rohr oder eine Stromstärke von 20 Ampere durch eine elektrische Leitung. Die Knoten sind die Kanalverbindungen. Mit Ausnahme der Quelle und der Senke fließt das Material durch diese Kanalverbindungen hindurch, ohne sich dort anzusammeln. Das heißt, die Rate, mit der das Material in einen Knoten hineinfließt, ist gleich der Rate, mit der es den Knoten verlässt. Wir bezeichnen diese Eigenschaft als „Flusserhaltung“; sie ist äquivalent zur Kirchhoffschen Knotenregel, wenn das Material elektrischer Strom ist. Beim Problem der Berechnung eines maximalen Flusses wollen wir die maximale Rate berechnen, mit der wir das Material von der Quelle zur Senke transportieren können, ohne dass wir eine Kapazitätsbeschränkung verletzen. Dies ist eines der einfachsten Probleme für Flussnetzwerke, und, wie wir in diesem Kapitel sehen werden, kann das Problem durch effiziente Algorithmen gelöst werden. Darüber hinaus können die grundlegenden Methoden, die in Algorithmen zur Bestimmung des maximalen Flusses angewendet werden, auch für die Lösung anderer Probleme in Flussnetzwerken angepasst werden. Dieses Kapitel stellt zwei allgemeine Methoden vor, die das Problem der Berechnung eines maximalen Flusses lösen. Abschnitt 26.1 formalisiert die Begriffe von Flüssen und Flussnetzwerken, um so zu einer formalen Definition des maximalen-Fluss-Problems zu kommen. In Abschnitt 26.2 wird die klassische Methode von Ford und Fulkerson zur Bestimmung von maximalen Flüssen beschrieben. Eine Anwendung dieser Methode, die Bestimmung eines maximalen Matchings in einem ungerichteten bipartiten Graphen, wird in Abschnitt 26.3 besprochen. Abschnitt 26.4 stellt die Push/Relabel-Methode vor, die vielen der schnellsten Algorithmen zur Lösung von Flussnetzwerk-Problemen
722
26 Maximaler Fluss
zugrunde liegt. Abschnitt 26.5 widmet sich dem „Relabel-to-Front“-Algorithmus, einer speziellen Implementierung der Push/Relabel-Methode, die in Zeit O(V 3 ) läuft. Obwohl dieser Algorithmus nicht der schnellste bekannte Algorithmus ist, illustriert er doch einige Methoden, die in den asymptotisch schnellsten Algorithmen verwendet werden und ist in der Praxis von akzeptabler Effizienz.
26.1
Flussnetzwerke
In diesem Abschnitt geben wir eine graphentheoretische Definition von Flussnetzwerken, diskutieren deren Eigenschaften und definieren das Problem der Berechnung eines maximalen Flusses formal. Außerdem führen wir eine Reihe nützlicher Bezeichnungen ein.
Flussnetzwerke und Flüsse Ein Flussnetzwerk G = (V, E) ist ein gerichteter Graph, in dem jede Kante (u, v) ∈ E eine nichtnegative Kapazität c(u, v) ≥ 0 hat. Wir fordern desweiteren, dass, wenn E eine Kante (u, v) enthält, es keine Kante (v, u) in der entgegengesetzten Richtung gibt. (Wir werden später sehen, wie wir diese Einschränkung umgehen können.) Ist (u, v) ∈ E, so definieren wir der Einfachheit halber, dass c(u, v) = 0 gilt; zudem verbieten wir Schlingen, d. h. Kanten deren Anfangs- und Endknoten gleich sind. Ein Flussnetzwerk enthält zwei ausgezeichnete Knoten, die eine besondere Rolle spielen: die Quelle s und die Senke t. Der Einfachheit halber setzen wir voraus, dass jeder Knoten auf einem Pfad von der Quelle zur Senke liegt, d. h. dass das Flussnetzwerk für jeden Knoten v ∈ V einen Pfad s ; v ; t enthält. Der Graph ist daher zusammenhängend und, da jeder Knoten mit Ausnahme von s wenigstens eine eingehende Kante hat, gilt |E| ≥ |V | − 1. Abbildung 26.1 zeigt ein Beispiel eines Flussnetzwerkes. Wir sind nun in der Lage, Flüsse formaler zu definieren. Sei G = (V, E) ein Flussnetzwerk mit einer Kapazitätsfunktion c. Weiter seien s die Quelle und t die Senke des Flussnetzwerkes. Ein Fluss in G ist eine reellwertige Funktion f : V × V → R, die die folgenden zwei Bedingungen erfüllt: Kapazitätsbeschränkung: Für alle u, v ∈ V fordern wir 0 ≤ f (u, v) ≤ c(u, v). Flusserhaltung: Für alle u ∈ V − {s, t} fordern wir f (v, u) = f (u, v) . v∈V
v∈V
Ist (u, v) ∈ E, so kann es keinen (direkten) Fluss von u nach v geben, sodass wir f (u, v) = 0 setzen können. Wir bezeichnen den nichtnegativen Wert f (u, v) als den Fluss von Knoten u nach Knoten v. Der Wert |f | eines Flusses f ist definiert als f (s, v) − f (v, s) , (26.1) |f | = v∈V
v∈V
26.1 Flussnetzwerke
723
13
v2
14
Calgary
v4
4
6 1/1
Winnipeg
v1
12/12
v3
1 s
8/1
3
v2
11/14
15/
20
7/7
t
7
4
s
20
9
v3
16 9
Vancouver
Saskatoon 12
4/
v1
1/4
Edmonton
v4
t
4/4
Regina (a)
(b)
Abbildung 26.1: (a) Ein Flussnetzwerk G = (V, E) für das Transportproblem der Firma Lucky Puck. Die Fabrik in Vancouver ist die Quelle s und das Lager in Winnipeg die Senke t. Die Firma transportiert die Pucks über verschiedene Zwischenstationen, wobei aber nur c(u, v) Kisten pro Tag von der Stadt u zur Stadt v befördert werden können. An jeder Kante ist deren Kapazität vermerkt. (b) Ein Fluss f in G mit dem Wert |f | = 19. Jede Kante ist mit f (u, v)/c(u, v) markiert. Der Schrägstrich trennt nur die Angabe des Flusses über die Kante und die Angabe der Kapazität der Kante und ist nicht als Division zu verstehen.
d. h. der Wert eines Flusses ist gegeben durch den gesamten Fluss, der aus der Quelle herausfließt, minus dem Fluss, der in die Quelle hineinfließt. (Hier steht die |·|-Notation für den Wert des Flusses und nicht für einen Absolutbetrag oder eine Kardinalität.) Üblicherweise wird ein Flussnetzwerk keine Kanten enthalten, die in die Quelle hineinlaufen, sodass der Fluss, der in die Quelle hineinfließt und durch die Summenformel f v∈V (v, s) gegeben ist, gleich 0 ist. Da in die Quelle einlaufende Flüsse jedoch bei Restnetzwerken, denen wir später in diesem Kapitel noch begegnen werden, eine wichtige Rolle spielen, belassen wir sie aber in der formalen Definition. In dem maximalenFluss-Problem ist ein Flussnetzwerk G mit Quelle s und Senke t gegeben und wir wollen einen Fluss mit maximalem Wert berechnen. Bevor wir uns ein Beispiel eines Flussnetzwerk-Problems anschauen, lassen Sie uns kurz die Definition eines Flusses und der zwei Flusseigenschafen etwas näher betrachten. Die Eigenschaft der Kapazitätsbeschränkung sagt einfach aus, dass der Fluss von einem Knoten zu einem anderen Knoten nicht negativ sein darf und die gegebene Kapazität nicht übersteigen darf. Die Eigenschaft der Flusserhaltung sagt aus, dass der gesamte Fluss, der in einen Knoten, der weder die Quelle noch die Senke ist, hineinfließt, gleich dem gesamten Fluss, der aus diesem Knoten herausfließt, sein muss – „eingehender Fluss gleich ausgehender Fluss“, um es lax auszudrücken.
Ein Beispiel für einen Fluss Ein Flussnetzwerk kann das in Abbildung 26.1(a) gezeigte Problem modellieren. Die Firma Lucky Puck besitzt eine Fabrik (Quelle s) in Vancouver, die Hockeypucks herstellt, und ein Lager (Senke t) in Winnipeg. Lucky Puck mietet von einer anderen Firma Frachtraum auf LKWs, um die Pucks von der Fabrik zum Lager zu transportieren. Da die LKWs entlang bestimmter Routen (Kanten) zwischen den Städten verkehren und eine beschränkte Platzkapazität haben, kann Lucky Puck höchstens c(u, v) Kisten pro Tag zwischen jedem Paar von Städten u und v (siehe Abbildung 26.1(a)) transportieren.
724
26 Maximaler Fluss
13
v2
14 (a)
s
10
v′ 10
v4
4
13
v3
v2
14
20 t
7
16 t
7
4
10
s
9
16
12
v1
20
9
v3
4
12
v1
v4
4
(b)
Abbildung 26.2: Transformation eines Netzwerkes mit antiparallelen Kanten in ein äquivalentes Netzwerk ohne antiparallele Kanten. (a) Ein Flussnetzwerk, das die beiden Kanten (v1 , v2 ) und (v2 , v1 ) enthält. (b) Ein dazu äquivalentes Netzwerk ohne antiparallele Kanten. Wir haben den neuen Knoten v hinzugefügt und die Kante (v1 , v2 ) durch die zwei Kanten (v1 , v ) und (v , v2 ) ersetzt, wobei beide Kanten die gleiche Kapazität wie die Kante (v1 , v2 ) haben.
Lucky Puck hat keine Kontrolle über diese Routen und Kapazitäten und so kann das Unternehmen das in Abbildung 26.1(a) gezeigte Flussnetzwerk nicht verändern. Das Unternehmen hat die maximale Anzahl p der Kisten zu berechnen, die pro Tag transportiert werden können, und dann die entsprechende Anzahl von Pucks herzustellen, da es keinen Sinn macht, mehr Pucks herzustellen, als zum Lager transportiert werden können. Lucky Puck ist es egal, wie lange ein bestimmter Puck braucht, um von der Fabrik bis zum Lager zu gelangen; sie wollen nur, dass p Kisten pro Tag die Fabrik verlassen und p Kisten pro Tag im Lager eintreffen. Wir können den „Fluss“ des Frachtguts durch einen Fluss in diesem Netzwerk modellieren, da die Anzahl der von einer Stadt zu einer anderen Stadt verfrachteten Kisten einer Kapazitätsbeschränkung unterliegt. Zudem muss das Modell die Flusserhaltung gewährleisten, da die Rate, mit der die Pucks in einer Zwischenstation ankommen, gleich der Rate sein muss, mit der sie sie verlassen. Anderenfalls würden sich die Kisten in Zwischenstädten anstauen.
Modellierung von Problemen mit antiparallelen Kanten Setzen Sie voraus, dass das Transportunternehmen der Firma Lucky Puck die Möglichkeit angeboten hat, Platz für 10 Kisten in Lastern zu mieten, die von Edmonton nach Calgary fahren. Es scheint naheliegend zu sein, diese Möglichkeit in unser Beispiel hinzuzufügen und das Netzwerk, das in Abbildung 26.2(a) gezeigt wird, zu konstruieren. Dieses Netzwerk hat jedoch ein Problem: es verletzt unsere ursprüngliche Voraussetzung, dass, wenn eine Kante (v1 , v2 ) ∈ E ist, die Kante (v2 , v1 ) nicht in E ist, d. h. (v2 , v1 ) ∈ E gilt. Wir sagen, dass die zwei Kanten (v1 , v2 ) und (v2 , v1 ) zueinander antiparallel sind. Wenn wir also ein Flussproblem mit antiparallelen Kanten modellieren wollen, müssen wir das Netzwerk in ein äquivalentes Netzwerk, das keine antiparallelen Kanten enthält, umformen. Abbildung 26.2(b) zeigt dieses äquivalente Netzwerk. Wir wählen eine der beiden zueinander antiparallelen Kanten aus – in diesem Fall die Kante (v1 , v2 ) – und spalten sie auf, indem wir einen neuen Knoten v hinzufügen und die
26.1 Flussnetzwerke
725
Kante (v1 , v2 ) durch die zwei Kanten (v1 , v ) und (v , v2 ) ersetzen. Wir setzen zudem die Kapazität der zwei neuen Kanten auf die Kapazität der ursprünglichen Kante. Das so resultierende Netzwerk erfüllt die Eigenschaft, dass, wenn eine Kante in dem Netzwerk enthalten ist, die entgegengesetzte Kante dies nicht ist. Übung 26.1-1 verlangt von Ihnen, zu beweisen, dass das resultierende Netzwerk äquivalent zu dem ursprünglichen Netzwerk ist. Wir sehen also, dass ein praktisch relevantes Flussproblem es möglicherweise nahelegt, dass es durch ein Netzwerk mit antiparallelen Kanten modelliert wird. Es erweist sich jedoch als günstiger, antiparallele Kanten zu verbieten, und so haben wir eine einfache Möglichkeit, ein Netzwerk mit antiparallelen Kanten in ein äquivalentes Netzwerk ohne antiparallele Kanten zu transformieren.
Netzwerke mit mehreren Quellen und Senken In einem maximalen-Fluss-Problem können mehrere Quellen und Senken statt jeweils einer einzigen auftreten. Die Firma Lucky Puck könnte beispielsweise eine Menge von m Fabriken {s1 , s2 , . . . , sm } und eine Menge von n Lagern {t1 , t2 , . . . , tn } besitzen (siehe Abbildung 26.3(a)). Erfreulicherweise ist dieses Problem nicht schwieriger zu lösen als das gewöhnliche maximale-Fluss-Problem. Wir können das Problem, den maximalen Fluss in einem Netzwerk mit mehreren Quellen und Senken zu bestimmen, auf ein gewöhnliches maximales-Fluss-Problem zurückführen. Abbildung 26.3(b) zeigt, wie wir das Netzwerk aus Teil (a) in ein Flussnetzwerk mit nur einer Quelle und einer Senke überführen können. Wir fügen eine Superquelle s und für alle i = 1, 2, . . . , m eine gerichtete Kante (s, si ) mit der Kapazität c(s, si ) = ∞ ein. Außerdem erzeugen wir eine neue Supersenke t und fügen für jedes i = {1, 2, . . . , n} eine gerichtete Kante (ti , t) mit der Kapazität c(ti , t) = ∞ ein. Offensichtlich entspricht jeder Fluss im Netzwerk (a) einem Fluss in Netzwerk (b) und umgekehrt. Die Superquelle versorgt die multiplen Quellen si mit dem gewünschten Fluss, und die Supersenke verbraucht gerade soviel, wie von den multiplen Senken ti gewünscht ist. In Übung 26.12 sollen Sie formal beweisen, dass die beiden Probleme äquivalent sind.
Übungen 26.1-1 Zeigen Sie, dass das Aufspalten einer Kante in einem Flussnetzwerk zu einem äquivalenten Netzwerk führt. Um es formaler zu formulieren, setzen Sie voraus, dass das Flussnetzwerk G die Kante (u, v) enthält und dass wir ein neues Flussnetzwerk G konstruieren, in dem wir einen neuen Knoten x einfügen und die Kante (u, v) durch die neuen Kanten (u, x) und (x, v) mit c(u, x) = c(x, v) = c(u, v) ersetzen. Zeigen Sie, dass ein maximaler Fluss in G den gleichen Wert hat wie ein maximaler Fluss in G. 26.1-2 Verallgemeinern Sie die Eigenschaften und Definitionen von Flüssen mit mehreren Quellen und Senken und dem dazugehörigen Flussproblem. Zeigen Sie, dass jeder Fluss in einem Netzwerk mit mehreren Quellen und Senken einem Fluss mit identischem Wert in einem Flussnetzwerk mit nur einer Quelle und
726
26 Maximaler Fluss
s1
s1
10
10
s2
12 t1
15
5
∞
8
6
s3
8 t2
s
∞
14
13 18
11
t3
∞
s4
7
13
s4
∞
t
t3
18
11
2
t2
20
14
∞
7
6
s3
20
t1
15
∞
5
3
s2
∞
3
∞
12
2
s5
s5 (a)
(b)
Abbildung 26.3: Tranformation eines Flussproblems mit mehreren Quellen und Senken in ein Problem mit einer einzigen Quelle und einer einzigen Senke. (a) Ein Flussnetzwerk mit fünf Quellen S = {s1 , s2 , s3 , s4 , s5 } und drei Senken T = {t1 , t2 , t3 }. (b) Ein äquivalentes Flussnetzwerk mit nur einer Quelle und einer Senke. Wir führen eine Superquelle s und je eine Kante mit unendlicher Kapazität von s zu jeder der multiplen Quellen ein. Außerdem führen wir eine Supersenke t und jeweils eine Kante mit unendlicher Kapazität von jeder der multiplen Senken zu t ein.
einer Senke, welches wir durch Einführen einer Superquelle und einer Supersenke erhalten, entspricht, und umgekehrt. 26.1-3 Nehmen Sie an, ein Flussnetzwerk G = (V, E) würde die Voraussetzung, dass das Netzwerk für jeden Knoten v ∈ V einen Pfad s ; v ; t enthält, verletzen. Sei u ein Knoten, für den es keinen Pfad s ; u ; t gibt. Zeigen Sie, dass es dann einen maximalen Fluss f in G geben muss, für den f (u, v) = f (v, u) = 0 für alle Knoten v ∈ V gilt. 26.1-4 Sei f ein Fluss in einem Netzwerk und α eine reelle Zahl. Das mit αf bezeichnete skalare Flussprodukt ist eine Funktion von V × V nach R, die durch (αf )(u, v) = α · f (u, v) definiert ist. Beweisen Sie, dass die Flüsse in einem Netzwerk eine konvexe Menge bilden. Sie sollen also zeigen, dass für alle α aus dem Bereich 0 ≤ α ≤ 1 αf1 + (1 − α)f2 ein Fluss ist, wenn f1 und f2 Flüsse sind.
26.2 Die Ford-Fulkerson-Methode
727
26.1-5 Formulieren Sie das maximale-Fluss-Problem als lineares Programm. 26.1-6 Professor Adam hat zwei Kinder, die einander bedauerlicherweise nicht mögen. Die Sache ist so ernst, dass sie es nicht nur ablehnen, miteinander zur Schule zu gehen, sondern sich sogar weigern, einen Häuserblock zu betreten, in dem das andere Kind an diesem Tag schon war. Die Kinder haben kein Problem damit, wenn sich ihre Wege an einer Straßenecke treffen. Zum Glück liegen sowohl das Haus des Professors als auch die Schule an einer Straßenecke, aber trotzdem ist er sich nicht sicher, ob es möglich ist, beide auf die gleiche Schule zu schicken. Der Professor besitzt eine Straßenkarte seiner Stadt. Formulieren Sie das Problem, festzustellen, ob beide Kinder auf die gleiche Schule gehen können, als maximales-Fluss-Problem. 26.1-7 Setzen sie voraus, dass ein Fluss neben Kantenkapazitäten auch Knotenkapazitäten zu berücksichtigen hat. Zu jedem Knoten v gibt es eine Schranke l(v), die angibt, wie viel Fluss durch v fließen darf. Zeigen Sie, wie wir ein Flussnetzwerk G = (V, E) mit Knotenkapazitäten in ein äquivalentes Flussnetzwerk G = (V , E ) ohne Knotenkapazitäten umformen können, sodass ein maximaler Fluss in G den gleichen Wert wie ein maximaler Fluss in G hat. Wie viele Knoten und Kanten hat G ?
26.2
Die Ford-Fulkerson-Methode
Dieser Abschnitt stellt die Ford-Fulkerson-Methode für die Lösung des maximalenFluss-Problems vor. Wir sprechen in diesem Fall von einer „Methode“ und nicht von einem „Algorithmus“, da sie verschiedene Implementierungen mit unterschiedlichen Laufzeiten ermöglicht. Die Ford-Fulkerson-Methode basiert auf drei wichtigen Ideen, die über diese Methode hinausreichen und für viele Flussalgorithmen und -probleme von Bedeutung sind: Restnetzwerke, Erweiterungspfade und Schnitte. Diese Ideen sind wesentlich für das wichtige maxflow-mincut-Theorem (Theorem 26.6), welches den Wert des maximalen Flusses mithilfe von Schnitten durch das Netzwerk charakterisiert. Wir beschließen diesen Abschnitt mit der Vorstellung einer spezifischen Implementierung der Ford-Fulkerson-Methode und analysieren deren Laufzeit. Die Ford-Fulkerson-Methode erhöht iterativ den Wert des Flusses. Wir starten mit f (u, v) = 0 für alle u, v ∈ V , was einem Anfangsfluss vom Wert 0 entspricht. In jeder Iteration erhöhen wir den Wert des Flusses, indem wir einen „Erweiterungspfad“ in einem dazugehörigen „Restnetzwerk“ Gf bestimmen. Sobald wir die Kanten eines Erweiterungspfades aus Gf kennen, können wir leicht Kanten aus G identifizieren, für die wir den Fluss so ändern können, dass wir den Wert des Flusses erhöhen. Wenngleich jede Iteration der Ford-Fulkerson-Methode den Wert des Flusses erhöht, werden wir sehen, dass der Fluss einer einzelnen Kante von G sich erhöhen oder verringern kann; das Verringern des Flusses auf einigen Kanten kann notwendig sein, um es einem Algorithmus zu erlauben, mehr Fluss von der Quelle zur Senke zu schicken. Wir erhöhen den Fluss solange schrittweise, bis das Restnetzwerk keinen Erweiterungspfad mehr hat. Das maxflow-mincut-Theorem wird zeigen, dass dieser Prozess bei Terminierung zu einem maximalen Fluss führt.
728
26 Maximaler Fluss
Ford-Fulkerson-Method(G, s, t) 1 initialisiere den Fluss f mit 0 2 while es gibt einen Erweiterungspfad p im Restnetzwerk Gf 3 erhöhe den Fluss f entlang p 4 return f Um die Ford-Fulkerson-Methode zu implementieren und zu analysieren, müssen wir noch einige weitere Konzepte einführen.
Restnetzwerke Informal betrachtet besteht das Restnetzwerk zu einem gegebenen Flussnetzwerk G und einem gegebenen Fluss f aus Kanten mit Kapazitäten, die angeben, wie wir den Fluss auf Kanten von G verändern können. Der Fluss einer Kante im Flussnetzwerk kann um einen Betrag erhöht werden, der nach oben durch die Differenz der Kantenkapazität und des Flusses auf dieser Kante beschränkt ist. Wenn diese Differenz positiv ist, dann nehmen wir diese Kante in das Restnetzwerk Gf mit auf und ordnen ihr eine „Restkapazität“ von cf (u, v) = c(u, v) − f (u, v) zu. Es sind nur die Kanten aus G in Gf , die eine (echte) Erhöhung ihres Flusses erlauben; die Kanten (u, v), deren Fluss gleich ihrer Kapazität ist, haben cf (u, v) = 0 und sind nicht in Gf enthalten. Das Restnetzwerk Gf kann aber auch Kanten enthalten, die nicht in G enthalten sind. Während ein Algorithmus den Fluss mit dem Ziel verändert, den Gesamtfluss zu erhöhen, muss er möglicherweise den Fluss auf einer bestimmten Kante verringern. Um eine mögliche Verringerung eines positiven Flusses f (u, v) auf einer Kante von G darzustellen, fügen wir eine Kante (v, u) in Gf mit der Restkapazität cf (v, u) = f (u, v) ein – d. h. eine Kante, die einen Fluss entgegen der Richtung von (u, v) erlaubt und über die der Fluss auf (u, v) höchstens aufgehoben werden kann. Eine solche entgegengerichtete Kante in dem Restnetzwerk erlaubt einem Algorithmus, einen Fluss, der bereits über eine Kante geschickt worden ist, zurückzuschicken. Das Zurückschicken eines Flusses entlang einer Kante ist äquivalent mit dem Verringern des Flusses auf der Kante, was eine zentrale Operation in vielen Algorithmen ist. Um dies formaler zu fassen, sei G = (V, E) ein Flussnetzwerk mit Quelle s und Senke t. Sei weiter f ein Fluss in G und betrachten Sie zwei Knoten u, v ∈ V . Wir definieren die Restkapazität cf (u, v) durch ⎧ ⎪ ⎨ c(u, v) − f (u, v) falls (u, v) ∈ E , falls (v, u) ∈ E , (26.2) cf (u, v) = f (v, u) ⎪ ⎩0 anderenfalls . Wegen unserer Voraussetzung, dass (u, v) ∈ E impliziert, dass (v, u) ∈ E gilt, trifft jeweils nur genau einer der Fälle aus Gleichung (26.2) auf jedes geordnete Paar von Knoten zu. Lassen Sie uns ein Beispiel zu Gleichung (26.2) angeben: Wenn c(u, v) = 16 und f (u, v) = 11 gelten, dann können wir den Fluss f (u, v) um cf (u, v) = 5 Einheiten
26.2 Die Ford-Fulkerson-Methode
v2
11/14
v4
4/4
v3
13
v2
11/14
19/
v4
t
4/4
(c)
11 1
s
12
12
v1
5
20
7/7
9
1/4
12/
t
4
v4
11
11 s
15
(b)
3
12/12
7 3
v2
1
v1
5
5
8
(a)
/16
v3
4 3
11 5
s
1
t
12
v1
5
20
v2
3
v3 7
3
15/
9
8/1
v3 7/7
s
12/12
4/ 9
v1 1/4
16 11/
729
v4
1 19
t
4
11 (d)
Abbildung 26.4: (a) Das Flussnetzwerk G und der Fluss f aus Abbildung 26.1(b). (b) Das Restnetzwerk Gf mit dem Erweiterungspfad p, der schattiert dargestellt ist; seine Restkapazität beträgt cf (p) = cf (v2 , v3 ) = 4. Kanten, deren Restkapazitäten gleich 0 sind, wie zum Beispiel Kante (v1 , v3 ), sind nicht eingezeichnet – eine Konvention, der wir auch im Rest dieses Abschnitts folgen. (c) Der Fluss in G, der entsteht, wenn wir die Flüsse entlang des Pfades p um die Restkapazität 4 des Pfades erhöhen. Kanten, über die kein Fluss fließt, wie zum Beispiel Kante (v3 , v2 ), sind nur mit ihren Kapazitäten markiert – eine weitere Konvention, der wir im Folgenden folgen werden. (d) Das Restnetzwerk, das durch den in (c) dargestellten Fluss induziert wird.
erhöhen, ohne die Kapazitätsbeschränkung der Kante (u, v) zu verletzen. Wir wollen es einem Algorithmus auch erlauben, 11 Einheiten von v nach u zurückfließen zu lassen, und so ist cf (v, u) = 11. Für ein gegebenes Flussnetzwerk G = (V, E) und einen gegebenen Fluss f ist das durch f induzierte Restnetzwerk Gf = (V, Ef ) durch Ef = {(u, v) ∈ V × V : cf (u, v) > 0}
(26.3)
gegeben. Das heißt, jede Kante des Restnetzwerks kann einen Fluss aufnehmen, der größer als 0 ist. Wir nennen eine solche Kante auch Restkante. Abbildung 26.4(a) zeigt noch einmal das Flussnetzwerk aus Abbildung 26.1(b), und Abbildung 26.4(b) zeigt das zugehörige Restnetzwerk Gf . Jede Kante aus Ef ist entweder eine Kante aus E oder eine einer Kante aus E entgegengerichtete Kante, und somit gilt |Ef | ≤ 2 |E| . Sie sollten bemerken, dass das Restnetzwerk Gf einem Flussnetzwerk mit Kapazitäten, die durch cf gegeben sind, sehr ähnlich ist. Es erfüllt jedoch nicht unsere Definition eines Flussnetzwerkes, da es sowohl eine Kante (u, v) als auch seine entgegengerichtete
730
26 Maximaler Fluss
Kante (v, u) enthalten kann. Von diesem Punkt abgesehen, besitzt ein Restnetzwerk aber die gleichen Eigenschaften wie ein Flussnetzwerk und wir können einen Fluss in dem Restnetzwerk definieren, der der Definition eines Flusses bezüglich den Kapazitäten cf im Netzwerk Gf genügt. Ein Fluss in einem Restnetzwerk gibt uns einen Plan, wie wir den Fluss in dem ursprünglichen Flussnetzwerk erhöhen können. Ist f ein Fluss in G und f ein Fluss in dem korrespondierenden Restnetzwerk Gf , dann definieren wir die Erhöhung f ↑ f eines Flusses f um f als die Funktion von V × V nach R, die durch
(f ↑ f )(u, v) =
f (u, v) + f (u, v) − f (v, u) 0
falls (u, v) ∈ E , anderenfalls
(26.4)
definiert ist. Die hinter dieser Definition stehende Intuition folgt der Definition des Restnetzwerkes. Wir erhöhen den Fluss auf (u, v) um f (u, v) und verringern ihn um f (v, u), da das Zurückfließenlassen eines Flusses auf einer entgegengerichteten Kante äquivalent dazu ist, den Fluss in dem ursprünglichen Netzwerk zu verringern. Einen Fluss über eine entgegengerichtete Kante in einem Restnetzwerk zurückfließen zu lassen, ist auch unter dem Begriff Löschung bekannt. Wenn wir beispielsweise 5 Kisten Hockey Pucks von u nach v schicken wollen und 2 Kisten von v nach u, dann könnten wir auch (jedenfalls aus Sicht des Endresultats) einfach 3 Kisten von u nach v und keine Kiste von v nach u schicken. Entsprechende Löschungen spielen eine zentrale Rolle in vielen Algorithmen zur Berechnung des maximalen Flusses.
Lemma 26.1 Sei G = (V, E) ein Flussnetzwerk mit Quelle s und Senke t und sei f ein Fluss in G. Sei Gf das Restnetzwerk von G, das durch f induziert wird, und sei f ein Fluss in Gf . Dann definiert die Funktion f ↑ f aus Gleichung (26.4) einen Fluss in G mit dem Wert |f ↑ f | = |f | + |f |. Beweis: Wir überprüfen zuerst, dass f ↑ f der Kapazitätsbeschränkung einer jeden Kante aus E und der Flusserhaltung an jedem Knoten aus V − {s, t} genügt. In Bezug auf die Kapazitätsbeschränkung sollten Sie zuerst bemerken, dass, wenn (u, v) ∈ E ist, dann gilt cf (v, u) = f (u, v). Daraus folgt f (v, u) ≤ cf (v, u) = f (u, v) und somit (f ↑ f )(u, v) = f (u, v) + f (u, v) − f (v, u) ≥ f (u, v) + f (u, v) − f (u, v) = f (u, v) ≥0 .
(wegen Gleichung (26.4)) (wegen f (v, u) ≤ f (u, v))
26.2 Die Ford-Fulkerson-Methode
731
Zudem gilt (f ↑ f )(u, v) = f (u, v) + f (u, v) − f (v, u) ≤ f (u, v) + f (u, v) ≤ f (u, v) + cf (u, v) = f (u, v) + c(u, v) − f (u, v) = c(u, v) .
(wegen Gleichung (26.4)) (da Flüsse nichtnegativ sind) (Kapazitätsbeschränkung) (Definition von cf )
Lassen Sie uns zu dem Beweis der Flusserhaltung kommen. Da sowohl f als auch f der Flusserhaltung genügen, haben wir für alle u ∈ V − {s, t},
(f ↑ f )(u, v) =
v∈V
(f (u, v) + f (u, v) − f (v, u))
v∈V
=
f (u, v) +
v∈V
=
=
f (u, v) −
v∈V
f (v, u) +
v∈V
f (v, u)
v∈V
f (v, u) −
v∈V
f (u, v)
v∈V
(f (v, u) + f (v, u) − f (u, v))
v∈V
=
(f ↑ f )(v, u) ,
v∈V
wobei die dritte Zeile aus der zweiten wegen der in f und f geltenden Flusserhaltung folgt. Um den Beweis abzuschließen, berechnen wir noch den Wert von f ↑ f . Rufen Sie sich in Erinnerung, dass wir keine antiparallelen Kanten in G erlauben (in Gf sind sie erlaubt), und so wissen wir für jeden Knoten v ∈ V , dass es eine Kante (s, v) oder (v, s) geben kann, aber nie beide zugleich. Wir definieren V1 = {v : (s, v) ∈ E} als die Menge der Knoten, die von s aus direkt über eine Kante erreichbar sind, und V2 = {v : (v, s) ∈ E} als die Menge von Knoten, von denen aus es eine ausgehende Kante nach s gibt. Es gilt V1 ∪ V2 ⊆ V und, da wir antiparallele Kanten nicht erlauben, auch V1 ∩ V2 = ∅. Wir berechnen nun
|f ↑ f | =
(f ↑ f ) (s, v) −
v∈V
=
v∈V1
(f ↑ f ) (v, s)
v∈V
(f ↑ f ) (s, v) −
v∈V2
(f ↑ f ) (v, s) ,
(26.5)
732
26 Maximaler Fluss
wobei die zweite Zeile folgt, da (f ↑ f )(w, x) gleich 0 ist, wenn (w, x) ∈ E. Wir wenden nun die Definition von f ↑ f auf die Gleichung (26.5) an, und erhalten durch Umordnen und Umgruppieren |f ↑ f | = (f (s, v) + f (s, v) − f (v, s)) − (f (v, s) + f (v, s) − f (s, v)) v∈V1
=
f (s, v) +
v∈V1
v∈V1
v∈V2
− =
f (s, v) −
v∈V1
v∈V1
=
f (s, v) −
v∈V1
f (s, v) −
v∈V2
f (v, s)
v∈V1
f (v, s) −
v∈V2
+
f (v, s) +
v∈V2
f (s, v)
v∈V2
f (v, s)
f (s, v) +
v∈V2
f (v, s) +
f (s, v) −
f (v, s) −
v∈V1
f (s, v) −
v∈V1 ∪V2
v∈V2
f (v, s)
v∈V2
f (v, s) .
(26.6)
v∈V1 ∪V2
In Gleichung (26.6) können wir alle vier Summenformeln so erweitern, dass wir über V aufsummieren, da jeder der zusätzlichen Terme den Wert 0 hat. (Übung 26.2-1 verlangt von Ihnen, dies formal zu beweisen.) Somit haben wir |f ↑ f | = f (s, v) − f (v, s) + f (s, v) − f (v, s) (26.7) v∈V
v∈V
v∈V
v∈V
= |f | + |f | .
Erweiterungspfade Zu einem gegebenen Flussnetzwerk G = (V, E) und einem Fluss f ist ein Erweiterungspfad p ein einfacher Pfad von s nach t im Restnetzwerk Gf . Nach der Definition des Restnetzwerks können wir den Fluss einer Kante (u, v) auf einem Erweiterungspfad um bis zu cf (u, v) erhöhen, ohne die Kapazitätsbeschränkungen auf einer der Kanten (u, v) oder (v, u) in dem ursprünglichen Flussnetzwerk G zu verletzen. Der schattierte Pfad in Abbildung 26.4(b) ist ein Erweiterungspfad. Wenn wir das Restnetzwerk Gf in der Abbildung als Flussnetzwerk auffassen, können wir den Fluss durch jede Kante dieses Pfades um bis zu 4 Einheiten erhöhen, ohne die Kapazitätsbeschränkungen zu verletzen, da die kleinste Restkapazität innerhalb dieses Pfades cf (v2 , v3 ) = 4 ist. Wir bezeichnen den maximalen Betrag, um den wir den Fluss durch jede Kante des Erweiterungspfades erhöhen können, als die Restkapazität von p, die formal durch cf (p) = min {cf (u, v) : (u, v) ist eine Kante von p}
26.2 Die Ford-Fulkerson-Methode
733
gegeben ist. Das folgende Lemma, das Sie in Übung 26.2-7 beweisen sollen, formuliert die obige Aussage exakt. Lemma 26.2 Sei G = (V, E) ein Flussnetzwerk, f ein Fluss in G und p ein Erweiterungspfad in Gf . Wir definieren eine Funktion fp : V × V → R durch cf (p) falls (u, v) zu p gehört , fp (u, v) = (26.8) 0 sonst . Dann ist fp ein Fluss in Gf mit dem Wert |fp | = cf (p) > 0. Das folgende Korollar zeigt, dass wir einen anderen Fluss in G erhalten, dessen Wert näher am Maximum liegt, wenn wir f um fp erhöhen. Abbildung 26.4(c) zeigt das Ergebnis des Erhöhens des Flusses f aus Abbildung 26.4(a) um den Fluss fp aus Abbildung 26.4(b) und Abbildung 26.4(d) zeigt das sich dann ergebende Restnetzwerk. Korollar 26.3 Sei G = (V, E) ein Flussnetzwerk, f ein Fluss in G und p ein Erweiterungspfad in Gf . Sei fp wie in Gleichung (26.8) gegeben und nehmen Sie an, wir würden f um fp erhöhen. Dann ist f ↑ fp ein Fluss in G mit dem Wert |f ↑ fp | = |f | + |fp | > |f |. Beweis: Der Beweis folgt unmittelbar aus den Lemmata 26.1 und 26.2.
Schnitte von Flussnetzwerken Die Ford-Fulkerson-Methode erhöht schrittweise den Fluss entlang von Erweiterungspfaden, bis sie einen maximalen Fluss gefunden hat. Wie können wir wissen, dass, wenn der Algorithmus terminiert, wir tatsächlich einen maximalen Fluss gefunden haben? Das maxflow-mincut-Theorem, das wir gleich beweisen werden, besagt, dass ein Fluss genau dann maximal ist, wenn sein Restnetzwerk keinen Erweiterungspfad enthält. Um dieses Theorem zu beweisen, müssen wir zunächst den Begriff des Schnitts eines Flussnetzwerks näher beleuchten. Ein Schnitt (S, T ) eines Flussnetzwerks G = (V, E) ist eine Partitionierung der Knotenmenge V in eine Menge S und eine Menge T = V − S mit s ∈ S und t ∈ T . (Diese Definition ähnelt der Definition des „Schnitts“, den wir in Kapitel 23 für minimale aufspannende Bäume verwendet haben. Allerdings partitionieren wir hier einen gerichteten statt einen ungerichteten Graphen und fordern, dass die Quelle zu S und die Senke zu T gehört.) Wenn f ein Fluss ist, dann ist der Nettofluss f (S, T ) über den Schnitt (S, T ) definiert durch f (S, T ) = f (u, v) − f (v, u) . (26.9) u∈S v∈T
u∈S v∈T
734
26 Maximaler Fluss
/16
v1
12/12
v3
8/1
3
v2
11/14
15/
20
7/7
s
4/ 9
1/4
11
v4
t 4/4
S T Abbildung 26.5: Ein Schnitt (S, T ) des Flussnetzwerkes aus Abbildung 26.1(b) mit S = {s, v1 , v2 } und T = {v3 , v4 , t}. Die Knoten von S sind schwarz und die Knoten von T weiß eingezeichnet. Der Nettofluss über (S, T ) ist f (S, T ) = 19 und die Kapazität beträgt c(S, T ) = 26.
Die Kapazität des Schnitts (S, T ) ist c(S, T ) = c(u, v) .
(26.10)
u∈S v∈T
Ein minimaler Schnitt eines Flussnetzwerks ist ein Schnitt, dessen Kapazität die kleinste von allen Schnitten des Netzwerks ist. Die Asymmetrie zwischen den Definitionen des Flusses und der Kapazität eines Flusses ist beabsichtigt und wichtig. Bei der Kapazität zählen wir nur die Kapazitäten der Kanten, die von S nach T gehen, wobei wir Kanten in entgegengesetzter Richtung ignorieren. Beim Fluss betrachten wir den Fluss von S nach T minus den Fluss in der entgegengesetzten Richtung von T nach S. Der Grund hierfür wird weiter hinten im Abschnitt ersichtlich. Abbildung 26.5 zeigt den Schnitt ({s, v1 , v2 } , {v3 , v4 , t}) des Flussnetzwerkes aus Abbildung 26.1(b). Der Nettofluss durch diesen Schnitt ist f (v1 , v3 ) + f (v2 , v4 ) − f (v3 , v2 ) = 12 + 11 − 4 = 19 , und die Kapazität dieses Schnitts beträgt c(v1 , v3 ) + c(v2 , v4 ) = 12 + 14 = 26 . Das folgende Lemma zeigt, dass bei einem gegebenen Fluss f der Nettofluss für jeden Schnitt gleich ist, und zwar gleich dem Wert |f | des Flusses. Lemma 26.4 Sei f ein Fluss in einem Flussnetzwerk G mit der Quelle s und der Senke t und (S, T ) ein beliebiger Schnitt von G. Dann ist der Nettofluss über (S, T ) gleich f (S, T ) = |f |.
26.2 Die Ford-Fulkerson-Methode
735
Beweis: Wir können die Flusserhaltungsbedingung für einen Knoten u ∈ V − {s, t} wie folgt umschreiben: f (u, v) − f (v, u) = 0 . (26.11) v∈V
v∈V
Addieren wir die linke Seite der Gleichung (26.11) für jeden Knoten aus S − {s} auf die Definition von |f | aus Gleichung (26.1), so erhalten wir 3 2 f (s, v) − f (v, s) + f (u, v) − f (v, u) , |f | = v∈V
v∈V
v∈V
u∈S−{s}
v∈V
da die linke Seite der Gleichung (26.11) gleich 0 ist. Indem wir Teilausdrücke umformen und Terme umgruppieren, erhalten wir |f | = f (s, v) − f (v, s) + f (u, v) − f (v, u) v∈V
=
v∈V
⎛
=
⎝f (s, v) +
v∈V
u∈S−{s} v∈V
⎞
f (u, v)⎠ −
f (u, v) −
v∈V u∈S
⎝f (v, s) +
v∈V
u∈S−{s}
u∈S−{s} v∈V
⎛
⎞
f (v, u)⎠
u∈S−{s}
f (v, u) .
v∈V u∈S
Wegen V = S ∪ T und S ∩ T = ∅ können wir jede Summe in je zwei Summen aufspalten, eine Summe über S und eine über T , sodass wir |f | = f (u, v) + f (u, v) − f (v, u) − f (v, u) v∈S u∈S
=
v∈T u∈S
f (u, v) −
v∈T u∈S
2 +
v∈S u∈S
f (v, u)
v∈T u∈S
f (u, v) −
v∈S u∈S
v∈T u∈S
3 f (v, u)
v∈S u∈S
erhalten. Tatsächlich sind die zwei Summen innerhalb der Klammern gleich, da für alle Knoten x, y ∈ S der Term f (x, y) in jeder der Summen genau einmal erscheint. Diese Summen heben sich demnach gegenseitig auf und wir erhalten |f | = f (u, v) − f (v, u) u∈S v∈T
u∈S v∈T
= f (S, T ) . Ein Korollar zu Lemma 26.4 zeigt, wie wir die Kapazität eines Schnitts nutzen können, um eine obere Schranke für den Wert eines Flusses zu bekommen.
736
26 Maximaler Fluss
Korollar 26.5 Der Wert eines beliebigen Flusses in einem Flussnetzwerk G ist durch die Kapazität eines beliebigen Schnitts von G nach oben beschränkt. Beweis: Sei (S, T ) ein Schnitt von G und f ein beliebiger Fluss in G. Wegen Lemma 26.4 und den Kapazitätsbeschränkungen folgt |f | = f (S, T ) = f (u, v) − f (v, u) u∈S v∈T
≤
u∈S v∈T
f (u, v)
u∈S v∈T
≤
c(u, v)
u∈S v∈T
= c(S, T ) . Korollar 26.5 impliziert direkt die Folgerung, dass der maximale Fluss in einem Netzwerk durch die Kapazität eines minimalen Schnitts des Netzwerks beschränkt ist. Das wichtige maxflow-mincut-Theorem, das wir nun formulieren und beweisen, besagt, dass der Wert eines maximalen Flusses tatsächlich gleich der Kapazität eines minimalen Schnitts ist. Theorem 26.6: (maxflow-mincut-Theorem) Wenn f ein Fluss in einem Flussnetzwerk G = (V, E) mit der Quelle s und der Senke t ist, dann sind die folgenden Bedingungen äquivalent: 1. f ist ein maximaler Fluss in G. 2. Das Restnetzwerk Gf enthält keine Erweiterungspfade. 3. Es gilt |f | = c(S, T ) für einen Schnitt (S, T ) von G.
Beweis: (1) ⇒ (2): Wir setzen voraus, dass f ein maximaler Fluss in G ist, und wollen die Annahme, dass Gf einen Erweiterungspfad p hätte, zum Widerspruch führen. Unter dieser Annahme ist wegen Korollar 26.3 der Fluss, den wir durch die Erhöhung von f um fp (fp ist durch Gleichung 26.8 gegeben) gefunden haben, ein Fluss in G mit einem Wert, der echt größer ist als |f |, was der Voraussetzung, dass f ein maximaler Fluss ist, widerspricht.
26.2 Die Ford-Fulkerson-Methode
737
(2) ⇒ (3): Setzen Sie voraus, dass Gf keinen Erweiterungspfad enthält, d. h. keinen Pfad von s nach t. Definieren Sie S = {v ∈ V : es existiert ein Pfad von s nach v in Gf } und T = V − S. Die Partitionierung (S, T ) ist ein Schnitt: Es gilt s ∈ S und t ∈ S, da es in Gf keinen Pfad von s nach t gibt. Betrachten Sie nun zwei Knoten u ∈ S und v ∈ T . Ist (u, v) ∈ E, so muss f (u, v) = c(u, v) gelten, denn ansonsten wäre (u, v) ∈ Ef , sodass v in der Menge S wäre. Ist (v, u) ∈ E, so muss f (v, u) = 0 gelten, denn ansonsten wäre cf (u, v) = f (v, u) positiv und somit (u, v) ∈ Ef , sodass v in S wäre. Wenn weder (u, v) noch (v, u) in E ist, dann gilt also f (u, v) = f (v, u) = 0. Wir haben demnach
f (S, T ) =
f (u, v) −
u∈S v∈T
=
f (v, u)
v∈T u∈S
c(u, v) −
u∈S v∈T
0
v∈T u∈S
= c(S, T ) .
Mit Lemma 26.4 folgt somit |f | = f (S, T ) = c(S, T ). (3) ⇒ (1): Nach Korollar 26.5 gilt |f | ≤ c(S, T ) für alle Schnitte (S, T ). Die Bedingung |f | = c(S, T ) impliziert daher, dass f ein maximaler Fluss ist.
Der Basis-Algorithmus von Ford und Fulkerson In jeder Iteration der Ford-Fulkerson-Methode finden wir einen Erweiterungspfad p, den wir verwenden, um den Fluss f zu verändern. Wie Lemma 26.2 und Korollar 26.3 andeuten, ersetzen wir f durch f ↑ fp und erhalten so einen neuen Fluss, dessen Wert |f |+|fp | ist. Die folgende Implementierung der Methode berechnet den maximalen Fluss in einem Flussnetzwerk G = (V, E), indem das Fluss-Attribut (u, v).f für jede Kante aktualisiert wird.1 Ist (u, v) ∈ E, setzen wir implizit (u, v).f = 0 voraus. Zudem setzen wir voraus, dass die Kapazitäten c(u, v) zusammen mit dem Flussnetzwerk gegeben sind und c(u, v) = 0 für (u, v) ∈ E gilt. Wir berechnen die Restkapazität cf (u, v) entsprechend der Formel (26.2). Der Ausdruck cf (p) im Pseudocode ist eine temporäre Variable, die die Restkapazität des Pfades p speichert.
1 Rufen Sie sich aus Abschnitt 22.1 in Erinnerung, dass wir ein Attribut f für eine Kante (u, v) in dem gleichen Notationsstil – (u, v). f – darstellen, wie die Attribute eines beliebigen anderen Objektes.
738
26 Maximaler Fluss
Ford-Fulkerson(G, s, t) 1 for jede Kante (u, v) ∈ G.E 2 (u, v).f = 0 3 while es existiert ein Pfad p von s nach t im Restnetzwerk Gf 4 cf (p) = min {cf (u, v) : (u, v) gehört zu p} 5 for jede Kante (u, v) von p 6 if (u, v) ∈ G.E 7 (u, v).f = (u, v).f + cf (p) 8 else (v, u).f = (v, u).f − cf (p) Die Prozedur Ford-Fulkerson erweitert nur den weiter vorn angegebenen Pseudocode Ford-Fulkerson-Method. Abbildung 26.6 zeigt das Ergebnis jeder Iteration für einen Beispiellauf. In den Zeilen 1–2 wird der Fluss mit 0 initialisiert. Die whileSchleife der Zeilen 3–8 bestimmt sukzessive einen Erweiterungspfad p in Gf und erhöht den Fluss f entlang des Pfades p um die Restkapazität cf (p). Jede Restkante vom Pfad p ist entweder eine Kante aus dem ursprünglichen Netzwerk oder eine Kante, die einer Kante des ursprünglichen Netzwerkes entgegengerichtet ist. Die Zeilen 6–8 aktualisieren den Fluss in beiden Fällen korrekt, in dem sie den Fluss erhöhen, wenn die Restkante eine Kante des ursprünglichen Netzwerkes ist, und anderenfalls den Fluss verringern. Wenn kein Erweiterungspfad existiert, dann ist der Fluss ein maximaler Fluss.
Analyse der Prozedur Ford-Fulkerson Die Laufzeit der Prozedur Ford-Fulkerson hängt davon ab, wie wir den Erweiterungspfad p in Zeile 3 bestimmen. Wenn wir ihn schlecht wählen, kann es sein, dass der Algorithmus nicht einmal terminiert: der Wert des Flusses wird sich mit den Erweiterungen schrittweise erhöhen, braucht aber mal nicht gegen den Wert des maximalen Flusses zu konvergieren.2 Wenn wir den Erweiterungspfad mithilfe der Breitensuche bestimmen (die wir in Abschnitt 22.2 kennengelernt haben) benötigt der Algorithmus polynomielle Zeit. Bevor wir dies beweisen, leiten wir eine einfache Schranke für den Fall her, in dem wir den Erweiterungspfad beliebig wählen und die Kapazitäten ganze Zahlen sind. In der Praxis tritt das maximale-Fluss-Problem oft mit ganzzahligen Kapazitäten auf. Wenn die Kapazitäten rationale Zahlen sind, können wir diese mithilfe einer geeigneten Skalentransformation durch ganzzahlige Werte ersetzen. Bezeichnet f ∗ einen maximalen Fluss in dem tranformierten Netzwerk, dann führt eine einfache Implementierung von Ford-Fulkerson die while-Schleife der Zeilen 3–8 höchstens |f ∗ |-mal aus, da sich der Fluss in jeder Iteration um wenigstens eine Einheit erhöht. Wir können die innerhalb der while-Schleife zu machende Arbeit effizient ausführen, wenn wir das Flussnetzwerk G = (V, E) mit der richtigen Datenstruktur implementieren und einen Erweiterungspfad in linearer Zeit finden. Lassen Sie uns annehmen, wir würden als Datenstruktur eine Datenstruktur für einen gerichteten Graphen G = (V, E ) mit E = {(u, v) : (u, v) ∈ E oder (v, u) ∈ E} verwenden. Kanten des Netzwerks G sind 2 Der Fall, dass die Ford-Fulkerson-Methode nicht terminiert, kann nur dann eintreten, wenn die Kantenkapazitäten irrationale Zahlen sind.
26.2 Die Ford-Fulkerson-Methode
s
9
20
7
s
5
v2
10
4/1
4
v4
3
4
16
v2
/16
v1
s
4
7
t
10 4
v4
4
4/1
3
4/2
t
4/14 8/12
v4
v3
4/4
8/2
0
5
v2
v3
8
4
4
s
v3
8 4
(c)
4 9
4
v1
8/12
4/4
0
4 12
t
v2
4/14
t
7
13
v1
4/14
v4
4
t
20
7
4
4
/16
v2
9
v3
4 4
s
8
v1
12
(b)
14
13
4
v4
v3
4/
v2
4/4
13
4/12
4/
t
v1
7
6 4/1
9
20
9
v3
4
s
4
(a)
12
7
v1
16
739
v4
4/4
Abbildung 26.6: Die Arbeitsweise des Basis-Algorithmus von Ford und Fulkerson. (a)(e) Aufeinanderfolgende Iterationen der while-Schleife. Die linke Seite zeigt jeweils das Restnetzwerk Gf aus Zeile 3, wobei der Erweiterungspfad p schattiert dargestellt ist. Die rechte Seite zeigt jeweils den neuen Fluss, der sich durch das Erhöhen von f um fp ergibt. Das Restnetzwerk in (a) ist das ursprüngliche Netzwerk G.
26 Maximaler Fluss
v2
10
s 11/
13
4
v4
v1
v2
4
11
v2
3
5
/16
t
13
4
11
v2
3
v3 7
12 2
9
s
12
4
(f)
v1
v4
s 11/
11
4
12/12
v3
v4 v3
12
15
v4
v1
9
7
8 2
9
s
v3
8 4
(e)
4
11/14
4
v1
8
8/12
v2
11/14
15/
20
t 4/4
19/
20
7/7
4
6 8/1
t
7
s
12 8
9
8 9
4
(d)
v3
8
9
8
4
4
v1
7/7
740
v4
t 4/4
1 19
t 4
11
Abbildung 26.6, fortgesetzt: (f ) Das Restnetzwerk beim letzten Test der Bedingung der while-Schleife. Es enthält keine Erweiterungspfade, sodass der in Teil (e) gezeigte Fluss der maximale Fluss ist. Der Wert des gefundenen maximalen Flusses ist 23.
auch Kanten von G und somit können wir leicht Kapazitäten und Flüsse in dieser Datenstruktur speichern. Für einen gegebenen Fluss f in G besteht die Kantenmenge des Restnetzwerks Gf aus allen Kanten (u, v) von G , für die cf (u, v) = 0 gilt, wobei cf der Gleichung (26.2) genügt. Die Zeit für die Bestimmung eines Pfades in einem Restnetzwerk ist daher O(V + E ) = O(E), wenn wir entweder Tiefensuche oder Breitensuche verwenden. Jede Iteration der while-Schleife benötigt also, wie die Zeilen 1–2, Zeit O(E), was zu einer Gesamtlaufzeit von O(E |f ∗ |) für die Prozedur Ford-Fulkerson führt. Wenn die Kapazitäten ganzzahlig sind und der optimale Wert des Flusses |f ∗ | klein ist, dann ist die Laufzeit des Ford-Fulkerson-Agorithmus gut. Abbildung 26.7(a) zeigt ein Beispiel dafür, was in einem einfachen Netzwerk passieren kann, für das |f ∗ | groß ist. Ein maximaler Fluss in diesem Netzwerk hat den Wert 2 000 000: 1 000 000 Einheiten des Flusses fließen entlang des Pfades s → u → t und weitere 1 000 000 Einheiten entlang des Pfades s → v → t. Wenn der erste Erweiterungspfad, der von der Prozedur FordFulkerson ausgewählt wird, der in Abbildung 26.7(a) gezeigte Pfad s → u → v → t ist, dann hat der Fluss nach der ersten Iteration den Wert 1. Das resultierende Restnetzwerk ist in Abbildung 26.7(b) dargestellt. Wenn die zweite Iteration, wie in Abbildung 26.7(b) gezeigt, den Erweiterungspfad s → v → u → t auswählt, erhält der Fluss dann den Wert 2. Abbildung 26.7(c) zeigt das resultierende Restnetzwerk. Wir können fortfahren, indem wir in Iterationen mit ungeradzahliger Ordnung den Erweiterungspfad
26.2 Die Ford-Fulkerson-Methode
100
000
0
v
t
(a)
0
000
100
100
u
1
s 100
0
v (b)
999
0
999
999
000
999
000
999
0
1
s
999
000
1
t
s
u
999
1
999
999
1
100
u
1
00
00 100
741
999
1
1 999
t
999
v
1
(c)
Abbildung 26.7: (a) Ein Flussnetzwerk, für das die Prozedur Ford-Fulkerson Zeit Θ(E |f ∗ |) benötigen kann, wobei f ∗ ein maximaler Fluss ist. Im Beispiel ist |f ∗ | = 2 000 000. Der schattierter Pfad ist ein Erweiterungspfad mit Restkapazität 1. (b) Das resultierende Restnetzwerk mit einem anderen Erweiterungspfad, dessen Restkapazität 1 ist. (c) Das resultierende Restnetzwerk.
s → u → v → t und in Iterationen mit geradzahliger Ordnung den Erweiterungspfad s → v → u → t auswählen. Wir würden dann insgesamt 2 000 000 Iterationen ausführen, wobei wir den Wert des Flusses jeweils nur um eine Einheit erhöhen.
Der Edmonds-Karp-Algorithmus Wir können die Schranke der Prozedur Ford-Fulkerson verbessern, indem wir die Berechnung des Erweiterungspfades p in Zeile 3 über eine Breitensuche implementieren, d. h. wir wählen den Erweiterungspfad als einen kürzesten Pfad von s nach t, wobei jede Kante die Distanz (das Gewicht) 1 hat. Die auf diese Weise implementierte FordFulkerson-Methode wird als Edmonds-Karp-Algorithmus bezeichnet. Wir beweisen nun, dass der Edmonds-Karp-Algorithmus eine Laufzeit von O(V E 2 ) hat. Die Analyse hängt von den Distanzen der Knoten des Restnetzwerks Gf ab. Das folgende Lemma verwendet die Bezeichnung δf (u, v) für die Distanz des kürzesten Pfades von u nach v in Gf , wobei jede Kante die Distanz 1 hat. Lemma 26.7 Wenn der Edmonds-Karp-Algorithmus auf ein Flussnetzwerk G = (V, E) mit der Quelle s und der Senke t angewendet wird, dann wächst für jeden Knoten v ∈ V − {s, t} die Distanz δf (s, v) des kürzesten Pfades von s nach v im Restnetzwerk Gf monoton mit jeder Flusserhöhung Beweis: Wir wollen annehmen, dass es für einen Knoten v ∈ V − {s, t} eine Flusserhöhung gäbe, die dazu führt, dass die Distanz des kürzesten Pfades von s nach v kleiner wird, und diese Annahme zum Widerspruch führen. Sei f der Fluss unmittelbar vor der ersten Flusserhöhung, die zu einer Verringerung der Distanz eines kürzesten Pfades führt, und f der Fluss unmittelbar danach. Weiter sei v der Knoten mit dem kleinsten δf (s, v), dessen Distanz durch die Flusserhöhung verringert wurde. Es gilt also δf (s, v) < δf (s, v). Sei p = s ; u → v ein kürzester Pfad von s nach v in Gf ,
742
26 Maximaler Fluss
sodass (u, v) ∈ Ef und δf (s, u) = δf (s, v) − 1
(26.12)
gilt. Wegen der Wahl von v, wissen wir, dass sich die Distanz zwischen s und u nicht verringert hat; es gilt also δf (s, u) ≥ δf (s, u) .
(26.13)
Wir behaupten, dass (u, v) ∈ Ef gilt. Weshalb? Im Falle (u, v) ∈ Ef müsste auch δf (s, v) ≤ δf (s, u) + 1 (nach Lemma 24.10, Dreiecksungleichung) ≤ δf (s, u) + 1 (wegen Ungleichung (26.13)) = δf (s, v) (wegen Gleichung (26.12)) gelten, was unserer Annahme δf (s, v) < δf (s, v) widerspricht. Wie ist es möglich, dass (u, v) ∈ Ef und (u, v) ∈ Ef gilt? Die Flusserhöhung muss den Fluss von v nach u erhöht haben. Der Edmonds-Karp-Algorithmus erweitert stets den Fluss entlang kürzester Pfade und somit erweitert er den Fluss entlang eines kürzesten Pfades von s nach u in Gf , dessen letzte Kante (v, u) ist. Hieraus folgt δf (s, v) = δf (s, u) − 1 ≤ δf (s, u) − 1 (wegen Ungleichung (26.13)) = δf (s, v) − 2 (wegen Gleichung (26.12)) , was unserer Annahme δf (s, v) < δf (s, v) widerspricht. Wir schlussfolgern, dass unsere Annahme, dass ein solcher Knoten v existieren würde, falsch sein muss. Das nächste Theorem liefert eine Schranke für die Anzahl der Iterationen des EdmondsKarp-Algorithmus. Theorem 26.8 Wird der Edmonds-Karp-Algorithmus auf ein Flussnetzwerk G = (V, E) mit der Quelle s und der Senke t angewendet, dann ist die Gesamtanzahl der durch den Algorithmus vorgenommenen Flusserhöhungen in O(V E). Beweis: Wir nennen eine Kante (u, v) in einem Restnetzwerk Gf kritische Kante eines Erweiterungspfades p, wenn die Restkapazität von p gleich der Restkapazität von (u, v) ist, d. h. wenn cf (p) = cf (u, v) gilt. Nachdem wir den Fluss entlang eines Erweiterungspfades erhöht haben, verschwinden alle kritischen Kanten des Pfades aus dem Restnetzwerk. Desweiteren muss auf jedem Erweiterungspfad mindestens eine Kante kritisch sein. Wir werden zeigen, dass jede der |E| Kanten während des Algorithmus höchstens |V | /2-mal kritisch sein kann.
26.2 Die Ford-Fulkerson-Methode
743
Seien u und v Knoten aus V , die durch eine Kante aus E verbunden sind. Da Erweiterungspfade kürzeste Pfade sind, gilt, wenn (u, v) zum ersten Mal kritisch ist, δf (s, v) = δf (s, u) + 1 . Nachdem der Fluss erhöht wurde, verschwindet die Kante (u, v) aus dem Restnetzwerk. Sie kann dann so lange nicht auf einem anderen Erweiterungspfad vorkommen, bis der Fluss von u nach v verringert wurde. Dies geschieht nur dann, wenn (v, u) in einem Erweiterungspfad vorkommt. Wenn f der Fluss in G ist, wenn dieses Ereignis eintritt, dann gilt δf (s, u) = δf (s, v) + 1 . Da nach Lemma 26.7 die Ungleichung δf (s, v) ≤ δf (s, v) gilt, folgt δf (s, u) = δf (s, v) + 1 ≥ δf (s, v) + 1 = δf (s, u) + 2 . Folglich wächst die Distanz von der Quelle zu u zwischen dem Zeitpunkt, zu dem (u, v) kritisch wird, und dem Zeitpunkt, zu dem sie das nächste Mal kritisch wird, um mindestens 2. Die Distanz von der Quelle zu u ist anfangs mindestens 0. Die Knoten s, u und t können nicht zu den Zwischenknoten auf einem kürzesten Pfad von s nach u gehören, denn wenn (u, v) auf dem Erweiterungspfad liegt, muss u = t gelten. Daher ist die Distanz von der Quelle zu u höchstens |V | − 2, zumindest so lange u von der Quelle aus erreichbar ist. Nachdem die Kante (u, v) das erste Mal kritisch geworden ist, kann sie also höchstens (|V | − 2)/2 = |V | /2 − 1-mal nochmals kritisch werden, also insgesamt höchstens |V | /2-mal. Da es O(E) Knotenpaare gibt, zwischen denen es eine Kante im Restgraphen geben kann, ist die Gesamtanzahl kritischer Kanten während des gesamten Laufes des Edmonds-Karp-Algorithmus gleich O(V E). Jeder Erweiterungspfad hat mindestens eine kritische Kante, womit das Theorem bewiesen ist. Da wir jede Iteration der Prozedur Ford-Fulkerson in Zeit O(E) ausführen können, wenn wir den Erweiterungspfad über Breitensuche bestimmen, ist die Gesamtlaufzeit des Edmonds-Karp-Algorithmus O(V E 2 ). Wir werden sehen, dass Push/RelabelAlgorithmen noch bessere Schranken liefern können. Der Algorithmus aus Abschnitt 26.4 stellt eine Methode mit Laufzeit O(V 2 E) vor, die die Grundlage für die Algorithmen mit Laufzeit O(V 3 ) darstellt, die in Abschnitt 26.5 behandelt werden.
Übungen 26.2-1 Beweisen Sie, dass die Summen aus Gleichung (26.6) gleich den Summen aus Gleichung (26.7) sind. 26.2-2 Wie groß ist der Fluss über den Schnitt ({s, v2 , v4 } , {v1 , v3 , t}) in dem in Abbildung 26.1(b) gezeigten Flussnetzwerk? Wie groß ist die Kapazität dieses Schnitts?
744
26 Maximaler Fluss
26.2-3 Illustrieren Sie die Arbeitsweise des Edmonds-Karp-Algorithmus auf dem Flussnetzwerk aus Abbildung 26.1(a). 26.2-4 Wie sieht in dem Beispiel von Abbildung 26.6 der minimale Schnitt aus, der zu dem gezeigten maximalen Fluss korrespondiert? Welche der in diesem Beispiel vorkommenden Erweiterungspfade löschen den Fluss? 26.2-5 Rufen Sie sich in Erinnerung, dass die in Abschnitt 26.1 angegebene Methode, ein Flussnetzwerk mit mehreren Quellen und Senken in ein Flussnetzwerk mit nur einer Quelle und einer Senke überzuführen, Kanten mit unendlicher Kapazität hinzufügt. Beweisen Sie, dass jeder Fluss in dem resultierenden Flussnetzwerk einen endlichen Wert hat, wenn die Kanten des ursprünglichen Netzwerks mit den multiplen Quellen und Senken endliche Kapazitäten haben. 26.2-6 Setzen Sie voraus, dass jede Quelle si eines Flussproblems mit mehreren Quellen und Senken genau pi Einheiten des Flusses erzeugt, sodass v∈V f (si , v) = Sie zudem voraus, dass jede Senke pi gilt. Setzen tj genau qj Einheiten absorbiert, sodass v∈V f (v, tj ) = qj mit i pi = j qj gilt. Zeigen Sie, wie wir das Problem, einen Fluss f zu bestimmen, der diese zusätzlichen Bedingungen erfüllt, in das Problem, einen maximalen Fluss in einem Netzwerk mit nur einer Quelle und einer Senke zu bestimmen, überführen können. 26.2-7 Beweisen Sie Lemma 26.2. 26.2-8 Setzen Sie voraus, dass wir das Restnetzwerk so umdefinieren, dass in s eingehende Kanten nicht erlaubt sind. Überlegen Sie, warum die Prozedur FordFulkerson weiterhin einen maximalen Fluss berechnet. 26.2-9 Setzen Sie voraus, dass sowohl f als auch f Flüsse in einem Netzwerk G sind und wir den Fluss f ↑ f berechnen. Genügt dieser Fluss der Flusserhaltungseigenschaft? Genügt er den Kapazitätsbeschränkungen? 26.2-10 Zeigen Sie, dass wir einen maximalen Fluss in einem Netzwerk G = (V, E) immer durch eine Folge von höchstens |E| Erweiterungspfaden finden können. (Hinweis: Bestimmen Sie die Pfade, nachdem ein maximaler Fluss gefunden wurde.) 26.2-11 Die Kantenzusammenhangszahl eines ungerichteten Graphen ist die kleinste Anzahl k von Kanten, die entfernt werden müssen, damit der Graph nicht mehr zusammenhängend ist. In einem Baum beispielsweise ist die Kantenzusammenhangszahl gleich 1, während sie bei einem Kreis von Knoten gleich 2 ist. Zeigen Sie, wie wir die Kantenzusammenhangszahl eines ungerichteten Graphen G = (V, E) bestimmen können, indem wir einen Algorithmus zur Bestimmung des maximalen Flusses auf höchstens |V | Flussnetzwerken mit jeweils O(V ) Knoten und O(E) Kanten ausführen. 26.2-12 Setzen Sie voraus, dass wir ein Flussnetzwerk G gegeben haben, in dem alle Kanten ganzzahlige Kapazitäten haben, und G Kanten enthält, die s als Endknoten haben. Sei f ein Fluss in G, bei dem der Fluss f (v, s) von einer der in s eingehenden Kanten (v, s) gleich 1 ist. Beweisen Sie, dass es einen anderen
26.3 Maximales bipartites Matching
745
Fluss f geben muss, für den f (v, s) = 0 und |f | = |f | gilt. Geben Sie einen Algorithmus mit Laufzeit O(E) an, der den Fluss f bei gegebenem Fluss f bestimmt. 26.2-13 Setzen Sie voraus, dass wir in der Menge aller minimalen Schnitte eines Flussnetzwerkes G, dessen Kanten ganzzahlige Kapazitäten haben, einen Schnitt finden wollen, der die kleinste Anzahl von Kanten enthält. Zeigen Sie, wie wir die Kapazitäten von G modifizieren müssen, um ein neues Flussnetzwerk G zu erhalten, sodass jeder minimale Schnitt von G ein minimaler Schnitt von G mit minimal vielen Kanten ist.
26.3
Maximales bipartites Matching
Einige kombinatorische Probleme können leicht als maximale-Fluss-Probleme modelliert werden. Das maximale-Fluss-Problem mit mehreren Quellen und Senken aus Abschnitt 26.1 ist ein solches Beispiel. Andere kombinatorische Probleme haben oberflächlich betrachtet scheinbar nichts mit Flussnetzwerken zu tun, können dennoch auf maximale-Fluss-Probleme zurückgeführt werden. Dieser Abschnitt stellt ein solches Problem vor: die Bestimmung eines maximalen Matchings in einem bipartiten Graphen (siehe Abschnitt B.4). Um dieses Problem zu lösen, nutzen wir die Ganzzahligkeitseigenschaft aus, der die Ford-Fulkerson-Methode genügt. Wir werden außerdem sehen, wie wir die Ford-Fulkerson-Methode verwenden können, um das Problem des maximalen bipartiten Matchings auf einem Graphen G = (V, E) in Zeit O(V E) zu lösen.
Das Problem des maximalen bipartiten Matchings In einem ungerichteten Graphen G = (V, E) ist ein Matching eine Teilmenge M der Kantenmenge E mit der Eigenschaft, dass es für jeden Knoten v ∈ V höchstens eine Kante in M gibt, die mit v inzidiert. Wir sagen, ein Knoten v ∈ V ist durch das Matching M gebunden, wenn eine Kante von M mit v inzidiert; anderenfalls heißt v ungebunden. Ein maximales Matching ist ein Matching mit maximaler Kardinalität, d. h. ein Matching M , für das |M | ≥ |M | für jedes Matching M gilt. In diesem Abschnitt beschränken wir uns darauf, das maximale Matching in bipartiten Graphen zu bestimmen, also in Graphen, deren Knotenmengen V in L ∪ R partitioniert werden können, wobei L und R disjunkt sind und alle Kanten von E zwischen L und R verlaufen. Zudem setzen wir voraus, dass jeder Knoten aus V mindestens eine inzidente Kante hat. Abbildung 26.8 illustriert den Begriff des Matchings. Das Problem, ein maximales Matching in einem bipartiten Graphen zu bestimmen, hat viele praktische Anwendungen. Zum Beispiel könnten wir das Matching einer Menge L von Maschinen mit einer Menge R von gleichzeitig auszuführenden Aufgaben betrachten. Wir interpretieren die Existenz einer Kante (u, v) in E so, dass die Maschine u ∈ L die Ausführung der Aufgabe v ∈ R übernehmen kann. Ein maximales Matching verteilt die Arbeit auf so viele Maschinen wie möglich.
746
26 Maximaler Fluss
s
L
R (a)
L
R (b)
t
L
R (c)
Abbildung 26.8: Ein bipartiter Graph G = (V, E) mit Knotenpartition V = L ∪ R. (a) Ein Matching mit Kardinalität 2; das Matching ist durch die schattierten Kanten angegeben. (b) Ein maximales Matching mit Kardinalität 3. (c) Das korrespondierende Flussnetzwerk und ein maximaler Fluss von diesem. Jede Kante hat eine Kapazität von 1. Schattierte Kanten haben einen Fluss von 1; über alle anderen Kanten fließt kein Fluss. Die schattierten Kanten von L nach R entsprechen denen des in (b) gezeigten maximalen Matchings.
Bestimmung eines maximalen bipartiten Matchings Wir können die Ford-Fulkerson-Methode verwenden, um in polynomieller Zeit in |V | und |E| ein maximales Matching in einem ungerichteten bipartiten Graphen G = (V, E) zu bestimmen. Die Idee besteht darin, ein Flussnetzwerk zu konstruieren, in dem der Fluss dem Matching entspricht (siehe Abbildung 26.8(c)). Wir definieren das zu dem bipartiten Graphen G korrespondierende Flussnetzwerk G = (V , E ) wie folgt. Die Quelle s und die Senke t sind neue Knoten, die nicht zu V gehören, und es gilt V = V ∪ {s, t}. Wenn die Knotenpartition von G durch V = L ∪ R gegeben ist, dann bestehen die gerichteten Kanten von G aus den Kanten von E, die von L nach R gerichtet sind, und |V | neue gerichtete Kanten: E = {(s, u) : u ∈ L} ∪ {(u, v) : (u, v) ∈ E} ∪ {(v, t) : v ∈ R} . Um die Konstruktion zu vervollständigen, ordnen wir jeder Kante von E die Kapazität 1 zu. Da jeder Knoten von V mindestens eine inzidente Kante hat, gilt |E| ≥ |V | /2. Es gilt also |E| ≤ |E | = |E| + |V | ≤ 3 |E| und somit |E | = Θ(E). Das folgende Lemma zeigt, dass ein Matching in G unmittelbar mit einem Fluss in dem zu G korrespondierenden Flussnetzwerk G zusammenhängt. Wir sagen, dass ein Fluss f in einem Flussnetzwerk G = (V, E) ganzzahlig ist, wenn f (u, v) für alle (u, v) ∈ V × V eine ganze Zahl ist. Lemma 26.9 Seien G = (V, E) ein bipartiter Graph mit der Knotenpartition V = L ∪ R und G = (V , E ) das dazu korrespondierende Flussnetzwerk. Wenn M ein Matching
26.3 Maximales bipartites Matching
747
in G ist, dann gibt es einen ganzzahligen Fluss f in G mit dem Wert |f | = |M |. Umgekehrt gibt es ein Matching M in G mit der Kardinalität |M | = |f |, wenn f ein ganzzahliger Fluss in G ist. Beweis: Wir zeigen zunächst, dass ein Matching M in G zu einem ganzzahligen Fluss f in G korrespondiert. Definieren Sie f wie folgt. Ist (u, v) ∈ M , dann sei f (s, u) = f (u, v) = f (v, t) = 1. Für alle anderen Kanten (u, v) ∈ E definieren wir f (u, v) = 0. Es ist leicht zu überprüfen, dass f die Kapazitätsbeschränkungen und die Flusserhaltungseigenschaft erfüllt. Offensichtlich entspricht jede Kante (u, v) ∈ M einer Einheit des Flusses in G , die den Pfad s → u → v → t durchfließt. Außerdem sind die Pfade, die durch die Kanten von M induziert werden, abgesehen von s und t, knotendisjunkt. Der Nettofluss durch den Schnitt (L ∪ {s} , R ∪ {t}) ist gleich |M |. Nach Lemma 26.4 ist der Wert des Flusses also |f | = |M |. Um die Umkehrung zu beweisen, nehmen wir an, dass f ein ganzzahliger Fluss in G und M = {(u, v) : u ∈ L, v ∈ R und f (u, v) > 0} ist. Jeder Knoten u ∈ L besitzt höchstens eine eintretende Kante, nämlich (s, u), und deren Kapazität ist 1. Also fließt in jeden Knoten u ∈ L nur eine Einheit des Flusses hinein, und wenn eine Einheit des positiven Flusses hineinfließt, dann muss wegen der Flusserhaltung auch eine Einheit herausfließen. Da f ganzzahlig ist, muss außerdem für jeden Knoten u ∈ L die eine Einheit über genau eine Kante in u hineinfließen und über genau eine Kante aus u herausfließen. Das bedeutet, dass eine Einheit des positiven Flusses genau dann in u hineinfließt, wenn es genau einen Knoten v ∈ R mit f (u, v) = 1 gibt, und dass höchstens eine aus u ∈ L austretende Kante einen positiven Fluss führt. Ein symmetrisches Argument können wir für jeden Knoten v ∈ R angeben. Die Menge M ist daher ein Matching. Um zu sehen, dass |M | = |f | gilt, haben Sie zu bemerken, dass f (s, u) = 1 für jeden gebundenen Knoten u ∈ L und f (u, v) = 0 für jede Kante (u, v) ∈ E − M gilt. Hieraus folgt, dass der Nettofluss f (L∪{s} , R∪{t}) über den Schnitt (L∪{s} , R∪{t}) gleich |M | ist. Wenden wir Lemma 26.4 an, erhalten wir |f | = f (L ∪ {s} , R ∪ {t}) = |M |. Mithilfe von Lemma 26.9 würden wir gerne schließen, dass ein maximales Matching in einem bipartiten Graphen G einem maximalen Fluss im zugehörigen Flussnetzwerk G entspricht. Dann könnten wir ein maximales Matching in G berechnen, indem wir einen Algorithmus laufen lassen, der einen maximalen Fluss in G bestimmt. Der einzige Haken an dieser Argumentation ist, dass der Algorithmus zur Berechnung eines maximalen Flusses einen Fluss in G zurückgeben könnte, für den eines der f (u, v) keine ganze Zahl ist, obwohl der Wert |f | des Flusses eine ganze Zahl sein muss. Das folgende Theorem zeigt, dass dieses Problem nicht auftreten kann, wenn wir die Ford-Fulkerson-Methode verwenden. Theorem 26.10: (Ganzzahligkeitstheorem) Falls die Kapazitätsfunktion c nur ganzzahlige Werte annimmt, dann hat der durch die Ford-Fulkerson-Methode erzeugte maximale Fluss die Eigenschaft, dass |f | eine
748
26 Maximaler Fluss
ganze Zahl ist. Außerdem ist der Wert von f (u, v) für alle Knoten u und v eine ganze Zahl. Beweis: Der Beweis erfolgt durch vollständige Induktion über die Anzahl der Iterationen. Wir überlassen den Beweis dem Leser in Übung 26.3-2. Wir können nun das folgende Korollar zu Lemma 26.9 beweisen. Korollar 26.11 Die Kardinalität eines maximalen Matchings M in einem bipartiten Graphen G ist gleich dem Wert des maximalen Flusses in seinem zugehörigen Flussnetzwerk G . Beweis: Wir verwenden die Nomenklatur aus Lemma 26.9. Nehmen Sie an, M wäre ein maximales Matching in G und der zugehörige Fluss f in G wäre nicht maximal. Dann gibt es einen maximalen Fluss f in G , für den |f | > |f | gilt. Da die Kapazitäten in G nach Theorem 26.10 ganzzahlig sind, können wir voraussetzen, dass f ganzzahlig ist. Also entspricht f einem Matching M in G mit Kardinalität |M | = |f | > |f | = |M |, was ein Widerspruch zu unserer Annahme ist, dass M ein maximales Matching wäre. Mit einem ähnlichen Argument können wir zeigen, dass, falls f ein maximaler Fluss in G ist, das zugehörige Matching ein maximales Matching in G ist. Wir können also zu einem gegebenen bipartiten ungerichteten Graphen G ein maximales Matching bestimmen, indem wir das Flussnetzwerk G erzeugen, die Ford-FulkersonMethode anwenden und ein maximales Matching M unmittelbar aus dem so bestimmten maximalen Fluss f berechnen. Da jedes Matching in einem bipartiten Graphen höchstens die Kardinalität min(L, R) = O(V ) hat, ist der Wert des maximalen Flusses in G in O(V ). Wir können daher ein maximales Matching in einem bipartiten Graphen in Zeit O(V E ) = O(V E) bestimmen, da |E | = Θ(E) gilt.
Übungen 26.3-1 Wenden Sie den Ford-Fulkerson-Algorithmus auf das Flussnetzwerk aus Abbildung 26.8(c) an und stellen Sie das Restnetzwerk nach jeder Flusserhöhung dar. Nummerieren Sie die Knoten aus L von oben nach unten mit 1 bis 5 und die Knoten aus R von oben nach unten mit 6 bis 9. Wählen Sie in jeder Iteration den lexikographisch kleinsten Erweiterungspfad. 26.3-2 Beweisen Sie Theorem 26.10. 26.3-3 Seien G = (V, E) ein bipartiter Graph mit der Knotenpartition V = L ∪ R und G das zugehörige Flussnetzwerk. Geben Sie eine gute obere Schranke für die Länge jedes Erweiterungspfades an, der in G während der Ausführung der Prozedur Ford-Fulkerson gefunden wird.
26.4 ∗ Push/Relabel-Algorithmen
749
26.3-4∗ Ein perfektes Matching ist ein Matching, bei dem jeder Knoten gebunden ist. Sei G = (V, E) ein ungerichteter bipartiter Graph mit der Knotenpartition V = L ∪ R mit |L| = |R|. Für jedes X ⊆ V definieren wir die Nachbarschaft von X durch N (X) = {y ∈ V : (x, y) ∈ E für ein x ∈ X} , d. h. die Menge der Knoten, die mit einem Element von X benachbart sind. Beweisen Sie Halls Theorem: Es existiert genau dann ein perfektes Matching in G, wenn |A| ≤ |N (A)| für jede Teilmenge A ⊆ L gilt. 26.3-5∗ Wir sagen, dass ein bipartiter Graph G = (V, E) mit V = L ∪ R d-regulär ist, wenn jeder Knoten v ∈ V genau den Grad d hat. Für jeden d-regulären bipartiten Graphen gilt |L| = |R|. Beweisen Sie, dass jeder d-reguläre bipartite Graph ein Matching der Kardinalität |L| besitzt, indem Sie zeigen, dass ein minimaler Schnitt im zugehörigen Flussnetzwerk die Kapazität |L| hat.
∗ 26.4 Push/Relabel-Algorithmen In diesem Abschnitt stellen wir den „Push/Relabel-Ansatz“ für die Berechnung maximaler Flüsse vor. Bis heute sind viele der asymptotisch schnellsten Algorithmen zur Bestimmung maximaler Flüsse Push/Relabel-Algorithmen, und die schnellsten derzeit existierenden Implementierungen von Algorithmen zur Berechnung maximaler Flüsse beruhen auf der Push/Relabel-Methode. Push/Relabel-Methoden lösen auch andere Flussprobleme effizient, wie zum Beispiel das Problem, ein Fluss mit minimalen Kosten zu bestimmen. In diesem Kapitel führen wir Goldbergs „generischen“ Algorithmus zur Berechnung maximaler Flüsse ein, der in einer einfachen Implementierung in Zeit O(V 2 E) läuft. Dies stellt eine Verbesserung gegenüber der O(V E 2 )-Schranke des Edmonds-Karp-Algorithmus dar. In Abschnitt 26.5 verfeinern wir den generischen Algorithmus und erhalten so einen anderen Push/Relabel-Algorithmus, der in Zeit O(V 3 ) läuft. Push/Relabel-Algorithmen arbeiten lokaler als dies die Ford-Fulkerson-Methode tut. Anstatt das gesamte Restnetzwerk zu sondieren, um einen Erweiterungspfad zu finden, arbeiten Push/Relabel-Algorithmen zu jedem Zeitpunkt mit nur einem Knoten und suchen nur in der Nachbarschaft des Knotens im Restnetzwerk. Außerdem verletzen Push/Relabel-Algorithmen, anders als die Ford-Fulkerson-Methode, die Flusserhaltungseigenschaft während der Ausführung. Sie arbeiten jedoch mit einem Vorfluss. Dies ist eine Funktion f : V × V → R, die die Kapazitätsbeschränkungen und die folgende Abschwächung der Flusserhaltungseigenschaft erfüllt: f (v, u) − f (u, v) ≥ 0 v∈V
v∈V
für alle Knoten u ∈ V − {s}. Der in einen Knoten eingehende Fluss darf also größer als der aus dem Knoten ausgehende Fluss sein. Wir nennen die Größe f (v, u) − f (u, v) (26.14) e(u) = v∈V
v∈V
750
26 Maximaler Fluss
der Flussüberschuss des Knotens u. Der Flussüberschuss an einem Knoten gibt den Betrag an, um den der eingehende Fluss größer als der ausgehende Fluss an diesem Knoten ist. Wir sagen, dass ein Knoten u ∈ V − {s, t} überflutet ist (oder ein Überschussknoten ist), wenn e(u) > 0 gilt. Wir beginnen dieses Kapitel damit, dass wir die Intuition angeben, die hinter der Push/Relabel-Methode steht. Dann untersuchen wir die beiden Operationen, die von der Methode verwendet werden: das „Pushen“ (oder Drücken) des Vorflusses und das „Relabeln“ (oder Ummarkieren) eines Knotens. Schließlich stellen wir einen generischen Push/Relabel-Algorithmus vor und analysieren seine Korrektheit sowie seine Laufzeit.
Intuition Sie können die Intuition, die hinter der Push/Relabel-Methode steht, am besten verstehen, wenn wir die Terminologie von Flüssigkeitsströmen benutzen: Wir betrachten ein Flussnetzwerk G = (V, E) als ein System von verbundenen Rohren mit bestimmten Kapazitäten. Wenden wir diese Analogie auf die Ford-Fulkerson-Methode an, können wir sagen, dass jeder Erweiterungspfad des Netzwerks einen zusätzlichen Flüssigkeitsstrom ohne Verzweigungspunkte zulässt, der von der Quelle zur Senke fließt. Die FordFulkerson-Methode fügt iterativ immer mehr Material zu dem Fluss hinzu, bis keine Erhöhung mehr möglich ist. Hinter dem generischen Push/Relabel-Algorithmus steckt eine etwas andere Intuition. Wie zuvor interpretieren wir die gerichteten Kanten als Rohre. Die Knoten, also die Rohrverbindungen, haben zwei interessante Eigenschaften. Erstens hat jeder Knoten ein Abflussrohr zu einem beliebig großen Reservoir, das den Flussüberschuss des Knotens aufnehmen kann. Zweitens befinden sich jeder Knoten, sein Reservoir und alle seine Rohrverbindungen auf einer Ebene, deren Höhe mit dem Fortschreiten des Algorithmus ansteigt. Die Höhen der Knoten geben an, wie der Fluss gedrückt wird. Wir drücken Fluss nur abwärts, also von einem höheren zu einem tieferen Knoten. Der Fluss von einem tieferen zu einem höheren Knoten kann positiv sein, aber Operationen, die den Fluss drücken, drücken ihn nur abwärts. Wir fixieren die Höhe der Quelle auf den festen Wert |V | und die Höhe der Senke auf den festen Wert 0. Alle anderen Knotenhöhen starten bei 0 und wachsen mit der Zeit. Der Algorithmus schickt zunächst so viel Material wie möglich von der Quelle abwärts zur Senke. Diese Materialmenge ist gerade groß genug, um jedes von der Quelle ausgehende Rohr bis zur Kapazitätsgrenze zu füllen, d. h. die ausgesendete Materialmenge ist so groß wie die Kapazität des Schnitts (s, V − s). Wenn der Fluss erstmalig einen Zwischenknoten erreicht, sammelt er sich im Reservoir des Knotens. Von dort drücken wir ihn schlussendlich abwärts. Es kann nun passieren, dass die einzigen Rohre, die von einem Knoten u ausgehen und die noch nicht gesättigt sind, zu Knoten verlaufen, die die gleiche Höhe wie u oder eine größere Höhe als u haben. In diesem Fall müssen wir die Höhe des Überschussknotens u erhöhen, um diesen von seinem Überschuss befreien zu können – eine Operation, die unter dem Namen „Relabeling“ (oder „Ummarkieren“) bekannt ist. Wir setzen seine Höhe um 1 größer als die Höhe seines tiefsten Nachbarn, zu dem er eine ungesättigte Verbindung hat. Nachdem ein Knoten ummarkiert wurde, hat er mindestens ein Ausgangsrohr,
26.4 ∗ Push/Relabel-Algorithmen
751
durch das wir mehr Material drücken können. Schlussendlich wird die gesamte Materialmenge, die durch das Netzwerk zur Senke gelangen kann, dort ankommen. Mehr kann nicht ankommen, da die Rohre den Kapazitätsbeschränkungen unterliegen; die Materialmenge, die durch jeden Schnitt fließen kann, ist noch immer durch die Kapazität des Schnitts beschränkt. Um den Vorfluss zu einem „zulässigen“ Fluss zu machen, schickt der Algorithmus den in den Reservoirs gesammelten Flussüberschuss der Überschussknoten zurück zur Quelle, indem er die Höhe dieser Knoten größer als die feste Höhe |V | der Quelle wählt. Wie wir sehen werden, ist, wenn wir alle Reservoirs geleert haben, der Vorfluss nicht nur ein „zulässiger“ Fluss, sondern sogar ein maximaler Fluss.
Die Grundoperationen Aus der vorhergehenden Diskussion sehen wir, dass ein Push/Relabel-Algorithmus zwei Grundoperationen ausführt: das Drücken eines Flussüberschusses von einem Knoten zu einem seiner Nachbarn und das Ummarkieren eines Knotens. Die Situation, in denen diese Operationen angewendet werden, hängen von den Höhen der Knoten ab, die wir nun exakt definieren wollen. Seien G = (V, E) ein Flussnetzwerk mit der Quelle s und der Senke t und f ein Vorfluss in G. Eine Funktion h : V → N ist eine Höhenfunktion 3 , falls h(s) = |V |, h(t) = 0 und für jede Restkante (u, v) ∈ Ef h(u) ≤ h(v) + 1 gilt. Wir erhalten unmittelbar das folgende Lemma. Lemma 26.12 Sei G = (V, E) ein Flussnetzwerk, f ein Vorfluss in G und h eine Höhenfunktion von V . Für jedes Knotenpaar u, v ∈ V mit h(u) > h(v) + 1 ist (u, v) keine Kante in dem Restgraphen.
Die Push-Operation Die Grundoperation Push(u, v) wird angewendet, wenn u ein Überschussknoten ist und cf (u, v) > 0 sowie h(u) = h(v) + 1 gilt. Der unten angegebene Pseudocode aktualisiert den Vorfluss f und den Flussüberschuss von u und v. Er setzt voraus, dass wir die Restkapazitäten in konstanter Zeit berechnen können, wenn c und f gegeben sind. Wir speichern den Flussüberschuss am Knoten u im Attribut u.e und die Höhe von u im Attribut u.h. Der Ausdruck Δf (u, v) ist eine temporäre Variable, die die Menge des Flusses speichert, die wir von u nach v drücken können. 3 In der Literatur wird eine Höhenfunktion häufig als „Distanzfunktion“ und die Höhe eines Knotens als „Distanzmarkierung“ bezeichnet. Wir verwenden den Begriff „Höhe“, da er die hinter dem Algorithmus stehende Intuition anschaulicher zum Ausdruck bringt. Die Verwendung des Begriffs „Relabel“ behalten wir uns für die Operation vor, die die Höhe eines Knotens vergrößert. Die Höhe eines Knotens ist verwandt mit seiner Distanz zur Senke t, wie wir sie bei einer Breitensuche auf dem transponierten Graphen GT finden würden.
752
26 Maximaler Fluss
Push(u, v) 1 // Anwendbar wenn: u ist überflutet, cf (u, v) > 0, und u.h = v.h + 1. 2 // Aktion: Drücke Δf (u, v) = min(u.e, cf (u, v)) Einheiten des Flusses von u nach v. 3 Δf (u, v) = min(u.e, cf (u, v)) 4 if (u, v) ∈ E 5 (u, v).f = (u, v).f + Δf (u, v) 6 else (v, u).f = (v, u).f − Δf (u, v) 7 u.e = u.e − Δf (u, v) 8 v.e = v.e + Δf (u, v) Der Pseudocode von Push arbeitet wie folgt: Da der Knoten u einen positiven Flussüberschuss u.e hat und die Restkapazität von (u, v) positiv ist, können wir den Fluss von u nach v um Δf (u, v) = min(u.e, cf (u, v)) erhöhen, ohne dass u.e negativ wird oder die Kapazität c(u, v) überschritten wird. Zeile 3 berechnet den Wert Δf (u, v) und die Zeilen 4–6 aktualisieren f . Die Zeile 5 erhöht den Fluss auf der Kante (u, v), da wir Fluss über eine Restkante drücken, die eine ursprüngliche Kante ist. Die Zeile 6 verringert den Fluss auf der Kante (v, u), da die Restkante tatsächlich eine Kante ist, die einer Kante des ursprünglichen Netzwerkes entgegengerichtet ist. Schlussendlich aktualisieren die Zeilen 7–8 den Flussüberschuss, der in die Knoten u und v eingeht. Ist f ein Vorfluss, bevor Push aufgerufen wird, so ist er auch nach der Abarbeitung von Push ein Vorfluss. Beachten Sie, dass der Code von Push an keiner Stelle von den Höhen von u und v abhängt. Trotzdem verbieten wir, dass er aufgerufen wird, wenn nicht u.h = v.h +1 gilt. Daher drücken wir einen Flussüberschuss jeweils nur um eine Höhendifferenz von 1 nach unten. Nach Lemma 26.12 existieren keine Restkanten zwischen Knoten, deren Höhen sich um mehr als 1 unterscheiden. Solange das Attribut h tatsächlich eine Höhenfunktion ist, würden wir also nichts gewinnen, wenn wir erlauben würden, Fluss über mehr als eine Höheneinheit zu drücken. Wir nennen die Operation Push(u, v) ein Push von u nach v. Wenn die Push-Operation auf eine Kante (u, v) angewendet wird, die den Knoten u verlässt, so sprechen wir auch von einem auf u angewendeten Push. Ein Push von u nach v ist sättigend , wenn die Kante (u, v) anschließend gesättigt ist (d. h. es gilt dann cf (u, v) = 0); anderenfalls ist die Push-Operation nichtsättigend . Wenn eine Kante gesättigt ist, verschwindet sie aus dem Restnetzwerk. Ein einfaches Lemma charakterisiert eins der Ergebnisse eines nichtsättigenden Pushes. Lemma 26.13 Nach einem nichtsättigenden Push von u nach v ist der Knoten u nicht mehr überflutet. Beweis: Da die Push-Operation nichtsättigend war, muss die Menge Δf (u, v) des Flusses, die tatsächlich von u nach v gedrückt worden ist, vor der Push-Operation gleich u.e gewesen sein. Da u.e um diesen Betrag reduziert wird, ist u.e nach der Push-Operation gleich 0.
26.4 ∗ Push/Relabel-Algorithmen
753
Die Relabel-Operation Die Operation Relabel(u) wird angewendet, wenn u einen Flussüberschuss besitzt und wenn u.h ≤ v.h für alle Kanten (u, v) ∈ Ef gilt. Wir können also einen überfluteten Knoten u ummarkieren, wenn wir für keinen Knoten v, für den es eine Restkapazität von u nach v gibt, Fluss von u nach v drücken können, weil v nicht unterhalb von u liegt. (Rufen Sie sich in Erinnerung, dass nach Definition weder die Quelle s noch die Senke t überflutet sein können, sodass s und t niemals ummarkiert werden.)
Relabel(u) 1 // Anwendbar wenn: u ist überflutet und für alle v ∈ V mit (u, v) ∈ Ef gilt u.h ≤ v.h. 2 // Aktion: setze die Höhe von u auf einen höheren Wert. 3 u.h = 1 + min {v.h : (u, v) ∈ Ef } Wenn wir die Operation Relabel(u) aufrufen, sagen wir, dass der Knoten u ummarkiert wird. Beachten Sie, dass Ef mindestens eine aus u austretende Kante enthalten muss, wenn u ummarkiert wird, sodass die Minimierung im Code über einer nichtleeren Menge ausgeführt wird. Diese Eigenschaft folgt aus der Voraussetzung, dass u überflutet ist, aus dem wir u.e =
v∈V
f (v, u) −
f (u, v) > 0
v∈V
folgern können. Da alle Flüsse nichtnegativ sind, muss es wenigstens einen Knoten v mit (v, u).f > 0 geben. Dann gilt aber cf (u, v) > 0, woraus (u, v) ∈ Ef folgt. Die Operation Relabel(u) weist u also die größte Höhe zu, die nach den Beschränkungen der Höhenfunktionen erlaubt ist.
Der generische Algorithmus Der generische Push/Relabel-Algorithmus verwendet die folgende Unterroutine, um einen initialen Vorfluss im Flussnetzwerk zu erzeugen. Initialize-Preflow(G, s) 1 for jeden Knoten v ∈ G.V 2 v.h = 0 3 v.e = 0 4 for jede Kante (u, v) ∈ G.E 5 (u, v).f = 0 6 s.h = |G.V | 7 for jeden Knoten v ∈ s.Adj 8 (s, v).f = c(s, v) 9 v.e = c(s, v) 10 s.e = s.e − c(s, v)
754
26 Maximaler Fluss
Initialize-Preflow erzeugt einen initialen Vorfluss f , der durch (u, v).f =
c(u, v) 0
falls u = s , anderenfalls .
(26.15)
definiert ist. Das heißt, wir füllen jede Kante, die aus der Quelle s austritt, bis zu ihrer Kapazität, während alle anderen Kanten keinen Fluss führen. Für jeden Knoten v, der mit der Quelle benachbart ist, gilt anfangs v.e = c(s, v), und wir initialisieren s.e mit dem Negativen der Summe dieser Kapazitäten. Der generische Algorithmus startet mit einer initialen Höhenfunktion h, die durch u.h =
|V | 0
falls u = s , anderenfalls .
(26.16)
gegeben ist. Gleichung (26.16) definiert eine Höhenfunktion, da die einzigen Kanten (u, v), für die u.h > v.h + 1 gilt, diejenigen mit u = s sind. Diese Kanten sind gesättigt, was bedeutet, dass sie nicht zum Restnetzwerk gehören. Die Initialisierung, gefolgt von einer Reihe von Push- und Relabel-Operationen, die in nicht näher bestimmter Reihenfolge ausgeführt werden, liefert den generischen Push/Relabel-Algorithmus:
Generic-Push-Relabel(G) 1 Initialize-Preflow(G, s) 2 while es existiert eine durchführbare Push- oder Relabel-Operation 3 wähle eine durchführbare Push- oder Relabel-Operation und führe sie aus Das folgende Lemma sagt aus, dass mindestens eine der beiden Grundoperationen anwendbar ist, solange es einen Überschussknoten gibt.
Lemma 26.14: (Überschussknoten können entweder gepusht oder ummarkiert werden.) Seien G = (V, E) ein Flussnetzwerk mit der Quelle s und der Senke t, f ein Vorfluss und h eine beliebige Höhenfunktion für f . Wenn u ein Überschussknoten ist, dann kann auf diesen entweder eine Push- oder eine Relabel-Operation angewendet werden. Beweis: Für jede Restkante (u, v) gilt h(u) ≤ h(v) + 1, da h eine Höhenfunktion ist. Wenn kein Push auf einen überfluteten Knoten u anwendbar ist, dann muss für jede Restkante (u, v) die Ungleichung h(u) < h(v) + 1 gelten, woraus h(u) ≤ h(v) folgt. Also kann eine Relabel-Operation für u durchgeführt werden.
26.4 ∗ Push/Relabel-Algorithmen
755
Korrektheit der Push/Relabel-Methode Um zu zeigen, dass der generische Push/Relabel-Algorithmus das maximale-Fluss-Problem löst, werden wir zunächst beweisen, dass der Vorfluss f bei Terminierung ein maximaler Fluss ist. Danach werden wir zeigen, dass der Algorithmus terminiert. Wir beginnen mit einigen Beobachtungen zu der Höhenfunktion h. Lemma 26.15: (Knotenhöhen verringern sich niemals) Während der Ausführung der Prozedur Generic-Push-Relabel auf einem Flussnetzwerk G = (V, E) verringert sich zu keinem Zeitpunkt die Knotenhöhe u.h eines Knoten u ∈ V . Zudem erhöht sich die Höhe u.h eines Knoten u jedes Mal jeweils um mindestens 1, wenn eine Relabel-Operation auf den Knoten u angewendet wird.
Beweis: Da sich die Knotenhöhen nur durch Relabel-Operationen ändern, genügt es, die zweite Aussage des Lemmas zu beweisen. Wenn der Knoten u ummarkiert wird, dann gilt für alle Knoten mit (u, v) ∈ Ef die Ungleichung u.h ≤ v.h. Damit ist u.h < 1 + min {v.h : (u, v) ∈ Ef } und die Operation muss u.h erhöhen.
Lemma 26.16 Sei G = (V, E) ein Flussnetzwerk mit der Quelle s und der Senke t. Während der Ausführung der Prozedur Generic-Push-Relabel auf G stellt das Attribut h immer eine Höhenfunktion dar. Beweis: Der Beweis erfolgt durch Induktion über die Anzahl der ausgeführten Grundoperationen. Anfangs ist h eine Höhenfunktion, wie wir bereits festgestellt haben. Wir behaupten, dass eine Relabel(u)-Operation die Funktion h als Höhenfunktion erhält, wenn h zuvor eine Höhenfunktion ist. Für eine aus u austretende Restkante (u, v) ∈ Ef stellt die Operation Relabel(u) sicher, dass anschließend u.h ≤ v.h + 1 gilt. Betrachten Sie nun eine in u eintretende Kante (w, u). Nach Lemma 26.15 folgt aus der Gültigkeit der Ungleichung w.h ≤ u.h + 1 vor der Relabel(u)-Operation, dass danach w.h < u.h + 1 gilt. Unter der Operation Relabel(u) bleibt h also eine Höhenfunktion. Betrachten Sie nun eine Push(u, v)-Operation. Diese Operation kann die Kante (v, u) zu Ef hinzufügen und sie kann die Kante (u, v) aus Ef entfernen. Im ersten Fall gilt v.h = u.h − 1 < u.h + 1, sodass h eine Höhenfunktion bleibt. Im zweiten Fall wird durch die Entfernung von (u, v) aus dem Restnetzwerk gleichzeitig die zugehörige Bedingung entfernt, sodass h auch in diesem Fall eine Höhenfunktion bleibt. Das folgende Lemma liefert eine wichtige Eigenschaft von Höhenfunktionen.
756
26 Maximaler Fluss
Lemma 26.17 Seien G = (V, E) ein Flussnetzwerk mit der Quelle s und der Senke t, f ein Vorfluss in G und h eine Höhenfunktion auf V . Dann gibt es keinen Pfad von der Quelle s zur Senke t im Restnetzwerk Gf . Beweis: Wir nehmen zum Zwecke des Widerspruchs an, dass es einen Pfad p = v0 , v1 , . . . , vk von s nach t in Gf gäbe, mit v0 = s und vk = t. Ohne Beschränkung der Allgemeinheit ist p ein einfacher Pfad, sodass k < |V | gilt. Für i = 0, 1, . . . , k − 1 gilt (vi , vi+1 ) ∈ Ef . Da h eine Höhenfunktion ist, gilt h(vi ) ≤ h(vi+1 ) + 1 für i = 0, 1, . . . , k − 1. Akkumulieren wir diese Ungleichungen über den Pfad p, so erhalten wir h(s) ≤ h(t) + k. Da aber h(t) = 0 gilt, folgt h(s) ≤ k < |V |, was der Voraussetzung widerspricht, dass für eine Höhenfunktion h(s) = |V | gilt. Wir sind nun in der Lage zu zeigen, dass der berechnete Vorfluss bei Terminierung des generischen Push/Relabel-Algorithmus ein maximaler Fluss ist. Theorem 26.18: (Korrektheit des generischen Push/Relabel-Algorithmus) Wenn der Algorithmus Generic-Push-Relabel auf einem Flussnetzwerk G = (V, E) mit der Quelle s und der Senke t terminiert, dann ist der berechnete Vorfluss ein maximaler Fluss für G. Beweis: Wir verwenden die folgende Schleifeninvariante: Jedes Mal, wenn der Test der while-Schleife in Zeile 2 von Generic-PushRelabel ausgeführt wird, ist f ein Vorfluss. Initialisierung: Initialize-Preflow sorgt dafür, dass f ein Vorfluss ist. Fortsetzung: Die einzigen Aktionen innerhalb der while-Schleife der Zeilen 2–3 sind Push- und Relabel-Operationen. Relabel-Operationen berühren nur die Höhenattribute und nicht die Flusswerte; folglich haben sie keinen Einfluss darauf, ob f ein Vorfluss ist. Wie auf Seite 752 gezeigt wurde, bleibt f nach einer PushOperation ein Vorfluss, wenn dies vorher der Fall war. Terminierung: Bei Terminierung hat kein Knoten aus V −{s, t} mehr einen Flussüberschuss, da es wegen Lemma 26.14 und der Invariante, dass f immer ein Vorfluss ist, keine Überschussknoten mehr gibt. Daher ist f ein Fluss. Lemma 26.16 zeigt, dass h bei Terminierung eine Höhenfunktion ist, und somit sagt uns Lemma 26.17, dass es im Restnetzwerk Gf keinen Pfad von s nach t gibt. Nach dem maxflowmincut-Theorem (Theorem 26.6) ist f daher ein maximaler Fluss.
26.4 ∗ Push/Relabel-Algorithmen
757
Analyse der Push/Relabel-Methode Um zu zeigen, dass der generische Push/Relabel-Algorithmus tatsächlich terminiert, werden wir die Anzahl der durch den Algorithmus auszuführenden Operationen abschätzen. Wir schätzen die Anzahl für jede der drei Operationstypen für sich ab: die RelabelOperation, die sättigende Push-Operation und die nichtsättigende Push-Operation. Mit Kenntnis dieser Schranken ist es nicht schwierig, einen Algorithmus zu konstruieren, der in Zeit O(V 2 E) läuft. Bevor wir mit der Analyse beginnen, wollen wir jedoch ein wichtiges Lemma beweisen. Rufen Sie sich hierfür in Erinnerung, dass wir Kanten, die in die Quelle des Restnetzwerkes einlaufen, erlaubt haben. Lemma 26.19 Seien G = (V, E) ein Flussnetzwerk mit der Quelle s und der Senke t und f ein Vorfluss in G. Dann gibt es für jeden Überschussknoten x im Restnetzwerk Gf einen einfachen Pfad von x nach s. Beweis: Sei U die Menge {v : es existiert ein einfacher Pfad von x nach v in Gf } für einen Überschussknoten x und nehmen Sie zum Zwecke des Widerspruchs an, dass s ∈ U gelten würde. Sei U = V − U . Wir summieren die Gleichung (26.14) der Definition des Flussüberschusses eines Knotens über alle Knoten aus U auf und erhalten wegen V = U ∪ U
e(u)
u∈U
=
u∈U
=
2
⎛⎛ ⎝⎝
u∈U
=
f (v, u) −
v∈V
=
u∈U v∈U
3 f (u, v)
v∈V
f (v, u) +
v∈U
f (v, u) +
u∈U v∈U
⎞
f (v, u)⎠ − ⎝ f (v, u) −
f (v, u) −
u∈U v∈U
u∈U v∈U
f (u, v) +
v∈U
v∈U
⎛
⎞⎞ f (u, v)⎠⎠
v∈U
f (u, v) −
f (u, v)
u∈U v∈U
f (u, v) .
u∈U v∈U
Wir wissen, dass u∈U e(u) positiv sein muss, da x ∈ U ist und e(x) > 0 gilt, alle Knoten mit Ausnahme von s einen nichtnegativen Flussüberschuss haben und nach Annahme s nicht in U enthalten ist. Somit gilt f (v, u) − f (u, v) > 0 . (26.17) u∈U v∈U
u∈U v∈U
Alle Kantenflüsse sind nichtnegativ, und so muss u∈U v∈U f (v, u) > 0 sein, damit Gleichung (26.17) gelten kann. Es muss also mindestens ein Paar von Knoten u ∈ U
758
26 Maximaler Fluss
und v ∈ U mit f (v , u ) > 0 geben. Wenn aber f (v , u ) > 0 gilt, dann muss es eine Restkante (u , v ) geben, was bedeutet, dass es einen einfachen Pfad von x nach v gibt (nämlich der Pfad x ; u → v ), was ein Widerspruch zu der Definition von U ist. Das nächste Lemma schätzt die Höhen der Knoten ab. Das dazugehörige Korollar liefert eine Schranke für die Anzahl der Relabel-Operationen, die insgesamt ausgeführt werden. Lemma 26.20 Sei G = (V, E) ein Flussnetzwerk mit der Quelle s und der Senke t. Zu jedem Zeitpunkt während der Ausführung von Generic-Push-Relabel auf G gilt u.h ≤ 2 |V | − 1 für alle Knoten u ∈ V . Beweis: Die Höhen der Quelle s und der Senke t verändern sich nie, da diese Knoten per Definition keinen Flussüberschuss haben. Es gilt daher immer s.h = |V | und t.h = 0. Beide Höhen sind somit nicht größer als 2 |V | − 1. Betrachten Sie nun einen beliebigen Knoten u ∈ V − {s, t}. Anfangs gilt u.h = 0 ≤ 2 |V | − 1. Wir werden zeigen, dass nach jeder Ummarkierung u.h ≤ 2|V | − 1 weiterhin gilt. Wird auf dem Knoten u eine Relabel-Operation ausgeführt, so hat er einen Flussüberschuss und Lemma 26.19 sagt uns, dass es einen einfachen Pfad p von u nach s in Gf gibt. Sei p = v0 , v1 , . . . , vk mit v0 = u, vk = s und k ≤ |V | − 1. Für i = 0, 1, . . . , k − 1 gilt (vi , vi+1 ) ∈ Ef und damit nach Lemma 26.16 vi .h ≤ vi+1 .h + 1. Akkumulieren wir diese Ungleichungen über den Pfad p, dann erhalten wir u.h = v0 .h ≤ vk .h + k ≤ s.h + (|V | − 1) = 2 |V | − 1 .
Korollar 26.21: (Schranke für die Anzahl der Relabel-Operationen) Sei G = (V, E) ein Flussnetzwerk mit der Quelle s und der Senke t. Dann beträgt die Anzahl der Relabel-Operationen während der Ausführung von GenericPush-Relabel auf G pro Knoten höchstens 2 |V | − 1 und insgesamt höchstens 2 (2 |V | − 1)(|V | − 2) < 2 |V | . Beweis: Nur die |V | − 2 Knoten aus V − {s, t} können ummarkiert werden. Sei also u ∈ V − {s, t}. Die Operation Relabel(u) erhöht u.h. Der Wert von u.h ist anfangs gleich 0 und wächst nach Lemma 26.20 bis auf maximal 2 |V | − 1. Also wird jeder Knoten u ∈ V − {s, t} höchstens (2 |V | − 1)-mal ummarkiert, und die Gesamtzahl der 2 durchgeführten Relabel-Operationen beträgt höchstens (2 |V | − 1)(|V | − 2) < 2 |V | . Mithilfe von Lemma 26.20 können wir auch eine Schranke für die Anzahl der sättigenden Push-Operationen angeben.
26.4 ∗ Push/Relabel-Algorithmen
759
Lemma 26.22: (Schranke für die Anzahl der sättigenden Push-Operationen) Die Anzahl der während der Ausführung von Generic-Push-Relabel auf einem Flussnetzwerk G = (V, E) durchgeführten sättigenden Push-Operationen ist kleiner als 2 |V | |E|. Beweis: Für jedes Knotenpaar u, v ∈ V werden wir nicht zwischen sättigenden Pushs von u nach v und sättigenden Pushs von v nach u unterscheiden und sie einheitlich als Pushs zwischen u und v bezeichnen. Falls es solche Push-Operationen gibt, so ist zumindest eines der geordneten Paare (u, v) und (v, u) tatsächlich eine Kante von E. Nehmen Sie nun an, dass eine sättigende Push-Operation von u nach v stattgefunden hätte. Zu diesem Zeitpunkt gilt v.h = u.h − 1. Damit zu einem späteren Zeitpunkt ein weiterer sättigender Push von u nach v stattfinden kann, muss der Algorithmus zunächst eine Menge des Flusses von v nach u pushen, was erst möglich ist, wenn v.h = u.h + 1 gilt. Da sich u.h niemals verringert, muss der Wert von v.h mindestens um 2 steigen, damit v.h = u.h + 1 erfüllt ist. Analog muss u.h zwischen zwei sättigenden PushOperationen von v nach u mindestens um 2 wachsen. Die Höhen sind anfangs 0 und überschreiten nach Lemma 26.20 niemals den Wert 2 |V |−1. Daraus folgt, dass die Höhe jedes Knotens weniger als |V |-mal um 2 erhöht werden kann. Da mindestens eine der Höhen u.h und v.h zwischen zwei sättigenden Pushs zwischen u und v um 2 ansteigen muss, gibt es weniger als 2 |V | sättigende Pushs zwischen u und v. Multipliziert man dies mit der Anzahl der Kanten, so erhält man eine Schranke für die Anzahl sättigender Pushs von insgesamt weniger als 2 |V | |E|. Das folgende Lemma gibt eine Schranke für die Anzahl der nichtsättigenden PushOperationen im generischen Push/Relabel-Algorithmus an. Lemma 26.23: (Schranke für die Anzahl der nichtsättigenden Push-Operationen) Die Anzahl der nichtsättigenden Push-Operationen, die während eines Laufes von Generic-Push-Relabel auf einem Flussnetzwerk G = (V, E) durchgeführt wer2 den, ist kleiner als 4 |V | (|V | + |E|). Beweis: Definieren Sie eine Potentialfunktion Φ= v:e(v)>0 v.h. Anfangs ist Φ = 0. Der Wert von Φ kann sich nach jeder Relabel-, sättigenden und nichtsättigenden PushOperation ändern. Wir werden den Umfang abschätzen, in dem sättigende Push-Operationen und Relabel-Operationen zu dem Anwachsen von Φ beitragen. Dann werden wir zeigen, dass jede nichtsättigende Push-Operation das Potential Φ um mindestens 1 verringern muss. Diese Schranken verwenden wir, um eine obere Schranke für die Anzahl der nichtsättigenden Push-Operationen abzuleiten. Lassen Sie uns die beiden Möglichkeiten untersuchen, durch die sich Φ erhöhen kann. Erstens erhöht eine Relabel-Operation auf einem Knoten u das Potential um weniger als 2 |V |, da die Menge, über die summiert wird, die gleiche ist, und die Relabel-Operation die Höhe von u nicht um mehr als seine maximal mögliche Höhe erhöhen kann. Diese ist
760
26 Maximaler Fluss
nach Lemma 26.20 höchstens 2 |V | − 1. Zweitens erhöht eine sättigende Push-Operation von einem Knoten u zu einem Knoten v das Potential Φ um weniger als 2 |V |, da sich keine Höhen ändern und nur der Knoten v, dessen Höhe maximal 2 |V | − 1 ist, einen Flussüberschuss bekommen kann. Wir zeigen nun, dass eine nichtsättigende Push-Operation von u nach v das Potential um mindestens 1 verringert. Warum? Vor dem nichtsättigenden Push von u nach v hatte u einen Flussüberschuss und v eventuell auch. Nach Lemma 26.13 hat u nach der Push-Operation keinen Flussüberschuss mehr. Sofern der Knoten v nicht die Quelle ist, kann v nach der Push-Operation eventuell einen Flussüberschuss haben. Daher ist die Potentialfunktion genau um den Betrag u.h gefallen, und entweder um 0 oder den Betrag v.h gewachsen. Wegen u.h − v.h = 1 besteht der Nettoeffekt darin, dass die Potentialfunktion mindestens um 1 gefallen ist. Der Gesamtumfang des Wachstums von Φ während der Ausführung des Algorithmus geht also auf Relabel- und sättigende Push-Operationen zurück und Korollar 26.21 und Lemma 26.22 beschränken den Aufwuchs nach oben durch 2
2
(2 |V |)(2 |V | ) + (2 |V |)(2 |V | |E|) = 4 |V | (|V | + |E|) . Wegen Φ ≥ 0, kann sich die Potentialfunktion auch nur höchstens um diesen Betrag reduzieren und damit ist die Gesamtanzahl der nichtsättigenden Push-Operationen klei2 ner als 4 |V | (|V | + |E|). Nachdem wir obere Schranken für die Anzahl der Relabel-Operationen sowie für die Anzahl der sättigenden und nicht-sättigenden Push-Operationen angegeben haben, sind wir in der Lage, die folgende Analyse der Prozedur Generic-Push-Relabel durchzuführen, die somit auch für jeden auf der Push/Relabel-Methode basierenden Algorithmus Gültigkeit hat. Theorem 26.24 Während der Ausführung der Prozedur Generic-Push-Relabel auf einem Flussnetzwerk G = (V, E) ist die Anzahl der Grundoperationen in O(V 2 E). Beweis: Der Beweis folgt unmittelbar aus Korollar 26.21 und den Lemmata 26.22 und 26.23. Der Algorithmus terminiert also nach O(V 2 E) Operationen. Nun müssen wir nur noch eine effiziente Methode für die Implementierung jeder Operation und für die Auswahl einer geeigneten auszuführenden Operation angeben. Korollar 26.25 Es gibt eine Implementierung des generischen Push/Relabel-Algorithmus, der auf jedem Flussnetzwerk in Zeit O(V 2 E) läuft.
26.4 ∗ Push/Relabel-Algorithmen
761
Beweis: Übung 26.4-2 verlangt von Ihnen zu zeigen, wie wir den generischen Algorithmus mit einem Zeitaufwand von O(V ) pro Relabel-Operation und von O(1) pro Push-Operation implementieren können. Zudem sollen Sie eine Datenstruktur entwerfen, die es Ihnen erlaubt, eine anwendbare Operation in Zeit O(1) auszuwählen. Hiermit folgt das Korollar.
Übungen 26.4-1 Zeigen Sie, dass, nachdem die Prozedur Initialize-Preflow(G, s) terminiert, die Ungleichung s.e ≤ − |f ∗ | gilt, wobei f ∗ ein maximaler Fluss in G ist. 26.4-2 Zeigen Sie, wie wir den generischen Push/Relabel-Algorithmus so implementieren können, dass er mit Zeit O(V ) pro Relabel-Operation, Zeit O(1) pro Push-Operation und Zeit O(1) für die Auswahl einer anwendbaren Operation, also insgesamt mit Zeit O(V 2 E) auskommt. 26.4-3 Beweisen Sie, dass der generische Push/Relabel-Algorithmus eine Gesamtzeit von nur O(V E) für die Ausführung aller O(V 2 ) Relabel-Operationen aufwendet. 26.4-4 Nehmen Sie an, wir hätten in einem Flussnetzwerk G = (V, E) mithilfe eines Push/ Relabel-Algorithmus einen maximalen Fluss gefunden. Geben Sie einen schnellen Algorithmus an, der einen minimalen Schnitt in G bestimmt. 26.4-5 Geben Sie einen effizienten Push/Relabel-Algorithmus an, der ein maximales Matching in einem bipartiten Graphen bestimmt. Analysieren Sie Ihren Algorithmus. 26.4-6 Setzen Sie voraus, dass alle Kantenkapazitäten in einem Flussnetzwerk G = (V, E) Werte aus der Menge {1, 2, . . . , k} annehmen. Analysieren Sie die Laufzeit des generischen Push/Relabel-Algorithmus in Abhängigkeit von |V |, |E| und k. (Hinweis: Wie oft kann auf jeder Kante eine nichtsättigende PushOperation angewendet werden, bevor sie gesättigt wird?) 26.4-7 Zeigen Sie, dass wir Zeile 6 der Prozedur Initialize-Preflow zu 6 s.h = |G.V | − 2 ändern können, ohne die Korrektheit oder die asymptotische Performanz des generischen Push/Relabel-Algorithmus zu berühren. 26.4-8 Sei δf (u, v) die Distanz (Anzahl der Kanten) von u nach v im Restnetzwerk Gf . Zeigen Sie, dass die Prozedur Generic-Push-Relabel die Eigenschaft erhält, dass u.h ≤ δf (u, t) aus u.h < |V | und u.h − |V | ≤ δf (u, s) aus u.h ≥ |V | folgt.
762
26 Maximaler Fluss
26.4-9∗ Wie bei der vorherigen Übung sei δf (u, v) die Distanz von u nach v im Restnetzwerk Gf . Zeigen Sie, wie wir den generischen Push/Relabel-Algorithmus so modifizieren können, dass er die Eigenschaft erhält, dass u.h = δf (u, t) aus u.h < |V | und u.h − |V | = δf (u, s) aus u.h ≥ |V | folgt. Die Gesamtlaufzeit, die Ihr Algorithmus für die Erhaltung dieser Eigenschaft aufwendet, sollte in O(V E) sein. 26.4-10 Zeigen Sie, dass die Anzahl der nichtsättigenden Push-Operationen, die der Algorithmus Generic-Push-Relabel auf einem Flussnetzwerk G = (V, E) 2 ausführt, für |V | ≥ 4 höchstens 4 |V | |E| ist.
∗ 26.5 Der Relabel-to-Front-Algorithmus Die Push/Relabel-Methode erlaubt es uns, die Grundoperationen in völlig beliebiger Reihenfolge auszuführen. Wenn wir jedoch die Reihenfolge mit Bedacht wählen und die Datenstruktur des Netzwerks effizient verwalten, dann können wir das maximale-FlussProblem schneller als in der durch Korollar 26.25 gegebenen Schranke von O(V 2 E) lösen. Wir werden nun den Relabel-to-Front-Algorithmus untersuchen. Dabei handelt es sich um einen Push/Relabel-Algorithmus mit einer Laufzeit von O(V 3 ), was asymptotisch mindestens so gut wie O(V 2 E) und für dichte Netzwerke sogar besser ist. Der Relabel-to-Front-Algorithmus verwaltet eine Liste der Knoten des Netzwerks. Beginnend am Kopf durchsucht der Algorithmus die Liste, wählt sukzessive einen Überschussknoten u aus und „entlädt“ ihn dann. Das heißt, er führt solange Push- und Relabel-Operationen aus, bis u keinen positiven Flussüberschuss mehr besitzt. Wenn immer wir einen Knoten ummarkieren, setzen wir ihn an den Kopf (engl.: front ) der Liste (daher auch der Name „Relabel-to-Front“) und der Algorithmus beginnt seine Suche von Neuem. Die Korrektheit und die Analyse des Relabel-to-Front-Algorithmus stützt sich auf den Begriff der „zulässigen“ Kanten. Dies sind jene Kanten des Restnetzwerks, durch die der Fluss gepusht werden kann. Nachdem wir einige Eigenschaften des Netzwerks der zulässigen Kanten bewiesen haben, werden wir die Operation des Entladens untersuchen und anschließend den Relabel-to-Front-Algorithmus vorstellen und analysieren.
Zulässige Kanten und Netzwerke Ist G = (V, E) ein Flussnetzwerk mit der Quelle s und der Senke t, f ein Vorfluss in G und h eine Höhenfunktion, dann nennen wir (u, v) eine zulässige Kante, wenn cf (u, v) > 0 und h(u) = h(v) + 1 gilt. Anderenfalls ist (u, v) unzulässig . Unter dem zulässigen Netzwerk verstehen wir den Graphen Gf,h = (V, Ef,h ), wobei Ef,h die Menge der zulässigen Kanten darstellt. Das zulässige Netzwerk besteht aus jenen Kanten, durch die der Fluss gepusht werden kann. Das folgende Lemma zeigt, dass dieses Netzwerk ein gerichteter azyklischer Graph ist.
26.5 ∗ Der Relabel-to-Front-Algorithmus
763
Lemma 26.26: (Das zulässige Netzwerk ist azyklisch.) Wenn G = (V, E) ein Flussnetzwerk, f ein Vorfluss in G und h eine Höhenfunktion auf G ist, dann ist das zulässige Netzwerk Gf,h = (V, Ef,h ) azyklisch. Beweis: Der Beweis erfolgt indirekt. Wir nehmen an, dass Gf,h einen Zyklus p = v0 , v1 , . . . , vk mit v0 = vk und k > 0 enthalten würde. Da jede Kante von p zulässig ist, gilt h(vi−1 ) = h(vi ) + 1 für i = 1, 2, . . . , k. Summieren wir die Höhen über alle Kanten des Zyklus, so erhalten wir k
h(vi−1 ) =
i=1
k
(h(vi ) + 1)
i=1
=
k
h(vi ) + k .
i=1
Da jeder Knoten des Zyklus p in jeder der beiden Summen genau einmal vorkommt, folgt hieraus der Widerspruch 0 = k. Die nächsten beiden Lemmata zeigen, wie Push- und Relabel-Operationen das zulässige Netzwerk verändern. Lemma 26.27 Sei G = (V, E) ein Flussnetzwerk, f ein Vorfluss in G und das Attribut h eine Höhenfunktion. Wenn u ein Überschussknoten und (u, v) eine zulässige Kante ist, dann ist die Operation Push(u, v) anwendbar. Die Operation erzeugt keine neuen zulässigen Kanten, kann aber dazu führen, dass (u, v) unzulässig wird. Beweis: Nach der Definition einer zulässigen Kante können wir Fluss von u nach v pushen. Da u einen Flussüberschuss besitzt, ist die Operation Push(u, v) anwendbar. Die einzige neue Restkante, die durch diesen Push von u nach v erzeugt werden kann, ist die Kante (v, u). Wegen v.h = u.h − 1 kann die Kante (v, u) nicht zulässig werden. Ist die Operation sättigend, dann gilt anschließend cf (u, v) = 0, und (u, v) ist unzulässig.
Lemma 26.28 Sei G = (V, E) ein Flussnetzwerk, f ein Vorfluss in G und das Attribut h eine Höhenfunktion. Wenn der Knoten u einen Flussüberschuss besitzt und es keine aus u austretenden zulässigen Kanten gibt, dann ist Relabel(u) anwendbar. Nach der Relabel-Operation gibt es mindestens eine zulässige, aus u austretende Kante, jedoch keine zulässige, in u eintretende Kante.
764
26 Maximaler Fluss
Beweis: Wenn der Knoten u einen Überschuss besitzt, dann ist nach Lemma 26.14 für diesen entweder eine Push- oder eine Relabel-Operationen anwendbar. Wenn es keine aus u austretenden zulässigen Kanten gibt, dann kann kein Fluss von u gepusht werden, sodass Relabel(u) anwendbar sein muss. Nach der Relabel-Operation gilt u.h = 1 + min {v.h : (u, v) ∈ Ef }. Wenn also v ein Knoten ist, der das Minimum dieser Menge annimmt, dann wird die Kante (u, v) zulässig. Folglich gibt es nach dem Ummarkieren mindestens eine zulässige Kante, die aus u austritt. Um zu zeigen, dass es nach der Relabel-Operation keine zulässigen Kanten gibt, die in u eintreten, nehmen wir an, dass es einen Knoten v gäbe, für den (v, u) eine zulässige Kante ist. Dann gilt nach der Relabel-Operation v.h = u.h + 1 und somit v.h > u.h + 1 unmittelbar vor der Relabel-Operation. Nach Lemma 26.12 existieren jedoch keine Restkanten zwischen Knoten, deren Höhen sich um mehr als 1 unterscheiden. Zudem ändert das Ummarkieren eines Knotens das Restnetzwerk nicht. Also gehört (v, u) nicht zum Restnetzwerk und somit auch nicht zum zulässigen Netzwerk.
Nachbarschaftslisten Im Relabel-to-Front-Algorithmus sind die Kanten in „Nachbarschaftslisten“ organisiert. Für ein gegebenes Flussnetzwerk G = (V, E) ist die Nachbarschaftsliste u.N für einen Knoten u ∈ V eine einfach verkettete Liste der Nachbarn von u in G. Der Knoten v erscheint also in der Nachbarschaftsliste u.N , wenn (u, v) ∈ E oder (v, u) ∈ E gilt. Die Nachbarschaftsliste u.N enthält genau jene Knoten v, für die es eine Restkante (u, v) geben kann. Das Attribut u.N .kopf zeigt auf den ersten Knoten der Liste u.N und v.n¨a chster zeigt auf den Knoten, der dem Knoten v in der Nachbarschaftsliste folgt; dieser Zeiger ist nil, wenn v der letzte Knoten in der Nachbarschaftsliste ist. Der Relabel-to-Front-Algorithmus geht zyklisch durch jede Nachbarschaftsliste, wobei die Reihenfolge beliebig, aber während eines Laufes des Algorithmus fest ist. Für jeden Knoten u zeigt das Attribut u.aktuell auf denjenigen Knoten aus u.N , der gerade bearbeitet wird. Zu Beginn wird u.aktuell auf u.N .kopf gesetzt.
Entladen eines Überschussknotens Ein Überschussknoten wird entladen, indem sein gesamter Flussüberschuss über die zulässigen Kanten zu seinen benachbarten Knoten gepusht wird, wobei u falls nötig ummarkiert wird, damit die aus u austretenden Kanten zulässig werden. Der Pseudocode sieht folgendermaßen aus. Discharge(u) 1 while u.e > 0 2 v = u.aktuell 3 if v = = nil 4 Relabel(u) 5 u.aktuell = u.N .kopf 6 elseif cf (u, v) > 0 und u.h = = v.h + 1 7 Push(u, v) 8 else u.aktuell = v.n¨a chster
26.5 ∗ Der Relabel-to-Front-Algorithmus
(a)
(b)
(c)
6 5 4 3 2 1 0 6 5 4 3 2 1 0 6 5 4 3 2 1 0
s –26 14
/14
x 0
5/5
y 19
8
14
/14
5/5
y 19
8
14
/14
5/5
y 11
8/8
2 s x z
3 s x z
5 s x z
6 s x z
7 s x z
8 s x z
9 s x z
4 s x z
z 0
s –26
x 0
1 s x z z 0
s –26
x 0
765
z 8
Abbildung 26.9: Das Entladen eines Knotens y. Es sind 15 Iterationen der while-Schleife von Discharge notwendig, um den gesamten Überschuss von y zu pushen. Nur die Nachbarn von y und die Kanten, die in y eintreten oder aus y austreten, sind in den Abbildungen eingezeichnet. Die Zahl innerhalb eines Knotens gibt den Flussüberschuss des Knotens zu Beginn der ersten der in dem jeweiligen Teil gezeigten Iterationen an, und jeder Knoten ist entsprechend seiner Höhe während des jeweiligen Abschnitts eingezeichnet. Die Nachbarschaftsliste y. N zu Beginn jeder Iteration ist jeweils rechts angegeben; in der obersten Zeile steht jeweils die Nummer der Iteration. Der schattierte Nachbar ist jeweils y. aktuell . (a) Zu Beginn sind 19 Einheiten des Überschusses von y zu pushen und es gilt y. aktuell = s. Die Iterationen 1, 2 und 3 setzen y. aktuell einfach nur weiter, da es keine von y ausgehenden zulässigen Kanten gibt. In Iteration 4 gilt y. aktuell = nil (was in der Abbildung daran erkennbar ist, dass sich die Schattierung unterhalb der Liste befindet). Daher wird y ummarkiert und y. aktuell wird an den Kopf der Nachbarschaftsliste zurückgesetzt. (b) Nach der Relabel-Operation hat der Knoten y die Höhe 1. In den Iterationen 5 und 6 stellt sich heraus, dass die Kanten (y, s) und (y, x) unzulässig sind; die Iteration 7 pusht jedoch 8 Einheiten des Flussüberschusses von y nach z. Wegen der PushOperation wird y. aktuell in dieser Iteration nicht weitergesetzt. (c) Da die Push-Operation in Iteration 7 die Kante (y, z) gesättigt hat, ist diese in Iteration 8 unzulässig. In Iteration 9 gilt y. aktuell = nil, sodass der Knoten y wieder ummarkiert und y. aktuell zurückgesetzt wird.
766
6 5 4 3 2 1 0
14
x 0
s –26
5/5
y 11
8/8
14
s –26
5
y 6
8/8
13 s x z
14 s x z
y 6
14/14
15 s x z z 8
y 0
8/14
x 5
12 s x z z 8
x 5
s –20
11 s x z
z 8
14/
x 5
10 s x z
8/8
(g)
6 5 4 3 2 1 0
14/
8/8
(f)
6 5 4 3 2 1 0
s –26
5
(e)
6 5 4 3 2 1 0
5
(d)
26 Maximaler Fluss
z 8
Abbildung 26.9, fortgesetzt: (d) In der Iteration 10 ist (y, s) unzulässig, Iteration 11 pusht jedoch 5 Einheiten des Flussüberschusses von y nach x. (e) Da y. aktuell in Iteration 11 nicht weitergesetzt wurde, ist (y, x) in Iteration 12 unzulässig. In Iteration 13 ist (y, z) unzulässig, und Iteration 14 markiert den Knoten y um und setzt y. aktuell zurück. (f ) Iteration 15 pusht 6 Einheiten des Flussüberschusses von y nach s. (g) Knoten y hat nun keinen Flussüberschuss mehr und Discharge terminiert. In diesem Beispiel zeigt der Zeiger aktuell am Anfang und am Ende der Prozedur Discharge auf den Kopf der Nachbarschaftliste; im Allgemeinen ist dies jedoch nicht der Fall.
26.5 ∗ Der Relabel-to-Front-Algorithmus
767
Abbildung 26.9 zeigt mehrere Iterationen der while-Schleife der Zeilen 1–8, die ausgeführt wird, solange Knoten u einen positiven Überschuss besitzt. Jede Iteration führt genau eine von drei Aktionen aus. Welche Aktion ausgeführt wird, hängt vom aktuellen Knoten v in der Nachbarschaftsliste u.N ab. 1. Ist v gleich nil, dann haben wir das Ende von u.N erreicht. Zeile 4 markiert Knoten u um und Zeile 5 setzt den aktuellen Nachbarn von u auf die erste Position in u.N . (Das folgende Lemma 26.29 besagt, dass in dieser Situation die RelabelOperation anwendbar ist.) 2. Ist v nicht nil und ist (u, v) eine zulässige Kante (was in Zeile 6 überprüft wird), dann pusht Zeile 7 einen Teil von u’s Flussüberschuss (oder wenn möglich den gesamten) nach v. 3. Ist v nicht nil, aber (u, v) ist eine unzulässige Kante, dann setzt Zeile 8 den Zeiger u.aktuell um eine Position in der Nachbarschaftsliste u.N weiter. Überlegen Sie sich, dass, wenn Discharge auf einen Überschussknoten u angewendet wird, die letzte von Discharge ausgeführte Aktion eine Push-Operation auf u sein muss. Warum? Die Prozedur terminiert nur, wenn u.e gleich 0 wird, und weder die Relabel-Operation noch das Weitersetzen des Zeigers u.aktuell den Wert von u.e beeinflusst. Es muss sichergestellt sein, dass bei einem Aufruf von Push oder Relabel durch die Prozedur Discharge, diese Operationen anwendbar sind. Das nächste Lemma beweist dies. Lemma 26.29 Ruft Discharge in Zeile 7 die Operation Push(u, v) auf, so ist eine Push-Operation auf (u, v) anwendbar. Ruft Discharge in Zeile 4 die Operation Relabel(u) auf, so ist eine Relabel-Operation auf u anwendbar. Beweis: Die Tests in den Zeilen 1 und 6 sichern, dass eine Push-Operation nur dann stattfindet, wenn die Operation anwendbar ist, was die erste Aussage des Lemmas beweist. Um die zweite Aussage zu beweisen, müssen wir gemäß dem Test in Zeile 1 und Lemma 26.28 nur zeigen, dass alle Kanten, die von u ausgehen, unzulässig sind. Zeigt der Zeiger u.aktuell zu Beginn eines Aufrufs von Discharge(u) auf den Kopf von u’s Nachbarschaftsliste und zeigt er am Ende auf das Ende der Liste, so sind alle von u ausgehenden Kanten unzulässig und die Relabel-Operation ist anwendbar. Es ist jedoch während des Aufrufs von Discharge(u) möglich, dass der Zeiger u.aktuell nur einen Teil der Liste traversiert hat, wenn die Prozedur terminiert. Es können dann Aufrufe von Discharge angewendet auf andere Knoten erfolgen, das Attribut u.aktuell wird jedoch während des nächsten Aufrufs Discharge(u) die restliche Liste abarbeiten. Wir betrachten nun, was während eines vollständigen Laufs über die Liste passiert, die beim Kopf von u.N beginnt und mit u.aktuell = nil endet. Sobald u.aktuell das Ende der Liste erreicht hat, markiert die Prozedur den Knoten u um und startet einen neuen Lauf. Damit der Zeiger u.aktuell während eines Durchlaufs an einem Knoten v ∈ u.N vorbeirückt, muss die Kante (u, v) bei dem Test in Zeile 6 für unzulässig befunden werden.
768
26 Maximaler Fluss
Wenn der Durchlauf beendet ist, ist also jede aus u austretende Kante zu irgendeinem Zeitpunkt während des Durchlaufs für unzulässig befunden worden. Die wichtigste Feststellung ist, dass am Ende des Durchlaufs noch immer alle von u ausgehenden Kanten unzulässig sind. Warum? Nach Lemma 26.27 können Push-Operationen keine zulässigen Kanten erzeugen. Also muss jede zulässige Kante durch eine Relabel-Operation erzeugt worden sein. Der Knoten u wurde jedoch während des Durchlaufs nicht ummarkiert und nach Lemma 26.28 hat jeder andere Knoten v, der während des Durchlaufs ummarkiert wird, nach der Relabel-Operation keine eintretenden zulässigen Kanten. Das bedeutet, dass am Ende eines Durchlaufs alle von u ausgehenden Kanten unzulässig bleiben, womit das Lemma bewiesen ist.
Der Relabel-to-Front-Algorithmus Im Relabel-to-Front-Algorithmus verwalten wir eine verkettete Liste L, die aus allen Knoten von V − {s, t} besteht. Eine wichtige Eigenschaft ist, dass die Knoten in L entsprechend des zulässigen Netzwerks topologisch sortiert sind, wie wir an der unten angegebenen Schleifeninvariante sehen werden. (Rufen Sie sich in Erinnerung, dass das zulässige Netzwerk nach Lemma 26.26 ein gerichteter azyklischer Graph ist.) Der Pseudeocode für den Relabel-to-Front-Algorithmus setzt voraus, dass die Nachbarschaftslisten u.N für alle Knoten u bereits erzeugt sind. Weiter setzen wir voraus, dass der Zeiger u.n¨a chster auf denjenigen Knoten zeigt, der u in der Liste L folgt, und dass wie gewöhnlich u.n¨a chster = nil gilt, wenn u der letzte Knoten der Liste ist. Relabel-To-Front(G, s, t) 1 Initialize-Preflow(G, s) 2 L = G.V − {s, t}, in einer beliebigen Reihenfolge 3 for jeden Knoten u ∈ G.V − {s, t} 4 u.aktuell = u.N .kopf 5 u = L.kopf 6 while u = nil 7 alte-h¨o he = u.h 8 Discharge(u) 9 if u.h > alte-h¨o he 10 setze u an den Kopf der Liste L 11 u = u.n¨a chster Der Relabel-to-Front-Algorithmus arbeitet folgendermaßen. Zeile 1 initialisiert den Vorfluss und die Höhen mit den gleichen Werten wie im generischen Push-Label-Algorithmus. Zeile 2 initialisiert die Liste L so, dass sie in beliebiger Reihenfolge alle potentiellen Überschussknoten enthält. Die Zeilen 3–4 initialisieren für jeden Knoten u den Zeiger u.aktuell , sodass er auf den ersten Knoten von u’s Nachbarschaftsliste zeigt. Wie Abbildung 26.10 zeigt, durchläuft die while-Schleife der Zeilen 6–11 die Liste L, und entlädt die Knoten. Zeile 5 sorgt dafür, dass mit dem ersten Knoten der Liste
26.5 ∗ Der Relabel-to-Front-Algorithmus
769
begonnen wird. Bei jedem Schleifendurchlauf entlädt Zeile 8 einen Knoten u. Wurde u durch die Prozedur Discharge ummarkiert, setzt Zeile 10 ihn an den Kopf der Liste L. Wir können feststellen, ob u ummarkiert worden ist, indem wir in Zeile 9 seine Höhe vor der Discharge-Operation, die wir in Zeile 7 in die Variable alte-h¨o he speichern, mit seiner Höhe danach vergleichen. Zeile 11 sorgt dafür, dass die nächste Iteration der while-Schleife den auf u folgenden Knoten der Liste L verwendet. Hat Zeile 10 den Knoten u an den Kopf der Liste gesetzt, so ist der in der nächsten Iteration verwendete Knoten derjenige, der in der Liste hinter u folgt. Um zu zeigen, dass der Algorithmus Relabel-To-Front einen maximalen Fluss berechnet, werden wir zeigen, dass er eine Implementierung des generischen Push/RelabelAlgorithmus ist. Zunächst müssen Sie feststellen, dass er nur dann Push- und RelabelOperationen ausführt, wenn sie anwendbar sind, denn Lemma 26.29 garantiert, dass Discharge sie nur ausführt, wenn sie anwendbar sind. Es muss demnach nur noch gezeigt werden, dass bei Terminierung von Relabel-To-Front keine Grundoperationen mehr anwendbar sind. Das Argument, um dies und somit die Korrektheit des Algorithmus zu beweisen, stützt sich auf folgende Schleifeninvariante: Bei jedem Test in Zeile 6 von Relabel-To-Front ist die Liste L eine topologische Sortierung der Knoten im zulässigen Netzwerk Gf,h = (V, Ef,h ) und kein Knoten, der in der Liste vor u steht, besitzt einen Flussüberschuss.
Initialisierung: Unmittelbar nach Ausführung von Initialize-Preflow gilt s.h = |V | und v.h = 0 für alle v ∈ V − {s}. Da V zumindest s und t enthält, gilt |V | ≥ 2, und somit kann keine Kante zulässig sein. Also ist Ef,h = ∅ und jede Anordnung von V − {s, t} ist eine topologische Sortierung von Gf,h . Da der Knoten u anfangs den Kopf der Liste L darstellt, gibt es keine Knoten, die vor ihm stehen, und daher auch keine vor ihm stehenden Knoten, die einen Flussüberschuss haben. Fortsetzung: Um zu sehen, dass jede Iteration der while-Schleife die Liste L topologisch sortiert belässt, stellen wir zuerst fest, dass das zulässige Netzwerk nur durch Push- und Relabel-Operationen verändert wird. Nach Lemma 26.27 führen Pushs nicht dazu, dass Kanten zulässig werden. Also können nur Relabel-Operationen zulässige Kanten erzeugen. Nachdem ein Knoten u ummarkiert wurde, folgt jedoch aus Lemma 26.28, dass es keine in u eintretenden zulässigen Kanten gibt, aber eventuell zulässige Kanten, die von u ausgehen. Indem der Algorithmus den Knoten u an den Kopf von L setzt, stellt er also sicher, dass jede zulässige Kante, die von u ausgeht, die topologisch sortierte Reihenfolge erfüllt. Um zu sehen, dass kein Knoten, der in L vor u liegt, einen Flussüberschuss hat, bezeichnen wir den Knoten, der in der nächsten Iteration u sein wird, mit u . Zu den Knoten, die in der nächsten Iteration vor u liegen, gehört das aktuelle u (wegen Zeile 11) und entweder keine weiteren Knoten (falls u ummarkiert wurde) oder die gleichen Knoten wie zuvor (falls u nicht ummarkiert wurde). Nachdem der Knoten u entladen wurde, hat er danach keinen Flussüberschuss. Wenn also u
770
5
y 14
8
7
16
z 0
10
x s y z t
y s x z
z x y t
L: N:
x s y z t
y s x z
z x y t
L: N:
y s x z
x s y z t
z x y t
t 0
s –26 14
/14
x 0
5/5
y 19
z 0
10
t 7
5
8/8
/12
x 5
7/16 7 8
y 0
8/14
s –20
L: N: /14
12
6 5 4 3 2 1 0
14
x 12
/12 12
(c)
6 5 4 3 2 1 0
s –26
/12
(b)
6 5 4 3 2 1 0
12
(a)
26 Maximaler Fluss
7/16
7
z 8
10
t 7
Abbildung 26.10: Die Arbeitsweise von Relabel-To-Front. (a) Ein Flussnetzwerk unmittelbar vor der ersten Iteration der while-Schleife. Zu Beginn verlassen 26 Einheiten des Flusses die Quelle s. Rechts ist die initiale Liste L = x, y, z gezeigt, wobei u = x gilt. Unter jedem Knoten der Liste L steht seine Nachbarschaftsliste, in der der aktuelle Nachbar durch Schattierung gekennzeichnet ist. Der Knoten x wird entladen. Er wird auf die Höhe 1 ummarkiert, es werden 5 Einheiten des Überschusses nach y und die restlichen 7 Einheiten zur Senke t gepusht. Da der Knoten x ummarkiert wurde, wird er an den Kopf von L gesetzt, was in diesem Fall die Struktur von L nicht ändert. (b) Nach x ist y der nächste Knoten aus L, der entladen wird. Abbildung 26.9 zeigt das Entladen von y in dieser Situation detailliert. Da der Knoten y ummarkiert wird, wird er an den Kopf von L gesetzt. (c) In L folgt Knoten x nun auf y, daher wird er wieder entladen. Es werden 5 Einheiten des Überflusses nach t gepusht. Da der Knoten x während dieser discharge-Operation nicht ummarkiert wird, bleibt er in L an seiner Position.
26.5 ∗ Der Relabel-to-Front-Algorithmus
5
8/8
7
z 8
10
x s y z t
z x y t
L: N:
z x y t
y s x z
x s y z t
t 12
5
8/8
/12
x 0
y s x z
y 0
8/14
s –20
L: N:
12/16
x 0
12
(e)
6 5 4 3 2 1 0
y 0
8/14
s –20 /12 12
(d)
6 5 4 3 2 1 0
771
7
12/16
z 0
8/10
t 20
Abbildung 26.10, fortgesetzt: (d) Da Knoten z in L auf x folgt, wird er entladen. Er wird auf die Höhe 1 ummarkiert und alle 8 Einheiten des Flussüberschusses werden nach t gepusht. Da z ummarkiert wird, wird er an den Kopf der Liste L gesetzt. (e) Knoten y folgt nun in L auf z und wird daher entladen. Da y aber keinen Flussüberschuss besitzt, springt Discharge sofort zurück und y bleibt in L an seiner Position. Knoten x wird wieder entladen. Da er ebenfalls keinen Flussüberschuss mehr besitzt, kehrt Discharge wieder zurück, wobei Knoten x an seiner Position in L bleibt. Relabel-To-Front hat damit das Ende der Liste erreicht und terminiert. Es gibt keinen Überschussknoten und der Vorfluss ist ein maximaler Fluss.
772
26 Maximaler Fluss während des Entladens ummarkiert wurde, haben keine vor u liegenden Knoten einen Flussüberschuss. Wenn u während des Entladens nicht ummarkiert wurde, haben keine vor ihm liegenden Knoten während des Entladens einen Flussüberschuss erhalten, da L während der ganzen Zeit des Entladens topologisch sortiert geblieben ist (wie wir eben gezeigt haben, werden zulässige Kanten nur durch Relabel- und nicht durch Push-Operationen erzeugt). Daher bewirkt jede PushOperation, dass sich der Flussüberschuss nur zu Knoten bewegt, die weiter hinten in der Liste liegen (oder zu s oder t). Wiederum haben keine Knoten, die vor u liegen, einen Flussüberschuss.
Terminierung: Wenn die Schleife terminiert, befindet sich u ganz am Ende der Liste L, und daher sichert die Schleifeninvariante, dass der Flussüberschuss für jeden Knoten gleich 0 ist. Deshalb sind keine Grundoperationen anwendbar.
Analyse Wir werden nun zeigen, dass der Algorithmus Relabel-To-Front für jedes Flussnetzwerk G = (V, E) in Zeit O(V 3 ) läuft. Da der Algorithmus eine Implementierung des generischen Push/Relabel-Algorithmus ist, werden wir Korollar 26.21 anwenden, das eine Schranke von O(V ) für die Anzahl der pro Knoten auszuführenden Relabel-Operationen sowie eine Schranke von O(V 2 ) für die Gesamtanzahl der Relabel-Operationen liefert. Zusätzlich liefert die Übung 26.4-3 eine Schranke von O(V E) für die Gesamtlaufzeit, die für Relabel-Operationen verwendet wird. Aus Lemma 26.22 folgt eine Schranke von O(V E) für die Gesamtanzahl der sättigenden Push-Operationen. Theorem 26.30 Die Laufzeit des Relabel-To-Front-Algorithmus auf einem Flussnetzwerk G = (V, E) ist O(V 3 ). Beweis: Lassen Sie uns unter einer „Phase“ des Relabel-to-Front-Algorithmus die Zeit verstehen, die zwischen zwei aufeinanderfolgenden Relabel-Operationen liegt. Es gibt O(V 2 ) Phasen, da es O(V 2 ) Relabel-Operationen gibt. Jede Phase besteht aus höchstens |V | Aufrufen von Discharge, was wir wie folgt sehen können. Führt Discharge keine Relabel-Operation aus, erfolgt der nächste Aufruf von Discharge weiter hinten in der Liste L und die Länge von L ist kleiner als |V |. Führt Discharge eine RelabelOperation aus, gehört der nächste Aufruf von Discharge zu einer anderen Phase. Da jede Phase höchstens |V | Aufrufe von Discharge enthält und es O(V 2 ) Phasen gibt, wird Discharge in Zeile 8 von Relabel-To-Front O(V 3 )-mal aufgerufen. Daher ist die gesamte durch die while-Schleife des Relabel-To-Front-Algorithmus geleistete Arbeit ohne Berücksichtigung der in Discharge durchgeführten Arbeit höchstens O(V 3 ). Wir müssen nun noch die während der Ausführung des Algorithmus innerhalb von Discharge geleistete Arbeit abschätzen. Jede Iteration der while-Schleife in Discharge führt eine von drei Aktionen aus. Wir werden den Gesamtumfang der für jede dieser drei Aktionen aufgewendeten Arbeit analysieren.
26.5 ∗ Der Relabel-to-Front-Algorithmus
773
Wir beginnen mit den Relabel-Operationen (Zeilen 4–5). Übung 26.4-3 liefert eine Zeitschranke von O(V E) für alle der O(V 2 ) auszuführenden Relabel-Operationen. Setzen Sie nun voraus, dass Zeile 8 den Zeiger u.aktuell aktualisiert. Dies geschieht O(grad(u))-mal für jede Relabel-Operation eines Knotens u und O(V · grad(u))-mal insgesamt für alle Knoten, wobei grad(u) der Grad des Knotens u ist. Für alle Knoten ist daher der Gesamtumfang der geleisteten Arbeit für das Weitersetzen der Zeiger in den Nachbarschaftslisten in O(V E), was aus dem Handshaking-Lemma (siehe Übung B.4-1) folgt. Die dritte der durch Discharge ausgeführten Möglichkeiten ist eine Push-Operation (Zeile 7). Wir wissen bereits, dass die Gesamtanzahl der sättigenden Push-Operationen O(V E) ist. Überlegen Sie sich, dass Discharge sofort zurückspringt, wenn eine nichtsättigende Push-Operation ausgeführt wird, denn der Push reduziert den Flussüberschuss auf 0. Es kann also pro Discharge-Aufruf höchstens eine nichtsättigende PushOperation geben. Wie wir festgestellt haben, wird Discharge O(V 3 )-mal aufgerufen, sodass die Gesamtzeit, die für die Ausführung von nichtsättigenden Push-Operationen verwendet wird, O(V 3 ) ist. Die Laufzeit von Relabel-To-Front ist daher O(V 3 + V E), was gleich O(V 3 ) ist.
Übungen 26.5-1 Illustrieren Sie die Ausführung von Relabel-To-Front entsprechend Abbildung 26.10 für das Flusnetzwerk aus Abbildung 26.1(a). Setzen Sie voraus, dass die initiale Reihenfolge der Knoten in L v1 , v2 , v3 , v4 ist und dass die Nachbarschaftslisten durch v1 .N v2 .N v3 .N v4 .N
= s, v2 , v3 , = s, v1 , v3 , v4 , = v1 , v2 , v4 , t , = v2 , v3 , t
gegeben sind. 26.5-2∗ Wir wollen einen Push/Relabel-Algorithmus implementieren, in dem wir eine FIFO-Warteschlange für die Überschussknoten verwenden. Der Algorithmus entlädt sukzessive den Knoten am Kopf der Warteschlange, und alle Knoten, die vor dem Entladen keinen Flussüberschuss hatten, aber nach dem Entladen einen haben, werden an das Ende der Warteschlange platziert. Nachdem der Knoten am Kopf der Warteschlange entladen wurde, wird er entfernt. Wenn die Warteschlange leer ist, terminiert der Algorithmus. Zeigen Sie, wie wir diesen Algorithmus so implementieren können, dass er den maximalen Fluss in Zeit O(V 3 ) berechnet. 26.5-3 Zeigen Sie, dass der generische Algorithmus auch dann noch funktioniert, wenn die Prozedur Relabel die Höhe u.h aktualisiert, indem sie einfach u.h
774
26 Maximaler Fluss auf u.h + 1 setzt. Wie würde diese Änderung die Analyse des Relabel-ToFront-Algorithmus beeinflussen?
26.5-4∗ Zeigen Sie, dass, wenn wir immer einen höchsten Überschussknoten entladen, wir die Laufzeit der Push/Relabel-Methode auf O(V 3 ) bringen können. 26.5-5 Angenommen, zu einem Zeitpunkt während der Ausführung eines Push/Relabel-Algorithmus würde eine ganze Zahl 0 < k ≤ |V | − 1 existieren, für die kein Knoten die Bedingung v.h = k erfüllt. Zeigen Sie, dass alle Knoten mit v.h > k auf der Seite der Quelle eines minimalen Schnitts liegen. Wenn ein solches k existiert, aktualisiert die Lücken-Heuristik jeden Knoten v ∈ V − {s}, für den v.h > k gilt, und setzt v.h = max(v.h, |V | + 1). Zeigen Sie, dass das resultierende Attribut h eine Höhenfunktion ist. (Die Lücken-Heuristik ist zentral, um effiziente Implementierungen der Push-Relabel-Methode für die Praxis zu erhalten.)
Problemstellungen 26-1 Escape-Problem Ein n × n Gitter ist ein ungerichteter Graph, der aus n Zeilen und n Spalten von Knoten besteht (siehe Abbildung 26.11). Wir bezeichnen den Knoten in der i-ten Zeile und der j-ten Spalte mit (i, j). Alle Knoten in einem Gitter außer den Randknoten haben genau vier Nachbarn. Randknoten sind jene Punkte (i, j), für die i = 1, i = n, j = 1 oder j = n gilt. Sind in dem Gitter m ≤ n2 Startpunkte (x1 , y1 ), (x2 , y2 ), . . . , (xm , ym ) gegeben, dann besteht das Escape-Problem darin, festzustellen, ob es m knotendisjunkte Pfade von den Startpunkten zu m beliebigen, paarweise verschiedenen Randpunkten gibt. In dem Gitter aus Abbildung 26.11(a) gibt es beispielsweise eine solche Menge von Pfaden, in dem Gitter aus Abbildung 26.11(b) dagegen nicht. a. Betrachten Sie ein Flussnetzwerk, in dem es für die Knoten ebenso wie für die Kanten Kapazitäten gibt. Das heißt, der gesamte positive Fluss, der in einen gegebenen Knoten einläuft, ist einer Kapazitätsschranke unterworfen. Zeigen Sie, dass das Problem der Bestimmung eines maximalen Flusses in einem Netzwerk mit Kanten- und Knotenkapazitäten auf ein gewöhnliches maximales-Fluss-Problem in einem Netzwerk vergleichbarer Größe zurückgeführt werden kann. b. Geben Sie einen effizienten Algorithmus für die Lösung des Escape-Problems an und analysieren Sie dessen Laufzeit. 26-2 Minimale Pfadüberdeckung Eine Pfadüberdeckung eines gerichteten Graphen G = (V, E) ist eine Menge P von knotendisjunkten Pfaden mit der Eigenschaft, dass jeder Knoten aus V auf genau einem Pfad der Menge P liegt. Die Pfade können beliebige Startund Endpunkte und beliebige Längen, einschließlich die Länge 0, haben. Eine
Problemstellungen zu Kapitel 26
(a)
775
(b)
Abbildung 26.11: Gitter für das Escape-Problem. Die Startpunkte sind schwarz eingezeichnet, die übrigen Punkte weiß. (a) Ein Gitter, in dem es eine Escape-Lösung gibt. Diese ist durch die schattierten Pfade dargestellt. (b) Ein Gitter, in dem es keine Escape-Lösung gibt.
minimale Pfadüberdeckung von G ist eine Pfadüberdeckung mit minimaler Anzahl von Pfaden. a. Geben Sie einen effizienten Algorithmus an, der für einen gerichteten azyklischen Graphen G = (V, E) eine minimale Pfadüberdeckung bestimmt. (Hinweis: Konstruieren Sie unter der Voraussetzung, dass die Knotenmenge V = {1, 2, . . . , n} ist, einen Graphen G = (V , E ) mit V = {x0 , x1 , . . . , xn } ∪ {y0 , y1 , . . . , yn } , E = {(x0 , xi ) : i ∈ V } ∪ {(yi , y0 ) : i ∈ V } ∪ {(xi , yj ) : (i, j) ∈ E} und lassen Sie darauf einen Algorithmus zur Bestimmung eines maximalen Flusses laufen.) b. Arbeitet Ihr Algorithmus auch für gerichtete Graphen, die Zyklen enthalten, korrekt? Begründen Sie Ihre Antwort. 26-3 Algorithmische Beratung Professor Gore will eine Firma zur algorithmischen Beratung gründen. Er hat n wichtige Teilgebiete von Algorithmen identifiziert (die ungefähr den verschiedenen Teilen dieses Lehrbuchs entsprechen), die er als Menge A = {A1 , A2 , . . . , An } darstellt. Für jedes Teilgebiet Ak kann er einen Experten für ck Dollar einstellen. Der Beratungsfima liegt eine Menge von J = {J1 , J2 , . . . , Jm } potentieller Aufträge vor. Um Job Ji machen zu können, muss die Firma Experten zu einer Teilmenge Ri ⊆ A von Teilgebieten einstellen. Jeder Experte kann gleichzeitig an mehreren Aufträgen arbeiten. Wenn die Firma sich entscheidet, Auftrag Ji anzunehmen, der der Firma Einkünfte in Höhe von pi Dollar bringt, muss sie über Experten aus allen Teilgebieten von Ri verfügen. Professor Gores Aufgabe ist es, zu entscheiden, für welche Teilgebiete er Experten einstellen und welche Aufträge er annehmen muss, um das Nettoeinkommen der Firma zu maximieren; das Nettoeinkommen ergibt sich aus den Gesamteinkünften
776
26 Maximaler Fluss der angenommenen Aufträge abzüglich der Gesamtkosten für die Beschäftigung der Experten. Betrachten Sie das folgende Flussnetzwerk G. Es enthält eine Quelle s, Knoten A1 , A2 , . . . , An , Knoten J1 , J2 , . . . , Jm und eine Senke t. Das Flussnetzwerk enthält für jedes k = 1, 2 . . . , n eine Kante (s, Ak ) mit Kapazität c(s, Ak ) = ck und für jedes i = 1, 2, . . . , m eine Kante (Ji , t) mit Kapazität c(Ji , t) = pi . Gilt Ak ∈ Ri für k = 1, 2, . . . , n und i = 1, 2, . . . , m, dann enthält G eine Kante (Ak , Ji ) mit Kapazität c(Ak , Ji ) = ∞. a. Zeigen Sie, dass, wenn Ji ∈ T für einen Schnitt (S, T ) von G mit endlicher Kapazität, dann gilt Ak ∈ T für alle Ak ∈ Ri . b. Zeigen Sie, wie wir das maximale Nettoeinkommen aus der Kapazität eines minimalen Schnitts von G und den gegebenen pi -Werten bestimmen können. c. Geben Sie einen effizienten Algorithmus an, um zu entscheiden, welche Jobs anzunehmen und welche Experten einzustellen sind. Analysieren Sie die m Laufzeit Ihres Algorithmus als Funktion in m, n und r = i=1 |Ri |.
26-4 Aktualisierung des maximalen Flusses Sei G = (V, E) ein Flussnetzwerk mit einer Quelle s, einer Senke t und ganzzahligen Kapazitäten. Gegeben sei ein maximaler Fluss in G. a. Nehmen Sie an, wir würden die Kapazität einer einzelnen Kante (u, v) ∈ E um 1 erhöhen. Geben Sie einen Algorithmus mit Laufzeit O(V + E) an, der den maximalen Fluss aktualisiert. b. Nehmen Sie an, wir würden die Kapazität einer einzelnen Kante (u, v) ∈ E um 1 verringern. Geben Sie einen Algorithmus mit Laufzeit O(V + E) an, der den maximalen Fluss aktualisiert. 26-5 Maximaler Fluss durch Skalierung Sei G = (V, E) ein Flussnetzwerk mit Quelle s, Senke t und einer ganzzahligen Kapazität c(u, v) für jede Kante (u, v) ∈ E. Weiter sei C = max(u,v)∈E c(u, v). a. Zeigen Sie, dass ein minimaler Schnitt von G höchstens die Kapazität C |E| hat. b. Zeigen Sie, wie wir für eine gegebene Zahl K in Zeit O(E) einen Erweiterungspfad, dessen Kapazität wenigstens K ist, bestimmen können, falls ein solcher existiert. Wir können die folgende Modifikation der Prozedur Ford-Fulkerson-Method verwenden, um einen maximalen Fluss in G zu bestimmen.
Problemstellungen zu Kapitel 26
777
Max-Flow-By-Scaling(G, s, t) 1 C = max(u,v)∈E c(u, v) 2 initialisiere den Fluss f mit 0 3 K = 2lg C 4 while K ≥ 1 5 while es existiert ein Erweiterungspfad p der Kapazität größer gleich K 6 erhöhe den Fluss f entlang p 7 K = K/2 8 return f c. Zeigen Sie, dass Max-Flow-By-Scaling einen maximalen Fluss berechnet. d. Zeigen Sie, dass die Kapazität eines minimalen Schnitts des Restgraphen Gf bei jeder Ausführung der Zeile 4 höchstens 2K |E| ist. e. Zeigen Sie, dass die innere while-Schleife der Zeilen 5–6 O(E)-mal für jeden Wert von K ausgeführt wird. f. Schlussfolgern Sie, dass der Algorithmus Max-Flow-By-Scaling so implementiert werden kann, dass seine Laufzeit in O(E 2 lg C) ist. 26-6 Der Hopcroft-Karp-Algorithmus zur Berechnung bipartiter Matchings In dieser Problemstellung beschreiben wir einen schnelleren Algorithmus zur Berechnung eines maximalen Matchings in einem bipartiten Graphen, √ der auf Hopcroft und Karp zurückgeht. Der Algorithmus läuft in Zeit O( V E). Für einen ungerichteten bipartiten Graphen G = (V, E) mit einer Partitionierung V = L∪R, die die Eigenschaft hat, dass für jede Kante genau einer ihrer Endpunkte in L liegt, sei M ein Matching in G. Wir nennen einen einfachen Pfad P in G einen Erweiterungspfad bezüglich M , wenn er an einem ungebundenen Knoten aus L startet, in einem ungebundenen Knoten aus R endet und seine Kanten abwechselnd zu M und E − M gehören. (Diese Definition eines Erweiterungspfades ist mit der in einem Flussnetzwerk verwandt, aber nicht mit dieser identisch.) Bei dieser Problemstellung behandeln wir einen Pfad als Folge von Kanten anstatt als Folge von Knoten. Ein kürzester Erweiterungspfad bezüglich eines Matchings M ist ein Erweiterungspfad mit minimaler Kantenzahl. Für zwei Mengen A und B ist die symmetrische Differenz A ⊕ B durch (A − B) ∪ (B − A) definiert, d. h. sie enthält genau diejenigen Elemente, die zu genau einer der beiden Mengen gehören. a. Zeigen Sie, dass für ein Matching M und einen Erweiterungspfad P bezüglich M die symmetrische Differenz M ⊕ P ein Matching ist und dass |M ⊕ P | = |M | + 1 gilt. Zeigen Sie, dass für knotendisjunkte Erweiterungspfade P1 , P2 , . . . , Pk bezüglich M die symmetrische Differenz M ⊕ (P1 ∪ P2 ∪ · · · ∪ Pk ) ein Matching mit der Kardinalität |M | + k ist. Die allgemeine Struktur unseres Algorithmus ist die folgende:
778
26 Maximaler Fluss Hopcroft-Karp(G) 1 M =∅ 2 repeat 3 sei P = {P1 , P2 , . . . , Pk } eine maximale Menge knotendisjunkter kürzester Erweiterungspfade bzgl. M 4 M = M ⊕ (P1 ∪ P2 ∪ · · · ∪ Pk ) 5 until P = = ∅ 6 return M Im Folgenden sollen Sie die Anzahl der Iterationen in diesem Algorithmus analysieren (d. h. die Anzahl der Iterationen in der repeat-Schleife) und eine Implementierung von Zeile 3 angeben. b. Zeigen Sie für zwei gegebene Matchings M und M ∗ in G, dass jeder Knoten des Graphen G = (V, M ⊕ M ∗ ) höchstens den Grad 2 hat. Schlussfolgern Sie, dass G eine disjunkte Vereinigung von einfachen Pfaden oder Zyklen ist. Zeigen Sie, dass die Kanten in jedem solchen einfachen Pfad oder Zyklus abwechselnd zu M oder M ∗ gehören. Beweisen Sie, dass M ⊕ M ∗ im Falle |M | ≤ |M ∗ | mindestens |M ∗ | − |M | knotendisjunkte Erweiterungspfade bezüglich M enthält. Sei l die Länge eines kürzesten Erweiterungspfades bezüglich eines Matchings M , P1 , P2 , . . . , Pk eine maximale Menge von knotendisjunkten Erweiterungspfaden der Länge l bezüglich M und M = M ⊕ (P1 ∪ · · · ∪ Pk ) und setzen Sie voraus, dass P ein kürzester Erweiterungspfad bezüglich M ist. c. Zeigen Sie, dass der Pfad P mehr als l Kanten hat, wenn er von der Menge P1 , P2 , . . . , Pk knotendisjunkt ist. d. Nehmen Sie nun an, P wäre nicht knotendisjunkt zu P1 , P2 , . . . , Pk . Sei A die Menge der Kanten (M ⊕M )⊕P . Zeigen Sie, dass A = (P1 ∪P2 ∪· · ·∪Pk )⊕P und |A| ≥ (k + 1) l gilt. Folgern Sie hieraus, dass P mehr als l Kanten hat. e. Beweisen Sie, dass die Größe eines maximalen Matchings höchstens |M | + |V | /(l + 1) ist, falls ein kürzester Erweiterungspfad bezüglich M aus l Kanten besteht. f. Zeigen Sie, dass die Anzahl der Iterationen in der repeat-Schleife des Al gorithmus höchstens 2 |V | ist. (Hinweis: Um wie viel kann M nach |V | Iterationen gewachsen sein?) g. Geben Sie einen Algorithmus an, der für ein gegebenes Matching M in Zeit O(E) eine maximale Menge knotendisjunkter Erweiterungspfade P1 , P2 , . . . , Pk bestimmt. Schlussfolgern √ Sie, dass die Gesamtlaufzeit des Algorithmus Hopcroft-Karp in O( V E) liegt.
Kapitelbemerkungen Ahuja, Magnanti und Orlin [7], Even [103], Lawler [224], Papadimitriou und Steiglitz [271] sowie Tarjan [330] sind gute Referenzen für Flussnetzwerke und verwandte Al-
Kapitelbemerkungen zu Kapitel 26
779
gorithmen. Goldberg, Tardos und Tarjan [139] liefern einen schönen Überblick über Probleme im Zusammenhang mit Flussnetzwerken. Schrijver [304] hat eine interessante Zusammenfassung der historischen Entwicklungen auf dem Gebiet der Flussnetzwerke geschrieben. Die Ford-Fulkerson-Methode geht auf Ford und Fulkerson [109] zurück, die die formale Untersuchung vieler Probleme auf dem Gebiet der Flussnetzwerke einschließlich der Probleme der Berechnung eines maximalen Flusses und der Berechnung eines bipartiten Matchings initiierten. Viele frühe Implementierungen der Ford-Fulkerson-Methode bestimmten Erweiterungspfade mithilfe der Breitensuche. Edmonds und Karp [102], und unabhängig von ihnen Dinic [89], bewiesen, dass diese Strategie zu Algorithmen mit polynomieller Laufzeit führen. Eine verwandte Idee, die Verwendung von „Sperrflüssen“, wurde ebenfalls zuerst von Dinic [89] entwickelt. Karzanov [202] entwickelte als erster die Idee des Vorflusses. Die Push/Relabel-Methode geht auf Goldberg [136] und Goldberg und Tarjan [140] zurück. Goldberg und Tarjan haben einen Algorithmus mit Laufzeit O(V 3 ) angegeben, der eine Warteschlange verwendet, um die Menge der Überschussknoten zu verwalten. Außerdem haben die Autoren einen Algorithmus entwickelt, der dynamische Bäume verwendet, um eine Laufzeit von O(V E lg(V 2 /E + 2)) zu erreichen. Verschiedene andere Forscher haben Push/Relabel-Algorithmen zur Berechnung eines maximalen Flusses entwickelt. Ahuja und Orlin [9] sowie Ahuja, Orlin und Tarjan [10] haben Algorithmen angegeben, die mit Skalierung arbeiten. Cheriyan und Maheshwari [62] haben vorgeschlagen, den Fluss vom Überschussknoten maximaler Höhe zu pushen. Von Cheriyan und Hagerup [61] stammt die Idee, die Nachbarschaftslisten zufällig zu permutieren, und verschiedene Forscher [14, 204, 276] entwickelten daraus eine Reihe von schnelleren Algorithmen. Der Algorithmus von King, Rao und Tarjan [204] ist der schnellste dieser Algorithmen. Er läuft in Zeit O(V E logE/(V lg V ) V ). Der derzeit schnellste asymptotische Algorithmus für das maximale-Fluss-Problem geht auf Goldberg und Rao [138] zurück und läuft in Zeit O(min(V 2/3 , E 1/2 ) E lg(V 2 /E + 2) lg C) mit C = max(u,v)∈E c(u, v). Dieser Algorithmus verwendet nicht die Push/Relabel-Methode, sondern beruht auf der Bestimmung von Sperrflüssen. Die zuvor genannten Algorithmen für das maximale-Fluss-Problem, einschließlich der in diesem Kapitel vorgestellten, verwenden einen Distanzbegriff (die Push/Relabel-Methode verwendet den analogen Begriff der Höhe), wobei jeder Kante implizit die Länge 1 zugeordnet ist. Dieser neue Algorithmus verfolgt einen anderen Ansatz und ordnet den Kanten hoher Kapazität die Länge 0 und den Kanten niedriger Kapazität die Länge 1 zu. Bezüglich dieser Längen haben kürzeste Pfade von der Quelle zur Senke tendenziell eine hohe Kapazität, was bedeutet, dass weniger Iterationen ausgeführt werden müssen. Beim maximalen-Fluss-Problem schlagen in der Praxis derzeit die Push/Relabel-Algorithmen die Algorithmen, die auf Erweiterungspfaden oder linearen Programmen basieren. Eine Untersuchung von Cherkassky und Goldberg [63] unterstreicht die Wichtigkeit der Verwendung von zwei Heuristiken bei der Implementierung eines Push/RelabelAlgorithmus. Die erste Heuristik besteht darin, im Restnetzwerk periodisch eine Breitensuche durchzuführen, um genauere Werte für die Höhen zu erhalten. Die zweite Heuristik ist die Lücken-Heuristik, die in Übung 26.5-5 beschrieben wird. Sie leiten daraus ab, dass die bestmögliche der verschiedenen Push/Relabel-Varianten diejenige ist, die den Überschussknoten mit der maximalen Höhe für das Entladen auswählt.
780
26 Maximaler Fluss
Der beste derzeit bekannte Algorithmus für die Bestimmung eines maximalen bipartiten √ Matchings wurde von Hopcroft und Karp [176] entwickelt und läuft in Zeit O( V E) (siehe Problemstellung 26-6). Das Buch von Lovász und Plummer [239] ist eine exzellente Referenz für das Matching-Problem.
Teil VII
Ausgewählte Themen
Einführung Dieser Teil enthält eine Auswahl von Themen zu Algorithmen, die den vorangegangenen Stoff dieses Buches erweitern und vervollständigen. Einige Kapitel führen neue Rechenmodelle ein, wie beispielsweise Schaltkreise und Parallelrechner. Andere behandeln spezielle Gebiete der Mathematik, wie beispielsweise die algorithmische Geometrie und die Zahlentheorie. Die letzten beiden Kapitel diskutieren einige bekannte Grenzen beim Entwurf effizienter Algorithmen und führen Methoden ein, um mit diesen Grenzen umzugehen. Kapitel 27 stellt ein algorithmisches Modell für paralleles Rechnen vor, das auf dynamischer Nebenläufigkeit, so genannter Mehrfädigkeit (engl.: multithreading) basiert. Das Kapitel führt in die Grundlagen des Modells ein und zeigt dabei, wie wir Parallelität als Funktion in Arbeit und Laufzeit messen können. Es untersucht dann einige interessante mehrfädige Algorithmen, einschließlich Algorithmen zur Matrizenmultipikation und zum Sortieren durch Mischen. Kapitel 28 betrachtet effiziente Algorithmen, die auf Matrizen arbeiten. Es stellt zwei weit verbreitete Methoden – LU-Zerlegung und LUP-Zerlegung – vor, die lineare Gleichungen mithilfe des Gaußschen Eliminationsverfahrens in Zeit O(n3 ) lösen. Es zeigt auch, dass Matrixinversion und Matrizenmultiplikation gleiche Laufzeiten haben. Das Kapitel zeigt schlussendlich noch, wie wir eine Approximation, die die Summe der quadratischen Fehler minimiert, berechnen können, wenn ein lineares Gleichungssystem keine exakte Lösung besitzt. Kapitel 29 beschäftigt sich mit linearer Programmierung, bei der wir eine Zielfunktion unter gegebenen beschränkten Ressourcen und konkurrierenden Nebenbedingungen maximieren oder minimieren wollen. Lineare Programmierung hat eine Vielzahl von praktischen Anwendungen. Dieses Kapitel behandelt die Formulierung und die Lösung linearer Programme. Die behandelte Lösungsmethode ist der Simplex-Algorithmus, bei dem es sich um den ältesten Algorithmus der linearen Programmierung handelt. Im Unterschied zu vielen Algorithmen in diesem Buch läuft der Simplex-Algorithmus im schlechtesten Fall nicht in polynomieller Zeit. Er ist dennoch ein recht effizienter und in der Praxis weit verbreiteter Algorithmus. Kapitel 30 untersucht Operationen auf Polynomen und zeigt, wie wir eine wohlbekannte signalverarbeitende Technik – die schnelle Fourier-Tranformation (engl.: Fast Fourier Transform, FFT) – verwenden können, um zwei Polynome vom Grad n in Zeit O(n lg n) miteinander zu multiplizieren. Außerdem stellt das Kapitel effiziente Implementierungen der FFT vor, einschließlich einer parallelen Schaltung für FFT. Kapitel 31 behandelt zahlentheoretische Algorithmen. Nach einem Überblick über die elementare Zahlentheorie stellt es Euklids Algorithmus zur Berechnung des größten gemeinsamen Teilers vor. Anschließend widmet es sich Algorithmen zur Lösung mo-
784
Teil VII Ausgewählte Themen
dularer linearer Gleichungen und dem Potenzieren einer Zahl modulo einer zweiten Zahl. Dann betrachtet das Kapitel eine wichtige Anwendung zahlentheoretischer Algorithmen: das RSA-Verschlüsselungssystem. Dieses Verschlüsselungssystem kann nicht nur dazu verwendet werden, Nachrichten so zu verschlüsseln, dass sie ein Gegner nicht lesen kann, sondern auch dazu, digitale Signaturen zur Verfügung zu stellen. Das Kapitel stellt anschließend den randomisierten Miller-Rabin-Primzahltest vor, mit dem wir große Primzahlen effizient bestimmen können – eine wesentliche Voraussetzung für das RSA-System. Schließlich behandelt das Kapitel die ρ-Heuristik von Pollard zum Faktorisieren ganzer Zahlen und diskutiert den aktuellen Wissensstand auf dem Gebiet der Faktorisierung ganzer Zahlen. Kapitel 32 untersucht das Problem, alle Stellen zu finden, an denen ein gegebenes Textmuster in einer gegebenen Textfolge vorkommt, ein Problem, das bei textverarbeitenden Programmen häufig auftritt. Nach der Untersuchung der naiven Herangehensweise stellt das Kapitel eine elegante Methode vor, die auf Rabin und Karp zurückgeht. Nach der Vorstellung einer auf endlichen Automaten beruhenden effizienten Lösung präsentiert das Kapitel den Knuth-Morris-Pratt-Algorithmus, der den Automaten-basierten Algorithmus durch eine geschickte Vorverarbeitung des Musters modifiziert, um Speicherplatz zu sparen. Kapitel 33 behandelt einige Probleme aus der algorithmischen Geometrie. Nach der Diskussion der Grundlagen der algorithmischen Geometrie zeigt das Kapitel, wie wir mit einer „Abtastmethode“ feststellen können, ob eine Menge von Strecken Schnittpunkte hat. Zwei ausgeklügelte Algorithmen zur Bestimmung der konvexen Hülle einer Punktmenge – der Graham-Scan-Algorithmus und der Jarvis-March-Algorithmus – veranschaulichen ebenfalls die Leistungsfähigkeit von solchen „Abtastmethoden“. Das Kapitel schließt mit einem effizienten Algorithmus zur Bestimmung eines am dichtesten beieinander liegenden Knotenpaares innerhalb einer gegebenen Punktmenge in der Ebene. Kapitel 34 beschäftigt sich mit NP-vollständigen Problemen. Viele interessante Entscheidungsprobleme sind NP-vollständig; es ist kein Algorithmus mit polynomieller Laufzeit für sie bekannt. Dieses Kapitel stellt Methoden vor, mit denen wir feststellen können, ob ein Problem NP-vollständig ist. Einige klassische Probleme werden als NP-vollständig bewiesen: die Entscheidung, ob ein Graph einen Hamiltonischen Kreis enthält, die Entscheidung, ob eine Boolesche Formel erfüllbar ist, und die Entscheidung, ob eine gegebene Menge von Zahlen eine Teilmenge besitzt, deren Elemente sich zu einem gegebenen Zielwert aufaddieren. Das Kapitel beweist auch, dass das bekannte Problem des Handelsreisenden NP-vollständig ist. Kapitel 35 zeigt, wie wir Approximationsalgorithmen einsetzen können, um auf effiziente Weise Näherungslösungen für NP-vollständige Probleme zu erhalten. Bei einigen NP-vollständigen Problemen sind nahezu optimale Näherungslösungen ziemlich einfach zu berechnen, für andere aber arbeiten auch die besten bekannten Approximationsalgorithmen bei steigender Problemgröße zunehmend schlechter. Schließlich gibt es Probleme, bei denen wir durch Investition von mehr Rechenzeit zunehmend bessere Näherungslösungen erhalten können. Dieses Kapitel erläutert diese Möglichkeiten anhand des Knotenüberdeckungsproblems in ungewichteter und gewichteter Version, einer „optimierenden“ Version der 3-CNF-Erfüllbarkeit, des Problems des Handelsreisenden, des Mengenüberdeckungsproblems und des Teilsummenproblems.
27
Mehrfädige Algorithmen
Fast alle Algorithmen in diesem Buch sind serielle Algorithmen, die für EinprozessorRechner geeignet sind, in dem zu jedem Zeitpunkt immer nur ein Maschinenbefehl ausgeführt werden kann. In diesem Kapitel erweitern wir unser algorithmisches Modell, damit es auch parallele Algorithmen abdeckt, die auf einem Multiprozessor-Rechner ausgeführt werden können, der es erlaubt, dass mehrere Maschinenbefehle gleichzeitig abgearbeitet werden können. Speziell werden wir uns ein elegantes Modell für dynamische mehrfädige Algorithmen anschauen, das sowohl einem algorithmischen Entwurf und einer Analyse als auch einer effizienten Implementierung in der Praxis zugänglich ist. Parallelrechner – Rechner mit mehreren Prozessoreinheiten – werden mehr und mehr gebräuchlich und sind in unterschiedlichen Preislagen und Performanz zu haben. Relativ kostengünstige, in Arbeitsplatzrechnern und Laptops vorzufindenden integrierte Multiprozessoren, so genannte multiprocessors on a chip, bestehen aus einem einzigen integrierten Mehrkern-Chip, der mehrere Rechnerkerne beherbergt; jeder dieser Kerne ist ein vollwertiger Prozessor, der auf den gemeinsamen Hauptspeicher zugreifen kann. In der mittleren Preis/Leistungskategorie finden wir Cluster, die aus vielen einzelnen Rechnern zusammengebaut sind – oft handelt es sich hierbei um einfache PCs –, die über ein festes Netzwerk untereinander verbunden sind. In der oberen Preisklasse haben wir Hochleistungsrechner, so genannte Supercomputer, bei denen wir oft eine Kombination maßgeschneiderter Architekturen und Netzwerken vorfinden, um maximale Performanz in Bezug auf die pro Sekunde ausführbaren Maschinenbefehle zu erreichen. Multiprozessoren existieren in der einen oder anderen Form bereits vielerorts seit Jahrzehnten. Obwohl sich die Fachleute in Bezug auf das serielle Rechnen sehr früh auf das Maschinenmodell mit wahlfreiem Zugriff verständigt haben, gibt es kein einziges Modell für paralleles Rechnen, welches breite Akzeptanz besitzt. Einer der Hauptgründe besteht darin, dass die Anbieter sich nicht auf ein gemeinsames Architekturmodell für Parallelrechner geeinigt haben. Beispielweise sind einige Rechner mit gemeinsamem Speicher ausgestattet, bei dem jeder Prozessor direkt auf jede beliebige Adresse des Speichers zugreifen kann. Andere Parallelrechner arbeiten mit verteiltem Speicher, bei dem jeder Prozessor einen privaten Speicher besitzt und eine explizite Nachricht zwischen Prozessoren gesendet werden muss, damit ein Prozessor auf den Speicher eines anderen Prozessors zugreifen kann. Mit dem Aufkommen der Mehrkern-Technologie ist nun jeder neue Laptop und jeder neue Arbeitsplatzrechner ein Parallelrechner mit gemeinsamem Speicher und der Trend scheint auch weiter in diese Richtung zu gehen. Wenngleich uns die Zeit eines besseren belehren könnte, ist dies der Ansatz, den wir in diesem Kapitel verfolgen werden. Ein gebräuchlicher Weg, Multiprozessoren oder andere Parallelrechner mit gemeinsamen
786
27 Mehrfädige Algorithmen
Speicher zu programmieren, besteht in der Benutzung statischer Nebenläufigkeit, die eine Software-Abstraktion „virtueller Prozessoren“ darstellt, oder Fäden, die sich einen gemeinsamen Speicher teilen – wir werden im Folgenden mitunter das englische Wort „Thread “ für „Faden“ benutzen; dieser Begriff hat sich unter deutschen Fachleuten eingebürgert, während der deutsche Begriff kaum benutzt wird. Jedem Thread ist ein eigener Befehlszähler zugeordnet und kann Maschinencode unabhängig von anderen Threads ausführen. Das Betriebssystem lädt einen Thread zu seiner Ausführung in einen Prozessor und verdrängt ihn, wenn ein anderer Thread zur Ausführung kommen muss. Wenngleich das Betriebssystem es den Programmierern erlaubt, Threads zu erzeugen und zu löschen, so sind diese beiden Operationen doch eher selten. Bei den meisten Anwendungen bestehen die Threads über die gesamte Zeit der Berechnung; dies ist der Grund, warum wir sie „statisch“ nennen. Leider ist es recht schwer und fehleranfällig, einen Parallelrechner mit gemeinsamem Speicher unter Benutzung statischer Threads direkt zu programmieren. Einer der Gründe besteht darin, dass das dynamische Aufteilen der Arbeit zwischen den Threads, sodass alle Threads ungefähr die gleiche Arbeitslast erhalten, ein kompliziertes Unterfangen ist. Bei allen Anwendungen, bis auf die wirklich einfachsten, muss der Programmierer komplexe Kommunikationsprotokolle benutzen, um einen Planer zur Lastverteilung zu implementieren. Aus dieser Sachlage heraus wurden spezielle Plattformen für Nebenläufigkeit, so genannte concurrency platforms, gebaut, die eine Softwareschicht zur Verfügung stellen, um die Ressourcen beim parallelen Rechnen zu koordinieren, zu planen und zu verwalten. Andere Plattformen stellen Laufzeitbibliotheken dar oder stellen sogar vollwertige parallele Programmiersprachen mit Übersetzer und Laufzeitsystem zur Verfügung.
Dynamische Programmierung mit Threads Eine wichtige Klasse solcher Plattformen für Nebenläufigkeit ist dynamisches Multithreading 1 ; dies ist das Modell, welches wir in diesem Kapitel benutzen werden. Dynamisches Multithreading erlaubt Programmierern, in Anwendungen Parallelität zu spezifizieren, ohne sich um Kommunikationsprotokolle, Lastverteilung und andere Unwägbarkeiten, die beim Programmieren mit statischen Threads, auftreten können, Gedanken machen zu müssen. Die Plattform enthält einen Planer oder Scheduler, der die Lastverteilung automatisch durchführt und somit die Arbeit des Programmierers stark vereinfacht. Wenngleich sich die Funktionalitäten der dynamischen-MultithreadingUmgebungen kontinuierlich und zum Teil auch unterschiedlich entwickeln, unterstützen fast alle zwei Funktionen: geschachtelte Parallelität und parallele Schleifen. Geschachtelte Parallelität erlaubt, dass eine Unterroutine „erzeugt“ (engl.: „spawned “) werden kann, sodass die aufrufende Routine weiterarbeiten kann, während die erzeugte Unterroutune ihr Ergebnis berechnet. Eine parallele Schleife sieht einer normalen for-Schleife ähnlich, mit dem Unterschied, dass die Iterationen der Schleife gleichzeitig ausgeführt werden können. Diese zwei Eigenschaften stellen die Basis des Modells für dynamisches Multithreading 1 Auch hier verzichten wir auf eine deutsche Übersetzung, die in etwa „Programmieren unter Verwendung dynamischer Fäden“ lauten würde.
27.1 Grundlagen von dynamischem Multithreading
787
dar, das wir in diesem Kapitel untersuchen werden. Ein Hauptaspekt dieses Modells besteht darin, dass die Programmierer nur die logische Parallelität innerhalb einer Berechnung zu spezifizieren haben; die Threads innerhalb der darunterliegenden Plattform planen und regeln die Lastbalancierung der Berechnungen untereinander. Wir werden sowohl mehrfädige, für dieses Modell geschriebene Algorithmen untersuchen als auch angeben, wie die darunterliegende Plattform die Berechnungen effizient planen kann. Unser Modell für dynamisches Multithreading bietet mehrere wichtige Vorteile: • Es handelt sich um eine einfache Erweiterung unseres seriellen Programmiermodells. Wir können mehrfädige Algorithmen beschreiben, indem wir unserem Pseudocode nur drei „parallele“ Schlüsselwörter hinzufügen: parallel, spawn und sync. Mehr noch, wenn wir diese parallelen Schlüsselwörter aus einem mehrfädigen Pseudocode streichen, ist der resultierende Text ein serieller Pseudocode für das gleiche Problem, den wir als „Serialisierung“ des mehrfädigen Algorithmus bezeichnen. • Es stellt eine saubere theoretische Methode zur Verfügung, um auf Basis der Begriffe „Arbeit“ und „Zeitspanne“ Parallelität zu quantifizieren. • Viele mehrfädige Algorithmen, die mit geschachtelter Parallelität arbeiten, folgen in natürlicher Art und Weise dem Teile-und-Beherrsche-Paradigma. Mehr noch, genau wie serielle Teile-und-Beherrsche-Algorithmen bieten sich auch mehrfädige Algorithmen an, dass wir sie mittels Rekursionsgleichungen analysieren. • Das Modell lehnt sich an die Art und Weise an, wie paralleles Rechnen sich in der Praxis entwickelt. Eine wachsende Zahl von Plattformen für Nebenläufigkeit unterstützen in der einen oder anderen Art und Weise dynamisches Multithreading, einschließlich Cilk [51, 118], Cilk++ [71], OpenMP [59], Task Parallel Library [230] und Threading Building Blocks [292]. Abschnitt 27.1 führt in das Modell des dynamischen Multithreading ein und stellt die Maße Arbeit, Zeitspanne und Parallelät vor, die wir benutzen werden, um mehrfädige Algorithmen zu untersuchen. Abschnitt 27.2 untersucht, wie wir Matrizen mit Multithreading multiplizieren können, und Abschnitt 27.3 geht das schwierigere Problem des mehrfädigen Sortieren durch Mischen an.
27.1
Grundlagen von dynamischem Multithreading
Wir werden mit den Überlegungen zu dynamischem Multithreading beginnen, in dem wir uns die rekursive Berechnung von Fibonacci-Zahlen anschauen. Rufen Sie sich in Erinnerung, dass die Fibonacci-Zahlen durch die Rekursion (3.22) definiert sind: F0 = 0 , F1 = 1 , Fi = Fi−1 + Fi−2
für i ≥ 2 .
788
27 Mehrfädige Algorithmen Fib(6)
Fib(5)
Fib(4)
Fib(4)
Fib(3)
Fib(2)
Fib(1)
Fib(1)
Fib(3)
Fib(2)
Fib(1)
Fib(0)
Fib(2)
Fib(1)
Fib(1)
Fib(0)
Fib(3)
Fib(2)
Fib(1)
Fib(1)
Fib(2)
Fib(1)
Fib(0)
Fib(0)
Fib(0)
Abbildung 27.1: Der Baum der rekursiven Prozedurinstanzen, die während der Berechnung von Fib(6) erzeugt werden. Jede Instanz von Fib mit dem gleichen Argument macht die gleiche Arbeit, um das gleiche Ergebnis zu erzeugen – eine ineffiziente, wenn auch interessante Methode, Fibonacci-Zahlen zu berechnen.
Ein einfacher rekursiver serieller Algorithmus zur Berechnung der n-ten Fibonacci-Zahl sieht wie folgt aus: Fib(n) 1 if n ≤ 1 2 return n 3 else x = Fib(n − 1) 4 y = Fib(n − 2) 5 return x + y Sie werden in Wirklichkeit mit diesem Algorithmus keine großen Fibonacci-Zahlen berechnen wollen, da diese Berechnung viel Arbeit mehrfach ausführt. Abbildung 27.1 zeigt den zu den rekursiven Prozedurinstanzen gehörigen Baum, die bei der Berechnung von F6 erzeugt werden. So ruft ein Aufruf von Fib(6) rekursiv Fib(5) und dann Fib(4) auf. Der Aufruf von Fib(5) hat aber auch einen Aufruf von Fib(4) zur Folge. Beide Instanzen von Fib(4) geben das gleiche Ergebnis (F4 = 3) zurück. Da die Prozedur Fib nicht memoisiert, führt der zweite Aufruf von Fib(4) die gleiche Arbeit aus, die der erste Aufruf schon gemacht hat. Lassen Sie uns die Laufzeit von Fib(n) mit T (n) bezeichnen. Da Fib(n) neben konstant viel Arbeit zwei rekursive Aufrufe enthält, erhalten wir die Rekursionsgleichung T (n) = T (n − 1) + T (n − 2) + Θ(1) . Diese Rekursionsgleichung hat T (n) = Θ(Fn ) als Lösung, was wir mit der Substitutionsmethode zeigen können. Setzen Sie für die Induktionsannahme voraus, dass
27.1 Grundlagen von dynamischem Multithreading
789
T (n) ≤ aFn − b gilt, wobei a > 1 und b > 0 Konstanten sind. Mittels Substitution erhalten wir T (n) ≤ (aFn−1 − b) + (aFn−2 − b) + Θ(1) = a(Fn−1 + Fn−2 ) − 2b + Θ(1) = aFn − b − (b − Θ(1)) ≤ aFn − b , wenn wir b ausreichend groß wählen, dass sie die Konstante in dem Θ(1)-Term dominiert. Wir können dann a so groß wählen, dass die initiale Bedingung erfüllt ist. Mit Gleichung (3.25) folgt dann die analytische Schranke T (n) = Θ(φn ) , (27.1) √ wobei φ = (1+ 5)/2 den Goldenen Schnitt bezeichnet. Da Fn exponentiell in n wächst, ist diese Prozedur ziemlich langsam, um Fibonacci-Zahlen zu berechnen. (Siehe Problemstellung 31-3 für viel schnellere Methoden.) Wenngleich die Prozedur Fib eine schlechte Methode ist, um Fibonacci-Zahlen zu berechnen, ist sie ein gutes Beispiel, um die Hauptkonzepte bei der Analyse von mehrfädigen Algorithmen zu illustrieren. Beachten Sie, dass innerhalb von Fib(n) die zwei rekursiven Aufrufe in den Zeilen 3 und 4, nämlich der Aufruf Fib(n − 1) und der Aufruf Fib(n − 2), voneinander unabhängig sind: sie könnten in einer beliebigen Reihenfolge ausgeführt werden und die Berechnung durch den einen berührt die Berechnung des anderen in keinster Weise. Die beiden rekursiven Aufrufe können so parallel zueinander ausgeführt werden. Wir erweitern unseren Pseudocode um die Möglichkeit, Parallelität anzugeben, indem wir die Schlüsselwörter für Parallelität spawn und sync zur Verfügung stellen. Wir können die Prozedur Fib dann wie folgt umschreiben, um dynamisches Multithreading zu verwenden: P-Fib(n) 1 if n ≤ 1 2 return n 3 else x = spawn P-Fib(n − 1) 4 y = P-Fib(n − 2) 5 sync 6 return x + y Beachten Sie, dass, wenn wir die Schlüsselwörter spawn and sync in P-Fib streichen würden, der resultierende Pseudocode-Text identisch mit dem von Fib ist (mal davon abgesehen, dass wir den Namen im Prozedurkopf und in den beiden rekursiven Aufrufen ändern müssten). Wir definieren die Serialisierung eines mehrfädigen Algorithmus als den seriellen Algorithmus, der entsteht, wenn wir in dem mehrfädigen Programm die Schlüsselwörter spawn, sync und parallel, dem wir begegnen werden, wenn wir
790
27 Mehrfädige Algorithmen
uns parallele Schleifen anschauen werden, streichen. In der Tat, unser Pseudocode für mehrfädige Algorithmen haben die nette Eigenschaft, dass eine Serialisierung immer ein normaler serieller Pseudocode ist, der das gleiche Problem löst. Geschachtelte Parallelität erfolgt, wenn, wie in Zeile 3, das Schlüsselwort spawn einem Prozeduraufruf vorausgeht. Die Semantik eines Spawns unterscheidet sich in dem Sinne von einem normalen Prozeduraufruf, dass die Prozedurinstanz, die das Spawn ausführt – der Vater – parallel zu der erzeugten Unterroutine – seinem Kind – weiterarbeiten kann, anstatt zu warten, bis das Kind mit seiner Arbeit fertig ist, wie dies normalerweise in einer seriellen Berechnung geschieht. In diesem Fall kann der Vater parallel zum erzeugten Kind, das P-Fib(n − 1) berechnet, P-Fib(n − 2) in Zeile 4 berechnen. Da die Prozedur P-Fib rekursiv ist, erzeugen diese zwei Unterroutinen, wie auch deren Kinder, selbst wieder geschachtelte Parallelität, sodass ein potentiell sehr großer Baum von Teilberechnungen entsteht, die alle parallel ausgeführt werden können. Das Schlüsselwort spawn besagt jedoch nicht, dass eine Prozedur parallel zu seinen erzeugten Kindern ausgeführt werden muss, sondern nur dass sie parallel zu seinen Kindern ausgeführt werden darf. Die Schlüsselwörter für Parallelität beschreiben die logische Parallelität einer Berechnung; sie geben an, welche Teile der Berechnung parallel abgearbeitet werden können. Zur Laufzeit ist es Aufgabe des Scheduler, festzulegen, welche Teilberechnungen in Wirklichkeit parallel ablaufen sollen, indem er ihnen während der Berechnung verfügbare Prozessoren zuordnet. Wir werden die Ideen, die hinter solchen Schedulern stehen, kurz diskutieren. Eine Prozedur kann die durch seine erzeugten Kindern zurückgegebenen Werte nicht sicher entgegennehmen, bis sie, wie in Zeile 5, eine sync-Anweisung ausführt. Das Schlüsselwort sync gibt an, dass die Prozedur warten muss, bis alle seine erzeugten Kinder mit ihrer Arbeit fertig sind, bevor sie die hinter der sync-Anweisung stehende Anweisung ausführen darf. In der Prozedur P-Fib wird eine sync-Anweisung vor der return-Anweisung in Zeile 6 benötigt, um zu vermeiden, dass x und y aufaddiert werden, bevor x berechnet ist. Neben der expliziten Synchronisierung, die durch die sync-Anweisung zur Verfügung gestellt wird, führt jede Prozedur ein implizites sync aus, bevor sie zurückkehrt, um so abzusichern, dass all seine Kinder mit ihrer Arbeit fertig sind.
Ein Modell für mehrfädige Ausführung Es hilft, sich eine mehrfädige Berechnung – die Menge der Anweisungen, die während der Laufzeit durch einen Prozessor im Rahmen eines mehrfädigen Programms ausgeführt werden – als einen gerichteten azyklischen Graphen G = (V, E) vorzustellen, den wir gerichteter azyklischer Berechnungsgraph nennen. Beispielsweise zeigt Abbildung 27.2 den gerichteten azyklischen Berechnungsgraphen, der aus der Berechnung von P-Fib(4) entsteht. Formal sind die Knoten aus V Anweisungen und die Kanten aus E stellen Abhängigkeiten zwischen Anweisungen dar, wobei eine Kante (u, v) ∈ E bedeutet, dass die Anweisung u vor der Anweisung v ausgeführt werden muss. Der Einfachheit halber gruppieren wir jedoch Anweisungen in einen Berechnungsfaden – jeder von denen steht für eine oder mehrere Anweisungen –, wenn eine Folge von Anweisungen keine Kontrollanweisungen zu Nebenläufigkeiten – weder ein spawn noch ein sync
27.1 Grundlagen von dynamischem Multithreading
791
P-FIB(4)
P-FIB(2)
P-FIB(3)
P-FIB(2)
P-FIB(1)
P-FIB(1)
P-FIB(1)
P-FIB(0)
P-FIB(0)
Abbildung 27.2: Ein gerichteter azyklischer Graph, der die Berechnung von P-Fib(4) darstellt. Jeder Kreis steht für einen Berechnungsfaden, wobei ein schwarzer Kreis entweder einen Basisfall oder den Teil der Prozedur(instanz) bis zum Spawn von P-Fib(n − 1) in Zeile 3 darstellt. Schattierte Kreise stellen den Teil der Prozedur dar, der P-Fib(n − 2) in Zeile 4 aufruft bis zur sync-Anweisung in Zeile 5, wo die Prozedur wartet bis das erzeugte Kind das Ergebnis von P-Fib(n − 1) fertig berechnet hat, und weiße Kreise den Teil der Prozedur, der hinter der sync-Anweisung kommt, in dem x und y aufaddiert werden und dieses Ergebnis zurückgegeben wird. Die Berechnungsfäden, die zur gleichen Prozedurinstanz gehören, sind jeweils durch ein abgerundetes Rechteck zusammengefasst, wobei erzeugte Prozeduren hell schattiert und (normal) aufgerufene Prozeduren dunkel schattiert gekennzeichnet sind. Kanten, die für erzeugte oder normale Aufrufe stehen, zeigen nach unten. Die Kanten, die für die Fortführung der Berechnung stehen, zeigen horizontal nach rechts und Kanten, die für das Zurückkehren aus einer Prozedurinstanz stehen, zeigen nach oben. Wenn wir voraussetzen, dass jeder Berechnungsfaden eine Zeiteinheit benötigt, braucht die hier gezeigte Arbeit zusammen 17 Zeiteinheiten, da es 17 Berechnungsfäden gibt; diese Arbeit kann in einer Zeitspanne von 8 Zeiteinheiten ausgeführt werden, da der kritische Pfad, der durch schattierte Kanten angegeben ist, aus 8 Berechnungsfäden besteht.
noch ein return aus einem Spawn (entweder durch eine explizite return-Anweisung oder durch eine Rückkehr, die implizit erfolgt, wenn das Ende einer Prozedur erreicht wird) – enthält. Kontrollanweisungen zu Nebenläufigkeiten sind nicht in Fäden enthalten, sondern werden in der Struktur des gerichteten azyklischen Graphen dargestellt. Hat beispielsweise ein Berechnungsfaden zwei Nachfolger, dann muss einer von ihnen mit einer spawn-Anweisung erzeugt worden sein, und ein Berechnungsfaden mit mehreren Vorgängern gibt an, dass die Vorgänger über eine sync-Anweisung verbunden sind. Im allgemeinen Fall besteht die Menge V aus der Menge der Berechnungsfäden und die Menge E aus gerichteten Kanten, die die durch Kontrollanweisungen zu Nebenläufigkeiten induzierten Abhängigkeiten zwischen Berechnungsfäden darstellen. Wenn G einen gerichteten Pfad vom Berechnungsfaden u zum Berechnungsfaden v enthält, so sind die zwei Berechnungsfäden (logisch) in Reihe. Anderenfalls, sind die Berechnungsfäden
792
27 Mehrfädige Algorithmen
u und v (logisch) parallel zueinander. Wir können uns eine mehrfädige Berechnung als einen gerichteten azyklischen Graphen vorstellen, in dem die Berechnungsfäden in einem Baum von Prozedurinstanzen eingebettet sind. Abbildung 27.1 beispielsweise zeigt den Baum der Prozedurinstanzen für P-Fib(6) ohne die exakte Struktur der Berechnungsfäden. Abbildung 27.2 konzentriert sich auf einen Ausschnitt dieses Baums und zeigt die entsprechenden Berechnungsfäden, die die jeweiligen Prozedurinstanzen darstellen. Alle gerichteten Kanten, die Berechnungsfäden untereinander verbinden, laufen entweder innerhalb einer Prozedur oder über Kanten des Prozedurbaums. Wir können die Kanten eines gerichteten azyklischen Berechnungsgraphen klassifizieren, um so den jeweiligen Typ der Abhängigkeit zwischen den verschiedenen Berechnungsfäden anzugeben. Eine Weiterführungskante (u, u ), die in Abbildung 27.2 jeweils horizontal eingezeichnet ist, verbindet einen Berechnungsfaden u mit seinem Nachfolger u innerhalb der gleichen Prozedurinstanz. Wenn ein Berechnungsfaden u einen Berechnungsfaden v über eine spawn-Anweisung erzeugt, dann enthält der gerichtete azyklische Graph eine Erzeugungskante (u, v), die in der Abbildung jeweils nach unten gerichtet eingezeichnet ist. Aufrufkanten, die normale Prozeduraufrufe darstellen, werden ebenfalls nach unten gerichtet eingezeichnet. Es ist ein Unterschied, ob der Berechnungsfaden u den Berechnungsfaden v erzeugt oder über einen normalen Prozeduraufruf aufruft: Eine spawn-Anweisung induziert eine horizontale Weiterführungskante von u zu dem in dieser Prozedur auf u folgenden Berechnungsfaden u , die angibt, dass u zu der gleichen Zeit wie v ausgeführt werden kann; ein Prozeduraufruf induziert keine solche Kante. Wenn ein Berechnungsfaden u zu seiner aufrufenden Prozedur zurückkehrt und x der Berechnungsfaden direkt hinter der nächsten sync-Anweisung der aufrufenden Prozedur ist, dann enthält der gerichtete azyklische Berechnungsgraph eine Rücksprungkante (u, x), die nach oben gerichtet eingezeichnet ist. Eine Berechnung startet mit ihrem einzigen initialen Berechnungsfaden – in Abbildung 27.2 der schwarze Knoten in der mit P-Fib(4) markierten Prozedur – und terminiert an ihrem einzigen finalen Berechnungsfaden – der weiße Knoten in der mit P-Fib(4) markierten Prozedur. Wir werden die Ausführung von mehrfädigen Algorithmen an einem idealen Parallelrechner untersuchen, der aus einer Menge von Prozessoren und einem sequentiellen konsistenten gemeinsamen Hauptspeicher besteht. Sequentielle Konsistenz bedeutet, das der gemeinsame Speicher, der in Wirklichkeit viele Lade- und Speicheroperationen von den Prozessoren zur gleichen Zeit ausführen muss, die gleichen Ergebnisse produziert, als wenn er in jedem Schritt genau eine Anweisung von einem der Prozessoren ausführt. Das bedeutet, dass der Hauptspeicher sich gleich verhält, als wenn die Anweisungen sequentiell nach einer globalen linearen Ordnung ausgeführt werden, die die individuelle Ordnung, in der jeder Prozessor seine eigenen Anweisungen lädt, nicht verletzt. Bei dynamischen mehrfädigen Berechnungen, die automatisch durch die Plattform auf die Prozessoren verteilt werden, verhält sich der gemeinsame Hauptspeicher, als wenn die Anweisungen der mehrfädigen Berechnung ineinander verschachtelt werden, um eine lineare Ordnung zu erhalten, die der partiellen Ordnung des gerichteten azyklischen Berechnungsgraphen genügt. Abhängig von der Planung kann die Ordnung bei verschiedenen Programmläufen unterschiedlich sein, das Verhalten einer Programmausführung
27.1 Grundlagen von dynamischem Multithreading
793
kann aber so verstanden werden, als ob wir voraussetzen würden, dass die Anweisungen in einer linearen, zu dem gerichteten azyklischen Berechnungsgraphen konsistenten Ordnung ausgeführt werden. Neben den Voraussetzungen zu semantischen Fragen macht das ideale-ParallelrechnerModell auch Voraussetzungen zu Performanzfragen. Speziell setzt es voraus, dass jeder Prozessor der Maschine gleiche Rechenleistung besitzt und es ignoriert die Kosten für das Verteilen der Arbeit auf die Prozessoren. Wenngleich diese letzte Voraussetzung sehr optimistisch erscheint, stellt es sich heraus, dass der Overhead für das Planen bei Algorithmen mit ausreichend großer „Parallelität“ (ein Begriff, den wir gleich genauer definieren werden) in der Praxis im Allgemeinen minimal ist.
Performanzmaße Wir können die theoretische Effizienz eines mehrfädigen Algorithmus mittels zwei Metriken messen: „Arbeit“ und „Zeitspanne“. Die Arbeit einer mehrfädigen Berechnung ist die Gesamtzeit, die benötigt wird, um die gesamte Berechnung auf nur einem Prozessor auszuführen. Das heißt, die Arbeit ist gegeben durch die Summe der Zeiten, die für die einzelnen Berechnungsfäden benötigt werden. Bei einem gerichteten azyklischen Berechnungsgraphen, in dem jeder Berechnungsfaden eine Zeiteinheit benötigt, ist die Arbeit also gleich der Anzahl der Knoten im gerichteten azyklischen Graphen. Die Zeitspanne ist die maximale Zeit, um die Berechnungsfäden eines Pfades in dem gerichteten azyklischen Graphen auszuführen. Bei einem gerichteten azyklischen Berechnungsgraphen, in dem jeder Berechnungsfaden eine Zeiteinheit benötigt, ist die Zeitspanne gleich der Anzahl der Knoten auf einem längsten oder kritischen Pfad in dem gerichteten azyklischen Graphen. (Sie sollten sich aus Abschnitt 24.2 daran erinnern, dass wir einen kritischen Pfad in einem gerichteten azyklischen Graphen G = (V, E) in Zeit Θ(V + E) bestimmen können.) Der gerichtete azyklische Graph aus Abbildung 27.2 beispielsweise hat insgesamt 17 Knoten und 8 Knoten liegen auf seinem kritischen Pfad, sodass, wenn jeder Berechnungsfaden eine Zeiteinheit benötigt, seine Arbeit 17 Zeiteinheiten und seine Zeitspanne 8 Zeiteinheiten betragen. Die wirkliche Laufzeit einer mehrfädigen Berechnung hängt nicht nur von seiner Arbeit und seiner Zeitspanne ab, sondern auch von der Anzahl der zur Verfügung stehenden Prozessoren und der Methode, mit der der Scheduler den Prozessoren Berechnungsfäden zuweist. Um die Laufzeit einer mehrfädigen Berechnung auf P Prozessoren anzugeben, werden wir den Index P verwenden. So werden wir die Laufzeit eines Algorithmus auf P Prozessoren mit TP bezeichnen. Die Arbeit ist die Laufzeit, wenn nur ein einziger Prozessor zur Verfügung steht, und somit bezeichnen wir sie mit T1 . Die Zeitspanne ist die Laufzeit, die wir erhalten würden, wenn wir jeden Berechnungsfaden auf seinem eigenen Prozessor ausführen würden – oder anders formuliert, wenn wir unendlich viele Prozessoren zur Verfügung hätten – und so bezeichnen wir die Zeitspanne mit T∞ . Die Arbeit und die Zeitspanne liefern uns untere Schranken für die Laufzeit TP einer mehrfädigen Berechnung auf P Prozessoren: • In einem Schritt kann ein idealer Parallelrechner mit P Prozessoren höchstens P Arbeitseinheiten erledigen und somit in Zeit TP höchstens P TP Arbeitseinhei-
794
27 Mehrfädige Algorithmen ten. Da die zu erledigende Arbeit gleich T1 ist, gilt P TP ≥ T1 . Teilen wir durch P , so erhalten wir das Arbeitsgesetz : TP ≥ T1 /P .
(27.2)
• Ein idealer Parallelrechner mit P Prozessoren kann nicht schneller laufen als eine Maschine mit einer unbeschränkten Anzahl von Prozessoren. Oder andersrum formuliert: Eine Maschine mit einer unbegrenzten Anzahl von Prozessoren kann eine Maschine mit P Prozessoren emulieren, indem sie nur P seiner Prozessoren einsetzt. Hieraus folgt das Zeitspannengesetz TP ≥ T∞ .
(27.3)
Wir definieren die Beschleunigung einer Berechnung auf P Prozessoren als das Verhältnis T1 /TP , das aussagt, um welchen Faktor die Berechnung auf P Prozessoren schneller als eine Berechnung auf einem Prozessor ist. Mit dem Arbeitsgesetz gilt TP ≥ T1 /P und somit T1 /TP ≤ P . Damit ist die Beschleunigung, die wir durch P Prozessoren erhalten können, höchstens P . Wenn die Beschleunigung linear in der Anzahl der Prozessoren ist, d. h. wenn T1 /TP = Θ(P ) gilt, dann sagen wir, dass die Berechnung lineare Beschleunigung zeigt, und wenn T1 /TP = P gilt, dann haben wir eine perfekte lineare Beschleunigung. Das Verhältnis T1 /T∞ zwischen Arbeit und Zeitspanne gibt die Parallelität einer mehrfädigen Berechnung an. Wir können die Parallelität aus drei verschiedenen Sichten betrachten. Als ein Verhältnis bezeichnet die Parallelität die durchschnittliche Menge von Arbeit, die in jedem Schritt auf einem kritischen Pfad parallel ausgeführt werden kann. Als obere Schranke gibt die Parallelität die maximal mögliche Beschleunigung an, die durch eine beliebige Anzahl von Prozessoren erreicht werden kann. Schlussendlich, und dies ist vielleicht die wichtigste Sichtweise, zeigt uns die Parallelität eine Grenze für die Möglichkeit auf, eine perfekte lineare Beschleunigung zu erreichen. Genauer, wenn auch immer die Anzahl der Prozessoren die Parallelität übersteigt, kann die Berechnung keine perfekte lineare Beschleunigung mehr erreichen. Um diesen letzten Punkt zu verstehen, setzen Sie voraus, dass P > T1 /T∞ gilt. In diesem Fall impliziert das Zeitspannengesetz, dass die Beschleunigung die Ungleichung T1 /TP ≤ T1 /T∞ < P erfüllt. P , wenn die Anzahl P der Prozessoren in dem idealen ParalZudem gilt T1 /TP lelrechner die Parallelität sehr übersteigt – d. h. wenn P T1 /T∞ gilt –, sodass die Beschleunigung viel kleiner als die Anzahl der Prozessoren ist. Oder anders formuliert, je mehr Prozessoren wir über die Parallelität hinaus einsetzen, desto weniger perfekt ist die Beschleunigung. Betrachten Sie als Beispiel die Berechnung P-Fib(4) in Abbildung 27.2 und setzen Sie voraus, dass jeder Berechnungsfaden eine Zeiteinheit benötigt. Wegen T1 = 17 und T∞ = 8, ist die Parallelität gleich T1 /T∞ = 17/8 = 2.125. Demzufolge ist es nicht möglich, eine Beschleunigung von viel mehr als 2 zu erreichen, unabhängig davon wie viele Prozessoren wir für die Berechnung einsetzen. Für größere Eingaben werden wir jedoch sehen, dass P-Fib(n) substantielle Parallelität besitzt. Wir verstehen unter dem (parallelen) Spielraum einer mehrfädigen Berechnung, die auf einem idealen Parallelrechner mit P Prozessoren ausgeführt wird, das Verhältnis
27.1 Grundlagen von dynamischem Multithreading
795
(T1 /T∞ )/P = T1 /(P T∞ ), welches den Faktor angibt, um den die Parallelität der Berechnung die Anzahl der Prozessoren in der Maschine übersteigt. Wenn der Spielraum also kleiner als 1 ist, können wir nicht hoffen, eine perfekte lineare Beschleunigung zu erhalten, da T1 /(P T∞ ) < 1 gilt und das Zeitspannengesetz impliziert, dass die Beschleunigung, die durch P Prozessoren erreicht werden kann, die Ungleichung T1 /TP ≤ T1 /T∞ < P erfüllt. In der Tat, wenn der Spielraum sich von 1 Richtung 0 verkleinert, divergiert die Beschleunigung der Berechnung immer weiter weg von einer perfekten linearen Beschleunigung. Ist der Spielraum jedoch größer als 1, dann ist die Arbeit pro Prozessor der limitierende Faktor. Wie wir sehen werden, kann ein guter Scheduler immer näher an eine perfekte lineare Beschleunigung herankommen, wenn der Spielraum von 1 aus größer wird.
Verteilen der Berechnungsfäden auf Prozessoren Eine gute Performanz benötigt mehr, als nur die Arbeit und die Zeitspanne zu minimieren. Die Berechnungsfäden müssen auch effizient auf die Prozessoren der parallelen Maschine verteilt werden. Unser mehrfädiges Programmiermodell enthält keine Möglichkeit, zu spezifizieren, welche Berechnungsfäden auf welchen Prozessoren ausgeführt werden sollen. Stattdessen verlassen wir uns auf den Scheduler der Plattform, um die (sich dynamisch entfaltende) Berechnung auf die einzelnen Prozessoren abzubilden. In der Praxis bildet der Scheduler die Berechnungsfäden auf statische Threads ab und das Betriebssystem plant die Threads auf den Prozessoren; dieser Punkt ist aber für unser Verständnis eher unwichtig. Wir können voraussetzen, dass der Scheduler der Plattform den Prozessoren die Berechnungsfäden direkt zuordnet. Ein mehrfädiger Scheduler muss die Berechnung auf die Prozessoren verteilen, ohne zu wissen, wann Berechnungsfäden erzeugt werden oder wann sie terminieren werden – er muss online arbeiten. Zudem arbeitet ein guter Scheduler verteilt, wobei die Threads, die den Scheduler implementieren, kooperieren, um eine ausgewogene Lastverteilung während der Berechnung zu erhalten. Beweisbar gute verteilte online-Scheduler existieren, wenngleich es kompliziert ist, sie zu analysieren. Um die Analyse einfach zu halten, werden wir hier einen nichtverteilten onlineScheduler untersuchen, der zu jedem Zeitpunkt den globalen Zustand der Berechnung kennt. Speziell werden wir Greedy-Scheduler analysieren, die den Prozessoren in jedem Zeitschritt so viele Berechnungsfäden wie irgendwie möglich zuordnen. Wenn in einem Zeitschritt wenigstens P Berechnungsfäden zur Ausführung bereitstehen, sagen wir, dass der Schritt ein vollständiger Schritt ist, und ein Greedy-Scheduler weist den Prozessoren P von den zur Ausführung bereitstehenden Berechnungsfäden zu. In dem anderen Fall stehen weniger als P Berechnungsfäden zur Ausführung bereit – wir sprechen von einem unvollständigen Schritt – und der Scheduler weist jedem zur Ausführung bereitstehenden Berechnungsfaden seinen eigenen Prozessor zu. Bei P Prozessoren ist die beste Laufzeit, auf die wir hoffen können, wegen dem Arbeitsgesetz gleich TP = T1 /P und wegen dem Zeitspannengesetz gleich TP = T∞ . Das folgende Theorem zeigt, dass Greedy-Scheduling in dem Sinne beweisbar gut ist, dass es ein obere Schranke erreicht, die durch die Summe dieser beiden unteren Schranken gegeben ist.
796
27 Mehrfädige Algorithmen
Theorem 27.1 Auf einem idealen Parallelrechner mit P Prozessoren führt ein Greedy-Scheduler eine mehrfädige Berechnung mit Arbeit T1 und Zeitspanne T∞ in Zeit TP ≤ T1 /P + T∞
(27.4)
aus. Beweis: Wir beginnen den Beweis, indem wir uns die vollständigen Schritte anschauen. In jedem vollständigen Schritt, führen die P Prozessoren eine Gesamtarbeit von P aus. Nehmen Sie zum Zwecke eines Beweises durch Widerspruch an, dass die Anzahl der vollständigen Schritte echt größer als T1 /P wäre. Dann ist die durch die vollständigen Schritte ausgeführte Arbeit wenigstens P · ( T1 /P + 1) = P T1 /P + P = T1 − (T1 mod P ) + P > T1
(wg. Gleichung (3.8)) (wg. Gleichung (3.9)) .
Wir erhalten somit einen Widerspruch dazu, dass die P Prozessoren mehr Arbeit erledigen, als die Berechnung benötigt. Hieraus folgt, dass die Anzahl der vollständigen Schritte höchstens gleich T1 /P sein kann. Betrachten Sie nun einen unvollständigen Schritt. Sei G der gerichtete azyklische Graph, der die ganze Berechnung darstellt, und setzen Sie ohne Beschränkung der Allgemeinheit voraus, dass jeder Berechnungsfaden eine Zeiteinheit benötigt. (Wir können jeden längeren Berechnungsfaden durch eine Kette von Berechnungsfäden mit jeweils einer Zeiteinheit ersetzen.) Sei G der Teilgraph von G, der zu Beginn des unvollständigen Schritts noch ausgeführt werden muss, und sei G der Teilgraph, der nach dem unvollständigen Schritt noch ausgeführt werden muss. Ein längster Pfad in einem gerichteten azyklischen Graphen muss notwendigerweise bei einem Knoten mit Eingangsgrad 0 starten. Da ein unvollständiger Schritt eines Greedy-Schedulers alle Berechnungsfäden aus G mit Eingangsgrad 0 ausführt, ist die Länge eines längsten Pfades in G um 1 kleiner als die Länge eines kürzesten Pfades in G . In anderen Worten, ein unvollständiger Schritt verkürzt die Zeitspanne des noch nicht ausgeführten gerichteten azyklischen Graphens um 1. Somit ist die Anzahl der unvollständigen Schritte höchstens gleich T∞ . Da jeder Schritt entweder vollständig oder unvollständig ist, folgt die Aussage des Theorems. Das folgende Korollar zu Theorem 27.1 zeigt, dass ein Greedy-Scheduler sich immer gut verhält. Korollar 27.2 Die Laufzeit TP einer jeden mehrfädigen Berechnung, die durch einen GreedyScheduler auf einem idealen Parallelrechner mit P Prozessoren erzeugt wird, ist höchstens um einen Faktor 2 schlechter als die optimale Laufzeit.
27.1 Grundlagen von dynamischem Multithreading
797
Beweis: Sei TP∗ die Laufzeit, die durch einen optimalen Scheduler auf einer Maschine mit P Prozessoren erreicht wird, und sei T1 und T∞ die Arbeit beziehungsweise die Zeitspanne dieser Berechnung. Da aus dem Arbeitsgesetz (siehe Ungleichung (27.2)) und dem Zeitspannengesetz (siehe Ungleichung (27.3)) TP∗ ≥ max(T1 /P, T∞ ) folgt, impliziert Theorem 27.1 TP ≤ T1 /P + T∞ ≤ 2 · max(T1 /P, T∞ ) ≤ 2 TP∗ . Das nächste Korollar zeigt, dass, wenn der Spielraum anwächst, ein Greedy-Scheduler in der Tat eine fast-perfekte lineare Beschleunigung auf einer beliebigen mehrfädigen Berechnung erreicht. Korollar 27.3 Sei TP die Laufzeit einer mehrfädigen Berechnung, die durch einen Greedy-Scheduler auf einem idealen Parallelrechner mit P Prozessoren erzeugt wird, und sei T1 und T∞ die Arbeit beziehungsweise die Zeitspanne der Berechnung. Dann haben wir TP ≈ T1 /P , d. h. eine Beschleunigung, die ungefähr gleich P ist, wenn P T1 /T∞ gilt. T1 /P Beweis: Wenn wir P T1 /T∞ voraussetzen, dann haben wir auch T∞ und Theorem 27.1 liefert uns TP ≤ T1 /P + T∞ ≈ T1 /P . Da das Arbeitsgesetz (siehe Ungleichung (27.2)) TP ≥ T1 /P vorschreibt, können wir schließen, dass TP ≈ T1 /P gilt, oder äquivalent dazu, dass für die Beschleunigung T1 /TP ≈ P gilt. Das -Symbol bezeichnet „viel kleiner“, aber um wieviel kleiner ist „viel kleiner“? Als Faustformel reicht ein Spielraum von ungefähr 10, d. h. 10-mal mehr Parallelität als Prozessoren, um eine gute Beschleunigung zu erhalten. Dann macht der Zeitspannen-Term in der Greedy-Schranke, (siehe Ungleichung (27.4)) weniger als 10% des Arbeit-proProzessor-Terms aus, was ausreichend gut in den meisten Situationen ist. Wenn eine Berechnung beispielsweise nur auf 10 oder 100 Prozessoren läuft, dann macht es keinen Sinn eine Parallelität von, sagen wir mal, 1.000.000 einer Parallelität von 10.000 zu bevorzugen, auch wenn wir hier eine Differenz vom Faktor 100 haben. Wie die Problemstellung 27-2 zeigt, können wir manchmal durch Reduzierung einer extremen Parallelität Algorithmen erhalten, die in Bezug auf andere Kriterien besser sind und trotzdem weiterhin gut auf einer vernünftigen Anzahl von Prozessoren skalieren.
Analyse von mehrfädigen Algorithmen Wir haben nun alle die Werkzeuge, die wir benötigen, um mehrfädige Algorithmen zu analysieren und gute Laufzeitschranken für unterschiedliche Anzahlen von Prozessoren
798
27 Mehrfädige Algorithmen A B
A
B Arbeit: T1 (A ∪ B) = T1 (A) + T1 (B)
Arbeit: T1 (A ∪ B) = T1 (A) + T1 (B)
Zeitspanne: T∞ (A ∪ B) = T∞ (A) + T∞ (B)
Zeitspanne: T∞ (A ∪ B) = max(T∞ (A), T∞ (B))
(a)
(b)
Abbildung 27.3: Die Arbeit und die Zeitspanne zusammengesetzter Teilberechnungen. (a) Wenn zwei Teilberechnungen in Reihe sind, ergibt sich die Arbeit der zusammengesetzten Berechnung durch die Summe ihrer Arbeit und die Zeitspanne durch die Summe ihrer Zeitspannen. (b) Wenn zwei Teilberechnungen parallel zueinander sind, dann bleibt die Arbeit der zusammengesetzten Berechnung die Summe ihrer Arbeit, aber die Zeitspanne der zusammengesetzten Berechnung ist nur noch gleich dem Maximum ihrer Zeitspannen.
herzuleiten. Die zu leistende Arbeit zu analysieren ist relativ einfach, da wir nichts anderes tun müssen, als die Laufzeit eines gewöhnlichen seriellen Algorithmus zu analysieren – nämlich der Serialisierung des mehrfädigen Algorithmus. Hiermit sollten Sie bereits vertraut sein, denn das ist das, was dieses Lehrbuch bisher gemacht hat! Die Zeitspanne zu analysieren ist viel interessanter, wenn auch im Allgemeinen nicht schwieriger, wenn Sie einmal den Dreh heraushaben. Wir werden die Kernidee in Verbindung mit dem P-Fib-Programm herausarbeiten. Die Arbeit T1 (n) von P-Fib(n) zu analysieren stellt uns vor kein Problem, da wir diese Analyse bereits gemacht haben. Die ursprüngliche Fib-Prozedur stellt im Wesentlichen die Serialisierung von P-Fib dar und somit gilt T1 (n) = T (n) = Θ(φn ) (siehe Gleichung (27.1)). Abbildung 27.3 illustriert, wie wir die Zeitspanne analysieren können. Wenn zwei Teilberechnungen in Reihe sind, dann müssen wir ihre Zeitspannen addieren, um die Zeitspanne der zusammengesetzten Berechnung zu erhalten. Wenn Sie parallel zueinander sind, dann ist die Zeitspanne der zusammengesetzten Berechnung gleich dem Maximum der Zeitspannen der beiden Teilberechnungen. In P-Fib(n) ist der erzeugte Aufruf von P-Fib(n − 1) in Zeile 3 parallel zu dem normalen Prozeduraufruf von P-Fib(n − 2) in Zeile 4. Somit können wir die Zeitspanne von P-Fib(n) durch die Rekursionsgleichung T∞ (n) = max(T∞ (n − 1), T∞ (n − 2)) + Θ(1) = T∞ (n − 1) + Θ(1) , beschreiben, die als Lösung T∞ (n) = Θ(n) hat. Die Parallelität von P-Fib(n) ist durch T1 (n)/T∞ (n) = Θ(φn /n) gegeben und wächst dramatisch, wenn n groß wird. Somit reicht ein mäßig großer Wert für n, um sogar auf den größten Parallelrechnern eine fast-perfekte lineare Beschleunigung für P-Fib(n) zu erreichen, da diese Prozedur einen großen parallelen Spielraum aufweist.
27.1 Grundlagen von dynamischem Multithreading
799
Parallele Schleifen Viele Algorithmen enthalten Schleifen, bei denen alle Iterationen zueinander parallel ausgeführt werden können. Wie wir sehen werden, können wir solche Schleifen parallelisieren, indem wir die Schlüsselwörter spawn und sync verwenden, aber es ist viel praktischer, direkt zu spezifizieren, dass die Iterationen solcher Schleifen nebenläufig ausgeführt werden können. Unser Pseudocode stellt diese Funktionalität über das Schlüsselwort parallel zur Verfügung, das dem Schlüsselwort for in einer for-SchleifenAnweisung vorangestellt wird. Betrachten Sie beispielsweise das Problem, eine n × n-Matrix A = (aij ) mit einem n) zu multiplizieren. Der resultierende n-Vektor y = (yi ) ist durch die Vektor x = (xj n Gleichung yi = j=1 aij xj , für i = 1, 2, . . . , n gegeben. Wir können die Matrix-VektorMultiplikation ausführen, indem wir alle Einträge von y wie folgt parallel berechnen: Mat-Vec(A, x) 1 n = A.zeilen 2 sei y ein neuer Vektor de Länge n 3 parallel for i = 1 to n 4 yi = 0 5 parallel for i = 1 to n 6 for new j = 1 to n 7 yi = yi + aij xj 8 return y In diesem Pseudocode geben die Schlüsselwörter parallel for in den Zeilen 3 und 5 an, dass die Iterationen der entsprechenden Schleifen jeweils parallel zueinander ausgeführt werden können. Das Schlüsselwort new in Zeile 6 gibt an, dass für jede Iteration von i eine neue Variable j allokiert werden muss und nicht auf eine und dieselbe Variable j zugegriffen werden darf; dies würde zu nicht erwünschten Nebeneffekten führen. Ein Compiler kann jede parallel for-Schleife mit Hilfe geschachelter Parallelität als eine Teile-und-Beherrsche-Unterroutine implementieren. Die parallel for-Schleife der Zeilen 5–7 beispielsweise kann durch einen Aufruf Mat-Vec-Main-Loop(A, x, y, n, 1, n) implementiert werden, wobei der Compiler die Hilfsprozedur Mat-Vec-Main-Loop wie folgt generieren würde: Mat-Vec-Main-Loop(A, x, y, n, i, i ) 1 if i = = i 2 for j = 1 to n 3 yi = yi + aij xj 4 else mitte = (i + i )/2 5 spawn Mat-Vec-Main-Loop(A, x, y, n, i, mitte) 6 Mat-Vec-Main-Loop(A, x, y, n, mitte + 1, i ) 7 sync
800
27 Mehrfädige Algorithmen
1,8
1,4
5,8
1,2
1,1
3,4
2,2
3,3
5,6
4,4
5,5
7,8
6,6
7,7
8,8
Abbildung 27.4: Ein gerichteter azyklischer Graph, der die Berechnung von Mat-Vec-Main-Loop(A, x, y, 8, 1, 8) darstellt. Die zwei Zahlen innerhalb eines jeden abgerundeten Rechtecks geben die Werte der letzten zwei Parameter (i und i im Prozedurkopf ) des Aufrufs (spawn-Anweisung oder normaler Aufruf ) der Prozedur an. Die schwarzen Kreise stellen die Berechnungsfäden dar, die zu dem Basisfall oder dem Teil der Prozedur vor der spawn-Anweisung in Zeile 5 von Mat-Vec-Main-Loop korrespondieren; die schattierten Kreise stehen für die Berechnungsfäden, die zu dem Teil der Prozedur korrespondieren, der in Zeile 6 Mat-Vec-Main-Loop aufruft, bis zu der sync-Anweisung in Zeile 7, wo gewartet wird, bis die durch die spawn-Anweisung in Zeile 5 erzeugte Unterroutine zurückkehrt; die weißen Kreise stellen die Berechnungsfäden dar, die zu dem (an sich vernachlässigbaren) Teil der Prozedur hinter der sync-Anweisung bis zu dem Punkt, an dem aus der Prozedur zurückgekehrt wird, korrespondieren.
Dieser Code erzeugt über eine spawn-Anweisung rekursiv die erste Hälfte der Iterationen der Schleife, damit diese parallel zu der zweiten Hälfte der Iterationen ausgeführt wird, und führt dann eine sync-Anweisung aus. Er erzeugt damit den in Abbildung 27.4 gezeigten binären Ausführungsbaum, in dem die Blätter für die einzelnen Schleifeniterationen stehen. Um die durch Mat-Vec auf einer n × n-Matrix zu leistende Arbeit T1 (n) zu berechnen, berechnen wir einfach die Laufzeit seiner Serialisierung, die wir erhalten, indem wir die parallel for-Schleifen durch gewöhnliche for-Schleifen ersetzen. Somit gilt T1 (n) = Θ(n2 ), da die quadratische Laufzeit der zweifach geschachtelten Schleifen in den Zeilen 5–7 die Laufzeit dominiert. Diese Analyse scheint den durch das rekursive Erzeugen der Berechnungsfäden verursachte Overhead, der entsteht, wenn wir die parallelen Schleifen implementieren, zu vernachlässigen. Dieser Overhead erhöht tatsächlich die Arbeit einer parallelen Schleife verglichen mit der Arbeit seiner Serialisierung, aber nicht asymptotisch. Um zu verstehen warum, haben Sie sich zu überlegen, dass, wenn der Baum der rekursiven Prozedurinstanzen ein vollständiger binärer Baum ist, die Anzahl der internen Knoten um 1 kleiner ist als die Anzahl der Blätter (siehe Übung B.5-3). Jeder interne Knoten führt konstant viel Arbeit aus, um den Bereich der Iterationen aufzuteilen, und jedes Blatt korrespondiert zu einer Iteration der Schleife, was wenigs-
27.1 Grundlagen von dynamischem Multithreading
801
tens konstante Zeit (in diesem Fall sogar Zeit Θ(n)) benötigt. Wir können also den Overhead, der durch das rekursive Erzeugen bedingt ist, gegen die Arbeit der Iterationen amortisieren, sodass die Gesamtarbeit höchstens um einen konstanten Faktor anwächst. Aus praktischen Gründen vergröbern dynamische-Multithreading-Plattformen manchmal die Blätter der Rekursion, indem sie mehrere Iterationen in einem einzigen Blatt ausführen, entweder automatisch oder unter der Kontrolle des Programmierers, um so den durch das Erzeugen von Berechnungsfäden verursachte Overhead zu verringern. Dieser reduzierte Overhead geht zu Lasten der Parallelität, aber wenn die Berechnung einen ausreichend großen parallelen Spielraum hat, muss dadurch die fast-perfekte lineare Beschleunigung nicht verloren gehen. Wir müssen auch bei der Analyse der Zeitspanne einer parallelen Schleife den Overhead berücksichtigen, der durch das rekursive Erzeugen von Berechnungsfäden verursacht wird. Da die Tiefe eines rekursiven Aufrufs logarithmisch in der Anzahl der Iterationen ist, ist die Zeitspanne einer parallelen Schleife mit n Iterationen, in der die i-te Iteration die Zeitspanne iter ∞ (i) hat, gleich T∞ (n) = Θ(lg n) + max iter ∞ (i) . 1≤i≤n
Beispielsweise hat die parallele Initialisierungsschleife der Zeilen 3–4 in der Mat-VecProzedur angewendet auf eine n×n-Matrix eine Zeitspanne von Θ(lg n), da das rekursive Erzeugen der Berechnungsfäden den konstanten Arbeitsaufwand einer jeden Iteration dominiert. Die Zeitspanne der doppelt verschachtelten Schleifen der Zeilen 5–7 ist Θ(n), da jede Iteration der äußeren parallel for-Schleife n Iterationen der inneren (seriellen) for-Schleife enthält. Die Zeitspanne des restlichen Codes der Prozedur ist konstant und damit wird die Zeitspanne durch die doppelt verschachtelten Schleifen dominiert. Da die Arbeit in Θ(n2 ) liegt, ist die Parallelität in Θ(n2 )/Θ(n) = Θ(n). (Übung 27.1-6 verlangt von Ihnen, eine Implementierung anzugeben, die sogar noch mehr Parallelität besitzt.)
Kritische Wettlaufsituationen Ein mehrfädiger Algorithmus ist deterministisch, wenn er bei gleicher Eingabe immer das gleiche Ergebnis erzeugt, unabhängig davon, wie die Instruktionen auf den Mehrkern-Rechner abgebildet werden. Er ist nichtdeterministisch, wenn sein Verhalten von Lauf zu Lauf unterschiedlich sein kann. Es kommt oft vor, dass ein mehrfädiger Algorithmus, der deterministisch sein sollte, es nicht ist, weil er „kritische Wettlaufsituationen“ enthält. Kritische Wettlaufsituationen sind der Fluch der Nebenläufigkeit. Bekannte, auf kritische Wettlaufsituationen zurückgehende Fehler (engl.: bugs) sind der Therac-25, ein Linearbeschleuniger zur Anwendung in der Strahlentherapie, der drei Menschen das Leben kostete und mehrere Menschen verletzte, und der Blackout in Nordamerika im Jahre 2003, der dazu führte, dass in mehr als 50 Millionen Haushalten der Strom ausfiel. Diese üblen Bugs sind notorisch schwer zu finden. Sie können tagelang Tests im Labor laufen lassen, ohne Ausfälle zu beobachten, und später im praktischen Einsatz feststellen, dass Ihre Software doch sporadisch abstürzt.
802
27 Mehrfädige Algorithmen
Eine Wettlaufsituation mit statischer Bestimmtheit tritt auf, wenn zwei logisch parallele Anweisungen auf die gleiche Hauptspeicherzelle zugreifen und wenigstens eine von den Anweisungen ein schreibender Zugriff ist. Die folgende Prozedur illustriert einen solchen kritischen Wettlauf: Race-Example( ) 1 x=0 2 parallel for i = 1 to 2 3 x = x+1 4 print x Nachdem x in Zeile 1 mit 0 initialisiert wurde, generiert Race-Example zwei parallele Berechnungsfäden, die jeweils x in Zeile 3 inkrementieren. Wenngleich es so aussieht, dass die Prozedur Race-Example immer den Wert 2 ausgeben würde (ihre Serialisierung macht das sicherlich), kann sie auch den Wert 1 ausgeben. Lassen Sie uns anschauen, wie diese Anomalie auftritt. Wenn ein Prozessor x inkrementiert, dann ist diese Operation nicht unteilbar, sondern besteht aus einer Folge von Anweisungen: 1. x wird aus dem Hauptspeicher gelesen und in eines der Register des Prozessors geschrieben. 2. Der Wert in dem Register wird inkrementiert. 3. Der Wert in dem Register wird in die zu x gehörige Hauptspeicherzelle zurückgeschrieben. Abbildung 27.5(a) zeigt einen gerichteten azyklischen Berechnungsgraphen, der die Ausführung von Race-Example darstellt, in dem die Berechnungsfäden auf die einzelnen Anweisungen heruntergebrochen sind. Rufen Sie sich in Erinnerung, dass wir bei einem idealen Parallelrechner, der sequentielle Konsistenz unterstützt, die parallele Ausführung eines mehrfädigen Algorithmus als eine Verschränkung der Instruktionen ansehen können. Teil (b) der Abbildung zeigt die Werte einer Ausführung der Berechnung, die die Anomalie hervorruft. Der Wert x ist im Hauptspeicher gespeichert und r1 und r2 sind Register von Prozessoren. In Schritt 1 setzt einer der Prozessoren x auf 0. In den Schritten 2 und 3 lädt Prozessor 1 den Wert von x aus dem Hauptspeicher in sein Register r1 und inkrementiert ihn, sodass dann der Wert 1 in r1 steht. An diesem Punkt kommt Prozessor 2 ins Spiel, indem er die Instruktionen 4–6 ausführt. Prozessor 2 lädt x vom Hauptspeicher in das Register r2 , inkrementiert diesen Wert, sodass dann ebenfalls der Wert 1 in r2 steht, und speichert diesen Wert nach x, sodass x zu 1 wird. Nun nimmt Prozessor 1 wieder seine Arbeit mit Schritt 7 auf, speichert den Wert 1, der in r1 gespeichert ist, nach x, was den Wert von x nicht verändert. Aus diesem Grunde gibt Schritt 8 den Wert 1 aus, und nicht den Wert 2, wie das die Serialisierung tun würde. Wir können sehen, was geschehen ist. Wenn die parallele Ausführung so abgelaufen wäre, dass Prozessor 1 alle seine Anweisungen vor Prozessor 2 ausgeführt hätte, dann wäre
27.1 Grundlagen von dynamischem Multithreading 1
803
x=0 Schritt x
2
r1 = x
4
r2 = x
3
incr r1
5
incr r2
7
x = r1
6
x = r2
8
print x (a)
1 2 3 4 5 6 7
0 0 0 0 0 1 1
r1
r2
– 0 1 1 1 1 1
– – – 0 1 1 1
(b)
Abbildung 27.5: Illustration der Wettlaufsituation in Race-Example. (a) Ein gerichteter azyklischer Berechnungsgraph, der die Abhängigkeiten zwischen einzelnen Anweisungen darstellt. r1 und r2 sind Register von Prozessoren. Anweisungen, wie zum Beispiel die Implementierung der Schleifenabfrage, die keinen Bezug zu der Wettlaufsituation haben, haben wir weggelassen. (b) Eine Ausführungssequenz, die zu einem Bug führt. Gezeigt werden der Wert von x im Hauptspeicher und die Werte der Register r1 und r2 in jedem Schritt der Ausführungssequenz.
der Wert 2 ausgegeben worden. Umgekehrt, wäre auch der Wert 2 ausgegeben worden, wenn Prozessor 2 all seine Anweisungen vor Prozessor 1 ausgeführt hätte. Wenn die Anweisungen der zwei Prozessoren jedoch gleichzeitig ausgeführt werden, dann ist es, wie in diesem Beispiel zu sehen, möglich, dass einer der Aktualisierungen von x verloren geht. Natürlich rufen viele Ausführungen diesen Bug nicht hervor. Wenn beispielsweise die Ausführungsreihenfolge 1, 2, 3, 7, 4, 5, 6, 8 oder 1, 4, 5, 6, 2, 3, 7, 8 gewesen wäre, hätten wir das korrekte Ergebnis erhalten. Das ist das Problem mit Wettlaufsituationen statischer Bestimmtheit. In der Regel produzieren die meisten Ausführungsreihenfolgen das korrekte Ergebnis – wie jede, bei der die Anweisungen links vor den Anweisungen rechts ausgeführt werden oder umgekehrt. Einige Ausführungsreihenfolgen erzeugen jedoch falsche Ergebnisse, wenn die Anweisungen ineinander verschränkt angeordnet sind. Aus diesem Grunde kann nur schwer auf Wettlaufsituationen getestet werden. Sie können tagelang Tests laufen lassen und nie auf einen Bug stoßen, und doch einen katastrophalen Systemabsturz im praktischen Einsatz erhalten. Wenngleich wir in verschiedenen Art und Weisen mit Wettlaufsituationen umgehen können, inklusive Methoden des gegenseitigen Ausschlusses und andere Synchronisationsmethoden, werden wir für unsere Zwecke lediglich absichern, dass alle Berechnungsfäden, die zueinander parallel sind, unabhängig voneinander sind: es gibt keine Wettlaufsituationen statischer Bestimmheit zwischen ihnen. Das heißt insbesondere, dass alle Iterationen eines parallel for-Konstrukts voneinander unabhängig sein sollten. Dies bedeutet in einigen Fällen, dass wir mit dem Schlüsselwort new arbeiten müssen, um sicherzustellen, dass verschiedene Iterationen nicht auf der gleichen Variable arbeiten. Ein entsprechendes Beispiel haben wir in Mat-Vec gesehen. Das Schlüssel-
804
27 Mehrfädige Algorithmen
wort new erlaubt, dass wir in den verschiedenen Iterationen mit einem und demselbem Variablennamen arbeiten können, der in den verschiedenen Iterationen aber auf unterschiedliche Hauptspeicherbereiche zeigt. Zwischen einer spawn-Anweisung und der korrespondierenden sync-Anweisung sollte der Code des erzeugten Kindes unabhängig sein von dem Code des Vaters, den Code eingeschlossen, der durch noch zusätzlich erzeugte oder aufgerufene Kinder ausgeführt wird. Wichtig in diesem Zusammenhang ist, dass Argumente, die einem erzeugten Kind übergeben werden, durch den Vater ausgewertet werden, bevor die eigentliche spawn-Anweisung ausgeführt wird; somit ist die Auswertung der Argumente einer erzeugten Unterroutine in Reihe mit den Zugriffen auf diese Argumente durch die erzeugte Unterroutine. Um ein Beispiel anzugeben, wie einfach es ist, einen Code mit einer Wettlaufsituation zu generieren, geben wir hier eine fehlerhafte Implementierung der mehrfädigen Matrix-Vektor-Multiplikation an, die durch Parallelisierung der inneren for-Schleife eine Zeitspanne von Θ(lg n) erreicht: Mat-Vec-Wrong(A, x) 1 n = A.zeilen 2 sei y ein neuer Vektor der Länge n 3 parallel for i = 1 to n 4 yi = 0 5 parallel for i = 1 to n 6 parallel for new j = 1 to n 7 yi = yi + aij xj 8 return y Diese Prozedur ist leider aufgrund von einer Wettlaufsituation in Bezug auf die Aktualisierung von y in Zeile 7 fehlerhaft, die für alle n Werte von y parallel ausgeführt wird. Die Übung 27.1-6 verlangt von Ihnen, eine korrekte Implementierung mit einer Zeitspanne von Θ(lg n) anzugeben. Ein mehrfädiger Algorithmus mit Wettlaufsituationen kann manchmal auch korrekt sein. Betrachten Sie beispielsweise zwei parallele Berechnungsfäden, die den gleichen Wert in eine gemeinsame Variable speichern möchten; natürlich ist es egal, welcher von beiden zuerst der Variablen den Wert zuweist. Im Allgemeinen werden wir aber Codes mit Wettlaufsituationen als nicht legal ansehen.
Eine Lehre aus dem Schach Wir schließen diesen Abschnitt mit einer wahren Geschichte, die sich während der Entwicklung des mehrfädigen Weltklasse-Schachprogramms Socrates [80] ereignete, wenngleich die Zeitangaben in der folgenden Darstellung vereinfacht angegeben werden. Das Programm wurde auf einer 32-Prozessor-Maschine entwickelt, war aber vorgesehen, später auf einem Supercomputer mit 512 Prozessoren zu laufen. Irgendwann während der Entwicklung arbeiteten die Entwickler eine Optimierung in das Programm ein, das seine Laufzeit auf einem wichtigen Benchmark von T32 = 65 Sekunden auf T32 = 40 Sekunden auf der 32-Prozessor-Maschine reduzierte. Mithilfe der Perfomanzmaße Arbeit und
27.1 Grundlagen von dynamischem Multithreading
805
Zeitspanne stellten die Entwickler dann aber fest, dass die optimierte Version, die auf der 32-Prozessor-Maschine schneller als die ursprüngliche Version war, auf 512 Prozessoren in Wirklichkeit langsamer als die ursprüngliche Version sein würde, sodass sie von der „Optimierung“ wieder absehen mussten. Ihre Analyse sah wie folgt aus. Die ursprüngliche Version des Programms hatte Arbeit T1 = 2048 Sekunden zu leisten und eine Zeitspanne von T∞ = 1 Sekunde. Wenn wir die Ungleichung (27.4) als Gleichung nehmen, d. h. TP = T1 /P + T∞ voraussetzen, und sie als eine Approximation der Laufzeit auf P Prozessoren verwenden, sehen wir, dass in der Tat T32 = 2048/32 + 1 = 65 gilt. Mit der Optimierung wäre die Arbeit gleich T1 = 1024 Sekunden und die Zeitspanne wäre T∞ = 8 Sekunden. Mit unserer Approximation würde dann T32 = 1024/32 + 8 = 40 folgen. Die relativen Geschwindigkeiten der zwei Versionen ändern sich jedoch, wenn wir uns die Laufzeiten auf 512 Prozessoren anschauen. Speziell gilt dann T512 = 2048/512 + 1 = 5 Sekunden und T512 = 1024/512+8 = 10 Sekunden. Die Optimierung, die das Programm auf 32 Prozessoren beschleunigt, würde das Programm auf 512 Prozessoren um das Zweifache verlangsamen! Die Zeitspanne der optimierten Version ist 8 Sekunden, was nicht der dominante Term in der Laufzeit bei 32 Prozessoren war, aber der dominante Term bei 512 Prozessoren werden würde. Hiermit würde der Vorteil, den wir durch die Benutzung von mehr Prozessoren erhalten, zunichte gemacht werden. Die Moral der Geschichte besteht darin, dass die Maße Arbeit und Zeitspanne ein besseres Mittel sind, um Performanz zu schätzen, als gemessene Laufzeiten.
Übungen 27.1-1 Setzen Sie voraus, dass wir P-Fib(n − 2) in Zeile 4 der Prozedur P-Fib mit einer spawn-Anweisung erzeugen und sie nicht wie in dem angegebenen Code aufrufen. Welchen Einfluss hat diese Änderung asymptotisch auf die Arbeit, die Zeitspanne und die Parallelität des Algorithmus? 27.1-2 Zeichnen sich den gerichteten azyklischen Berechnungsgraphen, der entsteht, wenn wir P-Fib(5) ausführen. Wenn Sie voraussetzen, dass jeder Berechnungsfaden eine Zeiteinheit benötigt, wie hoch ist dann die Arbeit und die Parallelität und wie groß die Zeitspanne der Berechnung? Zeigen Sie, wie wir den gerichteten azyklischen Graphen mithilfe eines Greedy-Schedulers auf 3 Prozessoren verteilen können, indem Sie jeden Berechnungsfaden mit dem Zeitschritt markieren, in dem er ausgeführt wird. 27.1-3 Beweisen Sie, dass ein Greedy-Scheduler die folgende Zeitschranke erreicht, die etwas stärker ist als die Schranke, die wir in Theorem 27.1 bewiesen haben: TP ≤
T1 − T∞ + T∞ . P
(27.5)
27.1-4 Konstruieren Sie einen gerichteten azyklischen Berechnungsgraphen, für den eine auf einem Greedy-Scheduler basierte Ausführung fast doppelt so viel Zeit benötigt als eine andere auf einem Greedy-Scheduler basierte Ausführung; die
806
27 Mehrfädige Algorithmen Anzahl der zur Verfügung stehenden Prozessoren soll bei beiden Ausführungen die gleiche sein. Beschreiben Sie, wie die zwei Ausführungen vorgehen würden.
27.1-5 Professor Karan misst ihren deterministischen mehrfädigen Algorithmus auf 4, 10 und 64 Prozessoren eines idealen Parallelrechners, wobei sie einen GreedyScheduler benutzt. Sie behauptet, dass die drei Läufe zu den Laufzeiten T4 = 80 Sekunden, T10 = 42 Sekunden und T64 = 10 Sekunden geführt hätten. Zeigen Sie, dass die Professorin entweder lügt oder inkompetent ist. (Hinweis: Wenden Sie das Arbeitsgesetz (27.2), das Zeitspannengesetz (27.3) und Ungleichung (27.5) aus Übung 27.1-3 an.) 27.1-6 Geben Sie einen mehrfädigen Algorithmus zur Multiplikation einer n × nMatrix mit einem n-Vektor an, der eine Parallelität von Θ(n2 / lg n) erreicht und die Arbeit aber bei Θ(n2 ) belässt. 27.1-7 Betrachten Sie den folgenden mehrfädigen Pseudocode zum in-place Transponieren einer n × n-Matrix A: P-Transpose(A) 1 n = A.zeilen 2 parallel for j = 2 to n 3 parallel for new i = 1 to j − 1 4 vertausche aij mit aji Analysieren Sie die Arbeit, die Zeitspanne und die Parallelität dieses Algorithmus. 27.1-8 Setzen Sie voraus, dass wir die parallel for-Schleife in Zeile 3 der Prozedur P-Transpose (siehe Übung 27.1-7) durch eine gewöhnliche for-Schleife ersetzen. Analysieren Sie die Arbeit, die Zeitspanne und die Parallelität des resultierenden Algorithmus. 27.1-9 Für viele Prozessoren laufen die zwei Versionen des Schachprogramms gleich schnell, wenn sie voraussetzen, dass TP = T1 /P + T∞ gilt?
27.2
Mehrfädige Matrizenmultiplikation
In diesem Abschnitt untersuchen wir, wie wir Matrizenmultiplikation, deren serielle Laufzeit wir in Abschnitt 4.2 bereit bestimmt haben, mit Multithreading parallel lösen können. Wir werden sowohl mehrfädige Algorithmen, die auf der Standardmethode einer dreifach verschachtelten Schleife basieren, als auch Teile-und-Beherrsche-Methoden betrachten.
Mehrfädige Matrizenmultiplikation Der erste Algorithmus, den wir untersuchen, ist der einfache Algorithmus, der die Schleifen der Prozedur Square-Matrix-Multiply auf Seite 77 parallel ausführt:
27.2 Mehrfädige Matrizenmultiplikation
807
P-Square-Matrix-Multiply(A, B) 1 n = A.zeilen 2 sei C eine neue n × n-Matrix 3 parallel for i = 1 to n 4 parallel for new j = 1 to n 5 cij = 0 6 for new k = 1 to n 7 cij = cij + aik · bkj 8 return C Um diesen Algorithmus zu analysieren, sollten Sie feststellen, dass die durch den Algorithmus verrichtete Arbeit gleich T1 (n) = Θ(n3 ) ist, also gleich der Laufzeit von Square-Matrix-Multiply, da die Serialisierung des Algorithmus gerade SquareMatrix-Multiply ist. Die Zeitspanne beläuft sich auf T∞ (n) = Θ(n), da der kritische Pfad gegeben ist durch einen Pfad, der den Rekursionsbaum der parallel for-Schleife, die in Zeile 3 beginnt, hinunter läuft, dann den Rekursionsbaum der parallel forSchleife, die in Zeile 4 beginnt, hinunter läuft und schließlich alle n Iterationen der gewöhnlichen for-Schleife in Zeile 6 ausführt. Dies führt zu einer Gesamtzeitspanne von Θ(lg n) + Θ(lg n) + Θ(n) = Θ(n). Somit ist die Parallelität gleich Θ(n3 )/Θ(n) = Θ(n2 ). Übung 27.2-3 verlangt von Ihnen, die innere Schleife zu parallelisieren, um eine Parallelität von Θ(n3 / lg n) zu erhalten, was sie aber nicht dadurch erreichen, indem Sie einfach nur eine parallel for-Schleife verwenden, da dies zu einer Wettlaufsituation führen würde.
Ein mehrfädiger Teile-und-Beherrsche-Algorithmus zur Matrizenmultiplikation Wie wir in Abschnitt 4.2 gelernt haben, können wir n × n-Matrizen seriell in Zeit Θ(nlg 7 ) = O(n2.81 ) berechnen, indem wir Strassens Teile-und-Beherrsche-Strategie anwenden, was uns motiviert, uns zu überlegen, ob wir einen solchen Algorithmus parallelisieren können. Wir beginnen, wie wir das bereits in Abschnitt 4.2 gemacht haben, mit der Parallelisierung eines einfachen Teile-und-Beherrsche-Algorithmus. Rufen Sie sich von Seite 79 in Erinnerung, dass die Prozedur Square-Matrix-MultiplyRecursive, die zwei n × n-Matrizen A und B multipliziert und eine n × n-Matrix C erzeugt, auf der Partitionierung jeder der drei Matrizen in jeweils vier n/2 × n/2Teilmatrizen beruht: A11 A12 B11 B12 C11 C12 A= , B= , C= . A21 A22 B21 B22 C21 C22 Wir können dann das Matrizenprodukt schreiben als A11 A12 B11 B12 C11 C12 = C21 C22 A21 A22 B21 B22 A12 B21 A12 B22 A11 B11 A11 B12 + . = A21 B11 A21 B12 A22 B21 A22 B22
(27.6)
808
27 Mehrfädige Algorithmen
Wir können also zwei n × n-Matrizen multiplizieren, indem wir 8 Multiplikationen von n/2 × n/2-Matrizen und eine Addition von n × n-Matrizen ausführen. Der folgende Pseudocode implementiert diese Teile-und-Beherrsche-Strategie unter Verwendung geschachtelter Parallelität. Im Unterschied zu Square-Matrix-Multiply-Recursive, auf die sie zurückgeht, ist bei der Prozedur P-Matrix-Multiply-Recursive die Ergebnismatrix ein Parameter, um zu vermeiden, dass unnötigerweise Matrizen allokiert werden. P-Matrix-Multiply-Recursive(C, A, B) 1 n = A.zeilen 2 if n = = 1 3 c11 = a11 b11 4 else sei T eine neue n × n-Matrix 5 partitioniere A, B, C und T in n/2 × n/2-Teilmatrizen A11 , A12 , A21 , A22 ; B11 , B12 , B21 , B22 ; C11 , C12 , C21 , C22 ; bzw. T11 , T12 , T21 , T22 ; 6 spawn P-Matrix-Multiply-Recursive(C11 , A11 , B11 ) 7 spawn P-Matrix-Multiply-Recursive(C12 , A11 , B12 ) 8 spawn P-Matrix-Multiply-Recursive(C21 , A21 , B11 ) 9 spawn P-Matrix-Multiply-Recursive(C22 , A21 , B12 ) 10 spawn P-Matrix-Multiply-Recursive(T11 , A12 , B21 ) 11 spawn P-Matrix-Multiply-Recursive(T12 , A12 , B22 ) 12 spawn P-Matrix-Multiply-Recursive(T21 , A22 , B21 ) 13 P-Matrix-Multiply-Recursive(T22 , A22 , B22 ) 14 sync 15 parallel for i = 1 to n 16 parallel for new j = 1 to n 17 cij = cij + tij Zeile 3 bearbeitet den Basisfall, in dem wir 1×1-Matrizen multiplizieren. Wir behandeln den rekursiven Fall in den Zeilen 4–17. Wir stellen eine Hilfsmatrix T in Zeile 4 bereit und Zeile 5 partitioniert jede der Matrizen A, B, C und T in n/2 × n/2-Teilmatrizen. (Wie wir das bereits bei der Prozedur Square-Matrix-Multiply-Recursive auf Seite 79 getan haben, gehen wir auch hier nicht darauf ein, wie die Darstellung der Teilmatrizen über Indexberechnungen erfolgen kann.) Der rekursive Aufruf in Zeile 6 ordnet der Teilmatrix C11 das Teilmatrizenprodukt A11 B11 zu, sodass C11 den ersten der zwei Terme darstellt, die in Gleichung (27.6) noch aufaddiert werden müssen. Analog dazu weisen die Zeilen 7–9 C12 , C21 und C22 die entsprechenden ersten der zwei Terme aus Gleichung (27.6) zu. Die Zeile 10 weist der Teilmatrix T11 das Teilmatrizenprodukt A12 B21 zu, sodass T11 gleich dem zweiten der zwei Terme ist, die wir brauchen, um die endgültige Teilmatrix C11 zu berechnen. Die Zeilen 11–13 weisen T12 , T21 , and T22 die entsprechenden zweiten Terme zu, die für die Berechnung der endgültigen Teilmatrizen C12 , C21 , und C22 benötigt werden. Die ersten sieben rekursiven Aufrufe erfolgen über spawn-Anweisungen und der letzte läuft innerhalb des Hauptberechnungsfaden. Die sync-Anweisung in Zeile 14 gewährleistet, dass alle Teilmatrizenprodukte aus den Zeilen
27.2 Mehrfädige Matrizenmultiplikation
809
6–13 berechnet worden sind, bevor wir die Produkte aus T auf C mit der doppelt verschachtelten parallel for-Schleifen der Zeilen 15–17 aufaddieren können. Wir analysieren zuerst die Arbeit M1 (n) von P-Matrix-Multiply-Recursive, indem wir die Laufzeitanalyse von seinen seriellen Vorläufer Square-Matrix-MultiplyRecursive wiederholen. Im rekursiven Fall partitionieren wir die Matrizen in Zeit Θ(1), führen 8 rekursive Multiplikationen von n/2×n/2-Matrizen aus und benötigen am Ende noch Arbeit in Höhe von Θ(n2 ), um die zwei n × n-Matrizen zu addieren. Somit ist die Rekursionsgleichung für die Arbeit M1 (n) M1 (n) = 8M1 (n/2) + Θ(n2 ) = Θ(n3 ) (siehe Fall 1 der Mastermethode). In anderen Worten, die Arbeit von unserem mehrfädigen Algorithmus ist asymptotisch die gleiche wie die Laufzeit der Prozedur SquareMatrix-Multiply aus Abschnitt 4.2 mit seinen dreifach verschachtelten Schleifen. Um die Zeitspanne M∞ (n) von P-Matrix-Multiply-Recursive zu bestimmen, bemerken wir, dass die Zeitspanne für das Partitionieren der Matrizen Θ(1) ist und somit dominiert wird durch die Zeitspanne von Θ(lg n) der doppelt verschachtelten parallel for-Schleifen in den Zeilen 15–17. Da die 8 parallelen rekursiven Aufrufe alle auf Matrizen der gleichen Größe arbeiten, ist die maximale Zeitspanne eines rekursiven Aufrufes durch die Zeitspanne von irgendeinem dieser Aufrufe gegeben. Somit ist die Rekursionsgleichung für die Zeitspannen M∞ (n) von P-Matrix-Multiply-Recursive durch M∞ (n) = M∞ (n/2) + Θ(lg n)
(27.7)
gegeben. Diese Rekursionsgleichung fällt unter keinen der Fälle der Mastermethode, genügt aber der Bedingung aus Übung 4.6-2. Aus Übung 4.6-2 folgt, dass die Lösung der Rekursionsgleichung (27.7) durch M∞ (n) = Θ(lg2 n) gegeben ist. Wir kennen nun die Arbeit und die Zeitspanne von P-Matrix-Multiply-Recursive, sodass wir seine Parallelität mit M1 (n)/M∞ (n) = Θ(n3 / lg2 n) bestimmen können, was ein sehr großer Wert ist.
Mehrfädige Methode von Strassen Um Strassens Algorithmus zu parallelisieren, folgen wir der allgemeinen Übersicht auf Seite 81, benutzen aber jetzt geschachtelte Parallelität: 1. Wir zerlegen die Eingabematrizen A und B sowie die Ausgabematrix C, wie in Gleichung (27.6) angegeben, in n/2 × n/2-Teilmatrizen. Dieser Schritt benötigt mit geeigneter Indexberechnung Arbeit und Zeitspanne von jeweils Θ(1). 2. Wir erzeugen 10 Matrizen S1 , S2 , . . . , S10 , die alle die Größe n/2 × n/2 haben und die Summe oder die Differenz von zwei Matrizen aus Schritt 1 darstellen. Wir können alle 10 Matrizen mit Arbeit in Höhe von Θ(n2 ) und Zeitspanne Θ(lg n) erzeugen, in dem wir doppelt verschachtelte parallel for-Schleifen benutzen.
810
27 Mehrfädige Algorithmen
3. Mithilfe der die Schritt 1 generierten Teilmatrizen und den 10 in Schritt 2 erzeugten Matrizen können wir die Berechnung von sieben n/2×n/2-Matrizenprodukten P1 , P2 , . . . , P7 rekursiv über spawn-Anweisungen erzeugen. 4. Wir berechnen die benötigten Teilmatrizen C11 , C12 , C21 , C22 der Ergebnismatrix C, indem wir verschiedene Kombinationen der Pi -Matrizen miteinander addieren und subtrahieren und dabei wieder doppelt verschachtelte parallel forSchleifen einsetzen. Wir können alle vier Teilmatrizen mit Θ(n2 ) Arbeit und einer Zeitspanne von Θ(lg n) berechnen. Um diesen Algorithmus zu analysieren, stellen wir zuerst fest, dass die zu leistende Arbeit gerade die Laufzeit des ursprünglichen seriellen Algorithmus ist, d. h. Θ(nlg 7 ), da seine Serialisierung genau diesem entspricht. Wir können eine Rekursionsgleichung für die Zeitspanne des Algorithmus wie bei der Prozedur P-Matrix-Multiply-Recursive herleiten. Hier werden 7 rekursive Aufrufe parallel ausgeführt; da aber alle auf Matrizen der gleichen Größe arbeiten, erhalten wir die gleiche Rekursionsgleichung (27.7), wie die, die wir für P-Matrix-Multiply-Recursive erhalten haben, und die als Lösung Θ(lg2 n) hat. Somit ergibt sich für die Parallelität der mehrfädigen Version von Strassens Algorithmus einen Wert von Θ(nlg 7 / lg2 n), der recht hoch ist, wenn auch leicht kleiner als die Parallelität von P-Matrix-Multiply-Recursive.
Übungen 27.2-1 Zeichnen Sie den gerichteten azyklischen Berechnungsgraphen der Berechnung von P-Square-Matrix-Multiply angewendet auf 2 × 2-Matrizen. Markieren Sie in Ihrem Diagramm, welche Knoten zu welchen Berechnungsfäden der Ausführung des Algorithmus korrespondieren. Arbeiten Sie mit der Konvention, dass Erzeugungs- und Aufrufkanten nach unten, Weiterführungskanten horizontal nach rechts und Rücksprungkanten aufwärts gerichtet eingezeichnet werden. Analysieren Sie die Arbeit, die Zeitspanne und die Parallelität dieser Berechnung unter der Voraussetzung, dass jeder Berechnungsfaden eine Zeiteinheit benötigt. 27.2-2 Lösen Sie Übung 27.2-1 für P-Matrix-Multiply-Recursive. 27.2-3 Geben Sie den Pseudocode eines mehrfädigen Algorithmus an, der zwei n × nMatrizen multipliziert und dabei Arbeit in Höhe von Θ(n3 ) leistet; die Zeitspanne soll aber nur Θ(lg n) sein. Analysieren Sie Ihren Algorithmus. 27.2-4 Geben Sie den Pseudocode eines effizienten mehrfädigen Algorithmus zur Multiplikation einer p × q-Matrix mit einer q × r-Matrix an. Ihr Algorithmus soll stark parallel sein, sogar dann, wenn einer der Werte p, q und r gleich 1 ist. Analysieren Sie Ihren Algorithmus. 27.2-5 Geben Sie den Pseudocode eines effizienten mehrfädigen Algorithmus an, der eine n × n-Matrix in-place transponiert, indem er über eine Teile-undBeherrsche-Strategie ohne Benutzung einer parallel for-Schleife die Matrix rekursiv in vier n/2 × n/2-Teilmatrizen zerlegt. Analysieren Sie Ihren Algorithmus.
27.3 Mehrfädiges Sortieren durch Mischen
811
27.2-6 Geben Sie den Pseudocode einer effizienten mehrfädigen Implementierung des Floyd-Warshall-Algorithmus (siehe Abschnitt 25.2) an, der kürzeste Pfade zwischen allen Knotenpaaren in einem Graphen mit Kantengewichten berechnet. Analysieren Sie Ihren Algorithmus.
27.3
Mehrfädiges Sortieren durch Mischen
Wir haben uns bereits serielles Sortieren durch Mischen in Abschnitt 2.3.1 angeschaut und haben seine Laufzeit in Abschnitt 2.3.2 analysiert und gezeigt, dass sie in Θ(n lg n) liegt. Da Sortieren durch Mischen bereits das Teile-und-Beherrsche-Paradigma verwendet, scheint das Problem ein wunderbarer Kandidat für Multithreading mit geschachtelter Parallelität zu sein. Wir können den Pseudocode einfach so modifizieren, dass der erste rekursive Aufruf durch eine spawn-Anweisung erzeugt wird: Merge-Sort’(A, p, r) 1 if p < r 2 q = (p + r)/2 3 spawn Merge-Sort (A, p, q) 4 Merge-Sort (A, q + 1, r) 5 sync 6 Merge(A, p, q, r) Wie ihr serielles Gegenstück sortiert die Prozedur Merge-Sort das Teilfeld A[p . . r]. Nachdem die zwei rekursiven Unterroutinen in den Zeilen 3 und 4 ihre Ausführung beendet haben, was durch die sync-Anweisung in Zeile 5 gewährleistet wird, ruft Merge-Sort die gleiche Merge-Prozedur wie die auf Seite 32 auf. Lassen Sie uns die Prozedur Merge-Sort analysieren. Um dies zu tun, müssen wir zuerst die Prozedur Merge analysieren. Rufen Sie sich in Erinnerung, dass ihre serielle Laufzeit, um n Elemente zu mischen, in Θ(n) liegt. Da Merge seriell arbeitet, liegt sowohl ihre Arbeit als auch ihre Zeitspanne in Θ(n). Somit charakterisiert die folgende Rekursionsgleichung die Arbeit MS 1 (n) der Prozedur Merge-Sort angewendet auf n Elemente: MS 1 (n) = 2 MS 1 (n/2) + Θ(n) = Θ(n lg n) , was die gleiche Laufzeit ist wie die serielle Laufzeit von Sortieren durch Mischen. Da die beiden rekursiven Aufrufe von Merge-Sort parallel laufen, ist die Zeitspanne MS ∞ durch die Rekursionsgleichung MS ∞ (n) = MS ∞ (n/2) + Θ(n) = Θ(n)
812
27 Mehrfädige Algorithmen p1
T
…
q1
≤x
r1
≥x
x
mische A
…
≤x p3
p2
q2
…
kopiere x
r2
≥x
T [p], so gibt sie den größten Index q aus dem Bereich p < q ≤ r + 1 mit T [q − 1] < x zurück. Der Pseudocode sieht dann wie folgt aus: Binary-Search(x, T, p, r) 1 linker -rand = p 2 rechter -rand = max(p, r + 1) 3 while linker -rand < rechter -rand 4 mitte = (linker -rand + rechter -rand)/2 5 if x ≤ T [mitte] 6 rechter -rand = mitte 7 else linker -rand = mitte + 1 8 return rechter -rand Der Aufruf der seriellen Prozesur Binary-Search(x, T, p, r) benötigt Θ(lg n) Zeit im schlechtesten Fall, wobei n = r − p + 1 die Größe des Teilfeldes ist, auf das die Prozedur
814
27 Mehrfädige Algorithmen
angewendet wird. (Siehe Übung 2.3-5.) Da Binary-Search eine serielle Prozedur ist, sind ihre Arbeit und Zeitspanne im schlechtesten Fall Θ(lg n). Wir können nun daraufhin arbeiten, den Pseudocode für die eigentliche mehrfädige Prozedur zum Mischen zu schreiben. Wie die Prozedur Merge auf Seite 32 setzt auch die Prozdur P-Merge voraus, dass die zwei zu mischenden Teilfelder in dem gleichen Feld liegen. Im Unterschied zu Merge setzt aber P-Merge nicht voraus, dass die zwei zu mischenden Teilfelder im Feld adjazent sind, d. h. P-Merge verlangt nicht, dass p2 = r1 + 1 gilt. Ein weiterer Unterschied zwischen Merge und P-Merge besteht darin, dass P-Merge das Ausgabeteilfeld A, in dem die gemischten Werte gespeichert werden sollen, als Parameter erhält. Der Aufruf P-Merge(T, p1 , r1 , p2 , r2 , A, p3 ) mischt die sortierten Teilfelder T [p1 . . r1 ] und T [p2 . . r2 ] und schreibt das Ergebnis in das Teilfeld A[p3 . . r3 ]; r3 = p3 + (r1 − p1 + 1) + (r2 − p2 + 1) − 1 = p3 + (r1 − p1 ) + (r2 − p2 ) + 1 wird dabei nicht als Eingabeparameter übergeben. P-Merge(T, p1 , r1 , p2 , r2 , A, p3 ) 1 n1 = r1 − p1 + 1 2 n2 = r2 − p2 + 1 // gewährleiste, dass n1 ≥ n2 gilt 3 if n1 < n2 4 vertausche p1 mit p2 5 vertausche r1 mit r2 6 vertausche n1 mit n2 7 if n1 = = 0 // sind beide Teilfelder leer? 8 return 9 else q1 = (p1 + r1 )/2 10 q2 = Binary-Search(T [q1 ], T, p2 , r2 ) 11 q3 = p3 + (q1 − p1 ) + (q2 − p2 ) 12 A[q3 ] = T [q1 ] 13 spawn P-Merge(T, p1 , q1 − 1, p2 , q2 − 1, A, p3 ) 14 P-Merge(T, q1 + 1, r1 , q2 , r2 , A, q3 + 1) 15 sync Die Prozedur P-Merge arbeitet wie folgt. Die Zeilen 1–2 berechnen die Längen n1 beziehungsweise n2 der Teilfelder T [p1 . . r1 ] beziehungsweise T [p2 . . r2 ]. Die Zeilen 3–6 erzwingen die Voraussetzung, dass n1 ≥ n2 im Folgenden gilt. Zeile 7 testet auf den Basisfall, in dem das Teilfeld T [p1 . . r1 ] leer ist (und somit auch T [p2 . . r2 ]) und wir einfach nur aus der Prozedur zurückkehren. Die Zeilen 9–15 implementieren die Teileund-Beherrsche-Strategie. Zeile 9 berechnet die Mitte von T [p1 . . r1 ] und Zeile 10 bestimmt den Index q2 in T [p2 . . r2 ] so, dass alle Elemente in T [p2 . . q2 − 1] kleiner als T [q1 ] (das gleich x ist) sind und alle Elemente in T [q2 . . r2 ] größer gleich T [q1 ] sind. Zeile 11 berechnet den Index q3 , der das Ausgabeteilfeld A[p3 . . r3 ] in A[p3 . . q3 − 1] und A[q3 + 1 . . r3 ] aufteilt; Zeile 12 kopiert dann T [q1 ] direkt in A[q3 ]. Anschließend steigen wir unter Einsatz geschachtelter Parallelität rekursiv in die Prozedur ab. Zeile 13 erzeugt über eine spawn-Anweisung das erste Teilproblem, während Zeile 14 das zweite Teilproblem parallel dazu aufruft. Die sync-Anweisung gewährleistet, dass die Teilprobleme fertig gelöst sind, bevor die Prozedur zurückkehrt. (Da jede
27.3 Mehrfädiges Sortieren durch Mischen
815
Prozedur implizit ein sync ausführt, bevor sie zurückkehrt, hätten wir die explizite sync-Anweisung in Zeile 15 auch weglassen können, aber es entspricht gutem Programmierstil, sie explizit anzugeben.) Es bedarf gewisser Geschicklichkeit im Code, um zu gewährleisten, dass, wenn das Teilfeld T [p2 . . r2 ] leer ist, der Code korrekt arbeitet. Der Grund dafür, dass es funktioniert, besteht darin, dass bei jedem rekursiven Aufruf, ein mittleres Element von T [p1 . . r1 ] in das Ausgabeteilfeld gesetzt wird, bis T [p1 . . r1 ] schlussendlich selbst leer ist und wir uns somit im Basisfall befinden.
Analyse des mehrfädigen Mischens Wir leiten zuerst eine Rekursionsgleichung für die Zeitspanne PM ∞ (n) von P-Merge ab, bei der die zwei Teilfelder zusammen n = n1 + n2 Elemente enthalten. Da der in Zeile 13 über eine spawn-Anweisung erzeugte Aufruf und der gewöhnliche Aufruf in Zeile 14 logisch parallel arbeiten, müssen wir nur den teuersten der beiden Aufrufe untersuchen. Der Schlüssel besteht darin, zu verstehen, dass im schlechtesten Fall die maximale Anzahl von Elementen in jedem der beiden rekursiven Aufrufe höchstens 3n/4 sein kann, was wir wie folgt sehen können. Da die Zeilen 3–6 gewährleisten, dass n2 ≤ n1 gilt, folgt n2 = 2n2 /2 ≤ (n1 + n2 )/2 = n/2. Im schlechtesten Fall, mischt einer der zwei rekursiven Aufrufe n1 /2 Elemente von T [p1 . . r1 ] mit allen n2 Elementen von T [p2 . . r2 ] und somit ist die Anzahl der in dem Aufruf involvierten Elementen gleich
n1 /2 + n2 ≤ n1 /2 + n2 /2 + n2 /2 = (n1 + n2 )/2 + n2 /2 ≤ n/2 + n/4 = 3n/4 . Addieren wir die Θ(lg n) Kosten des Aufrufs von Binary-Search aus Zeile 10 hinzu, so erhalten wir die folgende Rekursionsgleichung für die Zeitspanne im schlechtesten Fall: PM ∞ (n) = PM ∞ (3n/4) + Θ(lg n) .
(27.8)
(Im Basisfall ist die Zeitspanne gleich Θ(1), da die Zeilen 1–8 in konstanter Zeit ausgeführt werden können.) Diese Rekursionsgleichung fällt unter keinen der Fälle der Mastermethode, aber sie erfüllt die Bedingungen aus Übung 4.6-2. Somit ist die Lösung der Rekursionsgleichung (27.8) gleich PM ∞ (n) = Θ(lg2 n). Wir analysieren nun die Arbeit PM 1 (n) von P-Merge angewendet auf n Elemente, für die sich herausstellen wird, dass sie in Θ(n) ist. Da jedes der n Elemente vom Feld T in das Feld A kopiert werden muss, haben wir PM 1 (n) = Ω(n). Es bleibt also nur noch PM 1 (n) = O(n) zu zeigen. Wir werden zuerst eine Rekursionsgleichung für die Arbeit im schlechtesten Fall ableiten. Die binäre Suche in Zeile 10 kostet im schlechtesten Fall Θ(lg n), was die Arbeit der anderen Berechnungsfäden außerhalb der rekursiven Aufrufe dominiert. Für die rekursiven Aufrufe, sollten Sie bemerken, dass, wenngleich die rekursiven Aufrufe der Zeilen 13 und 14 möglicherweise unterschiedliche Anzahlen von Elementen mischen, die beiden rekursiven Aufrufe zusammen höchstens n Elemente mischen (in Wirklichkeit sogar
816
27 Mehrfädige Algorithmen
nur n − 1 Elemente, da T [q1 ] in keinem der beiden rekursiven Aufrufe involviert ist). Zudem arbeitet ein rekursiver Aufruf – wie wir gesehen haben, als wir die Zeitspanne analysierten – auf höchstens 3n/4 Elementen. Wir erhalten mit diesen Überlegungen die Rekursionsgleichung PM 1 (n) = PM 1 (αn) + PM 1 ((1 − α)n) + O(lg n) ,
(27.9)
wobei α in dem Bereich 1/4 ≤ α ≤ 3/4 liegt und der wirkliche Wert von α je nach Rekursionsebene variieren kann. Wir beweisen mithilfe der Substitutionsmethode, dass die Rekursionsgleichung (27.9) die Lösung PM 1 = O(n) hat. Setzen Sie voraus, dass PM 1 (m) ≤ c1 m − c2 lg m für m < n gilt, wobei c1 und c2 zwei geeignete positive Konstanten sind. Durch Substitution erhalten wir dann PM 1 (n) ≤ (c1 αn − c2 lg(αn)) + (c1 (1 − α)n − c2 lg((1 − α)n)) + Θ(lg n) = c1 (α + (1 − α))n − c2 (lg(αn) + lg((1 − α)n)) + Θ(lg n) = c1 n − c2 (lg α + lg n + lg(1 − α) + lg n) + Θ(lg n) = c1 n − c2 lg n − (c2 (lg n + lg(α(1 − α))) − Θ(lg n)) ≤ c1 n − c2 lg n , da wir c2 so groß wählen können, dass c2 (lg n + lg(α(1 − α))) den Θ(lg n)-Term dominiert. Zudem können wir c1 so groß wählen, dass die Ungleichung für den Basisfall der Rekursionsgleichung gilt. Da die Arbeit PM 1 (n) von P-Merge sowohl in Ω(n) als auch in O(n) liegt, haben wir PM 1 (n) = Θ(n). Die Parallelität von P-Merge ist demnach PM 1 (n)/PM ∞ (n) = Θ(n/ lg2 n).
Mehrfädiges Sortieren durch Mischen Jetzt, wo wir eine schön parallelisierte mehrfädige Prozedur zum Mischen haben, können wir sie in mehrfädiges Sortieren durch Mischen einbauen. Diese Version von Sortieren durch Mischen ist der Prozedur Merge-Sort , der wir weiter vorne begegnet sind, ähnlich. Im Unterschied zu Merge-Sort erhält sie als Parameter ein Ausgabefeld B, in das das sortierte Ergebnis gespeichert wird. Speziell sortiert der Aufruf P-Merge-Sort(A, p, r, B, s) die Elemente aus A[p . . r] und speichert sie in B[s . . s + r − p]. P-Merge-Sort(A, p, r, B, s) 1 n = r−p+1 2 if n = = 1 3 B[s] = A[p] 4 else sei T [1 . . n] ein neues Feld 5 q = (p + r)/2 6 q = q − p + 1 7 spawn P-Merge-Sort(A, p, q, T, 1) 8 P-Merge-Sort(A, q + 1, r, T, q + 1) 9 sync 10 P-Merge(T, 1, q , q + 1, n, B, s)
27.3 Mehrfädiges Sortieren durch Mischen
817
Nachdem Zeile 1 die Anzahl n der Elemente in dem Eingabeteilfeld A[p . . r] berechnet hat, behandeln die Zeilen 2 –3 den Basisfall, in dem das Feld nur aus einem Element besteht. Die Zeilen 4–6 bereiten die beiden rekursiven Aufrufe in den Zeilen 7 und 8 vor, die parallel zueinander arbeiten. Speziell stellt Zeile 4 ein Hilfsfeld T mit n Elementen bereit, in das das Ergebnis des Sortierens durch Mischen gespeichert werden soll. Zeile 5 berechnet den Index q in A[p . . r], an dem die Elemente in zwei Teilfelder A[p . . q] und A[q + 1 . . r] aufgeteilt werden, die im Folgenden rekursiv sortiert werden, und Zeile 6 berechnet die Anzahl q der Elemente in dem ersten Teilfeld A[p . . q], die Zeile 8 verwendet, um den Startindex in T zu bestimmen, ab dem das sortierte Ergebnis von A[q + 1 . . r] gespeichert werden soll. Es kommt dann der spawn-Aufruf und der gewöhnliche Prozeduraufruf gefolgt von der sync-Anweisung in Zeile 9, die die Prozedur zwingt, zu warten, bis die durch die spawn-Anweisung erzeugte Prozedur fertig ausgeführt ist. Schließlich ruft Zeile 10 die Prozedur P-Merge auf, um die sortierten Teilfelder, die jetzt in T [1 . . q ] und T [q + 1 . . n] gespeichert sind, zu mischen und in das Ausgabeteilfeld B[s . . s + r − p] zu schreiben.
Analyse des mehrfädigen Sortierens durch Mischen Wir beginnen mit der Analyse der Arbeit PMS 1 (n) von P-Merge-Sort, die wesentlich einfacher ist als die Analyse von P-Merge. In der Tat, die Arbeit ist gegeben durch die Rekursionsgleichung PMS 1 (n) = 2 PMS 1 (n/2) + PM 1 (n) = 2 PMS 1 (n/2) + Θ(n) . Diese Rekursionsgleichung ist gleich der Rekursionsgleichung (4.4) für das normale Merge-Sort aus Abschnitt 2.3.1 und hat wegen Fall 2 der Mastermethode die Lösung PMS 1 (n) = Θ(n lg n). Wir leiten nun eine Rekursionsgleichung für die Zeitspanne PMS ∞ (n) im schlechtesten Fall ab und analysieren sie. Da die zwei rekursiven Aufrufe von P-Merge-Sort in den Zeilen 7 und 8 logisch parallel arbeiten, können wir eine der beiden ignorieren und erhalten so die Rekursionsgleichung PMS ∞ (n) = PMS ∞ (n/2) + PM ∞ (n) = PMS ∞ (n/2) + Θ(lg2 n) .
(27.10)
Wie schon bei der Rekursionsgleichung (27.8) ist die Mastermethode auf die Rekursionsgleichung (27.10) nicht anwendbar, aber Übung 4.6-2 ist anwendbar. Die Lösung der Rekursionsgleichung ist somit PMS ∞ (n) = Θ(lg3 n) und demnach ist die Zeitspanne von P-Merge-Sort gleich Θ(lg3 n). Paralleles Mischen erlaubt der Prozedur P-Merge-Sort einen signifikanten Parallelitätsvorteil gegenüber Merge-Sort . Erinnern Sie sich daran, dass die Parallelität von Merge-Sort , die eine serielle Prozedur zum Mischen aufruft, nur in Θ(lg n) liegt. Für
818
27 Mehrfädige Algorithmen
P-Merge-Sort ist die Parallelität PMS 1 (n)/PMS ∞ (n) = Θ(n lg n)/Θ(lg3 n) = Θ(n/ lg2 n) , was viel besser ist, sowohl von der Theorie als auch von der Praxis her. Eine gute Implementierung würde ein wenig Parallelität preisgeben, da der Basisfall vergröbert werden würde, um die in der asymptotischen Notation verborgenen Konstanten zu verkleinern. Eine einfache Methode, den Basisfall zu vergröbern, besteht darin, zu einem seriellen Sortieralgorithmus, beispielsweise Quicksort, zu wechseln, wenn die Größe des Feldes ausreichend klein ist.
Übungen 27.3-1 Erklären Sie, wie wir den Basisfall von P-Merge vergröbern können. 27.3-2 Anstatt ein mittleres Element in dem größeren Teilfeld zu bestimmen – wie dies durch P-Merge gemacht wird –, betrachten wir eine Variante, bei der ein mittleres Element bezüglich allen Elementen der zwei sortierten Teilfelder bestimmt wird, indem wir die Lösung von Übung 9.3-8 anwenden. Geben Sie den Pseudocode einer effizienten mehrfädigen Prozedur für Mischen an, die diese Prozedur zum Bestimmen eines Medians verwendet. Analysieren Sie Ihren Algorithmus. 27.3-3 Geben Sie einen effizienten mehrfädigen Algorithmus zum Partitionieren eines Feldes bezüglich eines Pivotelementes an, wie dies durch die auf Seite 172 vorgestellte Prozedur Partition getan wird. Sie brauchen das Feld nicht in-place zu partitionieren. Gestalten Sie Ihren Algorithmus so parallel wie möglich. Analysieren Sie Ihren Algorithmus. (Hinweis: Sie benötigen möglicherweise ein Hilfsfeld und Sie müssen möglicherweise mehr als einmal über die Eingabewerte laufen.) 27.3-4 Geben Sie eine mehrfädige Version der auf Seite 923 vorgestellten Prozedur Recursive-FFT an. Gestalten Sie Ihren Algorithmus so parallel wie möglich. Analysieren Sie Ihren Algorithmus. 27.3-5∗Geben Sie eine mehrfädige Version von der auf Seite 216 vorgestellten Prozesur Randomized-Select an. Gestalten Sie Ihren Algorithmus so parallel wie möglich. Analysieren Sie Ihren Algorithmus. (Hinweis: Benutzen Sie den Partitionierungsalgorithmus aus Übung 27.3-3.) 27.3-6∗Zeigen Sie, wie wir die Prozedur Select aus Abschnitt 9.3 parallelisieren können. Gestalten Sie Ihren Algorithmus so parallel wie möglich. Analysieren Sie Ihren Algorithmus.
Problemstellungen zu Kapitel 27
819
Problemstellungen 27-1 Implementierung paralleler Schleifen unter Verwendung geschachtelter Parallelität Betrachten Sie den folgenden mehrfädigen Algorithmus zum Addieren von zwei n-elementigen Feldern A[1 . . n] und B[1 . . n]: Sum-Arrays(A, B, C) 1 parallel for i = 1 to A.l¨a nge 2 C[i] = A[i] + B[i] a. Schreiben Sie die parallele Schleife in Sum-Arrays so um, dass geschachtelte Parallelität (spawn und sync) wie in Mat-Vec-Main-Loop verwendet wird. Untersuchen Sie die Parallelität Ihrer Implementierung. Betrachten Sie die folgende alternative Implementierung der parallelen Schleife, die einen noch festzulegenden Wert skalierungsfaktor enthält: Sum-Arrays (A, B, C) 1 n = A.l¨a nge 2 skalierungsfaktor = ? // der Wert ist noch festzulegen 3 r = n/skalierungsfaktor 4 for k = 0 to r − 1 5 spawn Add-Subarray(A, B, C, k · skalierungsfaktor + 1, min((k + 1) · skalierungsfaktor , n)) 6 sync Add-Subarray(A, B, C, i, j) 1 for k = i to j 2 C[k] = A[k] + B[k] b. Setzen Sie voraus, dass wir den Skalierungsfaktor auf 1 setzen. Wie hoch wäre die Parallelität dieser Implementierung? c. Geben Sie eine Formel für die Zeitspanne der Prozedur Sum-Arrays als Funktion in n und skalierungsfaktor an. Wie müssen wir den Skalierungsfaktor wählen, um die Parallelität zu maximieren? 27-2 Verringerung des Hilfspeicherplatzes bei einer Matrizenmultiplikation Die Prozedur P-Matrix-Multiply-Recursive hat den Nachteil, dass eine Hilfsmatrix T der Größe n × n bereitgestellt werden muss, die die in der Θ-Notation versteckten Konstanten nachteilig beeinflussen kann. Die Prozedur P-MatrixMultiply-Recursive hat andererseits aber eine hohe Parallelität. Würden wir die Konstanten in der Θ-Notation vernachlässigen, so wäre die Parallelität bei der Multiplikation von beispielsweise 1000×1000-Matrizen ungefähr 10003 /102 = 107 , da lg 1000 ≈ 10 gilt. Die meisten Parallelrechner haben aber wesentlich weniger als 10 Millionen Prozessoren.
820
27 Mehrfädige Algorithmen a. Geben Sie einen rekursiven mehrfädigen Algorithmus an, der zu Lasten einer Erhöhung der Zeitspanne um Θ(n) auf die Hilfsmatrix T verzichtet. (Hinweis: Berechnen Sie C = C + AB, indem Sie der allgemeinen Strategie von PMatrix-Multiply-Recursive folgen, aber C parallel initialisieren und eine sync-Anweisung an einer geeigneten Stelle einfügen.) b. Geben Sie jeweils eine Rekursionsgleichung für die Arbeit und die Zeitspanne Ihrer Implementierung an und lösen sie die beiden Rekursionsgleichungen. c. Bestimmen Sie die Parallelität Ihrer Implementierung. Schätzen Sie die Parallelität bei 1000 × 1000-Matrizen ab; hierbei sollten Sie die in der Θ-Notation versteckten Konstanten vernachlässigen. Vergleichen Sie die Parallelität Ihrer Implementierung mit der von P-Matrix-Multiply-Recursive.
27-3 Mehrfädige Algorithmen auf Matrizen a. Parallelisieren Sie die auf der Seite 835 vorgestellte Prozedur LUDecomposition, indem Sie den Pseudocode einer mehrfädigen Version dieses Algorithmus angeben. Gestalten Sie Ihre Implementierung so parallel wie möglich und bestimmen Sie die Arbeit, die Zeitspanne und die Parallelität Ihrer Implementierung. b. Lösen Sie die gleiche Aufgabe für die auf Seite 838 vorgestellte Prozedur LUPDecomposition. c. Lösen Sie die gleiche Aufgabe für die auf Seite 831 vorgestellte Prozedur LUPSolve. d. Lösen Sie die gleiche Aufgabe für den auf Gleichung (28.13) basierenden mehrfädigen Algorithmus zum Invertieren einer symmetrischen positiv definiten Matrix. 27-4 Mehrfädige Reduktionen und Präfixberechnungen Die ⊗-Reduktion eines Feldes x[1 . . n], wobei ⊗ ein assoziativer Operator ist, ist definiert als der Wert y = x[1] ⊗ x[2] ⊗ · · · ⊗ x[n] . Die folgende Prozedur berechnet die ⊗-Reduktion eines Teilfeldes x[i . . j] seriell. Reduce(x, i, j) 1 y = x[i] 2 for k = i + 1 to j 3 y = y ⊗ x[k] 4 return y a. Verwenden Sie geschachtelte Parallelität, um einen mehrfädigen Algorithmus P-Reduce zu implementieren, der die gleiche Funktion mit Θ(n) Arbeit und Θ(lg n) Zeitspanne ausführt. Analysieren Sie Ihren Algorithmus.
Problemstellungen zu Kapitel 27
821
Ein verwandtes Problem ist das der ⊗-Präfixberechnung – zum Teil auch bekannt unter dem Namen eines ⊗-Scans – auf einem Feld x[1 . . n], wobei ⊗ wiederum ein assoziativer Operator ist. Die ⊗-Präfixberechnung produziert das Feld y[1 . . n], das durch y[1] = x[1] , y[2] = x[1] ⊗ x[2] , y[3] = x[1] ⊗ x[2] ⊗ x[3] , ... y[n] = x[1] ⊗ x[2] ⊗ x[3] ⊗ · · · ⊗ x[n] gegeben ist, d. h. aus allen mit ⊗ „aufsummierten“ Präfixen des Feldes x besteht. Die folgende serielle Prozedur Scan führt eine ⊗-Präfixberechnung durch: Scan(x) 1 n = x.l¨a nge 2 sei y[1 . . n] ein neues Feld 3 y[1] = x[1] 4 for i = 2 to n 5 y[i] = y[i − 1] ⊗ x[i] 6 return y Leider ist es nicht einfach, die Prozedur Scan zu parallelisieren. Das Ersetzen der for-Schleife durch eine parallel for-Schleife beispielsweise würde zu einer Wettlaufsituation führen, da jede Iteration des Schleifenblocks von der vorherigen Iteration abhängt. Die folgende Prozedur P-Scan-1 führt eine parallele, wenngleich ineffiziente ⊗-Präfixberechnung durch: P-Scan-1(x) 1 n = x.l¨a nge 2 sei y[1 . . n] ein neues Feld 3 P-Scan-1-Aux(x, y, 1, n) 4 return y P-Scan-1-Aux(x, y, i, j) 1 parallel for l = i to j 2 y[l] = P-Reduce(x, 1, l) b. Analysieren Sie die Arbeit, die Zeitspanne und die Parallelität von P-Scan-1. Mit geschachtelter Parallelität können wir eine effizientere ⊗-Präfixberechnung erhalten:
822
27 Mehrfädige Algorithmen P-Scan-2(x) 1 n = x.l¨a nge 2 sei y[1 . . n] ein neues Feld 3 P-Scan-2-Aux(x, y, 1, n) 4 return y P-Scan-2-Aux(x, y, i, j) 1 if i = = j 2 y[i] = x[i] 3 else k = (i + j)/2 4 spawn P-Scan-2-Aux(x, y, i, k) 5 P-Scan-2-Aux(x, y, k + 1, j) 6 sync 7 parallel for l = k + 1 to j 8 y[l] = y[k] ⊗ y[l] c. Begründen Sie, warum die Prozedur P-Scan-2 korrekt arbeitet und bestimmen Sie ihre Arbeit, Zeitspanne und Parallelität. Wir können sowohl P-Scan-1 als auch P-Scan-2 verbessern, indem wir zweimal über die Daten laufen, um die ⊗-Präfixberechnung auszuführen. Im ersten Durchlauf über die Daten sammeln wir die Terme für verschiedene zusammenhängende Teilfelder von x in einem Hilfsfeld t und im zweiten Durchlauf über die Daten verwenden wir die Terme in t, um das endgültige Ergebnis y zu berechnen. Der folgende Pseudocode implementiert diese Strategie, wobei wir aber verschiedene Ausdrücke weggelassen haben, die Sie sich selbst überlegen sollen: P-Scan-3(x) 1 n = x.l¨a nge 2 seien y[1 . . n] und t[1 . . n] neue Felder 3 y[1] = x[1] 4 if n > 1 5 P-Scan-Up(x, t, 2, n) 6 P-Scan-Down(x[1], x, t, y, 2, n) 7 return y P-Scan-Up(x, t, i, j) 1 if i = = j 2 return x[i] 3 else 4 k = (i + j)/2 5 t[k] = spawn P-Scan-Up(x, t, i, k) 6 rechts = P-Scan-Up(x, t, k + 1, j) 7 sync // fülle die Lücke aus 8 return
Problemstellungen zu Kapitel 27 P-Scan-Down(v, x, t, y, i, j) 1 if i = = j 2 y[i] = v ⊗ x[i] 3 else 4 k = (i + j)/2 5 spawn P-Scan-Down( , x, t, y, i, k) , x, t, y, k + 1, j) 6 P-Scan-Down( 7 sync
823
// fülle die Lücke aus // fülle die Lücke aus
d. Füllen Sie die drei Lücken in der Zeile 8 von P-Scan-Up und den Zeilen 5 und 6 von P-Scan-Down aus. Begründen Sie, warum die Prozedur PScan-3 mit den von Ihnen eingefügten Ausdrücken korrekt arbeitet. (Hinweis: Beweisen Sie, dass der Wert v, der in P-Scan-Down(v, x, t, y, i, j) übergeben wird, die Gleichung v = x[1] ⊗ x[2] ⊗ · · · ⊗ x[i − 1] erfüllt.) e. Analysieren Sie die Arbeit, die Zeitspanne und die Parallelität von P-Scan-3. 27-5 Multithreading einer einfachen Schablonen-Berechnung In der Informatik finden wir viele Algorithmen, in denen die Einträge eines Feldes berechnet werden müssen, wobei diese von den Werten bestimmter, bereits berechneter benachbarter Einträge und anderen Informationen, die sich während der Berechnung nicht verändern, abhängen. Das Muster der benachbarten Einträge, die jeweils zur Berechnung eines Eintrags benötigt werden, ändert sich während der Berechnung nicht und wird Schablone genannt. Abschnitt 15.4 beispielsweise stellt einen Schablonen-Algorithmus zur Berechnung einer längsten gemeinsamen Teilsequenz vor, in dem der Wert der Eintrags c[i, j] nur von den Werten in c[i − 1, j], c[i, j − 1] und c[i − 1, j − 1] und den Werten xi and yj innerhalb der beiden Eingabesequenzen abhängt. Die Eingabesequenzen verändern sich während der Berechnung nicht und der Algorithmus füllt das zweidimensionale Feld c in einer Reihenfolge aus, dass er den Eintrag c[i, j] berechnet, nachdem alle drei Einträge c[i − 1, j], c[i, j − 1] und c[i − 1, j − 1] berechnet worden sind. In dieser Problemstellung untersuchen wir, wie wir geschachtelte Parallelität verwenden können, um eine einfache Schablonen-Berechnung auf einem n × n-Feld A zu parallelisieren, in der der Wert, der für A[i, j] berechnet werden soll, nur von Werten in A[i , j ] mit i ≤ i und j ≤ j abhängen – natürlich gilt i = i oder j = j. Anders formuliert, der Wert eines Eintrags hängt nur von Werten ab, die oberhalb und/oder links von ihm liegen, sowie von Daten außerhalb des Feldes. Zudem setzen wir in dieser Problemstellung voraus, dass, wenn wir einmal die Einträge, von denen A[i, j] abhängt, ausgefüllt haben, wir A[i, j] in Zeit Θ(1) ausfüllen können (wie dies in der Prozedur LCS-Length aus Abschnitt 15.4 beispielsweise der Fall ist). Wir können das n × n-Feld A wie folgt in vier n/2 × n/2-Teilfelder partitionieren: A11 A12 A= . (27.11) A21 A22 Sie können nun feststellen, dass wir das Teilfeld A11 rekursiv ausfüllen können, da es von keinem Eintrag aus den anderen drei Teilfeldern abhängt. Sobald A11
824
27 Mehrfädige Algorithmen vollständig berechnet ist, können wir logisch parallel A12 und A21 rekursiv ausfüllen, da sie nicht voneinander abhängen, sondern nur von A11 . Schlussendlich können wir A22 rekursiv ausfüllen. a. Geben Sie einen mehrfädigen Pseudocode an, der diese einfache SchablonenBerechnung durchführt, indem er einen auf der Zerlegung (27.11) und auf der gerade geführten Diskussion basierenden Teile-und-Beherrsche-Algorithmus Simple-Stencil verwendet. (Machen Sie sich keine Gedanken über den Basisfall, der von der speziellen Schablone abhängt.) Geben Sie die Rekursionsgleichungen für die Arbeit und die Zeitspanne von diesem Algorithmus als Funktion in n an und lösen Sie diese. Wie groß ist die Parallelität? b. Modifizieren Sie Ihre Lösung aus Teil (a) in der Art, dass das n × n-Feld in neun n/3×n/3-Teilfelder partitioniert wird, wobei wieder mit möglichst hoher Parallelität rekursiv abgestiegen wird. Analysieren Sie diesen Algorithmus. Um wie viel größer oder kleiner ist die Parallelität von diesem Algorithmus verglichen mit dem Algorithmus aus Teil (a)? c. Verallgemeinern Sie Ihre Lösungen aus den Teilen (a) und (b) wie folgt. Wählen Sie eine ganze Zahl b ≥ 2. Partitionieren Sie ein n × n-Feld in b2 Teilfelder der Größe n/b × n/b und steigen Sie mit möglichst hoher Parallelität rekursiv ab. Wie groß ist die Arbeit, die Zeitspanne und die Parallelität Ihres Algorithmus als Funktion in n und b? Begründen Sie, warum die Parallelität für diesen Ansatz für jede Wahl von b ≥ 2 in o(n) sein muss. (Hinweis: Zeigen Sie für diesen letzten Punkt, dass für jede Wahl von b ≥ 2 der Exponent von n in der Parallelität echt kleiner als 1 ist.) d. Geben Sie den Pseudocode eines mehrfädigen Algorithmus für diese einfache Schablonen-Berechnung an, die eine Parallelität von Θ(n/ lg n) erreicht. Begründen Sie über die Begriffe der Arbeit und der Zeitspanne, dass das Problem tatsächlich eine inhärente Parallelität von Θ(n) besitzt – wie es sich also herausstellt, führt uns die Teile-und-Beherrsche-Natur unseres mehrfädigen Pseudocodes nicht zu dieser maximalen Parallelität.
27-6 Randomisierte mehrfädige Algorithmen Genau wie bei gewöhnlichen seriellen Algorithmen wollen wir manchmal randomisierte mehrfädige Algorithmen implementieren. Diese Problemstellung untersucht, wie wir die verschiedenen Performanzmaße anpassen können, um das erwartete Verhalten solcher Algorithmen zu beschreiben. Sie verlangt zudem von Ihnen, einen mehrfädigen Algorithmus für randomisiertes Quicksort zu entwerfen und zu analysieren. a. Erklären Sie, wie wir das Arbeitsgesetz (27.2), das Zeitspannengesetz (27.3) und die Greedy-Scheduler-Schranke (27.4) modifizieren müssen, damit Sie auf Erwartungswerte anwendbar sind, d. h. für den Fall, dass TP , T1 und T∞ Zufallsvariablen sind. b. Betrachten Sie einen randomisierten mehrfädigen Algorithmus, bei dem T1 = 104 und T10000 = 1 in 1% der Fälle gilt, aber T1 = T10000 = 109 in 99% der Fälle. Begründen Sie, dass die Beschleunigung eines randomisierten
Kapitelbemerkungen zu Kapitel 27
825
mehrfädigen Algorithmus durch E [T1 ] /E [TP ] definiert sein sollte und nicht durch E [T1 /TP ]. c. Begründen Sie, dass die Parallelität eines randomisierten mehrfädigen Algorithmus durch das Verhältnis E [T1 ] /E [T∞ ] definiert sein sollte. d. Parallelisieren Sie den auf Seite 180 vorstellten Randomized-QuicksortAlgorithmus mithilfe geschachtelter Parallelität. (Parallelisieren Sie nicht Randomized-Partition.) Geben Sie den Pseudocode Ihres P-RandomizedQuicksort-Algorithmus an. e. Analysieren Sie Ihren mehrfädigen Algorithmus für randomisiertes Quicksort. (Hinweis: Schauen Sie sich die Analyse von Randomized-Select auf Seite 216 an.)
Kapitelbemerkungen Parallelrechner, Modelle für Parallelrechner und algorithmische Modelle für parallele Programmierung finden wir in den letzten Jahren in den verschiedensten Formen vor. Frühere Editionen dieses Buches enthielten Materialien über Sortiernetzwerke und das PRAM-Modell (Parallel Random-Access Machine). Das datenparallele Modell [48, 168] ist ein anderes populäres algorithmisches Programmiermodell, das Operationen auf Vektoren und Matrizen als Basisoperationen enthält. Graham [149] und Brent [55] zeigten, dass es Scheduler gibt, die die Schranke aus Theorem 27.1 erreichen. Eager, Zahorjan und Lazowska [98] zeigten, dass jeder GreedyScheduler diese Schranke erreicht und schlugen die Methodik zur Analyse paralleler Algorithmen vor, in der Arbeit und Zeitspanne (wenn auch nicht unter diesen Bezeichnungen) benutzt werden. Blelloch [47] entwickelte ein algorithmisches, auf den Begriffen der Arbeit und der Zeitspanne (das er „Tiefe“ der Berechnung nannte) basierendes Programmiermodell für datenparallele Programmierung. Blumofe und Leiserson [52] stellten einen verteilten Scheduling-Algorithmus für dynamisches Multithreading vor, der auf randomisiertem „Arbeitsentzug“ basierte, und zeigten, dass der Algorithmus die Schranke E [TP ] ≤ T1 /P + O(T∞ ) erreicht. Arora, Blumofe und Plaxton [19] und Blelloch, Gibbons und Matias [49] stellten auch beweisbar gute Algorithmen zum Scheduling dynamischer mehrfädiger Berechnungen vor. Der mehrfädige Pseudocode und das Programmiermodell sind stark beeinflusst durch das Cilk-Projekt [51, 118] am MIT and die Cilk++-Erweiterung [71] von C++ der Cilk Arts, Inc. Viele der mehrfädigen Algorithmen aus diesem Kapitel erschienen in den unveröffentlichten Vorlesungsskripten von Leiserson and Prokop und wurden in Cilk oder Cilk++ implementiert. Der mehrfädige Algorithmus zum Sortieren durch Mischen wurde inspiriert durch ein Algorithmus von Akl [12]. Der Begriff der sequentielle Konsistenz geht auf Lamport [223] zurück.
28
Operationen auf Matrizen
Da Operationen auf Matrizen zentral für das wissenschaftliche Rechnen sind, haben effiziente Algorithmen zum Arbeiten mit Matrizen viele praktische Anwendungen. Dieses Kapitel konzentriert sich darauf, wie wir Matrizen multiplizieren und Systeme linearer Gleichungen lösen können. Die Grundlagen zu Matrizen finden Sie in Anhang D. Abschnitt 28.1 zeigt, wie wir ein lineares Gleichungssystem mithilfe der LUP-Zerlegung lösen können. Anschließend untersucht Abschnitt 28.2 die enge Beziehung zwischen dem Multiplizieren und dem Invertieren von Matrizen. Schließlich diskutiert Abschnitt 28.3 die wichtige Klasse der symmetrischen positiv definiten Matrizen und zeigt, wie wir diese benutzen können, um eine Lösung eines überbestimmten linearen Gleichungssystems so zu bestimmen, dass die Summe der quadratischen Fehler minimal ist. Eine wichtige, in der Praxis auftretende Frage ist die nach der numerischen Stabilität. Aufgrund der begrenzten Genauigkeit der Gleitkommadarstellung in heutigen Rechnern können Rundungsfehler bei numerischen Berechnungen im Verlaufe der Rechnung verstärkt werden, was zu fehlerhaften Resultaten führt; wir bezeichnen eine solche Berechnung als numerisch instabil. Wenngleich wir die numerische Stabilität gelegentlich kurz ansprechen, vertiefen wir dieses Problem in diesem Kapitel nicht. Wir verweisen Sie auf das exzellente Buch von Golub und Van Loan [144], in dem Stabilitätsfragen ausführlich diskutiert werden.
28.1
Lösen linearer Gleichungssysteme
Unzählige Anwendungen haben Systeme linearer Gleichungen zu lösen. Wir können ein lineares Gleichungssystem in Form einer Matrizengleichung schreiben, in der jedes Matrix- oder Vektorelement zu einem Körper, in der Regel zu dem Körper der reellen Zahlen R, gehört. Dieser Abschnitt diskutiert, wie wir lineare Gleichungssysteme mithilfe einer Methode lösen können, die unter dem Namen LUP-Zerlegung bekannt ist. Wir starten mit einem linearen Gleichungssystem in n Unbekannten x1 , x2 , . . . , xn : a11 x1 + a12 x2 + · · · + a1n xn = b1 , a21 x1 + a22 x2 + · · · + a2n xn = b2 , .. . an1 x1 + an2 x2 + · · · + ann xn = bn .
(28.1)
Eine Lösung der Gleichungen (28.1) ist eine Menge von Werten x1 , x2 , . . . , xn , die alle Gleichungen gleichzeitig erfüllen. In diesem Abschnitt behandeln wir nur den Fall, in dem es genau n Gleichungen in n Unbekannten gibt.
828
28 Operationen auf Matrizen
Wir können die Gleichungen (28.1) als Matrix-Vektor-Gleichung ⎞⎛ ⎞ ⎛ ⎞ ⎛ a11 a12 · · · a1n x1 b1 ⎜ a21 a22 · · · a2n ⎟ ⎜ x2 ⎟ ⎜ b2 ⎟ ⎟⎜ ⎟ ⎜ ⎟ ⎜ ⎜ .. .. . . .. ⎟ ⎜ .. ⎟ = ⎜ .. ⎟ ⎝ . . . . ⎠⎝ . ⎠ ⎝ . ⎠ an1 an2 · · · ann
xn
bn
schreiben, oder äquivalent dazu als Ax = b
(28.2)
mit A = (aij ), x = (xi ) und b = (bi ). Wenn die Matrix A regulär ist, dann besitzt sie eine Inverse A−1 und der Lösungsvektor ergibt sich durch x = A−1 b .
(28.3)
Wir können wie folgt beweisen, dass x eine eindeutige Lösung der Gleichung (28.2) ist. Wenn x und x zwei Lösungen sind, dann gilt Ax = Ax = b und x = Ix = (A−1 A)x = A−1 (Ax) = A−1 (Ax ) = (A−1 A)x = Ix = x , wobei I die Einheitsmatrix darstellt. In diesem Abschnitt werden wir uns vorwiegend mit dem Fall beschäftigen, dass A regulär ist, was äquivalent (wegen Theorem D.1) dazu ist, dass der Rang von A gleich der Anzahl n der Unbekannten ist. Es gibt jedoch andere Fälle, die eine kurze Diskussion verdienen. Wenn die Anzahl der Gleichungen geringer als die Anzahl n der Unbekannten ist – d. h. wenn der Rang von A kleiner als n ist –, dann ist das System unterbestimmt. Ein unterbestimmtes Gleichungssystem besitzt in der Regel unendlich viele Lösungen; es kann jedoch auch vorkommen, dass ein solches Gleichungssystem überhaupt keine Lösung besitzt, nämlich dann, wenn die Gleichungen inkonsistent sind. Wenn die Anzahl der Gleichungen größer als die Anzahl n der Unbekannten ist, so ist das System überbestimmt und es müssen keine Lösungen existieren. Abschnitt 28.3 widmet sich dem wichtigen Problem, gute Näherungslösungen von überbestimmten linearen Gleichungssystemen zu bestimmen. Lassen Sie uns zu unserem Problem zurückkommen, das System Ax = b mit n Gleichungen und n Unbekannten zu lösen. Wir könnten A−1 berechnen und anschließend A−1 mit b multiplizieren, was gemäß Gleichung (28.3) zu x = A−1 b führt. Dieser Ansatz leidet in der Praxis unter numerischer Instabilität. Zum Glück ist ein anderer Ansatz – die LUP-Zerlegung – numerisch stabil und hat darüber hinaus den Vorteil, in der Praxis schneller zu sein.
28.1 Lösen linearer Gleichungssysteme
829
Überblick über die LUP-Zerlegung Die Idee, die sich hinter der LUP-Zerlegung versteckt, besteht darin, drei n×n-Matrizen L, U und P zu berechnen, für die P A = LU
(28.4)
gilt, wobei • L eine untere Einheitsdreiecksmatrix, • U eine obere Dreiecksmatrix und • P eine Permutationsmatrix ist. Wir bezeichnen die Matrizen L, U und P , die die Gleichung (28.4) erfüllen, als eine LUP-Zerlegung der Matrix A. Wir werden zeigen, dass jede reguläre Matrix A eine solche Zerlegung besitzt. Der Vorteil, den wir erhalten, wenn wir eine LUP-Zerlegung der Matrix A berechnen, besteht darin, dass wir lineare Gleichungssysteme viel leichter lösen können, wenn sie Dreiecksform haben, wie dies sowohl für die Matrix L als auch für die Matrix U der Fall ist. Wenn wir einmal eine LUP-Zerlegung von A gefunden haben, können wir die Gleichung (28.2) Ax = b lösen, indem wir lediglich lineare Gleichungssysteme in Dreiecksform lösen. Multiplizieren wir beide Seiten von Ax = b mit P , so erhalten wir die äquivalente Gleichung P Ax = P b, was nach Übung D.1-3 dem Permutieren der Gleichungen (28.1) gleichkommt. Verwenden wir unsere Zerlegung (28.4), so erhalten wir LU x = P b . Wir können diese Gleichung nun lösen, indem wir zwei lineare Gleichungssysteme in Dreiecksform lösen. Lassen Sie uns y = U x definieren, wobei x der gesuchte Lösungsvektor ist. Zuerst lösen wir das untere Dreiecksgleichungssystem Ly = P b
(28.5)
nach dem unbekannten Vektor y durch „Vorwärtseinsetzen“. Haben wir nach y aufgelöst, lösen wir anschließend das obere Dreiecksgleichungssystem Ux = y
(28.6)
nach der Unbekannten x durch „Rückwärtseinsetzen“. Da die Permutationsmatrix P invertierbar ist (siehe Übung D.2-3), erhalten wir durch Multiplikation der beiden Seiten der Gleichung (28.4) mit P −1 die Gleichung P −1 P A = P −1 LU , sodass A = P −1 LU
(28.7)
830
28 Operationen auf Matrizen
gilt. Somit ist der Vektor x die Lösung von Ax = b: Ax = P −1 LU x = P −1 Ly = P −1 P b =b.
(wg. Gleichung (28.7)) (wg. Gleichung (28.6)) (wg. Gleichung (28.5))
Unser nächster Schritt besteht darin, dass wir zeigen, wie Vorwärts- und Rückwärtseinsetzen arbeiten; wir gehen dann das Problem an, die LUP-Zerlegung selbst zu bestimmen.
Vorwärts- und Rückwärtseinsetzen Das Vorwärtseinsetzen kann das untere Dreiecksgleichungssystem (28.5) in Zeit Θ(n2 ) lösen, wenn L, P und b gegeben sind. Der Einfachheit halber stellen wir die Permutation P kompakt durch ein Feld π[1 . . n] dar. Für i = 1, 2, . . . , n gibt das Element π[i] an, dass Pi,π[i] = 1 und Pij = 0 für j = π[i] gilt. Folglich steht in Zeile i und Spalte j von P A das Element aπ[i],j und P b hat bπ[i] als sein i-tes Element. Da L eine untere Einheitsdreiecksmatrix ist, können wir die Gleichung (28.5) in die Form y1 l21 y1 + y2 l31 y1 + l32 y2 +
y3
ln1 y1 + ln2 y2 + ln3 y3 + · · · + yn
= = = .. . =
bπ[1] , bπ[2] , bπ[3] ,
bπ[n]
umschreiben. Die erste Gleichung sagt uns, dass y1 = bπ[1] gilt. Kennen wir den Wert von y1 , so können wir sie in die zweite Gleichung einsetzen, was zu y2 = bπ[2] − l21 y1 führt. Nun können wir y1 und y2 in die dritte Gleichung einsetzen, wodurch wir y3 = bπ[3] − (l31 y1 + l32 y2 ) erhalten. Allgemein setzen wir y1 , y2 , . . . , yi−1 „vorwärts“ in die i-te Gleichung ein, um nach yi = bπ[i] −
i−1 j=1
aufzulösen.
lij yj
28.1 Lösen linearer Gleichungssysteme
831
Nachdem wir nach y aufgelöst haben, lösen wir Gleichung (28.6) nach x durch Rückeinsetzen auf, das dem Vorwärtseinsetzen gleicht. Beim Rückwärtseinsetzen lösen wir zuerst die n-te Gleichung und arbeiten uns rückwärts bis zur ersten Gleichung vor. Wie das Vorwärtseinsetzen läuft dieser Prozess in Zeit Θ(n2 ). Da U eine obere Dreiecksmatrix ist, können wir das Gleichungssystem (28.6) in die Form u11 x1 +u12 x2 +· · · + u22 x2 +· · · +
u1,n−2 xn−2 + u2,n−2 xn−2 +
u1,n−1 xn−1 + u2,n−1 xn−1 +
u1n xn = y1 , u2n xn = y2 , .. . un−2,n−2 xn−2 +un−2,n−1 xn−1 +un−2,n xn = yn−2 , un−1,n−1 xn−1 +un−1,n xn = yn−1 , un,n xn = yn .
umschreiben. Somit können wir sukzessive nach xn , xn−1 , . . . , x1 auflösen: xn = yn /un,n , xn−1 = (yn−1 − un−1,n xn )/un−1,n−1 , xn−2 = (yn−2 − (un−2,n−1 xn−1 + un−2,n xn ))/un−2,n−2 , .. . oder allgemein ⎛ xi = ⎝yi −
n
⎞ uij xj ⎠ /uii .
j=i+1
Sind P , L, U und b gegeben, so löst die Prozedur LUP-Solve das Gleichungssystem durch Kombination von Vorwärts- und Rückwärtseinsetzen nach x auf. Der Pseudocode setzt voraus, dass die Dimension n im Attribut L.zeilen enthalten und die Permutationsmatrix P durch das Feld π dargestellt ist. LUP-Solve(L, U, π, b) 1 n = L.zeilen 2 seien x und y zwei neue Vektoren der Länge n 3 for i = 1 to n 4 yi = bπ[i] − i−1 j=1 lij yj 5 for i = n downto 1 n 6 xi = yi − j=i+1 uij xj /uii 7 return x Die Prozedur LUP-Solve löst in den Zeilen 3–4 mittels Vorwärtseinsetzen nach y und anschließend in den Zeilen 5–6 mittels Rückwärtseinsetzen nach x auf. Da die
832
28 Operationen auf Matrizen
Summenbildungen in jeder der for-Schleifen eine implizite Schleife enthalten, ist die Laufzeit Θ(n2 ). Betrachten Sie beispielsweise das System ⎛ ⎞ ⎛ ⎞ 120 3 ⎝3 4 4⎠x = ⎝7⎠ 563 8 linearer Gleichungen, in dem also ⎛ ⎞ 120 A = ⎝3 4 4⎠ , 563 ⎛ ⎞ 3 b = ⎝7⎠ 8 ist. Wir wollen nach der Unbekannten x auflösen. Die LUP-Zerlegung ist gegeben durch ⎛ ⎞ 1 00 L = ⎝ 0,2 1 0 ⎠ , 0,6 0,5 1 ⎛ ⎞ 5 6 3 U = ⎝ 0 0,8 −0,6 ⎠ , 0 0 2,5 ⎛ ⎞ 001 P = ⎝1 0 0⎠ . 010 (Sie können nachprüfen, dass P A = LU erfüllt ist.) Durch Vorwärtseinsetzen lösen wir Ly = P b nach y auf: ⎞⎛ ⎞ ⎛ ⎞ ⎛ 8 y1 1 00 ⎝ 0,2 1 0 ⎠ ⎝ y2 ⎠ = ⎝ 3 ⎠ , 7 0,6 0,5 1 y3 wodurch wir
⎛
⎞ 8 y = ⎝ 1,4 ⎠ 1,5
erhalten, indem wir zuerst y1 , danach y2 und schließlich y3 berechnen. Durch Rückwärtseinsetzen lösen wir U x = y nach x auf: ⎛ ⎞⎛ ⎞ ⎛ ⎞ 5 6 3 8 x1 ⎝ 0 0,8 −0,6 ⎠ ⎝ x2 ⎠ = ⎝ 1,4 ⎠ , x3 0 0 2,5 1,5
28.1 Lösen linearer Gleichungssysteme
833
wodurch wir das gesuchte Ergebnis ⎛ ⎞ −1,4 x = ⎝ 2.2 ⎠ 0,6 erhalten, indem wir zuerst x3 , danach x2 und schließlich x1 berechnen.
Bestimmen einer LU-Zerlegung Wir haben gerade gezeigt, dass, wenn wir eine LUP-Zerlegung für eine reguläre Matrix A bestimmen können, Vorwärts- und Rückwärtseinsetzen das System Ax = b linearer Gleichungen lösen kann. Wir zeigen nun, wie wir eine LUP-Zerlegung für A effizient bestimmen können. Wir beginnen mit dem Fall, dass A eine reguläre n × n-Matrix ist und P fehlt, oder besser gesagt, P = In gilt. In diesem Fall haben wir eine Faktorisierung A = LU zu bestimmen. Wir bezeichnen die beiden Matrizen L und U als eine LUZerlegung von A. Wir verwenden einen Prozess, der unter dem Namen Gauß’sches Eliminationsverfahren bekannt ist, um eine LU-Zerlegung zu erzeugen. Wir subtrahieren zunächst Vielfache der ersten Gleichung von den anderen Gleichungen, um so die erste Variable aus diesen Gleichungen verschwinden zu lassen. Danach subtrahieren wir Vielfache der zweiten Gleichung von der dritten und den nachfolgenden Gleichungen, sodass die erste und zweite Variable aus ihnen eliminiert sind. Wir setzen diesen Prozess fort, bis das verbliebene System eine obere Dreiecksform hat – die so entstandene Matrix ist die Matrix U . Die Matrix L wird aus den Zeilenmultiplikatoren bestimmt, durch die die Variablen eliminiert wurden. Unser Algorithmus zur Implementierung dieser Strategie ist rekursiv. Wir wollen eine LU-Zerlegung einer regulären n × n-Matrix A konstruieren. Wenn n = 1 gilt, sind wir fertig, da wir in diesem Fall L = I1 und U = A wählen können. Im Falle n > 1 spalten wir A in vier Teile: ⎞ ⎛ a11 a12 · · · a1n ⎜ a21 a22 · · · a2n ⎟ ⎟ ⎜ A=⎜ . . . . ⎟ ⎝ .. .. . . .. ⎠ =
an1 an2 · · · ann a11 wT , v A
wobei v = (v2 , v3 , . . . , vn ) = (a21 , a31 , . . . , an1 ) ein (n − 1)-Spaltenvektor ist, wT ein (n − 1)-Zeilenvektor mit w = (w1 , w3 , . . . , wn ) = (a12 , a13 , . . . , a1n ) und A eine (n − 1) × (n − 1)-Matrix. Anschließend können wir A faktorisieren: a11 wT A= v A 1 0 a11 wT = . (28.8) v/a11 In−1 0 A − vwT /a11
834
28 Operationen auf Matrizen
(Sie können die Korrektheit der Gleichungen überprüfen, indem Sie einfach ausmultiplizieren.) Die Nullen in der ersten und zweiten Matrix in Gleichung (28.8) sind Zeilenbeziehungsweise Spaltenvektoren der Dimension n − 1. Der Term vwT /a11 , der durch Bilden des Vektorproduktes von v und w und durch Division jedes Elementes des Ergebnisvektors durch a11 entsteht, ist eine (n − 1) × (n − 1)-Matrix, deren Dimension der Dimension der Matrix A entspricht, von der sie subtrahiert wird. Die resultierende (n − 1) × (n − 1)-Matrix A − vwT /a11
(28.9)
wird als Schur-Komplement von A bezüglich a11 bezeichnet. Wir behaupten, dass, wenn A regulär ist, das Schur-Komplement auch regulär ist. Weshalb? Nehmen Sie an, das Schur-Komplement, das eine (n − 1) × (n − 1)-Matrix ist, wäre singulär. Dann besitzt es aufgrund des Theorems D.1 einen Zeilenrang, der echt kleiner als n − 1 ist. Da die unteren n − 1 Einträge der ersten Spalte der Matrix wT a11 0 A − vwT /a11 alle 0 sind, müssen die unteren n − 1 Zeilen dieser Matrix einen Zeilenrang haben, der echt kleiner als n − 1 ist. Der Zeilenrang der gesamten Matrix ist deshalb echt kleiner als n. Wenden wir Übung D.2-8 auf Gleichung (28.8) an, so sehen wir, dass der Rang von A echt kleiner als n ist. Aus Theorem D.1 leiten wir daraus den Widerspruch ab, dass A singulär ist. Weil das Schur-Komplement regulär ist, können wir nun eine LU-Zerlegung für diese Matrix bestimmen. Lassen Sie uns A − vwT /a11 = L U voraussetzen, wobei L eine untere Einheitsdreiecksmatrix und U eine obere Dreiecksmatrix ist. Mit Matrizenrechnung erhalten wir A= = =
1 v/a11 1 v/a11
a11 wT 0 A − vwT /a11 0 a11 wT In−1 0 L U 0 a11 wT L 0 U
1 0 v/a11 In−1
= LU , was uns die LU-Zerlegung liefert. (Beachten Sie, dass mit L auch L eine untere Einheitsdreiecksmatrix und mit U auch U eine obere Dreiecksmatrix ist.) Selbstverständlich funktioniert diese Methode für a11 = 0 nicht, da in diesem Falle eine Division durch 0 auftritt. Sie funktioniert ebenfalls nicht, wenn das am weitesten links
28.1 Lösen linearer Gleichungssysteme
835
liegende obere Element des Schur-Komplements A − vwT /a11 den Wert 0 hat, da wir im nächsten Schritt der Rekursion durch diesen Wert teilen. Die Elemente, durch die wir während der LU-Zerlegung teilen, werden Pivotelemente genannt. Sie belegen die Diagonalelemente der Matrix U . Der Grund, weshalb wir eine Permutationsmatrix P in die LUP-Zerlegung einbeziehen, besteht darin, dass wir dadurch Matrixdivisionen durch Nullelemente vermeiden können. Das Verwenden von Permutationen zur Vermeidung der Division durch 0 (oder durch kleine Zahlen) wird als Pivotstrategie bezeichnet. Eine wichtige Klasse von Matrizen, für die die LU-Zerlegung immer korrekt funktioniert, ist die Klasse der symmetrischen positiv definiten Matrizen. Solche Matrizen erfordern keine Pivotstrategie, sodass wir die oben erläuterte rekursive Strategie anwenden können, ohne uns Gedanken über die Division durch 0 machen zu müssen. Wir werden dieses Resultat zusammen mit anderen Ergebnissen in Abschnitt 28.3 beweisen. Unser Code für die LU-Zerlegung einer Matrix A folgt der eben geschilderten rekursiven Strategie, mit dem kleinen Unterschied, dass eine Iterationsschleife die Rekursion ersetzt. (Diese Transformation ist eine Standardoptimierung für „endrekursive“ Prozeduren – also für solche Prozeduren, bei denen die letzte Operation ein rekursiver Aufruf ihrer selbst ist. Siehe Problemstellung 7-4.) Die Prozedur setzt voraus, dass das Attribut A.zeilen die Dimension von A speichert. Wir initialisieren die Matrix U mit Nullen unterhalb der Diagonale und die Matrix L mit Einsen auf ihrer Diagonale und Nullen oberhalb der Diagonale. Jede Iteration arbeitet auf einer quadratischen Teilmatrix und benutzt das jeweils obere linke Element als Pivotelement, um die jeweiligen Vektoren v und w sowie das Schur-Komplement, auf der die nächste Iteration dann arbeitet, zu berechnen. LU-Decomposition(A) 1 n = A.zeilen 2 seien L und U neue n × n Matrizen 3 initialisiere U mit Nullen unterhalb der Diagonale 4 initialisiere L mit Einsen auf der Diagonale und Nullen oberhalb der Diagonale 5 for k = 1 to n 6 ukk = akk 7 for i = k + 1 to n // aik speichert vi 8 lik = aik /akk 9 uki = aki // aki speichert wi 10 for i = k + 1 to n 11 for j = k + 1 to n 12 aij = aij − lik ukj 13 return L und U Die in Zeile 5 beginnende äußere for-Schleife iteriert einmal pro Rekursionsschritt. Innerhalb dieser Schleife setzt Zeile 6 das Pivotelement auf ukk = akk . Die for-Schleife der Zeilen 7–9 (die nicht ausgeführt wird, wenn k = n gilt) verwendet die Vektoren v und w, um L und U zu aktualisieren. Zeile 8 bestimmt die unterhalb der Diagonale liegenden Elemente von L und speichert vi /akk in lik ab. Zeile 9 berechnet die oberhalb der Diagonale liegenden Elemente von U und speichert wi in uki ab. Schließlich
836
28 Operationen auf Matrizen
2 3 1 5 6 13 5 19 2 19 10 23 4 10 11 31 (a)
2
3 1 5 3 4 2 4 1 16 9 18 2 4 9 21 (b)
2 3 1 5 3 4 2 4 1 4 1 2 2 1 7 17 (c)
2 3 1 2
3 1 4 2 4 1 1 7 (d)
5 4 2 3
⎛
⎞ ⎛ ⎞⎛ ⎞ 2 3 1 5 1000 2315 ⎜ 6 13 5 19 ⎟ ⎜ 3 1 0 0 ⎟ ⎜ 0 4 2 4 ⎟ ⎝ 2 19 10 23 ⎠ = ⎝ 1 4 1 0 ⎠ ⎝ 0 0 1 2 ⎠ 4 10 11 31 2171 0003 A
L
U
(e) Abbildung 28.1: Die Arbeitsweise von LU-Decomposition. (a) Die Matrix A. (b) Das Element a11 = 2 im schwarzen Kreis ist das Pivotelement, die schattierte Spalte enthält v/a11 und die schattierte Zeile ist wT . Die bis dahin berechneten Elemente von U befinden sich oberhalb der horizontalen Linie, die Elemente von L sind links von der vertikalen Linie zu finden. Die Schur-Komplement-Matrix A − vwT /a11 belegt die untere rechte Seite. (c) Wir arbeiten nun auf der Schur-Komplement-Matrix, die in Teil (b) erzeugt wurde. Das Element a22 = 4 im schwarzen Kreis ist das Pivotelement und die schattierte Spalte und die schattierte Zeile sind v/a22 bzw. wT (bei der Zerlegung des Schur-Komplements). Linien unterteilen die Matrix in die bisher berechneten Elemente von U (oberhalb), die bis dahin berechneten Elemente von L (links) und das neue Schur-Komplement (unten rechts). (d) Nach dem nächsten Schritt ist die Matrix A faktorisiert. (Das Element 3 innerhalb des neuen Schur-Komplements wird zu einem Teil von U , wenn die Rekursion terminiert. (e) Die Faktorisierung A = LU .)
berechnen die Zeilen 10–12 die Elemente des Schur-Komplements und speichert sie in der Matrix A ab. (Wir müssen in Zeile 12 nicht durch akk teilen, da wir dies bereits bei der Berechnung von lik in Zeile 8 getan haben.) Weil Zeile 12 dreifach verschachtelt ist, benötigt LU-Decomposition Zeit Θ(n3 ). Abbildung 28.1 illustriert die Arbeitsweise von LU-Decomposition. Sie zeigt eine Standardoptimierung der Prozedur, in der wir die signifikanten Elemente von L und U in-place in die Matrix A speichern. Das heißt, wir können einen direkten Zusammenhang zwischen jedem Element aij und entweder lij (im Falle i > j) oder uij (im Falle i ≤ j) herstellen und die Matrix A so aktualisieren, dass sie sowohl L als auch U enthält, wenn die Prozedur terminiert. Um den Pseudocode dieses optimierten Algorithmus aus dem obigen Pseudocode zu erhalten, haben Sie einfach nur jeden Verweis auf l oder u durch einen Verweis auf a zu ersetzen. Sie können sehr einfach überprüfen, dass diese Transformation die Korrektheit wahrt.
Berechnung einer LUP-Zerlegung Im Allgemeinen müssen wir bei der Lösung linearer Gleichungssysteme Ax = b geeignete Nichtdiagonalelemente von A als Pivotelemente auswählen, um Divisionen durch 0 zu vermeiden. Durch 0 dividieren wäre natürlich verheerend. Aber wir sollten es auch
28.1 Lösen linearer Gleichungssysteme
837
vermeiden, durch einen kleinen Wert zu dividieren – selbst wenn A regulär ist –, da sich dadurch numerische Instabilitäten ergeben können. Deshalb versuchen wir, Pivotelemente mit großen Werten zu finden. Die Mathematik hinter der LUP-Zerlegung ist der der LU-Zerlegung ähnlich. Erinnern Sie sich daran, dass eine reguläre n × n-Matrix A gegeben ist und wir die Permutationsmatrix P , eine untere Einheitsdreiecksmatrix L und eine obere Dreiecksmatrix U bestimmen wollen, sodass P A = LU gilt. Bevor wir die Matrix A wie bei der LUZerlegung zerlegen, verschieben wir ein von 0 verschiedenes Element, zum Beispiel ak1 , von einer beliebigen Stelle in der ersten Spalte an die Position (1, 1) der Matrix. Wegen der numerischen Stabilität wählen wir ak1 als das Element aus der ersten Spalte, das den größten Absolutwert hat. (Die erste Spalte enthält nicht nur Nullen, denn ansonsten wäre A singulär, da ihre Determinante wegen der Theoreme D.4 und D.5 gleich 0 wäre.) Um das Gleichungssystem zu wahren, vertauschen wir die erste mit der k-ten Zeile, was äquivalent dazu ist, A mit einer Permutationsmatrix Q von links her zu multiplizieren (siehe Übung D.1-4). Folglich können wir QA in der Form ak1 wT QA = v A schreiben, wobei v = (a21 , a31 , . . . , an1 ) gilt, a11 durch das Element ak1 ersetzt ist, wT = (ak2 , ak3 , . . . , akn )T gilt und A eine (n − 1) × (n − 1)-Matrix ist. Wegen ak1 = 0, können wir nun im Wesentlichen dieselben Umformungen wie im Falle der LU-Zerlegung ausführen, wobei aber jetzt sichergestellt ist, dass wir nicht durch 0 dividieren: QA = =
ak1 wT v A
1 0 v/ak1 In−1
wT ak1 0 A − vwT /ak1
.
Wie wir bei der LU-Zerlegung gesehen haben, ist das Schur-Komplement A − vwT /ak1 ebenfalls regulär, wenn A regulär ist. Deshalb können wir rekursiv eine LUP-Zerlegung bestimmen, die aus einer unteren Einheitsdreiecksmatrix L , einer oberen Dreiecksmatrix U und einer Permutationsmatrix P besteht, sodass P (A − vwT /ak1 ) = L U gilt. Definieren Sie die Matrix 1 0 P = Q, 0 P die eine Permutationsmatrix ist, weil sie das Produkt zweier Permutationsmatrizen ist (siehe Übung D.1-4). Wir haben nun
838
28 Operationen auf Matrizen
PA = = = = = =
1 0 0 P 1 0 0 P
QA
1 P v/ak1 1 P v/ak1 1 P v/ak1 1 P v/ak1
wT 1 0 ak1 v/ak1 In−1 0 A − vwT /ak1 0 ak1 wT P 0 A − vwT /ak1 0 ak1 wT In−1 0 P (A − vwT /ak1 ) 0 ak1 wT In−1 0 L U 0 ak1 wT L 0 U
= LU , woraus sich die LU-Zerlegung ergibt. Da L eine untere Einheitsdreiecksmatrix ist, gilt dies auch für L, und weil U eine obere Dreiecksmatrix ist, ist U ebenfalls eine. Beachten Sie, dass wir bei diesem Vorgehen, anders als bei der LU-Zerlegung, sowohl den Spaltenvektor v/ak1 als auch das Schur-Komplement A − vwT /ak1 mit der Permutationsmatrix P multiplizieren müssen. Der Pseudocode für die LUP-Zerlegung sieht wie folgt aus: LUP-Decomposition(A) 1 n = A.zeilen 2 sei π[1 . . n] ein neues Feld 3 for i = 1 to n 4 π[i] = i 5 for k = 1 to n 6 p=0 7 for i = k to n 8 if |aik | > p 9 p = |aik | 10 k = i 11 if p = = 0 12 error “singuläre Matrix” 13 vertausche π[k] mit π[k ] 14 for i = 1 to n 15 vertausche aki mit ak i 16 for i = k + 1 to n 17 aik = aik /akk 18 for j = k + 1 to n 19 aij = aij − aik akj
28.1 Lösen linearer Gleichungssysteme
839
Wie bei LU-Decomposition ersetzt unsere Prozedur LUP-Decomposition für die LUP-Zerlegung die Rekursion durch eine Iterationsschleife. Als eine Verbesserung gegenüber einer direkten Implementierung der Rekursion verwalten wir die Permutationsmatrix P dynamisch in einem Feld π, wobei π[i] = j bedeutet, dass die i-te Zeile von P eine 1 in Spalte j besitzt. Zudem implementieren wir den Code so, dass die Berechnung von L und U in-place in der Matrix A erfolgt. Folglich gilt lij falls i > j , aij = falls i ≤ j , uij wenn die Prozedur terminiert. Abbildung 28.2 illustriert, wie LUP-Decomposition eine Matrix zerlegt. Die Zeilen 3–4 initialisieren das Feld π mit der Identität. Die in der Zeile 5 beginnende äußere for-Schleife implementiert die Rekursion. Bei jedem Durchlauf der äußeren Schleife bestimmen die Zeilen 6–10 das Element ak k mit dem größten absoluten Wert, das sich in der aktuellen ersten Spalte (Spalte k) der (n− k + 1)× (n− k + 1)-Matrix befindet, deren LUP-Zerlegung bestimmt werden soll. Wenn alle in der aktuellen ersten Spalte vorkommenden Elemente 0 sind, melden die Zeilen 11–12, dass die Matrix singulär ist. Zum Pivotieren vertauschen wir in Zeile 13 den Eintrag π[k ] mit π[k] und in den Zeilen 14–15 die k-te mit der k -ten Zeile von A, wodurch akk zum Pivotelement wird. (Es werden die kompletten Zeilen vertauscht, da in der obigen Methode nicht nur A −vwT /ak1 , sondern auch v/ak1 mit P multipliziert wird.) Schließlich wird das Schur-Komplement in den Zeilen 16–19 so wie in den Zeilen 7–12 der Prozedur LU-Decomposition bestimmt, mit dem Unterschied dass die Operation hier in-place arbeitet. Aufgrund ihrer dreifach verschachtelten Schleifenstruktur besitzt die Prozedur LUPDecomposition eine Laufzeit von Θ(n3 ), was gleich der Laufzeit der Prozedur LUDecomposition ist. Folglich kostet die Pivotstrategie höchstens einen konstanten Faktor an Rechenzeit.
Übungen 28.1-1 Lösen Sie die Gleichung ⎞ ⎛ ⎞⎛ ⎞ ⎛ 3 100 x1 ⎝ 4 1 0 ⎠ ⎝ x2 ⎠ = ⎝ 14 ⎠ −7 −6 5 1 x3 durch Vorwärtseinsetzen. 28.1-2 Bestimmen sie eine LU-Zerlegung der Matrix ⎛ ⎞ 4 −5 6 ⎝ 8 −6 7 ⎠ . 12 −7 12
840
28 Operationen auf Matrizen
1 2 3 4
2 3 5 –1
0 3 5 –2
2 0,6 4 –2 4 2 3,4 –1
3 2 1 4
5 3 2 –1
5 3 0 –2
(a) 5 0,6 0,4
5 0 –2
4 2 1,6 –3,2 0,4 –0,2
3 1 2
5 0,4 0,6
4
–0,2 –1
4,2 –0,6 (d)
4
–0,2 –1
1 2 4
5
5
2 –2 0,6 –1
3 2 1 4
5 5 0,6 0 0,4 –2 –0,2 –1
(b)
3 2 1
3
4 4 2 3,4
4
2
0,4 –2 0,4 –0,2 0,6 0 1,6 –3,2 –0,2 0,5 4 –0,5 (g)
5 –2 0
4 2 1,6 –3,2 0,4 –,2 4,2 –0,6 (c)
4 2 0,4 –0,2 1,6 –3,2
3 1 2
5 0,4 0,6
5 –2 0
4,2 –0,6
4
–0,2 0,5
(e) 3 1 4 2
5
5
4 2 0,4 –0,2 1,6 –3,2 4
–0,5
4
2
(f) 4
2
3
0,4 –2 0,4 –0,2 –0,2 0,5 4 –0,5 0,6 0 1,6 –3,2 (h)
1 4 2
5
5
0,4 –2 –0,2 0,5 0,6 0
0,4 –0,2 4 –0,5 0,4 –3 (i)
⎛
⎞⎛ ⎞ ⎛ ⎞⎛ ⎞ 0010 2 0 2 0,6 1 0 0 0 5 5 4 2 ⎜ 1 0 0 0 ⎟ ⎜ 3 3 4 −2 ⎟ ⎜ 0,4 1 0 0 ⎟ ⎜ 0 −2 0,4 −0,2 ⎟ ⎝ 0 0 0 1 ⎠ ⎝ 5 5 4 2 ⎠ = ⎝ −0,2 0,5 1 0 ⎠ ⎝ 0 0 4 −0,5 ⎠ 0100 −1 −2 3,4 −1 0,6 0 0,4 1 0 0 0 −3 P
A
L
U (j)
Abbildung 28.2: Die Arbeitsweise von LUP-Decomposition. (a) Die Eingabematrix A mit der Identität als Zeilenpermutation, die links der Matrix angegeben ist. Der erste Schritt des Algorithmus legt fest, dass das Element 5 im schwarzen Kreis in Zeile 3 zum Pivotelement der ersten Spalte werden soll. (b) Die Zeilen 1 und 3 werden vertauscht und die Permutation wird aktualisiert. Die schattiert dargestellte Spalte und die schattiert dargestellte Zeile stellen v beziehungsweise wT dar. (c) Der Vektor v wird durch v/5 und der rechte untere Teil der Matrix durch das Schur-Komplement ersetzt. Die Linien unterteilen die Matrix in drei Bereiche: Elemente von U (oberhalb), Elemente von L (links) und Elemente des Schur-Komplements (unten rechts). (d)-(f ) Der zweite Schritt. (g)-(i) Der dritte Schritt. Im vierten, und letzten, Schritt treten keine weiteren Veränderungen auf. (j) Die LUP-Zerlegung P A = LU .
28.2 Matrixinversion
841
28.1-3 Lösen Sie die Gleichung ⎛ ⎞⎛ ⎞ ⎛ ⎞ 154 x1 12 ⎝ 2 0 3 ⎠ ⎝ x2 ⎠ = ⎝ 9 ⎠ , 582 5 x3 indem Sie eine LUP-Zerlegung verwenden. 28.1-4 Beschreiben Sie die LUP-Zerlegung einer Diagonalmatrix. 28.1-5 Beschreiben Sie die LUP-Zerlegung einer Permutationsmatrix A und beweisen Sie, dass diese eindeutig ist. 28.1-6 Zeigen Sie, dass für alle n ≥ 1 eine singuläre n × n-Matrix existiert, die eine LU-Zerlegung besitzt. 28.1-7 Ist es in der Prozedur LU-Decomposition notwendig, die Iteration der äußeren for-Schleife auszuführen, wenn k = n ist? Wie ist es bei LUP-Decomposition?
28.2
Matrixinversion
Wenngleich wir in der Praxis im Allgemeinen die Matrixinversion nicht verwenden, um lineare Gleichungssysteme zu lösen, da wir stattdessen numerisch stabile Methoden wie beispielsweise die LUP-Zerlegung verwenden, müssen wir manchmal doch eine Matrixinverse berechnen. In diesem Abschnitt zeigen wir, wie wir mit der LUP-Zerlegung eine Matrixinverse berechnen können. Wir beweisen auch, dass die Matrizenmultiplikation und das Berechnen der Inversen einer Matrix in dem Sinne vergleichbar harte Probleme sind, dass wir (in Bezug auf die technischen Bedingungen) einen Algorithmus für die eine Aufgabe benutzen können, um die andere in derselben asymptotischen Laufzeit zu lösen. Folglich können wir Strassens Algorithmus (siehe Abschnitt 4.2) für die Matrizenmultiplikation auch für die Invertierung einer Matrix verwenden. Tatsächlich war Strassens Originalveröffentlichung durch das Problem motiviert zu zeigen, dass ein lineares Gleichungssystem viel schneller als durch das gewöhnliche Verfahren gelöst werden kann.
Berechnung einer Matrixinversen durch LUP-Zerlegung Setzen Sie voraus, dass wir eine LUP-Zerlegung einer Matrix A in Form von drei Matrizen L, U und P haben, sodass P A = LU gilt. Unter Verwendung von LUP-Solve können wir eine Gleichung der Form Ax = b in Zeit Θ(n2 ) lösen. Da die LUP-Zerlegung nur von A, nicht aber von b abhängig ist, können wir LUP-Solve auf einem zweiten linearen Gleichungssystem der Form Ax = b in zusätzlicher Zeit Θ(n2 ) laufen lassen. Es gilt allgemein, dass, wenn wir einmal die LUP-Zerlegung von A bestimmt haben, wir k Versionen der Gleichung Ax = b, die sich nur in b unterscheiden, in Zeit Θ(kn2 ) lösen können.
842
28 Operationen auf Matrizen
Wir können uns die Gleichung AX = In ,
(28.10)
die die Matrix X, d. h. die Inverse von A, definiert, als eine Menge von n verschiedenen Gleichungen der Form Ax = b denken. Für eine präzisere Formulierung, bezeichnen Sie die i-te Spalte von X mit Xi und erinnern Sie sich daran, dass der Einheitsvektor ei der i-ten Spalte von In entspricht. Wir können dann Gleichung (28.10) nach X auflösen, indem wir die LUP-Zerlegung von A verwenden, um jede Gleichung AXi = ei separat nach Xi aufzulösen. Wenn wir die LUP-Zerlegung einmal haben, können wir jede der n Spalten von X in Zeit Θ(n2 ) bestimmen und so die gesamte Matrix X in Zeit Θ(n3 ). Da wir die LUP-Zerlegung von A in Zeit Θ(n3 ) berechnen können, können wir die Inverse A−1 einer Matrix A in Zeit Θ(n3 ) bestimmen.
Matrizenmultiplikation und Matrixinversion Wir zeigen nun, dass die für die Matrizenmultiplikation erhaltenen theoretischen Beschleunigungen auch auf die Matrixinversion übertragbar sind. Tatsächlich beweisen wir sogar die etwas stärkere Aussage, dass wir n × n-Matrizen in Zeit O(M (n)) invertieren können, wenn M (n) die für die Multiplikation zweier n × n-Matrizen benötigte Zeit angibt. In diesem Sinne sind Matrixinversion und Matrizenmultiplikation äquivalent. Darüber hinaus können wir zwei n × n-Matrizen in Zeit O(I(n)) miteinander multiplizieren, wenn I(n) die Zeit zum Invertieren einer regulären n × n-Matrix angibt. Wir beweisen diese beiden Resultate in zwei separaten Theoremen. Theorem 28.1: (Multiplikation ist nicht härter als Inversion.) Wenn wir eine n × n-Matrix in Zeit I(n) invertieren können, wobei I(n) = Ω(n2 ) gilt und I(n) die Regularitätsbedingung I(3n) = O(I(n)) erfüllt, dann können wir zwei n × n-Matrizen in Zeit O(I(n)) miteinander multiplizieren. Beweis: Seien A und B zwei n × n-Matrizen, deren Matrizenprodukt C wir berechnen wollen. Wir definieren die 3n × 3n-Matrix D durch ⎞ ⎛ In A 0 D = ⎝ 0 In B ⎠ . 0 0 In Die Inverse von D ist ⎛ D−1
⎞ In −A AB = ⎝ 0 In −B ⎠ , 0 0 In
sodass wir das Produkt AB berechnen können, indem wir die obere rechte n × nTeilmatrix von D−1 extrahieren.
28.2 Matrixinversion
843
Wir können die Matrix D in Zeit Θ(n2 ) konstruieren, was in O(I(n)) ist, da wir I(n) = Ω(n2 ) vorausgesetzt haben, und aufgrund der Regularitätsbedingung an I(n) in Zeit O(I(3n)) = O(I(n)) invertieren. Somit gilt M (n) = O(I(n)). Beachten Sie, dass I(n) die Regularitätsbedingung erfüllt, wenn I(n) = Θ(nc lgd n) für Konstanten c > 0 und d ≥ 0 gilt. Der Beweis, dass Matrixinversion nicht härter als Matrizenmultiplikation ist, beruht auf Eigenschaften symmetrischer positiv definiter Matrizen, die in Abschnitt 28.3 bewiesen werden. Theorem 28.2: (Inversion ist nicht härter als Multiplikation.) Setzen Sie voraus, dass wir zwei reelle n × n-Matrizen in Zeit M (n) miteinander multiplizieren können, wobei M (n) = Ω(n2 ) gilt und M (n) die Regularitätsbedingung M (n + k) = O(M (n)) für beliebige k aus dem Bereich von 0 ≤ k ≤ n und die Regularitätsbedingung M (n/2) ≤ cM (n) für eine Konstante c < 1/2 erfüllt. Dann können wir die Inverse jeder beliebigen regulären reellen n × n-Matrix in Zeit O(M (n)) berechnen. Beweis: Wie beweisen das Theorem hier für reelle Matrizen. Die Übung 28.2-6 verlangt von Ihnen, den Beweis auf Matrizen, deren Einträge komplexe Zahlen sind, zu verallgemeinern. Wir können voraussetzen, dass n eine Potenz von 2 ist, da −1 −1 A 0 0 A = 0 Ik 0 Ik für beliebige k > 0 gilt. Wählen wir also k so, dass n+k eine Zweierpotenz ist, erweitern wir damit die Matrix auf die Dimension der nächsten Potenz von 2 und können das gesuchte Ergebnis A−1 aus der Antwort des so erweiterten Problems erhalten. Die erste Regularitätsbedingung an M (n) stellt sicher, dass diese Erweiterung keine Erhöhung der Laufzeit über einen konstanten Faktor hinaus verursacht. Lassen Sie uns vorübergehend voraussetzen, dass die n × n-Matrix A symmetrisch und positiv definit ist. Wir zerlegen sowohl die Matrix A als auch ihre Inverse A−1 in vier n/2 × n/2-Teilmatrizen: RT B CT −1 A= und A = . (28.11) UV C D Für das Schur-Komplement S = D − CB −1 C T
(28.12)
von A bezüglich B (wir werden in Abschnitt 28.3 mehr über diese Form des SchurKomplements kennenlernen) gilt dann −1 RT B + B −1 C T S −1 CB −1 −B −1 C T S −1 −1 A = , (28.13) = UV −S −1 CB −1 S −1
844
28 Operationen auf Matrizen
da AA−1 = In gilt, wie Sie überprüfen können, indem Sie die Matrizen ausmultiplizieren. Da A symmetrisch und positiv definit ist, implizieren Lemma 28.4 und Lemma 28.5 aus Abschnitt 28.3, dass B und S beide symmetrisch und positiv definit sind. Wegen Lemma 28.3 aus Abschnitt 28.3 existieren demnach die Inversen B −1 und S −1 und wegen Übung D.2-6 sind B −1 und S −1 symmetrisch, sodass (B −1 )T = B −1 und (S −1 )T = S −1 gilt. Wir können somit die Teilmatrizen R, T , U und V von A−1 wie folgt berechnen, wobei alle Matrizen die Größe n/2 × n/2 haben: 1. Bestimmen Sie die Teilmatrizen B, C, C T und D von A. 2. Berechnen Sie rekursiv die Inverse B −1 von B. 3. Berechnen Sie das Matrizenprodukt W = CB −1 und dann seine Transponierte W T , die gleich B −1 C T ist (wegen Übung D.1-2 und (B −1 )T = B −1 ). 4. Berechnen Sie das Matrizenprodukt X = W C T , das gleich CB −1 C T ist, und dann die Matrix S = D − X = D − CB −1 C T . 5. Berechnen Sie rekursiv die Inverse S −1 von S und setze V auf S −1 . 6. Berechnen Sie das Matrizenprodukt Y = S −1 W , das gleich S −1 CB −1 ist, und dann seine Transponierte Y T , die gleich B −1 C T S −1 ist (wegen Übung D.1-2, (B −1 )T = B −1 , und (S −1 )T = S −1 ). Setze T auf −Y T und U auf −Y . 7. Berechnen Sie das Matrizenprodukt Z = W T Y , das gleich B −1 C T S −1 CB −1 ist, und setze R auf B −1 + Z. Folglich können wir eine symmetrische positiv definite n × n-Matrix invertieren, indem wir in den Zeilen 2 und 5 zwei n/2 × n/2-Matrizen invertieren und in den Zeilen 3, 4, 6 und 7 vier Multiplikationen von n/2 × n/2-Matrizen durchführen; plus Kosten in Höhe von O(n2 ), um Teilmatrizen aus A zu extrahieren, um Teilmatrizen in A−1 einzufügen und eine konstante Anzahl von Additionen, Subtraktionen und Transpositionen auf n/2 × n/2-Matrizen auszuführen. Wir erhalten damit die Rekursionsgleichung I(n) ≤ 2I(n/2) + 4M (n/2) + O(n2 ) = 2I(n/2) + Θ(M (n)) = O(M (n)) . Die zweite Zeile gilt, da die zweite Regularitätsbedingung in der Aussage des Theorems die Ungleichung 4M (n/2) < 2M (n) impliziert und da wir M (n) = Ω(n2 ) vorausgesetzt haben. Die dritte Zeile folgt, da die zweite Regularitätsbedingung es uns erlaubt, Fall 3 der Mastermethode (Theorem 4.1) anzuwenden. Es bleibt zu beweisen, dass die asymptotische Laufzeit der Matrizenmultiplikation aus der Laufzeit der Matrixinversion abgeleitet werden kann, wenn A invertierbar, aber nicht symmetrisch und positiv definit ist. Die grundlegende Idee besteht darin, dass für eine beliebige reguläre Matrix A die Matrix AT A symmetrisch (wegen Übung D.1-2) und positiv definit (wegen Theorem D.6) ist. Der Trick besteht darin, das Problem der Inversion von A auf das Problem der Inversion von AT A zu reduzieren.
28.2 Matrixinversion
845
Die Reduktion basiert auf der Beobachtung, dass, wenn A eine reguläre n × n-Matrix ist, A−1 = (AT A)−1 AT , gilt, weil ((AT A)−1 AT )A = (AT A)−1 (AT A) = In gilt und die Matrixinverse eindeutig ist. Deshalb können wir A−1 berechnen, indem wir zuerst AT mit A multiplizieren, danach die symmetrische positiv definite Matrix AT A unter Verwendung des Teile-undBeherrsche-Algorithmus invertieren und schließlich das Resultat mit AT multiplizieren. Jeder dieser drei Schritte benötigt Zeit O(M (n)) und folglich können wir jede reguläre Matrix mit reellen Elementen in Zeit O(M (n)) invertieren. Der Beweis von Theorem 28.2 schlägt einen Weg zur Lösung der Gleichung Ax = b mithilfe der LU-Zerlegung ohne Pivotstrategie vor, solange A regulär ist. Wir multiplizieren beide Seiten der Gleichung mit AT , was zu (AT A)x = AT b führt. Diese Transformation berührt die Lösung x nicht, weil AT invertierbar ist, und wir deshalb die symmetrische positiv definite Matrix durch das Bestimmen einer LU-Zerlegung faktorisieren können. Wir benutzen dann das Vorwärts- und Rückwärtseinsetzen, um nach x mit der rechten Seite AT b aufzulösen. Obwohl diese Methode theoretisch korrekt ist, arbeitet die Prozedur LUP-Decomposition in der Praxis viel besser. Die erforderliche Anzahl arithmetischer Operationen ist bei der LUP-Zerlegung um einen konstanten Faktor geringer. Außerdem hat dieses Verfahren etwas bessere numerische Eigenschaften.
Übungen 28.2-1 Sei M (n) die zur Multiplikation von zwei n × n-Matrizen benötigte Zeit und S(n) die zum Quadrieren einer n × n-Matrix benötigte Zeit. Zeigen Sie, dass die Multiplikation und das Quadrieren im Wesentlichen denselben Schwierigkeitsgrad haben: Ein M (n)-Zeit-Algorithmus zur Matrizenmultiplikation impliziert einen O(M (n))-Zeit-Algorithmus zum Quadrieren und ein S(n)-ZeitAlgorithmus zum Quadrieren impliziert einen O(S(n))-Zeit-Algorithmus zur Matrizenmultiplikation. 28.2-2 Sei M (n) die zur Multiplikation von zwei n × n-Matrizen benötigte Zeit. Zeigen Sie, dass ein M (n)-Zeit-Algorithmus zur Matrizenmultiplikation einen O(M (n))-Zeit-Algorithmus für die LUP-Zerlegung impliziert. 28.2-3 Sei M (n) die zur Multiplikation von zwei n × n-Matrizen benötigte Zeit und D(n) die für das Bestimmen der Determinante einer n×n-Matrix erforderliche Zeit. Zeigen Sie, dass die Matrizenmultiplikation und das Berechnen der Determinante im Wesentlichen denselben Schwierigkeitsgrad besitzen: Ein M (n)Zeit Algorithmus zur Matrizenmultiplikation impliziert einen O(M (n))-ZeitAlgorithmus zur Bestimmung der Determinante und ein D(n)-Zeit-Algorithmus zum Bestimmen der Determinante impliziert einen O(D(n))-Zeit-Algorithmus für die Matrizenmultiplikation.
846
28 Operationen auf Matrizen
28.2-4 Sei M (n) die zur Multiplikation von zwei Booleschen n× n-Matrizen benötigte Zeit und T (n) die zum Bestimmen der transitiven Hülle einer Booleschen n×nMatrix benötigte Zeit (siehe Abschnitt 25.2). Zeigen Sie, dass ein M (n)-ZeitAlgorithmus zur Multiplikation Boolescher Matrizen einen O(M (n) lg n)-ZeitAlgorithmus zum Bestimmen der transitiven Hülle impliziert und ein T (n)Zeit-Algorithmus zum Bestimmen der transitiven Hülle einen O(T (n))-ZeitAlgorithmus zur Multiplikation Boolescher Matrizen impliziert. 28.2-5 Arbeitet der auf dem Theorem 28.2 basierende Algorithmus zur Matrixinversion korrekt, wenn die Matrixelemente dem Körper der ganzen Zahlen modulo 2 entnommen werden? Begründen Sie Ihre Antwort. 28.2-6∗ Verallgemeinern Sie den Algorithmus zur Matrixinversion aus Theorem 28.2 so, dass er mit Matrizen komplexer Zahlen arbeiten kann, und beweisen Sie, dass Ihre Verallgemeinerung korrekt arbeitet. (Hinweis: Verwenden Sie anstelle der Transponierten von A die konjugiert Transponierte A∗ , die wir aus der Transponierten von A erhalten, indem wir jeden Eintrag durch seinen konjugiert komplexen Wert ersetzen. Betrachten Sie anstelle symmetrischer Matrizen Hermitesche Matrizen, bei denen es sich um Matrizen handelt, für die A = A∗ gilt.)
28.3
Symmetrische positiv definite Matrizen und Minimierung der Summe der quadratischen Fehler bei überbestimmten Gleichungssystemen
Symmetrische positiv definite Matrizen besitzen viele interessante und wünschenswerte Eigenschaften. Zum Beispiel sind sie regulär und wir können die LU-Zerlegung auf ihnen ausführen, ohne uns Gedanken über Divisionen durch 0 machen zu müssen. In diesem Abschnitt werden wir einige grundlegende Eigenschaften symmetrischer positiv definiter Matrizen beweisen und eine interessante Anwendung zur Kurvenanpassung, bei der die Summe der quadratischen Fehler minimiert wird, untersuchen. Die erste Eigenschaft, die wir nun beweisen, ist die vielleicht grundlegendste. Lemma 28.3 Jede positiv definite Matrix ist regulär. Beweis: Betrachten Sie eine singuläre Matrix A. Dann existiert wegen Korollar D.3 ein von 0 verschiedener Vektor x, für den Ax = 0 gilt. Folglich gilt xT Ax = 0 und A kann nicht positiv definit sein.
28.3 Symmetrische positiv definite Matrizen, Summe der quadratischen Fehler
847
Der Beweis, dass wir die LU-Zerlegung auf einer symmetrischen positiv definiten Matrix A ausführen können, ohne durch 0 dividieren zu müssen, ist komplizierter. Wir starten mit dem Beweis von Eigenschaften bestimmter Teilmatrizen von A. Definieren Sie die k-te Hauptabschnittsmatrix von A als die Matrix Ak , die aus der Schnittmenge der ersten k Zeilen und der ersten k Spalten von A besteht. Lemma 28.4 Wenn A eine symmetrische positiv definite Matrix ist, dann ist jede Hauptabschnittsmatrix von A symmetrisch und positiv definit. Beweis: Dass jede Hauptabschnittsmatrix Ak symmetrisch ist, ist offensichtlich. Um die positive Definitheit von Ak zu beweisen, nehmen wir an, dass diese nicht vorliegen würde, und führen diese Annahme zum Widerspruch. Wenn Ak nicht positiv definit ist, dann existiert ein k-Vektor xk = 0, sodass xT k Ak xk ≤ 0 gilt. Sei A eine n × n-Matrix und Ak B T A= (28.14) B C für eine Teilmatrix B (die Größe (n − k) × k hat) und eine Teilmatrix C (der Größe T (n − k) × (n − k)). Definieren Sie den n-Vektor x = ( xT k 0 ) , in dem n − k Nullen nach xk folgen. Dann gilt
xk Ak B T 0 B C Ak xk = ( xT k 0) Bxk
xT Ax = ( xT k 0)
= xT k Ak xk ≤0, was der positiven Definitheit von A widerspricht.
Wir kommen nun zu einigen grundlegenden Eigenschaften des Schur-Komplements. Sei A eine symmetrische positiv definite Matrix und sei Ak eine k×k-Hauptabschnittsmatrix von A. Partitionieren Sie A erneut gemäß Gleichung (28.14). Wir verallgemeinern die Definition (28.9), indem wir das Schur-Komplement T S = C − BA−1 k B
(28.15)
von A bezüglich Ak definieren. (Wegen Lemma 28.4 ist Ak symmetrisch und positiv wegen Lemma 28.3 und S ist wohldefiniert.) Beachten definit; deshalb existiert A−1 k Sie, dass unsere frühere Definition (28.9) des Schur-Komplements konsistent mit der Definition (28.15) ist, indem wir k = 1 setzen.
848
28 Operationen auf Matrizen
Das nachfolgende Lemma zeigt, dass die Schur-Komplement-Matrizen von symmetrischen positiv definiten Matrizen selbst symmetrisch und positiv definit sind. Wir haben dieses Ergebnis in Theorem 28.2 verwendet und benötigten das zugehörige Korollar, um die Korrektheit der LU-Zerlegung für symmetrische positiv definite Matrizen zu beweisen. Lemma 28.5: (Schur-Komplement-Lemma) Wenn A eine symmetrische positiv definite Matrix und Ak eine k × k-Hauptabschnittsmatrix von A ist, dann ist das Schur-Komplement S von A bezüglich Ak symmetrisch und positiv definit. Beweis: Weil A symmetrisch ist, gilt dies auch für die Teilmatrix C. Das Produkt T BA−1 k B ist wegen Übung D.2-6 und S wegen Übung D.1-1 symmetrisch. Es bleibt zu zeigen, dass S positiv definit ist. Wir betrachten die Zerlegung von A, die durch Gleichung (28.14) gegeben ist. Für einen beliebigen von 0 verschiedenen Vektor x gilt xT Ax > 0 wegen der Voraussetzung, dass A positiv definit ist. Wir teilen x in zwei Teilvektoren y und z auf, die zu Ak bzw. C kompatibel sind. Da A−1 k existiert, gilt
y z T Ak y + B z = ( yT z T ) By + Cz
xT Ax = ( y T z T )
Ak B T B C
= y T Ak y + y T B T z + z T By + z T Cz −1 T −1 T T T T = (y + A−1 k B z) Ak (y + Ak B z) + z (C − BAk B )z , (28.16) (Sie können dies nachprüfen, indem Sie das Produkt ausmultiplizieren.) Die letzte Gleichung läuft auf die „quadratische Ergänzung“ der quadratischen Form hinaus (siehe Übung 28.3-2). Da xT Ax > 0 für jedes von 0 verschiedene x gilt, lassen Sie uns ein beliebiges von 0 T verschiedenes z nehmen, und dann y = −A−1 k B z wählen, wodurch der erste Term in Gleichung (28.16) eliminiert wird und somit T T z T (C − BA−1 k B )z = z Sz
als Wert des Ausdrucks übrigbleibt. Für jedes z = 0 gilt deshalb z T Sz = xT Ax > 0 und folglich ist S positiv definit.
Korollar 28.6 Die LU-Zerlegung einer symmetrischen positiv definiten Matrix führt niemals zu einer Division durch 0.
28.3 Symmetrische positiv definite Matrizen, Summe der quadratischen Fehler
849
Beweis: Sei A eine symmetrische positiv definite Matrix. Wir werden eine etwas stärkere Behauptung beweisen als die im Korollar aufgestellte: Jedes Pivotelement ist streng positiv. Das erste Pivotelement ist a11 . Sei e1 der erste Einheitsvektor, sodass a11 = eT 1 Ae1 > 0 gilt. Da der erste Schritt der LU-Zerlegung das Schur-Komplement von A bezüglich A1 = (a11 ) erzeugt, folgt aus Lemma 28.5 per Induktion, dass alle Pivotelemente positiv sind.
Minimierung der Summe der quadratischen Fehler Eine wichtige Anwendung symmetrischer positiv-definiter Matrizen tritt in Verbindung mit dem Anpassen von Kurven an eine gegebene Punktmenge auf. Setzen Sie voraus, wir hätten eine Menge von m Datenpunkten (x1 , y1 ), (x2 , y2 ), . . . , (xm , ym ) gegeben, wobei wir wissen, dass die yi -Werte mit Messfehlern behaftet sind. Wir wollen eine Funktion F (x) bestimmen, sodass für i = 1, 2, . . . , m die Approximationsfehler ηi = F (xi ) − yi
(28.17)
klein sind. Die Form der Funktion F hängt vom vorliegenden Problem ab. An dieser Stelle wollen wir voraussetzen, dass sie die Form einer linearen gewichteten Summe F (x) =
n
cj fj (x)
j=1
besitzt, wobei die Anzahl der Summanden n und die speziellen Basisfunktionen fj auf Basis der Kenntnisse über das vorliegende Problem gewählt werden. Eine oft anzutreffende Wahl ist fj (x) = xj−1 , was bedeutet, dass F (x) = c1 + c2 x + c3 x2 + · · · + cn xn−1 ein Polynom in x vom Grad n − 1 ist. Sind also m Datenpunkte (x1 , y1 ), (x2 , y2 ), . . . , (xm , ym ) gegeben, so wollen wir n Koeffizienten c1 , c2 , . . . , cn so bestimmen, dass sie die Approximationsfehler η1 , η2 , . . . , ηm minimieren. Wenn wir n = m wählen, können wir jedes yi in Gleichung (28.17) exakt berechnen. Ein solch hochgradiges F „passt sich an das Rauschen“ jedoch genauso wie die Daten an und liefert im Allgemeinen ein schlechtes Ergebnis, wenn wir sie dazu verwenden, um die y-Werte für vorher ungesehene x-Werte vorherzusagen. Gewöhnlich ist es besser, n signifikant kleiner als m zu wählen und zu hoffen, dass wir durch geeignete Wahl der Koeffizienten cj eine Funktion F erhalten, die die signifikanten Muster in den Datenpunkten herausfindet, ohne dem Rauschen übertriebene Aufmerksamkeit zu widmen. Für die Auswahl von n existieren einige theoretische Grundsätze, die jedoch über den Rahmen dieses Buches hinausgehen. Wie auch immer, wenn wir einmal einen Wert für n gewählt haben, der kleiner als m ist, erhalten wir ein überbestimmtes Gleichungssystem, dessen Lösung wir approximieren wollen. Wir zeigen nun, wie wir dies tun können.
850
28 Operationen auf Matrizen
Sei ⎛
⎞ f1 (x1 ) f2 (x1 ) . . . fn (x1 ) ⎜ f1 (x2 ) f2 (x2 ) . . . fn (x2 ) ⎟ ⎜ ⎟ A=⎜ ⎟ .. .. .. .. ⎝ ⎠ . . . . f1 (xm ) f2 (xm ) . . . fn (xm ) die Matrix mit Werten der Basisfunktionen an den gegebenen Punkten, d. h. es gilt aij = fj (xi ). Sei c = (ck ) der gesuchte n-Vektor der Koeffizienten. Dann ist ⎞⎛ ⎞ c1 f1 (x1 ) f2 (x1 ) . . . fn (x1 ) ⎜ f1 (x2 ) f2 (x2 ) . . . fn (x2 ) ⎟ ⎜ c2 ⎟ ⎜ ⎟⎜ ⎟ Ac = ⎜ ⎟ ⎜ .. ⎟ .. .. .. .. ⎝ ⎠⎝ . ⎠ . . . . f1 (xm ) f2 (xm ) . . . fn (xm ) cn ⎛ ⎞ F (x1 ) ⎜ F (x2 ) ⎟ ⎜ ⎟ =⎜ . ⎟ ⎝ .. ⎠ ⎛
F (xm ) der m-Vektor der „vorhergesagten Werte“ von y. Folglich ist η = Ac − y der m-Vektor der Approximationsfehler. Um die Approximationsfehler zu minimieren, wollen wir die Norm des Fehlervektors η minimieren, was zu einer Lösung der kleinsten Summe der Quadrate führt, da 2 !η! =
m
31/2 ηi2
i=1
gilt. Wegen !η!2 = !Ac − y!2 =
m i=1
⎛ ⎝
n
⎞2 aij cj − yi ⎠ ,
j=1
können wir !η! minimieren, indem wir !η!2 bezüglich ck ableiten und das Resultat anschließend gleich 0 setzen: ⎞ ⎛ m n 2 d !η! = 2⎝ aij cj − yi ⎠ aik = 0 . (28.18) dck i=1 j=1
28.3 Symmetrische positiv definite Matrizen, Summe der quadratischen Fehler
851
Die n Gleichungen (28.18) für k = 1, 2, . . . , n sind äquivalent zu der Matrizengleichung (Ac − y)T A = 0 oder (unter Verwendung von Übung D.1-2) äquivalent zu AT (Ac − y) = 0 , woraus AT Ac = AT y
(28.19)
folgt. In der Statistik wird diese Gleichung Normalgleichung genannt. Die Matrix AT A ist wegen Übung D.1-2 symmetrisch und, wenn A vollen Spaltenrang besitzt, nach Theorem D.6 auch positiv definit. Folglich existiert (AT A)−1 und die Lösung zur Gleichung (28.19) ist c = (AT A)−1 AT y = A+ y ,
(28.20) wobei die Matrix A+ = (AT A)−1 AT als die Pseudoinverse der Matrix A bezeichnet wird. Die Pseudoinverse verallgemeinert in natürlicher Weise den Begriff der Matrixinversen für den Fall, dass A nichtquadratisch ist. (Vergleichen Sie Gleichung (28.20) als die approximierte Lösung von Ac = y mit der Lösung A−1 b als exakte Lösung von Ax = b.) Als Beispiel für das Erzeugen einer Approximation mithilfe unserer Methode setzen wir voraus, dass wir 5 Datenpunkte (x1 , y1 ) = (−1, 2) , (x2 , y2 ) = (1, 1) , (x3 , y3 ) = (2, 1) , (x4 , y4 ) = (3, 0) , (x5 , y5 ) = (5, 3) gegeben haben, die in Abbildung 28.3 durch schwarze Punkte dargestellt sind. Wir wollen diese Punkte mithilfe eines quadratischen Polynoms F (x) = c1 + c2 x + c3 x2 approximieren. Wir starten mit der Matrix, die aus den Werten der Basisfunktionen besteht, ⎛ ⎞ ⎛ ⎞ 1 x1 x21 1 −1 1 ⎜ 1 x x2 ⎟ ⎜ 1 1 1 ⎟ ⎜ 2 2⎟ ⎜ ⎟ ⎜ ⎟ ⎜ ⎟ A = ⎜ 1 x3 x23 ⎟ = ⎜ 1 2 4 ⎟ , ⎜ ⎟ ⎜ ⎟ ⎝ 1 x4 x24 ⎠ ⎝ 1 3 9 ⎠ 1 5 25 1 x5 x25
852
28 Operationen auf Matrizen y 3,0 2,5 F(x) = 1,2 – 0,757x + 0,214x2
2,0 1,5 1,0 0,5 0,0 –2
–1
0
1
2
3
4
5
x
Abbildung 28.3: Eine Anpassung eines quadratischen Polynoms an die fünf Datenpunkte {(−1, 2), (1, 1), (2, 1), (3, 0), (5, 3)}, die die Summe der quadratischen Fehler minimiert. Die schwarzen Punkte stellen die Datenpunkte dar und die weißen Punkte sind die vom Polynom F (x) = 1,2 − 0,757x + 0,214x2 vorausgesagten Werte. Dieses quadratische Polynom minimiert die Summe der quadratischen Fehler. Jede schattierte Linie zeigt den Fehler für jeweils einen Datenpunkt.
deren Pseudoinverse durch ⎛ ⎞ 0,500 0,300 0,200 0,100 −0,100 A+ = ⎝ −0,388 0,093 0,190 0,193 −0,088 ⎠ 0,060 −0,036 −0,048 −0,036 0,060 gegeben ist. Multiplizieren wir A+ mit y, so erhalten wir den Koeffizientenvektor ⎛ ⎞ 1,200 c = ⎝ −0,757 ⎠ , 0,214 was dem quadratischen Polynom F (x) = 1,200 − 0,757x + 0,214x2 entspricht, das die beste quadratische Anpassung an die gegebenen Daten bezüglich der Summe der quadratischen Fehler ist. Vom praktischen Standpunkt aus gesehen, lösen wir die Normalgleichung (28.19), indem wir AT mit y multiplizieren und dann die LU-Zerlegung von AT A bestimmen. Wenn A vollen Rang besitzt, so ist die Matrix AT A mit Sicherheit regulär, da sie symmetrisch und positiv definit ist (siehe Übung D.1-2 und Theorem D.6).
Problemstellungen zu Kapitel 28
853
Übungen 28.3-1 Beweisen Sie, dass jedes Hauptdiagonalelement einer symmetrischen positiv definiten Matrix positiv ist. ab 28.3-2 Sei A = eine symmetrische positiv definite 2 × 2-Matrix. Beweisen Sie, bc dass deren Determinante ac− b2 positiv ist, indem Sie das „Quadrat ergänzen“, ähnlich wie im Beweis von Lemma 28.5. 28.3-3 Beweisen Sie, dass das maximale Element einer symmetrischen positiv definiten Matrix auf der Hauptdiagonale liegt. 28.3-4 Beweisen Sie, dass die Determinante von jeder Hauptabschnittsmatrix einer symmetrischen positiv definiten Matrix positiv ist. 28.3-5 Wir bezeichnen mit Ak die k-te Hauptabschnittsmatrix einer symmetrischen positiv definiten Matrix A. Beweisen Sie, dass det(Ak )/ det(Ak−1 ) das k-te Pivotelement der LU-Zerlegung ist, wobei per Vereinbarung det(A0 ) = 1 gilt. 28.3-6 Bestimmen Sie die Funktion der Form F (x) = c1 + c2 x lg x + c3 ex , die unter dem Gesichtspunkt der Summe der quadratischen Fehler die beste Anpassung an die Datenpunkte (1, 1), (2, 1), (3, 3), (4, 8) ist. 28.3-7 Zeigen Sie, dass die Pseudoinverse A+ die folgenden vier Gleichungen erfüllt: AA+ A = A , A+ AA+ = A+ , (AA+ )T = AA+ , (A+ A)T = A+ A .
Problemstellungen 28-1 Tridiagonale lineare Gleichungssysteme Betrachten Sie die tridiagonale Matrix ⎛ ⎞ 1 −1 0 0 0 ⎜ −1 2 −1 0 0 ⎟ ⎜ ⎟ A = ⎜ 0 −1 2 −1 0 ⎟ . ⎝ 0 0 −1 2 −1 ⎠ 0 0 0 −1 2
854
28 Operationen auf Matrizen a. Bestimmen Sie eine LU-Zerlegung von A. T b. Lösen Sie die Gleichung Ax = 1 1 1 1 1 durch Vorwärts- und Rückwärtseinsetzen. c. Bestimmen Sie die Inverse von A. d. Zeigen Sie, wie wir die Gleichung Ax = b für eine beliebige symmetrische positiv definite tridiagonale n × n-Matrix A und einen n-Vektor b in Zeit O(n) mithilfe einer LU-Zerlegung lösen können. Begründen Sie, warum jede Methode, die auf der Bildung von A−1 basiert, im schlechtesten Fall asymptotisch aufwendiger ist. e. Zeigen Sie, wie wir die Gleichung Ax = b für eine beliebige reguläre tridiagonale n×n-Matrix und einen n-Vektor b in Zeit O(n) mithilfe einer LUP-Zerlegung lösen können.
28-2 Splinefunktionen Eine praktische Methode zur Interpolation einer Punktmenge durch eine Kurve besteht darin, kubische Splinefunktionen zu verwenden. Wir haben eine Menge {(xi , yi ) : i = 0, 1, . . . , n} von n + 1 Wertepaaren gegeben, wobei x0 < x1 < · · · < xn gilt. Wir wollen eine stückweise-kubische Kurve (Spline) f (x) an die Punkte anpassen. Das heißt, die Kurve f (x) besteht für i = 0, 1, . . . , n − 1 aus n kubischen Polynomen fi (x) = ai + bi x + ci x2 + di x3 , wobei, wenn x in dem Bereich xi ≤ x ≤ xi+1 liegt, f (x) durch fi (x − xi ) gegeben ist. Die Punkte xi , an denen die kubischen Polynome „zusammengefügt“ werden, werden als Knoten bezeichnet. Der Einfachheit halber setzen wir für i = 0, 1, . . . , n voraus, dass die Gleichung xi = i gilt. Um die Stetigkeit von f (x) sicherzustellen, fordern wir, dass für i = 0, 1, . . . , n − 1 f (xi ) = fi (0) = yi , f (xi+1 ) = fi (1) = yi+1 erfüllt ist. Um sicherzustellen, dass f (x) hinreichend glatt ist, fordern wir die Stetigkeit der ersten Ableitung an jedem Knoten, d. h. (0) f (xi+1 ) = fi (1) = fi+1
für alle i = 0, 1, . . . , n − 2. a. Setzen Sie voraus, dass wir für i = 0, 1, . . . , n nicht nur die Wertepaare {(xi , yi )}, sondern auch die ersten Ableitungen Di = f (xi ) an jedem Knoten gegeben haben. Drücken Sie jeden Koeffizienten ai , bi , ci und di als Funktion in den Werten yi , yi+1 , Di und Di+1 aus. (Denken Sie daran, dass xi = i gilt.) Wie schnell können wir die 4n Koeffizienten aus den Wertepaaren und den ersten Ableitungen berechnen? Es bleibt zu klären, wie wir die ersten Ableitungen an den Knoten wählen müssen. Unsere Methode besteht darin, die Stetigkeit der zweiten Ableitungen an den Knoten zu fordern, d. h. (0) . f (xi+1 ) = fi (1) = fi+1
Kapitelbemerkungen zu Kapitel 28
855
für alle i = 0, 1, . . . , n − 2. Am ersten und letzten Knoten setzen wir voraus, dass f (x0 ) = f0 (0) = 0 und f (xn ) = fn−1 (1) = 0 ist; unter diesen Voraussetzungen ist f (x) eine natürliche kubische Splinefunktion. b. Verwenden Sie die Stetigkeitsbedingung der zweiten Ableitung, um für i = 1, 2, . . . , n − 1 die folgende Gleichung zu beweisen: Di−1 + 4Di + Di+1 = 3(yi+1 − yi−1 ) .
(28.21)
c. Zeigen Sie, dass folgende Gleichungen gelten: 2D0 + D1 = 3(y1 − y0 ) , Dn−1 + 2Dn = 3(yn − yn−1 ) .
(28.22) (28.23)
d. Formen Sie die Gleichungen (28.21)–(28.23) in eine Matrizengleichung um, die den Vektor D = D0 , D1 , . . . , Dn als Unbekannte enthält. Welche Attribute besitzt die Matrix in Ihrer Gleichung? e. Erklären Sie, warum eine natürliche kubische Splinefunktion eine Menge von n+1 Wertepaaren in Zeit O(n) interpolieren kann (siehe Problemstellung 28-1). f. Zeigen Sie, wie wir eine natürliche kubische Splinefunktion bestimmen können, die ein Menge von n + 1 Punkten (xi , yi ) mit x0 < x1 < · · · < xn interpoliert, auch wenn xi nicht notwendigerweise gleich i ist. Welche Matrizengleichung muss Ihre Methode lösen und wie schnell läuft ihr Algorithmus?
Kapitelbemerkungen Viele exzellente Lehrbücher beschreiben numerisches und wissenschaftliches Rechnen detaillierter, als der Platz in diesem Buch uns erlaubt hat. Die folgenden Lehrbücher sind besonders hervorzuheben: George und Liu [132], Golub und Van Loan [144], Press, Teukolsky, Vetterling und Flannery [283, 284] und Strang [323, 324]. Golub und Van Loan [144] diskutieren die numerische Stabilität. Sie zeigen, weshalb det(A) nicht notwendigerweise ein guter für die Stabilität einer @ Indikator @ n Matrix A ist. Stattdessen schlagen sie vor, !A!∞ @A−1 @∞ mit !A!∞ = max1≤i≤n j=1 |aij | zu verwenden. Sie beschäftigen sich auch mit der Frage, wie dieser Wert berechnet werden kann, ohne tatsächlich A−1 zu bestimmen. Das Gaußsche Eliminationsverfahren, auf dem die LU-Zerlegung und die LUP-Zerlegung beruhen, war die erste systematische Methode zur Lösung linearer Gleichungssysteme. Es war auch einer der ersten numerischen Algorithmen. Obwohl er bereits früher bekannt war, wird seine Entdeckung allgemein C. F. Gauß (1777–1855) zugeschrieben. Strassen zeigte in seiner berühmten Veröffentlichung [325], dass eine n × n-Matrix in Zeit O(nlg 7 ) invertiert werden kann. Winograd [358] bewies als erster, dass die Matrizenmultiplikation nicht härter als die Matrixinversion ist. Die Umkehrung des Beweises geht auf Aho, Hopcroft und Ullman [5] zurück. Eine andere wichtige Art der Matrixzerlegung ist die Singulärwertzerlegung (engl.: singular value decomposition) oder SVD. In der SVD wird eine m × n-Matrix A in
856
28 Operationen auf Matrizen
A = Q1 Σ QT 2 faktorisiert. Dabei ist Σ eine m×n-Matrix, die nur auf der Hauptdiagonale von 0 verschiedene Werte besitzt, Q1 eine m × m-Matrix mit orthonormalen Spalten und Q2 eine n × n-Matrix, deren Spalten ebenfalls orthonormal sind. Zwei Vektoren sind orthonormal, wenn ihr Skalarprodukt 0 ist und jeder Vektor die Norm 1 besitzt. Die Bücher von Strang [323, 324] sowie Golub und Van Loan [144] enthalten gute Darstellungen der SVD. Strang [324] bietet eine exzellente Darstellung symmetrischer positiv definiter Matrizen und der linearen Algebra im Allgemeinen.
29
Lineare Programmierung
Viele Probleme werden als Maximierung oder Minimierung einer Zielfunktion unter beschränkten Ressourcen und konkurrierenden Nebenbedingungen formuliert. Falls wir die Zielfunktion durch eine lineare Funktion bestimmter Variablen und die Nebenbedingungen an die Ressourcen durch lineare Gleichungen oder Ungleichungen dieser Variablen spezifizieren können, dann liegt ein lineares Programmierungsproblem vor. Lineare Programme kommen in einer Vielzahl praktischer Anwendungen vor. Wir beginnen mit der Untersuchung einer Anwendung aus der Wahlkampfpolitik.
Ein Beispiel aus der Politik Nehmen Sie an, Sie wären ein Politiker, der eine Wahl gewinnen möchte. Ihr Wahlkreis besteht aus drei Typen von Gebieten – urbanen, suburbanen und ländlichen. Diese Gebiete haben 100 000, 200 000 beziehungsweise 50 000 registrierte Wahlberechtigte. Wenngleich nicht alle registrierte Wähler wirklich zur Wahlurne gehen, ist es Ihr Ziel, dass wenigstens die Hälfte der registrierten Wähler eines jeden Gebietstypen Ihnen ihre Stimme gibt, damit Sie effektiv regieren können. Sie sind ehrlich und würden niemals politische Ideen unterstützen, an die Sie nicht glauben. Sie erkennen jedoch, dass bestimmte Themen in bestimmten Gebieten besonders dazu geeignet sind, Stimmen zu gewinnen. Ihre Hauptthemen sind der Bau neuer Straßen, Sicherheitspolitik, Beihilfen für die Landwirtschaft und eine Mineralölsteuer für die Verbesserung des öffentlichen Nahverkehrs. Aufgrund der Untersuchungen Ihrer Wahlkampfberater können Sie für jedes Ihrer Wahlkampfthemen einschätzen, wie viele Stimmen Sie in jeder Bevölkerungsschicht verlieren oder gewinnen, wenn Sie jeweils 1 000 Dollar an Werbemitteln für ein Thema einsetzen. Diese Informationen sind in der Tabelle in Abbildung 29.1 enthalten. Jeder Eintrag dieser Tabelle gibt die Anzahl der Stimmberechtigten (in Tausenden) aus urbanen, suburbanen beziehungsweise ländlichen Gebieten an, die durch die Aufwendung von 1 000 Dollar an Werbemitteln für ein bestimmtes Wahlkampfthema gewonnen werden können. Negative Einträge bedeuten, dass Stimmen verloren gehen würden. Ihre Aufgabe ist es, den minimalen Geldbetrag zu bestimmen, den Sie aufwenden müssen, um 50 000 Stimmen in den urbanen Gebieten, 100 000 Stimmen in den suburbanen Gebieten und 25 000 Stimmen in den ländlichen Gebieten zu gewinnen. Durch Ausprobieren (engl.: trial and error ) können Sie eine Strategie finden, mit der Sie die erforderlichen Stimmen bekommen, aber eine solche Strategie muss nicht die kostengünstigste sein. Sie könnten zum Beispiel in die Werbekampagnen für den Straßenbau 20 000 Dollar, für die Sicherheitspolitik 0 Dollar, für Landwirtschaftsbeihilfen 4 000 Dollar und für die Mineralölsteuer 9 000 Dollar stecken. In diesem Fall bekommen Sie 20(−2) + 0(8) + 4(0) + 9(10) = 50 Tausend Stimmen in den urbanen Gebieten, 20(5) + 0(2) + 4(0) + 9(0) = 100 Tausend Stimmen in den suburbanen Gebieten und
858
29 Lineare Programmierung
Wahlkampfthema Straßenbau Sicherheit Landwirtschaftsbeihilfe Mineralölsteuer
urban −2 8 0 10
suburban 5 2 0 0
ländlich 3 −5 10 −2
Abbildung 29.1: Die Auswirkungen von Wahlkampftaktiken auf die Wähler. Jeder Eintrag gibt die Anzahl der Wähler in Tausenden aus den urbanen, suburbanen und ländlichen Gebieten an, die durch den Einsatz von 1 000 Dollar Werbemitteln für ein bestimmtes Thema gewonnen werden können. Negative Einträge geben Stimmen an, die verloren gehen würden.
20(3) + 0(−5) + 4(10) + 9(−2) = 82 Tausend Stimmen in den ländlichen Gebieten. Sie würden in den urbanen und suburbanen Gebieten genau die gewünschte Anzahl von Stimmen und in den ländlichen Gebieten mehr Stimmen als notwendig bekommen. (Tatsächlich haben Sie in den ländlichen Gebieten sogar mehr Stimmen bekommen als es Wähler gibt!) Um diese Stimmen zu bekommen, müssten Sie 20+0+4+9 = 33 Tausend Dollar für Werbung bezahlen. Natürlich würden Sie sich fragen, ob Ihre Strategie die bestmögliche ist, d. h. ob Sie Ihre Ziele mit weniger Werbeaufwand hätten erreichen können. Weiteres Ausprobieren könnte Ihnen helfen, diese Frage zu beantworten. Aber hätten Sie nicht lieber eine systematische Methode, um derartige Fragen zu beantworten? Um eine solche Methode zu entwickeln, werden wir die Frage mathematisch formulieren. Wir führen vier Variablen ein: • x1 ist die Anzahl der Dollar in Tausenden, die für die Kampagne für den Straßenbau ausgegeben werden, • x2 ist die Anzahl der Dollar in Tausenden, die für die Kampagne für die Sicherheitspolitik ausgegeben werden, • x3 ist die Anzahl der Dollar in Tausenden, die für die Kampagne für Landwirtschaftsbeihilfen ausgegeben werden, • x4 ist die Anzahl der Dollar in Tausenden, die für die Werbung für die Mineralölsteuer ausgegeben werden. Die Forderung, mindestens 50 000 Stimmen in den urbanen Gebieten zu gewinnen, können wir durch die Ungleichung −2x1 + 8x2 + 0x3 + 10x4 ≥ 50
(29.1)
ausdrücken. Entsprechend schreiben wir die Forderung, mindestens 100 000 Stimmen in den suburbanen und mindestens 25 000 Stimmen in den ländlichen Gebieten zu gewinnen, in der Form 5x1 + 2x2 + 0x3 + 0x4 ≥ 100
(29.2)
29 Lineare Programmierung
859
und 3x1 − 5x2 + 10x3 − 2x4 ≥ 25 .
(29.3)
Jede Belegung der Variablen x1 , x2 , x3 , x4 , die die Ungleichungen (29.1)-(29.3) erfüllt, führt zu einer Strategie, mit der Sie in jeder der Bevölkerungsgruppen die notwendige Stimmenanzahl bekommen. Um die Kosten so gering wie möglich zu halten, wollen Sie den Werbeaufwand, d. h. den Ausdruck (29.4)
x1 + x2 + x3 + x4
minimieren. Zwar ist Negativwerbung etwas, was in politischen Kampagnen häufig vorkommt, jedoch gibt es keine Werbung mit negativen Kosten. Folglich fordern wir, dass x1 ≥ 0, x2 ≥ 0, x3 ≥ 0 und x4 ≥ 0
(29.5)
gilt. Kombinieren wir die Ungleichungen (29.1)-(29.3) und (29.5) mit der zu minimierenden Zielfunktion (29.4), so erhalten wir ein so genanntes „lineares Programm“. Wir schreiben dieses Problem in der Form minimiere x1 + x2 + x3 + x4
(29.6)
unter den Nebenbedingungen −2x1 + 8x2 + 0x3 + 10x4 ≥ 50 5x1 + 2x2 + 0x3 + 0x4 ≥ 100 3x1 − 5x2 + 10x3 − 2x4 ≥ 25 x1 , x2 , x3 , x4 ≥ 0.
(29.7) (29.8) (29.9) (29.10)
Die Lösung dieses linearen Programms liefert Ihnen eine optimale Strategie.
Allgemeine lineare Programme Beim allgemeinen Problem der linearen Programmierung wollen wir eine lineare Funktion unter einer Menge von linearen Nebenbedingungen optimieren. Für eine Menge reeller Zahlen a1 , a2 , . . . , an und eine Menge von Variablen x1 , x2 , . . . , xn ist eine lineare Funktion dieser Variablen durch f (x1 , x2 , . . . , xn ) = a1 x1 + a2 x2 + · · · + an xn =
n
aj xj
j=1
definiert. Wenn b eine reelle Zahl und f eine lineare Funktion ist, dann ist die Gleichung f (x1 , x2 , . . . , xn ) = b eine lineare Gleichung, und die Ungleichungen f (x1 , x2 , . . . , xn ) ≤ b
860
29 Lineare Programmierung
und f (x1 , x2 , . . . , xn ) ≥ b sind lineare Ungleichungen. Wir verwenden die allgemeine Bezeichnung lineare Nebenbedingungen sowohl für lineare Gleichungen als auch für lineare Ungleichungen. In der linearen Programmierung erlauben wir keine echten Ungleichungen. Formal ist ein lineares Programmierungsproblem das Problem, eine lineare Funktion unter einer endlichen Menge von linearen Nebenbedingungen entweder zu minimieren oder zu maximieren. Wenn wir minimieren müssen, nennen wir das lineare Programm ein lineares Minimierungsprogramm; wenn wir maximieren müssen, bezeichnen wir es als ein lineares Maximierungsprogramm. Der Rest dieses Kapitels befasst sich mit der Formulierung und Lösung linearer Programme. Wenngleich verschiedene Algorithmen mit polynomieller Laufzeit für lineare Programme entwickelt worden sind, werden wir sie in diesem Lehrbuch nicht betrachten. Stattdessen untersuchen wir den Simplexalgorithmus, den ältesten Algorithmus für lineare Programmierung. Der Simplexalgorithmus läuft im schlechtesten Fall nicht in polynomieller Zeit, ist jedoch in der Praxis recht effizient und weit verbreitet.
Lineare Programmierung im Überblick Um die Eigenschaften linearer Programme und Algorithmen für diese vorzustellen, ist es günstig, wenn wir die linearen Programme in einer kanonischen Form dartellen. Wir werden in diesem Kapitel zwei Formen benutzen, die Standardform und die Schlupfform. Wir werden beide in Abschnitt 29.1 exakt definieren. Vereinfacht gesagt, ist ein lineares Programm in Standardform die Maximierung einer linearen Funktion unter Beachtung linearer Ungleichungen, während ein lineares Programm in Schlupfform die Maximierung einer linearen Funktion unter Beachtung linearer Gleichungen ist. Wir werden in der Regel die Standardform für die Formulierung linearer Programme benutzen, wenngleich wir es als zweckdienlicher ansehen, die Schlupfform zu verwenden, wenn wir die Details des Simplexalgorithmus beschreiben. Vorerst richten wir unsere Aufmerksamkeit auf die Maximierung einer linearen Funktion in n Variablen unter Berücksichtigung einer Menge von m linearen Ungleichungen. Lassen Sie uns zunächst das folgende lineare Programm mit zwei Variablen betrachten: maximiere x1 + x2
(29.11)
unter den Nebenbedingungen 4x1 − x2 ≤ 8 2x1 + x2 ≤ 10 5x1 − 2x2 ≥ −2 x1 , x2 ≥ 0.
(29.12) (29.13) (29.14) (29.15)
Wir nennen jede Belegung der Variablen x1 und x2 , die alle Nebenbedingungen (29.12)(29.15) erfüllt, eine zulässige Lösung des linearen Programms. Wenn wir die Nebenbedingungen wie in Abbildung 29.2(a) in ein Koordinatensystem mit den kartesischen
29 Lineare Programmierung x2
1
5x
= x2 8
+ x1
+ 2x 1
x1 ≥ 0
+ x1
4x1 – x2 ≤ 8
– 2x
2
≥ –2
x2
861
= x2 4
x2≤ 10
x1
= x2
x1
+ x1
x2 ≥ 0
0
(a)
(b)
Abbildung 29.2: (a) Das durch (29.12)-(29.15) gegebene lineare Programm. Jede Nebenbedingung ist durch eine Gerade und eine Richtung dargestellt. Die Schnittmenge der Nebenbedingungen, das zulässige Gebiet, ist schattiert gekennzeichnet. (b) Die gepunkteten Linien zeigen die Punkte, für die der Wert der Zielfunktion 0, 4 bzw. 8 ist. Die optimale Lösung des linearen Programms ist x1 = 2 und x2 = 6 mit dem Zielfunktionswert 8.
Koordinaten x1 , x2 einzeichnen, so sehen wir, dass die Menge der zulässigen Lösungen (das schattierte Gebiet in der Abbildung) ein konvexes Gebiet1 in der Ebene bildet. Wir nennen dieses konvexe Gebiet den zulässigen Bereich und die Funktion, die wir maximieren wollen, die Zielfunktion. Prinzipiell könnten wir die Zielfunktion x1 + x2 für jeden Punkt innerhalb des zulässigen Bereichs auswerten. Wir nennen den Wert der Zielfunktion an einem bestimmten Punkt den Zielfunktionswert. Wir könnten dann einen Wert als optimale Lösung bestimmen, der den maximalen Zielfunktionswert hat. In diesem Beispiel (wie in den meisten linearen Programmen) enthält der zulässige Bereich unendlich viele Punkte. Daher benötigen wir eine effiziente Methode zur Bestimmung eines Punktes, der die Zielfunktion innerhalb des zulässigen Bereiches maximiert, ohne sie für alle Punkte des zulässigen Bereiches explizit auswerten zu müssen. Bei zwei Dimensionen können wir die Zielfunktion durch ein graphisches Verfahren optimieren. Die Menge der Punkte, für die x1 + x2 = z für irgendein z gilt, ist eine Gerade mit dem Anstieg −1. Für x1 + x2 = 0 erhalten wir die durch den Ursprung führende Gerade mit dem Anstieg −1 (siehe Abbildung 29.2(b)). Die Schnittmenge dieser Geraden mit dem zulässigen Bereich ist die Menge der zulässigen Lösungen, die den Zielfunktionswert 0 haben. In unserem Beispiel besteht diese Schnittmenge nur aus dem Punkt (0, 0). Allgemeiner ist die Menge der zulässigen Lösungen mit dem Zielfunktionswert z die Schnittmenge der Geraden x1 + x2 = z mit dem zulässigen Bereich. Abbildung 29.2(b) zeigt die Geraden x1 + x2 = 0, x1 + x2 = 4 und x1 + x2 = 8. Da der zulässige Bereich in Abbildung 29.2 beschränkt ist, muss es einen maximalen 1 Eine intuitive Definition eines konvexen Gebietes ergibt sich aus der Forderung, dass für jedes Paar von Punkten des Gebietes alle Punkte auf der sie verbindenden Strecke ebenfalls zum Gebiet gehören.
862
29 Lineare Programmierung
Wert z geben, für den die Schnittmenge der Geraden x1 + x2 = z mit dem zulässigen Bereich nichtleer ist. Jeder Punkt, für den dies gilt, ist eine optimale Lösung des linearen Programms. Im Beispiel ist dies der Punkt x1 = 2, x2 = 6 mit dem Zielfunktionswert 8. Es ist kein Zufall, dass eine optimale Lösung des linearen Programms an einer Ecke des zulässigen Bereichs liegt. Der maximale Wert von z, für den die Gerade x1 + x2 = z den zulässigen Bereich schneidet, muss auf dem Rand des zulässigen Bereichs liegen. Also ist der Schnitt dieser Geraden mit dem Rand des zulässigen Bereichs entweder eine einzelne Ecke oder eine Begrenzungslinie. Wenn der Schnitt eine einzelne Ecke ist, gibt es genau eine optimale Lösung und diese ist eine Ecke. Wenn der Schnitt eine Begrenzungslinie ist, dann muss jeder Punkt auf dieser Begrenzungslinie den gleichen Zielfunktionswert haben; insbesondere sind die beiden Endpunkte der Strecke optimale Lösungen. Da jeder Endpunkt der Begrenzungslinie eine Ecke ist, gibt es auch in diesem Fall eine optimale Lösung an einer Ecke. Obwohl wir lineare Programme mit mehr als zwei Variablen nicht einfach als einen Graphen zeichnen können, gilt die gleiche Intuition. Für drei Variablen entspricht jede Nebenbedingung einem Halbraum des dreidimensionalen Raums. Die Schnittmenge dieser Halbräume bildet den zulässigen Bereich. Die Menge der Punkte, für die die Zielfunktion einen bestimmten Wert z annimmt, ist nun eine Ebene (vorausgesetzt, dass es keine degenerierte Bedingungen gibt). Wenn alle Koeffizienten der Zielfunktion nichtnegativ sind, und wenn der Ursprung eine zulässige Lösung des linearen Programms ist, dann finden wir Punkte mit steigendem Wert der Zielfunktion, wenn wir diese Ebene orthogonal zur Zielfunktion vom Ursprung wegbewegen. (Wenn der Ursprung keine zulässige Lösung ist oder einige der Koeffizienten der Zielfunktion negativ sind, dann wird das intuitive Bild ein wenig komplizierter.) Wie in zwei Dimensionen muss die Menge der Punkte, die den optimalen Zielfunktionswert realisieren, eine Ecke des zulässigen Bereichs enthalten, da der zulässige Bereich konvex ist. Entsprechend korrespondiert im Falle von n Variablen jede Nebenbedingung einem Halbraum des n-dimensionalen Raums. Wir bezeichnen den zulässigen Bereich, der durch die Schnittmenge aller dieser Halbräume gegeben ist, als ein Simplex . Die Zielfunktion ist nun eine Hyperebene, und wegen der Konvexität wird auch in diesem Fall eine optimale Lösung an einem Eckpunkt des Simplex liegen. Der Simplexalgorithmus verwendet als Eingabe ein lineares Programm und gibt eine optimale Lösung zurück. Er beginnt an einer Ecke des Simplex und führt eine Reihe von Iterationen aus. In jeder Iteration bewegt er sich entlang einer Kante des Simplex von einer aktuellen Ecke zu einer benachbarten Ecke, deren Zielfunktionswert nicht kleiner (und in der Regel größer) als der der aktuellen Ecke ist. Der Simplexalgorithmus terminiert, wenn er ein lokales Maximum erreicht. Dies ist eine Ecke, für die alle Nachbarecken kleinere Zielfunktionswerte haben. Da der zulässige Bereich konvex und die Zielfunktion linear ist, ist dieses lokale Optimum in Wirklichkeit ein globales Optimum. In Abschnitt 29.4 werden wir ein als „Dualität“ bezeichnetes Konzept anwenden, um zu zeigen, dass die durch den Simplexalgorithmus zurückgegebene Lösung tatsächlich optimal ist. Obwohl die geometrische Sicht auf das Problem ein gutes intuitives Verständnis der Arbeitsweise des Simplexalgorithmus ermöglicht, werden wir uns nicht explizit auf diese beziehen, wenn wir in Abschnitt 29.3 die Details des Simplexalgorithmus darlegen.
29 Lineare Programmierung
863
Stattdessen verwenden wir einen algebraischen Zugang. Wir schreiben das gegebene lineare Programm zunächst in Schlupfform. Diese besteht aus einer Menge von linearen Gleichungen. Diese linearen Gleichungen drücken einige der Variablen, die als „Basisvariablen“ bezeichnet werden, durch andere Variablen aus, die „Nichtbasisvariablen“ genannt werden. Wir bewegen uns von einer Ecke zu einer anderen, indem wir eine Basisvariable zu einer Nichtbasisvariablen und eine Nichtbasisvariable zu einer Basisvariablen machen. Wir nennen diese Operation „Pivotieren“; algebraisch betrachtet ist es nichts anderes als eine Umformung des linearen Programms in eine äquivalente Schlupfform. Das oben angeführte Beispiel mit zwei Variablen war besonders einfach. Wir werden in diesem Kapitel verschiedene weitere Details ansprechen. Diese Themen umfassen die Identifizierung von linearen Programmen, die keine Lösungen haben, die keine endliche optimale Lösung haben beziehungsweise für die der Ursprung keine zulässige Lösung ist.
Anwendungen der linearen Programmierung Die lineare Programmierung hat eine Vielzahl von Anwendungen. Jedes Lehrbuch über Operations Research enthält eine Fülle von Beispielen zur linearen Programmierung. Lineare Programmierung ist ein Standardverfahren geworden, das den Studierenden in den meisten Wirtschaftsstudiengängen vermittelt wird. Das einführende Beispiel zu einer Wahlkampfstrategie ist ein typisches Beispiel. Zwei weitere Beispiele linearer Programmierung sind die folgenden: • Eine Fluggesellschaft möchte ihre Crews zusammenstellen. Von der zuständigen Luftfahrtbehörde werden viele Nebenbedingungen auferlegt, wie zum Beispiel die Beschränkung der Stundenzahl, die jedes Mitglied ohne Unterbrechung arbeiten darf, oder die Forderung, dass eine bestimmte Crew innerhalb eines Monats nur auf einem bestimmten Flugzeugtyp arbeiten darf. Die Fluggesellschaft will die Crews für alle Flüge so planen, dass so wenig Besatzungsmitglieder wie möglich gebraucht werden. • Eine Ölgesellschaft muss entscheiden, wo nach Öl gebohrt werden soll. Einer Bohrung an einem bestimmten Ort sind bestimmte Kosten und, auf der Basis geologischer Gutachten, ein erwarteter Gewinn in Form einer bestimmten Anzahl Barrel Öl zugeordnet. Die Gesellschaft hat ein begrenztes Budget für neue Bohrungen und möchte die zu erwartende Ölmenge unter Vorgabe dieses Budgets maximieren. Mit linearer Programmierung können wir auch Graphenprobleme und kombinatorische Probleme, wie solche, die in diesem Buch bereits vorgestellt wurden, modellieren und lösen. Wir haben bereits einen Spezialfall linearer Programmierung kennengelernt, der für das Lösen von Differenzbedingungen in Abschnitt 24.4 angewendet wurde. In Abschnitt 29.2 werden wir sehen, wie wir verschiedene Graphen- und Flussnetzwerkprobleme als lineare Programme formulieren können. In Abschnitt 35.4 werden wir die lineare Programmierung als Werkzeug anwenden, um eine Näherungslösung eines weiteren Graphenproblems zu bestimmen.
864
29 Lineare Programmierung
Algorithmen der linearen Programmierung In diesem Kapitel untersuchen wir den Simplexalgorithmus. Dieser Algorithmus löst bei sorgfältiger Implementierung allgemeine lineare Programme in der Praxis schnell. Für bestimmte, speziell konstruierte Eingaben kann der Simplexalgorithmus jedoch exponentielle Zeit benötigen. Der erste Algorithmus mit polynomieller Laufzeit für die lineare Programmierung war die Ellipsoid-Methode, die in der Praxis aber langsam ist. Eine zweite Klasse von Algorithmen mit polynomieller Laufzeit sind unter der Bezeichnung Innerer-Punkt-Methoden bekannt. Im Gegensatz zum Simplexalgorithmus, der sich am Rand des zulässigen Bereichs entlang bewegt und in jeder Iteration eine zulässige Lösung bestimmt, die eine Ecke des Simplex ist, bewegen sich diese Algorithmen im Inneren des zulässigen Bereichs. Die Zwischenlösungen sind zwar zulässig, aber nicht notwendigerweise Ecken des Simplex. Die endgültige Lösung ist jedoch eine Ecke. Der erste derartige Algorithmus wurde von Karmarkar entwickelt. Für große Eingaben können diese Algorithmen mit dem Simplexalgorithmus mithalten und sind manchmal sogar schneller. Wenn wir zu einem linearen Programm die zusätzliche Forderung hinzufügen, dass alle Variablen ganzzahlige Werte annehmen müssen, dann sprechen wir von einem ganzzahligen linearen Programm. In Übung 34.5-3 sollen Sie zeigen, dass die Bestimmung einer Lösung für dieses Problem NP-schwer ist. Da kein Algorithmus mit polynomieller Laufzeit für irgendein NP-schweres Problem bekannt ist, ist auch für ganzzahlige lineare Programmierungsprobleme kein Algorithmus mit polynomieller Laufzeit bekannt. Dagegen können wir allgemeine lineare Programmierungsprobleme in polynomieller Zeit lösen. ¯2 , . . . , x ¯n ) benutzen, wenn wir In diesem Kapitel werden wir die Bezeichnung x ¯ = (¯ x1 , x ein lineares Programm über den Variablen x = (x1 , x2 , . . . , xn ) haben und uns auf eine spezielle Belegung dieser Variablen beziehen wollen.
29.1
Standard- und Schlupfform
In diesem Abschnitt beschreiben wir zwei Formate, die Standard- und die Schlupfform, die nützlich sind, wenn wir lineare Programme spezifizieren und mit ihnen arbeiten wollen. In der Standardform sind alle Nebenbedingungen Ungleichungen, während die Nebenbedingungen in der Schlupfform die Form von Gleichungen haben (mit Ausnahme der Nebenbedingungen, die verlangen, dass die Variablen nichtnegativ sind).
Standardform In der Standardform sind n reelle Zahlen c1 , c2 , . . . , cn , m reelle Zahlen b1 , b2 , . . . , bm und mn reelle Zahlen aij mit i = 1, 2, . . . , m und j = 1, 2, . . . , n gegeben. Wir wollen n reelle Zahlen x1 , x2 , . . . , xn bestimmen, die die Zielfunktion n j=1
cj xj
(29.16)
29.1 Standard- und Schlupfform
865
unter den Nebenbedingungen n
aij xj ≤ bi
für i = 1, 2, . . . , m
(29.17)
xj ≥ 0
für j = 1, 2, . . . , n
(29.18)
j=1
maximieren. In Verallgemeinerung der Terminologie, die für das lineare Programm mit zwei Variablen eingeführt haben, bezeichnen wir den Ausdruck (29.16) als die Zielfunktion und die n + m Ungleichungen in den Zeilen (29.17) und (29.18) als die Nebenbedingungen. Die n Nebenbedingungen in der Zeile (29.18) sind die Nichtnegativitätsbedingungen . Ein lineares Programm muss keine Nichtnegativitätsbedingungen haben, die Standardform erfordert sie jedoch. Manchmal werden wir es zweckdienlich finden, ein lineares Programm in kompakterer Form auszudrücken. Wenn wir eine m × n-Matrix A = (aij ), einen m-Vektor b = (bi ), einen n-Vektor c = (cj ) und einen n-Vektor x = (xj ) bilden, dann können wir das durch (29.16)-(29.18) definierte lineare Programm in der Form
maximiere cT x
(29.19)
unter den Nebenbedingungen Ax ≤ b x ≥ 0
(29.20) (29.21)
schreiben. In der Zeile (29.19) bezeichnet cT x das Skalarprodukt zweier Vektoren. Der Ausdruck Ax in der Ungleichung (29.20) ist das Produkt einer Matrix mit einem Vektor und in der Ungleichung (29.21) bedeutet die Ungleichung x ≥ 0, dass jedes Element des Vektors x nichtnegativ sein soll. Wir sehen, dass wir ein lineares Programm in Standardform durch ein Tripel (A, b, c) charakterisieren können. Im Folgenden werden wir voraussetzen, dass A, b und c die oben angegebenen Dimensionen haben. Wir führen nun die Terminologie zur Beschreibung linearer Programme ein. Wir haben einige der Begriffe bereits weiter vorn in dem Beispiel des linearen Programms mit zwei Variablen verwendet. Wir nennen eine Belegung x ¯ der Variablen, die alle Nebenbedingungen erfüllt, eine zulässige Lösung . Eine Belegung x ¯ der Variablen, die eine oder mehr Nebenbedingungen nicht erfüllt, heißt unzulässige Lösung . Wir sagen, dass eine Lösung x ¯ den Zielfunktionswert cT x¯ hat. Eine zulässige Lösung x ¯, deren Zielfunktionswert über alle zulässigen Lösungen maximal ist, ist eine optimale Lösung und wir bezeichnen ihren Zielfunktionswert cT x¯ als optimalen Zielfunktionswert. Wenn ein lineares Programm keine zulässigen Lösungen besitzt, sagen wir, dass das lineare Programm unlösbar ist; anderenfalls ist es lösbar . Wenn ein lineares Programm zulässige Lösungen, aber keinen endlichen optimalen Zielfunktionswert hat, sagen wir, dass das lineare Programm unbeschränkt ist. In Übung 29.1-9 sollen Sie zeigen, dass ein lineares Programm selbst dann einen endlichen optimalen Zielfunktionswert haben kann, wenn der zulässige Bereich unbeschränkt ist.
866
29 Lineare Programmierung
Überführung linearer Programme in die Standardform Es ist immer möglich, ein lineares Programm, das als Minimierungs- oder Maximierungsaufgabe einer linearen Funktion unter linearen Nebenbedingungen formuliert ist, in die Standardform zu überführen. Ein lineares Programm kann nur aus einem der folgenden vier Gründe möglicherweise nicht in Standardform sein: 1. Die Zielfunktion soll minimiert statt maximiert werden. 2. Es gibt Variablen ohne Nichtnegativitätsbedingungen. 3. Es gibt Gleichheitsnebenbedingungen , d. h. Nebenbedingungen, die ein Gleichheitszeichen statt ein ≤-Zeichen enthalten. 4. Es gibt Ungleichheitsnebenbedingungen , die aber ein ≥-Zeichen und kein ≤Zeichen enthalten. Wenn wir ein lineares Programm L in ein anderes lineares Programm L überführen, wollen wir natürlich, dass eine optimale Lösung in L auch eine optimale Lösung in L liefert. Wir sagen, dass zwei lineare Maximierungsprogramme äquivalent sind, wenn es zu jeder zulässigen Lösung x ¯ von L mit dem Zielfunktionswert z eine zugehörige Lösung x ¯ von L mit dem Zielfunktionswert z gibt und umgekehrt. (Diese Definition bedeutet nicht, dass es eine eineindeutige Beziehung zwischen den zulässigen Lösungen gibt.) Ein lineares Minimierungsproblem L und ein lineares Maximierungsproblem L sind äquivalent, wenn es für jede zulässige Lösung x¯ von L mit dem Zielfunktionswert z eine entsprechende zulässige Lösung x ¯ von L mit dem Zielfunktionswert −z gibt und umgekehrt. Wir zeigen nun, wie jedes der oben aufgelisteten potentiellen Probleme eliminiert werden kann. Anschließend zeigen wir, dass das neue lineare Programm zu dem alten äquivalent ist. Um ein lineares Minimierungsprogramm L in ein lineares Maximierungsprogramm zu überführen, negieren wir einfach alle Koeffizienten der Zielfunktion L . Da die zulässigen Lösungsmengen von L und L identisch sind und für jede zulässige Lösung der Zielfunktionswert in L das Negative des Zielfunktionswerts in L ist, sind diese beiden linearen Programme äquivalent. Ist beispielsweise das lineare Programm minimiere − 2x1 + 3x2 unter den Nebenbedingungen x1 + x2 = 7 x1 − 2x2 ≤ 4 x1 ≥0 gegeben und negieren wir alle Koeffizienten der Zielfunktion, so erhalten wir maximiere 2x1 − 3x2
29.1 Standard- und Schlupfform
867
unter den Nebenbedingungen x1 + x2 = 7 x1 − 2x2 ≤ 4 x1 ≥ 0. Als nächstes zeigen wir, wie wir ein lineares Programm, in dem einige Variablen keiner Nichtnegativitätsbedingung unterliegen, in ein lineares Programm überführen können, in dem alle Variablen der Nichtnegativitätsbedingung unterliegen. Setzen Sie voraus, dass die Variable xj keiner Nichtnegativitätsbedingung unterliegt. Dann ersetzen wir xj überall durch xj − xj und fügen die Nichtnegativitätsbedingungen xj ≥ 0 und xj ≥ 0 hinzu. Wenn also die Zielfunktion einen Term cj xj enthält, dann ersetzen wir diesen durch cj xj − cj xj , und wenn die Nebenbedingung i einen Term aij xj enthält, des neuen lidann ersetzen wir diesen durch aij xj − aij xj . Jede zulässige Lösung x nearen Programms entspricht einer zulässigen Lösung x ¯ des ursprünglichen linearen Programms mit x¯j = x j − x j und dem gleichen Zielfunktionswert. Zudem korrespondiert jede zulässige Lösung x ¯ des ursprünglichen linearen Programms einer zulässigen Lösung x des neuen linearen Programms mit x j = x¯j und x j = 0, wenn x ¯j ≥ 0 ist, oder xj und x j = 0, wenn x¯j < 0 gilt. Die beiden linearen Programme sind daher mit x j = −¯ äquivalent. Wenden wir diese Transformation auf jede Variable an, die keiner Nichtnegativitätsbedingung unterliegt, so erhalten wir ein äquivalentes lineares Programm, in dem alle Variablen Nichtnegativitätsbedingungen haben. Wir wollen unser Beispiel weiterführen und sicherstellen, dass jede Variable einer Nichtnegativitätsbedingung unterliegt. Für die Variable x1 gibt es eine solche Nebenbedingung, für die Variable x2 dagegen nicht. Daher ersetzen wir die Variable x2 durch die beiden Variablen x2 und x2 und modifizieren das lineare Programm zu maximiere 2x1 − 3x2 + 3x2 unter den Nebenbedingungen x1 + x2 − x2 = 7 x1 − 2x2 + 2x2 ≤ 4 x1 , x2 , x2 ≥0.
(29.22)
Als nächstes überführen wir Gleichheitsnebenbedingungen in Ungleichheitsnebenbedingungen. Setzen Sie voraus, dass ein lineares Programm eine Gleichheitsnebenbedingung f (x1 , x2 , . . . , xn ) = b enthält. Da v = w genau dann gilt, wenn sowohl v ≥ w als auch v ≤ w gilt, können wir die Gleichheitsnebenbedingung durch die beiden Ungleichheitsbedingungen f (x1 , x2 , . . . , xn ) ≤ b und f (x1 , x2 , . . . , xn ) ≥ b ersetzen. Wiederholen wir diese Umformung für jede Gleichheitsnebenbedingung, erhalten wir ein lineares Programm, in dem alle Nebenbedingungen Ungleichungen sind. Schließlich können wir die ≥-Relationen in ≤-Relationen überführen, indem wir diese Nebenbedingungen mit −1 multiplizieren. Das heißt, jede Ungleichung der Form n j=1
aij xj ≥ bi
868
29 Lineare Programmierung
ist äquivalent zu n
−aij xj ≤ −bi .
j=1
Wenn wir jeden Koeffizienten aij durch −aij und jeden Wert bi durch −bi ersetzen, erhalten wir also eine ≤-Bedingung. Um das Beispiel abzuschließen, ersetzen wir die Gleichheitsnebenbedingung (29.22) durch zwei Ungleichungen und erhalten so maximiere
2x1 − 3x2 + 3x2
unter den Nebenbedingungen x1 + x2 − x2 x1 + x2 − x2 x1 − 2x2 + 2x2 x1 , x2 , x2
≤7 ≥7 ≤4 ≥ 0.
(29.23)
Abschließend negieren wir noch die Nebenbedingung (29.23). Um die Variablennamen konsistent zu halten, benennen wir x2 in x2 und x2 in x3 um und erhalten die Standardform maximiere
2x1 − 3x2 + 3x3
(29.24)
unter den Nebenbedingungen x1 + x2 − x3 −x1 − x2 + x3 x1 − 2x2 + 2x3 x1 , x2 , x3
≤ 7 ≤ −7 ≤ 4 ≥ 0.
(29.25) (29.26) (29.27) (29.28)
Überführung linearer Programme in die Schlupfform Um ein lineares Programm mit dem Simplexalgorithmus effizient zu lösen, ziehen wir es vor, es in einer Form darzustellen, in der einige der Nebenbedingungen Gleichungen sind. Genauer gesagt wollen wir das Programm in eine Form überführen, in der die Nichtnegativitätsbedingungen die einzigen Ungleichheitsnebenbedingungen sind, während die übrigen Nebenbedingungen Gleichungen sind. Sei n j=1
aij xj ≤ bi
(29.29)
29.1 Standard- und Schlupfform
869
eine Ungleichheitsnebenbedingung. Wir führen eine neue Variable s ein und drücken die Ungleichung (29.29) durch die beiden Bedingungen s = bi −
n
aij xj ,
(29.30)
j=1
s≥0
(29.31)
aus. Wir nennen s eine Schlupfvariable, da sie ein Maß für die Differenz oder den Schlupf zwischen der linken und der rechten Seite der Ungleichung (29.29) ist. (Wir werden gleich sehen, warum wir es als zweckdienlicher ansehen, die Nebenbedingungen so hinzuschreiben, dass die Schlupfvariable auf der linken Seite der Gleichung steht.) Da die Ungleichung (29.29) genau dann gilt, wenn sowohl (29.30) als auch (29.31) erfüllt sind, können wir diese Transformation für jede Ungleichheitsnebenbedingung des linearen Programms anwenden und erhalten ein äquivalentes lineares Programm, in dem die einzigen Ungleichheitsnebenbedingungen Nichtnegativitätsbedingungen sind. Bei der Überführung der Standardform in die Schlupfform verwenden wir die Bezeichnung xn+i (statt s) für die Schlupfvariable, die zur i-ten Ungleichung gehört. Die i-te Nebenbedingung ist daher xn+i = bi −
n
aij xj ,
(29.32)
j=1
zusammen mit der Nichtnegativitätsbedingung xn+i ≥ 0. Indem wir diese Transformation auf jede Nebenbedingung eines linearen Programms in Standardform anwenden, erhalten wir ein lineares Programm in einer anderen Form. Zum Beispiel führen wir bei dem durch die Zeilen (29.24)-(29.28) definierten linearen Programm die Schlupfvariablen x4 , x5 und x6 ein und erhalten maximiere
2x1 − 3x2 + 3x3
(29.33)
unter den Nebenbedingungen x4 = 7 − x1 − x2 + x3 x5 = −7 + x1 + x2 − x3 x6 = 4− x1 + 2x2 − 2x3 x1 , x2 , x3 , x4 , x5 , x6 ≥ 0 .
(29.34) (29.35) (29.36) (29.37)
In diesem linearen Programm sind alle Nebenbedingungen außer den Nichtnegativitätsbedingungen Gleichungen, und alle Variablen müssen eine Nichtnegativitätsbedingung erfüllen. Wir schreiben alle Gleichheitsnebenbedingungen so, dass jeweils eine Variable auf der linken Seite steht, und alle übrigen Variablen auf der rechten. Außerdem stehen in jeder Gleichung die gleichen Variablen auf der rechten Seite. Dies sind gerade diejenigen Variablen, die auch in der Zielfunktion vorkommen. Wir nennen die Variablen auf der linken Seite Basisvariablen und die Variablen auf der rechten Seite Nichtbasisvariablen.
870
29 Lineare Programmierung
Für lineare Programme, die diese Voraussetzungen erfüllen, werden wir gelegentlich die Worte „maximiere“ und „unter den Nebenbedingungen“ weglassen, ebenso die Nichtnegativitätsbedingungen. Außerdem werden wir die Variable z für den Wert der Zielfunktion verwenden. Das resultierende Schema bezeichnen wir als Schlupfform. Wenn wir das lineare Programm aus den Zeilen (29.33)-(29.37) in Schlupfform schreiben, erhalten wir z x4 x5 x6
= 2x1 = 7 − x1 = −7 + x1 = 4 − x1
− 3x2 − x2 + x2 + 2x2
+ 3x3 + x3 − x3 − 2x3 .
(29.38) (29.39) (29.40) (29.41)
Wie im Falle der Standardform finden wir es zweckdienlich, eine kompaktere Notation zu haben, um eine Schlupfform darzustellen. In Abschnitt 29.3 werden wir sehen, dass die Mengen der Basis- und Nichtbasisvariablen sich während der Ausführung des Simplexalgorithmus ändern. Mit N bezeichnen wir die Menge der Indizes der Nichtbasisvariablen und mit B die Menge der Indizes der Basisvariablen. Es gilt stets |N | = n, |B| = m und N ∪ B = {1, 2, . . . , n + m}. Die Gleichungen werden durch die Elemente aus B indiziert und die Variablen auf den rechten Seiten durch die Elemente aus N . Wie für die Standardform verwenden wir bi , cj und aij für die Bezeichnung der konstanten Terme und der Koeffizienten. Mit v bezeichnen wir einen optionalen konstanten Term in der Zielfunktion. (Wir werden gleich sehen, dass das Einfügen des konstanten Terms in die Zielfunktion das Bestimmen des Zielfunktionswertes erleichtert.) Damit können wir die Schlupfform in kompakter Form durch ein Tupel (N, B, A, b, c, v) definieren. Dieses stellt die Schlupfform z = v+ cj xj (29.42) j∈N
xi = bi −
aij xj
für i ∈ B
(29.43)
j∈N
dar, in der alle Variablen einer Nichtnegativitätsbedingung unterliegen. Da wir die Summe j∈N aij xj in Gleichung (29.43) subtrahieren, sind die Werte aij in Wirklichkeit die negativen Werte der Koeffizienten, wie sie in der Schlupfform erscheinen. Beispielsweise haben wir in der Schlupfform x3 6 x3 = 8+ 6 8x3 = 4− 3 x3 = 18 − 2
z = 28 − x1 x2 x4
B = {1, 2, 4}, N = {3, 5, 6},
x5 6 x5 + 6 2x5 − 3 x5 + 2 −
2x6 3 x6 − 3 x6 + 3 −
,
29.1 Standard- und Schlupfform
871
⎞ ⎛ ⎞ a13 a15 a16 −1/6 −1/6 1/3 A = ⎝ a23 a25 a26 ⎠ = ⎝ 8/3 2/3 −1/3 ⎠ , a43 a45 a46 1/2 −1/2 0 ⎛
⎞ ⎛ ⎞ 8 b1 b = ⎝ b2 ⎠ = ⎝ 4 ⎠ , b4 18 T T = −1/6 −1/6 −2/3 und v = 28. Beachten Sie, dass die Indizes, die c = c3 c5 c6 in A, b, und c verwendet werden, nicht notwendigerweise benachbarte ganze Zahlen sind; sie hängen von den Indexmengen B und N ab. Als Beispiel dafür, dass die Elemente von A die negativen Werte der Koeffizienten in der Schlupfform sind, sehen Sie, dass die Gleichung für x1 den Term x3 /6 enthält, während der Koeffizient a13 −1/6 statt +1/6 ist. ⎛
Übungen 29.1-1 Wie lauten n, m, A, b und c, wenn wir das lineare Programm (29.24)-(29.28) in der kompakten Schreibweise aus den Zeilen (29.19)-(29.21) formulieren? 29.1-2 Geben Sie drei zulässige Lösungen des linearen Programms (29.24)-(29.28) an. Wie groß ist jeweils der Zielfunktionswert? 29.1-3 Wie lauten N , B, A, b, c und v für die Schlupfform (29.38)-(29.41)? 29.1-4 Überführen Sie das folgende lineare Programm in die Standardform: minimiere 2x1 + 7x2 + x3 unter den Nebenbedingungen x1 − x3 3x1 + x2 x2 x3
= 7 ≥ 24 ≥ 0 ≤ 0.
29.1-5 Überführen Sie das folgende lineare Programm in die Schlupfform: maximiere 2x1 − 6x3 unter den Nebenbedingungen x1 + x2 − x3 3x1 − x2 −x1 + 2x2 + 2x3 x1 , x2 , x3
≤7 ≥8 ≥0 ≥0.
Welche sind die Basisvariablen und welche die Nichtbasisvariablen?
872
29 Lineare Programmierung
29.1-6 Zeigen Sie, dass das folgende lineare Programm unlösbar ist: maximiere 3x1 − 2x2 unter den Nebenbedingungen x1 + x2 ≤ 2 −2x1 − 2x2 ≤ −10 x1 , x2 ≥ 0. 29.1-7 Zeigen Sie, dass das folgende lineare Programm unbeschränkt ist: maximiere x1 − x2 unter den Nebenbedingungen −2x1 + x2 ≤ −1 x1 − 2x2 − ≤ −2 x1 , x2 ≥ 0. 29.1-8 Nehmen Sie an, wir würden ein allgemeines lineares Programm mit n Variablen und m Nebenbedingungen in die Standardform überführen. Geben Sie eine obere Schranke für die Anzahl der Variablen und der Nebenbedingungen im resultierenden linearen Programm an. 29.1-9 Geben Sie ein Beispiel für ein lineares Programm an, für das der optimale Zielfunktionswert endlich ist, der zulässige Bereich aber nicht beschränkt ist.
29.2
Darstellung von Problemen als lineare Programme
Auch wenn wir uns in diesem Kapitel auf den Simplexalgorithmus konzentrieren werden, so ist es ebenso wichtig, erkennen zu können, wann wir ein Problem als lineares Programm darstellen können. Wenn wir ein Problem einmal als lineares Programm mit polynomieller Größe formuliert haben, können wir es durch den Ellipsoid- oder den Inneren-Punkt-Algorithmus in polynomieller Laufzeit lösen. Diverse Softwarepakete zur linearen Programmierung können entsprechende Probleme effizient lösen. Wir werden uns mehrere konkrete Beispiele für lineare Programmierungsprobleme ansehen. Wir beginnen mit zwei Problemen, denen wir bereits begegnet sind: das Problem der Berechnung der kürzesten Pfade bei einem Startknoten (Kapitel 24) und das maximale-Fluss-Problem (Kapitel 26). Anschließend beschreiben wir das Problem der Berechnung eines Flusses mit minimalen Kosten. Wenngleich es für das Problem der Berechnung eines Flusses mit minimalen Kosten einen Algorithmus mit polynomieller Laufzeit gibt, der nicht auf linearer Programmierung basiert, werden wir diesen hier
29.2 Darstellung von Problemen als lineare Programme
873
nicht beschreiben. Schließlich beschreiben wir das Problem multipler Warenflüsse, für das der einzige bekannte Algorithmus mit polynomieller Laufzeit auf linearer Programmierung beruht. Als wir die Graphenprobleme in Teil VI gelöst haben, benutzten wir eine Attributsnotation, beispielsweise v.d und (u, v).f . Lineare Programme arbeiten jedoch typischerweise auf indizierten Variablen und nicht auf Objekten mit beigefügten Attributen. Aus diesem Grunde werden wir in diesem Kapitel Knoten und Kanten über Variablenindizes angeben. Beispielsweise werden wir das Gewicht eines kürzesten Pfades für Knoten v nicht durch v.d sondern durch dv angeben. Genauso schreiben wir für den Fluss von Knoten u zu Knoten v nicht (u, v).f sondern fuv . Werte, die Eingaben der Probleme darstellen, wie zum Beispiel Kantengewichte oder Kapazitäten, schreiben wir in der bis jetzt benutzten Notation, also als w(u, v) und c(u, v).
Kürzeste Pfade Wir können das kürzeste-Pfade-Problem mit einem Startknoten als lineares Programm formulieren. In diesem Abschnitt werden wir uns auf die Modellierung des kürzestenPfade-Problems für ein Knotenpaar konzentrieren und die Erweiterung auf das allgemeinere kürzeste-Pfade-Problem mit einem Startknoten der Übung 29.2-3 überlassen. Beim Problem der Berechnung eines kürzesten Pfades für ein gegebenes Knotenpaar ist ein gewichteter gerichteter Graph G = (V, E) mit der Gewichtsfunktion w : E → R, die die Kanten auf reellwertige Gewichte abbildet, sowie ein Startknoten s und ein Zielknoten t gegeben. Wir wollen den Wert dt berechnen, der das Gewicht eines kürzesten Pfades von s nach t angibt. Um dieses Problem durch ein lineares Programm zu formulieren, müssen wir eine Menge von Variablen und Nebenbedingungen spezifizieren, die definieren, wann ein kürzester Pfad von s nach t vorliegt. Erfreulicherweise tut der Bellman-Ford-Algorithmus genau dies. Wenn der Bellman-Ford-Algorithmus terminiert, hat er für jeden Knoten v einen Wert dv (wir verwenden hier die Notation über Indizes und nicht die Attributsnotation) berechnet, sodass für alle Kanten (u, v) ∈ E die Ungleichung dv ≤ du + w(u, v) erfüllt ist. Dem Startknoten wird zu Beginn ein Wert ds = 0 zugewiesen, der sich niemals ändert. Damit erhalten wir das folgende lineare Programm, um das Gewicht eines kürzesten Pfades von s nach t zu berechnen: maximiere
(29.44)
dt
unter den Nebenbedingungen dv ≤ du + w(u, v) ds = 0 .
für alle Kanten (u, v) ∈ E ,
(29.45) (29.46)
Sie sind möglicherweise überrascht, dass dieses lineare Programm eine Zielfunktion maximiert, wo wir doch kürzeste Pfade berechnen wollen. Wir minimieren die Zielfunktion nicht, da dann die Belegung d¯v = 0 für jede Variable v ∈ V zwar eine optimale Lösung des linearen Programms wäre, ohne eine Lösung des kürzesten-Pfad-Problems zu sein. Wir maximieren, da eine optimale Lösung zu für jeden dem kürzesten-Pfad-Problem ¯v der größKnoten v die Belegung d¯v auf minu:(u,v)∈E d¯u + w(u, v) setzt, sodass d te Wert ist, der kleiner oder gleich allen Werten der Menge d¯u + w(u, v) ist. Wir
874
29 Lineare Programmierung
wollen also dv für alle Knoten v auf einem kürzesten Pfad von s nach t unter diesen Nebenbedingungen maximieren und mit dem Maximieren von dt erreichen wir dieses Ziel. Dieses lineare Programm hat |V | Variablen dv , einen für jeden Knoten v ∈ V . Es enthält |E| + 1 Nebenbedingungen, eine pro Kante, plus die Nebenbedingung, dass die Kosten eines kürzesten Pfades des Startknotens immer den Wert 0 hat.
Maximaler Fluss Als nächstes formulieren wir das maximale-Fluss-Problem als lineares Programm. Rufen Sie sich in Erinnerung, dass wir bei diesem Problem einen gerichteten Graphen G = (V, E) gegeben haben, in dem jeder Kante (u, v) ∈ E eine nichtnegative Kapazität c(u, v) ≥ 0 zugeordnet ist, sowie zwei ausgezeichnete Knoten, eine Quelle s und eine Senke t. Wie in Abschnitt 26.1 definiert, ist ein Fluss eine nichtnegative reellwertige Funktion f : V × V → R, die der Kapazitätsbeschränkungen und der Flusserhaltung genügt. Ein maximaler Fluss ist ein Fluss, der diese Bedingungen erfüllt und dabei den Flusswert maximiert, der durch den Gesamtfluss, der von der Quelle ausgeht, abzüglich des Gesamtflusses, der in die Quelle hineinfließt, definiert ist. Ein Fluss erfüllt also lineare Nebenbedingungen und der Wert des Flusses ist eine lineare Funktion. Wir erinnern auch daran, dass wir c(u, v) = 0 voraussetzen, wenn (u, v) ∈ E ist, und dass es keine antiparallelen Kanten gibt. Das maximale-Fluss-Problem lässt sich somit als lineares Programm formulieren: fsv − fvs (29.47) maximiere v∈V
v∈V
unter den Nebenbedingungen v∈V
fuv ≤ c(u, v) fvu = fuv
für alle u, v ∈ V ,
(29.48)
für alle u ∈ V − {s, t}
(29.49)
für alle u, v ∈ V .
(29.50)
v∈V
fuv ≥ 0
2
Dieses lineare Programm hat |V | Variablen, die den Flüssen zwischen allen Knoten2 paaren entsprechen, und 2 |V | + |V | − 2 Nebenbedingungen. In der Regel ist es effizienter, kleine lineare Programme zu lösen. Um die Notation einfach zu halten, enthält das lineare Programm (29.47)–(29.50) für jedes Knotenpaar u, v mit (u, v) ∈ E den Fluss und die Kapazität 0. Es wäre effizienter, wenn wir das lineare Programm so umschreiben würden, dass es nur O(V + E) Nebenbedingungen enthält. Übung 29.2-5 verlangt von Ihnen, dies zu tun.
Fluss mit minimalen Kosten In diesem Abschnitt haben wir bisher lineare Programmierung eingesetzt, um Probleme zu lösen, für die wir bereits effiziente Algorithmen kannten. Ein effizienter Algorithmus,
29.2 Darstellung von Problemen als lineare Programme 5 c= 2 a=
x
s c= a= 2 5
c= a= 2 7 c=1 a=3
y (a)
4 c= 1 = a
2/5 2 a=
t
875
1/ a= 2 7
x
1/1 a=3
s 2/ a= 2 5
y
t 3/4 1 a=
(b)
Abbildung 29.3: (a) Ein Beispiel für das Problem der Berechnung eines Flusses mit minimalen Kosten. Wir bezeichnen die Kapazitäten mit c und die Kosten mit a. Der Knoten s ist die Quelle und der Knoten t die Senke. Wir wollen 4 Einheiten des Flusses von s nach t schicken. (b) Eine Lösung des Problems. An jeder Kante sind Fluss und Kapazität in der Form Fluss/Kapazität angegeben.
der speziell für ein bestimmtes Problem entworfen wurde, wie z. B. Dijkstras Algorithmus für das kürzeste-Pfade-Problem mit einem Startknoten oder die Push/RelabelMethode für das maximale-Fluss-Problem, wird sowohl aus theoretischer Sicht als auch in der Praxis meistens schneller sein als ein lineares Programm. Die eigentliche Stärke der linearen Programmierung liegt in ihrer Fähigkeit, neue Probleme zu lösen. Erinnern Sie sich an das Problem des Politikers zu Beginn dieses Kapitels. Das Problem, eine ausreichend große Anzahl von Stimmen zu bekommen und dabei nicht zu viel Geld auszugeben, wird durch keinen der Algorithmen, die wir in diesem Buch untersucht haben, gelöst; wir können es aber mit linearer Programmierung lösen. Bücher enthalten eine Fülle solcher realitätsnaher Probleme, die durch lineare Programmierung gelöst werden können. Die lineare Programmierung ist auch dann besonders nützlich, wenn Varianten von Problemen zu lösen sind, für die wir noch nicht wissen, ob sie einen effizienten Algorithmus besitzen. Betrachten Sie zum Beispiel die folgende Verallgemeinerung des maximalen-FlussProblems. Setzen Sie voraus, dass für jede Kante (u, v) neben einer Kapazität c(u, v) noch reellwertige Kosten a(u, v) gegeben sind. Wie beim maximalen-Fluss-Problem setzen wir voraus, dass c(u, v) = 0 gilt, wenn (u, v) ∈ E, und es keine antiparallelen Kanten gibt. Wenn wir fuv Einheiten Fluss über die Kante (u, v) schicken, verursachen wir Kosten in Höhe von a(u, v)fuv . Wir haben auch einen Flussbedarf d gegeben. Wir wollen d Einheiten Fluss von s nach t senden und dabei die durch den Fluss verursachten Gesamtkosten (u,v)∈E a(u, v)fuv minimieren. Dieses Problem ist unter dem Namen Problem der Berechnung eines Flusses mit minimalen Kosten bekannt. Abbildung 29.3(a) zeigt ein Beispiel für das Problem der Berechnung eines Flusses mit minimalen Kosten. Wir wollen 4 Einheiten des Flusses von s nach t schicken und dabei die Gesamtkosten minimal halten. Jeder spezielle zulässige Fluss, d. h. jede Funktion f, die die Bedingungen (29.48)-(29.50) erfüllt, verursacht Gesamtkosten in Höhe von (u,v)∈E a(u, v)fuv . Wir wollen den speziellen Fluss von 4 Einheiten bestimmen, der diese Kosten minimiert. Abbildung 29.3(b) zeigt eine optimale Lösung, die die Gesamt kosten (u,v)∈E a(u, v)fuv = (2 · 2) + (5 · 2) + (3 · 1) + (7 · 1) + (1 · 3) = 27 hat. Es gibt Algorithmen mit polynomieller Laufzeit, die speziell für das Problem der Be-
876
29 Lineare Programmierung
rechnung eines Flusses mit minimalen Kosten entworfen wurden, aber sie würden den Rahmen dieses Buches sprengen. Wir können das Problem der Berechnung eines Flusses mit minimalen Kosten jedoch als lineares Programm formulieren. Das lineare Programm ähnelt dem für das maximale-Fluss-Problem mit der zusätzlichen Nebenbedingung, dass der Wert des Flusses genau d Einheiten betragen muss, und mit der neuen Zielfunktion, um die Kosten zu minimieren: a(u, v)fuv (29.51) minimiere (u,v)∈E
unter den Nebenbedingungen
fvu −
v∈V
v∈V
fuv ≤ c(u, v)
für alle u, v ∈ V ,
fuv = 0
für alle u ∈ V − {s, t} ,
v∈V
fsv −
fvs = d
v∈V
fuv ≥ 0
für alle u, v ∈ V .
(29.52)
Multiple Warenflüsse Als abschließendes Beispiel betrachten wir ein weiteres Flussproblem. Nehmen Sie an, die Firma Lucky Puck aus Abschnitt 26.1 würde beschließen, ihre Produktpalette zu erweitern und nicht nur Hockeypucks, sondern auch Hockeyschläger und Hockeyhelme herzustellen. Jedes Teil wird in einer eigenen Fabrik hergestellt, hat sein eigenes Warenlager und muss jeden Tag von der Fabrik zum Warenlager transportiert werden. Die Schläger werden in Vancouver hergestellt und müssen nach Saskatoon gebracht werden, die Helme werden in Edmonton hergestellt und nach Regina transportiert. Die Kapazität des Transportnetzwerks ändert sich jedoch nicht und die verschiedenen Elemente, d. h. die Waren, müssen sich das gleiche Netzwerk teilen. Dieses Beispiel ist eine Instanz des Problems multipler Warenflüsse. Bei diesem Problem haben wir wieder einen gerichteten Graphen G = (V, E) gegeben, in dem jeder Kante (u, v) ∈ E eine nichtnegative Kapazität c(u, v) ≥ 0 zugeordnet ist. Wie beim maximalen-Fluss-Problem setzen wir implizit voraus, dass c(u, v) = 0 gilt, wenn (u, v) ∈ E, und der Graph keine antiparallelen Kanten enthält. Zusätzlich haben wir k verschiedene Waren K1 , K2 , . . . , Kk gegeben, wobei wir die Ware i durch das Tripel Ki = (si , ti , di ) spezifizieren. si ist die Quelle für Ware i, ti die Senke für Ware i und di der Bedarf für Ware i, also der gewünschte Wert des Flusses für die Ware von si nach ti . Wir definieren einen Fluss für die Ware i, den wir mit fi bezeichnen (sodass fiuv der Fluss der Ware i von Knoten u nach Knoten v ist), als eine reellwertige Funktion, die der Flusserhaltung und den Kapazitätsbeschränkungen genügt. Wir definieren nun fuv , den k Aggregat-Fluss, als die Summe der einzelnen Warenflüsse, d. h. fuv = i=1 fiuv . Der Aggregat-Fluss entlang der Kante (u, v) darf nicht größer als die Kapazität der Kante (u, v) sein. Wir versuchen in diesem Problem nicht, eine Zielfunktion zu minimieren;
29.2 Darstellung von Problemen als lineare Programme
877
wir wollen nur prüfen, ob ein solcher Fluss existiert. Daher schreiben wir ein lineares Programm, die als Zielfunktion die „Null“-Funktion hat: minimiere
0
unter den Nebenbedingungen k
fiuv −
v∈V
v∈V
fiuv ≤ c(u, v) für alle u, v ∈ V ,
i=1
fisi v −
fivu = 0
für alle i = 1, 2, . . . , k und für alle u ∈ V − {si , ti } ,
fivsi = di
für alle i = 1, 2, . . . , k ,
fiuv ≥ 0
für alle u, v ∈ V und für alle i = 1, 2, . . . , k .
v∈V
v∈V
Der einzige bekannte Algorithmus mit polynomieller Laufzeit für dieses Problem besteht darin, das Problem als lineares Programm zu formulieren und dieses durch einen Algorithmus zur Lösung linearer Programme mit polynomieller Laufzeit zu lösen.
Übungen 29.2-1 Überführen Sie das lineare Programm (29.44)-(29.46) für das kürzeste-PfadProblem für ein gegebenes Knotenpaar in die Standardform. 29.2-2 Formulieren Sie explizit das lineare Programm für die Bestimmung eines kürzesten Pfades vom Knoten s zum Knoten y aus Abbildung 24.2(a). 29.2-3 Beim kürzesten-Pfade-Problem mit einem Startknoten wollen wir die Gewichte der kürzesten Pfade von einem Startknoten s zu allen Knoten v ∈ V bestimmen. Schreiben Sie für einen gegebenen Graphen G ein lineares Programm, dessen Lösung die Eigenschaft hat, dass, für alle Knoten v ∈ V , dv das Gewicht des kürzesten Pfades von s nach v ist. 29.2-4 Formulieren Sie explizit das lineare Programm, das einen maximalen Fluss in Abbildung 26.1(a) berechnet. 29.2-5 Schreiben Sie das lineare Programm (29.47)–(29.50) für das maximale-FlussProblem so um, dass das neue lineare Programm nur O(V + E) Nebenbedingungen verwendet. 29.2-6 Formulieren Sie ein lineares Programm, das für einen gegebenen bipartiten Graphen G = (V, E) ein maximales bipartites Matching berechnet. 29.2-7 Beim Problem der multiplen Warenflüsse mit minimalen Kosten ist ein gerichteter Graph G = (V, E) gegeben, in dem jede Kante (u, v) ∈ E eine nichtnegative Kapazität c(u, v) ≥ 0 und Kosten a(u, v) hat. Wie beim
878
29 Lineare Programmierung Problem der multiplen Warenflüsse sind k verschiedene Waren K1 , K2 , . . . , Kk gegeben, wobei wir die Ware i durch das Tripel Ki = (si , ti , di ) spezifizieren. Wir definieren den Fluss fi für die Ware i und den Aggregat-Fluss fuv entlang der Kante (u, v) wie beim Problem der multiplen Warenflüsse. Ein Fluss ist zulässig, wenn der Aggregat-Fluss entlang jeder Kante nicht größer als die Kapazität der Kante (u, v) ist. Die Kosten eines Flusses sind u,v∈V a(u, v)fuv und das Ziel besteht darin, einen zulässigen Fluss mit minimalen Kosten zu bestimmen. Formulieren Sie dieses Problem als lineares Programm.
29.3
Der Simplexalgorithmus
Der Simplexalgorithmus ist das klassische Verfahren zur Lösung linearer Programme. Im Gegensatz zu den meisten anderen Algorithmen in diesem Buch ist seine Laufzeit im schlechtesten Fall nicht polynomiell. Er vermittelt jedoch einen guten Einblick in die lineare Programmierung und ist in der Praxis oft bemerkenswert schnell. Außer der geometrischen Interpretation, die wir weiter vorn in diesem Kapitel beschrieben haben, hat der Algorithmus auch eine gewisse Ähnlichkeit mit dem Gaußschen Eliminationsverfahren, das in Abschnitt 28.1 behandelt wurde. Die Gaußsche Elimination beginnt mit einem System linearer Gleichungen, dessen Lösung unbekannt ist. In jedem Iterationsschritt überführen wir das System in eine äquivalente Form, die gewisse zusätzliche Struktureigenschaften hat. Nach einer bestimmten Anzahl von Iterationen hat das System eine Form, aus der die Lösung leicht zu erhalten ist. Der Simplexalgorithmus verfährt in ähnlicher Weise. Wir können ihn als Gaußsches Eliminationsverfahren für Ungleichungen verstehen. Wir beschreiben nun die Grundidee, die sich hinter einer Iteration des Simplexalgorithmus versteckt. Jeder Iteration ist eine „Basislösung“ zugeordnet, die wir leicht aus der Schlupfform des linearen Programms erhalten können: Setzen Sie jede Nichtbasisvariable auf 0 und berechnen Sie die Werte der Basisvariablen aus den Gleichheitsnebenbedingungen. Eine Iteration konvertiert eine Schlupfform in eine zu ihr äquivalente Schlupfform. Der Zielfunktionswert der entsprechenden neuen zulässigen Basislösung ist nicht kleiner, in der Regel größer, als der Zielfunktionswert bei der vorherigen Iteration. Um diesen Anstieg des Zielfunktionswerts zu erreichen, wählen wir eine Nichtbasisvariable so aus, dass der Zielfunktionswert ansteigen würde, wenn wir den Wert dieser Variablen von 0 erhöhen würden. Der Betrag, um den wir die Variable erhöhen können, ist durch die anderen Nebenbedingungen beschränkt. Genauer gesagt erhöhen wir den Wert, bis eine der Basisvariablen 0 wird. Wir können die Schlupfform so umformen, dass die Rollen der Basis- und Nichtbasisvariablen vertauscht werden. Obwohl wir eine spezielle Festlegung der Variablen für die Beschreibung des Algorithmus verwendet haben und diese auch in unseren Beweisen benutzen, erhält der Algorithmus diese Lösung nicht explizit. Er formt das lineare Programm einfach nur solange um, bis eine optimale Lösung „offensichtlich“ wird.
29.3 Der Simplexalgorithmus
879
Ein Beispiel für den Simplexalgorithmus Wir beginnen mit einem umfangreichen Beispiel. Wir betrachten das folgende lineare Programm in Standardform: maximiere 3x1 + x2 + 2x3
(29.53)
unter den Nebenbedingungen x1 + x2 + 3x3 2x1 + 2x2 + 5x3 4x1 + x2 + 2x3 x1 , x2 , x3
≤ 30 ≤ 24 ≤ 36 ≥ 0.
(29.54) (29.55) (29.56) (29.57)
Damit wir den Simplexalgorithmus anwenden können, müssen wir das lineare Programm in die Schlupfform überführen. Wir haben in Abschnitt 29.1 gesehen, wie wir dies machen können. Diese algebraische Umformung ist auch ein nützliches algorithmisches Konzept. Erinnern Sie sich aus Abschnitt 29.1 daran, dass jede Variable einer Nichtnegativitätsbedingung unterliegt. Wir sagen, dass eine Gleichheitsnebenbedingung für eine spezielle Belegung ihrer Nichtbasisvariablen scharf ist, wenn sie dazu führt, dass die Basisvariable der Nebenbedingung den Wert 0 annimmt. Entsprechend verletzt eine Belegung der Nichtbasisvariablen eine Nebenbedingung, wenn die zugehörige Basisvariable beim Einsetzen dieser Werte in die Nebenbedingung negativ wird. Die Schlupfvariablen geben also an, wie weit jede Nebenbedingung davon entfernt ist, scharf zu sein, und so helfen sie, zu bestimmen, wie stark wir die Nichtbasisvariablen erhöhen können, ohne eine Nebenbedingung zu verletzen. Ordnen wir den Nebenbedingungen (29.54)–(29.56) die Schlupfvariablen x4 , x5 beziehungsweise x6 zu und bringen wir das lineare Programm in Schlupfform, so erhalten wir z x4 x5 x6
= 3x1 = 30 − x1 = 24 − 2x1 = 36 − 4x1
+ x2 − x2 − 2x2 − x2
+ 2x3 − 3x3 − 5x3 − 2x3 .
(29.58) (29.59) (29.60) (29.61)
Das System der Nebenbedingungen (29.59)–(29.61) besteht aus 3 Gleichungen und 6 Variablen. Jede Variablenbelegung für x1 , x2 und x3 definiert die Werte für x4 , x5 und x6 , sodass wir unendlich viele Lösungen für dieses Gleichungssystem haben. Eine Lösung ist zulässig, wenn die Werte aller Variablen x1 , x2 , . . . , x6 nichtnegativ sind. Die Anzahl der zulässigen Lösungen kann ebenfalls unendlich sein. Die unendliche Anzahl möglicher Lösungen eines Systems wird sich in den nachfolgenden Beweisen als nützlich herausstellen. Wir werden uns jeweils auf die Basislösung konzentrieren: Setzen Sie alle (Nichtbasis-)Variablen auf der rechten Seite auf 0 und berechnen Sie dann die Werte der (Basis-)Variablen auf der linken Seite. In diesem Beispiel ist die Basislösung (¯ x1 , x ¯2 , . . . , x ¯6 ) = (0, 0, 0, 30, 24, 36). Sie hat den Zielfunktionswert z = (3 · 0) + (1 · 0) + (2 · 0) = 0. Beachten Sie, dass diese Basislösung x ¯i = bi für alle
880
29 Lineare Programmierung
i ∈ B setzt. Eine Iteration des Simplexalgorithmus formt die Menge der Gleichungen und die Zielfunktion so um, dass eine andere Menge von Variablen auf der rechten Seite steht. Somit ist eine andere Basislösung dem umgeformten Gleichungssystem zugeordnet. Wir unterstreichen, dass die Umformung keineswegs das zugrunde liegende lineare Programmierungsproblem verändert; das Problem hat in jeder Iteration die gleiche Menge zulässiger Lösungen wie in der vorherigen Iteration. Es hat jedoch eine andere Basislösung als zuvor. Wenn eine Basislösung außerdem zulässig ist, bezeichnen wir sie als zulässige Basislösung. Während eines Laufs des Simplexalgorithmus ist die Basislösung meist fast immer zulässig. Wir werden jedoch in Abschnitt 29.5 sehen, dass die Basislösungen in den ersten Iterationen unter Umständen nicht zulässig sein können. Unser Ziel in jeder Iteration besteht darin, das lineare Programm so umzuformen, dass die Basislösung einen größeren Zielfunktionswert als zuvor hat. Wir wählen eine Nichtbasisvariable xe aus, deren Koeffizient in der Zielfunktion positiv ist, und erhöhen ihren Wert gerade um so viel, dass keine der Nebenbedingungen verletzt wird. Die Variable xe wird zu einer Basisvariablen, und eine andere Variable xl wird zu einer Nichtbasisvariable. Die Werte der anderen Basisvariablen und der Zielfunktion können sich ebenfalls ändern. Wir fahren mit unserem Beispiel fort, indem wir uns überlegen, was passiert, wenn der Wert von x1 erhöht wird. Wenn wir x1 erhöhen, dann werden die Werte von x4 , x5 und x6 kleiner. Da jede Variable eine Nichtnegativitätsbedingung erfüllen muss, können wir nicht zulassen, dass eine davon negativ wird. Wenn x1 größer als 30 wird, dann wird x4 negativ, während x5 und x6 negativ werden, wenn x1 größer als 12 bzw. 9 wird. Die Nebenbedingung (29.61) ist die schärfste Nebenbedingung. Sie ist daher diejenige Bedingung, die festlegt, wie stark x1 erhöht werden kann. Wir werden deshalb die Rollen von x1 und x6 vertauschen. Wir lösen die Gleichung (29.61) nach x1 auf und erhalten x1 = 9 −
x3 x6 x2 − − . 4 2 4
(29.62)
Um die anderen Gleichungen umzuformen, sodass x6 auf der rechten Seite steht, substituieren wir x1 mithilfe von Gleichung (29.62). Für die Gleichung (29.59) erhalten wir auf diese Weise
x4 = 30 − x1 − x2 − 3x3 x3 x6 x2 − − − x2 − 3x3 = 30 − 9 − 4 2 4 5x3 x6 3x2 − + . = 21 − 4 2 4
(29.63)
Analog dazu kombinieren wir Gleichung (29.62) mit der Nebenbedingung (29.60) und
29.3 Der Simplexalgorithmus
881
der Zielfunktion (29.58), um so unser lineares Programm umzuformen zu x3 3x6 x2 + − (29.64) 4 2 4 x3 x6 x2 − − (29.65) x1 = 9 − 4 2 4 5x3 x6 3x2 x4 = 21 − − + (29.66) 4 2 4 3x2 x6 x5 = 6 − − 4x3 + . (29.67) 2 2 Wir bezeichnen diese Umformung als Pivotieren oder Basistausch. Wie wir oben gezeigt haben, wählt ein Basisaustausch eine Nichtbasisvariable xe , die wir Eingangsvariable nennen, und eine Basisvariable xl , die wir Ausgangsvariable nennen, und vertauscht ihre Rollen. z = 27 +
Das durch die Gleichungen (29.64)–(29.67) gegebene lineare Programm ist äquivalent zu dem linearen Programm (29.58)–(29.61). Wir führen zwei Operationen im Simplexalgorithmus durch: Umformen von Gleichungen, sodass Variablen zwischen der linken und der rechten Seite der Gleichung wechseln, und Substituieren einer Gleichung in eine andere. Die erste Operation erzeugt offensichtlich ein äquivalentes Problem und die zweite Operation erzeugt aufgrund der elementaren linearen Algebra ein äquivalentes Problem (siehe Übung 29.3-3). Um diese Äquivalenz zu zeigen, stellen Sie fest, dass unsere ursprüngliche Basislösung (0, 0, 0, 30, 24, 36) die neuen Gleichungen (29.65)-(29.67) erfüllt und den Zielfunktionswert 27 + (1/4) · 0 + (1/2) · 0 − (3/4) · 36 = 0 hat. Die zu dem neuen linearen Programm gehörige Basislösung setzt die Nichtbasiswerte auf 0. Sie lautet (9, 0, 0, 21, 6, 0) und hat den Zielfunktionswert z = 27. Durch einfaches Nachrechnen können Sie überprüfen, dass diese Lösung ebenfalls die Gleichungen (29.59)-(29.61) erfüllt und, eingesetzt in die Zielfunktion (29.58), den Zielfunktionswert (3 · 9) + (1 · 0) + (2 · 0) = 27 ergibt. Zur Fortführung des Beispiels wollen wir eine neue Variable bestimmen, deren Wert wir erhöhen wollen. Die Variable x6 wollen wir nicht erhöhen, da dann der Wert der Zielfunktion sinken würde. Wir können versuchen, x2 oder x3 zu erhöhen. Lassen Sie uns x3 auswählen. Wie weit kann x3 erhöht werden, ohne eine Nebenbedingung zu verletzen? Die Nebenbedingung (29.65) liefert die Schranke 18, Nebenbedingung (29.66) die Schranke 42/5 und Nebenbedingung (29.67) die Schranke 3/2. Die dritte Schranke ist die schärfste, weshalb wir die dritte Nebenbedingung so umformen, dass x3 dann auf der linken und x5 auf der rechten Seite steht. Dann substituieren wir diese neue Gleichung, x3 = 3/2 − 3x2 /8 − x5 /4 + x6 /8 , in die Gleichungen (29.64)-(29.66) und erhalten das neue, aber äquivalente System 111 4 33 x1 = 4 3 x3 = 2 69 x4 = 4 z=
x2 16 x2 − 16 3x2 − 8 3x2 + 16 +
x5 8 x5 + 8 x5 − 4 5x5 + 8 −
11x6 16 5x6 − 16 x6 + 8 x6 − . 16 −
(29.68) (29.69) (29.70) (29.71)
882
29 Lineare Programmierung
Dieses System besitzt die Basislösung (33/4, 0, 3/2, 69/4, 0, 0) mit dem Zielfunktionswert 111/4. Nun ist die einzige Möglichkeit, den Zielfunktionswert zu erhöhen, die Erhöhung von x2 . Die drei Nebenbedingungen liefern die oberen Schranken 132, 4 bzw. ∞. (Die obere Schranke ∞ aus der Nebenbedingung (29.71) kommt daher, dass, wenn wir x2 erhöhen, der Wert der Basisvariablen x4 sich auch erhöht. Diese Nebenbedingung liefert also keine Einschränkung für den Betrag, um den wir x2 erhöhen können.) Wir erhöhen die Variable x2 auf 4, und machen sie zu einer Nichtbasisvariablen. Dann lösen wir Gleichung (29.70) nach x2 auf und substituieren sie in die anderen Gleichungen. Wir erhalten x3 6 x3 = 8+ 6 8x3 = 4− 3 x3 = 18 − 2
z = 28 − x1 x2 x4
x5 6 x5 + 6 2x5 − 3 x5 + 2 −
2x6 3 x6 − 3 x6 + 3
(29.73)
.
(29.75)
−
(29.72)
(29.74)
Nun sind alle Koeffizienten der Zielfunktion negativ. Wie wir weiter hinten in diesem Kapitel sehen werden, tritt diese Situation nur ein, wenn wir das lineare Programm in eine Form gebracht haben, in der die Basislösung eine optimale Lösung ist. Für dieses Problem ist daher die Lösung (8, 4, 0, 18, 0, 0) mit dem Zielfunktionswert 28 optimal. Wir können nun zu dem ursprünglichen, durch die Gleichungen (29.53)-(29.57) gegebenen linearen Programm zurückkehren. Die einzigen Variablen im ursprünglichen linearen Programm sind x1 , x2 und x3 , sodass unsere Lösung x1 = 8, x2 = 4, x3 = 0 lautet und den Zielfunktionswert (3 · 8) + (1 · 4) + (2 · 0) = 28 hat. Beachten Sie, dass die Werte der Schlupfvariablen in der endgültigen Lösung ein Maß dafür sind, wie viel Spielraum jede der Ungleichungen lässt. Die Schlupfvariable x4 ist 18, und in Ungleichung (29.54) ist die linke Seite mit dem Wert 8 + 4 + 0 = 12 um den Betrag 18 kleiner als die rechte Seite mit dem Wert 30. Die Schlupfvariablen x5 und x6 sind 0, und tatsächlich sind in den Ungleichungen (29.55) und (29.56) linke und rechte Seite jeweils gleich. Beachten Sie außerdem, dass, obwohl die Koeffizienten in der ursprünglichen Schlupfform ganzzahlig sind, die Koeffizienten in den anderen linearen Programmen und die Zwischenlösungen nicht unbedingt ganzzahlig sein müssen. Zudem muss auch die endgültige Lösung nicht ganzzahlig sein; es ist reiner Zufall, dass dies für die Lösung unseres Beispiels zutrifft.
Basistausch Wir formalisieren nun die Operation des Basistauschs. Die Prozedur Pivot verwendet als Eingabe eine Schlupfform, die durch das Tupel (N, B, A, b, c, v) gegeben ist, den Index l der Ausgangsvariable xl und den Index e der Eingangsvariable xe . Sie gibt das , B, A, b, Tupel (N c, v) zurück, das die neue Schlupfform darstellt. (Rufen Sie sich in die negativen Werte der KoeffiErinnerung, dass die Elemente der Matrizen A und A zienten, die in der Schlupfform vorkommen, sind.)
29.3 Der Simplexalgorithmus
883
Pivot(N, B, A, b, c, v, l, e) 1 // Berechne die Koeffizienten der Gleichung für die neue Basisvariable xe . eine neue m × n-Matrix 2 sei A 3 be = bl /ale 4 for alle j ∈ N − {e} 5 aej = alj /ale 6 ael = 1/ale 7 // Berechne die Koeffizienten der übrigen Nebenbedingungen. 8 for alle i ∈ B − {l} bi = bi − aiebe 9 10 for alle j ∈ N − {e} 11 aij = aij − aie aej 12 ail = −aie ael 13 // Berechne die Zielfunktion. 14 v = v + cebe 15 for alle j ∈ N − {e} aej 16 cj = cj − ce 17 cl = −ce ael 18 // Berechne die neuen Mengen der Basis- und Nichtbasisvariablen. = N − {e} ∪ {l} 19 N 20 B = B − {l} ∪ {e} , B, A, b, 21 return (N c, v) Die Prozedur Pivot arbeitet folgendermaßen. Die Zeilen 3–6 berechnen die Koeffizienten der neuen Gleichung für xe , indem die Gleichung, in der xl auf der linken Seite steht, so umgeformt wird, dass stattdessen xe auf der linken Seite steht. Die Zeilen 8–12 aktualisieren die übrigen Gleichungen, indem überall, wo xe vorkommt, die rechte Seite dieser neuen Gleichung eingesetzt wird. Die Zeilen 14–17 führen diese Substitution für die Zielfunktion durch, und die Zeilen 19 und 20 aktualisieren die Mengen der Basisund Nichtbasisvariablen. Zeile 21 gibt die neue Schlupfform zurück. Die Prozedur Pivot würde im Falle ale = 0 einen Division-durch-Null-Fehler verursachen, aber wie wir in den Beweisen der Lemmata 29.2 und 29.12 sehen werden, wird Pivot nur aufgerufen, wenn ale = 0 gilt. Wir fassen nun die Auswirkungen der Prozedur Pivot auf die Werte der Variablen in den Basislösungen zusammen. Lemma 29.1 Betrachten Sie einen Aufruf von Pivot(N, B, A, b, c, v, l, e), für den ale = 0 gilt. , B, A, b, Seien (N c, v) die durch die Prozedur zurückgegebenen Werte und x ¯ die Basislösung nach dem Aufruf. Dann gilt . 1. x ¯j = 0 für alle j ∈ N 2. x ¯e = bl /ale .
884
29 Lineare Programmierung − {e}. 3. x ¯i = bi − aiebe für alle i ∈ B
Beweis: Die erste Aussage ist wahr, da die Basislösung immer alle Nichtbasisvariablen auf 0 setzt. Wenn wir alle Nichtbasisvariablen in einer Nebenbedingung aij xj xi = bi − j∈N
Da e ∈ B wegen der Zeile 3 von Pivot auf 0 setzen, dann gilt x ¯i = bi für alle i ∈ B. gilt, folgt x¯e = be = bl /ale , − {e} womit die zweite Aussage bewiesen ist. Analog folgt aus Zeile 9 für alle i ∈ B x¯i = bi = bi − aiebe , was die dritte Aussage beweist.
Formale Beschreibung des Simplexalgorithmus Wir sind nun in der Lage, den Simplexalgorithmus, den wir durch ein Beispiel veranschaulicht haben, formal einzuführen. Das Beispiel war ein besonders schönes, und wir hätten noch verschiedene andere Fragen ansprechen können: • Wie stellen wir fest, ob ein lineares Programm lösbar ist? • Was haben wir zu tun, wenn das lineare Programm lösbar ist, die initiale Basislösung aber nicht zulässig ist? • Wie stellen wir fest, ob ein lineares Programm unbeschränkt ist? • Wie wählen wir die Eingangs- und Ausgangsvariablen aus? In Abschnitt 29.5 werden wir zeigen, wie wir überprüfen können, ob ein Problem lösbar ist und, wenn dies der Fall ist, wie wir eine Schlupfform bestimmen können, in der die initiale Basislösung zulässig ist. Lassen Sie uns deshalb voraussetzen, dass wir eine Prozedur Initialize-Simplex(A, b, c) haben, die als Eingabe ein lineares Programm in Standardform erhält, d. h. eine m × n-Matrix A = (aij ), einen m-Vektor b = (bi ) und einen n-Vektor c = (cj ). Falls das Problem unlösbar ist, gibt die Prozedur eine Meldung zurück, dass das Programm unlösbar ist, und terminiert dann. Anderenfalls gibt die Prozedur eine Schlupfform des Problems zurück, für die die Basislösung zulässig ist. Die Prozedur Simplex erhält als Eingabe ein lineares Programm in Standardform (in der gerade beschriebenen Form). Sie gibt einen n-Vektor x ¯ = (¯ xj ) zurück, der eine optimale Lösung des in den Zeilen (29.19)–(29.21) beschriebenen linearen Programms ist.
29.3 Der Simplexalgorithmus
885
Simplex(A, b, c) 1 (N, B, A, b, c, v) = Initialize-Simplex(A, b, c) 2 sei Δ ein neuer Vektor der Länge m 3 while es gibt einen Index j ∈ N mit cj > 0 4 wähle einen Index e ∈ N , für den ce > 0 gilt 5 for jeden Index i ∈ B 6 if aie > 0 7 Δi = bi /aie 8 else Δi = ∞ 9 wähle einen Index l ∈ B, der Δl minimiert 10 if Δl = = ∞ 11 return “unbeschränkt” 12 else (N, B, A, b, c, v) = Pivot(N, B, A, b, c, v, l, e) 13 for i = 1 to n 14 if i ∈ B 15 x ¯i = bi 16 else x ¯i = 0 17 return (¯ x1 , x ¯2 , . . . , x ¯n ) Die Prozedur Simplex arbeitet wie folgt. In Zeile 1 ruft sie die oben beschriebene Prozedur Initialize- Simplex(A, b, c) auf, die entweder feststellt, dass das lineare Programm unlösbar ist, oder eine Schlupfform zurückgibt, für die die Basislösung zulässig ist. Die while-Schleife in den Zeilen 3–12 bildet den Hauptteil des Algorithmus. Wenn alle Koeffizienten der Zielfunktion negativ sind, terminiert die while-Schleife. Anderenfalls wählt Zeile 4 eine Variable xe als Eingangsvariable aus, deren Koeffizient in der Zielfunktion positiv ist. Wenn wir auch die Freiheit haben, eine beliebige dieser Variablen auszuwählen, setzen wir voraus, dass wir unsere Wahl nach einer vorher festgelegten deterministischen Regel treffen. Als nächstes überprüfen die Zeilen 5–9 alle Nebenbedingungen und wählt diejenige aus, die den Betrag, um den xe ohne Verletzung einer Nichtnegativitätsbedingung erhöht werden kann, am meisten einschränkt; die Basisvariable, die zu dieser Nebenbedingung gehört, ist xl . Wiederum kann es sein, dass wir die Wahl zwischen mehreren Variablen für die Ausgangsvariable haben; wir setzen wieder voraus, dass wir die Auswahl gemäß einer zuvor festgelegten deterministischen Regel treffen. Wenn keine der Nebenbedingungen den Betrag beschränkt, um den wir die Eingangsvariable erhöhen können, dann gibt der Algorithmus in Zeile 11 die Meldung „unbeschränkt“ zurück. Anderenfalls vertauscht Zeile 12 die Rollen der Eingangs- und der Ausgangsvariable, indem sie die oben beschriebene Unterroutine Pivot(N, B, A, b, c, v, l, e) aufruft. Die ¯2 , . . . , x ¯n der Variablen des ursprünglichen liZeilen 13–16 berechnen eine Lösung x ¯1 , x nearen Problems, indem sie alle Nichtbasisvariablen auf 0 und alle Basisvariablen x¯i auf bi setzen. Zeile 17 gibt schlussendlich die berechneten Werte dieser Variablen zurück. Um zu beweisen, dass der Algorithmus Simplex korrekt arbeitet, zeigen wir zunächst, dass er unter der Voraussetzung, dass er überhaupt terminiert und eine zulässige Anfangslösung vorliegt, entweder eine zulässige Lösung zurückgibt oder meldet, dass das lineare Programm unbeschränkt ist. Dann zeigen wir, dass der Algorithmus terminiert. Schließlich zeigen wir in Abschnitt 29.4 (Theorem 29.10), dass die zurückgegebene Lösung optimal ist.
886
29 Lineare Programmierung
Lemma 29.2 Gegeben sei ein lineares Programm (A, b, c) und wir setzen voraus, dass der Aufruf von Initialize-Simplex in der Zeile 1 von Simplex eine Schlupfform zurückgibt, für die die Basislösung zulässig ist. Falls Simplex in Zeile 17 eine Lösung zurückgibt, dann ist diese Lösung eine zulässige Lösung des linearen Programms. Wenn Simplex in Zeile 11 die Meldung „unbeschränkt“ zurückgibt, dann ist das lineare Programm unbeschränkt. Beweis: Wir verwenden die folgende dreiteilige Schleifeninvariante: Zu Beginn jeder Iteration der while-Schleife in den Zeilen 3–12 gilt 1. die Schlupfform ist äquivalent zu der Schlupfform, die durch den Aufruf der Prozedur Initialize-Simplex erzeugt wurde, 2. für alle i ∈ B gilt bi ≥ 0, 3. die zu der Schlupfform gehörende Basislösung ist zulässig. Initialisierung: Die Äquivalenz der Schlupfformen ist für die erste Iteration trivial. Wir setzen in der Aussage des Lemmas voraus, dass der Aufruf von InitializeSimplex in Zeile 1 der Prozedur Simplex eine Schlupfform zurückgibt, für die die Basislösung zulässig ist. Somit ist der dritte Teil der Invariante wahr. Da die Basislösung zulässig ist, ist jede Basisvariable xi nichtnegativ. Außerdem gilt bi ≥ 0 für alle i ∈ B, da die Basislösung jede Basisvariable xi auf bi setzt. Damit gilt der zweite Teil der Invariante. Fortsetzung: Wir werden zeigen, dass jede Iteration der while-Schleife die Schleifeninvariante erhält, wobei wir voraussetzen, dass die return-Anweisung in Zeile 11 nicht ausgeführt wird. Wir werden den Fall, in dem die return-Anweisung in Zeile 11 ausgeführt wird, behandeln, wenn wir die Terminierung diskutieren. Eine Iteration der while-Schleife vertauscht die Rollen einer Basisvariablen und einer Nichtbasisvariablen, indem sie die Prozedur Pivot aufruft. Wegen Übung 29.33 ist die Schlupfform äquivalent mit jener aus der vorherigen Iteration, die wegen der Schleifeninvariante äquivalent mit der initialen Schlupfform ist. Wir zeigen nun den zweiten Teil der Schleifeninvariante. Wir setzen voraus, dass zu Beginn jeder Iteration der while-Schleife bi ≥ 0 für alle i ∈ B gilt, und werden zeigen, dass diese Ungleichungen nach dem Aufruf von Pivot in Zeile 12 weiter gültig bleiben. Da die einzigen Änderungen der Variablen bi und der Menge B der Basisvariablen in dieser Anweisung erfolgen, genügt es zu zeigen, dass Zeile 12 diesen Teil der Invariante erhält. Wir bezeichnen die Werte vor dem Aufruf von Pivot mit bi , aij und B und die von Pivot zurückgegebenen Werte mit bi . Zunächst bemerken wir, dass be ≥ 0 gilt, denn es gilt bl ≥ 0 wegen der Schleifeninvariante, ale > 0 wegen der Zeilen 6 und 9 von Simplex und be = bl /ale wegen Zeile 3 von Pivot.
29.3 Der Simplexalgorithmus
887
Für die restlichen Indizes i ∈ B − {l} gilt bi = bi − aiebe = bi − aie (bl /ale )
(wegen Zeile 9 von Pivot) (wegen Zeile 3 von Pivot) .
(29.76)
Wir haben zwei Fälle in Abbhängigkeit, ob aie > 0 und aie ≤ 0 gilt, zu betrachten. Da wir im Falle von aie > 0 den Index l so wählen, dass die Ungleichung bl /ale ≤ bi /aie für alle i ∈ B
(29.77)
erfüllt ist, haben wir bi = bi − aie (bl /ale ) (wegen Gleichung (29.76)) ≥ bi − aie (bi /aie ) (wegen Ungleichung (29.77)) = bi − bi =0 und somit bi ≥ 0. Für aie ≤ 0 folgt aus Gleichung (29.76), dass bi nichtnegativ sein muss, da ale , bi und bl alle nichtnegativ sind. Wir begründen nun, dass die Basislösung zulässig ist, d. h. dass alle Variablen nichtnegative Werte haben. Die Nichtbasisvariablen sind auf 0 gesetzt und somit nichtnegativ. Jede Basisvariable xi wird durch die Gleichung aij xj xi = bi − j∈N
definiert. Die Basislösung setzt x ¯i = bi . Unter Verwendung des zweiten Teils der Schleifeninvariante folgt also, dass alle Basisvariablen x ¯i nichtnegativ sind. Terminierung: Die while-Schleife kann auf zwei verschiedene Weisen terminieren. Wenn sie wegen der Bedingung in Zeile 3 terminiert, dann ist die aktuelle Lösung zulässig und Zeile 17 gibt diese Lösung zurück. Ansonsten terminiert sie, indem sie in Zeile 11 „unbeschränkt“ zurückgibt. In diesem Fall stellen wir fest, dass für jede Iteration der for-Schleife in den Zeilen 5–8 bei Ausführung von Zeile 6 aie ≤ 0 gilt. Betrachten Sie die Lösung x ¯, die durch ⎧ falls i = e , ⎪ ⎨∞ falls i ∈ N − {e} , x ¯i = 0 ⎪ ⎩b − a x ¯ falls i ∈ B i j∈N ij j definiert ist. Wir zeigen nun, dass diese Lösung zulässig ist, d. h. dass alle Variablen nichtnegativ sind. Die Nichtbasisvariablen außer x ¯e sind 0 und es gilt x¯e = ∞ > 0; also sind alle Nichtbasisvariablen nichtnegativ. Für alle Basisvariablen x ¯i gilt aij x ¯j x ¯i = bi − j∈N
= bi − aie x¯e .
888
29 Lineare Programmierung Die Schleifeninvariante impliziert bi ≥ 0, und wir haben aie ≤ 0 und x ¯e = ∞ > 0. Daraus folgt x ¯i ≥ 0. Nun zeigen wir, dass der Zielfunktionswert für die Lösung x ¯ unbeschränkt ist. Wegen Gleichung (29.42) ist der Zielfunktionswert z=v+ cj x ¯j j∈N
= v + ce x ¯e . Wegen ce > 0 (nach Zeile 4 von Simplex) und x ¯e = ∞ ist der Zielfunktionswert ∞ und das lineare Programm ist unbeschränkt. Es bleibt zu zeigen, dass die Prozedur Simplex terminiert und dass die Lösung, die er zurückgibt, optimal ist. Abschnitt 29.4 beschäftigt sich mit der Optimalität. Wir diskutieren jetzt die Terminierung.
Terminierung In dem zu Beginn des Kapitels angegebenen Beispiel erhöhte jede Iteration des Simplexalgorithmus den zur Basislösung gehörigen Zielfunktionswert. Wie Sie in Übung 29.3-2 zeigen sollen, kann keine Iteration der Prozedur Simplex den zur Basislösung gehörigen Zielfunktionswert verringern. Leider ist es aber möglich, dass eine Iteration den Zielfunktionswert unverändert lässt. Dieses als Entartung bezeichnetes Phänomen werden wir nun detaillierter untersuchen. Die Zuweisung v = v + cebe in Zeile 14 der Prozedur Pivot ändert den Zielfunktionswert. Da der Algorithmus Simplex die Prozedur Pivot nur aufruft, wenn ce > 0 gilt, kann der Zielfunktionswert nur dann unverändert (d. h. v = v) bleiben, wenn be gleich 0 ist. Dieser Wert wird als be = bl /ale in Zeile 3 von Pivot zugewiesen. Da wir Pivot immer mit ale = 0 aufrufen, sehen wir, dass bl = 0 gelten muss, damit be gleich 0 ist und die Zielfunktion unverändert bleibt. In der Tat, diese Situation kann eintreten. Betrachten Sie das lineare Programm z = x1 + x2 + x3 x4 = 8 − x1 − x2 x5 = x2 − x3 . Nehmen Sie an, wir würden x1 als Eingangs- und x4 als Ausgangsvariable wählen. Nach dem Basistausch erhalten wir z = 8 + x3 − x4 x1 = 8 − x2 − x4 x5 = x2 − x3 .
29.3 Der Simplexalgorithmus
889
An dieser Stelle können wir zum Basistausch nur noch x3 als Eingangs- und x5 als Ausgangsvariable wählen. Wegen b5 = 0 bleibt der Zielfunktionswert 8 bei diesem Basistausch unverändert: z = 8+x2 −x4 −x5 x1 = 8−x2 −x4 x3 = x2 −x5 . Der Zielfunktionswert hat sich nicht verändert, wohl aber unsere Schlupfform. Erfreulicherweise wächst der Zielfunktionswert (auf den Wert 16), wenn wir den nächsten Basistausch – mit x2 als Eingangs- und x1 als Ausgangsvariable – machen, und der Simplexalgorithmus kann fortfahren. Entartung kann verhindern, dass der Simplexalgorithmus terminiert, da er zu einem Phänomen führen kann, den wir unter Kreisen (engl.: cycling ) kennen: Die Schlupfformen in zwei verschiedenen Iterationen von Simplex sind identisch. Aufgrund der Entartung könnte Simplex eine Folge von Pivotierschritten auswählen, in der der Zielfunktionswert unverändert bleibt und eine Schlupfform sich wiederholt. Da Simplex ein deterministischer Algorithmus ist, wird er, wenn er einmal kreist, immer wieder durch die gleiche Folge von Schlupfformen kreisen und nie terminieren. Kreisen ist der einzige Grund, dass Simplex möglicherweise nicht terminiert. Um diese Tatsache zu zeigen, müssen wir zuerst noch einigen zusätzlichen Aufwand treiben. In jeder Iteration verwaltet Simplex neben den Mengen N und B die Daten A, b, c und v. Wenngleich wir A, b, c und v explizit halten müssen, um den Simplexalgorithmus effizient implementieren zu können, kommen wir prinzipiell auch ohne dies aus. In anderen Worten, die Menge der Basisvariablen und die Menge der Nichtbasisvariablen reichen aus, um die Schlupfform eindeutig zu bestimmen. Bevor wir dies beweisen, beweisen wir ein sehr nützliches algebraisches Lemma. Lemma 29.3 Sei I eine Menge von Indizes. Für alle j ∈ I, seien αj und βj reelle Zahlen und sei xj eine reellwertige Variable. Sei γ eine reelle Zahl. Setzen Sie voraus, dass für alle Belegungen der xj αj xj = γ + βj xj . (29.78) j∈I
j∈I
gilt. Dann ist αj = βj für alle j ∈ I und γ = 0. Beweis: Da die Gleichung (29.78) für beliebige Werte der xj gilt, können wir spezielle Werte wählen, um Schlussfolgerungen über α, β und γ zu ziehen. Wenn wir xj = 0 für alle j ∈ I setzen, können wir folgern, dass γ = 0 ist. Wählen Sie nun einen beliebigen Index j ∈ I und setzen Sie xj = 1 und xk = 0 für alle k = j. Dann muss αj = βj gelten. Da wir j beliebig aus I gewählt hatten, können wir folgern, dass αj = βj für alle j ∈ I gilt.
890
29 Lineare Programmierung
Ein spezielles lineares Programm besitzt viele verschiedene Schlupfformen; erinnern Sie sich daran, dass jede Schlupfform die gleiche Menge von zulässigen und optimalen Lösungen hat wie das ursprüngliche lineare Programm. Wir zeigen nun, dass die Schlupfform eines linearen Programms durch die Menge der Basisvariablen eindeutig bestimmt ist, d. h., ist eine Menge von Basisvariablen gegeben, so gehört eine eindeutige Schlupfform (eindeutige Menge von Koeffizienten und rechten Seiten) zu diesen Basisvariablen. Lemma 29.4 Sei (A, b, c) ein lineares Programm in Standardform. Ist eine Menge B von Basisvariablen gegeben, so ist die zugehörige Schlupfform eindeutig bestimmt. Beweis: Nehmen Sie zum Zwecke des Widerspruchs an, dass es zwei unterschiedliche Schlupfformen mit der gleichen Menge B von Basisvariablen gäbe. Die Schlupfformen müssen dann auch identische Mengen N = {1, 2, . . . , n + m}−B von Nichtbasisvariablen haben. Wir schreiben die erste Schlupfform als z=v+ cj xj (29.79) j∈N
xi = bi −
aij xj für i ∈ B ,
(29.80)
cj xj
(29.81)
aij xj für i ∈ B .
(29.82)
j∈N
und die zweite als z = v +
j∈N
xi = bi −
j∈N
Betrachten Sie das Gleichungssystem, das wir erhalten, wenn wir jede der Gleichungen in Zeile (29.82) von der entsprechenden Gleichung in Zeile (29.80) subtrahieren. Das resultierende System lautet 0 = (bi − bi ) − (aij − aij )xj für i ∈ B j∈N
oder äquivalent dazu aij xj = (bi − bi ) + aij xj für i ∈ B . j∈N
j∈N
Wenden Sie nun für alle i ∈ B Lemma 29.3 mit αj = aij , βj = aij und γ = bi − bi an. Wegen αj = βj gilt aij = aij für alle j ∈ N , und wegen γ = 0 gilt bi = bi . Für die beiden Schlupfformen sind also A und b mit A und b identisch. Übung 29.3-1 zeigt
29.3 Der Simplexalgorithmus
891
mit einer gleichen Argumentation, dass auch c = c und v = v gilt, und somit sind die beiden Schlupfformen identisch. Wir zeigen nun, dass Entartung der einzige Grund ist, der dazu führen kann, dass der Simplexalgorithmus nicht terminiert. Lemma 29.5 Falls Simplex nach maximal
n+m m -Iteration nicht terminiert, dann kreist er.
Beweis: Nach Lemma 29.4 bestimmt die Menge B der Basisvariablen eindeutig eine Es gibt n + m Variablen und es gilt |B| = m. Daher gibt es höchstens Schlupfform. n+m Möglichkeiten für die Wahl der Menge B. Somit gibt es nur höchstens n+m m m n+m eindeutige Schlupfformen. Wenn Simplex also mehr als m Iterationen ausführt, muss er kreisen. Kreisen ist theoretisch möglich, aber extrem selten. Wir können Kreisen vermeiden, wenn wir die Eingangs- und Ausgangsvariablen mit einer gewissen Sorgfalt wählen. Eine Möglichkeit besteht darin, dass wir die Eingabe leicht stören, sodass es unmöglich ist, zwei Lösungen mit dem gleichen Zielfunktionswert zu haben. Eine andere Möglichkeit besteht darin, Nichteindeutigkeiten aufzulösen, indem wir immer die Variable mit dem kleinsten Index wählen, eine Strategie, die als Blandsche Regel bekannt ist. Wir sehen von dem Beweis ab, dass diese Strategien Kreisen vermeiden. Lemma 29.6 Wenn die Zeilen 4 und 9 von Simplex Nichteindeutigkeiten immer auflösen, indem sie jeweils die Variable mit dem kleinsten Index wählen, dann muss Simplex terminieren. Wir beschließen diesen Abschnitt mit dem folgenden Lemma. Lemma 29.7 Unter der Voraussetzung, dass die Prozedur Initialize-Simplex eine Schlupfform zurückgibt, für die die Basislösung zulässig ist, meldet der Algorithmus Simplex entweder, n+m dass das lineare Programm unbeschränkt ist, oder terminiert nach maximal Iterationen mit einer zulässigen Lösung. m Beweis: Die Lemmata 29.2 und 29.6 zeigen, dass, wenn Initialize-Simplex eine Schlupfform mit zulässiger Basislösung zurückgibt, Simplex entweder meldet, dass das lineare Programm unbeschränkt ist, oder mit einer zulässigen Lösung terminiert. Wenn Simplex mit einer zulässigen Lösung terminiert, dann wissen wir aus Lemma 29.5, dass dies nach maximal n+m Iterationen geschieht. m
892
29 Lineare Programmierung
Übungen 29.3-1 Vervollständigen Sie den Beweis von Lemma 29.4, indem Sie zeigen, dass c = c und v = v gelten muss. 29.3-2 Zeigen Sie, dass der Aufruf der Prozedur Pivot in Zeile 12 von Simplex den Wert von v nie verringert. 29.3-3 Beweisen Sie, dass die Schlupfform, die an die Prozedur Pivot übergeben wird, und die Schlupfform, die die Prozedur zurückgibt, äquivalent sind. 29.3-4 Nehmen Sie an, wir würden ein lineares Programm (A, b, c) in Standardform in die Schlupfform überführen. Zeigen Sie, dass die Basislösung genau dann zulässig ist, wenn bi ≥ 0 für i = 1, 2, . . . , m gilt. 29.3-5 Lösen Sie das folgende lineare Programm unter Verwendung von Simplex: maximiere
18x1 + 12, 5 x2
unter den Nebenbedingungen x1 + x2 x1 x2 x1 , x2
≤ 20 ≤ 12 ≤ 16 ≥ 0.
29.3-6 Lösen Sie das folgende lineare Programm unter Verwendung von Simplex: maximiere
5x1 − 3x2
unter den Nebenbedingungen x1 − x2 ≤ 1 2x1 + x2 ≤ 2 x1 , x2 ≥ 0 . 29.3-7 Lösen Sie das folgende lineare Programm unter Verwendung von Simplex: minimiere
x1 + x2 + x3
unter den Nebenbedingungen 2x1 + 7, 5x2 + 3x3 ≥ 10000 20x1 + 5x2 + 10x3 ≥ 30000 x1 , x2 , x3 ≥ 0. 29.3-8 In dem Beweis von Lemma 29.5 argumentierten wir, dass es höchstens m+n n Möglichkeiten gibt, eine Menge B von Basisvariablen zu wählen. Geben Sie ein Beispiel eines linearen Programms an, in dem es echt weniger als m+n n Möglichkeiten gibt, B zu wählen.
29.4 Dualität
29.4
893
Dualität
Wir haben bewiesen, dass Simplex unter bestimmten Voraussetzungen terminiert. Wir haben jedoch noch nicht gezeigt, dass er tatsächlich eine optimale Lösung für das lineare Programm findet. Um dies tun zu können, führen wir ein mächtiges Konzept ein, das unter dem Namen Dualität der linearen Programmierung bekannt ist. Dualität erlaubt uns, zu beweisen, dass eine Lösung tatsächlich optimal ist. Wir sahen in Kapitel 26 mit Theorem 26.6, dem maxflow-mincut-Theorem, ein Beispiel für Dualität. Setzen Sie voraus, wir hätten zu einer gegebenen Instanz des maximalenFluss-Problems einen Fluss f mit dem Wert |f | bestimmt. Woher wissen wir, ob f ein maximaler Fluss ist? Nach dem maxflow-mincut-Theorem wissen wir, dass f dann ein maximaler Fluss ist, wenn wir einen Schnitt finden können, dessen Wert ebenfalls |f | ist. Diese Beziehung stellt ein Beispiel für Dualität dar: Zu einem gegebenen Maximierungsproblem definieren wir ein zugehöriges Minimierungsproblem, sodass beide Probleme den gleichen optimalen Zielfunktionswert haben. Wir werden beschreiben, wie wir zu einem gegebenen linearen Programm, dessen Zielfunktion wir maximieren wollen, ein duales lineares Programm formulieren können, für das die Zielfunktion zu minimieren ist und dessen optimaler Wert identisch mit dem des ursprünglichen linearen Programms ist. Wenn wir uns auf duale lineare Programme beziehen, bezeichnen wir das ursprüngliche lineare Programm als das primale lineare Programm. Ist ein primales lineares Programm in Standardform (siehe Gleichungen (29.16)–(29.18)) gegeben, dann definieren wir das duale lineare Programm durch minimiere
m
(29.83)
bi yi
i=1
unter den Nebenbedingungen m
aij yi ≥ cj
für j = 1, 2, . . . , n ,
(29.84)
yi ≥ 0
für i = 1, 2, . . . , m .
(29.85)
i=1
Um das duale Problem zu formulieren, ändern wir die Forderung nach Maximierung in die Forderung nach Minimierung, vertauschen die Rollen der Koeffizienten auf den rechten Seiten der Nebenbedingungen und der Koeffizienten der Zielfunktion und ersetzen ≤ durch ≥. Jeder der m Nebenbedingungen des primalen Problems entspricht eine Variable yi im dualen Problem und jeder der n Nebenbedingungen des dualen Problems entspricht eine Variable xj im primalen Problem. Betrachten Sie als Beispiel das in den Zeilen (29.53)–(29.57) gegebene lineare Programm. Das duale lineare Programm dazu ist minimiere
30y1 + 24y2 + 36y3
(29.86)
894
29 Lineare Programmierung unter den Nebenbedingungen y1 + 2y2 + 4y3 y1 + 2y2 + y3 3y1 + 5y2 + 2y3 y1 , y2 , y3
≥3 ≥1 ≥2 ≥0.
(29.87) (29.88) (29.89) (29.90)
Wir werden in Theorem 29.10 zeigen, dass der optimale Wert des dualen linearen Programms immer gleich dem optimalen Wert des primalen linearen Programms ist. Außerdem löst der Simplexalgorithmus in Wirklichkeit implizit das primale und das duale lineare Programm simultan – was einen Beweis der Optimalität liefert. Wir wollen zuerst nachweisen, dass schwache Dualität gilt, d. h. der Wert einer zulässigen Lösung des primalen linearen Programms nie größer ist als der Wert einer zulässigen Lösung des dualen linearen Programms. Lemma 29.8: (Schwache Dualität der linearen Programmierung) Sei x¯ eine zulässige Lösung des primalen linearen Programms (29.16)–(29.18) und y¯ eine zulässige Lösung des dualen linearen Programms (29.83)–(29.85). Dann gilt n
cj x ¯j ≤
j=1
m
bi y¯i .
i=1
Beweis: Es gilt n
cj x ¯j ≤
j=1
2m n j=1
=
m i=1
≤
m
i=1
⎛ ⎝
n
3 aij y¯i
x ¯j
(wegen Ungleichung (29.84))
⎞ aij x ¯j ⎠ y¯i
j=1
bi y¯i
(wegen Ungleichung (29.17)) .
i=1
Korollar 29.9 Sei x¯ eine zulässige Lösung eines primalen linearen Programms (A, b, c) und y¯ eine zulässige Lösung des zugehörigen dualen linearen Programms. Falls n j=1
cj x ¯j =
m i=1
bi y¯i
29.4 Dualität
895
gilt, dann sind x ¯ und y¯ optimale Lösungen des primalen beziehungsweise des dualen linearen Programms. Beweis: Nach Lemma 29.8 kann der Zielfunktionswert einer zulässigen Lösung des primalen linearen Programms nicht größer werden als der Zielfunktionswert einer zulässigen Lösung des dualen Problems. Wenn also die zulässigen Lösungen x ¯ und y¯ den gleichen Zielfunktionswert haben, kann keine von beiden mehr verbessert werden. Bevor wir beweisen, dass es immer eine duale Lösung gibt, deren Wert gleich dem einer optimalen primalen Lösung ist, beschreiben wir, wie wir eine solche Lösung finden können. Als wir den Simplexalgorithmus für das lineare Programm (29.53)–(29.57) laufen ließen, generierte die letzte Iteration die Schlupfform (29.72)–(29.75) mit der Zielfunktion z = 28 − x3 /6 − x5 /6 − 2x6 /3, B = {1, 2, 4} und N = {3, 5, 6}. Wie wir weiter unten zeigen werden, ist die zu der finalen Schlupfform gehörende Basislösung tatsächlich eine optimale Lösung des linearen Programms; eine optimale Lösung des linearen Programms (29.53)–(29.57) ist daher (¯ x1 , x ¯2 , x ¯3 ) = (8, 4, 0) mit dem Zielfunktionswert (3 · 8) + (1 · 4) + (2 · 0) = 28. Wie wir ebenfalls weiter unten zeigen werden, können wir daraus eine optimale duale Lösung ablesen: Die negativen Werte der Koeffizienten der primalen Zielfunktion sind die Werte der dualen Variablen. Genauer gesagt, wenn die letzte Schlupfform des primalen Problems gleich z = v + cj xj j∈N
xi =
bi
−
aij xj für i ∈ B
j∈N
ist, dann setzen wir −cn+i y¯i = 0
falls (n + i) ∈ N , , sonst
(29.91)
um eine optimale duale Lösung zu erzeugen. Eine optimale Lösung des dualen linearen Programms, das in den Zeilen (29.86)–(29.90) definiert ist, ist also y¯1 = 0 (da n + 1 = 4 ∈ B), y¯2 = −c5 = 1/6 und y¯3 = −c6 = 2/3. Wenn wir die duale Zielfunktion (29.86) auswerten, erhalten wir den Zielfunktionswert (30 · 0) + (24 · (1/6)) + (36 · (2/3)) = 28, was bestätigt, dass der Zielfunktionswert des primalen Problems tatsächlich gleich dem Zielfunktionswert des dualen Problems ist. Kombinieren wir diese Berechnungen mit Lemma 29.8, dann haben wir einen Beweis dafür, dass der optimale Zielfunktionswert des primalen linearen Programms gleich 28 ist. Wir zeigen nun, dass dieser Ansatz allgemein anwendbar ist: Wir können eine optimale Lösung des dualen linearen Problems finden und gleichzeitig beweisen, dass eine Lösung zu dem primalen linearen Problem optimal ist.
896
29 Lineare Programmierung
Theorem 29.10: (Dualität der linearen Programmierung) ¯2 , . . . , x¯n ) Setzen Sie voraus, dass der Simplexalgorithmus die Werte x¯ = (¯ x1 , x für das primale lineare Programm (A, b, c) zurückgibt. Seien N und B die Mengen der Nichtbasis- bzw. der Basisvariablen in der finalen Schlupfform und c die Koeffizienten der finalen Schlupfform. Weiter sei y¯ = (¯ y1 , y¯2 , . . . , y¯m ) durch die Gleichung (29.91) definiert. Dann ist x ¯ eine optimale Lösung des primalen linearen Programms, y¯ eine optimale Lösung des dualen linearen Programms und es gilt n
cj x ¯j =
j=1
m
(29.92)
bi y¯i .
i=1
Beweis: Wenn wir zulässige Lösungen x¯ und y¯ finden können, die der Gleichung (29.92) genügen, dann müssen x¯ und y¯ nach Korollar 29.9 optimale primale und duale Lösungen sein. Wir werden nun zeigen, dass die in der Aussage des Theorems beschriebenen Lösungen x ¯ und y¯ Gleichung (29.92) erfüllen. Setzen Sie voraus, dass wir den Simplexalgorithmus für ein primales lineares Programm, das wie in den Zeilen (29.16)–(29.18) gegeben ist, laufen lassen. Der Algorithmus durchläuft eine Folge von Schlupfformen, bis er mit einer finalen Schlupfform mit der Zielfunktion cj xj (29.93) z = v + j∈N
terminiert. Da der Algorithmus mit einer Lösung terminiert, wissen wir wegen der Bedingung in Zeile 3 des Pseudocodes, dass cj ≤ 0 für alle j ∈ N
(29.94)
gilt. Definieren wir cj = 0 für alle j ∈ B ,
(29.95)
so können wir die Gleichung (29.93) umformen in z = v +
cj xj
j∈N
= v +
cj xj +
j∈N
= v +
n+m
cj xj
(da cj = 0 für j ∈ B)
j∈B
cj xj
(da N ∪ B = {1, 2, . . . , n + m}) .
(29.96)
j=1
Für die zu dieser finalen Schlupfform gehörende Basislösung x ¯ gilt x ¯j = 0 für alle j ∈ N ¯ in die und z = v . Da alle Schlupfformen äquivalent sind, müssen wir, wenn wir x
29.4 Dualität
897
ursprüngliche Zielfunktion einsetzen, den gleichen Zielfunktionswert erhalten: n
cj x ¯j = v +
j=1
n+m
cj x ¯j
(29.97)
j=1
= v +
j∈N
=v +
cj x¯j +
cj x ¯j
j∈B
(cj
· 0) +
j∈N
(0 · x¯j )
(29.98)
j∈B
= v . Wir zeigen nun, dass die durch die Gleichung (29.91) definierte y¯ für das liduale Lösung m n ¯j neare Programm zulässig ist, und dass ihr Zielfunktionswert i=1 bi y¯i gleich j=1 cj x ist. Gleichung (29.97) besagt, dass die erste und die letzte Schlupfform, ausgewertet an der Stelle x¯, den gleichen Wert ergeben. Allgemeiner folgt aus der Äquivalenz aller Schlupfformen, dass für jede Menge von Werten x = (x1 , x2 , . . . , xn ) n
cj xj = v +
j=1
n+m
cj xj
j=1
gilt. Daher gilt für jede spezielle Menge von Werten x ¯ = (¯ x1 , x ¯2 , . . . , x ¯n ) n
cj x ¯j
j=1
=v +
n+m
cj x ¯j
j=1
=v +
n
cj x ¯j
+
j=1
= v +
n
cj x ¯j +
j=1
= v +
= v +
n
cj x ¯j +
n
m
= v +
j=1 n
v −
⎛ (−¯ yi ) ⎝bi −
i=1
cj x ¯j − cj x ¯j −
j=1
2 =
n
m i=1
m i=1 m
3
+
n
⎞ aij x ¯j ⎠
j=1
bi y¯i + bi y¯i +
i=1
bi y¯i
(wg. Gleichung (29.91) und (29.95))
(−¯ yi ) x ¯n+i
i=1
cj x ¯j +
cj x ¯j
j=n+1 m cn+i x¯n+i i=1 m
j=1
j=1
= v +
n+m
n j=1
2
m n i=1 j=1 n m
(aij x ¯j ) y¯i (aij y¯i ) x¯j
j=1 i=1
cj
+
m i=1
3 aij y¯i
x ¯j ,
(wg. Gleichung (29.32))
898
29 Lineare Programmierung
sodass n
2 cj x ¯j =
v −
j=1
m i=1
3 bi y¯i
+
n
2 cj
+
j=1
m
3 aij y¯i
x ¯j .
(29.99)
i=1
folgt. Wenden wir Lemma 29.3 auf Gleichung (29.99) an, erhalten wir v − cj +
m
bi y¯i = 0 ,
i=1 m
aij y¯i = cj für j = 1, 2, . . . , n .
(29.100) (29.101)
i=1
m Nach Gleichung (29.100) gilt i=1 bi y¯i = v , sodass der Zielfunktionswert des dualen m ¯i gleich dem des primalen linearen Problems (v ) ist. Es linearen Problems i=1 bi y bleibt noch zu zeigen, dass die Lösung y¯ zulässig für das duale lineare Problem ist. Aus den Gleichungen (29.94) und (29.95) folgt cj ≤ 0 für alle j = 1, 2, . . . , n + m. Damit folgt aus Gleichung (29.101) für jedes j = 1, 2, . . . , m cj = cj +
m
aij y¯i
i=1
≤
m
aij y¯i ,
i=1
was die Nebenbedingungen (29.84) des dualen linearen Problems erfüllt. Schlussendlich gilt y¯i ≥ 0, da cj ≤ 0 für alle j ∈ N ∪ B gilt, wenn wir y¯ gemäß Gleichung (29.91) setzen. Also ist auch die Nichtnegativitätsbedingung erfüllt. Wir haben damit Folgendes gezeigt: Falls die Prozedur Initialize-Simplex für ein lösbares lineares Programm eine zulässige Lösung zurückgibt und der Simplexalgorithmus nicht mit der Meldung „unbeschränkt“ terminiert, dann ist die zurückgegebene Lösung tatsächlich eine optimale Lösung. Außerdem haben wir gezeigt, wie eine optimale Lösung des dualen linearen Programms konstruiert werden kann.
Übungen 29.4-1 Formulieren Sie das duale lineare Programm zu dem in Übung 29.3–5 angegebenen linearen Programm. 29.4-2 Nehmen Sie an, wir hätten ein lineares Programm, das nicht in Standardform ist. Wir könnten zu diesem das duale lineare Programm erzeugen, indem wir es zunächst in die Standardform überführen und dann das duale lineare Programm bilden. Es wäre jedoch bequemer, wenn wir das duale lineare
29.5 Die initiale zulässige Basislösung
899
Programm direkt erzeugen könnten. Erklären Sie, wie wir zu einem beliebigen linearen Programm auf direktem Weg das duale lineare Programm bilden können. 29.4-3 Schreiben Sie das duale lineare Programm zu dem linearen Programm zur Berechnung eines maximalem Flusses auf, das durch die Zeilen (29.47)–(29.50) auf Seite 874 gegeben ist. Erläutern Sie, wie wir diese Formulierung als minimales-Schnitt-Problem interpretieren können. 29.4-4 Schreiben Sie das duale lineare Programm zu dem linearen Programm zur Berechnung eines Flusses mit minimalen Kosten auf, das durch die Zeilen (29.51)–(29.52) auf Seite 876 gegeben ist. Erläutern Sie, wie wir dieses Problem hinsichtlich Graphen und Flüssen interpretieren können. 29.4-5 Zeigen Sie, dass das duale lineare Programm eines dualen linearen Programms eines linearen Programms das primale lineare Programm ist. 29.4-6 Welches Ergebnis aus Kapitel 26 kann als schwache Dualität für das maximale Flussproblem interpretiert werden?
29.5
Die initiale zulässige Basislösung
In diesem Abschnitt beschreiben wir zunächst, wie wir überprüfen können, ob ein lineares Programm lösbar ist und, wenn dies der Fall ist, wie man eine Schlupfform erzeugt, für die die Basislösung zulässig ist. Wir schließen mit dem Beweis des Fundamentalsatzes der linearen Programmierung, der besagt, dass die Prozedur Simplex immer das korrekte Ergebnis liefert.
Bestimmung einer Anfangslösung In Abschnitt 29.3 haben wir vorausgesetzt, dass wir eine Prozedur Initialize-Simplex haben, die feststellt, ob ein lineares Programm überhaupt zulässige Lösungen besitzt, und, wenn dies der Fall ist, eine Schlupfform mit einer zulässigen Basislösung zurückgibt. Wir beschreiben diese Prozedur hier. Ein lineares Programm kann lösbar sein, auch wenn die initiale Basislösung nicht zulässig ist. Betrachten Sie zum Beispiel das folgende lineare Programm: maximiere 2x1 − x2
(29.102)
unter den Nebenbedingungen 2x1 − x2 ≤ 2 x1 − 5x2 ≤ −4 x1 , x2 ≥ 0 .
(29.103) (29.104) (29.105)
900
29 Lineare Programmierung
Wenn wir dieses lineare Programm in Schlupfform überführen würden, dann wäre die Basislösung x1 = 0 und x2 = 0. Diese Lösung verletzt die Nebenbedingung (29.104) und ist daher nicht zulässig. Die Prozedur Initialize-Simplex kann also nicht einfach nur die offensichtliche Schlupfform zurückgeben. Um festzustellen, ob ein lineares Programm eine zulässige Lösung hat, formulieren wir ein lineares Hilfsprogramm. Für dieses lineare Hilfsprogramm können wir (mit ein wenig Aufwand) eine Schlupfform finden, für die die Basislösung zulässig ist. Außerdem bestimmt die Lösung dieses linearen Hilfsprogramms, ob das ursprüngliche lineare Programm lösbar ist, und liefert, wenn dies der Fall ist, eine zulässige Lösung, mit der wir den Simplexalgorithmus initialisieren können. Lemma 29.11 Sei L ein lineares Programm in der Standardform (29.16)–(29.18). Sei x0 eine neue Variable und sei Laux das folgende lineare Programm mit n + 1 Variablen: maximiere − x0
(29.106)
unter den Nebenbedingungen n
aij xj − x0 ≤ bi
für i = 1, 2, . . . , m ,
(29.107)
xj ≥ 0 für j = 0, 1, . . . , n .
(29.108)
j=1
Das lineare Programm L ist genau dann lösbar, wenn der optimale Zielfunktionswert von Laux gleich 0 ist. Beweis: Setzen Sie voraus, dass L eine zulässige Lösung x¯ = (¯ x1 , x ¯2 , . . . , x ¯n ) hat. ¯ eine zulässige Lösung für Laux mit dem Dann ist die Lösung x¯0 = 0 zusammen mit x Zielfunktionswert 0. Da x0 ≥ 0 eine Nebenbedingung von Laux ist und die Zielfunktion −x0 maximiert werden soll, muss diese Lösung für Laux optimal sein. Für die Umkehrung setzen Sie voraus, dass der optimale Zielfunktionswert von Laux gleich 0 ist. Dann gilt x ¯0 = 0 und die Werte der restlichen Variablen von x ¯ erfüllen die Nebenbedingungen von L. Nun beschreiben wir die Strategie, mit der wir eine zulässige initiale Basislösung für ein lineares Programm L in Standardform bestimmen:
29.5 Die initiale zulässige Basislösung
901
Initialize-Simplex(A, b, c) 1 sei k der Index der kleinsten Komponente von b // ist die initiale Basislösung zulässig? 2 if bk ≥ 0 3 return ({1, 2, . . . , n} , {n + 1, n + 2, . . . , n + m} , A, b, c, 0) 4 bilde Laux durch Addieren von −x0 zur linken Seite jeder Gleichung und Setzen der Zielfunktion auf −x0 5 sei (N, B, A, b, c, v) die resultierende Schlupfform für Laux 6 l = n+k 7 // Laux hat n + 1 Nichtbasisvariablen und m Basisvariablen. 8 (N, B, A, b, c, v) = Pivot(N, B, A, b, c, v, l, 0) 9 // Die Basislösung ist nun für Laux zulässig. 10 iteriere die while-Schleife in den Zeilen 3–12 von Simplex, bis eine optimale Lösung von Laux gefunden ist 11 if die optimale Lösung von Laux setzt x ¯0 auf 0 12 if x ¯0 ist eine Basisvariable 13 führe einen (entarteten) Basistausch durch, um sie zu einer Nichtbasisvariable zu machen 14 lösche x0 aus den Nebenbedingungen der finalen Schlupfform von Laux und restauriere die ursprüngliche Zielfunktion von L, aber ersetze jede Basisvariable in dieser Zielfunktion durch die rechte Seite ihrer zugehörigen Nebenbedingung 15 return die modifizierte finale Schlupfform 16 else return “nicht lösbar” Die Prozedur Initialize-Simplex arbeitet folgendermaßen. In den Zeilen 1–3 testen wir implizit die Basislösung der initialen Schlupfform für L, die durch N = {1, 2, . . . , n}, B = {n + 1, n + 2, . . . , n + m}, x¯i = bi für alle i ∈ B und x¯j = 0 für alle j ∈ N gegeben ist. (Das Erzeugen der Schlupfform erfordert keinen expliziten Aufwand, da die Werte von A, b und c in der Schlupfform die gleichen sind wie in der Standardform.) Wenn Zeile 2 herausfindet, dass diese Basislösung eine zulässige Lösung ist – d. h. wenn x ¯i ≥ 0 für alle i ∈ N ∪B gilt – dann gibt Zeile 3 die Schlupfform zurück. Anderenfalls bilden wir in Zeile 4 das lineare Hilfsprogramm Laux , wie in Lemma 29.11 angegeben. Da die initiale Basislösung für L nicht zulässig ist, kann auch die initiale Basislösung der Schlupfform für Laux nicht zulässig sein. Um eine zulässige Basislösung zu finden, führen wie einen einzigen Basistausch durch. Zeile 6 wählt l = n + k als den Index der Basisvariablen, die in dem bevorstehenden Pivotieren die Basis verlässt. Da xn+1 , xn+2 , . . . , xn+m die Basisvariablen sind, wird die Ausgangsvariable xl diejenige mit dem größten negativen Wert sein. Zeile 8 führt diesen Aufruf von Pivot mit x0 als Eingangs- und xl als Ausgangsvariable aus. Wir werden in Kürze sehen, dass die Basislösung, die sich aus diesem Aufruf von Pivot ergibt, zulässig ist. Da wir nun eine Schlupfform mit zulässiger Basislösung haben, können wir in Zeile 10 wiederholt die Prozedur Pivot aufrufen, um das lineare Hilfsprogramm vollständig zu lösen. In Abhängigkeit des Tests in Zeile 11, in dem abgefragt wird, ob wir eine optimale Lösung für Laux mit dem Zielfunktionswert 0 gefunden haben, erzeugen wir in den Zeilen 12–14 eine Schlupfform für L, für die die zugehörige Basislösung zulässig ist. Hierzu behandeln wir zuerst in den Zeilen 12–13 den ¯0 = 0 ist. In diesem Fall führen entarteten Fall, in dem x0 eine Basisvariable mit Wert x
902
29 Lineare Programmierung
wir einen Basistausch durch, um x0 aus der Basis zu entfernen, indem wir ein e ∈ N so wählen, dass a0e = 0 gilt und als Eingangsvariable dienen kann. Die neue Basislösung bleibt zulässig und der degenerierte Basistausch verändert keinen Wert einer Variable. Als nächstes entfernen wir alle x0 -Terme aus den Nebenbedingungen und stellen die ursprüngliche Zielfunktion für L wieder her. Die ursprüngliche Zielfunktion kann sowohl Basis- als auch Nichtbasisvariablen enthalten. Deshalb ersetzen wir in der Zielfunktion jede Basisvariable durch die rechte Seite der entsprechenden Nebenbedingung. Zeile 15 gibt dann diese modifizierte Schlupfform zurück. Wenn anderenfalls Zeile 11 feststellt, dass das ursprüngliche lineare Programm unlösbar ist, wird diese Information in Zeile 16 zurückgegeben. Wir zeigen nun die Arbeitsweise der Prozedur Initialize-Simplex auf dem linearen Programm (29.102)-(29.105). Dieses lineare Programm ist lösbar, falls es nichtnegative Werte für x1 und x2 gibt, die die Ungleichungen (29.103) und (29.104) erfüllen. Unter Verwendung von Lemma 29.11 schreiben wir das lineare Hilfsprogramm in der Form maximiere − x0
(29.109)
unter den Nebenbedingungen 2x1 − x2 − x0 ≤ 2 x1 − 5x2 − x0 ≤ −4 x1 , x2 , x0 ≥ 0.
(29.110) (29.111)
Wenn der optimale Zielfunktionswert des linearen Hilfsprogramms gleich 0 ist, dann hat das ursprüngliche lineare Programm nach Lemma 29.11 eine zulässige Lösung. Wenn der optimale Zielfunktionswert des Hilfsprogramms negativ ist, dann hat das ursprüngliche lineare Programm keine zulässige Lösung. Wir formen das lineare Programm in Schlupfform um: z = − x0 x3 = 2 − 2x1 + x2 + x0 x4 = −4 − x1 + 5x2 + x0 . Damit sind wir noch nicht fertig, da die Basislösung, die x4 auf −4 setzen würde, keine zulässige Lösung des linearen Hilfsprogramms ist. Wir können diese Schlupfform jedoch durch einen Aufruf von Pivot in eine andere Schlupfform überführen, für die die Basislösung zulässig ist. Wie der Zeile 8 zu entnehmen ist, wählen wir x0 als Eingangsvariable. In Zeile 6 wählen wir x4 als Ausgangsvariable, die die Basisvariable ist, deren Wert in der Basislösung den betragsmäßig größten negativen Wert hat. Nach dem Basistausch erhalten wir die Schlupfform z = −4 − x1 + 5x2 − x4 x0 = 4 + x1 − 5x2 + x4 x3 = 6 − x1 − 4x2 + x4 . Die zugehörige Basislösung ist (¯ x0 , x ¯1 , x ¯2 , x ¯3 , x ¯4 ) = (4, 0, 0, 6, 0), die zulässig ist. Wir rufen nun wiederholt die Prozedur Pivot auf, bis wir eine optimale Lösung für Laux
29.5 Die initiale zulässige Basislösung
903
gefunden haben. In unserem Beispiel liefert ein Aufruf von Pivot mit x2 als Eingangsvariable und x0 als Ausgangsvariable z = x2 x3
−
x0 x0 x1 x4 4 − + + = 5 5 5 5 4x0 9x1 x4 14 + − + . = 5 5 5 5
Diese Schlupfform ist die endgültige Lösung des Hilfsproblems. Da für diese Lösung x0 = 0 gilt, wissen wir, dass unser ursprüngliches Problem lösbar ist. Außerdem können wir x0 aus der Menge der Nebenbedingungen streichen, da diese Variable gleich 0 ist. Wir restaurieren dann die ursprüngliche Zielfunktion, indem wir geeignete Substitutionen durchführen, sodass nur Nichtbasisvariablen in ihr enthalten sind. Für unser Beispiel erhalten wir die Zielfunktion 2x1 − x2 = 2x1 −
4 x0 x1 x4 − + + 5 5 5 5
.
Setzen wir x0 = 0 und vereinfachen wir den Ausdruck, erhalten wir die Zielfunktion x4 4 9x1 − − + 5 5 5 und die Schlupfform 9x1 x4 4 + − 5 5 5 x1 x4 4 + + = 5 5 5 9x1 x4 14 − + . = 5 5 5
z =− x2 x3
Diese Schlupfform hat eine zulässige Basislösung, die wir an die Prozedur Simplex zurückgeben. Wir zeigen nun die Korrektheit von Initialize-Simplex formal. Lemma 29.12 Falls ein lineares Programm L keine zulässige Lösung besitzt, dann gibt die Prozedur Initialize-Simplex die Meldung „nicht lösbar“ zurück. Anderenfalls gibt sie eine gültige Schlupfform zurück, deren Basislösung zulässig ist. Beweis: Setzen Sie zuerst voraus, dass das lineare Programm L keine zulässige Lösung hat. Dann ist nach Lemma 29.11 der optimale Zielfunktionswert des durch (29.106)–(29.108) definierten Hilfsproblems Laux verschieden von 0 und wegen der Nichtnegativitätsbedingung für x0 muss der optimale Zielfunktionswert negativ sein. Des
904
29 Lineare Programmierung
Weiteren muss dieser Zielfunktionswert endlich sein, denn die Lösung xi = 0 für i = m 1, 2, . . . , n und x0 = |minm i=1 {bi }| ist zulässig und hat − |mini=1 {bi }| als Zielfunktionswert. Daher findet Zeile 10 von Initialize-Simplex eine Lösung mit negativem Zielfunktionswert. Sei x ¯ die zur endgültigen Schlupfform gehörende Basislösung. Es kann nicht x¯0 = 0 gelten, da Laux dann den Zielfunktionswert 0 hätte, was der Tatsache widerspricht, dass der Zielfunktionswert negativ ist. Also bewirkt der Test in Zeile 11, dass die Zeile 16 die Meldung „nicht lösbar“ zurückgibt. Setzen Sie nun voraus, dass das lineare Programm L eine zulässige Lösung hat. Aus Übung 29.3-4 wissen wir, dass, wenn bi ≥ 0 für i = 1, 2, . . . , m gilt, die zur initialen Schlupfform gehörende Basislösung zulässig ist. In diesem Fall geben die Zeilen 2–3 die zur Eingabe gehörende Schlupfform zurück. (Es ist einfach, die Standardform in Schlupfform zu überführen, da A, b und c in beiden Fällen gleich sind.) Im Rest des Beweises behandeln wir den Fall, in dem das lineare Programm lösbar ist, wir aber nicht in Zeile 3 zurückkehren. Wir zeigen, dass in diesem Fall die Zeilen 4–10 eine zulässige Lösung für Laux mit dem Zielfunktionswert 0 bestimmen. Zunächst muss wegen den Zeilen 1–2 bk < 0 und bk ≤ bi für alle i ∈ B
(29.112)
gelten. In Zeile 8 führen wir einen Basistausch durch, bei dem die Ausgangsvariable xl (rufen Sie sich in Erinnerung, dass l = n + k und somit bl < 0 gilt) die linke Seite der Gleichung mit dem kleinsten bi und die Eingangsvariable die neu eingefügte Variable x0 ist. Wir zeigen nun, dass nach diesem Basistausch alle Elemente von b nichtnegativ ¯ die Basislösung nach dem sind und somit die Basislösung von Laux zulässig ist. Seien x Aufruf von Pivot und b sowie B die von Pivot zurückgegebenen Werte. Dann folgt aus Lemma 29.1 < − {e} , bi − aiebe falls i ∈ B x¯i = (29.113) falls i = e . bl /ale Beim Aufruf von Pivot in Zeile 8 ist e = 0. Wenn wir die Ungleichungen (29.107) zu n
aij xj ≤ bi für i = 1, 2, . . . , m
(29.114)
j=0
umformen, sodass sie ai0 enthalten, dann gilt ai0 = aie = −1 für alle i ∈ B .
(29.115)
(Beachten Sie, dass ai0 der Koeffizient von x0 ist, der in der Ungleichung (29.114) zu finden ist, und nicht der negative Wert des Koeffizienten, da Laux in Standardform
29.5 Die initiale zulässige Basislösung
905
und nicht in Schlupfform vorliegt.) Wegen l ∈ B gilt außerdem ale = −1. Damit gilt bl /ale > 0 und folglich x ¯e > 0. Für die übrigen Basisvariablen gilt x¯i = bi − aiebe = bi − aie (bl /ale ) = bi − bl ≥0
(nach (nach (nach (nach
Gleichung (29.113)) Zeile 3 von Pivot) Gleichung (29.115) und ale = −1) Ungleichung (29.112)) ,
woraus folgt, dass nun alle Basisvariablen nichtnegativ sind. Somit ist die Basislösung nach dem Aufruf von Pivot in Zeile 8 zulässig. Als nächstes führen wir Zeile 10 aus, die das System Laux löst. Da wir vorausgesetzt haben, dass L eine zulässige Lösung besitzt, folgt aus Lemma 29.11, dass Laux eine optimale Lösung mit dem Zielfunktionswert 0 hat. Da alle Schlupfformen äquivalent sind, muss für die endgültige Basislösung für Laux x ¯0 = 0 gelten. Nach dem Entfernen von x0 aus dem linearen Programm erhalten wir eine Schlupfform, die zulässig für L ist. Zeile 11 gibt dann diese Schlupfform zurück.
Fundamentalsatz der linearen Programmierung Wir beschließen dieses Kapitel mit dem Beweis, dass die Prozedur Simplex korrekt arbeitet. Jedes lineare Programm ist entweder unlösbar, unbeschränkt oder besitzt eine optimale Lösung mit einem endlichen Zielfunktionswert. In jedem dieser Fälle arbeitet Simplex korrekt. Theorem 29.13: (Fundamentalsatz der linearen Programmierung) Für jedes lineare Programm L, das in Standardform gegeben ist, gilt genau eine der folgenden drei Aussagen: 1. L besitzt eine optimale Lösung mit endlichem Zielfunktionswert. 2. L ist unlösbar. 3. L ist unbeschränkt. Falls L unlösbar ist, gibt die Prozedur Simplex die Meldung „nicht lösbar“ zurück. Falls L unbeschränkt ist, gibt die Prozedur Simplex die Meldung „unbeschränkt“ zurück. Anderenfalls gibt Simplex eine optimale Lösung mit einem endlichen Zielfunktionswert zurück. Beweis: Wenn ein lineares Programm L unlösbar ist, dann gibt die Prozedur Simplex nach Lemma 29.12 die Meldung „nicht lösbar“ zurück. Setzen Sie nun voraus, dass das lineare Programm L lösbar ist. Nach Lemma 29.12 gibt die Prozedur InitializeSimplex eine Schlupfform zurück, deren Basislösung zulässig ist. Nach Lemma 29.7 gibt die Prozedur Simplex daher entweder die Meldung „unbeschränkt“ zurück oder
906
29 Lineare Programmierung
terminiert mit einer zulässigen Lösung. Wenn sie mit einer endlichen Lösung terminiert, dann sagt uns Theorem 29.10, dass diese Lösung optimal ist. Wenn Simplex dagegen „unbeschränkt“ zurückgibt, dann sagt uns Lemma 29.2, dass das lineare Programm L auch tatsächlich unbeschränkt ist. Da Simplex immer auf eine dieser Weisen terminiert, ist das Theorem damit vollständig bewiesen.
Übungen 29.5-1 Geben Sie jeweils den ausführlichen Pseudocode für die Implementierung der Zeilen 5 und 14 von Initialize-Simplex an. 29.5-2 Zeigen Sie, dass, wenn die Hauptschleife von Simplex durch InitializeSimplex ausgeführt wird, sie niemals „unbeschränkt“ zurückgibt. 29.5-3 Setzen Sie voraus, dass wir ein lineares Programm L in Standardform gegeben haben und dass sowohl für L als auch für das zu L duale lineare Programm die zu den initialen Schlupfformen gehörigen Basislösungen zulässig sind. Zeigen Sie, dass der optimale Zielfunktionswert von L gleich 0 ist. 29.5-4 Setzen Sie voraus, dass wir in einem linearen Programm echte Ungleichungen erlauben. Zeigen Sie, dass in diesem Fall der Fundamentalsatz der linearen Programmierung nicht gilt. 29.5-5 Lösen Sie das folgende lineare Programm mithilfe von Simplex: maximiere
x1 + 3x2
unter den Nebenbedingungen x1 − x2 −x1 − x2 −x1 + 4x2 x1 , x2
≤ 8 ≤ −3 ≤ 2 ≥ 0.
29.5-6 Lösen Sie das folgende lineare Programm mithilfe von Simplex: maximiere
x1 − 2x2
unter den Nebenbedingungen x1 + 2x2 −2x1 − 6x2 x2 x1 , x2
≤ 4 ≤ −12 ≤ 1 ≥ 0.
Problemstellungen zu Kapitel 29
907
29.5-7 Lösen Sie das folgende lineare Programm mithilfe von Simplex: maximiere
x1 + 3x2
unter den Nebenbedingungen −x1 + x2 −x1 − x2 −x1 + 4x2 x1 , x2
≤ −1 ≤ −3 ≤ 2 ≥ 0.
29.5-8 Lösen Sie das in den Zeilen (29.6)-(29.10) gegebene lineare Programm. 29.5-9 Betrachten Sie das folgende lineare Programm P mit einer Variablen: maximiere
tx
unter den Nebenbedingungen rx ≤ s x ≥ 0, wobei r, s und t reelle Zahlen sind. Sei D das zu P duale lineare Problem. Geben Sie an, für welche Werte von r, s und t folgende Aussagen gelten: 1. Sowohl P als auch D haben optimale Lösungen mit endlichen Zielfunktionswerten. 2. P ist lösbar, aber D ist unlösbar. 3. D ist lösbar, aber P ist unlösbar. 4. Weder P noch D ist lösbar.
Problemstellungen 29-1 Erfüllbarkeit linearer Ungleichungen Gegeben sei eine Menge von m linearen Ungleichungen in n Variablen x1 , x2 , . . . , xn . Das Erfüllbarkeitsproblem für lineare Ungleichungen stellt die Frage, ob es eine Belegung der Variablen gibt, die alle Ungleichungen simultan erfüllt. a. Zeigen Sie, dass wir das Erfüllbarkeitsproblem für lineare Ungleichungen mit einem Algorithmus zur linearen Programmierung lösen können. Die Anzahl der Variablen und Nebenbedingungen, die Sie in dem linearen Programm verwenden, sollte polynomiell in n und m sein. b. Zeigen Sie, dass wir ein lineares Programmierungsproblem mit einem Algorithmus für das Erfüllbarkeitsproblem für lineare Ungleichungen lösen können. Die Anzahl der Variablen und linearen Ungleichungen, die Sie im Erfüllbarkeitsproblem für lineare Ungleichungen verwenden, sollte polynomiell in der Anzahl der Variablen und der Anzahl der Nebenbedingungen des linearen Programms sein.
908
29 Lineare Programmierung
29-2 Komplementärer Schlupf Der komplementäre Schlupf beschreibt eine Beziehung zwischen den Werten der primalen Variablen und der dualen Nebenbedingungen sowie zwischen den Werten der dualen Variablen und der primalen Nebenbedingungen. Sei x ¯ eine zulässige Lösung des in den Zeilen (29.16)–(29.18) gegebenen primalen linearen Programms und y¯ eine zulässige Lösung des in den Zeilen (29.83)–(29.85) gegebenen dualen linearen Programms. Das komplementäre-Schlupf-Theorem besagt, dass die folgenden Bedingungen notwendig und hinreichend dafür sind, dass x¯ und y¯ optimal sind: m
aij y¯i = cj oder x ¯j = 0 für j = 1, 2, . . . , n
i=1
und n
aij x¯j = bi oder y¯i = 0 für i = 1, 2, . . . , m .
j=1
a. Überprüfen Sie, dass das komplementäre-Schlupf-Theorem für das lineare Programm (29.53)–(29.57) gilt. b. Beweisen Sie, dass das komplementäre-Schlupf-Theorem für jedes primale lineare Programm und das zugehörige duale lineare Programm gilt. c. Beweisen Sie, dass eine zulässige Lösung x ¯ eines primalen linearen Programms, wie in den Zeilen (29.16)–(29.18) gegeben, genau dann optimal ist, wenn Werte y¯ = (¯ y1 , y¯2 , . . . , y¯m ) existieren, für die Folgendes gilt: 1. y¯ ist eine zulässige Lösung des in den Zeilen (29.83)–(29.85) gegebenen dualen linearen Programms. m ¯i = cj für alle j mit x ¯j > 0. 2. i=1 aij y n ¯j < bi . 3. y¯i = 0 für alle i mit j=1 aij x 29-3 Ganzzahlige lineare Programmierung Ein ganzzahliges lineares Programmierungsproblem ist ein lineares Programmierungsproblem mit der zusätzlichen Nebenbedingung, dass die Variablen x ganzzahlige Werte annehmen müssen. Übung 34.5-3 zeigt, dass bereits die Entscheidung, ob ein ganzzahliges lineares Programm eine zulässige Lösung hat, NPschwer ist, was insbesondere bedeutet, dass kein Algorithmus mit polynomieller Laufzeit für dieses Problem bekannt ist. a. Zeigen Sie, dass für ein ganzzahliges lineares Programm die schwache Dualität (Lemma 29.8) gilt. b. Zeigen Sie, dass für ein ganzzahliges lineares Programm nicht immer die Dualität (Theorem 29.10) gilt. c. Zu einem primalen linearen Programm in Standardform lassen Sie uns mit P dessen optimalen Zielfunktionswert bezeichnen, mit D den optimalen Zielfunktionswert seines dualen linearen Programms, mit IP den optimalen Zielfunktionswert der ganzzahligen Version des primalen linearen Programms
Problemstellungen zu Kapitel 29
909
(d. h. des primalen Programms mit der zusätzlichen Nebenbedingung, dass die Variablen ganzzahlige Werte annehmen müssen) und mit ID den optimalen Zielfunktionswert der ganzzahligen Version des dualen linearen Programms. Zeigen Sie, dass unter der Voraussetzung, dass das primale und das duale ganzzahlige Programm lösbar und beschränkt sind, IP ≤ P = D ≤ ID gilt. 29-4 Farkas Lemma Sei A eine m × n-Matrix und c ein n-Vektor. Dann besagt Farkas Lemma, dass genau eines der Systeme Ax ≤ 0 , cT x > 0 und AT y = c , y≥0 lösbar ist. Hierbei ist x ein n-Vektor und y ein m-Vektor. Beweisen Sie Farkas Lemma. 29-5 Kreislauf minimaler Kosten In diesem Problem betrachten wir eine Variante des Problems der Berechnung eines Flusses mit minimalen Kosten aus Abschnitt 29.2, in der wir weder ein Bedarf, eine Quelle noch eine Senke gegeben haben. Stattdessen haben wir wie vorher ein Flussnetzwerk und Kantenkosten a(u, v) gegeben. Ein Fluss ist zulässig, wenn er den Kapazitätsbeschränkungen auf jeder Kante und der Flusserhaltung auf jedem Knoten genügt. Das Ziel besteht darin, unter allen zulässigen Flüssen den zu bestimmen, der minimale Kosten hat. Wir bezeichnen dieses Problem als das minimale-Kreislauf-Problem. a. Formulieren Sie das minimale-Kreislauf-Problem als lineares Programm. b. Setzen Sie voraus, dass a(u, v) > 0 für alle Kanten (u, v) ∈ E gilt. Charakterisieren Sie eine optimale Lösung des minimalen-Kreislauf-Problems. c. Formulieren Sie das maximale-Fluss-Problem als ein lineares Programm eines minimalen-Kreislauf-Problems, d. h. gegeben sei eine Instanz G = (V, E) des maximalen-Fluss-Problems mit Quelle s, Senke t und Kantenkapazitäten c, erzeugen Sie ein minimales-Kreislauf-Problem, indem Sie ein (möglicherweise anderes) Netzwerk G = (V , E ) mit Kantenkapazitäten c und Kantenkosten a so angeben, dass Sie eine Lösung für das maximale-FlussProblem aus einer Lösung des minimalen-Kreislauf-Problems ableiten können. d. Formulieren Sie das kürzeste-Pfade-Problem mit einem Startknoten als ein lineares Programm des minimalen-Kreislauf-Problems.
910
29 Lineare Programmierung
Kapitelbemerkungen Dieses Kapitel stellt lediglich eine Einführung in das umfangreiche Gebiet der linearen Programmierung dar. Eine Reihe von Büchern ist ausschließlich diesem Thema gewidmet, zum Beispiel jene von Chvátal [69], Gass [130], Karloff [197], Schrijver [303] und Vanderbei [344]. Viele andere Bücher, wie das von Papadimitriou und Steiglitz [271] oder von Ahuja, Magnanti und Orlin [7], geben einen guten Überblick über lineare Programmierung. Die Darstellung in diesem Kapitel folgt dem durch Chvátal gewählten Zugang. Der Simplexalgorithmus für die lineare Programmierung wurde 1947 von G. Dantzig eingeführt. Kurz darauf entdeckten Wissenschaftler, wie eine Reihe von Problemen aus einer Vielzahl von Gebieten als lineare Programme formuliert werden können, und lösten sie mit dem Simplexalgorithmus. Als ein Ergebnis entwickelten sich viele Anwendungen der linearen Programmierung und entsprechende Algorithmen. Varianten des Simplexalgorithmus sind nach wie vor die gebräuchlichsten Methoden zum Lösen linearer Programme. Diese Entwicklung wird in einer Reihe von Quellen beschrieben, zum Beispiel in den Anmerkungen in [69] und [197]. Der Ellipsoid-Algorithmus war der erste Algorithmus zum Lösen linearer Programme mit polynomieller Laufzeit und geht auf L. G. Khachian aus dem Jahre 1979 zurück; er ˙ basiert auf früheren Arbeiten von N. ZShor, D. B. Judin und A. S. Nemirovskii. Grötschel, Lovász und Schrijver [154] beschreiben, wie die Ellipsoid-Methode angewendet werden kann, um eine Vielzahl von Problemen der kombinatorischen Optimierung zu lösen. Die Ellipsoid-Methode scheint aus heutiger Sicht in der Praxis nicht konkurrenzfähig zu dem Simplexalgorithmus zu sein. Die Arbeit von Karmarkar [198] enthält eine Beschreibung des ersten Inneren-PunktAlgorithmus. Viele Forscher haben im Folgenden Innere-Punkt-Algorithmen entworfen. Gute Übersichten sind in dem Artikel von Goldfarb und Todd [141] und dem Buch von Ye [361] zu finden. Die Analyse des Simplexalgorithmus bleibt ein aktives Forschungsgebiet. V. Klee und G. J. Minty haben ein Beispiel angegeben, für das der Simplexalgorithmus 2n − 1 Iterationen benötigt. Der Simplexalgorithmus ist in der Praxis normalerweise sehr effizient und viele Forscher haben versucht, theoretische Begründungen für diese empirische Beobachtung zu geben. Eine Forschungsrichtung, die von K. H. Borgwardt initiiert und von vielen anderen weitergeführt wurde, zeigt, dass der Simplexalgorithmus unter bestimmten probabilistischen Voraussetzungen an die Eingabe in polynomieller erwarteter Zeit konvergiert. Spielman und Teng [322] erzielten Fortschritte, indem sie die „geglättete Analyse von Algorithmen“ einführten und sie auf den Simplexalgorithmus anwandten. Es ist bekannt, dass der Simplexalgorithmus in bestimmten Spezialfällen effizienter läuft. Besonders bemerkenswert ist der Netzwerksimplexalgorithmus, ein auf Flussnetzwerkprobleme spezialisierter Simplexalgorithmus. Für bestimmte Netzwerkprobleme wie kürzeste Pfade, maximale Flüsse und Flüsse mit minimalen Kosten laufen Varianten des Netzwerksimplexalgorithmus in polynomieller Zeit. Wir empfehlen für entsprechende Details den Artikel von Orlin [268] und die dort angegebenen Referenzen.
30
Polynome und die FFT
Die gewöhnliche Methode zur Addition zweier Polynome vom Grade n nimmt Zeit Θ(n) in Anspruch, die gewöhnliche Methode zu deren Multiplikation benötigt jedoch Zeit Θ(n2 ). In diesem Kapitel werden wir zeigen, wie die schnelle Fourier-Transformation (engl.: Fast Fourier Transform), abgekürzt FFT, die Zeit zum Multiplizieren von zwei Matrizen auf Θ(n lg n) reduzieren kann. Das am meist bekannte Anwendungsgebiet der Fourier-Transformation, und somit auch der FFT, ist die Signalverarbeitung. Ein Signal ist im Zeitbereich in Form einer Funktion, die der Zeit eine Amplitude zuordnet, gegeben. Die Fourier-Analyse ermöglicht es, das Signal durch eine gewichtete Summe phasenverschobener Sinuskurven unterschiedlicher Frequenzen auszudrücken. Die mit der Frequenz verbundenen Gewichte und Phasen charakterisieren das Signal im Frequenzbereich. Unter den vielen alltäglichen Anwendungen der FFT sind Techniken zur Datenkompression, die zum Kodieren digitaler Videos und Audio-Informationen, wie z. B. MP3-Dateien, eingesetzt werden. Mehrere gute Bücher befassen sich eingehend mit dem ergiebigen Gebiet der Signalverarbeitung: die Kapitelbemerkungen verweisen auf einige wenige von ihnen.
Polynome Ein Polynom in der Variable x über einem algebraischen Körper F stellt eine Funktion A(x) durch eine Summe dar A(x) =
n−1
aj xj .
j=0
Wir bezeichnen die Werte a0 , a1 , . . . , an−1 als Koeffizienten des Polynoms. Die Koeffizienten sind Elemente eines Körpers F , üblicherweise der Menge C der komplexen Zahlen. Ein Polynom A(x) hat den Grad k, wenn dessen höchster, von 0 verschiedener Koeffizient der Koeffizient ak ist; wir verwenden die Schreibweise grad(A) = k. Jede ganze Zahl, die echt größer als der Grad eines Polynoms ist, wird als Gradschranke dieses Polynoms bezeichnet. Der Grad eines Polynoms mit der Gradschranke n kann also eine ganze Zahl im Bereich zwischen 0 und n − 1 sein. Wir können eine Vielzahl von Operationen auf Polynomen definieren. Für die Addition von Polynomen gilt, dass, wenn A(x) und B(x) Polynome mit der Gradschranke n sind, ihre Summe ein Polynom C(x) ebenfalls der Gradschranke n ist, sodass C(x) = A(x) + B(x) für alle x aus dem zugrundeliegenden Körper gilt. Das heißt, wenn A(x) =
n−1 j=0
aj xj
912
30 Polynome und die FFT
und B(x) =
n−1
bj xj
j=0
gilt, dann ist C(x) =
n−1
cj xj ,
j=0
wobei cj = aj + bj für alle j = 0, 1, . . . , n − 1 gilt. Zum Beispiel ist die Summe der Polynome A(x) = 6x3 + 7x2 − 10x + 9 und B(x) = −2x3 + 4x − 5 durch C(x) = 4x3 + 7x2 − 6x + 4 gegeben. Für die Multiplikation von Polynomen gilt, dass, wenn A(x) und B(x) Polynome mit der Gradschranke n sind, ihr Produkt C(x) ein Polynom der Gradschranke 2n − 1 ist, sodass C(x) = A(x)B(x) für alle x im zugrundeliegenden Körper gilt. Sie haben wahrscheinlich bisher Polynome miteinander multipliziert, indem Sie jeden Term aus A(x) mit jedem Term aus B(x) multipliziert und dann Terme gleicher Ordnung zusammengefasst haben. Beispielsweise können wir A(x) = 6x3 + 7x2 − 10x + 9 und B(x) = −2x3 + 4x − 5 wie folgt miteinander multiplizieren: 6x3 + 7x2 − 10x + 9 + 4x − 5 − 2x3 − 30x3 − 35x2 + 50x − 45 24x4 + 28x3 − 40x2 + 36x − 12x6 − 14x5 + 20x4 − 18x3 − 12x6 − 14x5 + 44x4 − 20x3 − 75x2 + 86x − 45 Eine andere Möglichkeit, das Produkt C(x) hinzuschreiben, ist C(x) =
2n−2
cj xj ,
(30.1)
j=0
mit cj =
j
ak bj−k .
(30.2)
k=0
Beachten Sie, dass grad(C) = grad(A) + grad(B) gilt, woraus folgt, dass C ein Polynom mit Gradschranke na + nb − 1 ist, wenn A ein Polynom mit Gradschranke na und B ein Polynom mit Gradschranke nb ist. Da ein Polynom mit Gradschranke k auch ein Polynom der Gradschranke k + 1 ist, werden wir (der Einfachheit halber) gewöhnlich sagen, dass das Produktpolynom C ein Polynom der Gradschranke na + nb ist.
30.1 Darstellung von Polynomen
913
Kapitelüberblick Abschnitt 30.1 stellt zwei Darstellungen von Polynomen vor: die Koeffizientendarstellung und die Stützstellendarstellung. Die einfachen Methoden zur Multiplikation von Polynomen – Gleichungen (30.1) und (30.2) – benötigen Zeit Θ(n2 ), wenn die Polynome in der Koeffizientenform gegeben sind, aber nur Zeit Θ(n), wenn sie in Stützstellenform vorliegen. Wir können jedoch Polynome in Koeffizientenform mit einem Zeitaufwand von nur Θ(n lg n) miteinander multiplizieren, wenn wir zwischen den beiden Darstellungen wechseln. Um zu sehen, warum dieser Ansatz funktioniert, müssen wir uns zuerst mit komplexen Einheitswurzeln befassen, was wir in Abschnitt 30.2 tun werden. Anschließend benutzen wir die FFT und die dazu inverse Transformation, die ebenfalls in Abschnitt 30.2 beschrieben wird, um die Wechsel zwischen den Darstellungen auszuführen. Abschnitt 30.3 zeigt, wie die FFT sowohl in seriellen als auch in parallelen Rechenmodellen schnell zu implementieren ist. Dieses Kapitel macht in starkem Maße Gebrauch von komplexen Zahlen.√Innerhalb dieses Kapitels werden wir das Symbol i ausschließlich als Bezeichnung von −1 benutzen.
30.1
Darstellung von Polynomen
Die Koeffizienten- und die Stützstellendarstellung von Polynomen sind in gewissem Sinne äquivalent; das heißt, ein Polynom in Stützstellenform besitzt ein eindeutiges Gegenstück in Koeffizientenform. In diesem Abschnitt führen wir die beiden Darstellungen ein und zeigen, wie wir diese miteinander kombinieren können, um die Multiplikation zweier Polynome mit Gradschranke n in Zeit Θ(n lg n) auszuführen.
Koeffizientendarstellung j Die Koeffizientendarstellung eines Polynoms A(x) = n−1 j=0 aj x mit Gradschranke n ist ein Vektor a = (a0 , a1 , . . . , an−1 ) von Koeffizienten. Innerhalb von Matrizengleichungen werden wir in diesem Kapitel Vektoren immer als Spaltenvektoren behandeln. Die Koeffizientendarstellung ist für bestimmte Operationen auf Polynomen gut geeignet, so zum Beispiel für die Auswertung eines Polynoms. Die Auswertung eines Polynoms A(x) an einem gegebenen Punkt x0 besteht in der Berechnung der Wertes von A(x0 ). Wir können ein Polynom mithilfe des Horner-Schemas in Zeit Θ(n) auswerten: A(x0 ) = a0 + x0 (a1 + x0 (a2 + · · · + x0 (an−2 + x0 (an−1 )) · · · )) . Ebenso benötigt die Addition von zwei durch die Koeffizientenvektoren a = (a0 , a1 , . . . , an−1 ) und b = (b0 , b1 , . . . , bn−1 ) dargestellten Polynome Zeit Θ(n): Wir erzeugen einfach nur den Koeffizientenvektor c = (c0 , c1 , . . . , cn−1 ) mit cj = aj + bj für j = 0, 1, . . . , n − 1. Betrachten Sie nun die Multiplikation zweier Polynome A(x) und B(x) mit Gradschranke n, die in Koeffizientendarstellung gegeben sind. Wenn wir die durch die Gleichungen (30.1) und (30.2) beschriebene Methode verwenden, benötigt die Multiplikation der Polynome Zeit Θ(n2 ), da wir jeden Koeffizient des Vektors a mit jedem Koeffizienten
914
30 Polynome und die FFT
des Vektors b multiplizieren müssen. Die Operation der Multiplikation von Polynomen scheint in Koeffizientendarstellung erheblich schwieriger zu sein als die Auswertung eines Polynoms oder die Addition von Polynomen. Der resultierende Koeffizientenvektor c, der durch Gleichung (30.2) gegeben ist, wird auch als die Faltung der Eingabevektoren a und b bezeichnet, die wir als c = a ⊗ b schreiben. Da die Multiplikation von Polynomen und die Berechnung von Faltungen grundlegende rechentechnische Probleme von erheblicher praktischer Bedeutung sind, konzentriert sich dieses Kapitel auf effiziente Algorithmen für diese Probleme.
Stützstellendarstellung Eine Stützstellendarstellung eines Polynoms A(x) mit Gradschranke n ist eine Menge von n Stützstellenpaaren {(x0 , y0 ), (x1 , y1 ), . . . , (xn−1 , yn−1 )} , wobei alle xk verschieden sind und für k = 0, 1, . . . , n − 1 (30.3)
yk = A(xk )
gilt. Ein Polynom besitzt viele verschiedene Stützstellendarstellungen, da wir jede Menge von n verschiedenen Punkten x0 , x1 , . . . , xn−1 als Grundlage für diese Darstellung benutzen können. Das Berechnen der Stützstellendarstellung aus einer gegebenen Koeffizientendarstellung eines Polynoms ist im Prinzip einfach. Alles, was wir zu tun haben, ist die Auswahl von n verschiedenen Punkten x0 , x1 , . . . , xn−1 und das Auswerten von A(xk ) für k = 0, 1, . . . , n − 1. Mithilfe des Horner-Schemas benötigt die Berechnung dieser n Punkte Zeit Θ(n2 ). Wir werden später sehen, dass, wenn wir die Punkte xk geschickt auswählen, wir diese Berechnung beschleunigen und eine Laufzeit von Θ(n lg n) erreichen können. Die inverse Operation – das Bestimmen der Koeffizientendarstellung eines Polynoms aus einer Stützstellendarstellung – heißt Interpolation. Das folgende Theorem zeigt, dass die Interpolation wohldefiniert ist, wenn das gesuchte interpolierende Polynom eine Gradschranke besitzen muss, die gleich der Anzahl der gegebenen Stützstellenpaare ist. Theorem 30.1: (Eindeutigkeit eines interpolierenden Polynoms) Für jede Menge {(x0 , y0 ), (x1 , y1 ), . . . , (xn−1 , yn−1 )} von n Stützstellenpaaren mit paarweise verschiedenen xk existiert ein eindeutiges Polynom A(x) mit Gradschranke n, sodass für k = 0, 1, . . . , n − 1 die Gleichung yk = A(xk ) gilt. Beweis: Der Beweis baut auf der Existenz der Inversen einer bestimmten Matrix auf. Gleichung (30.3) ist äquivalent zur Matrixgleichung ⎛ ⎞⎛ ⎞ ⎛ ⎞ 1 x0 x20 · · · xn−1 a0 y0 0 ⎜ 1 x1 x2 · · · xn−1 ⎟ ⎜ a1 ⎟ ⎜ y1 ⎟ ⎜ ⎟⎜ ⎟ ⎜ ⎟ 1 1 (30.4) ⎜. . ⎟⎜ . ⎟ = ⎜ . ⎟ . . . . . . . . . . . ⎝. . . . ⎠⎝ . ⎠ ⎝ . ⎠ . 1 xn−1 x2n−1 · · · xn−1 n−1
an−1
yn−1
30.1 Darstellung von Polynomen
915
Die Matrix auf der linken Seite wird mit V (x0 , x1 , . . . , xn−1 ) bezeichnet und ist als Vandermonde-Matrix bekannt. Wie Sie in der Problemstellung D-1 zeigen sollen, besitzt diese Matrix die Determinante A (xk − xj ) 0≤j 0 gilt dk ωdn = ωnk .
(30.7)
Beweis: Der Beweis des Lemmas folgt direkt aus Gleichung (30.6) dk dk ωdn = e2πi/dn k = e2πi/n = ωnk . 2 Viele Autoren definieren ω auf andere Weise, nämlich als ω = e−2πi/n . Diese alternative Defin n nition wird eher für Anwendungen aus dem Bereich der Signalverarbeitung verwendet. Die zugrundeliegende Mathematik ist für beide Definitionen von ωn im Wesentlichen dieselbe.
30.2 Die DFT und FFT
921
Korollar 30.4 n/2
Für jede beliebige gerade ganze Zahl n > 0 gilt ωn
= ω2 = −1 .
Den Beweis überlassen wir Übung 30.2-1. Lemma 30.5: (Halbierungslemma) Wenn n > 0 eine gerade ganze Zahl ist, dann sind die Quadrate der n komplexen n-ten Einheitswurzeln die n/2 komplexen (n/2)-ten Einheitswurzeln. Beweis: Aufgrund des Kürzungslemmas gilt für jede nichtnegative ganze Zahl k die k . Beachten Sie, dass wir jede (n/2)-te Einheitswurzel genau Gleichung (ωnk )2 = ωn/2 zweimal erhalten, wenn wir alle n-ten Einheitswurzeln quadrieren, denn es gilt (ωnk+n/2 )2 = ωn2k+n = ωn2k ωnn = ωn2k = (ωnk )2 . k+n/2
Folglich haben ωnk und ωn das gleiche Quadrat. Wir hätten auch Korollar 30.4 n/2 anwenden können, um diese Eigenschaft zu beweisen, da aus ωn = −1 die Gleichung k+n/2 k+n/2 2 ωn = −ωnk folgt und somit (ωn ) = (ωnk )2 gilt. Wie wir sehen werden, ist das Halbierungslemma für unsere Teile-und-BeherrscheMethode zur Konvertierung zwischen der Koeffizientendarstellung und der Stützstellendarstellung wesentlich, da es sicherstellt, dass die rekursiven Teilprobleme nur halb so groß sind. Lemma 30.6: (Summationslemma) Für jede ganze Zahl n ≥ 1 und jede von 0 verschiedene ganze Zahl k, die nicht durch n teilbar ist, gilt n−1
k j ωn = 0 .
j=0
Beweis: Die Gleichung (A.5) ist auf komplexe Zahlen genauso anwendbar wie auf reelle Zahlen und folglich gilt n−1
k j (ω k )n − 1 ωn = nk ωn − 1 j=0 (ωnn )k − 1 ωnk − 1 (1)k − 1 = k ωn − 1 =0. =
922
30 Polynome und die FFT
Da wir vorausgesetzt haben, dass k nicht durch n teilbar ist und ωnk = 1 nur gilt, wenn k durch n teilbar ist, stellen wir sicher, dass der Nenner nicht 0 ist.
Die DFT Rufen Sie sich in Erinnerung, dass wir das Polynom A(x) =
n−1
aj xj
j=0
mit Gradschranke n an den Stellen ωn0 , ωn1 , ωn2 , . . . , ωnn−1 auswerten wollen (d. h. an den n komplexen n-ten Einheitswurzeln).3 Wir setzen voraus, dass A in Koeffizientenform a = (a0 , a1 , . . . , an−1 ) gegeben ist, und definieren die Ergebnisse yk mit k = 0, 1, . . . , n − 1 durch yk = A(ωnk ) n−1 = aj ωnkj .
(30.8)
j=0
Der Vektor y = (y0 , y1 , . . . , yn−1 ) ist die diskrete Fourier-Transformierte des Koeffizientenvektors a = (a0 , a1 , . . . , an−1 ). Wir schreiben auch y = DFTn (a).
Die FFT Mit der Methode der schnellen Fourier-Transformation (FFT), die die speziellen Eigenschaften komplexer Einheitswurzeln ausnutzt, können wir DFTn (a) in Zeit Θ(n lg n) berechnen, während die naive Methode Zeit Θ(n2 ) benötigt. Wir können ohne Beschränkung der Allgemeinheit voraussetzen, dass n eine Zweierpotenz ist. Wenngleich Strategien, die mit Nichtzweierpotenzen arbeiten können, bekannt sind, sie würden den Rahmen dieses Buches sprengen. Die FFT-Methode wendet eine Teile-und-Beherrsche-Strategie an, indem sie die Koeffizienten von A(x) mit geradem Index und die Koeffizienten mit ungeradem Index separat behandelt, um zwei neue Polynome A[0] (x) und A[1] (x) mit Gradschranke n/2 zu definieren: A[0] (x) = a0 + a2 x + a4 x2 + · · · + an−2 xn/2−1 , A[1] (x) = a1 + a3 x + a5 x2 + · · · + an−1 xn/2−1 . Beachten Sie, dass A[0] alle Koeffizienten von A mit geradem Index (die Binärdarstellung des Index endet mit 0) und A[1] alle Koeffizienten mit ungeradem Index (die 3 Die Länge n ist in Wirklichkeit, wie in Abschnitt 30.1 beschrieben, der Wert 2n, da wir die Gradschranke der gegebenen Polynome vor dem Auswerten verdoppeln. Im Rahmen der Polynommultiplikation arbeiten wir in Wirklichkeit mit den komplexen (2n)-ten Einheitswurzeln.
30.2 Die DFT und FFT
923
Binärdarstellung des Index endet mit 1) enthält. Damit gilt A(x) = A[0] (x2 ) + xA[1] (x2 ) ,
(30.9)
sodass sich das Problem, A(x) an den Stellen ωn0 , ωn1 , . . . , ωnn−1 auszuwerten, auf die folgenden Schritte reduziert: 1. Auswerten der Polynome A[0] (x) und A[1] (x) mit Gradschranke n/2 an den Punkten (ωn0 )2 , (ωn1 )2 , . . . , (ωnn−1 )2 ,
(30.10)
2. Zusammenführen der Ergebnisse gemäß Gleichung (30.9). Aufgrund des Halbierungslemmas besteht die Werteliste (30.10) nicht aus n verschiedenen Werten, sondern nur aus den n/2 komplexen (n/2)-ten Einheitswurzeln, wobei jede Wurzel genau zweimal auftritt. Somit werten wir die Polynome A[0] und A[1] der Gradschranke n/2 an den n/2 komplexen (n/2)-ten Einheitswurzeln rekursiv aus. Diese Teilprobleme haben genau dieselbe Form wie das ursprüngliche Problem, sind aber nur halb so groß. Wir haben nun erfolgreich eine n-elementige DFTn -Berechnung in zwei n/2-elementige DFTn/2 -Berechnungen unterteilt. Diese Zerlegung bildet die Grundlage des folgenden FFT-Algorithmus, der die DFT eines n-elementigen Vektors a = (a0 , a1 , . . . , an−1 ) berechnet, wobei n eine Zweierpotenz ist. Recursive-FFT(a) 1 n = a.l¨a nge // n ist eine Zweierpotenz 2 if n = = 1 3 return a 4 ωn = e2πi/n 5 ω =1 6 a[0] = (a0 , a2 , . . . , an−2 ) 7 a[1] = (a1 , a3 , . . . , an−1 ) 8 y [0] = Recursive-FFT(a[0] ) 9 y [1] = Recursive-FFT(a[1] ) 10 for k = 0 to n/2 − 1 [0] [1] 11 yk = yk + ω yk [0] [1] 12 yk+(n/2) = yk − ω yk 13 ω = ω ωn 14 return y // y wird als Spaltenvektor angesehen Die Prozedur Recursive-FFT arbeitet folgendermaßen. Die Zeilen 2–3 bilden die Rekursionsbasis; die DFT eines Elementes ist das Element selbst, da in diesem Fall y0 = a0 ω10 = a0 · 1 = a0
924
30 Polynome und die FFT
gilt. Die Zeilen 6–7 definieren die Koeffizientenvektoren der Polynome A[0] und A[1] . Die Zeilen 4, 5 und 13 stellen sicher, dass ω richtig aktualisiert wird, sodass immer, wenn die Zeilen 11–12 ausgeführt werden, ω = ωnk ist. (Das Vorhalten einer Laufvariablen ω von einer Iteration zur nächsten spart Zeit gegenüber der jeweils neuen Berechnung von ωnk in jedem Durchlauf der for-Schleife.) Die Zeilen 8–9 führen die rekursiven DFTn/2 Berechnungen aus, indem sie für k = 0, 1, . . . , n/2 − 1 [0]
k yk = A[0] (ωn/2 ), [1]
k yk = A[1] (ωn/2 ) k setzen, was wegen des Kürzungslemmas ωn/2 = ωn2k äquivalent zu [0]
yk = A[0] (ωn2k ) , [1]
yk = A[1] (ωn2k ) . ist. Die Zeilen 11–12 fügen die Ergebnisse der rekursiven DFTn/2 -Berechnungen zusammen. Für y0 , y1 , . . . , yn/2−1 liefert Zeile 11 [0]
[1]
yk = yk + ωnk yk
= A[0] (ωn2k ) + ωnk A[1] (ωn2k ) = A(ωnk )
(wegen Gleichung (30.9)) .
Für yn/2 , yn/2+1 , . . . , yn−1 und k = 0, 1, . . . , n/2 − 1 liefert Zeile 12 [0]
[1]
yk+(n/2) = yk − ωnk yk [0]
[1]
(da ωnk+(n/2) = −ωnk )
= yk + ωnk+(n/2) yk
= A[0] (ωn2k ) + ωnk+(n/2) A[1] (ωn2k ) = A[0] (ωn2k+n ) + ωnk+(n/2) A[1] (ωn2k+n ) (da ωn2k+n = ωn2k ) = A(ωnk+(n/2) )
(wg. Gleichung (30.9)).
Folglich ist der von Recursive-FFT zurückgegebene Vektor y tatsächlich die DFT des Eingabevektors a. [1]
Die Zeilen 11 und 12 multiplizieren jeden Wert yk mit ωnk , für k = 0, 1, . . . , n/2 − 1. [0] Zeile 11 addiert dieses Produkt zu yk und Zeile 12 subtrahiert es. Da wir jeden Faktor k ωn sowohl in positiver als auch in negativer Form benutzen, nennen wir diese Faktoren Drehfaktoren. Um die Laufzeit der Prozedur Recursive-FFT zu bestimmen, stellen wir fest, dass, ohne die Laufzeiten der jeweiligen rekursiven Aufrufen mitzurechnen, jeder Aufruf Zeit Θ(n) benötigt, wobei n die Länge des Eingabevektors ist. Die Rekursionsgleichung für
30.2 Die DFT und FFT
925
die Laufzeit ist deshalb durch T (n) = 2T (n/2) + Θ(n) = Θ(n lg n) gegeben. Somit können wir ein Polynom mit Gradschranke n an den komplexen n-ten Einheitswurzeln in Zeit Θ(n lg n) mithilfe der schnellen Fourier-Transformation auswerten.
Interpolation an den komplexen Einheitswurzeln Wir vervollständigen die Methode für Polynommultiplikation, indem wir zeigen, wie wir die komplexen Einheitswurzeln durch ein Polynom interpolieren können, was uns erlaubt, eine Stützstellenform zurück in die Koeffizientenform zu überführen. Wir interpolieren, indem wir die DFT als Matrizengleichung schreiben und uns dann die Form der Matrixinversen anschauen. Mit Gleichung (30.4) können wir die DFT als Matrixprodukt y = Vn a schreiben, wobei Vn eine Vandermonde-Matrix ist, die die entsprechenden Potenzen von ωn enthält: ⎛ ⎞ ⎛ ⎞ ⎞ ⎛ 1 1 1 1 ··· 1 a0 y0 ⎜1 ω ⎟ 2 3 n−1 ωn ωn · · · ωn n ⎟ ⎜ a1 ⎟ ⎜ y1 ⎟ ⎜ ⎜ ⎟ ⎜ ⎟ ⎜ 2(n−1) ⎟ 2 4 6 ⎟ ⎜ a2 ⎟ ⎜ y2 ⎟ ⎜ 1 ωn ω ω · · · ω n n n ⎟⎜ ⎟ ⎜ ⎟ ⎜ ⎟ . ⎜ y ⎟=⎜ 3(n−1) ⎟ ⎜ ⎟ ⎜ a3 ⎟ ⎜ 3 ⎟ ⎜ 1 ωn3 ωn6 ωn9 · · · ωn ⎟⎜ . ⎟ ⎜ . ⎟ ⎜. . .. .. .. .. ⎟⎝ . ⎠ ⎝ .. ⎠ ⎜ . . . . ⎝. . ⎠ . . . (n−1)(n−1) n−1 2(n−1) 3(n−1) yn−1 a n−1 1 ωn ωn ωn · · · ωn Der Eintrag an der Stelle (k, j) von Vn ist ωnkj mit j, k = 0, 1, . . . , n − 1. Die Exponenten der Elemente von Vn bilden eine Multiplikationstabelle. Bei der inversen Operation, die wir als a = DFT−1 n (y) schreiben, multiplizieren wir y mit der Matrix Vn−1 , der Inversen von Vn . Theorem 30.7 Für j, k = 0, 1, . . . , n − 1 ist der Eintrag an der Stelle (j, k) von Vn−1 gleich ωn−kj /n. Beweis: Wir zeigen, dass Vn−1 Vn = In ist, d. h. das Produkt von der wie in der Aussage des Theorems beschriebenen Matrix und Vn gleich der n × n-Einheitsmatrix ist. Betrachten Sie den Eintrag (j, j ) von Vn−1 Vn : [Vn−1 Vn ]jj =
n−1
(ωn−kj /n)(ωnkj )
k=0
=
n−1 k=0
ωnk(j
−j)
/n .
926
30 Polynome und die FFT
Die Summe ergibt 1, wenn j = j gilt, und anderenfalls 0, was aus dem Summationslemma (Lemma 30.6) folgt. Beachten Sie, dass wir uns bei der Anwendung des Summationslemma darauf stützen, dass −(n − 1) ≤ j − j ≤ n − 1 gilt, sodass j − j nicht durch n teilbar ist. Liegt die inverse Matrix Vn−1 vor, dann ist DFT−1 n (y) mit j = 0, 1, . . . , n − 1 durch aj =
n−1 1 yk ωn−kj n
(30.11)
k=0
gegeben. Vergleichen wir die Gleichungen (30.8) und (30.11), so sehen wir, dass wir die inverse DFT berechnen können (siehe Übung 30.2-4), indem wir im FFT-Algorithmus die Rollen von a und y vertauschen, ωn durch ωn−1 ersetzen und jedes Element des Ergebnisses durch n teilen. Somit können wir DFT−1 n ebenfalls in Zeit Θ(n lg n) berechnen. Wir sehen, dass wir ein Polynom mit Gradschranke n zwischen seiner Koeffizientendarstellung und einer Stützstellendarstellung in Zeit Θ(n lg n) hin und zurück transformieren können, indem wir die FFT und die inverse FFT anwenden. Im Kontext der Polynommultiplikation haben wir das Folgende gezeigt. Theorem 30.8: (Faltungstheorem) Für zwei beliebige Vektoren a und b, deren Länge n eine Zweierpotenz ist, gilt a ⊗ b = DFT−1 2n (DFT2n (a) · DFT2n (b)) , wobei die Vektoren a und b mit Nullen auf die Länge 2n aufgefüllt werden und · das komponentenweise Produkt zweier 2n-elementiger Vektoren bezeichnet.
Übungen 30.2-1 Beweisen Sie Korollar 30.4. 30.2-2 Berechnen Sie die DFT des Vektors (0, 1, 2, 3). 30.2-3 Lösen Sie die Übung 30.1-1 unter Verwendung der Θ(n lg n)-Methode. 30.2-4 Geben Sie Pseudocode an, der DFT−1 n in Zeit Θ(n lg n) berechnet. 30.2-5 Beschreiben Sie die Verallgemeinerung der Prozedur FFT für den Fall, in dem n eine Potenz von 3 ist. Geben Sie eine Rekursionsgleichung für die Laufzeit an und lösen Sie diese. 30.2-6∗ Nehmen Sie an, wir würden einen Ring Zm von ganzen Zahlen modulo m verwenden, mit m = 2tn/2 + 1 für eine beliebige positive ganze Zahl t, anstatt eine n-elementige FFT über dem Körper der komplexen Zahlen auszuführen (wobei n gerade ist). Verwenden Sie als n-te Haupteinheitswurzel ω = 2t anstelle von ωn . Beweisen Sie, dass die DFT und die inverse DFT bei diesem Szenario wohldefiniert sind.
30.3 Effiziente Implementierung der FFT
927
30.2-7 Gegeben sei eine Liste mit Werten z0 , z1 , . . . , zn−1 (möglicherweise mit Wiederholungen). Zeigen Sie, wie wir die Koeffizienten eines Polynoms P (x) mit Gradschranke n + 1 bestimmen können, wenn das Polynom nur an den Stellen z0 , z1 , . . . , zn−1 (möglicherweise mit Wiederholungen) Nullstellen besitzt. Ihre Prozedur sollte in Zeit O(n lg2 n) laufen. (Hinweis: Das Polynom P (x) besitzt genau dann eine Nullstelle bei zj , wenn P (x) ein Vielfaches von (x − zj ) ist.) 30.2-8∗ Die Chirp-Transformation eines Vektors a = (a0 , a1 , . . . , an−1 ) ist der Vekn−1 tor y = (y0 , y1 , . . . , yn−1 ), wobei yk = j=0 aj z kj für irgendeine komplexe Zahl z gilt. Die DFT stellt somit einen Spezialfall der Chirp-Transformation dar, den wir durch die Wahl z = ωn erhalten. Zeigen Sie, wie wir die ChirpTransformation für eine komplexe Zahl z in Zeit O(n lg n) auswerten können. (Hinweis: Verwenden Sie die Gleichung yk = z
k2 /2
n−1
aj z j
2
/2
2 z −(k−j) /2 ,
j=0
um die Chirp-Transformation als Faltung zu interpretieren.)
30.3
Effiziente Implementierung der FFT
Da die praktischen Anwendungen der DFT, wie die Signalverarbeitung, äußerste Schnelligkeit erfordern, befasst sich dieser Abschnitt mit zwei effizienten Implementierungen. Zuerst werden wir eine iterative Version des FFT-Algorithmus untersuchen, die in Zeit Θ(n lg n) läuft, aber eine kleinere, in der Θ-Notation verborgene Konstante als die rekursive Version aus Abschnitt 30.2 besitzen kann. (Abhängig von der exakten Implementierung könnte die rekursive Version den Hardware-Cache sehr effizient nutzen.) Dann werden wir die Erkenntnisse, die uns zu der iterativen Implementierung geführt haben, nutzen, um einen effizienten parallelen FFT-Algorithmus zu entwickeln.
Eine iterative FFT-Implementierung Wir stellen zunächst fest, dass die for-Schleife in den Zeilen 10–13 von Recursive[1] FFT die Berechnung des Wertes ωnk yk zweimal enthält. Benutzen wir die Terminologie aus dem Compilerbau-Bereich, so nennen wir einen solchen Wert einen gemeinsamen Teilausdruck . Wir können die Schleife so verändern, dass er diesen Wert nur einmal berechnet, indem wir den Wert in einer temporären Variablen t speichern. for k = 0 to n/2 − 1 [1] t = ω yk [0] yk = yk + t [0] yk+(n/2) = yk − t ω = ω ωn [1]
Die Operation in dieser Schleife, den Drehfaktor ω = ωnk mit yk zu multiplizieren, das [0] Produkt nach t zu speichern sowie yk und t zu addieren und zu subtrahieren, ist als Butterfly-Operation bekannt und schematisch in Abbildung 30.3 dargestellt.
928
30 Polynome und die FFT
[0]
yk
[0]
[1]
[0]
yk + ωnk yk
+
ωnk [1]
yk
[0]
[1]
[0]
[1]
yk + ωnk yk
yk
ωnk [0]
[1]
[1]
yk − ωnk yk
–
•
yk − ωnk yk
yk
(b)
(a)
Abbildung 30.3: Eine Butterfly-Operation. (a) Die beiden Eingabewerte kommen von links, [1] der Drehfaktor ωnk wird mit yk multipliziert und die Summe und die Differenz werden rechts ausgegeben. (b) Eine vereinfachte Darstellung der Butterfly-Operation. Wir werden diese Darstellung in parallelen FFT-Schaltkreisen verwenden.
(a0,a1,a2,a3,a4,a5,a6,a7) (a0,a2,a4,a6) (a0,a4) (a0)
(a1,a3,a5,a7) (a2,a6)
(a4)
(a2)
(a1,a5) (a6)
(a1)
(a3,a7) (a5)
(a3)
(a7)
Abbildung 30.4: Der Baum der Eingabevektoren der rekursiven Aufrufe der Prozedur Recursive-FFT. Der erste Aufruf erfolgt für n = 8.
Wir zeigen nun, wie wir dem FFT-Algorithmus eine iterative anstelle der rekursiven Struktur verleihen können. In Abbildung 30.4 haben wir die Eingabevektoren der rekursiven Aufrufe eines Aufruf von Recursive-FFT in einer Baumstruktur angeordnet, wobei der erste Aufruf für n = 8 erfolgt. Der Baum besitzt für jeden Aufruf der Prozedur einen Knoten, den wir durch den zugehörigen Eingabevektor kennzeichnen. Jeder Aufruf von Recursive-FFT bewirkt zwei rekursive Aufrufe, außer wenn er einen 1elementigen Vektor als Eingabe erhält. Der erste rekursive Aufruf erscheint im linken Kind und der zweite Aufruf im rechten Kind. Wenn wir uns den Baum ansehen, dann stellen wir fest, dass, wenn wir die Elemente des initialen Vektors a in der Reihenfolge ordnen könnten, in der sie in den Blättern auftreten, dann könnten wir die Ausführung der Prozedur Recursive-FFT zurückverfolgen, wenngleich bottom-up und nicht top-down. Zuerst fassen wir die Elemente paarweise zusammen, berechnen die DFT von jedem Paar, indem wir eine ButterflyOperation durchführen, und ersetzen das Paar durch seine DFT. Der Vektor enthält dann n/2 2-elementige DFTs. Anschließend fassen wir diese n/2 DFTs paarweise zusammen und berechnen die DFT der 4 Vektorelemente, die durch die Anwendung zweier Butterfly-Operationen entstehen, und ersetzen die zwei 2-elementigen DFTs durch eine 4-elementige DFT. Der Vektor enthält dann n/4 4-elementige DFTs. Wir fahren in dieser Weise fort, bis der Vektor zwei (n/2)-elementige DFTs enthält, die wir mithilfe von n/2 Butterfly-Operationen zur finalen n-elementigen DFT zusammenfügen.
30.3 Effiziente Implementierung der FFT
929
Um diesen bottom-up Ansatz in Pseudocode zu gießen, legen wir ein Feld A[0 . . n − 1] an, das anfangs die Elemente des Eingabevektors a in der Reihenfolge enthält, in der sie in den Blättern des Baumes in Abbildung 30.4 vorkommen. (Wir werden später sehen, wie wir diese Reihenfolge, die als Bit-umkehrende Permutation bekannt ist, bestimmen können.) Da wir DFTs auf jeder Ebene des Baumes zusammenfügen müssen, führen wir eine Variable s zum Zählen der Ebenen ein, die Werte aus dem Bereich zwischen 1 (unten im Baum, wo wir Paare zusammenfügen, um 2-elementige DFTs zu bilden) und lg n (an der Wurzel, wo wir zwei (n/2)-elementige DFTs zusammenfügen, um das finale Ergebnis zu erzeugen) annimmt. Der Algorithmus weist somit folgende Struktur auf: 1 for s = 1 to lg n 2 for k = 0 to n − 1 by 2s 3 kombiniere die zwei 2s−1 -elementigen DFTs aus A[k . . k + 2s−1 − 1] und A[k + 2s−1 . . k + 2s − 1] zu der 2s -elementigen DFT von A[k . . k + 2s − 1] Wir können den Schleifenrumpf (Zeile 3) durch präziseren Pseudocode ausdrücken. Wir kopieren die for-Schleife aus der Prozedur Recursive-FFT, indem wir y [0] mit A[k . . k + 2s−1 − 1] und y [1] mit A[k + 2s−1 . . k + 2s − 1] identifizieren. Der bei jeder Butterfly-Operation benutzte Drehfaktor hängt vom Wert s ab; er ist eine Potenz von ωm , wobei m = 2s gilt. (Zum Zwecke der Lesbarkeit benutzen wir die Variable m.) Wir führen eine weitere temporäre Variable u ein, die es uns erlaubt, die ButterflyOperationen in-place auszuführen. Wenn wir Zeile 3 durch den Schleifenkörper ersetzen, erhalten wir den folgenden Pseudocode, der die Grundlage der parallelen Implementierung bildet, die wir später vorstellen werden. Der Code ruft zunächst die Hilfsprozedur Bit-Reverse-Copy(a, A) auf, um den Vektor a in das Feld A in der Reihenfolge zu kopieren, in der wir die Werte benötigen. Iterative-FFT(a) 1 Bit-Reverse-Copy(a, A) 2 n = a.l¨a nge // n ist eine Zweierpotenz 3 for s = 1 to lg n 4 m = 2s 5 ωm = e2πi/m 6 for k = 0 to n − 1 by m 7 ω =1 8 for j = 0 to m/2 − 1 9 t = ω A[k + j + m/2] 10 u = A[k + j] 11 A[k + j] = u + t 12 A[k + j + m/2] = u − t 13 ω = ω ωm 14 return A Wie bringt Bit-Reverse-Copy die Elemente des Eingabevektors a im Feld A in die gewünschte Reihenfolge? Die Reihenfolge, in der die Blätter in Abbildung 30.4 auftreten,
930
30 Polynome und die FFT
entspricht einer Bit-umkehrenden Permutation. Das heißt, wenn wir mit rev(k) die lg n-Bit-Integerzahl bezeichnen, die wir durch das Spiegeln der Bits in der binären Darstellung von k erhalten, dann wollen wir das Vektorelement ak an die Position A[rev(k)] bringen. In Abbildung 30.4 beispielsweise erscheinen die Blätter in der Reihenfolge 0, 4, 2, 6, 1, 5, 3, 7; diese Sequenz hat die Binärdarstellung 000, 100, 010, 110, 001, 101, 011, 111, und wenn wir die Bits jedes Wertes spiegeln, dann erhalten wir die Sequenz 000, 001, 010, 011, 100, 101, 110, 111. Um zu sehen, dass wir im Allgemeinen eine Bitumkehrende Permutation benötigen, haben wir zu bemerken, dass in der oberen Ebene des Baumes die Indizes, deren kleinstes Bit 0 ist, in den linken Teilbaum hineingehen, und die Indizes, deren kleinstes Bit 1 ist, in den rechten Teilbaum hineingehen. Wir streichen in jeder Ebene das Bit niedrigster Ordnung und setzen diesen Prozess den Baum abwärts fort, bis wir die Reihenfolge haben, die durch die Bit-umkehrende Permutation an den Blättern gegeben ist. Da wir die Funktion rev(k) leicht berechnen können, ist die Prozedur Bit-ReverseCopy recht einfach: Bit-Reverse-Copy(a, A) 1 n = a.l¨a nge 2 for k = 0 to n − 1 3 A[rev(k)] = ak Die iterative FFT-Implementierung läuft in Zeit Θ(n lg n). Der Aufruf Bit-ReverseCopy(a, A) braucht sicherlich nur Zeit O(n lg n), da wir n-mal iterieren und eine ganze Zahl zwischen 0 und n−1 mit lg n Bits in Zeit O(lg n) invertieren können. (Da wir in der Praxis gewöhnlich den initialen Wert von n im Voraus kennen, würden wir wahrscheinlich mit einer Tabelle arbeiten, die k auf rev(k) abbildet, um so eine Θ(n)-Laufzeit für Bit-Reverse-Copy mit einer sehr kleinen, in der asymptotischen Notation versteckten Konstante zu erhalten. Alternativ dazu könnten wir den, in Problemstellung 17–1 beschriebenen, raffinierten amortisierten umkehrende Binärzähler verwenden.) Um den Beweis abzuschließen, dass Iterative-FFT in Zeit Θ(n lg n) läuft, zeigen wir, dass L(n), d. h. die Häufigkeit, mit der der Körper der innersten Schleife (Zeilen 8–13) durchlaufen wird, in Θ(n lg n) ist. Die for-Schleife der Zeilen 6–13 iteriert für jeden Wert s genau n/m = n/2s -mal und die innerste Schleife der Zeilen 8–13 iteriert m/2 = 2s−1 mal. Somit gilt
L(n) =
=
lg n n · 2s−1 s 2 s=1 lg n n s=1
2
= Θ(n lg n) .
30.3 Effiziente Implementierung der FFT
931
a0
y0 ω20
a1
y1 ω40
a2
y2 ω20
ω41
a3
y3 ω80
a4
y4 ω81
ω20 a5
y5 ω40
ω82
ω41
ω83
a6
y6 ω20
a7
y7 Ebene s = 1
Ebene s = 2
Ebene s = 3
Abbildung 30.5: Ein Schaltkreis, der die FFT parallel berechnet, hier mit n = 8 Eingängen gezeigt. Jede Butterfly-Operation benötigt Eingabewerte an zwei Leitungen sowie einen Drehfaktor. Sie erzeugt Ausgabewerte an zwei Leitungen. Die Ebenen der Butterfly-Operatoren sind entsprechend der Iteration der äußersten Schleife der Prozedur Iterative-FFT gekennzeichnet. Nur die oberen und unteren Leitungen durch einen Butterfly-Operator interagieren mit diesem, Leitungen, die durch die Mitte eines Butterfly-Operators laufen, beeinflussen diesen nicht, und ihre Werte werden durch den Butterfly-Operator nicht verändert. Zum Beispiel hat der obere Butterfly-Operator in der Ebene 2 nichts mit der Leitung 1 (deren Ausgabe durch y1 gekennzeichnet ist) zu tun; seine Ein- und Ausgaben liegen nur auf den Leitungen 0 und 2 (die durch y0 beziehungsweise y2 gezeichnet sind). Dieser Schaltkreis hat Tiefe Θ(lg n) und führt insgesamt Θ(n lg n) Butterfly-Operationen aus.
Ein paralleler FFT-Schaltkreis Wir können viele der Eigenschaften, die uns erlaubten, einen effizienten iterativen FFTAlgorithmus zu implementieren, nutzen, um einen parallelen Algorithmus zur FFT zu entwerfen. Wir werden den parallelen FFT-Algorithmus durch einen Schaltkreis darstellen. Abbildung 30.5 zeigt einen parallelen Schaltkreis, der die FFT auf n = 8 Eingabewerten berechnet. Der Schaltkreis startet mit einer Bit-umkehrenden Permutation der Eingaben, gefolgt von lg n Stufen, wobei jede Stufe aus n/2 parallel ausgeführten Butterfly-Operationen besteht. Die Tiefe des Schaltkreises – d. h. die maximale Anzahl von Gattern zwischen einem Eingang und einem von diesem Eingang erreichbaren Ausgang – ist somit Θ(lg n). Der am weitesten links liegende Teilschaltkreis des parallelen FFT-Schaltkreises führt die Bit-umkehrende Permutation aus; der Restschaltkreis imitiert die iterative Prozedur Iterative-FFT. Da jede Iteration der äußeren for-Schleife n/2 voneinander unabhängige Butterfly-Operationen ausführt, führt der Schaltkreis diese parallel durch. Der Wert
932
30 Polynome und die FFT
von s in jeder der Iterationen von Iterative-FFT entspricht der Ebene eines in Abbildung 30.5 gezeigten Butterfly-Operators. Für s = 1, 2, . . . , lg n besteht die Ebene s aus n/2s Gruppen (die den verschiedenen Werten von k in Iterative-FFT entsprechen) mit jeweils 2s−1 Butterfly-Operatoren (die den verschiedenen Werten von j in Iterative-FFT entsprechen). Die in Abbildung 30.5 gezeigten Butterfly-Operatoren entsprechen den Butterfly-Operationen der innersten Schleife (Zeilen 9–12) von Iterative-FFT. Beachten Sie außerdem, dass die in den Butterfly-Operatoren verwendeten Drehfaktoren denen in Iterative-FFT verwendeten entsprechen: in der Ebene s bem/2−1 0 1 nutzen wir ωm , ωm , . . . , ωm mit m = 2s .
Übungen 30.3-1 Zeigen Sie, wie Iterative-FFT die DFT des Eingabevektors (0, 2, 3, −1, 4, 5, 7, 9) berechnet. 30.3-2 Zeigen Sie, wie wir einen FFT-Algorithmus implementieren können, der die Bit-umkehrende Permutation am Ende und nicht zu Beginn der Berechnung ausführt. (Hinweis: Betrachten Sie die inverse DFT.) 30.3-3 Wie häufig berechnet Iterative-FFT in jeder Ebene Drehfaktoren? Schreiben Sie Iterative-FFT so um, dass im Zustand s nur 2s−1 -mal Drehfaktoren berechnet werden. 30.3-4∗ Nehmen Sie an, die Addierer innerhalb der Butterfly-Operationen des FFTKreises würden manchmal in dem Sinne versagen, dass sie, unabhängig von ihren Eingaben, immer eine 0 ausgeben. Nehmen Sie weiter an, dass genau ein Addierer versagen würde, Sie aber nicht wissen welcher. Beschreiben Sie, wie wir den fehlerhaften Addierer identifizieren können, indem wir an den FFTSchaltkreis Eingaben anlegen und die Ausgaben beobachten. Wie effizient ist Ihre Methode?
Problemstellungen 30-1 Teile-und-Beherrsche-Multiplikation a. Zeigen Sie, wie wir zwei lineare Polynome ax + b und cx + d unter Verwendung von nur drei Multiplikationen miteinander multiplizieren können. (Hinweis: Eine der Multiplikationen ist (a + b) · (c + d).) b. Geben Sie zwei Teile-und-Beherrsche-Algorithmen zur Multiplikation zweier Polynome mit Gradschranke n an, die mit Zeit Θ(nlg 3 ) auskommen. Der erste Algorithmus sollte die Koeffizienten der Eingabepolynome in eine obere Hälfte und in eine untere Hälfte teilen. Der zweite Algorithmus sollte die Koeffizienten abhängig davon, ob der Index gerade oder ungerade ist, teilen. c. Zeigen Sie, wie wir zwei n-Bit-Integerzahlen in O(nlg 3 ) Schritten miteinander multiplizieren können, wobei jeder Schritt auf höchstens konstant vielen 1-BitWerten arbeitet.
Problemstellungen zu Kapitel 30
933
30-2 Toeplitz-Matrizen Eine Toeplitz-Matrix ist eine n × n-Matrix A = (akj ), für die akj = ak−1,j−1 mit k = 2, 3, . . . , n und j = 2, 3, . . . , n gilt. a. Ist die Summe zweier Toeplitz-Matrizen ebenfalls eine Toeplitz-Matrix? Ist das Produkt zweier Toeplitz-Matrizen ebenfalls eine Toeplitz-Matrix? b. Beschreiben Sie, wie wir eine Toeplitz-Matrix so darstellen können, dass wir zwei n × n-Toeplitz-Matrizen in Zeit O(n) addieren können. c. Geben Sie einen Algorithmus zur Multiplikation einer n × n-Toeplitz-Matrix mit einem Vektor der Länge n an, der eine Laufzeit von O(n lg n) hat. Verwenden Sie Ihre Darstellung aus Teil (b). d. Geben Sie einen effizienten Algorithmus zur Multiplikation zweier n × nToeplitz-Matrizen an. Analysieren Sie die Laufzeit Ihres Algorithmus. 30-3 Mehrdimensionale schnelle Fourier-Transformation Wir können die in Gleichung (30.8) definierte eindimensionale diskrete FourierTransformation auf d Dimensionen verallgemeinern. Die Eingabe ist ein d-dimensionales Feld A = (aj1 ,j2 ,...,jd ), dessen Dimensionen n1 , n2 , . . . , nd sind, mit n1 n2 · · · nd = n. Wir definieren die d-dimensionale diskrete Fourier-Transformation durch die Gleichung yk1 ,k2 ,...,kd =
n 1 −1 n 2 −1 j1 =0 j2 =0
···
n d −1
aj1 ,j2 ,...,jd ωnj11k1 ωnj22k2 · · · ωnjddkd
jd =0
mit 0 ≤ k1 < n1 , 0 ≤ k2 < n2 , . . . , 0 ≤ kd < nd . a. Zeigen Sie, dass wir eine d-dimensionale DFT berechnen können, indem wir der Reihe nach in jeder Dimension eindimensionale DFTs ausführen. Das heißt, wir berechnen zunächst n/n1 separate eindimensionale DFTs entlang der Dimension 1. Danach berechnen wir, unter Verwendung der Resultate der DFTs entlang der Dimension 1, n/n2 separate eindimensionale DFTs entlang der Dimension 2. Wir verwenden diese Ergebnisse als Eingaben zur Berechnung von n/n3 separaten eindimensionalen DFTs entlang Dimension 3, usw. bis zur Dimension d. b. Zeigen Sie, dass die Reihenfolge der Dimensionen keine Rolle spielt, sodass wir die d-dimensionale DFT berechnen können, indem wir die eindimensionalen DFTs in beliebiger Reihenfolge der d Dimensionen bestimmen. c. Zeigen Sie, dass, wenn wir jede eindimensionale DFT mittels schneller FourierTransformation bestimmen, die Gesamtzeit für die Berechnung einer d-dimensionalen DFT, unabhängig von d, in O(n lg n) liegt. 30-4 Auswertung aller Ableitungen eines Polynoms an einem Punkt Für ein gegebenes Polynom A(x) mit Gradschranke n definieren wir die t-te Ableitung durch ⎧ A(x) falls t = 0 , ⎪ ⎪ ⎨ (t−1) d (x) falls 1 ≤ t ≤ n − 1 , A(t) (x) = dx A ⎪ ⎪ ⎩ 0 falls t ≥ n .
934
30 Polynome und die FFT Wir wollen für einen gegebenen Punkt x0 für t = 0, 1, . . . , n − 1 den Ausdruck A(t) (x0 ) aus der Koeffizientendarstellung (a0 , a1 , . . . , an−1 ) von A(x) bestimmen. a. Zeigen Sie, wie wir für gegebene Koeffizienten b0 , b1 , . . . , bn−1 mit A(x) =
n−1
bj (x − x0 )j
j=0
A(t) (x0 ) für t = 0, 1, . . . , n − 1 in Zeit O(n) berechnen können. b. Erklären Sie, wie wir b0 , b1 , . . . , bn−1 in Zeit O(n lg n) bestimmen können, wenn die Werte A(x0 + ωnk ) für k = 0, 1, . . . , n − 1 gegeben sind. c. Beweisen Sie, dass ⎞ kr n−1 ω ⎝ n A(x0 + ωnk ) = f (j)g(r − j)⎠ r! r=0 j=0 n−1
⎛
mit f (j) = aj · j! und < x−l 0 /(−l)! g(l) = 0
falls −(n − 1) ≤ l ≤ 0 , falls 1 ≤ l ≤ n − 1
gilt. d. Erklären Sie, wie wir A(x0 + ωnk ) für k = 0, 1, . . . , n − 1 in Zeit O(n lg n) auswerten können. Schlussfolgern Sie, dass wir alle nichttrivialen Ableitungen von A(x) an der Stelle x0 in Zeit O(n lg n) auswerten können. 30-5 Auswertung eines Polynoms an verschiedenen Punkten Wir haben gesehen, dass wir ein Polynom mit Gradschranke n an einem einzelnen Punkt mithilfe des Horner-Schemas in Zeit O(n) auswerten können. Wir haben ebenfalls diskutiert, wie wir ein solches Polynom für alle n komplexen Einheitswurzeln mithilfe der FFT in Zeit O(n lg n) auswerten können. Wir werden nun zeigen, wie wir ein Polynom mit Gradschranke n an n beliebigen Punkten in Zeit O(n lg2 n) auswerten können. Dazu werden wir ausnutzen, dass wir den Rest eines Polynoms, der bei der Division des Polynoms durch ein anderes Polynoms entsteht, in Zeit O(n lg n) berechnen können. (Ein Resultat, das wir hier nicht beweisen werden.) Zum Beispiel ist der Rest der Division von 3x3 + x2 − 3x + 1 durch x2 + x + 2 gleich (3x3 + x2 − 3x + 1) mod (x2 + x + 2) = −7x + 5 . n−1 Wir wollen für ein in Koeffizientenform A(x) = k=0 ak xk gegebenes Polynom und n Punkte x0 , x1 , . . . , xn−1 die n Werte A(x0 ), A(x1 ), . . . , A(xn−1 ) berechnen. =t Für 0 ≤ s ≤ t ≤ n − 1 definieren wir die Polynome Pst (x) = k=s (x − xk ) und Qst (x) = A(x) mod Pst (x). Beachten Sie, dass Qst (x) höchstens den Grad t − s besitzt.
Problemstellungen zu Kapitel 30
935
a. Beweisen Sie, dass für jeden beliebigen Punkt z die Gleichung A(z) = A(x) mod (x − z) gilt. b. Beweisen Sie, dass Qkk (x) = A(xk ) und Q0,n−1 (x) = A(x) gilt. c. Beweisen Sie, dass für s ≤ k ≤ t die Gleichungen Qsk (x) = Qst (x) mod Psk (x) und Qkt (x) = Qst (x) mod Pkt (x) gelten. d. Geben Sie einen Algorithmus mit Laufzeit O(n lg2 n) für die Berechnung von A(x0 ), A(x1 ), . . . , A(xn−1 ) an. 30-6 FFT mithilfe modularer Arithmetik Per Definition verlangt die diskrete Fourier-Transformation von uns, mit komplexen Zahlen zu rechnen, was zu einem Verlust an Genauigkeit aufgrund von Rundungsfehlern führen kann. Bei einigen Problemen ist bekannt, dass das Ergebnis nur ganze Zahlen enthält und dass, indem wir eine auf modularer Arithmetik basierende Version der FFT anwenden, wir sicherstellen können, dass die Antwort exakt berechnet wird. Ein Beispiel für ein solches Problem ist die Multiplikation zweier Polynome mit ganzzahligen Koeffizienten. Übung 30.2-6 stellt einen Ansatz vor, der Moduli der Länge Ω(n) Bits verwendet, um eine DFT an n Punkten zu bearbeiten. Die hier behandelte Problemstellung stellt einen anderen Ansatz vor, der Moduli der mehr sinnvollen Länge O(lg n) verwendet. Um den weiteren Ausführungen folgen zu können, sollten Sie den Stoff aus Kapitel 31 verstanden haben. Wir setzen im Folgenden voraus, dass n eine Zweierpotenz ist. a. Nehmen Sie an, wir würden das kleinste k suchen, sodass p = kn + 1 eine Primzahl ist. Geben Sie ein einfaches heuristisches Argument an, weshalb wir vernünftigerweise erwarten können, dass k näherungsweise den Wert lg n hat. (Der Wert von k kann möglicherweise viel kleiner oder größer sein, aber wir können erwarten, dass wir im Mittel O(lg n) Kandidatenwerte für k zu untersuchen haben.) Wie groß ist die erwartete Länge von p im Vergleich zur Länge von n? Sei g ein erzeugendes Element von Z∗p , und sei w = g k mod p. b. Zeigen Sie, dass die DFT und die inverse DFT wohldefinierte Operationen modulo p sind, wobei w als n-te Haupteinheitswurzel verwendet wird. c. Zeigen Sie, wie wir die FFT und deren Inverse so modifizieren können, dass sie modulo p in Zeit O(n lg n) arbeiten, wobei Operationen auf Wörtern der Länge O(lg n) Bits eine Laufzeit in Höhe einer Zeiteinheit benötigen. Setzen Sie voraus, dass der Algorithmus p und w als Eingaben erhält. d. Berechnen Sie die DFT modulo p = 17 des Vektors (0, 5, 3, 7, 7, 2, 1, 6). Beachten Sie, dass g = 3 ein erzeugendes Element von Z∗17 ist.
936
30 Polynome und die FFT
Kapitelbemerkungen Das Buch von Van Loan [343] enthält eine hervorragende Behandlung der schnellen Fourier-Transformation. Press, Teukolsky, Vetterling und Flannery [283, 284] geben eine gute Beschreibung der schnellen Fourier-Transformation und ihrer Anwendungen. Für eine exzellente Einführung in die Signalverarbeitung, einem populären Anwendungsgebiet der FFT, verweisen wir auf die Bücher von Oppenheim und Schafer [266] sowie Oppenheim und Willsky [267]. Das Buch von Oppenheim und Schafer zeigt auch, wie Fälle, in denen n keine ganzzahlige Potenz von 2 ist, zu bearbeiten sind. Die Fourier-Analyse ist nicht auf eindimensionale Daten beschränkt. Sie wird in großem Maße in der Bildverarbeitung zur Analyse von Daten in zwei oder mehr Dimensionen verwendet. Die Bücher von Gonzalez und Woods [146] sowie Pratt [281] diskutieren die mehrdimensionale Fourier-Transformation und deren Verwendung in der Bildverarbeitung. In den Büchern von Tolimieri, An und Lu [338] und Van Loan [343] wird die Mathematik, die sich hinter der mehrdimensionalen schnellen Fourier-Transformation versteckt, diskutiert. Weitgehend wird Cooley und Tukey [76] die Entwicklung der FFT in den 1960-er Jahren zugeschrieben. Tatsächlich ist die FFT bereits vorher viele Male entdeckt worden, aber deren Wichtigkeit wurde vor dem Aufkommen moderner digitaler Rechensystemen nicht vollständig erkannt. Während Press, Teukolsky, Vetterling und Flannery die Ursprünge der Methode Runge und König im Jahr 1924 zuschreiben, führt ein Artikel von Heideman, Johnson und Burrus [163] die Geschichte der FFT sogar auf C. F. Gauß (1805) zurück. Frigo und Johnson [117] entwickelten eine schnelle und flexible Implementierung der FFT, die sie FFTW („fastest Fourier transform in the West“, oder übersetzt „schnellste Fourier-Transformation im Westen“) nennen. FFTW ist für Szenarien entworfen, die mehrere DFT-Berechnungen auf der gleichen Problemgröße erfordern. Bevor die DFTs tatsächlich berechnet werden, führt FFTW einen „Planer“ aus, der über eine Reihe von Versuchen bestimmt, wie für eine gegebene Problemgröße die FFT-Berechnung auf dem Hostrechner am besten zu zerlegen ist. FFTW versucht, den Hardware-Cache effizient auszunutzen, und sobald die Teilprobleme klein genug sind, löst FFTW sie mit einem optimierten gradlinigen Code. Zudem besitzt FFTW den ungewöhnlichen Vorteil, dass sie Zeit Θ(n lg n) für jede Problemgröße n benötigt, sogar dann, wenn n eine große Primzahl ist. Wenngleich die gewöhnliche Fourier-Transformation voraussetzt, dass die Eingabe aus Punkten besteht, die über den Zeitbereich gleichmäßig verteilt sind, können andere Verfahren die FFT auf „nichtäquidistanten“ Daten approximieren. Der Artikel von Ware [348] gibt hierzu einen Überblick.
31
Zahlentheoretische Algorithmen
Die Zahlentheorie wurde einst als schönes, aber im Großen und Ganzen nutzloses Gebiet der reinen Mathematik betrachtet. Heute werden zahlentheoretische Algorithmen vielfach verwendet, was zum großen Teil durch die Entwicklung kryptographischer Methoden auf der Basis großer Primzahlen bedingt ist. Diese Methoden sind praktikabel, da wir einfach große Primzahlen finden können, und sie sind sicher, da wir nicht wissen, wie wir das Produkt großer Primzahlen effizient faktorisieren können (oder verwandte Probleme wie die Berechnung diskreter Logarithmen effizient lösen können). Dieses Kapitel stellt einige Ergebnisse der Zahlentheorie sowie die dazugehörigen Algorithmen vor, die solchen Anwendungen zugrundeliegen. Abschnitt 31.1 führt grundlegende Konzepte der Zahlentheorie ein, wie zum Beispiel Teilbarkeit, modulare Äquivalenz und die Eindeutigkeit der Faktorisierung. Abschnitt 31.2 beschäftigt sich mit einem der ältesten Algorithmen überhaupt, dem Euklidischen Algorithmus zur Berechnung des größten gemeinsamen Teilers von zwei ganzen Zahlen. Abschnitt 31.3 gibt eine Zusammenfassung der Konzepte der modularen Arithmetik. Abschnitt 31.4 untersucht dann die Menge der Vielfachen einer Zahl a modulo n und zeigt, wie wir mithilfe des Euklidischen Algorithmus alle Lösungen der Gleichung ax ≡ b (mod n) finden können. Der chinesische Restsatz wird in Abschnitt 31.5 vorgestellt. Abschnitt 31.6 betrachtet Potenzen einer gegebenen Zahl a modulo n und stellt einen Algorithmus des wiederholten Quadrierens für die effiziente Berechnung von ab mod n bei gegebenem a, b und n vor. Diese Operation ist der Kern des Tests, ob eine gegebene Zahl eine Primzahl ist, und somit von großer Bedeutung in der modernen Kryptographie. Abschnitt 31.7 beschreibt dann die RSA-Verschlüsselung. Abschnitt 31.8 untersucht einen randomisierten Primzahltest. Wir können diesen Test anwenden, um große Primzahlen effizient zu finden, was wir können müssen, um Schlüssel für das RSA-Kryptosystem zu erzeugen. Abschließend gibt Abschnitt 31.9 eine einfache, aber effektive Heuristik für die Faktorisierung kleiner ganzer Zahlen an. Ironischerweise ist die Faktorisierung ein Problem, von dem sich viele Leute wünschen, dass es nicht in den Griff zu bekommen ist, da die Sicherheit von RSA von der Schwierigkeit der Faktorisierung großer Zahlen abhängt.
Größe der Eingaben und Kosten arithmetischer Berechnungen Da wir mit großen ganzen Zahlen arbeiten werden, müssen wir abgleichen, was wir unter der Größe einer Eingabe verstehen und wie wir die Kosten der elementaren arithmetischen Operationen ansetzen. In diesem Kapitel verstehen wir unter einer „großen Eingabe“ in der Regel eine Eingabe, die „große ganze Zahlen“ enthält, und nicht eine Eingabe, die aus „vielen Zahlen“ besteht (wie beispielsweise beim Sortieren). Wir werden daher die Größe einer Eingabe anhand
938
31 Zahlentheoretische Algorithmen
der Anzahl der Bits messen, die zu ihrer Darstellung erforderlich sind, und nicht anhand der Anzahl der Zahlen, die zur Eingabe gehören. Ein Algorithmus mit den ganzzahligen Eingabewerten a1 , a2 , . . . , ak ist ein Algorithmus mit polynomieller Laufzeit, wenn seine Laufzeit polynomiell in lg a1 , lg a2 , . . . , lg ak ist, d. h. polynomiell in den Längen seiner binär codierten Eingaben. In diesem Buch war es bisher angemessen, die arithmetischen Operationen (Additionen, Subtraktionen, Multiplikationen, Divisionen oder die Berechnung von Resten) als elementare Operationen zu betrachten, die jeweils eine Zeiteinheit erfordern. Durch die Anzahl solcher Operationen, die ein Algorithmus ausführt, haben wir eine Basis für eine realistische Abschätzung der tatsächlichen Laufzeit des Algorithmus auf einem Rechner. Elementare Operationen können jedoch zeitaufwendig sein, wenn ihre Eingaben groß sind. Es ist in diesem Fall angebrachter, zu messen, wie viele Bitoperationen ein entsprechender zahlentheoretischer Algorithmus erfordert. In diesem Modell benötigt die Multiplikation zweier β-Bit-Integerzahlen mit der gewöhnlichen Multiplikationsmethode Θ(β 2 ) Bitoperationen. Entsprechend können wir mit den gewöhnlichen Algorithmen in Zeit Θ(β 2 ) eine β-Bit-Integerzahl durch eine kürzere Integerzahl dividieren oder den Rest einer β-Bit-Integerzahl bei der Division durch eine kürzere Integerzahl bilden. (Siehe Übung 31.1-12.) Schnellere Methoden sind bekannt. Beispielsweise hat eine einfache Teile-und-Beherrsche-Methode für die Multiplikation zweier β-Bit-Integerzahlen eine Laufzeit von Θ(β lg 3 ) und die schnellste bekannte Methode hat eine Laufzeit von Θ(β lg β lg lg β). Für praktische Zwecke ist der Θ(β 2 )-Algorithmus jedoch häufig der beste, und wir werden diese Schranke als Basis für unsere Analysen benutzen. Wir werden in diesem Kapitel die Algorithmen in der Regel hinsichtlich der Anzahl der arithmetischen Operationen und der Anzahl der Bitoperationen, die sie benötigen, analysieren.
31.1
Elementare zahlentheoretische Begriffe
Dieser Abschnitt bietet einen kurzen Überblick über Begriffe der elementaren Zahlentheorie, die die Menge Z = {. . . , −2, −1, 0, 1, 2, . . .} der ganzen Zahlen und die Menge N = {0, 1, 2, . . .} der natürlichen Zahlen betreffen.
Teilbarkeit und Teiler Der Begriff der Teilbarkeit einer ganzen Zahl durch eine andere ist von zentraler Bedeutung in der Zahlentheorie. Die Bezeichnung d | a (gesprochen „d teilt a“) bedeutet, dass es eine ganze Zahl k gibt, für die a = kd gilt. Jede ganze Zahl teilt die 0. Für a > 0 und d | a gilt |d| ≤ |a|. Im Falle d | a sagen wir auch, dass a ein Vielfaches von d ist. Wenn d die Zahl a nicht teilt, schreiben wir d | a. Gilt d | a und d ≥ 0, sagen wir, dass d ein Teiler von a ist. Beachten Sie, dass d | a genau dann gilt, wenn auch −d | a gilt, sodass wir ohne Beschränkung der Allgemeinheit Teiler als nichtnegativ definieren können, mit dem Verständnis, dass der mit −1 multiplizierte Wert jedes Teilers von a die Zahl a ebenfalls teilt. Ein Teiler einer von 0 verschiedenen ganzen Zahl a ist mindestens gleich 1, aber nicht größer als |a|. Die Teiler von 24
31.1 Elementare zahlentheoretische Begriffe
939
beispielsweise sind 1, 2, 3, 4, 6, 8, 12 und 24. Jede ganze Zahl a ist durch die trivialen Teiler 1 und a teilbar. Nichttriviale Teiler von a sind die Faktoren von a. Die Faktoren von 20 beispielsweise sind 2, 4, 5 und 10.
Primzahlen und zusammengesetzte Zahlen Eine ganze Zahl a > 1, deren einzige Teiler die trivialen Teiler 1 und a sind, heißt Primzahl (oder einfach nur prim). Primzahlen haben viele spezielle Eigenschaften und spielen eine bedeutende Rolle in der Zahlentheorie. Die ersten 20 Primzahlen lauten 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71 . Übung 31.1-2 verlangt von Ihnen, zu zeigen, dass es unendlich viele Primzahlen gibt. Eine ganze Zahl a > 1, die keine Primzahl ist, heißt zusammengesetzte Zahl . Die Zahl 39 zum Beispiel ist zusammengesetzt, denn es gilt 3 | 39. Wir nennen die Zahl 1 Einheit und sie ist weder eine Primzahl noch eine zusammengesetzte Zahl. Auch die Zahl 0 und alle negativen ganzen Zahlen sind weder Primzahlen noch zusammengesetzte Zahlen.
Der Divisionssatz, Reste und modulare Äquivalenz Wir können für eine gegebene Zahl n die Menge der ganzen Zahlen in die Teilmenge der Zahlen, die ein Vielfaches von n sind, und in die Teilmenge der Zahlen, die kein Vielfaches von n sind, partitionieren. Viele Bereiche der Zahlentheorie basieren auf einer Verfeinerung dieser Partition, in der die Nichtvielfachen bezüglich ihrem Rest bei der Division durch n klassifiziert sind. Das folgende Theorem liefert die Grundlage für diese Verfeinerung. Wir geben den Beweis des Theorems hier nicht an und verweisen beispielsweise auf Niven und Zuckerman [265]). Theorem 31.1: (Divisionssatz) Für jede ganze Zahl a und jede positive ganze Zahl n existiert ein eindeutiges Paar von ganzen Zahlen q und r mit 0 ≤ r < n, für die a = qn + r gilt. Der Wert q = a/n ist der Quotient und der Wert r = a mod n der Rest der Division. Die Relation n | a gilt genau dann, wenn a mod n = 0 gilt. Wir können die ganzen Zahlen entsprechend ihrer Reste modulo n in n Äquivalenzklassen einteilen. Die Äquivalenzklasse modulo n einer ganzen Zahl a ist [a]n = {a + kn : k ∈ Z} . Beispielsweise ist [3]7 = {. . . , −11, −4, 3, 10, 17, . . .}; wir können diese Menge auch durch [−4]7 und [10]7 bezeichnen. In der auf Seite 56 definierten Notation ist die Aussage a ∈ [b]n identisch mit a ≡ b (mod n). Die Menge all dieser Äquivalenzklassen ist Zn = {[a]n : 0 ≤ a ≤ n − 1} .
(31.1)
940
31 Zahlentheoretische Algorithmen
Wenn Sie die Definition Zn = {0, 1, . . . , n − 1}
(31.2)
sehen, so sollten Sie sie als äquivalent mit Gleichung (31.1) interpretieren, mit dem Verständnis, dass 0 die Klasse [0]n , 1 die Klasse [1]n und so weiter darstellt. Jede Klasse wird durch ihr kleinstes nichtnegatives Element repräsentiert. Wenn wir beispielsweise auf die −1 als Element von Zn verweisen, so verweisen wir wegen −1 ≡ n − 1 (mod n) tatsächlich auf [n − 1]n .
Gemeinsame Teiler und größte gemeinsame Teiler Wenn d ein Teiler von a und ein Teiler von b ist, dann heißt d gemeinsamer Teiler von a und b. Die Teiler von 30 beispielsweise sind 1, 2, 3, 5, 6, 10, 15 und 30, sodass die gemeinsamen Teiler von 24 und 30 die Zahlen 1, 2, 3 und 6 sind. Beachten Sie, dass 1 ein gemeinsamer Teiler für jedes Paar ganzer Zahlen ist. Eine wichtige Eigenschaft gemeinsamer Teiler ist die Beziehung d | a und d | b ⇒ d | (a + b) und d | (a − b) .
(31.3)
Allgemeiner gilt d | a und d | b ⇒ d | (ax + by)
(31.4)
für beliebige ganze Zahlen x und y. Außerdem gilt im Falle a | b entweder |a| ≤ |b| oder b = 0, sodass a | b und b | a ⇒ a = ±b
(31.5)
gilt. Wir bezeichnen den größten gemeinsamen Teiler von zwei verschiedenen ganzen Zahlen a und b, die nicht beide gleich 0 sind, mit ggT(a, b). Beispielsweise gilt ggT(24, 30) = 6, ggT(5, 7) = 1 und ggT(0, 9) = 9. Wenn a und b beide von 0 verschieden sind, dann ist ggT(a, b) eine ganze Zahl zwischen 1 und min(|a| , |b|). Wir definieren ggT(0, 0) als 0. Diese Definition ist notwendig, damit die normalen Eigenschaften der Funktion ggT (wie beispielsweise Gleichung (31.9)) universell gültig sind. Die folgenden Beziehungen sind elementare Eigenschaften der Funktion ggT: ggT(a, b) = ggT(b, a) , ggT(a, b) = ggT(−a, b) , ggT(a, b) = ggT(|a| , |b|) , ggT(a, 0) = |a| , ggT(a, ka) = |a| für jedes k ∈ Z .
(31.6) (31.7) (31.8) (31.9) (31.10)
Das folgende Theorem ermöglicht eine alternative und nützliche Charakterisierung des größten gemeinsamen Teilers ggT(a, b).
31.1 Elementare zahlentheoretische Begriffe
941
Theorem 31.2 Für zwei von 0 verschiedene ganze Zahlen a und b ist ggT(a, b) das kleinste positive Element der Menge {ax + by : x, y ∈ Z} der Linearkombinationen von a und b. Beweis: Sei s die kleinste positive derartige Linearkombination von a und b und sei s = ax + by für zwei Zahlen x, y ∈ Z. Weiter sei q = a/s. Gleichung (3.8) impliziert dann a mod s = a − qs = a − q(ax + by) = a (1 − qx) + b (−qy) , sodass a mod s ebenfalls eine Linearkombination von a und b ist. Wegen 0 ≤ a mod s < s gilt aber a mod s = 0, da s die kleinste positive derartige Linearkombination ist. Somit haben wir s | a und mit einer analogen Argumentation auch s | b. Damit ist s ein gemeinsamer Teiler von a und b, und es gilt ggT(a, b) ≥ s. Aus Gleichung (31.4) folgt ggT(a, b) | s, da ggT(a, b) sowohl a als auch b teilt und s eine Linearkombination von a und b ist. Aus ggT(a, b) | s und s > 0 folgt aber ggT(a, b) ≤ s. Kombinieren wir die Ungleichungen ggT(a, b) ≥ s und ggT(a, b) ≤ s, so erhalten wir ggT(a, b) = s. Wir können also folgern, dass s der größte gemeinsame Teiler von a und b ist.
Korollar 31.3 Für jedes Paar von ganzen Zahlen a und b gilt d | ggT(a, b), wenn d | a und d | b. Beweis: Das Korollar folgt aus Gleichung (31.4), da ggT(a, b) nach Theorem 31.2 eine Linearkombination von a und b ist.
Korollar 31.4 Für jedes Paar ganzer Zahlen a und b und für jede nichtnegative ganze Zahl n gilt ggT(an, bn) = n ggT(a, b) .
Beweis: Für n = 0 ist das Korollar trivial. Für n > 0 ist ggT(an, bn) das kleinste positive Element der Menge {anx + bny : x, y ∈ Z}. Dieses ist n-mal so groß wie das kleinste positive Element der Menge {ax + by : x, y ∈ Z}.
Korollar 31.5 Für alle positiven ganzen Zahlen n, a und b mit n | ab und ggT(a, n) = 1 gilt n | b. Beweis: Den Beweis sollen Sie in Übung 31.1-5 führen.
942
31 Zahlentheoretische Algorithmen
Teilerfremde Zahlen Zwei ganze Zahlen a, b heißen teilerfremd oder relativ prim, wenn ihr einziger gemeinsamer Teiler gleich 1 ist, also wenn ggT(a, b) = 1 gilt. Die Zahlen 8 und 15 sind zum Beispiel teilerfremd, denn die Teiler von 8 sind 1, 2, 4 und 8, während die Teiler von 15 die Zahlen 1, 3, 5 und 15 sind. Das folgende Theorem sagt aus, dass das Produkt zweier ganzer Zahlen, die beide teilerfremd zu einer ganzen Zahl p sind, ebenfalls teilerfremd zu p ist. Theorem 31.6 Für alle ganzen Zahlen a, b und p mit ggT(a, p) = 1 und ggT(b, p) = 1 gilt ggT(ab, p) = 1. Beweis: Aus Theorem 31.2 folgt, dass es ganze Zahlen x, y, x und y gibt, für die ax + py = 1 , bx + py = 1 gilt. Multiplizieren wir diese Gleichungen miteinander, so erhalten wir ab(xx ) + p(ybx + y ax + pyy ) = 1 . Da 1 sich als positive Linearkombination von ab und p darstellen lässt, folgt aus Theorem 31.2 die Aussage des Theorems. Ganze Zahlen n1 , n2 , . . . , nk sind paarweise teilerfremd , wenn ggT(ni , nj ) = 1 für alle i = j gilt.
Eindeutigkeit der Faktorisierung Eine elementare, aber wichtige Tatsache im Zusammenhang mit der Teilbarkeit durch Primzahlen wird durch das folgende Theorem beschrieben. Theorem 31.7 Für jede Primzahl p und alle ganzen Zahlen a und b folgt aus p | ab, dass p | a oder p | b (oder beides) gilt. Beweis: Nehmen Sie zum Zwecke des Widerspruchs an, dass p | ab, p | a und p | b gelten würden. Dann ist ggT(a, p) = 1 und ggT(b, p) = 1, da p und 1 die einzigen Teiler von p sind und p laut unserer Annahme weder a noch b teilt. Aus Theorem 31.6 folgt dann ggT(ab, p) = 1, was unserer Annahme p | ab widerspricht, da aus p | ab ggT(ab, p) = p folgt. Dieser Widerspruch beweist das Theorem. Eine Folgerung aus Theorem 31.7 besteht darin, dass wir jede zusammengesetzte ganze Zahl eindeutig in ein Produkt von Primzahlen zerlegen können.
31.1 Elementare zahlentheoretische Begriffe
943
Theorem 31.8: (Eindeutigkeit der Faktorisierung) Es existiert jeweils genau eine Möglichkeit, eine zusammengesetzte ganze Zahl a als ein Produkt der Form a = pe11 pe22 · · · perr darzustellen, in dem die pi Primzahlen mit p1 < p2 < · · · < pr und die ei positive ganze Zahlen sind. Beweis: Der Beweis ist Gegenstand der Übung 31.1-11.
Die Zahl 6000 beispielsweise besitzt die eindeutige Primfaktorzerlegung 24 · 3 · 53 .
Übungen 31.1-1 Beweisen Sie, dass aus a > b > 0 und c = a + b die Gleichung c mod a = b folgt. 31.1-2 Beweisen Sie, dass es unendlich viele Primzahlen gibt. (Hinweis: Zeigen Sie, dass keine der Primzahlen p1 , p2 , . . . , pk die Zahl (p1 p2 · · · pk ) + 1 teilt.) 31.1-3 Beweisen Sie, dass aus a | b und b | c die Relation a | c folgt. 31.1-4 Beweisen Sie, dass, wenn p prim ist und 0 < k < p, ggT(k, p) = 1 gilt. 31.1-5 Beweisen Sie Korollar 31.5.
31.1-6 Beweisen Sie, dass, wenn p prim ist und 0 < k < p, die Relation p | kp gilt. Schlussfolgern Sie, dass für alle ganzen Zahlen a und b und alle Primzahlen p (a + b)p ≡ ap + bp
(mod p)
gilt. 31.1-7 Beweisen Sie, dass für beliebige positive ganze Zahlen a und b mit a | b und jedes x (x mod b) mod a = x mod a gilt. Beweisen Sie, dass unter den gleichen Voraussetzungen die Implikation x≡y
(mod b) ⇒ x ≡ y
(mod a)
für alle ganzen Zahlen x und y gilt. 31.1-8 Für eine ganze Zahl k > 0 ist eine ganze Zahl n eine k-te Potenz, falls es eine ganze Zahl a gibt, sodass ak = n gilt. Außerdem ist n > 1 eine nichttriviale Potenz, falls sie eine k-te Potenz für eine ganze Zahl k > 1 ist. Zeigen Sie, wie wir in polynomieller Zeit in β bestimmen können, ob eine gegebene β-BitIntegerzahl n eine nichttriviale Potenz ist.
944
31 Zahlentheoretische Algorithmen
31.1-9 Beweisen Sie die Gleichungen (31.6)–(31.10). 31.1-10 Beweisen Sie, dass der Operator ggT assoziativ ist, d. h. dass für alle ganzen Zahlen a, b und c ggT(a, ggT(b, c)) = ggT(ggT(a, b), c) gilt. 31.1-11∗ Beweisen Sie Theorem 31.8. 31.1-12 Geben Sie effiziente Algorithmen für das Dividieren einer β-Bit-Integerzahl durch eine kürzere Integerzahl sowie für das Bilden des Restes beim Dividieren durch eine kürzere Integerzahl an. Ihre Algorithmen sollten in Zeit O(β 2 ) laufen. 31.1-13 Geben Sie einen effizienten Algorithmus an, der eine in Binärdarstellung gegebene β-Bit-Integerzahl in Dezimaldarstellung umrechnet. Zeigen Sie, dass die Umrechnung von Binär- in Dezimaldarstellung in Zeit Θ(M (β) lg β) ausgeführt werden kann, wenn die Multiplikation oder die Division von ganzen Zahlen der maximalen Länge β Zeit M (β) benötigt. (Hinweis: Verwenden Sie einen Teile-und-Beherrsche-Ansatz, mit dem Sie die obere und die untere Hälfte des Ergebnisses durch separate Rekursionen erhalten.)
31.2
Größter gemeinsamer Teiler
In diesem Abschnitt beschreiben wir Euklids Algorithmus, mit dem wir den größten gemeinsamen Teiler von zwei ganzen Zahlen effizient berechnen können. Wenn wir die Laufzeit analysieren, werden wir einen überraschenden Zusammenhang mit den Fibonacci-Zahlen feststellen, die eine schlimmstmögliche Eingabe für Euklids Algorithmus darstellen. Wir beschränken uns in diesem Abschnitt auf nichtnegative ganze Zahlen. Diese Einschränkung ist wegen Gleichung (31.8) gerechtfertigt, die aussagt, dass ggT(a, b) = ggT(|a| , |b|) gilt. Prinzipiell können wir den größten gemeinsamen Teiler ggT(a, b) für positive ganze Zahlen a und b aus der Primfaktorzerlegung von a und b berechnen. Faktorisieren wir die beiden Zahlen in a = pe11 pe22 · · · perr ,
(31.11)
b = pf11 pf22 · · · pfrr ,
(31.12)
wobei in diesen beiden Gleichungen Exponenten verwendet werden können, die gleich 0 sind, damit die Mengen der benutzten Primzahlen in den beiden Gleichungen gleich sind, dann gilt, wie Übung 31.2-1 von Ihnen verlangt zu zeigen, min(e1 ,f1 ) min(e2 ,f2 ) p2
ggT(a, b) = p1
r ,fr ) · · · pmin(e . r
(31.13)
31.2 Größter gemeinsamer Teiler
945
Wie wir in Abschnitt 31.9 zeigen werden, laufen jedoch selbst die besten derzeit bekannten Algorithmen zur Faktorisierung nicht in polynomieller Zeit. Daher wird dieser Ansatz für die Berechnung des größten gemeinsamen Teilers kaum zu einem effizienten Algorithmus führen. Euklids Algorithmus zur Berechnung des größten gemeinsamen Teilers beruht auf dem folgenden Theorem. Theorem 31.9: (GGT-Rekursionssatz) Für jede nichtnegative ganze Zahl a und jede positive ganze Zahl b gilt ggT(a, b) = ggT(b, a mod b) . Beweis: Wir werden zeigen, dass ggT(a, b) und ggT(b, a mod b) einander teilen, sodass sie nach Gleichung (31.5) gleich sein müssen (da sie beide nichtnegativ sind). Zunächst zeigen wir ggT(a, b) | ggT(b, a mod b). Setzen wir d = ggT(a, b), dann gilt d | a und d | b. Nach Gleichung (3.8) ist (a mod b) = a − qb mit q = a/b. Da (a mod b) deshalb eine Linearkombination von a und b ist, folgt aus Gleichung (31.4) d | (a mod b). Wegen d | b und d | (a mod b) folgt dann aus Korollar 31.3 die Relation d | ggT(b, a mod b), oder äquivalent dazu ggT(a, b) | ggT(b, a mod b).
(31.14)
Der Beweis, dass ggT(b, a mod b) | ggT(a, b) gilt, verläuft ganz ähnlich. Setzen wir nun d = ggT(b, a mod b), dann gilt d | b und d | (a mod b). Aus a = qb + (a mod b) mit q =
a/b folgt, dass a eine Linearkombination von b und (a mod b) ist. Aus Gleichung (31.4) schlussfolgern wir d | a. Wegen d | b und d | a gilt nach Korollar 31.3 die Relation d | ggT(a, b), oder äquivalent dazu ggT(b, a mod b) | ggT(a, b).
(31.15)
Wenden wir Gleichung (31.5) an, um die beiden Gleichungen (31.14) und (31.15) zu kombinieren, so haben wir die Aussage des Theorems bewiesen.
Der Euklidische Algorithmus In den Elementen von Euklid (ca. 300 v. Chr.) wird unter anderem der folgende Algorithmus zur Bestimmung des größten gemeinsamen Teilers beschrieben. Sehr wahrscheinlich ist er noch früheren Ursprungs. Wir geben Euklids Algorithmus als ein rekursives Programm an, das unmittelbar auf Theorem 31.9 basiert. Die Eingabewerte a und b sind zwei beliebige nichtnegative ganze Zahlen. Euclid(a, b) 1 if b = = 0 2 return a 3 else return Euclid(b, a mod b)
946
31 Zahlentheoretische Algorithmen
Betrachten Sie als Beispiel für einen Lauf von Euclid die Berechnung von ggT(30, 21): Euclid(30, 21) = Euclid(21, 9) = Euclid(9, 3) = Euclid(3, 0) =3. Diese Berechnung ruft Euclid dreimal rekursiv auf. Die Korrektheit des Algorithmus Euclid folgt aus Theorem 31.9 und der Eigenschaft, dass, wenn der Algorithmus in Zeile 2 den Wert a zurückgibt, b = 0 gilt, sodass aus Gleichung (31.9) ggT(a, b) = ggT(a, 0) = a folgt. Der Algorithmus steigt nicht unendlich lang in die Rekursion ab, da das zweite Argument mit jedem Aufruf echt kleiner wird und immer nichtnegativ ist. Also terminiert der Algorithmus Euclid immer mit dem korrekten Ergebnis.
Die Laufzeit des Euklidischen Algorithmus Wir analysieren die Laufzeit der Prozedur Euclid im schlechtesten Fall als Funktion in der Größe von a und b. Wir setzen ohne Beschränkung der Allgemeinheit a > b ≥ 0 voraus. Diese Voraussetzung ist dadurch gerechtfertigt, dass, wenn b > a ≥ 0 gilt, Euclid(a, b) als nächstes den rekursiven Aufruf Euclid(b, a) durchführt. Das heißt, wenn das erste Argument kleiner als das zweite Argument ist, verwendet Euclid einen rekursiven Aufruf dafür, seine Argumente zu vertauschen, und fährt dann fort. Im Falle b = a > 0 terminiert die Prozedur wegen a mod b = 0 nach einem rekursiven Aufruf. Die Gesamtlaufzeit des Algorithmus Euclid ist proportional zur Anzahl der rekursiven Aufrufe. Unsere Analyse verwendet die Fibonacci-Zahlen Fk , die durch die Rekursionsgleichung (3.22) definiert sind. Lemma 31.10 Gilt a > b ≥ 1 und führt der Aufruf Euclid(a, b) k ≥ 1 rekursive Aufrufe aus, dann gilt a ≥ Fk+2 und b ≥ Fk+1 . Beweis: Der Beweis erfolgt durch Induktion nach k. Für den Induktionsanfang sei k = 1. Dann gilt b ≥ 1 = F2 , und wegen a > b muss a ≥ 2 = F3 gelten. Da b > (a mod b) gilt und in jedem rekursiven Aufruf das erste Argument echt größer als das zweite ist, ist die Voraussetzung a > b für jeden rekursiven Aufruf erfüllt. Setzen Sie induktiv voraus, dass das Lemma gilt, wenn k − 1 rekursive Aufrufe durchgeführt wurden. Wir werden dann beweisen, dass das Lemma für k rekursive Aufrufe ebenfalls gültig ist. Wegen k > 0 gilt b > 0, und Euclid(a, b) ruft die Prozedur Euclid(b, a mod b) rekursiv auf, die wiederum k − 1 rekursive Aufrufe durchführt. Aus der Induktionshypothese folgt dann b ≥ Fk+1 (womit ein Teil des Lemmas bewiesen ist)
31.2 Größter gemeinsamer Teiler
947
und (a mod b) ≥ Fk . Es gilt dann b + (a mod b) = b + (a − b a/b) ≤a, da aus a > b > 0 die Ungleichung a/b ≥ 1 folgt. Damit gilt a ≥ b + (a mod b) ≥ Fk+1 + Fk = Fk+2 . Das folgende Theorem ist eine unmittelbare Folgerung aus diesem Lemma. Theorem 31.11: (Satz von Lamé) Für jede ganze Zahl k ≥ 1 führt die Prozedur Euclid(a, b) weniger als k rekursive Aufrufe durch, wenn a > b ≥ 1 und b < Fk+1 gilt. Wir können zeigen, dass die in Theorem 31.11 bewiesene obere Schranke die bestmögliche ist, indem wir zeigen, dass der Aufruf Euclid(Fk+1 , Fk ) genau k − 1 rekursive Aufrufe macht, wenn k ≥ 2 ist. Wir beweisen dies durch Induktion nach k. Im Basisfall, k = 2, macht der Aufruf Euclid(F3 , F2 ) genau einen rekursiven Aufruf zu Euclid(1, 0). (Wir müssen bei k = 2 beginnen, da für k = 1 die Ungleichung F2 > F1 nicht gilt.) Setzen Sie für den Induktionsschritt voraus, dass Euclid(Fk , Fk−1 ) genau k − 2 rekursive Aufrufe macht. Für k > 2 gilt Fk > Fk−1 > 0 sowie Fk+1 = Fk + Fk−1 und somit wegen Übung 31.1-1 Fk+1 mod Fk = Fk−1 . Somit haben wir ggT(Fk+1 , Fk ) = ggT(Fk , Fk+1 mod Fk ) = ggT(Fk , Fk−1 ) . Demnach enthält der Aufruf Euclid(Fk+1 , Fk ) genau einen rekursiven Aufruf mehr als der Aufruf Euclid(Fk , Fk−1 ), d. h. genau k−1 Aufrufe, sodass die durch Theorem 31.11 gegebene obere Schranke scharf ist. √ φk / 5 ist, wobei φ der durch Gleichung (3.24) definierte goldene Da Fk näherungsweise √ Schnitt(1 + 5)/2 ist, ist die Anzahl der rekursiven Aufrufe in Euclid in O(lg b). (In Übung 31.2-5 sollen Sie eine schärfere Schranke zeigen.) Daraus folgt, dass Euclid, angewendet auf zwei β-Bit-Zahlen, O(β) arithmetische Operationen und O(β 3 ) Bitoperationen ausführt (vorausgesetzt, dass Multiplikation und Division von β-Bit-Zahlen O(β 2 ) Bitoperationen benötigen). Die Problemstellung 31.2-2 verlangt von Ihnen, eine Schranke von O(β 2 ) für die Anzahl der Bitoperationen zu beweisen.
948 a 99 78 21 15 6 3
31 Zahlentheoretische Algorithmen b 78 21 15 6 3 0
a/b 1 3 1 2 2 –
d 3 3 3 3 3 3
x −11 3 −2 1 0 1
y 14 −11 3 −2 1 0
Abbildung 31.1: Wie Extended-Euclid ggT(99, 78) berechnet. Jede Zeile zeigt eine Stufe der Rekursion: die Werte a und b der Eingabe, den berechneten Wert a/b und die zurückgegebenen Werte d, x und y. Das zurückgegebene Tripel (d, x, y) wird das Tripel (d , x , y ) in der nächst höheren Rekursionsstufe. Der Aufruf Extended-Euclid(99, 78) gibt (3, −11, 14) zurück, sodass ggT(99, 78) = 3 = 99 · (−11) + 78 · 14 gilt.
Die erweiterte Form des Euklidischen Algorithmus Wir formulieren nun den Euklidischen Algorithmus so um, dass er zusätzliche nützliche Informationen berechnet. Speziell wollen wir die ganzzahligen Koeffizienten x und y der Darstellung d = ggT(a, b) = ax + by
(31.16)
berechnen. Beachten Sie, dass x und y gleich 0 oder negativ sein können. Diese Koeffizienten werden sich uns bei der Berechnung der modularen multiplikativen Inverse als nützlich erweisen. Die Prozedur Extended-Euclid erhält ein Paar nichtnegativer ganzer Zahlen als Eingabe und gibt ein Tripel der Form (d, x, y) zurück, das die Gleichung (31.16) erfüllt. Extended-Euclid(a, b) 1 if b = = 0 2 return (a, 1, 0) 3 else (d , x , y ) = Extended-Euclid(b, a mod b) 4 (d, x, y) = (d , y , x − a/b y ) 5 return (d, x, y) Abbildung 31.1 illustriert, wie Extended-Euclid den größten gemeinsamen Teiler von 99 und 78 berechnet. Die Prozedur Extended-Euclid ist eine Variante der Prozedur Euclid. Zeile 1 von Extended-Euclid ist äquivalent zu dem Test „b = = 0 “ in Zeile 1 von Euclid. Ist b = 0, so gibt Extended-Euclid in Zeile 2 nicht nur d = a zurück, sondern auch die Koeffizienten x = 1 und y = 0, sodass a = ax + by gilt. Ist b = 0, so berechnet Extended-Euclid zunächst (d , x , y ) so, dass d = ggT(b, a mod b) und d = bx + (a mod b)y
(31.17)
gilt. Wie bei Euclid gilt in diesem Falle d = ggT(a, b) = d = ggT(b, a mod b). Um die Koeffizienten x und y so zu bestimmen, dass d = ax + by gilt, formen wir zunächst
31.2 Größter gemeinsamer Teiler
949
Gleichung (31.17) mithilfe der Gleichung d = d und Gleichung (3.8) um: d = bx + (a − b a/b)y = ay + b(x − a/b y ) . Für x = y und y = x − a/b y ist also die Gleichung d = ax + by erfüllt, womit die Korrektheit von Extended-Euclid bewiesen ist. Da die Anzahl der rekursiven Aufrufe in Euclid gleich der Anzahl der rekursiven Aufrufe in Extended-Euclid ist, sind die Laufzeiten von Euclid und ExtendedEuclid bis auf einen konstanten Faktor gleich. Das heißt, für a > b > 0 ist die Anzahl der rekursiven Aufrufe O(lg b).
Übungen 31.2-1 Beweisen Sie, dass aus den Gleichungen (31.11) und (31.12) die Gleichung (31.13) folgt. 31.2-2 Berechnen Sie die Werte (d, x, y), die der Aufruf Extended-Euclid(899, 493) zurückgibt. 31.2-3 Beweisen Sie, dass für beliebige ganze Zahlen a, k und n ggT(a, n) = ggT(a + kn, n) gilt. 31.2-4 Schreiben Sie die Prozedur Euclid in eine iterative Form um, die nur eine konstante Menge Speicher benötigt (d. h. nur eine konstante Anzahl von Integerwerten speichert). 31.2-5 Zeigen Sie, dass der Aufruf Euclid(a, b) für a > b ≥ 0 höchstens 1 + logφ b rekursive Aufrufe durchführt. Verbessern Sie diese Schranke auf 1+ logφ (b/ggT(a, b)). 31.2-6 Was gibt Extended-Euclid(Fk+1 , Fk ) zurück? Beweisen Sie, dass Ihre Antwort korrekt ist. 31.2-7 Definieren Sie die Funktion ggT für mehr als zwei Argumente durch die rekursive Gleichung ggT(a0 , a1 , . . . , an ) = ggT(a0 , ggT(a1 , a2 , . . . , an )). Zeigen Sie, dass der Wert der Funktion ggT von der Reihenfolge ihrer Argumente unabhängig ist. Zeigen Sie außerdem, wie wir ganze Zahlen x0 , x1 , . . . , xn bestimmen können, sodass ggT(a0 , a1 , . . . , an ) = a0 x0 + a1 x1 + · · · + an xn gilt. Zeigen Sie, dass die Anzahl der von Ihrem Algorithmus ausgeführten Divisionen in O(n + lg(max {a0 , a1 , . . . , an })) liegt. 31.2-8 Definieren Sie kgV(a1 , a2 , . . . , an ) als das kleinste gemeinsame Vielfache von n ganzen Zahlen a1 , a2 , . . . , an , d. h. als die kleinste nichtnegative ganze Zahl, die ein Vielfaches von jedem der ai ist. Zeigen Sie, wie wir kgV(a1 , a2 , . . . , an ) effizient berechnen können, indem wir die ggT-Operation (in zwei Argumenten) als Unterroutine verwenden.
950
31 Zahlentheoretische Algorithmen
31.2-9 Beweisen Sie, dass n1 , n2 , n3 und n4 genau dann paarweise teilerfremd sind, wenn ggT(n1 n2 , n3 n4 ) = ggT(n1 n3 , n2 n4 ) = 1 gilt. Zeigen Sie dann die allgemeinere Aussage, dass die Zahlen n1 , n2 , . . . , nk genau dann paarweise teilerfremd sind, wenn eine Menge von lg k aus den ni gebildeten Zahlenpaaren paarweise teilerfremd ist.
31.3
Modulare Arithmetik
Etwas vereinfacht gesagt, können wir uns die modulare Arithmetik als die gewöhnliche Arithmetik ganzer Zahlen vorstellen, mit dem Unterschied, dass, wenn wir mit modulo n rechnen, wir jedes Ergebnis x durch dasjenige Element aus {0, 1, . . . , n − 1} ersetzen, das äquivalent zu x modulo n ist (d. h. x wird durch x mod n ersetzt). Dieses informelle Modell reicht uns, wenn wir die Operationen Addition, Subtraktion und Multiplikation ausführen. Ein formaleres Konzept der modularen Arithmetik, welches wir nun einführen, kann am besten über die Gruppentheorie angegeben werden.
Endliche Gruppen Eine Gruppe (S, ⊕) ist eine Menge S mit einer auf ihr definierten binären Verknüpfung ⊕, die folgende Eigenschaften hat: 1. Abgeschlossenheit: Für alle a, b ∈ S ist a ⊕ b ein Element von S. 2. neutrales Element: Es existiert ein Element e ∈ S mit der Eigenschaft, für das e ⊕ a = a ⊕ e = a für alle a ∈ S gilt. Dieses Element wird neutrales Element der Gruppe genannt. 3. Assoziativität: Für jedes a, b, c ∈ S gilt (a ⊕ b) ⊕ c = a ⊕ (b ⊕ c). 4. Inverse: Für alle a ∈ S existiert ein eindeutig bestimmtes Element b ∈ S, für das a ⊕ b = b ⊕ a = e gilt. Dieses Element wird inverses Element von a (oder das Inverse von a) genannt. Als Beispiel betrachten wir die vertraute Gruppe (Z, +) der ganzen Zahlen Z mit der Addition als Verknüpfung: 0 ist das neutrale Element, und das inverse Element von a ist −a. Wenn eine Gruppe (S, ⊕) für alle a, b ∈ S das Kommutativgesetz a ⊕ b = b ⊕ a erfüllt, dann sprechen wir von einer abelschen (oder kommutative) Gruppe. Wenn eine Gruppe (S, ⊕) nur endlich viele Elemente enthält, d. h. |S| < ∞ gilt, so ist es eine endliche Gruppe.
31.3 Modulare Arithmetik
951
Die durch modulare Addition und Multiplikation definierten Gruppen Wir können zwei endliche abelsche Gruppen bilden, indem wir für eine positive ganze Zahl n die Addition und die Multiplikation modulo n als Operation nehmen. Diese Gruppen basieren auf den Äquivalenzklassen der ganzen Zahlen modulo n, die wir in Abschnitt 31.1 definiert haben. Um eine Gruppe auf Zn zu definieren, benötigen wir geeignete binäre Verknüpfungen, die wir durch Neudefinition der gewöhnlichen Operation für Addition und Multiplikation erhalten. Wir können sehr einfach Operationen für Addition und Multiplikation in Zn definieren, da die Äquivalenzklassen zweier ganzer Zahlen eindeutig die Äquivalenzklasse ihrer Summe bzw. ihres Produkts festlegen. Für a ≡ a (mod n) und b ≡ b (mod n) gilt a + b ≡ a + b a · b ≡ a · b
(mod n) , (mod n) .
Wir definieren daher die Addition und die Multiplikation modulo n, die wir mit +n und ·n bezeichnen, durch [a]n +n [b]n = [a + b]n ,
(31.18)
[a]n ·n [b]n = [a · b]n . (Wir können die Subtraktion auf Zn entsprechend durch [a]n −n [b]n = [a−b]n definieren, aber die Division ist komplizierter, wie wir sehen werden.) Diese Fakten rechtfertigen die übliche und bequeme Praxis, das kleinste nichtnegative Element jeder Äquivalenzklasse als dessen Repräsentanten zu verwenden, wenn man Berechnungen in Zn durchführt. Wir addieren, subtrahieren und multiplizieren die Repräsentanten der Klassen in der üblichen Art und Weise, ersetzen aber jedes Ergebnis x durch den Repräsentanten seiner Äquivalenzklasse, d. h. durch x mod n. Mit dieser Definition für die Addition modulo n definieren wir die additive Gruppe modulo n durch (Zn , +n ). Die Größe der additiven Gruppe modulo n ist |Zn | = n. Abbildung 31.2(a) zeigt die Operationstafel der Gruppe (Z6 , +6 ).
Theorem 31.12 Das System (Zn , +n ) ist eine endliche abelsche Gruppe.
Beweis: Gleichung (31.18) zeigt, dass (Zn , +n ) abgeschlossen ist. Assoziativität und Kommutativität von +n folgen aus der Assoziativität und Kommutativität der gewöhn-
952
31 Zahlentheoretische Algorithmen
+6
0
1
2
3
4
5
·15
1
0 1 2 3 4 5
0 1 2 3 4 5
1 2 3 4 5 0
2 3 4 5 0 1
3 4 5 0 1 2
4 5 0 1 2 3
5 0 1 2 3 4
1 2 4 7 8 11 13 14
1 2 4 7 8 11 13 14 2 4 8 14 1 7 11 13 4 8 1 13 2 14 7 11 7 14 13 4 11 2 1 8 8 1 2 11 4 13 14 7 11 7 14 2 13 1 8 4 13 11 7 1 14 8 4 2 14 13 11 8 7 4 2 1
2
(a)
4
7
8
11 13 14
(b)
Abbildung 31.2: Zwei endliche Gruppen. Die Äquivalenzklassen werden durch ihre Repräsentanten angegeben. (a) Die Gruppe (Z6 , +6 ). (b) Die Gruppe (Z∗15 , ·15 ).
lichen Addition +: ([a]n +n [b]n ) +n [c]n = [a + b]n +n [c]n = [(a + b) + c]n = [a + (b + c)]n = [a]n +n [b + c]n = [a]n +n ([b]n +n [c]n ) , [a]n +n [b]n = [a + b]n = [b + a]n = [b]n +n [a]n . Das neutrale Element von (Zn , +n ) ist 0 (d. h. [0]n ). Das (additive) Inverse eines Elementes a (d. h. von [a]n ) ist das Element −a (d. h. [−a]n oder [n − a]n ), denn es gilt [a]n +n [−a]n = [a − a]n = [0]n . Mit der Definition für die Multiplikation modulo n definieren wir die multiplikative Gruppe modulo n als (Z∗n , ·n ). Die Elemente von Z∗n sind diejenigen Elemente von Zn , die zu n teilerfremd sind, sodass jedes von ihnen ein eindeutiges Inverse modulo n besitzt: Z∗n = {[a]n ∈ Zn : ggT(a, n) = 1} . Um zu sehen, dass Z∗n wohldefiniert ist, müssen Sie bemerken, dass für 0 ≤ a < n die Gleichung a ≡ (a + kn) (mod n) für alle ganzen Zahlen k gilt. Gemäß Übung 31.2-3 folgt daher aus ggT(a, n) = 1 die Gleichung ggT(a + kn, n) = 1 für jede ganze Zahl
31.3 Modulare Arithmetik
953
k. Wegen [a]n = {a + kn : k ∈ Z} ist die Menge Z∗n wohldefiniert. Ein Beispiel für eine solche Gruppe ist Z∗15 = {1, 2, 4, 7, 8, 11, 13, 14} bei der die Gruppenoperation die Multiplikation modulo 15 ist. (Hier bezeichnen wir ein Element [a]15 mit a; beispielsweise bezeichnet 7 die Restklasse [7]15 ) Abbildung 31.2(b) zeigt die Gruppe (Z∗15 , ·15 ). In Z∗15 ist beispielsweise 8 · 11 ≡ 13 (mod 15). Das neutrale Element dieser Gruppe ist 1. Theorem 31.13 Das System (Z∗n , ·n ) ist eine endliche abelsche Gruppe. Beweis: Aus Theorem 31.6 folgt die Abgeschlossenheit von (Z∗n , ·n ). Die Assoziativität und die Kommutativität der Verknüpfung ·n können wie bei der Addition +n modulo n im Beweis des Theorems 31.12 bewiesen werden. Das neutrale Element ist [1]n . Um die Existenz der Inversen zu zeigen, sei a ein Element aus Z∗n und (d, x, y) das Tupel, das durch Extended-Euclid(a, n) zurückgegeben wird. Dann gilt wegen a ∈ Z∗n d = 1 und ax + ny = 1
(31.19)
oder äquivalent dazu ax ≡ 1
(mod n) .
Somit ist [x]n das multiplikative inverse Element von [a]n modulo n. Zudem behaupten wir, dass [x]n ∈ Z∗n gilt. Warum? Gleichung (31.19) sagt aus, dass die kleinste positive Linearkombination von x und n gleich 1 sein muss. Aus diesem Grunde impliziert Theorem 31.2, dass ggT(x, n) = 1 gilt. Wir stellen den Beweis, dass inverse Elemente jeweils eindeutig definiert sind, bis zu Korollar 31.26 zurück. Sei beispielweise a = 5 und n = 11. Dann gibt Extended-Euclid(a, n) (d, x, y) = (1, −2, 1) zurück, sodass 1 = 5 · (−2) + 11 · 1 gilt. Demzufolge ist [−2]11 (d. h., [9]11 ) das multiplikative Inverse von [5]11 . Wenn wir im verbleibenden Teil dieses Kapitels mit den Gruppen (Zn , +n ) und (Z∗n , ·n ) arbeiten, folgen wir der üblichen Praxis, die Äquivalenzklassen durch ihre Repräsentanten anzugeben und für die Verknüpfungen +n und ·n die gewöhnlichen arithmetischen Symbole + bzw. · (oder einfaches Konkatenieren, sodass ab = a · b) zu verwenden. Äquivalenzen modulo n können auch als Gleichungen in Zn interpretiert werden. Die folgenden beiden Aussagen sind beispielsweise äquivalent: ax ≡ b (mod n) , [a]n ·n [x]n = [b]n .
954
31 Zahlentheoretische Algorithmen
Als weitere Vereinfachung der Schreibweise werden wir für eine Gruppe (S, ⊕) manchmal einfach S schreiben, wenn aus dem Kontext ersichtlich ist, welche Verknüpfung gemeint ist. Wir schreiben also für die Gruppen (Zn , +n ) und (Z∗n , ·n ) kurz Zn bzw. Z∗n . Wir bezeichnen die (multiplikative) Inverse eines Elementes a mit (a−1 mod n) . Die Division in Z∗n ist durch die Gleichung a/b ≡ ab−1 (mod n) definiert. In Z∗15 gilt beispielsweise 7−1 ≡ 13 (mod 15), denn es ist 7 · 13 = 91 ≡ 1 (mod 15), sodass 4/7 ≡ 4 · 13 ≡ 7 (mod 15) gilt. Die Größe von Z∗n wird mit φ(n) bezeichnet. Diese Funktion, die als Eulersche φFunktion bekannt ist, erfüllt die Gleichung A 1 φ(n) = n 1− , (31.20) p p : p ist prim und p | n
sodass p über alle Primfaktoren von n läuft (einschließlich n selbst, falls n eine Primzahl ist). Wir werden diese Formel hier nicht beweisen. Intuitiv gesehen starten wir mit einer Liste der n Reste {0, 1, . . . , n − 1} und streichen dann für jede Primzahl p, die n teilt, alle Vielfachen von p aus der Liste. Da 3 und 5 Primfaktoren von 45 sind, gilt zum Beispiel 1 1 φ(45) = 45 1 − 1− 3 5 4 2 = 45 3 5 = 24 . Wenn p eine Primzahl ist, dann ist Z∗p = {1, 2, . . . , p − 1} und 1 φ(p) = p 1 − p =p−1 .
(31.21)
Wenn n eine zusammengesetzte Zahl ist, dann gilt φ(n) < n − 1, wenngleich gezeigt werden kann, dass n (31.22) φ(n) > γ 3 e ln ln n + ln ln n für n ≥ 3 gilt, wobei γ = 0.5772156649 . . . die Eulersche Konstante ist. Eine etwas einfachere (aber ungenauere) Schranke für n > 5 ist n . (31.23) φ(n) > 6 ln ln n Die untere Schranke (31.22) ist im Wesentlichen die best mögliche, da lim inf n→∞
gilt.
φ(n) = e−γ n/ ln ln n
(31.24)
31.3 Modulare Arithmetik
955
Untergruppen Ist (S, ⊕) eine Gruppe, S ⊆ S eine Teilmenge von S und (S , ⊕) ebenfalls eine Gruppe, dann ist (S , ⊕) eine Untergruppe von (S, ⊕). Beispielsweise bilden die geraden Zahlen eine Untergruppe der Gruppe der ganzen Zahlen bezüglich der Addition. Das folgende Theorem liefert ein nützliches Werkzeug zur Identifizierung von Untergruppen. Theorem 31.14: (Eine nichtleere abgeschlossene Teilmenge einer endlichen Gruppe ist eine Untergruppe.) Ist (S, ⊕) eine endliche Gruppe und S eine nichtleere Teilmenge von S, für die a ⊕ b ∈ S für alle a, b ∈ S gilt, dann ist (S , ⊕) eine Untergruppe von (S, ⊕). Beweis: Den Beweis sollen Sie in Übung 31.3-3 führen.
Die Menge {0, 2, 4, 6} beispielsweise bildet eine Untergruppe von Z8 , da sie nichtleer und abgeschlossen unter der Verknüpfung + (d. h. abgeschlossen unter +8 ) ist. Das folgende Theorem, das wir ohne Beweis angeben, liefert eine sehr nützliche Einschränkung in Bezug auf die Größe einer Untergruppe. Theorem 31.15: (Satz von Lagrange) Ist (S, ⊕) eine endliche Gruppe und (S , ⊕) eine Untergruppe von (S, ⊕), dann ist |S | ein Teiler von |S|. Eine Untergruppe S einer Gruppe S ist eine echte Untergruppe, falls S = S gilt. Wir werden das folgende Korollar während unserer Analyse des Miller-Rabin-Primzahltests in Abschnitt 31.8 anwenden. Korollar 31.16 Ist S eine echte Untergruppe einer endlichen Gruppe S, dann gilt |S | ≤ |S|/2.
Durch jeweils ein Element erzeugte Untergruppen Theorem 31.14 gibt uns eine einfache Möglichkeit, eine Untergruppe einer endlichen Gruppe (S, ⊕) zu erzeugen: Wählen Sie ein Element a und nehmen Sie alle Elemente, die aus a durch Anwendung der Gruppenoperation erzeugt werden können. Genauer gesagt, definieren Sie a(k) für k ≥ 1 durch a
(k)
=
k B i=1
a = a⊕ a⊕ ··· ⊕ a . DE F C k
956
31 Zahlentheoretische Algorithmen
Wenn wir zum Beispiel a = 2 aus der Gruppe Z6 nehmen, dann ist die Folge a(1) , a(2) , a(3) . . . gleich 2, 4, 0, 2, 4, 0, 2, 4, 0, . . . . In der Gruppe Zn gilt a(k) = ka mod n und in der Gruppe Z∗n gilt a(k) = ak mod n. Wir definieren die durch a erzeugte Untergruppe, die wir mit a oder (a, ⊕) bezeichnen, durch a = {a(k) : k ≥ 1} . Wir sagen, dass a die Untergruppe a erzeugt oder dass a ein erzeugendes Element (oder eine Erzeugende) von a ist. Da S endlich ist, ist a eine endliche Teilmenge von S, die möglicherweise alle Elemente von S enthält. Aus der Assoziativität von ⊕ folgt a(i) ⊕ a(j) = a(i+j) , sodass a abgeschlossen ist. Nach Theorem 31.14 ist a somit eine Untergruppe von S. In Z6 haben wir beispielsweise 0 = {0} , 1 = {0, 1, 2, 3, 4, 5} , 2 = {0, 2, 4} und in Z∗7 1 = {1} , 2 = {1, 2, 4} , 3 = {1, 2, 3, 4, 5, 6} . Der Rang von a (in der Gruppe S), den wir mit rang(a) bezeichnen, ist als die kleinste positive ganze Zahl t definiert, für die a(t) = e gilt. Theorem 31.17 Für jede endliche Gruppe (S, ⊕) und jedes a ∈ S ist der Rang von a gleich der Größe der von ihm erzeugten Untergruppe, also rang(a) = |a|. Beweis: Sei t = rang(a). Wegen a(t) = e und a(t+k) = a(t) ⊕ a(k) = a(k) für k ≥ 1 gilt a(i) = a(j) für ein j < i, falls i > t ist. Wenn wir also Elemente von a aus generieren, so begegnen wir nach a(t) keine neuen Elemente mehr. Somit gilt a = {a(1) , a(2) , . . . , a(t) } und |a| ≤ t. Um zu zeigen, dass |a| ≥ t gilt, zeigen wir, dass die Elemente der Folge a(1) , a(2) , . . . , a(t) paarweise verschieden sind. Nehmen Sie zum Zwecke des Widerspruchs an, dass a(i) = a(j) für ein Paar i, j mit 1 ≤ i < j ≤ t gelten würde. Dann ist a(i+k) = a(j+k) für k ≥ 0. Daraus folgt aber a(i+(t−j)) = a(j+(t−j)) = e, was ein Widerspruch ist, denn es gilt i + (t − j) < t, aber t ist der kleinste positive Wert, für den a(t) = e gilt. Daher sind die Elemente der Folge a(1) , a(2) , . . . , a(t) paarweise verschieden und es gilt |a| ≥ t. Daraus folgt rang(a) = |a|.
31.4 Lösen modularer linearer Gleichungen
957
Korollar 31.18 Die Folge a(1) , a(2) , . . . ist periodisch mit der Periode t = rang(a); d. h. es gilt a(i) = a(j) genau dann, wenn i ≡ j (mod t) gilt. Wir definieren in Übereinstimmung mit dem obigen Korollar a(0) als e und a(i) als a(i mod t) mit t = rang(a) für jede ganze Zahl i. Korollar 31.19 Ist (S, ⊕) eine endliche Gruppe mit dem neutralen Element e, dann gilt für alle a ∈ S a(|S|) = e .
Beweis: Aus dem Satz von Lagrange (Theorem 31.15) folgt, dass der Rang rang(a) von a ein Teiler von |S| ist, d. h. rang(a) | |S|, und somit gilt |S| ≡ 0 (mod t) mit t = rang(a). Daher gilt a(|S|) = a(0) = e.
Übungen 31.3-1 Geben Sie die Operationstafeln für die Gruppen (Z4 , +4 ) und (Z∗5 , ·5 ) an. Zeigen Sie, dass diese Gruppen isomorph sind, indem Sie eine eineindeutige Zuordnung α zwischen ihren Elementen herstellen, sodass a + b ≡ c (mod 4) genau dann gilt, wenn α(a) · α(b) ≡ α(c) (mod 5) gilt. 31.3-2 Geben Sie alle Untergruppen von Z9 und Z∗13 an. 31.3-3 Beweisen Sie Theorem 31.14. 31.3-4 Zeigen Sie, dass für eine Primzahl p und eine positive ganze Zahl e φ(pe ) = pe−1 (p − 1) gilt. 31.3-5 Zeigen Sie, dass für jede ganze Zahl n > 1 und jedes a ∈ Z∗n die durch fa (x) = ax mod n definierte Funktion fa : Z∗n → Z∗n eine Permutation von Z∗n ist.
31.4
Lösen modularer linearer Gleichungen
Wir betrachten nun das Problem, die Lösungen der Gleichung ax ≡ b
(mod n)
(31.25)
958
31 Zahlentheoretische Algorithmen
mit a > 0 und n > 0 zu bestimmen. Dieses Problem hat mehrere Anwendungen; beispielsweise werden wir es in Abschnitt 31.7 als Teil der Prozedur zur Bestimmung der Schlüssel im RSA-Verschlüsselungssystem begegnen. Wir setzen voraus, dass a, b und n gegeben sind und wir alle Werte von x modulo n bestimmen wollen, die die Gleichung (31.25) erfüllen. Die Gleichung kann keine, eine oder mehr als eine Lösung haben. Sei a die von a erzeugte Untergruppe von Zn . Wegen a = {a(x) : x > 0} = {ax mod n : x > 0} besitzt die Gleichung (31.25) genau dann eine Lösung, wenn b ∈ a gilt. Der Satz von Lagrange (Theorem 31.15) sagt uns, dass |a| ein Teiler von n sein muss. Das folgende Theorem liefert eine genaue Charakterisierung von a. Theorem 31.20 Für alle positiven ganzen Zahlen a und n gilt für d = ggT(a, n) in Zn a = d = {0, d, 2d, . . . , ((n/d) − 1)d}
(31.26)
und daher |a| = n/d. Beweis: Wir zeigen zunächst, dass d ∈ a gilt. Rufen Sie sich in Erinnerung, dass Prozedur Extended-Euclid(a, n) ganze Zahlen x und y erzeugt, sodass ax +ny = d gilt. Es gilt also ax ≡ d (mod n) und damit d ∈ a. In anderen Worten, d ist ein Vielfaches von a in Zn . Da d ∈ a gilt, gehört jedes Vielfache von d zur Restklasse a, weil jedes Vielfache eines Vielfachen von a selbst ein Vielfaches von a ist. Demzufolge enthält a alle Elemente aus {0, d, 2d, . . . , ((n/d) − 1)d} und es gilt d ⊆ a. Wir zeigen nun, dass auch a ⊆ d gilt. Für m ∈ a gilt m = ax mod n für eine ganze Zahl x und damit m = ax + ny für eine ganze Zahl y. Nach Gleichung (31.4) gilt aber d | a und d | n und somit auch d | m. Demnach haben wir m ∈ d. Kombinieren wir diese Ergebnisse, so erhalten wir a = d. Um zu sehen, dass |a| = n/d gilt, müssen Sie bemerken, dass es genau n/d Vielfache von d zwischen 0 und n − 1 (inklusive) gibt.
Korollar 31.21 Die Gleichung ax ≡ b (mod n) ist genau dann nach der Unbekannten x auflösbar, wenn d | b für d = ggT(a, n). Beweis: Die Gleichung ax ≡ b (mod n) ist genau dann lösbar, wenn [b] ∈ a, was wegen Theorem 31.20 das Gleiche ist wie (b mod n) ∈ {0, d, 2d, . . . , ((n/d) − 1)d} .
31.4 Lösen modularer linearer Gleichungen
959
Ist 0 ≤ b < n, so gilt b ∈ a genau dann, wenn d | b, weil die Elemente von a genau die Vielfachen von d sind. Ist b < 0 oder b ≥ n, so folgt das Korollar aus der Beobachtung, dass d | b genau dann gilt, wenn d | (b mod n), weil b und b mod n sich in einem Vielfachen von n unterscheiden, wobei n selbst ein Vielfaches von d ist.
Korollar 31.22 Die Gleichung ax ≡ b (mod n) hat entweder d verschiedene Lösungen modulo n, mit d = ggT(a, n), oder überhaupt keine Lösung. Beweis: Falls ax ≡ b (mod n) eine Lösung hat, dann gilt b ∈ a. Nach Theorem 31.17 ist rang(a) = |a|, sodass aus Korollar 31.18 und Theorem 31.20 folgt, dass die Folge ai mod n mit i = 0, 1, . . . periodisch mit der Periode |a| = n/d ist. Ist b ∈ a, so erscheint b genau d-mal in der Folge ai mod n mit i = 0, 1, . . . , n − 1, da der Block der Länge n/d der Werte a genau d-mal wiederholt wird, wenn i von 0 bis n − 1 läuft. Die Indizes x der d Stellen, für die ax mod n = b gilt, sind die Lösungen der Gleichung ax ≡ b (mod n).
Theorem 31.23 Sei d = ggT(a, n) und setzen Sie voraus, dass d = ax + ny für zwei ganze Zahlen x und y ist (die zum Beispiel durch den Algorithmus Extended-Euclid berechnet wurden). Ist d | b, so hat eine der Lösungen der Gleichung ax ≡ b (mod n) den Wert x0 mit x0 = x (b/d) mod n. Beweis: Es ist ax0 ≡ ax (b/d) ≡ d(b/d) ≡ b
(mod n) (mod n) (mod n)
(wegen ax ≡ d (mod n))
sodass x0 eine Lösung von ax ≡ b (mod n) ist.
Theorem 31.24 Setzen Sie voraus, dass die Gleichung ax ≡ b (mod n) lösbar ist, d. h. d | b mit d = ggT(a, n), und x0 eine Lösung dieser Gleichung ist. Dann besitzt diese Gleichung genau d verschiedene Lösungen, modulo n, die durch xi = x0 + i(n/d) für i = 0, 1, . . . , d − 1 gegeben sind. Beweis: Wegen n/d > 0 und 0 ≤ i(n/d) < n für i = 0, 1, . . . , d − 1 sind die Werte x0 , x1 , . . . , xd−1 paarweise verschieden modulo n. Da x0 eine Lösung von ax ≡ b
960
31 Zahlentheoretische Algorithmen
(mod n) ist, gilt ax0 mod n ≡ b (mod n). Daraus folgt für i = 0, 1, . . . , d − 1 axi mod n = a(x0 + in/d) mod n = (ax0 + ain/d) mod n = ax0 mod n (da d | a impliziert, dass ain/d ein Vielfaches von n ist) ≡ b (mod n) , und somit axi ≡ b (mod n), sodass xi auch eine Lösung ist. Wegen Korollar 31.22 hat die Gleichung ax ≡ b (mod n) genau d Lösungen, sodass x0 , x1 , . . . , xd−1 alle diese Lösungen sein müssen. Wir haben nun die nötigen mathematischen Grundlagen entwickelt, um die Gleichung ax ≡ b (mod n) zu lösen. Der folgende Algorithmus druckt alle Lösungen dieser Gleichung. Die Eingabewerte a und n sind beliebige positive ganze Zahlen und b ist eine beliebige ganze Zahl. Modular-Linear-Equation-Solver(a, b, n) 1 (d, x , y ) = Extended-Euclid(a, n) 2 if d | b 3 x0 = x (b/d) mod n 4 for i = 0 to d − 1 5 print (x0 + i(n/d)) mod n 6 else print “es gibt keine Lösungen” Lassen Sie uns als Beispiel die Gleichung 14x ≡ 30 (mod 100) anschauen, um die Arbeitsweise dieser Prozedur zu betrachten (hier ist also a = 14, b = 30 und n = 100). Durch den Aufruf Extended-Euclid in Zeile 1 erhalten wir (d, x , y ) = (2, −7, 1). Da 2 ein Teiler von 30 ist, kommen die Zeilen 3–5 zur Ausführung. Zeile 3 berechnet x0 = (−7)(15) mod 100 = 95. Die Schleife in den Zeilen 4–5 gibt die beiden Lösungen 95 und 45 aus. Die Prozedur Modular-Linear-Equation-Solver arbeitet folgendermaßen. Zeile 1 berechnet d = ggT(a, n) sowie die beiden Werte x und y so, dass d = ax + ny gilt, und zeigt, dass x eine Lösung der Gleichung ax ≡ d (mod n) ist. Ist d kein Teiler von b, so hat die Gleichung ax ≡ b (mod n) nach Korollar 31.21 keine Lösung. Zeile 2 prüft, ob d ein Teiler von b ist; falls nicht, meldet Zeile 6, dass es keine Lösungen gibt. Anderenfalls berechnet Zeile 3 eine Lösung x0 der Gleichung ax ≡ b (mod n) gemäß Theorem 31.23. Ist eine Lösung x0 gegeben, so können nach Theorem 31.24 die übrigen d − 1 Lösungen durch Addition von Vielfachen von (n/d) modulo n gebildet werden. Die for-Schleife der Zeilen 4–5 gibt alle d Lösungen beginnend mit x0 aus, wobei zwei aufeinander folgende Lösungen jeweils den Abstand (n/d) modulo n haben. Die Prozedur Modular-Linear-Equation-Solver führt O(lg n+ggT(a, n)) arithmetische Operationen aus, da Extended-Euclid O(lg n) arithmetische Operationen und
31.4 Lösen modularer linearer Gleichungen
961
jede Iteration der for-Schleife in den Zeilen 4–5 eine konstante Anzahl arithmetischer Operationen ausführt. Die folgenden Korollare zu Theorem 31.24 geben Spezialfälle an, die von besonderem Interesse sind. Korollar 31.25 Für jedes n > 1 mit ggT(a, n) = 1 hat die Gleichung ax ≡ b (mod n) eine eindeutige Lösung modulo n. Ist b = 1, was ein häufig vorkommender Fall von beträchtlichem Interesse ist, ist das x, nach dem wir suchen, eine multiplikative Inverse von a modulo n. Korollar 31.26 Sei n > 1. Ist ggT(a, n) = 1, so hat die Gleichung ax ≡ 1 (mod n) eine eindeutige Lösung modulo n. Anderenfalls hat die Gleichung keine Lösung. Dank Korollar 31.26, können wir die Notation (a−1 mod n) benutzen, um die multiplikative Inverse von a modulo n zu bezeichnen, wenn a und n teilerfremd sind. Ist ggT(a, n) = 1, so ist die eindeutige Lösung der Gleichung ax ≡ 1 (mod n) die ganze Zahl x, die von der Prozedur Extended-Euclid zurückgegeben wird, denn aus der Gleichung ggT(a, n) = 1 = ax + ny folgt ax ≡ 1 (mod n). Wir können also (a−1 mod n) effizient berechnen, indem wir Extended-Euclid benutzen.
Übungen 31.4-1 Bestimmen Sie alle Lösungen der Gleichung 35x ≡ 10 (mod 50). 31.4-2 Beweisen Sie, dass, wenn ggT(a, n) = 1 gilt, aus der Gleichung ax ≡ ay (mod n) die Gleichung x ≡ y (mod n) folgt. Zeigen Sie, dass die Bedingung ggT(a, n) = 1 notwendig ist, indem Sie für den Fall ggT(a, n) > 1 ein entsprechendes Gegenbeispiel angeben. 31.4-3 Betrachten Sie folgende Änderung in Zeile 3 der Prozedur Modular-LinearEquation-Solver: 3
x0 = x (b/d) mod (n/d)
Arbeitet die so modifizierte Prozedur korrekt? Erklären Sie warum bzw. warum nicht.
962
31 Zahlentheoretische Algorithmen
31.4-4∗Sei p eine Primzahl und f (x) ≡ f0 + f1 x + · · ·+ ft xt (mod p) ein Polynom vom Grad t, dessen Koeffizienten fi aus der Menge Zp sind. Wir sagen, dass a ∈ Zp eine Nullstelle von f ist, falls f (a) ≡ 0 (mod p) gilt. Beweisen Sie, dass f (x) ≡ (x − a)g(x) (mod p) für ein Polynom g(x) vom Grad t − 1 gilt, falls a eine Nullstelle von f ist. Beweisen Sie durch Induktion nach t, dass, wenn p eine Primzahl ist, ein Polynom f (x) vom Grad t höchstens t verschiedene Nullstellen modulo p haben kann.
31.5
Der chinesische Restsatz
Um 100 n. Chr. löste der chinesische Mathematiker Sun-Ts˘ u das Problem, diejenigen ganzen Zahlen x zu finden, die bei der Division durch 3, 5 bzw. 7 den Rest 2, 3 bzw. 2 haben. Eine solche Lösung ist x = 23. Alle Lösungen haben die Form 23 + 105k, wobei k eine beliebige ganze Zahl ist. Der „chinesische Restsatz“ liefert einen Zusammenhang zwischen einem System von Gleichungen modulo einer Menge von paarweise teilerfremden Moduli (beispielsweise 3, 5 und 7) und einer Gleichung modulo ihres Produktes (beispielsweise 105). Der chinesische Restsatz hat zwei Hauptanwendungen. Sei die Zahl n faktorisierbar in n = n1 n2 · · · nk , wobei die Faktoren ni paarweise teilerfremd sind. Zum einen ist der chinesische Restsatz ein beschreibendes „Strukturtheorem“, das besagt, dass die Struktur von Zn identisch ist mit der des kartesischen Produktes Zn1 × Zn2 × · · · × Znk mit komponentenweiser Addition und Multiplikation modulo ni in der i-ten Komponente. Zweitens hilft uns diese Beschreibung, um effiziente Algorithmen zu entwerfen, da das Arbeiten in jedem der Systeme Zni effizienter (im Sinne von Bitoperationen) sein kann als das Arbeiten modulo n. Theorem 31.27: (Chinesischer Restsatz) Sei n = n1 n2 · · · nk , wobei die ni paarweise teilerfremd sind. Betrachten Sie die Zuordnung a ↔ (a1 , a2 , . . . , ak ) ,
(31.27)
mit a ∈ Zn , ai ∈ Zni und ai = a mod ni für i = 1, 2, . . . , k. Dann ist die Abbildung (31.27) eine eineindeutige Zuordnung (Bijektion) zwischen Zn und dem kartesischen Produkt Zn1 × Zn2 × · · · × Znk . Die auf den Elementen von Zn ausgeführten Operationen können äquivalent auf den zugehörigen k-Tupeln ausgeführt werden, indem die Operationen unabhängig von einander auf jeder Koordinatenposition im jeweils entsprechenden System angewendet werden. Das heißt, ist a ↔ (a1 , a2 , . . . , ak ) , b ↔ (b1 , b2 , . . . , bk ) ,
31.5 Der chinesische Restsatz
963
so ist (a + b) mod n ↔ ((a1 + b1 ) mod n1 , . . . , (ak + bk ) mod nk ) , (a − b) mod n ↔ ((a1 − b1 ) mod n1 , . . . , (ak − bk ) mod nk ) , (ab) mod n ↔ (a1 b1 mod n1 , . . . , ak bk mod nk ) .
(31.28) (31.29) (31.30)
Beweis: Die Transformation zwischen den beiden Darstellungen ist recht unkompliziert. Der Übergang von a zu (a1 , a2 , . . . , ak ) ist einfach und erfordert nur k “mod”Operationen. Die Berechnung von a aus den Eingabewerten (a1 , a2 , . . . , ak ) ist ein klein wenig komplizierter. Wir definieren zunächst mi = n/ni für i = 1, 2, . . . , k, d. h. mi ist das Produkt aller nj ohne ni : mi = n1 n2 · · · ni−1 ni+1 · · · nk . Als nächstes definieren wir mod ni ) ci = mi (m−1 i
(31.31)
für i = 1, 2, . . . , k. Gleichung (31.31) ist immer wohldefiniert: Da mi und ni teilermod ni ) existiert. fremd sind (nach Theorem 31.6), garantiert Korollar 31.26, dass (m−1 i Schließlich können wir a als Funktion von a1 , a2 , . . . , ak wie folgt berechnen: a ≡ (a1 c1 + a2 c2 + · · · + ak ck )
(mod n) .
(31.32)
Wir zeigen nun, dass Gleichung (31.32) sicherstellt, dass a ≡ ai (mod ni ) für i = 1, 2, . . . , k gilt. Beachten Sie, dass, wenn j = i ist, mj ≡ 0 (mod ni ) gilt, woraus cj ≡ mj ≡ 0 (mod ni ) folgt. Außerdem gilt wegen Gleichung (31.31) ci ≡ 1 (mod ni ). Uns liegt also die eingängige und nützliche Zuordnung ci ↔ (0, 0, . . . , 0, 1, 0, . . . , 0) , vor, ein Vektor, in dem überall Nullen stehen, außer auf der i-ten Position, die auf 1 gesetzt ist. Die ci bilden also in gewissem Sinne eine „Basis“ für die Darstellung. Für alle i gilt daher a ≡ ai ci ai mi (m−1 i
≡ ≡ ai
(mod ni ) mod ni ) (mod ni ) (mod ni ) ,
was genau das ist, was wir zeigen wollten: Unsere Methode der Berechnung von a aus den ai erzeugt ein Ergebnis a, das die Bedingungen a ≡ ai (mod ni ) für i = 1, 2, . . . , k erfüllt. Die Zuordnung ist eineindeutig, da wir in beide Richtungen transformieren können. Schließlich folgen die Gleichungen (31.28)–(31.30) direkt aus der Übung 31.1-7, da x mod ni = (x mod n) mod ni für jedes x und i = 1, 2, . . . , k gilt. Wir werden die folgenden Korollare weiter hinten in diesem Kapitel benötigen. Korollar 31.28 Sind die Zahlen n1 , n2 , . . . , nk paarweise teilerfremd und gilt n = n1 n2 · · · nk , dann hat für beliebige ganzen Zahlen a1 , a2 , . . . , ak die Menge der simultanen Gleichungen x ≡ ai
(mod ni ) ,
i = 1, 2, . . . , k, eine eindeutige Lösung modulo n für die Unbekannte x.
964
31 Zahlentheoretische Algorithmen 0 0 26 52 13 39
0 1 2 3 4
1 40 1 27 53 14
2 15 41 2 28 54
3 55 16 42 3 29
4 30 56 17 43 4
5 5 31 57 18 44
6 45 6 32 58 19
7 20 46 7 33 59
8 60 21 47 8 34
9 35 61 22 48 9
10 10 36 62 23 49
11 50 11 37 63 24
12 25 51 12 38 64
Abbildung 31.3: Eine Illustration des chinesischen Restsatzes für n1 = 5 und n2 = 13. In diesem Beispiel ist c1 = 26 und c2 = 40. Der Wert in Zeile i und Spalte j ist a modulo 65, sodass a mod 5 = i und a mod 13 = j gilt. Beachten Sie, dass der Eintrag in Zeile 0, Spalte 0 gleich 0 ist. Entsprechend enthält Zeile 4, Spalte 12 eine 64 (äquivalent zu −1). Da c1 gleich 26 ist, erhöht sich a um 26, wenn wir uns in einer Spalte nach unten bewegen. Analog dazu bedeutet c2 = 40, dass sich a um 40 erhöht, wenn wir eine Spalte nach rechts gehen. Das Erhöhen von a um 1 entspricht der diagonalen Bewegung nach rechts unten. Wenn wir in der letzten Zeile angekommen sind, fahren wir in der ersten Zeile, um eins nach rechts verschoben, fort. Entsprechend verfahren wir, wenn wir die letzte Spalte erreicht haben.
Korollar 31.29 Sind die Zahlen n1 , n2 , . . . , nk paarweise teilerfremd und gilt n = n1 n2 · · · nk , dann gilt für jede ganze Zahl x und jede ganze Zahl a x≡a
(mod ni ) ,
i = 1, 2, . . . , k, genau dann, wenn x≡a
(mod n)
gilt. Lassen Sie uns als Beispiel für die Anwendung des chinesischen Restsatzes die beiden Gleichungen a≡2 a≡3
(mod 5) , (mod 13) ,
betrachten. Es gilt also a1 = 2, n1 = m2 = 5, a2 = 3 sowie n2 = m1 = 13 und wir wollen a mod 65 berechnen, da n = n1 n2 = 65. Wegen 13−1 ≡ 2 (mod 5) und 5−1 ≡ 8 (mod 13) gilt c1 = 13(2 mod 5) = 26 , c2 = 5(8 mod 13) = 40 und a ≡ 2 · 26 + 3 · 40 ≡ 52 + 120 ≡ 42
(mod 65) (mod 65) (mod 65) .
31.6 Potenzen eines Elements
965
Abbildung 31.3 illustriert den chinesischen Restsatz, modulo 65. Wir können also modulo n bearbeiten, indem wir direkt in modulo n rechnen oder indem wir in der transformierten Darstellung unter Verwendung separater modulo ni Berechnungen rechnen – so wie es gerade am bequemsten ist. Diese Berechnungen sind vollständig äquivalent zueinander.
Übungen 31.5-1 Bestimmen Sie alle Lösungen der Gleichungen x ≡ 4 (mod 5) und x ≡ 5 (mod 11). 31.5-2 Bestimmen Sie alle ganzen Zahlen x, die bei Division durch 9, 8 und 7 den Rest 1, 2 bzw. 3 haben. 31.5-3 Zeigen Sie, dass mit den in Theorem 31.27 gemachten Definitionen −1 −1 (a−1 mod n) ↔ ((a−1 1 mod n1 ), (a2 mod n2 ), . . . , (ak mod nk ))
gilt, wenn ggT(a, n) = 1 ist. 31.5-4 Beweisen Sie unter Verwendung der in Theorem 31.27 gemachten Definitionen, dass für jedes Polynom f die Anzahl der Wurzeln der Gleichung f (x) ≡ 0 (mod n) gleich dem Produkt der Anzahl der Wurzeln der Gleichungen f (x) ≡ 0 (mod n1 ), f (x) ≡ 0 (mod n2 ), . . . , f (x) ≡ 0 (mod nk ) ist.
31.6
Potenzen eines Elements
So häufig wir die Vielfachen eines gegebenen Elements a modulo n betrachten, so häufig betrachten wir die Folge der Potenzen von a modulo n mit a ∈ Z∗n : a0 , a1 , a2 , a3 , . . . ,
(31.33)
modulo n. Wenn wir mit 0 beginnend indizieren, ist der 0-te Wert dieser Folge a0 mod n = 1 und der i-te Wert ai mod n. Die Potenzen von 3 modulo 7 beispielsweise sind 8 2
9 6
10 4
11 5
··· ···
während die Potenzen von 2 modulo 7 durch i 0 1 2 3 4 5 6 7 8 i 2 mod 7 1 2 4 1 2 4 1 2 4
9 1
10 2
11 4
··· ···
i 3i mod 7
0 1
1 3
2 2
3 6
4 4
5 5
6 1
7 3
gegeben sind. In diesem Abschnitt bezeichnen wir mit a die durch a erzeugte Untergruppe von Z∗n , die durch wiederholte Multiplikation entsteht, und mit rangn (a) („Rang von a modulo n“) den Rang von a in Z∗n . In Z∗7 beispielsweise gilt 2 = {1, 2, 4} und rang7 (2) =
966
31 Zahlentheoretische Algorithmen
3. Unter Verwendung der Definition der Eulerschen φ-Funktion φ(n) für die Größe von Z∗n (siehe Abschnitt 31.3) übertragen wir nun das Korollar 31.19 in die Notation von Z∗n , um das Eulersche Theorem zu erhalten, und spezialisieren es auf den Fall Z∗p für eine Primzahl p, um den Satz von Fermat zu erhalten.
Theorem 31.30: (Satz von Euler) Für jede ganze Zahl n > 1 gilt aφ(n) ≡ 1 (mod n)
für alle a ∈ Z∗n .
Theorem 31.31: (Satz von Fermat) Ist p eine Primzahl, so gilt ap−1 ≡ 1
(mod p)
für alle a ∈ Z∗p .
Beweis: Nach Gleichung (31.21) ist φ(p) = p − 1, falls p eine Primzahl ist.
Fermats Satz lässt sich auf jedes Element von Zp außer der 0 anwenden, da 0 kein Element von Z∗p ist. Für alle a ∈ Zp gilt jedoch ap ≡ a (mod p), wenn p eine Primzahl ist. Ist rangn (g) = |Z∗n |, so ist jedes Element von Z∗n eine Potenz von g modulo n und g ist eine primitive Wurzel oder eine Erzeugende von Z∗n . Beispielsweise ist 3 eine primitive Wurzel modulo 7, 2 jedoch nicht. Besitzt Z∗n eine primitive Wurzel, so ist die Gruppe Z∗n zyklisch. Wir geben das folgende Theorem, das durch Niven und Zuckerman [265] bewiesen wurde, ohne Beweis an.
Theorem 31.32 Die Werte von n > 1, für die Z∗n zyklisch ist, sind 2, 4, pe und 2pe für alle Primzahlen p > 2 und alle positiven ganzen Zahlen e. Ist g eine primitive Wurzel von Z∗n und a ein beliebiges Element von Z∗n , dann existiert ein z, für das g z ≡ a (mod n) gilt. Dieser Wert z ist ein diskreter Logarithmus oder Index von a modulo n zur Basis g. Wir bezeichnen diesen Wert mit indn,g (a). Theorem 31.33: (Satz des diskreten Logarithmus) Ist g eine primitive Wurzel von Z∗n , dann gilt die Gleichung g x ≡ g y (mod n) genau dann, wenn die Gleichung x ≡ y (mod φ(n)) gilt.
31.6 Potenzen eines Elements
967
Beweis: Setzen Sie zunächst x ≡ y (mod φ(n)) voraus. Dann gilt x = y + kφ(n) für eine ganze Zahl k und somit g x ≡ g y+kφ(n) ≡ g · (g y
≡ g ·1 ≡ gy y
(mod n)
φ(n) k
)
k
(mod n) (mod n) (mod n) .
(nach dem Satz von Euler)
Um die andere Richtung zu zeigen, setzen Sie g x ≡ g y (mod n) voraus. Da die Folge der Potenzen von g jedes Element von g enthält und |g| = φ(n) gilt, folgt aus Korollar 31.18, dass die Folge der Potenzen von g periodisch mit der Periode φ(n) ist. Deshalb muss im Falle g x ≡ g y (mod n) die Gleichung x ≡ y (mod φ(n)) gelten. Wir werfen nun unseren Blick auf die Quadratwurzel der 1 modulo einer Potenz einer Primzahl. Das folgende Theorem wird bei der Entwicklung eines Primzahltests in Abschnitt 31.8 hilfreich sein.
Theorem 31.34 Ist p eine ungerade Primzahl und gilt e ≥ 1, dann besitzt die Gleichung x2 ≡ 1 (mod pe )
(31.34)
nur zwei Lösungen, nämlich x = 1 und x = −1.
Beweis: Gleichung (31.34) ist äquvalent zu pe | (x − 1)(x + 1) . Da p > 2, kann p | (x − 1) oder p | (x + 1) gelten, aber nicht beides. (Anderenfalls würde p auch ihre Differenz (x + 1) − (x − 1) = 2 wegen Eigenschaft (31.3) teilen. Ist p | (x − 1), so gilt ggT(pe , x − 1) = 1 und wegen Korollar 31.5 hätten wir pe | (x + 1), d. h. x ≡ −1 (mod pe ). Ist p | (x + 1), dann gilt ggT(pe , x + 1) = 1 und Korrollar 31.5 impliziert pe | (x−1), sodass x ≡ 1 (mod pe ) gilt. Somit gilt entweder x ≡ −1 (mod pe ) oder x ≡ 1 (mod pe ). Eine Zahl x ist eine nichttriviale Quadratwurzel von 1 modulo n, wenn sie die Gleichung x2 ≡ 1 (mod n) erfüllt, aber zu keiner der beiden „trivialen“ Quadratwurzeln 1 und −1 modulo n äquivalent ist. Beispielsweise ist 6 eine nichttriviale Quadratwurzel von 1 modulo 35. Wir werden das folgende Korollar zu Theorem 31.34 im Korrektheitsbeweis des Miller-Rabin-Primzahltests in Abschnitt 31.8 anwenden.
968
31 Zahlentheoretische Algorithmen
Korollar 31.35 Wenn eine nichttriviale Quadratwurzel von 1 modulo n existiert, dann ist n eine zusammengesetzte Zahl. Beweis: Nach Theorem 31.34 kann n keine ungerade Primzahl oder eine Potenz einer ungeraden Primzahl sein, wenn es eine nichttriviale Quadratwurzel von 1 modulo n gibt. Ist x2 ≡ 1 (mod 2), so folgt x ≡ 1 (mod 2) und alle Quadratwurzeln von 1 modulo 2 sind trivial. Also kann n keine Primzahl sein. Schließlich muss n > 1 gelten, damit eine nichttriviale Quadratwurzel von 1 existiert. Folglich ist n eine zusammengesetzte Zahl.
Potenzieren durch wiederholtes Quadrieren Eine häufig vorkommende Operation in der Zahlentheorie ist das Erheben einer Zahl in eine Potenz modulo einer anderen Zahl, was auch als modulares Potenzieren bekannt ist. Genauer gesagt, suchen wir nach einem effizienten Weg zur Berechnung von ab mod n für nichtnegative ganze Zahlen a, b und eine positive ganze Zahl n. Das modulare Potenzieren ist eine wesentliche Operation in vielen Prozeduren zum Primzahltest und für die RSA-Verschlüsselung. Die Methode des wiederholten Quadrierens löst dieses Problem unter Verwendung der Binärdarstellung von b effizient. Sei bk , bk−1 , . . . , b1 , b0 die Binärdarstellung von b. (Die Binärdarstellung besteht also aus k + 1 Bit, wobei bk das höchst- und b0 das niederwertigste Bit ist.) Die folgende Prozedur berechnet ac mod n, wobei c durch Verdoppelungen und Inkrementieren von 0 auf b erhöht wird. Modular-Exponentiation(a, b, n) 1 c=0 2 d=1 3 sei bk , bk−1 , . . . , b0 die Binärdarstellung von b 4 for i = k downto 0 5 c = 2c 6 d = (d · d) mod n 7 if bi == 1 8 c = c+1 9 d = (d · a) mod n 10 return d Das Quadrieren in jeder Iteration (Zeile 6) erklärt den Namen „wiederholtes Quadrieren“. Beispielsweise berechnet der Algorithmus für a = 7, b = 560 und n = 561 die in Abbildung 31.4 gezeigte Folge von Werten modulo 561. Die Folge der verwendeten Exponenten ist in der mit c bezeichneten Tabellenzeile angegeben.
31.6 Potenzen eines Elements i bi c d
9 1 1 7
8 0 2 49
7 0 4 157
6 0 8 526
5 1 17 160
969 4 1 35 241
3 0 70 298
2 0 140 166
1 0 280 67
0 0 560 1
Abbildung 31.4: Die Ergebnisse von Modular-Exponentiation bei der Berechnung von ab (mod n) mit a = 7, b = 560 = 1000110000 und n = 561. Die Werte sind nach jeder Ausführung der for-Schleife jeweils dargestellt. Das Endergebnis ist 1.
Die Variable c wird von dem Algorithmus nicht wirklich benötigt, sondern wird nur innerhalb der folgenden zweiteiligen Schleifeninvariante verwendet: Unmittelbar vor jeder Iteration der for-Schleife in den Zeilen 4–9 gilt: 1. Der Wert von c ist gleich dem Präfix bk , bk−1 , . . . , bi+1 der Binärdarstellung von b 2. d = ac mod n. Wir beweisen diese Schleifeninvariante wie folgt: Initialisierung: Zu Beginn gilt i = k, sodass der Präfix bk , bk−1 , . . . , bi+1 leer ist, was c = 0 entspricht. Außerdem gilt d = 1 = a0 mod n. Fortsetzung: Seien c und d die Werte von c und d am Ende einer Iteration der forSchleife und somit die Werte zu Beginn der nächsten Iteration. Jede Iteration aktualisiert c = 2c (im Falle bi = 0) oder c = 2c + 1 (im Falle bi = 1), sodass c zu Beginn der nächsten Iteration korrekt ist. Ist bi = 0, so gilt d = d2 mod n = (ac )2 mod n = a2c mod n = ac mod n. Ist bi = 1, so gilt d = d2 a mod n = (ac )2 a mod n = a2c+1 mod n = ac mod n. In beiden Fällen ist vor der nächsten Iteration d = ac mod n. Terminierung: Bei der Terminierung ist i = −1. Damit gilt c = b, da c den Wert des Präfix bk , bk−1 , . . . , b0 der Binärdarstellung von b hat. Folglich ist d = ac mod n = ab mod n. Wenn die Eingabewerte a, b und n β-Bit-Zahlen sind, dann ist die Gesamtanzahl der erforderlichen arithmetischen Operationen O(β) und die Gesamtanzahl der erforderlichen Bit-Operationen O(β 3 ).
Übungen 31.6-1 Zeichnen Sie einen Tabelle, die den Rang aller Elemente von Z∗11 zeigt. Wählen Sie die kleinste primitive Wurzel g aus und berechnen Sie eine Tabelle, die für alle x ∈ Z∗11 den Wert ind11,g (x) angibt. 31.6-2 Geben Sie einen Algorithmus für das modulare Potenzieren an, der die Bits von b von rechts nach links anstatt von links nach rechts auswertet. 31.6-3 Erläutern Sie, wie wir a−1 mod n für beliebige a ∈ Z∗n mithilfe der Prozedur Modular-Exponentiation berechnen können, wenn wir φ(n) kennen.
970
31.7
31 Zahlentheoretische Algorithmen
Das RSA-Kryptosystem
Mit einem Kryptosystem mit öffentlichen Schlüsseln (engl. public-key cryptosystem) können wir verschlüsselte Nachrichten so zwischen zwei kommunizierenden Parteien versenden, dass ein Lauscher, der die verschlüsselten Nachrichten abhört, nicht in der Lage ist, diese zu entschlüsseln. Ein Kryptosystem mit öffentlichen Schlüsseln ermöglicht es außerdem, dass wir eine fälschungssichere „digitale Signatur“ an das Ende einer elektronischen Nachricht anhängen können. Eine solche Signatur ist die elektronische Version einer handschriftlichen Unterschrift auf einem Papierdokument. Sie kann leicht von jedermann überprüft, aber von niemandem gefälscht werden und verliert ihre Gültigkeit, wenn auch nur ein Bit der Nachricht verändert wird. Dadurch ermöglicht sie die Beglaubigung sowohl der Identität des Unterzeichners als auch des Inhalts der signierten Nachricht. Sie ist das perfekte Werkzeug für elektronisch unterzeichnete Geschäftsverträge, elektronische Schecks, elektronische Warenbestellungen und andere elektronische Formen der Kommunikation, die die Parteien beglaubigt haben wollen. Das RSA-Kryptosystem ist ein System mit öffentlichen Schlüsseln und basiert auf der sehr großen Differenz in der Komplexität zwischen dem einfachen Bestimmen großer Primzahlen und der Schwierigkeit, ein Produkt von zwei großen Primzahlen zu faktorisieren. Abschnitt 31.8 beschreibt eine effiziente Prozedur, mit der große Primzahlen bestimmt werden können, und Abschnitt 31.9 behandelt das Problem der Faktorisierung großer Zahlen.
Kryptosysteme mit öffentlichem Schlüssel In einem Kryptosystem mit öffentlichem Schlüssel besitzt jeder Teilnehmer einen öffentlichen und einen geheimen Schlüssel . Jeder Schlüssel besteht aus Information. Im RSA-Kryptosystem beispielsweise besteht jeder Schlüssel aus einem Paar ganzer Zahlen. Die Teilnehmer heißen in kryptographischen Beispielen traditionell „Alice“ und „Bob“. Wir bezeichnen ihre öffentlichen und geheimen Schlüssel mit PA , SA für Alice und PB , SB für Bob. Jeder Teilnehmer erzeugt seinen oder ihren eigenen öffentlichen und geheimen Schlüssel. Geheime Schlüssel werden geheim gehalten, während öffentliche Schlüssel jedermann bekannt gegeben oder sogar veröffentlicht werden können. Tatsächlich ist es oft zweckdienlich vorauszusetzen, dass die öffentlichen Schlüssel aller Teilnehmer in einem öffentlichen Verzeichnis frei zugänglich sind, sodass jeder Teilnehmer den öffentlichen Schlüssel jedes anderen leicht erhalten kann. Die öffentlichen und geheimen Schlüssel spezifizieren Funktionen, die auf jede Nachricht angewendet werden können. Wir bezeichnen die Menge aller zulässigen Nachrichten mit D. Zum Beispiel könnte D die Menge aller Bitsequenzen endlicher Länge sein. In der einfachsten und ursprünglichen Formulierung der Kryptographie verlangen wir, dass die öffentlichen und geheimen Schlüssel eineindeutige Funktionen von D auf sich selbst spezifizieren. Die zum öffentlichen Schlüssel PA von Alice gehörende Funktion wird mit PA () bezeichnet und die zu ihrem geheimen Schlüssel gehörende Funktion mit SA (). Die Funktionen PA () und SA () sind also Permutationen von D. Wir setzen voraus, dass die Funktionen PA () und SA () bei gegebenen Schlüsseln PA bzw. SA effizient berechenbar sind.
31.7 Das RSA-Kryptosystem
971
Bob
Alice Kommunikationskanal
verschlüsseln M
PA
entschlüsseln C = PA(M)
SA
M
Lauscher
C Abbildung 31.5: Verschlüsselung in einem Kryptosystem mit öffentlichen Schlüsseln. Bob verschlüsselt die Nachricht M mit dem öffentlichen Schlüssel PA von Alice und sendet ihr über einen Kommunikationskanal die resultierende chiffrierte Nachricht C = PA (M ). Ein Lauscher, der die übertragene chiffrierte Nachricht abfängt, erlangt keine Information über M . Alice erhält die Nachricht C und entschlüsselt sie mithilfe ihres geheimen Schlüssels, um die ursprüngliche Nachricht M = SA (C) zu erhalten.
Der öffentliche und der geheime Schlüssel jedes Teilnehmers stellen in dem Sinne ein „zusammengehörendes Paar“ dar, dass sie Funktionen spezifizieren, die zueinander invers sind. Es gilt also M = SA (PA (M )) , M = PA (SA (M ))
(31.35) (31.36)
für jede Nachricht M ∈ D. Die hintereinander ausgeführte Transformation von M mit beiden Schlüsseln liefert wieder die ursprüngliche Nachricht M , egal in welcher Reihenfolge, sie ausgeführt werden. In einem Kryptosystem mit öffentlichen Schlüsseln verlangen wir, dass niemand außer Alice in der Lage ist, mit vernünftigem Zeitaufwand die Funktion SA () zu berechnen. Diese Voraussetzung ist zentral, um verschlüsselte Nachrichten an Alice vertraulich halten zu können und um zu wissen, dass digitale Unterschriften von Alice authentisch sind. Alice muss SA geheim halten; wenn sie dies nicht tut, dann verliert sie ihre Einzigartigkeit und das Kryptosystem kann ihr keine nur für sie spezifischen Fähigkeiten bieten. Die Voraussetzung, dass nur Alice SA () berechnen kann, muss gelten, obwohl jeder PA kennt und PA (), die inverse Funktion zu SA (), effizient berechnen kann. Um ein praktikables Kryptosystem mit öffentlichen Schlüsseln zu entwerfen, müssen wir uns überlegen, wie wir ein System erzeugen können, in dem wir eine Transformation PA () offenlegen können, ohne dadurch aufzudecken, wie die zugehörige inverse Transformation SA () berechnet wird. Diese Aufgabe erscheint sehr anspruchsvoll, aber wir werden sehen, wie wir sie bewerkstelligen können. In einem Kryptosystem mit öffentlichen Schlüsseln funktioniert die Verschlüsselung wie in Abbildung 31.5 dargestellt. Setzen Sie voraus, dass Bob Alice eine verschlüsselte Nachricht M so senden möchte, dass sie für einen Lauscher wie unverständliches Geschwätz aussieht. Das Szenario für das Versenden der Nachricht ist das folgende. • Bob erhält Alice’s öffentlichen Schlüssel PA (aus einem öffentlichen Verzeichnis oder direkt von Alice).
972
31 Zahlentheoretische Algorithmen Alice
Bob verifizieren
unterschreiben SA
σ = SA (M )
σ
PA
=? M
(M , σ)
M
akzeptieren
Kommunikationskanal Abbildung 31.6: Digitale Signaturen in einem Kryptosystem mit öffentlichen Schlüsseln. Alice unterzeichnet die Nachricht M , indem sie ihre digitale Signatur σ = SA (M ) anhängt. Sie überträgt das aus Nachricht und Signatur bestehende Paar (M , σ) an Bob, der sie durch Überprüfung der Gleichung M = PA (σ) verifiziert. Wenn die Gleichung erfüllt ist, akzeptiert er (M , σ) als eine von Alice unterzeichnete Nachricht.
• Bob berechnet die zu M gehörende chiffrierte Nachricht C = PA (M ) und sendet C an Alice. • Wenn Alice die chiffrierte Nachricht C empfängt, wendet sie ihren geheimen Schlüssel SA an, um die ursprüngliche Nachricht SA (C) = SA (PA (M )) = M zurückzugewinnen. Da SA () und PA () inverse Funktionen sind, kann Alice M aus C berechnen. Da nur Alice in der Lage ist, SA () zu berechnen, ist sie die einzige, die M aus C berechnen kann. Da Bob M mit PA () verschlüsselt hat, kann nur Alice die übermittelte Nachricht verstehen. Wir können genauso einfach digitale Signaturen innerhalb unserer Darstellung eines Kryptosystems mit öffentlichen Schlüsseln implementieren. (Es gibt andere Ansätze für das Problem der Erzeugung digitaler Signaturen; wir werden aber nicht im Rahmen dieses Buches auf sie eingehen.) Setzen Sie nun voraus, dass Alice eine digital unterzeichnete Antwort M an Bob senden möchte. Abbildung 31.6 zeigt, wie das entsprechende Szenario abläuft. • Alice berechnet ihre digitale Signatur σ für die Nachricht M mithilfe ihres geheimen Schlüssels SA und der Gleichung σ = SA (M ). • Alice sendet das aus Nachricht und Signatur bestehende Paar an Bob. • Wenn Bob das Paar (M , σ) empfängt, kann er verifizieren, ob die Nachricht tatsächlich von Alice stammt, indem er ihren öffentlichen Schlüssel anwendet, um die Gleichung M = PA (σ) zu überprüfen. (Vermutlich enthält M Alice’s Namen, sodass Bob weiß, wessen öffentlichen Schlüssel er anwenden muss.) Wenn die Gleichung erfüllt ist, schlussfolgert Bob, dass die Nachricht M tatsächlich von Alice unterzeichnet wurde. Wenn die Gleichung nicht erfüllt ist, weiß Bob, dass entweder die Nachricht M oder die digitale Signatur σ durch Übertragungsfehler beschädigt wurden oder dass das Paar (M , σ) eine versuchte Fälschung ist.
31.7 Das RSA-Kryptosystem
973
Da die digitale Signatur sowohl die Beglaubigung des Unterzeichners als auch die des Inhalts der unterzeichneten Nachricht ermöglicht, ist sie gleichwertig mit einer handschriftlichen Unterschrift unter einem Papierdokument. Eine digitale Signatur muss für jedermann überprüfbar sein, der Zugang zu dem öffentlichen Schlüssel des Unterzeichners hat. Eine unterzeichnete Nachricht kann von einer Partei verifiziert werden und dann zu anderen Parteien weitergeleitet werden, die die Signatur ebenfalls überprüfen können. Die Nachricht könnte zum Beispiel ein elektronischer Scheck von Alice an Bob sein. Nachdem Bob Alice’s Signatur auf dem Scheck überprüft hat, kann er den Scheck an seine Bank weitergeben, die die Signatur ihrerseits prüfen und die entsprechende Überweisung veranlassen kann. Eine unterschriebene Nachricht muss nicht notwendigerweise verschlüsselt sein, d. h. die Nachricht kann als „Klartext“ vorliegen und braucht nicht vor der Offenlegung geschützt zu sein. Durch Kombination der beiden oben beschriebenen Protokolle für das Verschlüsseln und das Signieren können wir Nachrichten erzeugen, die unterzeichnet und verschlüsselt sind. Der Unterzeichner oder die Unterzeichnerin hängt zuerst seine oder ihre digitale Signatur an die Nachricht an und verschlüsselt dann das entstehende Paar aus Nachricht und Signatur mit dem öffentlichen Schlüssel des beabsichtigten Empfängers oder der beabsichtigten Empfängerin. Der Empfänger oder die Empfängerin entschlüsselt die Nachricht mit seinem oder ihrem geheimen Schlüssel, um die Nachricht und ihre digitale Signatur zu erhalten. Dann kann er die digitale Signatur mithilfe des öffentlichen Schlüssels des Unterzeichners prüfen. Der entsprechende kombinierte Prozess für Dokumente in Papierform bestünde darin, das Papierdokument zu unterschreiben und es dann in einen Briefumschlag zu stecken, der nur vom beabsichtigten Empfänger geöffnet werden könnte.
Das RSA-Kryptosystem Im RSA-Kryptosystem mit öffentlichen Schlüsseln erzeugt ein Teilnehmer oder eine Teilnehmerin seinen oder ihren öffentlichen und geheimen Schlüssel mit der folgenden Prozedur. 1. Er oder sie wählt per Zufall zwei große Primzahlen p und q mit p = q. Die Primzahlen können zum Beispiel jeweils 1024 Bit lang sein. 2. Er oder sie berechnet n = pq. 3. Er oder sie wählt eine kleine ungerade ganze Zahl e, die teilerfremd zu der Zahl φ(n) ist. Diese ist nach Gleichung (31.20) gleich (p − 1)(q − 1). 4. Er oder sie berechnet d als die multiplikative Inverse von e modulo φ(n). (Korollar 31.26 stellt sicher, dass d existiert und eindeutig definiert ist. Er oder sie kann das Verfahren aus Abschnitt 31.4 verwenden, um d bei gegebenem e und φ(n) zu berechnen.) 5. Er oder sie veröffentlicht das Paar P = (e, n) als seinen oder ihren öffentlichen RSA-Schlüssel.
974
31 Zahlentheoretische Algorithmen
6. Er oder sie behält das Paar S = (d, n) als seinen oder ihren geheimen RSASchlüssel geheim. In diesem System ist der Wertebereich D gleich der Menge Zn . Um eine Nachricht M mit einem öffentlichen Schlüssel P = (e, n) zu transformieren, berechnen wir P (M ) = M e
(31.37)
(mod n) .
Um eine chiffrierte Nachricht C mit einem geheimen Schlüssel S = (d, n) zu transformieren, berechnen wir S(C) = C d
(31.38)
(mod n) .
Diese Gleichungen sind sowohl für die Verschlüsselung als auch für Signaturen anwendbar. Um eine Signatur zu erzeugen, wendet der Unterzeichner oder die Unterzeichnerin seinen oder ihren geheimen Schlüssel auf die zu unterzeichnende Nachricht an und nicht auf die chiffrierte Nachricht. Um die Signatur zu überprüfen, wird der öffentliche Schlüssel des Unterzeichners oder der Unterzeichnerin auf sie angewendet, nicht auf die zu verschlüsselnde Nachricht. Wir können die Operationen auf den öffentlichen und geheimen Schlüsseln implementieren, indem wir die in Abschnitt 31.6 beschriebene Prozedur Modular-Exponentiation anwenden. Um die Laufzeit dieser Operationen zu analysieren, setzen wir voraus, dass der öffentliche Schlüssel (e, n) und der geheime Schlüssel (d, n) die Beziehungen lg e = O(1), lg d ≤ β und lg n ≤ β erfüllen. Dann erfordert die Anwendung eines öffentlichen Schlüssels O(1) modulare Multiplikationen und O(β 2 ) Bitoperationen. Die Anwendung eines geheimen Schlüssels erfordert O(β) modulare Multiplikationen und O(β 3 ) Bitoperationen. Theorem 31.36: (Korrektheit der RSA-Verschlüsselung) Die RSA-Gleichungen (31.37) und (31.38) definieren inverse Transformationen in Zn , die die Gleichungen (31.35) und (31.36) erfüllen. Beweis: Aus den Gleichungen (31.37) und (31.38) folgt für beliebige M ∈ Zn P (S(M )) = S(P (M )) = M ed
(mod n) .
Da e und d multiplikative Inverse modulo φ(n) = (p − 1)(q − 1) sind, gilt ed = 1 + k(p − 1)(q − 1) für eine ganze Zahl k. Dann ist aber im Falle M ≡ 0 (mod p) M ed ≡ M (M p−1 )k(q−1) ≡ M ((M mod p)p−1 )k(q−1) ≡ M (1)k(q−1) ≡M
(mod (mod (mod (mod
p) p) p) p) .
(wegen Theorem 31.31)
31.7 Das RSA-Kryptosystem
975
Also gilt M ed ≡ M (mod p), wenn M ≡ 0 (mod p) gilt. Deshalb ist M ed ≡ M
(mod p)
für alle M . Entsprechend ist M ed ≡ M
(mod q)
für alle M . Nach Korollar 31.29 des Chinesischen Restsatzes gilt daher M ed ≡ M für alle M .
(mod n)
Die Sicherheit des RSA-Kryptosystems beruht zum großen Teil auf der Schwierigkeit, große Primzahlen zu faktorisieren. Wenn ein Angreifer den Modulus n in einem öffentlichen Schlüssel faktorisieren kann, dann kann der Angreifer den geheimen Schlüssel aus dem öffentlichen Schlüssel ableiten, indem er sein Wissen über die Faktoren p und q in der gleichen Weise verwendet wie der Erzeuger des öffentlichen Schlüssels. Wenn also das Faktorisieren großer Zahlen einfach ist, dann ist es auch einfach, die RSA-Verschlüsselung zu knacken. Die umgekehrte Aussage, dass das Knacken der RSAVerschlüsselung schwer ist, wenn das Faktorisieren schwer ist, ist nicht bewiesen. Nach zwei Jahrzehnten Forschung wurde jedoch keine einfachere Methode für das Knacken des RSA-Kryptosystems gefunden als das Faktorisieren des Modulus n. Und wie wir in Abschnitt 31.9 sehen werden, ist die Faktorisierung großer Zahlen überraschend schwierig. Indem wir zwei Primzahlen der Länge 1024 Bit zufällig auswählen und miteinander multiplizieren, können wir einen öffentlichen Schlüssel erzeugen, der mit heutigen Methoden nicht mit vertretbarem Zeitaufwand zu knacken ist. Wenn ein fundamentaler Durchbruch beim Entwerfen zahlentheoretischer Algorithmen weiter ausbleibt und das RSA-Kryptosystem mit Sorgfalt entsprechend der empfohlenen Standards implementiert wird, kann dieses System ein hohes Maß an Sicherheit in den Anwendungen bieten. Damit wir mit einem RSA-Kryptosystem Sicherheit erreichen, sollten wir ganze Zahlen benutzen, die ziemlich lang sind – mehrere hundert Bit oder sogar eintausend Bit lang – , um zukünftigen Fortschritten beim Faktorisieren Paroli bieten zu können. Zum Zeitpunkt der Erstellung dieses Buches (2009) waren RSA-Moduli typischerweise im Bereich zwischen 768 und 2048 Bit. Um Moduli dieser Größenordnung zu erzeugen, müssen wir in der Lage sein, entsprechend große Primzahlen effizient zu bestimmen. Abschnitt 31.8 geht auf dieses Problem ein. Aus Effizienzgründen wird RSA häufig in einem „Hybrid-“ oder „Schlüsselmanagement“Modus mit schnellen, nicht auf öffentlichen Schlüsseln basierten Kryptosystemen verwendet. In einem solchen System sind die Schlüssel zum Verschlüsseln und zum Entschlüsseln identisch. Wenn Alice eine lange private Nachricht an Bob senden möchte, wählt sie einen zufälligen Schlüssel K für das schnelle, nicht auf öffentlichen Schlüsseln basierte Kryptosystem und verschlüsselt M mit K, sodass sie die chiffrierte Nachricht C erhält. Hierbei ist C so lang wie M , aber der Schlüssel K ist in der Regel ziemlich
976
31 Zahlentheoretische Algorithmen
kurz. Dann verschlüsselt sie K mit Bobs öffentlichem RSA-Schlüssel. Da K kurz ist, ist die Berechnung von PB (K) schnell (viel schneller als die Berechnung von PB (M ) wäre). Dann sendet sie (C, PB (K)) an Bob, der PB (K) entschlüsselt, um K zu erhalten, und dann mit K die chiffrierte Nachricht C entschlüsselt, um schließlich M zu erhalten. Wir können einen ähnlichen hybriden Ansatz verwenden, um digitale Signaturen effizient zu machen. Dieser Ansatz kombiniert RSA mit einer öffentlichen kollisionsresistenten Hashfunktion h, einer Funktion, die leicht zu berechnen ist, für die es jedoch rechnerisch unmöglich ist, zwei Nachrichten M und M mit h(M ) = h(M ) zu finden. Der Wert h(M ) ist ein kurzer (sagen wir 256 Bit langer) „Fingerabdruck“ der Nachricht M . Wenn Alice eine Nachricht M unterzeichnen möchte, wendet sie zunächst h auf M an, um den Fingerabdruck h(M ) zu erhalten, den sie mit ihrem geheimen Schlüssel verschlüsselt. Sie sendet (M, SA (h(M ))) als ihre unterzeichnete Version von M an Bob. Bob kann die Signatur überprüfen, indem er h(M ) berechnet und verifiziert, dass PA angewendet auf das empfangene SA (h(M )) gleich h(M ) ist. Da niemand zwei Nachrichten mit dem gleichen Fingerabdruck erzeugen kann, ist es rechnerisch unmöglich, eine unterzeichnete Nachricht zu verändern, ohne die Gültigkeit der Signatur zu zerstören. Bevor wir den Abschnitt beenden, wollen wir anmerken, dass die Verwendung von Zertifikaten die Verteilung der öffentlichen Schlüssel stark vereinfacht. Setzen Sie beispielsweise voraus, dass es eine „vertrauenswürdige Autorität“ T gibt, deren öffentlicher Schlüssel jedermann bekannt ist. Alice kann von T eine unterzeichnete Nachricht erhalten (ihr Zertifikat), die besagt „der öffentliche Schlüssel von Alice ist PA “. Dieses Zertifikat ist „selbstbeglaubigend“, da jeder den öffentlichen Schlüssel PT kennt. Alice kann ihr Zertifikat in ihre unterzeichneten Nachrichten einfügen, sodass der Empfänger ihren öffentlichen Schlüssel unmittelbar zur Verfügung hat, um ihre Signatur zu überprüfen. Da ihr Schlüssel von T unterzeichnet wurde, weiß der Empfänger, dass der Schlüssel von Alice tatsächlich Alice gehört.
Übungen 31.7-1 Betrachten Sie einen RSA-Schlüssel mit p = 11, q = 29, n = 319 und e = 3. Welcher Wert von d sollte im geheimen Schlüssel verwendet werden? Wie sieht die Verschlüsselung der Nachricht M = 100 aus? 31.7-2 Beweisen Sie folgende Aussage. Wenn der Exponent e von Alice’s öffentlichem Schlüssel gleich 3 ist und ein Gegner den Exponenten d (mit 0 < d < φ(n)) ihres geheimen Schlüssels erhält, dann kann der Gegner Alice’s Modulus n in polynomieller Zeit in der Anzahl der Bits von n faktorisieren. (Obwohl Sie dies nicht beweisen sollen, interessiert es Sie vielleicht, dass dieses Ergebnis selbst dann gültig bleibt, wenn wir die Bedingung e = 3 fallen lassen (siehe Miller [255]).) 31.7-3∗ Beweisen Sie, dass RSA multiplikativ in dem Sinne ist, dass PA (M1 )PA (M2 ) ≡ PA (M1 M2 ) (mod n) gilt. Wenden Sie diese Eigenschaft an, um zu zeigen, dass, wenn ein Gegner eine Prozedur hätte, die 1 % der mit PA verschlüsselten Nachrichten aus Zn effizient entschlüsselt, dieser Gegner einen probabilistischen Algorithmus benutzen
31.8 ∗ Primzahltests
977
könnte, um jede mit PA verschlüsselte Nachricht mit hoher Wahrscheinlichkeit zu entschlüsseln.
∗ 31.8 Primzahltests In diesem Abschnitt beschäftigen wir uns mit dem Problem, große Primzahlen zu finden. Wir beginnen mit einer Diskussion der Verteilung von Primzahlen, fahren fort mit der Untersuchung eines plausiblen (aber unvollständigen) Ansatzes für das Testen, ob eine gegebene Zahl eine Primzahl ist, und stellen schließlich einen effektiven Primzahltest vor, der auf Miller und Rabin zurückgeht.
Die Verteilung der Primzahlen Für viele Anwendungen (zum Beispiel in der Kryptographie) ist es notwendig, große „zufällige“ Primzahlen zu bestimmen. Zum Glück sind Primzahlen nicht zu selten, sodass es machbar ist, zufällig gewählte ganze Zahlen geeigneter Größe zu überprüfen, bis eine Primzahl gefunden ist. Die Primzahlverteilungsfunktion π(n) gibt die Anzahl der Primzahlen an, die kleiner oder gleich n sind. Beispielsweise gilt π(10) = 4, da es 4 Primzahlen gibt, die kleiner als 10 sind, nämlich 2, 3, 5 und 7. Der Primzahlsatz liefert eine nützliche Approximation für π(n). Theorem 31.37: (Primzahlsatz)
π(n) =1. n→∞ n/ ln n lim
Die Approximation n/ ln n liefert selbst für kleine n Näherungswerte für π(n) von akzeptabler Genauigkeit. Beispielsweise beträgt der Fehler für n = 109 mit n/ ln n ≈ 48,254,942 gegenüber π(n) = 50,847,534 weniger als 6%. (Für einen Zahlentheoretiker ist 109 eine kleine Zahl.) Wir können den Prozess, eine ganze Zahl n zufällig zu wählen und zu überprüfen, ob sie eine Primzahl ist, als einen Bernoulli-Versuch verstehen (siehe Abschnitt C.4). Durch den Primzahlsatz ist die Wahrscheinlichkeit, dass der Versuch erfolgreich – d. h. dass n eine Primzahl ist – ungefähr 1/ ln n. Die geometrische Verteilung sagt uns, wie viele Versuche wir machen müssen, um erfolgreich zu sein, und die erwartete Anzahl von Versuchen ist wegen Gleichung (C.32) ungefähr ln n. Wir würden also erwarten, etwa ln n zufällig gewählte Zahlen um die Zahl n herum testen zu müssen, um eine Primzahl zu finden, die die gleiche Länge wie n hat. Beispielsweise erwarten wir, dass für das Finden einer 1024-bit Primzahl etwa ln 21024 ≈ 710 Tests einer zufällig gewählten 1024Bit Zahl notwendig wären. (Natürlich können wir diese Zahl halbieren, indem wir nur ungerade ganze Zahlen auswählen.)
978
31 Zahlentheoretische Algorithmen
Im verbleibenden Teil dieses Abschnitts betrachten wir das Problem, festzustellen, ob eine große ungerade ganze Zahl eine Primzahl ist. Um die Notation einfach zu halten, setzen wir voraus, dass n die Primzahlzerlegung n = pe11 pe22 · · · perr ,
r≥1
(31.39)
hat, wobei p1 , p2 , . . . , pr die Primfaktoren von n und e1 , e2 , . . . , er positive ganze Zahlen sind. n ist genau dann eine Primzahl, wenn r = 1 und e1 = 1 gilt. Ein einfacher Ansatz für einen Primzahltest ist die Probedivision. Wir versuchen, n √ durch alle ganzen Zahlen 2, 3, . . . , n zu teilen. (Wir können wiederum gerade ganze Zahlen, die größer als 2 sind, überspringen.) Es ist leicht einzusehen, dass n genau dann eine Primzahl ist, wenn keiner der Probeteiler n teilt. Wenn jede√Probedivision konstante Zeit benötigt, dann ist die Laufzeit im schlechtesten Fall Θ( n) und damit exponentiell in der Länge der Darstellung von n. (Rufen Sie sich in Erinnerung, dass √ bei einer Binärdarstellung von n mit β Bits β = lg(n + 1) gilt, sodass n = Θ(2β/2 ) ist.) Das bedeutet, dass die Probedivision nur gut arbeitet, wenn n sehr klein ist oder zufällig einen kleinen Primfaktor hat. Wenn sie funktioniert, dann hat die Probedivision den Vorteil, dass sie nicht nur feststellt, ob n eine Primzahl ist, sondern auch einen Primfaktor von n bestimmt, wenn n eine zusammengesetzte ganze Zahl ist. In diesem Abschnitt interessieren wir uns nur dafür, ob eine gegebene Zahl n eine Primzahl ist oder nicht. Falls n zusammengesetzt ist, befassen wir uns nicht damit, die Primfaktorzerlegung zu bestimmen. Wie wir in Abschnitt 31.9 sehen werden, ist die Berechnung der Primfaktorzerlegung sehr rechenaufwendig. Es überrascht vielleicht, dass es wesentlich einfacher ist, herauszufinden, ob eine gegebene Zahl eine Primzahl ist, als die Primfaktorzerlegung zu bestimmen, wenn sie keine Primzahl ist.
Pseudoprimzahltest Wir betrachten nun einen Primzahltest, der „fast immer funktioniert“ und der tatsächlich für viele praktische Anwendungen gut genug ist. Weiter hinten im Buch, werden wir eine Verfeinerung dieser Methode vorstellen, die die kleine Unzulänglichkeit eliminiert. Sei Z+ n die Menge der von 0 verschiedenen Elemente von Zn : Z+ n = {1, 2, . . . , n − 1} . ∗ Ist n eine Primzahl, so gilt Z+ n = Zn .
Wir sagen, dass n eine Basis-a-Pseudoprimzahl ist, wenn n zusammengesetzt ist und an−1 ≡ 1
(mod n)
(31.40)
gilt. Aus dem Satz von Fermat (Theorem 31.31) folgt, dass eine Primzahl n die Glei+ chung (31.40) für jedes a aus Z+ n erfüllt. Wenn wir also eine Zahl a ∈ Zn finden, für die n die Gleichung (31.40) nicht erfüllt, dann ist n mit Sicherheit zusammengesetzt. Überraschenderweise gilt die Umkehrung fast, sodass dieses Kriterium einen fast perfekten Test für Primzahlen ist. Wir testen, ob n die Gleichung (31.40) für a = 2 erfüllt. Ist dies
31.8 ∗ Primzahltests
979
nicht der Fall, deklarieren wir n als zusammengesetzt und geben zusammengesetzt zurück. Anderenfalls vermuten wir, dass n eine Primzahl ist (obwohl wir eigentlich nur wissen, dass n entweder eine Primzahl oder eine Basis-2-Pseudoprimzahl ist) und geben prim zurück. Die folgende Prozedur gibt in dieser Weise vor zu testen, ob n eine Primzahl ist. Sie verwendet die Prozedur Modular-Exponentiation aus Abschnitt 31.6. Die Eingabe n ist eine ungerade ganze Zahl größer als 2. Pseudoprime(n) 1 if Modular-Exponentiation(2, n − 1, n) ≡ 1 (mod n) 2 return zusammengesetzt // sicher 3 else return prim // wir hoffen! Diese Prozedur kann Fehler machen, aber nur von einem Typ. Wenn sie zurückgibt, dass n zusammengesetzt ist, dann ist dies mit Sicherheit korrekt. Wenn sie jedoch zurückgibt, dass n eine Primzahl ist, dann ist dies nur dann falsch, wenn n eine Basis2-Pseudoprimzahl ist. Wie häufig irrt diese Prozedur? Überraschend selten. Es gibt nur 22 Werte von n kleiner gleich 10 000, für die sie falsche Antworten gibt; die ersten vier dieser Werte sind 341, 561, 645 und 1105. Wenn wir es auch nicht beweisen werden, die Wahrscheinlichkeit, dass die Prozedur für eine β-Bit-Zahl einen Fehler macht, geht für β → ∞ gegen 0. Mit genaueren, auf Pomerance [279] zurückgehenden Abschätzungen für die Anzahl der Basis-2-Primzahlen einer gegebenen Größe können wir abschätzen, dass eine zufällig gewählte 512-Bit-Zahl, die von der obigen Prozedur als Primzahl deklariert wird, mit einer geringeren Wahrscheinlichkeit als 1 zu 1020 eine Basis-2-Pseudoprimzahl ist. Für eine zufällig gewählte 1024-Bit-Zahl beträgt diese Fehlerwahrscheinlichkeit weniger als 1 zu 1041 . Wenn Sie also einfach nur eine große Primzahl für eine Anwendung bestimmen wollen, dann machen Sie für praktische Zwecke fast nie einen Fehler, wenn Sie per Zufall große Zahlen wählen und diese mit der Prozedur Pseudoprime testen, bis für eine davon Pseudoprime den Wert prim zurückgibt. Wenn aber die Zahlen nicht zufällig gewählt werden, benötigen wir einen besseren Ansatz für das Testen auf Primzahlen. Wir werden sehen, dass wir durch eine geschicktere Vorgehensweise und durch Randomisierung eine Routine zum Testen von Primzahlen erhalten, die für alle Eingaben gut arbeitet. Leider können wir nicht alle Fehler vollständig eliminieren, indem wir einfach die Gleichung (31.40) auf eine zweite Basis überprüfen, zum Beispiel auf a = 3, da es zusammengesetzte Zahlen n, sogenannte Carmichael-Zahlen, gibt, die die Gleichung (31.40) für alle a ∈ Z∗n erfüllen. (Wir halten fest, dass die Gleichung (31.40) nicht erfüllt ist, wenn ggT(a, n) > 1 gilt – d. h. wenn a ∈ Z∗n – aber wir sollten nicht darauf hoffen, so beweisen zu können, dass n zusammengesetzt ist, da es schwierig ist, ein solches a zu finden, wenn n nur große Primfaktoren besitzt.) Die ersten drei Carmichael-Zahlen sind die Zahlen 561, 1105 und 1729. Carmichael-Zahlen sind extrem selten; es gibt zum Beispiel nur 255, die kleiner als 100 000 000 sind. Übung 31.8-2 hilft bei der Erklärung, warum Carmichael-Zahlen selten sind.
980
31 Zahlentheoretische Algorithmen
Als nächstes zeigen wir, wie wir unseren Primzahltest so verbessern können, dass er sich durch Carmichael-Zahlen nicht täuschen lässt.
Der randomisierte Miller-Rabin-Primzahltest Der Miller-Rabin-Primzahltest überwindet mit zwei Änderungen die Probleme der einfachen Prozedur Pseudoprime beim Testen von Primzahlen • Er testet mehrere zufällig gewählte Basiswerte a und nicht nur einen Basiswert. • Bei der Berechnung jeder modularen Potenz sucht er in der finalen Menge der Quadrate nach einer nichttrivialen Quadratwurzel von 1 modulo n. Findet er eine, so stoppt er die Berechnung und gibt zusammengesetzt zurück. Korollar 31.35 aus Abschnitt 31.6 liefert uns die Rechtfertigung, dass wir auf diesem Weg nach zusammengesetzten Werten suchen können. Der Pseudocode für den Miller-Rabin-Primzahltest folgt weiter unten. Die Eingabe n > 2 ist eine ungerade Zahl, von der festgestellt werden soll, ob sie eine Primzahl ist, und s ist die Anzahl der zufällig gewählten Basiswerte aus Z+ n , die getestet werden. Der Code verwendet den auf Seite 118 beschriebenen Zufallszahlengenerator Random: Random(1, n − 1) gibt eine zufällig gewählte ganze Zahl a mit 1 ≤ a ≤ n − 1 zurück. Außerdem verwendet der Code eine Hilfsprozedur Witness. Witness(a, n) gibt genau dann wahr zurück, wenn a ein „Zeuge“ dafür ist, dass n zusammengesetzt ist, d. h. wenn es unter Verwendung von a möglich ist, dies zu beweisen (auf eine Weise, die wir noch beschreiben werden). Der Test Witness(a, n) ist eine Erweiterung des Tests an−1 ≡ 1
(mod n) ,
der die Grundlage (mit a = 2) für Pseudoprime gebildet hat, aber effektiver als dieser ist. Zunächst stellen wir die Konstruktion der Prozedur Witness vor und diskutieren deren Korrektheit, um anschließend zu zeigen, wie sie im Miller-Rabin-Primzahltest verwendet wird. Sei n − 1 = 2t u mit t ≥ 1 und u ungerade, d. h. die Binärdarstellung von n − 1 ist die Binärdarstellung der ungeraden Zahl u, gefolgt von genau t Nullen. t Daher gilt an−1 ≡ (au )2 (mod n), sodass wir an−1 mod n berechnen können, indem wir zunächst au mod n berechnen und dann das Ergebnis t-mal erfolgreich quadrieren. Witness(a, n) 1 seien t und u so definiert, dass t ≥ 1, u ungerade und n − 1 = 2t u ist 2 x0 = Modular-Exponentiation(a, u, n) 3 for i = 1 to t 4 xi = x2i−1 mod n 5 if xi = = 1 und xi−1 = 1 und xi−1 = n − 1 6 return wahr 7 if xt = 1 8 return wahr 9 return falsch
31.8 ∗ Primzahltests
981
Dieser Pseudocode für Witness berechnet an−1 mod n, indem er in Zeile 2 zuerst den Wert x0 = au mod n berechnet und dann dieses Ergebnis in der for-Schleife der Zeilen 3–6 t-mal quadriert. Durch Induktion nach i können wir zeigen, dass die Foli ge der berechneten Werte x0 , x1 , . . . , xt die Gleichung xi ≡ a2 u (mod n) für alle i = 0, 1, . . . , t erfüllt, sodass insbesondere xt ≡ an−1 (mod n) gilt. Jedesmal nachdem Zeile 4 quadriert hat, kann die Schleife jedoch vorzeitig terminieren, wenn die Zeilen 5–6 feststellen, dass soeben eine nichttriviale Quadratwurzel gefunden wurde. (Wir werden diese Tests gleich erklären.) Ist dies der Fall, so terminiert der Algorithmus und gibt wahr zurück. Die Zeilen 7–8 geben wahr zurück, wenn der berechnete Wert für xt ≡ an−1 (mod n) nicht gleich 1 ist, genau wie die Prozedur Pseudoprime in diesem Fall den Wert zusammengesetzt zurückgibt. Zeile 9 gibt falsch zurück, wenn wir in den Zeilen 6 oder Zeile 8 nicht den Wert wahr zurückgegeben haben. Wir zeigen nun, dass, wenn Witness(a, n) den Wert wahr zurückgibt, wir einen Beweis dafür konstruieren können, dass n zusammengesetzt ist, indem wir a als Zeuge nehmen. Wenn die Prozedur Witness(a, n) in Zeile 8 wahr zurückgibt, dann hat sie entdeckt, dass xt = an−1 mod n = 1 gilt. Ist n prim, so gilt wegen dem Satz von Fermat (Theorem 31.31), dass an−1 ≡ 1 (mod n) für alle a ∈ Z+ n gilt. Also kann n keine Primzahl sein, und die Ungleichung an−1 mod n = 1 beweist diese Tatsache. Wenn die Prozedur Witness(a, n) in Zeile 6 wahr zurückgibt, dann hat sie entdeckt, dass xi−1 eine nichttriviale Quadratwurzel von xi = 1 modulo n ist, denn es gilt xi−1 ≡ ±1 (mod n) trotz xi ≡ x2i−1 ≡ 1 (mod n). Korollar 31.35 besagt, dass es nur dann eine nichttriviale Quadratwurzel von 1 modulo n geben kann, wenn n zusammengesetzt ist. Somit ist der Nachweis, dass xi−1 eine nichttriviale Quadratwurzel von 1 modulo n ist, ein Beweis dafür, dass n zusammengesetzt ist. Damit ist unser Beweis für die Korrektheit von Witness vollständig. Wenn wir feststellen, dass der Aufruf Witness(a, n) den Wert wahr zurückgibt, so ist n mit Sicherheit zusammengesetzt und der Zeuge a zusammen mit der Begründung, warum die Prozedur den Wert wahr zurückgibt (gibt sie ihn über Zeile 6 oder über Zeile 8 zurück?) ist ein Beweis, dass n zusammengesetzt ist. An dieser Stelle wollen wir kurz eine alternative Beschreibung des Verhaltens von Witness als Funktion der Folge X = x0 , x1 , . . . , xt angeben, die sich uns später bei der Analyse der Effizienz des Miller-Rabin-Primzahltests als nützlich erweisen wird. Beachten Sie, dass die Prozedur Witness im Falle xi = 1 für ein 0 ≤ i < t den Rest der Folge nicht zu berechnen braucht. Wenn sie dies trotzdem tun würde, dann wären alle Werte xi+1 , xi+2 , . . . , xt gleich 1. Wir betrachten daher alle diese Stellen in der Folge X als seien sie gleich 1. Es gibt vier Fälle: 1. X = . . . , d mit d = 1, d. h. die Folge X endet nicht mit einer 1: Zurückgegeben wird in Zeile 8 der Wert wahr und a ist (nach dem Satz von Fermat) ein Zeuge dafür, dass n zusammengesetzt ist. 2. X = 1, 1, . . . , 1, d. h. die Folge X besteht nur aus Einsen: Zurückgegeben wird der Wert falsch und a ist kein Zeuge dafür, dass n zusammengesetzt ist. 3. X = . . . , −1, 1, . . . , 1, d. h. die Folge X endet mit Einsen und das letzte von 1
982
31 Zahlentheoretische Algorithmen verschiedene Element ist −1: Zurückgegeben wird der Wert falsch und a ist kein Zeuge dafür, dass n zusammengesetzt ist.
4. X = . . . , d, 1, . . . , 1 mit d = ±1, d. h. die Folge X endet mit Einsen, aber das letzte von 1 verschiedene Element ist nicht −1: Zurückgegeben wird in Zeile 6 der Wert wahr und a ist ein Zeuge dafür, dass n zusammengesetzt ist, da d eine nichttriviale Quadratwurzel von 1 ist. Wir schauen uns nun den Miller-Rabin-Primzahltest an, der auf der Verwendung der Prozedur Witness basiert. Wir setzen wiederum voraus, dass n eine ungerade Primzahl ist. Miller-Rabin(n, s) 1 for j = 1 to s 2 a = Random(1, n − 1) 3 if Witness(a, n) 4 return zusammengesetzt 5 return prim
// sicher // fast sicher
Die Prozedur Miller-Rabin ist eine probabilistische Suche nach einem Beweis, dass n zusammengesetzt ist. Die Hauptschleife (die in Zeile 1 beginnt) wählt bis zu s zufällige Werte für a aus Z+ n (Zeile 2). Falls eines der ausgewählten a ein Zeuge dafür ist, dass n zusammengesetzt ist, dann gibt die Prozedur Miller-Rabin in Zeile 4 zusammengesetzt zurück. Eine solche Antwort ist aufgrund der Korrektheit von Witness immer korrekt. Wenn Miller-Rabin kein Zeuge in s Versuchen finden kann, nimmt die Prozedur an, dass dies daran läge, dass es keine Zeugen gibt und n prim ist. Wir werden sehen, dass dieses Resultat mit großer Wahrscheinlich korrekt ist, wenn s hinreichend groß ist. Es gibt jedoch eine sehr kleine Wahrscheinlichkeit, dass die Prozedur ihre Werte für a so unglücklich gewählt hat und Zeugen existieren, obwohl keine gefunden wurden. Um die Arbeitsweise von Miller-Rabin zu veranschaulichen, sei n die CarmichaelZahl 561, sodass n − 1 = 560 = 24 · 35, t = 4, und u = 35 gilt. Wenn die Prozedur a = 7 als Basis wählt, zeigt Abbildung 31.4 aus Abschnitt 31.6, dass Witness x0 ≡ a35 ≡ 241 (mod 561) berechnet und somit die Folge X = 241, 298, 166, 67, 1. Somit entdeckt Witness eine nichttriviale Quadratwurzel von 1 in dem letzten Quadrierungsschritt, da a280 ≡ 67 (mod n) und a560 ≡ 1 (mod n) gilt. Deshalb ist a = 7 ein Zeuge dafür, dass n zusammengesetzt ist. Witness(7, n) gibt den Wert wahr und Miller-Rabin den Wert zusammengesetzt zurück. Wenn n eine β-Bit-Zahl ist, dann benötigt die Prozedur Miller-Rabin O(sβ) arithmetische Operationen und O(sβ 3 ) Bitoperationen, da sie asymptotisch nicht mehr Arbeit erfordert als s modulare Potenzierungen.
Die Fehlerrate des Miller-Rabin-Primzahltests Wenn die Prozedur Miller-Rabin den Wert prim zurückgibt, dann gibt es eine sehr kleine Wahrscheinlichkeit, dass sie einen Fehler gemacht hat. Anders als für Pseudoprime hängt diese Fehlerwahrscheinlichkeit jedoch nicht von n ab; es gibt für diese
31.8 ∗ Primzahltests
983
Prozedur keine schlechten Eingaben. Stattdessen hängt die Fehlerwahrscheinlichkeit von der Größe von s und vom „Losglück“ bei der Auswahl der Basiswerte von a ab. Da zudem jeder Test strenger als eine einfache Überprüfung der Gleichung (31.40) ist, können wir aus prinzipiellen Gründen erwarten, dass die Fehlerrate für zufällig gewählte Zahlen n klein sein sollte. Das folgende Theorem liefert ein genaueres Argument. Theorem 31.38 Wenn n eine ungerade zusammengesetzte Zahl ist, dann ist die Anzahl der Zeugen für die Zusammengesetztheit von n mindestens (n − 1)/2. Beweis: Wir zeigen, dass die Anzahl der Nichtzeugen höchstens (n − 1)/2 ist, was die Aussage des Theorems beweisen würde. Wir beginnen mit der Behauptung, dass jeder Nichtzeuge ein Element von Z∗n sein muss. Warum? Betrachten Sie einen beliebigen Nichtzeugen a. Dieser muss die Gleichung an−1 ≡ 1 (mod n) erfüllen, d. h. a · an−2 ≡ 1 (mod n). Die Gleichung ax ≡ 1 (mod n) hat demnach eine Lösung, nämlich an−2 . Nach Korollar 31.21 gilt ggT(a, n) | 1, woraus wiederum ggT(a, n) = 1 folgt. Demnach ist a ein Element von Z∗n , und da a ein beliebiger Nichtzeuge ist, gehören alle Nichtzeugen zu Z∗n . Um den Beweis zu vervollständigen, werden wir zeigen, dass nicht nur alle Nichtzeugen in Z∗n enthalten sind, sondern sogar in einer echten Untergruppe B von Z∗n (rufen Sie sich in Erinnerung, dass wir B als echte Untergruppe von Z∗n bezeichnen, wenn B eine Untergruppe von Z∗n ist, aber nicht gleich Z∗n ist). Nach Korollar 31.16 gilt dann |B| ≤ |Z∗n | /2. Wegen |Z∗n | ≤ n − 1 erhalten wir |B| ≤ (n − 1)/2. Daher ist die Anzahl der Nichtzeugen höchstens (n − 1)/2 bzw. die Anzahl der Zeugen mindestens (n − 1)/2. Wir zeigen nun, wie man eine echte Untergruppe B von Z∗n findet, die alle Nichtzeugen enthält. Wir unterscheiden zwei Fälle. Fall 1: Es existiert ein x ∈ Z∗n , für das xn−1 ≡ 1 (mod n) gilt. Mit anderen Worten, n ist keine Carmichael-Zahl. Da Carmichael-Zahlen, wie wir bereits angemerkt haben, extrem selten sind, ist Fall 1 der Fall von beiden, der „in der Praxis“ vorkommt (zum Beispiel dann wenn n zufällig gewählt und auf Primzahl getestet wird). Sei B = b ∈ Z∗n : bn−1 ≡ 1 (mod n) . Offensichtlich ist B nichtleer, da 1 ∈ B gilt. Da B unter der Multiplikation modulo n abgeschlossen ist, ist B nach Theorem 31.14 eine Untergruppe von Z∗n . Beachten Sie, dass jeder Nichtzeuge zu B gehört, da ein Nichtzeuge a die Gleichung an−1 ≡ 1 (mod n) erfüllt. Da x ∈ Z∗n − B gilt, ist B eine echte Untergruppe von Z∗n . Fall 2: Für alle x ∈ Z∗n gilt xn−1 ≡ 1 (mod n) .
(31.41)
984
31 Zahlentheoretische Algorithmen
Mit anderen Worten, n ist eine Carmichael-Zahl. Dieser Fall ist in der Praxis extrem selten. Nichtsdestotrotz kann der Miller-Rabin-Test (im Gegensatz zu einem Pseudoprimzahltest) effizient feststellen, dass eine Carmichael-Zahl zusammengesetzt ist, was wir nun zeigen wollen. In dem betrachteten Fall kann n nicht die Potenz einer Primzahl sein. Um zu sehen weshalb, lassen Sie uns zum Zwecke des Widerspruchs annehmen, dass n = pe für eine Primzahl n und e > 1 gelten würde. Da n als ungerade vorausgesetzt wird, muss p ebenfalls ungerade sein. Aus Theorem 31.32 folgt, dass Z∗n eine zyklische Gruppe ist; sie enthält ein erzeugendes Element g, für das rangn (g) = |Z∗n | = φ(n) = pe (1 − 1/p) = (p − 1)pe−1 gilt. (Die Formel für φ(n) kommt von Gleichung (31.20).) Nach Gleichung (31.41) ist g n−1 ≡ 1 (mod n). Dann folgt aus dem Satz des diskreten Logarithmus (Theorem 31.33 mit y = 0), dass n − 1 ≡ 0 (mod φ(n)) gilt, oder (p − 1)pe−1 | pe − 1 . Dies ist ein Widerspruch für e > 1, da (p − 1)pe−1 durch die Primzahl p teilbar ist, pe − 1 jedoch nicht. Also ist n keine Potenz einer Primzahl. Da die ungerade zusammengesetzte Zahl n keine Primzahlpotenz ist, zerlegen wir sie in ein Produkt n1 n2 , wobei n1 und n2 zueinander teilerfremde ungerade Zahlen größer als 1 sind. (Es kann verschiedene Möglichkeiten geben, n zu zerlegen, und es ist egal, welche davon wir wählen. Beispielsweise könnten wir im Falle n = pe11 pe22 · · · perr die Zerlegung n1 = pe11 und n2 = pe22 pe33 · · · perr wählen.) Rufen Sie sich in Erinnerung, dass wir t und u so wählen, dass n − 1 = 2t u gilt, wobei t ≥ 1 und u ungerade ist, und dass die Prozedur Witness für eine Eingabe a die Folge 2
t
X = au , a2u , a2 u , . . . , a2 u berechnet (alle Berechnungen werden modulo n ausgeführt.) Lassen Sie uns ein Paar (v, j) ganzer Zahlen als akzeptabel bezeichnen, wenn v ∈ Z∗n , j ∈ {0, 1, . . . , t} und j
v2
u
≡ −1 (mod n)
gilt. Es existieren mit Sicherheit akzeptable Paare, da u ungerade ist; wir können v = n − 1 und j = 0 wählen, sodass (n − 1, 0) ein akzeptables Paar ist. Wählen Sie nun den größtmöglichen Wert für j, für den ein akzeptables Paar (v, j) existiert, und setzen Sie v so, dass (v, j) akzeptabel ist. Sei B = {x ∈ Z∗n : x2
j
u
≡ ±1 (mod n)} .
Da die Menge B unter der Multiplikation modulo n abgeschlossen ist, ist sie eine Untergruppe von Z∗n . Nach Korollar 31.16 ist daher |B| ein Teiler von |Z∗n |. Jeder Nichtzeuge muss ein Element von B sein, da die durch einen Nichtzeugen erzeugte Folge X entweder nur Einsen oder wegen der Maximalität von j eine −1 nicht später als an der j-ten Position enthält. (Ist (a, j ) akzeptabel, wobei a ein Nichtzeuge ist, so muss aufgrund unserer Wahl von j die Ungleichung j ≤ j gelten.)
31.8 ∗ Primzahltests
985
Wir verwenden nun die Existenz von v, um zu zeigen, dass es ein w ∈ Z∗n − B gibt und j B somit eine echte Untergruppe von Z∗n ist. Wegen v 2 u ≡ −1 (mod n) gilt nach dem j Korollar 31.29 des Chinesischen Restsatzes v 2 u ≡ −1 (mod n1 ). Nach Korollar 31.28 existiert ein w, das die beiden Gleichungen w≡v w≡1
(mod n1 ) , (mod n2 )
erfüllt. Daher gilt j
w2 w
u
j
2 u
j
≡ −1
(mod n1 ) ,
≡
(mod n2 ) .
1
j
j
Aus w2 u ≡ 1 (mod n1 ) folgt nach Korollar 31.29 w2 u ≡ 1 (mod n) und aus w2 u ≡ j j −1 (mod n2 ) folgt w2 u ≡ −1 (mod n). Folglich gilt w2 u ≡ ±1 (mod n) und somit w ∈ B. Es bleibt noch zu zeigen, dass w ∈ Z∗n gilt. Dazu arbeiten wir zunächst separat modulo n1 und modulo n2 . Arbeiten wir modulo n1 , so sehen wir, dass wegen v ∈ Z∗n ggT(v, n) = 1 und somit auch ggT(v, n1 ) = 1 gilt. Wenn v keinen gemeinsamen Teiler mit n hat, dann hat es mit Sicherheit auch keinen gemeinsamen Teiler mit n1 . Wegen w ≡ v (mod n1 ) gilt auch ggT(w, n1 ) = 1. Arbeiten wir modulo n2 , so stellen wir fest, dass aus w ≡ 1 (mod n2 ) ggT(w, n2 ) = 1 folgt. Um diese Ergebnisse zusammenzufügen, wenden wir Theorem 31.6 an, aus dem ggT(w, n1 n2 ) = ggT(w, n) = 1 folgt. Dies bedeutet, dass w ∈ Z∗n gilt. Daher gilt w ∈ Z∗n − B und wir beschließen Fall 2 mit der Folgerung, dass B eine echte Untergruppe von Z∗n ist. In beiden Fällen sehen wir, dass die Anzahl der Zeugen dafür, dass n zusammengesetzt ist, mindestens (n − 1)/2 ist.
Theorem 31.39 Für jede ungerade ganze Zahle n > 2 und jede positive ganze Zahl s ist die Fehlerwahrscheinlichkeit von Miller-Rabin(n, s) höchstens 2−s . Beweis: Mit Theorem 31.38 sehen wir, dass für zusammengesetzte n die Wahrscheinlichkeit, einen Zeugen dafür zu finden, dass n zusammengesetzt ist, in jeder Iteration der for-Schleife in den Zeilen 1-4 mindestens 1/2 ist. Die Prozedur Miller-Rabin macht nur dann einen Fehler, wenn sie in keiner der s Iterationen der Hauptschleife einen Zeugen entdeckt. Die Wahrscheinlichkeit dafür ist höchstens 2−s . Ist n prim, so gibt Miller-Rabin immer den Wert prim zurück; ist n zusammengesetzt, so ist die Wahrscheinlichkeit, dass Miller-Rabin den Wert prim zurückgibt, höchstens 2−s .
986
31 Zahlentheoretische Algorithmen
Wenn wir Miller-Rabin auf eine große zufällig gewählte ganze Zahl n anwenden, so müssen wir jedoch auch die Wahrscheinlichkeit betrachten, dass n prim ist, um das Ergebnis der Prozedur Miller-Rabin richtig interpretieren zu können. Setzen Sie voraus, dass wir die Bitlänge β festgelegt haben und wir zufällig eine ganze Zahl n der Länge β Bit auswählen, um sie dem Primzahltest zu unterziehen. Sei A das Ereignis, dass n prim ist. Mit Theorem 31.37 folgt, dass die Wahrscheinlichkeit, dass n prim ist, ungefähr Pr {A} ≈ 1/ ln n ≈ 1.443/β ist. Seinun B das Ereignis, dass Miller-Rabin den Wert prim zurückgibt. Es gilt Pr B | A = 0 (d. h. Pr {B | A} = 1) und Pr B | A ≤ 2−s (was äquivalent zu Pr B | A > 1 − 2−s ist). Was ist aber Pr {A | B}, d. h. die bedingte Wahrscheinlichkeit, dass n prim ist, unter der Voraussetzung, dass Miller-Rabin den Wert prim zurückgegeben hat? Mit der alternativen Formulierung von dem Satz von Bayes (Gleichung (C.18)) gilt Pr {A} Pr {B | A} Pr {A} Pr {B | A} + Pr A Pr B | A 1 . ≈ 1 + 2−s (ln n − 1)
Pr {A | B} =
Diese Wahrscheinlichkeit übersteigt 1/2 nicht, bis s größer als lg(ln n − 1) ist. Intuitiv gesehen gibt uns diese Gleichung an, wie viele fehlgeschlagene initiale Versuche benötigt werden, um das Vertrauen zu bekommen, dass n wirklich prim ist. Für eine Zahl mit β = 1024 Bits benötigen wir ungefähr lg(ln n − 1) ≈ lg(β/1.443) ≈9 Versuche. Wie auch immer, wählen wir s = 50, so sollte dies für jede vorstellbare Anwendung ausreichen. Tatsächlich sieht es wesentlich besser aus. Wenn wir versuchen, große Primzahlen zu bestimmen, indem wir Miller-Rabin auf große, zufällig gewählte ungerade ganze Zahlen anwenden, dann ist es sehr unwahrscheinlich, dass die Wahl eines kleinen Wertes für s (sagen wir 2) zu fehlerhaften Ergebnissen führt, auch wenn wir dies hier nicht beweisen wollen. Der Grund hierfür besteht darin, dass für eine zufällig gewählte zusammengesetzte ungerade ganze Zahl n die erwartete Anzahl von Nichtzeugen für die Zusammengesetztheit von n mit hoher Wahrscheinlichkeit sehr viel kleiner als (n − 1)/2 ist.
31.9 ∗ Primfaktorzerlegung
987
Wenn die ganze Zahl n jedoch nicht zufällig gewählt ist, ist das Beste, was wir beweisen können, dass die Anzahl der Nichtzeugen höchstens (n − 1)/4 ist, indem wir eine verbesserte Version von Theorem 31.38 anwenden. Zudem existieren ganze Zahlen n, für die die Anzahl der Nichtzeugen (n − 1)/4 ist.
Übungen 31.8-1 Beweisen Sie, dass für eine ungerade ganze Zahl n > 1, die weder eine Primzahl noch eine Potenz einer Primzahl ist, eine nichttriviale Quadratwurzel 1 modulo n existiert. 31.8-2∗ Es ist möglich, den Eulerschen Satz etwas strenger zu formulieren: Es gilt aλ(n) ≡ 1 (mod n) für alle a ∈ Z∗n mit n = pe11 · · · perr und λ(n) = kgV(φ(pe11 ), . . . , φ(perr )).
(31.42)
Beweisen Sie, dass λ(n) | φ(n) gilt. Eine zusammengesetzte Zahl n ist eine Carmichael-Zahl, falls λ(n) | n − 1 gilt. Die kleinste Carmichael-Zahl ist 561 = 3 · 11 · 17; in diesem Fall ist λ(n) = kgV(2, 10, 16) = 80, was ein Teiler von 560 ist. Beweisen Sie, dass Carmichael-Zahlen „quadratfrei“ (d. h. nicht durch das Quadrat irgendeiner Primzahl teilbar) und jeweils das Produkt von mindestens drei Primzahlen sind. (Das ist der Grund, dass sie nicht sehr häufig vorkommen.) 31.8-3 Beweisen Sie, dass für eine nichttriviale Quadratwurzel x von 1 modulo n sowohl ggT(x − 1, n) als auch ggT(x + 1, n) nichttriviale Teiler von n sind.
∗ 31.9 Primfaktorzerlegung Setzen Sie voraus, dass wir eine ganze Zahl n faktorisieren, d. h. sie in ein Produkt von Primzahlen zerlegen wollen. Der Primzahltest aus dem vorherigen Abschnitt sagt uns möglicherweise, dass n eine zusammengesetzte Zahl ist, er liefert uns jedoch nicht die Primfaktoren von n. Das Faktorisieren großer Zahlen scheint wesentlich schwieriger zu sein als das Entscheiden, ob n eine Primzahl ist oder zusammengesetzt ist. Wir können selbst mit heutigen Supercomputern und den besten heute bekannten Algorithmen nicht in vernünftiger Zeit eine beliebige 1024-Bit-Zahl faktorisieren.
Pollards ρ-Heuristik Durch Probedivision durch alle Zahlen bis einschließlich R kann jede Zahl bis R2 faktorisiert werden. Mit dem gleichen Aufwand kann die folgende Prozedur Pollard-Rho jede Zahl bis R4 faktorisieren (sofern wir kein Pech haben). Da die Prozedur nur eine Heuristik ist, sind weder die Laufzeit noch der Erfolg garantiert, wenngleich die Prozedur
988
31 Zahlentheoretische Algorithmen
in der Praxis sehr effektiv arbeitet. Ein weiterer Vorteil der Prozedur Pollard-Rho ist, dass sie nur einen konstanten Speicherverbrauch hat. (Wenn Sie das wollten, könnten Sie den Algorithmus auf einem programmierbaren Taschenrechner implementieren, um die Primfaktoren kleiner Zahlen zu bestimmen.) Pollard-Rho(n) 1 i=1 2 x1 = Random(0, n − 1) 3 y = x1 4 k =2 5 while wahr 6 i = i+1 7 xi = (x2i−1 − 1) mod n 8 d = ggT(y − xi , n) 9 if d = 1 und d = n 10 print d 11 if i = = k 12 y = xi 13 k = 2k Die Prozedur arbeitet folgendermaßen. Die Zeilen 1–2 initialisieren i mit 1 und x1 mit einem zufällig gewählten Wert aus Zn . Die in Zeile 5 beginnende while-Schleife iteriert ohne Abbruchkriterium und sucht Faktoren von n. Bei jeder Iteration der while-Schleife wird in Zeile 7 die Rekursionsgleichung xi = (x2i−1 − 1) mod n
(31.43)
angewendet, um das nächste xi in der unendlichen Folge x1 , x2 , x3 , x4 , . . .
(31.44)
zu erzeugen, wobei der entsprechende Wert von i in Zeile 6 inkrementiert wird. Im Pseudocode sind die Variablen mit Indizes geschrieben. Das Programm arbeitet jedoch genauso, wenn alle Indizes weggelassen werden, da nur der aktuelle Wert von xi aufbewahrt werden muss. Mit dieser Modifikation verwendet der Algorithmus nur eine konstante Anzahl von Speicherplätzen. Manchmal speichert das Programm den aktuell erzeugten xi -Wert in der Variable y. Genauer gesagt sind die Werte, die gespeichert werden, diejenigen, deren Indizes Zweierpotenzen sind, also x1 , x2 , x4 , x8 , x16 , . . . . Zeile 3 speichert den Wert x1 und Zeile 12 speichert den Wert xk , immer wenn i gleich k ist. Die Variable k wird in Zeile 4 mit dem Wert 2 initialisiert und Zeile 13 verdoppelt den Wert immer dann, wenn Zeile 12 den Wert von y aktualisiert. Daher nimmt k die Werte 1, 2, 4, 8, . . . an und gibt immer den Index des nächsten Wertes xk an, der in y gespeichert wird.
31.9 ∗ Primfaktorzerlegung
989
Die Zeilen 8–10 versuchen, einen Faktor von n mithilfe des gespeicherten Wertes von y und dem aktuellen Wert von xi zu finden. Speziell berechnet Zeile 8 den größten gemeinsamen Teiler d = ggT(y − xi , n). Findet Zeile 9 heraus, dass d ein nichttrivialer Teiler von n ist, dann druckt Zeile 10 den Wert d. Diese Prozedur zur Bestimmung eines Faktors sieht vielleicht auf den ersten Blick etwas mysteriös aus. Dennoch gibt Pollard-Rho niemals ein falsches Ergebnis aus; jede ausgegebene Zahl ist ein nichttrivialer Teiler von n. Es kann jedoch sein, dass die Prozedur Pollard-Rho überhaupt nichts ausgibt. Es gibt keine Garantie, dass sie ein Ergebnis findet. Wir werden aber sehen, dass es gute Gründe dafür gibt, zu erwarten, √ dass Pollard-Rho nach Θ( p) Iterationen der while-Schleife einen Faktor p von n ausgibt. Wenn n zusammengesetzt ist, können wir daher erwarten, dass die Prozedur genügend Teiler findet, um n nach etwa n1/4 Aktualisierungen vollständig zu faktorisieren, da jeder Primfaktor p von n, außer eventuell der größte, kleiner als die Wurzel von n ist. Wir beginnen unsere Analyse zum Verhalten dieser Prozedur, indem wir untersuchen, wie lange es dauert, bis in einer zufälligen Folge modulo n ein Wert wiederholt wird. Da Zn endlich ist und jeder Wert in der Folge (31.44) nur vom vorhergehenden Wert abhängt, muss sich die Folge letztendlich wiederholen. Wenn wir einmal ein xi erreichen, für das xi = xj für ein j < i gilt, befinden wir uns in einem Zyklus, da xi+1 = xj+1 , xi+2 = xj+2 usw. gilt. Der Grund für den Namen „ρ-Heuristik“ ist, dass wir, wie in Abbildung 31.7 illustriert, die Folge x1 , x2 , . . . , xj−1 als „Schwanz“ des griechischen Buchstabens ρ und der Zyklus xj , xj+1 , . . . , xi als dessen „Körper“ zeichnen können. Lassen Sie uns nun die Frage betrachten, wie lange es dauert, bis in der Folge eine Wiederholung auftritt. Diese Information ist nicht exakt das, was wir wirklich brauchen; wir werden später sehen, wie wir das Argument modifizieren können. Lassen Sie uns für diese Abschätzung voraussetzen, dass sich die Funktion fn (x) = (x2 − 1) mod n wie eine „zufällige“ Funktion verhält. Natürlich ist sie nicht wirklich zufällig, aber diese Voraussetzung führt zu Ergebnissen, die mit dem beobachteten Verhalten der Prozedur Pollard-Rho übereinstimmen. Wir können dann jedes xi als eine unabhängig aus Zn , gemäß einer gleichmäßigen Verteilung über Zn gezogene Zahl verstehen. Mit √ der Analyse des Geburtstagsparadoxons aus Abschnitt 5.4.1 erwarten wir, dass Θ( n) Schritte gemacht werden, bis die Folge in einem Zyklus mündet. Nun zu der erforderlichen Modifikation. Sei p ein nichttrivialer Faktor von n, für den ggT(p, n/p) = 1 gilt. Wenn n beispielsweise die Primfaktorzerlegung n = pe11 pe22 · · · perr hat, können wir p als pe11 wählen. (Ist e1 = 1, so ist p der kleinste Primfaktor von n, ein gutes Beispiel, das Sie sich merken sollten.) Die Folge xi induziert eine Folge xi modulo p mit xi = xi mod p für alle i.
990
31 Zahlentheoretische Algorithmen
996
310
814
396
x7
x7 177
84
x6 1186
120
x5 1194
339
31
x6 18 x5 26
529 595
1053
x4 63 x3 x2 x1
x3
8
x2
3 2
x4
6
mod 1387 (a)
x1
8 x6
2
x3
16
x5
mod 19 (b)
47
x4 63
x7
3
11
x2 x1
8 3
2
mod 73 (c)
Abbildung 31.7: Pollards ρ-Heuristik. (a) Die durch die Rekursionsgleichung xi+1 = (x2i − 1) mod 1387 erzeugten Werte, beginnend mit x1 = 2. Die Primzahlzerlegung von 1387 ist 19·73. Die dick eingezeichneten Pfeile entsprechen den Iterationsschritten, die ausgeführt werden, bevor der Faktor 19 entdeckt wird. Die dünn eingezeichnete Pfeile zeigen auf unerreichte Werte in der Iteration, um die ρ-Form zu illustrieren. Die schattiert eingezeichneten Werte sind die Werte y, die von Pollard-Rho gespeichert werden. Der Faktor 19 wird beim Erreichen von x7 = 177 entdeckt, wenn ggT(63 − 177, 1387) = 19 berechnet wird. Der erste x-Wert, der wiederholt würde, ist 1186; der Faktor 19 wird jedoch entdeckt, bevor dieser Wert wiederholt wird. (b) Die Werte, die durch die gleiche Rekursionsgleichung, modulo 19, entdeckt werden. Jeder in Teil (a) angegebene Wert xi ist äquivalent modulo 19 zu dem hier gezeigten Wert xi . Zum Beispiel sind x4 = 63 und x7 = 177 äquivalent zu 6 modulo 19. (c) Die durch die gleiche Rekursionsgleichung modulo 73 erzeugten Werte. Jeder in Teil (a) gegebene Wert xi ist äquivalent modulo 73 zu dem hier gezeigten Wert xi . Nach dem Chinesischen Restsatz korrespondiert jeder Knoten in Teil (a) mit einem Knotenpaar, von dem jeweils ein Knoten aus Teil (b) und einer aus Teil (c) stammt.
31.9 ∗ Primfaktorzerlegung
991
Da fn nur unter Verwendung arithmetischer Operationen (Quadrieren und Subtrahieren) modulo n definiert ist, können wir xi+1 aus xi berechnen; die „modulo p-Sicht“ auf die Folge ist eine verkleinerte Version dessen, was modulo n passiert: xi+1 = xi+1 mod p = fn (xi ) mod p = ((x2i − 1) mod n) mod p = (x2i − 1) mod p (wegen Übung 31.1-7) = ((xi mod p)2 − 1) mod p = ((xi )2 − 1) mod p = fp (xi ) . Daher ist die Folge xi , obwohl wir sie nicht explizit berechnen, wohldefiniert und genügt der gleichen Rekursionsgleichung wie die Folge xi . Argumentieren wir wie vorhin, so stellen wir fest, dass die erwartete Anzahl der Schritte, √ bis die Folge xi beginnt, sich zu wiederholen, Θ( p) ist. Wenn p klein im Vergleich zu n ist, könnte sich die Folge xi viel schneller wiederholen als die Folge xi . Tatsächlich wiederholt sich die Folge xi , wie die Teile (b) und (c) der Abbildung 31.7 zeigen, sobald zwei Elemente der Folge xi äquivalent modulo p sind, anstatt äquivalent modulo n. Sei t der Index des ersten wiederholten Wertes in der Folge xi und u > 0 die Länge des Zyklus, der dabei erzeugt wird. Das heißt, t und u > 0 sind die kleinsten Werte mit xt+i = xt+u+i für alle i ≥ 0. Mit dem oben angeführten Argumenten sind die √ Erwartungswerte von t und u beide in Θ( p). Beachten Sie, dass, wenn xt+i = xt+u+i , p ein Teiler von (xt+u+i − xt+i ) ist. Somit gilt ggT(xt+u+i − xt+i , n) > 1. Wenn daher Pollard-Rho einmal einen Wert xk mit k ≥ t als y gespeichert hat, dann ist y mod p immer auf dem Zyklus modulo p. (Wenn ein neuer Wert als y gespeichert wird, dann liegt dieser Wert ebenfalls auf dem Zyklus modulo p.) Letzten Endes wird k auf einen Wert größer u gesetzt und die Prozedur führt dann einen ganzen Umlauf um den Zyklus modulo p aus, ohne den Wert von y zu ändern. Die Prozedur entdeckt dann einen Faktor von n, wenn xi in den zuvor gespeicherten Wert von y modulo p „läuft“, d. h. wenn xi ≡ y (mod p) gilt. Voraussichtlich ist der gefundene Faktor der Faktor p, wenngleich es gelegentlich vorkommen kann, dass ein Vielfaches von p entdeckt wird. Da die Erwartungswerte von t √ und u beide Θ( p) sind, ist die erwartete Anzahl der Schritte, die erforderlich sind, um √ den Faktor p zu erzeugen, Θ( p). Dieser Algorithmus verhält sich aus zwei Gründen möglicherweise etwas anders als erwartet. Erstens ist die heuristische Laufzeitanalyse nicht genau und es ist möglich, dass √ der Zyklus der Werte modulo p viel größer als p ist. In diesem Fall arbeitet der Algorithmus korrekt, aber viel langsamer als erwünscht. Dies scheint jedoch ein Fall von rein akademischem Interesse zu sein, der in der Praxis keine Rolle spielt. Zweitens können die durch den Algorithmus erzeugten Teiler von n immer die trivialen Faktoren 1 oder n sein. Setzen Sie zum Beispiel voraus, dass n = p q für zwei Primzahlen p und q gilt. Es kann sein, dass die Werte von t und u für p die gleichen sind wie für q. Dann wird
992
31 Zahlentheoretische Algorithmen
der Faktor p immer in der gleichen ggT-Operation entdeckt, die den Faktor q entdeckt. Da beide Faktoren zur gleichen Zeit entdeckt werden, wird der triviale Faktor p q = n entdeckt, der nutzlos ist. Dieses Problem scheint in der Praxis ebenfalls bedeutungslos zu sein. Gegebenenfalls können wir die Heuristik mit einer anderen Rekursionsgleichung der Form xi+1 = (x2i − c) mod n erneut starten. (Wir sollten die Werte c = 0 und c = 2 aus Gründen, auf die wir hier nicht eingehen wollen, vermeiden; andere Werte sind jedoch in Ordnung.) Natürlich ist diese Analyse heuristisch und nicht rigoros, da die Rekursionsgleichung nicht wirklich „zufällig“ ist. Nichtsdestotrotz arbeitet die Prozedur in der Praxis gut und scheint tatsächlich so effizient zu sein, wie diese heuristische Analyse vorgibt. Sie ist für die Bestimmung kleiner Primfaktoren einer großen Zahl die Methode der Wahl. Um eine zusammengesetzte n vollständig zu faktorisieren, müssen wir nur β-Bit-Zahl alle Primfaktoren kleiner n1/2 finden, sodass wir höchstens n1/4 = 2β/4 arithmetische Operationen und höchstens n1/4 β 2 = 2β/4 β 2 Bit-Operationen für Pollard-Rho erwarten. Die Fähigkeit der Prozedur Pollard-Rho, einen kleinen Faktor p von n mit √ einer erwarteten Anzahl von Θ( p) arithmetischen Operationen zu finden, ist für viele Anwendungen ihre attraktivste Eigenschaft.
Übungen 31.9-1 Wann druckt Pollard-Rho bei der in Abbildung 31.7(a) gezeigten Ausführung den Faktor 73 von 1387? 31.9-2 Setzen Sie voraus, dass wir eine Funktion f : Zn → Zn und einen Anfangswert x0 ∈ Zn haben. Definieren Sie xi = f (xi−1 ) für i = 1, 2, . . .. Seien t und u > 0 die kleinsten Werte mit xt+i = xt+u+i für i = 0, 1, . . .. In der Terminologie von Pollards ρ-Algorithmus ist t die Länge des Schwanzes und u die Länge des Zyklus von ρ. Geben Sie einen effizienten Algorithmus an, der t und u exakt bestimmt, und analysieren Sie dessen Laufzeit. 31.9-3 Wie viele Schritte von Pollard-Rho würden Sie erwarten, bis sie einen Faktor der Form pe entdeckt, wobei p eine Primzahl ist und e > 1? 31.9-4∗ Ein Nachteil des Algorithmus Pollard-Rho in der angegebenen Form ist, dass er für jeden Schritt der Rekursionsgleichung eine ggT-Berechnung benötigt. Wir könnten die ggT-Berechnungen abarbeiten, indem wir das Produkt mehrerer xi -Werte in einer Reihe akkumulieren und dann dieses Produkt statt xi in der ggT-Berechnung verwenden. Beschreiben Sie ausführlich, wie Sie diese Idee implementieren würden, warum sie funktioniert und welche Stapelgröße Sie als die effektivste für eine β-Bit-Zahl wählen würden.
Problemstellungen 31-1 Binärer ggT-Algorithmus Die meisten Rechnern können die Operationen Subtraktion, Test der Parität einer binär dargestellten ganzen Zahl (ist die Zahl ungerade oder gerade?) und Halbieren schneller als die Berechnung der Reste ausführen. Diese Problemstellung untersucht den binären ggT-Algorithmus, der die im Euklidischen Algorithmus verwendeten Berechnungen von Resten vermeidet.
Problemstellungen zu Kapitel 31
993
a. Beweisen Sie, dass ggT(a, b) = 2 · ggT(a/2, b/2) gilt, wenn a und b beide gerade sind. b. Beweisen Sie, dass ggT(a, b) = ggT(a, b/2) gilt, wenn a ungerade und b gerade ist. c. Beweisen Sie, dass ggT(a, b) = ggT((a − b)/2, b) gilt, wenn a und b beide ungerade sind. d. Entwerfen Sie einen effizienten ggT-Algorithmus für ganzzahlige Eingabewerte a und b mit a ≥ b, der in Zeit O(lg a) läuft. Setzen Sie voraus, dass jede Subtraktion, jeder Paritätstest und jede Halbierung in einer Zeiteinheit ausgeführt werden kann. 31-2 Analyse der Bit-Operationen im Euklidischen Algorithmus a. Betrachten Sie den gewöhnlichen „Papier-und-Bleistift-Algorithmus“ der Division: das Teilen von a durch b, welches einen Quotienten q und einen Rest r liefert. Zeigen Sie, dass diese Methode O((1 + lg q) lg b) Bit-Operationen benötigt. b. Definieren Sie μ(a, b) = (1+lg a)(1+lg b). Zeigen Sie, dass die Anzahl der von der Prozedur Euclid(a, b) durchgeführten Bit-Operationen, um die Berechnung von ggT(a, b) auf die Berechnung von ggT(b, a mod b) zurückzuführen, höchstens c(μ(a, b) − μ(b, a mod b)) für eine hinreichend große Konstante c > 0 ist. c. Zeigen Sie, dass die Prozedur Euclid(a, b) allgemein O(μ(a, b)) Bit-Operationen und bei Anwendung auf zwei β-Bit-Zahlen O(β 2 ) Bit-Operationen benötigt. 31-3 Drei Algorithmen für Fibonacci-Zahlen Diese Problemstellung vergleicht die Effizienz von drei Methoden für die Berechnung der n-ten Fibonacci-Zahl Fn bei gegebenem n. Setzen Sie voraus, dass die Kosten für das Addieren, das Subtrahieren oder das Multiplizieren zweier Zahlen unabhängig von der Größe der Zahlen O(1) sind. a. Zeigen Sie, dass die Laufzeit der einfachen rekursiven Methode zur Berechnung von Fn auf Basis der Rekursionsgleichung (3.22) exponentiell in n ist. (Siehe beispielsweise die Prozedur Fib auf Seite 788.) b. Zeigen Sie, wie wir Fn mithilfe von Memoisation in Zeit O(n) berechnen können. c. Zeigen Sie, wie wir Fn in Zeit O(lg n) berechnen können, indem wir nur ganzzahlige Additionen und Multiplikationen verwenden. (Hinweis: Betrachten Sie die Matrix 01 11 und ihre Potenzen.)
994
31 Zahlentheoretische Algorithmen d. Setzen Sie nun voraus, dass die Addition zweier β-Bit-Zahlen Zeit Θ(β) und die Multiplikation zweier β-Bit-Zahlen Zeit Θ(β 2 ) benötigt. Wie ist die Laufzeit dieser drei Methoden unter dieser realistischeren Kostenannahme für die elementaren arithmetischen Operationen?
31-4 Quadratische Reste Sei p eine ungerade Primzahl. Eine Zahl a ∈ Zp∗ ist ein quadratischer Rest, falls die Gleichung x2 = a (mod p) eine Lösung für die Unbekannte x besitzt. a. Zeigen Sie, dass es genau (p − 1)/2 quadratische Reste modulo p gibt. b. Ist p eine Primzahl, so definieren wir das Legendre-Symbol ap für a ∈ Z∗p als 1, falls a ein quadratischer Rest modulo p ist, und sonst als −1. Beweisen Sie, dass für a ∈ Z∗p a ≡ a(p−1)/2 (mod p) p gilt. Geben Sie einen effizienten Algorithmus an, der bestimmt, ob eine gegebene Zahl a ein quadratischer Rest modulo p ist. Analysieren Sie die Effizienz Ihres Algorithmus. c. Beweisen Sie, dass, wenn p eine Primzahl der Form 4k + 3 und a ein quadratischer Rest in Z∗p ist, ak+1 mod p eine Quadratwurzel von a modulo p ist. Wie viel Zeit ist erforderlich, um die Quadratwurzel eines quadratischen Restes a modulo p zu berechnen? d. Beschreiben Sie einen effizienten randomisierten Algorithmus für die Bestimmung eines nichtquadratischen Restes modulo einer beliebigen Primzahl p, d. h. ein Element von Z∗p , das kein quadratischer Rest ist. Wie viele arithmetische Operationen benötigt Ihr Algorithmus im Mittel?
Kapitelbemerkungen Niven und Zuckerman [265] bieten eine exzellente Einführung in die elementare Zahlentheorie. Knuth [210] enthält eine gute Diskussion von Algorithmen zur Bestimmung des größten gemeinsamen Teilers sowie von anderen grundlegenden zahlentheoretischen Algorithmen. Bach [30] und Riesel [295] bieten aktuellere Übersichten über die algorithmische Zahlentheorie. Dixon [91] gibt einen Überblick über die Primfaktorzerlegung und Primzahltests. Der von Pomerance [280] herausgegebene Tagungsband enthält verschiedene hervorragende Übersichtsartikel. Bach und Shallit [31] haben einen exzellenten Überblick über die Grundlagen der algorithmischen Zahlentheorie vorgelegt. Knuth [210] diskutiert den Ursprung des Euklidischen Algorithmus. Dieser ist in Buch 7, Aussage 1 und 2 der Elemente des griechischen Mathematikers Euklid enthalten. Die Elemente wurden um 300 v. Chr. geschrieben. Euklids Beschreibung ist möglicherweise von einem auf Eudoxus, 375 v. Chr. zurückgehenden Algorithmus abgeleitet. Dem Euklidischen Algorithmus gebührt vermutlich die Ehre, der älteste nichttriviale Algorithmus zu sein. Diese Ehre könnte ihm höchstens ein Algorithmus für die Multiplikation streitig
Problemstellungen zu Kapitel 31
995
machen, der den alten Ägyptern bekannt war. Shallit [312] zeichnet die Geschichte der Analyse des Euklidischen Algorithmus nach. Knuth schreibt einen Spezialfall des Chinesischen Restsatzes (Theorem 31.27) dem chinesischen Mathematiker Sun-Ts˘ u zu, der irgendwann zwischen 200 v. Chr. und 200 n. Chr. lebte – die Lebensdaten sind ziemlich ungewiss. Der gleiche Spezialfall wurde von dem griechischen Mathematiker Nichomachus um 100 n. Chr. angegeben. Er wurde 1247 von Chhin Chiu-Shao verallgemeinert. Der chinesische Restsatz wurde in seiner vollen Allgemeingültigkeit 1734 von L. Euler formuliert und bewiesen. Der hier vorgestellte randomisierte Algorithmus für das Testen von Primzahlen geht auf Miller [255] und Rabin [289] zurück; er ist der schnellste bekannte randomisierte Algorithmus für das Testen von Primzahlen. Der Beweis von Theorem 31.39 ist eine leichte Anpassung eines von Bach [29] vorgeschlagenen Beweises. Ein Beweis für ein strengeres Ergebnis für die Prozedur Miller-Rabin wurde von Monier [258, 259] angegeben. Viele Jahre lang war der Primzahltest das klassische Beispiel eines Problems, bei dem Randomisierung notwendig erschien, um einen effizienten Algorithmus (mit polynomieller Laufzeit) zu erhalten. In 2002 überraschten jedoch Agrawal, Kayal und Saxema [4] jeden mit ihrem deterministischen Primzahltest-Algorithmus mit polynomieller Laufzeit. Bis dahin lief der schnellste bekannteste deterministische Primzahltest-Algorithmus, der auf Cohen und Lenstra [73] zurückgeht, auf der Eingabe n in Zeit (lg n)O(lg lg lg n) , was nur leicht superpolynomiell ist. Nichtsdestotrotz bleiben randomisierte Algorithmen zum Primzahltest für praktische Anwendungen effizienter und werden bevorzugt. Das Problem der Bestimmung großer „zufälliger“ Primzahlen wird in einem Artikel von Beauchemin, Brassard, Crépeau, Goutier und Pomerance [36] diskutiert. Das Konzept eines Kryptosystems mit öffentlichen Schlüsseln geht auf Diffie und Hellman [87] zurück. Das RSA-Kryptosystem wurde 1977 von Rivest, Shamir und Adleman [296] vorgeschlagen. Seit diesem Zeitpunkt ist das Gebiet der Kryptographie aufgeblüht. Unser Verständnis des RSA-Systems hat sich vertieft und moderne Implementierungen verwenden wesentliche Verfeinerungen der hier vorgestellten grundlegenden Methoden. Außerdem wurden viele neue Verfahren entwickelt, um zu beweisen, dass Kryptosysteme sicher sind. Beispielsweise zeigen Goldwasser und Micali [142], dass Randomisierung ein effektives Werkzeug beim Entwurf sicherer Kryptosysteme mit öffentlichen Schlüsseln sein kann. Goldwasser, Micali und Rivest [143] stellen ein System für digitale Signaturen vor, für das sie zeigen können, dass jede denkbare Art von Fälschung so schwierig ist wie das Faktorisieren von Primzahlen. Menezes u. a. [254] bieten einen Überblick über die angewandte Kryptographie. Die ρ-Heuristik für die Zerlegung ganzer Zahlen in Primfaktoren wurde von Pollard [277] eingeführt. Die hier vorgestellte Version ist eine Variante, die von Brent [56] vorgeschlagen wurde. Die besten Algorithmen für das Faktorisieren großer Primzahlen haben eine Laufzeit, die in etwa exponentiell mit der Kubikwurzel der Länge der zu faktorisierenden Zahl n wächst. Der allgemeine zahlentheoretische Algorithmus für das Faktorisieren mit einem Sieb, entwickelt von Buhler u. a. [57] als eine Erweiterung der Ideen von Pollard [278] und Lenstra u. a. [232] und weiterentwickelt von Coppersmith [77] und anderen, ist vielleicht der effizienteste derartige Algorithmus für große Eingaben. Obwohl eine strenge Analyse
996
31 Zahlentheoretische Algorithmen
für diesen Algorithmus schwierig ist, kann man unter vernünftigen Voraussetzungen α 1−α eine Abschätzung der Laufzeit von L(1/3, n)1,902+o(1) mit L(α, n) = e(ln n) (ln ln n) ableiten. Die auf Lenstra [233] zurückgehende Methode der elliptischen Kurven kann für manche Eingaben effektiver sein als die Methode des Siebs, da sie wie Pollards ρ-Methode kleine Primfaktoren p sehr schnell finden kann. Mit dieser Methode ist die Zeit zum Auffinden √ von p schätzungsweise L(1/2, p) 2+o(1) .
32
String-Matching
Textverarbeitungsprogramme haben oft alle Vorkommen eines Musters in einem Text zu finden. Typischerweise handelt es sich bei dem Text um ein zu editierendes Dokument und das gesuchte Muster ist ein spezielles Wort, das vom Nutzer vorgegeben ist. Effiziente Algorithmen zur Lösung dieses Problems – wir nennen es „String-Matching“ (übersetzt etwa „Zeichenketten-Suchproblem“) – können sehr zur Attraktivität eines Textverarbeitungsprogramms beitragen. Neben vielen anderen Anwendungen suchen String-Matching-Algorithmen nach speziellen Mustern in DNA-Sequenzen. InternetSuchmaschinen benutzen ebenfalls String-Matching-Algorithmen, um Webseiten zu finden, die zu einer Anfrage relevant sind. Wir formulieren das String-Matching-Problem folgendermaßen. Wir setzen voraus, dass der Text in einem Feld T [1 . . n] der Länge n und das Textmuster in einem Feld P [1 . . m] der Länge m ≤ n enthalten ist. Darüber hinaus setzen wir voraus, dass die Elemente von P und T Zeichen aus einem endlichen Alphabet Σ sind. Beispielsweise könnten wir mit Σ = {0,1} oder Σ = {a, b, . . . , z} arbeiten. Die Felder P und T werden häufig als aus Zeichen bestehende Strings bezeichnet. Wie in Abbildung 32.1 illustriert, sagen wir, dass ein Muster P mit der Verschiebung s innerhalb eines Textes T vorkommt (oder äquivalent dazu, dass das Muster P im Text an der Position s + 1 beginnend auftritt), wenn 0 ≤ s ≤ n − m und T [s + 1 . . s + m] = P [1 . . m] ist, d. h. wenn für 1 ≤ j ≤ m die Gleichung T [s + j] = P [j] gilt. Wenn P mit einer Verschiebung s in T vorkommt, dann bezeichnen wir s als eine gültige Verschiebung ; anderenfalls bezeichnen wir s als eine ungültige Verschiebung. Das String-Matching-Problem besteht darin, alle gültigen Verschiebungen zu bestimmen, mit denen ein gegebenes Muster P in einem gegebenen Text T auftritt. Abgesehen von dem naiven Algorithmus, den wir in Abschnitt 32.1 betrachten, führt jeder String-Matching-Algorithmus in diesem Kapitel einige vom Muster abhängige Vorverarbeitungsschritte aus und bestimmt alle gültigen Verschiebungen; wir werden diese
Text T Muster P
a b c a b a a b c a b a c s=3
a b a a
Abbildung 32.1: Das String-Matching-Problem. Das Ziel besteht darin, alle Stellen im Text T = abcabaabcabac zu finden, an denen das Muster P = abaa vorkommt. Das Muster tritt im Text nur einmal mit einer Verschiebung s = 3 auf, die wir als eine gültige Verschiebung bezeichnen. Eine vertikale Linie verbindet jedes Zeichen des Musters mit dem jeweiligen übereinstimmenden Zeichen im Text und alle übereinstimmenden Zeichen sind schattiert dargestellt.
998 Algorithmus naiv Rabin-Karp endlicher Automat Knuth-Morris-Pratt
32 String-Matching Vorverarbeitungszeit 0 Θ(m) O(m |Σ|) Θ(m)
Matchingzeit O((n − m + 1)m) O((n − m + 1)m) Θ(n) Θ(n)
Abbildung 32.2: Die String-Matching-Algorithmen aus diesem Kapitel mit ihren Vorverarbeitungs- und Matchingzeiten.
letzte Phase als „Matching“ bezeichnen. Abbildung 32.2 zeigt für jeden Algorithmus aus diesem Kapitel die Vorverarbeitungs- und Matchingzeiten. Die Gesamtlaufzeit jedes Algorithmus ergibt sich aus der Summe der beiden Zeiten. In Abschnitt 32.2 stellen wir einen interessanten String-Matching-Algorithmus vor, der auf Rabin und Karp zurückgeht. Obwohl seine Laufzeit im schlechtesten Fall Θ((n − m + 1)m) nicht besser als die der naiven Methode ist, arbeitet der Rabin-Karp-Algorithmus im Mittel und in der Praxis viel schneller. Er lässt sich auch sehr gut auf andere String-MatchingProbleme übertragen. In Abschnitt 32.3 beschreiben wir anschließend einen StringMatching-Algorithmus, der zuerst einen endlichen Automaten konstruiert, der speziell für die Suche nach dem gegebenen Muster P im Text entworfen ist. Dieser Algorithmus benötigt O(m |Σ|) Vorverarbeitungszeit, aber nur Θ(n) Matchingzeit. Abschnitt 32.4 stellt den ähnlichen, aber viel intelligenteren Knuth-Morris-Pratt-Algorithmus (abkürzend auch KMP-Algorithmus genannt) vor; der KMP-Algorithmus besitzt die gleiche Matchingzeit Θ(n) und reduziert die Vorverarbeitungszeit auf nur Θ(m).
Bezeichnung und Terminologie Wir bezeichnen mit Σ∗ die Menge aller Strings endlicher Länge, die aus den Zeichen des Alphabets Σ gebildet werden können. In diesem Kapitel betrachten wir ausschließlich Strings endlicher Länge. Der mit ε bezeichnete leere String der Länge 0 gehört ebenfalls zu Σ∗ . Die Länge eines Strings x wird mit |x| bezeichnet. Die mit xy bezeichnete Verknüpfung zweier Strings x und y hat die Länge |x| + |y| und besteht aus den Zeichen in x, gefolgt von den Zeichen in y. Wir sagen, dass ein String w ein Präfix eines Strings x ist, was wir durch w < x symbolisieren, wenn x = wy für einen String y ∈ Σ∗ gilt. Beachten Sie, dass im Falle w < x die Ungleichung |w| ≤ |x| gilt. Analog sagen wir, dass ein String w ein Suffix eines Strings x ist, was wir mit w = x darstellen, wenn x = yw für einen String y ∈ Σ∗ gilt. Wie bei einem Präfix, folgt auch hier |w| ≤ |x| aus w = x. Es gilt beispielsweise ab < abcca und cca = abcca. Der leere String ε ist sowohl Suffix als auch Präfix jedes Strings. Für alle Strings x und y und jedes Zeichen a gilt x = y genau dann, wenn xa = ya ist. Wir stellen außerdem fest, dass < und = transitive Relationen sind. Das folgende Lemma wird später hilfreich sein. Lemma 32.1: (Überlappende Suffixe) Seien x, y und z Strings, für die x = z und y = z gilt. Ist |x| ≤ |y|, so gilt x = y. Ist |x| ≥ |y|, so gilt y = x. Ist |x| = |y|, so gilt x = y.
32.1 Der naive String-Matching-Algorithmus x z
999
x
x
z
z
y
y
x y
x
x y
(a)
y
(b)
y (c)
Abbildung 32.3: Ein graphischer Beweis für Lemma 32.1. Wir setzen voraus, dass x = z und y = z gilt. Die drei Teile der Abbildung illustrieren die drei Fälle des Lemmas. Vertikale Linien verbinden jeweils übereinstimmende Abschnitte (schattiert gezeichnet) der Strings. (a) Ist |x| ≤ |y|, so gilt x = y. (b) Ist |x| ≥ |y|, so gilt y = x. (c) Ist |x| = |y|, so gilt x = y.
Beweis: Siehe Abbildung 32.3 für einen graphischen Beweis.
Um eine kompaktere Notation zu haben, bezeichnen wir den aus k Zeichen bestehenden Präfix P [1 . . k] des Musters P [1 . . m] mit Pk . Hiermit gilt P0 = ε und Pm = P = P [1 . . m]. Analog hierzu bezeichnen wir den aus k Zeichen bestehenden Präfix des Textes T mit Tk . Unter Verwendung dieser Bezeichnungen können wir das String-MatchingProblem folgendermaßen spezifizieren: Wir wollen alle Verschiebungen s im Bereich 0 ≤ s ≤ n − m finden, für die P = Ts+m gilt. In unserem Pseudocode erlauben wir den Vergleich zweier Strings gleicher Länge als eine primitive Operation. Wenn wir die Zeichen von links nach rechts miteinander vergleichen und stoppen, wenn wir eine Ungleichheit feststellen, dann setzen wir voraus, dass die durch diesen Test benötigte Zeit eine lineare Funktion in der Anzahl der entdeckten übereinstimmenden Zeichen ist. Um genauer zu sein, wir setzen voraus, dass der Test „x = = y“ die Laufzeit Θ(t + 1) hat, wobei t die Länge des längsten Strings z ist, für den z < x und z < y gilt. (Wir ziehen es vor, Θ(t + 1) anstelle von Θ(t) zu schreiben, um die Fälle, in denen t = 0 gilt, mit einzuschließen. Der Vergleich des ersten Zeichens liefert keine Übereinstimmung, aber er benötigt einen positiven Zeitaufwand zum Ausführen dieser Operation.)
32.1
Der naive String-Matching-Algorithmus
Der einfache Algorithmus bestimmt mithilfe einer Schleife alle gültigen Verschiebungen. Die Schleife überprüft für alle n − m + 1 möglichen Werte von s, ob die Bedingung
1000
32 String-Matching
P [1 . . m] = T [s + 1 . . s + m] erfüllt ist. Naive-String-Matcher(T, P ) 1 n = T.l¨a nge 2 m = P.l¨a nge 3 for s = 0 to n − m 4 if P [1 . . m] = = T [s + 1 . . s + m] 5 print “Das Muster tritt auf mit der Verschiebung” s Abbildung 32.4 stellt die einfache String-Matching-Prozedur als ein Gleiten einer „Suchmaske“ (die das Muster enthält) über den Text dar, wobei sie sich merkt, bei welchen Verschiebungen die Zeichen in der Suchmaske gleich den Zeichen im Text sind. Die for-Schleife der Zeilen 3–5 betrachtet jede mögliche Verschiebung explizit. Der Test in Zeile 4 überprüft, ob die aktuelle Verschiebung gültig ist; dieser Test erfolgt implizit über eine Schleife, in der jeweils ein Zeichenpaar auf Gleichheit getestet wird, bis alle Positionen erfolgreich überprüft sind oder eine Nichtübereinstimmung gefunden ist. Zeile 5 druckt jede gültige Verschiebung s. Die Prozedur Naive-String-Matcher benötigt Zeit O((n − m + 1)m) und diese Schranke ist im schlechtesten Fall scharf. Betrachten Sie beispielsweise als Text den Zeichenstring an (der String, der aus n a’s besteht) und das Muster am . Für jeden der n − m + 1 möglichen Verschiebungswerte s muss die implizite Schleife in Zeile 4 m Vergleiche korrespondierender Zeichen ausführen, um die Gültigkeit der jeweiligen Verschiebung festzustellen. Die Laufzeit im schlechtesten Fall ist folglich in Θ((n−m+1)m), was im Falle von m = n/2 der Laufzeit Θ(n2 ) entspricht. Da sie keine Vorverarbeitung benötigt, ist die Laufzeit von Naive-String-Matcher gleich ihrer Matchingzeit. Wie wir sehen werden, ist Naive-String-Matcher für dieses Problem keine optimale Prozedur. Wir werden in der Tat in diesem Kapitel sehen, dass der Knuth-MorrisPratt-Algorithmus im schlechtesten Fall wesentlich besser ist. Die Prozedur NaiveString-Matcher ist ineffizient, weil sie die Information, die sie über den Text bei der Behandlung eines Wertes von s gewonnen hat, bei der Bearbeitung anderer Werte von s vollständig vergisst. Solche Informationen können jedoch ziemlich wertvoll sein. Wenn beispielsweise P = aaab gilt und wir herausfinden, dass s = 0 gültig ist, dann ist keine der Verschiebungen 1, 2 oder 3 gültig, weil T [4] = b gilt. In den folgenden Abschnitten untersuchen wir verschiedene Möglichkeiten, um diese Art von Information auszunutzen.
Übungen 32.1-1 Geben Sie die Vergleiche an, die Naive-String-Matcher für das Muster P = 0001 in dem Text T = 000010001010001 ausführt. 32.1-2 Setzen Sie voraus, dass alle Zeichen im Muster P verschieden sind. Zeigen Sie, wie Sie die Prozedur Naive-String-Matcher so beschleunigen können, dass sie auf einem Text T mit n Zeichen in Zeit O(n) läuft. 32.1-3 Setzen Sie voraus, dass das Muster P und der Text T zufällig gewählte Strings der Länge m bzw. n sind. Die Zeichen stammen aus dem d-nären Alphabet
32.1 Der naive String-Matching-Algorithmus
s=0
a a b
a c a a b c
a c a a b c
a c a a b c
s=1
s=2
a a b
(a)
1001
(b)
a a b (c)
a c a a b c s=3
a a b (d)
Abbildung 32.4: Die Arbeitsweise der Prozedur Naive-String-Matcher für das Muster P = aab und den Text T = acaabc. Wir können uns das Muster P als eine „Suchmaske“ vorstellen, die wir über den Text gleiten lassen. (a)-(d) Die vier aufeinander folgenden, von der einfachen Prozedur geprüften Ausrichtungen der Suchmaske. In jedem Teil verbinden vertikale Linien übereinstimmende Bereiche (schattiert dargestellt) und eine gezackte Linie verbindet das jeweils erste gefundene Zeichenpaar, das nicht übereinstimmt, falls ein solches existiert. Der Algorithmus findet ein Vorkommen des Musters im Text, nämlich bei der Verschiebung s = 2, wie in Teil (c) dargestellt.
Σd = {0, 1, . . . , d − 1} mit d ≥ 2. Zeigen Sie, dass die erwartete Anzahl von Vergleichen – wir betrachten hier Vergleiche von jeweils einem Zeichen mit einem anderen –, die die implizite Schleife in Zeile 4 des einfachen Algorithmus durchgeführt, durch (n − m + 1)
1 − d−m ≤ 2(n − m + 1) 1 − d−1
gegeben ist, wenn wir über alle Ausführungen der Schleife summieren. (Setzen Sie voraus, dass der einfache Algorithmus mit dem Vergleichen der Zeichen bei einer gegebenen Verschiebung aufhört, wenn er eine Nichtübereinstimmung oder eine Übereinstimmung des gesamten Musters findet.) Somit ist der einfache Algorithmus für zufällig gewählte Strings recht effizient. 32.1-4 Setzen Sie voraus, dass wir erlauben, dass das Muster P Lückenzeichen ♦ enthält, die einem beliebigen String (sogar einem der Länge 0) entsprechen können. Zum Beispiel erscheint das Muster ab♦ba♦c im Text cabccbacbacab als cc CDEF ba CDEF cba CDEF c ab c CDEF ab CDEF ab
♦
ba
c
♦
und als ba CDEF CDEF c ab . c CDEF ab ccbac C DE F CDEF ab
♦
ba
♦
c
Beachten Sie, dass Lückenzeichen in beliebiger Anzahl im Muster vorkommen können, im Text aber überhaupt nicht. Geben Sie einen Algorithmus mit polynomieller Laufzeit an, der feststellt, ob ein solches Muster P in einem gegebenen Text vorkommt. Analysieren Sie die Laufzeit Ihres Algorithmus.
1002
32.2
32 String-Matching
Der Rabin-Karp-Algorithmus
Rabin und Karp schlugen einen String-Matching-Algorithmus vor, der in der Praxis gut arbeitet und außerdem zu Algorithmen zur Lösung verwandter Probleme, wie beispielsweise des zweidimensionalen String-Matching, verallgemeinert werden kann. Der Rabin-Karp-Algorithmus benötigt Θ(m) Vorverarbeitungszeit und seine Laufzeit ist im schlechtesten Fall Θ((n−m+1)m). Unter bestimmten Voraussetzungen ist seine mittlere Laufzeit jedoch viel besser. Dieser Algorithmus basiert auf elementaren zahlentheoretischen Begriffen, wie beispielsweise der Gleichheit zweier Zahlen modulo einer dritten Zahl. Sie können die relevanten Definitionen in Abschnitt 31.1 nachlesen. Lassen Sie uns in den Beispielen voraussetzen, dass Σ = {0, 1, 2, . . . , 9} ist, sodass jedes Zeichen eine Dezimalziffer ist. (Im allgemeinen Fall können wir voraussetzen, dass jedes Zeichen eine zur Basis d dargestellte Ziffer ist.) Wir können also einen aus k Zeichen bestehenden String als k-stellige Dezimalzahl auffassen. Der String 31415 entspricht also der Dezimalzahl 31415. Da wir die Eingabezeichen sowohl als graphische Zeichen als auch als Dezimalziffern interpretieren können, finden wir es in diesem Abschnitt zweckmäßig, die Eingabezeichen als Dezimalziffern anzugeben und benutzen dabei den Standardschrifttyp. Ist ein Muster P [1 . . m] gegeben, dann bezeichnen wir mit p die dazugehörige Dezimalzahl. Analog bezeichnen wir für einen gegebenen Text T [1 . . n] und s = 0, 1, . . . , n − m mit ts den Dezimalwert des Teilstrings T [s + 1 . . s + m] der Länge m. Offenbar gilt ts = p genau dann, wenn T [s + 1 . . s + m] = P [1 . . m] gilt. Folglich ist s genau dann eine zulässige Verschiebung, wenn ts = p gilt. Wenn wir p in Zeit Θ(m) und alle Werte ts in der Gesamtzeit Θ(n − m + 1)1 berechnen könnten, dann könnten wir alle gültigen Verschiebungen s in Zeit Θ(m) + Θ(n − m + 1) = Θ(n) bestimmen, indem wir p mit jedem der ts -Werte vergleichen. (Für den Moment sollten Sie sich keine Gedanken darüber machen, dass p und die ts -Werte möglicherweise sehr große Zahlen sein können.) Wir können p in Zeit Θ(m) unter Verwendung des Horner-Schemas berechnen (siehe Abschnitt 30.1): p = P [m] + 10 P [m − 1] + 10 P [m − 2] + · · · + 10(P [2] + 10P [1]) · · · . Entsprechend können wir den Wert t0 aus T [1 . . m] in Zeit Θ(m) berechnen. Zur Berechnung der verbleibenden Werte t1 , t2 , . . . , tn−m in Zeit Θ(n − m) haben wir nur zu sehen, dass wir ts+1 aus ts in konstanter Zeit berechnen können, da (32.1) ts+1 = 10 ts − 10m−1 T [s + 1] + T [s + m + 1] gilt. Die Subtraktion von 10m−1 T [s + 1] entfernt die höchstwertigste Ziffer aus ts , die Multiplikation mit 10 shiftet die Zahl um eine Ziffernstelle nach links und die Addition von T [s + m + 1] fügt die neue niederwertigste Ziffer ein. Ist beispielsweise m = 5 und 1 Wir schreiben Θ(n − m + 1) anstelle von Θ(n − m), da s genau n − m + 1 verschiedene Werte annimmt. Die „+1“ ist in asymptotischem Sinne signifikant. Im Falle m = n benötigt die Berechnung des einzelnen Wertes ts Zeit Θ(1), nicht Zeit Θ(0).
32.2 Der Rabin-Karp-Algorithmus
1003
ts = 31415, dann wollen wir die höchstwertige Ziffer T [s + 1] = 3 entfernen und die neue Ziffer mit niedrigster Wertigkeit (setzen Sie voraus, dass es T [s + 5 + 1] = 2 ist) einfügen, um ts+1 = 10 · (31415 − 10000 · 3) + 2 = 14152 zu erhalten. Wenn wir die Konstante 10m−1 vorberechnen (was wir in Zeit O(lg m) tun können, indem wir die Techniken aus Abschnitt 31.6 anwenden, wenngleich in dieser Anwendung auch eine einfache Methode mit Zeit O(m) ausreicht), dann benötigt jede Ausführung der Gleichung (32.1) eine konstante Anzahl arithmetischer Operationen. Dadurch können wir p in Zeit Θ(m) berechnen und alle t0 , t1 , . . . , tn−m in Zeit Θ(n−m+1). Demnach können wir alle Vorkommen des Musters P [1 . . m] in dem Text T [1 . . n] mit einer Vorverarbeitungszeit von Θ(m) und einer Matchingzeit von Θ(n − m + 1) finden. Bis jetzt haben wir absichtlich ein Problem übersehen: p und ts können zu groß sein, um in üblicher Art und Weise mit ihnen arbeiten zu können. Wenn P aus m Zeichen besteht, dann ist es nicht angemessen, wenn wir voraussetzen, dass jede arithmetische Operation auf dem Wert p (der m Ziffern lang ist) „konstante Zeit“ benötigt. Zum Glück können wir dieses Problem einfach lösen, wie dies Abbildung 32.5 illustriert: Wir berechnen p und die ts -Werte modulo eines passenden Modulus q. Wir können p modulo q in Zeit Θ(m) berechnen und alle Werte ts modulo q in Zeit Θ(n − m + 1). Wenn wir für den Wert q eine Primzahl wählen, für die 10q in nur ein Speicherwort passt, dann können wir alle notwendigen Berechnungen mithilfe der Arithmetik einfacher Genauigkeit ausführen. Im Allgemeinen wählen wir q bei einem d-nären Alphabet {0, 1, . . . , d − 1} so, dass dq in ein Speicherwort passt und passen die Rekursionsgleichung (32.1) so an, dass sie modulo q arbeitet, d. h. zu ts+1 = d(ts − T [s + 1]h) + T [s + m + 1] mod q (32.2) wird, wobei h ≡ dm−1 (mod q) die Wertigkeit der Ziffer „1“ an der höchstwertigsten Position eines m-stelligen Textfensters ist. Die Lösung, modulo q zu arbeiten, ist jedoch nicht perfekt, da aus ts ≡ p (mod q) nicht ts = p folgt. Auf der anderen Seite gilt mit Sicherheit ts = p, wenn ts ≡ p (mod q) ist, sodass die Verschiebung s ungültig ist. Wir können den Test ts ≡ p (mod q) folglich als einen schnellen heuristischen Test verwenden, um ungültige Verschiebungen s auszuschließen. Jede beliebige Verschiebung s, für die ts ≡ p (mod q) gilt, muss weiter getestet werden, um zu sehen, ob s tatsächlich gültig ist, oder ob ein unechter Treffer vorliegt. Dieser zusätzliche Test überprüft explizit die Bedingung P [1 . . m] = T [s + 1 . . s + m]. Wenn q hinreichend groß ist, können wir hoffen, dass unechte Treffer hinreichend selten vorkommen und die Kosten dieses zusätzlichen Tests somit gering sind. Die folgende Prozedur setzt diese Ideen um. Die Eingaben der Prozedur sind der Text T , das Muster P , die zu verwendende Basis (Radix) d (die gewöhnlich als |Σ| gewählt wird) und die zu verwendende Primzahl q.
1004
2
3
32 String-Matching
5
9
0
2
3
1
4
1
5
2
6
7
3
9
9
2
1
mod 13 7 (a)
1
2
3
4
5
6
7
8
9
10 11 12 13 14 15 16 17 18 19
2
3
5
9
0
2
3
1
4
1
5
… 8
9
3 11 0
2
6
7
3
9
… 1
7
8
4
gültige Übereinstimmung
9
…
2
1 mod 13
5 10 11 7 9 11 unechter Treffer
(b)
alte Ziffer höchster Wertigkeit
3
1
neue Ziffer niedrigster Wertigkeit
4
1
7
8
5
alte Ziffer höchster Wertigkeit
Verschiebung
neue Ziffer niedrigster Wertigkeit
14152 ≡ (31415 – 3·10000)·10 + 2 (mod 13) ≡ (7 – 3·3)·10 + 2 (mod 13) ≡ 8 (mod 13)
2
(c) Abbildung 32.5: Der Rabin-Karp-Algorithmus. Jedes Zeichen ist eine Dezimalziffer und wir berechnen Werte modulo 13. (a) Ein String. Ein Fenster der Länge 5 ist schattiert gezeichnet. Der numerische Wert der schattierten Zahl, die modulo 13 berechnet wird, ergibt den Wert 7. (b) Derselbe String mit den modulo 13 berechneten Werten für jede mögliche Position des fünfstelligen Fensters. Wir gehen davon aus, dass das Muster P gleich 31415 ist, und suchen nach einem Fenster, dessen Wert modulo 13 gleich 7 ist, da 31415 ≡ 7 (mod 13) gilt. Der Algorithmus findet zwei solche Fenster, die in der Abbildung schattiert dargestellt sind. Das erste, das an der Textposition 7 beginnt, ist tatsächlich eine Stelle, an der das Muster vorkommt, während das zweite, das an der Position 13 beginnt, ein unechter Treffer ist. (c) Wie wir den Wert eines Fensters in konstanter Zeit berechnen, wenn wir den Wert des vorhergehenden Fensters kennen. Das erste Fenster besitzt den Wert 31415. Indem wir die höchstwertigste Ziffer 3 löschen, dann um eine Stelle nach links shiften (Multiplikation mit 10) und schließlich die neue niederwertigste Ziffer 2 addieren, erhalten wir den neuen Wert 14152. Da alle Berechnungen modulo 13 ausgeführt werden, ist der Wert des ersten Fensters gleich 7 und der Wert des neuen Fensters gleich 8.
32.2 Der Rabin-Karp-Algorithmus
1005
Rabin-Karp-Matcher(T, P, d, q) 1 n = T.l¨a nge 2 m = P.l¨a nge 3 h = dm−1 mod q 4 p =0 5 t0 = 0 6 for i = 1 to m // Vorverarbeitung 7 p = (d p + P [i]) mod q 8 t0 = (d t0 + T [i]) mod q 9 for s = 0 to n − m // Test auf Übereinstimmung 10 if p = = ts 11 if P [1 . . m] = = T [s + 1 . . s + m] 12 print “Das Muster tritt auf mit Verschiebung” s 13 if s < n − m 14 ts+1 = (d(ts − T [s + 1]h) + T [s + m + 1]) mod q Die Prozedur Rabin-Karp-Matcher arbeitet folgendermaßen. Alle Zeichen werden als Zahlen der Basis d interpretiert. Der Index von t dient nur dem besseren Verständnis; das Programm arbeitet korrekt, wenn alle Indizes weggelassen werden. Zeile 3 initialisiert h mit der Wertigkeit einer Ziffer an der höchstwertigsten Stelle eines m-stelligen Fensters. Die Zeilen 4–8 berechnen p als den Wert von P [1 . . m] mod q und t0 als den Wert von T [1 . . m] mod q. Die for-Schleife der Zeilen 9–14 wird für alle möglichen Verschiebungen s wiederholt, wobei folgende Invariante erhalten bleibt: Immer wenn Zeile 10 ausgeführt wird, gilt ts = T [s + 1 . . s + m] mod q. Gilt p = ts in Zeile 10 (ein „Treffer“), so testet Zeile 11, ob P [1 . . m] = T [s + 1 . . s + m] gilt, um auszuschließen, dass ein unechter Treffer vorliegt. Zeile 12 druckt jede gefundene gültige Verschiebung. Wenn s < n − m gilt (was in Zeile 13 getestet wird), dann wird die for-Schleife mindestens noch einmal ausgeführt und so wird zuerst Zeile 14 ausgeführt, um sicherzustellen, dass die Schleifeninvariante gilt, wenn wir zu Zeile 10 zurückkommen. Zeile 14 berechnet mithilfe von Gleichung (32.2) den Wert ts+1 mod q in konstanter Zeit direkt aus dem Wert ts mod q. Die Prozedur Rabin-Karp-Matcher benötigt eine Vorverarbeitungszeit von Θ(m) und ihre Matchingzeit ist im schlechtesten Fall in Θ((n−m+1)m), weil der Rabin-KarpAlgorithmus (wie der einfache String-Matching-Algorithmus) jede gültige Verschiebung explizit prüft. Ist P = am und T = an , dann benötigen die Tests Zeit Θ((n − m + 1)m), da jede der n − m + 1 möglichen Verschiebungen gültig ist. Bei vielen Anwendungen erwarten wir wenige gültige Verschiebungen – vielleicht nur eine konstante Anzahl c; bei solchen Anwendungen ist die erwartete Matchingzeit des Algorithmus nur O((n − m + 1) + cm) = O(n + m) plus die Zeit zur Bearbeitung unechter Treffer. Wir können eine heuristische Analyse auf der Voraussetzung aufbauen, dass die Reduzierung der Werte modulo q wie eine zufällige Abbildung von Σ∗ nach Zq arbeitet. (Siehe die Diskussionen zur Verwendung der Division für das Hashing in Abschnitt 11.3.1. Es ist schwierig, eine solche Voraussetzung zu formalisieren und zu
1006
32 String-Matching
beweisen, obwohl eine praktikable Methode darin besteht, vorauszusetzen, dass q zufällig aus der Menge der ganzen Zahlen der geeigneten Größe gewählt wird. Wir werden diese Formalisierung hier aber nicht weiter verfolgen.) Dann können wir erwarten, dass die Anzahl der unechten Treffer O(n/q) ist, da wir die Wahrscheinlichkeit, dass ein beliebiges ts äquivalent zu p modulo q ist, mit 1/q abschätzen können. Da es O(n) Positionen gibt, an denen der Test von Zeile 10 fehlschlägt und wir Zeit O(m) für jeden Treffer verbrauchen, ist die vom Rabin-Karp-Algorithmus benötigte Matchingzeit O(n) + O(m(v + n/q)) , wobei v die Anzahl der gültigen Verschiebungen ist. Diese Laufzeit ist in O(n), wenn v = O(1) gilt, und wir q ≥ m wählen. Das heißt, wenn die erwartete Anzahl gültiger Verschiebungen klein ist (O(1)) und wir die Primzahl q größer als die Länge des Musters wählen, dann können wir erwarten, dass die Rabin-Karp-Prozedur eine Matchingzeit von nur O(n + m) benötigt. Da m ≤ n gilt, ist diese erwartete Matchingzeit in O(n).
Übungen 32.2-1 Setzen Sie voraus, dass wir modulo q = 11 arbeiten. Wie vielen unechten Treffern begegnet Rabin-Karp-Matcher im Text T = 3141592653589793, wenn wir nach dem Muster P = 26 suchen? 32.2-2 Wie würden Sie die Rabin-Karp-Methode erweitern, um ein Algorithmus zu erhalten, der überprüft, ob ein Text ein Muster aus einer gegebenen Menge von k Mustern enthält? Setzen Sie zunächst voraus, dass alle k Muster die gleiche Länge haben. Verallgemeinern Sie dann Ihre Lösung auf den Fall, dass die Muster unterschiedliche Längen haben können. 32.2-3 Zeigen Sie, wie wir die Rabin-Karp-Methode so erweitern können, dass sie nach einem m× m-Muster in einem n× n-Feld von Zeichen sucht. (Das Muster kann vertikal und horizontal verschoben, aber nicht gedreht werden.) 32.2-4 Alice besitzt eine Kopie einer langen n-Bit-Datei A = an−1 , an−2 , . . . , a0 und Bob hat ebenfalls eine n-Bit-Datei B = bn−1 , bn−2 , . . . , b0 . Alice und Bob wollen wissen, ob ihre Dateien identisch sind. Um die Übertragung der gesamten Datei A oder B zu vermeiden, benutzen sie den folgenden schnellen, probabilistischen Test. Sie wählen gemeinsam eine Primzahl q > 1000n und würfeln zufällig eine ganze Zahl x aus der Menge {0, 1, . . . , q − 1}. Dann wertet Alice 2n−1 3 i A(x) = ai x mod q i=0
aus, und Bob B(x). Beweisen Sie, dass es im Fall A = B eine Wahrscheinlichkeit von höchstens 1 : 1000 gibt, dass A(x) = B(x) gilt, während A(x) zwingend gleich B(x) ist, wenn die Dateien A und B identisch sind. (Hinweis: Siehe Übung 31.4–4.)
32.3 String-Matching mit endlichen Automaten
32.3
1007
String-Matching mit endlichen Automaten
Viele String-Matching-Algorithmen erzeugen einen endlichen Automaten – eine einfache Maschine zur Verarbeitung von Information –, der einen Textstring T nach allen Vorkommen eines Musters P durchsucht. Dieser Abschnitt stellt eine Methode zur Konstruktion eines solchen Automaten vor. Diese String-Matching-Automaten sind sehr effizient. Sie untersuchen jedes Textzeichen genau einmal, wobei eine konstante Zeit pro Textzeichen benötigt wird. Die verwendete Matchingzeit – nach der Vorverarbeitung des Musters zur Konstruktion des Automaten – ist deshalb Θ(n). Die Zeit zur Konstruktion des Automaten kann jedoch lang sein, wenn Σ groß ist. Abschnitt 32.4 beschreibt einen geschickten Weg, dem Problem beizukommen. Wir beginnen diesen Abschnitt mit der Definition eines endlichen Automaten. Wir untersuchen spezielle String-Matching-Automaten und zeigen, wie wir sie verwenden können, um Vorkommen eines Musters in einem Text zu finden. Am Ende dieses Abschnitts werden wir zeigen, wie wir einen String-Matching-Automat für ein gegebenes Eingabemuster konstruieren können.
Endliche Automaten Ein endlicher Automat M (siehe Abbildung 32.6 zur Ilustration) ist ein 5-Tupel (Q, q0 , A, Σ, δ), wobei • Q eine endliche Menge von Zuständen ist, • q0 ∈ Q der Startzustand ist, • A ⊆ Q eine ausgezeichnete Menge von akzeptierenden Zuständen ist, • Σ ein endliches Eingabealphabet ist, und • δ eine Funktion von Q × Σ in Q ist, die Übergangsfunktion von M . Der endliche Automat startet im Zustand q0 und liest ein Zeichen seines Eingabestrings pro Zeit ein. Wenn sich der Automat im Zustand q befindet und das Eingabezeichen a liest, wechselt er („macht einen Übergang“) vom Zustand q in den Zustand δ(q, a). Wenn sein aktueller Zustand q ein Element von A ist, dann sagen wir, dass die Maschine M den bis dahin gelesenen String akzeptiert hat. Eine nicht akzeptierte Eingabe wird als zurückgewiesen bezeichnet. Ein endlicher Automat M induziert eine Funktion φ, die als Endzustandsfunktion bezeichnet wird. Die Funktion bildet Σ∗ auf Q ab, wobei φ(w) der Zustand ist, in dem M sich befindet, nachdem der String w gelesen wurde. Folglich akzeptiert M einen String w genau dann, wenn φ(w) ∈ A gilt. Wir definieren die Funktion φ rekursiv unter Benutzung der Übergangsfunktion: φ(ε) = q0 , φ(wa) = δ(φ(w), a)
für w ∈ Σ∗ , a ∈ Σ .
1008
32 String-Matching
Eingabe Zustand a b 0 1 0 1 0 0 (a)
a b
0
1 a b (b)
Abbildung 32.6: Ein einfacher endlicher Automat mit zwei Zuständen und der Zustandsmenge Q = {0, 1}, dem Anfangszustand q0 = 0 und dem Eingabealphabet Σ = {a, b}. (a) Eine tabellarische Darstellung der Übergangsfunktion δ. (b) Ein äquivalentes Zustandsübergangsdiagramm. Der Zustand 1, der schwarz gekennzeichnet ist, ist der einzige akzeptierende Zustand. Gerichtete Kanten stellen Übergänge dar. Beispielsweise stellt die mit b gekennzeichnete Kante vom Zustand 1 zum Zustand 0 den Übergang δ(1, b) = 0 dar. Dieser Automat akzeptiert diejenigen Strings, die mit einer ungeraden Anzahl von a’s enden. Genauer gesagt, er akzeptiert einen String x genau dann, wenn x = yz gilt, wobei y gleich ε ist oder mit einem b endet und z = ak mit einem ungeraden k ist. Der Automat durchläuft beispielsweise bei der Eingabe abaaa die Zustandsfolge 0, 1, 0, 1, 0, 1 (einschließlich des Startzustandes), und folglich akzeptiert er diese Eingabe. Für die Eingabe abbaa durchläuft er die Zustandsfolge 0, 1, 0, 0, 1, 0 und weist so diese Eingabe zurück.
String-Matching-Automat Für ein gegebenes Muster P konstruieren wir in einem Vorverarbeitungsschritt einen String-Matching-Automaten, um ihn dann einzusetzen, um nach dem Textstring zu suchen. Abbildung 32.7 illustriert den Automaten für das Muster P = ababaca. Ab hier werden wir voraussetzen, dass P ein gegebenes festes Muster ist, und werden der Kürze wegen in unseren Notationen die Abhängigkeit von P nicht angeben. Um einen Matching-Automaten zu spezifizieren, der einem gegebenen Muster P [1 . . m] entspricht, definieren wir zunächst eine zu P gehörige Hilfsfunktion σ, die als die zu P gehörige Suffixfunktion bezeichnet wird. Die Funktion σ bildet Σ∗ auf die Menge {0, 1, . . . , m} so ab, dass σ(x) die Länge des längsten Präfixes von P ist, der auch ein Suffix von x ist: σ(x) = max {k : Pk = x} .
(32.3)
Die Suffixfunktion σ ist wohldefiniert, weil der leere String P0 = ε ein Suffix jedes Strings ist. Für das Muster P = ab beispielsweise gilt σ(ε) = 0, σ(ccaca) = 1 und σ(ccab) = 2. Für ein Muster P der Länge m gilt σ(x) = m genau dann, wenn P = x ist. Aus der Definition der Suffixfunktion folgt, dass σ(x) ≤ σ(y) gilt, wenn x = y ist. Wir definieren den String-Matching-Automaten für ein gegebenes Muster P [1 . . m] wie folgt: • Die Zustandsmenge Q ist {0, 1, . . . , m}. Der Startzustand q0 ist der Zustand 0 und der Zustand m ist der einzige akzeptierende Zustand. • Die Übergangsfunktion δ ist für jeden Zustand q und jedes Zeichen a definiert durch die Gleichung δ(q, a) = σ(Pq a) .
(32.4)
32.3 String-Matching mit endlichen Automaten
1009
Wir definieren δ(q, a) = σ(Pq a), da wir die Übersicht über den längsten Präfix des Musters P , der bislang mit dem Textstring T übereinstimmt, behalten wollen. Wir betrachten die zuletzt gelesenen Zeichen von T . Damit ein Teilstring von T – sagen wir der Teilstring, der bei T [i] endet – mit einem Präfix Pj von P übereinstimmen kann, muss dieser Präfix Pj ein Suffix von Ti sein. Setzen Sie voraus, dass q = φ(Ti ) ist, sodass sich der Automat nach dem Lesen von Ti im Zustand q befindet. Wir entwerfen die Übergangsfunktion δ so, dass die Zustandsnummer q uns die Länge des längsten Präfixes von P angibt, der mit einem Suffix von Ti übereinstimmt, d. h. für Zustand q gilt Pq = Ti und q = σ(Ti ). (Wenn immer q = m gilt, so stimmen alle m Zeichen von P mit einem Suffix von Ti überein und wir haben ein Vorkommen gefunden.) Da sowohl φ(Ti ) als auch σ(Ti ) gleich q sind, werden wir demzufolge (in dem Theorem 32.4 weiter unten) sehen, dass der Automat die folgende Invariante erfüllt: φ(Ti ) = σ(Ti ) .
(32.5)
Ist der Automat im Zustand q und liest er das nächste Zeichen T [i + 1] = a, so wollen wir, dass der Übergang zu dem Zustand führt, der zu dem längsten Präfix von P korrespondiert, der ein Suffix von Ti a ist, und dass dieser Zustand σ(Ti a) ist. Da Pq der längste Präfix von P ist, der ein Suffix von Ti ist, ist der längste Präfix von P , der ein Suffix von Ti a ist, nicht nur σ(Ti a) sondern auch σ(Pq a). (Lemma 32.3 auf Seite 1011 beweist, dass σ(Ti a) = σ(Pq a) gilt.) Wir wollen also, dass, wenn sich der Automat im Zustand q befindet, die Übergangsfunktion angewendet auf das Zeichen a den Automaten in den Zustand σ(Pq a) überführt. Es gibt zwei Fälle, die wir betrachten müssen. In dem ersten Fall ist a = P [q + 1], sodass das Zeichen a die Übereinstimmung mit dem Muster fortsetzt.; in diesem Fall gilt δ(q, a) = q + 1 und der Zustandsübergang erfolgt entlang des „Rückgrats“ des Automaten (das in Abbildung 32.7 durch die dunkel eingezeichneten Kanten dargestellt ist). In dem zweiten Fall ist a = P [q + 1], sodass mit a die Übereinstimmung mit dem Muster abbricht. In diesem Fall müssen wir einen kleineren Präfix von P finden, der auch ein Suffix von Ti ist. Da der Vorverarbeitungsschritt bei der Erzeugung des StringMatching-Automaten das Muster gegen sich selbst auf Übereinstimmung überprüft, identifiziert die Übergangsfunktion schnell den längsten solchen kleineren Präfix von P . Lassen Sie uns ein Beispiel anschauen. Für den String-Matching-Automaten aus Abbildung 32.7 gilt δ(5, c) = 6; dies illustriert den ersten Fall, in dem die Übereinstimmung weiter wächst. Um den zweiten Fall zu illustrieren, sollten Sie bemerken, dass für den Automaten aus Abbildung 32.7 δ(5, b) = 4 gilt. Wir machen diesen Übergang, da, wenn der Automat im Zustand q = 5 ein b liest, Pq b = ababab gilt und der längste Präfix von P , der auch ein Suffix von ababab ist, gleich P4 = abab ist. Um die Arbeitsweise eines String-Matching-Automaten zu verdeutlichen, geben wir nun ein einfaches effizientes Programm an, das das Verhalten eines solchen Automaten (dargestellt durch seine Übergangsfunktion δ) beim Suchen eines Musters P der Länge m in einem Eingabetext T [1 . . n] simuliert. Wie bei jedem String-Matching-Automaten für ein Muster der Länge m ist die Zustandsmenge Q die Menge {0, 1, . . . , m}, der Startzustand 0 und der einzige akzeptierende Zustand m.
1010
32 String-Matching
a 0
a
1
b
2
a
a
a
a 3
b
4
a
5
c
6
a
7
b b (a) Zustand 0 1 2 3 4 5 6 7
Eingabe a b c 1 0 0 1 2 0 3 0 0 1 4 0 5 0 0 1 4 6 7 0 0 1 2 0 (b)
P a b a b a c a
i T [i] Zustand φ(Ti )
— 1 2 3 4 5 6 7 8 9 10 11 — a b a b a b a c a b a 0 1 2 3 4 5 4 5 6 7 2 3 (c)
Abbildung 32.7: (a) Ein Zustandsübergangsdiagramm für den String-Matching-Automaten, der alle Strings akzeptiert, die mit dem String ababaca enden. Zustand 0 ist der Startzustand, und Zustand 7 (der schwarz gekennzeichnet ist) ist der einzige akzeptierende Zustand. Eine mit a gekennzeichnete gerichtete Kante vom Zustand i zum Zustand j stellt δ(i, a) = j dar. Die nach rechts verlaufenden, fett eingezeichneten Kanten stellen das „Rückgrat“ des Automaten dar und entsprechen der erfolgreichen Übereinstimmung zwischen dem Muster und den Eingabezeichen. Mit Ausnahme der Kanten von Zustand 7 zu den Zuständen 1 und 2 entsprechen die nach links verlaufenden Kanten fehlgeschlagenen Übereinstimmungen. Zur besseren Übersichtlichkeit sind einige der zu fehlgeschlagenen Übereinstimmungen korrespondierenden Kanten nicht eingezeichnet: es gilt δ(i, a) = 0, wenn der Zustand i keine mit a gekennzeichnete ausgehende Kante besitzt. (b) Die entsprechende Übergangsfunktion δ und das Muster P = ababaca. Die Einträge, die einer Übereinstimmung zwischen dem Muster und den Eingabezeichen entsprechen, sind schattiert eingezeichnet. (c) Die Arbeitsweise des Automaten auf dem Text T = abababacaba. Unterhalb jedes Textzeichens T [i] ist der Zustand φ(Ti ) angegeben, in dem sich der Automat befindet, nachdem der Präfix Ti bearbeitet wurde. Der Automat findet ein Vorkommen des Musters; dieses Vorkommen endet an der Position 9.
32.3 String-Matching mit endlichen Automaten
1011
Finite-Automaton-Matcher(T, δ, m) 1 n = T.l¨a nge 2 q =0 3 for i = 1 to n 4 q = δ(q, T [i]) 5 if q = = m 6 print “Das Muster tritt auf mit der Verschiebung” i − m Aus der einfachen Schleifenstruktur von Finite-Automaton-Matcher können wir leicht ersehen, dass die Matchingzeit auf einem Text der Länge n in Θ(n) ist. Diese Matchingzeit schließt allerdings die Vorverarbeitungszeit zum Berechnen der Übergangsfunktion δ nicht mit ein. Wir sprechen dieses Problem später an, nachdem wir zuerst bewiesen haben, dass die Prozedur Finite-Automaton-Matcher korrekt arbeitet. Betrachten Sie, wie der Automat auf einem Eingabetext T [1 . . n] arbeitet. Wir werden beweisen, dass sich der Automat im Zustand σ(Ti ) befindet, nachdem er sich das Zeichen T [i] angeschaut hat. Da σ(Ti ) = m genau dann gilt, wenn P = Ti ist, befindet sich der Automat genau dann im akzeptierenden Zustand m, wenn er sich gerade das Muster P angeschaut hat. Um dieses Ergebnis beweisen zu können, benutzen wir die folgenden beiden Lemmata für die Suffixfunktion σ. Lemma 32.2: (Suffixfunktionsungleichung) Für jeden String x und jedes Zeichen a gilt σ(xa) ≤ σ(x) + 1. Beweis: Entsprechend Abbildung 32.8 sei r = σ(xa). Ist r = 0, so ist die Schlussfolgerung σ(xa) = r ≤ σ(x) + 1 wegen der Nichtnegativität von σ(x) offensichtlich erfüllt. Setzen Sie nun voraus, dass r > 0 gilt. Wegen der Definition von σ gilt dann Pr = xa. Folglich ist Pr−1 = x, wenn wir a am Ende von Pr und am Ende von xa weglassen. Deshalb gilt r − 1 ≤ σ(x), weil σ(x) das größte k ist, für das Pk = x gilt. Somit gilt σ(xa) = r ≤ σ(x) + 1.
Lemma 32.3: (Rekursionslemma für die Suffixfunktion) Für jeden Text x und jedes Zeichen a gilt σ(xa) = σ(Pq a), wenn q = σ(x) ist. Beweis: Aus der Definition von σ folgt Pq = x. Wie Abbildung 32.9 zeigt, gilt auch Pq a = xa. Wenn wir r = σ(xa) setzen, dann gilt Pr = xa und wegen Lemma 32.2 auch r ≤ q + 1. Somit gilt |Pr | = r ≤ q + 1 = |Pq a|. Wegen Pq a = xa, Pr = xa und |Pr | ≤ |Pq a| folgt aus Lemma 32.1 Pr = Pq a. Deshalb ist r ≤ σ(Pq a), d. h. σ(xa) ≤ σ(Pq a). Es gilt aber auch σ(Pq a) ≤ σ(xa), weil Pq a = xa ist. Folglich gilt σ(xa) = σ(Pq a).
1012
32 String-Matching x Pr–1
a
Pr Abbildung 32.8: Eine Illustration des Beweises von Lemma 32.2. Die Abbildung zeigt, dass r ≤ σ(x) + 1 mit r = σ(xa) gilt.
x a Pq
a Pr
Abbildung 32.9: Eine Illustration des Beweises von Lemma 32.3. Die Abbildung zeigt, dass r = σ(Pq a) mit q = σ(x) und r = σ(xa) gilt.
Wir sind nun in der Lage, unseren Hauptsatz, der das Verhalten eines String-MatchingAutomaten auf einem gegebenen Text charakterisiert, zu beweisen. Wie bereits erwähnt, zeigt dieses Theorem, dass der Automat bei jedem Schritt lediglich den längsten Präfix des Musters, der ein Suffix des bisher gelesenen Textes ist, verfolgt. Mit anderen Worten, der Automat erhält die Schleifeninvariante (32.5). Theorem 32.4 Wenn φ die Endzustandsfunktion eines String-Matching-Automaten zu einem gegebenen Muster P ist und T [1 . . n] ein Eingabetext des Automaten ist, dann gilt für i = 0, 1, . . . , n φ(Ti ) = σ(Ti ) . Beweis: Der Beweis erfolgt per Induktion nach i. Für i = 0 gilt das Theorem trivialerweise, weil T0 = ε ist. Folglich gilt φ(T0 ) = 0 = σ(T0 ). Wir setzen nun voraus, dass φ(Ti ) = σ(Ti ) ist, und beweisen, dass φ(Ti+1 ) = σ(Ti+1 ) gilt. Wir bezeichnen φ(Ti ) mit q und T [i + 1] mit a. Dann ist φ(Ti+1 ) = = = = = =
φ(Ti a) δ(φ(Ti ), a) δ(q, a) σ(Pq a) σ(Ti a) σ(Ti+1 )
(wegen (wegen (wegen (wegen (wegen (wegen
der Definition von Ti+1 und a) der Definition von φ) der Definition von q) der Definition (32.4) von δ) Lemma 32.3 und per Induktion) der Definition von Ti+1 ) .
32.3 String-Matching mit endlichen Automaten
1013
Wenn der Automat in Zeile 4 den Zustand q annimmt, dann ist q wegen Theorem 32.4 der größte Wert, für den Pq = Ti gilt. Folglich gilt q = m in Zeile 5 genau dann, wenn die Maschine gerade ein Vorkommen des Musters P gelesen hat. Somit arbeitet Finite-Automaton-Matcher korrekt.
Berechnung der Übergangsfunktion Die folgende Prozedur berechnet die Übergangsfunktion δ zu einem gegebenen Muster P [1 . . m]. Compute-Transition-Function(P, Σ) 1 m = P.l¨a nge 2 for q = 0 to m 3 for jedes Zeichen a ∈ Σ 4 k = min(m + 1, q + 2) 5 repeat 6 k = k−1 7 until Pk = Pq a 8 δ(q, a) = k 9 return δ Diese Prozedur berechnet die Funktion δ(q, a) auf einfache Art und Weise entsprechend ihrer Definition in Gleichung (32.4). Die in den Zeilen 2 und 3 beginnenden verschachtelten Schleifen betrachten alle Zustände q und Zeichen a und die Zeilen 4–8 setzen δ(q, a) gleich dem größten k, für das Pk = Pq a gilt. Der Code beginnt mit dem größtmöglichen Wert von k, d. h. mit min(m, q + 1). Er verringert dann k bis Pk = Pq a gilt, was irgendwann der Fall sein muss, da P0 = ε ein Suffix eines jeden String ist. Die Laufzeit von Compute-Transition-Function ist O(m3 |Σ|), weil die äußeren Schleifen einen Faktor m |Σ| beiträgt, die innere repeat-Schleife höchstens (m + 1)mal durchlaufen werden kann und der Test in Zeile 7 zum Vergleich von bis zu m Zeichen führen kann. Viel schnellere Prozeduren existieren; indem wir klug berechnete Informationen über das Muster P benutzen (siehe Übung 32.4-8), können wir die zur Berechnung von δ für P benötigte Zeit auf O(m |Σ|) verbessern. Mit dieser verbesserten Prozedur zur Berechnung von δ können wir alle Vorkommen eines Musters der Länge m in einem Text der Länge n über dem Alphabet Σ mit O(m |Σ|) Vorverarbeitungszeit und Θ(n) Matchingzeit bestimmen.
Übungen 32.3-1 Konstruieren Sie den String-Matching-Automaten für das Muster P = aabab und illustrieren Sie dessen Arbeitsweise auf dem Text T = aaababaabaababaab. 32.3-2 Zeichnen Sie ein Zustandsübergangsdiagramm für den String-Matching-Automaten für das Muster ababbabbababbababbabb über dem Alphabet Σ = {a, b}.
1014
32 String-Matching
32.3-3 Wir bezeichnen ein Muster P als nichtüberlappend, wenn k = 0 oder k = q aus Pk = Pq folgt. Beschreiben Sie das Zustandsübergangsdiagramm des String-Matching-Automaten für ein nichtüberlappendes Muster. 32.3-4∗ Gegeben seien zwei Muster P und P . Beschreiben Sie, wie wir einen endlichen Automaten konstruieren können, der alle Vorkommen bestimmt, an denen eines der Muster vorkommt. Minimieren Sie die Anzahl der Zustände in Ihrem Automaten. 32.3-5 Gegeben sei ein Muster P , das Lückenzeichen enthält (siehe Übung 32.1-4). Zeigen Sie, wie wir einen endlichen Automaten konstruieren können, der ein Vorkommen von P im Text T in Matchingzeit O(n) mit n = |T | finden kann.
∗ 32.4 Der Knuth-Morris-Pratt-Algorithmus Wir präsentieren nun einen Linearzeit-String-Matching-Algorithmus, der auf Knuth, Morris und Pratt zurückgeht. Dieser Algorithmus vermeidet vollständig, die Übergangsfunktion δ zu berechnen, und seine Matchingzeit ist Θ(n), wobei er lediglich eine Hilfsfunktion π benutzt, die wir aus dem Muster in Zeit Θ(m) vorberechnen und in einem Feld π[1 . . m] speichern. Das Feld π erlaubt uns, die Übergangsfunktion effizient (im amortisierten Sinne) „on the fly“, also bei Bedarf, zu berechnen. Kurz gesagt, für jeden Zustand q = 0, 1, . . . , m und jedes beliebige Zeichen a ∈ Σ enthält der Wert π[q] die Information, die wir benötigen, um δ(q, a) zu berechnen; er hängt aber nicht von a ab. Da das Feld π nur m Elemente enthält, während δ Θ(m |Σ|) Elemente hat, sparen wir bei der Vorverarbeitungszeit einen Faktor |Σ|, wenn wir π statt δ berechnen.
Die Präfixfunktion für ein Muster Die Präfixfunktion π für ein Muster enthält implizit Informationen darüber, wie das Muster mit Verschiebungen seiner selbst übereinstimmt. Wir können diese Informationen verwenden, um den Test sinnloser Verschiebungen beim einfachen String-MatchingAlgorithmus zu vermeiden und die Vorberechnung der vollständigen Übergangsfunktion δ für den String-Matching-Automaten zu umgehen. Betrachten Sie die Arbeitsweise des einfachen String-Matching. Abbildung 32.10(a) zeigt eine spezielle Verschiebung s der Suchmaske, die das Muster P = ababaca enthält, gegenüber dem Text T . In diesem Beispiel stimmen q = 5 Zeichen erfolgreich überein, aber das 6. Zeichen des Musters stimmt nicht mit dem entsprechenden Textzeichen überein. Die Information, dass q Zeichen passen, bestimmt die korrespondierenden Textzeichen. Diese q Textzeichen zu kennen, erlaubt es uns, sofort festzustellen, dass bestimmte Verschiebungen ungültig sind. Im Beispiel aus der Abbildung ist die Verschiebung s + 1 notwendigerweise ungültig, da das erste Musterzeichen (a) an einem Textzeichen ausgerichtet werden würde, von dem wir wissen, dass es nicht mit dem zweiten Musterzeichen (b) übereinstimmt. Die im Teil (b) der Abbildung gezeigte Verschiebung s = s+2 richtet jedoch die ersten drei Musterzeichen an den drei Textzeichen aus, die notwendigerweise
32.4 ∗ Der Knuth-Morris-Pratt-Algorithmus b a c b a b a b a a b c b a b s
a b a b a c a q
1015 T
P
(a)
b a c b a b a b a a b c b a b s′ = s + 2
a b a b a c a k
T
P
(b)
a b a b a
Pq
a b a
Pk
(c)
Abbildung 32.10: Die Präfixfunktion π. (a) Das Muster P = ababaca ist bezüglich des Textes T so ausgerichtet, dass die ersten q = 5 Zeichen übereinstimmen. Die übereinstimmenden Zeichen, die wir schattiert eingezeichnet haben, sind durch vertikale Linien verbunden. (b) Indem wir unser Wissen über die 5 übereinstimmenden Zeichen verwenden, können wir ableiten, dass die Verschiebung s + 1 ungültig ist, aber eine Verschiebung von s = s + 2 mit unseren Informationen konsistent und deshalb potentiell gültig ist. (c) Wir können nützliche Informationen für solche Ableitungen vorberechnen, indem wir das Muster mit sich selbst vergleichen. In dem vorliegenden Beispiel sehen wir, dass der längste Präfix von P , der auch ein echter Suffix von P5 ist, P3 ist. Wir stellen diese vorberechnete Information in dem Feld π so dar, dass π[5] = 3 gilt. Stimmen q Zeichen bei einer Verschiebung von s erfolgreich überein, so ist die nächste potentiell gültige Verschiebung bei s = s + (q − π[q]) (siehe Teil (b)).
übereinstimmen müssen. Im Allgemeinen ist es nützlich, die Antwort auf folgende Frage zu kennen: Stimmen die Musterzeichen P [1 . . q] mit den Textzeichen T [s + 1 . . s + q] überein, wie groß ist die geringste Verschiebung s > s, für die P [1 . . k] = T [s + 1 . . s + k]
(32.6)
mit s + k = s + q für ein k < q gilt? In anderen Worten, wenn wir wissen, dass Pq = Ts+q gilt, dann wollen wir den längsten echten Präfix Pk von Pq haben, der auch ein Suffix von Ts+q ist. (Da s + k = s + q gilt, ist das Bestimmen der kleinsten Verschiebung s gleichbedeutend mit dem Bestimmen der Länge k des längsten Präfixes, wenn wir s und q gegeben haben.) Wir addieren die
1016
32 String-Matching
P5
a b a b a c a
P3
i P [i] π[i]
1
2
3
4
5
6
7
a b a b a c a 0 0 1 2 3 0 1
a b a b a c a
π[5] = 3
P1
a b a b a c a
π[3] = 1
P0
ε
π[1] = 0
(a)
a b a b a c a
(b)
Abbildung 32.11: Eine Illustration von Lemma 32.5 für das Muster P = ababaca und q = 5. (a) Die π-Funktion für das gegebene Muster. Da π[5] = 3, π[3] = 1 und π[1] = 0 ist, erhalten wir π ∗ [5] = {3, 1, 0} durch Iteration von π. (b) Wir lassen die Suchmaske mit dem Muster P nach rechts gleiten und notieren uns, wenn ein Präfix Pk von P mit einem echten Suffix von P5 übereinstimmt; wir erhalten Übereinstimmungen für k = 3, 1 und 0. In der Abbildung gibt die erste Zeile P an und die gepunktete vertikale Linie wurde unmittelbar nach P5 gezeichnet. Aufeinander folgende Zeilen veranschaulichen alle Suffixe von P , die bewirken, dass ein Präfix Pk von P mit einem Suffix von P5 übereinstimmt. Übereinstimmende Zeichen sind schattiert eingezeichnet. Vertikale Linien verbinden auf einander ausgerichtete übereinstimmende Zeichen. Folglich ist {k : k < 5 und Pk = P5 } = {3, 1, 0}. Lemma 32.5 sagt aus, dass π ∗ [q] = {k : k < q und Pk = Pq } für alle q gilt.
Differenz q − k der Längen dieser Präfixe von P auf die Verschiebung s, um die neue Verschiebung s = s + (q − k) zu bekommen. Im besten Fall, d. h. im Fall k = 0, ist s = s + q und wir schließen die Verschiebungen s + 1, s + 2, . . . , s + q − 1 direkt aus. Auf jeden Fall, brauchen wir an der neuen Verschiebung die ersten k Zeichen von P nicht mit den korrespondierenden Zeichen von T zu vergleichen, da die Gleichung (32.6) gewährleistet, dass sie übereinstimmen. Wir können die notwendige Information vorberechnen, indem wir, wie in Abbildung 32.10(c) gezeigt, das Muster mit sich selbst vergleichen. Da T [s + 1 . . s + k] ein Teil des bekannten Textabschnittes ist, ist es ein Suffix des Strings Pq . Wir können aus diesem Grunde die Gleichung (32.6) so interpretieren, dass nach dem größten k < q gesucht wird, für das Pk = Pq gilt. Dann ist die neue Verschiebung s = s + (q − k) die nächste potentiell gültige Verschiebung. Wir erachten es als zweckmäßig, für jeden Wert von q die Anzahl k der übereinstimmenden Zeichen bei der neuen Verschiebung s anstatt beispielsweise s − s zu speichern. Wir formalisieren die Information, die wir vorberechnen, wie folgt. Ist ein Muster P [1 . . m] gegeben, so ist die Präfixfunktion für P die Funktion π : {1, 2, . . . , m} → {0, 1, . . . , m − 1} , für die π[q] = max {k : k < q und Pk = Pq }
32.4 ∗ Der Knuth-Morris-Pratt-Algorithmus
1017
gilt. Das heißt, π[q] ist die Länge des längsten Präfixes von P , der ein echter Suffix von Pq ist. Abbildung 32.11(a) gibt die vollständige Präfixfunktion π für das Muster ababaca an. Der Pseudocode unten gibt den Knuth-Morris-Pratt-Matching-Algorithmus in Form der Prozedur KMP-Matcher an. Wie wir sehen werden, folgt die Prozedur im Großen und Ganzen aus Finite-Automaton-Matcher. KMP-Matcher ruft die Hilfsprozedur Compute-Prefix-Function auf, um π zu berechnen. KMP-Matcher(T, P ) 1 n = T.l¨a nge 2 m = P.l¨a nge 3 π = Compute-Prefix-Function(P ) 4 q =0 // Anzahl der übereinstimmenden Zeichen 5 for i = 1 to n // lies den Text von links nach rechts 6 while q > 0 und P [q + 1] = T [i] 7 q = π[q] // das nächste Zeichen stimmt nicht überein 8 if P [q + 1] = = T [i] 9 q = q+1 // das nächste Zeichen stimmt überein 10 if q = = m // stimmt ganz P überein? 11 print “das Muster tritt auf mit der Verschiebung” i − m 12 q = π[q] // suche nach der nächsten Übereinstimmung Compute-Prefix-Function(P ) 1 m = P.l¨a nge 2 sei π[1 . . m] ein neues Feld 3 π[1] = 0 4 k =0 5 for q = 2 to m 6 while k > 0 und P [k + 1] = P [q] 7 k = π[k] 8 if P [k + 1] = = P [q] 9 k = k+1 10 π[q] = k 11 return π Diese zwei Prozeduren haben viel gemeinsam, da beide einen String gegen das Muster P vergleichen: KMP-Matcher vergleicht den Text T gegen P und Compute-PrefixFunction vergleicht P gegen sich selbst. Wir beginnen mit der Analyse der Laufzeit dieser Prozeduren. Ihre Korrektheit zu beweisen ist wesentlich komplizierter.
Laufzeitanalyse Die Laufzeit von Compute-Prefix-Function ist Θ(m), was wir zeigen, indem wir die Aggregat-Methode der amortisieren Analyse anwenden (siehe Abschnitt 17.1). Der
1018
32 String-Matching
einzige Kniff besteht darin, zu zeigen, dass die while-Schleife der Zeilen 6–7 zusammen insgesamt O(m)-mal ausgeführt wird. Wir werden zeigen, dass sie höchstens m − 1 Iterationen durchführt. Beginnen wir mit einigen Beobachtungen zu k. Erstens, die Zeile 4 startet k bei 0 und der einzige Weg, k zu erhöhen, ist durch die Inkrementieroperation in Zeile 9 gegeben, die höchstens einmal pro Iteration der for-Schleife der Zeilen 5–10 ausgeführt wird. Somit kann k höchstens um m − 1 erhöht werden. Zweitens, da k < q ist, wenn die for-Schleife betreten wird, und jede Iteration der Schleife den Wert q inkrementiert, gilt k < q immer. Aus diesem Grunde sichern die Zuweisungen in den Zeilen 3 und 10 zu, dass π[q] < q für alle q = 1, 2, . . . , m gilt, was bedeutet, dass jede Iteration der while-Schleife k verkleinert. Drittens, k wird niemals negativ. Mit diesen drei Fakten sehen wir, dass die Gesamtreduzierung von k durch die while-Schleife über alle Iterationen der for-Schleife nach oben durch die Gesamterhöhung von k, die m − 1 ist, beschränkt ist. Somit iteriert die while-Schleife insgesamt höchstens (m − 1)-mal und Compute-Prefix-Function läuft in Zeit Θ(m). Übung 32.4-4 verlangt von Ihnen, mit einer ähnlichen Aggregat-Analyse zu zeigen, dass die Matchingzeit von KMP-Matcher in Θ(n) ist. Im Vergleich zur Prozedur Finite-Automaton-Matcher haben wir durch Verwendung von π anstelle von δ die Zeit für die Vorberechnung auf dem Muster von O(m |Σ|) auf Θ(m) verringert, während die Matchingzeit weiterhin in Θ(n) liegt.
Die Korrektheit der Berechnung der Präfixfunktion Wir werden weiter hinten sehen, dass die Präfixfunktion π uns hilft, die Übergangsfunktion δ eines String-Matching-Automaten zu simulieren. Zuerst müssen wir jedoch beweisen, dass die Prozedur Compute-Prefix-Function auch wirklich die Präfixfunktion korrekt berechnet. Um dies zu tun, müssen wir alle Präfixe Pk finden, die echte Suffixe eines gegebenen Präfixes Pq sind. Der Wert von π[q] gibt uns den längsten solchen Präfix; das folgende Lemma, das in Abbildung 32.11 illustriert ist, zeigt aber, dass wir durch Iteration in der Tat alle Präfixe Pk aufzählen können, die echte Suffixe von Pq sind. Sei π ∗ [q] = {π[q], π (2) [q], π (3) [q], . . . , π (t) [q]} , wobei π (i) [q] als funktionale Iteration zu verstehen ist, sodass π (0) [q] = q und π (i) [q] = π[π (i−1) [q]] für i ≥ 1 gilt, und die Sequenz in π ∗ [q] stoppt, wenn π (t) [q] = 0 erreicht ist. Lemma 32.5: (Iterationslemma für die Präfixfunktion) Sei P ein Muster der Länge m mit der Präfixfunktion π. Dann gilt für q = 1, 2, . . . , m π ∗ [q] = {k : k < q und Pk = Pq }. Beweis: Wir beweisen zunächst, dass π ∗ [q] ⊆ {k : k < q und Pk = Pq } gilt, d. h. i ∈ π ∗ [q] impliziert Pi = Pq .
(32.7)
Ist i ∈ π ∗ [q], so gilt i = π (u) [q] für ein u > 0. Wir beweisen Gleichung (32.7) per Induktion nach u. Für u = 1 gilt i = π[q] und die Behauptung folgt, weil i < q ist und
32.4 ∗ Der Knuth-Morris-Pratt-Algorithmus
1019
Pπ[q] = Pq nach Definition von π gilt. Die Verwendung der Beziehungen π[i] < i und Pπ[i] = Pi sowie der Transitivität von < und = begründet die Behauptung für alle i in π ∗ [q]. Deshalb gilt π ∗ [q] ⊆ {k : k < q und Pk = Pq }. Wir beweisen nun durch Widerspruch, dass {k : k < q und Pk = Pq } ⊆ π ∗ [q] gilt. Nehmen Sie an, die Behauptung würde nicht gelten, d. h. nehmen Sie an, dass die Menge {k : k < q und Pk = Pq } − π ∗ [q] nicht leer wäre und j der größte Wert in der Menge wäre. Weil π[q] der größte Wert in {k : k < q und Pk = Pq } und π[q] ∈ π ∗ [q] ist, muss j < π[q] gelten. Lassen Sie uns mit j die kleinste ganze Zahl aus π ∗ [q] bezeichnen, die größer als j ist. (Wir können j = π[q] wählen, wenn es keine andere Zahl in π ∗ [q] gibt, die größer als j ist.) Es gilt Pj = Pq , da j ∈ {k : k < q and Pk = Pq } ist, und es folgt Pj = Pq aus j ∈ π ∗ [q] und Gleichung (32.7). Folglich ist wegen Lemma 32.1 Pj = Pj und j ist der größte Wert kleiner als j mit dieser Eigenschaft. Deshalb muss π[j ] = j gelten und, weil j ∈ π ∗ [q] ist, muss auch j ∈ π ∗ [q] gelten. Dieser Widerspruch beweist das Lemma. Der Algorithmus Compute-Prefix-Function berechnet π[q] der Reihe nach für q = 1, 2, . . . , m. Die Belegung von π[1] auf 0 in Zeile 3 von Compute-Prefix-Function ist sicherlich korrekt, da π[q] < q für alle q gilt. Wir werden mithilfe des folgenden Lemmas und dessen Korollar beweisen, dass die Prozedur Compute-Prefix-Function π[q] für q > 1 korrekt berechnet. Lemma 32.6 Sei P ein Muster der Länge m und π die Präfixfunktion von P . Ist für q = 1, 2, . . . , m π[q] > 0, so gilt π[q] − 1 ∈ π ∗ [q − 1]. Beweis: Sei r = π[q] > 0, sodass r < q und Pr = Pq gilt. Folglich ist r − 1 < q − 1 und Pr−1 = Pq−1 (indem wir das letzte Zeichen von Pr und Pq entfernen, was wir wegen r > 0 machen können). Wegen Lemma 32.5 gilt deshalb r − 1 ∈ π ∗ [q − 1]. Es gilt somit π[q] − 1 = r − 1 ∈ π ∗ [q − 1]. Definieren Sie für q = 2, 3, . . . , m die Teilmenge Eq−1 ⊆ π ∗ [q − 1] durch Eq−1 = {k ∈ π ∗ [q − 1] : P [k + 1] = P [q]} = {k : k < q − 1 und Pk = Pq−1 und P [k + 1] = P [q]} (wegen Lemma 32.5) = {k : k < q − 1 und Pk+1 = Pq } . Die Menge Eq−1 besteht aus den Werten k < q − 1, für die Pk = Pq−1 gilt und für die (wegen P [k + 1] = P [q]) Pk+1 = Pq gilt. Folglich besteht Eq−1 aus denjenigen Werten k ∈ π ∗ [q − 1], für die wir Pk zu Pk+1 erweitern können und einen korrekten Suffix von Pq erhalten.
1020
32 String-Matching
Korollar 32.7 Sei P ein Muster der Länge m und π die Präfixfunktion von P . Für q = 2, 3, . . . , m gilt 0 falls Eq−1 = ∅ , π[q] = falls Eq−1 = ∅ . 1 + max {k ∈ Eq−1 } Beweis: Wenn Eq−1 leer ist, so gibt es kein k ∈ π ∗ [q − 1] (einschließlich k = 0), für das wir Pk zu Pk+1 erweitern können und einen korrekten Suffix von Pq erhalten. Deshalb ist π[q] = 0. Wenn Eq−1 nicht leer ist, dann gilt k + 1 < q und Pk+1 = Pq für alle k ∈ Eq−1 . Deshalb gilt wegen der Definition von π[q] π[q] ≥ 1 + max {k ∈ Eq−1 } .
(32.8)
Beachten Sie, dass π[q] > 0 gilt. Sei r = π[q]−1, d. h. r +1 = π[q], und somit Pr+1 = Pq . Wegen r+1 > 0 gilt P [r+1] = P [q]. Darüber hinaus gilt r ∈ π ∗ [q−1] wegen Lemma 32.6. Deshalb ist r ∈ Eq−1 und es gilt r ≤ max {k ∈ Eq−1 } oder äquivalent dazu π[q] ≤ 1 + max {k ∈ Eq−1 } . Mit den Gleichungen (32.8) und (32.9) vervollständigt sich der Beweis.
(32.9)
Wir kommen nun zum Schluss des Beweises, dass Compute-Prefix-Function π korrekt berechnet. In der Prozedur Compute-Prefix-Function gilt k = π[q − 1] zu Beginn jeder Iteration der for-Schleife der Zeilen 5–10. Diese Bedingung wird durch die Zeilen 3 und 4 beim ersten Eintritt in die Schleife erzwungen und bleibt in jeder folgenden Iteration aufgrund von Zeile 10 erhalten. Die Zeilen 6–9 passen k so an, dass er der korrekte Wert von π[q] wird. Die while-Schleife in den Zeilen 6–7 durchsucht die Werte k ∈ π ∗ [q − 1], bis sie einen Wert von k findet, für den P [k + 1] = P [q] gilt; zu diesem Moment ist k der größte Wert in der Menge Eq−1 , sodass wir wegen Korollar 32.7 π[q] auf k + 1 setzen können. Wenn die while-Schleife kein k ∈ π ∗ [q − 1] mit P [k + 1] = P [q] finden kann, ist k in Zeile 8 gleich 0. Ist P [1] = P [q], so sollten wir sowohl k als auch π[q] auf 1 setzen; anderenfalls sollten wir k belassen und π[q] auf 0 setzen. Die Zeilen 8–10 setzen in jedem Fall k und π[q] auf ihre korrekten Werte. Dies vervollständigt unseren Beweis der Korrektheit von Compute-Prefix-Function.
Korrektheit des Knuth-Morris-Pratt-Algorithmus Wir können uns die Prozedur KMP-Matcher als eine Reimplementierung der Prozedur Finite-Automaton-Matcher denken, in der wir die Präfixfunktion π benutzen, um die Zustandsübergänge zu berechnen. Speziell werden wir beweisen, dass in der iten Iteration der for-Schleife der Zustand q den gleichen Wert in beiden Prozeduren KMP-Matcher und Finite-Automaton-Matcher hat, wenn wir auf Gleichheit mit m testen (in Zeile 10 von KMP-Matcher und in Zeile 5 von Finite-AutomatonMatcher). Wenn wir einmal begründet haben, weshalb KMP-Matcher das Verhalten
32.4 ∗ Der Knuth-Morris-Pratt-Algorithmus
1021
von Finite-Automaton-Matcher simuliert, dann folgt die Korrektheit von KMPMatcher aus der Korrektheit von Finite-Automaton-Matcher (dabei werden wir ein bisschen später sehen, warum Zeile 12 von KMP-Matcher notwendig ist.) Bevor wir formal beweisen, dass KMP-Matcher die Prozedur Finite-AutomatonMatcher korrekt simuliert, nehmen wir uns einen Augenblick Zeit, um zu verstehen, wie die Präfixfunktion π die Übergangsfunktion δ ersetzt. Rufen Sie sich in Erinnerung, dass, wenn ein String-Matching-Automat im Zustand q ist und ein Zeichen a = T [i] liest, er in einen neuen Zustand δ(q, a) geht. Ist a = P [q + 1], sodass a die Übereinstimmung mit dem Muster fortführt, so gilt δ(q, a) = q + 1. Anderenfalls gilt a = P [q + 1], sodass a die Übereinstimmung mit dem Muster nicht fortführt, und es gilt 0 ≤ δ(q, a) ≤ q. Im ersten Fall, in dem a die Übereinstimmung fortsetzt, geht KMP-Matcher zum Zustand q + 1, ohne auf die π-Funktion zuzugreifen: der Test der while-Schleife wird das erste Mal negativ beschieden, der Test in Zeile 8 geht positiv aus und Zeile 9 inkrementiert q. Die π-Funktion kommt ins Spiel, wenn das Zeichen a die Übereinstimmung mit dem Muster nicht fortsetzt, sodass der neue Zustand δ(q, a) entweder q oder ein Zustand auf dem Rückgrat des Automaten links von q ist. Die while-Schleife in den Zeilen 6–7 der KMP-Matcher iteriert durch die Zustände aus π ∗ [q] und stoppt, entweder wenn sie in einem Zustand, sagen wir q , kommt, in dem a mit P [q + 1] übereinstimmt, oder wenn q gleich 0 ist. Stimmt a mit P [q + 1] überein, so setzt Zeile 9 den neuen Zustand auf q +1, der gleich δ(q, a) sein sollte, damit die Simulation korrekt arbeitet. In anderen Worten, der neue Zustand δ(q, a) sollte entweder Zustand 0 sein oder um 1 größer sein als ein Zustand aus π ∗ [q]. Lassen Sie uns das Beispiel aus den Abbildungen 32.7 und 32.11 nochmals anschauen, in denen das Muster P gleich ababaca ist. Setzen Sie voraus, dass der Automat im Zustand q = 5 ist; die Zustände in π ∗ [5] sind (in absteigender Reihenfolge) 3, 1 und 0. Ist das nächste Zeichen, das gelesen wird, ein c, so können wir einfach sehen, dass der Automat sowohl in Finite-Automaton-Matcher als auch in KMP-Matcher in den Zustand δ(5, c) = 6 übergeht. Setzen Sie nun voraus, dass das nächste Zeichen, das gelesen wird, b (anstatt c) ist, sodass der Automat in den Zustand δ(5, b) = 4 übergehen sollte. Die while-Schleife in KMP-Matcher terminiert, nachdem Zeile 7 einmal ausgeführt wurde, und geht in den Zustand q = π[5] = 3. Da P [q +1] = P [4] = b gilt, geht der Test in Zeile 8 positiv aus und KMP-Matcher geht in den neuen Zustand q + 1 = 4 = δ(5, b) über. Setzen Sie letztendlich voraus, dass das nächste Zeichen, das gelesen wird, a ist, sodass der Automat in den Zustand δ(5, a) = 1 gehen sollte. Die ersten drei Mal, wenn der Test in Zeile 6 ausgeführt wird, geht der jeweilige Test positiv aus. Das erste Mal sehen wir, dass P [6] = c = a gilt und KMP-Matcher in den Zustand π[5] = 3 übergeht (der erste Zustand in π ∗ [5]). Beim zweiten Mal sehen wir, dass P [4] = b = a gilt, und wir gehen in den Zustand π[3] = 1 (der zweite Zustand in π ∗ [5]). Beim dritten Mal sehen wir, dass P [2] = b = a gilt, und wir gehen in den Zustand π[1] = 0 (der letzte Zustand in π ∗ [5]). Die while-Schleife terminiert, wenn wir Zustand q = 0 erreicht haben. Nun findet Zeile 8 heraus, dass P [q + 1] = P [1] = a gilt, und Zeile 9 veranlasst, dass der Automat in den neuen Zustand q + 1 = 1 = δ(5, a) geht. Unsere Intuition ist also, dass die Prozedur KMP-Matcher über die Zustände aus π ∗ [q]
1022
32 String-Matching
in absteigender Reihenfolge iteriert, bei einem Zustand q stoppt und dann eventuell zum Zustand q + 1 übergeht. Wenngleich die Simulation der Berechnung von δ(q, a) nach viel Arbeit aussehen mag, sollten Sie im Hinterkopf behalten, dass KMP-Matcher asymptotisch nicht langsamer ist als Finite-Automaton-Matcher. Wir sind nun soweit, um die Korrektheit des Knuth-Morris-Pratt-Algorithmus formal zu beweisen. Wegen Theorem 32.4 gilt q = σ(Ti ) jedesmal, nachdem wir Zeile 4 von FiniteAutomaton-Matcher ausgeführt haben. Aus diesem Grunde reicht es zu zeigen, dass die gleiche Eigenschaft in Bezug auf die for-Schleife von KMP-Matcher gilt. Der Beweis geht über Induktion nach der Anzahl der Schleifeniterationen. Zu Beginn setzen beide Prozeduren q auf 0, da sie ihre jeweilige for-Schleife das erste Mal betreten. Betrachten Sie die i-te Iteration der for-Schleife von KMP-Matcher und sei q der Zustand zu Beginn dieser Schleifeniteration. Aufgrund der Induktionsvoraussetzung gilt q = σ(Ti−1 ). Wir müssen zeigen, dass q = σ(Ti ) in Zeile 10 gilt. (Wir werden Zeile 12 wiederum für sich separat betrachten.) Wenn wir das Zeichen T [i] betrachten, dann ist der längste Präfix von P , der ein Suffix von Ti ist, entweder Pq +1 (wenn P [q + 1] = T [i] gilt) oder ein Präfix (nicht notwendigerweise ein echter Präfix und möglicherweise auch leer) von Pq . Wir betrachten die drei Fälle, in denen σ(Ti ) = 0, σ(Ti ) = q + 1 und 0 < σ(Ti ) ≤ q gelten, jeweils für sich: • Ist σ(Ti ) = 0, so ist P0 = ε der einzige Präfix von P , der ein Suffix von Ti ist. Die while-Schleife der Zeilen 6–7 iteriert über die Werte aus π ∗ [q ], aber die Schleife findet nie ein q mit P [q + 1] = T [i], obwohl Pq = Ti−1 für jedes q ∈ π ∗ [q ] gilt. Die Schleife terminiert, wenn q den Wert 0 erreicht hat, und Zeile 9 wird natürlich nicht ausgeführt. Aus diesem Grunde gilt q = 0 in Zeile 10, sodass q = σ(Ti ) ist. • Ist σ(Ti ) = q + 1, so gilt P [q + 1] = T [i] und der Test in der while-Schleife der Zeile 6 scheitert beim ersten Mal. Zeile 9 wird ausgeführt, inkrementiert q, sodass danach q = q + 1 = σ(Ti ) gilt. • Ist 0 < σ(Ti ) ≤ q , so iteriert die while-Schleife der Zeilen 6–7 mindestens einmal, wobei sie in absteigender Reihenfolge jeden Wert q ∈ π ∗ [q ] überprüft, bis sie bei einem q < q stoppt. Folglich ist Pq der längste Präfix von Pq , für den P [q + 1] = T [i] gilt, sodass q + 1 = σ(Pq T [i]) gilt, wenn die Schleife terminiert. Da q = σ(Ti−1 ) ist, impliziert Lemma 32.3, dass σ(Ti−1 T [i]) = σ(Pq T [i]) gilt. Somit haben wir q + 1 = σ(Pq T [i]) = σ(Ti−1 T [i]) = σ(Ti ) , wenn die while-Schleife terminiert. Nachdem Zeile 9 den Wert q inkrementiert hat, haben wir q = σ(Ti ). Zeile 12 in KMP-Matcher ist notwendig, da wir anderenfalls in Zeile 6 möglicherweise auf P [m + 1] verweisen würden, nachdem wir ein Vorkommen von P gefunden haben. (Das Argument, dass bei der nächsten Bearbeitung von Zeile 6 die Gleichung
Problemstellungen zu Kapitel 32
1023
q = σ(Ti−1 ) weiterhin gilt, bleibt gültig aufgrund des in Übung 32.4-8 gegebenen Hinweises, dass δ(m, a) = δ(π[m], a), d. h. σ(P a) = σ(Pπ[m] a), für ein beliebiges a ∈ Σ gilt.) Das noch fehlende Argument für die Korrektheit des Knuth-Morris-Pratt-Algorithmus folgt aus der Korrektheit von Finite-Automaton-Matcher, da wir gezeigt haben, dass KMP-Matcher das Verhalten von Finite-Automaton-Matcher simuliert.
Übungen 32.4-1 Berechnen Sie die Präfixfunktion π für das Muster ababbabbabbababbabb. 32.4-2 Geben Sie eine obere Schranke für die Größe von π ∗ [q] als Funktion in q an und ein Beispiel, das zeigt, dass Ihre Schranke scharf ist. 32.4-3 Erläutern Sie, wie wir die Vorkommen eines Musters P in einem Text T finden können, indem wir die π-Funktion des Strings P T (d. h. des Strings der Länge m + n, der durch Konkatenation von P und T entsteht) untersuchen. 32.4-4 Wenden Sie eine Aggregat-Analyse an, um zu zeigen, dass die Laufzeit von KMP-Matcher in Θ(n) liegt. 32.4-5 Wenden Sie eine Potentialfunktion an, um zu zeigen, dass die Laufzeit von KMP-Matcher in Θ(n) liegt. 32.4-6 Zeigen Sie, wie wir KMP-Matcher verbessern können, indem wir in Zeile 7 (aber nicht in Zeile 12) π durch π ersetzen, wobei π rekursiv für q = 1, 2, . . . , m − 1 durch die Gleichung ⎧ falls π[q] = 0 , ⎪ ⎨0 falls π[q] = 0 und P [π[q] + 1] = P [q + 1] , π [q] = π [π[q]] ⎪ ⎩ π[q] falls π[q] = 0 und P [π[q] + 1] = P [q + 1] definiert ist. Erläutern Sie, weshalb der modifizierte Algorithmus korrekt arbeitet und in welchem Sinne diese Modifikation eine Verbesserung darstellt. 32.4-7 Geben Sie einen Linearzeit-Algorithmus an, der feststellt, ob ein Text T eine zyklische Rotation eines anderen Strings T ist. Zum Beispiel sind arc und car zyklische Rotationen voneinander. 32.4-8∗ Geben Sie einen effizienten Algorithmus mit Laufzeit O(m |Σ|) für die Berechnung der Übergangsfunktion δ für den zu einem gegebenen Muster P gehörigen String-Matching-Automaten an. (Hinweis: Zeigen Sie, dass δ(q, a) = δ(π[q], a) gilt, wenn q = m oder P [q + 1] = a ist.)
Problemstellungen 32-1 String-Matching auf Basis von Wiederholungsfaktoren Lassen Sie uns die i-malige Verknüpfung eines Strings y mit sich selbst mit y i
1024
32 String-Matching bezeichnen. Beispielsweise ist (ab)3 = ababab. Wir sagen, dass ein String x ∈ Σ∗ den Wiederholungsfaktor r besitzt, wenn für einen String y und ein r > 0 die Gleichung x = y r gilt. Wir bezeichnen mit ρ(x) den größten Wiederholungsfaktor von x. a. Geben Sie einen effizienten Algorithmus an, der als Eingabe das Muster P [1 . . m] bekommt und den Wert ρ(Pi ) für i = 1, 2, . . . , m berechnet. Wie ist die Laufzeit Ihres Algorithmus? b. Für ein beliebiges Muster P [1 . . m] sei ρ∗ (P ) durch max1≤i≤m ρ(Pi ) definiert. Beweisen Sie, dass der Erwartungswert von ρ∗ (P ) in O(1) ist, wenn das Muster P zufällig aus der Menge aller binären Strings der Länge m gewählt wird. c. Erklären Sie, warum der folgende String-Matching-Algorithmus alle Vorkommen des Musters P im Text T [1 . . n] in Zeit O(ρ∗ (P )n + m) korrekt bestimmt. Repetition-Matcher(P, T ) 1 m = P.l¨a nge 2 n = T.l¨a nge 3 k = 1 + ρ∗ (P ) 4 q =0 5 s=0 6 while s ≤ n − m 7 if T [s + q + 1] = = P [q + 1] 8 q = q+1 9 if q = = m 10 print “Das Muster kommt vor mit der Verschiebung” s 11 if q = = m oder T [s + q + 1] = P [q + 1] 12 s = s + max(1, q/k ) 13 q =0 Dieser Algorithmus geht auf Galil und Seiferas zurück. Indem sie die hier geschilderten Ideen nochmals stark erweitert haben, erhielten sie einen StringMatching-Algorithmus mit linearer Laufzeit, der, abgesehen vom Speicherbedarf für P und T , nur O(1) zusätzlichen Speicher verwendet.
Kapitelbemerkungen Der Zusammenhang zwischen dem String-Matching und der Theorie endlicher Automaten wird von Aho, Hopcroft und Ullman [5] diskutiert. Der Knuth-Morris-PrattAlgorithmus [214] wurde unabhängig voneinander von Knuth und Pratt sowie von Morris entwickelt; sie veröffentlichten ihre Arbeit aber gemeinsam. Reingold, Urban und Gries [294] zeigen eine alternative Aufbereitung des Knuth-Morris-Pratt-Algorithmus. Der Rabin-Karp-Algorithmus wurde von Rabin und Karp [201] vorgeschlagen. Galil und Seiferas [126] geben einen interessanten deterministischen String-Matching-Algorithmus an, der lineare Zeit benötigt und nur O(1) Speicherplatz mehr verbraucht, als zum Speichern des Textes und des Musters notwendig ist.
33
Algorithmische Geometrie
Die algorithmische Geometrie ist der Zweig der Informatik, der Algorithmen zum Lösen geometrischer Probleme untersucht. Sie besitzt in den modernen Ingenieurwissenschaften und in der Mathematik Anwendungen in so unterschiedlichen Gebieten wie graphische Datenverarbeitung, Robotertechnik, Entwurf integrierter Schaltungen (VLSIEntwurf), rechnergestützte Konstruktion (engl.: computer-aided design), molekulare Modellierung, Metallurgie, Textil-Layout, Forstwissenschaft und Statistik. Die Eingabe eines Problems der algorithmischen Geometrie besteht gewöhnlich aus einer Beschreibung einer Menge geometrischer Objekte, wie beispielsweise einer Punktmenge, einer Menge von Strecken oder einer Menge von Ecken eines Polygons in einer dem Uhrzeigersinn entgegengesetzten Reihenfolge. Die Ausgabe ist häufig eine Antwort auf eine Anfrage bezüglich der Objekte, wie beispielsweise die Frage danach, ob sich Strecken schneiden oder die Frage nach einem neuen geometrischen Objekt, wie der konvexen Hülle (kleinstes alle Punkte umschließendes Polygon) der Punktmenge. In diesem Kapitel betrachten wir einige Algorithmen der algorithmischen Geometrie in zwei Dimensionen, also in der Ebene. Wir stellen jedes Eingabeobjekt durch eine Punktmenge {p1 , p2 , p3 , . . .} mit pi = (xi , yi ) und xi , yi ∈ R dar. Zum Beispiel stellen wir ein Polygon P mit n Ecken durch eine Folge p0 , p1 , p2 , . . . , pn−1 seiner Ecken in der Reihenfolge ihres Auftretens auf dem Rand von P dar. Algorithmische Geometrie kann auch auf drei Dimensionen und sogar in noch höherdimensionalen Räumen angewendet werden. Solche Probleme und deren Lösungen können außerordentlich schwer zu visualisieren sein. Aber schon in zwei Dimensionen können wir einen guten Einblick in die Methoden der algorithmischen Geometrie gewinnen. Abschnitt 33.1 zeigt, wie wir einige grundlegende Fragen zu Strecken effizient und exakt beantworten können: ob eine Strecke im Uhrzeigersinn oder entgegen dem Uhrzeigersinn in Bezug auf eine andere Strecke liegt, mit der sie einen Endpunkt teilt, ob wir nach links oder rechts abbiegen, wenn wir zwei gegebene aneinander grenzende Strecken traversieren, und ob sich zwei Strecken schneiden. Abschnitt 33.2 stellt eine als „Sweeping“ bezeichnete Methode vor, die wir anwenden, um einen Algorithmus mit Laufzeit O(n lg n) zu entwickeln, der bestimmt, ob eine Menge von n Strecken Schnittpunkte enthält. Abschnitt 33.3 gibt zwei „Rotationssweep“-Algorithmen an, die die konvexe Hülle einer Menge von n Punkten berechnen: den in Zeit O(n lg n) laufenden GrahamScan-Algorithmus und den in Zeit O(nh) laufenden Jarvis Marsch-Algorithmus, wobei h die Anzahl der Ecken der konvexen Hülle ist. Schließlich wird in Abschnitt 33.4 ein in Zeit O(n lg n) laufender Teile-und-Beherrsche-Algorithmus vorgestellt, der das am dichtesten zusammenliegende Paar von Punkten in einer Menge von n in der Ebene liegenden Punkten bestimmt.
1026
33.1
33 Algorithmische Geometrie
Eigenschaften von Strecken
Einige der in diesem Kapitel behandelten Algorithmen benötigen Antworten auf Fragen zu Eigenschaften von Strecken. Eine konvexe Kombination zweier verschiedener Punkte p1 = (x1 , y1 ) und p2 = (x2 , y2 ) ist ein beliebiger Punkt p3 = (x3 , y3 ), für den x3 = αx1 + (1 − α)x2 und y3 = αy1 + (1 − α)y2 für ein α mit 0 ≤ α ≤ 1 gilt. Wir können auch p3 = αp1 + (1 − α)p2 schreiben. Anschaulich bedeutet dies, dass sich p3 auf der durch die Punkte p1 und p2 verlaufenden Geraden befindet, und auf dem Punkt p1 oder p2 liegt oder zwischen den Punkten p1 und p2 auf der Geraden liegt. Sind zwei verschiedene Punkte p1 und p2 gegeben, dann ist die Strecke p1 p2 die Menge der konvexen Kombinationen von p1 und p2 . Wir bezeichnen p1 und p2 als Endpunkte der Strecke p1 p2 . Manchmal ist die Reihenfolge von p1 und p2 von Bedeutung. Dann sprechen wir → von der gerichteten Strecke − p− 1 p2 . Wenn p1 der Ursprung (0, 0) ist, dann können − − → wir für die gerichtete Strecke p1 p2 den Vektor p2 nehmen. In diesem Abschnitt werden wir die folgenden Fragen untersuchen: −−→ −−→ −−→ → 1. Für zwei gegebene gerichtete Strecken − p− 0 p1 und p0 p2 , liegt p0 p1 von p0 p2 aus gesehen im Uhrzeigersinn bezüglich ihres gemeinsamen Endpunktes p0 ? 2. Für zwei gegebene Strecken p0 p1 und p1 p2 , bewegen wir uns am Punkt p1 nach links, wenn wir zuerst die Strecke p0 p1 und anschließend die Strecke p1 p2 durchlaufen? 3. Schneiden sich die Strecken p1 p2 und p3 p4 ? Es gibt keine Beschränkungen in Bezug auf die gegebenen Punkte. Wir können jede Frage in Zeit O(1) beantworten, was nicht überraschen sollte, da die Eingabegröße jeder Frage O(1) ist. Darüber hinaus verwenden unsere Methoden nur Additionen, Subtraktionen, Multiplikationen und Vergleiche. Wir benötigen weder die Division noch trigonometrische Funktionen, die beide sehr rechenaufwendig sein können und zu Rundungsfehlern neigen. Die einfache Methode zur Bestimmung, ob sich zwei Strecken schneiden, – Berechnung der Geradengleichung y = mx + b für jede der beiden Strecken, wobei m jeweils der Anstieg und b der Schnittpunkt mit der y-Achse ist, Bestimmung des Schnittpunktes der beiden Geraden und Überprüfung, ob sich dieser Punkt auf beiden Strecken befindet –, beispielsweise verwendet zum Bestimmen des Schnittpunktes die Division und ist wegen der Ungenauigkeit der Divisionsoperation auf realen Rechnern sehr störanfällig, d. h. wenig robust, wenn die Strecken nahezu parallel verlaufen. Die in diesem Abschnitt vorgestellte Methode vermeidet die Division und ist deshalb viel akkurater.
Kreuzprodukte Die Berechnung von Kreuzprodukten spielt eine zentrale Rolle bei unseren Methoden auf Strecken. Betrachten Sie die in Abbildung 33.1(a) gezeigten Vektoren p1 und p2 . Wir können das Kreuzprodukt p1 × p2 als den vorzeichenbehafteten Flächeninhalt des durch die Punkte (0, 0), p1 , p2 und p1 +p2 = (x1 +x2 , y1 +y2 ) gebildeten Parallelogramms
33.1 Eigenschaften von Strecken y
1027 y
p1 + p2
p
p2 (0,0)
x p1 (0,0)
x (a)
(b)
Abbildung 33.1: (a) Das Kreuzprodukt der Vektoren p1 und p2 ist der vorzeichenbehaftete Flächeninhalt des Parallelogramms. (b) Der schwach schattierte Bereich enthält die Vektoren, die sich von p aus gesehen in Richtung Uhrzeigersinn befinden. Der stark schattierte Bereich enthält die Vektoren, die sich von p aus gesehen entgegen dem Uhrzeigersinn befinden.
interpretieren. Eine äquivalente, aber für unsere Zwecke nützlichere Definition definiert das Kreuzprodukt als Determinante einer Matrix1
x1 x2 p1 × p2 = det y1 y2 = x1 y2 − x2 y1 = −p2 × p1 . Wenn p1 × p2 positiv ist, dann befindet sich p1 gegenüber p2 bezüglich des Ursprungs in Richtung Uhrzeigersinn. Ist dieses Kreuzprodukt negativ, dann liegt p1 gegenüber p2 in der entgegengesetzten Richtung (siehe Übung 33.1-1). Abbildung 33.1(b) zeigt die Gebiete, die sich im Uhrzeigersinn und entgegen dem Uhrzeigersinn bezüglich eines Vektors p befinden. Ein Grenzfall tritt auf, wenn das Kreuzprodukt den Wert 0 hat; in diesem Fall sind die Vektoren kollinear, sie zeigen entweder in die gleiche oder in entgegengesetzte Richtungen. → Wollen wir bestimmen, ob sich eine gerichtete Strecke − p− 0 p1 gegenüber einer gerichteten − − → Strecke p p näher im Uhrzeigersinn oder näher entgegen des Uhrzeigersinns befindet, 0 2
dann verschieben wir die Vektoren einfach so, dass wir p0 als Ursprung verwenden können. Das heißt, wir arbeiten mit dem Vektor p1 − p0 , den wir p1 = (x1 , y1 ) nennen wollen, wobei x1 = x1 − x0 und y1 = y1 − y0 ist. Analog bezeichnen wir mit dem Vektor p2 den Vektor p2 − p0 . Anschließend berechnen wir das Kreuzprodukt (p1 − p0 ) × (p2 − p0 ) = (x1 − x0 )(y2 − y0 ) − (x2 − x0 )(y1 − y0 ) . → Falls dieses Kreuzprodukt positiv ist, dann befindet sich − p− 0 p1 im Uhrzeigersinn ge− − → − − → → genüber p0 p2 ; ist es negativ, dann befindet sich p0 p1 gegenüber − p− 0 p2 entgegen dem Uhrzeigersinn. 1 Eigentlich entstammt das Kreuzprodukt einem dreidimensionalen Konzept. Es stellt einen Vektor dar, der sowohl zu p1 als auch zu p2 gemäß der „Rechtehandregel“ senkrecht steht und dessen Betrag |x1 y2 − x2 y1 | ist. In diesem Kapitel ist es für uns dienlicher, das Kreuzprodukt einfach als den Wert x1 y2 − x2 y1 anzusehen.
1028
33 Algorithmische Geometrie p2
p2 p1
entgegen dem Uhrzeigersinn p0
p1
im Uhrzeigersinn p0
(a)
(b)
Abbildung 33.2: Die Verwendung des Kreuzproduktes, um zu bestimmen, wie die Strecken → p0 p1 und p1 p2 am Punkt p1 zueinander liegen. Wir überprüfen, ob sich die Strecke − p− 0 p2 im − − → oder entgegen dem Uhrzeigersinn in Bezug auf die Strecke p0 p1 befindet. (a) Wenn sie entgegen dem Uhrzeigersinn liegt, so wird an dem Knoten p1 nach links abgebogen. (b) Wenn sie im Uhrzeigersinn liegt, so wird am Knoten p1 nach rechts abgebogen.
Bestimmen, ob ein Streckenzug nach links oder rechts abbiegt Unsere nächste Frage lautet, ob zwei aufeinander folgende Strecken p0 p1 und p1 p2 am Punkt p1 nach links oder nach rechts abbiegen. Anders formuliert suchen wir nach einer Methode, mit der wir bestimmen können, in welche Richtung sich ein Winkel ∠p0 p1 p2 wendet. Kreuzprodukte erlauben uns, diese Frage zu beantworten, ohne den Winkel zu berechnen. Wie Abbildung 33.2 zeigt, überprüfen wir einfach, ob sich die → gerichtete Strecke − p− 0 p2 im Uhrzeigersinn oder entgegen dem Uhrzeigersinn in Bezug → auf die gerichtete Strecke − p− 0 p1 befindet. Dazu berechnen wir das Kreuzprodukt (p2 − → p0 ) × (p1 − p0 ). Wenn das Vorzeichen dieses Kreuzproduktes negativ ist, dann ist − p− 0 p2 − − → entgegen dem Uhrzeigersinn bezüglich p0 p1 gedreht und so biegen wir an der Stelle p1 nach links ab. Ein positives Kreuzprodukt zeigt eine Drehung im Uhrzeigersinn an und somit eine Biegung nach rechts. Ein Kreuzprodukt von 0 bedeutet, dass die Punkte p0 , p1 und p2 kollinear sind.
Bestimmen, ob sich zwei Strecken schneiden Um zu entscheiden, ob sich zwei Strecken schneiden, überprüfen wir zunächst, ob jede Strecke die Gerade der jeweils anderen Strecke überspannt. Eine Strecke p1 p2 überspannt eine Gerade, wenn der Punkt p1 auf einer Seite der Geraden und der Punkt p2 auf der anderen Seite der Geraden liegt. Ein Grenzfall liegt vor, wenn p1 oder p2 direkt auf der Geraden liegt. Zwei Strecken schneiden sich genau dann, wenn mindestens eine der beiden folgenden Bedingungen erfüllt ist: 1. Jede Strecke überspannt die Gerade der anderen Strecke. 2. Ein Endpunkt einer Strecke liegt auf der anderen Strecke. (Diese Bedingung stellt den Grenzfall dar.) Die folgenden Prozeduren implementieren diese Idee. Die Prozedur Segments-Intersect gibt wahr zurück, wenn sich die Strecken p1 p2 und p3 p4 schneiden und falsch, wenn sie dies nicht tun. Sie ruft zwei Unterroutinen auf. Es sind die Prozedur Direction, die die relative Orientierung der Strecken mithilfe der oben angegebenen
33.1 Eigenschaften von Strecken
1029
Kreuzprodukt-Methode bestimmt, und die Prozedur On-Segment, die bestimmt, ob ein Punkt, der kollinear zu einer Strecke ist, auf dieser liegt. Segments-Intersect(p1 , p2 , p3 , p4 ) 1 d1 = Direction(p3 , p4 , p1 ) 2 d2 = Direction(p3 , p4 , p2 ) 3 d3 = Direction(p1 , p2 , p3 ) 4 d4 = Direction(p1 , p2 , p4 ) 5 if ((d1 > 0 und d2 < 0) oder (d1 < 0 und d2 > 0)) und ((d3 > 0 und d4 < 0) oder (d3 < 0 und d4 > 0)) 6 return wahr 7 elseif d1 = = 0 und On-Segment(p3 , p4 , p1 ) 8 return wahr 9 elseif d2 = = 0 und On-Segment(p3 , p4 , p2 ) 10 return wahr 11 elseif d3 = = 0 und On-Segment(p1 , p2 , p3 ) 12 return wahr 13 elseif d4 = = 0 und On-Segment(p1 , p2 , p4 ) 14 return wahr 15 else return falsch Direction(pi , pj , pk ) 1 return (pk − pi ) × (pj − pi ) On-Segment(pi , pj , pk ) 1 if min(xi , xj ) ≤ xk ≤ max(xi , xj ) und min(yi , yj ) ≤ yk ≤ max(yi , yj ) 2 return wahr 3 else return falsch Segments-Intersect arbeitet folgendermaßen. Die Zeilen 1–4 berechnen die relative Orientierung di jedes Endpunktes pi bezüglich der jeweils anderen Strecke. Wenn alle relativen Orientierungen von 0 verschieden sind, dann können wir leicht bestimmen, ob sich die Strecken p1 p2 und p3 p4 schneiden. Dabei gehen wir wie folgt vor. Die Strecke p1 p2 überspannt die Gerade, auf der die Strecke p3 p4 liegt, wenn die gerichteten Stre→ −−→ −−→ cken − p− 3 p1 und p3 p2 entgegengesetzte Orientierungen bezüglich der Strecke p3 p4 haben. In diesem Fall unterscheiden sich die Vorzeichen von d1 und d2 . Analog dazu überspannt die Strecke p3 p4 die Gerade, die die Strecke p1 p2 enthält, wenn sich die Vorzeichen von d3 und d4 unterscheiden. Wenn der Test in Zeile 5 wahr ist, dann überspannen sich die Strecken einander und Segments-Intersect gibt wahr zurück. Abbildung 33.3(a) zeigt diesen Fall. Anderenfalls überspannen die Strecken die Gerade der jeweils anderen Strecke nicht; es kann jedoch der Grenzfall vorliegen. Wenn alle relativen Orientierungen von 0 verschieden sind, dann kann kein Grenzfall vorliegen. Alle Tests auf 0 in den Zeilen 7–13 scheitern und Segments-Intersect gibt in Zeile 15 falsch zurück. Abbildung 33.3(b) veranschaulicht diesen Fall.
1030
33 Algorithmische Geometrie (p1–p3) × (p4–p3) < 0 p1
p4 (p4–p1) × (p2–p1) < 0
(p1–p3) × (p4–p3) < 0 p1
p4 (p4–p1) × (p2–p1) < 0
p2
(p2–p3) × (p4–p3) < 0 p2
(p3–p1) × (p2–p1) > 0
(p2–p3) × (p4–p3) > 0
p3
p3
(b)
(a)
p4
p1
(p3–p1) × (p2–p1) > 0
p4
p1
p3 p2 (c)
p2 (d)
p3
Abbildung 33.3: Die unterschiedlichen Fälle in der Prozedur Segments-Intersect. (a) Die Strecken p1 p2 und p3 p4 überspannen die Gerade der jeweils anderen Strecke. Da p3 p4 die Gerade, die p1 p2 enthält, überspannt, sind die Vorzeichen der Kreuzprodukte (p3 − p1 ) × (p2 − p1 ) und (p4 − p1 ) × (p2 − p1 ) verschieden. Weil p1 p2 die Gerade, die die Strecke p3 p4 enthält, überspannt, sind die Vorzeichen der Kreuzprodukte (p1 − p3 ) × (p4 − p3 ) und (p2 − p3 ) × (p4 − p3 ) verschieden. (b) Die Strecke p3 p4 überspannt die Gerade, die die Strecke p1 p2 enthält, aber die Strecke p1 p2 überspannt die Gerade mit der Strecke p3 p4 nicht. Die Vorzeichen der Kreuzprodukte (p1 − p3 ) × (p4 − p3 ) und (p2 − p3 ) × (p4 − p3 ) sind gleich. (c) Der Punkt p3 ist kollinear zur Strecke p1 p2 und liegt zwischen p1 und p2 . (d) Der Punkt p3 ist kollinear zur Strecke p1 p2 , liegt aber nicht zwischen p1 und p2 . Die Strecken schneiden sich nicht.
Ein Grenzfall tritt auf, wenn eine beliebige relative Orientierung dk den Wert 0 hat. In diesem Fall wissen wir, dass pk zur anderen Strecke kollinear ist. Der Punkt befindet sich genau dann auf der Strecke, wenn er sich zwischen den Endpunkten der anderen Strecke befindet. Die Prozedur On-Segment gibt zurück, ob sich pk zwischen den Endpunkten der Strecke pi pj befindet, wobei es sich beim Aufruf in den Zeilen 7–13 um die andere Strecke handelt. Die Prozedur setzt voraus, dass pk kollinear zur Strecke pi pj ist. Die Abbildungen 33.3(c) und (d) zeigen Fälle mit kollinearen Punkten. In Abbildung 33.3(c) liegt p3 auf der Strecke p1 p2 . Folglich gibt Segments-Intersect in Zeile 12 wahr zurück. Keiner der Endpunkte befindet sich in Abbildung 33.3(d) auf der anderen Strecke, sodass Segments-Intersect in Zeile 15 falsch zurückgibt.
Weitere Anwendungen für Kreuzprodukte Die nachfolgenden Kapiteln stellen weitere Anwendungen des Kreuzproduktes vor. In Abschnitt 33.3 sortieren wir eine Menge von Punkten nach ihren Polarwinkeln (bezüglich eines gegebenen Ursprungs). In Übung 33.1-3 sollen Sie zeigen, dass wir Kreuzprodukte verwenden können, um die Vergleiche während der Sortierprozedur auszuführen. In Abschnitt 33.2 werden wir Rot-Schwarz-Bäume verwenden, um die vertikale Ordnung einer Menge von Strecken zu erhalten. Anstatt explizite Schlüsselwerte zu verwalten,
33.1 Eigenschaften von Strecken
1031
die wir in dem Code eines Rot-Schwarz-Baumes miteinander vergleichen, werden wir ein Kreuzprodukt berechnen, um zu bestimmen, welche von zwei eine gegebene vertikale Gerade schneidenden Strecken über der anderen liegt.
Übungen 33.1-1 Beweisen Sie, dass sich der Vektor p1 im Uhrzeigersinn gegenüber p2 bezüglich des Ursprungs (0, 0) befindet, wenn p1 × p2 positiv ist. Wenn dieses Kreuzprodukt negativ ist, dann befindet sich p1 gegenüber p2 entgegen dem Uhrzeigersinn. 33.1-2 Professor van Pelt schlägt vor, dass nur die x-Koordinate in Zeile 1 von OnSegment getestet werden muss. Zeigen Sie, weshalb der Professor Unrecht hat. 33.1-3 Der Polarwinkel eines Punktes p1 bezüglich des Ursprungs p0 ist der Winkel des Vektors p1 − p0 im gewöhnlichen Polarkoordinatensystem. Beispielsweise ist der Polarwinkel von (3, 5) bezüglich (2, 4) der Winkel des Vektors (1, 1), der 45◦ bzw. π/4 beträgt. Der Polarwinkel von (3, 3) bezüglich (2, 4) ist der Winkel des Vektors (1, −1), der 315◦ bzw. 7π/4 beträgt. Schreiben Sie eine Prozedur in Pseudocode, um eine Sequenz p1 , p2 , . . . , pn von n Punkten nach ihren Polarwinkeln bezüglich eines gegebenen Ursprungs p0 zu sortieren. Ihr Programm sollte Zeit O(n lg n) benötigen und Kreuzprodukte zum Vergleich der Winkel verwenden. 33.1-4 Zeigen Sie, wie wir in Zeit O(n2 lg n) bestimmen können, ob es in einer Menge von n Punkten drei Punkte gibt, die kollinear sind. 33.1-5 Ein Polygon ist eine stückweise lineare geschlossene Kurve in der Ebene. Das heißt, es ist eine Kurve, die bei sich selbst endet und durch eine Sequenz von Strecken gebildet wird, die als Seiten des Polygons bezeichnet werden. Ein Punkt, der zu zwei aufeinander folgenden Seiten gehört, ist eine Ecke des Polygons. Wenn das Polygon einfach ist, was wir im Allgemeinen voraussetzen werden, dann schneidet es sich nicht selbst. Die Menge der in der Ebene durch das Polygon eingeschlossenen Punkte bildet das Innere des Polygons, die Menge der Punkte auf dem Polygon selbst bildet den Rand, und die Menge der das Polygon umgebenden Punkte bildet sein Äußeres. Ein einfaches Polygon heißt konvex, wenn für je zwei beliebige Punkte auf dem Rand oder aus dem Innern des Polygons alle Punkte der die beiden Punkte verbindenden Strecke im Rand des Polygons oder in seinem Inneren enthalten sind. Eine Ecke eines konvexen Polygons kann nicht als eine konvexe Kombination von zwei verschiedenen Punkten des Randes oder des Innern des Polygons dargestellt werden. Professor Amundsen schlägt die folgende Methode vor, um zu bestimmen, ob eine Sequenz p0 , p1 , . . . , pn−1 von n Punkten die aufeinander folgenden Ecken eines konvexen Polygons bildet. Sie gibt „ ja“ aus, wenn die Menge {∠pi pi+1 pi+2 : i = 0, 1, . . . , n − 1} nicht sowohl linke als auch rechte Biegungen enthält – die Addition in den Indizes wird modulo n ausgeführt. Anderenfalls
1032
33 Algorithmische Geometrie gibt sie „nein“ aus. Zeigen Sie, dass die Methode zwar in linearer Zeit läuft, aber nicht immer die korrekte Antwort liefert. Modifizieren Sie die Methode des Professors so, dass sie in linearer Zeit immer die korrekte Antwort liefert.
33.1-6 Der rechte horizontale Strahl eines gegebenen Punktes p0 = (x0 , y0 ) ist die Punktmenge {pi = (xi , yi ) : xi ≥ x0 und yi = y0 }, d. h. die Menge aller Punkte rechts von p0 zusammen mit p0 selbst. Zeigen Sie, wie wir in Zeit O(1) bestimmen können, ob der rechte horizontale Strahl eines gegebenen Punktes p0 eine Strecke p1 p2 schneidet, indem Sie das Problem auf die Entscheidung reduzieren, ob sich zwei Strecken schneiden. 33.1-7 Eine Möglichkeit zu bestimmen, ob sich ein Punkt p0 im Inneren eines einfachen, aber nicht notwendigerweise konvexen Polygons P befindet, besteht darin, dass wir uns jeden von p0 ausgehenden Strahl ansehen und überprüfen, ob der Strahl den Rand des Polygons in einer ungeraden Anzahl von Punkten schneidet, aber p0 sich nicht selbst auf dem Rand befindet. Zeigen Sie, wie wir in Zeit Θ(n) berechnen können, ob ein Punkt p0 zum Inneren eines n-eckigen Polygons P gehört. (Hinweis: Verwenden Sie Übung 33.1-6. Stellen Sie sicher, dass Ihr Algorithmus korrekt arbeitet, wenn der Strahl den Rand des Polygons an einer Ecke schneidet und wenn der Strahl eine Kante des Polygons überdeckt.) 33.1-8 Zeigen Sie, wie wir den Flächeninhalt eines einfachen n-eckigen, aber nicht notwendigerweise konvexen Polygons in Zeit Θ(n) berechnen können. (Siehe Übung 33.1-5 für die Definitionen zu Polygonen.)
33.2
Bestimmung von Schnittpunkten in einer Menge von Strecken
Dieser Abschnitt stellt Algorithmen vor, die bestimmen, ob eine Menge von Strecken zwei Strecken enthält, die sich schneiden. Der Algorithmus wendet eine als „Sweeping“ bekannte Methode an, die bei vielen Anwendungen der algorithmischen Geometrie vorkommt. Wie die Übungen am Ende dieses Abschnittes zeigen werden, können diese Algorithmen oder einfache Variationen davon helfen, andere Probleme der algorithmischen Geometrie zu lösen. Der Algorithmus läuft in Zeit O(n lg n), wobei n die Anzahl der gegebenen Strecken ist. Er bestimmt lediglich, ob ein Schnittpunkt existiert oder nicht; er gibt die Schnittpunkte nicht alle aus. (Nach Übung 33.2-1 benötigt er im schlechtesten Fall Zeit Ω(n2 ), um alle Schnittpunkte in einer Menge von n Strecken zu bestimmen.) Beim Sweeping (deutsch: Abtasten) bewegt sich eine imaginäre vertikale Sweep-Line (soviel wie „wandernde Gerade“) gewöhnlich von links nach rechts durch eine gegebene Menge geometrischer Objekte. Wir interpretieren die räumliche Dimension, durch die sich die Sweep-Line bewegt, in diesem Fall die x-Dimension, als Zeitachse. Sweeping liefert eine Methode, um geometrische Objekte zu ordnen, wobei diese in der Regel in einer dynamischen Datenstruktur zusammengefasst werden, und um räumliche Beziehungen
33.2 Bestimmung von Schnittpunkten in einer Menge von Strecken
1033
e d a
b
g
i
h
c
f r
t
u (a)
v
z (b)
w
Abbildung 33.4: Die Ordnung verschiedener Strecken an verschiedenen vertikalen SweepLines. (a) Es gilt a r c, a t b, b t c, a t c und b u c. Die Strecke d ist mit keiner anderen gezeigten Strecke vergleichbar. (b) Wenn sich die Strecken e und f schneiden, vertauschen sie ihre Reihenfolgen: Es gilt e v f , aber f w e. Jede Sweep-Line (wie beispielsweise z), die in dem schattierten Bereich liegt, enthält e und f direkt aufeinander folgend bezüglich der durch die Relation z gegebenen Ordnung.
zwischen ihnen zu bestimmen (und später auszunutzen). Der in diesem Abschnitt vorgestellte Algorithmus zur Bestimmung von Schnittpunkten von Strecken betrachtet alle Endpunkte der Strecken in der Reihenfolge von links nach rechts und prüft jedes Mal auf einen Schnittpunkt, wenn ein Endpunkt hinzugenommen wird. Um unseren Algorithmus zu beschreiben und zu beweisen, dass er korrekt feststellt, ob sich irgendwelche zwei Strecken aus einer Menge von n Strecken schneiden, werden wir zwei vereinfachende Annahmen machen. Erstens setzen wir voraus, dass keine Eingabestrecke vertikal verläuft. Zweitens setzen wir voraus, dass sich nicht drei Strecken in dem gleichen Punkt schneiden. In den Übungen 33.2-8 und 33.2-9 sollen Sie zeigen, dass der Algorithmus robust genug ist, sodass nur geringfügige Modifikationen notwendig sind, um auch dann korrekt zu arbeiten, wenn diese Voraussetzungen nicht erfüllt sind. Oft stellen jedoch der Verzicht auf solche vereinfachenden Voraussetzungen und die Behandlung von Randbedingungen den schwierigsten Teil der Programmierung von Algorithmen der algorithmischen Geometrie und des Beweises ihrer Korrektheit dar.
Ordnen von Strecken Da wir voraussetzen, dass es keine vertikalen Strecken gibt, schneidet jede Eingabestrecke, die eine gegebene vertikale Sweep-Line schneidet, diese genau an einem Punkt. Folglich können wir die Strecken, die eine vertikale Sweep-Line schneiden, nach den y-Koordinaten dieser Schnittpunkte ordnen. Um es genauer zu machen, betrachten Sie zwei Strecken s1 und s2 . Wir sagen, dass diese Strecken vergleichbar an der Stelle x sind, wenn die vertikale Sweep-Line an der Stelle x beide Strecken schneidet. Wir sagen, dass s1 an der Stelle x oberhalb von s2 liegt, in Zeichen s1 x s2 , wenn s1 und s2 an der Stelle x vergleichbar sind und der Schnittpunkt von s1 mit der Sweep-Line an der Stelle x oberhalb des Schnittpunktes von s2 mit der Sweep-Line an der Stelle x liegt. In Abbildung 33.4(a) gelten beispielsweise die Relationen a r c, a t b, b t c, a t c und b u c. Die Strecke d ist mit keiner anderen Strecke vergleichbar.
1034
33 Algorithmische Geometrie
Für ein beliebiges gegebenes x bildet die Relation „x “ eine totale Vorordnung (siehe Abschnitt B.2) auf allen Strecken, die die Sweep-Line an der Stelle x schneiden. Die Relation ist also transitiv und wenn die Segmente s1 und s2 die Sweep-Line an x schneiden, dann gilt entweder s1 x s2 oder s2 x s1 oder beides (wenn s1 und s2 sich an x schneiden). (Die Relation x ist auch reflexiv, aber weder symmetrisch noch antisymmetrisch.) Die vollständige Vorordnung kann für verschiedene Werte von x unterschiedlich sein, da Strecken zu der Ordnung hinzukommen oder die Ordnung verlassen können. Eine Strecke kommt zu der Ordnung hinzu, wenn ihr linker Endpunkt durch die Sweep-Line berührt wird, und verlässt die Ordnung, wenn ihr rechter Endpunkt auf die Sweep-Line trifft. Was passiert, wenn die Sweep-Line über den Schnittpunkt zweier Strecken läuft? Wie Abbildung 33.4(b) zeigt, vertauschen die Segmente ihre Reihenfolge bezüglich der totalen Vorordnung. Die Sweep-Lines v und w befinden sich rechts beziehungsweise links vom Schnittpunkt der Strecken e und f , und es gilt e v f und f w e. Da wir vorausgesetzt haben, dass sich keine drei Strecken in einem Punkt schneiden dürfen, gibt es eine vertikale Sweep-Line x, sodass die sich schneidenden Strecken e und f direkt aufeinander folgen in der zu x gehörigen totalen Vorordnung x . Jede Sweep-Line wie beispielsweise z, die den schattierten Bereich in Abbildung 33.4(b) durchläuft, hat e und f bezüglich ihrer totalen Vorordnung direkt nebeneinander.
Bewegen der Sweep-Line Sweeping-Algorithmen verwalten gewöhnlich zwei Datenmengen: 1. Der Sweep-Line-Status gibt die Beziehungen der Objekte an, die die Sweep-Line schneidet. 2. Die Ereignisliste ist eine Folge von Punkten, die wir Ereignispunkte nennen und von links nach rechts bezüglich ihrer x-Koordinate geordnet sind. Die Sweep-Line bewegt sich von links nach rechts und stoppt jedesmal, wenn sie die x-Koordinate eines Ereignispunktes erreicht, um diesen Ereignispunkt zu verarbeiten; nach der Verarbeitung des Ereignispunktes setzt sie ihre Bewegung nach rechts wieder fort. Änderungen an dem Sweep-Line-Status erfolgen nur an Ereignispunkten. Bei einigen Algorithmen (beispielsweise bei dem in Übung 33.2-7 behandelten Algorithmus) entwickelt sich die Ereignisliste während des Ablaufs des Algorithmus dynamisch. Der hier betrachtete Algorithmus bestimmt jedoch (auf Grundlage einfacher Eigenschaften der Eingabedaten) alle Ereignispunkte, bevor der eigentliche Sweep beginnt. Speziell ist jeder Endpunkt einer Strecke ein Ereignispunkt. Wir sortieren die Streckenendpunkte nach steigender x-Koordinate und arbeiten uns von links nach rechts vor. (Wenn zwei oder mehr Endpunkte kovertikal sind, d. h. die gleiche x-Koordinate besitzen, dann platzieren wir alle linken kovertikalen Endpunkte vor den rechten kovertikalen Endpunkten. Die linken kovertikalen Endpunkte sortieren wir monoton steigend nach ihren y-Koordinaten; genauso verfahren wir bei den rechten kovertikalen Endpunkten.) Wenn die Sweep-Line den linken Endpunkt einer Strecke trifft, dann fügen wir diese Strecke in den Sweep-Line-Status ein. Wir entfernen die Strecke aus dem Sweep-Line-Status,
33.2 Bestimmung von Schnittpunkten in einer Menge von Strecken
1035
wenn die Sweep-Line auf ihren rechten Endpunkt trifft. Immer wenn zwei Strecken in der totalen Vorordnung das erste Mal direkt hintereinander stehen, überprüfen wir, ob sie sich schneiden. Der Sweep-Line-Status ist eine totale Vorordnung T , für die wir folgende Operationen benötigen. • Insert(T, s): fügt die Strecke s in T ein. • Delete(T, s): entfernt die Strecke s aus T . • Above(T, s): gibt die Strecke zurück, die sich in T unmittelbar oberhalb der Strecke s befindet. • Below(T, s): gibt die Strecke zurück, die sich in T unmittelbar unterhalb der Strecke s befindet. Es ist möglich, dass für zwei Strecken s1 und s2 sowohl s1 oberhalb von s2 als auch s2 oberhalb von s1 in der totalen Vorordnung T liegt; diese Situation tritt auf, wenn s1 und s2 sich an der Sweep-Line, dessen totale Vorordnung durch T gegeben ist, schneiden. In diesem Fall dürfen wir die zwei Strecken in einer beliebigen der beiden Reihenfolgen in T platzieren. Wenn die Eingabe aus n Strecken besteht, dann können wir jede der Operationen Insert, Delete, Above und Below mithilfe von Rot-Schwarz-Bäumen in Zeit O(lg n) ausführen. Erinnern Sie sich aus Kapitel 13 daran, dass die Operationen auf RotSchwarz-Bäumen Schlüssel miteinander vergleichen. Wir können diese Vergleiche von Schlüsseln durch Vergleiche ersetzen, die unter Verwendung von Kreuzprodukten die relative Ordnung zweier Strecken bestimmen (siehe Übung 33.2-2).
Pseudocode zur Bestimmung von Streckenschnittpunkten Der auf der nächsten Seite stehende Algorithmus verwendet als Eingabe eine Menge S von n Strecken, wobei er den Booleschen Wert wahr zurückgibt, wenn sich ein Paar von Strecken aus S schneidet und anderenfalls falsch. Ein Rot-Schwarz-Baum verwaltet die totale Vorordnung T . Abbildung 33.5 illustriert die Arbeitsweise des Algorithmus. Zeile 1 initialisiert die totale Vorordnung mit der leeren Menge. Zeile 2 bestimmt die Ereignisliste durch Sortieren der 2n Streckenendpunkte von links nach rechts, wobei Mehrdeutigkeiten wie oben erläutert aufgelöst werden. Eine Möglichkeit, Zeile 2 zu realisieren, besteht darin, dass wir die Endpunkte jeweils durch das Tripel (x, e, y) darstellen, wobei x und y die gewöhnlichen Koordinaten des Endpunktes sind und wir e bei einem linken Endpunkt auf 0 und bei einem rechten Endpunkt aus 1 setzen, und diese Tripel dann lexikographisch sortieren.
1036
33 Algorithmische Geometrie
a
e
d
c f b a
a b
a c b
d a c b
d c b
e d c b Zeit
Abbildung 33.5: Die Arbeitsweise von Any-Segments-Intersect. Jede gestrichelte Linie stellt eine Sweep-Line an einem Ereignispunkt dar. Mit Ausnahme der rechten Sweep-Line, korrespondiert die Reihenfolge der Streckennamen unterhalb einer Sweep-Line jeweils der totalen Vorordnung T am Ende der for-Schleife, die den „aktuellen“ Ereignispunkt bearbeitet. Die rechte Sweep-Line tritt auf, wenn der rechte Endpunkt des Segmentes c bearbeitet wird; da die Strecken d und b die Strecke c umgeben und sich gegenseitig schneiden, gibt die Prozedur den Wert wahr zurück.
Any-Segments-Intersect(S) 1 T =∅ 2 sortiere die Endpunkte der Strecken in S von links nach rechts, löse Mehrdeutigkeiten auf, indem linke Endpunkte vor rechten Endpunkten gesetzt werden, und löse weitere Mehrdeutigkeiten auf, indem Punkte mit niedrigerer y-Koordinate zuerst gesetzt werden 3 for alle Punkte p in der sortierten Liste von Endpunkten 4 if p ist der linke Endpunkt einer Strecke s 5 Insert(T, s) 6 if (Above(T, s) existiert und schneidet s) oder (Below(T, s) existiert und schneidet s) 7 return wahr 8 if p ist der rechte Endpunkt einer Strecke s 9 if sowohl Above(T, s) als auch Below(T, s) existieren und Above(T, s) schneidet Below(T, s) 10 return wahr 11 Delete(T, s) 12 return falsch Jede Iteration der for-Schleife der Zeilen 3–11 bearbeitet einen Ereignispunkt p. Wenn p der linke Endpunkt einer Strecke s ist, dann fügt die Zeile 5 s in die totale Vorordnung ein und die Zeilen 6–7 geben wahr zurück, wenn s eine der in der totalen Vorordnung zu ihr benachbarten Strecken schneidet, wobei die totale Vorordnung durch die durch
33.2 Bestimmung von Schnittpunkten in einer Menge von Strecken
1037
p verlaufende Sweep-Line bestimmt wird. (Ein Spezialfall tritt auf, wenn p auf einer weiteren Strecke s liegt. In diesem Fall fordern wir nur, dass s und s in T aufeinander folgend gesetzt werden.) Wenn p der rechte Endpunkt einer Strecke s ist, dann haben wir s aus der totalen Vorordnung zu entfernen. Zuerst geben aber die Zeilen 9–10 den Wert wahr zurück, wenn es zwischen den die Strecke s direkt umgebenden (wieder bezüglich der zu p gehörigen totalen Vorordnung) Strecken einen Schnittpunkt gibt. Wenn sich diese Strecken nicht schneiden, dann entfernt die Zeile 11 die Strecke s aus der totalen Vorordnung. Wenn die beiden die Strecke s umgebenden Strecken sich schneiden, dann würden sie benachbart werden, wenn wir s aus der totalen Vorordnung entfernen würden. Das Korrektheitsargument, das in dem nächsten Absatz folgt, macht klar, warum es ausreicht, nur die Segmente zu überprüfen, die s direkt umgeben. Schlussendlich, wenn wir keine Schnittpunkte gefunden haben, nachdem wir alle 2n Ereignispunkte bearbeitet haben, gibt Zeile 12 den Wert falsch zurück.
Korrektheit Um zu zeigen, dass Any-Segments-Intersect korrekt arbeitet, werden wir beweisen, dass der Aufruf Any-Segments-Intersect(S) genau dann wahr zurückgibt, wenn es einen Schnittpunkt zwischen den in S enthaltenen Strecken gibt. Es ist leicht zu sehen, dass die Prozedur Any-Segments-Intersect genau dann wahr (in den Zeilen 7 und 10) zurückgibt, wenn sie einen Schnittpunkt zweier Eingabestrecken findet. Folglich gibt es einen Schnittpunkt, wenn wahr zurückgegeben wird. Wir müssen auch die Umkehrung zeigen: Wenn es einen Schnittpunkt gibt, dann gibt die Prozedur Any-Segments-Intersect wahr zurück. Lassen Sie uns voraussetzen, dass es mindestens einen Schnittpunkt gibt. Sei p der am weitesten links liegende Schnittpunkt – sollte damit der Schnittpunkt nicht eindeutig bestimmt sein, so wählen wir unter diesen Punkten den mit der kleinsten y-Koordinate – und seien a und b die Strecken, die sich am Punkt p schneiden. Da kein Schnittpunkt links von p auftritt, ist die durch T gegebene Ordnung an allen links von p liegenden Punkten korrekt. Da sich drei Strecken nicht am gleichen Punkt schneiden, werden a und b benachbart in einer totalen Vorordnung an einer Sweep-Line z.2 Darüber hinaus befindet sich z links von p oder verläuft durch p. Ein Streckenendpunkt q auf der Sweep-Line z ist der Ereignispunkt, an dem a und b bezüglich der totalen Vorordnung benachbart werden. Wenn sich p auf der Sweep-Line z befindet, dann ist q = p. Ist p nicht auf der Sweep-Line z, dann befindet sich q links von p. In beiden Fällen ist die durch T gegebene Ordnung, unmittelbar bevor wir auf q stoßen, korrekt. (Hier arbeiten wir nach der lexikographischen Ordnung, in der der Algorithmus die Ereignispunkte bearbeitet. Da p der kleinste, am weitesten links liegende Schnittpunkt ist, wird, auch wenn sich p auf der Sweep-Line z befindet und es einen anderen Schnittpunkt p auf z gibt, der Ereignispunkt q = p bearbeitet, bevor der andere Schnittpunkt p in der totalen Vorordnung T zum Tragen kommt. Darüber hinaus gilt, auch wenn p der linke Endpunkt einer Strecke, sagen wir mal a, und der 2 Wenn wir zulassen, dass sich drei Strecken am gleichen Punkt schneiden, kann es eine dazwischen liegende Strecke c geben, die sowohl a und b am Punkt p schneidet. Das heißt, es könnte a w c und c w b für alle links von p liegenden Sweep-Lines w gelten, für die a w b ist. Übung 33.2-8 verlangt von Ihnen zu zeigen, dass Any-Segments-Intersect auch dann korrekt arbeitet, wenn sich drei Strecken in dem gleichen Punkt schneiden.
1038
33 Algorithmische Geometrie
rechte Endpunkt der anderen Strecke, sagen wir mal b, ist, dass sich die Strecke b in T befindet, wenn wir der Strecke a erstmalig begegnen, weil Ereignisse linker Endpunkte vor Ereignissen rechter Endpunkte eintreten.) Entweder wird der Ereignispunkt q durch Any-Segments-Intersect bearbeitet oder nicht. Wenn q von Any-Segments-Intersect bearbeitet wird, dann kann nur eine von zwei möglichen Aktionen durchgeführt werden: 1. Entweder wird a oder b in T eingefügt, und die andere Strecke befindet sich oberhalb oder unterhalb dieser Strecke bezüglich der totalen Vorordnung. Die Zeilen 4–7 erkennen diesen Fall. 2. Die Strecken a und b befinden sich bereits in T und die Strecke, die bezüglich der totalen Ordnung zwischen ihnen liegt, wurde entfernt, wodurch a und b dann benachbart sind. Die Zeilen 8–11 erkennen diesen Fall. In beiden Fällen finden wir den Schnittpunkt und Any-Segments-Intersect gibt den Wert wahr zurück. Wenn der Ereignispunkt q durch Any-Segments-Intersect nicht bearbeitet wird, muss die Prozedur ins Hauptprogramm zurückgekehrt sein, bevor alle Ereignispunkte bearbeitet wurden. Diese Situation kann nur dann eintreten, wenn Any-SegmentsIntersect bereits einen Schnittpunkt gefunden und wahr zurückgegeben hat. Folglich gibt Any-Segments-Intersect wahr zurück, falls ein Schnittpunkt existiert. Wie wir bereits gesehen haben, gibt es einen Schnittpunkt, wenn die Prozedur wahr zurückgibt. Deshalb gibt die Prozedur Any-Segments-Intersect immer eine korrekte Antwort zurück.
Laufzeit Wenn die Menge S n Strecken enthält, dann läuft die Prozedur in Zeit O(n lg n). Zeile 1 benötigt Zeit O(1). Zeile 2 benötigt unter Verwendung von Mergesort oder Heapsort Zeit O(n lg n). Die for-Schleife in den Zeilen 3–11 iteriert höchstens einmal pro Ereignispunkt, und somit iteriert sie mit den 2n Ereignispunkten höchstens 2n mal. Jede Iteration benötigt Zeit O(lg n), weil jede Operation auf Rot-Schwarz-Bäumen Zeit O(lg n) benötigt. Außerdem verbraucht jeder Test auf einen Schnittpunkt unter Verwendung der Methode aus Abschnitt 33.1 Zeit O(1). Die Gesamtlaufzeit ist deshalb O(n lg n).
Übungen 33.2-1 Zeigen Sie, dass eine Menge von n Strecken Θ(n2 ) Schnittpunkte enthalten kann. 33.2-2 Gegeben seien zwei Strecken a und b, die an der Stelle x vergleichbar sind. Zeigen Sie, wie wir in Zeit O(1) bestimmen können, ob a x b oder b x a gilt. Setzen Sie dabei voraus, dass keine Strecke vertikal verläuft. (Hinweis: Wenn sich a und b nicht schneiden, können Sie einfach Kreuzprodukte verwenden. Wenn sich a und b schneiden – was Sie natürlich auch unter Verwendung
33.3 Bestimmen der konvexen Hülle
1039
von Kreuzprodukten feststellen können – dürfen Sie nur Addition, Subtraktion und Multiplikation verwenden; Division darf nicht angewendet werden. Natürlich können wir bei der hier betrachteten Anwendung der Relation x einfach anhalten, wenn sich a und b schneiden, und ausgeben, dass wir einen Schnittpunkt gefunden haben.) 33.2-3 Professor Mason schlägt vor, die Prozedur Any-Segments-Intersect so zu modifizieren, dass sie nicht zurückspringt, wenn sie einen Schnittpunkt gefunden hat, sondern die Strecken ausgibt, die sich schneiden, und dann mit der nächsten Iteration der for-Schleife fortfährt. Der Professor nennt die resultierende Prozedur Print-Intersecting-Segments und behauptet, dass sie alle Schnittpunkte von links nach rechts, so wie sie in der Menge der Strecken vorkommen, ausgibt. Professor Dixon glaubt das nicht und behauptet, dass Professor Masons Idee falsch ist. Welcher der beiden Professoren hat recht? Findet die Prozedur Print-Intersecting-Segments immer den sich am weitesten links befindlichen Schnittpunkt als ersten? Findet sie alle Schnittpunkte? 33.2-4 Geben Sie einen Algorithmus mit Laufzeit O(n lg n) an, der feststellt, ob ein n-eckiges Polygon einfach ist. 33.2-5 Geben Sie einen Algorithmus mit Laufzeit O(n lg n) an, der feststellt, ob sich zwei einfache Polygone mit insgesamt n Ecken schneiden. 33.2-6 Eine Kreisscheibe besteht aus einem Kreis und seinem Inneren und wird durch seinen Mittelpunkt und seinen Radius bestimmt. Zwei Kreisscheiben schneiden sich, wenn sie einen Punkt gemeinsam haben. Geben Sie einen Algorithmus mit Laufzeit O(n lg n) an, der bestimmt, ob es in einer Menge von n Kreisscheiben zwei Kreisscheiben gibt, die sich schneiden. 33.2-7 Gegeben sei eine Menge von n Strecken mit insgesamt k Schnittpunkten. Zeigen Sie, wie wir alle k Schnittpunkte in Zeit O((n + k) lg n) ausgeben können. 33.2-8 Zeigen Sie, dass Any-Segments-Intersect auch dann korrekt arbeitet, wenn sich drei oder mehr Strecken in dem gleichen Punkt schneiden. 33.2-9 Zeigen Sie, dass Any-Segments-Intersect auch dann korrekt arbeitet, wenn Strecken vertikal verlaufen dürfen, sofern wir den unteren Endpunkt einer vertikalen Strecke als linken Endpunkt und den oberen Endpunkt als rechten Endpunkt ansehen. Wie lautet Ihre Antwort auf Übung 33.2-2, wenn wir vertikale Strecken erlauben?
33.3
Bestimmen der konvexen Hülle
Die konvexe Hülle einer Menge Q von Punkten, die wir mit CH(Q) bezeichnen, ist das kleinste konvexe Polygon P , für das sich jeder Punkt aus Q entweder auf dem Rand von P oder in seinem Inneren befindet. (Siehe Übung 33.1-5 für eine präzise
1040
33 Algorithmische Geometrie p10
p11 p12
p7 p6
p9 p8
p5 p3
p4 p2 p1
p0 Abbildung 33.6: Eine Punktmenge Q = {p0 , p1 , . . . , p12 } mit der zugehörigen, grau gezeichneten konvexen Hülle CH(Q).
Definition eines konvexen Polygons.) Wir setzen implizit voraus, dass alle Punkte in der Menge Q „Unikate“ sind und dass Q wenigstens drei Punkte enthält, die nicht kollinear sind. Anschaulich können wir uns jeden Punkt von Q als einen Nagel vorstellen, der aus einem Brett herausragt. Die konvexe Hülle hat dann die Form, die durch ein straffes Gummiband gebildet wird, das alle Nägel umschließt. Abbildung 33.6 zeigt eine Punktmenge und ihre konvexe Hülle. In diesem Abschnitt werden wir zwei Algorithmen vorstellen, die die konvexe Hülle einer Menge von n Punkten berechnen. Beide Algorithmen geben die Ecken der konvexen Hülle entgegen dem Uhrzeigersinn aus. Der erste, als Grahams Scan-Methode bekannte Algorithmus läuft in Zeit O(n lg n). Der zweite, der unter dem Namen Jarvis MarschAlgorithmus bekannt ist, läuft in Zeit O(nh), wobei h die Anzahl der Ecken der konvexen Hülle ist. Wie Abbildung 33.6 illustriert, ist jede Ecke von CH(Q) ein Punkt in Q. Beide Algorithmen nutzen diese Eigenschaft aus, wobei Sie jeweils entscheiden, welche Punkte von Q als Ecken der konvexen Hülle zu behalten sind und welche Punkte von Q ausgelassen werden können. Wir können konvexe Hüllen in Zeit O(n lg n) mithilfe mehrere Methoden berechnen. Sowohl Grahams Scan-Methode als auch Jarvis Marsch verwenden eine als „Rotationssweep“ bezeichnete Technik, die die Ecken in der Reihenfolge ihrer Polarwinkel bearbeitet, die sie mit einer Referenzecke bilden. Andere Methoden lassen sich wie folgt charakterisieren: • Bei der inkrementellen Methode sortieren wir zuerst die Punkte von links nach rechts, was zu einer Sequenz p1 , p2 , . . . , pn führt. Im i-ten Schritt aktualisieren wir die konvexe Hülle der i − 1 am weitesten links liegenden Punkte CH({p1 , p2 , . . . , pi−1 }) mit dem i-ten Punkt von links, wodurch die konvexe Hülle CH({p1 , p2 , . . . , pi }) gebildet wird. Übung 33.3-6 verlangt von Ihnen zu zeigen, wie wir diese Methode so implementieren können, dass sie insgesamt nur Zeit O(n lg n) benötigt. • Bei der Teile-und-Beherrsche-Methode teilen wir die Menge von n Punkten in Zeit Θ(n) in zwei Teilmengen, von denen eine die n/2 am weitesten links
33.3 Bestimmen der konvexen Hülle
1041
liegenden Punkte und die andere die n/2 am weitesten rechts liegenden Punkte enthält, berechnen rekursiv die konvexen Hüllen der Teilmengen und setzen dann die beiden konvexen Hüllen mit einer ausgefeilten Methode in Zeit O(n) zu der konvexen Hülle aller Punkte zusammen. Die Laufzeit wird durch die bekannte Rekursionsgleichung T (n) = 2T (n/2) + O(n) beschrieben, sodass die Teile-undBeherrsche-Methode in Zeit O(n lg n) läuft. • Die Abschneiden-und-Suchen-Methode ähnelt dem Median-Algorithmus aus Abschnitt 9.3. Mit dieser Methode bestimmen wir den oberen Teil (oder die „obere Kette“) der konvexen Hülle durch wiederholtes Verwerfen eines konstanten Anteils der noch verbliebenen Punkte, bis nur die obere Kette der konvexen Hülle übrig bleibt. Dann behandelt der Algorithmus die untere Kette in gleicher Art und Weise. Die Methode ist die asymptotisch schnellste: Wenn die konvexe Hülle h Ecken enthält, benötigt sie nur Zeit O(n lg h). Das Berechnen der konvexen Hülle einer Punktmenge ist für sich schon ein interessantes Problem. Darüber hinaus beginnen Algorithmen für einige andere Probleme der algorithmischen Geometrie mit der Berechnung der konvexen Hülle. Betrachten wir beispielsweise das zweidimensionale Problem des am weitesten auseinander liegenden Paares (engl.: farthest-pair problem): Gegeben ist eine Menge von n Punkten in der Ebene und wir wollen die beiden Punkte bestimmen, deren Entfernung voneinander maximal ist. Wie Übung 33.3-3 von Ihnen verlangt zu zeigen, müssen diese beiden Punkte Ecken der konvexen Hülle sein. Auch wenn wir dies hier nicht beweisen werden, können wir das am weitesten auseinander liegende Paar von Ecken eines n-eckigen konvexen Polygons in Zeit O(n) bestimmen. Folglich können wir das am weitesten auseinander liegende Paar von Punkten aus einer beliebige Menge vom n Punkten in Zeit O(n lg n) bestimmen, indem wir die konvexe Hülle der n Eingabepunkte in Zeit O(n lg n) berechnen und anschließend das am weitesten auseinander liegende Paar von Ecken des resultierenden konvexen Polygons bestimmen.
Grahams Methode Grahams Methode löst das Problem der konvexen Hülle, indem sie einen Stapel S aus Punktkandidaten verwaltet. Sie legt jeden Punkt der Eingabemenge Q zunächst auf den Stapel und entfernt jeden Punkt, der keine Ecken der konvexen Hülle CH(Q) ist, letztendlich wieder vom Stapel. Wenn der Algorithmus terminiert, enthält der Stapel S genau die Ecken von CH(Q) entgegen dem Uhrzeigersinn ihres Auftretens auf dem Rand. Die Prozedur Graham-Scan erhält als Eingabe eine Punktmenge Q, wobei |Q| ≥ 3 ist. Sie ruft die Funktionen Top(S), die den obersten Punkt im Stapel S zurückgibt, ohne den Stapel zu verändern, und Next-To-Top(S) auf, die den darunter liegenden Punkt im Stapel S ohne Veränderung des Stapels zurückgibt. Wie wir gleich beweisen werden, enthält der von Graham-Scan zurückgegebene Stapel genau die Ecken der konvexen Hülle CH(Q) in der dem Uhrzeigersinn entgegengesetzten Reihenfolge.
1042
33 Algorithmische Geometrie
Graham-Scan(Q) 1 sei p0 der Punkt von Q mit der minimalen y-Koordinate, oder der am weitesten links liegende solche Punkt 2 seien p1 , p2 , . . . , pm die restlichen Punkte von Q, die nach dem Polarwinkel relativ zu p0 in der dem Uhrzeigersinn entgegengesetzten Reihenfolge sortiert sind (wenn mehr als ein Punkt denselben Winkel besitzt, dann entferne alle außer dem am weitesten von p0 entfernt liegenden) 3 if m < 2 4 return “Die konvexe Hülle ist leer” 5 sei S ein leerer Stapel 6 Push(p0 , S) 7 Push(p1 , S) 8 Push(p2 , S) 9 for i = 3 to m 10 while der von den Punkten Next-To-Top(S), Top(S), und pi gebildete Winkel ist nicht nach links gerichtet 11 Pop(S) 12 Push(pi , S) 13 return S Abbildung 33.7 illustriert die Arbeitsweise von Graham-Scan. Zeile 1 wählt den Punkt p0 als denjenigen Punkt mit der kleinsten y-Koordinate, wobei sie im Falle von Mehrdeutigkeiten den am weitesten links liegenden von diesen auswählt. Weil es in Q keinen Punkt unterhalb von p0 gibt und sich alle anderen Punkte mit der selben y-Koordinate rechts von ihm befinden, muss p0 eine Ecke von CH(Q) sein. Zeile 2 sortiert die verbleibenden Punkte von Q gemäß ihrem Polarwinkel relativ zu p0 mithilfe der gleichen Methode wie in Übung 33.1-3 – indem sie Kreuzprodukte vergleicht. Wenn zwei oder mehr Punkte denselben Polarwinkel relativ zu p0 haben, sind alle außer dem am weitesten entfernt liegenden Punkt konvexe Kombinationen von p0 und dem entferntesten Punkt. Folglich können wir sie völlig aus den weiteren Betrachtungen ausschließen. Wir bezeichnen die Anzahl der verbleibenden Punkte, ohne p0 , mit m. Der in Radianten gemessene Polarwinkel jedes Punktes in Q relativ zu p0 nimmt Werte aus dem halboffenen Intervall [0, π) an. Da die Punkte nach ihren Polarwinkeln sortiert sind, sind sie relativ zu p0 entgegen dem Uhrzeigersinn sortiert. Wir bezeichnen diese sortierte Punktsequenz mit p1 , p2 , . . . , pm . Beachten Sie, dass die Punkte p1 und pm Ecken von CH(Q) sind (siehe Übung 33.3-1). Abbildung 33.7(a) zeigt die Punkte aus Abbildung 33.6 nacheinander in der Reihenfolge steigender Polarwinkel relativ zu p0 . Der Rest der Prozedur verwendet den Stapel S. Die Zeilen 5–8 initialisieren den Stapel, der nun von unten nach oben die ersten drei Punkte p0 , p1 und p2 enthält. Abbildung 33.7(a) zeigt den Anfangsstapel S. Die for-Schleife der Zeilen 9–12 iteriert einmal für jeden Punkt in der Teilsequenz p3 , p4 , . . . , pm . Wir werden sehen, dass nach der Bearbeitung des Punktes pi der Stapel S, von unten nach oben betrachtet, die Ecken der konvexen Hülle CH({p0 , p1 , . . . , pi }) entgegen dem Uhrzeigersinn enthält. Die whileSchleife der Zeilen 10–11 entnimmt Punkte aus dem Stapel, wenn wir feststellen, dass sie
33.3 Bestimmen der konvexen Hülle
1043 p10
p10
p9
p11
p7
p6
p8
p9
p11
p12
p3
p2
p6
p8
p5 p4
p7
p5 p4
p12
p2 p1
p1 p0
p0
(a)
p10
(b)
p10
p9
p11
p7
p6
p8
p9
p11 p3
p4 p2
p7
p6 p5
p8
p5
p12
p0
p1 p0
(c)
p10
(d)
p10
p9
p7
p6
p8
p11 p5 p4
p12
p3
p2
p9
p7 p8
(e)
p6 p5 p4
p12
p3
p2
p1 p0
p3
p4 p2
p12
p1
p11
p3
p1 p0
(f)
Abbildung 33.7: Die Arbeitsweise von Graham-Scan auf der Menge Q aus Abbildung 33.6. Die aktuelle im Stapel S enthaltene konvexe Hülle ist in jedem Schritt grau eingezeichnet. (a) Die Sequenz p1 , p2 , . . . , p12 von Punkten in der Reihenfolge steigender Polarwinkel relativ zu p0 und der ursprüngliche Stapel S, der p0 , p1 und p2 enthält. (b)-(k) Der Stapel S nach jeder Iteration der for-Schleife der Zeile 9–12. Gestrichelte Linien kennzeichnen Biegungen, die nicht nach links verlaufen, was zum Entfernen dieser Punkte aus dem Stapel führt. [...]
1044
33 Algorithmische Geometrie p10
p10
p9
p11
p7
p6
p11
p6
p9 p7
p5 p8
p12
p4
p3
p2
p5
p8
p12
p4 p2
p1 p0
p1 p0
(g)
p10
(h)
p10
p11 p9
p7
p6
p9 p5 p4
p8
p12
p3
p2
p7
p6
p8
p11
p5 p4
p12
p0
p1 p0
(i)
p10
(j)
p10
p9
p7
p6
p8
p9 p5 p4
p12
p3
p2
p11
p7 p8
(k)
p6 p5 p4
p12
p3
p2
p1 p0
p3
p2
p1
p11
p3
p1 p0
(l)
Abbildung 33.7, fortgesetzt: [...] In Teil (h) bewirkt beispielsweise die Rechtsbiegung am Winkel ∠p7 p8 p9 , dass p8 aus dem Stapel entfernt wird; anschließend bewirkt die Rechtsbiegung am Winkel ∠p6 p7 p9 , dass p7 entfernt wird. (l) Die von der Prozedur zurückgegebene konvexe Hülle, die der aus Abbildung 33.6 entspricht.
33.3 Bestimmen der konvexen Hülle
1045
nicht zur konvexen Hülle gehören. Wenn wir die konvexe Hülle entgegen dem Uhrzeigersinn durchlaufen, müssen wir an jeder Ecke eine Biegung nach links vollziehen. Folglich entfernen wir jedesmal, wenn die while-Schleife einen Punkt findet, an dem keine Biegung nach links erfolgt, diesen Punkt vom Stapel. (Da wir überprüfen, ob keine Linksbiegung vorliegt anstatt einfach auf Rechtsbiegung zu testen, schließt dieser Test die Möglichkeit eines gestreckten Winkels an einem Eckpunkt der resultierenden konvexen Hülle aus. Wir sind nicht an gestreckten Winkeln interessiert, weil kein Eckpunkt eines konvexen Polygons eine konvexe Kombination anderer Eckpunkte des Polygons sein darf.) Nachdem wir alle Ecken, an denen wir nicht nach links abbiegen, wenn wir pi ansteuern, vom Stapel entfernt haben, legen wir pi auf den Stapel. Die Abbildungen 33.7(b)–(k) zeigen den Zustand des Stapels S nach jeder Iteration der for-Schleife. Letztendlich gibt Graham-Scan in Zeile 13 den Stapel S zurück. Abbildung 33.7(l) zeigt die entsprechende konvexe Hülle. Das folgende Theorem beweist formal die Korrektheit von Graham-Scan Theorem 33.1: (Korrektheit von Grahams Methode) Wenn Graham-Scan auf einer Punktmenge Q mit |Q| ≥ 3 ausgeführt wird, dann enthält der Stapel S bei Terminierung von unten nach oben genau die Punkte der konvexen Hülle CH(Q) in der dem Uhrzeigersinn entgegengesetzten Reihenfolge. Beweis: Nach Zeile 2 haben wir die Punktesequenz p1 , p2 , . . . , pm . Lassen Sie uns die Teilmengen Qi der Punkte Qi = {p0 , p1 , . . . , pi } für i = 2, 3, . . . , m definieren. Die Punkte in Q − Qm sind diejenigen, die entfernt wurden, weil sie denselben Polarwinkel relativ zu p0 wie ein Punkt in Qm hatten; diese Punkte sind nicht in CH(Q) enthalten und folglich gilt CH(Qm ) = CH(Q). Deshalb reicht es aus zu zeigen, dass, wenn Graham-Scan terminiert, der Stapel S aus den Eckpunkten der konvexen Hülle CH(Qm ) besteht und zwar in der Reihenfolge entgegen dem Uhrzeigersinn, wenn man die Knoten im Stack von unten nach oben auflistet. Beachten Sie, dass, genau wie die Punkte p0 , p1 und pm Eckpunkte der konvexen Hülle CH(Q) sind, die Punkte p0 , p1 und pi alle Eckpunkte von CH(Qi ) sind. Der Beweis verwendet die folgende Schleifeninvariante: Zu Beginn jeder Iteration der for-Schleife der Zeilen 9–12 besteht der Stapel S von unten nach oben betrachtet genau aus den Eckpunkten von CH(Qi−1 ) entgegen dem Uhrzeigersinn. Initialisierung: Beim ersten Ausführen von Zeile 9 gilt die Invariante, da der Stapel S zu diesem Zeitpunkt genau aus den Eckpunkten Q2 = Qi−1 besteht und diese aus drei Punkten bestehende Menge ihre eigene konvexe Hülle bildet. Darüber hinaus erscheinen sie von unten nach oben betrachtet in der dem Uhrzeigersinn entgegengesetzten Reihenfolge. Fortsetzung: Betreten wir eine Iteration der for-Schleife, so ist der oberste Punkt auf dem Stapel S der Punkt pi−1 , der am Ende der vorhergehenden Iteration auf
1046
33 Algorithmische Geometrie pj
pj
pk
pi
pr
pi pt
Qj
p2 p1
p0
p1 p0
(a)
(b)
Abbildung 33.8: Der Korrektheitsbeweis von Graham-Scan. (a) Weil der Polarwinkel von pi relativ zu p0 größer als der von pj ist und weil der Winkel ∠pk pj pi nach links verläuft, führt das Hinzufügen von pi zu CH(Qj ) genau auf die Eckpunkte von CH(Qj ∪ {pi }). (b) Wenn der Winkel ∠pr pt pi nicht nach links verläuft, dann befindet sich pt entweder im Inneren des durch p0 , pr und pi gebildeten Dreiecks oder liegt auf einer Seite des Dreiecks. Folglich kann er kein Eckpunkt von CH(Qi ) sein.
dem Stapel abgelegt wurde (oder vor der ersten Iteration, wenn i = 3 gilt.) Sei pj der oberste Punkt auf dem Stapel S, nachdem die while-Schleife der Zeilen 10–11 ausgeführt wurde, aber bevor Zeile 12 den Punkt pi auf den Stapel legt. Sei außerdem pk der Punkt unmittelbar unterhalb von pj auf S. Zu dem Zeitpunkt, an dem pj der oberste Punkt auf S ist und wir pi noch nicht abgelegt haben, enthält der Stapel S genau dieselben Punkte, die er nach der j-ten Iteration der for-Schleife enthielt. Wegen der Schleifeninvariante enthält S deshalb zu diesem Zeitpunkt genau die Eckpunkte von CH(Qj ), wiederum in der dem Uhrzeigersinn entgegengesetzten Reihenfolge, betrachten wir die Knoten im Stapel von unten nach oben. Lassen Sie uns, uns auf den Zeitpunkt unmittelbar vor dem Ablegen von pi auf den Stapel konzentrieren. Wir wissen, dass der Polarwinkel von pi relativ zu p0 größer als der Polarwinkel von pj ist und der Winkel ∠pk pj pi eine Linksbiegung vollführt (anderenfalls hätten wir pj vom Stapel genommen). Da S genau die Eckpunkte der konvexen Hülle CH(Qj ) enthält, sehen wir deshalb aus Abbildung 33.8(a), dass, wenn wir einmal pi auf den Stapel abgelegt haben, Stapel S zu diesem Zeitpunkt genau die Eckpunkte der konvexen Hülle CH(Qj ∪ {pi }) enthält und diese im Stapel in der dem Uhrzeigersinn entgegengesetzten Richtung erscheinen, betrachten wir den Stapel von unten nach oben. Wir zeigen nun, dass CH(Qj ∪ {pi }) die gleiche Punktmenge wie CH(Qi ) ist. Betrachten Sie einen beliebigen Punkt pt , der im Verlauf der Iteration i der forSchleife vom Stapel entnommen wurde, und sei pr der Punkt unmittelbar unter pt auf dem Stapel S zu dem Zeitpunkt, an dem pt entnommen wurde (pr kann pj sein). Der Winkel ∠pr pt pi macht keine Linksbiegung und der Polarwinkel von pt relativ zu p0 ist größer als der Polarwinkel von pr . Wie Abbildung 33.8(b) zeigt, muss pt entweder im Inneren des durch p0 , pr und pi gebildeten Dreiecks oder
33.3 Bestimmen der konvexen Hülle
1047
auf einer Seite dieses Dreiecks liegen (er ist aber kein Eckpunkt dieses Dreiecks). Weil pt innerhalb des von den drei anderen Punkten gebildeten Dreiecks von Qi liegt, kann es offensichtlich kein Eckpunkt der konvexen Hülle CH(Qi ) sein. Weil pt kein Eckpunkt der konvexen Hülle CH(Qi ) ist, gilt CH(Qi − {pt }) = CH(Qi ) .
(33.1)
Sei Pi die Punktmenge, die wir bei der Iteration i der for-Schleife dem Stapel entnommen haben. Da Gleichung (33.1) für alle Punkte aus Pi anwendbar ist, können wir sie wiederholt anwenden, um zu zeigen, dass CH(Qi − Pi ) = CH(Qi ) gilt. Es gilt aber Qi − Pi = Qj ∪ {pi } und somit folgt CH(Qj ∪ {pi }) = CH(Qi − Pi ) = CH(Qi ). Wir haben gezeigt, dass, wenn wir pi auf den Stapel gelegt haben, der Stapel S genau die Eckpunkte von CH(Qi ) enthält und diese in der dem Uhrzeigersinn entgegengesetzten Reihenfolge erscheinen, betrachten wir den Stapel von unten nach oben. Das Inkrementieren von i bewirkt dann, dass die Schleifeninvariante auch in der nächsten Iteration gilt. Terminierung: Wenn die Schleife terminiert, gilt i = m + 1 und somit impliziert die Schleifeninvariante, dass der Stapel S genau aus den Eckpunkten von CH(Qm ) (was gleich CH(Q) ist) in der dem Uhrzeigersinn engegengesetzten Reihenfolge von unten nach oben besteht. Damit ist der Beweis abgeschlossen. Wir zeigen nun, dass die Laufzeit von Graham-Scan O(n lg n) mit n = |Q| ist. Zeile 1 benötigt Zeit Θ(n). Zeile 2 benötigt unter Verwendung von Mergesort oder Heapsort zum Sortieren der Polarwinkel und der Kreuzproduktmethode aus Abschnitt 33.1 zum Vergleich der Winkel Zeit O(n lg n). (Wir können alle außer dem entferntesten Punkt mit denselben Polarwinkeln in Gesamtzeit O(n) entfernen.) Die Zeilen 5–8 benötigen Zeit O(1). Wegen m ≤ n − 1 wird die for-Schleife der Zeilen 9–12 höchstens (n − 3)-mal ausgeführt. Da Push Zeit O(1) benötigt, nimmt jede Iteration Zeit O(1) in Anspruch, zuzüglich der in der while-Schleife der Zeilen 10–11 verbrachten Zeit. Folglich braucht die for-Schleife, ohne Berücksichtigung der Zeit für die innere while-Schleife, insgesamt Zeit O(n). Wir zeigen mithilfe der Aggregat-Analyse, dass die while-Schleife insgesamt Zeit O(n) benötigt. Für i = 0, 1, . . . , m legen wir jeden Punkt pi genau einmal auf den Stapel S. Wie bei der Analyse der Prozedur Multipop in Abschnitt 17.1 stellen wir fest, dass wir höchstens so viele Objekte vom Stapel entfernen können, wie wir auf den Stapel legen. Mindestens drei Punkte – p0 , p1 und pm – werden niemals vom Stapel genommen, sodass insgesamt tatsächlich höchstens m − 2 Pop-Operationen ausgeführt werden. Jede Iteration der while-Schleife führt eine Pop-Operation aus, sodass es insgesamt höchstens m − 2 Iterationen der while-Schleife gibt. Da der Test in Zeile 10 Zeit O(1) benötigt, jeder Aufruf von Pop ebenfalls nur Zeit O(1) in Anspruch nimmt und m ≤ n − 1 gilt, ist die von der while-Schleife verbrauchte Gesamtzeit O(n). Folglich ist die Laufzeit von Graham-Scan O(n lg n).
1048
33 Algorithmische Geometrie
Jarvis Marsch Jarvis Marsch berechnet die konvexe Hülle einer Punktmenge Q mithilfe einer als Package-Wrapping (oder Gift-Wrapping, zu deutsch: Geschenkeeinpacken) bekannten Methode. Der Algorithmus läuft in Zeit O(nh), wobei h die Anzahl der Eckpunkte innerhalb der konvexen Hülle CH(Q) ist. Wenn h in o(lg n) ist, läuft Jarvis Marsch asymptotisch schneller als Grahams Methode. Anschaulich gesehen simuliert Jarvis Marsch das Umhüllen einer Menge Q mit einem straffen Papierstück. Wir beginnen damit, das Ende des Papiers mit dem untersten Punkt der Menge zu verkleben. Das ist der Punkt p0 , mit dem wir auch in Grahams Methode starten. Wir wissen, dass dieser Punkt ein Eckpunkt der konvexen Hülle sein muss. Wir ziehen das Papier nach rechts, um es zu straffen, und anschließend nach oben, bis es einen Punkt berührt. Dieser Punkt muss ebenfalls ein Eckpunkt der konvexen Hülle sein. Wir halten das Papier weiterhin straff und setzen den Weg um die Punktmenge fort, bis wir wieder zu unserem Ausgangspunkt p0 zurückkehren. Formal gesehen erzeugt Jarvis Marsch eine Sequenz H = p0 , p1 , . . . , ph−1 von Eckpunkten der konvexen Hülle. Wir beginnen mit p0 . Wie Abbildung 33.9 zeigt, hat der nächste Eckpunkt p1 der konvexen Hülle den kleinsten Polarwinkel relativ zu p0 . (Im Falle von Mehrdeutigkeiten wählen wir den von p0 am weitesten entfernten Punkt.) Analog besitzt p2 den kleinsten Polarwinkel relativ zu p1 , usw. Wenn wir den höchsten Eckpunkt pk (wobei wir im Falle von Mehrdeutigkeiten den am weitesten entfernten Punkt wählen) erreichen, haben wir, wie Abbildung 33.9 zeigt, die rechte Kette von CH(Q) konstruiert. Um die linke Kette zu konstruieren, beginnen wir bei pk und wählen pk+1 als den Punkt mit dem kleinsten Polarwinkel relativ zu pk , aber nun von der negativen x-Achse aus betrachtet. Wir fahren fort, die linke Kette zu bilden, indem wir Polarwinkeln von der negativen x-Achse aus betrachten, bis wir zu unserem Ausgangpunkt p0 zurückkehren. Wir könnten Jarvis Marsch durch einen einzigen konzeptionellen Sweep um die konvexe Hülle herum implementieren, d. h. ohne die rechte und linke Kette separat zu konstruieren. Solche Implementierungen verfolgen typischerweise die letzte gewählte Kante der konvexen Hülle und fordern, dass die Folge der Winkel streng steigend (im Bereich von 0 bis 2π) ist. Der Vorteil der Konstruktion separater Ketten besteht darin, dass wir die Winkel nicht explizit berechnen müssen; die Methoden aus Abschnitt 33.1 reichen zum Vergleich von Winkeln aus. Wenn wir Jarvis Marsch sorgfältig implementieren, besitzt das Verfahren eine Laufzeit von O(nh). Für jeden der h Eckpunkte von CH(Q) bestimmen wir den Eckpunkt mit minimalem Polarwinkel. Jeder Vergleich von Polarwinkeln benötigt unter Verwendung der Methoden aus Abschnitt 33.1 Zeit O(1). Wie Abschnitt 9.1 zeigt, können wir das Minimum von n Werten in Zeit O(n) berechnen, wenn jeder Vergleich Zeit O(1) benötigt. Folglich nimmt Jarvis Marsch Zeit O(nh) in Anspruch.
Übungen 33.3-1 Beweisen Sie, dass die Punkte p1 und pm in der Prozedur Graham-Scan Eckpunkte der konvexen Hülle CH(Q) sein müssen.
33.3 Bestimmen der konvexen Hülle
linke Kette
1049
rechte Kette p3
p4
p2
p1 p0 linke Kette
rechte Kette
Abbildung 33.9: Die Arbeitsweise von Jarvis Marsch. Wir wählen den ersten Eckpunkt als den niedrigsten Punkt p0 . Der nächste Eckpunkt p1 besitzt von allen Punkten den kleinsten Polarwinkel bezüglich p0 . Dann folgt p2 , der den niedrigsten Polarwinkel bezüglich p1 hat. Die rechte Kette verläuft bis zum höchsten Punkt p3 nach oben. Anschließend konstruieren wir die linke Kette, indem wir stets den Punkt mit dem kleinsten Polarwinkel, von der negativen x-Achse aus gesehen, bestimmen.
33.3-2 Betrachten Sie ein Rechenmodell, das Addition, Vergleich und Multiplikation unterstützt und bei dem es eine untere Schranke von Ω(n lg n) für das Sortieren von n Zahlen gibt. Beweisen Sie, dass in einem solchen Modell Ω(n lg n) eine untere Schranke für die Berechnung der Eckpunkte der konvexen Hülle einer Menge von n Punkten in korrekter Reihenfolge ist. 33.3-3 Gegeben sei eine Punktmenge Q. Beweisen Sie, dass das am weitesten auseinander liegende Paar von Punkten zur konvexen Hülle CH(Q) gehört. 33.3-4 Für ein gegebenes Polygon P und einen Punkt q auf dessen Rand ist der Schatten von q die Punktmenge r, für die die Strecke qr ausschließlich auf dem Rand oder im Inneren von P verläuft. Wie Abbildung 33.10 illustiert, ist ein Polygon sternförmig, wenn ein Punkt p im Inneren von P existiert, der sich im Schatten jedes Punktes auf dem Rand von P befindet. Die Menge all dieser Punkte wird als Kern von P bezeichnet. Gegeben sei ein sternförmiges n-eckiges Polygon P , das durch seine Eckpunkte in der dem Uhrzeigersinn entgegengesetzten Reihenfolge festgelegt ist. Zeigen Sie, wie dessen konvexe Hülle CH(P ) in Zeit O(n) berechnet werden kann.
1050
33 Algorithmische Geometrie q′
p q (a)
(b)
Abbildung 33.10: Die Definition eines sternförmigen Polygons, die in Übung 33.3-4 verwendet wird. (a) Ein sternförmiges Polygon. Die Strecke vom Punkt p zu einem beliebigen Punkt q auf dem Rand schneidet den Rand nur in q. (b) Ein nicht sternförmiges Polygon. Der schattierte Bereich auf der linken Seite ist der Schatten von q und der schattierte Bereich auf der rechten Seite ist der Schatten von q . Da diese Bereiche disjunkt sind, ist der Kern des Polynoms leer.
33.3-5 Beim Online-Problem der konvexen Hülle wird pro Zeiteinheit jeweils ein Punkt aus einer Menge Q von n Punkten eingelesen. Nach der Eingabe jedes Punktes soll die konvexe Hülle der bis dahin bekannten Punkte berechnet werden. Offensichtlich könnten wir Grahams Methode für jeden Punkt laufen lassen, woraus sich eine Gesamtlaufzeit von O(n2 lg n) ergäbe. Zeigen Sie, wie wir das Online-Problem der konvexen Hülle in der Gesamtzeit O(n2 ) lösen können. 33.3-6∗ Zeigen Sie, wie wir die inkrementelle Methode zur Berechnung der konvexen Hülle von n Punkten so implementieren können, dass sie in Zeit O(n lg n) läuft.
33.4
Berechnung des dichtesten Punktepaares
Wir betrachten nun das Problem des Berechnens des dichtesten Paares von Punkten in einer Menge Q von n ≥ 2 Punkten. Die Bezeichnung „dichtestes“ bezieht sich auf die gewöhnliche euklidische Distanz: die Distanz zwischen den Punkten p1 = (x1 , y1 ) und p2 = (x2 , y2 ) ist (x1 − x2 )2 + (y1 − y2 )2 . Zwei Punkte der Menge Q können übereinstimmen; dann ist die Distanz zwischen ihnen 0. Dieses Problem besitzt beispielsweise Anwendungen bei Verkehrsleitsystemen. Ein System zur Kontrolle des Luftraums oder der Seeschifffahrt muss bestimmen können, welche die dichtesten Vehikel sind, um potentielle Kollisionen zu erkennen. Ein naiver Algorithmus zur Bestimmung des dichtesten Paares betrachtet alle n2 = Θ(n2 ) Punktepaare. In diesem Abschnitt werden wir einen Teile-und-Beherrsche-Algorithmus für dieses Problem beschreiben, dessen Laufzeit durch die bekannte Rekursionsgleichung T (n) = 2T (n/2) + O(n) gegeben ist. Somit benötigt dieser Algorithmus nur Zeit O(n lg n).
33.4 Berechnung des dichtesten Punktepaares
1051
Der Teile-und-Beherrsche-Algorithmus Jeder rekursive Aufruf des Algorithmus benötigt als Eingabe eine Teilmenge P ⊆ Q und die Felder X und Y , von denen jedes alle Punkte der gegebenen Teilmenge P enthält. Die Punkte im Feld X sind so sortiert, dass ihre x-Koordinaten monoton steigend sind. Analog dazu sind die Elemente des Feldes Y monoton steigend nach der y-Koordinate sortiert. Beachten Sie, dass wir es uns nicht leisten können, bei jedem rekursiven Aufruf zu sortieren, wenn wir die Zeitschranke O(n lg n) einhalten wollen. Wenn wir dies tun würden, würden wir für die Laufzeit die Rekursionsgleichung T (n) = 2T (n/2)+O(n lg n) erhalten, deren Lösung T (n) = O(n lg2 n) ist. (Verwenden Sie die in Übung 4.6-2 gegebene Version der Mastermethode.) Wir werden in Kürze sehen, wie das „Vorsortieren“ zu verwenden ist, um die Sortierungen zu erhalten, ohne tatsächlich bei jedem rekursiven Aufruf zu sortieren. Ein gegebener rekursiver Aufruf mit den Eingaben P , X und Y überprüft zuerst, ob |P | ≤ 3 gilt. Wenn dies der Fall ist, führt der Aufruf die oben beschriebene naive Me thode aus: er probiert alle |P2 | Punktepaare aus und gibt das dichteste Paar zurück. Im Falle |P | > 3 führt der rekursive Aufruf die Teile-und-Beherrsche-Methode folgendermaßen aus. Teile: Er bestimmt eine vertikale Gerade l, die die Punktmenge in zwei Mengen PL und PR halbiert, sodass |PL | = |P | /2 , |PR | = |P | /2 gilt, alle Punkte in PL sich links von der Geraden l befinden und alle Punkte in PR auf der Geraden oder rechts von ihr liegen. Er teilt das Feld X in zwei Felder XL und XR , die die Punkte aus PL bzw. PR in der Reihenfolge monoton steigender x-Koordinaten enthalten. Er teilt analog dazu das Feld Y in zwei Felder YL und YR , die die Punkte PL bzw. PR , in der Reihenfolge monoton steigender y-Koordinaten enthalten. Beherrsche: Nachdem P in PL und PR aufgeteilt ist, macht er zwei rekursive Aufrufe, einen zum Bestimmen des dichtesten Punktepaares von PL und den anderen zum Bestimmen des dichtesten Punktepaares von PR . Die Eingaben zum ersten Aufruf sind die Teilmenge PL und die Felder XL und YL ; der zweite Aufruf hat die Eingaben PR , XR und YR . Seien die für PL und PR zurückgegebenen Abstände des dichtesten Paares δL bzw. δR und sei δ = min(δL , δR ). Kombiniere: Das dichteste Paar ist entweder das Paar mit dem durch die rekursiven Aufrufe bestimmten Distanz δ oder es ist ein Punktepaar, von dem ein Punkt in PL und der andere in PR liegt. Der Algorithmus bestimmt, ob es ein Paar mit einem Punkt in PL und einem Punkt in PR gibt, deren Distanz geringer als δ ist. Vergegenwärtigen Sie sich, dass, wenn es ein solches Paar von Punkten gibt, die eine Distanz von weniger als δ haben, dann müssen sich beide Punkte innerhalb von δ Einheiten von der Gerade l befinden. Wie Abbildung 33.11(a) zeigt, müssen folglich beide Punkte innerhalb eines 2δ-breiten vertikalen Streifens mit Mittellinie l liegen. Um ein solches Paar zu bestimmen, falls ein solches existiert, machen wir Folgendes: 1. Erzeugen Sie ein Feld Y , das alle Punkte des Feldes Y enthält, die sich innerhalb des 2δ-breiten vertikalen Streifens befinden. Das Feld Y ist ebenso wie Y nach der y-Koordinate sortiert.
1052
33 Algorithmische Geometrie 2. Für jeden Punkt p im Feld Y versuchen Sie, Punkte in Y zu finden, die sich innerhalb einer Entfernung von δ Einheiten von p befinden. Wie wir weiter unten sehen werden, müssen wir in Y nur die 7 auf p folgenden Punkte betrachten. Bestimmen Sie die Distanz von p zu jedem dieser 7 Punkte und merken Sie sich die Distanz des dichtesten Paares δ , das für alle Punktepaare in Y bestimmt wurde. 3. Im Falle δ < δ enthält der vertikale Streifen tatsächlich ein dichteres Paar als das, das die rekursiven Aufrufe bestimmt haben. Geben Sie dieses Paar und deren Distanz δ zurück. Anderenfalls geben Sie das durch die rekursiven Aufrufen gefundene dichteste Paar und deren Distanz δ zurück.
Die obige Beschreibung geht nicht auf alle Details der Implementierung ein, die notwendig sind, um die Laufzeit von O(n lg n) zu erreichen. Nach dem Beweis der Korrektheit des Algorithmus werden wir zeigen, wie wir den Algorithmus implementieren müssen, um die gewünschte Zeitschranke zu erreichen.
Korrektheit Die Korrektheit dieses Algorithmus zur Berechnung des dichtesten Paares ist bis auf zwei Aspekte offensichtlich. Zunächst stellen wir mit dem Auslaufen der Rekursion, wenn |P | ≤ 3 gilt, sicher, dass wir niemals versuchen, ein aus nur einem Punkt bestehendes Teilproblem zu lösen. Der zweite Aspekt liegt darin, dass wir nur die 7 auf p folgenden Punkte im Feld Y überprüfen müssen. Wir werden nun diese Eigenschaft beweisen. Setzen Sie voraus, dass in einer Rekursionsstufe das dichteste Punktepaar pL ∈ PL und pR ∈ PR ist. Folglich ist die Distanz δ zwischen pL und pR echt kleiner als δ. Der Punkt pL muss auf oder links der Geraden l und weniger als δ Einheiten entfernt liegen. Analog dazu befindet sich pR auf oder rechts von l und weniger als δ Einheiten entfernt. Darüber hinaus liegen pL und pR vertikal innerhalb eines Abstandes von δ voneinander entfernt. Folglich befinden sich pL und pR , wie Abbildung 33.11(a) zeigt, innerhalb eines δ × 2δ-Rechtecks, das an der Geraden l zentriert ist. (Es können sich auch andere Punkte innerhalb dieses Rechtecks befinden.) Wir zeigen nun, dass höchstens 8 Punkte aus P innerhalb dieses δ × 2δ-Rechtecks liegen können. Betrachten Sie das δ × δ-Quadrat, das die linke Hälfte dieses Rechtecks bildet. Weil alle Punkte innerhalb PL mindestens δ Einheiten entfernt sind, können höchstens 4 Punkte in diesem Quadrat existieren; Abbildung 33.11(b) zeigt auf welche Weise. Analog dazu können sich höchstens 4 Punkte in PR innerhalb des δ × δ-Quadrates befinden, das die rechte Hälfte des Rechtecks bildet. Folglich können höchstens 8 Punkte in P innerhalb des δ × 2δ-Rechtecks sein. (Beachten Sie, dass Punkte auf der Geraden l entweder zu PL oder PR gehören können. Deshalb kann es bis zu 4 Punkten auf l geben. Diese Grenze wird erreicht, wenn es zwei Paare aufeinander fallender Punkte gibt, sodass jedes Paar aus einem Punkt aus PL und einem Punkt aus PR besteht. Ein Paar befindet sich am Schnittpunkt von l und der oberen Kante des Rechtecks und das andere Paar liegt dort, wo sich l mit der unteren Kante des Rechtecks schneidet.) Nachdem wir gezeigt haben, dass sich höchstens 8 Punkte von P innerhalb des Rechtecks befinden können, können wir einfach sehen, dass wir nur die 7 Punkte, die auf jeden
33.4 Berechnung des dichtesten Punktepaares
1053
PR PL
PR
2δ
δ
PL
δ
pR
pL
δ
δ
l
l
(a)
(b)
koinzidente Punkte, einer in PL, einer in PR koinzidente Punkte, einer in PL, einer in PR
Abbildung 33.11: Wesentliche Konzepte des Beweises, dass der Algorithmus zur Berechnung des dichtesten Paares in Y nur die 7 auf jeden Punkt folgenden Punkte überprüfen muss. (a) Wenn pL ∈ PL und pR ∈ PR weniger als δ Einheiten voneinander entfernt sind, dann müssen sie sich in einem um die Gerade l zentrierten δ × 2δ-Rechteck befinden. (b) Eine Möglichkeit, wie 4 Punkte, die paarweise mindestens δ Einheiten voneinander entfernt sind, in einem δ × δ-Quadrat positioniert sein können. Auf der linken Seite befinden sich 4 Punkte in PL und auf der rechten Seite 4 Punkte in PR . Das δ × 2δ-Rechteck kann 8 Punkte enthalten, wenn die auf der Geraden l gezeigten Punkte tatsächlich Paare von zueinander koinzidenten Punkten mit einem Punkt in PL und einem Punkt in PR sind.
Punkt im Feld Y folgen, zu überprüfen haben. Wir setzen immer noch voraus, dass das dichteste Paar pL und pR ist. Setzen wir außerdem ohne Beschränkung der Allgemeinheit voraus, dass pL im Feld Y dem Punkt pR vorausgeht, dann befindet sich pR an einer der 7 auf pL folgenden Stellen, auch wenn pL so früh wie möglich und pR so spät wie möglich in Y vorkommt. Damit haben wir die Korrektheit des Algorithmus zur Berechnung des dichtesten Paares gezeigt.
Implementierung und Laufzeit Wie wir bereits angemerkt haben, besteht unser Ziel darin, die Rekursionsgleichung T (n) = 2T (n/2) + O(n) für die Laufzeit T (n) einer Menge mit n Punkten zu erhalten. Das Hauptproblem besteht darin, sicherzustellen, dass die den rekursiven Aufrufen übergebenen Felder XL , XR , YL und YR nach der richtigen Koordinate sortiert sind und dass das Feld Y nach der y-Koordinate sortiert ist. (Beachten Sie, dass, wenn das Feld X bei einem rekursiven Aufruf bereits sortiert übergeben wird, wir die Menge P leicht in linearer Zeit in PL und PR aufteilen können.) Die entscheidende Beobachtung ist, dass wir bei jedem Aufruf eine sortierte Teilmenge eines sortierten Feldes bilden wollen. Beispielsweise erhält ein spezieller Aufruf die Teilmenge P und das nach der y-Koordinate sortierte Feld Y . Nachdem wir P in PL und PR zerlegt haben, müssen die nach der y-Koordinate sortierten Felder YL und YR in linearer Zeit gebildet werden. Wir können die Methode als die Umkehrung der Prozedur Merge für Sortieren durch Mischen aus Abschnitt 2.3.1 ansehen: Wir teilen ein
1054
33 Algorithmische Geometrie
sortiertes Feld in zwei sortierte Felder. Der folgende Pseudocode setzt die Idee um: 1 2 3 4 5 6 7 8
seien YL [1 . . Y.l¨a nge] und YR [1 . . Y.l¨a nge] neue Felder YL .l¨a nge = YR .l¨a nge = 0 for i = 1 to Y.l¨a nge if Y [i] ∈ PL YL .l¨a nge = YL .l¨a nge + 1 YL [YL .l¨a nge] = Y [i] else YR .l¨a nge = YR .l¨a nge + 1 YR [YR .l¨a nge] = Y [i]
Wir untersuchen die Punkte im Feld Y einfach der Reihe nach. Wenn ein Punkt Y [i] in PL ist, dann hängen wir ihn an das Ende des Feldes YL an; anderenfalls hängen wir ihn an das Ende des Feldes YR an. Ein ähnlicher Pseudocode erzeugt die Felder XL , XR und Y . Die einzige offene Frage ist, wie wir diese Punkte zu Anfang sortieren können. Wir sortieren sie vor, d. h. wir sortieren die Punkte ein für alle Mal vor dem ersten rekursiven Aufruf. Diese sortierten Felder werden dem ersten rekursiven Aufruf übergeben und von da an, falls nötig, nach und nach zerschnitten. Das Vorsortieren bringt einen zusätzlichen Term O(n lg n) in die Laufzeit, aber nun benötigt jeder Rekursionsschritt, abgesehen von den rekursiven Aufrufen, lineare Zeit. Wenn also T (n) die Laufzeit jedes Rekursionschrittes und T (n) die Laufzeit des gesamten Algorithmus ist, erhalten wir T (n) = T (n) + O(n lg n) und 2T (n/2) + O(n) falls n > 3 , T (n) = O(1) falls n ≤ 3 . Also ist T (n) = O(n lg n) und T (n) = O(n lg n).
Übungen 33.4-1 Professor Williams hat sich ein Programm ausgedacht, das es dem Algorithmus zur Berechnung des dichtesten Paares erlaubt, in Y lediglich die 5 auf jeden Punkt folgenden Punkte zu überprüfen. Die Idee besteht darin, die Punkte auf der Geraden l immer der Menge PL zuzuordnen. Folglich kann es keine zusammenfallenden Punkte auf der Geraden l geben, von denen ein Punkt in PL und der andere in PR liegt. Somit können sich höchstens 6 Punkte im δ × 2δ-Rechteck befinden. Wo liegt die Schwachstelle im Programm des Professors? 33.4-2 Zeigen Sie, dass es in der Tat ausreicht, nur die Punkte zu überprüfen, die in den 5 Feldpositionen abgespeichert sind, die einem Punkt in dem Feld Y folgen. 33.4-3 Wir können für die Distanz zwischen zwei Punkten auch nicht-euklidische Distanzen definieren. In der Ebene ist die Lm -Distanz zwischen den Punkten
Problemstellungen zu Kapitel 33
1055
p1 und p2 durch den Ausdruck m
m 1/m
(|x1 − x2 | + |y1 − y2 | )
gegeben. Die euklidische Distanz ist also eine L2 -Distanz. Modifizieren Sie den Algorithmus zur Berechnung des dichtesten Paares, sodass die L1 -Distanz verwendet werden kann, die auch als Manhattan-Distanz bekannt ist. 33.4-4 Die L∞ -Distanz zweier Punkte p1 , p2 in der Ebene ist durch max(|x1 − x2 |, |y1 − y2 |) gegeben. Modifizieren Sie den Algorithmus zur Berechnung des dichtesten Paares, sodass die L∞ -Distanz verwendet werden kann. 33.4-5 Setzen Sie voraus, dass Ω(n) der Punkte, die dem Algorithmus zur Berechnung des dichtesten Paares übergeben werden, kovertikal sind. Zeigen Sie, wie wir die Mengen PL und PR bestimmen können und wie wir bestimmen können, ob ein Punkt von Y in PL oder in PR liegt, und zwar so, dass die Laufzeit des Algorithmus weiterhin in O(n lg n) liegt. 33.4-6 Schlagen Sie eine Veränderung des Algorithmus zur Berechnung des dichtesten Paares vor, der das Vorsortieren des Feldes Y umgeht, die Laufzeit aber bei O(n lg n) belässt. (Hinweis: Mischen Sie die Felder YL und YR , um das sortierte Feld Y zu erhalten.)
Problemstellungen 33-1 Konvexe Schichten Ist in der Ebene eine Punktemenge Q gegeben, dann definieren wir die konvexen Schichten induktiv. Die erste konvexe Schicht von Q besteht aus den Punkten in Q, die Eckpunkte der konvexen Hülle CH(Q) sind. Für i > 1 definieren wir Qi so, dass sie aus den Punkten von Q besteht, wobei alle Punkte der konvexen Schichten 1, 2, . . . , i − 1 entfernt wurden. Dann ist die i-te konvexe Schicht von Q CH(Qi ), wenn Qi = ∅ gilt. Anderenfalls ist die Schicht undefiniert. a. Geben Sie einen Algorithmus mit Laufzeit O(n2 ) zur Berechnung der konvexen Schichten einer Menge von n Punkten an. b. Beweisen Sie, dass Zeit Ω(n lg n) erforderlich ist, um die konvexen Schichten einer Menge von n Punkten mithilfe eines Rechenmodells zu bestimmen, das zum Sortieren von n reellen Zahlen Zeit Ω(n lg n) benötigt. 33-2 Maximale Schichten Sei Q eine Menge von n Punkten in der Ebene. Wir sagen, dass ein Punkt (x, y) einen Punkt (x , y ) dominiert, wenn x ≥ x und y ≥ y gilt. Ein Punkt in Q, der durch keinen anderen Punkt in Q dominiert wird, wird als maximal bezeichnet. Beachten Sie, dass es viele maximale Punkte geben kann, die sich folgendermaßen zu maximalen Schichten zusammenfassen lassen. Die erste maximale Schicht L1 ist die Menge maximaler Punkte von Q. Für i > 1 ist die Gi−1 i-te maximale Schicht Li die Menge maximaler Punkte in Q − j=1 Lj .
1056
33 Algorithmische Geometrie Setzen Sie voraus, dass Q k nichtleere maximale Schichten hat. Sei yi die yKoordinate des am weitesten links liegenden Punktes in Li (i = 1, 2, . . . , k). Für den Moment setzen wir voraus, dass keine zwei Punkte in Q dieselbe x- oder y-Koordinate besitzen. a. Zeigen Sie, dass y1 > y2 > · · · > yk gilt. Betrachten Sie einen Punkt (x, y), der sich links eines beliebigen Punktes in Q befindet und für den y verschieden von der y-Koordinate aller anderen Punkte in Q ist. Sei Q = Q ∪ {(x, y)}. b. Sei j der minimale Index, für den yj < y gilt – es sein denn es gilt y < yk ; in diesem Fall setzen wir j = k + 1. Zeigen Sie, dass die maximalen Schichten von Q folgende Eigenschaften haben. • Ist j ≤ k, dann sind die maximalen Schichten von Q gleich den maximalen Schichten von Q, mit dem Unterschied, dass Lj zusätzlich (x, y) als seinen neuen am weitesten links liegenden Punkt enthält. • Ist j = k+1, dann sind die ersten k maximalen Schichten von Q dieselben wie für Q. Zusätzlich besitzt aber Q eine nichtleere (k + 1)-te maximale Schicht, nämlich Lk+1 = {(x, y)}. c. Geben Sie einen Algorithmus mit Laufzeit O(n lg n) an, der die maximalen Schichten einer Menge Q von n Punkten berechnet. (Hinweis: Bewegen Sie eine Sweep-Line von rechts nach links.) d. Treten irgendwelche Schwierigkeiten auf, wenn wir nun zulassen, dass Eingabepunkte die gleichen x- oder y-Koordinaten haben können? Schlagen Sie Wege vor, wie wir solche Probleme lösen können.
33-3 Geisterjäger und Geister Eine Gruppe von n Geisterjägern kämpft gegen n Geister. Jeder Geisterjäger trägt einen Protonenwerfer, der einen Strahl auf einen Geist abschießt, der ihn auslöscht. Ein Strahl verläuft auf einer geraden Linie und bricht ab, wenn er den Geist getroffen hat. Die Geisterjäger entscheiden sich für die folgende Strategie. Sie werden mit den Geistern n Geisterjäger-Geist-Paare bilden und anschließend werden alle Geisterjäger gleichzeitig einen Strahl auf den ihm zugeteilten Geist abgeben. Wie wir alle wissen, ist es sehr gefährlich, wenn sich Strahlen kreuzen, weshalb die Geisterjäger die Paare so wählen müssen, dass sich die Strahlen nicht kreuzen. Setzen Sie voraus, dass die Position jedes Geisterjägers und jedes Geistes ein fester Punkt in der Ebene ist und dass keine drei Positionen kollinear sind. a. Zeigen Sie, dass es eine Gerade gibt, die durch einen Geisterjäger und einen Geist verläuft, sodass die Anzahl der Geisterjäger auf einer Seite der Geraden gleich der Anzahl der Geister auf der gleichen Seite ist. Beschreiben Sie, wie wir eine solche Gerade in Zeit O(n lg n) bestimmen können. b. Geben Sie einen Algorithmus mit Laufzeit O(n2 lg n) an, der die GeisterjägerGeist-Paare so bildet, dass sich die abgegebenen Strahlen nicht schneiden.
Kapitelbemerkungen zu Kapitel 33
1057
33-4 Mikado Professor Charon hat einen Menge von n Holzstäbchen, die in einer bestimmten Konfiguration übereinander liegen. Jedes Stäbchen wird durch seine Endpunkte beschrieben, und jeder Endpunkt ist ein geordnetes Tripel, das seine (x, y, z)Koordinate beschreibt. Kein Stäbchen liegt vertikal. Der Professor möchte pro Zeiteinheit ein Stäbchen aufnehmen, wobei er nur dann ein Stäbchen aufnehmen kann, wenn sich kein anderes Stäbchen darauf befindet. a. Geben Sie eine Prozedur an, die für zwei Stäbchen a und b bestimmt, ob sich a oberhalb oder unterhalb von b befindet, oder nicht in Relation zueinander stehen. b. Geben Sie einen effizienten Algorithmus an, der feststellt, ob es möglich ist, alle Stäbchen aufzunehmen. Falls dies zutrifft, dann soll der Algorithmus eine zulässige Reihenfolge ausgeben, in der die Stäbchen aufgehoben werden können. 33-5 Verteilungen mit spärlich besetzter Hülle Betrachten Sie die Berechnung der konvexen Hülle einer Menge von Punkten in der Ebene, deren Punkte gemäß einer bekannten Zufallsverteilung gewählt wurden. Unter gewissen Voraussetzungen besitzt die Anzahl der Eckpunkte (auch Größe der konvexen Hülle) genannt, der konvexen Hülle von n zufällig ausgewählten Punkten den Erwartungswert O(n1− ), wobei > 0 konstant ist. Wir nennen eine solche Verteilung Verteilung mit spärlich besetzter Hülle. Verteilungen mit spärlich besetzter Hülle finden wir in folgenden Fällen: • Die Punkte sind gleichmäßig im Einheitskreis verteilt. Die konvexe Hülle hat eine erwartete Größe von Θ(n1/3 ). • Die Punkte sind gleichmäßig im Inneren eines konvexen Polygons mit k Kanten verteilt, wobei k eine beliebige Konstante ist. Die konvexe Hülle hat eine erwartete Größe von Θ(lg n). • Die Punkte wurden gemäß einer zweidimensionalen Normalverteilung aus√ gewählt. Die konvexe Hülle hat eine erwartete Größe von Θ( lg n). a. Gegeben seien nun zwei konvexe Polygone mit n1 beziehungsweise n2 Eckpunkten. Zeigen Sie, wie wir die konvexe Hülle aller n1 + n2 Punkte in Zeit O(n1 + n2 ) berechnen können. (Die Polygone können sich überdecken.) b. Zeigen Sie, wie wir die konvexe Hülle einer Menge von n Punkten, die unabhängig voneinander gemäß einer Verteilung mit spärlich besetzter Hülle gewählt wurde, in erwarteter Zeit O(n) berechnen können. (Hinweis: Bestimmen Sie rekursiv die konvexe Hülle der ersten n/2 Punkte und der zweiten n/2 Punkte und kombinieren Sie anschließend die Resultate.)
Kapitelbemerkungen Dieses Kapitel berührt die algorithmische Geometrie und ihre Methoden nur oberflächlich. Bücher über algorithmischen Geometrie sind beispielsweise die von Preparata und Shamos [282], Edelsbrunner [99] und O’Rourke [269].
1058
33 Algorithmische Geometrie
Obwohl die Geometrie bereits seit der Antike erforscht wird, ist die Entwicklung von Algorithmen zur Lösung geometrischer Probleme relativ neu. Preparata und Shamos merken an, dass die früheste Vorstellung von der Komplexität eines Problems 1902 durch E. Lemoine gegeben wurde. Er untersuchte euklidische Konstruktionen – also solche, die nur Zirkel und Lineal verwenden – und legte fünf Primitive dafür fest: Setzen eines Schenkels des Zirkels auf einen gegebenen Punkt, Setzen eines Schenkels des Zirkels auf eine gegebene Gerade, Zeichnen eines Kreises, Anlegen der Kante des Lineals an einen gegebenen Punkt und Zeichnen einer Gerade. Lemoine war an der Anzahl der Primitiven interessiert, die zum Ausführen einer gegebenen Konstruktion benötigt werden; er nannte diese Summe die „Simplizität“ der Konstruktion. Der Algorithmus in Abschnitt 33.2, der bestimmt, ob sich zwei beliebige Strecken schneiden, geht auf Shamos und Hoey [313] zurück. Die ursprüngliche Version der Methode von Graham wurde von Graham [150] angegeben. Der Package-Wrapping-Algorithmus geht auf Jarvis [189] zurück. Unter Verwendung eines Entscheidungsbaum-Rechenmodells bewies Yao [359] eine untere Schranke von Ω(n lg n) für die Laufzeit jedes Algorithmus zur Bestimmung der konvexen Hülle. Wenn die Anzahl der Eckpunkte h der konvexen Hülle mitbetrachtet wird, dann ist der Ausschneiden-und-Suchen-Algorithmus von Kirkpatrick und Seidel [206], der Zeit O(n lg h) benötigt, asymptotisch optimal. Der in Zeit O(n lg n) laufende Teile-und-Beherrsche-Algorithmus zum Bestimmen des dichtesten Punktepaares ist von Shamos und ist in Preparata und Shamos [282] angegeben. Preparata und Shamos zeigen auch, dass der Algorithmus in einem Entscheidungsbaum-Modell asymptotisch optimal ist.
34
NP-Vollständigkeit
Fast alle bisher betrachteten Algorithmen waren Algorithmen mit polynomieller Laufzeit: Bei Eingaben der Größe n ist jeweils ihre Laufzeit im schlechtesten Fall in O(nk ) für eine Konstante k. Sie fragen sich womöglich, ob alle Probleme in polynomieller Zeit gelöst werden können. Die Antwort lautet nein. Es gibt beispielsweise Probleme, wie Turings berühmtes „Halteproblem“, das durch keinen Rechner gelöst werden kann, unabhängig davon, wie viel Laufzeit wir ihm zugestehen. Es gibt auch Probleme, die zwar gelöst werden können, allerdings nicht in Zeit O(nk ) für irgendeine Konstante k. Im Allgemeinen sehen wir Probleme, die in polynomieller Laufzeit gelöst werden können, als händelbar oder einfach an und Probleme, die superpolynomielle Laufzeit benötigen, als nicht händelbar oder hart. Im Zentrum dieses Kapitels steht eine interessante Klasse von Problemen, die als „NPvollständige“ Probleme bezeichnet werden und deren Status unbekannt ist. Es wurde bisher weder ein Algorithmus mit polynomieller Laufzeit für ein NP-vollständiges Problem entdeckt, noch konnte irgendjemand beweisen, dass für eines dieser Probleme kein Algorithmus mit polynomieller Laufzeit existieren kann. Diese so genannte P = NPFrage ist eines der schwierigsten, am meisten verblüffenden offenen Forschungsprobleme der theoretischen Informatik, seit es 1971 erstmals formuliert wurde. Mehrere NP-vollständige Probleme sind besonders spannend, da sie, oberflächlich betrachtet, Problemen ähneln, für die wir Algorithmen mit polynomieller Laufzeit kennen. Bei jedem der folgenden Paare von Problemen ist eines davon in polynomieller Laufzeit lösbar, während das andere NP-vollständig ist; der Unterschied zwischen den jeweiligen Problemen scheint allerdings geringfügig zu sein: Kürzeste vs. längste einfache Pfade: In Kapitel 24 haben wir gesehen, dass wir in einem gerichteten Graphen G = (V, E) sogar bei negativen Kantengewichten kürzeste Pfade von einem einzigem Startknoten in Zeit O(V E) berechnen können. Das Bestimmen eines längsten einfachen Pfades zwischen zwei Knoten ist jedoch schwierig. Schon die Entscheidung, ob ein Graph einen einfachen Pfad enthält, dessen Kantenanzahl mindestens so groß wie eine gegebene Zahl ist, ist NP-vollständig. Euler-Zug vs. Hamiltonischer Kreis: Ein Euler-Zug eines streng zusammenhängenden gerichteten Graphen G = (V, E) ist eine Zyklus, der jede Kante von G genau einmal traversiert, dabei aber einen Knoten mehrmals besuchen kann. Mithilfe des Algorithmus aus Problemstellung 22-3 benötigen wir nur Zeit O(E), um zu bestimmen, ob ein Graph einen Euler-Zug besitzt. Die tatsächlichen Kanten des Euler-Zuges können wir in Zeit O(E) bestimmen. Ein hamiltonischer Kreis eines gerichteten Graphen G = (V, E) ist ein einfacher Zyklus, der jeden Knoten
1060
34 NP-Vollständigkeit aus V enthält. Die Entscheidung, ob ein gerichteter Graph einen hamiltonischen Kreis besitzt, ist NP-vollständig. (Weiter hinten in diesem Kapitel werden wir beweisen, dass die Entscheidung, ob ein ungerichteter Graph einen hamiltonischen Kreis besitzt, NP-vollständig ist.)
2-CNF-Erfüllbarkeit vs. 3-CNF-Erfüllbarkeit: Eine Boolesche Formel enthält Variablen, deren Werte 0 oder 1 sind, Boolesche Verbindungen wie ∧ (UND), ∨ (ODER) und ¬ (NICHT) sowie Klammern. Eine Boolesche Formel ist erfüllbar, wenn es eine Belegung der Variablen aus Nullen und Einsen gibt, für die die Formel den Wert 1 hat. Wir werden die Begriffe weiter hinten in diesem Kapitel noch exakter definieren, aber informal ist eine Boolesche Formel eine k-konjunktive Normalform oder k-CNF (engl.: k-conjunctive normal form), wenn Klauseln konjunktiv (also mit UND) miteinander verknüpft werden, wobei jede Klausel eine Disjunktion (also mit ODER verknüpft) von genau k Literalen (Variable oder negierte Variable) ist. Beispielsweise ist die Boolesche Formel (x1 ∨ ¬x2 ) ∧ (¬x1 ∨ x3 ) ∧ (¬x2 ∨ ¬x3 ) in 2-CNF. (Sie besitzt die sie erfüllende Zuweisung x1 = 1, x2 = 0, x3 = 1.) Obwohl wir in polynomieller Zeit bestimmen können, ob eine 2-CNF-Formel erfüllbar ist, werden wir später in diesem Kapitel sehen, dass es NP-vollständig ist, zu entscheiden, ob eine 3-CNF-Formel erfüllbar ist.
NP-Vollständigkeit und die Klassen P und NP Im gesamten Kapitel werden wir uns auf drei Problemklassen beziehen: P, NP und NPC, wobei letztere Klasse die der NP-vollständigen Probleme ist. Wir beschreiben sie an dieser Stelle nur kurz und werden sie später genauer definieren. Die Klasse P besteht aus den in polynomieller Zeit lösbaren Problemen. Genauer gesagt, sind es Probleme, die in Zeit O(nk ) mit konstantem k gelöst werden können, wobei n die Eingabegröße angibt. Die meisten in den vorangegangenen Kapiteln untersuchten Probleme sind in P. Die Klasse NP besteht aus den in polynomieller Zeit „verifizierbaren“ Problemen. Was verstehen wir unter einem verifizierbaren Problem? Wenn wir (von wo auch immer) ein „Zertifikat“ einer Lösung gegeben haben, dann können wir in einer Zeit, die polynomiell in der Größe der Eingabe des Problems ist, verifizieren, dass das Zertifikat korrekt ist. Beispielsweise wäre beim Hamilton-Kreis-Problem für einen gegebenen gerichteten Graphen G = (V, E) ein Zertifikat eine Sequenz v1 , v2 , v3 , . . . , v|V | von |V | Knoten. Wir könnten leicht in polynomieller Zeit überprüfen, ob (vi , vi+1 ) ∈ E für i = 1, 2, 3, . . . , |V | − 1 und auch (v|V | , v1 ) ∈ E gilt. Als weiteres Beispiel wäre ein Zertifikat beim 3-CNF-Erfüllbarkeitsproblem eine Zuweisung von Werten an die Variablen. Wir könnten leicht in polynomieller Zeit überprüfen, ob diese Zuweisung die Boolesche Formel erfüllt. Jedes Problem in P ist auch in NP, da wir ein Problem in P sogar ohne gegebenes Zertifikat in polynomieller Zeit lösen können. Wir werden diese Notation später in diesem Kapitel formalisieren. Im Moment können wir jedoch voraussetzen, dass P ⊆ NP gilt. Die offene Frage besteht darin, ob P eine echte Teilmenge von NP ist oder nicht.
34 NP-Vollständigkeit
1061
Informal gesagt befindet sich ein Problem in der Klasse NPC – und wir werden es dann als NP-vollständig bezeichnen – wenn es sich in NP befindet und so „hart“ wie jedes Problem in NP ist. Wir werden später in diesem Kapitel genauer definieren, was wir mit „so hart wie jedes Problem in N P “ meinen. In der Zwischenzeit werden wir ohne Beweis festlegen, dass jedes Problem in NP einen Algorithmus mit polynomieller Laufzeit besitzt, wenn irgendein NP-vollständiges Problem in polynomieller Zeit gelöst werden kann. Die meisten theoretischen Informatiker glauben, dass NP-vollständige Probleme schwer zu handhaben sind, da, betrachtet man die große Anzahl von NP-vollständigen Problemen, die bisher untersucht worden sind – ohne dass irgendjemand für eines von ihnen eine Lösung in polynomieller Zeit gefunden hätte –, wäre es wirklich erstaunlich, wenn alle von ihnen in polynomieller Zeit gelöst werden könnten. Trotz der bisher aufgewendeten – jedoch ohne abschließendes Ergebnis gebliebenen – Anstrengungen, die schwere Handhabbarkeit NP-vollständiger Probleme zu beweisen, können wir die Möglichkeit nicht ausschließen, dass NP-vollständige Probleme tatsächlich doch in polynomieller Zeit lösbar sind. Um ein guter Entwickler von Algorithmen zu werden, müssen Sie die Grundlagen der Theorie zur NP-Vollständigkeit verstehen. Wenn Sie ein Problem als NP-vollständig erkennen können, liefert Ihnen dies ein gutes Indiz für seine Widerspenstigkeit. Als ein Entwickler sollten sie dann besser ihre Zeit mit dem Entwurf eines Approximationsalgorithmus (siehe Kapitel 35) verbringen oder einen handhabbaren Spezialfall lösen, als nach einem schnellen Algorithmus zu suchen, der das Problem exakt löst. Darüber hinaus sind viele natürliche und interessante Probleme, die oberflächlich betrachtet nicht härter sind als das Sortieren, die Suche in einem Graphen oder die Untersuchung von Netzwerkflüssen, in Wirklichkeit NP-vollständig. Aus diesem Grunde sollten Sie mit dieser wichtigen Problemklasse vertraut sein.
Wie beweist man die NP-Vollständigkeit von Problemen? Die Methoden, die zum Beweis der NP-Vollständigkeit einzelner Probleme verwendet werden, unterscheiden sich grundlegend von den bisher in diesem Buch zum Entwickeln und Analysieren von Algorithmen verwendeten Methoden. Wenn wir zeigen, dass ein Problem NP-vollständig ist, dann treffen wir eine Aussage über die Härte des Problems (oder zumindest darüber, für wie hart wir es halten) und nicht darüber, wie leicht es ist. Wir versuchen nicht, die Existenz eines effizienten Algorithmus zu beweisen, sondern zeigen vielmehr, dass wahrscheinlich kein effizienter Algorithmus existiert. In diesem Sinne haben NP-Vollständigkeitsbeweise Ähnlichkeit mit dem Beweis in Abschnitt 8.1, der eine untere Zeitschranke von Ω(n lg n) für jeden vergleichenden Sortieralgorithmus angibt. Die zum Beweis der NP-Vollständigkeit verwendeten speziellen Methoden unterscheiden sich allerdings von den in Abschnitt 8.1 verwendeten Entscheidungsbaum-Methoden. Wir stützen uns auf drei Schlüsselkonzepte, um ein Problem als NP-vollständig zu beweisen: Entscheidungsprobleme vs. Optimierungsprobleme Viele interessante Probleme sind Optimierungsprobleme, bei denen jede zulässige (d. h. „legale“) Lösung einen bestimmten Wert besitzt und wir eine zulässige Lösung mit dem bestmöglichen Wert bestimmen wollen. Beispielsweise sind bei einem Problem, das
1062
34 NP-Vollständigkeit
wir mit SHORTEST-PATH bezeichnen, ein ungerichteter Graph G und Knoten u und v gegeben, und wir wollen einen kürzesten Pfad von u nach v bestimmen. (Mit anderen Worten ist SHORTEST-PATH das Problem der kürzesten Pfade mit vorgegebenem Startknoten auf einem ungewichteten ungerichteten Graphen.) NP-Vollständigkeit ist nicht direkt auf Optimierungsprobleme anwendbar, wohl aber auf Entscheidungsprobleme, bei denen die Antwort einfach „Ja“ oder „Nein“ lautet (oder formaler ausgedrückt „1“ oder „0“). Wenngleich NP-vollständige Probleme Entscheidungsprobleme sind, können wir eine praktische Beziehung zwischen Optimierungs- und Entscheidungsproblemen nutzen. Gewöhnlich können wir ein gegebenes Optimierungsproblem in ein verwandtes Entscheidungsproblem umwandeln, indem wir dem zu optimierenden Wert eine Schranke auferlegen. Ein Entscheidungsproblem, das beispielsweise mit SHORTEST-PATH verwandt ist, ist PATH: Gegeben seien ein gerichteter Graph G, Knoten u und v und eine ganze Zahl k, existiert ein Pfad von u nach v, der höchstens aus k Kanten besteht? Wir können die Beziehung zwischen einem Optimierungsproblem und seinem verwandten Entscheidungsproblem ausnutzen, wenn wir zeigen wollen, dass das Optimierungsproblem „hart“ ist. Das liegt daran, dass das Entscheidungsproblem in gewissem Sinne „leichter“ oder zumindest „nicht härter“ ist. Beispielsweise können wir PATH lösen, indem wir SHORTEST-PATH lösen und dann die Anzahl der Kanten im berechneten kürzesten Pfad mit dem Wert des Parameters k vergleichen. Mit anderen Worten, ein Optimierungsproblem ist leicht, wenn sein zugehöriges Entscheidungsproblem leicht ist. Für die NP-Vollständigkeit bedeutet dies: Wenn wir beweisen können, dass ein Entscheidungsproblem hart ist, dann haben wir auch bewiesen, dass das zugehörige Optimierungsproblem hart ist. Obwohl die Theorie der NP-Vollständigkeit ihre Aufmerksamkeit auf Entscheidungsprobleme beschränkt, lässt diese Theorie häufig auch Folgerungen für Optimierungsprobleme zu. Reduktionen Die oben vorgestellte Idee zu zeigen, dass ein Problem nicht härter oder nicht leichter als ein anderes ist, ist auch anwendbar, wenn beide Probleme Entscheidungsprobleme sind. Wir nutzen diese Idee bei fast jedem Beweis der NP-Vollständigkeit. Lassen Sie uns ein Entscheidungsproblem A betrachten, das wir in polynomieller Zeit lösen wollen. Wir bezeichnen die Eingabe eines speziellen Problems als Instanz dieses Problems. Beispielsweise wäre die Instanz von PATH ein bestimmter Graph G, bestimmte Knoten u und v von G und eine bestimmte ganze Zahl k. Setzen Sie nun voraus, dass wir bereits wissen, wie wir ein anderes Entscheidungsproblem B in polynomieller Zeit lösen können. Setzen Sie als letztes voraus, dass wir über eine Prozedur verfügen, die jede beliebige Instanz α von A in eine Instanz β von B mit den folgenden Eigenschaften überführt: 1. Die Transformation benötigt polynomielle Zeit. 2. Die Antworten sind gleich. Das heißt, die Antwort für α lautet genau dann „Ja“, wenn auch die Antwort für β „Ja“ ist. Wir werden eine solche Prozedur Reduktionsalgorithmus nennen. Wie Abbildung 34.1 zeigt, liefert uns dieser eine Möglichkeit, Problem A in polynomieller Zeit zu lösen:
34 NP-Vollständigkeit Instanz α von A
1063 ja polynomieller Entscheidungsalgorithmus für B nein polynomieller Entscheidungsalgorithmus für A
polynomieller Reduktionsalgorithmus
Instanz β von B
ja nein
Abbildung 34.1: Wie wir einen in polynomieller Zeit laufenden Reduktionsalgorithmus nutzen können, um ein Entscheidungsproblem A in polynomieller Zeit zu lösen, wenn ein polynomieller Entscheidungsalgorithmus für ein anderes Problem B gegeben ist. Wir transformieren in polynomieller Zeit die Instanz α von A in die Instanz β von B, lösen B in polynomieller Zeit und nehmen die Antwort für β als Antwort für α.
1. Ist eine Instanz α des Problems A gegeben, dann benutzen Sie einen Reduktionsalgorithmus mit polynomieller Laufzeit, um diese Instanz in eine Instanz β des Problems B zu überführen. 2. Lassen Sie den Entscheidungsalgorithmus mit polynomieller Laufzeit für B auf der Instanz β laufen. 3. Verwenden Sie die Antwort für β als Antwort für α. So lange jeder dieser drei Schritte polynomielle Zeit benötigt, trifft dies auch auf alle drei Schritte insgesamt zu. Somit haben wir eine Möglichkeit, über α in polynomieller Zeit zu entscheiden. Mit anderen Worten benutzen wir die „Einfachheit“ von B, um die „Einfachheit“ von A zu beweisen, indem wir die Lösung des Problems A auf die Lösung des Problems B „reduzieren“. Denken Sie daran, dass es bei der NP-Vollständigkeit darum geht, zu zeigen, wie hart ein Problem ist und nicht wie einfach es ist. Wir wollen nun die polynomiellen Reduktionen in umgekehrter Richtung nutzen, um die NP-Vollständigkeit eines Problems zu zeigen. Lassen Sie uns die Idee einen Schritt weiter ausbauen und zeigen, wie Reduktionen in polynomieller Zeit verwendet werden können, um die Existenz eines polynomiellen Algorithmus zur Lösung eines bestimmten Problems B zu widerlegen. Setzen Sie voraus, dass ein Entscheidungsproblem A vorliegt, von dem wir bereits wissen, dass dafür kein Algorithmus mit polynomieller Laufzeit existieren kann. (Wir beschäftigen uns im Moment nicht damit, wie ein solches Problem A zu finden ist.) Setzen Sie weiter voraus, dass wir über eine polynomielle Reduktion verfügen, die jede Instanz von A in jeweils eine Instanz von B überführt. Nun können wir indirekt beweisen, dass für B kein Algorithmus in polynomieller Zeit existieren kann. Nehmen wir zum Zwecke des Widerspruchs an, dass B einen polynomiellen Algorithmus besäße. Dann gäbe es unter Verwendung der in Abbildung 34.1 gezeigten Methode eine Möglichkeit, das Problem A in polynomieller Zeit zu lösen, was unserer Annahme widerspricht, dass es für A keinen Algorithmus mit polynomieller Laufzeit gibt. Bei den Beweisen zur NP-Vollständigkeit eines Problems können wir nicht voraussetzen, dass es keinen Algorithmus mit polynomieller Laufzeit für A gibt. Die Beweismethode ist jedoch in dem Sinne ähnlich, dass wir die NP-Vollständigkeit von B unter der Voraussetzung beweisen, dass das Problem A auch NP-vollständig ist.
1064
34 NP-Vollständigkeit
Ein erstes NP-vollständiges Problem Da die Reduktionsmethode darauf beruht, ein bereits als NP-vollständig bekanntes Problem zur Verfügung zu haben, um damit die NP-Vollständigkeit eines anderen Problems zu beweisen, brauchen wir ein „erstes“ NP-vollständiges Problem. Wir werden dafür das Erfüllbarkeitsproblem auf Schaltkreisen wählen, bei dem ein Boolescher kombinatorischer Schaltkreis gegeben ist, der aus UND-, ODER- und NICHT-Gattern zusammengesetzt ist. Wir wollen wissen, ob es eine Menge von Booleschen Eingaben für diesen Schaltkreis gibt, die die Ausgabe 1 bewirken. Wir werden in Abschnitt 34.3 beweisen, dass dieses Problem NP-vollständig ist.
Kapitelüberblick Dieses Kapitel untersucht Aspekte der NP-Vollständigkeit, die sich direkt auf die Analyse von Algorithmen beziehen. In Abschnitt 34.1 formalisieren wir unseren Begriff eines „Problems“ und definieren die Komplexitätsklasse P der in Polynomialzeit lösbaren Entscheidungsprobleme. Wir werden auch sehen, wie diese Begriffe in den Rahmen der Theorie der formalen Sprachen passen. In Abschnitt 34.2 definieren wir die Klasse NP der Entscheidungsprobleme, deren Lösungen in polynomieller Zeit verifiziert werden können. Hier kommen wir auch auf die P = NP-Frage zu sprechen. Abschnitt 34.3 zeigt, wie wir Probleme mittels polynomieller „Reduktionen“ zueinander in Verbindung bringen können. Der Abschnitt definiert die NP-Vollständigkeit und skizziert einen Beweis dafür, dass ein als „Erfüllbarkeitsproblem auf Schaltkreisen“ bezeichnetes Problem NP-vollständig ist. Nachdem wir ein NP-vollständiges Problem gefunden haben, zeigen wir in Abschnitt 34.4, wie die NP-Vollständigkeit anderer Probleme wesentlich einfacher durch die Reduktionsmethode bewiesen werden kann. Wir illustrieren diese Methode, indem wir zeigen, dass zwei Erfüllbarkeitsprobleme der Aussagenlogik NP-vollständig sind. Mit weiteren Reduktionen zeigen wir in Abschnitt 34.5 die NPVollständigkeit einer Vielzahl anderer Probleme.
34.1
Polynomielle Zeit
Wir starten unsere Untersuchung der NP-Vollständigkeit, indem wir den Begriff der in polynomieller Zeit lösbaren Probleme formalisieren. Wir sehen diese Probleme allgemein als handhabbar an, allerdings eher aus philosophischen und nicht aus mathematischen Gründen. Wir können hierfür drei Argumente liefern. Erstens, wenngleich wir vernünftigerweise ein Problem als nicht handhabbar bezeichnen könnten, das Zeit Θ(n100 ) benötigt, erfordern nur sehr wenige praktisch relevante Probleme zu ihrer Lösung eine Laufzeit in der Größenordnung eines Polynoms mit einem derart hohen Grad. Die in der Praxis vorkommenden, in polynomieller Zeit lösbaren Probleme benötigen in der Regel viel weniger Zeit. Die Erfahrung hat gezeigt, dass wenn einmal ein Algorithmus mit polynomieller Laufzeit für ein Problem gefunden ist, häufig effizientere Algorithmen folgen. Auch wenn der derzeit beste Algorithmus für ein Problem eine Laufzeit im Bereich von Θ(n100 ) besitzt, ist es wahrscheinlich, dass bald ein Algorithmus mit viel besserer Laufzeit gefunden wird.
34.1 Polynomielle Zeit
1065
Zweitens gilt für viele vernünftige Rechenmodelle, dass ein Problem, welches in einem Modell in polynomieller Zeit gelöst werden kann, auch in den anderen Modellen in polynomieller Zeit gelöst werden kann. Beispielsweise ist die Klasse der durch eine serielle Maschine mit wahlfreiem Zugriff – dieses Rechenmodell haben wir in diesem Buch verwendet – in polynomieller Zeit lösbaren Probleme gleich der Klasse der durch abstrakte Turingmaschinen in polynomieller Zeit lösbaren Probleme.1 Sie entspricht auch der Klasse der von einem Parallelrechner in polynomieller Zeit lösbaren Probleme, wenn die Anzahl der Prozessoren mit der Eingabegröße polynomiell steigt. Drittens besitzt die Klasse der in polynomieller Zeit lösbaren Probleme angenehme Eingenschaften in Bezug auf die Abgeschlossenheit von Operationen, da Polynome bezüglich der Addition, der Multiplikation und der Komposition abgeschlossen sind. Wenn beispielsweise die Ausgabe eines Algorithmus mit polynomieller Laufzeit als Eingabe eines anderen Algorithmus mit polynomieller Laufzeit dient, dann hat der zusammengesetzte Algorithmus polynomielle Laufzeit. Übung 34.1-5 verlangt von Ihnen zu zeigen, dass, wenn ein Algorithmus eine (oder mehrere) Unterroutine(n) mit polynomieller Laufzeit konstant oft aufruft und ansonsten auch nur polynomiell viel Laufzeit benötigt, die Laufzeit des Gesamtalgorithmus polynomiell ist.
Abstrakte Probleme Um die Klasse der in polynomieller Zeit lösbaren Probleme zu verstehen, müssen wir zunächst eine formale Vorstellung davon haben, was ein „Problem“ ist. Wir definieren ein abstraktes Problem Q als eine binäre Relation einer Menge I von Probleminstanzen und eine Menge S von Problemlösungen. Beispielsweise ist eine Instanz für SHORTEST-PATH ein Tripel, das aus einem Graphen und zwei Knoten besteht. Eine Lösung ist eine Sequenz von Knoten des Graphen, wobei die leere Sequenz angibt, dass kein solcher Pfad existiert. Das Problem SHORTEST-PATH selbst ist die Relation, die jeder Instanz aus einem Graphen und zwei Knoten einen kürzesten Pfad im Graphen zuordnet, der die beiden Knoten verbindet. Da kürzeste Pfade nicht notwendigerweise eindeutig sein müssen, kann eine gegebene Probleminstanz mehr als eine Lösung haben. Diese Formulierung eines abstrakten Problems ist allgemeiner als wir dies für unsere Zwecke benötigen. Wie wir bereits oben gesehen haben, kümmert sich die Theorie der NP-Vollständigkeit nur um Entscheidungsprobleme, also Probleme mit Ja/NeinLösungen. In diesem Falle können wir ein abstraktes Entscheidungsproblem als eine Funktion betrachten, die eine Menge von Instanzen I auf die Lösungsmenge {0, 1} abbildet. Beispielsweise handelt es sich bei dem zu SHORTEST-PATH verwandten Entscheidungsproblem um das Problem PATH, das wir weiter oben schon kennengelernt haben. Wenn i = G, u, v, k eine Instanz des Entscheidungsproblems PATH ist, dann gilt PATH(i) = 1 (ja), falls ein kürzester Pfad von u nach v höchstens k Kanten besitzt; anderenfalls gilt PATH(i) = 0 (nein). Viele abstrakte Probleme sind keine Entscheidungsprobleme, sondern Optimierungsprobleme, bei denen ein Wert minimiert oder maximiert werden soll. Wie wir oben gesehen haben, können wir jedoch ein Optimierungsproblem in ein Entscheidungsproblem umzuwandeln, das nicht härter ist. 1 Eine umfassende Abhandlung über das Modell der Turingmaschinen finden Sie in Hopcroft und Ullman [180] oder Lewis und Papadimitriou [236].
1066
34 NP-Vollständigkeit
Kodierungen Damit ein Computerprogramm ein abstraktes Problem lösen kann, müssen wir die Probleminstanzen in einer für das Programm verständlichen Weise darstellen. Eine Kodierung einer Menge S abstrakter Objekte ist eine Abbildung e von S in die Menge der binären Strings.2 Beispielsweise sind wir alle mit der Kodierung der natürlichen Zahlen N = {0, 1, 2, 3, 4, . . .} durch binäre Strings {0, 1, 10, 11, 100, . . .} vertraut. Unter Verwendung dieser Kodierung gilt e(17) = 10001. Wenn Sie schon mal auf die Darstellung der Tastaturzeichen im Rechner geschaut haben, sind Sie voraussichtlich bereits dem ASCII-Code begegnet, in dem die Kodierung von A gleich 1000001 ist. Auch ein zusammengesetztes Objekt kann durch Kombination der Darstellungen seiner einzelnen Bestandteile durch einen binären String kodiert werden. Polygone, Graphen, Funktionen, geordnete Paare, Programme – alles kann durch binäre Strings kodiert werden. Folglich benutzt ein Computeralgorithmus, der ein abstraktes Entscheidungsproblem „löst“, eine Kodierung der Eingabeinstanz des Problems. Wir bezeichnen ein Problem, dessen Instanzmenge die Menge der binären Strings ist, als konkretes Problem. Wir sagen, dass ein Algorithmus ein konkretes Problem in Zeit O(T (n)) löst, wenn der Algorithmus angewendet auf eine gegebene Probleminstanz i der Länge n = |i| in Zeit O(T (n)) eine Lösung erzeugt.3 Ein konkretes Problem ist deshalb in polynomieller Zeit lösbar, wenn ein Algorithmus zur Lösung des Problems existiert, der die Laufzeit O(nk ) für eine Konstante k hat. Wir können nun die Komplexitätsklasse P formal als die Menge konkreter Entscheidungsprobleme definieren, die in polynomieller Zeit lösbar sind. Wir können Kodierungen verwenden, um abstrakte Probleme auf konkrete Probleme abzubilden. Ist ein abstraktes Entscheidungsproblem Q gegeben, das eine Instanzmen∗ ge I auf eine Menge {0, 1} abbildet, dann kann eine Kodierung e : I → {0, 1} ein verwandtes konkretes Entscheidungsproblem induzieren, das wir mit e(Q) bezeichnen.4 Wenn eine Lösung zu einer Instanz i ∈ I eines abstrakten Problems Q(i) ∈ {0, 1} ist, dann ist die Lösung zu einer Instanz e(i) ∈ {0, 1}∗ des konkreten Problems ebenfalls Q(i). Als rechnerisches Detail sollte erwähnt werden, dass einige binäre Strings keine sinnvolle Instanz zu einem abstrakten Problem darstellen. Der Einfachheit halber setzen wir voraus, dass jeder dieser Strings auf 0 abgebildet wird. Folglich erzeugt das konkrete Problem dieselben Lösungen wie das abstrakte Problem, wenn ihm die als binärer String kodierte Instanz des abstrakten Problems als Eingabe gegeben wird. Wir würden die Definition der Lösbarkeit in polynomieller Zeit gern von den konkreten Problemen auf die abstrakten Probleme erweitern, indem wir Kodierungen als Brücke benutzen. Allerdings wollen wir auch, dass die Definition von der speziellen Kodierung unabhängig ist. Das heißt, die Effizienz der Problemlösung sollte nicht von der Art 2 Es ist nicht zwingend erforderlich, dass der Wertebereich die Menge der binären Strings ist, jede andere Menge von Strings über einem endlichen Alphabet mit mindestens zwei Symbolen ist ebenfalls möglich. 3 Wir setzen voraus, dass die Ausgabe des Algorithmus von seiner Eingabe getrennt ist. Da mindestens ein Zeitschritt notwendig ist, um ein jedes Bit der Ausgabe zu erzeugen, und der Algorithmus O(T (n)) Zeitschritte benötigt, ist die Größe der Ausgabe in O(T (n)). 4 Wir bezeichnen mit {0, 1}∗ die Menge aller endlichen, aus den Symbolen der Menge {0, 1} zusammengesetzten Strings.
34.1 Polynomielle Zeit
1067
der Kodierung abhängen. Leider hängt sie aber ziemlich stark von der Kodierung ab. Setzen wir beispielsweise voraus, dass einem Algorithmus als einzige Eingabe eine ganze Zahl k geliefert wird, und dass die Laufzeit des Algorithmus Θ(k) ist. Wenn die ganze Zahl k in monadischer Kodierung vorliegt – ein String von k Einsen – dann ist die Laufzeit des Algorithmus O(n) für Eingaben der Länge n, was einer polynomiellen Laufzeit entspricht. Wenn wir jedoch die natürlichere binäre Darstellung der ganzen Zahl k verwenden, dann beträgt die Länge der Eingabe n = lg k + 1. In diesem Fall ist die Laufzeit des Algorithmus Θ(k) = Θ(2n ), was exponentiell in der Größe der Eingabe ist. Somit läuft der Algorithmus in Abhängigkeit von der Kodierung seiner Eingabe entweder in polynomieller oder superpolynomieller Zeit. Wie wir ein abstraktes Problem kodieren, hat großen Einfluss auf was wir unter polynomieller Zeit verstehen. Wir können nicht wirklich über das Lösen eines abstrakten Problems sprechen, ohne zunächst die Kodierung zu spezifizieren. Nichtsdestotrotz, in der Praxis, wenn wir „kostspielige“ Kodierungen wie die monadische ausschließen, beeinflusst die tatsächliche Kodierung eines Problems die Frage kaum, ob ein Problem in polynomieller Zeit lösbar ist. Beispielsweise hat die Entscheidung, ob wir ganze Zahlen zur Basis 3 oder zur Basis 2 darstellen, keinen Einfluss auf die Frage, ob das Problem in polynomieller Zeit lösbar ist, da wir eine zur Basis 3 dargestellte ganze Zahl in polynomieller Zeit in eine zur Basis 2 dargestellte ganze Zahl umwandeln können. ∗
∗
Wir sagen, dass eine Funktion f : {0, 1} → {0, 1} in polynomieller Zeit berechenbar ist, wenn ein Algorithmus A mit polynomieller Laufzeit existiert, der für eine ∗ gegebene Eingabe x ∈ {0, 1} die Ausgabe f (x) erzeugt. Wir sagen, dass zwei Kodierungen e1 und e2 für eine Menge I von Probleminstanzen polynomiell verwandt sind, wenn zwei in polynomieller Zeit berechenbare Funktionen f12 und f21 existieren, sodass für jedes i ∈ I die Gleichungen f12 (e1 (i)) = e2 (i) und f21 (e2 (i)) = e1 (i) gelten.5 Das heißt, ein Algorithmus mit polynomieller Laufzeit kann die Kodierung e2 (i) aus der Kodierung e1 (i) berechnen und umgekehrt. Wenn zwei Kodierungen e1 und e2 eines abstrakten Problems polynomiell verwandt sind, dann hängt die Antwort, ob das Problem in polynomieller Zeit lösbar ist oder nicht, nicht davon ab, welche der beiden Kodierungen wir verwenden. Das folgende Lemma zeigt dies. Lemma 34.1 Sei Q ein abstraktes Entscheidungsproblem auf einer Instanzmenge I und seien e1 und e2 polynomiell verwandte Kodierungen auf I. Dann gilt e1 (Q) ∈ P genau dann, wenn e2 (Q) ∈ P ist. Beweis: Wir müssen nur zeigen, dass mit e1 (Q) ∈ P auch e2 (Q) ∈ P gilt; der Beweis der Umkehrung läuft analog. Setzen Sie deshalb voraus, dass es eine Konstante k gibt, sodass e1 (Q) in Zeit O(nk ) gelöst werden kann. Setzen Sie desweiteren voraus, dass es eine Konstante c gibt, sodass für jede beliebige Probleminstanz i die Kodierung e1 (i) 5 Aus technischen Gründen fordern wir auch, dass die Funktionen f 12 und f21 „Nichtinstanzen auf Nichtinstanzen abbilden“. Eine Nichtinstanz einer Kodierung e ist ein String x ∈ {0, 1}∗ , sodass es keine Instanz i gibt, für die e(i) = x gilt. Wir fordern für jede Nichtinstanz x der Kodierung e1 , dass f12 (x) = y gilt, wobei y eine beliebige Nichtinstanz von e2 ist, und dass für jede Nichtinstanz x von e2 f21 (x ) = y gilt, wobei y eine beliebige Nichtinstanz von e1 ist.
1068
34 NP-Vollständigkeit
aus der Kodierung e2 (i) in Zeit O(nc ) berechnet werden kann, wobei n = |e2 (i)| gilt. Um das Problem e2 (Q) mit der Eingabe e2 (i) zu lösen, berechnen wir zunächst e1 (i) und lassen dann den Algorithmus für e1 (Q) auf e1 (i) laufen. Welche Zeit wird dafür benötigt? Die Umwandlung der Kodierung benötigt Zeit O(nc ), weshalb |e1 (i)| = O(nc ) gilt, da die Ausgabe eines seriellen Rechners nicht länger als die Laufzeit sein kann. Das k Lösen des Problems für e1 (i) benötigt Zeit O(|e1 (i)| ) = O(nck ), was mit konstantem c und k polynomiell ist. Die „Komplexität“ eines Problems, d. h. die Antwort auf die Frage, ob das Problem in polynomieller Zeit lösbar ist oder nicht, wird also nicht davon beeinflusst, ob die Instanz eines abstrakten Problems binär oder zur Basis 3 kodiert ist. Wenn allerdings die Instanzen monadisch kodiert sind, kann sich die Komplexität des Problems ändern. Um kodierungsunabhängige Aussagen treffen zu können, werden wir im Allgemeinen voraussetzen, dass die Probleminstanzen auf eine vernünftige und kurze Weise kodiert sind, wenn wir nichts anderes angeben. Genauer gesagt, wir setzen voraus, dass die Kodierung einer ganzen Zahl mit ihrer binären Darstellung polynomiell verwandt ist und dass die Kodierung einer endlichen Menge mit der Darstellung durch eine Liste ihrer Elemente (in Klammern eingeschlossen und mit Kommas getrennt) polynomiell verwandt ist. (ASCII ist ein solches Kodierungsschema.) Mit einer solchen „Standardkodierung“ können wir nun vernünftige Kodierungen anderer mathematischer Objekte, wie die von Tupeln, Graphen und Formeln, ableiten. Um anzugeben, dass ein Objekt in Standardkodierung kodiert ist, werden wir das Objekt in eckige Klammern einschließen; G bezeichnet also die Standardkodierung eines Graphen G. Solange wir implizit eine zu dieser Standardkodierung polynomiell verwandte Kodierung verwenden, können wir direkt über abstrakte Probleme sprechen, ohne uns auf eine spezielle Kodierung zu beziehen, weil wir wissen, dass die Wahl der Kodierung auf die polynomielle Lösbarkeit des Problems keinen Einfluss hat. Von nun an werden wir im Allgemeinen voraussetzen, dass es sich bei allen Probleminstanzen um binäre Strings in Standardkodierung handelt, außer wenn wir explizit etwas anderes sagen. Wir werden typischerweise auch die Unterscheidung zwischen abstrakten und konkreten Problemen vernachlässigen. Sie sollten in der Praxis jedoch darauf achten, ob für das zu lösende Problem die Standardkodierung eventuell nicht naheliegend ist und die Kodierung einen Unterschied macht.
Entscheidungsprobleme und formale Sprachen Wenn wir mit Entscheidungsproblemen arbeiten, können wir auf die Theorie der formalen Sprachen zurückgreifen. Lassen Sie uns einige Definition aus dieser Theorie hier anschauen. Ein Alphabet Σ ist eine endliche Menge von Symbolen. Eine Sprache L über Σ ist eine Menge von Strings, die aus Symbolen aus Σ zusammengesetzt sind. Wenn beispielsweise Σ = {0, 1} ist, dann ist die Menge L = {10, 11, 101, 111, 1011, 1101, 10001, . . .} die Sprache der binären Darstellungen der Primzahlen. Wir bezeichnen den leeren String mit ε und die leere Sprache mit ∅. Die Sprache aller Strings über Σ wird mit Σ∗ bezeichnet. Wenn beispielsweise Σ = {0, 1} gilt, dann ist Σ∗ = {ε, 0, 1, 00, 01, 10, 11, 000, . . .} die Menge aller binären Strings. Jede Sprache L über Σ
34.1 Polynomielle Zeit
1069
ist eine Teilmenge von Σ∗ . Wir können eine Vielzahl von Operationen auf Sprachen ausführen. Mengentheoretische Operationen, wie beispielsweise die Vereinigung und die Schnittmenge von Sprachen, folgen direkt aus mengentheoretischen Definitionen. Wir definieren das Komplement von L durch L = Σ∗ − L. Die Konkatenation L1 L2 zweier Sprachen L1 und L2 ist die Sprache L = {x1 x2 : x1 ∈ L1 und x2 ∈ L2 } . Der Abschluss oder Kleene-Stern einer Sprache L ist die Sprache L∗ = {ε} ∪ L ∪ L2 ∪ L3 ∪ · · · , wobei Lk die Sprache ist, die man durch k-fache Konkatenation von L mit sich selbst erhält. Vom Standpunkt der Theorie der formalen Sprachen ist die Menge der Instanzen eines Entscheidungsproblems Q einfach die Menge Σ∗ , wobei Σ = {0, 1} ist. Da Q durch diejenigen Probleminstanzen, die eine 1 (ja) als Antwort erzeugen, vollständig charakterisiert ist, können wir Q als eine Sprache L über Σ = {0, 1} betrachten mit L = {x ∈ Σ∗ : Q(x) = 1} . Beispielsweise ist die zu dem Entscheidungsproblem PATH korrespondierende Sprache PATH = {G, u, v, k : G = (V, E) ist ein ungerichteter Graph, u, v ∈ V, k ≥ 0 ist eine ganze Zahl und . es existiert ein Pfad von u nach v in G, der aus höchstens k Kanten besteht} . (Aus praktischen Gründen werden wir manchmal denselben Namen – in diesem Fall PATH – sowohl für das Entscheidungsproblem als auch für seine Sprache verwenden.) Formale Sprachen erlauben es uns, die Beziehung zwischen Entscheidungsproblemen und Algorithmen zu deren Lösung prägnant auszudrücken. Wir sagen, dass ein Algorithmus A einen String x ∈ {0, 1}∗ akzeptiert, wenn bei gegebener Eingabe x die Ausgabe A(x) des Algorithmus gleich 1 ist. A akzeptierte Die von einem Algorithmus Sprache ist die Menge der Strings L = x ∈ {0, 1}∗ : A(x) = 1 , d. h. die Menge der Strings, die der Algorithmus akzeptiert. Ein Algorithmus A weist einen String x zurück, wenn A(x) = 0 ist. Auch wenn die Sprache L durch einen Algorithmus A akzeptiert wird, wird der Algorithmus den Eingabestring x ∈ L nicht notwendigerweise zurückweisen. Beispielsweise kann der Algorithmus eine Endlosschleife durchlaufen. Eine Sprache L ist durch einen Algorithmus A entscheidbar, wenn jeder binäre String aus L von A akzeptiert wird und jeder nicht in L enthaltende binäre String von A zurückgewiesen wird. Eine Sprache L wird von einem Algorithmus A in polynomieller Zeit akzeptiert, wenn sie von A akzeptiert wird und zusätzlich eine Konstante k existiert, sodass jeder String x ∈ L der
1070
34 NP-Vollständigkeit
Länge n vom Algorithmus A in Zeit O(nk ) akzeptiert wird. Eine Sprache L ist durch einen Algorithmus A in polynomieller Zeit entscheidbar, wenn es eine Konstante k ∗ gibt, sodass der Algorithmus für jeden String x ∈ {0, 1} der Länge n in Zeit O(nk ) korrekt bestimmt, ob x ∈ L gilt. Demnach muss der Algorithmus für jeden String auf L eine Antwort ausgeben, wenn er die Sprache L akzeptiert; um die Sprache zu entscheiden, muss er jeden String aus {0, 1}∗ korrekt akzeptieren oder zurückweisen. Die Sprache PATH kann beispielsweise in polynomieller Zeit akzeptiert werden. Ein in polynomieller Zeit akzeptierender Algorithmus prüft, ob G einen ungerichteten Graphen kodiert und ob u und v Knoten aus G sind, benutzt die Breitensuche für die Berechnung eines kürzesten Pfades von u nach v in G und vergleicht dann die Anzahl der Kanten auf dem erhaltenen kürzesten Pfad mit k. Wenn G einen ungerichteten Graphen kodiert und der Pfad von u nach v höchstens k Kanten enthält, dann gibt der Algorithmus 1 aus und terminiert. Anderenfalls geht der Algorithmus in eine Endlosschleife. Dieser Algorithmus ist kein Entscheidungsalgorithmus für PATH, da er für Instanzen, bei denen ein kürzester Pfad mehr als k Kanten enthält, nicht explizit 0 ausgibt. Ein Entscheidungsalgorithmus für PATH muss binäre Strings explizit zurückweisen, die nicht zu PATH gehören. Für ein Entscheidungsproblem wie PATH ist ein solcher Entscheidungsalgorithmus einfach zu entwickeln: anstatt endlos weiter zu laufen, wenn es keinen Pfad von u nach v mit höchstens k Kanten gibt, gibt er 0 aus und terminiert; er muss auch 0 ausgeben und stoppen, wenn die Kodierung der Eingabe fehlerhaft ist. Für andere Probleme wie das Turingsche Halteproblem existiert ein akzeptierender Algorithmus, aber kein Entscheidungsalgorithmus. Wir können eine Komplexitätsklasse als eine Menge von Sprachen definieren, bei der die Zugehörigkeit durch ein Komplexitätsmaß bestimmt wird. Dies kann beispielsweise die Laufzeit eines Algorithmus sein, der entscheidet, ob ein gegebener String x zu einer Sprache L gehört. Die genaue Definition einer Komplexitätsklasse ist etwas technischer.6 Über formale Sprachen können wir eine alternative Definition der Komplexitätsklasse P angeben: ∗
P = {L ⊆ {0, 1} : es existiert ein Algorithmus A, der L in polynomieller Zeit entscheidet} . Tatsächlich ist P auch die Klasse der Sprachen, die in polynomieller Zeit akzeptiert werden können. Theorem 34.2 P = {L : L wird von einem Algorithmus in polynomieller Zeit akzeptiert} . Beweis: Da die von Algorithmen mit polynomieller Laufzeit entscheidbaren Sprachen eine Teilmenge der Klasse der von einem Algorithmus in polynomieller Zeit akzeptierten 6 Den interessierten Leser verweisen wir auf den fundamentalen Artikel von Hartmanis und Stearns [162].
34.1 Polynomielle Zeit
1071
Sprachen bilden, müssen wir nur zeigen, dass eine Sprache L durch einen Algorithmus mit polynomieller Laufzeit entscheidbar ist, wenn ein Algorithmus mit polynomieller Laufzeit L akzeptiert. Sei L die von einem Algorithmus A mit polynomieller Laufzeit akzeptierte Sprache. Wir werden ein klassisches „Simulationsargument“ verwenden, um einen anderen Algorithmus A mit polynomieller Laufzeit zu konstruieren, der L entscheidet. Da A die Sprache L in Zeit O(nk ) mit konstantem k akzeptiert, existiert eine Konstante c, sodass A die Sprache in höchstens cnk Schritten akzeptiert. Für einen beliebigen Eingabestring x simuliert der Algorithmus A cnk Schritte von A. Nachdem er cnk Schritte simuliert hat, prüft A das Verhalten von A. Wenn A den String x akzeptiert hat, dann akzeptiert A den String durch Ausgabe einer 1. Wenn A den String x nicht akzeptiert hat, dann weist A den String x durch Ausgabe von 0 zurück. Der zusätzliche Aufwand der Simulation von A durch A erhöht die Laufzeit um nicht mehr als einen polynomiellen Faktor. Folglich ist A ein Algorithmus mit polynomieller Laufzeit, der L entscheidet. Beachten Sie, dass der Beweis von Theorem 34.2 nicht konstruktiv ist. Ist eine Sprache L ∈ P gegeben, kann es sein, dass wir eine Schranke für die Laufzeit des Algorithmus A, der die Sprache L akzeptiert, nicht wirklich kennen. Wir wissen jedoch, dass eine solche Schranke existiert und deshalb auch, dass ein Algorithmus A zum Überprüfen dieser Schranke existiert, auch wenn wir nicht ohne weiteres dazu in der Lage sein werden, den Algorithmus A anzugeben.
Übungen 34.1-1 Definieren Sie das Optimierungsproblem LONGEST-PATH-LENGTH als die Relation, die jeder Instanz eines ungerichteten Graphen und zweier Knoten die Anzahl der Kanten eines längsten einfachen Pfades zwischen den zwei Knoten zuordnet. Definieren Sie das Entscheidungsproblem folgendermaßen: LONGEST-PATH = {G, u, v, k : G = (V, E) ist ein ungerichteter Graph, u, v ∈ V , k ≥ 0 ist eine ganze Zahl und es existiert ein einfacher Pfad von u nach v in G, der aus mindestens k Kanten besteht}. Zeigen Sie, dass das Optimierungsproblem LONGEST-PATH-LENGTH genau dann in polynomieller Zeit gelöst werden kann, wenn LONGEST-PATH ∈ P gilt. 34.1-2 Geben Sie eine formale Definition für das Problem an, den längsten einfachen Zyklus in einem ungerichteten Graphen zu bestimmen. Geben Sie das zugehörige Entscheidungsproblem an. Geben Sie die zu dem Entscheidungsproblem korrespondierende formale Sprache an. 34.1-3 Geben Sie unter Verwendung der Adjazenzmatrix-Darstellung eine formale Kodierung eines gerichteten Graphen durch binäre Strings an. Machen Sie das Gleiche für die Adjazenzlisten-Darstellung. Zeigen Sie, dass beide Darstellungen polynomiell verwandt sind. 34.1-4 Ist das dynamische Programm für das 0-1-Rucksackproblem, nach dem in Übung 16.2-2 gefragt wurde, ein Algorithmus mit polynomieller Laufzeit? Begründen Sie Ihre Antwort.
1072
34 NP-Vollständigkeit
34.1-5 Zeigen Sie, dass, wenn ein Algorithmus nur konstant viele Aufrufe von Unterroutinen, die jeweils polynomielle Laufzeit haben, enthält und ansonsten nur polynomielle Zeit benötigt, er polynomielle Laufzeit hat. Zeigen Sie auch, dass eine polynomielle Anzahl von Aufrufen von Unterroutinen, die jeweils polynomielle Laufzeit haben, zu einer exponentiellen Laufzeit führen kann. 34.1-6 Zeigen Sie, dass die Klasse P, als Menge von Sprachen betrachtet, unter den Operationen Vereinigung, Schnittmengenbildung, Konkatenation, Komplementbildung und Kleene-Stern abgeschlossen ist. Das heißt, sind L1 , L2 ∈ P, dann gilt L1 ∪ L2 ∈ P, L1 ∩ L2 ∈ P, L1 L2 ∈ P, L1 ∈ P, and L∗1 ∈ P.
34.2
Verifikation in polynomieller Zeit
Wir sehen uns nun Algorithmen an, die die Sprachzugehörigkeit verifizieren. Setzen Sie beispielsweise voraus, dass wir neben einer Instanz G, u, v, k des Entscheidungsproblems PATH auch einen Pfad p von u nach v gegeben haben. Wir können leicht überprüfen, ob p ein Pfad von u nach v in G ist und ob die Länge von p höchstens k beträgt. Falls dies zutrifft, können wir p als „Zertifikat“ ansehen, dass die Instanz tatsächlich zu PATH gehört. Für das Entscheidungsproblem PATH scheint uns dieses Zertifikat nicht viel zu bringen. Schließlich gehört PATH zu P – tatsächlich kann PATH in linearer Zeit gelöst werden – und folglich benötigen wir genauso lange, die Mengenzugehörigkeit aufgrund eines Zertifikats zu verifizieren, wie das Problem von Grund auf zu lösen. Wir werden nun ein Problem untersuchen, für das wir keinen in polynomieller Zeit arbeitenden Algorithmus kennen. Ist für dieses Problem jedoch ein Zertifikat gegeben, ist die Verifikation einfach.
Hamiltonische Kreise Das Problem, einen hamiltonischen Kreis in einem ungerichteten Graphen zu finden, wurde erstmalig vor über hundert Jahren untersucht. Ein hamiltonischer Kreis eines ungerichteten Graphen G = (V, E) ist ein einfacher Zyklus, der alle Knoten aus V enthält. Ein Pfad, der einen hamiltonischen Kreis enthält, wird als hamiltonisch bezeichnet; anderenfalls ist er nichthamiltonisch. Der Name geht auf W. R. Hamilton zurück, der ein mathematisches Spiel auf dem Dodekaeder beschrieb (Abbildung 34.2(a)), in dem ein Spieler fünf Nadeln in fünf benachbarte Ecken steckt und der andere Spieler den Pfad so vervollständigen muss, dass ein Zyklus gebildet wird, der alle Ecken enthält.7 Das Dodekaeder ist hamiltonisch; Abbildung 34.2(a) zeigt einen hamiltonischen Kreis. Jedoch sind nicht alle Graphen hamiltonisch. Abbildung 34.2(b) zeigt zum Beispiel einen bipartiten Graphen mit einer ungeraden Anzahl von Knoten. Übung 34.2-2 7 In einem Brief vom 17 Oktober 1856 an seinen Freund John T. Graves schrieb Hamilton [157, S. 624] “Ich habe gesehen, dass einige junge Leute sich amüsieren, indem sie ein neues mathematisches Spiel auf dem Dodekader ausprobieren, bei dem eine Person fünf Nadeln in fünf beliebige aufeinanderfolgende Positionen steckt . . . und der andere Spieler dann versucht, was aufgrund der Ausführungen in diesem Brief immer möglich ist, fünfzehn andere Nadeln in zyklischer Reihenfolge so einzufügen, dass alle anderen Punkte überdeckt sind und die Kette in unmittelbarer Nähe zu der Nadel, mit dem sein Gegner begann, endet.”
34.2 Verifikation in polynomieller Zeit
(a)
1073
(b)
Abbildung 34.2: (a) Ein Graph, der die Knoten, Kanten und Seiten eines Dodekaeders veranschaulicht. Die schattierten Kanten zeigen einen hamiltonischen Kreis. (b) Ein bipartiter Graph mit einer ungeraden Anzahl von Knoten. Solche Graphen sind alle nichthamiltonisch.
verlangt von Ihnen zu zeigen, dass all diese Graphen nichthamiltonisch sind. Wir können nun das Hamilton-Kreis-Problem „Besitzt ein Graph G einen hamiltonischen Kreis?“ durch eine formale Sprache definieren: HAM-CYCLE = {G : G ist ein hamiltonischer Graph} . Wie könnte ein Algorithmus die Sprache HAM-CYCLE entscheiden? Ist eine Probleminstanz G gegeben, listet ein möglicher Entscheidungsalgorithmus alle Permutationen der Knoten aus G auf und prüft anschließend für jede Permutation, ob es sich dabei um einen hamiltonischen Kreis handelt. Wie groß ist die Laufzeit dieses Algorithmus? Wenn wir als „vernünftige“ Kodierung eines Graphen seine Adjazenzmatrix verwenden, ist die √ Anzahl m der Knoten des Graphen Ω( n), wobei n = |G| die Länge der Kodierung von G ist. Es Permutationen der Knoten. Deshalb ist die Laufzeit √ √ gibt m! mögliche Ω(m!) = Ω( n !) = Ω(2 n ), was für keine Konstante k in O(nk ) liegt. Dieser naive Algorithmus läuft also nicht in polynomieller Zeit. Tatsächlich ist das Hamilton-KreisProblem NP-vollständig, wie wir in Abschnitt 34.5 beweisen werden.
Verifikationsalgorithmen Betrachten Sie ein etwas einfacheres Problem. Setzen Sie voraus, ein Freund teilt Ihnen mit, dass ein gegebener Graph hamiltonisch ist, und bietet eine Beweisführung an, indem er Ihnen die Knoten in der Reihenfolge ihres Erscheinens entlang eines hamiltonischen Kreises mitteilt. In diesem Fall wäre es sicher einfach, den Beweis zu verifizieren: Sie überprüfen, ob der Ihnen vorgelegte Kreis hamiltonisch ist, indem Sie testen, ob es sich um eine Permutation der Knoten von V handelt und ob jede der angegebenen Kanten auf dem hamiltonischen Kreis wirklich im Graphen existiert. Sie können sicherlich diesen
1074
34 NP-Vollständigkeit
Verifikationsalgorithmus so implementieren, dass er in Zeit O(n2 ) läuft, wobei n die Länge der Kodierung von G ist. Folglich kann ein Beweis, dass ein hamiltonischer Kreis in einem Graphen existiert, in polynomieller Zeit verifiziert werden. Wir definieren einen Verifikationsalgorithmus als einen Algorithmus A, der zwei Eingabeparameter hat, wobei das eine Argument der gewöhnliche Eingabestring x und das andere ein als Zertifikat bezeichneter binärer String y ist. Ein solcher Algorithmus A verifiziert einen Eingabestring x, wenn es ein Zertifikat y gibt, sodass A(x, y) = 1 gilt. Die durch einen Verifikationsalgorithmus A verifizierte Sprache ist L = x ∈ {0, 1}∗ : es existiert ein y ∈ {0, 1}∗ , sodass A(x, y) = 1 ist . Intuitiv betrachtet verifiziert ein Algorithmus A eine Sprache L, wenn es für jeden String x ∈ L ein Zertifikat y gibt, das A zum Beweis von x ∈ L benutzen kann. Darüber hinaus darf es für keinen String x ∈ L ein Zertifikat geben, das x ∈ L beweisen würde. Beim Hamilton-Kreis-Problem besteht das Zertifikat beispielsweise aus der Liste der Knoten eines hamiltonischen Kreises. Wenn ein Graph hamiltonisch ist, dann bietet ein hamiltonischer Kreis selbst bereits genug Information zur Verifikation dieser Tatsache. Umgekehrt kann es keine Knotenliste geben, die dem Verifikationsalgorithmus einen hamiltonischen Graphen vortäuschen kann, wenn der Graph nicht hamiltonisch ist, weil der Verifikationsalgorithmus den vorgeschlagenen „Zyklus“ sorgfältig prüft.
Die Komplexitätsklasse NP Die Komplexitätsklasse NP ist die Klasse der Sprachen, die jeweils durch einen Algorithmus mit polynomieller Laufzeit verifiziert werden können.8 Genauer gesagt, eine Sprache L gehört genau dann zu NP, wenn ein Algorithmus A mit polynomieller Laufzeit, der jeweils zwei Strings als Eingabe bekommt, und eine Konstante c existieren mit ∗
c
L = {x ∈ {0, 1} : es existiert ein Zertifikat y mit |y| = O(|x| ), sodass A(x, y) = 1} . Wir sagen, dass der Algorithmus A die Sprache L in polynomieller Zeit verifiziert. Aus unserer früheren Diskussion des Hamilton-Kreis-Problems sehen wir nun, dass HAM-CYCLE ∈ NP gilt. (Es ist immer gut zu wissen, dass eine wichtige Menge nicht leer ist.) Darüber hinaus ist L ∈ NP, wenn L ∈ P gilt, denn wenn es einen Algorithmus mit polynomieller Laufzeit zur Entscheidbarkeit von L gibt, kann dieser Algorithmus einfach in einen zweiargumentigen Verifikationsalgorithmus umgewandelt werden, der einfach die Zertifikate ignoriert und genau diejenigen Eingabestrings akzeptiert, die er als zu L zugehörig erkennt. Folglich gilt P ⊆ NP. Es ist unbekannt, ob P = NP gilt, aber die meisten Forscher glauben, dass P und NP nicht dieselbe Klasse darstellen. Intuitiv gesprochen, die Klasse P besteht aus Problemen, die schnell gelöst werden können; die Klasse NP besteht aus Problemen, für die 8 Der Name „NP“ steht für „nichtdeterministische Polynomialzeit“. Die Klasse NP wurde ursprünglich im Zusammenhang mit dem Nichtdeterminismus untersucht, aber dieses Buch verwendet den etwas einfacheren, trotzdem äquivalenten Begriff der Verifikation. Hopcroft und Ullman [180] geben einen guten Überblick der NP-Vollständigkeit anhand nichtdeterministischer Rechenmodelle.
34.2 Verifikation in polynomieller Zeit
1075
NP = co-NP P = NP = co-NP P (a)
co-NP
P = NP ∩ co-NP
(b)
NP
co-NP
NP ∩ co-NP
NP
P (c)
(d)
Abbildung 34.3: Die vier Möglichkeiten für die Beziehungen unter den Komplexitätsklassen. In jedem Diagramm kennzeichnet eine durch eine andere Menge umschlossene Menge eine Teilmengenbeziehung. (a) P = NP = co-NP. Die meisten Wissenschaftler betrachten diese Möglichkeit als die unwahrscheinlichste. (b) Wenn NP unter Komplementbildung vollständig ist, dann gilt NP = co-NP, aber P = NP muss nicht der Fall sein. (c) P = NP ∩ co-NP, aber NP ist unter Komplementbildung nicht abgeschlossen. (d) Es gilt NP = co-NP und P = NP∩co-NP. Die meisten Wissenschaftler betrachten diese Möglichkeit als die wahrscheinlichste.
eine Lösung schnell verifiziert werden kann. Aus eigener Erfahrung werden Sie wissen, dass es häufig viel schwieriger ist, ein Problem zu lösen als eine klar dargestellte Lösung zu verifizieren, besonders wenn Sie unter Zeitbeschränkungen arbeiten. Theoretische Informatiker glauben im Allgemeinen, dass diese Analogie auf die Klassen P und NP übertragbar ist, und dass NP folglich Sprachen enthält, die sich nicht in P befinden. Es gibt noch einen zwingenderen, wenn auch kein beweiskräftiger Hinweis dafür, dass P = NP gilt – die Existenz von „NP-vollständigen“ Sprachen. Wir werden diese Klasse im Abschnitt 34.3 untersuchen. Über die P = NP-Frage hinaus sind viele andere fundamentale Fragen ungelöst geblieben. Abbildung 34.3 zeigt einige mögliche Szenarien. Trotz umfangreicher Arbeit vieler Forscher weiß man nicht einmal, ob die Klasse NP bezüglich des Komplements abgeschlossen ist, d. h. ob aus L ∈ NP auch L ∈ NP folgt. Wir können die Komplexitätsklasse co-NP als die Menge der Sprachen L definieren, für die L ∈ NP gilt. Wir können die Frage, ob NP unter Komplementbildung abgeschlossen ist, umformulieren in die Frage, ob NP = co-NP gilt. Da P unter Komplementbildung abgeschlossen ist (Übung 34.1-6), folgt P ⊆ NP ∩ co-NP. Wiederum ist es unbekannt, ob P = NP ∩ co-NP gilt, oder ob es eine Sprache in NP ∩ co-NP − P gibt. Unsere Kenntnisse zu den genauen Beziehungen zwischen P und NP sind also bedauernswert unvollständig. Nichtsdestotrotz, wenngleich wir nicht fähig sind zu beweisen, dass ein spezielles Problem nicht handhabbar ist, erhalten wir wertvolle Informationen über das Problem, wenn wir beweisen können, dass es NP-vollständig ist.
1076
34 NP-Vollständigkeit
Übungen 34.2-1 Betrachten Sie die Sprache GRAPH-ISOMORPHISM = {G1 , G2 : G1 und G2 sind isomorphe Graphen}. Beweisen Sie, dass GRAPH-ISOMORPHISM ∈ NP gilt, indem Sie einen Algorithmus in polynomieller Zeit zur Verifikation der Sprache angeben. 34.2-2 Beweisen Sie, dass G nicht hamiltonisch ist, wenn G ein ungerichteter bipartiter Graph mit einer ungeraden Anzahl von Knoten ist. 34.2-3 Zeigen Sie, dass, wenn HAM-CYCLE ∈ P gilt, das Problem, die Knoten eines hamiltonischen Kreises der Reihenfolge nach aufzulisten, in polynomieller Zeit lösbar ist. 34.2-4 Beweisen Sie, dass die Klasse NP der Sprachen unter den Verknüpfungen Vereinigung, Durchschnitt, Konkatenation und Kleene-Stern abgeschlossen ist. Diskutieren Sie den Abschluss von NP unter der Komplementbildung. k
34.2-5 Zeigen Sie, dass jede Sprache in NP durch einen Algorithmus in Laufzeit 2O(n mit konstantem k entscheidbar ist.
)
34.2-6 Ein hamiltonischer Pfad in einem Graphen ist ein Pfad, der jeden Knoten genau einmal besucht. Zeigen Sie, dass die Sprache HAM-PATH = {G, u, v : es existiert ein hamiltonischer Pfad von u nach v im Graphen G} zu NP gehört. 34.2-7 Zeigen Sie, dass das Problem des hamiltonischen Pfades aus Übung 34.2-6 auf gerichteten azyklischen Graphen in polynomieller Zeit gelöst werden kann. Geben Sie einen effizienten Algorithmus für dieses Problem an. 34.2-8 Sei φ eine aus den Booleschen Eingabevariablen x1 , x2 , . . . , xk , Negationen (¬), UND (∧), ODER (∨) und Klammern konstruierte Boolesche Formel. Die Formel stellt eine Tautologie dar, wenn sie für alle Zuweisungen von 1 und 0 an die Eingabevariablen den Wert 1 annimmt. Definieren Sie TAUTOLOGY als die Sprache Boolescher Ausdrücke, die Tautologien darstellen. Zeigen Sie, dass TAUTOLOGY ∈ co-NP gilt. 34.2-9 Beweisen Sie, dass P ⊆ co-NP gilt. 34.2-10 Beweisen Sie, dass P = NP ist, wenn NP = co-NP gilt. 34.2-11 Sei G ein zusammenhängender ungerichteter Graph mit mindestens 3 Knoten und sei G3 der Graph, den wir erhalten, wenn wir alle Knotenpaare durch eine Kante verbinden, die im Graphen G durch einen Pfad mit der Länge von höchstens 3 verbunden sind. Beweisen Sie, dass G3 hamiltonisch ist. (Hinweis: Konstruieren Sie einen aufspannenden Baum von G und verwenden Sie ein induktives Argument.)
34.3 NP-Vollständigkeit und Reduktion
34.3
1077
NP-Vollständigkeit und Reduktion
Der vielleicht zwingendste Grund, weshalb theoretische Informatiker glauben, dass P = NP gilt, ist die Existenz der Klasse der „NP-vollständigen“ Probleme. Diese Klasse besitzt die verblüffende Eigenschaft, dass, wenn ein NP-vollständiges Problem in polynomieller Zeit gelöst werden kann, jedes Problem in NP in polynomieller Zeit gelöst werden kann, d. h. es gilt dann P = NP. Trotz langjährigem Bemühen ist jedoch kein Algorithmus mit polynomieller Laufzeit für ein NP-vollständiges Problem entdeckt worden. Die Sprache HAM-CYCLE ist ein NP-vollständiges Problem. Wenn wir HAM-CYCLE in polynomieller Zeit entscheiden könnten, dann könnten wir jedes Problem aus NP in polynomieller Zeit lösen. Wenn sich NP − P tatsächlich als nichtleer herausstellen sollte, könnten wir mit Bestimmtheit HAM-CYCLE ∈ NP − P behaupten. Die NP-vollständigen Sprachen sind in gewissen Sinne die „härtesten“ Sprachen in NP. In diesem Abschnitt werden wir zeigen, wie wir die relative „Härte“ von Sprachen mithilfe einer als „Polynomialzeitreduktion“ bezeichneten präzisen Notation untereinander vergleichen können. Anschließend definieren wir die NP-vollständigen Sprachen formal und schließen mit der Skizze eines Beweises, dass die als CIRCUIT-SAT bezeichnete Sprache NP-vollständig ist. In den Abschnitten 34.4 und 34.5 werden wir den Begriff der Reduktion benutzen, um die NP-Vollständigkeit vieler anderer Probleme zu zeigen.
Reduktion Intuitiv gesehen kann ein Problem Q auf ein anderes Problem Q reduziert werden, wenn jede Instanz von Q leicht in eine Instanz von Q „umformuliert“ werden kann, wobei die Lösung der Instanz von Q die Lösung der Instanz von Q liefert. Beispielsweise lässt sich das Problem der Lösung linearer Gleichungen in einer Unbekannten x auf das Problem der Lösung quadratischer Gleichungen reduzieren. Ist eine lineare Gleichung ax + b = 0 (mit der Lösung x = −b/a) gegeben, so transformieren wir sie in die quadratische Gleichung ax2 + bx + 0 = 0, deren Lösungen x = 0 und x = −b/a sind. Diese quadratische Gleichung liefert also insbesondere eine Lösung für ax+ b = 0. Wenn also ein Problem Q auf ein anderes Problem Q reduzierbar ist, dann ist Q in gewissem Sinne „nicht härter zu lösen“ als Q . Lassen Sie uns zu unseren formalen Sprachen für Entscheidungsprobleme zurückkehren. Wir sagen, dass eine Sprache L1 auf die Sprache L2 in polynomieller Zeit reduziert werden kann, was als L1 ≤P L2 geschrieben wird, wenn eine in polynomieller Zeit ∗ ∗ ∗ berechenbare Funktion f : {0, 1} → {0, 1} existiert, sodass für alle x ∈ {0, 1} die Gleichung x ∈ L1 genau dann wenn f (x) ∈ L2
(34.1)
gilt. Wir bezeichnen die Funktion f als Reduktionsfunktion. Der in polynomieller Zeit laufende Algorithmus F , der f berechnet, wird als Reduktionsalgorithmus bezeichnet. Abbildung 34.4 illustriert die Idee der Polynomialzeitreduktion einer Sprache L1 auf eine ∗ andere Sprache L2 . Jede Sprache ist eine Teilmenge von {0, 1} . Die Reduktionsfunktion
1078
34 NP-Vollständigkeit
{0,1}*
f
{0,1}* L2
L1
Abbildung 34.4: Illustration einer Polynomialzeitreduktion einer Sprache L1 auf eine Sprache L2 mithilfe der Reduktionsfunktion f . Für jede Eingabe x ∈ {0, 1}∗ ist die Antwort auf die Frage, ob x ∈ L1 gilt, gleich der Antwort auf die Frage, ob f (x) ∈ L2 gilt.
x
F
f (x)
ja, f (x) ∈ L2
ja, x ∈ L1
nein, f (x) ∈ L2
nein, x ∈ L1
A2 A1
Abbildung 34.5: Der Beweis von Lemma 34.3. Der Algorithmus F ist ein Reduktionsalgorithmus, der die Reduktionsfunktion f von L1 nach L2 in polynomieller Zeit jeweils auswertet. A2 ist ein Algorithmus mit polynomieller Laufzeit, der L2 entscheidet. Algorithmus A1 bestimmt, ob x ∈ L1 gilt, indem er F anwendet, um eine Eingabe x in f (x) zu transformieren und anschließend A2 anwendet, um zu entscheiden, ob f (x) ∈ L2 gilt.
f liefert eine Abbildung, die jeweils in polynomieller Zeit berechnet werden kann, sodass aus x ∈ L1 die Beziehung f (x) ∈ L2 folgt. Darüber hinaus ist f (x) ∈ L2 , wenn x ∈ L1 gilt. Folglich bildet die Reduktionsfunktion jede Instanz x des durch die Sprache L1 dargestellten Entscheidungsproblems auf eine Instanz f (x) des durch die Sprache L2 dargestellten Problems ab. Wenn es eine Antwort auf die Frage gibt, ob f (x) ∈ L2 gilt, dann liefert dies unmittelbar eine Antwort auf die Frage, ob x ∈ L1 gilt. Polynomialzeitreduktionen bilden ein leistungsfähiges Werkzeug, um zu beweisen, dass verschiedene Sprachen zu P gehören. Lemma 34.3 ∗
Sind L1 , L2 ⊆ {0, 1} Sprachen, für die L1 ≤P L2 gilt, dann folgt aus L2 ∈ P auch L1 ∈ P. Beweis: Sei A2 ein Algorithmus mit polynomieller Laufzeit, der L2 entscheidet, und sei F ein Reduktionsalgorithmus mit polynomieller Laufzeit, der die Reduktionsfunktion f berechnet. Wir werden einen Algorithmus A1 mit polynomieller Laufzeit konstruieren, der L1 entscheidet.
34.3 NP-Vollständigkeit und Reduktion
1079
Abbildung 34.5 illustriert die Konstruktion von A1 . Für eine gegebene Eingabe x ∈ ∗ {0, 1} wendet der Algorithmus A1 den Algorithmus F an, um x in f (x) zu transformieren, und wendet den Algorithmus A2 an, um zu testen, ob f (x) ∈ L2 gilt. Algorithmus A1 nimmt die Ausgabe von A2 und nimmt diese Antwort als seine eigene Antwort. Die Korrektheit von A1 folgt aus der Bedingung (34.1). Der Algorithmus läuft in polynomieller Zeit, da sowohl F als auch A2 in polynomieller Zeit laufen (siehe Übung 34.1-5).
NP-Vollständigkeit Polynomialzeitreduktionen stellen ein formales Mittel dar, mit dem wir zeigen können, dass ein Problem bis auf einen polynomiellen Zeitfaktor mindestens so hart wie ein anderes ist. Das heißt, wenn L1 ≤P L2 gilt, dann ist L1 um nicht mehr als einen polynomiellen Faktor härter als L2 . Dies ist der Grund dafür, weshalb die Bezeichnung „weniger als oder gleich“ für die Reduktion mnemonisch ist. Wir können nun die Menge der NP-vollständigen Sprachen definieren, die die härtesten Probleme in NP darstellen. Eine Sprache L ⊆ {0, 1}∗ wird als NP-vollständig bezeichnet, wenn 1. L ∈ NP ist und 2. L ≤P L für jedes L ∈ NP gilt. Wenn eine Sprache L die Eigenschaft 2 erfüllt, aber nicht notwendigerweise die Eigenschaft 1, dann sagen wir, dass L NP-schwer ist. Wir definieren außerdem NPC als die Klasse der NP-vollständigen Sprachen. Wie das folgende Theorem zeigt, liegt die NP-Vollständigkeit im Kern des Problems, ob P gleich NP ist. Theorem 34.4 Wenn ein NP-vollständiges Problem in polynomieller Zeit lösbar ist, dann gilt P = NP. Das heißt, wenn irgendein Problem aus NP nicht in polynomieller Zeit lösbar ist, dann ist kein NP-vollständiges Problem in Polynomialzeit lösbar. Beweis: Setzen Sie voraus, dass L ∈ P und auch L ∈ NPC ist. Für ein L ∈ NP gilt wegen Eigenschaft 2 der Definition der NP-Vollständigkeit L ≤P L. Folglich gilt wegen Lemma 34.3 auch, dass L ∈ P ist, was die erste Behauptung des Theorems beweist. Zum Beweis der zweiten Behauptung haben wir nur festzustellen, dass sie die Umkehrung der ersten Behauptung ist. Aus diesem Grunde dreht sich die Forschung bezüglich der Frage P = NP um die NP-vollständigen Probleme. Die meisten theoretischen Informatiker glauben, dass P =
1080
34 NP-Vollständigkeit
NP
NPC
P
Abbildung 34.6: Die Beziehungen zwischen P, NP und NPC, wie viele theoretische Informatiker sie sehen. Sowohl P als auch NPC sind vollständig in NP enthalten, und es gilt P∩NPC = ∅.
NP gilt, was zu den in Abbildung 34.6 gezeigten Beziehungen zwischen P, NP und NPC führen würde. Aber nach allem, was wir wissen, könnte sich doch jemand einen Algorithmus mit polynomieller Laufzeit für ein NP-vollständiges Problem ausdenken, wodurch P = NP bewiesen wäre. Nichtsdestotrotz liefert ein Beweis, dass ein Problem NP-vollständig ist, einen guten Hinweis auf dessen Nichthandhabbarkeit, weil bisher kein Algorithmus mit polynomieller Laufzeit für ein NP-vollständiges Problem entdeckt worden ist.
Erfüllbarkeit von Schaltkreisen Wir haben den Begriff eines NP-vollständigen Problems definiert, die NP-Vollständigkeit eines Problems aber bisher noch nicht wirklich bewiesen. Wenn wir für mindestens ein Problem die NP-Vollständigkeit bewiesen haben, können wir die Polynomialzeitreduktion als Hilfsmittel verwenden, um die NP-Vollständigkeit anderer Probleme zu beweisen. Aus diesem Grunde konzentrieren wir uns nun auf den Beweis der Existenz eines NPvollständigen Problems und beweisen, dass das Erfüllbarkeitsproblem von Schaltkreisen (auch als CSAT bezeichnet, engl.: circuit-satisfiability problem) NP-vollständig ist. Leider erfordert der formale Beweis der NP-Vollständigkeit des Erfüllbarkeitsproblems von Schaltkreisen technische Details, die über den Rahmen dieses Buches hinausgehen. Aus diesem Grunde werden wir nur einen Beweis skizzieren, der auf einem grundlegenden Verständnis Boolescher kombinatorischer Schaltkreise beruht. Boolesche kombinatorische Schaltkreise sind aus Booleschen kombinatorischen Schaltelementen zusammengesetzt, die durch Leitungen miteinander verbunden sind. Ein Boolesches kombinatorisches Schaltelement ist ein Element des Schaltkreises, das eine konstante Anzahl Boolescher Eingaben und Ausgaben besitzt und eine wohldefinierte Funktion ausführt. Die Booleschen Werte entstammen der Menge {0, 1}, wobei 0 für falsch und 1 für wahr steht. Die Booleschen kombinatorischen Schaltelemente, die wir beim Erfüllbarkeitsproblem von Schaltkreisen verwenden, berechnen jeweils eine einfache Boolesche Funktion und werden als logische Gatter bezeichnet. Abbildung 34.7 zeigt die drei grundlegenden logischen Gatter, mit denen wir beim Erfüllbarkeitsproblem von Schaltkreisen arbeiten: das NICHT-Gatter (auch Inverter genannt), das UND-Gatter und das ODERGatter. Der Inverter verwendet eine einzige binäre Eingabe x, deren Wert entweder 0 oder 1 ist, und erzeugt eine binäre Ausgabe z, deren Wert das Komplement des
34.3 NP-Vollständigkeit und Reduktion
x
z x
¬x
0 1
1 0
(a)
x y
z
1081 x y
z
x y x∧y
x y x∨y
0 0 1 1
0 0 1 1
0 1 0 1 (b)
0 0 0 1
0 1 0 1
0 1 1 1
(c)
Abbildung 34.7: Drei logische Basis-Gatter mit binären Ein- und Ausgaben. Unter jedem Gatter befindet sich die Wahrheitstabelle, die die Arbeitsweise des Gatters beschreibt. (a) Der Inverter. (b) Das UND-Gatter. (c) Das ODER-Gatter.
Eingabewertes ist. Jedes der beiden anderen Gatter erhält zwei binäre Eingaben x und y und erzeugt eine einzelne binäre Ausgabe z. Wir können die Arbeitsweise eines Gatters und eines Booleschen kombinatorischen Schaltkreises jeweils mithilfe einer Wahrheitstabelle beschreiben, wie sie in Abbildung 34.7 unter jedem Gatter gezeigt ist. Eine Wahrheitstabelle liefert die Ausgabe des Schaltelementes für jeden möglichen Satz von Eingabewerten. Beispielsweise sagt uns die Wahrheitstabelle des ODER-Gatters, dass im Falle der Eingabe x = 0 und y = 1 der Ausgabewert z = 1 ist. Wir verwenden die Symbole ¬ zur Bezeichnung der NICHT-Funktion, ∧ zur Bezeichnung der UND-Funktion und ∨ zur Bezeichnung der ODER-Funktion. Somit gilt beispielsweise 0 ∨ 1 = 1. Wir können UND- und ODER-Gatter so verallgemeinern, dass sie mehr als zwei Eingaben haben können. Die Ausgabe eines UND-Gatters ist 1, falls alle Eingaben den Wert 1 haben, anderenfalls 0. Die Ausgabe eines ODER-Gatters ist 1, wenn eines seiner Eingaben 1 ist, anderenfalls 0. Ein Boolescher kombinatorischer Schaltkreis besteht (aus einem oder) mehreren Booleschen kombinatorischen Schaltelementen, die durch Leitungen miteinander verbunden sind. Eine Leitung verbindet die Ausgabe eines Elementes mit der Eingabe eines anderen, wobei der Ausgabewert des ersten Elementes den Eingabewert des anderen liefert. Abbildung 34.8 zeigt zwei zueinander ähnliche Boolesche kombinatorische Schaltkreise; sie unterscheiden sich nur in einem Gatter. Teil (a) der Abbildung zeigt auch die Werte an den verschiedenen Leitungen, wenn die Eingabe x1 = 1, x2 = 1, x3 = 0 gegeben ist. Obwohl eine einzelne Leitung nie mit mehr als einem Ausgang eines Schaltelementes verbunden sein kann, kann sie die Eingaben zu verschiedenen Schaltelementen bereitstellen. Die Anzahl der durch eine Leitung versorgten Eingänge wird als Lastfaktor der Leitung bezeichnet. Wenn die Leitung mit keinem Ausgang verbunden ist, dann wird die Leitung als Eingangsport des Schaltkreises bezeichnet, die Eingabewerte von einer äußeren Quelle entgegennimmt. Wenn keine Eingabe eines Schaltelementes an eine Leitung angeschlossen ist, dann wird die Leitung als Ausgangsport des Schaltkreises bezeichnet, die die Ergebnisse der Schaltkreisberechnung der „Außenwelt“ mitteilt. (Eine Eingangsleitung kann auch mit einer Ausgabeleitung verbunden sein.) Zur
1082 x1 1 x2 1
34 NP-Vollständigkeit 1 1
x1 x2
1 0
1 0 x3 0
1 1 1
1
1 1 1
1 x3
(a)
(b)
Abbildung 34.8: Zwei Instanzen des Erfüllbarkeitsproblems von Schaltkreisen. (a) Die Eingabezuweisung x1 = 1, x2 = 1, x3 = 0 bewirkt für den Schaltkreis die Ausgabe 1. Der Schaltkreis ist deshalb erfüllbar. (b) Keine Eingabezuweisung führt bei diesem Schaltkreis zur Ausgabe 1. Der Schaltkreis ist deshalb unerfüllbar.
Definition des Erfüllbarkeitsproblems von Schaltkreisen begrenzen wir die Anzahl der Ausgangsports des Schaltkreises auf einen, wenngleich bei Booleschen kombinatorischen Schaltkreisen in wirklichen Hardware-Entwürfen in der Regel mehrere Ausgangsports zu finden sind. Boolesche kombinatorische Schaltkreise enthalten keine Zyklen. Um es anders zu formulieren: Setzen Sie voraus, dass wir einen Graphen G = (V, E) erzeugen mit jeweils einem Knoten für jedes logische Schaltelement und k gerichteten Kanten für jede Leitung, deren Lastfaktor k ist; der Graph enthält eine gerichtete Kante (u, v), wenn eine Leitung die Ausgabe des Schaltelementes u mit der Eingabe des Schaltelementes v verbindet. Dann muss G azyklisch sein. Eine Wertzuweisung eines Booleschen kombinatorischen Schaltkreises ist eine Belegung der Eingangsports des Schaltkreises mit Werten aus {0, 1}. Wir sagen, dass ein Boolescher kombinatorischer Schaltkreis mit einem Ausgangsport erfüllbar ist, wenn er eine erfüllende Zuweisung besitzt: eine Wertzuweisung, die in dem Schaltkreis die Ausgabe 1 bewirkt. Beispielsweise besitzt der Schaltkreis in Abbildung 34.8(a) die erfüllende Zuweisung x1 = 1, x2 = 1, x3 = 0, und ist somit erfüllbar. Wie Sie in Übung 34.31 zeigen sollen, bewirkt im Schaltkreis aus Abbildung 34.8(b) keine Wertzuweisung an x1 , x2 und x3 die Ausgabe 1. Der Schaltkreis erzeugt stets 0 und ist somit unerfüllbar. Das Erfüllbarkeitsproblem von Schaltkreisen lautet „Ist ein gegebener Boolescher kombinatorischer Schaltkreis, der aus UND-, ODER-und NICHT-Gattern zusammengesetzt ist, erfüllbar?“ Um diese Frage jedoch formal zu stellen, müssen wir uns auf eine Standardkodierung von Schaltkreisen einigen. Die Größe eines Booleschen kombinatorischen Schaltkreises ist die Anzahl der Booleschen kombinatorischen Schaltelemente plus die Anzahl der Leitungen im Schaltkreis. Wir können eine graphenähnliche Kodierung benutzen, die einen gegebenen Schaltkreis C auf einen binären String C abbildet, dessen Länge polynomiell in der Größe des Schaltkreises ist. Als formale Sprache können
34.3 NP-Vollständigkeit und Reduktion
1083
wir deshalb CIRCUIT-SAT = {C : C ist ein erfüllbarer Boolescher kombinatorischer Schaltkreis} definieren. Das Erfüllbarkeitsproblem von Schaltkreisen tritt bei der rechnergestützten Hardwareoptimierung auf. Wenn ein Teilschaltkreis immer die Ausgabe 0 erzeugt, dann ist dieser Teilschaltkreis überflüssig; der Entwerfer kann ihn durch einen einfacheren Teilschaltkreis ersetzen, der keine Schaltelemente enthält und als Ausgabe die Konstante 0 liefert. Sie sehen also, warum wir gerne einen Algorithmus mit polynomieller Laufzeit für dieses Problem haben wollen. Ist ein Schaltkreis C gegeben, könnten wir versuchen, durch einfaches Testen aller möglichen Eingabezuweisungen herauszufinden, ob der Schaltkreis erfüllbar ist. Leider müssten wir 2k mögliche Zuweisungen testen, wenn der Schaltkreis k Eingangsports besitzt. Wenn die Größe von C in k polynomiell ist, benötigt der Test Zeit Ω(2k ), was in der Größe des Schaltkreises superpolynomiell ist.9 Wie wir bereits angemerkt haben, gibt es tatsächlich starke Hinweise darauf, dass kein Algorithmus mit polynomieller Laufzeit existiert, der das Erfüllbarkeitsproblem von Schaltkreisen löst, weil dieses Erfüllbarkeitsproblem NP-vollständig ist. Wir führen den Beweis dieser Tatsache in zwei Schritten aus, gemäß der beiden Teile der Definition der NP-Vollständigkeit. Lemma 34.5 Das Erfüllbarkeitsproblem von Schaltkreisen gehört zur Klasse NP. Beweis: Wir werden einen Algorithmus mit zwei Argumenten und polynomieller Laufzeit vorstellen, der CIRCUIT-SAT verifizieren kann. Eine der Eingaben von A ist eine Standardkodierung eines Booleschen kombinatorischen Schaltkreises C. Die andere Eingabe ist ein Zertifikat, das einer Booleschen Wertzuweisung an die Leitungen von C entspricht. (Siehe Übung 34.3-4 für ein kleineres Zertifikat.) Wir konstruieren den Algorithmus A wie folgt. Für jedes logische Gatter im Schaltkreis überprüft der Algorithmus, ob der durch das Zertifikat an der Ausgangsleitung bereitgestellte Wert als Funktion der Werte an den Eingabeleitungen korrekt berechnet wird. Liegt der Ausgangsport des Schaltkreises in diesem Zertifikat auf dem Wert 1 und gehen die Tests an jedem logischen Gatter positiv aus, so gibt der Algorithmus 1 zurück, da die den Eingangsports des Schaltkreises C zugewiesenen Werte eine erfüllende Zuweisung darstellen. Anderenfalls gibt A den Wert 0 zurück. Immer wenn ein erfüllbarer Schaltkreis C die Eingabe des Algorithmus A bildet, gibt es ein Zertifikat, dessen Länge in der Größe von C polynomiell ist und das bewirkt, dass A 9 Andererseits hat ein Algorithmus mit Laufzeit O(2k ) eine in der Größe des Schaltkreises polynomielle Laufzeit, wenn die Größe des Schaltkreises C in Θ(2k ) ist. Auch wenn P = NP gilt, würde diese Situation nicht der NP-Vollständigkeit des Problems widersprechen; aus der Existenz eines Algorithmus mit polynomieller Laufzeit für einen Spezialfall folgt nicht, dass es in allen Fällen einen Algorithmus mit polynomieller Laufzeit gibt.
1084
34 NP-Vollständigkeit
den Wert 1 ausgibt. Immer wenn ein unerfüllbarer Schaltkreis C die Eingabe des Algorithmus A bildet, kann kein Zertifikat A die Erfüllbarkeit des Schaltkreises vortäuschen. Der Algorithmus A läuft in polynomieller Zeit; mit einer guten Implementierung reicht lineare Zeit aus. Somit können wir CIRCUIT-SAT in polynomieller Zeit verifizieren und es gilt CIRCUIT-SAT ∈ NP. Der zweite Teil des Beweises der NP-Vollständigkeit von CIRCUIT-SAT besteht darin, zu zeigen, dass die Sprache NP-schwer ist. Wir müssen also zeigen, dass jede Sprache in NP in polynomieller Zeit auf CIRCUIT-SAT reduzierbar ist. Der tatsächliche Beweis dieser Aussage ist voller technischer Details. Deshalb entscheiden wir uns für eine Beweisskizze, die ein gewisses Verständnis der Arbeitsweise eines Rechners voraussetzt. Ein Computerprogramm wird im Hauptspeicher des Rechners durch eine Folge von Anweisungen (in Maschinensprache) gespeichert. Eine typische Anweisung kodiert eine auszuführende Operation, Adressen von Operanden im Speicher und eine Adresse, an der das Ergebnis gespeichert werden soll. Mithilfe eines speziellen, als Befehlszähler bezeichneten Speicherplatzes wird verfolgt, welche Anweisung als nächstes ausgeführt werden soll. Der Befehlszähler wird beim Holen einer Anweisung aus dem Speicher automatisch inkrementiert. Dadurch wird erreicht, dass der Rechner die Anweisungen sequentiell ausführt. Die Ausführung einer Anweisung kann jedoch eine Wertzuweisung an den Befehlszähler bewirken, was zu einer Änderung der Ausführungsreihenfolge führt und dem Rechner das Ausführen von Schleifen und bedingten Sprüngen erlaubt. Zu jedem Zeitpunkt der Ausführung eines Programms enthält der Speicher des Rechners den gesamten Zustand der Berechnung. (Wir verwenden den Speicher, um das Programm selbst, den Befehlszähler, den Arbeitsspeicher und die verschiedenen Zustandsbits, die der Rechner zur Verwaltung benötigt, zu speichern.) Wir bezeichnen jeden speziellen Zustand des Speichers als Konfiguration. Wir können die Ausführung einer Anweisung als eine Abbildung einer Konfiguration in eine andere ansehen. Der Rechner, der diese Abbildung ausführt, kann als Boolescher kombinatorischer Schaltkreis implementiert werden, den wir im Beweis des folgenden Lemmas mit M bezeichnen. Lemma 34.6 Das Erfüllbarkeitsproblem von Schaltkreisen ist NP-schwer. Beweis: Sei L eine Sprache in NP. Wir werden einen Algorithmus F mit polynomieller Laufzeit beschreiben, der eine Reduktionsfunktion f berechnet, die jeden binären String x auf einen Schaltkreis C = f (x) abbildet, sodass x ∈ L genau dann gilt, wenn C ∈ CIRCUIT-SAT ist. Wegen L ∈ NP muss ein Algorithmus A existieren, der L in polynomieller Zeit verifiziert. Der von uns zu konstruierende Algorithmus F wird den Algorithmus A mit seinen zwei Eingaben verwenden, um die Reduktionsfunktion f zu berechnen.
34.3 NP-Vollständigkeit und Reduktion
c0
A
BZ
Zustand der HM
1085
x
y
Arbeitsspeicher
x
y
Arbeitsspeicher
x
y
Arbeitsspeicher
x
y
Arbeitsspeicher
M
c1
A
BZ
Zustand der HM
M
c2
A
BZ
Zustand der HM
M
… M
cT(n)
A
BZ
Zustand der HM
0/1 Ausgabe Abbildung 34.9: Die Folge der Konfigurationen, die durch einen auf der Eingabe x und dem Zertifikat y laufenden Algorithmus A erzeugt wird. Jede Konfiguration stellt einen Zustand des Rechners für einen Rechenschritt dar und enthält neben A, x und y den Befehlszähler (BZ), den Zustand der Hilfsmaschine (HM) und den Arbeitsspeicher. Abgesehen von dem Zertifikat y ist die initiale Konfiguration c0 konstant. Ein Boolescher kombinatorischer Schaltkreis M bildet jede Konfiguration auf die nächste Konfiguration ab. Die Ausgabe besteht aus einem ausgezeichneten Bit im Arbeitsspeicher.
1086
34 NP-Vollständigkeit
Sei T (n) die Laufzeit des Algorithmus A auf Eingabestrings der Länge n im schlechtesten Fall und sei k ≥ 1 eine Konstante, sodass T (n) = O(nk ) und die Länge des Zertifikates in O(nk ) ist. (Die Laufzeit von A ist eigentlich ein Polynom in der gesamten Eingabegröße, die sowohl den Eingabestring als auch das Zertifikat umfasst. Weil aber die Länge des Zertifikates polynomiell in der Länge n des Eingabestrings ist, ist die Laufzeit polynomiell in n.) Die Grundidee des Beweises besteht darin, die Berechnung von A als Folge von Konfigurationen darzustellen. Wie Abbildung 34.9 illustriert, können wir jede Konfiguration in Teile zerlegen, die aus dem Programm für A, dem Befehlszähler und dem Zustand der Hilfsmaschine, der Eingabe x, dem Zertifikat y und dem Arbeitsspeicher bestehen. Der kombinatorische Schaltkreis M , der den Rechner implementiert, bildet ausgehend von der initialen Konfiguration c0 jede Konfiguration ci auf die nächste Konfiguration ci+1 ab. Algorithmus A schreibt seine Ausgabe – 0 oder 1 – an eine bestimmte Stelle des Arbeitsspeichers, wenn er mit seiner Ausführung fertig ist, und wenn wir voraussetzen, dass A danach anhält, d. h. terminiert, dann ändert sich dieser Wert nicht mehr. Wenn der Algorithmus höchstens T (n) Schritte macht, dann erscheint somit die Ausgabe als eines der Bits der Konfiguration cT (n) . Der Reduktionsalgorithmus F konstruiert einen einzelnen kombinatorischen Schaltkreis, der alle aus einer gegebenen Ausgangskonfiguration erzeugten Konfigurationen berechnet. Die Idee besteht darin, T (n) Kopien des Schaltkreises aneinander zu fügen. Die Ausgabe des i-ten Schaltkreises, der die Konfiguration ci erzeugt, wird in den (i + 1)ten Schaltkreis direkt als Eingabe eingespeist. Die Konfigurationen sind also nicht im Speicher des Rechners sondern auf Leitungen gespeichert, die die Kopien von M verbinden. Rufen Sie sich in Erinnerung, was der Reduktionsalgorithmus F in polynomieller Laufzeit machen muss. Ist die Eingabe x gegeben, muss er einen Schaltkreis C = f (x) konstruieren, der genau dann erfüllbar ist, wenn ein Zertifikat y existiert, für das A(x, y) = 1 gilt. Wenn der Algorithmus eine Eingabe x erhält, dann berechnet er zuerst n = |x| und konstruiert einen kombinatorischen Schaltkreis C , der aus T (n) Kopien von M besteht. Die Eingabe an C ist eine initiale Konfiguration, die einer Berechnung von A(x, y) entspricht, und die Ausgabe ist die Konfiguration cT (n) . Der Algorithmus F modifiziert den Schaltkreis C leicht, um den Schaltkreis C = f (x) zu konstruieren. Erstens verbindet er die Eingaben an C , der dem Programm für A entspricht, den initialen Befehlszähler, die Eingabe x und den Anfangszustand des Speichers direkt mit diesen bekannten Werten. Folglich entsprechen die einzigen verbleibenden Eingabeports des Schaltkreises dem Zertifikat y. Zweitens ignoriert er alle Ausgangsports des Schaltkreises, abgesehen von dem einen Bit aus cT (n) , das der Ausgabe von A entspricht. Dieser so konstruierte Schaltkreis C berechnet C(y) = A(x, y) für jede Eingabe y der Länge O(nk ). Der Reduktionsalgorithmus F berechnet bei gegebenem Eingabestring x einen solchen Schaltkreis C und gibt ihn zurück. Wir müssen noch zwei Eigenschaften beweisen. Erstens müssen wir zeigen, dass F die Reduktionsfunktion f korrekt berechnet. Wir müssen also zeigen, dass C genau dann erfüllbar ist, wenn ein Zertifikat y existiert, für das A(x, y) = 1 gilt. Zweitens müssen wir zeigen, dass F in polynomieller Zeit arbeitet.
34.3 NP-Vollständigkeit und Reduktion
1087
Um zu zeigen, dass F die Reduktionsfunktion korrekt berechnet, setzen wir voraus, dass ein Zertifikat y der Länge O(nk ) existiert, für das A(x, y) = 1 gilt. Wenn wir anschließend die Bits von y als Eingabe von C verwenden, ist die Ausgabe von C gleich C(y) = A(x, y) = 1. Folglich ist C im Falle der Existenz eines solchen Zertifikates erfüllbar. Für die andere Richtung setzen Sie voraus, dass C erfüllbar ist. Folglich existiert eine Eingabe y für C, für die C(y) = 1 gilt, woraus wir schlussfolgern können, dass A(x, y) = 1 ist. Somit berechnet F die Reduktionsfunktion korrekt. Um die Beweisskizze zu vervollständigen, müssen wir nur noch zeigen, dass F in Zeit, die polynomiell in n = |x| ist, läuft. Die erste Feststellung ist, dass die Anzahl der zur Darstellung einer Konfiguration notwendigen Bits polynomiell in n ist. Das Programm für A hat selbst eine konstante Größe, unabhängig von der Länge seiner Eingabe x. Die Länge der Eingabe x ist n, und die Länge des Zertifikates y ist O(nk ). Da der Algorithmus höchstens O(nk ) Schritte ausführt, ist der Umfang des von A benötigten Arbeitsspeichers ebenfalls polynomiell in n. (Wir setzen voraus, dass dieser Speicher zusammenhängend ist; in Übung 34.3-5 sollen Sie das Argument auf den Fall erweitern, dass die Speicherplätze, auf die A zugreift, über einen viel größeren Speicherbereich verstreut sind. Dabei kann das spezielle „Streumuster“ für jede Eingabe x unterschiedlich sein.) Der kombinatorische Schaltkreis M , der den Rechner implementiert, hat eine in der Länge einer Konfiguration polynomielle Größe, wobei die Konfiguration polynomiell in O(nk ) ist; somit ist die Größe von M polynomiell in n. (Der Großteil dieser Schaltung implementiert die Logik des Speichersystems.) Der Schaltkreis C besteht aus höchstens t = O(nk ) Kopien von M und hat folglich eine in n polynomielle Größe. Der Reduktionsalgorithmus F kann C aus x in polynomieller Zeit konstruieren, da jeder Konstruktionsschritt polynomielle Zeit benötigt. Die Sprache CIRCUIT-SAT ist deshalb mindestens so hart wie jede andere Sprache in NP, und weil sie zu NP gehört, ist sie NP-vollständig. Theorem 34.7 Das Erfüllbarkeitsproblem von Schaltkreisen ist NP-vollständig. Beweis: Der Beweis folgt unmittelbar aus den Lemmata 34.5 und 34.6 sowie der Definition der NP-Vollständigkeit.
Übungen 34.3-1 Verifizieren Sie, dass der in Abbildung 34.8(b) gezeigte Schaltkreis unerfüllbar ist. 34.3-2 Zeigen Sie, dass die ≤P -Relation eine bezüglich der Sprachen transitive Relation ist. Sie sollen also zeigen, dass im Falle L1 ≤P L2 und L2 ≤P L3 die Relation L1 ≤P L3 gilt.
1088
34 NP-Vollständigkeit
34.3-3 Beweisen Sie, dass L ≤P L genau dann gilt, wenn L ≤P L ist. 34.3-4 Zeigen Sie, dass wir in einem alternativen Beweis zu Lemma 34.5 eine erfüllende Zuweisung als Zertifikat hätten verwenden können. Welches Zertifikat liefert einen einfacheren Beweis? 34.3-5 Der Beweis von Lemma 34.6 setzt voraus, dass der Arbeitsspeicher für den Algorithmus A einen zusammenhängenden Bereich polynomieller Größe belegt. An welcher Stelle des Beweises nutzen wir diese Voraussetzung aus? Erläutern Sie, weshalb wir diese Voraussetzung ohne Beschränkung der Allgemeinheit machen dürfen. 34.3-6 Eine Sprache L ist dann für eine Sprachklasse C bezüglich Polynomialzeitreduktionen vollständig, wenn L ∈ C und L ≤P L für alle L ∈ C gilt. Zeigen ∗ Sie, dass ∅ und {0, 1} die einzigen Sprachen in P sind, die für P bezüglich der Polynomialzeitreduktionen nicht vollständig sind. 34.3-7 Zeigen Sie, dass L für NP genau dann bezüglich Polynomialzeitreduktionen (siehe Übung 34.3-6) vollständig ist, wenn L für co-NP vollständig ist. 34.3-8 Der Reduktionsalgorithmus F im Beweis von Lemma 34.6 konstruiert den Schaltkreis C = f (x) auf Grundlage des Wissens über x, A und k. Professor Sartre beobachtet, dass der String x die Eingabe von F ist, aber nur die Existenz von A, k und des konstanten in der Laufzeit O(nk ) implizit enthaltenen Faktors dem Algorithmus F bekannt ist, nicht aber deren tatsächlichen Werte. Daher schlussfolgert der Professor, dass F den Schaltkreis möglicherweise nicht konstruieren kann und dass die Sprache CIRCUIT-SAT nicht notwendigerweise NP-schwer ist. Erklären Sie die Schwachstelle in der Argumentation des Professors.
34.4
NP-Vollständigkeitsbeweise
Wir haben durch einen direkten Beweis bewiesen, dass das Erfüllbarkeitsproblem von Schaltkreisen NP-vollständig ist, indem wir gezeigt haben, dass L ≤P CIRCUIT-SAT für jede Sprache L ∈ NP gilt. In diesem Abschnitt werden wir zeigen, wie wir die NPVollständigkeit von Sprachen beweisen können, ohne jede Sprache aus NP direkt auf die gegebene Sprache zu reduzieren. Wir werden diese Methode illustrieren, indem wir die NP-Vollständigkeit verschiedener Erfüllbarkeitsprobleme der Aussagenlogik beweisen. Abschnitt 34.5 liefert viele weitere Beispiele dieser Methodik. Das folgende Lemma bildet die Grundlage unserer Methode für den Beweis der NPVollständigkeit einer Sprache. Lemma 34.8 Wenn L eine Sprache ist, für die L ≤P L für ein L ∈ NPC gilt, dann ist L NPschwer. Ist zudem L ∈ NP, dann gilt sogar L ∈ NPC.
34.4 NP-Vollständigkeitsbeweise
1089
Beweis: Da L NP-vollständig ist, gilt für alle L ∈ NP auch L ≤P L . Nach Voraussetzung gilt L ≤P L, und damit wegen der Transitivität (Übung 34.3-2) L ≤P L. Dies zeigt, dass L NP-schwer ist. Im Falle L ∈ NP haben wir dann L ∈ NPC. Mit anderen Worten reduzieren wir jede Sprache aus NP auf L, indem wir eine bekannte NP-vollständige Sprache L auf L reduzieren. Damit liefert uns Lemma 34.8 eine Methode zum Beweis der NP-Vollständigkeit einer Sprache: 1. Beweisen Sie L ∈ NP. 2. Wählen Sie eine bekannte NP-vollständige Sprache L aus. 3. Geben Sie einen Algorithmus an, der eine Funktion f berechnet, die jede Instanz ∗ x ∈ {0, 1} von L auf eine Instanz f (x) von L abbildet. ∗
4. Beweisen Sie, dass die Funktion f die Eigenschaft hat, dass für alle x ∈ {0, 1} genau dann x ∈ L ist, wenn f (x) ∈ L gilt. 5. Beweisen Sie, dass der Algorithmus zur Berechnung von f in polynomieller Zeit läuft. (Die Schritte 2–5 zeigen, dass L NP-schwer ist.) Diese Methode, eine bekannte NPvollständige Sprache auf eine andere Sprache zu reduzieren, ist weit einfacher als der Prozess, zu beweisen, dass jede Sprache aus NP auf die betrachtete Sprache reduzierbar ist. Der Beweis von CIRCUIT-SAT ∈ NPC hat uns einen „Fuß in der Tür“ verschafft. Da wir wissen, dass das Erfüllbarkeitsproblem von Schaltkreisen NP-vollständig ist, können wir die NP-Vollständigkeit anderer Probleme viel einfacher beweisen. Darüber hinaus stehen uns immer mehr Auswahlmöglichkeiten hinsichtlich der Sprache, von der aus wir reduzieren können, zur Verfügung, wenn wir einen Katalog bekannter NP-vollständiger Probleme aufstellen.
Erfüllbarkeitsproblem der Aussagenlogik Wir illustrieren die Reduktionsmethode durch einen Beweis der NP-Vollständigkeit des Problems, zu entscheiden, ob eine Boolesche Formel (kein Schaltkreis) erfüllbar ist. Diesem Problem gebührt die historische Ehre, das erste Problem gewesen zu sein, dessen NP-Vollständigkeit bewiesen wurde. Wir formulieren das Erfüllbarkeitsproblem (der Aussagenlogik) in Form der Sprache SAT folgendermaßen. Eine Instanz von SAT ist eine Boolesche Formel φ die aus 1. n Booleschen Variablen: x1 , x2 , . . . , xn , 2. m Booleschen Verknüpfungen: jeweils eine beliebige Boolesche Funktion mit einer oder zwei Eingaben und einer Ausgabe, wie beispielsweise ∧ (UND), ∨ (ODER), ¬ (NICHT), → (Implikation), ↔ (genau dann wenn) und 3. Klammern – ohne Beschränkung der Allgemeinheit setzen wir voraus, dass es keine redundanten Klammern gibt, d. h. es gibt höchstens ein Klammerpaar pro Boolescher Verknüpfung –
1090
34 NP-Vollständigkeit
besteht. Wir können leicht eine Boolesche Formel φ mit einer in n + m polynomiellen Länge kodieren. Wie bei Booleschen kombinatorischen Schaltkreisen ist eine Wertzuweisung für eine Boolesche Formel φ eine Zuweisung von Werten an die Variablen von φ. Eine erfüllende Zuweisung ist eine Wertzuweisung, unter der die Formel den Wert 1 liefert. Eine Formel mit einer erfüllenden Zuweisung ist eine erfüllbare Formel. Das Erfüllbarkeitsproblem fragt danach, ob eine gegebene Boolesche Formel erfüllbar ist, und lässt sich durch die formale Sprache SAT = {φ : φ ist eine erfüllbare Boolesche Formel} . angeben. Zum Beispiel hat die Formel φ = ((x1 → x2 ) ∨ ¬((¬x1 ↔ x3 ) ∨ x4 )) ∧ ¬x2 die erfüllende Zuweisung x1 = 0, x2 = 0, x3 = 1, x4 = 1, da φ = ((0 → 0) ∨ ¬((¬0 ↔ 1) ∨ 1)) ∧ ¬0 = (1 ∨ ¬(1 ∨ 1)) ∧ 1 = (1 ∨ 0) ∧ 1 =1
(34.2)
gilt; somit gehört diese Formel zu SAT. Der naive Algorithmus, der entscheidet, ob eine beliebige Boolesche Formel erfüllbar ist, läuft nicht in polynomieller Zeit. Eine Formel φ mit n Variablen besitzt 2n mögliche Zuweisungen. Wenn die Länge von φ ein Polynom in n ist, dann erfordert der Test aller Zuweisungen Zeit Ω(2n ), was in der Länge von φ superpolynomiell ist. Wie das folgende Theorem zeigt, ist die Existenz eines Algorithmus mit polynomieller Laufzeit unwahrscheinlich. Theorem 34.9 Die Erfüllbarkeit Boolescher Formeln ist NP-vollständig. Beweis: Wir beweisen SAT ∈ NP zuerst. Dann beweisen wir, dass SAT NP-schwer ist, indem wir CIRCUIT-SAT ≤P SAT zeigen; mit Lemma 34.8 folgt dann das Theorem. Um zu beweisen, dass SAT zu NP gehört, zeigen wir, dass ein aus einer erfüllenden Zuweisung für eine Eingabeformel φ bestehendes Zertifikat in polynomieller Zeit verifiziert werden kann. Der Verifikationsalgorithmus ersetzt jede Variable in der Formel durch ihren entsprechenden Wert und berechnet den Ausdruck, genauso wie wir es oben bei der Gleichung (34.2) gemacht haben. Diese Aufgabe kann leicht in polynomieller Zeit erledigt werden. Wenn der Ausdruck zu 1 auswertet, dann hat der Algorithmus verifiziert, dass die Formel erfüllbar ist. Folglich gilt die erste Bedingung für die NP-Vollständigkeit aus Lemma 34.8. Um zu beweisen, dass SAT NP-schwer ist, zeigen wir, dass CIRCUIT-SAT ≤P SAT gilt. Wir müssen also zeigen, wie wir eine Instanz des Erfüllbarkeitsproblems von Schaltkreisen in polynomieller Zeit auf eine Instanz des Erfüllbarkeitsproblems der Aussagenlogik
34.4 NP-Vollständigkeitsbeweise x1 x2
1091
x5 x8 x6 x9
x3
x4
x10
x7
Abbildung 34.10: Die Reduktion des Erfüllbarkeitsproblems von Schaltkreisen auf das Erfüllbarkeitsproblem der Aussagenlogik. Die durch den Reduktionsalgorithmus gebildete Formel besitzt für jede Leitung des Schaltkreises eine Variable.
reduzieren können. Wir können Induktion anwenden, um jeden Booleschen kombinatorischen Schaltkreis durch eine Boolesche Formel zu beschreiben. Wir betrachten einfach das Gatter, das die Ausgabe des Schaltkreises produziert, und stellen induktiv jeden seiner Eingabeports als Formel dar. Wir erhalten die Formel für den Schaltkreis, indem wir einen Ausdruck hinschreiben, der die Funktion des Gatters auf die Formeln der Eingangsports des Gatters anwendet. Leider führt diese naive Methode nicht zu einer Polynomialzeitreduktion. Wie Sie in Übung 34.4-1 zeigen sollen, können gemeinsam benutzte Teilformeln – die aus Gattern resultieren, deren Ausgänge Lastfaktor 2 oder mehr haben – dazu führen, dass die Größe der erzeugten Formel exponentiell wächst. Folglich muss der Reduktionsalgorithmus etwas geschickter gewählt werden. Abbildung 34.10 illustriert anhand der Schaltung aus Abbildung 34.8(a), wie wir dieses Problem lösen können. Für jede Leitung xi des Schaltkreises C besitzt die Formel φ eine Variable xi . Wir können nun jeweils mittels einer kleinen Formel beschreiben, wie jedes Gatter arbeitet, wobei diese Formel nur Variablen von mit diesem Gatter inzidenten Leitungen enthält. Beispielsweise kann die Funktion des UND-Gatters durch die Formel x10 ↔ (x7 ∧ x8 ∧ x9 ) beschrieben werden. Wir nennen jede dieser kleinen Formeln eine Teilformel . Die durch den Reduktionsalgorithmus gebildete Formel φ ist das UND der Ausgabevariable des Schaltkreises mit der Konjunktion der Teilformeln, die die Operation eines jeden Gatters des Schaltkreises beschreiben. Für den Schaltkreis in der Abbildung lautet die Formel φ = x10 ∧ (x4 ↔ ¬x3 ) ∧ (x5 ↔ (x1 ∨ x2 )) ∧ (x6 ↔ ¬x4 ) ∧ (x7 ↔ (x1 ∧ x2 ∧ x4 )) ∧ (x8 ↔ (x5 ∨ x6 )) ∧ (x9 ↔ (x6 ∨ x7 )) ∧ (x10 ↔ (x7 ∧ x8 ∧ x9 )) . Für jeden gegebenen Schaltkreis C ist es einfach, eine solche Formel in polynomieller Zeit zu erzeugen.
1092
34 NP-Vollständigkeit
Weshalb ist der Schaltkreis C genau dann erfüllbar, wenn die Formel φ erfüllbar ist? Wenn C eine erfüllende Zuweisung besitzt, hat jede Leitung des Schaltkreises einen wohldefinierten Wert und die Ausgabe des Schaltkreises ist 1. Wenn wir also die Werte der Leitungen den Variablen in φ zuweisen, wertet jede Teilformel zu 1 aus und folglich auch die gesamte Konjunktion. Wenn es umgekehrt eine Zuweisung an die Variablen von φ gibt, für die φ zu 1 auswertet, dann ist der Schaltkreis C aufgrund eines analogen Argumentes erfüllbar. Damit haben wir gezeigt, dass CIRCUIT-SAT ≤P SAT gilt, was den Beweis abschließt.
3-CNF-Erfüllbarkeit Wir können viele Probleme als NP-vollständig beweisen, indem wir das Erfüllbarkeitsproblem der Aussagenlogik auf die Probleme reduzieren. Der jeweilige Reduktionsalgorithmus muss jedoch jede Eingabeformel behandeln, was zu einer riesigen Anzahl von Fällen führt, die wir jeweils betrachten müssen. Wir ziehen es häufig vor, von einer beschränkten Sprache Boolescher Formeln ausgehend zu reduzieren, sodass wir weniger Fälle betrachten müssen. Natürlich dürfen wir die Sprache nicht soweit einschränken, dass sie in polynomieller Zeit lösbar wird. Eine geeignete Sprache ist die 3-CNF-Erfüllbarkeit oder 3-CNF-SAT. Wir definieren die 3-CNF-Erfüllbarkeit unter Verwendung der folgenden Begriffe. Ein Literal in einer Booleschen Formel ist das Auftreten einer Variable oder deren Komplement. Eine Boolesche Formel befindet sich in konjunktiver Normalform oder in CNF, wenn sie eine UND-Verknüpfung von Klauseln ist. Eine Klausel ist eine Disjunktion (ODER-Verknüpfung) von einem oder mehrerer Literalen. Eine Formel befindet sich in 3-konjunktiver Normalform oder in 3-CNF, wenn jede Klausel aus exakt drei verschiedenen Literale besteht. Beispielsweise ist die Boolesche Formel (x1 ∨ ¬x1 ∨ ¬x2 ) ∧ (x3 ∨ x2 ∨ x4 ) ∧ (¬x1 ∨ ¬x3 ∨ ¬x4 ) in 3-CNF. Die erste ihrer drei Klauseln ist (x1 ∨ ¬x1 ∨ ¬x2 ). Sie besteht aus den drei Literalen x1 , ¬x1 und ¬x2 . Bei 3-CNF-SAT sollen wir entscheiden, ob eine gegebene Boolesche Formel in 3-CNF erfüllbar ist. Das folgende Theorem zeigt, dass die Existenz eines Algorithmus mit polynomieller Laufzeit, der die Erfüllbarkeit von Booleschen Formeln entscheidet, unwahrscheinlich ist, auch wenn die Formeln in dieser einfachen Normalform ausgedrückt sind. Theorem 34.10 Das Erfüllbarkeitsproblem Boolescher Formeln in 3-CNF ist NP-vollständig. Beweis: Das Argument, das wir im Beweis von Theorem 34.9 benutzt haben, um SAT ∈ NP zu zeigen, lässt sich hier ähnlich gut anwenden, um 3-CNF-SAT ∈ NP zu
34.4 NP-Vollständigkeitsbeweise
1093
y1 ∧ y2 ∨ y3
¬x2
y4
→
¬ y5
x1
∨
x2 y6 ↔ ¬x1
x4 x3
Abbildung 34.11: Der zu der Formel φ = ((x1 → x2 ) ∨ ¬((¬x1 ↔ x3 ) ∨ x4 )) ∧ ¬x2 korrespondierende Baum.
beweisen. Nach Lemma 34.8 müssen wir also nur noch zeigen, dass SAT ≤P 3-CNF-SAT gilt. Wir unterteilen den Reduktionsalgorithmus in drei Basisschritte. Jeder Schritt transformiert die Eingabeformel φ näher an die gewünschte 3-konjunktive Normalform. Der erste Schritt ähnelt dem in dem Beweis von CIRCUIT-SAT ≤P SAT im Beweis von Theorem 34.9 verwendeten Schritt. Zuerst konstruieren wir einen binären „Syntaxbaum“ für die Eingabeformel φ, dessen Blätter die Literale und dessen innere Knoten die Verknüpfungen darstellen. Abbildung 34.11 zeigt einen solchen Syntaxbaum für die Formel φ = ((x1 → x2 ) ∨ ¬((¬x1 ↔ x3 ) ∨ x4 )) ∧ ¬x2 .
(34.3)
Wir nutzen die Assoziativität, um den Ausdruck vollständig zu klammern, sodass jeder innere Knoten des resultierenden Baumes 1 oder 2 Kinder hat. Wir können uns den binären Syntaxbaum als einen Schaltkreis denken, der die Formel auswertet. In Anlehnung an die Reduktion im Beweis von Theorem 34.9 führen wir eine Variable yi für die Ausgabe jedes inneren Knotens ein. Wir schreiben dann die ursprüngliche Formel φ um, in dem wir die zu der Wurzel des Syntaxbaumes gehörige Variable mit einer Konjunktion der Teilformeln, die die Operationen der Knoten beschreiben, durch UND verknüpfen. Für die Formel (34.3) lautet der sich ergebende Ausdruck φ = y1 ∧ (y1 ∧ (y2 ∧ (y3 ∧ (y4 ∧ (y5 ∧ (y6
↔ (y2 ∧ ¬x2 )) ↔ (y3 ∨ y4 )) ↔ (x1 → x2 )) ↔ ¬y5 ) ↔ (y6 ∨ x4 )) ↔ (¬x1 ↔ x3 )) .
1094 y1 1 1 1 1 0 0 0 0
34 NP-Vollständigkeit y2 1 1 0 0 1 1 0 0
x2 1 0 1 0 1 0 1 0
(y1 ↔ (y2 ∧ ¬x2 )) 0 1 0 0 1 0 1 1
Abbildung 34.12: Die Wahrheitstabelle für die Klausel (y1 ↔ (y2 ∧ ¬x2 )).
Beachten Sie, dass die so entstandene Formel φ eine Konjunktion von Teilformeln φi ist, wobei jede von ihnen aus höchstens drei Literalen besteht. Die einzige Forderung, die wir eventuell noch nicht erfüllen, ist, dass jede dieser Teilformeln eine Klausel ist, d. h. eine Teilformel ist, die die Literale disjunktiv verknüpft. Der zweite Schritt der Reduktion wandelt jede Teilformel φi in eine konjunktive Normalform um. Wir konstruieren eine Wahrheitstabelle für φi , indem wir die Teilformel für jede mögliche Zuweisung an ihre Variablen auswerten. Jede Zeile der Wahrheitstabelle besteht aus einer möglichen Wertzuweisung an die Variablen der Teilformel und dem Wert der Teilformel unter dieser Zuweisung. Unter Verwendung der Einträge der Wahrheitstabelle, die zu 0 auswerten, konstruieren wir eine Formel in disjunktiver Normalform (oder DNF ) – ein ODER von UNDs – die äquivalent zu ¬φi ist. Wir komplementieren dann diese Formel und wandeln sie in eine CNF-Formel φi um, indem wir die Gesetze von de Morgan der Aussagenlogik (siehe Gleichung (B.2)) ¬(a ∧ b) = ¬a ∨ ¬b , ¬(a ∨ b) = ¬a ∧ ¬b anwenden, um alle Literale zu komplementieren und ODERs durch UNDs sowie UNDs durch ODERs zu ersetzen. In unserem Beispiel konvertieren wir die Teilformel φ1 = (y1 ↔ (y2 ∧ ¬x2 )) folgendermaßen in eine CNF. Die Wahrheitstabelle für φ1 ist in Abbildung 34.12 angegeben. Die zu ¬φ1 äquivalente DNF-Formel ist (y1 ∧ y2 ∧ x2 ) ∨ (y1 ∧ ¬y2 ∧ x2 ) ∨ (y1 ∧ ¬y2 ∧ ¬x2 ) ∨ (¬y1 ∧ y2 ∧ ¬x2 ) . Komplementieren wir die Formel und wenden wir die Gesetze von de Morgan an, erhalten wir die CNF-Formel φ1 =(¬y1 ∨ ¬y2 ∨ ¬x2 ) ∧ (¬y1 ∨ y2 ∨ ¬x2 ) ∧ (¬y1 ∨ y2 ∨ x2 ) ∧ (y1 ∨ ¬y2 ∨ x2 ) , die äquivalent zur ursprünglichen Klausel φ1 ist. An diesem Punkt haben wir jede Teilformel φi der Formel φ in eine CNF-Formel φi umgewandelt und somit ist φ äquivalent zur CNF-Formel φ , die aus einer Konjunktion der φi hervorgeht. Darüber hinaus besitzt jede Klausel von φ höchstens drei Literale.
34.4 NP-Vollständigkeitsbeweise
1095
Der dritte und letzte Schritt der Reduktion transformiert die Formel so, dass jede Klausel genau 3 verschiedene Literale enthält. Wir konstruieren die endgültige 3-CNFFormel φ aus den Klauseln der CNF-Formel φ . Die Formel φ enthält gegebenenfalls außerdem zwei Hilfsvariablen, die wir mit p und q bezeichnen. Anstelle der Klausel Ci aus φ nehmen wir folgende Klausel in φ auf: • Wenn Ci drei verschiedene Literale besitzt, dann nehmen wir Ci als Klausel von φ auf. • Wenn Ci zwei verschiedene Literale besitzt, d. h. wenn Ci = (l1 ∨ l2 ) mit den Literalen l1 und l2 gilt, dann nehmen wir (l1 ∨ l2 ∨ p) ∧ (l1 ∨ l2 ∨ ¬p) als Teilformel (bestehend aus 2 Klauseln) von φ auf. Die Literale p und ¬p erfüllen lediglich die syntaktische Forderung, dass jede Klausel genau 3 verschiedene Literale enthält. Unabhängig, ob p = 0 oder p = 1 gilt, ist eine der beiden Klauseln äquivalent zu (l1 ∨ l2 ) und die andere Klausel wertet zur 1 aus, die die Einheit bezüglich der UND-Operation darstellt. • Wenn Ci lediglich ein Literal l besitzt, dann nehmen wir (l ∨ p ∨ q) ∧ (l ∨ p ∨ ¬q) ∧ (l ∨ ¬p ∨ q) ∧ (l ∨ ¬p ∨ ¬q) als Teilformel von φ auf. Unabhängig von der Wertezuweisung an p und q, ist eine der 4 Klauseln äquivalent zu l und die anderen 3 Klauseln werten zu 1 aus. Wir stellen fest, dass die 3-CNF-Formel φ genau dann erfüllbar ist, wenn φ erfüllbar ist. Wie die Reduktion von CIRCUIT-SAT auf SAT, erhält auch die Konstruktion von φ aus φ im ersten Schritt die Erfüllbarkeit. Im zweiten Schritt wird eine CNF-Formel φ erzeugt, die algebraisch äquivalent zu φ ist. Im dritten Schritt wird eine 3-CNFFormel φ gebildet, die offensichtlich äquivalent zu φ ist, da jede Variablenzuweisung an p und q eine Formel erzeugt, die algebraisch äquivalent zu φ ist. Wir müssen noch zeigen, dass die Reduktion in polynomieller Zeit berechnet werden kann. Die Konstruktion von φ aus φ führt höchstens eine Variable und eine Teilformel pro Verknüpfung in φ ein. Die Konstruktion von φ aus φ ergibt für jede Teilformel aus φ höchstens 8 Teilformeln in φ , da jede Teilformel von φ höchstens 3 Variablen und die Wahrheitstabelle für jede Teilformel höchstens 23 = 8 Zeilen besitzt. Die Konstruktion von φ aus φ führt höchstens 4 Teilformeln in φ für jede Teilformel aus φ ein. Folglich ist die Größe der resultierenden Formel φ polynomiell in der Länge der ursprünglichen Formel. Jede dieser Konstruktionen kann leicht in polynomieller Zeit ausgeführt werden.
Übungen 34.4-1 Betrachten Sie die einfache (in nichtpolynomieller Zeit laufende) Reduktion im Beweis von Theorem 34.9. Geben Sie einen Schaltkreis der Größe n an, der durch diese Methode zu einer Formel transformiert wird, deren Größe exponentiell in n ist. 34.4-2 Geben Sie die 3-CNF-Formel an, die sich ergibt, wenn wir die Methode aus Theorem 34.10 auf die Formel (34.3) anwenden.
1096
34 NP-Vollständigkeit
34.4-3 Professor Jagger schlägt vor, die Ungleichung SAT ≤P 3-CNF-SAT lediglich unter Verwendung der Wahrheitstabellen-Methode im Beweis von Theorem 34.10 zu beweisen und auf die anderen Schritte zu verzichten. Der Professor schlägt also vor, die Boolesche Formel φ zu nehmen, eine Wahrheitstabelle für deren Variablen zu bilden, aus der Wahrheitstabelle eine Formel in 3-DNF abzuleiten, die zu ¬φ äquivalent ist, das Komplement der Formel zu bilden und danach die Gesetze von de Morgan anzuwenden, um eine zu φ äquivalente 3-CNF-Formel zu erzeugen. Zeigen Sie, dass diese Strategie nicht zu einer Polynomialzeitreduktion führt. 34.4-4 Zeigen Sie, dass das Entscheidungsproblem, ob eine gegebene Boolesche Formel eine Tautologie ist, co-NP-vollständig ist (siehe Übung 34.3-7). 34.4-5 Zeigen Sie, dass das Erfüllbarkeitsproblem für Boolesche Formeln in disjunktiver Normalform in polynomieller Zeit lösbar ist. 34.4-6 Setzen Sie voraus, dass irgendjemand Ihnen einen Algorithmus mit polynomieller Laufzeit gibt, der die Erfüllbarkeit von Formeln entscheidet. Beschreiben Sie, wie wir diesen Algorithmus verwenden können, um in polynomieller Zeit eine erfüllende Zuweisung zu bestimmen. 34.4-7 Sei 2-CNF-SAT die Menge der erfüllbaren Booleschen Formeln in CNF mit genau 2 Literalen pro Klausel. Zeigen Sie, dass 2-CNF-SAT ∈ P gilt. Gestalten Sie Ihren Algorithmus so effizient wie möglich. (Hinweis: Beachten Sie, dass x ∨ y äquivalent zu ¬x → y ist. Reduzieren Sie 2-CNF-SAT auf ein effizient lösbares Problem auf gerichteten Graphen.)
34.5
NP-vollständige Probleme
NP-vollständige Probleme kommen in verschiedenen Bereichen vor, beispielsweise in der Booleschen Logik, der Graphentheorie, der Arithmetik, beim Netzwerkentwurf, bei Mengen und Partitionen, beim Speichern und der Datenrettung, bei der Sequenzierung und Ablaufplanung, bei der mathematischen Programmierung, in der Algebra und der Zahlentheorie, beim Spielen und Puzzlen, bei Automaten und Formalen Sprachen, bei der Optimierung von Programmen, in der Biologie, der Chemie, der Physik und vielem mehr. In diesem Abschnitt werden wir die Reduktionsmethode anwenden, um NPVollständigkeitsbeweise für eine Vielzahl von Problemen aus der Graphentheorie und der Mengenpartitionierung anzugeben. Abbildung 34.13 umreißt die Struktur der NP-Vollständigkeitsbeweise in diesem Abschnitt und in Abschnitt 34.4. Wir beweisen jede Sprache aus der Abbildung als NPvollständig durch eine Reduktion von der Sprache, die auf sie zeigt. Die Wurzel bildet CIRCUIT-SAT, deren NP-Vollständigkeit in Theorem 34.7 bewiesen wurde.
34.5.1
Das Cliquenproblem
Eine Clique in einem ungerichteten Graphen G = (V, E) ist eine Teilmenge V ⊆ V von Knoten, von denen je zwei Knoten durch eine Kante E verbunden sind. Mit anderen
34.5 NP-vollständige Probleme
1097
CIRCUIT-SAT SAT 3-CNF-SAT CLIQUE
SUBSET-SUM
VERTEX-COVER HAM-CYCLE TSP Abbildung 34.13: Die Struktur der NP-Vollständigkeitsbeweise in den Abschnitten 34.4 und 34.5. Alle Beweise erfolgen letztendlich durch Reduktion aus der NP-Vollständigkeit von CIRCUIT-SAT.
Worten ist jede Clique ein vollständiger Teilgraph von G. Die Größe ist die Anzahl der in ihr enthaltenen Knoten. Das Cliquenproblem ist das Optimierungsproblem, eine Clique maximaler Größe in einem Graphen zu bestimmen. Als Entscheidungsproblem formuliert fragen wir einfach, ob eine Clique gegebener Größe k im Graphen existiert. Die formale Definition lautet CLIQUE = {G, k : G ist ein Graph mit einer Clique der Größe k} . Ein naiver Algorithmus für die Entscheidung, ob ein Graph G = (V, E) mit |V | Knoten eine Clique der Größe k besitzt, besteht darin, alle k-Teilmengen von V aufzuschreiben und für jede von ihnen zu prüfen, ob sie eine Clique bildet. Die Laufzeit dieses Algorithmus ist Ω(k 2 |Vk | ), was für konstantes k polynomiell ist. Im Allgemeinen könnte jedoch k in der Nähe von |V | /2 liegen. Dann läuft der Algorithmus in superpolynomieller Zeit. In der Tat ist die Existenz eines effizienten Algorithmus für das Cliquenproblem unwahrscheinlich. Theorem 34.11 Das Cliquenproblem ist NP-vollständig. Beweis: Um CLIQUE ∈ NP für einen gegebenen Graphen G = (V, E) zu beweisen, verwenden wir als Zertifikat für G die Menge V ⊆ V der Knoten in der Clique. Wir können in polynomieller Zeit überprüfen, ob V eine Clique ist, indem wir überprüfen, ob für je zwei Knoten u, v ∈ V die Kante (u, v) zu E gehört. Als nächstes beweisen wir, dass 3-CNF-SAT ≤P CLIQUE gilt, was zeigt, dass das Cliquenproblem NP-schwer ist. Sie sind möglicherweise etwas überrascht, dass wir ein
1098
34 NP-Vollständigkeit C1 = x1 ∨ ¬x2 ∨ ¬x3 x1
C2 = ¬x1 ∨ x2 ∨ x3
¬x2
¬x3
¬x1
x1
x2
x2
x3
x3
C3 = x1 ∨ x2 ∨ x3
Abbildung 34.14: Der aus der 3-CNF-Formel φ = C1 ∧ C2 ∧ C3 abgeleitete Graph G mit C1 = (x1 ∨ ¬x2 ∨ ¬x3 ), C2 = (¬x1 ∨ x2 ∨ x3 ) und C3 = (x1 ∨ x2 ∨ x3 ) bei der Reduktion von 3-CNF-SAT auf CLIQUE. Eine erfüllende Zuweisung der Formel ist x2 = 0, x3 = 1 und x1 kann entweder 0 oder 1 sein. Diese Zuweisung erfüllt C1 mit ¬x2 und C2 und C3 mit x3 , was der Clique mit den schwach schattierten Knoten entspricht.
solches Resultat beweisen können, haben doch logische Formeln auf den ersten Blick nur wenig mit Graphen zu tun. Der Reduktionsalgorithmus startet mit einer Instanz von 3-CNF-SAT. Sei φ = C1 ∧ C2 ∧ · · · ∧ Ck eine Boolesche Formel in 3-CNF mit k Klauseln. Für r = 1, 2, . . . , k besitzt jede Klausel Cr genau drei verschiedene Literale l1r , l2r und l3r . Wir werden einen Graphen G konstruieren, sodass φ genau dann erfüllbar ist, wenn G eine Clique der Größe k besitzt. Wir konstruieren den Graphen G = (V, E), indem wir für jede Klausel Cr = (l1r ∨ l2r ∨ l3r ) in φ ein Tripel von Knoten v1r , v2r und v3r in V aufnehmen. Wir setzen eine Kante zwischen zwei Knoten vir und vjs , wenn die beiden folgenden Bedingungen erfüllt sind: • vir und vjs befinden sich in verschiedenen Tripeln, d. h. r = s und • deren korrespondierenden Literale sind konsistent, d. h. lir ist kein Komplement von ljs . Wir können diesen Graphen leicht aus φ in polynomieller Zeit berechnen. Zur Illustration der Konstruktion zeigt Abbildung 34.14 den so konstruierten Graphen für φ = (x1 ∨ ¬x2 ∨ ¬x3 ) ∧ (¬x1 ∨ x2 ∨ x3 ) ∧ (x1 ∨ x2 ∨ x3 ) . Wir müssen zeigen, dass diese Transformation von φ auf G eine Reduktion darstellt. Setzen Sie zunächst voraus, dass φ eine erfüllende Zuweisung besitzt. Dann enthält jede
34.5 NP-vollständige Probleme
1099
Klausel Cr mindestens ein Literal lir , dem der Wert 1 zugewiesen wird, und jedes solche Literal entspricht einem Knoten vir . Das Herausnehmen eines solchen „wahren“ Literals aus jeder Klausel führt auf eine Menge V von k Knoten. Wir behaupten, dass V eine Clique ist. Für zwei Knoten vir , vjs ∈ V mit r = s werden die beiden zugehörigen Literale mithilfe der gegebenen erfüllenden Zuweisung an die Variablen der Formel auf 1 abgebildet. Folglich können die Literale nicht komplementär zueinander sein. Somit gehört die Kante (vir , vjs ) aufgrund der Konstruktion von G zu E. Um die Umkehrung zu beweisen, setzen Sie voraus, dass G eine Clique V der Größe k besitzt. Keine Kanten in G verbinden Knoten desselben Tripels, sodass V genau einen Knoten pro Tripel enthält. Wir können jedem Literal lir mit vir ∈ V den Wert 1 zuweisen, ohne dass wir uns Sorgen darüber machen müssen, sowohl dem Literal als auch seinem Komplement den Wert 1 zuzuweisen, weil G zwischen inkonsistenten Literalen keine Kanten enthält. Jede Klausel ist erfüllt, und somit ist φ erfüllt. (Jede Variable, die zu keinem Knoten aus der Clique korrespondiert, kann auf einen beliebigen Wert gesetzt werden.) Im Beispiel aus Abbildung 34.14 besteht eine erfüllende Zuweisung von φ aus x2 = 0 und x3 = 1. Eine zugehörige Clique der Größe k = 3 besteht aus dem zu ¬x2 gehörenden Knoten aus der ersten Klausel, x3 aus der zweiten Klausel und x3 aus der dritten Klausel. Da die Clique keine Knoten enthält, die entweder zu x1 oder ¬x1 gehören, können wir x1 in dieser erfüllenden Zuweisung entweder auf 0 oder 1 setzen. Beachten Sie, dass wir im Beweis von Theorem 34.11 eine beliebige Instanz von 3CNF-SAT auf eine Instanz von CLIQUE mit einer besonderen Struktur reduziert haben. Möglicherweise denken Sie nun, wir hätten nur gezeigt, dass CLIQUE auf Graphen NP-schwer ist, bei denen die Knoten nur in Tripeln vorkommen und bei denen es keine Kanten zwischen Knoten aus dem gleichen Tripel gibt. Tatsächlich haben wir gezeigt, dass CLIQUE schon in diesem eingeschränkten Fall NP-schwer ist. Dieser Beweis reicht aber aus, um zu zeigen, dass CLIQUE für allgemeine Graphen NP-schwer ist. Weshalb? Wenn wir einen Algorithmus mit polynomieller Laufzeit hätten, der CLIQUE auf allgemeinen Graphen löst, könnten wir CLIQUE auch auf eingeschränkten Graphen lösen. Der umgekehrte Ansatz – Instanzen von 3-CNF-SAT mit einer speziellen Struktur auf allgemeine Instanzen von CLIQUE zu reduzieren – wäre jedoch unzureichend als Beweis. Weshalb? Es kann sein, dass die von uns zur Reduktion gewählten Instanzen von 3-CNF-SAT „leicht“ sind. Somit hätten wir kein NP-schweres Problem auf CLIQUE reduziert. Beachten Sie auch, dass die Reduktion die Instanz von 3-CNF-SAT verwendet hat, aber nicht deren Lösung. Wir würden einen Fehler machen, wenn die Polynomialzeitreduktion wissen würde, ob die Formel φ erfüllbar ist, da wir nicht wissen, wie wir in polynomieller Zeit entscheiden können, ob φ erfüllbar ist.
1100
34 NP-Vollständigkeit u
v
z
u
w
y
x
v
z
w
y
(a)
x (b)
Abbildung 34.15: Das Reduzieren von CLIQUE auf VERTEX-COVER. (a) Ein ungerichteter Graph G = (V, E) mit der Clique V = {u, v, x, y}. (b) Der durch den Reduktionsalgorithmus erzeugte Graph G, der die Knotenüberdeckung V − V = {w, z} besitzt.
34.5.2
Das Knotenüberdeckungsproblem
Eine Knotenüberdeckung eines ungerichteten Graphen G = (V, E) ist eine Teilmenge V ⊆ V , sodass, wenn (u, v) ∈ E ist, entweder u ∈ V oder v ∈ V (oder beides) gilt. Das heißt, jeder Knoten „überdeckt“ seine inzidierenden Kanten, und eine Knotenüberdeckung für G ist eine Knotenmenge, die alle Kanten in E überdeckt. Die Größe einer Knotenüberdeckung ist die Anzahl der darin enthaltenen Knoten. Beispielsweise besitzt der Graph aus Abbildung 34.15(b) eine Knotenüberdeckung {w, z} der Größe 2. Das Knotenüberdeckungsproblem besteht darin, in einem gegebenen Graphen eine Knotenüberdeckung mit minimaler Größe zu finden. Formulieren wir dieses Optimierungsproblem als Entscheidungsproblem, dann wollen wir bestimmen, ob ein Graph eine Knotenüberdeckung gegebener Größe k besitzt. Als Sprache definieren wir VERTEX-COVER = {G, k : Der Graph G besitzt eine Knotenüberdeckung der Größe k.} Das folgende Theorem zeigt, dass dieses Problem NP-vollständig ist. Theorem 34.12 Das Knotenüberdeckungsproblem ist NP-vollständig. Beweis: Wir zeigen zunächst, dass VERTEX-COVER ∈ NP gilt. Setzen Sie voraus, dass wir einen Graph G = (V, E) und eine ganze Zahl k gegeben haben. Das von uns gewählte Zertifikat ist die Knotenüberdeckung V ⊆ V selbst. Der Verifikationsalgorithmus bestätigt, dass |V | = k gilt, und überprüft anschließend für jede Kante, ob u ∈ V oder v ∈ V gilt. Wir können das Zertifikat einfach in polynomieller Zeit verifizieren. Wir beweisen nun, dass das Knotenüberdeckungsproblem NP-schwer ist, indem wir CLIQUE ≤P VERTEX-COVER zeigen. Diese Reduktion basiert auf dem Begriff des „Komplements“ eines Graphen. Ist ein ungerichteter Graph G = (V, E) gegeben, dann
34.5 NP-vollständige Probleme
1101
definieren wir das Komplement von G als G = (V, E) mit E = {(u, v) : u, v ∈ V, u = v, und (u, v) ∈ E}. G ist also der Graph, der genau die Kanten enthält, die sich nicht in G befinden. Abbildung 34.15 zeigt einen Graphen und dessen Komplement und illustriert die Reduktion von CLIQUE auf VERTEX-COVER. Der Reduktionsalgorithmus verwendet als Eingabe eine Instanz G, k des Cliquenproblems. Es berechnet das Komplement G, was wir leicht in polynomieller Zeit machen können. Die Ausgabe des Reduktionsalgorithmus ist die Instanz G, |V | − k des Knotenüberdeckungsproblems. Um den Beweis zu vervollständigen, zeigen wir, dass diese Transformation tatsächlich eine Reduktion darstellt: Der Graph G besitzt genau dann eine Clique der Größe k, wenn der Graph G eine Knotenüberdeckung der Größe |V | − k besitzt. Setzen Sie voraus, dass G eine Clique V ⊆ V mit |V | = k enthält. Wir behaupten, dass V − V eine Knotenüberdeckung in G ist. Sei (u, v) eine beliebige Kante in E. Dann gilt (u, v) ∈ E, woraus folgt, dass mindestens ein Knoten von u oder v nicht in V ist, da je zwei Knoten aus V durch eine Kante aus E verbunden sind. Dies ist äquivalent dazu, dass mindestens einer der Knoten u oder v in V − V ist, was bedeutet, dass die Kante (u, v) durch V − V überdeckt ist. Da (u, v) beliebig aus E gewählt wurde, ist jede Kante von E durch einen Knoten in V − V überdeckt. Folglich bildet die Menge V − V mit Größe |V | − k eine Knotenüberdeckung für G. Um die andere Richtung zu zeigen, setzen wir voraus, dass G eine Knotenüberdeckung V ⊆ V mit |V | = |V | − k besitzt. Dann ist für alle (u, v) ∈ E mit u, v ∈ V entweder u ∈ V oder v ∈ V oder beides erfüllt. Die Umkehrung dieser Implikation lautet, dass für alle u, v ∈ V mit u ∈ V und v ∈ V die Beziehung (u, v) ∈ E gilt. Mit anderen Worten ist V − V eine Clique und sie besitzt die Größe |V | − |V | = k. Da VERTEX-COVER NP-vollständig ist, erwarten wir nicht, einen Algorithmus mit polynomieller Laufzeit zur Bestimmung einer Knotenüberdeckung mit minimaler Größe zu finden. Abschnitt 35.1 stellt jedoch einen „Approximationsalgorithmus“ mit polynomieller Laufzeit vor, der „approximative“ Lösungen zum Knotenüberdeckungsproblem liefert. Die Größe der Knotenüberdeckung, die durch diesen Algorithmus angewendet auf einen Graphen berechnet wird, ist höchstens zweimal so groß wie die kleinste Knotenüberdeckungen des Graphen. Wir sollten also nicht alle Hoffnung aufgeben, wenn ein Problem NP-vollständig ist. Wir können möglicherweise einen Approximationsalgorithmus mit polynomieller Laufzeit entwerfen, mit dem wir nahezu optimale Lösungen erhalten, auch wenn das Berechnen einer optimalen Lösung NP-vollständig ist. Kapitel 35 stellt verschiedene Approximationsalgorithmen NP-vollständiger Probleme vor.
34.5.3
Das Hamilton-Kreis-Problem
Wir kehren nun zu dem im Abschnitt 34.2 definierten Hamilton-Kreis-Problem zurück. Theorem 34.13 Das Hamilton-Kreis-Problem ist NP-vollständig.
1102
34 NP-Vollständigkeit
[u,v,1]
[v,u,1]
[u,v,2]
[v,u,2]
[u,v,3] [u,v,4]
Wuv
[v,u,3]
[v,u,5]
[u,v,6]
[v,u,6]
[v,u,1]
[u,v,1]
Wuv
[v,u,4]
[u,v,5]
(a)
[u,v,1]
[u,v,6]
[u,v,1]
Wuv
[v,u,6]
(b)
[v,u,1]
[u,v,6]
Wuv
[v,u,6]
(c)
[v,u,1]
[u,v,6]
[v,u,6]
(d)
Abbildung 34.16: Das bei der Reduktion des Knotenüberdeckungsproblems auf das HamiltonKreis-Problem verwendete Widget. Eine Kante (u, v) des Graphen G entspricht dem bei der Reduktion erzeugten Widget Wuv im Graphen G . (a) Das Widget mit individuell markierten Knoten. (b)-(d) Die schattierten Pfade sind die einzig möglichen Pfade durch das Widget, die alle Knoten enthalten, unter der Voraussetzung, dass die Verbindungen vom Widget zum Rest von G nur über die Knoten [u, v, 1], [u, v, 6], [v, u, 1] und [v, u, 6] möglich sind.
Beweis: Wir beweisen zunächst, dass HAM-CYCLE zu NP gehört. Für einen gegebenen Graphen G = (V, E) besteht unser Zertifikat aus der Folge von |V | Knoten, die den hamiltonischen Kreis bilden. Der Verifikationsalgorithmus überprüft, dass diese Folge jeden Knoten aus V genau einmal enthält und dass durch den am Ende wiederholten ersten Knoten ein Zyklus in G gebildet wird. Er überprüft also, ob zwischen jedem Paar aufeinanderfolgender Knoten sowie zwischen dem ersten und letzten Knoten eine Kante existiert. Wir können das Zertifikat in polynomieller Zeit verifizieren. Wir zeigen nun, dass VERTEX-COVER ≤P HAM-CYCLE gilt, was die NP-Vollständigkeit von HAM-CYCLE zeigt. Ist ein ungerichteter Graph G = (V, E) und eine ganze Zahl k gegeben, so konstruieren wir einen ungerichteten Graphen G = (V , E ), der genau dann einen hamiltonischen Kreis besitzt, wenn G eine Knotenüberdeckung der Größe k besitzt. Unsere Konstruktion benutzt ein Widget; ein Widget ist ein Teil eines Graphens, der bestimmte Eigenschaften nach sich zieht. Abbildung 34.16(a) zeigt das von uns verwendete Widget. Für jede Kante (u, v) ∈ E enthält der von uns konstruierte Graph G eine Kopie dieses mit Wuv bezeichneten Widgets. Wir bezeichnen jeden Knoten von Wuv mit [u, v, i] oder [v, u, i], wobei 1 ≤ i ≤ 6 gilt, sodass jedes Widget Wuv genau 12 Knoten enthält. Widget Wuv enthält außerdem die in Abbildung 34.16(a) gezeigten 14 Kanten. Zusammen mit der inneren Struktur des Widgets erzwingen wir auch die von uns gewünschten Eigenschaften, indem wir die Verbindungen zwischen dem Widget und dem Rest des konstruierten Graphen G einschränken. Insbesondere besitzen nur die Knoten [u, v, 1], [u, v, 6], [v, u, 1] und [v, u, 6] inzidente Kanten von außerhalb des Widgets Wuv . Jeder hamiltonische Kreis von G muss die Kanten von Wuv auf einem der drei, in den Abbildungen 34.16(b)–(d) gezeigten Wegen traversieren. Wenn der Zyklus über den Knoten [u, v, 1] in das Widget eintritt, muss er das Widget über den Knoten [u, v, 6] wieder verlassen. Entweder er besucht dabei alle 12 Knoten des Widgets (Abbildung 34.16(b)) oder die sechs Knoten [u, v, 1] bis [u, v, 6] (Abbildung 34.16(c)). Im letzten Fall muss der Zyklus noch einmal zum Widget zurückkehren, um die Knoten
34.5 NP-vollständige Probleme w
x
z
y
1103
(a)
s1
s2
(b) [w,x,1]
[x,w,1]
[x,y,1]
Wwx
[w,x,6]
[y,x,1]
[w,y,1]
Wxy
[x,w,6]
[x,y,6]
[y,w,1]
[w,z,1]
Wwy
[y,x,6]
[w,y,6]
[z,w,1]
Wwz
[y,w,6]
[w,z,6]
[z,w,6]
Abbildung 34.17: Reduzieren einer Instanz des Knotenüberdeckungsproblems auf eine Instanz des Hamilton-Kreis-Problems. (a) Ein ungerichteter Graph G mit einer Knotenüberdeckung der Größe 2, die aus den schwach schattierten Knoten w und y besteht. (b) Der durch die Reduktion erzeugte ungerichtete Graph G zusammen mit dem schattierten hamiltonischen Kreis, der der Knotenüberdeckung entspricht. Die Knotenüberdeckung {w, y} entspricht den im hamiltonischen Kreis auftretenden Kanten (s1 , [w, x, 1]) und (s2 , [y, x, 1]).
[v, u, 1] bis [v, u, 6] zu besuchen. Analog muss der Zyklus das Widget durch den Knoten [v, u, 6] verlassen, wenn er am Knoten [v, u, 1] eintritt. Entweder er besucht alle 12 Knoten des Widgets (Abbildung 34.16(d)) oder die sechs Knoten von [v, u, 1] bis [v, u, 6] (Abbildung 34.16(c)). Es sind keine anderen Pfade durch das Widget möglich, die alle 12 Knoten passieren. Insbesondere ist es unmöglich, zwei knotendisjunkte Pfade so zu konstruieren, von denen einer [u, v, 1] und [v, u, 6] verbindet und der andere die Knoten [v, u, 1] und [u, v, 6], dass die Vereinigung der beiden Pfade alle Knoten des Widgets enthält. Die einzigen nicht zu Widgets gehörenden Knoten von V sind die Auswahlknoten s1 , s2 , . . . , sk . Wir verwenden Kanten, die inzident mit den Auswahlknoten von G sind, um die k Knoten der Überdeckung von G auszuwählen. Zusätzlich zu den Kanten in den Widgets enthält E zwei andere Typen von Kanten, die in Abbildung 34.17 gezeigt werden. Zunächst fügen wir für jeden Knoten u ∈ V Kanten hinzu, um Widgetpaare aneinanderzufügen, um so einen Pfad bilden zu können, der alle Widgets enthält, die den mit u inzidenten Kanten in G entsprechen. Wir ordnen die mit einem Knoten u ∈ V benachbarten Knoten willkürlich in der Reihenfolge
1104
34 NP-Vollständigkeit
u(1) , u(2) , . . . , u(grad(u)) , wobei grad(u) die Anzahl der mit dem Knoten u benachbarten Knoten ist. Wir erzeugen in G einen Pfad durch alle Widgets, die den mit u inzidenten Kanten entsprechen, indem wir zu E die Kanten {([u, u(i) , 6], [u, u(i+1) , 1]) : 1 ≤ i ≤ grad(u) − 1} hinzufügen. In Abbildung 34.17 ordnen wir beispielsweise die mit w benachbarten Knoten mit x, y, z. Folglich enthält der Graph G in Teil (b) der Abbildung die Kanten ([w, x, 6], [w, y, 1]) und ([w, y, 6], [w, z, 1]). Für jeden Knoten u ∈ V bilden diese Kanten in G einen Pfad mit allen Widgets, die den mit u in G inzidenten Kanten entsprechen. Die der Einführung dieser Kanten zugrundeliegende Intuition ist die Folgende. Wenn wir einen Knoten u ∈ V für die Knotenüberdeckung von G auswählen, dann können wir einen Pfad von [u, u(1) , 1] nach [u, u(grad(u)) , 6] in G konstruieren, der alle Widgets „überdeckt“, die den mit u inzidenten Kanten entsprechen. Das heißt, für jedes dieser Widgets, zum Beispiel Wu,u(i) , enthält der Pfad entweder alle 12 Knoten (wenn u in der Knotenüberdeckung ist, nicht aber u(i) ) oder lediglich die sechs Knoten [u, u(i) , 1], [u, u(i) , 2], . . . , [u, u(i) , 6] (wenn sowohl u als auch u(i) in der Knotenüberdeckung enthalten sind). Der letzte Kantentyp in E verbindet den ersten Knoten [u, u(1) , 1] und den letzten Knoten [u, u(grad(u)) , 6] von jedem dieser Pfade mit jedem der Auswahlknoten. Das heißt, wir fügen die Kanten {(sj , [u, u(1) , 1]) : u ∈ V und 1 ≤ j ≤ k} ∪ {(sj , [u, u(grad(u)) , 6]) : u ∈ V und 1 ≤ j ≤ k} ein. Als nächstes zeigen wir, dass die Größe von G polynomiell in der Größe von G ist und wir folglich G in einer in der Größe von G polynomiellen Zeit konstruieren können. Die Knoten von G sind die der Widgets plus die Auswahlknoten. Mit 12 Knoten pro Widget und k ≤ |V | Auswahlknoten, haben wir insgesamt |V | = 12 |E| + k ≤ 12 |E| + |V | Knoten. Die Kantenmenge von G besteht aus den Kanten der Widgets, den zwischen den Widgets verlaufenden Kanten und denjenigen, die die Auswahlknoten mit den Widgets verbinden. Jedes Widget enthält 14 Kanten, also zusammen über alle Widgets 14 |E|. Für jeden Knoten u ∈ V besitzt der Graph G grad(u) − 1 viele Kanten zwischen den Widgets, sodass, über alle Knoten in V summiert,
(grad(u) − 1) = 2 |E| − |V |
u∈V
Kanten zwischen den Widgets verlaufen. Schließlich enthält G noch zwei Kanten für jedes aus einem Auswahlknoten und einem Knoten von V bestehendes Paar, also zu-
34.5 NP-vollständige Probleme
1105
sammen 2k |V | solcher Kanten. Die Gesamtanzahl der Kanten von G ist deshalb |E | = (14 |E|) + (2 |E| − |V |) + (2k |V |) = 16 |E| + (2k − 1) |V | ≤ 16 |E| + (2 |V | − 1) |V | . Nun zeigen wir, dass die Transformation von Graph G in G eine Reduktion darstellt. Wir müssen also zeigen, dass G genau dann eine Knotenüberdeckung der Größe k besitzt, wenn G einen hamiltonischen Kreis besitzt. Setzen Sie voraus, dass G = (V, E) eine Knotenüberdeckung V ∗ ⊆ V der Größe k besitzt. Sei V ∗ = {u1 , u2 , . . . , uk }. Wie Abbildung 34.17 zeigt, bilden wir in G einen hamiltonischen Kreis, indem wir die folgenden Kanten10 für jeden Knoten uj ∈ V ∗ (i) (i+1) nehmen. Wir wählen die Kanten {([uj , uj , 6], [uj , uj , 1]) : 1 ≤ i ≤ grad(uj ) − 1}, die alle Widgets verbinden, die den mit uj inzidenten Kanten entsprechen. Wir nehmen entsprechend Abbildung 34.16(b)–(d) auch die Kanten innerhalb dieser Widgets, abhängig davon, ob die Kante von einem oder zwei Knoten in V ∗ überdeckt wird. Der hamiltonische Kreis umfasst zudem die Kanten (1)
{(sj , [uj , uj , 1]) : 1 ≤ j ≤ k} (degree(uj ))
∪ {(sj+1 , [uj , uj ∪
, 6]) : 1 ≤ j ≤ k − 1}
(degree(uk )) , 6])} {(s1 , [uk , uk
.
Anhand von Abbildung 34.17 können Sie verifizieren, dass diese Kanten einen Zyklus bilden. Der Zyklus beginnt bei s1 , besucht alle Widgets, die den mit u1 inzidenten Kanten entsprechen, besucht dann s2 , und danach alle Widgets, die den mit u2 inzidenten Kanten entsprechen, usw., bis er zu s1 zurückkehrt. Der Zyklus besucht jedes Widget entweder einmal oder zweimal, abhängig davon, ob ein oder zwei Knoten von V ∗ die entsprechende Kante überdecken. Weil V ∗ eine Knotenüberdeckung von G ist, ist jede Kante von E mit einem Knoten von V ∗ inzident, und folglich besucht der Zyklus jeden Knoten in jedem Widget von G . Weil der Zyklus auch jeden Auswahlknoten besucht, ist er hamiltonisch. Um die Umkehrung zu beweisen, setzen Sie voraus, dass G = (V , E ) einen hamiltonischen Kreis C ⊆ E besitzt. Wir behaupten, dass die Menge V ∗ = {u ∈ V : (sj , [u, u(1) , 1]) ∈ C für ein 1 ≤ j ≤ k}
(34.4)
eine Knotenüberdeckung von G ist. Warum? Zerlegen Sie C in maximale Pfade, die an einem Auswahlknoten si beginnen, eine Kante (si , [u, u(1) , 1]) für ein u ∈ V durchlaufen und an einem Auswahlknoten sj enden, ohne einen anderen Auswahlknoten zu passieren. Lassen Sie uns jeden dieser Pfade als „Überdeckungspfad“ bezeichnen. Aus 10 Formal definieren wir einen Zyklus über Knoten und nicht über Kanten (siehe Abschnitt B.4). Zum besseren Verständnis halten wir uns hier aber nicht an diese formale Definition und betrachten den hamiltonischen Kreis als ob er über Kanten definiert wäre.
1106
34 NP-Vollständigkeit 4
u
v
1 3
2 1 x
5
w
Abbildung 34.18: Eine Instanz des Problems des Handelsreisenden. Schattierte Kanten kennzeichnen die Tour mit den minimalen Kosten 7.
der Konstruktion von G folgt, dass jeder Überdeckungspfad an einem Auswahlknoten si beginnen, die Kante (si , [u, u(1) , 1]) für einen Knoten u ∈ V durchlaufen, alle Widgets passieren, die den mit dem Knoten u inzidenten Kanten aus E entsprechen, und anschließend an einem Auswahlknoten sj enden muss. Wir bezeichnen diesen Überdeckungspfad mit pu und nach Gleichung (34.4) nehmen wir u in V ∗ auf. Jedes von pu besuchte Widget muss Wuv oder Wvu für ein v ∈ V sein. Die Knoten jedes von pu besuchten Widgets werden entweder von einem oder zwei Überdeckungspfaden besucht. Wenn sie von einem Überdeckungspfad besucht werden, dann ist die Kante (u, v) ∈ E in G durch den Knoten u überdeckt. Wenn zwei Überdeckungspfade das Widget besuchen, dann muss der andere Überdeckungspfad pv sein, woraus v ∈ V ∗ folgt; die Kante (u, v) ∈ E wird sowohl von u als auch von v überdeckt. Da jeder Knoten in jedem Widget von einem Überdeckungspfad besucht wird, sehen wir, dass jede Kante von E durch einen Knoten in V ∗ überdeckt wird.
34.5.4
Das Problem des Handelsreisenden
Beim Handelsreisenden-Problem, das eng mit dem Hamilton-Kreis-Problem verwandt ist, muss ein Handelsreisender n Städte besuchen. Modellieren wir das Problem durch einen vollständigen Graphen mit n Knoten, dann können wir sagen, dass der Handelsreisende eine Tour oder einen hamiltonischen Kreis durchläuft, der jede Stadt genau einmal besucht; er kommt in der Stadt an, von der er aus losgefahren ist. Der Handelsreisende hat nichtnegative ganze Kosten c(i, j), um von Stadt i zu Stadt j zu fahren. Er möchte seine Tour so durchführen, dass die Gesamtkosten, d. h. die Summe der individuellen Kosten entlang der Kanten der Tour, minimal sind. In der in Abbildung 34.18 gezeigten Instanz beispielsweise ist die billigste Tour die Tour, die entlang u, w, v, x, u verläuft; sie hat die Kosten 7. Die formale Sprache des zugehörigen Entscheidungsproblems ist TSP = {G, c, k : G = (V, E) ist ein vollständiger Graph, c ist eine Funktion von V × V → N, k ∈ N und G besitzt eine Tour des Handelsreisenden mit Kosten kleiner gleich k} . Das folgende Theorem zeigt, dass die Existenz eines schnellen Algorithmus für das Problem des Handelsreisenden unwahrscheinlich ist.
34.5 NP-vollständige Probleme
1107
Theorem 34.14 Das Problem des Handelsreisenden ist NP-vollständig. Beweis: Wir zeigen zunächst, dass TSP zu NP gehört. Ist eine Instanz des Problems gegeben, so benutzen wir die Folge der n Knoten der Tour als Zertifikat. Der Verifikationsalgorithmus überprüft, ob diese Folge jeden Knoten genau einmal enthält, summiert die Kantenkosten und überprüft, ob die Summe maximal k ist. Dieser Prozess kann sicherlich in polynomieller Zeit ablaufen. Um zu beweisen, dass TSP NP-schwer ist, zeigen wir, dass HAM-CYCLE ≤P TSP gilt. Sei G = (V, E) eine Instanz von HAM-CYCLE. Wir konstruieren eine Instanz von TSP folgendermaßen. Wir bilden den vollständigen Graphen G = (V, E ), wobei E = {(i, j) : i, j ∈ V und i = j} ist. Wir definieren die Kostenfunktion c durch 0 falls (i, j) ∈ E , c(i, j) = 1 falls (i, j) ∈ E . (Da es sich bei G um einen ungerichteten Graphen handelt, besitzt dieser keine Schlingen, und folglich gilt c(v, v) = 1 für alle Knoten v ∈ V .) Die Instanz von TSP ist dann G , c, 0, die wir leicht in polynomieller Zeit konstruieren können. Wir zeigen nun, dass der Graph G genau dann einen hamiltonischen Kreis besitzt, wenn der Graph G eine Tour mit Kosten kleiner gleich 0 enthält. Setzen Sie voraus, dass der Graph G einen hamiltonischen Kreis h besitzt. Jede Kante von h gehört zu E und besitzt folglich in G die Kosten 0. Somit ist h eine Tour in G mit den Kosten 0. Um die Umkehrung zu beweisen, setzen Sie voraus, dass der Graph G eine Tour h mit Kosten kleiner gleich 0 besitzt. Da die Kosten der Kanten von E die Werte 0 und 1 haben, betragen die Kosten der Tour h exakt 0 und jede Kante auf der Tour muss die Kosten 0 besitzen. Deshalb enthält h nur Kanten von E. Wir schlussfolgern, dass h ein hamiltonischer Zyklus des Graphen G ist.
34.5.5
Das Teilsummenproblem
Wir betrachten als nächstes ein arithmetisches NP-vollständiges Problem. Beim Teilsummenproblem haben wir eine endliche Menge S von positiven ganzen Zahlen und ein Zielwert t > 0 gegeben. Wir fragen, ob eine Teilmenge S ⊆ S existiert, deren Elemente sich zu t summieren. Wenn beispielsweise S = {1, 2, 7, 14, 49, 98, 343, 686, 2409, 2793, 16808, 17206, 117705, 117993} und t = 138457 ist, dann ist die Teilmenge S = {1, 2, 7, 98, 343, 686, 2409, 17206, 117705} eine Lösung. Wie üblich definieren wir das Problem durch eine Sprache: SUBSET-SUM = {S, t : es existiert eine Teilmenge S ⊆ S, sodass t = s∈S s gilt} . Wie bei jedem arithmetischen Problem ist es wichtig, sich daran zu erinnern, dass unsere Standardkodierung voraussetzt, dass die ganzzahligen Eingaben binär kodiert sind. Mit
1108
34 NP-Vollständigkeit
dieser Voraussetzung im Hinterkopf können wir zeigen, dass die Existenz eines schnellen Algorithmus für das Teilsummenproblem unwahrscheinlich ist. Theorem 34.15 Das Teilsummenproblem ist NP-vollständig. Beweis: Um zu beweisen, dass SUBSET-SUM in NP ist, nehmen wir für eine Instanz S, t die Teilmenge S als Zertifikat. Ein Verifikationsalgorithmus kann in polynomieller Zeit überprüfen, ob t = s∈S s gilt. Wir zeigen nun, dass 3-CNF-SAT ≤P SUBSET-SUM gilt. Ist eine 3-CNF-Formel φ mit den Variablen x1 , x2 , . . . , xn und den Klauseln C1 , C2 , . . . , Ck mit jeweils genau drei verschiedenen Literalen gegeben, so konstruiert der Reduktionsalgorithmus eine Instanz S, t des Teilsummenproblems, sodass φ genau dann erfüllbar ist, wenn es eine Teilmenge von S gibt, deren Summe genau t ergibt. Ohne Beschränkung der Allgemeinheit machen wir zwei vereinfachende Voraussetzungen bezüglich der Formel φ. Erstens enthält keine Klausel sowohl eine Variable als auch deren Komplement, da eine solche Klausel automatisch durch eine beliebige Wertzuweisung an die Variablen erfüllt ist. Zweitens erscheint jede Variable in mindestens einer Klausel, da es anderenfalls keinen Unterschied macht, welchen Wert wir dieser Variablen zuweisen. Die Reduktion erzeugt in der Menge S zwei Zahlen für jede Variable xi und zwei Zahlen für jede Klausel Cj . Wir werden die Zahlen zur Basis 10 bilden, wobei jede Zahl n + k Stellen enthält und jede Stelle entweder zu einer Variablen oder zu einer Klausel korrespondiert. Die Basis 10 (und wie wir sehen auch andere Basen) besitzt die Eigenschaft, die wir benötigen, um Überträge von niedrigeren Stellen auf höhere Stellen zu verhindern. Wie Abbildung 34.19 zeigt, konstruieren wir die Menge S und den Zielwert t folgendermaßen. Wir markieren jede Position entweder mit einer Variablen oder einer Klausel. Die k niederwertigen Stellen korrespondieren zu den Klauseln und die n höherwertigen Stellen zu den Variablen. • Der Zielwert t besitzt eine 1 an jeder mit einer Variablen gekennzeichneten Position und eine 4 an jeder durch eine Klausel gekennzeichneten Position. • Für jede Variable xi enthält S zwei ganze Zahlen vi und vi . Jede hat eine 1 an der mit xi gekennzeichneten Stelle und Nullen an den zu den anderen Variablen korrespondierenden Stellen. Wenn das Literal xi in einer Klausel Cj erscheint, dann enthält die mit Cj gekennzeichnete Stelle von vi eine 1. Wenn das Literal ¬xi in der Klausel Cj auftritt, dann enthält die mit Cj gekennzeichnete Stelle von vi eine 1. Alle anderen zu Klauseln korrespondierende Stellen in vi und vi sind 0. Alle Werte vi und vi der Menge S sind voneinander verschieden. Weshalb? Für l = i kann kein Wert vl oder vl an den n höherwertigen Stellen gleich vi oder vi sein. Darüber hinaus kann aufgrund unserer obigen vereinfachenden Voraussetzungen kein vi an den k niederwertigen Stellen gleich vi sein. Wenn vi und vi
34.5 NP-vollständige Probleme
1109
x1
x2
x3
C1
C2
C3
C4
= = = = = =
1 1 0 0 0 0
0 0 1 1 0 0
0 0 0 0 1 1
1 0 0 1 0 1
0 1 0 1 0 1
0 1 0 1 1 0
1 0 1 0 1 0
s4
= = = = = = = =
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
1 2 0 0 0 0 0 0
0 0 1 2 0 0 0 0
0 0 0 0 1 2 0 0
0 0 0 0 0 0 1 2
t
=
1
1
1
4
4
4
4
v1 v1 v2 v2 v3 v3 s1 s1 s2 s2 s3 s3 s4
Abbildung 34.19: Die Reduktion von 3-CNF-SAT auf SUBSET-SUM. Die Formel in 3-CNF ist φ = C1 ∧ C2 ∧ C3 ∧ C4 , wobei C1 = (x1 ∨ ¬x2 ∨ ¬x3 ), C2 = (¬x1 ∨ ¬x2 ∨ ¬x3 ), C3 = (¬x1 ∨ ¬x2 ∨ x3 ) und C4 = (x1 ∨ x2 ∨ x3 ) ist. Eine erfüllende Zuweisung von φ ist x1 = 0, x2 = 0, x3 = 1. Die von der Reduktion erzeugte Menge S besteht aus den zur Basis 10 dargestellten Zahlen; von oben nach unten gelesen ist S = {1001001, 1000110, 100001, 101110, 10011, 11100, 1000, 2000, 100, 200, 10, 20, 1, 2}. Der Zielwert t ist 1114444. Die Teilmenge S ⊆ S ist schwach schattiert und enthält v1 , v2 und v3 , was der erfüllenden Zuweisung entspricht. Sie enthält auch die Schlupfvariablen s1 , s1 , s2 , s3 , s4 und s4 , um in den mit C1 bis C4 gekennzeichneten Stellen den Zielwert 4 zu erreichen.
gleich wären, dann müssten xi und ¬xi in genau der gleichen Menge von Klauseln auftreten. Wir haben aber vorausgesetzt, dass keine Klausel sowohl xi als auch ¬xi enthält, und dass xi oder ¬xi in wenigstens einer Klausel vorkommt. Somit muss eine Klausel Cj existieren, für die sich vi und vi unterscheiden. • Für jede Klausel Cj enthält S zwei ganze Zahlen sj und sj . Sowohl sj als auch sj haben Nullen an allen Stellen, mit Ausnahme der zu Cj korrespondierenden Stelle. sj hat eine 1 an der Stelle Cj und sj eine 2 an dieser Stelle. Diese ganzen Zahlen sind „Schlupfvariablen“, die wir benutzen, damit sich die zu Klauseln korrespondierenden Stellen zum Zielwert 4 addieren können. Ein einfache Kontrolle der Abbildung 34.19 zeigt, dass alle Werte sj und sj aus S eindeutig in der Menge S sind. Beachten Sie, dass an jeder Stelle die Summe über die Zahlen genommen höchstens 6 ist, was an den zu den Klauseln Cj korrespondierenden Stellen auftritt (drei Einsen von den Werten vi und vi , da Cj genau 3 Literale enthält, plus 1 und 2 von den Werten
1110
34 NP-Vollständigkeit
sj und sj ). Interpretieren wir diese Zahlen zur Basis 10, kann es keine Überträge von niedrigeren zu höheren Stellen geben.11 Wir können die Reduktion in polynomieller Zeit ausführen. Die Menge S enthält 2n+2k Werte, von denen jede n+ k Stellen besitzt, und die zum Erzeugen jeder Ziffer benötigte Zeit ist polynomiell in n + k. Der Zielwert t besitzt n + k Stellen und die Reduktion erzeugt jede in konstanter Zeit. Wir zeigen nun, dass die 3-CNF-Formel φ genau dann erfüllbar ist, wenn eine Teilmenge S ⊆ S existiert, deren Summe gleich t ist. Zuerst setzen wir voraus, dass φ eine erfüllende Zuweisung besitzt. Wenn wir in dieser Zuweisung xi = 1 (i = 1, 2, . . . , n) setzen, dann nehmen wir vi in S auf. Anderenfalls nehmen wir vi auf. Mit anderen Worten, wir nehmen in S genau die Werte vi und vi auf, die zu den Literalen mit dem Wert 1 in der erfüllenden Zuweisung korrespondieren. Haben wir für jedes i entweder vi oder vi , aber nicht beide, in die Teilmenge aufgenommen, dann sehen wir, da die zu den Variablen korrespondierenden Ziffern in den Werten sj und sj gleich 0 sind, dass die Summe der Werte von S an jeder mit einer Variablen korrespondierenden Stelle mit dem Zielwert t an dieser Stelle, der gleich 1 ist, übereinstimmt. Da jede Klausel erfüllt ist, gibt es in jeder Klausel mindestens ein Literal mit dem Wert 1. Deshalb hat jede zu einer Klausel korrespondierende Stelle mindestens eine 1, die durch einen Wert vi oder vi aus S zu der entsprechenden Summe beiträgt. Tatsächlich können 1, 2 oder 3 Literale in jeder Klausel 1 sein, und folglich hat jede zu einer Klausel korrespondiere Stelle eine Summe von 1, 2 oder 3 alleine durch die Werte vi und vi aus S . In Abbildung 34.19 haben die Literale ¬x1 , ¬x2 und x3 in einer erfüllenden Zuweisung beispielsweise den Wert 1. Jede der Klauseln C1 und C4 enthält genau eines dieser Literale, und folglich tragen v1 , v2 und v3 zusammen 1 zu der Summe der Stellen für C1 und C4 bei. Die Klausel C2 enthält 2 dieser Literale, und v1 , v2 und v3 tragen 2 zur Summe an der Stelle für C2 bei. Die Klausel C3 enthält alle 3 Literale, und v1 , v2 und v3 tragen 3 zur Summe an der Stelle für C3 bei. Wir erhalten den Zielwert von 4 an jeder zu einer Klausel Cj korrespondierenden Stelle, indem wir in S die geeignete, nichtleere Teilmenge von Schlupfvariablen {sj , sj } aufnehmen. In Abbildung 34.19 enthält S s1 , s1 , s2 , s3 , s4 und s4 . Da nun alle Stellen der Summe mit dem Zielwert übereinstimmen, und keine Überträge vorkommen können, summieren sich die Werte von S zum Zielwert t. Setzen Sie nun voraus, dass es eine Teilmenge S ⊆ S gibt, die sich zu t summiert. Die Teilmenge S muss genau eines der vi und vi für jedes i = 1, 2, . . . , n enthalten, da sich anderenfalls die zu den Variablen korrespondierenden Stellen nicht jeweils zu 1 summieren würden. Wenn vi ∈ S ist, setzen wir xi = 1. Anderenfalls ist vi ∈ S und wir setzen xi = 0. Wir behaupten, dass jede Klausel Cj für j = 1, 2, . . . , k durch diese Zuweisung erfüllt ist. Um diese Behauptung zu beweisen, stellen wir fest, dass zum Erhalt der Summe 4 an der zu der Klausel Cj korrespondierenden Stelle die Teilmenge S mindestens einen Wert vi oder vi enthalten muss, der eine 1 an der zu Cj korrespondierenden Stelle besitzt, da sich die Beiträge der Schlupfvariablen sj und sj insgesamt zu höchstens 3 summieren. Wenn S ein vi enthält, das eine 1 an Cj ’s Position hat, dann erscheint das Literal xi in der Klausel Cj . Da wir xi = 1 im Falle vi ∈ S gesetzt 11 Tatsächlich würde jede Basis b mit b ≥ 7 ausreichen. Die Instanz zu Beginn dieses Teilabschnittes mit der Menge S und dem Zielwert t aus Abbildung 34.19 wird zur Basis 7 interpretiert, wobei S in sortierter Reihenfolge angegeben ist.
34.5 NP-vollständige Probleme
1111
haben, ist Klausel Cj erfüllt. Wenn S ein vi enthält, das eine 1 an Cj ’s Position hat, dann erscheint das Literal ¬xi in Cj . Da wir xi = 0 im Falle vi ∈ S gesetzt haben, ist die Klausel Cj wieder erfüllt. Folglich sind alle Klauseln von φ erfüllt, was den Beweis vervollständigt.
Übungen 34.5-1 Das Teilgraph-Isomorphie-Problem erhält als Eingabe zwei ungerichtete Graphen G1 und G2 und fragt, ob G1 zu einem Teilgraph von G2 isomorph ist. Zeigen Sie, dass das Teilgraph-Isomorphie-Problem NP-vollständig ist. 34.5-2 Gegeben seien eine ganzzahlige m×n-Matrix A und ein ganzzahliger m-Vektor b. Das Problem der ganzzahligen 0-1-Programmierung fragt, ob ein ganzzahliger n-Vektor x mit Ax ≤ b existiert, dessen Elemente in der Menge {0, 1} liegen. Beweisen Sie, dass das Problem der ganzzahligen 0-1-Programmierung NP-vollständig ist. (Hinweis: Reduzieren Sie von 3-CNF-SAT.) 34.5-3 Das Problem der ganzzahligen linearen Programmierung ähnelt dem in Übung 34.5-2 gegebenen Problem, außer dass die Werte des Vektors x beliebige ganze Zahlen sein können. Setzen Sie voraus, dass das Problem der ganzzahligen 0-1-Programmierung NP-schwer ist. Zeigen Sie unter dieser Voraussetzung, dass das Problem der ganzzahligen linearen Programmierung NP-vollständig ist. 34.5-4 Zeigen Sie, wie wir das Teilsummenproblem in polynomieller Zeit lösen können, wenn der Zielwert t unär dargestellt ist. 34.5-5 Das Mengenpartitionsproblem erhält als Eingabe eine Menge S von Zahlen. Die Frage ist, ob die Zahlen so in zwei Mengen A und A = S − A zerlegt werden können, dass x∈A x = x∈A x gilt. Zeigen Sie, dass das Mengenpartitionsproblem NP-vollständig ist. 34.5-6 Zeigen Sie, dass das Hamilton-Pfad-Problem NP-vollständig ist. 34.5-7 Das Problem des längsten einfachen Zyklus ist das Problem, in einem Graphen einen einfachen Zyklus (d. h. einen Zyklus, der jeden Knoten des Graphen höchstens einmal enthält) maximaler Länge zu bestimmen. Formulieren Sie ein verwandtes Entscheidungsproblem und zeigen Sie, dass das Entscheidungsproblem NP-vollständig ist. 34.5-8 Beim Halb-3-CNF-Erfüllbarkeitsproblem ist eine 3-CNF-Formel φ mit n Variablen und m Klauseln gegeben, wobei m geradzahlig ist. Wir wollen bestimmen, ob eine Wertzuweisung an die Variablen von φ existiert, sodass genau die Hälfte der Klauseln zu 0 und genau die Hälfte der Klauseln zu 1 auswerten. Beweisen Sie, dass das Halb-3-CNF-Erfüllbarkeitsproblem NP-vollständig ist.
1112
34 NP-Vollständigkeit
Problemstellungen 34-1 Unabhängige Mengen Eine unabhängige Menge eines Graphen G = (V, E) ist eine Teilmenge V ⊆ V von Knoten, sodass jede Kante von E höchstens mit einem Knoten aus V inzident ist. Das Problem der Bestimmung einer maximalen unabhängigen Menge besteht darin, eine unabhängige Menge maximaler Größe in G zu finden. a. Formulieren Sie ein zu dem Problem der Bestimmung einer maximalen unabhängigen Menge verwandtes Entscheidungsproblem und beweisen Sie, dass es NP-vollständig ist. (Hinweis: Reduzieren Sie das Cliquenproblem auf das Entscheidungsproblem.) b. Setzen Sie voraus, dass es eine „Black-Box“-Unterroutine gibt, die das von Ihnen in Teil (a) definierte Entscheidungsproblem löst. Geben Sie einen Algorithmus an, der eine unabhängige Menge maximaler Größe bestimmt. Die Laufzeit Ihres Algorithmus sollte polynomiell in |V | und |E| sein, wobei Abfragen der Black-Box als ein Schritt gezählt werden. Obwohl das zu dem Problem der Bestimmung einer maximalen unabhängigen Menge verwandte Entscheidungsproblem NP-vollständig ist, sind bestimmte Spezialfälle in polynomieller Zeit lösbar. c. Geben Sie einen effizienten Algorithmus an, der das Problem der Bestimmung einer maximalen unabhängigen Menge löst, wenn jeder Knoten von G den Grad 2 besitzt. Analysieren Sie die Laufzeit und beweisen Sie, dass Ihr Algorithmus korrekt arbeitet. d. Geben Sie einen effizienten Algorithmus an, der das Problem der Bestimmung einer maximalen unabhängigen Menge löst, wenn G bipartit ist. Analysieren Sie die Laufzeit und beweisen Sie, dass Ihr Algorithmus korrekt arbeitet. (Hinweis: Verwenden Sie die Ergebnisse aus Abschnitt 26.3.) 34-2 Bonnie und Clyde Bonnie und Clyde haben gerade eine Bank ausgeraubt. Sie haben einen Sack voll Geld und wollen ihn aufteilen. Geben Sie für jedes der folgenden Szenarien entweder einen Algorithmus mit polynomieller Laufzeit an oder beweisen Sie, dass das Problem NP-vollständig ist. Die Eingabe besteht in jedem Fall aus einer Liste von n sich im Sack befindenden Gegenständen zusammen mit deren Wert. a. Der Sack enthält n Münzen, aber nur zwei unterschiedliche Nennwerte: einige Münzen sind x Dollar wert und die anderen y Dollar. Bonnie und Clyde wollen das Geld genau gleich aufteilen. b. Der Sack enthält n Münzen, mit einer beliebigen Anzahl verschiedener Nennwerte, wobei aber jeder Nennwert eine nichtnegative ganzzahlige Potenz von 2 ist, d. h. die möglichen Nennwerte sind 1 Dollar, 2 Dollar, 4 Dollar usw. Bonnie und Clyde wollen das Geld genau gleich aufteilen. c. Der Sack enthält n Schecks, die überraschenderweise alle auf „Bonnie oder Clyde“ ausgestellt sind. Sie wollen die Schecks so aufteilen, dass jeder genau den gleichen Geldbetrag bekommt.
Problemstellungen zu Kapitel 34
1113
d. Der Sack enthält wie in Teil (c) n Schecks, aber diesmal sind Bonnie und Clyde bereit, eine Aufteilung zu akzeptieren, bei der die Differenz nicht größer als 100 Dollar ist. 34-3 Färbung von Graphen Kartographen versuchen, so wenig wie möglich Farben zu benutzen, wenn sie Länder auf einer Karte einzeichnen, wobei sie aber darauf achten, dass keine zwei Länder, die eine gemeinsame Grenze haben, in der gleichen Farbe gezeichnet werden. Wir können dieses Problem durch einen ungerichteten Graphen G = (V, E) modellieren, in dem jeder Knoten ein Land darstellt und Knoten, dessen zugehörige Länder eine gemeinsame Grenze haben, adjazent sind. Dann ist eine k-Färbung eine Funktion c : V → {1, 2, . . . , k}, sodass c(u) = c(v) für jede Kante (u, v) ∈ E gilt. Die Zahlen 1, 2, . . . , k stellen also die k Farben dar und benachbarte Knoten müssen verschiedene Farben haben. Das Färbungsproblem für Graphen besteht darin, die minimale Anzahl von Farben zu bestimmen, die nötig sind, um einen Graphen zu färben. a. Geben Sie einen effizienten Algorithmus an, der eine 2-Färbung eines Graphen berechnet, falls eine solche existiert. b. Formulieren Sie das Färbungsproblem eines Graphen als ein Entscheidungsproblem. Zeigen Sie, dass Ihr Entscheidungsproblem genau dann in polynomieller Zeit lösbar ist, wenn das Färbungsproblem eines Graphen in polynomieller Zeit lösbar ist. c. Sei die Sprache 3-COLOR die Menge von Graphen, die 3-färbbar sind. Zeigen Sie, dass Ihr Entscheidungsproblem aus Teil (b) NP-vollständig ist, wenn 3-COLOR NP-vollständig ist. Um die NP-Vollständigkeit von 3-COLOR zu beweisen, benutzen wir eine Reduktion von 3-CNF-SAT. Ist eine Formel φ von m Klauseln über n Variablen x1 , x2 , . . . , xn gegeben, so konstruieren wir einen Graphen G = (V, E) folgendermaßen. Die Menge V besteht aus einem Knoten pro Variable, einem Knoten für das Komplement einer jeden Variable, fünf Knoten für jede Klausel und drei speziellen Knoten namens wahr, falsch und rot. Die Kanten des Graphen sind in zwei Klassen unterteilt: von den Klauseln unabhängige „Literalkanten“ und von Klauseln abhängige „Klauselkanten“. Die Literalkanten bilden ein Dreieck mit den speziellen Knoten und bilden auch ein Dreieck mit xi , ¬xi und rot für i = 1, 2, . . . , n. d. Zeigen Sie, dass in einer 3-Färbung c eines Graphen, der die Literalkanten enthält, für jede Variable entweder die Variable selbst oder ihr Komplement mit c(wahr) gefärbt und die andere mit c(falsch) gefärbt ist. Zeigen Sie, dass für jede erfüllende Wertzuweisung von φ eine 3-Färbung des Graphen, der lediglich die Literalkanten enthält, existiert. Das in Abbildung 34.20 gezeigte Widget hilft uns, die zu einer Klausel (x ∨ y ∨ z) korrespondierende Bedingung durchzusetzen. Jede Klausel benötigt eine eindeutige Kopie der 5 in der Abbildung stark schattierten Knoten; sie sind, wie
1114
34 NP-Vollständigkeit
x
y WAHR
z Abbildung 34.20: Das der Klausel (x ∨ y ∨ z) entsprechende, in Problemstellung 34.5-3 verwendete Widget.
in der Abbildung dargestellt, mit den Literalen der Klausel und dem speziellen Knoten wahr verbunden. e. Begründen Sie, dass, wenn jeder der x, y und z mit c(wahr) oder c(falsch) gefärbt ist, das Widget genau dann 3-färbbar ist, wenn mindestens eines der x, y oder z mit c(wahr) gefärbt ist. f. Vervollständigen Sie den Beweis, dass 3-COLOR NP-vollständig ist. 34-4 Ablaufplanung mit Gewinnen und Terminen Setzen Sie voraus, dass wir eine Maschine und eine Menge von n Jobs a1 , a2 , . . . , an haben, wobei jeder von diesen Zeit auf der Maschine benötigt. Jeder Job aj benötigt tj Zeiteinheiten auf der Maschine (seine Berechnungszeit), führt zu einem Gewinn von pj und hat einen Termin dj . Die Maschine kann zu jeder Zeit nur einen Job ausführen und kein Job aj darf unterbrochen werden, wenn er mal gestartet worden ist, d. h. er läuft in tj aufeinanderfolgenden Zeiteinheiten ab. Wenn wir den Job aj zum Termin dj fertigstellen, erhalten wir einen Gewinn pj ; wenn wir aber den Job aj erst nach dem Termin dj fertigstellen, dann erhalten wir keinen Gewinn. Im Optimierungsproblem haben wir die Ausführungsszeiten, die Gewinne und die Termine für eine Menge von n Jobs gegeben und wir wollen einen Ablaufplan bestimmen, der den größtmöglichen Gewinn erzielt. Die Ausführungszeiten, die Gewinne und die Termine sind alle nichtnegative Zahlen. a. Formulieren Sie dieses Problem als Entscheidungsproblem. b. Zeigen Sie, dass das Entscheidungsproblem NP-vollständig ist. c. Geben Sie einen Algorithmus mit polynomieller Laufzeit für das Entscheidungsproblem an, wenn alle Ausführungszeiten ganze Zahlen von 1 bis n sind. (Hinweis: Benutzen Sie dynamische Programmierung.) d. Geben Sie einen Algorithmus mit polynomieller Laufzeit für das Optimierungsproblem an, wenn alle Ausführungszeiten ganze Zahlen von 1 bis n sind.
Kapitelbemerkungen zu Kapitel 34
1115
Kapitelbemerkungen Das Buch von Garey und Johnson [129] liefert eine wunderbare Einführung zur NPVollständigkeit, wobei die Theorie ausführlich diskutiert und ein Katalog mit vielen Problemen vorgestellt wird, die 1979 als NP-vollständig bekannt waren. Der Beweis von Theorem 34.13 wurde aus ihrem Buch übernommen. Die Liste NP-vollständiger Probleme zu Beginn von Abschnitt 34.5 wurde aus dem Inhaltsverzeichnis ihres Buches zusammengestellt. Johnson schrieb zwischen 1981 und 1992 eine Reihe von 23 Kolumnen im Journal of Algorithms, die über neue Entwicklungen auf dem Gebiet der NP-Vollständigkeit berichteten. Hopcroft, Motwani und Ullman [177], Lewis und Papadimitriou [236], Papadimitriou [270] sowie Sipser [317] beinhalten gute Abhandlungen der NP-Vollständigkeit im Zusammenhang mit der Komplexitätstheorie. Die Bücher von Aho, Hopcroft und Ullman [5], Dasgupta, Papadimitriou und Vazirani [82], Johnsonbaugh und Schaefer [193] sowie Kleinberg und Tardos [208] befassen sich ebenfalls mit NP-Vollständigkeit und Reduktionen. Die Klasse P wurde 1964 von Cobham [72] und unabhängig davon 1965 von Edmonds [100] eingeführt. Edmonds führte auch die Klasse NP ein und vermutete P = NP. Der Begriff NP-Vollständigkeit wurde 1971 von Cook [75] vorgeschlagen, der die ersten NP-Vollständigkeitsbeweise für die Erfüllbarkeit der Aussagenlogik und die 3-CNFErfüllbarkeit lieferte. Levin [234] entwickelte unabhängig davon den Begriff, wobei er einen NP-Vollständigkeitsbeweis für das „tiling“-Problem angab. Karp [199] führte 1972 die Methode der Reduktionen ein und zeigte die große Vielfalt NP-vollständiger Probleme. Karps Artikel umfasste die ursprünglichen NP-Vollständigkeitsbeweise des Cliquen-, Knotenüberdeckungs- und Hamilton-Kreis-Problems. Seit dieser Zeit wurde die NPVollständigkeit von tausenden von Problemen durch viele Wissenschaftler bewiesen. In einer Rede anlässlich der Feier des 60. Geburtstages von Karp im Jahre 1995 bemerkte Papadimitriou: „Etwa 6000 Artikel jährlich führen den Begriff „NP-vollständig“ in ihrem Titel, im Abstrakt oder der Stichwortliste. Das ist häufiger als jeder der Begriffe „Compiler“, „Datenbank“, „Experte“, „neurales Netz“ oder „Betriebssystem“ vorkommt.“ Die neuesten Arbeiten auf dem Gebiet der Komplexitätstherie beleuchten die Komplexität der Berechnung von Approximationslösungen. Diese Arbeiten geben eine neue Definition von NP unter Verwendung von „probabilistisch überprüfbaren Beweisen“. Diese neue Definition impliziert, dass für Probleme wie das Cliquenproblem, das Knotenüberdeckungsproblem und das Problem des Handelsreisenden mit der Dreiecksungleichung und viele weitere Probleme die Berechnung guter Approximationslösungen NP-schwer ist und folglich nicht leichter als die Berechnung optimaler Lösungen. Eine Einführung in dieses Gebiet findet man in der Doktorarbeit von Arora [20], einem Kapitel von Arora und Lund in [172], in einem Übersichtsartikel von Arora [21], in einem von Mayr, Prömel und Steger herausgegebenen Buch [246] und einem Übersichtsartikel von Johnson [191].
35
Approximationsalgorithmen
Viele Probleme von praktischer Bedeutung sind NP-vollständig; sie sind aber zu wichtig, dass wir nicht einfach aufgeben können, nur weil wir nicht wissen, wie wir eine optimale Lösung in polynomieller Zeit finden können. Selbst wenn ein Problem NPvollständig ist, kann es Hoffnung geben. Wir haben mindestens drei Ansätze, um mit der NP-Vollständigkeit fertig zu werden. Erstens, ein Algorithmus mit exponentieller Laufzeit kann völlig zufriedenstellend sein, wenn die in der Praxis vorkommenden Eingaben klein sind. Zweitens, wir können möglicherweise wichtige Spezialfälle finden, die wir in polynomieller Zeit lösen können. Drittens, wir könnten Ansätze finden, fast optimale Lösungen in polynomieller Zeit (entweder im schlechtesten Fall oder im erwarteten Fall) zu berechnen. In der Praxis sind oft fast optimale Lösungen gut genug. Wir nennen einen Algorithmus, der eine fast optimale Lösung zurückgibt, Approximationsalgorithmus. Dieses Kapitel stellt Approximationsalgorithmen mit polynomieller Laufzeit für verschiedene NP-vollständige Probleme vor.
Performanzverhältnisse für Approximationsalgorithmen Setzen Sie voraus, dass wir an einem Optimierungsproblem arbeiten, bei dem jede potentielle Lösung positive Kosten hat, und eine fast optimale Lösung finden wollen. In Abhängigkeit vom Problem können wir eine optimale Lösung als eine Lösung definieren, für die die Gesamtkosten maximal oder minimal sein müssen; d. h. das Problem kann entweder ein Maximierungs- oder ein Minimierungsproblem sein. Wir sagen, dass ein Algorithmus für ein Problem ein Approximationsverhältnis ρ(n) hat, falls die Kosten C der durch den Algorithmus erzeugten Lösung für jede Eingabe der Größe n innerhalb eines Faktors ρ(n) den Kosten C ∗ einer optimalen Lösung entsprechen: max
C C∗ , C∗ C
≤ ρ(n) .
(35.1)
Wenn ein Algorithmus ein Approximationsverhältnis von ρ(n) erreicht, dann bezeichnen wir ihn als einen ρ(n)-Approximationsalgorithmus. Die Definitionen des Approximationsverhältnisses und eines ρ(n)-Approximationsalgorithmus gelten sowohl für Minimierungs- als auch für Maximierungsprobleme. Für ein Maximierungsproblem gilt 0 < C ≤ C ∗ und das Verhältnis C ∗ /C gibt den Faktor an, um den die Kosten einer optimalen Lösung größer sind als die Kosten der approximativen Lösung. Analog gilt für ein Minimierungsproblem 0 < C ∗ ≤ C und das Verhältnis C/C ∗ gibt den Faktor an, um den die Kosten der approximativen Lösung größer sind als die Kosten einer optimalen Lösung. Da wir vorausgesetzt haben, dass alle Lösungen positive Kosten haben,
1118
35 Approximationsalgorithmen
sind diese Verhältnisse immer wohldefiniert. Das Approximationsverhältnis eines Approximationsalgorithmus ist niemals kleiner als 1, denn aus C/C ∗ ≤ 1 folgt C ∗ /C ≥ 1. Daher erzeugt ein 1-Approximationsalgorithmus1 eine optimale Lösung, während ein Approximationsalgorithmus mit einem großen Approximationsverhältnis eine Lösung liefern kann, die viel schlechter als die optimale ist. Für viele Probleme haben wir Approximationsalgorithmen mit polynomieller Laufzeit und kleinem konstantem Approximationsverhältnis; für andere Probleme weisen die besten bekannten Approximationsalgorithmen mit polynomieller Laufzeit Approximationsverhältnisse auf, die jeweils gemäß einer Funktion in der Eingabegröße n wachsen. Ein Beispiel für ein solches Problem ist das Mengenüberdeckungsproblem, das in Abschnitt 35.3 vorgestellt wird. Einige NP-vollständige Probleme erlauben Approximationsalgorithmen mit polynomieller Laufzeit, die immer bessere Approximationsverhältnisse erreichen können, aber dafür immer mehr Rechenzeit benötigen. Das heißt, wir können Rechenzeit gegen Güte der Approximation handeln. Ein Beispiel ist das Teilsummenproblem, das in Abschnitt 35.5 untersucht wird. Diese Situation ist wichtig genug, dass sie einen eigenen Namen verdient. Ein Approximationsschema für ein Optimierungsproblem ist ein Approximationsalgorithmus, der als Eingabe nicht nur eine Instanz des Problems erhält, sondern auch einen Wert > 0, mit dem das Schema ein (1 + )-Approximationsalgorithmus ist. Wir sagen, dass ein Approximationsschema ein Approximationsschema mit polynomieller Laufzeit ist, falls die Laufzeit des Schemas für jedes feste > 0 polynomiell in der Eingabegröße n der Instanz ist. Die Laufzeit eines Approximationsschemas mit polynomieller Laufzeit kann mit kleiner werdendem sehr schnell wachsen. Beispielsweise könnte die Laufzeit eines Polynomialzeit-Approximationsschema in O(n2/ ) sein. Im Idealfall, sollte, wenn um einen konstanten Faktor fällt, die Laufzeit, die notwendig ist, um die gewünschte Approximation zu erreichen, um nicht mehr als um einen konstanten Faktor anwachsen (wenngleich das nicht notwendigerweise der gleiche konstante Faktor sein muss wie der, um den kleiner wird). Wir sagen, dass ein Approximationsschema ein Approximationsschema mit vollständig polynomieller Laufzeit ist, falls seine Laufzeit polynomiell in 1/ und in der Größe n der Eingabeinstanz ist. Beispielsweise könnte ein solches Schema eine Laufzeit von O((1/)2 n3 ) haben. Bei einem solchen Schema führt jede Verringerung von um einen konstanten Faktor zu einen Anstieg der Laufzeit um einen korrespondierenden konstanten Faktor.
Kapitelübersicht Die ersten vier Abschnitte dieses Kapitels stellen einige Beispiele für Approximationsalgorithmen mit polynomieller Laufzeit für NP-vollständige Probleme vor und der fünfte 1 Wenn das Approximationsverhältnis unabhängig von n ist, verwenden wir die Begriffe Approximationsverhältnis ρ und ρ-Approximationsalgorithmus, was auf die Unabhängigkeit von n hinweisen soll.
35.1 Das Knotenüberdeckungsproblem
1119
Abschnitt präsentiert ein Approximationsschema mit vollständig polynomieller Laufzeit. Abschnitt 35.1 beginnt mit einer Untersuchung des Mengenüberdeckungsproblems, einem NP-vollständigen Minimierungsproblem, das einen Approximationsalgorithmus mit dem Approximationsverhältnis 2 besitzt. Abschnitt 35.2 stellt einen Approximationsalgorithmus mit dem Approximationsverhältnis 2 für das Problem des Handelsreisenden vor, wenn die Kostenfunktion die Dreiecksungleichung erfüllt. Außerdem wird gezeigt, dass beim allgemeinen Handelsreisendenproblem, in dem die Kostenfunktion der Dreiecksungleichung in der Regel nicht genügt, für beliebige Konstanten ρ ≥ 1 kein ρ-Approximationsalgorithmus existieren kann, es sei denn, es gilt P=NP. In Abschnitt 35.3 zeigen wir, wie wir eine Greedy-Methode als einen effektiven Approximationsalgorithmus für das Mengenüberdeckungsproblem verwenden können. Wir erhalten damit eine Überdeckung, deren Kosten im schlechtesten Fall um einen logarithmischen Faktor größer sind als die optimalen Kosten. Abschnitt 35.4 stellt zwei weitere Approximationsalgorithmen vor. Als erstes untersuchen wir die Optimierungsversion der 3-CNF-Erfüllbarkeit und geben einen einfachen randomisierten Algorithmus an, der eine Lösung mit einem erwarteten Approximationsverhältnis von 8/7 erzeugt. Dann untersuchen wir eine gewichtete Variante des Knotenüberdeckungsproblems und zeigen, wie wir mithilfe linearer Programmierung einen 2-Approximationsalgorithmus entwickeln können. Schließlich präsentiert Abschnitt 35.5 ein Approximationsschema mit vollständig polynomieller Laufzeit für das Teilsummenproblem.
35.1
Das Knotenüberdeckungsproblem
Abschnitt 34.5.2 definierte das Knotenüberdeckungsproblem und bewies, dass das Problem NP-vollständig ist. Eine Knotenüberdeckung eines ungerichteten Graphen G = (V, E) ist eine Teilmenge V ⊆ V mit der Eigenschaft, dass für jede Kante (u, v) aus G mindestens einer der beiden Knoten u, v zu V gehört. Die Größe einer Knotenüberdeckung ist die Anzahl der enthaltenen Knoten. Das Knotenüberdeckungsproblem besteht darin, eine Knotenüberdeckung minimaler Größe in einem gegebenen ungerichteten Graphen zu finden. Wir bezeichnen eine solche Überdeckung als optimale Knotenüberdeckung . Dieses Problem ist die Optimierungsversion eines NP-vollständigen Entscheidungsproblems. Wenngleich wir nicht wissen, wie wir eine optimale Knotenüberdeckung in einem Graphen G in polynomieller Zeit berechnen können, können wir effizient eine Knotenüberdeckung finden, die fast optimal ist. Der folgende Approximationsalgorithmus erhält als Eingabe einen ungerichteten Graphen G und gibt eine Knotenüberdeckung zurück, deren Größe mit Sicherheit nicht mehr als doppelt so groß wie die Größe einer optimalen Knotenüberdeckung ist. Approx-Vertex-Cover(G) 1 C =∅ 2 E = G.E 3 while E = ∅ 4 sei (u, v) eine beliebige Kante aus E 5 C = C ∪ {u, v} 6 entferne aus E jede Kante, die inzident zu u oder zu v ist 7 return C
1120
35 Approximationsalgorithmen
b
c
d
a
e
f
g
b
c
d
a
e
f
(a)
(b)
b
c
d
a
e
f
g
b
c
d
a
e
f
(c) b
c
a
e
g
(d) d
f (e)
g
g
b
c
a
e
d
f
g
(f)
Abbildung 35.1: Die Arbeitsweise von Approx-Vertex-Cover. (a) Der Eingabegraph G, der 7 Knoten und 8 Kanten hat. (b) Die stark gezeichnete Kante (b, c) ist die erste von Approx-Vertex-Cover gewählte Kante. Die Knoten b und c (schwach schattiert) werden zu der Menge C hinzugefügt, die die berechnete Knotenüberdeckung enthält. Die Kanten (a, b), (c, e) und (c, d) (Strichlinien) werden entfernt, da sie nun von einem Knoten aus C überdeckt werden. (c) Die Kante (e, f ) wird gewählt; die Knoten e und f werden zu C hinzugefügt. (d) Die Kante (d, g) wird gewählt; die Knoten d und g werden zu C hinzugefügt. (e) Die Menge C, d. h. die durch Approx-Vertex-Cover letztendlich berechnete Knotenüberdeckung, enthält die 6 Knoten b, c, d, e, f , g. (f ) Die optimale Knotenüberdeckung für dieses Problem enthält nur 3 Knoten, nämlich b, d und e.
Abbildung 35.1 illustriert, wie Approx-Vertex-Cover auf dem Beispielgraphen arbeitet. Die Variable C enthält die berechnete Knotenüberdeckung. Zeile 1 initialisiert C mit der leeren Menge. Zeile 2 initialisiert E mit einer Kopie der Kantenmenge G.E des Graphen. Die Schleife in den Zeilen 3–6 wählt jedesmal eine Kante (u, v) aus E aus, fügt ihre Endpunkte u und v zu C hinzu und entfernt alle Kanten aus E , die von u oder v überdeckt werden. Schlussendlich gibt Zeile 7 die Knotenüberdeckung C zurück. Die Laufzeit dieses Algorithmus ist O(V + E), falls Adjazenzlisten für die Darstellung von E verwendet werden. Theorem 35.1 Approx-Vertex-Cover ist ein 2-Approximationsalgorithmus mit polynomieller Laufzeit. Beweis: Wir haben bereits gezeigt, dass Approx-Vertex-Cover in polynomieller Zeit läuft.
35.1 Das Knotenüberdeckungsproblem
1121
Die Menge C der von Approx-Vertex-Cover zurückgegebenen Knoten ist eine Knotenüberdeckung, da die Schleife des Algorithmus solange wiederholt wird, bis jede Kante aus G.E von einem Knoten überdeckt wurde. Um zu sehen, dass Approx-Vertex-Cover eine Knotenüberdeckung zurückgibt, die höchstens doppelt so groß ist wie eine optimale Überdeckung, lassen Sie uns die Menge der Kanten, die in Zeile 4 der Prozedur Approx-Vertex-Cover ausgewählt werden, mit A bezeichnen. Um die Kanten aus A zu überdecken, muss jede Knotenüberdeckung – insbesondere also eine optimale Knotenüberdeckung C ∗ – mindestens einen Endpunkt von jeder Kante aus A enthalten. Keine zwei Kanten aus A teilen einen gleichen Endpunkt, denn wenn eine Kante einmal in Zeile 4 ausgewählt wurde, werden alle Kanten, die mit deren Endpunkten inzident sind, in Zeile 6 aus E entfernt. Daher werden keine zwei Kanten aus A durch den gleichen Knoten aus C ∗ überdeckt, sodass wir die untere Schranke |C ∗ | ≥ |A|
(35.2)
für die Größe einer optimalen Knotenüberdeckung haben. Jede Ausführung von Zeile 4 wählt eine Kante aus, für die keiner ihrer beiden Endpunkte bereits zu C gehört, was eine obere Schranke (tatsächlich eine exakte Schranke) für die Größe der zurückgegebenen Knotenüberdeckung liefert: |C| = 2 |A| .
(35.3)
Kombinieren wir die Gleichungen (35.2) und (35.3), so erhalten wir |C| = 2 |A| ≤ 2 |C ∗ | , womit das Theorem bewiesen ist.
Lassen Sie uns über diesen Beweis noch ein wenig nachdenken. Zunächst könnten Sie sich fragen, wie wir beweisen konnten, dass die durch Approx-Vertex-Cover zurückgegebene Knotenüberdeckung höchstens doppelt so groß wie eine optimale Knotenüberdeckung ist, ohne die Größe der optimalen Knotenüberdeckung zu kennen. Anstatt zu fordern, dass wir die exakte Größe einer optimalen Knotenüberdeckung kennen, ziehen wir uns auf eine untere Schranke für die Größe zurück. Wie Übung 35.1-2 von Ihnen verlangt zu zeigen, ist die Menge A der Kanten, die Zeile 4 von Approx-Vertex-Cover auswählt, in Wirklichkeit ein maximales Matching des Graphen G. (Ein maximales Matching ist ein Matching, das keine echte Teilmenge eines anderen Matchings ist.) Die Größe eines maximalen Matchings ist, wie wir im Beweis von Theorem 35.1 gezeigt haben, eine untere Schranke für die Größe einer optimalen Knotenüberdeckung. Der Algorithmus gibt eine Knotenüberdeckung zurück, deren Größe höchstens doppelt so groß ist wie die Größe des maximalen Matchings A. Wenn wir die Größe der zurückgegebenen Lösung in Beziehung zu der unteren Schranke setzen, erhalten wir unser Approximationsverhältnis. Wir werden diesen Ansatz auch in den nächsten Abschnitten verwenden.
1122
35 Approximationsalgorithmen
Übungen 35.1-1 Geben Sie ein Beispiel für einen Graphen an, für den die Prozedur ApproxVertex-Cover immer eine suboptimale Lösung liefert. 35.1-2 Beweisen Sie, dass die Menge der Kanten, die in Zeile 4 von Approx-VertexCover ausgewählt werden, ein maximales Matching von des Graphen G ist. 35.1-3∗ Professor Bündchen schlägt die folgende Heuristik für die Lösung des Knotenüberdeckungsproblems vor. Wähle wiederholt einen Knoten mit dem höchsten Grad aus und entferne alle mit ihm inzidenten Kanten. Geben Sie ein Beispiel dafür an, dass diese Heuristik nicht das Approximationsverhältnis 2 hat. (Hinweis: Versuchen Sie es mit einem bipartiten Graphen mit Knoten von einheitlichem Grad auf der linken und Knoten mit variierendem Grad auf der rechten Seite.) 35.1-4 Geben Sie einen effizienten Greedy-Algorithmus an, der in linearer Zeit eine optimale Knotenüberdeckung für einen Baum berechnet. 35.1-5 Aus dem Beweis von Theorem 34.12 wissen wir, dass das Knotenüberdeckungsproblem und das NP-vollständige Cliquenproblem komplementär in dem Sinne sind, dass eine optimale Knotenüberdeckung das Komplement einer Clique maximaler Größe im komplementären Graphen ist. Folgt aus dieser Beziehung, dass es für das Cliquenproblem einen Polynomialzeit-Approximationsalgorithmus mit einem konstanten Approximationverhältnis gibt? Begründen Sie Ihre Antwort.
35.2
Das Problem des Handelsreisenden
Beim Problem des Handelsreisenden, das in Abschnitt 34.5.4 eingeführt wurde, ist ein vollständiger ungerichteter Graph G = (V, E) gegeben, in dem jeder Kante (u, v) ∈ E nichtnegative ganzzahlige Kosten c(u, v) zugeordnet sind. Zu berechnen ist ein hamiltonischer Kreis (eine Tour) von G mit minimalen Kosten. In Erweiterung unserer Notation bezeichnen wir die Gesamtkosten der Kanten in der Teilmenge A ⊆ E mit c(A), c(u, v) . c(A) = (u,v)∈A
In vielen praktischen Situationen ist der billigste Weg, um von einer Stelle u zu einer Stelle w zu gehen, direkt, also ohne Zwischenstopps, von u nach w zu gehen. Andersrum formuliert, das Herausnehmen eines Zwischenstopps kann die Kosten niemals ansteigen lassen. Wir formalisieren dieses Argument durch die Aussage, dass die Kostenfunktion c für alle Knoten u, v, w ∈ V die Dreiecksungleichung c(u, w) ≤ c(u, v) + c(v, w) erfüllt.
35.2 Das Problem des Handelsreisenden
1123
Die Dreiecksungleichung erscheint als ob sie normalerweise gelten sollte, und sie ist in mehreren Anwendungen automatisch erfüllt. Sind beispielsweise die Knoten des Graphen Punkte in der Ebene und sind die Kosten für das Reisen zwischen zwei Knoten durch die gewöhnliche euklidische Distanz zwischen ihnen gegeben, dann ist die Dreiecksungleichung erfüllt. Zudem erfüllen viele andere Kostenfunktionen die Dreiecksungleichung. Wie Übung 35.2-2 zeigt, ist das Problem des Handelsreisenden selbst dann NP-vollständig, wenn wir fordern, dass die Kostenfunktion die Dreiecksungleichung erfüllt. Wir sollten deshalb nicht erwarten, dass wir einen Algorithmus mit polynomieller Laufzeit finden, der dieses Problem exakt löst. Stattdessen suchen wir nach guten Approximationsalgorithmen. In Abschnitt 35.2.1 untersuchen wir einen 2-Approximationsalgorithmus für das Problem des Handelsreisenden, in dem die Dreiecksungleichung erfüllt ist. In Abschnitt 35.2.2 zeigen wir, dass ohne die Dreiecksungleichung kein Approximationsalgorithmus mit polynomieller Laufzeit und konstantem Approximationsverhältnis existiert, es sei denn, es gilt P=NP.
35.2.1
Das Handelsreisendenproblem mit der Dreiecksungleichung
Mithilfe der Methodik aus dem vorhergehenden Abschnitt werden wir zunächst eine Struktur berechnen – einen minimalen Spannbaum – dessen Gewicht eine untere Schranke für die Länge einer optimalen Tour für den Handelsreisenden ist. Dann werden wir den minimalen Spannbaum verwenden, um eine Tour zu erzeugen, deren Kosten nicht mehr als doppelt so hoch wie die des minimalen Spannbaums sind. Der folgende Algorithmus implementiert diesen Ansatz, wobei der Algorithmus zur Berechnung eines minimalen Spannbaums aus Abschnitt 23.2 als Unterroutine aufgerufen wird. Der Parameter G ist ein vollständiger ungerichteter Graph und die Kostenfunktion c genügt der Dreiecksungleichung. Approx-TSP-Tour(G, c) 1 wähle einen Knoten r ∈ G.V als „Wurzel“ aus 2 berechne mithilfe von MST-Prim(G, c, r) einen minimalen Spannbaum T von G mit Wurzel r 3 sei H eine Liste von Knoten, geordnet in der Reihenfolge nach der sie bei einer Preorder-Traversierung von T zuerst besucht werden 4 return den hamiltonischen Kreis H Rufen Sie sich aus Abschnitt 12.1 in Erinnerung, dass eine Preorder-Traversierung jeden Knoten des Baumes rekursiv besucht, bevor eines seiner Kinder besucht wird, und einen Knoten auflistet, wenn er das erste Mal besucht wird.
1124
35 Approximationsalgorithmen
a
d
a
d
e b
f
a e
g
c
b
f
e g
c h (a)
f
g
a
(c)
d e
e b
f
h (b)
d
b c
h
a
d
g
b
f
g
c
c
h
h (d)
(e)
Abbildung 35.2: Die Arbeitsweise von Approx-TSP-Tour. (a) Ein vollständiger ungerichteter Graph. Die Knoten liegen auf einem ganzzahligen Gitter. Beispielsweise befindet sich f eine Einheit rechts und zwei Einheiten oberhalb von h. Die Kostenfunktion zwischen zwei Punkten ist die gewöhnliche euklidische Distanz. (b) Ein minimaler Spannbaum T des vollständigen Graphen, wie er von der Unterroutine MST-Prim berechnet wird. Der Knoten a ist die Wurzel. Die Abbildung zeigt nur die Kanten, die zum minimalen Spannbaum gehören. Die Knoten wurden so indiziert, dass sie von MST-Prim in alphabetischer Reihenfolge zum Hauptbaum hinzugefügt werden. (c) Eine Traversierung von T , beginnend bei a. Eine vollständige Traversierung von T besucht die Knoten in der Reihenfolge a, b, c, b, h, b, a, d, e, f, e, g, e, d, a. Eine Preorder-Traversierung von T listet einen Knoten auf, wenn er zum ersten Mal besucht wird, was in der Abbildung durch den Punkt neben dem Knoten angedeutet wird, und führt zu der Reihenfolge a, b, c, h, d, e, f, g. (d) Eine Tour, die wir erhalten, wenn wir die Knoten in der Reihenfolge der Preorder-Traversierung besuchen. Dies ist die Tour, die von Approx-TSP-Tour zurückgegeben wird. Die Gesamtkosten sind ungefähr 19, 074. (e) Eine optimale Tour H ∗ des ursprünglichen vollständigen Graphen. Ihre Gesamtkosten belaufen sich auf ungefähr 14, 715.
35.2 Das Problem des Handelsreisenden
1125
Abbildung 35.2 illustriert die Arbeitsweise von Approx-TSP-Tour. Teil (a) der Abbildung zeigt einen vollständigen ungerichteten Graphen und Teil (b) einen minimalen Spannbaum T , der durch die Unterroutine MST-Prim ausgehend von der Wurzel a erzeugt wird. Teil (c) zeigt, wie eine Preorder-Traversierung von T die Knoten besucht, und Teil (d) zeigt die korrespondierende Tour, die von Approx-TSP-Tour zurückgegeben wird. Teil (e) zeigt eine optimale Tour, die um etwa 23 % kürzer ist. Nach Übung 23.2-2 ist die Laufzeit von Approx-TSP-Tour selbst mit einer einfachen Implementierung von MST-Prim in Θ(V 2 ). Wir zeigen nun, dass Approx-TSP-Tour eine Tour zurückgibt, deren Kosten nicht mehr als doppelt so hoch wie die Kosten einer optimalen Tour sind, falls die Kostenfunktion die Dreiecksungleichung erfüllt. Theorem 35.2 Approx-TSP-Tour ist für das Problem des Handelsreisenden, in dem die Dreiecksungleichung erfüllt ist, ein 2-Approximationsalgorithmus mit polynomieller Laufzeit. Beweis: Wir haben bereits gesehen, dass Approx-TSP-Tour in polynomieller Zeit läuft. Sei H ∗ eine optimale Tour für die gegebene Knotenmenge. Wir erhalten einen Spannbaum, indem wir eine beliebige Kante aus der Tour entfernen, und die Kantengewichte sind nichtnegativ. Somit ist das Gewicht des minimalen Spannbaums T , der in Zeile 2 von Approx-TSP-Tour berechnet wird, eine untere Schranke für die Kosten einer optimalen Tour, d. h. es gilt c(T ) ≤ c(H ∗ ) .
(35.4)
Eine vollständige Traversierung listet einen Knoten auf, wenn er das erste Mal besucht wird und wenn nach jedem Besuch eines seiner Teilbäume zu ihm zurückgekehrt wird. Lassen Sie uns diese vollständige Traversierung mit W bezeichnen. Die vollständige Traversierung in unserem Beispiel ergibt die Reihenfolge a, b, c, b, h, b, a, d, e, f, e, g, e, d, a . Da die vollständige Traversierung jede Kante von T genau zweimal passiert, gilt (wenn wir unsere Definition der Kosten c in natürlicher Weise erweitern, um auch mit Multimengen von Kanten arbeiten zu können) c(W ) = 2 c(T ) .
(35.5)
Die Ungleichung (35.4) und die Gleichung (35.5) implizieren c(W ) ≤ 2 c(H ∗ ) ,
(35.6)
und so sind die Kosten von W nicht mehr als doppelt so hoch wie die Kosten einer optimalen Tour. Leider ist die vollständige Traversierung W im Allgemeinen keine Tour, da einige Knoten mehr als einmal besucht werden. Nach der Dreiecksungleichung können wir jedoch
1126
35 Approximationsalgorithmen
einen Besuch eines beliebigen Knotens streichen, ohne die Kosten zu erhöhen. (Wenn wir einen Knoten v zwischen den Besuchen von u und w aus W entfernen, legt die entstehende Reihenfolge fest, dass direkt von u nach w gegangen wird.) Nach wiederholter Anwendung dieser Operation können wir für jeden Knoten alle Besuche außer dem ersten aus W entfernen. In unserem Beispiel entsteht dadurch die Reihenfolge a, b, c, h, d, e, f, g . Diese Reihenfolge ist die gleiche wie die, die wir durch eine Preorder-Traversierung des Baumes T erhalten. Sei H der Zyklus, der dieser Preorder-Traversierung entspricht. Er ist ein hamiltonischer Kreis, da jeder Knoten genau einmal besucht wird, und tatsächlich ist es derjenige Zyklus, der von Approx-TSP-Tour berechnet wird. Da H durch das Entfernen von Knoten aus der vollständigen Traversierung W entsteht, gilt c(H) ≤ c(W ) .
(35.7)
Kombinieren wir die Ungleichungen (35.6) und (35.7), so sehen wir, dass c(H) ≤ 2c(H ∗ ) gilt, womit das Theorem bewiesen ist. Trotz des guten in Theorem 35.2 angegebenen Approximationsverhältnisses ist ApproxTSP-Tour in der Praxis gewöhnlich nicht die beste Wahl für die Lösung dieses Problems. Es gibt andere Approximationsalgorithmen, die in der Praxis meist eine viel bessere Performanz zeigen (siehe dazu die am Ende des Kapitels angegebenen Referenzen).
35.2.2
Das allgemeine Handelsreisendenproblem
Wenn wir die Voraussetzung fallen lassen, dass die Kostenfunktion c die Dreiecksungleichung erfüllt, können wir keine guten Approximationstouren in polynomieller Zeit finden, es sei denn, es gilt P=NP. Theorem 35.3 Im Falle P = NP gibt es für keine Konstante ρ ≥ 1 einen Approximationsalgorithmus mit dem Approximationsverhältnis ρ für das allgemeine Problem des Handelsreisenden. Beweis: Wir führen den Beweis indirekt und nehmen zum Zwecke des Widerspruchs an, dass für eine Konstante ρ ≥ 1 ein Approximationsalgorithmus A mit polynomieller Laufzeit und dem Approximationsverhältnis ρ existieren würde. Ohne Beschränkung der Allgemeinheit setzen wir voraus, dass ρ eine ganze Zahl ist, und runden gegebenenfalls auf. Wir zeigen dann, wie wir A verwenden können, um die Instanzen des HamiltonKreis-Problems (siehe Abschnitt 34.2) in polynomieller Zeit zu lösen. Da Theorem 34.13 aussagt, dass das Hamilton-Kreis-Problem NP-vollständig ist, impliziert somit Theorem 34.4, dass P=NP gilt, was im Widerspruch zu der Voraussetzung des Theorems steht.
35.2 Das Problem des Handelsreisenden
1127
Sei G = (V, E) eine Instanz des Hamilton-Kreis-Problems. Wir wollen effizient mit dem hypothetischen Algorithmus A effizient feststellen, ob G einen hamiltonischen Kreis besitzt. Wir transformieren G folgendermaßen in eine Instanz des Problems des Handelsreisenden. Sei G = (V, E ) ein vollständiger Graph auf V , d. h. E = {(u, v) : u, v ∈ V und u = v} . Wir ordnen jeder Kante aus E ganzzahlige Kosten zu: 1 falls (u, v) ∈ E , c(u, v) = ρ |V | + 1 sonst . Wir können in polynomieller Zeit in |V | und |E| aus einer Darstellung von G Darstellungen von G und c erzeugen. Betrachten Sie nun das Handelsreisendenproblem (G , c). Wenn der ursprüngliche Graph G einen hamiltonischen Kreis H besitzt, dann ordnet die Kostenfunktion c jeder Kante von H den Kostenfaktor 1 zu, sodass (G , c) eine Tour mit den Kosten |V | enthält. Wenn G dagegen keinen hamiltonischen Kreis enthält, dann muss jede Tour von G wenigstens eine Kante benutzen, die nicht zu E gehört. Jede Tour, die eine Kante enthält, die nicht in E ist, hat aber mindestens die Kosten (ρ |V | + 1) + (|V | − 1) = ρ |V | + |V | > ρ |V | . Da Kanten, die nicht zu G gehören, so teuer sind, gibt es einen Unterschied von mindestens ρ |V | zwischen den Kosten einer Tour, die einen hamiltonischen Kreis in G darstellt (deren Kosten gleich |V | sind), und den Kosten einer anderen Tour (deren Kosten mindestens ρ |V | + |V | sind). Somit sind die Kosten einer Tour, die keinen hamiltonischen Kreis von G darstellt, mindestens einen Faktor ρ + 1 größer als die Kosten einer Tour, die ein hamiltonischer Kreis in G ist. Setzen Sie nun voraus, dass wir den Approximationsalgorithmus A auf das Problem des Handelsreisenden (G , c) anwenden? Da A mit Sicherheit eine Tour zurückgibt, deren Kosten nicht größer sind als ρ-mal die Kosten einer optimalen Tour, muss A, falls G einen hamiltonischen Kreis enthält, diesen zurückgeben. Falls G keinen hamiltonischen Kreis enthält, dann gibt A eine Tour mit mindestens den Kosten ρ |V | zurück. Also können wir A benutzen, um das Hamilton-Kreis-Problem in polynomieller Zeit zu lösen. Der Beweis von Theorem 35.3 dient als ein Beispiel einer allgemeinen Technik, mit der wir beweisen können, dass wir ein Problem nicht gut approximieren können. Setzen Sie voraus, dass, gegeben ein NP-schweres Problem X, wir in polynomieller Zeit ein Minimierungsproblem Y so erzeugen können, dass „ ja“-Instanzen von X zu Instanzen von Y korrespondieren, deren Wert höchstens k ist (für irgendein k), und „nein“-Instanzen von X zu Instanzen von Y , deren Wert größer als ρk ist. Damit hätten wir gezeigt, dass, wenn P=NP nicht gilt, kein ρ-Approximationsalgorithmus mit polynomieller Laufzeit für das Problem Y existieren kann.
1128
35 Approximationsalgorithmen
Übungen 35.2-1 Setzen Sie voraus, dass ein vollständiger ungerichteter Graph G = (V, E) mit mindestens 3 Knoten eine Kostenfunktion c hat, die die Dreiecksungleichung erfüllt. Beweisen Sie, dass c(u, v) ≥ 0 für alle u, v ∈ V gilt. 35.2-2 Zeigen Sie, wie wir in polynomieller Zeit eine Instanz des Problems des Handelsreisenden in eine andere überführen können, deren Kostenfunktion die Dreiecksungleichung erfüllt. Die beiden Instanzen müssen die gleiche Menge optimaler Touren haben. Erläutern Sie, weshalb eine solche PolynomialzeitTransformation keinen Widerspruch zu Theorem 35.3 darstellt, wenn P = NP gilt. 35.2-3 Betrachten Sie die folgende nächste-Punkt-Heuristik für die Konstruktion einer approximativen Tour des Handelsreisenden, deren Kostenfunktion die Dreiecksungleichung erfüllt. Beginnen Sie mit einem trivialen Zyklus, der aus einem einzigen beliebig gewählten Knoten besteht. Suchen Sie in jedem Schritt nach demjenigen Knoten u, der nicht zum Zyklus gehört und dessen Abstand zu einem Knoten des Zyklus minimal ist. Setzen Sie nun voraus, dass der Knoten auf dem Zyklus, der u am nächsten ist, der Knoten v ist. Erweitern Sie den Zyklus um den Knoten u, indem Sie u unmittelbar hinter v einfügen. Wiederholen Sie dies, bis alle Knoten zum Zyklus gehören. Beweisen Sie, dass diese Heuristik eine Tour zurückgibt, deren Gesamtkosten nicht mehr als doppelt so hoch wie die Kosten einer optimalen Tour sind. 35.2-4 In dem Flaschenhals-Problem des Handelsreisenden wollen wir den hamiltonischen Kreis finden, der die Kosten der teuersten Kante des Kreises minimiert. Setzen Sie voraus, dass die Kostenfunktion die Dreiecksungleichung erfüllt, und zeigen Sie, dass ein Approximationsalgorithmus mit polynomieller Laufzeit und dem Approximationsfaktor 3 für dieses Problem existiert. (Hinweis: Zeigen Sie rekursiv, dass wir in einem Flaschenhals-Spannbaum (siehe Problemstellung 23-3) alle Knoten genau einmal besuchen können, indem wir eine vollständige Traversierung des Baumes durchführen und Knoten überspringen, jedoch nicht mehr als zwei hintereinander stehende Zwischenknoten. Zeigen Sie, dass die Kosten der teuersten Kante in einem FlaschenhalsSpannbaum höchstens gleich den Kosten der teuersten Kante in einem Flaschenhals-Hamilton-Kreis ist.) 35.2-5 Setzen Sie voraus, dass die Knoten für eine Instanz des Problems des Handelsreisenden Punkte in der Ebene sind und die Kosten c(u, v) durch die euklidischen Distanz zwischen den Punkten u und v gegeben sind. Zeigen Sie, dass eine optimale Tour sich niemals selbst schneidet.
35.3
Das Mengenüberdeckungsproblem
Das Mengenüberdeckungsproblem ist ein Optimierungsproblem, das viele Probleme modelliert, bei denen es um die Allokation von Ressourcen geht. Das zugehörige Entschei-
35.3 Das Mengenüberdeckungsproblem
1129
S1
S2 S6 S3
S4
S5
Abbildung 35.3: Eine Instanz (X, F) des Mengenüberdeckungsproblems, wobei X aus den 12 schwarzen Punkten besteht und F = {S1 , S2 , S3 , S4 , S5 , S6 } ist. Eine Mengenüberdeckung minimaler Größe ist C = {S3 , S4 , S5 }; sie hat Größe 3. Der Greedy-Algorithmus erzeugt eine Überdeckung der Größe 4, indem er entweder die Mengen S1 , S4 , S5 und S3 oder die Mengen S1 , S4 , S5 und S6 , jeweils in dieser Reihenfolge, auswählt.
dungsproblem verallgemeinert das NP-vollständige Knotenüberdeckungsproblem und ist daher NP-schwer. Der Approximationsalgorithmus, der für die Behandlung des Knotenüberdeckungsproblems entwickelt wurde, ist hier jedoch nicht anwendbar, sodass wir andere Ansätze ausprobieren müssen. Wir werden uns eine einfache Greedy-Heuristik mit einem logarithmischen Approximationsverhältnis anschauen. Das heißt, wenn die Größe der Instanz wächst, kann die Größe der approximativen Lösung relativ zur Größe einer optimalen Lösung wachsen. Da die Logarithmusfunktion jedoch recht langsam wächst, kann dieser Approximationsalgorithmus trotzdem nützliche Ergebnisse liefern. Eine Instanz (X, F) des Mengenüberdeckungsproblems besteht aus einer endlichen Menge X und einer Familie F von Teilmengen von X, sodass jedes Element von X zu mindestens einer Teilmenge aus F gehört: , S. X= S∈F
Wir sagen, dass eine Teilmenge S ∈ F ihre Elemente überdeckt. Das Problem besteht darin, eine minimale Teilmenge C ⊆ F zu finden, deren Elemente alle Elemente von X überdecken: , X= S. (35.8) S∈C
Erfüllt eine Teilmenge C Gleichung (35.8), so sagen wir, dass sie die Menge X überdeckt. Abbildung 35.3 illustriert das Mengenüberdeckungsproblem. Die Größe von C ist die Anzahl der in ihr enthaltenen Mengen und nicht die Anzahl der einzelnen Elemente in diesen Mengen. In Abbildung 35.3 hat die minimale Mengenüberdeckung die Größe 3. Das Mengenüberdeckungsproblem abstrahiert viele allgemein vorkommende kombinatorische Probleme. Setzen Sie beispielsweise voraus, dass X eine Menge von Fähigkeiten darstellt, die nötig sind, um ein Problem zu lösen. Zur Bearbeitung des Problems
1130
35 Approximationsalgorithmen
steht eine Menge von Personen zur Verfügung. Wir wollen ein Team bilden, das aus so wenig Personen wie möglich besteht und in dem es für jede erforderliche Fähigkeit aus X mindestens eine Person gibt, die diese Fähigkeit besitzt. Formulieren wir das Mengenüberdeckungsproblem als Entscheidungsproblem, dann fragen wir danach, ob es eine Überdeckung gibt, deren Größe höchstens k ist, wobei k ein zusätzlicher, in der Probleminstanz spezifizierter Parameter ist. Das Entscheidungsproblem zum Mengenüberdeckungsproblem ist NP-vollständig, was Sie in Übung 35.3-2 zeigen sollen.
Ein Greedy-Approximationsalgorithmus Die Greedy-Methode wählt in jeder Stufe diejenige Menge S aus, die die größte Anzahl der noch verbliebenen nicht überdeckten Elemente enthält. Greedy-Set-Cover(X, F) 1 U =X 2 C=∅ 3 while U = ∅ 4 wähle eine Menge S ∈ F, die |S ∩ U | maximiert 5 U = U −S 6 C = C ∪ {S} 7 return C Im Beispiel aus Abbildung 35.3 fügt Greedy-Set-Cover die Mengen S1 , S4 und S5 gefolgt von entweder S3 oder S6 in dieser Reihenfolge zu C hinzu. Der Algorithmus arbeitet folgendermaßen. Die Menge U enthält in jeder Iteration die Menge der noch verbliebenen nicht überdeckten Knoten. Die Menge C enthält die bis dahin erzeugte Überdeckung. Zeile 4 ist der Schritt, in dem die Greedy-Entscheidung getroffen wird, sie wählt eine Teilmenge S aus, die so viele nicht überdeckte Elemente wie möglich enthält (wobei bei gleich guten Teilmengen eine beliebige ausgewählt wird). Nachdem die Menge S ausgewählt wurde, entfernt Zeile 5 die Elemente von S aus U und Zeile 6 fügt S zu C hinzu. Wenn der Algorithmus terminiert, enthält die Menge C eine Teilmenge von F, die X überdeckt. Wir können den Algorithmus Greedy-Set-Cover leicht so implementieren, dass seine Laufzeit polynomiell in |X| und |F| ist. Da die Anzahl der Iterationen der Schleife in den Zeilen 3–6 nach oben durch min(|X| , |F|) beschränkt ist und wir den Schleifenrumpf so implementieren können, dass er in Zeit O(|X| |F|) läuft, benötigt eine einfache Implementierung Laufzeit O(|X| |F| min(|X| , |F|)). Übung 35.3-3 fragt nach einem Algorithmus mit linearer Laufzeit.
Analyse Wir zeigen nun, dass der Greedy-Algorithmus eine Mengenüberdeckung zurückgibt, die nicht sehr viel größer als die optimale Überdeckung ist. Wir bezeichnen in diesem d Kapitel die d-te harmonische Zahl Hd = i=1 1/i (siehe Abschnitt A.1) kurz mit H(d). Als Randbedingung definieren wir H(0) = 0.
35.3 Das Mengenüberdeckungsproblem
1131
Theorem 35.4 Greedy-Set-Cover ist ein ρ(n)-Approximationsalgorithmus mit polynomieller Laufzeit, und ρ(n) = H(max {|S| : S ∈ F}) . Beweis: Wir haben bereits gesehen, dass Greedy-Set-Cover in polynomieller Zeit läuft. Um zu zeigen, dass Greedy-Set-Cover ein ρ(n)-Approximationsalgorithmus ist, ordnen wir jeder durch den Algorithmus ausgewählten Menge die Kosten 1 zu, verteilen diese Kosten über die Elemente, die erstmals überdeckt werden, und verwenden dann diese Kosten, um die gewünschte Beziehung zwischen der Größe der optimalen Mengenüberdeckung C∗ und der Größe der durch den Algorithmus zurückgegebenen Mengenüberdeckung abzuleiten. Sei Si die i-te von Greedy-Set-Cover ausgewählte Teilmenge. Wenn der Algorithmus Si zu C hinzufügt, entstehen die Kosten 1. Wir verteilen diese Kosten gleichmäßig auf die Elemente, die durch Si erstmalig überdeckt werden. Für alle x ∈ X bezeichnen wir die Kosten, die auf das Element x entfallen, mit cx . Jedem Element werden nur einmal Kosten zugeordnet, nämlich dann, wenn es das erste Mal überdeckt wird. Wenn x erstmalig überdeckt wird, dann gilt cx =
1 . |Si − (S1 ∪ S2 ∪ · · · ∪ Si−1 )|
Jeder Schritt des Algorithmus weist eine Kosteneinheit zu, sodass |C| = cx
(35.9)
x∈X
gilt. Jedes Element x ∈ X is in mindestens einer Menge der optimalen Überdeckung C∗ und so haben wir cx ≥ cx . (35.10) S∈C∗ x∈S
x∈X
Kombinieren wir die Gleichung (35.9) und die Ungleichung (35.10), so erhalten wir |C| ≤ cx . (35.11) S∈C∗ x∈S
Der Rest des Beweises beruht auf der folgenden Ungleichung, die wir gleich beweisen wollen. Für jede Menge S aus der Familie F gilt cx ≤ H(|S|) . (35.12) x∈S
Aus den Ungleichungen (35.11) und (35.12) folgt |C| ≤ H(|S|) S∈C∗ ∗
≤ |C | · H(max {|S| : S ∈ F}) ,
1132
35 Approximationsalgorithmen
womit das Theorem bewiesen ist. Es ist nur noch zu zeigen, dass die Ungleichung (35.12) gilt. Betrachten Sie eine beliebige Menge S ∈ F und ein i = 1, 2, . . . , |C|, und sei ui = |S − (S1 ∪ S2 ∪ · · · ∪ Si )| die Anzahl der Elemente aus S, die noch nicht überdeckt sind, nachdem der Algorithmus die Mengen S1 , S2 , . . . , Si ausgewählt hat. Wir definieren u0 = |S| gleich der Anzahl der Elemente aus S, die anfangs alle nicht überdeckt sind. Sei k der kleinste Index, für den uk = 0 gilt, sodass jedes Element aus S durch mindestens eine der Mengen S1 , S2 , . . . , Sk überdeckt ist und mindestens ein Element aus S nicht durch S1 ∪ S2 ∪ · · · ∪ Sk−1 überdeckt ist. Dann gilt ui−1 ≥ ui und ui−1 − ui Elemente aus S werden erstmalig durch Si überdeckt (i = 1, 2, . . . , k). Somit gilt
cx =
k
(ui−1 − ui ) ·
i=1
x∈S
1 . |Si − (S1 ∪ S2 ∪ · · · ∪ Si−1 )|
Beachten Sie, dass |Si − (S1 ∪ S2 ∪ · · · ∪ Si−1 )| ≥ |S − (S1 ∪ S2 ∪ · · · ∪ Si−1 )| = ui−1 gilt, da die Greedy-Auswahl von Si sicherstellt, dass S nicht mehr neue Elemente überdecken kann als Si (anderenfalls hätte der Algorithmus S und nicht Si ausgewählt). Folglich erhalten wir
cx ≤
k
(ui−1 − ui ) ·
i=1
x∈S
1 ui−1
.
Wir schätzen diese Größe nun folgendermaßen ab:
k cx ≤ (ui−1 − ui ) · i=1
x∈S
=
1 ui−1
ui−1 k i=1 j=ui
1 u +1 i−1
ui−1 k 1 ≤ j i=1 j=u +1 i
(wegen j ≤ ui−1 )
35.3 Das Mengenüberdeckungsproblem
1133
⎞ ui 1 1 ⎠ ⎝ − = j j i=1 j=1 j=1 k
=
k
⎛
ui−1
(H(ui−1 ) − H(ui ))
i=1
= = = =
H(u0 ) − H(uk ) H(u0 ) − H(0) H(u0 ) H(|S|) ,
(da die Summe eine Teleskopreihe ist) (wegen H(0) = 0)
womit der Beweis von Ungleichung (35.12) abgeschlossen ist.
Korollar 35.5 Greedy-Set-Cover ist ein (ln |X| + 1)-Approximationsalgorithmus mit polynomieller Laufzeit. Beweis: Das Korollar folgt aus der Ungleichung (A.14) und Theorem 35.4.
In einigen Anwendungen ist max {|S| : S ∈ F} eine kleine Konstante, sodass die von Greedy-Set-Cover zurückgegebene Lösung höchstens um eine kleine Konstante größer als die optimale ist. Eine solche Anwendung liegt vor, wenn die gerade besprochene Heuristik eine approximative Knotenüberdeckung für einen Graphen bestimmt, dessen Knoten höchstens den Grad 3 haben. In diesem Fall ist die von Greedy-Set-Cover gefundene Lösung nicht mehr als H(3) = 11/6-mal so groß wie eine optimale Lösung – eine Performanz, die etwas besser ist als die von Approx-Vertex-Cover.
Übungen 35.3-1 Betrachten Sie jedes der folgenden Wörter als eine Menge von Buchstaben: {arid, dash, drain, heard, lost, nose, shun, slate, snare, thread}. Zeigen Sie, welche Mengenüberdeckung Greedy-Set-Cover erzeugt, wenn wir Mehrdeutigkeiten jeweils zugunsten des Wortes auflösen, das als erstes im Wörterbuch vorkommt. 35.3-2 Zeigen Sie, dass das zum Mengenüberdeckungsproblem korrespondierende Entscheidungsproblem NP-vollständig ist, indem Sie das Knotenüberdeckungsproblem darauf reduzieren. 35.3-3 Zeigen Sie, wie wir den Algorithmus Greedy-Set-Cover so implementieren können, dass er in Zeit O S∈F |S| läuft.
1134
35 Approximationsalgorithmen
35.3-4 Zeigen Sie, dass die folgende schwächere Form von Theorem 35.4 trivialerweise erfüllt ist: |C| ≤ |C∗ | max {|S| : S ∈ F} . 35.3-5 Greedy-Set-Cover kann in Abhängigkeit davon, wie wir in Zeile 4 die Mehrdeutigkeiten auflösen, eine Reihe unterschiedlicher Lösungen zurückgeben. Geben Sie eine Prozedur Bad-Set-Cover-Instance(n) an, die eine n-elementige Instanz des Mengenüberdeckungsproblems zurückgibt, für die Greedy-Set-Cover in Abhängigkeit davon, wie wir in Zeile 4 Mehrdeutigkeiten auflösen, eine Anzahl unterschiedlicher Lösungen zurückgeben kann, die exponentiell in n ist.
35.4
Randomisierung und lineare Programmierung
In diesem Abschnitt untersuchen wir zwei nützliche Methoden für den Entwurf von Approximationsalgorithmen: Randomisierung und lineare Programmierung. Wir werden einen einfachen randomisierten Algorithmus für eine Optimierungsversion der 3-CNFErfüllbarkeit angeben und dann mithilfe linearer Programmierung einen Approximationsalgorithmus für eine gewichtete Version des Knotenüberdeckungsproblems entwerfen. Dieser Abschnitt streift nur die Oberfläche dieser beiden mächtigen Methoden. In den Kapitelbemerkungen sind Referenzen angeben, über die Sie sich zu diesem Gebiet vertieft informieren können.
Ein randomisierter Approximationsalgorithmus für die MAX-3CNF-Erfüllbarkeit Wie manche randomisierte Algorithmen exakte Lösungen berechnen können, berechnen manche randomisierte Algorithmen approximative Lösungen. Wir sagen, dass ein randomisierter Algorithmus für ein Problem ein Approximationsverhältnis von ρ(n) hat, falls für beliebige Eingabegrößen n die erwarteten Kosten C der durch den randomisierten Algorithmus erzeugten Lösung innerhalb eines Faktors ρ(n) der Kosten C ∗ einer optimalen Lösung liegen: C C∗ max , ≤ ρ(n) . (35.13) C∗ C Wer bezeichnen einen randomisierten Algorithmus, der ein Approximationsverhältnis von ρ(n) erreicht, als einen randomisierten ρ(n)-Approximationsalgorithmus. Mit anderen Worten formuliert, ein randomisierter Approximationsalgorithmus ähnelt von der Definition her einem deterministischen Approximationsalgorithmus, mit dem Unterschied, dass sich das Approximationsverhältnis auf die erwarteten Kosten bezieht. Eine spezielle Instanz der 3-CNF-Erfüllbarkeit, die in Abschnitt 34.4 definiert wurde, kann erfüllbar sein oder auch nicht. Damit sie erfüllbar ist, muss eine Belegung der Variablen existieren, für die jede Klausel zum Wert 1 auswertet. Wenn eine Instanz nicht
35.4 Randomisierung und lineare Programmierung
1135
erfüllbar ist, wollen wir möglicherweise wissen, wie „nahe“ sie an der Erfüllbarkeit ist, d. h. wir wollen eine Belegung der Variablen finden, die so viele Klauseln wie möglich erfüllt. Wir bezeichnen das resultierende Problem als MAX-3-CNF-Erfüllbarkeit. Die Eingabe für dieses Problem ist die gleiche wie für die 3-CNF-Erfüllbarkeit und das Ziel ist es, eine Belegung zurückzugeben, die die Anzahl der erfüllten Klauseln maximiert. Wir zeigen hier, dass das zufällige Setzen jeder Variable auf 1 und 0 mit jeweils der Wahrscheinlichkeit 1/2 zu einem randomisierten 8/7-Approximationsalgorithmus führt. Nach Definition der 3-CNF-Erfüllbarkeit in Abschnitt 34.4 fordern wir, dass jede Klausel aus genau drei verschiedenen Literalen besteht. Außerdem setzen wir voraus, dass keine Klausel sowohl eine Variable als auch ihr Komplement enthält. (In Übung 35.4-1 sollen Sie diese letzte Voraussetzung fallen lassen.) Theorem 35.6 Für eine gegebene Instanz der MAX-3-CNF-Erfüllbarkeit mit n Variablen x1 , x2 , . . . , xn und m Klauseln ist der randomisierte Algorithmus, der jede Variable mit Wahrscheinlichkeit 1/2 auf 1 und mit Wahrscheinlichkeit 1/2 auf 0 setzt, ein randomisierter 8/7-Approximationsalgorithmus. Beweis: Setzen Sie voraus, dass wir jede Variable unabhängig von den übrigen Variablen mit Wahrscheinlichkeit 1/2 auf 1 und mit Wahrscheinlichkeit 1/2 auf 0 gesetzt haben. Für i = 1, 2, . . . , m definieren wir die Indikatorfunktion Yi = I {Klausel i ist erfüllt} , sodass Yi = 1 gilt, solange wir mindestens eines der Literale in der i-ten Klausel auf 1 gesetzt haben. Da kein Literal mehr als einmal in der gleichen Klausel auftritt und da wir vorausgesetzt haben, dass keine Variable und ihr Komplement gemeinsam in der gleichen Klausel vorkommen, sind die Zuweisungen an die drei Literale in jeder Klausel unabhängig voneinander. Eine Klausel ist nur dann nicht erfüllt, wenn alle drei Literale auf 0 gesetzt sind. Daher gilt Pr {Klausel i ist nicht erfüllt} = (1/2)3 = 1/8 bzw. Pr {Klausel i ist erfüllt} = 1 − 1/8 = 7/8 und nach Lemma 5.1 haben wir E [Yi ] = 7/8. Sei Y die Anzahl der insgesamt erfüllten Klauseln, also Y = Y1 + Y2 + · · · + Ym . Dann gilt "m # E [Y ] = E Yi i=1
= =
m i=1 m
E [Yi ]
(wegen der Linearität des Erwartungswertes)
7/8
i=1
= 7m/8 . Offensichtlich ist m eine obere Schranke für die Anzahl der erfüllten Klauseln und folglich ist das Approximationsverhältnis höchstens m/(7m/8) = 8/7.
1136
35 Approximationsalgorithmen
Approximation einer gewichteten Knotenüberdeckung mithilfe linearer Programmierung Beim Problem der Knotenüberdeckung mit minimalem Gewicht ist ein ungerichteter Graph G = (V, E) gegeben, in dem jedem Knoten v ∈ V ein positives Gewicht w(v) zugeordnet ist. Für jede Knotenüberdeckung V ⊆ V definieren wir deren Gewicht w(V ) = v∈V w(v). Das Ziel ist es, eine Knotenüberdeckung mit minimalem Gewicht zu finden. Wir können weder den Algorithmus für ungewichtete Knotenüberdeckungen noch eine zufällige Lösung verwenden, denn beide Methoden können Lösungen zurückgeben, die weit von der jeweils optimalen Lösung entfernt sind. Wir werden jedoch eine untere Schranke für das Gewicht der Knotenüberdeckung mit minimalem Gewicht berechnen, indem wir lineare Programmierung benutzen. Anschließend werden wir diese Lösung „runden“, um damit eine Knotenüberdeckung zu erhalten. Setzen Sie voraus, dass wir jedem Knoten v eine Variable x(v) zuordnen, und lassen Sie uns fordern, dass x(v) entweder gleich 0 oder gleich 1 für alle v ∈ V ist. Wir nehmen v genau dann in die Knotenüberdeckung mit auf, wenn x(v) = 1 gilt. Damit können wir die Nebenbedingung, dass für jede Kante (u, v) wenigstens einer der Knoten u und v zur Knotenüberdeckung gehören muss, als x(u) + x(v) ≥ 1 schreiben. Diese Sichtweise führt auf das folgende ganzzahlige 0-1-Programm: minimiere w(v)x(v) (35.14) v∈V
unter den Nebenbedingungen x(u)+x(v) ≥ 1 für alle (u, v) ∈ E x(v) ∈ {0, 1} für alle v ∈ V .
(35.15) (35.16)
In dem Spezialfall, in dem alle Gewichte w(v) gleich 1 sind, entspricht diese Formulierung der Optimierungsversion des NP-schweren Knotenüberdeckungsproblems. Setzen wir jedoch voraus, dass wir die Bedingung x(v) ∈ {0, 1} fallen lassen und sie durch 0 ≤ x(v) ≤ 1 ersetzen, dann erhalten wir ein lineares Programm, das als LPRelaxation bekannt ist: minimiere w(v)x(v) (35.17) v∈V
unter den Nebenbedingungen x(u)+x(v) ≥ 1 x(v) ≤ 1 x(v) ≥ 0
für alle (u, v) ∈ E für alle v ∈ V für alle v ∈ V .
(35.18) (35.19) (35.20)
Jede zulässige Lösung des 0-1-Programms (35.14)–(35.16) ist auch eine zulässige Lösung des linearen Programms (35.17)–(35.20). Daher ist der Wert einer optimalen Lösung
35.4 Randomisierung und lineare Programmierung
1137
des linearen Programms eine untere Schranke für den Wert der optimalen Lösung des 0-1-Programms und somit auch eine untere Schranke für das optimale Gewicht der Knotenüberdeckung mit minimalem Gewicht. Die folgende Prozedur verwendet die Lösung der LP-Relaxation, um eine approximative Lösung des Problems der Knotenüberdeckung mit minimalem Gewicht zu konstruieren: Approx-Min-Weight-VC(G, w) 1 C =∅ 2 berechne eine optimale Lösung x ¯ des linearen Programms (35.17)–(35.20) 3 for alle v ∈ V 4 if x ¯(v) ≥ 1/2 5 C = C ∪ {v} 6 return C Die Prozedur Approx-Min-Weight-VC arbeitet folgendermaßen. In Zeile 1 wird die Knotenüberdeckung mit der leeren Menge initialisiert. Zeile 2 formuliert das durch (35.17)–(35.20) gegebene lineare Programm und löst dieses. Eine optimale Lösung ordnet jedem Knoten v einen Wert x ¯(v) mit 0 ≤ x¯(v) ≤ 1 zu. Wir verwenden diesen Wert als Richtlinie für die Wahl der Knoten, die in den Zeilen 3–5 zur Knotenüberdeckung hinzugefügt werden. Im Falle x¯(v) ≥ 1/2 fügen wir v zu C hinzu, anderenfalls nicht. Effektiv „runden“ wir jede Variable der Lösung des linearen Programms, die einen gebrochenen Wert hat, auf 0 oder 1, um eine Lösung des 0-1-Programms (35.14)–(35.16) zu erhalten. Abschließend wird in Zeile 6 die Knotenüberdeckung C zurückgegeben. Theorem 35.7 Der Algorithmus Approx-Min-Weight-VC ist ein 2-Approximationsalgorithmus mit polynomieller Laufzeit für das Problem der Knotenüberdeckung mit minimalem Gewicht. Beweis: Da es einen Algorithmus mit polynomieller Laufzeit gibt, der das lineare Programm in Zeile 2 löst, und da die for-Schleife in den Zeilen 3–5 in polynomieller Zeit läuft, ist Approx-Min-Weight-VC ein Algorithmus mit polynomieller Laufzeit. Wir zeigen nun, dass Approx-Min-Weight-VC ein 2-Approximationsalgorithmus ist. Sei C ∗ eine optimale Lösung des Problems der Knotenüberdeckung mit minimalem Gewicht und z ∗ der Wert einer optimalen Lösung des linearen Programms (35.17)–(35.20). Da eine optimale Knotenüberdeckung eine zulässige Lösung des linearen Programms ist, muss z ∗ eine untere Schranke für w(C ∗ ) sein; es gilt also z ∗ ≤ w(C ∗ ) .
(35.21)
Wir behaupten nun, dass wir durch das Runden der gebrochenen Werte der Variablen x ¯(v) eine Menge C erzeugen, die eine Knotenüberdeckung bildet und die Ungleichung w(C) ≤ 2z ∗ erfüllt. Um zu sehen, dass C eine Knotenüberdeckung ist, betrachten wir eine beliebige Kante (u, v) ∈ E. Wegen der Nebenbedingung (35.18) wissen wir, dass
1138
35 Approximationsalgorithmen
x(u) + x(v) ≥ 1 gilt, was bedeutet, dass wenigstens eine der beiden Variablen x ¯(u) und x¯(v) mindestens gleich 1/2 ist. Daher ist mindestens einer der Knoten u und v in der Knotenüberdeckung enthalten, sodass jede Kante überdeckt wird. Wir betrachten nun das Gewicht der Überdeckung. Es gilt z∗ =
v∈V
≥
w(v) x ¯(v)
w(v) x ¯(v)
v∈V :¯ x(v)≥1/2
≥
w(v) ·
v∈V :¯ x(v)≥1/2
=
w(v) ·
v∈C
=
1 2
1 2
1 w(v) 2 v∈C
1 = w(C) . 2
(35.22)
Kombinieren wir die Ungleichungen (35.21) und (35.22), so erhalten wir w(C) ≤ 2z ∗ ≤ 2w(C ∗ ) , womit bewiesen ist, dass Approx-Min-Weight-VC ein 2-Approximationsalgorithmus ist.
Übungen 35.4-1 Zeigen Sie, dass, selbst wenn wir einer Klausel erlauben, dass sie eine Variable und ihr Komplement gleichzeitig enthalten darf, das zufällige Setzen jeder Variable auf 1 mit der Wahrscheinlichkeit 1/2 bzw. auf 0 mit der Wahrscheinlichkeit 1/2 immer noch zu einem randomisierten 8/7-Approximationsalgorithmus führt. 35.4-2 Das Problem der MAX-CNF-Erfüllbarkeit ist gleich dem Problem der MAX-3-CNF-Erfüllbarkeit, mit dem einzigen Unterschied, dass eine Klausel nicht mehr genau 3 Literale enthalten muss. Geben Sie einen randomisierten 2-Approximationsalgorithmus für das Problem der MAX-CNF-Erfüllbarkeit an. 35.4-3 Beim MAX-CUT-Problem ist ein ungewichteter ungerichteter Graph G = (V, E) gegeben. Wir definieren einen Schnitt (S, V − S) wie in Kapitel 23 und das Gewicht eines Schnitts als die Anzahl der Kanten, die den Schnitt
35.5 Das Teilsummenproblem
1139
schneiden. Das Ziel ist die Bestimmung eines Schnitts mit maximalem Gewicht. Setzen Sie voraus, dass wir jeden Knoten v zufällig und unabhängig mit Wahrscheinlichkeit 1/2 in S und mit Wahrscheinlichkeit 1/2 in V − S aufnehmen. Zeigen Sie, dass dieser Algorithmus ein randomisierter 2-Approximationsalgorithmus ist. 35.4-4 Zeigen Sie, dass die Nebenbedingungen (35.19) redundant in dem Sinne sind, dass, wenn wir sie aus dem linearen Programm (35.17)–(35.20) entfernen, jede optimale Lösung des resultierenden linearen Programms die Ungleichung x(v) ≤ 1 für alle v ∈ V erfüllen muss.
35.5
Das Teilsummenproblem
Rufen Sie sich aus Abschnitt 34.5.5 in Erinnerung, dass eine Instanz des Teilsummenproblems ein Paar (S, t) ist, wobei S eine Menge {x1 , x2 , . . . , xn } positiver ganzer Zahlen und t eine positive ganze Zahl ist. Dieses Entscheidungsproblem stellt die Frage, ob eine Teilmenge von S existiert, deren Elemente sich genau zu dem Zielwert t addieren. Wie wir in Abschnitt 34.5.5 gesehen haben, ist dieses Problem NP-vollständig. Das zu diesem Entscheidungsproblem gehörige Optimierungsproblem kommt in praktischen Anwendungen vor. Bei dem Optimierungsproblem wollen wir eine Teilmenge von {x1 , x2 , . . . , xn } finden, die so groß wie möglich, aber nicht größer als t ist. Beispielsweise könnte es sein, dass wir einen LKW haben, der nicht mit mehr als t Kilogramm beladen werden darf. Wir wollen den LKW so schwer wie möglich beladen, ohne die vorgegebene Grenze zu überschreiten. In diesem Abschnitt stellen wir einen Algorithmus mit exponentieller Laufzeit vor, der den optimalen Wert für dieses Optimierungsproblem berechnet, und zeigen dann, wie wir den Algorithmus so modifizieren können, dass er ein Approximationsschema mit vollständig polynomieller Laufzeit ist. (Rufen Sie sich in Erinnerung, dass ein solches Approximationsschema eine Laufzeit hat, die polynomiell in 1/ und in der Eingabegröße ist.)
Ein exakter Algorithmus mit exponentieller Laufzeit Setzen Sie voraus, dass wir für jede Teilmenge S von S die Summe der Elemente von S berechnet haben und dann unter den Teilmengen, deren jeweilige Summe kleiner gleich t ist, diejenige ausgewählt haben, deren Summe am nächsten an t liegt. Offensichtlich wird dieser Algorithmus die optimale Lösung zurückgeben, er kann dazu allerdings exponentielle Zeit benötigen. Um diesen Algorithmus zu implementieren, könnten wir eine iterative Prozedur verwenden, die in der i-ten Iteration die Summen aller Teilmengen von {x1 , x2 , . . . , xi } berechnet, wobei sie als Ausgangspunkt die Summen aller Teilmengen von {x1 , x2 , . . . , xi−1 } benutzt. Wenn wir dies tun, so erkennen wir, dass eine Teilmenge S mit einer Summe, die t überschreitet, nicht mehr betrachtet werden muss, da keine Obermenge von S eine optimale Lösung sein kann. Wir geben nun eine Implementierung dieser Strategie an.
1140
35 Approximationsalgorithmen
Die Prozedur Exact-Subset-Sum erhält als Eingabe eine Menge S = {x1 , x2 , . . . , xn } und einen Zielwert t; wir werden uns gleich den Pseudocode anschauen. Diese Prozedur berechnet iterativ Li , die Liste der Summen aller Teilmengen von {x1 , . . . , xi }, die t nicht überschreiten, und gibt den maximalen Wert von Ln zurück. Ist L eine Liste mit positiven ganzen Zahlen und x eine weitere positive ganze Zahl, dann bezeichnen wir mit L + x die Liste der ganzen Zahlen, die aus L abgeleitet ist, indem jedes Element aus L um x erhöht wird. Ist beispielsweise L = 1, 2, 3, 5, 9, dann ist L + 2 = 3, 4, 5, 7, 11. Wir verwenden diese Notation auch für Mengen, d. h. es gilt S + x = {s + x : s ∈ S} . Des Weiteren verwenden wir eine Hilfsprozedur Merge-Lists(L, L ), die die sortierte Liste zurückgibt, die durch die Verschmelzung der beiden Eingabelisten L und L entsteht, wobei doppelte Werte entfernt werden. Wie die Prozedur Merge (siehe Abschnitt 2.3.1) läuft Merge-Lists in Zeit O(|L| + |L |). (Wir verzichten an dieser Stelle auf den Pseudocode für Merge-Lists.) Exact-Subset-Sum(S, t) 1 n = |S| 2 L0 = 0 3 for i = 1 to n 4 Li = Merge-Lists(Li−1 , Li−1 + xi ) 5 entferne jedes Element aus Li , das größer als t ist 6 return das größte Element aus Ln Um zu verstehen, wie Exact-Subset-Sum arbeitet, lassen Sie uns die Menge aller Werte, die wir erhalten können, indem wir eine (möglicherweise leere) Teilmenge von {x1 , x2 , . . . , xi } auswählen und ihre Elemente aufsummieren, mit Pi bezeichnen. Für S = {1, 4, 5} ist beispielsweise P1 = {0, 1} , P2 = {0, 1, 4, 5} , P3 = {0, 1, 4, 5, 6, 9, 10} . Mit der Gleichung Pi = Pi−1 ∪ (Pi−1 + xi )
(35.23)
können wir durch Induktion über i beweisen (siehe Übung 35.5-1), dass die Liste Li eine sortierte Liste ist, die alle Elemente von Pi enthält, deren Wert nicht größer als t ist. Da die Länge von Li bis zu 2i sein kann, ist Exact-Subset-Sum im Allgemeinen ein Algorithmus mit exponentieller Laufzeit. In Spezialfällen, in denen t polynomiell in |S| ist oder alle Zahlen aus S durch ein Polynom in |S| beschränkt sind, ist seine Laufzeit allerdings polynomiell.
35.5 Das Teilsummenproblem
1141
Ein Approximationsschema mit vollständig polynomieller Laufzeit Wir können für das Teilsummenproblem ein Approximationsschema mit vollständig polynomieller Laufzeit ableiten, indem wir jede Liste Li „stutzen“, nachdem sie erzeugt wurde. Die Idee, die hinter dem Stutzen steht, besteht darin, dass, wenn zwei Werte aus L dicht beieinander liegen, wir beide nicht explizit aufbewahren müssen, da wir ja nur eine approximative Lösung bestimmen wollen. Zur Realisierung dieser Idee verwenden wir einen Stutzparameter δ mit 0 < δ < 1. Wenn wir eine Liste L um δ stutzen, entfernen wir so viele Elemente wie möglich aus L, sodass, wenn L die durch das Stutzen erzeugte Liste ist, es für jedes aus L entfernte Element y noch ein Element z in L gibt, das y approximiert, d. h. für das y ≤z≤y 1+δ
(35.24)
gilt. Wir können uns ein solches Element z als „Repräsentanten“ von y in der neuen Liste L denken. Jedes entfernte Element y wird durch ein z repräsentiert, das die Ungleichung (35.24) erfüllt. Beispielsweise können wir für δ = 1/10 die Liste L = 10, 11, 12, 15, 20, 21, 22, 23, 24, 29 auf L = 10, 12, 15, 20, 23, 29 stutzen, in der die entfernten Werte 11 durch 10, 21 und 22 durch 20 und 24 durch 23 repräsentiert werden. Da jedes Element der gestutzten Liste auch ein Element der ursprünglichen Liste ist, kann das Stutzen die Anzahl der zu verwaltetenden Elemente sehr verringern, wobei für jedes entfernte Element ein nahe (leicht kleinerer) Repräsentant in der Liste verbleibt. Die folgende Prozedur stutzt die Liste L = y1 , y2 , . . . , ym in Zeit Θ(m), wobei L und δ gegeben sind und vorausgesetzt wird, dass L in monoton steigender Ordnung sortiert vorliegt. Die Ausgabe der Prozedur ist eine gestutzte sortierte Liste. Trim(L, δ) 1 sei m die Länge von L 2 L = y1 3 letzter = y1 4 for i = 2 to m 5 if yi > letzter · (1 + δ) 6 hänge yi an das Ende von L 7 letzter = yi 8 return L
// yi ≥ letzter , da L sortiert ist
Die Prozedur betrachtet die Elemente von L in monoton steigender Reihenfolge. Eine Zahl wird nur dann in die zurückgegebene Liste L an das Ende angefügt, wenn sie das
1142
35 Approximationsalgorithmen
erste Element von L ist oder wenn sie sich nicht durch die zuletzt in L aufgenommene Zahl repräsentieren läßt. Mit der Prozedur Trim können wir unser Approximationsschema folgendermaßen konstruieren. Die Prozedur verwendet als Eingabe eine Menge S = {x1 , x2 , . . . , xn } von n ganzen Zahlen (in beliebiger Reihenfolge), einen ganzzahligen Zielwert t und einen „Approximationsparameter“ mit 0 1 + /2n erfüllt sein. Das heißt, sie müssen sich mindestens um einen Faktor 1 + /2n unterscheiden. Jede Liste enthält daher den Wert 0, möglicherweise den Wert
1 und bis zu log1+/2n t zusätzliche Werte. Die Anzahl der Elemente in jeder Liste Li beträgt somit höchstens ln t +2 ln(1 + /2n) 2n(1 + /2n) ln t ≤ +2 3n ln t +2 <
log1+/2n t + 2 =
(nach Ungleichung (3.17)) (nach Ungleichung (35.25)) .
Diese Schranke ist polynomiell in der Größe der Eingabe – die gleich der Anzahl lg t der Bits, die zur Darstellung von t nötig sind, plus der Anzahl der Bits, die nötig sind, um S darzustellen, die ebenfalls polynomiell in n ist – und in 1/. Da die Laufzeit von Approx-Subset-Sum polynomiell in den Längen der Li ist, können wir schlussfolgern, dass Approx-Subset-Sum ein Approximationsschema mit vollständig polynomieller Laufzeit ist.
Übungen 35.5-1 Beweisen Sie die Gleichung (35.23). Zeigen Sie anschließend, dass Li nach der Ausführung von Zeile 5 der Prozedur Exact-Subset-Sum eine sortierte Liste ist, die alle Elemente aus Pi enthält, deren Wert nicht größer als t ist. 35.5-2 Beweisen Sie mit Induktion nach i die Ungleichung (35.26). 35.5-3 Beweisen Sie die Ungleichung (35.29).
Problemstellungen zu Kapitel 35
1145
35.5-4 Wie würden Sie das in diesem Abschnitt vorgestellte Approximationsschema modifizieren, um eine gute Näherung für den kleinsten Wert, der nicht kleiner als t ist, zu finden, der eine Summe einer Teilmenge der gegebenen Eingabeliste ist? 35.5-5 Modifizieren Sie die Prozedur Approx-Subset-Sum derart, dass sie auch die Teilmenge von S zurückgibt, deren Summe gleich dem Wert z ∗ ist.
Problemstellungen 35-1 Kistenpacken (engl.: bin packing) Setzen Sie voraus, dass wir eine Menge von n Objekten haben, wobei die Größe si des i-ten Objekts die Ungleichung 0 < si < 1 erfüllt. Wir wollen alle Objekte in eine minimale Anzahl von Kisten jeweils mit Größe 1 packen. Jede Kiste kann eine beliebige Teilmenge der Objekte aufnehmen, deren Gesamtgröße 1 nicht überschreitet. a. Beweisen Sie, dass das Problem, die minimale Anzahl der erforderlichen Kisten zu bestimmen, NP-schwer ist. (Hinweis: Leiten Sie dies aus dem Teilmengensummenproblem ab.) Die First-Fit-Heuristik schaut sich die Objekte der Reihe nach an und platziert jedes n Objekt in die erste Kiste, in die es hineinpasst. Sei im Folgenden S = i=1 si . b. Zeigen Sie, dass die optimale Anzahl der erforderlichen Kisten mindestens S ist. c. Zeigen Sie, dass die First-Fit-Heuristik höchstens eine Kiste zu weniger als zur Hälfte füllt. d. Beweisen Sie, dass die Anzahl der durch die First-Fit-Heuristik verwendeten Kisten niemals größer als 2S ist. e. Beweisen Sie, dass die First-Fit-Heuristik ein Approximationsverhältnis von 2 hat. f. Geben Sie eine effiziente Implementierung der First-Fit-Heuristik an und analysieren Sie deren Laufzeit. 35-2 Approximation der Größe einer maximalen Clique Sei G = (V, E) ein ungerichteter Graph. Für jedes k ≥ 1 sei G(k) der ungerichtete Graph (V (k) , E (k) ), wobei V (k) die Menge aller geordneten k-Tupel von Knoten aus V ist und E (k) so definiert ist, dass (v1 , v2 , . . . , vk ) genau dann zu (w1 , w2 , . . . , wk ) adjazent ist, wenn für alle i mit 1 ≤ i ≤ k entweder der Knoten vi zu wi in G adjazent ist oder vi = wi gilt. a. Beweisen Sie, dass die Größe der maximalen Clique in G(k) gleich der k-ten Potenz der Größe der maximalen Clique in G ist.
1146
35 Approximationsalgorithmen b. Zeigen Sie, dass, wenn es ein Approximationsalgorithmus mit konstantem Approximationsverhältnis für das Problem, eine maximale Clique zu berechnen, gibt, dann gibt es ein Approximationsschema mit polynomieller Laufzeit für das Problem.
35-3 Gewichtetes Mengenüberdeckungsproblem Setzen Sie voraus, dass wir das Mengenüberdeckungsproblem dahingehend verallgemeinern, dass jeder Menge Si aus der Familie F ein Gewicht wi zugeordnet ist. Das Gewicht einer Überdeckung C ist Si ∈C wi . Wir wollen eine Überdeckung mit minimalem Gewicht bestimmen. (Abschnitt 35.3 behandelt den Fall, dass wi = 1 für alle i gilt.) Zeigen Sie, wie wir die Greedy-Heuristik für die Mengenüberdeckung so in natürlicher Weise verallgemeinern können, dass sie eine approximative Lösung für jede Instanz des gewichteten Mengenüberdeckungsproblems liefert. Zeigen Sie, dass Ihre Heuristik ein Approximationsverhältnis von H(d) hat, wobei d die maximale Größe aller Mengen Si ist. 35-4 Maximales Matching Rufen Sie sich in Erinnerung, dass ein Matching eines ungerichteten Graphen G eine Menge von Kanten ist mit der Eigenschaft, dass keine zwei Kanten der Menge mit dem selben Knoten inzident sind. In Abschnitt 26.3 haben wir gesehen, wie wir ein maximales Matching in einem bipartiten Graphen bestimmen können. In dieser Problemstellung suchen wir nach Matchings von allgemeinen ungerichteten Graphen (d. h. in Graphen, die nicht unbedingt bipartit sind). a. Ein lokal maximales Matching ist ein Matching, das keine echte Teilmenge eines anderen Matchings ist. Zeigen Sie, dass ein lokal maximales Matching kein maximales Matching sein muss, indem Sie einen ungerichteten Graphen G und ein lokal maximales Matching M von G angeben, das kein maximales Matching ist. (Hinweis: Sie können einen solchen Graphen mit nur 4 Knoten finden.) b. Betrachten Sie einen ungerichteten Graphen G = (V, E). Geben Sie einen Greedy-Algorithmus mit Laufzeit O(E) an, der ein lokal maximales Matching in G bestimmt. In dieser Problemstellung werden wir uns auf einen Approximationsalgorithmus für maximales Matching mit polynomieller Laufzeit konzentrieren. Während der schnellste bekannte Algorithmus für maximales Matching superlineare (aber polynomielle) Zeit benötigt, hat der hier diskutierte Algorithmus eine lineare Laufzeit. Sie werden zeigen, dass der Greedy-Linearzeit-Algorithmus für lokal maximales Matching aus Teil (b) ein 2-Approximationsalgorithmus für maximales Matching ist. c. Zeigen Sie, dass die Größe eines maximalen Matchings in G eine untere Schranke für die Größe jeder Knotenüberdeckung von G ist. d. Betrachten Sie ein lokal maximales Matching M in G = (V, E). Sei T = {v ∈ V : es gibt eine zu v inzidente Kante aus M } .
Problemstellungen zu Kapitel 35
1147
Was können Sie über den Teilgraphen von G aussagen, der durch diejenigen Knoten von G erzeugt wird, die nicht zu T gehören? e. Folgern Sie aus Teil (d), dass 2 |M | die Größe einer Knotenüberdeckung von G ist. f. Beweisen Sie unter Verwendung der Teile (c) und (e), dass der GreedyAlgorithmus aus Teil (b) ein 2-Approximationsalgorithmus für maximales Matching ist. 35-5 Ablaufplanung auf parallelen Maschinen Beim Problem der Ablaufplanung auf parallelen Maschinen sind n Jobs J1 , J2 , . . . , Jn gegeben, wobei jedem Job Jk eine nichtnegative Verarbeitungszeit pk zugeordnet ist. Wir haben zudem m identische Maschinen M1 , M2 , . . . , Mm gegeben. Jeder Job kann auf jeder Maschine laufen. Ein Ablaufplan spezifiziert für jeden Job Jk die Maschine, auf der er abgearbeitet werden soll, und die Zeitspanne, in der er abgearbeitet wird. Jeder Job Jk muss auf einer Maschine Mi während pk aufeinander folgenden Zeiteinheiten laufen. Während dieser Zeit kann kein anderer Job auf Mi laufen. Sei Ck die Fertigstellungszeit des Jobs Jk , d. h. der Zeitpunkt, zu dem der Job Jk abgearbeitet ist. Für einen gegebenen Ablaufplan definieren wir Cmax = max1≤j≤n Cj als dessen Verarbeitungsdauer. Das Ziel besteht darin, einen Ablaufplan mit minimaler Verarbeitungsdauer zu finden. Setzen Sie beispielsweise voraus, dass wir zwei Maschinen M1 und M2 und vier Jobs J1 , J2 , J3 , J4 mit p1 = 2, p2 = 12, p3 = 4 und p4 = 5 haben. Dann ist ein möglicher Ablaufplan, auf Maschine M1 Job J1 , gefolgt von Job J2 laufen zu lassen und auf Maschine M2 Job J4 gefolgt von Job J3 . Für diesen Ablaufplan gilt C1 = 2, C2 = 14, C3 = 9, C4 = 5 und Cmax = 14. Ein optimaler Ablaufplan verarbeitet Job J2 auf Maschine M1 und die Jobs J1 , J3 und J4 auf Maschine M2 . Für diesen Ablaufplan gilt C1 = 2, C2 = 12, C3 = 6, C4 = 11 und Cmax = 12. Für ein gegebenes Problem der Ablaufplanung auf parallelen Maschinen bezeich∗ nen wir die Fertigstellungszeit eines optimalen Ablaufplans mit Cmax . a. Zeigen Sie, dass die optimale Verarbeitungsdauer mindestens so groß ist wie die größte Verarbeitungszeit, d. h. es gilt ∗ Cmax ≥ max pk . 1≤k≤n
b. Zeigen Sie, dass die optimale Verarbeitungsdauer mindestens so groß ist wie die mittlere Maschinenlast, d. h. es gilt 1 ∗ Cmax ≥ pk . m 1≤k≤n
Setzen Sie voraus, dass wir den folgenden Greedy-Algorithmus für die Ablaufplanung auf parallelen Maschinen verwenden: immer wenn eine Maschine frei ist, weisen wir ihr einen beliebigen Job zu, der noch nicht zugewiesen wurde. c. Schreiben Sie eine Prozedur in Pseudocode, der diesen Greedy-Algorithmus implementiert. Wie hoch die Laufzeit Ihres Algorithmus?
1148
35 Approximationsalgorithmen d. Zeigen Sie, dass für den Ablaufplan, den der Greedy-Algorithmus zurückgibt, 1 pk + max pk Cmax ≤ 1≤k≤n m 1≤k≤n
gilt. Schlussfolgern Sie, dass dieser Algorithmus ein 2-Approximationsalgorithmus mit polynomieller Laufzeit ist. 35-6 Approximation eines maximalen Spannbaums Sei G = (V, E) ein ungerichteter Graph mit paarweise verschiedenen Kantengewichten w(u, v) auf den Kanten (u, v) ∈ E. Für jeden Knoten v ∈ V sei max(v) die zu dem Knoten v inzidente Kante mit dem größten Gewicht, also max(v) = argmax(u,v)∈E {w(u, v)}. Sei SG = {max(v) : v ∈ V } die Menge dieser Kanten über alle Knoten des Graphen und sei TG der Spannbaum von G mit maximalem Gewicht. Definieren Sie für jede Teilmenge E ⊆ E von Kanten w(E ) = (u,v)∈E w(u, v). a. Geben Sie ein Beispiel für einen Graphen an, der mindestens 4 Knoten enthält und für den SG = TG gilt. b. Geben Sie ein Beispiel für einen Graphen an, der mindestens 4 Knoten enthält und für den SG = TG gilt. c. Beweisen Sie, dass SG ⊆ TG für jeden Graphen G gilt. d. Beweisen Sie, dass w(TG ) ≥ w(SG )/2 für jeden Graphen G gilt. e. Geben Sie einen Algorithmus mit Laufzeit O(V + E) an, der eine 2-Approximation des maximalen Spannbaums berechnet. 35-7 Ein Approximationsalgorithmus für das 0-1-Rucksackproblem Rufen Sie sich das Rucksackproblem aus Abschnitt 16.2 in Erinnerung. Es gibt n Objekte, wobei das i-te Objekt vi Dollar wert ist und ein Gewicht von wi Pfund hat. Wir haben zudem einen Rucksack gegeben, der Objekte bis zu einem Gesamtgewicht von W Pfund aufnehmen kann. Wir setzen zudem voraus, dass jedes einzelne Gewicht wi höchstens W beträgt und die Objekte nach ihren Werten monoton fallend durchnummeriert sind: v1 ≥ v2 ≥ · · · ≥ vn . In dem 0-1-Rucksackproblem wollen wir eine Teilmenge der Objekte bestimmen, deren Gesamtgewicht höchstens W ist und deren Gesamtwert unter dieser Nebenbedingung maximal ist. Das gebrochene Rucksackproblem ist wie das 0-1-Rucksackproblem definiert mit dem einzigen Unterschied, dass wir auch Bruchteile von Objekten in den Rucksack legen dürfen und nicht darauf beschränkt sind, entweder jeweils ein ganzes Objekt oder nichts von diesem Objekt zu nehmen. Wenn wir einen Bruchteil xi des Objektes i mit 0 ≤ xi ≤ 1 nehmen, tragen wir mit xi wi zum Gewicht des Rucksacks bei und erhalten einen Wert in Höhe von xi vi . Unser Ziel besteht darin, einen 2-Approximationsalgorithmus für das 0-1-Rucksackproblem mit polynomieller Laufzeit zu entwerfen. Um einen Polynomialzeit-Algorithmus zu entwerfen, betrachten wir eingeschränkte Instanzen des 0-1-Rucksackproblems. Ist eine Instanz I des Rucksackproblems
Kapitelbemerkungen zu Kapitel 35
1149
gegeben, konstruieren wir die eingeschränkten Instanzen Ij mit j = 1, 2, . . . , n, indem wir die Objekte 1, 2, . . . , j − 1 entfernen und verlangen, dass die Lösung das Objekt j enthält (d. h. das ganze Objekt j sowohl im gebrochenen als auch im 0-1-Rucksackproblem). Es werden keine Objekte in der Instanz I1 entfernt. Für Instanz Ij sei Pj eine optimale Lösung des 0-1-Problems und Qj eine optimale Lösung des gebrochenen Problems. a. Erklären Sie, warum eine optimale Lösung für die Instanz I des 0-1-Rucksackproblems aus {P1 , P2 , . . . , Pn } ist. b. Beweisen Sie, dass wir eine optimale Lösung Qj für die Instanz Ij des gebrochenen Problems finden, indem wir Objekt j in die Lösung aufnehmen und dann den Greedy-Algorithmus verwenden, bei dem wir in jedem Schritt so viel wie möglich von dem noch nicht gewählten Objekt aus der Menge {j + 1, j + 2, . . . , n} nehmen, das einen maximalen Wert vi /wi pro Pfund aufweist. c. Beweisen Sie, dass wir immer eine optimale Lösung Qj für die Instanz Ij des gebrochenen Problems konstruieren können, die höchstens ein Objekt nur zum Teil enthält. Das heißt, von jedem Objekt mit Ausnahme von einem nehmen wir jeweils entweder alles oder nichts in den Rucksack auf. d. Sei eine optimale Lösung Qj für die Instanz Ij des gebrochenen Problems gegeben, konstruieren Sie aus Qj eine Lösung Rj , indem Sie alle nur zum Teil enthaltenen Objekte aus Qj entfernen. Sei v(S) der Gesamtwert der Objekte, die in einer Lösung S in den Rucksack aufgenommen werden. Beweisen Sie v(Rj ) ≥ v(Qj )/2 ≥ v(Pj )/2. e. Geben Sie einen Polynomialzeit-Algorithmus an, der eine Lösung mit maximalem Wert aus der Menge {R1 , R2 , . . . , Rn } zurückgibt, und beweisen Sie, dass Ihr Algorithmus ein 2-Approximationsalgorithmus für das 0-1Rucksackproblem mit polynomialer Laufzeit ist.
Kapitelbemerkungen Obwohl Methoden, die nicht notwendigerweise exakte Lösungen berechnen, seit tausenden von Jahren bekannt sind (zum Beispiel Methoden, die den Wert von π approximieren), ist der Begriff eines Approximationsalgorithmus relativ neu. Hochbaum [172] schreibt Garey, Graham und Ullman [128] sowie Johnson [190] die Formalisierung des Konzeptes eines Approximationsalgorithmus mit polynomieller Laufzeit zu. Der erste derartige Algorithmus wird häufig Graham [149] zugeschrieben. Seit dieser ersten Arbeit wurden tausende von Approximationsalgorithmen für eine große Zahl verschiedenartiger Probleme entworfen, und es gibt eine Menge Literatur zu diesem Gebiet. Neuere Arbeiten von Ausiello u. a. [26], Hochbaum [172] und Vazirani [345] beschäftigen sich ausschließlich mit Approximationsalgorithmen, was auch für die Übersichten von Shmoys [315] sowie Klein und Young [207] gilt. Verschiedene andere Publikationen, wie die von Garey und Johnson [129] oder von Papadimitriou und Steiglitz [271], befassen sich ebenfalls in großem Umfang mit Approximationsalgorithmen.
1150
35 Approximationsalgorithmen
Lawler, Lenstra, Rinnooy Kan und Shmoys [225] bieten eine ausführliche Diskussion zu Approximationsalgorithmen für das Problem des Handelsreisenden. Papadimitriou und Steiglitz schreiben den Algorithmus Approx-Vertex-Cover F. Gavril und M. Yannakakis zu. Das Knotenüberdeckungsproblem wurde ausgiebig untersucht (Hochbaum [172] zählt 16 verschiedene Approximationsalgorithmen für dieses Problem auf), aber alle Approximationsverhältnisse sind mindestens 2 − o(1). Der Algorithmus Approx-TSP-Tour kommt in einer Arbeit von Rosenkrantz, Stearns und Lewis [298] vor. Christofides verbesserte diesen Algorithmus und gab einen 3/2-Approximationsalgorithmus für das Problem des Handelsreisenden mit Dreiecksungleichung an. Arora [22] und Mitchell [257] haben gezeigt, dass es ein Approximationsschema mit polynomieller Laufzeit gibt, wenn die Punkte in der euklidischen Ebene liegen. Theorem 35.3 geht auf Sahni und Gonzalez [301] zurück. Die Analyse der Greedy-Heuristik für das Mengenüberdeckungsproblem ist an den von Chvátal [68] veröffentlichten Beweis eines allgemeineren Ergebnisses angelehnt. Das hier vorgestellte Ergebnis geht auf Johnson [190] und Lovász [238] zurück. Der Algorithmus Approx-Subset-Sum und seine Analyse sind in grober Anlehnung an verwandte Algorithmen von Ibarra und Kim [187] für das Rucksackproblem und Teilsummenprobleme modelliert. Die Problemstellung 35-7 ist eine kombinatorische Version eines allgemeineren Resultats zur Approximierung ganzzahliger Programme vom Rucksack-Typ von Bienstock und McClosky [45]. Der randomisierte Algorithmus für die MAX-3-CNF-Erfüllbarkeit ist implizit in der Arbeit von Johnson [190] enthalten. Der Algorithmus für die gewichtete Knotenüberdeckung stammt von Hochbaum [171]. Abschnitt 35.4 lässt die Mächtigkeit von Randomisierung und linearer Programmierung beim Entwurf von Approximationsalgorithmen nur erahnen. Die Kombination dieser beiden Ideen führt auf ein Verfahren, das als „randomisiertes Runden“ bezeichnet wird, das ein Problem als ganzzahliges lineares Programm formuliert, die LP-Relaxation löst und die Variablenwerte dieser Lösung als Wahrscheinlichkeiten interpretiert. Diese Wahrscheinlichkeiten leiten zur Lösung des ursprünglichen Problems. Dieses Verfahren wurde zuerst von Raghavan und Thompson [290] verwendet und erfuhr zahlreiche weitere Anwendungen (einen Überblick finden Sie in Motwani, Naor und Raghavan [261]). Mehrere andere neuere Ideen auf dem Gebiet der Approximationsalgorithmen umfassen die primale duale Methode (siehe [135] für einen Überblick), die Bestimmung dünner Schnitte zur Verwendung in Teile-undBeherrsche-Algorithmen [229] und die Verwendung semidefiniter Programmierung [134]. Wie schon in den Kapitelbemerkungen zu Kapitel 34 erwähnt wurde, haben neuere Ergebnisse für probabilistisch prüfbare Beweise zu unteren Schranken für die Approximierbarkeit vieler Probleme (zu denen auch einige der in diesem Kapitel vorgestellten gehören) geführt. Zusätzlich zu den dort angegebenen Referenzen enthält das Kapitel von Arora und Lund [23] eine gute Beschreibung der Beziehung zwischen probabilistischer Prüfbarkeit von Beweisen und der Schwierigkeit, bestimmte Probleme zu approximieren.
Teil VIII
Anhang Mathematische Grundlagen
Einführung Wenn wir Algorithmen analysieren, müssen wir oft auf verschiedene mathematische Werkzeuge zurückgreifen. Einige diese Werkzeuge haben Abiturniveau, andere sind möglicherweise neu für Sie. In Teil I dieses Buches haben wir die Verwendung von asymptotischen Notationen und das Lösen von Rekursionsgleichungen kennengelernt. Dieser Anhang enthält eine Übersicht verschiedener anderer Konzepte und Methoden, die wir bei der Analyse von Algorithmen verwenden. Wie wir bereits in der Einführung zu Teil I angemerkt haben, haben Sie möglicherweise schon viel von dem in diesem Anhang behandelten Stoff kennengelernt, bevor Sie das vorliegende Buch in die Hand genommen haben (wenn auch die eine oder andere Schreibweise in diesem Buch von derjenigen abweichen kann, die Sie anderswo gesehen haben). Sie sollten den Anhang daher als Referenz betrachten. Wie auch sonst im Buch haben wir Übungen und Problemstellungen in dem Anhang aufgenommen, damit Sie Ihre Fähigkeiten auf diesen Gebieten trainieren können. Anhang A stellt Methoden für das Auswerten und Abschätzen von Summen vor, die bei der Analyse von Algorithmen häufig vorkommen. Viele der Formeln aus diesem Kapitel finden Sie in in einem beliebigen Buch über Analysis wieder; vielleicht finden Sie aber eine kompakte Zusammenstellung dieser Methoden ganz praktisch. Anhang B enthält grundlegende Definitionen und Notationen für Mengen, Relationen, Funktionen, Graphen und Bäume. Er geht auch auf einige wichtige Eigenschaften dieser Objekte ein. Anhang C beginnt mit elementaren Konzepten der Kombinatorik: Permutationen, Kombinationen und ähnliches. Der Rest des Kapitels enthält Definitionen und Sätze zur elementaren Wahrscheinlichkeitstheorie. Für die Analyse der meisten Algorithmen in diesem Buch wird die Wahrscheinlichkeitstheorie nicht benötigt. Sie können daher beim ersten Lesen die letzten Abschnitte des Kapitels weglassen und brauchen Sie nicht einmal zu überfliegen. Wenn Sie später einer probabilistischen Analyse begegnen, die Sie besser verstehen wollen, wird Ihnen Anhang C eine nützliche Referenz sein. Anhang D definiert Matrizen, die auf ihnen anwendbaren Operationen und einige ihrer grundlegenden Eigenschaften. Sie werden wahrscheinlich dem größten Teil dieses Materials schon begegnet sein, wenn Sie einen Kurs zu Linearer Algebra besucht haben, Sie finden es aber vielleicht hilfreich, eine Stelle zu haben, an der sie die Notationen und Definitionen nachschlagen können.
A
Summen
Wenn ein Algorithmus eine iterative Kontrollstruktur wie eine while- oder for-Schleife enthält, dann drücken wir seine Laufzeit als Summe der Zeiten über die einzelnen Ausführungen des Schleifenkörpers aus. Zum Beispiel haben wir in Abschnitt 2.2 festgestellt, dass die j-te Iteration von Sortieren durch Einfügen im schlechtesten Fall eine Zeit proportional zu j benötigt. Durch Aufsummieren der Zeiten über alle Iterationen erhalten wir die Summe (oder Reihe) n
j.
j=2
Wenn wir diese Summe auswerten, so erhalten wir eine Schranke von Θ(n2 ) für die Laufzeit des Algorithmus im schlechtesten Fall. Dieses Beispiel illustriert, warum Sie solche Summen umformen und abschätzen können sollten. Abschnitt A.1 bietet eine Zusammenstellung elementarer Formeln für Summen. In Abschnitt A.2 werden nützliche Methoden für die Abschätzung von Summen vorgestellt. Wir geben die Formeln in Abschnitt A.1 ohne Beweis an; einige Beweise sind jedoch in Abschnitt A.2 zu finden; sie dienen zur Veranschaulichung der in diesem Abschnitt eingeführten Methoden. Sie können die meisten der anderen Beweise in einem beliebigen Buch über Analysis finden.
A.1
Summenformeln und Eigenschaften
Für eine gegebene Folge a1 , a2 , . . . , an von Zahlen, wobei n eine nichtnegative ganze Zahl ist, schreiben wir die endliche Summe a1 + a2 + · · · + an als n
ak .
k=1
Ist n = 0, so ist der Wert der Summe als 0 definiert. Der Wert einer endlichen Folge ist immer wohldefiniert und wir können ihre Terme in einer beliebigen Reihenfolge aufaddieren. Für eine gegebene unendliche Folge a1 , a2 , . . . von Zahlen, schreiben wir die unendliche Summe a1 + a2 + · · · als ∞ k=1
ak ,
1156
A Summen
die wir als lim
n→∞
n
ak
k=1
interpretieren. Falls der Grenzwert nicht existiert, dann divergiert die Reihe; anderenfalls konvergiert sie. Die Terme einer konvergenten Reihe können nicht immer in beliebiger Reihenfolge addiert werden. Wir können jedoch die Terme einerabsolut ∞ a konvergierende Reihe umordnen, d. h. einer Reihe ∞ k=1 k , für die auch k=1 |ak | konvergiert.
Linearität Für jede reelle Zahl c und beliebige endliche Folgen a1 , a2 , . . . , an und b1 , b2 , . . . , bn gilt n
(cak + bk ) = c
k=1
n
ak +
k=1
n
bk .
k=1
Die Linearitätseigenschaft gilt auch bei unendlichen konvergierenden Reihen. Wir können die Linearitätseigenschaft ausnutzen, um Summen in asymptotischen Notationen umzuformen. Beispielsweise gilt n
Θ(f (k)) = Θ
2 n
k=1
3 f (k)
.
k=1
In dieser Gleichung bezieht sich die Θ-Notation auf der linken Seite auf die Variable k, auf der rechten Seite dagegen auf n. Wir können derartige Umformungen auch auf konvergierende unendliche Reihen anwenden.
Arithmetische Reihen Die Summe n
k = 1 + 2 + ··· + n
k=1
ist eine arithmetische Reihe und hat den Wert n k=1
k=
1 n(n + 1) 2
= Θ(n2 ) .
(A.1) (A.2)
A.1 Summenformeln und Eigenschaften
1157
Summen von quadratischen und kubischen Termen Es gelten die folgenden Summenformeln für die Summation von quadratischen und kubischen Termen: n k=0 n
k2 =
n(n + 1)(2n + 1) , 6
(A.3)
k3 =
n2 (n + 1)2 . 4
(A.4)
k=0
Geometrische Reihen Für reelle Zahlen x = 1 ist die Summation n
xk = 1 + x + x2 + · · · + xn
k=0
eine geometrische Reihe oder Exponentialreihe. Sie hat den Wert n
xk =
k=0
xn+1 − 1 . x−1
(A.5)
Wenn die Summation unendlich viele Glieder hat und |x| < 1 gilt, dann handelt es sich um die unendliche fallende geometrische Reihe ∞ k=0
xk =
1 . 1−x
(A.6)
Da wir 00 = 1 voraussetzen, lassen sich diese Formeln auch anwenden, wenn x = 0 ist.
Harmonische Reihen Für eine positive ganze Zahl n ist die n-te harmonische Zahl durch 1 1 1 1 + + + ··· + 2 3 4 n n 1 = k
Hn = 1 +
k=1
= ln n + O(1) gegeben. (Wir werden eine verwandte Schranke in Abschnitt A.2 beweisen.)
(A.7)
1158
A Summen
Integration und Differentiation von Reihen Weitere Formeln erhalten wir, wenn wir die obigen Formeln integrieren oder differenzieren. Differenzieren wir zum Beispiel beide Seiten der Formel (A.6) für eine geometrische Reihe und multiplizieren dann mit x, so erhalten wir für |x| < 1 ∞
kxk =
k=0
x . (1 − x)2
(A.8)
Teleskopreihen Für jede Folge a0 , a1 , . . . , an gilt n
(ak − ak−1 ) = an − a0 ,
(A.9)
k=1
da jeder der Terme a1 , a2 , . . . , an−1 genau einmal addiert und einmal subtrahiert wird. Entsprechend gilt n−1
(ak − ak+1 ) = a0 − an .
k=0
Betrachten Sie als Beispiel für eine Teleskopreihe die Reihe n−1 k=1
1 . k(k + 1)
Da wir jeden Term zu 1 1 1 = − , k(k + 1) k k+1 umformen können, erhalten wir n−1 k=1
n−1 1 1 1 = − k(k + 1) k k+1 k=1
=1−
1 . n
Produkte Wir schreiben das endliche Produkt a1 a2 · · · an als n A k=1
ak .
A.2 Abschätzungen für Summen
1159
Für n = 0 ist der Wert des Produktes als 1 definiert. Wir können eine Produktbildung in eine Summation umformen, indem wir die Gleichung 2 lg
n A
3 ak
k=1
=
n
lg ak
k=1
anwenden.
Übungen A.1-1 Leiten Sie eine einfache Formel für
n
k=1 (2k
− 1) ab.
A.1-2∗ Zeigen Sie unter harmonischen Reihe die Gültigkeit der Sumn Verwendung der √ menformel k=1 1/(2k − 1) = ln( n) + O(1). A.1-3 Zeigen Sie, dass die Summenformel |x| < 1 gilt. A.1-4∗ Zeigen Sie, dass
∞
k=0 (k
A.1-5∗ Werten Sie die Summe
∞ k=0
k 2 xk = x(1 + x)/(1 − x)3 für
− 1)/2k = 0 gilt.
∞
k=1 (2k
+ 1)x2k für |x| < 1 aus.
n A.1-6 Beweisen Sie, dass nk=1 O(fk (i)) = O k=1 fk (i) gilt, indem Sie die Linearität von Summationen ausnutzen. A.1-7 Werten Sie das Produkt A.1-8∗ Werten Sie das Produkt
A.2
=n k=1
=n
2 · 4k aus.
k=2 (1
− 1/k 2 ) aus.
Abschätzungen für Summen
Uns stehen viele Techniken zur Verfügung, um Summen abzuschätzen, die die Laufzeit von Algorithmen beschreiben. Hier wir werden einige der am häufigsten benutzten Methoden vorstellen.
Mathematische Induktion Die grundlegendste Methode, eine Reihe auszuwerten, besteht darin, mathematische Induktion n anzuwenden. Lassen Sie uns als Beispiel beweisen, dass die arithmetische Reihe k=1 k gleich 12 n(n+1) ist. Wir können diese Aussage für n = 1 leicht überprüfen. Wir stellen die Induktionsvoraussetzung auf, dass die Aussage für n gilt, und beweisen,
1160
A Summen
dass sie dann auch für n + 1 gilt. Es ist n+1
k=
k=1
n
k + (n + 1)
k=1
1 n(n + 1) + (n + 1) 2 1 = (n + 1)(n + 2) . 2 =
Sie müssen nicht immer den exakten Wert einer Summe erraten, um mathematische Induktion anwenden zu können. Sie können stattdessen auch mathematische Induktion anwenden, um eine Schranke der Summation zu beweisen. Lassen Sie uns als Beispiel beweisen, dass die geometrische Reihe nk=0 3k in O(3n ) ist. Genauer gesagt, lassen Sie n uns beweisen, dass k=0 3k ≤ c 3n für eine Konstante c gilt. Für den Basisfall n = 0 ist 0 k k=0 3 = 1 ≤ c · 1, vorausgesetzt es gilt c ≥ 1. Wir setzen voraus, dass die Schranke für n gilt, und zeigen, dass sie dann auch für n + 1 gilt. Es ist n+1
3k =
k=0
n
3k + 3n+1
k=0
≤ c 3n + 3n+1 1 1 + = c 3n+1 3 c
(nach Induktionsvoraussetzung)
≤ c 3n+1 , vorausgesetzt es gilt (1/3 + 1/c) ≤ 1, d. h. c ≥ 3/2. Also gilt zeigen wollten.
n k=0
3k = O(3n ), was wir
Wir müssen vorsichtig sein, wenn wir die asymptotische Notation verwenden, um Schranken über mathematische Induktion zu nbeweisen. Betrachten Sie den folgenden Trugschluss beim Versuch, die Aussage k=1 k = O(n) zu beweisen. Offensichtlich gilt 1 k = O(1). Wenn wir voraussetzen, dass die Schranke für n gilt, dann können wir k=1 die Schranke auch für n + 1 beweisen: n+1 k=1
k=
n
k + (n + 1)
k=1
= O(n) + (n + 1)
⇐= falsch!!
= O(n + 1) . Der Trugschluss in der Beweisführung besteht darin, dass die in „groß oh“ verborgene Konstante mit n wächst und daher gar keine Konstante ist. Wir haben nicht gezeigt, dass die gleiche Konstante für alle n gilt.
A.2 Abschätzungen für Summen
1161
Abschätzung der Terme Wir können manchmal eine gute obere Schranke für eine Reihe erhalten, indem wir die einzelnen Terme der Reihe abschätzen, und es genügt häufig, den größten Term als Schranke für die anderen zu verwenden. Zum Beispiel erhalten wir so für die arithmetische Reihe (A.1) leicht die obere Schranke n k=1
k≤
n
n
k=1
= n2 . Setzen wir amax = max1≤k≤n ak , dann gilt allgemein für eine Reihe schätzung n
n k=1
ak die Ab-
ak ≤ n amax .
k=1
Diese Methode liefert keine besonders gute Schranke, wenn die Reihe in Wirklichkeit durch eine geometrische Reihe beschränkt ist. Setzen Sie voraus, dass die Glieder einer n gegebenen Reihe k=0 ak die Ungleichung ak+1 /ak ≤ r für alle k ≥ 0 erfüllen, wobei r eine Konstante mit 0 < r < 1 ist. Wir können dann die Reihe wegen ak ≤ a0 rk durch eine unendliche fallende geometrische Reihe abschätzen, und somit gilt n k=0
ak ≤
∞
a0 r k
k=0
= a0
∞
rk
k=0
= a0
1 . 1−r
∞ k Wir können diese Methode anwenden, um die Summe k=1 (k/3 ) abzuschätzen. Damit ∞ die Summation bei k = 0 startet, formen wir die Summe zu k=0 ((k + 1)/3k+1 ) um. Der erste Term (a0 ) ist 1/3, und das Verhältnis (r) von aufeinander folgenden Termen ist für alle k ≥ 0 1 (k + 2)/3k+2 = k+1 (k + 1)/3 3 2 ≤ 3
· .
k+2 k+1
1162
A Summen
Es gilt somit ∞ ∞ k k+1 = k 3 3k+1 k=1
k=0
1 1 · 3 1 − 2/3 =1.
≤
Ein häufiger Trugschluss bei der Anwendung dieser Methode besteht darin, zu zeigen, dass das Verhältnis aufeinanderfolgender Terme kleiner ist als 1, und dann vorauszusetzen, dass die Summe durch eine geometrische Reihe beschränkt ist. Ein Beispiel hierfür ist die unendliche harmonische Reihe, die wegen ∞ n 1 1 = lim k n→∞ k k=1
k=1
= lim Θ(lg n) n→∞ =∞
divergiert. Das Verhältnis des k-ten zum (k + 1)-ten Term ist k/(k + 1) < 1, aber trotzdem ist die Reihe nicht durch eine fallende geometrische Reihe beschränkt. Um die Reihe durch eine geometrische Reihe abzuschätzen, ist zu zeigen, dass es ein konstantes r < 1 mit der Eigenschaft gibt, dass das Verhältnis für alle aufeinanderfolgenden Terme kleiner als r ist. Für die harmonische Reihe existiert kein solches r, da das Verhältnis beliebig nahe an 1 rankommt.
Aufspalten von Summen Eine Möglichkeit, Schranken für kompliziertere Summe zu erhalten, besteht darin, die Reihe durch Zerlegen des Indexbereiches als Summe von zwei oder mehreren Reihen darzustellen und dann jede der entstehenden Reihen einzeln abzuschätzen. Setzen Sie zum Beispiel voraus, dass wir eine untere Schranke für die arithmetische Reihe nk=1 k suchen, von der wir bereits gesehen haben, dass sie eine obere Schranke von n2 hat. Wir könnten versuchen, jeden einzelnen Term durch den kleinsten Term abzuschätzen, aber da dieser 1 ist, erhalten wir als untere Schranke n, was sehr weit von der oberen Schranke n2 entfernt ist. Wir können eine bessere untere Schranke finden, wenn wir die Summe zunächst aufspalten. Lassen Sie uns aus Gründen der Bequemlichkeit voraussetzen, dass n gerade ist. Es gilt
A.2 Abschätzungen für Summen
n
k=
k=1
n/2
≥
n/2
n
k+
k=1
1163
k
k=n/2+1 n
0+
k=1
(n/2)
k=n/2+1
= (n/2)2 = Ω(n2 ) ,
was wegen
n k=1
k = O(n2 ) eine asymptotisch scharfe Schranke darstellt.
Eine Summation, die bei der Analyse eines Algorithmus vorkommt, können wir häufig aufspalten und eine konstante Anzahl von Anfangsgliedern vernachlässigen. n Allgemein ist diese Methode anwendbar, wenn alle Terme ak in einer Summation k=0 ak unabhängig von n sind. Dann können wir für jede Konstante k0 > 0 die Summe durch n
ak =
k=0
k 0 −1
ak +
k=0
= Θ(1) +
n
ak
k=k0 n
ak
k=k0
abschätzen, da die Anfangsglieder der Summe alle konstant sind deren Anzahl und n konstant ist. Wir können dann andere Techniken verwenden, um k=k0 ak abzuschätzen. Diese Technik ist auch auf unendliche Reihen anwendbar. Um beispielsweise eine asymptotisch obere Schranke für ∞ k2 k=0
2k
zu bestimmen, müssen wir nur bemerken, dass für das Verhältnis aufeinander folgender Terme im Falle k ≥ 3 (k + 1)2 (k + 1)2 /2k+1 = k 2 /2k 2k 2 8 ≤ 9
1164
A Summen
gilt. Daher kann die Summe aufgespalten werden in ∞ 2 ∞ k2 k2 k2 = + 2k 2k 2k k=0 k=0 k=3 2 ∞ k k2 9 8 ≤ + 2k 8 9 k=0
k=0
= O(1) , da die erste Summe aus einer konstanten Anzahl von Termen besteht und die zweite Summe eine fallende geometrische Reihe ist. Die Technik des Aufspaltens einer Summe kann uns helfen, asymptotische Schranken auch in wesentlich schwierigeren Szenarien zu bestimmen. Zum Beispiel erhalten wir mit dieser Technik eine Schranke von O(lg n) für die harmonische Reihe (A.7) Hn =
n 1 . k
k=1
Wir erhalten diese Schranke, indem wir den Bereich von 1 bis n in lg n + 1 viele Teilstücke zerlegen und den Beitrag jedes einzelnen Teilstücks nach oben durch 1 abschätzen. For i = 0, 1, . . . , lg n besteht das i-te Teilstück aus den Termen von 1/2i bis ausschließlich 1/2i+1 . Das letzte Teilstück enthält möglicherweise Terme, die nicht in der ursprünglichen harmonischen Reihe enthalten sind, und somit erhalten wir lg n 2 −1 n 1 1 ≤ k 2i + j i=0 j=0 i
k=1
lg n 2i −1
≤
1 2i i=0 j=0
lg n
=
1
i=0
≤ lg n + 1 .
(A.10)
Approximation durch Integrale Wenn eine Summation die Form nk=m f (k) hat, wobei f (k) eine monoton steigende Funktion ist, dann können wir sie durch Integrale approximieren: ! n ! n+1 n f (x) dx ≤ f (k) ≤ f (x) dx . (A.11) m−1
k=m
m
A.2 Abschätzungen für Summen
1165
f (x)
f (n)
f (n–1)
…
…
m+2
m+1
f (n–2)
m
f (m+2)
f (m+1)
f (m) m –1
…
x
…
n–2
n–1
n
n+1
(a)
f (x)
…
…
f (n)
m+2
…
f (n–1)
m+1
f (n–2)
m
f (m+2)
f (m+1)
f (m)
m –1
…
x n–2
n–1
n
n+1
(b)
n Abbildung A.1: Approximation der Reihe k=m f (k) durch Integrale. Der Flächeninhalt jedes Rechtecks ist jeweils in diesem angegeben. Die Gesamtfläche der Rechtecke entspricht dem Wert der Summation. Das Integral wird jeweils durch das schattiert gekennzeichnete Gebiet n unterhalb der Kurve dargestellt. Durch Vergleich der Flächen erhalten wir (a) m−1 f (x) dx ≤
n f (k) und, indem wir die Rechtecke alle jeweils um eine Einheit nach rechts verschieben, k=m n+1
f (x) dx. (b) n k=m f (k) ≤ m
1166
A Summen
Abbildung A.1 illustriert diese Approximation. Die Summe wird in der Abbildung durch den Flächeninhalt der Rechtecke dargestellt, und das Integral durch den Flächeninhalt des schattierten Bereichs unterhalb der Kurve. Wenn f (k) eine monoton fallende Funktion ist, dann liefert ein analoges Vorgehen die Schranken ! n+1 ! n n f (x) dx ≤ f (k) ≤ f (x) dx . (A.12) m
m−1
k=m
Die Integralapproximation (A.12) liefert eine scharfe Abschätzung für die n-te harmonische Zahl. Als eine untere Schranke erhalten wir ! n+1 n 1 dx ≥ k x 1 k=1
(A.13)
= ln(n + 1) . Für die obere Schranke leiten wir die Ungleichung ! n n 1 dx ≤ k x 1 k=2
= ln n ab, was uns die Schranke n 1 ≤ ln n + 1 k
(A.14)
k=1
liefert.
Übungen A.2-1 Zeigen Sie, dass die Reihe schränkt ist.
n k=1
1/k 2 nach oben durch eine Konstante be-
A.2-2 Bestimmen Sie eine asymptotisch obere Schranke für die Reihe lg n
0 1 n/2k . k=0
A.2-3 Zeigen Sie, dass die n-te harmonische Zahl in Ω(lg n) ist, indem Sie die harmonische Reihe geeignet aufspalten. n A.2-4 Approximieren Sie k=1 k 3 durch ein Integral. A.2-5 Warum haben wir die Integralapproximation (A.12) nicht direkt auf nk=1 1/k angewendet, um eine obere Schranke für die n-te harmonische Zahl zu erhalten?
Problemstellungen zu Kapitel A
1167
Problemstellungen A-1 Abschätzung von Summen Geben Sie asymptotisch exakte Schranken für die folgenden Summen an. Setzen Sie voraus, dass r ≥ 0 und s ≥ 0 Konstanten sind. a.
b.
c.
n k=1 n k=1 n
kr . lgs k. k r lgs k.
k=1
Kapitelbemerkungen Knuth [209] stellt eine exzellente Referenz für die in diesem Kapitel vorgestellte Materie dar. Sie können grundlegende Eigenschaften von Reihen in jedem guten Buch über Analysis finden, zum Beispiel in Apostol [18] oder Thomas et al. [334].
B
Mengen usw.
Viele Kapitel dieses Buches verwenden Elemente der diskreten Mathematik. Dieses Kapitel gibt einen Überblick über die Bezeichnungen, Definitionen und elementaren Eigenschaften von Mengen, Relationen, Funktionen, Graphen und Bäumen. Falls Sie mit diesen Themen bereits gut vertraut sind, brauchen Sie dieses Kapitel wahrscheinlich nur zu überfliegen.
B.1
Mengen
Eine Menge ist eine Sammlung unterscheidbarer Objekte, die als deren Elemente bezeichnet werden. Falls ein Objekt x Element einer Menge S ist, schreiben wir x ∈ S (gesprochen „x ist Element von S“). Falls x nicht Element von S ist, schreiben wir x ∈ S. Wir können eine Menge beschreiben, indem wir ihre Elemente explizit in einer Liste zwischen geschweiften Klammern aufzählen. Zum Beispiel können wir eine Menge S, die genau die Elemente 1, 2 und 3 enthält, definieren, indem wir S = {1, 2, 3} schreiben. Da 2 ein Element der Menge S ist, gilt 2 ∈ S, und da 4 kein Element von S ist, gilt 4 ∈ / S. Eine Menge kann dasselbe Objekt nicht mehr als einmal enthalten1 , und ihre Elemente sind nicht geordnet. Zwei Mengen A und B sind gleich, geschrieben A = B, wenn sie die gleichen Elemente enthalten. Beispielsweise gilt {1, 2, 3, 1} = {1, 2, 3} = {3, 2, 1}. Wir führen spezielle Notationen für häufig auftretende Mengen ein: • ∅ bezeichnet die leere Menge, d. h. die Menge, die keine Elemente enthält. • Z bezeichnet die Menge der ganzen Zahlen, d. h. {. . . , −2, −1, 0, 1, 2, . . .}. • R bezeichnet die Menge der reellen Zahlen. • N bezeichnet die Menge der natürlichen Zahlen, d. h. die Menge {0, 1, 2, . . .}.2 Wenn alle Elemente einer Menge A in einer Menge B enthalten sind, wenn also mit x ∈ A auch x ∈ B gilt, schreiben wir A ⊆ B und sagen, dass A eine Teilmenge von B ist. Eine Menge A ist eine echte Teilmenge von B (geschrieben A ⊂ B), wenn A ⊆ B und A = B gilt. (Manche Autoren verwenden das Symbol „⊂“ für die gewöhnliche Teilmengenrelation anstatt für die echte Teilmengenrelation.) Für jede beliebige Menge A gilt A ⊆ A. Für zwei Mengen A und B gilt A = B genau dann, wenn A ⊆ B und 1 Eine Verallgemeinerung des Mengenbegriffs, in der dasselbe Objekt mehr als einmal enthalten sein kann, wird Multimenge genannt. 2 Manche Autoren beginnen mit der Zählung der natürlichen Zahlen bei 1 statt bei 0. Der Trend scheint zu sein, bei 0 zu starten.
1170
B Mengen usw.
B ⊆ A gilt. Für beliebige drei Mengen A, B und C folgt aus A ⊆ B und B ⊆ C die Relation A ⊆ C. Für jede Menge A gilt ∅ ⊆ A. Mitunter definieren wir Mengen mithilfe anderer Mengen. Zu einer gegebenen Menge A können wir eine Menge B ⊆ A definieren, indem wir eine Eigenschaft festlegen, die die Elemente von B auszeichnet. Zum Beispiel können wir die Menge der geraden ganzen Zahlen durch {x : x ∈ Z und x/2 ist eine ganze Zahl} definieren. Der Doppelpunkt in dieser Darstellung ist zu lesen als „sodass“. (Manche Autoren verwenden anstelle des Doppelpunkts einen vertikalen Strich.) Zu zwei gegebenen Mengen A und B können wir außerdem durch Anwendung von Mengenoperationen neue Mengen definieren: • Die Schnittmenge der Mengen A und B ist die Menge A ∩ B = {x : x ∈ A und x ∈ B} . • Die Vereinigungsmenge der Mengen A und B ist die Menge A ∪ B = {x : x ∈ A oder x ∈ B} . • Die Differenzmenge der Mengen A und B ist die Menge A − B = {x : x ∈ A und x ∈ / B} . Für Mengenoperationen gelten die folgenden Gesetze: Eigenschaften einer leeren Menge: A∩∅ = ∅ , A∪∅ = A . Idempotenzgesetze: A∩A = A , A∪A = A . Kommutativgesetze: A∩B = B∩A , A∪B = B∪A . Assoziativgesetze: A ∩ (B ∩ C) = (A ∩ B) ∩ C , A ∪ (B ∪ C) = (A ∪ B) ∪ C .
B.1 Mengen
A
1171
B
A
B
− C A
A
C −
B
=
(B ∩ C)
A
C =
B
A − (B ∩ C)
A
C =
B
∪
=
(A − B)
C ∪
(A − C)
Abbildung B.1: Ein Venn-Diagramm, das das erste der Gesetze von de Morgan (B.2) illustriert. Jede der Mengen A, B und C ist durch einen Kreis dargestellt.
Distributivgesetze: A ∩ (B ∪ C) = (A ∩ B) ∪ (A ∩ C) , A ∪ (B ∩ C) = (A ∪ B) ∩ (A ∪ C) .
(B.1)
Absorptionsgesetze: A ∩ (A ∪ B) = A , A ∪ (A ∩ B) = A . Gesetze von de Morgan: A − (B ∩ C) = (A − B) ∪ (A − C) , A − (B ∪ C) = (A − B) ∩ (A − C) .
(B.2)
Abbildung B.1 illustriert das erste der Gesetze von de Morgan mithilfe eines VennDiagramms, eine Veranschaulichung, in der die Mengen als Gebiete in der Ebene dargestellt sind. Oft sind alle betrachteten Mengen Teilmengen einer größeren Menge U , die als Universalmenge bezeichnet wird. Wenn wir zum Beispiel verschiedene Mengen untersuchen, die nur aus ganzen Zahlen bestehen, so ist die Menge der ganzen Zahlen Z eine geeignete Universalmenge. Ist eine Universalmenge U gegeben, so definieren wir das Komplement einer Menge A als die Menge A = U − A = {x : x ∈ U and x ∈ A}. Für jede beliebige Menge A ⊆ U gelten die folgenden Eigenschaften: A=A, A∩A=∅ , A∪A=U . Wir können die Gesetze von de Morgan (B.2) unter Verwendung von Komplementen umformulieren. Für beliebige zwei Teilmengen B und C der Universalmenge U gilt B ∩C = B ∪C , B ∪C = B ∩C .
1172
B Mengen usw.
Zwei Mengen A und B sind disjunkt, wenn sie keine gemeinsamen Elemente besitzen, d. h. wenn A∩B = ∅ gilt. Eine Menge nichtleerer Mengen S = {Si } bildet eine Partition einer Menge S, falls • die Mengen paarweise disjunkt sind, d. h. falls Si ∩ Sj = ∅ aus Si , Sj ∈ S mit i = j folgt, und • ihre Vereinigung S ergibt, d. h. , S= Si . Si ∈S
Mit anderen Worten, S bildet eine Partition von S, falls jedes Element von S in genau einer Menge Si ∈ S enthalten ist. Die Anzahl der Elemente einer Menge S ist die Kardinalität oder die Größe der Menge und wird mit |S| bezeichnet. Zwei Mengen haben die gleiche Kardinalität, falls es zwischen ihren Elementen eine eineindeutige Zuordnung gibt. Die Kardinalität der leeren Menge ∅ ist 0. Falls die Kardinalität einer Menge eine natürliche Zahl ist, sagen wir, dass die Menge endlich ist; anderenfalls ist sie unendlich. Eine unendliche Menge, für die es eine eineindeutige Zuordnung zu der Menge der natürlichen Zahlen N gibt, ist abzählbar ; anderenfalls heißt sie überabzählbar . Die Menge Z der ganzen Zahlen ist abzählbar, die Menge R der reellen Zahlen dagegen ist überabzählbar. Für zwei beliebige Mengen A und B gilt die Identität |A ∪ B| = |A| + |B| − |A ∩ B| ,
(B.3)
aus der wir auf die Ungleichung |A ∪ B| ≤ |A| + |B| schließen können. Wenn A und B disjunkt sind, dann gilt |A ∩ B| = 0 und somit |A ∪ B| = |A| + |B|. Aus A ⊆ B folgt |A| ≤ |B|. Eine endliche Menge mit n Elementen wird mitunter als n-Menge bezeichnet. Eine 1-Menge wird Einermenge genannt. Eine Teilmenge einer Menge, die k Elemente enthält, wird mitunter als k-Teilmenge bezeichnet. Wir bezeichnen die Menge aller Teilmengen einer Menge S einschließlich der leeren Menge und S selbst mit 2S ; wir nennen 2S die Potenzmenge von S. Zum Beispiel gilt 2{a,b} = {∅, {a} , {b} , {a, b}}. Die Potenzmenge einer endlichen Menge S hat die Kardinalität 2|S| (siehe Übung B.1-5). Wir haben es manchmal mit mengenähnlichen Strukturen zu tun, deren Elemente geordnet sind. Ein geordnetes Paar aus zwei Elementen a und b wird mit (a, b) bezeichnet und ist formal definiert als die Menge (a, b) = {a, {a, b}}. Das geordnete Paar (a, b) ist also nicht das gleiche wie das geordnete Paar (b, a). Das kartesische Produkt A×B zweier Mengen A und B ist die Menge aller geordneten Paare, wobei jeweils das erste Element aus A und das zweite Element aus B ist; also formal ausgedrückt A × B = {(a, b) : a ∈ A und b ∈ B} .
B.1 Mengen
1173
Zum Beispiel gilt {a, b}× {a, b, c} = {(a, a), (a, b), (a, c), (b, a), (b, b), (b, c)}. Wenn A und B endliche Mengen sind, ist die Kardinalität ihres kartesischen Produktes |A × B| = |A| · |B| .
(B.4)
Das kartesische Produkt von n Mengen A1 , A2 , . . . , An ist die Menge der n-Tupel A1 × A2 × · · · × An = {(a1 , a2 , . . . , an ) : ai ∈ Ai , i = 1, 2, . . . , n} , dessen Kardinalität |A1 × A2 × · · · × An | = |A1 | · |A2 | · · · |An | ist, wenn alle Mengen endlich sind. Die Menge An = A × A × · · · × A bezeichnen wir als n-faches kartesisches Produkt einer Menge A; ihre Kardinalität ist |An | = |A|n , falls die Menge A endlich ist. Wir können ein n-Tupel auch als endliche Folge der Länge n auffassen (siehe Seite 1177).
Übungen B.1-1 Zeichnen Sie Venn-Diagramme, die das erste der Distributivgesetze (B.1) veranschaulichen. B.1-2 Beweisen Sie die Verallgemeinerung der Gesetze von de Morgan auf ein beliebiges Ensemble von Mengen: A1 ∩ A2 ∩ · · · ∩ An = A1 ∪ A2 ∪ · · · ∪ An , A1 ∪ A2 ∪ · · · ∪ An = A1 ∩ A2 ∩ · · · ∩ An . B.1-3∗ Beweisen Sie die Verallgemeinerung von Gleichung (B.3), die als das Prinzip der Inklusion-Exlusion bekannt ist: |A1 ∪ A2 ∪ · · · ∪ An | = |A1 | + |A2 | + · · · + |An | − |A1 ∩ A2 | − |A1 ∩ A3 | − · · · + |A1 ∩ A2 ∩ A3 | + · · · .. .
(alle Paare) (alle Tripel)
+ (−1)n−1 |A1 ∩ A2 ∩ · · · ∩ An | . B.1-4 Beweisen Sie, dass die Menge der ungeraden natürlichen Zahlen abzählbar ist. B.1-5 Zeigen Sie, dass die Potenzmenge 2S jeder endlichen Menge S genau 2|S| Elemente enthält (d. h., dass es 2|S| verschiedene Teilmengen von S gibt.) B.1-6 Geben Sie eine rekursive Definition für ein n-Tupel an, indem Sie die mengentheoretische Definition eines geordneten Paares erweitern.
1174
B.2
B Mengen usw.
Relationen
Eine binäre Relation R auf zwei Mengen A und B ist eine Teilmenge des kartesischen Produkts A × B. Für (a, b) ∈ R schreiben wir mitunter a R b. Wenn wir sagen, dass R eine binäre Relation auf einer Menge A ist, dann meinen wir, dass R eine Teilmenge von A× A ist. Beispielsweise ist die „kleiner als“-Relation auf den natürlichen Zahlen die Menge {(a, b) : a, b ∈ N und a < b}. Eine n-äre Relation auf den Mengen A1 , A2 , . . . , An ist eine Teilmenge von A1 × A2 × · · · × An . Eine binäre Relation R ⊆ A × A ist reflexiv , falls für alle a ∈ A aRa gilt. Zum Beispiel sind „=“ und „≤“ reflexive Relationen auf N, die Relation „