Kürzeste Wege in Graphen

Werbung
Kürzeste Wege in Graphen
Algorithmische Paradigmen
In diesem Abschnitt wollen wir nicht nur neue Algorithmen vorstellen, sondern auch den Blick auf
Gemeinsamkeiten und prinzipielle Unterschiede zwischen verschiedenen Algorithmen richten. Bei
genauerer Betrachtung findet man bestimmte Grundmuster, welche beim Entwurf von Algorithmen
sehr häufig auftreten, man spricht dann von algorithmischen Paradigmen. Drei solche Grundmuster
werden wir etwas näher kennenlernen:
1. Greedy–Algorithmen (gierige oder gefräßige Algorithmen)
2. Divide and Conquer (das Prinzip vom Teilen und Herrschen)
3. Dynamische Programmierung
Zu den weiteren Paradigmen (auf die wir hier nicht näher eingehen werden) gehören u.a. die Randomisierung, Approximationsalgorithmen, lineare Optimierung und genetische Algorithmen.
Greedy–Algorithmen sind per Definition eng mit dem mathematischen Begriff des Matroids verknüpft, aber man kann ihre Gemeinsamkeiten auch verbal beschreiben als Algorithmen für Optimierungsprobleme, bei denen eine vorhandene Lösung für ein Teilproblem immer schrittweise erweitert
wird bis man eine Lösung des Gesamtproblems erreicht hat. Typische Vertreter sind Breiten- und
Tiefensuche sowie der Algorithmus von Prim zur MST-Berechnung. Im Gegensatz dazu wird die
Gesamtlösung bei einem Ansatz mit Divide and Conquer aus den Lösungen mehrerer Teilprobleme
zusammengesetzt, in der Regel kombiniert mit rekursiven Aufrufen. Um zu einer effizienten Lösung
zu kommen, sollte bei jeder Aufspaltung die Anzahl der neuen Teilprobleme konstant sein (im Standardfall zwei) und die Größe der neuen Teilprobleme um einen konstanten Faktor c < 1 abnehmen
(z.B. Halbierung der Größe). Bekannte Vertreter sind Merge–Sort und die Binärsuche. Bei der dynamischen Programmierung wird die Lösungsmenge von vielen Teilproblemen einer bestimmten Größe
m im nächsten erweitert auf eine Lösungsmenge von Teilprobleme der Größe m + 1. Im Gegensatz zu
Greedy–Algorithmen reicht also die Lösung eines Teilproblems nicht aus zur Erweiterung, sondern
man muss den Zugriff auf verschiedene Teillösungen haben. Oft kann man die Teillösungen in Form
einer Tabelle erfassen und verwalten. Wir haben noch keinen typischen Vertreter kennengelernt, aber
die Berechnung der Binomialkoeffizienten mit dem Pascalschen Dreieck gehört im weiteren Sinne
auch zu dieser Klasse.
Im Folgenden wollen wir uns mit der Bestimmung kürzester Wege in Graphen beschäftigen. Dazu
sei G = (V, E) ein gerichteter Graph mit einer Gewichtsfunktion w : E −→ R, die den Kanten nichtnegative Gewichte (‘Länge’ der Kante) zuordnet. Wir definieren das Gewicht eines Weges als die
Summe der Gewichte seiner Kanten. Unser erstes Ziel wird es sein, für einen Startknoten s leichteste,
also im Sinne der Anwendung kürzeste, Wege zu allen anderen erreichbaren Knoten zu berechnen.
Man beachte, dass sich die Weglänge dann nicht auf die ursprüngliche Definition (Anzahl der Kanten)
bezieht, sonder auf die Summen der Kantengewichte. Haben alle Kanten das gleiche Gewicht, dann
stimmen die beiden Begriffsbildungen überein. In einem zweiten Schritt wird es darum gehen, für alle
Knotenpaae jeweils kürzeste Wege zu berechnen.
1
Der Algorithmus von Dijkstra
Vorüberlegungen:
• Die Eigenschaft ‘kürzester Weg’ vererbt sich auf Teilwege. Ist also p ein kürzester Weg von u
nach v über die Zwischenknoten u0 und v0 , dann ist der induzierte Teilweg von u0 nach v0 auch
ein kürzester.
• Damit kann man kürzeste Wege von s zu allen erreichbaren Knoten so auswählen, dass sie einen
Baum bilden, den man kompakt mittels der π–Zeiger verwaltet.
• Breitensuche ist ein Spezialfall eines Kürzesten-Wege-Problems, alle Kanten haben Gewicht 1.
Idee des Algorithmus
• In Analogie zum MST–Algorithmus von Prim vergrößern wir in jedem Schritt eine Menge S von
Knoten, für die die Abstände von s und kürzeste Wege bereits bestimmt sind. Der Algorithmus
ist ‘greedy’.
• Für jeden Knoten v in V \ S halten wir einen Schlüssel d[v] aufrecht, der sagt, wie lang der
bisher bekannte kürzeste Weg von s nach v ist und wir merken uns auch, wer der Vorgänger
π[v] auf einem solchen Weg ist. Die Schlüssel d[v] werden in einer Prioritäts–Warteschlange Q
verwaltet.
• Wir nehmen den Kopf u der Warteschlange in S auf und müssen überprüfen, ob man dessen
Nachbarn v von s aus über u besser (d.h. auf kürzerem Weg als bisher bekannt) erreichen kann.
Dies nennt man Relaxieren und formal ist es folgendes:
Falls d[v] > d[u] + w(u, v) dann ersetze d[v] durch d[u] + w(u, v) und π[v] zeigt jetzt auf u.
Adj[u]
v
u
s
S
V\S
Obige Abbildung zeigt, welche Knoten des Graphen nach Entfernung eines minimalen Elements u
aus der Prioritätswarteschlange betrachtet werden müssen. Hier ist der formale Pseudocode.
2
Dijkstra(G,w,s)
for all v ∈ V(G)
d[v] = ∞
π[v] = Nil
d[s] = 0
S = 0/
for all v ∈ V(G)
insert v in Q
while Q not empty
u = Extract-Min(Q)
S = S ∪ {u}
for all v ∈ Adj[u]
if (d[v] > d[u] + w(u,v))
π[v] = u
d[v] = d[u] + w(u,v)
Welche Laufzeit erreicht dieser Algorithmus? Die Antwort hängt von der konkreten Realisierung der
Prioritätsschlange ab:
• Verwendet man unsortierte Arrays, so kostet Extract-Min im worst case Länge des Arrays,
während die Update-Operation in konstanter Zeit geht, also insgesamt O(|V |2 ).
• Verwendet man binäre Heaps, so kostet Extract-Min O(log |V |) Zeit. Die Updates gehören nicht
zu den Basisoperationen einer Prioritätswarteschlange, aber man kann sie analog zum Verhalden eines neuen Elements in O(log |V |) Zeit implementieren. Zu beachten ist dabei, dass in
einem Feld die Positionen der Knoten in der Halde gespeichert werden müssen. Insgesamt erhalten wir eine Laufzeit von O(|E| log |V |).
Korrektheitsbeweis für den Algorithmus:
Bezeichne dG (s, v) die Länge eines kürzesten Weges von s nach v. Wir müssen zeigen, dass in dem
Moment, wenn v in S aufgenommen wird, dG (s, v) = d[v] gilt (danach wird d[v] nicht mehr verändert!)
Zunächst ist klar, dass im Algorithmus zu jedem Zeitpunkt und für jeden Knoten v gilt d[v] ≥ dG (s, v).
Die entgegengesetzte Ungleichung wird indirekt bewiesen.
u
S
s
Ein kuerzester Weg von s nach u
y ist der erste Knoten ausserhalb von S,
x
x der Vorgaenger von y auf diesem Weg
y
3
Beweis mit Widerspruch:
1. Sei u der zeitlich erste (!!) Knoten, für den bei Aufnahme in S gilt d[u] > dG (s, u), also der
Algorithmus einen Fehler macht.
2. Sei y erster Knoten auf einem tatsächlich kürzestem Weg von s nach u, der in V \ S liegt. Für y
gilt aber d[y] = dG (s, y).
Begründung: d[x] ist korrekt für den Vorgänger x von y (es gilt x ∈ S). Nach Annahme war u ja
erster Fehler. Bei Aufnahme von x in S wurde Kante (x, y) relaxiert, also ist auch d[y] korrekt.
Wenn y = u, sind wir hier schon fertig.
3. Ansonsten haben wir: d[u] > dG (s, u) = dG (s, y) + dG (y, u) = d[y] + dG (y, u).
Aber damit ist d[u] > d[y] bei der Aufnahme von u in S, weil dG (y, u) nichtnegativ ist.
4. Das ist aber der gesuchte Widerspruch, der Algorithmus hat u genommen, hätte aber tatsächlich
y mit dem kleineren Schlüssel nehmen müssen.
Anmerkung: Für die meisten Anwendungen ist die Voraussetzung von nichtnegativen Kantengewichten keine wirkliche Einschränkung. Der Algorithmus arbeitet aber sogar für Graphen mit negativen
Kantengewichten korrekt, wenn man die Existenz von sogenannten negativen Zyklen ausschließen
kann. Damit sind gerichtete Kreise gemeint, deren Kantengewichte sich zu einem negativen Wert
aufsummieren.
Der A∗ -Algorithmus
Wird vom Startknoten s nur der kürzeste Weg zu einem bestimmten Zielknoten w gesucht (Dijkstra
berechnet die kürzesten Wege zu allen möglichen Zielknoten), dann kann der Algorithmus unter bestimmten Voraussetzungen beschleunigt werden. Das ist insbesondere der Fall, wenn der Graph neben
den Kantengewichten auch eine Einbettung in die Ebene hat mit der Eigenschaft, dass für jede Kante (u, v) die Ungleichung wG (u, v) ≥ dist(u, v), wobei dist(u, v) den Euklidischen Abstand zwischen
den Punkten u und v angeben soll. Diese Situation liegt z.B. bei Navigationssystemen vor: Die Länge
wG (u, v) einer Straßenverbindung von u nach v kann nicht kürzer als die Luftlinie dist(u, v) sein.
Das Problem bei der Anwendung des Dijkstra–Algorithmus, um z.B. die kürzeste Route von Berlin nach München zu berechen, besteht darin, dass man erst zum richtigen Ergebnis kommen kann,
wenn man vorher auch die kürzesten Routen nach Hamburg, Kiel und Rostock berechnet hat, also von
Städten, die näher an Berlin liegen als München, aber für den kürzesten Weg nach München bestimmt
nicht relevant sind. Der A∗ –Algorithmus verfolgt die Idee, die Schlüsselwerte in der Prioritätswarteschlange so zu verändern, dass Orte, die dichter am Ziel liegen (und somit als Zwischenknoten in
Frage kommen) bevorzugt werden gegen Orte, die weit vom Ziel entfernt sind. Dafür führt man eine
Heuristik h : V → R+ ein mit welcher der kürzeste Weg von einem Zwischenknoten u zum Zielknoten
w von unten abschätzt. Es muss also immer h(u) ≤ dG (u, w) gelten. Bei Navigationssystemen kann
man h(u) = dist(u, w) setzen. An Stelle des Dijkstra-Schlüssels d[u], der die Länge des kürzesten
bekannten Wegs repräsentiert, setzt man als neuen Schlüssel den Wert f [u] = d[u] + h(u). Damit
bekämen in unserem Beispiel mit s = Berlin und w = München alle Orte nörlich von Berlin schon
allein durch den großen Euklidischen Abstand zu München einen hohen Schlüsselwert, Orte auf dem
kürzesten Weg nach München würden dagegen bevorzugt aus der Warteschlange entfernt werden.
4
Der Algorithmus von Floyd-Warshall
Ziel ist es, für einen gerichteten Graphen G = (V, E) und eine Kantengewichtsfunktion w alle Abstände
zwischen Knoten und realisierende kürzeste Wege zu finden. Das heißt dann All-Pair-Shortest-PathProblem.
Wir setzen wieder voraus, dass es keine Kreise mit negativem Gesamtgewicht gibt.
Natürlich läßt sich dieses Problem durch Anwendung des Dijkstra-Algorithmus für jeden möglichen
Startknoten s lösen.
Wir werden aber sehen, dass es konzeptionell viel einfacher geht und insbesondere nur elementare
Datenstrukturen nötig sind. Das dahinter stehende algorithmische Prinzip ist das des Dynamischen
Programmierens.
Die Idee: Wir benennen die Knoten mit 1, ...., n. Ziel ist, eine n × n–Matrix D zu berechnen, deren
Einträge di, j die Länge eines kürzesten gerichteten Weges von i nach j angibt. Ausgangspunkt ist die
Kostenmatrix W welche für vorhandene Kanten das Kantengewicht, Nullen auf der Diagonalen und
∞ für Nicht–Kanten enthält.
Zusätzlich werden wir mit π-Verweisen wie bei Dijkstra tatsächlich kürzeste Wege implizit verwalten.
Sei also Π die n × n–Matrix, deren Eintrag πi, j der Vorgänger des Knotens j auf einem kürzesten Weg
von i nach j ist.
Wir werden zunächst starke Einschränkungen auf das Problem legen, die eingeschränkten Problem
lösen und dann die Einschränkungen wieder Schritt für Schritt wegnehmen, bis wir wieder beim
Ausgangsproblem sind.
Wie sehen die Einschränkungen aus? Wir betrachten für die Knoten i, j nicht mehr alle Wege von i
nach j und suchen darunter einen kürzesten, sondern wir schränken die Menge der möglichen Zwischenknoten (die diese Wege benutzen können, aber nicht müssen) ein.
(k)
Wir definieren: Sei di, j die Länge eines kürzesten Weges von i nach j, der nur Zwischenknoten aus
der Knotenmenge {1, ..., k} benutzt. Nichtdefinierte Werte werden auf ∞ gesetzt. Für k = 0 erlauben
wir gar keine Zwischenknoten!
(k)
Die πi, j sind analog definiert, NIL falls der Knoten nicht existiert.
(k)
(k)
Zentrale Beobachtung: Die Matrizen D(k) der di, j –Werte und die Matrizen Π(k) der Knoten πi, j
lassen sich bottom–up sehr einfach berechnen.
• Initialisierung: Die Matrizen D(0) und Π(0) kann man direkt aus der Adjazenzmatrix A = (ai, j )
des Graphen (dies ist hier die geeignete Datenstruktur, ai, j die Länge der Kante) ablesen.

