ADS – Vorlesung zu Dijkstra/Dial Prof. Dr. Wolfram Conen Rund um Dijkstra: - Kosten, Dials Variante 06.05.2008 (c) W. Conen, FH GE, ADS 1 Kosten für Dijkstra mit Priority-Queues (kurz: PQueues) Zur Erinnerung: gerichteter Graph G = (V,E) mit nicht-negativen Gewichten w(·) (Bogenlänge in unserem Algorithmus), Startknoten s ∈ V; Zur einfacheren Darstellung: keine Schleifen, jeder Knoten ist von s aus erreichbar n = |V| = Anzahl Knoten, m = |E| = Anzahl Bögen Dijkstra liefert alle kürzesten Wege von s zu den anderen Knoten; in S werden die Knoten gesammelt, zu denen bereits kürzeste Wege bekannt sind In D(v) = Distanz(v) wird die bisher bekannt gewordene kürzeste Länge von s über Knoten in S zu v registriert; V(v) = Vorgänger(v) vermerkt den Vorgänger auf dem kürzesten bisher gefundenen Weg von s zu v (bei mehreren Wegen gleicher Länge wird hier der Vorgänger im ersten gefunden Weg vermerkt). Dijkstra-Ablauf: Initialisieren der Distanzen aller Knoten (INIT-Phase), S ← {s} (INSERT) Iteratives Hinzufügen aller Knoten aus V \ S zu S: jeweils Auswahl des Knotens v* mit der minimalen Distanz zu s (DELETE MIN) Anschauen aller von Bögen, die von v* ausgehen, ggfs. Updates der Distanzen (und Vorgängerbeziehung) (DECREASE KEY) 06.05.2008 (c) W. Conen, FH GE, ADS 2 Kosten für Dijkstra mit PQueues Dijkstra-Ablauf: Initialisieren der Distanzen aller Knoten (INIT-Phase), S ← {s} Iteratives Hinzufügen aller Knoten aus V \ S zu S: jeweils Auswahl des Knotens v* mit der minimalen Distanz zu s Anschauen aller von Bögen, die von v* ausgehen, ggfs. Updates der Distanzen (und Vorgängerbeziehung) wir vernachlässigen hier, dass nur noch Kanten nach V/S geprüft werden Anzahl der aus einem Knoten herausführenden Bögen, die Summe dieser Grade über alle Knoten ist natürlich die Anzahl der Kanten, m 06.05.2008 Dijkstra Kosten: n * INSERTs n * Hinzufügen; jeweils mit 1* DELETE MIN Gradout(v*) Vergleiche Insgesamt also n * INSERT n * DELETE MIN jeder Bogen wird angepackt, d.h. z.B. m Vergleiche max. DECREASE KEY-Aufrufe (maximale Anzahl Updates), s. nächste Folie (c) W. Conen, FH GE, ADS 3 Kosten für Dijkstra mit PQueues Wieviele Updates gibt es maximal? Beispiel: Graphen für 5 Knoten 5 2 1 5 3 1 s 0 7 5 4 2 9 9 8 1 5 7 6 7 6 7 06.05.2008 Insgesamt 10 Updates und 10 Kanten Davon kann man 4 per INSERT erledigen Also 6 DECREASE KEY Wenn man eine beliebige Kante entfernt, hat man weniger Updates! Wenn man eine Kante hinzufügt, kann man nicht mehr Updates bekommen! ⇒ Maximale Anzahl Updates für 5 Knoten: 5 * 2 = 5 * (5-1) / 2 = 10. Maximale Anzahl allgemein für n Knoten: n*(n-1) / 2 Maximales Verhältnis Updates pro Kante: 1 Minimales Verhältnis: (n-1) Updates bei n*(n-1) Kanten = 1/n Updates: 4 + 3 + 2 + 1 (c) W. Conen, FH GE, ADS 4 Kosten für Dijkstra mit PQueues Wie läßt sich das abhängig von m ausdrücken? Maximal kann es m = n*(n-1) / 2 Kanten geben, die zu einem Update führen können. Gibt es mehr Kanten, dann sind mindestens m – n*(n1)/2 hiervon irrelevant. Mindestens muss es m = n-1 Kanten geben (der Graph ist zusammenhängend), und die müssen alle zu Updates führen (Annahme: jeder Knoten ist erreichbar von s, maximales Kantengewicht nicht vorher bekannt) Ausgehend von m gibt es also maximal m Updates, aber nie mehr als n*(n-1)/2 insgesamt Hier sind die Updates in der Initialisierungsrunde mitgezählt, diese führen aber nicht zu einem DECREASE KEY, da man in dieser Runde das initiale INSERT mit dem Kantengewicht von s zu diesem Knoten ausführen würde, es ist also die Anzahl der von s ausgehenden Kanten jeweils abzuziehen: Also maximal (m-1)-DECREASE KEY, aber nie mehr als (n-1)*(n2)/2. 06.05.2008 (c) W. Conen, FH GE, ADS 5 Kosten für Dijkstra mit PQueues Von Interesse: Kosten für INSERT, DELETE MIN, DECREASE KEY Alle anderen Operationen betrachten wir als elementar zu konstanten Kosten, also Kosten in der Größenordnung O(1) Zwei Fragen: Wieviel Aufwand verursachen die Operationen für eine konkrete PQueueRealisierung bzw. Implementierung Wie groß ist der Aufwand mindestens, unabhängig von einer Implementierung (hierzu braucht man Annahmen über die Art der Realisierung, z.B. Sortieren per Vergleich zwischen je zwei Schlüsseln)? Welche Aufwände sind interessant: Worst case (schlimmster Fall), Best case (bester Fall), Average Case („durchschnittlicher Fall“) Wenn man Operationen wiederholt ausführt, dann kann z.B. die erste Ausführung teuer sein (Einfügen des ersten Elements in eine neu zu errichtende Datenbank), weitere Folgeoperationen werden aber deutlich günstiger. Dann analysiert man die amortisierten Kosten. 06.05.2008 (c) W. Conen, FH GE, ADS 6 Kosten für Dijkstra mit PQueues Simple Realisierung von PQueues: Annahme: alle Knoten sind eindeutig von 1 bis n nummeriert Array [1..n] speichert die Distanzen D(v) Kosten: INSERT: O(1) DELETE MIN: Suche über das ganze Array, also O(n) DECREASE KEY: O(1) Insgesamt also O(n2): n*O(INSERT) + n*O(DELETE MIN) + O(m)*O(DECREASE KEY) = n*O(1) + n*O(n) + O(m)*O(1) = O(n) + O(n2) + O(m) = 2 2 O(n ), weil m < n . Die ersten beiden Faktoren sind unabhängig von der Kantenanzahl, ihre Kosten fallen IMMER an. Der zweite Faktor dominiert alle anderen Kosten, d.h.: die Kosten fallen im worst, im average und im best case an! 06.05.2008 (c) W. Conen, FH GE, ADS 7 Kosten für Dijkstra mit PQueues Realisierung von PQueues mit „normalem“ Heap: Annahme: alle Knoten sind eindeutig von 1 bis n nummeriert Ein Heap speichert die Knoten mit ihren Distanzen D(v) als Key Kosten: INSERT, naiver Aufbau: ein Insert pro Knoten): log1+log2+...+log n-1=Ω(n log n)=O(n log n) DELETE MIN: Minimum entnehmen: O(1) + Heap reorganisieren maximal bis zur Tiefe log n: O(log n) = O(log n) DECREASE KEY: Key finden (direkt über Verweis auf Heapelement vom Knoten aus einem Knotenarray [1..n]), also O(1) + Reorg: O(log n) = O(log n) Insgesamt also O(n): (n log n) O(n log n) O(n*log n) O((n +m) + n*O(DELETE MIN) + O(m)*O(DECREASE KEY) = + n*O(log n) + O(m)*O(log n) = + O(m*log n) = * log n) = O(m * log n) [weil alle Knoten von s erreichbar sind nach Annahme und daher m zwischen O(n) und O(n2) liegt, also nicht von n dominiert wird, dieses aber ggfs. dominiert – im ersten Fall ergibt sich O(n + n) = O(n), im zweiten O(n2 + n) = O(n2), also jeweils O(m), ebenso für alle „Zwischenfälle“!] 06.05.2008 (c) W. Conen, FH GE, ADS 8 Kosten für Dijkstra mit PQueues Mit einer „normalen“ Heap-Implementierung von Priorityqueues erreichen wir also Kosten für Dijkstra von O(m* log n). Das ist jetzt zunächst eine Worst-Case-Abschätzung (wenn es gar keine Updates gibt, haben wir im Best case, z.B. nur O(n log n + m) an Kosten) Es ist aber auch eine average case Abschätzung, Amortisierung spielt hier keine Rolle (es wird nichts durch mehrfaches Verwenden billiger. Insgesamt ist das besser, als die O(n2)-Implementierung bei der einfachen Array-Implementierung, falls gilt m = o(n2/log n) (sonst lohnt der Aufwand nicht!) Typischerweise gibt es wesentlich mehr DECREASE KEY-Operationen, als EXTRACT MIN. Heapvarianten, die günstigere (amortisierte) Kosten für DECREASE KEY-Operationen haben (z.B. Fibonacci-Heap, Radix-Heap, beide gemeinsam) verbessern das Laufzeitverhalten weiter. Mit Fibonacci-Heaps erreichen wir O(n log n + m), weil die amortisierten Kosten für DECREASE KEY nur bei O(1) liegen! 06.05.2008 (c) W. Conen, FH GE, ADS 9 Kosten für Dijkstra mit PQueues Problem unserer Analysen: Wir bleiben ein wenig „unscharf“: ist ihre konkrete Implementierung effizient (in Sinne des Größenordnungmäßig erreichbaren)? Entspricht ihre Implementierung tatsächlich den Algorithmen für Heap-basierte Pqueues? Sind die Annahmen über die Kosten „elementarer“ Operation für ihre konkret verwendete Rechnerarchitektur und für ihre spezielle Maschine gerechtfertigt? Daraus ergibt sich auch, ob die „idealisierten“ Kostenanalysen für die PQueue-Operationen gerechtfertigt sind und sie diese übernehmen können. Wenn ja, ist sie auch im Hinblick auf die „Konstanten“ (die im OKalkül ausgeblendet werden) „gut“? 06.05.2008 Ist das überhaupt interessant? Ja! Denn n*d1 = n*d2 = O(n), auch wenn d1=5 und d2 = 500.000! (c) W. Conen, FH GE, ADS 10 Kostenanalysen generell „Exaktere“ Analysen: Unterstellen sehr genau eine bestimmte (abstrakte) Rechnerarchitektur (mit Details zur Darstellung von Zahlen, exakten Kosten zu einzelnen Klassen von Operationen, etc.), z.B. verwendet Knuth in The Art of Computer Programming eine Architektur mit einer eigenen (Assembler-)Sprache, deren Kosten (und Effekte!) sehr exakt analysierbar sind und in der er die Algorithmen darstellt Überlegungen zum Speicherbedarf haben wir noch gar nicht angestellt, ab einer bestimmten Größe für die Laufzeit wichtig, wo gespeichert wird: Cache, primär (Hauptspeicher), sekundär (Platte), tertiär Speicher (Netz, CD-RW, Band) Weitere Probleme: Was genau sind „average cases“? Wie bestimmt man amortisierte Kosten? etc. Details hierzu (generell und zu vielen Algorithmen und Datenstrukturen) z.B. in Cormen, Leierson, Rivest, Stein: Introduction to Algortithms, 2nd Edition, MIT Press, 2001. Für diese und ähnliche Analysen (und für vieles mehr ;-) braucht man häufig Zählargumente (Kombinatorik) und Wahrscheinlichkeitsanalysen (Probabilistik) – Prof. Engels kann das sicher gut erklären, ansonsten kann es auch nicht schaden, mal ein passendes Buch hierzu aufzuschlagen, z.B. Steger: Diskrete Strukturen (Band 1 und 2) 06.05.2008 (c) W. Conen, FH GE, ADS 11 Kosten für Dijkstra mit PQueues Jetzt schauen wir noch ein paar Spezialfälle an! Angenommen, alle Bogengewichte sind ganzzahlig und aus dem Intervall [1..C]. Für diesen Fall haben wir bereits eine Idee gesehen, die auf Dial (1969) zurückgeht: verwende ein Array [0..C*(n-1)] von Töpfen (Buckets), um die Knoten in Töpfe einzusortieren, die ihrem Gewicht entsprechen. Beginne vorn und schreite auf der Suche nach Knoten mit minimalem Abstand durch die Töpfe 06.05.2008 (c) W. Conen, FH GE, ADS 12 Kosten für Dijkstra mit PQueues Der längste kürzeste Weg von s zu anderen Knoten kann höchstens (n-1) Bögen lang sein: 1 s 2 n-1 z Maximales Gewicht eines kürzesten Wegs: (n-1)*C Wir haben Erreichbarkeit unterstellt, also können wir alle Knoten ungleich s mit D(v) = (n-1)*C initialisieren. Die Knoten-IDs sind weiter aus [1..n]. Wir merken uns für jeden Knoten den Topf, in dem er gerade steckt. Machen wir das an unserem Beispielgraphen durch! 06.05.2008 (c) W. Conen, FH GE, ADS 13 Dials simple Vorgehensweise 5 C = 9, maximales Bogengewicht C*(n-1) = 35, maximale kürzeste Weglänge 1 1 1 s 0 7 5 2 2 5 4 2 9 7 0 Init: 1 2 3 4 5 6 7 8 9 10 9 1 7 4 3 C*(n-1) s 1 2 3 4 v*=s 06.05.2008 1 2 3 (c) W. Conen, FH GE, ADS 4 14 Kosten für Dijkstra mit PQueues 5 C = 9, maximales Bogengewicht C*(n-1) = 35, maximale kürzeste Weglänge 1 1 1 s 0 7 5 2 2 5 3 4 2 9 7 0 1 2 3 4 11 v*=4 v*=2 v*=1 v*=3 06.05.2008 5 6 8 9 3 2 2 7 3 43 4 9 8 1 5 7 6 7 6 3 4 C*(n-1) 10 4 4 (c) W. Conen, FH GE, ADS D(1) = 1 D(2) = 3 D(3) = 5 D(4) = 6 15 Kosten von Dials Variante Im schlimmsten Fall erstreckt sich die Suche über alle Buckets, also von 1 bis C*(n-1) = O(nC). Das ist linear: C ist eine Konstante! Aber stören kann es schon...z.B. wenn für ihre Anwendungen n im Vergleich zu C eher klein ist ... Zudem führt eine direkte, naive Implementierung natürlich zu zusätzlichem Speicherbedarf von O(nC). Gesamte Kosten: INSERT (gesamt): O(n) DELETE MIN (gesamt): maximal O(nC) DECREASE KEY: Finden (mittels Hilfsarray): O(1), Löschen/Einfügen in Bucket: O(1) Insgesamt also: O(n) + O(nC) + O(m) = O(m + nC) 06.05.2008 (c) W. Conen, FH GE, ADS 16 Verbesserungen zu Dial? Nur ¼ des Bucket-Arrays wird im Beispiel genutzt! Wenn alle Knoten von s erreichbar sind: dann kann der nächste Knoten minimaler Distanz höchstens C Felder entfernt sein Wenn eine Distanz kleiner als C ist, z.B. C-x, und das momentane Array-Ende bei Ende liegt, dann werden die Felder [Ende-x+1..Ende] niemals besucht Ideal wäre die Suche, wenn man nur gefüllte Buckets besucht, diese nur Knoten mit gleicher Distanz enthalten und Verwaltung/Anlage/Update der Buckets billig ist. Das wird nicht gehen...aber wenig suchen und wenig und billig sortieren/finden (bei Knoten ungleichen Werts in den Buckets) ist schon ein gutes Ziel! Das versuchen Radix-Heaps, s. Literatur (im vorletzten Jahr haben wir die noch angeschaut, ist aber doch eine reichlich komplizierte Datenstruktur, deshalb sei sie hier nur erwähnt). Typische Implementierungen verwenden heute z.B. Fibonacci-Heaps (auch nicht ganz banal ;-) 06.05.2008 (c) W. Conen, FH GE, ADS 17 Literatur Allgemein zur Algorithmik, nochmals erinnert sei an: Cormen, Leierson, Rivest, Stein: Introduction to Algorithms, MIT Press, 2nd Edition, 2001 (ein inhaltlich sehr gutes und optisch sehr schönes Buch, zum Nachschlagen für den Schrank, zum Lernen nicht so doll, weil ohne Lösungshinweise; zum Verstehen aber gut!) Speziell zu Datenstrukturen für Dijkstra (only for the *very* brave ones): Ahuja, Mehlhorn, Orlin, Tarjan: Faster Algorithms for the Shortest Path Problem. Verwendet Radix-Heaps, Präsentation hierzu z.B. auf James Orlins Webseite am MIT. Mikkel Thorup: Integer Priority Queues with Decrease Key in Constant Time and the Single Source Shortest Paths Problem, Proc. STOC’03, ACM, 2003 (s. diesen Link) eine abstrakt beschriebene deterministische Fibonacci-Heap-Variante als Priority-Queue für Dijkstra mit insgesamt O(m + n log log C) für ganzzahlige Gewichte aus [0..C] – das löst ein offenes Problem aus dem obigen Paper – und verbessert die bekannten Grenzen! Ohne Gewichtsgrenze C erhält er O(m + n log log n), das größenordnungsmässig nur verbessert werden kann, wenn die bisher beste bekannte Lösung für deterministisches Sortieren von Han (eben mit O(n log log n) verbesserbar wäre. Leider nicht so leicht praktisch umsetzbar... Es nutzt die Idee, dass man nicht immer das Minimum auswählen muß, sondern nur einen Knoten, der nicht mehr verbessert werden kann (darunter ist auch immer das Minimum) ...und eine große Anzahl weiterer Arbeiten, u.a. zu Fibonacci-Heaps etc. im Zusammenhang mit Dikstra 06.05.2008 (c) W. Conen, FH GE, ADS 18