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