10. ¨Ubungsblatt zu Algorithmen I im SS 2010

Werbung
Karlsruher Institut für Technologie
Institut für Theoretische Informatik
Prof. Dr. Peter Sanders
G.V. Batz, C. Schulz, J. Speck
10. Übungsblatt zu Algorithmen I im SS 2010
http://algo2.iti.kit.edu/AlgorithmenI.php
{sanders,batz,christian.schulz,speck}@kit.edu
Musterlösungen
Aufgabe 1
(Tiefensuche iterativ, 6 Punkte)
Entwerfen Sie eine nicht rekursive Tiefensuche ausgehend von einem Knoten s und geben Sie Pseudocode an! Die Laufzeit O(m + n) darf nicht überschritten werden.
Musterlösung: Die folgende nicht rekursive Implementierung der Tiefensuche beruht auf der Verwendung eines Stacks. Die Idee ist es, jeweils die Nachbarn des derzeit bearbeiteten Knotens auf einen
Stack zulegen. Man initialisiert den Stack S mit dem Startknoten s. Solange der Stack S nicht leer ist
wird das vorderste Element vom Stack heruntergenommen, als gesichtet markiert und anschließend
alle noch nicht gesichteten Nachbarn vorne auf den Stack gelegt. So ist sichergestellt, das man zuerst
in die Tiefe geht.
1: procedure DFS (NodeId s, Graph G)
2:
Stack S = hsi
3:
visited = hfalse, ..., falsei Array of Boolean
4:
visited[s] = true
5:
while S 6= ∅ do
6:
NodeId u = S.pop()
7:
forall (u, v) ∈ E do
8:
if !visited[v] do
9:
visited[v] = true
10:
S.push(v)
11: return
Aufgabe 2
(Finden von Kreisen in Graphen, 2 + 2 + 2 Punkte)
Mittels einer modifizierten Tiefensuche kann man in O(n) Zeit bestimmen, ob ein ungerichteter
Graph mit n Knoten einen Kreis enthält oder nicht. Gehen Sie davon aus, dass der Graph als Adjazenzfeld mit bigerichteten Kanten dargestellt wird.
a) Geben Sie an, wie Sie die Tiefensuche modifizieren würden, um einen ensprechenden Algorithmus
zu erhalten.
b) Zeigen Sie, dass Ihr Algorithmus tatsächlich das gewünschte Laufzeitverhalten aufweist.
c) Liefert Ihr Algorithmus auch für gerichtete Graphen ein korrektes Ergebnis? Wenn nein,
wie muss der Algorithmus modifiziert werden? Welches asymptotische Laufzeitverhalten hat
der (möglicherweise modifizierte Algorithmus) für gerichteten Graphen schlimmstenfalls? Begründen Sie jeweils kurz.
Musterlösung:
1
a) Im Graph befindet sich genau dann ein Kreis, wenn bei der Tiefensuche traverseNonTreeEdge
(siehe Vorlesungsfolien) aufgerufen wird. In Folge dessen wird traverseNonTreeEdge folgendermaßen instanziiert:
1: traverseNonTreeEdge(u, v : NodeID)
2:
print Graph enthält einen Kreis.“
”
3:
exit
Ansonten wird ganz am Ende des Algorithmus die Anweisung print Graph enthält keinen
”
Kreis.“ eingefügt. Enthält der Graph einen Kreis, wird diese Anweisung natürlich nie erreicht.
b) Immer wenn die Tiefensuche einen noch nicht markierten Knoten betrachtet, wird dieser markiert. D.h., spätestens beim n-ten Knoten trifft die Suche aber auf einen bereits markierten
Knoten und wird abgebrochen. Zudem wird für jeden betrachteten Knoten höchstens eine Kante
betrachtet, also nicht mehr als n Kanten insgesamt.
Das Betrachten eines Knoten braucht aber nur konstante Zeit. Das selbe gilt für Kanten, da
man sich in Adjazenzfeldern die nächste Kante eines Knoten stets in konstanter Zeit besorgen
kann. Beim Backtracking geht man für jeden Knoten höchstens einmal zu seinem Vorgänger
zurück. Für jeden besuchten Knoten wird also auch durch das Backtracking nur konstante Zeit
verursacht. Insgesamt dauert die modifizierte Tiefensuche also nicht mehr als O(n) Zeit.
c) Der Algorithmus liefert für gerichtete Graphen so kein korrektes Ergebnis. In einem DAG kann
es z.B. passieren, dass ein bereits markierter Knoten gefunden wird, obwohl keine gerichteten
Kreise im Graph vorhanden sind. Allerdings kann man dieses Problem beheben, indem man
traverseNonTreeEdge nochmal ein wenig modifiziert:
1: traverseNonTreeEdge(u, v : NodeID)
2:
if (u, v) ist keine backward Kante return
3:
print Graph enthält einen Kreis.“
”
4:
exit
Der Zeitaufwand liegt im schlimmsten Fall nun in Ω(n2 ). Dies tritt z.B. auf, wenn man einen
DAG mit Ω(n2 ) Kanten hat (das gibt es, überlegen), an dem zusätzlich ein gerichteter Kreis
hängt. Im schlimmsten Fall wird der gerichtete Kreis von der Tiefensuche als letztes betrachtet
(dazu hänge der gerichtete Kreis am Startknoten der Suche).
DAG
Startknoten
Aufgabe 3
(Dijkstras Algorithmus, 4 Punkte)
Führen Sie auf folgendem Graphen den Algorithmus von Dijkstra aus, beginnend mit Knoten a. Als
Ergebnis soll in jedem Knoten folgendes stehen: links vom Doppelpunkt die Nummer des Schritts,
in dem der Knoten gescannt wurde; rechts vom Doppelpunkt die Länge des kürzesten Wegs zu a.
Zeichnen Sie außerdem den Baum der kürzesten Wege von a aus ein.
Sie können direkt in dieses Blatt einzeichnen.
2
b
3
:
5
f
:
:
3
7
e
a
:
:
7
15
d
2
c
g
:
1
2
23
9
:
5
8
1
3
h
i
:
:
1
13
j
:
Musterlösung:
b
c
7:13
7
3
7
e
f
g
2:1
1
1:0
6:11
2
9
Aufgabe 4
4:8
5:10
5
15
a
d
2
3
23
10:16
5
8
1
3
h
i
1
j
3:3
9:15
13
8:14
(Kürzeste Pfade, 2 + 2 + 2 + 2 Punkte)
a) Geben Sie einen gewichteten gerichteten Graph mit positiven Kantengewichten an, der einen
Knoten v ∗ enthält, dessen vorläufige Distanz von Dijkstras Algorithmus mindestens dreimal
mittles decreaseKey verringert wird. Geben Sie zusätzlich zum Graph auch den Knoten v ∗ und
den Startknoten einer entsprechenden Dijkstra-Suche an.
b) Zeigen Sie: In gewichteten gerichteten Graphen sind Teilpfade von kürzesten Pfade wiederum
kürzeste Pfade.
Ein gerichteter Graph heiße ein gerichteter Baum mit Wurzel r, wenn alle seine Knoten vom
Knoten r aus erreichbar sind und der Eingangsgrad aller seiner Knoten ≤ 1 ist.
Behauptung: Sei G = (V, E) ein gewichteter gerichteter Graph ohne negative Kreise, in dem von
einem Knoten s aus alle anderen Knoten erreichbar sind. Dann gibt es einen Teilgraph T von G, der
3
|V | Knoten hat, ein gerichteter Baum mit Wurzel s ist und dessen Pfade alle kürzeste Pfade in G sind.
c) Zeigen Sie die Behauptung für den Fall, dass in G alle kürzesten Pfade eindeutig sind. Hinweis:
Betrachten Sie den Teilgraph T0 , der aus allen kürzesten Pfaden besteht, die an s beginnen.
d) Zeigen Sie die Behauptung auch für den Fall, dass kürzeste Pfade nicht immer eindeutig sind.
Hinweis: Erweitern Sie das Ergebnis aus Teilaufgabe c).
Musterlösung:
a) Der Startknoten der Dijkstra-Suche ist s, der Knoten, dessen vorläufiges Gewicht mindestens
dreimal mit decreaseKey verringert wird, ist (wie in Aufgabenstellung bezeichnet) v ∗ :
3
5
8
12
6
s
v*
20
10
2
3
b) Sei P = hu, . . . , x, . . . , y, . . . , vi ein kürzester Pfad von u nach v und Q := hx, . . . , yi ein Teilpfad.
Annahme zum Widerspruch: Q ist kein kürzester Pfad von x nach y. Da ein Pfad Q existiert
und somit der Fall, dass es keinen Pfad gibt ausgeschlossen wird, gibt es also einen kürzeren Pfad
Q0 = hx, . . . , yi. Ersetze in P den Teilpfad Q durch Q0 , das ergibt einen gültigen Pfad von u
nach v der kürzer ist als P . Dies ist ein Widerspruch, da P kürzester Pfad.
c) Offensichtlich ist T0 ist ein Teilgraph mit |V | Knoten.
Angenommen, es gibt u 6= s mit Eingangsgrad ≥ 2 in T0 . Also gibt es zwei Kanten (x, u) und
(y, u) in T0 mit x 6= y. Da T0 Vereinigung von kürzesten Pfaden ist, die bei s beginnen, gibt es
zwei kürzeste Pfade P = hs, . . . , x, u, . . . , vi und Q = hs, . . . , y, u, . . . , wi in T0 . Nach Teilaufgabe
b) sind hs, . . . , x, ui und hs, . . . , y, ui zwei kürzeste Pfade. Da sie verschieden sind, ist das eine
Widerspruch zur Annahme, dass kürzeste Pfade eindeutig sind. Also ist T0 ein gerichteter Baum
mit Wurzel s.
Angenommen, es gibt einen Pfad hu, . . . , vi in T0 , der nicht kürzester Pfad in G ist. Da es in einem gerichteten Baum genau einen Pfad von der Wurzel zu einem Knoten gibt (andernfalls käme
ein Eingangsgrad ≥ 2 vor), muss es in T0 einen Pfad hs, . . . , ui geben, so dass hs, . . . , u, . . . , vi
der eindeutige Pfad von s nach v ist in T0 . Da T0 nach Definition nur aus kürzesten Pfaden in
G besteht, ist hs, . . . , u, . . . , vi kürzester Pfad in G und somit – nach Teilaufgabe b) – hu, . . . , vi
kürzester Pfad von u nach v in G. Dies ist ein Widerspruch. D.h. wenn alle kürzesten Pfade
in G eindeutig sind, dann sind alle Pfade in T0 kürzeste Pfade in G.
d) Ein Graph ohne negative Kreise hat eindeutige kürzeste Distanzen. Da s jeden Knoten erreichen
kann, gibt es eine eindeutige kürzeste Distanz µs (v) zu jedem Knoten v. Konstruiere einen
Graphen G0 aus G der eindeutige kürzeste Pfade hat und µs (·) nicht verändert: Solange es in G0
noch zwei verschiedene kürzeste Pfade P = hs, . . . , ui und Q = hs, . . . , ui gibt, gibt es auch noch
zwei kürzeste Pfade P 0 = hs, . . . , x, u0 i und Q0 = hs, . . . , y, u0 i mit x 6= y. Entferne iterativ (y, u0 )
aus G0 bis die kürzesten Pfad eindeutig sind.
Dass sich µs (·) wirklich nicht ändert, überlegt man sich so: Beim entfernen von Kanten werdem
kürzeste Distanzen allenfalls größer. Betrachte den kürzesten Pfad hs, . . . , y, u0 , . . . , vi von s nach
v mit Präfix Q0 . Dann ist hs, . . . , x, u0 , . . . , vi mit Präfix P 0 ein Pfad gleicher Länge und folglich
ebenfalls kürzester Pfad von s nach v. Nach Entfernen von (y, u0 ), was den Pfad Q0 zerstört, gibt
4
es also immer noch einen Pfad von s nach v der Länge µs (v). Es ist µs (v) aber immer noch eine
kürzeste Distanz, da alle kürzesten Distanzen allenfalls größer geworden wären.
Wir haben aber in c) schon gezeigt, dass bei eindeutigen kürzesten Pfaden ein entsprechender
Baum exisiert.
Zusatzaufgabe 1
(Speichereffiziente Breitensuche, 3 Punkte)
Gegeben sei ein gerichteter, stark zusammenhängender Graph in folgender Darstellung:
• Ein Knotenarray, das zu jedem Knoten v einen Eintrag mit seiner ID und einen Zeiger auf ein
Array mit den von v ausgehenden Kanten enthält. Die Knoten haben eindeutige IDs.
• Das Kantenarray mit den ausgehenden Kanten von v enthält für jede Kante e einen Eintrag mit
ihrer ID und der ID des Zielknotens der Kante.
ID . . .
s
ID ZielID
..
.
Auf diesem Graphen soll nun eine BFS ausgeführt werden, wobei zu jedem Zeitpunkt nur O(1) zusätzlicher Speicher verwendet werden soll. Die Laufzeit darf dabei schlechter als bei der üblichen BFS sein.
Die Art der Darstellung des Graphen soll während der BFS erhalten bleiben, es ist jedoch erlaubt z.B.
die Knoten im Knotenarray zu permutieren.
Geben Sie eine Pseudocode-Implementierung der BFS an die diese Bedingungen erfüllt und begründen
Sie warum diese Implementierung nur O(1) zusätzlichen Speicher benötigt. Es soll dabei für jeden
Knoten eine unbekannte Funktion f aufgerufen werden, die als Eingabe die Knoten-ID und die Ebene
(Entfernung zum Startknoten) des Knotens hat. Es kann angenommen werden, dass f während der
Ausführung O(1) und nach der Ausführung keinen Speicher benötigt.
Musterlösung:
1: procedure BFS (NodeArray[1 . . . n] nodes, NodeID start)
2:
Finde i mit nodes[i].ID = start
3:
Vertausche nodes[i] mit nodes[1]
4:
Setze q1 := 1, q2 := 2, ebene := 0 und u := 2
5:
while q1 ≤ n do
6:
while q1 < q2 do
7:
call f (nodes[q1 ].ID, ebene)
8:
forall Kanten k im Kantenarray von nodes[q1 ] do
9:
Suche i ∈ {u, . . . , n} mit nodes[i].ID = ZielID
10:
If i gefunden do
11:
Vertausche nodes[i] mit nodes[u]
12:
u++
13:
q1 + +
14:
q2 := u, ebene + +
15: return
Die Implementierung benötigt nur zusätzlichen Platz für die Indizies q1 , q2 , i und u und für die Ebenennummer und den Index im Kantenarray des gerade bearbeiteten Knotens. Der Platzbedarf für
diese 6 Zahlen ist nach Definition konstant. Zudem wird noch ein Speicherplatz für einen Knoten zum
Vertauschen benötigt.
5
Die Implementierung bringt alle Knoten immer im Array nodes unter ohne dessen Größe zu ändern.
Im Bereich 1, . . . , q1 − 1 liegen die schon bearbeiteten Knoten, im Bereich q1 , . . . , q2 − 1 liegt die Queue
der in dieser Ebene noch zu bearbeitenden Knoten, im Bereich q2 , . . . , u − 1 liegt die Queue der in der
nächsten Ebene zu bearbeitenden Knoten und im Bereich u, . . . , n liegen die noch nicht gefundenen
Knoten.
Zusatzaufgabe 2
(Organisatorisches, 1 Punkt)
Aus organisatorischen Gründen benötigen wir von Ihnen ein paar Informationen. Schreiben Sie bitte
Ihren Studiengang (Informatik, Mathematik, ...) und ihre Studienart (Bachelor, Master, Lehramt,...)
auf die erste Seite.
Musterlösung:
6
Herunterladen