Fibonacci-Heaps und deren Anwendung

Werbung
Fibonacci-Heaps und deren Anwendung
Alexander Schiffel und Stefan Hurtz
24. Juli 2005
Inhaltsverzeichnis
1 Einleitung und Motivation
2
2 Die Datenstruktur
2
3 Amortisierte Laufzeitanalyse
3
4 Grundoperationen und deren Laufzeiten
4.1 Initialisieren (Init) . . . . . . . . . . . . . . . . . . . .
4.2 Einfügen eines Elementes (Insert) . . . . . . . . . . . .
4.3 Zugriff auf das minimale Element (GetMin) . . . . . .
4.4 Extrahieren des minimalen Elementes (ExtractMin) . .
4.5 Verschmelzen von zwei (Sub-)Fibonacci-Heaps (Merge)
4.6 Erhöhen der Priorität von Elementen (DecreaseKey) .
4.7 Löschen eines Elementes (Delete) . . . . . . . . . . . .
4.8 Zusammenfassung . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
3
3
3
3
4
4
4
4
4
5 Weitere Eigenschaften der Fibonacci-Heaps
5
6 Binomial-Queues
5
7 Vergleich mit alternativen Datenstrukturen
6
8 Anwendungen
6
1
1
Einleitung und Motivation
Es gibt viele verschiedene Implementierungen von Prioritätswarteschlangen. Alle müssen mindestens folgende Operationen unterstützen: Einfügen eines Elementes, Auffinden des Elementes mit dem minimalen
Schlüssel und Löschen dieses Elementes. Darüber hinaus werden auch oft das Löschen eines beliebigen Elementes und die Verringerung der Prioritäten verlangt. Diese ganzen Operationen werden ständig aufgerufen
und sollten deshalb möglichst effizient sein. Aus der Grundstudiumsvorlesung Datenstrukturen und Algo”
rithmen“ sind in der Regel einige Implementierungen von Prioritätswarteschlangen bekannt, zum Beispiel mit
Hilfe von Warteschlangen oder Heaps. Die in diesem Vortrag vorgestellte Datenstruktur der Fibonacci-Heaps
ist ebenfalls eine Möglichkeit der Implementierung, und zwar eine äußerst effiziente, wie im Folgenden gezeigt
wird. Sie wurde 1984 von Fredman und Tarjan erstmals beschrieben.
Was ist aber nun eigentlich ein Fibonacci-Heap? Diese Frage wird nachfolgend ausführlich beantwortet.
Eine erste Kurzbeschreibung könnte lauten:
Ein Fibonacci-Heap ist eine Datenstruktur, die aus einer Menge von heapgeordneten Bäumen“ besteht und
”
die man als effiziente Prioritätsschlange einsetzen kann.
Was unter der noch nicht präzisierten Bezeichnung Menge von heapgeordneten Bäumen“ zu verstehen ist,
”
erfährt man im folgenden Kapitel.
2
Die Datenstruktur
Ein Fibonacci-Heap ist eine Kollektion von heapgeordneten Bäumen. Diese Bäume sind allerdings anders implementiert, als man dies von gewöhnlichen Binärbäumen gewohnt ist. Heapgeordnet bedeutet, dass
Nachfolgeknoten stets einen größeren Schlüsselwert haben als ihr Vaterknoten. Vom Vaterknoten zeigt ein
Zeiger auf nur einen der Nachfolger. Die Nachfolger innerhalb einer Ebene sind dafür untereinander in einer
zyklisch geschlossenen, doppelt verketteten Liste miteinander verknüpft. Außerdem hat jeder Knoten einen
Zeiger auf seinen Vater. Darüber hinaus werden in jedem Knoten der Schlüsselwert, also die Priorität (je
niedriger desto wichtiger) sowie der Rang, d.h. die Anzahl seiner Söhne, gespeichert. Letztlich benötigt jeder
Knoten noch eine Boolsche Variable, die angibt, ob er markiert ist oder nicht. Die Markierung wird später
für eine Operation benötigt. Das, was für jeden normalen Knoten gilt, gilt natürlich auch für die Wurzeln
der einzelnen Bäume: Auch sie sind untereinander in einer zyklisch geschlossenen, doppelt verketteten Liste
verknüpft. Man benötigt nun noch einen Hauptzeiger, der auf einen Startknoten zeigt. Deshalb gibt es noch
einen Zeiger, der auf das minimale Element zeigt, das wegen der Heap-Eigenschaft in einer der Wurzeln zu
finden ist.
Abbildung 1: Beispiel eines Fibonacci-Heaps
2
Ein Knoten eines Fibonacci-Heaps ist also durch folgende Typvereinbarung charakterisiert:
type heap_ordered_tree = ^.node;
node = record
left, right: ^.node;
father, son: ^.node;
key: integer;
rank: integer;
marker: boolean;
end;
3
Amortisierte Laufzeitanalyse
Wenn man die Gesamtkosten einer Abfolge von Operationen berechnen will, erhält man durch das simple
Aufsummieren der worst-case-Kosten keine praxisrelevante Aussage.
Um diesen Missstand zu beseitigen, ist bei der amortisierten Laufzeitanalyse also nun die Idee, dass man
nicht einfach die Kosten von einer einzigen Operation betrachtet, sondern dass man sich mehrere aufeinander
folgende Operationen anschaut und die Gesamtlaufzeit geschickt auf die Einzeloperationen verteilt.
Dazu führt man für die Datenstruktur ein Bankkonto“ ein, dem sein Potenzial ( Kontostand“) bal zugeordnet
”
”
wird.
Einfache“ konstante Operationen bezahlen zusätzlich zu ihren regulären Kosten eine gewisse Anzahl von
”
Kosteneinheiten dazu, so dass das Konto“ hierbei gefüllt wird, um als Reserve für komplexe Operationen
”
herhalten zu können.
Die amortisierten Kosten ai errechnen sich dann aus den tatsächlichen Kosten ti und der Veränderung des
Potenzials für diese Operation:
ai = ti + bali − bali−1
4
Grundoperationen und deren Laufzeiten
4.1
Initialisieren (Init)
Ein leerer Fibonacci-Heap wird erzeugt, bei dem der Pointer für das minimale Element auf NULL zeigt.
(Amortisierte) Laufzeit: O(1)
4.2
Einfügen eines Elementes (Insert)
Diese Operation besteht daraus, dass das neue Element zuerst in die zyklisch verkettete Wurzelliste eingefügt wird und bei Bedarf der Pointer auf das minimale Element aktualisiert wird.
Danach müssen so lange Bäume miteinander verschmolzen werden, bis keine zwei Bäume von gleichem Grad
mehr vorkommen (siehe Merge-Operation).
(Amortisierte) Laufzeit: O(1)
4.3
Zugriff auf das minimale Element (GetMin)
Der Zugriff auf das minimale Element erfolgt einfach über die Abfrage des entsprechenden Pointers. Dabei
handelt es sich aufgrund der Heap-Eigenschaft immer um eine Wurzel.
(Amortisierte) Laufzeit: O(1)
3
4.4
Extrahieren des minimalen Elementes (ExtractMin)
Extrahieren bedeutet hier zurückgeben und löschen. Nach dem Entfernen des Minimal-Knotens wird die
Liste der Söhne des Minimalelements zunächst an die Wurzelliste angehängt. Als nächster Schritt müssen
wieder zwei Bäume gleichen Ranges verbunden werden, bis nur noch Bäume mit paarweise verschiedenem
Rang vorhanden sind (siehe Merge-Operation).
Schließlich muss natürlich noch der Pointer für das Minimalelement auf das nun kleinste Element in der
Wurzelliste gesetzt werden.
Amortisierte Laufzeit: O(log n)
4.5
Verschmelzen von zwei (Sub-)Fibonacci-Heaps (Merge)
Um zwei Fibonacci-Heaps miteinander zu verknüpfen, werden einfach die beiden Wurzellisten aneinander
gehängt und der Minimum-Pointer auf das kleinere Minimumelement der beiden Heaps gesetzt.
(Amortisierte) Laufzeit: O(1)
4.6
Erhöhen der Priorität von Elementen (DecreaseKey)
Erhöhung der Priorität eines Elementes bedeutet das Erniedrigen des Schlüsselwertes. Hierbei kann es passieren, dass die Heapordnung des Baumes zerstört wird, da unter Umständen der Schlüsselwert des Knotens
kleiner als der seines Vaters wird.
Wenn dies der Fall ist, wird der Knoten nach Herabsetzen des Schlüssels von seinem Vater abgetrennt und
zusammen mit allen seinen Nachkommen in die Wurzelliste eingefügt.
Der Vaterknoten - falls er keine Wurzel ist - wird nun markiert, wodurch - je nachdem ob dieser Knoten
schon markiert war -, dieser wiederum auch komplett in die Wurzelliste verschoben wird. Dessen Vater muss
dann wiederum markiert werden, so dass es passieren kann, dass eine ganze Reihe von Abtrennungen erfolgen
muss, was man als cascading cuts bezeichnet.
Amortisierte Laufzeit: O(1)
4.7
Löschen eines Elementes (Delete)
Falls das Element der minimale Knoten ist, wird die ExtractMin-Operation durchgeführt und man ist fertig.
Ist dies nicht der Fall, wird es mitsamt seiner eventuellen Nachfolger in die Wurzelliste eingehangen. Hierbei
muss der eventuelle Vater markiert werden, was weitere cascading cuts nach sich ziehen kann.
Schließlich wird noch das eigentlich zu löschende Element aus der Wurzelliste entfernt und seine Söhne der
Wurzelliste hinzugefügt.
Amortisierte Laufzeit: O(log n)
4.8
Zusammenfassung
Hier eine Übersicht über die Laufzeiten:
Operationen
Insert
Merge
GetMin
ExtractMin
DecreaseKey
Delete
worst case
O(1)
O(1)
O(1)
O(n)
O(log n)
O(log n)
4
amortisiert
O(1)
O(1)
O(1)
O(log n)
O(1)
O(log n)
5
Weitere Eigenschaften der Fibonacci-Heaps
Es bleiben ein paar Fragen: Warum heißt diese Datenstruktur Fibonacci-Heap? Kann man eine Aussage
darüber treffen, ob und wie der maximale Grad eines Fibonacci-Heaps beschränkt ist? Dazu betrachte die
folgenden Aussagen.
(5.1) Lemma Sei Sk die minimale Anzahl von Knoten in einem Teilbaum, dessen Wurzel Grad i hat. Dann
gilt:
S0 = 1,
S1 ≥ 2 und
Si ≥ 2 + S0 + S1 + ... + Si−2 für i ≥ 2.
(5.2) Lemma
Sei Fi die i-te Fibonacci-Zahl, also

