Algorithmen und Datenstrukturen

Werbung
Algorithmen und Datenstrukturen
Sven O. Krumke
Entwurf vom 14. April 2004
Technische Universität Berlin
ii
Dieses Skript basiert auf der Vorlesung »Fortgeschrittene Datenstrukturen und Algorithmen« (Wintersemester 2002/2003) an der Technischen Universität
Berlin.
Über Kritik, Verbesserungsvorschläge oder gefundene Tippfehler würden ich mich sehr freuen!
Sven O. Krumke
[email protected]
Inhaltsverzeichnis
1 Einleitung
1.1 Zielgruppe und Voraussetzungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2 Danksagung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.3 Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2 Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
2.1 Der Algorithmus von Dijkstra . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2 Binäre Heaps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3 Erweiterungen von binären Heaps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3.1 d-näre Heaps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3.2 Intervall-Heaps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3.3 Eine Anwendung: Das eindimensionale komplementäre Bereichsproblem . . . . .
2.4 Minimale aufspannende Bäume: Der Algorithmus von Boruvka (in Variation) . . . . . . .
2.5 Binomial-Heaps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.5.1 Binomialbäume und Binomial-Heaps . . . . . . . . . . . . . . . . . . . . . . . .
2.5.2 Implementierung von Binomial-Heaps . . . . . . . . . . . . . . . . . . . . . . . .
2.5.3 Implementierung der einfachsten Heap-Operationen . . . . . . . . . . . . . . . .
2.5.4 Rückführen von I NSERT und E XTRACT-M IN auf M ELD . . . . . . . . . . . . . .
2.5.5 Vereinigen zweier Binomial-Heaps . . . . . . . . . . . . . . . . . . . . . . . . .
2.5.6 Konstruieren eines Binomial-Heaps . . . . . . . . . . . . . . . . . . . . . . . . .
2.6 Leftist-Heaps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.6.1 Verzögertes Verschmelzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.6.2 Nochmals der Algorithmus von Boruvka . . . . . . . . . . . . . . . . . . . . . .
1
2
2
2
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
5
6
11
16
16
17
19
23
32
32
35
35
37
39
43
46
52
54
3 Amortisierte Analyse
3.1 Stack-Operationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.2 Konstruieren eines Binomial-Heaps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.3 Dynamische Verwaltung einer Tabelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
61
61
63
63
4 Fibonacci-Heaps
4.1 Der Algorithmus von Prim . . . . . . . . . . . . . . . . . .
4.2 Der Aufbau von Fibonacci-Heaps . . . . . . . . . . . . . .
4.3 Implementierung der Basis-Operationen . . . . . . . . . . .
4.4 Das Verringern von Schlüsselwerten . . . . . . . . . . . . .
4.5 Beschränkung des Grades in Fibonacci-Heaps . . . . . . . .
4.6 Ein Minimalbaum-Algorithmus mit nahezu linearer Laufzeit
67
67
71
72
76
78
79
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
iv
5 Datenstrukturen für disjunkte Mengen
5.1 Der Algorithmus von Kruskal . . . . . . . . . . . . . . . . . . . . .
5.2 Eine einfache Datenstruktur . . . . . . . . . . . . . . . . . . . . . .
5.3 Baumrepräsentation mit Pfadkompression und Vereinigung nach Rang
5.4 Analyse von Pfadkompression und Vereinigung nach Rang . . . . . .
5.4.1 Eine explosiv wachsende Funktion . . . . . . . . . . . . . . .
5.4.2 Amortisierte Analyse mit Potentialfunktionsargument . . . .
6 Suchbäume und Selbstorganisierende Datenstrukturen
6.1 Optimale statische Suchbäume . . . . . . . . . . . . . . .
6.2 Der Algorithmus von Huffman . . . . . . . . . . . . . . .
6.3 Schüttelbäume . . . . . . . . . . . . . . . . . . . . . . . .
6.3.1 Rückführen der Suchbaumoperationen auf S PLAY
6.3.2 Implementierung der S PLAY-Operation . . . . . .
6.3.3 Analyse der S PLAY-Operation . . . . . . . . . . .
6.3.4 Analyse der Suchbaumoperationen . . . . . . . . .
7 Schnelle Algorithmen für Maximale Netz-Flüsse
7.1 Notation und grundlegende Definitionen . . . . . . . .
7.2 Residualnetze und flußvergrößernde Wege . . . . . . .
7.3 Maximale Flüsse und Minimale Schnitte . . . . . . . .
7.4 Grundlegende Algorithmen . . . . . . . . . . . . . . .
7.5 Präfluß-Schub-Algorithmen . . . . . . . . . . . . . . .
7.5.1 Anzahl der Markenerhöhungen im Algorithmus
7.5.2 Anzahl der Flußschübe im Algorithmus . . . .
7.5.3 Zeitkomplexität des generischen Algorithmus .
7.5.4 Der FIFO-Präfluß-Schub-Algorithmus . . . . .
7.6 Dynamische Bäume und ihr Nutzen in Flußalgorithmen
7.6.1 Operationen auf dynamischen Bäumen . . . .
7.6.2 Einsatz im Präfluß-Schub-Algorithmus . . . .
7.6.3 Implementierung der dynamischen Bäume . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
83
83
86
89
93
93
95
.
.
.
.
.
.
.
101
103
105
110
111
112
113
119
.
.
.
.
.
.
.
.
.
.
.
.
.
123
123
124
125
129
133
139
140
141
144
145
145
146
152
A Abkürzungen und Symbole
159
B Komplexität von Algorithmen
B.1 Größenordnung von Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
B.2 Berechnungsmodell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
B.3 Komplexitätsklassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
161
161
161
162
C Bemerkungen zum Dijkstra-Algorithmus
163
C.1 Ganzzahlige Längen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
C.2 Einheitslängen: Breitensuche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
Literaturverzeichnis
169
Einleitung
In der Kombinatorischen Optimierung treten beim Entwurf von Algorithmen viele „elementare Probleme“ auf. Mit Hilfe von geeigneten Algorithmen und Datenstrukturen für
diese „elementaren Probleme“ läßt sich der gesamte Algorithmus oft (theoretisch und auch
in der Praxis) deutlich beschleunigen.
Dieses Skript bietet anhand von ausgewählten Themen einen Einblick in moderne Datenstrukturen und Algorithmen sowie ihre Analyse. Dabei werden die einzelnen Datenstrukturen nicht isoliert behandelt, sondern stets im Zusammenhang mit konkreten Fragestellungen aus der Kombinatorischen Optimierung vorgestellt.
Kapitel 2 startet mit einer Einführung und kurzen Wiederholung. Ausgangsbasis ist der
binäre Heaps, mit dem man bereits Prioritätsschlangen effizient implementieren kann und
die keine Zeiger benötigen. Wir gehen dann auf Erweiterungen von binären Heaps ein, zeigen etwa, wie man mit Intervall-Heaps zweiendige Prioritätsschlangen verwalten kann. Die
Tatsache, daß binäre Heaps das Vereinigen von zwei Schlangen nicht effizient ermöglichen,
führt uns zu den Binomial-Heaps und den Leftist-Heaps, zwei ausgeklügelten Datenstrukturen.
In Kapitel 3 stellen wir die amortisierte Analyse von Algorithmen vor. Diese ist eines der
grundlegenden Hilfsmittel für die Analyse der Algorithmen und Datenstrukturen in diesem Skript. Informell gesprochen wird bei der amortisierten Analyse wird die Laufzeit
eines Algorithmus für eine Folge von Operationen nicht separat für jede einzelne Operation, sondern für die gesamte Folge analysiert. Mit Hilfe der amortisierten Analyse und
Potentialfunktionen sind oft verbesserte und aussagekräftigere Laufzeitabschätzungen für
Algorithmen möglich.
Kapitel 4 beschäftigt sich mit den sogenannten Fibonacci-Heaps. Diese Datenstruktur ermöglicht eine effiziente Verwaltung von Prioritätsschlangen, wie sie etwa bei kürzesteWege-Algorithmen (Dijkstra) und Algorithmen für Minimale Aufspannende Bäume (Prim)
benötigt werden. Mit Hilfe von Fibonacci-Heaps lassen sich die Algorithmen von Dijkstra
und Prim so implementieren, daß sie in O(m + n log n) Zeit auf Graphen mit n Ecken und
m Kanten laufen. Als weitere Anwendung von Fibonacci-Heaps stellen wir einen trickreichen Minimalbaum-Algorithmus vor, der eine Laufzeit von O(n + mβ(m, n)) besitzt,
wobei β(m, n) = min{ i : log(i) n ≤ m/n } eine extrem langsam wachsende Funktion ist.
Datenstrukturen für disjunkte Mengen oder Union-Find-Strukturen, wie sie Kapitel 5 besprochen werden, kommen dann ins Spiel, wenn man effizient Partitionen einer Menge
verwalten möchte, beispielsweise die Zusammenhangskomponenten eines Graphen im Algorithmus von Kruskal. Wir werden als Anwendung zeigen, daß geeignete Union-FindStrukturen es ermöglichen, Kruskals Algorithmus in Zeit O(m log m + mα(n)) laufen zu
lassen. Hier bezeichnet α(n) (im wesentlichen) die inverse Ackermann-Funktion, die für
alle Zahlen, die kleiner als die Anzahl der Atome im Universum sind, nach oben durch fünf
beschränkt ist.
2
Einleitung
In Kapitel 6 beschäftigen wir uns mit Suchbäumen und selbstorganisierenden Datenstrukturen. Zunächst wiederholen wir kurz einige Fakten über optimale statische Suchbäume.
Wir zeigen, wo solche Suchbäume unter anderem bei der Datenkompression eingesetzt
werden. Wir stellen dann die Datenstruktur der Schüttelbäume (engl. Splay-Trees) vor, die
asymptotisch genauso gut sind wie optimale statische Suchbäume, dies allerdings ohne
Informationen über die Verteilungen von Suchanfragen zu besitzen.
Netzwerkflußprobleme sind wichtige Bausteine in der Kombinatorischen Optimierung. In
Kapitel 7 stellen wir einige fortgeschrittene Techniken und Datenstrukturen vor, um Netzwerkflußprobleme effizient zu lösen. Nach einer kurzen Wiederholung der Grundbegriffe
(Max-Flow-Min-Cut-Theorem, klassische Algorithmen) stellen wir Algorithmen für das
Maximum-Flow-Problem vor, die schneller sind als die klassischen Algorithmen, die mit
flußvergrößernden Pfaden arbeiten. Wir zeigen dann, wie man mit Hilfe von geeigneten
Datenstrukturen deutliche Beschleunigungen erreichen kann.
Anhang C beschäftigt sich noch einmal kurz mit dem Algorithmus von Dijkstra zur Bestimmung kürzester Wege. Wir zeigen hier kurz, daß für ganzzahlige Längen auf den Kanten
unter bestimmten Voraussetzungen noch Laufzeitverbesserungen gegenüber den in vorhergehenden Kapiteln vorgestellten Implementierungen möglich sind. Ein Spezialfall ergibt
sich im Fall von ungewichteten Graphen. Die Breitensuche (engl. Breadth-First-Search)
ist ein nützliches Hilfsmittel, um in ungewichten Graphen in linearer Zeit kürzeste Wege
zu berechnen. Sie wird vor allem in Kapitel 7 benötigt und ist daher in Abschnitt C.2 der
Vollständigkeit halber aufgeführt und analysiert.
1.1 Zielgruppe und Voraussetzungen
Das Skript und die zugrundeliegende Vorlesung richten sich an Studenten der Mathematik
und Informatik im Grund- und Hauptstudium. Grundkenntnisse der Kombinatorischen Optimierung (Graphen, Netzwerke) sowie über elementare Datenstrukturen und Algorithmen
(Sortieren, Suchen) sind hilfreich, aber nicht zwingend erforderlich. Das Material ist als
Ergänzung zur Standardvorlesung »Algorithmen und Datenstrukturen« oder »Algorithmische Diskrete Mathematik« gedacht. Daher werden einige Themen, nur kurz oder gar nicht
angesprochen. Im Anhang dieses Skripts sind ein paar Grundlagen (O-Notation, Berechnungsmodell, etc.) erklärt. Eine hervorrangende Einführung bietet hier das Buch [3].
1.2 Danksagung
Ich möchte mich bei allen Teilnehmern der Vorlesung für Ihre Kommentare und Fragen
zum Stoff bedanken. Besonderer Dank gilt Diana Poensgen und Adrian Zymolka für zahlreiche konstruktive Verbesserungsvorschläge und das nimmermüde Finden von Tippfehlern
im Skript.
1.3 Literatur
Bücher zum Thema sind:
1.3 Literatur
Ref. Nr.
3
Buch
Preis
[3]
T. Cormen, C. Leiserson, R. L. Rivest, C. Stein. Introduction to Algorithms.
76,– EUR
[1]
R. K. Ahuja, T. L. Magnanti, J. B. Orlin. Network Flows.
78,– EUR
[4]
A. Fiat and G. J. Woeginger (eds.). Online Algorithms: The
State of the Art.
35,– EUR
4
Haufenweise Haufen: Heaps,
d-Heaps, Intervall-Heaps,
Binomial-Heaps und
Leftist-Heaps
Heaps (deutsch: Haufen) sind Datenstrukturen, um effizient sogenannte Prioritätsschlangen zu verwalten. Prioritätsschlangen stellen folgende Operationen zur Verfügung:
M AKE () erstellt eine leere Prioritätsschlange.
I NSERT (Q, x) fügt das Element x ein, dessen Schlüssel key[x] bereits korrekt gesetzt ist.
M INIMUM(Q) liefert einen Zeiger auf das Element in der Schlange, das minimalem
Schlüsselwert besitzt.
E XTRACT-M IN (Q) löscht das Element mit minimalem Schlüsselwert aus der Schlange
und liefert einen Zeiger auf das gelöschte Element.
D ECREASE -K EY (Q, x, k) weist dem Element x in der Schlange den neuen Schlüsselwert k zu. Dabei wird vorausgesetzt, daß k nicht größer als der aktuelle Schlüsselwert von x ist.
Prioritätsschlangen spielen bei vielen Algorithmen eine wichtige Rolle, etwa beim
Dijkstra-Algorithmus zur Berechnung kürzester Wege in einem gewichteten Graphen.
Die Abschnitte 2.1 und 2.2 sind vorwiegend als Einführung und Wiederholung gedacht.
Anhand des Algorithmus von Dijkstra zeigen wir in Abschnitt 2.1 auf, wo Prioritätsschlangen effektiv eingesetzt werden. Wir stellen in Abschnitt 2.2 die einfachste HeapDatenstruktur, den binären Heap, vor. Dieser Heap kommt ohne Zeiger aus und ist für
viele Anwendungen bereits hervorragend geeignet. Allerdings besitzt der binäre Heap
ein paar Defizite. In Abschnitt 2.3 stellen wir Erweiterungen des binären Heaps vor. Abschnitt 2.5 führt dann die erste kompliziertere Heap-Datenstruktur vor, den Binomial-Heap.
Der Binomial-Heap bietet alle Operationen des binären Heaps mit der gleichen Zeitkomplexität, stellt aber zusätzlich noch das Vereinigen von Heaps in logarithmischer Zeit zur
Verfügung. In Abschnitt 2.6 beschäftigen wir uns mit den sogenannten Leftist-Heaps. Diese
Heaps sind sehr einfach implementierbar und ermöglichen alle Operationen mit der gleichen Zeitkomplexität wie die Binomial-Heaps bis auf D ECREASE -K EY.
6
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
2.1 Der Algorithmus von Dijkstra
Sei G = (V, E) ein endlicher ungerichteter Graph ohne Parallelen1 und c : E → R≥0 eine
Gewichtsfunktion. Wir bezeichnen wie üblich mit n := |V | die Anzahl der Ecken und mit
m := |E| die Anzahl der Kanten von G.
Wir nehmen dabei an, daß G = (V, E) in Adjazenzlistendarstellung gegeben ist. Zur Erinnerung: Die Adjazenzlistendarstellung von G besteht aus den Zahlen n und m, sowie
einem Array Adj von n Listen, für jede Ecke eine. Die Liste Adj[u] enthält (Pointer auf) alle Ecken v mit (u, v) ∈ E und zusätzlich das Gewicht der entsprechenden Kante (u, v). Da
G ungerichtet ist, erscheint jede ungerichtete Kante (u, v) zweimal, einmal via v ∈ Adj[u]
und einmal via u ∈ Adj[v]). Abbildung 2.1 zeigt ein Beispiel für die Adjazenzlistenspeicherung eines Graphen.
Sei δc (u, v) die Länge eines kürzesten Weges von u nach v in G bezüglich der Kantengewichtsfunktion c. Oft schreiben wir auch nur δ(u, v), wenn die Gewichtsfunktion c klar
ist. Der Algorithmus von Dijkstra ist ein bekannter Algorithmus zur Bestimmung von kürzesten Wegen. Er ist in Algorithmus 2.1 im Pseudocode angegeben. Abbildung 2.2 zeigt
ein Beispiel für die Ausführung des Dijkstra-Algorithmus.
Wir zeigen nun die Korrektheit des Dijkstra-Algorithmus.
Lemma 2.1 Für alle v ∈ V gilt nach der Initialisierung bis zum Abbruch des Algorithmus
d[v] ≥ δc (s, v).
Beweis: Der Beweis folgt durch einfache Induktion nach der Anzahl der Relaxierungen
R ELAX (nur durch eine Relaxierung kann d[v] überhaupt sinken). Nach null Relaxierungen ist die Aussage trivial. Angenommen, die Behauptung gelte bis nach der iten Relaxierung. In der (i + 1)ten Relaxierung R ELAX(u, v) wird höchstens d[v] verändert. Wenn d[v]
unverändert bleibt, so ist nichts zu zeigen, ansonsten sinkt d[v] auf d[u] + c(u, v). Nach
Induktionsvoraussetzung gilt d[u] + c(u, v) ≥ δc (s, u) + c(u, v) ≥ δc (s, v). Dies zeigt den
Induktionsschritt.
2
Satz 2.2 Beim Abbruch des Algorithmus von Dijkstra gilt d[v] = δ c (v) für alle v ∈ V .
Weitherin ist für jedes v ∈ V mit d[v] < +∞ der Knoten p[v] Vorgänger von v auf einem
kürzesten Weg von s nach v .
Beweis: Wir nennen im Folgenden einen Weg w = (v1 , . . . , vp+1 ) einen Grenzweg, falls
v1 , . . . , vp ∈ S und vp+1 ∈ V \S sind. Sei Si die Menge S nach der iten Iteration der while
Schleife, es gilt also |Si | = i. Wir zeigen durch Induktion nach i, daß folgende Invarianten
gelten:
(i) Für alle u ∈ Si gilt d[u] = δ(s, u), und es existiert ein Weg von s nach u der
Länge d[u], der nur Knoten aus Si durchläuft.
(ii) Für alle u ∈ V \ Si gilt
d[u] = min{ c(w) : w ist Grenzweg mit Startknoten s und Zielknoten u }
bzw. d[u] = ∞, falls kein solcher Grenzweg existiert.
(Induktionsanfang): i = 1
1 Parallelen spielen bei der Berechnung kürzester Wege keine Rolle. Wir können jeweils die kürzeste der Parallelen im Graphen behalten und alle anderen vorab eliminieren.
2.1 Der Algorithmus von Dijkstra
7
1
2
4
v
Adj[v]
1
2 7
3 4
2
1 7
4 1
3
1 4
5 5
4
2 1
5 2
5
2 1
3 5
6
NULL
5 1
7
1
2
1
4
6
5
3
4 2
5
(a) Bei der Speicherung eines ungerichteten Graphen taucht jede Kante (u, v) zweimal
auf, je einmal in der Adjazenzliste der beiden Endknoten.
v
Adj[v]
1
2 7
3 4
2
4 1
5 1
3
5 5
4
5 2
5
NULL
6
NULL
1
2
4
7
1
2
1
4
5
3
6
5
(b) Bei der Speicherung eines gerichteten Graphen wird jede Kante genau
einmal abgespeichert.
Abbildung 2.1: Adjazenzlistenspeicherung von Graphen. Die grau hinterlegten Einträge
in den Listenelementen sind die Knotennummern, die anderen Einträge bezeichnen die
Kantengewichte.
8
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
Algorithmus 2.1 Algorithmus von Dijkstra
D IJKSTRA -S HORTEST-PATH(G, c, s)
Input:
Ein ungerichteter Graph G = (V, E) in Adjazenzlistendarstellung; eine
nichtnegative Gewichtsfunktion c : E → R≥0 und ein Knoten s ∈ V .
Output: Für jeden Knoten v ∈ V die Länge d[v] eines kürzesten Weges von s nach v;
zusätzlich noch einen Zeiger p[v] auf den Vorgänger von v im kürzesten Weg
von s nach v.
1 for all v ∈ V do
2
d[v] ← +∞
{ Bisher wurde noch kein Weg gefunden. }
3
p[v] ← NULL
4 end for
5 d[s] ← 0
6 Q ← M AKE ()
{ Erzeuge eine leere Prioritätschlange Q. }
7 I NSERT (Q, s)
{ Füge s mit Schlüssel d[s] = 0 in die Prioritätsschlange Q ein. }
8 S ←∅
{ S enthält die Knoten u mit d[u] = δ(s, u). }
9 while Q 6= ∅ do
10
u ← E XTRACT-M IN (Q)
11
S ← S ∪ {u}
12
for all v ∈ Adj[u] do
13
R ELAX(u, v)


