Informatik 2 - Script 3. Teil
Inhaltsverzeichnis
6 Hashing
6.1
90
Hashing mit direkter Verkettung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
90
6.1.1
Multiplikationsmethode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
91
6.1.2
Erweiterte Divisonsmethode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
91
6.1.3
Hashing mit offener Adressierung . . . . . . . . . . . . . . . . . . . . . . . . . . . .
91
7 Algorithmus auf Graphen
7.1
95
Grundbegriffe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
95
7.1.1
Grafische Darstellung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
95
7.1.2
Weitere Begriffe
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
96
7.1.3
Darstellung von Graphen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
97
7.2
Minimal spannende Bäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
99
7.3
Tiefen- und Breitensuche (DFS bzw. BFS) . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
7.3.1
Grundidee von DFS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
7.3.2
Algorithmus in Pseudocode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
7.3.3
Laufzeit von DFS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
7.3.4
Grundidee von BFS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
7.3.5
Anwendung von DFS/BFS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
7.4
Zusammenhangskomponenten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
7.5
Kürzeste Wege . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
7.5.1
Algorithmus von Dijkstra . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
7.5.2
Kürzeste Wege zwischen je zwei Knoten . . . . . . . . . . . . . . . . . . . . . . . . 114
89
6
Hashing
Verwaltung einer Menge S von Elementen unter den Operationen:
Suche(x): stelle fest, ob x in S ist
Einfüge(x): füge Element x in S ein
Entferne(x): entferne x aus S
Elemente entsprechen Schlüsseln.
Elemente x ∈ S seien jeweils Elemente eines Universums U.
Universum und dazu gehörige Hashtabelle
Idee: speichere die Elemente x ∈ S in einer Hashtabelle T[0],...,T[m-1] ab. Mit Hilfe
einer Hashfunktion h : M → {0,...,m-1} wird berechnet in welcher Komponente der
Tabelle ein Element abzuspeichern ist.
Häufig: m |M| (aus Speicherplatzgründen)
Beispiel für Hashfunktion: h(x) = (ax + b) mod m
Kollision: 2 Elemente aus S sollen in der selben Position der Hashtabelle abgespeichert
werden (d.h. für Elemente x,y ∈ S, x 6 = y gilt h(x) = h(y)).
6.1
Hashing mit direkter Verkettung
Jede Komponenten T[i], i ∈ {0,...,m-1} des Arrays T speichert eine einfach verkettete Liste.
Elemente mit demselben Hashwert werden in derselben Liste abgespeichert.
Beispiel: h(x) = (3x) mod 7, S={0,2,4,5,7,10,14}
h(0) = 0
h(2) = 6
h(4) = 5
h(5) = 1
h(7) = 0
h(10) = 2
h(14) = 0
⇒
0
1
2
3
4
5
6
→
→
→
→
→
→
→
0 → 7 → 14 → nil
5 → nil
10 → nil
nil
nil
4 → nil
2 → nil
90
Operationen:
Suche(x): durchlaufe Liste T[h(x)]
Einfüge(x): füge x am Anfang der Liste T[h(x)] ein
Entferne(x): durchlaufe Liste T[h(x)]
n = Anzahl der Elemente in S, m = Größe der Hashtabelle
Suche
Einfügen
Entferne
Worst-Case
O(n)
O(1)
O(n)
Average-Case
n
O( m
+1)
O(1)
n
O( m
+1)
Bemerkung:
1. eine Liste enthält durchschnittlich
n
m
Elemente
n
2. ist m
konstant, so is die durchschnittliche Laufzeit der Operationen konstant.
n
Der Wert α = m
wird Lastfaktor genannt.
Hashfunktionen: Divisionsmethode h(x) = x mod m, m = Primzahl
(a) S = {200,205,210,215,...,595}, m = 100 ⇒ {0,5,10,15,...,95}
(200 kollidiert mit 300, 400, 500; 205 kollidiert mit 305, 405, 505)
Jedes Element kollidiert mit 3 anderen!
(b) S = {200,205,210,215,...,300,305,310,315,...,400,405,410,415,...,500,505,510,515,...},m=101
→ {99,3,8,13,...,38,2,7,12,...,97,1,6,11,...,96,0,5,10,...}
⇒ Keinerlei Kollisionen!
6.1.1
Multiplikationsmethode
h(x) = b m (x · c
6.1.2
mod 1) c, wobei c ∈ (0,1) Konstante ist.
Erweiterte Divisonsmethode
h(x) = (ax + b) mod m, wobei a 6= 0, a,b ∈ Z und m = Primzahl
6.1.3
Hashing mit offener Adressierung
Hier sind die Elemente in der Hashtabelle selbst gespeichert. Möchte man ein neues Element x ∈ S einfügen und ist h(x) schon belegt (Kollision), so prüft man alternative
Positionen h1 (x), h2 (x),... bis man eine leere Position findet.
91
Beispiel: S={0,2,5,24,17,11}, h(x) = (3x) mod 11, hi (x) = (h(x) + i) mod 11
h(0) = 0
h(2) = 6 mod 11 = 6
h(5) = 15 mod 11 = 4
h(24) = 72 mod 11 = 6 (→ Kollision)
h1 (24) = 7
h(17) = 51 mod 11 = 7 (→ Kollision)
h1 (17) = 8
h(11) = 33 mod 11 = 0 (→ Kollision)
h1 (11) = 1
⇒
h(x) x
0
0
1
11
2
3
4
5
5
6
2
7
24
8
17
9
10
11
Strategien bei Kollisionen
• lineare Sondierung: hi (x) = (H(x) + i
mod m
• quadratische Sondierung: hi (x) = (h(x) + i2 ) mod m
etwas allgemeiner: hi (x) = (h(x) + c1 i + c2 i2 ) mod m
Hash-Tabelle formal in Java
Einfügen: wie beschrieben mit linearer Sondierung → h0 (x) = h(x) mod m, hi (x) =
h(x) + i mod m
Suchen: suche Folge der Positionen h0 (x), h1 (x),... ab, bis Element oder leere Position
gefunden
Entfernen: wird das Element gefunden, wird es entfernt und Position deaktiviert (d.h.
speziell markiert ”DELETED”) um zukünftiges Suchen nicht zu behindern.
Die Elemente seien positive ganze Zahlen größer 0!
public class HashTable{
public static final int MAX_M = 101;
private static final int EMPTY = 0;
private static final int DELETED = -1;
private int[] T;
private int m;
public HashTable(){
this(MAX_M);
}
92
public HashTable(int size){
m = size;
T = new int[m];
for(int i = 0; i < m; i++)
T[i] = EMPTY;
}
private int locate(int x) {
int initial = (ax + b) % m;
int i = 0;
while ( (i < m) && (T[(initial + i)%m ] != x)
&& (T(initial + i)%m ] != EMPTY))
i++;
return i;
}
private boolean search(int x){
if(T[locate(x)] == x)
return true;
else
return false;
}
private int locate1(int x){
//analog zu locate, while-Schleife
//stoppt auch wenn T[...] == DELETED
}
public void insert(int x) throws HashTableException {
if (T[locate(x)] == x)
throw new HashTableException(”Element bereits enthalten”);
int i = locate1(x);
if ((T[i] == EMPTY) || (T[i] == DELETED))
T[i] = x;
else
throw new HashTableException(”Tabelle ist voll”);
}
public void delete (int x) throws HashTableException{
int i = locate(x);
if (T[i] == x)
T[i] = DELETED;
else
throw new HashTableException(”Element nicht vorhanden”);
}
}
93
Analyse von universellem Hashing
n
< 1 (bzw. n < m)
Voraussetzung α = m
Annahmen(universelles Hashing):
für jedes Element x ∈ U h0 (x), h1 (x),... eine Permutation von {0,1,...,m} und jede Permutation tritt mit der gleichen Wahrscheinlichkeit auf.
Satz:
Die erwartete Anzahl von Proben in einer Einfüge-Operation ist ≤
Entferne-Operationen stattgefunden haben.
1
1−α
, sofern keine
Bemerkung:
α = 0,5 ⇒ ≤ 2 α = 0,9 ⇒ ≤ 10
Beweis:
Für i = 0,1,2,... sei pi = Prob(genau i Proben greifen auf belegte Felder zu)
⇒ die erwartete Anzahl der Proben ist nicht größer als
P∞
P∞
i=0 i · pi
i=0 (i + 1) pi = 1 +
Weiter sein für i = 1,2,...
qi = Prob(mindestens i Proben greifen auf belegte Felder zu)
P∞
k=i p · k
P∞ P∞
P
Es gilt ∞
k=i p · k
i=1
i=1 qi =
P∞
P∞
= i=1 i · pi = i=0 i · pi (1)
Frage: Wie groß sind die Wahrscheinlichkeiten qi ?
q1 =
n
,
m
da n der n der m Felder schon belegt sind
n n−1
q2 = m
, da im zweiten Versuch eines der restlichen m-1 Felder angesteuert wird und
m−1
davon sind n-1 belegt.
n n−1
n−i+1
qi = m
... m−i+1
, da im i-ten Versuch eines der restlichen m-i+1 Felder angesteuert
m−1
wird, davon sind n-i+1 belegt.
Nun gilt:
n i
) = αi (2)
qi ≤∗ ( m
(∗) gilt, da
n−j
m−j
⇔
⇔
⇔
⇔
n
≤m
(n-j)n ≤ (n-j)m
nm-jn ≤ nm-jm
jn ≤ jm
n≤ m
94
⇒1+
P∞
i=0
P∞
i · pi =(1) 1 +
i=1
qi
P
1
2
≤(2) 1 + ∞
i=1 αi = 1 + α + α + ...
P
1
= ∞
=
i=0 αi
1−α
|{z}
geometrische Reihe(α<1)
Satz (ohne Beweis):
Die Anzahl der Proben bei einer Suche- oder Entferne-Operation, sofern das x ∈ S, ist
1
höchstens α1 ln 1−α
Bemerkung: Es gilt
1
α
1
ln 1−α
≤
1
,
1−α
d.h. ”Einfügen ist langsamer als Entfernen/Suchen”.
Es gilt ln(1 + x) ≤ x, ∀x ≥ 0
1
1−α
=
Es gilt
7
1−α
1−α
1
α
α
1−α
=1+
α
1−α
1
ln 1−α
≤
1
α
α 1−α
=
+
1
.
1−α
Algorithmus auf Graphen
7.1
Grundbegriffe
Ein ungerichteter Graph G = (V,E) ist eine Struktur bestehend aus einer Menge V von
Knoten und einer Menge E von Kanten (zweielementige Teilmenge e = {u,v} von V).
Vor.: v 6= ∅, v endlich
Sprechweisen: (e = {u,v}):
• e ∈ E → verbindet Knoten u,v ∈ V
• u,v sind inzident
• u,v sind adjazent
7.1.1
Grafische Darstellung
d(v3 ) = 3, d(v1 ) = d(v4 ) = 2, d(v5 ) = 1, d(v2 ) = 0
95
7.1.2
Weitere Begriffe
isolierter Knoten: ist mit keinem anderen Knoten verbunden
Grad d(v): Anzahl des Auftretens von v als Endpunkt einer Kante
Teilgraph: G’ = (V’,E’) von G = (V,E), falls V 0 ⊂ V , E 0 ⊂ E
induzierter Teilgraph von G: Teilgraph von G mit Knotenmenge V’ und allen Kanten,
die in G mit Knoten aus V’ auftreten.
→ Bez.: G[V’]
Lemma: Die Anzahl der Knoten mit ungeradem Grad in einem Graphen G = (V,E) ist
gerade
Bew.:
Summiere
P Gradzahlen d(v) über alle Knoten v ∈ V .
P
Bilde v∈V d(v) ⇒ jede Kante wird genau zweimal gezählt ⇒ v∈V d(v) =
2|E|
|{z}
geradeZahl
X
X
d(v) +
v∈V
v∈V
|{z}
|{z}
d(v) gerade
|
⇒
2|E|
|{z}
gerade Zahl
d(v) ungerade
{z
gerade Zahl
P
d(v) =
v∈V,d(v)ungerade
}
d(v) ist ungerade Zahl
⇒ Anzahl der ungeraden Summanden (Knoten mit d(v) ungerade) ist gerade.
Pfad ist eine Folge von Knoten P = (v1 , v2 , ..., vk+1 ), so dass { vj , vj+1 } eine Kante in G
ist ∀1 ≤ j ≤ k.
Ein Pfad ist einfach, wenn vj 6= vk für alle j = k (erlaubt ist aber v1 = vk + 1).
Ein Kreis ist ein Pfad mit v1 = vk + 1.
Graph ist zusammenhängend (zh), falls je zwei Knoten u,v ∈ V durch einen Pfad
verbunden sind.
Ein Graph G ist kreisfrei, falls G keinen einfachen Kreis enthält.
96
Analog definiert man auch gerichtete Graphen (Digraph) D = (V,E):
Jede Kante e ∈ E hat einen Anfangsknoten u ∈ V und einen Endknoten v ∈ V.
Sprechweise: e ist von u nach v gerichtet.
e
Schreibweise: u
v
GGGGA
Innengrad: din (v) = Anzahl der eingehenden Kanten
Außengrad: dout (v) = Anzahl der ausgehenden Kanten
&
·v
%
%
→
& din (v) = 2, dout (v) = 3
Lemma:
X
din (v) =
v∈V
X
dout (v) = |E|
v∈V
Weitere Begriffe wie gerichteter Kreis, Weg, Teilgraph analog. Durch Weglassen der Richtungen entsteht der zugeordnete, ungerichtete Graph. Ein Digraph heißt zusammenhängend, falls der zugeordnete ungerichtete Graph zusammenhängend ist.
Digraph heißt stark zusammenhängend, falls für alle u,v ∈ V es einen gerichteten Pfad
von u nach v und von v nach u gibt.
D stark zusammenhängend : ⇒ D zusammenhänged
7.1.3
Darstellung von Graphen
1) Adjazenzmatrix zu G = (V,E) mit V = { v0 ,...,vn−1 } ist eine (n x n) Matrix
A[n][n] mit(
1 : {vi , vj } ∈ E
A[i][j] =
0 sonst
Platzbedarf: O(|V |2 )
2) Adjazenzlisten: für jeden Knoten v ∈ V wird
Adj(v) = {u ∈ V | {v,u} ∈ E } als Liste abgespeichert.
0
1
2
3
4
5
6
→
→
→
→
→
→
→
1
0
1
0
3
3
nil
→ 3 → nil
→ 2 → nil
→ nil
→ 4 → 5 →nil
→nil
→ nil
0
1
2
3
4
5
6
97
0
1
0
1
0
0
0
0
1
0
1
0
0
0
0
1
0
1
0
0
0
0
0
2
1
0
0
0
1
1
0
3
0
0
0
1
0
0
0
4
0
0
0
1
0
0
0
5
0
0
0
0
0
0
0
6
Sei G = (V,E) ungerichteter Graph.
G heißt Baum ⇐ G ist zusammenhängend und kreisfrei
Lemma: Es sei G = (V,E) ein ungerichteter, kreisfreier Graph mit E 6= ∅ und |V| ≥ 2
⇒ G enthält mindestens 2 Knoten vom Grad 1
Bem.: Knoten vom Grad 1 werden Blätter genannt.
Bew.: E 6= ∅ ⇒ wähle bel. Kante e = {a,b} ∈ E und erweitere e zu einem Pfad durch
hinzufügen von Kanten an die Eckpunkte (solange dies möglich ist).
G kreisfrei ⇒ keine Knotenwiederholung
V endlich ⇒ nach endlich vielen Schritten kann der Pfad nicht mehr verlängert werden
→ die beiden Eckpunkte des Pfades sind die gesuchten Knoten
Lemma:
Es sei G = (V,E) ein ungerichteter Graph mit |V| = n Knoten. Dann sind äquvalent:
(1) G ist ein Baum
(2) G ist kreisfrei und |E| = n-1
(3) G ist zusammenhängend und |E| = n-1
Bew.: durch Ringschluss: (1) ⇒ (2) ⇒ (3) ⇒ (1)
Es sei G = (V,E) ein unger. Graph. Ein spannender Baum von G ist ein Teilgraph von
G, der Baum ist und jeden Knoten v ∈ V enthält.
Beispiel:
98
Satz(ohne Beweis)(Layley)
Die Anzahl aller Bäume mit n Knoten (ist gleich der Anzahl der spannenden Bäume des
vollständigen Graphen Kn ) = nn−2
Spannende Bäume von
7.2
Minimal spannende Bäume
geg.: G = (V,E)ungerichteter, zusammenhängender Graph mit Kantengewichten w: E →
R
ges.: ein minimal spannender Baum T = (VT ,ET ), d.h. ein Teilgraph von G mit
(1) VT = V, ET ⊂ E
(2) der zusammenhängend und kreisfrei ist (→ Baum)
(3) und minimalem Gesamtgewicht
X
w(T )
e∈ET
99
w(e)
Wald = Menge von Bäumen
Idee des Algorithmus (Kruskal):
Wähle nacheinander Kanten mit minimalem Gewicht ohne das ein Kreis entsteht.
Algorithmus in Pseudo-Code:
ET = ∅ //leere Menge
for(int i = 1; i <= n; i++){
v[i] = {i}; //Menge {i}
}
sortiere die Kanten nach den Kanten
e1 ,...,em mit w(e1 )≤w(e2 )≤...≤w(em )
for(int k = 1; k <= m; k++){
bestimme Endpunkte v,w von Kante ek
und Mengen V[i] und V[j], die v und
w enthalten;
if (i != j){
ersetze V[i] und V[j] durch V[i]∪V[j];
ET = ET ∪ {{v,w}};
}
}
Der gesuchte Baum ist T = (V,ET)
Beispiel:
{1}, {2}, {3}, {4}, {5}
e1 = {1,3} → 1 ·——–· 3
·5
V1 = {1,3}, V2 = {2}, V4 = {4}, V5 = {5}
e2 = {2,3}
100
V1 = {1,2,3}, V4 = {4}, V5 = {5}
e3 = {1,2} ⇒ nein!
e4 = {4,5}
V1 = {1,2,3}, V4 = {4,5}
e5 = {3,4}
Bemerkung:
1. Die Mengen V[i] geben die sogenannten Zusammenhangskomponenten des momentanen Graphen mit Kantenlänge ET an.
2. Durch die Zusammenhangskomponente wird implizit getestet, ob durch die betrachtete Kante ek ein Kreis entsteht.
3. Laufzeit O(|E| log |E|)
Korrektheit folgt aus:
Lemma: Es sei G = (V,E) zusammenhängender Graph mit w: E → R, sei (V1 , E1 ),...,(Vk , Ek )
spannender Wald von G mit
V =
K
[
Vi
i=1
S
Ist (V, K
i=1 Ei ) erweiterbar zu einem minimalen spannenden Baum für G und {u,v} eine
Kante mit min. Kosten mit u ∈ Vl und v 6∈ Vl (l ∈ {1,...,k}, dann ist auch
(V,
K
[
Ei ∪ {{u, v}})
i=1
erweiterbar zu minimalem spannenden Baum für G.
101
Bew.: (indirekt)
S
0
Ann.: (V, K
E 0)
i=1 Ei ) sei erweiterbar durch spannenden Baum S = (V, S
mit {u,v} 6∈ E 0 und Kosten (S 0 ) < Kosten jeder Erweiterung von (V, K
i=1 Ei ∪ {{u, v}})
Füge {u,v} in S 0 ein. Dies liefert einen Kreis in S 0 mit einer Kante {u0 , v 0 } 6= {u,v} und
u0 ∈ Vl , v 0 6∈ Vl und w({u,v}) ≤ w({u0 , v 0 }).
Ersetze {u0 , v 0 } durch {u,v} und erhalte so aus S 0 einen Graphen.
⇒ S ist Baum, da der entsprechende Kreis eliminiert ist und da S zusammenhängend ist.
⇒ S ist spannender Baum
Ferner gilt: Kosten(S) ≤ Kosten(S’), ”≤” weil w({u,v} ≤ w({u0 , v 0 })
S
Kosten jeder Erweiterung von (V, i Ei ∪ {{u,v}})
da S eine solche Erw. ist (und insbesondere {u,v} enthält).
Korrektheit: folgt per Induktion über die Anzahl s der Schleifendurchläufe:
nach s Iterationen ist der konstante Wald zu einem minimal spannenden Baum für G
erweitert.
√
s = 0
s y s + 1: verwende Lemma.
Algorithmus von Prim
102
Lemma: Es sei ∅ ∪ U ∪ V, G[u] der von M induzierte Teilgraph von G und Tu ein
spannender Baum von G[u], der in einem min. spann. Baum von G enthalten ist.
Ferner sei e ∈ E eine Kante von U nach V \ U von min. Kosten w(e).
⇒ Tu + {e} ist auch in einem min. spann. Baum von G enthalten.
Bew.: analog
Algorithmus von Prim in Pseudo-Code
ET = ∅; //leere Menge
U = {1};
for(int k = 1; k <= n; k++){
bestimme Kante e = {i,j} mit min
Kosten von i = M und j ∈ V \ U ;
ET = ET ∪ {e};
U = U ∪ {j};
}
Am Ende ist T = (V,ET) ein min. spann. Baum.
Die Korrektheit folgt aus dem obigen Lemma:
1. für u = {u} ist Tu = ({1},∅) ein spann. Baum von G[u] und in einem min. spann.
Baum von G enthalten.
2. in jeder Iteration der for-Schleife wird eine Kante e = {i,j} (i ∈ U, j 6∈ U) zum
momentanen Baum Tu hinzugefügt, die min. Länge hat.
⇒Lemma Tu + {e} ist spann. Bam von G[M ∪ {j}] und in min. spann. Baum von G
enthalten.
Beispiel:
u
u
u
u
u
=
=
=
=
=
{1}
{1,3}
{1,2,3}
{1,2,3,4}
{1,2,3,4,5}
103
Laufzeit:
Schritt ”bestimme Kante mit min. Kosten” ist am aufwendigsten und wird (n-1) mal
aufgerufen.
Zur Bestimmung der kürzesten Kante benötigt man ≤ |E|-1 Vergleiche.
⇒ O(|V| · |E|)
Verbesserung durch Speichern des nächsten Nachbarns:
closest[v] = Knoten u ∈ U mit w({u,v}) ≤ w({u’,v}) f. a. u’ ∈ U,
für v ∈ V \ U mit Adj.(v)\M = ∅
Algorithmus von Prim in Pseudo-Code
ET = ∅; //leere Menge
U = {1};
for(int v = 2; v <= n; v++){
if(v ∈ Adj(1)){
closest[v] = 1;
else
closest[v] = 0; //0 entspr. undef.
}
Sei w({v,0}) = ∞
bestimme Kante e = {i,j} mit min
≤ n-2 Vergleiche
Kosten von i = M und j ∈ V \ U ;
ET = ET ∪ {e};
U = U ∪ {j };
}
for (int k = 1;k<=n-1;k++){
bestimme unter allen Kanten
{v,closest[v]} mit v ∈ V \ U und
≤ n-1 Vergleiche
closest[v] != 0 eine Kante {v0 ,u0 }
min. Länge;
U = U ∪ {v0 };
ET = ET ∪ {{u0 , v0 }};
for (all v ∈ (V \ U ) ∪ Adj(u0 )){
if (w({v,v0 } < w({closest[v]}))
closest[v] = v0 ;
}
}
}
Anm. d. Autors: Keine Garantie für den obigen Algorithmus!!! Fehler bitte an mich!
104
7.3
Tiefen- und Breitensuche (DFS bzw. BFS)
”Depth-f irst-search” und ”Breadth-f irst-search” sind Strategien, die einen Graphen systematisch und jeden Knoten und jede Kante durchlaufen.
(→Basis für komplexe Graphenprobleme)
7.3.1
Grundidee von DFS
geg.: Zusammenhängender, ungerichteter Graph
(1) wähle einen Knoten a und besuche ihn, dann besuche einen zu a adjazenten Knoten
b, dann einen zu b adjazenten Knoten c...
(2) irgendwann erreichen wir einen Knoten, dessen Nachbarn alle besucht sind. Dann
gehe zum vorherigen besuchten Knoten zurück und setze den Algorithmus dort fort.
(3) breche ab, falls Ausgangsknoten a wieder erreicht ist.
7.3.2
Algorithmus in Pseudocode
Hauptprogramm
setze DFSNummer[v] = 0 ∀v ∈ V
und Besucht[e] = 0 ∀e ∈ E, Vater[v] = -1 ∀v ∈ V ;
x = 0;
label = 0;
DFS(x);
Prozedur DFS(v):
VISIT(v);
FOR(each vertex u ∈ Ad(v)){
e = {v,u};
IF(Besucht[e] == 0){
//e nicht besucht
Besucht[e] = 1;
IF(DFSNummer[u] == 0){
//u nicht besucht
Vater[u] = ;
DFS(u);
}
}
}
Prozedur VISIT(v):
label++;
DFSNummer[v] = label;
105
Beispiel:
Die Adjazenzlisten werden in der Reihenfolge der Indizes der Knoten durchlaufen.
Diese Kanten (Vater[v],v) bilden einen Baum.
Definition: D = (V,E) heißt azyklisch, falls keine gerichteten Kreise in D auftreten.
D heißt ein gerichteter Baum, falls zusätzlich gilt:
(a) es existiert genau ein Knoten r (root, Wurzel) mit din (r) = 0
(b) ∀v ∈ V , v 6= r gilt din (v) = 1
Bemerkung: ∀v ∈ V, v 6= r existiert ein gerichteter Pfad von der Wurzel r nach v.
Beispiel:
106
Satz:
Es sei G = (V,E) ungerichteter zusammenhängender Graph. Dann gilt:
(a) T = (V,E 0 ) mit E 0 = {(Vater[v],v) | v ∈ V , v 6= r} ist ein gerichteter Baum, wobei
die Wurzel r = Startknoten der DFS-Suche ist.
(b) Ist v ein Vorgänger von u in T, dann
⇒ DFSNummer[v] <= DFSNummer[u]
(c) Für alle Knoten e = {a,b} ∈ E gilt:
a ist Vorgänger oder Nachfolger von b in T (d.h. es gibt in G keine ”Querkanten”
bzgl. T)
Beweis:
(a) aus dem Algorithmus folgt:
- T ist kreisfrei
- din (r) = 0
- din (v) = 1, ∀v 6= r
(b) folgt ebenso aus der Konstruktion
(c) sei e = {a,b} ∈ E, zeige: a ist Vorgänger oder Nachfolger von b in T
klar, falls e Baumkante ist
sei e also keine Baumkante und o.B.d.A. sei DFSNummer[a] < DFSNummer[b]
⇒(b) a kann kein Nachfolger von b sein
Ann.: b ist kein Nachfolger von a in T.
⇒ es gibt einen gemeinsamen ersten Vorgänger c von a und b in T mit c6=a, c6=b
Da DFSNummer[a] < DFSNummer[b] laufen wir bei DFS(c) zunächst in den Ast zu
Knoten a und Knoten b ist noch nicht besucht worden.
⇒ Kante e = {a,b} ist unbesucht beim Aufruf DFS(a)
⇒ Algorithmus würde versuchen von a nach b zu laufen (und nicht Backtracking
machen)
7.3.3
Laufzeit von DFS
Jede Kante wird 2x durchlaufen und jeder Knoten wird 1x besucht.
⇒ O(|v|+|E|) bei Adjazenzlisten
107
7.3.4
Grundidee von BFS
• wähle einen Startknoten und schreibe ihn in eine (anfangs leere) Warteliste bzw.
Queue
• nehme den ersten Knoten von der Queue; besuche alle seine Nachbarn und schreibe
die Nachbarn an das Ende der Queue
• verfahre entsprechend mit dem jeweils ersten Knoten der Queue bis die Queue abgearbeitet ist
Algorithmus in Pseudo-Code:
for(alle v ∈ V){
BFSNummer[v] = 0;
KnotenBesucht[v] = 0;}
for(alle e ∈ E){ KanteBesucht[e] = 0;}
errichte Queue Q = {r} //r Startknoten
LABEL = 0; KnotenBesucht[r] = 1;
while (Q != ∅){
nehme ersten Knoten v aus der Queue Q und lösche v in Q;
LABEL++;
BFSNummer[v] = LABEL;
for(alle u ∈ Adj(v)){
e = {v,u};
if (KanteBesucht[e] == 0){
KanteBesucht[e] = 1;
if (KanteBesucht[u] == 0){
füge u ans Ende von Q ein;
KnotenBesucht[u] = 1;
}
}
}
}
Q = {r} →Q = {b,c} →Q = {c,d,e,f}→Q = {d,e,f,a}
→Q = {e,f,a} →Q = {f,a} →Q = {a} →...→ →Q = ∅
108
BFS-Baum der Kanten (Vater[v], v)
Für einen gerichteten
Baum mit Wurzel r sei der Level eines Knotens definiert durch
(
0
:v=r
Level[v] =
1 + level[V ater[v]] : sonst
Satz:
1. T = (V,E’) mit E’ = { (Vater[v],v)| v ∈ V, v 6= r } ist gerichteter Baum mit Wurzel
r = Startknoten der BFS-Suche.
2. Ist v ein Vorgänger von u in T ⇒ BFSNummer[v] < BFSNummer[u]
3. Jede Kante e von G verbindet Knoten, deren Level sich um ≤ 1 unterscheiden.
Komplexität: O(|V| + |E|)
7.3.5
Anwendung von DFS/BFS
DFS
Zusammenhang
Test auf Planarität
topolog. Sortierung
109
BFS
kürzeste Wege
Test auf Kreise
Flüsse in Graphen
7.4
Zusammenhangskomponenten
C(v) = { u ∈ V |∃ Weg von u nach v in G } heißt die Zusammenhangskomponente von
v bzgl. G.
Die Mengen C(v) bilden eine disjunkte Zerlegung von V in die Zh.-Komponenten von G.
Graph mit 3 Zh.-Komponenten
Die Zh.-Komponenten lassen sich direkt mit DFS/BFS ermitteln. Beim Abbruch des
DFS/BFS-Algorithmus ist eine Zh.-Komponente ermittelt.
Dann startet man den DFS/BFS-Algorithmus in einem noch nicht besuchten Knoten.
Falls alle Knoten (nach dem ersten Durchlauf) besucht sind, ist G zusammenhängend.
Algorithmus:
wähle einen Startknoten r ∈ V;
Setze DFSNummer[v] = 0 ∀e ∈ E;
und Besucht[e] = 0 ∀e ∈ E;
LABEL = 0;
DFS(r);
while(DFSNummer[v] == 0 für ein v ∈ V){
wähle Knoten v mit DFSNummer[v] = 0;
DFS(v);
}
Komplexität: Es seien (Vi , Ei ) die Zh.-Komponenten von G = (V,E).
Die DFS-Suche für Komponente i benötigt O(|Vi |+|Ei |) = O(|Ei |) (da |Ei | ≥ |Vi |-1).
P
⇒ ki=1 O(|Ei |) = O(|E|) Zeit für alle DFS-Aufrufe.
Suche nach einem unbesuchten Knoten O(|V|) Zeit:
⇒ O(k · |V|) = O(|V |2 ) für alle Suchschritte.
Verbesserung durch Einrichtung eines Pointers, der in einer linearen Anordnung von V
auf den jeweils ersten Knoten v mit DFSNummer[v] = 0 zeigt.
→ Den nächsten unbesuchten Knoten muss man nur ”rechts” von v suchen.
⇒ O(|V|) insgesamt für alle Suchschritte.
⇒ Zh.-Komponenten bestimmbar in O(|V| + |E|).
110
7.5
Kürzeste Wege
geg.: gerichteter Graph D = (V,E), V = {1,...,n}, E ⊂ V x V mit
Länge von (i,j) : (i, j) ∈ E
Kantengewichten 0
:i=j
∞
sonst
Problem: bestimme uj Länge eines kürzesten Weges von 1 nach j (ohne Knotenwiederholungen).
Vor.: D enthalte keine gerichteten Kreise negativer Länge (ZL ≥ 0)
⇒ Bellmann - Gleichungen
u1 = 0
uj = mink6=j (uk + akj )
Unter der ZL ≥ 0 sind die Bedingungen (∗) notwendig für die kürzesten Weglängen, aber
nicht hinreichend. Die Lösung muss nicht eindeutig sein.
u1 = 0
u2 = min(u1 +10, u3 -1)
u3 = u2 +1
d.h.
→ u1 = 0, u2 = 0, u3 = 1 ist Lösung von G-System
→ u1 = 0, u2 = 10, u3 = 11 ist Lösung von Wegeproblem
..
.
Problem sind Kreise mit ZL = 0.
Lemma 1: Ist ZL ≥ 0 und ist j von 1 aus erreichbar, so existiert ein kürzester Weg von
1 nach j ohne Knotenwiederholungen.
Idee für Algorithmus: errechne folgende Werte:
u[j][m] = Länge eines kürzesten Weges von 1 nach j mit höchstens m Kanten
111
Algorithmus (Bellmann-Ford)
for(j = 1;j <= n;j++){
u[j][1] = a1j ;
}
for(k = 2;k <= n-1;k++){
for(j = 1;j <= n;j++){
u[j][k] = minh6=j (u[j][k-1],min(u[u][k-1] + auj ))
}
}
Berechnungsaufwand von u[j][k] für ein j,k:
(n-1)Add + (n-1)Vergleiche
Es gilt u[j][n-1] = uj
Für jedes j speichere das letzte K mit u[j][k] = u[K][k-1] + aKj ab (d.h. den Vorgänger auf einem Weg von 1 nach j).
Sei T die Menge der gemerkten Knoten am Ende des Algorithmus.
u[1][1] = 0, u[2][1] = -1, u[3][1] = 2, u[4][1] = ∞, u[5][1] = ∞
u[1][4] = 0, u[2][4] = -1, u[3][4] = 1, u[4][4] = 2, u[5][4] = 2,
wobei u1 = 0, u2 = u1 + a12 , u3 = u2 + a23 , u4 = u2 + a24 , u5 = u3 + a35
Lemma: Die Menge T bildet einen gerichteten Baum mit Wurzel 1, wobei der Weg von
1 nach j in T ein kürzester Weg im Digraph von 1 nach j ist.
Beweisidee: u[j,m] = Länge eines kürzesten Weges von 1 nach j mit ≤ m Kanten.
(1) uj = u[j][n-1]
(2) zeige, dass u[j][m] korrekt berechnet sind
(per Induktion über Anzahl m der Kanten)
112
Satz: Der Algorithmus von Bellman-Ford liefert unter Bed. ZL ≥ 0 die uj und einen
Baum kürzester Wege in O(|V |3 ) Schritten.
Spezialfall: nur positive Kantengewichte aij > 0
7.5.1
Algorithmus von Dijkstra
Verwende zwei Mengen P,Q mit V = P ∪ Q,
P permanent gelabelt mit uj und Q vorläufig gelabelt mit vj ≥ uj
Vor.: V = {1,...,n}
Alg.:
P = {1}; Q = {2,...,n}; u[1] = 0;
for (int j = 2; j <= n; j++)
u[j] = aij ;
while (P != V) {
Wählle k ∈ Q mit minimalem
v[k] = min{v[j]|j ∈ Q};
P = P ∪ {k};
Q = Q \ {k};
u[k] = v[k];
for (alle j ∈ Q)
v[j] = min(v[j], u[k] + akj
}
}
Beispiel:
u[1] = 0, v[2] = 1, v[3] = 4, v[4] = ∞,
k = 2 u[1] = 0, u[2] = 1, v[3] = 4, v[4]
k = 4 u[1] = 0, u[2] = 1, v[3] = 3, u[4]
k = 3 u[1] = 0, u[2] = 1, u[3] = 3, u[4]
k = 5 u[1] = 0, u[2] = 1, u[3] = 3, u[4]
113
v[5]
= 2,
= 2,
= 2,
= 2,
= ∞
v[5]
v[5]
v[5]
u[5]
=
=
=
=
∞
∞
5
5
Korrektheit des Alg.:
Induktion nach |P|: u[k] ist die Länge des kürzesten Weges von 1 nach k.
√
|P| = 1 , da u[1] = 0
Induktionsschritt: es sei k der gewählte Knoten aus Q.
Ann.: u[k] ist nicht korrekt berechnet.
Es sei W ein kürzester Weg von 1 nach k.
Weiter sei (i,l) die erste Kante auf W, die die Menge P verlässt.
Nach IV ist u[i] korrekt berechnet.
Ausserdem ist das Anfangsstück von W von 1 nach l ein kürzester Weg (ansonsten wäre
W nicht kürzester Weg von 1 nach k.
⇒ v[l] ist korrekt berechnet, als die ausgehenden Kanten von i betrachtet worden sind:
v[l] = u[i] + ail
Also ist v[l] = u[l].
⇒ v[l] = u[l] < kürzeste Weglänge von 1 nach k ≤ u[k] ≤ v[k]
Erhalte aus obiger Ungleichung: v[l] < v[k]
Wid. zur Wahl von k!
Laufzeit:
|Q| - 1 Vergleiche zur Bestimmung von k ∈ Q ≤ 2|Q| Operationen zum Updaten der v[j]
⇒ O(|V |2 ) Laufzeit.
7.5.2
Kürzeste Wege zwischen je zwei Knoten
Vor.: ZL ≥ 0
berechne u[i][j][m] = kürzeste Weglänge von u nach j mit Zwischenknoten ∈ {1,...,m-1}
da ZL ≥ 0 ⇒ u[i][i][m] = 0
114
(
aij
Lemma: u[i][j][1] =
0
i 6= j
sonst
u[i][j][m+1] = min(u[i][j][m],u[i][m][m] + u[m][j][m])
Bew.: ein kürzester Weg von i nach j ohne Zwischenknoten m+1,...,n geht entweder
• nicht über m ⇒ u[i][j][m+1] = u[i][j][m] oder
• über m ⇒ZL≥0 u[i][j][m+1] = u[i][m][m] + u[m][j][m]
ZK ∈ {1,...,m-1}!
wegen ZL ≥ 0 wird m nur 1 x besucht.
Durchführung der Berechnung:
∗ verwende Matrix A = (a[i][j])(i,j)
(
aij
mit a[i][j] =
0
i 6= j
0
und setze u(1) = A
∗ berechne um+1 = (u[i][j][m+1])(i,j) aus u(m) durch Verwendung der m-ten Zeile und
m-ten Spalte:
Vergleiche dazu u[i][j][m] mit u[i][m][m] + u[m][j][m] und speichere den kleineren
Wert in u(m+1) bei Stelle (i,j).
⇒ O(n2 ) Vergleiche und Additionen zur Berechnung von u(m+1) aus u(m)
Falls ZL ≥ 0 : u(m+1) = (uij ) (Länge des kürzesten Weges von i nach j)
Satz: Gilt ZL ≥ 0 so berechnet der Floyd-Warshall-Algorithmus die Matrix M der kürzesten Weglängen in O(|V |3 ) Zeit.
115
0 -1
∞ 0
u(1) = ∞ ∞
-1 ∞
∞ ∞
2
2
0
0
∞
∞
3
∞
0
1
∞
∞
1
∞
0
0
∞
u(2) = ∞
-1
∞
-1
0
∞
-2
∞
2
2
0
0
∞
∞
3
∞
0
1
∞
∞
1
∞
0
0 -1
∞ 0
u(3) = ∞ ∞
-1 -2
∞ ∞
1
2
0
0
∞
2
3
∞
0
1
∞
∞
1
∞
0
0
∞
u(4) = ∞
-1
∞
-1
0
∞
-2
∞
1
2
0
0
∞
2
3
∞
0
1
2
3
1
1
0
0
2
u(5) = ∞
-1
0
1
2
0
0
1
0
2
u(6) = 1
-1
0
-1 1
0 2
0 0
-2 0
-1 1
-1
0
∞
-2
-1
2
3
∞
0
1
2
3
1
1
0
2
3
2
0
1
2
3
1
1
0
Falls ZL < 0, dann sind kürzeste Wege ohne Knotenwiederholungen noch stets definiert.
Satz: Sei Z = {u ∈ V | i liegt auf Kreis negativer Länge}
Daraus ergibt sich:
(a) Z = {i ∈ V|u[i][i][m+1] < 0 im Floyd-Warshall-Alg.}
(b) Z kann in O(n3 ) Schritten brechnet werden.
Bemerkung: Mit Hilfe des Alg. von Bellman-Ford kann Kreis negativer Länge auch
bestimmt werden.
Anm. d. Autors:
Hiermit schloss Professor Jansen die Vorlesung Informatik 2 und läutete damit das Ende
dieser Mitschrift ein.
Es ist jedem freigestellt alle drei Teile dieses Scripts mit in die Klausur zu nehmen, es
beliebig zu vervielfältigen und weiterzugeben.
Für eventuelle Tip-, Sinn- und Rechtschreibfehler übernehme ich keine Verantwortung!
Bei Fragen: tim.dauer@gmx.de
Schöne Semesterferien!!!
116