ETH Zürich Institut für Theoretische Informatik Prof. Dr. Angelika Steger, Prof. Dr. Thomas Holenstein Ralph Keusch HS 2015 Algorithmen und Komplexität Lösungsvorschlag zu Übungsblatt 8 Lösungsvorschlag zu Aufgabe 1 Wir führen dieses Problem auf das Problem der L ÄNGSTEN G EMEINSAMEN T EILSEQUENZ zurück. Die Sequenz x speichert die n gegebenen Zahlen in der ursprünglichen Reihenfolge und die Sequenz y speichert die selben Zahlen in aufsteigender Reihenfolge. Wir bestimmen eine längste gemeinsame Teilsequenz z von x und y; beispielsweise mit dem Verfahren von Aufgabe 2 aus Serie 7. Offensichtlich ist diese Sequenz eine längste monoton steigende Teilsequenz von x. Wir benötigen Zeit O(n log n), um die n Zahlen zu sortieren, und Laufzeit O n2 , um z zu bestimmen (mit dem Algorithmus aus Aufgabe 2 der Serie 7). Insgesamt ergibt sich eine Laufzeit von O n2 . Lösungsvorschlag zu Aufgabe 2 Wir entwerfen einen Algorithmus nach dem Prinzip der Dynamischen Programmierung. Wir speichern die Werte unserer sechs Geldscheine 1, 6, 8, 15, 23, 25 aufsteigend in einem Array A. Geldschein Nummer 1 hat also den Wert A[1] = 1 und Geldschein Nummer 6 hat den Wert A[6] = 25. Sei nun für 1 ≤ i ≤ n und 1 ≤ k ≤ 6 f (i, k ) die minimale Anzahl Geldscheinen in welcher der Betrag i dargestellt werden kann, falls nur Geldscheine der ersten k Typen verwendet werden dürfen. Es folgt daraus, dass f (i, 1) = i für alle 1 ≤ i ≤ n (1) weil genau i Geldscheine mit Wert 1 verwendet werden müssen um den Betrag i darstellen zu können. Weiter gilt f (0, k) = 0 für alle 1 ≤ k ≤ 6 (2) weil kein Geldschein benötilgt wird um 0 darzustellen. Für 1 < k ≤ 6 und A[k] ≥ i ≥ n gilt f (i, k) = min { f (i − A[k], k) + 1, f (i, k − 1)} (3) weil der kte Geldschein entweder mindestens einmal oder nie benutzt werden kann und für 1 < k ≤ 6 und 0 < i < A[k ] gilt f (i, k ) = f (i, k − 1) (4) weil der kte Geldschein in diesem Fall nicht verwendet werden kann. Nach dem Prinzip der Dynamischen Programmierung bauen wir uns eine Tabelle f (i, k) bottom up auf und finden Die Antwort zu unserem Problem in der Zelle f (n, 6). Beweis der Korrektheit: Es reicht zu zeigen, dass die Rekursion (3), (4) und die Verankerungen (1), (2) korrekt sind und nie auf eine noch leere Zelle der Tabelle zugegriffen wird. Falls i = 0, dann werden offensichtlich 0 Geldschein benötigt um den Betrag i darzustellen. Falls i > 0 und k = 1 dann werden genau i Geldscheine mit Wert 1 benötigt. Falls i > A[k] und k > 1 so kann man entweder mindestens einen Geldschein vom Typen k verwenden und muss folglich noch den Betrag i − A[k] konstruieren, benötigt also f (i − A[k], k) + 1 Geldscheine oder man verwendet den kten Geldschein nicht und muss den Betrag i mit den verbleibenden k − 1 Typen zusammenstellen. Dazu benötigt man f (i, k − 1) Geldscheine. Falls i < A[k] kann der kte Geldschein nicht 1 Algorithm 1 MinGeldscheine(n) for k=1. . . 6 do f (0, k ) := 0 end for for i = 1 . . . n do f (i, 1) := i for k=2. . . 6 do if i ≥ A[k ] then f (i, k) := min { f (i − A[k ], k) + 1, f (i, k − 1)} else f (i, k) := f (i, k − 1) end if end for end for return f (n, 6) verwendet werden. Folglich benötigt man genau f (i, k − 1) Geldscheine. Damit haben wir (1)-(4) bewiesen. Es ist einfach zu sehen, dass in allen drei rekursiven Aufrufen wenn f (i, k) gesetzt wird nur auf Zellen f (i0 , k0 ) mit 0 ≤ i0 ≤ i und 1 ≤ k0 ≤ k zugegriffen wird. Diese Zellen sind per Definition des Algorithmus bereits ausgefüllt. Beweis der Laufzeit: Die erste Schleife wird nur 6 mal ausgeführt. Die zweite wird n mal ausgeführt wobei in jeder Iteration die Innere Schleife 5 mal ausgeführt wird. Daraus gibt sich eine Gesamtlaufzeit von 6 + n · 5 · O(1) = O(n). Sobald die Tabelle f (i, k) erstellt wurde kann mittels Backtracking eine optimale Lösung gefunden werden. Wir starten dafür mit f (n, 6). Immer wenn wir in einer Zelle f (i, k ) ankommen, gehen wir wie folgt vor. Falls 1 < k ≤ 6 und A[k] ≤ i ≤ n dann prüfen wir ob f (i, k ) = f (i − A[k], k) + 1. Falls dies der Falle ist geben wir einen Geldschein vom Typ k aus und fahren in der Zelle f (i − A[k], k) weiter. Falls dies nicht der Fall ist fahren wir ohne etwas auszugeben in der Zelle f (i, k − 1) weiter. Falls 1 < k ≤ 6 und i < A[k] fahren wir direkt ohne etwas auszugeben in Zelle f (i, k − 1) weiter. Wenn k = 1 stoppen wir und geben i mal den Geldschein 1 aus und falls i = 0 ist stoppen wir direkt. Lösungsvorschlag zu Aufgabe 3 Wir bestimmen zuerst die Höhen h T und h T 0 der beiden ( a, b)-Bäume. Da die Bäume höhenbalanciert sind, müssen wir jeweils nur von der Wurzel bis zu einem beliebigen Blatt laufen, und finden die Höhen der Bäume in O(log m + log n). Wir nehmen erst an, dass h T 0 ≤ h T . Wir laufen im Baum T solange von der Wurzel zum rechtesten Kind bis wir einen inneren Knoten mit Tiefe h T − h T 0 erreicht haben. Sei v dieser Knoten. Nun hängen wir T 0 an dieser Stelle ein. Das funktioniert, da alle Elemente in T 0 grösser sind als das grösste Element in T. Wir verschmelzen also v mit der Wurzel von T 0 . Hierbei müssen wir die Schlüsselmenge S von v durch S ∪ { g} ∪ K 0 ersetzen, wobei g das maximale Element in T bezeichnet (wir finden dieses in Laufzeit O(log m), indem wir einfach dem rechtesten Ast in T folgen) und K 0 die Schlüsselmenge in der Wurzel von T 0 ist. Nun kann es natürlich passieren, dass hierdurch die ( a, b)-Bedingung verletzt wird. Wir müssen dann eventuell noch Rebalancierungen vornehmen, was wir mit dem gleichen Verfahren wie bei Insert( x, T ) tun können, siehe Skript. Die Höhe des Baumes nimmt dabei maximal um 1 zu. Der Aufwand für die Rebalancierung ist O(log m) (siehe Skript). Für h T 0 > h T geht man analog vor. Man geht in T 0 nach links, bis man Tiefe h T 0 − h T erreicht hat, 2 und hängt dort T ein. Wieder passt man die Schlüsselmengen an und muss eventuell Rebalancierungen vornehmen, was maximal Zeit O(log n) benötigt. Für den gesamten Algorithmus ergibt sich also eine Laufzeit von O(log m + log n). 3