close

Вход

Забыли?

вход по аккаунту

?

3589.Algorithmen und Datenstrukturen 001 .pdf

код для вставкиСкачать
Algorithmen und Datenstrukturen
Sven O. Krumke
Entwurf vom 7. Februar 2003
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
krumke@zib.de
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
16
20
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
6
2
1
4
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
3
5
4
9
(c) Der Knoten 3 wird als Minimum
aus der Prioritätsschlange entfernt.
7
1
2
6
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
1
2
7
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
5
8
9
11
20
5
13
12
8
12
9
11
4
2
2
9
5
12
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
4
5
12
9
4
20
11
13
8
5
9
13
12
(a) Ausgangsheap.
(b) Die Wurzel wird durch das letzte Element ersetzt.
4
4
8
5
20
11
9
11
20
5
13
12
8
9
11
20
12
(c) Vertauschen mit den kleineren Sohn.
(d) Endposition.
Abbildung 2.5: Extrahieren des Minimums in einem binären Heap.
13
2.2 Binäre Heaps
15
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. }
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
16
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
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
Zeitkomplexität von B UILD -H EAP ist daher:


blog nc l
blog nc
m
X
X
n
h
O(h) = O n
2h+1
2h
h=0
h=0
!
∞
X
h
≤n·O
2h
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 .
2.3 Erweiterungen von binären Heaps
17
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.
Eine mögliche Lösung wäre der Aufbau von zwei Heaps für die zu verwaltenden Elemente, einer minimum-geordnet und einer maximum-geordnet. Jedes Element wird dabei in
jedem Heap einmal, also insgesamt zweimal, abgelegt. Während jetzt M INIMUM und M A XIMUM in O(1) Zeit ablaufen und I NSERT in O(log n) Zeit implementierbar ist, erfordern
E XTRACT-M IN und E XTRACT-M AX wiederum Ω(n) Zeit.
Als Ausweg bieten sich die sogenannten Intervall-Heaps an. Wir beschreiben zunächst
den Intervall-Heap über seine Repräsentation als binärer Baum. Genauso wie beim binären
Heap ist jedoch eine Implementierung als Array ohne Zeiger problemlos möglich, wie wir
gleich zeigen werden.
In einem Intervall-Heap gehört zu jedem Heap-Knoten v ein Intervall I(v) = [a, b] mit der
möglichen Ausnahme des letzten Knotens, der entweder ein Intervall oder auch nur einen
reellen Wert enthält. Wir schreiben hier zur Vereinfachung der Notation auch x ⊆ [a, b],
wenn x ∈ [a, b]. Ein Intervall-Heap ist gemäß der Halbordnung ⊆ max-Heap-geordnet: Ist
p Vater von v im Heap, so gilt I(v) ⊆ I(p).
Wir kommen jetzt zur Implementierung des Intervall-Heaps als Array. Abbildung 2.6 veranschaulicht einen Intervall-Heap und die im folgenden vorgestellte Abbildung in ein Array. Ein Array, welches einen Intervall-Heap repräsentiert, hat wie beim binären Heap
die Attribute length[A] und size[A]. Die Array-Einträge für den iten Knoten im Heap
(1 ≤ i ≤ bsize[A]/2c + 1) stehen in A[2i − 1] und A[2i]. Ist 2i − 1 der Startindex des
iten Knotens im Heap, so ist 2bi/2c − 1 der Startindex des Vaterknotens.
[1,15]
[3,10]
[4,8]
[2,7]
[5,9]
2
1
4
3
15
1
10
9
4
3
6
5
10
2
8
7
7
4
9
8
5
11
4
Abbildung 2.6: Ein Intervall-Heap als binärer Baum und seine Abbildung in ein lineares
Array.
Wegen der Heap-Ordnung stehen in der Wurzel des Intervall-Heaps das minimale und das
maximale Element. Somit sind M INIMUM und M AXIMUM sehr einfach in O(1) Zeit implementierbar. Das Erstellen eines leeren Intervall-Heaps funktioniert ebenfalls in O(1) Zeit.
Wir beschreiben jetzt in Algorithmus 2.9 noch, wie das Einfügen in einem Interval-Heap
in O(log n) Zeit funktioniert. Die Implementierung ist eine mehr oder weniger einfache
Verallgemeinerung des Einfügens in einen binären Heap (siehe Algorithmus 2.4).
Wir haben in Schritt 21 von Algorithmus 2.9 nicht alle ermüdenden Details aufgeführt.
Das Prinzip sollte jedoch klar sein: Ist [L, R] kein Teilintervall des im Vaterknoten gespeicherten Intervalls [a, b], so gilt L < a oder R > b. In diesem Fall erhält der Vaterknoten das neue Intervall [min{a, L}, max{b, R}] und der Sohnknoten das Intervall
[max{a, L}, min{b, R}. Das Verfahren läuft dann beim Vaterknoten weiter oben im Heap
iterativ fort. Da ein Intervall-Heap nur logarithmische Tiefe besitzt und wir pro Ebene nur
konstante Zeit benötigen, läuft I NTERVAL -I NSERT in einem Intervall-Heap mit n Knoten
in O(log n) Zeit. In Abbildung 2.7 ist ein Beispiel zu sehen.
Die Operationen E XTRACT-M IN, E XTRACT-M AX, D ECREASE -K EY und I NCREASE K EY sind ebenfalls in O(log n) Zeit implementierbar. Da die Pseudocodes für die Operationen sehr unübersichtlich werden, wie man anhand von I NTERVAL -I NSERT sehen kann,
18
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
Algorithmus 2.9 Einfügen in einen Intervall-Heap.
I NTERVAL -I NSERT(H, x)
1 if size[A] = length[A] then
2
return „Der Heap ist voll“
3 end if
4 size[A] = size[A] + 1
5 j ← size[A]
6 if j ist ungerade, j = 2i − 1 then
{ Ein neuer Knoten wird angefügt. }
7
i ← dj/2e
8
A[2i − 1] ← x
9
L←x
10
R←x
11 else
{ j ist gerade, x wird als zweiter Wert in den letzten Knoten eingefügt. }
12
i ← dj/2
13
L ← min{x, A[2i − 1]}
14
R ← max{x, A[2i − 1]}
15
A[2i − 1] ← L
16
A[2i] ← R
17 end if
18 j ← bi/2c − 1 { Der Vaterknoten des Knotens mit Daten in A[2i − 1] und A[2i] hat
seine Daten in A[2j − 1] und A[2j]. }
19
{ Heap-Eigenschaft wiederherstellen. }
20 while 2i − 1 > 1 und [L, R] 6⊆ [A[2j − 1], A[2j]] do
21
Stelle die Heap-Eigenschaft lokal durch geeignetes Vertauschen der Intervallgrenzen
wieder her.
22
Speichere in L und R die neuen Intervallgrenzen des Vaterknotens.
23
i←j
24
j ← bi/2c − 1
25 end while
2.3 Erweiterungen von binären Heaps
19
[2,30]
[3,25]
[8,16]
[8,16]
[4,25]
[4,20]
[9,15]
[10,15]
[5,12]
5
(a) Der Ausgangsheap, in den 2 eingefügt werden soll.
[2,30]
[3,25]
[4,25]
[8,16]
[8,16]
[4,20]
[9,15]
[10,15]
[5,12]
[5,2]
(b) Das neue Element wird in den letzten Knoten geschrieben.
[2,30]
[3,25]
[4,25]
[8,16]
[8,16]
[4,20]
[9,15]
[10,15]
[5,12]
[2,5]
(c) Zunächst wird das Blatt so geordnet, daß die linke Intervallgrenze nicht größer als die
rechte ist.
[2,30]
[3,25]
[4,25]
[8,16]
[8,16]
[2,20]
[9,15]
[10,15]
[5,12]
[4,5]
(d) Durch Vertauschen der Intervallgrenzen mit denen des Vaterknotens (sofern nötigt), wird
die Heap-Ordnung zum Vater wiederhergestellt.
[2,30]
[2,25]
[8,16]
[8,16]
[4,25]
[3,20]
[9,15]
[10,15]
[5,12]
[4,5]
(e) Das Verfahren terminiert, wenn die Heap-Ordnung mit den aktuellen Vater erfüllt ist oder
wir in der Wurzel angelangt sind.
Abbildung 2.7: Einfügen I NTERVAL -I NSERT(Q, 2) in einen Intervall-Heap.
20
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
beschreiben wir die Implementierungen als Text. Die Umsetzung in Pseudocode ist einfach,
aber ermüdend.
E XTRACT-M IN Wenn der Heap nur ein einziges Element speichert, so ist die Umsetzung
trivial. Wir nehmen also an, daß die Wurzel des Heaps das Interval [a, b] speichert
und folglich bei E XTRACT-M IN das Element a gelöscht wird. Wir ersetzen nun a
durch den kleineren der beiden linken Intervallgrenzen der Söhne und führen diese
Prozedur dann im entsprechenden Sohn fort. Das Verfahren terminiert in einem Knoten v, dessen linke Intervallgrenze gerade in den Vaterknoten hochgeschoben wurde.
Falls v der letzte Knoten des Heaps ist, so müssen wir lediglich die möglicherweise
existierende rechte Intervallgrenze in v auf die linke verschieben. Ansonsten entfernen wir einen Wert im letzten Knoten des Heaps, fügen ihn in v ein und führen
dann das beim Einfügen beschriebene Verfahren hoch, in dem wir wieder den Heap
hochsteigen. Pro Ebene im Heap benötigt das Verfahren nur O(1) Zeit, so daß wegen der logarithmischen Höhe des Heaps insgesamt nur O(log n) Zeit benötigt wird.
Abbildungen 2.8 und 2.9 zeigen ein Beispiel für E XTRACT-M IN.
E XTRACT-M AX wird analog zu E XTRACT-M IN implementiert.
D ECREASE -K EY Falls eine linke Intervallgrenze erniedrigt wird, so bleibt die HeapEigenschaft »nach unten hin«, d.h. zu den Söhnen hin, erhalten. Wir müssen nun
nur noch die Heap-Eigenschaft nach oben garantieren. Dies funktioniert analog zu
I NTERVAL -I NSERT. Falls eine rechte Intervallgrenze erniedrigt wird, so ist das Verfahren etwas komplizierter. Sei v der aktuelle Knoten, in dem die rechte Grenze
erniedrigt wird. Wir stellen zunächst in v die Eigenschaft her, daß die linke Grenze
nicht größer als die rechte Grenze ist. Dabei kann die linke Grenze natürlich nur kleiner werden, so daß wir mit der linken Grenze nach unten hin keine Probleme haben
(möglicherweise nach oben, aber darum kümmern wir uns noch gleich). Die rechte Seite in v ist möglicherweise kleiner geworden, so daß wir die Heap-Eigenschaft
nach unten möglicherweise wiederherstellen müssen. Dies geschieht einfach, indem
wir die rechte Seite in v durch die größte rechte Seite in einem Sohn ersetzen. Dann
setzen wir das Verfahren in diesem Sohn fort. Nachdem wir in einem Blatt angelangt sind, ist die Heap-Eigenschaft ab v nach unten wiederhergestellt. Jetzt sichern
wir die Heap-Eigenschaft nach oben wieder durch das bekannte Hochschieben. Man
sieht leicht, daß das eben beschriebenen Verfahren nur O(log n) Zeit benötigt.
I NCREASE -K EY wird analog zu D ECREASE -K EY implementiert.
2.3.3 Eine Anwendung: Das eindimensionale komplementäre Bereichsproblem
Wir schließen den Abschnitt über die Intervall-Heaps mit einer nicht ganz offensichtlichen
Anwendung. Gegeben sei eine endliche Punktmenge X ⊂ R, in die dynamisch Punkte eingefügt und entfernt werden. Nun erhalten wir ein Intervall [a, b] (über seine Grenzen) und
suchen alle Punkte aus X, die nicht in [a, b] liegen. Zur Illustration stelle man sich etwa
ein Computerspiel vor, in dem X gewisse Standorte speichert und dann eine »Bombe«alle
Standorte im Bereich [a, b] trifft. Wir nennen das obige Problem das eindimensionale komplementäre Bereichsproblem.
Indem man X mit Hilfe eines balancierten Baums implementiert, kann man das 1Bereichsproblem in O(log |X| + k) Zeit lösen, wobei k =
dimensionale komplementäre
X ∩ (R \ [a, b]) die Größe der Ausgabe, also die Anzahl der auszugebenden Punkte, ist.
Mit Hilfe von Intervall-Heaps kann man diese Zeitkomplexität auf O(k) verbessern.
2.3 Erweiterungen von binären Heaps
21
[1,30]
[2,25]
[4,25]
[8,16]
[8,16]
[3,20]
[9,15]
[4,5]
[10,15]
[5,12]
10
(a) Der Ausgangs-Heap.
[2,30]
[2,25]
[4,25]
[8,16]
[8,16]
[3,20]
[9,15]
[4,5]
[10,15]
[5,12]
10
(b) Im Wurzelknoten entsteht eine »Lücke« durch Löschen des Minimums.
[2,30]
[2,25]
[4,25]
[8,16]
[8,16]
[3,20]
[9,15]
[4,5]
[10,15]
[5,12]
10
(c) Das kleinste linke Intervallende der Sohnknoten wird in die Wurzel hochgezogen. Das Verfahren läuft bei diesem Sohnknoten weiter.
[2,30]
[3,25]
[4,25]
[8,16]
[8,16]
[4,20]
[9,15]
[2,5]
[10,15]
[5,12]
10
(d) Das Verfahren endet zunächst in einem Blatt, das nun kein linkes Intervallende mehr hat.
Abbildung 2.8: E XTRACT-M IN in einem Intervall-Heap.
22
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
[2,30]
[3,25]
[8,16]
[8,16]
[4,25]
[4,20]
[9,15]
[10,15]
[5,12]
[10,5]
(a) Ein Wert aus dem letzten Knoten im Heap (hier hat der letzte Knoten nur einen Wert) wird
benutzt, um die »Lücke« aufzufüllen.
[2,30]
[3,25]
[8,16]
[8,16]
[4,25]
[4,20]
[9,15]
[10,15]
[5,12]
[5,10]
(b) Ab nun läuft das Verfahren wie beim Einfügen. Das Blatt wird so geordnet, daß die linke
Intervallgrenze nicht größer als die rechte ist. Durch rekursives Vertauschen mit Werten aus dem
Vaterknoten wird die Heap-Ordnung wiederhergestellt. Im Beispiel ist nichts mehr zu tun, das
Verfahren terminiert also sofort.
Abbildung 2.9: Fortsetzung: E XTRACT-M IN in einem Intervall-Heap.
2.4 Minimale aufspannende Bäume: Der Algorithmus von Boruvka (in Variation)
Wir organisieren die Punkte in X in einem Intervall-Heap. Zunächst bemerken wir, daß wir
das Löschen eines Punkts x ∈ X dadurch lösen können, daß wir D ECREASE -K EY(x, −∞)
und dann E XTRACT-M IN ausführen. Somit ist auch das Löschen in logarithmischer Zeit
ausführbar.
Sei nun [a, b] das Anfrageintervall. Wir starten in der Wurzel des Heaps und durchlaufen
den Heap rekursiv wie folgt: sei v der aktuelle Heap-Knoten und I(v) das in v gespeicherte
Intervall.
(i) Falls I(v) ⊆ [a, b], so müssen wir keinen der Punkte, die in v gespeichert sind,
ausgeben. Weiterhin liegen auch alle Punkte im Teilbaum, der in v wurzelt, in [a, b],
so daß wir die Rekursion hier beenden können.
(ii) Falls I(v) ∩ [a, b] ( I(v), so existiert mindestens ein Endpunkt von I(v), der nicht
in [a, b] liegt. Wir geben alle (maximal zwei) Endpunkte von I(v) aus, die nicht
in [a, b] liegen. Sofern v noch Söhne hat, setzen wir die Suche rekursiv in den entsprechenden Teilbäumen fort.
Die Laufzeit unseres Algorithmus ist offenbar linear in der Anzahl der besuchten Knoten im Heap. Wenn der Falls (ii) auftritt, so können wir die Kosten für das Besuchen des
Knotens v den gefundenen Endpunkten von I(v), die nicht in [a, b] liegen, zuordnen. Falls
Fall (i) im Knoten v eintritt, so lag Fall (ii) im Vater von v vor. Wir ordnen die Kosten
für das Besuchen von v den im Vaterknoten ausgegebenen Punkten zu. In unserem Kostenzuordnungsschema erhalten somit nur die auszugebenden Punkte in X ∩ (R \ [a, b])
Kosten zugeordnet. Außerdem erhält jeder dieser Punkte nur O(1) Kosten zugeordnet (da
jeder Knoten nur zwei Söhne hat). Somit läuft unsere Algorithmus in O(k) Zeit. Dies ist
übrigens eine optimale Zeitschranke, da wir gefordert hatten, daß jeder der k Punkte in
X ∩ (R \ [a, b]) ausgegeben wird.
2.4 Minimale aufspannende Bäume: Der Algorithmus
von Boruvka (in Variation)
Für manche Anwendungen ist es wichtig, daß die verwendete Prioritätsschlagen auch die
folgende Operation unterstützt:
M ELD (Q1 , Q2 ) erzeugt eine neue Datenstruktur, die alle Elemente von Q1 und Q2 enthält. Die Strukturen Q1 und Q2 werden dabei zerstört.
Als Motivation für die M ELD-Operation stellen wir einen einfachen Algorithmus zur Bestimmung eines minimalen aufspannenden Baumes vor. Wir wiederholen kurz die Definition eines aufspannenden Baumes, Details finden sich etwa in [3, Kapitel 23] oder [1].
Definition 2.7 (Aufspannender Baum, minimaler aufspannender Baum)
Sei G = (V, E) ein ungerichteter Graph. Ein aufspannender Baum von G ist ein Teilgraph T = (V, ET ) von G mit gleicher Eckenmenge, der zusammenhängend und kreisfrei
ist. Ist c : E → R eine Kantenbewertung, so heißt ein aufspanndender Baum T von G ein
minimaler aufspannender Baum (MST), wenn
c(T ) = min{ c(T 0 ) : T 0 ist aufspannender Baum von G }.
Falls der Graph G nicht zusammenhängend ist, so kann kein aufspannender Baum von G
existieren. Wir setzen daher im Weiteren voraus, daß die Eingabegraphen zusammenhängend sind.
23
24
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
Es läßt sich zeigen, daß ein aufspannender Baum eines Graphen mit n Ecken genau n −
1 Kanten besitzt, siehe [3, Kapitel 23] oder [1]. Zum Beweis der Korrektheit der MSTAlgorithmen in diesem Skript benötigen wir ein paar Definitionen und Hilfsaussagen.
Definition 2.8 (Sichere Kante)
Sei E 0 ⊆ E eine Teilmenge der Kanten von G mit folgender Eigenschaft:
Es gibt einen MST T ∗ = (V, ET ∗ ) von G mit E 0 ⊆ ET ∗ .
(2.5)
Eine Kante e ∈ E heißt sicher für E 0 , wenn E 0 ∪ {e} ebenfalls die Eigenschaft (2.5)
besitzt.
Definition 2.9 (Schnitt in einem ungerichteten Graphen)
Sei G = (V, E) ein ungerichteter Graph und A ∪ B = V eine Partition von V . Dann
nennen wir
[A, B] := { (a, b) ∈ E : a ∈ A und b ∈ B }
den von A und B erzeugten Schnitt.
Abbildung 2.10 zeigt ein Beispiel für einen Schnitt in einem (ungerichteten) Graphen.
A
A
B
B
A
A
A
B
B
Abbildung 2.10: Ein Schnitt [A, B] in einem Graphen. Die Kanten in [A, B] sind gestrichelt
hervorgehoben.
Satz 2.10 Sei G = (V, E) ein zusammenhängender ungerichteter Graph und c : E → R
eine Gewichtsfunktion. Sei E 0 ⊆ E mit der Eigenschaft (2.5). Sei [A, B] ein Schnitt von V
mit der Eigenschaft: [A, B] ∩ E 0 = ∅. Sei nun e eine billigste Kante aus [A, B]. Dann ist e
sicher für E 0 .
Beweis: Sei T ∗ ein MST mit E 0 ⊆ T ∗ . Ist e ∈ T ∗ , so ist nichts zu zeigen. Ansonsten
erzeugt die Hinzunahme von e zu T ∗ einen einfachen Kreis w = (e1 , . . . , ek ) mit o.B.d.A.
e1 = e. Der Kreis w muß eine Kante ei ∈ [A, B] mit ei 6= e enthalten. Nach Voraussetzung
gilt c(ei ) ≥ c(e). Dann erfüllt T := T ∗ \ {ei } ∪ {e}:
c(T ) ≤ c(T ∗ ).
Ferner ist T zusammenhängend und besitzt |V | − 1 Kanten, ist also ein Baum. Somit ist T
ein MST, der alle Kanten aus E 0 ∪ {e} enthält.
2
Wir kommen nun zum versprochenen einfachen MST-Algorithmus, der auf Boruvka zurückgeht und uns in späteren Kapiteln nochmals begegnen wird. Die Grundidee ist dabei
die folgende: wir starten mit leerer Kantenmenge T = ∅, so daß anfangs jeder Knoten
vi ∈ V eine eigene Zusammenhangskomponente bildet. Sei nun T zu einem Zeitpunkt
2.4 Minimale aufspannende Bäume: Der Algorithmus von Boruvka (in Variation)
unsere aktuelle Kantenmenge, die Teilmenge eines MST ist, und V 1 , . . . , Vk die Zusammenhangskomponenten von (V, T ). Sei (u, v) eine billigste Kante mit der Eigenschaft, daß
u ∈ V1 und v ∈ Vj mit j 6= 1. Nach Satz 2.10 ist (u, v) sicher für T , und wir können
(u, v) zu T hinzufügen. Dabei werden die Zusammenhangskomponenten V 1 und Vj zu
einer neuen Komponente verschmolzen.
Algorithmus 2.10 Algorithmus von Boruvka zur Bestimmung eines MST.
MST-B ORUVKA(G, c)
Input:
Ein zusammenhängender ungerichteter Graph G = (V, E) mit n := |V | und
m := |E| in Adjazenzlistendarstellung, eine Kantenbewertungsfunktion
c : E → R.
Output: Ein minimaler aufspannender Baum T von G.
1 T ←∅
{ Kanten im zu erstellenden MST. }
2 L←∅
{ Eine doppelt verkettete Liste. }
3 for all vi ∈ V do
4
Vi ← {vi }
5
Füge Vi hinten an L an.
6 end for
7 while L hat mehr als ein Element do
8
Vi ← erstes Element von L
{ Vi werde dabei aus L gelöscht. }
9
Finde die billigste Kante (u, v) mit u ∈ Vi und v ∈
/ Vi
10
Sei v ∈ Vj ∈ L. Entferne Vj aus L.
11
Füge Vi ∪ Vj hinten an L an.
12
T ← T ∪ {(u, v)}
13 end while
Algorithmus 2.10 zeigt den Algorithmus von Boruvka im Pseudocode. In den Abbildungen 2.11 bis 2.14 ist die Ausführung des Algorithmus auf einem Beispielgraphen gezeigt.
Wir haben oben bereits argumentiert, daß der Algorithmus korrekt ist: jede hinzugefügte
Kante ist eine sichere Kante. Wie kann man den Algorithmus von Boruvka effizient implementieren?
25
26
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
1
1
4
2
3
3
1
2
4
2
5
2
4
7
5
6
5
3
2
8
L:
9
2
1
2
3
4
5
7
6
8
9
0
(a) Am Anfang ist jeder Knoten in seiner eigenen Zusammenhangskomponente. Die lineare Liste L, in der der
Algorithmus die Komponenten verwaltet, ist rechts neben dem Graphen zu sehen. Das schwarze Quadrat in der
Liste ist nicht tatsächlich in der Liste enthalten. Es dient hier nur zum Kennzeichnen des Endes einer Phase, die
wir in der später folgenden Implementierung mit Hilfe von Leftist-Heaps zur Analyse benötigen.
1
1
4
2
3
3
1
2
4
2
2
4
7
5
5
6
5
3
2
8
L:
9
2
2
3
4
5
7
6
8
9
0
(b) Die erste Komponente wird der Liste entnommen und die billigste herausführende Kante (gestrichelt
gezeichnet) bestimmt.
1
1-2
1-2
4
3
1
2
3
4
2
2
4
7
3
5
5
6
5
8
2
2
9
L:
3
4
5
6
7
8
9
0
1-2
(c) Die eben bestimmte Kante wird zum Baum hinzugefügt. Dabei wird die betroffene Komponente 2 aus L
entfernt und die Komponenten 1 und 2 zu einer Komponente 1-2 verschmolzen, die hinten an die Liste
angefügt wird.
Abbildung 2.11: Berechnung eines minimalen aufspannenden Baumes mit dem Algorithmus von Boruvka. Die dick gezeichneten Kanten sind jeweils in der aktuellen Lösung, die
zum Schluß einen minimalen aufspannenden Baum bildet, enthalten.
2.4 Minimale aufspannende Bäume: Der Algorithmus von Boruvka (in Variation)
Für eine Komponente Vi verwalten wir die Kanten Ei , die mindestens einen Endpunkt
in Vi besitzen, in einem Binomial-Heap. Dann läßt sich das Minimum (u, v) von E i in
O(log m) = O(log n) Zeit3 bestimmen. Sei u ∈ Vi . Falls v ∈ Vi , so extrahieren wir erneut
das Minimum aus (dem Heap für) Ei . Beim Vereinigen von Vi und Vj in Schritt 11 des
Algorithmus vereinigen wir (die Heaps für) Ei und Ej . Hier benötigen wir die Tatsache,
daß sich Binomial-Heaps effizient vereinigen lassen. Algorithmus 2.11 zeigt die Details der
Implementierung.
Algorithmus 2.11 Implementierung des Boruvka-Algorithmus mit Binomial-Heaps.
B INOM -MST-B ORUVKA(G, c)
Input:
Ein zusammenhängender ungerichteter Graph G = (V, E) mit n := |V | und
m := |E| in Adjazenzlistendarstellung, eine Kantenbewertungsfunktion
c : E → R.
Output: Ein minimaler aufspannender Baum T von G.
1 T ←∅
{ Kanten im zu erstellenden MST. }
2 L←∅
{ Eine doppelt verkettete Liste. }
3 for all vi ∈ V do
4
M AKE -S ET(vi )
{ Vi ← {vi } }
5
Ei ← { (vi , v) ∈ E }
6
Qi ← B UILD -H EAP(Ei )
{ Ordne die von vi ausgehenden Kanten in einem Min-Heap. }
7
Füge Qi hinten an L an.
8 end for
9 while L hat mehr als ein Element do
10
Qi ← erstes Element in L
{ Qi werde dabei aus L entfernt. }
11
found ← false
12
while found = false do
13
(u, v) ← E XTRACT-M IN(Q)
14
Vi ← F IND -S ET(u)
{ Finde die Menge Vi mit u ∈ Vi . }
15
Vj ← F IND -S ET(v)
{ analog für v. }
16
if i 6= j then
17
T ← T ∪ {(u, v)}
18
U NION (Vi , Vj )
{ Ersetze Vi und Vj durch Vi ∪ Vj . }
19
Entferne die zu Vi und Vj gehörenden Heaps Qi und Qj aus L.



 Einer der beiden Heaps o.B.d.A. Qi ist bereits aus L entfernt. Er ist 





 derjenige Heap, den wir zu Anfang der Iteration der while-Schleife 




 als erstes Element von L entfernt haben. Der zweite Heap Qj ist 
in L in konstanter Zeit auffindbar, wenn jeder Menge Vk einen Zei





 ger auf den zugehörigen Heap Qk in L speichert. Das Entfernen 






 eines Elements aus einer doppelt verketteten Liste ist in konstanter 
Zeit durchführbar.
20
Q ← M ELD(Qi , Qj )
21
Füge Q hinten an L an.
22
end if
23
end while
24 end while
In der Implementierung 2.11 benutzen wir die Operationen F IND -S ET und U NION einer Datenstruktur für disjunkte Mengen, die wir in Kapitel 5 noch kennenlernen werden.
M AKE -S ET(vi ) erstellt eine Menge, deren einziges Element vi ist, U NION(Vi , Vj ) ersetzt
die Mengen Vi und Vj durch ihre Vereinigung, und F IND -S ET(v) liefert die Menge in der
3 Wir können annehmen, daß keine parallelen Kanten vorhanden sind. Dann gilt m ∈ O(n 2 ) und log m =
O(log n2 ) = O(2 log n) = O(log n).
27
28
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
1
1-2
4
1-2
3
3
2
2
4
7
5
5
2
4
1
6
2
5
8
3
L:
9
2
4
5
7
6
8
9
0
1-2
(a) Als nächstes wird die Komponente 3 aus der Liste entfernt. Durch die billigste ausgehende Kante
wird sie mit Komponente 6 verschmolzen.
1
1-2
4
1-2
3
3-6
2
2
4
7
5
5
2
4
1
3-6
5
2
8
3
L:
9
2
7
5
8
9
0
3-6
1-2
(b) Im nächsten Schritt wird die Komponente 4 mit Komponente 5 verschmolzen.
1
1-2
1-2
3
4
3-6
2
2
4-5
4-5
2
4
7
1
5
3-6
5
3
8
2
2
9
L:
8
9
0
1-2
3-6
4-5
(c) Jetzt wird Komponente 7 mit der bereits in dieser Phase erstellten Komponente 4-5
verschmolzen.
Abbildung 2.12: Fortsetzung: Berechnung eines minimalen aufspannenden Baumes mit
dem Algorithmus von Boruvka. Die dick gezeichneten Kanten sind jeweils in der aktuellen
Lösung, die zum Schluß einen minimalen aufspannenden Baum bildet, enthalten.
2.4 Minimale aufspannende Bäume: Der Algorithmus von Boruvka (in Variation)
1
1-2
1-2
3
4
3-6
2
2
4-5-7
4-5-7
2
4
4-5-7
1
5
3-6
5
8
3
2
9
2
L:
0
9
3-6
1-2
4-5-7
1
(a) Verschmelzen der Komponenten 8 und 9 beendet die Phase 0.
1
1-2-4-5-7
1-2-4-5-7
3
4
3-6
2
2
1-2-4-5-7
1-2-4-5-7
2
4
1-2-4-5-7
3
1
5
3-6
5
2
8-9
8-9
L:
3-6
8-9
1
1-2-4-5-7
2
(b) Als erstes werden in Phase 1 die Komponenten 1-2 und 4-5-7 verschmolzen.
Abbildung 2.13: Fortsetzung: Berechnung eines minimalen aufspannenden Baumes mit
dem Algorithmus von Boruvka. Die dick gezeichneten Kanten sind jeweils in der aktuellen
Lösung, die zum Schluß einen minimalen aufspannenden Baum bildet, enthalten.
29
30
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
1
1-2-4-5-7
1-2-4-5-7
3
4
2
2
1-2-4-5-7
1-2-4-5-7
2
4
1-2-4-5-7
1
5
5
3
3-6-8-9
3-6-8-9
2
3-6-8-9
3-6-8-9
L:
1
1-2-4-5-7
3-6-8-9
2
2
(a) Mit dem Verschmelzen von 3-6 und 8-9 endet dann Phase 1.
1
3
4
2
2
4
2
1
5
5
3
2
2
(b) Die beiden verbliebenen Komponenten werden in der letzen Phase, Phase 2, verschmolzen. Das Endergebnis ist hier der Platzersparnis ohne 1-2-3-4-5-6-7-8-9 in den
Knoten gezeichnet.
Abbildung 2.14: Fortsetzung: Berechnung eines minimalen aufspannenden Baumes mit
dem Algorithmus von Boruvka. Die dick gezeichneten Kanten sind jeweils in der aktuellen
Lösung, die zum Schluß einen minimalen aufspannenden Baum bildet, enthalten.
2.4 Minimale aufspannende Bäume: Der Algorithmus von Boruvka (in Variation)
Partition, welche v enthält. Details für die Datenstruktur zur Verwaltung der Partition von V
finden sich in Kapitel 5.
Wir analysieren die Laufzeit der Implementierung von Algorithmus 2.11. Dabei vernachlässigen wir zunächst einmal den Zeitaufwand für die Verwaltung der Mengen V i in der
Partition. Der Zeitaufwand für P
das Erstellen des initialen Heaps für Ei ist O(|Ei |). Somit
benötigt die Initialisierung O( vi ∈V |Ei |) = O(2m) = O(m) Zeit, da jede Kante in
genau zwei initialen Mengen Ei auftaucht.
Es verbleibt, die Zeit für alle Iterationen der while-Schleife von Schritt 9 bis 24 abzuschätzen. Zunächst bemerken wir, daß die Schleife genau n − 1 mal durchlaufen wird: wir
starten mit n Komponenten, und in jedem Durchlauf verringert sich durch Vereinigen von
zwei Komponenten ihre Anzahl um eins. Man sieht leicht (vgl. auch die Kommentare im
Programmcode), daß alle Operationen eines Durchlaufs bis auf die Heap-Operationen und
die F IND -S ET/U NION-Operationen (die wir erst einmal ausgeklammert haben) in konstanter Zeit durchführbar sind.
Jeder Durchlauf der while-Schleife benötigt eine M ELD-Operation für unsere Heaps. Die
M ELD-Operation ist in binären und d-nären Heaps nur recht ineffizient implementierbar:
wir müssen die Elemente in ein neues Array umkopieren und die Elemente eines Heaps in
den anderen Heap einfügen. Dies benötigt Ω(n) Zeit, so daß wir auch gleich einen neuen
Heap aufbauen können. Die Binomial-Heaps unterstützen M ELD hingegen effizient: ein
M ELD zweier Heaps mit m1 und m2 Elementen benötigt O(log m1 + log m2 ) Zeit (vgl.
auch Tabelle 2.2). Daher ist der Zeitaufwand für alle n − 1 M ELDs bei Benutzung von
Binomial-Heaps O(n log m) = O(n log n).
Jede Kante (u, v) wird maximal zweimal als Minimum in Schritt 13 extrahiert: maximal einmal für jeden der beiden Endknoten. Somit finden insgesamt ≤ 2m E XTRACTM IN-Operationen auf Heaps der Größe ≤ m statt. Da Binomial-Heaps E XTRACT-M IN
in logarithmischer Zeit unterstützen (siehe Tabelle 2.2), ist der Zeitaufwand für alle
O(m) E XTRACT-M INs O(m log m).
Wir haben somit gezeigt, daß sich der Algorithmus von Boruvka mit Hilfe von BinomialHeaps in O((n+m) log n) implementieren läßt, wenn man den Zeitaufwand für die M AKE S ET/F IND -S ET/U NION-Operationen vernachlässigt. Im Verlauf von Algorithmus 2.11 finden n M AKE -S ET, ≤ 2m F IND -S ET und n − 1 U NION-Operationen statt. In Abschnitt 5.2 werden wir eine einfache Datenstruktur kennenlernen, die es ermöglicht, diese
O(n+m) Operationen in O(m+n log n) Zeit auszuführen. Damit erhöht sich die Laufzeit
von Algorithmus 2.11 auch durch die Berücksichtigung der Operationen für die disjunkten
Mengen nicht.
Beobachtung 2.11 Mit Hilfe eines von Binomial-Heaps und der Datenstruktur für disjunkte Mengen aus Kapitel 5 benötigt der Algorithmus von Boruvka zur Bestimmung eines
MST O(m log n) Zeit auf einem Graphen mit n Ecken und m Kanten.
Bemerkung 2.12
1. Der Algorithmus von Boruvka läßt sich auch ohne Benutzung von
Binomial-Heaps so implementieren, daß er in O(m log n) Zeit läuft. Die hier vorgestellte Implementierung ist aber etwas einfacher und illustriert den Nutzen einer
effizient durchführbaren M ELD-Operation in Prioritätsschlangen.
2. Mit ein paar Tricks läßt sich sowohl die hier vorgestellte Implementierung als auch
die alternative Implementierung ohne Binomial-Heaps noch so erweitern, daß die
Laufzeit sogar nur O(m log log n) beträgt. Wir werden später MST-Algorithmen
kennenlernen, deren Laufzeit sogar noch besser ist.
31
32
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
M ELD
B UILD
binärer Heap
O(1)
O(log n)
O(1)
O(log n)
O(log n)
Θ(n)
O(n)
d-närer Heap
O(1)
O(logd n)
O(1)
O(d · logd n)
O(logd n)
Θ(n)
O(n)
Binomial-Heap
O(1)
O(log n)
O(1)
O(log n)
O(log n)
O(log n)
O(n)
Tabelle 2.2: Zeitkomplexität der Prioritätsschlangen-Operationen bei Implementierung
durch einen binären Heap, durch einen d-nären Heap und durch einen Binomial-Heap der
Größe n.
2.5 Binomial-Heaps
Wir stellen nun die Binomial-Heaps vor und beschreiben, wie man die einzelnen
Prioritätsschlangen-Operationen in diesen Heaps implementiert. Unsere Implementierung
unterscheidet sich von der in [3, Kapitel 19] in folgenden Punkten:
1. Wir halten zusätzlich einen Zeiger min[H] auf das Minimum des Heaps. Das hat zur
Folge, daß M INIMUM in O(1) Zeit statt in O(log n) Zeit durchführbar ist.
2. Das Vereinigen von zwei Binomial-Heaps ist etwas effizienter gelöst. Das hat zur
Folge, daß das Aufbauen eines Binomial-Heaps aus n Elementen durch sukzessives
Einfügen in O(n) Zeit anstelle von O(n log n) Zeit implementiert werden kann.
Im Folgenden nennen wir einen (nicht notwendigerweise binären) Baum T Heap-geordnet,
wenn für jeden Knoten v ∈ T und seinen Vater parent(v) ∈ T gilt:
key[parent(v)] ≤ key[v].
2.5.1 Binomialbäume und Binomial-Heaps
Der Baustein für Binomial-Heaps sind die sogenannten Binomialbäume, die rekursiv wie
folgt definiert sind:
Definition 2.13 (Geordneter Binomialbaum)
Ein geordneter Binomialbaum Bk der Ordnung k ∈ N ist wie folgt definiert:
1. Der Binomialbaum B0 besteht aus einem einzelnen Knoten.
2. Ein Binomialbaum Bk der Ordnung k ≥ 1 besteht aus zwei Binomialbäumen der
Ordnung k−1, wobei die Wurzel des einen Baumes ein (beliebiger) Sohn des anderen
Baumes ist.
Abbildung 2.15 veranschaulicht die Binomialbäume. Durch Induktion nach k lassen sich
leicht die folgenden Eigenschaften beweisen.
Lemma 2.14 Für einen geordneten Binomialbaum Bk gilt:
(i) Bk enthält genau 2k Knoten.
(ii) Die Höhe von Bk ist k .
2.5 Binomial-Heaps
33
Bk−1
(a)
B0
Bk−1
(b) Bk
(c)
B0
(e) B2
(d)
B1
(f) B3
···
Bk−1
Bk−2
Bk−3
B2
(g) Bk
Abbildung 2.15: Rekursive Definition von Binomialbäumen.
B1
B0
34
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
(iii) Es gibt genau
k
i
Knoten mit Höhe i für i = 0, 1, . . . , k .
(iv) Der Wurzelknoten hat Grad k . Jeder andere Knoten hat strikt kleineren Grad. Die
k Söhne der Wurzel, geordnet von links nach rechts, sind Wurzelknoten von Binomialbäumen B0 , . . . , Bk−1 (d.h. zu jedem 0 ≤ j ≤ k − 1 existiert genau ein Sohn,
der Wurzelknoten eines Binomialbaums der Ordnung j ist).
Beweis: Der Induktionsanfang k = 0 ist trivial. Wir nehmen an, daß die Aussagen für k −1
bereits bewiesen sind.
(i) Bk besteht aus zwei Bk−1 , die nach Induktionsvoraussetzung jeweils 2k−1 Knoten
enthalten. Also hat Bk genau 2 · 2k−1 = 2k Knoten.
(ii) Die Höhe von Bk ist nach Konstruktion genau um eins größer als die von Bk−1 , also
(k − 1) + 1 = k.
Knoten des rechten Bk−1 und die k−1
(iii) Die Knoten in Bk der Höhe i sind die k−1
i−1
i
Knoten des linken Bk−1 . Also ist die Anzahl der Knoten in Bk mit Höhe i:
k−1
k−1
k
+
=
.
i
i−1
i
(iv) Der einzige Knoten in Bk , der größeren Grad besitzt als in Bk−1 ist die Wurzel.
Diese besitzt einen zusätzlichen Sohn, hat also Grad (k − 1) + 1 = k. Nach Induktionsvoraussetzung sind die Söhne von Bk−1 die Wurzeln von Binomialbäumen
Bk−2 , . . . , B0 in dieser Reihenfolge. Wenn nun aus zwei Bk−1 ein Bk entsteht, so
werden die Söhne des rechten Bk−1 , die Wurzeln von Bk−2 , . . . , B0 sind, zu Söhnen der Wurzel von Bk . Die Söhne der Wurzel von Bk sind also Wurzeln von Bk−1
(der linke der beiden Bk−1 , aus denen Bk konstruiert wird) und die Wurzeln von
Bk−2 , . . . , B0 .
2
Die Binomialbäume tragen ihren Namen von Eigenschaft (iii)
in Lemma 2.14: es besteht
ein Zusammenhang zwischen den Binomialkoeffizienten ki und den Knoten der Höhe i
in Bk .
Ist Bk nun ein Binomialbaum, der n Knoten enthält, so gilt wegen Lemma 2.14 (i) dann
2k = n, also k = log2 n. Mit Eigenschaft (iv) sehen wir dann, daß der maximale Grad in
diesem Binomialbaum genau log2 n beträgt:
Korollar 2.15 Der maximale Grad in einem Binomialbaum mit n Knoten ist log 2 n.
2
Mit Hilfe der geordneten Binomialbäume können wir nun den Binomial-Heap definieren:
Definition 2.16 (Binomial-Heap)
Ein Binomial-Heap H besteht aus einer Kollektion von Binomialbäumen, die alle Heapgeordnet sind. Zusätzlich exitiert zu jeder Zahl k ∈ N höchstens einen Binomialbaum in H ,
dessen Wurzel Grad k besitzt.
Lemma 2.17 Sei H ein Binomial-Heap mit n Elementen, n ∈ N+ . Dann gilt:
P
(i) Sei n = ki=0 bi 2i die Binärdarstellung von n mit k = blog2 nc. H besitzt genau
dann einen Binomialbaum der Ordnung i in der Wurzelliste, wenn b i = 1 gilt.
(ii) Der größte Binomialbaum in der Wurzelliste von H ist ein B k mit k = blog2 nc.
2.5 Binomial-Heaps
35
(iii) Die Wurzelliste von H besitzt v(n) Binomialbäume, wobei v(n) die Anzahl der
Einsen in der Binärdarstellung von n bezeichnet.
(iv) Die Anzahl der Kanten in allen Binomialbäumen von H beträgt n − v(n).
Beweis: Seien Bt0 , . . . , Bts die Binomialbäume in der Wurzelliste von H mit t0 ≤ · · · ≤
ts . Nach Voraussetzung gilt ti 6= tj für i 6= j. Da Bti genau 2ti Knoten besitzt, folgt bereits
2ts ≤ n, also ts ≤ k.
Pk
Mit di := 1, falls i ∈ {t0 , . . . , ts }, und di := 0 sonst, haben wir also n = i=1 di 2i =
Pk
i
4
i=0 bi 2 . Da die Zahldarstellung im Binärsystem eindeutig ist , folgt di = bi und somit (i).
Die Behauptungen (ii) und (iii) folgen sofort aus (i). Zum Beweis von (iv) bemerken wir,
daß ein Binomialbaum Bi genau 2i − 1 Kanten besitzt. Die Anzahl der Kanten in den
Bäumen von H ist somit
X
X
2i − v(n) = n − v(n).
(2i − 1) =
i:bi 6=0
i:bi 6=0
Dies beendet den Beweis.
2
2.5.2 Implementierung von Binomial-Heaps
Jeder Knoten in einem Binomial-Heap enthält die folgenden Informationen:
• p[x] ist ein Zeiger auf den Vaterknoten von x (NULL, falls x der Wurzelknoten ist)
• child[x] ist ein Zeiger auf den linkesten Sohn von x. Die Söhne von x sind in einer
einfach verketteten Liste organisiert.
• Für einen Sohn y ist right[y] ein Zeiger auf den Nachfolger in der verketteten Liste
der Kinder.
• In degree[x] ist der Grad von x, d.h. die Anzahl seiner Kinder, gespeichert.
Weiterhin bezeichnen wir wieder mit key[x] den Schlüsselwert des Knotens x. Ein
Binomial-Heap H ist über den Zeiger head[H] zugreifbar, der auf das erste Element in
der Wurzelliste zeigt. Die Wurzelliste ist dabei nach aufsteigendem Grad der Wurzeln sortiert. Falls H leer ist, so ist head[H] = NULL. Aus Effizienzgründen halten wir uns noch
zusätzlich einen Zeiger min[H], der auf das minimiale Element im Heap, also eine Wurzel
in der Wurzelliste, zeigt.
Abbildung 2.16 veranschaulicht die Organisation eines Binomial-Heaps im Computer.
2.5.3 Implementierung der einfachsten Heap-Operationen
Zunächst betrachten wir das Erstellen eines neuen leeren Binomial-Heaps (siehe Algorithmus 2.12). Dazu müssen wir einfach head = NULL setzen. Damit ist die Laufzeit für
B INOM -M AKE in Θ(1).
Das Finden des minimalen Elements B INOM -M INIMUM ist ebenfalls trivial (siehe Algorithmus 2.13), da wir uns einen Zeiger auf das minimale Element in der Wurzelliste gemerkt
hatten.
k
i
k
für unseren Fall: Wir haben
i=0 (di − bi )2 = 0. Da di − bi ∈ {−1, 1} und 2 >
− 1, folgt dk − bk = 0. Fortsetzung liefert nun di = bi für i = k, k − 1, . . . , 0.
4 Schnellbeweis
k−1
i=0
2i
=
2k
36
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
min[H]
head[H] 10
5
7
13
15
11
9
18
10
12
20
(a) Ein Binomial-Heap mit 11 Knoten. Die
Binärdarstellung von 11 ist 1011.
min[H]
10
0
5
1
7
3
head[H]
13
0
15
1
11
2
9
1
18
0
10
0
20
0
(b) Zeiger-Repräsentation im Computer.
p[x]
key[x]
degree[x]
child[x]
right[x]
(c) Bedeutung der
einzelnen Einträge in
den Knoten.
Abbildung 2.16: Organisation eines Binomial-Heaps.
12
0
2.5 Binomial-Heaps
Algorithmus 2.12 Erstellen eines leeren Binomial-Heaps.
B INOM -M AKE()
1 head[H] ← NULL
2 min[H] ← NULL
Algorithmus 2.13 Finden des Minimums in einem Binomial-Heap.
B INOM -M INIMUM(H)
1 if head[H] = NULL then
2
return „Fehler: Der Heap ist leer!“
3 end if
4 return key[min[H]]
Die Operation B INOM -D ECREASE -K EY ist ähnlich wie bei den binären Heaps implementiert: wir vertauschen den Knoten bei Bedarf rekursiv mit seinem Vaterknoten, bis daß die
Heap-Ordnung wieder hergestellt ist, siehe Algorithmus 2.14. Ein Beispiel ist in Abbildung 2.17 zu sehen.
Algorithmus 2.14 Verringern eines Schlüsselwerts in einem Binomial-Heap.
B INOM -D ECREASE -K EY(H, x, k)
( Das Verfahren entspricht im wesentlichen dem »Bubble-Up« im )
Binär-Heap plus einer eventuellen Aktualisierung des Minimum1
Zeigers.
2 key[x] ← k
3 y←x
4 z ← p[x]
5 while z 6= NULL und key[y] < key[z] do
6
Vertausche key[y] und key[z].
7
y←z
8
z ← p[z]
9 end while
10 if z = NULL then
{ Das Element wurde bis in die Wurzel eines Binomialbaums hochgeschoben. }
11
if key[y] < key[min[H]] then
12
min[H] ← y
13
end if
14 end if
Der Zeitaufwand läßt sich erneut recht einfach abschätzen: wenn sich der Knoten x, dessen
Schlüsselwert erniedrigt wird, auf Höhe h in einem Binomialbaum im Heap befindet, so
benötigen wir O(h) Operationen. Nach Lemma 2.14 (ii) hat jeder Binomialbaum in einem
Binomial-Heap mit n Knoten Höhe O(log n). Somit kann B INOM -D ECREASE -K EY in
O(log n) Zeit implementiert werden.
2.5.4 Rückführen von I NSERT und E XTRACT-M IN auf M ELD
Die Operationen B INOM -I NSERT und B INOM -E XTRACT-M IN lassen sich beide auf
B INOM -M ELD zurückführen. Wir zeigen dies zunächst für das Einfügen (siehe Algorithmus 2.15).
Wir erzeugen für das neue Element x einfach einen einelementigen Binomial-Heap H 0
in O(1) Zeit. Dann vereinigen wir H 0 mit dem bestehenden Heap H mittels B INOM -
37
38
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
min[H]
min[H]
head[H] 10
5
head[H] 10
7
13
15
11
9
18
10
12
5
7
13
2
20
9
18
10
12
20
(a) Der Ausgangsheap.
(b) Der Schlüsselwert wurde auf 2 verringert.
min[H]
head[H] 10
11
min[H]
5
7
13
11
2
9
18
10
head[H] 10
5
2
13
12
7
20
11
9
18
10
12
20
(c)
(d)
min[H]
head[H] 10
5
2
13
7
11
9
18
10
12
20
(e) Zum Schuß wird noch der Zeiger auf das
Minimum aktualisiert.
Abbildung 2.17: Ausführen der Operation D ECREASE -K EY(15, 2) in einem BinomialHeap.
2.5 Binomial-Heaps
39
Algorithmus 2.15 Einfügen eines Elements in einen Binomial-Heap.
B INOM -I NSERT(H, x)
1 H 0 ← B INOM -M AKE ()
2 p[x] ← NULL
3 child[x] ← NULL
4 right[x] ← NULL
5 degree[x] ← 0
6 head[H 0 ] ← x
7 min[H 0 ] ← x
8 H ← B INOM -M ELD (H, H 0 )
M ELD (siehe Abbildung 2.18). Da B INOM -M ELD in O(log n) Zeit läuft, wie wir in Abschnitt 2.5.5 zeigen werden, ist das Einfügen ebenfalls in O(log n) Zeit möglich.
x
B INOM -M ELD(H, H 0 )
Abbildung 2.18: In Binomial-Heaps können wir das Einfügen von Elementen in einen
Heap H auf das Verschmelzen zurückführen. Für das neue Element x wird ein einelementiger Binomial-Heap H 0 erzeugt, der dann mittels B INOM -M ELD mit H verschmolzen
wird.
Für B INOM -E XTRACT-M IN müssen wir etwas trickreicher arbeiten (siehe Algorithmus 2.16). Wir suchen die Wurzel x in der Wurzelliste mit minimalem Schlüsselwert (genauso wie in B INOM -M INIMUM). Dies ist in O(1) Zeit möglich, wie wir bereits gesehen
haben. Dann entfernen wir x mitsamt seines an ihm wurzelnden Binomialbaum T aus der
Wurzelliste. Dies benötigt ebenfalls nur O(log n) Zeit, da nur O(log n) Wurzeln in der
Wurzelliste sind.
Wir konstruieren nun einen neuen Binomial-Heap H 0 aus T (jedoch ohne x). Ist x Wurzel
eines Bk , so sind die Söhne von links nach rechts Wurzeln von Bk−1 , . . . , B0 . Im Prinzip
haben wir damit schon Bäume für einen Binomial-Heap, dessen Wurzelliste aus eben diesen Bäumen besteht. Allerdings haben wir bisher immer gefordert, daß die Wurzeln in der
Wurzelliste eines Binomial-Heaps nach aufsteigendem Grad geordnet sind. Die Söhne haben jedoch von links nach rechts absteigenden Grad. Daher kehren wir die Reihenfolge der
Sohnliste in O(log n) Zeit um und machen sie dann zur Wurzelliste eines neuen BinomialHeaps H 0 . Jetzt müssen wir nur noch H und H 0 mittels B INOM -M ELD vereinigen. Alle
Operationen sind in O(log n) Zeit durchführbar. Da auch B INOM -M ELD in O(log n) Zeit
läuft (siehe nächster Abschnitt), benötigt B INOM -E XTRACT-M IN nur O(log n) Zeit. Ein
Beispiel für B INOM -E XTRACT-M IN ist in Abbildung 2.19 zu sehen.
2.5.5 Vereinigen zweier Binomial-Heaps
Wir beschäftigen uns nun mit dem Vereinigen zweier Binomial-Heaps. Wir haben gesehen,
daß diese Operation als Basis für B INOM -I NSERT und B INOM -E XTRACT-M IN genutzt
werden kann.
40
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
Algorithmus 2.16 Löschen des Minimums in einen Binomial-Heap.
B INOM - EXTRACT- MIN(H)
1 x ← min[H]
2 Entferne x aus der Wurzelliste.
{ Dabei wird der gesamte Binomialbaum, der an x hängt, aus dem Heap entfernt. }
3 Aktualisiere min[H] auf das neue Minimum in der aktuellen Wurzelliste.
{ Dies erfolgt durch einmaliges Durchlaufen der Wurzelliste. }
4 H 0 ← B INOM -M AKE ()
5 L ← Liste der Söhne von x in umgekehrter Reihenfolge.
( Die Grade der Söhne von x sind absteigend: Wenn x Wur- )
zel eines Bk ist, dann sind die Söhne von links nach rechts
Wurzeln von Bk−1 , . . . , B0 , siehe Lemma 2.14 (iv).
6 head[H 0 ] ← L
7 H ← B INOM -M ELD (H, H 0 )
8 return x
min[H]
min[H]
head[H] 10
5
2
13
15
11
9
18
10
head[H] 10
12
5
2
13
15
20
11
9
18
10
12
20
(a) Der Ausgangsheap.
(b) Das Minimum wurde samt seines daran wurzelnden Baumes entfernt. Der Zeiger
auf das (temporäre) Minimum wurde aktualisiert.
min[H]
min[H 0 ]
min[H]
head[H] 10
5
13
head[H 0 ] 12
head[H] 10
9
10
11
15
18
20
(c) Es wird eine Liste der Söhne des alten Minimums in
umgekehrter Reihenfolge erstellt und damit ein neuer Binomialheap H 0 generiert.
5
12
15
11
9
18
10
13
20
(d) B INOM -M ELD(H, H 0 )
dann das obige Endresultat.
Abbildung 2.19: Extrahieren des Minimums aus einem Binomial-Heap.
ergibt
2.5 Binomial-Heaps
Im Prinzip funktioniert das Vereinigen zweier Binomial-Heaps H1 und H2 wie das Addieren von zwei Binärzahlen mittels der »Schulmethode«. Zur Erinnerung: die Größen der
Binomialbäume in den Wurzellisten von Hi sind Zweierpotenzen, und wir können zwei Binomialbäume gleicher Größe zu einem neuen Binomialbaum doppelter Größe verschmelzen, indem wir den einen Baum an die Wurzel des zweiten anhängen (genau wie in der
rekursiven Definition der Bk ).
Wir betrachten die Binomialbäume von H1 und H2 der Reihe nach in aufsteigender Größe.
Wir nehmen an, daß Hi genau ni Knoten enthält. Wie bei der Addition von Binärzahlen
betrachten wir in jedem Schritt zwei Binomialbäume gleicher Größe und eventuell einen als
»Übertrag« erhaltenen Binomialbaum. Anfangs hat man keinen Übertrag. Im iten Schritt
hat man alsP
Operanden einen Binomialbaum Bi von H1 , wenn in der Binärdarstellung
i
von n1 =
i bi 2 das Bit bi = 1 ist. Analog haben wir einen Binomialbaum aus der
Wurzelliste von H2 , falls das entsprechende Bit in der Binärdarstellung von n2 gesetzt ist.
Als dritten Operanden haben wir einen möglichen Übertrag.
Tritt kein Operand auf, so ist im Resultat ebenfalls kein Baum Bi vorhanden. Tritt ein Operand Bi auf, so enthält das Resultat genau diesen Baum Bi . Bei zwei Operanden werden
diese zu einem Bi+1 zusammengefasst und als Übertrag in die nächste Stelle weitergereicht, der Eintrag an Stelle i bleibt leer. Im letzten Fall treten alle drei Operanden auf, so
wird einer zur iten Stelle des Ergebnisses und zwei werden zu einem B i+1 , der wiederum
in den Übertrag geht.
Algorithmus 2.18 zeigt das eben beschriebene Vereinigen B INOM -M ELD zweier BinomialHeaps im Pseudocode. Abbildungen 2.21 und 2.22 zeigen ein Beispiel. Unsere Implementierung von B INOM -M ELD benötigt das elementare Unterprogramm B INOM -L INK. Weiterhin setzen wir zur Vereinfachung der Notation voraus, daß degree[NULL] := +∞ und
key[NULL] := +∞ gesetzt sind.
B INOM -L INK (y, z) (siehe Algorithmus 2.17 und Abbildung 2.20 für eine Illustration)
macht y zum ersten Sohn von z. Dabei wird angenommen, daß bei Aufruf der Prozedur
y und z Wurzeln zweier verschiedener Bk sind. Als Resultat ist dann z Wurzel eines
Bk+1 . Wie man aus der Implementierung unmittelbar ablesen kann, läuft B INOM -L INK
in O(1) Zeit.
Algorithmus 2.17 Anhängen eines Binomialbaums mit Wurzel y an einen Binomialbaum
mit Wurzel z.
B INOM -L INK(H, y, z)
1 if y = min[H] then
2
Vertausche yund z.

Der obige Test ist höchstens dann erfolgreich, wenn y und z den 



gleichen Schlüsselwert besitzen. In diesem Fall wollen wir auf jeden

 Fall den Zeiger min[H] gültig halten, d.h. er soll weiterhin auf eine 

Wurzel zeigen.
3 end if
4 p[y] ← z
5 right[y] ← child[z]
6 child[z] ← y
7 degree[z] ← degree[z] + 1
Die Laufzeit von B INOM -M ELD läßt sich wie folgt abschätzen. Hat Hi genau ni Knoten,
so besitzt Hi nach Lemma 2.17 (i) nur O(log ni ) Binomialbäume in der Wurzelliste. Da
B INOM -L INK nur O(1) Zeit benötigt und wir pro Binärstelle in der Binomialdarstellung
von n1 und n2 neben konstant vielen B INOM -L INK auch sonst nur O(1) Zeit investieren,
läuft B INOM -M ELD in O(log n1 + log n2 ) = O(log n) Zeit.
41
42
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
Algorithmus 2.18 Vereinigen zweier Binomial-Heaps.
B INOM -M ELD(H1 , H2 )
1
{ Zur Erinnerung: key[NULL] = +∞ und degree[NULL] = +∞. }
2 Setze min[H] auf die Wurzel min[H1 ] oder min[H2 ] mit geringerem Schlüsselwert.
3 H ← B INOM -M AKE ()
4 x1 ← head[H1 ]
{ Zeiger auf den aktuellen Baum in der Liste von H1 . }
5 x2 ← head[H2 ]
{ analog für H2 . }
6 carrybit_tree ← NULL
{ Zeiger auf den Übertragsbaum. }
7 while x1 6= NULL oder x2 6= NULL oder carrybit_tree 6= NULL do
8
if Einer der beiden Zeiger xi ist gleich NULL und carrybit_tree = NULL. then
{ Test aus Effizienzgründen, siehe Text. }
9
Sei o.B.d.A. x1 = NULL. Hänge den Rest der Wurzelliste von H2 , auf die x2 zeigt,
an die Wurzelliste
von H an.
Das Anhängen der kompletten Restliste benötigt nur konstante
Zeit, da es durch das Umhängen eines Zeigers erfolgt.
10
return H
11
end if
12
Sei k der minimale Grad der Bäume, auf die x1 , x2 und carrybit_tree zeigen.
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
{ 1. Fall: Genau ein Baum mit Grad k ist vorhanden.
}
if genau einer der Bäume x1 , x2 , carrybit_tree hat Grad k then
Sei x der Baum.
{ Genauer: ein Zeiger auf den Baum. }
Füge x hinten an die Wurzelliste von H an.
if x = x1 oder x = x2 then
Entferne x = xi aus der entsprechenden Wurzelliste und setze xi ←
right[xi ]. Beim Entfernen ist xi das vorderste Element der Liste von Hi ,
so daß das Entfernen in konstanter Zeit möglich ist.
end if
carrybit_tree ← NULL
end if
{ 2. Fall: Genau zwei Bäume mit Grad k sind vorhanden.
}
if genau zwei Bäume haben Grad k then
Seien dies y und z.
if key[z] < key[y] then
carrybit_tree ← B INOM -L INK (H, y, z)
else
carrybit_tree ← B INOM -L INK (H, z, y)
end if
Entferne analog zu oben bei Bedarf die Bäume aus den Wurzellisten von H 1 und
H2 .
end if
{ 3. Fall: Drei Bäume mit Grad k sind vorhanden.
if alle drei Bäume haben Grad k then
Füge carrybit_tree hinten an die Wurzelliste von H an.
Entferne x1 und x2 aus den Wurzellisten von H1 und H2 .
if key[x2 ] < key[x1 ] then
carrybit_tree ← B INOM -L INK (H, x1 , x2 )
else
carrybit_tree ← B INOM -L INK (H, x2 , x1 )
end if
x1 ← right[x1 ]
x2 ← right[x2 ]
end if
end while
}
2.5 Binomial-Heaps
43
y
z
···
···
(a) Die beiden Bionomialbäume
z
y
···
···
(b) Das Resultat von B INOM -L INK(H, y, z)
Abbildung 2.20: Illustration von B INOM -L INK(H, y, z). Der Binomialbaum mit Wurzel y
wird an den Binomialbaum mit Wurzel z angehängt.
Besondere Erwähnung verdient der Test in Zeile 9: falls eine der beiden Wurzellisten leer
wird und kein Übertrag mehr besteht, so hängen wir den Rest der zweiten Wurzelliste in
einem Rutsch an die Ergebnis-Wurzelliste an. Dies hat Effizienzvorteile, wie wir in Abschnitt 2.5.6 noch genauer sehen werden.
2.5.6 Konstruieren eines Binomial-Heaps
Ein Binomial-Heap zu einer vorgegebenen Menge von n Elementen wird durch iteratives
Einfügen der Elemente mittels B INOM -I NSERT erzeugt. Aus unseren bisherigen Überlegungen erhalten wir sofort die Zeitkomplexität O(n log n) für diese Operation, die wir im
folgenden mit B INOM -B UILD bezeichnen.
Eine etwas genauere Analyse liefert uns jedoch eine bessere Zeitkomplexität. Dazu erinnern wir uns an Lemma 2.17 (i): Ist H ein Binomal-Heap mit k Elementen und k =
Pblog2 kc i
bi 2 die Binärdarstellung von k, so besitzt H genau dann einen Binomialbaum
i=0
der Ordnung i in der Wurzelliste, wenn bi = 1 gilt. Was bedeutet das für das Einfügen von
Elementen?
Betrachten wir das Einfügen des kten Elements x. Das Einfügen hatten wir mittels der
B INOM -M ELD Prozedur implementiert, indem wir zunächst einen einelementigen Binomialheap H 0 für x erzeugen und dann H 0 mit dem aktuell bestehenden Heap H für die
ersten k − 1 Elemente vereinigen. Beim Vereinigen werden die Binomialbäume in den
Wurzellisten von H und H 0 analog zur Addition von Binärzahlen »addiert«. Ist nun k − 1
gerade, so hat H keinen Binomialbaum der Ordnung 0. Da die Wurzelliste von H 0 sowieso
nur aus dem einen Baum der Ordnung 0 besteht, benötigt das Einfügen für gerades k − 1,
also ungerades k, nur O(1) Zeit: das »Addieren« der Wurzellisten bricht nach dem ersten
Schritt ab.
Die obige Argumentation zeigt, daß für ungerades k, also etwa n/2 Elemente, nur
44
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
min[H1 ]
min[H2 ]
head[H1 ] 10
5
min[H]
head[H2 ] 12
13
9
11
10
15
head[H] NULL
18
20
(a) Die zwei Ausgangsheaps. Die schwarzen Knoten kennzeichnen die Zeiger x 1
und x2 . Der Zeiger für das Minimum des Resultatheaps H wurde bereits gesetzt. Der
Überlaufzeiger carrybit_tree ist gleich NULL, zwei Bäume haben Grad k = 0.
min[H]
head[H1 ]
5
head[H2 ]
13
9
10
11
15
head[H]
18
20
carrybit_tree 10
12
(b) Die zwei Bäume von Grad 0 wurden zusammengefügt. Nun haben drei
Bäume den Grad k = 1.
min[H]
head[H1 ]
NULL
head[H2 ]
15
11
head[H] 10
18
12
20
5
carrybit_tree
9
13
10
(c) Der carrybit_tree wurde in die Wurzelliste von H eingefügt, die zwei
anderen Bäume von Grad 1 wurden zusammengefügt. Wir haben zwei Bäume
mit Grad k = 2.
Abbildung 2.21: Beispiel für B INOM -M ELD(H1 , H2 ).
2.5 Binomial-Heaps
45
min[H]
head[H1 ]
NULL
head[H2 ]
head[H] 10
NULL
12
5
carrybit_tree
15
11
9
18
10
13
20
(a) Wir haben einen Baum vom Grad k = 3. Nach dem nächsten Schritt terminiert das Verfahren.
min[H]
head[H1 ]
NULL
head[H2 ]
NULL
head[H] 10
5
12
15
11
9
18
10
13
20
carrybit_tree
NULL
(b) Im letzten Schritt wurde der Übertragsbaum an die Wurzelliste von H angehängt. Dies ergibt das
obige Endresultat.
Abbildung 2.22: Fortsetzung des Beispiels für B INOM -M ELD(H1 , H2 ).
46
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
O(1) Zeit benötigt wird. Somit ist ein geschätzter Aufwand von O(log n) für jede der
n iterativen Einfügeoperationen viel zu grob. Wir leiten jetzt eine schärfere Schranke in
etwas allgemeinerer Form her. Sei H ein Binomial-Heap mit m Elementen, in den iterativ
n Elemente eingefügt werden (B INOM -B UILD ist der Spezialfall mit m = 0). Der Gesamtaufwand ist offenbar linear in der Anzahl der gesamten Additionen von Binomialbäumen
während des Einfügens.
Man beachte, daß jede Addition von zwei Binomialbäumen eine neue Kante erzeugt. Also
finden beim Einfügen insgesamt
(m + n − v(n + m))
{z
}
|
Kanten nachher, siehe Lemma 2.17 (iv)
− (m − v(m)) = n + v(m) − v(n + m) ∈ O(n + log m)
|
{z
}
Kanten vorher
Additionen statt. Folglich ist der Gesamtaufwand ebenfalls O(n + log m). Für B UILD H EAP erhalten wir daher einen linearen Zeitaufwand von O(n).
2.6 Leftist-Heaps
In diesem Abschnitt stellen wir eine weitere Datenstruktur zur Verfügung, die im wesentlichen die gleichen Komplexitäten für die Prioritätsschlangenoperationen garantiert wie die
Binomial-Heaps. Die sogenannten linkslastigen Heaps (engl. Leftist-Heaps) ermöglichen
es uns aber durch Einführen von sogenanntem verzögertem Meld (»lazy meld«), den Algorithmus von Boruvka noch schneller zu implementieren.
Bemerkung 2.18 In diesem Abschnitt wird nicht beschrieben, wie man D ECREASE -K EY
in Leftist-Heaps implementiert. In der Tat sind Leftist-Heaps nicht sehr gut geeignet, um
D ECREASE -K EY-Operationen auszuführen (vgl. hierzu auch Abbildung 2.24).
Sei T ein binärer Baum, d.h. ein Baum, in dem jeder Knoten maximal zwei Söhne besitzt. Unser binärer Baum sei mit Hilfe der Zeiger left und right organisiert. Dabei sind
left[v] und right[v] Zeiger auf den linken bzw. rechten Sohn von v im Baum. Ein Zeiger
ist NULL, falls kein entsprechender Sohn existiert.
Wir definieren für einen Knoten v ∈ T den Pfad-Rang rank[v] als eins plus die Länge
des kürzesten Weges von v zu einem Knoten mit höchstens einem Sohn in seinem Teilbaum. Formal setzen wir für einen Knoten v mit maximal einem Sohn rank[v] := 1 und
für alle anderen Knoten w dann rank[w] = 1 + min{rank[left[v]], rank[right[v]]}.
Um zahlreiche Fallunterscheidungen zu vermeiden, definieren wir rank[NULL] := 0 und
key[NULL] := +∞. Abbildung 2.23 zeigt einen binären Baum und die Ränge der Knoten.
2
3
1
1
4
6
1
1
14
1
2
10
12
1
1
16
11
20
Abbildung 2.23: Ein heap-geordneter Baum. Die Zahlen außerhalb der Knoten bezeichnen
die Pfad-Ränge der Knoten.
Wir nennen einen heap-geordneten Baum einen linksgerichteten Heap oder einen LeftistHeap, falls für jeden Knoten v gilt: rank[left[v]] ≥ rank[right[v]]. Intuitiv ist ein
2.6 Leftist-Heaps
Leftist-Heap »linkslastig», da in jedem Teilbaum das meiste Gewicht links liegt. Der Baum
aus Abbildung 2.23 ist ein Leftist-Heap.
In einem Leftist-Heap führt das iterative Verfolgen der rechten Söhne zu einem kürzesten
Weg zu einen Nachfolger mit höchstens einem Sohn. Wir nennen den kürzesten Weg von
der Wurzel zu einem Nachfolger mit höchstens einem Sohn daher auch den rechten Weg
des Leftist-Heaps. Man zeigt leicht durch Induktion nach k, daß ein Leftist-Heap, dessen
Wurzel Pfad-Rang k besitzt, mindestens 2k − 1 Knoten enthält. Folglich ist der rechte Weg
in einem Leftist-Heap mit n Knoten nur O(log n) lang.
Achtung, Leftist-Heaps sind nicht notwendigerweise balanciert. Für einen Knoten in einem
Leftist-Heap mit n Elementen kann sein Pfad zur Wurzel des Heaps Länge Ω(n) besitzen
(siehe auch Abbildung 2.24). Daher ist das »übliche Einfügen«, bei dem ein Element als
Blatt eingefügt und dann hochgeschoben wird, bei den Leftist-Heaps keine gute Lösung
(dies ist auch ein Grund, warum wir D ECREASE -K EY in Leftist-Heaps nicht als gut unterstützt bezeichnen).
1
4
1
8
1
9
1
11
1
18
1
42
Abbildung 2.24: Ein Leftist-Heap muß nicht notwendigerweise balanciert sein. Für einen
Knoten kann sein Pfad zur Wurzel des Heaps Länge Ω(n) besitzen.
In unserer Implementierung des Leftist-Heaps speichert jeder Knoten neben den Zeigern
auf die Söhne und dem Schlüsselwert noch seinen Rang. Wir halten uns noch einen Zeiger root[H] auf die Wurzel des Heaps. Das Erstellen eines leeren Leftist-Heaps L EFTISTM AKE ist daher extrem einfach, siehe Algorithmus 2.19, und benötigt nur konstante Zeit.
Algorithmus 2.19 Erstellen eines leeren Leftist-Heaps.
L EFTIST-M AKE()
1 root[H] ← NULL
Wir führen wieder die Operationen I NSERT und E XTRACT-M IN auf die Operation M ELD
zurück. Wir können ein Element x in einen Leftist-Heap H einfügen, indem wir zunächst
einen Leftist-Heap H 0 mit einem Element erzeugen. Dann verschmelzen wir H und H 0
mittels L EFTIST-M ELD(H, H 0 ). Der Zeitaufwand für I NSERT ist dann von der gleichen
Größenordnung wie für L EFTIST-M ELD(H, H 0 ). Das Einfügen ist im Pseudocode in Algorithmus 2.20 zu sehen.
Zum Extrahieren des Minimums E XTRACT-M IN (H) entfernen wir einfach die Wurzel und
verschmelzen den linken und rechten Teilbaum der Wurzel (siehe Algorithmus 2.21). Wiederum ist der Zeitaufwand von der gleichen Größenordnung wie für L EFTIST-M ELD.
Wir kommen jetzt zur Implementierung von L EFTIST-M ELD (siehe Algorithmus 2.22).
Zum Verschmelzen von zwei Leftist-Heaps H1 und H2 mit n1 bzw. n2 Elementen bestim-
47
48
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
Algorithmus 2.20 Einfügen in einen Leftist-Heap.
L EFTIST-I NSERT(H, x)
1 left[x] ← NULL
2 right[x] ← NULL
3 rank[x] ← 1
4 root[H 0 ] ← x
5 L EFTIST-M ELD(H, H 0 )
Algorithmus 2.21 Extrahieren des Minimums eines Leftist-Heaps.
L EFTIST-E XTRACT-M IN(H, x)
1 r ← root[H]
2 root[H1 ] ← left[r]
3 root[H2 ] ← right[r]
4 L EFTIST-M ELD(H1 , H2 )
men wir die rechten Pfade der Heaps und verschmelzen diese so, daß die Elemente auf
dem Resultatpfad absteigend geordnet sind. Dies ist in O(log n1 + log n2 ) Zeit möglich, da
jeder der beiden rechten Pfade bereits absteigend sortiert ist (die Leftist-Heaps sind heapgeordnet) und beide rechten Pfade logarithmische Länge haben. Als nächstes berechnen
wir die Ränge der Knoten auf dem Resultatpfad neu und stellen die Leftist-Eigenschaft
durch Vertauschen von linken und rechten Söhnen wieder her. Auch dafür benötigen wir
nur O(log n1 + log n2 ) Zeit. Die Abbildungen 2.25 und 2.26 zeigen ein Beispiel für das
Verschmelzen von zwei Leftist-Heaps.
Zum Erstellen eines Leftist-Heaps aus n Elementen könnten wir die Elemente nacheinander
in den anfangs leeren Heap einfügen. Das liefert uns aber wiederum nur eine Komplexität
von O(n log n) für L EFTIST-B UILD. Man kann diese Zeitschranke wieder auf O(n) verbessern. Dazu benutzen wir die Funktion H EAPIFY(L), die aus einer Liste L von LeftistHeaps einen gemeinsamen Leftist-Heap durch geschicktes Verschmelzen baut, siehe Algorithmus 2.23.
Wir analysieren zunächst die Zeitkomplexität von L EFTIST-H EAPIFY(L). Sei k die Anzahl der Heaps in der Liste L und n die Gesamtanzahl der Elemente. Wir betrachten den
ersten Durchlauf durch die Liste L. Nach dk/2e Aufrufen von L EFTIST-M ELD ist jeder
der k Ausgangsheaps mit einem anderen Heap verschmolzen worden. Uns verbleiben noch
bk/2c Heaps. Sei ni , i = 1, . . . , bk/2c die Anzahl der Elemente im iten dieser Heaps.
Pbk/2c
Dann ist n =
i=1 ni . Wir wissen, daß ein L EFTIST-M ELD , das zu einem Heap mit
ni Elementen führt, in O(log ni ) Zeit ausgeführt werden kann. Daher ist die Gesamtzeit
für die dk/2e Aufrufe von L EFTIST-M ELD:

bk/2c
O
X
i=1

max{1, log ni } .
(2.6)
Pbk/2c
Aus i=1
ni = n und 0 ≤ ni ≤ n folgt dann, daß der Term in (2.6) von der Größenordnung O(k max{1, log nk }) ist.
Wir haben somit gezeigt, daß der erste Lauf durch die Liste O(k max{1, log nk }) Zeit benötigt und die Anzahl der Heaps in der Liste für den nächsten Durchlauf auf bk/2c mindestens halbiert. Allgemein sind nach dem iten Durchlauf noch bk/2 i c Heaps übrig. Daher
sind nach spätestens blog2 kc Durchläufen noch zwei Heaps übrig und der Algorithmus
2.6 Leftist-Heaps
Algorithmus 2.22 Verschmelzen zweier Leftist-Heaps.
L EFTIST-M ELD(H1 , H2 )
( Der Algorithmus L EFTIST-M ELD dient eigentlich nur der Abfrage der Spezialfäl- )
le, daß einer der beiden Heaps leer ist. Die eigentliche Arbeit wird in L EFTIST1
M ESH erledigt.
2 if root[H1 ] = NULL then
3
root[H] ← root[H2 ]
4 else
5
if root[H2 ] = NULL then
6
root[H] ← root[H1 ]
7
end if
8 else
9
L EFTIST-M ESH(H1 , H2 )
{ L EFTIST-M ESH zerstört H1 und H2 und legt das Ergebnis in H1 ab. }
10
root[H] ← root[H1 ]
11 end if
L EFTIST-M ESH(H1 , H2 )
1 r1 ← root[H1 ]
{ Ein Zeiger auf die Wurzel von H1 . }
2 r2 ← root[H1 ]
{ Ein Zeiger auf die Wurzel von H2 . }
3 if key[r1 ] > key[r2 ] then { r1 ist danach die Wurzel mit kleinerem Schlüsselwert. }
4
Vertausche r1 und r2 .
5 end if
6
{ Zur Erinnerung: r1 ist die Wurzel mit kleinerem Schlüsselwert, siehe oben. }
7 if right[r1 ] = NULL then
{ Falls r1 keinen rechten Sohn hatte, dann reduziert sich das Verschmelzen der
rechten Pfade auf das Anhängen des rechten Pfades von H2 rechts an r1 . }
8
right[r1 ] ← r2
9 else
10
right[r1 ] ← L EFTIST-M ESH(right[r1 ], r2 )
11 end if
12 if rank[left[r1 ]] < rank[right[r1 ]] then
13
Vertausche left[r1 ] und right[r1 ].
14 end if
15 rank[r1 ] ← rank[right[r1 ]] + 1
16 return r1
49
50
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
2
2
2
5
1
2
4
1
1
14
1
6
2
1
10
12
30
1
1
1
8 1
15
16
11
20
(a) Die beiden Ausgangsheaps. Die rechten Pfad in den Heaps sind gestrichelt und
durch weiße Knoten hervorgehoben.
2
3
2
2
4
5
1
1
14
1
1
12
15
1
6
1
1
2
16
30
10
8
1
1
20
11
(b) Die beiden rechten Pfade der Heaps wurden so verschmolzen, daß auf dem Ergebnispfad die Schlüsselwerte absteigend sortiert sind.
2
3
2
2
4
5
1
1
14
2
1
12
15
1
6
1
16
1
2
30
10
8
1
1
20
11
(c) Wir starten mit dem letzten Knoten auf dem Resultatpfad und berechnen seinen
Pfad-Rang neu (was in unserem Beispiel kein neues Ergebnis liefert).
2
3
2
2
4
5
1
1
14
1
2
1
12
15
6
1
16
1
2
30
10
8
1
1
11
20
(d) Wir steigen nun den Resultatpfad zur Wurzel hinauf. Man geht für den aktuellen
Knoten davon aus, daß die Ränge seiner Söhne bereits korrekt berechnet sind. Dabei
ist zu beachten, daß sich ja nur der Rang eines Sohnes geändert hat. Falls der Rang
des rechten Sohnes größer als der des linken Sohnes ist, werden die beiden Söhne
vertauscht. Diese Vertauschung ist im aktuellen Knoten nicht nötig. Danach wird der
Rang des Knotens neu berechnet.
Abbildung 2.25: Verschmelzen von zwei Leftist-Heaps.
2.6 Leftist-Heaps
51
2
3
2
2
4
5
1
1
14
1
1
2
12
6
15
1
2
16
10
1
8
30
1
1
11
20
(a) Im Knoten 5 müssen zum ersten Mal die beiden Söhne vertauscht werden. Gezeigt ist hier schon das Ergebnis.
3
3
2
2
4
5
1
1
14
1
1
2
6
12
15
1
2
16
10
8
1
30
1
1
11
20
(b) Das Verfahren terminiert in der Wurzel, in der ebenfalls der Pfad-Rang neu berechnet wird.
Abbildung 2.26: Fortsetzung: Verschmelzen von zwei Leftist-Heaps.
Algorithmus 2.23 Konstruieren eines Leftist-Heaps aus einer Menge S von Elementen.
Die wichtigste Funktion ist L EFTIST-H EAPIFY.
L EFTIST-B UILD -H EAP(S)
1 L←∅
{ Eine leere Liste. }
2 for all s ∈ S do
3
Mache aus s einen einelementigen Leftist-Heap und hänge ihn hinten an L an.
4 end for
5 L EFTIST-H EAPIFY(L)
L EFTIST-H EAPIFY(L)
1 if |L| = 0 then
2
return NULL
{ Der leere Leftist-Heap. }
3 end if
4 while |L| ≥ 2 do
{ Solange noch mehr als ein Heap in der Liste ist. . . }
5
Entferne die ersten beiden Heaps H1 und H2 aus L.
{ Dies ist in konstanter Zeit
möglich, da wir die Elemente von vorne aus der Liste entfernen. }
6
L EFTIST-M ELD(H1 , H2 )
7
Hänge das Ergebnis hinten wieder an L an.
8 end while
9 return den einzelnen verbleibenden Heap in L.
52
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
Operation
M AKE
I NSERT
M INIMUM
E XTRACT-M IN
M ELD
B UILD
Leftist-Heap
O(1)
O(log n)
O(1)
O(log n)
Θ(n)
O(n)
Leftist-Heap mit verzögertem Verschmelzen
O(1)
O(1)
O k · max 1, log nk O k · max 1, log nk
O(1)
O(n)
Tabelle 2.3: Zeitkomplexität der Prioritätsschlangen-Operationen bei Implementierung
durch einen Leftist-Heap der Größe n und einen Leftist-Heap der Größe n mit verzögertem
Verschmelzen. Der Parameter k bei M INIMUM/E XTRACT-M IN bezeichnet die Anzahl der
Knoten, die bei M INIMUM/E XTRACT-M IN aus dem Heap entfernt werden.
terminiert. Der Aufwand ist dann


blog2 kc
o
n
X k
n
 = O k · max 1, log n
O
.
·
max
1,
log
2i
k/2i
k
i=1
Da wir nun die Zeitkomplexität für L EFTIST-H EAPIFY kennen, ist die Bestimmung des
Aufwandes für L EFTIST-B UILD nicht mehr schwierig. Für das Erstellen von n einelementigen Heaps benötigen wir O(n) Zeit. Danach lassen wir L EFTIST-H EAPIFY mit
k = n Heaps in der Liste und insgesamt n Elementen laufen, so daß wir eine Gesamtlaufzeit von O(n · max{1, log nn }) = O(n) erhalten.
Die Laufzeiten für die einzelnen Prioritätsschlangen-Orperationen in Leftist-Heaps sind in
Tabelle 2.3 aufgeführt. Dabei sind auch die Werte für eine Abwandlung des Leftist-Heaps,
nämlich des Leftist-Heaps mit verzögertem Verschmelzen, angegeben, mit dem wir uns im
nächsten Abschnitt beschäftigen.
2.6.1 Verzögertes Verschmelzen
Beim verzögerten Verschmelzen von zwei Leftist-Heaps H1 und H2 werden die beiden
Heaps nicht sofort anhand ihrer rechten Wege zusammengeordnet. Wir schieben die wirkliche Arbeit erst einmal auf und führen einen »Dummy-Knoten«als neue Wurzel ein, der
die beiden Heaps H1 und H2 als Söhne hat (siehe Abbildung 2.27)). Dabei müssen wir
lediglich darauf achten, daß der Heap mit dem größeren Grad in der Wurzel zum linken
Sohn wird. Algorithmus 2.24 zeigt das Verfahren im Pseudocode. Offenbar läuft L EFTISTL AZY-M ELD in O(1) Zeit.
Das Verschmelzen zweier Leftist-Heaps ist mit dem sehr bequemen L EFTIST-L AZY-M ELD
sehr schnell geworden. Allerdings müssen wir für diese Bequemlichkeit einen Preis bezahlen: nach ein paar Verschmelzungen enthält unser Heap zahlreiche Dummy-Knoten.
Insbesondere ist nun nicht mehr garantiert, daß in der Wurzel unseres Heaps das Element
mit minimalem Schlüsselwert steht. Bei einer M INIMUM-Anfrage müssen wir den Heap
erst einmal nach dem Minimum durchforsten. Haben wir uns etwa mehr Schwierigkeiten
eingehandelt als wir Nutzen bekommen haben?
Zunächst zeigen wir, wie wir L EFTIST-L AZY-M INIMUM, d.h. das Finden des Minimums
in einem Leftist-Heap, mit verzögertem Verschmelzen effizient implementieren können.
Wir erstellen eine Liste L von allen Nicht-Dummy-Knoten mit der Eigenschaft, daß alle
ihre Vorfahren im Heap Dummy-Knoten sind, und löschen zugleich alle Dummy-Knoten,
die nur Dummy-Knoten als Vorfahren haben. Dann verschmelzen wir die (disjunkten!)
Teilbäume mit Wurzeln in L mittels L EFTIST-H EAPIFY.
2.6 Leftist-Heaps
53
2
1
4
3
1
1
2
8
6
4
1
1
1
9
14
2
12
10
1
1
1
16
11
20
(a) Die beiden Ausgangsheaps.
2
2
1
3
4
2
2
4
1
1
14
1
8
2
12
1
9
10
1
1
16
1
6
11
20
(b) Ein speziell markierter Dummy-Knoten wird als neue Wurzel des Resultatheaps
eingeführt. Die Wurzel der Ausgangsheaps mit den größeren Pfad-Rang wird der linke Sohn der neuen Wurzel, die andere Wurzel wird zum rechten Sohn. Somit ist im
entstehenden Heap die Leftist-Eigenschaft gesichert.
Abbildung 2.27: Verzögertes Verschmelzen von Leftist-Heaps.
54
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
Algorithmus 2.24 Verzögertes Verschmelzen von Leftist-Heaps.
L EFTIST-L AZY-M ELD(H1, H2 )
Aus Effizienzgründen fragen wir die Sonderfälle, in denen einer der beiden Heaps
1
leer ist, gesondert ab.
2 if root[H1 ] = NULL then
3
root[H] ← root[H2 ]
4 else
5
if root[H2 ] = NULL then
6
root[H] ← root[H1 ]
7
end if
8 else
9
r ← neuer Knoten mit Dummy-Knoten-Kennzeichnung
( Die Dummy-Knoten-Kennzeichnung kann entweder durch ein )
zusätzliches Feld mark[v] = true in jedem Knoten v oder durch
Setzen von key[v] := −∞ erfolgen.
10
r1 ← root[H1 ]
{ Ein Zeiger auf die Wurzel von H1 . }
11
r2 ← root[H1 ]
{ Ein Zeiger auf die Wurzel von H2 . }
12
if rank[r1 ] < key[r2 ] then
{ r1 ist danach die Wurzel mit größerem Rang. }
13
Vertausche r1 und r2 .
14
end if
15
{ Zur Erinnerung: r1 ist die Wurzel mit größerem Pfad-Rang, siehe oben. }
16
left[r] ← r1
17
right[r] ← r2
18
rank[r] ← rank[r2 ] + 1
19 end if
Zum Erstellen der Liste L (Unterprogramm L EFTIST-P URGE) starten wir in der Wurzel des
Heaps und durchlaufen den Heap dann in Prä-Order: rekursiv wird ein Knoten v, dann sein
linker Teilbaum und danach sein rechter Teilbaum durchlaufen. Wir brechen die Rekursion
in einem Teilbaum ab, sobald wir einen Nicht-Dummy-Knoten gefunden haben.
Wenn durch den Aufruf von L EFTIST-P URGE k Knoten aus dem Heap gelöscht werden,
so enthält die Liste L maximal 2k Knoten: jeder Knoten in L hat als Vater einen der gelöschten Knoten, und jeder Knoten
im Heap
hat maximal zwei Söhne. Damit benötigt dann
L EFTIST-H EAPIFY O k · max 1, log nk Zeit.
Folglich ist der gesamte Zeitaufwand für L EFTIST-L AZY-M INIMUM O k · max 1, log nk .
Der Pseudocode für L EFTIST-L AZY-M INIMUM ist in Algorithmus 2.25 dargestellt, Abbildung 2.28 zeigt ein Beispiel. Im Code von Algorithmus 2.25 wird in Zeile 4 auf ein
Unterprogramm D ELETED zurückgegriffen. Im Moment benötigen wir nur das triviale Unterprogramm, das immer false zurückgibt. Wir werden im Abschnitt 2.6.2 ein hilfreiches
Unterprogramm für eine spezielle Anwendung benutzen.
Die Algorithmen 2.26 und 2.27 zeigen die Implementierungen der noch fehlenden Operationen zum Extrahieren des Minimums und zum Einfügen in einen Leftist-Heap mit verzögertem Verschmelzen. Die Zeitkomplexitäten sind unmittelbar ersichtlich und in Tabelle 2.3 mit den anderen Operationen zusammengefasst.
2.6.2 Nochmals der Algorithmus von Boruvka
Wir benutzen nun die Leftist-Heaps mit verzögertem Verschmelzen, um eine noch schnellere Implementierung des Algorithmus von Boruvka (Algorithmmus 2.10 bzw. 2.11) zu
erhalten. Unsere neue Implementierung ist in Algorithmus 2.28 angegeben. Im wesentli-
2.6 Leftist-Heaps
55
Algorithmus 2.25 Finden des Minimums in einem Leftist-Heap mit verzögertem Verschmelzen.
L EFTIST-L AZY-M INIMUM(H)
1 r ← root[H]
2 L ← L EFTIST-P URGE(r)
3 L EFTIST-H EAPIFY(L)
L EFTIST-P URGE(v)
1 if v = NULL then
2
return ∅
{ Eine leere Liste. }
3 end if
4 if v ist kein Dummy-Knoten and not D ELETED(v) then
5
return Liste mit einzigem Element v
6 else
7
L1 ← L EFTIST-P URGE(left[v])
8
L2 ← L EFTIST-P URGE(right[v])
9
Lösche v.
{ v ist ein Dummy-Knoten. Hier wird der Speicherplatz für v wieder freigegeben. }
10
return L1 , L2
{ Die Liste, die aus Aneinanderhängen von L1 und L2 entsteht. }
11 end if
D ELETED(v)
1 return false
2
2
1
2
2
2
1
1
16
1
6
1
1
2
12
1
1
6
4
2
4
10
1
16
1
20
2
12
10
(b) Zwei Knoten, deren sämtliche Vorfahren Dummy-Knoten sind, werden
gefunden. Die beiden in ihnen wurzelnden Teibäume werden L EFTISTH EAPIFY übergeben. Alle nicht mehr
aufgeführten Dummy-Knoten werden
durch L EFTIST-P URGE gelöscht.
1
1
20
(a) Der Ausgangsheap
2
4
2
1
6
1
2
10
1
1
12
16
1
20
(c) Endergebnis
H EAPIFY
nach
L EFIST-
Abbildung 2.28: Finden des Minimums in einem Leftist-Heap mit verzögertem Verschmelzen.
56
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
Algorithmus 2.26 Extrahieren des Minimums in einem Leftist-Heap mit verzögertem Verschmelzen.
L EFTIST-L AZY-E XTRACT-M IN(H)
1 L EFTIST-L AZY-M INIMUM(H)
{ Als Resultat steht nun im Wurzelknoten kein
Dummy-Knoten und damit das Minimum im Heap. }
2 root[H1 ] ← left[r]
3 root[H2 ] ← right[r]
4 L EFTIST-L AZY-M ELD(H1 , H2 )
Algorithmus 2.27 Einfügen in einen Leftist-Heap mit verzögertem Verschmelzen.
L EFTIST-L AZY-I NSERT(H, x)
1 left[x] ← NULL
2 right[x] ← NULL
3 rank[x] ← 1
4 root[H 0 ] ← x
5 L EFTIST-L AZY-M ELD(H, H 0 )
chen ist dies die gleiche Implementierung wie Algorithmus 2.11 (die Implementierung mit
Binomial-Heaps) mit folgenden Unterschieden:
• Wir benutzen Leftist-Heaps mit verzögertem Verschmelzen anstelle von BinomialHeaps.
• In Algorithmus 2.11 mußten wir in Schritt 13 das Entfernen des Minimums aus einem Heap solange wiederholen, bis wir eine Kante gefunden hatten, deren Endpunkte in verschiedenen Komponenten Vi und Vj lagen. Bei unserer Implementierung
mit Hilfe von Leftist-Heaps benutzen wir ein »implizites Löschen« solcher Kanten.
Hier kommt das Unterprogramm D ELETED in L EFTIST-L AZY-M INIMUM ins Spiel:
wir durchlaufen den Heap und löschen Dummy-Knoten. Gleichzeitig behandeln wir
Kanten (u, v) mit D ELETED(u, v) = true wie Dummy-Knoten. Dabei ist unser einfaches Unterprogramm D ELETED in Algorithmus 2.28 dargestellt: eine Kante wird
als Dummy-Knoten behandelt, wenn beide Endknoten in der gleichen Zusammenhangskomponente liegen.
Mit dieser Änderung wird unser Code für den Algorithmus von Boruvka sogar noch ein
wenig einfacher, da wir jedes Mal beim Extrahieren des Minimums wissen, daß die Kante
zu unserem Teil des MSTs hinzugefügt werden kann.
Wir analysieren nun die Laufzeit von Algorithmus 2.28. Sei Qi (i = 1, . . . , n) der LeftistHeap, der in Schritt 10 als ites vom Anfang der Liste entfernt wird und m i seine Größe. Sei
außerdem Vi die zu Qi gehörende Eckenmenge und letztendlich ki die Anzahl der DummyKnoten und implizit als gelöscht markierten Kanten, die aus Qi entfernt werden, wenn in
Schritt 11 das Minimum aus Qi entfernt wird.
Wir unterteilen die Ausführung des Algorithmus in Phasen. Phase 0 besteht aus dem Bearbeiten aller Heaps, die zu Anfang in der Liste L stehen. Für j > 0 besteht Phase j aus dem
Bearbeiten aller Heaps, die in Phase j − 1 zu L hinzugefügt wurden (vgl. hierzu auch die
Markierungen im Beispiel in den Abbildungen 2.11 bis 2.14).
Lemma 2.19 Ist Qk ein Heap aus Phase j und Vk die zugehörige Eckenmenge, so hat Vk
mindestens 2j Knoten. Folglich stoppt Algorithmus 2.28 nach maximal blog 2 nc Phasen
mit einem MST.
2.6 Leftist-Heaps
Algorithmus 2.28 Implementierung des Boruvka-Algorithmus mittels Leftist-Heaps mit
verzögertem Verschmelzen.
L EFTIST-MST-B ORUVKA(G, c)
Input:
Ein zusammenhängender ungerichteter Graph G = (V, E) mit n := |V | und
m := |E| in Adjazenzlistendarstellung, eine Kantenbewertungsfunktion
c: E → R
Output: Ein minimaler aufspannender Baum T von G.
1 T ←∅
{ Kanten im zu erstellenden MST. }
2 L←∅
{ Eine doppelt verkettete Liste. }
3 for all vi ∈ V do
4
M AKE -S ET(vi )
{ Vi ← {vi } }
5
Ei ← { (vi , v) ∈ E }
6
Qi ← L EFTIST-B UILD -H EAP(Ei )
{ Ordne die von vi ausgehenden Kanten in einem Min-Heap. }
7
Füge Qi hinten an L an.
8 end for
9 while L hat mehr als ein Element do
10
Qi ← erstes Element in L
{ Qi werde dabei aus L entfernt. }
11
(u, v) ←
L
EFTIST-L AZY-E XTRACT-M IN(Q)


Durch das »implizite Löschen«von Kanten unter Zuhilfenahme des 



Unterprogramms D ELETED (s.u.) innerhalb von L EFTIST-L AZY

 M INIMUM sind hier (im Gegensatz zur Implementierung mit Binomial- 
Heaps) die beiden Endpunkte u und v in verschiedenen Mengen.
12
T ← T ∪ {(u, v)}
13
Vi ← F IND -S ET(v)
{ Finde die Menge Vi mit v ∈ Vi . }
14
Vj ← F IND -S ET(v)
{ analog für v. }
15
Entferne die zu Vi und Vj gehörenden Heaps Qi und Qj aus L.
16
U NION (Vi , Vj )
{ Ersetze Vi und Vj durch Vi ∪ Vj . }


Einer der beiden Heaps, o.B.d.A. Qi , ist bereits aus L entfernt. Er ist 




derjenige Heap, den wir zu Anfang der Iteration der while-Schleife als 






erstes Element von L entfernt haben. Der zweite Heap Qj ist in L in
konstanter Zeit auffindbar, wenn jede Menge Vk einen Zeiger auf den 






zugehörigen
Heap Qk in L speichert. Das Entfernen eines Elements 




aus einer doppelt verketteten Liste ist in konstanter Zeit durchführbar.
17
Q ← L EFTIST-L AZY-M ELD(Qi , Qj )
18
Füge Q hinten an L an.
19 end while
D ELETED(u, v)


Dieses Unterprogramm wird für L EFTIST-L AZY-M INIMUM benötigt. Wir löschen 



Kanten »implizit«: eine Kante, die in einem Heap gespeichert wird, wird wie ein
1


 Dummy-Knoten (alternativ: gelöscht) behandelt, wenn beide Endpunkte in der 
gleichen Zusammenhangskomponente liegen.
2 if F IND -S ET (u) = F IND -S ET (v) then
3
return true
4 else
5
return false
6 end if
57
58
Haufenweise Haufen: Heaps, d-Heaps, Intervall-Heaps, Binomial-Heaps und Leftist-Heaps
Beweis: Der Beweis folgt sofort durch Induktion nach j, da für j > 1 eine Menge in
Phase j aus zwei Mengen der Phase j − 1 entstanden ist.
2
Wir beschränken nun die Gesamtgröße aller Heaps Qi , die jemals vom Algorithmus angefasst werden.
Lemma 2.20 Es gilt:
n−1
X
i=1
mi ≤ (2m + 2n − 2) · blog2 nc
Beweis: Die zu zwei Heaps einer Phase gehörenden Eckenmengen sind disjunkt. Jede
Kante ist in maximal zwei Heaps einer Phase gespeichert, maximal einmal für jeden ihrer beiden Endpunkte. Somit ist die Gesamtanzahl der nicht implizit gelöschten Kanten
(die auch keine Dummy-Knoten sind) in den Heaps einer Phase höchstens 2m. Bei jedem verzögerten Verschmelzen mittels L EFTIST-L AZY-M ELD und jedem L EFTIST-L AZYE XTRACT-M IN wird maximal ein Dummy-Knoten in einen Heap eingefügt. Da wir nur
jeweils n − 1 Verschmelzungen und Entfernen von Minima haben, haben wir somit in einer
Phase insgesamt maximal n − 1 Dummy-Knoten, mit den normalen Kanten also maximal
2m + 2(n − 1) = 2m + 2n − 2 Elemente in den Heaps. Da wir nach Lemma 2.19 nur
blog2 nc Phasen haben, folgt die Behauptung.
2
Wir haben nun alle wichtigen Hilfsmittel zur Laufzeitanalyse in der Hand. Wie bei der
Implementierung mit Hilfe der Binomial-Heaps benötigen wir zum Erstellen der initialen
Heaps O(m) Zeit (Leftist-Heaps können in linearer Zeit erstellt werden). Insgesamt finden
n − 1 verzögerte Verschmelzungen statt, von denen uns jede O(1) Zeit kostet. Bis auf die
L EFTIST-L AZY-E XTRACT-M IN-Operationen benötigen wir also O(n + m) Zeit.
Nach unseren Zeitschranken für L EFTIST-L AZY-E XTRACT-M IN benötigt das ite Entfernen
des Minimums O(ki · max{log mi /ki } · α(n)) Zeit, da wir O(ki · max{log mi /ki }) Operationen und somit dabei auch ebenso oft D ELETED aufrufen (das jeweils zwei F IND S ET benötigt). Hierbei ist α(n) die Zeitkomplexität für ein F IND -S ET. Wir werden in
Abschnitt 5.3 eine entsprechende Datenstruktur kennenlernen. Die Funktion α(n) wächst
dabei extrem langsam (deutlich langsamer als log log log log . . . log n).
mi
Wir nennen das ite L EFTIST-L AZY-E XTRACT-M IN schnell, wenn ki ≤ (log
n)2 , ansonsten
heißt es langsam. Der Zeitaufwand für die schnellen L EFTIST-L AZY-E XTRACT-M INs ist
höchstens
!
!
n−1
n−1
X
X mi
mi
O(ki · max log
O
· max {1, log mi } · α(n)
· α(n)) = O
ki
(log n)2
i=1
i=1
!
n−1
X mi
=O
· α(n)
(da mi ∈ O(n2 ))
log
n
i=1
= O(m · α(n))
(nach Lemma 2.20).
Für die langsamen L EFTIST-L AZY-E XTRACT-M IN-Operationen ist nach Definition k i >
mi
(log n)2 und daher der gesamte Zeitaufwand beschränkt durch:
!
!
n−1
n−1
X
X
mi
mi
O(ki · max 1, log
O
ki log
· α(n)) = O
· α(n)
ki
mi /(log n)2
i=1
i=1
!
n−1
X
=O
ki log log n · α(n)
i=1
= O (m log log n · α(n)) .
2.6 Leftist-Heaps
Für die letzte Abschätzung haben wir benutzt, daß jede der 2m in einem Heap gespeicherten Kanten (jede Kante ist maximal zweimal vorhanden) einmal und auch jeder der 2(n−1)
Pn−1
Dummy-Knoten maximal einmal gelöscht werden kann, d.h. i=1 ki ≤ 2m + 2n − 2.
Beobachtung 2.21 Mit Hilfe von Leftist-Heaps mit verzögertem Verschmelzen und der
Datenstruktur für disjunkte Mengen aus Abschnitt 5.3 benötigt der Algorithmus von Boruvka zur Bestimmung eines MST O(m log log n · α(n)) Zeit auf einem Graphen mit n Ecken
und m Kanten.
59
60
Amortisierte Analyse
Bei der amortisierten Analyse berechnet man die durchschnittlichen Worst-Case-Kosten
einer Operation über eine ganze Folge von Operationen. Ziel ist es, daß »im Durchschnitt«
die Kosten einer Operation niedrig liegen. Die Formulierung »im Durchschnitt« bedeutet hier das Mittel über eine Folge von Operationen im Worst-Case. Es findet hier keine
probabilistische Analyse statt.
In diesem Kapitel wenden wir die amortisierte Analyse auf zwei einfache Beispiele an.
Unsere Analysetechnik benutzt dabei eine Potentialfunktion, die als »Bankkonto« benutzt
wird, um teure gegen billige Operationen zu verrechnen.
3.1 Stack-Operationen
Unser erstes (sehr einfaches) Beispiel ist ein Stack. Ein Stack ist ein Last-in-First-OutSpeicher S, auf dem die folgenden Operationen definiert sind:
• P USH(S, x) legt das Objekt x oben auf den Stack.
• P OP(S) liefert das oberste Objekt auf dem Stack und entfernt es vom Stack (wenn
der Stack leer ist, dann bricht die Operation mit Fehler ab).
Beide Operationen kosten O(1) Zeit. Wir erlauben jetzt noch eine weitere Operation
M ULTIPOP(S, k), welche die obersten k Objekte des Stacks entfernt. Diese Operation benötigt O(k) Zeit.
Wie groß ist die Laufzeit für eine Folge von n Operationen aus P USH-, P OP, M ULTIPOP
auf einem anfangs leeren Stack?
Offenbar benötigt jede M ULTIPOP-Operation höchstens O(n)-Zeit, da der Stack zu jedem
Zeitpunkt höchstens n Elemente enthält. Da die P USH- und P OP-Operationen jeweils nur
O(1)-Zeit benötigen, können wir den Gesamtaufwand mit n · O(n) = O(n 2 ) abschätzen.
Obwohl diese Abschätzung korrekt ist, liefert sie kein scharfes Resultat. Tatsächlich ist
der Gesamtaufwand für n Operationen nur O(n), also deutlich weniger. Der Schlüssel
ist hier die M ULTIPOP-Operation. Obwohl ein einzelnes M ULTIPOP sehr teuer sein kann,
verringert es dabei doch die Stackgröße. Insgesamt kann jedes der maximal n Elemente
nur einmal durch eine M ULTIPOP- oder P OP-Operation vom Stack entfernt werden, so
daß der Gesamtaufwand für alle Pops in der Folge nur so groß wie die Anzahl der P USHOperationen sein kann. Somit erhält man die Gesamtlaufzeit von O(n).
Wir haben eben gezeigt, daß eine Folge von n Operationen O(n) Zeit benötigt. Im amortisierten Sinne, d.h. im Durchschnitt, kostet damit jede der n Operationen O(n)/n =
O(1) Zeit.
62
Amortisierte Analyse
Im folgenden benutzen wir eine Potentialfunktion, um das gleiche Ergebnis noch einmal
herzuleiten. Für das einfache Beispiel mag die Analyse nach einem zu großen Geschoß
aussehen, allerdings werden hier bereits die technischen Hilfsmittel sichtbar.
Eine Potentialfunktion Φ ordnet einer Datenstruktur D einen reellen Wert Φ(D), das Potential, zu, welches mißt, »wie gut« oder »wie schlecht« die aktuelle Konfiguration ist.
Man kann sich Φ(D) gewissermaßen als Bankkonto vorstellen. Wir verteuern künstlich eine billige Operation, indem wir zusätzlich zu den realen Kosten noch etwas auf das Konto
einzahlen. Bei real teuren Operationen entnehmen wir Geld von Konto, um die Operation
»amortisiert« ebenfalls günstig zu machen.
Wir starten mit einer Ausgangsdatenstruktur D0 , auf die n Operationen wirken. Wir bezeichnen mit ci die (realen) Kosten der iten Operation, welche auf der Datenstruktur D i−1
arbeitet und als Ergebnis Di liefert. Die amortisierten Kosten ai bei der iten Operation sind
ai =
ci
|{z}
reale Kosten für die ite Operation
+ Φ(Di ) − Φ(Di−1 ) .
|
{z
}
Potentialänderung
Wenn die Differenz Φ(Di ) − Φ(Di−1 ) negativ ist, dann unterschätzt ai die tatsächlichen
Kosten ci . Die Differenz wird durch das Entnehmen des Potentialverlustes aus dem Konto
abgedeckt. Es gilt nun:
n
X
ai =
i=1
n
X
i=1
Also haben wir:
(ci + Φ(Di ) − Φ(Di−1 )) =
n
X
i=1
ci =
n
X
i=1
n
X
i=1
ci + Φ(Dn ) − Φ(D0 ).
ai + Φ(D0 ) − Φ(Dn ).
(3.1)
(3.2)
Wenn wir ein Potential definieren können, so daß Φ(Dn ) ≥ Φ(D0 ) gilt, dann überschätzen
die amortisierten Kosten die realen Kosten, und eine obere Schranke für die amortisierten Kosten ist dann auch eine Schranke für die realen Kosten. Dieses Ergebnis ist derart
wichtig, daß wir es (in Variationen) in einem Satz festhalten.
Satz 3.1 Sei D0 eine Datenstruktur, auf die n Operationen wirken, wobei die ite Operation
die Datenstruktur Di−1 ∈ D in die Datenstruktur Di ∈ D überführt. Sei Φ : D → R ein
Potential.
(i) Gilt Φ(Dn ) ≥ Φ(D0 ), so sind die gesamten realen Kosten für die n Operationen
nach oben durch die amortisierten Kosten für die Folge beschränkt.
(ii) Gilt Φ(Di ) ≥ 0 für i = 0, . . . , n, so sind die realen Kosten durch die amortisierten
Kosten plus das Anfangspotential Φ(D0 ) nach oben beschränkt.
Beweis: Siehe oben.
2
Der einfachste Weg, um Φ(Dn ) ≥ Φ(D0 ) zu erhalten, ist es, ein Potential mit Φ(Di ) ≥ 0
und Φ(D0 ) = 0 zu finden. Wir führen dies an unserem Stack-Beispiel vor. Wir definieren Φ(S) := |S|. Offenbar erfüllt dieses Potential unsere Anforderungen. Wie groß sind
nun die amortisierten Kosten? Wir können annehmen, daß die P USH- und P OP-Operation
jeweils reale Kosten 1 und die M ULTIPOP-Operation reale Kosten k besitzt.
Wir betrachten die ite Operation. Ist diese ein P USH, so gilt
ai = 1 + |Si | − |Si−1 | = 1 + (|Si−1 | + 1 − |Si−1 |) = 2.
3.2 Konstruieren eines Binomial-Heaps
Im Falle eines M ULTIPOP (das als Spezialfall das P OP mit k = 1 enthält) gilt:
ai = k + |Si | − |Si−1 | = k + (|Si−1 | − k − |Si−1 |) = 0.
P
Eine Folge von n Operationen besitzt somit die amortisierten Kosten ni=1 ai ≤ 2n ∈
O(n). Mit Hilfe von Satz 3.1 erhalten wir dann auch die Schranke O(n) für die realen
Kosten der Folge.
3.2 Konstruieren eines Binomial-Heaps
In Abschnitt 2.5.6 haben wir eigentlich bereits eine amortisierte Analyse mit Hilfe einer
Potentialfunktion durchgeführt. Hier ging es darum, den Gesamtaufwand für das Einfügen
von n Elementen in einen Binomial-Heap mit anfänglich m Elementen abzuschätzen. Sei
das Potential eines Binomial-Heaps mit k Elementen definiert als die Anzahl der Einsen in
der Binärdarstellung von k, d.h. Φ := v(k)
Das Einfügen des iten Elements, i = 1, . . . , n, benötigt unterschiedlich viele Operationen, die davon abhängen, wie die Binärdarstellung von m + (i − 1), der bereits im Heap
vorhandenen Anzahl von Elementen, ist. Die Anzahl der Operationen ist jedoch höchstens
eins größer als die Anzahl der sich auf Null ändernden Bits. Wir nehmen an, daß das ite
Einfügen ti Bits auf Null setzt. Die amortisierten Kosten sind dann:
ai ≤ 1 + ti + v(m + i) − v(m + i − 1).
Man beachte,
daß v(m+i) ≤ v(m+i−1)−ti +1 gilt. Folglich
Pn ist ai ≤ 1+ti −ti +1 = 2. Es
Pn
folgt i=1 ai ∈ O(n). Die realen Kosten sind maximal i=1 ai + Φ0 = O(n) + v(m) =
O(n + log m). Genau dies ist das Ergebnis, welches wir bereits in Abschnitt 3.2 erhalten
hatten.
3.3 Dynamische Verwaltung einer Tabelle
Zur Speicherung einer dynamisch wachsenden Tabelle soll Speicherplatz alloziiert werden.
Speicherplatz steht in Form von Blöcken zur Verfügung. Die Tabelle muß in aufeinanderfolgenden Speicheradressen untergebracht werden. So lange die Tabelle noch nicht voll
belegt ist, können wir weitere Elemente einfügen.
Sobald die Tabelle voll ist, müssen wir eine neue Tabelle erzeugen, welche mehr Einträge
als die alte besitzt. Da die Tabelle immer in kontinuierlich im Speicher liegen soll, müssen
wir dann neuen Speicherplatz anfordern und die gesamte alte Tabelle in die neue kopieren.
Wir stellen nun einen Algorithmus zur dynamischen Verwaltung der Tabelle vor und analysieren seine Laufzeit. Dabei bezeichnen wir mit T das Tabellenobjekt und mit table[T ]
einen Zeiger auf den Speicherblock, ab dem die Tabelle im Speicher steht. Mit num[T ] bezeichnen wir die Anzahl der gespeicherten Einträge und mit size[T ] die Größe der Tabelle.
In unserem Beispiel ist die Tabelle anfänglich leer, num[T ] = size[T ] = 0. Letztendlich
sei INSERT die elementare Funktion, welche ein neues Element in die Tabelle einfügt.
In unserer Analyse nehmen wir an, daß die Laufzeit von TABLE -I NSERT linear in der Anzahl der elementaren INSERT-Operationen ist. Wir analysieren die Laufzeit daher in der
Anzahl der INSERT-Operationen.
Wie groß ist der Aufwand für n TABLE -I NSERT-Operationen wenn man mit einer leeren Tabelle startet? Analog zum Stack-Beispiel in Abschnitt 3.1 kann man schnell eine
grobe obere Schranke angeben. Falls noch Platz in der Tabelle ist, so kann die ite EinfügeOperation in O(1) Zeit durchgeführt werden. Bei voller Tabelle müssen hingegen i INSERTOperationen beim Kopieren der Tabelle ausgeführt werden, so daß insgesamt ein Aufwand
63
64
Amortisierte Analyse
Algorithmus 3.1 Algorithmus zur dynamischen Tabellenvewaltung
TABLE -I NSERT(T, x)
1 if size[T ] = 0 then
2
Alloziiere eine neue Tabelle table[T ] der Größe 1.
3
size[T ] ← 1
4 end if
5 if num[T ] = size[T ] then
6
Alloziiere eine neue Tabelle newtable der Größe 2 · size[T ].
7
Füge alle Einträge aus table[T ] mittels INSERT in newtable ein.
8
Gebe den Speicherplatz table[T ] frei.
9
table[T ] = newtable
10
size[T ] = 2 · size[T ]
11 end if
12 Füge x in table[T ] mittels INSERT ein.
13 num[T ] ← num[T ] + 1
von Θ(i) anfällt. Bei insgesamt n Operationen kommen wir bei einer Worst-Case-Zeit pro
Operation von Θ(n) auf eine Gesamtzeit von O(n2 ).
Wir zeigen nun, wie man wieder mit Hilfe der amortisierten Analyse eine scharfe obere
Schranke herleiten kann. Wir werden mit Hilfe einer geeigneten Potentialfunktion zeigen,
daß die amortisierten Kosten jeder einzelnen Operation nur O(1) sind. Insgesamt erhalten
wir dann einen Aufwand von O(n).
Die Potentialfunktion benutzt die Idee des Bankkontos. Unmittelbar nach einer Expansion
der Tabelle ist das Potential gleich 0. Bis zur nächsten Expansion steigt das Potential an,
so daß wir damit für die teure nächste Expansion mit Hilfe der Potentialdifferenz bezahlen
können. Wir benutzen folgende Potentialfunktion
Φ(T ) := 2 · num[T ] − size[T ].
(3.3)
Nach einer Expansion ist size[T ] = 2 · num[T ], also Φ(T ) = 0. Insbesondere ist das Potential am Anfang ebenfalls gleich 0. Im Verlaufe der Einfüge-Operationen ist die Tabelle
immer mindestens halb gefüllt, so daß wir auch Φ(T ) ≥ 0 haben. Aus Satz 3.1 ersehen wir,
daß die Summe der amortisierten Kosten eine obere Schranke für die realen Kosten ist.
Wir betrachten nun die ite TABLE -I NSERT-Operation. Falls keine Expansion notwendig
ist, so gilt size[Ti ] = size[Ti−1 ] und daher:
ai = 1 + Φ(Ti ) − Φ(Ti−1 )
= 1 + (2 · num[Ti ] − size[Ti ]) − (2 · num[Ti−1 ] − size[Ti−1 ])
= 1 + (2 · num[Ti ] − size[Ti−1 ]) − (2 · (num[Ti ] − 1) − size[Ti−1 ])
= 1 + 2 = 3.
Falls expandiert wird, haben wir size[Ti ] = 2 · size[Ti−1 ], und es folgt:
ai = num[Ti ] + (2 · num[Ti ] − size[Ti ]) − (2 · num[Ti−1 ] − size[Ti−1 ])
= num[Ti ] + (2 · num[Ti ] − 2size[Ti−1 ]) − (2 · (num[Ti ] − 1) − size[Ti−1 ])
= num[Ti ] + 2 − size[Ti−1 ]
= num[Ti−1 ] + 1 − size[Ti−1 ] + 2
= num[Ti−1 ] + 1 − num[Ti−1 ] + 2
= 3.
3.3 Dynamische Verwaltung einer Tabelle
Hierbei haben wir benutzt, daß num[Ti−1 ] = size[Ti−1 ] gilt, da sonst nicht expandiert
werden muß.
Insgesamt erhalten wir also den behaupteten Aufwand von O(n) für eine Folge von n
Einfüge-Operationen auf einer anfänglich leeren Tabelle. Das ist eine gute Nachricht: im
Durchschnitt kostet jede der n Operationen nur konstante Zeit!
65
66
Fibonacci-Heaps
Fibonacci-Heaps sind eine weitere Datenstruktur, um effizient Prioritätsschlangen zu verwalten. Prioritätsschlangen spielen bei vielen Algorithmen eine wichtige Rolle. Wir haben bereits in Kapitel 2 den Algorithmus von Dijkstra als konkretes Anwendungsfeld von
Prioritätsschlangen kennengelernt. Bei Verwendung eines binären Heaps erhalten wir eine
Gesamtlaufzeit von O((n + m) log n). Diese Laufzeit ist zwar schon recht brauchbar, allerdings kann in dichten Graphen m = Ω(n2 ) gelten, so daß wir in diesem Fall eine (mehr
als) quadratische Laufzeit erhalten. Dies ist insbesondere für große Graphen, wie sie etwa
bei der Routenplanung auftreten, nicht akzeptabel. Mit Hilfe der d-nären Heaps konnten
wir für m = Ω(nε ) lineare Laufzeit erhalten. Allerdings bleibt für m ∈ Ω(n log n) ∩ o(nε )
ein weiter Bereich, wo wir noch Verbesserungspotential haben.
Mit Hilfe der Fibonacci-Heaps, welche wir in diesem Kapitel vorstellen und analysieren,
kann man die Laufzeit des Dijkstra-Algorithmus auf O(m + n log n) verringern, was zu einer deutlichen Beschleunigung bei dichten Graphen führt. Der Schlüssel zur Verringerung
der Zeitkomplexität liegt hier bei einer effizienten Unterstützung der D ECREASE -K EYOperation. Die Laufzeit von O(m + n log n) ist durchgängig besser als für binäre und
d-näre Heaps.
Tabelle 4.1 zeigt die Laufzeiten, die wir in diesem Kapitel beweisen werden, im Vergleich
zum binären Heap und zum Binomial-Heap. Die Laufzeit von O(m + n log n) für den
Dijkstra-Algorithmus erhält man mit Hilfe der angegebenen Laufzeiten sofort aus Satz 2.3.
4.1 Der Algorithmus von Prim
Als weitere Motivation für die Fibonacci-Heaps betrachten wir die Berechnung eines aufspannenden Baumes mit minimalem Gewicht. Im folgenden sei wieder G = (V, E) ein
Operation
M AKE
I NSERT
M INIMUM
E XTRACT-M IN
D ECRASE -K EY
M ELD
Binärer Heap
(worst-case)
O(1)
O(log n)
O(1)
O(log n)
O(log n)
Θ(n)
Binomial-Heap
(worst-case)
O(1)
O(log n)
O(1)
O(log n)
O(log n)
O(log n)
Fibonacci-Heap
(amortisiert)
O(1)
O(1)
O(1)
O(log n)
O(1)
O(1)
Tabelle 4.1: Zeitkomplexität der Prioritätsschlangen-Operationen bei Implementierung
durch einen binären Heap, durch einen Binomial-Heap und durch einen Fibonacci-Heap
der Größe n.
68
Fibonacci-Heaps
ungerichteter Graph in Adjazenzlistendarstellung. Der Algorithmus von Prim startet mit
einer leeren Kantenmenge T . Der Algorithmus besitzt die Eigenschaft, daß die Kantenmenge T zu jedem Zeitpunkt zusammenhängend ist. In jedem Schritt fügt er die jeweils
billigste Kante hinzu, die T mit dem Restgraphen verbindet.
Der Algorithmus von Prim (dargestellt in Algorithmus 4.1) ist dem Algorithmus von Dijkstra (Algorithmus 2.1 auf Seite 8) sehr ähnlich. Ein Beispiel für die Ausführung des
Algorithmus von Prim ist in den Abbildungen 4.1 bis 4.2 zu sehen.
Die Korrektheit des Algorithmus von Prim folgt mit Hilfe Satz 2.10:
Satz 4.1 Der Algorithmus von Prim findet einen MST. Er benötigt in einem Graphen mit
n Knoten und m Kanten O(n) I NSERT-, O(n) E XTRACT-M IN und O(m) D ECREASE K EY-Operationen auf einer Prioritätsschlange mit maximal n Elementen.
2
Beweis: Wir bezeichnen mit Si die Menge S der bereits aus der Prioritätsschlange extrahierten Knoten nach i Durchläufen der while-Schleife. Wir zeigen durch Induktion
nach |Si | = 1, daß (S, T ) ein Baum ist und im iten Durchlauf eine leichteste Kante aus
dem Schnitt [Si , V \ Si ] hinzugefügt wird. Nach Satz 2.10 ist die hinzugenommene Kante
sicher und dies impliziert dann die Korrektheit.
Der Induktionsanfang, i = 1 ist trivial: |S1 | = |{s}| = 1 und T = ∅; der Baum, der aus
nur einem Knoten und keiner Kante besteht, ist ein Baum.
Im Induktionsschritt nehmen wir an, daß die Aussage für i richtig ist, und betrachten den
(i + 1)ten Durchlauf der while-Schleife. Sei wie im Algorithmus der Knoten u der Knoten,
der aus der Schlange Q entfernt wird. Nach Induktionsvoraussetzung enthält T zu Beginn
des aktuellen Durchlaufs noch keine Kante aus dem Schnitt [Si , V \ Si ] (sonst hätte T
Kanten, deren Endpunkte nicht in Si liegen). Da d[u] < +∞ (sonst wäre u nie in Q
aufgenommen worden), ist e[u] eine Kante e[u] = (v, u) ∈ [Si , V \ Si ], die in dieser
Iteration hinzugefügt wird. Wir müssen nur noch zeigen, daß (v, u) eine billigste Kante in
[Si , V \ Si ] ist.
Wäre (x, y) ∈ [Si , V \ Si ] mit c(x, y) < c(v, u), so ist genau einer der beiden Knoten
x und y in Si . Sei dies o.B.d.A. der Knoten x. Dann wurde x in einer früheren Iteration
aus der Schlange entfernt. Dabei wurde d[y] auf einen Wert von höchstens c(x, y) gesetzt.
Weiterhin ist danach y ∈ Q und wegen y ∈
/ §i wurde y auch noch nicht aus Q entfernt.
Da d[y] ≤ c(x, y) < c(v, u) = d[u] widerspricht dies aber der Annahme, daß u in der
aktuellen Iteration als Element mit minimalem Schlüsselwert aus der Schlange entfernt
wurde.
Die Laufzeit des Algorithmus ist leicht zu analysieren: Jeder Knoten wird genau einmal
in die Prioritätsschlange eingefügt und daraus entfernt. Für jede Kante im Graphen finden
maximal zwei D ECREASE -K EY-Operationen statt, eine für jeden Endpunkt.
2
4.1 Der Algorithmus von Prim
Algorithmus 4.1 Algorithmus von Prim.
MST-P RIM(G, c, s)
Input:
Ein zusammenhängender ungerichteter Graph G = (V, E) mit n := |V | und
m := |E|, eine Kantenbewertungsfunktion c : E → R und eine Startecke
s∈V.
Output: Ein minimaler aufspannender Baum T von G.
1 T ←∅
{ Die Kantenmenge, die zum Schluß den MST bildet. }
2 S ←∅
{ Die Menge der Ecken, die von T aufgespannt wird. }
3 for all v ∈ V do
4
d[v] ← +∞
5
e[v] ← NULL

/ S, so ist Kante e[v] ist die billigste Kante von v zu S. 
 Falls v ∈
Falls v ∈ S, so ist e[v] die Kante in T , durch deren Hinzunahme zu


T der Knoten v zu S gelangte.
6 end for
7 Q←∅
{ Erzeuge eine leere Prioritätsschlange Q. }
8 d[s] ← 0
9 I NSERT (Q, s)
{ Füge s mit Schlüsselwert d[s] = 0 in die Schlange ein. }
10 while Q 6= ∅ do
11
u ← E XTRACT-M IN (Q)
12
S ← S ∪ {u}
13
d[u] ← −∞
14
if u 6= s then
15
T ← T ∪ {e[u]}
16
end if
17
for all v ∈ Adj[u] do
18
if d[v] ← +∞ then
{ Es gab noch keine Kante (w, v) mit w ∈ S. }
19
d[v] ← c(u, v)
20
e[v] ← (u, v)
21
I NSERT(Q, v)
{ Füge v mit Schlüsselwert d[v] in Q ein. }
22
else
23
if c(u, v) < d[v] then
{ Es gab bereits eine Kante (w, v) mit w ∈ S, aber
diese hatte Kosten größer als c(u, v). }
24
d[v] ← c(u, v)
25
D ECREASE -K EY(Q, v, c(u, v))
{ Verringere den Schlüsselwert von v auf c(u, v). }
26
e[v] ← (u, v)
27
end if
28
end if
29
end for
30 end while
31 return T
69
70
Fibonacci-Heaps
0
1
1
+∞
+∞
4
2
3
2
+∞
3
+∞ 4
2
5
2
4
5
1
6 +∞
5
2
0
1
1
3
2
3 4
+∞
3
2
+∞
1
5
2
4
4
1
2
6 +∞
5
5
2
7
8
9
3
2
+∞
+∞
+∞
7
8
9
3
2
+∞
+∞
+∞
(a) Am Anfang sind alle
Distanzmarken +∞ bis
auf die des Startknotens,
hier der Knoten 1.
(b) Der Knoten 1 wird
aus der Schlange entfernt und alle adjazenten
Knoten betrachtet.
0
1
1
1
2
3
3 4
2
4
3
5
0
1
1
2
2
2
4
4
5
6 +∞
2
5
1
2
3
2
2 4
5
2
4
7
2
3
4
2
2
7
2
3
5
0
1
6 5
2
9
+∞
(e) Der Knoten 4 wird
aus der Schlange entfernt. Dabei wird diesmal keine einzige Distanzmarke verändert.
6 5
5
2
5
8
5
1
1
2
3
2
2
1
2
9
+∞
(d) Der Knoten 5 wird
aus der Schlange entfernt. Dabei wird unter
anderem der Schlüsselwert des Knotens 4 von 3
auf 2 erniedrigt.
1
5
4
3
4
2
2
2
4
4
3
5
8
5
2
2 4
(c) Der Knoten 2 wird
aus der Schlange entfernt.
1
1
2
3
7
9
8
3
2
+∞
+∞
+∞
0
1
1
2 4
5
2
4
7
2
4
2
2
4
3
1
5
6 5
5
3
8
3
2
2
9
+∞
(f) Der Knoten 7 wird
aus der Schlange entfernt.
Abbildung 4.1: Berechnung eines minimalen aufspannenden Baumes mit dem Algorithmus
von Prim. 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. Die dick gezeichneten Kanten sind jeweils
in der aktuellen Lösung, die zum Schluß einen minimalen aufspannenden Baum bildet,
enthalten.
4.2 Der Aufbau von Fibonacci-Heaps
0
1
1
1
2
3
2
2 4
5
2
4
7
2
4
2
2
1
6 5
2 4
1
2
2
1
2
3
2
2 4
5
2
4
7
2
4
9
2
4
7
2
2
2
8
3
8
3
1
1
2
3
2
6 2
2
2
6 2
5
5
3
0
1
1
5
1
2
9
2
2
(b) Der Knoten 9 wird
aus der Schlange entfernt.
1
3
5
3
2
2
5
2
4
3
4
3
(a) Der Knoten 8 wird
aus der Schlange entfernt.
0
1
1
2
1
2
5
8
3
0
1
4
3
5
3
71
9
2
(c) Der Knoten 6 wird
aus der Schlange entfernt.
2 4
5
2
4
7
2
4
2
2
1
3
1
5
6 2
5
3
8
3
2
2
9
2
(d) Nach dem Entfernen
des Knotens 3 terminiert
der Algorithmus.
Abbildung 4.2: Fortsetzung: Berechnung eines minimalen aufspannenden Baumes mit dem
Algorithmus von Prim. 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. Die dick gezeichneten Kanten sind
jeweils in der aktuellen Lösung, die zum Schluß einen minimalen aufspannenden Baum
bildet, enthalten.
Falls man die Prioritätsschlange Q als binären Heap verwaltet, so benötigen I NSERT,
E XTRACT-M IN und D ECREASE -K EY jeweils O(log |Q|) Zeit. Somit ist die Gesamtlaufzeit des Algorithmus von Prim O(m log n). Wieder kann man dies mit Hilfe der FibonacciHeaps auf O(m + n log n) beschleunigen.
Beobachtung 4.2 Mit Hilfe von binären Heaps benötigt der Algorithmus von Prim zur
Bestimmung eines MST O(m log n) Zeit auf einem Graphen mit n Ecken und m Kanten.
4.2 Der Aufbau von Fibonacci-Heaps
Ein Fibonacci-Heap besteht aus einer Kollektion von heap-geordneten Bäumen. Im Gegensatz zu den Binomial-Heaps aus Abschnitt 2.5 sind die einzelnen Bäume jedoch nicht
notwendigerweise Binomialbäume.
Jeder Knoten in einem Fibonacci-Heap enthält die folgenden Informationen:
• p[x] ist ein Zeiger auf den Vaterknoten von x (NULL, falls x der Wurzelknoten ist)
72
Fibonacci-Heaps
• child[x] ist ein Zeiger auf einen Sohn von x. Die Söhne von x sind in einer doppelt
verketteten zyklischen Liste organisert.
• Für einen Sohn y sind left[y] und right[y] Zeiger auf den Vorgänger bzw. Nachfolger in der verketteten Liste der Kinder.
• In degree[x] ist der Grad von x, d.h., die Anzahl seiner Kinder, gespeichert.
• mark[x] ist eine Markierung, die später für das noch genauer erklärt werden wird.
Abbildung 4.3 veranschaulicht die Organisation eines Fibonacci-Heaps. Ein FibonacciHeap H besitzt als ganzes noch folgende Attribute:
• min[H] ist ein Zeiger auf das Minimum im Heap.
• size[H] bezeichnet die Anzahl der Knoten im Heap.
In den folgenden Abschnitten werden wir zeigen, wie man die Operationen für Prioritätsschlangen mit Hilfe von Fibonacci-Heaps effizient implementieren kann. Die Analyse der
Laufzeit erfolgt mittels amortisierter Analyse (siehe Kapitel 3). Dabei verwenden wir die
folgende Potentialfunktion:
Φ(H) := t(H) + 2m · m(H),
wobei t(H) die Anzahl der Bäume in der Wurzelliste und m(H) die Anzahl der markierten
Knoten im Heap bezeichnet.
4.3 Implementierung der Basis-Operationen
Im folgenden sei D(n) eine obere Schranke für den maximalen Grad eines Knotens
in einem Fibonacci-Heap mit n Knoten. In der Laufzeitanalyse der Operationen FIB EXTRACT- MIN und FIB - DECREASE - KEY wird D(n) auftauchen. Wir zeigen später, daß
D(n) ∈ O(log n) gilt. Der Grund, warum wir den Beweis dieser logaritmischen oberen
Schranke auf später verschieben ist, daß wir dazu die Implementierung aller Operationen
kennen müssen.
Das Erstellen eines neuen Fibonacci-Heaps ist ganz einfach (siehe Algorithmus 4.2). Wir
setzen size[H] := 0, min[H] := NULL. Der reale Zeit-Aufwand dafür ist in O(1), ebenso
die amortisierten Kosten, da das Potential eines leeren Fibonacci-Heaps H gleich Φ(H) =
0 ist.
Algorithmus 4.2 Erstellen eines leeren Fibonacci-Heaps
F IB -M AKE()
1 min[H] ← NULL
2 size[H] ← 0
Als nächstes betrachten wir das Einfügen von neuen Elementen (siehe Algorithmus 4.3).
Im Folgenden gehen wir davon aus, daß jeder einzufügende Knoten x einen Schlüsselwert
key[x] besitzt, der bereits richtig gesetzt ist. FIB - INSERT(H, x) fügt das neue Element x
einfach unmarkiert in die zyklisch verkettete Wurzelliste ein: x wird quasi als Binomialbaum der Ordnung 0 behandelt und zur Liste hinzugefügt. Der Zeiger MIN[H] wird bei
Bedarf aktualisiert. Alle diese Operationen sind in konstanter Zeit durchführbar. Das Potential steigt um 1 (die Anzahl der Bäume in der Wurzelliste wird um eins größer), so daß
das Einfügen in amortisierter Zeit O(1) implementiert werden kann.
4.3 Implementierung der Basis-Operationen
min[H]
10
73
5
7
13
9
12
15
20
16
(a) Ein Fibonacci-Heap
min[H]
10
0
5
1
13
0
7
3
9
0
12
1
15
1
20
0
16
0
(b) Zeiger-Repräsentation im Computer.
Abbildung 4.3: Aufbau eines Fibonacci-Heaps
Algorithmus 4.3 Einfügen eines neuen Elements in einen Fibonacci-Heap
F IB - INSERT(H, x)
1 p[x] ← NULL
2 child[x] = NULL
3 degree[x] ← 0
4 mark[x] ← false
5 left[x] ← right[x] ← x
6 Verkette die Wurzelliste mit der einelementigen Wurzellisteliste, welche x enthält.
7 if min[H] = NULL oder key[min[H]] > key[x] then
8
min[H] ← x
9 end if
10 size[H] ← size[H] + 1
74
Fibonacci-Heaps
Das Vereinigen von zwei Fibonacci-Heaps H1 und H2 zu H geschieht ähnlich wie das
Einfügen eines neuen Elements (siehe Algorithmus 4.4). Die Wurzellisten von H 1 und H2
werden zu einer einzigen Liste in O(1) Zeit verkettet, der Zeiger auf das Minimum wird
entsprechend aktualisiert. Die Potentialänderung ist:
Φ(H) − Φ(H1 ) − Φ(H1 )
=t(H1 ) + t(H2 ) + 2m(H1 ) + 2m(H2 ) − (t(H1 ) + 2m(H1 )) − (t(H2 ) + 2m(H2 ))
=0.
Somit ist die amortisierte Zeit wiederum in O(1).
Algorithmus 4.4 Vereinigen zweier Fibonacci-Heaps
F IB -M ELD(H1 , H2 )
1 min[H] = min[H1 ]
2 if min[H] = NULL oder key[min[H]] > key[min[H 2 ]] then
3
min[H] ← min[H2 ]
4 end if
5 Verkette die Wurzellisten von H1 und H2 zu einer neuen Liste, der Wurzelliste von H.
6
size[H] ← size[H1 ] + size[H2 ]
Die Implementierung der MINIMUM-Operation ist trivial.
Das Extrahieren des Minimums in einem Fibonacci-Heap ist trickreicher als die »einfachen
Operationen« oben. Grob funktioniert es folgendermaßen: Sei z = min[H]. Wir entfernen
z aus der Wurzelliste und hängen alle Söhne von z in die Wurzelliste ein (siehe Algorithmus 4.5). Dies ist in O(D(n)) Zeit möglich. Danach folgt ein »Aufräumen«, in dem wir
die folgenden Schritte so lange wie möglich ausführen:
1. Wir finden zwei Wurzeln x und y in der Wurzelliste mit degree[x] = degree[y].
Sei key[x] < key[y].
2. Wir entfernen y aus der Wurzelliste und machen y zu einem Sohn von x. Dabei wird
degree[x] erhöht und die Markierung mark[y] gelöscht.
Ein Durchlauf der beiden Schritte oben kann in O(1) Zeit implementiert werden. Wir zeigen gleich, wie man das gesamte Aufräumen effizient organisieren kann.
Vorher soll aber noch etwas zu den Markierungen gesagt werden, die hier zum ersten mal
»richtig« auftauchen. Die Markierungen werden nachher noch beim Verringern von Schlüsselwerten (Abschnitt 4.4) eine entscheidende Rolle spielen. Hintergrund ist der folgende:
Wir wollen verhindern, daß ein heap-geordneter Baum »zu dünn« wird (und damit eine zu
große Höhe besitzt). Die Markierung an einem Knoten x zeigt im Prinzip an, daß x bereits
einen Sohn durch Löschen (bei FIB - DECREASE - KEY) verloren hat.
In Algorithmus 4.6 wird im Pseudo-Code angegeben, wie das Aufräumen nach dem Entfernen des Minimums und des Einhängens der Söhne effizient umgesetzt werden kann. Wir
verwenden ein Hilfsarray A mit folgender Bedeutung: Wenn A[i] = x 6= NULL, dann ist i
ein Knoten in der Wurzelliste mit degree[x] = i.
Zum Beginn des while-Loops in Zeile 7 gilt d = degree[x]. Die Wenn in Zeile 8 A[d] = y
gilt, so ist von allen bereits bearbeiteten Wurzeln der Knoten y der einzige mit degree[y] =
d. Nachdem y zum Sohn von x gemacht worden ist, kann man daher A[d] = NULL setzen.
Man sieht leicht, daß als Invariante im Loop gilt: d = degree[x]. Der while-Loop endet,
wenn es keine (bereits bearbeitete) Wurzel mit Grad d mehr gibt.
4.3 Implementierung der Basis-Operationen
Algorithmus 4.5 Extrahieren des Mininums eines Fibonacci-Heaps
F IB -E XTRACT-M IN(H)
1 z ← min[H]
2 if z 6= NULL then
3
for all Söhne x von z do
4
Füge x zur Wurzelliste von H hinzu.
5
p[x] ← NULL
6
end for
7
Entferne z aus der Wurzelliste von H.
8
if z = right[z] then
{ z war einziges Element im Heap }
9
min[H] ← NULL
{ temporäres Setzen des Minimums, wird durch FIB - CLEANUP korrigiert }
10
else
11
min[H] ← right[z]
12
FIB - CLEANUP (H)
13
end if
14
size[H] ← size[H] − 1
15 end if
Algorithmus 4.6 Aufräumen nach Extrahieren des Minimums im Fibonacci-Heap
F IB -C LEANUP(H)
1 for i ← 0, . . . , D(size[H]) do
2
A[i] ← NULL
3 end for
4 for all Knoten v in der Wurzelliste von H do
5
x←v
6
d ← degree[v]
7
while A[d] 6= NULL do
8
y ← A[d]
{ y ist ein Knoten in der Wurzelliste mit degree[y] = d }
9
if key[y] < key[x] then
10
vertausche x und y
11
end if
12
Entferne y aus der Wurzelliste.
13
Mache y zu einem Sohn von x, erhöhe degree[x].
14
mark[y] ← false
15
A[d] ← NULL
16
d← d+1
{ Der Grad von x hat sich erhöht }
17
end while
18
A[d] ← x
19 end for
20 min[H] = NULL
21 for i = 0, . . . , D(size[H]) do
22
if A[i] 6= NULL then
23
Füge A[i] zur Wurzelliste von H hinzu.
24
if min[H] = NULL oder key[A[i]] < key[min[H]] then
25
min[H] = A[i]
26
end if
27
end if
28 end for
75
76
Fibonacci-Heaps
Wie groß ist der Zeitaufwand für FIB - CLEANUP? Das Initialisieren des A-Arrays benötigt
O(D(n)) Zeit. Die Wurzelliste enthält zu Beginn von FIB - CLEANUP maximal t(H) − 1 +
D(n) Elemente (t(H) Wurzeln waren vor dem Extrahieren von z in der Liste, z wird
entfernt und maximal D(n) Söhne hinzugefügt). In jedem Durchlauf der while-Schleife in
FIB - CLEANUP werden zwei Bäume in der Liste zu einem in O(1) Zeit vereinigt. Somit ist
der Gesamtaufwand für den while-Loop O(t(H) + D(n)).
Das Potential ändert sich wie folgt: Vor dem Extrahieren ist das Potential t(H) + 2m(H).
Nach FIB - CLEANUP beträgt das Potential maximal D(n) + 1 + 2m(H), da wir für jeden
Grad zwischen 0 und D(n) maximal ein Element in der Wurzelliste haben. Die amortisierten Kosten sind also maximal:
O(t(H) + D(n)) + (D(n) + 1 + 2m(H)) − (t(H) + 1 + 2m(H))
|
{z
} |
{z
}
Potential am Ende
Potential zu Beginn
=O(t(H) + D(n)) + D(n) − t(H) + 1
=O(D(n)) + O(t(n)) − t(H)
=O(D(n)).
Für die letzte Gleichung haben wir benutzt, daß wir das Potential dergestalt skalieren könnten, daß die »versteckte Konstante« im O(t(n)) kompensiert wird.
4.4 Das Verringern von Schlüsselwerten
Das Verringern des Schlüsselwerts eines Knotens x im Fibonacci-Heap ist sehr trickreich
gelöst (siehe Algorithmus 4.7). Dabei kommen auch die Markierungen mark zum Einsatz.
Zunächst wird x der neue Schlüsselwert zugewiesen (der kleiner als der alte Schlüsselwert
sein muß). Falls der neue Schlüsselwert von x noch immer nicht kleiner ist als der des
Vaters y = p[x] von x (oder x keinen Vater besitzt), so sind wir bereits fertig.
Andernfalls müssen wir die Heap-Eigenschaft wieder herstellen. Wir entfernen x aus der
Sohnliste von y und fügen x zur Wurzelliste hinzu des Heaps hinzu. Dies erfolgt mit Hilfe
der Prozedur C UT.
Nachdem x von seinem Vater y abgetrennt worden ist, erfolgt ein rekursiver Aufruf von
CASCADING - CUT für den Vater y von x. Wenn y bereits markiert ist, dann wird auch y
von seinem Vater abgetrennt und in die Wurzelliste aufgenommen. Die Prozedur geht dann
beim Vater von y weiter.
Was bedeutet nun genau die Markierung? Eine Markierung mark[x] ist für einen Knoten x
genau dann auf true gesetzt, wenn folgende Aktionen hintereinander passiert sind:
1. x war eine Wurzel in der Wurzelliste.
2. x wird zum Sohn eines anderen Knotens gemacht.
3. x verliert einen Sohn.
Sobald nun x den zweiten Sohn verliert, wird x von seinem Vater abgeschnitten und wieder
in die Wurzelliste eingehängt.
Wir analysieren jetzt die Zeitkomplexität von FIB - DECREASE - KEY. Es ist einfach zu sehen,
daß jeder Aufruf von CUT und jeder rekursive Aufruf von CASCADING - CUT nur O(1) Zeit
benötigt. Wir nehmen an, daß insgesamt C rekursive Aufrufe von CASCADING - CUT erfolgen. Dann ist der reale Zeitaufwand für FIB - DECREASE - KEY in O(C).
4.4 Das Verringern von Schlüsselwerten
Algorithmus 4.7 Verringern eines Schlüsselwertes im Fibonacci-Heap
F IB -D ECREASE - KEY(H, x, k)
1 key[x] ← k
2 y ← p[x]
3 if y 6= NULL und key[x] < key[y] then
4
CUT (H, x, y)
5
CASCADING - CUT (H, y)
6 end if
7 if key[x] < key[min[H]] then
8
min[H] ← x
9 end if
C UT(H, x, y)
1 Entferne x aus der Sohnliste von y.
2 Füge x zur Wurzelliste von H hinzu.
3 p[x] ← NULL
4 mark[x] ← false
C ASCADING -C UT(H, y)
1 z ← p[y]
2 if z 6= NULL then
3
if mark[y] = true then
4
CUT (H, y, z)
5
CASCADING - CUT (H, z)
6
else
7
mark[y] ← true
8
end if
9 end if
77
78
Fibonacci-Heaps
Für die amortisierten Kosten müssen wir wiederum die Potentialänderung betrachten. Sei
H der Fibonacci-Heap vor dem FIB - DECREASE - KEY und H 0 der Aufwand nach der Ausführung von FIB - DECREASE - KEY. Alle rekursiven Aufrufe von CASCADING - CUT bis auf
den letzten erhöhen die Anzahl der Wurzeln in der Wurzelliste um eins und löschen eine
Markierung (der letzte Aufruf markiert möglicherweise einen Knoten und bricht dann ab).
Somit ist die Gesamtanzahl von Wurzeln in der Wurzelliste von H 0 maximal t(H) + C
(wir haben t(H) Bäume in der Wurzelliste von H, ein neuer Knoten ist x, dann kommen
noch C − 1 neue Knoten aus den rekursiven Aufrufen hinzu). Es werden C − 1 Markierungen gelöscht und maximal eine neue gesetzt, so daß m(H 0 ) ≤ m(H) − (C − 1) + 1 =
m(H) − C + 2 gilt.
Daher gilt:
Φ(H 0 ) − Φ(H) = t(H 0 ) + 2m(H 0 ) − (t(H) + 2m(H))
≤ t(H) + C + 2(m(H) − C + 2)) − t(H) − 2m(H)
= −C + 4
Damit sind die armortisierten Kosten für FIB - DECREASE - KEY in O(C) − C + 4 = O(1),
wobei wir wieder benutzen, daß wir das Potential derart skalieren können, daß die Konstante in der O-Notation kompensiert wird.
4.5 Beschränkung des Grades in Fibonacci-Heaps
Wir haben gezeigt, daß alle Operationen bis auf FIB - EXTRACT- MIN im Fibonacci-Heap in
O(1) amortisierter Zeit ausgeführt werden können. Für FIB - EXTRACT- MIN haben wir eine
amortisierte Schranke von O(D(n)) bewiesen, wobei D(n) der maximale Grad in einem
Fibonacci-Heap mit n Knoten ist. In diesem Abschnitt werden wir nun D(n) ∈ O(log n)
zeigen, so daß dann alle in Tabelle 4.1 aufgeführten Zeitkomplexitäten hergeleitet sind.
Lemma 4.3 Sei x ein beliebiger Knoten in einem Fibonacci-Heap H . Ordnet man die
Söhne von x in der Reihenfolge, wie sie an x angehängt wurden, so erfüllt für i ≥ 2 der ite
Sohn yi : degree[yi ] ≥ i − 2.
Beweis: Angenommen, x habe k Söhne y1 , . . . , yk (es ist übrigens möglich, daß x schon
mehr Söhne hatte, diese aber bereits wieder verloren hat). Als der ite Sohn y i an x
angehängt wurde, so hatte zu diesem Zeitpunkt x den gleichen Grad wie y i . Da dann
y1 , . . . , yi−1 (und möglicherweise noch mehr Knoten) Söhne von x waren, galt in diesem Moment degree[x] ≥ i − 1, also auch degree[yi ] ≥ i − 1. Im weiteren Verlauf kann
yi maximal einen Sohn verloren haben, da sonst yi von x abgeschnitten worden wäre, also
folgt degree[yi ] ≥ i − 2.
2
Mit Hilfe des Grad-Lemmas können wir nun eine untere Schranke für die Größe von
Teilbäumen in einem Fibonacci-Heap herleiten. Wir erinnern uns zunächst daran, wie die
Fibonacci-Zahlen Fk definiert waren:
F0 := 0
F1 := 1
Fk = Fk−2 + Fk−1
für k ≥ 2.
Die Fibonacci-Zahlen besitzen folgende Eigenschaften, die leicht durch Induktion zu beweisen sind:
Lemma 4.4
(i) Fk+2 = 1 +
Pk
i=1
Fi
4.6 Ein Minimalbaum-Algorithmus mit nahezu linearer Laufzeit
(ii) Fk ≥ φk , wobei φ =
√ 1+ 5
2
79
der Goldene Schnitt ist.
Die Fibonacci-Heaps tragen ihren Namen nicht zu Unrecht, wie das folgende Lemma zeigt.
Lemma 4.5 Sei x ein Knoten in einem Fibonacci-Heap H mit degree[x] = k und |T x |
die Anzahl der Knoten im Teilbaum, der an x hängt. Dann gilt |Tx | ≥ Fk+2 .
Beweis: Sei Sk die minimale Anzahl von Knoten in einem Teilbaum, dessen Wurzel Grad k
besitzt. Wir zeigen durch Induktion, daß Sk ≥ Fk+2 gilt. Wegen |Tx | ≥ Sk folgt dann die
Behauptung.
Offenbar gilt S0 = 1 und S1 = 2. Sei nun degree[x] = k und y1 , . . . , yk die Söhne von x
in der Reihenfolge, wie sie an x angehängt wurden. Wegen Lemma 4.3 gilt degree[y i ] ≥
i − 2 für i = 2, . . . , k. Da Sk monoton in k wächst folgt somit:
Sk ≥ 2 +
≥2+
≥2+
=1+
k
X
Sdegree[yi ]
(„+2 für x selbst und y1 )
Si−2
(nach Lemma 4.3)
Fi
(nach Induktionsvoraussetzung)
Fi
(da F1 = 1)
i=2
k
X
i=2
k
X
i=2
k
X
i=1
= Fk+2
(nach Lemma 4.4).
2
Damit können wir nun die gewünschte Schranke für D(n) zeigen. Ist x ein beliebiger Knoten in einem Fibonacci-Heap H mit n Knoten und degree[x] = k, so gilt nach Lemma 4.5
n = size[H] ≥ |Tx | ≥ φk .
(4.1)
Logarithmieren in (4.1) liefert dann k ≤ logφ n ∈ O(log n).
Beobachtung 4.6 Mit Hilfe eines binären Heaps als Datenstruktur für die Prioritätsschlange benötigt der Dijkstra-Algorithmus O(m + n log n) Zeit auf einem Graphen mit n Ecken
und m Kanten.
Beobachtung 4.7 Mit Hilfe eines binären Heaps als Datenstruktur für die Prioritätsschlange benötigt der Algorithmus von Prim zur Bestimmung eines MST O((n + m) log n) Zeit
auf einem Graphen mit n Ecken und m Kanten.
4.6 Ein Minimalbaum-Algorithmus mit nahezu linearer
Laufzeit
Als Abschluß des Kapitels geben wir als Anwendung der Fibonacci-Heaps einen schnellen
MST-Algorithmus, den Algorithmus von Fredman und Tarjan, an und analysieren seine
80
Fibonacci-Heaps
Laufzeit. Der Algorithmus baut auf dem Algorithmus von Prim (siehe Algorithmus 4.1)
auf und läuft in Zeit O(n + mβ(m, n)), wobei
β(m, n) := min{ i : log(i) n ≤ m/n }.
(4.2)
Die Funktion β wächst extrem langsam! Es gilt β(m, n) ≤ log ∗ n, wobei
log∗ n := min{ i : log(i) n ≤ 1 }.
(4.3)
Man beachte, daß log∗ 16 = 3, log∗ 65536 = 4, log∗ 265536 = 5. Zur Erinnerung: Die
geschätzte Zahl der Atome im Universum ist etwa 1080 < 2320 .
Die Idee des Algorithmus von Fredman und Tarjan ist es, die Prioritätsschlange Q in ihrer
Größe geschickt zu beschränken (denn E XTRACT-M IN benötigt O(log |Q|) amortisierte
Zeit). Die Grundidee des Algorithmus ist dabei die folgende:
1. Lasse einen einzelnen Baum T wie im Algorithmus von Prim wachsen, bis die
Schlange Q, welche die „Nachbarecken“ zu T enthält, eine gewisse Größe überschreitet.
2. Starte dann von einer neuen Ecke und stoppe wieder, falls Q zu groß wird.
3. Die ersten Schritte werden ausgeführt, bis jede Ecke in einem Baum enthalten ist.
Dann wird jeder Baum zu einer „Superecke“ kontrahiert, und der Algorithmus fährt
mit dem geschrumpften Graphen fort.
4. Nach einer genügenden Anzahl von Phasen bleibt nur noch eine Superecke übrig.
Expandieren liefert dann den MST.
Die Implementierung führt das Kontrahieren implizit aus. Für jeden Knoten v ∈ V halten
wir uns einen Eintrag tree[v], der angibt, in welchem Baum sich v befindet. In jeder Phase
beginnt man mit einem Wald von bisher gewachsenen alten Bäumen. Man verbindet man
dann die Bäume zu neuen Bäumen, die dann die alten Bäume für den nächste Phase werden.
Start einer Phase
1. Numeriere die alten Bäume und gib jeder Ecke die Nummer seines Baumes. Damit
kann man für jede Ecke v den Baum, dem sie zugehört, direkt aus tree[v] ablesen.
Der Aufwand für diesen Schritt ist O(n + m).
2. Aufräumen: Lösche aus dem Graphen alle Kanten, die zwei Ecken im gleichen Baum
verbinden. Behalte auch nur die jeweils billigsten Kanten zwischen verschiedenen
Bäumen.
Das Aufräumen kann in O(n+m) Zeit erfolgen: Sortiere die Kanten lexikographisch
nach den Nummern ihrer Endpunkte mittels zweier Durchgänge von Counting-Sort
(siehe z.B. [2, Kapitel 9]). Danach laufe die sortierte Liste einmal von vorne nach
hinten durch.
3. Nach dem Aufräumen erstellt man für jeden alten Baum T eine Liste mit den Kanten,
die einen Endpunkt in T haben.
4. Jeder alte Baum T erhält den Schlüssel d[T ] := +∞. Seine Markierung wird gelöscht.
4.6 Ein Minimalbaum-Algorithmus mit nahezu linearer Laufzeit
Wachsen eines neuen Baumes
1. Wähle irgendeinen unmarkierten alten Baum T0 und füge ihn in die Prioritätsschlange Q mit Schlüssel d[T0 ] = −∞ ein.
2. Wiederhole die folgenden Schritte, bis Q leer ist oder |Q| > 22m/t , wobei t die
Anzahl der alten Bäume zu Beginn der Phase ist.
(a) Lösche einen alten Baum T mit minimalem Schlüssel aus Q und setze d[T ] :=
−∞.
(b) Wenn T 6= T0 , dann füge e[T ] zum Wald hinzu (e[T ] verbindet den alten
Baum T mit dem aktuellen Baum, der T0 enthält).
(c) Wenn T markiert ist, dann stoppe und beende den Wachstumsschritt wie unten
geschildert.
(d) Sonst, markiere T . Für jede Kante (u, v) mit u ∈ T und c(u, v) < d[tree[v]]
setze e[tree[v]] := (u, v). Wenn d[tree[v]] = +∞, dann füge tree[v] in Q mit
Schlüssel c(u, v) ein. Ansonsten erniedrige den Schlüsselwert von T in Q auf
c(u, v).
3. Zum Beenden des Wachstumsschrittes leere Q und setze d[T ] := +∞ für jeden alte
Baum T mit endlichem Schlüsselwert (diese sind die Bäume, die während der Phase
in Q eingefügt wurden).
Wir analysieren jetzt die Laufzeit des MST-Algorithmus. Die Zeit für Aufräumen und Initialisieren ist O(m). Sei t die Anzahl der alten Bäume, dann ist die Zeit für den Wachstumsschritt
O(t log 22m/t + m) = O(m),
(4.4)
denn wir benötigen höchstens t E XTRACT-M IN Operationen auf einem Heap der Größe
höchstens 22m/t und O(m) andere Heap-Operationen, von denen jede nur O(1) amortisierte Zeit benötigt. Insgesamt sehen wir, daß eine Phase O(m) Zeit benötigt.
Es bleibt die Frage, wie viele Phasen notwendig sind. Seien zu Beginn eine Phase t alte
Bäume und m0 ≤ m Kanten vorhanden (einige Kanten sind möglicherweise gelöscht worden). Nach der Phase besitzt jeder Baum T , der übrigbleibt, mehr als 2 2m/t Kanten, die
mindestens einen Endpunkt in T haben (Wenn T0 der erste Baum war, aus dem T entstanden ist, dann wuchs T0 , bis daß der Heap die Größe 22m/t überschritt. Zu diesem Zeitpunkt
besaß der aktuelle Baum T 0 mehr als 22m/t inzidente Kanten. Nachher sind möglicherweise noch weitere Bäume mit T 0 verbunden worden, was zur Folge hatte, daß jetzt von diesen
inzidenten Kanten einige beide Endpunkte im Endbaum T besitzen).
Da jede der m0 Kanten nur zwei Endpunkte besitzt, erfüllt die Anzahl t0 der Bäume nach
Ende der Phase
2m0
t0 ≤ 2m/t .
2
Die Schranke für die Heap-Größe in der nächsten Phase ist dann
0
22m/t ≥ 22
2m/t
.
Da die Startschranke für die Heap-Größe 2m/n ist und eine Heap-Größe von n nur in der
letzten Phase möglich ist, haben wir höchstens
min{ i : log(i) n ≤ m/n } + 1 = β(m, n) + O(1)
Durchläufe. Wir hatten bereits oben festgestellt hatten, daß pro Phase nur O(m) Zeit benötigt wird. Daher ist die Gesamtkomplexität des Algorithmus in O(mβ(m, n)).
81
82
Datenstrukturen für disjunkte
Mengen
Eine Datenstruktur für disjunkte Mengen verwaltet eine Kollektion {S 1 , . . . , Sk } von disjunkten Mengen, welche sich dynamisch ändern. Jede Menge wird mit einem seiner Elemente, dem Repräsentanten der Menge, identifiziert. Folgende Operationen werden unterstüzt:
M AKE -S ET (x) Erstellt eine neue Menge, deren einziges Element und damit Repräsentant x ist.
U NION(x, y) Vereinigt die beiden Mengen, welche x und y enthalten, und erstellt eine
neue Menge, deren Repräsentant irgend ein Element aus der Vereinigungsmenge ist.
Es wird vorausgesetzt, daß die beiden Mengen disjunkt sind. Die Ausgangsmengen
werden bei dieser Operation zerstört.
F IND -S ET (x) Liefert (einen Zeiger auf) den Repräsentanten der Menge, welche x enthält.
Datenstrukturen für disjunkte Mengen kommen immer dann ins Spiel, wenn man effizient
dynamische Partitionen verwalten möchte. Wir haben bereits in Abschnitt 2.4 eine Anwendung, die Implementierung des MST-Algorithmus von Boruvka, kennengelernt. In diesem
Kapitel betrachten wir einen weiteren MST-Algorithmus, in dem eine Datenstruktur für
disjunkte Mengen extrem hilfreich ist.
5.1 Der Algorithmus von Kruskal
Wir haben in Abschnitt 4.1 bereits den Algorithmus von Prim kennengelernt, der mit Hilfe
der Fibonacci-Heaps aus Kapitel 4 in O(m + n log n) Zeit läuft. Abschnitt 4.6 konstruierte
einen Algorithmus mit Laufzeit O(n + mβ(m, n)).
Ein klassischer MST-Algorithmus ist der Algorithmus von Kruskal (Algorithmus 5.1), der
folgende einfache Strategie benutzt. Man startet mit einer leeren Kantenmenge T . Dann
fügt man iterativ die leichteste Kante zu T hinzu, die keinen Kreis induziert. Abbildung 5.1
zeigt ein Beispiel für die Ausführung des Algorithmus.
Die Korrektheit des Algorithmus von Kruskal läßt sich wie folgt mit Hilfe von Satz 2.10
beweisen.
Satz 5.1 Der Algorithmus von Kruskal findet einen minimalen aufspannenden Baum.
84
Datenstrukturen für disjunkte Mengen
Algorithmus 5.1 Algorithmus von Kruskal zur Konstruktion eines MST.
MST-K RUSKAL(G, c)
Input:
Ein zusammenhängender ungerichteter Graph G = (V, E) mit n := |V | und
m := |E|, eine Kantenbewertungsfunktion c : E → R.
Output: Ein minimaler aufspannender Baum T .
1 Sortiere die Kanten nach ihrem Gewicht: c(e1 ) ≤ · · · ≤ c(em ).
2 T ←∅
3 for i ← 1, . . . , m do
4
if (V, T ∪ {ei }) ist zyklenfrei then
5
T ← T ∪ {ei }
6
end if
7 end for
8 return T
Beweis: Wir zeigen durch Induktion nach der Kardinalität der Kantenmenge T des
Kruskal-Algorithmus, daß T Teilmenge eines minimalen aufspannenden Baumes ist. Dies
ist für T = ∅ trivial.
Wird in Schritt 4 eine Kante ei = (u, v) zu T hinzugefügt, so ist nach Konstruktion T ∪{ei }
zyklenfrei. Sei A die Zusammenhangskomponente von (V, T ), in der u liegt und B :=
V \ A. Dann enthält T keine Kante aus [A, B]: eine solche Kante (a, b) mit a ∈ A, b ∈ B
würde implizieren, daß auch b in der Komponente A liegt. Wir behaupten nun, daß e i eine
leichteste Kante aus [A, B] ist. Mit Hilfe von Satz 2.10 folgt dann, daß auch T ∪ {e i }
Teilmenge eines minimalen aufspannenden Baumes ist.
Um die Behauptung zu zeigen, genügt es zu beweisen, daß die Kanten e 1 , . . . , ei−1 nicht
im Schnitt [A, B] liegen. Wäre eine solche Kante ej = (a, b) im Schnitt, so folgt wegen
ej ∈
/ T , daß beim Testen von ej in Schritt 4 ej einen Zykel induziert hat. Dann gab es aber
vor der Hinzunahme von ej bereits eine Kante in T , die in [A, B] liegt. Dies widerspricht
der weiter oben gezeigten Tatsache, daß T keine Kante aus [A, B] enthält, wenn später e i
getestet wird.
Wir haben somit gezeigt, daß die Kantenmenge T , die der Kruskal-Algorithmus bei Abbruch liefert, Teilmenge eines minimalen aufspannenden Baumes ist. Die Menge (V, T ) ist
zyklenfrei und offenbar maximal mit dieser Eigenschaft (sonst hätten wir noch eine Kante
hinzufügen können). Daher ist (V, T ) ein auch ein aufspannender Baum.
2
Wir zeigen nun, wie man den Algorithmus von Kruskal in mit Hilfe einer Datenstruktur für disjunkte Mengen implementieren kann. Die Idee ist die folgende: Eine Kante
ei = (u, v) induziert genau dann einen Kreis im (kreisfreien) Graphen (V, E), wenn u
und v in der gleichen Zusammenhangskomponente vn (V, E) liegen. Da die Zusammenhangskomponenten eine Partition von V bilden, können wir diese mit einer Datenstruktur
für disjunkte Mengen verwalten. Dann reduziert sich der Zyklentest darauf zu prüfen, ob
F IND -S ET(u) = F IND -S ET(v) gilt. Wenn ei = (u, v) keinen Kreis induziert und zu E hinzugefügt wird, so verschmelzen die beiden disjunkten Zusammenhangskomponenten von u
und v zu einer gemeinsamen Komponente. Dies können wir mit Hilfe von U NION(x, y)
behandeln. Algorithmus 5.2 zeigt die Implementierung des Algorithmus von Kruskal mit
Hilfe einer Datenstruktur für disjunkte Mengen.
Im folgenden Satz fassen wir noch einmal die Korrektheit und die Laufzeit des Algorithmus
von Kruskal zusammen.
Satz 5.2 Der Algorithmus von Kruskal findet einen MST. Er benötigt neben dem Sortieren der Kanten gemäß ihrer Gewichte in einem Graphen mit n Knoten und m Kanten
O(n) M AKE -S ET-, O(m) F IND -S ET und O(n) U NION-Operationen.
5.1 Der Algorithmus von Kruskal
1
1
4
2
3
1
3
2
2
4
7
3
1
1
3
5
5
6
2
5
8
2
1
4
2
3
4
9
5
2
7
5
5
9
2
4
2
3
6
3
1
2
4
2
5
3
2
8
1
1
1
6
2
2
7
5
(b) In den nächsten Schritten werden die
Kanten (3, 6), (2, 5), (4, 5), (5, 7), (6, 9)
und (8, 9) in dieser Reihenfolge zum Baum
hinzugefügt, da keine von ihnen einen Zykel (durch graue Kantenhinterlegung hervorgehoben) induziert. Danach wird die Kante
(1, 4) geprüft. Diese induziert aber einen Zykel, weshalb die Kante (1, 4) verworfen wird.
3
2
4
1
5
3
2
4
3
2
4
(a) Zunächst wird die Kante (1, 2) untersucht
und zum Baum hinzugefügt.
1
4
2
2
2
4
85
9
8
2
(c) Die Kante (7, 8) kann wiederum zum
Baum hinzugefügt werden, ohne einen Zykel
zu verursachen.
5
2
4
5
6
2
5
7
9
8
3
2
(d) Nach dem Hinzufügen der Kante (7, 8)
induzieren alle weiteren Kanten Zykel, so daß
diese verworfen werden.
Abbildung 5.1: Berechnung eines minimalen aufspannenden Baumes mit dem KruskalAlgorithmus. Die dick gezeichneten Kanten gehören zur Menge T , die bei Ende des Algorithmus einen minimialen aufspannenden Baum bildet. Die gepunkteten Kanten sind
Kanten, die durch Zyklentests verworfen wurden. Die gestrichelte Kante ist die aktuell
untersuchte Kante.
86
Datenstrukturen für disjunkte Mengen
Algorithmus 5.2 Implementierung des Algorithmus von Kruskal mit Hilfe einer Datenstruktur für disjunkte Mengen.
Ein zusammenhängender ungerichteter Graph G = (V, E) mit n := |V | und
m := |E|, eine Kantenbewertungsfunktion c : E → R.
Output: Ein minimaler aufspannender Baum T .
1 Sortiere die Kanten nach ihrem Gewicht: c(e1 ) ≤ · · · ≤ c(em ).
2 T ←∅
3 for all v ∈ V do
4
M AKE -S ET(v)
5 end for
6 for i ← 1, . . . , m do
7
if F IND -S ET(u) 6= F IND -S ET(v) then
8
T ← T ∪ {(u, v)}
9
U NION (u, v)
10
end if
11 end for
12 return T
Input:
Beweis: Die Korrektheit haben wir bereits weiter oben in Satz 5.1 bewiesen. Offenbar
gibt es genau n M AKE -S ET-Operationen für die n Knoten des Graphen. Für jede Kante
werden zwei (vier, wenn jede Kante zweimal abgespeichert ist) F IND -S ET-Operationen
benötigt. Jede U NION-Operation verringert die Anzahl der Komponenten um eins, so daß
bei anfänglich n Komponenten nur n − 1 U NION-Operationen möglich sind.
2
Der Schlüssel für eine schnelle Laufzeit des Kruskal-Algorithmus ist eine effiziente Datenstruktur für disjunkte Mengen. Die Operationen für die Datenstruktur sind insbesondere
dann der Flaschenhals, wenn die Kanten bereits sortiert vorliegen bzw. ein Sortieren in
O(m) Zeit möglich ist, etwa, wenn alle Kantengewichte ganze Zahlen aus dem Bereich
1, . . . , m sind.
5.2 Eine einfache Datenstruktur
Eine einfache Möglichkeit, die Mengen in einer Partition zu verwalten, ist es, lineare Listen zu verwenden: Wir halten jede Menge als lineare Liste, bei der der Kopf der Liste
den Repräsentanten darstellt. Jedes Element in der Liste besitzt einen Zeiger auf den Kopf
der Liste. Zusätzlich halten wir uns noch Zeiger auf den Kopf und das Ende jeder Liste.
Abbildung 5.2 veranschaulicht die Listen-Implementierung.
head
c
a
b
tail
Abbildung 5.2: Datenstruktur für disjunkte Mengen auf Basis von linearen Listen. Im Bild
wird die Menge S = {a, b, c} dargestellt, ihr Repräsentant ist das Element c.
Das Erstellen einer neuen Menge mittels M AKE -S ET kann in der Listenrepräsentation in
O(1) Zeit implementiert werden: wir müssen lediglich ein Listenelement und Speicher
für head und tail alloziieren und vier Zeiger initialisieren. F IND -S ET ist ebenfalls in
5.2 Eine einfache Datenstruktur
87
konstanter Zeit möglich: für ein Element x haben wir einen Zeiger auf seinen Listenkopf,
so daß wir nur diesem Zeiger folgen müssen.
Etwas aufwendiger ist das Vereinigen zweier Mengen U NION (x, y). Sei L x die Liste für
die Menge, welche x enthält und Ly die entsprechende Liste für y (siehe Abbildung 5.3 (a)
und (b)). Wir müssen eine Liste an die andere Liste anhängen. Angenommen, wir hängen
Ly and Ly an. Da wir einen Zeiger auf das Ende von Lx halten, können wir die Listen
zunächst einmal in konstanter Zeit verketten, wobei wir gleich den Zeiger auf das Ende der
neuen Liste setzen (siehe Abbildung 5.3 (c)).
head
b
x
a
tail
(a) Die Menge Sx vor der Vereinigung
head
y
c
tail
(b) Die Menge Sy vor der Vereinigung
head
b
x
a
y
c
tail
(c) Anhängen der Liste von Sy an Sx
head
b
x
a
y
c
tail
(d) Endgültiges Ergebnis der Vereinigung
Abbildung 5.3: Vereinigen der beiden Mengen Sx = {x, a, b} und Sy = {y, c}
Bisher haben wir nur O(1) Zeit benötigt. Jetzt müssen wir aber noch für alle Elemente in
der Liste Ly den Zeiger an den Kopf der Liste neu setzen, so daß wir das Ergebnis aus
Abbildung 5.3 (d) erhalten. Das kostet uns Θ(ny ) Zeit, wobei ny die Anzahl der Elemente
in der Liste Ly bezeichnet.
Wir betrachten jetzt eine beliebige Folge von m Operationen M AKE -S ET, F IND -S ET und
U NION, von denen n M AKE -S ET-Operationen sind. Nach den obigen Überlegungen liefert
88
Datenstrukturen für disjunkte Mengen
die Implementierung mit Hilfe von linearen Listen eine Laufzeit von
n · O(1) + (m − n) · Θ(n)
|
{z
}
| {z }
für n M AKE -S ET
∈ O(mn),
n − m andere Operationen
wobei wir die Kosten für jedes U NION mit Θ(n) abgeschätzt haben, da keine Menge mehr
als n Elemente enthalten kann.
Wenn wir etwas genauer sind, können wir diese Schranke verbessern. Wenn wir die Liste Ly an die Liste Lx anhängen, entstehen und Kosten ny . Wahlweise könnten wir auch
umgekehrt Lx an Ly anhängen. Auf jeden Fall ist es günstiger, die kleinere Liste hinten
anzuhängen. Dazu müssen wir für jede Menge (Liste) noch zusätzlich ihre Größe abspeichern. Dann können wir bei U NION in O(1) Zeit die kleinere Liste erkennen und diese
hinten an die größere anhängen. Die Größe der Vereinigung ist natürlich in konstanter Zeit
aktualisierbar. Wir nennen diese Zusatzregel für die Vereinigung die Größenregel.
Der folgende Satz zeigt, daß die Größenregel die Laufzeit deutlich verringert, nämlich von
O(mn) auf O(m + n log n).
Satz 5.3 Eine Folge von m Operationen M AKE -S ET, F IND -S ET und U NION, von denen
n Operationen M AKE -S ET sind, benötigt in der Listenimplementierung mit der Größenregel benötigt O(m + n log n) Zeit.
Bevor wir den Satz beweisen, zeigen wir kurz, was er für den Kruskal-Algorithmus aus
Abschnitt 5.1 bedeutet. Zusammen mit Satz 5.2 ergibt sich eine Laufzeit von O((m +
n) + n log n) = O(m + n log n) plus O(m log m) für das Sortieren der Kanten. Falls die
Kanten bereits sortiert sind oder wir sie in O(m) Zeit sortieren können, so ist damit der
sehr einfache Algorithmus von Kruskal genauso schnell wie der Algorithmus von Prim mit
Fibonacci-Heaps. Wir werden dieses Ergebnis nachher noch für dünne Graphen mit Hilfe
einer neuen Datenstruktur für disjunkte Mengen verbessern.
Beobachtung 5.4 Wird der Kruskal-Algorithmus unter Zuhilfenahme der Listenimplementierung mit Größenregel implementiert, so benötigt er O(m + n log n) Zeit plus die
Zeit zum Sortieren der Kantengewichte.
Beweis: (Satz 5.3) Offenbar sind die U NION-Operationen bei der Listenimplementierung
der Punkt, auf den wir bei der Analyse besonders achten müssen, da m F IND-Operationen
sowieso nur insgesamt O(m) Zeit benötigen.
Den Gesamtaufwand für die U NION-Operationen können wir abschätzen, indem wir folgende Beobachtung benutzen: der Aufwand für alle U NION-Operationen beträgt (bis auf
einen konstanten Faktor) der Anzahl der Veränderungen für die Zeiger auf die Repräsentanten in den Listenelementen. Wenn wir somit zeigen können, daß sich für jedes Element
der Zeiger auf den Listenkopf (d.h. auf den Repräsentanten) im Verlauf der Operationenfolge nur O(log n) mal ändert, so ergibt dies die passende obere Schranke von O(n log n)
für die Kosten aller U NION-Operationen.
Sei dazu x ein Element. Wir zeigen durch Induktion, daß nach i Zeigeränderungen auf
den Listenkopf im Listenelement von x die Menge, in der x enthalten ist mindestens 2 i−1
Elemente enthält. Da keine Menge größer als n werden kann, ergibt dies die gewünschte
Anzahl von O(log n) Zeigeränderungen. Man beachte, daß sich die Größe einer Menge im
Verlauf der Operationenfolge niemals verkleinern kann.
Die Behauptung ist offenbar richtig für i = 1: die erste Zeigeränderung erfolgt durch
ein M AKE -S ET(x) (sonst wäre x gar nicht an unserer Operationenfolge beteiligt) und die
x enthaltende Menge hat 1 = 20 = 21−1 Elemente.
5.3 Baumrepräsentation mit Pfadkompression und Vereinigung nach Rang
89
Wir betrachten nun die ite Zeigeränderung, die durch ein U NION erfolgen muß. Sei S x die
Menge, welche x vor der Vereinigung enthält und Sy die andere Menge. Nach Induktionsvoraussetzung gilt |Sx | ≥ 2i−2 . Da sich der Zeiger von x ändert, muß Sx an Sy angehängt
werden. Nach der der Größenregel enthält dann Sy mindestens so viele Elemente wie Sx .
Da Sx und Sy disjunkt sind, gilt daher dann für die Vereinigungsmenge
|Sx ∪ Sy | = |Sx | + |Sy | ≥ |Sx | + |Sx | = 2 · |Sx | ≥ 2 · 2i−2 = 2i−1 .
Dies beendet den Beweis.
2
5.3 Baumrepräsentation mit Pfadkompression und Vereinigung nach Rang
In diesem Abschnitt stellen wir eine deutlich trickreichere Datenstruktur für disjunkte Mengen vor, die für fast alle Fälle eine deutlich bessere Laufzeit als die Listenimplementierung
ermöglicht.
Wir repräsentieren jede Menge in unserer Partition als Wurzelbaum, wobei die Wurzel des
Baums das Element enthält, welches die Menge repräsentiert. Jeder Knoten im Baum hat
einen Zeiger auf seinen Vater im Baum. Abbildung 5.4 veranschaulicht die neue Datenstruktur.
a
b
a
b
b
d
c
(a) Darstellung der Menge {a, b, c, d} mit a
als Repräsentanten
d
(b) Alternative
ge {a, b, c, d}
Darstellung
der
Men-
Abbildung 5.4: Datenstruktur für disjunkte Mengen auf Basis von Bäumen.
Eine M AKE -S ET-Operation erstellt in O(1) Zeit einen Baum mit einem Knoten (siehe
Algorithmus 5.3). Für einen Knoten x ist p[x] ein Zeiger auf den Vaterknoten, wobei wir
wie bei der Listenimplementierung zweckmäßigerweise für die Wurzel p[x] = x setzen.
Im Pseudocode für M AKE -S ET taucht auch die zusätzliche Information rank[x] auf. Diese
wird nachher noch von entscheidender Bedeutung sein. Für einen Knoten x ist rank[x]
eine obere Schranke für den längsten Weg von x zu einem Blatt im Teilbaum mit Wurzel x.
Bei F IND -S ET(x) können wir ausgehend von x entlang der Vaterzeiger bis zur Wurzel des
Baums gehen, der x enthält. Ist x in diesem Baum auf Tiefe h, so benötigen wir hierfür
Θ(h) Zeit. Daher wird es unser Anliegen sein, die Bäume möglichst »flach« zu halten: Ein
Baum wie in Abbildung 5.4 (a) ist sicherlich dem Baum in Abbildung 5.4 (b) vorzuziehen.
Eine Möglichkeit, die Höhe der Bäume zu verkürzen, ist die sogenannte Pfadkompression:
nachdem wir von x ausgehend den Weg zur Wurzel hinaufgestiegen sind, hängen wir jeden
90
Datenstrukturen für disjunkte Mengen
Algorithmus 5.3 Implementierung der M AKE -S ET-Operation in der Datenstruktur auf Basis von Bäumen.
M AKE -S ET(x)
1 p[x] ← x
2 rank[x] ← 0
Algorithmus 5.4 Implementierung der F IND -S ET-Operation mit Pfadkompression.
F IND -S ET(x)
1 if p[x] 6= x then
2
p[x] ← F IND -S ET(p[x])
3 end if
4 return p[x]
Knoten auf diesem Weg mitsamt seinem Teilbaum anschließend direkt als Sohn an die
Wurzel. Wie in Algorithmus 5.4 zu sehen, können wir diese Operation ausführen, ohne die
Laufzeit für ein F IND -S ET asymptotisch zu erhöhen: die Laufzeit erhöht sich nur um einen
konstanten Faktor. Abbildung 5.5 veranschaulicht das Vorgehen.
a
b
c
d
a
e
x
e
d
c
x
(a) Ausgangsbaum bei F IND -S ET(x)
(b) Resultat nach F IND -S ET(x)
Abbildung 5.5: Pfadkompression bei der F IND -S ET-Operation.
Zum Vereinigen zweier Mengen/Bäume S1 und S2 können wir die Wurzel von S1 zu einem
Sohn von S2 machen. Damit benötigen wir für U NION(x, y) außer der Zeit für die zwei
F IND -S ET(x) und F IND -S ET(y) nur O(1) Zeit. Man beachte, daß es uns prinzipiell frei
steht, welchen der beiden Wurzelknoten von S1 und S2 wir zur Wurzel der Vereinigungsmenge machen. Motiviert von der (erfolgreichen) Größenregel in der Listenimplementierung möchten wir gerne den kleineren Baum, bzw. den flacheren Baum, an den größeren
anhängen (siehe Abbildung 5.6).
b
5.3 Baumrepräsentation mit Pfadkompression und Vereinigung nach Rang
rank[a] rank[a]
a
rank[a] rank[a]
a
rank[b] + 1
b
b
rank[b]
b
b
a
a
(a) rank(a) < rank(b)
(b) rank(a) = rank(b)
Abbildung 5.6: Vereinigung nach Rang.
Hier kommen jetzt die Ränge rank[x] der Knoten ins Spiel. Wie bereits erwähnt, liefert
rank[x] eine untere Schranke für die Höhe des Baumes mit Wurzel x. Beim Vereinigen wird der Wurzelknoten mit dem größeren Rang zur Wurzel der Vereinigungsmenge.
Gleichzeitig aktualisieren wir die Ränge nach folgender Regel: werden zwei Bäume mit
gleichem Rang vereinigt, so erhöht sich der Rang der neuen Wurzel um eins (siehe Abbildung 5.6). Algorithmus 5.5 zeigt den Pseudocode für die Vereinigung nach Rang.
Algorithmus 5.5 Implementierung der U NION-Operation bei der Vereinigung nach Rang.
U NION(x, y)
1 rx ← F IND -S ET (x)
2 ry ← F IND -S ET (y)
3 L INK (rx , ry )
L INK(rx , ry )
1 if rank[rx ] < rank[ry ] then
2
p[rx ] ← ry
3 else
4
p[ry ] ← rx
5
if rank[rx ] = rank[ry ] then
6
rank[rx ] ← rank[rx ] + 1
7
end if
8 end if
Bevor wir mit Hilfe der amortisierten Analyse aus Kapitel 3 eine fast lineare Schranke für
m Operationen auf Basis der neuen Datenstruktur herleiten, beweisen wir eine schwächere
Schranke. Dabei zeigen wir auch viele Eigenschaften, die uns nachher bei der komplizierten Analyse hilfreich sein werden. Wir betrachten im Folgenden immer Operationenfolgen,
in denen n Operationen M AKE -S ET sind. Die Gesamtzahl der Operationen in der Folge bezeichnen wir wie bisher mit m ≥ n.
Lemma 5.5 Die Ränge, welche die Datenstruktur verwaltet, besitzen im Verlauf einer
Operationenfolge folgende Eigenschaften:
(i) Der Rang eines Knotens x ist anfangs null und wächst schwach monoton über die
Zeit. Sobald im Verlauf der Operationenfolge einmal p[x] 6= x gilt, bleibt der Rang
91
92
Datenstrukturen für disjunkte Mengen
von x konstant.
(ii) Für alle Knoten x gilt rank[x] ≤ rank[p[x]], mit strikter Ungleichheit, falls p[x] 6=
x.
(iii) Wenn man den Pfad von einem beliebigen Knoten zu seinem Wurzelknoten hinaufsteigt, so wachsen auf diesem Pfad die Ränge strikt monoton an.
Beweis: Wir beweisen die Aussagen durch Induktion nach der Anzahl k der Operationen.
Offenbar sind alle Aussagen für k = 0 oder k = 1 richtig. Wir betrachten nun die k te
Operation:
(i) Falls p[x] 6= x gilt, so kann x niemals mehr Wurzel eines Baums werden. Da sich
höchsten die Ränge von Wurzeln erhöhen, folgt die Aussage.
(ii) Wenn vor der kten Operation bereits p[x] 6= x galt, so folgt die Aussage aus (i), da
sich der Rang von x nicht mehr erhöhen kann. Falls nach der kten Operation immer
noch x = p[x] gilt, so ist ebenfalls nichts mehr zu zeigen. Es bleibt der Fall, daß
vor der kten Operation p[x] = x und nachher p[x] 6= x. Dann wird aber in der kten
Operation x Sohn eines Knotens, der entweder bereits vorher zumindest aber nachher
größeren Rang besitzt.
(iii) Da sich Ränge nur in den Wurzelknoten erhöhen können, folgt die Behauptung sofort
aus (i) und (ii).
2
Lemma 5.6 Ist rank[x] = k , so besitzt der (Teil-) Baum mit Wurzel x mindestens 2 k
Knoten.
Beweis: Wieder erfolgt der Beweis durch Induktion nach der Anzahl der Operationen.
Die Aussage ist trivial, wenn nur maximal eine Operation erfolgt. Wenn sich in der kten
Operation der Rang von x auf r erhöht (nur dann ist noch etwas zu zeigen), so wurde x
an den Baum mit Wurzel x ein anderer Baum mit Wurzel y und rank[y] = rank[x] =
r − 1 angehängt. Nach Induktionsvoraussetzung hat der Baum mit Wurzel y mindestens
2r−1 Knoten. Ebenfalls nach Induktionsvoraussetzung hat der alte an x wurzelnde Baum
mindestens 2r−1 Knoten, so daß nun im neuen Baum mit Wurzel x mindestens 2 r−1 +
2r−1 = 2r Knoten sind.
2
Korollar 5.7 Der Rang jedes Knoten ist maximal blog2 nc.
Beweis: Ist rank[x] = k, so hat der Teilbaum mit Wurzel x nach Lemma 5.6 mindestens
2k Knoten. Da es aber nur n Elemente überhaupt gibt, folgt 2k ≤ n, also k ≤ blog2 nc. 2
Korollar 5.8 In einer beliebigen Operationenfolge mit m Operationen, von denen
n M AKE -S ET-Operationen sind, hat jeder auftretende Baum maximal Höhe blog 2 nc + 1.
Beweis: Nach Korollar 5.7 hat jeder Knoten maximal Rang blog 2 nc. Da nach Lemma 5.5 (iii) die Ränge auf dem Weg von einem Knoten zum Vaterknoten strikt monoton
fallen und kein Rang negativ ist, kann ein Baum daher maximal Höhe blog 2 nc + 1 haben.
2
Korollar 5.8 impliziert, daß jede F IND -S ET-Operation O(log n) Zeit benötigt. Somit benötigt auch unsere Implementierung von U NION in Algorithmus 5.5 nur O(log n) Zeit.
Wenn wir also wiederum den gesamten Zeitaufwand für eine Folge von m Operationen,
von denen n M AKE -S ET sind, betrachten, so haben wir damit bereits gezeigt, daß in der
baumbasierten Implementierung dafür nur Kosten O(n + m log n) anfallen. Dieses Ergebnis benutzt übrigens nicht, daß wir Pfadkompression benutzen. Es beruht allein auf der
Tatsache, daß die Vereinigung nach Rang erfolgt.
5.4 Analyse von Pfadkompression und Vereinigung nach Rang
93
5.4 Analyse von Pfadkompression und Vereinigung nach
Rang
In diesem Abschnitt beweisen wir eine verbesserte (scharfe) obere Schranke für den Zeitaufwand, der bei der neuen Datenstruktur mit Pfadkompression und Vereingung nach Rang
anfällt. Dazu benutzen wir die amortisierte Analyse aus Kapitel 3 mit einem geschickten
Potentialfunktionsargument.
5.4.1 Eine explosiv wachsende Funktion
Zunächst führen wir aber die Funktion A und ihre funktionale Umkehrfunktion α ein, mit
deren Hilfe wir die Laufzeit ausdrücken werden. Für ganze Zahlen k ≥ 0 und j ≥ 1
definieren wir die Funktion Ak (j) durch:
A0 (j) := j + 1
(j+1)
für k ≥ 1.
Ak (j) := Ak−1 (j)
(j+1)
Hierbei bezeichnet Ak−1 die (j + 1)-fache Iteration der Funktion Ak−1 . Die ite Iteration
einer Funktion f ist dabei definiert als:
f (0) (x) := x
f (i) (x) := f (f (i−1) (x))
für i ≥ 1.
Mit anderen Worten:
f (i) (x) = f (f (. . . f (x) . . . ).
| {z }
i mal
Wir nennen den Index k in Ak die Stufe der Funktion A. Zu A definieren wir die inverse
Funktion α wie folgt:
α(n) := min{ k : Ak (1) ≥ n },
(5.1)
d.h., α(n) ist die kleinste Stufe k, in der Ak (1) mindestens n ist.
Wir zeigen zunächst einmal, wie explosiv die Funktion A wirklich wächst (im Umkehrschluß heißt das, daß α extrem langsam wächst). Dazu betrachten wir die ersten Stufen
von A. Wir wissen bereits aus der Definition, daß
A0 (j) = j + 1
(5.2)
ist, also A in der nullten Stufe linear wächst. Für die erste Stufe gilt dann
(j+1)
A1 (j) = A0
(j) = 2j + 1.
(5.3)
Das sieht immer noch recht »gemütlich« aus, die Explosion geht in der nächsten Stufe aber
(j+1)
los. In der zweiten Stufe haben wir A2 (j) = A1
(j). Wir müssen die (j + 1)fache
Interation der ersten Stufe aus (5.3) berechnen. Eine einfache Induktion nach i zeigt, daß
(i)
A1 (j) = 2i+1 (j + 1) − 1. Das ergibt:
A2 (j) = 2j+1 (j + 1) − 1.
(j+1)
Die dritte Stufe ist schon beeindruckend: Da A3 (j) = A2
··
(5.4)
(j), folgt aus (5.4), daß
·2
2
A3 (j) > 2
| {z },
(5.5)
j+1
wobei hier ein »Turm« aus j+1 Zweien gebaut wird. Bevor wir uns der Inversen Funktion α
widmen, beobachten wir zunächst, daß Ak (j) sowohl im Argument j als auch in der Stufe k
streng monoton wachsend ist.
94
Datenstrukturen für disjunkte Mengen
Lemma 5.9 Der Ausdruck Ak (j) ist streng monoton wachsend in j und k .
Beweis: Die Behauptung folgt durch Induktion nach k: Für k = 0 ist die Aussage aus der
Definition A0 (j) = j + 1 klar. Ansonsten haben wir, falls die Aussage für 0, . . . , k − 1
bereits bewiesen ist:
IV
(j+1)
(j+1)
Ak (j) = Ak−1 (j) ≥ Ak−2 (j) = Ak−1 (j)
und
(j+1)
IV
(j+1)
Ak (j) = Ak−1 (j) ≥ Ak−1 (j − 1) = Ak (j − 1).
Dies zeigt die gewünschte Monotonie.
2
Aus (5.2), (5.3), (5.4) und (5.5) folgt:
A0 (1) = 2
A1 (1) = 3
A2 (1) = 7
(2)
A3 (1) = A2 (1) = A2 (A2 (1)) = A2 (7) = 27+1 (7 + 1) − 1 = 28 · 8 − 1 = 21 1 − 1 = 2047
(2)
Wir rechnen noch A4 (1) aus. Da nach Definition A4 (1) = A3 (1) = A3 (A3 (1)) =
A3 (2047) gilt, entspricht der Wert A4 (1) mindestens dem Wert eines Turms aus 2048 Zweien (vgl. (5.5)), einem riesigen Wert. Wir schätzen diesen Wert extrem grob ab:
A4 (1) = A3 (2047)
A2 (2047)
=2
2048
=2
2054
2
2053
(da A monoton in jedem Argument ist, siehe Lemma 5.9)
· 2048 − 1
−1
= 10log10 (2)·2053
> 10684 .
Man schätzt, daß die Zahl der Atome im Universum etwa 1080 beträgt. Damit ist A4 (1)
bereits mit unserer Abschätzung (wir haben A3 (2047), d.h., den Turm aus 2048 Zweien
extrem grob nach unten durch A2 (2047) abgeschätzt) unvorstellbar groß.
Für die inverse Funktion α ergeben unsere Betrachtungen:

0





1






2
α(n) = 3


4





5




..
.
für 0 ≤ n ≤ 2
für n = 3
für 4 ≤ n ≤ 7
für 8 ≤ n ≤ 2047
für 2048 ≤ n ≤ A4 (1) ( 10684 )
für A4 (1) + 1 ≤ n ≤ A5 (1)
..
.
Bereits A4 (1) ist derart groß, daß man für »alle praktischen Fälle« α(n) ≤ 4 annehmen
kann.
5.4 Analyse von Pfadkompression und Vereinigung nach Rang
95
5.4.2 Amortisierte Analyse mit Potentialfunktionsargument
Für die amortisierte Analyse ordnen wir jedem Knoten x in unserer Datenstruktur D i nach
der iten Operation
ein Potential φi (x) zu. Das Potential der kompletten Datenstruktur ist
P
Φ(Di ) = x φi (x). Am Anfang setzen wir Φ(D0 ) := 0. Das Potential wird die Eigenschaft haben, daß Φ(Di ) ≥ 0 für alle i gilt, so daß mit unseren Überlegungen aus Kapitel 3
folgt, daß wir die realen Kosten für eine Folge von Operatione nach oben durch die amortisierten Kosten abschätzen können.
Zur Definition von φi (x) treffen wir eine Fallunterscheidung. Ist x eine Wurzel oder ist
rank[x] = 0, so sei φi (x) := α(n) · rank[x]. Für den anderen Fall benötigen wir zwei
weitere Hilfsgrößen. Die Stufe von x ist die größte Stufe k, so daß A k (rank[x]) höchstens
dem Rang von p[x] entspricht:
`(x) := max{ k ∈ N : Ak (rank[x]) ≤ rank[p[x]] }.
(5.6)
Wir haben A0 (rank[x]) = rank[x] + 1 ≤ rank[p[x]], wobei die letzte Ungleichung aus
Lemma 5.5 (ii) folgt. Außerdem gilt (falls rank[x] ≥ 1):
Aα(n) (rank[x]) ≥ Aα(n) (1)
≥n
(nach Lemma 5.9)
(nach Definition von α(n))
> blog2 nc
≥ rank[x]
(nach Korollar 5.7)
Daher ist `(x) wohldefiniert und es gilt die folgende Eigenschaft:
Eigenschaft 5.10 Für die Stufe `(x) eines Knotens x gilt:
0 ≤ `(x) < α(n).
Im Verlauf einer Operationenfolge steigt `(x) schwach monoton.
Die Monotonie von `(x) folgt daraus, daß sich der Rang von x nicht mehr ändert, wenn x
kein Wurzelknoten ist (siehe Lemma 5.5), und aus der Monotonie des Rangs des Vaterknotens.
Als zweite Hilfsgröße definieren wir den Index eines Knotens x durch
(i)
index(x) := max{ i ∈ N : A`(x) (rank[x]) ≤ rank[p[x]] }.
Der Wert index(x) ist die größte Anzahl von Iteration für A`(x) , angewendet auf rank[x],
so daß als Ergebnis höchstens der Rang des Vaterknotens entsteht.
Es gilt nach Definition von `(x):
(1)
A`(x) (rank[x]) = A`(x) (rank[x]) ≤ rank[p[x]],
also ist index(x) ≥ 1. Andererseits haben wir
(rank[x]+1)
A`(x)
(rank[x]) = A`(x)+1 (rank[x])
> rank[p[x]]
(nach Definition von A)
(nach Definition von `(x) und Lemma 5.9).
Daher ist auch index(x) wohldefiniert und es gilt:
Eigenschaft 5.11 Für den Index index(x) eines Knotens x gilt:
1 ≤ index(x) ≤ rank[x].
96
Datenstrukturen für disjunkte Mengen
Damit können wir nun die benötigte Potentialfunktion definieren:
Definition 5.12 Das Potential φi (x) eines Knotens x nach i Operationen ist definiert als:
(
α(n) · rank[x]
, falls x Wurzelknoten ist oder rank[x] = 0
φi (x) :=
(α(n) − `(x)) · rank[x] − index(x) , falls x kein Wurzelknoten ist und rank[x] ≥ 1
Das Potential Φi := Φ(Di ) unserer
P baumartigen Datenstruktur nach der iten Operation ist
definiert als Φ0 := 0 und Φi := x φi (x) für i ≥ 1.
Lemma 5.13 Für jeden Knoten x und für alle i ≥ 0 gilt:
0 ≤ φi (x) ≤ α(n) · rank[x].
Insbesondere ist Φi ≥ 0 für alle i ≥ 0.
Beweis: Die Aussage ist trivial, wenn x ein Wurzelknoten ist oder rank[x] = 0 gilt. Sei
daher rank[x] > 0 und x kein Wurzelknoten. Dann gilt:
(α(n) − `(x)) · rank[x] − index(x) ≥ rank[x] − index(x)
≥ rank[x] − rank[x]
= 0.
(da `(x) < α(n))
(nach Eigenschaft 5.11)
Andererseits haben wir auch
(α(n) − `(x)) · rank[x] − index(x) ≤ (α(n) − `(x)) · rank[x] (nach Eigenschaft 5.11)
≤ α(n) · rank[x]
(nach Eigenschaft 5.10).
2
Wir beginnen nun mit der Analyse der amortisierten Kosten der einzelnen Operationen:
Lemma 5.14 Die amortisierten Kosten jeder M AKE -S ET-Operation sind O(1).
Beweis: Die realen Kosten der M AKE -S ET-Operation sind O(1) (siehe Algorithmus 5.3).
Die Operation generiert einen neuen Wurzelknoten x, der nach Definition unseres Potentials Potentialwert φ(x) = 0 hat. Alle anderen Potentiale bleiben unverändert. Somit ergibt
sich auch insgesamt keine Potentialänderung und die amortisierten Kosten entsprechen den
(konstanten) realen Kosten.
2
Um die Kosten für eine U NION-Operation abzuschätzen, zerlegen wir U NION in die F IND S ET- und die L INK-Operation. Es genügt offenbar zu zeigen, daß jede F IND -S ET- und jede
L INK-Operation amortisierte Kosten O(α(n)) hat, um eine Schranke von O(α(n)) für die
Kosten von U NION zu erhalten.
Lemma 5.15 Die amortisierten Kosten für jede L INK-Operation sind O(α(n)).
Beweis: Sei die ite Operation L INK(rx , ry ), bei der ry zur Wurzel des gemeinsamen Baums
gemacht wird. Die realen Kosten hierfür sind in O(1). Es ändern sich höchstens die folgenden Potentiale:
1. φ(rx ), φ(ry );
5.4 Analyse von Pfadkompression und Vereinigung nach Rang
97
2. die Potentiale der Söhne z von ry , da zwar rank[z] unverändert bleibt, sich aber
möglicherweise rank[p[z]] = rank[ry ] um eins erhöht und sich damit wiederum
`(z) und index(z) ändern.
Für die Potentialänderung in der iten Operation ergibt sich damit:
X
Φi − Φi−1 = φi (rx ) − φi−1 (rx ) + φi (ry ) − φi−1 (ry ) +
z: z Sohn von ry
(φi (z) − φi−1 (z)).
Wir werden zunächst zeigen, daß im Fall 2 das Potential eines solchen Knotens z nicht
erhöht. Wir erinnern uns daran (Eigenschaft 5.10), daß ` monoton steigend war.
Wenn `(z) nun in der iten Operation unverändert bleibt, so kann index(z) nicht kleiner
werden, sondern höchstens wachsen. Damit folgt aus der Definition des Potentials φ i (z) ≤
φi−1 (z).
Sollte `(z) in der iten Operation ansteigen, so steigt `(z) um mindestens eins. Dann fällt
aber der vordere Teil des Potentials, also (α(n) − `(z)) · rank[z] um mindestens rank[z].
Da nach Eigenschaft 5.11 der Index index(z) höchstens gleich rank[z] ist, kann er auch
höchstens um rank[z] − 1 steigen/fallen. Als Resultat folgt, daß auch hier φ i (z) ≤ φ(z)
gilt.
Unser Zwischenresultat zeigt nun, daß für die Potentialänderung gilt:
X
Φi − Φi−1 = φi (rx ) − φi−1 (rx ) + φi (ry ) − φi−1 (ry ) +
(φi (z) − φi−1 (z))
{z
}
|
z: z Sohn von ry
≤0
≤ φi (rx ) − φi−1 (rx ) + φi (ry ) − φi−1 (ry )
Es verbleibt, die Potentialänderung für rx und ry abzuschätzen.
Da rx vor der iten Operation eine Wurzel war, gilt φi−1 (rx ) = α(n) · rank[rx ]. Andererseits gilt:
φi (rx ) = (α(n) − `(rx )) · rank[rx ] − index(rx )
< α(n) · rank[rx ]
= φi−1 (rx ).
(da `(x) ≥ 0 und index(x) ≥ 1)
Also fällt das Potential von rx um mindestens eins.
Der verbleibende Knoten ry ist sowohl vor als auch nach der L INK-Operation eine Wurzel,
d.h., φi−1 (ry ) = α(n) · rank[ry ] und φi (ry ) = α(n) · rank0 [ry ], wobei rank0 [ry ] den
neuen Rang von ry nach dem L INK bezeichnet. Wir haben also
φi (ry ) − φi−1 (ry ) = α(n) · (rank0 [y] − rank[y])
≤ α(n) · (rank[y] + 1 − rank[y])
= α(n).
Hierbei haben wir ausgenutzt, daß sich bei einem L INK der Rang eines Wurzelknotens
maximal um eins erhöhen kann.
Wir haben gezeigt, daß sich das Potential bei einem L INK maximal um α(n) erhöht. Da die
realen Kosten konstant sind, betragen die amortisierten Kosten maximal O(1) + α(n) =
O(α(n)).
2
Nun analysieren wir noch die F IND -S ET-Operation.
Lemma 5.16 Die amortisierten Kosten für jede F IND -S ET-Operation sind O(α(n)).
98
Datenstrukturen für disjunkte Mengen
Beweis: Sei die ite Operation F IND -S ET(x). Wenn sich x auf Höhe h befindet, d.h., wenn
der Pfad von x zur Wurzel rx seines Baumes genau h Elemente besitzt, dann sind die realen
Kosten für F IND -S ET(x) in O(h). Somit ergibt sich für die amortisierten Kosten:
ai = O(h) + Φi − Φi−1 .
(5.7)
Unser Ziel ist es, die Potentialdifferenz Φi − Φi−1 so abzuschätzen, daß wir ai ∈ O(α(n))
erhalten.
Zunächst zeigen wir, daß sich für keinen Knoten sein Potential bei der iten Operation erhöht. Dazu müssen wir folgende Knoten betrachten:
1. Die Wurzel rx des Baums, der x enthält.
Es gilt φi (rx ) = α(n) · rank[rx ] = φi−1 (rx ), da ein F IND -S ET die Ränge unverändert läßt.
2. Alle Knoten y im Baum von x mit y 6= rx .
In diesem Fall haben wir φj (y) = (α(n) − `(y)) · rank[y] − index(y) für j = i − 1, i.
Da y kein Wurzelknoten verändert die F IND -S ET-Operation rank[y] nicht.
Der Beweis, daß φi (y) ≤ φi−1 (y) verläuft analog zu Lemma 5.15. Wenn `(y) in der
iten Operation unverändert bleibt, so kann index(y) nicht kleiner werden, sondern
höchstens wachsen. Damit folgt aus der Definition des Potentials φ i (y) ≤ φi−1 (y).
Sollte `(y) in der iten Operation ansteigen, so steigt `(y) um mindestens eins. Dann
fällt aber der vordere Teil des Potentials, also (α(n) − `(y)) · rank[y] um mindestens
rank[y]. Da nach Eigenschaft 5.11 der Index index(y) höchstens gleich rank[y] ist,
kann er auch höchstens um rank[y] − 1 steigen/fallen. Als Resultat folgt, daß auch
hier φi (y) ≤ φi−1 (y) gilt.
Im Hinblick auf (5.7) haben wir mit der obigen Argumentation bereits gezeigt, daß a i =
O(h) gilt. Das ist aber noch nicht genug. Wir werden jetzt zeigen, daß für mindestens
max{0, h − (α(n) + 2)} Knoten auf dem Weg von x zur Wurzel das Potential um jeweils
mindestens eins fällt.
Sei a ein Knoten auf dem Suchweg vom gesuchten Element x zur Wurzel r x mit folgenden
Eigenschaften:
(i) rank[a] > 0
(ii) Es gibt einen Knoten b ∈
/ {a, rx } auf dem Weg von a zur Wurzel rx mit `(a) = `(b).
Abbildung 5.7 illustriert die Situation.
Zunächst argumentieren wir, daß es mindestens max{0, h − (α(n) + 2)} solche Knoten a
gibt: Nach Eigenschaft 5.10 kommen überhaupt nur Werte zwischen 0 und α(n) − 1 als
Stufen `(z) von Knoten z vor. Für jede Stufe k, k = 0, . . . , α(n) − 1 erfüllt höchstens der
letzte Knoten der Stufe (von unten auf dem Suchpfad zur Wurzel hin gesehen) die Bedingung (ii) mit der abgeschwächten Bedinung b 6= a statt b ∈
/ {a, r x } nicht. Mit der Wurzel rx
verletzen höchstens α(n) + 1 Knoten die Bedingung (ii). Möglicherweise erfüllt der erste
Knoten, der Knoten x auf dem Suchpfad die Bedingung (i) nicht (er könnte rank[x] = 0
haben). Es bleiben uns also noch mindestens h − α(n) − 2 = h − (α(n) + 2) Knoten übrig,
welche (i) und (ii) erfüllen.
Wir zeigen nun, daß für jeden der mindestens h − (α(h) + 2) Knoten das Potential um
mindestens eins fällt. Sei a ein solcher Knoten. Wir zeigen, daß das Potential von a bei
der iten Operation echt fällt: φi (a) ≤ φi−1 (a) − 1. Sei b ein Knoten auf dem Weg von
a zur Wurzel rx mit `(a) = `(b) = k (so ein Knoten existiert nach (ii)). Wir erinnern
5.4 Analyse von Pfadkompression und Vereinigung nach Rang
99
rx
b
rx
a
x
a
b
x
(a) Ausgangsbaum bei F IND -S ET(x). Es gilt
`(a) = `(b).
(b) Resultat nach F IND -S ET(x)
Abbildung 5.7: Analyse der F IND -S ET-Operation.
uns, daß die Ränge auf dem Weg zur Wurzel monoton steigen (Lemma 5.5 (ii)) und somit
rank[b] ≥ rank[p[a]] > rank[a] gilt. Damit ergibt sich:
rank[p[b]] ≥ Ak (rank[b])
≥ Ak (rank[p[a]])
(index(a)
≥ Ak (Ak
=
(nach Definition von k = `(b))
(da Ak monoton steigend)
(rank[a]))
(nach Definition von index(a))
(index(a)+1)
Ak
(rank[a]).
(5.8)
Sei s := index(a) der Wert von index(x) vor F IND -S ET (x) und der damit verbundenen Pfadkompression. Nach (5.8) haben wir vor der iten Operation rank[p[b]] ≥
(s+1)
Ak
(rank[a]). Nach F IND -S ET(x) und der damit verbundenen Pfadkompression gilt
p[a] = p[b] (vgl. Bild 5.7). Da rank[p[b]] bei der Pfadkompression auf keinen Fall kleiner
wird, haben wir nach der iten Operation damit
(s+1)
rank[p[a]] ≥ Ak
(rank[a]).
(5.9)
Entweder steigt nun durch die Pfadkompression `(a) um mindestens eins, oder wegen (5.9)
erhöht sich index(a) mindestens um eins auf i + 1.
Wenn `(a) unverändert bleibt und index(a) auf i + 1 ansteigt, so haben wir
φi (a) = (α(n) − `(a)) · rank[a] − (i + 1) = φi−1 (a) − 1.
Wenn `(a) ansteigt, so haben wir
φi (a) = φi−1 (a) − rank[a] − index(a)
≤ φi−1 (a) − 1,
100
Datenstrukturen für disjunkte Mengen
da nach Eigenschaft 5.11 1 ≤ index(a) ≤ rank[a] und sich somit index(a) um maximal
rank[a] − 1 ändern kann.
Wir haben somit gezeigt, daß sich in der iten Operation für keinen Knoten das Potential
erhöht, aber für mindestens max{0, h−(α(n)+2)} das Potential um mindestens eins fällt.
Daraus folgt nun für die amortisierten Kosten der iten Operation:
ai = Φi − Φi−1 + O(h) ≤ −(h − (α(n) + 2)) + O(h) = O(α(n)).
Hierbei haben wir wieder ausgenutzt, daß wir das Potential derart skalieren könnten, daß
die Konstante im O(h) Term ausgeglichen wird.
2
Wir fasssen die Ergebnisse unserer Analyse nochmals in einem Satz zusammen:
Satz 5.17 Eine Folge von m M AKE -S ET, U NION und F IND -S ET Operationen, von denen
n Operationen M AKE -S ET sind, kann man mit Hilfe der Baumrepräsentation mit Pfadkompression und Vereinigung nach Rang in Worst-Case Zeit O(mα(n)) implementieren. 2
Im Hinblick auf Satz 5.2 über die Laufzeit des Algorithmus von Kruskal erhalten wir folgendes Resulat:
Beobachtung 5.18 Mit Hilfe der Datenstruktur für disjunkte Mengen mit Pfadkompression und Vereinigung nach Rang benötigt der Algorithmus von Kruskal zur Bestimmung
eines MST O(mα(n)) Zeit plus die Zeit zum Sortieren der m Kanten auf einem Graphen
mit n Ecken und m Kanten.
Die letzte Beobachtung ist in folgendem Zusammenhang wichtig: Falls die Kanten des
Graphen bereits sortiert sind, so läuft der Kruskal-Algorithmus in O(mα(n)) Zeit! Der
Algorithmus und alle benutzten Datenstrukturen sind recht einfach. Dennoch erhalten wir
für diesen Fall einen Algorithmus mit fast linearer Laufzeit. Die Laufzeit von O(mα(n))
erhalten wir auch, falls wir die Kanten des Graphen gemäß ihres Gewichts in linearer Zeit
sortieren können. Dies ist etwa dann der Fall, wenn die Kantengewichte ganze Zahlen aus
{0, . . . , K} mit konstantem K oder aus {0, . . . , m} sind, siehe [2, Kapitel 9].
Suchbäume und
Selbstorganisierende
Datenstrukturen
Eine Heap-Ordnung ist nicht die einzige Möglichkeit, um Elemente in einem Baum anzuordnen. Manchmal benötigt man eine stärkere Ordnung (»Sortierung«). Wir betrachten
hier die sogenannte symmetrische Ordnung oder In-Order. Sei T ein binärer Baum. Jeder
Knoten in T besitzt neben der Schlüsselwertinformation key noch Zeiger left, right und
p, die auf den linken und rechten Sohn und auf den Vater im Baum zeigen (siehe Abbildung 6.1).
key
left
right
p
3
2
4
5
key
left
right
p
9
NULL
11
7
5
7
3
2
4
9
11
Abbildung 6.1: Ein binärer Baum und seine Implementierung mit Zeigern. Wir haben hier
der Einfachheit halber die Elemente mit den Schlüsselwerten identifiziert.
Definition 6.1 (Suchbaumeigenschaft (bzgl. der symmetrischen Ordnung))
Der binäre Baum T erfüllt die Suchbaumeigenschaft bezüglich der symmetrischen Ordnung, wenn für jeden Knoten x ∈ T folgendes gilt: Ist y ein Knoten im linken Teilbaum
von x, so gilt key[y] < key[x]. Ist z ein Knoten im rechten Teilbaum von x, so gilt
key[z] > key[x].
Wir haben in unserer Definition striktes „<“ und „>“ gefordert. Wir setzen in diesem Kapitel voraus, daß jedes Element einen eindeutigen Schlüssel besitzt, d.h., das jeder Schlüsselwert nur einmal vorkommt. Alle Ergebnisse lassen sich problemlos auch auf den Fall von
mehrfach vorkommenden Schlüsseln übertragen. Im Folgenden identifizieren wir meist der
Einfachheit halber die Elementen mit Ihren Schlüsselwerten. Dies erspart es uns key[x] für
den Schlüssel von x zu schreiben: wir können einfach x schreiben.
102
Suchbäume und Selbstorganisierende Datenstrukturen
Wie der Name bereits andeutet, können wir in einem Suchbaum T (effizient) suchen. Um
ein Element mit Schlüsselwert x in T zu suchen, starten wir in der Wurzel r von T . Wenn
key[r] = x, so sind wir fertig. Falls x < key[r], so suchen wir im linken Teilbaum von r
weiter, ansonsten suchen wir im rechten Teilbaum weiter. Falls ein Element mit Schlüssel x
in T enthalten ist, so finden wir dieses Element korrekt. Ansonsten terminiert die Suche in
einem leeren Teilbaum (Implementation: mit einem NULL-Zeiger). Hier können wir korrekt
feststellen, daß kein Element im Baum Schlüssel x hat. Algorithmus 6.1 zeigt den PseudoCode für die Suche. Die Suche nach x benötigt O(h) Zeit, falls x Tiefe h im Baum hat.
Algorithmus 6.1 Algorithmus zum Suchen eines Elements mit Schlüssel x in einem Suchbaum.
S EARCH -T REE -S EARCH(T, x)
1 v ← root[T ]
2 while v 6= NULL and key[v] 6= x do
3
if x < key[v] then
4
v ← left[v]
5
else
6
v ← right[v]
7
end if
8
if v 6= NULL then
9
return x wurde im Knoten v gefunden.
10
else
11
return x ist nicht im Baum enthalten.
12
end if
13 end while
Die Suchbaumeigenschaft ermöglicht es uns außerdem, die Schlüsselwerte in einem
Baum T sehr einfach sortiert auszugeben. Man muß dazu lediglich Algorithmus 6.2 mit
der Wurzel root[T ] aufrufen. Man sieht leicht, daß die Laufzeit von Algorithmus 6.2 für
einen Baum mit n Knoten Θ(n) beträgt.
Algorithmus 6.2 Rekursiver Algorithmus zur Ausgabe der Schlüsselwerte in einem Suchbaum in geordneter Reihenfolge.
I NORDER -T REE -T RAVERSAL(x)
1 if x 6= NULL then
2
I NORDER -T REE -T RAVERSAL(left[x])
3
print key[x]
4
I NORDER -T REE -T RAVERSAL(right[x])
5 end if
Definition 6.2 (Direkter Vorgänger und Nachfolger in einem Suchbaum)
Sei T ein Suchbaum, der eine Menge {x1 , . . . , xn } mit x1 < x2 < · · · < xn repräsentiert.
Wir setzen x0 := −∞ und xn := +∞. Ist x ∈
/ {x1 , . . . , xn } mit xi < x < xi+1 , so
heißen x− := xi der direkte Vorgänger von x und x+ := xi+1 der direkte Nachfolger
von x in T .
Für die Standard-Operationen auf (balancierten) Suchbäumen verweisen wir auf [3, Kapitel 12]. Wir beschäftigen uns in diesem Kapitel mit binären Suchbäumen, die »sich selbst
organisieren«. Was dies genau heißt, wird nachher noch genauer klar werden. Wir motivieren die Selbstorganisation durch eine spezielle Anwendung für einen optimalen (statischen) Suchbaum. Bevor wir diese Anwendung im nächsten Abschnitt genauer vorstellen,
notieren wir noch die dynamischen Mengenoperationen, von denen wir fordern, daß sie ein
Suchbaum effizient unterstützt:
6.1 Optimale statische Suchbäume
103
S EARCH (S, k) Sucht und liefert das Element mit Schlüsselwert k in der sortierten dynamischen Menge S. Falls x nicht im Baum vorhanden ist, soll NULL ausgegeben
werden.
I NSERT (S, x) Fügt das Element x in die dynamische sortierte Menge S ein.
D ELETE (S, x) Löscht das Element x in der Menge S.
Neben diesen »Standard-Operationen« fordern wir von unserer Datenstruktur noch, daß
auch folgende Operationen effizient unterstützt werden:
J OIN(S1 , S2 ) Liefert die sortierte Menge, die aus dem Elementen von S1 ∪ S2 besteht.
Diese Operation zerstört S1 und S2 und setzt voraus, daß für alle Schlüsselwerte k1 ∈
S1 und k2 ∈ S2 gilt: k1 ≤ k2 .
S PLIT (S, x) Teilt die Menge S, die x enthalten muß, in zwei Mengen: S1 enthält alle
Elemente aus S mit Schlüsselwerten kleiner oder gliech key[x] und S 2 enthält alle
Elemente aus S mit Schlüsselwerten größer als key[x].
Es gibt zahlreiche Klassen von balancierten Bäumen (etwa AVL-Bäume, Rot-SchwarzBäume, 2-3-Bäume, B-Bäume), die alle oben genannten Operationen in O(log n) Zeit unterstützen, wobei n die aktuelle Anzahl der Elemente in der Menge S sind, siehe [9, 3].
Wie schon erwähnt, ist unser Schwerpunkt in diesem Kapitel anders.
6.1 Optimale statische Suchbäume
Unsere Motation für optimale statische Suchbäume kommt aus dem Bereich der Codierungstheorie. Angenommen, wir haben eine Datei D mit 100.000 Zeichen, wobei jedes
Zeichen aus dem acht-elementigen Zeichenvorrat Σ = {a, b, c, d, e, f, g, h} stammt. Wenn
wir die Zeichen binär mit fester Länge codieren, so benötigen wir drei Bits pro Zeichen:
a = 000, b = 001, . . . , f = 101, g = 110, h = 111. Dies führt zu einem Platzbedarf von
100.000 Bits, um D zu speichern. Geht dies besser?
In unserem ersten Ansatz haben wir einen sogenannten Code mit fester Länge benutzt. Ein
Code mit variabler Länge kann eine deutliche Verbesserung der Speicherplatzausnutzung
ergeben. Beim Zählen, wie oft jedes Zeichen aus Σ in der Datei D auftaucht, ergibt sich
die Verteilung in Tabelle 6.1.
Häufigkeit (in 1000)
Codewort
fester Länge
Codewort
variabler Länge
a
40
b
13
c
12
d
5
e
18
f
6
g
3
h
3
000
001
010
011
100
101
110
111
1
010
011
00001
001
0001
000000
000001
Tabelle 6.1: Häufigkeiten der einzelnen Zeichen in der Beispieldatei und Codierung mit
fester bzw. variabler Länge.
Wenn wir die Zeichen gemäß des Codes in der dritten Zeile von Tabelle 6.1 codieren, so
benötigen wir folgende Anzahl von Bits:
1000 · (40 · 1 + 13 · 2 + 12 · 2 + 5 · 5 + 18 · 3 + 6 · 3 + 3 · 3 + 3 · 3) = 205.000.
Dies ist eine beträchtliche Ersparnis gegenüber dem Code mit fester Länge. Wie berechnet
man einen Code mit variabler Länge und was hat dies mit Suchbäumen zu tun?
104
Suchbäume und Selbstorganisierende Datenstrukturen
Der Code aus Tabelle 6.1 ist ein sogenannter Präfix-Code, d.h., kein Codewort ist ein Präfix
eines anderen Codeworts. Wir können den Code als binären Baum darstellen, der gleichzeitig als effizienter Decodier-Mechanismus gilt. Abbildung 6.2 zeigt den Code aus Tabelle 6.1 als binären Baum. Dabei ist das Codewort für ein ein Zeichen aus Σ im Pfad von
der Wurzel des Baums bis zum Blatt, welches das Zeichen enthält, »gespeichert«: eine 0
steht für »linker Sohn« eine 1 für »rechter Sohn«. Man sieht leicht, daß jedem Präfix-Code
ein Code-Baum entspricht und umgekehrt jeder Baum, dessen Blätter die Elemente aus Σ
bijektiv zugeordnet sind, einen Präfix-Code impliziert.
100
0
1
60
0
a: 40
1
35
0
17
b: 13
1
c: 12
f: 6
11
1
0
6
g: 3
e: 18
0
1
0
0
25
1
d: 5
1
h: 3
Abbildung 6.2: Code-Baum für den Beispielcode mit variabler Länge. Jedes Blatt ist mit
einem Zeichen aus dem Alphabet Σ und seiner relativen Häufigkeit (in Prozent) markiert.
Jeder innere Knoten enthält die Summe der relativen Häufigkeiten aller Blätter in seinem
Teilbaum.
Mit Hilfe eines Code-Baums kann man übrigens effizient decodieren: Man startet in der
Wurzel und folgt gemäß den gelesenen Bits einem Weg bis zu einem Blatt. Sobald man in
einem Blatt angelangt ist, hat man das entsprechende Zeichen aus Σ gefunden. Man startet
dann wieder in der Wurzel für das nächste Zeichen. Ist der Code-Baum bekannt, so kann
man einen Datenstrom in linearer Zeit decodieren.
Den Code-Baum zu einem Präfix-Code γ kann man auch als Suchbaum für die Elemente
in Σ betrachten. Ist für z ∈ Σ die Höhe des entsprechenden Blattes im Baum d T (z) und
p(z) ∈ [0, 1] die (relative) Häufigkeit von z in der zu codierenden Datei D, so benötigt die
Codierung von D mittels γ genau |D| · c(T ) Bits, wobei |D| die Anzahl der Zeichen in D
ist und
X
c(T ) :=
dT (z)p(z)
(6.1)
z∈Σ
die gewichtete durchschnittliche Blatthöhe von T ist. Wir können die relative Häufigkeit p(z) von z auch als Wahrscheinlichkeit ansehen, daß z angefragt wird. Dann entspricht
c(T ) dem Erwartungswert der Blatthöhe bei einer Suchanfrage. Einen optimalen PräfixCode erhalten wir, indem wir einen Suchbaum T ∗ konstruieren, der eine kleinstmögliche
erwartete Blatthöhe c(T ) besitzt.
Wenn die Verteilung p bekannt ist (im Fall unseres Codierungsbeispiels können wir p durch
einmaliges Durchlaufen der zu codierenden Datei D errechnen), so kann ein optimaler
6.2 Der Algorithmus von Huffman
Baum mit Hilfe des Algorithmus von Huffman (siehe nächster Abschnitt) bestimmt werden.
Allerdings hätten wir auch gerne für den Fall, daß die Verteilung p nicht bekannt ist, etwa
wenn Daten »online» über einen Datenkanal gesendet werden sollen, einen optimalen oder
zumindest »guten « Baum/Code, mit dem wir »online» codieren können. Wir werden in
Abschnitt 6.3 eine Datenstruktur, die Schüttelbäume, kennenlernen, die dieses Problem
lösen.
6.2 Der Algorithmus von Huffman
Wir führen auf dem Weg zu den selbstorganisierenden Datenstrukturen unseren kurzen
Ausflug in die Codierungstheorie fort. Wir zeigen, wie man einen optimalen statischen
Suchbaum effizient konstruieren kann. Zum einen rundet dies unseren Ausflug ab, zum
anderen werden wir sehen, wie man auch hier mit Hilfe von geeigneten Datenstrukturen
eine effiziente Laufzeit erhalten kann.
Im folgenden sei p : Σ → [0, 1] eine Wahrscheinlichkeitsverteilung auf Σ. Unsere Aufgabe
ist es, einen Baum T ∗ zu konstruieren, der optimale Kosten c(T ∗ ) (siehe Gleichung (6.1))
besitzt. Der Algorithmus von Huffman arbeitet wie folgt: er startet mit w[z] := p(z) für alle
z ∈ Σ. Dann fasst er iterativ die beiden »Zeichen«x und y (warum hier Anführungszeichen
stehen, wird gleich klar) mit den kleinsten Werten w[x], w[y] zusammen, indem er sie zu
Söhnen einer gemeinsamen Wurzel z macht, die Gewicht w[z] := w[x] + w[y] erhält. Die
Zeichen x und y werden entfernt und durch z ersetzt. Das Verfahren ist in Algorithmus 6.3
genauer im Pseudo-Code beschrieben. Ein Beispiel, wie der Huffman-Algorithmus einen
Code erzeugt, ist in den Abbildungen 6.3 und 6.4 zu sehen.
Algorithmus 6.3 Der Algorithmus von Huffman.
H UFFMAN -C ODE
1 for all z ∈ Σ do
2
w[z] ← p(z)
3
Alloziiere einen neuen Baumknoten z mit left[z] = right[z] = p[z] = NULL
4 end for
5 Q ← B UILD -H EAP (w)
Konstruiere einen Minimum-Heap für die Elemente aus Σ,
wobei das Element z ∈ Σ Schlüsselwert w[z] besitzt.
6 while |Q| > 1 do
7
x ← E XTRACT-M IN(Q)
8
y ← E XTRACT-M IN (Q)
9
Alloziiere einen neuen Baumknoten z.
10
left[z] ← x, p[x] ← z
11
p[y] ← z, p[x] ← z
12
right[z] ← y
13
p[z] ← NULL
14
w[z] ← w[x] + w[y]
15
I NSERT(Q, z)
{ Füge z in den Heap Q ein. }
16 end while
17 z ← E XTRACT-M IN (Q)
{ Q besteht jetzt nur noch aus einem Element. }
18 return z
Bevor wir die Korrektheit des Huffman-Algorithmus beweisen, analysieren wir seine Laufzeit. Sei n := |Σ| die Größe des Alphabets, das wir codieren wollen. Das Alloziieren der
n Knoten für die n Zeichen aus Σ benötigt dann O(n) Zeit. Ebenso ist für das Erstellen des
Heaps Q nur O(n) Zeit nötig. Die while-Schleife in den Zeilen 6 bis 16 wird insgesamt
105
106
Suchbäume und Selbstorganisierende Datenstrukturen
a: 40
b: 13
c: 12
d: 5
f: 6
e: 18
g: 3
h: 3
(a) Start: Die Knoten sind alle einzelne Zeichen aus Σ
6
a: 40
b: 13
c: 12
d: 5
f: 6
e: 18
g: 3
h: 3
(b) Situation nach Zusammenfassen der Knoten g und h mit kleinstem Gewicht
12
a: 40
b: 13
c: 12
e: 18
f: 6
6
g: 3
d: 5
h: 3
(c)
17
a: 40
b: 13
c: 12
f: 6
11
e: 18
6
g: 3
d: 5
h: 3
(d)
Abbildung 6.3: Erzeugung eines Beispielcodes durch den Huffman-Algorithmus.
6.2 Der Algorithmus von Huffman
107
25
a: 40
e: 18
b: 13
17
f: 6
11
c: 12
6
d: 5
g: 3
h: 3
(a)
25
a: 40
b: 13
35
17
c: 12
f: 6
11
6
g: 3
e: 18
d: 5
h: 3
(b)
60
35
a: 40
17
e: 18
b: 13
c: 12
f: 6
11
6
g: 3
25
d: 5
h: 3
(c)
100
60
a: 40
35
17
g: 3
e: 18
b: 13
c: 12
f: 6
11
6
25
d: 5
h: 3
(d) Fertiger Code-Baum
Abbildung 6.4: Fortsetzung: Erzeugung eines Beispielcodes durch den HuffmanAlgorithmus.
108
Suchbäume und Selbstorganisierende Datenstrukturen
n − 1 mal durchlaufen, da wir mit n Knoten starten und in jedem Schritt zwei Knoten zu einem verschmelzen (die Anzahl der Knoten also um eins reduzieren). Bis auf die E XTRACTM IN-Operationen läuft H UFMANN -C ODE also in linearer Zeit. Jede der 2n − 2 E XTRACTM IN-Aufrufe benötigt (bei Implementierung mit einem binären Heap) O(log n) Zeit, so
daß wir insgesamt eine Laufzeit von O(n log n) erhalten.
Satz 6.3 Der Huffman-Algorithmus findet einen Baum T∗ mit minimalen Kosten c(T ),
bzw. einen optimalen Präfix-Code mit variabler Länge. Der Algorithmus kann so implementiert werden, daß er in O(n log n) Zeit läuft.
Beweis: Die Laufzeit haben wir bereits bewiesen. Wir zeigen die Behauptung des Satzes
in zwei Schritten:
Behauptung 6.4 Seien x ∈ Σ und y ∈ Σ die zwei Zeichen mit geringster Häufigkeit p(x),
p(y). Es existiert ein optimaler Baum, in dem x und y Blätter größter Höhe sind, die außerdem einen gemeinsamen Vater besitzen.
Behauptung 6.5 Seien x ∈ Σ und y ∈ Σ die zwei Zeichen mit geringster Häufigkeit p(x),
p(y). Sei Σ0 := Σ\{x, y}∪{z}, wobei z ∈
/ Σ und p(z) := p(x)+p(y). Ist T 0 ein optimaler
0
Code-Baum für Σ , so ist der Baum T , der aus T 0 entsteht, indem man das Blatt für z durch
den Teilbaum x y ersetzt, ein optimaler Code-Baum für Σ.
Aus den Behauptungen 6.4 und 6.5 folgt dann sofort die Aussage des Satzes über die Korrektheit des Huffman-Algorithmus.
Beweis: (Behauptung 6.4) Sei T ein optimaler Code-Baum für Σ. Zunächst bemerken wir,
daß wir o.B.d.A. annehmen können, daß ein Blatt a maximaler Höhe in T immer einen
Bruder hat.
Falls a keinen Bruder besitzt, so ist a alleiniger Sohn seines Vaters p[a] (von p[a] kann
kein weiterer Teilbaum abzweigen, da in diesem sonst ein Blatt mit größerer Höhe als a
vorliegen würde). Daher können wir p[a] durch a ersetzen und den alten Knoten von a entfernen (siehe Abbildung 6.5). Die Kosten von T können höchstens geringer werden, da die
Höhe von a um eins sinkt, alle anderen Blätter ihre Höhe aber behalten. Fortsetzung dieses
Verfahrens liefert einen Baum, in dem das Blatt maximaler Höhe einen Bruder besitzt.
Ersetzen
von p[a]
durch a
p[a]
a
a
Abbildung 6.5: Ein Blatt maximaler Höhe a besitzt o.B.d.A. in einem optimalen Codebaum
einen Bruder. Ansonsten kann man den Baum ohne Kostenerhöhung modifizieren.
Seien nun a und b Blätter mit maximaler Höhe in T , die einen gemeinsamen Vater besitzen.
Wir nehmen o.B.d.A. an, daß p(a) ≤ p(b) ist. Nach Wahl von x und y gilt dann p(x) ≤
p(a) und o(y) ≤ p(b). Wir vertauschen a mit x und dann in einem zweiten Schritt b mit y,
so daß aus T zunächst T 0 und dann T 00 entsteht (siehe Abbildung 6.6).
6.2 Der Algorithmus von Huffman
109
Vertauschen
von x
und a
y
y
x
a
a
b
x
b
Vertauschen
von y
und b
b
a
x
y
Abbildung 6.6: Illustration des Beweises von Behauptung 6.4. Durch Vertauschen der Positionen von x und y mit derer von zwei Blättern maximaler Höhe mit gemeinsamem Vater
ensteht ein neuer Baum, ohne die Kosten zu erhöhen.
110
Suchbäume und Selbstorganisierende Datenstrukturen
Die Kosten von T 0 unterscheiden sich von denen von T durch die Terme für a und x (alle
anderen Blätter behalten ihre Höhen). Wir haben dann:
c(T 0 ) = c(T ) − (dT (a)p(a) + dT (x)p(x)) + dT 0 (a)p(a) + dT 0 (x)p(x)
= c(T ) − (dT (a)p(a) + dT (x)p(x)) + dT (x)p(a) + dT (a)p(x)
= c(T ) + (dT (x) − dT (a)) · (p(a) − p(x))
{z
} |
{z
}
|
≤0
≥0
≤ c(T ).
Dabei haben wir benutzt, daß dT (x) ≤ dT (a), da a ein Blatt größter Höhe ist, und p(a) ≤
p(x), da x kleinste Häufigkeit besitzt. Vollkommen analog zeigt man nun c(T 00 ) ≤ c(T 0 ).
Daher ist T 00 ebenfalls ein optimaler Baum, bei dem x und y an der gewünschten Position
liegen. Dies beendet den Beweis von Behauptung 6.4.
2
Beweis: (Behauptung 6.5) Wir müssen zeigen, daß der Baum T 0 aus der Behauptung ein
optimaler Code-Baum für Σ0 ist. Zunächst setzen wir die Kosten von T und T 0 in Beziehung. Der Baum T entspricht T 0 mit der Modifikation, daß z durch den dreiknotigen
Teilbaum mit Blättern x und y ersetzt wird. Wir haben also dT (x) = dT (y) = dT 0 (z) + 1.
Die Kosten von T errechnen sich daher wie folgt:
X
c(T ) =
dT (s)p(s)
s∈Σ
=
X
dT (s)p(s) + dT (y)p(y) + dT (x)p(x)
s∈Σ\{x,y}
=
X
dT 0 (s)p(s) + dT (y)p(y) + dT (x)p(x)
s∈Σ\{x,y}
=
X
s∈Σ\{x,y}
=
X
dT (s)p(s) + (dT 0 (z) + 1) (p(y) + p(x))
|
{z
}
=p(z)
dT 0 (s)p(s) + (p(y) + p(x))
s∈Σ\{x,y}∪{z}
= c(T 0 ) + (p(y) + p(x))
Angenommen, T̃ wäre ein Code-Baum für Σ0 mit c(T̃ ) < c(T 0 ). Wir ersetzen in T̃ das
Blatt z durch den dreiknotigen Teilbaum mit Blättern x und y (siehe Abbildung 6.7). Sei T̂
der entsprechende Code-Baum für Σ. Analog zum Kostenvergleich von T und T 0 errechnet
man
c(T̂ ) = c(T̃ ) + p(y) + p(x) < c(T 0 ) + p(y) + p(x) = c(T ).
Dies widerspricht der Annahme, daß T ein optimaler Code-Baum für Σ ist.
2
Wie bereits oben erwähnt, implizieren die Behauptungen 6.4 und 6.5 unmittelbar die Korrektheit des Hufmann-Algorithmus.
2
6.3 Schüttelbäume
Ein Schüttelbaum (engl. Splay-Tree) ist ein binärer Suchbaum, bei dem alle Suchbaumoperationen auf die folgende Operation S PLAY (»schüttele«) zurückgeführt werden:
6.3 Schüttelbäume
111
Ersetzen
von z
durch
y
x
z
x
y
Abbildung 6.7: Konstruktion eines neuen Code-Baums für Σ aus dem optimalen CodeBaum für Σ0 = Σ \ {x, y} ∪ {z}.
S PLAY(T, x) gibt einen Baum aus, der die selbe Menge von Elementen wie T darstellt.
Wenn x im Baum enthalten ist, so wird x zur Wurzel des Resultatbaums gemacht.
Wenn x nicht im Baum enthalten ist, so wird entweder der unmittelbare Vorgänger x− oder der umittelbare Nachfolger x+ von x im Baum T zur Wurzel.
6.3.1 Rückführen der Suchbaumoperationen auf S PLAY
Bevor wir die genaue Implementierung der S PLAY-Operation beschreiben (schon einmal
zur Vorwarnung: es ist wichtig, daß die S PLAY-Operation genau wie hier beschrieben ausgeführt wird, für andere Varianten gelten die gezeigten Ergebnisse nicht), zeigen wir, wie
die anderen Operationen auf S PLAY zurückgeführt werden können.
S EARCH Für S EARCH (T, x) führen wir S PLAY(T, x) aus und inspizieren die Wurzel.
Nach Definition der S PLAY-Operation befindet sich x nach S PLAY (T, x) genau in
der Wurzel, wenn x im Baum enthalten ist.
J OIN Um J OIN 2 (T1 , T2 ) zu implementieren, führen wir zunächst S PLAY(T1 , +∞) aus.
Als Resultat steht dann das größte Element in der Wurzel des geänderten Baums T 10 .
Diese Wurzel hat keinen rechten Sohn (da es kein größeres Element als +∞ gibt).
Wir können nun T2 zum rechten Teilbaum der Wurzel von T10 machen. Abbildung 6.8
veranschaulicht die Operationenfolge.
z
S PLAY(T1 , +∞)
T1
T2
z
T2
A
A
T2
Abbildung 6.8: Rückführen von J OIN (T1 , T2 ) auf S PLAY.
S PLIT Für S PLIT(T, x) führen wir S PLAY(T, x) aus und brechen dann eine der Verbindungen von der Wurzel zu den Teilbäumen auf, je nachdem, ob die Wurzel nach dem
S PLAY ein Element kleiner oder größer als x enthält, siehe Abbildung 6.9.
112
Suchbäume und Selbstorganisierende Datenstrukturen
z ∈ {x, x− , x+ }
z
S PLAY(T, x)
x−
Aufbrechen
z ∈
{x, x− }
A
B
A
B
Abbildung 6.9: Rückführen von S PLIT(T, x) auf S PLAY. Hier ist der Fall z ∈ {x, x − }
gezeigt. Der Fall z = x+ verläuft symmetrisch dazu.
I NSERT Um I NSERT(T, x) auszuführen, führen wir zunächst S PLIT(T, x) durch. Als Resultat erhalten wir zwei Bäume T − und T + , wobei T − alle Elemente kleiner als x
und T + alle Elemente größer als x enthält. Wir konstruieren einen neuen Baum mit
Wurzel x und T − als linkem und T + als rechtem Teilbaum. Das Vorgehen ist in
Abbildung 6.10 illustriert.
x
S PLIT(T, x)
T−
T+
T−
T+
Abbildung 6.10: Implementierung von I NSERT(T, x) in Schüttelbäumen.
D ELETE Zum Ausführen von D ELETE(T, x) führen wir S PLAY(T, x) aus, zerstören die
Wurzel, wodurch wir zwei Teilbäume T1 und T2 erhalten. Diese beiden Bäume werden dann wieder durch J OIN 2 (T1 , T2 ) zu einem neuen Baum verbunden, siehe Abbildung 6.11.
x
S PLAY(T, x)
J OIN(T1 , T2 )
T1
T1
T2
T2
Abbildung 6.11: Implementierung von D ELETE(T, x) in SchüttelbäumeSchüttelbäumen.
6.3.2 Implementierung der S PLAY-Operation
In diesem Abschnitt beschreiben wir, wie die zentrale Operation S PLAY(T, x) in einem
Schüttelbaum ausgeführt wird. Wie bereits erwähnt, gelten die in den folgenden Abschnitten gezeigten Ergebnisse nur, wenn die S PLAY-Operation wie hier beschrieben ausgeführt
wird. Insbesondere ist es dabei wichtig, daß die Operationen in der angegebenen Reihenfolge ausgeführt werden.
6.3 Schüttelbäume
Bei der Operation S PLAY(T, x) führen wir eine Anzahl von Rotationen im Baum T aus,
durch die x zur Wurzel gemacht wird. Wir starten dabei in x und unterscheiden verschiedene Fälle (im wesentlichen drei Fälle, von denen jeder zwei symmetrische Unterfälle hat),
je nachdem wie die Position von x zu seinem Vater p[x] und seinem Großvater p[p[x]] ist.
Sei u der Knoten, der x enthält. Diesen Knoten können wir durch die in Algorithmus 6.1
vorgestellte Suche in einem Suchbaum lokalisieren.
Falls u einen Vater, aber keinen Großvater besitzt, so führen wir eine Rotation am Vater v = p[u] durch, wodurch u zur Wurzel wird. Mit diesem Schritt terminiert das Verfahren. Der eben beschriebene Fall ist in Abbildung 6.12(a) gezeichnet. Die Zeichnung in
Abbildung 6.12(a) entspricht dem Fall, daß u linker Sohn seines Vaters ist. Falls u rechter
Sohn ist, so funktioniert die Rotation entsprechend symmetrisch. Es sollte klar sein, daß
eine Rotation in konstanter Zeit ausgeführt werden kann, da wir nur Zeiger auf die Teilbäume umhängen müssen. Details zu Rotationen in Suchbäumen, etwa zum Balancieren von
Bäumen, findet man in [3].
Falls u einen Großvater besitzt, so unterscheiden wir zwei Fälle, je nach der Stellung von
u zu seinem Vater und vom Vater p[u] zum Großvater w = p[v] = p[p[u]]. Je nachdem,
welcher Fall vorliegt, wird u durch eine geeignete Rotation weiter nach oben im Baum befördert. Algorithmus 6.4 zeigt die Details der S PLAY-Operation. In Abbildung 6.12 sind die
drei Fälle und die entsprechenden Rotationen gezeigt. Abbildung 6.13 zeigt ein Beispiel.
Algorithmus 6.4 Implementierung der S PLAY-Operation.
S PLAY(T, x)
1 u ← S EARCH -T REE -S EARCH (T, x)
{ Finde x mit Hilfe von Algorithmus 6.1. }
2 while p[u] 6= NULL do
{ Solange u noch nicht die Wurzel des Baums ist }
3
if u hat einen Vater v = p[u], aber keinen Großvater then
{ »Zick«-Fall, siehe Abbildung 6.12(a) }
4
Führe eine einfache Rotation an v = p[u] durch.
5
return
{ Beende das Verfahren. }
6
end if
7
if u hat einen Vater v = p[u] und einen Großvater w = p[v] = p[p[v]] and sowohl v
als auch u sind linke (rechte) Söhne ihres Vaters then
{ »Zick-Zick«-Fall, siehe Abbildung 6.12(b) }
8
Führe eine einfache Rotation an w gefolgt von einer einfachen Rotation an v aus.
9
else
10
{ »Zick-Zack«-Fall, siehe Abbildung 6.12(c). Der Knoten u hat einen
Vater v = p[u] und einen Großvater w = p[v] = p[p[v]] und v ist linker (rechter)
Sohn seines Vaters, u aber rechter (linker) Sohn seines Vaters }
11
Führe eine Doppelrotation an w aus.
12
end if
13 end while
Noch einmal soll darauf hingewiesen werden, daß die Reihenfolge der Rotationen von
entscheidender Bedeutung ist. So führen die Rotationen im Zick-Zack-Fall dazu, daß mit u
auch seine Teilbäume B und C näher an die Wurzel gelangen.
6.3.3 Analyse der S PLAY-Operation
In den nächsten beiden Abschnitten gehen wir der Frage nach, wie effizient Schüttelbäume sind. Dazu betrachten wir in diesem Abschnitt zunächst einmal die zentrale S PLAYOperation. Als Hilfsmittel dient uns die amortisierte Analyse aus Kapitel 3.
113
114
Suchbäume und Selbstorganisierende Datenstrukturen
Rotation
an v
v
u
u
v
C
A
A
B
B
C
(a) Zick: Der Knoten u wird durch Rotation zur Wurzel.
Rotation
an w
w
v
v
u
w
D
u
C
A
A
B
Rotation
an v
B
C
D
u
v
A
w
B
C
D
(b) Zick-Zick: Es erfolgt eine einfache Rotationen an w, gefolgt von einer einfachen Rotationen an v
Doppelrotation
an w
w
u
v
w
v
A
u
D
B
A
B
C
D
C
(c) Zick-Zack: Es erfolgt eine Doppelrotation an w.
Abbildung 6.12: Die drei Situationen beim Splay am Knoten u. Jeder Fall hat noch eine
symmetrische Variante, die hier nicht gezeichnet ist.
6.3 Schüttelbäume
115
8
8
9
6
7
2
7
2
5
1
9
6
3
1
4
4
3
5
(a) Im Ausgangsbaum wurde 3 gesucht. Es
wird jetzt am Knoten 13 geschüttelt. Es liegt
der Zick-Zick-Fall vor (angedeutet durch die
gestrichelten Kanten), bei dem zwei einfache
Rotation erfolgen.
(b) Nun liegt ein Zick-Zack-Fall vor. Es erfolgt eine Doppelrotation an 6.
8
3
3
9
6
2
1
6
1
7
4
8
2
5
(c) Nun liegt noch einmal der Zick-Fall vor,
bei dem 3 durch eine einfache Rotation zur
Wurzel wird und nach dem das Verfahren terminiert.
9
7
4
5
(d) Im Endergebnis steht 13 in der Wurzel.
Abbildung 6.13: Beispiel für eine S PLAY-Operation. Hier wird S PLAY(T, 1) ausgeführt.
116
Suchbäume und Selbstorganisierende Datenstrukturen
Sei U eine Menge von Elementen (»Universum«), die wir in den Suchbaum einfügen, im
Baum suchen und aus dem Baum löschen können. Die Menge U repräsentiert die möglichen Schlüsselwerte in unseren Bäumen. Sei g : U → R≥0 eine Gewichtsfunktion auf U .
Wir analysieren alle Operationen in Abhängigkeit der Gewichte der involvierten Elemente. Nachher werden wir g geeignet wählen, so daß wir eine ganze Reihe von hilfreichen
Ergebnissen erhalten.
Sei v ein Knoten im Baum T . Mit Tv bezeichnen wir den Teilbaum mit Wurzel v inklusive v
und mit G(v) das Gewicht aller Knoten in Tv , d.h.,
X
g(w).
G(v) :=
w∈Tv
Zur kürzeren Notation setzen wir außerdem:
G(T ) := G(root[T ]),
wobei root[T ] wie bisher die Wurzel von T ist. Nun definieren wir noch den GewichtsRang (oder einfach Rang) von v durch
!
X
r(v) := log2 G(v) = log2
g(w) .
(6.2)
w∈Tv
Letztendlich sei das Potential Φ(T ) eines Schüttelbaums definiert durch
X
Φ(T ) :=
r(v).
v∈T
Wir werden nun die amortisierten Kosten der S PLAY-Operation nach oben abschätzen. Wir
erinnern daran, daß die amortisierten Kosten einer Operation wie folgt definiert sind (siehe
Kapitel 3):
a := c + Φ(T 0 ) − Φ(T ),
wobei c der reale Zeitaufwand (reale Kosten) ist und Φ(T ) bzw. Φ(T 0 ) das Potential des
Baums vor bzw. nach der Operation bezeichnen.
Um die amortisierten Kosten der S PLAY-Operation abzuschätzen, zerlegen wir eine solche Operation in einzelne Splay-Schritte, in denen jeweils einer der drei Fälle aus Abbildung 6.12 vorliegt. Zunächst zeigen wir eine triviale, aber auch hilfreiche Eigenschaft der
Ränge.
Lemma 6.6 Für alle Knoten x mit p[x] 6= NULL gilt die Ungleichung r(p[x]) ≥ r(x).
Beweis: Die Ungleichung folgt sofort aus G(p[x]) = g(p[x]) + G(x) ≥ G(x).
2
Wir notieren noch ein hilfreiches Lemma:
Lemma 6.7 Die Logarithmusfunktion log 2 : R>0 → R ist konkav, erfüllt also insbesondere
log2 a + log2 b
a+b
log2
≤
2
2
für alle a, b > 0.
Beweis: Mit elementaren Mitteln der Analysis.
2
Lemma 6.8 Die amortisierten Kosten eines einzelnen Splay-Schrittes am Knoten u, bei
dem der Zick-Fall vorliegt, betragen höchstens 1 + 3(r 0 (u) − r(u)).
6.3 Schüttelbäume
117
Beweis: Wir bezeichnen mit r 0 die Ränge der einzelnen Knoten nach dem Splay-Schritt
und mit T 0 den Ergebnisbaum. Durch den Splay-Schritt ändern sich nur die Ränge von u
und seinem Vater v im Baum T , so daß für die Potentialdifferenz gilt:
Φ(T 0 ) − Φ(T ) = r0 (u) + r0 (v) − r(u) − r(v)
(6.3)
Weiterhin ist r0 (u) = r(v), da beide Größen dem Logarithmus der Summe der Gewichte
aller Elemente im Baum entsprechen. Die realen Kosten für den Splay-Schritt sind gleich 1.
Somit erhalten wir aus (6.3) für die amortisierten Kosten die obere Schranke:
1 + Φ(T 0 ) − Φ(T ) = 1 + r 0 (v) − r(u)
≤ 1 + r0 (u) − r(u)
≤ 1 + 3(r0 (u) − r(u))
(nach Lemma 6.6)
(da r 0 (u) = r(v) ≥ r(u) nach Lemma 6.6)
Somit folgt das Lemma.
2
Lemma 6.9 Die amortisierten Kosten eines einzelnen Splay-Schrittes am Knoten u, bei
dem der Zick-Zick-Fall oder ein Zick-Zack-Fall vorliegt, betragen höchstens 3(r 0 (u) −
r(u)).
Beweis: Wir betrachten als erstes den Zick-Zick-Fall. Die realen Kosten sind gleich 2 (für
zwei Rotationen) Die Ränge aller Knoten außer u, v und w bleiben unverändert. Daher sind
die amortisierten Kosten für den Splay-Schritt:
2 + Φ(T 0 ) − Φ(T ) = r0 (u) + r0 (v) + r0 (w) − r(u) − r(v) − r(w)
= 2 + r0 (v) + r0 (w) − r(u) − r(v)
≤ 2 + r0 (v) + r0 (w) − 2r(u)
≤ 2 + r0 (u) + r0 (w) − 2r(u)
(da r 0 (u) = r(w))
(da r(u) ≤ r(v))
(da r 0 (v) ≤ r0 (u))
(6.4)
Weiterhin gilt G(u) + G0 (w) ≤ G0 (u) (vgl. Abbildung 6.12(b)), also haben wir
r(u) + r0 (w) = log2 G(u) + log2 G0 (w)
G(u) + G0 (w)
2
= 2r0 (u) − 2.
≤ 2 log2
(nach Lemma 6.7)
≤ 2 log2
G0 (u)
2
Aus dieser Ungleichungskette erhalten wir r 0 (w) ≤ 2r0 (u) − 2 − r(u). Setzt man diese
Ungleichung in (6.4) ein, so erhalten wir
2 + Φ(T 0 ) − Φ(T ) ≤ 2 + r 0 (u) + (2r0 (u) − 2 − r(u)) − 2r(u) = 3(r 0 (u) − r(u)).
Damit haben wir die Behauptung des Lemmas für den Zick-Zick-Fall bewiesen.
Wir betrachten nun den Zick-Zack-Fall (siehe Abbildung 6.12(c)). Wie beim Zick-ZickFall ändern sich höchstens die Ränge von u, v und w. Daher sind die amortisierten Kosten
gegeben durch:
2 + Φ(T 0 ) − Φ(T ) = 2 + r 0 (u) + r0 (v) + r0 (w) − r(u) − r(v) − r(w)
= 2 + r0 (v) + r0 (w) − r(u) − r(v)
≤ 2 + r0 (v) + r0 (w) − 2r(u)
(da r 0 (u) = r(w))
(da r(v) ≥ r(u))
(6.5)
(6.6)
118
Suchbäume und Selbstorganisierende Datenstrukturen
Es gilt nun G0 (v) + G0 (w) ≤ G0 (u) (siehe Abbildung 6.12(c)). Damit folgt analog zum
Zick-Zick-Fall, daß r 0 (v) + r0 (w) ≤ 2r0 (u) − 2. Benutzt man diese Ungleichung in (6.5),
so erhält man:
2 + Φ(T 0 ) − Φ(T ) ≤ 2 + (2r 0 (u) − 2)) − 2r(u)
= 2(r0 (u) − r(u))
≤ 3(r0 (u) − r(u))
Dies beendet den Beweis des Lemmas.
(da r 0 (u) ≥ r(u))
2
Korollar 6.10 Sei T ein Schüttelbaum mit Wurzel root[T ] und x ein Knoten in T . Die
amortisierten Kosten für die Operation S PLAY (T, x) betragen höchstens
G(T )
,
(6.7)
1 + 3 · (r(root[T ]) − r(x)) = O log
G(x)
wobei root[T ] der Wurzelknoten von T ist.
Beweis: Die Abschätung durch den linken Term in (6.7) folgt sofort aus Lemma 6.8 und
6.9 durch Summieren der amortisierten Kosten für die einzelnen Splay-Schritte. Der rechte
Term ergibt sich aus r(v) = log2 G(v) und den Rechenregeln für den Logarithmus.
2
Aus unserer Implementierung des Suchens mittels der S PLAY-Operation ergibt sich die
gleiche (amortisierte) Zeitkomplexität wie für S PLAY(T, x) auch für S EARCH (T, x). Wir
werden im nächsten Abschnitt ähnliche Schranken wie in Korollar 6.10 für die anderen
Suchbaumoperationen beweisen.
Bevor wir dies tun, soll hier schon auf die Mächtigkeit der Aussage in Korollar 6.10 hingewiesen werden. Das Korollar gilt unabhängig von der Gewichtsfunktion g : U → R ≥0 . Es
steht uns frei, g geeignet zu wählen.
Zunächst erinnern wir nochmal daran, wie wir aus oberen Schranken für die amortisierten
Kosten einer Operationenfolge auch obere Schranken für die realen Kosten dieser Folge
herleiten können. Die realen Kosten entsprechen den amortisierten Kosten plus der Potentialdifferenz Φ(T ) − Φ(T 0 ), wobei T der Startbaum und T 0 der Endbaum nach der
Operationenfolge ist (vgl.˜(3.2)). Für die erwähnte Potentialdifferenz gilt:
X
Φ(T ) − Φ(T 0 ) =
r(u) − r0 (u)
u∈T
=
X
log
G(u)
G0 (u)
log
G(T )
g 0 (u)
u∈T
≤
X
u∈T
(6.8)
Wir sind nun bereit, das erste wichtige Ergebnis zu zeigen.
Satz 6.11 Die realen Kosten für eine Folge von m Suchzugriffen auf einen Schüttelbaum
mit n Elementen sind in O((m + n) log n).
Beweis: Wir setzen g(u) := 1/n für alle u ∈ T . Es gilt dann G(T ) = 1 und somit sind
nach Korollar 6.10 die amortisierten Kosten für einen Suchzugriff dann O(log n). Damit
ergibt sich unmittelbar die obere Schranke von O(m log n) für die amortisierten Kosten
einer Folge von m Suchoperationen. Die realen Kosten sind nach (6.8) in O(m log n) +
O(n · n) = O((n + m) log n).
2
6.3 Schüttelbäume
119
Das Ergebnis aus Satz 6.11 läßt sich informell wie folgt formulieren: auf einer genügend
langen Folge von Suchzugriffen sind Schüttelbäume mindestens so effizient wie ein beliebiger statischer Suchbaum, der »gleichmäßig balanciert« ist, d.h. in dem jedes Element
logarithmische Tiefe besitzt.
Wir verschärfen dieses Ergebnis jetzt, indem wir zeigen, daß Schüttelbäume mindestens so
effizient sind wie ein beliebiger statischer Suchbaum.
Satz 6.12 (Statische Optimialität von Schüttelbäumen) Die realen Kosten für eine Folge von m Suchzugriffen auf einen Schüttelbaum mit n Elementen, wobei Element u genau
q(u) ≥ 1 mal gesucht wird, sind in
!
X
m
O m+
.
q(u) log
q(u)
u∈T
Beweis: Für u ∈ T setzen wir G(u) := q(u)/m. Dann ist G(T ) = 1 und
nach Korollar 6.10 sind die amortisierten Kosten für einen Suchzugriff auf u in
O(log(m/q(u))). Daher sind die amortisierten Kosten für die q(u) Zugriffe auf u höchstens O(q(u) log(m/q(u))).P
Nach (6.8) ist mit den gerade definierten Gewichten der Po2
tentialverlust über die Folge u∈T log(m/q(u)).
Warum liefert uns Satz 6.12 eine »statische Optimalität« der Schüttelbäume? Wir benutzen
ein Resultat aus der Informationstheorie, welches die minimalen Suchkosten nach unten
abschätzt.
Satz 6.13 Die Kosten für eine Folge von m Suchoperationen auf einem statischen Suchbaum T ∗ mit n Elementen, von denen Elememt u genau q(u) ≥ 1 mal gesucht wird, sind
in
!
X
m
q(u) log
Ω m+
.
q(u)
∗
u∈T
2
Aus den Sätzen 6.12 und 6.13 folgt, daß die Schüttelbäume maximal um einen konstanten
Faktor schlechter sind als ein optimaler statischer Suchbaum. Die Schüttelbäume benötigen
dabei aber keinerlei Kenntnis über die Verteilung der Suchzugriffe!
6.3.4 Analyse der Suchbaumoperationen
Nachdem wir in Korollar 6.10 eine obere Schranke für die amortisierten Kosten der SplayOperation (und somit auch der Such-Operation) hergeleitet haben, beschäftigen wir uns
nun mit den anderen Suchbaumoperationen.
Satz 6.14 Für die amortisierten Kosten der Suchbaumoperationen in einem Schüttelbaum
gelten folgende Aussagen:
1. Die amortisierten Kosten für S PLAY(T, x) und S EARCH (T, x) sind in
 O log G(T )
falls x ∈ T
G(x)
G(T
)
O log
falls x ∈
/ T.
min{G(x− ),G(x+ )}
120
Suchbäume und Selbstorganisierende Datenstrukturen
2. Die amortisierten Kosten für J OIN(T1 , T2 ) betragen
G(T1 ) + G(T2 )
,
O log
G(z)
wobei z das größte Element im Baum T1 ist.
3. Die amortisierten Kosten für S PLIT(T, x) sind in
 O log G(T )
falls x ∈ T
G(x)
G(T
)
O log
falls x ∈
/ T.
min{G(x− ),G(x+ )}
4. Die amortisierten Kosten für I NSERT(T, x) sind
G(T ) + g(x)
,
O log
min{G(x− ), G(x+ ), g(x)}
wobei x− und x+ der direkte Vorgänger bzw. Nachfolger von x in T sind.
5. Die amortisierten Kosten für D ELETE(T, x) sind
G(T ) − g(x)
G(T )
+ O log
,
O log
G(x)
G(x− )
wobei x− der direkte Vorgänger von x im Baum T ist.
Beweis:
1. Die Schranke für S PLAY(T, x) haben wir bereits in Korollar 6.10 gezeigt. Wir haben auch bereits argumentiert, daß die Kosten von S EARCH(T, x) mit denen von
S PLAY(T, x) identisch sind, falls x ∈ T . Falls x ∈
/ T , so wird nach Definition der
S PLAY-Operation entweder der Vorgänger x− oder der Nachfolger x+ in die Wurzel
befördert.
2. Die amortisierten Kosten für J OIN 2 (T1 , T2 ) kann man wie folgt nach oben abschätzen (vgl. Abbildung 6.8):
G(T1 )
O log
(für S PLAY(T1 , +∞))
G(r)
+1
(für die realen Kosten,)
(um T2 an r anzuhängen)
+ O (log(G(T1 ) + G(T1 )) − log G(T1 ))
(Rangänderung von r)
(beim Anhängen von T2 )
G(T1 )
G(T1 ) + G(T2 )
= O log
+ O log
G(r)
G(T1 )
G(T1 ) + G(T2 )
= O log
G(r)
(da G(T1 ) ≥ G(r))
3. Bei S PLIT(T, x) wird zunächst ein S PLAY(T, x) ausgeführt (vgl. Abbildung 6.9).
Für diesen Schritt sind die amortisierten Kosten identisch mit der S PLAY-Operation.
Danach werden noch zwei Links von der Wurzel aufgebrochen, was konstante reale
Kosten erfordert. In diesem zweiten Schritt ändert sich maximal der Rang der Wurzel x. Da der Rang aber höchstens fällt, sind die amortisierten Kosten für den zweiten
Schritt auch konstant.
6.3 Schüttelbäume
121
4. Die Kosten für I NSERT(T, x) entsprechen bis auf die Potentialänderung durch das
Einfügen der neuen Wurzel denen von S PLIT(T, x). Durch das Einfügen der Wurzel x (siehe Abbildung 6.10) erhöht sich das Potential höchstens um
G(T ) + g(x)
.
log
g(x)
so daß die behauptete Schranke folgt.
5. Die Abschätzung für D ELETE(T, x) folgt aus den Abschätzungen für S PLAY(T, x)
und J OIN(T1 , T2 ), wobei T1 und T2 wie in Abbildung 6.11 sind. Man benutzt dabei,
daß G(T1 ) + G(T2 ) = G(T ) − g(x).
2
Setzt man g(u) := 1 für alle u ∈ U , so zeigt Satz 6.14, daß alle Suchbaumoperationen
in einem Schüttelbaum mit n Knoten in O(log n) amortisierter Zeit ausgeführt werden
können. Somit haben wir folgendes Ergebnis:
Satz 6.15 Eine Folgt von m Suchbaumoperationen
auf einer Menge von anfangs leeren
P
Schüttelbäume benötigt O(m + m
i=1 log ni ) Zeit, wobei ni die Anzahl der Knoten in
demjenigen Baum ist, auf den die ite Operation wirkt.
2
Satz 6.15 zeigt, daß die Schüttelbäume nicht nur bei der Suche, sondern bei allen Suchbaumoperationen asymptotisch so gut sind wie die gebräuchlichen Klassen von balancierten
Bäumen.
122
Schnelle Algorithmen für
Maximale Netz-Flüsse
Netzflüsse sind wichtige Werkzeuge zur Modellierung vieler Optimierungsprobleme. Informell besteht das »Maximalfluss-Problem« darum, in einem Netz mit Kapazitäten auf den
Bögen so viel Fluß wie möglich von einer ausgezeichneten Quelle s zu einer ausgezeichneten Quelle t zu schicken. Dabei dürfen die Kapazitäten auf den Kanten nicht überschritten
werden.
In diesem Kapitel stellen wir Algorithmen zur Bestimmung maximaler Flüsse vor. Wir
starten mit einer kurzen Wiederholung der Grundbegriffe und der einfachsten Algorithmen, die auf sogenannten augmentierenden Pfaden basieren. Der Schwerpunkt liegt dann
aber auf fortgeschritteneren Techniken. Dies sind zum einen Präfluß-Schub-Algorithmen
(engl. Preflow-Push) , die während des Laufs unzulässige Lösungen halten, letztendlich
aber mit einem gültigen Fluß terminieren. Außerdem führen wir die dynamischen Bäume,
eine Erweiterung der Schüttelbäume aus Abschnitt 6.3, ein, und zeigen, wie man mit dieser
ausgefeilten Datenstruktur auch Fluß-Algorithmen weiterhin beschleunigen kann.
7.1 Notation und grundlegende Definitionen
In diesem Kapitel bezeichnet G = (V, A) einen gerichteten Graphen mit Kapazitäten
c : A → R≥0 für die Kanten. Wir nehmen an, daß G keine parallelen Bögen besitzt.
Diese Annahme ist rein notationstechnisch: für die Bestimmung von maximalen Flüssen
können parallele Kanten zu einer Kante mit der Summe der Kapazitäten zusammengefasst
werden. Unsere zweite notationsvereinfachende Voraussetzung ist, daß G zu jedem Bogen
(u, v) ∈ A auch den inversen Bogen (v, u) enthält. Diese Voraussetzung kann man immer
dadurch erzwingen, daß wir für einen nicht vorhandenen inversen Bogen einen Bogen mit
Kapazität 0 einfügen.
Sei f : A → R≥0 eine Funktion. Wir stellen uns dabei f (u, v) als »Flußwert« auf dem
Bogen (u, v) vor. Für den Knoten v ∈ V bezeichnen wir mit
ef (v) :=
X
(u,v)∈A
f (u, v) −
X
f (v, w)
(7.1)
(v,w)∈A
den Überschuß von v unter f . Der erste Term in (7.1) entspricht dabei dem Zufluß in v über
eingehende Bögen, der zweite Term ist der Abfluß von v.
Definition 7.1 (Fluß in einem Netz)
Seien s, t ∈ V Knoten im Netz G mit Kapazitäten c : A → R≥0 . Ein (zulässiger) (s, t)Fluß ist eine Funktion f : A → R, die folgende Bedingungen erfüllt:
124
Schnelle Algorithmen für Maximale Netz-Flüsse
(i) f erfüllt die Kapazitätsbedingungen, d.h. für für jeden Bogen a ∈ A gilt: 0 ≤
f (a) ≤ c(a).
(ii) f gewährleistet Flußerhaltung in allen Knoten bis auf s und t, d.h. für jeden Knoten
v ∈ V \ {s, t} gilt: ef (v) = 0.
Der Knoten s heißt Quelle, der Knoten t Senke des Flusses f .
Ist f ein Fluß in G, so gilt:
ef (s) + ef (t) =
X
(da ef (v) = 0 für alle v ∈ V \ {s, t})
ef (v)
v∈V
=
X
v∈V
= 0.


X
(u,v)∈A
f (u, v) −
X
(v,w)∈A

f (v, w)
Dabei haben wir für die letzte Gleichheit ausgenutzt, daß in der Summe jeder Flußwert
f (x, y) einmal positiv in der ersten Summe für v = x und einmal negativ für v = y
auftaucht. Der Netto-Zufluß in die Senke t ist also gleich dem Netto-Abfluß aus der Quelle.
Den Wert val(f ) := e(t) = −e(s) nennt man den Flußwert des Flusses f . Ein Fluß heißt
maximaler (s, t)-Fluß, wenn er maximalen Flußwert unter allen zulässigen (s, t)-Flüssen
besitzt.
Grundsätzlich kann man sich bei der Betrachtung maximaler Flüsse auf den Fall beschränken, daß immer mindestens einer der Werte f (u, v) oder f (v, u) gleich null ist. Ist
f (u, v) > 0 und f (v, u) > 0, und setzen wir = min{f (u, v), f (u, v)}, so besitzt der
durch


f (u, v) − , falls (x, y) = (u, v)
0
f (x, y) := f (v, u) − , falls (x, y) = (v, u)


f (u, v)
, sonst
definierte Fluß den gleichen Flußwert wie f . Zusätzlich gilt f 0 (u, v) = 0 oder f 0 (v, u) =
0. Fortsetzung liefert einen Fluß, der auf maximal einem Bogen aus einem Paar inverser
Bögen positiven Fluß aufweist. Wir werden daher im Folgenden immer annehmen, daß ein
Fluß nur auf maximal einem Bogen aus einem Paar von inversen Bögen positiven Fluß
besitzt.
7.2 Residualnetze und flußvergrößernde Wege
Das Residualnetz Gf zu einem Fluß f spezifiziert, wie viel Fluß man maximal längs der
Bögen noch zusätzlich schicken kann.
Definition 7.2 (Residualnetz)
Sei f ein Fluß in G. Das Residualnetz Gf besitzt die gleiche Eckenmenge wie G und
enthält für jeden Bogen (u, v) ∈ A bis zu zwei Bögen:
• Falls f (u, v) < c(u, v), so enthält Gf einen Bogen (u, v)+ mit Residualkapazität
r((u, v)+ ) := c(u, v) − f (u, v).
• Falls f (u, v) > 0, so enthält Gf einen Bogen (v, u)− mit Residualkapazität
r((v, u)− ) := f (u, v).
7.3 Maximale Flüsse und Minimale Schnitte
125
Abbildung 7.1 zeigt ein Beispiel für ein Residualnetz. Prinzipiell kann G f durchaus parallele Bögen besitzen, obwohl G keine Parallelen hat. Gilt nämlich f (u, v) < c(u, v) und
f (v, u) > 0, so enthält Gf einmal den Bogen (u, v)+ wegen f (u, v) < c(u, v) und noch
einmal den Bogen (u, v)− wegen f (v, u) > 0. Um hier das Notationsproblem zu umschiffen, haben wir die »Vorzeichen« für die Bögen in Gf eingeführt. Ohne die Vorzeichen wäre
unklar, welchen der potentiell zwei Bögen (u, v) in Gf wir meinen. Im Folgenden benutzen
wir δ als Platzhalter für ein Vorzeichen, d.h. jeder Bogen in Gf hat die Form (u, v)δ .
2
2
(3, 4)
s
1
(5, 5)
(2, 3)
4
(2, 2)
1
(5, 9)
5
t
s
1
1
4
2
(0, 1)
3
4
5
5
t
1
3
(f (u, v), c(u, v))
u
5
3
2
r(u, v)
v
u
(a) Das Ausgangsnetzwerk G mit einem
Fluß f . Inverse Bögen mit Fluß 0 sind nicht
eingezeichnet.
v
(b) Das zugehörige Residualnetz Gf
Abbildung 7.1: Illustration für ein Residualnetz.
Residualnetze haben eine besondere Bedeutung für maximale Flüsse. Sei p ein gerichteter
Weg von s nach t im Residualnetz Gf und ∆(p) := min(u,v)δ ∈p r((u, v)δ ) die minimale
Residualkapazität auf den Bögen von p (vgl. Abbildung 7.2).
Wir können nun den Fluß f »längs des Weges p« erhöhen: Falls der Bogen (u, v) + von Gf
auf dem Weg p liegt, so ist f (u, v) < c(u, v) und wir setzen f 0 (u, v) := f (u, v) + ∆(p).
Liegt (u, v)− , so ist f (v, u) > 0 und wir setzen f 0 (v, u) := f (v, u)−∆(p). Für alle Bögen
(u, v)δ , die nicht auf p liegen, sei f 0 (u, v) := f (u, v) (vgl. Abbildung 7.2(c)). Offenbar ist
f 0 wieder ein Fluß in G und val(f 0 ) = val(f ) + ∆(p) > val(f ).
Definition 7.3 (Flußvergrößernder Weg)
Ein Weg im Residualnetz Gf heißt flußvergrößernder Weg für den Fluß f . Die Residualkapazität des Weges ist die minimale Residualkapazität auf seinen Bögen.
Aus unseren Überlegungen ergibt sich nun sofort das folgende Lemma:
Lemma 7.4 Existiert ein flußvergrößernder Weg für f , so ist f kein maximaler Fluß.
2
7.3 Maximale Flüsse und Minimale Schnitte
Definition 7.5 (Schnitt in einem gerichteten Graphen, Vorwärtsteil und Rückwärtsteil)
Sei G = (V, A) ein gerichteter Graph und S ∪ T = V eine Partition von V . Dann nennen
wir
[S, T ] := { (u, v) ∈ A : u ∈ S und v ∈ T } ∪ { (u, v) ∈ A : u ∈ T und v ∈ S }
126
Schnelle Algorithmen für Maximale Netz-Flüsse
2
2
(3, 5)
s
1
(5, 5)
(2, 3)
2
(5, 9)
4
(2, 2)
s
t
5
5
3
2
1
5
t
r(u, v)
v
u
(a) Das Ausgangsnetzwerk mit einem Fluß f
mit Wert val(f ) = 5.
v
(b) Ein flußvergrößernder Weg p (gestrichelt hervorgehoben) im Residualnetz Gf mit
∆(p) = 1.
2
2
(4, 5)
(5, 5)
(1, 3)
1
(6, 9)
4
(2, 2)
s
t
5
5
4
1
1
2
4
2
(1, 1)
3
3
6
5
1
3
(f (u, v), c(u, v))
u
5
3
(f (u, v), c(u, v))
1
4
1
3
s
4
2
(0, 1)
u
1
r(u, v)
v
u
v
(d) Das resultierende Residualnetz Gf 0 besitzt keinen Weg von s nach t mehr. Die noch
von der Quelle s in Residualnetz erreichbaren
Knoten S sind schwarz hervorgehoben.
(c) Flußerhöhung längs des Weges ergibt einen
neuen Fluß f 0 mit val(f 0 ) = val(f ) +
∆(p) = val(f ) + 1.
2
(4, 5)
s
1
(5, 5)
(1, 3)
4
(2, 2)
(6, 9)
5
t
(1, 1)
3
(f (u, v), c(u, v))
u
v
(e) Die im Residualnetz Gf 0 von s aus
erreichbaren Knoten S induzieren einen
Schnitt [S, T ] mit c[S, T ] = val(f ). Die Bögen im Vorwärtsteil (S, T ) sind gestrichelt
hervorgehoben.
Abbildung 7.2: Ein gerichteter (s, t)-Weg in einem Residualnetz kann zum Erhöhen des
Flußwertes benutzt werden.
t
7.3 Maximale Flüsse und Minimale Schnitte
127
den von S und T erzeugten Schnitt. Weiterhin nennen wir
(S, T ) := { (u, v) ∈ A : u ∈ S und v ∈ T }
(T, S) := { (u, v) ∈ A : u ∈ T und v ∈ S }
den Vorwärtsteil und (T, S) den Rückwärtsteil des Schnittes. Es gilt [S, T ] = (S, T ) ∪
(T, S). Der Schnitt [S, T ] heißt ein (s, t)-Schnitt, falls s ∈ S und t ∈ T .
Abbildung 7.3 zeigt ein Beispiel für einen Schnitt in einem gerichteten Graphen und seinen
Vorwärts- bzw. Rückwärtsteil.
2
s
3
2
1
4
5
s
t
1
4
6
5
(a) Ein (s, t)-Schnitt [S, T ] in einem gerichteten Graphen. Die Bögen
in [S, T ] sind gestrichelt hervorgehoben.
6
(b) Der Vorwärtsteil (S, T ) des
(s, t)-Schnittes: Die Bögen in (S, T )
sind gestrichelt hervorgehoben.
2
s
3
3
1
4
5
t
6
(c) Der Rückwärtsteil (T, S) des
(s, t)-Schnittes: Die Bögen in (T, S)
sind gestrichelt hervorgehoben.
Abbildung 7.3: Ein Schnitt [S, T ] in einem gerichtenen Graphen sowie sein Vorwärtsteil
(S, T ) und sein Rückwärtsteil (T, S).
Definition 7.6 (Kapazität eines Schnittes)
Ist c : A → R≥0 eine Kapazitätsfunktion auf den Bögen, so definieren wir die Kapazität
des Schnittes [S, T ] als die Summe der Kapazitäten der Bögen im Vorwärtsteil des Schnittes:
X
c[S, T ] :=
c(u, v).
(u,v)∈(S,T )
t
128
Schnelle Algorithmen für Maximale Netz-Flüsse
Seien f ein (s, t)-Fluß und [S, T ] ein (s, t)-Schnitt im Netz G. Dann gilt:
X
val(f ) = −e(s) = −
e(v)
v∈S
=
X
v∈S


X
(v,w)∈A
f (v, w) −
X
(u,v)∈A

f (u, v) .
(7.2)
Wenn für einen Bogen (x, y) sowohl x als auch y in S liegen, dann tritt der Term f (x, y)
in der Summe in (7.2) wieder einmal positiv und einmal negativ auf. Die Summe in (7.2)
reduziert sich also auf:
X
X
f (u, v).
(7.3)
f (v, w) −
val(f ) =
(u,v)∈(T,S)
(v,w)∈(S,T )
Benutzen wir, daß f zulässig ist, also 0 ≤ f (x, y) ≤ c(x, y) für alle (x, y) ∈ A gilt, so
erhalten wir aus (7.3):
X
X
X
c(v, w) = c[S, T ].
f (u, v) ≤
f (v, w) −
(v,w)∈(S,T )
(u,v)∈(T,S)
(v,w)∈(S,T )
Die obige Rechnung zeigt, daß die Kapazität eines beliebigen (s, t)-Schnittes eine obere
Schranke für den maximalen Flußwert eines (s, t)-Flusses ist. Wir notieren dieses wichtige
Ergebnis in einem Lemma:
Lemma 7.7 Ist f ein (s, t)-Fluß und [S, T ] ein (s, t)-Schnitt, so gilt:
val(f ) ≤ c[S, T ].
Da f und [S, T ] beliebig wählbar sind, folgt:
max
f ist (s, t)-Fluß in G
val(f ) ≤
min
[S, T ] ist (s, t)-Schnitt in G
c[S, T ].
(7.4)
2
Wir zeigen jetzt, daß in (7.4) tatsächlich Gleichheit gilt. Sei f ∗ ein maximaler (s, t)-Fluß
(die Existenz eines solchen Flusses folgt aus Stetigkeitsgründen). Nach Lemma 7.4 existiert
kein flußvergrößernder Weg für f ∗ . Folglich ist t von s aus in Gf ∗ nicht erreichbar und die
beiden Mengen
S := { v ∈ V : v ist in Gf + von s erreichbar }
T := { v ∈ V : v ist in Gf + von s nicht erreichbar }
sind beide nichtleer (wir haben s ∈ S und t ∈ T ) und definieren einen Schnitt [S, T ].
Sei (v, w) ∈ (S, T ) ein Bogen im Vorwärtsteil des Schnittes. Dann gilt f (v, w) = c(v, w),
denn sonst wäre (u, v)+ ein Bogen in Gf ∗ und w von s in Gf ∗ erreichbar im Widerspruch
zu w ∈ T (wir haben v ∈ S und somit ist nach Definition von S der Knoten v von s aus
in Gf ∗ erreichbar). Dies zeigt:
X
X
f (v, w) =
c(v, w) = c[S, T ].
(7.5)
(v,w)∈(S,T )
(u,v)∈(S,T )
Analog muß für jeden Bogen (u, v) ∈ (T, S) gelten, daß f (u, v) = 0, da sonst (v, u) −
Bogen in Gf ∗ wäre und mit v auch u von s aus erreichbar wäre. Also ist:
X
f (u, v) = 0.
(7.6)
(u,v)∈(T,S)
7.4 Grundlegende Algorithmen
129
Aus (7.5) und (7.6) folgt:
X
f (v, w) −
c[S, T ] =
(v,w)∈(S,T )
X
f (u, v)
(u,v)∈(T,S)
= val(f )
(nach Gleichung (7.3)).
Wegen Lemma 7.7 muß f ∗ ein maximaler Fluß und gleichzeitig [S, T ] ein minimaler
Schnitt, d.h. ein Schnitt mit minimaler Kapazität, sein. Unser Ergebnis, daß der maximale
Flußwert gleich der Kapazität eines minimalen Schnittes ist, ist als das berühmte MaxFlow-Min-Cut-Theorem bekannt:
Satz 7.8 (Max-Flow-Min-Cut-Theorem) In einem Netz ist der Wert eines maximalen
(s, t)-Flusses gleich der minimalen Kapazität eines (s, t)-Schnittes:
max
f ist (s, t)-Fluß in G
val(f ) ≤
min
[S, T ] ist (s, t)-Schnitt in G
Beweis: Siehe oben.
c[S, T ].
2
Das Max-Flow-Min-Cut-Theorem hat eine große Anzahl wichtiger und interessanter kombinatorischer Anwendungen, auf die wie aber hier wegen der Ausrichtung dieses Skripts
nicht eingehen. Details finden sich unter anderem in [1, 3, 6, 7].
Unser Beweis des Max-Flow-Min-Cut-Theorems oben hat noch ein zweites Nebenprodukt. Die Argumentation (angewendet auf einen beliebigen Fluß f anstelle des maximalen
Flusses f ∗ ) zeigt, daß, falls es keinen flußvergrößernden Weg in Gf ist, der Fluß f ein
maximaler Fluß ist. Die Umkehrung dieses Sachverhalts hatten wir bereits in Lemma 7.4
gezeigt. Dieses Ergebnis wird uns noch beim Beweis der Korrektheit von Flußalgorithmen
nützlich sein. Daher notieren wir es in einem Satz:
Satz 7.9 Ein Fluß f ist genau dann ein (s, t)-Fluß mit maximalem Flußwert, wenn es keinen flußvergrößernden Weg, d.h. keinen Weg in G f von s nach t, gibt.
2
7.4 Grundlegende Algorithmen
Satz 7.9 aus dem letzten Kapitel motiviert sofort die Idee zu einem einfachen Algorithmus zur Bestimmung eines maximalen Flusses (siehe Algorithmus 7.1): Wir starten mit
dem Nullfluß f ≡ 0. Solange Gf einen Weg von s nach t besitzt, erhöhen wir den Fluß
längs dieses Weges. Danach aktualisieren wir Gf . Dieser Algorithmus geht auf Ford und
Fulkerson zurück.
Sind die Kapazitäten c im Netzwerk ganzzahlig, ist also c : A → N0 , so wird durch Algorithmus 7.1 der Fluß in jedem Erhöhungsschritt um einen ganzzahligen Betrag erhöht (ist
der aktuelle Fluß f ganzzahlig, so sind alle Residualkapazitäten als Differenzen von ganzen
Zahlen wieder ganzzahlig). Somit ist jeder Fluß, der zwischenzeitlich entsteht ganzzahlig.
Außerdem erhöht sich der Fluß in jedem Schritt um mindestens 1.
Sei C := max{ c(a) : a ∈ A } die größte auftretende Kapazität. Dann hat der (s, t)Schnitt [S, T ] mit S := {s} und T := V \ S höchstens Kapazität (n − 1)C (in s starten
höchstens n − 1 Bögen, zu jedem der n − 1 anderen Knoten jeweils einer). Somit muß
der generische Algorithmus 7.1 nach maximal (n − 1)C Erhöhungen terminieren, weil er
keinen vergrößernden Weg mehr findet. Nach Satz 7.9 ist bei Abbruch gefundene Fluß (der
nach unseren Überlegungen oben ganzzahlig ist) dann auch maximal. Damit haben wir
folgende Ergebnisse bewiesen:
130
Schnelle Algorithmen für Maximale Netz-Flüsse
Algorithmus 7.1 Generischer Algorithmus auf Basis flußvergrößernder Wege.
AUGMENTING -PATH(G, c, s, t)
Input:
Ein gerichteter Graph G = (V, A) in Adjazenzlistendarstellung; eine
nichtnegative Kapazitätsfunktion c : E → R≥0 , zwei Knoten s, t ∈ V .
1
2
3
4
5
6
7
8
9
for all (u, v) ∈ A do
f (u, v) ← 0
end for
while in Gf existiert ein Weg von s nach t do
Wähle einen solchen Weg p.
∆ ← min{ r((u, v)δ ) : (u, v)δ ∈ p }
Erhöhe f längs p um ∆ Einheiten.
Aktualisiere Gf .
end while
{ Starte mit dem Nullfluß f ≡ 0. }
{ Residualkapazität des Weges p. }
Satz 7.10 Sind alle Kapazitäten ganzzahlig, so bricht der generische Algorithmus auf Basis flußvergrößernder Wege, Algorithmus 7.1, nach O(nC) Vergrößerungsschritten mit einem maximalen Fluß ab, der ganzzahlig ist.
2
Korollar 7.11 Sind alle Kapazitäten ganzzahlig, so existiert immer ein maximaler Fluß,
der ganzzahlig ist.
2
Das Ergebnis von Korollar 7.11 ist äußerst wichtig und Grundbaustein für viele kombinatorische Folgerungen aus dem Max-Flow-Min-Cut-Theorem. Achtung, das Korollar zeigt
nicht, daß jeder maximale Fluß ganzzahlig ist! Es zeigt nur, daß mindestens ein maximaler Fluß existiert, der zusätzlich ganzzahlig ist. Beispielsweise zeigt Abbildung 7.4 einen
maximalen Fluß der nicht ganzzahlig ist, obwohl alle Kapazitäten ganzzahlig sind.
2
( 12 , 1)
s
1
(1, 1)
( 12 , 1)
4
t
( 12 , 1)
3
(f (u, v), c(u, v))
u
v
Abbildung 7.4: Auch bei ganzzahligen Kapazitäten kann ein maximaler Fluß existieren, der
nicht ganzzahlig ist. Der gestrichelte Bogen ist der Vorwärtsteil eines minimalen Schnittes.
Nach Korollar 7.11 existiert aber mindestens ein ganzzahliger maximaler Fluß.
In Algorithmus 7.1 bleibt zunächst offen, wie wir in Schritt 5 einen flußvergrößernden Weg
wählen. Zur Flußerhöhung (und zum Beweis von Satz 7.10 und Korollar 7.11) genügt es,
irgendeinen Flußvergrößernden Weg zu finden. Dazu müssen wir im Residualgraphen G f
einen Weg von s nach t finden, bzw. feststellen, daß es keinen solchen Weg gibt. Mit Hilfe
der Breitensuche (siehe Abschnitt C.2 auf Seite 166) können wir diese Aufgabe in O(n +
m) Zeit lösen.
Da die Breitensuche immer einen kürzesten Weg von s nach t in G f liefert, finden alle Flußvergrößerungen auf kürzesten Wegen statt. Diese Auswahl der flußvergrößernden
7.4 Grundlegende Algorithmen
131
2
2
(0, C)
(0, C)
C
1
s
1
(0, 1)
4
s
t
C −1
1
1
t
4
1
(0, C)
(0, C)
3
(f (u, v), c(u, v))
u
C
C −1
3
r(u, v)
v
u
v
(b) Nach der Flußerhöhung dreht sich im
Residualnetzwerk die Richtung des Bogens (3, 2) um. Der flußvergrößernde Weg
(1, 2, 3, 4) hat wieder Residualkapazität 1.
(a) Das Ausgangsnetzwerk entspricht dem
Residualnetzwerk Gf für den Nullfluß f ≡
0. Der flußvergrößernde Weg (1, 3, 2, 4) hat
Residualkapazität 1.
2
C −1
s
1
1
1
1
C −1
4
1
C −1
2
C −1
t
s
C −1
C −2
C −2
4
1
r(u, v)
u
1
1
1
1
3
1
t
1
C −1
3
r(u, v)
v
(c) Wählt man nun wieder den flußvergrößernden Weg (1, 3, 2, 4) so hat dieser wieder
Residualkapazität 1.
u
v
(d) Auch im nächsten Schritt hat dann der
Weg (1, 2, 3, 4) wieder Residualkapazität 1,
so daß sich der Flußwert auch wieder um 1
erhöht.
Abbildung 7.5: Bei ungeschickter Wahl des flußvergrößernden Wegs kann der generische
Algorithmus 7.1 Ω(nC) Iterationen benötigen. Im oben gezeigten Beispiel werden abwechselnd die flußvergrößernden Wege (1, 3, 2, 4) und (1, 2, 3, 4) gewählt. In jeder Iteration erhöht sich der Flußwert um 1, der maximale Flußwert ist 2C, so daß insgesamt 2C = n/2·C
Iterationen benötigt werden.
132
Schnelle Algorithmen für Maximale Netz-Flüsse
Wege im generischen Algorithmus 7.1 liefert den Algorithmus von Edmonds und Karp.
Aus unseren bisherigen Überlegungen folgt, daß der Algorithmus von Edmonds und Karp
bei ganzzahligen Kapazitäten in O((n + m)nC) Zeit einen maximalen Fluß liefert. Wir
werden weiter unten (Satz 7.13 auf der folgenden Seite) noch eine polynomiale Schranke
für die Zeitkomplexität des Algorithmus von Edmonds und Karp herleiten.
Es sollte bemerkt werden, daß der Algorithmus 7.1 bei ungeschickter Wegeauswahl extrem
lange benötigen kann. Es gibt Beispiele, bei denen dann wirklich Ω(nC) Flußerhöhungen
vorgenommen werden. Ein solches Beispiel ist in Abbildung 7.5 dargestellt. Falls C = 2 n ,
so besitzt Algorithmus 7.1 exponentielle Laufzeit.
Ein weiterer theoretischer Nachteil des generischen Algorithmus 7.1 ist, daß er bei nicht
ganzzahligen Kapazitäten möglicherweise nicht terminiert (die Residualkapazitäten der gefundenen Wege konvergieren hier gegen 0) und daß selbst im Grenzprozeß der gefundene
Fluß dann möglicherweise nicht maximal ist. Für Beispiele verweisen wir wieder auf [1].
Lemma 7.12 Für jeden Knoten v ∈ V ist während des Algorithmus von Edmonds und
Karp der Abstand δ(s, v) von s nach v in Gf monoton wachsend.
Beweis: Wir zeigen die Behauptung durch Induktion nach der Anzahl der Erhöhungsschritte über flußvergrößernde Wege. Falls keine Erhöhung stattfindet, so ist die Aussage trivial.
Angenommen, die Aussage gelte bis nach der iten Erhöhung. Sei f der Fluß nach der iten
Erhöhung und f 0 der Fluß nach der (i + 1)ten Erhöhung. Wir bezeichnen mit δ(s, v) den
Abstand von s zu v in Gf und mit δ 0 (s, v) den entsprechenden Abstand in Gf 0 . Wir müssen
zeigen, daß δ 0 (s, v) ≥ δ(s, v) für alle v ∈ V gilt.
δ 0 (s, u)
s
u
v δ 0 (s, v) = δ 0 (s, u) + 1
Abbildung 7.6: Beweis von Lemma 7.12: Ist u Vorgänger von v auf dem kürzesten Weg
von s nach v in Gf 0 , so gilt δ 0 (s, v) = δ 0 (s, u) + 1.
Angenommen δ 0 (s, v) < δ(s, v) für ein v ∈ V . Dann gilt v 6= s, da der Abstand von s zu
sich selbst immer gleich Null ist. Sei v bereits so gewählt, daß δ 0 (s, v) minimal unter allen
Knoten ist, welche die Behauptung des Lemmas verletzen. Sei weiterhin u der Vorgänger
von v auf dem kürzesten Weg von s nach v in Gf 0 (vgl. Abbildung 7.6). Dann gilt
δ 0 (s, v) = δ 0 (s, u) + 1,
(7.7)
da der Teilweg von s nach u ein kürzester Weg von s nach u sein muß. Nach der Wahl von
v folgt
δ 0 (s, u) ≥ δ(s, u).
(7.8)
Wir wissen, daß der Bogen (u, v)δ in Gf 0 ist. Falls (u, v)δ ebenfalls in Gf vorhanden war,
so gilt:
δ(s, v) ≤ δ(s, u) + 1
≤ δ 0 (s, u) + 1
0
= δ (s, v)
Dies widerspricht der Annahme, daß δ 0 (s, v) < δ(s, v).
(nach (7.8))
(nach (7.7)).
7.5 Präfluß-Schub-Algorithmen
133
Also ist (u, v)δ nicht in Gf . Da aber (u, v)δ in Gf vorhanden ist, muß der im (i + 1)ten
Schritt gefundene flußvergrößernde Weg den Bogen (v, u)−δ benutzt haben. Da der Algorithmus längs kürzester Wege erhöht, gilt δ(s, u) = δ(s, v) + 1, also
δ(s, v) = δ(s, u) − 1
≤ δ 0 (s, u) − 1
(nach (7.8))
0
= δ (s, v) − 2
< δ 0 (s, v).
(nach (7.7))
Erneut erhalten wir einen Widerspruch.
2
Damit können wir nun die Komplexität des Algorithmus von Edmonds und Karp abschätzen.
Satz 7.13 Sei G ein Netzwerk mit ganzzahligen, rationalen oder reellen 1 Kapazitäten. Der
Algorithmus von Edmonds und Karp terminiert nach O(nm) Iterationen mit einem maximalen Fluß. Die Gesamtkomplexität des Algorithmus ist O(nm 2 ).
Beweis: Wir nennen einen Bogen (u, v)δ auf einen flußvergrößernden Weg p einen
Flaschenhals-Bogen, wenn die Residualkapazität von (u, v)δ der Residualkapazität von p
entspricht. In jeder Iteration des Algorithmus von Edmonds und Karp ist mindestens ein
Bogen ein Flaschenhals-Bogen. Durch die Flußerhöhung verschwinden alle FlaschenhalsBögen aus dem Residualnetzwerk.
Wir zeigen, daß jeder Bogen maximal O(n) mal Flaschenhals sein kann. Damit folgt dann
die behauptete Iterationszahl von O(nm). Die Komplexität für eine Iteration hatten wir
bereits mit O(n + m) bestimmt.
Sei (u, v)δ ein Flaschenhalsbogen in der aktuellen Iteration, und sei f der Fluß zu Beginn
dieser Iteration. Da der Algorithmus längs kürzester Wege erhöht, gilt für die Abstände
in Gf :
δ(s, v) = δ(s, u) + 1.
(7.9)
Wie bereits bemerkt, verschwindet durch die Erhöhung der Bogen (u, v) δ aus dem Residualnetzwerk. Insbesondere kann (u, v)δ so lange nicht wieder ein Flaschenhals werden,
bis in einem Erhöhungsschritt der neue flußvergrößernde Weg den Bogen (v, u) −δ benutzt.
Sei f 0 der Fluß, bei dem dies passiert. Dann gilt für die Abstände δ 0 in Gf 0 :
δ 0 (s, u) = δ 0 (s, v) + 1
≥ δ(s, v) + 1
= δ(s, u) + 2.(nach (7.9))
(da längs kürzester Wege erhöht wird)
(nach Lemma 7.12)
Somit hat sich der Abstand von s zu u um mindestens 2 erhöht. Der Abstand von s zu u
kann jedoch niemals größer als n − 1 werden (solange der Abstand endlich ist, was hier
der Fall ist, da längs eines Weges von s nach u erhöht wird). Damit folgt, daß der Bogen
(u, v)δ höchstens (n − 1)/2 = O(n) mal Flaschenhals sein kann.
2
7.5 Präfluß-Schub-Algorithmen
In diesem Abschnitt stellen wir die sogenannten Präfluß-Schub-Algorithmen zur Bestimmung maximaler Flüsse vor. Diese Algorithmen bieten sowohl theoretisch als auch praktisch effiziente Laufzeiten.
1 Im Fall von reellen Kapazitäten nehmen wir an, daß wir arithmetische Operationen auf reellen Zahlen ebenfalls in konstanter Zeit ausführen können
134
Schnelle Algorithmen für Maximale Netz-Flüsse
Ein Nachteil der (meisten) Algorithmen auf Basis flußvergrößernder Wege ist, daß sie in
jedem Schritt einen (potentiell langen) flußvergrößernden Weg suchen und in der nächsten
Iteration erneut mit der Suche nach einem solchen Weg starten, ohne Informationen aus der
letzten Suche zu benutzen. Abbildung 7.7 verdeutlicht diese Situation. Die Präfluß-SchubAlgorithmen versuchen, effizienter zu arbeiten, indem sie nicht längs ganzer Wege, sondern
nur längs einzelner Kanten Fluß »schieben«.
1
s
1
1
1
..
.
1
1
1
1
1
1
1
1
1
1
1
1
1
t
..
.
Abbildung 7.7: Bei Algorithmen auf Basis flußvergrößernder Wege wird der lange erste
Teil des Graphen in jedem Weg benutzt.
Wir nennen eine Knotenbewertung d : V → N0 eine Distanzmarkierung bezüglich Gf ,
wenn sie folgende Eigenschaften besitzt:
d(t) = 0
d(u) ≤ d(v) + 1
(7.10)
δ
für jeden Bogen (u, v) ∈ Gf .
(7.11)
Die Bedingungen (7.10) und (7.11) nennen wir die Gültigkeitsbedingungen und bezeichnen
d(v) als die Distanzmarke des Knotens v.
Ist p = (v = v0 , v1 , . . . , vk = t) ein Weg von v nach t in Gf , so folgt aus den Gültigkeitsbedingungen, daß
d(v) ≤ d(v1 ) + 1 ≤ d(v2 ) + 2 ≤ · · · ≤ d(t) + k = k.
Also ist die Distanzmarke d(v) höchstens so groß wie die Länge des Weges p (in Bögen).
Da p beliebig war, folgt, daß d(v) eine untere Schranke für die Länge des kürzesten Weges
von v nach t in Gf ist. Aus dieser Eigenschaft und Satz 7.9 ergibt sich das folgende Lemma:
Tatsache:
Lemma 7.14 Ist d eine Distanzmarkierung bezüglich Gf und d(s) ≥ n, so ist f ein maximaler Fluß.
Beweis: Gibt es einen Weg von s nach t in Gf , so existiert auch ein kreisfreier solcher Weg.
Nach den Gültigkeitsbedingungen müßte ein Weg von s nach t in G f mindestens Länge n
besitzen. Daraus folgt aber, daß dieser Weg einen Kreis aufweisen muß. Also existiert kein
flußvergrößernder Weg und f ist nach Satz 7.9.
2
7.5 Präfluß-Schub-Algorithmen
Wir benötigen vor der Beschreibung des ersten Präfluß-Schub-Algorithmus noch die Definition eines Präflusses. Für einem Präfluß lockern wir die Flußerhaltungsbedingungen
dahingehend, daß wir für alle Knoten v ∈ V \ {s, t} fordern, daß e f (v) ≥ 0 ist. In jeden
Knoten außer der Quelle und der Senke läuft also mindestens soviel Fluß, wie aus dem
Knoten abläuft.
Definition 7.15 (Präfluß, aktiver Knoten)
Seien s, t ∈ V Knoten im Netz G mit Kapazitäten c : A → R≥0 . Ein (zulässiger) (s, t)Präfluß (engl. Preflow) ist eine Funktion f : A → R, welche die Kapazitätsbedingungen
(siehe Definition 7.1) einhält, und die ef (v) ≥ 0 für alle v ∈ V \ {s, t} erfüllt.
Ein Knoten v ∈ V \ {s, t} mit ef (v) > 0 heißt aktiver Knoten.
Die Präfluß-Schub-Algorithmen basieren auf folgender Idee: Wir starten mit einem Präfluß f , der bewirkt, daß s von t in Gf nicht mehr erreichbar ist. Die Eigenschaft, daß es
in Gf keinen Weg von s nach t gibt, wird im Verlauf des Algorithmus invariant gesichert.
Erreichen wir, daß für alle Knoten v ∈ V \ {s, t} gilt: ef (v) = 0, so ist f ein Fluß, der
maximal sein muß, da kein flußvergrößernder Weg vorhanden ist. Falls noch ein aktiver
Knoten u, also ein Knoten u ∈ V \ {s, t} mit ef (u) > 0 existiert, so versuchen wir Fluß
von ihm zu einem Knoten v mit (u, v)δ ∈ Gf »wegzuschieben«.
Beim Schieben von Fluß längs der Bögen im Residualnetzwerk halten wir uns an folgende
Strategie: da wir letztendlich möglichst viel Fluß zur Senke schieben wollen, ist es unser
Ziel (Über-) Fluß von Knoten, die weiter von t entfernt sind, zu Knoten zu schieben, die
näher an t liegen. Hier kommen die Distanzmarkierungen ins Spiel.
Definition 7.16 (Zulässiger Bogen)
Sei Gf das Residualnetzwerk eines Präflusses f und d eine Distanzmarkierung bezüglich Gf . Ein Bogen (u, v)δ in Gf heißt zulässig, wenn d(u) = d(v) + 1.
Im Präfluß-Schub-Algorithmus schieben wir nur Fluß über zulässige Bögen im Residualnetzwerk. Die Arbeitsweise hat eine einprägsame und bildliche Interpretation. Wir stellen
uns den Fluß als Wasser in einem Röhrensystem vor. Die Distanzmarken der Knoten betrachten wir als »Höhen«. Das Wasser fließt immer »bergab«.
Anfangs heben wir die Quelle ganz hoch, so daß genügend Wasser ins Röhrensystem läuft.
Irgendwann sind wir möglicherweise in der Situation, daß ein aktiver Knoten u keinen
Abfluß besitzt, da alle benachbarten Knoten höher liegen. In diesem Fall heben wir u »genügend« an, so daß überflüssiges Wasser nach und nach wieder zur Quelle zurückströmt.
Algorithmus 7.2 zeigt den Pseudocode eines generischen Präfluß-Schub-Algorithmus. Er
started mit dem Nullfluß f ≡ 0, der ein gültiger Präfluß ist. Die exakten Distanzen zur Senke t werden in Schritt 4 berechnet. Anschließend werden in den Zeilen 5 bis 7 alle von der
Quelle ausgehenden Bögen (s, v) gesättigt. Die Quelle s wird in Schritt 8 »hochgehoben«.
Der Hauptteil des Algorithmus besteht aus dem wiederholten Aufruf des Unterprogramms
P USH -R ELABEL(u) für einen aktiven Knoten u. In P USH -R ELABEL(u) wird entweder
Fluß von u »bergab« über einen zulässigen Bogen zu einem Nachbarn in G f geschoben
(wir nennen dies einen Flußschub), oder u wird durch Erhöhen seiner Markierung »angehoben«(wir nennen dies eine Markenerhöhung). Der Algorithmus terminiert, sobald keine
aktiven Knoten mehr vorhanden sind. Abbildungen 7.8 bis 7.10 zeigen die Arbeitsweise
des Algorithmus an einem Beispiel.
Wir beschäftigen uns zunächst mit der Korrektheit von Algorithmus 7.2. Dazu betrachten
wir zuerst die Knoten-Markierungen d.
Lemma 7.17 Die Knotenbewertungen d[v] (v ∈ V ), die Algorithmus 7.2 hält, sind eine
gültige Distanzmarkierung.
135
136
Schnelle Algorithmen für Maximale Netz-Flüsse
Algorithmus 7.2 Generischer Präfluß-Schub-Algorithmus zur Bestimmung zur Bestimmung eines maximalen Flusses.
G ENERIC -P REFLOW-P USH(G, c, s, t)
Input:
Ein gerichteter Graph G = (V, A) in Adjazenzlistendarstellung; eine
nichtnegative Kapazitätsfunktion c : E → R≥0 , zwei Knoten s, t ∈ V .
Output: Ein maximaler (s, t)-Fluß f .
1 for all (u, v) ∈ A do
2
f (u, v) ← 0
{ Starte mit dem Preflow f ≡ 0. }
3 end for
4 Berechne die Abstände δ(v, t) in Gf und setze d[v] ← δ(v, t) für alle v ∈ V .


 Diese Abstandsberechnung kann mittels einer »umgedrehten 



 Breitensuche« von t aus in O(n + m) Zeit erfolgen. Bei der 

»umgedrehten Breitensuche« kehren wir die Richtung der





 Bögen um, so daß wir anstelle der Abstände von t die Ab- 

stände zu t erhalten.
5 for all (s, v) ∈ A do
6
f (s, v) ← c(s, v)
7 end for
8 d[s] ← n
9 while es existiert ein aktiver Knoten do
{ aktiver Knoten v: ef (v) > 0. }
10
Wähle einen aktiven Knoten u.
11
P USH -R ELABEL(u)
12 end while
P USH -R ELABEL(u)
1 if es gibt einen zulässigen Bogen (u, v)δ in Gf then
{ zulässiger Bogen: d[u] = d[v] + 1 }
2
δ ← min{ef (u), r((u, v)δ )}
3
Falls δ = +, setze f (u, v) ← f (u, v) + δ.
Falls δ = −, setze f (v, u) ← f (v, u) − δ.
{ »Schiebe δ Flußeinheiten von u nach v« (engl. Push). }
4 else
5
d[u] ← 1 + min{ d[v] : (u, v)δ ∈ Gf }
{ »Erhöhe die Marke von u« (engl. Relabel). }
6 end if
7.5 Präfluß-Schub-Algorithmen
137
(1, 0)
2
4
s
1
s
4
3
2
4
(2, 0)
4
4
(0, 0)
1
3
t
2
2
1
(1, 0)
3
s
(1, 4)
4
2
1
1
3
(b) Das Residualnetzwerk zum Präfluß f ≡ 0
mit den exakten Distanzmarken und den Überschüssen der Knoten. Die Höhen der Knoten illustrieren die Distanz. Die Bogenbewertungen zeigen die Residualkapazitäten.
(a) Das Ausgangsnetzwerk.
(4, −4)
s
4
(1, 4)
(4, −4)
4
2
1
4
(0, 0)
3
4
(0, 1)
t
2
(1, 2)
3
t
4
1
(c) Zuerst werden alle von s ausgehenden Bögen gesättigt. Zusätzlich wird die Distanzmarke von s auf d[s] = n gesetzt. Nicht zulässige Bögen sind gestrichelt gezeichnet. Sie führen
von einem Knoten zu einem Knoten mit mindestens »gleicher Höhe«. In der aktuellen Iteration
wird der aktive Knoten 3 ausgewählt. Der Bogen (3, 4) ist der einzige ausgehende zulässige
Bogen.
3
4
2
(2, 1)
3
t
1
(d) Es werde erneut der Knoten 3 ausgewählt.
Es existieren keine zulässigen ausgehenden Bögen. Daher wird die Marke von 3 auf 1 +
min{1, 4} = 2 erhöht. Dadurch wird der Bogen (3, 2) zulässig.
Abbildung 7.8: Bestimmung eines maximalen Flusses durch den Präfluß-SchubAlgorithmus.
138
Schnelle Algorithmen für Maximale Netz-Flüsse
s
(1, 4)
(4, −4)
4
2
1
s
4
(1, 0)
(4, −4)
4
2
1
(0, 4)
3
4
2
(2, 1)
t
(2, 1)
(a) In dieser Iteration werde Knoten 2 gewählt.
Der einzige zulässige ausgehende Bogen ist
(2, 4), über den 4 Einheiten Fluß geschoben
werden.
s
s
1
2
1
1
3
(4, −4)
t
1
(c) Der einzige aktive Knoten ist Knoten 2. Er
besitzt keinen zulässigen ausgehenden Bogen.
Seine Marke wird auf 1 + min{4, 2} = 3 erhöht. Dadurch wird der Bogen (2, 3) zulässig.
Es wird dann eine Einheit Fluß über den Bogen
(2, 3) geschoben.
2
4
1
1
3
(0, 5)
4
2
(2, 0)
1
t
4
(3, 0)
2
4
3
(0, 5)
(b) Es werde Knoten 3 gewählt. Der einzige zulässige ausgehende Bogen ist (3, 2), über den 1
Einheit Fluß geschoben werden.
(3, 1)
(4, −4)
4
2
1
3
3
2
(2, 1)
(0, 5)
4
3
t
1
(d) Als nächstes wird die Marke des einzigen aktiven Knoten 3 auf 1 + min{3, 4} = 4 erhöht.
Bogen (3, 2) wird zulässig und es wird wieder
eine Einheit Fluß über (3, 2) geschoben.
Abbildung 7.9: Fortsetzung: Bestimmung eines maximalen Flusses durch den PräflußSchub-Algorithmus.
7.5 Präfluß-Schub-Algorithmen
139
(5, 1)
2
4
s
(5, 0)
(4, −4)
1
1
2
2
(4, 0)
1
s
(4, −4)
1
1
1
2
(0, 5)
4
3
2
3
t
1
(a) Der Knoten 2 wird gewählt. Seine Marke
wird auf 1 + min{4, 4} = 5 erhöht. Es wird
dann eine Einheit Fluß über den Bogen (2, 1)
geschoben.
2
(4, 0)
1
(0, 5)
4
3
t
1
(b) Es existiert kein aktiver Knoten mehr. Der
Algorithmus terminiert mit einem Fluß, der wegen d[s] = n maximal sein muß.
Abbildung 7.10: Fortsetzung: Bestimmung eines maximalen Flusses durch den PräflußSchub-Algorithmus.
Beweis: Wir zeigen die Behauptung durch Induktion nach der Anzahl der Aufrufe des
P USH -R ELABEL-Unterprogramms. Vor dem ersten Aufruf sind alle Bedingungen (7.10)
und (7.11) offenbar erfüllt.
Durch eine Markenerhöhung des Knotens u auf 1 + min{ d[v] : (u, v) δ ∈ Gf } in Schritt 5
bleiben alle Bedingungen (7.11) offenbar erfüllt. Bei einem Schub von Fluß über den Bogen (u, v)δ in Schritt 3 kann der Bogen (v, u)−δ zum Residualnetzwerk hinzukommen. Für
diesen müssen wir die Bedingung d[v] ≤ d[u] + 1 verifizieren. Da der Algorithmus aber
nur gültige Bögen zum Schieben benutzt, beim Schub über (u, v) δ also nach Konstruktion
d[u] = d[v] + 1 gilt, ist dies aber gesichert.
2
Mit Hilfe des letzen Lemmas läßt sich nun die (partielle) Korrektheit des Algorithmus
folgern.
Lemma 7.18 Falls Algorithmus 7.2 abbricht, so ist f ein maximaler Fluß.
Beweis: Bei Abbruch existiert kein aktiver Knoten mehr, also ist f ein Fluß. Nach Lemma 7.17 ist d[s] ≥ n eine untere Schranke für den Abstand von t zur Quelle s. Nach
Lemma 7.14 muß f ein maximaler Fluß sein.
2
Unser nächster Schritt ist es zu zeigen, daß Algorithmus 7.2 immer nach endlich vielen
Schritten abbricht (also nach Lemma 7.18 immer einen maximalen Fluß liefert), und die
Komplexität bis zum Abbruch abzuschätzen. Es genügt, die Anzahl der Markenerhöhungen
und der Flußschübe zu beschränken.
7.5.1 Anzahl der Markenerhöhungen im Algorithmus
Lemma 7.19 Sei f der aktuelle Präfluß während der Ausführung von Algorithmus 7.2 und
v ∈ V ein aktiver Knoten. Dann existiert ein Weg von v nach s in G f .
140
Schnelle Algorithmen für Maximale Netz-Flüsse
Beweis: Sei S ⊆ V die Menge aller Knoten in Gf , von denen aus s erreichbar ist (d.h. für
die es einen Weg zu s gibt), und sei T := V \ S. Wir müssen zeigen, daß T keinen aktiven
Knoten enthält.
Es existiert kein Bogen (u, v)δ in Gf mit u ∈ T , v ∈ S (sonst wäre s auch von v aus
erreichbar). Also ist:
X
X
X
c(v, w).
(7.12)
f (v, w) = −
f (u, v) −
(v,w)∈(T,S)
(v,w)∈(T,S)
(u,v)∈(S,T )
Damit ergibt sich
X
ef (v)
0≤
(da f Präfluß ist und s ∈ S)
v∈T
=
X
v∈T
=


X
(u,v)∈A
X
(u,v)∈(S,T )
=−
X
f (u, v) −
f (u, v) −
c(v, w)
X
(v,w)∈A
X

f (v, w)
f (v, w)
(v,w)∈(T,S)
(nach (7.12))
(v,w)∈(T,S)
≤0
(da c ≥ 0).
P
Es folgt v∈T ef (v) = 0 und wegen ef (v) ≥ 0 dann auch ef (v) = 0 für alle v ∈ T .
2
Als Korollar aus dem letzten Lemma erhalten wir, daß der Algorithmus in Schritt 5 nie
über die leere Menge minimiert: Da es vom aktiven Knoten u einen Weg in G f zu s gibt,
startet insbesondere mindestens ein Bogen aus Gf in u.
Lemma 7.20 Während Algorithmus 7.2 gilt invariant d[v] ≤ 2n − 1 für alle v ∈ V . Die
Distanzmarke jedes Knotens wird höchstens 2n − 1 mal erhöht. Insgesamt finden O(n2 )
Markenerhöhungen statt.
Beweis: Algorithmus 7.2 erhöht nur die Distanzmarken von aktiven Knoten. Es genügt
daher zu zeigen, daß nie die Marke eines aktiven Knoten auf mehr als 2n − 1 erhöht wird.
Sei u ein aktiver Knoten. Nach Lemma 7.19 existiert dann ein Weg von u nach s. Dieser
Weg besteht ohne Einschränkung aus maximal n−1 Bögen (da er sonst einen Kreis besitzt).
Aus der Gültigkeit der Distanzmarken (siehe Lemma 7.17) folgt, daß d[u] ≤ (n − 1) +
d[s] = (n − 1) + n = 2n − 1 gilt.
2
7.5.2 Anzahl der Flußschübe im Algorithmus
Die Anzahl der Markenänderungen haben wir im letzten Abschnitt abgeschätzt. Wir wenden uns nun den Flußschüben zu. Dabei zeigt es sich als sinnvoll, die Flußschübe in zwei
Klassen einzuteilen.
Definition 7.21 (Sättigender und nicht-sättigender Flußschub)
Ein Flußschub in Schritt 3 von Algorithmus 7.2 heißt sättigend, wenn δ = r((u, v) δ ).
Andernfalls nennen wir den Flußschub nicht-sättigend.
Bei einem sättigenden Flußschub über (u, v)δ verschwindet (u, v)δ aus dem Residualnetzwerk Gf und der entsprechende inverse Bogen (v, u)−δ erscheint.
Lemma 7.22 Die Anzahl der sättigenden Flußschübe von Algorithmus 7.2 ist O(nm).
7.5 Präfluß-Schub-Algorithmen
141
Beweis: Sei (u, v)δ ein (potentieller) Bogen im Residualnetzwerk. Wir zeigen, daß nur
O(n) sättigende Flußschübe über (u, v)δ erfolgen. Daraus folgt dann, daß die Gesamtanzahl der sättigenden Flußschübe höchstens 2m · O(n) = O(nm) ist.
Bei einem sättigenden Flußschub über (u, v)δ gilt d[u] = d[v] + 1, da Algorithmus 7.2
nur über zulässige Bögen Fluß befördert. Nach dem sättigenden Flußschub verschwindet
(u, v)δ aus dem Residualnetzwerk und kann erst dann wieder erscheinen, wenn über den
zugehörigen inversen Bogen (v, u)−δ Fluß geschoben wird. Zu diesem Zeitpunkt muß dann
aber d0 [v] = d0 [u] + 1 gelten. Da Marken nie erniedrigt werden, haben wir d0 [v] ≥ d[u] +
1 = d[v] + 2.
Somit muß sich die Marke von v zwischen zwei sättigenden Flußschüben über (u, v) δ um
mindestens 2 erhöhen. Nach Lemma 7.20 kann dies aber maximal (2n − 1)/2 = O(n) mal
passieren, da d[v] ≤ 2n − 1.
2
Die Abschätzung der nicht-sättigenden Flußschübe ist etwas trickreicher.
Lemma 7.23 Die Anzahl der nicht-sättigenden Flußschübe von Algorithmus 7.2 ist
O(n2 m).
Beweis: Wir benutzen ein Potentialfunktionsargument ähnlich wie bei der amortisierten
Analyse in Kapitel 3. Sei I ⊆ V \ {s, t} die Menge aller aktiven Knoten und das Potential
Φ definiert als
X
Φ :=
d[v].
v∈I
Dann ist Φ nichtnegativ und vor dem Hauptteil des Algorithmus gilt Φ ≤ (n−1)(2n−1) <
2n2 = O(n2 ), da jeder der n − 1 Nachfolger von s nach Lemma 7.20 eine Marke von
höchstens 2n − 1 besitzt.
Wenn im Hauptteil des Algorithmus irgendwann Φ auf 0 fällt, so muß wegen der Nichtnegativität der Marken I = ∅ gelten. Der Algorithmus terminiert dann, da kein aktiver Knoten
mehr vorhanden ist.
Ein nicht-sättigender Flußschub über einen Bogen (u, v)δ verringert den Überschuß des
aktiven Knotens u auf 0. Möglicherweise wird v dabei aktiv. Das Potential fällt damit
um d[u] − d[v] = 1, da der Bogen (u, v)δ zulässig war und d[u] = d[v] + 1 gilt. Alle
Potentialerhöhungen werden also durch Erhöhungen von Marken oder sättigende Schübe
verursacht.
Eine Erhöhung der Marke d[u] eines aktiven Knotens u (nur solche Marken werden erhöht)
erhöht auch das Potential. Da für jeden der n − 2 potentiell aktiven Knoten die Marke nur
auf maximal 2n − 1 ansteigen, so daß die Summe aller Potentialanstiege durch Markenerhöhungen nach oben durch (n − 2)(2n − 1) = O(n2 ) beschränkt ist.
Ein sättigender Schub über einen Bogen (u, v)δ kann das Potential höchstens um d[v] ≤
2n − 1 erhöhen. Nach Lemma 7.22 finden nur O(nm) sättigende Schübe statt, so daß der
Potentialanstieg durch sättigende Schübe nach oben durch (2n − 1) · O(nm) = O(n 2 m)
abgeschätzt werden kann.
Wir haben gezeigt, daß über den gesamten Algorithmus die Summe aller Potentialanstiege
in O(n2 m) liegt. Das Ausgangspotential ist O(n2 ). Jeder nicht-sättigende Schub führt zu
einem Potentialverlust von 1, somit können insgesamt nur O(n 2 m) nicht-sättigende Flußschübe stattfinden.
2
7.5.3 Zeitkomplexität des generischen Algorithmus
In diesem Abschnitt zeigen wir, daß der Gesamtaufwand für den generischen Algorithmus 7.2 in O(n2 m) liegt. Später werden wir noch zeigen wie diese Komplexität durch
geschickte Wahl des aktiven Knotens in Schritt 10 verbessert werden kann.
142
Schnelle Algorithmen für Maximale Netz-Flüsse
Alle Operationen des Algorithmus in der Initialisierung bis einschließlich Zeile 7 sind in
O(n+m) Zeit durchführbar. Wir haben außerdem bereits Schranken von O(n 2 ) für die Anzahl der Markenerhöhungen und von O(n2 m) für die Anzahl der Flußschübe hergeleitet.
Das Unterprogramm P USH -R ELABEL wird daher also nur O(n2 + n2 m) = O(n2 m) mal
aufgerufen.
Allerdings ist nicht klar, daß wir für jeden Aufruf nur konstante Zeit benötigen (zumindest
im Durchschnitt benötigen wir diese Schranke, um bei O(n2 m) Aufrufen eine Gesamtkomplexität von O(n2 m) zu erreichen). Beispielsweise müssen wir für eine Markenänderung
bei u potentiell alle Marken für die Nachfolger von u in Gf betrachten. Außerdem müssen
wir in Schritt 1 für einen aktiven Knoten entscheiden, ob ein zulässiger ausgehender Bogen
in Gf existiert. Letztendlich stellt sich auch noch die Frage, wie wir in Zeile 10 in konstanter Zeit einen aktiven Knoten finden, bzw. feststellen, daß kein solcher Knoten vorhanden
ist.
Alle diese Probleme lösen wir, indem wir das Residualnetzwerk Gf geeignet (implizit)
speichern. Dazu speichern wir für jeden Knoten u ∈ V ähnlich wie bei der Adjazenzlistenspeicherung (siehe Abschnitt 2.1) eine Liste aller möglichen Bögen von G f , die in u
starten. Die Liste L[u] enthält also alle Bögen der Menge
{ (u, v)+ : (u, v) ∈ A } ∪ { (u, v)− : (v, u) ∈ A }.
Für jeden Bogen (u, v)δ wird im Listeneintrag zusätzlich seine Residualkapazität
r((u, v)δ ) und ein Zeiger auf den Listeneintrag des zugehörigen inversen Bogen (v, u) −δ
in der Liste L[v] abgelegt. Die Reihenfolge der Bögen in L[u] ist beliebig, wird aber
am Anfang einmal festgelegt und dann nicht mehr geändert. Die Liste L[u] enthält
−
+
−
deg+
G (u) + degG (u) Bögen, wobei deg
G (u) und degG (u) die Anzahl der in u startenden
P
bzw. endenden Bögen in G ist. Es gilt u∈V |L[u]| = 2m.
Für jede Liste L[u] halten wir noch einen Zeiger current[u] auf den »aktuellen«Listeneintrag. Zu Beginn zeigt current[u] auf den ersten Eintrag in der Liste L[u].
Für jeden Knoten u ∈ V speichern wir außer seiner Distanzmarke d[u] auch noch seinen
Überschuß e[u]. Es sollte klar sein, daß wir unsere Strukturen zum Speichern des Residualnetzwerks und der Knotendaten in O(n + m) Zeit aus dem Originalnetzwerk aufbauen
können.
Die Menge der aktiven Knoten verwalten wir in einer doppelt verketteten Liste L active .
Wir erinnern daran, daß man Elemente in einer doppelt verketteten Liste in konstanter
Zeit löschen und einfügen kann (siehe beispielsweise [3]). Ebenso kann in konstanter Zeit
getestet werden, ob Lactive leer ist.
Wird P USH -R ELABEL(u) aufgerufen, so müssen wir in Schritt 1 zunächst feststellen,
ob in u ein zulässiger Bogen startet. Dazu untersuchen wir ausgehend vom Eintrag
current[u] alle Listenelemente in L[u], bis daß wir entweder einen zulässigen Bogen
finden, oder erfolglos am Ende der Liste ankommen.
Falls wir einen zulässigen Bogen (u, v)δ gefunden haben, so setzen wir current[u] auf den
entsprechenden Listeneintrag. Da wir die Residualkapazität im Listeneintrag gespeichert
hatten und den Überschuß e[u] direkt aus dem Array ablesen können, kann dann der Wert
δ in Schritt 2 in konstanter Zeit bestimmt werden. Über den Zeiger von (u, v) δ auf den
zugehörigen inversen Bogen (v, u)−δ in L[v] können wir die Residualkapazitäten beider
Bögen ebenfalls in konstanter Zeit aktualisieren. Falls duch den Flußschub der Endknoten v
aktiv werden sollte, so fügen wir v zu Lactive hinzu. Somit ist ein Flußschub in konstanter
Zeit ausführbar. Nach Lemma 7.22 und 7.23 ist damit der gesamte Zeitaufwand für die
Flußschübe in O(n2 m).
Wenn wir erfolglos am Ende der Liste L[u] ankommen, führen wir eine Markenerhöhung
von u durch und setzen current[u] auf den ersten Eintrag in L[u] zurück (wir werden im
nächsten Absatz argumentieren, daß in diesem Fall tatsächlich kein zulässiger Bogen in u
7.5 Präfluß-Schub-Algorithmen
Algorithmus 7.3 Implementierung des Unterprogramms P USH -R ELABEL mit Hilfe der
Zeiger auf die aktuellen Bögen.
P USH -R ELABEL(u)
Input:
Ein aktiver Knoten u.
1 (u, v)δ ← current[u]
2 if d[u] = d[v] + 1 und r((u, v)δ ) > 0 then
{ Ist (u, v)δ zulässig? }
3
Falls δ = +, setze f (u, v) ← f (u, v) + δ.
Falls δ = −, setze f (v, u) ← f (v, u) − δ.
{ »Schiebe δ Flußeinheiten von u nach v« (engl. Push). }
4 else
{ d[u] ≤ d[v] oder r((u, v)δ ) = 0 }
δ
5
if (u, v) ist nicht der letzte Bogen in der Liste von u then
6
current[u] ← nächster Bogen in der Liste
7
else
{ (u, v)δ ist der letzte Bogen in der Liste von u }
8
current[u] ← erster Bogen in der Liste
9
d[u] ← 1 + min{ d[v] : (u, v)δ ∈ Gf }
{ »Erhöhe die Marke von u« (engl. Relabel). }
10
end if
11 end if
startet und wir korrekterweise eine Markenerhöhung durchführen können). Die Bestimmung des neuen Markenwerts kann durch einen vollständigen Durchlauf der Liste L[u]
von Anfang an erfolgen. Dies benötigt O(|L[u]|) Zeit. Da die Marke von u nur 2n − 1 mal
erhöht wird (siehe Lemma 7.20), fällt dieser Aufwand insgesamt nur höchstens 2n − 1 mal
an. Damit ist der gesamte Aufwand für alle Markenerhöhungen im Algorithmus nach oben
abschätzbar durch:
X
|L[u]| = (2n − 1)2m = O(nm).
(2n − 1)
u∈U
Wir müssen noch zeigen, daß wir beim erfolglosen Durchlauf der Liste L[u] ausgehend
vom Eintrag current[u] in u keine zulässigen Bögen mehr starten. Es ist natürlich trivial,
daß ab dem Eintrag current[u] keine zulässigen Bögen existieren (diese haben wir ja alle
untersucht). Was ist aber mit den Bögen vor current[u]?
Sei (u, x)δ ein Bogen, der vor current[u] in L[u] steht. Da wir current[u] irgendwann
einmal am Eintrag von (u, x)δ vorbeibewegt haben, war entweder zu diesem Zeitpunkt
r((u, x)δ ) = 0 und d[u] = d[x] + 1 oder r((u, x)δ ) > 0, aber (u, x)δ nicht zulässig.
Seit dem letzten Zeitpunkt, wo wir current[u] am Eintrag vorbeibewegt haben, hat keine
Markenerhöhung stattgefunden, da bei dieser current[u] wieder an den Anfang gesetzt
wird.
Im ersten Fall kann jetzt nur r((u, x)δ ) > 0 gelten, wenn zwischendurch ein Flußschub
über den inversen Bogen (x, u)−δ erfolgte. Dann gilt aber zu diesem Zeitpunkt d0 [x] =
d0 [u] + 1 ≥ d[u] + 1. Damit (u, x)δ wieder zulässig wird, müßte eine Markenerhöhung
von u stattfinden.
Im zweiten Fall haben wir r((u, x)δ ) > 0, aber (u, x)δ war nicht zulässig, also galt dann
d[u] ≤ d[x]. Damit (u, x)δ wieder zulässig wird, ist eine Markenerhöhung von u notwendig.
Algorithmus 7.3 zeigt die Implementierung des Unterprogramms P USH -R ELABEL mit Hilfe der Zeiger current[u] (u ∈ V ).
Wir können nun den Zeitaufwand abschätzen, der in Algorithmus 7.2 anfällt, um nach zulässigen Bögen zu suchen. Durch unseren Trick mit dem Zeiger current[u] bedingt ein
143
144
Schnelle Algorithmen für Maximale Netz-Flüsse
kompletter Durchlauf der Liste L[u] (eventuell aufgeteilt in mehrere Suchen nach zulässigen Bögen) eine Markenerhöhung von u. Nach Lemma 7.20 wird die Marke d[u] höchstens
2n−1 mal erhöht. Daher ist der Gesamtaufwand für die Suche nach zulässigen Bögen wieder abschätzbar durch
X
|L[u]| = (2n − 1)2m = O(nm).
(2n − 1)
u∈U
Damit haben wir nun folgenden Satz bewiesen:
Satz 7.24 Der generische Präfluß-Schub-Algorithmus kann so implementiert werden, daß
er in O(n2 m) Zeit einen maximalen Fluß findet.
2
7.5.4 Der FIFO-Präfluß-Schub-Algorithmus
Der Flaschenhals bei der Analyse der Laufzeit des generischen Präfluß-SchubAlgorithmus 7.2 sind die O(n2 m) nicht-sättigenden Flußschübe. Der Zeitaufwand für
alle anderen Operationen ist in O(nm), also deutlich besser. In diesem Abschnitt zeigen
wir, wie man durch etwas bessere Verwaltung der aktiven Knoten den die Flußschübe
auf O(n3 ) reduzieren können.
Wenn der generische Algorithmus 7.2 P USH -R ELABEL für einen aktiven Knoten u aufruft und einen sättigenden Flußschub ausführt, ist u möglicherweise immer noch aktiv. In
nächsten Schritt wird P USH -R ELABEL möglicherweise für einen anderen aktiven Knoten
aufgerufen.
Beim FIFO-Präfluß-Schub-Algorithmus halten wir die Menge der aktiven Knoten in einer First-in-First-Out-Schlange (FIFO-Schlange): wir entfernen aktive Knoten vom Kopf
der Schlange und fügen neue aktive Knoten hinten an die Schlange an. Wenn ein aktiver
Knoten u vom Kopf entfernt wird, dann rufen wir P USH -R ELABEL solange für u auf, bis
entweder u inaktiv oder die Marke von u erhöht wird. Im letzteren Fall wird u hinten an
die FIFO-Schlange angefügt.
Satz 7.25 Der FIFO-Präfluß-Schub-Algorithmus findet in O(n 3 ) Zeit einen maximalen
Fluß.
Beweis: Nach unseren bisherigen Ergebnissen genügt es zu zeigen, daß der Algorithmus
nur O(n3 ) nicht-sättigende Flußschübe ausführt. Wir partitionieren die Ausführung des
Algorithmus in Phasen. Phase 1 besteht aus der Bearbeitung aller Knoten, die nach der
Initialisierung in der FIFO-Schlange stehen. Phase i + 1 besteht aus der Bearbeitung aller
aktiver Knoten, die in Phase i zur FIFO-Schlange hinzugfügt werden.
Zunächst beobachten wir, daß in jeder Phase höchstens n nicht-sättigende Schübe stattfinden: bei einem nicht-sättigendem Schub in der Phase wird der aktive Knoten inaktiv. Wird
er wieder durch andere Flußschübe aktiv, so wird er durch die FIFO-Schlange in der aktuellen Phase nicht mehr betrachtet. Für jeden Knoten gibt es pro Phase also höchstens einen
nicht-sättigenden Schub.
Wir benutzen wieder ein Potentialfunktionsargument, um die Anzahl der Phasen abzuschätzen. Sei wieder I die Menge aller aktiven Knoten, dann ist unser Potential definiert durch
Φ := max{ d[u] : u ∈ I }.
Wir nennen eine Phase eine Aufstiegsphase, wenn Φ vom Start der Phase bis zum Ende der
Phase ansteigt. Ansonsten heißt die Phase eine Abstiegsphase.
7.6 Dynamische Bäume und ihr Nutzen in Flußalgorithmen
Findet in einer Phase keine Markenerhöhung statt, so wird der komplette Überschuß jedes
Knoten, der zu Beginn der Phase aktiv war, zu Knoten geschoben, die niedrigere Marken
haben. Es handelt sich also um eine Abstiegsphase.
Also kann eine Aufstiegsphase nur dann vorliegen, wenn in der Phase mindestens eine
Marke erhöht wird. Da nach Lemma 7.20 nur O(n2 ) Markenerhöhungen stattfinden, gibt
es also nur O(n2 ) Aufstiegsphasen.
Wenn wir die Potentialanstiege vom Start bis zum Ende der Phase über alle Aufstiegsphasen zusammenzählen, so erhalten wir eine obere Schranke für die Anzahl der Abstiegsphasen. Wir betrachten eine Anstiegsphase. Sei u ein Knoten mit größter Marke d 0 [u] am Ende
der Phase, also ein Knoten, der den Wert des Potentials bestimmt, und sei d[u] sein Markenwert am Anfang der Phase. Der Potentialanstieg in der Phase ist maximal d 0 [u] − d[u].
Die Summe der Potentialanstiege über alle Anstiegsphasen ist daher höchstens
der Summe
P
der Markenanstiege aller Knoten, nach Lemma 7.20 also höchstens u∈V (2n − 1) =
n(2n − 1) = O(n2 ). Daher existieren auch höchstens O(n2 ) Abstiegsphasen.
Wir haben gezeigt, daß insgesamt O(n2 ) Phasen vorliegen. Wie bereits am Anfang des Beweises bemerkt, enthält jede Phase maximal n nicht-sättigende Flußschübe. Dies beendet
den Beweis.
2
7.6 Dynamische Bäume und ihr Nutzen in Flußalgorithmen
In diesem Abschnitt benutzen wir die Datenstruktur der dynamischen Bäume, um den
FIFO-Präfluß-Schub-Algorithmus weiter zu beschleunigen. Zunächst benutzen wir die dynamischen Bäume als »Blackbox«: wir verwenden die von ihnen zur Verfügung gestellten
Operationen und die Zeitschranken ohne Beweis. In Abschnitt 7.6.3 zeigen wir dann, wie
die dynamischen Bäume durch eine Erweiterung der Schüttelbäume aus Kapitel 6 implementiert werden können.
7.6.1 Operationen auf dynamischen Bäumen
Die Datenstruktur der dynamischen Bäume verwaltet eine Kollektion von knotendisjunkten
Bäumen. Jeder Knoten v hat ein Gewicht g(v) ∈ R≥0 ∪ {−∞, +∞}. Die Bäume werden
als von unten nach oben gerichtet angesehen: für v ist p[v] der Vater von v im Baum, der v
enthält, wobei v = NULL, falls v eine Wurzel ist. Folgende Operationen werden unterstützt:
F IND -ROOT(v) liefert die Wurzel des Baums, der den Knoten v enthält.
F IND -S IZE (v) liefert die Anzahl der Knoten im Baum, der v enthält.
F IND -VALUE (v) liefert das Gewicht g(v).
F IND -M IN(v) liefert den Knoten w auf dem eindeutigen Weg von v zur Wurzel mit minimalem Gewicht g(w). Falls es mehrere solche Knoten gibt, dann liefere den Knoten,
der am dichtesten an der Wurzel ist.
C HANGE -VALUE (v, x) addiert den Wert x ∈ R zum Gewicht g(w) jedes Vorfahren von v
hinzu. Wir definieren (−∞) + (+∞) := 0.
L INK(v, w) kombiniert die Bäume, welche die Knoten v und w enthalten, indem w zum
Vaterknoten von v gemacht wird (vgl. Abbildung 7.11). Die Operation führt keine
Aktionen aus, falls v und w im bereits gleichen Baum sind oder v kein Wurzelknoten
ist.
145
146
Schnelle Algorithmen für Maximale Netz-Flüsse
C UT (v) zerschneidet den Baum, der v enthält, durch Entfernen des Bogens von v zu seinem Vaterknoten in zwei Bäume (vgl. Abbildung 7.12). Die Operation führt keine
Aktion aus, falls v ein Wurzelknoten ist.
a
c
b
g
e
a
d
f
i
c
b
h
L INK(x, d) e
d
g
f
j
i
x
y
x
h
y
z
j
z
Abbildung 7.11: Illustration der Operation L INK.
a
c
b
e
f
g
a
d
h
b
C UT(e)
c
g
e
d
f
h
i
i
j
j
Abbildung 7.12: Illustration der Operation C UT .
Satz 7.26 Startet man mit einer Kollektion von einelementigen Bäumen, so benötigt eine
Folge von ` Operationen O(` log k) Zeit, wobei k eine obere Schranke für die Größe der
während der Folge auftretenden Bäume ist.
Beweis: Siehe Abschnitt 7.6.3.
2
7.6.2 Einsatz im Präfluß-Schub-Algorithmus
Wir benutzen dynamische Bäume, um eine Teilmenge der »aktuellen Bögen« current[u]
(u ∈ V ) des Residualnetzwerkes zu speichern. Wir werden dabei nur zulässige Bögen
7.6 Dynamische Bäume und ihr Nutzen in Flußalgorithmen
147
speichern, d.h. Bögen (u, v)δ mit d[u] = d[v] + 1 und r((u, v)δ ) > 0. Allerdings werden
nicht alle zulässigen Bögen in den dynamischen Bäumen gespeichert.
Der Wert g(v) eines Knotens ist r((v, p[v])δ ), falls p[v] 6= NULL und +∞ sonst. Daher
sagen wir auch, daß die dynamischen Bäume den Bogen (v, p[v]) δ des Residualnetzwerkes
speichern. Zu Beginn des Algorithmus ist jeder Knoten v ∈ V in einem einelementigen
dynamischen Baum gespeichert.
Bevor wir die Details ausführen, soll hier bereits die Idee für den Einsatz der dynamischen Bäume angesprochen werden. Sei v ein aktiver Knoten. Dann besteht der Weg
v = v0 , . . . , vk von v zu F IND -ROOT (v) nur aus zulässigen Bögen. Die minimale Residualkapazität auf diesem Weg ist ε = F IND -M IN(v). Wir können also hintereinander
Flußschübe mit dem Betrag δ = min{ef (v), ε} auf allen Bögen (vi , vi+1 )δ ausführen. Dies
können wir mit Hilfe der dynamischen Bäume ausführen, ohne jeden dieser Bögen explizit
»anzufassen«: Es genügt nämlich, C HANGE -VALUE(v, −δ) auszuführen, um die Residualkapazitäten der betroffenen Bögen zu aktualisieren. Durch die Flußschübe verringert sich
die Residualkapazität mindestens eines Bogens (möglicherweise mehrerer Bögen) auf 0.
Diese Bögen können wir durch w = F IND -M IN (v) lokalisieren und mit C UT(w) dann aus
dem dynamischen Baum von v entfernen. Nach Satz 7.26 können wir (im wesentlichen)
jede Operation in logarithmischer Zeit ausführen. Das ist insbesondere dann von Vorteil,
wenn der Weg aus k = Ω(n) Bögen besteht.
Der Algorithmus T REE -P REFLOW-P USH
Algorithmus mit folgenden Modifikationen:
entspricht
dem
FIFO-Präfluß-Schub-
• Anstelle des Unterprogramms P USH -R ELABEL wird die auf dynamischen Bäumen
aufbauende Variante T REE -P USH -R ELABEL (Algorithmus 7.4) eingesetzt.
• Der Algorithmus T REE -P REFLOW-P USH speichert den Präfluß f auf zwei verschiedene Weisen:
– Ist für den Bogen (u, v) weder (u, v)+ noch (v, u)− in einem dynamischen
Baum gespeichert, so ist f (u, v) direkt mit (u, v) in der (erweiterten) Adjazenzliste gespeichert.
– Ist (u, v)δ in einem dynamischen Baum gespeichert, so beträgt der Flußwert f (u, v) dann g(u), wobei g(u) der in u »implizit« gespeicherte Wert
ist. Falls der Bogen (u, v)δ durch C UT (u) entfernt wird, so wird g(u) über
F IND -VALUE(u) aufgerufen und der Wert f (u, v) für den Bogen explizit in
der Adjazenzliste abgespeichert.
Das Unterprogramm T REE -P USH -R ELABEL wird nur für einen aktiven Knoten aufgerufen, der außerdem Wurzelknoten u eines dynamischen Baums ist. Wir werden in Lemma 7.28 auf Seite 149 zeigen, daß im Verlauf des Algorithmus überhaupt nur Wurzelknoten aktiv sein können. Beim Aufruf von T REE -P USH -R ELABEL unterscheidet die Prozedur
zwei Hauptfälle:
1. Der Bogen current[u] = (u, v)δ ist zulässig.
Falls die beiden Bäume, die u und v enthalten zusammen höchstens k Knoten enthalten, dann verbindet T REE -P USH -R ELABEL diese beiden Bäume, indem v zum
Vaterknoten von u wird. Man beachte, daß u und v tatsächlich in verschiedenen
Bäumen sind: es werden in den Bäumen nur zulässige Bögen gespeichert (siehe Lemma 7.27 auf Seite 149). Wäre v bereits im Baum von u, so müßte daher d[v] > d[u]
gelten. Andererseits ist (u, v)δ zulässig und d[u] = d[v] + 1.
Es erfolgt nun ein Schieben von Fluß längs des Pfades von u zur Wurzel des neuen
Baumes mit Hilfe von S END(u) (Zeilen 4 bis 7). Wir nennen das Schieben mittels
S END auch »Verschicken«.
148
Schnelle Algorithmen für Maximale Netz-Flüsse
Algorithmus 7.4 Neues Unterprogramm zum Schieben/Markenerhöhen für den PräflußSchub-Algorithmus mit dynamischen Bäumen.
T REE -P USH -R ELABEL(u)
Input:
Ein aktiver Knoten u, der zugleich Wurzel eines dynamischen Baums ist.
1 (u, v)δ ← current[u]
2 if d[u] = d[v] + 1 und r((u, v)δ ) > 0 then
{ Ist (u, v)δ zulässig? }
3
if F IND -S IZE(u) + F IND -S IZE(v) ≤ k then { Fall 1(a): Die Bäume sind klein }
4
C HANGE -VALUE(u, −∞)
5
C HANGE -VALUE(u, r((u, v)δ )
6
L INK(u, v)
{ u wird zum Sohn von v gemacht. }
7
S END(u)
8
else
{ Fall 1(b): F IND -S IZE(u) + F IND -S IZE(v) > k }
9
Falls δ = +, setze f (u, v) ← f (u, v) + δ.
Falls δ = −, setze f (v, u) ← f (v, u) − δ.
{ »Schiebe δ Flußeinheiten von u nach v« (engl. Push). }
10
S END(v)
11
end if
12 else
{ d[u] ≤ d[v] oder r((u, v)δ ) = 0 }
δ
13
if (u, v) ist nicht der letzte Bogen in der Liste von u then
{ Fall 2(a) }
14
current[u] ← nächster Bogen in der Liste
15
else
{ Fall 2(b): (u, v)δ ist der letzte Bogen in der Liste von u }
16
current[u] ← erster Bogen in der Liste
17
d[u] ← 1 + min{ d[v] : (u, v)δ ∈ Gf }
{ »Erhöhe die Marke von u« (engl. Relabel). }
18
Führe C UT(w) und C HANGE -VALUE(w, +∞) für jeden Sohn von u aus.
19
end if
20 end if
Algorithmus 7.5 Unterprogramm zum »Versenden« von Fluß für den Präfluß-SchubAlgorithmus mit dynamischen Bäumen.
S END(u)
Input:
1
2
3
4
5
6
7
8
9
Ein aktiver Knoten u.
while F IND -ROOT (u) 6= u und ef (u) > 0 do
δ ← min{ef (u), F IND -VALUE(F IND -M IN (u))}
C HANGE -VALUE(u, −δ)
{ Schiebe δ Flußeinheiten auf dem gesamten Pfad von v zur Wurzel. }
while F IND -VALUE(F IND -M IN (u)) = 0 do
w = F IND -M IN(u)
C UT(w)
{ Entferne den Bogen (w, p[w])δ , da dieser jetzt keine
Residualkapazität mehr besitzt. }
C HANGE -VALUE(w, +∞)
end while
end while
7.6 Dynamische Bäume und ihr Nutzen in Flußalgorithmen
149
Falls die beiden Bäume mehr als k Knoten besitzen, so erfolgt in Zeile 9 ein »herkömmlicher« Flußschub über den Bogen (u, v)δ gefolgt von einem Verschicken ab
dem Knoten v.
2. Der Bogen current[u] = (u, v)δ ist nicht zulässig.
In diesem Fall wird durch den Algorithmus current[u] weitergesetzt und, falls notwendig, die Marke von u erhöht. Dies entspricht bis hierhin genau der StandardImplementierung von P USH -R ELABEL (vgl. Algorithmus 7.3). Wenn die Marke
von u erhöht wird, so werden alle in v mündenden Bögen im dynamischen Baum
in Schritt 18 mittels C UT entfernt. Dadurch bleibt die Invariante bestehen, daß alle
Bögen im dynamischen Baum auch zulässige Bögen im Residualnetzwerk sind.
Lemma 7.27 Jeder Bogen (u, v)δ , der in einem dynamischen Baum gespeichert ist, ist ein
zulässiger Bogen des Residualnetzwerkes.
Beweis: Die Behauptung folgt leicht durch Induktion nach der Anzahl der Aufrufe von
T REE -P USH -R ELABEL. Zu Beginn ist kein Bogen gespeichert. Ein Bogen (u, v) δ wird
nur dann in Schritt 6 hinzugefügt, wenn d[u] = d[v] + 1. Falls die Marke eines Knotens
erhöht wird, so werden in Schritt 18 alle Bögen, die unzulässig werden könnten, entfernt.
Da außerdem noch alle Bögen mit Residualkapazität 0 aus dem Bäumen entfernt werden,
folgt die Behauptung.
2
Lemma 7.28 Ein Knoten v , der kein Wurzelknoten eines dynamischen Baumes ist, kann
nur kurzzeitig, genauer gesagt, zwischen Schritt 3 und Schritt 11 von T REE -P USH R ELABEL strikt positiven Überschuß ef (v) > 0 besitzen.
Beweis: Die Behauptung folgt wiederum durch Induktion nach der Anzahl der Aufrufe
von T REE -P USH -R ELABEL. Vor dem ersten Aufruf sind alle Knoten Wurzelknoten, die
Behauptung also trivial.
Die beiden einzigen Stellen in T REE -P USH -R ELABEL, an denen Überschuß an einem
Nicht-Wurzelknoten erzeugt werden können sind Schritt 7 sowie die Schritte 9 und 7.5.
In Schritt 7 wird S END(u) aufgerufen, bei dem möglicherweise ein Überschuß bei Knoten
auf dem Weg von u zur Wurzel z des Baums von u verbleiben könnte (u ist keine Wurzel
mehr und ist ein solcher Kandidat). Nach Konstruktion von S END wird aber in Schritt 6 für
alle Knoten w auf diesem Weg, die Ihren Überschuß nicht zum Vaterknoten p[w] weiterreichen können, der Bogen (w, p[w])δ mittels C UT(w) entfernt, so daß w ein Wurzelknoten
wird.
In Schritt 9 bleibt u Wurzelknoten, so daß die Situation bei u in Ordnung ist. Bei S END(v)
in Schritt 10 werden analog zu oben Knoten mit Überschuß zu Wurzelknoten.
2
Lemma 7.28 zeigt, daß es genügt, T REE -P USH -R ELABEL nur für Wurzelknoten aufzurufen. Nur diese können aktiv sein. Die FIFO-Schlange des Algorithmus zur Verwaltung
aktiver Knoten enthält also lediglich Wurzelknoten. Da im Algorithmus nur über zulässige
Bögen Fluß geschoben wird (vgl. hierzu auch Lemma 7.27 für die S END-Operationen),
folgt die Korrektheit des Algorithmus (mit unseren Standard-Argumenten für die PräflußSchub-Algorithmen):
Satz 7.29 Der Algorithmus T REE -P REFLOW-P USH findet einen maximalen Fluß.
2
Wir beschäftigen uns nun mit der Zeitkomplexität des Algorithmus.
Lemma 7.30 In Verlauf von Algorithmus T REE -P REFLOW-P USH werden maximal
O(nm) C UT-Operationen und O(nm) L INK-Operationen ausgeführt.
150
Schnelle Algorithmen für Maximale Netz-Flüsse
Beweis: Wir haben bereits gesehen, daß L INK nur Knoten in verschiedenen dynamischen
Bäumen verbindet. Daher kann die Anzahl der L INK-Operationen die Anzahl der C UTOperationen um maximal n − 1 übersteigen. Somit genügt es, die C UT-Operationen zu
beschränken.
C UT-Operationen werden an zwei Stellen ausgeführt: innerhalb von S END und in Zeile 18. Eine C UT-Operation innerhalb von S END wird durch einen sättigenden Schub verursacht. Da es nach Lemma 7.22 O(nm) sättigende Flußschübe gibt, werden innerhalb von
S END nur O(nm) C UT-Operationen ausgeführt. Ein C UT in Zeile 18 entspricht einer Markenerhöhung von u. Da wir nach Lemma 7.20 die Anzahl der Markenerhöhungen durch
O(n2 ) = O(nm) beschränken können, folgt die Behauptung des Lemmas.
2
Lemma 7.31 Sei h die Anzahl der Hinzufügungen von Knoten an das Ende der FIFOSchlange zur Verwaltung der aktiven Knoten. Der Algorithmus T REE -P REFLOW-P USH
benötigt O((nm + h) log k) Zeit.
Beweis: Zuerst betrachten wir die Anzahl der Aufrufe von T REE -P REFLOW-P USH. Beim
Aufruf von T REE -P REFLOW-P USH erfolgt ein L INK (Fall 1(a)), ein normaler Flußschub
(Fall 1(b)), ein Zeigerweiterrücken (Fall 2(a)) oder eine Markenerhöhung (Fall 2(b)). Nach
Lemma 7.30 tritt Fall 1(a) O(nm) mal auf. Da nach Lemma 7.20 jede Marke nur O(n) mal
erhöht wird (und dhaer jede Adjazenzliste nur O(n) mal durchlaufen wird), können wir die
Anzahl der Fälle 2(a) und 2(b) mit O(nm) und O(n2 ) = O(nm) abschätzen. Weiterhin
kann in Fall 1(b) nach Lemma 7.22 nur O(nm) mal ein sättigender Flußschub auftreten.
Bei einem nichtsättigendenden Flußschub wird der komplette Überschuß des aktiven Knotens u zu v geschoben, so daß für jeden Knoten in der FIFO-Schlange höchstens einmal diese Situation eintreten kann. Daher wird T REE -P REFLOW-P USH höchstens O(mn) + h mal
aufgerufen.
Den Zeitaufwand für einen Aufruf von T REE -P REFLOW-P USH teilen wir wie folgt auf:
1. Operationen innerhalb von S END,
2. Operationen in Fall 2(b).
3. sonstige Operationen auf dynamischen Bäumen,
4. Zeit für Markenerhöhungen,
5. sonstige elementare Operationen.
Durch Schritt 3 des Algorithmus wird sichergestellt, daß nur dann zwei dynamische Bäume
vereinigt werden, wenn der entstehende Baum höchstens k Knoten besitzt. Da im Algorithmus nur durch L INK Bäume wachsen können, folgt damit, daß jeder dynamische Baum im
Algorithmus Größe höchstens k besitzt. Das bedeutet, daß jede Operation auf dynamischen
Bäumen nur O(log k) (amortisierte) Zeit benötigt.
Jeder Aufruf von T REE -P REFLOW-P USH benötigt nur O(1) Operationen des Typs 3 und
des Typs 5. Das bedeutet insbesondere, daß über den gesamten Algorithmus nur O(nm)+h
Operationen auf dynamischen Bäumen vom Typ 3 ausgeführt werden. Wie wir in Abschnitt 7.5.3 bereits gezeigt haben, ist der gesamte Aufwand für alle Markenerhöhungen
in O(nm), somit können wir den Zeitaufwand für alle Typ 4-Operationen durch O(nm)
abschätzen.
Wir kommen nun zu den Operationen vom Typ 1. Wir können die Operationen innerhalb
von S END den dort ausgeführten C UT-Operationen zuordnen: pro C UT-Operation werden
O(1) Operationen auf dynamischen Bäumen und O(1) elementare Operationen ausgeführt.
Mit Lemma 7.30 erhalten wir eine Schranke von O(nm) für die Anzahl der Operationen
vom Typ 1.
7.6 Dynamische Bäume und ihr Nutzen in Flußalgorithmen
Die Operationen vom Typ 2 lassen sich auf ähnliche Weise den C UT-Operationen in Zeile 18 zuordnen, so daß jedem C UT nur O(1) Operationen zufallen. Lemma 7.30 liefert auch
hier die gewünschte obere Schranke von O(nm) für die Operationen vom Typ 2.
Wir haben gezeigt, daß insgesamt O(nm)+h elementare Operationen und O(nm)+h Operationen auf dynamischen Bäumen ausgeführt werden. Da jede Baumoperation O(log k)
amortisierte Zeit kostet, folgt die Behauptung des Lemmas.
2
Wir müssen jetzt noch die Anzahl h der Hinzufügungen von Knoten zur FIFO-Schlange
abschätzen. Wie im Beweis von Satz 7.25 unterteilen wir die Ausführung von Algorithmus
T REE -P REFLOW-P USH in Phasen. Phase 1 besteht aus der Bearbeitung aller Knoten, die
nach der Initialisierung in der FIFO-Schlange stehen. Phase i + 1 besteht aus der Bearbeitung aller aktiver Knoten, die in Phase i zur FIFO-Schlange hinzugfügt werden.
Bemerkung 7.32 Wie im Beweis von Satz 7.25 folgt, daß es nur O(n 2 ) Phasen gibt. Der
für Satz reftheorem:fifo-prefolow-push vorgeführte Beweis bleibt ohne Änderung auch für
T REE -P REFLOW-P USH gültig.
Lemma 7.33 Im Verlauf des Algorithmus wird O(nm + n3 /k) mal ein Knoten zur FIFOSchlange hinzugefügt.
Beweis: Es gibt genau zwei Situtationen, die bewirken können, daß ein Knoten w zur FIFOSchlange hinzugefügt wird:
(i) Die Marke des Knotens w wird erhöht.
In diesem Fall ist w = u ein aktiver Knoten, für den T REE -P USH -R ELABEL aufgerufen wurde und dessen Überschuß nicht komplett weggeschoben werden konnte. Es
wird genau ein Knoten, nämlich w = u an die FIFO-Schlange angefügt.
(ii) Der Überschuß des Knotens erhöht sich von 0 auf einen positiven Wert.
Diese Situation kann nur in Fall 1(a) und 1(b) von T REE -P USH -R ELABEL auftreten. Es wird maximal ein Knoten mehr zur FIFO-Schlange hinzugefügt, wie C UTOperationen in S END erfolgen.
Nach Lemma 7.20 tritt der Fall (i) nur O(n2 ) = O(nm) mal auf. Somit genügt es zu
zeigen, daß beim Fall (ii) insgesamt höchstens O(nm + n3 /k) Knoten zur FIFO-Schlange
hinzugefügt werden.
Bei Fall 1(a) erfolgt ein L INK-Aufruf. Nach Lemma 7.30 können wir die Anzahl der
L INK-Aufrufe nach oben durch O(nm) beschränken, so daß Fall 1(a) insgesamt O(nm) +
O(nm) = O(nm) Knoten in die Schlange stellt.
Die Knoten, die in Fall 1(b) an die FIFO-Schlange angehängt werden, ist etwas trickreicher
zu analysieren. Bei O(nm) Eintreten von Fall 1(b) wird ein C UT ausgeführt (da nach Lemma 7.30 nur O(nm) C UT-Operationen erfolgen). Außerdem kann nur bei O(nm) Eintreten
von Fall 1(b) ein sättigender Schub erfolgen (vgl. Lemma 7.22). Es verbleiben somit noch
die Situationen, in denen weder ein C UT noch ein sättigender Flußschub erfolgt. Wir nennen ein solches Auftreten von Fall 1(b) einen nicht-sättigenden b-Fall. Wir sind mit dem
Beweis fertig, wenn wir die Anzahl der nicht sättigenden b-Fälle geeignet beschränken
können.
Unsere Strategie dafür ist die folgende: wir ordnen jeden nicht-sättigenden b-Fall einer
L INK einer C UT-Operation oder einem bestimmten »großen« Baum zu (was »groß« genau heißt, definieren wir weiter unten exakt). Dabei stellen wir sicher, daß jedes Ziel nur
O(1) nicht-sättigende b-Fälle erhält. Die Anzahl der L INK/C UT-Operationen ist nach Lemma 7.30 O(nm). Ebenso werden wir die Anzahl der großen Bäume geeignet beschränken.
151
152
Schnelle Algorithmen für Maximale Netz-Flüsse
Mit Tu bezeichnen wir den dynamischen Baum, der den Knoten u enthält. Die Anzahl
seiner Knoten referenzieren wir wie üblich mit |Tu |. Der Baum Tu heißt groß, wenn |Tu | >
k/2. Ansonsten nennen wir Tu klein. Bei einem nicht-sättigenden b-Fall ist mindestens
einer der beiden Bäume groß.
Lemma 7.28 liefert uns, daß jeweils nur Wurzelknoten in der Schlange stehen, da nur solche positiven Überschuß haben. Die Bäume in der FIFO-Schlange sind knotendisjunkt.
Folglich enthält die FIFO-Schlange zu jedem Zeitpunkt maximal 2n/k große Bäume.
Wir betrachten einen nicht-sättigenden b-Fall beim Aufruf T REE -P USH -R ELABEL(u). Wie
wir bereits gesehen haben ist mindestens einer der beiden Bäume T u oder Tv groß. Nach
Konstruktion ist u Wurzel von Tu . Nach Definition des nicht-sättigenden b-Falls wird beim
Flußschub von u nach v in Zeile 9 der komplette Überschuß von u zu v geschoben. Folglich
kann ein nicht-sättigender b-Fall für jeden Knoten maximal einmal pro Phase auftreten.
1. Fall: Tu ist groß.
Falls Tu seit Beginn der Phase durch eine L INK- oder C UT-Operation verändert wurde,
so ordnen wir den nicht-sättigenden b-Fall der letzten dieser L INK -/C UT-Operationen vor
dem nicht-sättigenden b-Fall zu. Da jedes L INK einen neuen Baum und jedes C UT zwei
neue Bäume erzeugt, bekommt jedes L INK maximal einen und jedes C UT maximal zwei
nicht-sättigende b-Fälle zugeordnet. Die Anzahl der derart zugeordneten nicht-sättigenden
b-Fälle ist O(nm) für die gesamte Laufzeit.
Falls sich Tu nicht geändert hat, so ordnen wir den nicht-sättigenden b-Fall dem Baum T u
zu. Da der komplette Überschuß von u jetzt verschwindet, bekommt jeder solche Baum T u
pro Phasenur einen nicht-sättigenden b-Fall zugeordnet. Da T u groß ist, gibt ist pro Phase
maximal 2n/k große Bäume, insgesamt also 2n/k · O(n2 ) = O(n3 /k). Daher haben wir
auch O(n3 /k) nicht-sättigende b-Fälle zugeordnet.
2. Fall: Tv ist groß.
Sei r die Wurzel von Tv . Durch S END(v) wird der vollständige Überfluß von v zur Wurzel r
geschoben, da sonst ein C UT erfolgen müßte. Also wird r zur FIFO-Schlange hinzugefügt
(falls r bereits in der Schlange ist, so können wir den aktuellen Fall ignorieren). Wir ordnen
nun analog zum ersten Fall zu. Falls sich Tv seit dem Beginn der Phase geändert hat, so wird
der nicht-sättigende b-Fall dem L INK/C UT zugeordnet, das Tv zuletzt änderte. Andernfalls
ordnen wir den Fall dem großen Baum Tv zu. Man beachte, daß r nur einmal pro Phase zur
FIFO-Schlange hinzugefügt werden kann. Mit der gleichen Argumentation wie im ersten
Fall können wir die nicht-sättigenden b-Fälle durch O(nm + n3 /k) beschränken.
Dies beendet den Beweis.
2
Satz 7.34 Der Algorithmus T REE -P REFLOW-P USH bestimmt in O(nm
log k)Zeit einen
2
2
maximalen Fluß. Für k = n /m erhält man die Zeitkomplexität O nm log nm .
Beweis: Unmittelbar aus Lemma 7.31 und 7.33.
2
7.6.3 Implementierung der dynamischen Bäume
In diesem Abschnitt zeigen wir, wie wir dynamische Bäume durch eine Erweiterung der
Schüttelbäume aus Kapitel 6 implementieren können, so daß jede Operation auf einem
dynamischen Baum der Größe n nur O(log n) amortisierte Zeit benötigt.
Wir repräsentieren jeden dynamischen Baum T durch einen virtuellen Baum VT mit gleicher Knotenmenge, aber anderer Struktur. VT besteht aus einer hierarchischen Kollektion
von binären Bäumen in folgender Weise: Zusätzlich zu linkem und rechten Sohn (jeder
möglicherweise gleich NULL, also nichtexistent) hat jeder Knoten noch null oder mehr
mittlere Kinder. Wir stellen uns Kanten zwischen mittleren Kindern und den Vätern als
7.6 Dynamische Bäume und ihr Nutzen in Flußalgorithmen
»gestrichelt« vor. Der virtuelle Baum zerfällt also an den gestrichelten Kanten in eine Kollektion von durchgezogenen Bäumen. (vgl. Abbildung 7.13).
Der Zusammenhang zwischen T und VT ist wie folgt: Der Vater von v in T ist der LWRNachfolger von v im durchgezogenen Baum von VT, der v enthält. Für den gemäß LWROrdnung letzten Knoten in einem durchgezogenen Baum ist sein Vater der Vater der Wurzel
seines durchgezogenen Baums.
Wir werden jeden durchgezogenen Baum mit Hilfe eines Schüttelbaums umsetzen. Um die
Topologie des gesamten virtuellen Baumes VT zu speichern, genügt es wie bisher für jeden
Knoten u einen Zeiger p[u] auf den Vater von x und Zeiger left[u] und right[u] auf den
linken und rechten Sohn von u zu speichern. Wir können dann in konstanter Zeit feststellen,
ob u ein linker, rechter oder mittlerer Sohn seines Vaters p[u] ist: dazu vergleichen wir wir
einfach x mit left[p[u]] und right[p[u]].
Im Folgenden werden wir zur Restrukturierung eines virtuellen Baumes S PLAYOperationen in den durchgezogenen Teilbäumen ausführen. Dabei werden die mittleren
Kinder »einfach mitgenommen«. Genauer, rotieren wir so, als ob mittlere Kinder nicht
vorhanden wären in den durchgezogenen Bäumen. Da wir keine Zeiger auf mittlere Kinder,
sondern nur von den mittleren Kindern zu den Vätern halten, wandern die Kinder mit dem
Vater. Abbildung 7.14 zeigt eine Rotation unter Beibehaltunger der mittleren Kinder.
Die zweite Restrukturierungstechnik, die wir anwenden werden, ist das Vertauschen von
Söhnen. Genauer, wird dabei ein mittlerer Sohn v zum linken Sohn seines Vaters w gemacht, und der bisherige linke Sohn u wird zu einem mittleren Sohn. Abbildung 7.15 veranschaulicht diese Operation. Die Operation kann einfach durch Setzen von left[w] = v
ausgeführt werden und benötigt konstante Zeit.
Wir zeigen nun, wie wir die Gewichte g(u) für die Knoten im Baum speichern. Dazu werden wir g(u) nicht direkt bei u sondern »implizit« abspeichern. Die implizite Speicherung,
wie wir sie gleich vorstellen, hat den Vorteil, daß Aktualisierungen des Baumes, vor allem
die C HANGE -VALUE-Operation schneller vorgenommen werden können.
Für einen Knoten u bezeichnen wir mit g(u) sein Gewicht und mit m(u) das minimale
Gewicht eines Nachfolgers von u im durchgezogenen Teilbaum von u. Wir speichern nun
für jeden Knoten u die folgenden zwei Werte:
(
g(u)
falls u Wurzel eines durchgezogenen Baums ist
∆g(u) :=
g(u) − g(p[u]) sonst
∆m(u) := g(u) − m(u).
Bei der vorgestellten Speicherung können wir für jeden Knoten u die Werte g(u) und m(u)
in O(1) Zeit bestimmen. Außerdem können wir die Werte nach einer Rotation oder einer
Sohn-Vertauschung in O(1) Zeit wieder auf die korrekten Werte aktualisieren.
Wir zeigen dies für eine einfache Rotation, die beim Zick-Fall auftritt (vgl. Abbildung 6.12(a)). Die anderen Rotationen lassen sich analog behandeln. Sei u der linke Sohn
von v = p[u] und seien die Knoten a, b, c wie in Abbildung 7.14 gezeichnet. Nach der Rotation an v müssen die Daten für die Knoten u, v und b aktualisiert werden. Alle anderen
Werte werden durch die Rotation nicht berührt. Die neuen Daten ergeben sich durch:
∆g 0 (u) = ∆g(u) + ∆g(v)
∆g 0 (v) = −∆g(u)
∆g 0 (b) = ∆g(u) + ∆g(b)
∆m0 (v) = max{0, ∆m(b) − ∆g 0 (b), ∆m(c) − ∆g(c) }
∆m0 (u) = max{0, ∆m(a) − ∆g(a), ∆m0 (v) − ∆g 0 (v) }
∆m0 (b) = ∆m(b).
153
154
Schnelle Algorithmen für Maximale Netz-Flüsse
10 a
3 b
2 c
e 5
d 13
g 6
8 f
4 i
h 15
j 1
6 l
k 7
9 o p 1
n 10
m 15
r 4
12 q
2 s
12 t
8 u
3 v
3 w
(a) Der Baum T . Die Zahlen in den Knoten bezeichen die Kosten g(v).
f 8,6
l -2,2
q 6,0
-5,1 b
p 1,0
c -1,0
i -2,0
7,0 a
j 1,0
15,10 h
5,0 g
r 4,0
11,0 m
k -8,0
7,0 d o 2,0]
-10,0 e
10,0 n
v 3,1
w 0,0
9,10 t
u -4,0
-10,0 s
(b) Der virtuelle Baum VT. Die Zahlen in den Knoten bezeichnen ∆g und ∆m.
Abbildung 7.13: Ein dynamischer Baum T und ein virtueller Baum VT, der T repräsentiert.
7.6 Dynamische Bäume und ihr Nutzen in Flußalgorithmen
155
Rotation
an v
v
u
c
Z
a
b
X
Y
u
a
v
X
Y
c
b
Z
Abbildung 7.14: Rotation in einem Schüttelbaum unter Beibehaltung der mittleren Kinder.
w
u
v
w
x
v
u
x
Abbildung 7.15: Vertauschen von einem mittleren Sohn mit den linken Sohn.
Bei einer Sohn-Vertauschung wie in Abbildung 7.15 sind lediglich Werte bei den betroffenen Söhnen u und v zu aktualisieren. Die Formeln hierfür lauten:
∆g 0 (v) = ∆g(v) − ∆g(w)
∆g 0 (u) = ∆g(u) + ∆g(w)
∆m0 (w) = max{0, ∆m(v) − ∆g 0 (v), ∆m(right[w]) − ∆g(right[w])}.
Die Expose-Operation
Im virtuellen Baum VT werden wir die Operationen auf dynamischen Bäumen auf eine
abgewandelte Schüttel-Operation zurückführen. Um Mißverständnisse zu vermeiden, bezeichnen wir das Schütteln in einem durchgezogenen Baum, bei dem die mittleren Kinder
mitgenommen werden, als S PLAY und die abgewandelte Schüttel-Operation, die wir gleich
beschreiben als E XPOSE-Operation.
Wir beschreiben E XPOSE im virtuellen Baum VT als einen dreiphasigen Bottom-UpProzeß (die drei Phasen lassen sich auch zu einem kombinieren, die Darstellung ist mit
getrennten Phasen aber klarer). Sei x der Knoten, der exponiert werden soll.
In der ersten Phase folgen wir dem Pfad von x zur Wurzel von VT. Dabei schütteln wir innerhalb jedes durchgezogenen Baums wie folgt: zunächst wird x zur Wurzel seines durchgezogenen Baum geschüttelt. Sei y der resultierende Vater von x im virtuellen Baum, der
mit x durch eine gestrichelte Kante verbunden ist. Wir schütteln nun y zur Wurzel seines
durchgezogenen Baums. Dieses Verfahren setzen wir fort, bis wir an der Wurzel angelangt
sind. Nach der ersten Phase besteht der Pfad von x zur Wurzel des virtuellen Baums nur
aus gestrichelten Kanten.
156
Schnelle Algorithmen für Maximale Netz-Flüsse
In der zweiten Phase folgen wir wieder den (aktuellen) Pfad von x zur Wurzel von VT, wobei wir den aktuellen Knoten (der mittlerer Sohn seines Vaters ist) mit den linken Knoten
des Vaters vertauschen. Dabei wird der alte linke Sohn zu einem mittleren Sohn. Nach der
zweiten Phase befinden sich x und die Wurzel des virtuellen Baums im gleichen durchgezogenen Baum.
In der dritten Phase folgen wir ein letztes Mal dem Pfad von x zur Wurzel und schütteln in
der üblichen Weise x zur Wurzel. Nach der dritten Phase ist x dann Wurzel des virtuellen
Baums.
Zeitaufwand für E XPOSE
Wir analysieren nun die amortisierte Zeit für E XPOSE(v). Dabei benutzen ein Potential
analog zu Abschnitt 6.3.3. Jeder Knoten v hat Gewicht g(v) = 1, G(v) bezeichnet das Gewicht aller Knoten im Teilbaum mit Wurzel v (wobei wir hier sowohl durch gestrichelte als
auch durch durchgezogene Kanten erreichbare
P Knoten zählen) und r(v) = log 2 G(v). Das
Potential Φ(T ) eines Baums T ist dann 2 v∈T r(v). Es wird gleich klarwerden, warum
wir hier die doppelten Ränge benutzen.
Als reale Zeit zählen wir die Anzahl der ausgeführen Rotationen, die gleich der Ausgangstiefe des Knotens v ist. Analog zu Korollar 6.10 auf Seite 118 zeigt man nun das folgende
Ergebnis:
Lemma 7.35 Sei T ein durchgezogener Baum mit Wurzel root[T ] in einem virtuellen
Baum und x ein Knoten in T . Die amortisierten Kosten für die erweiterte Splay-Operation
S PLAY(T, x) betragen höchstens
1 + 6 · (r(root[T ]) − r(x))
(7.13)
wobei root[T ] der Wurzelknoten von T ist.
Falls wir jede Rotation bei den realen Kosten doppelt zählen, sind die amortisierten Kosten
immer noch höchstens 2 + 6 · (r(root[T ]) − r(x)).
Beweis: Der Beweis von Korollar 6.10 über Lemmas 6.8 und 6.9 bleibt gültig, auch wenn
mittlere Kinder vorhanden sind. Wir führen die leicht geänderten Lemmas mit den nahezu trivialen Änderungen an den Beweisen der Vollständigkeit halber nochmals auf. Die
Änderungen sind dabei durch Einkastelungen hervorgehoben.
Lemma 7.36 Die amortisierten Kosten eines einzelnen Splay-Schrittes am Knoten u, bei
dem der Zick-Fall vorliegt, betragen höchstens 1 + 6(r 0 (u) − r(u)). Falls wir jede Rotation
doppelt bei den realen Kosten zählen, sind die amortisierten Kosten höchstens 2+6(r 0 (u)−
r(u)).
Beweis: Wir bezeichnen mit r 0 die Ränge der einzelnen Knoten nach dem Splay-Schritt
und mit T 0 den Ergebnisbaum. Durch den Splay-Schritt ändern sich nur die Ränge von u
und seinem Vater v im Baum T , so daß für die Potentialdifferenz gilt:
Φ(T 0 ) − Φ(T ) = 2 (r 0 (u) + r0 (v) − r(u) − r(v))
(7.14)
Weiterhin ist r0 (u) = r(v), da beide Größen dem Logarithmus der Summe der Gewichte
aller Elemente im Baum entsprechen. Die realen Kosten für den Splay-Schritt sind gleich 1.
Somit erhalten wir aus (7.14) für die amortisierten Kosten die obere Schranke:
1 + Φ(T 0 ) − Φ(T ) = 1 + 2 (r 0 (v) − r(u))
≤ 1 + 2 (r0 (u) − r(u))
0
≤ 1 + 6 (r (u) − r(u))
(nach Lemma 6.6)
(da r 0 (u) = r(v) ≥ r(u) nach Lemma 6.6)
7.6 Dynamische Bäume und ihr Nutzen in Flußalgorithmen
157
Somit folgt der erste Teil des Lemmas. Falls wir jede Rotation doppelt in den realen Kosten
zählen, so ergeben sich offensichtlich Kosten höchstens 2 + 6(r 0 (u) − r(u)).
2
Lemma 7.37 Die amortisierten Kosten eines einzelnen Splay-Schrittes am Knoten u, bei
dem der Zick-Zick-Fall oder ein Zick-Zack-Fall vorliegt, betragen höchstens 3(r 0 (u) −
r(u)). Falls wir jede Rotation doppelt bei den realen Kosten zählen, sind die amortisierten
Kosten höchstens 6(r 0 (u) − r(u)).
Beweis: Wir betrachten als erstes den Zick-Zick-Fall. Die realen Kosten sind gleich 2 (für
zwei Rotationen) Die Ränge aller Knoten außer u, v und w bleiben unverändert. Daher sind
die amortisierten Kosten für den Splay-Schritt:
2 + Φ(T 0 ) − Φ(T ) = 2 + 2 (r 0 (u) + r0 (v) + r0 (w) − r(u) − r(v) − r(w))
= 2 + 2 (r0 (v) + r0 (w) − r(u) − r(v))
≤ 2 + 2 (r0 (v) + r0 (w) − 2r(u))
(da r 0 (u) = r(w))
(da r(u) ≤ r(v))
≤ 2 + 2 (r0 (u) + r0 (w) − 2r(u))
(da r 0 (v) ≤ r0 (u))
(7.15)
Weiterhin gilt G(u) + G0 (w) ≤ G0 (u), also haben wir
r(u) + r0 (w) = log2 G(u) + log2 G0 (w)
G(u) + G0 (w)
2
= 2r0 (u) − 2.
≤ 2 log2
(nach Lemma 6.7)
≤ 2 log2
G0 (u)
2
Aus dieser Ungleichungskette erhalten wir r 0 (w) ≤ 2r0 (u) − 2 − r(u). Setzt man diese
Ungleichung in (7.15) ein, so erhalten wir
2 + Φ(T 0 ) − Φ(T ) ≤ 2 + 2 (r 0 (u) + (2r0 (u) − 2 − r(u)) − 2r(u)) = 6 (r 0 (u) − r(u)) -2 .
Damit haben wir die erste Behauptung des Lemmas für den Zick-Zick-Fall bewiesen. Falls
wir die Rotationen doppelt bewerten, so werden die zusätzlichen Kosten von 2 bei den
realen Kosten durch die −2 in der letzten Ungleichung kompensiert.
Wir betrachten nun den Zick-Zack-Fall. Wie beim Zick-Zick-Fall ändern sich höchstens
die Ränge von u, v und w. Daher sind die amortisierten Kosten gegeben durch:
2 + Φ(T 0 ) − Φ(T ) = 2 + 2 (r 0 (u) + r0 (v) + r0 (w) − r(u) − r(v) − r(w))
= 2 + 2 (r0 (v) + r0 (w) − r(u) − r(v))
≤ 2 + 2 (r0 (v) + r0 (w) − 2r(u))
(da r 0 (u) = r(w))
(da r(v) ≥ r(u))
(7.16)
(7.17)
Es gilt nun G0 (v) + G0 (w) ≤ G0 (u). Damit folgt analog zum Zick-Zick-Fall, daß r 0 (v) +
r0 (w) ≤ 2r0 (u) − 2. Benutzt man diese Ungleichung in (7.16), so erhält man:
2 + Φ(T 0 ) − Φ(T ) ≤ 2 + 2 ((2r 0 (u) − 2) − 2r(u))
= 4 (r0 (u) − r(u)) -2
≤ 6 (r0 (u) − r(u)) -2
(da r 0 (u) ≥ r(u))
Wieder kompensiert das −2 die zusätzlichen Kosten von 2 beim doppelten Zählen der
Rotationen.
2
158
Schnelle Algorithmen für Maximale Netz-Flüsse
Lemma 7.35 folgt nun unmittelbar aus Lemma 7.36 und Lemma 7.37.
2
Mit Lemma 7.35 folgt, daß die Kosten für die erste Phase von E XPOSE(v) höchstens
6 log n + k ist, wobei k die Tiefe von v nach der ersten Phase bezeichnet. Man erhält
diese Schranke durch Summieren über die beteiligten durchgezogenen Bäume, in denen geschüttelt wird. Für das Schütteln im ersten Baum T1 (von unten gesehen), entstehen Kosten 1 + 6 · (r(root[T1 ]) − r(v)), für das Schütteln im zweiten Baum T2 dann
1 + 6 · (r(root[T2 ]) − r(y)), wobei y der Vater von v nach dem Schütteln in T1 ist.
Da r(y) ≥ r(root[T1 ]) ist, sind die Kosten für das Schütteln in T2 also höchstens
1+6·(r(root[T1 ]) − r(root[T2 ])). Die gesamte Summe ergibt dann eine Telekopsumme,
die sich auf k + 6 · (r(root[VT]) − r(v)) reduziert, wobei VT die Wurzel des virtuellen
Baums ist, die Rang höchstens n besitzt.
Die zweite Phase verändert das Potential des virtuellen Baums nicht und führt auch keine
Rotationen aus, so daß sie amortisierte Kosten 0 hat.
Man beachte, daß in der dritten Phase genau k Rotationen stattfinden. Wir schlagen k Rotationen aus der ersten Phase der dritten Phase zu, indem wir jede Rotation in der dritten
Phase doppelt zählen. Damit verringern sich die gerechneten amortisierten Kosten für die
erste Phase auf 6 log n. Aus Lemma 7.35 folgt, daß die amortisierten Kosten für die dritte Phase dann immer noch höchstens 6 log n + 2 betragen. Insgesamt haben wir somit für
E XPOSE(v) amortisierte Kosten höchstens 6 log n+6 log n+2 = 12 log n+2 = O(log n).
Lemma 7.38 Die amortisierten Kosten für E XPOSE(v) in einem virtuellen Baum mit
n Knoten sind O(log n).
2
Implementierung der Operationen mit Hilfe von Expose
Die einzelnen Operationen auf dynamischen Bäumen können wir folgt mit Hilfe der E X POSE -Operation implementiert werden:
F IND -ROOT(v) Wir führen E XPOSE(v) durch. Dann folgen wir den Zeigern für die rechten Söhne solange, bis wir beim LWR-letzten Knoten w im durchgezogenen Baum
angelangt sind. Wir führen E XPOSE(w) durch und liefern w zurück.
F IND -S IZE (v) wird dadurch implementiert, daß wir uns für jeden virtuellen Baum seine
Kardinalität merken.
F IND -VALUE (v) Wir führen E XPOSE(v) durch. Danach wird g(v) explizit geführt und
wir können den Wert g(v) zurückliefern.
F IND -M IN(v) Wir führen E XPOSE(v) durch und folgen dann den ∆g und ∆m Feldern
um zum letzten Nachfolger w von v im durchgezogenen Baum mit minimalen Kosten
zu gelangen. Wir führen E XPOSE(w) durch und liefern w zurück.
C HANGE -VALUE (v, x) Wir führen E XPOSE(v) aus, addieren x zu ∆g hinzu und subtrahieren x von ∆g(left[x]), falls left[x] 6= NULL.
L INK(v, w) Wir führen E XPOSE(v) und E XPOSE(w) durch. Danach machen wir v zu
einem mittleren Sohn von w, indem wir p[v] = w setzen.
C UT (v) Nach E XPOSE(v) addieren wir ∆g(v) zu ∆g(right[v]) und entfernen den Link
von v zu right[v], indem wir p[right[v] = NULL und right[v] = NULL setzen.
Man sieht leicht, daß alle Operationen oben O(log n) amortisierte Kosten haben, da
E XPOSE nur O(log n) amortisierte Zeit kostet.
Abkürzungen und Symbole
Abkürzungen
O. B. d. A.
Ohne Beschränkung der Allgemeinheit
Symbole
∀
∃
[a, b]
[a, b)
(a, b)
|A|
∅
N
Ω(g)
O(g)
R
Θ(g)
der Allquantor
der Existenzquantor
das geschlossene Intervall { x ∈ R a ≤ x ≤ b }
das halboffene Intervall { x ∈ R a ≤ x < b }, analog auch (a, b]
das offene Intervall { x ∈ R a < x < b }
die Kardinalität der Menge A
die leere Menge
die Menge der natürlichen Zahlen, N := {0, 1, 2, . . .}
Ω(g) := { f ∈ M | ∃c, n0 ∈ N : ∀n ≥ n0 : f (n) ≥ c · g(n) }
O(g) := { f ∈ M | ∃c, n0 ∈ N : ∀n ≥ n0 : f (n) ≤ c · g(n) }
die Menge der reellen Zahlen
Θ(g) := O(g) ∩ Ω(g)
160
Komplexität von Algorithmen
B.1 Größenordnung von Funktionen
Sei M die Menge aller reellwertigen Funktionen f : N → R auf den natürlichen Zahlen.
Jede Funktion g ∈ M legt dann drei Klassen von Funktionen wie folgt fest:
• O(g) := { f ∈ M | ∃c, n0 ∈ N : ∀n ≥ n0 : f (n) ≤ c · g(n) }
• Ω(g) := { f ∈ M | ∃c, n0 ∈ N : ∀n ≥ n0 : f (n) ≥ c · g(n) }
• Θ(g) := O(g) ∩ Ω(g)
Man nennt eine Funktion f von polynomieller Größenordnung oder einfach polynomiell,
wenn es ein Polynom g gibt, so daß f ∈ O(g) gilt.
B.2 Berechnungsmodell
Das bei der Laufzeit-Analyse verwendete Maschinenmodell ist das der Unit-Cost RAM
(Random Access Machine). Diese Maschine besitzt abzählbar viele Register, die jeweils
eine ganze Zahl beliebiger Größe aufnehmen können. Folgende Operationen sind jeweils
in einem Takt der Maschine durchführbar: Ein- oder Ausgabe eines Registers, Übertragen
eines Wertes zwischen Register und Hauptspeicher (evtl. mit indirekter Adressierung), Vergleich zweier Register und bedingte Verzweigung, sowie die arithmetischen Operationen
Addition, Subtraktion, Multiplikation und Division [8].
Dieses Modell erscheint für die Analyse der Laufzeit von Algorithmen besser geeignet als
das Modell der Turing-Maschine, denn es kommt der Arbeitsweise realer Rechner näher.
Allerdings ist zu beachten, daß die Unit-Cost RAM in einem Takt Zahlen beliebiger Größe
verarbeiten kann. Durch geeignete Codierungen können damit ausgedehnte Berechnungen
in einem einzigen Takt versteckt werden, ferner sind beliebig lange Daten in einem Takt zu
bewegen. Damit ist das Modell echt mächtiger als das der Turing-Maschine. Es gibt keine
Simulation einer Unit-Cost RAM auf einer (deterministischen) Turing-Maschine, die mit
einem polynomiell beschränkten Mehraufwand auskommt.
Um diesem Problem der zu großen Zahlen vorzubeugen, kann man auf das Modell der
Log-Cost RAM [8] zurückgreifen. Bei einer solchen Maschine wird für jede Operation ein
Zeitbedarf angesetzt, der proportional zum Logarithmus der Operanden, also proportional
zur Codierungslänge ist. Eine andere Möglichkeit, das Problem auszuschließen, besteht
darin, sicherzustellen, daß die auftretenden Zahlen nicht zu groß werden, also daß ihre
Codierungslänge polynomiell beschränkt bleibt. Diese Voraussetzung ist bei den hier vorgestellten Algorithmen stets erfüllt. Der einfacheren Analyse wegen wird daher das Modell
der Unit-Cost RAM zugrundegelegt.
162
Komplexität von Algorithmen
B.3 Komplexitätsklassen
Die Komplexität eines Algorithmus ist ein Maß dafür, welchen Aufwand an Ressourcen
ein Algorithmus bei seiner Ausführung braucht. Man unterscheidet die Zeitkomplexität,
die die benötigte Laufzeit beschreibt, und die Raumkomplexität, die Aussagen über die
Größe des benutzten Speichers macht. Raumkomplexitäten werden in diesem Skript nicht
untersucht.
Die Komplexität wird in der Regel als Funktion über der Länge der Eingabe angegeben.
Man nennt einen Algorithmus von der (worst-case-) Komplexität T , wenn die Laufzeit für
alle Eingaben der Länge n durch die Funktion T (n) nach oben beschränkt ist.
Die Komplexität von Algorithmen wird in dieser Arbeit als Funktion der Eckenzahl n und
Kantenzahl m des eingegebenen Graphen angegeben. Diese Angabe ist detaillierter als
die Abhängigkeit der Komplexität von der Eingabelänge: bei ecken- und kantenbewerteten
Graphen ist deren Codierungslänge mindestens von der Größenordnung Ω(n + m).
Besonders wichtig sind in diesem Zusammenhang die Klassen P und NP. Die Klasse P ist
die Menge aller Entscheidungsprobleme, die auf einer deterministischen Turing-Maschine
in polynomieller Zeit gelöst werden können. Entsprechend ist die Klasse NP definiert als
die Menge aller Probleme, deren Lösung auf einer nichtdeterministischen Turing-Maschine
in Polynomialzeit möglich ist. Man vergleiche dazu etwa [5].
Eine Transformation zwischen NP-Problemen heißt polynomielle Reduktion, wenn sie in
polynomieller Zeit Instanzen zweier Probleme aus NP so ineinander überführt, daß die
Antwort des Ausgangsproblems auf die Ausgangsinstanz dieselbe ist wie die Antwort
des zweiten Problems auf die transformierte Instanz. Ein Problem heißt NP-vollständig,
wenn jedes andere Problem aus NP polynomiell darauf reduziert werden kann. Der Reduktionsbegriff wird durch Einführen der Turing-Reduktion so erweitert, daß Reduktionen
zwischen Optimierungsproblemen und Entscheidungsproblemen in NP erfaßt werden. Ein
NP-Optimierungsproblem heißt dann NP-hart, wenn es von einem NP-vollständigen Entscheidungsproblem turing-reduziert werden kann.
Ein wesentliches Resultat der Komplexitätstheorie besagt, daß NP-harte Optimierungsprobleme nicht in polynomieller Zeit gelöst werden können, es sei denn, es gilt P = NP. Dies
ist der Grund, warum bei der Untersuchung von NP-harten Optimierungsproblemen auf
exakte Lösungen verzichtet wird und stattdessen Näherungen in Betracht gezogen werden.
Bemerkungen zum
Dijkstra-Algorithmus
Mit den im Skript vorgestellten Datenstrukturen für Prioritätsschlangen erhalten wir die
folgenden Laufzeiten für den Dijkstra-Algorithmus 2.1 auf Seite 8:
Datenstruktur
Array
binärer Heap
Binomial-Heap
Fibonacci-Heap
Laufzeit
O(m + n2 )
O((n + m) log n)
O((n + m) log n)
O(m + n log n)
Hierbei ist wie üblich n die Anzahl der Ecken und m die Anzahl der Kanten/Bögen im
Graphen. In diesem Anhang soll kurz darauf eingegangen werden, wie man im Falle von
ganzzahligen Kantenbewertungen alternative Zeitschranken erhält, die, je nach Anwendung, unter anderem besser sind.
In diesem Kapitel des Anhangs sei daher immer c : E → N0 bzw. c : A → N0 eine ganzzahlige nichtnegative Kanten-/Bogenbewertung. Mit C bezeichnen wir die größte auftretende Länge. Da jeder kürzeste Weg maximal n Kanten/Bögen besitzt (sonst würden Knoten wiederholt und der Weg besäße einen Kreis), hat jeder Knoten mit endlichem Abstand
von s höchstens Abstand nC.
C.1
Ganzzahlige Längen
Der ersten alternativen Implementierung einer Prioritätsschlange liegt folgende Idee zugrunde: Wir benutzen für die Schlange ein Array, wobei jeder Arrayeintrag aus einer doppelt verketteten Liste besteht, die wir Korb nennen. Der Korb L[x] im Eintrag x speichert
alle Knoten v mit Schlüsselwert d[v] = x.
Das Einfügen eines neuen Knotens u in die Schlange kann dann in konstanter Zeit erfolgen:
wir müssen u lediglich an den Korb L[d[u]] anhängen. Eine Verringerung des Schlüsselwertes kann ebenfalls in konstanter Zeit erfolgen: Wir müssen den betreffenden Konten aus
seinem aktuellen Korb entfernen und in den Korb einfügen, die seinem neuen Schlüsselwert
entspricht.
Zum Extrahieren des Minimums könnten wir das Array der Körbe von vorne nach hinten (d.h. startend vom Index 0) durchgehen und den ersten nichtleeren Korb finden. Dies
erfordert O(nC) Zeit pro E XTRACT-M IN. Dies geht allerdings viel geschickter.
Man beachte, daß die Folge der Minima der Prioritätsschlange im Dijkstra-Algorithmus
monoton wachsend ist (dies folgt unmittelbar aus der Nichtnegativität von c). Lag unser
164
Bemerkungen zum Dijkstra-Algorithmus
aktuelles Minimum, das wir gerade aus der Schlange entfernt haben, in Korb L[x], so müssen die Körbe L[0], . . . , L[x−1] leer sein und auch weiterhin leer bleiben. Zum Extrahieren
des Minimums genügt es also, sich eine Markierung k auf den Korb L[k] zu halten, der das
letzte Minimum enthielt. Beim nächsten E XTRACT-M IN starten wir bei der Suche nach
dem ersten nichtleeren Korb mit Korb L[k]. Damit reduziert sich der gesamte Aufwand für
alle O(n) E XTRACT-M IN-Operationen zusammen des Dijkstra-Algorithmus auaf O(nC).
Zusammen mit Satz 2.3 erhalten wir folgendes Ergebnis:
Beobachtung C.1 Mit Hilfe der oben beschriebenen Array-Listen-Implementierung für
die Prioritätsschlange läuft der Dijkstra-Algoirthmus in O(m + nC) Zeit und benötigt Θ(nC) Speicherplatz.
Für kleine Werte von C ist diese Implementierung unseren Heap-Implementierungen überlegen. Falls C eine (globale) Konstante ist, die nicht von der Eingabe abhängt, erhalten wir
sogar lineare Laufzeit. Allerdings sind die Speicherplatzanforderungen von Θ(nC) für die
Prioritätsschlange sehr groß (man denke etwa an den Fall C = 2n ).
Wir verfeinern jetzt die obige Konstruktion und verringern den Speicherplatzbedarf
auf O(C). Dazu benötigen wir eine weitere Beobachtung. Im Algorithmus von Dijkstra
steht am Anfang nur die Startecke s in der Prioritätsschlange Q. Diese hat den Schlüsselwert d[s] = 0. Danach werden alle Nachfolger von s eingefügt. Jeder dieser Nachfolger v
besitzt einen Schlüsselwert von maximal d[s] + c(s, v) ≤ C. Somit unterscheiden sich die
Schlüsselwerte in der Schlange nach Extrahieren von s um maximal C. Man sieht leicht,
daß dies allgemeiner gilt: Ist k = d[v] der Schlüsselwert des im zu Beginn des aktuellen
Durchlaufs der while-Schleife entfernten Minimums, so sind am Ende des Durchlaufs alle
Schlüsselwerte in der Schlange aus dem Bereich k, k + 1, · · · , k + C.
Dies bedeutet, daß aus unserem Array von nC Körben gleichzeitig immer nur C + 1 Körbe
aus einem zusammenhängenden Bereich benötigt werden. Wir organisieren die Prioritätsschlange daher als eine zyklisch verkettete Liste aus aus C + 1 Körben L[0], . . . , L[C]. Der
Korb L[k], k = 0, . . . , C, speichert die Knoten v mit Markierung k modulo C + 1:
L[k] = { v : v ist in der Schlange und d[v]
mod (C + 1) = k }.
(C.1)
Abbildung C.1 veranschaulicht die zyklische Anordnung der Körbe. Im Prinzip könnten
sich in Korb L[k] somit Einträge mit Schlüsselwerten k, k + (C + 1), k + 2(C + 1), . . .
befinden. Da sich im Verlauf des Dijkstra-Algorithmus zwei Schlüsselwerte in der Schlange
aber um maximal C unterscheiden können, wie wir oben erkannt haben, befinden sich
in L[k] nur Elemente mit gleichem Schlüsselwert.
Das Einfügen in die neue Datenstruktur sowie das Verringern von Schlüsselwerten erfolgt
ähnlich wie bei der Array-Listen-Implementierung mit dem Zusatz, daß wir modulo C + 1
rechnen müssen (vgl. (C.1)).
Zum Extrahieren des Minimums durchlaufen wir die zyklische Liste, bis daß wir den ersten
nichtleeren Korb L[k] gefunden haben. Beim nächsten E XTRACT-M IN starten wir wieder
bei L[k] und suchen weiter (ringsherum) nach dem nächsten nichtleeren Korb. Da wir nach
maximal einer »Umdrehung« das Minimum gefunden haben und wir höchstens n − 1 mal
das Minimum suchen, benötigen alle E XTRACT-M IN-Operationen wieder nur O(nC) Zeit.
Diese Implementierung ist auch als Dials Implementierung des Dijkstra-Algorithmus bekannt.
Beobachtung C.2 Mit Hilfe von Dials Implementierung läuft der Dijkstra-Algoirthmus in
O(m + nC) Zeit und benötigt Θ(n + C) Speicherplatz.
Wir können den Speicherplatzbedarf noch weiter reduzieren und dabei sogar noch die Laufzeit verringern. Wir benutzen B < C + 1 normale Körbe und einen Überlaufkorb. Wir
C.1 Ganzzahlige Längen
165
2
3
1
4
0
5
C
6
···
Abbildung C.1: Organisation der Körbe in der Implementierung von Dial.
unterteilen die Ausführung des Algorithmus in Phasen. In der iten Phase enthalten die
normalen Körbe alle Knoten mit Schlüsselwerten im Bereich [Bi , Bi + B − 1], so daß jeder Korb nur Knoten mit dem gleichen Schlüsselwert speichert. Die Knoten mit größeren
(endlichen) Schlüsselwerten sind im Überlaufkorb gespeichert. Einfügen und Verringern
von Schlüsselwerten ist wieder in konstanter Zeit möglich, da wir über den Index auf den
entsprechenden Korb zugreifen können.
Am Anfang sind wir in Phase 0. Wir setzen B0 := 0 und der Zeiger M für den ersten
nichtleeren Korb ist auf Korb 0 gesetzt. Wir extrahieren das Minimum, indem wir den ersten nichtleeren Korb suchen. Wie bei der Array-Listen-Implementierung und bei Dial’s
Implementierung starten wir dabei beim Korb, in dem wir das letzte Mal das Minimum gefunden hatten. Sobald M nun den Wert Bi + B erreicht (d.h., sobald wir unsere normalen
Körbe durchlaufen haben), beenden wir die aktuelle Phase und setzen B i+1 auf den minimalen Schlüsselwert im Überlaufkorb (dazu durchlaufen wir den Überlaufkorb einmal).
Danach verteilen wir die Elemente im Überlaufkorb wieder auf die normalen Körbe, die
jetzt den Bereich [Bi+1 , Bi + B − 1] speichern, und den Überlaufkorb.
Satz C.3 Die Implementierung von Dijkstras Algorithmus mit Hilfe der ÜberlaufkorbDatenstruktur benötigt O(m + n(C/B + B)) Zeit und Θ(n + B) Speicherplatz.
Beweis: Der gesamte Aufwand für alle I NSERT- und D ECREASE -K EY-Operationen ist in
O(n + m), da jede dieser Operationen in konstanter Zeit ausgeführt wird. Es genügt somit, den Aufwand für alle E XTRACT-M IN-Operationen und das Reorganisieren der Körbe
abzuschätzen.
In jeder Phase wird mindestens ein Knoten als Minimum aus dem Heap entfernt. Somit
existieren maximal n Phasen. In einer Phase werden die Körbe einmal durchlaufen, so
daß bis auf das Umverteilen des Überlaufkorbs, in jeder Phase O(B) Aufwand für das
Minimum-Suchen und Extrahieren anfällt. Dies ergibt O(nB) für alle Phasen zusammen.
Wenn ein Knoten das erste Mal im Überlaufkorb landet, so ist sein Schlüsselwert mindestens Bi + B (wobei i die Nummer der entsprechenden Phase ist). Da sich B i in der
nächsten Phase erhöht, kann der Knoten maximal O(C/B) mal im Überlaufkorb landen.
Daher ist der Gesamtaufwand für das Durchlaufen des Überlaufkorbs und das Umverteilen
seines Inhalts in allen Phasen zusammen in O(nC/B).
2
√
Wenn wir in Satz C.3 B = d Ce wählen, so erhalten wir folgendes Korollar:
166
Bemerkungen zum Dijkstra-Algorithmus
Korollar C.4 Die Implementierung von Dijkstras
Algorithmus mit
√
√ Hilfe der
Überlaufkorb-Datenstruktur benötigt O(m + n C) Zeit und Θ(n + C) Speicherplatz.
2
C.2
Einheitslängen: Breitensuche
Oft tritt der Fall auf, daß wir in einem ungewichteten Graphen einen (kürzesten) Weg von
einem Knoten s zu einem Knoten t (oder zu allen Knoten) finden müssen. Dieses Problem
stellt sich etwa bei einigen der Flußalgorithmen aus Kapitel 7. Falls alle Kantenlängen
(Bogenlängen) gleich 1 sind, so liefert Beobachtung C.1 bereits einen Algorithmus mit
O(n + m) Zeit und O(n + m) Speicherplatz. Es lohnt sich aber trotzdem, noch einmal
kurz über die Situation nachzudenken, da wir im vorliegenden Spezialfall eine einfachere
Datenstruktur für die Prioritätsschlange (sogar einen einfacheren Algorithmus) einsetzen
können, ohne Effizienz einzubüßen.
Wir erinnern uns daran, daß beim Dijkstra-Algorithmus die Schlüsselwerte in der Prioritätsschlange aus dem Bereich k, k +1, . . . , k +C sind, wobei k der minimale Schlüsselwert
in der Schlange und C die größte Kanten-/Bogenlänge ist. Im Fall C = 1 bedeutet dies,
daß wir nur maximal die Schlüsselwerte k und k + 1 in der Schlange haben.
Sei u der aktuell als Minimum aus der Schlange entfernte Knoten und k = d[u]. Jeder
Knoten v, der jetzt als Nachfolger von u neu in die Schlange eingefügt wird (weil vorher
d[v] = +∞ galt), wird mit Schlüsselwert d[v] + 1 = k + 1 eingefügt, also mit einem
Schlüsselwert, der mindestens so groß ist wie alle aktuellen Schlüsselwerte in der Schlange.
Jeder Knoten v, der beim Entfernen von u bereits in der Schlange war, hat bereits einen
Schlüsselwert von maximal k + 1. Also wird niemals ein Schlüsselwert erniedrigt!
Die Beobachtungen im letzten Absatz zeigen, daß wir die Prioritätsschlange als lineare
Liste verwalten können. Das Minimum ist immer das erste Listenelement. Neue Elemente
können wir einfach hinten an die Liste anfügen (dies zerstört die Ordnung nicht, da die
neuen Elementen Schlüsselwerte haben, die größer sind als alle Schlüssel in der Liste). Da
keine D ECREASE -K EY-Operationen ausgeführt werden, bleibt unsere Liste somit immer
nach Schlüsselwerten sortiert.
Den entsprechenden Algorithmus nennt man auch Breitensuche (engl. Breadth-FirstSearch). Algorithmus C.1 zeigt den Algorithmus im Pseudocode. Wir haben bereits argumentiert, daß Algorithmus C.1 korrekt arbeitet. Da jedes »E XTRACT-M IN« (Entfernen
des ersten Listenelements) und jedes »I NSERT« (Anhängen an das Ende der Liste) konstante Zeit benötigen und keine E XTRACT-M IN-Operationen benötigt werden, ist die Laufzeit
der Breitensuche linear.
Satz C.5 Der Algorithmus C.1 bestimmt korrekt die Abstände von s in einem ungewichteten Graphen. Seine Laufzeit ist O(n + m).
2
C.2 Einheitslängen: Breitensuche
Algorithmus C.1 Breitensuche (Breadth-First-Search)
B FS(G)
Input:
Ein (un-) gerichteter Graph G = (V, A) in Adjazenzlistendarstellung; ein
Knoten s ∈ V .
Output: Für jeden Knoten v ∈ V der Abstand d[v] von s zu 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 L ← {s}
{ Eine Liste, die nur s enthält. }
7 while L 6= ∅ do
8
Entferne das erste Element u aus L.
9
for all v ∈ Adj[u] do
10
if d[v] > d[u] + 1 then
11
d[v] ← d[u] + 1
12
p[v] ← u
13
Füge v an das Ende von L an.
14
end if
15
end for
16 end while
167
168
Literaturverzeichnis
[1] R. K. Ahuja, T. L. Magnanti, and J. B. Orlin, Networks flows, Prentice Hall, Englewood Cliffs, New Jersey,
1993.
[2] T. H. Cormen, C. E. Leiserson, and R. L. Rivest, Introduction to algorithms, MIT Press, 1990.
[3] T. H. Cormen, C. E. Leiserson, R. L. Rivest, and C. Stein, Introduction to algorithms, 2 ed., MIT Press,
2001.
[4] A. Fiat and G. J. Woeginger (eds.), Online algorithms: The state of the art, Lecture Notes in Computer
Science, vol. 1442, Springer, 1998.
[5] M. R. Garey and D. S. Johnson, Computers and intractability (a guide to the theory of NP-completeness),
W.H. Freeman and Company, New York, 1979.
[6] D. Jungnickel, Graphen, Netzwerke und Algorithmen, 2 ed., BI-Wissenschaftsverlag, 1990.
[7] H. Noltemeier, Graphentheorie: mit Algorithmen und Anwendungen, de Gruyter Lehrbuch, 1975.
[8] C. M. Papadimitriou, Computational complexity, Addison-Wesley Publishing Company, Inc., Reading,
Massachusetts, 1994.
[9] R. E. Tarjan, Data structures and networks algorithms, CBMS-NSF Regional Conference Series in Applied
Mathematics, vol. 44, Society for Industial and Applied Mathematics, 1983.
Документ
Категория
Без категории
Просмотров
12
Размер файла
936 Кб
Теги
3589, 001, pdf, datenstrukturen, algorithms, und
1/--страниц
Пожаловаться на содержимое документа