i= j
 0
(0)
ai, j ∃i → j
di, j =

∞ sonst

i= j
 i
(0)
i
∃i → j
πi, j =

NIL sonst
• D(k) gewinnt man aus D(k−1) mittels der folgenden rekursiven Beziehung:
5
(k−1)
(k)
di, j = min{di, j
(k−1)
, di,k
(k−1)
+ dk, j
}
Denn wenn der Knoten k als Zwischenknoten vorkommt dann nur ein Mal!!
Und analog:
(
(k)
πi, j
=
(k−1)
(k−1)
falls di, j
(k−1)
sonst
πi, j
πk, j
(k−1)
< di,k
(k−1)
+ dk, j
• Es ist offensichtlich D(n) = D und Π(n) = Π.
Zwischenknoten aus der Menge {1,2,.... ,k}
j
k
i
Zwischenknoten aus
Zwischenknoten aus
der Menge {1,2,.... ,k−1}
der Menge {1,2,.... ,k−1}
j
i
Zwischenknoten aus der Menge {1,2,.... ,k−1}, also ohne k
Dies ergibt sofort den folgenden Algorithmus für die Bestimmung von D(n) :
Floyd–Warshall(W,n) D(0) = W
for k = 1 to n
for i = 1 to n
for j = 1 to n
(k)
(k−1) (k−1)
(k−1)
di, j = min( di, j ,di,k + dk, j )
return D(n)
Die Laufzeit dieses Algorithmus ist offensichtlich Θ(n3 ).
Eine Variante: Die transitive Hülle eines gerichteten Graphen G = (V, E) ist der Graph G∗ = (V, E ∗ ),
wobei es in G∗ eine Kante von i nach j gibt, falls in G ein gerichteter Weg von i nach j existiert.
Man kann eine einfache Abwandlung des Floyd–Warshall dazu benutzen, die transitive Hülle zu bestimmen.
(k)
Dazu sei ti, j eine Boolesche Variable, die 1 ist, falls ein gerichteter Weg mit Zwischenknoten aus
6
{1, ..., k} von i nach j existiert und 0 sonst.
Offensichtlich gilt:
(k−1)
(k−1)
(k−1)
(k)
ti, j = ti, j ∨ (ti,k ∧ tk, j )
mit der Initialisierung

 1 i= j
(0)
1 ∃i → j
ti, j =

0 sonst
7
Herunterladen