7 Graphen - Ergänzung Wegesuche

Werbung
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
Herunterladen