Fully dynamic algorithms for the single source shortest path problem. Michael Baur Wintersemester 2001/2002 Zusammenfassung Im folgenden Paper werde ich Algorithmen für das dynamische Kürzeste-Wege-Problem mit ausgezeichentem Startknoten (single source shortest path, SSSP) vorstellen, die von Daniele Frigioni, Alberto Marchetti-Spaccamela und Umberto Nanni in [1] veröffentlicht wurden. Die Algorithmen ermöglichen das Ändern von Kantengewichte sowie das Einfügen und Löschen von Kanten und benötigen linearen Speicherplatz. Die Laufzeit wird in Abhängigkeit von der Anzahl der Ausgabeänderungen angegeben und liegt für Graphen mit einer k-beschränkten Besitzfunktion in O(k log n) pro Ausgabeänderung im worst-case, falls nur Änderungen der Kantengewichte zulässig sind Die gleiche amortisierte Laufzeit wird erreicht, falls auch des Löschen und Hinzufügen von Kanten erlaubt ist. Für viele Klassen von Graphen, z.B. mit beschränktem Grad oder beschränkter Pagenumber, bedeutet dies eine Laufzeit in O(log n). 1 Einleitung Das Finden kürzester Wege in einem gewichteten Graphen ist ein grundlegendes und häufig untersuchtes Problem der Informatik. Es taucht in vielen praktischen Anwendungen auf und ist eng verknüpft mit einer Reihe weiterer interessanter Probleme ([2] beschreibt viele theoretische und praktische Anwendungen des Kürzeste-Wege-Problems). Die Laufzeit des besten bekannten Algorithmus zur Lösung dieses Problems auf gerichtete Graphen mit n Knoten und m Kanten ist in O(m + n log n) (Dijkstras Algorithmus mit Fibonacci-Heaps als Knotenspeicher [3], [4]). Auf ungerichteten Graphen ist die von Thorup [5] vorgeschlagene Version mit einer Laufzeit in O(m) die schnellste. In diesem Paper betrachten wir die (vollständig) dynamische Version des Kürzesten-Wege-Problems mit einem ausgezeichneten Startknoten (single source shortest paths, SSSP). Das Problem besteht darin, die Informationen über die kürzesten Wege wieder herzustellen, nachdem sich der Graph geändert hat. Die Informationen sollen dabei nicht komplett neu erstellt werden, sondern möglichst von der älteren Version übernommen werden. Die häufigsten Änderungen an einem Graphen, die in diesem Zusammenhang vorgenommen werden, sind Erhöhen und Verringern der Kantengewichte, Hinzufügen und Entfernen von Kanten und Einfügen und Löschen von isolierten Knoten. Wenn eine beliebige Folge dieser Operationen auf einem Graphen erlaubt sein soll, bezeichnet man das Problem als (vollständig) dynamisch. Die dynamische Version des SSSP Problems spielt in vielen realen Anwendungen eine Rolle, etwa bei der Verwaltung sich ändernder Kommunikationsnetzwerke wie dem Internet. Das Entfernen einer Kante kann in diesem Beispiel den Ausfall einer Netzwerkverbindung repräsentieren, während das Verringern eines Kantengewichts die Erhöhung der Bandbreite einer existierenden Verbindung bedeuten könnte. 1.1 Frühere Ergebnisse In der Literatur sind bereits einige Arbeiten über das dynamische SSSP Problem vorhanden. Die bisher bekannten Algorithmen sind allerdings entweder nur theoretische Lösungen, da sie zu komplex für reale 1 Anwendungen sind, funktionieren nur für bestimmte Klassen von Graphen oder erlauben nur einen Teil der Änderungsoptionen, z.B. nur das Verändern von Kantengewichten. Bisher ist keine Löung für die vollständig dynamische Version des Problems auf beliebigen Graphen bekannt, die asymptotisch besser ist, als alle Information komplet neu zu berechnen. 1.2 Resultate Die in diesem Paper vorgestellten Algorithmen verwalten den zu einem Graphen G mit n Knoten und m Kanten gehörenden Kürzesten-Wege-Baum bezüglich eines Startknotens s, wobei eine beliebige Folge der oben beschriebenen Änderungsoperationen erlaubt ist. Anfragen werden in optimaler Zeit beantwortet, d.h. die Distanz eines Knotens von s wird in konstanter Zeit geliefert, der zugehörende kürzeste Weg in O(l), wobei l die Länge dieses Weges ist. Die Laufzeit der Änderungsoperationen wird in Abhängigkeit der Anzahl der Ausgabeänderungengemessen, unter Benutzung einer k-beschränkten Besitzfunktion für die Kanten von G (eine genaue Definition dieser Begriffe folgt im nächsten Kapitel). • Sind nur Änderungen der Kantengewichte zugelassen, liegt die worst-case Laufzeit des Algorithmus in O(k log n). • Bei beliebigen Folgen von Änderungen beträgt die amortiesierte Laufzeit O(k log n), wenn für jeden der durch die Änderungen entstehende Graphen eine k-beschränkte Besitzfunktion existiert. Für viele Klassen von Graphen bedeutet dies eine Laufzeit in O(log n), da für sie k konstant ist (siehe Kapitel 2.4). Die Algorithmen sind nicht nur Laufzeiteffizient, sondern auch einfach und effizient zu implementieren, da sie als Datenstrukturen nur Listen und Priority Queues benötigen. 1.3 Inhalt Im zweiten Kapitel werden die Grundlagen für die Algorithmen besprochen, d.h. Notation, Laufzeitmessung und Besitzfunktion. In Kapitel drei werden die Algorithmen für das Verändern der Kantengewichte ausführlich besprochen, da sie danach im vierten Kapitel für den vollstandig dynamischen Fall erweitert werden. 2 2.1 Grundlagen Notation Im folgenden werden die in der Graphentheorie üblichen Bezeichnungen benutzt. Sei G = (V, E) ein gewichteter ungerichteter Graph mit n := |V | Knoten und m := |E| Kanten und der Kantengewichtsfunktion w : E → R>0 , d.h. G hat positive Kantengewichte. Da wir das SSSP betrachten, benötigen wir außerdem einen Startknoten s und einen Kürzesten-WegeBaum (KWB) T (s) = (Vs , Es ) von diesem Startknoten aus. Für v ∈ V sei d(v) die Distanz zu s in T und V or(v) der Vorgänger in T . Auserdem bezeichne T (v) = (Vv , Ev ) den Teilbaum von T mit Wurzel v. 2.2 Dynamische Graphen Wir betrachten die vollständig dynamische Version des SSSP Problems, d.h. wir wollen den KürzestenWege-Baum von s und damit auch die Distanzen von s zu allen anderen Knoten berechen, auch wenn sich die Struktur von G ändert. Konkret bedeutet dies, dass die folgenden Operationen auf dem Graphen erlaubt sind: 2 • distance(v): liefert die aktuelle Distanz von s nach v, • min-path(v): liefert einen kürzesten Weg zwischen s und v, • insert(v, w, ²): fügt eine neue Kante {v, w} mit Gewicht ² in G ein, • delete(v, w): löscht die Kante {v, w}, • increase(v, w, ²): erhöht das Gewicht der Kante {v, w} um ², • decrease(v, w, ²): verringert das Gewicht der Kante {v, w} um ². Nach jeder Änderung an einer Kante muss ein neuer Kürzesten-Wege-Baum berechnet werden. Dabei soll nicht der komplette Baum von Grund auf neu erstellt werden, sondern nur die Teile, in denen sich etwas geändert hat. Das bedeutet für unser Problem, dass möglichst nur Knoten, die entweder ihre Entfernung von v oder ihren Vorgänger in T (s) geändert haben, bearbeitet werden. Aus diesem Grund werden wir zuerst eine von der üblichen asymptotischen Laufzeitangabe, die in Abhängigkeit von der Größe der Eingabe erfolgt, abweichende Methode einführen. 2.3 Laufzeit in Abhängigkeit von der Ausgabekomplexität Wir werden die Laufzeit der weiter unten vorgestellten Algorithmen in Abhängigkeit von der Anzahl der nach einer Operation nötigen Änderungen an der Ausgabe angeben. Im Fall des dynamischen SSSP Problems bedeutet dies, die Laufzeit pro Knoten, der entweder seinen Vorgänger im Kürzeste-Wege-Baum oder seine Distanz zu s ändert, anzugeben. Diese Laufzeit wird in der bekannten asymptotischen Notation angegeben. Es ist sinnvoll, die Laufzeit eines dynamischen Algorithmus auf diese Art zu messen, da eine Operation mit der gleichen Anzahl von Eingabeparametern zu völlig verschiedenen Laufzeiten führen kann. Das Verringern eines Kantengewichts zum Beispiel kann konstante Laufzeit benötigen, wenn die Kante eine Nichtbaumkante war und bleibt, aber genauso zu einer Änderung weiter Teile des Kürzeste-Wege-Baum führen, wenn sie eine Baumkante mit großem Unterbaum war. 2.4 Besitzfunktion (Accounting function) Für die Laufzeitbetrachtung werden wir für die Graphen eine durch k beschränkte Besitzfunktion benötigen. Definition 2.1 Sei G = (V, E) ein Graph. Eine Besitzfunktion B : E → V für G ordnet jeder Kante (v, w) ∈ E einen ihrer Endknoten zu, den Besitzer von (v, w). Die Menge B −1 (v) = {(v, w) ∈ E|B(v, w) = v} heisst Besitz(v), {(v, w) ∈ E|B(v, w) 6= v} heisst N ichtbesitz(v). G hat eine durch k beschränkte Besitzfunktion, wenn für alle v ∈ V die Kardinalität von Besitz(v) höchstens k ist. k k k k Beispiele für k-beschränkte Besitzfunktion für bestimmte Klassen von Graphen sind: √ = O( m) für allgemeine Graphen ≤3 für planare Graphen ≤g für Graphen mit maximalem Grad g ≤a für Graphen mit arboricity a Das minimale k, so dass G eine k-beschränkte Besitzfunktion hat, kann in polynomialer Zeit berechnet werden. 3 3 Algorithmen zur Änderung der Kantengewichte In diesem Kapitel werde ich Algorithmen für den Fall vorstellen, dass nur das Ändern der Kantengewichte als Operation zugelassen ist. Diese werden im nächsten Kapitel für das Hinzufügen und Entfernen von Kanten modifiziert. Sei also o.E. G in diesem Kapitel ein zusammenhängender Graph. Um die Arbeitsweise der Algorithmen genauer betrachten zu können, werde ich weiter unten die folgenden Bezeichnungen verwenden: zu einem Knoten v ∈ V ist d0 (v) die Distanz von s nach v nach Durchführung einer Operation und D(v) die Distanz, die der zugehörige Algorithmus berechnet hat. Die Algorithmen orientieren sich am Algorithmus von Dijkstra. Die Knoten werden in einer Priority Queue gespeichert, um sie in nicht-absteigender Reihenfolge ihrer Distanzen zu s zu bearbeiten. Wie bei Dijkstra wir ein Knoten höchsten einmal in diesen Knotenspeicher eingefügt, und erst dann aus ihm entfernt, wenn er seine endgültige Distanz zugewiesen bekommen hat. Da wir nur die Teile des Kürzeste-Wege-Baum betrachten wollen, in denen sich nach einer Operation etwas geändert hat, werden im Gegensatz zu Dijkstra beim Entfernen eines Knotens aus dem Knotenspeicher nicht alle seine Nachbarn betrachtet, sondern nur eine Teilmenge davon. Um einen Indikator für zu betrachtende Nachbarknoten zu erhalten, treffen wir folgende Definitionen: 3.1 Backward level und Forward level Definition 3.1 Sei G = (V, E) ein gewichteter Graph. Der Backward level bzw. der Forward level einer Kante (v, w) und des Knotens w relativ zum Knoten v ist Bv (w) = d(w) − ω(v, w) bzw. Fv (w) = d(w) + ω(v, w). Die Idee hinter diesen Definitionen ist, dass die Level einer Kante (v, w) Informationen über den kürzest möglichen Pfad von s nach w, der über v führt, liefern. Betrachten wir das Verringern eines Kantengewichts genauer. Nehmen wir an, der Knoten v hat nach einer solchen Operation seine Distanz verringert und es existiert eine Kante (v, w) mit d 0 (v) < Bv (w). Also gilt d0 (v) < d(w) − ω(v, w) und damit d0 (v) + ω(v, w) < d(w), was bedeutet, dass es einen kürzeren Pfad von s nach w über v gibt, als den ursprünglichen kürzesten Pfad. Wenn wir die Kanten (v, wi ) in nicht-aufsteigender Folge ihrer Backward level betrachten, stellen wir sicher, dass nur solche Kanten bearbeitet werden, bei denen auch wi die Distanz verringert. Eine ähnliche Funktion übernimmt der Forward level bei der Erhöhung eines Kantengewichts. Nehmen wir an, dass eine solche Operation auf eine Kante des kürzesten Pfades von s nach v angewandt wurde und sei l die Länge, die dieser Pfad nun besitzt. Wenn eine Kante (v, w) mit l > Fv (w) existiert, so gilt l > d(w) + ω(v, w) und damit existiert ein kürzerer Pfad von s nach v, der über w führt. Betrachten wir die Kanten (v, wi ) in nicht-absteigender Folge ihrer Forward level, stellen wir sicher, dass nur Kanten bearbeitet werden, die auch zu einem kürzeren Pfad für die wi führen. 3.2 Datenstrukturen und k-beschränkte Besitzfunktion Wenn ein Knoten bearbeitet wird, kann es passieren, dass alle seine adjazenten Kanten betrachtet werden. Im worst-case ist ihre Anzahl n mal größer als die Anzahl der Ausgabeänderungen. Betrachten wir nun eine durch k beschränkte Besitzfunktion für G wie in 2.4 beschrieben. Für alle v ∈ V speichern wir nun die adjazenten Kanten, die sich in N ichtbesitz(v) befinden, folgendermaßen: • Bv ist eine absteigend sortierende Priority Queue, in der die Kanten (v, wi ) (die sich im Besitz von wi befinden) mit der Priorität des Backward level Bv (wi ) gespeichert werden, und • Fv ist eine aufsteigend sortierende Priority Queue, in der die Kanten (v, wi ) (die sich im Besitz von wi befinden) mit der Priorität des Forward level Fv (wi ) gespeichert werden. 4 Wenn wir im Laufe des Algorithmus einen Knoten v aus dem Knotenspeicher entfernen und seine Nachbarknoten wi in der Reihenfolge ihrer Position in Bv bzw. Fv betrachten, können wir diesen Vorgang abbrechen, sobald zum ersten Mal für einen Knoten wi keine Verbesserung eintritt. Um einzusehen, warum dann insgesamt nur die behauptete Laufzeit erreicht wird, sehen wir uns die Algorithmen für das Verringern und Erhöhen eines Kantengewichts getrennt an. 3.3 Verringern eines Kantengewichts (Decrease) Nehmen wir an, v und w seien zwei Knoten des Graphen (o.E. d(v) ≤ d(w)) und das Gewicht der Kante (v, w) wurde um ² ≥ 0 verringert. Wenn w seine Distanz nicht verringert, kann dies auch kein anderer Knoten tun, der Kürzeste-Wege-Baum ändert sich nicht und der Algorithmus endet. Falls w seine Distanz verkleinert (d0 (w) < d(w)), verringern auch alle Knoten im Unterbaum T (w) von w ihre Distanz zu s (und zwar um den gleichen Betrag wie w). Außerdem können zum Unterbaum T 0 (w) Knoten hinzukommen, die nicht in T (w) enthalten waren, deren kürzester Pfad nach s nun aber die Kante (v, w) enthält. Die Anzahl der Ausgabeänderungen ist in diesem Fall die Anzahl der Knoten in T 0 (w). Betrachten wir nun einen Knoten z, der seine Distanz zu s verringert hat. Ein Nachbarknoten x i von z soll genau dann betrachtet werden, wenn eine der folgenden zwei Bedingungen zutrifft: • die Kante (z, xi ) gehört dem Knoten z; • die Kante (z, xi ) gehört dem Knoten xi und es gilt Bz (xi ) > d0 (z) (⇒ d(xi ) > d0 (z) + ω(z, xi ) ). Durch die Sortierung der Priority Queue stellen wir sicher, dass nur solche Nachbarn von z betrachtet werden, die tatsächlich ihre Distanz verkleinern. Außerdem werden alle Knoten im Unterbaum T (w) bearbeitet, da eine der beiden Bedingungen für jede Kante des Unterbaums erfüllt ist. Betrachten wir nun den Pseudocode des Algorithmus Weight-Decrease (siehe Abb. 1). Im ersten Schritt wird zuerst überprüft, ob die Gewichtsänderung zulässig ist. Die Funktion U pdateLocal(v, w) überprüft, welchem der Endknoten die Kante (v, w) gehört und aktualisiert ihre Einträge in den Priority Queues Bv und Fv oder Bw und Fw . Falls der Knoten w seine Distanz verringert hat, wird er in den Heap C eingefügt und eine Schleife wie im Algorithmus von Dijkstra gestartet (Schritt 2). Ansonsten endet der Algorithmus. Schritt zwei berechnet einen neuen Kürzeste-Wege-Baum auf ähnliche Art wie der Algorithmus von Dijkstra. Solange der Knotenspeicher C nicht leer ist, wird der Knoten mit minimaler Priorität entnommen (und z genannt) und ein Teil seiner Nachbarknoten betrachtet. Ein Nachbarknoten x wird genau dann betrachtet, wenn eine der beiden oben beschriebenen Bedingungen erfüllt ist. Die Kanten, für die die zweite Bedingung erfüllt ist, werden gefunden, indem solange der Knoten mit maximaler Priorität aus Bz entnommen wird, bis die Bedingung zum ersten mal nicht mehr erfüllt ist. Wenn die betrachtete Kante (z, x) dem Knoten x gehört, werden die entsprechenden Einträge in Bx und Fx aktualisiert. Falls die Entfernung von s nach x über z kleiner ist, als der bisher gefundene - dies ist der Fall, falls D(x) > D(z) + ω(z, x) gilt - werden die kürzeste Wege Informationen von x auf diesen Pfad gesetzt und x wird in den Knotenspeicher C eingefügt oder, falls er schon enthalten ist, seine Priorität auf die neue Distanz aktualisiert. 3.3.1 Korrektheitsanalyse Die Beweis läuft analog zum Beweis der Korrektheit des Algorithmus von Dijkstra und wird vielen Lesern bekannt erscheinen. Trotzdem werde ich ihn ausführen, da sich die Korrektheit der anderen Algorithmen auf ähnliche Art zeigen lässt. Wir stellen fest, dass vor der Ausführung des Algorithmus die folgenden drei Eigenschaften gelten: 5 Abbildung 1: Algorithmus Decrease Datenstrukturen: D: Array mit den Entfernungen der Knoten zu s P : Array mit dem Vorgänger eines Knotens C: Leerer aufsteigend sortierender Heap Funktion Decrease( Knoten v, w , real ² ) 1. Schritt: Initialisierung (O.E. D(v) < D(w)) Falls (² ≥ ω(v, w)) führe aus EXIT Gewicht wird nicht-positiv Setze w(v, w) = w(v, w) − ² UpdateLocal(v, w) Falls (D(w) ≤ D(v) + ω(v, w)) führe aus EXIT Keine Verbesserung der Distanz Setze D(w) = D(v) + ω(v, w) Setze P (w) = v Füge (w, D(w)) in C ein 2. Schritt: Knotenupdate Solange (C nicht leer) führe aus Setze (z, D(z)) = M inimum(C) Für alle (z, x) mit (z, x) ∈ Besitz(z) oder (z, x) ∈ N ichtbesitz(z) und D(z) < bz (x) Falls (z, x) ∈ Besitz(z) führe aus U pdateLocal(z, x) Falls (D(x) > D(z) + ω(z, x)) führe aus Setze D(x) = D(z) + ω(z, x) Setze P (x) = z HeapImprove(C, (x, D(x))) 6 • (E1) Für jeden Knoten v ∈ V wird die Länge eines kürzesten Weges korrekt berechnet, d.h D(v) = d(v). • (E2) T(s) ist ein Kürzester-Wege-Baum in G mit Wurzel s. • (E3) Für jeden Knoten v ∈ V und jede Kante (v, w) ∈ N ichtbesitz(v) gilt: backward-level v (w) = Bv (w) und forward-levelv (w) = Fv (w). Lemma 3.2 Wenn (E1) - (E3) vor der Ausführung des Algorithmus richtig waren, sind sie auch danach erfüllt und der Algorithmus damit offensichtlich korrekt. Betrachten wir zunächst die erste Eigenschaft und nehmen wir an, das Gewicht der Kante (v, w) wird verringert, wobei d(v) < d(w) und d0 (w) < d(w) gilt. Durch scharfes Ansehen des Algorithmus stellen wir fest, dass für jeden Knoten z ∈ V gilt: • D(z) kann nur kleiner werden, also D(z) ≤ d(z), und • D(z) wird nicht kleiner als d0 (z), also d0 (z) ≤ D(z). Um zu zeigen, dass für alle Knoten die richtige Distanz berechnet wurde, nehmen wir an, es gäbe Knoten mit falschen Werten. Wir betrachten davon den Knoten x mit der kleinesten (richtigen) Distanz zu s und zeigen denn Widerspruch, dass er den richtigen Wert hat, da für seinen Vorgänger im KürzesteWege-Baum die richtige Distanz berechnet wurde. Für jeden Knoten y mit d0 (y) < d0 (x) gelte also D(y) = d0 (y), und somit auch für den Knoten z der Kante (z, x) im kürzesten Weg von x nach s. Daraus folgt: d0 (z) + ω(z, x) = d0 (x) < D(x) ≤ d(x) (*). 1.Fall: d(z) = d0 (z), die Distanz von z hat sich durch das Verkleinern des Kantengwechts nicht geändert. Damit folgt aus (*): d0 (x) = d0 (z) + ω(z, x) = d(z) + ω(z, x) ≥ d(x). Andererseits haben wir festgestellt, dass immer d0 (x) ≤ d(x) gilt, womit wir d0 (x) = D(x) = d(x) erhalten und die Distanz von x richtig berechnet wurde. 2.Fall: d(z) > d0 (z). Wenn z aus C entfernt wird, gilt richtigerweise D(z) = d0 (z). Beim Betrachten der zu z inzidenten Kanten wird auf jeden Fall auch (z, x) bearbeitet, da sie entweder in Besitz(z) ist oder die zweite Bedingung erfüllt ist (z hat seine Distanz verkleinert und der Wert Bz (x) ist korrekt). Falls nun D(z) + ω(z, x) < D(x) gilt, wird die Distanz von x richtig auf D(z) + ω(z, x) gesetzt. Sonst gilt D(z) + ω(z, x) = D(x) und die Distanz ist bereits richtig berechnet worden. Die Korrektheit der anderen beiden Eigenschaften wird durch die folgenden einfachen Beobachtungen deutlich. Wenn die Distanz eines Knotens richtig auf einen neuen Wert gesetz wird, wird in der darauf folgenden Zeile auch der Vorgänger dieses Knotens im Kürzeste-Wege-Baum richtig gesetzt. Die Einträge Bx (z) und Fx (z) eines Knotens z werden richtig aktualisiert, da für z zu diesem Zeitpunkt bereits die korrekte Distanz errechnet wurde. 3.3.2 Laufzeit und Speicherplatzbedarf Die benutzten Datenstrukturen benötigen O(|V | + |E|) Speichplatz, um Anfragen in optimaler Zeit zu beantworten. Die Distanz eines Knotens wird in konstanter Zeit geliefert, der kürzeste Weg in O(l), wobei l die Weglänge ist. Lemma 3.3 Das Verringern eines Kantengewichts in einem Graphen mit k-beschränkter Besitzfunktion benötigt im worst-case eine Laufzeit in O(k log n) pro Knoten mit geänderten Werten. Sei z ein Knoten, der aus dem Knotenspeicher C entfernt wurde. Da der Knoten davor in C eingefügt wurde, hat sich seine Distanz im Laufe des Algorithmus verringert. Eine zu z adjazente Kante (z, x) wird betrachtet, wenn sie entweder zu z gehört oder zu x gehört und d0 (z) < Bz (x) gilt. 7 Im ersten Fall beträgt die Laufzeit für diese Kante O(log n). Da es höchstens k solche Kanten gibt, ergibt sich eine Laufzeit in O(k log n). Die Kanten, die die zweite Bedingung erfüllen, werden durch sukzessives Entfernen von Knoten aus Bz ermittelt. Bei der ersten Kante, die die Bedingung nicht mehr erfüllt, brechen wir ab. Die Laufzeit für diese Kante liegt in O(log n). Die anderen Kanten führen zu Knoten xi , die ihre Distanz verringern und gehören zu deren Besitz. Die Laufzeit pro Kante beträgt O(log n) und geht in die Laufzeit von O(k log n) ein, die xi benötigt, wenn er aus C entfernt wird. Die Gesamtlaufzeit ist also in O(k log n). 3.4 Erhöhen eines Kantengewichts (Increase) Betrachten wir nun den Algorithmus zum Erhöhen eines Kantengewichts. Seien v und w zwei Knoten des Graphen (o.E. d(v) ≤ d(w)) und das Gewicht der Kante (v, w) wurde um ² ≥ 0 vergrößert. Es ist schnell zu sehen, dass sich nur die Informationen von Knoten im Unterbaum T (w) ändern können und der neue Unterbaum von w möglicherweise weniger Knoten enthält. Wir unterscheiden drei Fälle, nach denen wir die Knoten einfärben: • Ein Knoten, der seine seine Distanz zu s vergrößert, erhält die Farbe rot. • Ein Knoten wird blau gefärbt, wenn er seine alte Distanz beibehält und nur einen neuen Vorgänger im Kürzeste-Wege-Baum erhält. • Wenn sich weder Vorgänger noch Distanz eines Knotens ändern, erhält er die Farbe weiß. Die Farbe eines Knotens bestimmt also die Farbe seiner Nachfolger im KWG. Ist er rot, sind die direkten Nachfolger rot oder blau. Ist er blau oder weiß, sind die direkten Nachfolger, und sogar alle Knoten im Unterbaum des Knotens, weiß. Die Färbung läßt sich also durch eine Breitensuche im Unterbaum T (w) herstellen, bei der zuerst alle Knoten (außer möglicherweise w) weiß sind, und solange die Nachfolger der roten Knoten eingefärbt werden, bis keine unbearbeiteten roten Knoten mehr existieren. Die Färbung der Knoten ergibt außerdem die Anzahl der Ausgabeänderungen, sie entspricht der Anzahl der roten und blauen Knoten. Für jeden solchen Knoten müssen die Nachbarn überprüft werden, ob sie einen kürzeren Pfad zu s liefern, als den aktuell gefundenen. Dabei sollen aber nicht alle Nachbarknoten betrachtet werden, um die Laufzeit nicht zu groß werden zu lassen. Wir definieren deshalb den besten nicht-roten Nachbarn eines Knotens z als den Knoten q aller nicht-roten Nachbarn q j , für den der Term Fz (qj ) − d(z) den minimalen Wert annimmt. Betrachten wir nun den Pseudocode des Algorithmus Increase (siehe Abb. 2). In Schritt eins werden zuerst durch die Funktion U pdateLocal(v, w) die Werte der Level der Kante (v, w) in den entsprechenden Datenstrukturen aktualisiert (siehe Algorithmus Decrease). Falls (v, w) keine Baumkante ist, endet der Algorithmus. Sonst wird w in den Heap M eingefügt und eine Schleife wie im Algorithmus von Dijkstra gestartet. Der zweite Schritt färbt die Knoten ein. Solange noch Knoten in M enthalten sind, wird der Knoten z mit minimaler Distanz aus M entfernt. Falls er einen noch nicht rot gefärbten Nachbarn x mit D(x) + ω(z, x) = D(z) hat, erhält er diesen Knoten als Vorgänger, ändert seine Distanz nicht und wird blau gefärbt. Da x eine geringere Distanz als z hat und daher bereits die richtige Farbe hat, ist diese Zuweisung korrekt. Damit ist die Bearbeitung von z abgeschlossen. Falls er keinen solchen Nachbarn hat, wird er rot gefärbt und alle seine direkten Nachfolger müssen rot oder blau gefärbt werden. Deshalb werden sie in M eingefügt. Nun sind nur noch die rot gefärbten Knoten zu bearbeiten. In Anlehnung an den Algorithmus von Dijkstra werden in Schritt 3.b ihre Distanzen in nicht-absteigender Reihenfolge berechnet. Davor wird in Schritt 3.a für jeden roten Knoten eine vorläufige Entfernung berechnet, die der Länge eines kürzesten 8 Abbildung 2: Algorithmus Increase Datenstrukturen: D: Array mit den Entfernungen der Knoten zu s P : Array mit dem Vorgänger eines Knotens M, N : Leere aufsteigend sortierende Heaps Funktion Increase( Knoten v, w , real ² ) 1. Schritt: Initialisierung (OE D(v) < D(w)) UpdateLocal(v, w) Falls ((v, w) ist keine Baumkante) dann EXIT Keine Änderungen nötig Sonst Füge (w, D(w)) in M ein 2. Schritt: Einfärben Solange (M nicht leer) führe aus Setze (z, D(z)) = M inimum(M ) Falls (es existiert nicht-roter Nachbar x von z mit D(x) + ω(x, z) = D(z)) dann Setze P (z) = x und färbe z blau Sonst Färbe z rot Für alle Nachfolger x von z Füge (x, D(x)) in M ein 3.a: Vorbereiten der roten Knoten Für jeden roten Knoten z führe aus Falls (z hat nur rote Nachbarn) dann Setze D(z) = unendlich Setze P (z) = N ull Sonst sei x der beste nicht-rote Nachbar von z Setze D(z) = D(x) + ω(x, z) Setze P (z) = x Füge (z, D(z)) in N ein 3.b: Bearbeiten der roten Knoten Solange (N nicht leer) führe aus Setze (z, D(z)) = M inimum(N ) Für alle Kanten (z, x) ∈ Besitz(z) führe aus Update bx (z) fx (z) Für alle Kanten (z, x) mit x ist rot führe aus Falls (D(z) + ω(z, x) < D(x)) dann Setze D(x) = D(z) + ω(z, x) Setze P (x) = z HeapImprove(Q, (x, D(x))) Färbe alle roten und blauen Knoten wieder weiß 9 Weges zu s, der keine roten Knoten enthält, entspricht. Wenn kein solcher Weg existiert, erhält der Knoten die Entfernung unendlich. Mit diesem Wert als Priorität wird der Knoten in den Heap N eingefügt. Nun wird wie üblich jeweils der Knoten z mit minimaler Distanz aus N entfernt. Für alle Kanten (z, x), die sich in seinem Besitz befinden, werden die Einträge Bx (z) und Fv (z) aktualisiert. Danach wird für alle roten Nachbarn x überprüft, ob sich ihr Entfernungswert verringert, wenn sie z als Vorgänger wählen und gegebenenfalls die Informationen geändert und die Position in N aktualisiert. Schließlich werden alle roten und blauen Knoten wieder weiß gefärbt. 3.4.1 Korrektheitsanalyse Der Beweis der Korrektheit von Increase kann analog zum Beweis der Korrektheit von Decrease geführt werden. Man betrachtet wieder den Knoten mit kleinster Distanz, für den die Informationen nicht korrekt berechnet wurde und erhält aus der Annahme, dass sein Vorgänger korrekt ist, den Widerspruch, dass er doch den richtigen Distanzwert hat. 3.4.2 Laufzeit und Speicherplatzbedarf Wieder benötigen die benutzten Datenstrukturen O(|V | + |E|) Speichplatz, um Anfragen in optimaler Zeit zu beantworten. Die Distanz eines Knotens kann in konstanter Zeit geliefert werden, der kürzeste Weg in O(l), wobei l die Weglänge ist. Lemma 3.4 Das Vergrößern eines Kantengewichts in einem Graphen mit k-beschränkter Besitzfunktion benötigt im worst-case eine Laufzeit in O(k log n) pro Knoten mit geänderten Werten. Die Laufzeit in den Schritten zwei und 3.a wird von der Suche nach dem besten nicht-roten Nachbarn dominiert. Wenn der beste nicht-rote Nachbar eines Knotens z gesucht wird, ist z entweder rot oder blau. Höchstens k der untersuchten Kanten gehören z. Da die Kanten in der Reihenfolge ihres Vorkommens in Fz betrachtet werden, führen alle bis auf eine der anderen Kanten zu Knoten, die auch rot sind. Für jede Kante beträgt der Aufwand O(log n), was zu einer Laufzeit von O(k log n) pro geändertem Knoten führt. Mit dem gleichen Argument gilt auch für Schritt 3.b eine Laufzeit von O(k log n) pro geändertem Knoten. 4 Algorithmen für Einfüge- und Löschoperationen In diesem Kapitel werden kurz die Änderungen besprochen, die nötig sind, um die Algorithmen zur Änderung der Kantengewichte für das Löschen und Einfügen von Kanten zu erweiteren. Im Fall eines sich vollständig dynamisch ändernden Graphen treten zwei Probleme auf, die im vorherigen Kapitel noch keine Rolle spielten. Zum einen kann der Graph in mehrere Komponenten aufgeteilt werden (und auch wieder zusammen gefügt werden), zum anderen ist nicht garantiert, dass die ursprüngliche k-beschränkte Besitzfunktion auch nach mehreren Änderungsoperationen noch gilt. Im Folgenden werden die gleichen Bezeichnungen benutzt, wie im dritten Kapitel. 4.1 Einfügen einer Kante (Insert) Man kann sich das Einfügen einer Kante (v, w) mit Kantengewicht ² vorstellen als das Verringern des Kantengewichts einer schon existierenden Kante (v, w) von einem sehr hohen Wert auf ². Deshalb ist es einsichtig, dass der Algorithmus Decrease größtenteils für das Einfügen übernommen werden kann. Die größten Unterschiede sind: 10 • Die neu eingefügte Kante wird dem Besitz einer ihrer Endknoten zugeordnet und die Datenstrukturen der Level werden entsprechend aktualisiert. Der Algorithmus endet danach, falls sich keiner der Endknoten verbessert. • Eine Knotenfärbung wird eingeführt. Zuerst sind alle Knoten weiß. Ein Knoten wird rot, wenn er seine Distanz zu s ändert, d.h. wenn er in C eingefügt wird. • Die Aktualisierung der Datenstrukturen für die Level wird nicht in Schritt zwei erledigt sondern im neu eingefügten Schritt drei (siehe nächster Punkt). Dies beeinträchtigt die Korrektheit nicht, da diese Informationen in diesem Durchlauf des Algorithmus nicht mehr benötigt werden. • Ein neuer Schritt drei wird angefügt, der den Besitzer bestimmter Kanten ändert. Dieser ist für die Korrektheit nicht erforderlich, garantiert aber die Einhaltung der gewünschten Laufzeit. Der Pseudocode des neuen Schrittes sieht folgendermaßen aus: Für alle roten Knoten z führe aus Für alle Kanten (z, x) ∈ Besitz(z) mit x ist nicht rot führe aus ChangeOwnership(z,x) Für alle Kanten (z, x) ∈ Besitz(z) mit x ist rot führe aus UpdateLocal(z,x) Färbe alle Knoten wieder weiß Dabei tauscht die Funktion ChangeOwnership(z, x) den Besitzer der Kante (z, x) und aktualisiert die zugehörigen Werte in den Datenstrukturen der Level. Die Korrektheit des Algorithmus folgt direkt aus der Korrektheit von Decrease und wird nicht ausgeführt. 4.2 Löschen einer Kante (Delete) Das Löschen einer Kante kann durch das Erhöhen des Gewichts dieser Kante auf einen sehr großen Wert simuliert werden. Deshalb kann der Algorithmus Increase mit wenigen Änderungen für diese Aufgabe verwendet werden. • Die Werte der gelöschte Kante werden aus den Datenstrukturen der Level entfernt. Danach endet der Algorithmus, falls eine Nichtbaumkante gelöscht wurde. • Wenn zu Beginn von Schritt 3.b alle Knoten in N die Priorität unendlich besitzen bedeutet dies, dass diese Knoten von der Komponente des Graphen, die s enthält, getrennt wurde. Alle Knoten in N erhalten die Distanz unendlich, ohne dass noch irgendwelche Kanten untersucht werden. • Die Aktualisierung der Datenstrukturen für die Level wird nicht in Schritt 3.b erledigt sondern im neu eingefügten Schritt vier (siehe nächster Punkt). Dies beeinträchtigt die Korrektheit nicht, da diese Informationen in diesem Durchlauf des Algorithmus nicht mehr benötigt werden. • Ein neuer Schritt drei wird angefügt, der genau dem neuen Schritt im Algorithmus Insert entspricht. Wieder folgt die Korrektheit direkt aus der Korrektheit von Increase und wird nicht ausgeführt. 11 5 Zusammenfassung In diesem Paper wurden Algorithmen für den dynamischen Fall des Kürzesten-Wege-Problems mit festem Startknoten vorgestellt. Diese sind sowohl effizient in Bezug auf die Ausgabekomplexität als auch praktisch implementierbar. Eine interessante noch offene Aufgabe ist die Erweiterung dieser Algorithmen für den Fall, dass eine ganze Folge von Kantenänderungen ohne Ausgabe der Zwischenergebnisse berechnet werden soll. Literatur [1] D. Frigioni, A. Marchetti-Spaccamela u. U. Nanni. Fully Dynamic Algorithms for Maintaining Shortest Paths Trees, Journal of Algorithms 34, pp. 251-281 (2000). [2] R. K. Ahuia, T. L. Magnanti and J. B. Orlin. Network Flows: Theory, Algorithms and Applications, Prentice-Hall, Englewood Cliffs, NJ, 1993. [3] E. W. Dijkstra. A note on two problems in connection with graphs, Numer. Math. 1, pp. 269-271 (1959). [4] M. L. Fredman and R. E. Tarjan. Fibonacci heaps and their use in improved network optimization algorithms, J. Assoc. Comput. Mach. 34, pp. 596-615 (1987). [5] M. Thorup. Undirected single source shortest path in linear time, Proceedings IEEE Symposium on Foundations of Computer Science, Miami Beach, Florida, October 20-22 1997, pp. 12-21 12