5.3.3. Algorithmen für kürzeste Wege

Lernziele

Algorithmen aus shortest_path.h
Kürzeste Wege in Abstands-Graphen

Schon in Abschnitt “Ein Anwendungsbeispiel: Breitensuche in einem Graphen” haben wir uns mit kürzesten Wegen in Graphen beschäftigt. Dort haben wir festgestellt, dass in einem Graphen, in dem die Kanten keine Abstandsinformation tragen oder alle Abstände zwischen den Knoten gleich sind, die kürzesten Wege von einem Startknoten aus zu allen anderen Knoten durch Breitensuche berechnet werden können.

Häufig kommt es aber vor, dass die Knoten unterschiedliche Abstände voneinander aufweisen, wie etwa in Abbildung 5.20; wir können uns die Knoten daraus z. B. als Bahnhöfe vorstellen und die Kanten als Bahnstrecken, die mit der Länge dieser Strecke beschriftet sind. Wiederum stellt sich dann in natürlicher Weise die Frage, welches ein kürzester Pfad (Weg) von einem Startknoten s zu einem anderen Knoten t ist. Als Reisende wollen wir selbstverständlich so wenig Kilometer wie möglich fahren, denn bei der Deutschen Bahn bezahlt man pro gefahrenem Kilometer!

Abbildung 5.20. Ein Abstands-Graph und kürzeste Wege darin

Ein Abstands-Graph und kürzeste Wege darin

Die Knoten sind als Bahnhöfe zu interpretieren, die Kanten als Bahnstrecken dazwischen. Die Beschriftung einer Kante entspricht der Länge der Strecke von Bahnhof A zu Bahnhof B. Der Graph ist gerichtet. I. Allg. existiert zu jeder Kante auch eine Rückwärtskante, weil man im Normalfall immer auch von B nach A zurückfahren kann. Diese Strecke muss aber nicht gleich lang sein und sie muss nicht immer existieren.

Die kürzesten Wege vom Knoten A aus sind als dicke Kanten gezeichnet. Erreichbare Knoten haben eine dicke Umrandung.


Die Länge eines Pfades ist hier die Summe der Abstandsinformationen der Kanten des Pfades. Ein kürzester Pfad von s nach t ist ein Pfad von s nach t, dessen Länge in Bezug auf alle anderen Pfade von nach nach t minimal ist. (Oft werden kürzeste Pfade auch als „kürzeste Wege“ bezeichnet.) Diese Länge wird als Distanz von s nach t bezeichnet.

Existiert ein solcher kürzester Weg zwischen zwei Knoten s und t immer? Zunächst einmal muss natürlich t von s aus erreichbar sein. Ist er dies nicht, so gibt es überhaupt keinen Weg, also auch kürzesten, und die Distanz von s nach t wird als minus unendlich festgelegt. Problematisch sind auch negative Zyklen, d. h., Zyklen, deren Kantensumme negativ ist. (Es gibt durchaus Abstands-Graphen, die Kanten mit negativer Abstandsinformation enthalten.) Da ein solcher Zyklus in einem Pfad beliebig oft durchlaufen werden darf, kann die Länge des Pfades immer kleiner gemacht werden, kleiner als jede negative Zahl; daher wird auch in diesem Fall die Distanz als minus unendlich angesehen. Noch schlimmer: Alle Distanzen in einer starken Zusammenhangskomponente, die einen negativen Zyklus enthält, sind -∞.

Zur Lösung solcher Kürzeste-Wege-Probleme (shortest path problems) bietet LEDA einige Funktionen an, die alle in der Header-Datei shortest_path.h deklariert sind.

Als Beispiel daraus zeigen wir die Verwendung der Funktion SHORTEST_PATH():

bool SHORTEST_PATH(graph& G, node s, edge_array<int>& cost, 
                   node_array<int>& dist, node_array<edge>& pred);

Diese Funktion erwartet in einem auf dem gerichteten Graphen G gültigen Kanten-Array cost die Abstände der einzelnen Knoten zueinander und berechnet für jeden Knoten w in G die Distanz vom Starknoten s nach w; diese Distanz legt sie im Knoten-Array dist ab. Die kürzesten Wege von s zu jedem anderen Knoten w legt sie im Knoten-Array pred ab: Jedem Knoten wird die zu ihm inzidente Kante auf einem kürzesten Weg von s aus mitgeteilt; durch Zurücklaufen entlang dieser Kanten kann der kürzeste Weg von s nach w konstruiert werden. (Der Wert dieser Kante ist nil, wenn w nicht erreichbar ist.) Die Funktion liefert false zurück, wenn es einen negativen Zyklus gibt, der von s aus erreichbar ist. In diesem Fall sind alle Distanzwerte als minus unendlich anzusehen; es hat dann wenig Sinn, von „kürzesten Wegen von s aus“ zu reden.

Das folgende Programm lässt den Benutzer einen (gerichteten) Graphen editieren, wobei er die Data-Labels der Kanten mit Abstandsinformationen beschriften muss. Danach kann er einen Startknoten s mit der Maus selektieren. Von diesem ausgehend berechnet dann SHORTEST_PATH() die kürzesten Wege zu den anderen Knoten und gibt diese aus. Abbildung 5.20 wurde damit erzeugt.

Filename: ShortestPath.C
LEDA-Benutzer ab Version 5.0: Beachten Sie, dass die Header-Dateien nun in Module (Unterverzeichnisse) partitioniert sind!
#include <LEDA/shortest_path.h>
#include <LEDA/graph.h>
#include <LEDA/graphwin.h>
#include <LEDA/node_list.h>
#include <LEDA/list.h>
#include <LEDA/color.h>
#include <iostream>

