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.