Spanning-Tree

Werbung
© U.-P. Schroeder, Uni Paderborn
16 Grundalgorithmen für Graphen
16.1
Ablaufen von Graphen
Eine häufig benötigte Basisoperation ist das Ablaufen aller Knoten in einem Graphen.
Da es in allgemeinen Graphen (im Gegensatz zum Wurzelbaum) keinen natürlicherweise
ausgezeichneten Knoten gibt, mit dem man beginnen kann, wird ein Anfangsknoten
(zufällig) ausgewählt.
Von dem aktuellen Knoten v wird nun zu dessen Nachbarn weitergegangen.
Dabei gibt es zwei Vorgehensweisen
•
Breitensuche
Erst werden alle Nachbarn von v besucht, bevor man sich weiter von v entfernt
•
Tiefensuche
Von einem Nachbarn von v ausgehend, versucht man soweit wie möglich fortzuschreiten,
bevor man den nächsten Nachbarn aufgreift
16-1
© U.-P. Schroeder, Uni Paderborn
16.1.1 Breitensuche
Gegeben sei ein Graph G als Adjazenzliste und ein ausgezeichneter Knoten s
Von s ausgehend breitet sich die Suche wie eine Welle über den Graphen aus.
Um über den Fortgang der Suche Buch zu führen, werden (als Statusinformation) die Knoten
eingefärbt.
Wir verwenden drei Farben
weiß:
noch nicht entdeckt
grau:
bereits entdeckt, aber noch nicht bearbeitet
schwarz:
bearbeitet
Die weißen Knoten repräsentieren also den noch unerforschten Teil des Graphen,
die schwarzen den bereits besuchten Teil.
Die grauen Knoten markieren den aktuellen Verlauf der „Suchfront“.
16-2
© U.-P. Schroeder, Uni Paderborn
Es gilt daher:
•
Zu Beginn sind alle Knoten weiß
•
Jeder Knoten wechselt seine Farbe von weiß nach grau, dann von grau nach schwarz.
•
Alle Nachbarn eines schwarzen Knotens sind grau oder schwarz.
•
Nach Abschluß der Breitensuche sind alle Knoten schwarz.
Die Breitensuche baut einen spannenden Baum auf mit dem Startknoten als Wurzel
(Breitensuchbaum des Graphen)
Vorgänger eines Knotens ist der Knoten, von dem aus er zum ersten Mal besucht wurde.
16-3
© U.-P. Schroeder, Uni Paderborn
void bfs(edge **adj, index no_nodes, index s, index *pred)
{ // performs breadth-first-search and builds corresponding tree
queue q = queue(no_nodes);
// to store gray nodes
color state[no_nodes];
// state of nodes
index u;
// temporary node variable
for (index i=0; i<no_nodes; i++)
{
// initialization
state[i] = white;
pred[i] = undefined;
// will contain tree
}
state[s] = gray;
// source node
q.enqueue(s);
while (!q.empty())
// there are still gray nodes available
{
u = q.dequeue();
// get next gray node
for (edge* v = adj[u]; v != NULL; v= v->next)// all neighbors of u
if (state[v->node] == white)
{
// gray and black neighbors are ignored
state[v->node] = gray; // color it
pred[v->node] = u;
// store predecessor ( bfs-tree)
q.enqueue(v->node);
// put it in queue
}
state[u] = black; // node u has been processed
}
}
16-4
© U.-P. Schroeder, Uni Paderborn
Beispiel
0
1
2
3
4
5
6
7
0
1
2
3
4
5
6
7
0
1
2
3
4
5
6
7
16-5
Queue:
1
Queue:
5
Queue:
0 2
0
6
© U.-P. Schroeder, Uni Paderborn
0
1
2
3
4
5
6
7
0
1
2
3
4
5
6
7
0
1
2
3
4
5
6
7
16-6
Queue:
2
6 4
Queue:
6 4
3
Queue:
4
7
3
© U.-P. Schroeder, Uni Paderborn
0
1
2
3
4
5
6
7
0
1
2
3
4
5
6
7
0
1
2
3
4
5
6
7
16-7
Queue:
3
Queue:
7
Queue:
∅
7
© U.-P. Schroeder, Uni Paderborn
Komplexität der Breitensuche
•
Der Aufwand für die Initialisierung ist Θ( V )
•
Da nach der Initialisierung kein Knoten jemals wieder weiß gemacht wird,
ist sichergestellt, daß jeder Knoten nur genau einmal in die Warteschlange kommt,
d.h. bearbeitet wird.
•
Da die Warteschlangenoperationen in Θ(1) ablaufen, erhalten wir einen Gesamtaufwand für
die Warteschlange von Θ( V )
•
Die Adjazenzliste jedes Knotens wird genau einmal durchlaufen, nämlich nach seinem
Entfernen aus der Warteschlange
•
Die Summe der Längen der Adjazenzlisten aller Knoten ist gleich der Anzahl der Kanten im
Graph, d.h. es ergibt sich dafür ein Aufwand von Θ( E )
•
Insgesamt benötigt die Breitensuche also Θ( V + E ) , d.h. ein Aufwand, der linear mit der
Größe der Adjazenzlistenrepräsentation des Graphen wächst.
16-8
© U.-P. Schroeder, Uni Paderborn
16.1.2 Tiefensuche
•
Statt wie bei der Breitensuche in breiter Front den Graphen abzusuchen und die Suchfront
schrittweise weiterzuschieben, versucht die Tiefensuche,
so tief wie möglich in den Graphen einzudringen
•
Vom aktuellen Knoten ausgehend, werden also nicht - wie bei der Breitensuche zuerst alle seine Nachbarn markiert,
sondern es wird vom ersten noch nicht markierten Nachbarn aus ein möglichst langer Pfad
markiert,
d.h. so lange von Knoten zu Knoten weitergegangen, bis man nicht weiterkommt, weil
der Knoten keine Nachfolger besitzt
alle Nachfolger bereits markiert sind
•
Aufgrund ihrer Struktur eignet sich die Tiefensuche besonders gut für eine rekursive
Formulierung
•
Um zu dokumentieren, in welcher Reihenfolge die Knoten grau bzw. schwarz eingefärbt
werden, führen wir einen Zähler time mit
•
Auch bei der Tiefensuche wird ein spannender Baum aufgebaut (Tiefensuchbaum)
16-9
© U.-P. Schroeder, Uni Paderborn
void dfs_visit(edge **adj, color *state, index s, int &time, int *fin,
int *disc, index *pred)
{
state[s] = gray; cout << "\ngray: " << s;
disc[s] = ++time; // node has been discovered
if (adj[s]!=NULL)
for ( edge* v = adj[s]; v != NULL; v=v->next)
if (state[v->node] == white)
{
pred[v->node] = s;
dfs_visit(adj, state, v->node,time,fin,disc,pred);
}
state[s] = black; cout << "\nblack: " << s;
fin[s] = ++time;
// processing of node has been finished
}
void dfs(edge **adj, index no_nodes, index s, int* fin, int* disc,
index* pred)
{ // recursive depth-first-search for strongly connected graphs
color state[no_nodes];
// state of nodes
for (index i=0; i<no_nodes; i++)
{
state[i] = white; // initialization
pred[i] = undefined;
}
int t=0;
dfs_visit(adj, state, s,t,fin,disc,pred);
}
16-10
© U.-P. Schroeder, Uni Paderborn
Beispiel
0
1
2
3
0
1/
1
2
3
1/
2/
4
5
6
7
4
5
6
7
0
1
2
3
0
1
2
3
1/
3/
1/
2/
4
3/
4/
2/
5
6
7
4
16-11
5
6
7
© U.-P. Schroeder, Uni Paderborn
0
1
1/
2
3/
2/
3
0
1
2
3
4/
1/
3/
4/
5/
2/
6/
5/
4
5
6
7
4
5
6
7
0
1
2
3
0
1
2
3
1/
3/
4/
1/
3/
4/
2/
6/7
5/
2/
6/7
5/8
4
5
6
7
4
5
6
7
0
1
2
3
0
1
2
3
4
1/
3/
4/9
1/
3/10
4/9
2/
6/7
5/8
2/
6/7
5/8
6
7
6
7
5
4
16-12
5
© U.-P. Schroeder, Uni Paderborn
0
1
2
3
0
1/
3/10
4/9
12/
2/11
6/7
5/8
1
2
3
1/
3/10
4/9
2/11
6/7
5/8
4
5
6
7
4
5
6
7
0
1
2
3
0
1
2
3
12/
1/
3/10
4/9
12/
1/
3/10
4/9
13/
2/11
6/7
5/8
13/14
2/11
6/7
5/8
4
5
6
7
4
5
6
7
0
1
2
3
0
1
2
3
12/15
1/
3/10
4/9
12/15
1/16
3/10
4/9
13/14
2/11
6/7
5/8
13/14
2/11
6/7
5/8
4
5
6
7
4
5
6
7
16-13
© U.-P. Schroeder, Uni Paderborn
Komplexität der Tiefensuche
•
Die Schleife für die Initialisierung hat einen Aufwand von Θ( V )
•
Die Prozedur dfs_visit wird im rekursiven Ablauf nur für weiße Knoten aufgerufen.
Da sie als erste Handlung den Knoten grau färbt, können daher maximal Θ( V ) Aufrufe von
dfs_visit stattfinden.
•
Innerhalb von dfs_visit wird die Adjazenzliste des aktuellen Knoten abgelaufen
•
Die Summe der Längen der Adjazenzlisten aller Knoten ist aber Θ( E )
Die Komplexität der Tiefensuche ist demnach Θ( V + E ) und damit dieselbe wie die der
Breitensuche
16-14
© U.-P. Schroeder, Uni Paderborn
Anmerkungen
•
Breiten- und Tiefensuche funktionieren in dieser Form sowohl für gerichtete als auch für
ungerichtete Graphen
•
Beide Verfahren beruhen auf der Erreichbarkeit aller Knoten vom Startknoten aus.
Bei nicht zusammenhängenden Graphen terminieren beide Verfahren in der jeweiligen
Zusammenhangskomponente.
•
Will man trotzdem den gesamten Graphen ablaufen, bzw. alle Zusammenhangskomponenten
finden, muß man einen noch weißen Knoten suchen und erneut den Algorithmus starten:
void dfs(edge **adj, index no_nodes, int* fin, int* disc, index* pred)
{ // recursive depth-first-search for general graphs
color state[no_nodes];
// state of nodes
for (index i=0; i<no_nodes; i++)
{
state[i] = white; // initialization
pred[i] = undefined;
}
int t=0;
for (index i=0; i<no_nodes; i++)
if (color[i] == white) dfs_visit(adj, state, i,t,fin,disc,pred);
}
16-15
© U.-P. Schroeder, Uni Paderborn
16.1.3 Topologische Sortierung
Eine topologische Sortierung eines gerichteten Graphen erzeugt eine Totalordnung der Knoten.
Jedem Knoten v wird eine Reihenfolgenummer r(v) zugeordnet.
Eine bijektive Abbildung r: V → {1,2,K , V } heißt topologische Sortierung der Knotenmenge V
eines gerichteten Graphen G = (V, E ) ⇔ ((u,v ) ∈E ⇒ r (u) < r (v ))
Die Topologische Sortierung ist nur in zyklenfreien (azyklischen) gerichteten Graphen möglich.
Sie kann leicht durch eine Tiefensuche gefunden werden:
Die Reihenfolge, in der die Knoten schwarz eingefärbt werden, ist invers zur topologischen
Sortierung
Wir erhalten den Algorithmus zut topologischen Sortierung daher durch eine leichte Modifikation
der Tiefensuche
Für jeden noch nicht behandelten (weißen) Knoten wird eine rekursive Tiefensuche gestartet.
Bei der Rückkehr wird dem betrachteten Knoten die größte noch nicht vergebene Nummer
zugeteilt
16-16
© U.-P. Schroeder, Uni Paderborn
void dfs_visit2(edge **adj,color *state, index s, index *r, index
&counter)
{ // depth-first-search recursive
state[s] = gray;
if (adj[s]!=NULL)
for ( edge* v = adj[s]; v != NULL; v=v->next)
if (state[v->endnode] == white)
dfs_visit2(adj, state, v->endnode, r, counter);
state[s] = black;
r[s] = counter--; // r[s] holds the topological number of node s
}
void top_sort(edge **adj, index no_nodes, index *rank)
{ // calculates topological sort by depth-first-search
color state[no_nodes];
// state of nodes
for (index i=0; i<no_nodes; i++)
state[i] = white;
// initialization
int counter = no_nodes;
// numbers the nodes in topol. order
for (index i=0; i<no_nodes; i++)
if (state[i] == white) dfs_visit2(adj, state, i,rank, counter);
}
16-17
© U.-P. Schroeder, Uni Paderborn
Beispiel
0
1
2
3
0
1
2
3
4
5
6
7
4
5
6
7
0
1
2
3
0
7
1
2
3
5
6
7
8
4
8
5
6
7
4
16-18
© U.-P. Schroeder, Uni Paderborn
0
1
2
3
0
7
7
8
8
1
2
3
4
5
6
7
4
5
6
7
0
7
1
2
3
0
7
1
2
6
3
5
6
7
8
4
8
5
6
7
4
16-19
© U.-P. Schroeder, Uni Paderborn
0
1
7
2
3
0
6
1
7
8
2
3
6
8
4
5
6
7
4
5
6
7
0
7
1
2
6
3
0
7
1
2
6
3
5
5
6
7
8
4
8
5
6
7
4
16-20
© U.-P. Schroeder, Uni Paderborn
0
1
7
2
3
0
6
5
8
1
2
3
7
6
5
4
8
3
4
4
5
6
7
4
5
6
7
0
7
1
2
6
3
5
0
7
1
1
2
6
3
5
8
2
3
4
8
2
3
4
6
7
4
6
7
4
5
16-21
5
© U.-P. Schroeder, Uni Paderborn
16.2 Verwaltung disjunkter Mengen
In vielen (graphentheoretischen) Algorithmen tritt die Notwendigkeit auf,
Mengen von Elementen in (Äquivalenz)klassen zu zerlegen.
Üblicherweise beginnt man mit einelementigen Mengen,
die durch Vereinigungsbildung immer größer werden.
Jede Menge verfügt über einen Repräsentanten, d.h. ein ausgewähltes Element.
Zur Verwaltung solcher Mengen kann man die folgenden Operationen verwenden:
make_set(v)
Erzeugt eine einelementige Menge bestehend aus dem Element v
Es wird angenommen, daß v noch in keiner anderen Menge enthalten ist
find(v)
Liefert den Repräsentanten der Menge, der v angehört.
union(u,v)
Vereinigt die beiden Mengen, denen u und v angehören.
Bei der Vereinigung muß ein neuer Repräsentant gewählt werden,
z.B. der Repräsentant der Menge von u.
Die beiden ursprünglichen Mengen gehen in der Vereinigungsbildung auf,
d.h. sie sind nach der Vereinigung nicht mehr vorhanden
16-22
© U.-P. Schroeder, Uni Paderborn
Beispiele
•
Ist u ein Element, so erzeugt make_set(u) die Menge {u}, deren Repräsentant u ist .
•
Sei {u,v,w,s} eine Menge und {x,y} eine Menge mit den Repräsentanten u bzw. x.
Dann erzeugt union(v,y) die Menge {u,v,w,s,x,y} mit dem Repräsentanten u
und die bisherigen beiden Mengen verschwinden
•
Sei {u,v,w,s} eine Menge und {x,y} eine Menge.
16-23
© U.-P. Schroeder, Uni Paderborn
Implementierung der Mengenoperationen
Eine einfache Realisierung ist die Speicherung als ungeordneter (invertierter) Baum
u
v
w
x
y
s
Der Repräsentant ist die Wurzel des Baumes und verweist auf sich selbst.
Von den Elementen aus wird der Repräsentant der Menge durch Ablaufen der Verweiskette
gefunden.
Um die Elemente schnell erreichen zu können werden sie in einem Feld gespeichert, d.h. sie
werden als Indizes für ein Feld verwendet, in dem ihre jeweiligen Nachfolger eingetragen sind.
Sind die Elemente bereits natürliche Zahlen, so können sie direkt als Indizes verwendet werden,
andernfalls sind sie entsprechend zu kodieren (z.B durch Verwendung eines Aufzählungstyps)
16-24
© U.-P. Schroeder, Uni Paderborn
C++-Beispiel
element enum(s,u,v,w,x,y);
element p[6];
void make_set(element i);
{ // i becomes root of tree
p[i] = i;
}
element find(element i)
{ // find root of tree
return i;
}
void union(element i, element j)
{ // make the root of first tree point to the root of second tree
p[find(j)] = find(i);
}
Das obige Beispiel mit den beiden disjunkten Mengen führt dann zu folgender Datenstruktur
p:
v u
s u
u v
v w
x x
x y
16-25
© U.-P. Schroeder, Uni Paderborn
Beispiel:
Wirkung von union(y,s)
x
u
x
v
u
y
v
s
w
w
p:
v u
s u
y
u v
v w
p:
x x
x y
16-26
v x
s u
s
u v
v w
x x
x y
© U.-P. Schroeder, Uni Paderborn
Komplexität der Mengenoperationen
make_set benötigt offenbar konstante Zeit (O(1)).
find läuft im schlimmsten Fall von einem Blatt bis zur Wurzel,
sein Aufwand hängt also von der Höhe des Baumes ab,
d.h. find benötigt im schlimmsten Fall O(n), wenn n die Zahl der Elemente ist.
Obwohl union den einen Baum nicht an ein Blatt, sondern an die Wurzel des andern Baum hängt,
kann trotzdem durch eine ungeschickte Reihenfolge der union-Operationen eine lineare Kette
entstehen.
Daher werden die folgenden Verbesserungen vorgeschlagen:
•
Geordnete Vereinigung
Beim Vereinigen (union) wird der kleinere Baum an die Wurzel des höheren Baums gehängt.
Um die Höhe der Bäume abzuschätzen, wird ein weiteres Feld rank[ ] verwendet.
•
Pfadverkürzung
Bei find wird dafür gesorgt, daß der betroffene Knoten des Baums direkt auf die Wurzel zeigt
16-27
© U.-P. Schroeder, Uni Paderborn
void make_set(element i);
{ // i becomes root of tree
p[i] = i;
rank[i] = 0;
}
element find(element i)
{ // find root of tree
if (i != p[i]) p[i] = find[p[i]];
return p[i];
}
void link(element i, element j)
{ // make the root of shorter tree point to the root of higher tree
if (rank[i]>rank[j]) p[j] = i;
else
{
p[i] = j;
if (rank[i] == rank[j]) rank[j]++;
}
}
void union(element i, element j)
{
link(find(i), find(j));
}
16-28
© U.-P. Schroeder, Uni Paderborn
Beispiel:
Wirkung der Geordneten Vereinigung: union(y,s)
u
x
v
u
v
y
s
w
x
s
w
y
Wirkung der Pfadverkürzung: find(s)
u
v
w
u
v
x
s
y
w
16-29
s
x
y
© U.-P. Schroeder, Uni Paderborn
Komplexität der verbesserten Implementierung
Es kann nachgewiesen werden, daß bei einer Menge von n Elementen für eine Folge von m>n
Mengenoperationen, die aus n make_set und m-n find- und union-Operationen besteht,
das folgende gilt
•
Bei Einsatz lediglich einer der beiden Verbesserungsideen (Pfadverkürzung oder Geordnete
Vereinigung) erhalten wir einen Aufwand von O(m log n)
•
Die einzelne Mengenoperation kann daher mit O(log n) abgeschätzt werden
Anmerkung:
•
Bei Einsatz beider Verbesserungsideen ist der Aufwand sogar O(m α (m,n)),
wobei α(m,n) die Inverse der sogenannten Ackermann-Funktion ist.
α(m,n) wächst so langsam, daß sie für alle praktischen Fälle mit gutem Gewissen als konstant
65536
abgeschätzt werden kann, denn es gilt z.B. α(m,n)
(Literatur: Cormen/Leiserson/Rivest: S. 450 ff.)
16-30
© U.-P. Schroeder, Uni Paderborn
16.3 Minimal spannende Bäume
16.3.1 Allgemeines
Def.:
Ein minimal spannender Baum oder Minimalbaum eines kantenbewerteten Graphen ist ein
spannender Baum, dessen Summe der Kantengewichte minimal ist.
Minimal spannende Bäume werden häufig benötigt, da sie die minimale Kantenaustattung eines
Graphen angeben, mit der die Erreichbarkeit aller Knoten in einem Netzwerk noch gewährleistet
ist.
8
1
4
0
2
7
3
9
2
11
8
7
8
7
4
14
6
1
4
10
6
2
5
Beispiel:
Will man in einem Rechnernetz eine Nachricht an alle Knoten schicken (broadcast), dann kann
dies entlang eines Minimalbaums geschehen
16-31
© U.-P. Schroeder, Uni Paderborn
Allgemeine Eigenschaften von Bäumen
•
Ein Baum mit n Knoten besitzt n-1 Kanten.
•
Fügt man einem Baum eine Kante hinzu, so enthält er einen Kreis und ist kein Baum mehr
Die Minimalbaum-Eigenschaft
Gegeben sei ein kantenbewerteter zusammenhängender Graph G = (V,E), U ⊂ V eine
nichtleere Teilmenge der Knoten.
Ist (u,v ) eine Kante minimaler Länge mit u ∈U und v ∈V \ U , so gibt es einen Minimalbaum,
der (u,v ) enthält
Beweis (durch Widerspruch)
16-32
© U.-P. Schroeder, Uni Paderborn
Allgemeine Algorithmusstruktur
Die bekanntesten Algorithmen verwenden die Greedy-Struktur (komponentenweiser Aufbau),
d.h. in jedem Schritt wird eine Kante hinzugefügt.
Allen liegt die gleiche Algorithmusschablone zugrunde:
void min_span_tree(V,E, &MST)
{ //algorithm template for calculation of minimum spanning tree
MST = ∅;
while (not yet finished)
{
select appropriate edge e;
MST = MST ∪{e};
}
}
Die einzelnen Algorithmen unterscheiden sich lediglich in der Auswahl der Kanten.
16-33
© U.-P. Schroeder, Uni Paderborn
16.3.2 Kruskals Algorithmus
Idee:
•
Sortiere die Kanten nach aufsteigenden Gewichten
•
Fasse jeden Knoten als einelementigen Baum auf.
•
Greife die Kanten der Reihe nach auf (kürzeste zuerst)
Verbindet die Kante zwei verschiedene Bäume, so vereinige die Bäume
durch Hinzufügen der Kante.
Andernfalls nimm die nächste (d.h. nächstgrößere) Kante.
(J. B. Kruskal, 1956)
16-34
© U.-P. Schroeder, Uni Paderborn
C++-Pseudocode für Kruskals Algorithmus
(Hier werden jetzt die im letzten Abschnitt eingeführten Mengenoperationen benötigt)
void kruskal_mst(V, E, &MST)
{ // calculates minimum spanning tree
for (each v ∈ V) make_set(v); // generate 1-element-sets
sort(E);
MST = ∅ ;
for (each (u,v) ∈ E in nondecreasing order)
{ {
union(u,v);
MST = MST ∪ {(u,v)};
}
}
}
Komplexität:
Das Sortieren der Kanten benötigt O( E log E ) Schritte.
Es werden O( E ) Mengenoperationen durchgeführt, wobei jede höchstens O(log E ) Schritte
benötigt.
Das ergibt eine Gesamtkomplexität von O( E log E )
16-35
© U.-P. Schroeder, Uni Paderborn
Beispiel (Kruskals Minimalbaumsuche)
8
1
4
0
2
7
3
11
8
4
7
8
7
14
6
4
0
11
4
7
8
4
0
2
7
3
7
4
14
6
4
4
10
6
1
2
8
1
9
8
7
8
10
6
1
4
5
2
(b)
2
11
14
6
(a)
1
3
9
8
7
8
5
2
7
2
10
6
1
2
4
9
2
8
1
0
7
4
14
6
4
10
6
1
(d)
16-36
3
9
8
7
8
(c)
7
2
11
5
2
2
5
© U.-P. Schroeder, Uni Paderborn
8
1
4
0
2
7
3
11
8
4
7
8
7
4
9
2
14
6
4
0
10
6
1
8
4
0
2
7
8
7
4
7
3
4
4
0
10
6
8
4
0
2
7
7
4
7
3
5
2
7
3
9
4
14
6
1
4
4
10
6
2
8
1
14
6
1
2
8
7
8
9
8
7
8
6
4
10
6
2
5
2
7
3
(h)
2
11
4
10
2
11
5
2
8
(g)
1
14
6
1
1
14
6
1
9
4
7
8
9
7
8
3
(f)
2
11
7
8
(e)
1
2
2
11
5
2
8
1
0
11
7
(i)
8
7
8
5
9
2
4
6
4
10
6
1
(j)
16-37
14
2
5
© U.-P. Schroeder, Uni Paderborn
8
1
4
0
2
7
3
11
8
4
7
8
7
4
9
2
14
6
4
0
10
6
1
8
4
0
2
7
7
7
3
4
14
6
1
4
4
10
6
2
8
1
14
6
1
9
4
7
8
9
8
7
8
3
4
10
6
2
5
2
7
3
(l)
2
11
7
8
(k)
1
2
2
11
5
2
8
1
0
11
7
(m)
8
7
8
5
9
2
4
6
4
10
6
1
(n)
16-38
14
2
5
© U.-P. Schroeder, Uni Paderborn
16.3.3 Prims Algorithmus
Auch dieser Algorithmus entspricht der obigen Schablone
Er benutzt die Minimalbaumeigenschaft, d.h. daß die kürzeste Schnittkante in einem Graphen zum
Minimalbaum gehört.
Idee (R.C. Prim, 1957):
•
Beginne den Baum mit einem beliebigen Knoten
•
Wähle jeweils die kürzeste Kante, die einen Knoten des Baums
mit einem noch nicht aufgenommenen Knoten verbindet.
Der Baum wächst also schrittweise.
Zu jedem Zeitpunkt besteht der Graph aus zwei disjunkten Teilmengen, dem Baum und dem Rest.
Durch die Minimalbaumeigenschaft ist sichergestellt, daß die gewählte Kante zum Minimalbaum
gehört
Da jede gewählte Kante einen Baumknoten mit einem (Noch-)Nichtbaumknoten verbindet, können
keine Zyklen entstehen
In grober Formulierung sieht der Algorithmus folgendermaßen aus
16-39
© U.-P. Schroeder, Uni Paderborn
void prim_mst(V, E, v0, &MST)
{ // calculates minimum spanning tree
MST = ∅;
// initialize tree
U =
// start node v0 (input parameter)
V\{v0};
∅}
{
select (u,v) with lowest cost and u ∈ U and v ∈ V\U;
MST = MST ∪ {(u,v)}; // add edge to tree
U = U\{u};
// remove node from U
}
}
Die Komplexität des Verfahrens hängt nun davon ab, wie wir die Auswahl der Kante
implementieren, d.h. welche Datenstruktur wir für die Verwaltung der Kanten wählen.
Die günstigste Wahl ist die Verwendung einer Prioritätswarteschlange auf der Basis eines Heaps.
Alle Knoten, die noch nicht im Baum sind (Menge U), werden in einer Warteschlange geordnet
nach einem Schlüsselwert key gehalten.
Dieser Schlüsselwert key[u] eines Knotens u ∈ U ist die Länge der kürzesten Kante, die u mit
einem Knoten v aus dem Baum (v ∈V \ U ) verbindet.
16-40
© U.-P. Schroeder, Uni Paderborn
Prims Minimalbaumsuche
In etwas feinerer Auflösung gelangen wir dann zu folgender Formulierung:
void prim_mst(V, E, v0, p)
{ // calculates minimum spanning tree
priority_queue Q; // values in increasing order
MST = ∅ ;
// initialize tree
for (each v ∈ V) key[v] = ∞ ;
build_heap(Q,V); // builds heap of elements from V
key[v0] = 0;
p[v0] = undefined; //p[v] indicates predecessor of v
while (!Q.empty)
{
u = Q.dequeue(); //removes element with smallest key from Q
for (each v ∈ adj[u])
{
if (Q.is_in(v) and c(u,v) < key[v])
{
// replace key[v] by c(u,v) and reorder heap:
Q.decrease(v, c(u,v)); // decrease is an additional
p[v] = u;
// operation of the priority queue
}
}
} // p now contains minimum spanning tree
}
16-41
© U.-P. Schroeder, Uni Paderborn
Beispiel (Prims Minimalbaumsuche)
8
1
4
0
3
9
8
4
7
8
7
14
6
8
4
2
2
7
5
3
9
2
11
8
4
7
8
7
14
6
8
4
2
2
7
5
3
9
2
11
8
7
8
7
4
14
6
1
4
10
6
1
1
4
10
6
1
1
0
7
2
11
0
2
4
10
6
2
5
16-42
© U.-P. Schroeder, Uni Paderborn
8
1
4
0
7
4
14
6
8
4
2
5
2
7
3
2
11
9
8
7
8
7
4
14
6
8
4
10
6
1
1
4
10
6
1
1
0
3
9
8
7
8
4
7
2
11
0
2
2
5
2
7
3
9
11
8
7
8
7
4
14
6
1
4
10
6
2
5
16-43
© U.-P. Schroeder, Uni Paderborn
4
0
8
1
8
7
8
7
0
3
14
6
4
10
6
8
1
2
5
2
7
3
9
8
7
8
7
0
4
1
11
4
7
9
11
4
2
4
6
8
4
10
6
1
1
14
2
5
2
7
3
9
11
8
7
8
7
4
14
6
1
4
10
6
2
5
16-44
© U.-P. Schroeder, Uni Paderborn
Komplexität von Prims Minimalbaumsuche
•
Die Initialisierung einschließlich des Aufbaus der Prioritätenwarteschlange mittels
build_heap kostet O( V ) Operationen.
•
Die while-Schleife wird V mal durchlaufen und jeder Aufruf von dequeue erfordert
O(log V ) Schritte, so daß der Gesamtaufwand für das Entnehmen von Elementen
aus der Schlange mit O( V log V ) anzusetzen ist.
•
Die for-Schleife wird insgesamt O( E ) mal durchlaufen, da alle Adjazenzlisten zusammen eine
Länge von 2 E besitzen.
Innerhalb der for-Schleife wird jeweils ein Schlüssel eines Elements in der Schlange geändert,
(heap-Operation decrease) was - inklusive der Reorganisation des Heaps - einen Aufwand
von O(log V ) impliziert.
Zusammend erhalten wir demnach einen Aufwand von O( V log V + E log V ) = O( E log V )
16-45
© U.-P. Schroeder, Uni Paderborn
16.4 Kürzeste Wege
16.4.1 Grundlagen
In kantenbewerteten Graphen war die Distanz zwischen zwei Knoten definiert als die Länge des
kürzesten Weges zwischen ihnen.
Gegeben sein ein Weg p = (v0 ,v1 ,K ,vn ) in einem Graphen G mit Kantengewicht c.
Die Länge des Weges ist dann definiert als
n
c( p) = ∑ c(vi−1 ,vi )
i=1
Bezeichnen wir mit P(u,v ) die Menge der Wege von Knoten u nach Knoten v, so ist die Distanz
zwischen u und v
min{c( p) p ∈P(u,v )} falls P(u,v ) ≠ ∅
d (u,v ) = 
sonst
∞
Ein kürzester Weg p von u nach v ist dann ein Weg p ∈P(u,v ) mit c( p) = d (u,v ).
16-46
© U.-P. Schroeder, Uni Paderborn
Problemvarianten
Das Problem der kürzesten Wege (KW-Problem) tritt in den folgenden Varianten auf:
1
1:1-KW-Problem (single-pair shortest-path problem)
Gesucht ist der kürzeste Weg von einem Knoten u zu einem Knoten v.
2
1:n-KW-Problem (single-source shortest-path problem)
Gesucht sind die kürzesten Wege von einem Quellknoten u zu allen anderen Knoten v.
3
n:1-KW-Problem (single-destination shortest-path problem)
Gesucht sind die kürzesten Wege zu einem Zielknoten v von allen anderen Knoten u.
4
n:n-KW-Problem (all-pairs shortest-path problem)
Gesucht sind kürzesten Wege zwischen allen Knoten des Graphen
Anmerkungen:
•
Problem 3 kann auf Problem 2 zurückgeführt werden (und umgekehrt), indem die Richtung
der Kanten vertauscht wird. In ungerichteten Graphen sind die beiden Probleme identisch
•
Eine Lösung für Problem 1 fällt Teil eines Lösung für Problem 2 ab. Außerdem ist kein
spezifischer Algorithmus für Problem 1 bekannt, der asymptotisch schneller wäre als die
besten Algorithmen für Problem 2
16-47
© U.-P. Schroeder, Uni Paderborn
Kanten mit negativen Gewichten
Kanten mit negativen Gewichten sind zwar untypisch für KW-Probleme, können jedoch
gelegentlich vorkommen.
Sie sind tolerierbar, solange keine vom Quellknoten erreichbare Zyklen negativer Länge entstehen.
Ein kürzester Weg wäre dann nicht mehr wohldefiniert.
1
4
6
-2
4
2
−∞
4
0 0
5
−∞
3
9
3
-8
−∞
4
Einige Algorithmen verlangen nichtnegative Gewichte, andere tolerieren sie, d.h. sie produzieren
ein korrektes Ergebnis, solange keine negativen Zyklen auftreten.
16-48
© U.-P. Schroeder, Uni Paderborn
Repräsentation kürzester Wege
In der Regel interessiert man sich nicht nur für die Länge eines kürzesten Weges, sondern auch für
den kürzesten Weg selbst.
Bei dem 1:n-KW-Problem, das wir betrachten wollen, bietet sich wieder die Verwendung eines
Feldes pred[ ] an, in dem für jeden Knoten v dessen Vorgänger u auf dem kürzesten Weg von
einem Quellknoten s nach v gespeichert ist (vgl. Breitensuche)
pred[ ] definiert einen Baum mit Wurzel s, der die kürzesten Wege zu allen anderen Knoten
enthält.
Wir wir sehen werden, genügen die kürzesten Wege dieser Baumstruktur.
16-49
© U.-P. Schroeder, Uni Paderborn
Beispiel:
Gerichteter Graph mit zwei unterschiedlichen KW-Bäumen mit Wurzel 0
1
3
2
9
6
1
3
3
3
1
0 0
2
4
7
2
5
1
0 0
4
2
2
7
5
5
3
pred:
2
9
6
11
4
6
-
0
1
0
3
0
1
2
3
4
5
3
pred:
16-50
11
4
6
-
0
3
1
2
0
1
2
3
4
© U.-P. Schroeder, Uni Paderborn
Eigenschaften kürzester Wege
Lemma: (Optimale Substruktur)
Gegeben sei ein Graph G=(V,E) mit Kantengewicht c. Sei p = (v0 ,v1 ,K ,vn ) ein kürzester Weg von
v0 nach vn . Sei pik = (vi ,vi+1 ,K ,vk ) mit 0 ≤ i ≤ k ≤ n ein Teilweg von p.
Dann ist pik = (vi ,vi+1 ,K ,vk ) ein kürzester Weg von vi nach vk .
Beweis:
Wenn wir p zerlegen in Teilwege p0i , pik und pkn , dann gilt: c( p) = c( p0i ) + c( pik ) + c( pkn )
Wir nehmen nun an, es gebe einen Weg pik′ von vi nach vk mit c( pik′ ) < c( pik ).
Dann ist p′ = p0i , pik′ , pkn ein Weg von v0 nach vn mit
c( p′ ) = c( p0i ) + c( pik′ ) + c( pkn ) < c( p0i ) + c( pik ) + c( pkn ) = c( p)
Dies ist ein Widerspruch zur Annahme, daß p ein kürzester Weg ist
Lemma
Gegeben sei ein Graph G=(V,E) mit Kantengewicht c und einem Ausgangsknoten s.
Dann gilt für alle Kanten (u,v ) ∈E: d ( s,v ) ≤ d ( s,u) + c(u,v )
Beweis (trivial)
16-51
© U.-P. Schroeder, Uni Paderborn
Relaxation
Zum Begriff der Relaxation
Gummiband entspannt sich
(Relaxation)
Gummiband
Zapfen wird entfernt
Wir verwenden während der Berechnung Schätzungen d(v) (obere Schranken) für die Distanz
zwischen Anfangsknoten s und allen anderen Knoten v.
Am Anfang werden alle oberen Schranken (mit Ausnahme von s) auf „unendlich“ gesetzt.
Anschließend findet Schritt für Schritt ein Relaxationsvorgang statt:
Es wird für eine Kante (u,v) geprüft, ob sich durch Berücksichtigung dieser Kante die bisher
gefundene Distanz d(v) von s nach v „entspannt“.
void relax(index u, index v)
{
if (d[v] > d[u] + c(u,v)) d[v] = d[u] + c(u,v);}
16-52
© U.-P. Schroeder, Uni Paderborn
16.4.2 Dijkstras Algorithmus
Die verschiedenen Kürzeste-Wege-Algorithmen unterscheiden sich im wesentlichen in der
Reihenfolge, in der Kanten zur Relaxation aufgegriffen werden.
Dijkstras Algorithmus (E.W. Dijkstra, 1959) geht davon aus,
daß alle Kantengewichte nichtnegativ sind.
•
Alle Knoten werden nach ihrer bisher gefundenen Distanz d(v) aufsteigend geordnet in einer
geeigneten Datenstruktur Q gehalten (am besten Prioritätswarteschlange mit Heap).
•
Der nächste Knoten u wird aus Q entfernt und alle von ihm ausgehenden Kanten werden
„relaxiert“.
•
Bei den Endknoten v der Kanten (Nachbarn von u) wird u als Vorgänger im KW-Baum
eingetragen, falls sich die Distanz d(v) verringert hat, also (u,v) eine Baumkante wird
•
Der Vorgang wird solange wiederholt, bis Q leer geworden ist.
16-53
© U.-P. Schroeder, Uni Paderborn
Beispiel
1
∞
2
∞
1
1
10
10
10
3
0 0
5
2
∞
1
2
∞
3
9
4
6
7
2
3
0 0
2
9
7
5
∞
4
5
3
16-54
4
6
2
∞
4
© U.-P. Schroeder, Uni Paderborn
1
8
2
14
1
1
8
10
10
3
0 0
2
9
4
6
5
3
2
9
1
7
7
4
5
3
2
9
1
8
10
4
6
5
2
1
8
3
0 0
7
5
7
4
2
2
9
1
10
3
0 0
2
13
1
2
9
4
6
7
5
5
3
2
3
0 0
2
9
7
5
7
4
5
3
16-55
4
6
2
7
4
© U.-P. Schroeder, Uni Paderborn
void dijkstra_shortest_path (V, E, v0, &p)
{ // calculates shortest path from v0 to all other nodes
priority_queue Q; // values d in increasing order
for (each v ∈ V)
{
d[v] = ∞ ; // distance to v0 (upper bound)
p[v] = undefined; // predecessor on shortest path
}
d[v0] = 0;
build_heap(Q,V); // builds heap of elements from V
while (!Q.empty)
{
u = Q.dequeue(); // removes element with smallest d from Q
for (each v ∈ adj[u])
{
if (d[v] > d[u] + c(u,v)) // relax
{ //replace d[v] by d[u]+c(u,v) and reorder heap
Q.decrease(v,d[u]+c(u,v));
p[v] = u;
}
}
} // p now contains shortest path tree with respect to v0
}
16-56
© U.-P. Schroeder, Uni Paderborn
Komplexität von Dijkstras KW-Algorithmus
•
Jede remove-Operation der Prioritätenwarteschlange benötigt O(log|V|) Schritte.
Insgesamt werden |V| dieser Operationen benötigt.
•
Der Aufbau des Heaps der Warteschlange kostet O(|V|)
•
Die Modifikation der d-Werte (relax) impliziert ein Umordnen des heaps,
das in O(log|V|) Schritten durchgeführt werden kann (Q.decrease).
Höchstens |E| dieser Operationen sind erforderlich.
•
Wir erhalten also O(|V| log|V| + |E| log|V|) = O((|V|+|E|) log|V|) = O(|E| log |V|)
16-57
© U.-P. Schroeder, Uni Paderborn
16.4.3 Fords Algorithmus (Bellman-Ford)
•
Der KW-Algorihmus nach Ford setzt keine nichtnegativen Gewichte voraus.
•
Er benutzt ebenfalls das Relaxationsprinzip.
Es wird also in jedem Iterationsschritt für jede Kante (u,v) geprüft,
ob ihre Verwendung die Distanz vom Quellknoten v0 zu ihrem Endknoten v reduziert.
Es wird so lange iteriert, bis keine Verbesserungen mehr möglich sind.
•
Bei Existenz negativer Zyklen würde dies bedeuten , daß der Algorithmus nicht stoppt.
•
Da jeder kürzeste Weg jedoch aus höchstens |V|-1 Kanten besteht, kann man sich überlegen,
daß |V|-1 Iterationsphasen ausreichen, um kürzeste Wege zu berechnen (falls existent)
•
In jedem Iterationsschritt i entstehen Teillösungen,
d.h. kürzeste Wege mit einer Kantenanzahl von i.
16-58
© U.-P. Schroeder, Uni Paderborn
result bellman_ford_shortest_path (V, E, v0, &p)
{ // calculates shortest path from v0 to all other nodes
// if there is a negative cycle result is ‘notfound’
// otherwise result is ‘found’ and p contains shortest path tree
for (each v ∈ V)
{
d[v] = ∞ ;
p[v] = undefined;
}
d[v0] = 0;
for (index i=1; i<|V|; i++)
for (each edge (u,v) ∈ E)
if (d[v] > d[u] + c(u,v)) // relax
{
d[v] = d[u] + c(u,v);
p[v] = u;
}
// if existing, p now contains shortest path tree with respect to v0
for (each edge (u,v) ∈ E) // check for negative cycles
if (d[v] > d[u] + c(u,v)) return notfound;
return found;
}
}
16-59
© U.-P. Schroeder, Uni Paderborn
Beispiel
(Bellman-Ford-Algorithmus für negative Gewichte)
1
−∞
6
2
−∞
5
-2
7
6
-3
8
0 0
1
6
7
2
−∞
3
9
-3
-4
7
2
7
7
3
16-60
-2
8
0 0
−∞
4
2
−∞
5
9
-4
−∞
4
© U.-P. Schroeder, Uni Paderborn
1
6
2
4
5
-2
6
1
2
6
-3
8
0 0
7
7
7
3
1
2
6
2
4
-2
-3
7
2
7
7
3
9
-4
-2
4
16-61
7
2
7
3
2
4
5
8
0 0
8
0 0
7
9
-2
-3
-4
2
2
4
5
9
-4
2
4
© U.-P. Schroeder, Uni Paderborn
Komplexität des Ford-Algorithmus
•
•
Die Initialisierung besteht aus Θ ( V ) Schritten
Die innere for-Schleife besteht aus der Relaxationsoperation (O(1)) und wird jeweils E mal
ausgeführt, d.h. ihr Aufwand beträgt O( E )
•
Die äußere for-Schleife wird V −1 mal ausgeführt, jeweils mit einem Aufwand von O( E ) ,
d.h. die geschachtelte for-Schleife ist mit O( V ⋅ E ) zu kalkulieren
•
Die nachfolgende Prüfung auf Existenz negativer Zyklen kostet O( E )
•
Die Gesamtkomplexität des Ford-Algorithmus beträgt daher O( V ⋅ E ).
16-62
© U.-P. Schroeder, Uni Paderborn
Das n:n-KW-Problem (all-pairs shortest paths)
•
•
Kürzeste Wege zwischen allen Knoten des Graphen erhält man, indem man für jeden Knoten
das 1:n-Problem löst, d.h. den Dijkstra- oder den Ford-Algorithmus |V| mal aufruft.
(
)
Dies führt zu einer Gesamtkomplexität von O V 2 E (Ford-Algorithmus) bzw. O( V E log V )
(Dijkstra-Algorithmus)
•
•
Bei dünn vermaschten Graphen, z.B. wenn E = O( V ), und nichtnegativen Gewichten ist
diese Vorgehensweise zu empfehlen und der Dijkstra-Algorithmus einzusetzen.
In anderen Fällen gibt es spezielle Algorithmen für das n:n-KW-Problem (z.B. FloydAlgorithmus) mit einer Komplexität von O V 3
( )
16-63
Herunterladen