using leda::GRAPH;
using leda::GraphWin;
using leda::window;
using leda::color;
using leda::node;
using leda::edge;
using leda::node_array;
using leda::edge_array;
using leda::node_list;
using leda::list;
using leda::string;
using std::cout;
using std::endl;

int main()
{
  GRAPH<string,int> G;

  GraphWin gw(G, 500, 700, "Shortest Path");
  gw.set_edge_label_type(leda::data_label);
  gw.set_node_label_type(leda::data_label);
  gw.set_node_border_width(2);
  gw.set_flush(false);
  gw.display(window::center,window::center);

  while(gw.edit()) {    

    // get start node s: get the one selected node of GraphWin
    node s;
    list<node> L = gw.get_selected_nodes();
    if(L.size() != 1) {
      gw.message("No start node or more than one selected! Click <done>.");
      gw.wait();
      gw.del_message();
      continue;
    }
    else s = L.head();

    // setup cost information: use information associated to GRAPH<,>
    edge_array<int> cost(G);
    edge e;
    forall_edges(e,G)
      cost[e] = G.inf(e);
    
    // setup node arrays for distance and predecessors
    node_array<int> dist(G);
    node_array<edge> pred(G);
    
    // compute shortest paths from s
    if(SHORTEST_PATH(G, s, cost, dist, pred)) {

      // shortest paths may have changed: reset attribute values
      gw.set_edge_color(leda::black);
      gw.set_edge_width(1);
      gw.set_node_border_color(leda::black);
      gw.set_node_border_width(1);
      
      cout << "The distances of the cities to ";
      cout << G.inf(s) << " are:" << endl;

      node v;
      forall_nodes(v, G) {

        if(v == s) { // distance is trivially 0   
          gw.set_border_color(v, leda::green);
          gw.set_border_width(v, 4);
          continue;  
        }
        cout << G.inf(v) << ": ";
        if(pred[v] == nil) { // v not reachable
          cout << G.inf(v) << " not reachable (infinite distance).";
          cout << endl;
          continue;
        }
        cout << dist[v] << " km";

        // collect nodes on path back to s
        node_list path;
        node w = v;
        w = G.opposite(w, pred[w]);
        while(w != s) {
          path.push(w); // reverse ordering
          w = G.opposite(w, pred[w]);
        }
        // if there are any, output them in reverse order
        if(!path.empty()) {
          cout << " via ";
          node x;
          forall(x, path)
            cout << G.inf(x) << " ";
        }
        cout << endl;

        // show edge as edge of a shortest path
        gw.set_color(pred[v], leda::red);
        gw.set_width(pred[v], 4);
        gw.set_border_color(v, leda::red);
        gw.set_border_width(v, 4);
      }
      gw.redraw();
    }

    else { // SHORTEST_PATH returned false
      cout << "This graph contains a negative cycle ";
      cout << "reachable from s." << endl;
    }
  }
}

Das Programm gibt folgende kürzesten Wege aus, wenn wir in Abbildung 5.20 zuerst mit der mittleren Maustaste den Knoten A als Startknoten auswählen und dann auf done drücken:

The distances of the cities to A are:
C: 5 km via D B
B: 4 km via D
D: 2 km
G: 7 km via D F
M: 7 km via D F J
L: 8 km via D F G
H: 6 km via D B
I: 8 km via D B H
F: 4 km via D
E: E not reachable (infinite distance).
O: 9 km via D F J M
K: 9 km via D F G L
P: 10 km via D F J M O
J: 6 km via D F
N: 9 km via D F J

Um im Edit-and-Run-Zyklus herauszufinden, welchen Knoten der Benutzer selektiert hat, verwenden wir die Methode get_selected_nodes() der Klasse GraphWin, die eine Liste der augenblicklich selektierten Knoten zurückgibt. Ist keiner oder mehr als ein Knoten selektiert, geht das Programm sofort in den nächsten Editiervorgang über.

Nachdem SHORTEST_PATH() die kürzesten Wege berechnet hat, benutzen wir die Kanten im Knoten-Array pred, um von jedem Knoten w entlang eines kürzesten Pfades zu s zurückzulaufen. Wir sammeln die dabei traversierten Knoten in einer node_list auf, die wir in umgekehrter Reihenfolge der Aufnahme wieder ausgeben. Dies ist zudem ein sinnvolles Beispiel für die Verwendung der speziellen Hilfsdatenstruktur node_list.

Laufzeiten und weitere Informationen

Die Funktion SHORTEST_PATH() hat Laufzeit O(n+m) auf azyklischen Graphen. Sonst läuft sie in Zeit O(m+n·log(n)), wenn alle Abstandsinformationen an den Kanten nicht-negativ sind; gibt es dagegen negative Abstände, läuft sie in Zeit O(m·min(D,n)), wobei D die maximale Anzahl von Kanten auf einem kürzesten Pfad im Graphen ist. (Diese Anzahl D ist +∞, wenn es negative Zyklen gibt, aber man beachte die Minimumfunktion!)

In shortest_path.h sind noch weitere Funktionen zum Berechnen von kürzesten Wegen deklariert. So gibt es z. B. ALL_PAIRS_SHORTEST_PATHS(), das für alle Knoten-Paare (v,w) die Länge eines kürzesten Weges von v nach w im Graphen berechnet.

Mehr Informationen über diese Funktionen finden sich auf der entsprechenden Manualseite.

Übungen

Übung 64.

Machen Sie die kürzesten Wege im obigen Programm mit Hilfe des GraphWin sichtbar.