Die k kürzesten Wege in gerichteten Graphen Marc Benkert Wintersemester 2001/2002 1 Einführung 1.1 Problemstellung In einem gerichteten, gewichteten Graphen G = (V, E) sollen die k kürzesten Wege zu zwei vorgegebenen Knoten s, t gefunden werden. 1.2 Technische Voraussetzungen Gegeben sei • Graph G = (V, E), |V | = n, |E| = m • Kantenfunktion l : E → R+ 0 • Zykel, Selbst- und Mehrfachkanten sind erlaubt • Startknoten s ∈ V • Zielknoten t ∈ V • jeder Knoten v ∈ V sei von s aus erreichbar • ein s−t Weg darf an einem Knoten mehrmals vorbeikommen, Zykel dürfen also durchlaufen werden 1.3 Motivation Was bringt es uns zusätzlich zu der kürzesten Verbindung zweier Punkte auch noch die nächstkürzten zu kennen? Eine Auswahl bezüglich anderer Kriterien als nur der Länge ist möglich. Ein Radfahrer wird z.B. auf die Höhenmeter achten, die er auf einer Strecke zurückzulegen hat (kürzeste Wege im Strassennetz). Die Sensitvität der Lösungen (Ähnlichkeit) kann beitragen zum Verständnis der Aussage, die der Graph illustrieren soll. Dies spielt vor allem in der Molekularbiologie eine Rolle. Allgemein kann man sagen, daß man sich von der Kenntnis der k kürzesten Wege ein besseres Verständnis der Aussage des Graphen verspricht, als von der Kenntnis lediglich eines kürzesten Weges. 1 1.4 Der vorgestellte Algorithmus Der Algorithmus den ich im folgenden vorstellen werde ist sehr konstruktiv. Er benötigt die Vorberechnung eines single destination shortest path tree. Die Destination wird in unserem Fall der Knoten t sein. Ich erhalte den single destination shortest path tree, indem ich in G R einen single source shortest path tree bzgl Knoten s bestimme, wobei GR den Graphen bezeichne, den ich aus G durch umdrehen aller Kanten erhalte. Siehe hierzu Abb. 1. Die erforderliche Laufzeit ist in O(m + n log n) [1] und ist somit verträglich mit der Laufzeit O(m+n log n+k log k), die der Algorithmus zur Bestimmung der k kürzesten s−t Wege benötigt. Zur Bestimmung der k kürzesten Wege von s zu allen anderen Knoten wird O(m + n log n + nk log k) benötigt. Die Idee des Algorithmus ist nun, einen s − t Weg durch einen beliebigen s − u und einen kürzesten u − t Weg darzustellen. Dieser wird durch den single destination shortest path tree induziert. Wir müssen nun wissen, wieviel man beim Laufen von s nach u bzgl eines kürzesten s − t Weges an Länge verliere. Hierzu wird die Kantenpotentialfunktion δ(e) eingeführt. Abb. 2 2 Vorbereitungen des Algorithmus 2.1 Verwendete Bezeichnungen • Graph G = (V, E), s, t ∈ V • T ⊂ E shortest destination path tree zu t • Für u, v ∈ V sei d(u, v) Distanz des kürzesten u − v Weges induziert durch T • Für e ∈ E sei δ(e) = l(e) + d(target(e), t) − d(source(e), t) Kantenpotentialfunktion • e ∈ G − T sidetrack-Kante • nextT (v) Nachfolger von v in T 2.2 Darstellung der Lösungswege Geben wir den kürzesten Weg explizit aus, so kann es passieren, daß der i-te kürzeste Weg die Länge i · n hat. Dies ist z.B. G aus einem Kreis. Summieren wir die Länge der Pkder Fall, besteht 2 k Wege auf erhalten wir: i=1 i · n ∈ O(k · n), was doch eher schädlich wäre gegenüber unser angestrebten Laufzeit des Algorithmus. Da aber nun ein s − t Weg durch die verwendeten sidetrack-Kanten eindeutig bestimmt ist, (zwischen den sidetrack-Kanten laufen wir auf T) genügt es diese auszugeben. Unsere Ausgabe besteht dann aus einer Liste, die einen Pointer auf das Listenelement enthält, aus der der gespeicherte Weg durch Ausführen derselben sidetracks des in diesem Element gespeicherten Weges plus Ausführen des letzten sidetracks, der ebenfalls im Listeneintrag gespeichert ist, hervorgeht. Darüberhinaus kann z.B. die Länge des Weges gespeichert werden. Die Ausgabe des i-ten Weges benötigt in unserer impliziten Darstellung also nur noch konstante 2 3 s 4 2 b e 4 h 12 10 2 6 4 8 2 5 11 9 3 4 a 2 3 1 3 3 c f 4 i 3 10 2 6 3 1 3 2 3 4 3 1 4 8 1 4 d g 4 t 11 6 7 Abbildung 1: Beispielgraph und single destination shortest path tree 1 12 10 6 4 2 9 7 6 2 8 10 6 3 1 4 3 10 0 13 11 7 0 Abbildung 2: Kantenpotentialfunktion δ(e) 3 0 Zeit, womit wir unser Problem gelöst hätten. Der explizite Weg läßt sich proportional zur Anzahl der Kanten des Weges berechnen. Sicherzustellen ist allerdings noch, daß die angegebenen sidetrack-Kanten überhaupt einen s − t Weg induzieren, sowie daß der Vorgänger-Weg eines Weges, induziert durch Weglassen des letzten sidetrack, vor dem Weg selbst ausgegeben wird. Dies wird 2.3.3 sichern. 2.3 2.3.1 Erste Folgerungen Folgerung (i) Für e ∈ E gilt δ(e) ≥ 0 (ii) Für e ∈ T gilt δ(e) = 0 Beweis: (i) Wäre δ(e) < 0 stände dies im Widerspruch dazu, daß T ein single destination shortest path tree ist. (ii) Für solche e gilt: l(e) = d(source(e), t) − d(target(e), t) somit erhalte ich die Behauptung direkt aus der Definition von δ. 2.3.2 Lemma P Für jeden s − t Weg p gilt l(p) = d(s, t) + e∈p δ(e) Beweis: Dies wird offensichtlich, da δ(e) den Verlust angibt, den ich bzgl des Weges, der in source(e) startet, e benutzt und dann auf T nach t läuft gegenüber dem Weg, der sofort von source(e) auf T nach t läuft, habe. P P Ein mathematischer P e∈p l(e) = e∈p δ(e)+d(source(e), t)−d(target(e), t) = P Beweis wäre: l(p) = d(s, t) + d(t, t) + e∈p δ(e) = d(s, t) + e∈p δ(e). 2.3.3 Folgerung Für jeden s − t Weg p = (e1 , e2 , ..., en ) gilt für den s − t Weg p− = (e1 , e2 , ..., en−1 ): l(p) ≥ l(p− ), wobei ei den i.ten sidetrack bezeichne. Beweis: Folgt direkt aus 2.3.1 (i) und 2.3.2. 2.4 Der Kürzeste-Wege-Baum (gradunbeschränkt) Wir bilden nun einen Baum, dessen Knoten aus s−t Wegen bestehen. Dabei stehen in den Knoten jeweils die ausgeführten sidetracks. Die Wurzel besteht aus der leeren Menge, also dem kürzesten s − t Weg laut T. Die sidetracks eines Kindes bestehen aus den sidetracks des Vorgängers sowie einem weiteren sidetrack. Hierbei muss man natürlich aufpassen, welche sidetracks ausführbar sind. 2.3.3 liefert uns eine Heap-Ordnung auf dem Baum: Man weiß, daß die Kinder eines Knotens einen höchstens längeren s − t Weg induzieren als der Knoten selbst. 4 {} 13 2 3 21 22 23 1 9 8 19 1,7 1,6 1,2 1,4 1,13 24 25 26 27 28 10 18 16 8,7 17 8,7,8 20 Abbildung 3: Kürzester Wegebaum, sidetrackkanten per δ identifiziert Abbildung 3 zeigt einen Ausschnitt dieses Baumes für unseren Beispielgraphen. Man erkennt, daß der Baum durchaus unendlich sein kann. In der Abbildung ist dies anhand des Zykels, der durch die sidetrackkanten (8,7) induziert wird, verdeutlicht. Wir brauchen uns allerdings gar nicht erst zu fragen, wieviel Laufzeit benötigt würde, diesen Baum zu erstellen, da das Suchen nach den k kürzesten Wegen in diesem Baum sowieso schon mehr Laufzeit verschlingen würde, als wir für unseren Algorithmus veranschlagt haben. Zur Erinnerung: Dies war O(m + n log n + k log k) 2.5 Das Auffinden der k kürzesten Wege in einem Wegebaum Allgemeiner gesagt suchen wir die k kleinsten Knotenwerte in einem Heap H mit der Restriktion, daß die Nachfolgeknoten einen geringeren Wert haben. Bei uns ist der Heap H der kürzeste Wegebaum, und die Knotenwerte sind die Summen der δ − W erte der sidetrackkanten. 5 Geschickterweise geht man folgendermaßen vor: Man fügt die Wurzel von H in einen neuen Heap F ein. Solange noch keine k Knoten ausgegeben worden sind oder der Heap F leer ist (dies wäre der Fall hätte Heap H gar keine k Knoten, in unserem Fall würden also keine k s − t Wege existieren) fügt man alle Kinder der aktuellen Wurzel von F in F ein, entnimmt die aktuelle Wurzel aus F und gibt sie aus. Alles in allem eine normale Breitensuche in einem Heap. d bezeichne nun die Anzahl der maximal möglichen Kinder eines Knotens. Die Breitensuche wird also Laufzeit O(d · k + k log k) erfordern. Dabei brauchen wir O(d · k) um den Heap F aufzubauen und O(k log k) für die solange-Schleife. Zu beachten ist hierbei, daß ein Heap in Linearzeit aufgebaut und in logarithmischer Zeit aktualisiert werden kann. In unserem Fall sind die Kinder aber nur durch die Anzahl aller Kanten m beschränkt. Hierzu stelle man sich einen ausreichend dichten Graphen (Anzahl der Kanten von T fällt nicht ins Gewicht) vor, dessen single destination shortest path tree T aus einem einzigen Weg bestehe. Dies führt zu einer Laufzeit von O(m · k + k log k) für die Breitensuche führen. Diese könnte wiederrum unsere veranschlagte Laufzeit dominieren! Die folgenden Konstruktionen haben deshalb nur ein Ziel: Einen Kürzesten-Wege-Baum zu erstellen, dessen Knoten konstant viele Nachfolger haben. In diesem Fall veranschlagt die Laufzeit der Breitensuche O(k log k) und verschlechtert die asymptotische Laufzeit des Algorithmus somit nicht mehr. 3 Konstruktion des Kürzesten-Wege-Baums (gradbeschränkt) Der Algorithmus Man konstruiert einen Graphen P (G) der einen Initialknoten r(s) enthalten wird und in dem alle Knoten maximal vier auslaufende Kanten haben. Der Kürzeste-Wege-Baum wird dann induziert durch alle von r(s) aus erreichbaren Knoten. Jeder Knoten hat also höchstens vier Nachfolger. Man geht dabei wie folgt vor: (i) Bilde für jeden Knoten v ∈ V einen 3-Heap HG (v) bestehend aus allen sidetrackkanten die ich von v aus auf dem durch T induzierten v − t Weg erreichen kann. Die Heap-Ordnung wird dabei gegeben durch die δ-Werte, der geringste Wert landet wieder in der Wurzel. (ii) Bilde mit all diesen Heaps einen azyklischen Graphen D(G) in dem jeder Knoten einer Kante in G − T entspricht und höchstens drei auslaufende Kanten hat. (iii) Bilde aus D(G) den gewünschten Graphen P (G) mit höchstens vier auslaufenden Kanten Die Konstruktionen im einzelnen: 3.1 Der Heap HG (v) Wiederrum sind drei Schritte erforderlich (i) Zunächst werden für alle Knoten 2-Heaps Hout (v) gebildet bestehend aus den von v direkt auslaufenden sidetrackkanten unter den üblichen Bedingungen. Hinzu kommt die Restriktion, daß die Wurzel nur einen Nachfolger haben darf. 6 Die erforderliche Laufzeit für einen Heap ist O(|out(v)|). Die Laufzeit für alle diese Heaps beträgt O(m) da jede Kante maximal einmal in einen Heap eingefügt wird. (ii) Nun bildet man für jeden Knoten einen 2-Heap HT (v), der für alle Knoten w auf dem v − t Weg in T die Wurzel von Hout (w) enthält. Bilde hierfür zunächst HT (t) und laufe dann die Wege in T rückwärts entlang. Der Heap HT (v) wird gebildet, indem man die Wurzel von Hout (v) in den bereits existierenden Heap HT (nextT (v)) einfügt. Die Laufzeit beträgt somit im worst-case (T ein Weg) O(n) für diesen Schritt. (iii) Der Heap HG (v) entsteht nun, indem die entsprechenden Heaps Hout (w) in den Heap HT (v) eingesetzt werden. Die Tatsache, daß der Heap HT (v) ein 2-Heap ist und die Restriktion in (i) sichern die 3-Heap-Eigenschaft von HG (v) Die Laufzeit zur Bildung von HG (v) ist also O(n + m) Als Beispiel für diese Konstruktion diene der Weg s, a, c, f, t in unserem Beispielgraphen. Die Heaps Hout ergeben sich sofort zu: • Hout (t) = 13 • Hout (f ) = 8 • Hout (c) = 9 → 10 • Hout (a) = 3 • Hout (s) = 1 → 2 Die Heaps HT sowie den Heap HG (s) entnehmen Sie der Abbildung 4. Die Knoten werden wiederrum durch die δ-Werte der Kanten identifiziert, denen sie entsprechen. Die *-Knoten werden von der Heapify-Operation, die die Wurzel von Hout (w) in HT (nextT (w)) einfügt, aktualisiert. Knoten, die kein * bekommen haben, werden später in D(G) dieselben Struktureigenschaften der Heaps HG ausnützen. Für sie wird nämlich in D(G) nur ein Knoten eingefügt werden. Dies ist besonders wichtig, um zu gewährleisten, daß die Anzahl der Knoten in D(G) nicht explodiert! Siehe hierzu auch Eigenschaften von D(G). 3.2 Der Graph D(G) Der Graph D(G) hat für jeden Knoten aus Hout , der nicht Wurzel ist (Typ 1), sowie für jeden *Knoten in HG (Typ 2) einen Knoten. Ansonsten übernimmt er die HG -Struktur. Beachte dabei, das Kanten zu Nicht-*-Knoten auf den letzten entsprechenden *-Knoten der Vorgänger-Heaps gerichtet sind. Abbildung 5 zeigt D(G) für unseren Beispielgraphen. Eigenschaften von D(G) • D(G) ist azyklisch. 7 8* 8* 13* 9* 13* HT (t) 13 HT (f ) HT (c) 3* 1* 8* 13 3* 9* 13 9 8* HT (s) HT (a) 1* 3* 9 2 13 8* HT (s) Abbildung 4: Heaps HT und Heap HG (s) 8 b 2 e 2 4 7 h 2 4 i 4 4 d t 13 13 g f 8 0 8 c a 8 3 9 8 13 s 1 3 2 6 10 9 8 Abbildung 5: Graph D(G) • Jeder Knoten w ∈ D(G) kann mit einer Kante ew ∈ G identifiziert werden • Jeder Knoten v ∈ G kann mit einem Knoten h(v) ∈ D(G) identifiziert werden, wobei h(v) gerade der Wurzelknoten von HG (v) ist, der immer ein *-Knoten ist. • Die in D(G) von h(v) aus erreichbaren Kanten bilden den Heap HG (v), insbesondere hat jeder Knoten also maximal drei auslaufende Kanten. • D(G) hat O(m + n · log n) viele Knoten. Dies entspricht auch der benötigten Laufzeit, D(G) zu erstellen, da die Heaps HG vorbestimmt waren. Den letzten Punkt schauen wir uns noch etwas genauer an: Da Knoten des Typs 1 mit sidetrackkanten in G identifiziert werden können, existieren maximal m − (n − 1) Knoten des Typs 1 (|T | = n − 1). Knoten vom Typ 2 sind etwas aufwendiger zu zählen. Beim Erstellen des Heaps HG (v) werden blog2 ic + 1 Knoten durch die Heapify-Operation beim Einfügen der Wurzel von Hout (v) in HT (nextT (v)) aktualisiert. Wobei i hier die Anzahl der Knoten auf dem v−t Weg in T bezeichnet. Im worst-case ist T wieder Pnein Weg. Die Anzahl Typ2-Knoten beläuft sich dann auf Pn−1 i=1 log2 i ≤ n + n · log2 n i=1 blog2 ic + 1 ≤ n + Typ1 und Typ2-Knoten aufsummiert ergeben somit O(m + n · log n) viele Knoten. 3.3 Der Graph P (G) Im Graphen D(G) bilden die von h(s) erreichbaren Knoten gerade den Heap H G (s). Ich kann somit den in h(s) startenden und in w ∈ D(G) endenden Weg identifizieren mit dem s − t Weg in G, der auf T läuft und lediglich die Kante ew als sidetrackkante benutzt. 9 In den meisten Graphen kann ich aber mehrere sidetracks ausführen. Ich muss dies also durch Einfügen zusätzlicher Kanten in D(G) gewährleisten. Hierzu wird für jeden Knoten w ∈ D(G), der zu der sidetrackkante (u, v) ∈ G gehört, ein Kante (w, h(v)) eingefügt. Dies ist sinnvoll, da ich von h(v) den Heap HG (v) erreiche, also alle von h(v) ausführbaren sidetracks. Der Graph P (G) entsteht also aus D(G) durch Einfügen dieser Kanten, sowie Einfügen eines Initialknotens r(s) und einer Initialkante (r(s), h(s)). Da wir aus P (G) unsere kürzesten s−t Wege ablesen wollen, benötigen wir eine Kantenfunktion, die angibt wieviel wir bzgl des kürzesten s−t Weges an Länge verlieren. Da P (G)-Wege, die in r(s) starten, mit s − t Wegen in G identifiziert werden sollen, geschieht dies wie folgt: • Die Initialkante (r(s), h(s)) bekommt das Gewicht δ(h(s)) wobei mit δ(h(s)) der δ-Wert der sidetrackkante gemeint ist, die zu h(s) gehört. • Kanten (u, v), die schon in D(G) existierten, bekommen Gewicht δ(v) − δ(u) • Hinzugefügte Kanten (w, h(v)) bekommen Gewicht δ(h(v)) Die Wegindentifizierung läuft nun folgendermassen ab: Der leere Weg in P (G) induziert den kürzesten s − t Weg laut T. Steige ich von r(s) zu h(s) ab, bedeutet dies, daß ich mindestens einen sidetrack ausführe. Da nun alle Knoten des Heaps HG (s) erreichbar sind, welche ja gerade alle von s erreichbaren sidetrackkanten identifizieren, wähle ich den auszuführenden sidetrack, indem ich zum entsprechenden Knoten in HG (s) laufe. Dabei wird ein sidetrack jeweils signifikant, falls der P (G)-Weg endet, oder ich wieder zu einem h(v)-Knoten aufsteige. Im Fall des Aufsteigens befinde ich mich im Heap HG (v) und wähle hier wiederrum einen sidetrack, den ich ausführen möchte. Dieses Vorgehen kann ich beliebig iterieren. Die Gewichte in P (G) sind so gewählt, daß die Länge des in r(s) startenden Weges den Verlust bzgl des kürzesten s − t Weges angibt. Man verdeutliche sich diesen Sachverhalt, indem man in D(G) selbst ein paar geeignete Kanten einfügt! Eigenschaften von P (G) • P (G) hat O(m + n · log n) viele Knoten, im Vergleich zu D(G) kommt lediglich der Initialknoten hinzu. • P (G) kann in O(m + n · log n) erstellt werden, da ich lediglich alle Knoten in D(G) betrachten muss, und für diese eine weitere Kante einzufügen habe. • Jeder Knoten v ∈ P (G) hat maximal vier auslaufende Kanten, da D(G) maximal drei auslaufende Kante hat und eine Kante pro Knoten dazukommt. • Jede Kante e ∈ P (G) hat ein positives Gewicht. Dies wird durch die Heap-Eigenschaft von HG und dadurch, daß alle Einträge im Heap nicht negativ sind gewährleistet. • Jeder in r(s) startende Weg in P (G) entspricht einem s − t Weg in G und umgekehrt 10 • Für die Länge L eines solchen Weges in P (G) gilt: d(s, t) + L = l(induzierter s − t Weg) Somit liefert mir die von r(s) in P (G) erreichbaren Knoten den Heap, in dem ich die in 2.5. vorgestellte, gradbeschränkte Breitensuche zum Auffinden der k kürzesten s − t Wege laufen lassen kann. Die Gesamtlaufzeit beträgt somit O(m + n log n + k log k), O(m + n log n) für das Erstellen von P (G) und O(k log k) für die anschliessende Breitensuche. 4 4.1 Variationen des Problems und abschliessendes Beispiel Die k kürzesten Wege von s zu allen Knoten Die gemachte Konstruktion löst primär das Problem, kürzeste Wege von allen Knoten zu t zu finden. (Einfügen weiterer Initialknoten r(v) in P (G) für beliebigen Knoten v ∈ G) Man behilft sich wieder durch GR . Dieselbe Konstruktion wird in GR mit Zielknoten s ausgeführt. Die gefundenen kürzesten Wege von allen Knoten zu s in GR korrespondieren somit zu den gesuchten kürzesten Wegen von s in G. Da wir die Breitensuche nun für jeden Knoten laufen lassen, ergibt sich die Laufzeit zu O(m + n log n + n · k log k). 4.2 Wege kürzer als vorgebene Grenze Wir können natürlich auch alle s − t Wege bestimmen, die kürzer sind als eine vorgegebene Grenze. Das Abbruchkriterium der Breitensuche verändert sich einfach zu: Solange der gefundene Weg eine Länge ≤ Grenze hat. 4.3 Kürzeste Wege in Graphen mit negativen Kantengewichten Mit der Einschränkung, daß der Graph keine Zykel negativer Länge enthalten darf (denn dann ist der kürzeste Weg nicht bestimmt) funktioniert der Algorithmus auch in Graphen mit negativen Kantengewichten. Man muss allerdings noch die Laufzeit beachten, die man für das Finden eines single source shortest path tree in einem solchen Graphen benötigt. Diese beläuft sich auf O(m · n) [2]. 4.4 Die k längsten Wege In azyklischen Graphen (das Problem eines negativen Zykels eleminiert sich also von selbst) können die k längsten Wege gefunden werden, indem man alle Kantengewichte negiert und in G− alle kürzesten Wege sucht. Diese induzieren die k längsten Wege in G. Hierzu noch folgendes Beispiel: 4.5 Das 0-1 KnapsackProblem Gegeben seien n Objekte, von denen jedes allerdings nur einmal vorhanden ist. (→ 0-1). Die Objekte haben Volumen ci ∈ N sowie Wertigkeiten wi ∈ R+ . 11 Das übliche Knapsack Problem P besteht nun darin, den Sack P mit Volumen L ∈ N möglichst wertvoll aufzufüllen. Formell: xi · wi unter der Bedingung xi · ci ≤ L zu maximieren. Wobei xi = 1 falls wir das Objekt i nehmen, 0 sonst. Zur Lösung bildet man den Graphen G, der zwei ausgezeichnete Knoten s,t besitzt, sowie für alle i = 1 . . . n + 1 und alle j = 0 . . . L einen Knoten i, j. Von s zu jedem Knoten 1, j verläuft eine Kante mit Gewicht 0, von jedem Knoten i, n + 1 verläuft eine Kante zu t mit Gewicht 0. Für jeden Knoten i, j mit j < n + 1 verläuft eine Kante zu i + 1, j mit Gewicht 0, und eine Kante zu i + 1, j + ci mit Gewicht wi, falls j + ci ≤ L. Abbildung 6 zeigt den Graphen G für folgende Werte: L=5 c1 = 5, w1 = 8 c2 = 3, w2 = 4 c3 = 2, w3 = 5 c4 = 1, w4 = 2 c5 = 3, w5 = 5 Stehe ich beim Knoten i, j bedeutet dies, daß ich schon j viel Raum verbraucht habe. Nehme ich eine Querkante, so ist xi =0. Nehme ich eine Kante nach unten, so ist xi = 1. Der gebildete Graph ist azyklisch. Ich kann also laut 4.4 die längsten s − t Wege suchen, die mir die besten Lösungen für das Knapsack Problem liefern. Berücksichtigen muss ich allerdings, daß mir zwei verschiedene Wege eine gleiche Lösung induzieren können. Literatur [1] M.L.Fredman and R.E.Tarjan. Fibonacci heaps and their uses in improved network optimization algorithms. J.Assoc Comput.Mach. 34:596-615. Assoc.for Computing Machinery, 1987. [2] A.V.Goldberg. Scaling algorithms for the shortest paths problem. SIAM J.Computing 24(3):494-504. Soc.Industrial and Applied Math., June 1995 [3] David Eppstein. Finding the k shortest paths, SIAM J.Computing, Vol.28(2), 652-673, 1998 12 1,0 2,0 3,0 4,0 5,0 6,0 5,1 6,1 2 1,1 2,1 3,1 5 4,1 2 4 5 5 s 1,2 2,2 8 3,2 4,2 4 5,2 6,2 5 2 5 1,3 2,3 3,3 4,3 5,3 2 4 6,3 5 5 1,4 2,4 3,4 4,4 5,4 6,4 5,5 6,5 2 1,5 2,5 3,5 4,5 Abbildung 6: Graph zum 0-1 Knapsack Problem 13 t