Karlsruher Institut für Technologie Institut für Theoretische Informatik Prof. Dr. Peter Sanders Dennis Luxen, Dr. Johannes Singler 1. Übungsblatt zu Algorithmen II im WS 2010/2011 http://algo2.iti.kit.edu/AlgorithmenII.php {luxen,sanders,singler}@kit.edu Musterlösungen Aufgabe 1 (Laufzeitschranken für Prioritätswarteschlangen) a) Beweisen Sie allgemein für adressierbare Prioritätswarteschlangen die untere Laufzeit-Schranke von Ω(log n) für deleteMin, gegeben dass insert konstante Laufzeit hat. b) Warum gilt die Laufzeitschranke nicht, wenn es keine Laufzeitbeschränkung für insert gibt? Musterlösung: a) Mit einer Laufzeit von O(f (n)) für deleteMin ließe sich in Zeit O(nf (n)) + n · O(1) vergleichsbasiert sortieren, indem man zuerst alle Elemente einfügt und dann eines nach dem anderen in aufsteigender Reihenfolge entnimmt. Für deleteMin in sublogarithmischer Zeit wäre das einen Widerspruch zur bekannten unteren Schranke für vergleichsbasiertes Sortieren von Ω(n log n). b) insert könnte nach jedem Aufruf eine aufsteigend sortierte Liste aller Elemente hinterlassen, mit deren Hilfe sich alle folgenden min- und deleteMin-Operationen in konstanter Zeit beantworten ließen. Aufgabe 2 (Entwurfsaufgabe: Auftragsmanagement) Ihr Firma bezieht ein Produkt von einem von mehreren möglichen Zulieferbetrieben. Sie wollen nun die Art und Weise, wie Aufträge vergeben werden, neu gestalten. Die Entscheidung, wie die Aufträge an die Zulieferer verteilt werden, geschieht nach folgender Regel. Derjenige Zulieferer, der bisher im laufenden Jahr das kleinste Auftragsvolumen erhalten hat, bekommt den nächsten Auftrag. a) Entwerfen Sie eine Algorithmus für diese Entscheidung, dessen Laufzeit sublinear ist. Im darauffolgenden Jahr wird die Auftragsvergabe anhand einer neuen Regel entschieden. Der Zulieferer mit der kleinsten Zahl noch nicht fertiggestellter Aufträge erhält den nächsten neuen Auftrag. Sie müssen also festhalten, wie viele noch nicht fertiggestellte Aufträge ein Zulieferer hat. b) Ergänzen Sie Ihre Lösung aus der obigen Teilaufgabe für die Entscheidungsfindung. Unterscheidet sich die Laufzeit asymptotisch von der vorherigen Lösung? Musterlösung: Jeder Zulieferer wird anhand einer ID identifiziert. a) Hauptdatenstruktur ist eine Prioritätswarteschlange. Der Key der Prioritätswarteschlange ist die Zahl der vergebenen Aufträge, also initial 0. Jeder Zulieferer wird eingefügt und der nächste Zulieferer mit deleteMin herausgefunden. Der neu beauftragte Zulieferer wird mit aktualisiertem Auftragsvolumen per insert danach wieder eingefügt. Mit einem Binären Heap lässt sich dies in Zeit O(log n) pro Auftrag erledigen. 1 b) Hauptdatenstruktur ist eine adressierbare Prioritätswarteschlange. Anstatt des Gesamtauftragsvolumen entspricht der Key in der Prioritätswarteschlange jetzt dem noch nicht abgearbeiteten Auftragsvolumen. Die Adressen für jeden Zulieferer müssen nach jedem insert natürlich gespeichert werden, um später drauf zu greifen zu können. Zuweisung von Aufträgen erfolgt wieder über deleteMin und insert, wie schon in Teilaufgabe a). Wird ein Teil eines Auftrags oder ein ganzer Auftrag fertig gemeldet, so wird per decreaseKey die Priorität des Zulieferers angepasst. Die Laufzeit ist identisch zur Teilaufgabe a), da deleteMin die Laufzeit dominiert. Aufgabe 3 (Spezialfälle von Prioritätswarteschlangen) Sie erarbeiten in dieser Aufgabe, dass die Datenstrukturen Stack und Queue Spezialfälle einer Prioritätswarteschlange sind. Machen Sie sich gegebenenfalls erneut mit den grundlegenden Operationen dieser beiden Datenstrukturen vertraut. a) Welchen Schlüssel muss man bei einem push auf einem Stack einfügen, damit die Prioritätswarteschlange zum Stack wird? b) Welchen Schlüssel muss man bei einem push auf einer Queue einfügen, damit die Prioritätswarteschlange zur Queue wird? c) Angenommen, Stack und Queue halten nie mehr als O(n) Elemente. Kommt man mit O(n) vielen verschiedenen Schlüsseln aus? Musterlösung: Als Keys verwenden wir natürliche Zahlen. a) Sei j der bisher kleinste Key in der Prioritätswarteschlange und k der aktuell einzufügende. Dann muss immer gelten k < j. Als erster Schlüssel wird 0 gewählt, danach immer k := j − 1. b) Als erster Schlüssel wird s := 0 gewählt, und s wird bei jedem push inkrementiert. c) Im Fall des Stacks reichen O(n) viele Schlüssel, denn der zuletzt entnommene Schlüssel kann jeweils neu benutzt werden (impliziert durch k := j − 1). Für den Fall der Queue gilt das nicht. Die Elemente werden in FIFO-Reihenfolge eingefügt und entnommen. Das heißt, dass die Differenz zwischen erstem und letztem Key immer mindestens Anzahl Elemente in der Queue minus 1 ist. Daraus folgt, dass die Schlüsselfolge monoton wachsend ist, sofern die Queue nie leer wird. Nur in letzterem Fall kann s zurückgesetzt werden. Im schlimmsten Fall benötigt man also O(i) Schlüssel, wobei i die Anzahl inserts ist. Aufgabe 4 (Asymptotische Laufzeit-Unterschiede zwischen Fibonacci-Heaps und Suchbäumen) a) Bei welchen Operationen hat ein Fibonacci-Heap asymptotische Laufzeit-Vorteile gegenüber Suchbäumen? b) Was ist der Unterschied zwischen Concatenation beim Suchbäumen und Merge bei FibonacciHeaps (Funktionalität und Laufzeit)? Musterlösung: a) Bei decreaseKey und insert benötigt ein Fibonacci-Heap nur O(1) statt O(log n) Zeit. Mergen von zwei Fibonacci-Heaps benötigt nur konstante Zeit, während dies bei Suchbäumen im Allgemeinen lineare Zeit braucht (auslesen der sortierten Listen, mischen, Aufbau eines neuen Baums aus der sortierten Liste). 2 left sibling or parent B1 data B2 B3 right sibling one child Abbildung 1: Ein Heap-Item für eine Drei-Zeiger-Implementierung eines Pairing Heaps (links) sowie die Bäume B1 , B2 und B3 . b) Concatenation bei (a, b)-Bäumen vereinigt wie Mergen zwei Suchbäume, allerdings ist Voraussetzung, dass die Wertebereiche der beiden Suchbäume disjunkt sind, d. h. das kleinste Element des einen größer gleich dem größten des anderen ist, oder umgekehrt. Dafür läuft das dann auch in logarithmischer Zeit in der Größe des größeren der beiden Suchbäume. Aufgabe 5 (Implementierung von Pairing Heaps) Betrachten Sie eine Implementierung von Pairing Heaps, die — wie in der Vorlesung vorgestellt — mit Hilfe von drei Pointern pro Heap-Item realisiert wurde. Abbildung 1 zeigt die schematische Darstellung eines einzelnen Heap-Items für diese Implementierung. Man darf aber nicht vergessen, dass auch die Menge der Wurzelknoten ( Root-Set“) irgendwie dargestellt werden muss. Wie in der Vorlesung ” angegeben, soll dies in Form einer doppelt verketteten Liste geschehen. a) Wie stellt die Datenstruktur einen Baum der Form B3 (siehe Abbildung 1) im Detail dar? Zeichnen Sie diesen Fall. Null-Zeiger sollen durch leere Kästchen symbolisiert werden. Das RootSet soll uns in dieser Teilaufgabe aber noch nicht interessieren, ignorieren Sie es also. b) Wie kann das Root-Set als doppelt verkettete Liste im Detail realisiert werden? Verwenden Sie nur die Heap-Items und keine zusätzlichen Datentypen. Wie stellt Ihre Realisierung einen Pairing Heap, der zwei Bäume der Form B1 bzw. B2 enthält, im Detail dar? Zeichnen Sie, diesmal mit Darstellung des Root-Set. c) Geben Sie den Pseudo-Code für die Operationen cut(h : Handle) und decreaseKey(h : Handle, k : Key) an. Spendieren Sie dazu den Heap-Items aber eine Markierung, um deren Mitgliedschaft im Root-Set anzuzeigen. d) Angenommen, Sie statten die Heap-Items nicht mit einer Markierung für die Wurzeleigenschaft aus. Wie wirkt sich das auf den Pseudo-Code von decreaseKey bzw. cut aus? Musterlösung: a) Der Einfachheit halber lassen wir in der folgenden Zeichnung das Daten-Feld der Heap-Items weg. Das Root-Set war ja nach Aufgabenstellung in dieser Teilaufgabe ebenfalls nicht zu betrachten. 3 b) Nun wird auch das Root-Set dargestellt, und zwar als doppelt verkettete Root-Liste“. Um auch ” den leeren Pairing Heap darstellen zu können (der ja nur aus einem leeren Root-Set besteht), muss es möglich sein, die leere Root-Liste darzustellen. Zu diesem Zweck gibt es in der RootListe ein speziell ausgezeichnetes Dummy-Item (grau). Wäre die Liste leer, würde sie nur aus dem Dummy-Item bestehen. Root−Liste B1 B2 c) Zunächst geben wir eine detaillierte Spezifikation des Datentyps für die Heap-Items an. Dabei spendieren wir den Heap-Items wie in der Aufgabenstellung gefordert eine Markierung für die Wurzeleigenschaft: 1: 2: 3: 4: 5: 6: 7: class Heap Item key : Key parent or left : Handle right : Handle child : Handle is root : Boolean end class Das Feld parent or left zeigt entweder auf auf den linken Nachbar oder — wenn es keinen gibt — auf das Elternelement. Nun können wir den Pseudo-Code für die Operation cut angeben. Letztlich handelt es sich dabei nur um eine Abfolge von Zeiger-Umbiegereien“. Dabei bezeichne ” root list das speziell ausgezeichnete Dummy-Item, das die Root-Liste repräsentiert. Zunächst entfernen wir h (und damit auch seinen Unterbaum) aus dem Unterbaum des Elternelementes von h (Zeilen 6 bis 15). Dann hängen wir h (und damit auch seinen Unterbaum) in 4 die Root-Liste ein (Zeilen 17 bis 22). Dabei sehen wir, dass cut einer Splice-Operation ähnelt (siehe doppelt verkettete Listen). 1: procedure cut(h : Pointer toItem) 2: 3: 4: // Wenn h schon Wurzel ist, macht die Anwendung von cut keinen Sinn assert not h.is root 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: // Entferne h samt Unterbaum aus dem Unterbaum des Elternelementes von h if h.parent or left.child = h then // erstes Kind des Elternknotens h.parent or left.child := h.right h.right.parent or left := h.parent else // weiteres Kind des Elternknotens h.parent or left.right := h.right h.right.parent or left := h.parent or left end if 16: 17: 18: 19: 20: 21: 22: // Füge h samt Unterbaum an das Ende der Root-Liste an last := root list.parent or left //lokale Variable root list.parent or left := h h.right := root list h.parent or left := last last.right := h 23: // h ist jetzt eine Wurzel h.is root := true 26: return 24: 25: Unter Nutzung des Attributes is root des Datentyps Heap Item können wir auch den PseudoCode für decreaseKey formulieren: procedure decreaseKey(h : Handle, k : Key) assert k ≤ h.key 3: h.key := k 4: if not h.is root then cut(h) 5: return 1: 2: Natürlich kann man (so wie in der Vorlesung) die Zeilen 17 bis 25 von cut durch einen Aufruf an newTree ersetzen. d) Es gibt nun außer dem Ablaufen der Root-Liste keine Möglichkeit mehr die Wurzeleigenschaft festzustellen. In cut muss nun Zeile 4 (das ist die assert-Anweisung) ersetzt werden durch assert h bezeichnet keine Wurzel da das Attribut is root ja nicht mehr existiert. Für die Zeile 4 von decreaseKey gibt es u.a. diese beiden Möglichkeiten: 1. Ersetze if not h.is root then cut(h) durch ein bloßes cut(h). 2. Statt das Attribut is root abzufragen, überprüfe die Wurzeleigenschaft durch Ablaufen der Root-Liste. 5 In beiden Fällen bleibt das äußere Verhalten des Pairing-Heap korrekt — wenn man das Zeitverhalten außer acht lässt. Im ersten Fall wird bei jedem Aufruf von decreaseKey(h, k) das Item von h samt seinem Unterbaum an das Ende der Liste verschoben — auch dann, wenn es sich bereits um ein Wurzelelement handelt. Wie sich dies aber auf das (amortisierte) Laufzeitverhalten des Pairing-Heap auswirkt, ist fraglich. Im zweiten Fall wird bei jedem Aufruf von decreaseKey(h, k) die Root-Liste abgelaufen. Dies dauert natürlich seine Zeit, was das (amortisierte) Zeitverhalten ebenfalls verändern könnte. An dieser Stelle muss aber darauf hingewiesen werden, dass das amortisierte Laufzeitverhalten von Pairing-Heaps sowieso noch nicht völlig verstanden ist. Aufgabe 6 (Rechenaufgabe Fibonacci-Heaps) Gegeben sei ein leerer Fibonacci-Heap M . Nun werde folgende Operationsfolge ausgeführt (M.insert(n) liefere dabei einen Zeiger auf ein neu eingefügtes Element mit Schlüssel n ∈ N zurück): M.insert(3); M.insert(8); M.insert(2); x := M.insert(5); M.insert(10); M.deleteMin(); M.insert(6); M.insert(7); M.decreaseKey(x, 2); M.deleteMin() a) Welche Zustände durchläuft der Fibonacci-Heap im Verlauf der Ausführung? Zeichnen Sie abstrakt (d. h. die einzelnen Zeiger müssen nicht gezeichnet werden). b) Nehmen Sie nun an, dass der Fibonacci-Heap mit Hilfe von vier Pointern pro Heap-Item realisiert wird (vgl. Buch von Mehlhorn und Sanders, Abbildung 6.8). Zeichnen Sie den Zustand nach Ausführung der letzten Operation detailliert. Musterlösung: a) Nach ausführen der ersten 5 Operationen hat der Fibonacci-Heap folgenden Zustand (die gestrichelte Linie symbolisiert das Root-Set): min 3 8 2 5 10 Durch das anschließende deleteMin() wird dann die 2 entfernt, was eine Konsolidierung“ der ” Datenstruktur nach sich zieht. Dabei wird das folgende Verfahren angewendet: Solange im RootSet noch Knoten gleichen Ranges vorkommen werden diese verbunden. Der Rang eines Knotens ist dabei die Anzahl seiner Kinder. Das verbinden zweier Wurzelknoten bedeutet, dass der eine als Kind an den anderen angehängt wird (der Rang des Knotens, der im Root-Set verbleibt steigt dabei um 1). Beim verbinden zweier Knoten ist jedoch zu beachten, dass die Heap-Bedingung erhalten werden muss (diese Besagt, dass ein Elternknoten nie einen größeren Schlüssel enthalten darf als einer seiner Kindknoten). Welcher Knoten an welchen angehängt wird, hängt also davon ab, welcher Knoten den kleineren Schlüssel hat (bei gleichen Schlüsseln kann frei entschieden werden). Das Verfahren terminiert erst, wenn keine Knoten gleichen Ranges mehr vorhanden sind. Wir führen nun die Konsolidierung für unser Beispiel durch: 6 min 3 8 5 10 3 5 8 10 3 8 5 10 Als nächstes werden zwei weitere Elemente eingefügt (mit Schlüsseln 6 und 7), und dann der Schlüssel 5 des Elements x auf 2 gesenkt (decreaseKey(x, 2)). Normalerweise hätten wir keinen Zugang zu dem entsprechenden Element, da das Interface einer Priority Queue keinen wahlfreien Zugriff bietet. Allerdings hatten wir uns ja einen Zeiger x auf dieses Element gemerkt. Die nächsten Zustände sind also die folgenden: min min 3 8 6 3 7 6 7 2 8 5 10 10 Nun wird noch einmal deleteMin() ausgeführt, was das Element mit der 2 entfernt und zu einer erneuten Konsolidierung der Datenstruktur führt: min 3 8 6 7 10 3 6 8 7 10 3 8 10 6 7 Bemerkung: Die Technik der kaskadierenden Schnitte muss in diesem Beispiel nicht angewendet werden, da der einzige Aufruf von decreaseKey nur ein Kind einer Wurzel betrifft. Wurzeln werden bei den kaskadierenden Schnitten aber nicht markiert. b) Wie in der Lösung zu Aufgabe 5(b) wird das Root-Set wieder als doppelt verkettete, zyklische List mit Dummy-Item (grau) realisiert. 7 3 10 6 8 7 Achtung: In der Übung haben wir den Eltern-Zeiger des Knotens, der den Schlüssel 6 hat, leider vergessen. Aber in einem Fibonacci-Heap mit 4-Pointer-Realisierung soll jeder Knoten auf seinen Elternknoten zeigen. Aufgabe 7 (Rechenaufgabe Fibonacci-Heaps) Abbildung 2 zeigt den inneren Zustand eines Fibonacci-Heap. Das angekreuzte Heap-Item sei dabei besonders markiert (wie es im Rahmen der Cascading-Cuts-Technik üblich ist). a) Gegeben sei ein Fibonacci-Heap mit einem inneren Zustand wie in Abbildung 2. Wie in der Abbildung dargestellt seien außerdem drei Zeiger x, y und z auf Heap-Items gegeben. Nun werde folgende Operationenfolge ausgeführt: decreaseKey(x, 2); decreaseKey(y, 7); decreaseKey(z, 4); deleteMin() Welche inneren Zustände durchläuft der Fibonacci-Heap? Zeichnen Sie abstrakt. b) Sind folgende Aussagen richtig oder falsch? Begründen Sie jeweils kurz. 1. In einem Pairing-Heap ist jeder Baum von der Form Bi für ein jeweils geeignetes i ∈ N0 . 2. Die Cascading-Cuts-Technik sorgt dafür, dass die Anzahl der Wurzelknoten in einem Fibonacci-Heap höchstens logarithmisch in n ist (n sei die Anzahl der Elemente im Heap). 3. Für Pairing-Heaps hat die Operation decreaseKey einen amortisierten Zeitaufwand von O(log n), wobei n die Anzahl der Elemente im Heap sein soll. Musterlösung: a) Als erstes wird die Operation decreaseKey(x, 2) ausgeführt. Dies hat zur Folge, dass die 12 zur 2 wird und das durch x bezeichnete Item zu einer Wurzel wird. Letzteres bedeutet aber, dass ein cut ausgeführt wird. Gemäß der Cascading-Cuts-Strategie muss also das Elternitem von ∗x (im Beispiel hat es den Schlüssel 9) markiert werden, falls es noch nicht markiert ist. Natürlich muss 8 min 3 6 11 z x 9 12 14 17 y Abbildung 2: Innerer Zustand eines Fibonacci-Heaps. auch der Zeiger auf das Item mit dem minimalen Schlüssel neu gesetzt werden. Entsprechend sieht der neue innere Zustand des Fibonacci-Heaps folgendermaßen aus (die kurzen doppelten Linien bezeichnen die Stelle, wo als nächstes abgeschnitten werden muss): min min 3 3 6 6 11 z x 11 9 12 9 14 14 17 2 y 17 Nun wird die Operation decreaseKey(y, 7) ausgeführt. Das Item ∗y ist nach wie vor das, welches den Schlüssel 17 trägt. Wie schon bei der vorigen Operation muss das Item abgeschnitten und zur Wurzel gemacht werden. Wiederum muss auch das zugehörige Elternitem (hat den Schlüssel 14) markiert werden, diesmal ist das Elternitem aber schon markiert. In solchen Fällen schreibt die Cascading-Cuts-Strategie vor, dass das Elternitem ebenfalls abgeschnitten werden muss und wiederum dessen Elternitem markiert werden soll, usw... Dieser Vorgang darf erst abgebrochen werden, wenn ein nicht markiertes Elternitem erreicht wird (dabei beachte man das Wurzeln nie markiert sind). Übrigens: Wenn ein markiertes Element abgeschnitten und somit zur Wurzel gemacht wird, dann wird eine etwaige Markierung entfernt. Für unser Beispiel sehen die durchlaufenen inneren Zustände wie folgt aus (die doppelten Linien bezeichnen wieder, wo als nächstes abgeschnitten werden muss): min 3 2 min 7 3 6 11 2 6 9 11 14 9 9 7 14 min 3 2 14 7 9 6 11 Als nächstes wird die Operation decreaseKey(z, 4) ausgeführt. Das Item ∗z ist immer noch das, welches den Schlüssel 11 trägt. Sein Elternitem ist markiert, so dass es ebenfalls abgeschnitten werden muss: min 3 7 14 9 4 7 14 9 4 2 6 min 3 2 6 Als letztes wird schließlich die Operation deleteMin() ausgeführt, was zu einer Konsolidierung des Fibonacci-Heap führt. Da das entsprechende Verfahren auf dem vorherigen Übungsblatt ja ausführlich behandelt worden ist, wird hier nur noch der Zustand nach Ende der Konsolidierung dargestellt: min 3 7 4 9 6 14 b) Wir betrachten die Aussagen 1 bis 3 aus der Aufgabenstellung der Reihe nach: Aussage 1: Falsch. Der B2 , wie direkt unten abgebildet, bezeichnet eine mögliche Form für einen Baum in einem Pairing-Heap (überlegen!): 10 min 3 7 9 14 x Nun werde decreaseKey(x, 4) ausgeführt. Dies hat zur Folge, dass das Item ∗x abgeschnitten wird und ein Baum entsteht, dessen Form sich nicht mehr durch Bi darstellen lässt (für ein i ∈ N0 ). Aussage 2: Falsch. Die Cascading-Cuts-Technik sorgt dafür, dass der Rang aller Knoten (d. h. Heap-Items) im Fibonacci-Heaps höchstens logarithmisch in n ist. Der Rang eines Heap-Items ist dabei die Anzahl der Kinder dieses Items. Aussage 3: Richtig. Der amortisierte Zeitaufwand von decreaseKey ist für Pairing-Heaps zwar noch nicht völlig verstanden, auf jeden Fall liegt er aber in O(log n). Möglicherweise ist er aber sogar besser. Aufgabe 8 (Starke Zusammenhangskomponenten) Gegeben sei ein gerichteter Graph G = (V, E). Wie schon in der Vorlesung definieren wir ∗ v ←→ w es gibt Pfade hv, . . . , wi und hw, . . . , vi in G :⇐⇒ ∗ für v, w ∈ V . Zeigen Sie nun, dass ←→ eine Äquivalenzrelation ist. Musterlösung: Um nachzuweisen, dass es sich um eine Äquivalenzrelation handelt, müssen Reflexivität, Symmetrie und Transitivität der Relation gezeigt werden. Reflexivität: Alle Pfade hvi mit v ∈ V existieren per Definition. ∗ Symmetrie: Es seien v, w ∈ V mit v ←→ w. Dann gibt es Pfade hv, . . . , wi und hw, . . . , vi in G. Damit ∗ gilt aber auch w ←→ v. ∗ ∗ Transitivität: Es seien u, v, w ∈ V mit u ←→ v und v ←→ w. Dann gibt es also Pfade hu, . . . , vi und hv, . . . , wi in G und somit auch einen Pfad hu, . . . , v, . . . , wi. Analog zeigt man, dass es einen Pfad ∗ ∗ hw, . . . , ui in G gibt. Damit folgt u ←→ w. Also ist ←→ transitiv. 11