Musterlösungen zu Datenstrukturen und Algorithmen SS 2005 Blatt

Werbung
Prof. Dr. Johannes Blömer
Paderborn, den 2. August 2005
Musterlösungen zu
Datenstrukturen und Algorithmen
SS 2005
Blatt 2, Aufgabe 3
Wir schreiben zunächst alle Funktionen zur Basis 2. Dann erhalten wir
√ log(n)
2
3n log(n) = 2log(3)n log(n) ,
n
= 2log(n) /2 ,
n10 = 210 log(n) ,
5n/2 = 2log(5)n/2 .
Benutzten wir nun die Sätze 2.10 und 2.11 aus der Vorlesung so sehen wir, dass von den
Funktionen im Exponenten log(3)n log(n) am schnellsten wächst. Dann kommt log(5)n/2,
dann log(n)2 /2 und schließlich 10 log(n). Damit wächst von den ursprünglichen Funktionen
√ log(n)
3n log(n) am schnellsten. Gefolgt von 5n/2 , n
und n10 , in dieser Reihenfolge.
Blatt 2, Aufgabe 4.2
Diese Aussage ist falsch. Um dieses einzusehen, betrachten wir das Beispiel f (n) = n und
g(n) = n/2. Damit gilt dann f (n) = O(g(n)). Würden nun c, n0 ∈ N existieren, so dass
2f (n) ≤ c2g(n) für alle n ≥ n0 , so würde hieraus folgen, dass 2n/2 ≤ c für alle n ≥ n0 . Dieses
trifft sicherlich nicht zu.
Blatt 3, Aufgabe 1(c)
Hier sind zunächst die beiden Invarianten.
Invariante Zeilen 3-5: Vor Durchlauf der Schleife mit Index j (und äußerem Index i) ist
A[k] ein Minimum der im Teilarray A[i..j − 1] gespeicherten Zahlen.
Invariante Zeilen 1-6: Vor Durchlauf der Schleife mit Index i stehen in A[1..i − 1] die
i − 1-kleinsten Eingabezahlen in aufsteigend sortierter Reihenfolge. Die restlichen Eingabezahlen stehen in A[i..length[A]].
Für beide Invarianten zeigen wir Initialisierung, Erhaltung und Terminierung. Dabei werden
wir die Terminierung der Invariante für die Zeilen 3-5 benutzen, um die Erhaltung der Invariante in Zeilen 1-6 zu zeigen. Die Terminierung der Invariante in Zeilen 1-6 wird dann die
Korrektheit des Algorithmus Selection-Sort beweisen.
Initialisierung Invariante Zeilen 3-5: Vor dem ersten Durchlauf mit Index j = i + 1
ist k = i. Weiter ist A[i] sicherlich das Minimum der im Teilarray A[i..i] gespeicherten Zahlen.
Erhaltung Invariante Zeilen 3-5: Angenommen die Invariante gilt vor Durchlauf der
Schleife mit Index j. Dann ist also A[k] das Minimum der im Teilarray A[i..j − 1] gespeicherten Zahlen. Nun wird aber in den Zeilen 4,5 dieses Minimum mit dem nächsten Element
A[j] verglichen und im Fall A[j] < A[k] der Index k durch j ersetzt. Damit ist sicher gestellt,
dass vor Durchlauf der Schleife mit Index j + 1 in A[k] das Minimum der Zahlen im Teilarray
A[i..j] gespeichert ist.
Terminierung Invariante Zeilen 3-5: Am Ende der Schleife ist in A[k] das Minimum
der Zahlen im Teilarray A[i..length(A)] gespeichert.
Nun zur Invariante für Schleife in den Zeilen 1-6.
Initialisierung Invariante Zeilen 1-6: Der erste Index ist i = 1. Vor dem Durchlauf
mit diesem Index ist das Array A[1..i − 1] leer. Die Aussage der Invariante ist damit korrekt.
Erhaltung Invariante Zeilen 1-6: Angenommen die Invariante gilt vor Durchlauf der
Schleife mit Index i. Damit stehen in A[1..i − 1] die i − 1-kleinsten Eingabezahlen in aufsteigend sortierter Reihenfolge. Die restlichen Eingabezahlen stehen in A[i..length[A]]. Nun
wissen wir aber aus der Terminierung der Schleife in den Zeilen 3-5, dass in A[k] nun das
Minimum der Zahlen im Array A[i..length[A]] gespeichert ist. Dieses wird nun in Zeile 6 mit
dem Eintrag in Position i getauscht. Damit enthält vor Durchlauf mit Index i + 1 das Array
A[1..i] die i-kleinsten Eingabezahlen in aufsteigend sortierter Reihenfolge. Außerdem stehen
die restlichen Eingabezahlen im Teilarray A[i + 1..length(A)].
Terminierung Invariante Zeilen 1-6: Vor Durchlauf der Schleife mit Index length(A)+1
stehen nun die Eingabezahlen im Array A[1..length(A)], also des ganzen Arrays, aufsteigend
sortiert in A[1..length(A)]. Damit ist Selection-Sort ein korrekter Sortieralgorithmus.
Blatt 4, Aufgabe 3(a)
Wie in der Aufgabe gefordert, führen wir einen Induktionsbeweis. Für n = 1, 2 folgt die
Korrektheit des Algorithmus sofort aus den Zeilen 1-4, insbesondere Zeilen 1 und 2. Dies
ist der Induktionsanfang. Nun zum Induktionsschluss. Die Induktionsvoraussetzung ist, dass
Stooge-Sort für alle Eingabegrößen m < n korrekt arbeitet. Nun wollen wir zeigen, dass
Stooge-Sort auch für Eingabegröße n korrekt ist.
Die Menge Ij , j = 1, 2, 3 enthalte nun alle Eingabezahlen, die bei einer korrekten Sortierung
im j-ten Drittel der sortierten Folge stehen. Wir zeigen für j = 1, 2, 3, dass Stooge-Sort die
Zahlen in Ij an die richtige Position bringt. Dabei können wir nach Induktionsvoraussetzung
annehmen, dass jeder der rekursiven Aufrufe von Stooge-Sort in den Zeilen 6-8 eine korrekte
Sortierung der entsprechenden Teilfolge liefert.
Sie zunächst j = 1. Alle Elemente aus I1 die zu Beginn im ersten oder zweiten Drittel von A
stehen werden durch den Aufruf von Stooge-Sort in Zeile 6 in das erste Drittel verschoben.
Durch den Aufruf von Stooge-Sort in Zeile 7 werden alle Elemente aus I1 , die zu Beginn
im letzten Drittel von A standen in das zweite Drittel verschoben. Schließlich werden diese
Elemente dann durch den letzten Aufruf von Stooge-Sort in Zeile 8 in das erste Drittel
verschoben. Außerdem sind beim Aufruf von Stooge-Sort in Zeile 8 alle Elemente aus I1 im
ersten oder zweiten Drittel. Damit wird Stooge-Sort in Zeile 8 die Elemente aus I1 nicht nur
alle ins erste Drittel verschieben, sondern die Elemente aus I1 werden auch in der richtigen
Reihenfolge im ersten Drittel stehen.
Für j = 2, 3 argumentiert man ähnlich. So stehen nach dem ersten Aufruf von Stooge-Sort
in Zeile 6, alle Element aus I3 im zweiten oder dritten Drittel. Diese werden dann durch den
Aufruf von Stooge-Sort in Zeile 7 richtig sortiert in das letzte Drittel des Arrays A geschoben.
Weiter stehen nach den ersten beiden Aufrufen von Stooge-Sort alle Elemente aus I2 in den
ersten beiden Dritteln. Wie bei den Elementen aus I1 werden diese Elemente dann durch
den Aufruf von Stooge-Sort in Zeile 8 richtig sortiert in das zweite Drittel des Arrays A
geschoben.
Blatt 4, Aufgabe 4
Es gilt T (n) ≤ 3T (n/2) + n2 für n > 1. Gemäß der Rekursionsgleichung können wir T (n/2)
abschätzen durch 3T (n/4) + (n/2)2 . Damit erhalten wir
T (n) ≤ 3(3T (n/4) + n2 /4) + n2 = 9T (n/4) + 3n2 /4 + n2 .
Nun können wir T (n/4) abschätzen durch 3T (n/8) + (n/4)2 und erhalten
T (n) ≤ 9(3T (n/8) + n2 /16) + n2 /4n2 = 27T (n/8) + 9n2 /16 + 3n2 /4 + n2 .
Fahren wir so fort, erhalten wir für allgemeines i die Abschätzung
i
i
T (n) ≤ 3 T (n/2 ) + n
2
i−1
X
(3/4)j .
j=0
Nun nehmen wir zur Vereinfachung an, dass n eine Zweierpotenz ist. Dann ist log(n) eine
natürliche Zahl. Setzen wir dann in der Abschätzung oben i = log(n), so erhalten wir
X
log(n)−1
T (n) ≤ 3
log(n)
T (1) + n
2
(3/4)j .
j=0
Nun gilt T (1) = c. Außerdem gilt
X
log(n)−1
(3/4)j =
j=0
1 − (3/4)log(n)
≤ 4,
1/4
da es sich um eine geometrische Summe handelt. Da 3log(n) = nlog(3) < n2 folgt
T (n) ≤ cnlog(3) + 4n2 = O(n2 ).
Blatt 5, Aufgabe 2
Wir zeigen durch vollständige Induktion, dass für jedes beliebige a ≥ c die Abschätzung
T (n) ≤ an3 gilt. Für n = 1 ist Nichts zu zeigen.
Nun zum Induktionsschluss. Induktionsvoraussetzung ist, dass T (k) ≤ ak 3 für alle k < n
gilt. Wir wollen zeigen, dass T (n) ≤ an3 für n ≥ 2. Wir betrachten nun für 1 ≤ k ≤ n − 1
den Ausdruck T (k − 1) + T (n − k) + cn2 . sowohl auf T (k − 1) als auch auf T (n − k) können
wir unsere Induktionsvoraussetzung anwenden. damit erhalten wir
T (k − 1) + T (n − k) + n2 ≤ a(k − 1)3 + a(n − k)3 + cn2 .
Nun benutzen wir, dass für alle s, t > 1 gilt (s + t)3 ≥ s3 + t3 . Damit ist
a(k − 1)3 + a(n − k)3 + cn2 ≤ a(n − 1)3 + cn2 .
Nun gilt für n ≥ 2
a(n − 1)3 + cn2 =
=
≤
≤
an3 − 3an2 + 3an − a + cn2
an3 − an2 − 2an2 + 3an − a + cn2
an3 − 4an + 3an
an3 .
Wobei wir n ≥ 2 und a ≥ c im Übergang von der 2. zur 3. Zeile ausgenutzt haben. Damit
gilt
T (n) ≤ max {T (k − 1) + T (n − k) + cn2 } ≤ an3 .
1≤k≤n−1
Damit ist T (n) = O(n3 ).
Blatt 5, Aufgabe 4
Wir betrachten den folgenden Algorithmus General-Median, der den Algorithmus Median als
Unteralgorithmus benutzt.
General-Median(A, i):
1. Berechne mit Hilfe von Median den Median A[z] des Arrays A.
2. Partitioniere die Elemente des Arrays A um A[z] herum.
3. Falls i = b n+1
c, Ausgabe A[z]. Falls i < b n+1
c, finde rekursiv den i-ten Median in den
2
2
Einträgen < A[z]. Falls i > b n+1
c
finde
rekursiv
den (i − b n+1
c)-ten Median in den
2
2
Einträgen > A[z].
Für den zweiten Schritt nehmen wir hierbei den Algorithmus Partition, den wir bei Quicksort
kennen gelernt haben.
Zunächst zeigen wir die Korrektheit von General-Median. Dabei gehen wir natürlich davon
aus, dass der Algorithmus Median korrekt arbeitet. Damit ist das im ersten Schritt berechnete Element A[z] der Median des Eingabearrays A. Wir zeigen die Korrektheit nun mit
vollständiger Induktion über die Länge n des Arrays. Ist n = 1, so wird im 3. Schritt immer
A[1] ausgegeben. Dies ist für n, i = 1 auch die korrekte Antwort.
Sei nun n ≥ 2. Als Induktionsvoraussetzung haben wir, dass für alle m < n und alle i, 1 ≤
i ≤ m der Algorithmus General-Median korrekt ist. Wir wollen zeigen, dass dann GeneralMedian für alle Arrays der Größe n und alle 1 ≤ i ≤ n ebenfalls korrekt ist. Die Korrektheit
für diese Fälle folgt nun aus den folgenden Beobachtungen:
• Ist i = b n+1
c, so folgt die Korrektheit unmittelbar aus der Korrektheit von Median und
2
der ersten Abfrage im 3. Schritt.
c, so ist der i-te Median von A der i-te Median des im 2. Schritt umsortierten
• Ist i < b n+1
2
Teilarrays A[1..b n+1
c]. Dies folgt unmittelbar aus der Definition des i-ten Medians. In
2
diesem Fall folgt die Korrektheit von General-Median aus der Korrektheit von Median,
der zweiten Abfrage im 3. Schritt und der Induktionsvoraussetzung.
• Ist i > b n+1
c, so ist der i-te Median von A der i − b n+1
c-te Median des im 2. Schritt
2
2
n+1
umsortierten Teilarrays A[b 2 c+1..n]. Dieses gilt, denn alle Elemente im umsortierten
Teilarray A[1..b n+1
c] sind dann auf jeden Fall kleiner als dieser Median. In diesem Fall
2
folgt die Korrektheit von General-Median wiederum aus der Korrektheit von Median,
der zweiten Abfrage im 3. Schritt und der Induktionsvoraussetzung.
Nun noch die Laufzeitanalyse. Sei cn eine obere Schranke für die Laufzeit von Median bei
einem Array der Länge n. Auch für Partition aus dem Abschnitt über Quicksort wissen wir,
dass es lineare Laufzeit besitzt. Wir nehmen an, dass cn auch eine obere Schranke für die
Laufzeit von Partition bei einem Array der Größe n ist. Dann gilt für die Laufzeit T (n) von
General-Median die folgende Rekursionsformel
T (n) ≤ T (n/2) + 2cn.
Dabei haben wir zur Vereinfachung angenommen, dass n durch 2 teilbar ist. Die Formel gilt,
c] − 1] oder
da im 3. Schritt der Algorithmus rekursiv entweder mit dem Array A[1..b n+1
2
mir dem Array A[b n+1
c]
+
1..n]
aufgerufen
wird.
Beide
Arrays
habe
Größe
höchstens
n/2.
2
Schließlich gilt T (1) = a für eine Konstante a. Damit erhalten wir für T (n) folgende Kette
von Ungleichungen (wir nehmen zur Vereinfachung an, dass n eine Zweierpotenz ist):
T (n) ≤ T (n/2) + 2cn
≤ T (n/4) + cn + 2cn
≤ T (n/8) + c/2n + cn + 2cn
..
.
X
log(n)−1
≤ T (1) + 2cn
(1/2)i .
i=0
Nun ist die Summe in der letzten Abschätzung eine geometrische Summe, die durch 2 beschränkt ist. Außerdem war T (1) ≤ a für eine Konstante a. Damit ist T (n) = O(n).
Blatt 6, Aufgabe 2
a) Für den Algorithmus k-Merge benutzen wir einen min-Heap H, der immer genau k
Elemente speichert. Jedes der sortierten Teilarrays Tj , j = 1, . . . , k, erweitern wir um
eine Dummyelement mit Wert ∞. Der min-Heap enthält zu jedem Zeitpunkt genau ein
Element aus jedem der Teilarrays. Zu jedem Teilarray speichern wir noch einen Index
nj , der zu Beginn für alle Teilarrays mit 1 initialisiert wird. Zu jedem Element im
min-Heap H speichern wir jeweils noch den Index j des Arrays, aus dem das Element
stammt. Initialisiert wird der min-Heap mit den ersten Elementen der Teilarrays. Dann
wiederholen wir folgenden Schritte für i = 1, . . . , n.
1. Kopiere das minimale Element H[1] des min-Heaps H in A[i].
2. Stammt H[1] aus dem Teilarray Tj und ist nj = l, so ersetze H[1] durch Tj [l + 1]
und erhöhe nj um 1.
3. Benutze min-Heapify(H, 1), um aus H wieder einen min-Heap zu machen.
Dabei ist min-Heapify das Analogon von max-Heapify für min-Heaps.
Mit diesem Algorithmus wird jeweils das i-kleinste Element der Eingabezahlen gefunden. Damit löst der so beschriebene Algorithmus unser Merge-Problem für k Teilarrays.
Für jedes i müssen dabei zwei Elemente kopiert werden. Außerdem muss einmal minHeapify aufgerufen werden. Da der Heap immer k Element besitzt, kostet jeder Aufruf
von min-Heapify Zeit O(log(k)) (wie bei max-Heapify). Da außerdem die Initailisierung
in Zeit O(k) erfolgen kann (durch Build-Min-Heap wie bei Build-Max-Heap), ist die
Gesamtlaufzeit des oben beschriebenen Algorithmus O(log(k)n).
b) Für die Laufzeit T (k, n) von k-Merge-Sort bei einem Array der Größe n erhalten wird
die folgende Rekursion
T (k, n) ≤ kT (k, n/k) + c log(k)n.
Dabei ist c eine Konstante, groß genug, um die Laufzeit von k-Merge durch c log(k)n
abschätzen zu können. Außerdem gilt T (k, 1) ≤ a, für eine Konstante a.
Lösen wir nun die Rekursionsgleichung auf unsere übliche Art und nehmen wir zur
Vereinfachung an, dass n eine Potenz von k ist, erhalten wir
T (k, n) ≤ k logk (n) T (k, 1) + c log(k)n(logk (n) − 1) ≤ an + cn log(n).
Wir erhalten als Abschätzung für die Laufzeit T (n) = O(n log(n)).
Blatt 7, Aufgabe 2
Wir zeigen durch einen Widerspruchsbeweis, dass es einen solchen Algorithmus A nicht geben
kann. Der Widerspruch wird dabei zur unteren Schranke von Ω(n log(n)) für Vergleichssortierer sein.
Angenommen wir haben ein zu sortierendes Array B der Größe n. Um dieses mit Hilfe des
angenommenen Algorithmus A zu sortieren, gehen wir folgendermaßen vor.
1. Mit Build-Max-Heap errichte auf B einen max-Heap.
2. Lege ein Hilfsarray C der Größe n an.
3. Für i = n, . . . , 1 wiederhole folgende Aktion. Wende auf B den Algorithmus A an und
schreibe das entfernte Element in C[i].
4. Kopiere C nach A.
Nach Annahme über A ist dieses ein korrekter Sortieralgorithmus. Er ist sogar ein Vergleichssortierer, da auch Build-Max-Heap nur die für einen Vergleichssortierer zulässigen Operationen benutzt. Schauen wir uns nun die Laufzeit dieses Vergleichssortierers an. Build-Max-Heap
benötigt wie in der Vorlesung gezeigt Zeit O(n). Das Anlegen von C und das abschließende
kopieren von C nach A benötigen ebenfalls Zeit O(n). Schließlich benötigen wir nach Annahmen über die Laufzeit von A auch für die Schleife im 3. Schritt des Algorithmus insgesamt
nur Zeit O(n), für jedes i nämlich Zeit O(1). Damit haben wir mit Hilfe von A einen Vergleichssortierer mit Laufzeit O(n) konstruiert. Da es einen solchen Algorithmus aufgrund der
unteren Schranke von Ω(n log(n)) für die Laufzeit von Vergleichssortierern nicht gibt, kann
ein Algorithmus A mit den in der Aufgabe beschriebenen Eigenschaften nicht existieren.
Blatt 8, Aufgabe 1
Man kann z.B. die folgende Lösung verwenden. Ein leerer Stack S wird als eine leeren Queue
Q initialisiert. Eine Push-Operation des Stacks wird an ein Enqueue-Operation der Queue
delegiert, d.h.:
push(S, x):
1 enqueue(Q, x)
Da die Queue-Operationen Laufzeit O(1) haben, hat somit auch push Laufzeit O(1). Die Delegation der Pop-Operation an die Queue benötigt dann aber einen größeren Aufwand. Dazu
kann zunächst ein spezielles Dummy-Element in die Queue eingefügt werden. Anschliessend
werden so lange die Elemente der Queue entnommen und und direkt wieder eingefügt, bis
das Dummy-Element am Kopf der Schlange auftaucht. Das zuletzt entnommene Element
wird dann nicht wieder eingefügt, sondern ausgegeben.
pop(S):
1 if empty(Q) then return Nil
2 enqueue(Q, Dummy)
3 x ← dequeue(Q)
4 while front(Q) 6= Dummy
5
do enqueue(Q, x)
6
x ← dequeue(Q)
7 dequeue(Q)
8 return x
Da jedes der n Elemente maximal einmal entnommen und eingefügt wird, ergibt sich eine
Laufzeit von O(n). Selbstverständlich kann der Stack auch analog zum oben beschriebenen
implementiert werden, so dass push Laufzeit O(n) und pop Laufzeit O(1) hat.
Blatt 9, Aufgabe 3
Wir beweisen die Aussage durch einen Widerspruchsbeweis. Wir nehmen also an, dass es
keine Teilmenge K des Universums der Größe mindestens n/m gibt, die alle auf denselben
Hashwert abgebildet werden. Dieses bedeutet, dass es zu jedem der m möglichen Hashwerte
höchstens n/m − 1 viele Urbilder im Universum gibt. Damit gibt es dann aber höchstens
m(n/m − 1) < n Elemente im Universum. Dieses ist es ein Widerspruch zur Voraussetzung,
dass U Größe n besitzt.
Blatt 10, Aufgabe 2
Als Datenstruktur bietet sich ein Rot-Schwarz-Baum oder ein ähnlicher balancierter binärer
Suchbaum mit Höhe O(log n) an. Die Operationen Insert und Delete werden bereits wie
in der Vorlesung definiert in Laufzeit O(log n) unterstützt.
Als zusätzliches Satellitendatum wird zu jedem Knoten ein Wert left-size[x] gespeichert. Dieser Wert soll stets die Anzahl der Knoten im linken Teilbaum von x enthalten. Dazu werden
neue Knoten — welche ja stets Blätter sind — mit dem Wert left-size[x] = 0 initialisiert.
Da Änderungen von left-size nur die Knoten auf dem Suchpfad zur Einfüge- bzw. Löschposition betreffen, kann die Aktualisierung mit konstantem Aufwand in jedem der O(log n)
Pfadknoten in die Operationen Insert und Delete eingebaut werden. Die Modifikationen
ändern die asymptotische Laufzeit von Insert und Delete also nicht. Auch die Rotation
des Rot-Schwarz-Baums, die in RB-Insert-Fixup auftaucht, lässt sich leicht so abändern,
dass left-size erhalten bleibt und sich die Laufzeit von RB-Insert-Fixup asymptotisch nicht
ändert.
Schließlich muss noch Count in Laufzeit O(log n) realisiert werden. Bezeichne LessThan
einen rekursiven Algorithmus, welcher die Anzahl der Elemente im Rot-Schwarz-Baum bestimmt, die kleiner k sind.
LessThan(k, x):
1 if x = Nil then return 0
2
else if k < key[x] then return LessThan(k,left(x))
3
else return left-size[x] + 1 + LessThan(k,right(x))
Dann kann Count wie folgt realisiert werden.
Count(a, b):
1 x ← LessThan(a, root[T ])
2 y ← LessThan(b, root[T ])
3 return y − x
Da LessThan den Rot-Schwarz-Baum maximal einmal von der Wurzel bis zu einem Blatt
durchläuft, beträgt dessen Laufzeit O(log n). Count verwendet nun im wesentlichen nur
zwei Aufrufe von LessThan, womit eine Laufzeit von O(log n) für Count folgt.
Blatt 11, Aufgabe 2
Um diese Aufgabe zu lösen, definieren wir zunächst zu jedem gerichteten Graphen G = (V, E)
~ wie folgt. Die Knotenmenge von G
~ ist die Knotenmenge V von G. Weiter ist
den Graphen G
~ von G
~ definiert durch
die Kantenmenge E
~ := {(u, v) : (v, u) ∈ E}.
E
~ aus G, in dem wir die Richtung aller Kanten in G umdrehen. Wir
Wir erhalten also G
~
können auch G effizient aus G berechnen. Hierzu legen wir zunächst für jeden Knoten u ∈
~
V eine neue Adjazenzliste Adj[u]
an. Dann durchlaufen wir die Adjazenzlisten Adj[v] für
~
den Graphen G. Ist nun u ∈ Adj[v], so fügen wir v zur Adjazenzliste Aadj[u]
hinzu. Man
~ in Zeit O(|V | + |E|) aus der
sieht, dass auf diese Weise die Adjazenzlistendarstellung von G
Adjazenzlistendarstellung von G berechnet werden kann.
Als nächstes erinnern wir uns, dass wir mit Hilfe einer Breitensuche in einem (gerichteten)
Graphen G alle von einem Knoten v des Graphen aus erreichbaren Knoten berechnen können.
Insbesondere können wir mit einer Breitensuche feststellen, ob alle Knoten eines Graphen von
einem Knoten v aus erreichbar sind. Die Zeit, die hierfür benötigt wird, ist die Laufzeit für
eine Breitensuche, also O(|V | + |E|), wobei G = (V, E).
Nun können wir den Algorithmus Senkensuche beschreiben, der feststellt, ob ein gerichteter
Graph eine Senke besitzt. Eingabe für den Algorithmus ist ein gerichteter Graph G = (V, E)
in Adjazenzlistendarstellung.
Senkensuche
~
1. Konstruiere aus G den Graphen G.
2. Für alle u ∈ V
~ ob jeder Knoten u ∈ V
a) Entscheide mit einer in v gestarteten Breitensuche in G,
~ von v aus erreichbar ist.
im Graphen G
b) Ist jeder Knoten u von v aus erreichbar, return G besitzt Senke, nämlich v.
3. return G besitzt keine Senke.
Zunächst zeigen wir die Korrektheit des Algorithmus. Hierzu beobachten wir zunächst, dass
~ von v aus erreichbar ist. Dieses
v genau dann eine Senke in G ist, wenn jeder Knoten u in G
~
folgt unmittelbar aus der Definition von G.
Wir müssen daher noch zeigen, dass die Schritte 2 und 3 von Algorithmus Senkensuche
~ einen Knoten v gibt, von dem aus alle Knoten u erreichbar
korrekt entscheiden, ob es in G
sind. Gibt es nun einen solchen Knoten v, so wird diese beim Durchlauf des zweiten Schritts
mit v als Startknoten der Breitensuche festgestellt. Dann ist die Ausgabe in Schritt 2b), dass
~ keinen Knoten, von dem aus alle Knoten erreichbar
G eine Senke besitzt. Besitzt hingegen G
sind, so wird es in Schritt 2b) für keinen Knoten v zu einer Ausgabe kommen. Die Ausgabe
ist dann im 3. Schritt, dass G keine Senke besitzt. Die Ausgabe ist also in jedem Fall korrekt.
Nun zur Laufzeit. Der erste Schritt benötigt, wie oben angemerkt, Zeit O(|V |+|E|). Der dritte
Schritt benötigt konstante Zeit. Nach den Bemerkungen, die wir oben über die Breitensuche
gemacht haben, benötigen wir für jeden Knoten v für die Schritte 2a) und 2b) Zeit O(|V | +
|E|). Damit benötigt der 2. Schritt für alle Knoten zusammen Zeit O(|V |(|V | + |E|)) =
O(|V |2 + |V |E|). Insgesamt also ist die Laufzeit durch O(|V |2 + |V |E|) gegeben. Dies beweist
die Aussage der Aufgabe.
Blatt 12, Aufgabe 2
Wir zeigen die drei Aussagen jeweils durch einen Widerspruchsbeweis.
a) Wir zeigen, dass G einen Kreis besitzt, falls jeder Knoten Ausgangsgrad > 0 oder jeder
Knoten Eingangsgrad > 0 hat. Da die Beweise für Ausgangs- bzw. Eingangsgrad sehr
ähnlich sind, führen wir den Beweis nur für den Ausgangsgrad. Sei hierzu |V | = n und v1
ein beliebiger Knoten des Graphen. Da v1 Ausgangsgrad > 0 hat, gibt es einen Knoten
v2 , so dass (v1 , v2 ) ∈ E. Dann gibt es weiter einen Knoten v3 , so dass (v2 , v3 ) ∈ E.
Dabei muss v3 6= v1 , denn sonst haben wir einen Kreis. Allgemein können wir unter der
Voraussetzung, dass jeder Knoten Ausgangsgrad > 0 besitzt, zu jedem i eine Folge von
Knoten v1 , v2 , v3 , . . . konstruieren, so dass (vi−1 , vi ) ∈ E. Ist aber i > n, so muss es zwei
Indizes k, l, l > k, geben mit vk = vl . Dann aber ist (vk , vk+1 ), (vk+1 , vk+2 ), . . . , (vl−1 , vl )
ein Kreis in G.
b) Wir zeigen die Aussage sogar für jeden Spannbaum, nicht nur minimalen Spannbaum.
Sei also T ein Spannbaum, der die Kante e nicht enthält. Sei e = (u, v). Da T ein
Spannbaum ist, gibt es in T einen Pfad P von u nach v. Der Pfad enthält die Kante
e nicht, da T die Kante e nicht enthält. Dann aber ist P zusammen mit e ein Kreis in
G. Dieses widerspricht der Annahme, dass e auf keinem Kreis in G enthalten ist. Also
kann es den Spannbaum T , der e nicht enthält, nicht geben.
c) Angenommen, es gibt zwei unterschiedliche Spannbäume T1 , T2 mit den Kanten e1 , . . . , en−1
und f1 , . . . , fn−1 . Wir nehmen, dass die Kanten so nummeriert sind, dass w(e1 ) <
w(e2 ) < · · · < w(en−1 ) und analog für die Kanten fi von T2 . Sei nun k der kleinste
Index, für den ek 6= fk gilt. Weiter nehmen wir an, dass w(fk ) > w(ek ). Nach Definition von k kann es keinen Index j < k mit fj = ek geben. Für alle Indizes j > k gilt
w(fj ) > w(fk ) > w(ek ). Also gilt auch für alle Indizes j > k, dass fj 6= ek . Damit kann
ek nicht unter den kanten in T2 auftauchen. Wir betrachten nun den Spannbaum T2
erweitert um die Kante ek . In diesem Teilgraphen von G muss es nun einen Kreis geben.
Da T2 alleine keinen Kreis enthält, muss an diesem Kreis ek beteiligt sind. Da T1 keinen
Kreis enthält, kann der Kreis nicht ausschließlich Kanten aus ej = fj , j = 1, . . . , k − 1
enthalten. Also gibt es in dem Kreis eine Kante fj , j > k. Wir entfernen nun aus T2
diese Kante fj und fügen ek hinzu. Dieses liefert einen neuen Spannbaum der echt kleineres Gewicht als T2 hat, denn w(ek ) < w(fj ) (wegen j > k und w(fk ) > w(ek )). Dieses
ist aber ein Widerspruch zur Annahme, dass T2 ein minimaler Spannbaum ist. Damit
kann es keine zwei unterschiedlichen minimalen Spannbäume geben.
Blatt 13, Aufgabe 1
Sei I = {i1 , . . . , ir } die Indexmenge der angefahrenen Tankstellen bei einer optimalen Lösung.
Sei J = {j1 , . . . , js } die Indexmenge der angefahrenen Tankstellen bei einer gierigen Lösung.
Wir müssen zeigen r = s. Da I zu einer optimalen Lösung gehört, gilt natürlich r ≤ s. Wir
müssen also den Fall s > r ausschließen.
Im Fall I = J, folgt natürlich sofort r = s. Wir können also annehmen, dass I 6= J. Dann sei
k, 1 ≤ k ≤ r, der kleinste Index mit ik 6= jk . Das heisst, beim k-ten Tankstopp weichen die
optimale und die gierige Lösung zum ersten Mal voneinander ab. Aufgrund der Definition der
gierigen Strategie muss dann ik < jk gelten, denn vom k − 1-ten Tankstopp tik−1 = tjk−1 kann
nicht weiter als bis zu tjk gefahren werden. Mit demselben Argument sieht man dann aber,
dass il ≤ jl fü alle l > k gilt. Insbesondere gilt ir ≤ jr . Das aber heisst, dass vom Tankstopp
tjr das Ziel bereits erreicht werden kann. Denn dieses gilt für tir und diese Tankstelle liegt
vor tjr , da ir ≤ jr . Damit kann der Fall s > r nicht auftreten.
Blatt 13, Aufgabe 3
Wir betrachten den Graphen in Abbildung 1.
Abbildung 1: Graph für Aufgabe 13.3
Der Algorithmus Gierige-Überdeckung wird hier als erstes den Knoten c auswählen. Dann
muss er aber noch aus jeder der drei Kanten (a, 1), (b, 2), (d, 3) jeweils einen Knoten auswählen.
Gierige-Überdeckung liefert also eine Überdeckung der Größe 4. Nun ist aber die Knotenmenge {1, 2, 3} bereits eine Überdeckung der Größe 3. Für diesen Fall liefert Gierige-Überdeckung
also keine optimale Lösung.
Blatt 14, Aufgabe 2
1. Wir zeigen die Aussagen durch vollständige Induktion über l.
Der Induktionsbeginn ist für l = 0. Erlauben wir auf einem Pfad von u nach v keine
Kante, so ist nur u selber überhaupt erreichbar. Der einzige Pfad der Länge 0 von u
nach u ist dann der leere Pfad, also der Pfad ohne Kanten. Damit ist für l = 0 die
Aussage bewiesen.
Nun zum Induktionsschluss für l > 0. Nach Induktionsvoraussetzung liefert die Formel
in Teil b) dieser Teilaufgabe für l − 1 und alle w ∈ V die korrekte Anzahl von Pfaden
der Länge l − 1 von u nach w.
Für jeden Knoten v ∈ V können wir nun jeden Pfad der Länge l von u nach v eindeutig
in zwei Teile aufteilen. Zunächst führt ein solcher Pfad auf einem Teilpfad der Länge
genau l − 1 zu einem Knoten w ∈ V , so dass (w, v) ∈ E. Der zweite Teil des Pfades
der Länge l von u nach v besteht dann nur noch aus der Kante (w, v). Aus dieser
Beobachtung und der Induktionsvoraussetzung folgt nun sofort die Gleichung
X
dl (u, v) =
dl−1 (u, w) für l > 0.
{w∈V :(w,v)∈E}
2. Der folgende Algorithmus PathCount berechnet die im ersten Teil der Aufgabe aufgestellten Gleichungen. Dabei benutzt der Algorithmus ein zweidimensionales Array d
mit k + 1 Zeilen und |V | Spalten. Der Eintrag d[l][v], 0 ≤ l ≤ k, v ∈ V, soll dabei am
Ende des Algorithmus die Anzahl der Pfade der Länge l von u nach v speichern. Die
gewünschte Antwort ist dann die k + 1-te Zeile d[k] des Arrays. Initialisiert werden alle
Werte d[l][v] mit 0 (Zeilen 1-3). Dann wird d[0][u] auf 1 gesetzt. Damit sind dann schon
alle Werte für l = 0 korrekt. Diese werden im weiteren Verlauf des Algorithmus auch
nicht mehr geändert. Die Schleifen in den Zeilen 5-8 bestimmen dann iterativ die Werte
d[l][v] für l = 1, . . . , k, mit Hilfe der im ersten Teil aufgestellten und eben bewiesenen
Gleichung.
PathCount(G, u, k):
1
2
3
4
5
6
7
8
9
for l ← 0 to k
do for alle v ∈ V
do d[l][v] ← 0
d[0][u] ← 1
for l ← 1 to k
do for alle w ∈ V
do for alle v ∈ Adj[w]
do d[l][v] ← d[l][v] + d[l − 1][w]
return d[k]
Schließlich noch die Laufzeit des Algorithmus PathCount. Der ersten vier Zeilen
benötigen Zeit O(k|V |), da für jeden Knoten in V genau (k + 1) Werte gesetzt werden.
In den Zeilen 5-8 durchlaufen wir für jeden Wert von l alle Adjazenzlisten genau einmal.
Damit benötigen wir für jeden Wert von l in den Zeilen 6-8 Zeit O(|V | + |E|). Damit ist
die Laufzeit für die Zeilen 5-8 O(k(|V | + |E|)). Dieses ist dann auch die Gesamtlaufzeit
von PathCount.
Herunterladen