0

1
Fi =

Fi−1 + Fi−2
falls i = 0
falls i = 1
falls 1 ≥ 2
(Die Folge der Fibonacci-Zahlen lautet: 0,1,1,2,3,5,8,...)
Dann gilt: Für i ≥ 0 ist Si ≥ Fi+2 .
Das ist der Grund, warum die Fibonacci-Heaps ihren Namen haben.
(5.3) Satz
Für den maximalen Grad dmax eines Knotens in einem Fibonacci-Heap gilt:
dmax ≤ logΦ n + O(1),
wobei
√
1+ 5
≈ 1, 61
Φ=
2
der Goldene Schnitt“ ist (bzw. dessen Kehrwert).
”
Der maximale Grad eines Fibonacci-Heaps ist also logarithmisch beschränkt.
6
Binomial-Queues
Fibonacci-Heaps sind eng verwandt mit Binomial-Bäumen und Binomial-Queues und verhalten sich ähnlich. Sie strukturieren sich unter bestimmten Umständen (wenn nur bestimmte Operationen genutzt werden)
von selbst zu Binomialbäumen, ohne dass dies explizit gefordert ist.
Eine Binomial-Queue besteht aus einer Ansammlung von Binomialbäumen und ist ebenfalls eine recht
effiziente Form einer Prioritätswarteschlange, lediglich das Verringern der Prioritäten erfordert eine höhere
Laufzeit. So gesehen könnte man Fibonacci-Heaps auch als Verbesserung oder Erweiterung von BinomialQueues bezeichnen, auch wenn die Definitionen nicht direkt aufeinander aufbauen. Deshalb hier ein kleiner
Exkurs in diese verwandte Datenstruktur.
Ein Binomialbaum ist induktiv definiert, wie aus Abbildung 2 ersichtlich ist.
(6.1) Lemma Bi hat 2i Knoten. Die Anzahl der Knoten in Tiefe t ist
von Bi Grad i, und kein anderer Knoten hat einen so hohen Grad.
i
t
. Insbesondere hat die Wurzel
(6.2) Definition Eine Menge von heap-geordneten Binomialbäumen, von denen keine zwei den gleichen
Grad haben, heißt Binomial-Queue.
(6.3) Beispiel Eine Binomial-Queue, die aus B0 , B1 und B2 besteht, kann also 20 + 21 + 22 = 1 + 2 + 4 = 7
Elemente aufnehmen.
5
Abbildung 2: Induktive Definition eines Binomialbaumes
Eine Binomial-Queue unterstützt natürlich als Prioritätswarteschlange genau wie ein Fibonacci-Heap alle
dazu notwendigen Operationen. Um diese zu implementieren, benötigt man auch das Verschmelzen von zwei
Binomial-Queues.
Auf die Laufzeiten der Binomial-Queues wird im Vortrag auch beim Vergleich der Datenstrukturen (Kapitel
7) eingegangen.
Fibonacci-Heaps sind den Binomial-Queues nicht nur ähnlich, sondern es gilt insbesondere auch folgendes:
Wenn man auf einen anfangs leeren Fibonacci-Heap nur die (im Folgenden noch näher erläuterten) Operationen Insert, FindMin, Merge und ExtractMin anwendet, so haben die einzelnen Bäume aus der Wurzelliste des
Fibonacci-Heaps immer die Struktur von Binomial-Bäume. Und am Ende einer DeleteMin-Operation bildet
der gesamte Fibonacci-Heap sogar eine Binomial-Queue.
7
Vergleich mit alternativen Datenstrukturen
Nachdem nun die Laufzeiten analysiert sind, ist natürlich interessant, diese mit denen anderer Implementierungen von Prioritätswarteschlangen zu vergleichen.
Insert
GetMin ExtractMin DecreaseKey
unsortierte Liste
O(1)
O(n)
O(n)
O(n)
sortierte Liste
O(n)
O(1)
O(1)
O(n)
binärer Suchbaum O(log n) O(log n)
O(log n)
O(log n)
binärer Heap
O(log n)
O(1)
O(log n)
O(log n)
Binomial-Queue*
O(1)
O(log n)
O(log n)
O(log n)
Fibonacci-Heap*
O(1)
O(1)
O(log n)
O(1)
*zum Teil amortisiert (sonst herkömmlicher“ worst case)
”
8
Delete
O(n)
O(n)
O(log n)
O(log n)
O(log n)
O(log n)
Anwendungen
Im Vortrag werden mit dem Dijkstra- und dem Prim-Algorithmus zwei Beispiel-Algorithmen betrachtet,
in denen Fibonacci-Heaps als effiziente Prioritätswarteschlangen eingesetzt werden können. Beim DijkstraAlgorthmus erreicht man dadurch eine Laufzeit in O(|E| + |V | log |V |), der Prim-Algorithmus verbessert sich
auf O(|V | log |V | + |E|)
6
Literatur
[Corm]
Cormen, Leierson, Rivest, Stein: Introduction to Algorithms. MIT Press 2001
[Ottm]
Ottmann, Wittmayer: Algorithmen und Datenstrukturen, 3. Auflage, Spektrum Lehrbuch
[Web1]
http://infos.aus-germanien.de/Fibonacci-Heap
[Web2a] http://wwwmayr.informatik.tu-muenchen.de/skripten/ead ws9899 html
[Web2b] http://wwwmayr.informatik.tu-muenchen.de/skripten/ead ws9898 chap1.ps
[Web2c] http://wwwmayr.informatik.tu-muenchen.de/skripten/jensernst.ps
[Web3]
http://www.mathematik.uni-kl.de/˜krumke/Teaching/SS2004/proseminar/Data/fibonacci.pdf
[Web4]
http://www.mathematik.uni-marburg.de/˜tick/Pages/Lehre/EA1/heapsd.pdf
[Web5]
http://www.mpi-sb.mpg.de/˜da98/skript.ps.gz
[Web6]
http://ad.informatik.uni-freiburg.de/bibiothek/diplom/lauer.pdf
[Web7]
http://en.wikipedia.org/wiki/Fibonacci heap
7
Herunterladen