Seite Wege-1 Wege in Graphen Grundidee. Zu den häufigsten Aufgaben, die mit Hilfe der Graphentheorie gelöst werden können, zählt die Bestimmung von Wegen in Graphen: a b Gibt es einen Weg von a nach b? Gibt es einen Weg zwischen zwei gegebenen Knoten a und b? Eine solche Frage stellt sich beispielsweise bei der Planung eines Einbahnstraßennetzes oder bei der Untersuchung eines Höhlenlabyrinthes, aber auch bei der Planung eines Schwertransportes unter Berücksichtigung von Höhenbeschränkungen, minimalen Kurvenradien, Gewichtsbeschränkungen usw. Diese Frage können wir beantworten, wenn wir die transitive Hülle des Graphen berechnet haben. Allerdings wäre ein solches Vorgehen unnötig aufwendig, wenn wir nur nach einem einzigen Knotenpaar a, b fragen, denn die transitive Hülle beantwortet unsere Frage ja für alle möglichen Knotenpaare. Außerdem sind wir häufig nicht nur an der Existenz eines Weges interessiert, sondern wir wollen den Weg auch kennen, falls er existiert. Da hilft uns aber die transitive Hülle nicht weiter. Was ist der "kürzeste Weg" zwischen zwei gegebenen Knoten a und b? Wir sind häufig an Wegen minimaler Länge interessiert. Die Länge eines Weges ist definiert als Anzahl der Kanten, die der Weg enthält. Zum Beispiel sind wir bei der Planung einer Bahnreise mit umfangreichem Gepäck daran interssiert, möglichst selten umsteigen zu müssen. Die Umsteigpunkte sind dann die Knoten unseres Graphen, und die Kanten dazwischen stellen die Bahnfahrten zwischen den Umsteigbahnhöfen dar. – Ist unser Graph ein kantenbewerteter Graph, so meint man mit der Suche nach dem kürzesten Weg jedoch nicht den Weg mit minimaler Länge, sondern jenen Weg mit minimalen Kosten. Unter den Kosten des Weges wird dann die Summe der Kantenbewertungen verstanden. Man beachte, daß der "kürzeste Weg" nicht in jedem Fall existieren muß: Enthält der Graph Zyklen mit negativen Bewertungen, so kann man die Gesamtkosten eines Weges durch Durchlaufen eines solchen Zyklusses verkleinern, so daß der Begriff des "kürzesten Weges" dann keinen rechten Sinn mehr macht! Bei der Routenplanung in einer digitalen Straßenkarte treten solche Probleme nicht auf, denn es bietet sich an, die Kanten in einem solchen Graphen mit der erforderlichen Fahrzeit oder mit der Länge der Verbindungsstrecke zu bewerten (also mit Werten, die immer Seite Wege-2 größer oder gleich Null sind). Bei der Fahrt auf einem Autobahnnetz mit Straßenbenützungsgebühren werden wir die Bewertungen natürlich entsprechend der jeweils zu zahlenden Maut festlegen. Flensburg Kiel Rostock Lübeck Hamburg Wilhelmshaven Lüneburg Emden Oldenburg Bremen Berlin Frankfurt/Oder Hannover Braunschweig Wolfenbüttel Magdeburg Hildesheim Osnabrück Cottbus Bielefeld Clausthal-Zellerfeld Paderborn Jülich Köln Aachen Bonn Halle Göttingen Dortmund Bochum Essen Duisburg Hagen Wuppertal Düsseldorf Leipzig Kassel Dresden W eimar Erfurt Jena Gera Chemnitz Zwickau Ilmenau Hof Siegen Giessen Koblenz Frankfurt/Main Wiesbaden Mainz Darmstadt Schweinfurt Bamberg Bayreuth Würzburg Trier W orms Ludwigshafen Kaiserslautern Heidelberg Saarbrücken Erlangen Crailsheim Nürnberg Landau/Pfalz Heilbronn Regensburg Karlsruhe Ingolstadt Ludwigsburg Pforzheim Stuttgart Tübingen Reutlingen Ulm Augs burg München MemmingenLandsberg/Lech Freiburg Passau Landshut Salzburg Garmisc h-Partenkirchen Konstanz Lindau Routenplanung in einem Straßennetz (kantenbewerteter Graph, Kantenbewertung=Streckenlänge) Bei einem knotenbewerteten Graphen werden wir die Kosten eines Weges als Summe der Bewertungen derjenigen Knoten ermitteln, die im Weg enthalten sind. Zum Beispiel könnte es sein, daß wir eine Bahnreise unternehmen und dabei die Länge der Fußwege an den Umsteigbahnhöfen minimieren wollen, weil wir z.B. schlecht zu Fuß sind. In diesem Falle sind die Knoten für die "Kosten" (Fußwege) ausschlaggebend, nicht die Kanten. – In manchen Fällen sind wir nicht an kürzesten, sondern an längsten Wegen interessiert. Eine solche Fragestellung tritt zum Beispiel bei der Projektplanung eines Bauvorhabens auf. Jede Kante entspricht einem Teilvorgang des Gesamtprojektes. Die Knoten eines solchen Graphen entsprechen Beginn und Ende der jeweiligen Bauvorgänge. Die Kantenbewertungen entsprechen der Zeitdauer des jeweiligen Vorganges. Auch die Abfolge der Teilvorgänge (erst das Fundament legen, dann die Pfeiler bauen, zum Schluß den Überbau der Brücke auflegen) Seite Wege-3 wird durch Kanten definiert (ggf. sind hier auch Wartezeiten, also Kantenbewertungen größer Null, zu berücksichtigen: Der Pfeiler kann erst gebaut werden, wenn das Fundament eine gewisse Zeit geruht hat, usw.). Sucht man die erforderliche (minimale!) Bauzeit und die dafür relevanten Vorgänge, so muß der längste Weg (Weg mit maximalen Kosten) vom Baubeginn bis zu Fertigstellung ermittelt werden. Diese graphentheoretische Aufgabe stellt die Grundlage der sogenannten Netzplantechnik dar. Single-source shortest paths, all-pairs shortest paths. Gelegentlich sind wir nicht nur an Wegen zwischen einem einzigen Knotenpaar interessiert, sondern an allen Knotenpaaren, die zum Beispiel einen bestimmten Startknoten s enthalten. Gelegentlich werden auch alle Verbindungen zwischen allen Knoten eines Graphen gesucht, also diejenigen Wege, deren Existenz in der transitiven Hülle des Graphen angezeigt ist. Prinzipielle Vorgehensweise. Im folgenden wollen wir zunächst die prinzipielle Vorgehensweise eines Traversierungsverfahrens schildern. Unter der Traversierung eines Graphen verstehen wir das systematische Aufsuchen der Knoten des Graphen, ausgehend von einem gegebenen Startknoten. Der zu untersuchende Graph ist gegeben als G(V,E), wobei V die Menge der Knoten des Graphen und E die zugehörige Kantenmenge bezeichnen soll. Die im folgenden angegebenen Algorithmen lassen sich sowohl für gerichtete als auch für ungerichtete Graphen einsetzen, falls nichts anderes angegeben ist. Beim Traversieren des Graphen werden wir uns von einem Knoten zum nächsten "durchhangeln", indem wir den Kanten folgen, die vom gerade betrachteten Knoten ausgehen. Die Endknoten dieser Kanten bezeichnen wir als "Nachbarn" des gerade betrachteten Knotens. Menge F Menge A Menge U s Prinzipielles Vorgehen bei der Wegesuche (Traversierung) Zur Durchführung der Traversierung eines Graphen ist es notwendig, an jedem Knoten des Graphen Markierungen anbringen zu können. Eine Markierung ist eine temporäre Kennzeichung eines Knotens, die wir zum Zweck der Durchführung unseres Verfahrens benötigen; im Gegensatz zu den Bewertungen ändern sich die Markierungen also im Laufe des Verfahrens. Sobald wir beim Durchsuchen eines Graphen einen Knoten erstmals entdecken, markieren wir ihn als "bekannt", ähnlich wie man bei der Suche nach dem Ausgang aus einem Höhlenlabyrinth an jeder Verzweigung, an der man ankommt, einen Kreidestrich anbringt. Die Markierungen dienen dazu, die Knoten des Graphen temporär einer von drei Mengen zuzuordnen (siehe vorstehende Abbildung). Wir unterscheiden die Menge U der noch unbesuchten Knoten, die Menge A der aktiven Knoten und die Menge F (wie "fertig") der "erledigten" oder endgültig behandelten Knoten. Ziel der Traversierung ist es, alle Knoten, die vom gegebenen Startknoten aus erreichbar sind, der Reihe nach aufzufinden Seite Wege-4 und von der Menge U in die Menge F zu befördern. Dabei werden die betrachteten Knoten in der Menge A zwischengelagert: Im Ausgangszustand sind alle Knoten des Graphen als "unbekannt" zu markieren. Anders ausgedrückt, zu Beginn der Traversierung befinden sich alle Knoten des Graphen in der Menge U der unbesuchten Knoten. Aus dieser Menge wird jetzt zunächst der Startknoten entfernt und als AKTIV markiert. Damit wird der Startknoten zum aktiven Knoten, wird also aus U entfernt und in die Menge der aktiven Knoten A eingetragen. Nun nimmt eine iterative Vorgehensweise ihren Lauf, bei der die Menge A im Zentrum des Interesses steht. In jedem Schritt des Traversierungsverfahrens werden wir jeweils einen aktiven Knoten v aus A auswählen und von diesem Knoten aus den Graphen weiter durchsuchen. Alle Nachbarn von v, die noch in U enthalten sind, werden aus U entfernt und in die Menge A eingetragen. Sobald wir alle Nachbarn des gerade aktuell bearbeiteten aktiven Knotens v untersucht haben, hat der aktuell bearbeitete Knoten v seinen Dienst geleistet. Er wird aus der Menge der aktiven Knoten A entfernt und in die dritte Menge, nämlich die Menge F der "erledigten" Knoten eingereiht. Wir führen das Verfahren solange fort, solange noch Elemente in der Menge A enthalten sind. Am Ende des Verfahrens ist die Menge A also leer. Alle Knoten, die vom gewählten Startknoten aus erreichbar sind, finden wir dann in der Menge F, und alle Knoten, die vom gewählten Startpunkt aus nicht erreichbar sind, sind am Ende des Verfahrensablaufes immer noch in der Menge U enthalten. Was wir hier in Prosa geschildert haben, können wir auch als Rohentwurf unseres Algorithmus formalisiert anschreiben. Dabei bezeichnen wir die Knotenmenge unseres Graphen mit V und die Kantenmenge mit E. vs sei der vorgegebene Startknoten, von dem aus die Traversierung des Graphen erfolgen soll: Algorithmusskizze "Traversierung": gegeben: Graph G(V,E),Startknoten vs Für alle Knoten v aus V: v.statusÅUNBESUCHT Erzeuge: Menge der aktiven Knoten: A A.Einfügen(vs) Solange A.Anzahl()>0 wiederhole: vÅA.EntferneEinenKnoten() für alle Nachbarknoten w von v: falls w.status=UNBESUCHT: w.statusÅAKTIV A.Einfügen(w) v.status=ERLEDIGT In unserem Pseudocode haben wir das Zeichen Å als Zuweisungszeichen benützt, um es vom Operator "Vergleich auf Gleichheit" = abzuheben. Unsere Algorithmusskizze setzt voraus, daß jeder Knoten eine Markierung v.status trägt, die die Werte {UNBESUCHT, AKTIV, ERLEDIGT} annehmen kann. Je nach Wert der Markierung gehört der Knoten zur Menge U, A oder F. Außerdem haben wir formal die Menge A mit den zugehörigen Operationen A.Einfügen(v) A.EntferneEinenKnoten() Seite Wege-5 eingeführt. Die erste Operation fügt den Knoten v in die Menge A ein, die zweite entfernt irgendeinen Knoten aus A und liefert als Ergebnis den entfernten Knoten zurück. Nach welchem Prinzip der zu entfernende Knoten aus den gerade in der Menge A enthaltenen Elementen ausgewählt werden soll, haben wir vorläufig offengelassen. Wir könnten hier beispielsweise das Prinzip Zufall walten lassen, oder aber eine strenge Regel vorgeben. Erst durch die genaue Festlegung der Funktion A.EntferneEinenKnoten()wird aus unserer Algorithmusskizze ein echter Algorithmus. Die Funktion A.Anzahl() liefert die Anzahl der aktuell in A enthaltenen Elemente zurück. Offengelassen ist in der Algorithmusskizze auch die Frage, was die Anweisung für alle Nachbarknoten w von v: genau bedeuten soll. In welcher Reihenfolge die Nachbarn des Knotens v gefunden werden, wird in unserer Implementierung von der Wahl und der genauen Gestalt der Datenstruktur abhängen, die wir zur Speicherung des Graphen verwenden. Beispielsweise ist bei Wahl der Datenstruktur "Adjazenzliste" die genaue Reihenfolge der Nachbarn von v gegeben. In den folgenden Beispielen geben wir daher nicht nur die graphische Darstellung des Graphen an, sondern auch die verwendete Datenstruktur, so daß die Vorgehensweise eindeutig festgelegt ist. Abschließend merken wir zur Algorithmusskizze noch an, daß die Mengen U und F nur implizit über die Markierung v.status gegeben sind, während wir für die Menge A tatsächlich eine eigenständige Datenstruktur benötigen, die zur Durchführung des Algorithmus temporär aufgebaut und aktualisiert werden muß. Tiefensuche. Eine naheliegende Vorgehensweise zur Konkretisierung unserer Algorithmusskizze besteht darin, die Menge A als Stapel zu verwalten. Ein Stapel (stack) ist eine Organisationsstruktur für Daten, die dem Prinzip "LIFO", d.h. "last in – first out" gehorcht. Aus dem täglichen Leben kennen wir das vom Fahren im Fahrstuhl: Wer als letzter eingestiegen ist, steht in der Regel am nächsten an der Tür und verläßt den Aufzug als erster. Man kann sich den Stapel auch bildlich als einen Stapel von Blättern (z.B. Briefe im Posteingangskörbchen) denken, der immer von oben bearbeitet wird: neue Blätter werden obenauf gelegt, zu bearbeitende Blätter werden oben vom Stapel heruntergenommen. Die Verweilzeit der zuerst auf den Stapel gelegten Blätter wird also viel länger sein als die der später hinzugekommenen. Da Stapel sehr häufig in der Informatik vorkommen, hat es sich eingebürgert, für die beiden Operationen "lege ein neues Element auf den Stapel" und "entferne das oberste Element des Stapels" die standardisieren Namen push(element) und pop() zu verwenden. Um aus unserer Algorithmusskizze den Algorithmus "Tiefensuche" zu machen, ersetzen wir die Zeile Erzeuge: Menge der aktiven Knoten: A durch Stapel A und die Anweisungen vÅA.EntferneEinenKnoten() durch vÅA.pop() sowie A.Einfügen(w) durch A.push(w). Damit finden wir: Seite Wege-6 Algorithmus "Tiefensuche": gegeben: Graph G(V,E),Startknoten vs Für alle Knoten v aus V: v.statusÅUNBESUCHT Stapel A A.push(vs) Solange A.Anzahl()>0 wiederhole: vÅA.pop() für alle Nachbarknoten w von v: falls w.status=UNBESUCHT: w.statusÅAKTIV A.push(w) v.status=ERLEDIGT Was jetzt noch fehlt, ist eine genaue Festlegung, in welcher Reihenfolge wir die Nachbarn des jeweils aktuell betrachteten Knotens v bearbeiten wollen: für alle Nachbarknoten w von v: An sich sind alle Nachbarn eines Knotens gleichberechtigt; die Reihenfolge, die durch unsere Datenstruktur (Adjazenzliste, Kantenliste) unter diesen gleichberechtigten Nachbarn festgelegt wird, ist willkürlich. Ändert man die Reihenfolge der Nachbarn eines Knotens, so stellt auch die geänderte Datenstruktur denselben Graphen dar; allerdings wird sich der Ablauf unseres Verfahrens dann auch ändern, weil wir die Knoten in anderer Reihenfolge aufsuchen werden. Der Tiefensuche-Algorithmus reagiert sehr empfindlich auf eine Änderung der Reihenfolge, in der die Nachbarknoten des aktuell untersuchten Knotens aufgesucht werden. Um für die folgenden Beispiele eine Eindeutigkeit und möglichst große Anschaulichkeit zu erzielen, legen wir daher hier willkürlich folgende Vorgehensweise bei der Tiefensuche fest: Entsprechend der Wahl der Datenstruktur "Stapel" für die Menge A fügen wir die Nachbarn eines Knotens genau in der umgekehrten Reihenfolge in die Menge A ein, in der sie in der Adjazenzliste oder Kantenliste stehen, damit auch auf Ebene des einzelnen Knotens die Nachbarn in der Reihenfolge "last in – first out" bearbeitet werden. Das folgende Beispiel soll illustrieren, daß der Algorithmus "Tiefensuche" der Vorgehensweise entspricht, die man anwendet, wenn man den Ausgang aus einem Höhlenlabyrinth sucht: Man wählt vom Ausgangspunkt einen Gang und geht in diesen hinein. An jeder Gabelung des Ganges entscheidet man sich für einen der weiterführenden Gänge, bis man an einem toten Ende angelangt ist. Dann geht man zurück bis zur letzten Gabelung und untersucht von dort aus die anderen möglichen Gänge. Im schlimmsten Fall muß man bis zum Ausgangspunkt zurückkehren und von dort aus in anderen Richtungen weitersuchen. Man geht also bevorzugt in die "Tiefe" und nicht in die "Breite". Auf diese Weise vermeidet man, mehrfach dieselben Gänge durchschreiten zu müssen, es kann aber passieren, daß man viel Zeit mit einer Suche in einer wenig erfolgversprechenden Richtung verbringt. Das stört uns zunächst nicht, weil das Ziel unseres Traversierungsverfahrens ja nicht darin besteht, einen bestimmten Knoten (den "Ausgang aus der Höhle") zu finden, sondern vielmehr darin, das gesamte Höhlensystem vom gegebenen Startknoten aus zu erkunden und zu erforschen. Beispiel. Die nachfolgende Adjazenzliste beschreibt den auch graphisch dargestellten gerichteten Graphen: Seite Wege-7 1 2 5 3 6 7 4 Adjazenzliste: Knoten Liste der Nachbarn 1 2,3,4 2 5,6 3 6 4 3,6 5 6,7 6 3 7 Mit diesem Graphen und der gegebenen Datenstruktur führen wir jetzt eine Tiefensuche vom Knoten 1 aus durch. Wir erhalten somit folgenden Ablauf des Verfahrens: Tiefensuche Schritt aktueller Knoten v 0 1 1 2 2 3 5 4 7 5 6 6 3 7 4 Menge A, als Stapel verwaltet: 1 4,3,2 4,3,6,5 4,3,6,7 4,3,6 4,3 4 - In jeder Iteration des Verfahrens werden die neu aufgefundenen Knoten an die bisher in der Menge A befindlichen angehängt; im nächsten Schritt wird jeweils das letzte Element der Menge A entfernt und als neuer aktueller Knoten gewählt. Das Verfahren nimmt alle Kanten des Graphen, die von den gefundenen Knoten ausgehen, genau einmal in die Hand. Allerdings liefert uns nicht jede Kante einen bis dahin unbekannten Knoten. Manche Kanten führen zu bereits vorher aufgefundenen Knoten zurück. Nur solche Knoten, die bislang noch unentdeckt geblieben sind (also die Markierung status= UNBESUCHT tragen), dürfen in die Menge A eingetragen werden, sonst terminiert unser Verfahren nicht, weil wir ständig im Kreis herumgehen. Seite Wege-8 1 2 5 3 6 7 4 Im vorstehenden Bild haben wir alle Kanten hervorgehoben, die wir während der Durchführung des Verfahrens tatsächlich benützt haben, um von einem aktuellen Knoten zum jeweils nächsten aktuellen Knoten zu gelangen. Falls es eine solche Kante nicht gibt, müssen wir im Tiefensuche-Algorithmus soweit zurückgehen, bis wir an einem früher behandelten Knoten eine Kante finden, die zum nächsten aktuellen Knoten führt. Diese Kanten bilden zusammen mit den vom Startknoten aus erreichbaren Knoten einen sogenannten Baum. In unserem Fall wird dieser Baum als Tiefensuchebaum bezeichnet. Es handelt sich um einen Baum, der den von 1 aus erreichbaren Teilgraphen des Graphen G vollständig aufspannt. Das bedeutet, daß wir ohne Beinträchtigung der Erreichbarkeit der anderen Knoten vom Startknoten 1 aus alle Kanten aus dem Graphen entfernen könnten, die nicht im Tiefensuchebaum enthalten sind. Man beachte, daß eine geringe Änderung der Reihenfolge, in der die an sich gleichberechtigten Nachbarn des aktuell gerade untersuchten Knotens abgearbeitet werden, eine gänzlich andere Suchreihenfolge auslösen kann. Unabhängig davon strebt der Tiefensuche-Algorithmus aber stets sofort in die "Tiefe" des Graphen und versucht, die Umgebung des Startknotens möglichst schnell zu verlassen und nur dann ein Stück zurückzukehren, wenn die Suche in einer "Sackgasse" endet. Je nach den Details, wie der Algorithmus durchgeführt wird, ergeben sich verschiedene, aber letztlich äquivalente Tiefensuchbäume. Rekursive Version der Tiefensuche. Der Tiefensuche-Algorithmus läßt sich in sehr bequemer Weise als rekursiver Algorithmus programmieren. Daher soll auch diese Version der Tiefensuche hier vorgestellt werden. Algorithmus "Tiefensuche rekursiv": Prozedur Start(Knoten vs): gegeben: Graph G(V,E) Für alle Knoten v aus V: v.statusÅUNBESUCHT Besuche(vs) Prozedur Besuche(Knoten v): v.statusÅAKTIV für alle Nachbarknoten w von v: falls w.status=UNBESUCHT: Besuche(w) v.statusÅERLEDIGT Seite Wege-9 Hier kommen wir ohne eine explizite Beschreibung der aktiven Knotenmenge A aus, weil der Stapel implizit im Rechner durch Speichern der Rücksprungadressen der rekursiven Aufrufe der Prozedur Besuche angelegt wird. Um einen identischen Ablauf wie bei der nichtrekursiven Version der Tiefensuche zu erreichen, nehmen wir uns in der Schleife für alle Nachbarknoten w von v: die Nachbarknoten nunmehr in exakt derjenigen Reihenfolge vor, in der sie durch die Adjazenzliste oder Kantenliste gegeben sind (also nicht in umgekehrter Reihenfolge wie bei der nichtrekursiven Version). Das Verfahren wird durch Aufruf der Prozedur Start mit dem vorher bestimmten Startknoten vs angestoßen. Mit den Daten des Beispiels zur nichtrekursiven Version der Tiefensuche erhalten wir für eine vom Knoten 1 ausgehende Tiefensuche folgende Reihenfolge der Aufrufe: Besuche(1) Besuche(2) Besuche(5) Besuche(6) Besuche(3) - Rücksprung (zu 6) - Rücksprung (zu 5) Besuche(7) - Rücksprung (zu 5) - Rücksprung (zu 2) - Rücksprung (zu 1) Besuche(4) Wir erhalten also den folgenden Tiefensuchebaum: 1 2 5 3 6 7 4 Dieser Baum unterscheidet sich geringfügig von dem der nichtrekursiven Version. In unserem Fall wird von Knoten 6 aus der Knoten 3 aufgesucht, da dieser zum fraglichen Zeitpunkt von 1 aus noch nicht als AKTIV markiert worden ist. Will man die nichtrekursive Version der Tiefensuche derart umbauen, daß diese die Knoten exakt in derselben Reihenfolge besucht wie die rekursive Version, so muß man zulassen, daß die Datenstruktur A (aktive Knoten) denselben Knoten auch mehrfach enthalten darf: Seite Wege-10 Algorithmus "Tiefensuche mit Mehrfachnennung": gegeben: Graph G(V,E),Startknoten vs Für alle Knoten v aus V: v.statusÅUNBESUCHT Stapel A A.push(vs) Solange A.Anzahl()>0 wiederhole: vÅA.pop() falls v.status≠ERLEDIGT für alle Nachbarknoten w von v: falls w.status≠ERLEDIGT: w.besuchtÅAKTIV A.push(w) v.statusÅERLEDIGT Wir tragen jetzt auch solche Knoten in den Stapel ein, die bereits AKTIV gesetzt worden sind. Um zu verhindern, daß wir im "Kreis" herumsuchen, müssen wir eine weitere Abfrage falls v.status≠ERLEDIGT ergänzen, so daß jeder Knoten nur einmal seine Nachbarn absuchen kann. Mit diesem etwas "exotischen" Algorithmus liefert die nichtrekursive Variante der Tiefensuche für unser Beispiel den folgenden Ablauf: Tiefensuche, Mehrfachnennung von Knoten in A zugelassen Schritt aktueller Stapel A Knoten v 0 1 1 1 4,3,2 2 2 4,3,6,5 3 5 4,3,6,7,6 4 6 4,3,6,7,3 5 3 4,3,6,7 6 7 4,3,6 (Leerschritt) 6 4,3 (Leerschritt) 3 4 7 4 Jetzt ist die Tiefensuchereihenfolge also identisch mit der Knotenreihenfolge, die die rekursive Version geliefert hat. Allerdings werden unnötige Schritte verbraucht, um Elemente aus dem Stapel A zu entfernen, die bereits als ERLEDIGT markiert sind (hier die Elemente 6 und 3). Außerdem ist der Stapel A dann nicht mehr als Menge im mathematischen Sinne anzusehen. Man wird bei einer nichtrekursiven Implementierung also nicht zu dieser Version greifen. Das vorstehende Bild liefert aber eine präzise Vorstellung von dem Stapel A, der bei der rekursiven Variante implizit aufgebaut wird. Die rekursive Programmierung ist bestechend einfach, wir brauchen die Datenstruktur "Stapel" nicht selbst zu programmieren, sondern können die Verwaltung des Stapels dem Betriebssystem des Rechners überlassen; die überflüssigen Operationen, die in der rekursiven Variante ausgeführt werden (mehrfache Abfrage derselben Knoten, ob diese bereits erledigt sind), müssen wir dann eben in Kauf nehmen. Seite Wege-11 Da die Tiefensuchereihenfolge bei der nichtrekursiven Version mit Stapel sehr stark von den Details der Implementierung abhängt, beziehen wir uns im folgenden immer auf die rekursive Version der Tiefensuche, wobei die Nachbarn jedes Knotens in der Reihenfolge betrachtet werden, wie sie durch die Adjazenz- oder Kantenliste vorgegeben ist. Da bei der rekursiven Version der Stapel automatisch erzeugt wird, ist so eine größtmögliche Eindeutigkeit des Verfahrens gewährleistet. Anwendungen der Tiefensuche. Test auf Kreisfreiheit, topologische Sortierung. Wir wollen kurz einige typische Anwendungen der Tiefensuche vorstellen. Bei der Planung von Prozessen haben wir häufig einzelne Teilprozesse in eine widerspruchsfreie Reihung zu bringen. Beispielsweise sind bei der Abwicklung eines Bauprojektes bestimmte Planunterlagen fertigzustellen, bevor überhaupt ein Antrag auf Baugenehmigung gestellt werden kann; ist diese genehmigt, kann die Ausführungsplanung, die Ausschreibung und schließlich die Errichtung des Bauwerkes angegangen werden. Wir wollen die Problematik an einem etwas überschaubareren Prozeß erläutern. Die nachfolgende Tabelle gibt Teilprozesse des Prozesses "Ankleiden" an. Dabei sind jedem Prozeß diejenigen anderen Prozesse zugeordnet, die Voraussetzung für die Durchführung sind (um einen zusammenhängenden Graphen zu erhalten, sind einige Voraussetzungen eingeführt worden, die dem Leser unnötig erscheinen mögen, z.B. fordern wir, daß erst die Unterhose und dann die Socken angezogen werden): lfd. Nummer 1 2 3 4 5 6 7 8 Beschreibung Unterhose Socken Hose Schuhe Mantel ausgehfertig! Unterhemd Hemd Voraussetzungen 1 1 2,3 3,8 4,5 1 7 In Form eines Graphen sieht die Aufgabenstellung aus wie folgt: 7 2 4 1 3 8 5 6 Seite Wege-12 Wir wollen jetzt die genannten 8 Vorgänge in eine zeitliche Reihung bringen, die widerspruchsfrei ist, d.h. die 8 Knoten sollen derart angeordnet werden, daß alle Kanten von links nach rechts verlaufen. Dazu führen wir, ausgehend vom Knoten 1 ("Unterhose", der einzige Knoten des Graphen, der den Eingangsgrad 0 hat), eine rekursive Tiefensuche durch (siehe oben). Sobald wir einen Knoten im Tiefensuchalgorithmus endgültig verlassen, fügen wir den Knoten an das Ende einer Liste aller besuchten Knoten an: Algorithmus "Topologische Sortierung": Prozedur Start(Knoten vs): gegeben: Graph G(V,E) Für alle Knoten v aus V: v.statusÅUNBESUCHT Liste L BesucheListe(vs,L) Prozedur BesucheListe(Knoten v, Liste L): v.statusÅAKTIV für alle Nachbarknoten w von v: falls w.status=UNBESUCHT: Besuche(w) v.statusÅERLEDIGT Liste.Append(v) Die Funktion Liste.Append fügt am Ende unserer "Liste" eine neue Zeile an und trägt dort den angegebenen Knoten ein. Nach Durchführung des Verfahrens geben wir die Einträge der Liste in umgekehrter Reihenfolge aus. In unserem Beispiel gibt sich dabei folgendes Ergebnis: Knoten 1 7 8 3 5 2 4 6 Beschreibung Unterhose Unterhemd Hemd Hose Mantel Socken Schuhe ausgehfertig! Man sieht, daß alle angeführten Tätigkeiten in der angegebenen Reihenfolge nacheinander durchgeführt werden können, ohne daß Unzuträglichkeiten entstehen. Die "Sortierung", die wir hier erzielt haben, heißt topologische Sortierung. Sie macht bei allen Aufgaben Sinn, bei denen die Kanten eines gerichteten Graphen eine "Ordnungsrelation" beschreiben (gerichtete Kante a-b existiert, falls "a kleiner b", oder "a kommt vor b", oder "a ist Voraussetzung von b"). Die topologische Sortierung existiert nur dann, wenn der Graph keine Widersprüche enthält, etwa a<b<c<a. Man sieht anschaulich leicht, daß solche Widersprüche Zyklen im Graphen entsprechen. Seite Wege-13 Man kann die Tiefensuche auch benützen, um den Test auf genau solche Widersprüche durchzuführen. Dazu traversiert man den Graphen mit Hilfe der Tiefensuche. Trifft man dabei auf Knoten, die bereits als AKTIV markiert sind, so enthält der Graph Zyklen. Ein ungerichteter Baum, der keine Zyklen enthält, wird als Wald bezeichnet. Ist dieser Graph zusammenhängend, so heißt er Baum. Breitensuche. Eine zur Tiefensuche alternative Idee besteht darin, anstelle der gleich tief in den Graphen vordringenden Tiefensuche eine Art "ringförmiger" Suche um den Ausgangsknoten herum zu veranstalten. Diese Vorgehensweise entspricht vielleicht derjenigen, die man anwendet, um nach einem Wohnortwechsel die neue Umgebung zu erkunden: Erst wird man die umliegenden Straßen kennenlernen, dann die Nachbarortschaften, schließlich wird man den Radius der Ausflüge auch ins weitere Umland ausdehnen. Für unseren Algorithmus entspricht dies der Verwaltung der Elemente der Menge A in einer Datenstruktur, die dem Prinzip "FIFO", d.h. "first in – first out" genügt. Eine solche Datenstruktur wird auch als Warteschlange (queue) bezeichnet. Unser Algorithmus lautet dann wie folgt: Algorithmus "Breitensuche": gegeben: Graph G(V,E),Startknoten vs Für alle Knoten v aus V: v.statusÅUNBESUCHT Warteschlange A A.Append (vs) Solange A.Anzahl()>0 wiederhole: vÅA.RemoveFront() für alle Nachbarknoten w von v: falls w.status=UNBESUCHT: w.statusÅAKTIV A.Append(w) v.statusÅERLEDIGT Hier haben wir die Datenstruktur "Warteschlange" mit den Operationen Append(v) (Anhängen des Knotens v an das Ende der Warteschlange) und RemoveFront() (Herausnehmen des vordersten Knotens aus der Warteschlange) eingeführt. Die Schleife für alle Nachbarknoten w von v: bearbeiten wir jetzt der Einfachheit halber exakt in derjenigen Reihenfolge, in der die Nachbarn in der zugrundegelegten Datenstruktur des Graphen angetroffen werden. Seite Wege-14 1 2 5 3 6 7 4 Adjazenzliste: Knoten Liste der Nachbarn 1 2,3,4 2 5,6 3 6 4 3,6 5 6,7 6 3 7 Mit diesem Graphen und der gegebenen Datenstruktur führen wir jetzt eine Breitensuche vom Knoten 1 aus durch. Wir erhalten somit folgenden Ablauf des Verfahrens: Breitensuche Schritt aktueller Menge A, als Warteschlange verwaltet: Knoten v 0 1 1 1 2,3,4 2 2 3,4,5,6 3 3 4,5,6 4 4 5,6 5 5 6,7 6 6 7 7 7 Im folgenden Bild sind diejenigen Kanten hervorgehoben, die im Verlauf der Breitensuche jeweils zu einem bis dahin noch unbekannten Knoten führen: Seite Wege-15 1 2 5 3 6 7 4 Diese Kanten bilden zusammen mit den vom Startknoten aus erreichbaren Knoten den Breitensuchebaum. Man erkennt deutlich, daß die Breitensuche weiter entfernte Knoten des Graphen später findet als solche Knoten, zu denen vom Startknoten aus ein Weg geringer Länge führt. Selbstverständlich gibt es zur Breitensuche keine rekursive Version, denn Rekursion heißt ja immer, daß die Datenstruktur "Stapel" irgendwie ins Spiel kommt. Die Breitensuche stützt sich hingegen ausschließlich auf das FIFO-Prinzip der Warteschlange. Die Reihenfolge, in der die Nachbarn des aktuellen Knotens aufgesucht werden, hat bei der Breitensuche nur lokalen Einfluß; der prinzipielle Ablauf, daß sich die besuchten Knoten "wellenartig" vom Startknoten weg ausbreiten, ist unabhängig von dieser Detailfrage. Breitensuchealgorithmen zählen zu den wichtigsten Grundlagen der Wegesuche in Graphen. Aufgaben wie die optimale Numerierung von Knoten eines Finite-Elemente-Netzes, die Bestimmung kürzester Wege oder kritischer Wege oder die Programmierung eines entscheidungsunterstützenden Systems löst man durch Abwandlung des Grundprinzips der Breitensuche. Aufzeichnung des Rückweges. Sowohl mit der Tiefensuche als auch mit der Breitensuche können wir feststellen, ob vom Startknoten ein Weg zu einem beliebigen anderen Knoten des Graphen führt. Bisher ist allerdings die wichtige Frage unbeantwortet geblieben, wie wir diesen Weg eigentlich nachher nachvollziehen können. Die Breiten- oder Tiefensuche, wie wir sie hier vorgestellt haben, geht immer von einem Startknoten aus; von dort aus werden alle anderen erreichbaren Knoten besucht. Ein Durchlauf des Algorithmus liefert also ein ganzes Bündel von Wegen, die alle vom Startknoten ausgehen und an verschiedenen Zielknoten enden. Es ist daher sinnvoll, in jedem Knoten den Rückweg festzuhalten, also den Knoten, von dem wir im Laufe der Traversierung des Graphen hergekommen sind. Von dort können wir dann rekursiv bis zum Startknoten zurückgehen. Um den Rückweg aufzuzeichnen, bringen wir an jedem Knoten eine weitere Markierung an. Sobald wir einenKnoten auffinden, markieren wir den Knoten mit demjenigen Knoten, von dem wir hergekommen sind. Unsere Traversierung bekommt mit diesen Vorgaben folgende Gestalt: Seite Wege-16 Algorithmusskizze "Traversierung mit Rückweg": gegeben: Graph G(V,E),Startknoten vs Für alle Knoten v aus V: v.statusÅUNBESUCHT v.vorgaengerÅNULL Menge der aktiven Knoten: A A.Einfügen(vs) Solange A.Anzahl()>0 wiederhole: vÅA.EntferneEinenKnoten() für alle Nachbarknoten w von v: falls w.status=UNBESUCHT: w.statusÅAKTIV w.vorgaengerÅv A.Einfügen(w) v.statusÅERLEDIGT Wir haben hier die Markierung v.vorgaenger eingeführt. NULL soll einen undefinierten Wert bezeichnen. Am Beispiel der Breitensuche in dem oben vorgestellten Graphen wollen wir das Vorgehen beispielhaft illustrieren: Breitensuche Schritt aktueller Knoten v 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 Menge A, als Warteschlange verwaltet: 1 2,3,4 3,4,5,6 4,5,6 5,6 6,7 7 - Vorgän- Vorgän- Vorgän- Vorgän- Vorgän- Vorgän- Vorgänger 1 ger 2 ger 3 ger 4 ger 5 ger 6 ger 7 NULL NULL NULL NULL NULL NULL NULL NULL NULL 1 1 1 1 1 1 1 NULL 1 1 1 1 1 1 1 NULL 1 1 1 1 1 1 1 NULL NULL 2 2 2 2 2 2 NULL NULL 2 2 2 2 2 2 NULL NULL NULL NULL NULL 5 5 5 Am Ende des Verfahrens kennt jetzt jeder Knoten mit Ausnahme des Knotens 1 einen Vorgänger. Wollen wir beispielsweise den Weg ermitteln, der uns zu Knoten 7 geführt hat, so ist dies nun ganz einfach: Vorgänger von 7 ist laut Tabelle der Knoten 5. Vorgänger von 5 wiederum ist Knoten 2. Knoten 2 hat als Vorgänger Knoten 1, und Knoten 1 hat keinen Vorgänger, wir sind also am Startknoten angekommen. Der Rückweg ist also als (7,5,2,1) ermittelt worden. Der Weg, auf dem wir von Knoten 1 zu Knoten 7 kommen können, ist somit natürlich der Weg (1,2,5,7). Die Tabelle der Vorgänger gibt uns also genau den Breitensuchebaum an, allerdings mit jeweils gerade umgekehrt orientierten Kanten. Vorzeitiger Abbruch der Traversierungsalgorithmen. Wenn wir lediglich einen Weg von einem gegebenen Startknoten s zu einem einzigen Zielknoten z suchen, können wir das Breiten- oder Tiefensuchverfahren sofort abbrechen, sobald wir den Zielknoten zum ersten Mal aufgefunden haben, sobald wir den Zielknoten also in die Menge A eingetragen haben. Wir werden später bei der Kürzeste-Wege-Suche sehen, daß wir dort das Verfahren noch ein Stück fortführen müssen, um garantiert den kürzesten Weg zu erhalten. Steuerung der Breitensuche durch eine "Steuerungsfunktion". Am Anfang dieses Kapitels hatten wir insbesondere auf die Bedeutung von Wegalgorithmen für bewertete Seite Wege-17 Graphen hingewiesen. Um uns dieser Problematik zu nähern, wollen wir unseren Breitensuchalgorithmus jetzt formal dadurch modifizieren, daß wir nicht die Warteschlange als Datenstruktur für die Menge A verwenden, sondern die Priorität, mit der jeder einzelne Knoten aus der Menge A entfernt werden soll, durch eine Steuerungsfunktion f beschreiben, die jedem Knoten einen Zahlenwert zuordnen soll. Aus der Menge A soll dann in jeder Iteration der Breitensuche genau jener Knoten entfernt werden, für den die Steuerungsfunktion den kleinsten Wert liefert. Wir wollen den zugehörigen modifizierten Breitensuchealgorithmus vorstellen und diskutieren: Algorithmus "Breitensuche mit Steuerungsfunktion": gegeben: Graph G(V,E),Startknoten vs Für alle Knoten v aus V: v.statusÅUNBESUCHT v.vorgaengerÅNULL f(v)ÅINF Menge A A.Einfügen(vs) Integer zeit zeitÅ0 f(vs) Åzeit Solange A.Anzahl()>0 wiederhole: vÅA.EntferneMinimum() für alle Nachbarknoten w von v: falls w.status=UNBESUCHT: zeitÅzeit+1 w.statusÅAKTIV w.vorgaengerÅv f(w)Åzeit A.Einfügen(w) v.statusÅERLEDIGT INF steht hier für einen "unendlich großen" Funktionswert (∞). Die Steuerungsfunktion f(v) können wir uns als ein Feld vorstellen, in dem zu jedem Knoten v der zugehörige Funktionswert gespeichert ist. Die Warteschlange wird jetzt einfach nach dem Prinzip "Wer zuerst kommt, mahlt zuerst" realisiert: Für jeden Knoten, den wir neu finden, stellen wir unsere abstrakte Systemuhr zeit um eine Sekunde weiter. Jedem Knoten ist somit eine eindeutige Zeit zugeordnet, zu der er "in der Warteschlange eingetroffen" ist. Die Funktion A.EntferneMinimum() durchsucht die Menge A, vergleicht die Funktionswerte f(v) aller Knoten aus A und entfernt schließlich denjenigen Knoten, für den die Funktion den kleinsten Wert annimmt. Das ist der Knoten, dem die kleinste "Systemzeit" zugeordnet ist, also der "älteste" der noch in der Menge A verbliebenen Knoten. An die Stelle der Datenstruktur "Warteschlange", die automatisch gewährleistet, daß derjenige Knoten als nächster "drankommt", der schon "am längsten wartet", haben wir jetzt also einfach ein Durchsuchen einer "unstrukturierten" Menge A gesetzt. Am Ablauf des Algorithmus hat sich ansonsten nichts geändert. Bei der konkreten Implementierung muß natürlich darauf geachtet werden, daß die Menge A derart organisiert wird, daß das Element mit dem minimalen Wert der Steuerungsfunktion mit möglichst geringem Aufwand gefunden werden kann. Die soeben vorgestellte Form der Breitensuche-Traversierung ermöglicht eine einfache Änderung des Traversierungsalgorithmus. Zum Beispiel erhalten wir eine Tiefensuche, indem wir lediglich die Zeit "rückwärts" laufen lassen, also die Zeile Seite Wege-18 zeitÅzeit+1 durch die Zeile zeitÅzeit-1 ersetzen (das entspricht der Auswahl desjenigen Elementes aus A, das nicht die minimale, sondern die maximale Ankunftszeit hat). Abschließend merken wir noch an, daß die Laufzeit der Breiten- bzw. Tiefensuche natürlich optimal ist, wenn wir das Minimum der Menge A eben gerade nicht jedesmal durch ein Durchsuchen von A bestimmen müssen, sondern sozusagen "sofort" haben. Wenn A als Stapel oder Warteschlange verwaltet wird, läßt sich dies durch Verwendung entsprechender Datenstrukturen sicherstellen. Für die folgenden Algorithmen ist dies jedoch nicht mehr so einfach zu realisieren. Anwendung der Breitensuche mit "Steuerungsfunktion": CUTHILL-MCKEETraversierung eines Graphen. Eine einfache Änderung der Steuerungsfunktion f(v) ergibt statt der Breitensuche einen Traversierungsalgorithmus, der in der Methode der finiten Elemente eine große Rolle spielt und als sogenannte CUTHILL-MCKEE-Traversierung bekannt ist. Gegeben sei eine symmetrische, dünn besetzte Matrix. Unter einer dünn besetzten Matrix verstehen wir eine Matrix, die zahlreiche Einträge besitzt, die den Wert Null haben. Dünn besetzte Matrizen treten in der Methode der finiten Elemente auf. Wollen wir einen direkten Gleichungslöser (also eine Variante der GAUSS-Elimination, z.B. den CHOLESKYAlgorithmus) auf ein Gleichungssystem mit dünn besetzter Koeffizientenmatrix anwenden. so können wir dank der dünnen Besetzung Rechenoperationen einsparen, aber nur dann, wenn die Nichtnullelemente der Matrix in der Nähe der Hauptdiagonale der Matrix versammelt sind. Durch geeignete Zeilen- bzw. zugehörige Spaltenvertauschungen können wir dies erreichen. Gesucht ist nun eine Umnumerierung der Zeilen bzw. Spalten der Matrix derart, daß die Matrix nach Durchführung der Vertauschung möglichst nur in der Nähe der Hauptdiagonale besetzt ist. Eine solche Matrix können wir als Adjazenzmatrix eines ungerichteten Graphen interpretieren: Jeder Nichtnulleintrag der Matrix stellt eine Kante dar. Einedünn besetzte Matrix, deren Nichtnullelemente in der Nähe der Hauptdiagonalen versammelt sind, repräsentiert einen Graphen, in dem jeder Knoten nur mit einer begrenzten Anzahl "nahe benachbarter" anderer Knoten verbunden ist. Diese Beobachtung legt nahe, daß das Ziel einer möglichst günstigen Zeilen- bzw. Spaltenvertauschung sich durch eine Umnumerierung der Knoten gemäß einem Breitensucheverfahren erreichen läßt: Die Breitensuche hatten wir ja gerade als einen Algorithmus geschildert, der erst die "nahe Nachbarschaft", und erst später die weitere Umgebung des Startknotens erkundet. Im Prinzip müssen wir also eine Breitensuche durchführen. Die Reihenfolge, in der wir die Nachbarn des aktuellen Knotens in der Breitensuche aufsuchen, ist zunächst einmal relativ willkürlich und spielt keine dominante Rolle für das Endergebnis. Heuristisch erscheint es jedoch einleuchtend, daß Knoten mit einem großen Ausgangsgrad mit einer höheren Wahrscheinlichkeit "weiter" in den Graphen hineinführen als Knoten mit einem kleinen Ausgangsgrad. Daher werden die Nachbarn beim Algorithmus von CUTHILL und MCKEE nach ihrem Ausgangsgrad sortiert untersucht. Seite Wege-19 Wir führen wie bei der Breitensuche eine Zeitvariable ein, inkrementieren diese jedoch nur jedesmal, wenn wir einen Knoten aus der Menge A entfernen, um den Wert 1. Damit würden alle Nachbarn des aktuellen Knotens dieselbe Bewertung f(v) zugewiesen bekommen. Um innerhalb dieser "fast gleichrangigen" Nachbarn diejenigen mit kleineren Ausgangsgraden zu bevorzugen, addieren wir zum Zeitpunkt des Auffindens des Knotens noch einen Wert, der nichtnegativ und betragsmäßig immer kleiner Eins ist. Um einen solchen Wert zu finden, bedenken wir, daß die Anzahl der Kanten eines schlingenfreien Graphen immer höchstens E < V −1 sein kann. Somit ist die Zahl Ausgangsgrad (v) 0≤r = < 1. E Diese Idee ist im nachfolgenden Algorithmus realisiert: Algorithmus "CUTHILL-MCKEE-Traversierung": gegeben: Graph G(V,E),Startknoten vs Für alle Knoten v aus V: v.statusÅUNBESUCHT v.vorgaengerÅNULL f(v) ÅINF Menge A A.Einfügen(vs) Integer zeit zeitÅ0 f(vs) Åzeit Solange A.Anzahl()>0 wiederhole: vÅA.EntferneMinimum() zeitÅzeit+1 für alle Nachbarknoten w von v: falls w.status=UNBESUCHT: w.statusÅAKTIV w.vorgaengerÅv f(w)Åzeit+v.Ausgangsgrad()/|V| A.Einfügen(w) v.statusÅERLEDIGT Verwendet man diesen Algorithmus, um die Knoten in der Reihenfolge ihrer Entnahme aus der Menge A zu numerieren, so erhält man eine für den gewählten Startknoten vs "günstige" Numerierung. Um die "optimale" Numerierung zu erhalten, müßte der Algorithmus für alle Knoten des Graphen als Startknoten wiederholt werden. In der Praxis beschränkt man sich darauf, einige wenige Knoten mit geringem Ausgangsgrad als Stratpunkte für die CUTHILLMCKEE-Traversierung durchzuführen und dann die günstigste der so erzeugten Numerierungen des Graphen zur Zeilen- und Spaltenvertauschung der Matrix zu verwenden. Am Beispiel des folgenden ungerichteten Graphen wollen wir die CUTHILL-MCKEETraversierung vorführen. Wir starten wieder mit dem Knoten 1; dieser ist ein günstiger Ausgangspunkt für die Traversierung, da er wenige Nachbarn besitzt: Seite Wege-20 1 Schritt= neue Nr. 1 2 3 4 5 6 7 8 aktueller Knoten 1 7 2 5 8 3 6 4 Menge A 2,5,7 2,5,8 5,8,3,6 8,3,6 3,6,4 6,4 4 2 3 5 6 7 8 4 f(1) f(2) f(3) f(4) f(5) f(6) f(7) f(8) 0 1.5 ∞ ∞ 1.625 ∞ 1.375 - ∞ 2.5 - 3.375 3.6235 5.325 - - Wir bekommen also folgende neue Numerierung des Graphen: 1 3 6 4 7 2 5 8 Wenn wir die Adjazenzmatrizen beider Graphen vergleichen (also die Besetzungsstruktur der beiden Matrizen), so erhalten wir folgendes Bild: Seite Wege-21 vorher: nachher: Es ist deutlich sichtbar, daß mit der neuen Numerierung die Nichtnullelemente dichter um die Hauptdiagonale herum gruppiert sind. Kürzeste Wege nach DIJKSTRA. Es fehlt nicht mehr viel, um mit unserem Breitensuchalgorithmus kürzeste Wege bestimmen zu können. Anstatt die abstrakte Systemzeit als Steuerungsfunktion zu verwenden, werden wir bei unserer Traversierung eines kantenbewerteten Graphen als Steuerungsfunktion jedem Knoten die summierten Bewertungen der Kanten zuordnen, die wir auf dem Weg zum Knoten durchlaufen haben. In einer ersten, zunächst noch nicht dem gewünschten Zweck dienenden Version der Breitensuche könnte das aussehen wie folgt: Algorithmusskizze "Breitensuche in einem bewerteten Graphen": gegeben: Graph G(V,E),Startknoten vs Für alle Knoten v aus V: v.statusÅUNBESUCHT v.vorgaengerÅNULL f(v) ÅINF Menge A f(vs)Å0 A.Einfügen(vs) Solange A.Anzahl()>0 wiederhole: vÅA.EntferneMinimum() für alle Nachbarknoten w von v: falls w.status=UNBESUCHT: w.statusÅAKTIV w.vorgaengerÅv f(w)Åf(v)+Bewertung(v,w) A.Einfügen(w) v.statusÅERLEDIGT Die entscheidende Änderung gegenüber unserem vorherigen Algorithmus besteht darin, daß wir den Wert der Steuerungsfunktion für jeden neu entdeckten Knoten schrittweise aufsummieren: f(w)Åf(v)+Bewertung(v,w) Wir ordnen dem Knoten w als "Abstand" zum Startknoten jetzt den Abstand seines Vorgängers v vom Startknoten, zuzüglich der "Länge" der Kante v-w (Bewertung(v,w)), zu. Was wir hier als "Abstand" oder "Länge" bezeichnen, kann in unserer realen Anwendung auch die Bedeutung von "Zeitdauer", "Kosten" oder "Aufwand" haben. Wir gehen im folgenden davon aus, daß alle Kanten des untersuchten Graphen eine Bewertung haben, die positiv oder Null ist, aber nicht negativ werden kann. Seite Wege-22 In unseren typischen Anwendungen (z.B. Routensuche: Route minimaler Länge, minimaler Fahrtdauer, minimalen Energieverbrauchs, minimaler Mautkosten) ist diese Voraussetzung automatisch erfüllt. Wenn wir mit dem oben skizzierten Algorithmus an einem Zielknoten ankommen, dann kennen wir dessen "Abstand" vom Startknoten, also die Länge des Weges. Es ist jedoch noch nicht garantiert, daß der per Rückweg aufgezeichnete Weg, den wir gefunden haben, auch der kürzeste ist. Um dies zu erzielen, müssen wir eine Änderung der Bewertung von Knoten der Menge A während der Durchführung des Verfahrens zulassen. Es kann ja vorkommen, daß wir zunächst einen Weg vom Startknoten vs zu einem bestimmten Knoten u finden, später aber noch einen kürzeren Weg entdecken. Dann sollten wir f(u) gemäß der neuentdeckten kürzeren Wegstrecke aktualisieren und zudem den geänderten Rückweg von u aus speichern. Wenn wir diese Ideen realisieren, kommen wir zum folgenden Algorithmus: Algorithmus "Kürzeste Wege nach DIJKSTRA": gegeben: Graph G(V,E),Startknoten vs Für alle Knoten v aus V: v.statusÅUNBESUCHT v.vorgaengerÅNULL f(v) ÅINF Menge A f(vs)Å0 A.Einfügen(vs) Solange A.Anzahl()>0 wiederhole: vÅA.EntferneMinimum() für alle Nachbarknoten w von v: falls w.status=UNBESUCHT: w.statusÅAKTIV w.vorgaengerÅv f(w)Åf(v)+Bewertung(v,w) A.Einfügen(w) sonst falls w.status=AKTIV: falls f(v)+Bewertung(v,w) < f(w) w.vorgaengerÅv f(w)Åf(v)+Bewertung(v,w) v.statusÅERLEDIGT Die entscheidende Änderung des Algorithmus liegt in der Möglichkeit der "Umwertung des Knotens w" und der entsprechenden Änderung des Vorgängers von w: falls w.status=UNBESUCHT: ... sonst falls w.status=AKTIV falls f(v)+Bewertung(v,w) < f(w) w.vorgaengerÅv f(w)Åf(v)+Bewertung(v,w) Der oben dargestellte Algorithmus stammt von E. W. DIJKSTRA (A note on two problems in connection with graphs, Numerische Mathematik 1, 1959) und ist der wichtigste Algorithmus zur Bestimmung kürzester Wege in einem sogenannten Distanzgraphen (kantenbewerteter Graph mit nichtnegativen Bewertungen). Seite Wege-23 Der DIJKSTRA-Algorithmus ist ein typisches Beispiel für einen sogenannten GreedyAlgorithmus (englisch greed=Gier): So werden Algorithmen bezeichnet, die einem lokalen Optimalitätskriterium folgen ("take what you get"). Wir folgen lokal immer zuerst derjenigen Kante, die im Hinblick auf unser Ziel, kürzeste Wege zu finden, besonders verlockend erscheint, nämlich der kürzesten Kante, die wir noch nicht erkundet haben. Damit wird das Ziel verfolgt, die Anzahl erforderlicher "Umwertungen" von Knoten möglichst gering zu halten. Das lokale "Greedy"-Kriterium garantiert natürlich nicht die globale Optimalität, daher werden dennoch Umwertungen notwendig; heuristisch erwartet man aber, daß in vielen Fällen eine Aneinanderreihung "lokaler" Optima gewissermaßen "zufällig" auch zu einem globalen Optimum führt. Wir wollen den DIJKSTRA-Algorithmus an einem Beispiel illustrieren. Diesmal wählen wir den nachstehend angegebenen ungerichteten Graphen als Beispiel. 2 2 1 4 5 3 1 4 3 7 1 2 3 4 4 6 Die Bewertungen der Kanten sind neben den einzelnen Kanten angeschrieben. Mit diesem Graphen stellt sich die Suche nach kürzesten Wegen vom Knoten 1 in tabellarischer Form wie folgt dar: aktueller Knoten 1 2 4 3 5 6 7 Menge A 2,3,4 3,4,5 3,5,6 5,6 6,7 7 Kn. 1 Kn. 2 f, Vorg. f, Vorg. 2/1 0/NULL ⎯ ⎯ Kn. 3 f, Vorg. 4/1 4/1 4/1 ⎯ Kn. 4 Kn. 5 Kn. 6 Kn. 7 f, Vorg. f, Vorg. f, Vorg. f, Vorg. 3/1 ∞/NULL ∞/NULL ∞/NULL 3/1 6/2 ∞/NULL ∞/NULL 6/2 7/4 ∞/NULL ⎯ 5/3 6/3 ∞/NULL 6/3 8/5 ⎯ 7/6 ⎯ ⎯ In den Spalten, die den Knoten 1 bis 7 in der Tabelle zugeordnet sind, haben wir jeweils den Wert der Steuerfunktion f für jeden Knoten angegeben, außerdem den aktuell gültigen Vorgänger des jeweiligen Knotens (eingerahmte Knotennummer). Im ersten Schritt werden von Knoten 1 aus die drei Nachbarknoten 2, 3 und 4 gefunden. Damit ist Knoten 1 erledigt. Der DIJKSTRA-Algorithmus weist den Nachbarn des aktuell betrachteten Knotens eine eindeutige Reihenfolge zu, die sich aus deren Entfernung vom Startknoten ergibt. Da Knoten 2 am wenigsten weit vom Start entfernt ist, wird als nächstes Knoten 2 als aktueller Knoten gewählt. Dabei wird Knoten 5 neu entdeckt, und die Wegestrecke 1-2-5 hat die "Länge" 6. Seite Wege-24 Somit ist im nächsten Schritt Knoten 4 derjenige, der die geringste Entfernung vom Startpunkt besitzt, und wird somit als nächster aktueller Knoten ausgewählt. Dabei wird ein Weg zu Knoten 6 gefunden (1-4-6), der die Gesamtlänge 7 hat. Im nächsten Schritt (Knoten 3 aktueller Knoten) stellt es sich heraus, daß weder der bisher bekannte Weg zu Knoten 5 noch der zu Knoten 6 der kürzeste gewesen ist, denn beide Knoten sind über Knoten 3 schneller erreichbar: Der Weg (1-3-5) hat nur die Länge 5,und der Weg (1-3-6) hat die Länge 6. Deshalb müssen wir jetzt beide Knoten, 5 und 6, "umbewerten": Die bisher notierte Entfernung vom Startknoten wird für beide Knoten aktualisiert, und die neuen Vorgänger beider Knoten werden notiert (jeweils Knoten 3). In unserem Beispiel tritt die "Umwertung" bzw. Aktualisierung der Entfernung vom Startknoten noch ein weiteres Mal auf, nämlich dann, wenn Knoten 6 zum aktuellen Knoten wird: Dann wird der längere Weg 1-3-5-7 durch den kürzeren Weg 1-3-6-7 ersetzt (Knoten 7 hat den Abstand 7 vom Startknoten, nicht 6, wie zuvor ermittelt worden war). Diese "Umwertung" von Knoten, die aktuell in der Menge A sind, ist das wichtigste Merkmal des DIJKSTRA-Algorithmus. Ansonsten verläuft der Algorithmus wie eine Breitensuche. Wenn alle Kanten die Bewertung 1 haben, ist der DIJKSTRA-Algorithmus identisch mit einer Breitensuche. Die Breitensuche findet den "kürzesten Weg" vom Startknoten zu allen anderen erreichbaren Knoten, wenn die "Länge" des Weges in der Anzahl der durchlaufenen Kanten gemessen wird. In diesem Spezialfall ist keine "Umbewertung" von Kanten erforderlich. Für die Beantwortung der Frage "welche Zugverbindung von a nach b erfordert die wenigsten Umstiege" wäre also die Breitensuche der richtige Algorithmus Vielleicht ist es garnicht so einfach, einzusehen, daß der DIJKSTRA-Algorithmus das Auffinden der kürzesten Wege wirklich garantiert. Anschaulich kann man sich das Verfahren aber vorstellen, indem man den Graphen als Rohrleitungsnetz interpretiert, in das beim Startknoten eine Flüssigkeit eingeleitet wird, die sich mit konstanter Geschwindigkeit ausbreitet. Jedesmal, wenn ein Knoten in die Menge A eingetragen wird, beginnt die Flüssigkeit, in Richtung auf diesen Knoten durch das jeweilige Rohr zu fließen. Jedesmal, wenn ein Knoten die Menge A verläßt, hat die Flüssigkeit den jeweiligen Knoten erreicht. Zur Zeit, zu der die Flüssigkeit den Knoten über ein bestimmtes Rohr erreicht, kann durchaus in einem schon früher "gefluteten" anderen Rohr das Wasser noch unterwegs sein. Unsere "Steuerungsfunktion" f ist zu jedem Zeitpunkt des Verfahrens eine "Schätzung" der tatsächlichen Entfernung des Knotens vom Startknoten; erst dann, wenn die Flüssigkeit wirklich am Knoten ankommt, kennen wir die wahre Entfernung. Die Funktion f(v) gibt zu jedem Zeitpunkt des Verfahrens eine obere Schranke für die Entfernung des Knotens v vom Startknoten an. Im folgenden wollen wir die Korrektheit des Verfahrens jeoch auch mathematisch exakt beweisen, da uns der Beweis weitere wichtige Erkenntnisse verschafft. Beweis der Korrektheit des DIJKSTRA-Algorithmus. Die Korrektheit des DIJKSTRAAlgorithmus kann man mit vollständiger Induktion beweisen. Wir gehen dabei von einem Startknoten s und einem Zielknoten z aus. Wir wollen beweisen, daß wir in dem Moment, in dem wir einen Knoten aus der Menge A der "aktiven" Knoten entfernen, den kürzesten Weg zu diesem Knoten gefunden haben, so daß wir ihn aus der weiteren Betrachtung herausnehmen können. Die Länge des kürzesten Weges von s zu einem beliebigen Knoten v wollen wir im folgenden mit δ ( s, v) bezeichnen. Temporär haben wir außerdem jeden Seite Wege-25 Knoten, der in der Menge A enthalten ist, mit einer gerade gültigen oberen Schranke d(s,v) markiert (es ist f(v)=d(s,v)). Für die noch in der Menge U der bislang unentdeckten Knoten enthaltenen Knoten ist diese "Schranke" "unendlich". Induktionsvoraussetzungen. Wir nehmen dazu zunächst einmal an, zu einem bestimmten Zeitpunkt hätten wir bereits k>0 Einträge in der Menge F. Wir wollen annehmen, daß die in diesen Knoten gespeicherten "Rückwege" tatsächlich die kürzesten Wege zu diesen Knoten sind und daß diese kürzesten Wege ausschließlich innerhalb der Menge F verlaufen. Gleichzeitig nehmen wir an, daß die Menge A zu diesem Zeitpunkt ausschließlich solche Knoten enthält, die einen direkten Vorgänger in der Menge F besitzen. (Die Elemente der Menge U können wir in unserer Betrachtung vernachlässigen, denn jedes Element, das in die Menge F gelangt, muß ja zuvor in die Menge A eingetragen worden sein). Wir nehmen außerdem an, daß d(s,v) für jeden Knoten der Menge A die Länge des kürzesten speziellen Wegs enthält, der ausschließlich durch Knoten der Menge F verläuft und erst im letzten Schritt aus der Menge F zum Knoten v in der Menge A springt. Beweis. Nun müssen wir zweierlei beweisen: 1. Induktionsbasis: Für k=1 ist die Induktionsvoraussetzung erfüllt. Für k=1 haben wir lediglich den Startknoten s in der Menge F; damit ist trivialerweise die Voraussetzung erfüllt, daß wir für s den kürzesten Weg von s her kennen (den Weg der Länge Null). Außerdem haben wir in dem Augenblick, in dem wir s in F eingetragen haben, die direkten Nachbarn von s in die Menge A aufgenommen, so daß auch die zweite Hälfte der Annahme erfüllt ist. 2. Induktion von k nach k+1. Nun müssen wir noch beweisen, daß unser Verfahren beim Eintragen des (k+1)-ten Knotens (man nenne ihn v) in die Menge F gewährleistet, daß die im Schritt k erfüllten Voraussetzungen auch weiterhin erfüllt bleiben. Wir müssen also beweisen, (a) daß im Schritt k+1 die Markierung d(s,v)= δ ( s, v) ist, und (b) daß für einen beliebigen Knoten u, der auch nach Schritt k+1 noch in A enthalten ist, der gespeicherte Rückweg der kürzeste spezielle Weg ist. Die beiden Teile des Induktionsschritts können wir leicht beweisen: Menge F Menge A Menge U v s x y Es kann keinen Weg von s über y und x nach v geben, der kürzer ist als d(s,v), wenn d ( s, v) = min d ( s, w) w∈ A a) Im Schritt k+1 ist d ( s, v) = min d ( s, w) . Wir zeigen (siehe obenstehende Abbildung), w∈ A daß die Annahme d ( s, v) ≠ δ ( s, v) auf einen Widerspruch führt: Wir nehmen an, es Seite Wege-26 gebe einen Weg, der beim Knoten s in der Menge F beginnt und bis zu einem bestimmten Knoten y innerhalb der Menge F bleibt (der Weg muß innerhalb von F beginnen, weil der Startknoten s in F liegt). Am Knoten y springe der betrachtete Weg zu einem beliebigen Knoten x der Menge A und verlaufe irgendwie weiter bis zum Knoten v. Wir nehmen an, die Länge dieses Weges sei kürzer als d(s,v), so daß d ( s, v) ≠ δ ( s, v) . Daß diese Annahme nicht stimmt, kann man leicht nachrechnen: Die Länge des vermeintlich kürzeren Weges von s über y und x nach v ist L( s, x, v) = δ ( s, y ) + B( y, x) + L( x, v) = d ( s, x) + L( x, v) . Hier soll B(y,x) die Bewertung der Kante (x,y) und L(a,b) die Länge irgendeines Weges durch die Knoten a und b bezeichnen. Das Gleichheitszeichen gilt wegen des zweiten Teils der Induktionsvoraussetzung. Da aber wegen der positiven Kantenbewertung L(x,v)≥0, gilt: L( s, x, v) ≥ d ( s, x) . Da aber unsere Auswahl des Knotens v sicherstellt, daß d(s,x) ≥d(s,v), kann der Weg über x nicht kürzer sein als d(s,v), und somit muß d ( s, v) = δ ( s, v) gelten. Jetzt ist noch zu zeigen, daß nach der Aufnahme des Knotens v in die Menge F die b) restlichen Knoten u in der Menge wiederum mit der Länge des jeweils kürzesten speziellen Weges von s nach u markiert sind (Markierungen d(s,u)). Man macht sich leicht klar, daß das Umbewerten der Knoten in A beim Entfernen des Knotens v aus A genau die Erhaltung dieser Voraussetzung sicherstellt: Entweder gibt es zu einem Knoten u aus A einen kürzesten speziellen Weg, der über den neu in die Menge F aufgenommenen Knoten v führt; dann haben wir diesen neuen Weg gerade eben beim Aufnehmen von v in F entdeckt und d(s,u) entsprechend aktualisiert. Oder aber der bisher kürzeste spezielle Weg von s nach u führt nicht über v, dann hat sich an diesem Weg, seiner Länge und der Markierung d(s,u) nichts geändert, und wir sind ebenfalls fertig. Wir haben jetzt also bewiesen, daß das Verfahren von DIJKSTRA garantiert, daß wir die kürzesten Wege δ ( s, v) vom Startknoten s zu allen anderen von s aus erreichbaren Knoten v findet. Im Teil (a) des Induktionsbeweises ging dabei die Voraussetzung nichtnegativer Kantenbewertungen ein. Für Graphen, die auch negative Kantenbewertungen enthalten können, funktioniert der DIJKSTRA-Algorithmus nicht. Der Beweis gibt uns aber noch eine weitere Erkenntnis: Der kürzeste Weg δ ( s, v) ist genau in dem Augenblick bekannt, in dem wir den Knoten v aus der Menge A entfernen, also als "aktuellen Knoten" bearbeitet haben. Vorzeitiger Abbruch der Traversierungsalgorithmen. Im Gegensatz zur Suche nach irgendeinem Weg vom Startknoten s zum Zielknoten z können wir die DIJKSTRA-Suche nach dem kürzesten Weg nicht schon dann abbrechen, wenn der Knoten z zum ersten Mal erreicht, also in die Menge A aufgenommen wird. Es kann zwar sein, daß wir zu diesem Zeitpunkt bereits den kürzesten Weg von s nach z kennen, dies ist aber nicht garantiert, weil sich die Bewertung d(s,z) ja noch ändern kann (sogar noch mehrfach!), bevor z schließlich endgültig aus A entfernt wird. Der Beweis des DIJKSTRA-Verfahrens hat uns aber gezeigt, daß spätestens mit der Entnahme von z aus A dann wirklich garantiert der kürzeste Weg bekannt ist. Suchen wir nur diesen einen Weg, so sind wir fertig und können die Suche abbrechen. Aufwand des DIJKSTRA-Verfahrens. Im Gegensatz zur Breiten- und Tiefensuche gibt es keine elementare Datenstruktur, die beim DIJKSTRA-Verfahren die Bestimmung des "minimalen" Elementes v aus A trivial ermöglicht; ein wichtiger Grund dafür ist, daß die Knoten in der Menge A während ihrer Aufenthaltszeit in dieser Menge unter Umständen umbewertet werden müssen. Es hilft also nichts, die Menge A zum Beispiel nach den Funktionswerten f(v)=d(s,v) zu sortieren, weil man sonst in jedem Schritt die Menge A wieder neu sortieren müßte. Zunächst scheint also nur die Möglichkeit zu bleiben, die Menge A Seite Wege-27 jedesmal linear zu durchsuchen, um das Minimum zu bestimmen. Es gibt jedoch Datenstrukturen, die auch das Umbewerten und Ermitteln des Minimums in besserer Laufzeit ermöglichen, sogenannte partiell sortierte Datenstrukturen (heaps). Diese Organisationsformen für Daten sind jedoch jenseits des Stoffgebietes, den die vorliegende Vorlesung abdecken kann. Eine andere Anmerkung ist auch noch wichtig: Wenn wir nur den kürzesten Weg für ein einziges Knotenpaar (s,z) suchen, müssen wir im schlimmsten Fall trotzdem alle erreichbaren Knoten des Graphen absuchen, denn es könnte ja per Zufall sein, daß z gerade als letzter Knoten aus A entfernt wird. Bis heute sind keine Algorithmen bekannt, die für den kürzesten Weg zwischen einem einzigen Knotenpaar (s,z) eine günstigere Abschätzung für den maximalen Aufwand aufweisen. Bei großen Graphen kann der Aufwand des DIJKSTRAVerfahrens wie auch der anderen Algorithmen prohibitiv werden. Eine erste Anwendung des DIJKSTRA-Verfahrens. Es liegt nahe, das Problem der Routensuche mit dem Dijkstra-Verfahren lösen zu wollen. Unter Routensuche versteht man die Bestimmung einer Fahrtroute zwischen zwei Positionen s und z anhand einer digitalen Straßenkarte oder z.B. anhand eines digitalen Netzplans des ÖPNV. Die digitale Karte ist in der Regel ein kantenbewerteter Graph mit nichtnegativen Bewertungen. Als Bewertungen kommen zum Beispiel in Frage Länge der Straßenverbindung zwischen zwei benachbarten Knoten¸ erforderliche Fahrzeit (berechenbar als Länge/Fahrgeschwindigkeit), Mautgebühren, Benzinverbrauch. Wir suchen nun eine möglichst "billige" (kurze, schnelle, kostengünstige, energiesparende) Verbindung. Wenn wir keine weiteren Zusatzinformationenzur Verfügung haben, sondern nur die Adjazenzbeziehungen des Graphen und die Bewertungen verwenden können, so ist der DIJKSTRA-Algorithmus zur Lösung dieser Aufgabe heranzuziehen. Wenn man beispielsweise eine Fahrt mit dem Zug oder im öffentlichen Personennahverkehr (ÖPNV) plant, muß man den DIJKSTRA-Algorithmus noch etwas modifizieren und auch Knotenbewertungen einführen, um z.B. Umsteige-Wartezeiten berücksichtigen zu können. Das folgende Bild zeigt eine schematische Autobahnkarte der Bundesrepublik Deutschland (nicht ganz wirklichkeitsgetreu) mit den wichtigsten Anschluß- und Knotenpunkten. Seite Wege-28 Flensburg Kiel Rostock Lübeck Hamburg Wilhelmshaven Lüneburg Emden Oldenburg Bremen Berlin Frankfurt/Oder Hannover Braunschweig Wolfenbüttel Magdeburg Hildesheim Osnabrück Bielefeld Cottbus Clausthal-Zellerfeld Paderborn Jülich Köln Aachen Bonn Halle Göttingen Dortmund Bochum Essen Duisburg Hagen Wuppertal Düsseldorf Leipzig Kassel Dresden Weimar Erfurt Jena Gera Chemnitz Zwickau Ilmenau Hof Siegen Giessen Koblenz Trier Schweinfurt Frankfurt/Main Bamberg Bayreuth Wiesbaden Mainz Darmstadt Würzburg Worms Ludwigshafen Kaiserslautern Heidelberg Erlangen Crailsheim Nürnberg Saarbrücken Landau/Pfalz Heilbronn Regensburg Karlsruhe Ingols tadt Ludwigsburg Pforzheim Stuttgart Tübingen Reutlingen Ulm Augsburg München Landsberg/Lech Memmingen Freiburg Passau Landshut Salzburg Garmisch-Partenkirchen Konstanz Lindau Die Kanten sind hier der Einfachheit halber als gerade Linien dargestellt, wie üblich in der Graphentheorie (ungerichteter Graph). Jedoch ist die wahre Länge jeder Verbindung als Bewertung der zugehörigen Kante zugeordnet. Zum Beispiel hat die Kante München-Salzburg in unserem Beispiel die Länge 156.3 km. Wir wollen nun die kürzeste Verbindung von München nach Hamburg suchen. Das nachfolgende Bild stellt alle kürzesten Wege dar, die wir bis zum Schritt k=19 des Verfahrens gefunden haben: Seite Wege-29 Flensburg Kiel Rostock Lübeck Hamburg Wilhelmshaven Lüneburg Emden Oldenburg Bremen Berlin Frankfurt/Oder Hannover Braunschweig Wolfenbüttel Magdeburg Hildesheim Osnabrück Cottbus Bielefeld Clausthal-Zellerfeld Paderborn Jülich Aachen Leipzig Kassel Dresden Weimar Erfurt Jena Gera Chemnitz Zwickau Ilmenau Hof Siegen Köln Bonn Halle Göttingen Dortmund Bochum Essen Duisburg Hagen Wuppertal Düsseldorf Giessen Koblenz Frankfurt/Main Wiesbaden Mainz Darmstadt Schweinfurt BambergBayreuth Würzburg Trier Worms Ludwigshafen Kaiserslautern Heidelberg Saarbrücken Erlangen Crailsheim Landau/Pfalz Nürnberg Heilbronn Regensburg Karlsruhe Ludwigsburg Pforzheim Stuttgart Tübingen Reutlingen Ingolstadt Passau Landshut Ulm Augsburg München MemmingenLandsberg/Lech Freiburg Salzburg Garmisch-Partenkirchen Konstanz Lindau Wir erkennen, daß sich die Suche ähnlich wie bei der Breitensuche "wellenartig" vom Startknoten "München" ausbreitet, in alle Richtungen gleichmäßig. Im nächsten Bild haben wir die Situation dargestellt, zu der der Zielknoten "Hamburg" aus der Menge A entfernt wird. Außerdem ist der dann gefundene kürzeste Weg hervorgehoben. Seite Wege-30 Flensburg Kiel Rostock Lübeck Hamburg Wilhelmshaven Lüneburg Emden Oldenburg Bremen Berlin Frankfurt/Oder Hannover Braunschweig Wolfenbüttel Magdeburg Hildesheim Osnabrück Cottbus Bielefeld Clausthal-Zellerfeld Paderborn Jülich Aachen Leipzig Kassel Dresden Weimar Erfurt Jena Gera Chemnitz Zwickau Ilmenau Hof Siegen Köln Bonn Halle Göttingen Dortmund Bochum Essen Duisburg Hagen Wuppertal Düsseldorf Giessen Koblenz Frankfurt/Main Wiesbaden Mainz Darmstadt Schweinfurt Bamberg Bayreuth Würzburg Trier Worms Ludwigshafen Kaiserslautern Heidelberg Saarbrücken Erlangen Crailsheim Nürnberg Landau/Pfalz Heilbronn Karlsruhe Ludwigsburg Pforzheim Stuttgart Tübingen Reutlingen Regensburg Ingolstadt Passau Landshut Ulm Augsburg München MemmingenLandsberg/Lech Freiburg Salzburg Garmisch-Partenkirchen Konstanz Lindau An diesem Beispiel wird deutlich, daß wir fast den gesamten Graphen absuchen müssen, um den kürzesten Weg nach Hamburg zu finden. Algorithmen, die sozusagen "zielgerichtet" suchen, gibt es innerhalb der Graphentheorie nicht, denn in der Graphentheorie haben die Knoten keine geometrische Bedeutung ("Ortskoordinaten"). Das ist vielmehr eine Eigenschaft unserer speziellen Problemstellung, und in der Tat kann man Algorithmen entwickeln, die mehr "zielorientiert suchen", wenn man die geometrische Zusatzinformation mit ausnützt. Ein solches Verfahren, das aus der Graphentheorie in die geometrischen Algorithmen überleitet, wollen wir am Ende dieses Skriptums diskutieren. Der DIJKSTRA-Algorithmus hat aber dennoch eine wesentliche Bedeutung, denn es gibt auch zahlreiche Problemstellungen, in denen wir ohne "äußere Zusatzinformationen" auskommen müssen. Ein solches Beispiel diskutieren wir im nächsten Abschnitt. Seite Wege-31 Modifikation des DIJKSTRA-Algorithmus für längste Wege in der Bauprojektplanung. Wenn man im DIJKSTRA-Algorithmus alle (nichtnegativen) Kantenbewertungen mit negativem Vorzeichen versieht (oder alternativ immer das Element mit der maximalen Markierung aus A auswählt und immer die "längsten Rückwege" an allen Knoten speichert), erhält man einen Algorithmus zur Bestimmung längster Wege in nichtnegativ kantenbewerteten Graphen. Längste Wege existieren allerdings nur in ganz bestimmten Typen von Graphen, nämlich in sogenannten gerichteten kreisfreien Graphen (DAG=directed acyclic graph). In einem ungerichteten Graphen können wir, wenn es sich nicht um einen Baum oder Wald handelt, immer im Kreis gehen und somit einen Weg von a nach b jederzeit verlängern, so daß die Vorstellung des "längsten Weges" nicht klar definiert erscheint. In einem kreisfreien gerichteten Graphen gibt es solche Probleme nicht. Der modifizierte Algorithmus lautet: Algorithmus "Längste Wege": gegeben: Graph G(V,E),Startknoten vs Für alle Knoten v aus V: v.statusÅUNBESUCHT v.vorgaengerÅNULL f(v) ÅINF Menge A f(vs)=0 A.Einfügen(vs) Solange A.Anzahl()>0 wiederhole: vÅA.EntferneMinimum() für alle Nachbarknoten w von v: falls w.status=UNBESUCHT: w.statusÅAKTIV w.vorgaengerÅv f(w)Åf(v)-Bewertung(v,w) A.Einfügen(w) sonst: falls f(v)-Bewertung(v,w) < f(w) w.vorgaengerÅv f(w)Åf(v)-Bewertung(v,w) falls w.status≠AKTIV w.statusÅAKTIV A.Einfügen(w) v.statusÅERLEDIGT Man beachte, daß wir nun nicht schon vorzeitig aus der Suchschleife aussteigen können, bevor die Menge A leer ist. Vielmehr müssen wir unter Umständen Knoten, die wir bereits aus der Menge A entfernt hatten und in die Menge F aufgenommen hatten, nachträglich noch ein weiteres Mal die Menge A eintragen. Erst wenn die Menge A leer ist, können wir sicher sein, den längsten Weg gefunden zu haben. Wir wollen den dargestellten Algorithmus wieder an einem Beispiel verdeutlichen. Gegeben ist der nachfolgende gerichtete, zyklenfreie Graph. Seite Wege-32 2 10 4 3 9 8 1 6 1 11 3 3 5 6 Mit diesem Graphen finden wir den längsten Weg mit dem dargestellten Algorithmus in 9 Schritten, wie die nachfolgende Tabelle zeigt: aktueller Knoten v d(s,v) 1 2 4 6 3 5 6 4 6 0 -10 -16 -19 -8 -19 -21 -20 -23 Vorgänger(v) Menge A (in Klammer ist für jeden Knoten u die Markierung d(s,u) angegeben) 0 2(-10) 3(-8) 1 3(-8) 4(-16) 2 3(-8) 6(-19) 4 3(-8) 1 4(-17) 5(-19) 3 4(-20) 6(-21) 5 4(-20) 5 6(-23) 4 Die letzte Spalte unserer Tabelle gibt den Inhalt der Menge A jeweils nach Bearbeitung des "aktuellen Knotens" an. Man sieht, daß in unserem Beispiel der Knoten 4 zweimal und der Knoten 6 sogar dreimal neu in die Menge A aufgenommen werden muß; beide Knoten werden zu einem Zeitpunkt "umbewertet", zu dem sie bereits in die Menge F eingetragen worden sind. Der längste Weg ist (1-3-5-4-6); im nachfolgenden Bild ist dieser Weg hervorgehoben. 2 10 1 6 4 3 9 8 3 1 11 5 3 6 Wenn man sich den Ablauf des Längste-Wege-Suchverfahrens näher ansieht, so stellt man fest, daß das Verfahren eine gewisse Verwandtschaft zur Tiefensuche aufweist, wohingegen die Suche nach kürzesten Wegen ja eher der Breitensuche ähnelt. Beim normalen Tiefensuchalgorithmus müssen wir allerdings jeden Knoten nur einmal zum aktuellen Knoten machen; um garantieren zu können, daß wir den längsten Weg finden, ist das mehrfache Aufsuchen derselben Knoten unumgänglich. Das Verfahren für den längsten Weg, das wir hier vorgestellt haben, hat starke Ähnlichkeit mit der sogenannten iterativen Tiefensuche, auf die wir aber im Rahmen der vorliegenden Vorlesung nicht eingehen können. Man beachte, daß die fehlende Möglichkeit des Ausstiegs aus der Suchschleife uns dazu zwingt, immer den gesamten Graphen abzusuchen, ehe wir den längsten Weg kennen. Damit kann die LängsteWege-Suche in großen Graphen zum ernsthaften Problem werden. Seite Wege-33 Man kann den kritischen Weg auch bestimmen, indem man zunächst eine Tiefensuche zur topologischen Sortierung des Graphen durchführt. Dann braucht man jeden Knoten nur noch einmal "anzufassen", um die Länge des längsten Weges vom Startknoten s zum fraglichen Knoten zu ermitteln. Man betrachtet die Knoten dazu in der Reihenfolge der topologischen Sortierung und weist jedem Knoten als Markierung das Maximum der Markierungen seiner Vorgänger, zuzüglich der Bewertungen der Kanten vom jeweiligen Vorgänger zum betrachteten Knoten, zu. Will man Adjazenzlisten als Datenstruktur zur Speicherung des Graphen verwenden, so muß man zusätzlich zu den adjazenten Kanten jedes Knotens auch seine inzidenten Kanten speichern. Man hat also einen höheren Speicheraufwand für den Graphen als beim modifizierten DIJKSTRA-Verfahren, spart dafür aber am Such- und Speicheraufwand für die Menge A ein wenig. Man mag sich fragen, wieso jemand überhaupt an längsten Wegen interessiert sein sollte; jedoch haben solche Wege (auch als kritische Wege bezeichnet) herausragende Bedeutung in der Projektplanung (sogenanntes Operations Research, ein Teilgebiet der Wirtschaftswissenschaft), zum Beispiel in der Bauzeitenplanung. Wir wollen dazu ein Beispiel betrachten. Gebaut werden soll eine dreifeldrige Brücke über einen Taleinschnitt, siehe Bild. Ü1 W1 Ü2 P1 Ü3 P2 W2 F1 F2 Die Brücke besteht aus den Bauteilen Widerlager 1 (W1), Widerlager 2 (W2), Fundament 1 (F1), Fundament 2 (F2), Pfeiler 1 (P1), Pfeiler 2 (P2), sowie Überbau 1 (Ü1), 2 (Ü2) und 3 (Ü3). Natürlich können wir nicht einfach mit Überbau 3 zu bauen beginnen, sondern wir müssen zunächst die Baustelle einrichten; dann können wir – eventuell gleichzeitig – an den Widerlagern und den Fundamenten zu bauen beginnen. Sobald die Fundamente fertig sind, können wir – eventuell nach einer gewissen Wartezeit, die zum Erhärten des Betons notwendig ist – die Pfeiler auf das Fundament setzen. Die Überbauten können wir erst dann bauen, wenn die zugehörigen Pfeiler und Widerlager fertig und belastbar sind. Wenn alle Bauteile vollendet sind, ist unsere Brücke fertig. Wir fragen nun nach der minimal erforderlichen Bauzeit. Dazu formulieren wir die Problembeschreibung zunächst, wie in der Projektplanung üblich, als Abfolge von "Vorgängen", die durch "Abfolgen" miteinander verbunden sind. Dadurch erhalten wir die folgende Darstellung (mit Angabe der jeweils erforderlichen Vorgangsdauer bzw. Wartezeit): Seite Wege-34 W1(20) 8 0 0 Baustelle (5) 8 F1(14) P1(9) 8 0 8 0 F2(10) Ü1(7) 8 8 P2(12) 0 0 Ü2(8) fertig 0 8 Ü3(7) 8 W2(20) Beschreibung des Bauprojektes "Brücke" Man beachte, daß kein Vorgang begonnen werden kann, bevor alle anderen Vorgänge beendet sind, von denen aus ein Pfeil auf den betrachteten Vorgang verweist. – Noch hat diese formalisierte Darstellung nicht die vertraute Gestalt eines Graphen. Bedenken wir jedoch, daß es für die Bestimmung der Bauzeit völlig unerheblich ist, ob wir Zeit verbrauchen, indem wir bauen, oder, indem wir warten, so können wir die Pfeile und die Kästchen der vorangehenden Abbildung beide als Kanten eines Graphen betrachten. Dazu führen wir am Beginn und Ende eines jeden Vorganges einen Knoten ein, der Beginn- und Endpunkt des Vorganges repräsentieren soll. Zum Beispiel ersetzen wir Vorgang "Bau des Pfeilers P1" durch eine Kante zwischen Knoten "P1b" (wie "Pfeiler 1 begonnen") nach "P1e" (wie "Pfeiler 1 erstellt"). Die Kante bewerten wir mit der erforderlichen Bauzeit, hier also 9. Die Pfeile der Darstellung können wir genauso in gerichtete Kanten eines Graphen verwandeln, wobei wir als Kantenbewertung hier die erforderliche Wartezeit anschreiben. Jeder Knoten gibt also einen bestimmten Teilbauzustand der Struktur an. So kommen wir zur folgenden Darstellung des Projektes in Form eines nichtnegativ kantenbewerteten, gerichteten, kreisfreien Graphen. W1b 20 W1e 14 F1e 8 0 F1b Bb 5 0 Be 8 P1b 9 P1e Ü1b 8 8 Ü1e 0 8 Ü2b 0 F2b 10 F2e 8 P2b 12 P2e 8 Ü2e 8 0 8 Ü3b 0 W2b 20 W2e 0 8 Ü3e 8 Beschreibung des Projektes als nichtnegativ kantenbewerteter DAG fertig Seite Wege-35 Nun suchen wir vom Startknoten "Bb" (Baustelleneinrichtung begonnen) einen längsten Weg zum Zielknoten "fertig" (Ein "Projekt" ist ein Graph, in dem es zwei derart ausgezeichnete Knoten gibt, deren einer keine inzidenten Kanten hat (Startknoten) und deren anderer nur inzidente Kanten besitzt (Projektende)). Wir müssen längste Wege bestimmen, weil wir keine Aktivität beginnen können, solange die Vorgängeraktivitäten noch nicht beendet sind. Wir führen den Längste-Wege-Algorithmus durch. Der Verlauf des Verfahrens wird hier aus Platzgründen nicht abgedruckt, wir geben nur das Ergebnis an. Es sei jedoch erwähnt, daß wir im folgenden die längsten Wege zu allen Knoten des Graphen benötigen, so daß uns die fehlende Möglichkeit zum frühzeitigen "Ausstieg" aus der Wegesuche nicht schmerzt. Im folgenden Bild ist der Graph mit Angabe der Länge des jeweils längsten Weges D(Bb,w) für jeden Knoten w nochmals abgedruckt. Der längste Weg zum Knoten "fertig" ist hervorgehoben: 25 5 W1b 0 5 F1b 0 5 Bb 20 W1e 8 19 14 F1e 27 8 P1b 44 36 9 P1e 8 8 5 0 5 15 23 35 5 44 52 P2e 25 W2e 52 fertig 8 8 W2b 20 Ü1e Ü2b 8 Ü2e 0 F2b 10 F2e 8 P2b 12 0 51 0 0 Be Ü1b 8 43 50 0 Ü3b 8 Ü3e 8 Längen der längsten Wege zu allen Knoten und längster Weg vom Baubeginn zur Fertigstellung ("kritischer Weg") Unser Bauwerk ist also frühestens nach 52 Tagen fertiggestellt. Wenn sich aus irgendwelchen Gründen eine Kante, die Bestandteil des längsten Weges ist, "verlängert" (also z.B. die Erstellung des Pfeilers P1 länger dauert), wird der längste Weg "länger", d.h. die Fertigstellung unseres Projektes wird sich notwendigerweise auch verzögern. Aus diesen Gründen wird der längste Weg auch kritischer Weg genannt. Die Markierungen D(Bb,w) für jeden Knoten w geben an, wann der betreffende Knoten frühestmöglich erreicht werden kann. Wir können jetzt nocheinmal die Längste-Wege-Suche durchführen, diesmal jedoch für einen Graphen, der genau die umgekehrte Kantenrichtung besitzt, und mit dem Startpunkt "fertig". Die Lage und Länge des kürzesten Weges ist natürlich dieselbe wie zuvor, und wir erhalten folgendes Bild: Seite Wege-36 15 35 W1b 0 20 47 F1b 52 47 Bb 14 W1e 8 7 33 25 16 F1e P1b P1e 8 9 8 8 5 8 F2b 10 28 36 F2e 8 P2b 12 16 0 P2e 35 7 0 0 Ü3b 8 Ü3e 15 W2b 20 fertig 8 8 0 0 Ü2b 8 Ü2e 0 46 0 Ü1e 0 0 Be 0 Ü1b 8 8 W2e Rückwärtssuche vom Zielknoten zum Startknoten Wenn wir die soeben durch die "Rückwärtssuche" ermittelten längsten Wegstrecken von der minimalen Bauzeit 52 abziehen, dann erhalten wir für jeden Knoten des Graphen die spätest zulässigen Zeiten, die ohne Bauzeitverlängerung gerade noch zugelassen werden können: 37 17 W1b 0 5 F1b 0 5 Bb 20 14 W1e 8 45 19 27 36 F1e P1b P1e 8 9 8 8 5 0 4 16 26 36 17 52 P2e 37 W2e 52 fertig 8 8 W2b 20 44 Ü2b 8 Ü2e 0 F2b 10 F2e 8 P2b 12 0 52 Ü1e 0 0 Be Ü1b 8 45 52 0 Ü3b 8 Ü3e 8 Spätestmögliche Zeitpunkte ohne Bauzeitverlängerung Aus diesem Bild in Verbindung mit den frühestmöglichen Zeitpunkten für jeden Knoten können wir erkennen, ob wir uns bei einigen Vorgängen "Zeit lassen" können. Zum Beispiel sehen wir, daß wir das Widerlager W1 frühestens nach 5 Tagen beginnen können. Ohne Schaden für die Gesamtbauzeit könnten wir aber auch bis zum 17. Tag warten, bevor wir das Widerlager zu bauen beginnen. Oder wir könnten zwar schon am 5. Tag anfangen, uns jedoch gemächlich 12 Tage mehr genehmigen! Solche Informationen sind für den Bauleiter natürlich Gold wert. In der Anwendung auf Projektplanungsprobleme wird die Graphentheorie unter dem Namen "Netzplantechnik" angewendet (auch CPM=critical path method). Seite Wege-37 Ein Routensuchealgorithmus mit Zusatzinformation (A*-Algorithmus). Die Routensucheproblematik haben wir noch nicht ganz zufriedenstellend gelöst. Der DIJKSTRAAlgorithmus findet zwar die kürzeste Autobahnverbindung zwischen München und Hamburg, jedoch sucht er für unser Empfinden recht "orientierungslos" im Netz herum. Das liegt daran, daß der DIJKSTRA-Algorithmus die geometrische Zusatzinformation, die wir anhand der Straßenkarte sofort "sehen", nicht kennt und nicht ausnützt. Wir wollen nun einen Algorithmus vorstellen, der diesen Mangel behebt. Damit verlassen wir allerdings die reine Graphentheorie und begeben uns, je nach Sichtweise, in das Gebiet der künstlichen Intelligenz oder der diskreten Optimierung. Der sogenannte A*-Algorithmus stammt von Peter E. Hart, Nils J. Nilsson und Bertram Raphael (A formal basis for the heuristic determination of minimum cost paths¸ IEEE Transactions on Systems Science and Cybernetics 4, 1968). Er kann als Beispiel der sogenannten Branch-and-Bound-Strategie angesehen werden. Kern des A*-Algorithmus ist die Überlegung, daß wir beim DIJKSTRA-Algorithmus als Kriterium dafür, welchen Knoten wir als nächsten zur näheren Betrachtung aus der Menge A auswählen, lediglich die Distanz dieses Knotens vom Startknoten s gewählt haben, während wir ja eigentlich an der Minimierung der Gesamtlänge eines Weges vom Start- zum Zielknoten interessiert sind. Deswegen sucht der DIJKSTRA-Algorithmus zum Beispiel bei unserem obigen Beispiel der Routensuche von München nach Hamburg zuerst Wege, die über Salzburg oder Garmisch führen, nur weil diese Orte nahe an München liegen. Das Problem einer besseren Steuerung des Suchalgorithmus läuft darauf hinaus, die restliche Weglänge von einem Knoten in der Menge A bis zum Zielknoten z abzuschätzen. Wir benötigen eine garantierte untere Schranke für die Kosten dieses Restweges, um eine "zielgerichtete Suche" ("informed search") durchführen zu können. Wir führen dazu in jedem Knoten v eine Steuerungsfunktion der Form f (v ) = d ( s , v ) + h(v ) ein. Die Schätzfunktion h(v) schätzt die restliche Weglänge vom Knoten v zum Zielknoten z. Die Schätzfunktion muß mindestens die Bedingung erfüllen ("zulässige Schätzfunktion") 0 ≤ h (v ) ≤ δ ( v, z ) , wobei δ (v, z ) die Länge des kürzesten Weges von der Zwischenstation v nach dem Zielknoten z darstellt. Die Qualität unserer Suche hängt von der Qualität der Schätzfunktion ab. Die Frage ist nun, ob der Ablauf der Wegesuche bei Verwendung einer zulässigen Schätzfunktion derselbe bleiben kann wie zuvor beim DIJKSTRA-Verfahren. Diese Frage ist zu verneinen: Beim DIJKSTRA-Verfahren können wir sicher sein, daß ein einmal aus der Menge A entfernter und somit in die Menge F aufgenommener Knoten nie wieder in die Menge A zurückkehren muß; für die informierte Suche mit einer zulässigen Schätzfunktion gilt dies im allgemeinen nicht. Wir wollen daher nun eine Schätzfunktion betrachten, die eine Zusatzvoraussetzung erfüllt (sogenannte konsistente Schätzfunktion). Betrachten wir dazu zwei beliebige Knoten u und v des Graphen. h(u) und h(v) sind die Schätzwerte für die restlichen Wegekosten bis nach z. Eine Schätzfunktion heißt konsistent, wenn gilt: h(u ) ≤ h(v) + δ (u, v) Diese Bedingung entspricht einer "Dreiecksungleichung", wie die nachfolgende graphische Darstellung veranschaulicht: Seite Wege-38 v h(v) δ(u,z) u z h(u) In unserem Problem "Routensuche" ist es einfach, eine konsistente Schätzfunktion zu konstruieren: Wir wählen als Schätzfunktion h(v,z) einfach die Länge der Luftlinie zwischen v und z. Jede Straßenverbindung zwischen den beiden Knoten wird mindestens so lang sein wie die Luftlinienentfernung. Die Dreiecksungleichung ist dann als Ungleichung auf dem auch geometrisch faßbaren Dreieck u-z-v gegeben. Auch bei Kriterien wie "Fahrzeit" oder "Energieverbrauch" können wir ähnlich einfach eine konsistente Schätzfunktion konstruieren, indem wir die erforderliche Fahrzeit zum Beispiel durch die Länge der Luftlinie und eine obere Grenze für die Fahrgeschwindigkeit (zum Beispiel Geschwindigkeit <200 km/h) ermitteln, analog für den Treibstoffverbrauch. Der A*-Algorithmus läuft unter dieser Voraussetzung genau gleich wie der DIJKSTRAAlgorithmus ab: Seite Wege-39 "Routensuche mit A*-Algorithmus mit konsistenter Schätzfunktion h": gegeben: Graph G(V,E), Startknoten vs, Zielknoten z Für alle Knoten v aus V: v.statusÅUNBESUCHT v.vorgaengerÅNULL d(v) ÅINF h(v) ÅLuftlinienentfernung (v,z) Menge A d(vs)Å0 f(vs)Åh(vs) A.Einfügen(vs) Solange A.Anzahl()>0 wiederhole: vÅA.EntferneMinimum() falls v=z: ENDE für alle Nachbarknoten w von v: falls w.status=UNBESUCHT: w.statusÅAKTIV w.vorgaengerÅv d(w)Åd(v)+Bewertung(v,w) A.Einfügen(w) sonst falls w.status=AKTIV: falls d(v)+Bewertung(v,w) < d(w) w.vorgaengerÅv d(w)Åd(v)+Bewertung(v,w) f(w)Åd(w)+h(w) v.statusÅERLEDIGT Die gegenüber dem reinen Dijkstra-Algorithmus neuen Zeilen sind fett gedruckt. Wir entnehmen nach wie vor dasjenige Element v als nächstes aus der Menge A, das den kleinsten Wert der Steuerungsfunktion f(v) aufweist; dieser Wert gibt aber eine Schätzung für die minimale Länge eines Weges von s nach z unter der Nebenbedingung¸ daß dieser Weg durch den Knoten v verläuft, an. Unsere "Gier" (greed) ist jetzt also darauf gerichtet, möglichst kurze Gesamtwege zu berechnen. Mit Hilfe der konsistenten Schätzfunktion gelingt dies auch tatsächlich. Dadurch, daß wir immer den Knoten mit der minimalen unteren Schranke für die Länge des Gesamtweges als nächsten auswählen, scheiden wir "Umwege" von vornherein zunächst aus; nur wenn der vermeintlich "direkteste" Weg später doch in einer Sackgasse endet, werden auch vermeintliche "Umwege" noch einmal näher untersucht. Die "untere Schranke" für die gesuchte minimale Weglänge (lower bound) wird im Laufe des Verfahrens immer weiter angehoben, so daß die "Entscheidung" (branch) für vermeintliche "Umwege" mit wachsender unterer Schranke immer interessanter wird. In der Optimierung ist die Branch-and-BoundMethode bekannt, die auf eine solche Strategie aufbaut. Unser Algorithmus terminiert sofort, wenn der kürzeste Weg zum Zielknoten z gefunden worden ist. Wir suchen also nur für ein einziges Knotenpaar (s,z) den kürzesten Weg. Das Verhalten des A*-Algorithmus wollen wir ebenfalls an einem Beispiel zeigen: Seite Wege-40 Flensburg Kiel Rostock Lübeck Hamburg Wilhelmshaven Lüneburg Emden Oldenburg Bremen Berlin Frankfurt/Oder Hannover Braunschweig Wolfenbüttel Magdeburg Hildesheim Osnabrück Cottbus Bielefeld Clausthal-Zellerfeld Paderborn Jülich Aachen Leipzig Kassel Dresden Weimar Erfurt Jena Gera Chemnitz Zwickau Ilmenau Hof Siegen Köln Bonn Halle Göttingen Dortmund Bochum Essen Duisburg Hagen Wuppertal Düsseldorf Giessen Koblenz Frankfurt/Main Wiesbaden Mainz Darmstadt Schweinfurt Bamberg Bayreuth Würzburg Trier Worms Ludwigshafen Kaiserslautern Heidelberg Saarbrücken Erlangen Crailsheim Nürnberg Landau/Pfalz Heilbronn Karlsruhe Ludwigsburg Pforzheim Stuttgart Tübingen Reutlingen Regensburg Ingolstadt Passau Landshut Ulm Augsburg München MemmingenLandsberg/Lech Freiburg Salzburg Garmisch-Partenkirchen Konstanz Lindau Man erkennt deutlich, daß der Algorithmus von vorneherein in der "richtigen Richtung" sucht. In unserem Beispiel werden für die Bestimmung des kürzesten Weges von München nach Hamburg nur 29 Knoten aus der Menge A behandelt, während der DIJKSTRAAlgorithmus den Zielknoten "Hamburg" erst im 81. Schritt findet. Die Frage ist jetzt nur, ob der skizzierte Algorithmus auch genau wie der DIJKSTRAAlgorithmus garantiert die kürzesten Wege findet. Interessanterweise können wir dies ohne Mühe beweisen: Wir gehen von exakt derselben Induktionsvoraussetzung und von denselben Induktionsschritten wie beim DIJKSTRAVerfahren aus. Anstelle jedoch den Knoten v aus der Bedingung d ( s, v) = min d ( s, w) w∈ A zu bestimmen, wählen wir ihn jetzt gemäß der modifizierten Bedingung f (v) = min f ( w) w∈ A Seite Wege-41 mit f ( w) = d ( s, w) + h( w) . Ansonsten bleibt der Algorithmus unverändert. Wir müssen jetzt also nur zeigen, daß auch bei Verwendung des neuen Kriteriums für die Auswahl des nächsten Knotens v aus der Menge A die Bedingung d ( s, v ) = δ ( s, v ) erfüllt bleibt. Dazu berechnen wir wieder wie beim DIJKSTRA-Verfahren die Länge eines (vermeintlich kürzeren) Weges s-y-x-v, wobei y der letzte Knoten dieses Weges ist, der in F enthalten ist, während x der erste Knoten des Weges ist, der in der Menge A liegt: Menge F Menge A Menge U v s y x Die Länge L(s,x,v) dieses Weges ergibt sich unter Berücksichtung der Induktionsvoraussetzung, daß vor Ausführung des Schritts k+1 die Knoten in der Menge F ihre kürzesten Wege von s aus kennen und unter Verwendung der Dreiecksungleichung h ( x ) ≤ h (v ) + δ ( x, v ) zu: L ( s , x, v ) = δ ( s , y ) + B ( y , x ) + L ( x, v ) = = d ( s , x ) + L ( x, v ) ≥ d ( s , x ) + δ ( x, v ) = d ( s , x ) + h( x ) − h(v ) = f ( x ) − h (v ) Da wir nun aber den Knoten v als nächsten Kandidaten für die Auswahl gewählt haben, galt f (v ) ≤ f ( x ) , und wir können weiter schreiben: L ( s , x, v ) ≥ f ( x ) − h (v ) ≥ f (v ) − h(v ) = d ( s , v ) Somit muß der vermeintlich kürzere Weg von s nach v für jeden beliebigen Knoten x also länger sein als der Weg, den wir als Rückweg von v nach s gespeichert haben, also muß wieder wie zuvor beim DIJKSTRA-Verfahren gelten, daß d ( s, v) = δ ( s, v) ist, daß wir also für jeden Knoten, den wir aus der Menge A als nächsten aktuellen Knoten auswählen, zum Zeitpunkt seiner Wahl den kürzesten Weg vom Startknoten s her kennen. Damit bleibt die Eigenschaft der Menge F, daß die in ihr enthaltenen Knoten ihren kürzesten Weg von s her kennen, während der Durchführung des Verfahrens stets erhalten. Wir haben somit bewiesen, daß auch der A*-Algorithmus das Auffinden des kürzesten Weges von s nach z garantiert. Es sei noch angemerkt, daß die Schätzfunktion h(v)=0 für alle Knoten Seite Wege-42 v ebenfalls eine konsistente Schätzfunktion ist. Mit dieser Schätzfunktion entartet unser A*Algorithmus zum DIJKSTRA-Algorithmus. Wir haben also eine Hierarchie der Verfahren gemäß Breitensuche ⊂ Dijkstra ⊂ A * . Der A*-Algorithmus mit bloß zulässiger, aber nicht notwendig konsistenter Schätzfunktion hat eine große Bedeutung in der Spieltheorie und in anderen Anwendungsbereichen der künstlichen Intelligenz (artificial intelligence, AI). Ein heuristischer Greedy-Algorithmus zur Routensuche. Abschließend wollen wir noch demonstrieren, daß die Greedy-Strategie, die im Falle des DIJKSTRA-Algorithmus (und auch noch beim A*-Algorithmus) erfolgreich ist und ein garantiertes Minimum findet, keineswegs immer derart überzeugend und zielführend ist. Man könnte sich – gerade bei unserem Routensucheproblem – fragen, warum wir überhaupt von einem breitensucheartigen Algorithmus ausgehen. Bei der Routensuche mit dem Atlas "von Hand" würde man ja vielleicht einfach an jedem Ort, an dem sich mögliche Routen verzweigen, eine reine Greedy-Strategie anwenden. Das heißt, daß wir an jedem Knoten u diejenige Kante wählen, die verspricht, uns dem Ziel am meisten näherzubringen. Das "Nutzen-Kosten-Verhältnis" einer Kante u-v könnten wir zum Beispiel lokal definieren als h(u ) − h(v) N (u , v) = B(u , v) Hier soll die Schätzfunktion h(.) wie zuvor die konsistente untere Schranke für die Entfernung eines gegebenen Knoten vom Zielknoten angeben. Liegt v näher am Ziel als u, so wäre der Nutzen der Kante positiv. Um den Nutzen zu normieren, so daß lange und kurze Kanten vergleichbar werden, haben wir den Distanzgewinn noch auf die Kosten B(u,v) der Kante bezogen. So kann bei Verwendung unserer konsistenten Schätzfunktion N(u,v) nur noch Werte zwischen –1 und 1 annehmen. Notfalls würden wir also von einem einmal erreichten Knoten auch "fast in Gegenrichtung" weiterfahren, wenn sonst keine anderen Kanten verfügbar sind. Allerdings sind wir damit noch nicht dagegen gefeit, in einer Sackgasse zu landen. Damit wir als übergeordneten Algorithmus eine Tiefensuche machen, markieren wir die von u aus erreichbaren Nachbarknoten mit f (v) = − g (u ) − N (u , v) , wobei g(v) der Tiefensuche-Schritt ist, in dem wir u gefunden haben. Ohne diese Erweiterung der Strategie kann es passieren, daß wir in einer "Sackgasse" landen und nie mehr herausfinden. Mit der Tiefensuche-Komponenten hingegen kehren wir im schlimmsten Fall auf dem gefahrenen Weg solange zurück, bis sich eine Gelegenheit findet, das "Hindernis" weiträumig zu "umfahren". Mit diesen Komponenten kommen wir zur folgenden Algorithmusidee: Seite Wege-43 Algorithmus "heuristische Routensuche": gegeben: Graph G(V,E), Startknoten vs, Zielknoten z Für alle Knoten v aus V: v.statusÅUNBESUCHT v.vorgaengerÅNULL d(v) ÅINF h(v) ÅLuftlinienentfernung (v,z) Menge A A.Einfügen(vs) Integer zeit zeitÅ0 f(vs) Åzeit Solange A.Anzahl()>0 wiederhole: zeitÅzeit+1 vÅA.EntferneMinimum() für alle Nachbarknoten w von v: falls w.status=UNBESUCHT: w.statusÅAKTIV w.vorgaengerÅv f(w)Å-zeit-(h(v)-h(w))/Bewertung(v,w) A.Einfügen(w) v.statusÅERLEDIGT In manchen Fällen findet dieser auf den ersten Blick bestechende Algorithmus sehr schnell einen Weg vom Startknoten s zum Zielknoten z. Man lasse sich aber nicht täuschen: Der gefundene Weg ist zwar oft ein "kurzer" Weg, aber nicht der wirklich kürzeste! Bei der Verwendung von scheinbar einleuchtenden Heuristiken ist also Vorsicht geboten. Für viele Anwendungen ist allerdings der geringe Speicherplatzaufwand interessant, den Tiefensuchalgorithmen erfordern, und daher wird die Tiefensuche öfter angewendet, wenn keine theoretisch untermauerte bessere Suchstrategie verfügbar ist. Als Beispiel zeigen wir den Weg, den der genannte Algorithmus in unserem Autobahnbeispiel von München nach Hamburg wählt: Seite Wege-44 Flensburg Kiel Rostock Lübeck Hamburg Wilhelmshaven Lüneburg Emden Oldenburg Bremen Berlin Frankfurt/Oder Hannover Braunschweig Wolfenbüttel Magdeburg Hildesheim Osnabrück Cottbus Bielefeld Clausthal-Zellerfeld Paderborn Jülich Köln Aachen Bonn Halle Leipzig Göttingen Dortmund Bochum Essen Duisburg Hagen Wuppertal Düsseldorf Kassel Dresden Weimar Erfurt Jena Gera Chemnitz Zwickau Ilmenau Hof Siegen Giessen Koblenz Trier Schweinfurt Frankfurt/Main Bamberg Bayreuth Wiesbaden Mainz Darmstadt Würzburg Worms Ludwigshafen Kaiserslautern Heidelberg Erlangen Crailsheim Nürnberg Saarbrücken Landau/Pfalz Heilbronn Regensburg Karlsruhe Ingolstadt Ludwigsburg Pforzheim Stuttgart Tübingen Reutlingen Ulm Augsburg München Landsberg/Lech Memmingen Garmisch-Partenkirchen Freiburg Passau Landshut Salzburg Konstanz Lindau Dieser Weg sieht zwar "gut" aus, hat jedoch die Länge 729km, während der zuvor mit dem DIJKSTRA- und A*-Algorithmus gefundene Weg nur die Länge 713km aufweist. Die Summe lokaler "optimaler" Lösungen ergibt also keinesfalls ein globales Optimum! Allerdings ist der Aufwand, den wir zum Berechnen des "fast kürzesten Weges" hier gebraucht haben, unschlagbar gering ...! Wir haben nur solche Knoten betrachtet, die auf dem zuletzt gefundenen Weg liegen. Seite Wege-45 Flensburg Kiel Lübeck Wilhelmshaven Hamburg Lüneburg Emden Oldenburg Bremen Hannover Osnabrück Hildesheim Bielefeld Unser letztes Bild schließlich demonstriert, wie unser Algorithmus dank der TiefensucheKomponente den Weg aus einer "Sackgasse" herausfindet. Bei der Suche nach einem Weg von Osnabrück nach Flensburg tritt dieses Problem in unserem Beispielgraphen auf, weil der scheinbar kürzeste Weg durch die Deutsche Bucht führt. © 2003 Prof. Dr.-Ing. Stefan M. Holzer, Institut für Mathematik und Bauinformatik, Universität der Bundeswehr München