R ELAX(u, v) prüft, ob über den Knoten u und die Kan- 



te (u, v) ein kürzerer Weg von s nach v gefunden werden

 kann als der bereits bekannte Weg von s nach v (sofern 

ein solcher existiert).
14
end for
15 end while
R ELAX(u, v)
1 if d[v] = +∞ then
{ Es war noch kein Weg von s nach v bekannt. }
2
d[v] ← d[u] + c(u, v)
3
p[v] ← u
4
I NSERT(Q, v)
5 else
6
if d[v] > d[u] + c(u, v) then { Der bekannte Weg war länger als d[u] + c(u, v). }
7
d[v] ← d[u] + c(u, v)
8
D ECREASE -K EY(Q, v, d[u] + c(u, v))
{ Vermindere den Schlüssel d[v] von v in Q auf d[u] + c(u, v). }
9
p[v] ← u
10
end if
11 end if
2.1 Der Algorithmus von Dijkstra
+∞
1
2
9
+∞
7
4
1
+∞
2
7
4
7
0
1
2
1
4
+∞
0
6
1
+∞
5
5
+∞
+∞
2
3
5
4
+∞
(b) Der Knoten 1 wird als Minimum aus der Prioritätsschlange entfernt. Für alle Nachfolger werden die
Distanzmarken d mittels R ELAX korrigiert.
(a) Initialisierung, der Startknoten ist
der Knoten 1.
1
6
5
3
7
2
1
4
+∞
7
4
8
1
2
7
4
7
0
1
2
1
4
+∞
0
6
1
+∞
5
5
4
9
(c) Der Knoten 3 wird als Minimum
aus der Prioritätsschlange entfernt.
1
2
6
5
3
7
2
1
4
3
5
4
8
(d) Der Knoten 2 wird als Minimum aus der Prioritätsschlange entfernt. Dabei wird unter anderem die
Distanzmarke von Knoten 5 von 9
auf 8 verringert.
8
7
4
2
7
1
8
4
7
0
1
2
1
4
+∞
0
6
1
+∞
2
1
4
5
6
5
3
5
4
8
(e) Der Knoten 4 wird als Minimum
aus der Prioritätsschlange entfernt.
3
5
4
8
(f) Der Knoten 5 wird als Minimum
aus der Prioritätsschlange entfernt.
Danach terminiert der Algorithmus,
da die Prioritätsschlange leer ist.
Abbildung 2.2: Arbeitsweise des Dijkstra-Algorithmus auf einem ungerichteten Graphen.
Die Zahlen an den Knoten bezeichnen die Distanzmarken d, die vom Algorithmus vergeben werden. Die schwarz gefärbten Knoten sind diejenigen Knoten, die in die Menge S
aufgenommen wurden. Der weiße Knoten ist der Knoten, der gerade als Minimum aus der
Prioritätsschlange entfernt wurde.
10
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
Mit d[s] = 0 folgt S1 = {s} und (i) gilt offensichtlich. Nach der for-Schleife ab Zeile 12
gilt für alle u ∈ V \ S1 , daß
d[u] = min{ c(s, u) : (s, u) ∈ E }
bzw. d[u] = ∞, falls es kein (s, u) ∈ Adj[s]. Da Adj[s] alle Grenzwege enthält, folgt
Aussage (ii).
(Induktionsvoraussetzung): Es gelten die Invarianten (i) und (ii) für ein i.
(Induktionsschritt): i → i + 1
Sei Si+1 = Si ∪ {v}. Nach Konstruktion des Algorithmus gilt dann d[v] < +∞. Nach
Lemma 2.1 gilt d[v] ≥ δc (s, v). Wäre d[v] > δc (s, v), so existiert ein Weg w von s nach v
mit c(w) < d[v].
Nach der Induktionsvoraussetzung (ii) hat für Si der minimale Grenzweg von s nach v die
Länge d[v]. Somit ist w kein Grenzweg für Si . Sei u der erste Knoten von w, der nicht in Si
liegt, und sei w 0 der Teilweg von w mit w 0 = (s, . . . , u). Dann ist w 0 ein Grenzweg von s
nach v, und mit Induktionsvoraussetzung (ii) folgt c(w 0 ) ≥ d[u].
Da im (i+1)ten Schritt v das minimale Heap-Element war, gilt d[u] ≥ d[v]. Da c eine nichtnegative Gewichtsfunktion ist, folgt c(w) ≥ c(w 0 ) ≥ d[v] im Widerspruch zur Annahme,
daß c(w) < d[v]. Somit folgt d[v] = δc (s, v). Nach Induktionsvoraussetzung, Teil (ii)
existiert zudem ein Grenzweg von s nach v der Länge d[v], der damit gleichzeitig der
kürzeste Weg von s nach v ist. Dies zeigt (i).
Es verbleibt zu zeigen, daß (ii) gilt. Sei dazu u ∈ V \ Si+1 . Wir bezeichnen mit d[u] und
d[v] die Werte d bei Entfernen von v aus dem Heap, aber vor den R ELAX-Aufrufen.
Für die Länge c(w) eines kürzesten Si+1 -Grenzweges w von s nach u gilt:
c(w) = min min{ c(w0 ) : w0 ist Si -Grenzweg von s nach u },
δ(s, v) + min{ c(v, u) : (v, u) ∈ E }
(2.1)
(2.2)
Nach Induktionsvoraussetzung ist der Term in (2.1) gleich d[u] und der Term in (2.2) entspricht
d[v] + min{ c(v, u) : (v, u) ∈ E }
Somit gilt:
c(w) = min d[u], d[v] + min{ c(v, u) : (v, u) ∈ E }
(2.3)
Nach den D ECREASE -K EY-Operationen, die auf das Entfernen von v aus Q folgen, wird
der neue Wert d[u] aber genau auf den Wert aus (2.3) gesetzt. Dies zeigt (ii).
2
Wir analysieren nun die Laufzeit des Dijkstra-Algorithmus. Diese kann wie folgt abgeschätzt werden: Jeder Knoten wird maximal einmal in die Prioritätsschlange Q eingefügt,
jeder Knoten wird maximal einmal aus Q entfernt. Weiterhin gibt es maximal 2m := 2|E|
Operationen, welche Schlüsselwerte verringern. Wir erhalten somit:
Satz 2.3 Die Laufzeit des Dijkstra-Algorithmus liegt in
O(n + n · TINSERT (n) + n · TEXTRACT-M IN (n) + m · TDECREASE -K EY (n)).
Hierbei bezeichnen TINSERT (n), TEXTRACT-MIN (n) und TDECREASE -K EY (n) die Zeitkomplexitäten zum Einfügen, Entfernen des Minimums und zum Verringern des Schlüssels in einer
2
Prioritätsschlange mit n Elementen.
2.2 Binäre Heaps
11
Um den Dijkstra-Algorithmus möglichst schnell zu machen, müssen wir die Prioritätsschlange möglichst effizient implementieren. Eine Möglichkeit, die Prioritätsschlange Q
zu verwalten, ist, einfach das unsortierte Array d zu benutzen (dies wurde übrigens in der
ursprünglichen Arbeit von Dijkstra so vorgeschlagen). Der Eintrag d[v] ist dann einfach an
der Stelle v im Array gespeichert. I NSERT(Q, v) und D ECREASE -K EY(Q, v, k) sind dann
trivial zu implementieren: wir ändern einfach den entsprechenden Eintrag im Array. Dies
ist in O(1) Zeit möglich. Bei E XTRACT-M IN müssen wir das ganze Array durchlaufen,
um das Minimum zu bestimmen. Das kostet uns Θ(n) Zeit. Wenn man diese Zeiten für die
Prioritätsschlangen-Operationen einsetzt, erhält man folgendes Ergebnis:
Beobachtung 2.4 Mit Hilfe eines Arrays als Datenstruktur für die Prioritätsschlange benötigt der Dijkstra-Algorithmus O(m + n2 ) Zeit auf einem Graphen mit n Ecken und
m Kanten.
Mit Hilfe von ausgeklügelten Datenstrukturen werden wir die oben angegebene Zeitschranke im Folgenden deutlich verbessern.
2.2 Binäre Heaps
Ein binärer Heap ist ein Array A, welches man als „fast vollständigen“ binären Baum mit
besonderen Eigenschaften auffassen kann. Ein Array, welches einen binären Heap repräsentiert, hat folgende Attribute:
• length[A] bezeichnet die Größe des Arrays;
• size[A] speichert die Anzahl der im Heap abgelegten Elemente.
Für einen Heap-Knoten 1 ≤ i ≤ size[A] ist parent(i) := bi/2c der Vater von i im Heap.
Umgekehrt sind für einen Knoten j dann left(i) := 2i und right(i) := 2i + 1 der linke
und der rechte Sohn2 im Heap (sofern diese existieren). Abbildung 2.3 zeigt einen Heap
und seine Visualisierung als Baum.
2
2
1
2
3
5
4
9
6
5
8
11
5
20
8
9
11
20
Abbildung 2.3: Ein Heap als Array und seine Visualisierung als Baum.
Die entscheidende Heap-Eigenschaft ist, daß für alle 1 ≤ i ≤ size[A] gilt:
A[i] ≥ A[parent(i)].
(2.4)
Folglich steht in der Wurzel des Baumes bzw. in A[1] das kleinste Element. Einen Heap mit
der Eigenschaft (2.4) nennt man auch minimum-geordnet. Analog dazu kann man natürlich
auch maximum-geordnete Heaps betrachten, bei denen das Ungleichheitszeichen in (2.4)
umgekehrt ist. Hier steht dann das größte Element in der Wurzel.
Man sieht leicht, daß der Baum, den ein binärer Heap mit size[A] = n repräsentiert, eine
Höhe von blog2 nc = O(log n) besitzt: auf Höhe h, h = 0, 1, . . . befinden sich maximal
2 In diesem Skript verwenden wir aus historischen Gründen die Begriffe »Sohn« und »Vater« für Knoten in
Bäumen. Natürlich könnten wir genausogut »Tochter« und »Mutter« verwenden.
12
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
Operation
M AKE
I NSERT
M INIMUM
E XTRACT-M IN
D ECREASE -K EY
B UILD
binärer Heap
O(1)
O(log n)
O(1)
O(log n)
O(log n)
O(n)
d-närer Heap
O(1)
O(logd n)
O(1)
O(d · logd n)
O(logd n)
O(n)
Tabelle 2.1: Zeitkomplexität der Prioritätsschlangen-Operationen bei Implementierung
durch einen binären Heap der Größe n und einen d-nären Heap der Größe n.
2h Knoten, und, bevor ein Knoten auf Höhe h existiert, müssen alle Höhen h 0 < h bereits
voll sein.
Die Prioritätsschlangen-Operationen lassen sich sehr einfach im binären Heap implementieren. Das Erstellen eines leeren binären Heaps (siehe Algorithmus 2.2) und das Liefern
des Minimums (siehe Algorithmus 2.3) sind nahezu trivial und benötigen nur konstante
Zeit.
Das Einfügen eines neuen Elements x in den Heap funktioniert wie folgt. Angenommen,
der aktuelle Heap habe n Elemente. Wir fügen das Element an die Position n + 1 an. Im
Baum bedeutet dies, daß x Sohn des Knotens b(n + 1)/2c wird. Jetzt lassen wir x durch
sukzessives Vertauschen mit seinem Vaterknoten soweit im Baum »hochsteigen«, bis die
Heap-Eigenschaft wiederhergestellt ist. Der Code für das Einfügen ist in Algorithmus 2.4
beschrieben, Abbildung 2.4 zeigt ein Beispiel. Da ein binärer Heap für n Elemente die
Höhe O(log n) besitzt, benötigen wir zum Einfügen O(log n) Zeit.
Beim Extrahieren des Minimums (siehe Algorithmus 2.5) ersetzen wir A[1] durch das letzte Element y des Heaps. Nun lassen wir y im Heap durch Vertauschen mit dem kleineren
seiner Söhne soweit im Heap »absinken«, bis die Heap-Eigenschaft wieder erfüllt ist. Abbildung 2.5 zeigt ein Beispiel. Das Extrahieren des Minimums benötigt ebenfalls nur logarithmische Zeit, da wir pro Ebene des Baumes nur konstanten Aufwand investieren und der
Baum logarithmische Höhe besitzt.
Das Verringern des Schlüsselwerts eines Elements an Position j läuft analog zum Einfügen
ab und ist in Algorithmus 2.6 dargestellt. Nach Verringern des Schlüsselwerts lassen wir
das Element im Heap durch sukzessives Vertauschen mit dem Vaterknoten aufsteigen, bis
die Heap-Ordnung wieder hergestellt ist. Auch hier erhält man eine logarithmische Zeitkomplexität. Tabelle 2.1 fasst die Zeitkomplexitäten für die Operationen im binären Heap
zusammen.
Algorithmus 2.2 Erstellen eines leeren binären Heaps
M AKE()
1 size[A] ← 0
Algorithmus 2.3 Minimum eines binären Heaps
M INIMUM(A)
1 return A[1]
Bei Implementierung des Dijkstra-Algorithmus mit Hilfe eines binären Heaps ergibt sich
aus Satz 2.3 und den Komplexitäten in Tabelle 2.1 eine Laufzeit von O(n + n · log n + m ·
log n) = O((n + m) log n):
2.2 Binäre Heaps
13
Algorithmus 2.4 Einfügen eines neuen Elements in einen binären Heap
I NSERT(A, x)
1 if size[A] = length[A] then
2
return „Der Heap ist voll“
3 else
4
size[A] = size[A] + 1
5
i ← size[A]
6
A[i] ← x
7
B UBBLE -U P(A, i)
8 end if
B UBBLE -U P(A, i)
1 while i > 1 und A[i] < A[parent(i)] do
2
Vertausche A[i] und A[parent(i)].
3
i ← parent(i)
4 end while
2
2
9
5
8
11
20
9
5
13
12
8
12
11
4
2
2
5
12
9
11
13
(b) Einfügen des neuen Elements 4.
(a) Ausgangsheap.
4
20
20
9
4
13
8
(c) Nach einer Vertauschung mit den Vaterknoten.
5
12
11
20
13
8
(d) Endposition, die Heap-Ordnung ist wiederhergestellt.
Abbildung 2.4: Einfügen des neuen Elements 4 in einen binären Heap. Das neue Element
wird unten in den Heap eingefügt und steigt dann durch Vertauschen mit den Vaterknoten
solange auf, bis die Heap-Ordnung wiederhergestellt ist.
14
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
Algorithmus 2.5 Extrahieren des Minimums in einem binären Heap
E XTRACT-M IN(A)
1 r := A[1]
{ Das Minimum, welches zurückgeliefert wird. }
2 A[1] := A[size(A)]
{ Das alte Minimum wird überschrieben. }
3 size[A] = size[A] − 1
4 i←1
{ Das neue Element in A[1] muß nun im Heap absinken, bis die
Heap-Eigenschaft wieder hergestellt ist. }
5 while i < size[A] do
6
j ← left(i)
7
if right(i) ≤ size[A] und A[right(i)] < A[left(i)] then
8
j ← right(i)
9
end if
10
if A[i] > A[j] then
{ A[j] ist der Sohn mit den kleinsten Schlüsselwert. }
11
Vertausche A[i] und A[j].
12
i←j
13
else
14
return r
15
end if
16 end while
17 return r
2
8
9
4
5
20
11
13
8
12
9
4
5
13
12
(a) Ausgangsheap.
(b) Die Wurzel wird durch das letzte Element ersetzt.
4
4
9
8
5
20
11
11
20
9
5
13
12
8
11
20
13
12
(c) Vertauschen mit den kleineren Sohn.
(d) Endposition.
Abbildung 2.5: Extrahieren des Minimums in einem binären Heap.
Algorithmus 2.6 Verringern des Schlüsselwerts des Elements an Position j in einem binären Heap
D ECREASE -K EY(A, j, k)
1 i←j
2 A[i] ← k
3 B UBBLE -U P (A, i)
{ B UBBLE -U P steht in Algorithmus 2.4 auf Seite 13. }
2.2 Binäre Heaps
Beobachtung 2.5 Mit Hilfe eines binären Heaps als Datenstruktur für die Prioritätsschlange benötigt der Dijkstra-Algorithmus O((n + m) log n) Zeit auf einem Graphen mit
n Ecken und m Kanten.
Abschließend soll noch erwähnt werden, wie man einen binären Heap für n Elemente in
linearer Zeit O(n) aufbaut (die »offensichtliche Lösung« mit n-fachem Einfügen eines
Elements führt auf die Zeitkomplexität O(n log n)).
Algorithmus 2.7 Algorithmus zum Herstellen der Heap-Eigenschaft im Array A[i], . . . , A[size[A]], wobei angenommen wird, daß die Teilheaps mit Wurzeln
left(i) und right(i) bereits korrekt geordnet sind.
H EAPIFY(A, i)
1 l ← left(i)
2 r ← right(i)
3 if l < size[A] und A[i] < A[l] then
4
s←l
5 else
6
s←i
7 end if
8 if r < size[A] und A[r] < A[s] then
9
s←r
10 end if
11 if s 6= i then
12
Vertausche A[i] und A[s]
13
H EAPIFY(A, s)
14 end if
Algorithmus 2.8 Algorithmus zum Erstellen eines Heaps aus Elementen in einem Array
in linearer Zeit
B UILD -H EAP(A)
1 size[A] ← length[A]
2 for i ← blength[A]/2c, . . . , 1 do
3
H EAPIFY(A, i)
4 end for
Ein wichtiges Unterprogramm für das Erstellen eines Heaps ist die Prozedur H EAPIFY aus
Algorithmus 2.7. Beim Aufruf H EAPIFY(A, i) wird vorausgesetzt, daß die binären Bäume
mit Wurzeln left(i) und right(i) bereits Heap-geordnet sind. H EAPIFY läßt nun A[i] so
lange im Heap weiter nach unten sinken, indem es A[i] rekursiv immer mit dem kleinsten
Sohn vertauscht, bis die Heap-Eigenschaft auch im Array A[i], . . . , A[size[A] gilt. Man
sieht leicht, daß die Laufzeit von H EAPIFY auf einem Heap der Höhe h in O(h) ist.
Zum Erstellen eines Heaps (B UILD -H EAP in Algorithmus 2.8) nutzen wir H EAPIFY wie
folgt. Die Elemente A[bn/2c + 1], . . . , A[n] sind Blätter im binären Baum. Man kann sie
also als Wurzeln von einelementigen Heaps betrachten. Zu Beginn von H EAPIFY ist induktiv jeder Knoten i + 1, . . . , n Wurzel eines Heaps. Durch den Durchlauf wird dann i mit
Hilfe von H EAPIFY korrekt zur Wurzel eines Heaps.
Da in einem n elementigen Heap maximal dn/2h+1e Knoten der Höhe h existieren, wird
H EAPIFY höchstens dn/2h+1 e mal für einen Heap der Höhe h aufgerufen. Die gesamte
15
16
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
Zeitkomplexität von B UILD -H EAP ist daher:
blog nc l
X
h=0
n m
2h+1


X h

O(h) = O n
2h
h=0
!
∞
X
h
≤n·O
2h
blog nc
h=0
= n · O (1)
= O(n).
2.3 Erweiterungen von binären Heaps
2.3.1 d-näre Heaps
Als naheliegende Verallgemeinerung von binären Heaps bieten sich die d-nären Heaps an,
in denen jeder Knoten nicht zwei sondern bis zu d Söhne hat. Für einen Heap-Knoten
1 ≤ i ≤ size[A] ist dann parent(i) := bi/dc der Vater von i im Heap. Umgekehrt sind
für einen Knoten j dann d · (j − 1) + 2, . . . , min{(d · (j − 1) + d + 1, size[A]} die Söhne
von j.
Die Implementierung der Prioritätsschlangen-Operationen in d-nären Heaps ist eine einfache Erweiterung der Implementierung für binäre Heaps. Man erhält die Zeitkomplexitäten, die in Tabelle 2.1 aufgeführt sind. Für den Algorithmus von Dijkstra bedeutet dies
eine Laufzeit von O(m logd n + nd logd n). Was haben wir im Vergleich zum binären
Heap gewonnen? Wir können den Parameter d so wählen, daß die bestmögliche Laufzeit daraus resultiert. Dazu wählen wir d dergestalt, daß beide Terme in der O-Notation
identisch werden: d = max{2, dm/ne}. Dies ergibt eine Laufzeit von O(m log d n) =
O(m logmax{2,dm/ne} n).
Beobachtung 2.6 Mit Hilfe eines d-nären Heaps (d = max{2, dm/ne}) als Datenstruktur
für die Prioritätsschlange benötigt der Dijkstra-Algorithmus O(m log max{2,dm/ne} n) Zeit
auf einem Graphen mit n Ecken und m Kanten.
Für dünne Graphen, d.h. m = O(n), ist die Laufzeit dann O(n log n). Für dichtere Graphen mit m = Ω(n1+ε ) für ein ε > 0 erhalten wir
O(m logd n) = O(m log n/ log d) = O(m log n/ log nε ) = O(m/ε) = O(m),
wobei die letzte Gleichheit folgt, da ε > 0 konstant ist. Für diesen Fall erhalten wir also eine
lineare Laufzeit. Dies ist sicherlich optimal, da jeder korrekte Algorithmus für kürzestes
Wege zumindest jede der m Kanten einmal betrachten muß.
2.3.2 Intervall-Heaps
Eine zweiseitige Prioritätsschlange unterstützt zusätzlich zu den Operationen M AKE, I N SERT , M INIMUM , E XTRACT-M IN und D ECREASE -K EY auch die Operationen M AXI MUM , E XTRACT-M AX und I NCREASE -K EY .
Eine zweiseitige Prioritätsschlange ist nicht effizient mit Hilfe eines einfachen binären
Heaps implementierbar: Wenn der Heap minimum-geordnet ist (wie wir das bisher immer angenommen haben), so erfordert etwa M AXIMUM das komplette Durchsuchen des
Heaps, was Ω(n) Zeit bei n Elementen im Heap erfordert.
Herunterladen